feat: 视频预览/数据看板/下载配置 三项业务功能
- 新增视频预览:VideoPlayer 组件、VideoController/DyCollectVideoService/Repository、VideoPageRequestDTO 对应后端改动 - 新增数据看板:workplace/statics.vue、RecordTable、相关 store/http/axios 改动 - 新增下载配置:DownloadConfigService、ConfigController、AppSet.vue、AppConfig - 其他:DyHttpClientService / 同步 Job(Collect, Favorit) / Cookie 表 / CommonService / ServiceExtension 的配套调整 - 仓库卫生:.gitignore 追加 feiniu 过滤;运维脚本归档到 docs/scripts/
This commit is contained in:
+115
-112
@@ -13,17 +13,12 @@ const columns = [
|
||||
{
|
||||
title: 'Cookie名',
|
||||
dataIndex: 'userName',
|
||||
width: 100,
|
||||
width: 120,
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status', width: 80 },
|
||||
{ title: '收藏路径', dataIndex: 'savePath', width: 100 },
|
||||
{ title: '喜欢路径', dataIndex: 'favSavePath', width: 100 },
|
||||
{ title: 'Up主路径', dataIndex: 'upSavePath', width: 100 },
|
||||
// { title: 'Cookie', dataIndex: 'cookies' },
|
||||
{ title: 'Up主secUserId', dataIndex: 'upSecUserIds' },
|
||||
{ title: '用户secUserId', dataIndex: 'secUserId' },
|
||||
{ title: '操作', dataIndex: 'edit', width: 200 },
|
||||
// { title: 'id', dataIndex: 'id', width: 200, hiden: false },
|
||||
{ title: '存储路径', dataIndex: 'paths', width: 300, ellipsis: true },
|
||||
{ title: 'SecUserId', dataIndex: 'userIds', width: 200, ellipsis: true },
|
||||
{ title: '操作', dataIndex: 'edit', width: 180, fixed: 'right' },
|
||||
];
|
||||
|
||||
// 定义数组中单个对象的类型:包含uper和uid字段(可选字符串类型,根据实际需求调整是否可选)
|
||||
@@ -47,16 +42,6 @@ type DataItem = {
|
||||
upSavePath?: string;
|
||||
};
|
||||
|
||||
// const dataSource = reactive<DataItem[]>([
|
||||
// {
|
||||
// userName: 'Li Zhi',
|
||||
// cookies: '131231',
|
||||
// savePath: 'x',
|
||||
// status: 1,
|
||||
// id: '1',
|
||||
// },
|
||||
// ]);
|
||||
|
||||
const loading = ref(false);
|
||||
const datas: UnwrapRef<DataItem[]> = reactive([]);
|
||||
const pagination = ref({
|
||||
@@ -153,9 +138,6 @@ function submit() {
|
||||
} else {
|
||||
copyObject(editRecord.value, resData);
|
||||
}
|
||||
// alert(JSON.stringify(form.upSecUserIdsJson));
|
||||
// console.log('1', authors);
|
||||
// console.log('2', res);
|
||||
useApiStore()
|
||||
.UpdateConfig(resData)
|
||||
.then((res) => {
|
||||
@@ -177,17 +159,15 @@ function submit() {
|
||||
|
||||
const editRecord = ref<DataItem>();
|
||||
|
||||
import { Modal } from 'ant-design-vue'; // 假设使用Ant Design Vue的Modal组件
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
const deleted = (id: string) => {
|
||||
// 显示确认对话框
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这条记录吗?此操作不可撤销。',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
// 用户确认后执行删除操作
|
||||
useApiStore()
|
||||
.deleteCookie(id)
|
||||
.then((res) => {
|
||||
@@ -200,15 +180,11 @@ const deleted = (id: string) => {
|
||||
});
|
||||
},
|
||||
onCancel: () => {
|
||||
// 用户取消删除,不执行任何操作
|
||||
console.log('已取消删除');
|
||||
},
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 编辑
|
||||
* @param record
|
||||
*/
|
||||
|
||||
function edit(record: DataItem) {
|
||||
editRecord.value = record;
|
||||
copyObject(form, record);
|
||||
@@ -264,7 +240,7 @@ const rowCount = 4;
|
||||
<a-form-item label="喜欢的存储路径" name="favSavePath">
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<a-input v-model:value="form.favSavePath" style="flex: 1;" />
|
||||
<a-tooltip title="同步“我喜欢的”视频时,必填!!!">
|
||||
<a-tooltip title="同步'我喜欢的'视频时,必填!!!">
|
||||
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px;" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
@@ -272,7 +248,7 @@ const rowCount = 4;
|
||||
<a-form-item label="SecUserId" name="secUserId">
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<a-input v-model:value="form.secUserId" style="flex: 1;" />
|
||||
<a-tooltip title="同步“我喜欢的”视频时,必填!!!">
|
||||
<a-tooltip title="同步'我喜欢的'视频时,必填!!!">
|
||||
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px;" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
@@ -287,8 +263,7 @@ const rowCount = 4;
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="博主信息" name="upSecUserIdsJson">
|
||||
<a-form-item-rest> <!-- 用这个包裹所有辅助元素 -->
|
||||
<!-- 添加行按钮 -->
|
||||
<a-form-item-rest>
|
||||
<a-button type="primary" @click="addRow" style="margin-bottom: 12px">
|
||||
添加抖音Up主
|
||||
<template #icon>
|
||||
@@ -296,23 +271,15 @@ const rowCount = 4;
|
||||
</template>
|
||||
</a-button>
|
||||
|
||||
<!-- 动态渲染所有行 -->
|
||||
<div v-for="(row, index) in form.upSecUserIdsJson" :key="index" style="display: flex; gap: 12px; margin-bottom: 8px; align-items: center">
|
||||
<!-- uper输入框 -->
|
||||
<a-input v-model:value="row.uper" placeholder="博主别名,可自定义" style="flex: 1" />
|
||||
|
||||
<!-- uid输入框 -->
|
||||
<a-input v-model:value="row.uid" placeholder="博主secUserId" style="flex: 3" />
|
||||
|
||||
<!-- syncAll 开关字段 -->
|
||||
<div style="flex: 1; display: flex; align-items: center;">
|
||||
<a-tooltip title="默认关闭,仅同步 UP 主最新一页数据;开启将同步全部作品(量大不建议开启)">
|
||||
<span style="margin-right: 8px; cursor: default;color:#faad14">同步全部作品</span>
|
||||
</a-tooltip>
|
||||
<a-switch v-model:checked="row.syncAll" />
|
||||
</div>
|
||||
|
||||
<!-- 删除当前行按钮 -->
|
||||
<a-button type="text" danger @click="removeRow(index)">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
@@ -330,10 +297,8 @@ const rowCount = 4;
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 成员表格 -->
|
||||
<a-table v-bind="$attrs" :columns="columns" :dataSource="dataSource" :pagination="false">
|
||||
<a-table v-bind="$attrs" :columns="columns" :dataSource="dataSource" :pagination="false" :scroll="{ x: 900 }">
|
||||
<template #title>
|
||||
<!-- 关键修改:将 justify-between 改为 justify-end(使子元素靠右侧对齐) -->
|
||||
<div class="flex justify-end pr-4">
|
||||
<a-button type="primary" @click="GetRecords()" :loading="formLoading" class="mr-2">
|
||||
<template #icon>
|
||||
@@ -359,23 +324,51 @@ const rowCount = 4;
|
||||
</template>
|
||||
</a-badge>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'cookies'">
|
||||
<!-- 触发按钮 -->
|
||||
<a-button @click="showCookies(record)">查看</a-button>
|
||||
<template v-else-if="column.dataIndex === 'paths'">
|
||||
<div class="path-info">
|
||||
<div class="path-row" v-if="record.savePath">
|
||||
<span class="path-label">收藏:</span>
|
||||
<a-tooltip :title="record.savePath">
|
||||
<span class="path-value">{{ record.savePath }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="path-row" v-if="record.favSavePath">
|
||||
<span class="path-label">喜欢:</span>
|
||||
<a-tooltip :title="record.favSavePath">
|
||||
<span class="path-value">{{ record.favSavePath }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="path-row" v-if="record.upSavePath">
|
||||
<span class="path-label">Up主:</span>
|
||||
<a-tooltip :title="record.upSavePath">
|
||||
<span class="path-value">{{ record.upSavePath }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'upSecUserIds'">
|
||||
<!-- 触发按钮 -->
|
||||
<a-button @click="showUpers(record)">查看</a-button>
|
||||
<template v-else-if="column.dataIndex === 'userIds'">
|
||||
<div class="userid-info">
|
||||
<div class="userid-row" v-if="record.secUserId">
|
||||
<span class="userid-label">用户:</span>
|
||||
<a-tooltip :title="record.secUserId">
|
||||
<span class="userid-value">{{ record.secUserId }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="userid-row" v-if="record.upSecUserIds">
|
||||
<span class="userid-label">Up主:</span>
|
||||
<a-button size="small" type="link" @click="showUpers(record)" style="padding: 0; height: auto;">查看</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'edit'">
|
||||
<a-button :disabled="showModal" type="link" @click="edit(record)">
|
||||
<a-button :disabled="showModal" type="link" @click="edit(record)" size="small">
|
||||
<template #icon>
|
||||
<EditFilled />
|
||||
</template>
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<a-button type="link" @click="deleted(record.id)" danger>
|
||||
<a-button type="link" @click="deleted(record.id)" danger size="small">
|
||||
<template #icon>
|
||||
<DeleteFilled />
|
||||
</template>
|
||||
@@ -388,17 +381,13 @@ const rowCount = 4;
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- Modal弹窗 -->
|
||||
<a-modal title="Cookie 详情" :visible="showCookiesModal" style="width:1200px;" @cancel="showCookiesModal = false" @ok="showCookiesModal = false">
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="cookie-content">
|
||||
{{ showCookiesData || '无Cookie数据' }}
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- Modal弹窗 -->
|
||||
<a-modal title="要同步的Up主信息" :visible="showUpersModal" style="width:1200px;" @cancel="showUpersModal = false" @ok="showUpersModal = false">
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="cookie-content">
|
||||
{{ showUpersData || '未设置' }}
|
||||
</div>
|
||||
@@ -414,100 +403,114 @@ const rowCount = 4;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 透明滚动条样式 - WebKit内核浏览器 */
|
||||
.cookie-content::-webkit-scrollbar {
|
||||
width: 6px; /* 更细的滚动条 */
|
||||
}
|
||||
|
||||
/* 轨道完全透明 */
|
||||
.cookie-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 滑块半透明(默认几乎看不见) */
|
||||
.cookie-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(150, 150, 150, 0.2); /* 浅灰透明 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* hover时稍微显示一点 */
|
||||
.cookie-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(150, 150, 150, 0.4); /* 略深一点的透明 */
|
||||
}
|
||||
|
||||
/* 角落也透明 */
|
||||
.cookie-content::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Firefox 透明滚动条适配 */
|
||||
.cookie-content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
|
||||
}
|
||||
|
||||
.cookie-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.cookie-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.cookie-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cookie-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cookie-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(150, 150, 150, 0.4);
|
||||
}
|
||||
|
||||
.cookie-content::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cookie-content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
|
||||
}
|
||||
|
||||
/* ---------------------- 新增:a-textarea 透明滚动条 ---------------------- */
|
||||
/* 1. 穿透 scoped,定位 a-textarea 内部的原生 textarea 元素 */
|
||||
:deep(.ant-input-textarea-input) {
|
||||
/* 确保内容超出时显示滚动条(a-textarea 默认已配置,可省略) */
|
||||
overflow-y: auto;
|
||||
/* Firefox 透明滚动条:thin 细滚动条 + 滑块颜色/轨道颜色 */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
|
||||
}
|
||||
|
||||
/* 2. WebKit 浏览器(Chrome/Safari/Edge)透明滚动条 */
|
||||
/* 滚动条宽度 */
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar {
|
||||
width: 6px; /* 与 cookie-content 保持一致的细滚动条 */
|
||||
height: 6px; /* 横向滚动条(如需) */
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
/* 滚动条轨道(完全透明) */
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 滚动条滑块(半透明,hover 时加深) */
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb {
|
||||
background: rgba(150, 150, 150, 0.2); /* 浅灰透明,默认几乎看不见 */
|
||||
border-radius: 3px; /* 圆角优化 */
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(150, 150, 150, 0.4); /* hover 时略深,提升交互感知 */
|
||||
background: rgba(150, 150, 150, 0.4);
|
||||
}
|
||||
|
||||
/* 滚动条角落(完全透明,避免留白) */
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
.path-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.path-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.path-label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.path-value {
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.userid-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.userid-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.userid-label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.userid-value {
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,6 +16,47 @@
|
||||
<a-alert message="每次扫描行数,第一次同步完成后,可以适当调高提高效率" type="success" />
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">下载配置</a-divider>
|
||||
<a-alert message="默认情况下,系统不会自动下载视频,避免占用过多存储空间。如需下载,请配置以下选项。" type="info" style="margin-bottom: 16px;" />
|
||||
|
||||
<a-form-item label="启用自动下载">
|
||||
<a-switch v-model:checked="formState.EnableAutoDownload" />
|
||||
<a-alert message="关闭后,系统只同步视频信息到数据库,不会下载视频文件" type="warning" style="margin-top: 8px;" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="下载时间范围(天)" v-if="formState.EnableAutoDownload">
|
||||
<a-input-number v-model:value="formState.DownloadDaysRange" :min="0" :max="3650" style="width: 200px;" />
|
||||
<a-alert message="0表示不限制时间,只下载指定天数内的视频(例如:30表示只下载最近30天的视频)" type="success" style="margin-top: 8px;" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="下载喜欢的视频" v-if="formState.EnableAutoDownload">
|
||||
<a-switch v-model:checked="formState.DownloadFavorite" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="下载收藏的视频" v-if="formState.EnableAutoDownload">
|
||||
<a-switch v-model:checked="formState.DownloadCollect" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="作者白名单" v-if="formState.EnableAutoDownload">
|
||||
<a-textarea
|
||||
v-model:value="formState.AuthorWhitelist"
|
||||
placeholder="每行一个作者名称或ID,留空则不限制 例如: 作者1 作者2 MS4wLjABAAAA..."
|
||||
:rows="4"
|
||||
style="width: 500px;"
|
||||
/>
|
||||
<a-alert message="配置后,只有白名单中的作者视频才会被下载。留空则不限制。" type="success" style="margin-top: 8px;" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="作者黑名单" v-if="formState.EnableAutoDownload">
|
||||
<a-textarea
|
||||
v-model:value="formState.AuthorBlacklist"
|
||||
placeholder="每行一个作者名称或ID,留空则不限制 例如: 作者1 作者2 MS4wLjABAAAA..."
|
||||
:rows="4"
|
||||
style="width: 500px;"
|
||||
/>
|
||||
<a-alert message="配置后,黑名单中的作者视频不会被下载。留空则不限制。" type="success" style="margin-top: 8px;" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :wrapper-col="{ span: 10, offset: 3 }">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="onUpdate" v-if="componentDisabled">修改配置</a-button>
|
||||
@@ -43,11 +84,23 @@ interface FormState {
|
||||
Cron: string;
|
||||
Id: string;
|
||||
BatchCount: number;
|
||||
EnableAutoDownload?: boolean;
|
||||
DownloadDaysRange?: number;
|
||||
AuthorWhitelist?: string;
|
||||
AuthorBlacklist?: string;
|
||||
DownloadFavorite?: boolean;
|
||||
DownloadCollect?: boolean;
|
||||
}
|
||||
const formState: UnwrapRef<FormState> = reactive({
|
||||
Cron: '30',
|
||||
Id: '0',
|
||||
BatchCount: 10,
|
||||
EnableAutoDownload: false,
|
||||
DownloadDaysRange: 0,
|
||||
AuthorWhitelist: '',
|
||||
AuthorBlacklist: '',
|
||||
DownloadFavorite: false,
|
||||
DownloadCollect: false,
|
||||
});
|
||||
|
||||
const rules: Record<string, Rule[]> = {
|
||||
@@ -62,6 +115,21 @@ const getConfig = () => {
|
||||
formState.Cron = res.data.cron;
|
||||
formState.Id = res.data.id;
|
||||
formState.BatchCount = res.data.batchCount;
|
||||
// 下载配置
|
||||
formState.EnableAutoDownload = res.data.enableAutoDownload ?? false;
|
||||
formState.DownloadDaysRange = res.data.downloadDaysRange ?? 0;
|
||||
try {
|
||||
formState.AuthorWhitelist = res.data.authorWhitelist ? JSON.parse(res.data.authorWhitelist).join('\n') : '';
|
||||
} catch {
|
||||
formState.AuthorWhitelist = '';
|
||||
}
|
||||
try {
|
||||
formState.AuthorBlacklist = res.data.authorBlacklist ? JSON.parse(res.data.authorBlacklist).join('\n') : '';
|
||||
} catch {
|
||||
formState.AuthorBlacklist = '';
|
||||
}
|
||||
formState.DownloadFavorite = res.data.downloadFavorite ?? false;
|
||||
formState.DownloadCollect = res.data.downloadCollect ?? false;
|
||||
} else {
|
||||
message.error(res.erro, 8);
|
||||
}
|
||||
@@ -79,12 +147,32 @@ const onSubmit = () => {
|
||||
.validate()
|
||||
.then(() => {
|
||||
console.log('values', formState, toRaw(formState));
|
||||
// 处理作者白名单和黑名单,转换为JSON数组
|
||||
const submitData = { ...toRaw(formState) };
|
||||
if (submitData.AuthorWhitelist) {
|
||||
const whitelist = submitData.AuthorWhitelist.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
submitData.AuthorWhitelist = whitelist.length > 0 ? JSON.stringify(whitelist) : '';
|
||||
} else {
|
||||
submitData.AuthorWhitelist = '';
|
||||
}
|
||||
if (submitData.AuthorBlacklist) {
|
||||
const blacklist = submitData.AuthorBlacklist.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
submitData.AuthorBlacklist = blacklist.length > 0 ? JSON.stringify(blacklist) : '';
|
||||
} else {
|
||||
submitData.AuthorBlacklist = '';
|
||||
}
|
||||
|
||||
useApiStore()
|
||||
.apiUpdateConfig(toRaw(formState))
|
||||
.apiUpdateConfig(submitData)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
message.success('配置修改生效,同步任务将按照新的规则执行');
|
||||
componentDisabled.value = true;
|
||||
getConfig(); // 重新加载配置
|
||||
} else {
|
||||
message.error(res.erro, 8);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
<a-form-item label="抖音作者" ref="author" name="author">
|
||||
<a-input v-model:value="quaryData.author"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="视频类型">
|
||||
<a-select
|
||||
v-model:value="selectedCategories"
|
||||
mode="multiple"
|
||||
placeholder="请选择视频类型(可多选)"
|
||||
style="width: 300px"
|
||||
:options="categoryOptions"
|
||||
:loading="categoryLoading"
|
||||
allow-clear
|
||||
@change="onCategoryChanged"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model:value="quaryData.viedoType" button-style="solid" @change="onViedoTypeChanged">
|
||||
<a-radio-button value="*">全部</a-radio-button>
|
||||
@@ -15,69 +28,165 @@
|
||||
<a-radio-button value="3">关注的</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="删除状态">
|
||||
<a-select
|
||||
v-model:value="quaryData.isDeleted"
|
||||
placeholder="全部"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
@change="handleQuery"
|
||||
>
|
||||
<a-select-option :value="undefined">全部</a-select-option>
|
||||
<a-select-option :value="false">未删除</a-select-option>
|
||||
<a-select-option :value="true">已删除</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="GetRecords">查询</a-button>
|
||||
<a-button type="primary" @click="handleQuery">查询</a-button>
|
||||
<a-button type="danger" @click="StartNow">立即重新同步</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-table :columns="columns" :data-source="dataSource" bordered :pagination="pagination" @change="handleTableChange" :loading="loading">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
bordered
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<!-- 如果视频文件不存在,显示"已删除" -->
|
||||
<span v-if="!record.videoSavePath" style="color: #999; font-size: 13px;">已删除</span>
|
||||
<!-- 如果视频文件存在,显示预览按钮 -->
|
||||
<a-button v-else type="link" size="small" @click="handlePreview(record)">
|
||||
<template #icon><PlayCircleOutlined /></template>
|
||||
预览
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 视频播放器 -->
|
||||
<VideoPlayer ref="videoPlayerRef" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, reactive, ref } from 'vue';
|
||||
import { defineComponent, reactive, ref, watch } from 'vue';
|
||||
import { useApiStore } from '@/store';
|
||||
import type { UnwrapRef } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
|
||||
import { PlayCircleOutlined } from '@ant-design/icons-vue';
|
||||
import VideoPlayer from '@/components/video/VideoPlayer.vue';
|
||||
type RangeValue = [Dayjs, Dayjs];
|
||||
import 'dayjs/locale/zh-cn';
|
||||
dayjs.locale('zh-cn');
|
||||
const columns = ref([
|
||||
{
|
||||
title: '同步时间',
|
||||
dataIndex: 'syncTimeStr',
|
||||
// sorter: true,
|
||||
align: 'center',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '同步类型',
|
||||
dataIndex: 'viedoTypeStr',
|
||||
// sorter: true,
|
||||
align: 'center',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '视频类型',
|
||||
dataIndex: 'viedoCate',
|
||||
// sorter: true,
|
||||
width: 300,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '作者',
|
||||
dataIndex: 'author',
|
||||
// sorter: true,
|
||||
align: 'center',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '视频名称',
|
||||
dataIndex: 'videoTitle',
|
||||
// sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
|
||||
const videoPlayerRef = ref<InstanceType<typeof VideoPlayer> | null>(null);
|
||||
|
||||
// 当前排序状态
|
||||
const currentSortField = ref<string | null>(null);
|
||||
const currentSortOrder = ref<'asc' | 'desc' | null>(null);
|
||||
|
||||
// 处理列头点击排序
|
||||
const handleColumnSort = (fieldKey: string) => {
|
||||
console.log('列头点击排序:', fieldKey);
|
||||
|
||||
// 如果点击的是当前排序列,切换排序方向;否则设置为升序
|
||||
if (currentSortField.value === fieldKey) {
|
||||
if (currentSortOrder.value === 'asc') {
|
||||
currentSortOrder.value = 'desc';
|
||||
} else if (currentSortOrder.value === 'desc') {
|
||||
// 第三次点击取消排序
|
||||
currentSortField.value = null;
|
||||
currentSortOrder.value = null;
|
||||
}
|
||||
} else {
|
||||
currentSortField.value = fieldKey;
|
||||
currentSortOrder.value = 'asc';
|
||||
}
|
||||
|
||||
// 更新查询参数
|
||||
quaryData.sortField = currentSortField.value;
|
||||
quaryData.sortOrder = currentSortOrder.value;
|
||||
|
||||
console.log('设置排序参数:', {
|
||||
sortField: quaryData.sortField,
|
||||
sortOrder: quaryData.sortOrder
|
||||
});
|
||||
|
||||
// 更新列配置以反映排序状态
|
||||
updateColumnSortState();
|
||||
|
||||
// 重置到第一页并重新查询
|
||||
pagination.value.current = 1;
|
||||
GetRecords();
|
||||
};
|
||||
|
||||
// 更新列配置的排序状态
|
||||
const updateColumnSortState = () => {
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.sorter && col.key) {
|
||||
if (currentSortField.value === col.key) {
|
||||
col.sorter.order = currentSortOrder.value === 'asc' ? 'ascend' : currentSortOrder.value === 'desc' ? 'descend' : null;
|
||||
} else {
|
||||
col.sorter.order = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 强制触发响应式更新
|
||||
columns.value = [...columns.value];
|
||||
};
|
||||
|
||||
// 监听排序状态变化,自动更新列配置
|
||||
watch([currentSortField, currentSortOrder], () => {
|
||||
updateColumnSortState();
|
||||
}, { immediate: true });
|
||||
|
||||
// 创建可排序列配置
|
||||
const createSortableColumn = (title: string, dataIndex: string, key: string, width?: number, align: string = 'left') => {
|
||||
const col: any = {
|
||||
title,
|
||||
dataIndex,
|
||||
key,
|
||||
width,
|
||||
align,
|
||||
sorter: {
|
||||
compare: () => 0, // 禁用前端排序
|
||||
multiple: false,
|
||||
order: null, // 初始化为 null,后续通过 updateColumnSortState 更新
|
||||
},
|
||||
customHeaderCell: () => {
|
||||
return {
|
||||
onClick: () => {
|
||||
handleColumnSort(key);
|
||||
},
|
||||
style: { cursor: 'pointer' }
|
||||
};
|
||||
},
|
||||
};
|
||||
return col;
|
||||
};
|
||||
|
||||
const columns = ref([
|
||||
createSortableColumn('同步时间', 'syncTimeStr', 'syncTime', 180, 'center'),
|
||||
createSortableColumn('同步类型', 'viedoTypeStr', 'viedoType', 100, 'center'),
|
||||
createSortableColumn('视频类型', 'viedoCate', 'viedoCate', 300, 'center'),
|
||||
createSortableColumn('作者', 'author', 'author', 150, 'center'),
|
||||
createSortableColumn('视频名称', 'videoTitle', 'videoTitle', undefined, 'left'),
|
||||
createSortableColumn('用户', 'dyUser', 'dyUser', 200, 'center'),
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'dyUser',
|
||||
// sorter: true,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
},
|
||||
]);
|
||||
const loading = ref(false);
|
||||
@@ -90,7 +199,11 @@ interface QuaryParam {
|
||||
pageSize: number;
|
||||
author: string;
|
||||
tag: string;
|
||||
tags?: string[];
|
||||
viedoType: string;
|
||||
isDeleted?: boolean | null;
|
||||
sortField?: string | null;
|
||||
sortOrder?: string | null;
|
||||
}
|
||||
|
||||
const value1 = ref<RangeValue>();
|
||||
@@ -104,26 +217,127 @@ const quaryData: UnwrapRef<QuaryParam> = reactive({
|
||||
pageSize: 20,
|
||||
author: '',
|
||||
tag: '',
|
||||
tags: undefined,
|
||||
viedoType: '*',
|
||||
isDeleted: undefined,
|
||||
sortField: null,
|
||||
sortOrder: null,
|
||||
});
|
||||
|
||||
// 视频类型相关
|
||||
const selectedCategories = ref<string[]>([]);
|
||||
const categoryOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const categoryLoading = ref(false);
|
||||
|
||||
// 加载视频类型列表
|
||||
const loadCategories = async () => {
|
||||
categoryLoading.value = true;
|
||||
try {
|
||||
console.log('开始加载视频类型列表...');
|
||||
const res = await useApiStore().VideoCategories();
|
||||
console.log('视频类型 API 响应(完整):', JSON.stringify(res, null, 2));
|
||||
console.log('视频类型 API 响应类型:', typeof res);
|
||||
console.log('视频类型 API 响应是否为数组:', Array.isArray(res));
|
||||
|
||||
// 处理不同的响应格式
|
||||
let categories: string[] = [];
|
||||
|
||||
if (Array.isArray(res)) {
|
||||
// 如果直接返回数组
|
||||
categories = res;
|
||||
} else if (res && typeof res === 'object') {
|
||||
// 如果是对象,尝试获取 data 字段
|
||||
if (res.code === 0 && res.data) {
|
||||
categories = Array.isArray(res.data) ? res.data : [];
|
||||
} else if (Array.isArray(res.data)) {
|
||||
categories = res.data;
|
||||
} else if (res.code === 0 && Array.isArray(res)) {
|
||||
categories = res as any;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('提取的视频类型数据:', categories);
|
||||
|
||||
if (categories && categories.length > 0) {
|
||||
categoryOptions.value = categories.map(cat => ({
|
||||
label: cat || '其他',
|
||||
value: cat || '其他',
|
||||
}));
|
||||
console.log('视频类型选项:', categoryOptions.value);
|
||||
} else {
|
||||
console.warn('视频类型数据为空,可能原因:1. 数据库中没有视频数据 2. 所有视频的 Tag1 和 Tag2 都为空');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载视频类型列表失败:', err);
|
||||
} finally {
|
||||
categoryLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 视频类型选择变化
|
||||
const onCategoryChanged = (values: string[]) => {
|
||||
selectedCategories.value = values;
|
||||
if (values && values.length > 0) {
|
||||
quaryData.tags = values;
|
||||
quaryData.tag = ''; // 清空单标签筛选
|
||||
} else {
|
||||
quaryData.tags = undefined;
|
||||
}
|
||||
pagination.value.current = 1;
|
||||
GetRecords();
|
||||
};
|
||||
// 查询处理函数:先刷新类型列表,再查询记录
|
||||
const handleQuery = async () => {
|
||||
// 先刷新视频类型列表(确保包含最新的类型)
|
||||
await loadCategories();
|
||||
// 然后查询记录
|
||||
GetRecords();
|
||||
};
|
||||
|
||||
const GetRecords = () => {
|
||||
loading.value = true;
|
||||
quaryData.pageIndex = pagination.value.current;
|
||||
quaryData.pageSize = pagination.value.defaultPageSize;
|
||||
|
||||
// 构建请求参数,确保排序参数被包含
|
||||
const requestParams = {
|
||||
...quaryData,
|
||||
sortField: quaryData.sortField || null,
|
||||
sortOrder: quaryData.sortOrder || null,
|
||||
};
|
||||
|
||||
console.log('发送查询请求,参数:', JSON.stringify(requestParams, null, 2));
|
||||
|
||||
useApiStore()
|
||||
.VideoPageList(quaryData)
|
||||
.VideoPageList(requestParams)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
console.log('查询响应:', res);
|
||||
if (res.code === 0) {
|
||||
console.log('接收到的数据:', res.data.data);
|
||||
console.log('数据条数:', res.data.data?.length);
|
||||
if (res.data.data && res.data.data.length > 0) {
|
||||
console.log('第一条数据的同步时间:', res.data.data[0].syncTimeStr);
|
||||
console.log('最后一条数据的同步时间:', res.data.data[res.data.data.length - 1].syncTimeStr);
|
||||
}
|
||||
|
||||
dataSource.value = res.data.data;
|
||||
pagination.value.current = res.data.pageIndex;
|
||||
pagination.value.defaultPageSize = res.data.pageSize;
|
||||
pagination.value.total = res.data.total;
|
||||
pagination.value.showTotal = () => `共 ${res.data.total} 条`;
|
||||
|
||||
// 数据加载完成后,确保排序状态正确显示
|
||||
updateColumnSortState();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
loading.value = false;
|
||||
console.error('查询失败:', err);
|
||||
});
|
||||
};
|
||||
onMounted(() => {
|
||||
loadCategories(); // 加载视频类型列表
|
||||
GetRecords();
|
||||
});
|
||||
const pagination = ref({
|
||||
@@ -133,12 +347,88 @@ const pagination = ref({
|
||||
showTotal: () => `共 ${0} 条`,
|
||||
});
|
||||
|
||||
const handleTableChange = (e) => {
|
||||
console.log(e);
|
||||
pagination.value.current = e.current;
|
||||
pagination.value.defaultPageSize = e.defaultPageSize;
|
||||
pagination.value.total = e.total;
|
||||
pagination.value.showTotal = () => `共 ${e.total} 条`;
|
||||
const handleTableChange = (e: any) => {
|
||||
console.log('表格变化事件:', e);
|
||||
console.log('事件对象键:', Object.keys(e));
|
||||
console.log('sorter 属性:', e.sorter);
|
||||
|
||||
// 处理分页变化
|
||||
if (e.current !== undefined) {
|
||||
pagination.value.current = e.current;
|
||||
}
|
||||
if (e.pageSize !== undefined) {
|
||||
pagination.value.defaultPageSize = e.pageSize;
|
||||
}
|
||||
if (e.total !== undefined) {
|
||||
pagination.value.total = e.total;
|
||||
pagination.value.showTotal = () => `共 ${e.total} 条`;
|
||||
}
|
||||
|
||||
// 处理排序变化(Ant Design Vue 3.x 中排序通过 change 事件的 sorter 属性传递)
|
||||
if (e.sorter) {
|
||||
console.log('检测到排序变化:', e.sorter);
|
||||
handleSorterChange(e.sorter);
|
||||
} else {
|
||||
// 如果没有排序变化,直接查询
|
||||
console.log('无排序变化,直接查询');
|
||||
GetRecords();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理排序变化
|
||||
const handleSorterChange = (sorter: any) => {
|
||||
console.log('排序变化事件:', sorter);
|
||||
console.log('排序变化类型:', typeof sorter, Array.isArray(sorter) ? '数组' : '对象');
|
||||
console.log('当前查询参数:', quaryData);
|
||||
|
||||
// Ant Design Vue 3.x 的 sorterChange 可能传递单个对象或对象数组
|
||||
let activeSorter: any = null;
|
||||
|
||||
if (Array.isArray(sorter)) {
|
||||
// 如果是数组,找到第一个有 order 的排序器
|
||||
activeSorter = sorter.find((s: any) => s && s.order);
|
||||
} else if (sorter && typeof sorter === 'object') {
|
||||
// 如果是单个对象
|
||||
activeSorter = sorter;
|
||||
}
|
||||
|
||||
console.log('活动排序器:', activeSorter);
|
||||
|
||||
if (!activeSorter || !activeSorter.field || !activeSorter.order) {
|
||||
// 没有排序或取消排序
|
||||
quaryData.sortField = null;
|
||||
quaryData.sortOrder = null;
|
||||
console.log('取消排序');
|
||||
} else {
|
||||
// 映射前端字段名(key)到后端字段名
|
||||
const fieldMap: Record<string, string> = {
|
||||
'syncTime': 'syncTime',
|
||||
'viedoType': 'viedoType',
|
||||
'viedoCate': 'viedoCate',
|
||||
'author': 'author',
|
||||
'videoTitle': 'videoTitle',
|
||||
'dyUser': 'dyUser',
|
||||
};
|
||||
|
||||
const field = activeSorter.field;
|
||||
const order = activeSorter.order;
|
||||
const backendField = fieldMap[field] || field;
|
||||
|
||||
// 有排序:ascend -> asc, descend -> desc
|
||||
quaryData.sortField = backendField;
|
||||
quaryData.sortOrder = order === 'ascend' ? 'asc' : 'desc';
|
||||
|
||||
console.log('设置排序参数:', {
|
||||
field: field,
|
||||
backendField: backendField,
|
||||
order: order,
|
||||
sortField: quaryData.sortField,
|
||||
sortOrder: quaryData.sortOrder
|
||||
});
|
||||
}
|
||||
|
||||
// 重置到第一页并重新查询
|
||||
pagination.value.current = 1;
|
||||
GetRecords();
|
||||
};
|
||||
|
||||
@@ -159,6 +449,36 @@ const onViedoTypeChanged = (e) => {
|
||||
pagination.value.current = 1;
|
||||
GetRecords();
|
||||
};
|
||||
|
||||
// 预览视频
|
||||
const handlePreview = (record: any) => {
|
||||
console.log('handlePreview called', record);
|
||||
|
||||
if (!record.id) {
|
||||
console.error('视频记录没有 id', record);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!videoPlayerRef.value) {
|
||||
console.error('videoPlayerRef.value 为 null,组件可能未正确挂载');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('调用 videoPlayerRef.value.open');
|
||||
try {
|
||||
videoPlayerRef.value.open(record.id, {
|
||||
id: record.id,
|
||||
videoTitle: record.videoTitle || '未知标题',
|
||||
author: record.author || '未知作者',
|
||||
viedoCate: record.viedoCate || '未分类',
|
||||
resolution: record.resolution || '未知',
|
||||
fileSize: record.fileSize || 0,
|
||||
videoSavePath: record.videoSavePath,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('打开视频播放器失败:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
+1126
-23
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user