Commit 4a88ffa9 by lijiabin

【需求 17679】 wip: 继续完善页面

parent 21438b37
...@@ -3,11 +3,15 @@ import type { ...@@ -3,11 +3,15 @@ import type {
AddOrUpdateArticleDto, AddOrUpdateArticleDto,
ArticleItemDto, ArticleItemDto,
ArticleSearchParams, ArticleSearchParams,
InterviewOptionDto,
ColumnOptionDto,
AddCommentDto,
CommentItemDto,
CommentSearchParams,
InterviewItemDto, InterviewItemDto,
ColumnItemDto, ColumnItemDto,
} from './types' } from './types'
import type { BackendServicePageResult } from '@/utils/request/types' import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types'
import { ArticleTypeEnum } from '@/constants'
// 文章相关的接口(帖子 视频 实践等) // 文章相关的接口(帖子 视频 实践等)
...@@ -44,32 +48,100 @@ export const getArticleDetail = (articleId: number | string) => { ...@@ -44,32 +48,100 @@ export const getArticleDetail = (articleId: number | string) => {
} }
/** /**
* 收藏或者取消收藏 * 获取专访列表选项 --不分页 用户新增时候选择
*/ */
export const addOrCancelCollect = (data: { articleId: number | string; type: ArticleTypeEnum }) => { export const getInterviewOptions = () => {
return service.request<InterviewOptionDto[]>({
url: '/api/cultureColumn/listNoPage?type=interview',
method: 'POST',
})
}
/**
* 获取首页专访列表list —— 分页
*/
export const getInterviewList = (data: PageSearchParams) => {
return service.request<BackendServicePageResult<InterviewItemDto>>({
url: '/api/yaCulture/listByPage',
method: 'POST',
data: {
...data,
type: 'interview',
},
})
}
/**
* 获取专栏列表选项-- 不分页 用户新增时候选择
*/
export const getColumnOptions = () => {
return service.request<ColumnOptionDto[]>({
url: '/api/cultureColumn/listNoPage?type=column',
method: 'POST',
})
}
/**
* 获取首页专栏列表list —— 分页
*/
export const getColumnList = (data: PageSearchParams) => {
return service.request<BackendServicePageResult<ColumnItemDto>>({
url: '/api/yaCulture/listByPage',
method: 'POST',
data: {
...data,
type: 'column',
},
})
}
/**
* 点赞或者取消点赞文章
*/
export const addOrCanceArticlelLike = (articleId: number | string) => {
return service.request<boolean>({ return service.request<boolean>({
url: `/api/cultureCollect/addOrCancelCollect`, url: `/api/cultureArticle/likeArticle?articleId=${articleId}`,
method: 'POST',
})
}
/**
* 收藏或者取消收藏文章
*/
export const addOrCanceArticlelCollect = (articleId: number | string) => {
return service.request<boolean>({
url: `/api/cultureArticle/collectArticle?articleId=${articleId}`,
method: 'POST',
})
}
/**
* 获取评论列表
*/
export const getCommentList = (data: CommentSearchParams) => {
return service.request<BackendServicePageResult<CommentItemDto>>({
url: `/api/cultureComment/getComment`,
method: 'POST', method: 'POST',
data, data,
}) })
} }
/** /**
* 获取专访列表--不分页 * 新增评论
*/ */
export const getInterviewList = () => { export const addComment = (data: AddCommentDto) => {
return service.request<InterviewItemDto[]>({ return service.request<boolean>({
url: '/api/cultureColumn/listNoPage?type=interview', url: `/api/cultureComment/addComment`,
method: 'POST', method: 'POST',
data,
}) })
} }
/** /**
* 获取专栏列表-- 不分页 * 点赞评论
*/ */
export const getColumnList = () => { export const addOrCancelCommentLike = (commentId: number | string) => {
return service.request<ColumnItemDto[]>({ return service.request<boolean>({
url: '/api/cultureColumn/listNoPage?type=column', url: `/api/cultureComment/likeComment?commentId=${commentId}`,
method: 'POST', method: 'POST',
}) })
} }
...@@ -6,6 +6,7 @@ import type { PageSearchParams } from '@/utils/request/types' ...@@ -6,6 +6,7 @@ import type { PageSearchParams } from '@/utils/request/types'
*/ */
export interface ArticleSearchParams extends PageSearchParams { export interface ArticleSearchParams extends PageSearchParams {
type?: ArticleTypeEnum type?: ArticleTypeEnum
sortLogic?: number
} }
/** /**
...@@ -41,7 +42,7 @@ interface AddOrUpdateColumnBase { ...@@ -41,7 +42,7 @@ interface AddOrUpdateColumnBase {
faceUrl?: string faceUrl?: string
imgUrl?: string imgUrl?: string
// 关联的专栏栏目 // 关联的专栏栏目
relateColumn: number relateColumnId: number
mainTagId: string mainTagId: string
isRelateColleague: BooleanFlag isRelateColleague: BooleanFlag
sendTime?: string sendTime?: string
...@@ -71,7 +72,7 @@ export interface AddOrUpdateInterviewBase { ...@@ -71,7 +72,7 @@ export interface AddOrUpdateInterviewBase {
faceUrl?: string faceUrl?: string
imgUrl?: string imgUrl?: string
// 关联的专访栏目 // 关联的专访栏目
relateColumn: number relateColumnId: number
mainTagId: string mainTagId: string
sendTime?: string sendTime?: string
} }
...@@ -95,7 +96,6 @@ export interface AddOrUpdateInterviewDto extends AddOrUpdateInterviewBase { ...@@ -95,7 +96,6 @@ export interface AddOrUpdateInterviewDto extends AddOrUpdateInterviewBase {
* 文章详情 * 文章详情
*/ */
// 更严格的类型定义
export interface ArticleItemDto { export interface ArticleItemDto {
id: number id: number
title: string title: string
...@@ -114,10 +114,17 @@ export interface ArticleItemDto { ...@@ -114,10 +114,17 @@ export interface ArticleItemDto {
praiseCount: number praiseCount: number
collectionCount: number collectionCount: number
replyCount: number replyCount: number
hasPraised: BooleanFlag hasPraised: boolean
hasCollect: boolean
imgUrl: string
createUserAvatar: string
createUserName: string
} }
export interface ColumnItemDto { /**
* 专栏选项
*/
export interface ColumnOptionDto {
color: string color: string
createTime: number createTime: number
createUserId: number createUserId: number
...@@ -129,7 +136,34 @@ export interface ColumnItemDto { ...@@ -129,7 +136,34 @@ export interface ColumnItemDto {
type: 'column' type: 'column'
} }
export interface InterviewItemDto { /**
* 专栏列表Item
*/
export interface ColumnItemDto {
title: string
color: string
sort: number
yaColumnVoList: {
articleId: number
collectCount: number
content: string
createTime: number
description: string
faceUrl: string
hasPraised: boolean
isRecommend: number
praiseCount: number
replyCount: number
title: string
type: ArticleTypeEnum.COLUMN
viewCount: number
}[]
}
/**
* 专访选项
*/
export interface InterviewOptionDto {
color: string color: string
createTime: number createTime: number
createUserId: number createUserId: number
...@@ -140,3 +174,65 @@ export interface InterviewItemDto { ...@@ -140,3 +174,65 @@ export interface InterviewItemDto {
title: string title: string
type: 'column' type: 'column'
} }
/**
* 专访列表Item
*/
export interface InterviewItemDto {
title: string
color: string
sort: number
yaColumnVoList: {
articleId: number
collectCount: number
content: string
createTime: number
description: string
faceUrl: string
hasPraised: boolean
isRecommend: number
praiseCount: number
replyCount: number
title: string
type: ArticleTypeEnum.INTERVIEW
viewCount: number
}[]
}
/**
* 评论列表
*/
export interface CommentSearchParams extends PageSearchParams {
articleId: number | string
}
/**
* 新增评论
*/
export interface AddCommentDto {
articleId: number | string
content: string
pId?: number | string
}
/**
* 评论信息
*/
export interface CommentItemDto {
articleId: number
avatar: string
children: CommentItemDto[]
content: string
createTime: number
hasPraise: BooleanFlag
hiddenAvatar: string
hiddenName: string
id: number
isFeatured: number
isTop: number
pid: number
postPriseCount: number
region: string
regionHide: number
replyUser: string
userId: number
}
// 专栏列表 // 专栏列表
import service from '@/utils/request/index' import service from '@/utils/request/index'
import type { ColumnItemDto } from './types' import type { ColumnOptionDto } from './types'
/** /**
* 获取专栏列表 * 获取专栏列表
*/ */
export const getColumnList = () => { export const getColumnOptions = () => {
return service.request<ColumnItemDto[]>({ return service.request<ColumnOptionDto[]>({
url: '/api/cultureColumn/listNoPage?type=column', url: '/api/cultureColumn/listNoPage?type=column',
method: 'POST', method: 'POST',
}) })
......
import { BooleanFlag } from '@/constants' import { BooleanFlag } from '@/constants'
export interface ColumnItemDto { export interface ColumnOptionDto {
color: string color: string
createTime: number createTime: number
createUserId: number createUserId: number
......
...@@ -4,13 +4,28 @@ import type { FielItemDto } from './types' ...@@ -4,13 +4,28 @@ import type { FielItemDto } from './types'
/** /**
* 获取常规的接口 * 获取常规的接口
*/ */
// export const uploadFile = (file: File, onProgress?: (progress: number) => void) => {
// const formData = new FormData()
// formData.append('file', file)
// return service.request<FielItemDto>({
// url: '/mobiles/file-upload/singleUpload',
// method: 'POST',
// data: formData,
// onUploadProgress: (progressEvent) => {
// const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1))
// onProgress?.(percentCompleted)
// },
// })
// }
/**
* 暂时调用oa正式接口
*/
import axios from 'axios'
export const uploadFile = (file: File, onProgress?: (progress: number) => void) => { export const uploadFile = (file: File, onProgress?: (progress: number) => void) => {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('fileList', file)
return service.request<FielItemDto>({ return axios.post('http://47.112.96.71:8082/mobiles/uploadFile', formData, {
url: '/mobiles/file-upload/singleUpload',
method: 'POST',
data: formData,
onUploadProgress: (progressEvent) => { onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1)) const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1))
onProgress?.(percentCompleted) onProgress?.(percentCompleted)
......
// 专栏列表 // 专栏列表
import service from '@/utils/request/index' import service from '@/utils/request/index'
import type { ColumnItemDto } from './types' import type { ColumnOptionDto } from './types'
/** /**
* 获取专栏列表 * 获取专栏列表
*/ */
export const getInterviewList = () => { export const getInterviewOptions = () => {
return service.request<ColumnItemDto[]>({ return service.request<ColumnOptionDto[]>({
url: '/api/cultureColumn/listNoPage?type=interview', url: '/api/cultureColumn/listNoPage?type=interview',
method: 'POST', method: 'POST',
}) })
......
import { BooleanFlag } from '@/constants' import { BooleanFlag } from '@/constants'
export interface InterviewItemDto { export interface InterviewOptionDto {
color: string color: string
createTime: number createTime: number
createUserId: number createUserId: number
......
...@@ -58,7 +58,7 @@ export interface AuditListItemDto { ...@@ -58,7 +58,7 @@ export interface AuditListItemDto {
isRelateColleague: boolean isRelateColleague: boolean
playCount: number playCount: number
praiseCount: number praiseCount: number
relateColumn: string relateColumnId: string
releaseStatus: AuditStatusEnum releaseStatus: AuditStatusEnum
replyCount: number replyCount: number
showAvatar: string showAvatar: string
......
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1763611281052" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7362" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M190.193225 471.411583c14.446014 0 26.139334-11.718903 26.139334-26.13831 0-14.44499-11.69332-26.164916-26.139334-26.164916-0.271176 0-0.490164 0.149403-0.73678 0.149403l-62.496379 0.146333c-1.425466-0.195451-2.90005-0.295735-4.373611-0.295735-19.677155 0-35.621289 16.141632-35.621289 36.114522L86.622358 888.550075c0 19.949354 15.96767 35.597753 35.670407 35.597753 1.916653 0 3.808746 0.292666 5.649674 0l61.022819 0.022513c0.099261 0 0.148379 0.048095 0.24764 0.048095 0.097214 0 0.146333-0.048095 0.24457-0.048095l0.73678 0 0-0.148379c13.413498-0.540306 24.174586-11.422144 24.174586-24.960485 0-13.55983-10.760065-24.441669-24.174586-24.981974l0-0.393973-50.949392 0 1.450025-402.275993L190.193225 471.409536z" fill="#606266" p-id="7363"></path><path d="M926.52241 433.948343c-19.283182-31.445176-47.339168-44.172035-81.289398-45.546336-1.77032-0.246617-3.536546-0.39295-5.380544-0.39295l-205.447139-0.688685c13.462616-39.059598 22.698978-85.58933 22.698978-129.317251 0-28.349675-3.193739-55.962569-9.041934-82.542948l-0.490164 0.049119c-10.638291-46.578852-51.736315-81.31498-100.966553-81.31498-57.264215 0-95.466282 48.15065-95.466282 106.126063 0 3.241834-0.294712 6.387477 0 9.532097-2.996241 108.386546-91.240027 195.548698-196.23636 207.513194l0 54.881958-0.785899 222.227314 0 229.744521 10.709923 0 500.025271 0.222057 8.746198-0.243547c19.35686 0.049119 30.239721-4.817726 47.803749-16.116049 16.682961-10.761088 29.236881-25.50079 37.490869-42.156122 2.260483-3.341095 4.028757-7.075139 5.106298-11.20111l77.018118-344.324116c1.056052-4.053316 1.348718-8.181333 1.056052-12.160971C943.643346 476.446249 938.781618 453.944769 926.52241 433.948343zM893.82573 486.837924l-82.983993 367.783411-0.099261-0.049119c-2.555196 6.141884-6.879688 11.596106-12.872169 15.427364-4.177136 2.727111-8.773827 4.351098-13.414521 4.964058-1.49812-0.195451-3.046383 0-4.620227 0l-477.028511-0.540306-0.171915-407.408897c89.323375-40.266076 154.841577-79.670527 188.596356-173.661202 0.072655 0.024559 0.124843 0.049119 0.195451 0.072655 2.99931-9.137101 6.313799-20.73423 8.697079-33.164331 5.551436-29.185716 5.258771-58.123792 5.258771-58.123792-4.937452-37.98001 25.940812-52.965306 44.364417-52.965306 25.304316 0.860601 50.263777 33.656541 50.263777 52.326762 0 0 5.600555 27.563776 5.649674 57.190537 0.048095 37.366026-4.6673 56.847729-4.6673 56.847729l-0.466628 0c-5.872754 30.879288-16.214287 60.138682-30.464849 86.964654l0.36839 0.342808c-2.358721 4.815679-3.709485 10.220782-3.709485 15.943111 0 19.922748 19.088754 21.742187 38.765909 21.742187l238.761895 0.270153c0 0 14.666024 0.465604 14.690584 0.465604l0 0.100284c12.132318-0.638543 24.221658 5.207605 31.100322 16.409738 5.504364 9.016351 6.437619 19.6045 3.486404 28.988218L893.82573 486.837924z" fill="#606266" p-id="7364"></path><path d="M264.827039 924.31872c0.319272 0.024559 0.441045 0.024559 0.295735-0.024559 0.243547-0.048095 0.367367-0.074701-0.295735-0.074701s-0.539282 0.026606-0.271176 0.074701C264.43409 924.343279 264.532327 924.343279 264.827039 924.31872z" fill="#606266" p-id="7365"></path></svg>
\ No newline at end of file
...@@ -125,7 +125,9 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => ...@@ -125,7 +125,9 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) =>
// 上传文件 // 上传文件
const { data } = await uploadFileApi(uploadFile.raw) const { data } = await uploadFileApi(uploadFile.raw)
console.log('data', data)
const url = data.fileUrl || data.data[0].filePath
const name = data.fileName || data.data[0].finalName
// ✅ 上传完成后重新查找索引(第二次查找) // ✅ 上传完成后重新查找索引(第二次查找)
fileIndex = fileList.value.findIndex((file) => file.uid === uid) fileIndex = fileList.value.findIndex((file) => file.uid === uid)
...@@ -133,8 +135,8 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => ...@@ -133,8 +135,8 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) =>
// 更新文件信息 // 更新文件信息
fileList.value[fileIndex] = { fileList.value[fileIndex] = {
...fileList.value[fileIndex], ...fileList.value[fileIndex],
url: data.fileUrl, url,
name: data.fileName, name,
status: 'success', status: 'success',
} }
ElMessage.success('上传成功') ElMessage.success('上传成功')
......
...@@ -2,7 +2,7 @@ import type { SetupContext, MaybeRef } from 'vue' ...@@ -2,7 +2,7 @@ import type { SetupContext, MaybeRef } from 'vue'
type Events = { type Events = {
handleBackTop(): void handleBackTop(): void
} }
function ScrollTopComp(_: Record<string, never>, { emit }: SetupContext<Events>) { function ScrollTopComp(_: any, { emit }: SetupContext<Events>) {
return ( return (
<button <button
class="back-top-btn group cursor-pointer flex items-center gap-3 px-4 py-2.5 bg-gradient-to-r from-blue-50 to-indigo-50 hover:from-blue-100 hover:to-indigo-100 border border-blue-200/50 rounded-full transition-all duration-300 hover:shadow-lg hover:-translate-y-1 active:scale-95 shadow-sm" class="back-top-btn group cursor-pointer flex items-center gap-3 px-4 py-2.5 bg-gradient-to-r from-blue-50 to-indigo-50 hover:from-blue-100 hover:to-indigo-100 border border-blue-200/50 rounded-full transition-all duration-300 hover:shadow-lg hover:-translate-y-1 active:scale-95 shadow-sm"
......
...@@ -12,6 +12,7 @@ import type { AddOrUpdateColumnForm, AddOrUpdateColumnDto } from '@/api/article/ ...@@ -12,6 +12,7 @@ import type { AddOrUpdateColumnForm, AddOrUpdateColumnDto } from '@/api/article/
export default defineComponent((_, { expose }) => { export default defineComponent((_, { expose }) => {
const columnStore = useColumnStore() const columnStore = useColumnStore()
const { columnList } = storeToRefs(columnStore) const { columnList } = storeToRefs(columnStore)
console.log(columnList.value)
const [form, resetForm] = useResetData<AddOrUpdateColumnForm>({ const [form, resetForm] = useResetData<AddOrUpdateColumnForm>({
title: '', title: '',
content: '', content: '',
...@@ -23,7 +24,7 @@ export default defineComponent((_, { expose }) => { ...@@ -23,7 +24,7 @@ export default defineComponent((_, { expose }) => {
sendType: SendTypeEnum.IMMEDIATE, sendType: SendTypeEnum.IMMEDIATE,
sendTime: '', sendTime: '',
isRelateColleague: BooleanFlag.NO, isRelateColleague: BooleanFlag.NO,
relateColumn: 0, relateColumnId: 0,
type: ArticleTypeEnum.COLUMN, type: ArticleTypeEnum.COLUMN,
}) })
const formRef = ref<InstanceType<typeof ElForm>>() const formRef = ref<InstanceType<typeof ElForm>>()
...@@ -34,7 +35,7 @@ export default defineComponent((_, { expose }) => { ...@@ -34,7 +35,7 @@ export default defineComponent((_, { expose }) => {
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }], mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }], sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [{ required: true, message: '请选择发布时间', trigger: 'blur' }], sendTime: [{ required: true, message: '请选择发布时间', trigger: 'blur' }],
relateColumn: [ relateColumnId: [
{ required: true, message: '请选择专栏栏目', trigger: 'blur', type: 'number', min: 1 }, { required: true, message: '请选择专栏栏目', trigger: 'blur', type: 'number', min: 1 },
], ],
} }
...@@ -111,8 +112,8 @@ export default defineComponent((_, { expose }) => { ...@@ -111,8 +112,8 @@ export default defineComponent((_, { expose }) => {
{/* @ts-ignore */} {/* @ts-ignore */}
<UploadFile v-model={form.value.imgUrl} /> <UploadFile v-model={form.value.imgUrl} />
</el-form-item> </el-form-item>
<el-form-item label="专栏栏目选择" prop="relateColumn"> <el-form-item label="专栏栏目选择" prop="relateColumnId">
<el-radio-group v-model={form.value.relateColumn}> <el-radio-group v-model={form.value.relateColumnId}>
{columnList.value.map((item) => ( {columnList.value.map((item) => (
<el-radio value={item.id}>{item.title}</el-radio> <el-radio value={item.id}>{item.title}</el-radio>
))} ))}
......
...@@ -22,7 +22,7 @@ export default defineComponent((_, { expose }) => { ...@@ -22,7 +22,7 @@ export default defineComponent((_, { expose }) => {
sendType: SendTypeEnum.IMMEDIATE, sendType: SendTypeEnum.IMMEDIATE,
sendTime: '', sendTime: '',
type: ArticleTypeEnum.INTERVIEW, type: ArticleTypeEnum.INTERVIEW,
relateColumn: 0, relateColumnId: 0,
}) })
const formRef = ref<InstanceType<typeof ElForm>>() const formRef = ref<InstanceType<typeof ElForm>>()
const rules = { const rules = {
...@@ -32,7 +32,7 @@ export default defineComponent((_, { expose }) => { ...@@ -32,7 +32,7 @@ export default defineComponent((_, { expose }) => {
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }], mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }], sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [{ required: true, message: '请选择发布时间', trigger: 'blur' }], sendTime: [{ required: true, message: '请选择发布时间', trigger: 'blur' }],
relateColumn: [ relateColumnId: [
{ required: true, message: '请选择专访栏目', trigger: 'blur', type: 'number', min: 1 }, { required: true, message: '请选择专访栏目', trigger: 'blur', type: 'number', min: 1 },
], ],
} }
...@@ -109,8 +109,8 @@ export default defineComponent((_, { expose }) => { ...@@ -109,8 +109,8 @@ export default defineComponent((_, { expose }) => {
{/* @ts-ignore */} {/* @ts-ignore */}
<UploadFile v-model={form.value.imgUrl} /> <UploadFile v-model={form.value.imgUrl} />
</el-form-item> </el-form-item>
<el-form-item label="专访栏目选择" prop="relateColumn"> <el-form-item label="专访栏目选择" prop="relateColumnId">
<el-radio-group v-model={form.value.relateColumn}> <el-radio-group v-model={form.value.relateColumnId}>
{columnList.value.map((item) => ( {columnList.value.map((item) => (
<el-radio value={item.id}>{item.title}</el-radio> <el-radio value={item.id}>{item.title}</el-radio>
))} ))}
......
...@@ -27,7 +27,7 @@ export default defineComponent( ...@@ -27,7 +27,7 @@ export default defineComponent(
return { return {
...form.value, ...form.value,
releaseStatus, releaseStatus,
imgUrl: form.value.imgUrl?.split(',').filter(Boolean)[0] || '', faceUrl: form.value.imgUrl?.split(',').filter(Boolean)[0] || '',
} }
} }
......
...@@ -34,7 +34,7 @@ const routes = [ ...@@ -34,7 +34,7 @@ const routes = [
], ],
}, },
{ {
path: 'videoDetail/:', path: 'videoDetail/:id',
name: 'CultureVideoDetail', name: 'CultureVideoDetail',
component: () => import('@/views/videoDetail/index.vue'), component: () => import('@/views/videoDetail/index.vue'),
}, },
......
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getColumnList } from '@/api' import { getColumnOptions } from '@/api'
import type { ColumnItemDto } from '@/api/article/types' import type { ColumnOptionDto } from '@/api/article/types'
/** /**
* 关于专栏的不分页数据 * 关于专栏的不分页数据
*/ */
export const useColumnStore = defineStore('column', () => { export const useColumnStore = defineStore('column', () => {
const columnList = ref<ColumnItemDto[]>([]) const columnList = ref<ColumnOptionDto[]>([])
let isLoading = false let isLoading = false
...@@ -14,7 +14,7 @@ export const useColumnStore = defineStore('column', () => { ...@@ -14,7 +14,7 @@ export const useColumnStore = defineStore('column', () => {
if (isLoading) return if (isLoading) return
isLoading = true isLoading = true
try { try {
const { data } = await getColumnList() const { data } = await getColumnOptions()
columnList.value = data columnList.value = data
console.log(columnList.value, 'columnList') console.log(columnList.value, 'columnList')
} catch (error) { } catch (error) {
......
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getInterviewList } from '@/api' import { getInterviewOptions } from '@/api'
import type { InterviewItemDto } from '@/api/article/types' import type { InterviewOptionDto } from '@/api/article/types'
/** /**
* 关于专访的相关数据 --不分页 * 关于专访的相关数据 --不分页
*/ */
export const useInterviewStore = defineStore('interview', () => { export const useInterviewStore = defineStore('interview', () => {
const interviewList = ref<InterviewItemDto[]>([]) const interviewList = ref<InterviewOptionDto[]>([])
let isLoading = false let isLoading = false
...@@ -14,7 +14,7 @@ export const useInterviewStore = defineStore('interview', () => { ...@@ -14,7 +14,7 @@ export const useInterviewStore = defineStore('interview', () => {
if (interviewList.value.length > 0) return if (interviewList.value.length > 0) return
isLoading = true isLoading = true
try { try {
const { data } = await getInterviewList() const { data } = await getInterviewOptions()
interviewList.value = data interviewList.value = data
console.log(interviewList.value, 'interviewList') console.log(interviewList.value, 'interviewList')
} catch (error) { } catch (error) {
......
<template> <template>
<div ref="listRef"> <div ref="listRef">
<div v-loading="loading" v-if="list.length > 0"> <div v-loading="loading" v-if="list.length">
<div class="space-y-3 sm:space-y-4"> <div class="space-y-3 sm:space-y-4">
<div <div
v-for="item in list" v-for="item in list"
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
> >
<div class="flex gap-3 justify-between"> <div class="flex gap-3 justify-between">
<!-- 内容区域 --> <!-- 内容区域 -->
<div class="flex-1 min-w-0 flex flex-col justify-between"> <div class="flex-1 min-w-0 flex flex-col justify-between h-24">
<!-- 标题 --> <!-- 标题 -->
<h2 <h2
class="text-xl font-semibold text-gray-900 line-clamp-1 group-hover:text-blue-600 transition-colors duration-200 leading-tight" class="text-xl font-semibold text-gray-900 line-clamp-1 group-hover:text-blue-600 transition-colors duration-200 leading-tight"
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<!-- 内容摘要 --> <!-- 内容摘要 -->
<div class="my-2 space-y-1"> <div class="my-2 space-y-1">
<p class="text-gray-600 text-sm sm:text-base leading-relaxed line-clamp-2"> <p class="text-gray-600 text-sm sm:text-base leading-relaxed line-clamp-1">
{{ item.content }} {{ item.content }}
</p> </p>
</div> </div>
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
</div> </div>
<!-- 图片区域 --> <!-- 图片区域 -->
<div v-if="item.faceUrl" class="relative flex-shrink-0 w-36 h-24"> <div v-show="item.faceUrl" class="relative flex-shrink-0 w-36 h-24">
<img <img
:src="item.faceUrl" :src="item.faceUrl"
alt="文章配图" alt="文章配图"
...@@ -116,16 +116,16 @@ ...@@ -116,16 +116,16 @@
<script setup lang="ts" name="RecommendList"> <script setup lang="ts" name="RecommendList">
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import { getArticleList } from '@/api' import { getArticleList } from '@/api'
import { ArticleTypeEnum, TABS_REF_KEY } from '@/constants' import { TABS_REF_KEY } from '@/constants'
import { useScrollTop } from '@/hooks' import { useScrollTop } from '@/hooks'
import dayjs from 'dayjs' import dayjs from 'dayjs'
const router = useRouter() const router = useRouter()
const { list, total, searchParams, loading, goToPage, changePageSize, reset } = usePageSearch( const { list, total, searchParams, loading, goToPage, changePageSize, refresh } = usePageSearch(
getArticleList, getArticleList,
{ {
// defaultParams: { type: ArticleTypeEnum.POST }, defaultParams: { sortLogic: 0 },
defaultCurrent: 1, defaultCurrent: 1,
defaultSize: 5, defaultSize: 5,
}, },
...@@ -135,6 +135,10 @@ const tabsRef = inject(TABS_REF_KEY) ...@@ -135,6 +135,10 @@ const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!) const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
defineExpose({ defineExpose({
refresh: reset, refresh: (sortLogic: number) => {
console.log('sortLogic', sortLogic)
searchParams.value.sortLogic = sortLogic
refresh()
},
}) })
</script> </script>
<template> <template>
<div class="min-h-screen bg-gray-50/30"> <div class="bg-gray-50/30">
<!-- 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">
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
</div> </div>
</div> </div>
<!-- 第一页的特殊布局 --> <!-- 第一页的特殊布局 -->
<template v-if="currentPage === 1"> <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">
<!-- 第一个视频 - 占据两列 --> <!-- 第一个视频 - 占据两列 -->
...@@ -34,7 +34,6 @@ ...@@ -34,7 +34,6 @@
<div class="relative overflow-hidden"> <div class="relative overflow-hidden">
<img <img
src="https://picsum.photos/800/400?random=1" src="https://picsum.photos/800/400?random=1"
alt="视频封面"
class="w-full h-72 object-cover group-hover:scale-105 transition-transform duration-700" class="w-full h-72 object-cover group-hover:scale-105 transition-transform duration-700"
/> />
<div <div
...@@ -113,7 +112,6 @@ ...@@ -113,7 +112,6 @@
<div class="relative overflow-hidden"> <div class="relative overflow-hidden">
<img <img
:src="`https://picsum.photos/400/200?random=${n + 1}`" :src="`https://picsum.photos/400/200?random=${n + 1}`"
alt="视频封面"
class="w-full h-36 object-cover group-hover:scale-105 transition-transform duration-500" class="w-full h-36 object-cover group-hover:scale-105 transition-transform duration-500"
/> />
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div> <div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
...@@ -168,14 +166,13 @@ ...@@ -168,14 +166,13 @@
<!-- 剩余视频 - 标准网格 --> <!-- 剩余视频 - 标准网格 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div <div
v-for="item in 12" v-for="item in list"
:key="item" :key="item.id"
class="group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer" class="group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer"
> >
<div class="relative overflow-hidden"> <div class="relative overflow-hidden">
<img <img
:src="`https://picsum.photos/300/200?random=${item + 10}`" :src="item.faceUrl"
alt="视频封面"
class="w-full h-44 object-cover group-hover:scale-105 transition-transform duration-500" class="w-full h-44 object-cover group-hover:scale-105 transition-transform duration-500"
/> />
<div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent"></div> <div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent"></div>
...@@ -184,16 +181,14 @@ ...@@ -184,16 +181,14 @@
<div <div
class="absolute top-3 left-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-2.5 py-1 rounded-full text-xs font-semibold" class="absolute top-3 left-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-2.5 py-1 rounded-full text-xs font-semibold"
> >
{{ item % 2 === 0 ? '🔬 科技' : '🎨 设计' }} {{ item.tagNameList[0] }}
</div> </div>
<!-- 时长 --> <!-- 时长 -->
<div <div
class="absolute bottom-3 right-3 bg-black/80 backdrop-blur-sm text-white px-2 py-1 rounded-lg text-xs" class="absolute bottom-3 right-3 bg-black/80 backdrop-blur-sm text-white px-2 py-1 rounded-lg text-xs"
> >
{{ String(Math.floor(Math.random() * 60)).padStart(2, '0') }}:{{ 15:18
String(Math.floor(Math.random() * 60)).padStart(2, '0')
}}
</div> </div>
<!-- 数据 --> <!-- 数据 -->
...@@ -202,13 +197,13 @@ ...@@ -202,13 +197,13 @@
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg" class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
> >
<SvgIcon name="icon_play" size="12" /> <SvgIcon name="icon_play" size="12" />
<span>{{ (Math.random() * 10).toFixed(1) }}</span> <span>{{ item.viewCount }}</span>
</div> </div>
<div <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg" class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
> >
<SvgIcon name="icon_comment" size="12" /> <SvgIcon name="icon_comment" size="12" />
<span>{{ Math.floor(Math.random() * 1000) }}</span> <span>{{ item.replyCount }}</span>
</div> </div>
</div> </div>
...@@ -228,23 +223,23 @@ ...@@ -228,23 +223,23 @@
<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-2"
> >
{{ ['设计师必看的创意灵感来源', '前端开发实战教程', 'UI设计从入门到精通'][item % 3] }} {{ item.title }}
</h3> </h3>
<p class="text-gray-600 text-sm mb-3 line-clamp-2 leading-relaxed"> <p class="text-gray-600 text-sm mb-3 line-clamp-2 leading-relaxed">
这是一个非常有趣的视频教程,包含了丰富的内容和实用的技巧,适合初学者和进阶用户学习... {{ item.content }}
</p> </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 <div
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" 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"
> >
{{ String(item).slice(-1) }} {{ item.createUserName }}
</div> </div>
<span class="font-medium">UP主{{ item }}</span> <span class="font-medium">UP主{{ 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">{{
>{{ Math.floor(Math.random() * 30) + 1 }}天前</span dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm')
> }}</span>
</div> </div>
</div> </div>
</div> </div>
...@@ -255,14 +250,14 @@ ...@@ -255,14 +250,14 @@
<template v-else> <template v-else>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div <div
v-for="item in 12" @click="router.push(`/videoDetail/${item.id}`)"
:key="item" v-for="item in list"
:key="item.id"
class="group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer" class="group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer"
> >
<div class="relative overflow-hidden"> <div class="relative overflow-hidden">
<img <img
:src="`https://picsum.photos/300/200?random=${item + 10}`" :src="item.faceUrl"
alt="视频封面"
class="w-full h-44 object-cover group-hover:scale-105 transition-transform duration-500" class="w-full h-44 object-cover group-hover:scale-105 transition-transform duration-500"
/> />
<div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent"></div> <div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent"></div>
...@@ -271,16 +266,14 @@ ...@@ -271,16 +266,14 @@
<div <div
class="absolute top-3 left-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-2.5 py-1 rounded-full text-xs font-semibold" class="absolute top-3 left-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-2.5 py-1 rounded-full text-xs font-semibold"
> >
{{ item % 2 === 0 ? '🔬 科技' : '🎨 设计' }} {{ item.tagNameList[0] }}
</div> </div>
<!-- 时长 --> <!-- 时长 -->
<div <div
class="absolute bottom-3 right-3 bg-black/80 backdrop-blur-sm text-white px-2 py-1 rounded-lg text-xs" class="absolute bottom-3 right-3 bg-black/80 backdrop-blur-sm text-white px-2 py-1 rounded-lg text-xs"
> >
{{ String(Math.floor(Math.random() * 60)).padStart(2, '0') }}:{{ 15:18
String(Math.floor(Math.random() * 60)).padStart(2, '0')
}}
</div> </div>
<!-- 数据 --> <!-- 数据 -->
...@@ -288,25 +281,30 @@ ...@@ -288,25 +281,30 @@
<div <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg" class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
> >
<SvgIcon name="icon_play" size="12" /> <el-icon class="text-sm"><View /></el-icon>
<span>{{ (Math.random() * 10).toFixed(1) }}</span> <span>{{ item.viewCount }}</span>
</div> </div>
<div <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg" class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
> >
<SvgIcon name="icon_comment" size="12" /> <el-icon class="text-sm"><ChatDotRound /></el-icon>
<span>{{ Math.floor(Math.random() * 1000) }}</span> <span>{{ item.replyCount }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"><Star /></el-icon>
<span>{{ item.replyCount }}</span>
</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
class="w-12 h-12 bg-white/90 backdrop-blur-sm rounded-full flex items-center justify-center shadow-xl transform scale-90 group-hover:scale-100 transition-transform duration-300" class="bg-white/90 backdrop-blur-sm rounded-full flex items-center justify-center shadow-xl transform scale-90 group-hover:scale-100 transition-transform duration-300"
> >
<SvgIcon name="icon_play" size="20" color="#374151" /> <el-icon size="50" color="#333"><VideoPlay /></el-icon>
</div> </div>
</div> </div>
</div> </div>
...@@ -315,23 +313,21 @@ ...@@ -315,23 +313,21 @@
<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-2"
> >
{{ ['设计师必看的创意灵感来源', '前端开发实战教程', 'UI设计从入门到精通'][item % 3] }} {{ item.title }}
</h3> </h3>
<p class="text-gray-600 text-sm mb-3 line-clamp-2 leading-relaxed"> <p class="text-gray-600 text-sm mb-3 line-clamp-2 leading-relaxed">
这是一个非常有趣的视频教程,包含了丰富的内容和实用的技巧,适合初学者和进阶用户学习... {{ item.content }}
</p> </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 <div
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" 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"
> >
{{ String(item).slice(-1) }} {{ item.createUserName }}
</div> </div>
<span class="font-medium">UP主{{ item }}</span> <span class="font-medium">UP主{{ item.createUserName }}</span>
</div> </div>
<span class="bg-gray-100 px-2 py-1 rounded-full" <span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm') }}</span>
>{{ Math.floor(Math.random() * 30) + 1 }}天前</span
>
</div> </div>
</div> </div>
</div> </div>
...@@ -353,11 +349,11 @@ ...@@ -353,11 +349,11 @@
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3" class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
> >
<el-pagination <el-pagination
v-model:current-page="currentPage" v-model:current-page="searchParams.current"
v-model:page-size="pageSize" v-model:page-size="searchParams.size"
:page-sizes="[15, 30, 45, 60]" :page-sizes="[12, 24, 36, 48]"
layout="prev, pager, next, jumper, total" layout="prev, pager, next, jumper, total"
:total="400" :total="total"
class="custom-pagination" class="custom-pagination"
/> />
</div> </div>
...@@ -371,14 +367,22 @@ ...@@ -371,14 +367,22 @@
<script setup lang="ts" name="RecommendList"> <script setup lang="ts" name="RecommendList">
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useScrollTop } from '@/hooks' import { useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants' import { ArticleTypeEnum, TABS_REF_KEY } from '@/constants'
import { usePageSearch } from '@/hooks'
import { getArticleList } from '@/api'
import dayjs from 'dayjs'
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!) const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
const { list, total, searchParams, loading, goToPage, changePageSize, refresh } = usePageSearch(
const currentPage = ref(1) getArticleList,
const pageSize = ref(15) {
defaultParams: { type: ArticleTypeEnum.VIDEO },
defaultCurrent: 1,
defaultSize: 12,
},
)
// Tabs 配置 // Tabs 配置
const router = useRouter() const router = useRouter()
const tabs = [ const tabs = [
...@@ -392,6 +396,12 @@ const activeTab = ref('latest') ...@@ -392,6 +396,12 @@ const activeTab = ref('latest')
const goVideoDetail = (n: number) => { const goVideoDetail = (n: number) => {
router.push(`/videoDetail?id=${n}`) router.push(`/videoDetail?id=${n}`)
} }
defineExpose({
refresh: (sortLogic: number) => {
searchParams.value.sortLogic = sortLogic
refresh()
},
})
</script> </script>
<style> <style>
......
...@@ -44,12 +44,20 @@ const activeTabComponentRef = ...@@ -44,12 +44,20 @@ const activeTabComponentRef =
useTemplateRef<InstanceType<typeof RecommendList>>('activeTabComponentRef') useTemplateRef<InstanceType<typeof RecommendList>>('activeTabComponentRef')
const handleRefresh = () => { const handleRefresh = () => {
activeTabComponentRef.value?.refresh?.() if (activeTab.value === '推荐') {
activeTabComponentRef.value?.refresh?.(0)
} else if (activeTab.value === '最新') {
activeTabComponentRef.value?.refresh?.(1)
} else if (activeTab.value === '视频') {
activeTabComponentRef.value?.refresh?.(2)
}
} }
const handleTabChange = (tab: string) => { const handleTabChange = (tab: string) => {
if (tab === '最新') { if (tab === '最新') {
activeTabComponentRef.value?.refresh?.() activeTabComponentRef.value?.refresh?.(1)
} else if (tab === '推荐') {
activeTabComponentRef.value?.refresh?.(0)
} }
} }
</script> </script>
......
...@@ -10,22 +10,25 @@ ...@@ -10,22 +10,25 @@
<div class="flex gap-3"> <div class="flex gap-3">
<div class="left flex-1 basis-full xl:basis-3/4 transition-all duration-500"> <div class="left flex-1 basis-full xl:basis-3/4 transition-all duration-500">
<div ref="tabsRef" class="tabs-container h-70px flex relative md:h-60px rounded-lg mb-3"> <div ref="tabsRef" class="tabs-container h-75px flex relative rounded-lg mb-3 shadow-md">
<div <div
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.path" :key="tab.path"
class="flex-1 flex items-center justify-center cursor-pointer relative transition-all duration-300 hover:bg-white/10 gap-2 group" class="flex-1 flex items-center justify-center cursor-pointer relative transition-all duration-300 group"
@click="toggleTab(tab)" @click="toggleTab(tab)"
> >
<svg-icon :name="tab.svg" class="h-60px w-auto md:h-50px sm:h-40px" size="40" />
<div class="text-18px font-500 text-gray-800 md:text-16px sm:text-14px">
{{ tab.name }}
</div>
<!-- 下划线 -->
<div <div
class="absolute bottom-0 left-0 right-0 h-4px bg-[linear-gradient(to_right,#60a5fa_0%,#c084fc_100%)] transform scale-x-0 transition-transform duration-300 origin-center" class="flex items-center gap-2 px-12 py-2.5 rounded-xl transition-all duration-300"
:class="{ 'scale-x-50': activeTab === tab.name }" :class="{
></div> 'bg-#fffdfd shadow-[inset_0_2px_4px_0_rgb(0,0,0,0.1)]': activeTab === tab.name,
'hover:bg-white/60': activeTab !== tab.name,
}"
>
<svg-icon :name="tab.svg" class="h-60px w-auto md:h-50px sm:h-40px" size="40" />
<div class="text-18px font-500 text-gray-800 md:text-16px sm:text-14px">
{{ tab.name }}
</div>
</div>
</div> </div>
</div> </div>
...@@ -162,20 +165,27 @@ ...@@ -162,20 +165,27 @@
<!-- 任务中心 --> <!-- 任务中心 -->
<div class="task-container common-box rounded-lg bg-#FFF0E5"> <div class="task-container common-box rounded-lg bg-#FFF0E5">
<div class="flex items-center justify-between mb-2"> <div class="flex items-start justify-start mb-2">
<div> <div class="w-full">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-1 h-4 bg-gradient-to-b from-pink-500 to-rose-500 rounded-full"></div> <div class="w-1 h-4 bg-gradient-to-b from-pink-500 to-rose-500 rounded-full"></div>
<h1 class="text-sm sm:text-base font-bold">任务中心</h1> <h1 class="text-sm sm:text-base font-bold">任务中心</h1>
</div> </div>
<h2 class="text-xs sm:text-sm mb-1 text-gray-600 ml-3"> <h2
<span class="w-full text-xs sm:text-sm mt-1 text-gray-600 flex items-center justify-between"
v-for="item in taskTypeList" >
:key="item.value" <div>
class="text-#333 cursor-pointer after:content-['|'] after:mx-2 after:text-#999 last:after:content-none" <span
:class="{ 'text-#999': currentTask !== item.value }" v-for="item in taskTypeList"
@click="currentTask = item.value" :key="item.value"
>{{ item.label }} class="text-#333 cursor-pointer after:content-['|'] after:mx-2 after:text-#999 last:after:content-none"
:class="{ 'text-#999': currentTask !== item.value }"
@click="currentTask = item.value"
>{{ item.label }}
</span>
</div>
<span class="text-#999 cursor-pointer" @click="router.push(`/userPage?key=task`)">
查看更多
</span> </span>
</h2> </h2>
</div> </div>
...@@ -210,13 +220,23 @@ ...@@ -210,13 +220,23 @@
</div> </div>
</div> </div>
</div> </div>
<el-button <button
class="bg-[linear-gradient(to_right,#FFC5A1_0%,#FFB77F_100%)] shadow-[0_1px_8px_0_rgba(255,141,54,0.25)] border-none hover:-translate-y-1 transition-all duration-200 text-xs sm:text-sm rounded-full" class="w-72px h-32px shadow-[0_1px_8px_0_rgba(255,141,54,0.25)] border-none text-xs sm:text-sm rounded-full"
type="primary" :class="[
item.currentCount === item.limitCount
? 'bg-#FFC5A1'
: 'bg-[linear-gradient(to_right,#FFC5A1_0%,#FFB77F_100%)] hover:-translate-y-1 transition-all duration-200 cursor-pointer',
]"
@click="handleTask(item)" @click="handleTask(item)"
> >
<span class="text-black text-xs sm:text-sm">去完成</span> <span
</el-button> class="text-black text-sm"
:style="{
color: item.currentCount === item.limitCount ? '#999' : '#000',
}"
>{{ item.currentCount === item.limitCount ? '已完成' : '去完成' }}</span
>
</button>
</div> </div>
<!-- 分割线 --> <!-- 分割线 -->
<el-divider style="margin: 0" /> <el-divider style="margin: 0" />
...@@ -332,6 +352,8 @@ const onDailySign = async () => { ...@@ -332,6 +352,8 @@ const onDailySign = async () => {
} }
const handleTask = (item: TaskItemDto) => { const handleTask = (item: TaskItemDto) => {
if (item.currentCount === item.limitCount) return
// if (item.svgName === 'svgName') { // if (item.svgName === 'svgName') {
handleBackTop() handleBackTop()
triggerAnimation() triggerAnimation()
......
<template> <template>
<div class="w-full max-w-6xl mx-auto"> <div>
<div <div v-loading="loading" v-if="list.length">
v-for="item in columnList" <div class="w-full max-w-6xl mx-auto">
:key="item.id" <div
class="bg-white rounded-lg shadow-sm mb-6 overflow-hidden" v-for="(item, index) in list"
:style="{ '--dynamic-color': item.color }" :key="index"
> class="bg-white rounded-lg shadow-sm mb-6 overflow-hidden"
<div :style="{ '--dynamic-color': item.color }"
class="flex items-center justify-between pr-4 pl-4 pt-2 pb-2 bg-green-50 border-b border-green-100" >
:style="{ backgroundColor: item.color, '--dynamic-color': item.color }" <div
> class="flex items-center justify-between pr-4 pl-4 pt-2 pb-2 bg-green-50 border-b border-green-100"
<h3 class="text-lg font-medium text-gray-800 flex items-center"> :style="{ backgroundColor: item.color, '--dynamic-color': item.color }"
<span class="w-1 h-5 mr-2 bg-#444"></span>
{{ item.title }}
</h3>
<div class="flex items-center cursor-pointer hover:text-[var(--dynamic-color)]">
<span class="mr-1 text-14px color-#606266 hover:text-[var(--dynamic-color)]"
>查看更多</span
> >
<el-icon><ArrowRight /></el-icon> <h3 class="text-lg font-medium text-gray-800 flex items-center">
</div> <span class="w-1 h-5 mr-2 bg-#444"></span>
</div> {{ item.title }}
</h3>
<div class="flex items-center cursor-pointer">
<span class="mr-1 text-14px color-#606266">查看更多</span>
<el-icon><ArrowRight /></el-icon>
</div>
</div>
<div class="p-4"> <div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div v-if="item.yaColumnVoList.length" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div v-for="item in section2Items" :key="item.id" class="group cursor-pointer"> <div
<div class="relative mb-3 overflow-hidden rounded-lg"> v-for="i in item.yaColumnVoList"
<img :key="i.articleId"
src="@/assets/img/culture/ask.png" class="group cursor-pointer"
:alt="item.title" @click="router.push(`/postDetail/${i.articleId}`)"
class="w-full h-32 object-cover group-hover:scale-105 transition-transform duration-300" >
/> <div class="relative mb-3 overflow-hidden rounded-lg">
<div class="absolute top-2 left-2"> <img
<el-tag size="small" class="bg-orange-500 text-white border-none"> :src="i.faceUrl"
{{ item.tag }} class="w-full aspect-[5/3] object-cover group-hover:scale-105 transition-transform duration-300"
</el-tag> />
<div class="absolute top-2 left-2">
<el-tag size="small" class="bg-orange-500 text-white border-none">
1111111
</el-tag>
</div>
</div>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors">
{{ i.title }}
</h3>
<p class="text-xs text-gray-500 mb-3 line-clamp-2">
{{ i.content }}
</p>
<div class="flex items-center justify-between text-xs text-gray-400">
<div class="flex items-center space-x-4">
<span class="flex items-center">
<el-icon class="mr-1"><View /></el-icon>
{{ i.viewCount }}
</span>
<span class="flex items-center">
<el-icon class="mr-1"><ChatDotRound /></el-icon>
{{ i.replyCount }}
</span>
<span class="flex items-center">
<el-icon class="mr-1"><Star /></el-icon>
{{ i.collectCount }}
</span>
</div>
<span>{{ dayjs(i.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div>
</div> </div>
</div> </div>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors"> <div v-else class="flex items-center justify-center h-48">
{{ item.title }} <el-empty description="暂无数据" />
</h3> </div>
<p class="text-xs text-gray-500 mb-3 line-clamp-2"> </div>
{{ item.description }} </div>
</p> </div>
<div class="flex items-center justify-between text-xs text-gray-400"> <div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
<div class="flex items-center space-x-4"> <div class="max-w-7xl mx-auto py-6">
<span class="flex items-center"> <div class="flex items-center justify-between">
<el-icon class="mr-1"><View /></el-icon> <!-- 左侧:回到顶部按钮 -->
{{ item.views }} <div class="left">
</span> <ScrollTopComp />
<span class="flex items-center"> </div>
<el-icon class="mr-1"><ChatDotRound /></el-icon> <!-- 右侧:分页器 -->
{{ item.comments }} <div class="right">
</span> <div
<span class="flex items-center"> class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
<el-icon class="mr-1"><Star /></el-icon> >
{{ item.likes }} <el-pagination
</span> v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:page-sizes="[15, 30, 45, 60]"
layout="prev, pager, next, jumper, total"
:total="total"
class="custom-pagination"
@current-change="
(e) => {
;(handleBackTop(), goToPage(e))
}
"
@size-change="changePageSize"
/>
</div> </div>
<span>{{ item.date }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<template v-else>
<div class="flex items-center justify-center h-full">
<el-empty description="暂无数据" />
</div>
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ArrowRight, View, ChatDotRound, Star } from '@element-plus/icons-vue' import { ArrowRight, View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { useColumnStore } from '@/stores/column' import { getColumnList } from '@/api'
import { storeToRefs } from 'pinia' import { usePageSearch, useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
const columnStore = useColumnStore() import dayjs from 'dayjs'
const { columnList } = storeToRefs(columnStore) import { useRouter } from 'vue-router'
const router = useRouter()
const tabsRef = inject(TABS_REF_KEY)
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const section2Items = ref([ const { list, total, searchParams, goToPage, changePageSize, loading } = usePageSearch(
{ getColumnList,
id: 4,
title: '宿主,你的负面清单该更新啦!',
description: '宿主你好呀!正在文化一本分与平常心的知识,我们一起来学习吧的知识。',
image: 'https://via.placeholder.com/300x200/DDA0DD/FFFFFF?text=Image4',
tag: '推荐',
views: '300',
comments: '0',
likes: '24',
date: '2025-04-13 12:01',
},
{
id: 5,
title: '不打破惯性思维,永远无法创新',
description:
'当我们的思路被惯性思维束缚了,想要得到一些不一样的结果就会变得很困难。但是,我们可以通过一些方法来打破惯性思维。',
image: 'https://via.placeholder.com/300x200/F0E68C/FFFFFF?text=Image5',
tag: '推荐',
views: '280',
comments: '0',
likes: '24',
date: '2025-04-13 12:10',
},
{ {
id: 6, defaultSize: 3,
title: '文化共创|你提意见"我听你"',
description: '巧妙设手段第三名,出让各有所需满意的额外为为你们带来更多一些精彩。',
image: 'https://via.placeholder.com/300x200/FFA07A/FFFFFF?text=Image6',
tag: '推荐',
views: '280',
comments: '0',
likes: '24',
date: '2025-04-13 14:28',
}, },
]) )
</script> </script>
<style scoped></style> <style scoped></style>
<template> <template>
<div class="w-full max-w-6xl mx-auto"> <div>
<div <div v-loading="loading" v-if="list.length">
v-for="item in interviewList" <div class="w-full max-w-6xl mx-auto">
:key="item.id" <div
class="bg-white rounded-lg shadow-sm mb-6 overflow-hidden" v-for="(item, index) in list"
:style="{ '--dynamic-color': item.color }" :key="index"
> class="bg-white rounded-lg shadow-sm mb-6 overflow-hidden"
<div :style="{ '--dynamic-color': item.color }"
class="flex items-center justify-between pr-4 pl-4 pt-2 pb-2 bg-green-50 border-b border-green-100" >
:style="{ backgroundColor: item.color, '--dynamic-color': item.color }" <div
> class="flex items-center justify-between pr-4 pl-4 pt-2 pb-2 bg-green-50 border-b border-green-100"
<h3 class="text-lg font-medium text-gray-800 flex items-center"> :style="{ backgroundColor: item.color, '--dynamic-color': item.color }"
<span class="w-1 h-5 mr-2 bg-#444"></span>
{{ item.title }}
</h3>
<div class="flex items-center cursor-pointer hover:text-[var(--dynamic-color)]">
<span class="mr-1 text-14px color-#606266 hover:text-[var(--dynamic-color)]"
>查看更多</span
> >
<el-icon><ArrowRight /></el-icon> <h3 class="text-lg font-medium text-gray-800 flex items-center">
</div> <span class="w-1 h-5 mr-2 bg-#444"></span>
</div> {{ item.title }}
</h3>
<div class="flex items-center cursor-pointer hover:text-[var(--dynamic-color)]">
<span class="mr-1 text-14px color-#606266 hover:text-[var(--dynamic-color)]"
>查看更多</span
>
<el-icon><ArrowRight /></el-icon>
</div>
</div>
<div class="p-4"> <div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div v-if="item.yaColumnVoList.length" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div v-for="item in section2Items" :key="item.id" class="group cursor-pointer"> <div
<div class="relative mb-3 overflow-hidden rounded-lg"> v-for="i in item.yaColumnVoList"
<img :key="i.articleId"
src="@/assets/img/culture/ask.png" class="group cursor-pointer"
:alt="item.title" @click="router.push(`/postDetail/${i.articleId}`)"
class="w-full h-32 object-cover group-hover:scale-105 transition-transform duration-300" >
/> <div class="relative mb-3 overflow-hidden rounded-lg">
<div class="absolute top-2 left-2"> <img
<el-tag size="small" class="bg-orange-500 text-white border-none"> :src="i.faceUrl"
{{ item.tag }} class="w-full h-32 object-cover group-hover:scale-105 transition-transform duration-300"
</el-tag> />
<div class="absolute top-2 left-2">
<el-tag size="small" class="bg-orange-500 text-white border-none">
1111111
</el-tag>
</div>
</div>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors">
{{ i.title }}
</h3>
<p class="text-xs text-gray-500 mb-3 line-clamp-2">
{{ i.content }}
</p>
<div class="flex items-center justify-between text-xs text-gray-400">
<div class="flex items-center space-x-4">
<span class="flex items-center">
<el-icon class="mr-1"><View /></el-icon>
{{ i.viewCount }}
</span>
<span class="flex items-center">
<el-icon class="mr-1"><ChatDotRound /></el-icon>
{{ i.replyCount }}
</span>
<span class="flex items-center">
<el-icon class="mr-1"><Star /></el-icon>
{{ i.collectCount }}
</span>
</div>
<span>{{ dayjs(i.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div>
</div> </div>
</div> </div>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors"> <div v-else class="flex items-center justify-center h-48">
{{ item.title }} <el-empty description="暂无数据" />
</h3> </div>
<p class="text-xs text-gray-500 mb-3 line-clamp-2"> </div>
{{ item.description }} </div>
</p> </div>
<div class="flex items-center justify-between text-xs text-gray-400"> <div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
<div class="flex items-center space-x-4"> <div class="max-w-7xl mx-auto py-6">
<span class="flex items-center"> <div class="flex items-center justify-between">
<el-icon class="mr-1"><View /></el-icon> <!-- 左侧:回到顶部按钮 -->
{{ item.views }} <div class="left">
</span> <ScrollTopComp />
<span class="flex items-center"> </div>
<el-icon class="mr-1"><ChatDotRound /></el-icon> <!-- 右侧:分页器 -->
{{ item.comments }} <div class="right">
</span> <div
<span class="flex items-center"> class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
<el-icon class="mr-1"><Star /></el-icon> >
{{ item.likes }} <el-pagination
</span> v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:page-sizes="[15, 30, 45, 60]"
layout="prev, pager, next, jumper, total"
:total="total"
class="custom-pagination"
@current-change="
(e) => {
;(handleBackTop(), goToPage(e))
}
"
@size-change="changePageSize"
/>
</div> </div>
<span>{{ item.date }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<template v-else>
<div class="flex items-center justify-center h-full">
<el-empty description="暂无数据" />
</div>
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ArrowRight, View, ChatDotRound, Star } from '@element-plus/icons-vue' import { ArrowRight, View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { useInterviewStore } from '@/stores/interview' import { getInterviewList } from '@/api'
import { storeToRefs } from 'pinia' import { usePageSearch, useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
const interviewStore = useInterviewStore() import { useRouter } from 'vue-router'
const { interviewList } = storeToRefs(interviewStore) import dayjs from 'dayjs'
const router = useRouter()
const tabsRef = inject(TABS_REF_KEY)
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const section2Items = ref([ const { list, total, searchParams, goToPage, changePageSize, loading } = usePageSearch(
{ getInterviewList,
id: 4,
title: '宿主,你的负面清单该更新啦!',
description: '宿主你好呀!正在文化一本分与平常心的知识,我们一起来学习吧的知识。',
image: 'https://via.placeholder.com/300x200/DDA0DD/FFFFFF?text=Image4',
tag: '推荐',
views: '300',
comments: '0',
likes: '24',
date: '2025-04-13 12:01',
},
{
id: 5,
title: '不打破惯性思维,永远无法创新',
description:
'当我们的思路被惯性思维束缚了,想要得到一些不一样的结果就会变得很困难。但是,我们可以通过一些方法来打破惯性思维。',
image: 'https://via.placeholder.com/300x200/F0E68C/FFFFFF?text=Image5',
tag: '推荐',
views: '280',
comments: '0',
likes: '24',
date: '2025-04-13 12:10',
},
{ {
id: 6, defaultSize: 3,
title: '文化共创|你提意见"我听你"',
description: '巧妙设手段第三名,出让各有所需满意的额外为为你们带来更多一些精彩。',
image: 'https://via.placeholder.com/300x200/FFA07A/FFFFFF?text=Image6',
tag: '推荐',
views: '280',
comments: '0',
likes: '24',
date: '2025-04-13 14:28',
}, },
]) )
</script> </script>
<style scoped></style> <style scoped></style>
<template> <template>
<div class="min-h-screen"> <div>
<!-- 头部Tabs --> <!-- 头部Tabs -->
<div class="header h-40px items-center justify-between"> <div class="header h-40px items-center justify-between">
<div class="left flex gap-3 flex items-center"> <div class="left flex gap-3 flex items-center">
...@@ -11,35 +11,9 @@ ...@@ -11,35 +11,9 @@
</div> </div>
</div> </div>
<el-divider style="margin: 10px 0 20px 0" /> <el-divider style="margin: 10px 0 20px 0" />
<ColumnList v-if="activeTab === '专栏'" /> <keep-alive>
<InterviewList v-if="activeTab === '专访'" /> <component :is="curretComponent" />
<PracticeList v-if="activeTab === '实践'" /> </keep-alive>
<!-- 底部分页 -->
<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="flex items-center justify-between">
<!-- 左侧:回到顶部按钮 -->
<div class="left">
<ScrollTopComp />
</div>
<!-- 右侧:分页器 -->
<div class="right">
<div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[15, 30, 45, 60]"
layout="prev, pager, next, jumper, total"
:total="400"
class="custom-pagination"
/>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</template> </template>
...@@ -50,14 +24,14 @@ import { Refresh } from '@element-plus/icons-vue' ...@@ -50,14 +24,14 @@ import { Refresh } from '@element-plus/icons-vue'
import ColumnList from './components/columnList.vue' import ColumnList from './components/columnList.vue'
import InterviewList from './components/interviewList.vue' import InterviewList from './components/interviewList.vue'
import PracticeList from './components/practiceList.vue' import PracticeList from './components/practiceList.vue'
import { useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
const tabsRef = inject(TABS_REF_KEY) const curretComponent = computed(() => {
return {
const { ScrollTopComp } = useScrollTop(tabsRef!) 专栏: ColumnList,
const currentPage = ref(1) 专访: InterviewList,
const pageSize = ref(10) 实践: PracticeList,
}[activeTab.value]
})
const activeTab = ref('专栏') const activeTab = ref('专栏')
const tabs = [ const tabs = [
...@@ -66,12 +40,5 @@ const tabs = [ ...@@ -66,12 +40,5 @@ const tabs = [
{ label: '专访', value: '专访' }, { label: '专访', value: '专访' },
{ label: '视频', value: '视频' }, { label: '视频', value: '视频' },
] ]
const handleBackTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
> >
<div <div
class="flex flex-col items-center justify-center w-56px h-56px transition-all duration-200 group" class="flex flex-col items-center justify-center w-56px h-56px transition-all duration-200 group"
@click="handleClick(item)" @click="handleClick(item as StatItem)"
> >
<el-icon <el-icon
class="group-hover:text-blue-500! cursor-pointer" class="group-hover:text-blue-500! cursor-pointer"
...@@ -39,11 +39,10 @@ import { Star, ChatLineSquare, Pointer } from '@element-plus/icons-vue' ...@@ -39,11 +39,10 @@ import { Star, ChatLineSquare, Pointer } from '@element-plus/icons-vue'
import type { ArticleItemDto } from '@/api' import type { ArticleItemDto } from '@/api'
import type { Component } from 'vue' import type { Component } from 'vue'
import { useScrollTop } from '@/hooks' import { useScrollTop } from '@/hooks'
import { addOrCancelCollect } from '@/api' import { addOrCanceArticlelCollect, addOrCanceArticlelLike } from '@/api'
import { COMMENT_REF_KEY } from '@/constants' import { COMMENT_REF_KEY } from '@/constants'
const { articleDetail } = defineProps<{
articleDetail: ArticleItemDto const modelValue = defineModel<ArticleItemDto>('modelValue', { required: true })
}>()
const commentRef = inject(COMMENT_REF_KEY) const commentRef = inject(COMMENT_REF_KEY)
...@@ -55,38 +54,52 @@ interface StatItem { ...@@ -55,38 +54,52 @@ interface StatItem {
count: number count: number
label: string label: string
active?: boolean active?: boolean
actionFn?: () => Promise<boolean> actionFn?: () => Promise<void>
} }
const stats = computed(() => { const stats = computed(() => {
return [ return [
{ {
icon: Pointer, icon: Pointer,
count: articleDetail?.praiseCount, count: modelValue.value?.praiseCount ?? 0,
label: '点赞', label: '点赞',
active: articleDetail?.viewCount, active: modelValue.value?.hasPraised,
api: addOrCanceArticlelLike,
async actionFn() {
await addOrCanceArticlelLike(modelValue.value.id)
// 前端直接本地修改
const isAdd = !modelValue.value.hasPraised
modelValue.value.hasPraised = isAdd
modelValue.value.praiseCount = isAdd
? modelValue.value.praiseCount + 1
: modelValue.value.praiseCount - 1
ElMessage.success(isAdd ? '点赞成功' : '取消点赞成功')
},
}, },
{ {
icon: Star, icon: Star,
count: articleDetail?.collectionCount, count: modelValue.value?.collectionCount ?? 0,
label: '收藏', label: '收藏',
active: articleDetail?.isRecommend, active: modelValue.value?.hasCollect,
api: addOrCancelCollect, api: addOrCanceArticlelCollect,
async actionFn(item: StatItem) { async actionFn() {
const res = await addOrCancelCollect({ await addOrCanceArticlelCollect(modelValue.value.id)
articleId: articleDetail.id, // 前端直接本地修改
type: articleDetail.type, const isAdd = !modelValue.value.hasCollect
}) modelValue.value.hasCollect = isAdd
if (res) { modelValue.value.collectionCount = isAdd
item.active = !item.active ? modelValue.value.collectionCount + 1
} : modelValue.value.collectionCount - 1
ElMessage.success(isAdd ? '收藏成功' : '取消收藏成功')
}, },
}, },
{ {
icon: ChatLineSquare, icon: ChatLineSquare,
count: articleDetail?.replyCount, count: modelValue.value?.replyCount ?? 0,
label: '评论', label: '评论',
active: articleDetail?.isRecommend, // active: modelValue.value?.replyCount > 0,
actionFn: handleBackTop, actionFn: handleBackTop,
}, },
] ]
......
<template> <template>
<div class="min-h-screen px-20"> <div class="min-h-screen px-20">
<!-- 主内容区 --> <!-- 主内容区 -->
<ActionButtons :articleDetail="articleDetail"></ActionButtons> <ActionButtons v-model="articleDetail"></ActionButtons>
<div class="lg:col-span-3"> <div class="lg:col-span-3">
<!-- 帖子主体 --> <!-- 帖子主体 -->
<div <div
class="bg-white/90 backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 overflow-hidden" class="bg-white backdrop-blur-sm rounded-2xl 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 gap-4"> <div class="flex items-center gap-4">
<div class="relative"> <div class="relative">
<img <img
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face" :src="articleDetail?.createUserAvatar"
alt="" alt=""
class="w-12 h-12 rounded-full object-cover" class="w-12 h-12 rounded-full object-cover"
/> />
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h3 class="font-semibold text-gray-800">前端小李</h3> <h3 class="font-semibold text-gray-800">{{ articleDetail?.createUserName }}</h3>
<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" class="px-2 py-0.5 text-xs bg-gradient-to-r from-purple-100 to-blue-100 text-purple-600 rounded-full"
> >
...@@ -36,11 +36,11 @@ ...@@ -36,11 +36,11 @@
· {{ articleDetail?.viewCount || 0 }} 阅读 · {{ articleDetail?.viewCount || 0 }} 阅读
</p> </p>
</div> </div>
<button <!-- <button
class="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all" class="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
> >
+ 关注 + 关注
</button> </button> -->
</div> </div>
</div> </div>
...@@ -50,57 +50,41 @@ ...@@ -50,57 +50,41 @@
{{ articleDetail?.title }} {{ articleDetail?.title }}
</h1> </h1>
<!-- 标签 -->
<div class="flex flex-wrap gap-2 mb-6">
<span
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"
>
# Vue3
</span>
<span
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"
>
# UnoCSS
</span>
<span
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"
>
# 前端开发
</span>
<span
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"
>
# 最佳实践
</span>
</div>
<!-- 文章内容 --> <!-- 文章内容 -->
<div class="prose prose-lg max-w-none"> <div class="prose prose-lg max-w-none">
<div class="text-gray-700 leading-relaxed space-y-4"> <div class="text-gray-700 leading-relaxed space-y-4">
{{ articleDetail?.description }} {{ articleDetail?.content }}
</div> </div>
<!-- 图片内容 --> <!-- 图片内容 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6"> <div v-if="articleDetail.imgUrl" class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
<img <img
src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=500&h=300&fit=crop" v-for="item in articleDetail.imgUrl.split(',')"
alt="" :key="item"
class="rounded-xl object-cover w-full h-64 hover:scale-105 transition-transform cursor-pointer" :src="item"
/>
<img
src="https://images.unsplash.com/photo-1627398242454-45a1465c2479?w=500&h=300&fit=crop"
alt="" alt=""
class="rounded-xl object-cover w-full h-64 hover:scale-105 transition-transform cursor-pointer" class="rounded-xl object-cover w-full h-64 hover:scale-105 transition-transform cursor-pointer"
/> />
</div> </div>
</div> </div>
<!-- 标签 -->
<div class="flex flex-wrap gap-2 mt-6">
<span
v-for="item in articleDetail?.tagNameList"
:key="item"
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"
>
#{{ item }}
</span>
</div>
</div> </div>
</div> </div>
<!-- 评论区 --> <!-- 评论区 -->
<div <div
ref="commentRef" ref="commentRef"
class="mt-6 bg-white/90 backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 overflow-hidden" class="mt-6 bg-white backdrop-blur-sm rounded-2xl 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">
...@@ -156,7 +140,7 @@ ...@@ -156,7 +140,7 @@
<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()"
@click="handleComment" @click="handleComment()"
> >
发表 发表
</button> </button>
...@@ -166,81 +150,53 @@ ...@@ -166,81 +150,53 @@
</div> </div>
<!-- 评论列表 --> <!-- 评论列表 -->
<div class="divide-y divide-gray-100"> <div class="divide-y divide-gray-100" v-if="list.length">
<!-- 评论1 --> <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">
<img <img :src="item.avatar" alt="" class="w-10 h-10 rounded-full object-cover" />
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&h=100&fit=crop&crop=face" <div class="flex-1">
alt="" <div class="flex items-center gap-2 mb-2">
class="w-10 h-10 rounded-full object-cover" <span class="font-semibold text-gray-800">{{ item.replyUser }}</span>
/> <span
<div class="flex-1"> class="px-2 py-0.5 text-xs bg-gradient-to-r from-purple-100 to-blue-100 text-purple-600 rounded-full"
<div class="flex items-center gap-2 mb-2"> >
<span class="font-semibold text-gray-800">前端大佬</span> 技术专家
<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 class="px-2 py-0.5 text-xs bg-red-100 text-red-600 rounded-full"
> >置顶</span
技术专家
</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">
写得很好!Vue 3 + UnoCSS 确实是一个很棒的组合,我在项目中也有类似的实践。特别是
Composition API 的使用,让代码组织变得更加清晰。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>1小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>24</span>
</button>
<button
class="cursor-pointer hover:text-blue-500 transition-colors"
@click="handleReply(1)"
> >
回复
</button>
</div> </div>
</div> <!-- 换行 -->
<p class="text-gray-700 mb-3 break-all">
<!-- 回复列表 --> {{ item.content }}
<div class="mt-3 ml-4 space-y-3"> </p>
<div class="flex gap-2 p-3 bg-gray-50 rounded-lg"> <div class="flex items-center justify-between">
<img <div class="flex items-center gap-4 text-sm text-gray-500">
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face" <span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
alt="" <!-- <button class="flex items-center gap-1 hover:text-red-500 transition-colors"> -->
class="w-8 h-8 rounded-full object-cover" <div
/> class="flex items-center gap-1 cursor-pointer"
<div class="flex-1"> @click="handleLickComment(item)"
<div class="flex items-center gap-2 mb-1"> >
<span class="font-medium text-sm text-gray-800">前端小李</span> <el-icon
<span class="text-xs text-gray-500">30分钟前</span> :size="16"
</div> :style="{ color: item.hasPraise ? '#409eff' : '#606266' }"
<p class="text-sm text-gray-700"> >
谢谢认可!确实这个组合在开发效率上提升很大。 <Pointer />
</p> </el-icon>
<div class="flex items-center justify-between"> <span>{{ item.postPriseCount }}</span>
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>1小时前</span>
<button
class="flex items-center gap-1 hover:text-red-500 transition-colors"
>
<i class="i-carbon-favorite"></i>
<span>24</span>
</button>
<button
@click="handleReply(1)"
class="cursor-pointer hover:text-blue-500 transition-colors"
>
回复
</button>
</div>
</div> </div>
<!-- </button> -->
<button
class="cursor-pointer hover:text-blue-500 transition-colors"
@click="handleReply(item)"
>
回复
</button>
</div> </div>
</div> </div>
<div v-show="currentId === 1" class="flex gap-3"> <!-- <div v-show="currentId === item.id" class="flex gap-3">
<img <img
:src="userInfo?.avatar" :src="userInfo?.avatar"
alt="" alt=""
...@@ -265,83 +221,62 @@ ...@@ -265,83 +221,62 @@
<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()"
@click="handleComment" @click="handleComment(item.id)"
> >
发表 发表
</button> </button>
</div> </div>
</div> </div>
</div> </div> -->
</div> <!-- 回复列表 -->
</div> <div v-if="item.children.length" class="mt-3 ml-4 space-y-3">
</div> <div
</div> v-for="child in item.children"
<el-divider /> :key="child.id"
class="flex gap-2 p-3 bg-gray-50 rounded-lg"
<!-- 评论2 --> >
<div class="p-4 hover:bg-gray-50/50 transition-colors"> <img :src="child.avatar" alt="" class="w-8 h-8 rounded-full object-cover" />
<div class="flex gap-3"> <div class="flex-1">
<img <div class="flex items-center gap-2 mb-1">
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face" <span class="font-medium text-sm text-gray-800">{{
alt="" child.replyUser
class="w-10 h-10 rounded-full object-cover" }}</span>
/> <span class="text-xs text-gray-500">{{
<div class="flex-1"> dayjs(child.createTime * 1000).format('YYYY-MM-DD HH:mm:ss')
<div class="flex items-center gap-2 mb-2"> }}</span>
<span class="font-semibold text-gray-800">Vue开发者</span> </div>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full" <p class="text-sm text-gray-700">
>热门</span {{ child.content }}
> </p>
</div> <div class="flex items-center justify-between">
<p class="text-gray-700 mb-3"> <div class="flex items-center gap-4 text-sm text-gray-500">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。 <span>{{
</p> dayjs(child.createTime * 1000).format('YYYY-MM-DD HH:mm:ss')
<div class="flex items-center justify-between"> }}</span>
<div class="flex items-center gap-4 text-sm text-gray-500"> <div
<span>2小时前</span> class="flex items-center gap-1 cursor-pointer"
<button class="flex items-center gap-1 hover:text-red-500 transition-colors"> @click="handleLickComment(child)"
<i class="i-carbon-favorite"></i> >
<span>18</span> <el-icon
</button> :size="16"
</div> :style="{ color: child.hasPraise ? '#409eff' : '#606266' }"
</div> >
<Pointer />
<!-- 回复列表 --> </el-icon>
<div class="mt-3 ml-4 space-y-3"> <span>{{ child.postPriseCount }}</span>
<div class="flex gap-2 p-3 bg-gray-50 rounded-lg"> </div>
<img <button
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face" @click="handleReply(child)"
alt="" class="cursor-pointer hover:text-blue-500 transition-colors"
class="w-8 h-8 rounded-full object-cover" >
/> 回复
<div class="flex-1"> </button>
<div class="flex items-center gap-2 mb-1"> </div>
<span class="font-medium text-sm text-gray-800">前端小李</span>
<span class="text-xs text-gray-500">30分钟前</span>
</div>
<p class="text-sm text-gray-700">
谢谢认可!确实这个组合在开发效率上提升很大。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>1小时前</span>
<button
class="flex items-center gap-1 hover:text-red-500 transition-colors"
>
<i class="i-carbon-favorite"></i>
<span>24</span>
</button>
<button
@click="handleReply(1)"
class="cursor-pointer hover:text-blue-500 transition-colors"
>
回复
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-show="currentId === 1" class="flex gap-3"> <div v-show="showCommentBox(item)" class="flex gap-3">
<img <img
:src="userInfo?.avatar" :src="userInfo?.avatar"
alt="" alt=""
...@@ -366,7 +301,7 @@ ...@@ -366,7 +301,7 @@
<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()"
@click="handleComment" @click="handleComment(item.id)"
> >
发表 发表
</button> </button>
...@@ -376,214 +311,20 @@ ...@@ -376,214 +311,20 @@
</div> </div>
</div> </div>
</div> </div>
<el-divider />
</div> </div>
<div class="p-4 hover:bg-gray-50/50 transition-colors"> <!-- 底部分页 -->
<div class="flex gap-3"> <!-- 靠右侧 -->
<img <div class="flex justify-end">
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face" <div class="w-fit">
alt="" <el-pagination
class="w-10 h-10 rounded-full object-cover" v-model:current-page="searchParams.current"
/> v-model:page-size="searchParams.size"
<div class="flex-1"> :total="total"
<div class="flex items-center gap-2 mb-2"> @current-change="goToPage"
<span class="font-semibold text-gray-800">Vue开发者</span> @size-change="changePageSize"
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full" layout="prev, pager, next, total"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
<button
class="cursor-pointer hover:text-blue-500 transition-colors"
@click="handleReply(2)"
>
回复
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/> />
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -594,11 +335,21 @@ ...@@ -594,11 +335,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { getArticleDetail, type ArticleItemDto } from '@/api' import {
getArticleDetail,
type ArticleItemDto,
addComment,
getCommentList,
addOrCancelCommentLike,
} from '@/api'
import ActionButtons from './components/actionButtons.vue' import ActionButtons from './components/actionButtons.vue'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { COMMENT_REF_KEY } from '@/constants' 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 userStore = useUserStore()
const commentRef = useTemplateRef<HTMLElement | null>('commentRef') const commentRef = useTemplateRef<HTMLElement | null>('commentRef')
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
...@@ -611,22 +362,100 @@ const comment = ref('') ...@@ -611,22 +362,100 @@ const comment = ref('')
const currentId = ref(-1) const currentId = ref(-1)
const fetchArticleDetail = async () => { const { list, searchParams, goToPage, changePageSize, total, reset } = usePageSearch(
const { data } = await getArticleDetail(id) getCommentList,
articleDetail.value = data {
defaultParams: {
articleId: id,
},
},
)
const initPage = () => {
Promise.allSettled([getArticleDetail(id)]).then(([r1]) => {
if (r1.status === 'fulfilled') {
articleDetail.value = r1.value.data
}
})
}
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 handleReply = (id: number) => { 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 = '' comment.value = ''
currentId.value = id 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 () => { const handleComment = async (pid?: number) => {
console.log(comment.value) 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) provide(COMMENT_REF_KEY, commentRef)
onMounted(async () => { onMounted(async () => {
fetchArticleDetail() initPage()
}) })
</script> </script>
...@@ -85,10 +85,10 @@ ...@@ -85,10 +85,10 @@
</el-form-item> </el-form-item>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<el-form-item prop="description"> <el-form-item prop="content">
<label class="block text-sm font-semibold text-gray-700 mb-3">视频简介</label> <label class="block text-sm font-semibold text-gray-700 mb-3">视频简介</label>
<el-input <el-input
v-model="form.description" v-model="form.content"
type="textarea" type="textarea"
:rows="5" :rows="5"
placeholder="请写上您希望介绍的内容,让更多的人了解您的作品吧!" placeholder="请写上您希望介绍的内容,让更多的人了解您的作品吧!"
...@@ -201,7 +201,7 @@ ...@@ -201,7 +201,7 @@
<script setup lang="ts"> <script setup lang="ts">
import UploadVideo from '@/components/common/UploadVideo/index.vue' import UploadVideo from '@/components/common/UploadVideo/index.vue'
import { useResetData } from '@/hooks' import { useResetData } from '@/hooks'
import { ArticleTypeEnum, SendTypeEnum } from '@/constants' import { ArticleTypeEnum, ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants'
import { addOrUpdateArticle } from '@/api' import { addOrUpdateArticle } from '@/api'
import SelectTags from '@/components/common/SelectTags/index.vue' import SelectTags from '@/components/common/SelectTags/index.vue'
...@@ -213,11 +213,12 @@ const [form] = useResetData({ ...@@ -213,11 +213,12 @@ const [form] = useResetData({
videoUrl: '', videoUrl: '',
title: '视频标题', title: '视频标题',
type: ArticleTypeEnum.VIDEO, type: ArticleTypeEnum.VIDEO,
description: '', content: '',
mainTagId: '', mainTagId: '',
tagList: [], tagList: [],
sendType: SendTypeEnum.IMMEDIATE, sendType: SendTypeEnum.IMMEDIATE,
sendTime: '', sendTime: '',
releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
}) })
const filterTagsFn = (tags: TagItemDto[]) => { const filterTagsFn = (tags: TagItemDto[]) => {
...@@ -227,7 +228,7 @@ const filterTagsFn = (tags: TagItemDto[]) => { ...@@ -227,7 +228,7 @@ const filterTagsFn = (tags: TagItemDto[]) => {
const rules = { const rules = {
videoUrl: [{ required: true, message: '请上传视频', trigger: 'change' }], videoUrl: [{ required: true, message: '请上传视频', trigger: 'change' }],
title: [{ required: true, message: '请输入视频标题', trigger: 'blur' }], title: [{ required: true, message: '请输入视频标题', trigger: 'blur' }],
description: [{ required: true, message: '请输入视频简介', trigger: 'blur' }], content: [{ required: true, message: '请输入视频简介', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择标签', trigger: 'change' }], mainTagId: [{ required: true, message: '请选择标签', trigger: 'change' }],
} }
......
...@@ -167,9 +167,12 @@ import { hasOfficialAccount } from '@/api' ...@@ -167,9 +167,12 @@ import { hasOfficialAccount } from '@/api'
const editUserInfoRef = useTemplateRef<InstanceType<typeof EditUserInfo>>('editUserInfoRef') const editUserInfoRef = useTemplateRef<InstanceType<typeof EditUserInfo>>('editUserInfoRef')
const userStore = useUserStore() const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
console.log(userInfo.value)
const route = useRoute()
const key = route.query.key as string
console.log(key)
// 当前激活的菜单 // 当前激活的菜单
const activeMenu = ref('posts') const activeMenu = ref(key || 'posts')
// 当前激活的标签页 // 当前激活的标签页
const activeTab = ref('published') const activeTab = ref('published')
......
<template> <template>
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen px-20">
<!-- 主容器 --> <!-- 左侧主内容区 -->
<div class="max-w-6xl mx-auto px-4 py-6"> <div class="lg:col-span-3">
<!-- 视频播放器 --> <!-- 视频播放器 -->
<div class="relative w-full aspect-video bg-black rounded-lg overflow-hidden mb-4"> <div class="bg-black rounded-2xl overflow-hidden shadow-lg">
<div class="absolute inset-0"> <video
<!-- 模拟视频内容 --> ref="videoRef"
<div class="w-full h-full bg-gradient-to-br from-gray-800 to-black relative"> :src="videoDetail?.videoUrl"
<!-- 视频标题叠加 --> class="w-full aspect-video"
<div class="absolute inset-0 flex items-center justify-center"> controls
<div class="text-center"> @play="handlePlay"
<h1 class="text-4xl md:text-6xl font-bold text-yellow-400 mb-4 drop-shadow-lg"> @pause="handlePause"
《黑神话:钟馗》 ></video>
</h1> </div>
<p class="text-xl text-white drop-shadow">里的钟馗,有着怎样的身世?</p>
</div>
</div>
<!-- 播放控制栏 --> <!-- 视频信息卡片 -->
<div <div class="mt-4 bg-white backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 p-6">
class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent p-4" <!-- 标题和标签 -->
> <h1 class="text-2xl font-bold text-gray-900 mb-4">
<div class="flex items-center justify-between text-white"> {{ videoDetail?.title }}
<div class="flex items-center space-x-4"> </h1>
<button class="hover:text-blue-400 transition-colors">⏮️</button>
<button class="hover:text-blue-400 transition-colors">▶️</button> <div class="flex flex-wrap gap-2 mb-6">
<button class="hover:text-blue-400 transition-colors">⏭️</button> <span
<span class="text-sm">00:00 / 10:35</span> v-for="tag in videoDetail?.tags"
</div> :key="tag"
<div class="flex items-center space-x-3"> 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"
<button class="hover:text-blue-400 transition-colors">自动</button> >
<button class="hover:text-blue-400 transition-colors">倍速</button> #{{ tag }}
<button class="hover:text-blue-400 transition-colors">字幕</button> </span>
<button class="hover:text-blue-400 transition-colors">🔊</button>
<button class="hover:text-blue-400 transition-colors">⚙️</button>
<button class="hover:text-blue-400 transition-colors">📺</button>
<button class="hover:text-blue-400 transition-colors"></button>
</div>
</div>
</div>
</div>
</div> </div>
</div>
<!-- 视频信息区域 --> <!-- 视频数据统计 -->
<div class="bg-white rounded-lg p-6 mb-4 shadow-sm"> <div
<!-- 标题和基本信息 --> class="flex items-center gap-6 text-sm text-gray-600 mb-6 pb-6 border-b border-gray-100"
<div class="mb-4"> >
<h1 class="text-xl md:text-2xl font-bold text-gray-900 mb-3 leading-tight"> <div class="flex items-center gap-2">
《黑神话:钟馗》里的钟馗,有着怎样的身世? <i class="i-carbon-view text-lg"></i>
</h1> <span>{{ formatNumber(videoDetail?.viewCount) }} 播放</span>
</div>
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-600 mb-4"> <div class="flex items-center gap-2">
<div class="flex items-center gap-1"> <i class="i-carbon-thumbs-up text-lg"></i>
<span>▶️</span> <span>{{ formatNumber(videoDetail?.likeCount) }}</span>
<span>63.2万</span> </div>
</div> <div class="flex items-center gap-2">
<div class="flex items-center gap-1"> <i class="i-carbon-star text-lg"></i>
<span>💬</span> <span>{{ formatNumber(videoDetail?.collectCount) }} 收藏</span>
<span>589</span> </div>
</div> <div class="flex items-center gap-2">
<div class="text-gray-500">2025-08-20 12:24:18</div> <i class="i-carbon-share text-lg"></i>
<div class="flex items-center gap-1 text-blue-500"> <span>{{ formatNumber(videoDetail?.shareCount) }} 分享</span>
<span>#文化标签</span>
</div>
</div> </div>
<span class="text-gray-400">{{
dayjs(videoDetail?.publishTime * 1000).format('YYYY-MM-DD HH:mm')
}}</span>
</div> </div>
<!-- 互动按钮区域 --> <!-- UP主信息和操作按钮 -->
<div class="flex flex-wrap items-center justify-between gap-4 mb-4"> <div class="flex items-center justify-between">
<div class="flex flex-wrap items-center gap-3"> <div class="flex items-center gap-4">
<img
:src="videoDetail?.authorAvatar"
alt=""
class="w-12 h-12 rounded-full object-cover cursor-pointer hover:opacity-80 transition-opacity"
/>
<div>
<h3
class="font-semibold text-gray-800 hover:text-blue-500 cursor-pointer transition-colors"
>
{{ videoDetail?.authorName }}
</h3>
<p class="text-sm text-gray-500">{{ formatNumber(videoDetail?.fansCount) }} 粉丝</p>
</div>
<button <button
class="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors" 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>👍</span> <span v-if="!videoDetail?.hasFollowed">+ 关注</span>
<span class="font-medium">1.1万</span> <span v-else>已关注</span>
</button> </button>
</div>
<div class="flex items-center gap-3">
<button <button
class="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors border-2 border-blue-500" class="flex items-center gap-2 px-4 py-2 rounded-full transition-all"
:class="
videoDetail?.hasLiked
? 'bg-blue-50 text-blue-500'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
"
@click="handleLike"
> >
<div class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center"> <i class="i-carbon-thumbs-up text-lg"></i>
<span class="text-white text-sm">💰</span> <span>{{ videoDetail?.hasLiked ? '已赞' : '点赞' }}</span>
</div>
<span class="font-medium text-blue-500">1121</span>
</button> </button>
<button <button
class="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors" 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"
> >
<span></span> <i class="i-carbon-star text-lg"></i>
<span class="font-medium">4341</span> <span>{{ videoDetail?.hasCollected ? '已收藏' : '收藏' }}</span>
</button> </button>
<button <button
class="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors" class="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-600 rounded-full hover:bg-gray-200 transition-all"
@click="handleShare"
> >
<span>📤</span> <i class="i-carbon-share text-lg"></i>
<span class="font-medium">488</span> <span>分享</span>
</button> </button>
</div> </div>
</div> </div>
<!-- UP主信息 --> <!-- 视频简介 -->
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg mb-4"> <div v-if="videoDetail?.description" class="mt-6 pt-6 border-t border-gray-100">
<div class="flex items-center gap-3"> <div class="text-gray-700 leading-relaxed whitespace-pre-wrap">
<div {{ videoDetail?.description }}
class="w-12 h-12 bg-gradient-to-r from-pink-400 to-purple-500 rounded-full flex items-center justify-center"
>
<span class="text-white font-bold">UP</span>
</div>
<div>
<h3 class="font-medium text-gray-900">UP主名称</h3>
<p class="text-sm text-gray-600">粉丝 12.3万 · 获赞 45.6万</p>
</div>
</div> </div>
<button
class="px-6 py-2 bg-pink-500 hover:bg-pink-600 text-white rounded-full transition-colors"
>
+ 关注
</button>
</div> </div>
<!-- 评论区 --> </div>
<div class="bg-white rounded-lg p-6 shadow-sm">
<div class="flex items-center justify-between mb-6"> <!-- 评论区 -->
<h3 class="text-lg font-bold flex items-center gap-2"> <div
<span>评论</span> ref="commentRef"
<span class="text-blue-500">1171</span> class="mt-6 bg-white backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 overflow-hidden"
</h3> >
<div class="flex gap-1"> <!-- 评论头部 -->
<button class="px-3 py-1 text-sm bg-pink-100 text-pink-600 rounded transition-colors"> <div class="p-6 border-b border-gray-100">
最热 <div class="flex items-center justify-between">
</button> <span class="text-xl font-bold text-gray-800">
<button 评论 {{ formatNumber(videoDetail?.commentCount) }}
class="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded transition-colors" </span>
> <div class="flex items-center gap-2">
最新
</button>
<button
class="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded transition-colors"
>
精选
</button>
<button <button
class="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded transition-colors" v-for="filter in commentFilters"
:key="filter.value"
class="px-4 py-1.5 text-sm rounded-full transition-all"
:class="
activeFilter === filter.value
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-md'
: 'hover:bg-gray-100 text-gray-600'
"
@click="activeFilter = filter.value"
> >
置顶 {{ filter.label }}
</button> </button>
</div> </div>
</div> </div>
</div>
<!-- 评论输入框 --> <!-- 发表评论 -->
<div class="flex gap-3 mb-6"> <div class="p-6 border-b border-gray-100">
<div <div class="flex gap-4">
class="w-10 h-10 bg-teal-400 rounded-full flex items-center justify-center flex-shrink-0" <img :src="userInfo?.avatar" alt="" class="w-10 h-10 rounded-full object-cover" />
>
<span class="text-white">👤</span>
</div>
<div class="flex-1"> <div class="flex-1">
<input <el-input
type="text" v-model="commentContent"
placeholder="这里是评论区,不是无人区:)" type="textarea"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500 transition-colors" placeholder="发一条友善的评论"
/> :rows="3"
</div> :maxlength="500"
</div> show-word-limit
></el-input>
<!-- 评论列表 --> <div class="flex justify-between items-center mt-3">
<div class="space-y-6"> <div class="flex items-center gap-3">
<!-- 评论项 1 --> <button class="text-gray-500 hover:text-blue-500 transition-colors">
<div class="flex gap-3"> <i class="i-carbon-face-satisfied text-xl"></i>
<div
class="w-10 h-10 bg-red-400 rounded-full flex items-center justify-center flex-shrink-0"
>
<span class="text-white">👤</span>
</div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-medium text-gray-900">秋名</span>
<span class="bg-red-500 text-white text-xs px-2 py-0.5 rounded">UP主</span>
</div>
<p class="text-gray-800 mb-2">
正值抗日反法西斯胜利80周年之际
<br />
《黑神话 钟馗》发布可谓正当其时!🔥
</p>
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2025-08-20 12:41</span>
<button class="flex items-center gap-1 hover:text-blue-500 transition-colors">
<span>👍</span>
<span>887</span>
</button> </button>
<button class="hover:text-blue-500 transition-colors">👎</button> <button class="text-gray-500 hover:text-blue-500 transition-colors">
<button class="hover:text-blue-500 transition-colors">回复</button> <i class="i-carbon-image text-xl"></i>
</div>
</div>
</div>
<!-- 评论项 2 -->
<div class="flex gap-3">
<div
class="w-10 h-10 bg-green-400 rounded-full flex items-center justify-center flex-shrink-0"
>
<span class="text-white">👤</span>
</div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-medium text-gray-900">低昂星残</span>
<span class="bg-red-500 text-white text-xs px-2 py-0.5 rounded">UP主</span>
</div>
<p class="text-gray-800 mb-2">我比较期待:黑神话 妲己🤔</p>
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2025-08-20 13:45</span>
<button class="flex items-center gap-1 hover:text-blue-500 transition-colors">
<span>👍</span>
<span>551</span>
</button> </button>
<button class="hover:text-blue-500 transition-colors">👎</button>
<button class="hover:text-blue-500 transition-colors">回复</button>
</div> </div>
<button
class="px-8 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!commentContent.trim()"
@click="publishComment()"
>
发布
</button>
</div> </div>
</div> </div>
</div>
</div>
<!-- 评论项 3 --> <!-- 评论列表 -->
<div class="flex gap-3"> <div class="divide-y divide-gray-100">
<div <div
class="w-10 h-10 bg-purple-400 rounded-full flex items-center justify-center flex-shrink-0" v-for="comment in commentList"
> :key="comment.id"
<span class="text-white">👤</span> class="p-6 hover:bg-gray-50/50 transition-colors"
</div> >
<div class="flex gap-4">
<img :src="comment.avatar" alt="" class="w-10 h-10 rounded-full object-cover" />
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<span class="font-medium text-gray-900">希木林M</span> <span class="font-semibold text-gray-800">{{ comment.username }}</span>
<span class="bg-red-500 text-white text-xs px-2 py-0.5 rounded">UP主</span> <span
v-if="comment.isAuthor"
class="px-2 py-0.5 text-xs bg-gradient-to-r from-pink-100 to-red-100 text-pink-600 rounded"
>
UP主
</span>
<span
v-if="comment.isPinned"
class="px-2 py-0.5 text-xs bg-yellow-100 text-yellow-600 rounded"
>
置顶
</span>
</div> </div>
<p class="text-gray-800 mb-2">这是黑神话😂</p> <p class="text-gray-700 mb-3 leading-relaxed">{{ comment.content }}</p>
<div class="flex items-center gap-4 text-sm text-gray-500"> <div class="flex items-center gap-6 text-sm text-gray-500">
<span>2025-08-20 14:08</span> <span>{{ dayjs(comment.createTime * 1000).fromNow() }}</span>
<button class="flex items-center gap-1 hover:text-blue-500 transition-colors"> <button
<span>👍</span> class="flex items-center gap-1 hover:text-blue-500 transition-colors"
<span>296</span> @click="handleCommentLike(comment)"
>
<i
:class="
comment.hasLiked
? 'i-carbon-thumbs-up-filled text-blue-500'
: 'i-carbon-thumbs-up'
"
></i>
<span :class="comment.hasLiked ? 'text-blue-500' : ''">{{
comment.likeCount || '点赞'
}}</span>
</button>
<button
class="hover:text-blue-500 transition-colors"
@click="handleReply(comment)"
>
回复
</button> </button>
<button class="hover:text-blue-500 transition-colors">👎</button>
<button class="hover:text-blue-500 transition-colors">回复</button>
</div> </div>
</div>
</div>
<!-- 评论项 4 --> <!-- 回复输入框 -->
<div class="flex gap-3"> <div v-show="replyingCommentId === comment.id" class="mt-4 flex gap-3">
<div <img :src="userInfo?.avatar" alt="" class="w-8 h-8 rounded-full object-cover" />
class="w-10 h-10 bg-pink-400 rounded-full flex items-center justify-center flex-shrink-0" <div class="flex-1">
> <el-input
<span class="text-white">👤</span> v-model="replyContent"
</div> type="textarea"
<div class="flex-1"> :placeholder="`回复 @${comment.username}`"
<div class="flex items-center gap-2 mb-2"> :rows="2"
<span class="font-medium text-gray-900">夏冰</span> ></el-input>
<span class="bg-red-500 text-white text-xs px-2 py-0.5 rounded">UP主</span> <div class="flex justify-end gap-2 mt-2">
<button
class="px-4 py-1.5 text-sm text-gray-600 hover:bg-gray-100 rounded-full transition-all"
@click="replyingCommentId = null"
>
取消
</button>
<button
class="px-4 py-1.5 text-sm bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full hover:shadow-lg transition-all disabled:opacity-50"
:disabled="!replyContent.trim()"
@click="publishComment(comment.id)"
>
回复
</button>
</div>
</div>
</div> </div>
<p class="text-gray-800 mb-2">那还不如叫期待全能游戏本创作的游戏</p>
<div class="flex items-center gap-4 text-sm text-gray-500"> <!-- 子回复列表 -->
<span>2025-08-20 15:07</span> <div v-if="comment.replies?.length" class="mt-4 space-y-3">
<button class="flex items-center gap-1 hover:text-blue-500 transition-colors"> <div
<span>👍</span> v-for="reply in comment.replies"
<span>70</span> :key="reply.id"
</button> class="flex gap-3 p-3 bg-gray-50 rounded-lg"
<button class="hover:text-blue-500 transition-colors">👎</button> >
<button class="hover:text-blue-500 transition-colors">回复</button> <img :src="reply.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">{{ reply.username }}</span>
<span class="text-gray-400 text-xs">回复</span>
<span class="font-medium text-sm text-gray-800"
>@{{ reply.replyToUser }}</span
>
</div>
<p class="text-sm text-gray-700 mb-2">{{ reply.content }}</p>
<div class="flex items-center gap-4 text-xs text-gray-500">
<span>{{ dayjs(reply.createTime * 1000).fromNow() }}</span>
<button
class="flex items-center gap-1 hover:text-blue-500 transition-colors"
@click="handleCommentLike(reply)"
>
<i
:class="
reply.hasLiked
? 'i-carbon-thumbs-up-filled text-blue-500'
: 'i-carbon-thumbs-up'
"
></i>
<span :class="reply.hasLiked ? 'text-blue-500' : ''">{{
reply.likeCount || '点赞'
}}</span>
</button>
<button
class="hover:text-blue-500 transition-colors"
@click="handleReply(reply, comment.id)"
>
回复
</button>
</div>
</div>
</div>
</div> </div>
<div class="text-xs text-gray-500 mt-3">共37条回复,点击查看</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 分页 -->
<div class="flex justify-center py-6">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:total="total"
@current-change="goToPage"
@size-change="changePageSize"
layout="prev, pager, next, total"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
// 可以在这里添加响应式数据和方法 import dayjs from 'dayjs'
</script> import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import { usePageSearch } from '@/hooks/usePageSearch'
import { getArticleDetail } from '@/api'
dayjs.extend(relativeTime)
dayjs.locale('zh-cn')
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const route = useRoute()
const router = useRouter()
const videoId = route.params.id as string
<style scoped lang="scss"> // 视频详情
// 自定义样式 const videoDetail = ref({
.line-clamp-2 { title: '',
display: -webkit-box; videoUrl: '',
-webkit-line-clamp: 2; tags: [],
-webkit-box-orient: vertical; viewCount: 0,
overflow: hidden; likeCount: 0,
collectCount: 0,
shareCount: 0,
commentCount: 0,
publishTime: 0,
authorName: '',
authorAvatar: '',
fansCount: 0,
description: '',
hasFollowed: false,
hasLiked: false,
hasCollected: false,
})
// 评论筛选
const commentFilters = [
{ label: '最热', value: 'hot' },
{ label: '最新', value: 'new' },
{ label: '置顶', value: 'pinned' },
]
const activeFilter = ref('hot')
// 评论相关
const commentRef = useTemplateRef<HTMLElement | null>('commentRef')
const commentContent = ref('')
const replyContent = ref('')
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) => {
if (!num) return 0
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万'
}
return num
}
// 视频播放处理
const handlePlay = () => {
// 记录播放
} }
</style>
const handlePause = () => {
// 记录暂停
}
// 关注
const handleFollow = () => {
videoDetail.value.hasFollowed = !videoDetail.value.hasFollowed
ElMessage.success(videoDetail.value.hasFollowed ? '关注成功' : '取消关注')
}
// 点赞
const handleLike = () => {
videoDetail.value.hasLiked = !videoDetail.value.hasLiked
videoDetail.value.likeCount += videoDetail.value.hasLiked ? 1 : -1
ElMessage.success(videoDetail.value.hasLiked ? '点赞成功' : '取消点赞')
}
// 收藏
const handleCollect = () => {
videoDetail.value.hasCollected = !videoDetail.value.hasCollected
videoDetail.value.collectCount += videoDetail.value.hasCollected ? 1 : -1
ElMessage.success(videoDetail.value.hasCollected ? '收藏成功' : '取消收藏')
}
// 分享
const handleShare = () => {
ElMessage.success('分享链接已复制')
}
// 发布评论
const publishComment = async (parentId?: number) => {
const content = parentId ? replyContent.value : commentContent.value
// await addVideoComment({ videoId, content, parentId })
ElMessage.success('评论发布成功')
if (parentId) {
replyContent.value = ''
replyingCommentId.value = null
} else {
commentContent.value = ''
}
// 刷新评论列表
}
// 回复评论
const handleReply = (comment: any, parentId?: number) => {
replyingCommentId.value = parentId || comment.id
replyContent.value = ''
}
// 评论点赞
const handleCommentLike = (comment: any) => {
comment.hasLiked = !comment.hasLiked
comment.likeCount += comment.hasLiked ? 1 : -1
}
// 跳转视频
const handleVideoClick = (id: number) => {
router.push(`/video/${id}`)
}
onMounted(async () => {
const { data } = await getArticleDetail(videoId)
console.log(data)
videoDetail.value = data
})
</script>
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