Commit 4a0c4abf authored by drigle's avatar drigle

fix: 增加拓扑图多层级联流程筛选

parent 64fcb2ae
import { Button, Checkbox, Popover } from '@heroui/react';
import { ChevronRight, Close } from '@/components/AppIcons';
import { useEffect, useMemo, useState } from 'react';
function collectDescendantIds(node) {
const ids = [];
const walk = (item) => {
item.children.forEach((child) => {
ids.push(child.id);
walk(child);
});
};
walk(node);
return ids;
}
export default function ProcessCascade({ tree = [], value = [], isDisabled = false, onChange }) {
const [columns, setColumns] = useState([]);
const [open, setOpen] = useState(false);
const { itemsById, pathsById } = useMemo(() => {
const items = new Map();
const paths = new Map();
const walk = (item) => {
items.set(item.id, item);
paths.set(item.id, item.path);
item.children.forEach(walk);
};
tree.forEach(walk);
return { itemsById: items, pathsById: paths };
}, [tree]);
const selectedPaths = useMemo(
() => value.map((id) => pathsById.get(String(id))).filter(Boolean),
[pathsById, value],
);
const checkedIds = useMemo(() => {
const ids = new Set();
selectedPaths.forEach((path) => {
path.forEach((node) => ids.add(node.id));
const item = itemsById.get(path[path.length - 1].id);
if (item) collectDescendantIds(item).forEach((id) => ids.add(id));
});
return ids;
}, [itemsById, selectedPaths]);
useEffect(() => {
setColumns((current) => {
const next = [];
let items = tree;
for (const id of current) {
const item = items.find((entry) => entry.id === String(id));
if (!item?.children.length) break;
next.push(item.id);
items = item.children;
}
return next;
});
}, [tree]);
useEffect(() => {
if (!open || !selectedPaths.length) return;
const path = selectedPaths[selectedPaths.length - 1];
setColumns(path.slice(0, -1).map((node) => node.id));
}, [open]);
const activeColumns = useMemo(() => {
const result = [tree];
columns.forEach((id) => {
const item = itemsById.get(String(id));
if (item?.children.length) result.push(item.children);
});
return result;
}, [columns, itemsById, tree]);
const selectedLabel = selectedPaths.length
? selectedPaths.map((path) => path[path.length - 1]?.title).join('、')
: '筛选流程';
const expandColumn = (depth, item) => {
setColumns((current) => current.slice(0, depth).concat(item.id));
};
const togglePath = (path) => {
const id = String(path[path.length - 1].id);
onChange(value.includes(id) ? value.filter((item) => item !== id) : [...value, id]);
};
return (
<span className="relative inline-flex max-w-full shrink-0">
<Popover isOpen={open} onOpenChange={setOpen}>
<Popover.Trigger>
<Button
aria-label="筛选流程"
className="h-10 w-72 justify-between gap-2 px-3"
isDisabled={isDisabled}
variant="secondary"
>
<span className={`min-w-0 flex-1 truncate text-left ${selectedPaths.length ? 'text-foreground' : 'text-muted'}`}>
{selectedLabel}
</span>
{selectedPaths.length ? <span className="shrink-0 text-xs text-muted">{selectedPaths.length}</span> : null}
</Button>
</Popover.Trigger>
{selectedPaths.length ? (
<button
aria-label="清空流程筛选"
className="absolute right-2 top-1/2 z-10 grid size-6 -translate-y-1/2 place-items-center rounded-full text-muted hover:bg-surface-secondary hover:text-foreground"
onClick={() => onChange([])}
type="button"
>
<Close size={12} />
</button>
) : null}
<Popover.Content className="overflow-hidden rounded-xl border border-separator bg-background shadow-lg" placement="bottom start">
<Popover.Dialog aria-label="筛选流程">
<div className="flex max-h-96 overflow-x-auto">
{activeColumns.map((items, depth) => (
<div
className="max-h-96 w-56 shrink-0 overflow-y-auto border-r border-separator bg-background py-1 last:border-r-0"
key={`process-column-${depth}`}
role="listbox"
aria-label={`第 ${depth + 1} 级流程`}
>
{items.map((item) => {
const checked = checkedIds.has(item.id);
const active = columns[depth] === item.id;
return (
<div
aria-checked={checked}
aria-selected={active}
className={`flex min-h-10 cursor-pointer select-none items-center gap-2 px-3 text-sm hover:bg-surface-secondary ${active ? 'bg-surface-secondary/80' : ''}`}
key={item.id}
onClick={() => item.children.length && expandColumn(depth, item)}
role="option"
title={item.title}
>
<span
className="inline-flex shrink-0"
onClick={(event) => event.stopPropagation()}
role="presentation"
>
<Checkbox
aria-label={`选择 ${item.title}`}
className="pointer-events-auto"
isSelected={checked}
onChange={() => togglePath(item.path)}
variant="secondary"
>
<Checkbox.Content className="size-4">
<Checkbox.Control className="size-4 rounded-[4px]">
<Checkbox.Indicator />
</Checkbox.Control>
</Checkbox.Content>
</Checkbox>
</span>
<span className={`min-w-0 flex-1 truncate ${checked ? 'text-foreground' : 'text-foreground/85'}`}>
{item.title}
</span>
{item.children.length ? (
<ChevronRight className={`size-4 shrink-0 transition-transform ${active ? 'text-foreground' : 'text-muted'}`} />
) : null}
</div>
);
})}
</div>
))}
</div>
</Popover.Dialog>
</Popover.Content>
</Popover>
</span>
);
}
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
- 拓扑图沿用旧版 React Flow + Dagre 的层级画布逻辑,左侧提供“全部 / 待分类 / 节点 / 删除节点”四个列表 Tab;“节点”和“删除节点”列表支持搜索和点击定位,点击隐藏层级中的节点时会先自动展开该节点的全部祖先路径,再将画布平滑移动到目标节点并打开详情。打开时默认展开根节点和第一层流程节点,第二层及更深节点按节点右侧计数按钮逐级展开。节点支持拖拽、缩放、邻接连线、展开/折叠和双击进入关联制度详情。 - 拓扑图沿用旧版 React Flow + Dagre 的层级画布逻辑,左侧提供“全部 / 待分类 / 节点 / 删除节点”四个列表 Tab;“节点”和“删除节点”列表支持搜索和点击定位,点击隐藏层级中的节点时会先自动展开该节点的全部祖先路径,再将画布平滑移动到目标节点并打开详情。打开时默认展开根节点和第一层流程节点,第二层及更深节点按节点右侧计数按钮逐级展开。节点支持拖拽、缩放、邻接连线、展开/折叠和双击进入关联制度详情。
- 拓扑页高度锁定为当前视口可用高度,标题、工具栏和画布保持在当前屏幕内;节点列表和详情区域在容器内部滚动,页面外层不因节点数量增长而整体向下延伸。 - 拓扑页高度锁定为当前视口可用高度,标题、工具栏和画布保持在当前屏幕内;节点列表和详情区域在容器内部滚动,页面外层不因节点数量增长而整体向下延伸。
- 工具栏支持现行制度显示、已移除节点显示、现行/待新增/待修订/待废止筛选、当前版本和归档版本切换;归档版本只读,不能导入或保存。 - 工具栏支持现行制度显示、已移除节点显示、现行/待新增/待修订/待废止筛选、当前版本和归档版本切换;归档版本只读,不能导入或保存。
- 流程筛选使用多层级联路径,不将全部节点平铺到单层下拉;用户从第一层流程开始逐级展开并勾选路径。筛选保留选中路径、路径祖先和最后一个节点的子孙,不会因保留根节点路径而放行同一父级下的其他流程分支。
- 右侧详情支持编辑标题和备注、查看流程路径/制度路径/历史版本、折叠子节点和打开关联制度;右键可添加流程节点或描述。 - 右侧详情支持编辑标题和备注、查看流程路径/制度路径/历史版本、折叠子节点和打开关联制度;右键可添加流程节点或描述。
- 当前版本支持上传 `.xmind`(兼容 `content.json` 与 `content.xml`),会将 `《制度名称》` 主题匹配到制度记录,生成流程节点及连线,并同步 `graph_process` 分类;也支持将待处理制度拖入选中节点下。 - 当前版本支持上传 `.xmind`(兼容 `content.json` 与 `content.xml`),会将 `《制度名称》` 主题匹配到制度记录,生成流程节点及连线,并同步 `graph_process` 分类;也支持将待处理制度拖入选中节点下。
- 保存写入 `internal_regulation_graph` 的 `graph_info.nodes/edges`,导出生成制度列表 CSV;XMind 导入、节点编辑和拖拽归类均需保存后持久化图谱。 - 保存写入 `internal_regulation_graph` 的 `graph_info.nodes/edges`,导出生成制度列表 CSV;XMind 导入、节点编辑和拖拽归类均需保存后持久化图谱。
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment