feat: add task management and progress reporting

- Implemented task and subtask structures with progress tracking.
- Added reporting functionality to log progress at various stages in hotel room status management scripts.
- Created a task store to manage tasks and their states, including persistence to local storage.
- Updated UI components to display task lists and handle task actions (retry, remove).
- Removed deprecated TaskCard and TaskList components, replacing them with a new structure for better maintainability.
- Enhanced script execution service to emit progress events for UI updates.
This commit is contained in:
DEV_DSW
2026-04-16 16:59:49 +08:00
parent b1f589a674
commit 210e8eb363
24 changed files with 788 additions and 237 deletions

164
src/stores/task.ts Normal file
View File

@@ -0,0 +1,164 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { IPC_EVENTS, CONFIG_KEYS } from '@lib/constants';
import type { Task, SubTask, TaskProgressPayload } from '@lib/task-types';
export const useTaskStore = defineStore('task', () => {
const tasks = ref<Task[]>([]);
let initialized = false;
// 初始化时从 electron-store 加载
const init = async () => {
if (initialized) return;
try {
const saved = await window.api.invoke(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.TASK_LIST);
if (Array.isArray(saved)) {
tasks.value = saved;
}
initialized = true;
} catch (e) {
console.error('Failed to load tasks from store', e);
}
};
// 持久化 helper用于减少写盘次数可在需要时调用
const persist = async () => {
try {
await window.api.invoke(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.TASK_LIST, tasks.value);
} catch (e) {
console.error('Failed to persist tasks', e);
}
};
const createTask = (options: any): Task => {
const taskId = crypto.randomUUID();
const roomTypeObj = options.roomList.find((item: any) => item.id === options.roomType);
const scriptMappings = [
{ prop: 'fzName', scriptId: 'fg_trace.js', name: '飞猪房态追踪' },
{ prop: 'mtName', scriptId: 'mt_trace.js', name: '美团房态追踪' },
{ prop: 'dyHotelName', scriptId: 'dy_hotel_trace.js', name: '抖音酒店房态追踪' },
{ prop: 'dyHotSpringName', scriptId: 'dy_hot_spring_trace.js', name: '抖音温泉房态追踪' },
];
const subTasks: SubTask[] = scriptMappings
.filter(({ prop }) => roomTypeObj?.[prop])
.map(({ scriptId, name, prop }) => ({
id: `${taskId}_${prop}`,
taskId,
scriptId,
name,
status: 'pending',
progress: 0,
message: '等待执行',
stdoutTail: '',
stderrTail: '',
startedAt: new Date().toISOString(),
}));
const task: Task = {
id: taskId,
title: `${options.operation === 'open' ? '开启' : '关闭'}渠道房型 - ${roomTypeObj?.pmsName || ''}`,
operation: options.operation,
roomType: options.roomType,
dateRange: [options.startTime, options.endTime],
status: 'pending',
subTasks,
roomList: options.roomList || [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
tasks.value = [task, ...tasks.value];
persist();
return task;
};
const updateSubTaskProgress = (payload: TaskProgressPayload & { taskId: string; subTaskId: string }) => {
const task = tasks.value.find((t) => t.id === payload.taskId);
if (!task) return;
const subTask = task.subTasks.find((s) => s.id === payload.subTaskId);
if (!subTask) return;
if (subTask.status === 'pending') subTask.status = 'running';
if (payload.progress !== undefined) subTask.progress = payload.progress;
if (payload.message !== undefined) subTask.message = payload.message;
if (payload.stdoutTail !== undefined) subTask.stdoutTail = payload.stdoutTail;
if (payload.stderrTail !== undefined) subTask.stderrTail = payload.stderrTail;
task.status = deriveTaskStatus(task.subTasks);
task.updatedAt = new Date().toISOString();
// 进度更新不持久化,只在内存中更新以减少写盘
};
const completeSubTask = (payload: { taskId: string; subTaskId: string; success: boolean; exitCode: number | null; error?: string }) => {
const task = tasks.value.find((t) => t.id === payload.taskId);
if (!task) return;
const subTask = task.subTasks.find((s) => s.id === payload.subTaskId);
if (!subTask) return;
subTask.status = payload.success ? 'success' : 'failed';
subTask.progress = payload.success ? 100 : subTask.progress;
if (payload.error) subTask.error = payload.error;
subTask.completedAt = new Date().toISOString();
task.status = deriveTaskStatus(task.subTasks);
task.updatedAt = new Date().toISOString();
persist();
};
const retryFailedSubTasks = async (taskId: string) => {
const task = tasks.value.find((t) => t.id === taskId);
if (!task) return;
const failedSubTasks = task.subTasks.filter((s) => s.status === 'failed');
if (failedSubTasks.length === 0) return;
failedSubTasks.forEach((s) => {
s.status = 'pending';
s.progress = 0;
s.message = '等待执行';
s.stdoutTail = '';
s.stderrTail = '';
s.error = undefined;
s.completedAt = undefined;
});
task.status = 'pending';
task.updatedAt = new Date().toISOString();
persist();
// 重新触发主进程执行
const options = {
taskId: task.id,
roomType: task.roomType,
startTime: task.dateRange[0],
endTime: task.dateRange[1],
operation: task.operation,
roomList: [], // 主进程会自行从 roomList 中查找,但这里可以传空;若主进程已支持 taskId 则无需重复传 roomList
};
// 由于当前 roomList 不在 store 中持久化调用方UI应负责在重试时补充 roomList。
// 为了兼容性UI 层重试时直接调用 window.api.executeScript({ taskId, roomType, startTime, endTime, operation, roomList })
// 本方法只负责重置状态并持久化,不直接调用 executeScript避免 roomList 丢失)。
// 若 UI 没有传入 roomList当前主进程逻辑依赖它。建议 UI 层在调用 retry 后重新发起 executeScript。
};
const removeTask = (taskId: string) => {
tasks.value = tasks.value.filter((t) => t.id !== taskId);
persist();
};
const pendingTasks = computed(() => tasks.value.filter((t) => t.status === 'pending' || t.status === 'running'));
const completedTasks = computed(() => tasks.value.filter((t) => t.status === 'success' || t.status === 'failed' || t.status === 'partial_failed'));
function deriveTaskStatus(subTasks: SubTask[]): Task['status'] {
if (subTasks.every((s) => s.status === 'success')) return 'success';
if (subTasks.every((s) => s.status === 'failed')) return 'failed';
if (subTasks.some((s) => s.status === 'failed') && subTasks.some((s) => s.status === 'success')) return 'partial_failed';
if (subTasks.some((s) => s.status === 'running')) return 'running';
return 'pending';
}
return {
tasks,
init,
createTask,
updateSubTaskProgress,
completeSubTask,
retryFailedSubTasks,
removeTask,
pendingTasks,
completedTasks,
};
});