quark-auto-save/app/templates/index.html
Milo Brion d526caf90b
Update index.html
修改ui,添加功能全局黑名单和全局正则等小功能。
2025-06-29 12:17:21 +08:00

1148 lines
77 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="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- Vue.js & Libs -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.21.4/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/v-jsoneditor@1.4.1/dist/v-jsoneditor.min.js"></script>
<style>
/* --- 1. 配色方案: 深蓝与灰 --- */
:root {
--primary-color: #4f46e5; /* 主色 (靛蓝) */
--primary-hover: #4338ca; /* 主色悬浮 */
--background-color: #f8fafc; /* 背景色 (最浅灰) */
--surface-color: #ffffff; /* 表面色 (白) */
--border-color: #e2e8f0; /* 边框色 (浅灰) */
--text-primary: #1e293b; /* 主要文字 (深灰) */
--text-secondary: #64748b; /* 次要文字 (中灰) */
--highlight-bg: #eef2ff; /* 高亮背景 (浅靛蓝) */
--status-active: #22c55e; /* 状态: 启用 (绿) */
--status-disabled: #9ca3af; /* 状态: 禁用 (灰) */
--status-warning: #f59e0b; /* 状态: 警告 (黄) */
}
/* --- 2. 布局与滚动修复 --- */
html, body {
height: 100%;
overflow: hidden; /* 防止body滚动 */
}
#app {
display: flex;
flex-direction: column;
height: 100%;
}
.navbar {
flex-shrink: 0; /* 防止导航栏收缩 */
z-index: 1030;
}
.container-fluid, .row {
flex-grow: 1;
height: 100%;
min-height: 0;
}
#sidebarMenu {
height: calc(100vh - 56px); /* 视口高度减去导航栏高度 */
background-color: var(--surface-color);
border-right: 1px solid var(--border-color);
flex-shrink: 0;
}
main {
height: calc(100vh - 56px); /* 关键样式:仅主内容区可滚动 */
overflow-y: auto;
flex-grow: 1;
}
/* --- 3. 通用UI优化 --- */
body { background-color: var(--background-color); color: var(--text-primary); }
.card {
border: 1px solid var(--border-color); border-radius: .75rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.card-header { background-color: var(--surface-color); font-weight: 500; border-bottom: 1px solid var(--border-color); cursor: pointer; }
.btn-primary { background-color: var(--primary-color); border-color: var(--primary-color); }
.btn-primary:hover, .btn-primary:focus { background-color: var(--primary-hover); border-color: var(--primary-hover); }
.nav-link { color: var(--text-secondary); }
.nav-link.active {
font-weight: 600; color: var(--primary-color) !important;
background-color: var(--highlight-bg) !important; border-radius: .5rem;
}
.nav-link:hover { color: var(--primary-color); }
.sidebar .nav-link i { width: 24px; }
.nav-bottom {
position: absolute; bottom: 15px; left: 0; right: 0; padding: 0 1rem;
text-align: center; font-size: 0.8rem;
}
.nav-bottom p { margin-bottom: 0.5rem; }
.nav-bottom a { color: var(--text-secondary); }
.nav-bottom a:hover { color: var(--primary-color); }
.cursor-pointer { cursor: pointer; }
/* --- 4. 任务状态指示灯 --- */
.task-status-indicator {
display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 0.75rem;
vertical-align: middle; transition: background-color 0.2s;
}
.status-active { background-color: var(--status-active); }
.status-disabled { background-color: var(--status-disabled); }
.status-warning { background-color: var(--status-warning); }
/* --- 自定义复选框样式 --- */
.custom-checkbox-wrapper {
position: relative;
display: inline-flex;
align-items: center;
cursor: pointer;
}
.custom-checkbox-wrapper input[type="checkbox"] {
position: absolute;
opacity: 0;
cursor: pointer;
}
.custom-checkbox {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 3px;
margin-right: 5px;
transition: all 0.2s;
}
.custom-checkbox i {
color: white;
font-size: 12px;
visibility: hidden;
}
.custom-checkbox-wrapper input[type="checkbox"]:checked ~ .custom-checkbox {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.custom-checkbox-wrapper input[type="checkbox"]:checked ~ .custom-checkbox i {
visibility: visible;
}
.custom-checkbox-wrapper input[type="checkbox"]:focus ~ .custom-checkbox {
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.25);
}
/* --- 5. 日志与返回顶部按钮 --- */
.log-panel {
background-color: var(--surface-color); /* 日志背景改为白色 */
color: var(--text-primary); /* 日志文字改为深色 */
border: 1px solid var(--border-color);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.875rem; border-radius: 0.5rem; padding: 1rem; height: 65vh; overflow-y: auto; white-space: pre-wrap; word-break: break-all;
}
.log-highlight { background-color: #fef9c3; color: #713a0a; }
#backToTopBtn {
position: fixed; bottom: 20px; right: 20px; z-index: 1030; width: 45px; height: 45px; border-radius: 50%;
background-color: var(--surface-color); border: 1px solid var(--border-color);
box-shadow: 0 4px 6px -1px rgba(0,0,0,.1); display: none; align-items: center; justify-content: center; font-size: 1.2rem;
}
/* 自动保存提示 */
#autoSaveNotification {
position: fixed;
top: 20px; /* 改为顶部 */
left: 50%; /* 居中显示 */
transform: translateX(-50%) translateY(-20px); /* 水平居中并初始位置在上方 */
background-color: var(--primary-color);
color: white;
padding: 8px 16px;
border-radius: 4px;
font-size: 0.875rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
z-index: 1050; /* 提高层级确保显示在最上方 */
}
#autoSaveNotification.show {
opacity: 1;
transform: translateX(-50%) translateY(0); /* 显示时移入视图 */
}
#autoSaveNotification.hide {
opacity: 0;
transform: translateX(-50%) translateY(-20px); /* 隐藏时向上移出 */
}
/* 操作反馈动画 */
.action-notification {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background-color: var(--primary-color);
color: white;
padding: 8px 16px;
border-radius: 4px;
font-size: 0.875rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
z-index: 1050;
}
.action-notification.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.action-notification.hide {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
main { padding-bottom: 3rem; }
</style>
</head>
<body>
<div id="app">
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow-sm">
<a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#"><i class="bi bi-clouds-fill"></i> 夸克自动转存</a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse" data-target="#sidebarMenu"><span class="navbar-toggler-icon"></span></button>
<div class="d-flex align-items-center ml-auto pr-3">
<button class="btn btn-success btn-sm mr-2" @click.prevent="saveConfig" title="保存配置 (Ctrl+S)">
<i class="bi bi-save-fill"></i> 保存
</button>
<button class="btn btn-primary btn-sm" @click.prevent="runScriptNow()" title="运行所有任务 (Ctrl+R)">
<i class="bi bi-play-circle-fill"></i> 运行
</button>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block sidebar collapse">
<div class="sidebar-sticky pt-3 px-2">
<ul class="nav flex-column">
<li class="nav-item mb-1"><a class="nav-link" href="#" :class="{active: activeTab === 'tasklist'}" @click="changeTab('tasklist')"><i class="bi bi-list-task"></i> 任务列表 <span class="badge badge-pill ml-1" style="background-color: var(--primary-color); color: white;" v-if="formData.tasklist.length">{{ formData.tasklist.length }}</span></a></li>
<li class="nav-item mb-1"><a class="nav-link" href="#" :class="{active: activeTab === 'config'}" @click="changeTab('config')"><i class="bi bi-gear-fill"></i> 系统配置</a></li>
<li class="nav-item mb-1"><a class="nav-link" href="#" :class="{active: activeTab === 'logs'}" @click="changeTab('logs')"><i class="bi bi-body-text"></i> 日志</a></li>
</ul>
<div class="nav-bottom text-center">
<p><a href="/logout"><i class="bi bi-box-arrow-right mr-1"></i>退出</a></p>
<p><a target="_blank" href="https://github.com/Cp0204/quark-auto-save/wiki"><i class="bi bi-wechat mr-1"></i>使用交流</a></p>
<p><a href="./static/js/qas.addtask.user.js"><i class="bi bi-cloud-plus-fill mr-1"></i>推送任务油猴脚本</a></p>
<p><a target="_blank" href="https://github.com/Cp0204/quark-auto-save"><i class="bi bi-github mr-1"></i>quark-auto-save</a></p>
<p style="position: relative;" v-html="versionTips"></p>
</div>
</div>
</nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4 py-4">
<form @submit.prevent="saveConfig" @keydown.enter.prevent>
<div v-if="activeTab === 'config'">
<!-- 配置页内容 -->
<div class="d-flex justify-content-between align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">系统配置</h1>
</div>
<div class="row">
<div class="col-lg-6">
<!-- Cookie Card -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-cookie mr-2"></i>Cookie</h5>
<button type="button" class="btn btn-outline-secondary btn-sm" @click="addCookie()"><i class="bi bi-plus-lg"></i> 添加</button>
</div>
<div class="card-body p-4">
<p class="card-text text-muted small">1. 所有账号执行签到,纯<a class="" href="https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间" target="_blank">签到</a>只需移动端参数即可!<br>2. 仅第一个账号执行转存,请自行确认顺序。<b>最好是手机验证码<a target="_blank" href="https://pan.quark.cn/">登录</a>CK比较完整</b>如需签到参数附在CK后面。</p>
<transition-group name="list-anim" tag="div">
<div v-for="(value, index) in formData.cookie" :key="index" class="input-group mb-2">
<div class="input-group-prepend"><span class="input-group-text">#{{ index + 1 }}</span></div>
<input type="text" v-model="formData.cookie[index]" class="form-control" placeholder="打开 pan.quark.com 按 F12 抓取">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" @click="pasteCookie(index)" title="粘贴"><i class="bi bi-clipboard"></i></button>
<button type="button" class="btn btn-outline-danger" @click="removeCookie(index)" title="删除"><i class="bi bi-trash"></i></button>
</div>
</div>
</transition-group>
<div v-if="!formData.cookie.length" class="alert alert-light text-center small p-2">暂无Cookie</div>
</div>
</div>
<!-- Crontab Card -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-clock-history mr-2"></i>定时规则</h5>
</div>
<div class="card-body p-4">
<div class="input-group">
<input type="text" v-model="formData.crontab" class="form-control" placeholder="例如: 0 */2 * * * (每2小时运行一次)">
<div class="input-group-append">
<a href="https://tool.lu/crontab/" target="_blank" class="btn btn-outline-secondary" title="CRON表达式在线生成与校验">?</a>
</div>
</div>
</div>
</div>
<!-- Push Config Card -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0 d-inline-flex align-items-center"><i class="bi bi-bell-fill mr-2"></i>通知推送
<a href="https://github.com/Cp0204/quark-auto-save/wiki/通知推送服务配置" target="_blank" class="ml-2 badge badge-light font-weight-normal">?</a>
</h5>
<div>
<button type="button" class="btn btn-outline-info btn-sm mr-2" title="通知推送测试" @click="testPush()"><i class="bi bi-lightning-fill"></i> 测试</button>
<button type="button" class="btn btn-outline-secondary btn-sm" @click="addPush()"><i class="bi bi-plus-lg"></i> 添加</button>
</div>
</div>
<div class="card-body p-4">
<p class="card-text text-muted small">支持多个通知渠道。</p>
<transition-group name="list-anim" tag="div">
<div v-for="(value, key) in formData.push_config" :key="key" class="input-group mb-2">
<div class="input-group-prepend"><span class="input-group-text" style="min-width: 120px;">{{ 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 type="button" class="btn btn-outline-danger" @click="removePush(key)"><i class="bi bi-trash"></i></button>
</div>
</div>
</transition-group>
<div v-if="!Object.keys(formData.push_config).length" class="alert alert-light text-center small p-2">暂无通知配置</div>
</div>
</div>
</div>
<div class="col-lg-6">
<!-- Blacklist Card -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-slash-circle-fill mr-2"></i>文件黑名单</h5>
<button type="button" class="btn btn-outline-secondary btn-sm" @click="addBlacklistItem()"><i class="bi bi-plus-lg"></i> 添加</button>
</div>
<div class="card-body p-4">
<p class="card-text text-muted small">匹配的文件名将被跳过,不支持正则。</p>
<transition-group name="list-anim" tag="div">
<div v-for="(item, index) in formData.file_blacklist" :key="index" class="input-group mb-2">
<input type="text" v-model="formData.file_blacklist[index]" class="form-control" placeholder="输入要屏蔽的完整文件名">
<div class="input-group-append">
<button type="button" class="btn btn-outline-danger" @click="removeBlacklistItem(index)" title="删除"><i class="bi bi-trash"></i></button>
</div>
</div>
</transition-group>
<div v-if="!formData.file_blacklist || !formData.file_blacklist.length" class="alert alert-light text-center small p-2">暂无黑名单规则</div>
</div>
</div>
<!-- CloudSaver Card -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0 d-inline-flex align-items-center"><i class="bi bi-search-heart-fill mr-2"></i>CloudSaver 资源搜索
<a href="https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源" target="_blank" class="ml-2 badge badge-light font-weight-normal">?</a>
</h5>
</div>
<div class="card-body p-4">
<p class="card-text text-muted small">用于任务名称智能搜索,需要部署<a href="https://github.com/Cp0204/CloudSaver" target="_blank">CloudSaver</a>服务。</p>
<div class="form-group row mb-2"><label class="col-sm-3 col-form-label">服务器</label><div class="col-sm-9"><input type="text" v-model="formData.source.cloudsaver.server" class="form-control" placeholder="http://127.0.0.1:8008"></div></div>
<div class="form-group row mb-2"><label class="col-sm-3 col-form-label">用户名</label><div class="col-sm-9"><input type="text" v-model="formData.source.cloudsaver.username" class="form-control"></div></div>
<div class="form-group row mb-0"><label class="col-sm-3 col-form-label">密码</label><div class="col-sm-9"><input type="password" v-model="formData.source.cloudsaver.password" class="form-control"></div></div>
</div>
</div>
<!-- Shortcuts Card -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-keyboard-fill mr-2"></i>快捷键设置</h5>
</div>
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<label for="saveShortcutSwitch" class="mb-0">保存配置 (Ctrl/Cmd + S)</label>
<div class="custom-control custom-switch"><input type="checkbox" class="custom-control-input" id="saveShortcutSwitch" v-model="formData.shortcuts.saveEnabled"><label class="custom-control-label" for="saveShortcutSwitch"></label></div>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<label for="autoSaveSwitch" class="mb-0">自动保存配置</label>
<div class="custom-control custom-switch"><input type="checkbox" class="custom-control-input" id="autoSaveSwitch" v-model="formData.shortcuts.autoSaveEnabled"><label class="custom-control-label" for="autoSaveSwitch"></label></div>
</div>
<div class="d-flex justify-content-between align-items-center">
<label for="runShortcutSwitch" class="mb-0">运行任务 (Ctrl/Cmd + R)</label>
<div class="custom-control custom-switch"><input type="checkbox" class="custom-control-input" id="runShortcutSwitch" v-model="formData.shortcuts.runEnabled"><label class="custom-control-label" for="runShortcutSwitch"></label></div>
</div>
</div>
</div>
</div>
<div class="col-12">
<!-- Regex Card -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-magic mr-2"></i>正则设置</h5>
</div>
<div class="card-body p-4">
<!-- Global Regex -->
<div class="card mb-3">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0">全局正则</h6>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="globalRegexSwitch" v-model="formData.global_regex.enabled">
<label class="custom-control-label" for="globalRegexSwitch">{{ formData.global_regex.enabled ? '已启用' : '已禁用' }}</label>
</div>
</div>
<div class="card-body p-3">
<p class="text-muted small">启用后,所有任务将使用此处设置的正则表达式,忽略任务中的正则设置。</p>
<div class="input-group mb-2">
<div class="input-group-prepend"><span class="input-group-text">匹配表达式</span></div>
<input type="text" v-model="formData.global_regex.pattern" class="form-control" placeholder="输入正则表达式..." :disabled="!formData.global_regex.enabled">
</div>
<div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">替换表达式</span></div>
<input type="text" v-model="formData.global_regex.replace" class="form-control" placeholder="输入替换表达式..." :disabled="!formData.global_regex.enabled">
</div>
</div>
</div>
<!-- Magic Regex -->
<div class="card">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0 d-inline-flex align-items-center">预设正则(魔法匹配)
<a href="https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程#21-魔法匹配" target="_blank" class="ml-2 badge badge-light font-weight-normal">?</a>
</h6>
<button type="button" class="btn btn-outline-secondary btn-sm" @click="addMagicRegex()"><i class="bi bi-plus-lg"></i> 添加</button>
</div>
<div class="card-body p-3">
<p class="text-muted small">预定义正则匹配规则,可在任务中引用。</p>
<div v-for="(value, key) 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" :value="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 type="button" class="btn btn-outline-danger" @click="removeMagicRegex(key)"><i class="bi bi-trash"></i></button>
</div>
</div>
</div>
<div v-if="!Object.keys(formData.magic_regex).length" class="alert alert-light text-center small p-2">暂无魔法匹配规则</div>
</div>
</div>
</div>
</div>
<!-- Plugins Card -->
<div class="card mb-4" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0 d-inline-flex align-items-center"><i class="bi bi-plugs-fill mr-2"></i>插件配置
<a href="https://github.com/Cp0204/quark-auto-save/wiki/插件配置" target="_blank" class="ml-2 badge badge-light font-weight-normal">?</a>
</h5>
</div>
<div class="card-body p-4">
<p class="card-text text-muted small">各插件的配置选项,具体键值由插件定义。</p>
<div v-for="(plugin, pluginName) in getAvailablePlugins(formData.plugins)" :key="pluginName" class="mb-3">
<div class="card">
<div class="card-header bg-light" data-toggle="collapse" :data-target="'#collapse_'+pluginName">
<i class="bi bi-caret-right-fill"></i> <span class="font-weight-bold">{{ pluginName }}</span>
</div>
<div class="collapse" :id="'collapse_'+pluginName">
<div class="card-body">
<div v-for="(value, key) in plugin" :key="key" class="form-group row">
<label class="col-sm-3 col-lg-2 col-form-label">{{ key }}</label>
<div class="col-sm-9 col-lg-10">
<input type="text" v-model="formData.plugins[pluginName][key]" class="form-control">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- API Card -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0 d-inline-flex align-items-center"><i class="bi bi-link-45deg mr-2"></i>API
<a href="https://github.com/Cp0204/quark-auto-save/wiki/API接口" target="_blank" class="ml-2 badge badge-light font-weight-normal">?</a>
</h5>
</div>
<div class="card-body p-4">
<p class="card-text text-muted small">用于第三方添加任务等操作Token由系统自动生成。</p>
<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: #e9ecef;" disabled>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" @click="copyToken()" title="复制"><i class="bi bi-clipboard"></i></button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="activeTab === 'logs'">
<!-- 日志页内容 -->
<div class="d-flex justify-content-between align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">运行日志</h1>
<div class="col-5">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="bi bi-search"></i></span>
</div>
<input type="text" class="form-control" v-model="logSearchQuery" placeholder="搜索日志...">
<div class="input-group-append">
<button type="button" class="btn btn-outline-primary" @click="fetchCurrentLog()" title="刷新日志">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 错误提示 -->
<div v-if="run_log_page.includes('任务异常结束')" class="alert alert-danger mb-3" role="alert">
<i class="bi bi-exclamation-triangle-fill mr-2"></i> 任务异常结束!可能原因:网络问题、权限问题或其他错误。请检查日志详情。
<button type="button" class="btn btn-sm btn-outline-danger ml-3" @click="resetTaskStatus()">
<i class="bi bi-arrow-clockwise"></i> 重置任务状态
</button>
</div>
<div class="card"><div class="card-body p-2"><pre class="log-panel" v-html="filteredLog || '暂无日志记录。'"></pre></div></div>
</div>
<div v-if="activeTab === 'tasklist'">
<!-- 任务列表页内容 -->
<div class="d-flex justify-content-between align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">任务列表</h1>
<button type="button" class="btn btn-primary" @click="addTask()">
<i class="bi bi-plus-circle-fill mr-1"></i> 增加新任务
</button>
</div>
<div class="card mb-4">
<div class="card-body p-3">
<div class="row align-items-center">
<div class="col-md-5 col-lg-4 mb-2 mb-md-0 d-flex align-items-center">
<label class="custom-checkbox-wrapper mb-0 mr-3">
<input type="checkbox" @change="toggleSelectAllTasks" :checked="allTasksSelected">
<span class="custom-checkbox"><i class="bi bi-check"></i></span>
全选
</label>
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary dropdown-toggle btn-sm" data-toggle="dropdown" :disabled="selectedTasks.length === 0">批量操作 ({{ selectedTasks.length }})</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#" @click.prevent="bulkRunSelected()"><i class="bi bi-play-circle-fill text-primary"></i> 批量运行</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" @click.prevent="bulkToggleEnable(true)"><i class="bi bi-check-circle-fill text-success"></i> 批量启用</a>
<a class="dropdown-item" href="#" @click.prevent="bulkToggleEnable(false)"><i class="bi bi-slash-circle-fill text-secondary"></i> 批量禁用</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" @click.prevent="bulkDelete()"><i class="bi bi-trash-fill text-danger"></i> 批量删除</a>
</div>
</div>
</div>
<div class="col-md-7 col-lg-8">
<div class="input-group">
<div class="input-group-prepend"><span class="input-group-text"><i class="bi bi-search"></i></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" @click="clearData('taskNameFilter')" title="清除"><i class="bi bi-x-lg"></i></button></div>
<select class="form-control" v-model="taskDirSelected">
<option value="">所有路径</option>
<option v-for="(dir, index) in taskDirs" :key="index" :value="dir" v-html="dir"></option>
</select>
<div class="input-group-append"><button type="button" class="btn btn-outline-secondary" @click="clearData('taskDirSelected')" title="清除"><i class="bi bi-x-lg"></i></button></div>
</div>
</div>
</div>
</div>
</div>
<div v-for="(task, index) in formData.tasklist" :key="task.id">
<template v-if="(taskDirSelected == '' || getParentDirectory(task.savepath) == taskDirSelected) && (task.taskname||'').toLowerCase().includes(taskNameFilter.toLowerCase())">
<div class="card task-card mb-3">
<div class="card-header d-flex align-items-center" data-toggle="collapse" :data-target="'#collapse_'+task.id">
<label class="custom-checkbox-wrapper mb-0 mr-1" @click.stop>
<input type="checkbox" v-model="selectedTasks" :value="task.id">
<span class="custom-checkbox"><i class="bi bi-check"></i></span>
</label>
<span class="task-status-indicator" :class="getTaskStatusClass(task)" :title="getTaskStatusTitle(task)"></span>
<span class="font-weight-bold flex-grow-1">#{{ index + 1 }}: {{ task.taskname || '未命名任务' }}</span>
<div class="task-actions">
<button class="btn btn-sm btn-outline-warning" v-if="task.shareurl_ban" :title="task.shareurl_ban" disabled @click.stop><i class="bi bi-exclamation-triangle-fill"></i></button>
<button type="button" class="btn btn-sm btn-outline-primary" @click.stop="runScriptNow([task.id])" title="运行此任务" v-else><i class="bi bi-play-fill"></i></button>
<button type="button" class="btn btn-sm btn-outline-danger ml-2" @click.stop="removeTask(task.id)" title="删除此任务"><i class="bi bi-trash3-fill"></i></button>
</div>
</div>
<div class="collapse" :id="'collapse_'+task.id"><div class="card-body">
<div class="alert alert-warning" role="alert" v-if="task.shareurl_ban" v-html="task.shareurl_ban"></div>
<div class="form-group row"><label class="col-sm-3 col-lg-2 col-form-label">任务名称</label><div class="col-sm-9 col-lg-10"><div class="input-group"><input type="text" class="form-control" v-model="task.taskname" @focus="focusTaskname(task)" @input="changeTaskname(task)"><div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.taskSuggestions.success && smart_param.task_id === task.id"><div class="dropdown-item text-muted text-center" style="font-size:12px;">{{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : 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(task.id, suggestion)" style="font-size: 12px;" :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"><button class="btn btn-primary" type="button" @click="searchSuggestions(task.id, task.taskname)" title="深度搜索"><i v-if="smart_param.isSearching && smart_param.task_id === task.id" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></i><i v-else class="bi bi-search-heart"></i></button><a target="_blank" :href="`https://www.google.com/search?q=%22pan.quark.cn/s%22+${task.taskname}`" class="btn btn-outline-secondary" title="Google搜索"><i class="bi bi-google"></i></a></div></div></div></div>
<div class="form-group row"><label class="col-sm-3 col-lg-2 col-form-label">分享链接</label><div class="col-sm-9 col-lg-10"><div class="input-group"><input type="text" class="form-control" v-model="task.shareurl" @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;fileSelect.sortBy='file_name';fileSelect.sortOrder='desc';showShareSelect(task.id)" title="选择文件夹"><i class="bi bi-folder"></i></button><a target="_blank" :href="task.shareurl" class="btn btn-outline-secondary" title="打开链接"><i class="bi bi-box-arrow-up-right"></i></a></div></div></div></div>
<div class="form-group row"><label class="col-sm-3 col-lg-2 col-form-label">保存路径</label><div class="col-sm-9 col-lg-10"><div class="input-group"><input type="text" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(task, index)"><div class="input-group-append"><button class="btn btn-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"><i class="bi bi-reply"></i></button><button class="btn btn-outline-secondary" type="button" @click="fileSelect.sortBy='file_name';fileSelect.sortOrder='asc';showSavepathSelect(index)">选择</button></div></div></div></div>
<div class="form-group row"><label class="col-sm-3 col-lg-2 col-form-label">保存规则</label><div class="col-sm-9 col-lg-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';fileSelect.sortOrder='asc';showShareSelect(task.id)" title="预览正则处理效果">正则处理</button></div><input type="text" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex"><input type="text" class="form-control" v-model="task.replace" placeholder="替换表达式"><div class="input-group-append"><div class="input-group-text"><input type="checkbox" v-model="task.ignore_extension"> 忽略后缀</div></div></div><datalist id="magicRegex"><option v-for="(value, key) in formData.magic_regex" :key="key" :value="`${key}`" v-html="`${value.pattern.replace('<', '<\u200B')} → ${value.replace}`"></option></datalist></div></div>
<div class="form-group row"><label class="col-sm-3 col-lg-2 col-form-label">文件起始</label><div class="col-sm-9 col-lg-10"><div class="input-group"><input type="text" class="form-control" v-model="task.startfid" placeholder="留空则转存分享链接中的所有文件"><div class="input-group-append" v-if="task.shareurl"><button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=false;fileSelect.previewRegex=false;fileSelect.sortBy='file_name';fileSelect.sortOrder='desc';showShareSelect(task.id)" title="选择起始文件"><i class="bi bi-file-earmark-check"></i></button></div></div></div></div>
<div class="form-group row"><label class="col-sm-3 col-lg-2 col-form-label">运行星期</label><div class="col-sm-9 col-lg-10 col-form-label"><div class="form-check form-check-inline"><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, d_index) in weekdays" :key="d_index"><input class="form-check-input" type="checkbox" v-model="task.runweek" :value="d_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"><label class="col-sm-3 col-lg-2 col-form-label">插件选项</label><div class="col-sm-9 col-lg-10"><v-jsoneditor v-model="task.addition" :options="{mode:'tree'}" :plus="false" height="180px"></v-jsoneditor></div></div>
</div></div>
</div>
</template>
</div>
<div v-if="!formData.tasklist.some(task => (taskDirSelected == '' || getParentDirectory(task.savepath) == taskDirSelected) && (task.taskname||'').toLowerCase().includes(taskNameFilter.toLowerCase()))" class="text-center text-muted p-5"><i class="bi bi-journal-x" style="font-size: 3rem;"></i><p class="mt-3">没有找到匹配的任务。<br>可以尝试调整筛选条件或<a href="#" @click.prevent="addTask()">创建新任务</a></p></div>
</div>
</form>
</main>
</div>
</div>
<button id="backToTopBtn" class="btn" @click="scrollToTop" title="返回顶部"><i class="bi bi-arrow-up"></i></button>
<div id="autoSaveNotification">
<i class="bi bi-check-circle mr-1"></i> 已自动保存
</div>
<div id="actionNotification" class="action-notification">
<i class="bi bi-check-circle mr-1"></i> <span id="actionMessage"></span>
</div>
<div class="modal fade" tabindex="-1" id="fileSelectModal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-folder2-open mr-2"></i>
<b v-if="fileSelect.previewRegex">正则处理预览</b>
<b v-else-if="fileSelect.selectDir">选择{{fileSelect.selectShare ? '需转存的' : '保存到的'}}文件夹</b>
<b v-else>选择起始文件</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">
<span aria-hidden="true">×</span>
</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>
<table class="table table-hover table-sm">
<thead>
<tr>
<th scope="col" class="cursor-pointer" @click="sortFileList('file_name')">
文件名
<span v-if="fileSelect.sortBy === 'file_name'">{{ fileSelect.sortOrder === "asc" ? "↑" : "↓" }}</span>
</th>
<th scope="col" v-if="fileSelect.previewRegex">预览结果</th>
<template v-else>
<th scope="col">大小</th>
<th scope="col" class="cursor-pointer" @click="sortFileList('updated_at')">
修改日期
<span v-if="fileSelect.sortBy === 'updated_at'">{{ fileSelect.sortOrder === "asc" ? "↑" : "↓" }}</span>
</th>
<th scope="col" v-if="!fileSelect.selectShare">操作</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) : null) : selectStartFid(file.fid)" :class="{'cursor-pointer': fileSelect.selectDir ? file.dir : true}">
<td><i class="bi mr-1" :class="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></i>{{file.file_name}}</td>
<template v-if="!fileSelect.previewRegex">
<td v-if="file.dir">{{ file.include_items }}项</td>
<td v-else>{{file.size | size}}</td>
<td>{{file.updated_at | ts2date}}</td>
<td v-if="!fileSelect.selectShare"><a @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td>
</template>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer" v-if="fileSelect.selectDir && !fileSelect.previewRegex">
<span v-html="fileSelect.selectShare ? '转存:' : '保存到:'"></span>
<button type="button" class="btn btn-primary btn-sm" @click="selectCurrentFolder()">当前文件夹</button>
<button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare && fileSelect.index < formData.tasklist.length" @click="selectCurrentFolder(true)">当前文件夹<span class="badge badge-light" v-html="'/'+formData.tasklist[fileSelect.index].taskname"></span></button>
</div>
</div>
</div>
</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
version: "v0.7.0",
versionTips: "v0.7.0",
plugin_flags: "[[ plugin_flags ]]",
weekdays: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
formData: {
cookie: [],
crontab: "",
push_config: {},
plugins: {},
tasklist: [],
magic_regex: {},
file_blacklist: [],
api_token: "",
shortcuts: { saveEnabled: true, runEnabled: true, autoSaveEnabled: true },
source: { cloudsaver: { server: "", username: "", password: "", token: "" } },
global_regex: { enabled: false, pattern: "", replace: "" }
},
newTask: { taskname: "", shareurl: "", savepath: "/", pattern: "", replace: "", enddate: "", addition: {}, ignore_extension: false, startfid: "", runweek: [1, 2, 3, 4, 5, 6, 7] },
run_log_page: "",
logSearchQuery: "",
activeTab: 'tasklist',
taskNameFilter: "",
taskDirSelected: "",
selectedTasks: [],
modalLoading: false,
configModified: false,
autoSaveTimer: null,
notificationTimer: null,
logRefreshTimer: null,
errorNotified: false,
smart_param: { task_id: null, index: null, savepath: "", origin_savepath: "", taskSuggestions: {}, showSuggestions: false, isSearching: false, searchTimer: null },
fileSelect: { index: null, task_id: null, shareurl: "", stoken: "", fileList: [], paths: [], selectDir: true, selectShare: true, previewRegex: false, sortBy: "updated_at", sortOrder: "desc" },
},
filters: {
ts2date(value){if(!value)return'';const d=new Date(value);return`${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes().toString().padStart(2,'0')}`},
size(value){if(!value)return"";const u=["B","KB","MB","GB","TB"],s=parseFloat(value),i=s?Math.floor(Math.log(s)/Math.log(1024)):0;return(s/Math.pow(1024,i)).toFixed(1).replace(/\.?0+$/,"")+u[i]}
},
computed: {
// 注意: 主任务列表渲染现在使用带有内部v-if的v-for来确保索引稳定性。
// 这个计算属性现在只用于"全选"功能。
filteredTasks() { return this.formData.tasklist.filter(t=>(t.taskname||'').toLowerCase().includes(this.taskNameFilter.toLowerCase())&&(this.taskDirSelected===""||this.getParentDirectory(t.savepath)===this.taskDirSelected))},
allTasksSelected() { if(this.filteredTasks.length===0)return false;return this.selectedTasks.length===this.filteredTasks.length},
filteredLog() {
if (!this.logSearchQuery.trim()) return this.run_log_page;
const query = this.logSearchQuery.trim().replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
const regex = new RegExp(query, 'gi');
return this.run_log_page.replace(regex, `<span class="log-highlight">$&</span>`);
}
},
mounted() {
this.fetchData();
this.checkNewVersion();
document.querySelector('main').addEventListener('scroll', this.handleScroll);
document.addEventListener('keydown', this.handleKeyDown);
// 添加表单输入监听
this.$nextTick(() => {
document.querySelectorAll('input, textarea, select').forEach(el => {
el.addEventListener('input', this.handleInputChange);
});
});
// 获取当前运行的日志
this.fetchCurrentLog();
},
created() {
// 检查是否有任务正在运行,并自动启动日志刷新
this.checkRunningTask();
// 添加页面关闭前的事件处理,停止计时器
window.addEventListener('beforeunload', this.stopLogRefresh);
},
beforeDestroy() {
document.querySelector('main').removeEventListener('scroll', this.handleScroll);
document.removeEventListener('keydown', this.handleKeyDown);
// 移除表单输入监听
document.querySelectorAll('input, textarea, select').forEach(el => {
el.removeEventListener('input', this.handleInputChange);
});
// 清除日志刷新定时器
this.stopLogRefresh();
},
watch: {
formData: {
handler: function(newVal, oldVal) {
// 仅在初始化后才触发自动保存
if (this.configModified && this.formData.shortcuts.autoSaveEnabled) {
// 使用防抖函数避免频繁保存
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer);
this.autoSaveTimer = setTimeout(() => {
this.autoSave();
}, 1500); // 1.5秒防抖
}
},
deep: true
},
activeTab: function(newTab) {
// 当切换到日志页面时,获取最新日志
if (newTab === 'logs') {
this.fetchCurrentLog();
// 开始定时刷新日志
this.startLogRefresh();
} else {
// 离开日志页面时,停止定时刷新
this.stopLogRefresh();
}
}
},
methods: {
// --- 核心应用逻辑 ---
fetchData(){axios.get('/data').then(r=>{let d=r.data.data;d.tasklist=d.tasklist||[];d.tasklist.forEach((t,i)=>{t.id=t.id||`task_${Date.now()}_${i}`;if(!t.hasOwnProperty('runweek'))t.runweek=[1,2,3,4,5,6,7];if(!t.hasOwnProperty('addition'))t.addition={};if(!t.hasOwnProperty('startfid'))t.startfid="";});if(!d.shortcuts){d.shortcuts={saveEnabled:true,runEnabled:true,autoSaveEnabled:false};}this.formData=d;this.formData.plugins=d.plugins||{};this.formData.file_blacklist=d.file_blacklist||[];this.updateTaskDirs(d.tasklist);setTimeout(()=>this.configModified=false,100);}).catch(e=>console.error('获取数据出错:',e))},
saveConfig(){
axios.post('/update',this.formData).then(r=>{
if(r.data.success){
this.configModified=false;
this.showActionNotification("配置已成功保存!");
}else{
this.showActionNotification("保存失败: "+r.data.message, "error");
}
}).catch(e=>{
console.error('保存配置出错:',e);
this.showActionNotification("保存配置出错", "error");
})
},
autoSave() {
axios.post('/update', this.formData).then(r => {
if (r.data.success) {
this.configModified = false;
this.showAutoSaveNotification();
}
}).catch(e => console.error('自动保存配置出错:', e));
},
showAutoSaveNotification() {
const notification = document.getElementById('autoSaveNotification');
notification.classList.add('show');
notification.classList.remove('hide');
if (this.notificationTimer) clearTimeout(this.notificationTimer);
this.notificationTimer = setTimeout(() => {
notification.classList.remove('show');
notification.classList.add('hide');
}, 3000);
},
handleInputChange() {
this.configModified = true;
},
runScriptNow(taskIds=null,test=false){
let body={};
if(test){
body={quark_test:true,cookie:this.formData.cookie,push_config:this.formData.push_config};
}else if(taskIds){
const tasksToRun=this.formData.tasklist.filter(t=>taskIds.includes(t.id)).map(t=>({...t,id:undefined}));
if(tasksToRun.length===0)return;
body={tasklist:tasksToRun};
}else if(this.configModified){
this.showActionNotification("配置已修改但未保存,自动保存中...");
this.autoSave();
}
this.activeTab='logs';
this.run_log_page='';
// 重置错误通知状态
this.errorNotified = false;
this.modalLoading=true;
fetch(`/run_script_now`,{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
}).then(r=>{
const reader=r.body.getReader();
const decoder=new TextDecoder();
const process=({done,value})=>{
if(done){
this.modalLoading=false;
return;
}
const chunk=decoder.decode(value,{stream:true});
const lines=chunk.split('\n').filter(l=>l.startsWith('data:'));
for(const line of lines){
const eventData=line.substring(5).trim();
if(eventData==='[DONE]'){
this.modalLoading=false;
return;
}
const cleanData=eventData.replace(/</g,'<\u200B')+'\n';
this.run_log_page+=cleanData;
this.$nextTick(()=>{
const el=document.querySelector('.log-panel');
if(el)el.scrollTop=el.scrollHeight;
});
}
return reader.read().then(process);
};
return reader.read().then(process);
}).catch(e=>{
this.modalLoading=false;
const err='运行出错: '+e;
this.run_log_page=err;
})
},
checkNewVersion(){axios.get('https://api.github.com/repos/Cp0204/quark-auto-save/tags').then(r=>{const latest=r.data[0].name;if(latest!=this.version){this.versionTips=`${this.version} <sup><span class="badge badge-danger ml-1">${latest}</span></sup>`;}}).catch(e=>{console.error('检查新版本出错:',e);});},
handleKeyDown(event){if(event.ctrlKey||event.metaKey){if(event.key==='s'&&this.formData.shortcuts.saveEnabled){event.preventDefault();this.saveConfig();}else if(event.key==='r'&&this.formData.shortcuts.runEnabled){event.preventDefault();this.runScriptNow();}}},
changeTab(tab){this.activeTab=tab;if(window.innerWidth<=768)$('#sidebarMenu').collapse('toggle')},
// --- 配置页方法 ---
addCookie(){this.formData.cookie.push("");this.configModified=true},
removeCookie(idx){this.formData.cookie.splice(idx,1);this.configModified=true;this.showActionNotification("已删除Cookie");},
testPush(){this.runScriptNow(null,true)},
addPush(){const key=prompt("增加的键名","");if(key){this.$set(this.formData.push_config,key,"");this.configModified=true;this.showActionNotification("已添加通知推送");}},
removePush(key){this.$delete(this.formData.push_config,key);this.configModified=true;this.showActionNotification("已删除通知推送");},
addBlacklistItem(){if(!this.formData.file_blacklist)this.$set(this.formData,'file_blacklist',[]);this.formData.file_blacklist.push("");this.configModified=true},
removeBlacklistItem(idx){this.formData.file_blacklist.splice(idx,1);this.configModified=true;this.showActionNotification("已删除黑名单项");},
addMagicRegex(){const k=`$MAGIC_${Object.keys(this.formData.magic_regex).length+1}`;this.$set(this.formData.magic_regex,k,{pattern:'',replace:''});this.configModified=true},
updateMagicRegexKey(ok,nk){if(ok!==nk){this.$set(this.formData.magic_regex,nk,this.formData.magic_regex[ok]);this.$delete(this.formData.magic_regex,ok);this.configModified=true}},
removeMagicRegex(key){this.$delete(this.formData.magic_regex,key);this.configModified=true;this.showActionNotification(`已删除魔法匹配 [${key}]`);},
// --- 任务列表与插件方法 ---
clearData(key) { if (key === 'taskNameFilter') {this.taskNameFilter = "";} else if (key === 'taskDirSelected') {this.taskDirSelected = "";}},
getTaskStatusClass(task){if(task.shareurl_ban)return'status-warning';if(task.runweek&&task.runweek.length>0)return'status-active';return'status-disabled'},
getTaskStatusTitle(task){if(task.shareurl_ban)return`警告: ${task.shareurl_ban}`;if(task.runweek&&task.runweek.length>0)return'已启用';return'已禁用'},
getAvailablePlugins(p){if(!p)return{};const aP={},pFA=this.plugin_flags.split(',');for(const pN in p){if(!pFA.includes(`-${pN}`))aP[pN]=p[pN];}return aP},
getTaskById(id){return this.formData.tasklist.find(t=>t.id===id)},
addTask(){const nT={...this.newTask,id:`task_${Date.now()}_${Math.random()}`};nT.taskname=this.taskNameFilter;this.formData.tasklist.push(nT);this.updateTaskDirs();this.$nextTick(()=>{const mainEl=document.querySelector('main');$(`#collapse_${nT.id}`).collapse('show');mainEl.scrollTo({top:mainEl.scrollHeight,behavior:"smooth"})})},
removeTask(id){const idx=this.formData.tasklist.findIndex(t=>t.id===id);if(idx>-1){const taskName=this.formData.tasklist[idx].taskname;this.formData.tasklist.splice(idx,1);this.updateTaskDirs();this.showActionNotification(`已删除任务 [${taskName}]`);}},
updateTaskDirs(tl=this.formData.tasklist){const d=new Set([""]);tl.forEach(i=>d.add(this.getParentDirectory(i.savepath)));this.taskDirs=Array.from(d).sort()},
focusTaskname(t, index){this.smart_param.task_id=t.id;this.smart_param.index=index;this.smart_param.origin_savepath=t.savepath;const r=new RegExp(`/${t.taskname.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')}(/|$)`);if(t.savepath.includes('TASKNAME'))this.smart_param.savepath=t.savepath;else if(t.savepath.match(r))this.smart_param.savepath=t.savepath.replace(t.taskname,'TASKNAME');else this.smart_param.savepath=undefined},
changeTaskname(t){if(this.smart_param.searchTimer)clearTimeout(this.smart_param.searchTimer);this.smart_param.searchTimer=setTimeout(()=>this.searchSuggestions(t.id,t.taskname),1000);if(this.smart_param.savepath)t.savepath=this.smart_param.savepath.replace('TASKNAME',t.taskname)},
changeShareurl(t){if(!t.shareurl)return;this.$set(t,"shareurl_ban",undefined);try{const m=decodeURIComponent(t.shareurl).match(/\/(\w{32})-([^\/]+)$/);if(m){t.taskname=t.taskname==""?m[2]:t.taskname;t.savepath=t.savepath.replace(/TASKNAME/g,m[2]);}}catch(e){console.error("解析URL出错:",e);}
axios.post('/get_share_detail',{shareurl:t.shareurl}).then(r=>{const d=r.data.data;if(!r.data.success){if(d.error.includes("提取码")){const p=prompt(`检查失败[${d.error}],请输入提取码:`);if(p!=null){t.shareurl=t.shareurl.replace(/pan.quark.cn\/s\/(\w+)(\?pwd=\w*)*/,`pan.quark.cn/s/$1?pwd=${p}`);this.changeShareurl(t);return;}}this.$set(t,"shareurl_ban",d.error);this.showActionNotification(`分享链接检查失败: ${d.error}`,"error");}else{t.taskname=t.taskname==""?d.share.title:t.taskname;t.savepath=t.savepath.replace(/TASKNAME/g,d.share.title);this.$set(t,"shareurl_ban",undefined);this.showActionNotification("分享链接检查成功");}}).catch(e=>{console.error('获取分享详情出错:',e);this.showActionNotification("获取分享详情出错","error");})},
toggleAllWeekdays(t){t.runweek=t.runweek.length===7?[]:[1,2,3,4,5,6,7]},
toggleSelectAllTasks(e){this.selectedTasks=e.target.checked?this.filteredTasks.map(t=>t.id):[]},
bulkRunSelected(){if(this.selectedTasks.length>0){this.runScriptNow(this.selectedTasks);this.showActionNotification(`正在运行 ${this.selectedTasks.length} 个任务`);}},
bulkDelete(){if(this.selectedTasks.length>0){this.formData.tasklist=this.formData.tasklist.filter(t=>!this.selectedTasks.includes(t.id));this.showActionNotification(`已删除 ${this.selectedTasks.length} 个任务`);this.selectedTasks=[];this.updateTaskDirs();}},
bulkToggleEnable(e){if(this.selectedTasks.length>0){this.formData.tasklist.forEach(t=>{if(this.selectedTasks.includes(t.id))t.runweek=e?[1,2,3,4,5,6,7]:[];});this.selectedTasks=[]}},
getParentDirectory(p){const pd=p.substring(0,p.lastIndexOf('/'));return pd===""?"/":pd},
handleScroll(e){const btn=document.getElementById('backToTopBtn');if(btn)btn.style.display=e.target.scrollTop>300?'flex':'none'},
scrollToTop(){document.querySelector('main').scrollTo({top:0,behavior:'smooth'})},
searchSuggestions(id,q,d=0){if(q.length<2)return;this.smart_param.isSearching=true;this.smart_param.task_id=id;axios.get('/task_suggestions',{params:{q,d}}).then(r=>{this.smart_param.taskSuggestions=r.data;this.smart_param.showSuggestions=true;}).finally(()=>this.smart_param.isSearching=false)},
selectSuggestion(id,sug){this.smart_param.showSuggestions=false;this.fileSelect={...this.fileSelect,selectDir:true,previewRegex:false};this.showShareSelect(id,sug.shareurl)},
showShareSelect(id,url=null){const t=this.getTaskById(id);if(!t)return;this.fileSelect={...this.fileSelect,selectShare:true,fileList:[],paths:[],error:undefined,task_id:id};const nU=url||t.shareurl;if(this.getShareurl(this.fileSelect.shareurl)!=this.getShareurl(nU))this.fileSelect.stoken="";this.fileSelect.shareurl=nU;$('#fileSelectModal').modal('show');this.getShareDetail()},
getShareDetail(){this.modalLoading=true;axios.post('/get_share_detail',{shareurl:this.fileSelect.shareurl,stoken:this.fileSelect.stoken,task:this.getTaskById(this.fileSelect.task_id),magic_regex:this.formData.magic_regex,global_regex:this.formData.global_regex}).then(r=>{if(r.data.success){this.fileSelect.fileList=r.data.data.list;this.sortFileList('file_name','desc');this.fileSelect.paths=r.data.data.paths;this.fileSelect.stoken=r.data.data.stoken;}else{this.fileSelect.error=r.data.data.error;}this.modalLoading=false;}).catch(e=>{this.fileSelect.error="获取失败";this.modalLoading=false;})},
// --- 开始: 带有日志记录的路径选择旧逻辑 ---
showSavepathSelect(index) {
console.log(`[showSavepathSelect] 为索引 ${index} 调用函数`);
console.log(`[showSavepathSelect] 目标任务:`, this.formData.tasklist[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;
$('#fileSelectModal').modal('toggle');
this.formData.tasklist[index].savepath = this.formData.tasklist[index].savepath.replace(/\/+/g, "/");
this.getSavepathDetail(this.formData.tasklist[index].savepath);
},
getSavepathDetail(params = 0) {
if (typeof params === 'string' && params.includes('/')) {
params = { path: params };
} else {
params = { fid: params };
}
console.log("[getSavepathDetail] 使用参数请求路径详细信息:", params);
this.modalLoading = true;
axios.get('/get_savepath_detail', { params: params })
.then(response => {
console.log("[getSavepathDetail] [成功] 接收到数据:", response.data);
this.fileSelect.fileList = response.data.data.list;
this.sortFileList(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 => {
console.error("[getSavepathDetail] [错误] 获取路径详细信息失败:", error);
this.fileSelect.error = "获取文件夹列表失败,请检查浏览器控制台(F12)中的错误信息。";
this.modalLoading = false;
});
},
navigateTo(fid, name) {
const dir = { fid: fid, name: name };
if (this.fileSelect.selectShare) {
if (dir.fid == 0) {
this.fileSelect.shareurl = this.fileSelect.shareurl.match(`.*s/[a-z0-9]+(\\?pwd=[^#]+)?`)[0]
} else if (this.fileSelect.shareurl.includes(dir.fid)) {
this.fileSelect.shareurl = this.fileSelect.shareurl.match(`.*/${dir.fid}[^/]*`)[0]
} else if (this.fileSelect.shareurl.includes('#/list/share')) {
this.fileSelect.shareurl = `${this.fileSelect.shareurl}/${dir.fid}-${dir.name?.replace('-', '*101')}`
} else {
this.fileSelect.shareurl = `${this.fileSelect.shareurl}#/list/share/${dir.fid}-${dir.name?.replace('-', '*101')}`
}
this.getShareDetail();
} else {
if (fid === "0") {
this.fileSelect.paths = [];
} else {
let 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.getSavepathDetail(fid);
}
},
selectCurrentFolder(addTaskname = false) {
let newPath = "/" + this.fileSelect.paths.map(item => item.name).join("/");
if (addTaskname) {
newPath += "/" + this.formData.tasklist[this.fileSelect.index].taskname;
}
console.log(`[selectCurrentFolder] 为索引 ${this.fileSelect.index} 设置路径: "${newPath}"`);
this.formData.tasklist[this.fileSelect.index].savepath = newPath || "/";
this.updateTaskDirs();
$('#fileSelectModal').modal('hide');
},
sortFileList(column, order) {
if (this.fileSelect.sortBy === column && !order) {
this.fileSelect.sortOrder = this.fileSelect.sortOrder === "asc" ? "desc" : "asc";
} else {
this.fileSelect.sortBy = column;
this.fileSelect.sortOrder = order || "asc";
}
this.fileSelect.fileList.sort((a, b) => {
let valA = a[this.fileSelect.sortBy];
let valB = b[this.fileSelect.sortBy];
if (typeof valA === "string") valA = valA.toLowerCase();
if (typeof valB === "string") valB = valB.toLowerCase();
if (valA < valB) return this.fileSelect.sortOrder === "asc" ? -1 : 1;
if (valA > valB) return this.fileSelect.sortOrder === "asc" ? 1 : -1;
return 0;
});
},
// --- 结束: 带有日志记录的路径选择旧逻辑 ---
selectStartFid(fid){const t=this.getTaskById(this.fileSelect.task_id);if(t){this.$set(t,'startfid',fid);$('#fileSelectModal').modal('hide')}},
getShareurl(url,dir={}){if(Object.keys(dir).length===0||dir.fid==0){url=url.match(`.*s/[a-z0-9]+(\\?pwd=[^#]+)?`)[0]}else if(url.includes(dir.fid)){url=url.match(`.*/${dir.fid}[^/]*`)[0]}else if(url.includes('#/list/share')){url=`${url}/${dir.fid}-${dir.name?.replace('-','*101')}`}else{url=`${url}#/list/share/${dir.fid}-${dir.name?.replace('-','*101')}`}return url;},
deleteFile(fid,fname,isDir){if(fid!=""){axios.post('/delete_file',{fid:fid}).then(r=>{if(r.data.code==0){this.fileSelect.fileList=this.fileSelect.fileList.filter(i=>i.fid!=fid);this.showActionNotification(`已删除${isDir?'目录':'文件'} [${fname}]`);}else{this.showActionNotification('删除失败:'+r.data.message,"error");}}).catch(e=>{console.error('删除文件出错:',e);this.showActionNotification('删除文件出错',"error");});}},
fetchCurrentLog() {
axios.get('/get_current_log')
.then(response => {
if (response.data.success) {
if (response.data.log) {
if (this.run_log_page !== response.data.log) {
console.log('日志已更新');
const oldLog = this.run_log_page;
this.run_log_page = response.data.log;
if (this.run_log_page.includes('任务异常结束') && !this.errorNotified) {
this.errorNotified = true;
this.showActionNotification("任务异常结束,请查看日志获取详情", "error");
}
if (this.run_log_page.trim() !== '') {
this.$nextTick(() => {
const el = document.querySelector('.log-panel');
if (el) el.scrollTop = el.scrollHeight;
});
}
}
}
if (response.data.running) {
console.log('任务仍在运行中');
this.modalLoading = true;
} else {
console.log('任务已完成或未运行');
this.modalLoading = false;
if (this.run_log_page.includes('任务异常结束') && !this.errorNotified) {
this.errorNotified = true;
this.showActionNotification("任务异常结束,请查看日志获取详情", "error");
}
}
}
})
.catch(error => {
console.error('获取当前日志失败:', error);
});
},
startLogRefresh() {
console.log('开始日志刷新');
this.stopLogRefresh();
this.logRefreshTimer = setInterval(() => {
console.log('定时刷新日志');
this.fetchCurrentLog();
}, 3000);
},
stopLogRefresh() {
if (this.logRefreshTimer) {
console.log('停止日志刷新');
clearInterval(this.logRefreshTimer);
this.logRefreshTimer = null;
}
},
showActionNotification(message,type="success"){const notification=document.getElementById('actionNotification');const messageEl=document.getElementById('actionMessage');messageEl.textContent=message;if(type==="error"){notification.style.backgroundColor="#dc3545";}else{notification.style.backgroundColor="var(--primary-color)";}
notification.classList.add('show');notification.classList.remove('hide');setTimeout(()=>{notification.classList.remove('show');notification.classList.add('hide');},3000);},
copyToken() {
const tokenInput = document.querySelector('input[v-model="formData.api_token"]');
if (tokenInput) {
const textarea = document.createElement('textarea');
textarea.value = this.formData.api_token;
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
this.showActionNotification('Token已复制到剪贴板');
}
},
pasteCookie(index) {
// 使用Vue的方式先清空输入框
this.$set(this.formData.cookie, index, "");
// 然后读取剪贴板内容并粘贴
navigator.clipboard.readText()
.then(text => {
this.$set(this.formData.cookie, index, text);
this.configModified = true;
this.showActionNotification('已成功粘贴Cookie');
})
.catch(err => {
console.error('粘贴失败:', err);
this.showActionNotification('粘贴失败,请手动粘贴', 'error');
});
},
checkRunningTask() {
axios.get('/get_current_log')
.then(response => {
if (response.data.success) {
if (response.data.log && response.data.log.trim() !== '') {
this.run_log_page = response.data.log;
this.activeTab = 'logs';
this.startLogRefresh();
}
if (response.data.running) {
console.log('检测到任务正在运行,自动切换到日志页面');
this.modalLoading = true;
this.activeTab = 'logs';
this.startLogRefresh();
}
}
})
.catch(error => {
console.error('检查运行任务失败:', error);
setTimeout(() => this.checkRunningTask(), 3000);
});
},
resetTaskStatus() {
if (confirm("确定要重置任务状态吗?这将终止所有正在运行的任务。")) {
this.modalLoading = true;
this.errorNotified = false;
axios.post('/reset_task_status')
.then(response => {
if (response.data.success) {
this.showActionNotification("任务状态已重置");
this.activeTab = 'logs';
this.fetchCurrentLog();
} else {
this.showActionNotification("重置失败: " + response.data.message, "error");
}
})
.catch(error => {
console.error('重置任务状态失败:', error);
this.showActionNotification("重置任务状态失败", "error");
})
.finally(() => {
this.modalLoading = false;
});
}
},
}
});
</script>
</body>
</html>