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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user