From 18f344e19c54bee1ca9873a46bc3dd04626918ac Mon Sep 17 00:00:00 2001 From: x1ao4 Date: Wed, 21 May 2025 19:08:20 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AList=20=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E6=97=A0=E6=B3=95=E5=88=B7=E6=96=B0=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/alist.py | 268 ++++++++++++++++++++++++++++++++++++--------- quark_auto_save.py | 59 ++++++++-- 2 files changed, 264 insertions(+), 63 deletions(-) diff --git a/plugins/alist.py b/plugins/alist.py index 5d3a306..6dd24e2 100644 --- a/plugins/alist.py +++ b/plugins/alist.py @@ -2,6 +2,7 @@ import os import re import json import requests +import time class Alist: @@ -17,32 +18,82 @@ class Alist: quark_root_dir = None def __init__(self, **kwargs): + """初始化AList插件""" + # 标记插件名称,便于日志识别 + self.plugin_name = self.__class__.__name__.lower() + if kwargs: + # 加载配置 for key, _ in self.default_config.items(): if key in kwargs: setattr(self, key, kwargs[key]) else: - print(f"{self.__class__.__name__} 模块缺少必要参数: {key}") - if self.url and self.token: - if self.get_info(): - success, result = self.storage_id_to_path(self.storage_id) - if success: - self.storage_mount_path, self.quark_root_dir = result - self.is_active = True + print(f"{self.plugin_name} 模块缺少必要参数: {key}") + + # 检查基本配置 + if not self.url or not self.token or not self.storage_id: + return + + # 确保URL格式正确 + if not self.url.startswith(("http://", "https://")): + self.url = f"http://{self.url}" + + # 移除URL末尾的斜杠 + self.url = self.url.rstrip("/") + + # 验证AList连接 + if self.get_info(): + # 解析存储ID + success, result = self.storage_id_to_path(self.storage_id) + if success: + self.storage_mount_path, self.quark_root_dir = result + + # 确保路径格式正确 + if self.quark_root_dir != "/": + if not self.quark_root_dir.startswith("/"): + self.quark_root_dir = f"/{self.quark_root_dir}" + self.quark_root_dir = self.quark_root_dir.rstrip("/") + + if not self.storage_mount_path.startswith("/"): + self.storage_mount_path = f"/{self.storage_mount_path}" + self.storage_mount_path = self.storage_mount_path.rstrip("/") + + self.is_active = True + else: + print(f"AList 刷新: 存储信息解析失败") + else: + print(f"AList 刷新: 服务器连接失败") def run(self, task, **kwargs): - if task.get("savepath") and task.get("savepath").startswith( - self.quark_root_dir - ): - alist_path = os.path.normpath( - os.path.join( - self.storage_mount_path, - task["savepath"].replace(self.quark_root_dir, "", 1).lstrip("/"), - ) - ).replace("\\", "/") - self.refresh(alist_path) + """ + 插件主入口,当有新文件保存时触发刷新AList目录 + + Args: + task: 任务信息,包含savepath等关键信息 + **kwargs: 其他参数,包括tree和rename_logs + + Returns: + task: 返回原任务信息 + """ + # 检查路径是否在夸克根目录内 + if task.get("savepath"): + # 确保路径符合要求 + quark_path = task.get("savepath", "") + if not quark_path.startswith("/"): + quark_path = f"/{quark_path}" + + # 检查路径是否在夸克根目录下,或夸克根目录是否为根目录 + if self.quark_root_dir == "/" or quark_path.startswith(self.quark_root_dir): + # 映射到AList路径 + alist_path = self.map_quark_to_alist_path(quark_path) + + # 执行刷新 + self.refresh(alist_path) + + return task def get_info(self): + """获取AList服务器信息""" url = f"{self.url}/api/admin/setting/list" headers = {"Authorization": self.token} querystring = {"group": "1"} @@ -51,26 +102,35 @@ class Alist: response.raise_for_status() response = response.json() if response.get("code") == 200: - print( - f"AList 刷新: {response.get('data',[])[1].get('value','')} {response.get('data',[])[0].get('value','')}" - ) + print(f"AList 刷新: {response.get('data',[])[1].get('value','')} {response.get('data',[])[0].get('value','')}") return True else: print(f"AList 刷新: 连接失败 ❌ {response.get('message')}") - except requests.exceptions.RequestException as e: - print(f"获取 AList 信息出错: {e}") + except Exception as e: + print(f"AList 刷新: 连接出错 ❌ {str(e)}") return False def storage_id_to_path(self, storage_id): + """ + 将存储ID转换为挂载路径和夸克根目录 + + Args: + storage_id: 存储ID + + Returns: + tuple: (成功状态, (挂载路径, 夸克根目录)) + """ storage_mount_path, quark_root_dir = None, None + # 1. 检查是否符合 /aaa:/bbb 格式 if match := re.match(r"^(\/[^:]*):(\/[^:]*)$", storage_id): # 存储挂载路径, 夸克根文件夹 storage_mount_path, quark_root_dir = match.group(1), match.group(2) file_list = self.get_file_list(storage_mount_path) if file_list.get("code") != 200: - print(f"AList 刷新: 获取挂载路径失败 ❌ {file_list.get('message')}") + print(f"AList 刷新: 挂载路径无效") return False, (None, None) + # 2. 检查是否数字,调用 Alist API 获取存储信息 elif re.match(r"^\d+$", storage_id): if storage_info := self.get_storage_info(storage_id): @@ -82,14 +142,15 @@ class Alist: quark_root_dir = self.get_root_folder_full_path( addition["cookie"], addition["root_folder_id"] ) - elif storage_info["driver"] == "QuarkTV": - print( - f"AList 刷新: [QuarkTV] 驱动 ⚠️ storage_id 请手动填入 /Alist挂载路径:/Quark目录路径" - ) else: - print(f"AList 刷新: 不支持 [{storage_info['driver']}] 驱动 ❌") + print(f"AList 刷新: 不支持 [{storage_info['driver']}] 驱动") + else: + print(f"AList 刷新: 获取存储信息失败") + return False, (None, None) else: - print(f"AList 刷新: storage_id [{storage_id}] 格式错误 ❌") + print(f"AList 刷新: storage_id 格式错误") + return False, (None, None) + # 返回结果 if storage_mount_path and quark_root_dir: return True, (storage_mount_path, quark_root_dir) @@ -97,6 +158,7 @@ class Alist: return False, (None, None) def get_storage_info(self, storage_id): + """获取AList存储详细信息""" url = f"{self.url}/api/admin/storage/get" headers = {"Authorization": self.token} querystring = {"id": storage_id} @@ -105,34 +167,119 @@ class Alist: response.raise_for_status() data = response.json() if data.get("code") == 200: - return data.get("data", []) + return data.get("data", {}) else: - print(f"AList 刷新: 存储 {storage_id} 连接失败 ❌ {data.get('message')}") + print(f"AList 刷新: 获取存储信息失败 ({data.get('message', '未知错误')})") except Exception as e: - print(f"AList 刷新: 获取 AList 存储出错 {e}") - return [] + print(f"AList 刷新: 获取存储信息出错 ({str(e)})") + return None - def refresh(self, path): - data = self.get_file_list(path, True) - if data.get("code") == 200: - print(f"📁 AList 刷新: 目录 [{path}] 成功 ✅") - return data.get("data") - elif "object not found" in data.get("message", ""): - # 如果是根目录就不再往上查找 - if path == "/" or path == self.storage_mount_path: - print(f"📁 AList 刷新: 根目录不存在,请检查 AList 配置") - return False - # 获取父目录 - parent_path = os.path.dirname(path) - print(f"📁 AList 刷新: [{path}] 不存在,转父目录 [{parent_path}]") - # 递归刷新父目录 - return self.refresh(parent_path) + def refresh(self, path, retry_count=2): + """ + 刷新AList目录,支持重试和自动回溯到父目录 + + Args: + path: 需要刷新的路径 + retry_count: 重试次数,默认重试2次 + """ + # 实现重试机制 + for attempt in range(retry_count + 1): + if attempt > 0: + # 不输出重试信息 + pass + + data = self.get_file_list(path, True) + + if data.get("code") == 200: + print(f"📁 刷新 AList 目录: [{path}] 成功 ✅") + return data.get("data") + elif "object not found" in data.get("message", ""): + # 如果是根目录就不再往上查找 + if path == "/" or path == self.storage_mount_path: + print(f"📁 AList 刷新: 根目录不存在,请检查配置") + return False + + # 自动获取父目录并尝试刷新 + parent_path = os.path.dirname(path) + + # 先刷新父目录 + parent_result = self.get_file_list(parent_path, True) + if parent_result.get("code") == 200: + # 再次尝试刷新原目录 + retry_data = self.get_file_list(path, True) + if retry_data.get("code") == 200: + print(f"📁 刷新 AList 目录: [{path}] 成功 ✅") + return retry_data.get("data") + + # 如果刷新父目录后仍不成功,则递归处理父目录 + return self.refresh(parent_path, retry_count) + elif attempt < retry_count: + # 如果还有重试次数,等待后继续 + time.sleep(1) # 等待1秒后重试 + else: + # 已达到最大重试次数 + error_msg = data.get("message", "未知错误") + print(f"📁 AList 刷新: 失败 ❌ {error_msg}") + return None + + def map_quark_to_alist_path(self, quark_path): + """ + 将夸克路径映射到AList路径 + + Args: + quark_path: 夸克路径,例如 /Movies/2024 + + Returns: + str: 对应的AList路径,例如 /movies/2024 + """ + # 确保路径格式正确 + if not quark_path.startswith("/"): + quark_path = f"/{quark_path}" + + # 特殊处理根目录的情况 + if self.quark_root_dir == "/": + # 夸克根目录是/,直接映射到AList挂载路径 + if quark_path == "/": + return self.storage_mount_path + else: + # 组合路径 + alist_path = os.path.normpath( + os.path.join(self.storage_mount_path, quark_path.lstrip("/")) + ).replace("\\", "/") + return alist_path + + # 常规情况:检查路径是否在夸克根目录下 + if not quark_path.startswith(self.quark_root_dir): + # 尝试强制映射,去掉前导路径 + relative_path = quark_path.lstrip("/") else: - print(f"📁 AList 刷新: 失败 ❌ {data.get('message')}") + # 去除夸克根目录前缀,并确保路径格式正确 + relative_path = quark_path.replace(self.quark_root_dir, "", 1).lstrip("/") + + # 构建AList路径 + alist_path = os.path.normpath( + os.path.join(self.storage_mount_path, relative_path) + ).replace("\\", "/") + + return alist_path def get_file_list(self, path, force_refresh=False): + """ + 获取AList指定路径下的文件列表 + + Args: + path: AList文件路径 + force_refresh: 是否强制刷新,默认False + + Returns: + dict: AList API返回的数据 + """ url = f"{self.url}/api/fs/list" - headers = {"Authorization": self.token} + headers = { + "Authorization": self.token, + "Content-Type": "application/json", + "Accept": "application/json", + } payload = { "path": path, "refresh": force_refresh, @@ -140,15 +287,28 @@ class Alist: "page": 1, "per_page": 0, } + try: - response = requests.request("POST", url, headers=headers, json=payload) + response = requests.request( + "POST", + url, + headers=headers, + json=payload, + timeout=10 + ) response.raise_for_status() return response.json() + except requests.exceptions.RequestException as e: + print(f"📁 AList 刷新: 网络请求出错 ❌ {str(e)}") + except json.JSONDecodeError as e: + print(f"📁 AList 刷新: 解析数据出错 ❌ {str(e)}") except Exception as e: - print(f"📁 AList 刷新: 获取文件列表出错 ❌ {e}") - return {} + print(f"📁 AList 刷新: 未知错误 ❌ {str(e)}") + + return {"code": 500, "message": "获取文件列表出错"} def get_root_folder_full_path(self, cookie, pdir_fid): + """获取夸克根文件夹的完整路径""" if pdir_fid == "0": return "/" url = "https://drive-h.quark.cn/1/clouddrive/file/sort" @@ -178,5 +338,5 @@ class Alist: path = f"{path}/{item['file_name']}" return path except Exception as e: - print(f"AList 刷新: 获取 Quark 路径出错 {e}") + print(f"AList 刷新: 获取路径出错 ❌ {str(e)}") return "" diff --git a/quark_auto_save.py b/quark_auto_save.py index f7ae471..9b34d72 100644 --- a/quark_auto_save.py +++ b/quark_auto_save.py @@ -3004,7 +3004,6 @@ def verify_account(account): # 验证账号 print(f"▶️ 验证第 {account.index} 个账号") if "__uid" not in account.cookie: - print(f"💡 不存在 cookie 必要参数,判断为仅签到") return False else: account_info = account.init() @@ -3052,7 +3051,8 @@ def do_sign(account): ): print(message) else: - message = message.replace("今日", f"[{account.nickname}]今日") + if account.nickname: + message = message.replace("今日", f"{account.nickname} 今日") add_notify(message) else: print(f"📅 签到异常: {sign_return}") @@ -4061,24 +4061,65 @@ def do_save(account, tasklist=[]): if not display_files and file_nodes: # 查找目录中修改时间最新的文件(可能是刚刚转存的) today = datetime.now().strftime('%Y-%m-%d') - recent_files = [] + recent_files = [] # 定义并初始化recent_files变量 # 首先尝试通过修改日期过滤当天的文件 for file in file_nodes: # 如果有时间戳,转换为日期字符串 if 'updated_at' in file and file['updated_at']: - update_time = datetime.fromtimestamp(file['updated_at']).strftime('%Y-%m-%d') - if update_time == today: - recent_files.append(file) + try: + # 检查时间戳是否在合理范围内 (1970-2100年) + timestamp = file['updated_at'] + if timestamp > 4102444800: # 2100年的时间戳 + # 可能是毫秒级时间戳,尝试转换为秒级 + timestamp = timestamp / 1000 + + # 再次检查时间戳是否在合理范围内 + if 0 < timestamp < 4102444800: + update_time = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d') + if update_time == today: + recent_files.append(file) + else: + print(f"警告: 文件 {file.get('file_name', '未知')} 的时间戳 {file['updated_at']} 超出范围") + except (ValueError, OSError, OverflowError) as e: + print(f"警告: 处理文件 {file.get('file_name', '未知')} 的时间戳时出错: {e}") # 如果没有找到当天的文件,至少显示一个最新的文件 if not recent_files and file_nodes: - # 按修改时间排序 - recent_files = sorted(file_nodes, key=lambda x: x.get('updated_at', 0), reverse=True) + # 定义安全的排序键函数 + def safe_timestamp_key(x): + try: + timestamp = x.get('updated_at', 0) + # 如果时间戳太大,可能是毫秒级时间戳 + if timestamp > 4102444800: # 2100年的时间戳 + timestamp = timestamp / 1000 + # 再次检查范围 + if timestamp < 0 or timestamp > 4102444800: + return 0 # 无效时间戳返回0 + return timestamp + except (ValueError, TypeError): + return 0 # 无效返回0 + try: + # 按修改时间排序,使用安全的排序函数 + recent_files = sorted(file_nodes, key=safe_timestamp_key, reverse=True) + except Exception as e: + print(f"警告: 文件排序时出错: {e}") + # 如果排序出错,直接使用原始列表 + recent_files = file_nodes + # 只取第一个作为显示 if recent_files: - display_files.append(recent_files[0]['file_name']) + try: + display_files.append(recent_files[0]['file_name']) + except (IndexError, KeyError) as e: + print(f"警告: 获取文件名时出错: {e}") + # 如果出错,尝试添加第一个文件(如果有) + if file_nodes: + try: + display_files.append(file_nodes[0]['file_name']) + except (KeyError, IndexError): + print("警告: 无法获取有效的文件名") # 添加成功通知 - 修复问题:确保在有文件时添加通知 if display_files: From 0b19935e069325cf7ca841176d4b2107303c924c Mon Sep 17 00:00:00 2001 From: x1ao4 Date: Thu, 22 May 2025 00:56:03 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=B8=BA=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=A2=9E=E5=8A=A0=E9=9A=8F=E6=9C=BA=E5=BB=B6=E8=BF=9F?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/run.py | 21 +++++++++++++++ app/static/css/main.css | 40 +++++++++++++++++++++++++++ app/templates/index.html | 58 +++++++++++++++++++++++++++++++++++++--- quark_auto_save.py | 1 - 4 files changed, 115 insertions(+), 5 deletions(-) diff --git a/app/run.py b/app/run.py index 64791a0..58c77b0 100644 --- a/app/run.py +++ b/app/run.py @@ -25,6 +25,8 @@ import base64 import sys import os import re +import random +import time parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, parent_dir) @@ -683,6 +685,18 @@ def add_task(): # 定时任务执行的函数 def run_python(args): logging.info(f">>> 定时运行任务") + # 检查是否需要随机延迟执行 + if delay := config_data.get("crontab_delay"): + try: + delay_seconds = int(delay) + if delay_seconds > 0: + # 在0到设定值之间随机选择一个延迟时间 + random_delay = random.randint(0, delay_seconds) + logging.info(f">>> 随机延迟执行 {random_delay}秒") + time.sleep(random_delay) + except (ValueError, TypeError): + logging.warning(f">>> 延迟执行设置无效: {delay}") + os.system(f"{PYTHON_PATH} {args}") @@ -708,6 +722,9 @@ def reload_tasks(): logging.info(">>> 重载调度器") logging.info(f"调度状态: {scheduler_state_map[scheduler.state]}") logging.info(f"定时规则: {crontab}") + # 记录延迟执行设置 + if delay := config_data.get("crontab_delay"): + logging.info(f"延迟执行: 0-{delay}秒") logging.info(f"现有任务: {scheduler.get_jobs()}") return True else: @@ -740,6 +757,10 @@ def init(): # 默认定时规则 if not config_data.get("crontab"): config_data["crontab"] = "0 8,18,20 * * *" + + # 默认延迟执行设置 + if "crontab_delay" not in config_data: + config_data["crontab_delay"] = 0 # 初始化插件配置 _, plugins_config_default, task_plugins_config_default = Config.load_plugins() diff --git a/app/static/css/main.css b/app/static/css/main.css index da24ce9..685390c 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -3724,3 +3724,43 @@ input::-moz-list-button { background-color: #f7f7fa; /* 表头悬停背景色 */ cursor: pointer; } + +/* 移除number类型输入框的上下箭头 */ +input.no-spinner::-webkit-outer-spin-button, +input.no-spinner::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox也需要特别处理 */ +input.no-spinner { + -moz-appearance: textfield; +} + +/* 秒字框正方形样式 */ +.square-append { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 !important; +} + +/* 确保Crontab和延迟执行框在移动端也保持一行 */ +@media (max-width: 767.98px) { + .row.mb-2 .col-sm-6 { + flex: 0 0 50%; + max-width: 50%; + } + + .row.mb-2 .col-sm-6.pr-1 { + padding-right: 4px !important; + padding-left: 15px !important; + } + + .row.mb-2 .col-sm-6.pl-1 { + padding-left: 4px !important; + padding-right: 15px !important; + } +} diff --git a/app/templates/index.html b/app/templates/index.html index 280f0d9..dd1845b 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -273,11 +273,26 @@ -
-
- Crontab +
+
+
+
+ Crontab +
+ +
+
+
+
+
+ 延迟执行 +
+ +
+ +
+
-
@@ -1438,6 +1453,7 @@ return task; }); + // 获取所有任务父目录 config_data.tasklist.forEach(item => { parentDir = this.getParentDirectory(item.savepath) @@ -3072,6 +3088,40 @@ } }); }, + validateNumberInput(event, field, max) { + // 获取当前输入值 + let value = event.target.value; + // 获取输入框的当前光标位置 + const cursorPosition = event.target.selectionStart; + + // 记录原始长度 + const originalLength = value.length; + + // 移除非数字字符 + const cleanValue = value.replace(/[^\d]/g, ''); + + // 如果有非数字字符被移除 + if (cleanValue !== value) { + // 计算移除了多少个字符 + const diff = originalLength - cleanValue.length; + + // 更新输入框的值 + event.target.value = cleanValue; + + // 调整光标位置(考虑到字符被移除) + setTimeout(() => { + event.target.setSelectionRange(Math.max(0, cursorPosition - diff), Math.max(0, cursorPosition - diff)); + }, 0); + } + + // 确保不超过最大值 + if (cleanValue !== '' && parseInt(cleanValue) > max) { + event.target.value = max.toString(); + } + + // 更新数据模型,如果为空则默认为0 + this.formData[field] = event.target.value === '' ? 0 : parseInt(event.target.value); + }, } }); diff --git a/quark_auto_save.py b/quark_auto_save.py index 9b34d72..4bfb170 100644 --- a/quark_auto_save.py +++ b/quark_auto_save.py @@ -4283,7 +4283,6 @@ def do_save(account, tasklist=[]): ) elif is_new_tree is False: # 明确没有新文件 print(f"任务完成: 没有新的文件需要转存") - print() print() From 982455f1a6c393f995f56ea7baa521826d6aed4b Mon Sep 17 00:00:00 2001 From: x1ao4 Date: Thu, 22 May 2025 01:40:45 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Cookie=20=E4=B8=AD?= =?UTF-8?q?=E4=BB=85=E7=AD=BE=E5=88=B0=E8=B4=A6=E5=8F=B7=E7=9A=84=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/run.py | 5 ++++- app/templates/index.html | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/run.py b/app/run.py index 58c77b0..82e16b0 100644 --- a/app/run.py +++ b/app/run.py @@ -892,10 +892,13 @@ def get_user_info(): "is_active": account.is_active }) else: + # 检查是否有移动端参数 + has_mparam = bool(account.mparam) user_info_list.append({ "index": idx, "nickname": "", - "is_active": False + "is_active": False, + "has_mparam": has_mparam }) return jsonify({"success": True, "data": user_info_list}) diff --git a/app/templates/index.html b/app/templates/index.html index dd1845b..664f447 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -252,7 +252,7 @@
- {{ userInfoList[index].nickname || '未登录' }} + {{ userInfoList[index].nickname || (userInfoList[index].has_mparam ? '仅签到' : '未登录') }}
From cf3f3727f9704919bd8c3a8d9ecdbc6846b40378 Mon Sep 17 00:00:00 2001 From: x1ao4 Date: Thu, 22 May 2025 19:03:09 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E8=BD=AC=E5=AD=98=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/run.py | 57 ++++++++++++ app/static/css/main.css | 91 +++++++++++++++++- app/templates/index.html | 194 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 327 insertions(+), 15 deletions(-) diff --git a/app/run.py b/app/run.py index 82e16b0..596bbd0 100644 --- a/app/run.py +++ b/app/run.py @@ -836,6 +836,63 @@ def get_history_records(): return jsonify({"success": True, "data": result}) +# 删除转存记录 +@app.route("/delete_history_records", methods=["POST"]) +def delete_history_records(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + # 获取要删除的记录ID列表 + record_ids = request.json.get("record_ids", []) + + if not record_ids: + return jsonify({"success": False, "message": "未提供要删除的记录ID"}) + + # 初始化数据库 + db = RecordDB() + + # 删除记录 + deleted_count = 0 + for record_id in record_ids: + deleted_count += db.delete_record(record_id) + + return jsonify({ + "success": True, + "message": f"成功删除 {deleted_count} 条记录", + "deleted_count": deleted_count + }) + + +# 删除单条转存记录 +@app.route("/delete_history_record", methods=["POST"]) +def delete_history_record(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + # 获取要删除的记录ID + record_id = request.json.get("id") + + if not record_id: + return jsonify({"success": False, "message": "未提供要删除的记录ID"}) + + # 初始化数据库 + db = RecordDB() + + # 删除记录 + deleted = db.delete_record(record_id) + + if deleted: + return jsonify({ + "success": True, + "message": "成功删除 1 条记录", + }) + else: + return jsonify({ + "success": False, + "message": "记录删除失败,可能记录不存在", + }) + + # 辅助函数:格式化记录 def format_records(records): for record in records: diff --git a/app/static/css/main.css b/app/static/css/main.css index 685390c..f4a70c1 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -92,6 +92,8 @@ body.login-page { left: 50%; transform: translate(-50%, -50%); z-index: 9999; + width: auto; + max-width: 80%; } .toast-custom { @@ -100,13 +102,15 @@ body.login-page { box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1); border: none; border-radius: 6px; + margin: 0 auto; } .toast-body-custom { text-align: center; - padding: 1rem 0.75rem; + padding: 1rem 1.2rem; color: #fff; font-size: 0.95rem; + white-space: nowrap; } /* --------------- 底部按钮 --------------- */ @@ -3764,3 +3768,88 @@ input.no-spinner { padding-right: 15px !important; } } + +/* --------------- 转存记录相关样式 --------------- */ +.selected-record { + background-color: var(--button-gray-background-color) !important; +} + +/* 删除按钮样式 */ +.delete-record-btn { + color: #dc3545; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 4px; + transition: background-color 0.2s ease; + visibility: hidden; /* 默认隐藏 */ +} + +/* 删除按钮图标大小 */ +.delete-record-btn .bi-trash3 { + font-size: 1rem; +} + +/* 选中行或鼠标悬停行时显示删除按钮 */ +tr.selected-record .delete-record-btn, +.selectable-records tbody tr:hover .delete-record-btn { + visibility: visible; +} + +/* 表头中的删除按钮仅在有选中行时显示 */ +table th .delete-record-btn { + visibility: hidden; +} + +/* 禁止在表格中选择文本,以便更好地支持点击选择 */ +table.selectable-records { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +table.selectable-records tbody tr { + cursor: pointer; +} + +/* 修改表格行悬停样式,使用变量保持一致性 */ +table.selectable-records tbody tr:hover { + background-color: var(--button-gray-background-color); +} + +/* 确保展开按钮在选中状态下仍然可见 */ +tr.selected-record .expand-button { + z-index: 2; +} + +/* 当鼠标悬停在展开按钮或删除按钮上时,不改变按钮的背景色 */ +table.selectable-records .expand-button:hover, +table.selectable-records .delete-record-btn:hover { + background-color: transparent !important; +} + +/* 选中行或鼠标悬停行的大小列样式 */ +tr.selected-record .file-size-cell .file-size-value, +.selectable-records tbody tr:hover .file-size-cell .file-size-value { + display: none; /* 隐藏文件大小信息 */ +} + +tr.selected-record .file-size-cell .delete-record-btn, +.selectable-records tbody tr:hover .file-size-cell .delete-record-btn { + display: flex; + justify-content: flex-start; /* 居左对齐 */ + align-items: center; + width: auto; + height: 100%; + margin-left: 0; /* 确保没有左边距 */ + padding-left: 0; /* 确保没有左内边距 */ +} + +/* 当鼠标悬停在展开按钮或删除按钮上时,不改变按钮的背景色 */ +table.selectable-records .expand-button:hover { + background-color: #fff !important; /* 保持展开按钮原有的白色背景 */ +} diff --git a/app/templates/index.html b/app/templates/index.html index 664f447..fe2363b 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -685,14 +685,14 @@
- +
- + @@ -700,7 +700,9 @@ - + - + @@ -754,7 +761,7 @@
- 显示 {{ history.pagination && history.pagination.total_records > 0 ? ((historyParams.page - 1) * historyParams.page_size + 1) + '-' + Math.min(historyParams.page * historyParams.page_size, history.pagination.total_records) : '0' }} 条,共 {{ history.pagination ? history.pagination.total_records : 0 }} 条记录 + 显示 {{ history.pagination && history.pagination.total_records > 0 ? ((historyParams.page - 1) * historyParams.page_size + 1) + '-' + Math.min(historyParams.page * historyParams.page_size, history.pagination.total_records) : '0' }} 条,共 {{ history.pagination ? history.pagination.total_records : 0 }} 条记录{{ selectedRecords.length > 0 ? ',已选中 ' + selectedRecords.length + ' 条记录' : '' }}
转存日期 任务名称 原文件 转存为 大小 大小 修改日期
暂无记录
{{ record.transfer_time_readable }}
{{ record.task_name }}
-
+
@@ -723,7 +725,7 @@ v-check-overflow="index + '|original_name'"> {{ record.original_name }}
-
+
@@ -737,14 +739,19 @@ v-check-overflow="index + '|renamed_to'"> {{ record.renamed_to }}
-
+
{{ record.renamed_to }}
{{ record.file_size_readable }} + {{ record.file_size_readable }} + + + + {{ record.modify_date_readable }}