Files
video/admin/src/views/CollectSources.vue
2026-10-17 19:04:05 +08:00

213 lines
8.0 KiB
Vue
Raw Permalink 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="collect-sources">
<el-card shadow="never">
<template #header>
<div class="header">
<span>采集源管理</span>
<div>
<el-button :loading="syncAllLoading" @click="syncAll">全部同步</el-button>
<el-button @click="batchVisible = true">批量添加</el-button>
<el-button type="primary" @click="showCreate">+ 添加采集源</el-button>
</div>
</div>
</template>
<el-alert type="info" :closable="false" style="margin-bottom: 12px">
添加"分类名 + 列表URL"后点同步即可自动采集;URL 中用 <code v-pre>{{page}}</code> 表示页码占位。
分类名不存在时会自动创建。同步为增量:已入库的视频自动跳过。
</el-alert>
<el-table :data="list" v-loading="loading" stripe>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="分类名" width="130" />
<el-table-column prop="list_url" label="列表 URL" min-width="320" show-overflow-tooltip />
<el-table-column label="状态" width="80">
<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="last_sync_at" label="上次同步" width="160">
<template #default="{ row }">{{ row.last_sync_at || '从未' }}</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="warning" :loading="syncingId === row.id" @click="syncOne(row)">同步</el-button>
<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-card>
<el-dialog v-model="batchVisible" title="批量添加采集源" width="640px">
<el-alert type="info" :closable="false" style="margin-bottom: 10px">
每行一条,格式:<code v-pre>分类名 URL</code>(URL 可带反引号)。提交后自动创建分类并立即开始采集。
</el-alert>
<el-input v-model="batchText" type="textarea" :rows="10"
:placeholder="'每日更新 https://mdcmai4.xyz/api/v1/videos?categoryId=27&page={{page}}&size=60\n清纯少女 https://mdcmai4.xyz/api/v1/videos?categoryId=13&page={{page}}&size=60'" />
<div v-if="batchResult" style="margin-top: 10px; font-size: 13px">
<div v-if="batchResult.created.length" style="color: #67c23a">
已创建 {{ batchResult.created.length }} 个并启动采集:{{ batchResult.created.map(c => c.name).join('、') }}
</div>
<div v-if="batchResult.skipped.length" style="color: #e6a23c">
跳过 {{ batchResult.skipped.length }} 条(URL 已存在):{{ batchResult.skipped.map(s => s.name).join('、') }}
</div>
<div v-if="batchResult.failed.length" style="color: #f56c6c">
失败 {{ batchResult.failed.length }} 条:{{ batchResult.failed.map(f => f.line).join(';') }}
</div>
</div>
<template #footer>
<el-button @click="batchVisible = false">关闭</el-button>
<el-button type="primary" :loading="batchSubmitting" @click="submitBatch">提交</el-button>
</template>
</el-dialog>
<el-dialog v-model="dialogVisible" :title="editing ? '编辑采集源' : '添加采集源'" width="560px">
<el-form :model="form" label-width="90px">
<el-form-item label="分类名">
<el-input v-model="form.name" maxlength="50" placeholder="如:每日更新(不存在会自动创建分类)" />
</el-form-item>
<el-form-item label="列表 URL">
<el-input v-model="form.list_url" type="textarea" :rows="3"
placeholder="https://example.com/api/v1/videos?categoryId=27&page={{page}}&size=60" />
</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 loading = ref(false)
const dialogVisible = ref(false)
const editing = ref(null)
const submitting = ref(false)
const syncingId = ref(0)
const syncAllLoading = ref(false)
const form = reactive({ name: '', list_url: '' })
const batchVisible = ref(false)
const batchText = ref('')
const batchSubmitting = ref(false)
const batchResult = ref(null)
async function submitBatch() {
if (!batchText.value.trim()) { ElMessage.warning('请粘贴内容'); return }
batchSubmitting.value = true
batchResult.value = null
try {
const r = await request({ url: '/admin/collection/sources/batch', method: 'post', data: { text: batchText.value } })
batchResult.value = r
if (r.created.length) { batchText.value = ''; setTimeout(load, 3000) }
else load()
} catch (e) {} finally {
batchSubmitting.value = false
}
}
onMounted(load)
async function load() {
loading.value = true
try {
const r = await request({ url: '/admin/collection/sources', method: 'get' })
list.value = r.list
} catch (e) {} finally {
loading.value = false
}
}
function showCreate() {
editing.value = null
Object.assign(form, { name: '', list_url: '' })
dialogVisible.value = true
}
function showEdit(row) {
editing.value = row
Object.assign(form, { name: row.name, list_url: row.list_url })
dialogVisible.value = true
}
async function save() {
if (!form.name || !form.list_url) { ElMessage.warning('请填写完整'); return }
if (!form.list_url.includes('{{page}}')) { ElMessage.warning('URL 需包含 {{page}} 页码占位符'); return }
submitting.value = true
try {
if (editing.value) {
await request({ url: `/admin/collection/sources/${editing.value.id}`, method: 'put', data: form })
ElMessage.success('已更新')
} else {
await request({ url: '/admin/collection/sources', method: 'post', data: form })
ElMessage.success('已添加,点击"同步"开始采集')
}
dialogVisible.value = false
load()
} catch (e) {} finally {
submitting.value = false
}
}
async function toggleStatus(row) {
try {
await request({
url: `/admin/collection/sources/${row.id}`, method: 'put',
data: { status: row.status === 1 ? 0 : 1 },
})
ElMessage.success('已更新')
load()
} catch (e) {}
}
async function del(row) {
await ElMessageBox.confirm(
`确认删除采集源「${row.name}」?已采集的视频不会被删除。`, '警告', { type: 'warning' }
)
try {
await request({ url: `/admin/collection/sources/${row.id}`, method: 'delete' })
ElMessage.success('已删除')
load()
} catch (e) {}
}
async function syncOne(row) {
syncingId.value = row.id
try {
await request({ url: `/admin/collection/sources/${row.id}/sync`, method: 'post', data: { max_pages: 0 } })
ElMessage.success(`「${row.name}」已触发后台同步,稍后刷新查看`)
setTimeout(load, 20000)
} catch (e) {} finally {
syncingId.value = 0
}
}
async function syncAll() {
syncAllLoading.value = true
try {
const r = await request({ url: '/admin/collection/sources/sync-all', method: 'post' })
ElMessage.success(`已触发 ${r.started.length} 个源的后台同步`)
setTimeout(load, 20000)
} catch (e) {} finally {
syncAllLoading.value = false
}
}
</script>
<style scoped>
.header { display: flex; justify-content: space-between; align-items: center; }
</style>