diff --git a/app/run.py b/app/run.py index f4b16bd..2921619 100644 --- a/app/run.py +++ b/app/run.py @@ -591,6 +591,22 @@ def get_share_detail(): episode_pattern = regex.get("episode_naming") episode_patterns = regex.get("episode_patterns", []) + # 获取默认的剧集模式 + default_episode_pattern = {"regex": '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?'} + + # 获取配置的剧集模式,确保每个模式都是字典格式 + episode_patterns = [] + raw_patterns = config_data.get("episode_patterns", [default_episode_pattern]) + for p in raw_patterns: + if isinstance(p, dict) and p.get("regex"): + episode_patterns.append(p) + elif isinstance(p, str): + episode_patterns.append({"regex": p}) + + # 如果没有有效的模式,使用默认模式 + if not episode_patterns: + episode_patterns = [default_episode_pattern] + # 添加中文数字匹配模式 chinese_patterns = [ {"regex": r'第([一二三四五六七八九十百千万零两]+)集'}, @@ -600,74 +616,7 @@ def get_share_detail(): {"regex": r'([一二三四五六七八九十百千万零两]+)期'}, {"regex": r'([一二三四五六七八九十百千万零两]+)话'} ] - - # 合并中文模式到episode_patterns - if episode_patterns: - episode_patterns.extend(chinese_patterns) - else: - episode_patterns = chinese_patterns - - # 调用全局的集编号提取函数 - def extract_episode_number_local(filename): - return extract_episode_number(filename, episode_patterns=episode_patterns) - - # 构建剧集命名的正则表达式 (主要用于检测已命名文件) - if episode_pattern == "[]": - # 对于单独的[],使用特殊匹配 - regex_pattern = "^(\\d+)$" # 匹配纯数字文件名 - elif "[]" in episode_pattern: - # 特殊处理E[]、EP[]等常见格式,使用更宽松的匹配方式 - if episode_pattern == "E[]": - # 对于E[]格式,只检查文件名中是否包含形如E01的部分 - regex_pattern = "^E(\\d+)$" # 只匹配纯E+数字的文件名格式 - elif episode_pattern == "EP[]": - # 对于EP[]格式,只检查文件名中是否包含形如EP01的部分 - regex_pattern = "^EP(\\d+)$" # 只匹配纯EP+数字的文件名格式 - else: - # 对于其他带[]的格式,使用常规转义和替换 - regex_pattern = re.escape(episode_pattern).replace('\\[\\]', '(\\d+)') - else: - # 如果输入模式不包含[],则使用简单匹配模式,避免正则表达式错误 - regex_pattern = "^" + re.escape(episode_pattern) + "(\\d+)$" - - # 实现高级排序算法 - def extract_sorting_value(file): - if file["dir"]: # 跳过文件夹 - return (float('inf'), 0, 0, 0) # 返回元组以确保类型一致性 - - filename = file["file_name"] - - # 尝试获取剧集序号 - episode_num = extract_episode_number_local(filename) - if episode_num is not None: - # 返回元组以确保类型一致性 - return (0, episode_num, 0, 0) - - # 如果无法提取剧集号,则使用通用的排序函数 - return sort_file_by_name(file) - - # 过滤出非目录文件,并且排除已经符合命名规则的文件 - files_to_process = [] - for f in share_detail["list"]: - if f["dir"]: - continue # 跳过目录 - - # 检查文件是否已符合命名规则 - if episode_pattern == "[]": - # 对于单独的[],检查文件名是否为纯数字 - file_name_without_ext = os.path.splitext(f["file_name"])[0] - if file_name_without_ext.isdigit(): - # 增加判断:如果是日期格式的纯数字,不视为已命名 - if not is_date_format(file_name_without_ext): - continue # 跳过已符合命名规则的文件 - elif re.match(regex_pattern, f["file_name"]): - continue # 跳过已符合命名规则的文件 - - # 添加到待处理文件列表 - files_to_process.append(f) - - # 根据提取的排序值进行排序 - sorted_files = sorted(files_to_process, key=extract_sorting_value) + episode_patterns.extend(chinese_patterns) # 应用过滤词过滤 filterwords = regex.get("filterwords", "") @@ -675,32 +624,25 @@ def get_share_detail(): # 同时支持中英文逗号分隔 filterwords = filterwords.replace(",", ",") filterwords_list = [word.strip() for word in filterwords.split(',')] - for item in sorted_files: - # 被过滤的文件不会有file_name_re,与不匹配正则的文件显示一致 + for item in share_detail["list"]: + # 被过滤的文件显示一个 × if any(word in item['file_name'] for word in filterwords_list): item["filtered"] = True + item["file_name_re"] = "×" - # 为每个文件生成新文件名并存储剧集编号用于排序 - for file in sorted_files: - if not file.get("filtered"): - # 获取文件扩展名 - file_ext = os.path.splitext(file["file_name"])[1] - # 尝试提取剧集号 - episode_num = extract_episode_number_local(file["file_name"]) - if episode_num is not None: - # 生成预览文件名 - if episode_pattern == "[]": - # 对于单独的[],直接使用数字序号作为文件名 - file["file_name_re"] = f"{episode_num:02d}{file_ext}" - else: - file["file_name_re"] = episode_pattern.replace("[]", f"{episode_num:02d}") + file_ext - # 存储原始的剧集编号,用于数值排序 - file["episode_number"] = episode_num - else: - # 无法提取剧集号,标记为无法处理 - file["file_name_re"] = "❌ 无法识别剧集号" - file["episode_number"] = 9999999 # 给一个很大的值,确保排在最后 + # 处理未被过滤的文件 + for file in share_detail["list"]: + if not file["dir"] and not file.get("filtered"): # 只处理未被过滤的非目录文件 + extension = os.path.splitext(file["file_name"])[1] + # 从文件名中提取集号 + episode_num = extract_episode_number(file["file_name"], episode_patterns=episode_patterns) + if episode_num is not None: + file["file_name_re"] = episode_pattern.replace("[]", f"{episode_num:02d}") + extension + else: + # 没有提取到集号,显示无法识别的提示 + file["file_name_re"] = "× 无法识别剧集编号" + return share_detail else: # 普通正则命名预览 @@ -990,7 +932,7 @@ def get_history_records(): # 如果请求所有任务名称,单独查询并返回 if get_all_task_names: cursor = db.conn.cursor() - cursor.execute("SELECT DISTINCT task_name FROM transfer_records ORDER BY task_name") + cursor.execute("SELECT DISTINCT task_name FROM transfer_records WHERE task_name NOT IN ('rename', 'undo_rename') ORDER BY task_name") all_task_names = [row[0] for row in cursor.fetchall()] # 如果同时请求分页数据,继续常规查询 @@ -1001,7 +943,8 @@ def get_history_records(): sort_by=sort_by, order=order, task_name_filter=task_name_filter, - keyword_filter=keyword_filter + keyword_filter=keyword_filter, + exclude_task_names=["rename", "undo_rename"] ) # 添加所有任务名称到结果中 result["all_task_names"] = all_task_names @@ -1021,7 +964,8 @@ def get_history_records(): sort_by=sort_by, order=order, task_name_filter=task_name_filter, - keyword_filter=keyword_filter + keyword_filter=keyword_filter, + exclude_task_names=["rename", "undo_rename"] ) # 处理记录格式化 @@ -1233,7 +1177,371 @@ def reset_folder(): return jsonify({"success": False, "message": f"重置文件夹时出错: {str(e)}"}) +# 获取文件列表 +@app.route("/file_list") +def get_file_list(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + # 获取请求参数 + folder_id = request.args.get("folder_id", "root") + sort_by = request.args.get("sort_by", "file_name") + order = request.args.get("order", "asc") + page = int(request.args.get("page", 1)) + page_size = int(request.args.get("page_size", 15)) + + try: + # 初始化夸克网盘客户端 + account = Quark(config_data["cookie"][0], 0) + + # 获取文件列表 + if folder_id == "root": + folder_id = "0" # 根目录的ID为0 + paths = [] # 根目录没有路径 + else: + # 获取当前文件夹的路径 + paths = account.get_paths(folder_id) + + # 获取文件列表 + files = account.ls_dir(folder_id) + if isinstance(files, dict) and files.get("error"): + return jsonify({"success": False, "message": f"获取文件列表失败: {files.get('error', '未知错误')}"}) + + # 计算总数 + total = len(files) + + # 排序 + if sort_by == "file_name": + files.sort(key=lambda x: x["file_name"].lower()) + elif sort_by == "file_size": + files.sort(key=lambda x: x["size"] if not x["dir"] else 0) + else: # updated_at + files.sort(key=lambda x: x["updated_at"]) + + if order == "desc": + files.reverse() + + # 根据排序字段决定是否将目录放在前面 + if sort_by == "updated_at": + # 修改日期排序时严格按照日期排序,不区分文件夹和文件 + sorted_files = files + else: + # 其他排序时目录始终在前面 + directories = [f for f in files if f["dir"]] + normal_files = [f for f in files if not f["dir"]] + sorted_files = directories + normal_files + + # 分页 + start_idx = (page - 1) * page_size + end_idx = min(start_idx + page_size, total) + paginated_files = sorted_files[start_idx:end_idx] + + return jsonify({ + "success": True, + "data": { + "list": paginated_files, + "total": total, + "paths": paths + } + }) + + except Exception as e: + logging.error(f">>> 获取文件列表时出错: {str(e)}") + return jsonify({"success": False, "message": f"获取文件列表时出错: {str(e)}"}) + + +# 预览重命名 +@app.route("/preview_rename") +def preview_rename(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + # 获取请求参数 + folder_id = request.args.get("folder_id", "root") + pattern = request.args.get("pattern", "") + replace = request.args.get("replace", "") + naming_mode = request.args.get("naming_mode", "regex") # regex, sequence, episode + include_folders = request.args.get("include_folders", "false") == "true" + filterwords = request.args.get("filterwords", "") + + if not pattern: + pattern = ".*" + + try: + # 初始化夸克网盘客户端 + account = Quark(config_data["cookie"][0], 0) + + # 获取文件列表 + if folder_id == "root": + folder_id = "0" # 根目录的ID为0 + + files = account.ls_dir(folder_id) + if isinstance(files, dict) and files.get("error"): + return jsonify({"success": False, "message": f"获取文件列表失败: {files.get('error', '未知错误')}"}) + + # 过滤要排除的文件 + # 替换中文逗号为英文逗号 + filterwords = filterwords.replace(",", ",") + filter_list = [keyword.strip() for keyword in filterwords.split(",") if keyword.strip()] + filtered_files = [] + for file in files: + # 如果不包含文件夹且当前项是文件夹,跳过 + if not include_folders and file["dir"]: + continue + + # 检查是否包含过滤关键词 + should_filter = False + for keyword in filter_list: + if keyword and keyword in file["file_name"]: + should_filter = True + break + + if not should_filter: + filtered_files.append(file) + + # 按不同命名模式处理 + preview_results = [] + + if naming_mode == "sequence": + # 顺序命名模式 + # 排序文件(按文件名或修改时间) + filtered_files.sort(key=lambda x: sort_file_by_name(x["file_name"])) + + sequence = 1 + for file in filtered_files: + extension = os.path.splitext(file["file_name"])[1] if not file["dir"] else "" + new_name = pattern.replace("{}", f"{sequence:02d}") + extension + preview_results.append({ + "original_name": file["file_name"], + "new_name": new_name, + "file_id": file["fid"] + }) + sequence += 1 + + elif naming_mode == "episode": + # 剧集命名模式 + # 获取默认的剧集模式 + default_episode_pattern = {"regex": '第(\\d+)集|第(\\d+)期|第(\\d+)话|(\\d+)集|(\\d+)期|(\\d+)话|[Ee][Pp]?(\\d+)|(\\d+)[-_\\s]*4[Kk]|\\[(\\d+)\\]|【(\\d+)】|_?(\\d+)_?'} + + # 获取配置的剧集模式,确保每个模式都是字典格式 + episode_patterns = [] + raw_patterns = config_data.get("episode_patterns", [default_episode_pattern]) + for p in raw_patterns: + if isinstance(p, dict) and p.get("regex"): + episode_patterns.append(p) + elif isinstance(p, str): + episode_patterns.append({"regex": p}) + + # 如果没有有效的模式,使用默认模式 + if not episode_patterns: + episode_patterns = [default_episode_pattern] + + # 添加中文数字匹配模式 + chinese_patterns = [ + {"regex": r'第([一二三四五六七八九十百千万零两]+)集'}, + {"regex": r'第([一二三四五六七八九十百千万零两]+)期'}, + {"regex": r'第([一二三四五六七八九十百千万零两]+)话'}, + {"regex": r'([一二三四五六七八九十百千万零两]+)集'}, + {"regex": r'([一二三四五六七八九十百千万零两]+)期'}, + {"regex": r'([一二三四五六七八九十百千万零两]+)话'} + ] + episode_patterns.extend(chinese_patterns) + + # 处理每个文件 + for file in filtered_files: + extension = os.path.splitext(file["file_name"])[1] if not file["dir"] else "" + # 从文件名中提取集号 + episode_num = extract_episode_number(file["file_name"], episode_patterns=episode_patterns) + + if episode_num is not None: + new_name = pattern.replace("[]", f"{episode_num:02d}") + extension + preview_results.append({ + "original_name": file["file_name"], + "new_name": new_name, + "file_id": file["fid"] + }) + else: + # 没有提取到集号,显示无法识别的提示 + preview_results.append({ + "original_name": file["file_name"], + "new_name": "× 无法识别剧集编号", + "file_id": file["fid"] + }) + + else: + # 正则命名模式 + for file in filtered_files: + try: + # 应用正则表达式 + if replace: + new_name = re.sub(pattern, replace, file["file_name"]) + else: + # 如果没有提供替换表达式,则检查是否匹配 + if re.search(pattern, file["file_name"]): + new_name = file["file_name"] # 匹配但不替换 + else: + new_name = "" # 表示不匹配 + + preview_results.append({ + "original_name": file["file_name"], + "new_name": new_name if new_name != file["file_name"] else "", # 如果没有改变,返回空表示不重命名 + "file_id": file["fid"] + }) + except Exception as e: + # 正则表达式错误 + preview_results.append({ + "original_name": file["file_name"], + "new_name": "", # 表示无法重命名 + "file_id": file["fid"], + "error": str(e) + }) + + return jsonify({ + "success": True, + "data": preview_results + }) + + except Exception as e: + logging.error(f">>> 预览重命名时出错: {str(e)}") + return jsonify({"success": False, "message": f"预览重命名时出错: {str(e)}"}) + + +# 批量重命名 +@app.route("/batch_rename", methods=["POST"]) +def batch_rename(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + # 获取请求参数 + data = request.json + files = data.get("files", []) + + if not files: + return jsonify({"success": False, "message": "没有文件需要重命名"}) + + try: + # 初始化夸克网盘客户端 + account = Quark(config_data["cookie"][0], 0) + + # 批量重命名 + success_count = 0 + failed_files = [] + save_path = data.get("save_path", "") + batch_time = int(time.time() * 1000) # 批次时间戳,确保同一批transfer_time一致 + for file_item in files: + file_id = file_item.get("file_id") + new_name = file_item.get("new_name") + original_name = file_item.get("old_name") or "" + if file_id and new_name: + try: + result = account.rename(file_id, new_name) + if result.get("code") == 0: + success_count += 1 + # 记录重命名 + record_db.add_record( + task_name="rename", + original_name=original_name, + renamed_to=new_name, + file_size=0, + modify_date=int(time.time()), + file_id=file_id, + file_type="file", + save_path=save_path, + transfer_time=batch_time + ) + else: + failed_files.append({ + "file_id": file_id, + "new_name": new_name, + "error": result.get("message", "未知错误") + }) + except Exception as e: + failed_files.append({ + "file_id": file_id, + "new_name": new_name, + "error": str(e) + }) + + return jsonify({ + "success": True, + "message": f"成功重命名 {success_count} 个文件,失败 {len(failed_files)} 个", + "success_count": success_count, + "failed_files": failed_files + }) + + except Exception as e: + logging.error(f">>> 批量重命名时出错: {str(e)}") + return jsonify({"success": False, "message": f"批量重命名时出错: {str(e)}"}) + + +# 撤销重命名接口 +@app.route("/undo_rename", methods=["POST"]) +def undo_rename(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + data = request.json + save_path = data.get("save_path", "") + if not save_path: + return jsonify({"success": False, "message": "缺少目录参数"}) + try: + account = Quark(config_data["cookie"][0], 0) + # 查询该目录下最近一次重命名(按transfer_time分组,task_name=rename) + records = record_db.get_records_by_save_path(save_path) + rename_records = [r for r in records if r["task_name"] == "rename"] + if not rename_records: + return jsonify({"success": False, "message": "没有可撤销的重命名记录"}) + # 找到最近一批(transfer_time最大值) + latest_time = max(r["transfer_time"] for r in rename_records) + latest_batch = [r for r in rename_records if r["transfer_time"] == latest_time] + success_count = 0 + failed_files = [] + deleted_ids = [] + for rec in latest_batch: + file_id = rec["file_id"] + old_name = rec["original_name"] + try: + result = account.rename(file_id, old_name) + if result.get("code") == 0: + success_count += 1 + deleted_ids.append(rec["id"]) + else: + failed_files.append({ + "file_id": file_id, + "old_name": old_name, + "error": result.get("message", "未知错误") + }) + except Exception as e: + failed_files.append({ + "file_id": file_id, + "old_name": old_name, + "error": str(e) + }) + # 撤销成功的,直接删除对应rename记录 + for rid in deleted_ids: + record_db.delete_record(rid) + return jsonify({ + "success": True, + "message": f"成功撤销 {success_count} 个文件重命名,失败 {len(failed_files)} 个", + "success_count": success_count, + "failed_files": failed_files + }) + except Exception as e: + logging.error(f">>> 撤销重命名时出错: {str(e)}") + return jsonify({"success": False, "message": f"撤销重命名时出错: {str(e)}"}) + + +@app.route("/api/has_rename_record") +def has_rename_record(): + save_path = request.args.get("save_path", "") + db = RecordDB() + records = db.get_records_by_save_path(save_path) + has_rename = any(r["task_name"] == "rename" for r in records) + return jsonify({"has_rename": has_rename}) + + if __name__ == "__main__": init() reload_tasks() + # 初始化全局db对象,确保所有接口可用 + record_db = RecordDB() app.run(debug=DEBUG, host="0.0.0.0", port=PORT) diff --git a/app/sdk/db.py b/app/sdk/db.py index 316de4e..3321f83 100644 --- a/app/sdk/db.py +++ b/app/sdk/db.py @@ -15,7 +15,7 @@ class RecordDB: os.makedirs(os.path.dirname(self.db_path), exist_ok=True) # 创建数据库连接 - self.conn = sqlite3.connect(self.db_path) + self.conn = sqlite3.connect(self.db_path, check_same_thread=False) cursor = self.conn.cursor() # 创建表,如果不存在 @@ -49,13 +49,14 @@ 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="", save_path=""): + duration="", resolution="", file_id="", file_type="", save_path="", transfer_time=None): """添加一条转存记录""" cursor = self.conn.cursor() + now_ms = int(time.time() * 1000) if transfer_time is None else transfer_time cursor.execute( "INSERT INTO transfer_records (transfer_time, task_name, original_name, renamed_to, file_size, " "duration, resolution, modify_date, file_id, file_type, save_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - (int(time.time()), task_name, original_name, renamed_to, file_size, + (now_ms, task_name, original_name, renamed_to, file_size, duration, resolution, modify_date, file_id, file_type, save_path) ) self.conn.commit() @@ -123,7 +124,7 @@ class RecordDB: return 0 def get_records(self, page=1, page_size=20, sort_by="transfer_time", order="desc", - task_name_filter="", keyword_filter=""): + task_name_filter="", keyword_filter="", exclude_task_names=None): """获取转存记录列表,支持分页、排序和筛选 Args: @@ -133,6 +134,7 @@ class RecordDB: order: 排序方向(asc/desc) task_name_filter: 任务名称筛选条件(精确匹配) keyword_filter: 关键字筛选条件(模糊匹配任务名) + exclude_task_names: 需要排除的任务名称列表 """ cursor = self.conn.cursor() offset = (page - 1) * page_size @@ -159,6 +161,10 @@ class RecordDB: params.append(f"%{keyword_filter}%") params.append(f"%{keyword_filter}%") + if exclude_task_names: + where_clauses.append("task_name NOT IN ({})".format(",".join(["?" for _ in exclude_task_names]))) + params.extend(exclude_task_names) + where_clause = " AND ".join(where_clauses) where_sql = f"WHERE {where_clause}" if where_clause else "" diff --git a/app/static/css/main.css b/app/static/css/main.css index 8311f99..02f5743 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -94,6 +94,7 @@ body.login-page { z-index: 9999; width: auto; max-width: 80%; + pointer-events: none; } .toast-custom { @@ -1131,8 +1132,8 @@ textarea.form-control { .expand-button { position: absolute; right: 5px; - top: 10.5px; /* 恢复为固定位置 */ - transform: none; /* 移除垂直居中的转换 */ + top: 11px; /* 固定位置,不再使用百分比定位 */ + transform: none; /* 移除垂直居中转换 */ cursor: pointer; opacity: 0; transition: opacity 0.2s; @@ -1152,12 +1153,11 @@ textarea.form-control { .expanded-text { white-space: normal; - padding: 0 0; /* 展开时的上下内边距 */ word-break: break-all; display: block; /* 确保展开的内容是块级元素 */ - line-height: 1.5; /* 设置合理的行高 */ - margin-top: 4px; /* 与上方内容保持一定间距 */ - margin-bottom: 4px; /* 添加底部边距,与表格底部分割线保持一致的距离 */ + line-height: 24px !important; /* 与表头保持一致的行高 */ + margin-top: 0; /* 移除顶部边距 */ + margin-bottom: 0; /* 移除底部边距 */ position: relative; /* 确保定位准确 */ padding-right: 25px; /* 为展开按钮预留空间 */ } @@ -1174,25 +1174,18 @@ textarea.form-control { overflow-x: auto; } -.table th { - white-space: nowrap; - background-color: #f8f9fa; - position: sticky; - top: 0; - z-index: 10; -} - /* 表头样式调整 */ -.table thead th { +.table thead th, +table.table thead th { vertical-align: middle; - border-bottom: 1px solid var(--border-color); /* 修改底部边框为1px */ - border-top: 1px solid var(--border-color); /* 添加上边框线 */ - padding-top: 8.5px; /* 增加上内边距 */ - padding-bottom: 8.5px; /* 增加下内边距 */ - padding-left: 9px !important; /* 表头左内边距,与按钮一致 */ - padding-right: 9px !important; /* 表头右内边距,与按钮一致 */ - background-color: var(--button-gray-background-color); /* 表头背景色 */ - color: var(--dark-text-color); /* 表头文字颜色 */ + border-bottom: 1px solid var(--border-color) !important; /* 修改底部边框为1px */ + border-top: 1px solid var(--border-color) !important; /* 添加上边框线 */ + padding: 8px 9px !important; /* 统一表头内边距 */ + background-color: var(--button-gray-background-color) !important; /* 表头背景色 */ + color: var(--dark-text-color) !important; /* 表头文字颜色 */ + height: 40px !important; /* 确保表头高度为40px */ + line-height: 24px !important; /* 设置行高以确保文字垂直居中 */ + box-sizing: border-box !important; /* 确保边框包含在总高度内 */ } /* 表头悬停样式 */ @@ -1202,18 +1195,17 @@ textarea.form-control { /* 表格单元格样式 */ .table td { - vertical-align: top; /* 改为顶部对齐,避免内容居中问题 */ - height: auto; /* 根据内容自动调整高度 */ + vertical-align: middle !important; /* 统一使用垂直居中对齐 */ + height: 40px !important; /* 与表头保持一致的高度 */ max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - padding-top: 8px; /* 单元格上内边距 */ - padding-bottom: 8px; /* 单元格下内边距 */ - padding-left: 9px !important; /* 单元格左内边距,与按钮一致 */ - padding-right: 9px !important; /* 单元格右内边距,与按钮一致 */ + padding: 8px 9px !important; /* 统一单元格内边距 */ border-bottom: 1px solid var(--border-color); /* 单元格分割线颜色 */ - transform: translateY(0.5px); /* 文本下移0.5px,不影响元素实际高度 */ + border-top: none !important; /* 确保没有上边框 */ + box-sizing: border-box !important; /* 确保边框包含在总高度内 */ + line-height: 24px !important; /* 与表头保持一致的行高 */ } /* 表格行悬停样式 */ @@ -1227,23 +1219,22 @@ textarea.form-control { word-break: break-word; } -/* 新增:设置表格单元格中的position-relative样式,以便正确定位内容 */ +/* 设置表格单元格中的position-relative样式,以便正确定位内容 */ .table td.position-relative { position: relative; /* 确保相对定位生效 */ - padding-top: 4px; /* 减少顶部内边距 */ - padding-bottom: 4px; /* 减少底部内边距 */ + padding: 8px 9px !important; /* 保持与其他单元格一致的内边距 */ + vertical-align: top !important; /* 确保内容顶部对齐 */ } -/* 新增:调整text-truncate在表格单元格中的布局 */ +/* 调整text-truncate在表格单元格中的布局 */ .table td .text-truncate { display: block; /* 使其成为块级元素 */ max-width: 100%; /* 确保不超出单元格宽度 */ padding-right: 25px; /* 为展开按钮留出空间 */ - padding-top: 4px; /* 与单元格顶部保持一定距离 */ - padding-bottom: 4px; /* 与单元格底部保持一定距离 */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + line-height: 24px !important; /* 与表头保持一致的行高 */ } /* --------------- 模态框样式 --------------- */ @@ -1606,7 +1597,7 @@ button.close:focus, top: 0; z-index: 5; vertical-align: middle; /* 添加:垂直居中对齐 */ - height: auto; /* 添加:自动高度,确保与内容一致 */ + height: 40px !important; /* 添加:自动高度,确保与内容一致 */ } /* 模态框表格列宽设置 - 基于内容类型 */ @@ -1689,15 +1680,6 @@ button.close:focus, cursor: pointer; } -/* 弹窗内文件夹和文件图标样式 */ -#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; @@ -2150,6 +2132,10 @@ div.jsoneditor-tree button.jsoneditor-button:focus { font-size: 1rem; } +.sidebar .nav-link .bi-archive { + font-size: 1rem; +} + .sidebar .nav-link .bi-power { font-size: 1.27rem; } @@ -3345,7 +3331,7 @@ div[id^="collapse_"][id*="plugin"] .input-group { #fileSelectModal .badge-info { background-color: var(--focus-border-color) !important; color: white !important; - font-size: 0.84rem !important; + font-size: 0.83rem !important; padding: 6px 3px !important; font-weight: normal !important; border-radius: 4px !important; @@ -3386,7 +3372,7 @@ div[id^="collapse_"][id*="plugin"] .input-group { /* 多行表达式间距 - 针对预览区域显示的多行表达式 */ #fileSelectModal .mb-3[v-if="fileSelect.previewRegex"] > div > span.badge-info + span.badge-info { - margin-top: 1px !important; /* 减少多行表达式之间的间距 */ + margin-top: 5px !important; /* 减少多行表达式之间的间距 */ display: inline-block; } @@ -3602,10 +3588,22 @@ input::-moz-list-button { padding-right: 16px !important; /* 强制设置右边距为16px */ box-sizing: border-box; } - + #fileSelectModal[data-modal-type="preview"] .table { width: 460px; } + + /* 针对文件整理页面命名预览模式 - 2列表格 */ + #fileSelectModal[data-modal-type="preview-filemanager"] .breadcrumb { + min-width: 460px; /* 2列表格总宽度: 230px + 230px */ + margin-right: 0; + padding-right: 16px !important; /* 强制设置右边距为16px */ + box-sizing: border-box; + } + + #fileSelectModal[data-modal-type="preview-filemanager"] .table { + width: 460px; + } /* 确保面包屑导航内容不被截断 */ #fileSelectModal .breadcrumb-item { @@ -3688,7 +3686,7 @@ input::-moz-list-button { #fileSelectModal .expand-button { position: absolute; right: 5px; - top: 7.5px; /* 固定高度,不再使用百分比定位 */ + top: 7.5px; /* 固定位置 */ transform: none; /* 移除垂直居中转换 */ cursor: pointer; opacity: 0; @@ -3717,6 +3715,7 @@ input::-moz-list-button { /* 确保模态框中的表格单元格可以正确显示展开按钮 */ #fileSelectModal .table td.position-relative { position: relative; + vertical-align: top !important; /* 确保内容顶部对齐 */ } /* 当表格行被点击时,防止展开按钮的点击事件冒泡 */ @@ -3728,6 +3727,8 @@ input::-moz-list-button { #fileSelectModal .table td [style*="white-space: normal"] { display: block; width: 100%; + margin-top: 0; /* 移除顶部边距 */ + margin-bottom: 7px; } /* 确保表格行内容保持顶部对齐 */ @@ -3799,24 +3800,8 @@ input.no-spinner { /* 文件大小值的文本位置调整 */ .file-size-cell .file-size-value { - transform: translateY(-1px); /* 文本下移 */ display: inline-block; /* 确保transform生效 */ -} - -/* 转存记录删除按钮样式 */ -.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; /* 默认隐藏 */ - position: relative; /* 添加相对定位 */ - top: -0.5px; /* 上移 */ + transform: translateY(0px); } /* 删除按钮图标大小 */ @@ -3845,6 +3830,7 @@ table.selectable-records { table.selectable-records tbody tr { cursor: pointer; + transition: none; } /* 修改表格行悬停样式,使用变量保持一致性 */ @@ -3878,6 +3864,10 @@ tr.selected-record .file-size-cell .delete-record-btn, height: 100%; margin-left: 0; /* 确保没有左边距 */ padding-left: 0; /* 确保没有左内边距 */ + left: 9px; /* 与大小值保持相同的左边距 */ + position: absolute; /* 使用绝对定位 */ + top: 8px; /* 固定位置 */ + transform: none; /* 移除垂直居中转换 */ } /* 当鼠标悬停在展开按钮或删除按钮上时,不改变按钮的背景色 */ @@ -3898,24 +3888,37 @@ table.selectable-records .expand-button:hover { /* 特别调整红色"×"符号的位置 */ #fileSelectModal .table td.col-rename.text-danger div:not(.expand-button) { position: relative; - top: -1px; /* 将"×"标记上移1px */ + top: 2px !important; /* 将"×"标记上移1px */ +} + +#fileSelectModal[data-modal-type="preview"] .table td.col-rename > * { + position: relative; + top: 3px !important; /* 原来是3.5px,上移0.5px */ +} + +/* 文件整理页面命名预览模式下的重命名列通用样式 */ +#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename > * { + position: relative; + top: 3px !important; /* 与任务配置页面保持一致 */ } /* 文件名列已经通过图标微调过,保持原样或细微调整 */ #fileSelectModal .bi-folder-fill { color: #ffc107; - font-size: 0.9rem; - margin-right: 5px; + font-size: 0.95rem; + margin-right: 4.5px; position: relative; - top: 1px; /* 负值向上移动,正值向下移动 */ + top: 0.5px !important; /* 负值向上移动,正值向下移动 */ + left: -0.55px; } #fileSelectModal .bi-file-earmark { color: var(--dark-text-color); - font-size: 0.9rem; - margin-right: 5px; + font-size: 0.95rem; + margin-right: 4.5px; position: relative; - top: 0px; /* 负值向上移动,正值向下移动 */ + top: 0.5px !important; /* 负值向上移动,正值向下移动 */ + left: -0.4px; } /* 添加选中文件的样式 */ @@ -4129,3 +4132,943 @@ select.task-filter-select, .task-name-hover:hover { color: var(--focus-border-color) !important; } + +/* 文件整理页面样式 */ +.file-manager-card { + display: none; /* 隐藏卡片 */ +} + +.file-manager-card .card-body { + padding: 0; /* 移除内边距 */ +} + +.file-manager-input { + flex: 1; +} + +/* 文件整理页面面包屑导航样式 */ +.file-manager-breadcrumb { + margin-top: 20px; +} + +.file-manager-breadcrumb .breadcrumb-item { + color: var(--dark-text-color); + padding: 0; + position: relative; /* 添加相对定位 */ +} + +.file-manager-breadcrumb .breadcrumb-item a { + color: var(--dark-text-color); + text-decoration: none; +} + +.file-manager-breadcrumb .breadcrumb-item a:hover { + color: var(--focus-border-color); +} + +/* 隐藏Bootstrap默认的面包屑分隔符 */ +.file-manager-breadcrumb .breadcrumb-item + .breadcrumb-item::before { + color: var(--dark-text-color); + content: "/"; /* 使用斜杠作为分隔符 */ + position: relative; + display: inline-block; + vertical-align: middle; /* 垂直居中 */ + padding: 0 8px; /* 在斜杠两侧添加间距 */ + top: -1px; /* 将分隔符上移1px */ +} + +.file-actions { + white-space: nowrap; +} + +.selectable-files tr.selected-file { + background-color: rgba(var(--primary-rgb), 0.1); +} + +.batch-rename-btn { + margin-left: 8px; +} + +/* 文件表格中的展开按钮 */ +.expand-button { + position: absolute; + right: 5px; + top: 11px; /* 固定位置,与其他表格保持一致 */ + transform: none; /* 移除垂直居中转换 */ + cursor: pointer; + opacity: 0; + transition: opacity 0.2s; + background: #fff; + border-radius: 50%; + width: 18px; + height: 18px; + text-align: center; + line-height: 18px; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); + z-index: 2; +} + +.expand-button:hover { + opacity: 1; +} + +/* 批量重命名模态框样式 */ +#batchRenameModal .modal-dialog { + max-width: 80%; +} + +#batchRenameModal .table { + margin-bottom: 0; +} + +#batchRenameModal .badge { + font-size: 90%; + font-weight: 500; + word-break: break-all; +} + +#batchRenameModal .alert { + margin-bottom: 15px; +} + +#batchRenameModal .text-danger { + background-color: rgba(var(--danger-rgb), 0.05); +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .file-manager-rule-bar { + flex-direction: column; + } + + .file-manager-rule-bar .input-group { + margin-bottom: 10px; + } + + .batch-rename-btn { + margin-left: 0; + margin-top: 10px; + } + + #batchRenameModal .modal-dialog { + max-width: 95%; + } +} + +/* 文件大小单元格样式 */ +.file-size-cell { + position: relative; +} + +.file-size-cell .file-size-value { + display: inline-block; +} + +.delete-record-btn .bi-trash3 { + font-size: 1rem; +} + +/* 当行被选中或鼠标悬停时显示删除按钮 */ +tr:hover .delete-record-btn, +tr.selected-file .delete-record-btn { + display: inline-block; +} + +/* 表格标题行不显示删除按钮 */ +table th .delete-record-btn { + display: none !important; +} + +/* 可选择的表格样式 */ +table.selectable-files { + border-collapse: separate; + border-spacing: 0; +} + +table.selectable-files tbody tr { + transition: none; +} + +table.selectable-files tbody tr:hover { + background-color: rgba(var(--primary-rgb), 0.05); +} + +/* 选中行时展开按钮的样式 */ +tr.selected-file .expand-button { + background-color: var(--button-gray-background-color); +} + +/* 展开按钮悬停样式 */ +table.selectable-files .expand-button:hover, +tr.selected-file .expand-button:hover { + background-color: #fff; + opacity: 1; +} + +/* 选中行时文件大小和删除按钮的样式 */ +tr.selected-file .file-size-cell .file-size-value, +tr.selected-file .file-size-cell .delete-record-btn { + position: relative; + z-index: 1; +} + +/* 文件整理页面表格样式 */ +.selectable-files { + border-collapse: collapse !important; +} + +.selectable-files td, +.selectable-files th { + border: none !important; + border-top: 1px solid var(--border-color) !important; /* 添加顶部边框线 */ + border-bottom: 1px solid var(--border-color) !important; /* 添加底部分割线 */ +} + +.selectable-files tbody tr { + cursor: pointer; + transition: none; +} + +.selectable-files tbody tr:hover { + background-color: var(--button-gray-background-color) !important; +} + +/* 删除按钮样式调整 */ +.selectable-files .delete-record-btn { + color: #dc3545; + cursor: pointer; + display: none; + align-items: center; + justify-content: flex-start; /* 居左对齐 */ + width: 24px; + height: 24px; + border-radius: 4px; + transition: background-color 0.2s ease; + position: absolute; + right: auto; /* 移除右对齐 */ + left: 9px; /* 与大小值保持相同的左边距 */ + top: 50%; + transform: translateY(-50%); +} + +.selectable-files tr:hover .delete-record-btn, +.selectable-files tr.selected-file .delete-record-btn { + display: inline-flex; +} + +.selectable-files tr:hover .file-size-value, +.selectable-files tr.selected-file .file-size-value { + visibility: hidden; +} + +/* 确保选中行的样式正确 */ +.selectable-files tr.selected-file { + background-color: var(--button-gray-background-color) !important; +} + +/* 使面包屑导航样式与表头一致 */ +.file-manager-breadcrumb .breadcrumb { + background-color: var(--button-gray-background-color) !important; + border-radius: 0px; + font-size: 0.95rem; + padding: 0 9px !important; + margin-bottom: 8px; + border-top: 1px solid var(--border-color) !important; + border-bottom: 1px solid var(--border-color) !important; + display: flex; + align-items: center; + height: 42px !important; /* 确保面包屑导航内容区域高度为42px(包含上下边框) */ + line-height: 23px !important; /* 设置行高以确保文字垂直居中 */ + box-sizing: border-box !important; /* 确保边框包含在总高度内 */ +} + +.file-manager-breadcrumb .breadcrumb-item { + color: var(--dark-text-color); + padding: 0; + position: relative; /* 添加相对定位 */ +} + +.file-manager-breadcrumb .breadcrumb-item a { + color: var(--dark-text-color); + text-decoration: none; +} + +.file-manager-breadcrumb .breadcrumb-item a:hover { + color: var(--focus-border-color); +} + +/* 让"全部文件"链接悬停时文字变为蓝色 */ +.file-manager-breadcrumb .breadcrumb-item.cursor-pointer:hover { + background-color: transparent; /* 移除背景色 */ + color: var(--focus-border-color); +} + +.file-manager-breadcrumb .breadcrumb-item.cursor-pointer:hover { + color: var(--focus-border-color); +} + +/* 文件整理页面样式 */ +.file-manager-input { + flex: 1; +} + +/* 文件整理页面面包屑导航样式 */ +.file-manager-breadcrumb { + margin-top: 20px; +} + +/* 文件整理规则栏样式 */ +.file-manager-rule-bar { + width: 100%; +} + +/* 禁止在表格中选择文本,以便更好地支持点击选择 */ +table.selectable-files { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +/* 确保展开按钮在选中状态下仍然可见 */ +tr.selected-file .expand-button { + z-index: 2; +} + +/* 当鼠标悬停在展开按钮或删除按钮上时,不改变按钮的背景色 */ +table.selectable-files .expand-button:hover, +table.selectable-files .delete-record-btn:hover { + background-color: transparent !important; +} + +/* 选中行或鼠标悬停行的大小列样式 */ +tr.selected-file .file-size-cell .file-size-value, +.selectable-files tbody tr:hover .file-size-cell .file-size-value { + display: none; /* 隐藏文件大小信息 */ +} + +tr.selected-file .file-size-cell .delete-record-btn, +.selectable-files 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; /* 确保没有左内边距 */ + left: 9px; /* 与大小值保持相同的左边距 */ + position: absolute; /* 使用绝对定位 */ + top: 50%; /* 垂直居中 */ + transform: translateY(-50%); /* 确保垂直居中 */ +} + +/* 当鼠标悬停在展开按钮或删除按钮上时,不改变按钮的背景色 */ +table.selectable-files .expand-button:hover { + background-color: #fff !important; /* 保持展开按钮原有的白色背景 */ +} + +/* 文件整理页面表头样式,与转存记录页面保持一致 */ +.selectable-files th, +table.selectable-files th { + vertical-align: middle; + border-bottom: 1px solid var(--border-color) !important; /* 修改底部边框为1px */ + border-top: 1px solid var(--border-color) !important; /* 添加上边框线 */ + padding: 8px 9px !important; /* 统一表头内边距 */ + background-color: var(--button-gray-background-color) !important; /* 表头背景色 */ + color: var(--dark-text-color) !important; /* 表头文字颜色 */ + height: 40px !important; /* 确保表头高度为40px */ + line-height: 24px !important; /* 设置行高以确保文字垂直居中 */ + box-sizing: border-box !important; /* 确保边框包含在总高度内 */ +} + +/* 文件整理页面表格单元格样式,与转存记录页面保持一致 */ +.selectable-files td, +table.selectable-files td { + vertical-align: middle !important; /* 统一使用垂直居中对齐 */ + height: 40px !important; /* 与表头保持一致的高度 */ + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 8px 9px !important; /* 统一单元格内边距 */ + border-bottom: 1px solid var(--border-color); /* 单元格分割线颜色 */ + border-top: none !important; /* 确保没有上边框 */ + box-sizing: border-box !important; /* 确保边框计入总高度 */ + line-height: 24px !important; /* 与表头保持一致的行高 */ +} + +/* 确保文件大小列的垂直对齐保持一致 */ +.table .file-size-cell, +.selectable-files .file-size-cell, +table.selectable-files .file-size-cell { + vertical-align: middle !important; +} + +/* 表头悬停样式 */ +.table thead th.cursor-pointer:hover { + background-color: #f7f7fa; /* 表头悬停背景色 */ +} + +/* 确保所有表格的表头悬停样式一致 */ +.table th.cursor-pointer:hover, +.selectable-files th.cursor-pointer:hover, +table.selectable-files th.cursor-pointer:hover, +table.selectable-records th.cursor-pointer:hover { + background-color: #f7f7fa !important; /* 表头悬停背景色 */ + cursor: pointer; +} + +/* 添加表头排序列的鼠标指针样式 */ +.table th.sortable, +.selectable-files th.sortable, +table.selectable-files th.sortable, +table.selectable-records th.sortable { + cursor: pointer; +} + +/* 确保展开后其他列(如转存日期、大小、修改日期)的内容保持在原始位置 */ +.table tr td { + vertical-align: top !important; /* 确保所有单元格内容顶部对齐 */ +} + +/* 确保文件整理页面和模态框中的表格也应用相同的规则 */ +#fileSelectModal .table tr td, +.selectable-files tr td, +table.selectable-files tr td, +table.selectable-records tr td { + vertical-align: top !important; /* 确保所有单元格内容顶部对齐 */ +} + +/* 确保展开后的行中所有单元格内容保持原位 */ +.table tr:has(.expanded-text) td, +#fileSelectModal .table tr:has([style*="white-space: normal"]) td, +.selectable-files tr:has([style*="white-space: normal"]) td { + vertical-align: top !important; /* 确保展开后所有单元格内容顶部对齐 */ +} + +/* 确保删除按钮在展开行中的位置保持不变 */ +tr:has(.expanded-text) .delete-record-btn, +#fileSelectModal .table tr:has([style*="white-space: normal"]) .delete-record-btn, +.selectable-files tr:has([style*="white-space: normal"]) .delete-record-btn { + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 确保不使用任何转换 */ +} + +/* 文件大小列中展开行的删除按钮特殊处理 */ +tr:has(.expanded-text) .file-size-cell .delete-record-btn, +.selectable-records tbody tr:has(.expanded-text) .file-size-cell .delete-record-btn, +#fileSelectModal .table tr:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn, +.selectable-files tr:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn { + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 确保不使用任何转换 */ + display: flex !important; /* 确保显示 */ + left: 9px !important; /* 确保左边距固定 */ +} + +/* 修复删除按钮位置问题 - 使用更强制的方法 */ +.delete-record-btn { + color: #dc3545; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: flex-start; + width: 24px; + height: 24px; + border-radius: 4px; + transition: background-color 0.2s ease; + visibility: hidden; + position: absolute !important; /* 强制绝对定位 */ + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 强制禁用任何转换 */ + left: 0; +} + +/* 删除按钮图标大小 */ +.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; +} + +/* 选中行或鼠标悬停行的大小列样式 */ +tr.selected-record .file-size-cell .file-size-value, +.selectable-records tbody tr:hover .file-size-cell .file-size-value { + display: none; /* 隐藏文件大小信息 */ +} + +/* 文件大小列中的删除按钮特殊处理 - 完全重写这部分规则 */ +tr .file-size-cell .delete-record-btn, +tr.selected-record .file-size-cell .delete-record-btn, +.selectable-records tbody tr .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: 24px; + margin-left: 0; + padding-left: 0; + left: 9px !important; /* 强制固定左边距 */ + position: absolute !important; /* 强制绝对定位 */ + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 强制禁用任何转换 */ +} + +/* 确保在展开行中删除按钮位置保持不变 - 对所有可能的情况进行覆盖 */ +tr:has(.expanded-text) .delete-record-btn, +tr.selected-record:has(.expanded-text) .delete-record-btn, +.selectable-records tbody tr:has(.expanded-text) .delete-record-btn, +.selectable-records tbody tr:hover:has(.expanded-text) .delete-record-btn, +#fileSelectModal .table tr:has([style*="white-space: normal"]) .delete-record-btn, +.selectable-files tr:has([style*="white-space: normal"]) .delete-record-btn { + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 强制禁用任何转换 */ +} + +/* 文件大小列中展开行的删除按钮特殊处理 - 对所有可能的情况进行覆盖 */ +tr:has(.expanded-text) .file-size-cell .delete-record-btn, +tr.selected-record:has(.expanded-text) .file-size-cell .delete-record-btn, +.selectable-records tbody tr:has(.expanded-text) .file-size-cell .delete-record-btn, +.selectable-records tbody tr:hover:has(.expanded-text) .file-size-cell .delete-record-btn, +#fileSelectModal .table tr:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn, +.selectable-files tr:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn { + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 强制禁用任何转换 */ + display: flex !important; /* 确保显示 */ + left: 9px !important; /* 确保左边距固定 */ +} + +/* 文件整理页面表格行悬停样式 */ +.selectable-files tbody tr:hover { + background-color: var(--button-gray-background-color); +} + +/* --------------- 文件整理页面样式 --------------- */ + +/* 删除按钮样式调整 */ +.selectable-files .delete-record-btn { + color: #dc3545; + cursor: pointer; + display: none; + align-items: center; + justify-content: flex-start; /* 居左对齐 */ + width: 24px; + height: 24px; + border-radius: 4px; + transition: background-color 0.2s ease; + position: absolute; + right: auto; /* 移除右对齐 */ + left: 9px; /* 与大小值保持相同的左边距 */ + top: 50%; + transform: translateY(-50%); +} + +/* 修复:确保在悬停和选中状态下删除按钮显示 */ +.selectable-files tr:hover .file-size-cell .delete-record-btn, +.selectable-files tr.selected-file .file-size-cell .delete-record-btn { + display: inline-flex !important; + visibility: visible !important; +} + +/* 修复:确保在悬停和选中状态下文件大小值隐藏 */ +.selectable-files tr:hover .file-size-cell .file-size-value, +.selectable-files tr.selected-file .file-size-cell .file-size-value { + display: none !important; + visibility: hidden !important; +} + +/* 修复:确保在悬停和选中状态下删除按钮显示 */ +.selectable-files tbody tr:hover .file-size-cell .delete-record-btn, +.selectable-files tr.selected-file .file-size-cell .delete-record-btn { + display: flex !important; + visibility: visible !important; + position: absolute !important; + top: 8px !important; + left: 9px !important; + transform: none !important; + height: 24px !important; + width: auto !important; + align-items: center !important; + justify-content: flex-start !important; + z-index: 5 !important; /* 确保删除按钮在其他元素之上 */ +} + +/* 修复文件大小列中的删除按钮特殊处理 */ +.selectable-files .file-size-cell .delete-record-btn { + position: absolute !important; + left: 9px !important; + top: 8px !important; + transform: none !important; + display: none; /* 默认隐藏 */ + visibility: hidden; /* 默认隐藏 */ +} + +/* 修复:确保在悬停和选中状态下文件大小值隐藏 */ +.selectable-files tbody tr:hover .file-size-cell .file-size-value, +.selectable-files tr.selected-file .file-size-cell .file-size-value { + display: none !important; + visibility: hidden !important; +} + +/* 确保文件整理页面的删除按钮在悬停和选中状态下始终可见 - 最高优先级 */ +body .selectable-files tbody tr:hover .file-size-cell .delete-record-btn, +body .selectable-files tr.selected-file .file-size-cell .delete-record-btn { + display: flex !important; + visibility: visible !important; + position: absolute !important; + top: 8px !important; + left: 9px !important; + transform: none !important; + height: 24px !important; + width: auto !important; + align-items: center !important; + justify-content: flex-start !important; + z-index: 5 !important; + opacity: 1 !important; +} + +/* 文件整理页面的文件名单元格样式 */ +.selectable-files td .text-truncate { + display: block; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 25px; /* 为展开按钮预留空间 */ + line-height: inherit; /* 使用继承的行高 */ +} + +/* 文件整理页面展开后的文本样式 */ +.selectable-files td div:not(.text-truncate):not(.expand-button):not(.d-flex) { + white-space: normal; + word-break: break-all; + display: block; + line-height: inherit; /* 使用继承的行高 */ + margin-top: 0; + margin-bottom: -1px; + position: relative; + padding-right: 25px; + max-width: 100%; /* 确保不超出单元格宽度 */ + top: -1px; +} + +/* 展开按钮悬停状态 */ +.selectable-files .expand-button:hover { + background-color: #fff !important; /* 保持白色背景 */ + opacity: 1 !important; +} + +/* 展开按钮样式 */ +.selectable-files .expand-button { + position: absolute; + right: 5px; + top: 11px; /* 固定位置,不使用垂直居中 */ + transform: none; /* 移除垂直居中转换 */ + cursor: pointer; + opacity: 0; + transition: opacity 0.2s; + background: #fff; + border-radius: 50%; + width: 18px; + height: 18px; + text-align: center; + line-height: 18px; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); + z-index: 2; + pointer-events: auto; /* 确保点击事件不被阻止 */ +} + +/* 展开按钮悬停时可见 */ +.selectable-files .position-relative:hover .expand-button { + opacity: 1; +} + +/* 展开按钮中的图标样式 */ +.selectable-files .expand-button .bi { + font-size: 0.7em; + vertical-align: middle; + position: relative; + top: -1px; +} + +/* 确保单元格垂直居中 */ +.selectable-files td { + vertical-align: middle !important; +} + +/* 确保图标与文本垂直对齐 */ +.selectable-files .d-flex.align-items-center { + align-items: center !important; +} + +/* 确保图标固定大小不变形 */ +.selectable-files .d-flex.align-items-center i.bi { + flex-shrink: 0; +} + +/* 确保图标与文本垂直对齐 - 使用固定高度 */ +.selectable-files .d-flex.align-items-center { + align-items: flex-start !important; /* 改为顶部对齐,使用固定高度 */ +} + +/* 确保图标固定大小不变形,使用固定位置 */ +.selectable-files .d-flex.align-items-center i.bi { + flex-shrink: 0; + position: relative; + top: 0px; /* 固定图标位置,向下移动4px */ +} + +/* 修复文件整理页面展开后文件大小列的垂直对齐问题 */ +/* 确保展开后的行中所有单元格都使用顶部对齐 */ +.selectable-files tr:has([style*="white-space: normal"]) td, +.selectable-files tr:has(div:not(.text-truncate):not(.expand-button):not(.d-flex)) td { + vertical-align: top !important; /* 强制展开后所有单元格顶部对齐 */ +} + +/* 特别处理文件大小列,确保展开后也使用顶部对齐 */ +.selectable-files tr:has([style*="white-space: normal"]) .file-size-cell, +.selectable-files tr:has(div:not(.text-truncate):not(.expand-button):not(.d-flex)) .file-size-cell { + vertical-align: top !important; /* 强制文件大小列顶部对齐 */ +} + +/* 确保文件大小列中的删除按钮在展开状态下位置正确 */ +.selectable-files tr:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn, +.selectable-files tr:has(div:not(.text-truncate):not(.expand-button):not(.d-flex)) .file-size-cell .delete-record-btn { + top: 8px !important; /* 强制固定位置 */ + transform: none !important; /* 强制禁用任何转换 */ + display: flex !important; /* 确保显示 */ + left: 9px !important; /* 确保左边距固定 */ + position: absolute !important; /* 强制绝对定位 */ +} + +/* 确保选中状态下文件整理页面的展开按钮也保持白色背景 */ +.selectable-files .expand-button { + background-color: #fff !important; +} + +/* 文件整理页面重命名配置框输入框样式 - 去除圆角 */ +.file-manager-rule-bar .file-manager-input { + border-radius: 0 !important; /* 去除圆角 */ +} + +/* 文件整理页面重命名配置框输入框相邻边框重叠样式 */ +.file-manager-rule-bar .file-manager-input:not(:first-child) { + margin-left: -1px; /* 向左移动1px,使边框重叠 */ +} + +/* 确保输入框在输入组中的边框重叠 */ +.file-manager-rule-bar .input-group .file-manager-input:not(:first-child) { + border-left: 1px solid var(--border-color) !important; /* 确保左边框显示 */ + margin-left: -1px; /* 向左移动1px,使边框重叠 */ +} + +/* 文件整理页面正则命名按钮样式 - 保持左边圆角,去除右边圆角 */ +.file-manager-rule-bar .input-group-prepend .btn { + position: relative; /* 添加相对定位以应用z-index */ + z-index: 2; /* 确保边框在顶层 */ + border-top-left-radius: 6px !important; /* 保持左上角圆角 */ + border-bottom-left-radius: 6px !important; /* 保持左下角圆角 */ + border-top-right-radius: 0 !important; /* 去除右上角圆角 */ + border-bottom-right-radius: 0 !important; /* 去除右下角圆角 */ + border-right: 1px solid var(--dark-text-color) !important; /* 确保右边框显示 */ +} + +/* 文件整理页面匹配表达式输入框左边框与正则命名按钮右边框重叠 */ +.file-manager-rule-bar .input-group-prepend + input.file-manager-input { + margin-left: 0px; +} + +/* 文件整理页面激活的输入框边框置顶 */ +.file-manager-rule-bar .file-manager-input:focus { + position: relative; + z-index: 3; +} + +/* 仅影响文件选择模态框表格的表头和单元格高度,减少5px */ +#fileSelectModal .table th { + padding-top: 4.5px !important; /* 原7px,减少2.5px */ + padding-bottom: 4px !important; /* 原6.5px,减少2.5px */ + height: 35px !important; /* 原40px,减少5px */ + line-height: 19px !important; /* 原24px,减少5px */ +} + +#fileSelectModal .table td { + padding-top: 3px !important; /* 原5.5px,减少2.5px */ + padding-bottom: 4.5px !important; /* 原7px,减少2.5px */ + height: 35px !important; /* 原40px,减少5px */ + line-height: 19px !important; /* 原24px,减少5px */ +} + +#fileSelectModal .table td { + vertical-align: middle !important; + position: relative; +} + +#fileSelectModal .table td > *:not(.expand-button) { + position: relative; + top: 3.5px; +} + +#fileSelectModal .table td.col-size, +#fileSelectModal .table td.col-date { + padding-top: 6px !important; +} + +#fileSelectModal .table td.col-rename, +#fileSelectModal .table td.col-action { + padding-top: 2.5px !important; +} + +#fileSelectModal[data-modal-type="preview"] .table td.col-rename { + padding-top: 3px !important; +} + +.selectable-files .bi-file-earmark { + font-size: 1.06rem; /* 比模态框的0.95rem大一些 */ + margin-right: 5px; /* 可适当调整间距 */ + position: relative; + top: 1px; /* 可微调垂直对齐 */ + left: -1px; /* 可微调水平对齐 */ +} + +.bi-folder-fill { + font-size: 1.06rem; /* 比模态框的0.95rem大一些 */ + margin-right: 5px; /* 可适当调整间距 */ + position: relative; + top: 1px; /* 可微调垂直对齐 */ + left: -1px; /* 可微调水平对齐 */ + color: #ffc107; /* 保持黄色 */ +} + +/* 文件整理页面无法识别剧集编号样式 */ +#fileSelectModal[data-modal-type="preview-filemanager"] .episode-number-text { + position: relative; + top: 1.5px; /* 或你想要的像素 */ + display: inline-block; +} + +/* 任务配置页面无法识别剧集编号样式 */ +#fileSelectModal[data-modal-type="preview"] .episode-number-text { + position: relative; + top: 1px; + display: inline-block; + left: 2px; +} + +/* 文件整理页面无法识别剧集编号前面的 × 样式 */ +#fileSelectModal[data-modal-type="preview-filemanager"] .episode-x { + position: relative; + top: 0.5px; + display: inline-block; + margin-right: 2px; +} + +/* 文件整理页面命名预览模式下的绿色重命名文本上移0.5px */ +#fileSelectModal[data-modal-type="preview-filemanager"] .table td.col-rename.text-success > * { + position: relative; + top: 3px !important; /* 原来是3px,上移0.5px */ +} + +/* 文件整理页面命名预览模式下的取消按钮样式 */ +#fileSelectModal[data-modal-type="preview-filemanager"] .modal-footer .btn-cancel { + background-color: var(--button-gray-background-color) !important; + border-color: var(--button-gray-background-color) !important; + color: var(--dark-text-color) !important; +} +#fileSelectModal[data-modal-type="preview-filemanager"] .modal-footer .btn-cancel:hover { + background-color: #e0e2e6 !important; + border-color: #e0e2e6 !important; + color: var(--dark-text-color) !important; +} + +@media (max-width: 767.98px) { + .file-manager-rule-bar-responsive { + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + } + .file-manager-rule-bar-responsive .rule-row { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 8px; + gap: 8px; + width: 100%; + } + .file-manager-rule-bar-responsive .rule-row:last-child { + margin-bottom: 0; + } + .file-manager-rule-bar-responsive .rule-row input, + .file-manager-rule-bar-responsive .rule-row button, + .file-manager-rule-bar-responsive .rule-row .input-group-text, + .file-manager-rule-bar-responsive .rule-row label { + flex: 1 1 0; + min-width: 0; + } + .file-manager-rule-bar-responsive .rule-row .input-group-prepend, + .file-manager-rule-bar-responsive .rule-row .input-group-text { + flex: 0 0 auto; + } + .file-manager-rule-bar-responsive .filter-label { + flex: 0 0 auto; + margin-right: 4px; + white-space: nowrap; + } +} + +.file-manager-rule-bar-responsive { display: none; } +.file-manager-rule-bar { display: flex; } +@media (max-width: 767.98px) { + .file-manager-rule-bar { display: none !important; } + .file-manager-rule-bar-responsive { display: block !important; } + .file-manager-rule-bar-responsive .input-group { width: 100%; } + .file-manager-rule-bar-responsive .input-group + .input-group { margin-top: 8px; } + /* 含文件夹样式调整 */ + .file-manager-rule-bar-responsive .input-group-text.file-folder-rounded { + border-top-left-radius: 0 !important; /* 左侧不要圆角 */ + border-bottom-left-radius: 0 !important; /* 左侧不要圆角 */ + border-top-right-radius: 6px !important; /* 右侧圆角6px */ + border-bottom-right-radius: 6px !important; /* 右侧圆角6px */ + margin-left: -1px; /* 左边框与过滤规则输入框重叠 */ + position: relative; + z-index: 1; /* 确保边框显示正确 */ + } + + /* 过滤规则输入框在含文件夹前面时去除右侧圆角 */ + .file-manager-rule-bar-responsive .input-group .form-control + .input-group-text.file-folder-rounded { + border-left: 1px solid #ced4da; /* 确保左边框显示 */ + } + + /* 过滤规则输入框右侧圆角调整 */ + .file-manager-rule-bar-responsive .input-group .form-control:not(:last-child) { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } + /* 预览并执行重命名按钮与含文件夹间距8px,且与上方input-group间距8px */ + .file-manager-rule-bar-responsive .input-group-append .btn { + margin-left: 8px; + } + /* 只影响移动端的“预览并执行重命名”按钮上边距 */ + .file-manager-rule-bar-responsive .batch-rename-btn { + margin-top: 0px; /* 你想要的上边距 */ + } + + /* 移动端预览并执行重命名按钮与含文件夹间距 */ + .file-manager-rule-bar-responsive .input-group .batch-rename-btn { + margin-left: 8px !important; + } +} +@media (min-width: 768px) { + .file-manager-rule-bar { display: flex !important; } + .file-manager-rule-bar-responsive { display: none !important; } +} diff --git a/app/static/js/sort_file_by_name.js b/app/static/js/sort_file_by_name.js new file mode 100644 index 0000000..4be5773 --- /dev/null +++ b/app/static/js/sort_file_by_name.js @@ -0,0 +1,155 @@ +// 与后端 quark_auto_save.py 的 sort_file_by_name 完全一致的排序逻辑 +// 用于前端文件列表排序 + +function chineseToArabic(chinese) { + // 简单实现,支持一到一万 + const cnNums = { + '零': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, + '十': 10, '百': 100, '千': 1000, '万': 10000 + }; + let result = 0, unit = 1, num = 0; + for (let i = chinese.length - 1; i >= 0; i--) { + const char = chinese[i]; + if (cnNums[char] >= 10) { + unit = cnNums[char]; + if (unit === 10 && (i === 0 || cnNums[chinese[i - 1]] === undefined)) { + num = 1; + } + } else if (cnNums[char] !== undefined) { + num = cnNums[char]; + result += num * unit; + } + } + return result || null; +} + +function sortFileByName(file) { + // 兼容 dict 或字符串 + let filename = typeof file === 'object' ? (file.file_name || '') : file; + let update_time = typeof file === 'object' ? (file.updated_at || 0) : 0; + let file_name_without_ext = filename.replace(/\.[^/.]+$/, ''); + let date_value = Infinity, episode_value = Infinity, segment_value = 0; + + // 1. 日期提取 + let match; + // YYYY-MM-DD + match = filename.match(/((?:19|20)\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})/); + if (match) { + date_value = parseInt(match[1]) * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]); + } + // YY-MM-DD + if (date_value === Infinity) { + match = filename.match(/((?:19|20)?\d{2})[-./\s](\d{1,2})[-./\s](\d{1,2})/); + if (match && match[1].length === 2) { + let year = parseInt('20' + match[1]); + date_value = year * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]); + } + } + // YYYYMMDD + if (date_value === Infinity) { + match = filename.match(/((?:19|20)\d{2})(\d{2})(\d{2})/); + if (match) { + date_value = parseInt(match[1]) * 10000 + parseInt(match[2]) * 100 + parseInt(match[3]); + } + } + // YYMMDD + if (date_value === Infinity) { + match = filename.match(/(? 12) [month, day] = [day, month]; + date_value = year * 10000 + month * 100 + day; + } + } + // MM-DD + if (date_value === Infinity) { + match = filename.match(/(? 12) [month, day] = [day, month]; + date_value = 20000000 + month * 100 + day; + } + } + + // 2. 期数/集数 + // 第X期/集/话 + match = filename.match(/第(\d+)[期集话]/); + if (match) episode_value = parseInt(match[1]); + // 第[中文数字]期/集/话 + if (episode_value === Infinity) { + match = filename.match(/第([一二三四五六七八九十百千万零两]+)[期集话]/); + if (match) { + let arabic = chineseToArabic(match[1]); + if (arabic !== null) episode_value = arabic; + } + } + // X集/期/话 + if (episode_value === Infinity) { + match = filename.match(/(\d+)[期集话]/); + if (match) episode_value = parseInt(match[1]); + } + // [中文数字]集/期/话 + if (episode_value === Infinity) { + match = filename.match(/([一二三四五六七八九十百千万零两]+)[期集话]/); + if (match) { + let arabic = chineseToArabic(match[1]); + if (arabic !== null) episode_value = arabic; + } + } + // S01E01 + if (episode_value === Infinity) { + match = filename.match(/[Ss](\d+)[Ee](\d+)/); + if (match) episode_value = parseInt(match[2]); + } + // E01/EP01 + if (episode_value === Infinity) { + match = filename.match(/[Ee][Pp]?(\d+)/); + if (match) episode_value = parseInt(match[1]); + } + // 1x01 + if (episode_value === Infinity) { + match = filename.match(/(\d+)[Xx](\d+)/); + if (match) episode_value = parseInt(match[2]); + } + // [数字]或【数字】 + if (episode_value === Infinity) { + match = filename.match(/\[(\d+)\]|【(\d+)】/); + if (match) episode_value = parseInt(match[1] || match[2]); + } + // 纯数字文件名 + if (episode_value === Infinity) { + if (/^\d+$/.test(file_name_without_ext)) { + episode_value = parseInt(file_name_without_ext); + } else { + match = filename.match(/(\d+)/); + if (match) episode_value = parseInt(match[1]); + } + } + + // 3. 上中下 + if (/[上][集期话部篇]?|[集期话部篇]上/.test(filename)) segment_value = 1; + else if (/[中][集期话部篇]?|[集期话部篇]中/.test(filename)) segment_value = 2; + else if (/[下][集期话部篇]?|[集期话部篇]下/.test(filename)) segment_value = 3; + + return [date_value, episode_value, segment_value, update_time]; +} + +// 用法: +// arr.sort((a, b) => { +// const ka = sortFileByName(a), kb = sortFileByName(b); +// for (let i = 0; i < ka.length; ++i) { +// if (ka[i] !== kb[i]) return ka[i] > kb[i] ? 1 : -1; +// } +// return 0; +// }); \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 3ee4995..fed8fb2 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -16,6 +16,7 @@ + diff --git a/quark_auto_save.py b/quark_auto_save.py index 842ec15..83cbf69 100644 --- a/quark_auto_save.py +++ b/quark_auto_save.py @@ -229,6 +229,9 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None): Returns: int: 提取到的剧集号,如果无法提取则返回None """ + # 首先去除文件扩展名 + file_name_without_ext = os.path.splitext(filename)[0] + # 预处理:排除文件名中可能是日期的部分,避免误识别 date_patterns = [ # YYYY-MM-DD 或 YYYY.MM.DD 或 YYYY/MM/DD 或 YYYY MM DD格式(四位年份) @@ -245,10 +248,10 @@ def extract_episode_number(filename, episode_patterns=None, config_data=None): r'(?