mirror of
https://github.com/Cp0204/quark-auto-save.git
synced 2026-01-14 00:10:43 +08:00
commit
64c7d47242
@ -9,6 +9,8 @@
|
||||
- **数据库**:引入 SQLite 数据库,记录和管理所有转存历史,便于查询和追踪。
|
||||
- **转存记录**:支持通过 WebUI 的转存记录页面查看、查询历史转存记录的相关信息,支持删除转存记录。
|
||||
- **WebUI**:对整个 WebUI 进行了重塑,增加了更多实用功能,如文件选择和预览界面的排序功能、资源搜索的过滤功能、TMDB 和豆瓣搜索功能、页面视图切换功能、账号设置功能等等。
|
||||
- **查重逻辑**:支持优先通过历史转存记录查重,对于有转存记录的文件,即使删除网盘文件,也不会重复转存。
|
||||
- **Aria2**:支持成功添加 Aria2 下载任务后自动删除夸克网盘内对应的文件,清理网盘空间。
|
||||
|
||||
本项目修改后的版本为个人需求定制版,目的是满足我自己的使用需求,某些(我不用的)功能可能会因为修改而出现 BUG,不一定会被修复。若你要使用本项目,请知晓本人不是程序员,我无法保证本项目的稳定性,如果你在使用过程中发现了 BUG,可以在 Issues 中提交,但不保证每个 BUG 都能被修复,请谨慎使用,风险自担。
|
||||
|
||||
@ -32,7 +34,7 @@
|
||||
|
||||
- 文件管理
|
||||
- [x] 目标目录不存在时自动新建
|
||||
- [x] 跳过已转存过的文件
|
||||
- [x] 跳过已转存过的文件(**即使删除网盘文件,也不会重复转存**)
|
||||
- [x] **过滤不需要转存的文件或文件夹**
|
||||
- [x] 转存后文件名整理(正则命名、**顺序命名**、**剧集命名**)
|
||||
- [x] 可选忽略文件后缀
|
||||
@ -53,6 +55,7 @@
|
||||
- [x] 每日签到领空间 <sup>[?](https://github.com/x1ao4/quark-auto-save-x/wiki/使用技巧集锦#每日签到领空间)</sup>
|
||||
- [x] 支持多个通知推送渠道 <sup>[?](https://github.com/x1ao4/quark-auto-save-x/wiki/通知推送服务配置)</sup>
|
||||
- [x] 支持多账号(多账号签到,仅首账号转存)
|
||||
- [x] 支持网盘文件下载、strm 文件生成等功能 <sup>[?](https://github.com/x1ao4/quark-auto-save-x/wiki/插件配置)</sup>
|
||||
|
||||
## 部署
|
||||
### Docker 部署
|
||||
|
||||
203
app/run.py
203
app/run.py
@ -187,6 +187,62 @@ def get_data():
|
||||
return jsonify({"success": True, "data": data})
|
||||
|
||||
|
||||
def sync_task_plugins_config():
|
||||
"""同步更新所有任务的插件配置
|
||||
|
||||
1. 检查每个任务的插件配置
|
||||
2. 如果插件配置不存在,使用默认配置
|
||||
3. 如果插件配置存在但缺少新的配置项,添加默认值
|
||||
4. 保留原有的自定义配置
|
||||
5. 只处理已启用的插件(通过PLUGIN_FLAGS检查)
|
||||
6. 清理被禁用插件的配置
|
||||
"""
|
||||
global config_data, task_plugins_config_default
|
||||
|
||||
# 如果没有任务列表,直接返回
|
||||
if not config_data.get("tasklist"):
|
||||
return
|
||||
|
||||
# 获取禁用的插件列表
|
||||
disabled_plugins = set()
|
||||
if PLUGIN_FLAGS:
|
||||
disabled_plugins = {name.lstrip('-') for name in PLUGIN_FLAGS.split(',')}
|
||||
|
||||
# 遍历所有任务
|
||||
for task in config_data["tasklist"]:
|
||||
# 确保任务有addition字段
|
||||
if "addition" not in task:
|
||||
task["addition"] = {}
|
||||
|
||||
# 清理被禁用插件的配置
|
||||
for plugin_name in list(task["addition"].keys()):
|
||||
if plugin_name in disabled_plugins:
|
||||
del task["addition"][plugin_name]
|
||||
|
||||
# 遍历所有插件的默认配置
|
||||
for plugin_name, default_config in task_plugins_config_default.items():
|
||||
# 跳过被禁用的插件
|
||||
if plugin_name in disabled_plugins:
|
||||
continue
|
||||
|
||||
# 如果任务中没有该插件的配置,添加默认配置
|
||||
if plugin_name not in task["addition"]:
|
||||
task["addition"][plugin_name] = default_config.copy()
|
||||
else:
|
||||
# 如果任务中有该插件的配置,检查是否有新的配置项
|
||||
current_config = task["addition"][plugin_name]
|
||||
# 确保current_config是字典类型
|
||||
if not isinstance(current_config, dict):
|
||||
# 如果不是字典类型,使用默认配置
|
||||
task["addition"][plugin_name] = default_config.copy()
|
||||
continue
|
||||
|
||||
# 遍历默认配置的每个键值对
|
||||
for key, default_value in default_config.items():
|
||||
if key not in current_config:
|
||||
current_config[key] = default_value
|
||||
|
||||
|
||||
# 更新数据
|
||||
@app.route("/update", methods=["POST"])
|
||||
def update():
|
||||
@ -202,6 +258,10 @@ def update():
|
||||
config_data["webui"]["password"] = value.get("password", config_data["webui"]["password"])
|
||||
else:
|
||||
config_data.update({key: value})
|
||||
|
||||
# 同步更新任务的插件配置
|
||||
sync_task_plugins_config()
|
||||
|
||||
Config.write_json(CONFIG_PATH, config_data)
|
||||
# 更新session token,确保当前会话在用户名密码更改后仍然有效
|
||||
session["token"] = get_login_token()
|
||||
@ -650,6 +710,55 @@ def delete_file():
|
||||
account = Quark(config_data["cookie"][0], 0)
|
||||
if fid := request.json.get("fid"):
|
||||
response = account.delete([fid])
|
||||
|
||||
# 处理delete_records参数
|
||||
if request.json.get("delete_records") and response.get("code") == 0:
|
||||
try:
|
||||
# 初始化数据库
|
||||
db = RecordDB()
|
||||
|
||||
# 获取save_path参数
|
||||
save_path = request.json.get("save_path", "")
|
||||
|
||||
# 如果没有提供save_path,则不删除任何记录
|
||||
if not save_path:
|
||||
response["deleted_records"] = 0
|
||||
# logging.info(f">>> 删除文件 {fid} 但未提供save_path,不删除任何记录")
|
||||
return jsonify(response)
|
||||
|
||||
# 查询与该文件ID和save_path相关的所有记录
|
||||
cursor = db.conn.cursor()
|
||||
|
||||
# 使用file_id和save_path进行精确匹配
|
||||
cursor.execute("SELECT id FROM transfer_records WHERE file_id = ? AND save_path = ?", (fid, save_path))
|
||||
record_ids = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# 如果没有找到匹配的file_id记录,尝试通过文件名查找
|
||||
if not record_ids:
|
||||
# 获取文件名(如果有的话)
|
||||
file_name = request.json.get("file_name", "")
|
||||
if file_name:
|
||||
# 使用文件名和save_path进行精确匹配
|
||||
cursor.execute("""
|
||||
SELECT id FROM transfer_records
|
||||
WHERE (original_name = ? OR renamed_to = ?)
|
||||
AND save_path = ?
|
||||
""", (file_name, file_name, save_path))
|
||||
|
||||
record_ids = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# 删除找到的所有记录
|
||||
deleted_count = 0
|
||||
for record_id in record_ids:
|
||||
deleted_count += db.delete_record(record_id)
|
||||
|
||||
# 添加删除记录的信息到响应中
|
||||
response["deleted_records"] = deleted_count
|
||||
# logging.info(f">>> 删除文件 {fid} 同时删除了 {deleted_count} 条相关记录")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f">>> 删除记录时出错: {str(e)}")
|
||||
# 不影响主流程,即使删除记录失败也返回文件删除成功
|
||||
else:
|
||||
response = {"success": False, "message": "缺失必要字段: fid"}
|
||||
return jsonify(response)
|
||||
@ -766,6 +875,22 @@ def init():
|
||||
_, plugins_config_default, task_plugins_config_default = Config.load_plugins()
|
||||
plugins_config_default.update(config_data.get("plugins", {}))
|
||||
config_data["plugins"] = plugins_config_default
|
||||
|
||||
# 获取禁用的插件列表
|
||||
disabled_plugins = set()
|
||||
if PLUGIN_FLAGS:
|
||||
disabled_plugins = {name.lstrip('-') for name in PLUGIN_FLAGS.split(',')}
|
||||
|
||||
# 清理所有任务中被禁用插件的配置
|
||||
if config_data.get("tasklist"):
|
||||
for task in config_data["tasklist"]:
|
||||
if "addition" in task:
|
||||
for plugin_name in list(task["addition"].keys()):
|
||||
if plugin_name in disabled_plugins:
|
||||
del task["addition"][plugin_name]
|
||||
|
||||
# 同步更新任务的插件配置
|
||||
sync_task_plugins_config()
|
||||
|
||||
# 更新配置
|
||||
Config.write_json(CONFIG_PATH, config_data)
|
||||
@ -961,6 +1086,84 @@ def get_user_info():
|
||||
return jsonify({"success": True, "data": user_info_list})
|
||||
|
||||
|
||||
# 重置文件夹(删除文件夹内所有文件和相关记录)
|
||||
@app.route("/reset_folder", methods=["POST"])
|
||||
def reset_folder():
|
||||
if not is_login():
|
||||
return jsonify({"success": False, "message": "未登录"})
|
||||
|
||||
# 获取请求参数
|
||||
save_path = request.json.get("save_path", "")
|
||||
task_name = request.json.get("task_name", "")
|
||||
|
||||
if not save_path:
|
||||
return jsonify({"success": False, "message": "保存路径不能为空"})
|
||||
|
||||
try:
|
||||
# 初始化夸克网盘客户端
|
||||
account = Quark(config_data["cookie"][0], 0)
|
||||
|
||||
# 1. 获取文件夹ID
|
||||
# 先检查是否已有缓存的文件夹ID
|
||||
folder_fid = account.savepath_fid.get(save_path)
|
||||
|
||||
# 如果没有缓存的ID,则尝试创建文件夹以获取ID
|
||||
if not folder_fid:
|
||||
mkdir_result = account.mkdir(save_path)
|
||||
if mkdir_result.get("code") == 0:
|
||||
folder_fid = mkdir_result["data"]["fid"]
|
||||
account.savepath_fid[save_path] = folder_fid
|
||||
else:
|
||||
return jsonify({"success": False, "message": f"获取文件夹ID失败: {mkdir_result.get('message', '未知错误')}"})
|
||||
|
||||
# 2. 获取文件夹内的所有文件
|
||||
file_list = account.ls_dir(folder_fid)
|
||||
if isinstance(file_list, dict) and file_list.get("error"):
|
||||
return jsonify({"success": False, "message": f"获取文件列表失败: {file_list.get('error', '未知错误')}"})
|
||||
|
||||
# 收集所有文件ID
|
||||
file_ids = []
|
||||
for item in file_list:
|
||||
file_ids.append(item["fid"])
|
||||
|
||||
# 3. 删除所有文件
|
||||
deleted_files = 0
|
||||
if file_ids:
|
||||
delete_result = account.delete(file_ids)
|
||||
if delete_result.get("code") == 0:
|
||||
deleted_files = len(file_ids)
|
||||
|
||||
# 4. 删除相关的历史记录
|
||||
deleted_records = 0
|
||||
try:
|
||||
# 初始化数据库
|
||||
db = RecordDB()
|
||||
|
||||
# 查询与该保存路径相关的所有记录
|
||||
cursor = db.conn.cursor()
|
||||
cursor.execute("SELECT id FROM transfer_records WHERE save_path = ?", (save_path,))
|
||||
record_ids = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# 删除找到的所有记录
|
||||
for record_id in record_ids:
|
||||
deleted_records += db.delete_record(record_id)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f">>> 删除记录时出错: {str(e)}")
|
||||
# 即使删除记录失败,也返回文件删除成功
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"重置成功,删除了 {deleted_files} 个文件和 {deleted_records} 条记录",
|
||||
"deleted_files": deleted_files,
|
||||
"deleted_records": deleted_records
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f">>> 重置文件夹时出错: {str(e)}")
|
||||
return jsonify({"success": False, "message": f"重置文件夹时出错: {str(e)}"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init()
|
||||
reload_tasks()
|
||||
|
||||
@ -31,9 +31,17 @@ class RecordDB:
|
||||
resolution TEXT,
|
||||
modify_date INTEGER NOT NULL,
|
||||
file_id TEXT,
|
||||
file_type TEXT
|
||||
file_type TEXT,
|
||||
save_path TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
# 检查save_path字段是否存在,如果不存在则添加
|
||||
cursor.execute("PRAGMA table_info(transfer_records)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
if 'save_path' not in columns:
|
||||
cursor.execute('ALTER TABLE transfer_records ADD COLUMN save_path TEXT')
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
def close(self):
|
||||
@ -41,19 +49,19 @@ class RecordDB:
|
||||
self.conn.close()
|
||||
|
||||
def add_record(self, task_name, original_name, renamed_to, file_size, modify_date,
|
||||
duration="", resolution="", file_id="", file_type=""):
|
||||
duration="", resolution="", file_id="", file_type="", save_path=""):
|
||||
"""添加一条转存记录"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute(
|
||||
"INSERT INTO transfer_records (transfer_time, task_name, original_name, renamed_to, file_size, "
|
||||
"duration, resolution, modify_date, file_id, file_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"duration, resolution, modify_date, file_id, file_type, save_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(int(time.time()), task_name, original_name, renamed_to, file_size,
|
||||
duration, resolution, modify_date, file_id, file_type)
|
||||
duration, resolution, modify_date, file_id, file_type, save_path)
|
||||
)
|
||||
self.conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
def update_renamed_to(self, file_id, original_name, renamed_to, task_name=""):
|
||||
def update_renamed_to(self, file_id, original_name, renamed_to, task_name="", save_path=""):
|
||||
"""更新最近一条记录的renamed_to字段
|
||||
|
||||
Args:
|
||||
@ -61,6 +69,7 @@ class RecordDB:
|
||||
original_name: 原文件名
|
||||
renamed_to: 重命名后的文件名
|
||||
task_name: 任务名称,可选项,如提供则作为附加筛选条件
|
||||
save_path: 保存路径,可选项,如提供则同时更新保存路径
|
||||
|
||||
Returns:
|
||||
更新的记录数量
|
||||
@ -97,11 +106,17 @@ class RecordDB:
|
||||
|
||||
if result:
|
||||
record_id = result[0]
|
||||
# 更新记录
|
||||
cursor.execute(
|
||||
"UPDATE transfer_records SET renamed_to = ? WHERE id = ?",
|
||||
(renamed_to, record_id)
|
||||
)
|
||||
# 根据是否提供save_path决定更新哪些字段
|
||||
if save_path:
|
||||
cursor.execute(
|
||||
"UPDATE transfer_records SET renamed_to = ?, save_path = ? WHERE id = ?",
|
||||
(renamed_to, save_path, record_id)
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
"UPDATE transfer_records SET renamed_to = ? WHERE id = ?",
|
||||
(renamed_to, record_id)
|
||||
)
|
||||
self.conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
@ -124,7 +139,7 @@ class RecordDB:
|
||||
|
||||
# 构建SQL查询
|
||||
valid_columns = ["transfer_time", "task_name", "original_name", "renamed_to",
|
||||
"file_size", "duration", "resolution", "modify_date"]
|
||||
"file_size", "duration", "resolution", "modify_date", "save_path"]
|
||||
|
||||
if sort_by not in valid_columns:
|
||||
sort_by = "transfer_time"
|
||||
@ -140,7 +155,8 @@ class RecordDB:
|
||||
params.append(task_name_filter)
|
||||
|
||||
if keyword_filter:
|
||||
where_clauses.append("task_name LIKE ?")
|
||||
where_clauses.append("(task_name LIKE ? OR original_name LIKE ?)")
|
||||
params.append(f"%{keyword_filter}%")
|
||||
params.append(f"%{keyword_filter}%")
|
||||
|
||||
where_clause = " AND ".join(where_clauses)
|
||||
@ -192,4 +208,33 @@ class RecordDB:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("DELETE FROM transfer_records WHERE id = ?", (record_id,))
|
||||
self.conn.commit()
|
||||
return cursor.rowcount
|
||||
return cursor.rowcount
|
||||
|
||||
def get_records_by_save_path(self, save_path, include_subpaths=False):
|
||||
"""根据保存路径查询记录
|
||||
|
||||
Args:
|
||||
save_path: 要查询的保存路径
|
||||
include_subpaths: 是否包含子路径下的文件
|
||||
|
||||
Returns:
|
||||
匹配的记录列表
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
if include_subpaths:
|
||||
# 如果包含子路径,使用LIKE查询
|
||||
query = "SELECT * FROM transfer_records WHERE save_path LIKE ? ORDER BY transfer_time DESC"
|
||||
cursor.execute(query, [f"{save_path}%"])
|
||||
else:
|
||||
# 精确匹配路径
|
||||
query = "SELECT * FROM transfer_records WHERE save_path = ? ORDER BY transfer_time DESC"
|
||||
cursor.execute(query, [save_path])
|
||||
|
||||
records = cursor.fetchall()
|
||||
|
||||
# 将结果转换为字典列表
|
||||
if records:
|
||||
columns = [col[0] for col in cursor.description]
|
||||
return [dict(zip(columns, row)) for row in records]
|
||||
return []
|
||||
@ -805,6 +805,11 @@ main div[v-if="activeTab === 'config'"] .row.title:first-child {
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
/* 重置文件夹图标样式 */
|
||||
.bi-folder-x {
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
/* 恢复图标样式 */
|
||||
.bi-reply {
|
||||
color: var(--dark-text-color);
|
||||
@ -1120,7 +1125,7 @@ textarea.form-control {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
/* --------------- 扩展按钮样式 --------------- */
|
||||
/* --------------- 转存记录展开按钮样式 --------------- */
|
||||
.expand-button {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
@ -1180,8 +1185,8 @@ textarea.form-control {
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid var(--border-color); /* 修改底部边框为1px */
|
||||
border-top: 1px solid var(--border-color); /* 添加上边框线 */
|
||||
padding-top: 8px; /* 增加上内边距 */
|
||||
padding-bottom: 8px; /* 增加下内边距 */
|
||||
padding-top: 8.5px; /* 增加上内边距 */
|
||||
padding-bottom: 8.5px; /* 增加下内边距 */
|
||||
padding-left: 9px !important; /* 表头左内边距,与按钮一致 */
|
||||
padding-right: 9px !important; /* 表头右内边距,与按钮一致 */
|
||||
background-color: var(--button-gray-background-color); /* 表头背景色 */
|
||||
@ -1206,6 +1211,7 @@ textarea.form-control {
|
||||
padding-left: 9px !important; /* 单元格左内边距,与按钮一致 */
|
||||
padding-right: 9px !important; /* 单元格右内边距,与按钮一致 */
|
||||
border-bottom: 1px solid var(--border-color); /* 单元格分割线颜色 */
|
||||
transform: translateY(0.5px); /* 文本下移0.5px,不影响元素实际高度 */
|
||||
}
|
||||
|
||||
/* 表格行悬停样式 */
|
||||
@ -1273,8 +1279,8 @@ button.close:focus,
|
||||
align-items: center;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
height: 35.5px;
|
||||
min-height: 35.5px;
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
/* 任务列表中的警告框样式 */
|
||||
@ -1499,7 +1505,7 @@ button.close:focus,
|
||||
background-color: var(--button-gray-background-color);
|
||||
border-radius: 0px;
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 12px;
|
||||
padding: 6.5px 12px 6px 12px; /* 上右下左内边距 */
|
||||
margin-bottom: 8px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
@ -1591,7 +1597,7 @@ button.close:focus,
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--dark-text-color);
|
||||
font-weight: 600;
|
||||
padding: 6.5px 12.5px !important; /* 表头上下边距,左右边距 */
|
||||
padding: 7px 12.5px 6.5px 12.5px !important; /* 表头上右下左内边距 */
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
position: sticky;
|
||||
@ -1632,10 +1638,11 @@ button.close:focus,
|
||||
|
||||
/* 操作列 - 固定宽度 */
|
||||
#fileSelectModal .table .col-action {
|
||||
width: 53px;
|
||||
min-width: 53px;
|
||||
max-width: 53px;
|
||||
text-align: center;
|
||||
width: 188px;
|
||||
min-width: 188px;
|
||||
max-width: 188px;
|
||||
text-align: left;
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
|
||||
/* 确保单元格内容溢出时正确显示 */
|
||||
@ -1655,7 +1662,7 @@ button.close:focus,
|
||||
}
|
||||
|
||||
#fileSelectModal .table td {
|
||||
padding: 6px 12px !important; /* 单元格上下边距,左右边距 */
|
||||
padding: 5.5px 12.5px 7px 12px !important; /* 单元格上右下左内边距 */
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--dark-text-color);
|
||||
@ -1685,12 +1692,16 @@ button.close:focus,
|
||||
color: #ffc107;
|
||||
font-size: 0.9rem;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
top: 1px; /* 负值向上移动,正值向下移动 */
|
||||
}
|
||||
|
||||
#fileSelectModal .bi-file-earmark {
|
||||
color: var(--dark-text-color);
|
||||
font-size: 0.9rem;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
top: 0px !important; /* 负值向上移动,正值向下移动 */
|
||||
}
|
||||
|
||||
/* 弹窗删除链接样式 */
|
||||
@ -1717,8 +1728,14 @@ button.close:focus,
|
||||
padding-right: 12px; /* 右边距 */
|
||||
}
|
||||
|
||||
/* 添加文件选择模态框左下角文件信息文本的左边距样式 */
|
||||
#fileSelectModal .modal-footer .file-selection-info {
|
||||
margin-left: 0px; /* 与表格左边距保持一致 */
|
||||
font-size: 0.85rem !important; /* 覆盖内联样式 */
|
||||
}
|
||||
|
||||
#fileSelectModal .modal-footer span {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--dark-text-color);
|
||||
margin-right: auto;
|
||||
}
|
||||
@ -1781,7 +1798,7 @@ button.close:focus,
|
||||
background-color: transparent !important;
|
||||
color: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: bold !important;
|
||||
font-weight: normal !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
display: inline-flex !important;
|
||||
@ -3294,7 +3311,7 @@ div[id^="collapse_"][id*="plugin"] .input-group {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
font-weight: normal;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
@ -3565,11 +3582,11 @@ input::-moz-list-button {
|
||||
|
||||
/* 针对选择保存到的文件夹模式 - 带操作列的表格 */
|
||||
#fileSelectModal[data-modal-type="target"] .breadcrumb {
|
||||
min-width: 513px; /* 4列表格总宽度: 230px + 90px + 140px + 53px */
|
||||
min-width: 648px; /* 4列表格总宽度: 230px + 90px + 140px + 188px */
|
||||
}
|
||||
|
||||
#fileSelectModal[data-modal-type="target"] .table {
|
||||
width: 513px;
|
||||
width: 648px;
|
||||
}
|
||||
|
||||
/* 针对命名预览模式 - 2列表格 */
|
||||
@ -3665,7 +3682,7 @@ input::-moz-list-button {
|
||||
#fileSelectModal .expand-button {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 8px; /* 固定高度,不再使用百分比定位 */
|
||||
top: 7.5px; /* 固定高度,不再使用百分比定位 */
|
||||
transform: none; /* 移除垂直居中转换 */
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
@ -3774,7 +3791,13 @@ input.no-spinner {
|
||||
background-color: var(--button-gray-background-color) !important;
|
||||
}
|
||||
|
||||
/* 删除按钮样式 */
|
||||
/* 文件大小值的文本位置调整 */
|
||||
.file-size-cell .file-size-value {
|
||||
transform: translateY(-1px); /* 文本下移 */
|
||||
display: inline-block; /* 确保transform生效 */
|
||||
}
|
||||
|
||||
/* 转存记录删除按钮样式 */
|
||||
.delete-record-btn {
|
||||
color: #dc3545;
|
||||
cursor: pointer;
|
||||
@ -3786,6 +3809,8 @@ input.no-spinner {
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
visibility: hidden; /* 默认隐藏 */
|
||||
position: relative; /* 添加相对定位 */
|
||||
top: -0.5px; /* 上移 */
|
||||
}
|
||||
|
||||
/* 删除按钮图标大小 */
|
||||
@ -3853,3 +3878,46 @@ tr.selected-record .file-size-cell .delete-record-btn,
|
||||
table.selectable-records .expand-button:hover {
|
||||
background-color: #fff !important; /* 保持展开按钮原有的白色背景 */
|
||||
}
|
||||
|
||||
/* 模态框表格单元格文本垂直对齐调整 */
|
||||
#fileSelectModal .table td {
|
||||
transform: translateY(0.5px); /* 文本下移0.5px,不影响元素实际高度 */
|
||||
}
|
||||
|
||||
/* 确保文件大小、修改日期和操作列的文本位置与文件名一致 */
|
||||
#fileSelectModal .table td:not(.col-filename) {
|
||||
transform: translateY(1.5px); /* 非文件名列文本下移1.5px */
|
||||
}
|
||||
|
||||
/* 特别调整红色"×"符号的位置 */
|
||||
#fileSelectModal .table td.col-rename.text-danger div:not(.expand-button) {
|
||||
position: relative;
|
||||
top: -1px; /* 将"×"标记上移1px */
|
||||
}
|
||||
|
||||
/* 文件名列已经通过图标微调过,保持原样或细微调整 */
|
||||
#fileSelectModal .bi-folder-fill {
|
||||
color: #ffc107;
|
||||
font-size: 0.9rem;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
top: 1px; /* 负值向上移动,正值向下移动 */
|
||||
}
|
||||
|
||||
#fileSelectModal .bi-file-earmark {
|
||||
color: var(--dark-text-color);
|
||||
font-size: 0.9rem;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
top: 0px; /* 负值向上移动,正值向下移动 */
|
||||
}
|
||||
|
||||
/* 添加选中文件的样式 */
|
||||
.selected-file {
|
||||
background-color: var(--button-gray-background-color);
|
||||
}
|
||||
|
||||
/* 确保文件选择模态框中的表格行在选中状态下保持可见 */
|
||||
#fileSelectModal .table tr.selected-file:hover {
|
||||
background-color: var(--button-gray-background-color);
|
||||
}
|
||||
|
||||
@ -554,6 +554,7 @@
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" v-if="smart_param.savepath && smart_param.index == index && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath" title="恢复保存路径"><i class="bi bi-reply"></i></button>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="showSavepathSelect(index)" title="选择文件夹"><i class="bi bi-folder"></i></button>
|
||||
<button type="button" class="btn btn-outline-secondary" @click="resetFolder(index)" title="重置文件夹:此操作将删除当前保存路径中的所有文件及相关转存记录,且不可恢复,请谨慎操作"><i class="bi bi-folder-x"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -912,7 +913,14 @@
|
||||
</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}" style="vertical-align: top;">
|
||||
<tr v-for="(file, key) in fileSelect.fileList" :key="key"
|
||||
@click="fileSelect.selectDir ? (file.dir ? navigateTo(file.fid, file.file_name) : selectFileItem($event, file.fid)) : selectStartFid(file.fid)"
|
||||
:class="{
|
||||
'cursor-pointer': file.dir || !fileSelect.selectShare || (!fileSelect.selectDir && !file.dir),
|
||||
'selected-file': fileSelect.selectedFiles.includes(file.fid)
|
||||
}"
|
||||
style="vertical-align: top;"
|
||||
@mousedown="preventTextSelection($event, file.dir)">
|
||||
<td class="col-filename position-relative" style="padding-left: 5px; vertical-align: top;">
|
||||
<div v-if="!file._expandedFields || !file._expandedFields.includes('file_name')"
|
||||
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 25px;"
|
||||
@ -945,7 +953,10 @@
|
||||
<td class="col-size" v-if="file.dir" style="vertical-align: top;">{{ file.include_items }} 项</td>
|
||||
<td class="col-size" v-else style="vertical-align: top;">{{file.size | size}}</td>
|
||||
<td class="col-date" style="vertical-align: top;">{{file.updated_at | ts2date}}</td>
|
||||
<td class="col-action" v-if="!fileSelect.selectShare" style="vertical-align: top;"><a @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)" style="cursor: pointer;">删除</a></td>
|
||||
<td class="col-action" v-if="!fileSelect.selectShare" style="vertical-align: top;">
|
||||
<a @click.stop.prevent="deleteSelectedFiles(file.fid, file.file_name, file.dir)" style="cursor: pointer;">删除文件</a>
|
||||
<a @click.stop.prevent="deleteSelectedFiles(file.fid, file.file_name, file.dir, true)" style="cursor: pointer; margin-left: 10px;">删除文件和记录</a>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -953,6 +964,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" v-if="fileSelect.selectDir && !fileSelect.previewRegex">
|
||||
<div class="file-selection-info mr-auto" style="color: var(--dark-text-color); font-size: 0.875rem; line-height: 1.5;">
|
||||
共 {{ fileSelect.fileList.length }} 个项目<span v-if="fileSelect.selectedFiles.length > 0">,已选中 {{ fileSelect.selectedFiles.length }} 个项目</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm" @click="selectCurrentFolder()">{{fileSelect.selectShare ? '转存当前文件夹' : '保存到当前文件夹'}}</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare" @click="selectCurrentFolder(true)">保存到当前位置的「<span class="badge badge-light" v-html="formData.tasklist[fileSelect.index].taskname"></span>」文件夹</button>
|
||||
</div>
|
||||
@ -1046,7 +1060,9 @@
|
||||
selectShare: true,
|
||||
previewRegex: false,
|
||||
sortBy: "updated_at", // 默认排序字段
|
||||
sortOrder: "desc" // 默认排序顺序
|
||||
sortOrder: "desc", // 默认排序顺序
|
||||
selectedFiles: [], // 存储选中的文件ID
|
||||
lastSelectedFileIndex: -1 // 记录最后选择的文件索引
|
||||
},
|
||||
historyParams: {
|
||||
sortBy: "transfer_time",
|
||||
@ -1203,13 +1219,20 @@
|
||||
this.checkNewVersion();
|
||||
this.fetchUserInfo(); // 获取用户信息
|
||||
|
||||
// 从本地存储中恢复之前的标签页状态
|
||||
// 添加点击事件监听
|
||||
document.addEventListener('click', this.handleOutsideClick);
|
||||
document.addEventListener('click', this.handleModalOutsideClick);
|
||||
|
||||
// 添加模态框关闭事件监听
|
||||
$('#fileSelectModal').on('hidden.bs.modal', () => {
|
||||
this.fileSelect.selectedFiles = [];
|
||||
this.fileSelect.lastSelectedFileIndex = -1;
|
||||
});
|
||||
|
||||
// 检查本地存储中的标签页状态
|
||||
const savedTab = localStorage.getItem('quarkAutoSave_activeTab');
|
||||
if (savedTab) {
|
||||
this.activeTab = savedTab;
|
||||
} else {
|
||||
// 默认显示任务列表页面
|
||||
this.activeTab = 'tasklist';
|
||||
}
|
||||
|
||||
// 从本地存储中恢复侧边栏折叠状态
|
||||
@ -1251,6 +1274,15 @@
|
||||
// 添加点击事件监听器,用于在点击表格外区域时取消选择记录
|
||||
document.addEventListener('click', this.handleOutsideClick);
|
||||
|
||||
// 添加点击事件监听器,用于在点击模态框表格外区域时取消选择文件
|
||||
document.addEventListener('click', this.handleModalOutsideClick);
|
||||
|
||||
// 添加模态框关闭事件监听,清空选中文件列表
|
||||
$('#fileSelectModal').on('hidden.bs.modal', () => {
|
||||
this.fileSelect.selectedFiles = [];
|
||||
this.fileSelect.lastSelectedFileIndex = -1;
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
||||
|
||||
// 监听模态框显示事件,检查滚动条状态
|
||||
@ -2260,18 +2292,57 @@
|
||||
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);
|
||||
deleteFile(fid, fname, isDir, deleteRecords = false) {
|
||||
// 根据是否删除记录显示不同的确认提示
|
||||
let confirmMessage = deleteRecords
|
||||
? `确定要删除此项目及其关联记录吗?`
|
||||
: `确定要删除此项目吗?`;
|
||||
|
||||
if (!confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前路径作为save_path参数
|
||||
let save_path = "";
|
||||
if (this.fileSelect && this.fileSelect.paths) {
|
||||
save_path = this.fileSelect.paths.map(item => item.name).join("/");
|
||||
}
|
||||
|
||||
axios.post('/delete_file', { fid: fid, file_name: fname, delete_records: deleteRecords, save_path: save_path })
|
||||
.then(response => {
|
||||
if (response.data.code === 0) {
|
||||
// 从列表中移除文件
|
||||
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid !== fid);
|
||||
|
||||
// 如果文件在选中列表中,也从选中列表中移除
|
||||
if (this.fileSelect.selectedFiles.includes(fid)) {
|
||||
this.fileSelect.selectedFiles = this.fileSelect.selectedFiles.filter(id => id !== fid);
|
||||
|
||||
// 如果选中列表为空,重置最后选择的索引
|
||||
if (this.fileSelect.selectedFiles.length === 0) {
|
||||
this.fileSelect.lastSelectedFileIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示成功消息,根据是否删除记录显示不同的消息
|
||||
if (deleteRecords) {
|
||||
const deletedRecords = response.data.deleted_records || 0;
|
||||
this.showToast(`成功删除 1 个项目${deletedRecords > 0 ? `及其关联的 ${deletedRecords} 条记录` : ''}`);
|
||||
} else {
|
||||
this.showToast('成功删除 1 个项目');
|
||||
}
|
||||
|
||||
// 如果同时删除了记录,无论当前在哪个页面,都刷新历史记录
|
||||
if (deleteRecords) {
|
||||
this.loadHistoryRecords();
|
||||
}
|
||||
} else {
|
||||
alert('删除失败:' + response.data.message);
|
||||
alert('删除失败: ' + response.data.message);
|
||||
}
|
||||
}).catch(error => {
|
||||
// 错误处理
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('删除项目出错:', error);
|
||||
alert('删除项目出错: ' + (error.response?.data?.message || error.message));
|
||||
});
|
||||
},
|
||||
getSavepathDetail(params = 0) {
|
||||
@ -3288,6 +3359,231 @@
|
||||
this.lastSelectedRecordIndex = -1;
|
||||
}
|
||||
},
|
||||
selectFileItem(event, fileId) {
|
||||
// 如果是在预览模式或选择分享模式,不允许选择
|
||||
if (this.fileSelect.previewRegex || this.fileSelect.selectShare) return;
|
||||
|
||||
// 获取当前文件的索引
|
||||
const currentIndex = this.fileSelect.fileList.findIndex(file => file.fid === fileId);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
// 如果是Shift+点击,选择范围
|
||||
if (event.shiftKey && this.fileSelect.selectedFiles.length > 0) {
|
||||
// 找出所有已选中文件的索引
|
||||
const selectedIndices = this.fileSelect.selectedFiles.map(id =>
|
||||
this.fileSelect.fileList.findIndex(file => file.fid === id)
|
||||
).filter(index => index !== -1); // 过滤掉未找到的文件
|
||||
|
||||
if (selectedIndices.length > 0) {
|
||||
// 找出已选中文件中最靠前的索引
|
||||
const earliestSelectedIndex = Math.min(...selectedIndices);
|
||||
// 确定最终的选择范围
|
||||
const startIndex = Math.min(earliestSelectedIndex, currentIndex);
|
||||
const endIndex = Math.max(earliestSelectedIndex, currentIndex);
|
||||
|
||||
// 获取范围内所有文件的ID(排除文件夹)
|
||||
this.fileSelect.selectedFiles = this.fileSelect.fileList
|
||||
.slice(startIndex, endIndex + 1)
|
||||
.filter(file => !file.dir) // 只选择文件,不选择文件夹
|
||||
.map(file => file.fid);
|
||||
} else {
|
||||
// 如果没有有效的选中文件(可能是由于列表刷新),则只选择当前文件
|
||||
const file = this.fileSelect.fileList[currentIndex];
|
||||
if (!file.dir) { // 不选择文件夹
|
||||
this.fileSelect.selectedFiles = [fileId];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果是Ctrl/Cmd+点击,切换单个文件选择状态
|
||||
else if (event.ctrlKey || event.metaKey) {
|
||||
const file = this.fileSelect.fileList[currentIndex];
|
||||
if (file.dir) return; // 不允许选择文件夹
|
||||
|
||||
if (this.fileSelect.selectedFiles.includes(fileId)) {
|
||||
this.fileSelect.selectedFiles = this.fileSelect.selectedFiles.filter(id => id !== fileId);
|
||||
} else {
|
||||
this.fileSelect.selectedFiles.push(fileId);
|
||||
}
|
||||
}
|
||||
// 普通点击,清除当前选择并选择当前文件
|
||||
else {
|
||||
const file = this.fileSelect.fileList[currentIndex];
|
||||
if (file.dir) return; // 不允许选择文件夹
|
||||
|
||||
if (this.fileSelect.selectedFiles.length === 1 && this.fileSelect.selectedFiles.includes(fileId)) {
|
||||
this.fileSelect.selectedFiles = [];
|
||||
} else {
|
||||
this.fileSelect.selectedFiles = [fileId];
|
||||
}
|
||||
}
|
||||
|
||||
// 更新最后选择的文件索引,只有在有选择文件时才更新
|
||||
if (this.fileSelect.selectedFiles.length > 0) {
|
||||
this.fileSelect.lastSelectedFileIndex = currentIndex;
|
||||
} else {
|
||||
this.fileSelect.lastSelectedFileIndex = -1;
|
||||
}
|
||||
|
||||
// 阻止事件冒泡
|
||||
event.stopPropagation();
|
||||
},
|
||||
deleteSelectedFiles(clickedFid, clickedFname, isDir, deleteRecords = false) {
|
||||
// 如果是文件夹或者没有选中的文件,则按原来的方式删除单个文件
|
||||
if (isDir || this.fileSelect.selectedFiles.length === 0) {
|
||||
this.deleteFile(clickedFid, clickedFname, isDir, deleteRecords);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果点击的文件不在选中列表中,也按原来的方式删除单个文件
|
||||
if (!this.fileSelect.selectedFiles.includes(clickedFid)) {
|
||||
this.deleteFile(clickedFid, clickedFname, isDir, deleteRecords);
|
||||
return;
|
||||
}
|
||||
|
||||
// 多选删除
|
||||
const selectedCount = this.fileSelect.selectedFiles.length;
|
||||
|
||||
// 根据选中数量和是否删除记录使用不同的确认提示
|
||||
let confirmMessage = '';
|
||||
if (deleteRecords) {
|
||||
confirmMessage = selectedCount === 1
|
||||
? `确定要删除此项目及其关联记录吗?`
|
||||
: `确定要删除选中的 ${selectedCount} 个项目及其关联记录吗?`;
|
||||
} else {
|
||||
confirmMessage = selectedCount === 1
|
||||
? `确定要删除此项目吗?`
|
||||
: `确定要删除选中的 ${selectedCount} 个项目吗?`;
|
||||
}
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
// 获取当前路径作为save_path参数
|
||||
let save_path = "";
|
||||
if (this.fileSelect && this.fileSelect.paths) {
|
||||
save_path = this.fileSelect.paths.map(item => item.name).join("/");
|
||||
}
|
||||
|
||||
// 创建一个Promise数组来处理所有删除请求
|
||||
const deletePromises = this.fileSelect.selectedFiles.map(fid => {
|
||||
// 查找对应的文件对象,获取文件名
|
||||
const fileObj = this.fileSelect.fileList.find(file => file.fid === fid);
|
||||
const fileName = fileObj ? fileObj.file_name : '';
|
||||
|
||||
return axios.post('/delete_file', { fid: fid, file_name: fileName, delete_records: deleteRecords, save_path: save_path })
|
||||
.then(response => {
|
||||
return { fid: fid, success: response.data.code === 0, deleted_records: response.data.deleted_records || 0 };
|
||||
})
|
||||
.catch(error => {
|
||||
return { fid: fid, success: false, deleted_records: 0 };
|
||||
});
|
||||
});
|
||||
|
||||
// 等待所有删除请求完成
|
||||
Promise.all(deletePromises)
|
||||
.then(results => {
|
||||
// 统计成功和失败的数量
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failCount = results.length - successCount;
|
||||
// 统计删除的记录数
|
||||
const totalDeletedRecords = results.reduce((sum, r) => sum + (r.deleted_records || 0), 0);
|
||||
|
||||
// 从文件列表中移除成功删除的文件
|
||||
const successfullyDeletedFids = results.filter(r => r.success).map(r => r.fid);
|
||||
this.fileSelect.fileList = this.fileSelect.fileList.filter(item => !successfullyDeletedFids.includes(item.fid));
|
||||
|
||||
// 清空选中文件列表
|
||||
this.fileSelect.selectedFiles = [];
|
||||
this.fileSelect.lastSelectedFileIndex = -1;
|
||||
|
||||
// 显示结果
|
||||
if (failCount > 0) {
|
||||
alert(`成功删除 ${successCount} 个项目,${failCount} 个项目删除失败`);
|
||||
} else {
|
||||
// 根据是否删除记录显示不同的消息
|
||||
if (deleteRecords) {
|
||||
this.showToast(`成功删除 ${successCount} 个项目${totalDeletedRecords > 0 ? `及其关联的 ${totalDeletedRecords} 条记录` : ''}`);
|
||||
} else {
|
||||
this.showToast(`成功删除 ${successCount} 个项目`);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果同时删除了记录,无论当前在哪个页面都刷新历史记录
|
||||
if (deleteRecords) {
|
||||
this.loadHistoryRecords();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
handleModalOutsideClick(event) {
|
||||
// 如果当前不是文件选择模式或者没有选中的文件,则不处理
|
||||
if (this.fileSelect.previewRegex || this.fileSelect.selectShare || this.fileSelect.selectedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查点击是否在表格内
|
||||
const tableElement = document.querySelector('#fileSelectModal .table');
|
||||
|
||||
// 如果点击不在表格内,则清除选择
|
||||
if (tableElement && !tableElement.contains(event.target)) {
|
||||
this.fileSelect.selectedFiles = [];
|
||||
this.fileSelect.lastSelectedFileIndex = -1;
|
||||
}
|
||||
},
|
||||
preventTextSelection(event, isDir) {
|
||||
// 如果是文件夹,不阻止默认行为
|
||||
if (isDir) return;
|
||||
|
||||
// 如果是Shift点击或Ctrl/Cmd点击,阻止文本选择
|
||||
if (event.shiftKey || event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
resetFolder(index) {
|
||||
// 重置文件夹
|
||||
this.formData.tasklist[index].savepath = this.formData.tasklist[index].savepath.split('/').slice(0, -1).join('/');
|
||||
if (this.formData.tasklist[index].savepath.endsWith('/')) {
|
||||
this.formData.tasklist[index].savepath = this.formData.tasklist[index].savepath.slice(0, -1);
|
||||
}
|
||||
this.showToast('文件夹已重置');
|
||||
},
|
||||
resetFolder(index) {
|
||||
// 获取当前任务的保存路径
|
||||
const savePath = this.formData.tasklist[index].savepath;
|
||||
const taskName = this.formData.tasklist[index].taskname;
|
||||
|
||||
if (!savePath) {
|
||||
this.showToast('保存路径为空,无法重置');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示确认对话框
|
||||
if (confirm(`确定要重置文件夹「${savePath}」吗?`)) {
|
||||
// 显示加载状态
|
||||
this.modalLoading = true;
|
||||
|
||||
// 调用后端API
|
||||
axios.post('/reset_folder', {
|
||||
save_path: savePath,
|
||||
task_name: taskName
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
this.showToast(`重置成功:删除了 ${response.data.deleted_files || 0} 个文件,${response.data.deleted_records || 0} 条记录`);
|
||||
// 如果当前是历史记录页面,刷新记录
|
||||
if (this.activeTab === 'history') {
|
||||
this.loadHistoryRecords();
|
||||
}
|
||||
} else {
|
||||
alert(response.data.message || '重置文件夹失败');
|
||||
}
|
||||
this.modalLoading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('重置文件夹出错:', error);
|
||||
alert('重置文件夹出错: ' + (error.response?.data?.message || error.message));
|
||||
this.modalLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -29,6 +29,7 @@ class Aria2:
|
||||
default_task_config = {
|
||||
"auto_download": False, # 是否自动添加下载任务
|
||||
"pause": False, # 添加任务后为暂停状态,不自动开始(手动下载)
|
||||
"auto_delete_quark_files": False, # 是否在添加下载任务后自动删除夸克网盘文件
|
||||
}
|
||||
is_active = False
|
||||
rpc_url = None
|
||||
@ -116,6 +117,7 @@ class Aria2:
|
||||
# 筛选出当次转存的文件
|
||||
file_fids = []
|
||||
file_paths = []
|
||||
file_info = [] # 存储文件的完整信息,包括ID和路径
|
||||
|
||||
for file in dir_files:
|
||||
if file.get("dir", False):
|
||||
@ -132,7 +134,14 @@ class Aria2:
|
||||
|
||||
if is_current_file:
|
||||
file_fids.append(file["fid"])
|
||||
file_paths.append(f"{savepath}/{file_name}")
|
||||
file_path = f"{savepath}/{file_name}"
|
||||
file_paths.append(file_path)
|
||||
# 保存完整信息
|
||||
file_info.append({
|
||||
"fid": file["fid"],
|
||||
"path": file_path,
|
||||
"name": file_name
|
||||
})
|
||||
|
||||
if not file_fids:
|
||||
print("📝 Aria2: 未能匹配到需要下载的文件")
|
||||
@ -161,6 +170,9 @@ class Aria2:
|
||||
|
||||
# 使用全局排序函数对文件进行排序
|
||||
download_items.sort(key=lambda x: x["sort_key"])
|
||||
|
||||
# 记录成功添加到下载队列的文件信息,用于后续删除
|
||||
downloaded_files = []
|
||||
|
||||
# 按排序后的顺序下载文件
|
||||
for item in download_items:
|
||||
@ -193,8 +205,34 @@ class Aria2:
|
||||
]
|
||||
try:
|
||||
self.add_uri(aria2_params)
|
||||
# 记录成功添加到下载队列的文件信息
|
||||
idx = file_paths.index(file_path)
|
||||
if idx >= 0 and idx < len(file_info):
|
||||
downloaded_files.append(file_info[idx])
|
||||
except Exception as e:
|
||||
print(f"📥 Aria2 添加下载任务失败: {e}")
|
||||
|
||||
# 如果配置了自动删除且有成功添加下载任务的文件,则删除夸克网盘中的文件
|
||||
if task_config.get("auto_delete_quark_files") and downloaded_files:
|
||||
try:
|
||||
# 提取要删除的文件ID
|
||||
files_to_delete = []
|
||||
for file_data in downloaded_files:
|
||||
# 再次确认文件路径,确保只删除指定目录下的文件
|
||||
if file_data["path"].startswith(savepath):
|
||||
files_to_delete.append(file_data["fid"])
|
||||
|
||||
if files_to_delete:
|
||||
account.delete(files_to_delete)
|
||||
except Exception as e:
|
||||
print(f"📝 Aria2: 删除夸克网盘文件失败: {e}")
|
||||
else:
|
||||
if not task_config.get("auto_delete_quark_files"):
|
||||
# 未启用自动删除,不需要输出信息
|
||||
pass
|
||||
elif not downloaded_files:
|
||||
# 没有需要删除的文件,不需要输出信息
|
||||
pass
|
||||
|
||||
def _make_rpc_request(self, method, params=None):
|
||||
"""发出 JSON-RPC 请求."""
|
||||
|
||||
@ -819,7 +819,7 @@ class Quark:
|
||||
fids += response["data"]
|
||||
file_paths = file_paths[50:]
|
||||
else:
|
||||
print(f"获取目录ID:失败, {response['message']}")
|
||||
print(f"获取目录ID: 失败, {response['message']}")
|
||||
break
|
||||
if len(file_paths) == 0:
|
||||
break
|
||||
@ -1146,6 +1146,19 @@ class Quark:
|
||||
# 目前只是添加占位符,未来可以扩展功能
|
||||
pass
|
||||
|
||||
# 获取保存路径
|
||||
save_path = task.get("savepath", "")
|
||||
# 如果file_info中有子目录路径信息,则拼接完整路径
|
||||
subdir_path = file_info.get("subdir_path", "")
|
||||
if subdir_path:
|
||||
# 确保路径格式正确,避免双斜杠
|
||||
if save_path.endswith('/') and subdir_path.startswith('/'):
|
||||
save_path = save_path + subdir_path[1:]
|
||||
elif not save_path.endswith('/') and not subdir_path.startswith('/'):
|
||||
save_path = save_path + '/' + subdir_path
|
||||
else:
|
||||
save_path = save_path + subdir_path
|
||||
|
||||
# 添加记录到数据库
|
||||
db.add_record(
|
||||
task_name=task.get("taskname", ""),
|
||||
@ -1156,7 +1169,8 @@ class Quark:
|
||||
duration=duration,
|
||||
resolution=resolution,
|
||||
file_id=file_id,
|
||||
file_type=file_type
|
||||
file_type=file_type,
|
||||
save_path=save_path
|
||||
)
|
||||
|
||||
# 关闭数据库连接
|
||||
@ -1195,12 +1209,26 @@ class Quark:
|
||||
file_id = file_info.get("fid", "")
|
||||
task_name = task.get("taskname", "")
|
||||
|
||||
# 获取保存路径
|
||||
save_path = task.get("savepath", "")
|
||||
# 如果file_info中有子目录路径信息,则拼接完整路径
|
||||
subdir_path = file_info.get("subdir_path", "")
|
||||
if subdir_path:
|
||||
# 确保路径格式正确,避免双斜杠
|
||||
if save_path.endswith('/') and subdir_path.startswith('/'):
|
||||
save_path = save_path + subdir_path[1:]
|
||||
elif not save_path.endswith('/') and not subdir_path.startswith('/'):
|
||||
save_path = save_path + '/' + subdir_path
|
||||
else:
|
||||
save_path = save_path + subdir_path
|
||||
|
||||
# 更新记录
|
||||
updated = db.update_renamed_to(
|
||||
file_id=file_id,
|
||||
original_name=original_name,
|
||||
renamed_to=renamed_to,
|
||||
task_name=task_name
|
||||
task_name=task_name,
|
||||
save_path=save_path
|
||||
)
|
||||
|
||||
# 关闭数据库连接
|
||||
@ -1255,13 +1283,39 @@ class Quark:
|
||||
# 使用原文件名和任务名查找记录
|
||||
task_name = task.get("taskname", "")
|
||||
|
||||
# 获取保存路径
|
||||
save_path = task.get("savepath", "")
|
||||
# 注意:从日志中无法获取子目录信息,只能使用任务的主保存路径
|
||||
|
||||
# 检查文件是否已存在于记录中
|
||||
# 先查询是否有匹配的记录
|
||||
cursor = db.conn.cursor()
|
||||
query = "SELECT file_id FROM transfer_records WHERE original_name = ? AND task_name = ? AND save_path = ?"
|
||||
cursor.execute(query, (old_name, task_name, save_path))
|
||||
result = cursor.fetchone()
|
||||
|
||||
# 如果找到了匹配的记录,使用file_id进行更新
|
||||
file_id = result[0] if result else ""
|
||||
|
||||
# 更新记录
|
||||
updated = db.update_renamed_to(
|
||||
file_id="", # 不使用file_id查询,因为在日志中无法获取
|
||||
original_name=old_name,
|
||||
renamed_to=new_name,
|
||||
task_name=task_name
|
||||
)
|
||||
if file_id:
|
||||
# 使用file_id更新
|
||||
updated = db.update_renamed_to(
|
||||
file_id=file_id,
|
||||
original_name="", # 不使用原文件名,因为已有file_id
|
||||
renamed_to=new_name,
|
||||
task_name=task_name,
|
||||
save_path=save_path
|
||||
)
|
||||
else:
|
||||
# 使用原文件名更新
|
||||
updated = db.update_renamed_to(
|
||||
file_id="", # 不使用file_id查询,因为在日志中无法获取
|
||||
original_name=old_name,
|
||||
renamed_to=new_name,
|
||||
task_name=task_name,
|
||||
save_path=save_path
|
||||
)
|
||||
|
||||
# 关闭数据库连接
|
||||
db.close()
|
||||
@ -1270,7 +1324,7 @@ class Quark:
|
||||
except Exception as e:
|
||||
print(f"根据日志更新转存记录失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# 批量处理重命名日志
|
||||
def process_rename_logs(self, task, rename_logs):
|
||||
"""处理重命名日志列表,更新数据库记录
|
||||
@ -1282,12 +1336,55 @@ class Quark:
|
||||
for log in rename_logs:
|
||||
if "重命名:" in log and "→" in log and "失败" not in log:
|
||||
self.update_transfer_record_from_log(task, log)
|
||||
|
||||
def check_file_exists_in_records(self, file_id, task=None):
|
||||
"""检查文件ID是否存在于转存记录中
|
||||
|
||||
Args:
|
||||
file_id: 要检查的文件ID
|
||||
task: 可选的任务信息,用于进一步筛选
|
||||
|
||||
Returns:
|
||||
bool: 文件是否已存在于记录中
|
||||
"""
|
||||
if not file_id:
|
||||
return False
|
||||
|
||||
try:
|
||||
# 初始化数据库
|
||||
db = RecordDB()
|
||||
|
||||
# 构建查询条件
|
||||
conditions = ["file_id = ?"]
|
||||
params = [file_id]
|
||||
|
||||
# 如果提供了任务信息,添加任务名称条件
|
||||
if task and task.get("taskname"):
|
||||
conditions.append("task_name = ?")
|
||||
params.append(task.get("taskname"))
|
||||
|
||||
# 构建WHERE子句
|
||||
where_clause = " AND ".join(conditions)
|
||||
|
||||
# 查询是否存在匹配的记录
|
||||
cursor = db.conn.cursor()
|
||||
query = f"SELECT COUNT(*) FROM transfer_records WHERE {where_clause}"
|
||||
cursor.execute(query, params)
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
# 关闭数据库连接
|
||||
db.close()
|
||||
|
||||
return count > 0
|
||||
except Exception as e:
|
||||
print(f"检查文件记录时出错: {e}")
|
||||
return False
|
||||
|
||||
def do_save_task(self, task):
|
||||
# 判断资源失效记录
|
||||
if task.get("shareurl_ban"):
|
||||
print(f"分享资源已失效:{task['shareurl_ban']}")
|
||||
add_notify(f"❗《{task['taskname']}》分享资源已失效:{task['shareurl_ban']}\n")
|
||||
print(f"分享资源已失效: {task['shareurl_ban']}")
|
||||
add_notify(f"❗《{task['taskname']}》分享资源已失效: {task['shareurl_ban']}\n")
|
||||
return
|
||||
|
||||
# 标准化保存路径,去掉可能存在的首位斜杠,然后重新添加
|
||||
@ -1304,8 +1401,8 @@ class Quark:
|
||||
is_sharing, stoken = self.get_stoken(pwd_id, passcode)
|
||||
if not is_sharing:
|
||||
task["shareurl_ban"] = stoken
|
||||
print(f"分享详情获取失败:{stoken}")
|
||||
add_notify(f"❗《{task['taskname']}》分享详情获取失败:{stoken}\n")
|
||||
print(f"分享详情获取失败: {stoken}")
|
||||
add_notify(f"❗《{task['taskname']}》分享详情获取失败: {stoken}\n")
|
||||
return
|
||||
share_detail = self.get_detail(pwd_id, stoken, pdir_fid, _fetch_share=1)
|
||||
# 获取保存路径fid
|
||||
@ -1393,7 +1490,7 @@ class Quark:
|
||||
if not share_file_list:
|
||||
if subdir_path == "":
|
||||
task["shareurl_ban"] = "分享为空,文件已被分享者删除"
|
||||
add_notify(f"❌《{task['taskname']}》:{task['shareurl_ban']}\n")
|
||||
add_notify(f"❌《{task['taskname']}》: {task['shareurl_ban']}\n")
|
||||
return tree
|
||||
elif (
|
||||
len(share_file_list) == 1
|
||||
@ -1445,7 +1542,7 @@ class Quark:
|
||||
if task.get("use_sequence_naming") or task.get("use_episode_naming"):
|
||||
# 计算剩余的实际可用文件数(排除文件夹)
|
||||
remaining_usable_count = len([f for f in share_file_list if not f.get("dir", False)])
|
||||
print(f"📑 应用过滤词: {task['filterwords']},剩余{remaining_usable_count}个项目")
|
||||
print(f"📑 应用过滤词: {task['filterwords']},剩余 {remaining_usable_count} 个项目")
|
||||
else:
|
||||
# 正则模式下,需要先检查哪些文件/文件夹会被实际转存
|
||||
pattern, replace = "", ""
|
||||
@ -1479,7 +1576,7 @@ class Quark:
|
||||
print(f"⚠️ 计算可处理项目时出错: {str(e)}")
|
||||
remaining_count = len([f for f in share_file_list if re.search(pattern, f["file_name"])])
|
||||
|
||||
print(f"📑 应用过滤词: {task['filterwords']},剩余{remaining_count}个项目")
|
||||
print(f"📑 应用过滤词: {task['filterwords']},剩余 {remaining_count} 个项目")
|
||||
print()
|
||||
|
||||
# 获取目标目录文件列表
|
||||
@ -1563,8 +1660,14 @@ class Quark:
|
||||
filtered_share_files = []
|
||||
for share_file in share_file_list:
|
||||
if share_file["dir"]:
|
||||
# 不再直接添加目录到filtered_share_files
|
||||
# 目录处理会在后续专门的循环中进行
|
||||
filtered_share_files.append(share_file)
|
||||
continue
|
||||
|
||||
# 检查文件ID是否存在于转存记录中
|
||||
file_id = share_file.get("fid", "")
|
||||
if file_id and self.check_file_exists_in_records(file_id, task):
|
||||
# 文件ID已存在于记录中,跳过处理
|
||||
continue
|
||||
|
||||
file_size = share_file.get("size", 0)
|
||||
@ -1775,6 +1878,12 @@ class Quark:
|
||||
|
||||
# 添加符合的
|
||||
for share_file in share_file_list:
|
||||
# 检查文件ID是否存在于转存记录中
|
||||
file_id = share_file.get("fid", "")
|
||||
if file_id and self.check_file_exists_in_records(file_id, task):
|
||||
# 文件ID已存在于记录中,跳过处理
|
||||
continue
|
||||
|
||||
# 检查文件是否已存在(通过大小和扩展名)- 新增的文件查重逻辑
|
||||
is_duplicate = False
|
||||
if not share_file["dir"]: # 文件夹不进行内容查重
|
||||
@ -2215,7 +2324,7 @@ class Quark:
|
||||
else:
|
||||
err_msg = save_file_return["message"]
|
||||
if err_msg:
|
||||
add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n")
|
||||
add_notify(f"❌《{task['taskname']}》转存失败: {err_msg}\n")
|
||||
else:
|
||||
# 没有新文件需要转存
|
||||
if not subdir_path: # 只在顶层(非子目录)打印一次消息
|
||||
@ -2369,7 +2478,7 @@ class Quark:
|
||||
# 移除直接打印的部分,由do_save负责打印
|
||||
# print(rename_log)
|
||||
except Exception as e:
|
||||
rename_log = f"重命名出错: {dir_file['file_name']} → {save_name},错误:{str(e)}"
|
||||
rename_log = f"重命名出错: {dir_file['file_name']} → {save_name},错误: {str(e)}"
|
||||
rename_logs.append(rename_log)
|
||||
# 移除直接打印的部分,由do_save负责打印
|
||||
# print(rename_log)
|
||||
@ -2455,7 +2564,7 @@ class Quark:
|
||||
# 获取分享详情
|
||||
is_sharing, stoken = self.get_stoken(pwd_id, passcode)
|
||||
if not is_sharing:
|
||||
print(f"分享详情获取失败:{stoken}")
|
||||
print(f"分享详情获取失败: {stoken}")
|
||||
return False, []
|
||||
|
||||
# 获取分享文件列表
|
||||
@ -2472,6 +2581,12 @@ class Quark:
|
||||
if task.get("update_subdir") and re.search(task["update_subdir"], share_file["file_name"]):
|
||||
filtered_share_files.append(share_file)
|
||||
continue
|
||||
|
||||
# 检查文件ID是否存在于转存记录中
|
||||
file_id = share_file.get("fid", "")
|
||||
if file_id and self.check_file_exists_in_records(file_id, task):
|
||||
# 文件ID已存在于记录中,跳过处理
|
||||
continue
|
||||
|
||||
# 从共享文件中提取剧集号
|
||||
episode_num = extract_episode_number_local(share_file["file_name"])
|
||||
@ -2758,11 +2873,11 @@ class Quark:
|
||||
return True, rename_logs
|
||||
else:
|
||||
err_msg = query_task_return["message"]
|
||||
add_notify(f"❌《{task['taskname']}》转存失败:{err_msg}\n")
|
||||
add_notify(f"❌《{task['taskname']}》转存失败: {err_msg}\n")
|
||||
return False, []
|
||||
else:
|
||||
print(f"❌ 保存文件失败: {save_file_return['message']}")
|
||||
add_notify(f"❌《{task['taskname']}》转存失败:{save_file_return['message']}\n")
|
||||
add_notify(f"❌《{task['taskname']}》转存失败: {save_file_return['message']}\n")
|
||||
return False, []
|
||||
else:
|
||||
# print("没有需要保存的新文件")
|
||||
@ -2950,7 +3065,7 @@ class Quark:
|
||||
if new_name != orig_name:
|
||||
rename_operations.append((dir_file, new_name))
|
||||
except Exception as e:
|
||||
print(f"正则替换出错: {dir_file['file_name']},错误:{str(e)}")
|
||||
print(f"正则替换出错: {dir_file['file_name']},错误: {str(e)}")
|
||||
|
||||
# 按原始文件名字母顺序排序,使重命名操作有序进行
|
||||
# rename_operations.sort(key=lambda x: x[0]["file_name"])
|
||||
@ -2989,7 +3104,7 @@ class Quark:
|
||||
rename_logs.append(error_log)
|
||||
except Exception as e:
|
||||
# 收集错误日志但不打印
|
||||
error_log = f"重命名出错: {dir_file['file_name']} → {new_name},错误:{str(e)}"
|
||||
error_log = f"重命名出错: {dir_file['file_name']} → {new_name},错误: {str(e)}"
|
||||
rename_logs.append(error_log)
|
||||
else:
|
||||
# 重名警告但不打印
|
||||
|
||||
Loading…
Reference in New Issue
Block a user