Files
video/admin/src/views/CreateVideo.vue
T
2026-10-16 16:05:01 +08:00

249 lines
8.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="create-video">
<el-card shadow="never">
<template #header>
<span>发布视频</span>
</template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<!-- 标题 / 分类 / 简介 -->
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" maxlength="100" show-word-limit placeholder="视频标题" />
</el-form-item>
<el-form-item label="分类" prop="category_id">
<el-select v-model="form.category_id" placeholder="请选择分类" style="width: 240px">
<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-input v-model="form.description" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="视频描述(可选)" />
</el-form-item>
<!-- 两种模式切换 -->
<el-form-item label="视频来源">
<el-radio-group v-model="mode">
<el-radio-button label="upload">上传文件(自动转码 HLS)</el-radio-button>
<el-radio-button label="url">填 m3u8 地址</el-radio-button>
</el-radio-group>
</el-form-item>
<!-- 模式 A:上传 -->
<template v-if="mode === 'upload'">
<el-form-item label="视频文件" required>
<el-upload
:auto-upload="true"
:show-file-list="false"
:http-request="uploadFile"
accept=".mp4,.mov,.mkv,.avi"
:disabled="!!form.upload_token"
>
<el-button type="primary" :loading="uploading">
{{ uploading ? '上传中…' : '选择视频上传' }}
</el-button>
<span v-if="form.upload_token" class="ml8 ok">✓ 已上传:{{ uploadedName }}</span>
<span v-else class="ml8 hint">支持 mp4/mov/mkv/avi,最大由服务器 nginx 限制</span>
</el-upload>
</el-form-item>
</template>
<!-- 模式 B:URL -->
<template v-else>
<el-form-item label="m3u8 地址" prop="video_url">
<el-input v-model="form.video_url" placeholder="https://xxx.com/videos/xxx/index.m3u8" />
</el-form-item>
<el-form-item label="封面图 URL" prop="cover_url">
<el-input v-model="form.cover_url" placeholder="https://xxx.com/cover.jpg" />
</el-form-item>
<el-form-item label="时长(秒)">
<el-input-number v-model="form.duration" :min="1" :max="36000" :step="10" placeholder="可选" />
</el-form-item>
</template>
<el-form-item label="发布状态">
<el-radio-group v-model="form.status">
<el-radio :label="1">待审核</el-radio>
<el-radio :label="2">直接发布</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="归属系列">
<el-select v-model="form.series_id" clearable placeholder="不归属系列"
style="width: 240px" @change="onSeriesChange">
<el-option v-for="s in seriesList" :key="s.id" :label="s.title" :value="s.id" />
</el-select>
<el-input-number v-if="form.series_id" v-model="form.episode_no"
:min="1" :step="1" placeholder="第几集" style="margin-left: 8px; width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="submitting" @click="submit">提交</el-button>
<el-button @click="reset">重置</el-button>
<span v-if="resultMsg" class="ml8 ok">提交成功,video_id={{ resultMsg }}</span>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/api/request'
const formRef = ref()
const uploading = ref(false)
const submitting = ref(false)
const mode = ref('upload') // 'upload' | 'url'
const uploadedName = ref('')
const resultMsg = ref('')
const categories = ref([])
const seriesList = ref([])
const form = reactive({
title: '',
description: '',
category_id: null,
upload_token: '',
video_url: '',
cover_url: '',
duration: null,
status: 1,
series_id: null,
episode_no: null,
})
const rules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
category_id: [{ required: true, message: '请选择分类', trigger: 'change' }],
video_url: [
{
validator: (_, v, cb) => {
if (mode.value !== 'url') return cb()
if (!v) return cb(new Error('请填 m3u8 地址'))
if (!v.startsWith('http')) return cb(new Error('必须 http(s) 开头'))
cb()
},
trigger: 'blur',
},
],
cover_url: [
{
validator: (_, v, cb) => {
if (mode.value !== 'url') return cb()
if (!v) return cb(new Error('URL 模式必须填封面'))
cb()
},
trigger: 'blur',
},
],
}
onMounted(async () => {
try {
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?.list || []
} catch (e) {}
})
function onSeriesChange() {
if (!form.series_id) form.episode_no = null
}
async function uploadFile({ file }) {
uploading.value = true
try {
// 1) 先拿上传凭证
const t = await request({ url: '/admin/videos/upload', method: 'post' })
const token = t.token
const uploadUrl = t.upload_url
// 2) 上传文件(multipart/form-data,带 token query)
const fd = new FormData()
fd.append('file', file)
const res = await fetch(uploadUrl + (uploadUrl.includes('?') ? '&' : '?') + 'token=' + token, {
method: 'POST',
headers: { Authorization: 'Bearer ' + localStorage.getItem('admin_token') },
body: fd,
})
const json = await res.json()
if (json.code === 0) {
form.upload_token = json.data.upload_token
uploadedName.value = file.name + '(' + (json.data.file_size / 1024 / 1024).toFixed(1) + 'MB)'
ElMessage.success('上传成功')
} else {
ElMessage.error(json.message || '上传失败')
}
} catch (e) {
ElMessage.error('上传异常:' + (e.message || e))
} finally {
uploading.value = false
}
}
async function submit() {
await formRef.value.validate()
if (mode.value === 'upload' && !form.upload_token) {
ElMessage.warning('请先上传视频文件')
return
}
if (mode.value === 'url' && !form.video_url) {
ElMessage.warning('请填 m3u8 地址')
return
}
submitting.value = true
try {
const payload = {
title: form.title,
description: form.description,
category_id: form.category_id,
status: form.status,
}
if (mode.value === 'upload') {
payload.upload_token = form.upload_token
} else {
payload.video_url = form.video_url
payload.cover_url = form.cover_url
if (form.duration) payload.duration = form.duration
}
const res = await request({ url: '/admin/videos', method: 'post', data: payload })
const newVideoId = res?.video_id
resultMsg.value = newVideoId
// 如果选了系列 + 集数,绑定进去
if (newVideoId && form.series_id && form.episode_no) {
try {
await request({
url: `/admin/videos/${newVideoId}/series`, method: 'post',
data: { series_id: form.series_id, episode_no: form.episode_no },
})
} catch (e) {}
}
ElMessage.success('提交成功')
reset()
} catch (e) {} finally {
submitting.value = false
}
}
function reset() {
formRef.value.resetFields()
form.upload_token = ''
form.video_url = ''
form.cover_url = ''
form.duration = null
form.status = 1
form.series_id = null
form.episode_no = null
uploadedName.value = ''
resultMsg.value = ''
}
</script>
<style scoped>
.ml8 { margin-left: 8px; }
.hint { color: #909399; font-size: 12px; }
.ok { color: #67c23a; }
</style>