1
This commit is contained in:
@@ -20,3 +20,8 @@ export function reviewVideo(videoId, data) {
|
||||
export function offlineVideo(videoId, data) {
|
||||
return request.post(`/admin/videos/${videoId}/offline`, data)
|
||||
}
|
||||
|
||||
/** 视频基础编辑 { title, description, cover_url, category_id, series_id, episode_no, status } */
|
||||
export function updateVideo(videoId, data) {
|
||||
return request.put(`/admin/videos/${videoId}`, data)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ const menuConfig = [
|
||||
{ title: '发布视频', path: '/videos/create', icon: 'Upload', roles: [] },
|
||||
{ title: '视频管理', path: '/videos', icon: 'Film', roles: [] },
|
||||
{ title: '系列管理', path: '/series', icon: 'CollectionTag', roles: [] },
|
||||
{ title: '分类管理', path: '/categories', icon: 'Menu', roles: [] },
|
||||
{ title: '标签管理', path: '/tags', icon: 'PriceTag', roles: [] },
|
||||
{ title: '评论管理', path: '/comments', icon: 'ChatDotRound', roles: [] },
|
||||
{ title: '举报处理', path: '/reports', icon: 'Warning', roles: [] },
|
||||
],
|
||||
|
||||
@@ -58,6 +58,18 @@ const routes = [
|
||||
component: () => import('@/views/SeriesManage.vue'),
|
||||
meta: { title: '系列管理', icon: 'CollectionTag', roles: [] },
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
name: 'category-manage',
|
||||
component: () => import('@/views/CategoryManage.vue'),
|
||||
meta: { title: '分类管理', icon: 'Menu', roles: [] },
|
||||
},
|
||||
{
|
||||
path: 'tags',
|
||||
name: 'tag-manage',
|
||||
component: () => import('@/views/TagManage.vue'),
|
||||
meta: { title: '标签管理', icon: 'PriceTag', roles: [] },
|
||||
},
|
||||
{
|
||||
path: 'comments',
|
||||
name: 'comment-list',
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="cat-manage">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="header">
|
||||
<span>分类管理</span>
|
||||
<div>
|
||||
<el-input v-model="keyword" placeholder="搜索分类名" clearable
|
||||
style="width: 200px; margin-right: 8px" @keyup.enter="load" />
|
||||
<el-button type="primary" @click="showCreate">+ 新建分类</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="分类名" min-width="120" />
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="video_count" label="视频数" width="90" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="showEdit(row)">编辑</el-button>
|
||||
<el-button link type="primary" @click="toggleStatus(row)">
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="del(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
style="margin-top: 12px; text-align: right"
|
||||
@current-change="load"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新建/编辑弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" :title="editing ? '编辑分类' : '新建分类'" width="400px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="分类名" required>
|
||||
<el-input v-model="form.name" maxlength="50" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序(小→前)">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/api/request'
|
||||
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const form = reactive({ name: '', sort: 0, status: 1 })
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await request({
|
||||
url: '/admin/categories/admin', method: 'get',
|
||||
params: { keyword: keyword.value || undefined, page: page.value, page_size: pageSize },
|
||||
})
|
||||
list.value = r.list
|
||||
total.value = r.total
|
||||
} catch (e) {} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showCreate() {
|
||||
editing.value = null
|
||||
Object.assign(form, { name: '', sort: 0, status: 1 })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function showEdit(row) {
|
||||
editing.value = row
|
||||
Object.assign(form, { name: row.name, sort: row.sort, status: row.status })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.name.trim()) {
|
||||
ElMessage.warning('分类名不能为空')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
if (editing.value) {
|
||||
await request({
|
||||
url: `/admin/categories/${editing.value.id}`, method: 'put',
|
||||
data: { name: form.name, sort: form.sort, status: form.status },
|
||||
})
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await request({
|
||||
url: '/admin/categories', method: 'post',
|
||||
data: { name: form.name, sort: form.sort, status: form.status },
|
||||
})
|
||||
ElMessage.success('已创建')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
load()
|
||||
} catch (e) {} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleStatus(row) {
|
||||
const next = row.status === 1 ? 0 : 1
|
||||
try {
|
||||
await request({
|
||||
url: `/admin/categories/${row.id}`, method: 'put',
|
||||
data: { status: next },
|
||||
})
|
||||
ElMessage.success('已更新')
|
||||
load()
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function del(row) {
|
||||
await ElMessageBox.confirm(`确认删除「${row.name}」?有视频绑定时拒绝删除。`, '警告', { type: 'warning' })
|
||||
try {
|
||||
await request({ url: `/admin/categories/${row.id}`, method: 'delete' })
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
} catch (e) {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header { display: flex; justify-content: space-between; align-items: center; }
|
||||
</style>
|
||||
@@ -140,12 +140,12 @@ const rules = {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await request({ url: '/categories', method: 'get' })
|
||||
categories.value = res.data || []
|
||||
const res = await request({ url: '/admin/categories', method: 'get' })
|
||||
categories.value = res || []
|
||||
} catch (e) {}
|
||||
try {
|
||||
const s = await request({ url: '/admin/series', method: 'get', params: { page_size: 200 } })
|
||||
seriesList.value = s.data?.list || []
|
||||
seriesList.value = s?.list || []
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
@@ -158,8 +158,8 @@ async function uploadFile({ file }) {
|
||||
try {
|
||||
// 1) 先拿上传凭证
|
||||
const t = await request({ url: '/admin/videos/upload', method: 'post' })
|
||||
const token = t.data.token
|
||||
const uploadUrl = t.data.upload_url
|
||||
const token = t.token
|
||||
const uploadUrl = t.upload_url
|
||||
// 2) 上传文件(multipart/form-data,带 token query)
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
@@ -209,7 +209,7 @@ async function submit() {
|
||||
if (form.duration) payload.duration = form.duration
|
||||
}
|
||||
const res = await request({ url: '/admin/videos', method: 'post', data: payload })
|
||||
const newVideoId = res.data?.video_id
|
||||
const newVideoId = res?.video_id
|
||||
resultMsg.value = newVideoId
|
||||
// 如果选了系列 + 集数,绑定进去
|
||||
if (newVideoId && form.series_id && form.episode_no) {
|
||||
|
||||
@@ -138,7 +138,7 @@ async function uploadCover(file) {
|
||||
url: '/admin/upload/image', method: 'post', data: fd,
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
form.cover_url = r.data.url
|
||||
form.cover_url = r.url
|
||||
ElMessage.success('封面上传成功')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || '上传失败')
|
||||
@@ -150,8 +150,8 @@ async function uploadCover(file) {
|
||||
onMounted(async () => {
|
||||
load()
|
||||
try {
|
||||
const r = await request({ url: '/categories', method: 'get' })
|
||||
categories.value = r.data || []
|
||||
const r = await request({ url: '/admin/categories', method: 'get' })
|
||||
categories.value = r || []
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
@@ -162,8 +162,8 @@ async function load() {
|
||||
url: '/admin/series', method: 'get',
|
||||
params: { keyword: keyword.value, page: page.value, page_size: pageSize },
|
||||
})
|
||||
list.value = r.data.list
|
||||
total.value = r.data.total
|
||||
list.value = r.list
|
||||
total.value = r.total
|
||||
} catch (e) {} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div class="tag-manage">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="header">
|
||||
<span>标签管理</span>
|
||||
<div>
|
||||
<el-input v-model="keyword" placeholder="搜索标签名" clearable
|
||||
style="width: 200px; margin-right: 8px" @keyup.enter="load" />
|
||||
<el-button type="primary" @click="showCreate">+ 新建标签</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="标签" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag effect="plain">{{ row.name }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="video_count" label="视频数" width="100" sortable />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="showEdit(row)">重命名</el-button>
|
||||
<el-button link type="danger" @click="del(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
style="margin-top: 12px; text-align: right"
|
||||
@current-change="load"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新建/编辑弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" :title="editing ? '重命名标签' : '新建标签(支持批量)'" width="400px">
|
||||
<el-form :model="form" label-width="70px">
|
||||
<el-form-item :label="editing ? '标签名' : '标签'">
|
||||
<el-input
|
||||
v-if="editing"
|
||||
v-model="form.name"
|
||||
maxlength="20"
|
||||
show-word-limit
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="form.name"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="用 空格 / 逗号(, ,) / 句号(. 。) / 竖线(| |) / 换行 分隔多个标签,一次可创建多个"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/api/request'
|
||||
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const form = reactive({ name: '' })
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await request({
|
||||
url: '/admin/tags', method: 'get',
|
||||
params: { keyword: keyword.value || undefined, page: page.value, page_size: pageSize },
|
||||
})
|
||||
list.value = r.list
|
||||
total.value = r.total
|
||||
} catch (e) {} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showCreate() {
|
||||
editing.value = null
|
||||
form.name = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function showEdit(row) {
|
||||
editing.value = row
|
||||
form.name = row.name
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
submitting.value = true
|
||||
try {
|
||||
if (editing.value) {
|
||||
const name = form.name.trim()
|
||||
if (!name) return ElMessage.warning('标签名不能为空')
|
||||
if (name.length > 20) return ElMessage.warning('标签名不能超过 20 字')
|
||||
await request({
|
||||
url: `/admin/tags/${editing.value.id}`, method: 'put',
|
||||
data: { name },
|
||||
})
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
// 批量分隔:空格/制表/换行/全半角逗号/全半角句号/全半角竖线
|
||||
const tokens = form.name
|
||||
.split(/[\s,,。.||]+/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
// 去重 + 限长
|
||||
const unique = [...new Set(tokens)].filter(t => t.length <= 20)
|
||||
if (!unique.length) {
|
||||
return ElMessage.warning('请至少输入一个标签')
|
||||
}
|
||||
const skipped = tokens.length - unique.length
|
||||
let created = 0, dup = 0
|
||||
for (const t of unique) {
|
||||
try {
|
||||
await request({ url: '/admin/tags', method: 'post', data: { name: t } })
|
||||
created++
|
||||
} catch (e) {
|
||||
// 已存在的跳过(后端返回 DUPLICATE 错误)
|
||||
dup++
|
||||
}
|
||||
}
|
||||
const parts = [`成功创建 ${created} 个`]
|
||||
if (dup) parts.push(`${dup} 个已存在跳过`)
|
||||
if (skipped > 0) parts.push(`${skipped} 个重复合并`)
|
||||
if (tokens.some(t => t.length > 20)) parts.push('超长标签已忽略')
|
||||
ElMessage.success(parts.join(','))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
load()
|
||||
} catch (e) {} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function del(row) {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除标签「${row.name}」?同时会从 ${row.video_count} 条视频上解绑该标签。`,
|
||||
'警告', { type: 'warning' }
|
||||
)
|
||||
try {
|
||||
await request({ url: `/admin/tags/${row.id}`, method: 'delete' })
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
} catch (e) {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header { display: flex; justify-content: space-between; align-items: center; }
|
||||
</style>
|
||||
+156
-25
@@ -25,6 +25,9 @@
|
||||
<el-table-column label="作者" width="140">
|
||||
<template #default="{ row }">{{ row.author?.nickname || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分类" width="90">
|
||||
<template #default="{ row }">{{ categoryName(row.category_id) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small">{{ row.status_text }}</el-tag>
|
||||
@@ -33,9 +36,10 @@
|
||||
<el-table-column prop="view_count" label="播放" width="80" />
|
||||
<el-table-column prop="like_count" label="点赞" width="80" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="View" @click="openPreview(row)">预览</el-button>
|
||||
<el-button link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status === 2" link type="danger" :icon="Delete" @click="offline(row)">
|
||||
下架
|
||||
</el-button>
|
||||
@@ -50,15 +54,65 @@
|
||||
</div>
|
||||
|
||||
<VideoPreview v-model="previewVisible" :video-url="previewUrl" :title="previewTitle" />
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<el-dialog v-model="editVisible" title="编辑视频" width="560px">
|
||||
<el-form :model="editForm" label-width="90px">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="editForm.title" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="封面图">
|
||||
<div class="edit-cover-row">
|
||||
<el-image v-if="editForm.cover_url" :src="editForm.cover_url" fit="cover" class="edit-cover" />
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
:on-change="(f) => uploadEditCover(f)"
|
||||
>
|
||||
<el-button plain :loading="coverUploading">选择图片</el-button>
|
||||
</el-upload>
|
||||
<el-input v-model="editForm.cover_url" placeholder="或直接粘贴封面 URL" style="margin-left: 8px" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类">
|
||||
<el-select v-model="editForm.category_id" placeholder="请选择" style="width: 100%" clearable>
|
||||
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="归属系列">
|
||||
<el-select v-model="editForm.series_id" placeholder="不归属系列" style="width: 100%" clearable
|
||||
@change="onEditSeriesChange">
|
||||
<el-option v-for="s in seriesList" :key="s.id" :label="s.title" :value="s.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="editForm.series_id" label="第几集">
|
||||
<el-input-number v-model="editForm.episode_no" :min="1" :max="9999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="editForm.status" style="width: 100%">
|
||||
<el-option v-for="(text, code) in STATUS_TEXT" :key="code" :label="text" :value="Number(code)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="简介">
|
||||
<el-input v-model="editForm.description" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="editSubmitting" @click="saveEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Delete, Refresh, Search, View } from '@element-plus/icons-vue'
|
||||
import { Delete, Edit, Refresh, Search, View } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import { listVideos, offlineVideo } from '@/api/videos'
|
||||
import request from '@/api/request'
|
||||
import { listVideos, offlineVideo, updateVideo } from '@/api/videos'
|
||||
import VideoPreview from '@/components/VideoPreview.vue'
|
||||
import { askReason } from '@/utils/reason'
|
||||
|
||||
@@ -80,14 +134,32 @@ const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = reactive({ keyword: '', status: '', author_id: '' })
|
||||
|
||||
const categories = ref([])
|
||||
const seriesList = ref([])
|
||||
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewTitle = ref('')
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editSubmitting = ref(false)
|
||||
const coverUploading = ref(false)
|
||||
const editingVideoId = ref(null)
|
||||
const editForm = reactive({
|
||||
title: '', description: '', cover_url: '',
|
||||
category_id: 0, series_id: null, episode_no: null,
|
||||
status: 2,
|
||||
})
|
||||
|
||||
function statusTagType(status) {
|
||||
return STATUS_TAG[status] || 'info'
|
||||
}
|
||||
|
||||
function categoryName(cid) {
|
||||
const c = categories.value.find(x => x.id === cid)
|
||||
return c?.name || (cid ? `#${cid}` : '-')
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -105,15 +177,9 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
page.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function search() { page.value = 1; load() }
|
||||
function reset() {
|
||||
filters.keyword = ''
|
||||
filters.status = ''
|
||||
filters.author_id = ''
|
||||
filters.keyword = ''; filters.status = ''; filters.author_id = ''
|
||||
search()
|
||||
}
|
||||
|
||||
@@ -127,30 +193,95 @@ async function offline(row) {
|
||||
let reason
|
||||
try {
|
||||
reason = await askReason({ title: '下架视频', message: `下架《${row.title}》并通知作者` })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
} catch { return }
|
||||
await offlineVideo(row.video_id, { reason })
|
||||
ElMessage.success('已下架')
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
// ---------- 编辑弹窗 ----------
|
||||
function openEdit(row) {
|
||||
editingVideoId.value = row.video_id
|
||||
Object.assign(editForm, {
|
||||
title: row.title || '',
|
||||
description: row.description || '',
|
||||
cover_url: row.cover_url || '',
|
||||
category_id: row.category_id || 0,
|
||||
series_id: row.series_id || null,
|
||||
episode_no: row.episode_no || null,
|
||||
status: row.status,
|
||||
})
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
function onEditSeriesChange() {
|
||||
if (!editForm.series_id) editForm.episode_no = null
|
||||
}
|
||||
|
||||
async function uploadEditCover(file) {
|
||||
if (!file || !file.raw) return
|
||||
coverUploading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file.raw)
|
||||
const r = await request({
|
||||
url: '/admin/upload/image', method: 'post', data: fd,
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
editForm.cover_url = r.url
|
||||
ElMessage.success('封面上传成功')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || '上传失败')
|
||||
} finally {
|
||||
coverUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!editForm.title.trim()) { ElMessage.warning('标题不能为空'); return }
|
||||
editSubmitting.value = true
|
||||
try {
|
||||
const payload = {
|
||||
title: editForm.title.trim(),
|
||||
description: editForm.description,
|
||||
cover_url: editForm.cover_url,
|
||||
category_id: editForm.category_id || 0,
|
||||
status: editForm.status,
|
||||
}
|
||||
// 只有用户动过才提交
|
||||
if (editForm.series_id) {
|
||||
payload.series_id = editForm.series_id
|
||||
if (editForm.episode_no) payload.episode_no = editForm.episode_no
|
||||
}
|
||||
await updateVideo(editingVideoId.value, payload)
|
||||
ElMessage.success('已更新')
|
||||
editVisible.value = false
|
||||
load()
|
||||
} catch (e) {} finally {
|
||||
editSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
load()
|
||||
try {
|
||||
categories.value = await request({ url: '/admin/categories', method: 'get' }) || []
|
||||
} catch (e) {}
|
||||
try {
|
||||
const r = await request({ url: '/admin/series', method: 'get', params: { page_size: 200 } })
|
||||
seriesList.value = r?.list || []
|
||||
} catch (e) {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cover {
|
||||
width: 72px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
width: 72px; height: 40px; border-radius: 4px;
|
||||
}
|
||||
|
||||
.cover.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
background-color: #f5f7fa;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 12px; color: #c0c4cc; background-color: #f5f7fa;
|
||||
}
|
||||
.edit-cover-row { display: flex; align-items: center; width: 100%; }
|
||||
.edit-cover { width: 72px; height: 40px; border-radius: 4px; margin-right: 8px; object-fit: cover; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user