Commit 5d0b8441 by lijiabin

【需求 17679】 wip: 继续完善页面,新增后台管理页面

parent 4a950630
...@@ -121,6 +121,8 @@ export interface ArticleItemDto { ...@@ -121,6 +121,8 @@ export interface ArticleItemDto {
imgUrl: string imgUrl: string
createUserAvatar: string createUserAvatar: string
createUserName: string createUserName: string
showAvatar: string
showName: string
} }
/** /**
...@@ -205,6 +207,7 @@ export interface InterviewItemDto { ...@@ -205,6 +207,7 @@ export interface InterviewItemDto {
*/ */
export interface CommentSearchParams extends PageSearchParams { export interface CommentSearchParams extends PageSearchParams {
articleId: number | string articleId: number | string
sortType: number
} }
/** /**
......
import service from '@/utils/request/index'
import type { AddOrUpdateTagDto, BackendTagListItemDto, BackendTagSearchParams } from './types'
import type { BackendServicePageResult } from '@/utils/request/types'
// 后台管理标签相关接口
/**
* 获取轮播图列表 不分页 数量不多
*/
export const getCarouselList = () => {
return service.request<BackendServicePageResult<BackendTagListItemDto>>({
url: '/api/cultureCarousel/listNoPage',
method: 'POST',
data: {},
})
}
/**
* 添加轮播图
*/
export const addCarousel = (data: AddOrUpdateCarouselDto) => {
return service.request({
url: '/api/cultureCarousel/addCarousel',
method: 'POST',
data,
})
}
/**
* 更新轮播图
*/
export const editCarousel = (data: AddOrUpdateCarouselDto) => {
return service.request({
url: '/api/cultureCarousel/editCarousel',
method: 'POST',
data,
})
}
/**
* 更改发布状态
*/
export const deleteCarousel = (id: number) => {
return service.request<boolean>({
url: `/api/cultureCarousel/updateRelease?id=${id}`,
method: 'POST',
})
}
import service from '@/utils/request/index'
import type {
AddOrUpdateColumnDto,
BackendColumnSearchParams,
BackendColumnListItemDto,
} from './types'
import type { BackendServicePageResult } from '@/utils/request/types'
// 后台管理栏目相关内容
/**
* 新增或者修改栏目
*/
export const addOrUpdateColumn = (data: AddOrUpdateColumnDto) => {
return service.request({
url: '/api/cultureColumn/addOrUpdate',
method: 'POST',
data,
})
}
/**
* 栏目列表
*/
export const listOfCultureColumn = (data: BackendColumnSearchParams) => {
return service.request<BackendServicePageResult<BackendColumnListItemDto>>({
url: '/api/cultureColumn/listByPage',
method: 'POST',
data,
})
}
/**
* 删除栏目设置
*/
export const deleteColumn = (ids: number[]) => {
return service.request<boolean>({
url: `/api/cultureColumn/delete`,
method: 'POST',
data: ids,
})
}
/**
* 隐藏栏目设置
*/
export const hideColumn = (ids: number[]) => {
return service.request<boolean>({
url: `/api/cultureColumn/hidden`,
method: 'POST',
data: ids,
})
}
import type { PageSearchParams } from '@/utils/request/types'
export interface BackendColumnSearchParams extends PageSearchParams {
type: string
status: number
title: string
}
export interface AddOrUpdateColumnDto {
id?: number
color: string
title: string
type: string
sort: number
}
export interface BackendColumnListItemDto {
color: string
createTime: number
createUserId: number
id: number
isDelete: number
postCount: number
sort: number
status: number
title: string
type: string
}
// 方法
export * from './tags'
export * from './carousel'
export * from './columnSettings'
// 类型
export * from './tags/types'
export * from './carousel/types'
export * from './columnSettings/types'
import service from '@/utils/request/index'
import type { AddOrUpdateTagDto, BackendTagListItemDto, BackendTagSearchParams } from './types'
import type { BackendServicePageResult } from '@/utils/request/types'
// 后台管理标签相关接口
/**
* 获取标签列表 分页
*/
export const getTagList = (params: BackendTagSearchParams) => {
return service.request<BackendServicePageResult<BackendTagListItemDto>>({
url: '/api/cultureTag/listByPage',
method: 'POST',
data: params,
})
}
/**
* 新增或者更新标签
*/
export const addOrUpdateTag = (data: AddOrUpdateTagDto) => {
return service.request({
url: '/api/cultureTag/addOrUpdate',
method: 'POST',
data,
})
}
/**
* 删除标签
*/
export const deleteTag = (id: number) => {
return service.request<boolean>({
url: `/api/cultureTag/delete?id=${id}`,
method: 'POST',
})
}
import type { PageSearchParams } from '@/utils/request/types'
export interface BackendTagSearchParams extends PageSearchParams {
title: string
type: string
}
export interface AddOrUpdateTagDto {
id?: number
color: string
description: string
title: string
type: string
}
export interface BackendTagListItemDto {
color: string
createId: number
createTime: number
description: string
id: number
imgUrl: string
style: number
title: string
type: string
}
...@@ -22,6 +22,7 @@ export interface AddOrUpdatePracticeDto { ...@@ -22,6 +22,7 @@ export interface AddOrUpdatePracticeDto {
*/ */
export interface PracticeSearchParams extends PageSearchParams { export interface PracticeSearchParams extends PageSearchParams {
sortLogic?: number sortLogic?: number
tagIdList?: number[]
} }
/** /**
......
...@@ -5,6 +5,11 @@ import type { ...@@ -5,6 +5,11 @@ import type {
AuditListSearchParams, AuditListSearchParams,
AuditListItemDto, AuditListItemDto,
AuditArticleDto, AuditArticleDto,
SelfPublishSearchParams,
SelfCollectSearchParams,
SelfCollectDetailDto,
SelfPraiseSearchParams,
SelfPraiseDetailDto,
} from './types' } from './types'
import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types' import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types'
...@@ -33,7 +38,7 @@ export const hasOfficialAccount = () => { ...@@ -33,7 +38,7 @@ export const hasOfficialAccount = () => {
/** /**
* 获取我的发布列表 * 获取我的发布列表
*/ */
export const getSelfPublishList = (data: PageSearchParams) => { export const getSelfPublishList = (data: SelfPublishSearchParams) => {
return service.request<BackendServicePageResult<SelfPublishDetailDto>>({ return service.request<BackendServicePageResult<SelfPublishDetailDto>>({
url: '/api/personalCenter/selfPublish', url: '/api/personalCenter/selfPublish',
method: 'POST', method: 'POST',
...@@ -55,16 +60,8 @@ export const getSelfDraftList = (data: PageSearchParams) => { ...@@ -55,16 +60,8 @@ export const getSelfDraftList = (data: PageSearchParams) => {
/** /**
* 获取我的收藏列表 * 获取我的收藏列表
*/ */
export const getSelfCollectList = (data: PageSearchParams) => { export const getSelfCollectList = (data: SelfCollectSearchParams) => {
return service.request< return service.request<BackendServicePageResult<SelfCollectDetailDto>>({
BackendServicePageResult<{
id: number
title: string
content: string
createTime: string
updateTime: string
}>
>({
url: '/api/personalCenter/selfCollect', url: '/api/personalCenter/selfCollect',
method: 'POST', method: 'POST',
data, data,
...@@ -74,16 +71,8 @@ export const getSelfCollectList = (data: PageSearchParams) => { ...@@ -74,16 +71,8 @@ export const getSelfCollectList = (data: PageSearchParams) => {
/** /**
* 获取我的点赞列表 * 获取我的点赞列表
*/ */
export const getSelfPraiseList = (data: PageSearchParams) => { export const getSelfPraiseList = (data: SelfPraiseSearchParams) => {
return service.request< return service.request<BackendServicePageResult<SelfPraiseDetailDto>>({
BackendServicePageResult<{
id: number
title: string
content: string
createTime: string
updateTime: string
}>
>({
url: '/api/personalCenter/selfPraise', url: '/api/personalCenter/selfPraise',
method: 'POST', method: 'POST',
data, data,
......
import { AuditStatusEnum } from '@/constants' import { ArticleTypeEnum, AuditStatusEnum } from '@/constants'
import type { PageSearchParams } from '@/utils/request/types' import type { PageSearchParams } from '@/utils/request/types'
// 我的发布 详情 export interface SelfPublishSearchParams extends PageSearchParams {
type: ArticleTypeEnum
}
// 我的发布
export interface SelfPublishDetailDto { export interface SelfPublishDetailDto {
collectionCount: number collectionCount: number
content: string content: string
...@@ -23,6 +26,55 @@ export interface SelfPublishDetailDto { ...@@ -23,6 +26,55 @@ export interface SelfPublishDetailDto {
viewCount: number viewCount: number
} }
// 我的收藏
export interface SelfCollectSearchParams extends PageSearchParams {
type: ArticleTypeEnum
}
export interface SelfCollectDetailDto {
collectionCount: number
content: string
createTime: number
createUserId: number
description: string
faceUrl: string
hasPraised: boolean
id: number
isRecommend: boolean
praiseCount: number
replyCount: number
tagNameList: string[]
title: string
type: string
viewCount: number
}
// 我的点赞
export interface SelfPraiseSearchParams extends PageSearchParams {
type: ArticleTypeEnum
}
export interface SelfPraiseDetailDto {
collectionCount: number
content: string
createTime: number
createUserId: number
description: string
faceUrl: string
hasPraised: boolean
id: number
isRecommend: boolean
isRelateColleague: boolean
praiseCount: number
releaseStatus: number
replyCount: number
tagNameList: string[]
title: string
type: string
videoUrl: string
viewCount: number
}
// 我的草稿 详情 // 我的草稿 详情
export interface SelfDraftDetailDto { export interface SelfDraftDetailDto {
id: number id: number
......
<template>
<div
ref="commentRef"
class="mt-6 bg-white backdrop-blur-sm rounded-lg shadow-sm border border-white/50 overflow-hidden"
>
<!-- 评论筛选 -->
<div class="p-4 border-b border-gray-100">
<div class="flex items-center gap-4 justify-between">
<span class="text-lg font-semibold text-gray-800">评论 ({{ total }})</span>
<div class="flex items-center gap-2">
<button
class="cursor-pointer px-3 py-1.5 text-sm bg-gradient-to-r text-gray-600 rounded-full transition-all hover:bg-gray-100"
@click="((searchParams.sortType = 2), refresh())"
:class="{
'bg-gradient-to-r from-blue-500 to-purple-500 text-white! shadow-md ':
searchParams.sortType === 2,
}"
>
最新
</button>
<button
class="cursor-pointer px-3 py-1.5 text-sm bg-gradient-to-r text-gray-600 rounded-full transition-all hover:bg-gray-100"
:class="{
'bg-gradient-to-r from-blue-500 to-purple-500 text-white! shadow-md':
searchParams.sortType === 1,
}"
@click="((searchParams.sortType = 1), refresh())"
>
最多评论
</button>
<button
class="cursor-pointer px-3 py-1.5 text-sm bg-gradient-to-r text-gray-600 rounded-full transition-all hover:bg-gray-100"
:class="{
'bg-gradient-to-r from-blue-500 to-purple-500 text-white! shadow-md':
searchParams.sortType === 4,
}"
@click="((searchParams.sortType = 4), refresh())"
>
最多点赞
</button>
<!-- <Tabs v-model="searchParams.sortType" :tabs="tabs" /> -->
</div>
</div>
</div>
<div>
<!-- 发表评论 -->
<div class="p-4 border-b border-gray-100">
<div class="flex gap-3">
<img :src="userInfo?.avatar" alt="" class="w-10 h-10 rounded-full object-cover" />
<div class="flex-1">
<div ref="commentInputRef">
<el-input
v-model="comment"
type="textarea"
placeholder="写下你的评论..."
:rows="3"
></el-input>
</div>
<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() || loading"
@click="handleComment()"
>
发表
</button>
</div>
</div>
</div>
</div>
<!-- 评论列表 -->
<div v-loading="loading" class="divide-y divide-gray-100" v-if="list.length">
<div v-for="item in list" :key="item.id">
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img :src="item.avatar" 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">{{ item.replyUser }}</span>
<span
class="px-2 py-0.5 text-xs bg-gradient-to-r from-purple-100 to-blue-100 text-purple-600 rounded-full"
>
技术专家
</span>
<span class="px-2 py-0.5 text-xs bg-red-100 text-red-600 rounded-full">置顶</span>
</div>
<!-- 换行 -->
<p class="text-gray-700 mb-3 break-all">
{{ item.content }}
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
<!-- <button class="flex items-center gap-1 hover:text-red-500 transition-colors"> -->
<div
class="flex items-center gap-1 cursor-pointer"
@click="handleLickComment(item)"
>
<el-icon
:size="16"
:style="{ color: item.hasPraise ? '#409eff' : '#606266' }"
>
<Pointer />
</el-icon>
<span>{{ item.postPriseCount }}</span>
</div>
<!-- </button> -->
<button
class="cursor-pointer hover:text-blue-500 transition-colors"
@click="handleReply(item)"
>
回复
</button>
</div>
</div>
<!-- 回复列表 -->
<div v-if="item.children.length" class="mt-3 ml-4 space-y-3">
<div
v-for="child in item.children"
:key="child.id"
class="flex gap-2 p-3 bg-gray-50 rounded-lg"
>
<img :src="child.avatar" 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">{{ child.replyUser }}</span>
<span class="text-xs text-gray-500">{{
dayjs(child.createTime * 1000).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</div>
<p class="text-sm text-gray-700">
{{ child.content }}
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>{{
dayjs(child.createTime * 1000).format('YYYY-MM-DD HH:mm:ss')
}}</span>
<div
class="flex items-center gap-1 cursor-pointer"
@click="handleLickComment(child)"
>
<el-icon
:size="16"
:style="{ color: child.hasPraise ? '#409eff' : '#606266' }"
>
<Pointer />
</el-icon>
<span>{{ child.postPriseCount }}</span>
</div>
<button
@click="handleReply(child)"
class="cursor-pointer hover:text-blue-500 transition-colors"
>
回复
</button>
</div>
</div>
</div>
</div>
</div>
<div v-show="showCommentBox(item)" 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(item.id)"
>
发表
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="px-4">
<el-divider class="my-1" />
</div>
</div>
<!-- 底部分页 -->
<!-- 靠右侧 -->
<div class="flex justify-end">
<div class="w-fit p-4">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:total="total"
@current-change="handleCurrentChange"
@size-change="changePageSize"
layout="prev, pager, next, total"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { getCommentList, addOrCancelCommentLike, addComment } from '@/api'
import { usePageSearch, useScrollTop, useHintAnimation } from '@/hooks'
import { BooleanFlag } from '@/constants'
import type { CommentItemDto } from '@/api'
import dayjs from 'dayjs'
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
const { id } = defineProps<{
id: number | string
}>()
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const commentRef = useTemplateRef<HTMLElement | null>('commentRef')
const commentInputRef = useTemplateRef<HTMLElement | null>('commentInputRef')
const { handleBackTop } = useScrollTop(commentRef)
const { triggerAnimation } = useHintAnimation(commentInputRef, {
classes: ['scale-bounce', 'highlight', 'shake-x'],
})
const { list, searchParams, goToPage, total, loading, changePageSize, refresh } = usePageSearch(
getCommentList,
{
defaultParams: {
articleId: id,
sortType: 2,
},
},
)
const handleCurrentChange = async (e: number) => {
await goToPage(e)
handleBackTop()
setTimeout(() => {
triggerAnimation()
}, 500)
}
const comment = ref('')
const handleLickComment = async (item: CommentItemDto) => {
await addOrCancelCommentLike(item.id)
if (item.hasPraise === BooleanFlag.YES) {
ElMessage.success('取消点赞成功')
item.postPriseCount--
item.hasPraise = BooleanFlag.NO
} else {
ElMessage.success('点赞成功')
item.postPriseCount++
item.hasPraise = BooleanFlag.YES
}
}
const currentParentCommentId = ref(0)
const currentSonCommentId = ref(0)
const handleReply = (item: CommentItemDto) => {
if (item.pid) {
// 点击的是子评论
if (currentSonCommentId.value) {
// 置为空
if (currentSonCommentId.value !== item.id) {
currentSonCommentId.value = item.id
currentParentCommentId.value = item.pid
} else {
currentSonCommentId.value = 0
currentParentCommentId.value = 0
}
} else {
currentSonCommentId.value = item.id
currentParentCommentId.value = item.pid
}
} else {
// 点击的是父评论
if (currentParentCommentId.value) {
// 置为空
if (currentParentCommentId.value !== item.id) {
currentParentCommentId.value = item.id
} else {
currentParentCommentId.value = 0
}
} else {
currentParentCommentId.value = item.id
currentSonCommentId.value = 0
}
}
comment.value = ''
console.log('parent', currentParentCommentId.value, 'son', currentSonCommentId.value)
}
const showCommentBox = (item: CommentItemDto) => {
if (currentParentCommentId.value && currentSonCommentId.value) {
// 说明在评论子评论
return (
item.id === currentParentCommentId.value &&
item.children?.some((i) => i.id === currentSonCommentId.value)
)
} else if (currentParentCommentId.value) {
// 说明在评论父评论
return item.id === currentParentCommentId.value
}
}
const handleComment = async (pid?: number) => {
console.log(comment.value)
const res = await addComment({
articleId: id,
content: comment.value,
...(pid ? { pid } : {}),
})
console.log(res)
ElMessage.success('发表评论成功')
refresh()
comment.value = ''
}
defineExpose({
scrollToCommentBox: () => handleBackTop(),
})
</script>
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
:multiple="multiple" :multiple="multiple"
:limit="limit" :limit="limit"
:disabled="hasReachedLimit && !multiple" :disabled="hasReachedLimit && !multiple"
class="custom-upload"
> >
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
</el-upload> </el-upload>
...@@ -97,11 +98,7 @@ watch( ...@@ -97,11 +98,7 @@ watch(
{ deep: true }, { deep: true },
) )
/**
* 处理文件变化(上传)- 修复版本
*/
const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => { const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => {
// 检查是否超出限制
if (uploadFiles.length > props.limit) { if (uploadFiles.length > props.limit) {
ElMessage.error(`最多上传 ${props.limit} 个文件`) ElMessage.error(`最多上传 ${props.limit} 个文件`)
const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid) const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid)
...@@ -111,28 +108,23 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => ...@@ -111,28 +108,23 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) =>
return return
} }
// 如果是新上传的文件
if (uploadFile.raw && uploadFile.status === 'ready') { if (uploadFile.raw && uploadFile.status === 'ready') {
// 保存 uid 用于后续查找
const uid = uploadFile.uid const uid = uploadFile.uid
try { try {
// 更新状态为上传中(第一次查找)
let fileIndex = fileList.value.findIndex((file) => file.uid === uid) let fileIndex = fileList.value.findIndex((file) => file.uid === uid)
if (fileIndex !== -1) { if (fileIndex !== -1) {
fileList.value[fileIndex].status = 'uploading' fileList.value[fileIndex].status = 'uploading'
} }
// 上传文件
const { data } = await uploadFileApi(uploadFile.raw) const { data } = await uploadFileApi(uploadFile.raw)
console.log('data', data) console.log('data', data)
const url = data.fileUrl || data.data[0].filePath const url = data.fileUrl || data.data[0].filePath
const name = data.fileName || data.data[0].finalName const name = data.fileName || data.data[0].finalName
// ✅ 上传完成后重新查找索引(第二次查找)
fileIndex = fileList.value.findIndex((file) => file.uid === uid) fileIndex = fileList.value.findIndex((file) => file.uid === uid)
if (fileIndex !== -1) { if (fileIndex !== -1) {
// 更新文件信息
fileList.value[fileIndex] = { fileList.value[fileIndex] = {
...fileList.value[fileIndex], ...fileList.value[fileIndex],
url, url,
...@@ -147,7 +139,6 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => ...@@ -147,7 +139,6 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) =>
console.error('上传失败:', error) console.error('上传失败:', error)
ElMessage.error('上传失败,请重试') ElMessage.error('上传失败,请重试')
// 移除上传失败的文件
const fileIndex = fileList.value.findIndex((file) => file.uid === uid) const fileIndex = fileList.value.findIndex((file) => file.uid === uid)
if (fileIndex !== -1) { if (fileIndex !== -1) {
fileList.value.splice(fileIndex, 1) fileList.value.splice(fileIndex, 1)
...@@ -189,4 +180,48 @@ defineExpose({ ...@@ -189,4 +180,48 @@ defineExpose({
}) })
</script> </script>
<style scoped></style> <style scoped>
/* 方案1: 适中尺寸(推荐) */
.custom-upload :deep(.el-upload--picture-card) {
width: 100px;
height: 100px;
line-height: 100px;
}
.custom-upload :deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 100px;
height: 100px;
}
/* 方案2: 更小尺寸 */
/* .custom-upload :deep(.el-upload--picture-card) {
width: 80px;
height: 80px;
line-height: 80px;
}
.custom-upload :deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 80px;
height: 80px;
}
.custom-upload :deep(.el-icon) {
font-size: 20px;
} */
/* 方案3: 迷你尺寸 */
/* .custom-upload :deep(.el-upload--picture-card) {
width: 60px;
height: 60px;
line-height: 60px;
}
.custom-upload :deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 60px;
height: 60px;
}
.custom-upload :deep(.el-icon) {
font-size: 16px;
} */
</style>
...@@ -77,7 +77,7 @@ export function usePageSearch< ...@@ -77,7 +77,7 @@ export function usePageSearch<
list.value = data.list || [] list.value = data.list || []
total.value = data.total || 0 total.value = data.total || 0
} catch (error) { } catch (error) {
console.error('分页搜索失败:', error) console.log('分页搜索失败:', error)
list.value = [] list.value = []
total.value = 0 total.value = 0
} finally { } finally {
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<!-- 时长显示卡片 --> <!-- 时长显示卡片 -->
<div <div
class="w-24 md:w-28 lg:w-32 h-16 md:h-18 lg:h-20 rounded-2xl bg-gradient-to-br from-white via-blue-50 to-purple-50 shadow-lg hover:shadow-xl border border-white/50 flex flex-col justify-center items-center transition-all duration-300 ease-out hover:scale-105 hover:-translate-y-1 backdrop-blur-sm relative overflow-hidden group transform-gpu backface-hidden will-change-transform" class="w-24 md:w-28 lg:w-32 h-16 md:h-18 lg:h-20 rounded-lg bg-gradient-to-br from-white via-blue-50 to-purple-50 shadow-lg hover:shadow-xl border border-white/50 flex flex-col justify-center items-center transition-all duration-300 ease-out hover:scale-105 hover:-translate-y-1 backdrop-blur-sm relative overflow-hidden group transform-gpu backface-hidden will-change-transform"
> >
<!-- 其他内容保持不变 --> <!-- 其他内容保持不变 -->
<div <div
......
...@@ -96,6 +96,56 @@ const routes = [ ...@@ -96,6 +96,56 @@ const routes = [
name: 'Test', name: 'Test',
component: () => import('@/test.vue'), component: () => import('@/test.vue'),
}, },
{
path: '/backend',
name: 'Backend',
component: () => import('@/views/backend/index.vue'),
redirect: '/backend/manager',
children: [
{
path: 'manager',
name: 'ManagerManagement',
component: () => import('@/views/backend/manager/index.vue'),
meta: { title: '企业文化管理员' },
},
{
path: 'tags',
name: 'OfficialManagement',
component: () => import('@/views/backend/tags/index.vue'),
meta: { title: '官方标签' },
},
{
path: 'carousel',
name: 'CarouselManagement',
component: () => import('@/views/backend/carousel/index.vue'),
meta: { title: '轮播图设置' },
},
// {
// path: 'topic-admin',
// name: 'TopicAdminManagement',
// component: () => import('@/views/backend/topic-admin/index.vue'),
// meta: { title: '专栏——管理员' },
// },
{
path: 'columnSettings',
name: 'ColumnSettingsManagement',
component: () => import('@/views/backend/columnSettings/index.vue'),
meta: { title: '专栏——栏目管理' },
},
// {
// path: 'interview-admin',
// name: 'InterviewAdminManagement',
// component: () => import('@/views/backend/interview-admin/index.vue'),
// meta: { title: '专访——管理员' },
// },
{
path: 'interviewSettings',
name: 'InterviewSettingsManagement',
component: () => import('@/views/backend/interviewSettings/index.vue'),
meta: { title: '专访——栏目管理' },
},
],
},
] ]
const scrollPositionMap = new Map<string, number>() const scrollPositionMap = new Map<string, number>()
......
<!-- views/backend/carousel/index.vue -->
<template>
<div class="carousel-page">
<!-- 顶部操作栏 -->
<div class="action-section">
<el-button type="primary" :icon="Search" @click="getList">搜索</el-button>
<el-button type="primary" class="add-btn" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
</div>
<!-- 表格区域 -->
<div class="table-section">
<el-table v-loading="loading" :data="list">
<el-table-column type="selection" width="55" />
<el-table-column prop="image" label="跳转路径" min-width="300">
<template #default="{ row }">
<div class="image-cell">
<el-image
:src="row.assetUrl"
fit="cover"
class="carousel-image"
:preview-src-list="[row.assetUrl]"
/>
</div>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="100" align="center" />
<el-table-column prop="type" label="类型" width="120" align="center">
<template #default="{ row }">
<el-tag :type="row.type === 0 ? 'success' : 'primary'">
{{ row.type === 0 ? '图片' : '视频' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator" label="操作人/时间" width="200">
<template #default="{ row }">
<div class="creator-info">
<div>{{ row.creator }}</div>
<div class="create-time">{{ row.createTime }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
:close-on-click-modal="false"
>
<el-alert
title="以下网址请仔细核对是否正确,若输入错误,会导致跳转错误页面,或者资源加载不出来"
type="info"
:closable="false"
style="margin-bottom: 20px"
/>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
<el-form-item label="轮播类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio :value="0">图片</el-radio>
<el-radio value="1">视频</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="显示资源" prop="image">
<div class="upload-section">
<UploadFile v-model="form.assetUrl" />
<div class="upload-hint">上传图片,推荐比例为 W/H: 1280 / 480</div>
</div>
</el-form-item>
<el-form-item label="跳转链接" prop="link">
<el-input
v-model="form.link"
placeholder="不填表示点击不跳转"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="form.sort"
:min="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">
<el-icon class="btn-icon"><Upload /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Search, Plus, Upload } from '@element-plus/icons-vue'
import { useResetData } from '@/hooks'
import { getCarouselList, addCarousel, editCarousel } from '@/api/backend'
import type { FormInstance, FormRules } from 'element-plus'
import UploadFile from '@/components/common/UploadFile/index.vue'
import type { CarouselListItemDto, AddOrUpdateCarouselDto } from '@/api/backend'
// 列表数据
const loading = ref(false)
const list = ref<CarouselListItemDto[]>([])
// 获取列表
const getList = async () => {
loading.value = true
try {
const res = await getCarouselList()
list.value = res.data
} finally {
loading.value = false
}
}
// 对话框
const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑轮播' : '新增轮播'))
const formRef = ref<FormInstance>()
// 表单数据
const [form, resetForm] = useResetData<AddOrUpdateCarouselDto>({
type: 0,
assetUrl: '',
link: '',
sort: 0,
id: undefined,
isRelease: 1,
})
// 表单验证规则
const formRules: FormRules = {
type: [{ required: true, message: '请选择轮播类型', trigger: 'change' }],
assetUrl: [{ required: true, message: '请上传资源', trigger: 'change' }],
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
}
// 新增
const handleAdd = () => {
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: CarouselListItemDto) => {
resetForm()
form.value = {
type: row.type,
assetUrl: row.assetUrl,
link: row.link,
sort: row.sort,
id: row.id,
isRelease: row.isRelease,
}
dialogVisible.value = true
}
// 删除
const handleDelete = async (row: CarouselListItemDto) => {
try {
await ElMessageBox.confirm('确定要删除该轮播吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await editCarousel({
id: row.id,
isRelease: 0,
})
ElMessage.success('删除成功')
getList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (form.value.id) {
await editCarousel(form.value)
} else {
await addCarousel(form.value)
}
ElMessage.success(form.value.id ? '编辑成功' : '新增成功')
dialogVisible.value = false
getList()
} catch (error) {
console.error('表单验证失败:', error)
}
}
// 初始化
onMounted(() => {
getList()
})
</script>
<style scoped lang="scss">
.carousel-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
// 操作区域
.action-section {
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
gap: 12px;
flex-shrink: 0;
.add-btn {
margin-left: auto;
}
}
// 表格区域
.table-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
min-height: 0;
overflow: auto;
}
// 图片单元格
.image-cell {
padding: 8px 0;
.carousel-image {
width: 240px;
height: 90px;
border-radius: 4px;
cursor: pointer;
}
}
// 操作人信息
.creator-info {
.create-time {
color: #909399;
font-size: 12px;
margin-top: 4px;
}
}
// 上传区域
.upload-section {
width: 100%;
.upload-hint {
color: #409eff;
font-size: 12px;
margin-top: 8px;
}
}
.btn-icon {
margin-right: 4px;
}
</style>
<!-- views/backend/official/index.vue -->
<template>
<div class="official-tag-page">
<!-- 搜索栏 -->
<div class="search-section">
<div class="flex-1 flex gap-2">
<el-input
v-model="searchParams.title"
placeholder="请输入栏目标题"
class="w-200px"
></el-input>
<el-select
v-model="searchParams.status"
placeholder="请选择发布状态"
class="search-select"
clearable
>
<el-option label="发布" :value="1" />
<el-option label="隐藏" :value="0" />
</el-select>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div>
<div class="flex justify-end">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
<el-button type="primary" @click="handleBatchPublish"> 批量发布/隐藏 </el-button>
<el-button type="danger" @click="handleBatchDelete"> 批量删除 </el-button>
</div>
</div>
<!-- 表格区域 -->
<div class="table-section">
<!-- 表格 -->
<div class="table-wrapper">
<el-table
v-loading="loading"
:data="list"
height="100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="sort" label="栏目顺序" width="180"> </el-table-column>
<el-table-column prop="title" label="栏目名称" min-width="200" />
<el-table-column prop="postCount" label="栏目帖子数量" min-width="200" />
<el-table-column prop="color" label="颜色" width="300">
<template #default="{ row }">
<div class="color-cell">
<div class="color-block" :style="{ backgroundColor: row.color }" />
<span class="color-text">{{ row.color }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="createUserId" label="创建人" min-width="200" />
<el-table-column prop="createTime" label="创建时间" min-width="200">
<template #default="{ row }">
{{ dayjs(row.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="200">
<template #default="{ row }">
<el-switch
:model-value="row.status"
:active-value="1"
:inactive-value="0"
@change="handleStatusChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:total="total"
:page-sizes="[10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="changePageSize"
@current-change="goToPage"
/>
</div>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="form.color" />
<span class="color-value">{{ form.color }}</span>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="100" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">
<el-icon class="btn-icon"><Upload /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Search, Plus, Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks'
import { listOfCultureColumn, addOrUpdateColumn, deleteColumn, hideColumn } from '@/api/backend'
import type { FormInstance, FormRules } from 'element-plus'
import type { BackendColumnListItemDto, AddOrUpdateColumnDto } from '@/api/backend'
import dayjs from 'dayjs'
const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } =
usePageSearch(listOfCultureColumn, {
defaultParams: {
type: 'column',
},
})
// 对话框
const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑标签' : '新增标签'))
const formRef = ref<FormInstance>()
// 表单数据
const [form, resetForm] = useResetData<AddOrUpdateColumnDto>({
title: '',
color: '#000000',
id: undefined,
sort: 0,
type: 'column',
})
// 表单验证规则
const formRules: FormRules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
color: [{ required: true, message: '请选择颜色', trigger: 'change' }],
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
}
// 新增
const handleAdd = () => {
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: BackendColumnListItemDto) => {
resetForm()
form.value = {
title: row.title,
color: row.color,
id: row.id,
sort: row.sort,
type: 'column',
}
dialogVisible.value = true
}
// 状态改变
const handleStatusChange = async (row: BackendColumnListItemDto) => {
await hideColumn([row.id])
refresh()
}
// 删除
const handleDelete = async (row: BackendColumnListItemDto) => {
try {
await ElMessageBox.confirm(`确定要删除标签"${row.title}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await deleteColumn([row.id])
ElMessage.success('删除成功')
refresh()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (form.value.id) {
await addOrUpdateColumn(form.value)
} else {
await addOrUpdateColumn(form.value)
}
ElMessage.success(form.value.id ? '编辑成功' : '新增成功')
dialogVisible.value = false
if (form.value.id) {
search()
} else {
refresh()
}
} catch (error) {
console.error('表单验证失败:', error)
}
}
const selectedRows = ref<BackendColumnListItemDto[]>([])
// 选择
const handleSelectionChange = (selection: BackendColumnListItemDto[]) => {
selectedRows.value = selection
}
// 批量发布/隐藏
const handleBatchPublish = async () => {
await hideColumn(selectedRows.value.map((item) => item.id))
refresh()
selectedRows.value = []
ElMessage.success('发布/隐藏成功')
}
// 批量删除
const handleBatchDelete = async () => {
await deleteColumn(selectedRows.value.map((item) => item.id))
refresh()
selectedRows.value = []
ElMessage.success('删除成功')
}
</script>
<style scoped lang="scss">
.official-tag-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
// 搜索区域
.search-section {
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
gap: 12px;
flex-shrink: 0;
.search-select {
width: 200px;
}
}
// 表格区域
.table-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
min-height: 0;
}
.table-wrapper {
flex: 1;
min-height: 0;
.color-cell {
display: flex;
align-items: center;
gap: 12px;
.color-block {
width: 100%;
height: 36px;
border-radius: 4px;
border: 1px solid #e5e7eb;
flex: 1;
}
.color-text {
color: #fff;
font-size: 14px;
font-weight: 500;
position: absolute;
left: 50%;
transform: translateX(-50%);
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding-top: 16px;
flex-shrink: 0;
}
// 对话框内的颜色显示
.color-value {
margin-left: 12px;
color: #606266;
font-family: monospace;
}
.btn-icon {
margin-right: 4px;
}
</style>
<!-- views/backend/index.vue -->
<template>
<div class="backend-layout">
<!-- 侧边栏 -->
<aside class="backend-sidebar">
<div class="sidebar-header">
<img src="/webicon.png" alt="SOUNDASIA" class="logo" />
<span class="header-title">企业文化后台管理</span>
</div>
<el-menu :default-active="activeMenu" class="sidebar-menu" @select="handleMenuSelect">
<el-menu-item v-for="item in menuList" :key="item.path" :index="item.path">
<span>{{ item.title }}</span>
</el-menu-item>
</el-menu>
</aside>
<!-- 右侧主内容区 -->
<div class="backend-content">
<!-- 顶部标题栏 -->
<header class="content-header">
<div class="header-left">
<img src="/webicon.png" alt="" class="header-icon" />
<h1 class="header-title">{{ currentTitle }}</h1>
</div>
<div class="header-right">
<!-- 可以放置用户信息、退出登录等 -->
</div>
</header>
<!-- 内容区 -->
<main class="content-main">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</main>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
interface MenuItem {
path: string
title: string
}
const menuList: MenuItem[] = [
{ path: '/backend/manager', title: '企业文化管理员' },
{ path: '/backend/tags', title: '官方标签' },
{ path: '/backend/carousel', title: '轮播图设置' },
{ path: '/backend/topic-admin', title: '专栏——管理员' },
{ path: '/backend/columnSettings', title: '专栏——栏目管理' },
{ path: '/backend/interview-admin', title: '专访——管理员' },
{ path: '/backend/interviewSettings', title: '专访——栏目管理' },
]
const activeMenu = computed(() => route.path)
const currentTitle = computed(() => {
const current = menuList.find((item) => item.path === route.path)
return current?.title || ''
})
const handleMenuSelect = (path: string) => {
router.push(path)
}
</script>
<style scoped lang="scss">
.backend-layout {
display: flex;
height: 100vh;
overflow: hidden;
}
.backend-sidebar {
width: 200px;
background: #fff;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
.sidebar-header {
height: 70px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
.logo {
height: 32px;
width: 32px;
}
.header-title {
font-size: 14px;
color: #333;
font-weight: 500;
}
}
.sidebar-menu {
flex: 1;
border-right: none;
:deep(.el-menu-item) {
height: 48px;
line-height: 48px;
font-size: 14px;
&:hover {
background: #f5f7fa;
}
&.is-active {
background: #e6f4ff;
color: #409eff;
border-right: 3px solid #409eff;
}
}
}
}
.backend-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: #f5f7fa;
}
.content-header {
height: 60px;
background: #fff;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
z-index: 10;
.header-left {
display: flex;
align-items: center;
gap: 12px;
.header-icon {
width: 32px;
height: 32px;
}
.header-title {
font-size: 18px;
font-weight: 500;
color: #333;
margin: 0;
}
}
}
.content-main {
flex: 1;
overflow: auto;
padding: 20px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<!-- views/backend/official/index.vue -->
<template>
<div class="official-tag-page">
<!-- 搜索栏 -->
<div class="search-section">
<div class="flex-1 flex gap-2">
<el-input
v-model="searchParams.title"
placeholder="请输入栏目标题"
class="w-200px"
></el-input>
<el-select
v-model="searchParams.status"
placeholder="请选择发布状态"
class="search-select"
clearable
>
<el-option label="发布" :value="1" />
<el-option label="隐藏" :value="0" />
</el-select>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div>
<div class="flex justify-end">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
<el-button type="primary" @click="handleBatchPublish"> 批量发布/隐藏 </el-button>
<el-button type="danger" @click="handleBatchDelete"> 批量删除 </el-button>
</div>
</div>
<!-- 表格区域 -->
<div class="table-section">
<!-- 表格 -->
<div class="table-wrapper">
<el-table
v-loading="loading"
:data="list"
height="100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="sort" label="栏目顺序" width="180"> </el-table-column>
<el-table-column prop="title" label="栏目名称" min-width="200" />
<el-table-column prop="postCount" label="栏目帖子数量" min-width="200" />
<el-table-column prop="color" label="颜色" width="300">
<template #default="{ row }">
<div class="color-cell">
<div class="color-block" :style="{ backgroundColor: row.color }" />
<span class="color-text">{{ row.color }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="createUserId" label="创建人" min-width="200" />
<el-table-column prop="createTime" label="创建时间" min-width="200">
<template #default="{ row }">
{{ dayjs(row.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="200">
<template #default="{ row }">
<el-switch
:model-value="row.status"
:active-value="1"
:inactive-value="0"
@change="handleStatusChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:total="total"
:page-sizes="[10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="changePageSize"
@current-change="goToPage"
/>
</div>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="form.color" />
<span class="color-value">{{ form.color }}</span>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="100" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">
<el-icon class="btn-icon"><Upload /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Search, Plus, Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks'
import { listOfCultureColumn, addOrUpdateColumn, deleteColumn, hideColumn } from '@/api/backend'
import type { FormInstance, FormRules } from 'element-plus'
import type { BackendColumnListItemDto, AddOrUpdateColumnDto } from '@/api/backend'
import dayjs from 'dayjs'
const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } =
usePageSearch(listOfCultureColumn, {
defaultParams: {
type: 'interview',
},
})
// 对话框
const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑标签' : '新增标签'))
const formRef = ref<FormInstance>()
// 表单数据
const [form, resetForm] = useResetData<AddOrUpdateColumnDto>({
title: '',
color: '#000000',
id: undefined,
sort: 0,
type: 'interview',
})
// 表单验证规则
const formRules: FormRules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
color: [{ required: true, message: '请选择颜色', trigger: 'change' }],
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
}
// 新增
const handleAdd = () => {
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: BackendColumnListItemDto) => {
resetForm()
form.value = {
title: row.title,
color: row.color,
id: row.id,
sort: row.sort,
type: 'interview',
}
dialogVisible.value = true
}
// 状态改变
const handleStatusChange = async (row: BackendColumnListItemDto) => {
await hideColumn([row.id])
refresh()
}
// 删除
const handleDelete = async (row: BackendColumnListItemDto) => {
try {
await ElMessageBox.confirm(`确定要删除标签"${row.title}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await deleteColumn([row.id])
ElMessage.success('删除成功')
refresh()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (form.value.id) {
await addOrUpdateColumn(form.value)
} else {
await addOrUpdateColumn(form.value)
}
ElMessage.success(form.value.id ? '编辑成功' : '新增成功')
dialogVisible.value = false
if (form.value.id) {
search()
} else {
refresh()
}
} catch (error) {
console.error('表单验证失败:', error)
}
}
const selectedRows = ref<BackendColumnListItemDto[]>([])
// 选择
const handleSelectionChange = (selection: BackendColumnListItemDto[]) => {
selectedRows.value = selection
}
// 批量发布/隐藏
const handleBatchPublish = async () => {
await hideColumn(selectedRows.value.map((item) => item.id))
refresh()
selectedRows.value = []
ElMessage.success('发布/隐藏成功')
}
// 批量删除
const handleBatchDelete = async () => {
await deleteColumn(selectedRows.value.map((item) => item.id))
refresh()
selectedRows.value = []
ElMessage.success('删除成功')
}
</script>
<style scoped lang="scss">
.official-tag-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
// 搜索区域
.search-section {
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
gap: 12px;
flex-shrink: 0;
.search-select {
width: 200px;
}
}
// 表格区域
.table-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
min-height: 0;
}
.table-wrapper {
flex: 1;
min-height: 0;
.color-cell {
display: flex;
align-items: center;
gap: 12px;
.color-block {
width: 100%;
height: 36px;
border-radius: 4px;
border: 1px solid #e5e7eb;
flex: 1;
}
.color-text {
color: #fff;
font-size: 14px;
font-weight: 500;
position: absolute;
left: 50%;
transform: translateX(-50%);
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding-top: 16px;
flex-shrink: 0;
}
// 对话框内的颜色显示
.color-value {
margin-left: 12px;
color: #606266;
font-family: monospace;
}
.btn-icon {
margin-right: 4px;
}
</style>
<!-- views/backend/topic-target/index.vue -->
<template>
<div class="topic-target-page">
<!-- 搜索栏 -->
<div class="search-section">
<el-input
v-model="searchForm.name"
placeholder="请输入栏目板块"
class="search-input"
clearable
/>
<el-select v-model="searchForm.status" placeholder="请选择" class="search-select" clearable>
<el-option label="全部" value="" />
<el-option label="已发布" value="published" />
<el-option label="未发布" value="unpublished" />
</el-select>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<!-- 表格区域 -->
<div class="table-section">
<!-- 操作按钮 -->
<div class="action-bar">
<el-button type="primary" @click="handleAdd">新增</el-button>
<el-button @click="handleBatchPublish">批量发布/隐藏</el-button>
<el-button type="danger" @click="handleBatchDelete">批量删除</el-button>
</div>
<!-- 表格 -->
<div class="table-wrapper">
<el-table
v-loading="loading"
:data="tableData"
height="100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="order" label="栏目顺序" width="100" />
<el-table-column prop="name" label="栏目名称" min-width="150" />
<el-table-column prop="color" label="颜色" width="150">
<template #default="{ row }">
<div class="color-cell">
<div class="color-block" :style="{ backgroundColor: row.color }" />
<span>{{ row.color }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="count" label="栏目标签数量" width="120" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column prop="creator" label="创建人" width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-switch
v-model="row.published"
active-text="发布"
inactive-text="隐藏"
@change="handleStatusChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:total="pagination.total"
:page-sizes="[20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="栏目名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入栏目名称" />
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="formData.color" />
</el-form-item>
<el-form-item label="顺序" prop="order">
<el-input-number v-model="formData.order" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
interface SearchForm {
name: string
status: string
}
interface TableRow {
id: number
order: number
name: string
color: string
count: number
createTime: string
creator: string
published: boolean
}
interface FormData {
id?: number
name: string
color: string
order: number
}
interface Pagination {
page: number
size: number
total: number
}
// 搜索表单
const searchForm = reactive<SearchForm>({
name: '',
status: '',
})
// 表格数据
const loading = ref(false)
const tableData = ref<TableRow[]>([])
const selectedRows = ref<TableRow[]>([])
// 分页
const pagination = reactive<Pagination>({
page: 1,
size: 20,
total: 0,
})
// 对话框
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const formData = reactive<FormData>({
name: '',
color: '#88FFCB',
order: 0,
})
const dialogTitle = computed(() => {
return dialogType.value === 'add' ? '新增栏目' : '编辑栏目'
})
const formRules: FormRules = {
name: [{ required: true, message: '请输入栏目名称', trigger: 'blur' }],
color: [{ required: true, message: '请选择颜色', trigger: 'change' }],
order: [{ required: true, message: '请输入顺序', trigger: 'blur' }],
}
// 搜索
const handleSearch = () => {
pagination.page = 1
fetchData()
}
// 重置
const handleReset = () => {
searchForm.name = ''
searchForm.status = ''
handleSearch()
}
// 新增
const handleAdd = () => {
dialogType.value = 'add'
Object.assign(formData, {
name: '',
color: '#88FFCB',
order: 0,
})
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: TableRow) => {
dialogType.value = 'edit'
Object.assign(formData, {
id: row.id,
name: row.name,
color: row.color,
order: row.order,
})
dialogVisible.value = true
}
// 删除
const handleDelete = async (row: TableRow) => {
try {
await ElMessageBox.confirm('确定要删除该栏目吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
// TODO: 调用删除接口
ElMessage.success('删除成功')
fetchData()
} catch {
// 取消删除
}
}
// 批量删除
const handleBatchDelete = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请选择要删除的数据')
return
}
try {
await ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 条数据吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
// TODO: 调用批量删除接口
ElMessage.success('删除成功')
fetchData()
} catch {
// 取消删除
}
}
// 批量发布/隐藏
const handleBatchPublish = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请选择要操作的数据')
return
}
// TODO: 调用批量发布接口
ElMessage.success('操作成功')
fetchData()
}
// 状态切换
const handleStatusChange = (row: TableRow) => {
// TODO: 调用状态更新接口
ElMessage.success(row.published ? '已发布' : '已隐藏')
}
// 表格选择
const handleSelectionChange = (rows: TableRow[]) => {
selectedRows.value = rows
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
// TODO: 调用新增/编辑接口
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '编辑成功')
dialogVisible.value = false
fetchData()
}
})
}
// 分页变化
const handlePageChange = (page: number) => {
pagination.page = page
fetchData()
}
const handleSizeChange = (size: number) => {
pagination.size = size
pagination.page = 1
fetchData()
}
// 获取数据
const fetchData = async () => {
loading.value = true
try {
// TODO: 调用接口获取数据
// 模拟数据
tableData.value = [
{
id: 123,
order: 123,
name: '专栏标签5',
color: '#88FFCB',
count: 1,
createTime: '2025-11-20 14:15:07',
creator: '178126911664085000',
published: true,
},
{
id: 8,
order: 8,
name: '专栏标签4',
color: '#9878FF',
count: 1,
createTime: '2025-11-20 14:14:49',
creator: '178126911664085000',
published: true,
},
{
id: 0,
order: 0,
name: '专栏标签3',
color: '#f0fdf4',
count: 4,
createTime: '2025-11-10 18:05:37',
creator: '178126911664085000',
published: true,
},
{
id: 11,
order: 11,
name: '专栏标签2',
color: '#fdf2f8',
count: 0,
createTime: '2025-11-10 17:46:40',
creator: '178126911664085000',
published: true,
},
{
id: 3,
order: 3,
name: '专栏标签1',
color: '#eff6ff',
count: 3,
createTime: '2025-11-10 17:46:30',
creator: '178126911664085000',
published: true,
},
]
pagination.total = 5
} catch (error) {
ElMessage.error('获取数据失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchData()
})
</script>
<style scoped lang="scss">
.topic-target-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
// 搜索区域
.search-section {
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
gap: 12px;
flex-shrink: 0;
.search-input {
width: 240px;
}
.search-select {
width: 160px;
}
}
// 表格区域 - 占满剩余高度
.table-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
min-height: 0; // 重要:允许flex子元素收缩
}
.action-bar {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-shrink: 0;
}
// 表格包裹器 - 自适应高度
.table-wrapper {
flex: 1;
min-height: 0; // 重要:允许flex子元素收缩
.color-cell {
display: flex;
align-items: center;
gap: 8px;
.color-block {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid #e5e7eb;
}
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding-top: 16px;
flex-shrink: 0;
}
</style>
<!-- views/backend/official/index.vue -->
<template>
<div class="official-tag-page">
<!-- 搜索栏 -->
<div class="search-section">
<el-select
v-model="searchParams.type"
placeholder="请选择类型"
class="search-select"
clearable
>
<el-option label="文化关键词" value="culture" />
<el-option label="年度主推关键词" value="year_recommend" />
<el-option label="关联场景" value="related_scenarios" />
</el-select>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="primary" class="add-btn" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
</div>
<!-- 表格区域 -->
<div class="table-section">
<!-- 表格 -->
<div class="table-wrapper">
<el-table v-loading="loading" :data="list" height="100%">
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="type" label="类型" width="180">
<template #default="{ row }">
{{
row.type === 'culture'
? '文化关键词'
: row.type === 'year_recommend'
? '年度主推关键词'
: '关联场景'
}}
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="200" />
<el-table-column prop="description" label="描述" min-width="200" />
<el-table-column prop="color" label="颜色" width="300">
<template #default="{ row }">
<div class="color-cell">
<div class="color-block" :style="{ backgroundColor: row.color }" />
<span class="color-text">{{ row.color }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:total="total"
:page-sizes="[10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="changePageSize"
@current-change="goToPage"
/>
</div>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
<el-form-item label="类型" prop="type">
<el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
<el-option label="文化关键词" value="culture" />
<el-option label="年度主推关键词" value="year_recommend" />
<el-option label="关联场景" value="related_scenarios" />
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" placeholder="请输入描述" />
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="form.color" />
<span class="color-value">{{ form.color }}</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">
<el-icon class="btn-icon"><Upload /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Search, Plus, Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks'
import { getTagList, addOrUpdateTag, deleteTag } from '@/api/backend'
import type { FormInstance, FormRules } from 'element-plus'
import type { BackendTagListItemDto, AddOrUpdateTagDto } from '@/api/backend'
const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } =
usePageSearch(getTagList)
// 对话框
const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑标签' : '新增标签'))
const formRef = ref<FormInstance>()
// 表单数据
const [form, resetForm] = useResetData<AddOrUpdateTagDto>({
type: '',
title: '',
description: '',
color: '#000000',
id: undefined,
})
// 表单验证规则
const formRules: FormRules = {
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
description: [{ required: true, message: '请输入描述', trigger: 'blur' }],
color: [{ required: true, message: '请选择颜色', trigger: 'change' }],
}
// 新增
const handleAdd = () => {
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: BackendTagListItemDto) => {
resetForm()
form.value = {
type: row.type,
title: row.title,
description: row.description,
color: row.color,
id: row.id,
}
dialogVisible.value = true
}
// 删除
const handleDelete = async (row: BackendTagListItemDto) => {
try {
await ElMessageBox.confirm(`确定要删除标签"${row.title}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await deleteTag(row.id)
ElMessage.success('删除成功')
refresh()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (form.value.id) {
await addOrUpdateTag(form.value)
} else {
await addOrUpdateTag(form.value)
}
ElMessage.success(form.value.id ? '编辑成功' : '新增成功')
dialogVisible.value = false
if (form.value.id) {
search()
} else {
refresh()
}
} catch (error) {
console.error('表单验证失败:', error)
}
}
</script>
<style scoped lang="scss">
.official-tag-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
// 搜索区域
.search-section {
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
gap: 12px;
flex-shrink: 0;
.search-select {
width: 200px;
}
.add-btn {
margin-left: auto;
}
}
// 表格区域
.table-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
min-height: 0;
}
.table-wrapper {
flex: 1;
min-height: 0;
.color-cell {
display: flex;
align-items: center;
gap: 12px;
.color-block {
width: 100%;
height: 36px;
border-radius: 4px;
border: 1px solid #e5e7eb;
flex: 1;
}
.color-text {
color: #fff;
font-size: 14px;
font-weight: 500;
position: absolute;
left: 50%;
transform: translateX(-50%);
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding-top: 16px;
flex-shrink: 0;
}
// 对话框内的颜色显示
.color-value {
margin-left: 12px;
color: #606266;
font-family: monospace;
}
.btn-icon {
margin-right: 4px;
}
</style>
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
<div class="mx-auto py-6"> <div class="mx-auto py-6">
<!-- 发布问题卡片 --> <!-- 发布问题卡片 -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-6 overflow-hidden"> <div class="bg-white rounded-lg shadow-sm border border-gray-100 mb-6 overflow-hidden">
<div class="p-6"> <div class="p-6">
<!-- 用户头像和输入区域 --> <!-- 用户头像和输入区域 -->
<div class="flex items-start"> <div class="flex items-start">
...@@ -118,7 +118,7 @@ ...@@ -118,7 +118,7 @@
<el-card <el-card
v-for="(question, index) in questions" v-for="(question, index) in questions"
:key="index" :key="index"
class="question-card !rounded-2xl mb-4" class="question-card !rounded-lg mb-4"
shadow="hover" shadow="hover"
> >
<!-- 问题标题 --> <!-- 问题标题 -->
...@@ -239,7 +239,7 @@ ...@@ -239,7 +239,7 @@
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
<div class="right"> <div class="right">
<div <div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3" class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
> >
<el-pagination <el-pagination
v-model:current-page="currentPage" v-model:current-page="currentPage"
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
v-for="item in list" v-for="item in list"
:key="item.id" :key="item.id"
class="group bg-white rounded-lg p-4 sm:p-6 cursor-pointer transition-all duration-300 hover:shadow-lg hover:shadow-gray-100 hover:-translate-y-1 border border-gray-100 hover:border-gray-200 mb-3 sm:mb-4" class="group bg-white rounded-lg p-4 sm:p-6 cursor-pointer transition-all duration-300 hover:shadow-lg hover:shadow-gray-100 hover:-translate-y-1 border border-gray-100 hover:border-gray-200 mb-3 sm:mb-4"
@click="router.push(`/postDetail/${item.id}`)" @click="handleClickItem(item)"
> >
<div class="flex gap-3 justify-between"> <div class="flex gap-3 justify-between">
<!-- 内容区域 --> <!-- 内容区域 -->
...@@ -59,12 +59,12 @@ ...@@ -59,12 +59,12 @@
<img <img
:src="item.faceUrl" :src="item.faceUrl"
alt="文章配图" alt="文章配图"
class="w-full h-full object-cover rounded-lg sm:rounded-xl group-hover:scale-105 transition-transform duration-300" class="w-full h-full object-cover rounded-lg sm:rounded-lg group-hover:scale-105 transition-transform duration-300"
loading="lazy" loading="lazy"
/> />
<!-- 图片遮罩效果 --> <!-- 图片遮罩效果 -->
<div <div
class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-5 rounded-lg sm:rounded-xl transition-all duration-300" class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-5 rounded-lg sm:rounded-lg transition-all duration-300"
></div> ></div>
</div> </div>
</div> </div>
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
<div class="right"> <div class="right">
<div <div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3" class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
> >
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
...@@ -115,8 +115,8 @@ ...@@ -115,8 +115,8 @@
<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, type ArticleItemDto } from '@/api'
import { TABS_REF_KEY } from '@/constants' import { TABS_REF_KEY, ArticleTypeEnum } from '@/constants'
import { useScrollTop } from '@/hooks' import { useScrollTop } from '@/hooks'
import dayjs from 'dayjs' import dayjs from 'dayjs'
...@@ -135,6 +135,14 @@ const tabsRef = inject(TABS_REF_KEY) ...@@ -135,6 +135,14 @@ const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!) const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
const handleClickItem = (item: ArticleItemDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`)
} else {
router.push(`/postDetail/${item.id}`)
}
}
defineExpose({ defineExpose({
refresh: (sortLogic?: number) => { refresh: (sortLogic?: number) => {
console.log('sortLogic', sortLogic) console.log('sortLogic', sortLogic)
......
<template> <template>
<div class="bg-gray-50/30"> <div>
<!-- tabs --> <!-- tabs -->
<div class="shadow-sm"> <div class="shadow-sm">
<div class="max-w-7xl mx-auto px-4"> <div class="max-w-7xl mx-auto px-4">
...@@ -8,14 +8,14 @@ ...@@ -8,14 +8,14 @@
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<div <div
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.key" :key="tab.sortLogic"
:class="[ :class="[
'px-6 py-2 rounded-full text-sm font-medium cursor-pointer transition-all duration-200', 'px-6 py-2 rounded-full text-sm font-medium cursor-pointer transition-all duration-200',
activeTab === tab.key searchParams.sortLogic === tab.sortLogic
? 'bg-orange-400 text-white shadow-md' ? 'bg-orange-400 text-white shadow-md'
: 'text-gray-600 hover:text-orange-500 hover:bg-orange-50', : 'text-gray-600 hover:text-orange-500 hover:bg-orange-50',
]" ]"
@click="activeTab = tab.key" @click="toggleTab(tab.sortLogic)"
> >
{{ tab.label }} {{ tab.label }}
</div> </div>
...@@ -23,13 +23,14 @@ ...@@ -23,13 +23,14 @@
</div> </div>
</div> </div>
</div> </div>
<div v-loading="loading">
<!-- 第一页的特殊布局 --> <!-- 第一页的特殊布局 -->
<template v-if="searchParams.current === 0"> <template v-if="searchParams.current === 0">
<!-- 前三个特殊布局 --> <!-- 前三个特殊布局 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- 第一个视频 - 占据两列 --> <!-- 第一个视频 - 占据两列 -->
<div <div
class="lg:col-span-2 group relative rounded-2xl overflow-hidden bg-white shadow-sm hover:shadow-xl transition-all duration-500 cursor-pointer" class="lg:col-span-2 group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-xl transition-all duration-500 cursor-pointer"
> >
<div class="relative overflow-hidden"> <div class="relative overflow-hidden">
<img <img
...@@ -141,7 +142,7 @@ ...@@ -141,7 +142,7 @@
<div class="p-4"> <div class="p-4">
<h3 <h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-2" class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
> >
{{ n === 1 ? '复日奶茶' : 'PS最新版零基础全套' }} {{ n === 1 ? '复日奶茶' : 'PS最新版零基础全套' }}
</h3> </h3>
...@@ -221,7 +222,7 @@ ...@@ -221,7 +222,7 @@
<div class="p-4"> <div class="p-4">
<h3 <h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-2" class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
> >
{{ item.title }} {{ item.title }}
</h3> </h3>
...@@ -235,7 +236,7 @@ ...@@ -235,7 +236,7 @@
> >
{{ item.createUserName }} {{ item.createUserName }}
</div> </div>
<span class="font-medium">UP主{{ item.createUserName }}</span> <span class="font-medium">{{ item.createUserName }}</span>
</div> </div>
<span class="bg-gray-100 px-2 py-1 rounded-full">{{ <span class="bg-gray-100 px-2 py-1 rounded-full">{{
dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm') dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm')
...@@ -298,7 +299,7 @@ ...@@ -298,7 +299,7 @@
</div> </div>
</div> </div>
<!-- 播放按钮 --> <!-- 播放按钮 -->
<div <!-- <div
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300" class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
> >
<div <div
...@@ -306,26 +307,19 @@ ...@@ -306,26 +307,19 @@
> >
<el-icon size="50" color="#333"><VideoPlay /></el-icon> <el-icon size="50" color="#333"><VideoPlay /></el-icon>
</div> </div>
</div> </div> -->
</div> </div>
<div class="p-4"> <div class="p-4">
<h3 <h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-2" class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
> >
{{ item.title }} {{ item.title }}
</h3> </h3>
<p class="text-gray-600 text-sm mb-3 line-clamp-2 leading-relaxed">
{{ item.content }}
</p>
<div class="flex items-center justify-between text-gray-500 text-xs"> <div class="flex items-center justify-between text-gray-500 text-xs">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <img :src="item.showAvatar" alt="" class="w-6 h-6 rounded-full object-cover" />
class="w-6 h-6 bg-gradient-to-r from-green-400 to-blue-500 rounded-full flex items-center justify-center text-white text-xs font-bold" <span class="font-medium">{{ item.showName }}</span>
>
{{ item.createUserName }}
</div>
<span class="font-medium">UP主{{ item.createUserName }}</span>
</div> </div>
<span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm') }}</span> <span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm') }}</span>
</div> </div>
...@@ -336,7 +330,7 @@ ...@@ -336,7 +330,7 @@
<!-- 底部分页 --> <!-- 底部分页 -->
<div class="bottom-pagination backdrop-blur-8 border-t border-gray-200"> <div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
<div class="max-w-7xl mx-auto px-8 py-6"> <div class="max-w-7xl mx-auto py-6">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<!-- 左侧:回到顶部按钮 --> <!-- 左侧:回到顶部按钮 -->
<div class="left"> <div class="left">
...@@ -346,7 +340,7 @@ ...@@ -346,7 +340,7 @@
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
<div class="right"> <div class="right">
<div <div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3" class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
> >
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
...@@ -355,6 +349,12 @@ ...@@ -355,6 +349,12 @@
layout="prev, pager, next, jumper, total" layout="prev, pager, next, jumper, total"
:total="total" :total="total"
class="custom-pagination" class="custom-pagination"
@current-change="
(e) => {
;(handleBackTop(), goToPage(e))
}
"
@size-change="changePageSize"
/> />
</div> </div>
</div> </div>
...@@ -362,6 +362,7 @@ ...@@ -362,6 +362,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts" name="RecommendList"> <script setup lang="ts" name="RecommendList">
...@@ -374,25 +375,27 @@ import dayjs from 'dayjs' ...@@ -374,25 +375,27 @@ import dayjs from 'dayjs'
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
const router = useRouter()
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!) const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
const { list, total, searchParams, loading, goToPage, changePageSize, refresh } = usePageSearch( const { list, total, searchParams, loading, goToPage, changePageSize, refresh } = usePageSearch(
getArticleList, getArticleList,
{ {
defaultParams: { type: ArticleTypeEnum.VIDEO }, defaultParams: { type: ArticleTypeEnum.VIDEO, sortLogic: 0 },
defaultCurrent: 1, defaultCurrent: 1,
defaultSize: 12, defaultSize: 12,
immediate: false, immediate: false,
}, },
) )
// Tabs 配置
const router = useRouter()
const tabs = [ const tabs = [
{ key: 'latest', label: '最新发布' }, { label: '最多播放', sortLogic: 0 },
{ key: 'popular', label: '最多模仿' }, { label: '最新发布', sortLogic: 1 },
{ key: 'favorites', label: '最多收藏' },
] ]
const activeTab = ref('latest') const toggleTab = (sortLogic: number) => {
searchParams.value.sortLogic = sortLogic
refresh()
}
const goVideoDetail = (n: number) => { const goVideoDetail = (n: number) => {
router.push(`/videoDetail?id=${n}`) router.push(`/videoDetail?id=${n}`)
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
@click="toggleTab(tab)" @click="toggleTab(tab)"
> >
<div <div
class="flex items-center gap-2 px-12 py-2.5 rounded-xl transition-all duration-300" class="flex items-center gap-2 px-12 py-2.5 rounded-lg transition-all duration-300"
:class="{ :class="{
'bg-#fffdfd shadow-[inset_0_2px_4px_0_rgb(0,0,0,0.1)]': activeTab === tab.name, 'bg-#fffdfd shadow-[inset_0_2px_4px_0_rgb(0,0,0,0.1)]': activeTab === tab.name,
'hover:bg-white/60': activeTab !== tab.name, 'hover:bg-white/60': activeTab !== tab.name,
...@@ -204,19 +204,20 @@ ...@@ -204,19 +204,20 @@
<div <div
class="flex flex-col items-start justify-center ml-2 sm:ml-3 min-w-0 flex-1" class="flex flex-col items-start justify-center ml-2 sm:ml-3 min-w-0 flex-1"
> >
<div class="text-14px truncate w-full font-medium mb-1"> <el-tooltip :content="item.description" placement="top">
<div class="text-14px truncate w-full font-medium mb-1 cursor-pointer">
{{ item.title }}({{ item.currentCount }}/{{ item.limitCount }}) {{ item.title }}({{ item.currentCount }}/{{ item.limitCount }})
</div> </div>
</el-tooltip>
<div class="color-#333 text-xs w-full flex items-center flex-nowrap"> <div class="color-#333 text-xs w-full flex items-center flex-nowrap">
<svg-icon name="small_coin" size="16" class="mr-1" /> <svg-icon name="small_coin" size="16" class="mr-1" />
<el-tooltip :content="item.description" placement="top"> <div class="truncate w-130px">
<div class="truncate w-130px cursor-pointer">
+{{ item.rewardValue }}亚币{{ +{{ item.rewardValue }}亚币{{
TaskDateLimitTypeText[item.limitType] && TaskDateLimitTypeText[item.limitType] &&
`(${TaskDateLimitTypeText[item.limitType] + '刷新'})` `(${TaskDateLimitTypeText[item.limitType] + '刷新'})`
}} }}
</div> </div>
</el-tooltip>
</div> </div>
</div> </div>
</div> </div>
...@@ -292,6 +293,13 @@ const tabs = [ ...@@ -292,6 +293,13 @@ const tabs = [
const activeTab = ref( 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 || '首页',
) )
watch(
() => route.path,
(newPath) => {
activeTab.value = tabs.find((item) => item.path === newPath.split('/').at(-1))?.name || '首页'
},
)
const toggleTab = (tab: { name: string; path: string }) => { const toggleTab = (tab: { name: string; path: string }) => {
activeTab.value = tab.name activeTab.value = tab.name
router.push(`/homePage/${tab.path}`) router.push(`/homePage/${tab.path}`)
...@@ -349,46 +357,52 @@ const userRecordData = ref({} as UserRecordDataDto) ...@@ -349,46 +357,52 @@ const userRecordData = ref({} as UserRecordDataDto)
const onDailySign = async () => { const onDailySign = async () => {
await dailySign() await dailySign()
await refreshTaskData(true)
} }
const handleTask = (item: TaskItemDto) => { const handleTask = (item: TaskItemDto) => {
if (item.currentCount === item.limitCount) return if (item.currentCount === item.limitCount) return
if ((item.taskKey = 'VALID_COMMENT')) {
router.push(`/homePage/homeTab`)
} else {
console.log(item)
// if (item.svgName === 'svgName') { // if (item.svgName === 'svgName') {
handleBackTop() handleBackTop()
triggerAnimation() triggerAnimation()
}
// } // }
} }
const initPage = () => { const initPage = () => {
Promise.allSettled([ Promise.allSettled([getCarouselList(), getUserAccountData(), getRecordData()]).then(
getCarouselList(), ([r1, r2, r3]) => {
getTaskList(),
getUserAccountData(),
getRecordData(),
]).then(([r1, r2, r3, r4]) => {
if (r1.status === 'fulfilled') { if (r1.status === 'fulfilled') {
carouselList.value = r1.value.data carouselList.value = r1.value.data
} }
if (r2.status === 'fulfilled') { if (r2.status === 'fulfilled') {
regularTaskList.value = r2.value.data.filter( userAccountData.value = r2.value.data
(item) => item.taskType === TaskTypeEnum.REGULAR_TASK,
)
specialTaskList.value = r2.value.data.filter(
(item) => item.taskType === TaskTypeEnum.SPECIAL_TASK,
)
} }
if (r3.status === 'fulfilled') { if (r3.status === 'fulfilled') {
console.log(r3) userRecordData.value = r3.value.data
userAccountData.value = r3.value.data
} }
if (r4.status === 'fulfilled') { },
console.log(r4) )
userRecordData.value = r4.value.data }
const refreshTaskData = async (refreshRecordData = false) => {
if (refreshRecordData) {
const { data } = await getRecordData()
userRecordData.value = data
} }
}) const { data } = await getTaskList()
regularTaskList.value = data.filter((item) => item.taskType === TaskTypeEnum.REGULAR_TASK)
specialTaskList.value = data.filter((item) => item.taskType === TaskTypeEnum.SPECIAL_TASK)
} }
onActivated(() => {
refreshTaskData(false)
})
onMounted(() => { onMounted(() => {
initPage() initPage()
}) })
......
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
<div class="right"> <div class="right">
<div <div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3" class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
> >
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
...@@ -114,7 +114,7 @@ ...@@ -114,7 +114,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ArrowRight, View, ChatDotRound, Star } from '@element-plus/icons-vue' import { View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { getColumnList } from '@/api' import { getColumnList } from '@/api'
import { usePageSearch, useScrollTop } from '@/hooks' import { usePageSearch, useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants' import { TABS_REF_KEY } from '@/constants'
...@@ -124,12 +124,19 @@ const router = useRouter() ...@@ -124,12 +124,19 @@ const router = useRouter()
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!) const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const { list, total, searchParams, goToPage, changePageSize, loading } = usePageSearch( const { list, total, searchParams, goToPage, changePageSize, loading, refresh } = usePageSearch(
getColumnList, getColumnList,
{ {
defaultSize: 3, defaultSize: 3,
immediate: false,
}, },
) )
defineExpose({
refresh: () => {
searchParams.value.current = 0
refresh()
},
})
</script> </script>
<style scoped></style> <style scoped></style>
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
<div class="right"> <div class="right">
<div <div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3" class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
> >
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
...@@ -126,12 +126,18 @@ const router = useRouter() ...@@ -126,12 +126,18 @@ const router = useRouter()
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!) const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const { list, total, searchParams, goToPage, changePageSize, loading } = usePageSearch( const { list, total, searchParams, goToPage, changePageSize, loading, refresh } = usePageSearch(
getInterviewList, getInterviewList,
{ {
defaultSize: 3, defaultSize: 3,
immediate: false,
}, },
) )
defineExpose({
refresh: () => {
refresh()
},
})
</script> </script>
<style scoped></style> <style scoped></style>
<template> <template>
<div class="min-h-screen"> <div>
<!-- 发布区域 --> <!-- 发布区域 -->
<div class="bg-white p-6 mb-6 rounded-lg shadow-sm"> <div class="bg-white p-6 mb-6 rounded-lg shadow-sm">
<div class="flex-1 bg-white rounded-lg border border-gray-200"> <div class="flex-1 bg-white rounded-lg border border-gray-200">
...@@ -116,10 +116,10 @@ ...@@ -116,10 +116,10 @@
<el-tag <el-tag
v-for="tag in tagList" v-for="tag in tagList"
:key="tag.id" :key="tag.id"
:type="activeTag === tag.id ? 'primary' : 'info'" :type="searchParams.tagIdList?.includes(tag.id) ? 'primary' : 'info'"
:effect="activeTag === tag.id ? 'dark' : 'plain'" :effect="searchParams.tagIdList?.includes(tag.id) ? 'dark' : 'plain'"
class="cursor-pointer" class="cursor-pointer"
@click="activeTag = tag.id" @click="toggleTag(tag.id)"
> >
{{ tag.title }} {{ tag.title }}
</el-tag> </el-tag>
...@@ -133,7 +133,7 @@ ...@@ -133,7 +133,7 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-1 h-6 bg-red-500 rounded"></div> <div class="w-1 h-6 bg-red-500 rounded"></div>
<h2 class="text-lg font-medium"> <h2 class="text-lg font-medium">
{{ tagList.find((tag) => tag.id === activeTag)?.title ?? '最新' }} {{ tagList.find((tag) => searchParams.tagIdList?.includes(tag.id))?.title ?? '最新' }}
</h2> </h2>
</div> </div>
<div <div
...@@ -143,6 +143,7 @@ ...@@ -143,6 +143,7 @@
查看更多 >> 查看更多 >>
</div> </div>
</div> </div>
<el-divider />
<!-- 动态列表 --> <!-- 动态列表 -->
<div class="divide-y divide-gray-100"> <div class="divide-y divide-gray-100">
...@@ -174,11 +175,6 @@ ...@@ -174,11 +175,6 @@
</div> </div>
</div> </div>
<!-- 无图片的内容 -->
<!-- <div v-else class="text-gray-600 text-sm leading-relaxed mb-3">
{{ item.content }}
</div> -->
<!-- 互动数据 --> <!-- 互动数据 -->
<div class="flex items-center gap-4 text-gray-400 text-sm"> <div class="flex items-center gap-4 text-gray-400 text-sm">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
...@@ -218,7 +214,7 @@ ...@@ -218,7 +214,7 @@
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
<div class="right"> <div class="right">
<div <div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3" class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
> >
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
...@@ -253,18 +249,16 @@ import { useTagsStore } from '@/stores/tags' ...@@ -253,18 +249,16 @@ import { useTagsStore } from '@/stores/tags'
const tagsStore = useTagsStore() const tagsStore = useTagsStore()
const { tagList } = storeToRefs(tagsStore) const { tagList } = storeToRefs(tagsStore)
const activeTag = ref(tagList.value[0]?.id ?? 0)
const router = useRouter() const router = useRouter()
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
const { userInfo } = storeToRefs(useUserStore()) const { userInfo } = storeToRefs(useUserStore())
// 标签数据
const filterOptions = ref([ const filterOptions = ref([
{ title: '最', id: 0 }, { title: '最', id: 0 },
{ title: '最', id: 1 }, { title: '最', id: 1 },
{ title: '最观看', id: 2 }, { title: '最观看', id: 2 },
]) ])
const tagInput = ref('') const tagInput = ref('')
...@@ -273,9 +267,10 @@ const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!) ...@@ -273,9 +267,10 @@ const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const { list, total, searchParams, goToPage, changePageSize, refresh } = usePageSearch( const { list, total, searchParams, goToPage, changePageSize, refresh } = usePageSearch(
getPracticeList, getPracticeList,
{ {
defaultParams: { sortLogic: filterOptions.value[0]?.id }, defaultParams: { sortLogic: filterOptions.value[0]?.id, tagIdList: [] },
defaultCurrent: 1, defaultCurrent: 1,
defaultSize: 5, defaultSize: 5,
immediate: false,
}, },
) )
...@@ -284,6 +279,17 @@ const toggleFilter = (id: number) => { ...@@ -284,6 +279,17 @@ const toggleFilter = (id: number) => {
refresh() refresh()
handleBackTop() handleBackTop()
} }
const toggleTag = (id: number) => {
searchParams.value.tagIdList = [id]
refresh()
handleBackTop()
}
defineExpose({
refresh: () => {
refresh()
},
})
</script> </script>
<style scoped></style> <style scoped></style>
...@@ -5,40 +5,64 @@ ...@@ -5,40 +5,64 @@
<div class="left flex gap-3 flex items-center"> <div class="left flex gap-3 flex items-center">
<Tabs v-model="activeTab" :tabs="tabs" /> <Tabs v-model="activeTab" :tabs="tabs" />
<!-- 刷新图标 --> <!-- 刷新图标 -->
<el-icon size="15" class="cursor-pointer hover:rotate-180 transition-all duration-300" <el-icon
size="15"
class="cursor-pointer hover:rotate-180 transition-all duration-300"
@click="handleRefresh"
><Refresh ><Refresh
/></el-icon> /></el-icon>
</div> </div>
</div> </div>
<el-divider style="margin: 10px 0 20px 0" /> <el-divider style="margin: 10px 0 20px 0" />
<transition name="fade" mode="out-in" @enter="handleEnter">
<keep-alive> <keep-alive>
<component :is="curretComponent" /> <component ref="activeTabComponentRef" :is="activeTabComponent" />
</keep-alive> </keep-alive>
</transition>
</div> </div>
</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 { Refresh } from '@element-plus/icons-vue' 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'
const curretComponent = computed(() => {
return {
专栏: ColumnList,
专访: InterviewList,
实践: PracticeList,
}[activeTab.value]
})
const activeTab = ref('专栏')
const tabs = [ const tabs = [
{ label: '专栏', value: '专栏' }, { label: '专栏', value: '专栏', component: ColumnList },
{ label: '实践', value: '实践' }, { label: '实践', value: '实践', component: PracticeList },
{ label: '专访', value: '专访' }, { label: '专访', value: '专访', component: InterviewList },
{ label: '视频', value: '视频' }, { label: '视频', value: '视频', component: () => h('h1', '11') },
] ]
const activeTab = ref('专栏')
const activeTabComponent = computed(() => {
return tabs.find((tab) => tab.value === activeTab.value)?.component
})
const activeTabComponentRef =
useTemplateRef<InstanceType<typeof ColumnList>>('activeTabComponentRef')
const handleEnter = () => {
handleRefresh()
}
const handleRefresh = () => {
activeTabComponentRef.value?.refresh?.()
}
onMounted(() => {
handleRefresh()
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.fade-enter-from,
.fade-leave-to {
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>
...@@ -24,7 +24,7 @@ export default function ExchangeContent( ...@@ -24,7 +24,7 @@ export default function ExchangeContent(
<div class="flex justify-center mb-8"> <div class="flex justify-center mb-8">
<div class="relative"> <div class="relative">
<div class="w-32 h-32 bg-gradient-to-br from-orange-100 to-pink-100 rounded-3xl flex items-center justify-center shadow-lg"> <div class="w-32 h-32 bg-gradient-to-br from-orange-100 to-pink-100 rounded-3xl flex items-center justify-center shadow-lg">
<div class="w-20 h-20 bg-white rounded-2xl flex items-center justify-center shadow-sm"> <div class="w-20 h-20 bg-white rounded-lg flex items-center justify-center shadow-sm">
<img src={ask} alt={item.name} class="w-16 h-16 object-contain" /> <img src={ask} alt={item.name} class="w-16 h-16 object-contain" />
</div> </div>
</div> </div>
...@@ -51,7 +51,7 @@ export default function ExchangeContent( ...@@ -51,7 +51,7 @@ export default function ExchangeContent(
{/* 办公点选择和数量 */} {/* 办公点选择和数量 */}
{item.itemType === ShopGoodsTypeEnum.REAL_GOODS && ( {item.itemType === ShopGoodsTypeEnum.REAL_GOODS && (
<div class=" rounded-2xl px-5 mx-2 space-y-4"> <div class=" rounded-lg px-5 mx-2 space-y-4">
{/* 办公点选择 */} {/* 办公点选择 */}
<div> <div>
<label class="text-gray-700 text-sm font-medium mb-2 block">办公点</label> <label class="text-gray-700 text-sm font-medium mb-2 block">办公点</label>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div class="max-w-[1440px] mx-auto"> <div class="max-w-[1440px] mx-auto">
<!-- 顶部积分卡片 --> <!-- 顶部积分卡片 -->
<div <div
class="bg-gradient-to-r from-purple-50 to-blue-50 rounded-2xl p-5 shadow-sm mb-8 border border-purple-100" class="bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg p-5 shadow-sm mb-8 border border-purple-100"
> >
<div class="flex justify-between items-center flex-wrap gap-4"> <div class="flex justify-between items-center flex-wrap gap-4">
<div class="flex items-baseline gap-3"> <div class="flex items-baseline gap-3">
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
<div <div
v-for="item in virtualGoodsList" v-for="item in virtualGoodsList"
:key="item.id" :key="item.id"
class="group bg-gradient-to-br from-purple-50 to-pink-50 rounded-2xl p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-purple-200 hover:-translate-y-1" class="group bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-purple-200 hover:-translate-y-1"
@click="onExchangeGoods(item)" @click="onExchangeGoods(item)"
> >
<div class="w-24 h-24 mb-3 flex items-center justify-center"> <div class="w-24 h-24 mb-3 flex items-center justify-center">
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
<!-- 分页 --> <!-- 分页 -->
<div class="flex justify-end mt-6"> <div class="flex justify-end mt-6">
<div class="bg-gray-50 rounded-xl shadow-sm border border-gray-200 p-3"> <div class="bg-gray-50 rounded-lg shadow-sm border border-gray-200 p-3">
<el-pagination <el-pagination
size="small" size="small"
v-model:current-page="virtualGoodsSearchParams.current" v-model:current-page="virtualGoodsSearchParams.current"
...@@ -111,7 +111,7 @@ ...@@ -111,7 +111,7 @@
<div <div
v-for="item in realGoodsList" v-for="item in realGoodsList"
:key="item.id" :key="item.id"
class="group bg-gradient-to-br from-blue-50 to-indigo-50 rounded-2xl p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-blue-200 hover:-translate-y-1" class="group bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-blue-200 hover:-translate-y-1"
@click="onExchangeGoods(item)" @click="onExchangeGoods(item)"
> >
<div class="w-24 h-24 mb-3 flex items-center justify-center"> <div class="w-24 h-24 mb-3 flex items-center justify-center">
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
<!-- 分页 --> <!-- 分页 -->
<div class="flex justify-end mt-6"> <div class="flex justify-end mt-6">
<div class="bg-gray-50 rounded-xl shadow-sm border border-gray-200 p-3"> <div class="bg-gray-50 rounded-lg shadow-sm border border-gray-200 p-3">
<el-pagination <el-pagination
v-model:current-page="realGoodsSearchParams.current" v-model:current-page="realGoodsSearchParams.current"
v-model:page-size="realGoodsSearchParams.size" v-model:page-size="realGoodsSearchParams.size"
......
...@@ -40,14 +40,14 @@ import type { ArticleItemDto } from '@/api' ...@@ -40,14 +40,14 @@ import type { ArticleItemDto } from '@/api'
import type { Component } from 'vue' import type { Component } from 'vue'
import { useScrollTop } from '@/hooks' import { useScrollTop } from '@/hooks'
import { addOrCanceArticlelCollect, addOrCanceArticlelLike } from '@/api' import { addOrCanceArticlelCollect, addOrCanceArticlelLike } from '@/api'
import { COMMENT_REF_KEY } from '@/constants'
const modelValue = defineModel<ArticleItemDto>('modelValue', { required: true }) const modelValue = defineModel<ArticleItemDto>('modelValue', { required: true })
const commentRef = inject(COMMENT_REF_KEY) const emit = defineEmits<{
(e: 'scrollToCommentBox'): void
}>()
const { ScrollTopComp } = useScrollTop(window) const { ScrollTopComp } = useScrollTop(window)
const { handleBackTop } = useScrollTop(commentRef!)
interface StatItem { interface StatItem {
icon: Component icon: Component
...@@ -100,7 +100,9 @@ const stats = computed(() => { ...@@ -100,7 +100,9 @@ const stats = computed(() => {
count: modelValue.value?.replyCount ?? 0, count: modelValue.value?.replyCount ?? 0,
label: '评论', label: '评论',
// active: modelValue.value?.replyCount > 0, // active: modelValue.value?.replyCount > 0,
actionFn: handleBackTop, actionFn: () => {
emit('scrollToCommentBox')
},
}, },
] ]
}) })
......
<template> <template>
<div class="min-h-screen px-20"> <div class="min-h-screen px-20">
<!-- 主内容区 --> <!-- 主内容区 -->
<ActionButtons v-model="articleDetail"></ActionButtons> <ActionButtons
v-model="articleDetail"
@scrollToCommentBox="commentRef?.scrollToCommentBox()"
></ActionButtons>
<div class="lg:col-span-3"> <div class="lg:col-span-3">
<!-- 帖子主体 --> <!-- 帖子主体 -->
<div <div
class="bg-white backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 overflow-hidden" class="bg-white backdrop-blur-sm rounded-lg shadow-sm border border-white/50 overflow-hidden"
> >
<!-- 发布者信息 --> <!-- 发布者信息 -->
<div class="p-6 border-b border-gray-100"> <div class="p-6 border-b border-gray-100">
...@@ -63,7 +66,7 @@ ...@@ -63,7 +66,7 @@
:key="item" :key="item"
:src="item" :src="item"
alt="" alt=""
class="rounded-xl object-cover w-full h-64 hover:scale-105 transition-transform cursor-pointer" class="rounded-lg object-cover w-full h-64 hover:scale-105 transition-transform cursor-pointer"
/> />
</div> </div>
</div> </div>
...@@ -82,42 +85,50 @@ ...@@ -82,42 +85,50 @@
</div> </div>
<!-- 评论区 --> <!-- 评论区 -->
<div <Comment ref="commentRef" :id="id" />
<!-- <div
ref="commentRef" ref="commentRef"
class="mt-6 bg-white backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 overflow-hidden" class="mt-6 bg-white backdrop-blur-sm rounded-lg shadow-sm border border-white/50 overflow-hidden"
> >
<!-- 评论筛选 -->
<div class="p-4 border-b border-gray-100"> <div class="p-4 border-b border-gray-100">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4 justify-between">
<span class="text-lg font-semibold text-gray-800" <span class="text-lg font-semibold text-gray-800">评论 ({{ total }})</span>
>评论 ({{ articleDetail?.replyCount || 0 }})</span
>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
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" class="cursor-pointer px-3 py-1.5 text-sm bg-gradient-to-r text-gray-600 rounded-full transition-all hover:bg-gray-100"
> @click="((searchParams.sortType = 2), refresh())"
最热 :class="{
</button> 'bg-gradient-to-r from-blue-500 to-purple-500 text-white! shadow-md ':
<button searchParams.sortType === 2,
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="cursor-pointer 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 bg-gradient-to-r text-gray-600 rounded-full transition-all hover:bg-gray-100"
:class="{
'bg-gradient-to-r from-blue-500 to-purple-500 text-white! shadow-md':
searchParams.sortType === 1,
}"
@click="((searchParams.sortType = 1), refresh())"
> >
置顶 最多评论
</button> </button>
<button <button
class="cursor-pointer 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 bg-gradient-to-r text-gray-600 rounded-full transition-all hover:bg-gray-100"
:class="{
'bg-gradient-to-r from-blue-500 to-purple-500 text-white! shadow-md':
searchParams.sortType === 4,
}"
@click="((searchParams.sortType = 4), refresh())"
> >
精选 最多点赞
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- 发表评论 --> <div>
<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 :src="userInfo?.avatar" alt="" class="w-10 h-10 rounded-full object-cover" /> <img :src="userInfo?.avatar" alt="" class="w-10 h-10 rounded-full object-cover" />
...@@ -139,7 +150,7 @@ ...@@ -139,7 +150,7 @@
</div> </div>
<button <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" 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()" :disabled="!comment.trim() || loading"
@click="handleComment()" @click="handleComment()"
> >
发表 发表
...@@ -149,8 +160,7 @@ ...@@ -149,8 +160,7 @@
</div> </div>
</div> </div>
<!-- 评论列表 --> <div v-loading="loading" class="divide-y divide-gray-100" v-if="list.length">
<div class="divide-y divide-gray-100" v-if="list.length">
<div v-for="item in list" :key="item.id"> <div v-for="item in list" :key="item.id">
<div class="p-4 hover:bg-gray-50/50 transition-colors"> <div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3"> <div class="flex gap-3">
...@@ -167,14 +177,14 @@ ...@@ -167,14 +177,14 @@
>置顶</span >置顶</span
> >
</div> </div>
<!-- 换行 -->
<p class="text-gray-700 mb-3 break-all"> <p class="text-gray-700 mb-3 break-all">
{{ item.content }} {{ item.content }}
</p> </p>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500"> <div class="flex items-center gap-4 text-sm text-gray-500">
<span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span> <span>{{
<!-- <button class="flex items-center gap-1 hover:text-red-500 transition-colors"> --> dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss')
}}</span>
<div <div
class="flex items-center gap-1 cursor-pointer" class="flex items-center gap-1 cursor-pointer"
@click="handleLickComment(item)" @click="handleLickComment(item)"
...@@ -187,7 +197,6 @@ ...@@ -187,7 +197,6 @@
</el-icon> </el-icon>
<span>{{ item.postPriseCount }}</span> <span>{{ item.postPriseCount }}</span>
</div> </div>
<!-- </button> -->
<button <button
class="cursor-pointer hover:text-blue-500 transition-colors" class="cursor-pointer hover:text-blue-500 transition-colors"
@click="handleReply(item)" @click="handleReply(item)"
...@@ -196,39 +205,6 @@ ...@@ -196,39 +205,6 @@
</button> </button>
</div> </div>
</div> </div>
<!-- <div v-show="currentId === item.id" 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(item.id)"
>
发表
</button>
</div>
</div>
</div> -->
<!-- 回复列表 -->
<div v-if="item.children.length" class="mt-3 ml-4 space-y-3"> <div v-if="item.children.length" class="mt-3 ml-4 space-y-3">
<div <div
v-for="child in item.children" v-for="child in item.children"
...@@ -311,17 +287,21 @@ ...@@ -311,17 +287,21 @@
</div> </div>
</div> </div>
</div> </div>
<el-divider /> <div class="px-4">
<el-divider class="my-1" />
</div>
</div> </div>
<!-- 底部分页 -->
<!-- 靠右侧 -->
<div class="flex justify-end"> <div class="flex justify-end">
<div class="w-fit"> <div class="w-fit p-4">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
:total="total" :total="total"
@current-change="goToPage" @current-change="
async (e) => {
;(await goToPage(e), handleBackTop())
}
"
@size-change="changePageSize" @size-change="changePageSize"
layout="prev, pager, next, total" layout="prev, pager, next, total"
/> />
...@@ -329,47 +309,23 @@ ...@@ -329,47 +309,23 @@
</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 { import { getArticleDetail, type ArticleItemDto } from '@/api'
getArticleDetail,
type ArticleItemDto,
addComment,
getCommentList,
addOrCancelCommentLike,
} from '@/api'
import ActionButtons from './components/actionButtons.vue' import ActionButtons from './components/actionButtons.vue'
import { useUserStore } from '@/stores' import Comment from '@/components/common/Comment/index.vue'
import { storeToRefs } from 'pinia'
import { BooleanFlag, COMMENT_REF_KEY } from '@/constants'
import { usePageSearch } from '@/hooks/usePageSearch'
import { Pointer } from '@element-plus/icons-vue'
import type { CommentItemDto } from '@/api/article/types'
const userStore = useUserStore() const commentRef = useTemplateRef<typeof Comment | null>('commentRef')
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({} as ArticleItemDto) const articleDetail = ref({} as ArticleItemDto)
const comment = ref('')
const currentId = ref(-1)
const { list, searchParams, goToPage, changePageSize, total, reset } = usePageSearch(
getCommentList,
{
defaultParams: {
articleId: id,
},
},
)
const initPage = () => { const initPage = () => {
Promise.allSettled([getArticleDetail(id)]).then(([r1]) => { Promise.allSettled([getArticleDetail(id)]).then(([r1]) => {
...@@ -378,83 +334,6 @@ const initPage = () => { ...@@ -378,83 +334,6 @@ const initPage = () => {
} }
}) })
} }
const handleLickComment = async (item: CommentItemDto) => {
await addOrCancelCommentLike(item.id)
if (item.hasPraise === BooleanFlag.YES) {
ElMessage.success('取消点赞成功')
item.postPriseCount--
item.hasPraise = BooleanFlag.NO
} else {
ElMessage.success('点赞成功')
item.postPriseCount++
item.hasPraise = BooleanFlag.YES
}
}
const currentParentCommentId = ref(0)
const currentSonCommentId = ref(0)
const handleReply = (item: CommentItemDto) => {
if (item.pid) {
// 点击的是子评论
if (currentSonCommentId.value) {
// 置为空
if (currentSonCommentId.value !== item.id) {
currentSonCommentId.value = item.id
currentParentCommentId.value = item.pid
} else {
currentSonCommentId.value = 0
currentParentCommentId.value = 0
}
} else {
currentSonCommentId.value = item.id
currentParentCommentId.value = item.pid
}
} else {
// 点击的是父评论
if (currentParentCommentId.value) {
// 置为空
if (currentParentCommentId.value !== item.id) {
currentParentCommentId.value = item.id
} else {
currentParentCommentId.value = 0
}
} else {
currentParentCommentId.value = item.id
currentSonCommentId.value = 0
}
}
comment.value = ''
console.log('parent', currentParentCommentId.value, 'son', currentSonCommentId.value)
}
const showCommentBox = (item: CommentItemDto) => {
if (currentParentCommentId.value && currentSonCommentId.value) {
// 说明在评论子评论
return (
item.id === currentParentCommentId.value &&
item.children?.some((i) => i.id === currentSonCommentId.value)
)
} else if (currentParentCommentId.value) {
// 说明在评论父评论
return item.id === currentParentCommentId.value
}
}
const handleComment = async (pid?: number) => {
console.log(comment.value)
const res = await addComment({
articleId: id,
content: comment.value,
...(pid ? { pid } : {}),
})
console.log(res)
ElMessage.success('发表评论成功')
reset()
comment.value = ''
}
provide(COMMENT_REF_KEY, commentRef)
onMounted(async () => { onMounted(async () => {
initPage() initPage()
}) })
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="min-h-screen bg-gradient-to-br"> <div class="min-h-screen bg-gradient-to-br">
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<!-- 主表单卡片 --> <!-- 主表单卡片 -->
<div class="bg-white rounded-2xl shadow-lg p-8"> <div class="bg-white rounded-lg shadow-lg p-8">
<el-form ref="formRef" :model="form" label-position="top" size="default"> <el-form ref="formRef" :model="form" label-position="top" size="default">
<!-- 案例编号 --> <!-- 案例编号 -->
<div class="mb-6 flex items-center gap-2"> <div class="mb-6 flex items-center gap-2">
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
placeholder="请输入【案例】标题" placeholder="请输入【案例】标题"
:maxlength="100" :maxlength="100"
show-word-limit show-word-limit
size="large"
class="text-lg" class="text-lg"
/> />
</el-form-item> </el-form-item>
...@@ -57,12 +56,12 @@ ...@@ -57,12 +56,12 @@
</el-form-item> </el-form-item>
<!-- 是否同步发布 --> <!-- 是否同步发布 -->
<!-- <el-form-item label="*是否同步发布到实践" class="mb-6"> <el-form-item label="*是否同步发布到实践" class="mb-6">
<el-radio-group v-model="form.publishToPractice"> <el-radio-group v-model="form.publishToPractice">
<el-radio :label="true"></el-radio> <el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio> <el-radio :label="false"></el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> --> </el-form-item>
<!-- 发布时间 --> <!-- 发布时间 -->
<!-- <el-form-item label="*发布时间" class="mb-8"> <!-- <el-form-item label="*发布时间" class="mb-8">
...@@ -82,11 +81,9 @@ ...@@ -82,11 +81,9 @@
<!-- 底部按钮组 --> <!-- 底部按钮组 -->
<div class="flex items-center justify-between gap-4 pt-4"> <div class="flex items-center justify-between gap-4 pt-4">
<div class="flex gap-4"> <div class="flex gap-4">
<el-button size="large" @click="handleCancel"> 取消 </el-button> <el-button @click="handleCancel"> 取消 </el-button>
<el-button size="large" @click="handlePreview"> 预览 </el-button> <el-button @click="handlePreview"> 预览 </el-button>
<el-button size="large" type="info" plain @click="handleSaveDraft"> <el-button type="info" plain @click="handleSaveDraft"> 存草稿 </el-button>
存草稿
</el-button>
</div> </div>
<el-button type="primary" @click="handleSubmit"> 提交 </el-button> <el-button type="primary" @click="handleSubmit"> 提交 </el-button>
</div> </div>
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<div class="col-span-8 space-y-6"> <div class="col-span-8 space-y-6">
<!-- 视频上传区域 --> <!-- 视频上传区域 -->
<div <div
class="bg-white backdrop-blur-sm rounded-2xl shadow-lg border border-white/20 p-8 hover:shadow-xl transition-all duration-300" class="bg-white backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-8 hover:shadow-xl transition-all duration-300"
> >
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-bold text-gray-800">上传视频</h3> <h3 class="text-xl font-bold text-gray-800">上传视频</h3>
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
</div> </div>
<!-- 基本设置 --> <!-- 基本设置 -->
<div <div
class="bg-white backdrop-blur-sm rounded-2xl shadow-lg border border-white/20 p-8 hover:shadow-xl transition-all duration-300" class="bg-white backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-8 hover:shadow-xl transition-all duration-300"
> >
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-bold text-gray-800">基本设置</h3> <h3 class="text-xl font-bold text-gray-800">基本设置</h3>
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
<!-- 主封面 --> <!-- 主封面 -->
<div class="relative group"> <div class="relative group">
<div <div
class="w-48 h-28 bg-gradient-to-br from-gray-100 to-gray-200 rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-all duration-300" class="w-48 h-28 bg-gradient-to-br from-gray-100 to-gray-200 rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-all duration-300"
> >
<img <img
src="@/assets/img/culture/ask.png" src="@/assets/img/culture/ask.png"
...@@ -126,12 +126,12 @@ ...@@ -126,12 +126,12 @@
</div> </div>
</div> </div>
<div <div
class="bg-white backdrop-blur-sm rounded-2xl shadow-lg border border-white/20 p-6 hover:shadow-xl transition-all duration-300" class="bg-white backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-6 hover:shadow-xl transition-all duration-300"
> >
<h4 class="text-lg font-bold text-gray-800 mb-4">发布设置</h4> <h4 class="text-lg font-bold text-gray-800 mb-4">发布设置</h4>
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-xl"> <div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div> <div>
<div class="text-sm font-medium text-gray-800">定时发布</div> <div class="text-sm font-medium text-gray-800">定时发布</div>
<div class="text-xs text-gray-500">设置发布时间</div> <div class="text-xs text-gray-500">设置发布时间</div>
...@@ -160,9 +160,7 @@ ...@@ -160,9 +160,7 @@
</div> </div>
</div> </div>
</div> </div>
<div <div class="bg-white/70 backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-6">
class="bg-white/70 backdrop-blur-sm rounded-2xl shadow-lg border border-white/20 p-6"
>
<div class="space-y-3 flex justify-center items-center gap-4"> <div class="space-y-3 flex justify-center items-center gap-4">
<el-button <el-button
size="large" size="large"
...@@ -188,7 +186,7 @@ ...@@ -188,7 +186,7 @@
</el-form> </el-form>
<!-- 右上角tips --> <!-- 右上角tips -->
<!-- <div <!-- <div
class="w-200px fixed bottom-0 left-0 bg-gradient-to-br from-amber-50 to-orange-50 rounded-2xl p-6 border border-amber-200" class="w-200px fixed bottom-0 left-0 bg-gradient-to-br from-amber-50 to-orange-50 rounded-lg p-6 border border-amber-200"
> >
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<el-icon class="text-amber-500 mt-1"><Warning /></el-icon> <el-icon class="text-amber-500 mt-1"><Warning /></el-icon>
...@@ -246,6 +244,8 @@ const tansformData = () => { ...@@ -246,6 +244,8 @@ const tansformData = () => {
sort: index, sort: index,
})), })),
mainTagId: Number(form.value.mainTagId), mainTagId: Number(form.value.mainTagId),
faceUrl:
'https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/readName/png/2025/11/21/Common/1763710823097.png',
} }
} }
const handleSubmit = async () => { const handleSubmit = async () => {
......
...@@ -88,7 +88,7 @@ ...@@ -88,7 +88,7 @@
v-if="list.length" v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200" class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
> >
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"> <div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
v-if="list.length" v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200" class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
> >
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"> <div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
......
<template> <template>
<div class="flex-1 flex flex-col" v-loading="loading"> <div class="flex-1 flex flex-col" v-loading="loading">
<!-- List Container -->
<div class="flex-1 p-4 pt-1"> <div class="flex-1 p-4 pt-1">
<el-tabs v-model="searchParams.type" @tab-change="toggleTab"> <el-tabs v-model="searchParams.type" @tab-change="toggleTab">
<el-tab-pane <el-tab-pane
...@@ -16,7 +15,6 @@ ...@@ -16,7 +15,6 @@
<el-icon class="text-2xl text-gray-300"><Document /></el-icon> <el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div> </div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div> <div class="text-gray-500 text-lg mb-2">暂无内容</div>
<div class="text-gray-400 text-sm">{{ getEmptyText() }}</div>
</div> </div>
<div v-else class="space-y-4"> <div v-else class="space-y-4">
...@@ -25,7 +23,6 @@ ...@@ -25,7 +23,6 @@
:key="item.id" :key="item.id"
class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer" class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
> >
<!-- Content -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div> <div class="text-gray-900 font-medium truncate">{{ item.title }}</div>
<div class="text-gray-500 text-sm mt-1 truncate"> <div class="text-gray-500 text-sm mt-1 truncate">
...@@ -39,10 +36,8 @@ ...@@ -39,10 +36,8 @@
</div> </div>
</div> </div>
<!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4"> <div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link>编辑</el-button> <el-button type="primary" link @click="handleView(item)">查看</el-button>
<el-button type="danger" link>删除</el-button>
</div> </div>
</div> </div>
</div> </div>
...@@ -52,7 +47,7 @@ ...@@ -52,7 +47,7 @@
v-if="list.length" v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200" class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
> >
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"> <div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
...@@ -76,13 +71,16 @@ import { usePageSearch } from '@/hooks' ...@@ -76,13 +71,16 @@ import { usePageSearch } from '@/hooks'
import { articleTypeListOptions } from '@/constants/options' import { articleTypeListOptions } from '@/constants/options'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ArticleTypeEnum } from '@/constants/enums' import { ArticleTypeEnum } from '@/constants/enums'
import type { TabPaneName } from 'element-plus'
import type { SelfCollectDetailDto } from '@/api'
const toggleTab = (key: string) => { const router = useRouter()
searchParams.value.type = key
const toggleTab = (key: TabPaneName) => {
searchParams.value.type = key as ArticleTypeEnum
refresh() refresh()
} }
// State
const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch( const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch(
getSelfCollectList, getSelfCollectList,
{ {
...@@ -92,22 +90,12 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize } ...@@ -92,22 +90,12 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
}, },
) )
// Computed const handleView = (item: SelfCollectDetailDto) => {
const paginatedList = computed(() => { console.log(item)
const start = (searchParams.value.current - 1) * searchParams.value.size if (item.type === ArticleTypeEnum.VIDEO) {
const end = start + searchParams.value.size router.push(`/videoDetail/${item.id}`)
return list.value.slice(start, end) } else {
}) router.push(`/postDetail/${item.id}`)
const getEmptyText = () => {
const emptyTexts: Record<string, string> = {
posts: '还没有发布任何帖子',
videos: '还没有上传任何视频',
questions: '还没有提出任何问题',
articles: '还没有发表任何专栏文章',
practice: '还没有分享任何实践经验',
interviews: '还没有参与任何专访',
} }
return emptyTexts[searchParams.type] || '暂无数据'
} }
</script> </script>
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
v-if="list.length" v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200" class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
> >
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"> <div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
v-if="list.length" v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200" class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
> >
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"> <div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
v-if="list.length" v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200" class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
> >
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"> <div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
......
<template> <template>
<div class="flex-1 flex flex-col" v-loading="loading"> <div class="flex-1 flex flex-col" v-loading="loading">
<!-- List Container -->
<div class="flex-1 p-4 pt-1"> <div class="flex-1 p-4 pt-1">
<el-tabs v-model="searchParams.type" @tab-change="toggleTab"> <el-tabs v-model="searchParams.type" @tab-change="toggleTab">
<el-tab-pane <el-tab-pane
...@@ -16,7 +15,6 @@ ...@@ -16,7 +15,6 @@
<el-icon class="text-2xl text-gray-300"><Document /></el-icon> <el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div> </div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div> <div class="text-gray-500 text-lg mb-2">暂无内容</div>
<div class="text-gray-400 text-sm">{{ getEmptyText() }}</div>
</div> </div>
<div v-else class="space-y-4"> <div v-else class="space-y-4">
...@@ -25,7 +23,6 @@ ...@@ -25,7 +23,6 @@
:key="item.id" :key="item.id"
class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer" class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
> >
<!-- Content -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div> <div class="text-gray-900 font-medium truncate">{{ item.title }}</div>
<div class="text-gray-500 text-sm mt-1 truncate"> <div class="text-gray-500 text-sm mt-1 truncate">
...@@ -39,10 +36,8 @@ ...@@ -39,10 +36,8 @@
</div> </div>
</div> </div>
<!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4"> <div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link>编辑</el-button> <el-button type="primary" link @click="handleView(item)">查看</el-button>
<el-button type="danger" link>删除</el-button>
</div> </div>
</div> </div>
</div> </div>
...@@ -52,7 +47,7 @@ ...@@ -52,7 +47,7 @@
v-if="list.length" v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200" class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
> >
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"> <div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
...@@ -70,19 +65,21 @@ ...@@ -70,19 +65,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Document } from '@element-plus/icons-vue' import { Document } from '@element-plus/icons-vue'
import type { TabPaneName } from 'element-plus'
import { getSelfPraiseList } from '@/api' import { getSelfPraiseList } from '@/api'
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import { articleTypeListOptions } from '@/constants/options' import { articleTypeListOptions } from '@/constants/options'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ArticleTypeEnum } from '@/constants/enums' import { ArticleTypeEnum } from '@/constants/enums'
import type { SelfPraiseDetailDto } from '@/api/user/types'
const router = useRouter()
const toggleTab = (key: string) => { const toggleTab = (key: TabPaneName) => {
searchParams.value.type = key searchParams.value.type = key as ArticleTypeEnum
refresh() refresh()
} }
// State
const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch( const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch(
getSelfPraiseList, getSelfPraiseList,
{ {
...@@ -92,22 +89,11 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize } ...@@ -92,22 +89,11 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
}, },
) )
// Computed const handleView = (item: SelfPraiseDetailDto) => {
const paginatedList = computed(() => { if (item.type === ArticleTypeEnum.VIDEO) {
const start = (searchParams.value.current - 1) * searchParams.value.size router.push(`/videoDetail/${item.id}`)
const end = start + searchParams.value.size } else {
return list.value.slice(start, end) router.push(`/postDetail/${item.id}`)
})
const getEmptyText = () => {
const emptyTexts: Record<string, string> = {
posts: '还没有发布任何帖子',
videos: '还没有上传任何视频',
questions: '还没有提出任何问题',
articles: '还没有发表任何专栏文章',
practice: '还没有分享任何实践经验',
interviews: '还没有参与任何专访',
} }
return emptyTexts[searchParams.type] || '暂无数据'
} }
</script> </script>
<template> <template>
<div class="flex-1 flex flex-col" v-loading="loading"> <div class="flex-1 flex flex-col" v-loading="loading">
<!-- List Container -->
<div class="flex-1 p-4 pt-1"> <div class="flex-1 p-4 pt-1">
<el-tabs v-model="searchParams.type" @tab-change="toggleTab"> <el-tabs v-model="searchParams.type" @tab-change="toggleTab">
<el-tab-pane <el-tab-pane
...@@ -16,7 +15,6 @@ ...@@ -16,7 +15,6 @@
<el-icon class="text-2xl text-gray-300"><Document /></el-icon> <el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div> </div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div> <div class="text-gray-500 text-lg mb-2">暂无内容</div>
<div class="text-gray-400 text-sm">{{ getEmptyText() }}</div>
</div> </div>
<div v-else class="space-y-4"> <div v-else class="space-y-4">
...@@ -25,7 +23,6 @@ ...@@ -25,7 +23,6 @@
:key="item.id" :key="item.id"
class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer" class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
> >
<!-- Content -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div> <div class="text-gray-900 font-medium truncate">{{ item.title }}</div>
<div class="text-gray-500 text-sm mt-1 truncate"> <div class="text-gray-500 text-sm mt-1 truncate">
...@@ -39,9 +36,8 @@ ...@@ -39,9 +36,8 @@
</div> </div>
</div> </div>
<!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4"> <div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link>编辑</el-button> <el-button type="primary" link @click="handleView(item)">查看</el-button>
<el-button type="danger" link>删除</el-button> <el-button type="danger" link>删除</el-button>
</div> </div>
</div> </div>
...@@ -52,7 +48,7 @@ ...@@ -52,7 +48,7 @@
v-if="list.length" v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200" class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
> >
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"> <div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
...@@ -70,19 +66,20 @@ ...@@ -70,19 +66,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Document } from '@element-plus/icons-vue' import { Document } from '@element-plus/icons-vue'
import { getSelfPublishList } from '@/api' import { getSelfPublishList } from '@/api'
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import { articleTypeListOptions } from '@/constants/options' import { articleTypeListOptions } from '@/constants/options'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ArticleTypeEnum } from '@/constants/enums' import { ArticleTypeEnum } from '@/constants/enums'
import type { SelfPublishDetailDto } from '@/api'
import type { TabPaneName } from 'element-plus'
const router = useRouter()
const toggleTab = (key: string) => { const toggleTab = (key: TabPaneName) => {
searchParams.value.type = key searchParams.value.type = key as ArticleTypeEnum
refresh() refresh()
} }
// State
const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch( const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch(
getSelfPublishList, getSelfPublishList,
{ {
...@@ -92,22 +89,11 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize } ...@@ -92,22 +89,11 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
}, },
) )
// Computed const handleView = (item: SelfPublishDetailDto) => {
const paginatedList = computed(() => { if (item.type === ArticleTypeEnum.VIDEO) {
const start = (searchParams.value.current - 1) * searchParams.value.size router.push(`/videoDetail/${item.id}`)
const end = start + searchParams.value.size } else {
return list.value.slice(start, end) router.push(`/postDetail/${item.id}`)
})
const getEmptyText = () => {
const emptyTexts: Record<string, string> = {
posts: '还没有发布任何帖子',
videos: '还没有上传任何视频',
questions: '还没有提出任何问题',
articles: '还没有发表任何专栏文章',
practice: '还没有分享任何实践经验',
interviews: '还没有参与任何专访',
} }
return emptyTexts[searchParams.type] || '暂无数据'
} }
</script> </script>
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
<el-icon class="text-2xl text-gray-300"><Document /></el-icon> <el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div> </div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div> <div class="text-gray-500 text-lg mb-2">暂无内容</div>
<div class="text-gray-400 text-sm">{{ getEmptyText() }}</div>
</div> </div>
<div v-else class="space-y-4"> <div v-else class="space-y-4">
...@@ -25,24 +24,27 @@ ...@@ -25,24 +24,27 @@
:key="item.id" :key="item.id"
class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer" class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
> >
<!-- Content -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div> <div class="text-gray-900 font-medium truncate">
{{ item.title }}({{ item.currentCount }}/{{ item.limitCount }})
</div>
<div class="text-gray-500 text-sm mt-1 truncate"> <div class="text-gray-500 text-sm mt-1 truncate">
<span class="mr-2">{{ item.description }}</span>
<span class="mr-2"> +{{ item.rewardValue }}亚币</span>
<span class="mr-2"> <span class="mr-2">
{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }} {{
TaskDateLimitTypeText[item.limitType] &&
`${TaskDateLimitTypeText[item.limitType] + '刷新'}`
}}
</span> </span>
<span class="mr-2">浏览 {{ item.viewCount }}</span>
<span class="mr-2">点赞 {{ item.replyCount }}</span>
<span class="mr-2">评论 {{ item.collectionCount }}</span>
<span class="mr-2">收藏 {{ item.praiseCount }}</span>
</div> </div>
</div> </div>
<!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4"> <div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link>编辑</el-button> <el-button v-if="item.currentCount === item.limitCount" type="info" link disabled
<el-button type="danger" link>删除</el-button> >已完成</el-button
>
<el-button v-else type="primary" link>去完成</el-button>
</div> </div>
</div> </div>
</div> </div>
...@@ -52,7 +54,7 @@ ...@@ -52,7 +54,7 @@
v-if="list.length" v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200" class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
> >
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"> <div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
...@@ -76,13 +78,14 @@ import { usePageSearch } from '@/hooks' ...@@ -76,13 +78,14 @@ import { usePageSearch } from '@/hooks'
import { taskTypeListOptions } from '@/constants/options' import { taskTypeListOptions } from '@/constants/options'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { TaskTypeEnum } from '@/constants/enums' import { TaskTypeEnum } from '@/constants/enums'
import type { TabPaneName } from 'element-plus'
import { TaskDateLimitTypeText } from '@/constants'
const toggleTab = (key: string) => { const toggleTab = (key: TabPaneName) => {
searchParams.value.taskType = key searchParams.value.taskType = key as TaskTypeEnum
refresh() refresh()
} }
// State
const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch( const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch(
getSelfTaskList, getSelfTaskList,
{ {
...@@ -91,23 +94,4 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize } ...@@ -91,23 +94,4 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
}, },
}, },
) )
// 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.value.taskType] || '暂无数据'
}
</script> </script>
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
<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="info" plain size="small" @click="handleAdmin">后台管理</el-button>
</div> </div>
</div> </div>
...@@ -81,53 +82,6 @@ ...@@ -81,53 +82,6 @@
<component :is="currentComponent" /> <component :is="currentComponent" />
</keep-alive> </keep-alive>
</transition> </transition>
<!-- <div class="border-b border-gray-200">
<div class="flex">
<div
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key"
:class="[
'px-6 py-4 cursor-pointer text-sm font-medium transition-colors relative',
activeTab === tab.key ? 'text-blue-600' : 'text-gray-500 hover:text-gray-700',
]"
>
{{ tab.label }}
<div
v-if="activeTab === tab.key"
class="absolute bottom-0 left-0 right-0 h-2px bg-blue-600"
></div>
</div>
</div>
</div>
<div class="p-8">
<div class="flex flex-col items-center justify-center py-20">
<div class="w-120px h-120px mb-6 opacity-30">
<svg viewBox="0 0 1024 1024" class="w-full h-full fill-gray-400">
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M623.6 316.7C593.6 290.4 554 276 512 276s-81.6 14.4-111.6 40.7C369.2 344 352 380.7 352 420v7.6c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V420c0-44.1 43.1-80 96-80s96 35.9 96 80c0 31.1-22 59.6-56.1 72.7-21.2 8.1-39.2 22.3-52.1 40.9-13.1 19-19.9 41.8-19.9 65.1V620c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-21.3c0-33.6 32.2-62.1 72.9-81.68C625.9 495.1 672 442.2 672 420c0-39.3-17.2-76-48.4-103.3z"
/>
<circle cx="512" cy="732" r="40" />
</svg>
</div>
<p class="text-gray-400 text-sm">暂无帖子</p>
</div>
<div class="flex justify-center mt-8">
<el-pagination
v-model:current-page="currentPage"
:page-size="10"
:total="0"
layout="prev, pager, next"
:hide-on-single-page="true"
/>
</div>
</div> -->
</div> </div>
</div> </div>
</div> </div>
...@@ -138,15 +92,10 @@ ...@@ -138,15 +92,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
Edit,
User, User,
Trophy,
Document, Document,
Star, Star,
ChatDotRound, ChatDotRound,
Link,
View,
Setting,
Pointer, Pointer,
Collection, Collection,
Finished, Finished,
...@@ -170,16 +119,10 @@ const { userInfo } = storeToRefs(userStore) ...@@ -170,16 +119,10 @@ const { userInfo } = storeToRefs(userStore)
const route = useRoute() const route = useRoute()
const key = route.query.key as string const key = route.query.key as string
console.log(key)
// 当前激活的菜单 // 当前激活的菜单
const activeMenu = ref(key || 'posts') const activeMenu = ref(key || 'posts')
// 当前激活的标签页
const activeTab = ref('published')
// 当前页码
const currentPage = ref(1)
// 左侧普通用户菜单 // 左侧普通用户菜单
const menuUserItems = [ const menuUserItems = [
{ key: 'posts', label: '我的帖子', icon: User, component: SelfPublish, tab: '发布' }, { key: 'posts', label: '我的帖子', icon: User, component: SelfPublish, tab: '发布' },
...@@ -224,6 +167,10 @@ const getIsOfficial = () => { ...@@ -224,6 +167,10 @@ const getIsOfficial = () => {
}, 1000) }, 1000)
} }
const handleAdmin = () => {
window.open('/backend')
}
onMounted(() => { onMounted(() => {
getIsOfficial() getIsOfficial()
}) })
......
<template> <template>
<div class="min-h-screen px-20"> <div class="min-h-screen px-20">
<!-- 左侧主内容区 -->
<div class="lg:col-span-3"> <div class="lg:col-span-3">
<!-- 视频播放器 --> <!-- 视频播放器 -->
<div class="bg-black rounded-2xl overflow-hidden shadow-lg"> <div class="bg-black rounded-lg overflow-hidden shadow-lg">
<video <video
ref="videoRef" ref="videoRef"
:src="videoDetail?.videoUrl" :src="videoDetail?.videoUrl"
...@@ -14,122 +13,106 @@ ...@@ -14,122 +13,106 @@
></video> ></video>
</div> </div>
<!-- 视频信息卡片 --> <!-- 上方卡片:视频信息 -->
<div class="mt-4 bg-white backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 p-6"> <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-5">
<!-- 标题和标签 --> <!-- 标题 -->
<h1 class="text-2xl font-bold text-gray-900 mb-4"> <h1 class="text-xl font-semibold text-gray-900 mb-3 leading-tight">
{{ videoDetail?.title }} {{ videoDetail?.title }}
</h1> </h1>
<div class="flex flex-wrap gap-2 mb-6"> <!-- 数据统计行 -->
<div class="flex items-center gap-4 text-sm text-gray-500 mb-3 flex-wrap">
<span class="flex items-center gap-1">
<i class="i-carbon-view"></i>
{{ formatNumber(videoDetail?.viewCount) }} 播放
</span>
<span class="flex items-center gap-1">
<i class="i-carbon-thumbs-up"></i>
{{ formatNumber(videoDetail?.postPriseCount) }}
</span>
<span class="flex items-center gap-1">
<i class="i-carbon-star"></i>
{{ formatNumber(videoDetail?.collectCount) }}
</span>
<span class="flex items-center gap-1">
<i class="i-carbon-share"></i>
{{ formatNumber(videoDetail?.shareCount) }}
</span>
<span class="ml-auto text-gray-400 text-xs">
{{ dayjs(videoDetail?.createTime * 1000).format('YYYY-MM-DD HH:mm') }}
</span>
</div>
<!-- 标签 -->
<div class="flex flex-wrap gap-2 mb-3">
<span <span
v-for="tag in videoDetail?.tags" v-for="tag in videoDetail?.tagNameList"
:key="tag" :key="tag"
class="px-3 py-1 text-sm bg-gradient-to-r from-cyan-100 to-blue-100 text-cyan-600 rounded-full hover:shadow-md transition-all cursor-pointer" class="px-2.5 py-1 text-xs bg-cyan-50 text-cyan-600 rounded hover:bg-cyan-100 transition-colors cursor-pointer"
> >
#{{ tag }} #{{ tag }}
</span> </span>
</div> </div>
<!-- 视频数据统计 --> <!-- 简介 -->
<div <div
class="flex items-center gap-6 text-sm text-gray-600 mb-6 pb-6 border-b border-gray-100" v-if="videoDetail?.content"
class="pt-3 border-t border-gray-100 text-sm text-gray-600 leading-relaxed whitespace-pre-wrap"
> >
<div class="flex items-center gap-2"> {{ videoDetail?.content }}
<i class="i-carbon-view text-lg"></i>
<span>{{ formatNumber(videoDetail?.viewCount) }} 播放</span>
</div> </div>
<div class="flex items-center gap-2">
<i class="i-carbon-thumbs-up text-lg"></i>
<span>{{ formatNumber(videoDetail?.likeCount) }}</span>
</div>
<div class="flex items-center gap-2">
<i class="i-carbon-star text-lg"></i>
<span>{{ formatNumber(videoDetail?.collectCount) }} 收藏</span>
</div>
<div class="flex items-center gap-2">
<i class="i-carbon-share text-lg"></i>
<span>{{ formatNumber(videoDetail?.shareCount) }} 分享</span>
</div>
<span class="text-gray-400">{{
dayjs(videoDetail?.publishTime * 1000).format('YYYY-MM-DD HH:mm')
}}</span>
</div> </div>
<!-- UP主信息和操作按钮 --> <!-- 下方卡片:UP主和操作 -->
<div class="flex items-center justify-between"> <div class="my-4 bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex items-center gap-4"> <div class="flex items-center justify-between flex-wrap gap-4">
<!-- 左侧:UP主信息 -->
<div class="flex items-center gap-3">
<img <img
:src="videoDetail?.authorAvatar" :src="videoDetail?.createUserAvatar"
alt="" alt=""
class="w-12 h-12 rounded-full object-cover cursor-pointer hover:opacity-80 transition-opacity" class="w-10 h-10 rounded-full object-cover cursor-pointer"
/> />
<div> <div>
<h3 <h3 class="text-sm font-medium text-gray-900 cursor-pointer transition-colors">
class="font-semibold text-gray-800 hover:text-blue-500 cursor-pointer transition-colors" {{ videoDetail?.createUserName }}
>
{{ videoDetail?.authorName }}
</h3> </h3>
<p class="text-sm text-gray-500">{{ formatNumber(videoDetail?.fansCount) }} 粉丝</p>
</div> </div>
<button
class="px-6 py-2 bg-gradient-to-r from-pink-500 to-red-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
@click="handleFollow"
>
<span v-if="!videoDetail?.hasFollowed">+ 关注</span>
<span v-else>已关注</span>
</button>
</div> </div>
<div class="flex items-center gap-3"> <!-- 右侧:操作按钮 -->
<button <div class="flex items-center gap-2">
class="flex items-center gap-2 px-4 py-2 rounded-full transition-all" <el-icon
:class=" class="group-hover:text-blue-500! cursor-pointer"
videoDetail?.hasLiked size="20"
? 'bg-blue-50 text-blue-500' :style="{ color: videoDetail?.hasPraised ? '#409eff' : '#606266' }"
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
"
@click="handleLike"
>
<i class="i-carbon-thumbs-up text-lg"></i>
<span>{{ videoDetail?.hasLiked ? '已赞' : '点赞' }}</span>
</button>
<button
class="flex items-center gap-2 px-4 py-2 rounded-full transition-all"
:class="
videoDetail?.hasCollected
? 'bg-yellow-50 text-yellow-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
"
@click="handleCollect"
> >
<i class="i-carbon-star text-lg"></i> <Star />
<span>{{ videoDetail?.hasCollected ? '已收藏' : '收藏' }}</span> </el-icon>
</button> <span>{{ videoDetail?.praiseCount || 1 }}</span>
<button
class="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-600 rounded-full hover:bg-gray-200 transition-all" <el-icon
@click="handleShare" class="group-hover:text-blue-500! cursor-pointer"
size="20"
:style="{ color: videoDetail?.hasCollect ? '#409eff' : '#606266' }"
> >
<i class="i-carbon-share text-lg"></i> <Star />
<span>分享</span> </el-icon>
</button> <span>{{ videoDetail?.collectionCount || 1 }}</span>
</div> <el-icon class="group-hover:text-blue-500! cursor-pointer" size="20">
</div> <Star />
</el-icon>
<!-- 视频简介 --> <span>{{ videoDetail?.replyCount || 1 }}</span>
<div v-if="videoDetail?.description" class="mt-6 pt-6 border-t border-gray-100">
<div class="text-gray-700 leading-relaxed whitespace-pre-wrap">
{{ videoDetail?.description }}
</div> </div>
</div> </div>
</div> </div>
<!-- 评论区 --> <!-- 评论区 -->
<div <Comment ref="commentRef" :id="videoId" />
<!-- <div
ref="commentRef" ref="commentRef"
class="mt-6 bg-white backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 overflow-hidden" class="mt-6 bg-white backdrop-blur-sm rounded-lg shadow-sm border border-white/50 overflow-hidden"
> >
<!-- 评论头部 -->
<div class="p-6 border-b border-gray-100"> <div class="p-6 border-b border-gray-100">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-xl font-bold text-gray-800"> <span class="text-xl font-bold text-gray-800">
...@@ -153,7 +136,6 @@ ...@@ -153,7 +136,6 @@
</div> </div>
</div> </div>
<!-- 发表评论 -->
<div class="p-6 border-b border-gray-100"> <div class="p-6 border-b border-gray-100">
<div class="flex gap-4"> <div class="flex gap-4">
<img :src="userInfo?.avatar" alt="" class="w-10 h-10 rounded-full object-cover" /> <img :src="userInfo?.avatar" alt="" class="w-10 h-10 rounded-full object-cover" />
...@@ -187,7 +169,6 @@ ...@@ -187,7 +169,6 @@
</div> </div>
</div> </div>
<!-- 评论列表 -->
<div class="divide-y divide-gray-100"> <div class="divide-y divide-gray-100">
<div <div
v-for="comment in commentList" v-for="comment in commentList"
...@@ -238,7 +219,6 @@ ...@@ -238,7 +219,6 @@
</button> </button>
</div> </div>
<!-- 回复输入框 -->
<div v-show="replyingCommentId === comment.id" class="mt-4 flex gap-3"> <div v-show="replyingCommentId === comment.id" class="mt-4 flex gap-3">
<img :src="userInfo?.avatar" alt="" class="w-8 h-8 rounded-full object-cover" /> <img :src="userInfo?.avatar" alt="" class="w-8 h-8 rounded-full object-cover" />
<div class="flex-1"> <div class="flex-1">
...@@ -266,7 +246,6 @@ ...@@ -266,7 +246,6 @@
</div> </div>
</div> </div>
<!-- 子回复列表 -->
<div v-if="comment.replies?.length" class="mt-4 space-y-3"> <div v-if="comment.replies?.length" class="mt-4 space-y-3">
<div <div
v-for="reply in comment.replies" v-for="reply in comment.replies"
...@@ -315,7 +294,6 @@ ...@@ -315,7 +294,6 @@
</div> </div>
</div> </div>
<!-- 分页 -->
<div class="flex justify-center py-6"> <div class="flex justify-center py-6">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
...@@ -326,23 +304,21 @@ ...@@ -326,23 +304,21 @@
layout="prev, pager, next, total" layout="prev, pager, next, total"
/> />
</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 relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { usePageSearch } from '@/hooks/usePageSearch' import { usePageSearch } from '@/hooks/usePageSearch'
import { getArticleDetail } from '@/api' import { getArticleDetail } from '@/api'
import type { ArticleItemDto } from '@/api/article/types'
dayjs.extend(relativeTime) import Comment from '@/components/common/Comment/index.vue'
dayjs.locale('zh-cn')
const userStore = useUserStore() const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
...@@ -352,24 +328,7 @@ const router = useRouter() ...@@ -352,24 +328,7 @@ const router = useRouter()
const videoId = route.params.id as string const videoId = route.params.id as string
// 视频详情 // 视频详情
const videoDetail = ref({ const videoDetail = ref({} as ArticleItemDto)
title: '',
videoUrl: '',
tags: [],
viewCount: 0,
likeCount: 0,
collectCount: 0,
shareCount: 0,
commentCount: 0,
publishTime: 0,
authorName: '',
authorAvatar: '',
fansCount: 0,
description: '',
hasFollowed: false,
hasLiked: false,
hasCollected: false,
})
// 评论筛选 // 评论筛选
const commentFilters = [ const commentFilters = [
...@@ -379,27 +338,11 @@ const commentFilters = [ ...@@ -379,27 +338,11 @@ const commentFilters = [
] ]
const activeFilter = ref('hot') const activeFilter = ref('hot')
// 评论相关
const commentRef = useTemplateRef<HTMLElement | null>('commentRef') const commentRef = useTemplateRef<HTMLElement | null>('commentRef')
const commentContent = ref('') const commentContent = ref('')
const replyContent = ref('') const replyContent = ref('')
const replyingCommentId = ref<number | null>(null) const replyingCommentId = ref<number | null>(null)
// 推荐列表
const recommendList = ref([])
// 模拟数据和API调用
const commentList = ref([])
const { searchParams, goToPage, changePageSize, total } = usePageSearch(
// getVideoCommentList,
Promise.resolve as any,
{
defaultParams: {
videoId,
},
},
)
// 格式化数字 // 格式化数字
const formatNumber = (num: number) => { const formatNumber = (num: number) => {
if (!num) return 0 if (!num) return 0
......
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