新增拼音排序支持,优化起始文件、顺序命名逻辑,文件整理页面新增移动文件和新建文件夹功能

Merge pull request #30 from x1ao4/dev
This commit is contained in:
x1ao4 2025-07-04 15:21:28 +08:00 committed by GitHub
commit 223a46d906
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1311 additions and 282 deletions

View File

@ -11,7 +11,7 @@
- **WebUI**:对整个 WebUI 进行了重塑增加了更多实用功能如文件选择和预览界面的排序功能、资源搜索的过滤功能、TMDB 和豆瓣搜索功能、页面视图切换功能、账号设置功能等等。
- **查重逻辑**:支持优先通过历史转存记录查重,对于有转存记录的文件,即使删除网盘文件,也不会重复转存。
- **Aria2**:支持成功添加 Aria2 下载任务后自动删除夸克网盘内对应的文件,清理网盘空间。
- **文件整理**:支持浏览和管理多个夸克账号的网盘文件,支持批量重命名(支持应用完整的命名、过滤规则和撤销重命名等操作)、删除文件等操作。
- **文件整理**:支持浏览和管理多个夸克账号的网盘文件,支持单项/批量重命名(支持应用完整的命名、过滤规则和撤销重命名等操作)、移动文件、删除文件、新建文件夹等操作。
本项目修改后的版本为个人需求定制版,目的是满足我自己的使用需求,某些(我不用的)功能可能会因为修改而出现 BUG不一定会被修复。若你要使用本项目请知晓本人不是程序员我无法保证本项目的稳定性如果你在使用过程中发现了 BUG可以在 Issues 中提交,但不保证每个 BUG 都能被修复,请谨慎使用,风险自担。

View File

@ -40,6 +40,14 @@ from quark_auto_save import Config, format_bytes
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from quark_auto_save import extract_episode_number, sort_file_by_name, chinese_to_arabic, is_date_format
# 导入拼音排序工具
try:
from utils.pinyin_sort import get_filename_pinyin_sort_key
except ImportError:
# 如果导入失败,使用简单的小写排序作为备用
def get_filename_pinyin_sort_key(filename):
return filename.lower()
# 导入数据库模块
try:
# 先尝试相对导入
@ -856,9 +864,11 @@ def get_share_detail():
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
# 添加episode_number字段用于前端排序
file["episode_number"] = episode_num
else:
# 没有提取到集号,显示无法识别的提示
file["file_name_re"] = "× 无法识别剧集编号"
@ -1011,6 +1021,114 @@ def delete_file():
return jsonify(response)
@app.route("/move_file", methods=["POST"])
def move_file():
if not is_login():
return jsonify({"success": False, "message": "未登录"})
# 获取账号索引参数
account_index = int(request.json.get("account_index", 0))
# 验证账号索引
if account_index < 0 or account_index >= len(config_data["cookie"]):
return jsonify({"success": False, "message": "账号索引无效"})
account = Quark(config_data["cookie"][account_index], account_index)
# 获取参数
file_ids = request.json.get("file_ids", [])
target_folder_id = request.json.get("target_folder_id")
if not file_ids:
return jsonify({"success": False, "message": "缺少文件ID列表"})
if not target_folder_id:
return jsonify({"success": False, "message": "缺少目标文件夹ID"})
try:
# 调用夸克网盘的移动API
response = account.move(file_ids, target_folder_id)
if response["code"] == 0:
return jsonify({
"success": True,
"message": f"成功移动 {len(file_ids)} 个文件",
"moved_count": len(file_ids)
})
else:
return jsonify({
"success": False,
"message": response.get("message", "移动失败")
})
except Exception as e:
return jsonify({
"success": False,
"message": f"移动文件时出错: {str(e)}"
})
@app.route("/create_folder", methods=["POST"])
def create_folder():
if not is_login():
return jsonify({"success": False, "message": "未登录"})
# 获取请求参数
data = request.json
parent_folder_id = data.get("parent_folder_id")
folder_name = data.get("folder_name", "新建文件夹")
account_index = int(data.get("account_index", 0))
# 验证参数
if not parent_folder_id:
return jsonify({"success": False, "message": "缺少父目录ID"})
# 验证账号索引
if account_index < 0 or account_index >= len(config_data["cookie"]):
return jsonify({"success": False, "message": "账号索引无效"})
try:
# 初始化夸克网盘客户端
account = Quark(config_data["cookie"][account_index], account_index)
# 调用新建文件夹API
response = account.mkdir_in_folder(parent_folder_id, folder_name)
if response.get("code") == 0:
# 创建成功,返回新文件夹信息
new_folder = response.get("data", {})
return jsonify({
"success": True,
"message": "文件夹创建成功",
"data": {
"fid": new_folder.get("fid"),
"file_name": new_folder.get("file_name", folder_name),
"dir": True,
"size": 0,
"updated_at": new_folder.get("updated_at"),
"include_items": 0
}
})
else:
# 处理特定的错误信息
error_message = response.get("message", "创建文件夹失败")
# 检查是否是同名文件夹冲突
if "同名" in error_message or "已存在" in error_message or "重复" in error_message or "doloading" in error_message:
error_message = "已存在同名文件夹,请修改名称后再试"
return jsonify({
"success": False,
"message": error_message
})
except Exception as e:
logging.error(f">>> 创建文件夹时出错: {str(e)}")
return jsonify({
"success": False,
"message": f"创建文件夹时出错: {str(e)}"
})
# 添加任务接口
@app.route("/api/add_task", methods=["POST"])
def add_task():
@ -1581,9 +1699,11 @@ def get_file_list():
# 优化排序:使用更高效的排序方法
def get_sort_key(file_item):
if sort_by == "file_name":
return file_item["file_name"].lower()
# 使用拼音排序
return get_filename_pinyin_sort_key(file_item["file_name"])
elif sort_by == "file_size":
return file_item["size"] if not file_item["dir"] else 0
# 文件夹按项目数量排序,文件按大小排序
return file_item.get("include_items", 0) if file_item["dir"] else file_item["size"]
else: # updated_at
return file_item["updated_at"]
@ -1738,7 +1858,8 @@ def preview_rename():
preview_results.append({
"original_name": file["file_name"],
"new_name": new_name,
"file_id": file["fid"]
"file_id": file["fid"],
"episode_number": episode_num # 添加集数字段用于前端排序
})
else:
# 没有提取到集号,显示无法识别的提示

View File

@ -174,9 +174,34 @@ class RecordDB:
total_records = cursor.fetchone()[0]
# 获取分页数据
query_sql = f"SELECT * FROM transfer_records {where_sql} ORDER BY {sort_by} {order_direction} LIMIT ? OFFSET ?"
cursor.execute(query_sql, params + [page_size, offset])
records = cursor.fetchall()
if sort_by in ["task_name", "original_name", "renamed_to"]:
# 对于需要拼音排序的字段先获取所有数据然后在Python中进行拼音排序
query_sql = f"SELECT * FROM transfer_records {where_sql}"
cursor.execute(query_sql, params)
all_records = cursor.fetchall()
# 使用拼音排序
from utils.pinyin_sort import get_filename_pinyin_sort_key
# 根据排序字段选择对应的索引
field_index_map = {
"task_name": 2, # task_name字段索引
"original_name": 3, # original_name字段索引
"renamed_to": 4 # renamed_to字段索引
}
field_index = field_index_map[sort_by]
sorted_records = sorted(all_records, key=lambda x: get_filename_pinyin_sort_key(x[field_index]), reverse=(order_direction == "DESC"))
# 手动分页
start_idx = offset
end_idx = offset + page_size
records = sorted_records[start_idx:end_idx]
else:
# 其他字段使用SQL排序
query_sql = f"SELECT * FROM transfer_records {where_sql} ORDER BY {sort_by} {order_direction} LIMIT ? OFFSET ?"
cursor.execute(query_sql, params + [page_size, offset])
records = cursor.fetchall()
# 将结果转换为字典列表
columns = [col[0] for col in cursor.description]

View File

@ -804,12 +804,16 @@ main div[v-if="activeTab === 'config'"] .row.title:first-child {
/* 文件夹图标样式 */
.bi-folder {
color: var(--dark-text-color);
font-size: 0.98rem;
font-size: 1.01rem;
position: relative;
top: 0.5px;
}
/* 重置文件夹图标样式 */
.bi-folder-x {
font-size: 0.98rem;
font-size: 1.01rem;
position: relative;
top: 0.5px;
}
/* 恢复图标样式 */
@ -876,7 +880,9 @@ select.form-control {
/* 链接图标样式 */
.bi-link-45deg {
color: var(--dark-text-color);
font-size: 1.15rem;
font-size: 1.2rem;
position: relative;
top: 0.5px;
}
/* 谷歌图标样式 */
@ -2118,7 +2124,7 @@ div.jsoneditor-tree button.jsoneditor-button:focus {
/* 侧边栏菜单项图标样式 */
.sidebar .nav-link .bi-list-ul {
font-size: 1.08rem;
font-size: 1.09rem;
position: relative;
top: 0.5px; /* 向下微调 */
}
@ -4187,6 +4193,11 @@ select.task-filter-select,
color: var(--focus-border-color);
}
/* 文件整理页面面包屑导航当前目录样式 */
.file-manager-breadcrumb .breadcrumb-item .text-muted {
color: var(--dark-text-color) !important;
}
/* 隐藏Bootstrap默认的面包屑分隔符 */
.file-manager-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
color: var(--dark-text-color);
@ -4244,6 +4255,13 @@ select.task-filter-select,
font-size: 1.17rem;
}
/* 文件整理页面新建文件夹按钮图标大小 */
.batch-rename-btn .bi-folder-plus {
font-size: 1.01rem;
position: relative;
top: 0px;
}
/* 确保文件整理页面的Plex和AList按钮样式与任务列表一致 */
.batch-rename-btn.btn-outline-plex,
.batch-rename-btn.btn-outline-alist {
@ -4288,6 +4306,8 @@ select.task-filter-select,
filter: brightness(0) invert(1);
}
/* 文件表格中的展开按钮 */
.expand-button {
position: absolute;
@ -4582,6 +4602,11 @@ table.selectable-files .rename-record-btn:hover {
color: #0A42CC !important;
}
/* 文件整理页面移动按钮悬停效果 */
table.selectable-files .move-record-btn:hover {
color: #0A42CC !important;
}
/* 文件整理页面删除按钮悬停效果 */
table.selectable-files .delete-record-btn:hover {
color: #b02a37 !important;
@ -4694,17 +4719,6 @@ table.selectable-records tr td {
/* 文件大小列中展开行的删除按钮特殊处理 */
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; /* 确保不使用任何转换 */
left: 32px !important; /* 确保左边距固定,在重命名按钮右侧 */
/* 不设置display保持默认的隐藏状态 */
}
/* 文件大小列中展开行的重命名按钮特殊处理 */
tr:has(.expanded-text) .file-size-cell .rename-record-btn,
.selectable-records tbody tr:has(.expanded-text) .file-size-cell .rename-record-btn,
@ -4716,6 +4730,28 @@ tr:has(.expanded-text) .file-size-cell .rename-record-btn,
/* 不设置display保持默认的隐藏状态 */
}
/* 文件大小列中展开行的移动按钮特殊处理 */
tr:has(.expanded-text) .file-size-cell .move-record-btn,
.selectable-records tbody tr:has(.expanded-text) .file-size-cell .move-record-btn,
#fileSelectModal .table tr:has([style*="white-space: normal"]) .file-size-cell .move-record-btn,
.selectable-files tr:has([style*="white-space: normal"]) .file-size-cell .move-record-btn {
top: 8px !important; /* 强制固定位置 */
transform: none !important; /* 确保不使用任何转换 */
left: 32px !important; /* 确保左边距固定,在重命名按钮右侧 */
/* 不设置display保持默认的隐藏状态 */
}
/* 文件大小列中展开行的删除按钮特殊处理 */
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; /* 确保不使用任何转换 */
left: 59px !important; /* 确保左边距固定,在移动按钮右侧 */
/* 不设置display保持默认的隐藏状态 */
}
/* 修复删除按钮位置问题 - 使用更强制的方法 */
.delete-record-btn {
color: #dc3545;
@ -4803,11 +4839,34 @@ tr.selected-record .file-size-cell .delete-record-btn,
/* 文件整理页面的重命名文件按钮图标大小 */
.selectable-files .rename-record-btn .bi-pencil {
font-size: 0.98rem;
font-size: 0.99rem;
position: relative;
left: 0.5px;
}
/* 移动按钮样式 */
.selectable-files .move-record-btn {
color: var(--focus-border-color);
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 4px;
position: absolute;
left: 32px; /* 在重命名按钮右侧,调整间距 */
top: 50%;
transform: translateY(-50%);
font-size: 14px;
}
/* 文件整理页面的移动文件按钮图标大小 */
.selectable-files .move-record-btn .bi-arrow-up-right-circle {
font-size: 1.07rem;
position: relative;
}
/* 删除按钮样式调整 */
.selectable-files .delete-record-btn {
color: #dc3545;
@ -4820,7 +4879,7 @@ tr.selected-record .file-size-cell .delete-record-btn,
border-radius: 4px;
position: absolute;
right: auto; /* 移除右对齐 */
left: 32px; /* 在重命名按钮右侧,调整间距 */
left: 59px; /* 在移动按钮右侧,调整间距 */
top: 50%;
transform: translateY(-50%);
font-size: 14px;
@ -4832,9 +4891,11 @@ tr.selected-record .file-size-cell .delete-record-btn,
min-width: 80px;
}
/* 修复:确保在悬停和选中状态下重命名和删除按钮显示 */
/* 修复:确保在悬停和选中状态下重命名、移动和删除按钮显示 */
.selectable-files tr:hover .file-size-cell .rename-record-btn,
.selectable-files tr.selected-file .file-size-cell .rename-record-btn,
.selectable-files tr:hover .file-size-cell .move-record-btn,
.selectable-files tr.selected-file .file-size-cell .move-record-btn,
.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;
@ -4887,6 +4948,23 @@ body .selectable-files tr.selected-file .file-size-cell .rename-record-btn {
opacity: 1 !important;
}
/* 确保文件整理页面的移动按钮在悬停和选中状态下始终可见 - 最高优先级 */
body .selectable-files tbody tr:hover .file-size-cell .move-record-btn,
body .selectable-files tr.selected-file .file-size-cell .move-record-btn {
display: inline-flex !important;
visibility: visible !important;
position: absolute !important;
top: 50% !important;
transform: translateY(-50%) !important;
left: 32px !important;
width: 24px !important;
height: 24px !important;
align-items: center !important;
justify-content: center !important;
z-index: 5 !important;
opacity: 1 !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 {
@ -4895,7 +4973,7 @@ body .selectable-files tr.selected-file .file-size-cell .delete-record-btn {
position: absolute !important;
top: 50% !important;
transform: translateY(-50%) !important;
left: 32px !important;
left: 59px !important;
width: 24px !important;
height: 24px !important;
align-items: center !important;
@ -4921,6 +4999,23 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
opacity: 1 !important;
}
/* 展开状态下的移动按钮悬停和选中状态 - 最高优先级 */
body .selectable-files tbody tr:hover:has([style*="white-space: normal"]) .file-size-cell .move-record-btn,
body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .file-size-cell .move-record-btn {
display: inline-flex !important;
visibility: visible !important;
position: absolute !important;
top: 8px !important;
transform: none !important;
left: 32px !important;
width: 24px !important;
height: 24px !important;
align-items: center !important;
justify-content: center !important;
z-index: 5 !important;
opacity: 1 !important;
}
/* 展开状态下的删除按钮悬停和选中状态 - 最高优先级 */
body .selectable-files tbody tr:hover:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn,
body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .file-size-cell .delete-record-btn {
@ -4929,7 +5024,7 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
position: absolute !important;
top: 8px !important;
transform: none !important;
left: 32px !important;
left: 59px !important;
width: 24px !important;
height: 24px !important;
align-items: center !important;
@ -5192,6 +5287,18 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
color: var(--dark-text-color) !important;
}
/* 移动文件模态框的取消按钮样式 */
#fileSelectModal[data-modal-type="move"] .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="move"] .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;

1
app/static/js/pinyin-pro.min.js vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -30,6 +30,19 @@ function sortFileByName(file) {
let file_name_without_ext = filename.replace(/\.[^/.]+$/, '');
let date_value = Infinity, episode_value = Infinity, segment_value = 0;
// 生成拼音排序键(第五级排序)
let pinyin_sort_key;
try {
// 尝试使用 pinyinPro 库进行拼音转换
if (typeof pinyinPro !== 'undefined') {
pinyin_sort_key = pinyinPro.pinyin(filename, { toneType: 'none', type: 'string' }).toLowerCase();
} else {
pinyin_sort_key = filename.toLowerCase();
}
} catch (e) {
pinyin_sort_key = filename.toLowerCase();
}
// 1. 日期提取
let match;
// YYYY-MM-DD
@ -142,7 +155,7 @@ function sortFileByName(file) {
else if (/[中][集期话部篇]?|[集期话部篇]中/.test(filename)) segment_value = 2;
else if (/[下][集期话部篇]?|[集期话部篇]下/.test(filename)) segment_value = 3;
return [date_value, episode_value, segment_value, update_time];
return [date_value, episode_value, segment_value, update_time, pinyin_sort_key];
}
// 用法:

View File

@ -17,6 +17,7 @@
<script src="./static/js/axios.min.js"></script>
<script src="./static/js/v-jsoneditor.min.js"></script>
<script src="./static/js/sort_file_by_name.js"></script>
<script src="./static/js/pinyin-pro.min.js"></script>
<script>
// 添加检测文本溢出的自定义指令
Vue.directive('check-overflow', {
@ -748,7 +749,7 @@
<div class="input-group">
<input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)">
<div class="input-group-append" v-if="task.shareurl">
<button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.previewRegex=false;showShareSelect(index)" title="选择文件夹"><i class="bi bi-folder"></i></button>
<button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.previewRegex=false;showShareSelect(index)" title="选择需转存的文件夹"><i class="bi bi-folder"></i></button>
<div class="input-group-text">
<a target="_blank" :href="task.shareurl"><i class="bi bi-link-45deg"></i></a>
</div>
@ -763,7 +764,7 @@
<input type="text" name="savepath[]" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(index, task)">
<div class="input-group-append">
<button class="btn btn-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 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>
@ -801,18 +802,18 @@
</div>
</div>
<div class="form-group row" title="只转存修改日期大于选中文件的文件,请在符合筛选条件的文件中进行选择,在更换分享链接时非常有用">
<div class="form-group row" title="只转存比选中文件更新的文件,请在符合筛选条件的文件中进行选择,在更换分享链接时非常有用">
<label class="col-sm-2 col-form-label">起始文件</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" placeholder="可选,只转存修改日期大于此文件的文件,请在符合筛选条件的文件中进行选择" name="startfid[]" v-model="task.startfid">
<input type="text" class="form-control" placeholder="可选,只转存比此文件更新的文件,请在符合筛选条件的文件中进行选择" name="startfid[]" v-model="task.startfid">
<div class="input-group-append" v-if="task.shareurl">
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.previewRegex=false;showShareSelect(index)" title="选择文件"><i class="bi bi-folder"></i></button>
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.previewRegex=false;showShareSelect(index)" title="选择起始文件"><i class="bi bi-folder"></i></button>
</div>
</div>
</div>
</div>
<div class="form-group row" title="匹配成功的文件夹的所有嵌套目录都会被更新,并且会应用与根目录相同的命名和过滤规则。注意:原理是逐级索引,深层嵌套目录的场景下效率非常低,慎用.*">
<div class="form-group row" title="匹配成功的文件夹的所有嵌套目录都会被更新,并且会应用与根目录相同的正则命名和过滤规则。注意:原理是逐级索引,深层嵌套目录的场景下效率非常低,慎用.*">
<label class="col-sm-2 col-form-label">更新目录</label>
<div class="col-sm-10">
<input type="text" name="update_subdir[]" class="form-control" v-model="task.update_subdir" placeholder="可选输入需要更新的子目录的文件夹名称或正则表达式多个项目用竖线分隔4K|1080P">
@ -824,7 +825,7 @@
<div class="input-group">
<input type="date" name="enddate[]" class="form-control date-input-no-icon" v-model="task.enddate" placeholder="可选" :ref="'enddate_' + index">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" @click="openDatePicker(index)" title="选择日期">
<button type="button" class="btn btn-outline-secondary" @click="openDatePicker(index)" title="选择截止日期">
<i class="bi bi-calendar3"></i>
</button>
</div>
@ -1051,6 +1052,9 @@
</div>
<button type="button" class="btn btn-outline-plex batch-rename-btn" v-if="formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" @click="refreshFileManagerPlexLibrary" title="刷新Plex媒体库"><img src="./static/Plex.svg" class="plex-icon"></button>
<button type="button" class="btn btn-outline-alist batch-rename-btn" v-if="formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" @click="refreshFileManagerAlistDirectory" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="createNewFolder" title="新建文件夹">
<i class="bi bi-folder-plus"></i>
</button>
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="refreshCurrentFolderCache" title="刷新当前目录缓存">
<i class="bi bi-arrow-clockwise"></i>
</button>
@ -1068,6 +1072,9 @@
</div>
<button type="button" class="btn btn-outline-plex batch-rename-btn" v-if="formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" @click="refreshFileManagerPlexLibrary" title="刷新Plex媒体库"><img src="./static/Plex.svg" class="plex-icon"></button>
<button type="button" class="btn btn-outline-alist batch-rename-btn" v-if="formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" @click="refreshFileManagerAlistDirectory" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="createNewFolder" title="新建文件夹">
<i class="bi bi-folder-plus"></i>
</button>
<button type="button" class="btn btn-outline-primary batch-rename-btn" @click="refreshCurrentFolderCache" title="刷新当前目录缓存">
<i class="bi bi-arrow-clockwise"></i>
</button>
@ -1206,6 +1213,9 @@
<span class="rename-record-btn" @click.stop="startRenameFile(file)" title="重命名文件">
<i class="bi bi-pencil"></i>
</span>
<span class="move-record-btn" @click.stop="startMoveFile(file)" title="移动文件">
<i class="bi bi-arrow-up-right-circle"></i>
</span>
<span class="delete-record-btn" @click.stop="deleteSelectedFilesForManager(file.fid, file.file_name, file.dir)" title="删除文件">
<i class="bi bi-trash3"></i>
</span>
@ -1302,9 +1312,9 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">
<span v-if="fileSelect.previewRegex && fileSelect.index >= 0" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">{{ formData.tasklist[fileSelect.index].use_sequence_naming ? '顺序命名预览' : (formData.tasklist[fileSelect.index].use_episode_naming ? '剧集命名预览' : '正则命名预览') }}</span>
<span v-if="fileSelect.previewRegex && fileSelect.index >= 0 && formData.tasklist[fileSelect.index]" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">{{ formData.tasklist[fileSelect.index].use_sequence_naming ? '顺序命名预览' : (formData.tasklist[fileSelect.index].use_episode_naming ? '剧集命名预览' : '正则命名预览') }}</span>
<span v-else-if="fileSelect.previewRegex && fileSelect.index === -1" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">{{ fileManager.use_sequence_naming ? '顺序命名预览' : (fileManager.use_episode_naming ? '剧集命名预览' : '正则命名预览') }}</span>
<span v-else-if="fileSelect.selectDir" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">选择{{fileSelect.selectShare ? '需转存的' : '保存到的'}}文件夹</span>
<span v-else-if="fileSelect.selectDir" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">选择{{fileSelect.moveMode ? '移动到的' : (fileSelect.selectShare ? '需转存的' : '保存到的')}}文件夹</span>
<span v-else style="font-weight: 600; font-family: inherit; letter-spacing: normal;">选择起始文件</span>
<div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div>
</h5>
@ -1328,7 +1338,7 @@
<!-- 文件列表 -->
<div class="mb-3" v-if="fileSelect.previewRegex">
<!-- 任务配置的命名预览 -->
<div v-if="fileSelect.index >= 0">
<div v-if="fileSelect.index >= 0 && formData.tasklist[fileSelect.index]">
<div v-if="formData.tasklist[fileSelect.index].use_sequence_naming">
<div style="margin-bottom: 1px; padding-left: 12px; display: flex; align-items: center;">
<span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">顺序命名表达式:</span><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
@ -1385,7 +1395,7 @@
<template v-if="!fileSelect.previewRegex">
<th scope="col" class="col-size cursor-pointer" style="font-family: inherit; letter-spacing: normal;" @click="sortFileList('size')">大小 <i v-if="fileSelect.sortBy === 'size'" :class="fileSelect.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
<th scope="col" class="col-date cursor-pointer" style="font-family: inherit; letter-spacing: normal;" @click="sortFileList('updated_at')">修改日期 <i v-if="fileSelect.sortBy === 'updated_at'" :class="fileSelect.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
<th scope="col" class="col-action" v-if="!fileSelect.selectShare" style="font-family: inherit; letter-spacing: normal;">操作</th>
<th scope="col" class="col-action" v-if="!fileSelect.selectShare && !fileSelect.moveMode" style="font-family: inherit; letter-spacing: normal;">操作</th>
</template>
</tr>
</thead>
@ -1440,7 +1450,7 @@
<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;">
<td class="col-action" v-if="!fileSelect.selectShare && !fileSelect.moveMode" 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>
@ -1454,8 +1464,12 @@
<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>
<button type="button" class="btn btn-primary btn-cancel" @click="$('#fileSelectModal').modal('hide')" v-if="fileSelect.moveMode">取消</button>
<button type="button" class="btn btn-primary btn-sm" @click="selectCurrentFolder()">
<span v-if="fileSelect.moveMode">移动到当前文件夹</span>
<span v-else>{{fileSelect.selectShare ? '转存当前文件夹' : '保存到当前文件夹'}}</span>
</button>
<button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare && !fileSelect.moveMode && fileSelect.index !== null && fileSelect.index >= 0 && formData.tasklist[fileSelect.index]" @click="selectCurrentFolder(true)">保存到当前位置的「<span class="badge badge-light" v-html="formData.tasklist[fileSelect.index].taskname"></span>」文件夹</button>
</div>
<div class="modal-footer" v-if="fileSelect.previewRegex && fileSelect.index === -1">
<div class="file-selection-info mr-auto" style="color: var(--dark-text-color); font-size: 0.875rem; line-height: 1.5;">
@ -1571,7 +1585,9 @@
sortOrder: "desc", // 默认排序顺序
selectedFiles: [], // 存储选中的文件ID
lastSelectedFileIndex: -1, // 记录最后选择的文件索引
canUndoRename: false
canUndoRename: false,
moveMode: false, // 是否为移动文件模式
moveFileIds: [] // 要移动的文件ID列表
},
historyParams: {
sortBy: "transfer_time",
@ -1700,7 +1716,7 @@
}
});
return [...taskNames].sort();
return this.sortTaskNamesByPinyin([...taskNames]);
},
taskNames() {
// 从任务列表中提取唯一的任务名称
@ -1715,7 +1731,7 @@
}
});
return [...taskNames].sort();
return this.sortTaskNamesByPinyin([...taskNames]);
},
totalPages() {
// 直接使用后端返回的total_pages
@ -1822,6 +1838,9 @@
$('#fileSelectModal').on('hidden.bs.modal', () => {
this.fileSelect.selectedFiles = [];
this.fileSelect.lastSelectedFileIndex = -1;
// 重置移动模式相关参数
this.fileSelect.moveMode = false;
this.fileSelect.moveFileIds = [];
});
// 检查本地存储中的标签页状态
@ -1882,6 +1901,9 @@
$('#fileSelectModal').on('hidden.bs.modal', () => {
this.fileSelect.selectedFiles = [];
this.fileSelect.lastSelectedFileIndex = -1;
// 重置移动模式相关参数
this.fileSelect.moveMode = false;
this.fileSelect.moveFileIds = [];
});
window.addEventListener('beforeunload', this.handleBeforeUnload);
@ -1953,6 +1975,14 @@
document.removeEventListener('click', this.handleOutsideClick);
},
methods: {
// 拼音排序辅助函数
sortTaskNamesByPinyin(taskNames) {
return taskNames.sort((a, b) => {
const aKey = pinyinPro.pinyin(a, { toneType: 'none', type: 'string' }).toLowerCase();
const bKey = pinyinPro.pinyin(b, { toneType: 'none', type: 'string' }).toLowerCase();
return aKey > bKey ? 1 : -1;
});
},
// 添加格式化分享链接警告信息的方法
formatShareUrlBanMessage(message) {
if (!message) return message;
@ -2052,9 +2082,19 @@
// 重置页码并切换到新账号的最后访问目录
this.fileManager.currentPage = 1;
this.loadFileListWithFallback(newAccountLastFolder);
// 如果移动文件模态框正在显示,需要重新加载目录
if ($('#fileSelectModal').hasClass('show')) {
const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
if (modalType === 'move') {
// 重置路径并重新加载根目录
this.fileSelect.paths = [];
this.getSavepathDetail(0);
}
}
},
refreshCurrentFolderCache() {
refreshCurrentFolderCache(retryCount = 0) {
// 刷新当前目录的缓存,强制重新请求最新的文件列表
// 调用后端接口,添加强制刷新参数
const params = {
@ -2064,7 +2104,8 @@
page_size: this.fileManager.pageSize,
page: this.fileManager.currentPage,
account_index: this.fileManager.selectedAccountIndex,
force_refresh: true // 强制刷新参数
force_refresh: true, // 强制刷新参数
timestamp: Date.now() // 添加时间戳避免缓存
};
axios.get('/file_list', { params })
@ -2076,16 +2117,163 @@
this.fileManager.paths = response.data.data.paths || [];
this.fileManager.gotoPage = this.fileManager.currentPage;
// 移除成功通知
} else {
this.showToast('刷新失败:' + response.data.message);
}
// 检测当前的命名模式
this.detectFileManagerNamingMode();
// 检测当前的命名模式
this.detectFileManagerNamingMode();
} else {
// 如果刷新失败且重试次数少于2次则重试
if (retryCount < 2) {
setTimeout(() => {
this.refreshCurrentFolderCache(retryCount + 1);
}, 1000);
} else {
this.showToast('刷新失败:' + response.data.message);
}
}
})
.catch(error => {
console.error('刷新缓存失败:', error);
this.showToast('刷新缓存失败,请稍后重试');
// 如果网络错误且重试次数少于2次则重试
if (retryCount < 2) {
setTimeout(() => {
this.refreshCurrentFolderCache(retryCount + 1);
}, 1000);
} else {
this.showToast('刷新缓存失败,请稍后重试');
}
});
},
// 获取指定文件夹的上级目录ID
getParentFolderId(folderId) {
// 如果是根目录,没有上级目录
if (folderId === 'root' || folderId === '0') {
return null;
}
// 如果是当前目录源目录从fileManager.paths中获取上级目录
if (folderId === this.fileManager.currentFolder || folderId === (this.fileManager.currentFolder || "0")) {
if (this.fileManager.paths.length === 0) {
// 当前目录是根目录的直接子目录,上级是根目录
return 'root';
} else {
// 当前目录的上级是paths中的最后一个目录
return this.fileManager.paths[this.fileManager.paths.length - 1].fid;
}
}
// 如果是目标目录从fileSelect.paths中获取上级目录
if (this.fileSelect.paths && this.fileSelect.paths.length > 0) {
// 目标目录就是fileSelect.paths的最后一个目录其上级目录是倒数第二个
if (this.fileSelect.paths.length === 1) {
// 目标目录是根目录的直接子目录
return 'root';
} else {
// 目标目录的上级是paths中的倒数第二个目录
return this.fileSelect.paths[this.fileSelect.paths.length - 2].fid;
}
} else {
// 如果fileSelect.paths为空说明目标目录是根目录没有上级
return null;
}
},
// 刷新指定文件夹的缓存
refreshFolderCache(folderId) {
// 刷新指定目录的缓存,强制重新请求最新的文件列表
// 调用后端接口,添加强制刷新参数
const params = {
folder_id: folderId || 'root',
sort_by: this.fileManager.sortBy,
order: this.fileManager.sortOrder,
page_size: this.fileManager.pageSize,
page: 1, // 使用第一页来刷新缓存
account_index: this.fileManager.selectedAccountIndex,
force_refresh: true // 强制刷新参数
};
axios.get('/file_list', { params })
.then(response => {
// 如果刷新的是当前显示的文件夹,则更新显示
if (folderId === this.fileManager.currentFolder) {
if (response.data.success) {
this.fileManager.fileList = response.data.data.list;
this.fileManager.total = response.data.data.total;
this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
this.fileManager.paths = response.data.data.paths || [];
this.fileManager.gotoPage = this.fileManager.currentPage;
// 检测当前的命名模式
this.detectFileManagerNamingMode();
} else {
this.showToast('刷新失败:' + response.data.message);
}
}
// 如果不是当前文件夹,只是刷新缓存,不更新显示
})
.catch(error => {
console.error('刷新文件夹缓存失败:', error);
// 只有在刷新当前文件夹时才显示错误提示
if (folderId === this.fileManager.currentFolder) {
this.showToast('刷新缓存失败,请稍后重试');
}
});
},
// 新建文件夹
createNewFolder() {
// 检查是否有文件正在编辑状态
const editingFile = this.fileManager.fileList.find(file => file._editing);
if (editingFile) {
// 如果有其他文件正在编辑,先保存编辑状态
this.saveRenameFile(editingFile);
return;
}
// 生成带时间戳的文件夹名称
const timestamp = new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/[\/\s:]/g, '');
const folderName = `新建文件夹 ${timestamp}`;
// 调用后端API创建文件夹
axios.post('/create_folder', {
parent_folder_id: this.fileManager.currentFolder || '0',
folder_name: folderName,
account_index: this.fileManager.selectedAccountIndex
})
.then(response => {
if (response.data.success) {
// 创建成功,将新文件夹添加到文件列表
const newFolder = response.data.data;
// 设置编辑状态
this.$set(newFolder, '_editing', true);
this.$set(newFolder, '_editingName', newFolder.file_name);
// 将新文件夹添加到列表开头(文件夹通常显示在前面)
this.fileManager.fileList.unshift(newFolder);
this.fileManager.total += 1;
// 下一个tick后聚焦输入框并全选文本
this.$nextTick(() => {
const input = this.$refs['renameInput_' + newFolder.fid];
if (input) {
input.focus();
input.select();
}
});
this.showToast('文件夹创建成功');
} else {
this.showToast(response.data.message || '创建文件夹失败');
}
})
.catch(error => {
console.error('创建文件夹失败:', error);
this.showToast('创建文件夹失败');
});
},
// 添加一个检查分享链接状态的方法
@ -3106,9 +3294,9 @@
// 根据模态框类型决定使用哪个账号
// 任务配置相关的模态框始终使用主账号索引0
// 文件整理页面的预览模态框使用选中的账号
// 文件整理页面的预览模态框和移动文件模态框使用选中的账号
const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
const accountIndex = (modalType === 'preview-filemanager') ? this.fileManager.selectedAccountIndex : 0;
const accountIndex = (modalType === 'preview-filemanager' || modalType === 'move') ? this.fileManager.selectedAccountIndex : 0;
// 添加账号索引参数
if (typeof params === 'object' && params !== null) {
@ -3152,7 +3340,7 @@
this.fileSelect.fileList = [];
this.fileSelect.paths = [];
this.fileSelect.index = index;
// 重置排序状态为默认值
// 重置排序状态为默认值 - 选择需转存的文件夹模态框默认修改时间倒序
this.fileSelect.sortBy = "updated_at";
this.fileSelect.sortOrder = "desc";
@ -3171,21 +3359,43 @@
},
getShareDetail(retryCount = 0, maxRetries = 1) {
this.modalLoading = true;
// 检查index是否有效如果无效则使用默认值
let regexConfig = {};
if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
const task = this.formData.tasklist[this.fileSelect.index];
regexConfig = {
pattern: task.pattern,
replace: task.replace,
taskname: task.taskname,
filterwords: task.filterwords,
magic_regex: this.formData.magic_regex,
use_sequence_naming: task.use_sequence_naming,
sequence_naming: task.sequence_naming,
use_episode_naming: task.use_episode_naming,
episode_naming: task.episode_naming,
episode_patterns: this.formData.episode_patterns
};
} else {
// 使用默认配置
regexConfig = {
pattern: '',
replace: '',
taskname: '',
filterwords: '',
magic_regex: this.formData.magic_regex,
use_sequence_naming: false,
sequence_naming: '',
use_episode_naming: false,
episode_naming: '',
episode_patterns: this.formData.episode_patterns
};
}
axios.post('/get_share_detail', {
shareurl: this.fileSelect.shareurl,
stoken: this.fileSelect.stoken,
regex: {
pattern: this.formData.tasklist[this.fileSelect.index].pattern,
replace: this.formData.tasklist[this.fileSelect.index].replace,
taskname: this.formData.tasklist[this.fileSelect.index].taskname,
filterwords: this.formData.tasklist[this.fileSelect.index].filterwords,
magic_regex: this.formData.magic_regex,
use_sequence_naming: this.formData.tasklist[this.fileSelect.index].use_sequence_naming,
sequence_naming: this.formData.tasklist[this.fileSelect.index].sequence_naming,
use_episode_naming: this.formData.tasklist[this.fileSelect.index].use_episode_naming,
episode_naming: this.formData.tasklist[this.fileSelect.index].episode_naming,
episode_patterns: this.formData.episode_patterns
}
regex: regexConfig
}).then(response => {
if (response.data.success) {
this.fileSelect.fileList = response.data.data.list;
@ -3239,10 +3449,14 @@
// 命名预览模式下使用重命名列倒序排序
this.fileSelect.sortBy = "file_name_re";
this.fileSelect.sortOrder = "desc";
} else {
// 其他情况使用修改日期倒序排
} else if (this.fileSelect.selectDir) {
// 选择需转存的文件夹模态框:默认修改时间倒
this.fileSelect.sortBy = "updated_at";
this.fileSelect.sortOrder = "desc";
} else {
// 选择起始文件模态框:使用文件名倒序排序(使用全局文件排序函数)
this.fileSelect.sortBy = "file_name";
this.fileSelect.sortOrder = "desc";
}
if (this.getShareurl(this.fileSelect.shareurl) != this.getShareurl(this.formData.tasklist[index].shareurl)) {
this.fileSelect.stoken = "";
@ -3311,24 +3525,122 @@
}
},
selectCurrentFolder(addTaskname = false) {
if (this.fileSelect.selectShare) {
this.formData.tasklist[this.fileSelect.index].shareurl_ban = undefined;
// 如果是分享文件夹且文件列表为空则设置shareurl_ban
if (!this.fileSelect.fileList || this.fileSelect.fileList.length === 0) {
this.$set(this.formData.tasklist[this.fileSelect.index], "shareurl_ban", "该分享已被删除,无法访问");
if (this.fileSelect.moveMode) {
// 移动文件模式
this.moveFilesToCurrentFolder();
} else if (this.fileSelect.selectShare) {
// 检查index是否有效
if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
this.formData.tasklist[this.fileSelect.index].shareurl_ban = undefined;
// 如果是分享文件夹且文件列表为空则设置shareurl_ban
if (!this.fileSelect.fileList || this.fileSelect.fileList.length === 0) {
this.$set(this.formData.tasklist[this.fileSelect.index], "shareurl_ban", "该分享已被删除,无法访问");
}
this.formData.tasklist[this.fileSelect.index].shareurl = this.fileSelect.shareurl;
}
this.formData.tasklist[this.fileSelect.index].shareurl = this.fileSelect.shareurl;
} else {
// 去掉前导斜杠,避免双斜杠问题
this.formData.tasklist[this.fileSelect.index].savepath = this.fileSelect.paths.map(item => item.name).join("/");
if (addTaskname) {
this.formData.tasklist[this.fileSelect.index].savepath += "/" + this.formData.tasklist[this.fileSelect.index].taskname
// 检查index是否有效
if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
// 去掉前导斜杠,避免双斜杠问题
this.formData.tasklist[this.fileSelect.index].savepath = this.fileSelect.paths.map(item => item.name).join("/");
if (addTaskname) {
this.formData.tasklist[this.fileSelect.index].savepath += "/" + this.formData.tasklist[this.fileSelect.index].taskname
}
}
}
$('#fileSelectModal').modal('hide')
if (!this.fileSelect.moveMode) {
$('#fileSelectModal').modal('hide')
}
},
// 移动文件到当前文件夹
moveFilesToCurrentFolder() {
if (!this.fileSelect.moveFileIds || this.fileSelect.moveFileIds.length === 0) {
alert('没有选择要移动的文件');
return;
}
// 获取目标文件夹ID
const targetFolderId = this.fileSelect.paths.length > 0
? this.fileSelect.paths[this.fileSelect.paths.length - 1].fid
: "0";
// 记录源目录ID文件原本所在的目录
const sourceFolderId = this.fileManager.currentFolder || "0";
const fileCount = this.fileSelect.moveFileIds.length;
const confirmMessage = fileCount === 1
? '确定要移动此文件吗?'
: `确定要移动选中的 ${fileCount} 个文件吗?`;
if (!confirm(confirmMessage)) {
return;
}
// 调用移动文件API
axios.post('/move_file', {
file_ids: this.fileSelect.moveFileIds,
target_folder_id: targetFolderId,
account_index: this.fileManager.selectedAccountIndex
})
.then(response => {
if (response.data.success) {
this.showToast(`成功移动 ${response.data.moved_count} 个文件`);
// 关闭模态框
$('#fileSelectModal').modal('hide');
// 重置移动模式相关参数
this.fileSelect.moveMode = false;
this.fileSelect.moveFileIds = [];
this.fileSelect.index = null; // 重置index避免后续访问undefined
// 清空选中的文件
this.fileManager.selectedFiles = [];
// 延迟刷新以确保后端处理完成
setTimeout(() => {
// 刷新当前页面显示(源目录)
this.refreshCurrentFolderCache();
// 如果目标目录不同于源目录,也刷新目标目录的缓存
if (targetFolderId !== sourceFolderId) {
this.refreshFolderCache(targetFolderId); // 刷新目标目录缓存
}
// 刷新源目录的上级目录(更新源文件夹的项目数量)
const sourceParentFolderId = this.getParentFolderId(sourceFolderId);
if (sourceParentFolderId !== null) {
this.refreshFolderCache(sourceParentFolderId);
}
// 刷新目标目录的上级目录(更新目标文件夹的项目数量)
if (targetFolderId !== sourceFolderId) {
const targetParentFolderId = this.getParentFolderId(targetFolderId);
if (targetParentFolderId !== null && targetParentFolderId !== sourceParentFolderId) {
this.refreshFolderCache(targetParentFolderId);
}
}
}, 500); // 延迟500ms确保后端处理完成
} else {
const errorMessage = response.data.message || '移动失败';
// 对于"不能移动至相同目录"错误使用Toast通知
if (errorMessage.includes('不能移动至相同目录')) {
this.showToast('不能移动至相同目录');
} else {
this.showToast(errorMessage);
}
}
})
.catch(error => {
console.error('移动文件失败:', error);
this.showToast('移动文件失败');
});
},
selectStartFid(fid) {
Vue.set(this.formData.tasklist[this.fileSelect.index], 'startfid', fid);
// 检查index是否有效
if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
Vue.set(this.formData.tasklist[this.fileSelect.index], 'startfid', fid);
}
$('#fileSelectModal').modal('hide')
},
getShareurl(shareurl, path = {}) {
@ -3705,7 +4017,7 @@
}
}).then(response => {
if (response.data.success && response.data.data.all_task_names) {
this.allTaskNames = response.data.data.all_task_names.sort();
this.allTaskNames = this.sortTaskNamesByPinyin(response.data.data.all_task_names);
} else {
// 如果API失败回退到从当前页记录中提取任务名称
if (this.history.records && this.history.records.length > 0) {
@ -3715,7 +4027,7 @@
taskNames.add(record.task_name);
}
});
this.allTaskNames = [...taskNames].sort();
this.allTaskNames = this.sortTaskNamesByPinyin([...taskNames]);
}
}
}).catch(error => {
@ -3727,7 +4039,7 @@
taskNames.add(record.task_name);
}
});
this.allTaskNames = [...taskNames].sort();
this.allTaskNames = this.sortTaskNamesByPinyin([...taskNames]);
}
});
},
@ -3859,12 +4171,25 @@
// 文件夹始终在前
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
let aValue = a.file_name.toLowerCase();
let bValue = b.file_name.toLowerCase();
if (this.fileSelect.sortOrder === 'asc') {
return aValue > bValue ? 1 : -1;
// 检查当前模态框类型,选择起始文件模态框使用全局文件排序函数
const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
if (modalType === 'start-file') {
// 选择起始文件模态框:使用全局文件排序函数
const ka = sortFileByName(a), kb = sortFileByName(b);
for (let i = 0; i < ka.length; ++i) {
if (ka[i] !== kb[i]) return this.fileSelect.sortOrder === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
}
return 0;
} else {
return aValue < bValue ? 1 : -1;
// 其他模态框:使用拼音排序
let aValue = pinyinPro.pinyin(a.file_name, { toneType: 'none', type: 'string' }).toLowerCase();
let bValue = pinyinPro.pinyin(b.file_name, { toneType: 'none', type: 'string' }).toLowerCase();
if (this.fileSelect.sortOrder === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
}
}
if (field === 'file_name_re') {
@ -3884,12 +4209,19 @@
let aValue, bValue;
// 对于重命名列优先使用episode_number进行数值排序如果存在
if (a.episode_number !== undefined && b.episode_number !== undefined) {
aValue = a.episode_number;
bValue = b.episode_number;
// 确保进行数值比较
aValue = parseInt(a.episode_number, 10);
bValue = parseInt(b.episode_number, 10);
// 如果解析失败,回退到字符串比较
if (isNaN(aValue) || isNaN(bValue)) {
aValue = String(a.episode_number);
bValue = String(b.episode_number);
}
} else {
// 否则使用重命名后的文件名进行字符串排序
aValue = (a.file_name_re || '').toLowerCase();
bValue = (b.file_name_re || '').toLowerCase();
// 否则使用重命名后的文件名进行拼音排序
aValue = pinyinPro.pinyin(a.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
bValue = pinyinPro.pinyin(b.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
}
if (this.fileSelect.sortOrder === 'asc') {
@ -3934,8 +4266,9 @@
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
let aValue = a.dir ? 0 : (a.size || 0);
let bValue = b.dir ? 0 : (b.size || 0);
// 文件夹按项目数量排序,文件按大小排序
let aValue = a.dir ? (a.include_items || 0) : (a.size || 0);
let bValue = b.dir ? (b.include_items || 0) : (b.size || 0);
if (this.fileSelect.sortOrder === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
@ -3958,13 +4291,12 @@
if (field === 'file_name') {
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
let aValue = a.file_name.toLowerCase();
let bValue = b.file_name.toLowerCase();
if (order === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
// 使用智能排序函数
const ka = sortFileByName(a), kb = sortFileByName(b);
for (let i = 0; i < ka.length; ++i) {
if (ka[i] !== kb[i]) return order === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
}
return 0;
}
if (field === 'file_name_re') {
const aHasValidRename = a.file_name_re && a.file_name_re !== '×' && !a.file_name_re.startsWith('×');
@ -3983,12 +4315,19 @@
let aValue, bValue;
// 对于重命名列优先使用episode_number进行数值排序如果存在
if (a.episode_number !== undefined && b.episode_number !== undefined) {
aValue = a.episode_number;
bValue = b.episode_number;
// 确保进行数值比较
aValue = parseInt(a.episode_number, 10);
bValue = parseInt(b.episode_number, 10);
// 如果解析失败,回退到字符串比较
if (isNaN(aValue) || isNaN(bValue)) {
aValue = String(a.episode_number);
bValue = String(b.episode_number);
}
} else {
// 否则使用重命名后的文件名进行字符串排序
aValue = (a.file_name_re || '').toLowerCase();
bValue = (b.file_name_re || '').toLowerCase();
// 否则使用重命名后的文件名进行拼音排序
aValue = pinyinPro.pinyin(a.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
bValue = pinyinPro.pinyin(b.file_name_re || '', { toneType: 'none', type: 'string' }).toLowerCase();
}
if (order === 'asc') {
@ -4033,8 +4372,9 @@
if (a.dir && !b.dir) return -1;
if (!a.dir && b.dir) return 1;
let aValue = a.dir ? 0 : (a.size || 0);
let bValue = b.dir ? 0 : (b.size || 0);
// 文件夹按项目数量排序,文件按大小排序
let aValue = a.dir ? (a.include_items || 0) : (a.size || 0);
let bValue = b.dir ? (b.include_items || 0) : (b.size || 0);
if (order === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
@ -4890,8 +5230,11 @@
if (previewItem) {
// 设置重命名字段
file.file_name_re = previewItem.new_name;
// 设置集数字段用于排序
file.episode_number = previewItem.episode_number;
} else {
file.file_name_re = null;
file.episode_number = undefined;
}
});
@ -4987,6 +5330,39 @@
}
});
},
// 开始移动文件
startMoveFile(file) {
// 如果是文件夹,不允许移动
if (file.dir) {
this.showToast('暂不支持移动文件夹');
return;
}
// 设置移动模式相关参数
this.fileSelect.moveMode = true;
this.fileSelect.moveFileIds = this.fileManager.selectedFiles.length > 0 && this.fileManager.selectedFiles.includes(file.fid)
? this.fileManager.selectedFiles
: [file.fid];
this.fileSelect.selectShare = false;
this.fileSelect.selectDir = true;
this.fileSelect.previewRegex = false;
this.fileSelect.error = undefined;
this.fileSelect.fileList = [];
this.fileSelect.paths = [];
this.fileSelect.index = -1;
// 重置排序状态为默认值 - 选择移动目标文件夹模态框默认修改时间倒序
this.fileSelect.sortBy = "updated_at";
this.fileSelect.sortOrder = "desc";
// 设置模态框类型为move移动目标文件夹
document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'move');
$('#fileSelectModal').modal('show');
// 加载根目录
this.getSavepathDetail(0);
},
// 保存重命名
saveRenameFile(file) {
if (!file._editing) return;
@ -5490,6 +5866,9 @@
$('#fileSelectModal').on('hidden.bs.modal', () => {
this.fileSelect.selectedFiles = [];
this.fileSelect.lastSelectedFileIndex = -1;
// 重置移动模式相关参数
this.fileSelect.moveMode = false;
this.fileSelect.moveFileIds = [];
});
// 检查本地存储中的标签页状态
@ -5547,6 +5926,9 @@
$('#fileSelectModal').on('hidden.bs.modal', () => {
this.fileSelect.selectedFiles = [];
this.fileSelect.lastSelectedFileIndex = -1;
// 重置移动模式相关参数
this.fileSelect.moveMode = false;
this.fileSelect.moveFileIds = [];
});
window.addEventListener('beforeunload', this.handleBeforeUnload);

89
app/utils/pinyin_sort.py Normal file
View File

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
"""
拼音排序工具模块
用于将中文字符串转换为拼音后进行排序
"""
try:
from pypinyin import lazy_pinyin, Style
PYPINYIN_AVAILABLE = True
except ImportError:
PYPINYIN_AVAILABLE = False
print("Warning: pypinyin not available, falling back to simple string sort")
def to_pinyin_for_sort(text):
"""
将字符串转换为拼音用于排序
保留非中文字符只转换中文字符为拼音
Args:
text (str): 要转换的字符串
Returns:
str: 转换后的拼音字符串用于排序比较
"""
if not text:
return ''
if PYPINYIN_AVAILABLE:
# 使用pypinyin库进行转换
# Style.NORMAL: 普通风格,不带声调
# errors='default': 保留无法转换的字符
pinyin_list = lazy_pinyin(text, style=Style.NORMAL, errors='default')
return ''.join(pinyin_list).lower()
else:
# 如果pypinyin不可用直接返回小写字符串
return text.lower()
def pinyin_compare(a, b):
"""
拼音排序比较函数
Args:
a (str): 第一个字符串
b (str): 第二个字符串
Returns:
int: 比较结果 (-1, 0, 1)
"""
pinyin_a = to_pinyin_for_sort(a)
pinyin_b = to_pinyin_for_sort(b)
if pinyin_a < pinyin_b:
return -1
elif pinyin_a > pinyin_b:
return 1
else:
return 0
def get_filename_pinyin_sort_key(filename):
"""
为文件名生成拼音排序键
保持完整内容只将汉字转换为拼音
Args:
filename (str): 文件名
Returns:
str: 用于排序的拼音字符串
"""
return to_pinyin_for_sort(filename)
def pinyin_sort_files(files, key_func=None, reverse=False):
"""
使用拼音排序对文件列表进行排序
Args:
files (list): 文件列表
key_func (callable): 从文件对象中提取文件名的函数默认为None假设files是字符串列表
reverse (bool): 是否逆序排序
Returns:
list: 排序后的文件列表
"""
if key_func is None:
# 假设files是字符串列表
return sorted(files, key=to_pinyin_for_sort, reverse=reverse)
else:
# 使用key_func提取文件名后进行拼音排序
return sorted(files, key=lambda x: to_pinyin_for_sort(key_func(x)), reverse=reverse)

View File

@ -8,15 +8,33 @@ import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
from quark_auto_save import sort_file_by_name
from app.utils.pinyin_sort import get_filename_pinyin_sort_key
except ImportError:
# 如果无法导入,提供一个简单的排序函数作为替代
def sort_file_by_name(file):
if isinstance(file, dict):
filename = file.get("file_name", "")
update_time = file.get("updated_at", 0)
else:
filename = file
# 简单排序,主要通过文件名进行
return filename
update_time = 0
# 简单排序,主要通过文件名进行(使用拼音排序)
try:
from pypinyin import lazy_pinyin, Style
pinyin_list = lazy_pinyin(filename, style=Style.NORMAL, errors='ignore')
pinyin_sort_key = ''.join(pinyin_list).lower()
except ImportError:
pinyin_sort_key = filename.lower()
# 返回五级排序元组:(日期, 期数, 上中下, 修改时间, 拼音排序)
return (float('inf'), float('inf'), 0, update_time, pinyin_sort_key)
def get_filename_pinyin_sort_key(filename):
try:
from pypinyin import lazy_pinyin, Style
pinyin_list = lazy_pinyin(filename, style=Style.NORMAL, errors='ignore')
return ''.join(pinyin_list).lower()
except ImportError:
return filename.lower()
class Aria2:

File diff suppressed because it is too large Load Diff

View File

@ -2,3 +2,4 @@ flask
apscheduler
requests
treelib
pypinyin