在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -0,0 +1,309 @@
/**
* 按业务域分组的任务选择器。
*
* 从 /api/tasks/registry 获取任务注册表,按业务域折叠展示,
* 支持全选/反选和按 Flow 层级过滤。
* 当 Flow 包含 DWD 层时,在 DWD 任务下方内嵌表过滤子选项。
*/
import React, { useEffect, useState, useMemo, useCallback } from "react";
import {
Collapse,
Checkbox,
Spin,
Alert,
Button,
Space,
Typography,
Tag,
Divider,
} from "antd";
import type { CheckboxChangeEvent } from "antd/es/checkbox";
import { fetchTaskRegistry, fetchDwdTables } from "../api/tasks";
import type { TaskDefinition } from "../types";
const { Text } = Typography;
/* ------------------------------------------------------------------ */
/* Props */
/* ------------------------------------------------------------------ */
export interface TaskSelectorProps {
/** 当前 Flow 包含的层(如 ["ODS", "DWD"] */
layers: string[];
/** 已选中的任务编码列表 */
selectedTasks: string[];
/** 选中任务变化回调 */
onTasksChange: (tasks: string[]) => void;
/** DWD 表过滤:已选中的表名列表 */
selectedDwdTables?: string[];
/** DWD 表过滤变化回调 */
onDwdTablesChange?: (tables: string[]) => void;
}
/* ------------------------------------------------------------------ */
/* 过滤逻辑 */
/* ------------------------------------------------------------------ */
export function filterTasksByLayers(
tasks: TaskDefinition[],
layers: string[],
): TaskDefinition[] {
if (layers.length === 0) return [];
return tasks;
}
/* ------------------------------------------------------------------ */
/* 组件 */
/* ------------------------------------------------------------------ */
const TaskSelector: React.FC<TaskSelectorProps> = ({
layers,
selectedTasks,
onTasksChange,
selectedDwdTables = [],
onDwdTablesChange,
}) => {
const [registry, setRegistry] = useState<Record<string, TaskDefinition[]>>({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// DWD 表定义(按域分组)
const [dwdTableGroups, setDwdTableGroups] = useState<Record<string, string[]>>({});
const showDwdFilter = layers.includes("DWD") && !!onDwdTablesChange;
/* ---------- 加载任务注册表 ---------- */
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
const promises: Promise<void>[] = [
fetchTaskRegistry()
.then((data) => { if (!cancelled) setRegistry(data); })
.catch((err) => { if (!cancelled) setError(err?.message ?? "获取任务列表失败"); }),
];
// 如果包含 DWD 层,同时加载 DWD 表定义
if (layers.includes("DWD")) {
promises.push(
fetchDwdTables()
.then((data) => { if (!cancelled) setDwdTableGroups(data); })
.catch(() => { /* DWD 表加载失败不阻塞任务列表 */ }),
);
}
Promise.all(promises).finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [layers]);
/* ---------- 按 layers 过滤后的分组 ---------- */
const filteredGroups = useMemo(() => {
const result: Record<string, TaskDefinition[]> = {};
for (const [domain, tasks] of Object.entries(registry)) {
const visible = filterTasksByLayers(tasks, layers);
if (visible.length > 0) {
result[domain] = [...visible].sort((a, b) => {
if (a.is_common === b.is_common) return 0;
return a.is_common ? -1 : 1;
});
}
}
return result;
}, [registry, layers]);
const allVisibleCodes = useMemo(
() => Object.values(filteredGroups).flatMap((t) => t.map((d) => d.code)),
[filteredGroups],
);
// DWD 表扁平列表
const allDwdTableNames = useMemo(
() => Object.values(dwdTableGroups).flat(),
[dwdTableGroups],
);
/* ---------- 事件处理 ---------- */
const handleDomainChange = useCallback(
(domain: string, checkedCodes: string[]) => {
const otherDomainCodes = selectedTasks.filter(
(code) => !filteredGroups[domain]?.some((t) => t.code === code),
);
onTasksChange([...otherDomainCodes, ...checkedCodes]);
},
[selectedTasks, filteredGroups, onTasksChange],
);
const handleSelectAll = useCallback(() => {
onTasksChange(allVisibleCodes);
}, [allVisibleCodes, onTasksChange]);
const handleInvertSelection = useCallback(() => {
const currentSet = new Set(selectedTasks);
const inverted = allVisibleCodes.filter((code) => !currentSet.has(code));
onTasksChange(inverted);
}, [allVisibleCodes, selectedTasks, onTasksChange]);
/* ---------- DWD 表过滤事件 ---------- */
const handleDwdDomainTableChange = useCallback(
(domain: string, checked: string[]) => {
if (!onDwdTablesChange) return;
const domainTables = new Set(dwdTableGroups[domain] ?? []);
const otherSelected = selectedDwdTables.filter((t) => !domainTables.has(t));
onDwdTablesChange([...otherSelected, ...checked]);
},
[selectedDwdTables, dwdTableGroups, onDwdTablesChange],
);
const handleDwdSelectAll = useCallback(() => {
onDwdTablesChange?.(allDwdTableNames);
}, [allDwdTableNames, onDwdTablesChange]);
const handleDwdClearAll = useCallback(() => {
onDwdTablesChange?.([]);
}, [onDwdTablesChange]);
/* ---------- 渲染 ---------- */
if (loading) return <Spin tip="加载任务列表…" />;
if (error) return <Alert type="error" message="加载失败" description={error} />;
const domainEntries = Object.entries(filteredGroups);
if (domainEntries.length === 0) return <Text type="secondary"> Flow </Text>;
const selectedCount = selectedTasks.filter((c) => allVisibleCodes.includes(c)).length;
// DWD 装载任务是否被选中
const dwdLoadSelected = selectedTasks.includes("DWD_LOAD_FROM_ODS");
return (
<div>
<Space style={{ marginBottom: 8 }}>
<Button size="small" onClick={handleSelectAll}></Button>
<Button size="small" onClick={handleInvertSelection}></Button>
<Text type="secondary"> {selectedCount} / {allVisibleCodes.length}</Text>
</Space>
<Collapse
defaultActiveKey={domainEntries.map(([d]) => d)}
items={domainEntries.map(([domain, tasks]) => {
const domainCodes = tasks.map((t) => t.code);
const domainSelected = selectedTasks.filter((c) => domainCodes.includes(c));
const allChecked = domainSelected.length === domainCodes.length;
const indeterminate = domainSelected.length > 0 && !allChecked;
const handleDomainCheckAll = (e: CheckboxChangeEvent) => {
handleDomainChange(domain, e.target.checked ? domainCodes : []);
};
return {
key: domain,
label: (
<span onClick={(e) => e.stopPropagation()}>
<Checkbox
indeterminate={indeterminate}
checked={allChecked}
onChange={handleDomainCheckAll}
style={{ marginRight: 8 }}
/>
{domain}
<Text type="secondary" style={{ marginLeft: 4 }}>
({domainSelected.length}/{domainCodes.length})
</Text>
</span>
),
children: (
<Checkbox.Group
value={domainSelected}
onChange={(checked) => handleDomainChange(domain, checked as string[])}
>
<Space direction="vertical" style={{ width: "100%" }}>
{tasks.map((t) => (
<Checkbox key={t.code} value={t.code}>
<Text strong style={t.is_common === false ? { color: "#999" } : undefined}>{t.code}</Text>
<Text type="secondary" style={{ marginLeft: 8 }}>{t.name}</Text>
{t.is_common === false && (
<Tag color="default" style={{ marginLeft: 6, fontSize: 11 }}></Tag>
)}
</Checkbox>
))}
</Space>
</Checkbox.Group>
),
};
})}
/>
{/* DWD 表过滤:仅在 DWD 层且 DWD_LOAD_FROM_ODS 被选中时显示 */}
{showDwdFilter && dwdLoadSelected && allDwdTableNames.length > 0 && (
<>
<Divider style={{ margin: "12px 0 8px" }} />
<div style={{ padding: "0 4px" }}>
<Space style={{ marginBottom: 6 }}>
<Text strong style={{ fontSize: 13 }}>DWD </Text>
<Text type="secondary" style={{ fontSize: 12 }}>
{selectedDwdTables.length === 0
? "(未选择 = 全部装载)"
: `已选 ${selectedDwdTables.length} / ${allDwdTableNames.length}`}
</Text>
</Space>
<div style={{ marginBottom: 6 }}>
<Space size={4}>
<Button size="small" type="link" style={{ padding: 0, fontSize: 12 }} onClick={handleDwdSelectAll}>
</Button>
<Button size="small" type="link" style={{ padding: 0, fontSize: 12 }} onClick={handleDwdClearAll}>
</Button>
</Space>
</div>
<Collapse
size="small"
items={Object.entries(dwdTableGroups).map(([domain, tables]) => {
const domainSelected = selectedDwdTables.filter((t) => tables.includes(t));
const allDomainChecked = domainSelected.length === tables.length;
const domainIndeterminate = domainSelected.length > 0 && !allDomainChecked;
return {
key: domain,
label: (
<span onClick={(e) => e.stopPropagation()}>
<Checkbox
indeterminate={domainIndeterminate}
checked={allDomainChecked}
onChange={(e: CheckboxChangeEvent) =>
handleDwdDomainTableChange(domain, e.target.checked ? tables : [])
}
style={{ marginRight: 8 }}
/>
{domain}
<Text type="secondary" style={{ marginLeft: 4, fontSize: 12 }}>
({domainSelected.length}/{tables.length})
</Text>
</span>
),
children: (
<Checkbox.Group
value={domainSelected}
onChange={(checked) => handleDwdDomainTableChange(domain, checked as string[])}
>
<Space direction="vertical">
{tables.map((table) => (
<Checkbox key={table} value={table}>
<Text style={{ fontSize: 12 }}>{table}</Text>
</Checkbox>
))}
</Space>
</Checkbox.Group>
),
};
})}
/>
</div>
</>
)}
</div>
);
};
export default TaskSelector;