quark-auto-save/app/templates/index.html

6003 lines
288 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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);
}
}
}
});
// 添加检测文件整理页面文件名溢出的自定义指令
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到设定秒数之间随机延迟执行。建议值036000表示不延迟">
<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 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时获取的文件数量适当增大该数值可减少请求次数提升大文件夹的加载效率。建议值100500">
<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-3000表示不缓存">
<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>
</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)">
<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)">
<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">&nbsp;忽略后缀
</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 }}
</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 }}
</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">&nbsp;含文件夹
</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">&nbsp;含文件夹
</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="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></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="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></i>
<div class="text-truncate"
:title="file.file_name"
v-check-file-overflow="index + '|file_name'">
{{ file.file_name }}
</div>
</div>
<div v-else style="white-space: normal; word-break: break-word; padding-right: 25px;">
<i class="bi mr-2" :class="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></i>{{ file.file_name }}
</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="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></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="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></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"
},
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: "",
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: {
// 拼音排序辅助函数
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"
};
}
// 确保文件整理性能配置存在
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);
// 数据加载完成后检查分享链接状态
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;
}
},
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);
// 清理任务名称中的连续空格和特殊符号
const cleanTaskName = task.taskname.replace(/\s+/g, ' ').trim();
// 提取任务名称中的分隔符格式
// 使用与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;
isTVShow = isTVShow || hasSeason;
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 {
// 如果没有匹配到季序号格式默认使用整个任务名作为剧名季序号为01
showName = cleanTaskName;
seasonNumber = '01';
}
// 更新保存路径 - 无论是否使用智能路径,都确保倒数第二级目录更新
if (task.savepath) {
// 分割保存路径为各级目录
const pathParts = task.savepath.split('/');
if (pathParts.length >= 2) {
// 如果智能路径已设置,使用原有逻辑更新最后一级
if (this.smart_param.savepath) {
// 更新最后一级目录,但保留前面的路径结构
const newPath = this.smart_param.savepath.replace('TASKNAME', task.taskname);
const newPathParts = newPath.split('/');
pathParts[pathParts.length - 1] = newPathParts[newPathParts.length - 1];
} else {
// 根据是否为电视节目决定处理方式
if (isTVShow) {
// 电视节目格式:剧名 + 分隔符 + S季序号
pathParts[pathParts.length - 1] = showName + nameSeparator + 'S' + seasonNumber.padStart(2, '0');
} else {
// 非电视节目直接使用任务名称
pathParts[pathParts.length - 1] = cleanTaskName;
}
}
// 处理倒数第二级目录(剧名+年份)- 无论是否使用智能路径,都更新
if (pathParts.length >= 3 && isTVShow) {
// 只有电视节目才更新倒数第二级目录
const parentDir = pathParts[pathParts.length - 2];
// 提取年份信息
const yearMatch = parentDir.match(/\((\d{4})\)|\(\d{4})\|[\s\-_]+(\d{4})(?:[\s\-_]+|$)/);
const year = yearMatch ? (yearMatch[1] || yearMatch[2] || yearMatch[3] || '2025') : '2025';
// 重建倒数第二级目录,使用新的剧名和原有的年份
pathParts[pathParts.length - 2] = showName + ' (' + year + ')';
}
// 更新保存路径
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]);
}
});
},
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') {
// 文件夹始终在前
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
// 检查当前模态框类型,选择起始文件模态框使用全局文件排序函数
const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
if (modalType === 'start-file') {
// 选择起始文件模态框:使用全局文件排序函数
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 {
// 其他模态框:使用拼音排序
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') {
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>