115 lines
3.6 KiB
Vue
115 lines
3.6 KiB
Vue
<script setup lang="ts">
|
|
import { NButton, NSpin, useDialog } from 'naive-ui'
|
|
import { computed, onUnmounted, ref } from 'vue'
|
|
|
|
import { deleteTask, listTasks, runTask, updateTask } from '@/api/tasks'
|
|
import EmptyCard from '@/components/EmptyCard.vue'
|
|
import TaskCard from '@/components/task/TaskCard.vue'
|
|
import TaskFormModal from '@/components/task/TaskFormModal.vue'
|
|
import { parseSnatch } from '@/components/task/taskDisplay'
|
|
import { useAsync } from '@/composables/useAsync'
|
|
import { useScopeStore } from '@/stores/scope'
|
|
import type { Task } from '@/types/api'
|
|
import { useToast } from '@/composables/useToast'
|
|
|
|
const message = useToast()
|
|
const dialog = useDialog()
|
|
const scope = useScopeStore()
|
|
|
|
const tasks = useAsync(listTasks)
|
|
const showCreate = ref(false)
|
|
const editingTask = ref<Task | null>(null)
|
|
|
|
// 每 5 秒静默刷新(不闪 loading)
|
|
const refreshTimer = setInterval(() => void tasks.run({ silent: true }), 5000)
|
|
onUnmounted(() => clearInterval(refreshTimer))
|
|
|
|
const cfgAliasById = computed(
|
|
() => new Map((scope.configs.data ?? []).map((c) => [c.id, c.alias])),
|
|
)
|
|
|
|
function aliasFor(task: Task): string | undefined {
|
|
const s = parseSnatch(task)
|
|
return s ? cfgAliasById.value.get(s.cfgId) : undefined
|
|
}
|
|
|
|
function openEdit(task: Task) {
|
|
editingTask.value = task
|
|
showCreate.value = true
|
|
}
|
|
|
|
function openCreate() {
|
|
editingTask.value = null
|
|
showCreate.value = true
|
|
}
|
|
|
|
async function act(fn: () => Promise<unknown>, ok: string) {
|
|
try {
|
|
await fn()
|
|
message.success(ok)
|
|
void tasks.run({ silent: true })
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '操作失败')
|
|
}
|
|
}
|
|
|
|
/** active ↔ paused 切换;failed(熔断停止)同走恢复交互重新启用 */
|
|
function toggle(task: Task) {
|
|
const resume = task.status !== 'active'
|
|
void act(
|
|
() => updateTask(task.id, { status: resume ? 'active' : 'paused' }),
|
|
resume ? (task.status === 'failed' ? '已重新启用' : '已恢复调度') : '已暂停',
|
|
)
|
|
}
|
|
|
|
function confirmRemove(task: Task) {
|
|
dialog.warning({
|
|
title: '删除任务',
|
|
content: `删除任务「${task.name}」及其全部日志?`,
|
|
positiveText: '删除',
|
|
negativeText: '取消',
|
|
onPositiveClick: () => act(() => deleteTask(task.id), '已删除任务及其日志'),
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<div class="flex flex-wrap items-baseline gap-2.5">
|
|
<h1 class="page-title">任务</h1>
|
|
<span class="text-[13px] text-ink-3">
|
|
{{ tasks.data.value?.length ?? '…' }} 个任务 · cron 为服务器本地时区
|
|
</span>
|
|
</div>
|
|
<NButton size="small" type="primary" @click="openCreate">新建任务</NButton>
|
|
</div>
|
|
|
|
<div
|
|
v-if="tasks.loading.value && !tasks.data.value"
|
|
class="panel flex items-center justify-center py-20"
|
|
>
|
|
<NSpin size="small" />
|
|
</div>
|
|
<div v-else class="flex flex-col gap-3">
|
|
<TaskCard
|
|
v-for="task in tasks.data.value ?? []"
|
|
:key="task.id"
|
|
:task="task"
|
|
:cfg-alias="aliasFor(task)"
|
|
@run="act(() => runTask(task.id), `已触发执行:${task.name}`)"
|
|
@toggle="toggle(task)"
|
|
@edit="openEdit(task)"
|
|
@remove="confirmRemove(task)"
|
|
/>
|
|
<EmptyCard
|
|
v-if="tasks.data.value && !tasks.data.value.length"
|
|
title="暂无任务"
|
|
note="点击右上角「新建任务」创建测活 / 抢机 / 成本任务"
|
|
/>
|
|
</div>
|
|
|
|
<TaskFormModal v-model:show="showCreate" :task="editingTask" @created="tasks.run()" />
|
|
</div>
|
|
</template>
|