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

View File

@@ -1,63 +0,0 @@
<!--
* @Author: kongbeiwu lishaohua-520@qq.com
* @Date: 2025-12-21 23:02:06
* @LastEditors: kongbeiwu lishaohua-520@qq.com
* @LastEditTime: 2025-12-28 11:09:00
* @FilePath: /project/zn-ai/src/renderer/components/TaskList/Card.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div v-for="item in task" :key="item.id"
class="border border-solid border-[#E5E8EE] dark:border-[#2a2a2d] rounded-[12px] p-[12px] mb-[12px] task dark:bg-[#1f1f22]">
<div class="flex items-center pb-[12px] border-b border-dashed border-[#E5E8EE] dark:border-[#2a2a2d]">
<!-- <img class="w-[24px] h-[24px] rounded-[8px]] mr-[4px]" src="@assets/images/task/xc.png" /> -->
<div
class="w-[24px] h-[24px] rounded-[4px] bg-[#EFF6FF] dark:bg-[#1f1f22] text-[#2B7FFF] text-[14px] font-bold border border-solid border-[#BEDBFF] dark:border-[#2a2a2d] flex justify-center items-center">
{{ item.name[0] }}</div>
<div class="text-[16px] text-[#171717] dark:text-gray-100 font-bold mr-[8px] ml-[4px]">{{ item.name }}</div>
<div class="pl-[8px] pr-[8px] text-[12px] rounded-[100px]" :class="item.statusColor">{{
item.statusText }}</div>
</div>
<div class="flex items-center mt-[12px]">
<component :is="item.desIcon" :color="item.color" class="w-[15px] mr-[4px]" />
<div class="text-[14px]" :class="`text-[${item.color}]`" :style="{ color: item.color }">{{ item.des }}</div>
</div>
<div class="mt-[24px]">
<button class="w-[100%] h-[40px] bg-[#2B7FFF] text-white text-[14px] rounded-[12px]">{{ item.statusColor !==
'error' ? '查看' : '处理' }}</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import { task } from '@constant/task'
</script>
<style scoped>
.task {
position: relative;
z-index: 1;
transition: all .2s linear;
}
.task .success {
background-color: #E0FAEC;
color: #1FC16B;
}
.task .error {
background-color: #FFEBEC;
color: #FB3748;
}
.task .warning {
background-color: #FFF3EB;
color: #FA7319;
}
.task:hover {
z-index: 2;
box-shadow: 0 10px 20px rgba(0, 0, 0, .1);
transform: translate3d(0, -2px, 0);
}</style>

View File

@@ -1,77 +0,0 @@
<template>
<div class="task p-3">
<div class="flex border border-[#BEDBFF] dark:border-[#2a2a2d] h-12 p-1 rounded-[10px] bg-[#EFF6FF] dark:bg-[#1f1f22] task-tab">
<div v-for="item in tabs" :key="item.value" class="flex-1 flex text-center items-center h-full align-middle text" :class="active === item.value && 'active'" @click="changeTab(item.value)">
<div class="flex-1">{{ item.name }}<span v-if="item.total">{{`${item.total > 98 && item.total + '+' || item.total}`}}</span></div>
</div>
</div>
<div class="flex justify-between mt-3 mb-3 text-[14px]">
<div class="text-[#171717] dark:text-gray-100">今天</div>
<div class="text-[#99A0AE] dark:text-gray-500">02:32:05</div>
</div>
<div>
<Card />
</div>
</div>
</template>
<script setup lang="ts">
import Card from './Card.vue';
import { ref, reactive } from "vue";
const tabs = reactive([
{
name: '待处理',
value: 1,
total: 10,
},
{
name: '已处理',
value: 2,
total: 99,
}
])
const active = ref(1);
const changeTab = (val:number) => {
active.value = val;
};
</script>
<style scoped>
.task-tab .text {
color: #525866;
font-size: 14px;
cursor: pointer;
}
:global(.dark) .task-tab .text {
color: #9ca3af;
}
.task-tab .active {
position: relative;
color: #2B7FFF;
background: #FFFFFF;
border-radius: 8px;
}
:global(.dark) .task-tab .active {
color: #2B7FFF;
background: #1f1f22;
}
.task-tab .active::after {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
content: '';
border-radius: 8px;
border: 1px solid #2B7FFF;
}
:global(.dark) .task-tab .active::after {
border: 1px solid #2B7FFF;
}
</style>

View File

@@ -52,6 +52,11 @@ export enum IPC_EVENTS {
// 执行脚本
EXECUTE_SCRIPT = 'execute-script',
// 任务事件
TASK_PROGRESS = 'task:progress',
TASK_STARTED = 'task:started',
TASK_COMPLETED = 'task:completed',
// 打开渠道
OPEN_CHANNEL = 'open-channel',
@@ -104,6 +109,7 @@ export enum CONFIG_KEYS {
AUTO_DOWNLOAD_UPDATE = 'autoDownloadUpdate',
SELECTED_CHANNELS = 'selectedChannels',
IMAGE_CACHE = 'imageCache',
TASK_LIST = 'taskList',
}
export enum MENU_IDS {

40
src/lib/task-types.ts Normal file
View File

@@ -0,0 +1,40 @@
export type SubTaskStatus = 'pending' | 'running' | 'success' | 'failed';
export interface SubTask {
id: string;
taskId: string;
scriptId: string;
name: string;
status: SubTaskStatus;
progress: number;
message: string;
stdoutTail: string;
stderrTail: string;
error?: string;
startedAt: string;
completedAt?: string;
}
export type TaskStatus = 'pending' | 'running' | 'success' | 'partial_failed' | 'failed';
export interface Task {
id: string;
title: string;
operation: 'open' | 'close';
roomType: string;
dateRange: [string, string];
status: TaskStatus;
subTasks: SubTask[];
roomList: any[];
createdAt: string;
updatedAt: string;
}
export interface TaskProgressPayload {
taskId: string;
subTaskId: string;
progress?: number;
message?: string;
stdoutTail?: string;
stderrTail?: string;
}

View File

@@ -1,65 +0,0 @@
<template>
<div class="task p-3">
<div class="flex border border-[#BEDBFF] dark:border-[#2a2a2d] h-[48px] p-[4px] rounded-[10px] bg-[#EFF6FF] dark:bg-[#222225] task-tab">
<div v-for="item in tabs" :key="item.value" class="flex-1 flex text-center items-center h-full align-middle text" :class="active === item.value && 'active'" @click="changeTab(item.value)">
<div class="flex-1">{{ item.name }}<span v-if="item.total">{{`${item.total > 98 && item.total + '+' || item.total}`}}</span></div>
</div>
</div>
<div class="flex justify-end">
<div>今天</div>
<div>02:32:05</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
const tabs = reactive([
{
name: '待处理',
value: 1,
total: 10,
},
{
name: '已处理',
value: 2,
total: 99,
}
])
const active = ref(1);
const changeTab = (val:number) => {
active.value = val;
};
</script>
<style scoped>
.task-tab .text {
color: #525866;
font-size: 14px;
cursor: pointer;
}
.dark .task-tab .text {
color: #9ca3af;
}
.task-tab .active {
position: relative;
color: #2B7FFF;
background: #FFFFFF;
border-radius: 8px;
}
.dark .task-tab .active {
background: #1f1f22;
border-color: #2a2a2d;
}
.task-tab .active::after {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
content: '';
border-radius: 8px;
border: 1px solid #2B7FFF;
}
</style>

View File

@@ -0,0 +1,161 @@
<template>
<div
class="border border-solid border-[#E5E8EE] dark:border-[#2a2a2d] rounded-[12px] p-[12px] mb-[12px] task dark:bg-[#1f1f22]">
<div class="flex items-center pb-[12px] border-b border-dashed border-[#E5E8EE] dark:border-[#2a2a2d]">
<div
class="w-[24px] h-[24px] rounded-[4px] bg-[#EFF6FF] dark:bg-[#1f1f22] text-[#2B7FFF] text-[14px] font-bold border border-solid border-[#BEDBFF] dark:border-[#2a2a2d] flex justify-center items-center">
{{ displayTitle[0] }}</div>
<div class="text-[16px] text-[#171717] dark:text-gray-100 font-bold mr-[8px] ml-[4px]">{{ displayTitle }}</div>
<div v-if="isSubTask && parentTitle" class="text-[12px] text-[#99A0AE] dark:text-gray-500 mr-[8px]">{{ parentTitle }}</div>
<div class="pl-[8px] pr-[8px] text-[12px] rounded-[100px]" :class="statusClass">{{ statusText }}</div>
</div>
<!-- Running SubTask: progress and message -->
<div v-if="isSubTask && item.status === 'running'" class="mt-[12px]">
<div class="w-full h-[6px] bg-[#E5E8EE] dark:bg-[#2a2a2d] rounded-[3px] overflow-hidden mb-[8px]">
<div class="h-full bg-[#2B7FFF] rounded-[3px] transition-all duration-300"
:style="{ width: `${(item as SubTask).progress}%` }" />
</div>
<div class="text-[14px] text-[#525866] dark:text-gray-400 mb-[8px]">{{ (item as SubTask).message }}</div>
<div v-if="showStdout"
class="text-[12px] text-[#99A0AE] dark:text-gray-500 bg-[#F5F5F5] dark:bg-[#2a2a2d] rounded-[8px] p-[8px] max-h-[120px] overflow-y-auto whitespace-pre-wrap">
{{ (item as SubTask).stdoutTail || '暂无输出' }}
</div>
</div>
<!-- Default description -->
<div v-else class="flex items-center mt-[12px]">
<RiErrorWarningFill :color="desColor" class="w-[15px] mr-[4px]" />
<div class="text-[14px]" :class="`text-[${desColor}]`" :style="{ color: desColor }">{{ description }}</div>
</div>
<div class="mt-[24px]">
<button
class="w-[100%] h-[40px] bg-[#2B7FFF] text-white text-[14px] rounded-[12px]"
@click="handleButtonClick">
{{ buttonText }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { RiErrorWarningFill } from '@remixicon/vue'
import type { Task, SubTask } from '@lib/task-types'
const props = defineProps<{
item: SubTask | Task
isSubTask?: boolean
parentTitle?: string
}>()
const emit = defineEmits<{
retry: [taskId: string]
remove: [taskId: string]
}>()
const showStdout = ref(false)
const displayTitle = computed(() => {
return props.isSubTask ? (props.item as SubTask).name : (props.item as Task).title
})
const statusClass = computed(() => {
const status = props.item.status
if (status === 'success') return 'success'
if (status === 'failed' || status === 'partial_failed') return 'error'
return 'warning'
})
const statusText = computed(() => {
const status = props.item.status
if (status === 'success') return '成功'
if (status === 'failed' || status === 'partial_failed') return '失败'
if (status === 'running') return '执行中'
return '等待中'
})
const desColor = computed(() => {
const status = props.item.status
if (status === 'success') return '#1FC16B'
if (status === 'failed' || status === 'partial_failed') return '#FB3748'
return '#FA7319'
})
const description = computed(() => {
if (props.isSubTask) {
const sub = props.item as SubTask
if (sub.status === 'failed' && sub.error) return sub.error
if (sub.message) return sub.message
if (sub.status === 'success') return '任务执行成功'
return '任务执行中,请勿关闭浏览器'
}
const task = props.item as Task
const successCount = task.subTasks.filter(s => s.status === 'success').length
const failedCount = task.subTasks.filter(s => s.status === 'failed').length
const total = task.subTasks.length
if (task.status === 'success') return '任务执行成功'
if (task.status === 'failed') return `${failedCount}/${total} 个子任务失败`
if (task.status === 'partial_failed') return `${successCount} 成功, ${failedCount} 失败`
return '任务执行中,请勿关闭浏览器'
})
const buttonText = computed(() => {
const status = props.item.status
if (status === 'running') return '查看'
if (status === 'failed' || status === 'partial_failed') return '重试失败项'
if (status === 'success') return '移除'
return '查看'
})
const handleButtonClick = () => {
const status = props.item.status
if (status === 'running' && props.isSubTask) {
showStdout.value = !showStdout.value
return
}
if (status === 'running' || status === 'pending') {
// 查看:无操作或提示等待
return
}
if (status === 'failed' || status === 'partial_failed') {
const taskId = props.isSubTask ? (props.item as SubTask).taskId : (props.item as Task).id
emit('retry', taskId)
return
}
if (status === 'success') {
const taskId = props.isSubTask ? (props.item as SubTask).taskId : (props.item as Task).id
emit('remove', taskId)
}
}
</script>
<style scoped>
.task {
position: relative;
z-index: 1;
transition: all .2s linear;
}
.task .success {
background-color: #E0FAEC;
color: #1FC16B;
}
.task .error {
background-color: #FFEBEC;
color: #FB3748;
}
.task .warning {
background-color: #FFF3EB;
color: #FA7319;
}
.task:hover {
z-index: 2;
box-shadow: 0 10px 20px rgba(0, 0, 0, .1);
transform: translate3d(0, -2px, 0);
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<div class="task p-3">
<div class="flex border border-[#BEDBFF] dark:border-[#2a2a2d] h-12 p-1 rounded-[10px] bg-[#EFF6FF] dark:bg-[#1f1f22] task-tab">
<div v-for="item in tabs" :key="item.value" class="flex-1 flex text-center items-center h-full align-middle text"
:class="active === item.value && 'active'" @click="changeTab(item.value)">
<div class="flex-1">{{ item.name }}<span v-if="item.total">{{
`${item.total > 98 && item.total + '+' || item.total}` }}</span></div>
</div>
</div>
<div class="flex justify-between mt-3 mb-3 text-[14px]">
<div class="text-[#171717] dark:text-gray-100">{{ currentDateLabel }}</div>
<div class="text-[#99A0AE] dark:text-gray-500">{{ currentTime }}</div>
</div>
<div>
<!-- Pending tab: flatten to subtask cards -->
<template v-if="active === 1">
<template v-for="task in taskStore.pendingTasks" :key="task.id">
<Card v-for="subTask in task.subTasks" :key="subTask.id" :item="subTask" :is-sub-task="true"
:parent-title="task.title" @retry="handleRetry" @remove="handleRemove" />
</template>
</template>
<!-- Completed tab: task cards -->
<template v-if="active === 2">
<Card v-for="task in taskStore.completedTasks" :key="task.id" :item="task" :is-sub-task="false"
@retry="handleRetry" @remove="handleRemove" />
</template>
</div>
</div>
</template>
<script setup lang="ts">
import Card from './TaskCard.vue'
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useTaskStore } from '@stores/task'
import type { Task } from '@lib/task-types'
const taskStore = useTaskStore()
const now = ref(new Date())
let timer: ReturnType<typeof setInterval> | null = null
const currentDateLabel = computed(() => {
const today = new Date()
const y = today.getFullYear()
const m = today.getMonth()
const d = today.getDate()
const current = now.value
const cy = current.getFullYear()
const cm = current.getMonth()
const cd = current.getDate()
if (cy === y && cm === m && cd === d) {
return '今天'
}
const yesterday = new Date(y, m, d - 1)
if (cy === yesterday.getFullYear() && cm === yesterday.getMonth() && cd === yesterday.getDate()) {
return '昨天'
}
const pad = (n: number) => String(n).padStart(2, '0')
return `${pad(cm + 1)}/${pad(cd)}`
})
const currentTime = computed(() => {
const pad = (n: number) => String(n).padStart(2, '0')
const h = pad(now.value.getHours())
const m = pad(now.value.getMinutes())
const s = pad(now.value.getSeconds())
return `${h}:${m}:${s}`
})
onMounted(() => {
timer = setInterval(() => {
now.value = new Date()
}, 1000)
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
timer = null
}
})
const tabs = reactive([
{
name: '待处理',
value: 1,
total: computed(() => taskStore.pendingTasks.length),
},
{
name: '已处理',
value: 2,
total: computed(() => taskStore.completedTasks.length),
}
])
const active = ref(1)
const changeTab = (val: number) => {
active.value = val
}
const handleRetry = (taskId: string) => {
taskStore.retryFailedSubTasks(taskId)
const task = taskStore.pendingTasks.find((t: Task) => t.id === taskId) || taskStore.completedTasks.find((t: Task) => t.id === taskId)
if (task) {
const options = {
taskId: task.id,
roomType: task.roomType,
startTime: task.dateRange[0],
endTime: task.dateRange[1],
operation: task.operation,
roomList: task.roomList || [],
}
window.api.executeScript(options)
}
}
const handleRemove = (taskId: string) => {
taskStore.removeTask(taskId)
}
</script>
<style scoped>
.task-tab .text {
color: #525866;
font-size: 14px;
cursor: pointer;
}
:global(.dark) .task-tab .text {
color: #9ca3af;
}
.task-tab .active {
position: relative;
color: #2B7FFF;
background: #FFFFFF;
border-radius: 8px;
}
:global(.dark) .task-tab .active {
color: #2B7FFF;
background: #1f1f22;
}
.task-tab .active::after {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
content: '';
border-radius: 8px;
border: 1px solid #2B7FFF;
}
:global(.dark) .task-tab .active::after {
border: 1px solid #2B7FFF;
}
</style>

View File

@@ -41,8 +41,11 @@
<script setup lang="ts">
import { hotelStaffTypeMappingListUsingPost } from '@api/index'
import { useTaskStore } from '@stores/task'
import { ref } from 'vue'
const taskStore = useTaskStore()
const isVisible = ref(false)
const roomList: any = ref([])
const title = ref('渠道房型操作')
@@ -119,6 +122,8 @@ const confirm = () => {
/**
* 坑传给进程的参数不能是ref包裹的reactive对象
*/
const task = taskStore.createTask(options)
options.taskId = task.id
window.api.executeScript(options)
reset()

View File

@@ -17,7 +17,7 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import TaskList from '../../components/TaskList/index.vue'
import TaskList from './components/TaskList.vue'
import TaskOperationDialog from './components/TaskOperationDialog.vue'
import AddChannelDialog from './components/AddChannelDialog.vue'
import ChatHistory from './ChatHistory.vue'
@@ -27,12 +27,14 @@ import { useChatStore } from '@stores/chat'
import { useProviderStore } from '@stores/providers'
import { useChannelStore } from '@stores/channel'
import { useScriptStore } from '@stores/script'
import { useTaskStore } from '@stores/task'
import emitter from '@src/utils/emitter'
const chatStore = useChatStore()
const providerStore = useProviderStore()
const channelStore = useChannelStore()
const scriptStore = useScriptStore()
const taskStore = useTaskStore()
const taskOperationDialog = ref()
const addChannelDialog = ref()
@@ -42,6 +44,13 @@ onMounted(async () => {
chatStore.subscribeToGateway()
await scriptStore.fetchScripts()
await channelStore.loadSelectedChannels()
await taskStore.init()
window.api.onTaskProgress((payload) => taskStore.updateSubTaskProgress(payload))
window.api.onTaskStarted((payload) => {
taskStore.updateSubTaskProgress({ ...payload, message: '开始执行' })
})
window.api.onTaskCompleted((payload) => taskStore.completeSubTask(payload))
})
onBeforeUnmount(() => {

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,
};
});