From 59af57c0fa82226a364b8961dbf224827a5c07e3 Mon Sep 17 00:00:00 2001 From: Toyas Date: Fri, 24 Apr 2026 17:35:13 +0800 Subject: [PATCH] refactor(frontend): Rework home and video detail interactions --- frontend/video-frontend-web/src/api/video.ts | 15 + .../video-frontend-web/src/components.d.ts | 1 - .../src/components/comment/CommentSection.vue | 2 +- .../src/components/layout/AppHeader.vue | 696 +++++++++---- .../src/components/video/VideoPlayer.vue | 19 +- frontend/video-frontend-web/src/mock/data.ts | 66 +- frontend/video-frontend-web/src/mock/index.ts | 3 +- .../video-frontend-web/src/router/index.ts | 6 + .../video-frontend-web/src/stores/favorite.ts | 2 +- .../video-frontend-web/src/stores/user.ts | 2 +- .../video-frontend-web/src/stores/video.ts | 31 +- .../video-frontend-web/src/styles/index.scss | 84 +- .../video-frontend-web/src/types/index.ts | 13 + .../video-frontend-web/src/utils/request.ts | 54 +- .../video-frontend-web/src/utils/toast.ts | 67 ++ .../src/views/favorite/detail.vue | 47 +- .../src/views/favorite/index.vue | 3 +- .../src/views/home/category.vue | 245 ++++- .../src/views/home/index.vue | 937 +++++++++--------- .../src/views/login/index.vue | 74 +- .../src/views/login/register.vue | 51 +- .../src/views/message/detail.vue | 2 +- .../src/views/message/index.vue | 2 +- .../src/views/message/notifications.vue | 2 +- .../src/views/search/index.vue | 2 +- .../src/views/upload/index.vue | 2 +- .../src/views/user/followers.vue | 2 +- .../src/views/user/following.vue | 2 +- .../src/views/user/history.vue | 143 ++- .../src/views/user/my-video-detail.vue | 495 +++++++++ .../src/views/user/my-videos.vue | 145 ++- .../src/views/user/notification.vue | 2 +- .../src/views/user/privacy.vue | 2 +- .../src/views/user/profile-edit.vue | 33 +- .../src/views/user/profile.vue | 50 +- .../src/views/user/settings.vue | 3 +- .../src/views/user/videos.vue | 2 +- .../src/views/video/detail.vue | 372 ++++++- 38 files changed, 2693 insertions(+), 986 deletions(-) create mode 100644 frontend/video-frontend-web/src/utils/toast.ts create mode 100644 frontend/video-frontend-web/src/views/user/my-video-detail.vue diff --git a/frontend/video-frontend-web/src/api/video.ts b/frontend/video-frontend-web/src/api/video.ts index bcc0a0e..3d6ba04 100644 --- a/frontend/video-frontend-web/src/api/video.ts +++ b/frontend/video-frontend-web/src/api/video.ts @@ -7,6 +7,7 @@ import type { VideoUploadParams, VideoUploadResult, PlayRecord, + PlayHistoryItem, PageResult } from '@/types' @@ -134,6 +135,20 @@ export const videoApi = { return request.post('/content/video/play', params) }, + /** + * 获取播放历史 + */ + getPlayHistory(params?: { pageNum?: number; pageSize?: number }): Promise> { + return request.get('/content/video/history', params) + }, + + /** + * 清空播放历史 + */ + clearPlayHistory(): Promise { + return request.delete('/content/video/history') + }, + /** * 增加评论数量(内部接口) */ diff --git a/frontend/video-frontend-web/src/components.d.ts b/frontend/video-frontend-web/src/components.d.ts index 73c8aa4..cbfcc6e 100644 --- a/frontend/video-frontend-web/src/components.d.ts +++ b/frontend/video-frontend-web/src/components.d.ts @@ -31,7 +31,6 @@ declare module 'vue' { VanLoading: typeof import('vant/es')['Loading'] VanNavBar: typeof import('vant/es')['NavBar'] VanPicker: typeof import('vant/es')['Picker'] - VanPopover: typeof import('vant/es')['Popover'] VanPopup: typeof import('vant/es')['Popup'] VanProgress: typeof import('vant/es')['Progress'] VanRadio: typeof import('vant/es')['Radio'] diff --git a/frontend/video-frontend-web/src/components/comment/CommentSection.vue b/frontend/video-frontend-web/src/components/comment/CommentSection.vue index c5baf7e..35c4da9 100644 --- a/frontend/video-frontend-web/src/components/comment/CommentSection.vue +++ b/frontend/video-frontend-web/src/components/comment/CommentSection.vue @@ -113,7 +113,7 @@ diff --git a/frontend/video-frontend-web/src/views/login/index.vue b/frontend/video-frontend-web/src/views/login/index.vue index 0598e79..b6da92d 100644 --- a/frontend/video-frontend-web/src/views/login/index.vue +++ b/frontend/video-frontend-web/src/views/login/index.vue @@ -50,28 +50,6 @@ class="login-form" @submit="handleLogin" > -
-
- 安全验证 - 点击验证码图片可刷新,当前登录必须填写验证码。 -
- - - -
- import { onMounted, reactive, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { showSuccessToast, showToast } from 'vant' +import { showSuccessToast, showToast } from '@/utils/toast' import { authApi, type CaptchaResult } from '@/api/auth.ts' import { useUserStore } from '@/stores/user.ts' @@ -199,8 +177,6 @@ const handleLogin = async () => { const redirect = route.query.redirect as string router.replace(redirect || '/') } catch (error: any) { - const message = error?.response?.data?.message || error?.message || '登录失败' - showToast(message) refreshCaptcha() } finally { loading.value = false @@ -336,59 +312,16 @@ onMounted(() => { line-height: 1.7; } -.captcha-banner { - display: flex; - justify-content: space-between; - gap: 12px; - align-items: center; - margin-bottom: 16px; - padding: 14px 16px; - border-radius: 18px; - background: #f8fafc; - - strong { - display: block; - margin-bottom: 4px; - color: #18191c; - font-size: 14px; - } - - span { - color: #64748b; - font-size: 12px; - line-height: 1.6; - } -} - -.captcha-image, .captcha-btn { flex-shrink: 0; cursor: pointer; } -.captcha-image { - width: 120px; - height: 40px; -} - .captcha-btn { width: 100px; height: 32px; } -.captcha-error { - width: 120px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - background: #eef2f7; - color: #64748b; - font-size: 12px; - cursor: pointer; - border-radius: 12px; -} - .login-form { :deep(.van-cell-group--inset) { margin: 0; @@ -451,11 +384,6 @@ onMounted(() => { border-radius: 24px; } - .captcha-banner { - flex-direction: column; - align-items: flex-start; - } - .logo { font-size: 30px; } diff --git a/frontend/video-frontend-web/src/views/login/register.vue b/frontend/video-frontend-web/src/views/login/register.vue index 627b037..87e2d28 100644 --- a/frontend/video-frontend-web/src/views/login/register.vue +++ b/frontend/video-frontend-web/src/views/login/register.vue @@ -47,19 +47,6 @@ -
-
- 注册验证 - 当前注册同样要求验证码,点击图片可以刷新。 -
- -
- import { onMounted, reactive, ref } from 'vue' import { useRouter } from 'vue-router' -import { showSuccessToast, showToast } from 'vant' +import { showSuccessToast, showToast } from '@/utils/toast' import { authApi, type CaptchaResult } from '@/api/auth.ts' const router = useRouter() @@ -198,7 +185,6 @@ const handleRegister = async () => { showSuccessToast('注册成功') router.push('/login') } catch (error: any) { - showToast(error.message || '注册失败') refreshCaptcha() } finally { loading.value = false @@ -333,41 +319,11 @@ onMounted(() => { line-height: 1.7; } -.captcha-banner { - display: flex; - justify-content: space-between; - gap: 12px; - align-items: center; - margin-bottom: 16px; - padding: 14px 16px; - border-radius: 18px; - background: #f8fafc; - - strong { - display: block; - margin-bottom: 4px; - color: #18191c; - font-size: 14px; - } - - span { - color: #64748b; - font-size: 12px; - line-height: 1.6; - } -} - -.captcha-image, .captcha-btn { flex-shrink: 0; cursor: pointer; } -.captcha-image { - width: 120px; - height: 40px; -} - .captcha-btn { width: 100px; height: 32px; @@ -433,11 +389,6 @@ onMounted(() => { border-radius: 24px; } - .captcha-banner { - flex-direction: column; - align-items: flex-start; - } - .logo { font-size: 30px; } diff --git a/frontend/video-frontend-web/src/views/message/detail.vue b/frontend/video-frontend-web/src/views/message/detail.vue index 05e2ae2..775598a 100644 --- a/frontend/video-frontend-web/src/views/message/detail.vue +++ b/frontend/video-frontend-web/src/views/message/detail.vue @@ -60,7 +60,7 @@ diff --git a/frontend/video-frontend-web/src/views/user/my-videos.vue b/frontend/video-frontend-web/src/views/user/my-videos.vue index f572525..946710e 100644 --- a/frontend/video-frontend-web/src/views/user/my-videos.vue +++ b/frontend/video-frontend-web/src/views/user/my-videos.vue @@ -20,22 +20,51 @@
-
+
-
+ +
-
+ +
-
- 异常状态 - {{ exceptionalCount }} -
+ + +
- 内容列表 - {{ videos.length }} 条记录 + {{ currentFilterLabel }} + {{ filteredVideos.length }} 条记录
-
+
+ +
import { computed, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' -import { showToast } from 'vant' +import { showToast } from '@/utils/toast' import { videoApi } from '@/api/video.ts' import { useUserStore } from '@/stores/user.ts' import { formatDuration, formatNumber, formatPlayCount } from '@/utils' @@ -117,16 +154,67 @@ const userStore = useUserStore() const videos = ref([]) const resolvedUserId = ref(null) +const selectedFilter = ref<'all' | number>('all') const defaultCover = 'data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 180\"%3E%3Crect width=\"320\" height=\"180\" fill=\"%23edf2f7\"/%3E%3Ctext x=\"50%25\" y=\"50%25\" dy=\".3em\" text-anchor=\"middle\" fill=\"%239499a0\"%3EHAOVP%3C/text%3E%3C/svg%3E' const publishedCount = computed(() => videos.value.filter((video) => video.status === 1).length) const reviewingCount = computed(() => videos.value.filter((video) => video.status === 0).length) -const exceptionalCount = computed(() => videos.value.filter((video) => video.status !== 0 && video.status !== 1).length) +const offlineCount = computed(() => videos.value.filter((video) => video.status === 2).length) +const rejectedCount = computed(() => videos.value.filter((video) => video.status === 3).length) +const currentFilterLabel = computed(() => { + if (selectedFilter.value === 'all') return '内容列表' + if (selectedFilter.value === 1) return '已发布内容' + if (selectedFilter.value === 0) return '审核中内容' + if (selectedFilter.value === 2) return '已下架内容' + return '审核拒绝内容' +}) const getCoverUrl = (coverUrl?: string) => resolveMediaUrl(coverUrl) || defaultCover +const getVideoTimestamp = (video: VideoInfo) => { + const value = video.publishTime || video.createTime || '' + const time = new Date(value).getTime() + return Number.isNaN(time) ? 0 : time +} + +const toggleFilter = (filter: 'all' | number) => { + if (filter !== 'all' && selectedFilter.value === filter) { + selectedFilter.value = 'all' + return + } + selectedFilter.value = filter +} + +const getPriorityWeight = (video: VideoInfo) => { + if (selectedFilter.value === 'all') { + if (video.status === 0) return 0 + if (video.status === 1) return 1 + return 2 + } + return 0 +} + +const filteredVideos = computed(() => { + let list = [...videos.value] + + if ( + selectedFilter.value === 0 || + selectedFilter.value === 1 || + selectedFilter.value === 2 || + selectedFilter.value === 3 + ) { + list = list.filter((video) => video.status === selectedFilter.value) + } + + return list.sort((a, b) => { + const priorityDiff = getPriorityWeight(a) - getPriorityWeight(b) + if (priorityDiff !== 0) return priorityDiff + return getVideoTimestamp(b) - getVideoTimestamp(a) + }) +}) + const getStatusType = (status: number): 'warning' | 'success' | 'danger' | 'primary' => { const types: Record = { 0: 'warning', @@ -171,7 +259,7 @@ const loadVideos = async () => { } const goToVideo = (id: number) => { - router.push(`/video/${id}`) + router.push(`/my/videos/${id}`) } const initPage = async () => { @@ -258,14 +346,20 @@ onMounted(initPage) .stats-grid { margin-top: 16px; display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 16px; } .stat-card { + appearance: none; + border: 0; + width: 100%; + text-align: left; padding: 18px; display: grid; gap: 8px; + cursor: pointer; + transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease; span { color: #9499a0; @@ -277,6 +371,21 @@ onMounted(initPage) font-size: 28px; line-height: 1; } + + &:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md); + } + + &.active { + background: linear-gradient(135deg, rgba(0, 161, 214, 0.12), rgba(251, 114, 153, 0.12)); + box-shadow: inset 0 0 0 1px rgba(0, 161, 214, 0.22), var(--shadow-md); + + span, + strong { + color: #18191c; + } + } } .videos-panel { diff --git a/frontend/video-frontend-web/src/views/user/notification.vue b/frontend/video-frontend-web/src/views/user/notification.vue index ff400d8..f45cb1d 100644 --- a/frontend/video-frontend-web/src/views/user/notification.vue +++ b/frontend/video-frontend-web/src/views/user/notification.vue @@ -73,7 +73,7 @@