mirror of
https://github.com/Cp0204/quark-auto-save.git
synced 2026-01-12 15:20:44 +08:00
6438 lines
307 KiB
HTML
6438 lines
307 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>夸克自动转存</title>
|
||
<!-- CSS -->
|
||
<link rel="stylesheet" href="./static/css/bootstrap.min.css">
|
||
<link rel="stylesheet" href="./static/css/bootstrap-icons.min.css">
|
||
<link rel="stylesheet" href="./static/css/main.css">
|
||
<!-- Bootstrap JS -->
|
||
<script src="./static/js/jquery-3.5.1.slim.min.js"></script>
|
||
<script src="./static/js/bootstrap.bundle.min.js"></script>
|
||
<!-- Vue.js -->
|
||
<script src="./static/js/vue@2.js"></script>
|
||
<script src="./static/js/axios.min.js"></script>
|
||
<script src="./static/js/v-jsoneditor.min.js"></script>
|
||
<script src="./static/js/sort_file_by_name.js"></script>
|
||
<script src="./static/js/pinyin-pro.min.js"></script>
|
||
<script>
|
||
// 添加检测文本溢出的自定义指令
|
||
Vue.directive('check-overflow', {
|
||
inserted: function(el, binding, vnode) {
|
||
// 检查元素是否溢出
|
||
const isOverflowing = el.scrollWidth > el.clientWidth;
|
||
|
||
// 如果绑定了值,则绑定到该值对应的记录属性上
|
||
if (binding.value) {
|
||
const indexAndField = binding.value.split('|');
|
||
const index = parseInt(indexAndField[0]);
|
||
const field = indexAndField[1];
|
||
|
||
// 设置记录的_isOverflowing属性
|
||
// 获取记录数组
|
||
const records = vnode.context.filteredHistoryRecords;
|
||
if (records && records[index]) {
|
||
// 初始化_isOverflowing属性(如果不存在)
|
||
if (!records[index]._isOverflowing) {
|
||
vnode.context.$set(records[index], '_isOverflowing', {});
|
||
}
|
||
// 设置对应字段的溢出状态
|
||
vnode.context.$set(records[index]._isOverflowing, field, isOverflowing);
|
||
}
|
||
}
|
||
},
|
||
// 添加update钩子,在DOM更新时重新检测溢出状态
|
||
update: function(el, binding, vnode) {
|
||
// 检查元素是否溢出
|
||
const isOverflowing = el.scrollWidth > el.clientWidth;
|
||
|
||
// 如果绑定了值,则绑定到该值对应的记录属性上
|
||
if (binding.value) {
|
||
const indexAndField = binding.value.split('|');
|
||
const index = parseInt(indexAndField[0]);
|
||
const field = indexAndField[1];
|
||
|
||
// 设置记录的_isOverflowing属性
|
||
const records = vnode.context.filteredHistoryRecords;
|
||
if (records && records[index]) {
|
||
// 初始化_isOverflowing属性(如果不存在)
|
||
if (!records[index]._isOverflowing) {
|
||
vnode.context.$set(records[index], '_isOverflowing', {});
|
||
}
|
||
// 设置对应字段的溢出状态
|
||
vnode.context.$set(records[index]._isOverflowing, field, isOverflowing);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 根据文件扩展名获取对应的Bootstrap图标类名
|
||
function getFileIconClass(fileName, isDir = false) {
|
||
// 如果是文件夹,返回文件夹图标
|
||
if (isDir) {
|
||
return 'bi-folder-fill';
|
||
}
|
||
|
||
// 获取文件扩展名(转为小写)
|
||
const ext = fileName.toLowerCase().split('.').pop();
|
||
|
||
// 视频文件
|
||
const videoExts = ['mp4', 'mkv', 'avi', 'mov', 'rmvb', 'flv', 'wmv', 'm4v', 'ts', 'webm', '3gp', 'f4v'];
|
||
if (videoExts.includes(ext)) {
|
||
return 'bi-file-earmark-play';
|
||
}
|
||
|
||
// 音频文件
|
||
const audioExts = ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a', 'wma', 'ape', 'ac3', 'dts'];
|
||
if (audioExts.includes(ext)) {
|
||
return 'bi-file-earmark-music';
|
||
}
|
||
|
||
// 图片文件
|
||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'svg', 'ico', 'raw'];
|
||
if (imageExts.includes(ext)) {
|
||
return 'bi-file-earmark-image';
|
||
}
|
||
|
||
// 文本文件(包括歌词文件和字幕文件)
|
||
const textExts = ['txt', 'md', 'rtf', 'log', 'ini', 'cfg', 'conf', 'lrc', 'srt', 'ass', 'ssa', 'vtt', 'sup'];
|
||
if (textExts.includes(ext)) {
|
||
return 'bi-file-earmark-text';
|
||
}
|
||
|
||
// 富文本文件
|
||
const richtextExts = ['rtf', 'odt'];
|
||
if (richtextExts.includes(ext)) {
|
||
return 'bi-file-earmark-richtext';
|
||
}
|
||
|
||
// 压缩文件
|
||
const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'lzma', 'cab', 'iso'];
|
||
if (archiveExts.includes(ext)) {
|
||
return 'bi-file-earmark-zip';
|
||
}
|
||
|
||
// 字体文件
|
||
const fontExts = ['ttf', 'otf', 'woff', 'woff2', 'eot'];
|
||
if (fontExts.includes(ext)) {
|
||
return 'bi-file-earmark-font';
|
||
}
|
||
|
||
// 代码文件
|
||
const codeExts = ['js', 'html', 'css', 'py', 'java', 'c', 'cpp', 'php', 'go', 'json', 'xml', 'yml', 'yaml', 'sql', 'sh', 'bat', 'ps1', 'rb', 'swift', 'kt', 'ts', 'jsx', 'tsx', 'vue', 'scss', 'sass', 'less'];
|
||
if (codeExts.includes(ext)) {
|
||
return 'bi-file-earmark-code';
|
||
}
|
||
|
||
// PDF文件
|
||
if (ext === 'pdf') {
|
||
return 'bi-file-earmark-pdf';
|
||
}
|
||
|
||
// Word文档
|
||
const wordExts = ['doc', 'docx'];
|
||
if (wordExts.includes(ext)) {
|
||
return 'bi-file-earmark-word';
|
||
}
|
||
|
||
// Excel文档
|
||
const excelExts = ['xls', 'xlsx', 'csv'];
|
||
if (excelExts.includes(ext)) {
|
||
return 'bi-file-earmark-excel';
|
||
}
|
||
|
||
// PowerPoint文档
|
||
const pptExts = ['ppt', 'pptx'];
|
||
if (pptExts.includes(ext)) {
|
||
return 'bi-file-earmark-ppt';
|
||
}
|
||
|
||
// 医疗/健康相关文件
|
||
const medicalExts = ['dcm', 'dicom', 'hl7'];
|
||
if (medicalExts.includes(ext)) {
|
||
return 'bi-file-earmark-medical';
|
||
}
|
||
|
||
// 默认文件图标
|
||
return 'bi-file-earmark';
|
||
}
|
||
|
||
// 添加检测文件整理页面文件名溢出的自定义指令
|
||
Vue.directive('check-file-overflow', {
|
||
inserted: function(el, binding, vnode) {
|
||
// 检查元素是否溢出
|
||
const isOverflowing = el.scrollWidth > el.clientWidth;
|
||
|
||
// 如果绑定了值,则绑定到该值对应的文件属性上
|
||
if (binding.value) {
|
||
const indexAndField = binding.value.split('|');
|
||
const index = parseInt(indexAndField[0]);
|
||
const field = indexAndField[1];
|
||
|
||
// 设置文件的_isOverflowing属性
|
||
const files = vnode.context.fileManager.fileList;
|
||
if (files && files[index]) {
|
||
// 初始化_isOverflowing属性(如果不存在)
|
||
if (!files[index]._isOverflowing) {
|
||
vnode.context.$set(files[index], '_isOverflowing', {});
|
||
}
|
||
// 设置对应字段的溢出状态
|
||
vnode.context.$set(files[index]._isOverflowing, field, isOverflowing);
|
||
}
|
||
}
|
||
},
|
||
// 添加update钩子,在DOM更新时重新检测溢出状态
|
||
update: function(el, binding, vnode) {
|
||
// 检查元素是否溢出
|
||
const isOverflowing = el.scrollWidth > el.clientWidth;
|
||
|
||
// 如果绑定了值,则绑定到该值对应的文件属性上
|
||
if (binding.value) {
|
||
const indexAndField = binding.value.split('|');
|
||
const index = parseInt(indexAndField[0]);
|
||
const field = indexAndField[1];
|
||
|
||
// 设置文件的_isOverflowing属性
|
||
const files = vnode.context.fileManager.fileList;
|
||
if (files && files[index]) {
|
||
// 初始化_isOverflowing属性(如果不存在)
|
||
if (!files[index]._isOverflowing) {
|
||
vnode.context.$set(files[index], '_isOverflowing', {});
|
||
}
|
||
// 设置对应字段的溢出状态
|
||
vnode.context.$set(files[index]._isOverflowing, field, isOverflowing);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 添加检测模态框表格文本溢出的自定义指令
|
||
Vue.directive('check-modal-overflow', {
|
||
inserted: function(el, binding, vnode) {
|
||
// 检查元素是否溢出
|
||
const isOverflowing = el.scrollWidth > el.clientWidth;
|
||
|
||
// 如果绑定了值,则绑定到该值对应的文件属性上
|
||
if (binding.value) {
|
||
const indexAndField = binding.value.split('|');
|
||
const index = parseInt(indexAndField[0]);
|
||
const field = indexAndField[1];
|
||
|
||
// 设置文件的_isOverflowing属性
|
||
const files = vnode.context.fileSelect.fileList;
|
||
if (files && files[index]) {
|
||
// 初始化_isOverflowing属性(如果不存在)
|
||
if (!files[index]._isOverflowing) {
|
||
vnode.context.$set(files[index], '_isOverflowing', {});
|
||
}
|
||
// 设置对应字段的溢出状态
|
||
vnode.context.$set(files[index]._isOverflowing, field, isOverflowing);
|
||
}
|
||
}
|
||
},
|
||
// 添加update钩子,在DOM更新时重新检测溢出状态
|
||
update: function(el, binding, vnode) {
|
||
// 检查元素是否溢出
|
||
const isOverflowing = el.scrollWidth > el.clientWidth;
|
||
|
||
// 如果绑定了值,则绑定到该值对应的文件属性上
|
||
if (binding.value) {
|
||
const indexAndField = binding.value.split('|');
|
||
const index = parseInt(indexAndField[0]);
|
||
const field = indexAndField[1];
|
||
|
||
// 设置文件的_isOverflowing属性
|
||
const files = vnode.context.fileSelect.fileList;
|
||
if (files && files[index]) {
|
||
// 初始化_isOverflowing属性(如果不存在)
|
||
if (!files[index]._isOverflowing) {
|
||
vnode.context.$set(files[index], '_isOverflowing', {});
|
||
}
|
||
// 设置对应字段的溢出状态
|
||
vnode.context.$set(files[index]._isOverflowing, field, isOverflowing);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 添加中文数字转阿拉伯数字的函数
|
||
function chineseToArabic(chinese) {
|
||
if (!chinese) {
|
||
return null;
|
||
}
|
||
|
||
// 数字映射
|
||
const digitMap = {
|
||
'零': 0, '一': 1, '二': 2, '三': 3, '四': 4,
|
||
'五': 5, '六': 6, '七': 7, '八': 8, '九': 9,
|
||
'两': 2
|
||
};
|
||
|
||
// 单位映射
|
||
const unitMap = {
|
||
'十': 10,
|
||
'百': 100,
|
||
'千': 1000,
|
||
'万': 10000
|
||
};
|
||
|
||
// 如果是单个字符,直接返回对应数字
|
||
if (chinese.length === 1) {
|
||
if (chinese === '十') return 10;
|
||
return digitMap[chinese];
|
||
}
|
||
|
||
let result = 0;
|
||
let section = 0;
|
||
let number = 0;
|
||
|
||
// 从左向右处理
|
||
for (let i = 0; i < chinese.length; i++) {
|
||
const char = chinese[i];
|
||
|
||
if (char in digitMap) {
|
||
number = digitMap[char];
|
||
} else if (char in unitMap) {
|
||
const unit = unitMap[char];
|
||
// 如果前面没有数字,默认为1,例如"十"表示1*10=10
|
||
section += (number || 1) * unit;
|
||
number = 0;
|
||
|
||
// 如果是万级单位,累加到结果并重置section
|
||
if (unit === 10000) {
|
||
result += section;
|
||
section = 0;
|
||
}
|
||
} else {
|
||
// 非法字符
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 加上最后的数字和小节
|
||
result += section + number;
|
||
|
||
return result;
|
||
}
|
||
</script>
|
||
</head>
|
||
|
||
<body>
|
||
<div id="app">
|
||
<!-- 添加通知组件 -->
|
||
<div class="toast-container toast-container-center">
|
||
<div class="toast toast-custom" ref="toast" data-delay="2000">
|
||
<div class="toast-body toast-body-custom">
|
||
{{ toastMessage }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
|
||
<a class="navbar-brand col-md-2 col-lg-2 mr-0 px-3" href="#" @click.prevent="window.innerWidth > 767.98 && toggleSidebar()" :class="{'sidebar-collapsed-navbar-brand': sidebarCollapsed}" :title="sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'">
|
||
<i class="bi bi-clouds"></i> <span class="navbar-title">夸克自动转存</span>
|
||
</a>
|
||
<!-- 顶部操作按钮组 -->
|
||
<div class="navbar-actions">
|
||
<button type="button" class="navbar-action-btn" title="保存(CTRL+S)" @click="saveConfig()">
|
||
<i class="bi bi-check2"></i>
|
||
</button>
|
||
<button type="button" class="navbar-action-btn" title="运行全部任务(CTRL+R)" @click="runScriptNow()">
|
||
<i class="bi bi-caret-right"></i>
|
||
</button>
|
||
<button type="button" class="navbar-action-btn" title="单击回到顶部/双击回到底部" @click="scrollToX(0)" @dblclick="scrollToX()">
|
||
<i class="bi bi-chevron-expand"></i>
|
||
</button>
|
||
<button type="button" class="navbar-action-btn d-none d-md-inline-block" :title="'页面宽度:' + (pageWidthMode === 'narrow' ? '窄' : pageWidthMode === 'medium' ? '中' : '宽')" @click="togglePageWidth()">
|
||
<i class="bi bi-arrows"></i>
|
||
</button>
|
||
</div>
|
||
<button class="navbar-toggler navbar-toggler-square position-absolute d-md-none collapsed" type="button" data-toggle="collapse" data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false">
|
||
<i class="bi bi-list"></i>
|
||
</button>
|
||
</nav>
|
||
|
||
<div class="container-fluid">
|
||
<nav id="sidebarMenu" class="col-md-2 col-lg-2 d-md-block bg-light sidebar collapse" :class="{'sidebar-collapsed': sidebarCollapsed}">
|
||
<div class="sidebar-sticky">
|
||
<ul class="nav flex-column">
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="#" :class="{active: activeTab === 'tasklist'}" @click="changeTab('tasklist')">
|
||
<i class="bi bi-list-ul"></i> <span class="nav-text">任务列表</span>
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="#" :class="{active: activeTab === 'history'}" @click="changeTab('history')">
|
||
<i class="bi bi-clock-history"></i> <span class="nav-text">转存记录</span>
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="#" :class="{active: activeTab === 'filemanager'}" @click="changeTab('filemanager')">
|
||
<i class="bi bi-archive"></i> <span class="nav-text">文件整理</span>
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="#" :class="{active: activeTab === 'config'}" @click="changeTab('config')">
|
||
<i class="bi bi-gear"></i> <span class="nav-text">系统配置</span>
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="/logout">
|
||
<i class="bi bi-power"></i> <span class="nav-text">退出</span>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
<div class="bottom-links">
|
||
<ul class="nav flex-column">
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="https://github.com/x1ao4/quark-auto-save-x/wiki" target="_blank">
|
||
<i class="bi bi-book"></i>
|
||
<span class="nav-text">Quark Auto Save X</span>
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="https://github.com/x1ao4/quark-auto-save-x/releases" target="_blank">
|
||
<i class="bi bi-github"></i>
|
||
<span class="nav-text"><span v-html="versionTips"></span></span>
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="https://github.com/x1ao4/quark-auto-save-x/wiki/赞赏与交流群" target="_blank">
|
||
<i class="bi bi-wechat"></i>
|
||
<span class="nav-text">WeChat Group</span>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main class="col-md-10 col-lg-10 ml-sm-auto">
|
||
<form @submit.prevent="saveConfig" @keydown.enter.prevent>
|
||
|
||
<div v-if="activeTab === 'config'">
|
||
<div style="height: 0.5px;"></div>
|
||
<div class="row title">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">账号设置</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a href="#" title="夸克自动转存WebUI账号设置"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="row mb-2">
|
||
<div class="col webui-username-col">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">用户名</span>
|
||
</div>
|
||
<input type="text" v-model="formData.webui.username" class="form-control" placeholder="登录用户名">
|
||
</div>
|
||
</div>
|
||
<div class="col webui-password-col">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">密码</span>
|
||
</div>
|
||
<input :type="showWebuiPassword ? 'text' : 'password'" v-model="formData.webui.password" class="form-control" placeholder="登录密码">
|
||
<div class="input-group-append">
|
||
<button type="button" class="btn btn-outline-secondary" @click="toggleWebuiPassword">
|
||
<i :class="['bi', showWebuiPassword ? 'bi-eye' : 'bi-eye-slash']"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row title">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">Cookie</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a href="https://github.com/x1ao4/quark-auto-save-x/wiki/使用技巧集锦" target="_blank" title="夸克网盘账号的Cookie,用于转存和签到,查阅Wiki了解详情"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<p style="margin-top: -4px; margin-bottom: 4px;">所有账号都会进行签到(纯签到只需填写移动端参数),只有第一个账号会进行转存,请自行确认账号顺序;所有填写了 Cookie 的账号均支持文件整理,如需签到请在 Cookie 后方添加签到参数。</p>
|
||
<div v-for="(value, index) in formData.cookie" :key="index" class="input-group mb-2">
|
||
<div class="input-group-prepend" v-if="userInfoList[index]">
|
||
<span class="input-group-text" :style="userInfoList[index].nickname ? (userInfoList[index].is_active ? 'color: var(--dark-text-color);' : 'color: red;') : 'color: var(--dark-text-color);'">
|
||
{{ userInfoList[index].nickname || (userInfoList[index].has_mparam ? '仅签到' : '未登录') }}
|
||
</span>
|
||
</div>
|
||
<div class="input-group-prepend" v-else>
|
||
<span class="input-group-text" style="color: var(--dark-text-color);">未验证</span>
|
||
</div>
|
||
<input type="text" v-model="formData.cookie[index]" class="form-control" placeholder="打开 pan.quark.com 按 F12 抓取" @change="fetchUserInfo">
|
||
<div class="input-group-append">
|
||
<button v-if="index === formData.cookie.length - 1" type="button" class="btn btn-outline-primary" @click="addCookie()"><i class="bi bi-plus-lg"></i></button>
|
||
<button type="button" class="btn btn-outline-danger" @click="removeCookie(index)"><i class="bi bi-dash-lg"></i></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row title">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">定时规则</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a target="_blank" href="https://tool.lu/crontab/" title="Crontab执行时间计算器"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="row mb-2">
|
||
<div class="col-sm-6 pr-1">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">Crontab</span>
|
||
</div>
|
||
<input type="text" v-model="formData.crontab" class="form-control" placeholder="必填">
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-6 pl-1">
|
||
<div class="input-group" title="添加随机延迟时间:定时任务将在0到设定秒数之间随机延迟执行。建议值:0–3600,0表示不延迟">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">延迟执行</span>
|
||
</div>
|
||
<input type="text" v-model="formData.crontab_delay" class="form-control no-spinner" placeholder="0-3600" @input="validateNumberInput($event, 'crontab_delay', 3600)">
|
||
<div class="input-group-append">
|
||
<span class="input-group-text square-append">秒</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row title" title="通知推送,支持多个渠道,查阅Wiki了解详情">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">通知</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a href="https://github.com/x1ao4/quark-auto-save-x/wiki/通知推送服务配置" target="_blank"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div v-for="(value, key, index) in formData.push_config" :key="key" class="input-group mb-2">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text" v-html="key"></span>
|
||
</div>
|
||
<div class="input-group-prepend" v-if="(key == 'DEER_KEY' || key == 'PUSH_KEY')">
|
||
<a type="button" class="btn btn-warning" target="_blank" href="https://sct.ftqq.com/r/13249" title="Server酱推荐计划"><i class="bi bi-award"></i></a>
|
||
</div>
|
||
<input type="text" v-model="formData.push_config[key]" class="form-control">
|
||
<div class="input-group-append">
|
||
<button v-if="index === Object.keys(formData.push_config).length - 1" type="button" class="btn btn-outline-primary" @click="addPush()"><i class="bi bi-plus-lg"></i></button>
|
||
<button type="button" class="btn btn-outline-danger" @click="removePush(key)"><i class="bi bi-dash-lg"></i></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row title" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length" title="插件配置,具体键值由插件定义,查阅Wiki了解详情">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">插件</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a href="https://github.com/x1ao4/quark-auto-save-x/wiki/插件配置" target="_blank"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div v-for="(plugin, pluginName) in getAvailablePlugins(formData.plugins)" :key="pluginName" style="margin-bottom: -8px;" :style="pluginName === Object.keys(getAvailablePlugins(formData.plugins))[0] ? 'margin-top: -8px;' : ''">
|
||
<div class="form-group row mb-0" style="display:flex; align-items:center;">
|
||
<div data-toggle="collapse" :data-target="'#collapse_'+pluginName" aria-expanded="true" :aria-controls="'collapse_'+pluginName">
|
||
<div class="btn btn-block text-left">
|
||
<i class="bi bi-caret-right-fill"></i> <span v-html="`${pluginName}`"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="collapse" :id="'collapse_'+pluginName" style="margin-left: 26px;">
|
||
<div v-for="(value, key, keyIndex) in plugin" :key="key" class="input-group mb-2" :style="{ marginBottom: keyIndex === Object.keys(plugin).length - 1 && pluginName === Object.keys(getAvailablePlugins(formData.plugins)).pop() ? '8.5px !important' : '' }">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text" v-html="key" :title="getPluginConfigHelp(pluginName, key)"></span>
|
||
</div>
|
||
<input type="text" v-model="formData.plugins[pluginName][key]" class="form-control"
|
||
:placeholder="getPluginConfigPlaceholder(pluginName, key)"
|
||
:title="getPluginConfigHelp(pluginName, key)">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row title" title="预定义的正则匹配规则,在任务列表中可直接点击使用,查阅Wiki了解详情">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">魔法匹配</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a href="https://github.com/x1ao4/quark-auto-save-x/wiki/正则处理教程#21-魔法匹配" target="_blank"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div v-for="(value, key, index) in formData.magic_regex" :key="key" class="form-group mb-2">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">魔法名</span>
|
||
</div>
|
||
<input type="text" :data-oldkey="key" v-model="key" class="form-control" @change="updateMagicRegexKey($event.target.dataset.oldkey, $event.target.value)" placeholder="自定义名称">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">正则命名</span>
|
||
</div>
|
||
<input type="text" v-model="value.pattern" class="form-control" placeholder="匹配表达式">
|
||
<input type="text" v-model="value.replace" class="form-control" placeholder="替换表达式">
|
||
<div class="input-group-append">
|
||
<button v-if="index === Object.keys(formData.magic_regex).length - 1" type="button" class="btn btn-outline-primary" @click="addMagicRegex()"><i class="bi bi-plus-lg"></i></button>
|
||
<button type="button" class="btn btn-outline-danger" @click="removeMagicRegex(key)"><i class="bi bi-dash-lg"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 剧集识别模块 -->
|
||
<div class="row title" title="识别文件名中的剧集编号,用于自动重命名,查阅Wiki了解详情">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">剧集识别</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a href="https://github.com/x1ao4/quark-auto-save-x/wiki/正则处理教程#25-剧集命名" target="_blank"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">集编号识别规则</span>
|
||
</div>
|
||
<input type="text" class="form-control" v-model="episodePatternsText" placeholder="输入用于识别集编号的正则表达式,多个表达式用竖线分隔,特殊符号需要转义">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row title" title="API接口,用于第三方添加任务等操作,查阅Wiki了解详情">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">API</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a href="https://github.com/x1ao4/quark-auto-save-x/wiki/API接口" target="_blank"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">Token</span>
|
||
</div>
|
||
<input type="text" v-model="formData.api_token" class="form-control" style="background-color:white;" disabled>
|
||
</div>
|
||
|
||
<div class="row title" title="资源搜索服务配置,用于任务名称智能搜索,查阅Wiki了解详情">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">CloudSaver</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a href="https://github.com/x1ao4/quark-auto-save-x/wiki/CloudSaver搜索源" target="_blank"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="input-group mb-2">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">服务器</span>
|
||
</div>
|
||
<input type="text" v-model="formData.source.cloudsaver.server" class="form-control" placeholder="资源搜索服务器地址,如 http://172.17.0.1:8008">
|
||
</div>
|
||
<div class="row mb-2">
|
||
<div class="col cloudsaver-username-col">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">用户名</span>
|
||
</div>
|
||
<input type="text" v-model="formData.source.cloudsaver.username" class="form-control" placeholder="用户名">
|
||
</div>
|
||
</div>
|
||
<div class="col cloudsaver-password-col">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">密码</span>
|
||
</div>
|
||
<input :type="showCloudSaverPassword ? 'text' : 'password'" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="密码">
|
||
<div class="input-group-append">
|
||
<button type="button" class="btn btn-outline-secondary" @click="toggleCloudSaverPassword">
|
||
<i :class="['bi', showCloudSaverPassword ? 'bi-eye' : 'bi-eye-slash']"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row title" title="设置任务列表页面的任务按钮的显示方式,刷新Plex媒体库和刷新AList目录按钮仅在配置了对应插件的前提下才支持显示">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">显示设置</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a href="#"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="row mb-2 display-setting-row">
|
||
<div class="col-lg-3 col-md-6 mb-2">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">运行此任务</span>
|
||
</div>
|
||
<select class="form-control" v-model="formData.button_display.run_task">
|
||
<option value="always">始终显示</option>
|
||
<option value="hover">悬停显示</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-3 col-md-6 mb-2">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">删除此任务</span>
|
||
</div>
|
||
<select class="form-control" v-model="formData.button_display.delete_task">
|
||
<option value="always">始终显示</option>
|
||
<option value="hover">悬停显示</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-3 col-md-6 mb-2">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">刷新 Plex 媒体库</span>
|
||
</div>
|
||
<select class="form-control" v-model="formData.button_display.refresh_plex">
|
||
<option value="always">始终显示</option>
|
||
<option value="hover">悬停显示</option>
|
||
<option value="disabled">禁用</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-3 col-md-6 mb-2">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">刷新 AList 目录</span>
|
||
</div>
|
||
<select class="form-control" v-model="formData.button_display.refresh_alist">
|
||
<option value="always">始终显示</option>
|
||
<option value="hover">悬停显示</option>
|
||
<option value="disabled">禁用</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="row mb-2 display-setting-row">
|
||
<div class="col-lg-3 col-md-6 mb-2">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">最近转存文件</span>
|
||
</div>
|
||
<select class="form-control" v-model="formData.button_display.latest_transfer_file">
|
||
<option value="always">始终显示</option>
|
||
<option value="hover">悬停显示</option>
|
||
<option value="disabled">禁用</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-3 col-md-6 mb-2">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">最近更新日期</span>
|
||
</div>
|
||
<select class="form-control" v-model="formData.button_display.latest_update_date">
|
||
<option value="always">始终显示</option>
|
||
<option value="hover">悬停显示</option>
|
||
<option value="disabled">禁用</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-3 col-md-6 mb-2">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">当日更新标识</span>
|
||
</div>
|
||
<select class="form-control" v-model="formData.button_display.today_update_indicator">
|
||
<option value="always">始终显示</option>
|
||
<option value="hover">悬停显示</option>
|
||
<option value="disabled">禁用</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 性能设置 -->
|
||
<div class="row title" title="调整文件整理页面的请求参数和缓存时长,可提升大文件夹的加载速度和数据刷新效率。合理配置可减少API请求次数,同时保证数据及时更新">
|
||
<div class="col">
|
||
<h2 style="display: inline-block; font-size: 1.5rem;">性能设置</h2>
|
||
<span class="badge badge-pill badge-light">
|
||
<a href="#"><i class="bi bi-question-circle"></i></a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="row mb-2 performance-setting-row">
|
||
<div class="col-lg-6 col-md-6 mb-2">
|
||
<div class="input-group" title="每次请求夸克API时获取的文件数量,适当增大该数值可减少请求次数,提升大文件夹的加载效率。建议值:100–500">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">单次请求文件数量</span>
|
||
</div>
|
||
<input type="text" class="form-control no-spinner" v-model="formData.file_performance.api_page_size" placeholder="200">
|
||
<div class="input-group-append">
|
||
<span class="input-group-text square-append">个</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-6 col-md-6 mb-2">
|
||
<div class="input-group" title="文件列表在本地缓存的持续时间,过期后将自动清除。设置过短会增加API请求次数,设置过长可能无法及时反映最新变动。建议值:0-300,0表示不缓存">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">文件列表缓存时长</span>
|
||
</div>
|
||
<input type="text" class="form-control no-spinner" v-model="formData.file_performance.cache_expire_time" placeholder="30">
|
||
<div class="input-group-append">
|
||
<span class="input-group-text square-append">秒</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div v-if="activeTab === 'tasklist'">
|
||
<div style="height: 20px;"></div>
|
||
<div class="row" style="margin-bottom: 20px;">
|
||
<div class="col-lg-6 col-md-6 mb-2 mb-md-0">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">名称筛选</span>
|
||
</div>
|
||
<input type="text" class="form-control" v-model="taskNameFilter" placeholder="任务名称关键词">
|
||
<div class="input-group-append">
|
||
<button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('taskNameFilter')"><i class="bi bi-x-lg"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-6 col-md-6">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">任务筛选</span>
|
||
</div>
|
||
<div class="position-relative" style="flex: 1;">
|
||
<select class="form-control task-filter-select" v-model="taskDirSelected" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 24px !important;">
|
||
<option value="">全部任务</option>
|
||
<option v-for="task in taskNames" :value="task" v-html="task"></option>
|
||
</select>
|
||
<!-- <i class="bi bi-chevron-down select-arrow" style="position: absolute; pointer-events: none; color: var(--dark-text-color);"></i> -->
|
||
</div>
|
||
<div class="input-group-append">
|
||
<button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('taskDirSelected')"><i class="bi bi-x-lg"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-for="(task, index) in formData.tasklist" :key="index" class="task mb-3">
|
||
<template v-if="(taskDirSelected == '' || task.taskname == taskDirSelected) && task.taskname.includes(taskNameFilter)">
|
||
<hr>
|
||
<div class="form-group row" style="align-items:center">
|
||
<div class="col pl-0" data-toggle="collapse" :data-target="'#collapse_'+index" aria-expanded="true" :aria-controls="'collapse_'+index">
|
||
<div class="btn btn-block text-left">
|
||
<i class="bi bi-caret-right-fill"></i> #<span v-html="`${String(index+1).padStart(2, '0')} ${task.taskname}`"></span>
|
||
<span v-if="formData.button_display.latest_transfer_file !== 'disabled' && taskLatestFiles[task.taskname]"
|
||
class="task-latest-file"
|
||
:class="{'hover-only': formData.button_display.latest_transfer_file === 'hover'}">
|
||
· {{ taskLatestFiles[task.taskname] }}
|
||
</span>
|
||
<span v-if="formData.button_display.latest_update_date !== 'disabled' && taskLatestRecords[task.taskname]"
|
||
class="task-latest-date"
|
||
:class="{'hover-only': formData.button_display.latest_update_date === 'hover'}">
|
||
· {{ getTaskLatestRecordDisplay(task.taskname) }}
|
||
</span>
|
||
<span v-if="isTaskUpdatedToday(task.taskname) && shouldShowTodayIndicator()"
|
||
class="task-today-indicator"
|
||
:class="getTodayIndicatorClass()">
|
||
<i class="bi bi-stars"></i>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="col-auto task-buttons">
|
||
<button type="button" class="btn btn-outline-plex" v-if="formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" :class="{'hover-only': formData.button_display.refresh_plex === 'hover'}" @click="refreshPlexLibrary(index)" title="刷新Plex媒体库"><img src="./static/Plex.svg" class="plex-icon"></button>
|
||
<button type="button" class="btn btn-outline-alist" v-if="formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" :class="{'hover-only': formData.button_display.refresh_alist === 'hover'}" @click="refreshAlistDirectory(index)" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
|
||
<button class="btn btn-warning" v-if="task.shareurl_ban" :title="formatShareUrlBanMessage(task.shareurl_ban)" disabled><i class="bi bi-exclamation-circle"></i></button>
|
||
<button type="button" class="btn btn-outline-primary" @click="runScriptNow(index)" title="运行此任务" v-if="!task.shareurl_ban" :class="{'hover-only': formData.button_display.run_task === 'hover'}"><i class="bi bi-caret-right"></i></button>
|
||
<button type="button" class="btn btn-outline-danger" @click="removeTask(index)" title="删除此任务" :class="{'hover-only': formData.button_display.delete_task === 'hover'}"><i class="bi bi-trash3"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="collapse ml-3" :id="'collapse_'+index">
|
||
<div class="alert alert-warning" role="alert" v-if="task.shareurl_ban" v-html="formatShareUrlBanMessage(task.shareurl_ban)"></div>
|
||
<div class="form-group row">
|
||
<label class="col-sm-2 col-form-label">任务名称</label>
|
||
<div class="col-sm-10">
|
||
<div class="input-group">
|
||
<input type="text" name="taskname[]" class="form-control" v-model="task.taskname" placeholder="必填" @focus="smart_param.showSuggestions=true;focusTaskname(index, task)" @input="changeTaskname(index, task)">
|
||
<div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.index === index && (smart_param.taskSuggestions.success || smart_param.isSearching)">
|
||
<div class="dropdown-item text-muted" v-if="smart_param.isSearching" style="font-size:14px; padding: 0 8px; text-align: left;">
|
||
<span v-if="smart_param.validating">
|
||
<i class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></i>
|
||
正在验证链接有效性...({{ smart_param.validateProgress.current }}/{{ smart_param.validateProgress.total }})<span v-if="smart_param.validateProgress.valid > 0">已找到 {{ smart_param.validateProgress.valid }} 个有效链接</span>
|
||
</span>
|
||
<span v-else>正在搜索中...</span>
|
||
</div>
|
||
<div class="dropdown-item text-muted" v-else style="font-size:14px; padding-left: 8px; text-align: left;">
|
||
{{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data && smart_param.taskSuggestions.data.length ? `以下资源由 ${smart_param.taskSuggestions.source} 搜索提供(仅显示有效链接),如有侵权请联系资源发布方` : "未搜索到有效资源" }}
|
||
</div>
|
||
<div v-for="suggestion in smart_param.taskSuggestions.data || []" :key="suggestion.taskname" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(index, suggestion)" style="font-size: 14px;" :title="suggestion.content">
|
||
<span v-html="suggestion.verify ? '✅': ''"></span> {{ suggestion.taskname }}
|
||
<small class="text-muted">
|
||
<a :href="suggestion.shareurl" target="_blank" @click.stop>{{ suggestion.shareurl }}</a>
|
||
</small>
|
||
</div>
|
||
</div>
|
||
<div class="input-group-append" title="资源搜索">
|
||
<button class="btn btn-primary" type="button" @click="searchSuggestions(index, task.taskname)">
|
||
<i v-if="smart_param.isSearching && smart_param.index === index" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></i>
|
||
<i v-else class="bi bi-search"></i>
|
||
</button>
|
||
<div class="input-group-text" title="谷歌搜索">
|
||
<a target="_blank" :href="`https://www.google.com/search?q=%22pan.quark.cn/s%22+${cleanTaskNameForSearch(task.taskname)}`"><i class="bi bi-google"></i></a>
|
||
</div>
|
||
<div class="input-group-text" title="TMDB搜索">
|
||
<a target="_blank" :href="`https://www.themoviedb.org/search?query=${cleanTaskNameForSearch(task.taskname)}`"><img src="./static/TMDB.svg" class="tmdb-icon"></a>
|
||
</div>
|
||
<div class="input-group-text" title="豆瓣搜索">
|
||
<a target="_blank" :href="`https://search.douban.com/movie/subject_search?search_text=${cleanTaskNameForSearch(task.taskname)}`"><img src="./static/Douban.svg" class="douban-icon"></a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group row" title="支持子目录链接,Web端打开分享点入目录,复制浏览器的URL即可;支持带提取码链接,查阅Wiki了解详情">
|
||
<label class="col-sm-2 col-form-label">分享链接</label>
|
||
<div class="col-sm-10">
|
||
<div class="input-group">
|
||
<input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)">
|
||
<div class="input-group-append" v-if="task.shareurl">
|
||
<button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.previewRegex=false;showShareSelect(index)" title="选择需转存的文件夹"><i class="bi bi-folder"></i></button>
|
||
<div class="input-group-text">
|
||
<a target="_blank" :href="task.shareurl"><i class="bi bi-link-45deg"></i></a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group row">
|
||
<label class="col-sm-2 col-form-label">保存路径</label>
|
||
<div class="col-sm-10">
|
||
<div class="input-group">
|
||
<input type="text" name="savepath[]" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(index, task)" @input="onSavepathChange(index, task)">
|
||
<div class="input-group-append">
|
||
<button class="btn btn-outline-secondary" type="button" v-if="smart_param.savepath && smart_param.index == index && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath" title="恢复保存路径"><i class="bi bi-reply"></i></button>
|
||
<button class="btn btn-outline-secondary" type="button" @click="showSavepathSelect(index)" title="选择保存到的文件夹"><i class="bi bi-folder"></i></button>
|
||
<button type="button" class="btn btn-outline-secondary" @click="resetFolder(index)" title="重置文件夹:此操作将删除当前保存路径中的所有文件及相关转存记录,且不可恢复,请谨慎操作"><i class="bi bi-folder-x"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group row" :title="task.use_sequence_naming || task.use_episode_naming ? (task.use_sequence_naming ? '输入带{}占位符的重命名格式,如:剧名 - S01E{}、剧名.S03E{}等,{}将被替换为集序号(按文件名和修改日期智能排序),目录中的文件夹会被自动过滤' : '输入带[]占位符的重命名格式,如:剧名 - S01E[]、剧名.S03E[]等,[]将被替换为从文件名中提取的集编号,目录中的文件夹会被自动过滤') : '可用作筛选,只转存匹配到文件名的文件,留空则直接转存所有文件'">
|
||
<label class="col-sm-2 col-form-label">命名规则</label>
|
||
<div class="col-sm-10">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.previewRegex=true;fileSelect.sortBy='file_name_re';fileSelect.sortOrder='desc';showShareSelect(index)" :title="task.use_sequence_naming ? '预览顺序命名效果' : (task.use_episode_naming ? '预览剧集命名效果' : '预览正则命名效果')">
|
||
{{ task.use_sequence_naming ? '顺序命名' : (task.use_episode_naming ? '剧集命名' : '正则命名') }}
|
||
</button>
|
||
</div>
|
||
<input type="text" name="pattern[]" class="form-control" v-model="task.pattern" :placeholder="task.use_sequence_naming ? '输入带{}占位符的重命名格式,如:剧名 - S01E{}' : (task.use_episode_naming ? '输入带[]占位符的重命名格式,如:剧名 - S01E[]' : '匹配表达式')" list="magicRegex" @input="detectNamingMode(task); onPatternChange(index, task)">
|
||
<input v-if="!task.use_sequence_naming && !task.use_episode_naming" type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式">
|
||
<div class="input-group-append" title="保存时只比较文件名的部分,01.mp4和01.mkv视同为同一文件,不重复转存">
|
||
<div class="input-group-text">
|
||
<input type="checkbox" v-model="task.ignore_extension"> 忽略后缀
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<datalist id="magicRegex">
|
||
<option v-for="(value, key) in formData.magic_regex" :key="key" :value="`${key}`" v-html="`${value.pattern.replace('<', '<\u200B')} → ${value.replace}`"></option>
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group row" title="名称包含过滤词汇的项目不会被转存,多个词用逗号分隔,支持通过文件名和扩展名过滤文件和文件夹">
|
||
<label class="col-sm-2 col-form-label">过滤规则</label>
|
||
<div class="col-sm-10">
|
||
<div class="input-group">
|
||
<input type="text" name="filterwords[]" class="form-control" v-model="task.filterwords" placeholder="可选,输入过滤词汇,用逗号分隔,如:纯享,txt,超前企划,名称包含过滤词汇的项目不会被转存">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group row" title="只转存比选中文件更新的文件,请在符合筛选条件的文件中进行选择,在更换分享链接时非常有用">
|
||
<label class="col-sm-2 col-form-label">起始文件</label>
|
||
<div class="col-sm-10">
|
||
<div class="input-group">
|
||
<input type="text" class="form-control" placeholder="可选,只转存比此文件更新的文件,请在符合筛选条件的文件中进行选择" name="startfid[]" v-model="task.startfid">
|
||
<div class="input-group-append" v-if="task.shareurl">
|
||
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.previewRegex=false;showShareSelect(index)" title="选择起始文件"><i class="bi bi-folder"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group row" title="匹配成功的文件夹的所有嵌套目录都会被更新,并且会应用与根目录相同的正则命名和过滤规则。注意:原理是逐级索引,深层嵌套目录的场景下效率非常低,慎用.*">
|
||
<label class="col-sm-2 col-form-label">更新目录</label>
|
||
<div class="col-sm-10">
|
||
<input type="text" name="update_subdir[]" class="form-control" v-model="task.update_subdir" placeholder="可选,输入需要更新的子目录的文件夹名称(或正则表达式),多个项目用竖线分隔,如:4K|1080P">
|
||
</div>
|
||
</div>
|
||
<div class="form-group row">
|
||
<label class="col-sm-2 col-form-label">截止日期</label>
|
||
<div class="col-sm-10">
|
||
<div class="input-group">
|
||
<input type="date" name="enddate[]" class="form-control date-input-no-icon" v-model="task.enddate" placeholder="可选" :ref="'enddate_' + index">
|
||
<div class="input-group-append">
|
||
<button type="button" class="btn btn-outline-secondary" @click="openDatePicker(index)" title="选择截止日期">
|
||
<i class="bi bi-calendar3"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group row" title="只在勾选的星期时才运行,对周更的内容非常有用">
|
||
<label class="col-sm-2 col-form-label">执行周期</label>
|
||
<div class="col-sm-10 col-form-label">
|
||
<div class="form-check form-check-inline" title="也可用作任务总开关">
|
||
<input class="form-check-input" type="checkbox" :checked="task.runweek.length === 7" @change="toggleAllWeekdays(task)" :indeterminate.prop="task.runweek.length > 0 && task.runweek.length < 7">
|
||
<label class="form-check-label">全选</label>
|
||
</div>
|
||
<div class="form-check form-check-inline" v-for="(day, index) in weekdays" :key="index">
|
||
<input class="form-check-input" type="checkbox" v-model="task.runweek" :value="index+1">
|
||
<label class="form-check-label" v-html="day"></label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group row" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length" title="单个任务的插件配置,具体键值由插件定义,查阅Wiki了解详情">
|
||
<label class="col-sm-2 col-form-label">插件配置</label>
|
||
<div class="col-sm-10">
|
||
<v-jsoneditor v-model="task.addition" :options="{mode:'tree'}" :plus="false" height="162px" style="margin-bottom: -8px;"></v-jsoneditor>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<hr v-if="formData.tasklist.length > 0" class="task-divider">
|
||
<div class="row">
|
||
<div class="col-sm-12 d-flex justify-content-end">
|
||
<button type="button" class="btn btn-outline-primary" @click="addTask()" title="添加任务"><i class="bi bi-plus-lg"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="activeTab === 'history'">
|
||
<div style="height: 20px;"></div>
|
||
<div class="row" style="margin-bottom: 20px;">
|
||
<div class="col-lg-6 col-md-6 mb-2 mb-md-0">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">名称筛选</span>
|
||
</div>
|
||
<input type="text" class="form-control" v-model="historyNameFilter" placeholder="任务或转存为名称关键词">
|
||
<div class="input-group-append">
|
||
<button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('historyNameFilter')"><i class="bi bi-x-lg"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-6 col-md-6">
|
||
<div class="input-group">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">任务筛选</span>
|
||
</div>
|
||
<div class="position-relative" style="flex: 1;">
|
||
<select class="form-control task-filter-select" v-model="historyTaskSelected" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 24px !important;">
|
||
<option value="">全部任务</option>
|
||
<option v-for="task in historyTasks" :value="task" v-html="task"></option>
|
||
</select>
|
||
<!-- <i class="bi bi-chevron-down select-arrow" style="position: absolute; pointer-events: none; color: var(--dark-text-color);"></i> -->
|
||
</div>
|
||
<div class="input-group-append">
|
||
<button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('historyTaskSelected')"><i class="bi bi-x-lg"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="table table-hover selectable-records">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:10%" class="cursor-pointer" @click="sortHistory('transfer_time')">转存日期 <i v-if="historyParams.sortBy === 'transfer_time'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<th style="width:18%" class="cursor-pointer" @click="sortHistory('task_name')">任务名称 <i v-if="historyParams.sortBy === 'task_name'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<th style="width:25%" class="cursor-pointer" @click="sortHistory('original_name')">原文件 <i v-if="historyParams.sortBy === 'original_name'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<th style="width:25%" class="cursor-pointer" @click="sortHistory('renamed_to')">转存为 <i v-if="historyParams.sortBy === 'renamed_to'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<th style="width:7%; min-width: 60px;" class="cursor-pointer file-size-column" @click="sortHistory('file_size')">大小 <i v-if="historyParams.sortBy === 'file_size'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<th style="width:10%" class="cursor-pointer" @click="sortHistory('modify_date')">修改日期 <i v-if="historyParams.sortBy === 'modify_date'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-if="filteredHistoryRecords.length === 0">
|
||
<td colspan="6" class="text-center">暂无记录</td>
|
||
</tr>
|
||
<tr v-for="(record, index) in filteredHistoryRecords" :key="record.id"
|
||
:class="{'selected-record': selectedRecords.includes(record.id)}"
|
||
@click="selectRecord($event, record.id)">
|
||
<td>{{ record.transfer_time_readable }}</td>
|
||
<td class="position-relative">
|
||
<div v-if="!record._expandedFields || !record._expandedFields.includes('task_name')"
|
||
class="text-truncate"
|
||
:title="record.task_name"
|
||
v-check-overflow="index + '|task_name'"
|
||
:class="{'task-name-hover': true}"
|
||
@click.stop="filterByTaskName(record.task_name, $event)">
|
||
{{ record.task_name }}
|
||
</div>
|
||
<div class="expand-button" v-if="isTextTruncated(record.task_name, index, 'task_name')" @click.stop="toggleExpand(index, 'task_name', $event)">
|
||
<i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
||
</div>
|
||
<div class="expanded-text task-name-hover" v-if="record._expandedFields && record._expandedFields.includes('task_name')" @click.stop="filterByTaskName(record.task_name, $event)">
|
||
{{ record.task_name }}
|
||
</div>
|
||
</td>
|
||
<td class="position-relative">
|
||
<div v-if="!record._expandedFields || !record._expandedFields.includes('original_name')"
|
||
class="text-truncate"
|
||
:title="record.original_name"
|
||
v-check-overflow="index + '|original_name'">
|
||
{{ record.original_name }}
|
||
</div>
|
||
<div class="expand-button" v-if="isTextTruncated(record.original_name, index, 'original_name')" @click.stop="toggleExpand(index, 'original_name', $event)">
|
||
<i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
||
</div>
|
||
<div class="expanded-text" v-if="record._expandedFields && record._expandedFields.includes('original_name')">
|
||
{{ record.original_name }}
|
||
</div>
|
||
</td>
|
||
<td class="position-relative">
|
||
<div v-if="!record._expandedFields || !record._expandedFields.includes('renamed_to')"
|
||
class="text-truncate"
|
||
:title="record.renamed_to"
|
||
v-check-overflow="index + '|renamed_to'">
|
||
{{ record.renamed_to }}
|
||
<span v-if="isRecordUpdatedToday(record) && shouldShowTodayIndicator()"
|
||
class="task-today-indicator"
|
||
:class="getTodayIndicatorClass()">
|
||
<i class="bi bi-stars"></i>
|
||
</span>
|
||
</div>
|
||
<div class="expand-button" v-if="isTextTruncated(record.renamed_to, index, 'renamed_to')" @click.stop="toggleExpand(index, 'renamed_to', $event)">
|
||
<i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
||
</div>
|
||
<div class="expanded-text" v-if="record._expandedFields && record._expandedFields.includes('renamed_to')">
|
||
{{ record.renamed_to }}
|
||
<span v-if="isRecordUpdatedToday(record) && shouldShowTodayIndicator()"
|
||
class="task-today-indicator"
|
||
:class="getTodayIndicatorClass()">
|
||
<i class="bi bi-stars"></i>
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="file-size-cell">
|
||
<span class="file-size-value">{{ record.file_size_readable }}</span>
|
||
<span class="delete-record-btn" @click.stop="deleteRecord(record.id, record.original_name)" title="删除记录">
|
||
<i class="bi bi-trash3"></i>
|
||
</span>
|
||
</td>
|
||
<td>{{ record.modify_date_readable }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 分页控制 -->
|
||
<div class="pagination-container d-flex justify-content-between align-items-center mt-3">
|
||
<div class="page-info text-secondary">
|
||
显示 {{ history.pagination && history.pagination.total_records > 0 ? ((historyParams.page - 1) * historyParams.page_size + 1) + '-' + Math.min(historyParams.page * historyParams.page_size, history.pagination.total_records) : '0' }} 条,共 {{ history.pagination ? history.pagination.total_records : 0 }} 条记录{{ selectedRecords.length > 0 ? ',已选中 ' + selectedRecords.length + ' 条记录' : '' }}
|
||
</div>
|
||
<div class="pagination-controls d-flex align-items-center">
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: historyParams.page <= 1 }" @click="changePage(historyParams.page - 1)" :disabled="historyParams.page <= 1">
|
||
<i class="bi bi-chevron-left"></i>
|
||
</button>
|
||
|
||
<!-- 分页按钮 -->
|
||
<template v-if="totalPages > 0">
|
||
<!-- 第一页按钮 -->
|
||
<button type="button" class="btn btn-sm mx-1"
|
||
:class="historyParams.page === 1 ? 'btn-primary' : 'btn-outline-secondary'"
|
||
@click="changePage(1)">
|
||
1
|
||
</button>
|
||
|
||
<!-- 省略号(前) -->
|
||
<span class="mx-1" v-if="getVisiblePageNumbers().length > 0 && getVisiblePageNumbers()[0] > 2">...</span>
|
||
|
||
<!-- 中间页码 -->
|
||
<button v-for="page in getVisiblePageNumbers()" :key="page" type="button"
|
||
class="btn btn-sm mx-1"
|
||
:class="historyParams.page === page ? 'btn-primary' : 'btn-outline-secondary'"
|
||
@click="changePage(page)">
|
||
{{ page }}
|
||
</button>
|
||
|
||
<!-- 省略号(后) -->
|
||
<span class="mx-1" v-if="getVisiblePageNumbers().length > 0 && getVisiblePageNumbers()[getVisiblePageNumbers().length - 1] < totalPages - 1">...</span>
|
||
|
||
<!-- 最后一页按钮 -->
|
||
<button v-if="totalPages > 1" type="button" class="btn btn-sm mx-1"
|
||
:class="historyParams.page === totalPages ? 'btn-primary' : 'btn-outline-secondary'"
|
||
@click="changePage(totalPages)">
|
||
{{ totalPages }}
|
||
</button>
|
||
</template>
|
||
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: historyParams.page >= totalPages }" @click="changePage(historyParams.page + 1)" :disabled="historyParams.page >= totalPages">
|
||
<i class="bi bi-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
<div class="pagination-settings d-flex align-items-center">
|
||
<span class="text-secondary mr-1">跳到第</span>
|
||
<div class="input-group mx-1" style="width: 70px">
|
||
<input type="number" class="form-control form-control-sm" v-model.number="gotoPage" min="1" :max="totalPages">
|
||
</div>
|
||
<span class="text-secondary mr-2">页</span>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mr-2" @click="changePage(gotoPage)">确定</button>
|
||
<div class="dropdown">
|
||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
|
||
{{ historyParams.page_size === 99999 ? '全部记录' : historyParams.page_size + ' 条/页' }}
|
||
</button>
|
||
<div class="dropdown-menu">
|
||
<a class="dropdown-item" href="#" v-for="size in [15, 30, 50, 100, 500, 1000, 'all']" :key="size" @click.prevent="changePageSizeTo(size)">{{ size === 'all' ? '全部记录' : size + ' 条/页' }}</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="activeTab === 'filemanager'">
|
||
<div style="height: 20px;"></div>
|
||
<!-- 账号选择栏 -->
|
||
<div class="row file-manager-account-selector">
|
||
<div class="col-12">
|
||
<!-- 桌面端账号选择 -->
|
||
<div class="d-flex align-items-center file-manager-rule-bar">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">夸克账号</span>
|
||
</div>
|
||
<div class="position-relative" style="flex: 1;">
|
||
<select class="form-control task-filter-select file-manager-input file-manager-account-select" v-model="fileManager.selectedAccountIndex" @change="onAccountChange">
|
||
<option v-for="(account, index) in accountsDetail" :key="index" :value="index">{{ account.display_text }}</option>
|
||
</select>
|
||
</div>
|
||
<button type="button" class="btn btn-outline-plex batch-rename-btn" v-if="formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" @click="refreshFileManagerPlexLibrary" title="刷新Plex媒体库"><img src="./static/Plex.svg" class="plex-icon"></button>
|
||
<button type="button" class="btn btn-outline-alist batch-rename-btn" v-if="formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" @click="refreshFileManagerAlistDirectory" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
|
||
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="createNewFolder" title="新建文件夹">
|
||
<i class="bi bi-folder-plus"></i>
|
||
</button>
|
||
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="refreshCurrentFolderCache" title="刷新当前目录缓存">
|
||
<i class="bi bi-arrow-clockwise"></i>
|
||
</button>
|
||
</div>
|
||
<!-- 移动端账号选择 -->
|
||
<div class="file-manager-rule-bar-responsive">
|
||
<div class="d-flex align-items-center">
|
||
<div class="input-group" style="flex: 1;">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">夸克账号</span>
|
||
</div>
|
||
<select class="form-control task-filter-select file-manager-account-select" v-model="fileManager.selectedAccountIndex" @change="onAccountChange">
|
||
<option v-for="(account, index) in accountsDetail" :key="index" :value="index">{{ account.display_text }}</option>
|
||
</select>
|
||
</div>
|
||
<button type="button" class="btn btn-outline-plex batch-rename-btn" v-if="formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" @click="refreshFileManagerPlexLibrary" title="刷新Plex媒体库"><img src="./static/Plex.svg" class="plex-icon"></button>
|
||
<button type="button" class="btn btn-outline-alist batch-rename-btn" v-if="formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" @click="refreshFileManagerAlistDirectory" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
|
||
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="createNewFolder" title="新建文件夹">
|
||
<i class="bi bi-folder-plus"></i>
|
||
</button>
|
||
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="refreshCurrentFolderCache" title="刷新当前目录缓存">
|
||
<i class="bi bi-arrow-clockwise"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 重命名规则设置栏 -->
|
||
<div class="row mb-3">
|
||
<div class="col-12">
|
||
<!-- 桌面端结构(原有) -->
|
||
<div class="d-flex align-items-center file-manager-rule-bar">
|
||
<div class="input-group-prepend">
|
||
<button class="btn btn-outline-secondary" type="button" @click="previewAndRename"
|
||
:title="fileManager.use_sequence_naming ? '预览顺序命名效果' : (fileManager.use_episode_naming ? '预览剧集命名效果' : '预览正则命名效果')">
|
||
{{ fileManager.use_sequence_naming ? '顺序命名' : (fileManager.use_episode_naming ? '剧集命名' : '正则命名') }}
|
||
</button>
|
||
</div>
|
||
<input type="text" class="form-control file-manager-input" v-model="fileManager.pattern"
|
||
:placeholder="fileManager.use_sequence_naming ? '输入带{}占位符的重命名格式,如:剧名 - S01E{}' : (fileManager.use_episode_naming ? '输入带[]占位符的重命名格式,如:剧名 - S01E[]' : '匹配表达式')"
|
||
@input="detectFileManagerNamingMode"
|
||
:title="fileManager.use_sequence_naming ? '输入带{}占位符的重命名格式,如:剧名 - S01E{}、剧名.S03E{}等,{}将被替换为集序号(按文件名和修改日期智能排序)' : (fileManager.use_episode_naming ? '输入带[]占位符的重命名格式,如:剧名 - S01E[]、剧名.S03E[]等,[]将被替换为从文件名中提取的集编号' : '只重命名匹配到文件名的文件,留空不会进行重命名')">
|
||
<input v-if="!fileManager.use_sequence_naming && !fileManager.use_episode_naming" type="text" class="form-control file-manager-input" v-model="fileManager.replace" placeholder="替换表达式" title="替换表达式">
|
||
<input type="text" class="form-control file-manager-input" v-model="fileManager.filterwords" placeholder="可选,输入过滤词汇,用逗号分隔,如:纯享,txt,超前企划" title="名称包含过滤词汇的项目不会被重命名,多个词用逗号分隔,支持通过文件名和扩展名过滤文件和文件夹">
|
||
<div class="input-group-append">
|
||
<div class="input-group-text" title="勾选后,重命名和过滤规则也将应用于文件夹">
|
||
<input type="checkbox" v-model="fileManager.include_folders"> 含文件夹
|
||
</div>
|
||
</div>
|
||
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="previewAndRename" title="预览并执行重命名">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
</div>
|
||
<!-- 移动端结构(还原为input-group风格,两行,样式与桌面端一致) -->
|
||
<div class="file-manager-rule-bar-responsive">
|
||
<!-- 第一行:命名按钮+表达式+替换表达式 -->
|
||
<div class="input-group mb-2">
|
||
<div class="input-group-prepend">
|
||
<button class="btn btn-outline-secondary" type="button" @click="previewAndRename"
|
||
:title="fileManager.use_sequence_naming ? '预览顺序命名效果' : (fileManager.use_episode_naming ? '预览剧集命名效果' : '预览正则命名效果')">
|
||
{{ fileManager.use_sequence_naming ? '顺序命名' : (fileManager.use_episode_naming ? '剧集命名' : '正则命名') }}
|
||
</button>
|
||
</div>
|
||
<input type="text" class="form-control" v-model="fileManager.pattern"
|
||
:placeholder="fileManager.use_sequence_naming ? '输入带{}占位符的重命名格式,如:剧名 - S01E{}' : (fileManager.use_episode_naming ? '输入带[]占位符的重命名格式,如:剧名 - S01E[]' : '匹配表达式')"
|
||
@input="detectFileManagerNamingMode"
|
||
:title="fileManager.use_sequence_naming ? '输入带{}占位符的重命名格式,如:剧名 - S01E{}、剧名.S03E{}等,{}将被替换为集序号(按文件名和修改日期智能排序)' : (fileManager.use_episode_naming ? '输入带[]占位符的重命名格式,如:剧名 - S01E[]、剧名.S03E[]等,[]将被替换为从文件名中提取的集编号' : '只重命名匹配到文件名的文件,留空不会进行重命名')">
|
||
<input v-if="!fileManager.use_sequence_naming && !fileManager.use_episode_naming" type="text" class="form-control" v-model="fileManager.replace" placeholder="替换表达式" title="替换表达式">
|
||
</div>
|
||
<!-- 第二行:过滤规则+含文件夹+按钮,flex包裹,含文件夹有圆角,按钮有8px间距和圆角 -->
|
||
<div class="input-group d-flex align-items-center">
|
||
<div class="input-group-prepend">
|
||
<span class="input-group-text">过滤规则</span>
|
||
</div>
|
||
<input type="text" class="form-control" v-model="fileManager.filterwords" placeholder="可选,输入过滤词汇,用逗号分隔,如:纯享,txt,超前企划" title="名称包含过滤词汇的项目不会被重命名,多个词用逗号分隔,支持通过文件名和扩展名过滤文件和文件夹">
|
||
<span class="input-group-text file-folder-rounded">
|
||
<input type="checkbox" v-model="fileManager.include_folders"> 含文件夹
|
||
</span>
|
||
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="previewAndRename" title="预览并执行重命名">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 面包屑导航 -->
|
||
<nav aria-label="breadcrumb" class="file-manager-breadcrumb">
|
||
<ol class="breadcrumb">
|
||
<li class="breadcrumb-item cursor-pointer" @click="navigateToFolder('root')">全部文件</li>
|
||
<li v-for="(item, index) in fileManager.paths" :key="index" class="breadcrumb-item">
|
||
<a v-if="index != fileManager.paths.length - 1" href="#" @click.prevent="navigateToFolder(item.fid, item.name)">{{ item.name }}</a>
|
||
<span v-else class="text-muted">{{ item.name }}</span>
|
||
</li>
|
||
</ol>
|
||
</nav>
|
||
|
||
<!-- 文件表格 -->
|
||
<div class="table-responsive">
|
||
<table class="table table-hover selectable-files">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:80%" class="cursor-pointer" @click="sortFiles('file_name')">文件名 <i v-if="fileManager.sortBy === 'file_name'" :class="fileManager.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<th style="width:10%; min-width: 80px;" class="cursor-pointer" @click="sortFiles('file_size')">大小 <i v-if="fileManager.sortBy === 'file_size'" :class="fileManager.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<th style="width:10%" class="cursor-pointer" @click="sortFiles('updated_at')">修改日期 <i v-if="fileManager.sortBy === 'updated_at'" :class="fileManager.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-if="fileManager.loading">
|
||
<td colspan="3" class="text-center">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="sr-only">加载中...</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<tr v-else-if="fileManager.fileList.length === 0">
|
||
<td colspan="3" class="text-center">当前文件夹为空</td>
|
||
</tr>
|
||
<template v-else>
|
||
<tr v-for="(file, index) in fileManager.fileList" :key="file.fid"
|
||
@click="handleFileRowClick(file, $event)"
|
||
:class="{'selected-file': fileManager.selectedFiles.includes(file.fid)}"
|
||
class="cursor-pointer">
|
||
<td class="position-relative">
|
||
<!-- 编辑状态 -->
|
||
<div v-if="file._editing" class="d-flex align-items-center" @click.stop>
|
||
<i class="bi mr-2" :class="getFileIconClass(file.file_name, file.dir)"></i>
|
||
<input
|
||
type="text"
|
||
class="form-control form-control-sm rename-input"
|
||
v-model="file._editingName"
|
||
@keyup.enter="saveRenameFile(file)"
|
||
@keyup.escape="cancelRenameFile(file)"
|
||
@blur="saveRenameFile(file)"
|
||
@click.stop
|
||
:ref="'renameInput_' + file.fid">
|
||
</div>
|
||
<!-- 正常显示状态 -->
|
||
<div v-else-if="!file._expanded" class="d-flex align-items-center">
|
||
<i class="bi mr-2" :class="getFileIconClass(file.file_name, file.dir)"></i>
|
||
<div class="text-truncate"
|
||
:title="file.file_name"
|
||
v-check-file-overflow="index + '|file_name'">
|
||
{{ file.file_name }}
|
||
<span v-if="isFileUpdatedToday(file) && shouldShowTodayIndicator()"
|
||
class="task-today-indicator"
|
||
:class="getTodayIndicatorClass()">
|
||
<i class="bi bi-stars"></i>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div v-else style="white-space: normal; word-break: break-word; padding-right: 25px;">
|
||
<i class="bi mr-2" :class="getFileIconClass(file.file_name, file.dir)"></i>{{ file.file_name }}
|
||
<span v-if="isFileUpdatedToday(file) && shouldShowTodayIndicator()"
|
||
class="task-today-indicator"
|
||
:class="getTodayIndicatorClass()">
|
||
<i class="bi bi-stars"></i>
|
||
</span>
|
||
</div>
|
||
<div class="expand-button" v-if="!file._editing && isFilenameTruncated(file.file_name, index, 'file_name')" @click.stop="toggleExpandFilename(file)">
|
||
<i :class="file._expanded ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
||
</div>
|
||
</td>
|
||
<td class="file-size-cell">
|
||
<span class="file-size-value">{{ file.dir ? (file.include_items + ' 项') : formatFileSize(file.size || file.file_size) }}</span>
|
||
<span class="rename-record-btn" @click.stop="startRenameFile(file)" title="重命名文件">
|
||
<i class="bi bi-pencil"></i>
|
||
</span>
|
||
<span class="move-record-btn" @click.stop="startMoveFile(file)" title="移动文件">
|
||
<i class="bi bi-arrow-up-right-circle"></i>
|
||
</span>
|
||
<span class="delete-record-btn" @click.stop="deleteSelectedFilesForManager(file.fid, file.file_name, file.dir)" title="删除文件">
|
||
<i class="bi bi-trash3"></i>
|
||
</span>
|
||
</td>
|
||
<td>{{ formatDate(file.updated_at) }}</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 分页区域 -->
|
||
<div class="pagination-container d-flex justify-content-between align-items-center mt-3">
|
||
<div class="page-info text-secondary">
|
||
显示 {{ fileManager.currentPage > 0 ? ((fileManager.currentPage - 1) * fileManager.pageSize + 1) + '-' + Math.min(fileManager.currentPage * fileManager.pageSize, fileManager.total) : '0' }} 项,共 {{ fileManager.total }} 个项目{{ fileManager.selectedFiles.length > 0 ? ',已选中 ' + fileManager.selectedFiles.length + ' 项' : '' }}
|
||
</div>
|
||
<div class="pagination-controls d-flex align-items-center">
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: fileManager.currentPage <= 1 }" @click="changeFolderPage(fileManager.currentPage - 1)" :disabled="fileManager.currentPage <= 1">
|
||
<i class="bi bi-chevron-left"></i>
|
||
</button>
|
||
|
||
<template v-if="fileManager.totalPages > 0">
|
||
<button type="button" class="btn btn-sm mx-1"
|
||
:class="fileManager.currentPage === 1 ? 'btn-primary' : 'btn-outline-secondary'"
|
||
@click="changeFolderPage(1)">1</button>
|
||
|
||
<span class="mx-1" v-if="getVisibleFolderPageNumbers().length > 0 && getVisibleFolderPageNumbers()[0] > 2">...</span>
|
||
|
||
<button v-for="page in getVisibleFolderPageNumbers()" :key="page" type="button"
|
||
class="btn btn-sm mx-1"
|
||
:class="fileManager.currentPage === page ? 'btn-primary' : 'btn-outline-secondary'"
|
||
@click="changeFolderPage(page)">
|
||
{{ page }}
|
||
</button>
|
||
|
||
<span class="mx-1" v-if="getVisibleFolderPageNumbers().length > 0 && getVisibleFolderPageNumbers()[getVisibleFolderPageNumbers().length - 1] < fileManager.totalPages - 1">...</span>
|
||
|
||
<button v-if="fileManager.totalPages > 1" type="button" class="btn btn-sm mx-1"
|
||
:class="fileManager.currentPage === fileManager.totalPages ? 'btn-primary' : 'btn-outline-secondary'"
|
||
@click="changeFolderPage(fileManager.totalPages)">
|
||
{{ fileManager.totalPages }}
|
||
</button>
|
||
</template>
|
||
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: fileManager.currentPage >= fileManager.totalPages }" @click="changeFolderPage(fileManager.currentPage + 1)" :disabled="fileManager.currentPage >= fileManager.totalPages">
|
||
<i class="bi bi-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
<div class="pagination-settings d-flex align-items-center">
|
||
<span class="text-secondary mr-1">跳到第</span>
|
||
<div class="input-group mx-1" style="width: 70px">
|
||
<input type="number" class="form-control form-control-sm" v-model.number="fileManager.gotoPage" min="1" :max="fileManager.totalPages">
|
||
</div>
|
||
<span class="text-secondary mr-2">页</span>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mr-2" @click="changeFolderPage(fileManager.gotoPage)">确定</button>
|
||
<div class="dropdown">
|
||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
|
||
{{ fileManager.pageSize === 99999 ? '全部项目' : fileManager.pageSize + ' 项/页' }}
|
||
</button>
|
||
<div class="dropdown-menu">
|
||
<a class="dropdown-item" href="#" v-for="size in [15, 30, 50, 100, 500, 1000, 'all']" :key="size" @click.prevent="changeFileManagerPageSize(size)">{{ size === 'all' ? '全部项目' : size + ' 项/页' }}</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</form>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- 模态框 运行日志 -->
|
||
<div class="modal" tabindex="-1" id="logModal">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title"><b>运行日志</b>
|
||
<div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div>
|
||
</h5>
|
||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||
<i class="bi bi-x-lg"></i>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<pre v-html="run_log"></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模态框 文件选择 -->
|
||
<div class="modal" tabindex="-1" id="fileSelectModal">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">
|
||
<span v-if="fileSelect.previewRegex && fileSelect.index >= 0 && formData.tasklist[fileSelect.index]" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">{{ formData.tasklist[fileSelect.index].use_sequence_naming ? '顺序命名预览' : (formData.tasklist[fileSelect.index].use_episode_naming ? '剧集命名预览' : '正则命名预览') }}</span>
|
||
<span v-else-if="fileSelect.previewRegex && fileSelect.index === -1" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">{{ fileManager.use_sequence_naming ? '顺序命名预览' : (fileManager.use_episode_naming ? '剧集命名预览' : '正则命名预览') }}</span>
|
||
<span v-else-if="fileSelect.selectDir" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">选择{{fileSelect.moveMode ? '移动到的' : (fileSelect.selectShare ? '需转存的' : '保存到的')}}文件夹</span>
|
||
<span v-else style="font-weight: 600; font-family: inherit; letter-spacing: normal;">选择起始文件</span>
|
||
<div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div>
|
||
</h5>
|
||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||
<i class="bi bi-x-lg"></i>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body small">
|
||
<div class="alert alert-warning" v-if="fileSelect.error" v-html="fileSelect.error"></div>
|
||
<div v-else>
|
||
<!-- 面包屑导航 -->
|
||
<nav aria-label="breadcrumb" v-if="fileSelect.selectDir">
|
||
<ol class="breadcrumb">
|
||
<li class="breadcrumb-item cursor-pointer" @click="navigateTo('0','/')"><i class="bi bi-house-door"></i></li>
|
||
<li v-for="(item, index) in fileSelect.paths" class="breadcrumb-item">
|
||
<a v-if="index != fileSelect.paths.length - 1" href="#" @click="navigateTo(item.fid, item.name)">{{ item.name }}</a>
|
||
<span v-else class="text-muted">{{ item.name }}</span>
|
||
</li>
|
||
</ol>
|
||
</nav>
|
||
<!-- 文件列表 -->
|
||
<div class="mb-3" v-if="fileSelect.previewRegex">
|
||
<!-- 任务配置的命名预览 -->
|
||
<div v-if="fileSelect.index >= 0 && formData.tasklist[fileSelect.index]">
|
||
<div v-if="formData.tasklist[fileSelect.index].use_sequence_naming">
|
||
<div style="margin-bottom: 1px; padding-left: 12px; display: flex; align-items: center;">
|
||
<span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">顺序命名表达式:</span><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
|
||
</div>
|
||
<div style="padding-left: 12px; display: flex; align-items: center;">
|
||
<span class="text-muted" style="font-family: inherit; letter-spacing: normal;">预览结果仅供参考,新文件序号将基于网盘或转存记录中已有文件的最大序号递增。若分享链接文件不完整,实际结果可能有所不同。</span>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="formData.tasklist[fileSelect.index].use_episode_naming">
|
||
<div style="padding-left: 12px; display: flex; align-items: center;">
|
||
<span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">剧集命名表达式:</span><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
|
||
</div>
|
||
</div>
|
||
<div v-else>
|
||
<div style="margin-bottom: 1px; padding-left: 12px; display: flex; align-items: center;">
|
||
<span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">匹配表达式:</span><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
|
||
</div>
|
||
<div style="padding-left: 12px; display: flex; align-items: center;">
|
||
<span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">替换表达式:</span><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].replace"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 文件管理器的命名预览 -->
|
||
<div v-else-if="fileSelect.index === -1">
|
||
<div v-if="fileManager.use_sequence_naming">
|
||
<div style="margin-bottom: 12px; padding-left: 12px; display: flex; align-items: center;">
|
||
<span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">顺序命名表达式:</span><span class="badge badge-info" v-html="fileManager.pattern"></span>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="fileManager.use_episode_naming">
|
||
<div style="padding-left: 12px; display: flex; align-items: center;">
|
||
<span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">剧集命名表达式:</span><span class="badge badge-info" v-html="fileManager.pattern"></span>
|
||
</div>
|
||
</div>
|
||
<div v-else>
|
||
<div style="margin-bottom: 1px; padding-left: 12px; display: flex; align-items: center;">
|
||
<span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">匹配表达式:</span><span class="badge badge-info" v-html="fileManager.pattern"></span>
|
||
</div>
|
||
<div style="padding-left: 12px; display: flex; align-items: center;">
|
||
<span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">替换表达式:</span><span class="badge badge-info" v-html="fileManager.replace"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<table class="table table-hover table-sm">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col" class="col-filename cursor-pointer" style="padding-left: 5px; font-family: inherit; letter-spacing: normal;" @click="sortFileList('file_name')">文件名 <i v-if="fileSelect.sortBy === 'file_name'" :class="fileSelect.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<th scope="col" class="col-rename cursor-pointer" v-if="fileSelect.selectShare || fileSelect.previewRegex" style="padding-left: 5px; font-family: inherit; letter-spacing: normal;" @click="sortFileList('file_name_re')">{{ fileSelect.index !== null && fileSelect.index >= 0 && formData.tasklist[fileSelect.index] ?
|
||
(formData.tasklist[fileSelect.index].use_sequence_naming ? '顺序命名' :
|
||
(formData.tasklist[fileSelect.index].use_episode_naming ? '剧集命名' : '正则命名')) :
|
||
(fileManager.use_sequence_naming ? '顺序命名' :
|
||
(fileManager.use_episode_naming ? '剧集命名' : '正则命名')) }} <i v-if="fileSelect.sortBy === 'file_name_re'" :class="fileSelect.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<template v-if="!fileSelect.previewRegex">
|
||
<th scope="col" class="col-size cursor-pointer" style="font-family: inherit; letter-spacing: normal;" @click="sortFileList('size')">大小 <i v-if="fileSelect.sortBy === 'size'" :class="fileSelect.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<th scope="col" class="col-date cursor-pointer" style="font-family: inherit; letter-spacing: normal;" @click="sortFileList('updated_at')">修改日期 <i v-if="fileSelect.sortBy === 'updated_at'" :class="fileSelect.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
|
||
<th scope="col" class="col-action" v-if="!fileSelect.selectShare && !fileSelect.moveMode" style="font-family: inherit; letter-spacing: normal;">操作</th>
|
||
</template>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(file, key) in fileSelect.fileList" :key="key"
|
||
@click="fileSelect.selectDir ? (file.dir ? navigateTo(file.fid, file.file_name) : selectFileItem($event, file.fid)) : selectStartFid(file.fid)"
|
||
:class="{
|
||
'cursor-pointer': (fileSelect.previewRegex ? file.dir : (file.dir || !fileSelect.selectShare || (!fileSelect.selectDir && !file.dir))),
|
||
'selected-file': fileSelect.selectedFiles.includes(file.fid)
|
||
}"
|
||
style="vertical-align: top;"
|
||
@mousedown="preventTextSelection($event, file.dir)">
|
||
<td class="col-filename position-relative" style="padding-left: 5px; vertical-align: top;">
|
||
<div v-if="!file._expandedFields || !file._expandedFields.includes('file_name')"
|
||
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 25px;"
|
||
:title="file.file_name"
|
||
v-check-modal-overflow="key + '|file_name'">
|
||
<i class="bi" :class="getFileIconClass(file.file_name, file.dir)"></i> {{file.file_name}}
|
||
</div>
|
||
<div class="expand-button" v-if="isModalTextTruncated(file.file_name, key, 'file_name')" @click.stop="toggleModalExpand(key, 'file_name')">
|
||
<i :class="file._expandedFields && file._expandedFields.includes('file_name') ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
||
</div>
|
||
<div v-if="file._expandedFields && file._expandedFields.includes('file_name')" style="white-space: normal; word-break: break-word; padding-right: 25px; display: block; width: 100%; position: relative;">
|
||
<i class="bi" :class="getFileIconClass(file.file_name, file.dir)"></i> {{file.file_name}}
|
||
</div>
|
||
</td>
|
||
<td class="col-rename position-relative" v-if="fileSelect.selectShare || fileSelect.previewRegex" :class="{'text-success': file.file_name_re && !file.file_name_re.startsWith('×'), 'text-danger': !file.file_name_re || file.file_name_re === '×' || file.file_name_re.startsWith('× ')}" style="padding-left: 5px; vertical-align: top;">
|
||
<div v-if="!file._expandedFields || !file._expandedFields.includes('file_name_re')"
|
||
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 25px; color: inherit;"
|
||
:title="file.file_name_re || '×'"
|
||
v-check-modal-overflow="key + '|file_name_re'">
|
||
<template v-if="file.file_name_re && file.file_name_re.startsWith('× ')">
|
||
<span class="episode-x">×</span> <span class="episode-number-text">无法识别剧集编号</span>
|
||
</template>
|
||
<template v-else>
|
||
<span class="episode-x">{{file.file_name_re || '×'}}</span>
|
||
</template>
|
||
</div>
|
||
<div class="expand-button" v-if="isModalTextTruncated(file.file_name_re || '×', key, 'file_name_re')" @click.stop="toggleModalExpand(key, 'file_name_re')">
|
||
<i :class="file._expandedFields && file._expandedFields.includes('file_name_re') ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
|
||
</div>
|
||
<div v-if="file._expandedFields && file._expandedFields.includes('file_name_re')" style="white-space: normal; word-break: break-word; padding-right: 25px; display: block; width: 100%; position: relative; color: inherit;">
|
||
<template v-if="file.file_name_re && file.file_name_re.startsWith('× ')">
|
||
<span class="episode-x">×</span> <span class="episode-number-text">无法识别剧集编号</span>
|
||
</template>
|
||
<template v-else>
|
||
<span class="episode-x">{{file.file_name_re || '×'}}</span>
|
||
</template>
|
||
</div>
|
||
</td>
|
||
<template v-if="!fileSelect.previewRegex">
|
||
<td class="col-size" v-if="file.dir" style="vertical-align: top;">{{ file.include_items }} 项</td>
|
||
<td class="col-size" v-else style="vertical-align: top;">{{file.size | size}}</td>
|
||
<td class="col-date" style="vertical-align: top;">{{file.updated_at | ts2date}}</td>
|
||
<td class="col-action" v-if="!fileSelect.selectShare && !fileSelect.moveMode" style="vertical-align: top;">
|
||
<a @click.stop.prevent="deleteSelectedFiles(file.fid, file.file_name, file.dir)" style="cursor: pointer;">删除文件</a>
|
||
<a @click.stop.prevent="deleteSelectedFiles(file.fid, file.file_name, file.dir, true)" style="cursor: pointer; margin-left: 10px;">删除文件和记录</a>
|
||
</td>
|
||
</template>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer" v-if="fileSelect.selectDir && !fileSelect.previewRegex">
|
||
<div class="file-selection-info mr-auto" style="color: var(--dark-text-color); font-size: 0.875rem; line-height: 1.5;">
|
||
共 {{ fileSelect.fileList.length }} 个项目<span v-if="fileSelect.selectedFiles.length > 0">,已选中 {{ fileSelect.selectedFiles.length }} 个项目</span>
|
||
</div>
|
||
<button type="button" class="btn btn-primary btn-cancel" @click="$('#fileSelectModal').modal('hide')" v-if="fileSelect.moveMode">取消</button>
|
||
<button type="button" class="btn btn-primary btn-sm" @click="selectCurrentFolder()">
|
||
<span v-if="fileSelect.moveMode">移动到当前文件夹</span>
|
||
<span v-else>{{fileSelect.selectShare ? '转存当前文件夹' : '保存到当前文件夹'}}</span>
|
||
</button>
|
||
<button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare && !fileSelect.moveMode && fileSelect.index !== null && fileSelect.index >= 0 && formData.tasklist[fileSelect.index]" @click="selectCurrentFolder(true)">保存到当前位置的「<span class="badge badge-light" v-html="formData.tasklist[fileSelect.index].taskname"></span>」文件夹</button>
|
||
</div>
|
||
<div class="modal-footer" v-if="fileSelect.previewRegex && fileSelect.index === -1">
|
||
<div class="file-selection-info mr-auto" style="color: var(--dark-text-color); font-size: 0.875rem; line-height: 1.5;">
|
||
共 {{ fileSelect.fileList.filter(f => f.file_name_re && f.file_name_re !== '×' && !f.file_name_re.startsWith('×')).length }} 个项目可重命名
|
||
</div>
|
||
<button type="button" class="btn btn-primary btn-cancel" @click="cancelPreviewRename">取消</button>
|
||
<button type="button" class="btn btn-primary" :disabled="!fileSelect.canUndoRename || modalLoading" @click="undoPreviewRename">撤销</button>
|
||
<button type="button" class="btn btn-primary" :disabled="modalLoading || !fileSelect.fileList.some(f => f.file_name_re && f.file_name_re !== '×' && !f.file_name_re.startsWith('×'))" @click="applyPreviewRename">
|
||
重命名
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<script>
|
||
var app = new Vue({
|
||
el: '#app',
|
||
data: {
|
||
version: "[[ version ]]",
|
||
versionTips: "",
|
||
plugin_flags: "[[ plugin_flags ]]",
|
||
weekdays: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||
sidebarCollapsed: false,
|
||
showCloudSaverPassword: false,
|
||
showWebuiPassword: false,
|
||
pageWidthMode: 'medium', // 页面宽度模式:narrow, medium, wide
|
||
formData: {
|
||
cookie: [],
|
||
push_config: {},
|
||
media_servers: {},
|
||
tasklist: [],
|
||
magic_regex: {},
|
||
episode_patterns: [],
|
||
source: {
|
||
cloudsaver: {
|
||
server: "",
|
||
username: "",
|
||
password: "",
|
||
token: ""
|
||
}
|
||
},
|
||
webui: {
|
||
username: "",
|
||
password: ""
|
||
},
|
||
button_display: {
|
||
run_task: "always",
|
||
delete_task: "always",
|
||
refresh_plex: "always",
|
||
refresh_alist: "always",
|
||
latest_update_date: "always",
|
||
latest_transfer_file: "always",
|
||
today_update_indicator: "always"
|
||
},
|
||
file_performance: {
|
||
api_page_size: 200,
|
||
cache_expire_time: 30
|
||
}
|
||
},
|
||
userInfoList: [], // 用户信息列表
|
||
accountsDetail: [], // 账号详细信息列表
|
||
newTask: {
|
||
taskname: "",
|
||
shareurl: "",
|
||
savepath: "",
|
||
pattern: "",
|
||
replace: "",
|
||
enddate: "",
|
||
addition: {},
|
||
ignore_extension: false,
|
||
filterwords: "",
|
||
runweek: [1, 2, 3, 4, 5, 6, 7],
|
||
sequence_naming: "",
|
||
use_sequence_naming: false,
|
||
episode_naming: "",
|
||
use_episode_naming: false
|
||
},
|
||
run_log: "",
|
||
taskDirs: [""],
|
||
taskDirSelected: "",
|
||
taskNameFilter: "",
|
||
taskLatestRecords: {}, // 存储每个任务的最新转存记录日期
|
||
taskLatestFiles: {}, // 存储每个任务的最近转存文件
|
||
modalLoading: false,
|
||
smart_param: {
|
||
index: null,
|
||
savepath: "",
|
||
origin_savepath: "",
|
||
taskSuggestions: {
|
||
success: false,
|
||
data: []
|
||
},
|
||
showSuggestions: false,
|
||
isSearching: false,
|
||
validating: false,
|
||
validateProgress: {
|
||
total: 0,
|
||
current: 0,
|
||
valid: 0
|
||
},
|
||
searchTimer: null,
|
||
},
|
||
activeTab: 'config',
|
||
configModified: false,
|
||
fileSelect: {
|
||
index: null,
|
||
shareurl: "",
|
||
stoken: "",
|
||
fileList: [],
|
||
paths: [],
|
||
selectDir: true,
|
||
selectShare: true,
|
||
previewRegex: false,
|
||
sortBy: "updated_at", // 默认排序字段
|
||
sortOrder: "desc", // 默认排序顺序
|
||
selectedFiles: [], // 存储选中的文件ID
|
||
lastSelectedFileIndex: -1, // 记录最后选择的文件索引
|
||
canUndoRename: false,
|
||
moveMode: false, // 是否为移动文件模式
|
||
moveFileIds: [] // 要移动的文件ID列表
|
||
},
|
||
historyParams: {
|
||
sortBy: "transfer_time",
|
||
order: "desc",
|
||
page_size: 15,
|
||
page: 1
|
||
},
|
||
history: {
|
||
records: [],
|
||
pagination: {}
|
||
},
|
||
historyNameFilter: "",
|
||
historyTaskSelected: "",
|
||
gotoPage: 1,
|
||
totalPages: 1,
|
||
displayedPages: [],
|
||
allTaskNames: [],
|
||
toastMessage: "",
|
||
selectedRecords: [],
|
||
lastSelectedRecordIndex: -1, // 记录最后选择的记录索引,用于Shift选择
|
||
fileManager: {
|
||
loading: false,
|
||
// 当前文件夹ID - 根据localStorage和账号索引决定初始目录
|
||
currentFolder: (() => {
|
||
const selectedAccountIndex = parseInt(localStorage.getItem('quarkAutoSave_selectedAccountIndex') || '0');
|
||
return localStorage.getItem(`quarkAutoSave_fileManagerLastFolder_${selectedAccountIndex}`) || 'root';
|
||
})(),
|
||
// 选中的账号索引 - 从localStorage读取,默认为0
|
||
selectedAccountIndex: parseInt(localStorage.getItem('quarkAutoSave_selectedAccountIndex') || '0'),
|
||
// 面包屑导航路径
|
||
paths: [],
|
||
// 文件列表
|
||
fileList: [],
|
||
// 总数
|
||
total: 0,
|
||
// 当前页码
|
||
currentPage: 1,
|
||
// 页面大小
|
||
pageSize: 15,
|
||
// 总页数
|
||
totalPages: 1,
|
||
// 排序列 - 从localStorage读取,如果没有则使用默认值
|
||
sortBy: (() => {
|
||
const savedSort = localStorage.getItem('quarkAutoSave_fileManagerSort');
|
||
if (savedSort) {
|
||
try {
|
||
return JSON.parse(savedSort).sortBy || 'file_name';
|
||
} catch (e) {
|
||
return 'file_name';
|
||
}
|
||
}
|
||
return 'file_name';
|
||
})(),
|
||
// 排序方式 - 从localStorage读取,如果没有则使用默认值
|
||
sortOrder: (() => {
|
||
const savedSort = localStorage.getItem('quarkAutoSave_fileManagerSort');
|
||
if (savedSort) {
|
||
try {
|
||
return JSON.parse(savedSort).sortOrder || 'asc';
|
||
} catch (e) {
|
||
return 'asc';
|
||
}
|
||
}
|
||
return 'asc';
|
||
})(),
|
||
// 命名模式
|
||
use_sequence_naming: false,
|
||
use_episode_naming: false,
|
||
// 正则表达式
|
||
pattern: '',
|
||
replace: '',
|
||
// 过滤关键词
|
||
filterwords: '',
|
||
// 包含文件夹
|
||
include_folders: false,
|
||
// 选中的文件
|
||
selectedFiles: [],
|
||
// 上次选中的文件索引
|
||
lastSelectedFileIndex: -1,
|
||
// 跳转到页码
|
||
gotoPage: 1
|
||
},
|
||
batchRename: {
|
||
loading: false,
|
||
results: [],
|
||
namingMode: 'regex',
|
||
errors: []
|
||
}
|
||
},
|
||
computed: {
|
||
episodePatternsText: {
|
||
get() {
|
||
if (!this.formData.episode_patterns) return '';
|
||
return this.formData.episode_patterns.map(p => p.regex || '').join('|');
|
||
},
|
||
set(value) {
|
||
// 允许直接输入正则表达式,当用户按下Enter键或失焦时再处理
|
||
// 这里我们创建一个单一的正则表达式对象,而不是拆分
|
||
this.formData.episode_patterns = [{
|
||
regex: value.trim()
|
||
}];
|
||
}
|
||
},
|
||
filteredHistoryRecords() {
|
||
// 直接返回服务器端已筛选的数据
|
||
if (!this.history.records || this.history.records.length === 0) {
|
||
return [];
|
||
}
|
||
return this.history.records;
|
||
},
|
||
historyTasks() {
|
||
// 如果已经通过API加载了所有任务名称,则使用它
|
||
if (this.allTaskNames && this.allTaskNames.length > 0) {
|
||
return this.allTaskNames;
|
||
}
|
||
|
||
// 否则,从当前页的历史记录中提取唯一的任务名称(后备方案)
|
||
if (!this.history.records || this.history.records.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const taskNames = new Set();
|
||
this.history.records.forEach(record => {
|
||
if (record.task_name) {
|
||
taskNames.add(record.task_name);
|
||
}
|
||
});
|
||
|
||
return this.sortTaskNamesByPinyin([...taskNames]);
|
||
},
|
||
taskNames() {
|
||
// 从任务列表中提取唯一的任务名称
|
||
if (!this.formData.tasklist || this.formData.tasklist.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const taskNames = new Set();
|
||
this.formData.tasklist.forEach(task => {
|
||
if (task.taskname) {
|
||
taskNames.add(task.taskname);
|
||
}
|
||
});
|
||
|
||
return this.sortTaskNamesByPinyin([...taskNames]);
|
||
},
|
||
totalPages() {
|
||
// 直接使用后端返回的total_pages
|
||
if (this.history.pagination && this.history.pagination.total_pages) {
|
||
return parseInt(this.history.pagination.total_pages);
|
||
}
|
||
|
||
// 后备方案
|
||
return 1;
|
||
},
|
||
displayedPages() {
|
||
// 显示有限数量的页码,最多显示5个页码
|
||
const pageCount = this.totalPages;
|
||
const currentPage = this.historyParams.page;
|
||
|
||
if (pageCount <= 5) {
|
||
// 页数少于等于5,显示所有页码
|
||
return Array.from({ length: pageCount }, (_, i) => i + 1);
|
||
} else {
|
||
// 页数多于5,显示当前页附近的页码
|
||
const pages = [];
|
||
let startPage = Math.max(currentPage - 2, 1);
|
||
let endPage = Math.min(startPage + 4, pageCount);
|
||
|
||
if (endPage - startPage < 4) {
|
||
startPage = Math.max(endPage - 4, 1);
|
||
}
|
||
|
||
for (let i = startPage; i <= endPage; i++) {
|
||
pages.push(i);
|
||
}
|
||
|
||
return pages;
|
||
}
|
||
}
|
||
},
|
||
filters: {
|
||
ts2date: function (value) {
|
||
const date = new Date(value);
|
||
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||
},
|
||
size: function (value) {
|
||
if (!value) return "";
|
||
const unitArr = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||
const srcsize = parseFloat(value);
|
||
const index = srcsize ? Math.floor(Math.log(srcsize) / Math.log(1024)) : 0;
|
||
const size = (srcsize / Math.pow(1024, index)).toFixed(1).replace(/\.?0+$/, "");
|
||
return size + " " + unitArr[index];
|
||
}
|
||
},
|
||
watch: {
|
||
formData: {
|
||
handler(newVal, oldVal) {
|
||
this.configModified = true;
|
||
},
|
||
deep: true
|
||
},
|
||
historyNameFilter: {
|
||
handler(newVal, oldVal) {
|
||
// 延迟加载,避免频繁请求
|
||
if (this._filterTimeout) {
|
||
clearTimeout(this._filterTimeout);
|
||
}
|
||
this._filterTimeout = setTimeout(() => {
|
||
this.loadHistoryRecords();
|
||
}, 500);
|
||
}
|
||
},
|
||
historyTaskSelected: {
|
||
handler(newVal, oldVal) {
|
||
// 选择任务筛选条件后立即加载数据
|
||
this.loadHistoryRecords();
|
||
}
|
||
},
|
||
activeTab(newValue, oldValue) {
|
||
// 如果切换到文件整理页面,则加载文件列表
|
||
if (newValue === 'filemanager') {
|
||
this.fetchAccountsDetail();
|
||
this.loadFileListWithoutLoading(this.fileManager.currentFolder);
|
||
}
|
||
},
|
||
'fileManager.pattern': {
|
||
handler(newValue, oldValue) {
|
||
// 自动检测并切换命名模式
|
||
this.detectFileManagerNamingMode();
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
this.fetchData();
|
||
this.checkNewVersion();
|
||
this.fetchUserInfo(); // 获取用户信息
|
||
this.fetchAccountsDetail(); // 获取账号详细信息
|
||
|
||
// 迁移旧的localStorage数据到新格式(为每个账号单独存储目录)
|
||
this.migrateFileManagerFolderData();
|
||
|
||
// 添加点击事件监听
|
||
document.addEventListener('click', this.handleOutsideClick);
|
||
document.addEventListener('click', this.handleModalOutsideClick);
|
||
document.addEventListener('click', this.handleFileManagerOutsideClick);
|
||
|
||
// 添加模态框关闭事件监听
|
||
$('#fileSelectModal').on('hidden.bs.modal', () => {
|
||
this.fileSelect.selectedFiles = [];
|
||
this.fileSelect.lastSelectedFileIndex = -1;
|
||
// 重置移动模式相关参数
|
||
this.fileSelect.moveMode = false;
|
||
this.fileSelect.moveFileIds = [];
|
||
});
|
||
|
||
// 检查本地存储中的标签页状态
|
||
const savedTab = localStorage.getItem('quarkAutoSave_activeTab');
|
||
if (savedTab) {
|
||
this.activeTab = savedTab;
|
||
}
|
||
|
||
// 从本地存储中恢复侧边栏折叠状态
|
||
const savedSidebarState = localStorage.getItem('quarkAutoSave_sidebarCollapsed');
|
||
if (savedSidebarState) {
|
||
this.sidebarCollapsed = savedSidebarState === 'true';
|
||
}
|
||
|
||
// 从本地存储中恢复用户设置的每页记录数
|
||
const savedPageSize = localStorage.getItem('quarkAutoSave_pageSize');
|
||
if (savedPageSize) {
|
||
this.historyParams.page_size = savedPageSize === 'all' ? 99999 : parseInt(savedPageSize);
|
||
}
|
||
|
||
// 从本地存储中恢复文件管理器的每页显示数
|
||
const savedFileManagerPageSize = localStorage.getItem('quarkAutoSave_fileManagerPageSize');
|
||
if (savedFileManagerPageSize) {
|
||
this.fileManager.pageSize = savedFileManagerPageSize === 'all' ? 99999 : parseInt(savedFileManagerPageSize);
|
||
}
|
||
|
||
// 从本地存储中恢复页面宽度设置
|
||
const savedPageWidthMode = localStorage.getItem('quarkAutoSave_pageWidthMode');
|
||
if (savedPageWidthMode) {
|
||
this.pageWidthMode = savedPageWidthMode;
|
||
document.body.classList.add('page-width-' + this.pageWidthMode);
|
||
} else {
|
||
// 默认使用中等宽度
|
||
document.body.classList.add('page-width-medium');
|
||
}
|
||
|
||
$('[data-toggle="tooltip"]').tooltip();
|
||
document.addEventListener('keydown', this.handleKeyDown);
|
||
document.addEventListener('click', (e) => {
|
||
// 如果点击的是输入框、搜索按钮或下拉菜单本身,不关闭下拉菜单
|
||
if (e.target.closest('.input-group input') ||
|
||
e.target.closest('.btn-primary[type="button"]') ||
|
||
e.target.closest('.dropdown-menu.task-suggestions') ||
|
||
e.target.closest('.bi-search')) {
|
||
return;
|
||
}
|
||
// 只隐藏下拉菜单,不清空搜索结果,这样点击同一任务的输入框时还能看到之前的搜索结果
|
||
this.smart_param.showSuggestions = false;
|
||
});
|
||
|
||
// 添加点击事件监听器,用于在点击表格外区域时取消选择记录
|
||
document.addEventListener('click', this.handleOutsideClick);
|
||
|
||
// 添加点击事件监听器,用于在点击模态框表格外区域时取消选择文件
|
||
document.addEventListener('click', this.handleModalOutsideClick);
|
||
|
||
// 添加模态框关闭事件监听,清空选中文件列表
|
||
$('#fileSelectModal').on('hidden.bs.modal', () => {
|
||
this.fileSelect.selectedFiles = [];
|
||
this.fileSelect.lastSelectedFileIndex = -1;
|
||
// 重置移动模式相关参数
|
||
this.fileSelect.moveMode = false;
|
||
this.fileSelect.moveFileIds = [];
|
||
});
|
||
|
||
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
||
|
||
// 监听模态框显示事件,检查滚动条状态
|
||
$('#fileSelectModal').on('shown.bs.modal', this.checkPreviewScrollbar);
|
||
// 监听窗口大小改变,重新检查滚动条状态
|
||
window.addEventListener('resize', this.checkPreviewScrollbar);
|
||
|
||
// 初始化时检查所有任务的命名模式
|
||
setTimeout(() => {
|
||
if (this.formData.tasklist && this.formData.tasklist.length > 0) {
|
||
this.formData.tasklist.forEach(task => {
|
||
// 检查现有的顺序命名设置
|
||
if (task.use_sequence_naming && task.sequence_naming) {
|
||
// 已经设置过顺序命名的,将顺序命名模式转换为匹配表达式
|
||
if (!task.pattern || task._pattern_backup) {
|
||
task.pattern = task.sequence_naming;
|
||
}
|
||
} else if (task.use_episode_naming && task.episode_naming) {
|
||
// 已经设置过剧集命名的,将剧集命名模式转换为匹配表达式
|
||
if (!task.pattern || task._pattern_backup) {
|
||
task.pattern = task.episode_naming;
|
||
}
|
||
} else {
|
||
// 检测是否包含顺序命名或剧集命名模式
|
||
this.detectNamingMode(task);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 如果没有剧集识别模式,添加默认模式
|
||
if (!this.formData.episode_patterns || this.formData.episode_patterns.length === 0) {
|
||
this.formData.episode_patterns = [
|
||
{ regex: '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?' }
|
||
];
|
||
}
|
||
|
||
// 如果当前标签是历史记录,则加载历史记录
|
||
if (this.activeTab === 'history') {
|
||
this.loadHistoryRecords();
|
||
// 加载所有任务名称用于筛选
|
||
this.loadAllTaskNames();
|
||
}
|
||
|
||
// 如果当前标签是文件整理,则加载文件列表
|
||
if (this.activeTab === 'filemanager') {
|
||
this.fetchAccountsDetail();
|
||
this.loadFileListWithoutLoading(this.fileManager.currentFolder);
|
||
}
|
||
|
||
// 添加对history.pagination的监听
|
||
this.$watch('history.pagination', function(newVal) {
|
||
if (newVal && newVal.total_pages) {
|
||
this.$nextTick(() => {
|
||
// 强制Vue更新视图
|
||
this.$forceUpdate();
|
||
});
|
||
}
|
||
}, { deep: true });
|
||
|
||
// 检查分享链接状态
|
||
this.checkShareUrlStatus();
|
||
}, 500);
|
||
},
|
||
beforeDestroy() {
|
||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||
// 移除点击事件监听器
|
||
document.removeEventListener('click', this.handleOutsideClick);
|
||
},
|
||
methods: {
|
||
// 获取文件图标类名
|
||
getFileIconClass(fileName, isDir = false) {
|
||
return getFileIconClass(fileName, isDir);
|
||
},
|
||
// 拼音排序辅助函数
|
||
sortTaskNamesByPinyin(taskNames) {
|
||
return taskNames.sort((a, b) => {
|
||
const aKey = pinyinPro.pinyin(a, { toneType: 'none', type: 'string' }).toLowerCase();
|
||
const bKey = pinyinPro.pinyin(b, { toneType: 'none', type: 'string' }).toLowerCase();
|
||
return aKey > bKey ? 1 : -1;
|
||
});
|
||
},
|
||
// 添加格式化分享链接警告信息的方法
|
||
formatShareUrlBanMessage(message) {
|
||
if (!message) return message;
|
||
|
||
if (message.includes("分享者用户封禁链接查看受限") ||
|
||
message.includes("文件涉及违规内容") ||
|
||
message.includes("分享地址已失效")) {
|
||
return "该分享已失效,不可访问";
|
||
} else if (message.includes("好友已取消了分享")) {
|
||
return "该分享已被取消,无法访问";
|
||
} else if (message.includes("文件已被分享者删除") || message === "文件已被分享者删除或文件夹为空") {
|
||
return "该分享已被删除,无法访问";
|
||
}
|
||
|
||
return message;
|
||
},
|
||
|
||
// 获取插件配置的占位符文本
|
||
getPluginConfigPlaceholder(pluginName, key) {
|
||
if (pluginName === 'plex' && key === 'quark_root_path') {
|
||
return '输入夸克根目录相对于 Plex 媒体库目录的路径,多个路径用逗号分隔';
|
||
} else if (pluginName === 'alist' && key === 'storage_id') {
|
||
return '输入 AList 服务器夸克存储的 ID,多个 ID 用逗号分隔';
|
||
}
|
||
return '';
|
||
},
|
||
|
||
// 获取插件配置的帮助文本
|
||
getPluginConfigHelp(pluginName, key) {
|
||
if (pluginName === 'plex' && key === 'quark_root_path') {
|
||
return '多账号支持:多个路径用逗号分隔,顺序与Cookie顺序对应,如:/path1, /path2';
|
||
} else if (pluginName === 'alist' && key === 'storage_id') {
|
||
return '多账号支持:多个存储ID用逗号分隔,顺序与Cookie顺序对应,如:1, 2, 3';
|
||
}
|
||
return '';
|
||
},
|
||
|
||
fetchUserInfo() {
|
||
// 获取所有cookie对应的用户信息
|
||
axios.get('/get_user_info')
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.userInfoList = response.data.data;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取用户信息失败:', error);
|
||
});
|
||
},
|
||
|
||
fetchAccountsDetail() {
|
||
// 获取所有账号的详细信息,包括空间使用情况
|
||
axios.get('/get_accounts_detail')
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.accountsDetail = response.data.data;
|
||
// 验证当前选中的账号索引是否有效
|
||
if (this.fileManager.selectedAccountIndex >= this.accountsDetail.length) {
|
||
this.fileManager.selectedAccountIndex = 0;
|
||
localStorage.setItem('quarkAutoSave_selectedAccountIndex', '0');
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取账号详细信息失败:', error);
|
||
});
|
||
},
|
||
|
||
// 迁移旧的localStorage数据到新格式(为每个账号单独存储目录)
|
||
migrateFileManagerFolderData() {
|
||
const oldFolderKey = 'quarkAutoSave_fileManagerLastFolder';
|
||
const oldFolderValue = localStorage.getItem(oldFolderKey);
|
||
|
||
if (oldFolderValue) {
|
||
// 如果存在旧数据,将其迁移到当前选中账号的新格式
|
||
const currentAccountIndex = this.fileManager.selectedAccountIndex;
|
||
const newFolderKey = `quarkAutoSave_fileManagerLastFolder_${currentAccountIndex}`;
|
||
|
||
// 只有当新格式的数据不存在时才进行迁移
|
||
if (!localStorage.getItem(newFolderKey)) {
|
||
localStorage.setItem(newFolderKey, oldFolderValue);
|
||
console.log(`已将文件管理器目录数据迁移到账号 ${currentAccountIndex}: ${oldFolderValue}`);
|
||
}
|
||
|
||
// 删除旧的数据
|
||
localStorage.removeItem(oldFolderKey);
|
||
}
|
||
},
|
||
|
||
onAccountChange() {
|
||
// 保存选中的账号索引到localStorage
|
||
localStorage.setItem('quarkAutoSave_selectedAccountIndex', this.fileManager.selectedAccountIndex.toString());
|
||
|
||
// 获取新账号最后一次访问的目录
|
||
const newAccountLastFolder = localStorage.getItem(`quarkAutoSave_fileManagerLastFolder_${this.fileManager.selectedAccountIndex}`) || 'root';
|
||
|
||
// 重置页码并切换到新账号的最后访问目录
|
||
this.fileManager.currentPage = 1;
|
||
this.loadFileListWithFallback(newAccountLastFolder);
|
||
|
||
// 如果移动文件模态框正在显示,需要重新加载目录
|
||
if ($('#fileSelectModal').hasClass('show')) {
|
||
const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
|
||
if (modalType === 'move') {
|
||
// 重置路径并重新加载根目录
|
||
this.fileSelect.paths = [];
|
||
this.getSavepathDetail(0);
|
||
}
|
||
}
|
||
},
|
||
|
||
refreshCurrentFolderCache(retryCount = 0) {
|
||
// 刷新当前目录的缓存,强制重新请求最新的文件列表
|
||
// 调用后端接口,添加强制刷新参数
|
||
const params = {
|
||
folder_id: this.fileManager.currentFolder || 'root',
|
||
sort_by: this.fileManager.sortBy,
|
||
order: this.fileManager.sortOrder,
|
||
page_size: this.fileManager.pageSize,
|
||
page: this.fileManager.currentPage,
|
||
account_index: this.fileManager.selectedAccountIndex,
|
||
force_refresh: true, // 强制刷新参数
|
||
timestamp: Date.now() // 添加时间戳避免缓存
|
||
};
|
||
|
||
axios.get('/file_list', { params })
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.fileManager.fileList = response.data.data.list;
|
||
this.fileManager.total = response.data.data.total;
|
||
this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
|
||
this.fileManager.paths = response.data.data.paths || [];
|
||
this.fileManager.gotoPage = this.fileManager.currentPage;
|
||
// 移除成功通知
|
||
|
||
// 检测当前的命名模式
|
||
this.detectFileManagerNamingMode();
|
||
} else {
|
||
// 如果刷新失败且重试次数少于2次,则重试
|
||
if (retryCount < 2) {
|
||
setTimeout(() => {
|
||
this.refreshCurrentFolderCache(retryCount + 1);
|
||
}, 1000);
|
||
} else {
|
||
this.showToast('刷新失败:' + response.data.message);
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('刷新缓存失败:', error);
|
||
// 如果网络错误且重试次数少于2次,则重试
|
||
if (retryCount < 2) {
|
||
setTimeout(() => {
|
||
this.refreshCurrentFolderCache(retryCount + 1);
|
||
}, 1000);
|
||
} else {
|
||
this.showToast('刷新缓存失败,请稍后重试');
|
||
}
|
||
});
|
||
},
|
||
// 获取指定文件夹的上级目录ID
|
||
getParentFolderId(folderId) {
|
||
// 如果是根目录,没有上级目录
|
||
if (folderId === 'root' || folderId === '0') {
|
||
return null;
|
||
}
|
||
|
||
// 如果是当前目录(源目录),从fileManager.paths中获取上级目录
|
||
if (folderId === this.fileManager.currentFolder || folderId === (this.fileManager.currentFolder || "0")) {
|
||
if (this.fileManager.paths.length === 0) {
|
||
// 当前目录是根目录的直接子目录,上级是根目录
|
||
return 'root';
|
||
} else {
|
||
// 当前目录的上级是paths中的最后一个目录
|
||
return this.fileManager.paths[this.fileManager.paths.length - 1].fid;
|
||
}
|
||
}
|
||
|
||
// 如果是目标目录,从fileSelect.paths中获取上级目录
|
||
if (this.fileSelect.paths && this.fileSelect.paths.length > 0) {
|
||
// 目标目录就是fileSelect.paths的最后一个目录,其上级目录是倒数第二个
|
||
if (this.fileSelect.paths.length === 1) {
|
||
// 目标目录是根目录的直接子目录
|
||
return 'root';
|
||
} else {
|
||
// 目标目录的上级是paths中的倒数第二个目录
|
||
return this.fileSelect.paths[this.fileSelect.paths.length - 2].fid;
|
||
}
|
||
} else {
|
||
// 如果fileSelect.paths为空,说明目标目录是根目录,没有上级
|
||
return null;
|
||
}
|
||
},
|
||
// 刷新指定文件夹的缓存
|
||
refreshFolderCache(folderId) {
|
||
// 刷新指定目录的缓存,强制重新请求最新的文件列表
|
||
// 调用后端接口,添加强制刷新参数
|
||
const params = {
|
||
folder_id: folderId || 'root',
|
||
sort_by: this.fileManager.sortBy,
|
||
order: this.fileManager.sortOrder,
|
||
page_size: this.fileManager.pageSize,
|
||
page: 1, // 使用第一页来刷新缓存
|
||
account_index: this.fileManager.selectedAccountIndex,
|
||
force_refresh: true // 强制刷新参数
|
||
};
|
||
|
||
axios.get('/file_list', { params })
|
||
.then(response => {
|
||
// 如果刷新的是当前显示的文件夹,则更新显示
|
||
if (folderId === this.fileManager.currentFolder) {
|
||
if (response.data.success) {
|
||
this.fileManager.fileList = response.data.data.list;
|
||
this.fileManager.total = response.data.data.total;
|
||
this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
|
||
this.fileManager.paths = response.data.data.paths || [];
|
||
this.fileManager.gotoPage = this.fileManager.currentPage;
|
||
|
||
// 检测当前的命名模式
|
||
this.detectFileManagerNamingMode();
|
||
} else {
|
||
this.showToast('刷新失败:' + response.data.message);
|
||
}
|
||
}
|
||
// 如果不是当前文件夹,只是刷新缓存,不更新显示
|
||
})
|
||
.catch(error => {
|
||
console.error('刷新文件夹缓存失败:', error);
|
||
// 只有在刷新当前文件夹时才显示错误提示
|
||
if (folderId === this.fileManager.currentFolder) {
|
||
this.showToast('刷新缓存失败,请稍后重试');
|
||
}
|
||
});
|
||
},
|
||
// 新建文件夹
|
||
createNewFolder() {
|
||
// 检查是否有文件正在编辑状态
|
||
const editingFile = this.fileManager.fileList.find(file => file._editing);
|
||
if (editingFile) {
|
||
// 如果有其他文件正在编辑,先保存编辑状态
|
||
this.saveRenameFile(editingFile);
|
||
return;
|
||
}
|
||
|
||
// 生成带时间戳的文件夹名称
|
||
const timestamp = new Date().toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
}).replace(/[\/\s:]/g, '');
|
||
const folderName = `新建文件夹 ${timestamp}`;
|
||
|
||
// 调用后端API创建文件夹
|
||
axios.post('/create_folder', {
|
||
parent_folder_id: this.fileManager.currentFolder || '0',
|
||
folder_name: folderName,
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
// 创建成功,将新文件夹添加到文件列表
|
||
const newFolder = response.data.data;
|
||
|
||
// 设置编辑状态
|
||
this.$set(newFolder, '_editing', true);
|
||
this.$set(newFolder, '_editingName', newFolder.file_name);
|
||
|
||
// 将新文件夹添加到列表开头(文件夹通常显示在前面)
|
||
this.fileManager.fileList.unshift(newFolder);
|
||
this.fileManager.total += 1;
|
||
|
||
// 下一个tick后聚焦输入框并全选文本
|
||
this.$nextTick(() => {
|
||
const input = this.$refs['renameInput_' + newFolder.fid];
|
||
if (input) {
|
||
input.focus();
|
||
input.select();
|
||
}
|
||
});
|
||
|
||
this.showToast('文件夹创建成功');
|
||
} else {
|
||
this.showToast(response.data.message || '创建文件夹失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('创建文件夹失败:', error);
|
||
this.showToast('创建文件夹失败');
|
||
});
|
||
},
|
||
// 添加一个检查分享链接状态的方法
|
||
checkShareUrlStatus() {
|
||
// 只在任务列表页面检查
|
||
if (this.activeTab !== 'tasklist') return;
|
||
|
||
// 遍历所有任务
|
||
this.formData.tasklist.forEach((task, index) => {
|
||
// 如果任务有分享链接且没有设置shareurl_ban
|
||
if (task.shareurl && !task.shareurl_ban) {
|
||
// 检查分享链接
|
||
axios.get('/get_share_detail', { params: { shareurl: task.shareurl } })
|
||
.then(response => {
|
||
const share_detail = response.data.data;
|
||
if (!response.data.success) {
|
||
// 使用格式化函数处理错误信息
|
||
this.$set(task, "shareurl_ban", this.formatShareUrlBanMessage(share_detail.error));
|
||
} else if (share_detail.list !== undefined && share_detail.list.length === 0) {
|
||
// 检查文件列表是否为空,确保列表存在且为空
|
||
this.$set(task, "shareurl_ban", "该分享已被删除,无法访问");
|
||
}
|
||
})
|
||
.catch(error => {
|
||
// 错误处理
|
||
console.error('检查分享链接状态失败:', error);
|
||
});
|
||
}
|
||
});
|
||
},
|
||
toggleSidebar() {
|
||
this.sidebarCollapsed = !this.sidebarCollapsed;
|
||
// 保存侧边栏状态到本地存储
|
||
localStorage.setItem('quarkAutoSave_sidebarCollapsed', this.sidebarCollapsed);
|
||
},
|
||
cleanTaskNameForSearch(taskName) {
|
||
if (!taskName) return '';
|
||
|
||
// 清理任务名称中的连续空格和特殊符号
|
||
let cleanedName = taskName.replace(/\u3000/g, ' ').replace(/\t/g, ' ');
|
||
cleanedName = cleanedName.replace(/\s+/g, ' ').trim();
|
||
|
||
// 匹配常见的季数格式
|
||
const seasonPatterns = [
|
||
/^(.*?)[\s\.\-_]+S\d+$/i, // 黑镜 - S07、折腰.S01、音你而来-S02
|
||
/^(.*?)[\s\.\-_]+Season\s*\d+$/i, // 黑镜 - Season 1
|
||
/^(.*?)\s+S\d+$/i, // 快乐的大人 S02
|
||
/^(.*?)[\s\.\-_]+S\d+E\d+$/i, // 处理 S01E01 格式
|
||
/^(.*?)\s+第\s*\d+\s*季$/i, // 处理 第N季 格式
|
||
/^(.*?)[\s\.\-_]+第\s*\d+\s*季$/i, // 处理 - 第N季 格式
|
||
/^(.*?)\s+第[一二三四五六七八九十零]+季$/i, // 处理 第一季、第二季 格式
|
||
/^(.*?)[\s\.\-_]+第[一二三四五六七八九十零]+季$/i, // 处理 - 第一季、- 第二季 格式
|
||
];
|
||
|
||
for (const pattern of seasonPatterns) {
|
||
const match = cleanedName.match(pattern);
|
||
if (match) {
|
||
let showName = match[1].trim();
|
||
// 去除末尾可能残留的分隔符
|
||
showName = showName.replace(/[\s\.\-_]+$/, '');
|
||
return encodeURIComponent(showName);
|
||
}
|
||
}
|
||
|
||
// 如果没有匹配到季数格式,执行原有的清理逻辑
|
||
// 移除孤立的特殊符号
|
||
cleanedName = cleanedName.replace(/\s*[-–—_]\s*$/, '');
|
||
// 移除开头和结尾的特殊符号
|
||
cleanedName = cleanedName.trim().replace(/^[-–—_\s]+|[-–—_\s]+$/g, '');
|
||
|
||
return encodeURIComponent(cleanedName);
|
||
},
|
||
changeTab(tab) {
|
||
this.activeTab = tab;
|
||
// 在本地存储中保存当前标签页状态
|
||
localStorage.setItem('quarkAutoSave_activeTab', tab);
|
||
if (window.innerWidth <= 768) {
|
||
$('#sidebarMenu').collapse('toggle')
|
||
}
|
||
|
||
// 当切换到历史记录标签时加载数据
|
||
if (tab === 'history') {
|
||
this.loadHistoryRecords();
|
||
// 加载所有任务名称用于筛选
|
||
this.loadAllTaskNames();
|
||
}
|
||
|
||
// 当切换到文件整理标签时加载文件列表
|
||
if (tab === 'filemanager') {
|
||
// 从本地存储中恢复文件管理器的分页大小设置
|
||
const savedFileManagerPageSize = localStorage.getItem('quarkAutoSave_fileManagerPageSize');
|
||
if (savedFileManagerPageSize) {
|
||
this.fileManager.pageSize = savedFileManagerPageSize === 'all' ? 99999 : parseInt(savedFileManagerPageSize);
|
||
}
|
||
// 获取账号详细信息
|
||
this.fetchAccountsDetail();
|
||
this.loadFileListWithoutLoading(this.fileManager.currentFolder);
|
||
}
|
||
},
|
||
checkNewVersion() {
|
||
// 移除本地版本中的v前缀
|
||
const localVersionClean = this.version.replace(/^v/i, '');
|
||
|
||
// 显示时也去掉v前缀
|
||
this.versionTips = localVersionClean;
|
||
|
||
axios.get('https://api.github.com/repos/x1ao4/quark-auto-save-x/tags')
|
||
.then(response => {
|
||
const latestVersion = response.data[0].name; // GitHub的版本不带v,不需要replace
|
||
|
||
// 使用处理后的版本号进行比较
|
||
if (latestVersion !== localVersionClean) {
|
||
this.versionTips += ` <sup><span class="badge badge-pill badge-danger">${latestVersion}</span></sup>`;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
});
|
||
},
|
||
fetchData() {
|
||
axios.get('/data')
|
||
.then(response => {
|
||
config_data = response.data.data
|
||
// cookie兼容
|
||
if (typeof config_data.cookie === 'string')
|
||
config_data.cookie = [config_data.cookie];
|
||
// 添加星期预设
|
||
config_data.tasklist = config_data.tasklist.map(task => {
|
||
if (!task.hasOwnProperty('runweek')) {
|
||
task.runweek = [1, 2, 3, 4, 5, 6, 7];
|
||
}
|
||
|
||
// 格式化已有的警告信息
|
||
if (task.shareurl_ban) {
|
||
task.shareurl_ban = this.formatShareUrlBanMessage(task.shareurl_ban);
|
||
}
|
||
|
||
return task;
|
||
});
|
||
|
||
// 获取所有任务父目录
|
||
config_data.tasklist.forEach(item => {
|
||
parentDir = this.getParentDirectory(item.savepath)
|
||
if (!this.taskDirs.includes(parentDir))
|
||
this.taskDirs.push(parentDir);
|
||
});
|
||
this.newTask.addition = config_data.task_plugins_config_default;
|
||
// 确保source配置存在
|
||
if (!config_data.source) {
|
||
config_data.source = {};
|
||
}
|
||
if (!config_data.source.cloudsaver) {
|
||
config_data.source.cloudsaver = {
|
||
server: "",
|
||
username: "",
|
||
password: "",
|
||
token: ""
|
||
};
|
||
}
|
||
// 确保剧集识别模式存在
|
||
if (!config_data.episode_patterns) {
|
||
config_data.episode_patterns = [];
|
||
}
|
||
// 确保按钮显示配置存在
|
||
if (!config_data.button_display) {
|
||
config_data.button_display = {
|
||
run_task: "always",
|
||
delete_task: "always",
|
||
refresh_plex: "always",
|
||
refresh_alist: "always",
|
||
latest_update_date: "always",
|
||
latest_transfer_file: "always",
|
||
today_update_indicator: "always"
|
||
};
|
||
}
|
||
// 确保最近更新日期配置存在(向后兼容)
|
||
if (!config_data.button_display.latest_update_date) {
|
||
config_data.button_display.latest_update_date = "always";
|
||
}
|
||
// 确保最近转存文件配置存在(向后兼容)
|
||
if (!config_data.button_display.latest_transfer_file) {
|
||
config_data.button_display.latest_transfer_file = "always";
|
||
}
|
||
// 确保当日更新图标配置存在(向后兼容)
|
||
if (!config_data.button_display.today_update_indicator) {
|
||
config_data.button_display.today_update_indicator = "always";
|
||
}
|
||
// 确保文件整理性能配置存在
|
||
if (!config_data.file_performance) {
|
||
config_data.file_performance = {
|
||
api_page_size: 200,
|
||
cache_expire_time: 30
|
||
};
|
||
} else {
|
||
// 确保必要的字段存在,移除废弃的字段
|
||
if (!config_data.file_performance.api_page_size) {
|
||
config_data.file_performance.api_page_size = 200;
|
||
}
|
||
if (!config_data.file_performance.cache_expire_time) {
|
||
config_data.file_performance.cache_expire_time = 30;
|
||
}
|
||
// 移除废弃的字段
|
||
delete config_data.file_performance.large_page_size;
|
||
delete config_data.file_performance.cache_cleanup_interval;
|
||
}
|
||
this.formData = config_data;
|
||
setTimeout(() => {
|
||
this.configModified = false;
|
||
}, 100);
|
||
|
||
// 加载任务最新信息(包括记录和文件)
|
||
this.loadTaskLatestInfo();
|
||
|
||
// 数据加载完成后检查分享链接状态
|
||
if (this.activeTab === 'tasklist') {
|
||
setTimeout(() => {
|
||
this.checkShareUrlStatus();
|
||
}, 300);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
// 错误处理
|
||
});
|
||
},
|
||
handleKeyDown(event) {
|
||
if (event.ctrlKey || event.metaKey) {
|
||
if (event.keyCode === 83 || event.key === 's') {
|
||
event.preventDefault();
|
||
this.saveConfig();
|
||
} else if (event.keyCode === 82 || event.key === 'r') {
|
||
event.preventDefault();
|
||
this.runScriptNow();
|
||
}
|
||
}
|
||
},
|
||
handleBeforeUnload(e) {
|
||
if (this.configModified) {
|
||
e.preventDefault();
|
||
e.returnValue = '配置已修改但未保存,确定要离开吗?';
|
||
return e.returnValue;
|
||
}
|
||
},
|
||
saveConfig() {
|
||
// 保存前处理每个任务的命名模式
|
||
if (this.formData.tasklist && this.formData.tasklist.length > 0) {
|
||
this.formData.tasklist.forEach(task => {
|
||
// 如果是顺序命名模式,确保sequence_naming字段已正确设置
|
||
if (task.use_sequence_naming && task.pattern && task.pattern.includes('{}')) {
|
||
task.sequence_naming = task.pattern;
|
||
}
|
||
// 如果是剧集命名模式,确保episode_naming字段已正确设置
|
||
if (task.use_episode_naming && task.pattern) {
|
||
if (task.pattern === "[]" || task.pattern.includes('[]')) {
|
||
task.episode_naming = task.pattern;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
axios.post('/update', this.formData)
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.configModified = false;
|
||
// 使用Toast通知替代alert
|
||
this.showToast(response.data.message);
|
||
// 保存成功后更新用户信息
|
||
this.fetchUserInfo();
|
||
} else {
|
||
// 错误信息仍然使用alert,确保用户看到
|
||
alert(response.data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
// 错误处理
|
||
alert("保存失败: " + (error.response?.data?.message || error.message || "未知错误"));
|
||
});
|
||
},
|
||
addCookie() {
|
||
this.formData.cookie.push("");
|
||
// 添加cookie后,添加一个空的用户信息占位
|
||
this.userInfoList.push({
|
||
index: this.userInfoList.length,
|
||
nickname: "",
|
||
is_active: false
|
||
});
|
||
},
|
||
removeCookie(index) {
|
||
if (this.formData.cookie[index] == "" || confirm("确定要删除吗?")) {
|
||
this.formData.cookie.splice(index, 1);
|
||
// 删除对应的用户信息
|
||
if (this.userInfoList.length > index) {
|
||
this.userInfoList.splice(index, 1);
|
||
// 更新剩余用户信息的索引
|
||
this.userInfoList.forEach((user, idx) => {
|
||
user.index = idx;
|
||
});
|
||
}
|
||
}
|
||
},
|
||
addPush() {
|
||
key = prompt("增加的键名", "");
|
||
if (key != "" && key != null)
|
||
this.$set(this.formData.push_config, key, "");
|
||
},
|
||
removePush(key) {
|
||
if (confirm("确定要删除吗?"))
|
||
this.$delete(this.formData.push_config, key);
|
||
},
|
||
addTask() {
|
||
if (!this.formData.tasklist)
|
||
this.formData.tasklist = [];
|
||
let newTask = { ...this.newTask };
|
||
|
||
// 如果有上一个任务,继承保存路径和命名规则
|
||
if (this.formData.tasklist.length > 0) {
|
||
const lastTask = this.formData.tasklist[this.formData.tasklist.length - 1];
|
||
newTask.savepath = lastTask.savepath || "";
|
||
newTask.pattern = lastTask.pattern || "";
|
||
|
||
// 继承命名规则选择模式
|
||
newTask.use_sequence_naming = lastTask.use_sequence_naming || false;
|
||
newTask.use_episode_naming = lastTask.use_episode_naming || false;
|
||
newTask.sequence_naming = lastTask.sequence_naming || "";
|
||
newTask.episode_naming = lastTask.episode_naming || "";
|
||
newTask.replace = lastTask.replace || "";
|
||
}
|
||
|
||
this.formData.tasklist.push(newTask)
|
||
const index = this.formData.tasklist.length - 1;
|
||
|
||
// 清除之前任务的搜索记录,避免影响新任务
|
||
this.smart_param.taskSuggestions = {
|
||
success: false,
|
||
data: []
|
||
};
|
||
|
||
// 等Vue更新DOM后,自动展开新添加的任务
|
||
this.$nextTick(() => {
|
||
$(`#collapse_${index}`).collapse('show');
|
||
// 滚动到底部
|
||
this.scrollToX();
|
||
});
|
||
},
|
||
focusTaskname(index, task) {
|
||
// 如果当前聚焦的任务不同于上一个任务,清除搜索记录
|
||
if (this.smart_param.index !== index) {
|
||
this.smart_param.taskSuggestions = {
|
||
success: false,
|
||
data: []
|
||
};
|
||
}
|
||
|
||
this.smart_param.index = index
|
||
this.smart_param.origin_savepath = task.savepath
|
||
regex = new RegExp(`/${task.taskname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(/|$)`)
|
||
if (task.savepath.includes('TASKNAME')) {
|
||
this.smart_param.savepath = task.savepath;
|
||
} else if (task.savepath.match(regex)) {
|
||
this.smart_param.savepath = task.savepath.replace(task.taskname, 'TASKNAME');
|
||
} else {
|
||
this.smart_param.savepath = undefined;
|
||
}
|
||
},
|
||
// Sxx季数信息同步工具函数
|
||
syncSeasonNumber(sourceValue, targetValue) {
|
||
if (!sourceValue || !targetValue) return targetValue;
|
||
|
||
// 从源字符串中提取Sxx格式的季数
|
||
const sourceMatch = sourceValue.match(/S(\d+)/i);
|
||
if (!sourceMatch) return targetValue; // 源字符串没有Sxx,不同步
|
||
|
||
const sourceSeasonNumber = sourceMatch[1];
|
||
|
||
// 检查目标字符串是否包含Sxx格式
|
||
const targetMatch = targetValue.match(/S(\d+)/i);
|
||
if (!targetMatch) return targetValue; // 目标字符串没有Sxx,不同步
|
||
|
||
// 替换目标字符串中的季数
|
||
return targetValue.replace(/S\d+/i, 'S' + sourceSeasonNumber.padStart(2, '0'));
|
||
},
|
||
|
||
// 保存路径变化时的处理函数
|
||
onSavepathChange(index, task) {
|
||
// 同步Sxx到命名规则
|
||
if (task.pattern) {
|
||
task.pattern = this.syncSeasonNumber(task.savepath, task.pattern);
|
||
|
||
// 同步到相关的命名规则字段
|
||
if (task.use_sequence_naming && task.sequence_naming) {
|
||
task.sequence_naming = this.syncSeasonNumber(task.savepath, task.sequence_naming);
|
||
}
|
||
if (task.use_episode_naming && task.episode_naming) {
|
||
task.episode_naming = this.syncSeasonNumber(task.savepath, task.episode_naming);
|
||
}
|
||
}
|
||
},
|
||
|
||
// 命名规则变化时的处理函数
|
||
onPatternChange(index, task) {
|
||
// 同步Sxx到保存路径
|
||
if (task.savepath) {
|
||
task.savepath = this.syncSeasonNumber(task.pattern, task.savepath);
|
||
}
|
||
},
|
||
|
||
changeTaskname(index, task) {
|
||
if (this.smart_param.searchTimer) {
|
||
clearTimeout(this.smart_param.searchTimer);
|
||
}
|
||
this.smart_param.searchTimer = setTimeout(() => {
|
||
this.searchSuggestions(index, task.taskname, 0);
|
||
}, 1000);
|
||
|
||
// 添加防抖机制,避免频繁的中间状态触发同步
|
||
if (this.smart_param.syncTimer) {
|
||
clearTimeout(this.smart_param.syncTimer);
|
||
}
|
||
this.smart_param.syncTimer = setTimeout(() => {
|
||
this.performTasknameSync(index, task);
|
||
}, 300); // 300ms 防抖延迟
|
||
},
|
||
|
||
performTasknameSync(index, task) {
|
||
// 重新计算智能路径参数
|
||
this.smart_param.index = index;
|
||
this.smart_param.origin_savepath = task.savepath;
|
||
const regex = new RegExp(`/${task.taskname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(/|$)`);
|
||
if (task.savepath && task.savepath.includes('TASKNAME')) {
|
||
this.smart_param.savepath = task.savepath;
|
||
} else if (task.savepath && task.savepath.match(regex)) {
|
||
this.smart_param.savepath = task.savepath.replace(task.taskname, 'TASKNAME');
|
||
} else {
|
||
this.smart_param.savepath = undefined;
|
||
}
|
||
|
||
// 清理任务名称中的连续空格和特殊符号
|
||
const cleanTaskName = task.taskname.replace(/\s+/g, ' ').trim();
|
||
|
||
// 检测是否是不完整的季数格式(如 S0, S, - S 等),如果是则不处理
|
||
const incompleteSeasonPattern = /[\s\.\-_]+S\d{0,1}$/i;
|
||
if (incompleteSeasonPattern.test(cleanTaskName)) {
|
||
return; // 不处理不完整的季数格式
|
||
}
|
||
|
||
// 提取任务名称中的分隔符格式
|
||
// 使用与cleanTaskNameForSearch相同的季数格式匹配模式
|
||
let showName, seasonNumber, nameSeparator = ' - '; // 默认分隔符
|
||
let isTVShow = false; // 标记是否为电视节目
|
||
let separatorMatch = null;
|
||
|
||
// 匹配常见的季数格式
|
||
const seasonPatterns = [
|
||
/^(.*?)([\s\.\-_]+)S(\d+)$/i, // 黑镜 - S07、折腰.S01、音你而来-S02
|
||
/^(.*?)([\s\.\-_]+)Season\s*(\d+)$/i, // 黑镜 - Season 1
|
||
/^(.*?)(\s+)S(\d+)$/i, // 快乐的大人 S02
|
||
/^(.*?)([\s\.\-_]+)S(\d+)E\d+$/i, // 处理 S01E01 格式
|
||
/^(.*?)(\s+)第\s*(\d+)\s*季$/i, // 处理 第N季 格式
|
||
/^(.*?)([\s\.\-_]+)第\s*(\d+)\s*季$/i, // 处理 - 第N季 格式
|
||
/^(.*?)(\s+)第([一二三四五六七八九十零]+)季$/i, // 处理 第一季、第二季 格式
|
||
/^(.*?)([\s\.\-_]+)第([一二三四五六七八九十零]+)季$/i, // 处理 - 第一季、- 第二季 格式
|
||
];
|
||
|
||
// 尝试匹配各种格式
|
||
for (const pattern of seasonPatterns) {
|
||
const match = cleanTaskName.match(pattern);
|
||
if (match) {
|
||
separatorMatch = match;
|
||
// 根据不同的格式,确定季序号的位置
|
||
if (match[3] && match[3].match(/[一二三四五六七八九十百千万零两]/)) {
|
||
// 将中文数字转换为阿拉伯数字
|
||
const arabicNumber = chineseToArabic(match[3]);
|
||
seasonNumber = arabicNumber !== null ? String(arabicNumber) : '01';
|
||
} else {
|
||
seasonNumber = match[3] || '01';
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 判断保存路径是否包含电视节目相关的关键词
|
||
if (task.savepath) {
|
||
const tvKeywords = ["电视", "节目", "剧", "动漫", "动画", "番", "综艺", "真人秀", "TV", "Tv", "tv", "Series", "series", "Show", "show"];
|
||
isTVShow = tvKeywords.some(keyword => task.savepath.includes(keyword));
|
||
}
|
||
|
||
// 当任务名称包含季信息时,也按电视节目处理
|
||
const hasSeason = separatorMatch !== null;
|
||
if (hasSeason) {
|
||
isTVShow = true;
|
||
}
|
||
|
||
if (separatorMatch) {
|
||
// 提取剧名(去除末尾空格和特殊符号)
|
||
showName = separatorMatch[1].trim().replace(/[\s\.\-_]+$/, '');
|
||
// 季序号已在上面的循环中提取
|
||
// 提取实际使用的分隔符
|
||
const rawSeparator = separatorMatch[2];
|
||
|
||
// 处理特殊情况:如果是"黑镜 - 第五季"这种格式,需要保留完整的" - "分隔符
|
||
// 检查原始字符串中剧名后面的分隔符
|
||
const fullSeparator = cleanTaskName.substring(showName.length, cleanTaskName.length - (separatorMatch[3] ? separatorMatch[3].length + 1 : 0)).match(/^([\s\.\-_]+)/);
|
||
|
||
if (fullSeparator && fullSeparator[1]) {
|
||
// 使用完整的分隔符
|
||
nameSeparator = fullSeparator[1];
|
||
} else {
|
||
// 规范化分隔符(如果是连续的空格,转为单个空格)
|
||
nameSeparator = rawSeparator.replace(/\s+/g, ' ');
|
||
// 如果只有一个点号,保留点号
|
||
if (rawSeparator === '.') {
|
||
nameSeparator = '.';
|
||
}
|
||
// 如果是单个空格,保留单个空格
|
||
else if (rawSeparator === ' ') {
|
||
nameSeparator = ' ';
|
||
}
|
||
}
|
||
// 其他情况保持原样
|
||
} else {
|
||
// 如果没有匹配到季序号格式,处理删除季数信息的情况
|
||
showName = cleanTaskName;
|
||
|
||
// 检查保存路径中是否已有季数信息,如果有且是电视节目类型,则保留原有季数
|
||
let preserveExistingSeason = false;
|
||
|
||
if (task.savepath && isTVShow) {
|
||
const existingSeasonMatch = task.savepath.match(/S(\d+)/i);
|
||
|
||
if (existingSeasonMatch) {
|
||
// 只有当季数是有效的(不是00或空)时才保留
|
||
const existingSeasonNum = existingSeasonMatch[1];
|
||
if (existingSeasonNum && existingSeasonNum !== '00' && existingSeasonNum !== '0') {
|
||
seasonNumber = existingSeasonNum;
|
||
preserveExistingSeason = true;
|
||
|
||
// 从保存路径中提取分隔符格式
|
||
const pathParts = task.savepath.split('/');
|
||
if (pathParts.length > 0) {
|
||
const lastPart = pathParts[pathParts.length - 1];
|
||
const separatorMatch = lastPart.match(/^(.*?)([\s\.\-_]+)S\d+/i);
|
||
if (separatorMatch) {
|
||
nameSeparator = separatorMatch[2];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果没有保留现有季数,则使用默认值
|
||
if (!preserveExistingSeason) {
|
||
seasonNumber = '01';
|
||
}
|
||
}
|
||
|
||
// 更新保存路径 - 无论是否使用智能路径,都确保倒数第二级目录更新
|
||
if (task.savepath) {
|
||
// 分割保存路径为各级目录
|
||
const pathParts = task.savepath.split('/');
|
||
|
||
if (pathParts.length >= 2) {
|
||
// 如果智能路径已设置,使用原有逻辑更新最后一级
|
||
if (this.smart_param.savepath) {
|
||
// 更新最后一级目录,但保留前面的路径结构
|
||
let replacementName;
|
||
if (isTVShow) {
|
||
// 电视节目格式:剧名 + 分隔符 + S季序号
|
||
replacementName = showName + nameSeparator + 'S' + seasonNumber.padStart(2, '0');
|
||
} else {
|
||
// 非电视节目直接使用任务名称
|
||
replacementName = cleanTaskName;
|
||
}
|
||
const newPath = this.smart_param.savepath.replace('TASKNAME', replacementName);
|
||
const newPathParts = newPath.split('/');
|
||
pathParts[pathParts.length - 1] = newPathParts[newPathParts.length - 1];
|
||
} else {
|
||
// 根据是否为电视节目决定处理方式
|
||
if (isTVShow) {
|
||
// 电视节目格式:剧名 + 分隔符 + S季序号
|
||
const newLastPart = showName + nameSeparator + 'S' + seasonNumber.padStart(2, '0');
|
||
pathParts[pathParts.length - 1] = newLastPart;
|
||
} else {
|
||
// 非电视节目直接使用任务名称
|
||
pathParts[pathParts.length - 1] = cleanTaskName;
|
||
}
|
||
}
|
||
|
||
// 处理倒数第二级目录(剧名+年份)- 无论是否使用智能路径,都更新
|
||
if (pathParts.length >= 3) {
|
||
const parentDir = pathParts[pathParts.length - 2];
|
||
// 检查倒数第二级是否符合 "名称 (年份)" 格式
|
||
const yearMatch = parentDir.match(/^(.+?)\s*[\((](\d{4})[\))]$/);
|
||
|
||
if (yearMatch) {
|
||
if (isTVShow) {
|
||
// 电视节目:更新倒数第二级为新的剧名 + 年份
|
||
const year = yearMatch[2];
|
||
pathParts[pathParts.length - 2] = showName + ' (' + year + ')';
|
||
} else {
|
||
// 非电视节目:去除倒数第二级目录
|
||
pathParts.splice(pathParts.length - 2, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新保存路径
|
||
task.savepath = pathParts.join('/');
|
||
}
|
||
}
|
||
|
||
// 额外处理:检查保存路径最末级是否包含中文季序号,如果包含则转换为标准格式
|
||
if (task.savepath) {
|
||
const pathParts = task.savepath.split('/');
|
||
if (pathParts.length > 0) {
|
||
const lastPart = pathParts[pathParts.length - 1];
|
||
// 匹配中文季序号格式
|
||
const chineseSeasonMatch = lastPart.match(/^(.*?)([\s\.\-_]+)第([一二三四五六七八九十百千万零两]+)季$/);
|
||
if (chineseSeasonMatch) {
|
||
const showName = chineseSeasonMatch[1].trim();
|
||
const separator = chineseSeasonMatch[2];
|
||
const chineseSeason = chineseSeasonMatch[3];
|
||
|
||
// 将中文数字转换为阿拉伯数字
|
||
const arabicNumber = chineseToArabic(chineseSeason);
|
||
const seasonNumber = arabicNumber !== null ? String(arabicNumber) : '1';
|
||
|
||
// 更新最末级目录为标准格式
|
||
pathParts[pathParts.length - 1] = showName + separator + 'S' + seasonNumber.padStart(2, '0');
|
||
task.savepath = pathParts.join('/');
|
||
}
|
||
|
||
// 匹配阿拉伯数字季序号格式
|
||
const arabicSeasonMatch = lastPart.match(/^(.*?)([\s\.\-_]+)第(\d+)季$/);
|
||
if (arabicSeasonMatch) {
|
||
const showName = arabicSeasonMatch[1].trim();
|
||
const separator = arabicSeasonMatch[2];
|
||
const seasonNumber = arabicSeasonMatch[3];
|
||
|
||
// 更新最末级目录为标准格式
|
||
pathParts[pathParts.length - 1] = showName + separator + 'S' + seasonNumber.padStart(2, '0');
|
||
task.savepath = pathParts.join('/');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新命名规则 - 始终使用当前任务名称中的信息
|
||
if (task.pattern) {
|
||
// 根据是否为电视节目决定处理方式
|
||
if (isTVShow) {
|
||
// 处理剧集命名模式
|
||
if (task.use_episode_naming) {
|
||
// 直接使用任务名称中提取的信息构建命名规则
|
||
task.pattern = showName + nameSeparator + 'S' + seasonNumber.padStart(2, '0') + 'E[]';
|
||
task.episode_naming = task.pattern;
|
||
}
|
||
// 处理顺序命名模式
|
||
else if (task.use_sequence_naming) {
|
||
// 直接使用任务名称中提取的信息构建命名规则
|
||
task.pattern = showName + nameSeparator + 'S' + seasonNumber.padStart(2, '0') + 'E{}';
|
||
task.sequence_naming = task.pattern;
|
||
}
|
||
} else {
|
||
// 非电视节目,仅在使用剧集或顺序命名时更新
|
||
if (task.use_episode_naming) {
|
||
// 如果原模式是纯E[],直接使用任务名称
|
||
if (task.pattern.trim() === 'E[]') {
|
||
task.pattern = cleanTaskName + 'E[]';
|
||
} else if (task.pattern.includes('[]')) {
|
||
// 尝试保留原有格式,但更新剧名部分
|
||
const patternParts = task.pattern.split(/E\[\]/i);
|
||
if (patternParts.length > 0) {
|
||
task.pattern = cleanTaskName + 'E[]' + (patternParts[1] || '');
|
||
} else {
|
||
task.pattern = cleanTaskName + 'E[]';
|
||
}
|
||
}
|
||
task.episode_naming = task.pattern;
|
||
} else if (task.use_sequence_naming) {
|
||
// 同样处理顺序命名
|
||
if (task.pattern.trim() === 'E{}' || task.pattern.trim() === '{}') {
|
||
task.pattern = cleanTaskName + 'E{}';
|
||
} else if (task.pattern.includes('{}')) {
|
||
const patternParts = task.pattern.split(/E\{\}/i);
|
||
if (patternParts.length > 0) {
|
||
task.pattern = cleanTaskName + 'E{}' + (patternParts[1] || '');
|
||
} else {
|
||
task.pattern = cleanTaskName + 'E{}';
|
||
}
|
||
}
|
||
task.sequence_naming = task.pattern;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
},
|
||
removeTask(index) {
|
||
if (confirm("确定要删除任务 [#" + (index + 1) + ": " + this.formData.tasklist[index].taskname + "] 吗?"))
|
||
this.formData.tasklist.splice(index, 1);
|
||
},
|
||
changeShareurl(task) {
|
||
if (!task.shareurl)
|
||
return;
|
||
this.$set(task, "shareurl_ban", undefined);
|
||
// 从URL中提取任务名
|
||
try {
|
||
const matches = decodeURIComponent(task.shareurl).match(/\/(\w{32})-([^\/]+)$/);
|
||
if (matches) {
|
||
task.taskname = task.taskname == "" ? matches[2] : task.taskname;
|
||
task.savepath = task.savepath.replace(/TASKNAME/g, matches[2]);
|
||
}
|
||
} catch (e) {
|
||
// 错误处理
|
||
}
|
||
// 从分享中提取任务名
|
||
axios.get('/get_share_detail', { params: { shareurl: task.shareurl } })
|
||
.then(response => {
|
||
share_detail = response.data.data
|
||
if (!response.data.success) {
|
||
if (share_detail.error.includes("提取码")) {
|
||
const passcode = prompt("检查失败[" + share_detail.error + "],请输入提取码:");
|
||
if (passcode != null) {
|
||
task.shareurl = task.shareurl.replace(/pan.quark.cn\/s\/(\w+)(\?pwd=\w*)*/, `pan.quark.cn/s/$1?pwd=${passcode}`);
|
||
this.changeShareurl(task);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 使用格式化函数处理错误信息
|
||
this.$set(task, "shareurl_ban", this.formatShareUrlBanMessage(share_detail.error));
|
||
} else {
|
||
// 检查文件列表是否为空
|
||
if (share_detail.list !== undefined && share_detail.list.length === 0) {
|
||
this.$set(task, "shareurl_ban", "该分享已被删除,无法访问");
|
||
} else {
|
||
task.taskname = task.taskname == "" ? share_detail.share.title : task.taskname;
|
||
task.savepath = task.savepath.replace(/TASKNAME/g, share_detail.share.title);
|
||
this.$set(task, "shareurl_ban", undefined);
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
// 错误处理
|
||
});
|
||
},
|
||
clearData(target) {
|
||
this[target] = "";
|
||
},
|
||
async runScriptNow(task_index = null) {
|
||
body = {};
|
||
if (task_index != null) {
|
||
task = { ...this.formData.tasklist[task_index] };
|
||
delete task.runweek;
|
||
delete task.enddate;
|
||
body = {
|
||
"tasklist": [task],
|
||
"original_index": task_index + 1 // 添加原始索引,从1开始计数
|
||
};
|
||
} else if (this.configModified) {
|
||
if (!confirm('配置已修改但未保存,是否继续运行?')) {
|
||
return;
|
||
}
|
||
}
|
||
$('#logModal').modal('toggle');
|
||
this.modalLoading = true;
|
||
this.run_log = '';
|
||
try {
|
||
// 1. 发送 POST 请求
|
||
const response = await fetch(`/run_script_now`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(body)
|
||
});
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||
}
|
||
// 2. 处理 SSE 流
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let partialData = '';
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) {
|
||
this.modalLoading = false;
|
||
// 运行后刷新数据
|
||
this.fetchData();
|
||
break;
|
||
}
|
||
partialData += decoder.decode(value);
|
||
const lines = partialData.split('\n').filter(line => line.trim() !== '');
|
||
for (const line of lines) {
|
||
if (line.startsWith('data:')) {
|
||
const eventData = line.substring(5).trim();
|
||
if (eventData === '[DONE]') {
|
||
this.modalLoading = false;
|
||
this.fetchData();
|
||
return;
|
||
}
|
||
this.run_log += eventData + '\n';
|
||
// 在更新 run_log 后将滚动条滚动到底部
|
||
this.$nextTick(() => {
|
||
const modalBody = document.querySelector('.modal-body');
|
||
modalBody.scrollTop = modalBody.scrollHeight;
|
||
});
|
||
}
|
||
}
|
||
partialData = '';
|
||
}
|
||
} catch (error) {
|
||
this.modalLoading = false;
|
||
}
|
||
},
|
||
getParentDirectory(path) {
|
||
parentDir = path.substring(0, path.lastIndexOf('/'))
|
||
if (parentDir == "")
|
||
parentDir = "/"
|
||
return parentDir;
|
||
},
|
||
scrollToX(top = undefined) {
|
||
if (top == undefined)
|
||
top = document.documentElement.scrollHeight
|
||
window.scrollTo({
|
||
top: top,
|
||
behavior: "smooth"
|
||
});
|
||
},
|
||
getAvailablePlugins(plugins) {
|
||
availablePlugins = {};
|
||
const pluginsFlagsArray = this.plugin_flags.split(',');
|
||
for (const pluginName in plugins) {
|
||
if (!pluginsFlagsArray.includes(`-${pluginName}`)) {
|
||
availablePlugins[pluginName] = plugins[pluginName];
|
||
}
|
||
}
|
||
return availablePlugins;
|
||
},
|
||
toggleAllWeekdays(task) {
|
||
if (task.runweek.length === 7) {
|
||
task.runweek = [];
|
||
} else {
|
||
task.runweek = [1, 2, 3, 4, 5, 6, 7];
|
||
}
|
||
},
|
||
searchSuggestions(index, taskname, deep = 1) {
|
||
if (taskname.length < 2) {
|
||
return;
|
||
}
|
||
// 开始搜索前清除之前的搜索结果
|
||
this.smart_param.taskSuggestions = {
|
||
success: false,
|
||
data: []
|
||
};
|
||
this.smart_param.isSearching = true;
|
||
this.smart_param.validating = false; // 重置验证状态
|
||
this.smart_param.index = index;
|
||
// 确保显示下拉菜单
|
||
this.smart_param.showSuggestions = true;
|
||
try {
|
||
axios.get('/task_suggestions', {
|
||
params: {
|
||
q: taskname,
|
||
d: deep
|
||
}
|
||
}).then(response => {
|
||
// 接收到数据后,过滤无效链接
|
||
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
||
// 使用新增的方法验证链接有效性
|
||
this.validateSearchResults(response.data);
|
||
} else {
|
||
this.smart_param.taskSuggestions = response.data;
|
||
// 重新确认设置为true
|
||
this.smart_param.showSuggestions = true;
|
||
this.smart_param.isSearching = false;
|
||
this.smart_param.validating = false; // 重置验证状态
|
||
}
|
||
}).catch(error => {
|
||
this.smart_param.isSearching = false;
|
||
this.smart_param.validating = false; // 重置验证状态
|
||
});
|
||
} catch (e) {
|
||
this.smart_param.taskSuggestions = {
|
||
error: "网络异常"
|
||
};
|
||
this.smart_param.isSearching = false;
|
||
this.smart_param.validating = false; // 重置验证状态
|
||
}
|
||
},
|
||
// 添加新方法来验证搜索结果
|
||
validateSearchResults(searchData) {
|
||
const invalidTerms = [
|
||
"分享者用户封禁链接查看受限",
|
||
"好友已取消了分享",
|
||
"文件已被分享者删除",
|
||
"文件夹为空",
|
||
"分享地址已失效",
|
||
"文件涉及违规内容"
|
||
];
|
||
|
||
// 如果没有搜索结果,直接返回
|
||
if (!searchData.data || searchData.data.length === 0) {
|
||
this.smart_param.taskSuggestions = searchData;
|
||
this.smart_param.isSearching = false;
|
||
this.smart_param.validating = false;
|
||
return;
|
||
}
|
||
|
||
// 设置验证状态
|
||
this.smart_param.validating = true;
|
||
|
||
// 初始化验证进度
|
||
this.smart_param.validateProgress = {
|
||
total: searchData.data.length,
|
||
current: 0,
|
||
valid: 0
|
||
};
|
||
|
||
// 初始化一个结果数组和待处理数组
|
||
const validResults = [];
|
||
const toProcess = [...searchData.data]; // 复制数组,保留原始顺序
|
||
|
||
// 批量处理的大小,每批次最多处理5个链接
|
||
const batchSize = 5;
|
||
|
||
// 处理单个链接的函数
|
||
const processLink = (link) => {
|
||
return new Promise((resolve) => {
|
||
if (!link.shareurl) {
|
||
// 没有分享链接,直接跳过
|
||
resolve(null);
|
||
return;
|
||
}
|
||
|
||
// 使用现有的get_share_detail接口验证链接
|
||
axios.get('/get_share_detail', {
|
||
params: { shareurl: link.shareurl }
|
||
})
|
||
.then(response => {
|
||
// 更新进度
|
||
this.smart_param.validateProgress.current++;
|
||
|
||
if (response.data.success) {
|
||
// 检查文件列表是否为空
|
||
const shareDetail = response.data.data;
|
||
if (shareDetail.list && shareDetail.list.length > 0) {
|
||
// 链接有效,添加到有效结果列表
|
||
this.smart_param.validateProgress.valid++;
|
||
resolve(link);
|
||
return;
|
||
}
|
||
} else {
|
||
// 检查是否包含已知的失效原因
|
||
const error = response.data.data.error || "";
|
||
let isInvalid = false;
|
||
|
||
for (const term of invalidTerms) {
|
||
if (error.includes(term)) {
|
||
isInvalid = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果不是已知的失效原因,保留该结果
|
||
if (!isInvalid) {
|
||
this.smart_param.validateProgress.valid++;
|
||
resolve(link);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 链接无效
|
||
resolve(null);
|
||
})
|
||
.catch(error => {
|
||
// 验证出错,保守处理为有效
|
||
this.smart_param.validateProgress.current++;
|
||
this.smart_param.validateProgress.valid++;
|
||
resolve(link);
|
||
});
|
||
});
|
||
};
|
||
|
||
// 修改processBatch函数,增加快速显示功能
|
||
const processBatch = async () => {
|
||
// 取下一批处理
|
||
const batch = toProcess.splice(0, batchSize);
|
||
if (batch.length === 0) {
|
||
// 所有批次处理完毕
|
||
this.finishValidation(searchData, validResults);
|
||
return;
|
||
}
|
||
|
||
// 并行处理当前批次
|
||
const results = await Promise.all(batch.map(processLink));
|
||
|
||
// 收集有效结果
|
||
results.forEach(result => {
|
||
if (result) {
|
||
validResults.push(result);
|
||
}
|
||
});
|
||
|
||
// 快速显示:如果已经找到至少3个有效链接,并且还没有显示过结果
|
||
// 同时已验证的链接数量超过总数的30%或者已经找到5个有效链接
|
||
const hasEnoughValidLinks = validResults.length >= 3;
|
||
const hasProcessedEnough = this.smart_param.validateProgress.current >= this.smart_param.validateProgress.total * 0.3 || validResults.length >= 5;
|
||
|
||
if (hasEnoughValidLinks && hasProcessedEnough && !this.smart_param._hasShownInterimResults) {
|
||
// 标记已显示过快速结果
|
||
this.smart_param._hasShownInterimResults = true;
|
||
|
||
// 创建一个中间结果显示,同时保持验证状态
|
||
const interimResult = {
|
||
success: searchData.success,
|
||
source: searchData.source,
|
||
data: [...validResults], // 复制当前有效结果
|
||
message: `已找到${validResults.length}个有效链接,验证继续进行中...`
|
||
};
|
||
|
||
// 更新显示但保持验证状态
|
||
this.smart_param.taskSuggestions = interimResult;
|
||
}
|
||
|
||
// 继续处理下一批
|
||
processBatch();
|
||
};
|
||
|
||
// 开始批量处理前重置快速显示标记
|
||
this.smart_param._hasShownInterimResults = false;
|
||
processBatch();
|
||
|
||
// 设置超时,避免永久等待
|
||
setTimeout(() => {
|
||
// 如果验证还在进行中,强制完成
|
||
if (this.smart_param.validating) {
|
||
// 将剩余未验证的链接添加到结果中
|
||
const remaining = toProcess.filter(item => item.shareurl);
|
||
validResults.push(...remaining);
|
||
|
||
// 更新进度统计
|
||
this.smart_param.validateProgress.current = this.smart_param.validateProgress.total;
|
||
this.smart_param.validateProgress.valid = validResults.length;
|
||
|
||
// 完成验证
|
||
this.finishValidation(searchData, validResults);
|
||
}
|
||
}, 30000); // 30秒超时
|
||
},
|
||
// 完成验证并更新搜索结果
|
||
finishValidation(searchData, validResults) {
|
||
// 清除快速显示标记
|
||
this.smart_param._hasShownInterimResults = false;
|
||
|
||
// 更新搜索结果
|
||
const result = {
|
||
success: searchData.success,
|
||
source: searchData.source,
|
||
data: validResults,
|
||
message: validResults.length === 0 ? "未找到有效的分享链接" : searchData.message
|
||
};
|
||
|
||
this.smart_param.taskSuggestions = result;
|
||
this.smart_param.isSearching = false;
|
||
this.smart_param.validating = false;
|
||
this.smart_param.showSuggestions = true;
|
||
},
|
||
selectSuggestion(index, suggestion) {
|
||
this.smart_param.showSuggestions = false;
|
||
// 确保显示的是选择需转存的文件夹界面,而不是命名预览界面
|
||
this.fileSelect.previewRegex = false;
|
||
this.fileSelect.selectDir = true;
|
||
this.showShareSelect(index, suggestion.shareurl);
|
||
},
|
||
addMagicRegex() {
|
||
const newKey = `$MAGIC_${Object.keys(this.formData.magic_regex).length + 1}`;
|
||
this.$set(this.formData.magic_regex, newKey, { pattern: '', replace: '' });
|
||
},
|
||
updateMagicRegexKey(oldKey, newKey) {
|
||
if (oldKey !== newKey) {
|
||
if (this.formData.magic_regex[newKey]) {
|
||
alert(`魔法名 [${newKey}] 已存在,请使用其他名称`);
|
||
return;
|
||
}
|
||
this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]);
|
||
this.$delete(this.formData.magic_regex, oldKey);
|
||
}
|
||
},
|
||
removeMagicRegex(key) {
|
||
if (confirm(`确定要删除魔法匹配规则 [${key}] 吗?`)) {
|
||
this.$delete(this.formData.magic_regex, key);
|
||
}
|
||
},
|
||
deleteFileForSelect(fid, fname, isDir, deleteRecords = false) {
|
||
let confirmMessage = deleteRecords
|
||
? `确定要删除此项目及其关联记录吗?`
|
||
: `确定要删除此项目吗?`;
|
||
|
||
if (!confirm(confirmMessage)) {
|
||
return;
|
||
}
|
||
|
||
// 获取当前路径作为save_path参数
|
||
let save_path = "";
|
||
if (this.fileSelect && this.fileSelect.paths) {
|
||
save_path = this.fileSelect.paths.map(item => item.name).join("/");
|
||
}
|
||
|
||
axios.post('/delete_file', { fid: fid, file_name: fname, delete_records: deleteRecords, save_path: save_path })
|
||
.then(response => {
|
||
if (response.data.code === 0) {
|
||
// 从列表中移除文件
|
||
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid !== fid);
|
||
|
||
// 如果文件在选中列表中,也从选中列表中移除
|
||
if (this.fileSelect.selectedFiles.includes(fid)) {
|
||
this.fileSelect.selectedFiles = this.fileSelect.selectedFiles.filter(id => id !== fid);
|
||
|
||
// 如果选中列表为空,重置最后选择的索引
|
||
if (this.fileSelect.selectedFiles.length === 0) {
|
||
this.fileSelect.lastSelectedFileIndex = -1;
|
||
}
|
||
}
|
||
|
||
// 显示成功消息,根据是否删除记录显示不同的消息
|
||
if (deleteRecords) {
|
||
const deletedRecords = response.data.deleted_records || 0;
|
||
this.showToast(`成功删除 1 个项目${deletedRecords > 0 ? `及其关联的 ${deletedRecords} 条记录` : ''}`);
|
||
} else {
|
||
this.showToast('成功删除 1 个项目');
|
||
}
|
||
|
||
// 如果同时删除了记录,无论当前在哪个页面,都刷新历史记录
|
||
if (deleteRecords) {
|
||
this.loadHistoryRecords();
|
||
}
|
||
} else {
|
||
alert('删除失败: ' + response.data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('删除项目出错:', error);
|
||
alert('删除项目出错: ' + (error.response?.data?.message || error.message));
|
||
});
|
||
},
|
||
getSavepathDetail(params = 0, retryCount = 0, maxRetries = 1) {
|
||
if (params === "" || params === null || params === undefined) {
|
||
// 为空字符串时直接使用根目录fid
|
||
params = 0;
|
||
} else if (params.includes && params.includes('/')) {
|
||
params = { path: params }
|
||
} else {
|
||
params = { fid: params }
|
||
}
|
||
this.modalLoading = true;
|
||
|
||
// 根据模态框类型决定使用哪个账号
|
||
// 任务配置相关的模态框始终使用主账号(索引0)
|
||
// 文件整理页面的预览模态框和移动文件模态框使用选中的账号
|
||
const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
|
||
const accountIndex = (modalType === 'preview-filemanager' || modalType === 'move') ? this.fileManager.selectedAccountIndex : 0;
|
||
|
||
// 添加账号索引参数
|
||
if (typeof params === 'object' && params !== null) {
|
||
params.account_index = accountIndex;
|
||
} else {
|
||
params = {
|
||
fid: params,
|
||
account_index: accountIndex
|
||
};
|
||
}
|
||
axios.get('/get_savepath_detail', {
|
||
params: params
|
||
}).then(response => {
|
||
this.fileSelect.fileList = response.data.data.list
|
||
// 应用默认排序,但不改变当前的排序顺序
|
||
this.manualSortFileList(this.fileSelect.sortBy, this.fileSelect.sortOrder);
|
||
if (response.data.data.paths.length > 0) {
|
||
this.fileSelect.paths = response.data.data.paths
|
||
}
|
||
this.modalLoading = false;
|
||
}).catch(error => {
|
||
// 如果还有重试次数,则进行重试
|
||
if (retryCount < maxRetries) {
|
||
console.log(`获取文件夹列表失败,正在进行第 ${retryCount + 1} 次重试...`);
|
||
// 短暂延迟后重试
|
||
setTimeout(() => {
|
||
this.getSavepathDetail(params, retryCount + 1, maxRetries);
|
||
}, 1000); // 1秒后重试
|
||
} else {
|
||
// 超过最大重试次数,显示错误信息
|
||
this.fileSelect.error = "获取文件夹列表失败,请关闭窗口再试一次";
|
||
this.modalLoading = false;
|
||
}
|
||
});
|
||
},
|
||
showSavepathSelect(index) {
|
||
this.fileSelect.selectShare = false;
|
||
this.fileSelect.selectDir = true;
|
||
this.fileSelect.previewRegex = false;
|
||
this.fileSelect.error = undefined;
|
||
this.fileSelect.fileList = [];
|
||
this.fileSelect.paths = [];
|
||
this.fileSelect.index = index;
|
||
// 重置排序状态为默认值 - 选择需转存的文件夹模态框默认修改时间倒序
|
||
this.fileSelect.sortBy = "updated_at";
|
||
this.fileSelect.sortOrder = "desc";
|
||
|
||
// 设置模态框类型为target(保存目标文件夹)
|
||
document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'target');
|
||
|
||
$('#fileSelectModal').modal('toggle');
|
||
|
||
// 当savepath为空时,直接加载根目录
|
||
const savepath = this.formData.tasklist[index].savepath;
|
||
if (!savepath || savepath === "") {
|
||
this.getSavepathDetail(0); // 加载根目录
|
||
} else {
|
||
this.getSavepathDetail(savepath);
|
||
}
|
||
},
|
||
getShareDetail(retryCount = 0, maxRetries = 1) {
|
||
this.modalLoading = true;
|
||
|
||
// 检查index是否有效,如果无效则使用默认值
|
||
let regexConfig = {};
|
||
if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
|
||
const task = this.formData.tasklist[this.fileSelect.index];
|
||
regexConfig = {
|
||
pattern: task.pattern,
|
||
replace: task.replace,
|
||
taskname: task.taskname,
|
||
filterwords: task.filterwords,
|
||
magic_regex: this.formData.magic_regex,
|
||
use_sequence_naming: task.use_sequence_naming,
|
||
sequence_naming: task.sequence_naming,
|
||
use_episode_naming: task.use_episode_naming,
|
||
episode_naming: task.episode_naming,
|
||
episode_patterns: this.formData.episode_patterns
|
||
};
|
||
} else {
|
||
// 使用默认配置
|
||
regexConfig = {
|
||
pattern: '',
|
||
replace: '',
|
||
taskname: '',
|
||
filterwords: '',
|
||
magic_regex: this.formData.magic_regex,
|
||
use_sequence_naming: false,
|
||
sequence_naming: '',
|
||
use_episode_naming: false,
|
||
episode_naming: '',
|
||
episode_patterns: this.formData.episode_patterns
|
||
};
|
||
}
|
||
|
||
axios.post('/get_share_detail', {
|
||
shareurl: this.fileSelect.shareurl,
|
||
stoken: this.fileSelect.stoken,
|
||
regex: regexConfig
|
||
}).then(response => {
|
||
if (response.data.success) {
|
||
this.fileSelect.fileList = response.data.data.list;
|
||
// 应用默认排序,但不改变当前的排序顺序
|
||
this.manualSortFileList(this.fileSelect.sortBy, this.fileSelect.sortOrder);
|
||
this.fileSelect.paths = response.data.data.paths;
|
||
this.fileSelect.stoken = response.data.data.stoken;
|
||
|
||
// 检查文件列表是否为空,仅在预览界面显示错误,不影响任务本身
|
||
if (this.fileSelect.selectShare && (!this.fileSelect.fileList || this.fileSelect.fileList.length === 0)) {
|
||
this.fileSelect.error = "该分享已被删除,无法访问";
|
||
// 移除下面这段代码,避免在预览时直接设置任务的shareurl_ban
|
||
// if (this.fileSelect.index !== null && this.formData.tasklist[this.fileSelect.index]) {
|
||
// this.$set(this.formData.tasklist[this.fileSelect.index], "shareurl_ban", "该分享已被删除,无法访问");
|
||
// }
|
||
}
|
||
|
||
// 命名预览模式下,在数据加载完成后检查滚动条状态
|
||
if (this.fileSelect.previewRegex) {
|
||
this.$nextTick(() => {
|
||
this.checkPreviewScrollbar();
|
||
});
|
||
}
|
||
} else {
|
||
// 使用格式化函数处理错误信息
|
||
this.fileSelect.error = this.formatShareUrlBanMessage(response.data.data.error);
|
||
}
|
||
this.modalLoading = false;
|
||
}).catch(error => {
|
||
// 如果还有重试次数,则进行重试
|
||
if (retryCount < maxRetries) {
|
||
console.log(`获取文件夹列表失败,正在进行第 ${retryCount + 1} 次重试...`);
|
||
// 短暂延迟后重试
|
||
setTimeout(() => {
|
||
this.getShareDetail(retryCount + 1, maxRetries);
|
||
}, 1000); // 1秒后重试
|
||
} else {
|
||
// 超过最大重试次数,显示错误信息
|
||
this.fileSelect.error = "获取文件夹列表失败,请关闭窗口再试一次";
|
||
this.modalLoading = false;
|
||
}
|
||
});
|
||
},
|
||
showShareSelect(index, shareurl = null) {
|
||
this.fileSelect.selectShare = true;
|
||
this.fileSelect.fileList = [];
|
||
this.fileSelect.paths = [];
|
||
this.fileSelect.error = undefined;
|
||
// 根据模式设置不同的默认排序
|
||
if (this.fileSelect.previewRegex) {
|
||
// 命名预览模式下使用重命名列倒序排序
|
||
this.fileSelect.sortBy = "file_name_re";
|
||
this.fileSelect.sortOrder = "desc";
|
||
} else if (this.fileSelect.selectDir) {
|
||
// 选择需转存的文件夹模态框:默认修改时间倒序
|
||
this.fileSelect.sortBy = "updated_at";
|
||
this.fileSelect.sortOrder = "desc";
|
||
} else {
|
||
// 选择起始文件模态框:使用文件名倒序排序(使用全局文件排序函数)
|
||
this.fileSelect.sortBy = "file_name";
|
||
this.fileSelect.sortOrder = "desc";
|
||
}
|
||
if (this.getShareurl(this.fileSelect.shareurl) != this.getShareurl(this.formData.tasklist[index].shareurl)) {
|
||
this.fileSelect.stoken = "";
|
||
}
|
||
this.fileSelect.shareurl = shareurl || this.formData.tasklist[index].shareurl;
|
||
this.fileSelect.index = index;
|
||
|
||
// 根据不同条件设置模态框类型
|
||
if (this.fileSelect.previewRegex) {
|
||
document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'preview');
|
||
} else if (this.fileSelect.selectDir) {
|
||
document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'source');
|
||
} else {
|
||
document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'start-file');
|
||
}
|
||
|
||
$('#fileSelectModal').modal('toggle');
|
||
// 调用getShareDetail时不传递任何参数,使用默认的重试机制
|
||
this.getShareDetail(0, 1);
|
||
|
||
// 命名预览模式下,确保在模态框显示后检查滚动条状态
|
||
if (this.fileSelect.previewRegex) {
|
||
this.$nextTick(() => {
|
||
this.checkPreviewScrollbar();
|
||
});
|
||
}
|
||
},
|
||
navigateTo(fid, name) {
|
||
// 新增:命名预览模式下(文件整理页面),切换目录时刷新预览
|
||
if (this.fileSelect.previewRegex && this.fileSelect.index === -1) {
|
||
// 更新fileManager当前目录
|
||
this.fileManager.currentFolder = fid;
|
||
// 更新面包屑路径
|
||
if (fid == "0") {
|
||
this.fileSelect.paths = [];
|
||
} else {
|
||
const index = this.fileSelect.paths.findIndex(item => item.fid === fid);
|
||
if (index !== -1) {
|
||
this.fileSelect.paths = this.fileSelect.paths.slice(0, index + 1);
|
||
} else {
|
||
this.fileSelect.paths.push({ fid: fid, name: name });
|
||
}
|
||
}
|
||
// 直接刷新命名预览(不关闭模态框)
|
||
this.showFileManagerNamingPreview(fid);
|
||
return;
|
||
}
|
||
path = { fid: fid, name: name }
|
||
if (this.fileSelect.selectShare) {
|
||
this.fileSelect.shareurl = this.getShareurl(this.fileSelect.shareurl, path);
|
||
// 使用重试机制调用getShareDetail
|
||
this.getShareDetail(0, 1);
|
||
} else {
|
||
if (fid == "0") {
|
||
this.fileSelect.paths = []
|
||
} else {
|
||
index = this.fileSelect.paths.findIndex(item => item.fid === fid);
|
||
if (index !== -1) {
|
||
this.fileSelect.paths = this.fileSelect.paths.slice(0, index + 1)
|
||
} else {
|
||
this.fileSelect.paths.push({ fid: fid, name: name })
|
||
}
|
||
}
|
||
// 使用重试机制调用getSavepathDetail
|
||
this.getSavepathDetail(fid, 0, 1);
|
||
}
|
||
},
|
||
selectCurrentFolder(addTaskname = false) {
|
||
if (this.fileSelect.moveMode) {
|
||
// 移动文件模式
|
||
this.moveFilesToCurrentFolder();
|
||
} else if (this.fileSelect.selectShare) {
|
||
// 检查index是否有效
|
||
if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
|
||
this.formData.tasklist[this.fileSelect.index].shareurl_ban = undefined;
|
||
// 如果是分享文件夹且文件列表为空,则设置shareurl_ban
|
||
if (!this.fileSelect.fileList || this.fileSelect.fileList.length === 0) {
|
||
this.$set(this.formData.tasklist[this.fileSelect.index], "shareurl_ban", "该分享已被删除,无法访问");
|
||
}
|
||
this.formData.tasklist[this.fileSelect.index].shareurl = this.fileSelect.shareurl;
|
||
}
|
||
} else {
|
||
// 检查index是否有效
|
||
if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
|
||
// 去掉前导斜杠,避免双斜杠问题
|
||
this.formData.tasklist[this.fileSelect.index].savepath = this.fileSelect.paths.map(item => item.name).join("/");
|
||
if (addTaskname) {
|
||
this.formData.tasklist[this.fileSelect.index].savepath += "/" + this.formData.tasklist[this.fileSelect.index].taskname
|
||
}
|
||
}
|
||
}
|
||
if (!this.fileSelect.moveMode) {
|
||
$('#fileSelectModal').modal('hide')
|
||
}
|
||
},
|
||
// 移动文件到当前文件夹
|
||
moveFilesToCurrentFolder() {
|
||
if (!this.fileSelect.moveFileIds || this.fileSelect.moveFileIds.length === 0) {
|
||
alert('没有选择要移动的文件');
|
||
return;
|
||
}
|
||
|
||
// 获取目标文件夹ID
|
||
const targetFolderId = this.fileSelect.paths.length > 0
|
||
? this.fileSelect.paths[this.fileSelect.paths.length - 1].fid
|
||
: "0";
|
||
|
||
// 记录源目录ID(文件原本所在的目录)
|
||
const sourceFolderId = this.fileManager.currentFolder || "0";
|
||
|
||
const fileCount = this.fileSelect.moveFileIds.length;
|
||
const confirmMessage = fileCount === 1
|
||
? '确定要移动此文件吗?'
|
||
: `确定要移动选中的 ${fileCount} 个文件吗?`;
|
||
|
||
if (!confirm(confirmMessage)) {
|
||
return;
|
||
}
|
||
|
||
// 调用移动文件API
|
||
axios.post('/move_file', {
|
||
file_ids: this.fileSelect.moveFileIds,
|
||
target_folder_id: targetFolderId,
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.showToast(`成功移动 ${response.data.moved_count} 个文件`);
|
||
|
||
// 关闭模态框
|
||
$('#fileSelectModal').modal('hide');
|
||
|
||
// 重置移动模式相关参数
|
||
this.fileSelect.moveMode = false;
|
||
this.fileSelect.moveFileIds = [];
|
||
this.fileSelect.index = null; // 重置index避免后续访问undefined
|
||
|
||
// 清空选中的文件
|
||
this.fileManager.selectedFiles = [];
|
||
|
||
// 延迟刷新以确保后端处理完成
|
||
setTimeout(() => {
|
||
// 刷新当前页面显示(源目录)
|
||
this.refreshCurrentFolderCache();
|
||
|
||
// 如果目标目录不同于源目录,也刷新目标目录的缓存
|
||
if (targetFolderId !== sourceFolderId) {
|
||
this.refreshFolderCache(targetFolderId); // 刷新目标目录缓存
|
||
}
|
||
|
||
// 刷新源目录的上级目录(更新源文件夹的项目数量)
|
||
const sourceParentFolderId = this.getParentFolderId(sourceFolderId);
|
||
if (sourceParentFolderId !== null) {
|
||
this.refreshFolderCache(sourceParentFolderId);
|
||
}
|
||
|
||
// 刷新目标目录的上级目录(更新目标文件夹的项目数量)
|
||
if (targetFolderId !== sourceFolderId) {
|
||
const targetParentFolderId = this.getParentFolderId(targetFolderId);
|
||
if (targetParentFolderId !== null && targetParentFolderId !== sourceParentFolderId) {
|
||
this.refreshFolderCache(targetParentFolderId);
|
||
}
|
||
}
|
||
}, 500); // 延迟500ms确保后端处理完成
|
||
} else {
|
||
const errorMessage = response.data.message || '移动失败';
|
||
// 对于"不能移动至相同目录"错误使用Toast通知
|
||
if (errorMessage.includes('不能移动至相同目录')) {
|
||
this.showToast('不能移动至相同目录');
|
||
} else {
|
||
this.showToast(errorMessage);
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('移动文件失败:', error);
|
||
this.showToast('移动文件失败');
|
||
});
|
||
},
|
||
selectStartFid(fid) {
|
||
// 检查index是否有效
|
||
if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
|
||
Vue.set(this.formData.tasklist[this.fileSelect.index], 'startfid', fid);
|
||
}
|
||
$('#fileSelectModal').modal('hide')
|
||
},
|
||
getShareurl(shareurl, path = {}) {
|
||
if (path == {} || path.fid == 0) {
|
||
shareurl = shareurl.match(`.*s/[a-z0-9]+`)[0]
|
||
} else if (shareurl.includes(path.fid)) {
|
||
shareurl = shareurl.match(`.*/${path.fid}[^\/]*`)[0]
|
||
} else if (shareurl.includes('#/list/share')) {
|
||
shareurl = `${shareurl}/${path.fid}-${path.name}`
|
||
} else {
|
||
shareurl = `${shareurl}#/list/share/${path.fid}-${path.name}`
|
||
}
|
||
return shareurl;
|
||
},
|
||
detectNamingMode(task) {
|
||
// 检测是否为顺序命名模式或剧集命名模式
|
||
let isSequenceNaming = false;
|
||
let isEpisodeNaming = false;
|
||
const currentValue = task.pattern;
|
||
|
||
if (currentValue !== undefined) {
|
||
isSequenceNaming = currentValue.includes('{}');
|
||
isEpisodeNaming = !isSequenceNaming && currentValue.includes('[]');
|
||
}
|
||
|
||
// 只要不是正则命名,强制清空 replace
|
||
if (isSequenceNaming || isEpisodeNaming) {
|
||
task.replace = '';
|
||
}
|
||
|
||
if (isSequenceNaming) {
|
||
// 切换到顺序命名模式
|
||
if (!task.use_sequence_naming) {
|
||
task._pattern_backup = task.pattern;
|
||
task._replace_backup = task.replace;
|
||
task.use_sequence_naming = true;
|
||
task.use_episode_naming = false;
|
||
}
|
||
task.sequence_naming = task.pattern;
|
||
} else if (isEpisodeNaming) {
|
||
// 切换到剧集命名模式
|
||
if (!task.use_episode_naming) {
|
||
task._pattern_backup = task.pattern;
|
||
task._replace_backup = task.replace;
|
||
task.use_episode_naming = true;
|
||
task.use_sequence_naming = false;
|
||
}
|
||
task.episode_naming = task.pattern;
|
||
} else {
|
||
// 切换回正则命名模式
|
||
if (task.use_sequence_naming || task.use_episode_naming) {
|
||
task.use_sequence_naming = false;
|
||
task.use_episode_naming = false;
|
||
task.sequence_naming = null;
|
||
task.episode_naming = null;
|
||
}
|
||
}
|
||
|
||
// 保存当前值,用于下次比较
|
||
task._lastPatternValue = currentValue;
|
||
|
||
// 强制Vue更新视图
|
||
this.$forceUpdate();
|
||
},
|
||
// 增加剧集识别模式
|
||
addEpisodePattern() {
|
||
// 此方法保留但不再使用
|
||
},
|
||
|
||
// 移除剧集识别模式
|
||
removeEpisodePattern(index) {
|
||
// 此方法保留但不再使用
|
||
},
|
||
loadHistoryRecords() {
|
||
// 构建请求参数
|
||
const params = {
|
||
sort_by: this.historyParams.sortBy || "transfer_time",
|
||
order: this.historyParams.order || "desc",
|
||
page_size: parseInt(this.historyParams.page_size || 15),
|
||
page: this.historyParams.page || 1
|
||
};
|
||
|
||
// 添加筛选条件
|
||
if (this.historyTaskSelected) {
|
||
params.task_name = this.historyTaskSelected;
|
||
}
|
||
|
||
if (this.historyNameFilter) {
|
||
params.keyword = this.historyNameFilter;
|
||
}
|
||
|
||
// 判断筛选条件是否变化,只有变化时才重置页码
|
||
const isFilterChanged =
|
||
(this._lastTaskFilter !== this.historyTaskSelected) ||
|
||
(this._lastNameFilter !== this.historyNameFilter);
|
||
|
||
if (isFilterChanged) {
|
||
// 筛选条件变化时重置为第一页
|
||
params.page = 1;
|
||
this.gotoPage = 1;
|
||
}
|
||
|
||
// 记录当前筛选条件
|
||
this._lastTaskFilter = this.historyTaskSelected;
|
||
this._lastNameFilter = this.historyNameFilter;
|
||
|
||
// 更新当前参数
|
||
this.historyParams = {
|
||
sortBy: params.sort_by,
|
||
order: params.order,
|
||
page_size: params.page_size,
|
||
page: params.page
|
||
};
|
||
|
||
axios.get('/history_records', {
|
||
params: params
|
||
}).then(response => {
|
||
if (response.data.success) {
|
||
this.history = response.data.data;
|
||
|
||
// 确保页码信息正确反映在UI上
|
||
if (this.history.pagination) {
|
||
this.totalPages = parseInt(this.history.pagination.total_pages);
|
||
}
|
||
} else {
|
||
console.error('Error:', response.data.message);
|
||
}
|
||
}).catch(error => {
|
||
console.error('Error loading history records:', error);
|
||
});
|
||
},
|
||
|
||
sortHistory(field) {
|
||
// 更新排序参数
|
||
if (this.historyParams.sortBy === field) {
|
||
this.historyParams.order = this.historyParams.order === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
this.historyParams.sortBy = field;
|
||
// 保持默认排序顺序为'desc',与后端一致
|
||
this.historyParams.order = 'desc';
|
||
}
|
||
|
||
// 构建请求参数
|
||
const params = {
|
||
sort_by: this.historyParams.sortBy,
|
||
order: this.historyParams.order,
|
||
page_size: parseInt(this.historyParams.page_size || 15),
|
||
page: this.historyParams.page || 1
|
||
};
|
||
|
||
// 添加筛选条件
|
||
if (this.historyTaskSelected) {
|
||
params.task_name = this.historyTaskSelected;
|
||
}
|
||
|
||
if (this.historyNameFilter) {
|
||
params.keyword = this.historyNameFilter;
|
||
}
|
||
|
||
// 重新加载数据,使用当前页和当前设置的排序方式
|
||
axios.get('/history_records', { params })
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.history = response.data.data;
|
||
// 确保总页数更新
|
||
if (this.history.pagination) {
|
||
this.totalPages = parseInt(this.history.pagination.total_pages);
|
||
}
|
||
} else {
|
||
console.error('排序失败:', response.data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('排序请求错误:', error);
|
||
});
|
||
},
|
||
|
||
changePage(page) {
|
||
if (page < 1) page = 1;
|
||
if (page > this.totalPages) page = this.totalPages;
|
||
|
||
// 更新当前页码
|
||
this.historyParams.page = page;
|
||
this.gotoPage = page;
|
||
|
||
// 直接请求新页的数据
|
||
const params = {
|
||
sort_by: this.historyParams.sortBy,
|
||
order: this.historyParams.order,
|
||
page_size: parseInt(this.historyParams.page_size),
|
||
page: page
|
||
};
|
||
|
||
if (this.historyTaskSelected) {
|
||
params.task_name = this.historyTaskSelected;
|
||
}
|
||
|
||
if (this.historyNameFilter) {
|
||
params.keyword = this.historyNameFilter;
|
||
}
|
||
|
||
axios.get('/history_records', { params })
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.history = response.data.data;
|
||
|
||
// 确保页码信息正确反映在UI上
|
||
if (this.history.pagination) {
|
||
this.totalPages = parseInt(this.history.pagination.total_pages);
|
||
}
|
||
} else {
|
||
console.error('Error:', response.data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error changing page:', error);
|
||
});
|
||
},
|
||
|
||
changePageSize() {
|
||
// 重置为第一页
|
||
this.historyParams.page = 1;
|
||
this.gotoPage = 1;
|
||
|
||
// 构建参数
|
||
const params = {
|
||
sort_by: this.historyParams.sortBy,
|
||
order: this.historyParams.order,
|
||
page_size: parseInt(this.historyParams.page_size),
|
||
page: 1
|
||
};
|
||
|
||
if (this.historyTaskSelected) {
|
||
params.task_name = this.historyTaskSelected;
|
||
}
|
||
|
||
if (this.historyNameFilter) {
|
||
params.keyword = this.historyNameFilter;
|
||
}
|
||
|
||
// 重新加载数据
|
||
axios.get('/history_records', { params })
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.history = response.data.data;
|
||
|
||
// 确保页码信息正确反映在UI上
|
||
if (this.history.pagination) {
|
||
this.totalPages = parseInt(this.history.pagination.total_pages);
|
||
}
|
||
} else {
|
||
console.error('Error:', response.data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error changing page size:', error);
|
||
});
|
||
},
|
||
|
||
changePageSizeTo(size) {
|
||
this.historyParams.page_size = size === 'all' ? 99999 : size;
|
||
// 保存用户设置的每页记录数到本地存储
|
||
localStorage.setItem('quarkAutoSave_pageSize', size.toString());
|
||
this.changePageSize();
|
||
},
|
||
|
||
getVisiblePageNumbers() {
|
||
const current = parseInt(this.historyParams.page) || 1;
|
||
const total = parseInt(this.totalPages) || 1;
|
||
const delta = 2; // 当前页左右显示的页码数
|
||
|
||
// 处理特殊情况
|
||
if (total <= 1) return [];
|
||
|
||
let range = [];
|
||
|
||
// 确定显示范围
|
||
let rangeStart = Math.max(2, current - delta);
|
||
let rangeEnd = Math.min(total - 1, current + delta);
|
||
|
||
// 调整范围,确保显示足够的页码
|
||
if (rangeEnd - rangeStart < delta * 2) {
|
||
if (current - rangeStart < delta) {
|
||
// 当前页靠近开始,扩展结束范围
|
||
rangeEnd = Math.min(total - 1, rangeStart + delta * 2);
|
||
} else {
|
||
// 当前页靠近结束,扩展开始范围
|
||
rangeStart = Math.max(2, rangeEnd - delta * 2);
|
||
}
|
||
}
|
||
|
||
// 生成页码数组
|
||
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||
range.push(i);
|
||
}
|
||
|
||
return range;
|
||
},
|
||
isTextTruncated(text, recordIndex, field) {
|
||
if (!this.filteredHistoryRecords[recordIndex]) return false;
|
||
|
||
// 确保文本是字符串类型
|
||
const str = String(text || '');
|
||
|
||
// 先检查文本是否有足够长度,太短的文本肯定不需要展开
|
||
if (str.length <= 20) return false;
|
||
|
||
// 从记录的_isOverflowing属性中获取溢出状态
|
||
const record = this.filteredHistoryRecords[recordIndex];
|
||
|
||
// 如果已经展开了,那么我们认为它可能是需要截断的
|
||
if (record._expandedFields && record._expandedFields.includes(field)) {
|
||
return true;
|
||
}
|
||
|
||
return record._isOverflowing && record._isOverflowing[field];
|
||
},
|
||
toggleExpand(index, field, event) {
|
||
// 阻止事件冒泡,避免触发行选择
|
||
if (event) {
|
||
event.stopPropagation();
|
||
}
|
||
|
||
// 获取当前记录
|
||
const record = this.filteredHistoryRecords[index];
|
||
// 初始化_expandedFields属性(如果不存在)
|
||
if (!record._expandedFields) {
|
||
this.$set(record, '_expandedFields', []);
|
||
}
|
||
|
||
// 检查字段是否已经展开
|
||
const fieldIndex = record._expandedFields.indexOf(field);
|
||
const isExpanding = fieldIndex === -1;
|
||
|
||
// 所有可能需要展开的字段
|
||
const allFields = ['task_name', 'original_name', 'renamed_to'];
|
||
|
||
if (isExpanding) {
|
||
// 如果是展开操作,展开该行所有超长字段
|
||
allFields.forEach(fieldName => {
|
||
// 只有超长的字段才需要展开
|
||
if (this.isTextTruncated(record[fieldName], index, fieldName) &&
|
||
!record._expandedFields.includes(fieldName)) {
|
||
record._expandedFields.push(fieldName);
|
||
}
|
||
});
|
||
} else {
|
||
// 如果是收起操作,收起该行所有字段
|
||
record._expandedFields = [];
|
||
}
|
||
|
||
// 在下一个DOM更新周期中强制重新检测溢出状态
|
||
this.$nextTick(() => {
|
||
// 确保_isOverflowing属性存在
|
||
if (!record._isOverflowing) {
|
||
this.$set(record, '_isOverflowing', {});
|
||
}
|
||
|
||
// 在收起状态下,通过设置为true确保展开按钮依然可见
|
||
allFields.forEach(fieldName => {
|
||
if (!isExpanding) {
|
||
this.$set(record._isOverflowing, fieldName, true);
|
||
}
|
||
});
|
||
});
|
||
},
|
||
loadAllTaskNames() {
|
||
// 向后端请求获取所有可用的任务名称
|
||
axios.get('/history_records', {
|
||
params: {
|
||
get_all_task_names: true,
|
||
page_size: 1, // 只需要任务名称,不需要大量记录
|
||
page: 1
|
||
}
|
||
}).then(response => {
|
||
if (response.data.success && response.data.data.all_task_names) {
|
||
this.allTaskNames = this.sortTaskNamesByPinyin(response.data.data.all_task_names);
|
||
} else {
|
||
// 如果API失败,回退到从当前页记录中提取任务名称
|
||
if (this.history.records && this.history.records.length > 0) {
|
||
const taskNames = new Set();
|
||
this.history.records.forEach(record => {
|
||
if (record.task_name) {
|
||
taskNames.add(record.task_name);
|
||
}
|
||
});
|
||
this.allTaskNames = this.sortTaskNamesByPinyin([...taskNames]);
|
||
}
|
||
}
|
||
}).catch(error => {
|
||
// 如果API请求出错,回退到从当前页记录中提取任务名称
|
||
if (this.history.records && this.history.records.length > 0) {
|
||
const taskNames = new Set();
|
||
this.history.records.forEach(record => {
|
||
if (record.task_name) {
|
||
taskNames.add(record.task_name);
|
||
}
|
||
});
|
||
this.allTaskNames = this.sortTaskNamesByPinyin([...taskNames]);
|
||
}
|
||
});
|
||
},
|
||
loadTaskLatestInfo() {
|
||
// 获取所有任务的最新转存信息(包括日期和文件)
|
||
axios.get('/task_latest_info')
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.taskLatestRecords = response.data.data.latest_records;
|
||
this.taskLatestFiles = response.data.data.latest_files;
|
||
} else {
|
||
console.error('获取任务最新信息失败:', response.data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取任务最新信息失败:', error);
|
||
});
|
||
},
|
||
getTaskLatestRecordDisplay(taskName) {
|
||
// 获取任务最新记录的显示文本
|
||
const latestRecord = this.taskLatestRecords[taskName];
|
||
return latestRecord ? latestRecord.display : '';
|
||
},
|
||
isTaskUpdatedToday(taskName) {
|
||
// 检查任务是否在今天更新
|
||
const latestRecord = this.taskLatestRecords[taskName];
|
||
if (!latestRecord || !latestRecord.full) {
|
||
return false;
|
||
}
|
||
|
||
// 获取今天的完整日期,格式为 YYYY-MM-DD
|
||
const today = new Date();
|
||
const todayFormatted = today.getFullYear() + '-' +
|
||
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
||
String(today.getDate()).padStart(2, '0');
|
||
|
||
return latestRecord.full === todayFormatted;
|
||
},
|
||
isRecordUpdatedToday(record) {
|
||
// 检查转存记录是否在今天更新
|
||
if (!record || !record.transfer_time_readable) {
|
||
return false;
|
||
}
|
||
|
||
// 获取今天的日期,格式为 YYYY-MM-DD
|
||
const today = new Date();
|
||
const todayFormatted = today.getFullYear() + '-' +
|
||
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
||
String(today.getDate()).padStart(2, '0');
|
||
|
||
// 从 transfer_time_readable 中提取日期部分(格式通常为 "YYYY-MM-DD HH:MM:SS")
|
||
const recordDate = record.transfer_time_readable.split(' ')[0];
|
||
|
||
return recordDate === todayFormatted;
|
||
},
|
||
isFileUpdatedToday(file) {
|
||
// 检查文件是否在今天更新(基于修改日期)
|
||
if (!file || !file.updated_at) {
|
||
return false;
|
||
}
|
||
|
||
// 获取今天的日期,格式为 YYYY-MM-DD
|
||
const today = new Date();
|
||
const todayFormatted = today.getFullYear() + '-' +
|
||
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
||
String(today.getDate()).padStart(2, '0');
|
||
|
||
// 使用与 formatDate 方法相同的逻辑处理时间戳
|
||
try {
|
||
const fileDate = new Date(file.updated_at);
|
||
const fileDateFormatted = fileDate.getFullYear() + '-' +
|
||
String(fileDate.getMonth() + 1).padStart(2, '0') + '-' +
|
||
String(fileDate.getDate()).padStart(2, '0');
|
||
|
||
return fileDateFormatted === todayFormatted;
|
||
} catch (error) {
|
||
console.error('处理文件时间戳时出错:', error, file.updated_at);
|
||
return false;
|
||
}
|
||
},
|
||
shouldShowTodayIndicator() {
|
||
// 检查是否应该显示当日更新图标
|
||
return this.formData.button_display.today_update_indicator !== 'disabled';
|
||
},
|
||
getTodayIndicatorClass() {
|
||
// 获取当日更新图标的CSS类
|
||
if (this.formData.button_display.today_update_indicator === 'hover') {
|
||
return 'hover-only';
|
||
}
|
||
return '';
|
||
},
|
||
openDatePicker(index) {
|
||
// 使用$refs访问对应的日期选择器并打开它
|
||
const dateRef = this.$refs[`enddate_${index}`];
|
||
if (dateRef && dateRef[0]) {
|
||
// 由于Vue的ref在v-for中会返回一个数组,所以我们需要访问第一个元素
|
||
dateRef[0].showPicker();
|
||
|
||
// 点击按钮后立即使按钮失去焦点
|
||
document.activeElement.blur();
|
||
}
|
||
},
|
||
toggleCloudSaverPassword() {
|
||
this.showCloudSaverPassword = !this.showCloudSaverPassword;
|
||
},
|
||
toggleWebuiPassword() {
|
||
this.showWebuiPassword = !this.showWebuiPassword;
|
||
},
|
||
togglePageWidth() {
|
||
// 循环切换页面宽度模式:窄 -> 中 -> 宽 -> 窄
|
||
if (this.pageWidthMode === 'narrow') {
|
||
this.pageWidthMode = 'medium';
|
||
} else if (this.pageWidthMode === 'medium') {
|
||
this.pageWidthMode = 'wide';
|
||
} else {
|
||
this.pageWidthMode = 'narrow';
|
||
}
|
||
// 保存用户选择到本地存储
|
||
localStorage.setItem('quarkAutoSave_pageWidthMode', this.pageWidthMode);
|
||
// 应用页面宽度类
|
||
document.body.classList.remove('page-width-narrow', 'page-width-medium', 'page-width-wide');
|
||
document.body.classList.add('page-width-' + this.pageWidthMode);
|
||
},
|
||
showToast(message) {
|
||
this.toastMessage = message;
|
||
$(this.$refs.toast).toast('show');
|
||
},
|
||
checkPreviewScrollbar() {
|
||
// 检查命名预览界面的滚动条状态
|
||
const fileSelectModal = document.getElementById('fileSelectModal');
|
||
if (fileSelectModal && fileSelectModal.getAttribute('data-modal-type') === 'preview') {
|
||
// 查找命名预览模式的内容容器
|
||
const contentContainers = fileSelectModal.querySelectorAll('.modal-body > div:not(.alert-warning)');
|
||
|
||
contentContainers.forEach(container => {
|
||
// 检查是否有水平滚动条(滚动宽度大于客户端宽度)
|
||
if (container.scrollWidth > container.clientWidth) {
|
||
container.classList.add('has-scrollbar');
|
||
} else {
|
||
container.classList.remove('has-scrollbar');
|
||
}
|
||
});
|
||
}
|
||
},
|
||
isModalTextTruncated(text, fileIndex, field) {
|
||
if (!this.fileSelect.fileList[fileIndex]) return false;
|
||
|
||
// 确保文本是字符串类型
|
||
const str = String(text || '');
|
||
|
||
// 先检查文本是否有足够长度,太短的文本肯定不需要展开
|
||
if (str.length <= 20) return false;
|
||
|
||
// 从文件的_isOverflowing属性中获取溢出状态
|
||
const file = this.fileSelect.fileList[fileIndex];
|
||
|
||
// 如果已经展开了,那么我们认为它可能是需要截断的
|
||
if (file._expandedFields && file._expandedFields.includes(field)) {
|
||
return true;
|
||
}
|
||
|
||
return file._isOverflowing && file._isOverflowing[field];
|
||
},
|
||
toggleModalExpand(index, field) {
|
||
// 获取当前文件
|
||
const file = this.fileSelect.fileList[index];
|
||
// 初始化_expandedFields属性(如果不存在)
|
||
if (!file._expandedFields) {
|
||
this.$set(file, '_expandedFields', []);
|
||
}
|
||
|
||
// 检查字段是否已经展开
|
||
const fieldIndex = file._expandedFields.indexOf(field);
|
||
const isExpanding = fieldIndex === -1;
|
||
|
||
if (isExpanding) {
|
||
// 如果是展开操作,添加字段到展开列表
|
||
file._expandedFields.push(field);
|
||
} else {
|
||
// 如果是收起操作,从展开列表中移除字段
|
||
file._expandedFields.splice(fieldIndex, 1);
|
||
}
|
||
|
||
// 在下一个DOM更新周期中强制重新检测溢出状态
|
||
this.$nextTick(() => {
|
||
// 确保_isOverflowing属性存在
|
||
if (!file._isOverflowing) {
|
||
this.$set(file, '_isOverflowing', {});
|
||
}
|
||
|
||
// 在收起状态下,通过设置为true确保展开按钮依然可见
|
||
if (!isExpanding) {
|
||
this.$set(file._isOverflowing, field, true);
|
||
}
|
||
|
||
// 强制更新视图
|
||
this.$forceUpdate();
|
||
});
|
||
|
||
// 阻止事件冒泡,避免触发行点击事件
|
||
event.stopPropagation();
|
||
},
|
||
// 文件选择模态框的排序方法
|
||
sortFileList(field) {
|
||
// 切换排序方向
|
||
if (this.fileSelect.sortBy === field) {
|
||
this.fileSelect.sortOrder = this.fileSelect.sortOrder === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
this.fileSelect.sortBy = field;
|
||
// 默认降序(除了文件名外)
|
||
this.fileSelect.sortOrder = field === 'file_name' ? 'asc' : 'desc';
|
||
}
|
||
|
||
// 按选定字段和顺序对文件列表进行排序
|
||
this.fileSelect.fileList.sort((a, b) => {
|
||
if (field === 'file_name') {
|
||
// 检查当前模态框类型,选择起始文件模态框使用全局文件排序函数
|
||
const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
|
||
if (modalType === 'start-file') {
|
||
// 选择起始文件模态框:按过滤状态和类型排序
|
||
// 1. 检查过滤状态 - 包括没有file_name_re字段的情况
|
||
const aFiltered = !a.file_name_re || a.file_name_re === '×' || a.file_name_re.startsWith('×');
|
||
const bFiltered = !b.file_name_re || b.file_name_re === '×' || b.file_name_re.startsWith('×');
|
||
|
||
// 无论升序还是降序,未被过滤的项目都排在前面,被过滤的项目排在后面
|
||
if (!aFiltered && bFiltered) return -1;
|
||
if (aFiltered && !bFiltered) return 1;
|
||
|
||
// 2. 在同一过滤状态内,文件夹排在文件前面
|
||
if (a.dir && !b.dir) return -1;
|
||
if (!a.dir && b.dir) return 1;
|
||
|
||
// 使用全局文件排序函数
|
||
const ka = sortFileByName(a), kb = sortFileByName(b);
|
||
for (let i = 0; i < ka.length; ++i) {
|
||
if (ka[i] !== kb[i]) return this.fileSelect.sortOrder === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
|
||
}
|
||
return 0;
|
||
} else {
|
||
// 其他模态框:文件夹排在文件前面
|
||
if (a.dir && !b.dir) return -1;
|
||
if (!a.dir && b.dir) return 1;
|
||
// 其他模态框:使用拼音排序
|
||
let aValue = pinyinPro.pinyin(a.file_name, { toneType: 'none', type: 'string' }).toLowerCase();
|
||
let bValue = pinyinPro.pinyin(b.file_name, { toneType: 'none', type: 'string' }).toLowerCase();
|
||
if (this.fileSelect.sortOrder === 'asc') {
|
||
return aValue > bValue ? 1 : -1;
|
||
} else {
|
||
return aValue < bValue ? 1 : -1;
|
||
}
|
||
}
|
||
}
|
||
if (field === 'file_name_re') {
|
||
const aHasValidRename = a.file_name_re && a.file_name_re !== '×' && !a.file_name_re.startsWith('×');
|
||
const bHasValidRename = b.file_name_re && b.file_name_re !== '×' && !b.file_name_re.startsWith('×');
|
||
|
||
// 有效重命名的项目排在前面
|
||
if (aHasValidRename && !bHasValidRename) return -1;
|
||
if (!aHasValidRename && bHasValidRename) return 1;
|
||
|
||
// 对于有效重命名的项目,按重命名结果排序
|
||
if (aHasValidRename && bHasValidRename) {
|
||
// 同类型内,文件夹排在文件前面
|
||
if (a.dir && !b.dir) return -1;
|
||
if (!a.dir && b.dir) return 1;
|
||
|
||
let aValue, bValue;
|
||
// 对于重命名列,优先使用episode_number进行数值排序(如果存在)
|
||
if (a.episode_number !== undefined && b.episode_number !== undefined) {
|
||
// 确保进行数值比较
|
||
aValue = parseInt(a.episode_number, 10);
|
||
bValue = parseInt(b.episode_number, 10);
|
||
|
||
// 如果解析失败,回退到字符串比较
|
||
if (isNaN(aValue) || isNaN(bValue)) {
|
||
aValue = String(a.episode_number);
|
||
bValue = String(b.episode_number);
|
||
}
|
||
} else {
|
||
// 否则使用重命名后的文件名进行拼音排序
|
||
aValue = pinyinPro.pinyin(a.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
|
||
bValue = pinyinPro.pinyin(b.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
|
||
}
|
||
|
||
if (this.fileSelect.sortOrder === 'asc') {
|
||
return aValue > bValue ? 1 : -1;
|
||
} else {
|
||
return aValue < bValue ? 1 : -1;
|
||
}
|
||
}
|
||
|
||
// 对于无效重命名的项目,按照特定优先级排序
|
||
if (!aHasValidRename && !bHasValidRename) {
|
||
// 定义无效重命名的优先级(数字越小优先级越高)
|
||
const getInvalidRenamePriority = (item) => {
|
||
const renameResult = item.file_name_re || '×';
|
||
if (renameResult.includes('无法识别剧集编号')) return 1;
|
||
if (renameResult === '×') return 2;
|
||
return 3; // 其他无效重命名
|
||
};
|
||
|
||
const aPriority = getInvalidRenamePriority(a);
|
||
const bPriority = getInvalidRenamePriority(b);
|
||
|
||
// 如果优先级不同,按优先级排序
|
||
if (aPriority !== bPriority) {
|
||
return aPriority - bPriority;
|
||
}
|
||
|
||
// 同一优先级内,文件夹排在文件前面
|
||
if (a.dir && !b.dir) return -1;
|
||
if (!a.dir && b.dir) return 1;
|
||
|
||
// 同一优先级且同一类型内,使用智能排序
|
||
const ka = sortFileByName(a), kb = sortFileByName(b);
|
||
for (let i = 0; i < ka.length; ++i) {
|
||
if (ka[i] !== kb[i]) return this.fileSelect.sortOrder === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
|
||
}
|
||
return 0;
|
||
}
|
||
return 0;
|
||
} else if (field === 'size') {
|
||
// 文件夹始终在前
|
||
if (a.dir && !b.dir) return -1;
|
||
if (!a.dir && b.dir) return 1;
|
||
|
||
// 文件夹按项目数量排序,文件按大小排序
|
||
let aValue = a.dir ? (a.include_items || 0) : (a.size || 0);
|
||
let bValue = b.dir ? (b.include_items || 0) : (b.size || 0);
|
||
if (this.fileSelect.sortOrder === 'asc') {
|
||
return aValue > bValue ? 1 : -1;
|
||
} else {
|
||
return aValue < bValue ? 1 : -1;
|
||
}
|
||
} else if (field === 'updated_at') {
|
||
// 修改日期排序严格按照修改日期,不区分文件夹和文件
|
||
let aValue = a.updated_at || 0;
|
||
let bValue = b.updated_at || 0;
|
||
if (this.fileSelect.sortOrder === 'asc') {
|
||
return aValue > bValue ? 1 : -1;
|
||
} else {
|
||
return aValue < bValue ? 1 : -1;
|
||
}
|
||
}
|
||
});
|
||
},
|
||
manualSortFileList(field, order) {
|
||
this.fileSelect.fileList.sort((a, b) => {
|
||
if (field === 'file_name') {
|
||
// 检查当前模态框类型,选择起始文件模态框使用特殊排序逻辑
|
||
const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
|
||
if (modalType === 'start-file') {
|
||
// 选择起始文件模态框:按过滤状态和类型排序
|
||
// 1. 检查过滤状态 - 包括没有file_name_re字段的情况
|
||
const aFiltered = !a.file_name_re || a.file_name_re === '×' || a.file_name_re.startsWith('×');
|
||
const bFiltered = !b.file_name_re || b.file_name_re === '×' || b.file_name_re.startsWith('×');
|
||
|
||
// 无论升序还是降序,未被过滤的项目都排在前面,被过滤的项目排在后面
|
||
if (!aFiltered && bFiltered) return -1;
|
||
if (aFiltered && !bFiltered) return 1;
|
||
|
||
// 2. 在同一过滤状态内,文件夹排在文件前面
|
||
if (a.dir && !b.dir) return -1;
|
||
if (!a.dir && b.dir) return 1;
|
||
|
||
// 使用全局文件排序函数
|
||
const ka = sortFileByName(a), kb = sortFileByName(b);
|
||
for (let i = 0; i < ka.length; ++i) {
|
||
if (ka[i] !== kb[i]) return order === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
|
||
}
|
||
return 0;
|
||
} else {
|
||
// 其他模态框:文件夹排在文件前面
|
||
if (a.dir && !b.dir) return -1;
|
||
if (!a.dir && b.dir) return 1;
|
||
// 使用智能排序函数
|
||
const ka = sortFileByName(a), kb = sortFileByName(b);
|
||
for (let i = 0; i < ka.length; ++i) {
|
||
if (ka[i] !== kb[i]) return order === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
|
||
}
|
||
return 0;
|
||
}
|
||
}
|
||
if (field === 'file_name_re') {
|
||
const aHasValidRename = a.file_name_re && a.file_name_re !== '×' && !a.file_name_re.startsWith('×');
|
||
const bHasValidRename = b.file_name_re && b.file_name_re !== '×' && !b.file_name_re.startsWith('×');
|
||
|
||
// 有效重命名的项目排在前面
|
||
if (aHasValidRename && !bHasValidRename) return -1;
|
||
if (!aHasValidRename && bHasValidRename) return 1;
|
||
|
||
// 对于有效重命名的项目,按重命名结果排序
|
||
if (aHasValidRename && bHasValidRename) {
|
||
// 同类型内,文件夹排在文件前面
|
||
if (a.dir && !b.dir) return -1;
|
||
if (!a.dir && b.dir) return 1;
|
||
|
||
let aValue, bValue;
|
||
// 对于重命名列,优先使用episode_number进行数值排序(如果存在)
|
||
if (a.episode_number !== undefined && b.episode_number !== undefined) {
|
||
// 确保进行数值比较
|
||
aValue = parseInt(a.episode_number, 10);
|
||
bValue = parseInt(b.episode_number, 10);
|
||
|
||
// 如果解析失败,回退到字符串比较
|
||
if (isNaN(aValue) || isNaN(bValue)) {
|
||
aValue = String(a.episode_number);
|
||
bValue = String(b.episode_number);
|
||
}
|
||
} else {
|
||
// 否则使用重命名后的文件名进行拼音排序
|
||
aValue = pinyinPro.pinyin(a.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
|
||
bValue = pinyinPro.pinyin(b.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
|
||
}
|
||
|
||
if (order === 'asc') {
|
||
return aValue > bValue ? 1 : -1;
|
||
} else {
|
||
return aValue < bValue ? 1 : -1;
|
||
}
|
||
}
|
||
|
||
// 对于无效重命名的项目,按照特定优先级排序
|
||
if (!aHasValidRename && !bHasValidRename) {
|
||
// 定义无效重命名的优先级(数字越小优先级越高)
|
||
const getInvalidRenamePriority = (item) => {
|
||
const renameResult = item.file_name_re || '×';
|
||
if (renameResult.includes('无法识别剧集编号')) return 1;
|
||
if (renameResult === '×') return 2;
|
||
return 3; // 其他无效重命名
|
||
};
|
||
|
||
const aPriority = getInvalidRenamePriority(a);
|
||
const bPriority = getInvalidRenamePriority(b);
|
||
|
||
// 如果优先级不同,按优先级排序
|
||
if (aPriority !== bPriority) {
|
||
return aPriority - bPriority;
|
||
}
|
||
|
||
// 同一优先级内,文件夹排在文件前面
|
||
if (a.dir && !b.dir) return -1;
|
||
if (!a.dir && b.dir) return 1;
|
||
|
||
// 同一优先级且同一类型内,使用智能排序
|
||
const ka = sortFileByName(a), kb = sortFileByName(b);
|
||
for (let i = 0; i < ka.length; ++i) {
|
||
if (ka[i] !== kb[i]) return order === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
|
||
}
|
||
return 0;
|
||
}
|
||
return 0;
|
||
} else if (field === 'size') {
|
||
// 文件夹始终在前
|
||
if (a.dir && !b.dir) return -1;
|
||
if (!a.dir && b.dir) return 1;
|
||
|
||
// 文件夹按项目数量排序,文件按大小排序
|
||
let aValue = a.dir ? (a.include_items || 0) : (a.size || 0);
|
||
let bValue = b.dir ? (b.include_items || 0) : (b.size || 0);
|
||
if (order === 'asc') {
|
||
return aValue > bValue ? 1 : -1;
|
||
} else {
|
||
return aValue < bValue ? 1 : -1;
|
||
}
|
||
} else if (field === 'updated_at') {
|
||
// 修改日期排序严格按照修改日期,不区分文件夹和文件
|
||
let aValue = a.updated_at || 0;
|
||
let bValue = b.updated_at || 0;
|
||
if (order === 'asc') {
|
||
return aValue > bValue ? 1 : -1;
|
||
} else {
|
||
return aValue < bValue ? 1 : -1;
|
||
}
|
||
}
|
||
});
|
||
},
|
||
validateNumberInput(event, field, max) {
|
||
// 获取当前输入值
|
||
let value = event.target.value;
|
||
// 获取输入框的当前光标位置
|
||
const cursorPosition = event.target.selectionStart;
|
||
|
||
// 记录原始长度
|
||
const originalLength = value.length;
|
||
|
||
// 移除非数字字符
|
||
const cleanValue = value.replace(/[^\d]/g, '');
|
||
|
||
// 如果有非数字字符被移除
|
||
if (cleanValue !== value) {
|
||
// 计算移除了多少个字符
|
||
const diff = originalLength - cleanValue.length;
|
||
|
||
// 更新输入框的值
|
||
event.target.value = cleanValue;
|
||
|
||
// 调整光标位置(考虑到字符被移除)
|
||
setTimeout(() => {
|
||
event.target.setSelectionRange(Math.max(0, cursorPosition - diff), Math.max(0, cursorPosition - diff));
|
||
}, 0);
|
||
}
|
||
|
||
// 确保不超过最大值
|
||
if (cleanValue !== '' && parseInt(cleanValue) > max) {
|
||
event.target.value = max.toString();
|
||
}
|
||
|
||
// 更新数据模型,如果为空则默认为0
|
||
this.formData[field] = event.target.value === '' ? 0 : parseInt(event.target.value);
|
||
},
|
||
selectRecord(event, recordId) {
|
||
// 获取当前记录的索引
|
||
const currentIndex = this.filteredHistoryRecords.findIndex(record => record.id === recordId);
|
||
if (currentIndex === -1) return;
|
||
|
||
// 如果是Shift+点击,选择范围
|
||
if (event.shiftKey && this.selectedRecords.length > 0) {
|
||
// 找出所有已选中记录的索引
|
||
const selectedIndices = this.selectedRecords.map(id =>
|
||
this.filteredHistoryRecords.findIndex(record => record.id === id)
|
||
).filter(index => index !== -1); // 过滤掉未找到的记录
|
||
|
||
if (selectedIndices.length > 0) {
|
||
// 找出已选中记录中最靠前的索引
|
||
const earliestSelectedIndex = Math.min(...selectedIndices);
|
||
// 确定最终的选择范围
|
||
const startIndex = Math.min(earliestSelectedIndex, currentIndex);
|
||
const endIndex = Math.max(earliestSelectedIndex, currentIndex);
|
||
|
||
// 获取新的选择范围内所有记录的ID
|
||
this.selectedRecords = this.filteredHistoryRecords
|
||
.slice(startIndex, endIndex + 1)
|
||
.map(record => record.id);
|
||
} else {
|
||
// 如果没有有效的选中记录(可能是由于列表刷新),则只选择当前记录
|
||
this.selectedRecords = [recordId];
|
||
}
|
||
}
|
||
// 如果是Ctrl/Cmd+点击,切换单个记录选择状态
|
||
else if (event.ctrlKey || event.metaKey) {
|
||
if (this.selectedRecords.includes(recordId)) {
|
||
this.selectedRecords = this.selectedRecords.filter(id => id !== recordId);
|
||
} else {
|
||
this.selectedRecords.push(recordId);
|
||
}
|
||
}
|
||
// 普通点击,清除当前选择并选择当前记录
|
||
else {
|
||
if (this.selectedRecords.length === 1 && this.selectedRecords.includes(recordId)) {
|
||
this.selectedRecords = [];
|
||
} else {
|
||
this.selectedRecords = [recordId];
|
||
}
|
||
}
|
||
|
||
// 更新最后选择的记录索引,只有在有选择记录时才更新
|
||
if (this.selectedRecords.length > 0) {
|
||
this.lastSelectedRecordIndex = currentIndex;
|
||
} else {
|
||
this.lastSelectedRecordIndex = -1;
|
||
}
|
||
},
|
||
deleteSelectedRecords() {
|
||
if (this.selectedRecords.length === 0) return;
|
||
|
||
if (confirm(`确定要删除选中的 ${this.selectedRecords.length} 条记录吗?`)) {
|
||
axios.post('/delete_history_records', {
|
||
record_ids: this.selectedRecords
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.showToast(response.data.message);
|
||
// 重新加载记录
|
||
this.loadHistoryRecords();
|
||
// 清空选择
|
||
this.selectedRecords = [];
|
||
this.lastSelectedRecordIndex = -1;
|
||
} else {
|
||
alert(response.data.message || '删除失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('删除记录时出错:', error);
|
||
alert('删除记录时出错: ' + (error.response?.data?.message || error.message));
|
||
});
|
||
}
|
||
},
|
||
deleteRecord(id, original_name) {
|
||
// 检查是否有多条记录被选中
|
||
if (this.selectedRecords.length > 1) {
|
||
// 如果有多条记录被选中,调用批量删除方法
|
||
if (confirm(`确定要删除选中的 ${this.selectedRecords.length} 条记录吗?`)) {
|
||
axios.post('/delete_history_records', {
|
||
record_ids: this.selectedRecords
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.showToast(`成功删除 ${this.selectedRecords.length} 条记录`);
|
||
// 重新加载记录
|
||
this.loadHistoryRecords();
|
||
// 清空选择
|
||
this.selectedRecords = [];
|
||
this.lastSelectedRecordIndex = -1;
|
||
} else {
|
||
alert(response.data.message || '删除失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('删除记录时出错:', error);
|
||
alert('删除记录时出错: ' + (error.response?.data?.message || error.message));
|
||
});
|
||
}
|
||
} else {
|
||
// 单条记录删除
|
||
if (confirm(`确定要删除此条记录吗?`)) {
|
||
axios.post('/delete_history_record', {
|
||
id: id
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.showToast('成功删除 1 条记录');
|
||
// 重新加载记录
|
||
this.loadHistoryRecords();
|
||
// 清空选择
|
||
this.selectedRecords = [];
|
||
this.lastSelectedRecordIndex = -1;
|
||
} else {
|
||
alert(response.data.message || '删除失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('删除记录时出错:', error);
|
||
alert('删除记录时出错: ' + (error.response?.data?.message || error.message));
|
||
});
|
||
}
|
||
}
|
||
},
|
||
// 处理点击表格外区域的事件
|
||
handleOutsideClick(event) {
|
||
// 如果当前不是历史记录页面或者没有选中的记录,则不处理
|
||
if (this.activeTab !== 'history' || this.selectedRecords.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 检查点击是否在表格内
|
||
const tableElement = document.querySelector('.table.selectable-records');
|
||
// 检查点击是否在分页控制区域
|
||
const paginationElement = document.querySelector('.pagination-container');
|
||
|
||
// 如果点击不在表格内且不在分页控制区域内,则清除选择
|
||
if (tableElement && !tableElement.contains(event.target) &&
|
||
paginationElement && !paginationElement.contains(event.target)) {
|
||
this.selectedRecords = [];
|
||
this.lastSelectedRecordIndex = -1;
|
||
}
|
||
},
|
||
// 处理重命名编辑状态的外部点击事件
|
||
handleRenameOutsideClick(event) {
|
||
// 如果当前不是文件整理页面,则不处理
|
||
if (this.activeTab !== 'filemanager') {
|
||
return;
|
||
}
|
||
|
||
// 检查是否有文件正在编辑状态
|
||
const editingFile = this.fileManager.fileList.find(file => file._editing);
|
||
if (!editingFile) {
|
||
return;
|
||
}
|
||
|
||
// 检查点击是否在重命名按钮上
|
||
const renameButton = event.target.closest('.rename-record-btn');
|
||
if (renameButton) {
|
||
return; // 点击在重命名按钮上,不处理
|
||
}
|
||
|
||
// 点击在任何地方(包括输入框内),都保存重命名并关闭输入框
|
||
this.saveRenameFile(editingFile);
|
||
|
||
// 阻止事件继续传播,避免与其他点击事件处理器冲突
|
||
event.stopPropagation();
|
||
},
|
||
selectFileItem(event, fileId) {
|
||
// 如果是在预览模式或选择分享模式,不允许选择
|
||
if (this.fileSelect.previewRegex || this.fileSelect.selectShare) return;
|
||
|
||
// 获取当前文件的索引
|
||
const currentIndex = this.fileSelect.fileList.findIndex(file => file.fid === fileId);
|
||
if (currentIndex === -1) return;
|
||
|
||
// 如果是Shift+点击,选择范围
|
||
if (event.shiftKey && this.fileSelect.selectedFiles.length > 0) {
|
||
// 找出所有已选中文件的索引
|
||
const selectedIndices = this.fileSelect.selectedFiles.map(id =>
|
||
this.fileSelect.fileList.findIndex(file => file.fid === id)
|
||
).filter(index => index !== -1); // 过滤掉未找到的文件
|
||
|
||
if (selectedIndices.length > 0) {
|
||
// 找出已选中文件中最靠前的索引
|
||
const earliestSelectedIndex = Math.min(...selectedIndices);
|
||
// 确定最终的选择范围
|
||
const startIndex = Math.min(earliestSelectedIndex, currentIndex);
|
||
const endIndex = Math.max(earliestSelectedIndex, currentIndex);
|
||
|
||
// 获取范围内所有文件的ID(排除文件夹)
|
||
this.fileSelect.selectedFiles = this.fileSelect.fileList
|
||
.slice(startIndex, endIndex + 1)
|
||
.filter(file => !file.dir) // 只选择文件,不选择文件夹
|
||
.map(file => file.fid);
|
||
} else {
|
||
// 如果没有有效的选中文件(可能是由于列表刷新),则只选择当前文件
|
||
const file = this.fileSelect.fileList[currentIndex];
|
||
if (!file.dir) { // 不选择文件夹
|
||
this.fileSelect.selectedFiles = [fileId];
|
||
}
|
||
}
|
||
}
|
||
// 如果是Ctrl/Cmd+点击,切换单个文件选择状态
|
||
else if (event.ctrlKey || event.metaKey) {
|
||
const file = this.fileSelect.fileList[currentIndex];
|
||
if (file.dir) return; // 不允许选择文件夹
|
||
|
||
if (this.fileSelect.selectedFiles.includes(fileId)) {
|
||
this.fileSelect.selectedFiles = this.fileSelect.selectedFiles.filter(id => id !== fileId);
|
||
} else {
|
||
this.fileSelect.selectedFiles.push(fileId);
|
||
}
|
||
}
|
||
// 普通点击,清除当前选择并选择当前文件
|
||
else {
|
||
const file = this.fileSelect.fileList[currentIndex];
|
||
if (file.dir) return; // 不允许选择文件夹
|
||
|
||
if (this.fileSelect.selectedFiles.length === 1 && this.fileSelect.selectedFiles.includes(fileId)) {
|
||
this.fileSelect.selectedFiles = [];
|
||
} else {
|
||
this.fileSelect.selectedFiles = [fileId];
|
||
}
|
||
}
|
||
|
||
// 更新最后选择的文件索引,只有在有选择文件时才更新
|
||
if (this.fileSelect.selectedFiles.length > 0) {
|
||
this.fileSelect.lastSelectedFileIndex = currentIndex;
|
||
} else {
|
||
this.fileSelect.lastSelectedFileIndex = -1;
|
||
}
|
||
|
||
// 阻止事件冒泡
|
||
event.stopPropagation();
|
||
},
|
||
deleteSelectedFiles(clickedFid, clickedFname, isDir, deleteRecords = false) {
|
||
// 如果是文件夹或者没有选中的文件,则按原来的方式删除单个文件
|
||
if (isDir || this.fileSelect.selectedFiles.length === 0) {
|
||
this.deleteFileForSelect(clickedFid, clickedFname, isDir, deleteRecords);
|
||
return;
|
||
}
|
||
|
||
// 如果点击的文件不在选中列表中,也按原来的方式删除单个文件
|
||
if (!this.fileSelect.selectedFiles.includes(clickedFid)) {
|
||
this.deleteFileForSelect(clickedFid, clickedFname, isDir, deleteRecords);
|
||
return;
|
||
}
|
||
|
||
// 多选删除
|
||
const selectedCount = this.fileSelect.selectedFiles.length;
|
||
|
||
// 根据选中数量和是否删除记录使用不同的确认提示
|
||
let confirmMessage = '';
|
||
if (deleteRecords) {
|
||
confirmMessage = selectedCount === 1
|
||
? `确定要删除此项目及其关联记录吗?`
|
||
: `确定要删除选中的 ${selectedCount} 个项目及其关联记录吗?`;
|
||
} else {
|
||
confirmMessage = selectedCount === 1
|
||
? `确定要删除此项目吗?`
|
||
: `确定要删除选中的 ${selectedCount} 个项目吗?`;
|
||
}
|
||
|
||
if (confirm(confirmMessage)) {
|
||
// 获取当前路径作为save_path参数
|
||
let save_path = "";
|
||
if (this.fileSelect && this.fileSelect.paths) {
|
||
save_path = this.fileSelect.paths.map(item => item.name).join("/");
|
||
}
|
||
|
||
// 创建一个Promise数组来处理所有删除请求
|
||
const deletePromises = this.fileSelect.selectedFiles.map(fid => {
|
||
// 查找对应的文件对象,获取文件名
|
||
const fileObj = this.fileSelect.fileList.find(file => file.fid === fid);
|
||
const fileName = fileObj ? fileObj.file_name : '';
|
||
|
||
return axios.post('/delete_file', { fid: fid, file_name: fileName, delete_records: deleteRecords, save_path: save_path })
|
||
.then(response => {
|
||
return { fid: fid, success: response.data.code === 0, deleted_records: response.data.deleted_records || 0 };
|
||
})
|
||
.catch(error => {
|
||
return { fid: fid, success: false, deleted_records: 0 };
|
||
});
|
||
});
|
||
|
||
// 等待所有删除请求完成
|
||
Promise.all(deletePromises)
|
||
.then(results => {
|
||
// 统计成功和失败的数量
|
||
const successCount = results.filter(r => r.success).length;
|
||
const failCount = results.length - successCount;
|
||
// 统计删除的记录数
|
||
const totalDeletedRecords = results.reduce((sum, r) => sum + (r.deleted_records || 0), 0);
|
||
|
||
// 从文件列表中移除成功删除的文件
|
||
const successfullyDeletedFids = results.filter(r => r.success).map(r => r.fid);
|
||
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => !successfullyDeletedFids.includes(item.fid));
|
||
|
||
// 清空选中文件列表
|
||
this.fileSelect.selectedFiles = [];
|
||
this.fileSelect.lastSelectedFileIndex = -1;
|
||
|
||
// 显示结果
|
||
if (failCount > 0) {
|
||
alert(`成功删除 ${successCount} 个项目,${failCount} 个项目删除失败`);
|
||
} else {
|
||
// 根据是否删除记录显示不同的消息
|
||
if (deleteRecords) {
|
||
this.showToast(`成功删除 ${successCount} 个项目${totalDeletedRecords > 0 ? `及其关联的 ${totalDeletedRecords} 条记录` : ''}`);
|
||
} else {
|
||
this.showToast(`成功删除 ${successCount} 个项目`);
|
||
}
|
||
}
|
||
|
||
// 如果同时删除了记录,无论当前在哪个页面,都刷新历史记录
|
||
if (deleteRecords) {
|
||
this.loadHistoryRecords();
|
||
}
|
||
});
|
||
}
|
||
},
|
||
handleModalOutsideClick(event) {
|
||
// 如果当前不是文件选择模式或者没有选中的文件,则不处理
|
||
if (this.fileSelect.previewRegex || this.fileSelect.selectShare || this.fileSelect.selectedFiles.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 检查点击是否在表格内
|
||
const tableElement = document.querySelector('#fileSelectModal .table');
|
||
|
||
// 如果点击不在表格内,则清除选择
|
||
if (tableElement && !tableElement.contains(event.target)) {
|
||
this.fileSelect.selectedFiles = [];
|
||
this.fileSelect.lastSelectedFileIndex = -1;
|
||
}
|
||
},
|
||
preventTextSelection(event, isDir) {
|
||
// 如果是文件夹,不阻止默认行为
|
||
if (isDir) return;
|
||
|
||
// 如果是Shift点击或Ctrl/Cmd点击,阻止文本选择
|
||
if (event.shiftKey || event.ctrlKey || event.metaKey) {
|
||
event.preventDefault();
|
||
}
|
||
},
|
||
resetFolder(index) {
|
||
// 重置文件夹
|
||
this.formData.tasklist[index].savepath = this.formData.tasklist[index].savepath.split('/').slice(0, -1).join('/');
|
||
if (this.formData.tasklist[index].savepath.endsWith('/')) {
|
||
this.formData.tasklist[index].savepath = this.formData.tasklist[index].savepath.slice(0, -1);
|
||
}
|
||
this.showToast('文件夹已重置');
|
||
},
|
||
resetFolder(index) {
|
||
// 获取当前任务的保存路径
|
||
const savePath = this.formData.tasklist[index].savepath;
|
||
const taskName = this.formData.tasklist[index].taskname;
|
||
|
||
if (!savePath) {
|
||
this.showToast('保存路径为空,无法重置');
|
||
return;
|
||
}
|
||
|
||
// 显示确认对话框
|
||
if (confirm(`确定要重置文件夹「${savePath}」吗?`)) {
|
||
// 显示加载状态
|
||
this.modalLoading = true;
|
||
|
||
// 调用后端API
|
||
axios.post('/reset_folder', {
|
||
save_path: savePath,
|
||
task_name: taskName
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.showToast(`重置成功:删除了 ${response.data.deleted_files || 0} 个文件,${response.data.deleted_records || 0} 条记录`);
|
||
// 如果当前是历史记录页面,刷新记录
|
||
if (this.activeTab === 'history') {
|
||
this.loadHistoryRecords();
|
||
}
|
||
} else {
|
||
alert(response.data.message || '重置文件夹失败');
|
||
}
|
||
this.modalLoading = false;
|
||
})
|
||
.catch(error => {
|
||
console.error('重置文件夹出错:', error);
|
||
alert('重置文件夹出错: ' + (error.response?.data?.message || error.message));
|
||
this.modalLoading = false;
|
||
});
|
||
}
|
||
},
|
||
refreshPlexLibrary(index) {
|
||
axios.post('/refresh_plex_library', { task_index: index })
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.showToast(response.data.message);
|
||
} else {
|
||
alert(response.data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert("刷新 Plex 媒体库失败: " + (error.response?.data?.message || error.message || "未知错误"));
|
||
});
|
||
},
|
||
refreshAlistDirectory(index) {
|
||
axios.post('/refresh_alist_directory', { task_index: index })
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.showToast(response.data.message);
|
||
} else {
|
||
alert(response.data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert("刷新 AList 目录失败: " + (error.response?.data?.message || error.message || "未知错误"));
|
||
});
|
||
},
|
||
// 文件整理页面刷新Plex媒体库
|
||
refreshFileManagerPlexLibrary() {
|
||
// 获取当前目录路径
|
||
const currentPath = this.getCurrentFolderPath();
|
||
|
||
axios.post('/refresh_filemanager_plex_library', {
|
||
folder_path: currentPath,
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.showToast(response.data.message);
|
||
} else {
|
||
alert(response.data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert("刷新 Plex 媒体库失败: " + (error.response?.data?.message || error.message || "未知错误"));
|
||
});
|
||
},
|
||
// 文件整理页面刷新AList目录
|
||
refreshFileManagerAlistDirectory() {
|
||
// 获取当前目录路径
|
||
const currentPath = this.getCurrentFolderPath();
|
||
|
||
axios.post('/refresh_filemanager_alist_directory', {
|
||
folder_path: currentPath,
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.showToast(response.data.message);
|
||
} else {
|
||
alert(response.data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert("刷新 AList 目录失败: " + (error.response?.data?.message || error.message || "未知错误"));
|
||
});
|
||
},
|
||
// 获取当前文件夹路径(相对于夸克网盘根目录)
|
||
getCurrentFolderPath() {
|
||
if (this.fileManager.currentFolder === 'root') {
|
||
return '/'; // 根目录返回 /,表示夸克网盘根目录
|
||
}
|
||
|
||
// 构建完整路径(相对于夸克网盘根目录)
|
||
let path = '';
|
||
for (const pathItem of this.fileManager.paths) {
|
||
path += '/' + pathItem.name;
|
||
}
|
||
return path || '/';
|
||
},
|
||
filterByTaskName(taskName, event) {
|
||
// 防止事件冒泡,避免触发行选择
|
||
if (event) {
|
||
event.stopPropagation();
|
||
}
|
||
|
||
// 如果当前已经筛选了该任务,则取消筛选
|
||
if (this.historyTaskSelected === taskName) {
|
||
this.historyTaskSelected = "";
|
||
} else {
|
||
// 设置任务筛选值
|
||
this.historyTaskSelected = taskName;
|
||
}
|
||
|
||
// 重新加载记录
|
||
this.loadHistoryRecords();
|
||
},
|
||
navigateToFolder(folderId, folderName) {
|
||
this.fileManager.selectedFiles = []; // 清空选中的文件
|
||
|
||
if (folderId === 'root') {
|
||
// 回到根目录
|
||
this.fileManager.paths = [];
|
||
} else {
|
||
// 查找是否已经在路径中
|
||
const index = this.fileManager.paths.findIndex(path => path.fid === folderId);
|
||
|
||
if (index !== -1) {
|
||
// 如果已存在,截取到该位置
|
||
this.fileManager.paths = this.fileManager.paths.slice(0, index + 1);
|
||
} else {
|
||
// 否则添加到路径
|
||
this.fileManager.paths.push({
|
||
fid: folderId,
|
||
name: folderName
|
||
});
|
||
}
|
||
}
|
||
|
||
// 保存当前目录到localStorage(为每个账号单独保存)
|
||
localStorage.setItem(`quarkAutoSave_fileManagerLastFolder_${this.fileManager.selectedAccountIndex}`, folderId);
|
||
|
||
// 重置页码并加载新文件夹内容,不显示加载动画
|
||
this.fileManager.currentPage = 1;
|
||
this.loadFileListWithoutLoading(folderId);
|
||
},
|
||
selectFileForManager(event, file) {
|
||
// 如果是文件夹,不允许选择
|
||
if (file.dir) return;
|
||
|
||
// 如果是Shift+点击,选择范围
|
||
if (event.shiftKey && this.fileManager.selectedFiles.length > 0) {
|
||
// 找出所有已选中文件的索引
|
||
const selectedIndices = this.fileManager.selectedFiles.map(id =>
|
||
this.fileManager.fileList.findIndex(file => file.fid === id)
|
||
).filter(index => index !== -1); // 过滤掉未找到的文件
|
||
|
||
if (selectedIndices.length > 0) {
|
||
// 找出已选中文件中最靠前的索引
|
||
const earliestSelectedIndex = Math.min(...selectedIndices);
|
||
// 确定最终的选择范围
|
||
const startIndex = Math.min(earliestSelectedIndex, this.fileManager.fileList.indexOf(file));
|
||
const endIndex = Math.max(earliestSelectedIndex, this.fileManager.fileList.indexOf(file));
|
||
|
||
// 获取范围内所有文件的ID(排除文件夹)
|
||
this.fileManager.selectedFiles = this.fileManager.fileList
|
||
.slice(startIndex, endIndex + 1)
|
||
.filter(file => !file.dir) // 只选择文件,不选择文件夹
|
||
.map(file => file.fid);
|
||
} else {
|
||
// 如果没有有效的选中文件(可能是由于列表刷新),则只选择当前文件
|
||
this.fileManager.selectedFiles = [file.fid];
|
||
}
|
||
}
|
||
// 如果是Ctrl/Cmd+点击,切换单个文件选择状态
|
||
else if (event.ctrlKey || event.metaKey) {
|
||
if (this.fileManager.selectedFiles.includes(file.fid)) {
|
||
this.fileManager.selectedFiles = this.fileManager.selectedFiles.filter(id => id !== file.fid);
|
||
} else {
|
||
this.fileManager.selectedFiles.push(file.fid);
|
||
}
|
||
}
|
||
// 普通点击,清除当前选择并选择当前文件
|
||
else {
|
||
if (this.fileManager.selectedFiles.length === 1 && this.fileManager.selectedFiles.includes(file.fid)) {
|
||
this.fileManager.selectedFiles = [];
|
||
} else {
|
||
this.fileManager.selectedFiles = [file.fid];
|
||
}
|
||
}
|
||
|
||
// 更新最后选择的文件索引,只有在有选择文件时才更新
|
||
if (this.fileManager.selectedFiles.length > 0) {
|
||
this.fileManager.lastSelectedFileIndex = this.fileManager.fileList.indexOf(file);
|
||
} else {
|
||
this.fileManager.lastSelectedFileIndex = -1;
|
||
}
|
||
|
||
// 阻止事件冒泡
|
||
event.stopPropagation();
|
||
},
|
||
formatFileSize(fileSize) {
|
||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
let size = fileSize;
|
||
let unitIndex = 0;
|
||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||
size /= 1024;
|
||
unitIndex++;
|
||
}
|
||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||
},
|
||
formatDate(date) {
|
||
// 将时间戳转换为年-月-日 时:分:秒格式
|
||
const d = new Date(date);
|
||
const year = d.getFullYear();
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
const hours = String(d.getHours()).padStart(2, '0');
|
||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
},
|
||
changeFolderPage(page) {
|
||
if (page < 1) page = 1;
|
||
if (page > this.fileManager.totalPages) page = this.fileManager.totalPages;
|
||
|
||
// 更新当前页码
|
||
this.fileManager.currentPage = page;
|
||
this.fileManager.gotoPage = page;
|
||
|
||
// 使用不显示加载动画的方式加载文件列表
|
||
this.loadFileListWithoutLoading(this.fileManager.currentFolder);
|
||
},
|
||
getVisibleFolderPageNumbers() {
|
||
const current = parseInt(this.fileManager.currentPage) || 1;
|
||
const total = parseInt(this.fileManager.totalPages) || 1;
|
||
const delta = 2; // 当前页左右显示的页码数
|
||
|
||
// 处理特殊情况
|
||
if (total <= 1) return [];
|
||
|
||
let range = [];
|
||
|
||
// 确定显示范围
|
||
let rangeStart = Math.max(2, current - delta);
|
||
let rangeEnd = Math.min(total - 1, current + delta);
|
||
|
||
// 调整范围,确保显示足够的页码
|
||
if (rangeEnd - rangeStart < delta * 2) {
|
||
if (current - rangeStart < delta) {
|
||
// 当前页靠近开始,扩展结束范围
|
||
rangeEnd = Math.min(total - 1, rangeStart + delta * 2);
|
||
} else {
|
||
// 当前页靠近结束,扩展开始范围
|
||
rangeStart = Math.max(2, rangeEnd - delta * 2);
|
||
}
|
||
}
|
||
|
||
// 生成页码数组
|
||
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||
range.push(i);
|
||
}
|
||
|
||
return range;
|
||
},
|
||
isFilenameTruncated(file_name, index, field) {
|
||
if (!this.fileManager.fileList[index]) return false;
|
||
|
||
// 确保文本是字符串类型
|
||
const str = String(file_name || '');
|
||
|
||
// 先检查文本是否有足够长度,太短的文本肯定不需要展开
|
||
if (str.length <= 20) return false;
|
||
|
||
// 从文件的_isOverflowing属性中获取溢出状态
|
||
const file = this.fileManager.fileList[index];
|
||
|
||
// 如果已经展开了,那么我们认为它可能是需要截断的
|
||
if (file._expanded) {
|
||
return true;
|
||
}
|
||
|
||
return file._isOverflowing && file._isOverflowing[field];
|
||
},
|
||
toggleExpandFilename(file) {
|
||
// 切换展开状态
|
||
this.$set(file, '_expanded', !file._expanded);
|
||
|
||
// 在下一个DOM更新周期中强制重新检测溢出状态
|
||
this.$nextTick(() => {
|
||
// 确保_isOverflowing属性存在
|
||
if (!file._isOverflowing) {
|
||
this.$set(file, '_isOverflowing', {});
|
||
}
|
||
|
||
// 在收起状态下,通过设置为true确保展开按钮依然可见
|
||
if (!file._expanded) {
|
||
this.$set(file._isOverflowing, 'file_name', true);
|
||
}
|
||
});
|
||
|
||
// 确保视图更新
|
||
this.$forceUpdate();
|
||
},
|
||
renameFile(file) {
|
||
// 单个文件重命名
|
||
const newName = prompt('请输入新的文件名:', file.file_name);
|
||
if (newName && newName !== file.file_name) {
|
||
axios.post('/batch_rename', {
|
||
files: [{
|
||
file_id: file.fid,
|
||
new_name: newName
|
||
}],
|
||
// 添加账号索引参数,使用文件整理页面选中的账号
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.showToast('重命名成功');
|
||
// 刷新文件列表以确保缓存同步
|
||
this.refreshCurrentFolderCache();
|
||
// 如果命名预览模态框是打开的,也要刷新它
|
||
if ($('#fileSelectModal').hasClass('show')) {
|
||
this.showFileManagerNamingPreview(this.fileManager.currentFolder);
|
||
}
|
||
} else {
|
||
alert(response.data.message || '重命名失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('重命名文件失败:', error);
|
||
alert('重命名文件失败');
|
||
});
|
||
}
|
||
},
|
||
previewAndRename() {
|
||
// 显示文件整理页面的命名预览模态框
|
||
this.showFileManagerNamingPreview();
|
||
},
|
||
|
||
showFileManagerNamingPreview(folderId) {
|
||
// 显示文件整理页面的命名预览模态框(区别于任务配置的命名预览)
|
||
this.fileSelect.selectShare = false;
|
||
this.fileSelect.selectDir = true;
|
||
this.fileSelect.previewRegex = true;
|
||
this.fileSelect.fileList = [];
|
||
this.fileSelect.paths = [];
|
||
this.fileSelect.error = undefined;
|
||
this.fileSelect.selectedFiles = [];
|
||
// 设置排序方式为按重命名结果降序排序
|
||
this.fileSelect.sortBy = "file_name_re";
|
||
this.fileSelect.sortOrder = "desc";
|
||
// 展示当前文件夹的文件
|
||
const currentFolderId = folderId || this.fileManager.currentFolder;
|
||
// 模拟一个任务,用于预览
|
||
const previewTask = {
|
||
pattern: this.fileManager.pattern,
|
||
replace: this.fileManager.replace,
|
||
use_sequence_naming: this.fileManager.use_sequence_naming,
|
||
use_episode_naming: this.fileManager.use_episode_naming,
|
||
filterwords: this.fileManager.filterwords
|
||
};
|
||
// 存储临时任务索引
|
||
this.fileSelect.index = -1; // 使用-1表示这是文件管理器的预览
|
||
// 设置模态框标题类型
|
||
document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'preview-filemanager');
|
||
// 显示模态框
|
||
if (!folderId) $('#fileSelectModal').modal('toggle'); // 仅首次打开时toggle
|
||
// 获取文件列表并处理重命名预览
|
||
this.modalLoading = true;
|
||
// 获取重命名预览结果
|
||
axios.get('/preview_rename', {
|
||
params: {
|
||
folder_id: currentFolderId,
|
||
pattern: this.fileManager.pattern,
|
||
replace: this.fileManager.replace,
|
||
naming_mode: this.fileManager.use_sequence_naming ? 'sequence' : (this.fileManager.use_episode_naming ? 'episode' : 'regex'),
|
||
include_folders: this.fileManager.include_folders,
|
||
filterwords: this.fileManager.filterwords,
|
||
// 添加页面大小参数,获取所有文件
|
||
page_size: 99999,
|
||
// 添加账号索引参数,使用文件整理页面选中的账号
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
}
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
// 处理预览结果
|
||
const previewData = response.data.data || [];
|
||
|
||
// 获取当前文件夹信息,包括路径
|
||
axios.get('/file_list', {
|
||
params: {
|
||
folder_id: currentFolderId,
|
||
sort_by: this.fileSelect.sortBy,
|
||
order: this.fileSelect.sortOrder,
|
||
// 使用一个较大的值以获取全部文件,不使用分页
|
||
page_size: 99999,
|
||
page: 1,
|
||
// 同时添加文件夹包含参数
|
||
include_folders: this.fileManager.include_folders,
|
||
// 添加账号索引参数,使用文件整理页面选中的账号
|
||
account_index: this.fileManager.selectedAccountIndex,
|
||
// 强制刷新缓存
|
||
force_refresh: true
|
||
}
|
||
})
|
||
.then(listResponse => {
|
||
if (listResponse.data.success) {
|
||
// 获取文件列表和路径
|
||
const fileList = listResponse.data.data.list;
|
||
this.fileSelect.paths = listResponse.data.data.paths || [];
|
||
|
||
// 将重命名结果添加到文件列表
|
||
fileList.forEach(file => {
|
||
// 尝试通过ID匹配预览结果
|
||
let previewItem = previewData.find(item => item.file_id === file.fid);
|
||
|
||
// 如果通过ID没有匹配到,尝试通过文件名匹配
|
||
if (!previewItem) {
|
||
previewItem = previewData.find(item =>
|
||
item.original_name === file.file_name &&
|
||
(!item.file_id || item.file_id === file.fid)
|
||
);
|
||
}
|
||
|
||
if (previewItem) {
|
||
// 设置重命名字段
|
||
file.file_name_re = previewItem.new_name;
|
||
// 设置集数字段用于排序
|
||
file.episode_number = previewItem.episode_number;
|
||
} else {
|
||
file.file_name_re = null;
|
||
file.episode_number = undefined;
|
||
}
|
||
});
|
||
|
||
// 更新文件列表
|
||
this.fileSelect.fileList = fileList;
|
||
|
||
// 按照重命名结果排序,使用统一的排序逻辑
|
||
if (this.fileSelect.sortBy === 'file_name_re') {
|
||
this.manualSortFileList('file_name_re', this.fileSelect.sortOrder);
|
||
}
|
||
} else {
|
||
this.fileSelect.error = listResponse.data.message || '获取文件列表失败';
|
||
}
|
||
this.modalLoading = false;
|
||
|
||
// 检查滚动条
|
||
this.$nextTick(() => {
|
||
this.checkPreviewScrollbar();
|
||
});
|
||
})
|
||
.catch(error => {
|
||
console.error('获取文件列表失败:', error);
|
||
this.fileSelect.error = '获取文件列表失败,请稍后再试';
|
||
this.modalLoading = false;
|
||
});
|
||
} else {
|
||
this.fileSelect.error = response.data.message || '获取预览失败';
|
||
this.modalLoading = false;
|
||
}
|
||
// 判断当前目录下是否还有可撤销的rename记录
|
||
axios.get('/api/has_rename_record', {
|
||
params: { save_path: currentFolderId }
|
||
}).then(resp => {
|
||
this.fileSelect.canUndoRename = !!(resp.data && resp.data.has_rename);
|
||
}).catch(() => {
|
||
this.fileSelect.canUndoRename = false;
|
||
});
|
||
})
|
||
.catch(error => {
|
||
console.error('获取预览失败:', error);
|
||
this.fileSelect.error = '获取预览失败,请稍后再试';
|
||
this.modalLoading = false;
|
||
});
|
||
},
|
||
// showBatchRenameModal方法已删除,功能已整合到showFileManagerNamingPreview中
|
||
// confirmBatchRename方法已删除,功能已整合到applyPreviewRename中
|
||
deleteFile(file) {
|
||
if (confirm(`确定要删除${file.dir ? '文件夹' : '文件'} "${file.file_name}" 吗?`)) {
|
||
// 获取当前路径作为save_path参数
|
||
let save_path = "";
|
||
if (this.fileManager && this.fileManager.paths) {
|
||
save_path = this.fileManager.paths.map(item => item.name).join("/");
|
||
}
|
||
|
||
axios.post('/delete_file', {
|
||
fid: file.fid,
|
||
file_name: file.file_name,
|
||
delete_records: false,
|
||
save_path: save_path,
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
})
|
||
.then(response => {
|
||
if (response.data.code === 0) {
|
||
this.showToast('成功删除 1 个项目');
|
||
// 刷新文件列表以确保缓存同步
|
||
this.refreshCurrentFolderCache();
|
||
// 如果命名预览模态框是打开的,也要刷新它
|
||
if ($('#fileSelectModal').hasClass('show')) {
|
||
this.showFileManagerNamingPreview(this.fileManager.currentFolder);
|
||
}
|
||
} else {
|
||
alert(response.data.message || '删除失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('删除文件失败:', error);
|
||
alert('删除文件失败');
|
||
});
|
||
}
|
||
},
|
||
// 开始重命名文件
|
||
startRenameFile(file) {
|
||
// 设置编辑状态
|
||
this.$set(file, '_editing', true);
|
||
this.$set(file, '_editingName', file.file_name);
|
||
|
||
// 下一个tick后聚焦输入框并全选文本
|
||
this.$nextTick(() => {
|
||
const input = this.$refs['renameInput_' + file.fid];
|
||
if (input) {
|
||
input.focus();
|
||
input.select();
|
||
}
|
||
});
|
||
},
|
||
// 开始移动文件
|
||
startMoveFile(file) {
|
||
// 如果是文件夹,不允许移动
|
||
if (file.dir) {
|
||
this.showToast('暂不支持移动文件夹');
|
||
return;
|
||
}
|
||
|
||
// 设置移动模式相关参数
|
||
this.fileSelect.moveMode = true;
|
||
this.fileSelect.moveFileIds = this.fileManager.selectedFiles.length > 0 && this.fileManager.selectedFiles.includes(file.fid)
|
||
? this.fileManager.selectedFiles
|
||
: [file.fid];
|
||
this.fileSelect.selectShare = false;
|
||
this.fileSelect.selectDir = true;
|
||
this.fileSelect.previewRegex = false;
|
||
this.fileSelect.error = undefined;
|
||
this.fileSelect.fileList = [];
|
||
this.fileSelect.paths = [];
|
||
this.fileSelect.index = -1;
|
||
|
||
// 重置排序状态为默认值 - 选择移动目标文件夹模态框默认修改时间倒序
|
||
this.fileSelect.sortBy = "updated_at";
|
||
this.fileSelect.sortOrder = "desc";
|
||
|
||
// 设置模态框类型为move(移动目标文件夹)
|
||
document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'move');
|
||
|
||
$('#fileSelectModal').modal('show');
|
||
|
||
// 加载根目录
|
||
this.getSavepathDetail(0);
|
||
},
|
||
// 保存重命名
|
||
saveRenameFile(file) {
|
||
if (!file._editing) return;
|
||
|
||
const newName = file._editingName.trim();
|
||
|
||
// 如果名称没有变化,直接取消编辑
|
||
if (newName === file.file_name) {
|
||
this.cancelRenameFile(file);
|
||
return;
|
||
}
|
||
|
||
// 如果新名称为空,提示错误并恢复原始文件名,然后退出编辑状态
|
||
if (!newName) {
|
||
this.showToast('文件名不能为空');
|
||
// 恢复原始文件名并退出编辑状态
|
||
this.cancelRenameFile(file);
|
||
return;
|
||
}
|
||
|
||
// 调用重命名API
|
||
axios.post('/batch_rename', {
|
||
files: [{
|
||
file_id: file.fid,
|
||
new_name: newName,
|
||
old_name: file.file_name
|
||
}],
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
})
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
// 更新文件名
|
||
file.file_name = newName;
|
||
this.showToast('重命名成功');
|
||
// 刷新文件列表以确保缓存同步
|
||
this.refreshCurrentFolderCache();
|
||
// 如果命名预览模态框是打开的,也要刷新它
|
||
if ($('#fileSelectModal').hasClass('show')) {
|
||
this.showFileManagerNamingPreview(this.fileManager.currentFolder);
|
||
}
|
||
} else {
|
||
this.showToast(response.data.message || '重命名失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('重命名文件失败:', error);
|
||
this.showToast('重命名文件失败');
|
||
})
|
||
.finally(() => {
|
||
// 无论成功失败都退出编辑状态
|
||
this.cancelRenameFile(file);
|
||
});
|
||
},
|
||
// 取消重命名
|
||
cancelRenameFile(file) {
|
||
this.$set(file, '_editing', false);
|
||
this.$set(file, '_editingName', '');
|
||
},
|
||
// 处理文件行点击事件
|
||
handleFileRowClick(file, event) {
|
||
// 如果文件正在编辑状态,不处理点击事件
|
||
if (file._editing) {
|
||
return;
|
||
}
|
||
|
||
// 检查是否有任何文件正在编辑状态
|
||
const editingFile = this.fileManager.fileList.find(f => f._editing);
|
||
if (editingFile) {
|
||
// 如果有其他文件正在编辑,先保存编辑状态
|
||
this.saveRenameFile(editingFile);
|
||
return;
|
||
}
|
||
|
||
// 正常处理点击事件
|
||
if (file.dir) {
|
||
this.navigateToFolder(file.fid, file.file_name);
|
||
} else {
|
||
this.selectFileForManager(event, file);
|
||
}
|
||
},
|
||
loadFileList(folderId) {
|
||
this.fileManager.loading = true;
|
||
this.fileManager.currentFolder = folderId || 'root';
|
||
|
||
const params = {
|
||
folder_id: folderId || 'root',
|
||
sort_by: this.fileManager.sortBy,
|
||
order: this.fileManager.sortOrder,
|
||
page_size: this.fileManager.pageSize,
|
||
page: this.fileManager.currentPage,
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
};
|
||
|
||
axios.get('/file_list', { params })
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.fileManager.fileList = response.data.data.list;
|
||
this.fileManager.total = response.data.data.total;
|
||
this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
|
||
this.fileManager.paths = response.data.data.paths || [];
|
||
this.fileManager.gotoPage = this.fileManager.currentPage;
|
||
} else {
|
||
alert(response.data.message || '获取文件列表失败');
|
||
}
|
||
this.fileManager.loading = false;
|
||
})
|
||
.catch(error => {
|
||
console.error('获取文件列表失败:', error);
|
||
this.fileManager.loading = false;
|
||
});
|
||
},
|
||
sortFiles(field) {
|
||
if (this.fileManager.sortBy === field) {
|
||
// 如果已经按此字段排序,则切换排序方向
|
||
this.fileManager.sortOrder = this.fileManager.sortOrder === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
// 否则切换排序字段,并设置默认排序方向
|
||
this.fileManager.sortBy = field;
|
||
// 文件名默认按字母升序,日期和大小默认按降序
|
||
this.fileManager.sortOrder = field === 'file_name' ? 'asc' : 'desc';
|
||
}
|
||
|
||
// 保存排序设置到localStorage
|
||
const sortSettings = {
|
||
sortBy: this.fileManager.sortBy,
|
||
sortOrder: this.fileManager.sortOrder
|
||
};
|
||
localStorage.setItem('quarkAutoSave_fileManagerSort', JSON.stringify(sortSettings));
|
||
|
||
// 使用不显示加载动画的方式加载文件列表
|
||
this.loadFileListWithoutLoading(this.fileManager.currentFolder);
|
||
},
|
||
changeFileManagerPageSize(size) {
|
||
// 先更新内存中的值
|
||
this.fileManager.pageSize = size === 'all' ? 99999 : size;
|
||
this.fileManager.currentPage = 1;
|
||
|
||
// 然后保存到localStorage
|
||
localStorage.setItem('quarkAutoSave_fileManagerPageSize', size.toString());
|
||
|
||
// 最后重新加载文件列表
|
||
this.loadFileListWithoutLoading(this.fileManager.currentFolder);
|
||
},
|
||
navigateToFolder(folderId, folderName) {
|
||
this.fileManager.selectedFiles = []; // 清空选中的文件
|
||
|
||
if (folderId === 'root') {
|
||
// 回到根目录
|
||
this.fileManager.paths = [];
|
||
} else {
|
||
// 查找是否已经在路径中
|
||
const index = this.fileManager.paths.findIndex(path => path.fid === folderId);
|
||
|
||
if (index !== -1) {
|
||
// 如果已存在,截取到该位置
|
||
this.fileManager.paths = this.fileManager.paths.slice(0, index + 1);
|
||
} else {
|
||
// 否则添加到路径
|
||
this.fileManager.paths.push({
|
||
fid: folderId,
|
||
name: folderName
|
||
});
|
||
}
|
||
}
|
||
|
||
// 保存当前目录到localStorage(为每个账号单独保存)
|
||
localStorage.setItem(`quarkAutoSave_fileManagerLastFolder_${this.fileManager.selectedAccountIndex}`, folderId);
|
||
|
||
// 重置页码并加载新文件夹内容,不显示加载动画
|
||
this.fileManager.currentPage = 1;
|
||
this.loadFileListWithoutLoading(folderId);
|
||
},
|
||
|
||
// 添加一个不显示加载动画的文件列表加载方法
|
||
loadFileListWithoutLoading(folderId) {
|
||
this.fileManager.currentFolder = folderId || 'root';
|
||
|
||
// 从localStorage读取分页大小设置(仅在初始化时使用)
|
||
// 在changeFileManagerPageSize方法中,我们已经确保了正确的执行顺序
|
||
const savedFileManagerPageSize = localStorage.getItem('quarkAutoSave_fileManagerPageSize');
|
||
if (savedFileManagerPageSize && this.fileManager.pageSize === 15) { // 只在默认值时读取
|
||
this.fileManager.pageSize = savedFileManagerPageSize === 'all' ? 99999 : parseInt(savedFileManagerPageSize);
|
||
}
|
||
|
||
const params = {
|
||
folder_id: folderId || 'root',
|
||
sort_by: this.fileManager.sortBy,
|
||
order: this.fileManager.sortOrder,
|
||
page_size: this.fileManager.pageSize,
|
||
page: this.fileManager.currentPage,
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
};
|
||
|
||
axios.get('/file_list', { params })
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.fileManager.fileList = response.data.data.list;
|
||
this.fileManager.total = response.data.data.total;
|
||
this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
|
||
this.fileManager.paths = response.data.data.paths || [];
|
||
this.fileManager.gotoPage = this.fileManager.currentPage;
|
||
} else {
|
||
console.error('获取文件列表失败:', response.data.message);
|
||
}
|
||
|
||
// 检测当前的命名模式
|
||
this.detectFileManagerNamingMode();
|
||
})
|
||
.catch(error => {
|
||
console.error('获取文件列表失败:', error);
|
||
});
|
||
},
|
||
|
||
// 添加带fallback机制的文件列表加载方法,用于账号切换
|
||
loadFileListWithFallback(folderId) {
|
||
this.fileManager.currentFolder = folderId || 'root';
|
||
|
||
const params = {
|
||
folder_id: folderId || 'root',
|
||
sort_by: this.fileManager.sortBy,
|
||
order: this.fileManager.sortOrder,
|
||
page_size: this.fileManager.pageSize,
|
||
page: this.fileManager.currentPage,
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
};
|
||
|
||
axios.get('/file_list', { params })
|
||
.then(response => {
|
||
if (response.data.success) {
|
||
this.fileManager.fileList = response.data.data.list;
|
||
this.fileManager.total = response.data.data.total;
|
||
this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
|
||
this.fileManager.paths = response.data.data.paths || [];
|
||
this.fileManager.gotoPage = this.fileManager.currentPage;
|
||
|
||
// 检测当前的命名模式
|
||
this.detectFileManagerNamingMode();
|
||
} else {
|
||
// 如果获取文件列表失败,可能是目录在新账号中不存在
|
||
console.warn('当前目录在新账号中不存在,自动切换到根目录:', response.data.message);
|
||
this.fallbackToRoot();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取文件列表失败,自动切换到根目录:', error);
|
||
this.fallbackToRoot();
|
||
});
|
||
},
|
||
|
||
// fallback到根目录的方法
|
||
fallbackToRoot() {
|
||
this.fileManager.currentFolder = 'root';
|
||
this.fileManager.paths = [];
|
||
this.fileManager.currentPage = 1;
|
||
|
||
// 更新localStorage中的当前目录(为每个账号单独保存)
|
||
localStorage.setItem(`quarkAutoSave_fileManagerLastFolder_${this.fileManager.selectedAccountIndex}`, 'root');
|
||
|
||
// 重新加载根目录
|
||
this.loadFileListWithoutLoading('root');
|
||
},
|
||
getVisibleFolderPageNumbers() {
|
||
const current = parseInt(this.fileManager.currentPage) || 1;
|
||
const total = parseInt(this.fileManager.totalPages) || 1;
|
||
const delta = 2; // 当前页左右显示的页码数
|
||
|
||
// 处理特殊情况
|
||
if (total <= 1) return [];
|
||
if (total <= 5) {
|
||
return Array.from({ length: total - 2 }, (_, i) => i + 2);
|
||
}
|
||
|
||
let range = [];
|
||
|
||
// 确定显示范围
|
||
let rangeStart = Math.max(2, current - delta);
|
||
let rangeEnd = Math.min(total - 1, current + delta);
|
||
|
||
// 调整范围,确保显示足够的页码
|
||
if (rangeEnd - rangeStart < delta * 2) {
|
||
if (current - rangeStart < delta) {
|
||
// 当前页靠近开始,扩展结束范围
|
||
rangeEnd = Math.min(total - 1, rangeStart + delta * 2);
|
||
} else {
|
||
// 当前页靠近结束,扩展开始范围
|
||
rangeStart = Math.max(2, rangeEnd - delta * 2);
|
||
}
|
||
}
|
||
|
||
// 生成页码数组
|
||
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||
range.push(i);
|
||
}
|
||
|
||
return range;
|
||
},
|
||
// cancelBatchRename和applyBatchRename方法已删除,功能已整合到fileSelectModal中
|
||
handleFileManagerOutsideClick(event) {
|
||
// 如果当前不是文件整理页面或者没有选中的文件,则不处理
|
||
if (this.activeTab !== 'filemanager' || this.fileManager.selectedFiles.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 如果有文件正在编辑状态,不处理文件选择逻辑,让重命名处理器优先处理
|
||
const editingFile = this.fileManager.fileList.find(file => file._editing);
|
||
if (editingFile) {
|
||
return;
|
||
}
|
||
|
||
// 检查点击是否在表格内
|
||
const tableElement = document.querySelector('.table.selectable-files');
|
||
// 检查点击是否在分页控制区域
|
||
const paginationElement = document.querySelector('.pagination-container');
|
||
|
||
// 如果点击不在表格内且不在分页控制区域内,则清除选择
|
||
if (tableElement && !tableElement.contains(event.target) &&
|
||
paginationElement && !paginationElement.contains(event.target)) {
|
||
this.fileManager.selectedFiles = [];
|
||
this.fileManager.lastSelectedFileIndex = -1;
|
||
}
|
||
},
|
||
deleteSelectedFilesForManager(clickedFid, clickedFname, isDir, deleteRecords = false) {
|
||
// 如果是文件夹或者没有选中的文件,则按原来的方式删除单个文件
|
||
if (isDir || this.fileManager.selectedFiles.length === 0) {
|
||
this.deleteFile({ fid: clickedFid, file_name: clickedFname, dir: isDir });
|
||
return;
|
||
}
|
||
|
||
// 如果点击的文件不在选中列表中,也按原来的方式删除单个文件
|
||
if (!this.fileManager.selectedFiles.includes(clickedFid)) {
|
||
this.deleteFile({ fid: clickedFid, file_name: clickedFname, dir: isDir });
|
||
return;
|
||
}
|
||
|
||
// 多选删除
|
||
const selectedCount = this.fileManager.selectedFiles.length;
|
||
|
||
// 根据选中数量使用不同的确认提示
|
||
let confirmMessage = selectedCount === 1
|
||
? `确定要删除此项目吗?`
|
||
: `确定要删除选中的 ${selectedCount} 个项目吗?`;
|
||
|
||
if (confirm(confirmMessage)) {
|
||
// 获取当前路径作为save_path参数
|
||
let save_path = "";
|
||
if (this.fileManager && this.fileManager.paths) {
|
||
save_path = this.fileManager.paths.map(item => item.name).join("/");
|
||
}
|
||
|
||
// 创建一个Promise数组来处理所有删除请求
|
||
const deletePromises = this.fileManager.selectedFiles.map(fid => {
|
||
// 查找对应的文件对象,获取文件名
|
||
const fileObj = this.fileManager.fileList.find(file => file.fid === fid);
|
||
const fileName = fileObj ? fileObj.file_name : '';
|
||
|
||
return axios.post('/delete_file', {
|
||
fid: fid,
|
||
file_name: fileName,
|
||
delete_records: deleteRecords,
|
||
save_path: save_path,
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
})
|
||
.then(response => {
|
||
return { fid: fid, success: response.data.code === 0, deleted_records: response.data.deleted_records || 0 };
|
||
})
|
||
.catch(error => {
|
||
return { fid: fid, success: false, deleted_records: 0 };
|
||
});
|
||
});
|
||
|
||
// 等待所有删除请求完成
|
||
Promise.all(deletePromises)
|
||
.then(results => {
|
||
// 统计成功和失败的数量
|
||
const successCount = results.filter(r => r.success).length;
|
||
const failCount = results.length - successCount;
|
||
|
||
// 显示结果
|
||
if (failCount > 0) {
|
||
alert(`成功删除 ${successCount} 个项目,${failCount} 个项目删除失败`);
|
||
} else {
|
||
this.showToast(`成功删除 ${successCount} 个项目`);
|
||
}
|
||
|
||
// 如果有成功删除的文件,刷新文件列表以确保缓存同步
|
||
if (successCount > 0) {
|
||
this.refreshCurrentFolderCache();
|
||
// 如果命名预览模态框是打开的,也要刷新它
|
||
if ($('#fileSelectModal').hasClass('show')) {
|
||
this.showFileManagerNamingPreview(this.fileManager.currentFolder);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
},
|
||
detectFileManagerNamingMode() {
|
||
// 检测是否为顺序命名模式或剧集命名模式
|
||
const currentValue = this.fileManager.pattern;
|
||
|
||
if (currentValue !== undefined) {
|
||
// 检查是否包含完整的{}占位符(确保不是分开的{和})
|
||
const isSequenceNaming = currentValue.includes('{}');
|
||
|
||
// 如果不是顺序命名模式,检查是否包含完整的[]占位符
|
||
const isEpisodeNaming = !isSequenceNaming && currentValue.includes('[]');
|
||
|
||
// 处理模式切换
|
||
if (isSequenceNaming) {
|
||
// 切换到顺序命名模式
|
||
this.fileManager.use_sequence_naming = true;
|
||
this.fileManager.use_episode_naming = false;
|
||
} else if (isEpisodeNaming) {
|
||
// 切换到剧集命名模式
|
||
this.fileManager.use_sequence_naming = false;
|
||
this.fileManager.use_episode_naming = true;
|
||
} else {
|
||
// 切换到正则命名模式
|
||
this.fileManager.use_sequence_naming = false;
|
||
this.fileManager.use_episode_naming = false;
|
||
}
|
||
}
|
||
},
|
||
cancelPreviewRename() {
|
||
$('#fileSelectModal').modal('hide');
|
||
},
|
||
async applyPreviewRename() {
|
||
if (this.modalLoading) return;
|
||
this.modalLoading = true;
|
||
try {
|
||
const renameList = this.fileSelect.fileList.filter(f => f.file_name_re && f.file_name_re !== '×' && !f.file_name_re.startsWith('×'));
|
||
if (renameList.length === 0) {
|
||
this.showToast('没有可重命名的项目');
|
||
this.modalLoading = false;
|
||
return;
|
||
}
|
||
// 只传 file_id 和 new_name,并加上 save_path
|
||
const files = renameList.map(f => ({
|
||
file_id: f.fid,
|
||
new_name: f.file_name_re,
|
||
old_name: f.file_name // 传递原文件名,便于撤销
|
||
}));
|
||
const save_path = this.fileManager.currentFolder;
|
||
const response = await axios.post('/batch_rename', {
|
||
files,
|
||
save_path,
|
||
// 添加账号索引参数,使用文件整理页面选中的账号
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
});
|
||
if (response.data.success) {
|
||
this.showToast(`成功重命名 ${response.data.success_count || files.length} 个项目`);
|
||
// 强制刷新文件列表缓存
|
||
this.refreshCurrentFolderCache();
|
||
$('#fileSelectModal').modal('hide');
|
||
this.fileSelect.canUndoRename = true; // 重命名后立即可撤销
|
||
} else {
|
||
this.showToast(response.data.message || '重命名失败');
|
||
}
|
||
} catch (e) {
|
||
this.showToast('重命名请求失败');
|
||
}
|
||
this.modalLoading = false;
|
||
},
|
||
async undoPreviewRename() {
|
||
if (this.modalLoading) return;
|
||
this.modalLoading = true;
|
||
try {
|
||
const save_path = this.fileManager.currentFolder;
|
||
const response = await axios.post('/undo_rename', {
|
||
save_path,
|
||
// 添加账号索引参数,使用文件整理页面选中的账号
|
||
account_index: this.fileManager.selectedAccountIndex
|
||
});
|
||
if (response.data.success) {
|
||
this.showToast(`成功撤销 ${response.data.success_count} 个项目重命名`);
|
||
this.showFileManagerNamingPreview(save_path);
|
||
// 强制刷新文件列表缓存
|
||
this.refreshCurrentFolderCache();
|
||
// 撤销后如无可撤销项再设为false(由showFileManagerNamingPreview刷新)
|
||
} else {
|
||
this.showToast(response.data.message || '撤销失败');
|
||
this.fileSelect.canUndoRename = false;
|
||
}
|
||
} catch (e) {
|
||
this.showToast('撤销失败: ' + (e.message || e));
|
||
this.fileSelect.canUndoRename = false;
|
||
}
|
||
this.modalLoading = false;
|
||
}
|
||
},
|
||
mounted() {
|
||
this.fetchData();
|
||
this.checkNewVersion();
|
||
this.fetchUserInfo(); // 获取用户信息
|
||
|
||
// 添加点击事件监听
|
||
document.addEventListener('click', this.handleOutsideClick);
|
||
document.addEventListener('click', this.handleModalOutsideClick);
|
||
document.addEventListener('click', this.handleFileManagerOutsideClick);
|
||
|
||
// 添加模态框关闭事件监听
|
||
$('#fileSelectModal').on('hidden.bs.modal', () => {
|
||
this.fileSelect.selectedFiles = [];
|
||
this.fileSelect.lastSelectedFileIndex = -1;
|
||
// 重置移动模式相关参数
|
||
this.fileSelect.moveMode = false;
|
||
this.fileSelect.moveFileIds = [];
|
||
});
|
||
|
||
// 检查本地存储中的标签页状态
|
||
const savedTab = localStorage.getItem('quarkAutoSave_activeTab');
|
||
if (savedTab) {
|
||
this.activeTab = savedTab;
|
||
}
|
||
|
||
// 从本地存储中恢复侧边栏折叠状态
|
||
const savedSidebarState = localStorage.getItem('quarkAutoSave_sidebarCollapsed');
|
||
if (savedSidebarState) {
|
||
this.sidebarCollapsed = savedSidebarState === 'true';
|
||
}
|
||
|
||
// 从本地存储中恢复用户设置的每页记录数
|
||
const savedPageSize = localStorage.getItem('quarkAutoSave_pageSize');
|
||
if (savedPageSize) {
|
||
this.historyParams.page_size = savedPageSize === 'all' ? 99999 : parseInt(savedPageSize);
|
||
}
|
||
|
||
// 从本地存储中恢复页面宽度设置
|
||
const savedPageWidthMode = localStorage.getItem('quarkAutoSave_pageWidthMode');
|
||
if (savedPageWidthMode) {
|
||
this.pageWidthMode = savedPageWidthMode;
|
||
document.body.classList.add('page-width-' + this.pageWidthMode);
|
||
} else {
|
||
// 默认使用中等宽度
|
||
document.body.classList.add('page-width-medium');
|
||
}
|
||
|
||
$('[data-toggle="tooltip"]').tooltip();
|
||
document.addEventListener('keydown', this.handleKeyDown);
|
||
document.addEventListener('click', (e) => {
|
||
// 如果点击的是输入框、搜索按钮或下拉菜单本身,不关闭下拉菜单
|
||
if (e.target.closest('.input-group input') ||
|
||
e.target.closest('.btn-primary[type="button"]') ||
|
||
e.target.closest('.dropdown-menu.task-suggestions') ||
|
||
e.target.closest('.bi-search')) {
|
||
return;
|
||
}
|
||
// 只隐藏下拉菜单,不清空搜索结果,这样点击同一任务的输入框时还能看到之前的搜索结果
|
||
this.smart_param.showSuggestions = false;
|
||
});
|
||
|
||
// 添加点击事件监听器,用于在点击表格外区域时取消选择记录
|
||
document.addEventListener('click', this.handleOutsideClick);
|
||
|
||
// 添加点击事件监听器,用于在点击模态框表格外区域时取消选择文件
|
||
document.addEventListener('click', this.handleModalOutsideClick);
|
||
|
||
// 添加点击事件监听器,用于处理重命名编辑状态
|
||
document.addEventListener('click', this.handleRenameOutsideClick);
|
||
|
||
// 添加模态框关闭事件监听,清空选中文件列表
|
||
$('#fileSelectModal').on('hidden.bs.modal', () => {
|
||
this.fileSelect.selectedFiles = [];
|
||
this.fileSelect.lastSelectedFileIndex = -1;
|
||
// 重置移动模式相关参数
|
||
this.fileSelect.moveMode = false;
|
||
this.fileSelect.moveFileIds = [];
|
||
});
|
||
|
||
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
||
|
||
// 监听模态框显示事件,检查滚动条状态
|
||
$('#fileSelectModal').on('shown.bs.modal', this.checkPreviewScrollbar);
|
||
// 监听窗口大小改变,重新检查滚动条状态
|
||
window.addEventListener('resize', this.checkPreviewScrollbar);
|
||
|
||
// 初始化时检查所有任务的命名模式
|
||
setTimeout(() => {
|
||
if (this.formData.tasklist && this.formData.tasklist.length > 0) {
|
||
this.formData.tasklist.forEach(task => {
|
||
// 检查现有的顺序命名设置
|
||
if (task.use_sequence_naming && task.sequence_naming) {
|
||
// 已经设置过顺序命名的,将顺序命名模式转换为匹配表达式
|
||
if (!task.pattern || task._pattern_backup) {
|
||
task.pattern = task.sequence_naming;
|
||
}
|
||
} else if (task.use_episode_naming && task.episode_naming) {
|
||
// 已经设置过剧集命名的,将剧集命名模式转换为匹配表达式
|
||
if (!task.pattern || task._pattern_backup) {
|
||
task.pattern = task.episode_naming;
|
||
}
|
||
} else {
|
||
// 检测是否包含顺序命名或剧集命名模式
|
||
this.detectNamingMode(task);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 如果没有剧集识别模式,添加默认模式
|
||
if (!this.formData.episode_patterns || this.formData.episode_patterns.length === 0) {
|
||
this.formData.episode_patterns = [
|
||
{ regex: '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?' }
|
||
];
|
||
}
|
||
|
||
// 如果当前标签是历史记录,则加载历史记录
|
||
if (this.activeTab === 'history') {
|
||
this.loadHistoryRecords();
|
||
// 加载所有任务名称用于筛选
|
||
this.loadAllTaskNames();
|
||
}
|
||
|
||
// 添加对history.pagination的监听
|
||
this.$watch('history.pagination', function(newVal) {
|
||
if (newVal && newVal.total_pages) {
|
||
this.$nextTick(() => {
|
||
// 强制Vue更新视图
|
||
this.$forceUpdate();
|
||
});
|
||
}
|
||
}, { deep: true });
|
||
|
||
// 检查分享链接状态
|
||
this.checkShareUrlStatus();
|
||
}, 500);
|
||
},
|
||
beforeDestroy() {
|
||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||
// 移除点击事件监听器
|
||
document.removeEventListener('click', this.handleOutsideClick);
|
||
document.removeEventListener('click', this.handleRenameOutsideClick);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|
||
|