quark-auto-save/app/templates/index.html
Cp0204 55e338f35c
Some checks failed
Docker Publish / build-and-push (push) Has been cancelled
fix: 修复任务导入参数缺失导致的错误
2025-12-28 12:23:20 +08:00

1353 lines
66 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/dashboard.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>
</head>
<body>
<div id="app">
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
<a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#"><i class="bi bi-clouds"></i> 夸克自动转存</a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse" data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false">
<span class="navbar-toggler-icon"></span>
</button>
</nav>
<div class="container-fluid">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="sidebar-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<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">
<a class="nav-link" href="#" :class="{active: activeTab === 'tasklist'}" @click="changeTab('tasklist')">
<i class="bi bi-list-task"></i> 任务列表
</a>
</li>
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">dashboard</h6>
<li class="nav-item">
<a class="nav-link" href="/logout">
<i class="bi bi-box-arrow-right"></i></i> 退出
</a>
</li>
</ul>
<div class="nav-bottom text-center">
<p class="position-relative" hidden>
<b class="text-success"><i class="bi bi-record-circle mr-1"></i>视频教程</b>
<span class="position-absolute qrcode-tutorial">
使用夸克扫码查看<br>
<img src="./static/img/qrcode_tutorial.png">
</span>
</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><span v-html="versionTips"></span></p>
</div>
</div>
</nav>
<main class="col-md-9 col-lg-10 ml-sm-auto">
<form @submit.prevent="saveConfig" @keydown.enter.prevent>
<div v-if="activeTab === 'config'">
<div class="row title">
<div class="col-10">
<h2><i class="bi bi-cookie"></i> Cookie</h2>
</div>
<div class="col-2 text-right">
<button type="button" class="btn btn-outline-primary" @click="addCookie()">+</button>
</div>
</div>
<p>1. 所有账号执行签到,纯<a class="" href="https://github.com/Cp0204/quark-auto-save/wiki/使用技巧集锦#每日签到领空间">签到</a>只需移动端参数即可!</p>
<p>2. 仅第一个账号执行转存,请自行确认顺序。<b>最好是手机验证码<a target="_blank" href="https://pan.quark.cn/">登录</a>CK比较完整</b>如需签到参数附在CK后面。</p>
<div v-for="(value, index) in formData.cookie" :key="index" class="input-group mb-2">
<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-danger" @click="removeCookie(index)">-</button>
</div>
</div>
<div class="row title">
<div class="col">
<h2 style="display: inline-block;"><i class="bi bi-clock"></i> 定时规则</h2>
<span class="badge badge-pill badge-light">
<a target="_blank" href="https://tool.lu/crontab/" title="CRON时间计算器">?</a>
</span>
</div>
</div>
<div class="input-group mb-2">
<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 class="row title" title="通知推送支持多个渠道见Wiki">
<div class="col-8">
<h2 style="display: inline-block;"><i class="bi bi-bell"></i> 通知</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/通知推送服务配置" target="_blank">?</a>
</span>
</div>
<div class="col-4 text-right">
<button type="button" class="btn btn-success" title="通知推送测试" @click="testPush()"><i class="bi bi-lightning"></i></button>
<button type="button" class="btn btn-outline-primary" @click="addPush()">+</button>
</div>
</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" 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 type="button" class="btn btn-outline-danger" @click="removePush(key)">-</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;"><i class="bi bi-plug"></i> 插件</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/插件配置" target="_blank">?</a>
</span>
</div>
</div>
<div v-for="(plugin, pluginName) in getAvailablePlugins(formData.plugins)" :key="pluginName">
<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 ml-3" :id="'collapse_'+pluginName">
<div v-for="(value, key) in plugin" :key="key" class="form-group row">
<label class="col-sm-2 col-form-label" v-html="key"></label>
<div class="col-sm-10">
<input type="text" v-model="formData.plugins[pluginName][key]" class="form-control">
</div>
</div>
</div>
</div>
<div class="row title" title="预定义的正则匹配规则,在任务列表中可直接点击使用">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-magic"></i> 魔法匹配</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/正则处理教程#21-魔法匹配" target="_blank">?</a>
</span>
</div>
<div class="col-2 text-right">
<button type="button" class="btn btn-outline-primary" @click="addMagicRegex()">+</button>
</div>
</div>
<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" 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 type="button" class="btn btn-outline-danger" @click="removeMagicRegex(key)">-</button>
</div>
</div>
</div>
<div class="row title" title="API接口用以第三方添加任务等操作见Wiki">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-link-45deg"></i> API</h2>
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/API接口" target="_blank">?</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="资源搜索服务配置,用于任务名称智能搜索">
<div class="col-10">
<h2 style="display: inline-block;"><i class="bi bi-search"></i> 资源搜索</h2>
</div>
</div>
<div class="form-group row mb-0" style="display:flex; align-items:center;">
<div data-toggle="collapse" data-target="#collapse_net" aria-expanded="true" aria-controls="collapse_net">
<div class="btn btn-block text-left">
<i class="bi bi-caret-right-fill"></i> 网络公开搜索
</div>
</div>
</div>
<div class="collapse show ml-3" id="collapse_net">
<div class="form-group row">
<label class="col-sm-2 col-form-label">启用</label>
<div class="col-sm-10 d-flex align-items-center">
<input type="checkbox" class="form-check-input" v-model="formData.source.net.enable" placeholder="是否启用网络公开搜索,默认启用">
</div>
</div>
</div>
<div class="form-group row mb-0" style="display:flex; align-items:center;">
<div data-toggle="collapse" data-target="#collapse_cloudsaver" aria-expanded="true" aria-controls="collapse_cloudsaver">
<div class="btn btn-block text-left">
<i class="bi bi-caret-right-fill"></i> CloudSaver
<span class="badge badge-pill badge-light">
<a href="https://github.com/Cp0204/quark-auto-save/wiki/CloudSaver搜索源" target="_blank">?</a>
</span>
</div>
</div>
</div>
<div class="collapse show ml-3" id="collapse_cloudsaver">
<div class="form-group row">
<label class="col-sm-2 col-form-label">服务器</label>
<div class="col-sm-10">
<input type="text" v-model="formData.source.cloudsaver.server" class="form-control" placeholder="资源搜索服务器地址,如 http://172.17.0.1:8008">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">用户名</label>
<div class="col-sm-10">
<input type="text" v-model="formData.source.cloudsaver.username" class="form-control" placeholder="用户名">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">密码</label>
<div class="col-sm-10">
<input type="password" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="密码">
</div>
</div>
</div>
<div class="form-group row mb-0" style="display:flex; align-items:center;">
<div data-toggle="collapse" data-target="#collapse_pansou" aria-expanded="true" aria-controls="collapse_pansou">
<div class="btn btn-block text-left">
<i class="bi bi-caret-right-fill"></i> PanSou
<span class="badge badge-pill badge-light">
<a href="https://github.com/fish2018/pansou" target="_blank">?</a>
</span>
</div>
</div>
</div>
<div class="collapse show ml-3" id="collapse_pansou">
<div class="form-group row">
<label class="col-sm-2 col-form-label">服务器</label>
<div class="col-sm-10">
<input type="text" v-model="formData.source.pansou.server" class="form-control" placeholder="资源搜索服务器地址,如 https://so.252035.xyz">
</div>
</div>
</div>
</div>
<div v-if="activeTab === 'tasklist'">
<div class="row title">
<div class="col">
<h2>任务列表</h2>
</div>
</div>
<hr>
<div class="row">
<div class="col-lg-6 col-md-12">
<div class="row">
<label class="col-form-label col-md-3">名称筛选</label>
<div class="input-group col-md-9">
<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')"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
</div>
<div class="col-lg-6 col-md-12">
<div class="row">
<label class="col-form-label col-md-3">路径筛选</label>
<div class="input-group col-md-9">
<select class="form-control" v-model="taskDirSelected">
<option v-for="(dir, index) in taskDirs" :value="dir" v-html="dir"></option>
</select>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" @click="clearData('taskDirSelected')"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
</div>
</div>
<div v-for="(task, index) in formData.tasklist" :key="index" class="task mb-3">
<template v-if="(taskDirSelected == '' || getParentDirectory(task.savepath) == 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="`${index+1}: ${task.taskname}`"></span>
</div>
</div>
<div class="col-auto">
<button type="button" class="btn btn-outline-primary btn-sm" @click="copyTaskToClipboard(index)" title="复制任务参数到粘贴板"><i class=" bi bi-clipboard-check-fill"></i></button>
<button class="btn btn-warning btn-sm" v-if="task.shareurl_ban" :title="task.shareurl_ban" disabled><i class="bi bi-exclamation-triangle-fill"></i></button>
<button type="button" class="btn btn-outline-primary btn-sm" @click="runScriptNow(index)" title="运行此任务" v-else><i class="bi bi-play-fill"></i></button>
<button type="button" class="btn btn-outline-danger btn-sm" @click="removeTask(index)" title="删除此任务"><i class="bi bi-trash3-fill"></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="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.taskSuggestions.success && smart_param.index === index">
<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 ? `以下资源来自网络搜索,请自行辨识,如有侵权请联系资源方` : "未搜索到资源" }}</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: 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>
<span class="badge bg-transparent border border-success text-success">{{ suggestion.source || "网络公开" }}</span>
<span class="badge bg-transparent border border-info text-info">{{ suggestion.channel }}</span>
<span v-if="suggestion.datetime" class="badge bg-transparent border border-dark text-dark">{{ suggestion.datetime }}</span>
</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-heart"></i>
</button>
<div class="input-group-text" title="谷歌搜索">
<a target="_blank" :href="`https://www.google.com/search?q=%22pan.quark.cn/s%22+${task.taskname}`"><i class="bi bi-google"></i></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.switchShare=false;fileSelect.previewRegex=false;fileSelect.sortBy='file_name';fileSelect.sortOrder='desc';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-box-arrow-up-right"></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-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" title="可用作筛选,只转存匹配到的文件名的文件,留空则转存所有文件">
<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.switchShare=false;fileSelect.previewRegex=true;fileSelect.sortBy='file_name';fileSelect.sortOrder='asc';showShareSelect(index)" title="预览正则处理效果">正则处理</button>
</div>
<input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex" @dblclick="inputRawMagicRegex(task)" title="双击可将魔法匹配释放为填入原始正则表达式">
<input 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" 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.switchShare=false;fileSelect.previewRegex=false;fileSelect.sortBy='updated_at';fileSelect.sortOrder='desc';showShareSelect(index)">选择</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">
<div class="input-group">
<input type="text" name="update_subdir[]" class="form-control" v-model="task.update_subdir" placeholder="可选,匹配需更新子目录(含各级嵌套目录)的正则表达式,多项以|分割,如 4k|1080p">
<div class="input-group-append" title="重存模式:删除该目录下所有文件,重新转存,大资源包时推荐使用&#x0A;不勾选为递归模式:递归检查,逐级更新嵌套目录,效率低">
<div class="input-group-text">
<input type="checkbox" v-model="task.update_subdir_resave_mode">&nbsp;重存模式
</div>
</div>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">截止日期</label>
<div class="col-sm-10">
<input type="date" name="enddate[]" class="form-control" v-model="task.enddate" placeholder="可选">
</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="180px"></v-jsoneditor>
</div>
</div>
</div>
</template>
</div>
<div class="row mt-5">
<div class="col-sm-12 text-center">
<div class="btn-group" role="group" aria-label="任务操作">
<button type="button" class="btn btn-primary" @click="addTask()"><i class="bi bi-plus"></i> 增加任务</button>
<button type="button" class="btn btn-primary" @click="addTaskForClipboard()" title="从粘贴板导入"><i class="bi bi-clipboard-plus"></i></button>
</div>
</div>
</div>
</div>
<div class="bottom-buttons">
<button class="btn btn-success" data-toggle="tooltip" data-placement="top" title="保存 CTRL+S"><i class="bi bi-floppy2-fill"></i></button>
<button type="button" class="btn btn-primary" data-toggle="tooltip" data-placement="top" title="运行 CTRL+R" @click="runScriptNow()"><i class="bi bi-play-fill"></i></button>
<button type="button" class="btn btn-info" data-toggle="tooltip" data-placement="top" title="单击回顶,双击到底" @click="scrollToX(0)" @dblclick="scrollToX()"><i class="bi bi-chevron-bar-up"></i></button>
</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">
<span aria-hidden="true">&times;</span>
</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">
<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">&times;</span>
</button>
</div>
<div class="modal-body small">
<!-- 分享链接来源 -->
<div class="mb-3 row" v-if="fileSelect.switchShare">
<div class="col-sm-8">
<div>
<b>名称:</b>
<span :title="fileSelect.share.content">{{ fileSelect.share.taskname }}</span>
</div>
<div>
<b>链接:</b>
<a :href="fileSelect.share.shareurl" target="_blank" @click.stop>{{ fileSelect.share.shareurl }}</a>
</div>
<div>
<b>来源:</b>
<span class="badge bg-transparent border border-success text-success">{{ fileSelect.share.source || "网络公开" }}</span>
<span class="badge bg-transparent border border-info text-info" v-if="fileSelect.share.channel">{{ fileSelect.share.channel }}</span>
</div>
<div v-if="fileSelect.share.datetime">
<b>时间:</b>
<span>{{ fileSelect.share.datetime }}</span>
</div>
</div>
<div class="col-sm-4 text-right">
<div class="btn-group" title="资源搜索结果切换">
<button type="button" class="btn btn-sm btn-outline-primary" @click="switchShare(-1)">上一个</button>
<button type="button" class="btn btn-sm btn-outline-primary" @click="switchShare(1)">下一个</button>
</div>
</div>
</div>
<div class="alert alert-warning" v-if="fileSelect.error" v-html="fileSelect.error"></div>
<div v-else>
<!-- 正则处理表达式 -->
<div class="mb-3" v-if="fileSelect.previewRegex && fileSelect.index<this.formData.tasklist.length">
<div><b>匹配表达式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
<span class="badge badge-info" v-if="formData.tasklist[fileSelect.index].pattern in formData.magic_regex">{{ formData.magic_regex[formData.tasklist[fileSelect.index].pattern].pattern }}</span>
</div>
<div><b>替换表达式:</b><span class="badge badge-info" v-if="formData.tasklist[fileSelect.index].replace" v-html="formData.tasklist[fileSelect.index].replace"></span>
<span class="badge badge-info" v-else-if="formData.tasklist[fileSelect.index].pattern in formData.magic_regex">{{ formData.magic_regex[formData.tasklist[fileSelect.index].pattern].replace }}</span>
</div>
</div>
<!-- 面包屑导航 -->
<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.selectShare">
正则处理
</th>
<template v-if="!fileSelect.previewRegex">
<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>
<td v-if="fileSelect.selectShare" :class="file.file_name_re ? 'text-success' : file.file_name_saved ? 'text-muted' : 'text-danger'">{{file.file_name_re || file.file_name_saved || '&times;'}}</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 class="cursor-pointer text-muted" @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" @click="selectCurrentFolder(true)">当前文件夹<span class="badge badge-light" v-if="fileSelect.index<this.formData.tasklist.length" v-html="'/'+formData.tasklist[fileSelect.index].taskname"></span></button>
</div>
</div>
</div>
</div>
<!-- Toast 提示 -->
<div class="toast-container">
<div v-for="toast in toasts" :key="toast.id" class="toast show shadow-sm" :class="toast.type" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body d-flex align-items-center">
<i class="bi mr-2" :class="getToastIcon(toast.type)"></i>
<span>{{ toast.message }}</span>
<button type="button" class="ml-auto close" @click="removeToast(toast.id)" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
version: "[[ version ]]",
versionTips: "",
plugin_flags: "[[ plugin_flags ]]",
weekdays: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
formData: {
cookie: [],
push_config: {},
media_servers: {},
tasklist: [],
magic_regex: {},
source: {
net: {
enable: ""
},
cloudsaver: {
server: "",
username: "",
password: "",
token: ""
},
pansou: {
server: ""
}
},
},
toasts: [],
newTask: {
taskname: "",
shareurl: "",
savepath: "/",
pattern: "",
replace: "",
enddate: "",
addition: {},
ignore_extension: false,
runweek: [1, 2, 3, 4, 5, 6, 7]
},
run_log: "",
taskDirs: [""],
taskDirSelected: "",
taskNameFilter: "",
modalLoading: false,
smart_param: {
index: null,
savepath: "",
origin_savepath: "",
taskSuggestions: {},
showSuggestions: false,
isSearching: false,
searchTimer: null,
},
activeTab: 'tasklist',
configModified: false,
fileSelect: {
index: null,
share: {},
shareurl: "",
stoken: "",
fileList: [],
paths: [],
selectDir: true,
selectShare: true,
switchShare: false,
previewRegex: false,
sortBy: "updated_at",
sortOrder: "desc"
},
},
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
}
},
mounted() {
this.fetchData();
this.checkNewVersion();
$('[data-toggle="tooltip"]').tooltip();
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('click', (e) => {
if (!e.target.closest('.input-group')) {
this.smart_param.showSuggestions = false;
}
});
window.addEventListener('beforeunload', this.handleBeforeUnload);
},
beforeDestroy() {
window.removeEventListener('beforeunload', this.handleBeforeUnload);
},
methods: {
changeTab(tab) {
this.activeTab = tab;
if (window.innerWidth <= 768) {
$('#sidebarMenu').collapse('toggle')
}
},
checkNewVersion() {
this.versionTips = this.version;
axios.get('https://api.github.com/repos/Cp0204/quark-auto-save/tags')
.then(response => {
latestVersion = response.data[0].name;
console.log(`检查版本:当前 ${this.version} 最新 ${latestVersion}`);
if (latestVersion != this.version) {
this.versionTips += ` <sup><span class="position-absolute 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];
}
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.source.pansou) {
config_data.source.pansou = {
server: ""
};
}
if (!config_data.source.net) {
config_data.source.net = {
enable: ""
};
}
this.formData = config_data;
setTimeout(() => {
this.configModified = false;
}, 100);
})
.catch(error => {
console.error('Error fetching data:', 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() {
axios.post('/update', this.formData)
.then(response => {
if (response.data.success) {
this.configModified = false;
this.showToast(response.data.message, 'success');
} else {
this.showToast(response.data.message, 'error');
}
console.log('Config saved result:', response.data);
})
.catch(error => {
console.error('Error saving config:', error);
});
},
addCookie() {
this.formData.cookie.push("");
},
removeCookie(index) {
if (this.formData.cookie[index] == "" || confirm("确认删除吗?"))
this.formData.cookie.splice(index, 1);
},
testPush() {
this.runScriptNow(1, true);
},
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() {
newTask = { ...this.newTask }
newTask.taskname = this.taskNameFilter;
if (this.formData.tasklist.length > 0) {
lastTask = this.formData.tasklist[this.formData.tasklist.length - 1];
if (this.taskDirSelected) {
newTask.savepath = this.taskDirSelected + '/TASKNAME';
} else {
if (newTask.taskname) {
newTask.savepath = lastTask.savepath.replace(lastTask.taskname, newTask.taskname);
} else {
newTask.savepath = lastTask.taskname ? lastTask.savepath.replace(lastTask.taskname, 'TASKNAME') : lastTask.savepath;
}
}
}
this.formData.tasklist.push(newTask);
// 滚到最下
setTimeout(() => {
$('#collapse_' + (this.formData.tasklist.length - 1)).collapse('show').on('shown.bs.collapse', () => {
this.scrollToX();
});
}, 1);
},
focusTaskname(index, task) {
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);
if (this.smart_param.savepath)
task.savepath = this.smart_param.savepath.replace('TASKNAME', task.taskname);
},
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) {
console.error("Error decodeURIComponent:", e);
}
// 从分享中提取任务名
axios.post('/get_share_detail', {
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", share_detail.error);
} 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 => {
console.error('Error get_share_detail:', error);
});
},
clearData(target) {
this[target] = "";
},
async runScriptNow(task_index = null, test = false) {
body = {};
if (test) {
body = {
"quark_test": true,
"cookie": this.formData.cookie,
"push_config": this.formData.push_config
};
} else if (task_index != null) {
task = { ...this.formData.tasklist[task_index] };
delete task.runweek;
delete task.enddate;
body = {
"tasklist": [task]
};
} 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) {
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;
if (task_index == null) {
this.fetchData();
}
break;
}
this.run_log += eventData.replace('<', '<\u200B') + '\n';
// 在更新 run_log 后将滚动条滚动到底部
this.$nextTick(() => {
const modalBody = document.querySelector('.modal-body');
modalBody.scrollTop = modalBody.scrollHeight;
});
} else {
console.warn('Unexpected line:', line);
}
}
partialData = '';
}
} catch (error) {
this.modalLoading = false;
console.error('Error:', error);
}
},
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) {
taskname = taskname.replace(/\((19|20)\d{2}\)/g, '').trim();
if (taskname.length < 2) {
console.log(`任务名[${taskname}]过短${taskname.length} 不进行搜索`);
return;
}
this.smart_param.isSearching = true;
this.smart_param.index = index;
try {
axios.get('/task_suggestions', {
params: {
q: taskname,
d: deep
}
}).then(response => {
this.smart_param.taskSuggestions = response.data;
this.smart_param.showSuggestions = true;
}).catch(error => {
console.error('Error fetching suggestions:', error);
}).finally(() => {
this.smart_param.isSearching = false;
});
} catch (e) {
this.smart_param.taskSuggestions = {
error: "网络异常"
};
}
},
selectSuggestion(index, suggestion) {
this.smart_param.showSuggestions = false;
this.fileSelect.selectDir = true;
this.fileSelect.switchShare = true;
this.fileSelect.previewRegex = false;
this.fileSelect.share = suggestion;
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]) {
this.showToast(`魔法名 [${newKey}] 已存在,请使用其他名称`, 'warning');
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);
}
},
deleteFile(fid, fname, isDir) {
if (fid != "" && confirm(`确认删除${isDir ? '目录' : '文件'} [${fname}] 吗?`))
axios.post('/delete_file', {
fid: fid
}).then(response => {
if (response.data.code == 0) {
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid != fid);
} else {
this.showToast('删除失败:' + response.data.message, 'error');
}
}).catch(error => {
console.error('Error /delete_file:', error);
});
},
getSavepathDetail(params = 0) {
if (params.includes('/')) {
params = { path: params }
} else {
params = { fid: params }
}
this.modalLoading = true;
axios.get('/get_savepath_detail', {
params: params
}).then(response => {
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('Error /get_savepath_detail:', error);
this.fileSelect.error = "获取文件夹列表失败";
this.modalLoading = false;
});
},
showSavepathSelect(index) {
this.fileSelect.selectShare = false;
this.fileSelect.selectDir = true;
this.fileSelect.switchShare = false;
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);
},
getShareDetail() {
this.modalLoading = true;
axios.post('/get_share_detail', {
shareurl: this.fileSelect.shareurl,
stoken: this.fileSelect.stoken,
task: this.formData.tasklist[this.fileSelect.index],
magic_regex: this.formData.magic_regex,
}).then(response => {
if (response.data.success) {
this.fileSelect.fileList = response.data.data.list;
this.sortFileList(this.fileSelect.sortBy, this.fileSelect.sortOrder);
this.fileSelect.paths = response.data.data.paths;
this.fileSelect.stoken = response.data.data.stoken;
} else {
this.fileSelect.error = response.data.data.error
}
this.modalLoading = false;
}).catch(error => {
console.error('Error getting folders:', error);
this.fileSelect.error = "获取文件夹列表失败";
this.modalLoading = false;
});
},
showShareSelect(index, shareurl = null) {
this.fileSelect.selectShare = true;
this.fileSelect.fileList = [];
this.fileSelect.paths = [];
this.fileSelect.error = undefined;
// 如果分享链接发生变化,则重置 stoken
const newShareurl = shareurl || this.formData.tasklist[index].shareurl
if (this.getShareurl(this.fileSelect.shareurl) != this.getShareurl(newShareurl)) {
this.fileSelect.stoken = "";
}
this.fileSelect.shareurl = newShareurl;
this.fileSelect.index = index;
$('#fileSelectModal').modal('toggle');
this.getShareDetail();
},
switchShare(index) {
currentIndex = this.smart_param.taskSuggestions.data.indexOf(this.fileSelect.share);
nextIndex = currentIndex + index;
if (nextIndex < 0) {
this.showToast("没有上一个啦", "info");
} else if (nextIndex >= this.smart_param.taskSuggestions.data.length) {
this.showToast("没有下一个啦", "info");
} else {
this.fileSelect.error = "";
this.fileSelect.stoken = "";
this.fileSelect.share = this.smart_param.taskSuggestions.data[nextIndex];
this.fileSelect.shareurl = this.smart_param.taskSuggestions.data[nextIndex].shareurl;
this.fileSelect.paths = [];
this.fileSelect.fileList = [];
this.getShareDetail();
}
},
navigateTo(fid, name) {
dir = { fid: fid, name: name }
if (this.fileSelect.selectShare) {
this.fileSelect.shareurl = this.getShareurl(this.fileSelect.shareurl, dir);
this.getShareDetail();
} 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 })
}
}
this.getSavepathDetail(fid);
}
},
selectCurrentFolder(addTaskname = false) {
if (this.fileSelect.selectShare) {
this.formData.tasklist[this.fileSelect.index].shareurl_ban = undefined;
this.formData.tasklist[this.fileSelect.index].shareurl = this.fileSelect.shareurl;
} else {
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
}
}
$('#fileSelectModal').modal('hide')
},
selectStartFid(fid) {
Vue.set(this.formData.tasklist[this.fileSelect.index], 'startfid', fid);
$('#fileSelectModal').modal('hide')
},
getShareurl(shareurl, dir = {}) {
if (dir == {} || dir.fid == 0) {
shareurl = shareurl.match(`.*s/[a-z0-9]+(\\?pwd=[^#]+)?`)[0]
} else if (shareurl.includes(dir.fid)) {
shareurl = shareurl.match(`.*/${dir.fid}[^/]*`)[0]
} else if (shareurl.includes('#/list/share')) {
shareurl = `${shareurl.split('#')[0]}#/list/share/${dir.fid}`
} else {
shareurl = `${shareurl.split('#')[0]}#/list/share/${dir.fid}`
}
return shareurl;
},
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;
});
},
inputRawMagicRegex(task) {
const item = this.formData.magic_regex[task.pattern];
if (item) {
task.pattern = item.pattern;
task.replace = item.replace;
}
},
copyText(text, callback = () => { }) {
if (!text) {
console.error('No text to copy');
return;
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text);
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '0';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
textarea.setSelectionRange(0, 99999);
document.execCommand("copy");
document.body.removeChild(textarea);
}
callback()
},
copyTaskToClipboard(index) {
const task = { ...this.formData.tasklist[index] };
delete task.addition;
const _this = this;
this.copyText(JSON.stringify(task), function () {
_this.showToast("任务参数已复制到剪贴板", "success");
});
},
async addTaskForClipboard() {
text = null
try {
text = await navigator.clipboard.readText();
} catch (error) {
text = prompt("当前环境不支持自动读取粘贴板,请手动粘贴任务参数", "");
}
if (text) {
try {
let task = JSON.parse(text);
task = { ...this.newTask, ...task };
this.formData.tasklist.push(task);
this.showToast("剪贴板参数已成功导入任务", "success");
// 滚到最下
setTimeout(() => {
$('#collapse_' + (this.formData.tasklist.length - 1)).collapse('show').on('shown.bs.collapse', () => {
this.scrollToX();
});
}, 1);
} catch (error) {
this.showToast("解析剪贴板内容失败", "error");
}
}
},
showToast(message, type = 'info', duration = 3000) {
const id = Date.now();
this.toasts.push({ id, message, type });
setTimeout(() => {
this.removeToast(id);
}, duration);
},
removeToast(id) {
this.toasts = this.toasts.filter(t => t.id !== id);
},
getToastIcon(type) {
switch (type) {
case 'success': return 'bi-check-circle-fill text-success';
case 'error': return 'bi-exclamation-circle-fill text-danger';
case 'warning': return 'bi-exclamation-triangle-fill text-warning';
default: return 'bi-info-circle-fill text-info';
}
},
}
});
</script>
</body>
</html>