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:
@@ -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>
|
||||
161
src/pages/home/components/TaskCard.vue
Normal file
161
src/pages/home/components/TaskCard.vue
Normal 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>
|
||||
163
src/pages/home/components/TaskList.vue
Normal file
163
src/pages/home/components/TaskList.vue
Normal 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>
|
||||
@@ -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()
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user