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:
yuming
2026-04-25 00:22:33 +08:00
parent ca530312d0
commit b328e82eb2
27 changed files with 3602 additions and 348 deletions
+1 -2
View File
@@ -1,2 +1 @@
VITE_BASE_URL=/
VITE_API_URL=http://localhost:10101
VITE_API_URL=http://localhost:10101
+580
View File
@@ -0,0 +1,580 @@
<template>
<a-modal
v-model:visible="visible"
:width="900"
:footer="null"
:maskClosable="false"
@cancel="handleClose"
class="video-player-modal"
:destroyOnClose="true"
>
<template #title>
<div class="modal-title-wrapper">
<span class="modal-title-text">{{ modalTitle }}</span>
</div>
</template>
<div class="video-player-container" v-if="currentVideoId">
<!-- 视频信息 -->
<div class="video-info" v-if="currentVideoInfo">
<div class="info-row">
<span class="label">作者:</span>
<span class="value">{{ currentVideoInfo.author || '未知' }}</span>
</div>
<div class="info-row">
<span class="label">分类:</span>
<span class="value">{{ currentVideoInfo.viedoCate || '未分类' }}</span>
</div>
<div class="info-row">
<span class="label">分辨率:</span>
<span class="value">{{ currentVideoInfo.resolution || '未知' }}</span>
</div>
<div class="info-row">
<span class="label">文件大小:</span>
<span class="value">{{ formatFileSize(currentVideoInfo.fileSize || 0) }}</span>
</div>
</div>
<!-- 视频播放器 -->
<div class="video-wrapper" v-if="currentVideoId">
<video
ref="videoPlayer"
:src="videoUrl"
controls
preload="auto"
class="video-element"
@error="handleVideoError"
@loadstart="handleLoadStart"
@loadedmetadata="handleLoadedMetadata"
@canplay="handleCanPlay"
@canplaythrough="handleCanPlayThrough"
@progress="handleProgress"
@waiting="handleWaiting"
@playing="handlePlaying"
>
您的浏览器不支持视频播放
</video>
</div>
<div v-else class="video-wrapper">
<a-empty description="视频信息加载中..." />
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-overlay">
<a-spin size="large" tip="加载视频中..." />
</div>
<!-- 错误提示 -->
<div v-if="error" class="error-message">
<a-alert
:message="error"
type="error"
show-icon
closable
@close="error = ''"
/>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, watch, onUnmounted } from 'vue';
import { message } from 'ant-design-vue';
import Cookie from 'js-cookie';
interface VideoInfo {
id: string;
videoTitle: string;
author: string;
viedoCate: string;
resolution: string;
fileSize: number;
videoSavePath?: string;
}
const props = defineProps<{
videoId?: string;
videoInfo?: VideoInfo;
}>();
const emit = defineEmits<{
close: [];
}>();
const visible = ref(false);
const loading = ref(false);
const error = ref('');
const videoPlayer = ref<HTMLVideoElement | null>(null);
const currentVideoId = ref<string>('');
const currentVideoInfo = ref<VideoInfo | null>(null);
const videoDirectUrl = ref<string>('');
// 视频URL - 直接使用流式 URL,让浏览器处理 Range 请求
const videoUrl = computed(() => videoDirectUrl.value);
// 标题(完整显示,支持多行)
const modalTitle = computed(() => {
return currentVideoInfo.value?.videoTitle || '视频预览';
});
// 设置视频流 URL,让浏览器直接处理 Range 请求实现流式播放
const loadVideoStream = async (videoId: string) => {
try {
loading.value = true;
error.value = '';
// 构建视频流 URL,浏览器会自动发送 Range 请求
// 添加时间戳避免缓存问题
const url = `/api/video/stream/${videoId}?t=${Date.now()}`;
console.log('[VideoPlayer] 设置视频流 URL:', url);
// 直接使用 URL,让浏览器处理 Range 请求
// 浏览器会自动发送 Range 请求头,实现边加载边播放
videoDirectUrl.value = url;
// 确保 video 元素存在,并且等待 DOM 更新
await new Promise(resolve => setTimeout(resolve, 100));
if (videoPlayer.value) {
// 先清空 src,避免使用旧的 URL
videoPlayer.value.src = '';
// 等待一下,确保清空操作完成
await new Promise(resolve => setTimeout(resolve, 50));
// 设置新的视频 URL
videoPlayer.value.src = videoDirectUrl.value;
console.log('[VideoPlayer] 视频 src 已设置:', videoDirectUrl.value);
// 加载视频元数据(不等待完整下载)
// 浏览器会自动发送 Range 请求,只加载必要的部分
videoPlayer.value.load();
console.log('[VideoPlayer] 调用 video.load(),开始加载视频');
// 视频元素会自动处理 Range 请求,实现流式播放
// 当有足够的数据可以播放时(通常只需要几秒的数据),浏览器就可以开始播放
// 不需要等待整个文件下载完成
// 监听网络状态变化,用于调试
const checkNetworkState = () => {
const networkState = videoPlayer.value?.networkState;
const readyState = videoPlayer.value?.readyState;
const buffered = videoPlayer.value?.buffered;
console.log('[VideoPlayer] 网络状态:', {
networkState,
readyState,
bufferedLength: buffered?.length || 0,
bufferedRanges: buffered?.length ? Array.from({ length: buffered.length }, (_, i) => ({
start: buffered.start(i),
end: buffered.end(i)
})) : []
});
};
// 定期检查网络状态(用于调试)
const networkCheckInterval = setInterval(() => {
if (videoPlayer.value && loading.value) {
checkNetworkState();
} else {
clearInterval(networkCheckInterval);
}
}, 1000);
// 5秒后停止检查
setTimeout(() => {
clearInterval(networkCheckInterval);
}, 5000);
}
} catch (err: any) {
console.error('[VideoPlayer] 设置视频流 URL 失败:', err);
loading.value = false;
// 根据错误类型设置错误消息
if (err.message) {
error.value = err.message;
} else {
error.value = '视频加载失败,请稍后重试';
}
// 清空 URL
videoDirectUrl.value = '';
}
};
// 打开播放器
const open = async (id?: string, info?: VideoInfo) => {
console.log('VideoPlayer.open called', { id, info });
if (!id && !info?.id) {
message.error('视频ID不能为空');
return;
}
const videoId = id || info!.id;
const videoInfo = info || {
id: videoId,
videoTitle: '视频预览',
author: '未知',
viedoCate: '未分类',
resolution: '未知',
fileSize: 0,
};
console.log('设置视频信息', { videoId, videoInfo });
// 先清空之前的状态
if (videoPlayer.value) {
videoPlayer.value.pause();
videoPlayer.value.src = '';
}
videoDirectUrl.value = '';
error.value = '';
loading.value = true;
// 设置视频信息
currentVideoId.value = videoId;
currentVideoInfo.value = videoInfo;
// 先显示模态框
visible.value = true;
// 等待 DOM 渲染完成后再加载视频流
await new Promise(resolve => setTimeout(resolve, 100));
// 加载视频流
await loadVideoStream(videoId);
};
// 关闭播放器
const handleClose = () => {
visible.value = false;
if (videoPlayer.value) {
videoPlayer.value.pause();
videoPlayer.value.src = '';
}
videoDirectUrl.value = '';
loading.value = false;
error.value = '';
currentVideoId.value = '';
currentVideoInfo.value = null;
emit('close');
};
// 视频加载开始
const handleLoadStart = () => {
loading.value = true;
error.value = '';
console.log('[VideoPlayer] loadstart 事件触发');
};
// 视频元数据加载完成
const handleLoadedMetadata = () => {
const video = videoPlayer.value;
if (video) {
console.log('[VideoPlayer] loadedmetadata 事件触发', {
duration: video.duration,
videoWidth: video.videoWidth,
videoHeight: video.videoHeight,
readyState: video.readyState,
networkState: video.networkState
});
}
// 不在这里关闭 loading,等待 canplay 事件
};
// 视频可以开始播放(有足够的数据)
const handleCanPlay = () => {
const video = videoPlayer.value;
loading.value = false;
if (video) {
console.log('[VideoPlayer] canplay 事件触发 - 视频可以开始播放', {
duration: video.duration,
readyState: video.readyState,
networkState: video.networkState,
bufferedLength: video.buffered?.length || 0
});
}
// 如果视频还没有播放,可以尝试自动播放(需要用户交互)
if (video && video.paused) {
// 注意:自动播放可能被浏览器阻止,需要用户交互
// 这里不自动播放,让用户点击播放按钮
}
};
// 视频可以流畅播放(有足够的数据缓冲)
const handleCanPlayThrough = () => {
const video = videoPlayer.value;
loading.value = false;
if (video) {
console.log('[VideoPlayer] canplaythrough 事件触发 - 视频可以流畅播放', {
duration: video.duration,
readyState: video.readyState,
networkState: video.networkState,
bufferedLength: video.buffered?.length || 0
});
}
};
// 视频加载进度
const handleProgress = () => {
const video = videoPlayer.value;
if (video && video.buffered.length > 0) {
const bufferedEnd = video.buffered.end(video.buffered.length - 1);
const bufferedPercent = video.duration > 0 ? (bufferedEnd / video.duration) * 100 : 0;
console.log('[VideoPlayer] progress 事件 - 缓冲进度:', {
bufferedPercent: bufferedPercent.toFixed(2) + '%',
bufferedEnd,
duration: video.duration,
readyState: video.readyState
});
// 如果已经有足够的数据可以播放,关闭加载状态
if (bufferedEnd > 0 && video.readyState >= 2) {
loading.value = false;
}
}
};
// 视频等待数据
const handleWaiting = () => {
console.log('[VideoPlayer] waiting 事件 - 视频等待数据');
loading.value = true;
};
// 视频开始播放
const handlePlaying = () => {
console.log('[VideoPlayer] playing 事件 - 视频开始播放');
loading.value = false;
};
// 视频加载错误
const handleVideoError = (e: Event) => {
const video = e.target as HTMLVideoElement;
// 如果 src 为空或正在加载中,忽略错误(可能是时序问题)
if (!video.src || !videoDirectUrl.value || video.networkState === 0) {
console.log('忽略视频加载错误(可能是时序问题):', {
src: video.src,
directUrl: videoDirectUrl.value,
networkState: video.networkState,
});
return;
}
console.error('视频加载错误详情:', {
error: video.error,
errorCode: video.error?.code,
errorMessage: video.error?.message,
networkState: video.networkState,
readyState: video.readyState,
src: video.src,
directUrl: videoDirectUrl.value,
});
loading.value = false;
if (video.error) {
switch (video.error.code) {
case 1: // MEDIA_ERR_ABORTED
error.value = '视频加载被中止';
break;
case 2: // MEDIA_ERR_NETWORK
// 网络错误可能是认证失败(401)或其他网络问题
// 检查是否是认证问题
if (video.networkState === 3) { // NETWORK_NO_SOURCE
// 可能是认证失败,尝试提示用户
error.value = '视频加载失败,可能是认证问题。请检查是否已登录,或刷新页面后重试';
} else {
error.value = '网络错误,请检查网络连接';
}
break;
case 3: // MEDIA_ERR_DECODE
error.value = '视频解码失败,可能是视频格式问题或文件损坏';
break;
case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
// 尝试重新加载视频
if (videoDirectUrl.value && video.src === videoDirectUrl.value) {
console.log('尝试重新加载视频...');
setTimeout(() => {
if (videoPlayer.value && videoDirectUrl.value) {
videoPlayer.value.load();
}
}, 500);
return; // 不显示错误,等待重试
}
error.value = '视频格式不支持。请检查:1) 视频文件是否完整 2) 浏览器是否支持该格式 3) 文件扩展名是否正确';
break;
default:
error.value = `视频加载失败 (错误代码: ${video.error.code})`;
}
} else {
error.value = '视频加载失败,请检查网络连接和视频文件';
}
};
// 监听视频加载完成
watch(
() => videoPlayer.value?.readyState,
(readyState) => {
if (readyState && readyState >= 2) {
// HAVE_CURRENT_DATA 或更高
loading.value = false;
}
}
);
// 组件卸载时清理
onUnmounted(() => {
if (videoPlayer.value) {
videoPlayer.value.pause();
videoPlayer.value.src = '';
}
videoDirectUrl.value = '';
});
// 格式化文件大小
const formatFileSize = (bytes: number): string => {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
};
// 暴露方法
defineExpose({
open,
close: handleClose,
});
</script>
<style scoped lang="less">
.video-player-modal {
:deep(.ant-modal-body) {
padding: 20px;
}
// 确保标题区域不遮挡关闭按钮,支持多行显示
:deep(.ant-modal-header) {
position: relative;
padding-right: 56px !important; // 为关闭按钮留出足够空间
min-height: 55px;
padding-bottom: 16px; // 增加底部内边距,为多行标题留空间
.ant-modal-title {
padding: 0 !important;
margin: 0 !important;
}
}
// 自定义标题样式
.modal-title-wrapper {
width: 100%;
max-width: calc(100% - 56px); // 减去关闭按钮的宽度
padding-right: 8px;
box-sizing: border-box;
}
.modal-title-text {
display: block;
overflow: visible;
word-wrap: break-word;
word-break: break-all;
white-space: normal;
line-height: 1.5;
max-height: 80px;
overflow-y: auto;
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
font-size: 16px;
}
// 确保关闭按钮始终可见且在最上层
:deep(.ant-modal-close) {
position: absolute !important;
right: 0 !important;
top: 0 !important;
z-index: 1000 !important;
width: 56px !important;
height: 55px !important;
line-height: 55px !important;
text-align: center !important;
cursor: pointer;
border: none;
background: transparent;
margin: 0 !important;
padding: 0 !important;
&:hover {
background-color: rgba(0, 0, 0, 0.06);
}
.ant-modal-close-x {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.video-player-container {
position: relative;
}
.video-info {
margin-bottom: 16px;
padding: 12px;
background: #f5f5f5;
border-radius: 4px;
.info-row {
display: inline-block;
margin-right: 24px;
margin-bottom: 8px;
.label {
font-weight: 500;
color: #666;
}
.value {
color: #333;
}
}
}
.video-wrapper {
position: relative;
width: 100%;
background: #000;
border-radius: 4px;
overflow: hidden;
.video-element {
width: 100%;
height: auto;
max-height: 70vh;
display: block;
}
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
z-index: 10;
}
.error-message {
margin-top: 16px;
}
</style>
+115 -112
View File
@@ -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>
+89 -1
View File
@@ -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,留空则不限制&#10;例如:&#10;作者1&#10;作者2&#10;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,留空则不限制&#10;例如:&#10;作者1&#10;作者2&#10;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);
}
+369 -49
View File
@@ -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>
File diff suppressed because it is too large Load Diff
+41 -1
View File
@@ -138,6 +138,34 @@ export const useApiStore = defineStore('coreapi', () => {
});
}
// 清理数据库中文件不存在的视频记录
async function VideoCleanup() {
return http.request<any, Response<any>>('/api/video/cleanup', 'post_json').then(r => {
return r.data;
}).finally(() => {
});
}
// 获取所有视频类型列表
async function VideoCategories() {
return http.request<any, Response<any>>('/api/video/categories', 'get').then(r => {
console.log('VideoCategories 原始响应:', r);
// 响应拦截器已经处理了 code === 0 的情况,直接返回 data
// 如果 r 本身就是 Response 对象,返回 r.data
// 如果 r 已经是 data(拦截器处理后的结果),直接返回
if (r && typeof r === 'object' && 'code' in r && 'data' in r) {
return r.data;
}
return r;
}).catch(err => {
console.error('VideoCategories API 错误:', err);
throw err;
}).finally(() => {
});
}
//cookies
async function CookiePageList(param: object) {
return http.request<any, Response<any>>('/api/config/paged', 'post_json', param).then(r => {
@@ -163,6 +191,15 @@ export const useApiStore = defineStore('coreapi', () => {
});
}
// 删除指定作者的所有视频数据
async function DeleteAuthorVideos(authorName: string) {
return http.request<any, Response<any>>('/api/video/delete-author', 'post_json', { authorName }).then(r => {
return r.data;
}).finally(() => {
});
}
return {
deleteCookie,
UpdateConfig,
@@ -176,6 +213,9 @@ export const useApiStore = defineStore('coreapi', () => {
StartJobNow,
VideoStatics,
VideoPageList,
CookiePageList
VideoCategories,
VideoCleanup,
CookiePageList,
DeleteAuthorVideos
};
});
+9 -2
View File
@@ -41,13 +41,20 @@ http.interceptors.response.use(
(error) => {
// console.log(error)
// debugger
if (error.response.status === 401) {
useAccountStore().setLogged(false)
if (error.response && error.response.status === 401) {
useAccountStore().setLogged(false);
return Promise.reject({
message: '未授权,请重新登录',
code: 401,
error: 'Unauthorized',
data: error.response.data,
});
} else {
if (error.response && isAxiosResponse(error.response)) {
return Promise.reject({
message: error.response.statusText,
code: error.response.status,
error: error.response.data?.error || error.response.statusText,
data: error.response.data,
});
}
+29 -13
View File
@@ -83,20 +83,18 @@ function toUrlencoded(params?: Record<string | number, any>) {
function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
const _axios = axios.create(config);
// 添加响应拦截器处理401状态
_axios.interceptors.response.use(
// 成功响应直接返回
response => response,
// 错误响应处理
error => {
// 检查是否是401未授权错误
if (error.response && error.response.status === 401) {
// 调用removeAuthorization方法清除token
http.removeAuthorization();
// 这里可以添加额外的处理,比如跳转到登录页
// 示例: window.location.href = '/login';
// 添加请求拦截器,自动添加Authorization header
_axios.interceptors.request.use(
(config) => {
// 从Cookie中读取Authorization token
const token = Cookie.get(config.xsrfCookieName || 'Authorization');
if (token && config.headers) {
// 将token添加到请求头
config.headers[config.xsrfHeaderName || 'Authorization'] = token;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
@@ -155,6 +153,24 @@ function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
},
};
// 添加响应拦截器处理401状态(在http对象创建后)
_axios.interceptors.response.use(
// 成功响应直接返回
response => response,
// 错误响应处理
error => {
// 检查是否是401未授权错误
if (error.response && error.response.status === 401) {
// 清除token
http.removeAuthorization();
// 这里可以添加额外的处理,比如跳转到登录页
// 示例: window.location.href = '/login';
}
return Promise.reject(error);
}
);
return http;
}
+2 -2
View File
@@ -34,10 +34,10 @@ export default ({ command, mode }) => {
open: false, //自动打开
proxy: {
'/api': {
target: env.VITE_API_URL,
target: env.VITE_API_URL || 'http://localhost:10101',
ws: true,
changeOrigin: true,
rewrite: (path) => path.replace(/^\//, ''),
// 不重写路径,保持 /api 前缀
},
},
hmr: true,