Files
labelmain-demo/app/management/team/sync_task/page.tsx
2026-02-27 10:21:43 +08:00

495 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { getProjectList } from "@/components/label/api/project"
import {
getAllSyncServerList,
getDataSyncTaskList,
newSyncTask,
updateSyncTaskStatus,
} from "@/components/label/api/sync"
import { Response } from "@/components/label/api/sync/typing"
import useAuth from "@/components/label/hooks/useAuth"
import {
ActionIcon,
Button,
Group,
Modal,
Paper,
Select,
Stack,
Text,
} from "@mantine/core"
import { modals } from "@mantine/modals"
import { notifications } from "@mantine/notifications"
import {
IconPlayerPause,
IconPlayerPlay,
IconPlus,
IconRefresh,
IconSearch,
IconTrash,
IconX,
} from "@tabler/icons-react"
import { DataTable, DataTableColumn } from "mantine-datatable"
import { useCallback, useEffect, useMemo, useState } from "react"
const StatusEnum = new Map<number, string>([
[1, "待同步"],
[2, "同步中"],
[3, "同步失败"],
[4, "同步成功"],
[5, "待删除"],
[6, "删除中"],
[7, "删除失败"],
[8, "删除完成"],
[9, "等待暂停"],
[10, "已暂停"],
[11, "等待关闭"],
[12, "已关闭"],
])
export default function TeamSyncTaskPage() {
const { isShow } = useAuth()
const [records, setRecords] = useState<Response.SyncTask[]>([])
const [loading, setLoading] = useState(false)
const [totalItems, setTotalItems] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [status, setStatus] = useState<string | null>(null)
const [projectId, setProjectId] = useState<string | null>(null)
const [serverId, setServerId] = useState<string | null>(null)
const [appliedStatus, setAppliedStatus] = useState<string | null>(null)
const [appliedProjectId, setAppliedProjectId] = useState<string | null>(null)
const [appliedServerId, setAppliedServerId] = useState<string | null>(null)
const [projectOpt, setProjectOpt] = useState<
Array<{ label: string; value: string }>
>([])
const [serverAllOpt, setServerAllOpt] = useState<
Array<{ label: string; value: string }>
>([])
const [serverOnlineOpt, setServerOnlineOpt] = useState<
Array<{ label: string; value: string }>
>([])
const [serverMap, setServerMap] = useState<Map<number, string>>(new Map())
const [open, setOpen] = useState(false)
const [newProjectId, setNewProjectId] = useState<string | null>(null)
const [newServerId, setNewServerId] = useState<string | null>(null)
const [creating, setCreating] = useState(false)
const loadOptions = useCallback(async () => {
const [projectRes, serverRes] = await Promise.all([
getProjectList({ page_number: 1, page_size: 10000 }),
getAllSyncServerList(),
])
const projects = projectRes?.project_list ?? []
setProjectOpt(projects.map((p) => ({ label: p.name, value: String(p.id) })))
const allServers = serverRes ?? []
setServerAllOpt(
allServers.map((s) => ({ label: s.ip, value: String(s.id) }))
)
setServerOnlineOpt(
allServers
.filter((s) => s.is_online)
.map((s) => ({ label: s.ip, value: String(s.id) }))
)
setServerMap(new Map(allServers.map((s) => [s.id, s.ip])))
}, [])
const load = useCallback(async () => {
try {
setLoading(true)
const params: any = {
page_number: page,
page_size: pageSize,
}
if (appliedStatus) params.status = Number(appliedStatus)
if (appliedProjectId) params.project_id = Number(appliedProjectId)
if (appliedServerId) params.sync_server_id = Number(appliedServerId)
const res = await getDataSyncTaskList(params)
setRecords(res?.sync_tasks ?? [])
setTotalItems(res?.total_items ?? 0)
} catch (e) {
setRecords([])
setTotalItems(0)
notifications.show({
color: "red",
title: "加载失败",
message: e instanceof Error ? e.message : "请求失败",
})
} finally {
setLoading(false)
}
}, [appliedProjectId, appliedServerId, appliedStatus, page, pageSize])
useEffect(() => {
queueMicrotask(loadOptions)
}, [loadOptions])
useEffect(() => {
load()
}, [load])
const performStatusAction = async (
id: number,
statusTo: number,
successText: string
) => {
try {
await updateSyncTaskStatus(id, statusTo)
notifications.show({
color: "green",
title: "操作成功",
message: successText,
})
load()
} catch (e) {
notifications.show({
color: "red",
title: "操作失败",
message: e instanceof Error ? e.message : "请求失败",
})
}
}
const columns = useMemo(() => {
const cols: DataTableColumn<Response.SyncTask>[] = [
{ accessor: "id", title: "ID", width: 80 },
{ accessor: "uuid", title: "UUID", width: 320 },
{
accessor: "status",
title: "状态",
width: 120,
render: (record) => StatusEnum.get(record.status ?? 0) ?? "-",
},
{ accessor: "project_name", title: "项目名称", width: 260 },
{
accessor: "sync_server_id",
title: "同步服务IP",
width: 160,
render: (record) =>
record.sync_server_id
? (serverMap.get(record.sync_server_id) ?? "-")
: "-",
},
{ accessor: "location", title: "IP所在地", width: 140 },
{ accessor: "create_at", title: "创建时间", width: 180 },
{ accessor: "update_at", title: "更新时间", width: 180 },
{
accessor: "operation",
title: "操作",
width: 200,
textAlign: "center",
render: (record) => {
const couldRun =
isShow("task_edit") && [10].includes(record.status ?? -1)
const couldStop =
isShow("task_edit") && [1, 2].includes(record.status ?? -1)
const couldDelete =
isShow("task_edit") &&
[1, 2, 3, 4, 9, 10].includes(record.status ?? -1)
const couldClose =
isShow("task_edit") && ![11, 12].includes(record.status ?? -1)
return (
<Group justify="center" gap={4} wrap="nowrap">
{couldRun ? (
<ActionIcon
variant="subtle"
color="green"
onClick={() =>
performStatusAction(record.id as number, 1, "已恢复同步")
}>
<IconPlayerPlay size={16} />
</ActionIcon>
) : null}
{couldStop ? (
<ActionIcon
variant="subtle"
color="yellow"
onClick={() => {
modals.openConfirmModal({
title: "暂停任务",
centered: true,
children: (
<Text size="sm"></Text>
),
labels: { confirm: "确定", cancel: "取消" },
onConfirm: () =>
performStatusAction(
record.id as number,
9,
"已暂停任务"
),
})
}}>
<IconPlayerPause size={16} />
</ActionIcon>
) : null}
{couldDelete ? (
<ActionIcon
variant="subtle"
color="red"
onClick={() => {
modals.openConfirmModal({
title: "删除任务",
centered: true,
children: (
<Text size="sm"></Text>
),
labels: { confirm: "确定", cancel: "取消" },
confirmProps: { color: "red" },
onConfirm: () =>
performStatusAction(
record.id as number,
5,
"已提交删除"
),
})
}}>
<IconTrash size={16} />
</ActionIcon>
) : null}
{couldClose ? (
<ActionIcon
variant="subtle"
color="gray"
onClick={() => {
modals.openConfirmModal({
title: "关闭任务",
centered: true,
children: (
<Text size="sm"></Text>
),
labels: { confirm: "确定", cancel: "取消" },
onConfirm: () =>
performStatusAction(
record.id as number,
11,
"已关闭任务"
),
})
}}>
<IconX size={16} />
</ActionIcon>
) : null}
</Group>
)
},
},
]
return cols
}, [isShow, serverMap])
const createTask = async () => {
if (!newProjectId || !newServerId) {
notifications.show({
color: "yellow",
title: "参数不完整",
message: "请先选择项目和同步服务",
})
return
}
try {
setCreating(true)
await newSyncTask({
project_id: Number(newProjectId),
sync_server_id: Number(newServerId),
})
notifications.show({
color: "green",
title: "操作成功",
message: "已新增同步任务",
})
setOpen(false)
setNewProjectId(null)
setNewServerId(null)
load()
} catch (e) {
notifications.show({
color: "red",
title: "操作失败",
message: e instanceof Error ? e.message : "请求失败",
})
} finally {
setCreating(false)
}
}
return (
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
<Paper
withBorder
p="md"
radius="md"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
}}>
<Group justify="space-between" wrap="wrap">
<Group>
<Select
placeholder="任务状态"
size="xs"
radius="xs"
data={Array.from(StatusEnum.entries()).map(([value, label]) => ({
label,
value: String(value),
}))}
clearable
searchable
value={status}
onChange={setStatus}
/>
<Select
placeholder="项目名称"
size="xs"
radius="xs"
data={projectOpt}
clearable
searchable
value={projectId}
onChange={setProjectId}
/>
<Select
placeholder="同步服务IP"
size="xs"
radius="xs"
data={serverAllOpt}
clearable
searchable
value={serverId}
onChange={setServerId}
/>
<Button
size="xs"
radius="xs"
leftSection={<IconSearch size={16} />}
onClick={() => {
setAppliedStatus(status)
setAppliedProjectId(projectId)
setAppliedServerId(serverId)
setPage(1)
}}>
</Button>
<Button
variant="default"
size="xs"
radius="xs"
leftSection={<IconRefresh size={16} />}
onClick={() => {
setStatus(null)
setProjectId(null)
setServerId(null)
setAppliedStatus(null)
setAppliedProjectId(null)
setAppliedServerId(null)
setPage(1)
setPageSize(20)
}}>
</Button>
</Group>
{isShow("task_add") ? (
<Button
size="xs"
radius="xs"
leftSection={<IconPlus size={16} />}
onClick={() => {
setNewProjectId(null)
setNewServerId(null)
setOpen(true)
}}>
</Button>
) : null}
</Group>
</Paper>
<Paper
withBorder
p="md"
radius="md"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
flex: 1,
minHeight: 0,
display: "flex",
}}>
<DataTable<Response.SyncTask>
withTableBorder
withRowBorders
fetching={loading}
records={records}
columns={columns}
totalRecords={totalItems}
recordsPerPage={pageSize}
page={page}
onPageChange={setPage}
onRecordsPerPageChange={(v) => {
setPageSize(v)
setPage(1)
}}
recordsPerPageOptions={[10, 20, 50, 100]}
noRecordsText="暂无数据"
scrollAreaProps={{ type: "auto" }}
style={{ width: "100%" }}
/>
</Paper>
<Modal
opened={open}
onClose={() => setOpen(false)}
title="新增同步任务"
centered
size={520}
closeOnClickOutside={false}>
<Stack gap="sm">
<Select
label="项目名称"
size="xs"
radius="xs"
data={projectOpt}
searchable
value={newProjectId}
onChange={setNewProjectId}
/>
<Select
label="同步服务IP仅在线"
size="xs"
radius="xs"
data={serverOnlineOpt}
searchable
value={newServerId}
onChange={setNewServerId}
/>
<Group justify="flex-end">
<Button
variant="default"
size="xs"
radius="xs"
onClick={() => setOpen(false)}>
</Button>
<Button
size="xs"
radius="xs"
loading={creating}
onClick={createTask}>
</Button>
</Group>
</Stack>
</Modal>
</Stack>
)
}