Commit cfe3bd46 by lijiabin

【需求 17679】 perf: 优化问吧、评论等页面

parent 9662ea89
...@@ -219,6 +219,9 @@ export interface ColumnItemDto { ...@@ -219,6 +219,9 @@ export interface ColumnItemDto {
title: string title: string
type: ArticleTypeEnum.COLUMN type: ArticleTypeEnum.COLUMN
viewCount: number viewCount: number
videoDuration: string
showName: string
showAvatar: string
}[] }[]
} }
...@@ -331,6 +334,8 @@ export interface CommentItemDto { ...@@ -331,6 +334,8 @@ export interface CommentItemDto {
childrenPageCurrent: number childrenPageCurrent: number
childrenPageList: CommentItemDto[] childrenPageList: CommentItemDto[]
loadingChildren: boolean loadingChildren: boolean
showComment: boolean
isExpand: boolean
} }
/** /**
......
...@@ -10,7 +10,13 @@ ...@@ -10,7 +10,13 @@
:src="articleDetail?.createUserAvatar" :src="articleDetail?.createUserAvatar"
alt="" alt=""
class="w-12 h-12 rounded-full object-cover cursor-pointer" class="w-12 h-12 rounded-full object-cover cursor-pointer"
@click="router.push(`/otherUserPage/${articleDetail?.createUserId}/0`)" @click="
jumpToUserHomePage({
userId: articleDetail?.createUserId,
isReal:
articleDetail.type === 'practice' || articleDetail.type === 'interview' ? 1 : 0,
})
"
/> />
<!-- <div <!-- <div
class="absolute -bottom-1 -right-1 w-6 h-6 bg-gradient-to-r from-yellow-400 to-orange-400 rounded-full flex items-center justify-center text-xs font-bold text-white" class="absolute -bottom-1 -right-1 w-6 h-6 bg-gradient-to-r from-yellow-400 to-orange-400 rounded-full flex items-center justify-center text-xs font-bold text-white"
...@@ -105,8 +111,8 @@ import dayjs from 'dayjs' ...@@ -105,8 +111,8 @@ import dayjs from 'dayjs'
import type { ArticleItemDto } from '@/api' import type { ArticleItemDto } from '@/api'
import { articleTypeListOptions, ArticleTypeEnum } from '@/constants' import { articleTypeListOptions, ArticleTypeEnum } from '@/constants'
import ActionMore from '@/components/common/ActionMore/index.vue' import ActionMore from '@/components/common/ActionMore/index.vue'
import { jumpToUserHomePage } from '@/utils'
const router = useRouter()
const { articleDetail } = defineProps<{ const { articleDetail } = defineProps<{
articleDetail: ArticleItemDto articleDetail: ArticleItemDto
}>() }>()
......
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
:src="userAvatar" :src="userAvatar"
alt="" alt=""
class="w-10 h-10 rounded-full object-cover cursor-pointer" class="w-10 h-10 rounded-full object-cover cursor-pointer"
@click="router.push(`/userPage`)" @click="jumpToUserHomePage({ userId: userInfo.userId, isReal: 0 })"
/> />
<div class="flex-1"> <div class="flex-1">
<div ref="commentInputRef"> <div ref="commentInputRef">
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
<div class="p-4 transition-colors"> <div class="p-4 transition-colors">
<div class="flex gap-3"> <div class="flex gap-3">
<img <img
@click="handleUserInfo(item)" @click="jumpToUserHomePage({ userId: item.userId, isReal: isReal })"
:src="item.avatar" :src="item.avatar"
alt="" alt=""
class="w-10 h-10 rounded-full object-cover cursor-pointer" class="w-10 h-10 rounded-full object-cover cursor-pointer"
...@@ -159,7 +159,7 @@ ...@@ -159,7 +159,7 @@
class="flex gap-2 p-3 rounded-lg" class="flex gap-2 p-3 rounded-lg"
> >
<img <img
@click="handleUserInfo(child)" @click="jumpToUserHomePage({ userId: child.userId, isReal: isReal })"
:src="child.avatar" :src="child.avatar"
alt="" alt=""
class="w-8 h-8 rounded-full object-cover cursor-pointer" class="w-8 h-8 rounded-full object-cover cursor-pointer"
...@@ -215,7 +215,7 @@ ...@@ -215,7 +215,7 @@
</div> </div>
</template> </template>
<!-- 只有大于5 才会显示这个 --> <!-- 只有大于5 才会显示这个 -->
<div class="ml-4" v-show="item.childrenNum > 5"> <div class="ml-4" v-show="item.childrenNum > 5 && !isQuestion">
<!-- 展示 展开回复 --> <!-- 展示 展开回复 -->
<button <button
v-show="!item.showChildrenPage" v-show="!item.showChildrenPage"
...@@ -249,13 +249,13 @@ ...@@ -249,13 +249,13 @@
</div> </div>
</div> </div>
<!-- 展示 回复评论的输入框 --> <!-- 展示 回复评论的输入框 -->
<!-- <transition name="fade" mode="out-in"> --> <transition name="fade" mode="in-out">
<div v-show="showCommentBox(item)" class="flex gap-3 mt-4"> <div v-show="showCommentBox(item)" class="flex gap-3 mt-4">
<img <img
:src="userAvatar" :src="userAvatar"
alt="" alt=""
class="w-10 h-10 rounded-full object-cover cursor-pointer" class="w-10 h-10 rounded-full object-cover cursor-pointer"
@click="router.push(`/userPage`)" @click="jumpToUserHomePage({ userId: userInfo.userId, isReal: isReal })"
/> />
<div class="flex-1"> <div class="flex-1">
<el-input <el-input
...@@ -283,7 +283,7 @@ ...@@ -283,7 +283,7 @@
</div> </div>
</div> </div>
</div> </div>
<!-- </transition> --> </transition>
</div> </div>
</div> </div>
</div> </div>
...@@ -309,7 +309,7 @@ ...@@ -309,7 +309,7 @@
</div> </div>
</div> </div>
</div> </div>
<CommentDialog ref="commentDialogRef" :articleId="id" :pid="currentDialogCommentPid" /> <CommentListDialog ref="commentListDialogRef" :articleId="id" :pid="currentDialogCommentPid" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
...@@ -326,7 +326,8 @@ import type { CommentItemDto } from '@/api' ...@@ -326,7 +326,8 @@ import type { CommentItemDto } from '@/api'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import CommentDialog from '../CommentDialog/index.vue' import CommentListDialog from '../CommentListDialog/index.vue'
import { jumpToUserHomePage } from '@/utils'
const { const {
id, id,
defaultSize = 10, defaultSize = 10,
...@@ -354,7 +355,7 @@ const userStore = useUserStore() ...@@ -354,7 +355,7 @@ const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
const userAvatar = computed(() => (isReal ? userInfo.value.avatar : userInfo.value.hiddenAvatar)) const userAvatar = computed(() => (isReal ? userInfo.value.avatar : userInfo.value.hiddenAvatar))
const commentRef = useTemplateRef<HTMLElement | null>('commentRef') const commentRef = useTemplateRef<HTMLElement | null>('commentRef')
const commentDialogRef = useTemplateRef<HTMLElement | null>('commentDialogRef') const commentListDialogRef = useTemplateRef<typeof CommentListDialog>('commentListDialogRef')
const commentInputRef = useTemplateRef<HTMLElement | null>('commentInputRef') const commentInputRef = useTemplateRef<HTMLElement | null>('commentInputRef')
const commentItemRefList = ref<HTMLElement[]>([]) const commentItemRefList = ref<HTMLElement[]>([])
// 回滚到评论框 // 回滚到评论框
...@@ -525,14 +526,10 @@ const getCurrentChildrenList = (item: CommentItemDto) => { ...@@ -525,14 +526,10 @@ const getCurrentChildrenList = (item: CommentItemDto) => {
} }
} }
const handleUserInfo = (item: CommentItemDto) => {
router.push(`/otherUserPage/${item.userId}/${isReal}`)
}
const currentDialogCommentPid = ref(0) const currentDialogCommentPid = ref(0)
const handleOpenCommentDialog = (item: CommentItemDto) => { const handleOpenCommentDialog = (item: CommentItemDto) => {
currentDialogCommentPid.value = item.id currentDialogCommentPid.value = item.id
commentDialogRef.value?.open() commentListDialogRef.value?.open()
} }
defineExpose({ defineExpose({
...@@ -541,14 +538,12 @@ defineExpose({ ...@@ -541,14 +538,12 @@ defineExpose({
}) })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.fade-enter-from, .fade-enter-from {
.fade-leave-to {
opacity: 0; opacity: 0;
transform: translateY(10px); transform: translateY(-10px);
} }
.fade-enter-active, .fade-enter-active {
.fade-leave-active { transition: all 0.5s ease-out;
transition: all 0.3s ease;
} }
</style> </style>
<template> <template>
<el-dialog <el-dialog v-model="visible" title="发表评论" width="500px" :before-close="handleClose">
v-model="visible" <div class="flex gap-3">
:title="dialogTitle" <!-- 用户头像 -->
width="600px" <el-avatar :size="40" :src="userInfo.hiddenAvatar" />
class="rounded-xl overflow-hidden"
:show-close="false"
append-to-body
destroy-on-close
>
<!-- 自定义头部 -->
<template #header="{ close, titleId, titleClass }">
<div class="flex items-center gap-2 py-2 border-b border-gray-100">
<el-icon class="cursor-pointer hover:text-blue-500" @click="close" :size="20">
<ArrowLeft />
</el-icon>
<span :id="titleId" :class="titleClass" class="text-base font-bold text-gray-800"
>评论回复</span
>
</div>
</template>
<div class="flex flex-col h-[70vh]">
<!-- 中间滚动区域 -->
<div class="flex-1 overflow-y-auto custom-scrollbar p-4" ref="scrollContainer">
<!-- 1. 顶部:父级评论展示 -->
<div v-if="parentComment" class="flex gap-3 mb-6">
<img
:src="parentComment.hiddenAvatar"
class="w-10 h-10 rounded-full object-cover border border-gray-100"
/>
<div class="flex-1">
<div class="flex items-center justify-between mb-1">
<span class="font-bold text-gray-900 text-sm">{{ parentComment.hiddenName }}</span>
<!-- 点赞按钮 -->
<div
class="flex items-center gap-1 cursor-pointer text-gray-400 hover:text-blue-500 transition-colors"
@click="handleLike(parentComment)"
>
<el-icon :size="16">
<svg-icon :name="parentComment.hasPraise ? 'praise_fill' : 'praise'"></svg-icon>
</el-icon>
<span class="text-xs">{{ parentComment.postPriseCount || 0 }}</span>
</div>
</div>
<p class="text-gray-800 text-sm leading-relaxed mb-2">{{ parentComment.content }}</p>
<div class="text-xs text-gray-400 flex items-center gap-3">
<span>{{ dayjs(parentComment.createTime * 1000).format('MM-DD HH:mm') }}</span>
<!-- <span>IP: {{ parentComment.ipLocation || '未知' }}</span> -->
<!-- <span class="bg-red-50 text-red-500 px-1.5 py-0.5 rounded text-[10px]">热评</span> -->
</div>
</div>
</div>
<!-- 分割线 & 统计 -->
<div class="h-2 bg-gray-50 -mx-4 mb-4"></div>
<div class="text-sm font-bold text-gray-800 mb-4">{{ total }} 条回复</div>
<!-- 2. 下方:回复列表 -->
<div v-loading="loading" class="space-y-6">
<div v-for="item in list" :key="item.id" class="flex gap-3 relative group">
<img
:src="item.avatar"
class="w-8 h-8 rounded-full object-cover cursor-pointer hover:opacity-80"
@click="handleUserInfo(item)"
/>
<div class="flex-1 border-b border-gray-50 pb-4">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-2">
<span class="font-semibold text-gray-800 text-sm">{{ item.replyUser }}</span>
<span
v-if="item.replyName && item.replyName !== parentComment?.replyUser"
class="text-gray-400 text-xs flex items-center"
>
<el-icon class="mx-1"><CaretRight /></el-icon>
{{ item.replyName }}
</span>
</div>
<!-- 列表项点赞 -->
<div class="flex items-center gap-4">
<div
class="flex items-center gap-1 cursor-pointer text-gray-400 hover:text-blue-500 transition-colors"
@click="handleReplyInline(item)"
>
<span class="text-xs">回复</span>
</div>
<div
class="flex items-center gap-1 cursor-pointer text-gray-400 hover:text-blue-500 transition-colors"
@click="handleLike(item)"
>
<el-icon :size="16">
<svg-icon :name="item.hasPraise ? 'praise_fill' : 'praise'"></svg-icon>
</el-icon>
<span class="text-xs">{{ item.postPriseCount || 0 }}</span>
</div>
</div>
</div>
<p class="text-gray-700 text-sm mb-2 break-all">{{ item.content }}</p>
<div class="text-xs text-gray-400">
<span>{{ formatDate(item.createTime) }}</span>
</div>
<!-- 内嵌回复框 (点击列表回复按钮出现) --> <!-- 评论输入框 -->
<div
v-if="currentInlineReplyId === item.id"
class="mt-3 bg-gray-50 p-3 rounded-lg border border-gray-100 animate-fade-in"
>
<el-input <el-input
v-model="inlineCommentContent" v-model="commentContent"
type="textarea" type="textarea"
:rows="2" :rows="4"
:placeholder="`回复 ${item.replyUser}`" placeholder="写下你的评论..."
class="bg-white mb-2" maxlength="500"
resize="none" show-word-limit
/> class="flex-1"
<div class="flex justify-between items-center">
<div class="flex gap-2 text-gray-400 text-lg">
<i class="cursor-pointer i-carbon-face-satisfied hover:text-yellow-500"></i>
<i class="cursor-pointer i-carbon-image hover:text-blue-500"></i>
</div>
<el-button
type="primary"
size="small"
class="!rounded-full !px-4"
:disabled="!inlineCommentContent.trim()"
@click="submitReply(item.id)"
>
发布
</el-button>
</div>
</div>
</div>
</div>
<!-- 加载更多 / 分页 -->
<div class="flex justify-end py-4" v-if="total > 0">
<el-pagination
v-model:current-page="searchParams.current"
:page-size="searchParams.size"
:total="total"
layout="prev, pager, next"
small
background
@current-change="goToPage"
@size-change="changePageSize"
/> />
</div> </div>
<el-empty v-if="!loading && list.length === 0" description="暂无回复" :image-size="60" /> <template #footer>
</div> <div class="flex justify-end gap-2">
</div> <el-button @click="handleClose" class="rounded-lg">取消</el-button>
<!-- 3. 底部固定回复框 (回复给最上方的父评论) -->
<div
class="border-t border-gray-100 p-3 bg-white flex items-center gap-3 z-10 shadow-[0_-2px_10px_rgba(0,0,0,0.02)]"
>
<img :src="currentUserAvatar" class="w-8 h-8 rounded-full object-cover" />
<div
class="flex-1 bg-gray-100 rounded-full px-4 py-2 flex items-center cursor-text hover:bg-gray-200 transition-colors"
@click="focusBottomInput"
>
<input
ref="bottomInputRef"
v-model="bottomCommentContent"
:autosize="{ minRows: 1, maxRows: 4 }"
type="textarea"
class="bg-transparent w-full outline-none text-sm text-gray-700 placeholder-gray-400"
:placeholder="`回复 ${parentComment?.replyUser || '...'}`"
@keyup.enter="submitReply(parentComment?.id)"
/>
</div>
<el-button <el-button
type="primary" type="primary"
circle @click="handleSubmit"
:disabled="!bottomCommentContent.trim()" class="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
@click="submitReply(parentComment?.id)" >发表</el-button
> >
<el-icon><Position /></el-icon>
</el-button>
</div>
</div> </div>
</template>
</el-dialog> </el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, nextTick } from 'vue'
import { ArrowLeft, Star, StarFilled, CaretRight, Position } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { import { addComment } from '@/api'
getSecondCommentChildren,
addComment,
addOrCancelCommentLike,
getCommentDetail,
} from '@/api'
import type { CommentItemDto } from '@/api'
import { BooleanFlag } from '@/constants'
import { usePageSearch } from '@/hooks' // 假设你有这个hook
// Props
const { articleId, pid } = defineProps<{
articleId: number
pid: number
}>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'refresh'): void // 通知父组件刷新 (e: 'commentSuccess'): void
}>() }>()
// Store
const userStore = useUserStore() const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
const currentUserAvatar = computed(() => userInfo.value.hiddenAvatar) // 弹窗显示状态
// State
const visible = ref(false) const visible = ref(false)
const parentComment = ref<CommentItemDto | null>(null) // 评论内容
const dialogTitle = ref('详情') const commentContent = ref('')
let articleId = 0
// Inline Reply State
const currentInlineReplyId = ref<number | null>(null)
const inlineCommentContent = ref('')
const bottomCommentContent = ref('')
const bottomInputRef = ref<HTMLInputElement>()
const scrollContainer = ref<HTMLElement>()
// --- Actions --- // 暴露 open 方法
const open = (id: number) => {
const { list, total, search, searchParams, goToPage, changePageSize, refresh, loading } = articleId = id
usePageSearch(getSecondCommentChildren, {
defaultParams: {
articleId: articleId,
pid: 0,
},
immediate: false,
})
const open = async (item: CommentItemDto) => {
// const { data } = await getSecondCommentChildren({
// pid: item.id,
// current: 1,
// size: 10,
// articleId: articleId,
// })
// console.log('res', data)
// list.value = data.list
// parentComment.value = item
// // Reset state
// currentInlineReplyId.value = null
// inlineCommentContent.value = ''
// bottomCommentContent.value = ''
console.log('pid', pid)
await nextTick()
searchParams.value.pid = pid
search()
const { data } = await getCommentDetail(pid)
console.log('data', data)
parentComment.value = data
visible.value = true visible.value = true
commentContent.value = ''
} }
const close = () => { // 关闭弹窗
const handleClose = () => {
visible.value = false visible.value = false
commentContent.value = ''
} }
const formatDate = (time: number) => { // 提交评论
return dayjs(time * 1000).format('MM-DD HH:mm') const handleSubmit = async () => {
} if (!commentContent.value.trim()) {
ElMessage.warning('请输入评论内容')
// 点击列表中的“回复” return
const handleReplyInline = (item: CommentItemDto) => {
if (currentInlineReplyId.value === item.id) {
currentInlineReplyId.value = null // Toggle off
} else {
currentInlineReplyId.value = item.id
inlineCommentContent.value = '' // Clear previous
} }
}
// 聚焦底部输入框 // TODO: 这里处理提交逻辑
const focusBottomInput = () => {
bottomInputRef.value?.focus()
}
// 提交评论 (共用逻辑)
// targetId: 如果是回复父评论,传 parentComment.id;如果是回复子评论,传 item.id
const submitReply = async (targetId: number | undefined) => {
if (!targetId) return
// 判断使用的是哪个输入框的内容
const isBottom = targetId === parentComment.value?.id
const content = isBottom ? bottomCommentContent.value : inlineCommentContent.value
if (!content.trim()) return
try {
await addComment({ await addComment({
articleId: articleId, articleId: articleId,
content: content, content: commentContent.value,
pid: targetId, // 这里的pid逻辑根据您的后端接口来,通常回复子评论也是传该子评论ID作为pid
}) })
console.log('评论内容:', commentContent.value)
ElMessage.success('回复成功') ElMessage.success('评论发表成功')
handleClose()
// 清空输入框 emit('commentSuccess')
if (isBottom) {
bottomCommentContent.value = ''
} else {
inlineCommentContent.value = ''
currentInlineReplyId.value = null
}
// 刷新列表
refresh()
// 通知父组件可能需要更新评论数
emit('refresh')
} catch (error) {
console.error(error)
}
}
// 点赞
const handleLike = async (item: CommentItemDto) => {
try {
await addOrCancelCommentLike(item.id)
// 乐观更新 UI
if (item.hasPraise === BooleanFlag.YES) {
item.hasPraise = BooleanFlag.NO
item.postPriseCount--
} else {
item.hasPraise = BooleanFlag.YES
item.postPriseCount++
}
} catch (error) {
console.error(error)
}
}
const handleUserInfo = (item: CommentItemDto) => {
// 您的跳转逻辑
// router.push(...)
} }
// 暴露方法给父组件
defineExpose({ defineExpose({
open, open,
}) })
</script> </script>
<style scoped lang="scss"> <style scoped>
/* 使用 UnoCSS 这里写一些 Element Plus 的样式覆盖 */ /* 如果需要额外样式可以在这里添加 */
:deep(.el-dialog__header) {
margin-right: 0;
padding: 0 16px;
}
:deep(.el-dialog__body) {
padding: 0;
}
/* 自定义滚动条 */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #e5e7eb;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background-color: transparent;
}
.animate-fade-in {
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style> </style>
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="700px"
class="rounded-2xl overflow-hidden"
:show-close="false"
top="5vh"
append-to-body
destroy-on-close
>
<!-- 自定义头部 -->
<template #header="{ close, titleId, titleClass }">
<div class="flex items-center gap-3 py-4 px-2 border-b border-gray-100">
<el-icon
class="cursor-pointer hover:text-blue-500 transition-colors"
@click="close"
:size="24"
>
<ArrowLeft />
</el-icon>
<span :id="titleId" :class="titleClass" class="text-lg font-bold text-gray-800">
评论回复
</span>
</div>
</template>
<div class="flex flex-col h-[80vh]">
<!-- 中间滚动区域 -->
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 pt-0" ref="scrollContainer">
<!-- 1. 顶部:父级评论展示 -->
<div v-if="parentComment" class="flex gap-4 bg-gray-50 p-5 rounded-xl">
<img
:src="parentComment.hiddenAvatar"
class="w-12 h-12 rounded-full object-cover border-2 border-gray-200 flex-shrink-0"
/>
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<span class="font-bold text-gray-900 text-base">{{ parentComment.hiddenName }}</span>
<!-- 点赞按钮 -->
<div
class="flex items-center gap-1.5 cursor-pointer text-gray-400 hover:text-blue-500 transition-colors px-3 py-1.5 rounded-full hover:bg-blue-50"
@click="handleLike(parentComment)"
>
<el-icon :size="18">
<svg-icon :name="parentComment.hasPraise ? 'praise_fill' : 'praise'"></svg-icon>
</el-icon>
<span class="text-sm font-medium">{{ parentComment.postPriseCount || 0 }}</span>
</div>
</div>
<p class="text-gray-800 text-base leading-relaxed mb-3">{{ parentComment.content }}</p>
<div class="text-sm text-gray-400 flex items-center gap-4">
<span>{{ dayjs(parentComment.createTime * 1000).format('MM-DD HH:mm') }}</span>
</div>
</div>
</div>
<!-- 分割线 & 统计 -->
<div class="h-2.5"></div>
<div class="text-base font-bold text-gray-800 mb-6 px-1">{{ total }} 条回复</div>
<!-- 2. 下方:回复列表 -->
<div v-loading="loading" class="space-y-6">
<div v-for="item in list" :key="item.id" class="flex gap-4 relative group">
<img
:src="item.avatar"
class="w-10 h-10 rounded-full object-cover cursor-pointer hover:opacity-80 transition-opacity flex-shrink-0"
@click="handleUserInfo(item)"
/>
<div class="flex-1 border-b border-gray-100">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="font-semibold text-gray-900 text-base">{{ item.replyUser }}</span>
<span
v-if="item.replyName && item.replyName !== parentComment?.replyUser"
class="text-gray-400 text-sm flex items-center"
>
<el-icon class="mx-1"><CaretRight /></el-icon>
{{ item.replyName }}
</span>
</div>
<!-- 列表项点赞 -->
<div class="flex items-center gap-4">
<div
class="flex items-center gap-1.5 cursor-pointer text-gray-400 hover:text-blue-500 transition-colors px-3 py-1.5 rounded-full hover:bg-blue-50"
@click="handleReplyInline(item)"
>
<span class="text-sm font-medium">回复</span>
</div>
<div
class="flex items-center gap-1.5 cursor-pointer text-gray-400 hover:text-blue-500 transition-colors px-3 py-1.5 rounded-full hover:bg-blue-50"
@click="handleLike(item)"
>
<el-icon :size="18">
<svg-icon :name="item.hasPraise ? 'praise_fill' : 'praise'"></svg-icon>
</el-icon>
<span class="text-sm font-medium">{{ item.postPriseCount || 0 }}</span>
</div>
</div>
</div>
<p class="text-gray-700 text-base mb-3 break-all leading-relaxed">
{{ item.content }}
</p>
<div class="text-sm text-gray-400">
<span>{{ formatDate(item.createTime) }}</span>
</div>
<!-- 内嵌回复框 -->
<div
v-if="currentInlineReplyId === item.id"
class="mt-4 bg-gray-50 p-4 rounded-xl border border-gray-200 animate-fade-in animate-fade-out"
>
<el-input
v-model="inlineCommentContent"
type="textarea"
:rows="3"
:placeholder="`回复 ${item.replyUser}`"
class="bg-white mb-3 text-base"
resize="none"
/>
<div class="flex justify-between items-center">
<div class="flex gap-3 text-gray-400 text-xl">
<i
class="cursor-pointer i-carbon-face-satisfied hover:text-yellow-500 transition-colors"
></i>
<i
class="cursor-pointer i-carbon-image hover:text-blue-500 transition-colors"
></i>
</div>
<el-button
type="primary"
size="default"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
:disabled="!inlineCommentContent.trim()"
@click="submitReply(item.id)"
>
发布
</el-button>
</div>
</div>
</div>
</div>
<!-- 加载更多 / 分页 -->
<div class="flex justify-end" v-if="total > 0">
<el-pagination
v-model:current-page="searchParams.current"
:page-size="searchParams.size"
:total="total"
layout="prev, pager, next,total"
@current-change="goToPage"
@size-change="changePageSize"
/>
</div>
<el-empty v-if="!loading && list.length === 0" description="暂无回复" :image-size="80" />
</div>
</div>
<!-- 3. 底部固定回复框 -->
<div
class="border-t border-gray-100 p-5 bg-white flex items-center gap-4 z-10 shadow-[0_-4px_16px_rgba(0,0,0,0.04)]"
>
<img :src="currentUserAvatar" class="w-10 h-10 rounded-full object-cover flex-shrink-0" />
<div
class="flex-1 bg-gray-100 rounded-full px-5 py-3 flex items-center cursor-text hover:bg-gray-200 transition-colors"
@click="focusBottomInput"
>
<input
ref="bottomInputRef"
v-model="bottomCommentContent"
type="text"
class="bg-transparent w-full outline-none text-base text-gray-700 placeholder-gray-400"
:placeholder="`回复 ${parentComment?.replyUser || '...'}`"
@keyup.enter="submitReply(parentComment?.id)"
/>
</div>
<el-button
type="primary"
size="large"
circle
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
:disabled="!bottomCommentContent.trim()"
@click="submitReply(parentComment?.id)"
>
<el-icon :size="20"><Position /></el-icon>
</el-button>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick } from 'vue'
import { ArrowLeft, Star, StarFilled, CaretRight, Position } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import {
getSecondCommentChildren,
addComment,
addOrCancelCommentLike,
getCommentDetail,
} from '@/api'
import type { CommentItemDto } from '@/api'
import { BooleanFlag } from '@/constants'
import { usePageSearch } from '@/hooks' // 假设你有这个hook
// Props
const { articleId, pid } = defineProps<{
articleId: number
pid: number
}>()
const emit = defineEmits<{
(e: 'refresh'): void // 通知父组件刷新
}>()
// Store
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const currentUserAvatar = computed(() => userInfo.value.hiddenAvatar)
// State
const visible = ref(false)
const parentComment = ref<CommentItemDto | null>(null)
const dialogTitle = ref('详情')
// Inline Reply State
const currentInlineReplyId = ref<number | null>(null)
const inlineCommentContent = ref('')
const bottomCommentContent = ref('')
const bottomInputRef = ref<HTMLInputElement>()
const scrollContainer = ref<HTMLElement>()
// --- Actions ---
const { list, total, search, searchParams, goToPage, changePageSize, refresh, loading } =
usePageSearch(getSecondCommentChildren, {
defaultParams: {
articleId: articleId,
pid: 0,
},
immediate: false,
})
const open = async (item: CommentItemDto) => {
// const { data } = await getSecondCommentChildren({
// pid: item.id,
// current: 1,
// size: 10,
// articleId: articleId,
// })
// console.log('res', data)
// list.value = data.list
// parentComment.value = item
// // Reset state
// currentInlineReplyId.value = null
// inlineCommentContent.value = ''
// bottomCommentContent.value = ''
console.log('pid', pid)
await nextTick()
searchParams.value.pid = pid
search()
const { data } = await getCommentDetail(pid)
console.log('data', data)
parentComment.value = data
visible.value = true
}
const close = () => {
visible.value = false
}
const formatDate = (time: number) => {
return dayjs(time * 1000).format('MM-DD HH:mm')
}
// 点击列表中的“回复”
const handleReplyInline = (item: CommentItemDto) => {
if (currentInlineReplyId.value === item.id) {
currentInlineReplyId.value = null // Toggle off
} else {
currentInlineReplyId.value = item.id
inlineCommentContent.value = '' // Clear previous
}
}
// 聚焦底部输入框
const focusBottomInput = () => {
bottomInputRef.value?.focus()
}
// 提交评论 (共用逻辑)
// targetId: 如果是回复父评论,传 parentComment.id;如果是回复子评论,传 item.id
const submitReply = async (targetId: number | undefined) => {
if (!targetId) return
// 判断使用的是哪个输入框的内容
const isBottom = targetId === parentComment.value?.id
const content = isBottom ? bottomCommentContent.value : inlineCommentContent.value
if (!content.trim()) return
try {
await addComment({
articleId: articleId,
content: content,
pid: targetId, // 这里的pid逻辑根据您的后端接口来,通常回复子评论也是传该子评论ID作为pid
})
ElMessage.success('回复成功')
// 清空输入框
if (isBottom) {
bottomCommentContent.value = ''
} else {
inlineCommentContent.value = ''
currentInlineReplyId.value = null
}
// 刷新列表
refresh()
// 通知父组件可能需要更新评论数
emit('refresh')
} catch (error) {
console.error(error)
}
}
// 点赞
const handleLike = async (item: CommentItemDto) => {
try {
await addOrCancelCommentLike(item.id)
// 乐观更新 UI
if (item.hasPraise === BooleanFlag.YES) {
item.hasPraise = BooleanFlag.NO
item.postPriseCount--
} else {
item.hasPraise = BooleanFlag.YES
item.postPriseCount++
}
} catch (error) {
console.error(error)
}
}
const handleUserInfo = (item: CommentItemDto) => {
// 您的跳转逻辑
// router.push(...)
}
defineExpose({
open,
})
</script>
<style scoped lang="scss">
/* 使用 UnoCSS 这里写一些 Element Plus 的样式覆盖 */
:deep(.el-dialog__header) {
margin-right: 0;
padding: 0 16px;
}
:deep(.el-dialog__body) {
padding: 0;
}
/* 自定义滚动条 */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #e5e7eb;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background-color: transparent;
}
.animate-fade-in {
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
...@@ -96,11 +96,7 @@ const widthRate = computed(() => { ...@@ -96,11 +96,7 @@ const widthRate = computed(() => {
// 在线时长格式化 将秒级 格式化为 00:00 // 在线时长格式化 将秒级 格式化为 00:00
const formatSeconds = computed(() => { const formatSeconds = computed(() => {
if (currentSeconds.value >= maxSeconds) {
return '30:00'
} else {
return dayjs.utc(currentSeconds.value * 1000).format('mm:ss') return dayjs.utc(currentSeconds.value * 1000).format('mm:ss')
}
}) })
onMounted(async () => { onMounted(async () => {
......
...@@ -167,7 +167,7 @@ export const constantsRoute = [ ...@@ -167,7 +167,7 @@ export const constantsRoute = [
component: () => import('@/views/questionDetail/index.vue'), component: () => import('@/views/questionDetail/index.vue'),
}, },
{ {
path: 'publishLongArticle', path: 'publishLongArticle/:type',
name: 'CulturePublishLongArticle', name: 'CulturePublishLongArticle',
component: () => import('@/views/publishLongArticle/index.vue'), component: () => import('@/views/publishLongArticle/index.vue'),
}, },
......
import type { ArticleType, BooleanFlag } from '@/constants'
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
/** /**
* 页面改变标题 * 页面改变标题
* @param title * @param title
...@@ -59,3 +62,26 @@ export function isCulturePath() { ...@@ -59,3 +62,26 @@ export function isCulturePath() {
const path = window.location.pathname const path = window.location.pathname
return path.includes('/culture') return path.includes('/culture')
} }
// 点击头像跳转用户首页
export function jumpToUserHomePage({ userId, isReal }: { userId: number; isReal: BooleanFlag }) {
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const isSelf = userInfo.value.userId === userId
if (isSelf) {
window.open(`/userPage/selfPublish`)
} else {
window.open(`/otherUserPage/${userId}/${isReal}`)
}
}
// 根据文章类型跳到对应的文章详情页面
export function jumpToArticleDetailPage({ type, id }: { type: ArticleType; id: number }) {
if (type === 'video') {
window.open(`/videoDetail/${id}`)
} else if (type === 'question') {
window.open(`/questionDetail/${id}`)
} else {
window.open(`/articleDetail/${id}`)
}
}
export * from './app'
...@@ -68,7 +68,7 @@ export function selectDepOrUser(wxOption = {}): Promise<ISelectDepOrUser> { ...@@ -68,7 +68,7 @@ export function selectDepOrUser(wxOption = {}): Promise<ISelectDepOrUser> {
...wxOption, ...wxOption,
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
wx.invoke('selectEnterpriseContact', option, function (res: ISelectDepOrUser) { ww.invoke('selectEnterpriseContact', option, function (res: ISelectDepOrUser) {
if (res.err_msg == 'selectEnterpriseContact:ok') { if (res.err_msg == 'selectEnterpriseContact:ok') {
resolve(res) resolve(res)
} }
......
...@@ -44,9 +44,15 @@ ...@@ -44,9 +44,15 @@
class="text-gray-600 text-base leading-relaxed transition-all duration-300" class="text-gray-600 text-base leading-relaxed transition-all duration-300"
:class="{ 'line-clamp-3': !item.isExpand }" :class="{ 'line-clamp-3': !item.isExpand }"
> >
<!-- 如果有评论的话 就展示 最热的 没有的话 说明没有评论 就展示内容 -->
<template v-if="item.cultureCommentListVo?.hiddenName">
{{ item.cultureCommentListVo?.hiddenName }}{{ {{ item.cultureCommentListVo?.hiddenName }}{{
item.cultureCommentListVo?.content item.cultureCommentListVo?.content
}} }}
</template>
<template v-else>
{{ item.content }}
</template>
</p> </p>
<!-- 展开/收起按钮 靠右边布局 --> <!-- 展开/收起按钮 靠右边布局 -->
<div class="flex justify-end"> <div class="flex justify-end">
...@@ -112,7 +118,12 @@ ...@@ -112,7 +118,12 @@
<!-- 当前最热评论的评论有几条 --> <!-- 当前最热评论的评论有几条 -->
<el-button size="small" plain @click.stop="handleComment(item, index)"> <el-button size="small" plain @click.stop="handleComment(item, index)">
<el-icon><Edit /></el-icon> <el-icon><Edit /></el-icon>
{{ item.cultureCommentListVo?.childNum || 0 }}条评论 <template v-if="item.cultureCommentListVo?.childNum">
{{ item.cultureCommentListVo?.childNum }}条评论
</template>
<template v-else>
<span>写评论</span>
</template>
</el-button> </el-button>
<ActionMore class="ml-4" :articleDetail="item" /> <ActionMore class="ml-4" :articleDetail="item" />
...@@ -152,7 +163,7 @@ ...@@ -152,7 +163,7 @@
</div> </div>
<Transition name="fade"> <Transition name="fade">
<Comment <Comment
v-show="item.showComment" v-if="item.showComment"
:ref="(e) => (commentRefList[index] = e as InstanceType<typeof Comment>)" :ref="(e) => (commentRefList[index] = e as InstanceType<typeof Comment>)"
:id="item.id" :id="item.id"
:total="item.cultureCommentListVo?.childNum || 0" :total="item.cultureCommentListVo?.childNum || 0"
...@@ -183,6 +194,13 @@ ...@@ -183,6 +194,13 @@
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
@current-change="
async (e) => {
await goToPage(e)
handleBackTop()
}
"
@size-change="changePageSize"
:page-sizes="[15, 30, 45, 60]" :page-sizes="[15, 30, 45, 60]"
layout="prev, pager, next, jumper, total" layout="prev, pager, next, jumper, total"
:total="total" :total="total"
...@@ -207,6 +225,7 @@ ...@@ -207,6 +225,7 @@
</el-tour-step> </el-tour-step>
<template #indicators></template> <template #indicators></template>
</el-tour> </el-tour>
<CommentDialog ref="commentDialogRef" @commentSuccess="search" />
</div> </div>
</template> </template>
...@@ -223,12 +242,14 @@ import { getArticleList, addOrCanceArticlelCollect, addOrCancelToAnswerList } fr ...@@ -223,12 +242,14 @@ import { getArticleList, addOrCanceArticlelCollect, addOrCancelToAnswerList } fr
import type { ArticleItemDto } from '@/api/article/types' import type { ArticleItemDto } from '@/api/article/types'
import { useQuestionStore } from '@/stores/question' import { useQuestionStore } from '@/stores/question'
import ActionMore from '@/components/common/ActionMore/index.vue' import ActionMore from '@/components/common/ActionMore/index.vue'
import CommentDialog from '@/components/common/CommentDialog/index.vue'
const { fetchUserQestionNum } = useQuestionStore() const { fetchUserQestionNum } = useQuestionStore()
const route = useRoute() const route = useRoute()
const open = ref(false) const open = ref(false)
const publishBoxRef = useTemplateRef('publishBoxRef') const publishBoxRef = useTemplateRef('publishBoxRef')
const commentDialogRef = useTemplateRef<typeof CommentDialog>('commentDialogRef')
const activeTab = ref('最新') const activeTab = ref('最新')
const tabs = [ const tabs = [
...@@ -237,7 +258,8 @@ const tabs = [ ...@@ -237,7 +258,8 @@ const tabs = [
{ label: '关注', value: '关注' }, { label: '关注', value: '关注' },
] ]
const { list, total, searchParams, loading, refresh } = usePageSearch(getArticleList, { const { list, total, searchParams, loading, refresh, goToPage, changePageSize, search } =
usePageSearch(getArticleList, {
immediate: false, immediate: false,
defaultParams: { defaultParams: {
type: ArticleTypeEnum.QUESTION, type: ArticleTypeEnum.QUESTION,
...@@ -248,7 +270,7 @@ const { list, total, searchParams, loading, refresh } = usePageSearch(getArticle ...@@ -248,7 +270,7 @@ const { list, total, searchParams, loading, refresh } = usePageSearch(getArticle
showComment: false, showComment: false,
isExpand: false, isExpand: false,
})), })),
}) })
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
...@@ -261,9 +283,19 @@ const handleCollect = async (item: ArticleItemDto) => { ...@@ -261,9 +283,19 @@ const handleCollect = async (item: ArticleItemDto) => {
ElMessage.success(item.hasCollect ? '收藏成功' : '取消收藏') ElMessage.success(item.hasCollect ? '收藏成功' : '取消收藏')
} }
const handleComment = (item: ArticleItemDto, index: number) => { // 一个是直接写这个问题的首评论 一个是直接给最热评论写评论
commentRefList.value[index]?.search() const handleComment = async (item: ArticleItemDto, index: number) => {
if (item.cultureCommentListVo?.hiddenName) {
item.showComment = !item.showComment item.showComment = !item.showComment
await nextTick()
if (item.showComment) {
commentRefList.value[index]?.search()
}
} else {
// 直接让他写评论
commentDialogRef.value?.open(item.id)
}
} }
const handleCommentSuccess = (item: ArticleItemDto) => { const handleCommentSuccess = (item: ArticleItemDto) => {
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
v-for="item in list" v-for="item in list"
:key="item.id" :key="item.id"
class="group bg-white rounded-lg p-4 sm:p-6 cursor-pointer transition-all duration-300 hover:shadow-lg hover:shadow-gray-100 hover:-translate-y-1 border border-gray-100 hover:border-gray-200 mb-3 sm:mb-4" class="group bg-white rounded-lg p-4 sm:p-6 cursor-pointer transition-all duration-300 hover:shadow-lg hover:shadow-gray-100 hover:-translate-y-1 border border-gray-100 hover:border-gray-200 mb-3 sm:mb-4"
@click="handleClickItem(item)" @click="jumpToArticleDetailPage({ type: item.type, id: item.id })"
> >
<div class="flex gap-3 justify-between"> <div class="flex gap-3 justify-between">
<!-- <div <!-- <div
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
</h2> </h2>
<!-- 内容摘要 --> <!-- 内容摘要 -->
<div class="my-2 space-y-1"> <div v-if="!item.content?.includes('</')" class="my-2 space-y-1">
<p <p
class="text-gray-600 text-sm sm:text-base leading-relaxed line-clamp-1 break-all" class="text-gray-600 text-sm sm:text-base leading-relaxed line-clamp-1 break-all"
> >
...@@ -38,6 +38,19 @@ ...@@ -38,6 +38,19 @@
<div <div
class="flex flex-wrap items-center gap-2 sm:gap-4 text-gray-500 text-xs sm:text-sm" class="flex flex-wrap items-center gap-2 sm:gap-4 text-gray-500 text-xs sm:text-sm"
> >
<!-- 发布人名称和头像 -->
<div class="flex items-center gap-2">
<el-avatar :size="24" :src="item.showAvatar" />
<span class="text-sm text-gray-500">{{ item.showName }}</span>
</div>
<!-- 时间 -->
<span class="text-gray-500 font-medium ml-auto sm:ml-0">
<span class="hidden sm:inline">{{
dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm')
}}</span>
</span>
<!-- 分隔符 -->
<div class="hidden sm:block w-1 h-1 bg-gray-300 rounded-full"></div>
<div class="flex items-center gap-1 hover:text-blue-500 transition-colors"> <div class="flex items-center gap-1 hover:text-blue-500 transition-colors">
<el-icon class="text-sm"><View /></el-icon> <el-icon class="text-sm"><View /></el-icon>
<span class="font-medium">{{ item.viewCount }}</span> <span class="font-medium">{{ item.viewCount }}</span>
...@@ -50,20 +63,6 @@ ...@@ -50,20 +63,6 @@
<el-icon class="text-sm"><Star /></el-icon> <el-icon class="text-sm"><Star /></el-icon>
<span class="font-medium">{{ item.collectionCount }}</span> <span class="font-medium">{{ item.collectionCount }}</span>
</div> </div>
<!-- 分隔符 -->
<div class="hidden sm:block w-1 h-1 bg-gray-300 rounded-full"></div>
<!-- 发布人名称和头像 -->
<div class="flex items-center gap-2">
<el-avatar :size="24" :src="item.showAvatar" />
<span class="text-sm text-gray-500">{{ item.showName }}</span>
</div>
<!-- 时间 -->
<span class="text-gray-500 font-medium ml-auto sm:ml-0">
<span class="hidden sm:inline">{{
dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm')
}}</span>
</span>
</div> </div>
</div> </div>
...@@ -106,8 +105,9 @@ ...@@ -106,8 +105,9 @@
:total="total" :total="total"
class="custom-pagination" class="custom-pagination"
@current-change=" @current-change="
(e) => { async (e) => {
;(handleBackTop(), goToPage(e)) await goToPage(e)
handleBackTop()
} }
" "
@size-change="changePageSize" @size-change="changePageSize"
...@@ -128,10 +128,11 @@ ...@@ -128,10 +128,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import { getArticleList, type ArticleItemDto } from '@/api' import { getArticleList } from '@/api'
import { TABS_REF_KEY, ArticleTypeEnum } from '@/constants' import { TABS_REF_KEY } from '@/constants'
import { useScrollTop } from '@/hooks' import { useScrollTop } from '@/hooks'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { jumpToArticleDetailPage } from '@/utils'
const { list, total, searchParams, loading, goToPage, changePageSize, refresh } = usePageSearch( const { list, total, searchParams, loading, goToPage, changePageSize, refresh } = usePageSearch(
getArticleList, getArticleList,
...@@ -146,14 +147,6 @@ const tabsRef = inject(TABS_REF_KEY) ...@@ -146,14 +147,6 @@ const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!) const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
const handleClickItem = (item: ArticleItemDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
window.open(`/videoDetail/${item.id}`)
} else {
window.open(`/articleDetail/${item.id}`)
}
}
defineExpose({ defineExpose({
refresh: (sortLogic?: number) => { refresh: (sortLogic?: number) => {
console.log('sortLogic', sortLogic) console.log('sortLogic', sortLogic)
......
...@@ -6,12 +6,17 @@ ...@@ -6,12 +6,17 @@
<div class="flex items-center justify-between pb-4"> <div class="flex items-center justify-between pb-4">
<!-- 左侧 Tabs --> <!-- 左侧 Tabs -->
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<div v-for="tab in tabs" :key="tab.sortLogic" :class="[ <div
v-for="tab in tabs"
:key="tab.sortLogic"
:class="[
'px-6 py-2 rounded-full text-sm font-medium cursor-pointer transition-all duration-200', 'px-6 py-2 rounded-full text-sm font-medium cursor-pointer transition-all duration-200',
searchParams.sortLogic === tab.sortLogic searchParams.sortLogic === tab.sortLogic
? 'bg-orange-400 text-white shadow-md' ? 'bg-orange-400 text-white shadow-md'
: 'text-gray-600 hover:text-orange-500 hover:bg-orange-50', : 'text-gray-600 hover:text-orange-500 hover:bg-orange-50',
]" @click="toggleTab(tab.sortLogic)"> ]"
@click="toggleTab(tab.sortLogic)"
>
{{ tab.label }} {{ tab.label }}
</div> </div>
</div> </div>
...@@ -24,12 +29,19 @@ ...@@ -24,12 +29,19 @@
<!-- 前三个特殊布局 --> <!-- 前三个特殊布局 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- 第一个视频 - 占据两列 --> <!-- 第一个视频 - 占据两列 -->
<div v-show="list.length >= 1" @click="openDetailPage(`/videoDetail/${list[0]?.id}`)" <div
class="lg:col-span-2 group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-xl transition-all duration-500 cursor-pointer"> v-show="list.length >= 1"
@click="jumpToArticleDetailPage({ type: ArticleTypeEnum.VIDEO, id: list[0]?.id })"
class="lg:col-span-2 group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-xl transition-all duration-500 cursor-pointer"
>
<div class="relative overflow-hidden mb-2"> <div class="relative overflow-hidden mb-2">
<img :src="list[0]?.faceUrl" <img
class="w-full h-90 object-cover group-hover:scale-105 transition-transform duration-700" /> :src="list[0]?.faceUrl"
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent"></div> class="w-full h-90 object-cover group-hover:scale-105 transition-transform duration-700"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent"
></div>
<!-- 标签和数据 --> <!-- 标签和数据 -->
<!-- <div <!-- <div
...@@ -37,29 +49,38 @@ ...@@ -37,29 +49,38 @@
> >
🔥 推荐 🔥 推荐
</div> --> </div> -->
<div v-if="list[0]?.isRecommend" <div
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"> v-if="list[0]?.isRecommend"
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"
>
<img class="w-6" src="@/assets/img/culture/recommend.png" /> <img class="w-6" src="@/assets/img/culture/recommend.png" />
<div class="text-12px text-#000 line-height-12px">推荐</div> <div class="text-12px text-#000 line-height-12px">推荐</div>
</div> </div>
<div <div
class="absolute bottom-4 right-4 bg-black/80 backdrop-blur-sm text-white px-3 py-1.5 rounded-lg text-sm"> class="absolute bottom-4 right-4 bg-black/80 backdrop-blur-sm text-white px-3 py-1.5 rounded-lg text-sm"
>
{{ list[0]?.videoDuration }} {{ list[0]?.videoDuration }}
</div> </div>
<div class="absolute bottom-4 left-4 flex gap-4 text-white"> <div class="absolute bottom-4 left-4 flex gap-4 text-white">
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<View /> <View />
</el-icon> </el-icon>
<span>{{ list[0]?.viewCount }}</span> <span>{{ list[0]?.viewCount }}</span>
</div> </div>
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<ChatDotRound /> <ChatDotRound />
</el-icon> </el-icon>
<span>{{ list[0]?.replyCount }}</span> <span>{{ list[0]?.replyCount }}</span>
</div> </div>
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<Star /> <Star />
</el-icon> </el-icon>
...@@ -80,7 +101,9 @@ ...@@ -80,7 +101,9 @@
</div> </div>
<div class="p-6"> <div class="p-6">
<h3 class="font-bold text-2xl mb-3 text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-1"> <h3
class="font-bold text-xl mb-3 text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-1"
>
{{ list[0]?.title }} {{ list[0]?.title }}
</h3> </h3>
<h2 class="text-gray-600 mb-4 line-clamp-1 leading-relaxed text-xl"> <h2 class="text-gray-600 mb-4 line-clamp-1 leading-relaxed text-xl">
...@@ -88,7 +111,11 @@ ...@@ -88,7 +111,11 @@
</h2> </h2>
<div class="flex items-center justify-between text-gray-500 text-base"> <div class="flex items-center justify-between text-gray-500 text-base">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<img :src="list[0]?.showAvatar" alt="" class="w-6 h-6 rounded-full object-cover" /> <img
:src="list[0]?.showAvatar"
alt=""
class="w-6 h-6 rounded-full object-cover"
/>
<span class="font-medium">{{ list[0]?.showName }}</span> <span class="font-medium">{{ list[0]?.showName }}</span>
</div> </div>
<span class="text-base px-2 py-1 rounded-full">{{ <span class="text-base px-2 py-1 rounded-full">{{
...@@ -100,40 +127,54 @@ ...@@ -100,40 +127,54 @@
<!-- 右侧两个视频 --> <!-- 右侧两个视频 -->
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div v-for="(item, index) in list.slice(1, 3)" :key="index" <div
v-for="(item, index) in list.slice(1, 3)"
:key="index"
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"
@click="openDetailPage(`/videoDetail/${item?.id}`)"> @click="jumpToArticleDetailPage({ type: ArticleTypeEnum.VIDEO, id: item?.id })"
>
<div class="relative overflow-hidden"> <div class="relative overflow-hidden">
<img :src="item?.faceUrl" <img
class="w-full h-44 object-cover group-hover:scale-105 transition-transform duration-500" /> :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> <div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent"></div>
<div v-if="item?.isRecommend" <div
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"> v-if="item?.isRecommend"
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"
>
<img class="w-6" src="@/assets/img/culture/recommend.png" /> <img class="w-6" src="@/assets/img/culture/recommend.png" />
<div class="text-12px text-#000 line-height-12px">推荐</div> <div class="text-12px text-#000 line-height-12px">推荐</div>
</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"
>
{{ item?.videoDuration }} {{ item?.videoDuration }}
</div> </div>
<!-- 数据 --> <!-- 数据 -->
<div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs"> <div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs">
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<View /> <View />
</el-icon> </el-icon>
<span>{{ item?.viewCount }}</span> <span>{{ item?.viewCount }}</span>
</div> </div>
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<ChatDotRound /> <ChatDotRound />
</el-icon> </el-icon>
<span>{{ item?.replyCount }}</span> <span>{{ item?.replyCount }}</span>
</div> </div>
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<Star /> <Star />
</el-icon> </el-icon>
...@@ -153,7 +194,9 @@ ...@@ -153,7 +194,9 @@
</div> </div>
<div class="p-4"> <div class="p-4">
<h3 class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"> <h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
>
{{ item?.title }} {{ item?.title }}
</h3> </h3>
<div class="flex items-center justify-between text-gray-500 text-xs"> <div class="flex items-center justify-between text-gray-500 text-xs">
...@@ -174,11 +217,17 @@ ...@@ -174,11 +217,17 @@
<!-- 剩余视频 - 标准网格 --> <!-- 剩余视频 - 标准网格 -->
<div v-show="list.length > 3" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div v-show="list.length > 3" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div @click="openDetailPage(`/videoDetail/${item.id}`)" v-for="item in list.slice(3)" :key="item.id" <div
class="group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer"> @click="jumpToArticleDetailPage({ type: ArticleTypeEnum.VIDEO, id: item.id })"
v-for="item in list.slice(3)"
: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"> <div class="relative overflow-hidden">
<img :src="item.faceUrl" <img
class="w-full h-44 object-cover group-hover:scale-105 transition-transform duration-500" /> :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> <div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent"></div>
<!-- 标签 --> <!-- 标签 -->
...@@ -187,33 +236,42 @@ ...@@ -187,33 +236,42 @@
> >
{{ item.tagNameList[0] }} {{ item.tagNameList[0] }}
</div> --> </div> -->
<div v-if="item.isRecommend" <div
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"> v-if="item.isRecommend"
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"
>
<img class="w-6" src="@/assets/img/culture/recommend.png" alt="" /> <img class="w-6" src="@/assets/img/culture/recommend.png" alt="" />
<div class="text-12px text-#000 line-height-12px">推荐</div> <div class="text-12px text-#000 line-height-12px">推荐</div>
</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"
>
{{ item.videoDuration }} {{ item.videoDuration }}
</div> </div>
<!-- 数据 --> <!-- 数据 -->
<div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs"> <div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs">
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<View /> <View />
</el-icon> </el-icon>
<span>{{ item.viewCount }}</span> <span>{{ item.viewCount }}</span>
</div> </div>
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<ChatDotRound /> <ChatDotRound />
</el-icon> </el-icon>
<span>{{ item.replyCount }}</span> <span>{{ item.replyCount }}</span>
</div> </div>
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<Star /> <Star />
</el-icon> </el-icon>
...@@ -233,7 +291,9 @@ ...@@ -233,7 +291,9 @@
</div> </div>
<div class="p-4"> <div class="p-4">
<h3 class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"> <h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
>
{{ item?.title }} {{ item?.title }}
</h3> </h3>
<div class="flex items-center justify-between text-gray-500 text-xs"> <div class="flex items-center justify-between text-gray-500 text-xs">
...@@ -253,11 +313,17 @@ ...@@ -253,11 +313,17 @@
<!-- 其他页面 - 标准3列网格 --> <!-- 其他页面 - 标准3列网格 -->
<div v-show="searchParams.current !== 1"> <div v-show="searchParams.current !== 1">
<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 @click="openDetailPage(`/videoDetail/${item.id}`)" v-for="item in list" :key="item.id" <div
class="group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer"> @click="jumpToArticleDetailPage({ type: ArticleTypeEnum.VIDEO, id: 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"> <div class="relative overflow-hidden">
<img :src="item.faceUrl" <img
class="w-full h-44 object-cover group-hover:scale-105 transition-transform duration-500" /> :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> <div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent"></div>
<!-- 标签 --> <!-- 标签 -->
...@@ -266,33 +332,42 @@ ...@@ -266,33 +332,42 @@
> >
{{ item.tagNameList[0] }} {{ item.tagNameList[0] }}
</div> --> </div> -->
<div v-if="item.isRecommend" <div
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"> v-if="item.isRecommend"
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"
>
<img class="w-6" src="@/assets/img/culture/recommend.png" alt="" /> <img class="w-6" src="@/assets/img/culture/recommend.png" alt="" />
<div class="text-12px text-#000 line-height-12px">推荐</div> <div class="text-12px text-#000 line-height-12px">推荐</div>
</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"
>
{{ item?.videoDuration }} {{ item?.videoDuration }}
</div> </div>
<!-- 数据 --> <!-- 数据 -->
<div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs"> <div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs">
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<View /> <View />
</el-icon> </el-icon>
<span>{{ item.viewCount }}</span> <span>{{ item.viewCount }}</span>
</div> </div>
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<ChatDotRound /> <ChatDotRound />
</el-icon> </el-icon>
<span>{{ item.replyCount }}</span> <span>{{ item.replyCount }}</span>
</div> </div>
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"> <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"> <el-icon class="text-sm">
<Star /> <Star />
</el-icon> </el-icon>
...@@ -312,7 +387,9 @@ ...@@ -312,7 +387,9 @@
</div> </div>
<div class="p-4"> <div class="p-4">
<h3 class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"> <h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
>
{{ item.title }} {{ item.title }}
</h3> </h3>
<div class="flex items-center justify-between text-gray-500 text-xs"> <div class="flex items-center justify-between text-gray-500 text-xs">
...@@ -340,14 +417,24 @@ ...@@ -340,14 +417,24 @@
<!-- 右侧:分页器 --> <!-- 右侧:分页器 -->
<div class="right"> <div class="right">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"> <div
<el-pagination v-model:current-page="searchParams.current" v-model:page-size="searchParams.size" class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
:page-sizes="[9, 24, 36, 48]" layout="prev, pager, next, jumper, total" :total="total" >
class="custom-pagination" @current-change=" <el-pagination
(e) => { v-model:current-page="searchParams.current"
; (handleBackTop(), goToPage(e)) v-model:page-size="searchParams.size"
:page-sizes="[9, 24, 36, 48]"
layout="prev, pager, next, jumper, total"
:total="total"
class="custom-pagination"
@current-change="
async (e) => {
await goToPage(e)
handleBackTop()
} }
" @size-change="changePageSize" /> "
@size-change="changePageSize"
/>
</div> </div>
</div> </div>
</div> </div>
...@@ -368,6 +455,7 @@ import { ArticleTypeEnum, TABS_REF_KEY } from '@/constants' ...@@ -368,6 +455,7 @@ import { ArticleTypeEnum, TABS_REF_KEY } from '@/constants'
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import { getArticleList } from '@/api' import { getArticleList } from '@/api'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { jumpToArticleDetailPage } from '@/utils'
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
...@@ -391,10 +479,6 @@ const toggleTab = (sortLogic: number) => { ...@@ -391,10 +479,6 @@ const toggleTab = (sortLogic: number) => {
refresh() refresh()
} }
const openDetailPage = (path: string) => {
window.open(path)
}
defineExpose({ defineExpose({
refresh: () => { refresh: () => {
refresh() refresh()
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
v-for="i in item.yaColumnVoList" v-for="i in item.yaColumnVoList"
:key="i.articleId" :key="i.articleId"
class="group cursor-pointer" class="group cursor-pointer"
@click="openArticleDetail(i.articleId)" @click="jumpToArticleDetailPage({ type: i.type, id: i.articleId })"
> >
<div class="relative mb-3 overflow-hidden rounded-lg"> <div class="relative mb-3 overflow-hidden rounded-lg">
<img <img
...@@ -133,9 +133,10 @@ ...@@ -133,9 +133,10 @@
import { View, ChatDotRound, Star } from '@element-plus/icons-vue' import { View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { getColumnList } from '@/api' import { getColumnList } from '@/api'
import { usePageSearch, useScrollTop } from '@/hooks' import { usePageSearch, useScrollTop } from '@/hooks'
import { ArticleTypeEnum, TABS_REF_KEY } from '@/constants' import { TABS_REF_KEY } from '@/constants'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { jumpToArticleDetailPage } from '@/utils'
const router = useRouter() const router = useRouter()
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!) const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
...@@ -153,10 +154,6 @@ defineExpose({ ...@@ -153,10 +154,6 @@ defineExpose({
refresh() refresh()
}, },
}) })
const openArticleDetail = (id: number) => {
window.open(`/articleDetail/${id}`)
}
</script> </script>
<style scoped></style> <style scoped></style>
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
v-for="i in item.yaColumnVoList" v-for="i in item.yaColumnVoList"
:key="i.articleId" :key="i.articleId"
class="group cursor-pointer" class="group cursor-pointer"
@click="openArticleDetail(i.articleId)" @click="jumpToArticleDetailPage({ type: i.type, id: i.articleId })"
> >
<div class="relative mb-3 overflow-hidden rounded-lg"> <div class="relative mb-3 overflow-hidden rounded-lg">
<img <img
...@@ -107,8 +107,9 @@ ...@@ -107,8 +107,9 @@
:total="total" :total="total"
class="custom-pagination" class="custom-pagination"
@current-change=" @current-change="
(e) => { async (e) => {
;(handleBackTop(), goToPage(e)) await goToPage(e)
handleBackTop()
} }
" "
@size-change="changePageSize" @size-change="changePageSize"
...@@ -134,6 +135,7 @@ import { usePageSearch, useScrollTop } from '@/hooks' ...@@ -134,6 +135,7 @@ import { usePageSearch, useScrollTop } from '@/hooks'
import { TABS_REF_KEY, ArticleTypeEnum } from '@/constants' import { TABS_REF_KEY, ArticleTypeEnum } from '@/constants'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { jumpToArticleDetailPage } from '@/utils'
const router = useRouter() const router = useRouter()
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
...@@ -151,10 +153,6 @@ defineExpose({ ...@@ -151,10 +153,6 @@ defineExpose({
refresh() refresh()
}, },
}) })
const openArticleDetail = (id: number) => {
window.open(`/articleDetail/${id}`)
}
</script> </script>
<style scoped></style> <style scoped></style>
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
</div> </div>
<div class="divide-y bg-#fff"> <div class="divide-y bg-#fff">
<div <div
@click="openArticleDetail(item.id)" @click="jumpToArticleDetailPage({ type: ArticleTypeEnum.PRACTICE, id: item.id })"
v-for="item in list" v-for="item in list"
:key="item.id" :key="item.id"
class="p-4 hover:bg-gray-50 transition-colors cursor-pointer pl-8" class="p-4 hover:bg-gray-50 transition-colors cursor-pointer pl-8"
...@@ -141,8 +141,9 @@ ...@@ -141,8 +141,9 @@
:total="total" :total="total"
class="custom-pagination" class="custom-pagination"
@current-change=" @current-change="
(e) => { async (e) => {
;(handleBackTop(), goToPage(e)) await goToPage(e)
handleBackTop()
} }
" "
@size-change="changePageSize" @size-change="changePageSize"
...@@ -164,6 +165,7 @@ import { TABS_REF_KEY } from '@/constants' ...@@ -164,6 +165,7 @@ import { TABS_REF_KEY } from '@/constants'
import { useTagsStore } from '@/stores/tags' import { useTagsStore } from '@/stores/tags'
import PublishPractice from '@/components/common/PublishBox/index.vue' import PublishPractice from '@/components/common/PublishBox/index.vue'
import { ArticleTypeEnum } from '@/constants' import { ArticleTypeEnum } from '@/constants'
import { jumpToArticleDetailPage } from '@/utils'
const tagsStore = useTagsStore() const tagsStore = useTagsStore()
const { tagList } = storeToRefs(tagsStore) const { tagList } = storeToRefs(tagsStore)
...@@ -215,10 +217,6 @@ defineExpose({ ...@@ -215,10 +217,6 @@ defineExpose({
refresh() refresh()
}, },
}) })
const openArticleDetail = (id: number) => {
window.open(`/articleDetail/${id}`)
}
</script> </script>
<style scoped></style> <style scoped></style>
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
</div> </div>
<div v-loading="loadingMore" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-6"> <div v-loading="loadingMore" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
<div <div
@click="handleOpenPage(item.articleId) " @click="jumpToArticleDetailPage({ type: ArticleTypeEnum.VIDEO, id: item.articleId })"
v-for="item in listMore" v-for="item in listMore"
:key="item.articleId" :key="item.articleId"
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"
...@@ -79,16 +79,6 @@ ...@@ -79,16 +79,6 @@
<span>{{ item.replyCount }}</span> <span>{{ item.replyCount }}</span>
</div> </div>
</div> </div>
<!-- 播放按钮 -->
<!-- <div
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
>
<div
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"
>
<el-icon size="50" color="#333"><VideoPlay /></el-icon>
</div>
</div> -->
</div> </div>
<div class="p-4"> <div class="p-4">
...@@ -145,10 +135,77 @@ ...@@ -145,10 +135,77 @@
<div <div
v-for="i in item.yaColumnVoList" v-for="i in item.yaColumnVoList"
:key="i.articleId" :key="i.articleId"
class="group cursor-pointer" class="group cursor-pointer rounded-lg overflow-hidden"
@click="handleOpenPage(i.articleId)" @click="jumpToArticleDetailPage({ type: i.type, id: i.articleId })"
>
<div class="relative overflow-hidden">
<img
:src="i.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>
<!-- 标签 -->
<!-- <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.tagNameList[0] }}
</div> -->
<div
v-if="i.isRecommend"
class="absolute top-0 left-0 w-15 h-7 z-1000 bg-#FFF9B9 flex items-center justify-center border-2px border-solid border-#f4f0eb rounded-tl-lg rounded-br-lg"
>
<img class="w-6" src="@/assets/img/culture/recommend.png" alt="" />
<div class="text-12px text-#000 line-height-12px">推荐</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"
>
{{ i.videoDuration }}
</div>
<!-- 数据 -->
<div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs">
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
> >
<div class="relative mb-3 overflow-hidden rounded-lg"> <el-icon class="text-sm"><View /></el-icon>
<span>{{ i.viewCount }}</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"><ChatDotRound /></el-icon>
<span>{{ i.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>{{ i.replyCount }}</span>
</div>
</div>
</div>
<div class="p-4">
<h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
>
{{ i.title }}
</h3>
<div class="flex items-center justify-between text-gray-500 text-xs">
<div class="flex items-center gap-2 max-w-55%">
<img :src="i.showAvatar" alt="" class="w-6 h-6 rounded-full object-cover" />
<el-tooltip :content="i.showName" placement="top">
<span class="font-medium">{{ i.showName }}</span>
</el-tooltip>
</div>
<span>{{ dayjs(i.createTime * 1000).format('YYYY-MM-DD HH:mm') }}</span>
</div>
</div>
<!-- <div class="relative mb-3 overflow-hidden rounded-lg">
<img <img
:src="i.faceUrl" :src="i.faceUrl"
class="w-full aspect-[5/3] object-cover group-hover:scale-105 transition-transform duration-300" class="w-full aspect-[5/3] object-cover group-hover:scale-105 transition-transform duration-300"
...@@ -189,7 +246,7 @@ ...@@ -189,7 +246,7 @@
</span> </span>
</div> </div>
<span>{{ dayjs(i.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span> <span>{{ dayjs(i.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div> </div> -->
</div> </div>
</div> </div>
<div v-else class="flex items-center justify-center h-48"> <div v-else class="flex items-center justify-center h-48">
...@@ -218,8 +275,9 @@ ...@@ -218,8 +275,9 @@
:total="total" :total="total"
class="custom-pagination" class="custom-pagination"
@current-change=" @current-change="
(e) => { async (e) => {
;(handleBackTop(), goToPage(e)) await goToPage(e)
handleBackTop()
} }
" "
@size-change="changePageSize" @size-change="changePageSize"
...@@ -242,9 +300,10 @@ ...@@ -242,9 +300,10 @@
import { View, ChatDotRound, Star } from '@element-plus/icons-vue' import { View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { getVideoList, getVideoListViewMore } from '@/api' import { getVideoList, getVideoListViewMore } from '@/api'
import { usePageSearch, useScrollTop } from '@/hooks' import { usePageSearch, useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants' import { TABS_REF_KEY, ArticleTypeEnum } from '@/constants'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { jumpToArticleDetailPage } from '@/utils'
const router = useRouter() const router = useRouter()
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
...@@ -283,10 +342,6 @@ const changeSort = (sortLogic: number) => { ...@@ -283,10 +342,6 @@ const changeSort = (sortLogic: number) => {
refreshMore() refreshMore()
} }
const handleOpenPage = (articleId: number) => {
window.open(`/videoDetail/${articleId}`)
}
defineExpose({ defineExpose({
refresh: () => { refresh: () => {
// searchParams.value.current = 0 // searchParams.value.current = 0
......
<template> <template>
<div class="min-h-screen bg-[#fff] p-6 font-sans"> <div class="min-h-screen bg-[#fff] font-sans">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<!-- 顶部面包屑或标题(可选) --> <!-- 顶部面包屑或标题(可选) -->
<el-form :model="form" label-position="top" class="grid grid-cols-12 gap-6 items-start"> <el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
class="grid grid-cols-12 gap-6 items-start"
>
<!-- 左侧:沉浸式创作区 (占 9 列) --> <!-- 左侧:沉浸式创作区 (占 9 列) -->
<div class="col-span-12 lg:col-span-9 space-y-6"> <div class="col-span-12 lg:col-span-9 space-y-6">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-8 min-h-[80vh]"> <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-8 min-h-[80vh]">
<!-- 标题输入:模拟大标题风格,去掉边框 --> <!-- 标题输入:模拟大标题风格,去掉边框 -->
<el-form-item prop="title" class="mb-6 !border-b !border-gray-100 pb-2"> <el-form-item prop="title" class="!border-b !border-gray-100">
<el-input <el-input
v-model="form.title" v-model="form.title"
placeholder="请输入文章标题..." placeholder="请输入文章标题..."
class="title-input" class="title-input"
:maxlength="100"
show-word-limit show-word-limit
type="textarea" type="textarea"
:autosize="{ minRows: 1, maxRows: 2 }"
resize="none" resize="none"
/> />
</el-form-item> </el-form-item>
<!-- 富文本编辑器 --> <!-- 富文本编辑器 -->
<div class="editor-container"> <div class="editor-container">
<WangEditor v-model="form.content" style="height: 600px" /> <el-form-item prop="content" class="!border-b !border-gray-100">
<WangEditor v-model="form.content" style="height: 800px" />
</el-form-item>
</div> </div>
</div> </div>
</div> </div>
<!-- 右侧:配置侧边栏 (占 3 列,吸顶) --> <!-- 右侧:配置侧边栏 (占 3 列,吸顶) -->
<div class="col-span-12 lg:col-span-3 space-y-4 sticky top-4"> <div class="col-span-12 lg:col-span-3 space-y-4">
<!-- 卡片1:基础设置 --> <!-- 卡片1:基础设置 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-5"> <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-5">
<div class="font-bold text-gray-800 mb-4 flex items-center gap-2"> <div class="font-bold text-gray-800 mb-4 flex items-center gap-2">
...@@ -39,10 +45,14 @@ ...@@ -39,10 +45,14 @@
<!-- 文章类型 --> <!-- 文章类型 -->
<el-form-item label="文章类型" prop="type"> <el-form-item label="文章类型" prop="type">
<el-radio-group v-model="form.type" class="w-full grid grid-cols-3 gap-2"> <el-radio-group
<el-radio-button :value="ArticleTypeEnum.POST">帖子</el-radio-button> v-model="form.type"
<el-radio-button :value="ArticleTypeEnum.COLUMN">专栏</el-radio-button> class="w-full grid grid-cols-3 gap-2"
<el-radio-button :value="ArticleTypeEnum.INTERVIEW">专访</el-radio-button> fill="#3b82f6"
>
<el-radio-button :value="ArticleTypeEnum.POST">{{
articleTypeListOptions.find((item) => item.value === type)?.label
}}</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
...@@ -170,7 +180,15 @@ ...@@ -170,7 +180,15 @@
</div> </div>
<div class="mb-6 flex items-center justify-end w-full"> <div class="mb-6 flex items-center justify-end w-full">
<div class="flex gap-3"> <div class="flex gap-3">
<el-button type="primary" round class="!px-8 w-full!" @click="handlePublish"> <el-button class="rounded-lg" @click="handleClosed">取消</el-button>
<el-button class="rounded-lg" @click="handleSubmit(ReleaseStatusTypeEnum.DRAFT)">
存草稿
</el-button>
<el-button
type="primary"
@click="handleSubmit(ReleaseStatusTypeEnum.PUBLISH)"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
>
发布 发布
</el-button> </el-button>
</div> </div>
...@@ -182,8 +200,15 @@ ...@@ -182,8 +200,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from 'element-plus'
import WangEditor from '@/components/common/WangEditor/index.vue' import WangEditor from '@/components/common/WangEditor/index.vue'
import { ArticleTypeEnum, SendTypeEnum, BooleanFlag, ReleaseStatusTypeEnum } from '@/constants' import {
ArticleTypeEnum,
SendTypeEnum,
BooleanFlag,
ReleaseStatusTypeEnum,
articleTypeListOptions,
} from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue' import UploadFile from '@/components/common/UploadFile/index.vue'
import { useResetData } from '@/hooks' import { useResetData } from '@/hooks'
import { useColumnStore } from '@/stores/column' import { useColumnStore } from '@/stores/column'
...@@ -192,9 +217,13 @@ import { addOrUpdateArticle } from '@/api' ...@@ -192,9 +217,13 @@ import { addOrUpdateArticle } from '@/api'
// ... (逻辑部分保持不变,直接复用您的即可) // ... (逻辑部分保持不变,直接复用您的即可)
const columnStore = useColumnStore() const columnStore = useColumnStore()
const { columnList } = storeToRefs(columnStore) const { columnList } = storeToRefs(columnStore)
const router = useRouter()
const route = useRoute()
const type = route.params.type as ArticleTypeEnum
const formRef = useTemplateRef<FormInstance>('formRef')
const [form, resetForm] = useResetData({ const [form, resetForm] = useResetData({
articleType: ArticleTypeEnum.POST, type: type,
title: '', title: '',
content: '', content: '',
faceUrl: '', faceUrl: '',
...@@ -208,13 +237,28 @@ const [form, resetForm] = useResetData({ ...@@ -208,13 +237,28 @@ const [form, resetForm] = useResetData({
releaseStatus: ReleaseStatusTypeEnum.PUBLISH, releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
}) })
const rules = {
title: [{ required: true, message: '请输入文章标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入文章内容', trigger: 'blur' }],
type: [{ required: true, message: '请选择文章类型', trigger: 'blur' }],
faceUrl: [{ required: true, message: '请上传封面图', trigger: 'blur' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [{ required: true, message: '请选择发布时间', trigger: 'blur' }],
releaseStatus: [{ required: true, message: '请选择发布状态', trigger: 'blur' }],
}
const filterTagsFn = (allTags: any[]) => { const filterTagsFn = (allTags: any[]) => {
return allTags.filter((tag) => tag.id !== Number(form.value.mainTagId)) return allTags.filter((tag) => tag.id !== Number(form.value.mainTagId))
} }
const handlePublish = async () => { const handleSubmit = async () => {
try {
await formRef.value.validate()
const res = await addOrUpdateArticle(form.value) const res = await addOrUpdateArticle(form.value)
console.log(res) console.log(res)
} catch (error) {
console.log(error)
}
} }
</script> </script>
......
<template> <template>
<div class="min-h-screen pb-10 font-sans text-slate-800 flex justify-center"> <div class="min-h-screen pb-10 font-sans text-slate-800 flex justify-center px-20">
<div class="w-full max-w-[1000px] space-y-4"> <div class="w-full space-y-4">
<!-- 1. 问题卡片 --> <!-- 1. 问题卡片 -->
<div <div
class="bg-white rounded-lg p-6 shadow-[0_1px_3px_rgba(0,0,0,0.02)] border border-slate-100" class="bg-white rounded-lg p-6 shadow-[0_1px_3px_rgba(0,0,0,0.02)] border border-slate-100"
...@@ -22,17 +22,17 @@ ...@@ -22,17 +22,17 @@
</h1> </h1>
<!-- 描述:次要信息,深灰 --> <!-- 描述:次要信息,深灰 -->
<div class="text-slate-700 leading-relaxed text-[15px] mb-6"> <div class="text-gray-600 text-base leading-relaxed transition-all duration-300">
<span :class="{ 'line-clamp-3': !isExpandDesc }"> <div :class="{ 'line-clamp-3': !isExpand }" ref="questionContentRef">
{{ questionDetail.content }} {{ questionDetail.content }}
</span> </div>
<button <button
v-if="questionDetail.content && questionDetail.content.length > 100" v-if="isOverThreeLine"
class="text-blue-500 text-sm font-medium hover:text-blue-600 mt-1 flex items-center gap-0.5" class="text-blue-500 text-sm font-medium hover:text-blue-600 mt-1 flex items-center gap-0.5 cursor-pointer"
@click="isExpandDesc = !isExpandDesc" @click="isExpand = !isExpand"
> >
{{ isExpandDesc ? '收起' : '显示全部' }} {{ isExpand ? '收起' : '显示全部' }}
<el-icon :class="{ 'rotate-180': isExpandDesc }" class="transition-transform" <el-icon :class="{ 'rotate-180': isExpand }" class="transition-transform"
><CaretBottom ><CaretBottom
/></el-icon> /></el-icon>
</button> </button>
...@@ -42,7 +42,8 @@ ...@@ -42,7 +42,8 @@
<div class="flex items-center justify-between mt-4"> <div class="flex items-center justify-between mt-4">
<div class="flex gap-3"> <div class="flex gap-3">
<button <button
class="px-5 py-1.5 border border-blue-500 text-blue-500 hover:bg-blue-50 rounded-[4px] text-sm font-medium transition-colors flex items-center gap-1 cursor-pointer" class="px-5 py-1.5 border !bg-blue-500 !text-white rounded-[4px] text-sm font-medium transition-colors flex items-center gap-1 cursor-pointer"
@click="openCommentDialog"
> >
<el-icon><EditPen /></el-icon> <el-icon><EditPen /></el-icon>
写回答 写回答
...@@ -81,7 +82,7 @@ ...@@ -81,7 +82,7 @@
<!-- 2. 列表控制栏 --> <!-- 2. 列表控制栏 -->
<div <div
class="bg-white rounded-t-lg border-b border-slate-100 p-6 flex justify-between items-center shadow-sm" class="bg-white rounded-lg border-b border-slate-100 p-6 flex justify-between items-center shadow-sm"
> >
<h3 class="font-bold text-slate-800 text-base">{{ total }} 个回答</h3> <h3 class="font-bold text-slate-800 text-base">{{ total }} 个回答</h3>
<div class="flex text-sm text-slate-500 bg-slate-50 rounded p-0.5"> <div class="flex text-sm text-slate-500 bg-slate-50 rounded p-0.5">
...@@ -89,6 +90,7 @@ ...@@ -89,6 +90,7 @@
size="small" size="small"
v-model="searchParams.sortType" v-model="searchParams.sortType"
@change="(val) => changeSortType(val as number)" @change="(val) => changeSortType(val as number)"
fill="#3b82f6"
> >
<el-radio-button label="最新" :value="2" /> <el-radio-button label="最新" :value="2" />
<el-radio-button label="最多评论" :value="1" /> <el-radio-button label="最多评论" :value="1" />
...@@ -135,20 +137,20 @@ ...@@ -135,20 +137,20 @@
>优秀回答</span >优秀回答</span
> --> > -->
</div> </div>
<div class="text-xs text-slate-400 mt-0.5 max-w-md truncate"> <!-- <div class="text-xs text-slate-400 mt-0.5 max-w-md truncate">
{{ answer.description || '暂无简介' }} {{ answer.description || '暂无简介' }}
</div> </div> -->
</div> </div>
</div> </div>
<!-- 赞同票数 (微小的灰色文字,增加信息密度) --> <!-- 赞同票数 (微小的灰色文字,增加信息密度) -->
<div class="text-xs text-slate-400 mb-2"> <div class="text-xs text-slate-400 mb-2">
{{ answer.praiseCount || 0 }} 人赞同了该回答 {{ answer.postPriseCount || 0 }} 人赞同了该回答
</div> </div>
<!-- 正文 --> <!-- 正文 换行 -->
<div <div
class="text-slate-800 leading-7 text-[15px] mb-4 rich-text-content" class="text-slate-800 leading-7 text-[15px] mb-4 rich-text-content break-all"
v-html="answer.content" v-html="answer.content"
></div> ></div>
...@@ -167,7 +169,8 @@ ...@@ -167,7 +169,8 @@
> >
<el-icon><CaretTop /></el-icon> <el-icon><CaretTop /></el-icon>
<span <span
>{{ answer.hasPraise ? '已赞同' : '赞同' }} {{ answer.praiseCount || '' }}</span >{{ answer.hasPraise ? '已赞同' : '赞同' }}
{{ answer.postPriseCount || '' }}</span
> >
</button> </button>
<!-- <button <!-- <button
...@@ -182,7 +185,7 @@ ...@@ -182,7 +185,7 @@
@click="handleComment(answer, index)" @click="handleComment(answer, index)"
> >
<el-icon class="text-base"><ChatRound /></el-icon> <el-icon class="text-base"><ChatRound /></el-icon>
<span :class="{ 'text-slate-800 font-medium': answer.showComment }"> <span>
{{ answer.childrenNum ? `${answer.childrenNum} 条评论` : '添加评论' }} {{ answer.childrenNum ? `${answer.childrenNum} 条评论` : '添加评论' }}
</span> </span>
</button> </button>
...@@ -204,7 +207,6 @@ ...@@ -204,7 +207,6 @@
:immediate="false" :immediate="false"
:isQuestion="true" :isQuestion="true"
:commentId="answer.id" :commentId="answer.id"
@commentSuccess="() => handleCommentSuccess(answer)"
/> />
</div> </div>
</Transition> </Transition>
...@@ -212,18 +214,18 @@ ...@@ -212,18 +214,18 @@
</div> </div>
<!-- 底部加载更多 --> <!-- 底部加载更多 -->
<div class="py-3 flex justify-center bg-#fff"> <div class="py-3 px-6 flex justify-end bg-#fff rounded-lg">
<el-pagination <el-pagination
v-model:current-page="searchParams.current" v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size" v-model:page-size="searchParams.size"
:total="total" :total="total"
layout="prev, pager, next,total" layout="prev, pager, next,total"
background
small small
@current-change="goToPage" @current-change="goToPage"
/> />
</div> </div>
</div> </div>
<CommentDialog ref="commentDialogRef" @commentSuccess="refresh" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -234,9 +236,10 @@ import { ...@@ -234,9 +236,10 @@ import {
addOrCanceArticlelCollect, addOrCanceArticlelCollect,
addOrCancelCommentLike, addOrCancelCommentLike,
} from '@/api' } from '@/api'
import type { ArticleItemDto } from '@/api/types' import type { ArticleItemDto } from '@/api'
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import Comment from '@/components/common/Comment/index.vue' import Comment from '@/components/common/Comment/index.vue'
import CommentDialog from '@/components/common/CommentDialog/index.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
const route = useRoute() const route = useRoute()
...@@ -244,11 +247,28 @@ const questionId = Number(route.params.id) ...@@ -244,11 +247,28 @@ const questionId = Number(route.params.id)
const commentRefList = ref<InstanceType<typeof Comment>[]>([]) const commentRefList = ref<InstanceType<typeof Comment>[]>([])
const questionDetail = ref<ArticleItemDto>({} as ArticleItemDto) const questionDetail = ref<ArticleItemDto>({} as ArticleItemDto)
const commentDialogRef = useTemplateRef<typeof CommentDialog>('commentDialogRef')
const questionContentRef = useTemplateRef<HTMLElement>('questionContentRef')
const isExpand = ref(false)
// 检测当前是否超过三行 要用到具体的dom
const isOverThreeLine = ref(false)
const checkIsOverThreeLine = () => {
if (!questionContentRef.value || !questionDetail.value.content) return false
const lineHeight = parseFloat(getComputedStyle(questionContentRef.value).lineHeight)
const height = questionContentRef.value!.scrollHeight
const maxHeight = lineHeight * 3
console.log(maxHeight, height)
return height > maxHeight
}
const getQuestionDetail = async () => { const getQuestionDetail = async () => {
const { data } = await getArticleDetail(questionId) const { data } = await getArticleDetail(questionId)
questionDetail.value = data questionDetail.value = data
console.log(questionDetail.value) console.log(questionDetail.value)
await nextTick()
isOverThreeLine.value = checkIsOverThreeLine()
} }
const { list, total, searchParams, goToPage, refresh } = usePageSearch(getCommentList, { const { list, total, searchParams, goToPage, refresh } = usePageSearch(getCommentList, {
...@@ -276,7 +296,7 @@ const handleLikeArticle = async () => { ...@@ -276,7 +296,7 @@ const handleLikeArticle = async () => {
questionDetail.value.praiseCount = questionDetail.value.hasPraised questionDetail.value.praiseCount = questionDetail.value.hasPraised
? questionDetail.value.praiseCount + 1 ? questionDetail.value.praiseCount + 1
: questionDetail.value.praiseCount - 1 : questionDetail.value.praiseCount - 1
ElMessage.success(`${questionDetail.value.hasPraised ? '点赞成功' : '取消点赞成功'}`) ElMessage.success(`${questionDetail.value.hasPraised ? '点赞该问题' : '取消点赞该问题'}`)
} }
const handleCollectArticle = async () => { const handleCollectArticle = async () => {
...@@ -290,9 +310,9 @@ const handleCollectArticle = async () => { ...@@ -290,9 +310,9 @@ const handleCollectArticle = async () => {
const handleLikeAnswer = async (answer: any) => { const handleLikeAnswer = async (answer: any) => {
await addOrCancelCommentLike(answer.id) await addOrCancelCommentLike(answer.id)
ElMessage.success(`${answer.hasPraise ? '点赞成功' : '取消点赞成功'}`)
answer.hasPraise = !answer.hasPraise answer.hasPraise = !answer.hasPraise
answer.praiseCount = answer.hasPraise ? answer.praiseCount + 1 : answer.praiseCount - 1 answer.postPriseCount = answer.hasPraise ? answer.postPriseCount + 1 : answer.postPriseCount - 1
ElMessage.success(`${answer.hasPraise ? '点赞该回答' : '取消点赞该回答'}`)
} }
const handleComment = (answer: any, index: number) => { const handleComment = (answer: any, index: number) => {
...@@ -300,6 +320,10 @@ const handleComment = (answer: any, index: number) => { ...@@ -300,6 +320,10 @@ const handleComment = (answer: any, index: number) => {
answer.showComment = !answer.showComment answer.showComment = !answer.showComment
} }
const openCommentDialog = () => {
commentDialogRef.value?.open(questionDetail.value.id)
}
onMounted(() => { onMounted(() => {
getQuestionDetail() getQuestionDetail()
}) })
......
...@@ -48,7 +48,11 @@ ...@@ -48,7 +48,11 @@
> >
{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }} {{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}
<span class="flex items-center px-2"> 举报帖子 </span> <span class="flex items-center px-2"> 举报帖子 </span>
<el-link type="primary" :underline="false" @click="handleToDetail(item)"> <el-link
type="primary"
:underline="false"
@click="jumpToArticleDetailPage({ type: item.type, id: item.articleId })"
>
{{ item.title }} {{ item.title }}
</el-link> </el-link>
</span> </span>
...@@ -118,9 +122,10 @@ import { getComplaintList, auditComplaint } from '@/api' ...@@ -118,9 +122,10 @@ import { getComplaintList, auditComplaint } from '@/api'
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import { auditTypeListOptions } from '@/constants/options' import { auditTypeListOptions } from '@/constants/options'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { AuditStatusEnum, ArticleTypeEnum } from '@/constants' import { AuditStatusEnum } from '@/constants'
import type { AuditComplaintDto, ComplaintListItemDto } from '@/api' import type { AuditComplaintDto } from '@/api'
import type { TabPaneName } from 'element-plus' import type { TabPaneName } from 'element-plus'
import { jumpToArticleDetailPage } from '@/utils'
const toggleTab = (key: TabPaneName) => { const toggleTab = (key: TabPaneName) => {
searchParams.value.status = key as AuditStatusEnum searchParams.value.status = key as AuditStatusEnum
...@@ -154,12 +159,4 @@ const handleAudit = async (data: AuditComplaintDto) => { ...@@ -154,12 +159,4 @@ const handleAudit = async (data: AuditComplaintDto) => {
refresh() refresh()
} }
const handleToDetail = (item: ComplaintListItemDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
window.open(`/videoDetail/${item.articleId}`)
} else {
window.open(`/articleDetail/${item.articleId}`)
}
}
</script> </script>
...@@ -36,7 +36,12 @@ ...@@ -36,7 +36,12 @@
</div> </div>
<div class="flex items-center text-gray-400 text-sm ml-4"> <div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link @click="openArticleDetail(item.id)"> 去回复 </el-button> <el-button
type="primary"
link
@click="jumpToArticleDetailPage({ type: 'question', id: item.id })"
>去回复</el-button
>
</div> </div>
</div> </div>
</div> </div>
...@@ -67,6 +72,7 @@ import { Document, Refresh } from '@element-plus/icons-vue' ...@@ -67,6 +72,7 @@ import { Document, Refresh } from '@element-plus/icons-vue'
import { answerQuestionPage } from '@/api' import { answerQuestionPage } from '@/api'
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { jumpToArticleDetailPage } from '@/utils'
const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch( const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch(
answerQuestionPage, answerQuestionPage,
...@@ -77,8 +83,4 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize } ...@@ -77,8 +83,4 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
onActivated(() => { onActivated(() => {
refresh() refresh()
}) })
const openArticleDetail = (id: number) => {
window.open(`/articleDetail/${id}`)
}
</script> </script>
...@@ -47,7 +47,12 @@ ...@@ -47,7 +47,12 @@
</div> </div>
<div class="flex items-center text-gray-400 text-sm ml-4"> <div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link @click="handleView(item)">查看</el-button> <el-button
type="primary"
link
@click="jumpToArticleDetailPage({ type: item.type, id: item.id })"
>查看</el-button
>
</div> </div>
</div> </div>
</div> </div>
...@@ -85,8 +90,8 @@ import { ...@@ -85,8 +90,8 @@ import {
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ArticleTypeEnum } from '@/constants/enums' import { ArticleTypeEnum } from '@/constants/enums'
import type { TabPaneName } from 'element-plus' import type { TabPaneName } from 'element-plus'
import type { SelfCollectDetailDto } from '@/api'
import { IS_REAL_KEY } from '@/constants/symbolKey' import { IS_REAL_KEY } from '@/constants/symbolKey'
import { jumpToArticleDetailPage } from '@/utils'
const isReal = inject(IS_REAL_KEY) const isReal = inject(IS_REAL_KEY)
...@@ -111,14 +116,6 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize } ...@@ -111,14 +116,6 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
}, },
) )
const handleView = (item: SelfCollectDetailDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
window.open(`/videoDetail/${item.id}`)
} else {
window.open(`/articleDetail/${item.id}`)
}
}
onActivated(() => { onActivated(() => {
searchParams.value.type = filterArticleType.value[0]!.value searchParams.value.type = filterArticleType.value[0]!.value
refresh() refresh()
......
...@@ -61,7 +61,12 @@ ...@@ -61,7 +61,12 @@
<!-- Meta Info --> <!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4"> <div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link @click="handleView(item)">查看</el-button> <el-button
type="primary"
link
@click="jumpToArticleDetailPage({ type: item.type, id: item.articleId })"
>查看</el-button
>
<el-button type="danger" link @click="handleDelete(item.id)">删除</el-button> <el-button type="danger" link @click="handleDelete(item.id)">删除</el-button>
</div> </div>
</div> </div>
...@@ -99,9 +104,9 @@ import { ...@@ -99,9 +104,9 @@ import {
} from '@/constants/options' } from '@/constants/options'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { CommentTypeEnum, ArticleTypeEnum } from '@/constants/enums' import { CommentTypeEnum, ArticleTypeEnum } from '@/constants/enums'
import type { SelfCommentItemDto } from '@/api'
import type { TabPaneName } from 'element-plus' import type { TabPaneName } from 'element-plus'
import { IS_REAL_KEY } from '@/constants/symbolKey' import { IS_REAL_KEY } from '@/constants/symbolKey'
import { jumpToArticleDetailPage } from '@/utils'
const route = useRoute() const route = useRoute()
const isReal = inject(IS_REAL_KEY) const isReal = inject(IS_REAL_KEY)
...@@ -140,14 +145,6 @@ const handleDelete = async (id: number) => { ...@@ -140,14 +145,6 @@ const handleDelete = async (id: number) => {
refresh() refresh()
} }
const handleView = (item: SelfCommentItemDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
window.open(`/videoDetail/${item.articleId}`)
} else {
window.open(`/articleDetail/${item.articleId}`)
}
}
onActivated(() => { onActivated(() => {
if (route.query.type) { if (route.query.type) {
searchParams.value.type = route.query.type as ArticleTypeEnum searchParams.value.type = route.query.type as ArticleTypeEnum
......
...@@ -46,7 +46,12 @@ ...@@ -46,7 +46,12 @@
</div> </div>
<div class="flex items-center text-gray-400 text-sm ml-4"> <div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link @click="handleView(item)">查看</el-button> <el-button
type="primary"
link
@click="jumpToArticleDetailPage({ type: item.type, id: item.id })"
>查看</el-button
>
</div> </div>
</div> </div>
</div> </div>
...@@ -83,8 +88,8 @@ import { ...@@ -83,8 +88,8 @@ import {
} from '@/constants/options' } from '@/constants/options'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ArticleTypeEnum } from '@/constants/enums' import { ArticleTypeEnum } from '@/constants/enums'
import type { SelfPraiseDetailDto } from '@/api'
import { IS_REAL_KEY } from '@/constants/symbolKey' import { IS_REAL_KEY } from '@/constants/symbolKey'
import { jumpToArticleDetailPage } from '@/utils'
const isReal = inject(IS_REAL_KEY) const isReal = inject(IS_REAL_KEY)
...@@ -114,14 +119,6 @@ onActivated(() => { ...@@ -114,14 +119,6 @@ onActivated(() => {
refresh() refresh()
}) })
const handleView = (item: SelfPraiseDetailDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
window.open(`/videoDetail/${item.id}`)
} else {
window.open(`/articleDetail/${item.id}`)
}
}
defineExpose({ defineExpose({
refresh: () => { refresh: () => {
searchParams.value.type = filterArticleType.value[0]!.value searchParams.value.type = filterArticleType.value[0]!.value
......
...@@ -47,7 +47,12 @@ ...@@ -47,7 +47,12 @@
</div> </div>
<div class="flex items-center text-gray-400 text-sm ml-4"> <div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link @click="handleView(item)">查看</el-button> <el-button
type="primary"
link
@click="jumpToArticleDetailPage({ type: item.type, id: item.id })"
>查看</el-button
>
<el-button type="danger" link @click="handleDelete(item.id)">删除</el-button> <el-button type="danger" link @click="handleDelete(item.id)">删除</el-button>
</div> </div>
</div> </div>
...@@ -84,9 +89,9 @@ import { ...@@ -84,9 +89,9 @@ import {
} from '@/constants/options' } from '@/constants/options'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ArticleTypeEnum } from '@/constants/enums' import { ArticleTypeEnum } from '@/constants/enums'
import type { SelfPublishDetailDto } from '@/api'
import type { TabPaneName } from 'element-plus' import type { TabPaneName } from 'element-plus'
import { IS_REAL_KEY } from '@/constants/symbolKey' import { IS_REAL_KEY } from '@/constants/symbolKey'
import { jumpToArticleDetailPage } from '@/utils'
const isReal = inject(IS_REAL_KEY) const isReal = inject(IS_REAL_KEY)
const filterArticleType = computed(() => { const filterArticleType = computed(() => {
...@@ -115,13 +120,6 @@ onActivated(() => { ...@@ -115,13 +120,6 @@ onActivated(() => {
refresh() refresh()
}) })
const handleView = (item: SelfPublishDetailDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
window.open(`/videoDetail/${item.id}`)
} else {
window.open(`/articleDetail/${item.id}`)
}
}
const handleDelete = async (articleId: number) => { const handleDelete = async (articleId: number) => {
await ElMessageBox.confirm('确定删除该吗?', '提示', { await ElMessageBox.confirm('确定删除该吗?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
......
...@@ -28,8 +28,15 @@ ...@@ -28,8 +28,15 @@
@click="handleBackUser" @click="handleBackUser"
>返回个人账号</el-button >返回个人账号</el-button
> >
<!-- v-if="userInfo.isAdmin" 暂时不加权限 --> <!-- 暂时不加权限 -->
<el-button type="primary" plain size="small" @click="handleAdmin">后台管理</el-button> <el-button
v-if="userInfo.isAdmin || userInfo.isOfficialAccount"
type="primary"
plain
size="small"
@click="handleAdmin"
>后台管理</el-button
>
</div> </div>
</div> </div>
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
<!-- 左侧:UP主信息 --> <!-- 左侧:UP主信息 -->
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<img <img
@click="router.push(`/otherUserPage/${videoDetail?.createUserId}/0`)" @click="jumpToUserHomePage({ userId: videoDetail?.createUserId, isReal: 0 })"
:src="videoDetail?.createUserAvatar" :src="videoDetail?.createUserAvatar"
class="w-12 h-12 rounded-full object-cover cursor-pointer hover:opacity-80 transition-opacity ring-2 ring-gray-100" class="w-12 h-12 rounded-full object-cover cursor-pointer hover:opacity-80 transition-opacity ring-2 ring-gray-100"
/> />
...@@ -243,9 +243,9 @@ import type { ArticleItemDto } from '@/api/article/types' ...@@ -243,9 +243,9 @@ import type { ArticleItemDto } from '@/api/article/types'
import Comment from '@/components/common/Comment/index.vue' import Comment from '@/components/common/Comment/index.vue'
import RewardDialog from './components/rewardDialog.vue' import RewardDialog from './components/rewardDialog.vue'
import ActionMore from '@/components/common/ActionMore/index.vue' import ActionMore from '@/components/common/ActionMore/index.vue'
import { jumpToUserHomePage } from '@/utils'
const route = useRoute() const route = useRoute()
const router = useRouter()
const videoId = route.params.id as string const videoId = route.params.id as string
// 视频详情 // 视频详情
......
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