Commit 797a579b by lijiabin

【需求 17679】 feat: 完善首页、视频发布页、搜索页等相关内容

parent 53921419
...@@ -9,6 +9,13 @@ ...@@ -9,6 +9,13 @@
@click="handleClickItem(item)" @click="handleClickItem(item)"
> >
<div class="flex gap-3 justify-between"> <div class="flex gap-3 justify-between">
<!-- <div
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="" />
<div class="text-12px text-#000 line-height-12px">推荐</div>
</div> -->
<!-- 内容区域 --> <!-- 内容区域 -->
<div class="flex-1 min-w-0 flex flex-col justify-between h-24"> <div class="flex-1 min-w-0 flex flex-col justify-between h-24">
<!-- 标题 --> <!-- 标题 -->
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<!-- tabs --> <!-- tabs -->
<div class="shadow-sm"> <div class="shadow-sm">
<div class="max-w-7xl mx-auto px-4"> <div class="max-w-7xl mx-auto px-4">
<div class="flex items-center justify-between py-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 <div
...@@ -25,50 +25,64 @@ ...@@ -25,50 +25,64 @@
</div> </div>
<div v-loading="loading"> <div v-loading="loading">
<!-- 第一页的特殊布局 --> <!-- 第一页的特殊布局 -->
<template v-if="searchParams.current === 0"> <div v-show="searchParams.current === 1">
<!-- 前三个特殊布局 --> <!-- 前三个特殊布局 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- 第一个视频 - 占据两列 --> <!-- 第一个视频 - 占据两列 -->
<div <div
@click="router.push(`/videoDetail/${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" class="lg:col-span-2 group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-xl transition-all duration-500 cursor-pointer"
> >
<div class="relative overflow-hidden"> <div class="relative overflow-hidden mb-5">
<img <img
src="https://picsum.photos/800/400?random=1" :src="list[0]?.faceUrl"
class="w-full h-72 object-cover group-hover:scale-105 transition-transform duration-700" class="w-full h-90 object-cover group-hover:scale-105 transition-transform duration-700"
/> />
<div <div
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent"
></div> ></div>
<!-- 标签和数据 --> <!-- 标签和数据 -->
<div <!-- <div
class="absolute top-4 left-4 bg-gradient-to-r from-orange-500 to-red-500 text-white px-3 py-1.5 rounded-full text-sm font-semibold shadow-lg" class="absolute top-4 left-4 bg-gradient-to-r from-orange-500 to-red-500 text-white px-3 py-1.5 rounded-full text-sm font-semibold shadow-lg"
> >
🔥 推荐 🔥 推荐
</div> -->
<div
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" />
<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"
> >
01:34:30 {{ 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 <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg" class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg"
> >
<SvgIcon name="icon_play" size="14" /> <el-icon class="text-sm"><View /></el-icon>
<span class="text-sm font-medium">1.7万</span> <span>{{ list[0]?.viewCount }}</span>
</div> </div>
<div <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg" class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg"
> >
<SvgIcon name="icon_comment" size="14" /> <el-icon class="text-sm"><ChatDotRound /></el-icon>
<span class="text-sm font-medium">112</span> <span>{{ list[0]?.replyCount }}</span>
</div>
<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"><Star /></el-icon>
<span>{{ list[0]?.replyCount }}</span>
</div> </div>
</div> </div>
<!-- 播放按钮 --> <!-- 播放按钮 -->
<div <!-- <div
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300" class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
> >
<div <div
...@@ -76,28 +90,30 @@ ...@@ -76,28 +90,30 @@
> >
<SvgIcon name="icon_play" size="24" color="#374151" /> <SvgIcon name="icon_play" size="24" color="#374151" />
</div> </div>
</div> </div> -->
</div> </div>
<div class="p-6"> <div class="p-6">
<h3 <h3
class="font-bold text-xl mb-3 text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-2" class="font-bold text-xl mb-3 text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-1"
> >
为什么你的配色"不高级"? {{ list[0]?.title }}
</h3> </h3>
<p class="text-gray-600 mb-4 line-clamp-2 leading-relaxed"> <h2 class="text-gray-600 mb-4 line-clamp-2 leading-relaxed">
【色彩搭配】别让配色毁了你的设计!视觉传达专业暑假改指南,3步让作品集秒变高级... {{ list[0]?.content }}
</p> </h2>
<div class="flex items-center justify-between text-gray-500 text-sm"> <div class="flex items-center justify-between text-gray-500 text-sm">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <img
class="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center text-white text-xs font-bold" :src="list[0]?.showAvatar"
> alt=""
class="w-6 h-6 rounded-full object-cover"
</div> />
<span class="font-medium">平面设计小课堂_柏子</span> <span class="font-medium">{{ list[0]?.showName }}</span>
</div> </div>
<span class="text-xs bg-gray-100 px-2 py-1 rounded-full">8-1</span> <span class="text-xs bg-gray-100 px-2 py-1 rounded-full">{{
dayjs((list[0]?.createTime ?? 0) * 1000).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</div> </div>
</div> </div>
</div> </div>
...@@ -105,58 +121,85 @@ ...@@ -105,58 +121,85 @@
<!-- 右侧两个视频 --> <!-- 右侧两个视频 -->
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div <div
v-for="n in 2" v-for="(item, index) in [list[1], list[2]]"
:key="n" :key="index"
class="group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer flex-1" class="group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer flex-1"
@click="goVideoDetail(n)" @click="router.push(`/videoDetail/${item?.id}`)"
> >
<div class="relative overflow-hidden"> <div class="relative overflow-hidden">
<img <img
:src="`https://picsum.photos/400/200?random=${n + 1}`" :src="item?.faceUrl"
class="w-full h-36 object-cover group-hover:scale-105 transition-transform duration-500" class="w-full h-44 object-cover group-hover:scale-105 transition-transform duration-500"
/> />
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div> <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 <div
class="absolute top-2 left-2 bg-gradient-to-r from-blue-500 to-cyan-500 text-white px-2 py-1 rounded-full text-xs font-semibold" 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"
> >
{{ n === 1 ? '数码' : '教程' }} <img class="w-6" src="@/assets/img/culture/recommend.png" />
<div class="text-12px text-#000 line-height-12px">推荐</div>
</div> </div>
<!-- 时长 -->
<div <div
class="absolute bottom-2 right-2 bg-black/80 backdrop-blur-sm text-white px-2 py-1 rounded text-xs" class="absolute bottom-3 right-3 bg-black/80 backdrop-blur-sm text-white px-2 py-1 rounded-lg text-xs"
> >
09:35 {{ item?.videoDuration }}
</div> </div>
<!-- 播放按钮 --> <!-- 数据 -->
<div <div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs">
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300" <div
> class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"><View /></el-icon>
<span>{{ item?.viewCount }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"><ChatDotRound /></el-icon>
<span>{{ item?.replyCount }}</span>
</div>
<div <div
class="w-10 h-10 bg-white/90 backdrop-blur-sm rounded-full flex items-center justify-center shadow-lg" class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
> >
<SvgIcon name="icon_play" size="16" color="#374151" /> <el-icon class="text-sm"><Star /></el-icon>
<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">
<h3 <h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1" class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
> >
{{ n === 1 ? '复日奶茶' : 'PS最新版零基础全套' }} {{ item?.title }}
</h3> </h3>
<p class="text-gray-600 text-sm mb-3 line-clamp-2">
{{
n === 1
? 'Figma+3D 奶茶小卡片|清爽夏日实用指南...'
: '【PS全套教程】3节课,Photoshop零基础入门...'
}}
</p>
<div class="flex items-center justify-between text-gray-500 text-xs"> <div class="flex items-center justify-between text-gray-500 text-xs">
<span class="font-medium">{{ n === 1 ? '设计师的日常' : 'PS教程' }}</span> <div class="flex items-center gap-2">
<span class="bg-gray-100 px-2 py-1 rounded-full">{{ <img :src="item?.showAvatar" alt="" class="w-6 h-6 rounded-full object-cover" />
n === 1 ? '7-29' : '7-27' <span class="font-medium">{{ item?.showName }}</span>
</div>
<span>{{
dayjs((item?.createTime ?? 0) * 1000).format('YYYY-MM-DD HH:mm')
}}</span> }}</span>
</div> </div>
</div> </div>
...@@ -167,7 +210,8 @@ ...@@ -167,7 +210,8 @@
<!-- 剩余视频 - 标准网格 --> <!-- 剩余视频 - 标准网格 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div <div
v-for="item in list" @click="router.push(`/videoDetail/${item.id}`)"
v-for="item in list.slice(3)"
:key="item.id" :key="item.id"
class="group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer" class="group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer"
> >
...@@ -179,17 +223,24 @@ ...@@ -179,17 +223,24 @@
<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 <!-- <div
class="absolute top-3 left-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-2.5 py-1 rounded-full text-xs font-semibold" class="absolute top-3 left-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-2.5 py-1 rounded-full text-xs font-semibold"
> >
{{ item.tagNameList[0] }} {{ item.tagNameList[0] }}
</div> -->
<div
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="" />
<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"
> >
15:18 {{ item.videoDuration }}
</div> </div>
<!-- 数据 --> <!-- 数据 -->
...@@ -197,58 +248,54 @@ ...@@ -197,58 +248,54 @@
<div <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg" class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
> >
<SvgIcon name="icon_play" size="12" /> <el-icon class="text-sm"><View /></el-icon>
<span>{{ item.viewCount }}</span> <span>{{ item.viewCount }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"><ChatDotRound /></el-icon>
<span>{{ item.replyCount }}</span>
</div> </div>
<div <div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg" class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
> >
<SvgIcon name="icon_comment" size="12" /> <el-icon class="text-sm"><Star /></el-icon>
<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 <div
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300" class="bg-white/90 backdrop-blur-sm rounded-full flex items-center justify-center shadow-xl transform scale-90 group-hover:scale-100 transition-transform duration-300"
> >
<div <el-icon size="50" color="#333"><VideoPlay /></el-icon>
class="w-12 h-12 bg-white/90 backdrop-blur-sm rounded-full flex items-center justify-center shadow-xl transform scale-90 group-hover:scale-100 transition-transform duration-300"
>
<SvgIcon name="icon_play" size="20" color="#374151" />
</div>
</div> </div>
</div> -->
</div> </div>
<div class="p-4"> <div class="p-4">
<h3 <h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1" class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
> >
{{ item.title }} {{ item?.title }}
</h3> </h3>
<p class="text-gray-600 text-sm mb-3 line-clamp-2 leading-relaxed">
{{ item.content }}
</p>
<div class="flex items-center justify-between text-gray-500 text-xs"> <div class="flex items-center justify-between text-gray-500 text-xs">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <img :src="item?.showAvatar" alt="" class="w-6 h-6 rounded-full object-cover" />
class="w-6 h-6 bg-gradient-to-r from-green-400 to-blue-500 rounded-full flex items-center justify-center text-white text-xs font-bold" <span class="font-medium">{{ item?.showName }}</span>
>
{{ item.createUserName }}
</div>
<span class="font-medium">{{ item.createUserName }}</span>
</div> </div>
<span class="bg-gray-100 px-2 py-1 rounded-full">{{ <span>{{ dayjs(item?.createTime * 1000).format('YYYY-MM-DD HH:mm') }}</span>
dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm')
}}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </div>
<!-- 其他页面 - 标准3列网格 --> <!-- 其他页面 - 标准3列网格 -->
<template v-else> <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 <div
@click="router.push(`/videoDetail/${item.id}`)" @click="router.push(`/videoDetail/${item.id}`)"
...@@ -264,17 +311,24 @@ ...@@ -264,17 +311,24 @@
<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 <!-- <div
class="absolute top-3 left-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-2.5 py-1 rounded-full text-xs font-semibold" class="absolute top-3 left-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-2.5 py-1 rounded-full text-xs font-semibold"
> >
{{ item.tagNameList[0] }} {{ item.tagNameList[0] }}
</div> -->
<div
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="" />
<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"
> >
15:18 {{ item?.videoDuration }}
</div> </div>
<!-- 数据 --> <!-- 数据 -->
...@@ -326,7 +380,7 @@ ...@@ -326,7 +380,7 @@
</div> </div>
</div> </div>
</div> </div>
</template> </div>
<!-- 底部分页 --> <!-- 底部分页 -->
<div class="bottom-pagination backdrop-blur-8 border-t border-gray-200"> <div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
...@@ -345,7 +399,7 @@ ...@@ -345,7 +399,7 @@
<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"
:page-sizes="[12, 24, 36, 48]" :page-sizes="[9, 24, 36, 48]"
layout="prev, pager, next, jumper, total" layout="prev, pager, next, jumper, total"
:total="total" :total="total"
class="custom-pagination" class="custom-pagination"
...@@ -383,7 +437,7 @@ const { list, total, searchParams, loading, goToPage, changePageSize, refresh } ...@@ -383,7 +437,7 @@ const { list, total, searchParams, loading, goToPage, changePageSize, refresh }
{ {
defaultParams: { type: ArticleTypeEnum.VIDEO, sortLogic: 0 }, defaultParams: { type: ArticleTypeEnum.VIDEO, sortLogic: 0 },
defaultCurrent: 1, defaultCurrent: 1,
defaultSize: 12, defaultSize: 9,
immediate: false, immediate: false,
}, },
) )
...@@ -397,9 +451,6 @@ const toggleTab = (sortLogic: number) => { ...@@ -397,9 +451,6 @@ const toggleTab = (sortLogic: number) => {
refresh() refresh()
} }
const goVideoDetail = (n: number) => {
router.push(`/videoDetail?id=${n}`)
}
defineExpose({ defineExpose({
refresh: () => { refresh: () => {
refresh() refresh()
...@@ -407,11 +458,4 @@ defineExpose({ ...@@ -407,11 +458,4 @@ defineExpose({
}) })
</script> </script>
<style> <style></style>
/* 文本截断样式 */
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
...@@ -36,19 +36,19 @@ ...@@ -36,19 +36,19 @@
/> />
<div <div
v-if="i.isRecommend" 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" class="absolute top--1 left--1 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>
<h3 class="text-sm font-medium text-gray-800 mb-2 transition-colors"> <h3 class="text font-medium text-gray-800 mb-2 transition-colors">
{{ i.title }} {{ i.title }}
</h3> </h3>
<p class="text-xs text-gray-500 mb-3 line-clamp-2"> <p class="text-sm text-gray-500 mb-3 line-clamp-2">
{{ i.content }} {{ i.content }}
</p> </p>
<div class="flex items-center justify-between text-xs text-gray-400"> <div class="flex items-center justify-between text-xs text-gray-500">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex items-center"> <span class="flex items-center">
<el-icon class="mr-1"><View /></el-icon> <el-icon class="mr-1"><View /></el-icon>
......
<template> <template>
<div> <div>
<!-- 发布区域 --> <!-- 发布区域 -->
<div class="bg-white p-6 mb-6 rounded-lg shadow-sm"> <PublishPractice />
<div class="flex-1 bg-white rounded-lg border border-gray-200">
<!-- 主输入区域 -->
<div class="flex gap-3 mb-4 items-center">
<!-- 用户头像 -->
<el-avatar :size="48" :src="userInfo.avatar" class="flex-shrink-0">
<el-icon><User /></el-icon>
</el-avatar>
<!-- 输入区域 -->
<div class="flex-1">
<!-- 话题标签输入 -->
<div class="mb-4">
<el-input
v-model="tagInput"
placeholder="话题描述...... (非必填)"
class="tag-input"
clearable
/>
</div>
<!-- 主要内容输入 -->
<div class="relative mb-3">
<el-input
type="textarea"
placeholder="请输入你想发布的话题"
:rows="3"
:maxlength="500"
resize="none"
class="main-textarea"
/>
<!-- 字符计数 -->
<div class="absolute bottom-3 right-3 text-xs text-gray-400">1/30</div>
</div>
</div>
</div>
<!-- 工具栏 -->
<div class="flex items-center justify-between pl-15">
<!-- 左侧工具按钮 -->
<div class="flex items-center gap-1">
<el-tooltip content="添加标签" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><CollectionTag /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="添加图片" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><Picture /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="添加视频" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><VideoPlay /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="添加附件" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><Paperclip /></el-icon>
</el-button>
</el-tooltip>
</div>
<!-- 右侧操作按钮 -->
<div class="flex items-center gap-3">
<el-button
class="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-50 rounded-lg border border-gray-200 text-sm"
>
存草稿
</el-button>
<el-button
type="primary"
:disabled="!tagInput"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
>
发布话题
</el-button>
</div>
</div>
</div>
</div>
<!-- 标签导航 --> <!-- 标签导航 -->
<div class="bg-white p-4 mb-6 rounded-lg shadow-sm"> <div class="bg-white p-4 mb-6 rounded-lg shadow-sm">
...@@ -133,7 +38,7 @@ ...@@ -133,7 +38,7 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-1 h-6 bg-red-500 rounded"></div> <div class="w-1 h-6 bg-red-500 rounded"></div>
<h2 class="text-lg font-medium"> <h2 class="text-lg font-medium">
{{ tagList.find((tag) => searchParams.tagIdList?.includes(tag.id))?.title ?? '最新' }} {{ filterText }}
</h2> </h2>
</div> </div>
<div <div
...@@ -143,10 +48,9 @@ ...@@ -143,10 +48,9 @@
查看更多 >> 查看更多 >>
</div> </div>
</div> </div>
<el-divider /> <el-divider class="my-1!" />
<!-- 动态列表 --> <div class="divide-y bg-#fff">
<div class="divide-y divide-gray-100">
<div <div
@click="router.push(`/postDetail/${item.id}`)" @click="router.push(`/postDetail/${item.id}`)"
v-for="item in list" v-for="item in list"
...@@ -156,9 +60,9 @@ ...@@ -156,9 +60,9 @@
<div class="flex gap-3 items-center"> <div class="flex gap-3 items-center">
<!-- 左侧内容 --> <!-- 左侧内容 -->
<div class="flex-1"> <div class="flex-1">
<h3 class="text-base font-medium text-gray-900 mb-2 leading-relaxed"> <h2 class="font-medium text-gray-900 mb-2 leading-relaxed line-clamp-1">
{{ item.title }} {{ item.title }}
</h3> </h2>
<!-- 带图片的内容 --> <!-- 带图片的内容 -->
<div class="flex gap-3 mb-3"> <div class="flex gap-3 mb-3">
...@@ -169,7 +73,7 @@ ...@@ -169,7 +73,7 @@
class="w-20 h-20 object-cover rounded-lg flex-shrink-0" class="w-20 h-20 object-cover rounded-lg flex-shrink-0"
/> />
<div class="flex-1"> <div class="flex-1">
<div class="text-gray-600 text-sm leading-relaxed"> <div class="text-gray-600 text-sm leading-relaxed line-clamp-3">
{{ item.content }} {{ item.content }}
</div> </div>
</div> </div>
...@@ -239,13 +143,13 @@ ...@@ -239,13 +143,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { getPracticeList } from '@/api/practice' import { getPracticeList } from '@/api/practice'
import { usePageSearch, useScrollTop } from '@/hooks' import { usePageSearch, useScrollTop } from '@/hooks'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { TABS_REF_KEY } from '@/constants' import { TABS_REF_KEY } from '@/constants'
import { useTagsStore } from '@/stores/tags' import { useTagsStore } from '@/stores/tags'
import PublishPractice from './publishPractice.vue'
const tagsStore = useTagsStore() const tagsStore = useTagsStore()
const { tagList } = storeToRefs(tagsStore) const { tagList } = storeToRefs(tagsStore)
...@@ -253,7 +157,6 @@ const { tagList } = storeToRefs(tagsStore) ...@@ -253,7 +157,6 @@ const { tagList } = storeToRefs(tagsStore)
const router = useRouter() const router = useRouter()
const tabsRef = inject(TABS_REF_KEY) const tabsRef = inject(TABS_REF_KEY)
const { userInfo } = storeToRefs(useUserStore())
const filterOptions = ref([ const filterOptions = ref([
{ title: '最热', id: 0 }, { title: '最热', id: 0 },
...@@ -261,8 +164,6 @@ const filterOptions = ref([ ...@@ -261,8 +164,6 @@ const filterOptions = ref([
{ title: '最多观看', id: 2 }, { title: '最多观看', id: 2 },
]) ])
const tagInput = ref('')
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!) const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const { list, total, searchParams, goToPage, changePageSize, refresh } = usePageSearch( const { list, total, searchParams, goToPage, changePageSize, refresh } = usePageSearch(
getPracticeList, getPracticeList,
...@@ -273,6 +174,15 @@ const { list, total, searchParams, goToPage, changePageSize, refresh } = usePage ...@@ -273,6 +174,15 @@ const { list, total, searchParams, goToPage, changePageSize, refresh } = usePage
immediate: false, immediate: false,
}, },
) )
const filterText = computed(() => {
return (
filterOptions.value.find((item) => item.id === searchParams.value.sortLogic)?.title +
(searchParams.value.tagIdList?.length ? '——' : '') +
(searchParams.value.tagIdList?.length
? tagList.value.find((item) => searchParams.value.tagIdList?.includes(item.id))?.title
: '')
)
})
const toggleFilter = (id: number) => { const toggleFilter = (id: number) => {
searchParams.value.sortLogic = id searchParams.value.sortLogic = id
...@@ -285,6 +195,7 @@ const toggleTag = (id: number) => { ...@@ -285,6 +195,7 @@ const toggleTag = (id: number) => {
refresh() refresh()
handleBackTop() handleBackTop()
} }
defineExpose({ defineExpose({
refresh: () => { refresh: () => {
refresh() refresh()
......
<template>
<div class="bg-white p-6 mb-6 rounded-lg shadow-sm">
<div class="flex-1 bg-white rounded-lg border border-gray-200">
<!-- 主输入区域 -->
<div class="flex gap-3 mb-4 items-start">
<!-- 用户头像 -->
<el-avatar :size="48" :src="userInfo.avatar" class="flex-shrink-0">
<el-icon><User /></el-icon>
</el-avatar>
<!-- 输入区域 -->
<div class="flex-1">
<!-- 话题标签输入 -->
<div class="mb-4">
<el-input v-model="form.title" placeholder="实践标题" class="tag-input" clearable />
</div>
<!-- 主要内容输入 -->
<div class="relative mb-3">
<el-input
type="textarea"
placeholder="请输入实践内容"
:rows="3"
:maxlength="500"
resize="none"
class="main-textarea"
v-model="form.content"
/>
<!-- 字符计数 -->
<div class="absolute bottom-3 right-3 text-xs text-gray-400">1/30</div>
</div>
<!-- 标签内容 -->
<div class="mb-2">
<!-- 选择的标签内容 -->
<div class="flex items-center gap-2">
<span v-if="mainTagText" class="text-sm text-gray-500"
>主标签:
<el-tag>{{ mainTagText }}</el-tag>
</span>
<span v-if="subTagTextList.length > 0" class="text-sm text-gray-500"
>副标签:
<el-tag class="mr-2" v-for="tag in subTagTextList" :key="tag">{{ tag }}</el-tag>
</span>
</div>
</div>
<!-- 图片相关 -->
<div v-if="form.imgUrl.length" class="flex flex-wrap gap-2">
<!-- 删除图片 -->
<div
class="relative w-20 h-20 rounded-lg overflow-hidden group"
v-for="img in form.imgUrl"
:key="img"
>
<div
class="absolute top-1 right-1 z-10 w-5 h-5 flex items-center justify-center bg-black/60 rounded-full cursor-pointer opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-black/80 hover:scale-110"
@click="handleDeleteImg(img)"
>
<el-icon class="text-white text-xs">
<Close />
</el-icon>
</div>
<el-image
:src="img"
class="w-full h-full rounded-lg border border-gray-200"
fit="cover"
/>
</div>
</div>
</div>
</div>
<!-- 工具栏 -->
<div class="flex items-center justify-between pl-15">
<!-- 左侧工具按钮 -->
<div class="flex items-center gap-1">
<el-tooltip content="添加标签" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
@click="handleAddTag"
>
<el-icon size="18"><CollectionTag /></el-icon>
</el-button>
</el-tooltip>
<!-- 隐藏上传文件的input -->
<input type="file" class="hidden" ref="fileInputRef" @change="handleFileChange" />
<el-tooltip content="添加图片" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
@click="fileInputRef?.click()"
>
<el-icon size="18"><Picture /></el-icon>
</el-button>
</el-tooltip>
<!-- <el-tooltip content="添加视频" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><VideoPlay /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="添加附件" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><Paperclip /></el-icon>
</el-button>
</el-tooltip> -->
</div>
<!-- 右侧操作按钮 -->
<div class="flex items-center gap-3">
<el-button
class="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-50 rounded-lg border border-gray-200 text-sm"
@click="handlePublish(ReleaseStatusTypeEnum.DRAFT)"
>
存草稿
</el-button>
<el-button
type="primary"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
@click="handlePublish(ReleaseStatusTypeEnum.PUBLISH)"
>
发布实践
</el-button>
</div>
</div>
</div>
<SelectTagsDialog
v-model:mainTagId="form.mainTagId"
v-model:tagList="form.tagList"
ref="selectTagsDialogRef"
/>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import SelectTagsDialog from './selectTagsDialog.vue'
import { useResetData } from '@/hooks'
import { ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants'
import { useTagsStore } from '@/stores'
import { uploadFile } from '@/api'
import { Close } from '@element-plus/icons-vue'
import { addOrUpdatePractice } from '@/api'
import type { AddOrUpdatePracticeDto } from '@/api/practice/types'
const tagsStore = useTagsStore()
const { tagList } = storeToRefs(tagsStore)
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const selectTagsDialogRef =
useTemplateRef<InstanceType<typeof SelectTagsDialog>>('selectTagsDialogRef')
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
const [form, resetForm] = useResetData({
title: '',
content: '',
imgUrl: [],
releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
mainTagId: '',
tagList: [],
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
})
const mainTagText = computed(() => {
return tagList.value.find((tag) => tag.id === Number(form.value.mainTagId))?.title
})
const subTagTextList = computed(() => {
return form.value.tagList.map((tag) => tagList.value.find((t) => t.id === tag)?.title)
})
const handleAddTag = () => {
selectTagsDialogRef.value?.open()
}
const handleFileChange = async (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) {
const { data } = await uploadFile(file)
form.value.imgUrl.push(data.data[0].filePath)
}
}
const handleDeleteImg = (img: string) => {
form.value.imgUrl = form.value.imgUrl.filter((item) => item !== img)
}
const validateForm = () => {
if (!form.value.title) {
ElMessage.error('请输入实践标题')
return false
}
if (!form.value.content) {
ElMessage.error('请输入实践内容')
return false
}
if (!form.value.mainTagId) {
ElMessage.error('请选择主标签')
return false
}
return true
}
const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdatePracticeDto => {
return {
...form.value,
releaseStatus,
faceUrl: form.value.imgUrl[0] || '',
imgUrl: form.value.imgUrl.join(','),
tagList: [form.value.mainTagId, ...form.value.tagList].map((item, index) => ({
sort: index,
tagId: Number(item),
})),
}
}
const handlePublish = async (releaseStatus: ReleaseStatusTypeEnum) => {
if (!validateForm()) return
await addOrUpdatePractice(transformForm(releaseStatus))
ElMessage.success(releaseStatus === ReleaseStatusTypeEnum.PUBLISH ? '发布成功' : '存草稿成功')
resetForm()
}
</script>
<style scoped></style>
<template>
<el-dialog v-model="dialogVisible" title="选择标签" width="500px" :close-on-click-modal="false">
<div class="space-y-6 px-2">
<div class="flex items-start gap-4">
<div class="text-sm text-gray-700 w-16 flex-shrink-0">主标签</div>
<div class="flex-1">
<SelectTags v-model="mainTagId" />
</div>
</div>
<div class="flex items-start gap-4">
<div class="text-sm text-gray-700 w-16 flex-shrink-0">副标签</div>
<div class="flex-1">
<SelectTags
v-model="subTagIdList"
:max-selected-tags="3"
:filter-tags-fn="filterTagsFn"
/>
</div>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import SelectTags from '@/components/common/SelectTags/index.vue'
import type { TagItemDto } from '@/api'
const dialogVisible = ref(false)
const mainTagId = defineModel<string>('mainTagId', { required: true })
const subTagIdList = defineModel<number[]>('tagList', { required: true })
const open = () => {
dialogVisible.value = true
}
const filterTagsFn = (allTags: TagItemDto[]) => {
return allTags.filter((tag) => tag.id !== Number(mainTagId.value))
}
defineExpose({
open,
})
</script>
<style scoped></style>
...@@ -3,20 +3,9 @@ ...@@ -3,20 +3,9 @@
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<!-- 主表单卡片 --> <!-- 主表单卡片 -->
<div class="bg-white rounded-lg shadow-lg p-8"> <div class="bg-white rounded-lg shadow-lg p-8">
<el-form ref="formRef" :model="form" label-position="top" size="default"> <el-form ref="formRef" :model="form" label-position="top" :rules="rules">
<!-- 案例编号 -->
<div class="mb-6 flex items-center gap-2">
<span class="text-gray-700 font-medium">案例编号:</span>
<span class="text-gray-900 font-semibold">{{ form.number }}</span>
<el-tooltip content="案例编号自动生成" placement="top">
<el-icon class="text-pink-500 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
<!-- 标题输入 --> <!-- 标题输入 -->
<el-form-item class="mb-6" label="标题"> <el-form-item class="mb-6" label="标题" prop="title">
<el-input <el-input
v-model="form.title" v-model="form.title"
placeholder="请输入【案例】标题" placeholder="请输入【案例】标题"
...@@ -27,7 +16,7 @@ ...@@ -27,7 +16,7 @@
</el-form-item> </el-form-item>
<!-- 内容输入 --> <!-- 内容输入 -->
<el-form-item class="mb-6 relative" label="内容"> <el-form-item class="mb-6 relative" label="内容" prop="content">
<el-input <el-input
v-model="form.content" v-model="form.content"
type="textarea" type="textarea"
...@@ -38,7 +27,7 @@ ...@@ -38,7 +27,7 @@
/> />
</el-form-item> </el-form-item>
<!-- 关键词选择 --> <!-- 关键词选择 -->
<el-form-item label="文化关键词" class="mb-6"> <el-form-item label="文化关键词" class="mb-6" prop="mainTagId">
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
主标签 主标签
<SelectTags v-model="form.mainTagId" /> <SelectTags v-model="form.mainTagId" />
...@@ -56,34 +45,19 @@ ...@@ -56,34 +45,19 @@
</el-form-item> </el-form-item>
<!-- 是否同步发布 --> <!-- 是否同步发布 -->
<el-form-item label="*是否同步发布到实践" class="mb-6"> <el-form-item label="是否同步发布到实践" class="mb-6" prop="isSync">
<el-radio-group v-model="form.publishToPractice"> <el-radio-group v-model="form.isSync">
<el-radio :label="true"></el-radio> <el-radio :label="1"></el-radio>
<el-radio :label="false"></el-radio> <el-radio :label="0"></el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<!-- 发布时间 -->
<!-- <el-form-item label="*发布时间" class="mb-8">
<el-radio-group v-model="form.publishTime">
<el-radio label="now">立即发布</el-radio>
<el-radio label="scheduled">定时发布</el-radio>
</el-radio-group>
<el-date-picker
v-if="form.publishTime === 'scheduled'"
v-model="form.scheduledTime"
type="datetime"
placeholder="选择发布时间"
class="ml-4"
/>
</el-form-item> -->
<!-- 底部按钮组 --> <!-- 底部按钮组 -->
<div class="flex items-center justify-between gap-4 pt-4"> <div class="flex items-center justify-between gap-4 pt-4">
<div class="flex gap-4"> <div class="flex gap-4">
<el-button @click="handleCancel"> 取消 </el-button> <el-button @click="handleCancel"> 取消 </el-button>
<el-button @click="handlePreview"> 预览 </el-button> <!-- <el-button @click="handlePreview"> 预览 </el-button> -->
<el-button type="info" plain @click="handleSaveDraft"> 存草稿 </el-button> <el-button type="info" plain @click="handleSubmit"> 存草稿 </el-button>
</div> </div>
<el-button type="primary" @click="handleSubmit"> 提交 </el-button> <el-button type="primary" @click="handleSubmit"> 提交 </el-button>
</div> </div>
...@@ -94,14 +68,15 @@ ...@@ -94,14 +68,15 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { QuestionFilled } from '@element-plus/icons-vue' import { addOrUpdateCase } from '@/api'
import { addOrUpdateCase, getMaxCaseNumber } from '@/api'
import { useResetData } from '@/hooks' import { useResetData } from '@/hooks'
import type { AddOrUpdateCaseDto } from '@/api' import type { AddOrUpdateCaseDto } from '@/api'
import SelectTags from '@/components/common/SelectTags/index.vue' import SelectTags from '@/components/common/SelectTags/index.vue'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import type { TagItemDto } from '@/api' import type { TagItemDto } from '@/api'
import { TagTypeEnum, TagLevelEnum } from '@/constants' import { TagTypeEnum, TagLevelEnum, BooleanFlag, ReleaseStatusTypeEnum } from '@/constants'
const router = useRouter()
const formRef = useTemplateRef<FormInstance>('formRef') const formRef = useTemplateRef<FormInstance>('formRef')
...@@ -110,31 +85,35 @@ type FormData = Omit<AddOrUpdateCaseDto, 'tagRelationDtoList'> & { ...@@ -110,31 +85,35 @@ type FormData = Omit<AddOrUpdateCaseDto, 'tagRelationDtoList'> & {
subTagIds: number[] subTagIds: number[]
} }
const [form] = useResetData<FormData>({ const rules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入内容', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择文化关键词', trigger: 'blur' }],
isSync: [{ required: true, message: '请选择是否同步发布到实践', trigger: 'blur' }],
}
const [form, resetForm] = useResetData({
title: '', title: '',
content: '', content: '',
number: '',
mainTagId: '', mainTagId: '',
subTagIds: [], subTagIds: [],
isSync: BooleanFlag.NO,
releaseStatus: ReleaseStatusTypeEnum.DRAFT,
}) })
// 取消 // 取消
const handleCancel = () => { const handleCancel = () => {
ElMessage.warning('确定要取消吗?') resetForm()
// 可以添加确认对话框 router.back()
} }
// 预览 // 预览
const handlePreview = () => { // const handlePreview = () => {
console.log('预览', form) // console.log('预览', form)
ElMessage.success('预览功能待实现') // ElMessage.success('预览功能待实现')
} // }
// 保存草稿 // 保存草稿
const handleSaveDraft = () => {
console.log('保存草稿', form)
ElMessage.success('草稿保存成功')
}
const transformData = (formData: FormData): AddOrUpdateCaseDto => { const transformData = (formData: FormData): AddOrUpdateCaseDto => {
const { mainTagId, subTagIds, ...rest } = formData const { mainTagId, subTagIds, ...rest } = formData
...@@ -142,6 +121,7 @@ const transformData = (formData: FormData): AddOrUpdateCaseDto => { ...@@ -142,6 +121,7 @@ const transformData = (formData: FormData): AddOrUpdateCaseDto => {
...rest, ...rest,
tagRelationDtoList: [], tagRelationDtoList: [],
} }
// 添加标签内容
obj.tagRelationDtoList.push({ obj.tagRelationDtoList.push({
tagId: Number(mainTagId), tagId: Number(mainTagId),
type: TagTypeEnum.CULTURE_TAG, type: TagTypeEnum.CULTURE_TAG,
...@@ -163,6 +143,8 @@ const handleSubmit = async () => { ...@@ -163,6 +143,8 @@ const handleSubmit = async () => {
const res = await addOrUpdateCase(transformData(form.value)) const res = await addOrUpdateCase(transformData(form.value))
if (res) { if (res) {
ElMessage.success('提交成功') ElMessage.success('提交成功')
resetForm()
router.back()
} }
} }
...@@ -230,14 +212,8 @@ const filterTagsFn = (allTags: TagItemDto[]) => { ...@@ -230,14 +212,8 @@ const filterTagsFn = (allTags: TagItemDto[]) => {
return allTags.filter((tag) => tag.id !== Number(form.value.mainTagId)) return allTags.filter((tag) => tag.id !== Number(form.value.mainTagId))
} }
const fetchMaxCaseNumber = async () => {
const { data } = await getMaxCaseNumber()
form.value.number = data
}
onActivated(() => { onActivated(() => {
showSubmissionGuide() showSubmissionGuide()
fetchMaxCaseNumber()
}) })
</script> </script>
......
...@@ -12,9 +12,10 @@ ...@@ -12,9 +12,10 @@
<h3 class="text-xl font-bold text-gray-800">上传视频</h3> <h3 class="text-xl font-bold text-gray-800">上传视频</h3>
</div> </div>
<el-form-item prop="videoUrl"> <el-form-item prop="videoUrl">
<UploadVideo v-model="form.videoUrl" /> <UploadVideo v-model="form.videoUrl" @uploadSuccess="handleVideoChange" />
</el-form-item> </el-form-item>
</div> </div>
<!-- 基本设置 --> <!-- 基本设置 -->
<div <div
class="bg-white backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-8 hover:shadow-xl transition-all duration-300" class="bg-white backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-8 hover:shadow-xl transition-all duration-300"
...@@ -25,49 +26,44 @@ ...@@ -25,49 +26,44 @@
<!-- 封面设置 --> <!-- 封面设置 -->
<div class="mb-8"> <div class="mb-8">
<label class="block text-sm font-semibold text-gray-700 mb-4">封面选择</label> <el-form-item prop="faceUrl">
<div class="flex gap-6 items-start"> <div class="">
<!-- 主封面 --> <label class="block text-sm font-semibold text-gray-700 mb-3">封面选择</label>
<div class="relative group"> <div class="flex gap-6">
<div <!-- 主封面预览 -->
class="w-48 h-28 bg-gradient-to-br from-gray-100 to-gray-200 rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-all duration-300" <div class="relative group">
> <div
<img class="w-48 h-28 bg-gradient-to-br from-gray-100 to-gray-200 rounded-lg shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer overflow-hidden"
src="@/assets/img/culture/ask.png" @click="showCoverDialog = true"
alt="主封面" >
class="w-full h-full object-cover" <img
/> v-if="form.faceUrl"
<div :src="form.faceUrl"
class="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300" alt="视频封面"
></div> class="w-full h-full object-cover"
<div />
class="absolute bottom-2 left-2 bg-gradient-to-r from-indigo-500 to-purple-600 text-white text-xs px-2 py-1 rounded-md font-medium" <div
> v-else
智能封面 class="w-full h-full flex items-center justify-center text-gray-400"
>
<el-icon :size="32"><Picture /></el-icon>
</div>
<div
class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center shadow-lg"
>
<span class="text-white text-sm font-medium">点击选择封面</span>
</div>
<div
v-if="form.faceUrl"
class="absolute bottom-2 left-2 bg-gradient-to-r from-indigo-500 to-purple-600 text-white text-xs px-2 py-1 rounded-md font-medium"
>
当前封面
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</el-form-item>
<!-- 封面选项 -->
<div class="flex gap-3">
<div
v-for="i in 4"
:key="i"
class="w-20 h-12 bg-gray-100 rounded-lg overflow-hidden border-2 hover:border-indigo-400 cursor-pointer transition-all duration-200"
:class="i === 1 ? 'border-indigo-500 shadow-md' : 'border-gray-200'"
>
<img
src="@/assets/img/culture/ask.png"
alt="封面选项"
class="w-full h-full object-cover"
/>
</div>
</div>
</div>
<p class="text-sm text-gray-500 mt-3 flex items-center gap-2">
<el-icon class="text-indigo-500"><InfoFilled /></el-icon>
选择封面UI中的第一张,让作品获得更多推荐机会!
</p>
</div> </div>
<!-- 标题 --> <!-- 标题 -->
...@@ -79,11 +75,11 @@ ...@@ -79,11 +75,11 @@
placeholder="请输入视频标题..." placeholder="请输入视频标题..."
maxlength="80" maxlength="80"
show-word-limit show-word-limit
size="large"
class="title-input" class="title-input"
/> />
</el-form-item> </el-form-item>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<el-form-item prop="content"> <el-form-item prop="content">
<label class="block text-sm font-semibold text-gray-700 mb-3">视频简介</label> <label class="block text-sm font-semibold text-gray-700 mb-3">视频简介</label>
...@@ -95,43 +91,44 @@ ...@@ -95,43 +91,44 @@
/> />
</el-form-item> </el-form-item>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<el-form-item prop="mainTagId"> <el-form-item prop="mainTagId">
<label class="block text-sm font-semibold text-gray-700 mb-3" <label class="block text-sm font-semibold text-gray-700 mb-3">
>主标签 主标签
<el-tooltip content="添加相关标签可以帮助更多人发现您的作品" placement="top"> <el-tooltip content="主标签最多添加一个" placement="top">
<el-icon class="text-gray-400 cursor-help" <el-icon class="text-gray-400 cursor-help"><QuestionFilled /></el-icon>
><QuestionFilled </el-tooltip>
/></el-icon> </el-tooltip </label>
></label>
<SelectTags class="w-full" v-model="form.mainTagId" :max-selected-tags="1" /> <SelectTags class="w-full" v-model="form.mainTagId" :max-selected-tags="1" />
</el-form-item> </el-form-item>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<el-form-item prop="mainTagId"> <el-form-item prop="tagList">
<label class="block text-sm font-semibold text-gray-700 mb-3" <label class="block text-sm font-semibold text-gray-700 mb-3">
>副标签 副标签
<el-tooltip content="副标签可以添加多个,最多3个" placement="top"> <el-tooltip content="副标签最多添加3个" placement="top">
<el-icon class="text-gray-400 cursor-help" <el-icon class="text-gray-400 cursor-help"><QuestionFilled /></el-icon>
><QuestionFilled </el-tooltip>
/></el-icon> </el-tooltip </label>
></label>
<SelectTags <SelectTags
class="w-full" class="w-full"
v-model="form.tagList" v-model="form.tagList as number[]"
:max-selected-tags="3" :max-selected-tags="3"
:filter-tags-fn="filterTagsFn" :filter-tags-fn="filterTagsFn"
/> />
</el-form-item> </el-form-item>
</div> </div>
</div> </div>
<div <div
class="bg-white backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-6 hover:shadow-xl transition-all duration-300" class="bg-white backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-6 hover:shadow-xl transition-all duration-300"
> >
<h4 class="text-lg font-bold text-gray-800 mb-4">发布设置</h4> <h4 class="text-lg font-bold text-gray-800 mb-4">发布设置</h4>
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"> <div class="flex items-center justify-between p-3 rounded-lg">
<div> <div>
<div class="text-sm font-medium text-gray-800">定时发布</div> <div class="text-sm font-medium text-gray-800">定时发布</div>
<div class="text-xs text-gray-500">设置发布时间</div> <div class="text-xs text-gray-500">设置发布时间</div>
...@@ -160,6 +157,7 @@ ...@@ -160,6 +157,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white/70 backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-6"> <div class="bg-white/70 backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-6">
<div class="space-y-3 flex justify-center items-center gap-4"> <div class="space-y-3 flex justify-center items-center gap-4">
<el-button <el-button
...@@ -169,7 +167,6 @@ ...@@ -169,7 +167,6 @@
<el-icon class="mr-2"><Document /></el-icon> <el-icon class="mr-2"><Document /></el-icon>
保存草稿 保存草稿
</el-button> </el-button>
<span></span>
<el-button <el-button
type="primary" type="primary"
size="large" size="large"
...@@ -184,36 +181,87 @@ ...@@ -184,36 +181,87 @@
</div> </div>
</div> </div>
</el-form> </el-form>
<!-- 右上角tips -->
<!-- <div <!-- 封面选择弹窗 -->
class="w-200px fixed bottom-0 left-0 bg-gradient-to-br from-amber-50 to-orange-50 rounded-lg p-6 border border-amber-200" <el-dialog
v-model="showCoverDialog"
title="选择视频封面"
width="800px"
:close-on-click-modal="false"
> >
<div class="flex items-start gap-3"> <div v-if="locationVideoBlolUrl" class="space-y-6">
<el-icon class="text-amber-500 mt-1"><Warning /></el-icon> <!-- 视频预览 -->
<div> <div class="relative">
<h5 class="text-sm font-semibold text-amber-800 mb-2">发布小贴士</h5> <video
<ul class="text-xs text-amber-700 space-y-1"> ref="videoRef"
<li>• 选择合适的封面能提高点击率</li> :src="locationVideoBlolUrl"
<li>• 添加相关标签帮助推荐</li> class="w-full max-h-96 rounded-lg bg-black"
<li>• 详细的简介让观众更了解内容</li> @loadedmetadata="onVideoLoaded"
</ul> @timeupdate="onTimeUpdate"
/>
<div class="absolute bottom-4 left-4 right-4 bg-black/50 backdrop-blur-sm rounded-lg p-3">
<div class="text-white text-sm mb-2">
当前时间:{{ formatTime(currentTime) }} / {{ formatTime(videoDuration) }}
</div>
<el-slider
v-model="currentTime"
:max="videoDuration"
:step="0.1"
@input="(time) => seekVideo(time as number)"
class="custom-slider"
/>
</div>
</div>
<!-- 封面预览 -->
<div class="bg-gray-50 rounded-lg p-4">
<div class="text-sm font-semibold text-gray-700 mb-3">封面预览</div>
<div class="flex items-center gap-6">
<div class="relative w-60 h-34 bg-gray-200 rounded-lg overflow-hidden">
<img v-if="form.faceUrl" :src="form.faceUrl" class="w-full h-full object-cover" />
<div v-else class="w-full h-full flex items-center justify-center text-gray-400">
<el-icon :size="40"><Picture /></el-icon>
</div>
<canvas ref="canvasRef" class="hidden" />
</div>
<div class="flex-1">
<el-button type="primary" @click="captureFrame" :icon="Camera">
截取当前帧
</el-button>
<p class="text-xs text-gray-500 mt-2">
拖动进度条找到合适的画面,点击截取按钮生成封面
</p>
</div>
</div>
</div> </div>
</div> </div>
</div> -->
<template #footer>
<div class="flex justify-end gap-3">
<el-button @click="showCoverDialog = false">取消</el-button>
<el-button type="primary" @click="confirmCover" :disabled="!form.faceUrl">
确认使用
</el-button>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import UploadVideo from '@/components/common/UploadVideo/index.vue' import UploadVideo from '@/components/common/UploadVideo/index.vue'
import { useResetData } from '@/hooks' import { useResetData } from '@/hooks'
import { ArticleTypeEnum, ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants' import { ArticleTypeEnum, ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants'
import { addOrUpdateArticle } from '@/api' import { addOrUpdateArticle, uploadFile } from '@/api'
import SelectTags from '@/components/common/SelectTags/index.vue' import SelectTags from '@/components/common/SelectTags/index.vue'
import type { TagItemDto } from '@/api' import type { TagItemDto, AddOrUpdateVideoDto } from '@/api'
import { Camera, Picture } from '@element-plus/icons-vue'
const router = useRouter()
const formRef = useTemplateRef('formRef') const formRef = useTemplateRef('formRef')
const [form] = useResetData({ const [form, resetData] = useResetData<AddOrUpdateVideoDto>({
videoUrl: '', videoUrl: '',
title: '视频标题', title: '视频标题',
type: ArticleTypeEnum.VIDEO, type: ArticleTypeEnum.VIDEO,
...@@ -223,8 +271,89 @@ const [form] = useResetData({ ...@@ -223,8 +271,89 @@ const [form] = useResetData({
sendType: SendTypeEnum.IMMEDIATE, sendType: SendTypeEnum.IMMEDIATE,
sendTime: '', sendTime: '',
releaseStatus: ReleaseStatusTypeEnum.PUBLISH, releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
faceUrl: '', // 封面URL
videoDuration: '',
}) })
// 封面选择相关
const locationVideoBlolUrl = ref('')
const showCoverDialog = ref(false)
const videoRef = ref<HTMLVideoElement>()
const canvasRef = ref<HTMLCanvasElement>()
const videoDuration = ref(0)
const currentTime = ref(0)
// 视频加载完成
const onVideoLoaded = () => {
if (videoRef.value) {
videoDuration.value = videoRef.value.duration
currentTime.value = 1 // 默认第1秒
seekVideo(1)
}
}
// 视频时间更新
const onTimeUpdate = () => {
if (videoRef.value) {
currentTime.value = videoRef.value.currentTime
}
}
// 跳转到指定时间
const seekVideo = (time: number) => {
if (videoRef.value) {
videoRef.value.currentTime = time
}
}
// 截取当前帧
const captureFrame = () => {
const video = videoRef.value
const canvas = canvasRef.value
if (!video || !canvas) return
// 用canvas 绘制 视频当前帧的图片 然后上传
canvas.width = video.videoWidth
canvas.height = video.videoHeight
// 绘制当前帧
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
// 转换为Blob
canvas.toBlob(
async (blob) => {
if (blob) {
// 直接在这里上传 不转为本地了
const { data } = await uploadFile(new File([blob], `video${Date.now()}.jpg`))
form.value.faceUrl = data.data[0].filePath
ElMessage.success('封面截取成功')
}
},
'image/jpeg',
0.9,
)
}
}
// 确认使用封面
const confirmCover = async () => {
if (!form.value.faceUrl) return
showCoverDialog.value = false
ElMessage.success('封面设置成功')
}
// 格式化时间
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
const filterTagsFn = (tags: TagItemDto[]) => { const filterTagsFn = (tags: TagItemDto[]) => {
return tags.filter((tag) => tag.id !== Number(form.value.mainTagId)) return tags.filter((tag) => tag.id !== Number(form.value.mainTagId))
} }
...@@ -233,7 +362,8 @@ const rules = { ...@@ -233,7 +362,8 @@ const rules = {
videoUrl: [{ required: true, message: '请上传视频', trigger: 'change' }], videoUrl: [{ required: true, message: '请上传视频', trigger: 'change' }],
title: [{ required: true, message: '请输入视频标题', trigger: 'blur' }], title: [{ required: true, message: '请输入视频标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入视频简介', trigger: 'blur' }], content: [{ required: true, message: '请输入视频简介', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择标签', trigger: 'change' }], mainTagId: [{ required: true, message: '请选择主标签', trigger: 'change' }],
faceUrl: [{ required: true, message: '请选择视频封面', trigger: 'change' }],
} }
const tansformData = () => { const tansformData = () => {
...@@ -244,17 +374,59 @@ const tansformData = () => { ...@@ -244,17 +374,59 @@ const tansformData = () => {
sort: index, sort: index,
})), })),
mainTagId: Number(form.value.mainTagId), mainTagId: Number(form.value.mainTagId),
faceUrl:
'https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/readName/png/2025/11/21/Common/1763710823097.png',
} }
} }
const loading = ref(false)
const resetPageData = () => {
resetData()
locationVideoBlolUrl.value = ''
}
const handleSubmit = async () => { const handleSubmit = async () => {
await formRef.value?.validate() await formRef.value?.validate()
const res = await addOrUpdateArticle(tansformData()) loading.value = true
if (res) { try {
addOrUpdateArticle(tansformData())
ElMessage.success('发布成功') ElMessage.success('发布成功')
resetPageData()
router.push('/')
// 重置数据
} catch (e) {
console.log(e)
} finally {
loading.value = false
} }
} }
const handleVideoChange = ({
file,
videoDuration,
}: {
file: File
url: string
videoDuration: string
}) => {
if (locationVideoBlolUrl.value) {
URL.revokeObjectURL(locationVideoBlolUrl.value)
}
if (file) {
locationVideoBlolUrl.value = URL.createObjectURL(file)
}
form.value.videoDuration = videoDuration
}
</script> </script>
<style scoped></style> <style scoped>
/* 自定义滑块样式 */
:deep(.custom-slider .el-slider__runway) {
background-color: rgba(255, 255, 255, 0.3);
}
:deep(.custom-slider .el-slider__bar) {
background-color: #6366f1;
}
:deep(.custom-slider .el-slider__button) {
border-color: #6366f1;
}
</style>
<template> <template>
<div class="min-h-screen bg-white"> <div ref="searchPageRef" class="bg-white/90">
<div class="max-w-1400px mx-auto p-6"> <div class="max-w-1400px mx-auto p-6">
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="mb-8"> <div class="mb-1 p-4">
<div class="relative flex items-center gap-3"> <div class="relative flex items-center gap-3">
<el-input <el-input
v-model="searchKeyword" v-model="searchParams.title"
placeholder="输入关键词搜索" placeholder="输入关键词搜索"
size="large" class="w-300px! flex-1"
class="search-input flex-1"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
clearable
/> />
<el-button type="primary" size="large" class="search-btn" @click="handleSearch"> <el-button type="primary" @click="handleSearch"> 搜索 </el-button>
搜索
</el-button>
</div> </div>
</div> </div>
<!-- 分类 Tabs --> <!-- 一级分类 -->
<div class="mb-6 flex gap-3 flex items-center"> <div class="flex items-center justify-between mb-6 border-b border-gray-200 p-4">
<Tabs
v-model="activeTab"
:tabs="tabs"
@change="(value) => handleTabChange(value as string)"
/>
<button
v-for="sort in sortOptions"
:key="sort.value"
class="px-4 py-1.5 rounded-lg text-14px transition-colors"
:class="{
'text-blue-600 bg-blue-50': activeSort === sort.value,
'text-gray-600 hover:text-blue-600': activeSort !== sort.value,
}"
@click="activeSort = sort.value"
>
{{ sort.label }}
</button>
</div>
<!-- 排序方式 -->
<div class="flex items-center justify-between mb-6 py-4 border-b border-gray-200">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-gray-600 text-14px">排序方式</span> <span class="text-gray-600 text-14px">分类</span>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
v-for="sort in sortOptions" v-for="sort in articleTypeListOptions"
:key="sort.value" :key="sort.value"
class="px-4 py-1.5 rounded-lg text-14px transition-colors" class="px-4 py-1.5 rounded-lg text-14px transition-colors cursor-pointer"
:class="{ :class="{
'text-blue-600 bg-blue-50': activeSort === sort.value, 'text-blue-600 bg-blue-50': searchParams.type === sort.value,
'text-gray-600 hover:text-blue-600': activeSort !== sort.value, 'text-gray-600 hover:text-blue-600': searchParams.type !== sort.value,
}" }"
@click="activeSort = sort.value" @click="changeType(sort.value)"
> >
{{ sort.label }} {{ sort.label }}
</button> </button>
</div> </div>
</div> </div>
<div class="text-gray-500 text-14px"> <div class="text-gray-500 text-14px">
共找到 <span class="text-blue-600 font-600">{{ totalResults }}</span> 条结果 <el-select
v-model="searchParams.sortLogic"
placeholder="请选择排序方式"
class="w-100px!"
@change="changeSort"
>
<el-option
v-for="sort in sortOptions"
:key="sort.value"
:label="sort.label"
:value="sort.value"
/>
</el-select>
</div> </div>
</div> </div>
<!-- 二级分类 -->
<!-- <div
v-show="
searchParams.type === ArticleTypeEnum.COLUMN ||
searchParams.type === ArticleTypeEnum.INTERVIEW
"
class="flex items-center gap-2 px-4"
>
<span class="text-gray-600 text-14px">二级分类:</span>
<div class="flex gap-2">
<button
v-for="sort in articleTypeListOptions"
:key="sort.value"
class="px-4 py-1.5 rounded-lg text-14px transition-colors cursor-pointer"
:class="{
'text-blue-600 bg-blue-50': searchParams.type === sort.value,
'text-gray-600 hover:text-blue-600': searchParams.type !== sort.value,
}"
@click="changeType(sort.value)"
>
{{ sort.label }}
</button>
</div>
</div> -->
<!-- 搜索结果列表 --> <div v-show="list.length">
<div class="space-y-4"> <div class="space-y-4">
<div
v-for="item in searchResults"
:key="item.id"
class="flex gap-4 p-4 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer"
>
<!-- 封面图 -->
<div <div
class="flex-shrink-0 w-240px h-135px rounded-lg overflow-hidden bg-gray-100 relative" v-for="item in list"
:key="item.id"
class="flex gap-4 p-4 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer"
@click="handleClick(item)"
> >
<img :src="item.cover" class="w-full h-full object-cover" /> <!-- 封面图 -->
<div <div
class="absolute bottom-2 right-2 bg-black/70 text-white text-12px px-2 py-0.5 rounded" v-if="item.faceUrl"
class="flex-shrink-0 w-240px h-135px rounded-lg overflow-hidden bg-gray-100 relative"
> >
{{ item.duration }} <img :src="item.faceUrl" class="w-full h-full object-cover" />
<div
v-if="item.type === ArticleTypeEnum.VIDEO"
class="absolute bottom-2 right-2 bg-black/70 text-white text-12px px-2 py-0.5 rounded"
>
{{ item.videoDuration }}
</div>
</div> </div>
</div>
<!-- 内容信息 --> <!-- 内容信息 -->
<div class="flex-1 flex flex-col justify-between min-w-0"> <div class="flex-1 flex flex-col justify-between min-w-0">
<div> <div>
<!-- 标题 --> <!-- 标题 -->
<h3 class="text-16px font-500 text-gray-900 mb-2 line-clamp-1"> <h3 class="text-16px font-500 text-gray-900 mb-2 line-clamp-1">
{{ item.title }} {{ item.title }}
</h3> </h3>
<!-- 描述 --> <!-- 描述 -->
<p class="text-14px text-gray-600 mb-3 line-clamp-2"> <p class="text-14px text-gray-600 mb-3 line-clamp-2">
{{ item.description }} {{ item.content }}
</p> </p>
</div>
<!-- 底部信息 -->
<div class="flex items-center gap-4 text-13px text-gray-500">
<span>{{ item.showName }}</span>
<span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
<div class="flex items-center gap-1">
<el-icon class="text-sm"><View /></el-icon>
<span class="font-medium text-gray-500">{{ item.viewCount }}</span>
</div>
<div class="flex items-center gap-1">
<el-icon class="text-sm"><ChatDotRound /></el-icon>
<span class="font-medium">{{ item.replyCount }}</span>
</div>
<div class="flex items-center gap-1">
<el-icon class="text-sm"><Star /></el-icon>
<span class="font-medium">{{ item.praiseCount }}</span>
</div>
</div>
</div> </div>
<!-- 底部信息 --> <div class="flex-shrink-0 self-end">
<div class="flex items-center gap-4 text-13px text-gray-500"> <span class="text-blue-600 text-13px">{{
<span>{{ item.author }}</span> articleTypeListOptions.find((i) => i.value === item.type)?.label
<span>{{ item.views }}</span> }}</span>
<span>{{ item.time }}</span>
</div> </div>
</div> </div>
</div>
<!-- 右侧视频标签 --> <div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
<div class="flex-shrink-0 self-end"> <div class="max-w-7xl mx-auto py-6">
<span class="text-blue-600 text-13px">视频</span> <div class="flex items-center justify-end gap-4">
<!-- 左侧:回到顶部按钮 -->
<div class="left">
<ScrollTopComp />
</div>
<!-- 右侧:分页器 -->
<div class="right">
<div
class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
>
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:page-sizes="[5, 20, 1]"
layout="prev, pager, next, jumper, total"
:total="total"
class="custom-pagination"
@current-change="
(e) => {
;(handleBackTop(), goToPage(e))
}
"
@size-change="changePageSize"
/>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div v-show="!list.length">
<!-- 分页 --> <div class="flex items-center justify-center h-full">
<div class="mt-8 flex justify-center"> <el-empty description="暂无数据" />
<el-pagination </div>
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="totalResults"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { articleTypeListOptions } from '@/constants'
import Tabs from '@/components/common/Tabs' import { getArticleList } from '@/api/article'
import { usePageSearch, useScrollTop } from '@/hooks'
interface SearchResult { import { ArticleTypeEnum } from '@/constants'
id: number import dayjs from 'dayjs'
title: string import type { ArticleListItemDto } from '@/api'
description: string
cover: string const router = useRouter()
duration: string const route = useRoute()
author: string const searchPageRef = ref<HTMLElement | null>(null)
views: string const { ScrollTopComp, handleBackTop } = useScrollTop(searchPageRef)
time: string
} const sortOptions = [
{ label: '最热', value: 0 },
const searchKeyword = ref('wayward') { label: '最新', value: 1 },
const tabs = [
{ label: '综合', value: '综合', count: 189 },
{ label: '视频', value: '视频', count: 89 },
{ label: '帖子', value: '帖子', count: 0 },
{ label: '实践', value: '实践', count: 0 },
{ label: '专栏', value: '专栏', count: 0 },
{ label: '专访', value: '专访', count: 51 },
{ label: '问题', value: '问题', count: 0 },
] ]
const activeTab = ref('综合') const { total, goToPage, changePageSize, refresh, searchParams, list } = usePageSearch(
getArticleList,
const sortOptions = ref([
{ label: '综合排序', value: 'default' },
{ label: '最多播放', value: 'views' },
{ label: '最新发布', value: 'time' },
{ label: '最多收藏', value: 'favorite' },
])
const activeSort = ref('default')
const totalResults = ref(189)
const currentPage = ref(1)
const pageSize = ref(20)
const searchResults = ref<SearchResult[]>([
{
id: 1,
title: 'wayward:已经20小时了!第3次挑战灭火了',
description: 'wayward这次挑战能否成功?经历了前两次失败后,这次能否成功灭火?让我们拭目以待...',
cover: 'https://picsum.photos/400/225?random=1',
duration: '20:45',
author: 'waywardzz',
views: '1.2万',
time: '2天前',
},
{
id: 2,
title: 'wayward:只是喝成这样还怎么玩?长江水果实太快了',
description: '这期节目真的太搞笑了,wayward的状态实在是...',
cover: 'https://picsum.photos/400/225?random=2',
duration: '15:30',
author: 'waywardzz',
views: '8956',
time: '3天前',
},
{ {
id: 3, defaultParams: {
title: 'wayward生存挑战20小时记录', type: articleTypeListOptions[0]?.value,
description: '完整记录wayward的20小时生存挑战过程,从开始到结束的所有精彩瞬间...', sortLogic: sortOptions[0]?.value,
cover: 'https://picsum.photos/400/225?random=3', },
duration: '1:20:15', immediate: false,
author: 'waywardFan',
views: '5.6万',
time: '1周前',
}, },
]) )
const handleSearch = () => { const changeType = (value: ArticleTypeEnum) => {
console.log('搜索关键词:', searchKeyword.value) searchParams.value.type = value
refresh()
} }
const handleTabChange = (value: string) => { const changeSort = (value: number) => {
console.log('切换分类:', value) searchParams.value.sortLogic = value
activeTab.value = value refresh()
} }
</script> const handleSearch = () => {
refresh()
<style scoped>
:deep(.search-input .el-input__wrapper) {
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
} }
:deep(.search-btn) { const handleClick = (item: ArticleListItemDto) => {
background: #3b82f6; if (item.type === ArticleTypeEnum.VIDEO) {
border: none; router.push(`/videoDetail/${item.id}`)
border-radius: 0.5rem; } else {
padding: 0 2rem; router.push(`/postDetail/${item.id}`)
}
} }
.line-clamp-1 { onActivated(() => {
display: -webkit-box; if (route.query.title) {
-webkit-line-clamp: 1; searchParams.value.title = route.query.title as string
-webkit-box-orient: vertical; }
overflow: hidden; if (route.query.type) {
} searchParams.value.type = route.query.type as ArticleTypeEnum
}
refresh()
})
</script>
.line-clamp-2 { <style scoped></style>
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
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