Commit 4a88ffa9 by lijiabin

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

parent 21438b37
......@@ -3,11 +3,15 @@ import type {
AddOrUpdateArticleDto,
ArticleItemDto,
ArticleSearchParams,
InterviewOptionDto,
ColumnOptionDto,
AddCommentDto,
CommentItemDto,
CommentSearchParams,
InterviewItemDto,
ColumnItemDto,
} from './types'
import type { BackendServicePageResult } from '@/utils/request/types'
import { ArticleTypeEnum } from '@/constants'
import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types'
// 文章相关的接口(帖子 视频 实践等)
......@@ -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>({
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',
data,
})
}
/**
* 获取专访列表--不分页
* 新增评论
*/
export const getInterviewList = () => {
return service.request<InterviewItemDto[]>({
url: '/api/cultureColumn/listNoPage?type=interview',
export const addComment = (data: AddCommentDto) => {
return service.request<boolean>({
url: `/api/cultureComment/addComment`,
method: 'POST',
data,
})
}
/**
* 获取专栏列表-- 不分页
* 点赞评论
*/
export const getColumnList = () => {
return service.request<ColumnItemDto[]>({
url: '/api/cultureColumn/listNoPage?type=column',
export const addOrCancelCommentLike = (commentId: number | string) => {
return service.request<boolean>({
url: `/api/cultureComment/likeComment?commentId=${commentId}`,
method: 'POST',
})
}
......@@ -6,6 +6,7 @@ import type { PageSearchParams } from '@/utils/request/types'
*/
export interface ArticleSearchParams extends PageSearchParams {
type?: ArticleTypeEnum
sortLogic?: number
}
/**
......@@ -41,7 +42,7 @@ interface AddOrUpdateColumnBase {
faceUrl?: string
imgUrl?: string
// 关联的专栏栏目
relateColumn: number
relateColumnId: number
mainTagId: string
isRelateColleague: BooleanFlag
sendTime?: string
......@@ -71,7 +72,7 @@ export interface AddOrUpdateInterviewBase {
faceUrl?: string
imgUrl?: string
// 关联的专访栏目
relateColumn: number
relateColumnId: number
mainTagId: string
sendTime?: string
}
......@@ -95,7 +96,6 @@ export interface AddOrUpdateInterviewDto extends AddOrUpdateInterviewBase {
* 文章详情
*/
// 更严格的类型定义
export interface ArticleItemDto {
id: number
title: string
......@@ -114,10 +114,17 @@ export interface ArticleItemDto {
praiseCount: number
collectionCount: number
replyCount: number
hasPraised: BooleanFlag
hasPraised: boolean
hasCollect: boolean
imgUrl: string
createUserAvatar: string
createUserName: string
}
export interface ColumnItemDto {
/**
* 专栏选项
*/
export interface ColumnOptionDto {
color: string
createTime: number
createUserId: number
......@@ -129,7 +136,34 @@ export interface ColumnItemDto {
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
createTime: number
createUserId: number
......@@ -140,3 +174,65 @@ export interface InterviewItemDto {
title: string
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 type { ColumnItemDto } from './types'
import type { ColumnOptionDto } from './types'
/**
* 获取专栏列表
*/
export const getColumnList = () => {
return service.request<ColumnItemDto[]>({
export const getColumnOptions = () => {
return service.request<ColumnOptionDto[]>({
url: '/api/cultureColumn/listNoPage?type=column',
method: 'POST',
})
......
import { BooleanFlag } from '@/constants'
export interface ColumnItemDto {
export interface ColumnOptionDto {
color: string
createTime: number
createUserId: number
......
......@@ -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) => {
const formData = new FormData()
formData.append('file', file)
return service.request<FielItemDto>({
url: '/mobiles/file-upload/singleUpload',
method: 'POST',
data: formData,
formData.append('fileList', file)
return axios.post('http://47.112.96.71:8082/mobiles/uploadFile', formData, {
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1))
onProgress?.(percentCompleted)
......
// 专栏列表
import service from '@/utils/request/index'
import type { ColumnItemDto } from './types'
import type { ColumnOptionDto } from './types'
/**
* 获取专栏列表
*/
export const getInterviewList = () => {
return service.request<ColumnItemDto[]>({
export const getInterviewOptions = () => {
return service.request<ColumnOptionDto[]>({
url: '/api/cultureColumn/listNoPage?type=interview',
method: 'POST',
})
......
import { BooleanFlag } from '@/constants'
export interface InterviewItemDto {
export interface InterviewOptionDto {
color: string
createTime: number
createUserId: number
......
......@@ -58,7 +58,7 @@ export interface AuditListItemDto {
isRelateColleague: boolean
playCount: number
praiseCount: number
relateColumn: string
relateColumnId: string
releaseStatus: AuditStatusEnum
replyCount: number
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) =>
// 上传文件
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)
......@@ -133,8 +135,8 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) =>
// 更新文件信息
fileList.value[fileIndex] = {
...fileList.value[fileIndex],
url: data.fileUrl,
name: data.fileName,
url,
name,
status: 'success',
}
ElMessage.success('上传成功')
......
......@@ -2,7 +2,7 @@ import type { SetupContext, MaybeRef } from 'vue'
type Events = {
handleBackTop(): void
}
function ScrollTopComp(_: Record<string, never>, { emit }: SetupContext<Events>) {
function ScrollTopComp(_: any, { emit }: SetupContext<Events>) {
return (
<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"
......
......@@ -12,6 +12,7 @@ import type { AddOrUpdateColumnForm, AddOrUpdateColumnDto } from '@/api/article/
export default defineComponent((_, { expose }) => {
const columnStore = useColumnStore()
const { columnList } = storeToRefs(columnStore)
console.log(columnList.value)
const [form, resetForm] = useResetData<AddOrUpdateColumnForm>({
title: '',
content: '',
......@@ -23,7 +24,7 @@ export default defineComponent((_, { expose }) => {
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
isRelateColleague: BooleanFlag.NO,
relateColumn: 0,
relateColumnId: 0,
type: ArticleTypeEnum.COLUMN,
})
const formRef = ref<InstanceType<typeof ElForm>>()
......@@ -34,7 +35,7 @@ export default defineComponent((_, { expose }) => {
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [{ required: true, message: '请选择发布时间', trigger: 'blur' }],
relateColumn: [
relateColumnId: [
{ required: true, message: '请选择专栏栏目', trigger: 'blur', type: 'number', min: 1 },
],
}
......@@ -111,8 +112,8 @@ export default defineComponent((_, { expose }) => {
{/* @ts-ignore */}
<UploadFile v-model={form.value.imgUrl} />
</el-form-item>
<el-form-item label="专栏栏目选择" prop="relateColumn">
<el-radio-group v-model={form.value.relateColumn}>
<el-form-item label="专栏栏目选择" prop="relateColumnId">
<el-radio-group v-model={form.value.relateColumnId}>
{columnList.value.map((item) => (
<el-radio value={item.id}>{item.title}</el-radio>
))}
......
......@@ -22,7 +22,7 @@ export default defineComponent((_, { expose }) => {
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
type: ArticleTypeEnum.INTERVIEW,
relateColumn: 0,
relateColumnId: 0,
})
const formRef = ref<InstanceType<typeof ElForm>>()
const rules = {
......@@ -32,7 +32,7 @@ export default defineComponent((_, { expose }) => {
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [{ required: true, message: '请选择发布时间', trigger: 'blur' }],
relateColumn: [
relateColumnId: [
{ required: true, message: '请选择专访栏目', trigger: 'blur', type: 'number', min: 1 },
],
}
......@@ -109,8 +109,8 @@ export default defineComponent((_, { expose }) => {
{/* @ts-ignore */}
<UploadFile v-model={form.value.imgUrl} />
</el-form-item>
<el-form-item label="专访栏目选择" prop="relateColumn">
<el-radio-group v-model={form.value.relateColumn}>
<el-form-item label="专访栏目选择" prop="relateColumnId">
<el-radio-group v-model={form.value.relateColumnId}>
{columnList.value.map((item) => (
<el-radio value={item.id}>{item.title}</el-radio>
))}
......
......@@ -27,7 +27,7 @@ export default defineComponent(
return {
...form.value,
releaseStatus,
imgUrl: form.value.imgUrl?.split(',').filter(Boolean)[0] || '',
faceUrl: form.value.imgUrl?.split(',').filter(Boolean)[0] || '',
}
}
......
......@@ -34,7 +34,7 @@ const routes = [
],
},
{
path: 'videoDetail/:',
path: 'videoDetail/:id',
name: 'CultureVideoDetail',
component: () => import('@/views/videoDetail/index.vue'),
},
......
import { defineStore } from 'pinia'
import { getColumnList } from '@/api'
import type { ColumnItemDto } from '@/api/article/types'
import { getColumnOptions } from '@/api'
import type { ColumnOptionDto } from '@/api/article/types'
/**
* 关于专栏的不分页数据
*/
export const useColumnStore = defineStore('column', () => {
const columnList = ref<ColumnItemDto[]>([])
const columnList = ref<ColumnOptionDto[]>([])
let isLoading = false
......@@ -14,7 +14,7 @@ export const useColumnStore = defineStore('column', () => {
if (isLoading) return
isLoading = true
try {
const { data } = await getColumnList()
const { data } = await getColumnOptions()
columnList.value = data
console.log(columnList.value, 'columnList')
} catch (error) {
......
import { defineStore } from 'pinia'
import { getInterviewList } from '@/api'
import type { InterviewItemDto } from '@/api/article/types'
import { getInterviewOptions } from '@/api'
import type { InterviewOptionDto } from '@/api/article/types'
/**
* 关于专访的相关数据 --不分页
*/
export const useInterviewStore = defineStore('interview', () => {
const interviewList = ref<InterviewItemDto[]>([])
const interviewList = ref<InterviewOptionDto[]>([])
let isLoading = false
......@@ -14,7 +14,7 @@ export const useInterviewStore = defineStore('interview', () => {
if (interviewList.value.length > 0) return
isLoading = true
try {
const { data } = await getInterviewList()
const { data } = await getInterviewOptions()
interviewList.value = data
console.log(interviewList.value, 'interviewList')
} catch (error) {
......
<template>
<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
v-for="item in list"
......@@ -10,7 +10,7 @@
>
<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
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 @@
<!-- 内容摘要 -->
<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 }}
</p>
</div>
......@@ -55,7 +55,7 @@
</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
:src="item.faceUrl"
alt="文章配图"
......@@ -116,16 +116,16 @@
<script setup lang="ts" name="RecommendList">
import { usePageSearch } from '@/hooks'
import { getArticleList } from '@/api'
import { ArticleTypeEnum, TABS_REF_KEY } from '@/constants'
import { TABS_REF_KEY } from '@/constants'
import { useScrollTop } from '@/hooks'
import dayjs from 'dayjs'
const router = useRouter()
const { list, total, searchParams, loading, goToPage, changePageSize, reset } = usePageSearch(
const { list, total, searchParams, loading, goToPage, changePageSize, refresh } = usePageSearch(
getArticleList,
{
// defaultParams: { type: ArticleTypeEnum.POST },
defaultParams: { sortLogic: 0 },
defaultCurrent: 1,
defaultSize: 5,
},
......@@ -135,6 +135,10 @@ const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
defineExpose({
refresh: reset,
refresh: (sortLogic: number) => {
console.log('sortLogic', sortLogic)
searchParams.value.sortLogic = sortLogic
refresh()
},
})
</script>
<template>
<div class="min-h-screen bg-gray-50/30">
<div class="bg-gray-50/30">
<!-- tabs -->
<div class="shadow-sm">
<div class="max-w-7xl mx-auto px-4">
......@@ -24,7 +24,7 @@
</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">
<!-- 第一个视频 - 占据两列 -->
......@@ -34,7 +34,6 @@
<div class="relative overflow-hidden">
<img
src="https://picsum.photos/800/400?random=1"
alt="视频封面"
class="w-full h-72 object-cover group-hover:scale-105 transition-transform duration-700"
/>
<div
......@@ -113,7 +112,6 @@
<div class="relative overflow-hidden">
<img
: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"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
......@@ -168,14 +166,13 @@
<!-- 剩余视频 - 标准网格 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="item in 12"
: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"
>
<div class="relative overflow-hidden">
<img
:src="`https://picsum.photos/300/200?random=${item + 10}`"
alt="视频封面"
:src="item.faceUrl"
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>
......@@ -184,16 +181,14 @@
<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"
>
{{ item % 2 === 0 ? '🔬 科技' : '🎨 设计' }}
{{ item.tagNameList[0] }}
</div>
<!-- 时长 -->
<div
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') }}:{{
String(Math.floor(Math.random() * 60)).padStart(2, '0')
}}
15:18
</div>
<!-- 数据 -->
......@@ -202,13 +197,13 @@
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<SvgIcon name="icon_play" size="12" />
<span>{{ (Math.random() * 10).toFixed(1) }}</span>
<span>{{ item.viewCount }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<SvgIcon name="icon_comment" size="12" />
<span>{{ Math.floor(Math.random() * 1000) }}</span>
<span>{{ item.replyCount }}</span>
</div>
</div>
......@@ -228,23 +223,23 @@
<h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-2"
>
{{ ['设计师必看的创意灵感来源', '前端开发实战教程', 'UI设计从入门到精通'][item % 3] }}
{{ item.title }}
</h3>
<p class="text-gray-600 text-sm mb-3 line-clamp-2 leading-relaxed">
这是一个非常有趣的视频教程,包含了丰富的内容和实用的技巧,适合初学者和进阶用户学习...
{{ item.content }}
</p>
<div class="flex items-center justify-between text-gray-500 text-xs">
<div class="flex items-center gap-2">
<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"
>
{{ String(item).slice(-1) }}
{{ item.createUserName }}
</div>
<span class="font-medium">UP主{{ item }}</span>
<span class="font-medium">UP主{{ item.createUserName }}</span>
</div>
<span class="bg-gray-100 px-2 py-1 rounded-full"
>{{ Math.floor(Math.random() * 30) + 1 }}天前</span
>
<span class="bg-gray-100 px-2 py-1 rounded-full">{{
dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm')
}}</span>
</div>
</div>
</div>
......@@ -255,14 +250,14 @@
<template v-else>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="item in 12"
:key="item"
@click="router.push(`/videoDetail/${item.id}`)"
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"
>
<div class="relative overflow-hidden">
<img
:src="`https://picsum.photos/300/200?random=${item + 10}`"
alt="视频封面"
:src="item.faceUrl"
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>
......@@ -271,16 +266,14 @@
<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"
>
{{ item % 2 === 0 ? '🔬 科技' : '🎨 设计' }}
{{ item.tagNameList[0] }}
</div>
<!-- 时长 -->
<div
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') }}:{{
String(Math.floor(Math.random() * 60)).padStart(2, '0')
}}
15:18
</div>
<!-- 数据 -->
......@@ -288,25 +281,30 @@
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<SvgIcon name="icon_play" size="12" />
<span>{{ (Math.random() * 10).toFixed(1) }}</span>
<el-icon class="text-sm"><View /></el-icon>
<span>{{ item.viewCount }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<SvgIcon name="icon_comment" size="12" />
<span>{{ Math.floor(Math.random() * 1000) }}</span>
<el-icon class="text-sm"><ChatDotRound /></el-icon>
<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
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
>
<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>
......@@ -315,23 +313,21 @@
<h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-2"
>
{{ ['设计师必看的创意灵感来源', '前端开发实战教程', 'UI设计从入门到精通'][item % 3] }}
{{ item.title }}
</h3>
<p class="text-gray-600 text-sm mb-3 line-clamp-2 leading-relaxed">
这是一个非常有趣的视频教程,包含了丰富的内容和实用的技巧,适合初学者和进阶用户学习...
{{ item.content }}
</p>
<div class="flex items-center justify-between text-gray-500 text-xs">
<div class="flex items-center gap-2">
<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"
>
{{ String(item).slice(-1) }}
{{ item.createUserName }}
</div>
<span class="font-medium">UP主{{ item }}</span>
<span class="font-medium">UP主{{ item.createUserName }}</span>
</div>
<span class="bg-gray-100 px-2 py-1 rounded-full"
>{{ Math.floor(Math.random() * 30) + 1 }}天前</span
>
<span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm') }}</span>
</div>
</div>
</div>
......@@ -353,11 +349,11 @@
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]"
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:page-sizes="[12, 24, 36, 48]"
layout="prev, pager, next, jumper, total"
:total="400"
:total="total"
class="custom-pagination"
/>
</div>
......@@ -371,14 +367,22 @@
<script setup lang="ts" name="RecommendList">
import { useRouter } from 'vue-router'
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 { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
const currentPage = ref(1)
const pageSize = ref(15)
const { list, total, searchParams, loading, goToPage, changePageSize, refresh } = usePageSearch(
getArticleList,
{
defaultParams: { type: ArticleTypeEnum.VIDEO },
defaultCurrent: 1,
defaultSize: 12,
},
)
// Tabs 配置
const router = useRouter()
const tabs = [
......@@ -392,6 +396,12 @@ const activeTab = ref('latest')
const goVideoDetail = (n: number) => {
router.push(`/videoDetail?id=${n}`)
}
defineExpose({
refresh: (sortLogic: number) => {
searchParams.value.sortLogic = sortLogic
refresh()
},
})
</script>
<style>
......
......@@ -44,12 +44,20 @@ const activeTabComponentRef =
useTemplateRef<InstanceType<typeof RecommendList>>('activeTabComponentRef')
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) => {
if (tab === '最新') {
activeTabComponentRef.value?.refresh?.()
activeTabComponentRef.value?.refresh?.(1)
} else if (tab === '推荐') {
activeTabComponentRef.value?.refresh?.(0)
}
}
</script>
......
......@@ -10,22 +10,25 @@
<div class="flex gap-3">
<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
v-for="tab in tabs"
: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)"
>
<div
class="flex items-center gap-2 px-12 py-2.5 rounded-xl transition-all duration-300"
:class="{
'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
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="{ 'scale-x-50': activeTab === tab.name }"
></div>
</div>
</div>
</div>
......@@ -162,13 +165,16 @@
<!-- 任务中心 -->
<div class="task-container common-box rounded-lg bg-#FFF0E5">
<div class="flex items-center justify-between mb-2">
<div>
<div class="flex items-start justify-start mb-2">
<div class="w-full">
<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>
<h1 class="text-sm sm:text-base font-bold">任务中心</h1>
</div>
<h2 class="text-xs sm:text-sm mb-1 text-gray-600 ml-3">
<h2
class="w-full text-xs sm:text-sm mt-1 text-gray-600 flex items-center justify-between"
>
<div>
<span
v-for="item in taskTypeList"
:key="item.value"
......@@ -177,6 +183,10 @@
@click="currentTask = item.value"
>{{ item.label }}
</span>
</div>
<span class="text-#999 cursor-pointer" @click="router.push(`/userPage?key=task`)">
查看更多
</span>
</h2>
</div>
</div>
......@@ -210,13 +220,23 @@
</div>
</div>
</div>
<el-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"
type="primary"
<button
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"
: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)"
>
<span class="text-black text-xs sm:text-sm">去完成</span>
</el-button>
<span
class="text-black text-sm"
:style="{
color: item.currentCount === item.limitCount ? '#999' : '#000',
}"
>{{ item.currentCount === item.limitCount ? '已完成' : '去完成' }}</span
>
</button>
</div>
<!-- 分割线 -->
<el-divider style="margin: 0" />
......@@ -332,6 +352,8 @@ const onDailySign = async () => {
}
const handleTask = (item: TaskItemDto) => {
if (item.currentCount === item.limitCount) return
// if (item.svgName === 'svgName') {
handleBackTop()
triggerAnimation()
......
<template>
<div>
<div v-loading="loading" v-if="list.length">
<div class="w-full max-w-6xl mx-auto">
<div
v-for="item in columnList"
:key="item.id"
v-for="(item, index) in list"
:key="index"
class="bg-white rounded-lg shadow-sm mb-6 overflow-hidden"
:style="{ '--dynamic-color': item.color }"
>
......@@ -14,103 +16,119 @@
<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
>
<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="grid grid-cols-1 md:grid-cols-3 gap-4">
<div v-for="item in section2Items" :key="item.id" class="group cursor-pointer">
<div v-if="item.yaColumnVoList.length" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div
v-for="i in item.yaColumnVoList"
:key="i.articleId"
class="group cursor-pointer"
@click="router.push(`/postDetail/${i.articleId}`)"
>
<div class="relative mb-3 overflow-hidden rounded-lg">
<img
src="@/assets/img/culture/ask.png"
:alt="item.title"
class="w-full h-32 object-cover group-hover:scale-105 transition-transform duration-300"
:src="i.faceUrl"
class="w-full aspect-[5/3] object-cover group-hover:scale-105 transition-transform duration-300"
/>
<div class="absolute top-2 left-2">
<el-tag size="small" class="bg-orange-500 text-white border-none">
{{ item.tag }}
1111111
</el-tag>
</div>
</div>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors">
{{ item.title }}
{{ i.title }}
</h3>
<p class="text-xs text-gray-500 mb-3 line-clamp-2">
{{ item.description }}
{{ 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>
{{ item.views }}
{{ i.viewCount }}
</span>
<span class="flex items-center">
<el-icon class="mr-1"><ChatDotRound /></el-icon>
{{ item.comments }}
{{ i.replyCount }}
</span>
<span class="flex items-center">
<el-icon class="mr-1"><Star /></el-icon>
{{ item.likes }}
{{ i.collectCount }}
</span>
</div>
<span>{{ item.date }}</span>
<span>{{ dayjs(i.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div>
</div>
</div>
<div v-else class="flex items-center justify-center h-48">
<el-empty description="暂无数据" />
</div>
</div>
</div>
</div>
<div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
<div class="max-w-7xl mx-auto 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="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>
</div>
</div>
</div>
</div>
<template v-else>
<div class="flex items-center justify-center h-full">
<el-empty description="暂无数据" />
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ArrowRight, View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { useColumnStore } from '@/stores/column'
import { storeToRefs } from 'pinia'
import { getColumnList } from '@/api'
import { usePageSearch, useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
import dayjs from 'dayjs'
import { useRouter } from 'vue-router'
const router = useRouter()
const tabsRef = inject(TABS_REF_KEY)
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const columnStore = useColumnStore()
const { columnList } = storeToRefs(columnStore)
const section2Items = ref([
{
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',
},
const { list, total, searchParams, goToPage, changePageSize, loading } = usePageSearch(
getColumnList,
{
id: 6,
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',
defaultSize: 3,
},
])
)
</script>
<style scoped></style>
<template>
<div>
<div v-loading="loading" v-if="list.length">
<div class="w-full max-w-6xl mx-auto">
<div
v-for="item in interviewList"
:key="item.id"
v-for="(item, index) in list"
:key="index"
class="bg-white rounded-lg shadow-sm mb-6 overflow-hidden"
:style="{ '--dynamic-color': item.color }"
>
......@@ -23,94 +25,112 @@
</div>
<div class="p-4">
<div 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 v-if="item.yaColumnVoList.length" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div
v-for="i in item.yaColumnVoList"
:key="i.articleId"
class="group cursor-pointer"
@click="router.push(`/postDetail/${i.articleId}`)"
>
<div class="relative mb-3 overflow-hidden rounded-lg">
<img
src="@/assets/img/culture/ask.png"
:alt="item.title"
:src="i.faceUrl"
class="w-full h-32 object-cover group-hover:scale-105 transition-transform duration-300"
/>
<div class="absolute top-2 left-2">
<el-tag size="small" class="bg-orange-500 text-white border-none">
{{ item.tag }}
1111111
</el-tag>
</div>
</div>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors">
{{ item.title }}
{{ i.title }}
</h3>
<p class="text-xs text-gray-500 mb-3 line-clamp-2">
{{ item.description }}
{{ 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>
{{ item.views }}
{{ i.viewCount }}
</span>
<span class="flex items-center">
<el-icon class="mr-1"><ChatDotRound /></el-icon>
{{ item.comments }}
{{ i.replyCount }}
</span>
<span class="flex items-center">
<el-icon class="mr-1"><Star /></el-icon>
{{ item.likes }}
{{ i.collectCount }}
</span>
</div>
<span>{{ item.date }}</span>
<span>{{ dayjs(i.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div>
</div>
</div>
<div v-else class="flex items-center justify-center h-48">
<el-empty description="暂无数据" />
</div>
</div>
</div>
</div>
<div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
<div class="max-w-7xl mx-auto 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="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>
</div>
</div>
</div>
</div>
<template v-else>
<div class="flex items-center justify-center h-full">
<el-empty description="暂无数据" />
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ArrowRight, View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { useInterviewStore } from '@/stores/interview'
import { storeToRefs } from 'pinia'
import { getInterviewList } from '@/api'
import { usePageSearch, useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
import { useRouter } from 'vue-router'
import dayjs from 'dayjs'
const router = useRouter()
const tabsRef = inject(TABS_REF_KEY)
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const interviewStore = useInterviewStore()
const { interviewList } = storeToRefs(interviewStore)
const section2Items = ref([
{
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',
},
const { list, total, searchParams, goToPage, changePageSize, loading } = usePageSearch(
getInterviewList,
{
id: 6,
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',
defaultSize: 3,
},
])
)
</script>
<style scoped></style>
<template>
<div class="min-h-screen">
<div>
<!-- 头部Tabs -->
<div class="header h-40px items-center justify-between">
<div class="left flex gap-3 flex items-center">
......@@ -11,35 +11,9 @@
</div>
</div>
<el-divider style="margin: 10px 0 20px 0" />
<ColumnList v-if="activeTab === '专栏'" />
<InterviewList v-if="activeTab === '专访'" />
<PracticeList v-if="activeTab === '实践'" />
<!-- 底部分页 -->
<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>
<keep-alive>
<component :is="curretComponent" />
</keep-alive>
</div>
</template>
......@@ -50,14 +24,14 @@ import { Refresh } from '@element-plus/icons-vue'
import ColumnList from './components/columnList.vue'
import InterviewList from './components/interviewList.vue'
import PracticeList from './components/practiceList.vue'
import { useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp } = useScrollTop(tabsRef!)
const currentPage = ref(1)
const pageSize = ref(10)
const curretComponent = computed(() => {
return {
专栏: ColumnList,
专访: InterviewList,
实践: PracticeList,
}[activeTab.value]
})
const activeTab = ref('专栏')
const tabs = [
......@@ -66,12 +40,5 @@ const tabs = [
{ label: '专访', value: '专访' },
{ label: '视频', value: '视频' },
]
const handleBackTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
</script>
<style lang="scss" scoped></style>
......@@ -12,7 +12,7 @@
>
<div
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
class="group-hover:text-blue-500! cursor-pointer"
......@@ -39,11 +39,10 @@ import { Star, ChatLineSquare, Pointer } from '@element-plus/icons-vue'
import type { ArticleItemDto } from '@/api'
import type { Component } from 'vue'
import { useScrollTop } from '@/hooks'
import { addOrCancelCollect } from '@/api'
import { addOrCanceArticlelCollect, addOrCanceArticlelLike } from '@/api'
import { COMMENT_REF_KEY } from '@/constants'
const { articleDetail } = defineProps<{
articleDetail: ArticleItemDto
}>()
const modelValue = defineModel<ArticleItemDto>('modelValue', { required: true })
const commentRef = inject(COMMENT_REF_KEY)
......@@ -55,38 +54,52 @@ interface StatItem {
count: number
label: string
active?: boolean
actionFn?: () => Promise<boolean>
actionFn?: () => Promise<void>
}
const stats = computed(() => {
return [
{
icon: Pointer,
count: articleDetail?.praiseCount,
count: modelValue.value?.praiseCount ?? 0,
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,
count: articleDetail?.collectionCount,
count: modelValue.value?.collectionCount ?? 0,
label: '收藏',
active: articleDetail?.isRecommend,
api: addOrCancelCollect,
async actionFn(item: StatItem) {
const res = await addOrCancelCollect({
articleId: articleDetail.id,
type: articleDetail.type,
})
if (res) {
item.active = !item.active
}
active: modelValue.value?.hasCollect,
api: addOrCanceArticlelCollect,
async actionFn() {
await addOrCanceArticlelCollect(modelValue.value.id)
// 前端直接本地修改
const isAdd = !modelValue.value.hasCollect
modelValue.value.hasCollect = isAdd
modelValue.value.collectionCount = isAdd
? modelValue.value.collectionCount + 1
: modelValue.value.collectionCount - 1
ElMessage.success(isAdd ? '收藏成功' : '取消收藏成功')
},
},
{
icon: ChatLineSquare,
count: articleDetail?.replyCount,
count: modelValue.value?.replyCount ?? 0,
label: '评论',
active: articleDetail?.isRecommend,
// active: modelValue.value?.replyCount > 0,
actionFn: handleBackTop,
},
]
......
<template>
<div class="min-h-screen px-20">
<!-- 主内容区 -->
<ActionButtons :articleDetail="articleDetail"></ActionButtons>
<ActionButtons v-model="articleDetail"></ActionButtons>
<div class="lg:col-span-3">
<!-- 帖子主体 -->
<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="flex items-center gap-4">
<div class="relative">
<img
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face"
:src="articleDetail?.createUserAvatar"
alt=""
class="w-12 h-12 rounded-full object-cover"
/>
......@@ -24,7 +24,7 @@
</div>
<div class="flex-1">
<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
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 @@
· {{ articleDetail?.viewCount || 0 }} 阅读
</p>
</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"
>
+ 关注
</button>
</button> -->
</div>
</div>
......@@ -50,57 +50,41 @@
{{ articleDetail?.title }}
</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="text-gray-700 leading-relaxed space-y-4">
{{ articleDetail?.description }}
{{ articleDetail?.content }}
</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
src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=500&h=300&fit=crop"
alt=""
class="rounded-xl object-cover w-full h-64 hover:scale-105 transition-transform cursor-pointer"
/>
<img
src="https://images.unsplash.com/photo-1627398242454-45a1465c2479?w=500&h=300&fit=crop"
v-for="item in articleDetail.imgUrl.split(',')"
:key="item"
:src="item"
alt=""
class="rounded-xl object-cover w-full h-64 hover:scale-105 transition-transform cursor-pointer"
/>
</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
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">
......@@ -156,7 +140,7 @@
<button
class="cursor-pointer disabled:opacity-50 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled="!comment.trim()"
@click="handleComment"
@click="handleComment()"
>
发表
</button>
......@@ -166,81 +150,53 @@
</div>
<!-- 评论列表 -->
<div class="divide-y divide-gray-100">
<!-- 评论1 -->
<div class="divide-y divide-gray-100" v-if="list.length">
<div v-for="item in list" :key="item.id">
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<img :src="item.avatar" alt="" class="w-10 h-10 rounded-full object-cover" />
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">前端大佬</span>
<span class="font-semibold text-gray-800">{{ item.replyUser }}</span>
<span
class="px-2 py-0.5 text-xs bg-gradient-to-r from-purple-100 to-blue-100 text-purple-600 rounded-full"
>
技术专家
</span>
<span class="px-2 py-0.5 text-xs bg-red-100 text-red-600 rounded-full">置顶</span>
</div>
<p class="text-gray-700 mb-3">
写得很好!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)"
<span class="px-2 py-0.5 text-xs bg-red-100 text-red-600 rounded-full"
>置顶</span
>
回复
</button>
</div>
</div>
<!-- 回复列表 -->
<div class="mt-3 ml-4 space-y-3">
<div class="flex gap-2 p-3 bg-gray-50 rounded-lg">
<img
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-8 h-8 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-sm text-gray-800">前端小李</span>
<span class="text-xs text-gray-500">30分钟前</span>
</div>
<p class="text-sm text-gray-700">
谢谢认可!确实这个组合在开发效率上提升很大。
<!-- 换行 -->
<p class="text-gray-700 mb-3 break-all">
{{ item.content }}
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>1小时前</span>
<button
class="flex items-center gap-1 hover:text-red-500 transition-colors"
<span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
<!-- <button class="flex items-center gap-1 hover:text-red-500 transition-colors"> -->
<div
class="flex items-center gap-1 cursor-pointer"
@click="handleLickComment(item)"
>
<i class="i-carbon-favorite"></i>
<span>24</span>
</button>
<el-icon
:size="16"
:style="{ color: item.hasPraise ? '#409eff' : '#606266' }"
>
<Pointer />
</el-icon>
<span>{{ item.postPriseCount }}</span>
</div>
<!-- </button> -->
<button
@click="handleReply(1)"
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
:src="userInfo?.avatar"
alt=""
......@@ -265,74 +221,52 @@
<button
class="cursor-pointer disabled:opacity-50 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled="!comment.trim()"
@click="handleComment"
@click="handleComment(item.id)"
>
发表
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<el-divider />
<!-- 评论2 -->
<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 class="mt-3 ml-4 space-y-3">
<div class="flex gap-2 p-3 bg-gray-50 rounded-lg">
<img
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-8 h-8 rounded-full object-cover"
/>
<div v-if="item.children.length" class="mt-3 ml-4 space-y-3">
<div
v-for="child in item.children"
:key="child.id"
class="flex gap-2 p-3 bg-gray-50 rounded-lg"
>
<img :src="child.avatar" alt="" class="w-8 h-8 rounded-full object-cover" />
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-sm text-gray-800">前端小李</span>
<span class="text-xs text-gray-500">30分钟前</span>
<span class="font-medium text-sm text-gray-800">{{
child.replyUser
}}</span>
<span class="text-xs text-gray-500">{{
dayjs(child.createTime * 1000).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</div>
<p class="text-sm text-gray-700">
谢谢认可!确实这个组合在开发效率上提升很大。
{{ child.content }}
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>1小时前</span>
<button
class="flex items-center gap-1 hover:text-red-500 transition-colors"
<span>{{
dayjs(child.createTime * 1000).format('YYYY-MM-DD HH:mm:ss')
}}</span>
<div
class="flex items-center gap-1 cursor-pointer"
@click="handleLickComment(child)"
>
<i class="i-carbon-favorite"></i>
<span>24</span>
</button>
<el-icon
:size="16"
:style="{ color: child.hasPraise ? '#409eff' : '#606266' }"
>
<Pointer />
</el-icon>
<span>{{ child.postPriseCount }}</span>
</div>
<button
@click="handleReply(1)"
@click="handleReply(child)"
class="cursor-pointer hover:text-blue-500 transition-colors"
>
回复
......@@ -341,7 +275,8 @@
</div>
</div>
</div>
<div v-show="currentId === 1" class="flex gap-3">
</div>
<div v-show="showCommentBox(item)" class="flex gap-3">
<img
:src="userInfo?.avatar"
alt=""
......@@ -366,7 +301,7 @@
<button
class="cursor-pointer disabled:opacity-50 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled="!comment.trim()"
@click="handleComment"
@click="handleComment(item.id)"
>
发表
</button>
......@@ -376,214 +311,20 @@
</div>
</div>
</div>
<el-divider />
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
<button
class="cursor-pointer hover:text-blue-500 transition-colors"
@click="handleReply(2)"
>
回复
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">Vue开发者</span>
<span class="px-2 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"
>热门</span
>
</div>
<p class="text-gray-700 mb-3">
能分享一下具体的项目配置吗?我在集成 UnoCSS 的时候遇到了一些问题。
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>2小时前</span>
<button class="flex items-center gap-1 hover:text-red-500 transition-colors">
<i class="i-carbon-favorite"></i>
<span>18</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 hover:bg-gray-50/50 transition-colors">
<div class="flex gap-3">
<img
src="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
alt=""
class="w-10 h-10 rounded-full object-cover"
<!-- 底部分页 -->
<!-- 靠右侧 -->
<div class="flex justify-end">
<div class="w-fit">
<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 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>
......@@ -594,11 +335,21 @@
<script lang="ts" setup>
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 { useUserStore } from '@/stores'
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 commentRef = useTemplateRef<HTMLElement | null>('commentRef')
const { userInfo } = storeToRefs(userStore)
......@@ -611,22 +362,100 @@ const comment = ref('')
const currentId = ref(-1)
const fetchArticleDetail = async () => {
const { data } = await getArticleDetail(id)
articleDetail.value = data
const { list, searchParams, goToPage, changePageSize, total, reset } = usePageSearch(
getCommentList,
{
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 = ''
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)
const res = await addComment({
articleId: id,
content: comment.value,
...(pid ? { pid } : {}),
})
console.log(res)
ElMessage.success('发表评论成功')
reset()
comment.value = ''
}
provide(COMMENT_REF_KEY, commentRef)
onMounted(async () => {
fetchArticleDetail()
initPage()
})
</script>
......@@ -85,10 +85,10 @@
</el-form-item>
</div>
<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>
<el-input
v-model="form.description"
v-model="form.content"
type="textarea"
:rows="5"
placeholder="请写上您希望介绍的内容,让更多的人了解您的作品吧!"
......@@ -201,7 +201,7 @@
<script setup lang="ts">
import UploadVideo from '@/components/common/UploadVideo/index.vue'
import { useResetData } from '@/hooks'
import { ArticleTypeEnum, SendTypeEnum } from '@/constants'
import { ArticleTypeEnum, ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants'
import { addOrUpdateArticle } from '@/api'
import SelectTags from '@/components/common/SelectTags/index.vue'
......@@ -213,11 +213,12 @@ const [form] = useResetData({
videoUrl: '',
title: '视频标题',
type: ArticleTypeEnum.VIDEO,
description: '',
content: '',
mainTagId: '',
tagList: [],
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
})
const filterTagsFn = (tags: TagItemDto[]) => {
......@@ -227,7 +228,7 @@ const filterTagsFn = (tags: TagItemDto[]) => {
const rules = {
videoUrl: [{ required: true, message: '请上传视频', trigger: 'change' }],
title: [{ required: true, message: '请输入视频标题', trigger: 'blur' }],
description: [{ required: true, message: '请输入视频简介', trigger: 'blur' }],
content: [{ required: true, message: '请输入视频简介', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择标签', trigger: 'change' }],
}
......
......@@ -167,9 +167,12 @@ import { hasOfficialAccount } from '@/api'
const editUserInfoRef = useTemplateRef<InstanceType<typeof EditUserInfo>>('editUserInfoRef')
const userStore = useUserStore()
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')
......
<template>
<div class="min-h-screen bg-gray-50">
<!-- 主容器 -->
<div class="max-w-6xl mx-auto px-4 py-6">
<div class="min-h-screen px-20">
<!-- 左侧主内容区 -->
<div class="lg:col-span-3">
<!-- 视频播放器 -->
<div class="relative w-full aspect-video bg-black rounded-lg overflow-hidden mb-4">
<div class="absolute inset-0">
<!-- 模拟视频内容 -->
<div class="w-full h-full bg-gradient-to-br from-gray-800 to-black relative">
<!-- 视频标题叠加 -->
<div class="absolute inset-0 flex items-center justify-center">
<div class="text-center">
<h1 class="text-4xl md:text-6xl font-bold text-yellow-400 mb-4 drop-shadow-lg">
《黑神话:钟馗》
</h1>
<p class="text-xl text-white drop-shadow">里的钟馗,有着怎样的身世?</p>
<div class="bg-black rounded-2xl overflow-hidden shadow-lg">
<video
ref="videoRef"
:src="videoDetail?.videoUrl"
class="w-full aspect-video"
controls
@play="handlePlay"
@pause="handlePause"
></video>
</div>
<!-- 视频信息卡片 -->
<div class="mt-4 bg-white backdrop-blur-sm rounded-2xl shadow-sm border border-white/50 p-6">
<!-- 标题和标签 -->
<h1 class="text-2xl font-bold text-gray-900 mb-4">
{{ videoDetail?.title }}
</h1>
<div class="flex flex-wrap gap-2 mb-6">
<span
v-for="tag in videoDetail?.tags"
:key="tag"
class="px-3 py-1 text-sm bg-gradient-to-r from-cyan-100 to-blue-100 text-cyan-600 rounded-full hover:shadow-md transition-all cursor-pointer"
>
#{{ tag }}
</span>
</div>
<!-- 播放控制栏 -->
<!-- 视频数据统计 -->
<div
class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent p-4"
class="flex items-center gap-6 text-sm text-gray-600 mb-6 pb-6 border-b border-gray-100"
>
<div class="flex items-center justify-between text-white">
<div class="flex items-center space-x-4">
<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>
<span class="text-sm">00:00 / 10:35</span>
</div>
<div class="flex items-center space-x-3">
<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>
<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 class="flex items-center gap-2">
<i class="i-carbon-view text-lg"></i>
<span>{{ formatNumber(videoDetail?.viewCount) }} 播放</span>
</div>
<div class="flex items-center gap-2">
<i class="i-carbon-thumbs-up text-lg"></i>
<span>{{ formatNumber(videoDetail?.likeCount) }}</span>
</div>
<div class="flex items-center gap-2">
<i class="i-carbon-star text-lg"></i>
<span>{{ formatNumber(videoDetail?.collectCount) }} 收藏</span>
</div>
<div class="flex items-center gap-2">
<i class="i-carbon-share text-lg"></i>
<span>{{ formatNumber(videoDetail?.shareCount) }} 分享</span>
</div>
<span class="text-gray-400">{{
dayjs(videoDetail?.publishTime * 1000).format('YYYY-MM-DD HH:mm')
}}</span>
</div>
<!-- 视频信息区域 -->
<div class="bg-white rounded-lg p-6 mb-4 shadow-sm">
<!-- 标题和基本信息 -->
<div class="mb-4">
<h1 class="text-xl md:text-2xl font-bold text-gray-900 mb-3 leading-tight">
《黑神话:钟馗》里的钟馗,有着怎样的身世?
</h1>
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-600 mb-4">
<div class="flex items-center gap-1">
<span>▶️</span>
<span>63.2万</span>
</div>
<div class="flex items-center gap-1">
<span>💬</span>
<span>589</span>
</div>
<div class="text-gray-500">2025-08-20 12:24:18</div>
<div class="flex items-center gap-1 text-blue-500">
<span>#文化标签</span>
</div>
</div>
<!-- UP主信息和操作按钮 -->
<div class="flex items-center justify-between">
<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>
<!-- 互动按钮区域 -->
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
<div class="flex flex-wrap items-center gap-3">
<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 class="font-medium">1.1万</span>
<span v-if="!videoDetail?.hasFollowed">+ 关注</span>
<span v-else>已关注</span>
</button>
</div>
<div class="flex items-center gap-3">
<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">
<span class="text-white text-sm">💰</span>
</div>
<span class="font-medium text-blue-500">1121</span>
<i class="i-carbon-thumbs-up text-lg"></i>
<span>{{ videoDetail?.hasLiked ? '已赞' : '点赞' }}</span>
</button>
<button
class="flex items-center gap-2 px-4 py-2 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>
<span class="font-medium">4341</span>
<i class="i-carbon-star text-lg"></i>
<span>{{ videoDetail?.hasCollected ? '已收藏' : '收藏' }}</span>
</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>
<span class="font-medium">488</span>
<i class="i-carbon-share text-lg"></i>
<span>分享</span>
</button>
</div>
</div>
<!-- UP主信息 -->
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg mb-4">
<div class="flex items-center gap-3">
<div
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 v-if="videoDetail?.description" class="mt-6 pt-6 border-t border-gray-100">
<div class="text-gray-700 leading-relaxed whitespace-pre-wrap">
{{ videoDetail?.description }}
</div>
<div>
<h3 class="font-medium text-gray-900">UP主名称</h3>
<p class="text-sm text-gray-600">粉丝 12.3万 · 获赞 45.6万</p>
</div>
</div>
<!-- 评论区 -->
<div
ref="commentRef"
class="mt-6 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="flex items-center justify-between">
<span class="text-xl font-bold text-gray-800">
评论 {{ formatNumber(videoDetail?.commentCount) }}
</span>
<div class="flex items-center gap-2">
<button
class="px-6 py-2 bg-pink-500 hover:bg-pink-600 text-white rounded-full 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>
</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">
<span>评论</span>
<span class="text-blue-500">1171</span>
</h3>
<div class="flex gap-1">
<button class="px-3 py-1 text-sm bg-pink-100 text-pink-600 rounded transition-colors">
最热
</button>
<button
class="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded transition-colors"
>
最新
</div>
</div>
<!-- 发表评论 -->
<div class="p-6 border-b border-gray-100">
<div class="flex gap-4">
<img :src="userInfo?.avatar" alt="" class="w-10 h-10 rounded-full object-cover" />
<div class="flex-1">
<el-input
v-model="commentContent"
type="textarea"
placeholder="发一条友善的评论"
:rows="3"
:maxlength="500"
show-word-limit
></el-input>
<div class="flex justify-between items-center mt-3">
<div class="flex items-center gap-3">
<button class="text-gray-500 hover:text-blue-500 transition-colors">
<i class="i-carbon-face-satisfied text-xl"></i>
</button>
<button
class="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded transition-colors"
>
精选
<button class="text-gray-500 hover:text-blue-500 transition-colors">
<i class="i-carbon-image text-xl"></i>
</button>
</div>
<button
class="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded transition-colors"
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 class="flex gap-3 mb-6">
<div
class="w-10 h-10 bg-teal-400 rounded-full flex items-center justify-center flex-shrink-0"
>
<span class="text-white">👤</span>
</div>
<div class="flex-1">
<input
type="text"
placeholder="这里是评论区,不是无人区:)"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500 transition-colors"
/>
</div>
</div>
<!-- 评论列表 -->
<div class="space-y-6">
<!-- 评论项 1 -->
<div class="flex gap-3">
<div class="divide-y divide-gray-100">
<div
class="w-10 h-10 bg-red-400 rounded-full flex items-center justify-center flex-shrink-0"
v-for="comment in commentList"
:key="comment.id"
class="p-6 hover:bg-gray-50/50 transition-colors"
>
<span class="text-white">👤</span>
</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 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 class="hover:text-blue-500 transition-colors">👎</button>
<button class="hover:text-blue-500 transition-colors">回复</button>
</div>
</div>
<span class="font-semibold text-gray-800">{{ comment.username }}</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>
<!-- 评论项 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"
<p class="text-gray-700 mb-3 leading-relaxed">{{ comment.content }}</p>
<div class="flex items-center gap-6 text-sm text-gray-500">
<span>{{ dayjs(comment.createTime * 1000).fromNow() }}</span>
<button
class="flex items-center gap-1 hover:text-blue-500 transition-colors"
@click="handleCommentLike(comment)"
>
<span class="text-white">👤</span>
<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>
</div>
<!-- 回复输入框 -->
<div v-show="replyingCommentId === comment.id" class="mt-4 flex gap-3">
<img :src="userInfo?.avatar" alt="" class="w-8 h-8 rounded-full object-cover" />
<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>
<el-input
v-model="replyContent"
type="textarea"
:placeholder="`回复 @${comment.username}`"
:rows="2"
></el-input>
<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>
<button class="hover:text-blue-500 transition-colors">👎</button>
<button class="hover:text-blue-500 transition-colors">回复</button>
</div>
</div>
</div>
<!-- 评论项 3 -->
<div class="flex gap-3">
<!-- 子回复列表 -->
<div v-if="comment.replies?.length" class="mt-4 space-y-3">
<div
class="w-10 h-10 bg-purple-400 rounded-full flex items-center justify-center flex-shrink-0"
v-for="reply in comment.replies"
:key="reply.id"
class="flex gap-3 p-3 bg-gray-50 rounded-lg"
>
<span class="text-white">👤</span>
</div>
<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-2">
<span class="font-medium text-gray-900">希木林M</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 14:08</span>
<button class="flex items-center gap-1 hover:text-blue-500 transition-colors">
<span>👍</span>
<span>296</span>
<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>
<button class="hover:text-blue-500 transition-colors">👎</button>
<button class="hover:text-blue-500 transition-colors">回复</button>
</div>
</div>
</div>
<!-- 评论项 4 -->
<div class="flex gap-3">
<div
class="w-10 h-10 bg-pink-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 15:07</span>
<button class="flex items-center gap-1 hover:text-blue-500 transition-colors">
<span>👍</span>
<span>70</span>
</button>
<button class="hover:text-blue-500 transition-colors">👎</button>
<button class="hover:text-blue-500 transition-colors">回复</button>
</div>
<div class="text-xs text-gray-500 mt-3">共37条回复,点击查看</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>
</template>
<script setup lang="ts">
// 可以在这里添加响应式数据和方法
</script>
<script lang="ts" setup>
import dayjs from 'dayjs'
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'
<style scoped lang="scss">
// 自定义样式
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
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
// 视频详情
const videoDetail = ref({
title: '',
videoUrl: '',
tags: [],
viewCount: 0,
likeCount: 0,
collectCount: 0,
shareCount: 0,
commentCount: 0,
publishTime: 0,
authorName: '',
authorAvatar: '',
fansCount: 0,
description: '',
hasFollowed: false,
hasLiked: false,
hasCollected: false,
})
// 评论筛选
const commentFilters = [
{ 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 = () => {
// 记录播放
}
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 = ''
}
// 刷新评论列表
}
</style>
// 回复评论
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