Commit aefe108b by lijiabin

【需求 17679】wip: 继续加页面,首页、详情页等

parent 5ba0ac6a
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
<div class="flex gap-3"> <div class="flex gap-3">
<div class="left flex-1 basis-full xl:basis-3/4 transition-all duration-500"> <div class="left flex-1 basis-full xl:basis-3/4 transition-all duration-500">
<div class="tabs-container h-70px flex relative md:h-60px rounded-lg mb-3"> <div ref="tabsRef" class="tabs-container h-70px flex relative md:h-60px rounded-lg mb-3">
<div <div
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.path" :key="tab.path"
...@@ -199,7 +199,16 @@ ...@@ -199,7 +199,16 @@
<div class="w-1 h-4 bg-gradient-to-b from-pink-500 to-rose-500 rounded-full"></div> <div class="w-1 h-4 bg-gradient-to-b from-pink-500 to-rose-500 rounded-full"></div>
<h1 class="text-sm sm:text-base font-bold">任务中心</h1> <h1 class="text-sm sm:text-base font-bold">任务中心</h1>
</div> </div>
<h2 class="text-xs sm:text-sm mb-1 text-gray-600 ml-3">常规任务 | 特殊任务</h2> <h2 class="text-xs sm:text-sm mb-1 text-gray-600 ml-3">
<span
v-for="item in taskTypeList"
:key="item.value"
class="text-#333 cursor-pointer after:content-['|'] after:mx-2 after:text-#999 last:after:content-none"
:class="{ 'text-#999': currentTask !== item.value }"
@click="currentTask = item.value"
>{{ item.label }}
</span>
</h2>
</div> </div>
</div> </div>
...@@ -239,7 +248,6 @@ ...@@ -239,7 +248,6 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import front from '@/assets/img/culture/front_page.png' import front from '@/assets/img/culture/front_page.png'
import ya from '@/assets/img/culture/ya_culture.png' import ya from '@/assets/img/culture/ya_culture.png'
...@@ -249,11 +257,13 @@ import { getTaskList, dailySign, getCarouselList } from '@/api' ...@@ -249,11 +257,13 @@ import { getTaskList, dailySign, getCarouselList } from '@/api'
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import { TaskTypeEnum } from '@/constants' import { TaskTypeEnum } from '@/constants'
import type { CarouselItemDto } from '@/api' import type { CarouselItemDto } from '@/api'
import { TABS_REF_KEY } from '@/constants'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const carouselList = ref<CarouselItemDto[]>([]) const carouselList = ref<CarouselItemDto[]>([])
const tabsRef = useTemplateRef('tabsRef')
const tabs = [ const tabs = [
{ name: '首页', path: 'deshboard', img: front, svg: 'culture-home' }, { name: '首页', path: 'deshboard', img: front, svg: 'culture-home' },
...@@ -265,6 +275,19 @@ const activeTab = ref( ...@@ -265,6 +275,19 @@ const activeTab = ref(
tabs.find((item) => item.path === route.path.split('/').at(-1))?.name || '首页', tabs.find((item) => item.path === route.path.split('/').at(-1))?.name || '首页',
) )
const taskTypeList = [
{
label: '常规任务',
value: TaskTypeEnum.REGULAR_TASK,
},
{
label: '特殊任务',
value: TaskTypeEnum.SPECIAL_TASK,
},
]
const currentTask = ref(TaskTypeEnum.REGULAR_TASK)
const toggleTab = (tab: { name: string; path: string }) => { const toggleTab = (tab: { name: string; path: string }) => {
activeTab.value = tab.name activeTab.value = tab.name
router.push(`/mainContainer/${tab.path}`) router.push(`/mainContainer/${tab.path}`)
...@@ -292,6 +315,8 @@ onMounted(async () => { ...@@ -292,6 +315,8 @@ onMounted(async () => {
const { data } = await getCarouselList() const { data } = await getCarouselList()
carouselList.value = data carouselList.value = data
}) })
provide(TABS_REF_KEY, tabsRef)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.main-container { .main-container {
......
...@@ -3,7 +3,7 @@ import UploadFile from '@/components/common/UploadFile/index.vue' ...@@ -3,7 +3,7 @@ import UploadFile from '@/components/common/UploadFile/index.vue'
import { useResetData } from '@/hooks' import { useResetData } from '@/hooks'
export default defineComponent((_, { expose }) => { export default defineComponent((_, { expose }) => {
const [form] = useResetData({ const [form, resetForm] = useResetData({
title: '', title: '',
content: '', content: '',
faceUrl: '', faceUrl: '',
...@@ -28,16 +28,24 @@ export default defineComponent((_, { expose }) => { ...@@ -28,16 +28,24 @@ export default defineComponent((_, { expose }) => {
return null return null
} }
} }
const resetFields = () => {
formRef.value?.resetFields()
resetForm()
}
expose({ expose({
validate, validate,
resetFields,
}) })
return () => ( return () => (
<div> <div>
<el-form <el-form
ref={formRef} ref={formRef}
model={form.value} model={form.value}
label-width="100px" label-width="auto"
label-position="top" label-position="right"
size="small"
rules={rules} rules={rules}
> >
<el-form-item label="标题" prop="title"> <el-form-item label="标题" prop="title">
......
import { ArticleTypeEnum, ReleaseStatusEnum } from '@/constants' import { ArticleTypeEnum, ReleaseStatusEnum, SendTypeEnum } from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue' import UploadFile from '@/components/common/UploadFile/index.vue'
import SelectTags from '@/components/common/SelectTags/index.vue' import SelectTags from '@/components/common/SelectTags/index.vue'
...@@ -6,7 +6,7 @@ import SelectTags from '@/components/common/SelectTags/index.vue' ...@@ -6,7 +6,7 @@ import SelectTags from '@/components/common/SelectTags/index.vue'
import { useResetData } from '@/hooks' import { useResetData } from '@/hooks'
export default defineComponent((_, { expose }) => { export default defineComponent((_, { expose }) => {
const [form] = useResetData({ const [form, resetForm] = useResetData({
title: '', title: '',
content: '', content: '',
faceUrl: '', faceUrl: '',
...@@ -14,6 +14,8 @@ export default defineComponent((_, { expose }) => { ...@@ -14,6 +14,8 @@ export default defineComponent((_, { expose }) => {
type: ArticleTypeEnum.PRACTICE, type: ArticleTypeEnum.PRACTICE,
mainTagId: '', mainTagId: '',
tagList: [], tagList: [],
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
}) })
const formRef = ref<InstanceType<typeof ElForm>>() const formRef = ref<InstanceType<typeof ElForm>>()
const rules = { const rules = {
...@@ -22,6 +24,8 @@ export default defineComponent((_, { expose }) => { ...@@ -22,6 +24,8 @@ export default defineComponent((_, { expose }) => {
faceUrl: [{ required: true, message: '请上传贴图', trigger: 'change' }], faceUrl: [{ required: true, message: '请上传贴图', trigger: 'change' }],
releaseStatus: [{ required: true, message: '请选择发布时间', trigger: 'blur' }], releaseStatus: [{ required: true, message: '请选择发布时间', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }], mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [{ required: true, message: '请选择发布时间', trigger: 'blur' }],
} }
const transformForm = () => { const transformForm = () => {
...@@ -53,16 +57,23 @@ export default defineComponent((_, { expose }) => { ...@@ -53,16 +57,23 @@ export default defineComponent((_, { expose }) => {
return allTags.filter((tag) => tag.value !== Number(form.value.mainTagId)) return allTags.filter((tag) => tag.value !== Number(form.value.mainTagId))
} }
const resetFields = () => {
formRef.value?.resetFields()
resetForm()
}
expose({ expose({
validate, validate,
resetFields,
}) })
return () => ( return () => (
<div> <div>
<el-form <el-form
size="small"
ref={formRef} ref={formRef}
model={form.value} model={form.value}
label-width="100px" label-width="auto"
label-position="top" label-position="right"
rules={rules} rules={rules}
> >
<el-form-item label="标题" prop="title"> <el-form-item label="标题" prop="title">
...@@ -89,27 +100,63 @@ export default defineComponent((_, { expose }) => { ...@@ -89,27 +100,63 @@ export default defineComponent((_, { expose }) => {
<UploadFile v-model={form.value.faceUrl} /> <UploadFile v-model={form.value.faceUrl} />
</el-form-item> </el-form-item>
<el-form-item label="主标签" prop="mainTagId"> <el-form-item label="主标签" prop="mainTagId">
{/* @ts-ignore */} {{
<SelectTags v-model={form.value.mainTagId} /> // @ts-ignore
default: () => <SelectTags v-model={form.value.mainTagId} />,
label: () => (
// <el-tooltip content="主标签最多选1个" placement="top">
<span class="cursor-pointer">
副标签
{/* <el-icon class="ml-1">
<InfoFilled />
</el-icon> */}
</span>
// </el-tooltip>
),
}}
</el-form-item> </el-form-item>
<el-form-item label="副标签"> <el-form-item label="副标签">
{/* @ts-ignore */} {{
<SelectTags default: () => (
v-model={form.value.tagList} // @ts-ignore
maxSelectedTags={2} <SelectTags
filterTagsFn={filterTagsFn} v-model={form.value.tagList}
/> filterTagsFn={filterTagsFn}
maxSelectedTags={3}
/>
),
label: () => (
// <el-tooltip content="副标签最多选3个" placement="top">
<span class="cursor-pointer">
副标签
{/* <el-icon class="ml-1">
<InfoFilled />
</el-icon> */}
</span>
// </el-tooltip>
),
}}
</el-form-item> </el-form-item>
<el-form-item label="发布时间" prop="releaseStatus"> <el-form-item label="发布类型" prop="sendType">
<el-radio-group v-model={form.value.releaseStatus} class="radio-group"> <el-radio-group v-model={form.value.sendType} class="radio-group">
<el-radio value={ReleaseStatusEnum.PUBLISH} class="radio-item immediate"> <el-radio value={SendTypeEnum.IMMEDIATE} class="radio-item immediate">
立即发布 立即发布
</el-radio> </el-radio>
<el-radio value={ReleaseStatusEnum.DRAFT} class="radio-item scheduled"> <el-radio value={SendTypeEnum.SCHEDULED} class="radio-item scheduled">
定时发布 定时发布
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
{form.value.sendType === SendTypeEnum.SCHEDULED && (
<el-form-item label="发布时间" prop="sendTime">
<el-date-picker
class="ml-2"
v-model={form.value.sendTime}
type="datetime"
placeholder="请选择发布时间"
/>
</el-form-item>
)}
</el-form> </el-form>
</div> </div>
) )
......
...@@ -3,37 +3,27 @@ ...@@ -3,37 +3,27 @@
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogTitle" :title="dialogTitle"
:close-on-click-modal="false" :close-on-click-modal="false"
width="600px" width="500px"
height="600px"
:show-close="false"
class="post-dialog" class="post-dialog"
align-center align-center
@closed="handleClosed"
> >
<div class="bg-white/95 rounded-16px p-24px backdrop-blur-10px"> <div class="bg-white/95 rounded-16px p-24px backdrop-blur-10px">
<keep-alive> <!-- <keep-alive> -->
<component :is="currentFormComp" ref="formComponentRef" /> <component :is="currentFormComp" ref="formComponentRef" />
</keep-alive> <!-- </keep-alive> -->
<!-- 底部按钮 --> <!-- 底部按钮 -->
<div class="flex justify-end gap-12px mt-24px pt-20px border-t border-blue-100"> <div class="flex justify-end gap-1">
<button <el-button @click="handleClosed">取消</el-button>
@click.prevent="handleCancel" <el-button @click="handleSaveDraft" class="">存草稿</el-button>
class="bg-white/80 border-2 border-gray-200 text-gray-600 rounded-10px px-20px py-10px font-500 transition-all-300 hover:bg-white/90 hover:border-gray-300" <el-button
> type="primary"
取消 @click="handleSubmit"
</button> class="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
<button
@click.prevent="handlePreview"
class="bg-white/80 border-2 border-blue-300 text-blue-500 rounded-10px px-20px py-10px font-500 transition-all-300 hover:bg-blue-50 hover:border-blue-500"
>
预览
</button>
<button
@click.prevent="handleSubmit"
class="cursor-pointer bg-gradient-to-r from-blue-400 to-cyan-400 border-none rounded-10px px-24px py-10px font-500 text-white transition-all-300 hover:transform hover:translate-y--2px hover:shadow-lg hover:shadow-blue-300"
> >
发布 发布
</button> </el-button>
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
...@@ -61,11 +51,11 @@ const typeMap: Record<ArticleTypeEnum, { title: string; component: Component }> ...@@ -61,11 +51,11 @@ const typeMap: Record<ArticleTypeEnum, { title: string; component: Component }>
}, },
[ArticleTypeEnum.POST]: { [ArticleTypeEnum.POST]: {
title: '帖子', title: '帖子',
component: PostForm, component: defineAsyncComponent(() => import('./postForm.tsx')),
}, },
[ArticleTypeEnum.PRACTICE]: { [ArticleTypeEnum.PRACTICE]: {
title: '实践', title: '实践',
component: PracticeForm, component: defineAsyncComponent(() => import('./practiceForm.tsx')),
}, },
[ArticleTypeEnum.COLUMN]: { [ArticleTypeEnum.COLUMN]: {
title: '专栏', title: '专栏',
...@@ -99,14 +89,15 @@ const open = (type: ArticleTypeEnum) => { ...@@ -99,14 +89,15 @@ const open = (type: ArticleTypeEnum) => {
// 关闭弹窗 // 关闭弹窗
const close = () => { const close = () => {
dialogVisible.value = false dialogVisible.value = false
formComponentRef.value?.resetFields()
} }
// 事件处理 const handleClosed = () => {
const handleCancel = () => { dialogVisible.value = false
close() formComponentRef.value?.resetFields()
} }
const handlePreview = async () => {} const handleSaveDraft = async () => {}
const handleSubmit = async () => { const handleSubmit = async () => {
const formData = await formComponentRef.value?.validate() const formData = await formComponentRef.value?.validate()
......
...@@ -30,8 +30,8 @@ ...@@ -30,8 +30,8 @@
@click="router.push('/userPage')" @click="router.push('/userPage')"
> >
<img <img
class="w-8 h-8 object-contain flex-shrink-0" class="w-8 h-8 object-contain flex-shrink-0 rounded-full"
src="@/assets/img/culture/avatar_girl.png" :src="userInfo?.avatar"
alt="个人中心" alt="个人中心"
/> />
<span class="ml-2 text-sm text-gray-700 whitespace-nowrap hidden lg:inline" <span class="ml-2 text-sm text-gray-700 whitespace-nowrap hidden lg:inline"
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
class="flex items-center cursor-pointer px-2 py-1 rounded sm:px-3 sm:py-2 hover:shadow-lg duration-200" class="flex items-center cursor-pointer px-2 py-1 rounded sm:px-3 sm:py-2 hover:shadow-lg duration-200"
> >
<img <img
class="w-8 h-8 object-contain flex-shrink-0" class="w-7 h-7 object-contain flex-shrink-0"
src="@/assets/img/culture/feedback.png" src="@/assets/img/culture/feedback.png"
alt="" alt=""
/> />
...@@ -50,12 +50,17 @@ ...@@ -50,12 +50,17 @@
>意见反馈</span >意见反馈</span
> >
</div> </div>
<el-dropdown placement="top-start" @command="handlePost"> <el-dropdown
placement="top-start"
@command="handlePost"
@visible-change="(val) => (isDropdownHover = val)"
>
<button <button
class="cursor-pointer w-24 h-9 text-black hover:text-black bg-[linear-gradient(to_right,#B3B8FD_0%,#7083FF_100%)] border-none hover:shadow-[0_1px_8px_0_rgba(95,0,237,0.25)] duration-200 flex-1 text-xs sm:text-sm rounded-xl group" class="cursor-pointer w-24 h-9 text-black hover:text-black bg-[linear-gradient(to_right,#B3B8FD_0%,#7083FF_100%)] border-none hover:shadow-[0_1px_8px_0_rgba(95,0,237,0.25)] duration-200 flex-1 text-xs sm:text-sm rounded-xl group"
> >
<img <img
class="h-7 w-7 transition-transform duration-300 ease-in-out group-hover:rotate-90" class="h-7 w-7 transition-transform duration-300 ease-in-out"
:class="{ 'rotate-90': isDropdownHover }"
src="@/assets/img/culture/post.png" src="@/assets/img/culture/post.png"
alt="" alt=""
/> />
...@@ -63,18 +68,10 @@ ...@@ -63,18 +68,10 @@
</button> </button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item :command="ArticleTypeEnum.POST" class="group" <el-dropdown-item :command="ArticleTypeEnum.POST">帖子</el-dropdown-item>
>帖子</el-dropdown-item <el-dropdown-item :command="ArticleTypeEnum.PRACTICE">实践</el-dropdown-item>
> <el-dropdown-item :command="ArticleTypeEnum.COLUMN">专栏</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.PRACTICE" class="group" <el-dropdown-item :command="ArticleTypeEnum.INTERVIEW">专访</el-dropdown-item>
>实践</el-dropdown-item
>
<el-dropdown-item :command="ArticleTypeEnum.COLUMN" class="group"
>专栏</el-dropdown-item
>
<el-dropdown-item :command="ArticleTypeEnum.INTERVIEW" class="group"
>专访</el-dropdown-item
>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
...@@ -107,6 +104,12 @@ import OnlineTime from './components/onlineTime.vue' ...@@ -107,6 +104,12 @@ import OnlineTime from './components/onlineTime.vue'
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router' import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'
import PublishDialog from './components/publishDialog.vue' import PublishDialog from './components/publishDialog.vue'
import { ArticleTypeEnum } from '@/constants' import { ArticleTypeEnum } from '@/constants'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const router = useRouter() const router = useRouter()
const search = ref('') const search = ref('')
...@@ -129,6 +132,7 @@ const handlePost = async (type: string) => { ...@@ -129,6 +132,7 @@ const handlePost = async (type: string) => {
// router.push(command) // router.push(command)
PublishDialogRef.value?.open(type) PublishDialogRef.value?.open(type)
} }
const isDropdownHover = ref(false)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.layout-culture { .layout-culture {
......
...@@ -233,21 +233,7 @@ ...@@ -233,21 +233,7 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<!-- 左侧:回到顶部按钮 --> <!-- 左侧:回到顶部按钮 -->
<div class="left"> <div class="left">
<button <ScrollTopComp />
class="back-top-btn group flex items-center gap-3 px-4 py-2.5 bg-gradient-to-r from-blue-50 to-indigo-50 hover:from-blue-100 hover:to-indigo-100 border border-blue-200/50 rounded-full transition-all duration-300 hover:shadow-lg hover:-translate-y-1 active:scale-95"
title="回到顶部"
@click="handleBackTop"
>
<div
class="w-8 h-8 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full flex items-center justify-center group-hover:rotate-12 transition-transform duration-300 shadow-sm"
>
<SvgIcon color="#409eff" name="icon_top" size="16" />
</div>
<span
class="text-sm font-medium text-gray-700 group-hover:text-blue-600 transition-colors"
>回到顶部</span
>
</button>
</div> </div>
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
...@@ -272,7 +258,6 @@ ...@@ -272,7 +258,6 @@
</template> </template>
<script setup lang="ts" name="CultureAsk"> <script setup lang="ts" name="CultureAsk">
import { ref } from 'vue'
import Tabs from '@/components/common/Tabs' import Tabs from '@/components/common/Tabs'
import { import {
User, User,
...@@ -284,6 +269,8 @@ import { ...@@ -284,6 +269,8 @@ import {
ChatDotRound, ChatDotRound,
ArrowDown, ArrowDown,
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
...@@ -297,12 +284,10 @@ const tabs = [ ...@@ -297,12 +284,10 @@ const tabs = [
const questionContent = ref('') const questionContent = ref('')
const tagInput = ref('') const tagInput = ref('')
const handleBackTop = () => { const tabsRef = inject(TABS_REF_KEY)
window.scrollTo({
top: 0, const { ScrollTopComp } = useScrollTop(tabsRef!)
behavior: 'smooth',
})
} // 示例数据
const questions = ref([ const questions = ref([
{ {
title: '什么是顶级思维?', title: '什么是顶级思维?',
......
<template> <template>
<div> <div ref="listRef">
<template v-if="list.length > 0"> <template v-if="list.length > 0 && !loading">
<div class="space-y-3 sm:space-y-4"> <div class="space-y-3 sm:space-y-4">
<div <div
v-for="item in list" v-for="item in list"
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
<!-- 时间 --> <!-- 时间 -->
<span class="text-gray-400 font-medium ml-auto sm:ml-0"> <span class="text-gray-400 font-medium ml-auto sm:ml-0">
<span class="hidden sm:inline">{{ <span class="hidden sm:inline">{{
dayjs(item.createTime).format('YYYY-MM-DD HH:mm') dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm')
}}</span> }}</span>
</span> </span>
</div> </div>
...@@ -86,21 +86,7 @@ ...@@ -86,21 +86,7 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<!-- 左侧:回到顶部按钮 --> <!-- 左侧:回到顶部按钮 -->
<div class="left"> <div class="left">
<button <ScrollTopComp />
class="back-top-btn group flex items-center gap-3 px-4 py-2.5 bg-gradient-to-r from-blue-50 to-indigo-50 hover:from-blue-100 hover:to-indigo-100 border border-blue-200/50 rounded-full transition-all duration-300 hover:shadow-lg hover:-translate-y-1 active:scale-95"
title="回到顶部"
@click="handleBackTop"
>
<div
class="w-8 h-8 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full flex items-center justify-center group-hover:rotate-12 transition-transform duration-300 shadow-sm"
>
<SvgIcon color="#409eff" name="icon_top" size="16" />
</div>
<span
class="text-sm font-medium text-gray-700 group-hover:text-blue-600 transition-colors"
>回到顶部</span
>
</button>
</div> </div>
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
...@@ -115,7 +101,11 @@ ...@@ -115,7 +101,11 @@
layout="prev, pager, next, jumper, total" layout="prev, pager, next, jumper, total"
:total="total" :total="total"
class="custom-pagination" class="custom-pagination"
@current-change="goToPage" @current-change="
(e) => {
;(handleBackTop(), goToPage(e))
}
"
@size-change="changePageSize" @size-change="changePageSize"
/> />
</div> </div>
...@@ -124,6 +114,12 @@ ...@@ -124,6 +114,12 @@
</div> </div>
</div> </div>
</template> </template>
<template v-else-if="loading">
<div class="flex items-center justify-center h-full">
<el-icon class="is-loading mr-2 text-gray-500"><Loading /></el-icon>
<span class="text-gray-500">加载中...</span>
</div>
</template>
<template v-else> <template v-else>
<div class="flex items-center justify-center h-full"> <div class="flex items-center justify-center h-full">
<el-empty description="暂无数据" /> <el-empty description="暂无数据" />
...@@ -135,23 +131,23 @@ ...@@ -135,23 +131,23 @@
<script setup lang="ts" name="RecommendList"> <script setup lang="ts" name="RecommendList">
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import { getArticleList } from '@/api' import { getArticleList } from '@/api'
import { ArticleTypeEnum } from '@/constants' import { ArticleTypeEnum, TABS_REF_KEY } from '@/constants'
import { useScrollTop } from '@/hooks'
import dayjs from 'dayjs' import dayjs from 'dayjs'
const router = useRouter() const router = useRouter()
const { list, total, searchParams, goToPage, changePageSize } = usePageSearch(getArticleList, { const { list, total, searchParams, loading, goToPage, changePageSize } = usePageSearch(
defaultParams: { type: ArticleTypeEnum.POST }, getArticleList,
defaultCurrent: 1, {
defaultSize: 5, defaultParams: { type: ArticleTypeEnum.POST },
}) defaultCurrent: 1,
defaultSize: 5,
const handleBackTop = () => { },
window.scrollTo({ )
top: 0, const tabsRef = inject(TABS_REF_KEY)
behavior: 'smooth',
}) const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
}
</script> </script>
<!-- <style scoped> <!-- <style scoped>
/* 自定义分页器样式 */ /* 自定义分页器样式 */
......
...@@ -344,21 +344,7 @@ ...@@ -344,21 +344,7 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<!-- 左侧:回到顶部按钮 --> <!-- 左侧:回到顶部按钮 -->
<div class="left"> <div class="left">
<button <ScrollTopComp />
class="back-top-btn group flex items-center gap-3 px-4 py-2.5 bg-gradient-to-r from-blue-50 to-indigo-50 hover:from-blue-100 hover:to-indigo-100 border border-blue-200/50 rounded-full transition-all duration-300 hover:shadow-lg hover:-translate-y-1 active:scale-95"
title="回到顶部"
@click="handleBackTop"
>
<div
class="w-8 h-8 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full flex items-center justify-center group-hover:rotate-12 transition-transform duration-300 shadow-sm"
>
<SvgIcon color="#409eff" name="icon_top" size="16" />
</div>
<span
class="text-sm font-medium text-gray-700 group-hover:text-blue-600 transition-colors"
>回到顶部</span
>
</button>
</div> </div>
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
...@@ -384,6 +370,12 @@ ...@@ -384,6 +370,12 @@
<script setup lang="ts" name="RecommendList"> <script setup lang="ts" name="RecommendList">
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(15) const pageSize = ref(15)
...@@ -396,9 +388,6 @@ const tabs = [ ...@@ -396,9 +388,6 @@ const tabs = [
] ]
const activeTab = ref('latest') const activeTab = ref('latest')
const handleBackTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const goVideoDetail = (n: number) => { const goVideoDetail = (n: number) => {
router.push(`/videoDetail?id=${n}`) router.push(`/videoDetail?id=${n}`)
......
...@@ -37,12 +37,14 @@ const activeTab = ref('推荐') ...@@ -37,12 +37,14 @@ const activeTab = ref('推荐')
color: #000 !important; color: #000 !important;
box-shadow: none !important; /* 如果你还要禁掉 hover 阴影 */ box-shadow: none !important; /* 如果你还要禁掉 hover 阴影 */
} }
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-enter-from,
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;
transform: translateX(30px);
filter: blur(4px);
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
} }
</style> </style>
<template>
<div
class="fixed right-50 top-70% -translate-y-50% flex flex-col items-center content-center gap-5"
>
<div class="flex flex-col bg-white rounded-8px shadow-md overflow-hidden">
<div
v-for="item in stats"
:key="item.label"
:content="item.label"
placement="left"
:show-after="300"
>
<div
class="flex flex-col items-center justify-center w-56px h-56px transition-all duration-200 group"
@click="handleClick(item)"
>
<el-icon
class="group-hover:text-blue-500! cursor-pointer"
size="20"
:style="{ color: item.active ? '#409eff' : '#606266' }"
>
<component :is="item.icon" />
</el-icon>
<span
class="text-12px cursor-pointer group-hover:text-blue-500!"
:style="{ color: item.active ? '#409eff' : '#606266' }"
>
{{ item.count }}
</span>
</div>
</div>
</div>
<ScrollTopComp class="flex flex-col" />
</div>
</template>
<script setup lang="ts">
import { Star, ChatLineSquare, Pointer } from '@element-plus/icons-vue'
import type { ArticleItemDto } from '@/api'
import type { Component } from 'vue'
import { useScrollTop } from '@/hooks'
import { addOrCancelCollect } from '@/api'
import { COMMENT_REF_KEY } from '@/constants'
const { articleDetail } = defineProps<{
articleDetail: ArticleItemDto
}>()
const commentRef = inject(COMMENT_REF_KEY)
const { ScrollTopComp } = useScrollTop(window)
const { handleBackTop } = useScrollTop(commentRef!)
interface StatItem {
icon: Component
count: number
label: string
active?: boolean
actionFn?: () => Promise<boolean>
}
const stats = computed(() => {
return [
{
icon: Pointer,
count: articleDetail?.praiseCount,
label: '点赞',
active: articleDetail?.viewCount,
},
{
icon: Star,
count: articleDetail?.collectionCount,
label: '收藏',
active: articleDetail?.isRecommend,
api: addOrCancelCollect,
async actionFn(item: StatItem) {
const res = await addOrCancelCollect({
articleId: articleDetail.id,
type: articleDetail.type,
})
if (res) {
item.active = !item.active
}
},
},
{
icon: ChatLineSquare,
count: articleDetail?.replyCount,
label: '评论',
active: articleDetail?.isRecommend,
actionFn: handleBackTop,
},
]
})
const handleClick = async (item: StatItem) => {
if (item.actionFn) {
await item.actionFn()
}
}
</script>
<template> <template>
<div class="min-h-screen bg-gradient-to-br from-blue-50 via-purple-50 to-cyan-50"> <div class="min-h-screen px-20">
<!-- <div class="max-w-6xl mx-auto px-4 py-6 grid grid-cols-1 lg:grid-cols-4 gap-6"> -->
<!-- 主内容区 --> <!-- 主内容区 -->
<ActionButtons :articleDetail="articleDetail"></ActionButtons>
<div class="lg:col-span-3"> <div class="lg:col-span-3">
<!-- 帖子主体 --> <!-- 帖子主体 -->
<div <div
...@@ -32,8 +32,8 @@ ...@@ -32,8 +32,8 @@
</span> </span>
</div> </div>
<p class="text-sm text-gray-500 mt-1"> <p class="text-sm text-gray-500 mt-1">
{{ dayjs(articleDetail?.createTime || 0 * 1000).format('YYYY-MM-DD HH:mm:ss') }} · {{ dayjs((articleDetail?.createTime || 0) * 1000).format('YYYY-MM-DD HH:mm:ss') }}
{{ articleDetail?.viewCount || 0 }} 阅读 · {{ articleDetail?.viewCount || 0 }} 阅读
</p> </p>
</div> </div>
<button <button
...@@ -95,38 +95,11 @@ ...@@ -95,38 +95,11 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 互动按钮 -->
<div class="px-6 py-4 border-t border-gray-100 bg-gray-50/50">
<div class="flex items-center justify-between">
<div class="flex items-center gap-6">
<button
class="flex items-center gap-2 px-4 py-2 rounded-full hover:bg-gray-100 text-gray-600 transition-all"
>
<i class="i-carbon-favorite"></i>
<span>128</span>
</button>
<button
class="flex items-center gap-2 px-4 py-2 rounded-full hover:bg-gray-100 text-gray-600 transition-all"
>
<i class="i-carbon-bookmark"></i>
<span>56</span>
</button>
<button
class="flex items-center gap-2 px-4 py-2 rounded-full hover:bg-gray-100 text-gray-600 transition-all"
>
<i class="i-carbon-share"></i>
<span>分享</span>
</button>
</div>
</div>
</div>
</div> </div>
<!-- 评论区 --> <!-- 评论区 -->
<div <div
ref="commentRef"
class="mt-6 bg-white/90 backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 overflow-hidden" class="mt-6 bg-white/90 backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 overflow-hidden"
> >
<!-- 评论筛选 --> <!-- 评论筛选 -->
...@@ -137,22 +110,22 @@ ...@@ -137,22 +110,22 @@
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
class="px-3 py-1.5 text-sm bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full shadow-md" class="cursor-pointer px-3 py-1.5 text-sm bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full shadow-md"
> >
最热 最热
</button> </button>
<button <button
class="px-3 py-1.5 text-sm hover:bg-gray-100 text-gray-600 rounded-full transition-all" class="cursor-pointer px-3 py-1.5 text-sm hover:bg-gray-100 text-gray-600 rounded-full transition-all"
> >
最新 最新
</button> </button>
<button <button
class="px-3 py-1.5 text-sm hover:bg-gray-100 text-gray-600 rounded-full transition-all" class="cursor-pointer px-3 py-1.5 text-sm hover:bg-gray-100 text-gray-600 rounded-full transition-all"
> >
置顶 置顶
</button> </button>
<button <button
class="px-3 py-1.5 text-sm hover:bg-gray-100 text-gray-600 rounded-full transition-all" class="cursor-pointer px-3 py-1.5 text-sm hover:bg-gray-100 text-gray-600 rounded-full transition-all"
> >
精选 精选
</button> </button>
...@@ -163,17 +136,14 @@ ...@@ -163,17 +136,14 @@
<!-- 发表评论 --> <!-- 发表评论 -->
<div class="p-4 border-b border-gray-100"> <div class="p-4 border-b border-gray-100">
<div class="flex gap-3"> <div class="flex gap-3">
<img <img :src="userInfo?.avatar" alt="" class="w-10 h-10 rounded-full object-cover" />
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1"> <div class="flex-1">
<textarea <el-input
v-model="comment"
type="textarea"
placeholder="写下你的评论..." placeholder="写下你的评论..."
class="w-full p-3 border border-gray-200 rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" :rows="3"
rows="3" ></el-input>
></textarea>
<div class="flex justify-between items-center mt-3"> <div class="flex justify-between items-center mt-3">
<div class="flex items-center gap-2 text-sm text-gray-500"> <div class="flex items-center gap-2 text-sm text-gray-500">
<button class="hover:text-blue-500 transition-colors"> <button class="hover:text-blue-500 transition-colors">
...@@ -184,7 +154,9 @@ ...@@ -184,7 +154,9 @@
</button> </button>
</div> </div>
<button <button
class="px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all" class="cursor-pointer disabled:opacity-50 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled="!comment.trim()"
@click="handleComment"
> >
发表 发表
</button> </button>
...@@ -224,7 +196,12 @@ ...@@ -224,7 +196,12 @@
<i class="i-carbon-favorite"></i> <i class="i-carbon-favorite"></i>
<span>24</span> <span>24</span>
</button> </button>
<button class="hover:text-blue-500 transition-colors">回复</button> <button
class="cursor-pointer hover:text-blue-500 transition-colors"
@click="handleReply(1)"
>
回复
</button>
</div> </div>
</div> </div>
...@@ -244,12 +221,62 @@ ...@@ -244,12 +221,62 @@
<p class="text-sm text-gray-700"> <p class="text-sm text-gray-700">
谢谢认可!确实这个组合在开发效率上提升很大。 谢谢认可!确实这个组合在开发效率上提升很大。
</p> </p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>1小时前</span>
<button
class="flex items-center gap-1 hover:text-red-500 transition-colors"
>
<i class="i-carbon-favorite"></i>
<span>24</span>
</button>
<button
@click="handleReply(1)"
class="cursor-pointer hover:text-blue-500 transition-colors"
>
回复
</button>
</div>
</div>
</div>
</div>
<div v-show="currentId === 1" class="flex gap-3">
<img
:src="userInfo?.avatar"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<el-input
v-model="comment"
type="textarea"
placeholder="写下你的评论..."
:rows="3"
></el-input>
<div class="flex justify-between items-center mt-3">
<div class="flex items-center gap-2 text-sm text-gray-500">
<button class="hover:text-blue-500 transition-colors">
<i class="i-carbon-face-satisfied"></i>
</button>
<button class="hover:text-blue-500 transition-colors">
<i class="i-carbon-image"></i>
</button>
</div>
<button
class="cursor-pointer disabled:opacity-50 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled="!comment.trim()"
@click="handleComment"
>
发表
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<el-divider />
<!-- 评论2 --> <!-- 评论2 -->
<div class="p-4 hover:bg-gray-50/50 transition-colors"> <div class="p-4 hover:bg-gray-50/50 transition-colors">
...@@ -278,30 +305,327 @@ ...@@ -278,30 +305,327 @@
</button> </button>
</div> </div>
</div> </div>
<!-- 回复列表 -->
<div class="mt-3 ml-4 space-y-3">
<div class="flex gap-2 p-3 bg-gray-50 rounded-lg">
<img
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-8 h-8 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-sm text-gray-800">前端小李</span>
<span class="text-xs text-gray-500">30分钟前</span>
</div>
<p class="text-sm text-gray-700">
谢谢认可!确实这个组合在开发效率上提升很大。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>1小时前</span>
<button
class="flex items-center gap-1 hover:text-red-500 transition-colors"
>
<i class="i-carbon-favorite"></i>
<span>24</span>
</button>
<button
@click="handleReply(1)"
class="cursor-pointer hover:text-blue-500 transition-colors"
>
回复
</button>
</div>
</div>
</div>
</div>
<div v-show="currentId === 1" class="flex gap-3">
<img
:src="userInfo?.avatar"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<el-input
v-model="comment"
type="textarea"
placeholder="写下你的评论..."
:rows="3"
></el-input>
<div class="flex justify-between items-center mt-3">
<div class="flex items-center gap-2 text-sm text-gray-500">
<button class="hover:text-blue-500 transition-colors">
<i class="i-carbon-face-satisfied"></i>
</button>
<button class="hover:text-blue-500 transition-colors">
<i class="i-carbon-image"></i>
</button>
</div>
<button
class="cursor-pointer disabled:opacity-50 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled="!comment.trim()"
@click="handleComment"
>
发表
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
<button
class="cursor-pointer hover:text-blue-500 transition-colors"
@click="handleReply(2)"
>
回复
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- </div> -->
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { getArticleDetail, type ArticleItemDto } from '@/api' import { getArticleDetail, type ArticleItemDto } from '@/api'
import ActionButtons from './components/actionButtons.vue'
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import { COMMENT_REF_KEY } from '@/constants'
const userStore = useUserStore()
const commentRef = useTemplateRef<HTMLElement | null>('commentRef')
const { userInfo } = storeToRefs(userStore)
const route = useRoute() const route = useRoute()
const id = route.params.articleId as string const id = route.params.articleId as string
const articleDetail = ref<ArticleItemDto>() const articleDetail = ref({} as ArticleItemDto)
const comment = ref('')
const currentId = ref(-1)
const fetchArticleDetail = async () => { const fetchArticleDetail = async () => {
const { data } = await getArticleDetail(id) const { data } = await getArticleDetail(id)
articleDetail.value = data articleDetail.value = data
} }
const handleReply = (id: number) => {
comment.value = ''
currentId.value = id
}
const handleComment = async () => {
console.log(comment.value)
}
provide(COMMENT_REF_KEY, commentRef)
onMounted(async () => { onMounted(async () => {
fetchArticleDetail() fetchArticleDetail()
}) })
......
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
v-model="dialogVisible" v-model="dialogVisible"
title="个人信息" title="个人信息"
width="500px" width="500px"
:before-close="handleClose" :close-on-click-modal="false"
class="user-info-dialog" class="user-info-dialog"
:closed="close"
> >
<el-form ref="formRef" :model="form" :rules="rules" label-width="60px" class="px-4"> <el-form ref="formRef" :model="form" :rules="rules" label-width="60px" class="px-4">
<!-- 昵称 --> <!-- 昵称 -->
...@@ -75,11 +76,6 @@ const close = () => { ...@@ -75,11 +76,6 @@ const close = () => {
resetForm() resetForm()
} }
// 关闭弹窗前的处理
const handleClose = (done: () => void) => {
done()
}
// 取消操作 // 取消操作
const handleCancel = () => { const handleCancel = () => {
close() close()
......
<template>
<div class="w-full h-full bg-white rounded-lg overflow-hidden">
<!-- Custom Tabs (保持原样式) -->
<div class="flex border-b border-gray-200">
<div
v-for="tab in tabs"
:key="tab.key"
class="px-6 py-4 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors relative"
:class="[
searchParams.type === tab.key
? 'text-blue-500! font-medium'
: 'text-gray-600 hover:text-blue-400',
]"
@click="toggleTab(tab.key)"
>
{{ tab.label }}
</div>
</div>
<!-- Content Area -->
<div class="flex-1 flex flex-col">
<!-- List Container -->
<div class="flex-1 p-6">
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center h-64">
<el-icon class="is-loading mr-2 text-gray-500"><Loading /></el-icon>
<span class="text-gray-500">加载中...</span>
</div>
<!-- Empty State -->
<div v-else-if="!list.length" class="flex flex-col items-center justify-center h-64">
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div>
<div class="text-gray-400 text-sm">{{ getEmptyText() }}</div>
</div>
<!-- List Items -->
<div v-else class="space-y-4">
<div
v-for="item in paginatedList"
:key="item.id"
class="flex items-center p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
>
<!-- Avatar -->
<el-avatar class="mr-4 flex-shrink-0" :size="48" style="background-color: #e5f3ff">
<el-icon class="text-blue-500"><User /></el-icon>
</el-avatar>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div>
<div class="text-gray-500 text-sm mt-1 truncate">{{ item.description }}</div>
</div>
<!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4">
<el-icon class="mr-1"><Clock /></el-icon>
<span>{{ item.time }}</span>
</div>
</div>
</div>
</div>
<!-- Custom Pagination (保持原样式) -->
<div
v-if="list.length"
class="flex items-center justify-between px-6 py-4 border-t border-gray-200"
>
<div class="right">
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:page-sizes="[10, 20, 30, 40]"
layout="prev, pager, next, jumper, total"
:total="total"
class="custom-pagination"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { Loading, User, Clock, Document } from '@element-plus/icons-vue'
import { getSelfCollectList } from '@/api'
import { usePageSearch } from '@/hooks'
import { ArticleTypeEnum } from '@/constants'
interface TabItem {
key: string
label: string
}
const toggleTab = (key: string) => {
searchParams.value.type = key
refresh()
}
const tabs: TabItem[] = [
{ key: 'posts', label: '帖子' },
{ key: 'videos', label: '视频' },
{ key: 'questions', label: '问题' },
{ key: 'articles', label: '专栏' },
{ key: 'practice', label: '实践' },
{ key: 'interviews', label: '专访' },
]
// State
const { list, loading, searchParams, total, refresh } = usePageSearch(getSelfCollectList, {
defaultParams: {
type: ArticleTypeEnum.POST,
},
})
// Computed
const paginatedList = computed(() => {
const start = (searchParams.value.current - 1) * searchParams.value.size
const end = start + searchParams.value.size
return list.value.slice(start, end)
})
const getEmptyText = () => {
const emptyTexts: Record<string, string> = {
posts: '还没有发布任何帖子',
videos: '还没有上传任何视频',
questions: '还没有提出任何问题',
articles: '还没有发表任何专栏文章',
practice: '还没有分享任何实践经验',
interviews: '还没有参与任何专访',
}
return emptyTexts[searchParams.type] || '暂无数据'
}
</script>
<template>
<div class="w-full h-full bg-white rounded-lg overflow-hidden">
<!-- Custom Tabs (保持原样式) -->
<div class="flex border-b border-gray-200">
<div
v-for="tab in tabs"
:key="tab.key"
class="px-6 py-4 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors relative"
:class="[
searchParams.type === tab.key
? 'text-blue-500! font-medium'
: 'text-gray-600 hover:text-blue-400',
]"
@click="toggleTab(tab.key)"
>
{{ tab.label }}
</div>
</div>
<!-- Content Area -->
<div class="flex-1 flex flex-col">
<!-- List Container -->
<div class="flex-1 p-6">
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center h-64">
<el-icon class="is-loading mr-2 text-gray-500"><Loading /></el-icon>
<span class="text-gray-500">加载中...</span>
</div>
<!-- Empty State -->
<div v-else-if="!list.length" class="flex flex-col items-center justify-center h-64">
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div>
<div class="text-gray-400 text-sm">{{ getEmptyText() }}</div>
</div>
<!-- List Items -->
<div v-else class="space-y-4">
<div
v-for="item in paginatedList"
:key="item.id"
class="flex items-center p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
>
<!-- Avatar -->
<el-avatar class="mr-4 flex-shrink-0" :size="48" style="background-color: #e5f3ff">
<el-icon class="text-blue-500"><User /></el-icon>
</el-avatar>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div>
<div class="text-gray-500 text-sm mt-1 truncate">{{ item.description }}</div>
</div>
<!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4">
<el-icon class="mr-1"><Clock /></el-icon>
<span>{{ item.time }}</span>
</div>
</div>
</div>
</div>
<!-- Custom Pagination (保持原样式) -->
<div
v-if="list.length"
class="flex items-center justify-between px-6 py-4 border-t border-gray-200"
>
<div class="right">
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:page-sizes="[10, 20, 30, 40]"
layout="prev, pager, next, jumper, total"
:total="total"
class="custom-pagination"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { Loading, User, Clock, Document } from '@element-plus/icons-vue'
import { getSelfPraiseList } from '@/api'
import { usePageSearch } from '@/hooks'
import { ArticleTypeEnum } from '@/constants'
interface TabItem {
key: string
label: string
}
const toggleTab = (key: string) => {
searchParams.value.type = key
refresh()
}
const tabs: TabItem[] = [
{ key: 'posts', label: '帖子' },
{ key: 'videos', label: '视频' },
{ key: 'questions', label: '问题' },
{ key: 'articles', label: '专栏' },
{ key: 'practice', label: '实践' },
{ key: 'interviews', label: '专访' },
]
// State
const { list, loading, searchParams, total, refresh } = usePageSearch(getSelfPraiseList, {
defaultParams: {
type: ArticleTypeEnum.POST,
},
})
// Computed
const paginatedList = computed(() => {
const start = (searchParams.value.current - 1) * searchParams.value.size
const end = start + searchParams.value.size
return list.value.slice(start, end)
})
const getEmptyText = () => {
const emptyTexts: Record<string, string> = {
posts: '还没有发布任何帖子',
videos: '还没有上传任何视频',
questions: '还没有提出任何问题',
articles: '还没有发表任何专栏文章',
practice: '还没有分享任何实践经验',
interviews: '还没有参与任何专访',
}
return emptyTexts[searchParams.type] || '暂无数据'
}
</script>
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
<div class="absolute top-4 right-4 flex gap-2"> <div class="absolute top-4 right-4 flex gap-2">
<el-button type="info" plain size="small">清除缓存</el-button> <el-button type="info" plain size="small">清除缓存</el-button>
<el-button type="info" plain size="small">切换账号</el-button> <el-button type="info" plain size="small">切换账号</el-button>
<el-button type="primary" size="small">编辑个人资料</el-button>
</div> </div>
</div> </div>
...@@ -58,8 +57,13 @@ ...@@ -58,8 +57,13 @@
<!-- 右侧内容区域 --> <!-- 右侧内容区域 -->
<div class="flex-1"> <div class="flex-1">
<div class="bg-white rounded-lg shadow-sm min-h-500px"> <div class="bg-white rounded-lg shadow-sm min-h-500px">
<!-- 标签页 --> <transition name="fade" mode="out-in">
<div class="border-b border-gray-200"> <keep-alive>
<component :is="currentComponent" />
</keep-alive>
</transition>
<!-- <div class="border-b border-gray-200">
<div class="flex"> <div class="flex">
<div <div
v-for="tab in tabs" v-for="tab in tabs"
...@@ -79,9 +83,7 @@ ...@@ -79,9 +83,7 @@
</div> </div>
</div> </div>
<!-- 内容区域 -->
<div class="p-8"> <div class="p-8">
<!-- 空状态 -->
<div class="flex flex-col items-center justify-center py-20"> <div class="flex flex-col items-center justify-center py-20">
<div class="w-120px h-120px mb-6 opacity-30"> <div class="w-120px h-120px mb-6 opacity-30">
<svg viewBox="0 0 1024 1024" class="w-full h-full fill-gray-400"> <svg viewBox="0 0 1024 1024" class="w-full h-full fill-gray-400">
...@@ -97,7 +99,6 @@ ...@@ -97,7 +99,6 @@
<p class="text-gray-400 text-sm">暂无帖子</p> <p class="text-gray-400 text-sm">暂无帖子</p>
</div> </div>
<!-- 分页 -->
<div class="flex justify-center mt-8"> <div class="flex justify-center mt-8">
<el-pagination <el-pagination
v-model:current-page="currentPage" v-model:current-page="currentPage"
...@@ -107,7 +108,7 @@ ...@@ -107,7 +108,7 @@
:hide-on-single-page="true" :hide-on-single-page="true"
/> />
</div> </div>
</div> </div> -->
</div> </div>
</div> </div>
</div> </div>
...@@ -132,6 +133,9 @@ import { ...@@ -132,6 +133,9 @@ import {
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import EditUserInfo from './components/editUserInfo.vue' import EditUserInfo from './components/editUserInfo.vue'
import SelfCollect from './components/selfCollect.vue'
import SelfPraise from './components/selfPraise.vue'
import { hasOfficialAccount } from '@/api'
const editUserInfoRef = useTemplateRef<InstanceType<typeof EditUserInfo>>('editUserInfoRef') const editUserInfoRef = useTemplateRef<InstanceType<typeof EditUserInfo>>('editUserInfoRef')
const userStore = useUserStore() const userStore = useUserStore()
...@@ -147,19 +151,22 @@ const activeTab = ref('published') ...@@ -147,19 +151,22 @@ const activeTab = ref('published')
const currentPage = ref(1) const currentPage = ref(1)
// 左侧菜单项 // 左侧菜单项
const menuItems = ref([ const menuItems = [
{ key: 'posts', label: '我的帖子', icon: User }, { key: 'posts', label: '我的帖子', icon: User },
{ key: 'activity', label: '我的活动', icon: Calendar }, { key: 'activity', label: '我的活动', icon: Calendar },
{ key: 'votes', label: '我的投票', icon: Trophy }, { key: 'votes', label: '我的投票', icon: Trophy },
{ key: 'articles', label: '我的文章', icon: Document }, { key: 'articles', label: '我的文章', icon: Document },
{ key: 'favorites', label: '我的收藏', icon: Star }, { key: 'favorites', label: '我的收藏', icon: Star, component: SelfCollect },
{ key: 'praise', label: '我的点赞', icon: Star, component: SelfPraise },
{ key: 'comments', label: '评论回复', icon: ChatDotRound }, { key: 'comments', label: '评论回复', icon: ChatDotRound },
{ key: 'follows', label: '获赞列表', icon: Trophy }, { key: 'follows', label: '获赞列表', icon: Trophy },
{ key: 'social', label: '关注和粉', icon: Link }, { key: 'social', label: '关注和粉', icon: Link },
{ key: 'feedback', label: '反馈列表', icon: View }, { key: 'feedback', label: '反馈列表', icon: View },
{ key: 'screen', label: '屏蔽管理', icon: Setting }, { key: 'screen', label: '屏蔽管理', icon: Setting },
]) ]
const currentComponent = computed(() => {
return menuItems.find((item) => item.key === activeMenu.value)?.component
})
// 标签页 // 标签页
const tabs = ref([ const tabs = ref([
{ key: 'published', label: '发布' }, { key: 'published', label: '发布' },
...@@ -174,6 +181,17 @@ const handleEdit = () => { ...@@ -174,6 +181,17 @@ const handleEdit = () => {
signature: '', signature: '',
}) })
} }
const getIsOfficial = () => {
setTimeout(async () => {
const { data } = await hasOfficialAccount()
console.log(data)
}, 1000)
}
onMounted(() => {
getIsOfficial()
})
</script> </script>
<style scoped> <style scoped>
...@@ -181,4 +199,12 @@ const handleEdit = () => { ...@@ -181,4 +199,12 @@ const handleEdit = () => {
background-image: url('http://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/2022/10/13/1665643787806.jpg'); background-image: url('http://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/2022/10/13/1665643787806.jpg');
} }
/* 如果需要额外的样式可以在这里添加 */ /* 如果需要额外的样式可以在这里添加 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style> </style>
...@@ -20,21 +20,7 @@ ...@@ -20,21 +20,7 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<!-- 左侧:回到顶部按钮 --> <!-- 左侧:回到顶部按钮 -->
<div class="left"> <div class="left">
<button <ScrollTopComp />
class="back-top-btn group flex items-center gap-3 px-4 py-2.5 bg-gradient-to-r from-blue-50 to-indigo-50 hover:from-blue-100 hover:to-indigo-100 border border-blue-200/50 rounded-full transition-all duration-300 hover:shadow-lg hover:-translate-y-1 active:scale-95"
title="回到顶部"
@click="handleBackTop"
>
<div
class="w-8 h-8 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full flex items-center justify-center group-hover:rotate-12 transition-transform duration-300 shadow-sm"
>
<SvgIcon color="#409eff" name="icon_top" size="16" />
</div>
<span
class="text-sm font-medium text-gray-700 group-hover:text-blue-600 transition-colors"
>回到顶部</span
>
</button>
</div> </div>
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
<div class="right"> <div class="right">
...@@ -64,6 +50,12 @@ import { Refresh } from '@element-plus/icons-vue' ...@@ -64,6 +50,12 @@ import { Refresh } from '@element-plus/icons-vue'
import ColumnList from './components/columnList.vue' import ColumnList from './components/columnList.vue'
import InterviewList from './components/interviewList.vue' import InterviewList from './components/interviewList.vue'
import PracticeList from './components/practiceList.vue' import PracticeList from './components/practiceList.vue'
import { useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp } = useScrollTop(tabsRef!)
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment