为任务列表新增了海报视图,可切换显示模式

This commit is contained in:
x1ao4 2025-09-12 02:57:23 +08:00
parent a2af2dcbe0
commit a51cd1251a
2 changed files with 248 additions and 9 deletions

View File

@ -6618,6 +6618,24 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil
color: white;
}
/* 新增:按钮行容器,使用相对定位+inline-flex保证自动补位 */
.discovery-actions-row {
position: absolute;
left: 8px;
right: 8px;
height: 22px;
display: inline-flex;
gap: 6px;
}
/* 行内部的按钮相对定位使其不受按钮自身left样式影响已覆盖 */
.discovery-actions-row > .discovery-refresh-metadata,
.discovery-actions-row > .discovery-edit-metadata {
position: relative;
top: 0;
left: 0;
}
.discovery-info {
padding: 0 0px;
text-align: left;
@ -7914,3 +7932,50 @@ div:has(> .collapse:not(.show)):has(+ .row.title[title^="资源搜索"]) {
flex-shrink: 0;
}
}
/* 海报卡片:悬停时隐藏带有 hide-on-hover 类的徽标(如任务编号/进度) */
.discovery-poster:hover .hide-on-hover { opacity: 0; transition: opacity 0.15s ease-in-out; }
.discovery-poster .hide-on-hover { opacity: 1; transition: opacity 0.15s ease-in-out; }
/* 任务列表海报视图:操作按钮图标大小可定制 */
.tasklist-run-btn i {
font-size: 0.94rem;
}
.tasklist-delete-btn i {
font-size: 0.73rem;
position: relative;
top: 0.5px;
}
.tasklist-edit-metadata-btn i {
font-size: 0.83rem;
position: relative;
top: 1px;
left: 0.5px;
}
/* 任务列表视图切换按钮的图标大小,统一为与追剧日历相同 */
.tasklist-view-toggle-btn i.bi-grid-3x3-gap { /* 切换至海报视图图标 */
color: var(--dark-text-color);
font-size: 0.98rem; /* 与 .bi-grid-3x3-gap 统一 */
}
.tasklist-view-toggle-btn i.bi-list-ol { /* 切换至列表视图图标 */
color: var(--dark-text-color);
font-size: 1.1rem; /* 与网格图标保持一致的视觉尺寸 */
position: relative;
top: 0.5px;
}
.tasklist-view-toggle-btn.btn.btn-outline-secondary:hover i.bi-grid-3x3-gap,
.tasklist-view-toggle-btn.btn.btn-outline-secondary:hover i.bi-list-ol {
color: #fff; /* 与日历按钮 hover 一致 */
}
/* 任务列表海报视图:统一集数“/”分隔符的上移微调,与列表视图一致 */
.tasklist-poster-mode .discovery-genre .count-slash {
position: relative;
top: -2px;
display: inline-block;
font-size: 0.8em; /* 与任务列表相同的斜杠缩小比例 */
font-weight: 600; /* 适度加粗,与列表视觉一致 */
}

View File

@ -985,10 +985,21 @@
{{ tasklistSort.order === 'asc' ? '升序排列' : '降序排列' }}
</span>
</div>
<!-- 视图切换按钮:列表视图 与 海报视图 -->
<div class="ml-2">
<button type="button"
class="btn btn-outline-secondary btn-sm tasklist-view-toggle-btn"
@click="toggleTasklistViewMode"
:title="tasklist.viewMode === 'list' ? '当前:列表视图,点击切换到海报视图' : '当前:海报视图,点击切换到列表视图'">
<i :class="tasklist.viewMode === 'list' ? 'bi bi-grid-3x3-gap' : 'bi bi-list-ol'"></i>
</button>
</div>
</div>
</div>
<div v-for="(task, index) in sortedTasklist" :key="index" class="task mb-3" v-show="shouldShowTasklist">
</div>
<!-- 任务列表:列表视图 -->
<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)">
<hr>
<div class="form-group row" style="align-items:center">
@ -1031,13 +1042,13 @@
</div>
<div class="col-auto task-buttons">
<template v-for="key in formData.button_display_order">
<button v-if="key==='refresh_plex' && 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'" type="button" class="btn btn-outline-plex" :class="{'hover-only': formData.button_display.refresh_plex === 'hover'}" @click="refreshPlexLibrary(index)" title="刷新Plex媒体库"><img src="./static/images/Plex.svg" class="plex-icon"></button>
<button v-else-if="key==='refresh_alist' && formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" type="button" class="btn btn-outline-alist" :class="{'hover-only': formData.button_display.refresh_alist === 'hover'}" @click="refreshAlistDirectory(index)" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
<button v-if="key==='refresh_plex' && 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'" type="button" class="btn btn-outline-plex" :class="{'hover-only': formData.button_display.refresh_plex === 'hover'}" @click="refreshPlexLibrary(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="刷新Plex媒体库"><img src="./static/images/Plex.svg" class="plex-icon"></button>
<button v-else-if="key==='refresh_alist' && formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" type="button" class="btn btn-outline-alist" :class="{'hover-only': formData.button_display.refresh_alist === 'hover'}" @click="refreshAlistDirectory(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
<template v-else-if="key==='run_task'">
<button v-if="!task.shareurl_ban" type="button" class="btn btn-outline-primary" :class="{'hover-only': formData.button_display.run_task === 'hover'}" @click="runScriptNow(index)" title="运行此任务"><i class="bi bi-caret-right"></i></button>
<button v-if="!task.shareurl_ban" type="button" class="btn btn-outline-primary" :class="{'hover-only': formData.button_display.run_task === 'hover'}" @click="runScriptNow(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="运行此任务"><i class="bi bi-caret-right"></i></button>
<button v-else type="button" class="btn btn-warning" :title="formatShareUrlBanMessage(task.shareurl_ban)" disabled><i class="bi bi-exclamation-circle"></i></button>
</template>
<button v-else-if="key==='delete_task'" type="button" class="btn btn-outline-danger" :class="{'hover-only': formData.button_display.delete_task === 'hover'}" @click="removeTask(index)" title="删除此任务"><i class="bi bi-trash3"></i></button>
<button v-else-if="key==='delete_task'" type="button" class="btn btn-outline-danger" :class="{'hover-only': formData.button_display.delete_task === 'hover'}" @click="removeTask(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="删除此任务"><i class="bi bi-trash3"></i></button>
</template>
</div>
</div>
@ -1196,8 +1207,97 @@
</div>
</template>
</div>
<hr v-if="formData.tasklist.length > 0" class="task-divider">
<div class="row">
</template>
<!-- 任务列表:海报视图 -->
<div v-else class="tasklist-poster-mode">
<!-- 复用管理视图的海报网格样式与自适应布局 -->
<div class="discovery-grid">
<div class="discovery-item"
v-for="(task, index) in sortedTasklist"
:key="'poster-'+index"
v-if="(taskDirSelected == '' || task.taskname == taskDirSelected) && task.taskname.includes(taskNameFilter) && tasklistFilterByType(task)">
<div class="discovery-poster" @mouseenter="handleManagementPosterHover($event, getCalendarTaskByName(task.taskname) || {})">
<img :src="getEpisodePosterUrl(getTasklistPosterLikeEpisode(task))"
:alt="(getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).matched_show_name) ? getCalendarTaskByName(task.taskname).matched_show_name : task.taskname"
referrerpolicy="no-referrer"
crossorigin="anonymous"
@error="handleImageError($event)">
<!-- 按钮行容器:自动补位布局 -->
<div class="discovery-actions-row" style="top: 8px;">
<div class="discovery-refresh-metadata tasklist-run-btn" @click.stop="runScriptNow(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="运行此任务">
<i class="bi bi-caret-right"></i>
</div>
<div class="discovery-edit-metadata tasklist-delete-btn" @click.stop="removeTask(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="删除此任务">
<i class="bi bi-trash3"></i>
</div>
</div>
<div class="discovery-actions-row" style="top: 36px;">
<div class="discovery-refresh-metadata" v-if="getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).match_tmdb_id" @click="refreshSeasonMetadata(getCalendarTaskByName(task.taskname))" title="刷新元数据">
<i class="bi bi-arrow-clockwise"></i>
</div>
<div class="discovery-edit-metadata tasklist-edit-metadata-btn" @click.stop="openEditMetadataModal(getCalendarTaskByName(task.taskname) || { task_name: task.taskname })" title="编辑元数据">
<i class="bi bi-tag"></i>
</div>
</div>
<!-- 转存进度徽标(复用评分样式) -->
<div class="discovery-rating"
v-if="getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).matched_show_name && getCalendarTaskByName(task.taskname).season_counts"
:class="getProgressBadgeClass(getCalendarTaskByName(task.taskname))"
:title="'已转存/已播出:' + getTaskTransferredCount(getCalendarTaskByName(task.taskname)) + '/' + getTaskAiredCount(getCalendarTaskByName(task.taskname))">
{{ getTransferProgress(getCalendarTaskByName(task.taskname)) }}%
</div>
<!-- 左上角任务编号徽标:按需求移除 -->
<!-- 海报悬停信息 -->
<div class="discovery-poster-overlay">
<!-- 任务编号 -->
<div class="info-line">#{{ String((task.__originalIndex !== undefined ? task.__originalIndex : index) + 1).padStart(2, '0') }}</div>
<!-- 匹配的剧名 -->
<div class="info-line">
<template v-if="getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).matched_show_name">
{{ getCalendarTaskByName(task.taskname).matched_show_name }}
</template>
<template v-else>
未匹配
</template>
</div>
<!-- 最近转存文件 -->
<div class="info-line" v-if="taskLatestFiles[task.taskname]">{{ taskLatestFiles[task.taskname] }}</div>
<!-- 最近更新日期 -->
<div class="info-line" v-if="taskLatestRecords[task.taskname]">{{ getTaskLatestRecordDisplay(task.taskname) }}</div>
</div>
</div>
<div class="discovery-info">
<div class="discovery-title" :title="getTasklistPosterTitle(task, index)" style="cursor: default;">
{{ task.taskname }}
<span v-if="getTaskShowStatus(task.taskname)"> · {{ getTaskShowStatus(task.taskname) }}</span>
<span v-if="isTaskUpdatedToday(task.taskname) && shouldShowTodayIndicator()"
class="task-today-indicator"
:class="getTodayIndicatorClass()">
<i class="bi bi-stars"></i>
</span>
</div>
<div class="discovery-genre"
:title="(getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).matched_show_name && getCalendarTaskByName(task.taskname).season_counts)
? ('已转存集数 / 已播出集数 / 节目总集数:' + getTaskTransferredCount(getCalendarTaskByName(task.taskname)) + ' / ' + getTaskAiredCount(getCalendarTaskByName(task.taskname)) + ' / ' + getTaskTotalCount(getCalendarTaskByName(task.taskname)))
: ''">
<template v-if="getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).matched_show_name && getCalendarTaskByName(task.taskname).season_counts">
<span v-html="`${getTaskTransferredCount(getCalendarTaskByName(task.taskname))} <span class='count-slash'>/</span> ${getTaskAiredCount(getCalendarTaskByName(task.taskname))} <span class='count-slash'>/</span> ${getTaskTotalCount(getCalendarTaskByName(task.taskname))}`"></span>
</template>
<template v-else>
暂无数据
</template>
</div>
</div>
</div>
</div>
</div>
<hr v-if="tasklist.viewMode === 'list' && formData.tasklist.length > 0" class="task-divider">
<!-- 海报视图下隐藏底部添加任务按钮(仅列表视图显示) -->
<div class="row" v-if="tasklist.viewMode === 'list'">
<div class="col-sm-12 d-flex justify-content-end">
<button type="button" class="btn btn-outline-primary" @click="addTask()" title="添加任务"><i class="bi bi-plus-lg"></i></button>
</div>
@ -2989,7 +3089,9 @@
// 任务列表:类型筛选状态
tasklist: {
selectedType: (localStorage.getItem('tasklist_selected_type') || 'all'),
contentTypes: []
contentTypes: [],
// 任务列表视图模式list 或 poster默认列表视图支持持久化
viewMode: (localStorage.getItem('tasklist_view_mode') === 'poster') ? 'poster' : 'list'
},
// 任务列表排序设置记忆到localStorage
tasklistSort: (() => {
@ -3582,6 +3684,40 @@
this.stopCalendarAutoWatch();
},
methods: {
// 任务列表海报标题(悬停:#编号 任务名称 · 状态)
getTasklistPosterTitle(task, index) {
try {
const num = String(((task && task.__originalIndex !== undefined) ? task.__originalIndex : index) + 1).padStart(2, '0');
const name = task && task.taskname ? String(task.taskname).trim() : '';
const status = this.getTaskShowStatus(name);
return `#${num} ${name}${status ? ' · ' + status : ''}`;
} catch (e) { return ''; }
},
// 切换任务列表视图模式并持久化
toggleTasklistViewMode() {
try {
this.tasklist.viewMode = this.tasklist.viewMode === 'list' ? 'poster' : 'list';
try { localStorage.setItem('tasklist_view_mode', this.tasklist.viewMode); } catch (e) {}
} catch (e) {}
},
// 根据任务名获取对应的日历任务对象(若已加载)
getCalendarTaskByName(taskName) {
try {
const key = (taskName || '').trim();
if (!key || !this.calendar || !this.calendar.taskMapByName) return null;
return this.calendar.taskMapByName[key] || null;
} catch (e) { return null; }
},
// 将任务列表项适配为获取海报所需的 episode-like 对象
getTasklistPosterLikeEpisode(task) {
try {
const calTask = this.getCalendarTaskByName(task && task.taskname);
// 复用管理视图的取海报逻辑(优先匹配海报)
if (calTask) return this.getTaskPosterLikeEpisode(calTask);
// 兜底仅返回空对象getEpisodePosterUrl 内部会处理默认图
return { poster_local_path: '' };
} catch (e) { return { poster_local_path: '' }; }
},
// 任务列表排序:获取显示名称
getTasklistSortDisplayName(key) {
if (key === 'name') return '任务名称';
@ -4874,7 +5010,45 @@
// 热更新任务与日历
// 避免触发“未保存修改”提示:本次更新由后端变更引发
this.suppressConfigModifiedOnce = true;
await this.refreshCalendarData();
// 并行刷新:优先尽快更新任务列表的类型按钮与映射
try {
const tasksPromise = axios.get('/api/calendar/tasks');
// 同时后台刷新日历数据不阻塞UI
const refreshPromise = this.refreshCalendarData().catch(() => {});
const tasksResponse = await tasksPromise;
if (tasksResponse.data && tasksResponse.data.success) {
this.calendar.tasks = tasksResponse.data.data.tasks || [];
try {
this.calendar.taskMapByName = {};
(this.calendar.tasks || []).forEach(t => {
const key = (t.task_name || t.taskname || '').trim();
if (key) this.calendar.taskMapByName[key] = t;
});
} catch (e) { this.calendar.taskMapByName = {}; }
// 同步更新任务列表类型集合(热更新左上角类型按钮)
try {
let rawTypes = (tasksResponse.data && tasksResponse.data.data && tasksResponse.data.data.content_types) || [];
if (!rawTypes || rawTypes.length === 0) {
rawTypes = Array.from(new Set((this.calendar.tasks || []).map(t => (t && t.content_type) || '').filter(Boolean)));
}
const known = ['tv','anime','variety','documentary'];
const uniq = Array.from(new Set(rawTypes.filter(Boolean)));
const ordered = uniq.sort((a,b) => {
const ia = known.indexOf(a); const ib = known.indexOf(b);
if (ia === -1 && ib === -1) return 0;
if (ia === -1) return 1;
if (ib === -1) return -1;
return ia - ib;
});
this.tasklist.contentTypes = ordered;
const validSet = ['all', ...ordered];
if (!validSet.includes(this.tasklist.selectedType)) {
this.tasklist.selectedType = 'all';
}
} catch (e) { /* 忽略类型热更新异常 */ }
}
await refreshPromise;
} catch (e) { /* 忽略后台刷新异常,前端不报错 */ }
} else {
this.showToast(res.data.message || '保存失败');
}