213 lines
8.0 KiB
Vue
213 lines
8.0 KiB
Vue
<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>
|