优化页面、修复BUG
This commit is contained in:
@@ -75,6 +75,7 @@ const isFullWidthPage = computed(() => {
|
||||
return path.startsWith('/admin') || path === '/login'
|
||||
|| path.startsWith('/blog/')
|
||||
|| path.startsWith('/works/')
|
||||
|| path.startsWith('/columns')
|
||||
})
|
||||
|
||||
// 应用网站配置到页面标题和meta标签
|
||||
|
||||
@@ -18,24 +18,68 @@
|
||||
<template v-else-if="video">
|
||||
<h1 class="font-serif text-3xl md:text-4xl text-white mb-6">{{ video.title }}</h1>
|
||||
|
||||
<!-- 有视频地址时挂载 DPlayer 容器 -->
|
||||
<div
|
||||
v-if="video.videoUrl"
|
||||
ref="playerRef"
|
||||
class="w-full aspect-video rounded-xl overflow-hidden bg-black mb-8"
|
||||
class="w-full aspect-video rounded-xl overflow-hidden bg-black mb-6"
|
||||
></div>
|
||||
<!-- 无视频地址时的占位提示 -->
|
||||
<div
|
||||
v-else
|
||||
class="w-full aspect-video rounded-xl overflow-hidden bg-black/40 border border-white/10 flex items-center justify-center mb-8"
|
||||
class="w-full aspect-video rounded-xl overflow-hidden bg-black/40 border border-white/10 flex items-center justify-center mb-6"
|
||||
>
|
||||
<p class="text-art-muted text-sm">暂无视频地址</p>
|
||||
</div>
|
||||
|
||||
<!-- 专辑内前后导航:前 2 + 后 2 -->
|
||||
<div v-if="video.albumContext" class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<p class="text-sm text-art-muted">
|
||||
来自专辑 <span class="text-white">{{ video.albumContext.albumName }}</span>
|
||||
</p>
|
||||
<router-link
|
||||
:to="`/videos/albums/${video.albumContext.albumId}`"
|
||||
class="text-xs text-art-accent hover:underline"
|
||||
>返回专辑</router-link>
|
||||
</div>
|
||||
<div class="flex gap-3 overflow-x-auto pb-2">
|
||||
<!-- 前序视频 -->
|
||||
<div
|
||||
v-for="pv in video.albumContext.prevVideos"
|
||||
:key="'prev-' + pv.id"
|
||||
class="shrink-0 w-36 cursor-pointer group"
|
||||
@click="router.push(`/videos/${pv.id}`)"
|
||||
>
|
||||
<div class="aspect-video rounded-lg overflow-hidden border border-white/10 group-hover:border-art-accent/50 transition-colors bg-black/40">
|
||||
<img v-if="pv.cover" :src="pv.cover" class="w-full h-full object-cover opacity-80 group-hover:opacity-100" alt="" />
|
||||
</div>
|
||||
<p class="text-xs text-art-muted mt-1 line-clamp-2 group-hover:text-white">{{ pv.title }}</p>
|
||||
</div>
|
||||
<!-- 当前视频高亮 -->
|
||||
<div class="shrink-0 w-36">
|
||||
<div class="aspect-video rounded-lg overflow-hidden border-2 border-art-accent bg-black/40">
|
||||
<img v-if="video.cover" :src="video.cover" class="w-full h-full object-cover" alt="" />
|
||||
</div>
|
||||
<p class="text-xs text-art-accent mt-1 line-clamp-2 font-medium">正在播放</p>
|
||||
</div>
|
||||
<!-- 后续视频 -->
|
||||
<div
|
||||
v-for="nv in video.albumContext.nextVideos"
|
||||
:key="'next-' + nv.id"
|
||||
class="shrink-0 w-36 cursor-pointer group"
|
||||
@click="router.push(`/videos/${nv.id}`)"
|
||||
>
|
||||
<div class="aspect-video rounded-lg overflow-hidden border border-white/10 group-hover:border-art-accent/50 transition-colors bg-black/40">
|
||||
<img v-if="nv.cover" :src="nv.cover" class="w-full h-full object-cover opacity-80 group-hover:opacity-100" alt="" />
|
||||
</div>
|
||||
<p class="text-xs text-art-muted mt-1 line-clamp-2 group-hover:text-white">{{ nv.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-3xl">
|
||||
<div class="flex gap-4 text-sm text-art-muted mb-4">
|
||||
<span v-if="video.categoryName">{{ video.categoryName }}</span>
|
||||
<span>{{ video.createdAt }}</span>
|
||||
<span v-if="video.createdAt">发布于 {{ video.createdAt }}</span>
|
||||
</div>
|
||||
<p v-if="video.description" class="text-white/80 leading-relaxed whitespace-pre-wrap">{{ video.description }}</p>
|
||||
</div>
|
||||
@@ -46,25 +90,26 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import DPlayer from 'dplayer'
|
||||
import 'dplayer/dist/DPlayer.min.css'
|
||||
import { fetchVideo, type VideoItem } from '../services/api'
|
||||
import { fetchVideo, type VideoDetailItem } from '../services/api'
|
||||
import Icon from '../components/Icon.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const video = ref<VideoItem | null>(null)
|
||||
const video = ref<VideoDetailItem | null>(null)
|
||||
const playerRef = ref<HTMLElement | null>(null)
|
||||
let dp: DPlayer | null = null
|
||||
|
||||
/** 销毁 DPlayer 实例,避免内存泄漏 */
|
||||
/** 销毁 DPlayer 实例 */
|
||||
const destroyPlayer = () => {
|
||||
if (dp) { dp.destroy(); dp = null }
|
||||
}
|
||||
|
||||
/** 在容器与视频 URL 就绪后创建 DPlayer */
|
||||
/** 创建 DPlayer 播放器 */
|
||||
const initPlayer = async () => {
|
||||
destroyPlayer()
|
||||
if (!playerRef.value || !video.value?.videoUrl) return
|
||||
@@ -77,7 +122,7 @@ const initPlayer = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
/** 加载视频详情数据 */
|
||||
/** 加载视频详情 */
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
@@ -92,7 +137,6 @@ const load = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 video、容器 DOM、loading 状态,在 DOM 挂载后再初始化播放器
|
||||
watch([video, playerRef, loading], async ([v, el, isLoading]) => {
|
||||
if (isLoading || !v?.videoUrl || !el) return
|
||||
await nextTick()
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
<p class="text-art-muted">精选视频内容与系列专辑。</p>
|
||||
</div>
|
||||
|
||||
<!-- 分类 Tab -->
|
||||
<div class="flex flex-wrap gap-2 mb-10">
|
||||
<!-- 分类 Tab 快捷筛选 -->
|
||||
<div class="flex flex-wrap gap-2 mb-8">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-1.5 rounded-full text-sm border transition-colors"
|
||||
:class="activeCategory === 0 ? 'border-art-accent text-art-accent bg-art-accent/10' : 'border-white/10 text-art-muted hover:text-white'"
|
||||
@click="activeCategory = 0; loadData()"
|
||||
@click="selectCategory(0)"
|
||||
>全部</button>
|
||||
<button
|
||||
v-for="cat in categories"
|
||||
@@ -20,19 +20,42 @@
|
||||
type="button"
|
||||
class="px-4 py-1.5 rounded-full text-sm border transition-colors"
|
||||
:class="activeCategory === cat.id ? 'border-art-accent text-art-accent bg-art-accent/10' : 'border-white/10 text-art-muted hover:text-white'"
|
||||
@click="activeCategory = cat.id; loadData()"
|
||||
@click="selectCategory(cat.id)"
|
||||
>{{ cat.name }}</button>
|
||||
</div>
|
||||
|
||||
<!-- 分类概览卡片 -->
|
||||
<div v-if="categories.length" class="mb-14">
|
||||
<h3 class="font-mono text-xs text-art-muted uppercase tracking-widest mb-6">分类</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div
|
||||
v-for="cat in categories"
|
||||
:key="'card-' + cat.id"
|
||||
class="group p-5 rounded-xl border cursor-pointer transition-colors"
|
||||
:class="activeCategory === cat.id ? 'border-art-accent/50 bg-art-accent/5' : 'border-white/10 hover:border-art-accent/30 hover:bg-white/5'"
|
||||
@click="selectCategory(cat.id)"
|
||||
>
|
||||
<h4 class="text-white font-medium group-hover:text-art-accent transition-colors">{{ cat.name }}</h4>
|
||||
<p v-if="cat.description" class="text-xs text-art-muted mt-2 line-clamp-2">{{ cat.description }}</p>
|
||||
<div class="flex gap-4 mt-3 text-xs text-art-muted">
|
||||
<span>{{ cat.videoCount ?? 0 }} 个视频</span>
|
||||
<span>{{ cat.albumCount ?? 0 }} 个专辑</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!loading && !error" class="mb-10 text-center text-art-muted text-sm py-6 border border-dashed border-white/10 rounded-xl">
|
||||
暂无视频分类,请先在后台创建分类
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="flex justify-center py-20"><div class="animate-spin h-10 w-10 border-t-2 border-art-accent rounded-full"></div></div>
|
||||
|
||||
<!-- API 加载失败 -->
|
||||
<div v-else-if="error" class="text-center py-12">
|
||||
<p class="text-red-400 mb-4">{{ error }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 border border-white/20 rounded-full text-sm text-white hover:border-art-accent hover:text-art-accent transition-colors"
|
||||
@click="loadData"
|
||||
@click="loadAll"
|
||||
>重试</button>
|
||||
</div>
|
||||
|
||||
@@ -44,19 +67,45 @@
|
||||
<div
|
||||
v-for="album in albums"
|
||||
:key="album.id"
|
||||
class="group cursor-pointer rounded-xl overflow-hidden border border-white/10 hover:border-art-accent/40 transition-colors"
|
||||
@click="router.push(`/videos/albums/${album.id}`)"
|
||||
class="group rounded-xl overflow-hidden border border-white/10 hover:border-art-accent/40 transition-colors"
|
||||
>
|
||||
<div class="aspect-video bg-gray-900 overflow-hidden">
|
||||
<div
|
||||
class="aspect-video bg-gray-900 overflow-hidden cursor-pointer"
|
||||
@click="router.push(`/videos/albums/${album.id}`)"
|
||||
>
|
||||
<img v-if="album.cover" :src="album.cover" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" alt="" />
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<!-- 专辑预览视频缩略图 -->
|
||||
<div
|
||||
v-if="album.previewVideos?.length"
|
||||
class="flex gap-1 px-2 py-2 bg-black/30 border-t border-white/5 overflow-x-auto"
|
||||
>
|
||||
<div
|
||||
v-for="pv in album.previewVideos"
|
||||
:key="pv.id"
|
||||
class="shrink-0 w-16 h-10 rounded overflow-hidden cursor-pointer border border-transparent hover:border-art-accent/50"
|
||||
@click.stop="router.push(`/videos/${pv.id}`)"
|
||||
>
|
||||
<img v-if="pv.cover" :src="pv.cover" class="w-full h-full object-cover" alt="" />
|
||||
<div v-else class="w-full h-full bg-white/5 flex items-center justify-center">
|
||||
<Icon name="play" :size="12" class="text-white/50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 cursor-pointer" @click="router.push(`/videos/albums/${album.id}`)">
|
||||
<h4 class="text-white font-medium group-hover:text-art-accent transition-colors">{{ album.name }}</h4>
|
||||
<p class="text-xs text-art-muted mt-1">{{ album.videoCount ?? 0 }} 个视频</p>
|
||||
<p class="text-xs text-art-muted mt-1">
|
||||
{{ album.videoCount ?? 0 }} 个视频
|
||||
<span v-if="album.latestVideoUpdatedAt" class="ml-2">· 更新于 {{ album.latestVideoUpdatedAt }}</span>
|
||||
</p>
|
||||
<p v-if="album.categoryName" class="text-xs text-art-muted/70 mt-1">{{ album.categoryName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mb-10 text-center text-art-muted text-sm py-8 border border-dashed border-white/10 rounded-xl">
|
||||
暂无专辑
|
||||
</div>
|
||||
|
||||
<!-- 最新视频 -->
|
||||
<div>
|
||||
@@ -78,7 +127,10 @@
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h4 class="text-white font-medium line-clamp-2">{{ video.title }}</h4>
|
||||
<p class="text-xs text-art-muted mt-1">{{ video.categoryName || '未分类' }}</p>
|
||||
<p class="text-xs text-art-muted mt-1">
|
||||
{{ video.categoryName || '未分类' }}
|
||||
<span v-if="video.createdAt" class="ml-2">· 发布于 {{ video.createdAt }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,12 +156,15 @@ const albums = ref<VideoAlbum[]>([])
|
||||
const videos = ref<VideoItem[]>([])
|
||||
const activeCategory = ref(0)
|
||||
|
||||
// 滚动入场动画:animate-slide-down 初始 opacity:0,需 initObserver 添加 .visible
|
||||
const { initObserver } = useScrollAnimation()
|
||||
|
||||
/**
|
||||
* 加载视频分类下的专辑与视频列表
|
||||
*/
|
||||
/** 切换分类并重新加载数据 */
|
||||
const selectCategory = (catId: number) => {
|
||||
activeCategory.value = catId
|
||||
loadData()
|
||||
}
|
||||
|
||||
/** 加载专辑与视频列表 */
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
@@ -122,17 +177,21 @@ const loadData = async () => {
|
||||
console.error('加载视频列表失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
// 数据渲染后触发 IntersectionObserver,使页面从透明变为可见
|
||||
await initObserver()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
/** 加载分类并刷新列表 */
|
||||
const loadAll = async () => {
|
||||
try {
|
||||
categories.value = await fetchVideoCategories()
|
||||
} catch (err) {
|
||||
error.value = '加载分类失败,请稍后重试'
|
||||
console.error('加载视频分类失败:', err)
|
||||
return
|
||||
}
|
||||
await loadData()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(loadAll)
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="admin-card overflow-hidden">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>名称</th><th>分类</th><th>视频数</th><th>状态</th><th class="text-right">操作</th></tr>
|
||||
<tr><th>ID</th><th>名称</th><th>分类</th><th>视频数</th><th>最新视频更新</th><th>状态</th><th class="text-right">操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in list" :key="item.id">
|
||||
@@ -15,6 +15,7 @@
|
||||
<td class="text-white">{{ item.name }}</td>
|
||||
<td class="text-art-muted">{{ item.categoryName || '-' }}</td>
|
||||
<td class="text-art-muted">{{ item.videoCount ?? 0 }}</td>
|
||||
<td class="text-xs text-art-muted">{{ item.latestVideoUpdatedAt || '-' }}</td>
|
||||
<td><span :class="item.isActive ? 'text-green-400' : 'text-red-400'">{{ item.isActive ? '启用' : '禁用' }}</span></td>
|
||||
<td class="text-right">
|
||||
<router-link :to="`/admin/video-albums/${item.id}/edit`" class="admin-btn-secondary py-1 px-3 text-xs mr-2">编辑</router-link>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div class="w-full animate-reveal">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<!-- 悬浮顶栏:滚动时取消/保存始终可见 -->
|
||||
<div class="sticky top-0 z-20 -mx-6 px-6 py-4 mb-4 bg-art-admin-bg/95 backdrop-blur-md border-b border-white/5 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-serif italic text-white">{{ isEditing ? '编辑视频' : '新建视频' }}</h1>
|
||||
<div class="flex gap-3">
|
||||
<button type="button" class="admin-btn-secondary" @click="router.push('/admin/videos')">取消</button>
|
||||
<button type="button" class="admin-btn-primary" :disabled="submitting" @click="handleSubmit">{{ submitting ? '提交中...' : '保存' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card p-6 max-w-3xl space-y-5">
|
||||
<div><label class="block text-xs text-art-muted mb-1">标题</label><input v-model="form.title" class="admin-input" required /></div>
|
||||
<div><label class="block text-xs text-art-muted mb-1">分类</label>
|
||||
@@ -15,6 +17,18 @@
|
||||
<option v-for="c in categories" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 所属专辑多选 -->
|
||||
<div>
|
||||
<label class="block text-xs text-art-muted mb-2">所属专辑(可多选)</label>
|
||||
<div v-if="albums.length" class="max-h-48 overflow-y-auto space-y-2 border border-white/10 rounded-lg p-3">
|
||||
<label v-for="a in albums" :key="a.id" class="flex items-center gap-3 p-2 rounded hover:bg-white/5 cursor-pointer">
|
||||
<input type="checkbox" :value="a.id" v-model="selectedAlbumIds" />
|
||||
<span class="text-sm text-white">{{ a.name }}</span>
|
||||
<span v-if="a.categoryName" class="text-xs text-art-muted">({{ a.categoryName }})</span>
|
||||
</label>
|
||||
</div>
|
||||
<p v-else class="text-xs text-art-muted">暂无专辑,请先在「视频专辑」中创建</p>
|
||||
</div>
|
||||
<div><label class="block text-xs text-art-muted mb-1">视频文件</label><VideoUpload v-model="form.videoUrl" :category-id="1" /></div>
|
||||
<div><label class="block text-xs text-art-muted mb-1">封面图</label><ImageUpload v-model="form.cover" :category-id="1" /></div>
|
||||
<div><label class="block text-xs text-art-muted mb-1">描述</label><textarea v-model="form.description" rows="4" class="admin-input resize-none" /></div>
|
||||
@@ -28,7 +42,7 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getAdminVideoCategories, getAdminVideo, createVideo, updateVideo } from '../../services/api'
|
||||
import { getAdminVideoCategories, getAdminVideoAlbums, getAdminVideo, createVideo, updateVideo, type VideoAlbum } from '../../services/api'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
import ImageUpload from '../../components/admin/ImageUpload.vue'
|
||||
import VideoUpload from '../../components/admin/VideoUpload.vue'
|
||||
@@ -39,26 +53,40 @@ const toast = useToast()
|
||||
const submitting = ref(false)
|
||||
const isEditing = computed(() => !!route.params.id)
|
||||
const categories = ref<{ id: number; name: string }[]>([])
|
||||
const albums = ref<VideoAlbum[]>([])
|
||||
const selectedAlbumIds = ref<number[]>([])
|
||||
const form = reactive({ title: '', description: '', videoUrl: '', cover: '', poster: '', categoryId: 0, isPublished: 1 })
|
||||
|
||||
onMounted(async () => {
|
||||
categories.value = await getAdminVideoCategories()
|
||||
albums.value = await getAdminVideoAlbums()
|
||||
if (isEditing.value) {
|
||||
const item = await getAdminVideo(route.params.id as string)
|
||||
Object.assign(form, { title: item.title, description: item.description, videoUrl: item.videoUrl, cover: item.cover, poster: item.poster, categoryId: item.categoryId, isPublished: item.isPublished })
|
||||
Object.assign(form, {
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
videoUrl: item.videoUrl,
|
||||
cover: item.cover,
|
||||
poster: item.poster,
|
||||
categoryId: item.categoryId,
|
||||
isPublished: item.isPublished,
|
||||
})
|
||||
selectedAlbumIds.value = item.albumIds ?? []
|
||||
}
|
||||
})
|
||||
|
||||
/** 提交视频表单,同步专辑关联 */
|
||||
const handleSubmit = async () => {
|
||||
if (!form.videoUrl) { toast.error('请上传视频'); return }
|
||||
submitting.value = true
|
||||
try {
|
||||
form.poster = form.cover || form.poster
|
||||
const payload = { ...form, albumIds: selectedAlbumIds.value }
|
||||
if (isEditing.value) {
|
||||
await updateVideo(route.params.id as string, form)
|
||||
await updateVideo(route.params.id as string, payload)
|
||||
toast.success('更新成功')
|
||||
} else {
|
||||
await createVideo(form)
|
||||
await createVideo(payload)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
router.push('/admin/videos')
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="admin-card overflow-hidden">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr><th>标题</th><th>分类</th><th>状态</th><th>创建时间</th><th class="text-right">操作</th></tr>
|
||||
<tr><th>标题</th><th>分类</th><th>状态</th><th>发布时间</th><th class="text-right">操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in list" :key="item.id">
|
||||
|
||||
@@ -171,6 +171,8 @@ export interface VideoAlbum {
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
videoCount?: number
|
||||
latestVideoUpdatedAt?: string
|
||||
previewVideos?: VideoItem[]
|
||||
}
|
||||
|
||||
export interface VideoItem {
|
||||
@@ -188,6 +190,24 @@ export interface VideoItem {
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/** 专辑内前后导航上下文 */
|
||||
export interface VideoAlbumNavContext {
|
||||
albumId: number
|
||||
albumName: string
|
||||
prevVideos: VideoItem[]
|
||||
nextVideos: VideoItem[]
|
||||
}
|
||||
|
||||
/** 视频详情(含专辑导航) */
|
||||
export interface VideoDetailItem extends VideoItem {
|
||||
albumContext?: VideoAlbumNavContext
|
||||
}
|
||||
|
||||
/** 管理端视频(含所属专辑) */
|
||||
export interface AdminVideoItem extends VideoItem {
|
||||
albumIds?: number[]
|
||||
}
|
||||
|
||||
// 分类相关类型
|
||||
export interface Category {
|
||||
id: number
|
||||
@@ -2140,8 +2160,8 @@ export const fetchVideos = async (categoryId?: number): Promise<VideoItem[]> =>
|
||||
return Array.isArray(result) ? result : []
|
||||
}
|
||||
|
||||
export const fetchVideo = async (id: string): Promise<VideoItem> => {
|
||||
const result = await fetchJson<VideoItem>(`${API_BASE}/videos/${id}`)
|
||||
export const fetchVideo = async (id: string): Promise<VideoDetailItem> => {
|
||||
const result = await fetchJson<VideoDetailItem>(`${API_BASE}/videos/${id}`)
|
||||
if (!result) throw new Error('Video not found')
|
||||
return result
|
||||
}
|
||||
@@ -2205,30 +2225,30 @@ export const getAlbumVideoIds = async (albumId: number): Promise<string[]> => {
|
||||
return Array.isArray(result) ? result : []
|
||||
}
|
||||
|
||||
export const getAdminVideos = async (categoryId?: number): Promise<VideoItem[]> => {
|
||||
export const getAdminVideos = async (categoryId?: number): Promise<AdminVideoItem[]> => {
|
||||
const q = categoryId ? `?categoryId=${categoryId}` : ''
|
||||
const response = await authFetch(`${API_BASE}/admin/videos${q}`, { headers: getAuthHeaders() })
|
||||
const result = await parseApiResponse<VideoItem[]>(response)
|
||||
const result = await parseApiResponse<AdminVideoItem[]>(response)
|
||||
return Array.isArray(result) ? result : []
|
||||
}
|
||||
|
||||
export const getAdminVideo = async (id: string): Promise<VideoItem> => {
|
||||
export const getAdminVideo = async (id: string): Promise<AdminVideoItem> => {
|
||||
const response = await authFetch(`${API_BASE}/admin/videos/${id}`, { headers: getAuthHeaders() })
|
||||
return await parseApiResponse<VideoItem>(response)
|
||||
return await parseApiResponse<AdminVideoItem>(response)
|
||||
}
|
||||
|
||||
export const createVideo = async (data: Partial<VideoItem>): Promise<VideoItem> => {
|
||||
export const createVideo = async (data: Partial<VideoItem> & { albumIds?: number[] }): Promise<AdminVideoItem> => {
|
||||
const response = await authFetch(`${API_BASE}/admin/videos`, {
|
||||
method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(data),
|
||||
})
|
||||
return await parseApiResponse<VideoItem>(response)
|
||||
return await parseApiResponse<AdminVideoItem>(response)
|
||||
}
|
||||
|
||||
export const updateVideo = async (id: string, data: Partial<VideoItem>): Promise<VideoItem> => {
|
||||
export const updateVideo = async (id: string, data: Partial<VideoItem> & { albumIds?: number[] }): Promise<AdminVideoItem> => {
|
||||
const response = await authFetch(`${API_BASE}/admin/videos/${id}`, {
|
||||
method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(data),
|
||||
})
|
||||
return await parseApiResponse<VideoItem>(response)
|
||||
return await parseApiResponse<AdminVideoItem>(response)
|
||||
}
|
||||
|
||||
export const deleteVideo = async (id: string): Promise<void> => {
|
||||
|
||||
Reference in New Issue
Block a user