Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
I
internal_regulation_manager_new
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
internal_regulation_manager_new
Commits
4a0c4abf
Commit
4a0c4abf
authored
Sep 15, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 增加拓扑图多层级联流程筛选
parent
64fcb2ae
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
205 additions
and
4 deletions
+205
-4
ProcessCascade.jsx
src/components/ProcessCascade.jsx
+176
-0
RegulationGraph.jsx
src/pages/internal_regulation_manager/RegulationGraph.jsx
+28
-4
internal-regulation.business-flow.md
...l_regulation_manager/internal-regulation.business-flow.md
+1
-0
No files found.
src/components/ProcessCascade.jsx
0 → 100644
View file @
4a0c4abf
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
>
);
}
src/pages/internal_regulation_manager/RegulationGraph.jsx
View file @
4a0c4abf
...
...
@@ -18,6 +18,7 @@ import {
Search
,
Upload
,
}
from
'@/components/AppIcons'
;
import
ProcessCascade
from
'@/components/ProcessCascade'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
GraphNode
from
'./GraphNode'
;
...
...
@@ -211,6 +212,28 @@ function xmindTreeToNodes(root, regulations, revisions) {
return
{
nodes
,
edges
:
nodes
.
filter
((
node
)
=>
node
.
parent
&&
nodes
.
some
((
parent
)
=>
parent
.
id
===
node
.
parent
)).
map
((
node
)
=>
({
id
:
`
${
node
.
parent
}
->
${
node
.
id
}
`
,
source
:
node
.
parent
,
target
:
node
.
id
}))
};
}
function
buildProcessTree
(
nodes
)
{
const
childrenByParent
=
new
Map
();
nodes
.
forEach
((
node
)
=>
{
const
parent
=
node
.
parent
?
String
(
node
.
parent
)
:
''
;
childrenByParent
.
set
(
parent
,
[...(
childrenByParent
.
get
(
parent
)
||
[]),
node
]);
});
const
walk
=
(
node
,
path
)
=>
{
const
nextPath
=
[...
path
,
{
id
:
String
(
node
.
id
),
title
:
node
.
title
}];
return
{
id
:
String
(
node
.
id
),
path
:
nextPath
,
title
:
node
.
title
,
children
:
(
childrenByParent
.
get
(
String
(
node
.
id
))
||
[]).
map
((
child
)
=>
walk
(
child
,
nextPath
)),
};
};
const
roots
=
childrenByParent
.
get
(
''
)
||
[];
return
roots
.
flatMap
((
node
)
=>
{
const
children
=
(
childrenByParent
.
get
(
String
(
node
.
id
))
||
[]).
map
((
child
)
=>
walk
(
child
,
[{
id
:
String
(
node
.
id
),
title
:
node
.
title
}]));
return
children
.
length
?
children
:
[{
id
:
String
(
node
.
id
),
path
:
[{
id
:
String
(
node
.
id
),
title
:
node
.
title
}],
title
:
node
.
title
,
children
:
[]
}];
});
}
export
default
function
RegulationGraph
()
{
const
[
graph
,
setGraph
]
=
useState
(
EMPTY_GRAPH
);
const
[
regulations
,
setRegulations
]
=
useState
([]);
...
...
@@ -278,14 +301,15 @@ export default function RegulationGraph() {
if
(
selectedGroupIds
.
length
)
{
const
selected
=
new
Set
(
selectedGroupIds
.
map
(
String
));
selectedGroupIds
.
forEach
((
groupId
)
=>
{
if
(
!
nodeById
.
has
(
String
(
groupId
)))
return
;
selected
.
add
(
String
(
groupId
));
let
parent
=
nodeById
.
get
(
String
(
groupId
));
while
(
parent
)
{
selected
.
add
(
parent
.
id
);
candidates
.
filter
((
node
)
=>
node
.
parent
===
parent
.
id
).
forEach
((
node
)
=>
selected
.
add
(
node
.
id
));
parent
=
parent
.
parent
?
nodeById
.
get
(
String
(
parent
.
parent
))
:
null
;
}
});
const
queue
=
[...
selected
]
;
const
queue
=
selectedGroupIds
.
map
(
String
)
;
while
(
queue
.
length
)
{
const
parentId
=
queue
.
shift
();
candidates
.
filter
((
node
)
=>
node
.
parent
===
parentId
).
forEach
((
node
)
=>
{
...
...
@@ -341,7 +365,7 @@ export default function RegulationGraph() {
return
[...
regulationItems
,
...
additionItems
].
filter
((
record
)
=>
!
query
||
String
(
metadataOf
(
record
).
regulation_name
||
record
.
title
||
''
).
toLocaleLowerCase
().
includes
(
query
));
},
[
regulations
,
revisions
,
searchText
]);
const
selectedNode
=
selectedId
?
nodeById
.
get
(
String
(
selectedId
))
:
null
;
const
process
Options
=
useMemo
(()
=>
graph
.
nodes
.
filter
((
node
)
=>
node
.
type
===
'groupNode'
).
map
((
node
)
=>
({
value
:
node
.
id
,
label
:
node
.
title
})
),
[
graph
.
nodes
]);
const
process
Tree
=
useMemo
(()
=>
buildProcessTree
(
graph
.
nodes
),
[
graph
.
nodes
]);
const
toggleCollapsed
=
useCallback
((
nodeId
)
=>
setCollapsedIds
((
current
)
=>
{
const
next
=
new
Set
(
current
);
const
id
=
String
(
nodeId
);
if
(
next
.
has
(
id
))
next
.
delete
(
id
);
else
next
.
add
(
id
);
return
next
;
}),
[]);
const
visibleEdges
=
useMemo
(()
=>
graph
.
edges
.
filter
((
edge
)
=>
visibleIds
.
has
(
edge
.
source
)
&&
visibleIds
.
has
(
edge
.
target
)),
[
graph
.
edges
,
visibleIds
]);
const
flowNodes
=
useMemo
(()
=>
{
...
...
@@ -460,7 +484,7 @@ export default function RegulationGraph() {
{
notice
?
<
div
role=
"status"
className=
"rounded-xl border border-success/30 bg-success/5 p-3 text-sm text-success"
>
{
notice
}
</
div
>
:
null
}
<
div
className=
"flex min-h-0 flex-1 overflow-hidden rounded-2xl border border-separator bg-background shadow-sm"
ref=
{
canvasRef
}
>
<
aside
className=
{
`shrink-0 overflow-hidden border-e border-separator transition-[width] ${leftOpen ? 'w-72' : 'w-0'}`
}
><
div
className=
"flex h-full w-72 flex-col"
><
div
className=
"border-b border-separator p-3"
><
TextField
fullWidth
value=
{
searchText
}
onChange=
{
setSearchText
}
><
Label
className=
"sr-only"
>
搜索节点
</
Label
><
div
className=
"relative"
><
Search
className=
"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted"
size=
{
16
}
/><
Input
className=
"pl-9"
fullWidth
placeholder=
"搜索制度或节点"
variant=
"secondary"
/></
div
></
TextField
></
div
><
Tabs
className=
"min-w-0"
selectedKey=
{
activePanel
}
variant=
"primary"
onSelectionChange=
{
(
key
)
=>
setActivePanel
(
String
(
key
))
}
><
Tabs
.
ListContainer
className=
"w-full min-w-0 overflow-hidden border-b border-separator px-2"
><
Tabs
.
List
className=
"grid w-full min-w-0 grid-cols-4"
style=
{
{
display
:
'grid'
,
gridTemplateColumns
:
'repeat(4, minmax(0, 1fr))'
,
width
:
'100%'
}
}
aria
-
label=
"拓扑图节点列表"
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"all"
>
全部
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"pending"
>
待分类
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"nodes"
>
节点
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"removed"
>
删除节点
<
Tabs
.
Indicator
/></
Tabs
.
Tab
></
Tabs
.
List
></
Tabs
.
ListContainer
></
Tabs
><
div
className=
"min-h-0 flex-1 overflow-y-auto p-2"
>
{
listNodes
.
map
((
node
)
=>
{
const
isRecordList
=
activePanel
===
'all'
||
activePanel
===
'pending'
;
const
title
=
isRecordList
?
metadataOf
(
node
).
regulation_name
||
node
.
title
:
node
.
title
;
const
type
=
isRecordList
?
node
.
__pendingStatus
||
(
metadataOf
(
node
).
type
===
'新增'
?
'addition'
:
metadataOf
(
node
).
type
===
'废止'
?
'forbidden'
:
'revise'
)
:
nodeStatus
(
node
);
return
<
button
draggable=
{
isRecordList
}
onDragStart=
{
(
event
)
=>
{
event
.
dataTransfer
.
effectAllowed
=
'copy'
;
event
.
dataTransfer
.
setData
(
'application/x-regulation-revision'
,
JSON
.
stringify
(
node
));
}
}
className=
{
`flex min-h-11 w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm hover:bg-surface-secondary ${selectedId === node.id ? 'bg-accent/10 text-accent' : ''}`
}
key=
{
node
.
_id
||
node
.
id
}
onClick=
{
()
=>
isRecordList
?
null
:
focusNode
(
node
)
}
><
span
className=
"min-w-0 flex-1 truncate"
>
{
title
}
</
span
><
Chip
color=
{
STATUS_OPTIONS
.
find
((
item
)
=>
item
.
value
===
type
)?.
color
||
'default'
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
isRecordList
?
(
type
===
'active'
?
'现行有效'
:
`待${metadataOf(node).type || '处理'}`
)
:
nodeTypeLabel
(
node
.
type
)
}
</
Chip
.
Label
></
Chip
></
button
>;
})
}{
listNodes
.
length
===
0
?
<
p
className=
"p-4 text-center text-sm text-muted"
>
暂无节点
</
p
>
:
null
}
</
div
></
div
></
aside
>
<
main
className=
"relative min-h-0 min-w-0 flex-1 bg-surface-secondary/30"
onDragOver=
{
(
event
)
=>
event
.
preventDefault
()
}
onDrop=
{
dropPendingRevision
}
><
div
className=
"absolute inset-x-3 top-3 z-10 flex flex-wrap items-center justify-between gap-2"
><
div
className=
"flex flex-wrap items-center gap-2 rounded-xl border border-separator bg-background/95 p-2 shadow-sm backdrop-blur"
><
XinyuanAutocomplete
className=
"w-40"
aria
-
label=
"归档版本"
options=
{
archives
.
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
archive
}
onSelectionChange=
{
(
value
)
=>
loadArchive
(
String
(
value
))
}
/><
XinyuanAutocomplete
className=
"w-52"
aria
-
label=
"流程筛选"
options=
{
processOptions
}
selectedKeys=
{
new
Set
(
selectedGroupIds
)
}
selectionMode=
"multiple"
onSelectionChange=
{
(
values
)
=>
setSelectedGroupIds
(
values
===
'all'
?
processOptions
.
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
values
||
[]))
}
/><
Checkbox
isSelected=
{
showCurrent
}
variant=
"secondary"
onChange=
{
setShowCurrent
}
><
Checkbox
.
Content
><
Checkbox
.
Control
><
Checkbox
.
Indicator
/></
Checkbox
.
Control
></
Checkbox
.
Content
><
Label
>
显示现行
</
Label
></
Checkbox
><
Checkbox
isSelected=
{
showRemoved
}
variant=
"secondary"
onChange=
{
setShowRemoved
}
><
Checkbox
.
Content
><
Checkbox
.
Control
><
Checkbox
.
Indicator
/></
Checkbox
.
Control
></
Checkbox
.
Content
><
Label
>
显示已移除
</
Label
></
Checkbox
>
{
showCurrent
?
<
div
className=
"flex items-center gap-1"
>
{
STATUS_OPTIONS
.
map
((
option
)
=>
<
button
className=
{
`rounded-md px-2 py-1 text-xs ${selectedStatuses.includes(option.value) ? 'bg-accent/10 text-accent' : 'text-muted hover:bg-surface-secondary'}`
}
key=
{
option
.
value
}
onClick=
{
()
=>
setSelectedStatuses
((
current
)
=>
current
.
includes
(
option
.
value
)
?
current
.
filter
((
value
)
=>
value
!==
option
.
value
)
:
[...
current
,
option
.
value
])
}
>
{
option
.
label
}
</
button
>)
}
</
div
>
:
null
}
</
div
><
div
className=
"flex items-center gap-1 rounded-xl border border-separator bg-background/95 p-1 shadow-sm backdrop-blur"
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"恢复视图"
size=
"sm"
variant=
"tertiary"
onPress=
{
restoreView
}
><
Reload
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
恢复视图
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"展开全部"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
setCollapsedIds
(
new
Set
())
}
><
Expand
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
展开全部
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"折叠全部"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
setCollapsedIds
(
defaultCollapsedIds
(
graph
.
nodes
))
}
><
ArrowPathIcon
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
折叠到第一层
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"全屏查看"
size=
"sm"
variant=
"tertiary"
onPress=
{
toggleFullscreen
}
><
Fullscreen
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
全屏查看
</
Tooltip
.
Content
></
Tooltip
></
div
></
div
><
div
className=
"h-full min-h-0 w-full"
onContextMenu=
{
(
event
)
=>
event
.
preventDefault
()
}
>
{
visibleNodes
.
length
?
<
ReactFlowProvider
><
ReactFlow
nodes=
{
flowNodes
}
edges=
{
visibleEdges
}
nodeTypes=
{
NODE_TYPES
}
fitView
onInit=
{
(
instance
)
=>
{
flowRef
.
current
=
instance
;
instance
.
fitView
({
padding
:
0.22
});
}
}
onNodeClick=
{
(
_
,
node
)
=>
focusNode
(
nodeById
.
get
(
String
(
node
.
id
)))
}
onNodeDoubleClick=
{
(
_
,
node
)
=>
{
const
current
=
nodeById
.
get
(
String
(
node
.
id
));
const
relatedId
=
current
?.
relatedRegulation
||
current
?.
relatedReviseRegulation
;
if
(
relatedId
)
history
.
push
(
`/internal_regulation/${relatedId}`
);
}
}
onNodeDragStop=
{
(
_
,
node
)
=>
setGraph
((
current
)
=>
({
...
current
,
nodes
:
current
.
nodes
.
map
((
item
)
=>
item
.
id
===
node
.
id
?
{
...
item
,
position
:
node
.
position
}
:
item
)
}))
}
onNodeContextMenu=
{
(
event
,
node
)
=>
{
event
.
preventDefault
();
const
rect
=
canvasRef
.
current
?.
getBoundingClientRect
();
if
(
rect
)
setContextMenu
({
x
:
event
.
clientX
-
rect
.
left
,
y
:
event
.
clientY
-
rect
.
top
,
nodeId
:
String
(
node
.
id
)
});
}
}
onPaneContextMenu=
{
(
event
)
=>
{
event
.
preventDefault
();
const
rect
=
canvasRef
.
current
?.
getBoundingClientRect
();
if
(
rect
)
setContextMenu
({
x
:
event
.
clientX
-
rect
.
left
,
y
:
event
.
clientY
-
rect
.
top
,
nodeId
:
null
});
}
}
onPaneClick=
{
()
=>
setContextMenu
(
null
)
}
proOptions=
{
{
hideAttribution
:
true
}
}
><
MiniMap
pannable
zoomable
/><
Controls
showInteractive=
{
false
}
/><
Background
color=
"#e2e8f0"
gap=
{
24
}
/></
ReactFlow
></
ReactFlowProvider
>
:
<
div
className=
"flex h-full min-h-0 items-center justify-center text-sm text-muted"
>
暂无可展示的拓扑节点
</
div
>
}
</
div
>
{
contextMenu
?
<
div
className=
"absolute z-30 w-40 rounded-lg border border-separator bg-background p-1 shadow-lg"
style=
{
{
left
:
contextMenu
.
x
,
top
:
contextMenu
.
y
}
}
onClick=
{
(
event
)
=>
event
.
stopPropagation
()
}
><
button
className=
"flex min-h-10 w-full items-center gap-2 rounded-md px-3 text-left text-sm hover:bg-surface-secondary"
onClick=
{
()
=>
openAddNode
(
contextMenu
.
nodeId
,
'group'
)
}
><
PlusIcon
size=
{
15
}
/>
添加流程节点
</
button
>
{
contextMenu
.
nodeId
?
<
button
className=
"flex min-h-10 w-full items-center gap-2 rounded-md px-3 text-left text-sm hover:bg-surface-secondary"
onClick=
{
()
=>
openAddNode
(
contextMenu
.
nodeId
,
'description'
)
}
><
Edit
size=
{
15
}
/>
添加描述
</
button
>
:
null
}
</
div
>
:
null
}
<
button
aria
-
label=
{
leftOpen
?
'收起左侧节点列表'
:
'展开左侧节点列表'
}
className=
"absolute left-1 top-1/2 z-20 grid size-8 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-separator bg-background shadow-sm"
onClick=
{
()
=>
setLeftOpen
((
value
)
=>
!
value
)
}
><
ChevronRight
className=
{
leftOpen
?
'rotate-180'
:
''
}
size=
{
15
}
/></
button
></
main
>
<
main
className=
"relative min-h-0 min-w-0 flex-1 bg-surface-secondary/30"
onDragOver=
{
(
event
)
=>
event
.
preventDefault
()
}
onDrop=
{
dropPendingRevision
}
><
div
className=
"absolute inset-x-3 top-3 z-10 flex flex-wrap items-center justify-between gap-2"
><
div
className=
"flex flex-wrap items-center gap-2 rounded-xl border border-separator bg-background/95 p-2 shadow-sm backdrop-blur"
><
XinyuanAutocomplete
className=
"w-40"
aria
-
label=
"归档版本"
options=
{
archives
.
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
archive
}
onSelectionChange=
{
(
value
)
=>
loadArchive
(
String
(
value
))
}
/><
ProcessCascade
tree=
{
processTree
}
value=
{
selectedGroupIds
}
onChange=
{
setSelectedGroupIds
}
/><
Checkbox
isSelected=
{
showCurrent
}
variant=
"secondary"
onChange=
{
setShowCurrent
}
><
Checkbox
.
Content
><
Checkbox
.
Control
><
Checkbox
.
Indicator
/></
Checkbox
.
Control
></
Checkbox
.
Content
><
Label
>
显示现行
</
Label
></
Checkbox
><
Checkbox
isSelected=
{
showRemoved
}
variant=
"secondary"
onChange=
{
setShowRemoved
}
><
Checkbox
.
Content
><
Checkbox
.
Control
><
Checkbox
.
Indicator
/></
Checkbox
.
Control
></
Checkbox
.
Content
><
Label
>
显示已移除
</
Label
></
Checkbox
>
{
showCurrent
?
<
div
className=
"flex items-center gap-1"
>
{
STATUS_OPTIONS
.
map
((
option
)
=>
<
button
className=
{
`rounded-md px-2 py-1 text-xs ${selectedStatuses.includes(option.value) ? 'bg-accent/10 text-accent' : 'text-muted hover:bg-surface-secondary'}`
}
key=
{
option
.
value
}
onClick=
{
()
=>
setSelectedStatuses
((
current
)
=>
current
.
includes
(
option
.
value
)
?
current
.
filter
((
value
)
=>
value
!==
option
.
value
)
:
[...
current
,
option
.
value
])
}
>
{
option
.
label
}
</
button
>)
}
</
div
>
:
null
}
</
div
><
div
className=
"flex items-center gap-1 rounded-xl border border-separator bg-background/95 p-1 shadow-sm backdrop-blur"
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"恢复视图"
size=
"sm"
variant=
"tertiary"
onPress=
{
restoreView
}
><
Reload
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
恢复视图
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"展开全部"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
setCollapsedIds
(
new
Set
())
}
><
Expand
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
展开全部
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"折叠全部"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
setCollapsedIds
(
defaultCollapsedIds
(
graph
.
nodes
))
}
><
ArrowPathIcon
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
折叠到第一层
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"全屏查看"
size=
"sm"
variant=
"tertiary"
onPress=
{
toggleFullscreen
}
><
Fullscreen
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
全屏查看
</
Tooltip
.
Content
></
Tooltip
></
div
></
div
><
div
className=
"h-full min-h-0 w-full"
onContextMenu=
{
(
event
)
=>
event
.
preventDefault
()
}
>
{
visibleNodes
.
length
?
<
ReactFlowProvider
><
ReactFlow
nodes=
{
flowNodes
}
edges=
{
visibleEdges
}
nodeTypes=
{
NODE_TYPES
}
fitView
onInit=
{
(
instance
)
=>
{
flowRef
.
current
=
instance
;
instance
.
fitView
({
padding
:
0.22
});
}
}
onNodeClick=
{
(
_
,
node
)
=>
focusNode
(
nodeById
.
get
(
String
(
node
.
id
)))
}
onNodeDoubleClick=
{
(
_
,
node
)
=>
{
const
current
=
nodeById
.
get
(
String
(
node
.
id
));
const
relatedId
=
current
?.
relatedRegulation
||
current
?.
relatedReviseRegulation
;
if
(
relatedId
)
history
.
push
(
`/internal_regulation/${relatedId}`
);
}
}
onNodeDragStop=
{
(
_
,
node
)
=>
setGraph
((
current
)
=>
({
...
current
,
nodes
:
current
.
nodes
.
map
((
item
)
=>
item
.
id
===
node
.
id
?
{
...
item
,
position
:
node
.
position
}
:
item
)
}))
}
onNodeContextMenu=
{
(
event
,
node
)
=>
{
event
.
preventDefault
();
const
rect
=
canvasRef
.
current
?.
getBoundingClientRect
();
if
(
rect
)
setContextMenu
({
x
:
event
.
clientX
-
rect
.
left
,
y
:
event
.
clientY
-
rect
.
top
,
nodeId
:
String
(
node
.
id
)
});
}
}
onPaneContextMenu=
{
(
event
)
=>
{
event
.
preventDefault
();
const
rect
=
canvasRef
.
current
?.
getBoundingClientRect
();
if
(
rect
)
setContextMenu
({
x
:
event
.
clientX
-
rect
.
left
,
y
:
event
.
clientY
-
rect
.
top
,
nodeId
:
null
});
}
}
onPaneClick=
{
()
=>
setContextMenu
(
null
)
}
proOptions=
{
{
hideAttribution
:
true
}
}
><
MiniMap
pannable
zoomable
/><
Controls
showInteractive=
{
false
}
/><
Background
color=
"#e2e8f0"
gap=
{
24
}
/></
ReactFlow
></
ReactFlowProvider
>
:
<
div
className=
"flex h-full min-h-0 items-center justify-center text-sm text-muted"
>
暂无可展示的拓扑节点
</
div
>
}
</
div
>
{
contextMenu
?
<
div
className=
"absolute z-30 w-40 rounded-lg border border-separator bg-background p-1 shadow-lg"
style=
{
{
left
:
contextMenu
.
x
,
top
:
contextMenu
.
y
}
}
onClick=
{
(
event
)
=>
event
.
stopPropagation
()
}
><
button
className=
"flex min-h-10 w-full items-center gap-2 rounded-md px-3 text-left text-sm hover:bg-surface-secondary"
onClick=
{
()
=>
openAddNode
(
contextMenu
.
nodeId
,
'group'
)
}
><
PlusIcon
size=
{
15
}
/>
添加流程节点
</
button
>
{
contextMenu
.
nodeId
?
<
button
className=
"flex min-h-10 w-full items-center gap-2 rounded-md px-3 text-left text-sm hover:bg-surface-secondary"
onClick=
{
()
=>
openAddNode
(
contextMenu
.
nodeId
,
'description'
)
}
><
Edit
size=
{
15
}
/>
添加描述
</
button
>
:
null
}
</
div
>
:
null
}
<
button
aria
-
label=
{
leftOpen
?
'收起左侧节点列表'
:
'展开左侧节点列表'
}
className=
"absolute left-1 top-1/2 z-20 grid size-8 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-separator bg-background shadow-sm"
onClick=
{
()
=>
setLeftOpen
((
value
)
=>
!
value
)
}
><
ChevronRight
className=
{
leftOpen
?
'rotate-180'
:
''
}
size=
{
15
}
/></
button
></
main
>
<
aside
className=
{
`shrink-0 overflow-hidden border-s border-separator transition-[width] ${rightOpen ? 'w-80' : 'w-0'}`
}
><
div
className=
"flex h-full w-80 flex-col"
>
{
selectedNode
?
<><
div
className=
"flex items-center justify-between border-b border-separator p-4"
><
div
><
p
className=
"text-xs text-muted"
>
{
nodeTypeLabel
(
selectedNode
.
type
)
}
</
p
><
h2
className=
"mt-1 max-w-56 truncate font-semibold"
>
{
selectedNode
.
title
}
</
h2
></
div
><
Button
isIconOnly
aria
-
label=
"清除选择"
size=
"sm"
variant=
"ghost"
onPress=
{
()
=>
setSelectedId
(
null
)
}
>
×
</
Button
></
div
><
div
className=
"min-h-0 flex-1 overflow-y-auto p-4"
><
div
className=
"space-y-4"
><
TextField
fullWidth
value=
{
selectedNode
.
title
}
onChange=
{
(
value
)
=>
setNodeField
(
'title'
,
value
)
}
><
Label
>
标题
</
Label
><
Input
fullWidth
variant=
"secondary"
/></
TextField
><
TextField
fullWidth
value=
{
selectedNode
.
content
||
''
}
onChange=
{
(
value
)
=>
setNodeField
(
'content'
,
value
)
}
><
Label
>
备注
</
Label
><
TextArea
fullWidth
rows=
{
5
}
variant=
"secondary"
/></
TextField
><
div
><
Label
>
节点状态
</
Label
><
div
className=
"mt-2 flex flex-wrap gap-2"
><
Chip
color=
{
STATUS_OPTIONS
.
find
((
item
)
=>
item
.
value
===
nodeStatus
(
selectedNode
))?.
color
||
'default'
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
STATUS_OPTIONS
.
find
((
item
)
=>
item
.
value
===
nodeStatus
(
selectedNode
))?.
label
||
nodeTypeLabel
(
selectedNode
.
type
)
}
</
Chip
.
Label
></
Chip
></
div
></
div
><
div
><
Label
>
节点操作
</
Label
><
div
className=
"mt-2 grid gap-2"
><
Button
variant=
"secondary"
onPress=
{
()
=>
toggleCollapsed
(
selectedNode
.
id
)
}
>
{
collapsedIds
.
has
(
selectedNode
.
id
)
?
'展开子节点'
:
'折叠子节点'
}
</
Button
>
{
selectedNode
.
relatedRegulation
||
selectedNode
.
relatedReviseRegulation
?
<
Button
variant=
"tertiary"
onPress=
{
()
=>
history
.
push
(
`/internal_regulation/${selectedNode.relatedRegulation || selectedNode.relatedReviseRegulation}`
)
}
><
Edit
size=
{
16
}
/>
查看关联制度
</
Button
>
:
null
}
</
div
></
div
><
Tabs
defaultSelectedKey=
"process"
variant=
"primary"
><
Tabs
.
ListContainer
className=
"border-b border-separator"
><
Tabs
.
List
aria
-
label=
"节点信息"
><
Tabs
.
Tab
id=
"process"
>
流程
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
id=
"regulation"
>
制度
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
id=
"history"
>
历史
<
Tabs
.
Indicator
/></
Tabs
.
Tab
></
Tabs
.
List
></
Tabs
.
ListContainer
><
Tabs
.
Panel
className=
"pt-4"
id=
"process"
><
div
className=
"space-y-2 text-sm text-muted"
>
{
selectedNode
.
ancestors
.
filter
((
item
)
=>
item
.
type
===
'groupNode'
).
map
((
item
)
=>
<
div
className=
"flex items-center gap-1"
key=
{
item
.
id
}
><
ChevronRight
size=
{
14
}
/>
{
item
.
title
}
</
div
>)
}{
!
selectedNode
.
ancestors
.
some
((
item
)
=>
item
.
type
===
'groupNode'
)
?
'暂无流程路径'
:
null
}
</
div
></
Tabs
.
Panel
><
Tabs
.
Panel
className=
"pt-4"
id=
"regulation"
><
div
className=
"space-y-2 text-sm text-muted"
>
{
selectedNode
.
ancestors
.
filter
((
item
)
=>
item
.
type
===
'regulationNode'
).
map
((
item
)
=>
<
div
className=
"flex items-center gap-1"
key=
{
item
.
id
}
><
ChevronRight
size=
{
14
}
/>
{
item
.
title
}
</
div
>)
}{
!
selectedNode
.
ancestors
.
some
((
item
)
=>
item
.
type
===
'regulationNode'
)
?
'暂无制度路径'
:
null
}
</
div
></
Tabs
.
Panel
><
Tabs
.
Panel
className=
"pt-4"
id=
"history"
><
div
className=
"space-y-2 text-sm text-muted"
>
{
selectedNode
.
historyRegulation
.
map
((
item
)
=>
<
button
className=
"block text-left text-accent hover:underline"
key=
{
item
.
_id
||
item
.
id
||
item
.
name
}
onClick=
{
()
=>
history
.
push
(
`/internal_regulation/${item._id || item.name}`
)
}
>
{
metadataOf
(
item
).
regulation_name
||
item
.
title
}
</
button
>)
}{
!
selectedNode
.
historyRegulation
.
length
?
'暂无历史版本'
:
null
}
</
div
></
Tabs
.
Panel
></
Tabs
></
div
></
div
><
div
className=
"border-t border-separator p-3"
><
div
className=
"grid grid-cols-4 gap-2"
><
label
className=
{
`flex min-h-10 cursor-pointer items-center justify-center rounded-lg border border-dashed border-separator text-xs text-muted hover:border-accent ${archive !== '当前版本' || saving ? 'pointer-events-none opacity-50' : ''}`
}
><
Upload
size=
{
15
}
/><
span
className=
"sr-only"
>
导入 XMind
</
span
><
input
accept=
".xmind,.json"
className=
"sr-only"
disabled=
{
archive
!==
'当前版本'
||
saving
}
type=
"file"
onChange=
{
importGraph
}
/></
label
><
Button
isDisabled=
{
saving
||
archive
!==
'当前版本'
}
variant=
"secondary"
onPress=
{
save
}
><
Save
size=
{
15
}
/>
保存
</
Button
><
Button
variant=
"tertiary"
onPress=
{
exportGraph
}
><
Download
size=
{
15
}
/>
导出
</
Button
><
Button
isIconOnly
aria
-
label=
"添加流程节点"
variant=
"tertiary"
onPress=
{
()
=>
openAddNode
(
selectedNode
.
id
,
'group'
)
}
><
PlusIcon
size=
{
16
}
/></
Button
></
div
></
div
></>
:
<
div
className=
"flex h-full items-center justify-center p-6 text-center text-sm text-muted"
>
点击中间节点查看编辑和流程信息
</
div
>
}
</
div
></
aside
>
<
button
aria
-
label=
{
rightOpen
?
'收起右侧节点详情'
:
'展开右侧节点详情'
}
className=
"absolute right-1 top-1/2 z-20 grid size-8 translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-separator bg-background shadow-sm"
onClick=
{
()
=>
setRightOpen
((
value
)
=>
!
value
)
}
><
ChevronRight
className=
{
rightOpen
?
''
:
'rotate-180'
}
size=
{
15
}
/></
button
>
</
div
>
...
...
src/pages/internal_regulation_manager/internal-regulation.business-flow.md
View file @
4a0c4abf
...
...
@@ -32,6 +32,7 @@
-
拓扑图沿用旧版 React Flow + Dagre 的层级画布逻辑,左侧提供“全部 / 待分类 / 节点 / 删除节点”四个列表 Tab;“节点”和“删除节点”列表支持搜索和点击定位,点击隐藏层级中的节点时会先自动展开该节点的全部祖先路径,再将画布平滑移动到目标节点并打开详情。打开时默认展开根节点和第一层流程节点,第二层及更深节点按节点右侧计数按钮逐级展开。节点支持拖拽、缩放、邻接连线、展开/折叠和双击进入关联制度详情。
-
拓扑页高度锁定为当前视口可用高度,标题、工具栏和画布保持在当前屏幕内;节点列表和详情区域在容器内部滚动,页面外层不因节点数量增长而整体向下延伸。
-
工具栏支持现行制度显示、已移除节点显示、现行/待新增/待修订/待废止筛选、当前版本和归档版本切换;归档版本只读,不能导入或保存。
-
流程筛选使用多层级联路径,不将全部节点平铺到单层下拉;用户从第一层流程开始逐级展开并勾选路径。筛选保留选中路径、路径祖先和最后一个节点的子孙,不会因保留根节点路径而放行同一父级下的其他流程分支。
-
右侧详情支持编辑标题和备注、查看流程路径/制度路径/历史版本、折叠子节点和打开关联制度;右键可添加流程节点或描述。
-
当前版本支持上传
`.xmind`
(兼容
`content.json`
与
`content.xml`
),会将
`《制度名称》`
主题匹配到制度记录,生成流程节点及连线,并同步
`graph_process`
分类;也支持将待处理制度拖入选中节点下。
-
保存写入
`internal_regulation_graph`
的
`graph_info.nodes/edges`
,导出生成制度列表 CSV;XMind 导入、节点编辑和拖拽归类均需保存后持久化图谱。
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment