Commit 797a579b by lijiabin

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

parent 53921419
......@@ -9,6 +9,13 @@
@click="handleClickItem(item)"
>
<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">
<!-- 标题 -->
......
......@@ -3,7 +3,7 @@
<!-- tabs -->
<div class="shadow-sm">
<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 -->
<div class="flex items-center space-x-1">
<div
......@@ -25,50 +25,64 @@
</div>
<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
@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"
>
<div class="relative overflow-hidden">
<div class="relative overflow-hidden mb-5">
<img
src="https://picsum.photos/800/400?random=1"
class="w-full h-72 object-cover group-hover:scale-105 transition-transform duration-700"
:src="list[0]?.faceUrl"
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
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
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 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"
>
<SvgIcon name="icon_play" size="14" />
<span class="text-sm font-medium">1.7万</span>
<el-icon class="text-sm"><View /></el-icon>
<span>{{ list[0]?.viewCount }}</span>
</div>
<div
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" />
<span class="text-sm font-medium">112</span>
<el-icon class="text-sm"><ChatDotRound /></el-icon>
<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
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
>
<div
......@@ -76,28 +90,30 @@
>
<SvgIcon name="icon_play" size="24" color="#374151" />
</div>
</div>
</div> -->
</div>
<div class="p-6">
<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>
<p class="text-gray-600 mb-4 line-clamp-2 leading-relaxed">
【色彩搭配】别让配色毁了你的设计!视觉传达专业暑假改指南,3步让作品集秒变高级...
</p>
<h2 class="text-gray-600 mb-4 line-clamp-2 leading-relaxed">
{{ list[0]?.content }}
</h2>
<div class="flex items-center justify-between text-gray-500 text-sm">
<div class="flex items-center gap-2">
<div
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"
>
</div>
<span class="font-medium">平面设计小课堂_柏子</span>
<img
:src="list[0]?.showAvatar"
alt=""
class="w-6 h-6 rounded-full object-cover"
/>
<span class="font-medium">{{ list[0]?.showName }}</span>
</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>
......@@ -105,58 +121,85 @@
<!-- 右侧两个视频 -->
<div class="flex flex-col gap-6">
<div
v-for="n in 2"
:key="n"
v-for="(item, index) in [list[1], list[2]]"
:key="index"
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">
<img
:src="`https://picsum.photos/400/200?random=${n + 1}`"
class="w-full h-36 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/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
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
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
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
>
<!-- 数据 -->
<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"
>
<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
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
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 class="p-4">
<h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
>
{{ n === 1 ? '复日奶茶' : 'PS最新版零基础全套' }}
{{ item?.title }}
</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">
<span class="font-medium">{{ n === 1 ? '设计师的日常' : 'PS教程' }}</span>
<span class="bg-gray-100 px-2 py-1 rounded-full">{{
n === 1 ? '7-29' : '7-27'
<div class="flex items-center gap-2">
<img :src="item?.showAvatar" alt="" class="w-6 h-6 rounded-full object-cover" />
<span class="font-medium">{{ item?.showName }}</span>
</div>
<span>{{
dayjs((item?.createTime ?? 0) * 1000).format('YYYY-MM-DD HH:mm')
}}</span>
</div>
</div>
......@@ -167,7 +210,8 @@
<!-- 剩余视频 - 标准网格 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="item in list"
@click="router.push(`/videoDetail/${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"
>
......@@ -179,17 +223,24 @@
<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"
>
{{ 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
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>
<!-- 数据 -->
......@@ -197,58 +248,54 @@
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<SvgIcon name="icon_play" size="12" />
<span>{{ item.viewCount }}</span>
<el-icon class="text-sm"><View /></el-icon>
<span>{{ item.viewCount }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"><ChatDotRound /></el-icon>
<span>{{ item.replyCount }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<SvgIcon name="icon_comment" size="12" />
<el-icon class="text-sm"><Star /></el-icon>
<span>{{ item.replyCount }}</span>
</div>
</div>
<!-- 播放按钮 -->
<!-- <div
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
>
<div
class="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
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>
<el-icon size="50" color="#333"><VideoPlay /></el-icon>
</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"
>
{{ item.title }}
{{ item?.title }}
</h3>
<p class="text-gray-600 text-sm mb-3 line-clamp-2 leading-relaxed">
{{ item.content }}
</p>
<div class="flex items-center justify-between text-gray-500 text-xs">
<div class="flex items-center gap-2">
<div
class="w-6 h-6 bg-gradient-to-r from-green-400 to-blue-500 rounded-full flex items-center justify-center text-white text-xs font-bold"
>
{{ item.createUserName }}
</div>
<span class="font-medium">{{ item.createUserName }}</span>
<img :src="item?.showAvatar" alt="" class="w-6 h-6 rounded-full object-cover" />
<span class="font-medium">{{ item?.showName }}</span>
</div>
<span class="bg-gray-100 px-2 py-1 rounded-full">{{
dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm')
}}</span>
<span>{{ dayjs(item?.createTime * 1000).format('YYYY-MM-DD HH:mm') }}</span>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- 其他页面 - 标准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
@click="router.push(`/videoDetail/${item.id}`)"
......@@ -264,17 +311,24 @@
<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"
>
{{ 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
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>
<!-- 数据 -->
......@@ -326,7 +380,7 @@
</div>
</div>
</div>
</template>
</div>
<!-- 底部分页 -->
<div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
......@@ -345,7 +399,7 @@
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:page-sizes="[12, 24, 36, 48]"
:page-sizes="[9, 24, 36, 48]"
layout="prev, pager, next, jumper, total"
:total="total"
class="custom-pagination"
......@@ -383,7 +437,7 @@ const { list, total, searchParams, loading, goToPage, changePageSize, refresh }
{
defaultParams: { type: ArticleTypeEnum.VIDEO, sortLogic: 0 },
defaultCurrent: 1,
defaultSize: 12,
defaultSize: 9,
immediate: false,
},
)
......@@ -397,9 +451,6 @@ const toggleTab = (sortLogic: number) => {
refresh()
}
const goVideoDetail = (n: number) => {
router.push(`/videoDetail?id=${n}`)
}
defineExpose({
refresh: () => {
refresh()
......@@ -407,11 +458,4 @@ defineExpose({
})
</script>
<style>
/* 文本截断样式 */
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
<style></style>
......@@ -36,19 +36,19 @@
/>
<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"
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="" />
<div class="text-12px text-#000 line-height-12px">推荐</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 }}
</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 }}
</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">
<span class="flex items-center">
<el-icon class="mr-1"><View /></el-icon>
......
<template>
<div>
<!-- 发布区域 -->
<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-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>
<PublishPractice />
<!-- 标签导航 -->
<div class="bg-white p-4 mb-6 rounded-lg shadow-sm">
......@@ -133,7 +38,7 @@
<div class="flex items-center gap-2">
<div class="w-1 h-6 bg-red-500 rounded"></div>
<h2 class="text-lg font-medium">
{{ tagList.find((tag) => searchParams.tagIdList?.includes(tag.id))?.title ?? '最新' }}
{{ filterText }}
</h2>
</div>
<div
......@@ -143,10 +48,9 @@
查看更多 >>
</div>
</div>
<el-divider />
<el-divider class="my-1!" />
<!-- 动态列表 -->
<div class="divide-y divide-gray-100">
<div class="divide-y bg-#fff">
<div
@click="router.push(`/postDetail/${item.id}`)"
v-for="item in list"
......@@ -156,9 +60,9 @@
<div class="flex gap-3 items-center">
<!-- 左侧内容 -->
<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 }}
</h3>
</h2>
<!-- 带图片的内容 -->
<div class="flex gap-3 mb-3">
......@@ -169,7 +73,7 @@
class="w-20 h-20 object-cover rounded-lg flex-shrink-0"
/>
<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 }}
</div>
</div>
......@@ -239,13 +143,13 @@
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
import { getPracticeList } from '@/api/practice'
import { usePageSearch, useScrollTop } from '@/hooks'
import dayjs from 'dayjs'
import { TABS_REF_KEY } from '@/constants'
import { useTagsStore } from '@/stores/tags'
import PublishPractice from './publishPractice.vue'
const tagsStore = useTagsStore()
const { tagList } = storeToRefs(tagsStore)
......@@ -253,7 +157,6 @@ const { tagList } = storeToRefs(tagsStore)
const router = useRouter()
const tabsRef = inject(TABS_REF_KEY)
const { userInfo } = storeToRefs(useUserStore())
const filterOptions = ref([
{ title: '最热', id: 0 },
......@@ -261,8 +164,6 @@ const filterOptions = ref([
{ title: '最多观看', id: 2 },
])
const tagInput = ref('')
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const { list, total, searchParams, goToPage, changePageSize, refresh } = usePageSearch(
getPracticeList,
......@@ -273,6 +174,15 @@ const { list, total, searchParams, goToPage, changePageSize, refresh } = usePage
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) => {
searchParams.value.sortLogic = id
......@@ -285,6 +195,7 @@ const toggleTag = (id: number) => {
refresh()
handleBackTop()
}
defineExpose({
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 @@
<div class="max-w-4xl mx-auto">
<!-- 主表单卡片 -->
<div class="bg-white rounded-lg shadow-lg p-8">
<el-form ref="formRef" :model="form" label-position="top" size="default">
<!-- 案例编号 -->
<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 ref="formRef" :model="form" label-position="top" :rules="rules">
<!-- 标题输入 -->
<el-form-item class="mb-6" label="标题">
<el-form-item class="mb-6" label="标题" prop="title">
<el-input
v-model="form.title"
placeholder="请输入【案例】标题"
......@@ -27,7 +16,7 @@
</el-form-item>
<!-- 内容输入 -->
<el-form-item class="mb-6 relative" label="内容">
<el-form-item class="mb-6 relative" label="内容" prop="content">
<el-input
v-model="form.content"
type="textarea"
......@@ -38,7 +27,7 @@
/>
</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">
主标签
<SelectTags v-model="form.mainTagId" />
......@@ -56,34 +45,19 @@
</el-form-item>
<!-- 是否同步发布 -->
<el-form-item label="*是否同步发布到实践" class="mb-6">
<el-radio-group v-model="form.publishToPractice">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
<el-form-item label="是否同步发布到实践" class="mb-6" prop="isSync">
<el-radio-group v-model="form.isSync">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</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 gap-4">
<el-button @click="handleCancel"> 取消 </el-button>
<el-button @click="handlePreview"> 预览 </el-button>
<el-button type="info" plain @click="handleSaveDraft"> 存草稿 </el-button>
<!-- <el-button @click="handlePreview"> 预览 </el-button> -->
<el-button type="info" plain @click="handleSubmit"> 存草稿 </el-button>
</div>
<el-button type="primary" @click="handleSubmit"> 提交 </el-button>
</div>
......@@ -94,14 +68,15 @@
</template>
<script setup lang="tsx">
import { QuestionFilled } from '@element-plus/icons-vue'
import { addOrUpdateCase, getMaxCaseNumber } from '@/api'
import { addOrUpdateCase } from '@/api'
import { useResetData } from '@/hooks'
import type { AddOrUpdateCaseDto } from '@/api'
import SelectTags from '@/components/common/SelectTags/index.vue'
import type { FormInstance } from 'element-plus'
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')
......@@ -110,31 +85,35 @@ type FormData = Omit<AddOrUpdateCaseDto, 'tagRelationDtoList'> & {
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: '',
content: '',
number: '',
mainTagId: '',
subTagIds: [],
isSync: BooleanFlag.NO,
releaseStatus: ReleaseStatusTypeEnum.DRAFT,
})
// 取消
const handleCancel = () => {
ElMessage.warning('确定要取消吗?')
// 可以添加确认对话框
resetForm()
router.back()
}
// 预览
const handlePreview = () => {
console.log('预览', form)
ElMessage.success('预览功能待实现')
}
// const handlePreview = () => {
// console.log('预览', form)
// ElMessage.success('预览功能待实现')
// }
// 保存草稿
const handleSaveDraft = () => {
console.log('保存草稿', form)
ElMessage.success('草稿保存成功')
}
const transformData = (formData: FormData): AddOrUpdateCaseDto => {
const { mainTagId, subTagIds, ...rest } = formData
......@@ -142,6 +121,7 @@ const transformData = (formData: FormData): AddOrUpdateCaseDto => {
...rest,
tagRelationDtoList: [],
}
// 添加标签内容
obj.tagRelationDtoList.push({
tagId: Number(mainTagId),
type: TagTypeEnum.CULTURE_TAG,
......@@ -163,6 +143,8 @@ const handleSubmit = async () => {
const res = await addOrUpdateCase(transformData(form.value))
if (res) {
ElMessage.success('提交成功')
resetForm()
router.back()
}
}
......@@ -230,14 +212,8 @@ const filterTagsFn = (allTags: TagItemDto[]) => {
return allTags.filter((tag) => tag.id !== Number(form.value.mainTagId))
}
const fetchMaxCaseNumber = async () => {
const { data } = await getMaxCaseNumber()
form.value.number = data
}
onActivated(() => {
showSubmissionGuide()
fetchMaxCaseNumber()
})
</script>
......
......@@ -12,9 +12,10 @@
<h3 class="text-xl font-bold text-gray-800">上传视频</h3>
</div>
<el-form-item prop="videoUrl">
<UploadVideo v-model="form.videoUrl" />
<UploadVideo v-model="form.videoUrl" @uploadSuccess="handleVideoChange" />
</el-form-item>
</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"
......@@ -25,49 +26,44 @@
<!-- 封面设置 -->
<div class="mb-8">
<label class="block text-sm font-semibold text-gray-700 mb-4">封面选择</label>
<div class="flex gap-6 items-start">
<!-- 主封面 -->
<div class="relative group">
<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"
>
<img
src="@/assets/img/culture/ask.png"
alt="主封面"
class="w-full h-full object-cover"
/>
<div
class="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
></div>
<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"
>
智能封面
<el-form-item prop="faceUrl">
<div class="">
<label class="block text-sm font-semibold text-gray-700 mb-3">封面选择</label>
<div class="flex gap-6">
<!-- 主封面预览 -->
<div class="relative group">
<div
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"
@click="showCoverDialog = true"
>
<img
v-if="form.faceUrl"
:src="form.faceUrl"
alt="视频封面"
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="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 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>
</el-form-item>
</div>
<!-- 标题 -->
......@@ -79,11 +75,11 @@
placeholder="请输入视频标题..."
maxlength="80"
show-word-limit
size="large"
class="title-input"
/>
</el-form-item>
</div>
<div class="mb-8">
<el-form-item prop="content">
<label class="block text-sm font-semibold text-gray-700 mb-3">视频简介</label>
......@@ -95,43 +91,44 @@
/>
</el-form-item>
</div>
<div class="mb-8">
<el-form-item prop="mainTagId">
<label class="block text-sm font-semibold text-gray-700 mb-3"
>主标签
<el-tooltip content="添加相关标签可以帮助更多人发现您的作品" placement="top">
<el-icon class="text-gray-400 cursor-help"
><QuestionFilled
/></el-icon> </el-tooltip
></label>
<label class="block text-sm font-semibold text-gray-700 mb-3">
主标签
<el-tooltip content="主标签最多添加一个" placement="top">
<el-icon class="text-gray-400 cursor-help"><QuestionFilled /></el-icon>
</el-tooltip>
</label>
<SelectTags class="w-full" v-model="form.mainTagId" :max-selected-tags="1" />
</el-form-item>
</div>
<div class="mb-8">
<el-form-item prop="mainTagId">
<label class="block text-sm font-semibold text-gray-700 mb-3"
>副标签
<el-tooltip content="副标签可以添加多个,最多3个" placement="top">
<el-icon class="text-gray-400 cursor-help"
><QuestionFilled
/></el-icon> </el-tooltip
></label>
<el-form-item prop="tagList">
<label class="block text-sm font-semibold text-gray-700 mb-3">
副标签
<el-tooltip content="副标签最多添加3个" placement="top">
<el-icon class="text-gray-400 cursor-help"><QuestionFilled /></el-icon>
</el-tooltip>
</label>
<SelectTags
class="w-full"
v-model="form.tagList"
v-model="form.tagList as number[]"
:max-selected-tags="3"
:filter-tags-fn="filterTagsFn"
/>
</el-form-item>
</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"
>
<h4 class="text-lg font-bold text-gray-800 mb-4">发布设置</h4>
<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 class="text-sm font-medium text-gray-800">定时发布</div>
<div class="text-xs text-gray-500">设置发布时间</div>
......@@ -160,6 +157,7 @@
</div>
</div>
</div>
<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">
<el-button
......@@ -169,7 +167,6 @@
<el-icon class="mr-2"><Document /></el-icon>
保存草稿
</el-button>
<span></span>
<el-button
type="primary"
size="large"
......@@ -184,36 +181,87 @@
</div>
</div>
</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">
<el-icon class="text-amber-500 mt-1"><Warning /></el-icon>
<div>
<h5 class="text-sm font-semibold text-amber-800 mb-2">发布小贴士</h5>
<ul class="text-xs text-amber-700 space-y-1">
<li>• 选择合适的封面能提高点击率</li>
<li>• 添加相关标签帮助推荐</li>
<li>• 详细的简介让观众更了解内容</li>
</ul>
<div v-if="locationVideoBlolUrl" class="space-y-6">
<!-- 视频预览 -->
<div class="relative">
<video
ref="videoRef"
:src="locationVideoBlolUrl"
class="w-full max-h-96 rounded-lg bg-black"
@loadedmetadata="onVideoLoaded"
@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> -->
<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>
</template>
<script setup lang="ts">
import UploadVideo from '@/components/common/UploadVideo/index.vue'
import { useResetData } from '@/hooks'
import { ArticleTypeEnum, ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants'
import { addOrUpdateArticle } from '@/api'
import { addOrUpdateArticle, uploadFile } from '@/api'
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 [form] = useResetData({
const [form, resetData] = useResetData<AddOrUpdateVideoDto>({
videoUrl: '',
title: '视频标题',
type: ArticleTypeEnum.VIDEO,
......@@ -223,8 +271,89 @@ const [form] = useResetData({
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
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[]) => {
return tags.filter((tag) => tag.id !== Number(form.value.mainTagId))
}
......@@ -233,7 +362,8 @@ const rules = {
videoUrl: [{ required: true, message: '请上传视频', trigger: 'change' }],
title: [{ 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 = () => {
......@@ -244,17 +374,59 @@ const tansformData = () => {
sort: index,
})),
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 () => {
await formRef.value?.validate()
const res = await addOrUpdateArticle(tansformData())
if (res) {
loading.value = true
try {
addOrUpdateArticle(tansformData())
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>
<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>
<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="mb-8">
<div class="mb-1 p-4">
<div class="relative flex items-center gap-3">
<el-input
v-model="searchKeyword"
v-model="searchParams.title"
placeholder="输入关键词搜索"
size="large"
class="search-input flex-1"
class="w-300px! flex-1"
@keyup.enter="handleSearch"
clearable
/>
<el-button type="primary" size="large" class="search-btn" @click="handleSearch">
搜索
</el-button>
<el-button type="primary" @click="handleSearch"> 搜索 </el-button>
</div>
</div>
<!-- 分类 Tabs -->
<div class="mb-6 flex gap-3 flex items-center">
<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 justify-between mb-6 border-b border-gray-200 p-4">
<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">
<button
v-for="sort in sortOptions"
v-for="sort in articleTypeListOptions"
: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="{
'text-blue-600 bg-blue-50': activeSort === sort.value,
'text-gray-600 hover:text-blue-600': activeSort !== sort.value,
'text-blue-600 bg-blue-50': searchParams.type === sort.value,
'text-gray-600 hover:text-blue-600': searchParams.type !== sort.value,
}"
@click="activeSort = sort.value"
@click="changeType(sort.value)"
>
{{ sort.label }}
</button>
</div>
</div>
<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
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 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 v-show="list.length">
<div class="space-y-4">
<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
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 class="flex-1 flex flex-col justify-between min-w-0">
<div>
<!-- 标题 -->
<h3 class="text-16px font-500 text-gray-900 mb-2 line-clamp-1">
{{ item.title }}
</h3>
<!-- 描述 -->
<p class="text-14px text-gray-600 mb-3 line-clamp-2">
{{ item.description }}
</p>
<!-- 内容信息 -->
<div class="flex-1 flex flex-col justify-between min-w-0">
<div>
<!-- 标题 -->
<h3 class="text-16px font-500 text-gray-900 mb-2 line-clamp-1">
{{ item.title }}
</h3>
<!-- 描述 -->
<p class="text-14px text-gray-600 mb-3 line-clamp-2">
{{ item.content }}
</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 class="flex items-center gap-4 text-13px text-gray-500">
<span>{{ item.author }}</span>
<span>{{ item.views }}</span>
<span>{{ item.time }}</span>
<div class="flex-shrink-0 self-end">
<span class="text-blue-600 text-13px">{{
articleTypeListOptions.find((i) => i.value === item.type)?.label
}}</span>
</div>
</div>
</div>
<!-- 右侧视频标签 -->
<div class="flex-shrink-0 self-end">
<span class="text-blue-600 text-13px">视频</span>
<div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
<div class="max-w-7xl mx-auto py-6">
<div class="flex items-center justify-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 class="mt-8 flex justify-center">
<el-pagination
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 v-show="!list.length">
<div class="flex items-center justify-center h-full">
<el-empty description="暂无数据" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Tabs from '@/components/common/Tabs'
interface SearchResult {
id: number
title: string
description: string
cover: string
duration: string
author: string
views: string
time: string
}
const searchKeyword = ref('wayward')
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 },
import { articleTypeListOptions } from '@/constants'
import { getArticleList } from '@/api/article'
import { usePageSearch, useScrollTop } from '@/hooks'
import { ArticleTypeEnum } from '@/constants'
import dayjs from 'dayjs'
import type { ArticleListItemDto } from '@/api'
const router = useRouter()
const route = useRoute()
const searchPageRef = ref<HTMLElement | null>(null)
const { ScrollTopComp, handleBackTop } = useScrollTop(searchPageRef)
const sortOptions = [
{ label: '最热', value: 0 },
{ label: '最新', value: 1 },
]
const activeTab = ref('综合')
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天前',
},
const { total, goToPage, changePageSize, refresh, searchParams, list } = usePageSearch(
getArticleList,
{
id: 3,
title: 'wayward生存挑战20小时记录',
description: '完整记录wayward的20小时生存挑战过程,从开始到结束的所有精彩瞬间...',
cover: 'https://picsum.photos/400/225?random=3',
duration: '1:20:15',
author: 'waywardFan',
views: '5.6万',
time: '1周前',
defaultParams: {
type: articleTypeListOptions[0]?.value,
sortLogic: sortOptions[0]?.value,
},
immediate: false,
},
])
)
const handleSearch = () => {
console.log('搜索关键词:', searchKeyword.value)
const changeType = (value: ArticleTypeEnum) => {
searchParams.value.type = value
refresh()
}
const handleTabChange = (value: string) => {
console.log('切换分类:', value)
activeTab.value = value
const changeSort = (value: number) => {
searchParams.value.sortLogic = value
refresh()
}
</script>
<style scoped>
:deep(.search-input .el-input__wrapper) {
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
const handleSearch = () => {
refresh()
}
:deep(.search-btn) {
background: #3b82f6;
border: none;
border-radius: 0.5rem;
padding: 0 2rem;
const handleClick = (item: ArticleListItemDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`)
} else {
router.push(`/postDetail/${item.id}`)
}
}
.line-clamp-1 {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
onActivated(() => {
if (route.query.title) {
searchParams.value.title = route.query.title as string
}
if (route.query.type) {
searchParams.value.type = route.query.type as ArticleTypeEnum
}
refresh()
})
</script>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
<style scoped></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