|
@@ -1,6 +1,62 @@
|
|
|
<template>
|
|
<template>
|
|
|
<section class="space-y-6">
|
|
<section class="space-y-6">
|
|
|
|
|
+ <!-- 搜索框 - 固定在Header下方 -->
|
|
|
<div
|
|
<div
|
|
|
|
|
+ class="sticky top-16 z-30 bg-surface/90 backdrop-blur supports-[backdrop-filter]:bg-surface/70 border-b border-white/5 pb-3"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="flex gap-3">
|
|
|
|
|
+ <div class="relative flex-1">
|
|
|
|
|
+ <input
|
|
|
|
|
+ v-model="searchKeyword"
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ placeholder="搜索视频..."
|
|
|
|
|
+ class="w-full px-4 py-3 pr-12 rounded-xl bg-white/5 border border-white/10 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500/50 transition"
|
|
|
|
|
+ @keyup.enter="() => handleSearch()"
|
|
|
|
|
+ />
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="searchKeyword"
|
|
|
|
|
+ @click="clearSearch"
|
|
|
|
|
+ class="absolute right-3 top-1/2 transform -translate-y-1/2 text-white/50 hover:text-white/80 transition"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg
|
|
|
|
|
+ class="w-5 h-5"
|
|
|
|
|
+ fill="none"
|
|
|
|
|
+ stroke="currentColor"
|
|
|
|
|
+ viewBox="0 0 24 24"
|
|
|
|
|
+ >
|
|
|
|
|
+ <path
|
|
|
|
|
+ stroke-linecap="round"
|
|
|
|
|
+ stroke-linejoin="round"
|
|
|
|
|
+ stroke-width="2"
|
|
|
|
|
+ d="M6 18L18 6M6 6l12 12"
|
|
|
|
|
+ />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="() => handleSearch()"
|
|
|
|
|
+ class="px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white hover:border-white/20 transition"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg
|
|
|
|
|
+ class="w-5 h-5"
|
|
|
|
|
+ fill="none"
|
|
|
|
|
+ stroke="currentColor"
|
|
|
|
|
+ viewBox="0 0 24 24"
|
|
|
|
|
+ >
|
|
|
|
|
+ <path
|
|
|
|
|
+ stroke-linecap="round"
|
|
|
|
|
+ stroke-linejoin="round"
|
|
|
|
|
+ stroke-width="2"
|
|
|
|
|
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
|
|
|
+ />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 为你推荐卡片 -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="!isSearchMode"
|
|
|
class="rounded-2xl bg-gradient-to-br from-brand to-emerald-500 text-slate-900 p-4 shadow-lg"
|
|
class="rounded-2xl bg-gradient-to-br from-brand to-emerald-500 text-slate-900 p-4 shadow-lg"
|
|
|
>
|
|
>
|
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center gap-3">
|
|
@@ -27,37 +83,632 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="flex flex-wrap gap-2">
|
|
|
|
|
- <button class="chip">热门</button>
|
|
|
|
|
- <button class="chip">国产</button>
|
|
|
|
|
- <button class="chip">日韩</button>
|
|
|
|
|
- <button class="chip">最新</button>
|
|
|
|
|
- <button class="chip">高分</button>
|
|
|
|
|
|
|
+ <!-- 动态标签栏 -->
|
|
|
|
|
+ <div v-if="!isSearchMode" class="relative">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="flex flex-wrap gap-2 transition-all duration-300"
|
|
|
|
|
+ :class="{ 'max-h-20 overflow-hidden': !showAllTags }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="menu in videoMenus"
|
|
|
|
|
+ :key="menu.hash"
|
|
|
|
|
+ class="chip"
|
|
|
|
|
+ :class="{ 'chip-active': selectedMenu === menu.hash }"
|
|
|
|
|
+ @click="selectMenu(menu.hash)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ menu.name }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 展开/收起按钮 -->
|
|
|
|
|
+ <div v-if="videoMenus.length > 6" class="mt-2 text-center">
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="toggleTags"
|
|
|
|
|
+ class="text-xs text-white/60 hover:text-white/80 transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ showAllTags ? "收起" : `展开更多 (${videoMenus.length - 6})` }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- 排序选择栏 -->
|
|
|
|
|
+ <div v-if="!isSearchMode" class="flex flex-wrap gap-1.5">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="sortOption in sortOptions"
|
|
|
|
|
+ :key="sortOption.value"
|
|
|
|
|
+ class="sort-chip"
|
|
|
|
|
+ :class="{ 'sort-chip-active': selectedSort === sortOption.value }"
|
|
|
|
|
+ @click="selectSort(sortOption.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ sortOption.label }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 视频列表 -->
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="grid grid-cols-2 gap-3">
|
|
|
<article
|
|
<article
|
|
|
- v-for="i in 8"
|
|
|
|
|
- :key="i"
|
|
|
|
|
- class="group rounded-2xl overflow-hidden bg-white/5 border border-white/10"
|
|
|
|
|
|
|
+ v-for="video in videoList"
|
|
|
|
|
+ :key="video.id"
|
|
|
|
|
+ @click="playVideo(video)"
|
|
|
|
|
+ class="group rounded-2xl overflow-hidden bg-white/5 border border-white/10 cursor-pointer hover:bg-white/10 transition"
|
|
|
>
|
|
>
|
|
|
- <div
|
|
|
|
|
- class="aspect-[9/12] bg-gradient-to-br from-slate-700/60 to-slate-800/60 group-hover:from-slate-600/60 transition"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div class="aspect-[9/12] relative">
|
|
|
|
|
+ <img
|
|
|
|
|
+ :src="video.cover"
|
|
|
|
|
+ :alt="video.name"
|
|
|
|
|
+ class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
|
|
|
|
+ @error="handleImageError"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ formatDuration(video.duration) }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
<div class="p-3">
|
|
<div class="p-3">
|
|
|
<h3 class="text-[15px] font-medium text-white/90 truncate">
|
|
<h3 class="text-[15px] font-medium text-white/90 truncate">
|
|
|
- 精选条目 {{ i }}
|
|
|
|
|
|
|
+ {{ video.name }}
|
|
|
</h3>
|
|
</h3>
|
|
|
<p class="text-xs text-white/50 mt-0.5 truncate">
|
|
<p class="text-xs text-white/50 mt-0.5 truncate">
|
|
|
- 高清 · 热门 · 推荐
|
|
|
|
|
|
|
+ {{ video.view }} 次观看 · {{ video.like }} 点赞
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
</article>
|
|
</article>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 加载状态 -->
|
|
|
|
|
+ <div v-if="loading" class="text-center py-8">
|
|
|
|
|
+ <div class="text-white/50">加载中...</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 空状态 -->
|
|
|
|
|
+ <div v-if="!loading && videoList.length === 0" class="text-center py-8">
|
|
|
|
|
+ <div class="text-white/50">暂无视频内容</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 分页组件 -->
|
|
|
|
|
+ <div v-if="totalPages > 1" class="py-6">
|
|
|
|
|
+ <!-- 桌面端:完整分页 -->
|
|
|
|
|
+ <div class="hidden sm:block">
|
|
|
|
|
+ <!-- 上一页、页码、下一页 -->
|
|
|
|
|
+ <div class="flex justify-between items-center gap-2 mb-3">
|
|
|
|
|
+ <!-- 上一页按钮 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(currentPage - 1)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, currentPage - 1)
|
|
|
|
|
+ "
|
|
|
|
|
+ :disabled="currentPage <= 1"
|
|
|
|
|
+ class="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ <
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 页码区域 -->
|
|
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
|
|
+ <!-- 首页 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="currentPage > 3"
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(1)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, 1)
|
|
|
|
|
+ "
|
|
|
|
|
+ class="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ 1
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 省略号 -->
|
|
|
|
|
+ <span v-if="currentPage > 4" class="text-white/50 text-sm"
|
|
|
|
|
+ >...</span
|
|
|
|
|
+ >
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 当前页前后 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="currentPage > 1"
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(currentPage - 1)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, currentPage - 1)
|
|
|
|
|
+ "
|
|
|
|
|
+ class="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ currentPage - 1 }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 当前页 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="px-3 py-2 rounded-lg bg-emerald-500/20 text-emerald-400 border border-emerald-400/30 text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ currentPage }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 当前页后 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="currentPage < totalPages"
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(currentPage + 1)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, currentPage + 1)
|
|
|
|
|
+ "
|
|
|
|
|
+ class="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ currentPage + 1 }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 省略号 -->
|
|
|
|
|
+ <span
|
|
|
|
|
+ v-if="currentPage < totalPages - 3"
|
|
|
|
|
+ class="text-white/50 text-sm"
|
|
|
|
|
+ >...</span
|
|
|
|
|
+ >
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 跳转输入框(在省略号和末页之间) -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="currentPage < totalPages - 3"
|
|
|
|
|
+ class="flex items-center gap-1"
|
|
|
|
|
+ >
|
|
|
|
|
+ <input
|
|
|
|
|
+ v-model="jumpToPage"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ :min="1"
|
|
|
|
|
+ :max="totalPages"
|
|
|
|
|
+ class="w-10 px-1 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm text-center focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500/50"
|
|
|
|
|
+ @keyup.enter="jumpToPageHandler"
|
|
|
|
|
+ @blur="jumpToPageHandler"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 末页 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="currentPage < totalPages - 2"
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(totalPages)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, totalPages)
|
|
|
|
|
+ "
|
|
|
|
|
+ class="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ totalPages }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 下一页按钮 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(currentPage + 1)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, currentPage + 1)
|
|
|
|
|
+ "
|
|
|
|
|
+ :disabled="currentPage >= totalPages"
|
|
|
|
|
+ class="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ >
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 页码信息 -->
|
|
|
|
|
+ <div class="text-center">
|
|
|
|
|
+ <div class="text-sm text-white/50">
|
|
|
|
|
+ 第 {{ currentPage }} 页,共 {{ totalPages }} 页
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 移动端:动态分页 -->
|
|
|
|
|
+ <div class="sm:hidden">
|
|
|
|
|
+ <div class="flex justify-between items-center gap-1 mb-3">
|
|
|
|
|
+ <!-- 上一页按钮 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(currentPage - 1)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, currentPage - 1)
|
|
|
|
|
+ "
|
|
|
|
|
+ :disabled="currentPage <= 1"
|
|
|
|
|
+ class="px-2 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ <
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 页码区域 -->
|
|
|
|
|
+ <div class="flex items-center gap-1 flex-1 justify-center">
|
|
|
|
|
+ <!-- 首页 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="currentPage > 2"
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(1)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, 1)
|
|
|
|
|
+ "
|
|
|
|
|
+ class="px-2 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ 1
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 省略号 -->
|
|
|
|
|
+ <span v-if="currentPage > 3" class="text-white/50 text-sm"
|
|
|
|
|
+ >...</span
|
|
|
|
|
+ >
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 当前页前 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="currentPage > 1"
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(currentPage - 1)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, currentPage - 1)
|
|
|
|
|
+ "
|
|
|
|
|
+ class="px-2 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ currentPage - 1 }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 当前页 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="px-2 py-2 rounded-lg bg-emerald-500/20 text-emerald-400 border border-emerald-400/30 text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ currentPage }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 当前页后 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="currentPage < totalPages"
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(currentPage + 1)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, currentPage + 1)
|
|
|
|
|
+ "
|
|
|
|
|
+ class="px-2 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ currentPage + 1 }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 省略号 -->
|
|
|
|
|
+ <span
|
|
|
|
|
+ v-if="currentPage < totalPages - 2"
|
|
|
|
|
+ class="text-white/50 text-sm"
|
|
|
|
|
+ >...</span
|
|
|
|
|
+ >
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 末页 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="currentPage < totalPages - 1"
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(totalPages)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, totalPages)
|
|
|
|
|
+ "
|
|
|
|
|
+ class="px-2 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ totalPages }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 跳转输入框 -->
|
|
|
|
|
+ <input
|
|
|
|
|
+ v-model="jumpToPage"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ :min="1"
|
|
|
|
|
+ :max="totalPages"
|
|
|
|
|
+ class="w-8 px-1 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-xs text-center focus:outline-none focus:ring-1 focus:ring-emerald-500/50 focus:border-emerald-500/50"
|
|
|
|
|
+ @keyup.enter="jumpToPageHandler"
|
|
|
|
|
+ @blur="jumpToPageHandler"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 下一页按钮 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="
|
|
|
|
|
+ isSearchMode
|
|
|
|
|
+ ? handleSearch(currentPage + 1)
|
|
|
|
|
+ : loadVideosByTag(selectedMenu, currentPage + 1)
|
|
|
|
|
+ "
|
|
|
|
|
+ :disabled="currentPage >= totalPages"
|
|
|
|
|
+ class="px-2 py-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed transition text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ >
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 页码信息 -->
|
|
|
|
|
+ <div class="text-center">
|
|
|
|
|
+ <div class="text-sm text-white/50">
|
|
|
|
|
+ 第 {{ currentPage }} 页,共 {{ totalPages }} 页
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</section>
|
|
</section>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, onMounted } from "vue";
|
|
|
|
|
+import { useRouter } from "vue-router";
|
|
|
|
|
+import {
|
|
|
|
|
+ getVideoMenu,
|
|
|
|
|
+ searchVideoByTags,
|
|
|
|
|
+ searchVideoByKeyword,
|
|
|
|
|
+} from "@/services/api";
|
|
|
|
|
+
|
|
|
|
|
+// 路由
|
|
|
|
|
+const router = useRouter();
|
|
|
|
|
+
|
|
|
|
|
+// 生成MAC地址作为设备标识
|
|
|
|
|
+const generateMacAddress = (): string => {
|
|
|
|
|
+ const hex = "0123456789ABCDEF";
|
|
|
|
|
+ let mac = "";
|
|
|
|
|
+ for (let i = 0; i < 6; i++) {
|
|
|
|
|
+ if (i > 0) mac += ":";
|
|
|
|
|
+ mac += hex[Math.floor(Math.random() * 16)];
|
|
|
|
|
+ mac += hex[Math.floor(Math.random() * 16)];
|
|
|
|
|
+ }
|
|
|
|
|
+ return mac;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 视频菜单数据
|
|
|
|
|
+const videoMenus = ref<any[]>([]);
|
|
|
|
|
+const selectedMenu = ref<string>("");
|
|
|
|
|
+const device = generateMacAddress();
|
|
|
|
|
+
|
|
|
|
|
+// 视频列表数据
|
|
|
|
|
+const videoList = ref<any[]>([]);
|
|
|
|
|
+const loading = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+// 分页数据
|
|
|
|
|
+const currentPage = ref(1);
|
|
|
|
|
+const pageSize = ref(6);
|
|
|
|
|
+const totalPages = ref(0);
|
|
|
|
|
+const totalCount = ref(0);
|
|
|
|
|
+const jumpToPage = ref<number | string>("");
|
|
|
|
|
+
|
|
|
|
|
+// 排序选项
|
|
|
|
|
+const sortOptions = ref([
|
|
|
|
|
+ { value: "time", label: "最新" },
|
|
|
|
|
+ { value: "view", label: "热门" },
|
|
|
|
|
+ { value: "like", label: "点赞" },
|
|
|
|
|
+]);
|
|
|
|
|
+const selectedSort = ref<string>("time");
|
|
|
|
|
+
|
|
|
|
|
+// 标签展开状态
|
|
|
|
|
+const showAllTags = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+// 搜索相关
|
|
|
|
|
+const searchKeyword = ref("");
|
|
|
|
|
+const isSearchMode = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+// 格式化时长
|
|
|
|
|
+const formatDuration = (duration: string | number): string => {
|
|
|
|
|
+ const seconds = parseInt(String(duration));
|
|
|
|
|
+ const minutes = Math.floor(seconds / 60);
|
|
|
|
|
+ const remainingSeconds = seconds % 60;
|
|
|
|
|
+ return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 处理图片加载错误
|
|
|
|
|
+const handleImageError = (event: Event) => {
|
|
|
|
|
+ const img = event.target as HTMLImageElement;
|
|
|
|
|
+ img.src =
|
|
|
|
|
+ "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjMzMzIi8+CjxwYXRoIGQ9Ik0xMDAgNzVMMTI1IDEwMEgxMDBWMTI1SDc1TDEwMCAxMDBaIiBmaWxsPSIjNjY2Ii8+Cjwvc3ZnPgo=";
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 跳转到指定页面
|
|
|
|
|
+const jumpToPageHandler = () => {
|
|
|
|
|
+ const inputValue = String(jumpToPage.value).trim();
|
|
|
|
|
+
|
|
|
|
|
+ // 检查输入是否为空
|
|
|
|
|
+ if (!inputValue) {
|
|
|
|
|
+ console.log("输入框为空,不执行跳转");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const page = parseInt(inputValue);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否为有效数字
|
|
|
|
|
+ if (isNaN(page)) {
|
|
|
|
|
+ console.log("输入的不是有效数字:", inputValue);
|
|
|
|
|
+ jumpToPage.value = "";
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查页码范围
|
|
|
|
|
+ if (page < 1 || page > totalPages.value) {
|
|
|
|
|
+ console.log(`页码超出范围: ${page}, 总页数: ${totalPages.value}`);
|
|
|
|
|
+ jumpToPage.value = "";
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否与当前页相同
|
|
|
|
|
+ if (page === currentPage.value) {
|
|
|
|
|
+ console.log("跳转到当前页,无需操作");
|
|
|
|
|
+ jumpToPage.value = "";
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(
|
|
|
|
|
+ `准备跳转到第 ${page} 页,当前页: ${currentPage.value}, 总页数: ${totalPages.value}`
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (isSearchMode.value) {
|
|
|
|
|
+ console.log("搜索模式,执行搜索跳转");
|
|
|
|
|
+ handleSearch(page);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log("标签模式,执行标签跳转");
|
|
|
|
|
+ loadVideosByTag(selectedMenu.value || "", page);
|
|
|
|
|
+ }
|
|
|
|
|
+ jumpToPage.value = "";
|
|
|
|
|
+ console.log("跳转完成");
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("跳转失败:", error);
|
|
|
|
|
+ jumpToPage.value = "";
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 选择菜单并加载视频
|
|
|
|
|
+const selectMenu = async (hash: string) => {
|
|
|
|
|
+ selectedMenu.value = hash;
|
|
|
|
|
+ await loadVideosByTag(hash, 1);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 选择排序方式
|
|
|
|
|
+const selectSort = async (sort: string) => {
|
|
|
|
|
+ selectedSort.value = sort;
|
|
|
|
|
+ // 重新查询当前显示的内容
|
|
|
|
|
+ if (isSearchMode.value) {
|
|
|
|
|
+ // 如果是搜索模式,重新执行搜索
|
|
|
|
|
+ await handleSearch(1);
|
|
|
|
|
+ } else if (selectedMenu.value) {
|
|
|
|
|
+ // 如果选择了特定菜单,按菜单查询
|
|
|
|
|
+ await loadVideosByTag(selectedMenu.value, 1);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果没有选择菜单,查询所有视频
|
|
|
|
|
+ await loadVideosByTag("", 1);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 切换标签展开状态
|
|
|
|
|
+const toggleTags = () => {
|
|
|
|
|
+ showAllTags.value = !showAllTags.value;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 搜索功能
|
|
|
|
|
+const handleSearch = async (page = 1) => {
|
|
|
|
|
+ if (!searchKeyword.value.trim()) return;
|
|
|
|
|
+
|
|
|
|
|
+ isSearchMode.value = true;
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ currentPage.value = page;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await searchVideoByKeyword(
|
|
|
|
|
+ device,
|
|
|
|
|
+ searchKeyword.value.trim(),
|
|
|
|
|
+ page,
|
|
|
|
|
+ pageSize.value,
|
|
|
|
|
+ "long",
|
|
|
|
|
+ "zh_CN"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (response.status === 0 && response.data?.list) {
|
|
|
|
|
+ videoList.value = response.data.list;
|
|
|
|
|
+ // 更新分页信息
|
|
|
|
|
+ if (response.data.pageInfo) {
|
|
|
|
|
+ totalPages.value = response.data.pageInfo.pageCount || 0;
|
|
|
|
|
+ totalCount.value = response.data.pageInfo.pageTotal || 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ videoList.value = [];
|
|
|
|
|
+ totalPages.value = 0;
|
|
|
|
|
+ totalCount.value = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("搜索视频失败:", error);
|
|
|
|
|
+ videoList.value = [];
|
|
|
|
|
+ totalPages.value = 0;
|
|
|
|
|
+ totalCount.value = 0;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 清除搜索
|
|
|
|
|
+const clearSearch = () => {
|
|
|
|
|
+ searchKeyword.value = "";
|
|
|
|
|
+ isSearchMode.value = false;
|
|
|
|
|
+ // 恢复默认显示(加载所有视频)
|
|
|
|
|
+ loadVideosByTag("");
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 播放视频
|
|
|
|
|
+const playVideo = (video: any) => {
|
|
|
|
|
+ router.push({
|
|
|
|
|
+ name: "VideoPlayer",
|
|
|
|
|
+ params: { id: video.id },
|
|
|
|
|
+ query: {
|
|
|
|
|
+ name: video.name,
|
|
|
|
|
+ cover: video.cover,
|
|
|
|
|
+ m3u8: video.m3u8,
|
|
|
|
|
+ duration: video.duration,
|
|
|
|
|
+ view: video.view,
|
|
|
|
|
+ like: video.like,
|
|
|
|
|
+ time: video.time,
|
|
|
|
|
+ taginfo: JSON.stringify(video.taginfo || []),
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 根据标签加载视频
|
|
|
|
|
+const loadVideosByTag = async (tagHash: string, page = 1) => {
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ currentPage.value = page;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await searchVideoByTags(
|
|
|
|
|
+ device,
|
|
|
|
|
+ page,
|
|
|
|
|
+ pageSize.value,
|
|
|
|
|
+ tagHash || undefined,
|
|
|
|
|
+ "long",
|
|
|
|
|
+ selectedSort.value as "view" | "like" | "time"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (response.status === 0 && response.data?.list) {
|
|
|
|
|
+ videoList.value = response.data.list;
|
|
|
|
|
+ // 更新分页信息
|
|
|
|
|
+ if (response.data.pageInfo) {
|
|
|
|
|
+ totalPages.value = response.data.pageInfo.pageCount || 0;
|
|
|
|
|
+ totalCount.value = response.data.pageInfo.pageTotal || 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ videoList.value = [];
|
|
|
|
|
+ totalPages.value = 0;
|
|
|
|
|
+ totalCount.value = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("获取视频列表失败:", error);
|
|
|
|
|
+ videoList.value = [];
|
|
|
|
|
+ totalPages.value = 0;
|
|
|
|
|
+ totalCount.value = 0;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取视频菜单
|
|
|
|
|
+const fetchVideoMenus = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await getVideoMenu(device, "1", 1, 20); // 获取长视频菜单
|
|
|
|
|
+ if (response.status === 0 && response.data) {
|
|
|
|
|
+ videoMenus.value = response.data;
|
|
|
|
|
+ // 默认不选择任何菜单,加载所有视频
|
|
|
|
|
+ await loadVideosByTag("");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("获取视频菜单失败:", error);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ fetchVideoMenus();
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
.chip {
|
|
.chip {
|
|
|
@apply px-3 py-1.5 rounded-full bg-white/5 border border-white/10 text-sm text-white/70 hover:bg-white/10 hover:text-white transition;
|
|
@apply px-3 py-1.5 rounded-full bg-white/5 border border-white/10 text-sm text-white/70 hover:bg-white/10 hover:text-white transition;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+.chip-active {
|
|
|
|
|
+ @apply bg-white/10 text-white border-white/20;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sort-chip {
|
|
|
|
|
+ @apply px-2 py-1 rounded-full bg-white/5 border border-white/10 text-[10px] text-white/60 hover:bg-white/10 hover:text-white/80 transition;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sort-chip-active {
|
|
|
|
|
+ @apply bg-emerald-500/20 text-emerald-400 border-emerald-400/30;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|