1、去重

2、分享
3、视频播放
4、批量删除,重新下载
5、自定义标题(仅博主视频)
6、授权页面去掉博主添加,新增关注列表
7、图文视频合成优化
8、其他优化
This commit is contained in:
jianzhichu
2025-11-30 00:42:25 +08:00
parent 63b51d211e
commit e6e9ff3cc3
94 changed files with 8155 additions and 1544 deletions
+205 -83
View File
@@ -6,26 +6,63 @@
<h3 class="section-title">任务调度</h3>
<a-form-item has-feedback label="同步周期(分钟)" name="Cron" :wrapper-col="{ span: 6}" style="margin-left:30px">
<!-- <a-input v-model:value="formState.Cron" placeholder="请输入数字" /> -->
<a-input-number v-model:value="formState.Cron" placeholder="请输入数字" :min="1" />
<a-input-number v-model:value="formState.Cron" placeholder="请输入数字" :min="15" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>同步任务的执行间隔最小15分钟建议使用默认值</span>
</div>
</a-form-item>
<a-form-item has-feedback label="每次同步(条数)" name="BatchCount" :wrapper-col="{ span:6}" style="margin-left:30px">
<a-input-number v-model:value="formState.BatchCount" placeholder="请输入查询条数(最大30)" :min="10" :max="30" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>每次同步获取的条数范围10-30建议使用默认值</span>
</div>
</a-form-item>
</div>
<!-- 文件保存配置 -->
<div class="form-section">
<h3 class="section-title">博主视频</h3>
<h3 class="section-title">博主视频仅关注有效</h3>
<a-form-item has-feedback label="标题当文件名" name="UperUseViedoTitle" :wrapper-col="{ span: 6 }">
<a-form-item has-feedback label="标题当文件名" name="UperUseViedoTitle" :wrapper-col="{ span: 20 }">
<a-switch v-model:checked="formState.UperUseViedoTitle" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>启用后关注的视频文件名将直接使用原视频标题否则使用模板生成,如果既没有模板也没有开启则系统默认使用视频Id作为文件名</span>
</div>
</a-form-item>
<a-form-item has-feedback label="不创建子目录" name="UperSaveTogether" :wrapper-col="{ span: 6 }">
<a-form-item has-feedback label="不创建子目录" name="UperSaveTogether" :wrapper-col="{ span: 20 }">
<a-switch v-model:checked="formState.UperSaveTogether" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>默认是按博主名字创建文件夹存储启用后关注的视频直接存放在根目录</span>
</div>
</a-form-item>
<!-- 模板相关配置只有关闭"标题当文件名"时才显示 -->
<a-form-item has-feedback label="定义标题模板" name="FollowedTitleTemplate" :wrapper-col="{ span: 12 }" v-if="!formState.UperUseViedoTitle">
<a-select v-model:value="formState.FollowedTitleTemplate" :options="template_options" mode="multiple" size="middle" placeholder="请选择"></a-select>
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>选择生成文件名的占位符顺序即为文件名顺序需配合分隔符使用</span>
</div>
</a-form-item>
<a-form-item has-feedback label="模板分隔符" name="FollowedTitleSeparator" :wrapper-col="{ span: 12 }" v-if="!formState.UperUseViedoTitle">
<a-input v-model:value="formState.FollowedTitleSeparator" placeholder="请输入分隔符(如 - _ 或空)" style="width: 200px" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>占位符之间的连接符-_为空则直接拼接</span>
</div>
</a-form-item>
<!-- 完整模板预览只有关闭"标题当文件名"时才显示 -->
<a-form-item label="完整模板预览" :wrapper-col="{ span: 12 }" v-if="!formState.UperUseViedoTitle">
<a-input v-model:value="formState.FullFollowedTitleTemplate" placeholder="最终的模板" disabled style="background: #f5f5f5" />
</a-form-item>
</div>
<div class="form-section" v-if="downImgVideo">
@@ -33,24 +70,62 @@
<a-form-item has-feedback label="图文视频合成" name="DownImageVideo" :wrapper-col="{ span: 6 }">
<a-switch v-model:checked="formState.DownImageVideo" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>启用后图文视频将合成为视频文件下载</span>
</div>
</a-form-item>
<a-form-item has-feedback label="单独下载音频" name="DownMp3" :wrapper-col="{ span: 6 }">
<a-form-item has-feedback label="额外下载音频" name="DownMp3" :wrapper-col="{ span: 6 }">
<a-switch v-model:checked="formState.DownMp3" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>启用后将额外下载音频文件</span>
</div>
</a-form-item>
<a-form-item has-feedback label="单独下载图片" name="DownImage" :wrapper-col="{ span: 6 }">
<a-form-item has-feedback label="额外下载图片" name="DownImage" :wrapper-col="{ span: 6 }">
<a-switch v-model:checked="formState.DownImage" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>启用后将额外下载所有图片文件</span>
</div>
</a-form-item>
<a-form-item has-feedback label="是否统一存储" name="ImageViedoSaveAlone" :wrapper-col="{ span: 10 }">
<a-switch v-model:checked="formState.ImageViedoSaveAlone" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>
启用后所有图文视频统一存储到 Cookie(抖音授权) 设置的目录<br />否则按类型存储到对应文件夹比如视频属于收藏的视频则存储到收藏视频的目录
</span>
</div>
</a-form-item>
</div>
<!-- 系统配置 -->
<div class="form-section">
<h3 class="section-title">其他配置</h3>
<a-form-item has-feedback label="日志保留(天数)" name="LogKeepDay" :wrapper-col="{ span: 6 }" style="margin-left:30px">
<a-input-number v-model:value="formState.LogKeepDay" placeholder="请输入保留天数" :min="1" :max="90" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>系统运行日志的保留天数范围1-90过期自动清理</span>
</div>
</a-form-item>
<a-form-item has-feedback label="是否统一存储" name="AutoDistinct" :wrapper-col="{ span: 10 }">
<a-switch v-model:checked="formState.AutoDistinct" />
<div class="flex items-start mt-1 text-sm text-gray-500">
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
<span>
启用后同一个视频,只会下载一次
</span>
</div>
</a-form-item>
</div>
<!-- 操作按钮 -->
<a-form-item :wrapper-col="{ span: 12, offset: 5 }" class="form-actions">
<a-space size="middle">
@@ -64,22 +139,34 @@
</template>
<script lang="ts" setup>
import { reactive, toRaw, ref, watch, onMounted } from 'vue';
import { reactive, toRaw, ref, watch, onMounted, computed } from 'vue';
import type { UnwrapRef } from 'vue';
import { Form } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import type { FormInstance } from 'ant-design-vue';
import { useApiStore } from '@/store';
import { message } from 'ant-design-vue';
import { InfoCircleOutlined } from '@ant-design/icons-vue';
// 表单引用
const formRef = ref<FormInstance>();
// 模板字段:结构化数组
const template_options = [
{ label: '{Id}', value: '{Id}' },
{ label: '{VideoTitle}', value: '{VideoTitle}' },
{ label: '{FileHash}', value: '{FileHash}' },
// { label: '{FileSize}', value: '{FileSize}' },
{ label: '{Resolution}', value: '{Resolution}' },
// { label: '{SyncTime}', value: '{SyncTime}' },
{ label: '{ReleaseTime}', value: '{ReleaseTime}' },
];
// 状态定义
const componentDisabled = ref(true);
const downImgVideo = ref(false);
// 表单数据结构
// 表单数据结构(新增 FullFollowedTitleTemplate 字段)
interface FormState {
Cron: number;
Id: string;
@@ -91,6 +178,11 @@ interface FormState {
LogKeepDay: number;
DownImage: boolean;
DownMp3: boolean;
ImageViedoSaveAlone: boolean;
FollowedTitleTemplate: string[]; // 占位符数组
FollowedTitleSeparator: string; // 分隔符
FullFollowedTitleTemplate: string; // 新增:完整模板字符串(自动生成)
AutoDistinct: boolean;
}
// 表单初始数据
@@ -105,44 +197,70 @@ const formState: UnwrapRef<FormState> = reactive({
DownImageVideoFromEnv: false,
DownMp3: false,
DownImage: false,
ImageViedoSaveAlone: true,
FollowedTitleTemplate: [],
FollowedTitleSeparator: '',
FullFollowedTitleTemplate: '', // 初始为空
AutoDistinct: false,
});
// 表单校验规则
// 实时计算完整模板(可选:让用户实时预览,提交时无需重复计算)
const computeFullTemplate = computed(() => {
return formState.FollowedTitleTemplate.join(formState.FollowedTitleSeparator);
});
// 监听占位符/分隔符变化,实时更新预览(可选)
watch(
[() => [...formState.FollowedTitleTemplate], () => formState.FollowedTitleSeparator],
() => {
if (!formState.UperUseViedoTitle) {
// 只有关闭标题当文件名时才更新预览
formState.FullFollowedTitleTemplate = computeFullTemplate.value;
}
},
{ immediate: true, deep: true }
);
// 监听"标题当文件名"开关变化
watch(
() => formState.UperUseViedoTitle,
(isEnabled) => {
if (isEnabled) {
// 开启时清空模板相关数据,避免数据残留
formState.FollowedTitleTemplate = [];
formState.FollowedTitleSeparator = '';
formState.FullFollowedTitleTemplate = '';
// 清空相关表单项的校验状态
formRef.value?.clearValidate(['FollowedTitleTemplate', 'FollowedTitleSeparator', 'FullFollowedTitleTemplate']);
} else {
// 关闭时重新计算完整模板
formState.FullFollowedTitleTemplate = computeFullTemplate.value;
}
},
{ immediate: true }
);
// 表单校验规则(新增 FullFollowedTitleTemplate 校验)
const rules: Record<string, Rule[]> = {
// Cron: [
// { required: true, message: '请输入任务调度周期', trigger: 'change' },
// {
// validator: (rule, value) => {
// // 验证数字或cron表达式
// if (/^\d+$/.test(value) || /^(\d+\s*){5,6}$/.test(value)) {
// return Promise.resolve();
// }
// return Promise.reject(new Error('请输入有效的数字或Cron表达式'));
// },
// trigger: 'change',
// },
// ],
// BatchCount: [
// { required: true, message: '请输入每次查询条数', trigger: 'change' },
// { type: 'number', min: 1, max: 100, message: '查询条数应在1-100之间', trigger: 'change' },
// ],
// LogKeepDay: [
// { required: true, message: '请输入日志保留天数', trigger: 'change' },
// { type: 'number', min: 1, max: 90, message: '保留天数应在1-90之间', trigger: 'change' },
// ],
FollowedTitleSeparator: [{ max: 5, message: '分隔符长度不能超过5个字符', trigger: 'change' }],
FullFollowedTitleTemplate: [{ max: 200, message: '完整模板字符串长度不能超过200个字符', trigger: 'change' }],
};
// 布局配置
const labelCol = { style: { width: '150px', textAlign: 'right' } };
const wrapperCol = { span: 12 };
// 获取配置数据
// 获取配置数据(适配 FullFollowedTitleTemplate 字段)
const getConfig = () => {
useApiStore()
.apiGetConfig()
.then((res) => {
if (res.code === 0) {
// 使用Object.assign避免重复赋值
// 优先从 FullFollowedTitleTemplate 解析占位符数组(确保数据一致)
const fullTemplate = res.data.FullFollowedTitleTemplate || res.data.followedTitleTemplate || '';
const parsedTemplateArr = parseTemplateToArr(fullTemplate);
// 赋值所有字段(包含新增的 FullFollowedTitleTemplate
Object.assign(formState, {
Cron: res.data.cron,
Id: res.data.id,
@@ -154,9 +272,13 @@ const getConfig = () => {
LogKeepDay: res.data.logKeepDay,
DownImage: res.data.downImage,
DownMp3: res.data.downMp3,
FollowedTitleTemplate: parsedTemplateArr,
FollowedTitleSeparator: res.data.followedTitleSeparator || '',
FullFollowedTitleTemplate: fullTemplate, // 回显完整模板
ImageViedoSaveAlone: res.data.imageViedoSaveAlone,
AutoDistinct: res.data.autoDistinct,
});
// 监听环境变量配置,控制是否显示图片视频下载选项
downImgVideo.value = res.data.downImageVideoFromEnv;
} else {
message.error(res.erro || '获取配置失败', 8);
@@ -168,26 +290,46 @@ const getConfig = () => {
});
};
// 模板字符串 → 占位符数组(原有逻辑不变)
const parseTemplateToArr = (templateStr: string | null | undefined) => {
if (!templateStr || typeof templateStr !== 'string' || templateStr.trim() === '') {
return [];
}
const placeholderReg = /\{[a-zA-Z0-9]+\}/g;
const matchedPlaceholders = templateStr.match(placeholderReg) || [];
const validPlaceholderValues = template_options.map((item) => item.value);
const validPlaceholders = matchedPlaceholders.filter((placeholder) => validPlaceholderValues.includes(placeholder));
return Array.from(new Set(validPlaceholders));
};
// 组件挂载时获取配置
onMounted(() => {
getConfig();
});
// // 监听环境变量变化
// watch(
// () => formState.DownImageVideoFromEnv,
// (value) => {
// downImgVideo.value = value;
// }
// );
// 提交表单
// 提交表单(核心:自动生成完整模板字符串)
const onSubmit = () => {
formRef.value
.validate()
.then(() => {
// 1. 如果开启了标题当文件名,清空模板相关字段
let fullTemplate = '';
let templateTitle = '';
if (!formState.UperUseViedoTitle) {
// 自动拼接完整模板字符串(数组 + 分隔符)
fullTemplate = formState.FollowedTitleTemplate.join(formState.FollowedTitleSeparator);
}
// 2. 构造提交数据(包含三个模板相关字段)
const submitData = {
...toRaw(formState),
FullFollowedTitleTemplate: fullTemplate, // 确保提交最新拼接结果
FollowedTitleTemplate: formState.FollowedTitleTemplate.join(''),
};
// 3. 提交接口
useApiStore()
.apiUpdateConfig(toRaw(formState))
.apiUpdateConfig(submitData)
.then((res) => {
if (res.code === 0) {
message.success('修改成功,同步任务将在5-10秒按新配置运行...');
@@ -215,8 +357,11 @@ const onUpdate = () => {
// 取消编辑
const onCancel = () => {
componentDisabled.value = true;
// 可以考虑添加重置表单的逻辑
formRef.value?.clearValidate();
// 取消时恢复完整模板预览(只有关闭标题当文件名时)
if (!formState.UperUseViedoTitle) {
formState.FullFollowedTitleTemplate = computeFullTemplate.value;
}
};
</script>
@@ -229,6 +374,9 @@ const onCancel = () => {
&:last-child {
border-bottom: none;
}
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.section-title {
@@ -240,44 +388,6 @@ const onCancel = () => {
border-left: 3px solid @primary-color;
}
// // 关键改动:强制标签靠左并添加100px左侧间距
// .ant-form-item-label {
// text-align: left !important; /* 强制靠左对齐,覆盖默认右对齐 */
// padding-right: 0 !important; /* 清除默认右侧内边距 */
// margin-left: 100px !important; /* 标签左侧间距100px */
// }
// 保持配置项内容区域与标签对齐(可选,根据实际布局调整)
// .ant-form-item-control {
// margin-left: 0 !important; /* 清除之前可能添加的左侧间距,避免双重缩进 */
// }
.help-text {
margin-top: 8px;
font-size: 12px;
color: #666;
line-height: 1.5;
p {
margin: 4px 0;
}
}
.cron-link {
color: @primary-color;
text-decoration: underline;
transition: all 0.3s;
&:hover {
color: #096dd9;
}
}
.form-actions {
margin-top: 30px;
text-align: center;
}
.ant-form-item {
margin-bottom: 18px;
}
@@ -286,7 +396,19 @@ const onCancel = () => {
color: rgb(164 158 158) !important;
background-color: #e6e6e6 !important;
}
.ant-form-item-label {
text-align: left !important;
}
// 统一提醒文字样式
:deep(.flex.items-start.mt-1.text-sm.text-gray-500) {
line-height: 1.6;
white-space: normal;
}
// 禁用输入框样式优化
:deep(.ant-input-disabled) {
color: #666 !important;
}
</style>