在任务列表页面新增排序功能

This commit is contained in:
x1ao4 2025-09-10 02:49:45 +08:00
parent 591c9e9fe1
commit d8749ff69f
2 changed files with 323 additions and 22 deletions

View File

@ -7743,6 +7743,167 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
/* 任务列表:类型筛选按钮与上方名称筛选区域的间距 */
.tasklist-type-filter {
margin-top: 8px;
margin-bottom: 20px;
}
margin-top: 0px; /* 上移8px从8px调整为0px */
margin-bottom: 4px;
}
/* 排序模块内部的行容器,承载三段控件 */
.tasklist-sort-wrapper {
display: flex;
align-items: center;
position: relative;
}
/* spacer撑开空间以将控件推至右侧 */
.tasklist-sort-spacer {
flex: 1 0 auto;
}
/* 三段胶囊通用样式(灰底、统一高度/字号) */
.tasklist-sort-pill {
background-color: var(--button-gray-background-color) !important;
color: var(--dark-text-color);
border: none !important;
display: inline-flex;
align-items: center;
height: 32px;
line-height: 32px;
margin: 0 !important;
font-size: 0.95rem; /* 与左侧类型按钮一致 */
}
/* 左侧“按”按钮:保留边框,去右边框,与中间边框重叠 */
.tasklist-sort-pill-icon {
width: 31px;
min-width: 31px;
justify-content: center;
border: 1px solid var(--border-color) !important;
border-right: none !important;
border-radius: 6px 0 0 6px;
background-color: var(--button-gray-background-color);
}
/* 中间下拉:白底,负责显示唯一可见边框(上下左右均有) */
.tasklist-sort-select {
padding: 0 8px;
border: 1px solid var(--border-color) !important;
border-radius: 0;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: #fff !important;
cursor: pointer;
background-image: none;
height: 32px;
line-height: 30px;
padding-top: 0 !important;
padding-bottom: 0 !important;
box-sizing: border-box !important;
vertical-align: middle;
display: inline-flex;
align-items: center;
font-size: 0.95rem; /* 与左侧类型按钮一致 */
}
/* 隐藏 IE 默认下拉箭头 */
.tasklist-sort-select::-ms-expand {
display: none;
}
/* 聚焦高亮,保持与任务筛选一致 */
.tasklist-sort-select:focus {
outline: none;
box-shadow: none;
border-color: var(--focus-border-color) !important; /* 聚焦时四边统一为主题色 */
background: #fff;
}
/* 下拉聚焦时,右侧按钮与其接缝的左边框采用同色,视觉连贯 */
.tasklist-sort-select:focus + .tasklist-sort-order {
border-left-color: var(--focus-border-color) !important; /* 接缝同色,视觉连贯 */
}
/* 右侧"升/降序排列"按钮:保留边框,去左边框,与中间重叠 */
.tasklist-sort-order {
padding: 0 8px;
cursor: pointer;
border: 1px solid var(--border-color) !important;
border-left: none !important;
border-radius: 0 6px 6px 0;
background: var(--button-gray-background-color) !important;
font-size: 0.95rem; /* 与左侧类型按钮一致 */
}
/* 兼容旧结构:若存在下拉菜单 DOM最小宽度不小于 160px */
.tasklist-sort-dropdown .dropdown-menu {
min-width: 160px;
}
/* 任务列表:移动端样式优化(复用追剧日历样式) */
@media (max-width: 768px) {
/* 任务列表:顶部一行(类型筛选 + 排序)不换行 */
.tasklist-header-row {
flex-wrap: nowrap !important;
align-items: center;
overflow-x: auto !important; /* 整行作为滚动容器 */
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
column-gap: 8px; /* 左右两组之间 8px 间距 */
margin-top: 0px !important;
/* 去除内边距,使用遮罩实现两侧 20px 的"硬遮挡"效果(无渐变) */
padding-left: 0;
padding-right: 0;
-webkit-mask-image: linear-gradient(to right,
transparent 0,
transparent 15px,
black 15px,
black calc(100% - 15px),
transparent calc(100% - 15px),
transparent 100%
);
mask-image: linear-gradient(to right,
transparent 0,
transparent 15px,
black 15px,
black calc(100% - 15px),
transparent calc(100% - 15px),
transparent 100%
);
}
.tasklist-header-row::-webkit-scrollbar { display: none; }
/* 两列都按内容宽度不允许被压缩到0通过外层行滚动 */
.tasklist-header-row > .col-lg-8.col-md-6,
.tasklist-header-row > .col-lg-4.col-md-6 {
flex: 0 0 auto !important;
width: auto !important;
max-width: none !important;
}
/* 去除列内侧补白,避免视觉间距异常 */
.tasklist-header-row > .col-lg-8.col-md-6 { padding-right: 0 !important; }
.tasklist-header-row > .col-lg-4.col-md-6 { padding-left: 0 !important; }
/* 左侧类型筛选按钮容器:保持单行(滚动交给外层行)*/
.tasklist-header-row .calendar-category-buttons {
flex-wrap: nowrap !important;
margin-top: 0 !important; /* 覆盖全局 -12px避免被遮挡 */
}
/* 右侧排序组件容器:强制单行,超出部分横向滚动,隐藏滚动条 */
.tasklist-sort-controls {
width: auto !important;
max-width: 100% !important;
flex-wrap: nowrap !important;
overflow-x: auto !important;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
margin-top: -4px !important;
}
.tasklist-sort-controls::-webkit-scrollbar { display: none; }
/* 排序组件内部容器:保持紧凑布局 */
.tasklist-sort-controls .tasklist-sort-wrapper {
flex-shrink: 0;
}
}

View File

@ -949,30 +949,52 @@
</div>
</div>
</div>
<!-- 任务列表:类型筛选按钮(复用追剧日历样式) -->
<div class="calendar-category-buttons tasklist-type-filter">
<button type="button"
class="btn btn-outline-secondary calendar-category-btn"
:class="{ active: tasklist.selectedType === 'all' }"
@click="selectTasklistType('all')">
全部
</button>
<button type="button"
class="btn btn-outline-secondary calendar-category-btn"
v-for="type in tasklist.contentTypes"
:key="'tasklist-'+type"
:class="{ active: tasklist.selectedType === type }"
@click="selectTasklistType(type)">
{{ getContentTypeDisplayName(type) }}
</button>
<!-- 任务列表:类型筛选按钮和排序组件(复用追剧日历移动端样式) -->
<div class="row mb-3 tasklist-header-row">
<div class="col-lg-8 col-md-6">
<!-- 类型筛选按钮 -->
<div class="calendar-category-buttons tasklist-type-filter">
<button type="button"
class="btn btn-outline-secondary calendar-category-btn"
:class="{ active: tasklist.selectedType === 'all' }"
@click="selectTasklistType('all')">
全部
</button>
<button type="button"
class="btn btn-outline-secondary calendar-category-btn"
v-for="type in tasklist.contentTypes"
:key="'tasklist-'+type"
:class="{ active: tasklist.selectedType === type }"
@click="selectTasklistType(type)">
{{ getContentTypeDisplayName(type) }}
</button>
</div>
</div>
<div class="col-lg-4 col-md-6">
<!-- 排序组件 -->
<div class="tasklist-sort-controls d-flex justify-content-end">
<div class="tasklist-sort-wrapper">
<span class="tasklist-sort-pill tasklist-sort-pill-icon" title="切换排序"></span>
<select class="tasklist-sort-pill tasklist-sort-select task-filter-select" :value="tasklistSort.by" @change="changeTasklistSortBy($event.target.value)" title="选择排序方式">
<option value="index">任务编号</option>
<option value="name">任务名称</option>
<option v-if="formData.tmdb_api_key" value="progress">任务进度</option>
<option value="update_time">更新时间</option>
</select>
<span class="tasklist-sort-pill tasklist-sort-order" @click="toggleTasklistSortOrder" title="切换升降序">
{{ tasklistSort.order === 'asc' ? '升序排列' : '降序排列' }}
</span>
</div>
</div>
</div>
</div>
<div v-for="(task, index) in formData.tasklist" :key="index" class="task mb-3">
<div v-for="(task, index) in sortedTasklist" :key="index" class="task mb-3" v-show="shouldShowTasklist">
<template v-if="(taskDirSelected == '' || task.taskname == taskDirSelected) && task.taskname.includes(taskNameFilter) && tasklistFilterByType(task)">
<hr>
<div class="form-group row" style="align-items:center">
<div class="col pl-0" data-toggle="collapse" :data-target="'#collapse_'+index" aria-expanded="true" :aria-controls="'collapse_'+index">
<div class="btn btn-block text-left">
<i class="bi bi-caret-right-fill"></i> #<span v-html="`${String(index+1).padStart(2, '0')} ${task.taskname}`"></span>
<i class="bi bi-caret-right-fill"></i> #<span v-html="`${String((task.__originalIndex !== undefined ? task.__originalIndex : index) + 1).padStart(2, '0')} ${task.taskname}`"></span>
<template v-for="key in formData.button_display_order">
<span v-if="key==='latest_transfer_file' && formData.button_display.latest_transfer_file !== 'disabled' && taskLatestFiles[task.taskname]"
class="task-latest-file"
@ -2969,6 +2991,18 @@
selectedType: (localStorage.getItem('tasklist_selected_type') || 'all'),
contentTypes: []
},
// 任务列表排序设置记忆到localStorage
tasklistSort: (() => {
try {
const saved = JSON.parse(localStorage.getItem('tasklist_sort_options') || '{}');
let by = ['index','name','progress','update_time'].includes(saved.by) ? saved.by : 'index';
const order = (saved.order === 'asc' || saved.order === 'desc') ? saved.order : 'asc';
// 注意:这里无法检查 TMDB API 配置,会在 mounted 中处理
return { by, order };
} catch (e) {
return { by: 'index', order: 'asc' };
}
})(),
// 日历页面resize监听器
calendarResizeHandler: null,
// 日历自动检测更新相关
@ -3054,6 +3088,35 @@
return list;
},
// 任务列表:带排序的视图数据(使用计算属性,不修改原始数据)
sortedTasklist() {
return this.getSortedTasklist();
},
// 判断是否应该显示任务列表(避免排序闪烁)
shouldShowTasklist() {
const { by } = this.tasklistSort || { by: 'index' };
// 任务编号和任务名称排序不需要额外数据,可以直接显示
if (by === 'index' || by === 'name') {
return true;
}
// 任务进度排序需要 taskLatestFiles 和 calendar.tasks 数据
if (by === 'progress') {
return this.taskLatestFiles && Object.keys(this.taskLatestFiles).length > 0 &&
this.calendar.tasks && Array.isArray(this.calendar.tasks) && this.calendar.tasks.length > 0;
}
// 更新时间排序需要 taskLatestRecords 数据
if (by === 'update_time') {
return this.taskLatestRecords && Object.keys(this.taskLatestRecords).length > 0;
}
// 默认显示
return true;
},
// 海报和日历视图下过滤掉所有内容都是未匹配的分类
filteredContentTypes() {
if (!this.calendar.tasks || this.calendar.tasks.length === 0) {
@ -3519,6 +3582,79 @@
this.stopCalendarAutoWatch();
},
methods: {
// 任务列表排序:获取显示名称
getTasklistSortDisplayName(key) {
if (key === 'name') return '任务名称';
if (key === 'progress') return '任务进度';
if (key === 'update_time') return '更新时间';
return '任务编号';
},
// 任务列表排序保存到localStorage
saveTasklistSort() {
try {
localStorage.setItem('tasklist_sort_options', JSON.stringify({ by: this.tasklistSort.by, order: this.tasklistSort.order }));
} catch (e) {}
},
// 任务列表排序:切换排序字段
// 仅切换排序字段,不改变当前升降序
changeTasklistSortBy(by) {
if (!['index','name','progress','update_time'].includes(by)) return;
if (this.tasklistSort.by !== by) {
this.tasklistSort.by = by;
// 不重置、不切换 order保持现状
this.saveTasklistSort();
// 不再调用 applyTasklistSort因为现在使用计算属性
}
},
// 任务列表排序:切换升降序
toggleTasklistSortOrder() {
this.tasklistSort.order = this.tasklistSort.order === 'asc' ? 'desc' : 'asc';
this.saveTasklistSort();
// 不再调用 applyTasklistSort因为现在使用计算属性
},
// 获取排序后的任务列表(不修改原始数据,避免触发 configModified
getSortedTasklist() {
try {
const { by, order } = this.tasklistSort || { by: 'index', order: 'asc' };
const factor = order === 'desc' ? -1 : 1;
// 建立稳定排序:附带原始索引,避免相等时抖动
const withIndex = (this.formData.tasklist || []).map((t, idx) => ({ t, idx }));
withIndex.sort((a, b) => {
let cmp = 0;
if (by === 'name') {
const an = (a.t.taskname || '').toString();
const bn = (b.t.taskname || '').toString();
try {
const ak = pinyinPro.pinyin(an, { toneType: 'none', type: 'string' }).toLowerCase();
const bk = pinyinPro.pinyin(bn, { toneType: 'none', type: 'string' }).toLowerCase();
if (ak < bk) cmp = -1; else if (ak > bk) cmp = 1; else cmp = 0;
} catch (e) { cmp = an.localeCompare(bn); }
} else if (by === 'progress') {
const ap = (this.getTaskProgress && this.getTaskProgress(a.t.taskname) != null) ? Number(this.getTaskProgress(a.t.taskname)) : -1;
const bp = (this.getTaskProgress && this.getTaskProgress(b.t.taskname) != null) ? Number(this.getTaskProgress(b.t.taskname)) : -1;
cmp = ap - bp;
} else if (by === 'update_time') {
// 按任务最近转存时间排序
const aRecord = this.taskLatestRecords[a.t.taskname];
const bRecord = this.taskLatestRecords[b.t.taskname];
const aTime = aRecord && aRecord.full ? new Date(aRecord.full).getTime() : 0;
const bTime = bRecord && bRecord.full ? new Date(bRecord.full).getTime() : 0;
cmp = aTime - bTime;
} else {
// 按任务名称中的编号排序(解析 #XX 格式)
const aNum = parseInt((a.t.taskname || '').match(/^#?(\d+)/)?.[1] || '0');
const bNum = parseInt((b.t.taskname || '').match(/^#?(\d+)/)?.[1] || '0');
cmp = aNum - bNum;
}
if (cmp === 0) cmp = a.idx - b.idx; // 稳定
return cmp * factor;
});
// 返回带原始索引的任务对象
return withIndex.map(x => ({ ...x.t, __originalIndex: x.idx }));
} catch (e) {
return (this.formData.tasklist || []).map((t, idx) => ({ ...t, __originalIndex: idx }));
}
},
// 选择日历日期
selectCalendarDate(day) {
try {
@ -6022,8 +6158,11 @@
return task;
});
// 直接赋值,排序通过计算属性处理
this.formData.tasklist = config_data.tasklist || [];
// 获取所有任务父目录
config_data.tasklist.forEach(item => {
this.formData.tasklist.forEach(item => {
parentDir = this.getParentDirectory(item.savepath)
if (!this.taskDirs.includes(parentDir))
this.taskDirs.push(parentDir);
@ -6202,6 +6341,7 @@
}
}
this.formData = config_data;
setTimeout(() => {
this.configModified = false;
}, 100);