Compare commits

...

9 Commits

Author SHA1 Message Date
x1ao4
664b818f03
Merge pull request #73 from x1ao4/dev
新增任务数量指示器和任务状态筛选功能
2025-11-25 15:52:34 +08:00
x1ao4
1dbdef2b0f
新增状态筛选功能说明 2025-11-25 15:36:25 +08:00
x1ao4
6c2e715bdf 修复任务列表顶部组件的间距问题 2025-11-25 15:07:50 +08:00
x1ao4
0c81dd48b7 为转存记录添加状态筛选功能 2025-11-25 15:01:27 +08:00
x1ao4
85da3ec023 为追剧日历添加状态筛选功能 2025-11-24 23:39:02 +08:00
x1ao4
b520378d26 新增任务状态筛选功能,修复窄屏下排序与分页的样式溢出问题
- 新增任务状态筛选下拉及逻辑,优化筛选布局与间距
- 修复排序、分页按钮在窄屏下文字溢出、间距异常等问题
2025-11-24 22:26:09 +08:00
x1ao4
93f9fbe4cb 调整任务数量指示器样式 2025-11-24 14:53:52 +08:00
x1ao4
710c91840f 在任务列表页面增加了任务数量指示器 2025-11-18 16:06:11 +08:00
x1ao4
1ca3230153 优化手动执行模式:支持忽略执行周期规则的单任务运行
- 新增 IGNORE_EXECUTION_RULES 环境变量,用于标记手动运行单个任务时忽略执行周期/进度限制
- run_script_now 在手动运行单个任务时设置该标记,并保留原始任务索引用于日志显示
- do_save 支持 ignore_execution_rules 参数,单任务手动运行时直接跳过执行周期与进度判断
- 保持手动运行 ALL 和定时任务的执行周期/进度规则不变,避免影响现有功能
2025-11-17 17:21:19 +08:00
6 changed files with 486 additions and 33 deletions

View File

@ -14,12 +14,13 @@
- **文件整理**:支持浏览和管理多个夸克账号的网盘文件,支持单项/批量重命名(支持应用完整的命名、过滤规则和撤销重命名等操作)、移动文件、删除文件、新建文件夹等操作。
- **更新状态**:支持在任务列表页面显示任务的最近更新日期、最近转存文件,支持在任务列表、转存记录、文件整理页面显示当日更新标识(对于当日更新的内容)。
- **影视发现**:支持在影视发现页面浏览豆瓣热门影视榜单,一键快速创建任务,智能填充任务配置,实现便捷订阅。
- **任务筛选**:支持在任务列表页面按照任务类型(如剧集、动画、综艺等)筛选任务,在对应的筛选视图下创建任务将会自动继承同类任务的基础配置,便于自动填充任务配置。
- **类型筛选**:支持在任务列表页面按照任务类型(如剧集、动画、综艺等)筛选任务,在对应的筛选视图下创建任务将会自动继承同类任务的基础配置,便于自动填充任务配置。
- **任务排序**:支持在任务列表页面按照任务编号、任务名称、任务进度和更新时间进行排序,支持升序和降序排列。
- **任务视图**:支持在任务列表页面使用列表视图和海报视图两种视图浏览和管理任务,海报视图将通过 TMDB 与任务对应的电视节目进行匹配,并显示对应的节目海报。
- **任务匹配**:在配置了 TMDB API 密钥后,所有任务将自动通过 TMDB 进行元数据匹配,匹配后将获取电视节目的海报和元数据,丰富任务相关信息的展示,包括集数信息统计、当前任务进度和电视节目状态等。
- **追剧日历**:支持在追剧日历页面查看和浏览任务对应电视节目的信息和播出时间表,追踪电视节目的播出情况,了解任务的完成进度。追剧日历支持海报视图和日历视图两种视图,可方便快捷的了解订阅内容的实时状态。
- **字幕命名规则**:支持在全局设置字幕文件的语言代码后缀,在重命名字幕文件时自动添加语言代码后缀,如 `.zh.srt`、`.zh.ass` 等。
- **状态筛选**:支持在任务列表、转存记录、追剧日历页面按照任务状态(进度状态或对应节目状态)筛选任务(记录或节目),便于筛除次要信息。
本项目修改后的版本为个人需求定制版,目的是满足我自己的使用需求,某些(我不用的)功能可能会因为修改而出现 BUG不一定会被修复。若你要使用本项目请知晓本人不是程序员我无法保证本项目的稳定性如果你在使用过程中发现了 BUG可以在 Issues 中提交,但不保证每个 BUG 都能被修复,请谨慎使用,风险自担。

View File

@ -1855,8 +1855,12 @@ def run_script_now():
if tasklist:
process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False)
# 添加原始任务索引的环境变量
if len(tasklist) == 1 and 'original_index' in request.json:
process_env["ORIGINAL_TASK_INDEX"] = str(request.json['original_index'])
if len(tasklist) == 1:
# 单任务手动运行:前端传入 original_index用于日志展示
if 'original_index' in request.json:
process_env["ORIGINAL_TASK_INDEX"] = str(request.json['original_index'])
# 单任务手动运行应忽略执行周期和任务进度限制包括自动模式下进度100%也要执行)
process_env["IGNORE_EXECUTION_RULES"] = "1"
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
@ -3622,6 +3626,18 @@ def get_history_records():
# 获取筛选参数
task_name_filter = request.args.get("task_name", "")
keyword_filter = request.args.get("keyword", "")
task_names_raw = request.args.get("task_names", "")
task_name_list = []
if task_names_raw:
try:
decoded_names = json.loads(task_names_raw)
if isinstance(decoded_names, list):
task_name_list = [
str(name).strip() for name in decoded_names
if isinstance(name, (str, bytes)) and str(name).strip()
]
except Exception:
task_name_list = []
# 是否只请求所有任务名称
get_all_task_names = request.args.get("get_all_task_names", "").lower() in ["true", "1", "yes"]
@ -3644,6 +3660,7 @@ def get_history_records():
order=order,
task_name_filter=task_name_filter,
keyword_filter=keyword_filter,
task_name_list=task_name_list,
exclude_task_names=["rename", "undo_rename"]
)
# 添加所有任务名称到结果中
@ -3665,6 +3682,7 @@ def get_history_records():
order=order,
task_name_filter=task_name_filter,
keyword_filter=keyword_filter,
task_name_list=task_name_list,
exclude_task_names=["rename", "undo_rename"]
)

View File

@ -167,7 +167,8 @@ class RecordDB:
@retry_on_locked(max_retries=3, base_delay=0.1)
def get_records(self, page=1, page_size=20, sort_by="transfer_time", order="desc",
task_name_filter="", keyword_filter="", exclude_task_names=None):
task_name_filter="", keyword_filter="", exclude_task_names=None,
task_name_list=None):
"""获取转存记录列表,支持分页、排序和筛选
Args:
@ -178,6 +179,7 @@ class RecordDB:
task_name_filter: 任务名称筛选条件精确匹配
keyword_filter: 关键字筛选条件模糊匹配任务名转存为名称
exclude_task_names: 需要排除的任务名称列表
task_name_list: 任务名称集合包含多个任务名时使用IN筛选
"""
cursor = self.conn.cursor()
offset = (page - 1) * page_size
@ -208,6 +210,11 @@ class RecordDB:
where_clauses.append("task_name NOT IN ({})".format(",".join(["?" for _ in exclude_task_names])))
params.extend(exclude_task_names)
if task_name_list:
placeholders = ",".join(["?" for _ in task_name_list])
where_clauses.append(f"task_name IN ({placeholders})")
params.extend(task_name_list)
where_clause = " AND ".join(where_clauses)
where_sql = f"WHERE {where_clause}" if where_clause else ""

View File

@ -3244,6 +3244,13 @@ div.jsoneditor-treepath * {
height: 32px;
display: flex;
align-items: center;
white-space: nowrap;
flex-shrink: 0;
}
.pagination-settings .btn {
white-space: nowrap;
flex-shrink: 0;
}
/* 分页下拉菜单样式 */
@ -7994,6 +8001,8 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
border-radius: 0 6px 6px 0;
background: var(--button-gray-background-color) !important;
font-size: 0.95rem; /* 与左侧类型按钮一致 */
white-space: nowrap;
flex-shrink: 0;
}
/* 兼容旧结构:若存在下拉菜单 DOM最小宽度不小于 160px */
@ -8101,6 +8110,37 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
top: 0.5px;
}
/* 任务列表:顶部筛选框列间距改为 8px */
.tasklist-filter-row {
margin-left: -4px;
margin-right: -4px;
row-gap: 8px;
}
.tasklist-filter-row > [class*="col-"] {
padding-left: 4px;
padding-right: 4px;
margin-bottom: 0 !important;
}
.tasklist-filter-row > [class*="col-"]:last-child {
margin-bottom: 0 !important;
}
@media (min-width: 768px) {
.tasklist-filter-row {
display: flex;
flex-wrap: nowrap;
}
.tasklist-filter-row > [class*="col-"] {
flex: 1 1 0;
max-width: 33.3333%;
min-width: 0;
}
.tasklist-filter-row .input-group {
width: 100%;
}
}
/* 任务列表视图切换按钮的图标大小,统一为与追剧日历相同 */
.tasklist-view-toggle-btn i.bi-grid-3x3-gap { /* 切换至海报视图图标 */
color: var(--dark-text-color);
@ -8339,4 +8379,40 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
.crontab-link:hover {
color: var(--focus-border-color);
text-decoration: none;
}
/* 任务列表:任务数量指示器 */
.tasklist-count-indicator {
min-width: 32px;
height: 32px;
border-radius: 6px;
border: 1px solid var(--border-color);
background-color: var(--button-gray-background-color);
color: var(--dark-text-color);
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 8px;
font-size: 0.95rem;
line-height: 32px;
user-select: none;
cursor: default;
flex-shrink: 0;
white-space: nowrap;
box-sizing: border-box;
}
/* 任务数量指示器悬停时显示文本光标,和其他标题框一致 */
.tasklist-count-single-digit {
width: 32px;
padding: 0;
}
.tasklist-count-number {
cursor: text;
user-select: text;
}
.calendar-filter-row {
margin-bottom: 20px; /* 桌面端保持与下方组件净间距 8px抵消分类与控制按钮的 -12px 上移) */
}

View File

@ -967,8 +967,8 @@
<div v-if="activeTab === 'tasklist'">
<div style="height: 20px;"></div>
<div class="row" style="margin-bottom: 8px;">
<div class="col-lg-6 col-md-6 mb-2 mb-md-0">
<div class="row tasklist-filter-row" style="margin-bottom: 8px;">
<div class="col-xl-4 col-lg-4 col-md-6 mb-2 mb-lg-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">名称筛选</span>
@ -979,14 +979,14 @@
</div>
</div>
</div>
<div class="col-lg-6 col-md-6">
<div class="col-xl-4 col-lg-4 col-md-6 mb-2 mb-lg-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">任务筛选</span>
</div>
<div class="position-relative" style="flex: 1;">
<select class="form-control task-filter-select" v-model="taskDirSelected" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 24px !important;">
<option value="">全部任务</option>
<select class="form-control task-filter-select" v-model="taskDirSelected" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 8px !important;">
<option value="">全部</option>
<option v-for="task in taskNames" :value="task" v-html="task"></option>
</select>
<!-- <i class="bi bi-chevron-down select-arrow" style="position: absolute; pointer-events: none; color: var(--dark-text-color);"></i> -->
@ -996,6 +996,30 @@
</div>
</div>
</div>
<div class="col-xl-4 col-lg-4 col-md-6">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">状态筛选</span>
</div>
<div class="position-relative" style="flex: 1;">
<select class="form-control task-filter-select"
v-model="taskStatusFilter"
style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 8px !important;">
<option value="">全部</option>
<option value="incomplete">未完成</option>
<option value="ongoing">进行中</option>
<option value="completed">已完成</option>
<option value="airing">播出中</option>
<option value="finale">本季终</option>
<option value="ended">已完结</option>
<option value="unmatched">未匹配</option>
</select>
</div>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('taskStatusFilter')"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
</div>
<!-- 任务列表:类型筛选按钮和排序组件(复用追剧日历移动端样式) -->
<div class="row mb-3 tasklist-header-row">
@ -1033,6 +1057,13 @@
{{ tasklistSort.order === 'asc' ? '升序排列' : '降序排列' }}
</span>
</div>
<!-- 任务数量显示 -->
<div
class="tasklist-count-indicator ml-2"
:class="{'tasklist-count-single-digit': tasklistVisibleCount < 10}"
:title="`共${tasklistVisibleCount}个任务`">
<span class="tasklist-count-number">{{ tasklistVisibleCount }}</span>
</div>
<!-- 视图切换按钮:列表视图 与 海报视图 -->
<div class="ml-2">
<button type="button"
@ -1057,7 +1088,7 @@
<!-- 任务列表:列表视图 -->
<template v-if="tasklist.viewMode === 'list'">
<div v-for="(task, index) in sortedTasklist" :key="'list-'+index" class="task mb-3" v-show="shouldShowTasklist">
<template v-if="(taskDirSelected == '' || task.taskname == taskDirSelected) && task.taskname.includes(taskNameFilter) && tasklistFilterByType(task)">
<template v-if="(taskDirSelected == '' || task.taskname == taskDirSelected) && task.taskname.includes(taskNameFilter) && tasklistFilterByType(task) && tasklistFilterByStatus(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">
@ -1276,7 +1307,7 @@
<div class="discovery-item"
v-for="(task, index) in sortedTasklist"
:key="'poster-'+(task.taskname || index)"
v-if="(taskDirSelected == '' || task.taskname == taskDirSelected) && task.taskname.includes(taskNameFilter) && tasklistFilterByType(task)">
v-if="(taskDirSelected == '' || task.taskname == taskDirSelected) && task.taskname.includes(taskNameFilter) && tasklistFilterByType(task) && tasklistFilterByStatus(task)">
<div class="discovery-poster" @mouseenter="handleManagementPosterHover($event, getCalendarTaskByName(task.taskname) || {})">
<img :src="getEpisodePosterUrl(getTasklistPosterLikeEpisode(task))"
:alt="task.taskname"
@ -1394,8 +1425,8 @@
<div v-if="activeTab === 'history'">
<div style="height: 20px;"></div>
<div class="row" style="margin-bottom: 20px;">
<div class="col-lg-6 col-md-6 mb-2 mb-md-0">
<div class="row tasklist-filter-row" style="margin-bottom: 20px;">
<div class="col-xl-4 col-lg-4 col-md-6 mb-2 mb-lg-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">名称筛选</span>
@ -1406,23 +1437,47 @@
</div>
</div>
</div>
<div class="col-lg-6 col-md-6">
<div class="col-xl-4 col-lg-4 col-md-6 mb-2 mb-lg-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">任务筛选</span>
</div>
<div class="position-relative" style="flex: 1;">
<select class="form-control task-filter-select" v-model="historyTaskSelected" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 24px !important;">
<option value="">全部任务</option>
<select class="form-control task-filter-select" v-model="historyTaskSelected" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 8px !important;">
<option value="">全部</option>
<option v-for="task in historyTasks" :value="task" v-html="task"></option>
</select>
<!-- <i class="bi bi-chevron-down select-arrow" style="position: absolute; pointer-events: none; color: var(--dark-text-color);"></i> -->
</div>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('historyTaskSelected')"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-4 col-md-6">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">状态筛选</span>
</div>
<div class="position-relative" style="flex: 1;">
<select class="form-control task-filter-select"
v-model="historyStatusFilter"
style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 8px !important;">
<option value="">全部</option>
<option value="incomplete">未完成</option>
<option value="ongoing">进行中</option>
<option value="completed">已完成</option>
<option value="airing">播出中</option>
<option value="finale">本季终</option>
<option value="ended">已完结</option>
<option value="unmatched">未匹配</option>
<option value="ended_task">已结束</option>
</select>
</div>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('historyStatusFilter')"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
</div>
<div class="table-responsive">
@ -1919,8 +1974,8 @@
<!-- 追剧日历内容 -->
<div v-else>
<!-- 顶部筛选和导航 -->
<div class="row calendar-filter-row" style="margin-bottom: 20px;">
<div class="col-lg-6 col-md-6 mb-2 mb-md-0">
<div class="row tasklist-filter-row calendar-filter-row">
<div class="col-xl-4 col-lg-4 col-md-6 mb-2 mb-lg-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">名称筛选</span>
@ -1931,14 +1986,14 @@
</div>
</div>
</div>
<div class="col-lg-6 col-md-6">
<div class="col-xl-4 col-lg-4 col-md-6 mb-2 mb-lg-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">任务筛选</span>
</div>
<div class="position-relative" style="flex: 1;">
<select class="form-control task-filter-select" v-model="calendar.taskFilter" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 24px !important;">
<option value="">全部任务</option>
<select class="form-control task-filter-select" v-model="calendar.taskFilter" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 8px !important;">
<option value="">全部</option>
<option v-for="task in calendar.taskNames" :value="task" v-html="task"></option>
</select>
</div>
@ -1947,6 +2002,30 @@
</div>
</div>
</div>
<div class="col-xl-4 col-lg-4 col-md-6 mb-2 mb-lg-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">状态筛选</span>
</div>
<div class="position-relative" style="flex: 1;">
<select class="form-control task-filter-select"
v-model="calendar.statusFilter"
style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 8px !important;">
<option value="">全部</option>
<option value="incomplete">未完成</option>
<option value="ongoing">进行中</option>
<option value="completed">已完成</option>
<option value="airing">播出中</option>
<option value="finale">本季终</option>
<option value="ended">已完结</option>
<option value="unmatched">未匹配</option>
</select>
</div>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearCalendarFilter('statusFilter')"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
</div>
<!-- 分类按钮和视图切换 -->
@ -2976,6 +3055,13 @@
taskDirs: [""],
taskDirSelected: "",
taskNameFilter: "",
taskStatusFilter: (() => {
try {
return localStorage.getItem('tasklist_status_filter') || '';
} catch (e) {
return '';
}
})(),
taskLatestRecords: {}, // 存储每个任务的最新转存记录日期
taskLatestFiles: {}, // 存储每个任务的最近转存文件
modalLoading: false,
@ -3058,6 +3144,13 @@
},
historyNameFilter: "",
historyTaskSelected: "",
historyStatusFilter: (() => {
try {
return localStorage.getItem('history_status_filter') || '';
} catch (e) {
return '';
}
})(),
gotoPage: 1,
totalPages: 1,
displayedPages: [],
@ -3065,6 +3158,9 @@
toastMessage: "",
selectedRecords: [],
lastSelectedRecordIndex: -1, // 记录最后选择的记录索引用于Shift选择
_lastTaskFilter: "",
_lastNameFilter: "",
_lastStatusFilter: "",
fileManager: {
hasLoaded: false, // 标记是否已完成首次加载
// 当前文件夹ID - 根据localStorage和账号索引决定初始目录
@ -3219,6 +3315,13 @@
selectedType: (localStorage.getItem('calendar_selected_type') || 'all'),
nameFilter: '',
taskFilter: '',
statusFilter: (() => {
try {
return localStorage.getItem('calendar_status_filter') || '';
} catch (e) {
return '';
}
})(),
taskNames: [],
viewMode: (localStorage.getItem('calendar_view_mode') === 'month' || localStorage.getItem('calendar_view_mode') === 'poster')
? localStorage.getItem('calendar_view_mode')
@ -3343,6 +3446,9 @@
if (this.calendar.taskFilter && this.calendar.taskFilter.trim() !== '') {
list = list.filter(t => (t.task_name || '') === this.calendar.taskFilter);
}
if (this.calendar.statusFilter && this.calendar.statusFilter.trim() !== '') {
list = list.filter(t => this.filterTaskByStatus(t, this.calendar.statusFilter));
}
// 按匹配状态和任务名称拼音排序:匹配的项目在前,未匹配的项目在后
try {
list.sort((a, b) => {
@ -3368,6 +3474,42 @@
return this.getSortedTasklist();
},
// 任务列表:可见任务集合(用于数量展示)
tasklistVisibleTasks() {
try {
if (!this.shouldShowTasklist) {
return [];
}
const keyword = this.taskNameFilter || '';
const selectedTaskName = this.taskDirSelected || '';
return (this.sortedTasklist || []).filter(task => {
if (!task) return false;
const taskName = task.taskname || '';
if (selectedTaskName && taskName !== selectedTaskName) {
return false;
}
if (keyword && !taskName.includes(keyword)) {
return false;
}
if (!this.tasklistFilterByType(task)) {
return false;
}
return this.tasklistFilterByStatus(task);
});
} catch (e) {
return [];
}
},
// 任务列表:当前可见任务数量
tasklistVisibleCount() {
try {
return this.tasklistVisibleTasks.length;
} catch (e) {
return 0;
}
},
// 判断是否应该显示任务列表(避免排序闪烁)
shouldShowTasklist() {
const { by } = this.tasklistSort || { by: 'index' };
@ -3458,7 +3600,6 @@
filteredHistoryRecords() {
// 直接返回服务器端已筛选的数据
if (!this.history.records || this.history.records.length === 0) {
return [];
}
@ -3576,6 +3717,19 @@
this.initializeCalendarDates();
}
},
'calendar.statusFilter': function(newVal, oldVal) {
if (this.calendar.viewMode === 'month') {
this.initializeCalendarDates();
}
try {
localStorage.setItem('calendar_status_filter', newVal || '');
} catch (e) {}
},
taskStatusFilter(newValue) {
try {
localStorage.setItem('tasklist_status_filter', newValue || '');
} catch (e) {}
},
// 侧边栏折叠/展开变化时,触发布局重算
sidebarCollapsed(val) {
if (this.activeTab === 'calendar') {
@ -3614,6 +3768,14 @@
this.loadHistoryRecords();
}
},
historyStatusFilter: {
handler(newVal) {
try {
localStorage.setItem('history_status_filter', newVal || '');
} catch (e) {}
this.loadHistoryRecords();
}
},
activeTab(newValue, oldValue) {
// 如果切换到任务列表页面,则刷新任务最新信息和元数据
if (newValue === 'tasklist') {
@ -4324,6 +4486,97 @@
return contentType === this.tasklist.selectedType;
} catch (e) { return true; }
},
// 统一的状态筛选逻辑(任务列表与追剧日历共用)
filterTaskByStatus(task, filterValue) {
try {
const filter = filterValue || '';
if (!filter) return true;
if (!task) return false;
const taskName = task.taskname || task.task_name || '';
const status = this.getTasklistFullStatus(task);
const progress = this.getTaskProgress && taskName ? this.getTaskProgress(taskName) : null;
const normalizedProgress = (progress === null || progress === undefined) ? null : Number(progress);
const isCompleted = this.isTaskCompletedByStatusAndProgress(status, normalizedProgress);
const isMatched = this.isTaskMatchedWithMetadata(task);
switch (filter) {
case 'incomplete':
return !isCompleted;
case 'ongoing':
return normalizedProgress === null || normalizedProgress < 100;
case 'completed':
return isCompleted;
case 'airing':
return status === '播出中';
case 'finale':
return status === '本季终';
case 'ended':
return status === '已完结';
case 'unmatched':
return !isMatched;
default:
return true;
}
} catch (e) {
return true;
}
},
tasklistFilterByStatus(task) {
return this.filterTaskByStatus(task, this.taskStatusFilter);
},
getTasklistFullStatus(task) {
try {
if (!task) return '';
const taskName = task.taskname || task.task_name || '';
const calTask = taskName ? this.getCalendarTaskByName(taskName) : null;
const candidates = [
calTask && calTask.matched_status,
calTask && calTask.status,
task.matched_status,
task.status,
((task.calendar_info || {}).match || {}).status,
((task.calendar_info || {}).extracted || {}).status
];
for (const val of candidates) {
if (val && String(val).trim() !== '') {
return String(val).trim();
}
}
return '';
} catch (e) {
return '';
}
},
isTaskMatchedWithMetadata(task) {
try {
if (!task) return false;
const taskName = task.taskname || task.task_name || '';
const calTask = taskName ? this.getCalendarTaskByName(taskName) : null;
const candidates = [
calTask && (calTask.match_tmdb_id || (calTask.match && calTask.match.tmdb_id) || calTask.tmdb_id),
task.match_tmdb_id,
(task.match && task.match.tmdb_id),
((task.calendar_info || {}).match || {}).tmdb_id,
task.tmdb_id
];
return candidates.some(id => {
if (id === null || id === undefined) return false;
const str = String(id).trim();
return str !== '';
});
} catch (e) {
return false;
}
},
isTaskCompletedByStatusAndProgress(status, progressValue) {
try {
const finalStatuses = ['本季终', '已完结', '已取消'];
if (!finalStatuses.includes(status)) return false;
if (progressValue === null || progressValue === undefined) return false;
return Number(progressValue) >= 100;
} catch (e) {
return false;
}
},
// 获取根据当前视图筛选条件过滤后的任务中编号最大的任务
getLastTaskByCurrentFilter() {
try {
@ -4843,10 +5096,9 @@
// 先进行基础筛选
let filteredEpisodes = this.calendar.episodes.filter(episode => {
// 根据筛选条件过滤
const matchedTask = this.findTaskByShowName(episode.show_name);
if (this.calendar.selectedType !== 'all') {
// 通过剧集名称匹配任务,获取内容类型
const matchedTask = this.findTaskByShowName(episode.show_name);
const episodeContentType = matchedTask ? matchedTask.content_type : 'other';
const episodeContentType = matchedTask ? (matchedTask.content_type || 'other') : 'other';
if (episodeContentType !== this.calendar.selectedType) {
return false;
}
@ -4863,12 +5115,16 @@
// 任务筛选:检查任务名称
if (this.calendar.taskFilter && this.calendar.taskFilter.trim() !== '') {
const matchedTask = this.findTaskByShowName(episode.show_name);
const taskName = matchedTask ? matchedTask.task_name : '';
const taskName = matchedTask ? (matchedTask.task_name || matchedTask.taskname || '') : '';
if (taskName !== this.calendar.taskFilter) {
return false;
}
}
// 状态筛选:复用任务列表逻辑
if (!this.filterTaskByStatus(matchedTask, this.calendar.statusFilter)) {
return false;
}
return episode.air_date === date;
});
@ -5824,6 +6080,8 @@
this.calendar.nameFilter = '';
} else if (filterType === 'taskFilter') {
this.calendar.taskFilter = '';
} else if (filterType === 'statusFilter') {
this.calendar.statusFilter = '';
}
},
@ -9004,10 +9262,16 @@
params.keyword = this.historyNameFilter;
}
// 追加状态筛选
if (!this.prepareStatusFilterParams(params, true)) {
return;
}
// 判断筛选条件是否变化,只有变化时才重置页码
const isFilterChanged =
(this._lastTaskFilter !== this.historyTaskSelected) ||
(this._lastNameFilter !== this.historyNameFilter);
(this._lastNameFilter !== this.historyNameFilter) ||
(this._lastStatusFilter !== this.historyStatusFilter);
if (isFilterChanged) {
// 筛选条件变化时重置为第一页
@ -9018,6 +9282,7 @@
// 记录当前筛选条件
this._lastTaskFilter = this.historyTaskSelected;
this._lastNameFilter = this.historyNameFilter;
this._lastStatusFilter = this.historyStatusFilter;
// 更新当前参数
this.historyParams = {
@ -9046,6 +9311,74 @@
this.history.hasLoaded = true;
});
},
prepareStatusFilterParams(params, handleEmptyResult = false) {
if (!this.historyStatusFilter) {
return true;
}
const taskNames = this.getStatusFilteredTaskNames(this.historyStatusFilter);
if (!taskNames || taskNames.length === 0) {
if (handleEmptyResult) {
this.handleEmptyHistoryResult(params.page_size);
}
return false;
}
params.task_names = JSON.stringify(taskNames);
return true;
},
getStatusFilteredTaskNames(filterValue) {
if (!filterValue) {
return [];
}
if (filterValue === 'ended_task') {
const allTaskNames = (this.allTaskNames && this.allTaskNames.length > 0)
? this.allTaskNames
: this.extractTaskNamesFromHistory();
if (!this.allTaskNames || this.allTaskNames.length === 0) {
this.loadAllTaskNames();
}
const currentNames = new Set();
(this.formData.tasklist || []).forEach(task => {
if (task && task.taskname) {
currentNames.add(task.taskname);
}
});
return allTaskNames.filter(name => name && !currentNames.has(name));
}
const matchedNames = new Set();
(this.formData.tasklist || []).forEach(task => {
if (!task || !task.taskname) {
return;
}
if (this.filterTaskByStatus(task, filterValue)) {
matchedNames.add(task.taskname);
}
});
return Array.from(matchedNames);
},
extractTaskNamesFromHistory() {
const names = new Set();
(this.history.records || []).forEach(record => {
if (record && record.task_name) {
names.add(record.task_name);
}
});
return Array.from(names);
},
handleEmptyHistoryResult(pageSize) {
const resolvedPageSize = pageSize || parseInt(this.historyParams.page_size || 15);
this.history.records = [];
this.history.pagination = {
total_records: 0,
total_pages: 1,
current_page: 1,
page_size: resolvedPageSize
};
this.history.hasLoaded = true;
this.totalPages = 1;
this.historyParams.page = 1;
this.gotoPage = 1;
this.selectedRecords = [];
},
sortHistory(field) {
// 更新排序参数
@ -9074,6 +9407,10 @@
params.keyword = this.historyNameFilter;
}
if (!this.prepareStatusFilterParams(params, true)) {
return;
}
// 重新加载数据,使用当前页和当前设置的排序方式
axios.get('/history_records', { params })
.then(response => {
@ -9116,6 +9453,10 @@
params.keyword = this.historyNameFilter;
}
if (!this.prepareStatusFilterParams(params, true)) {
return;
}
axios.get('/history_records', { params })
.then(response => {
if (response.data.success) {
@ -9155,6 +9496,10 @@
params.keyword = this.historyNameFilter;
}
if (!this.prepareStatusFilterParams(params, true)) {
return;
}
// 重新加载数据
axios.get('/history_records', { params })
.then(response => {
@ -9182,8 +9527,8 @@
},
getVisiblePageNumbers() {
const current = parseInt(this.historyParams.page) || 1;
const total = parseInt(this.totalPages) || 1;
const current = parseInt(this.historyParams.page) || 1;
// 根据屏幕宽度动态调整显示的页码数:移动端显示较少页码,桌面端显示较多页码
const isMobile = window.innerWidth <= 768;
const delta = isMobile ? 1 : 2; // 移动端左右各显示1个页码桌面端左右各显示2个页码

View File

@ -4466,7 +4466,7 @@ def do_sign(account):
print()
def do_save(account, tasklist=[]):
def do_save(account, tasklist=[], ignore_execution_rules=False):
print(f"🧩 载入插件")
plugins, CONFIG_DATA["plugins"], task_plugins_config = Config.load_plugins(
CONFIG_DATA.get("plugins", {})
@ -4479,6 +4479,9 @@ def do_save(account, tasklist=[]):
sent_notices = set()
def is_time(task):
# 若为手动单任务运行并明确要求忽略执行周期/进度限制,则始终执行
if ignore_execution_rules:
return True
# 获取任务的执行周期模式优先使用任务自身的execution_mode否则使用系统配置的execution_mode
execution_mode = task.get("execution_mode") or CONFIG_DATA.get("execution_mode", "manual")
@ -5892,9 +5895,12 @@ def main():
print(f"===============转存任务===============")
# 任务列表
if tasklist_from_env:
do_save(accounts[0], tasklist_from_env)
# 若通过环境变量传入任务列表,视为手动运行,可由外层控制是否忽略执行周期/进度限制
ignore_execution_rules = os.environ.get("IGNORE_EXECUTION_RULES", "").lower() in ["1", "true", "yes"]
do_save(accounts[0], tasklist_from_env, ignore_execution_rules=ignore_execution_rules)
else:
do_save(accounts[0], CONFIG_DATA.get("tasklist", []))
# 定时任务或命令行全量运行,始终遵循执行周期/进度规则
do_save(accounts[0], CONFIG_DATA.get("tasklist", []), ignore_execution_rules=False)
print()
# 通知
if NOTIFYS: