Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
L
law_reference_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
law_reference_new
Commits
86bbaf04
Commit
86bbaf04
authored
Sep 30, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: use modal confirmations and adjust filter tree defaults
parent
a288b353
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
71 additions
and
27 deletions
+71
-27
ConfiguredSpecialViews.jsx
src/pages/ConfiguredSpecialViews.jsx
+13
-3
LawRecordPage.jsx
src/pages/LawRecordPage.jsx
+52
-6
LawSubjectPage.jsx
src/pages/LawSubjectPage.jsx
+2
-16
law-reference.business-flow.md
src/pages/law-reference.business-flow.md
+4
-2
No files found.
src/pages/ConfiguredSpecialViews.jsx
View file @
86bbaf04
...
...
@@ -34,13 +34,23 @@ function useSubjectRecords(subjectName, groupName) {
function
ProcessListView
({
subjectName
,
config
})
{
const
groupSubject
=
config
?.
group_subject
||
'process_flow_group'
;
const
[
groups
,
setGroups
]
=
useState
([]);
const
[
active
,
setActive
]
=
useState
(
''
);
const
[
modal
,
setModal
]
=
useState
(
false
);
const
[
draft
,
setDraft
]
=
useState
({});
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
groups
,
setGroups
]
=
useState
([]);
const
[
active
,
setActive
]
=
useState
(
''
);
const
[
modal
,
setModal
]
=
useState
(
false
);
const
[
draft
,
setDraft
]
=
useState
({});
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
deleting
,
setDeleting
]
=
useState
(
false
);
const
[
deleteTarget
,
setDeleteTarget
]
=
useState
(
null
);
const
[
deleteError
,
setDeleteError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useState
(
''
);
const
{
group
,
items
,
loading
,
error
:
groupError
,
reload
}
=
useSubjectRecords
(
subjectName
,
active
);
const
loadGroups
=
async
()
=>
{
try
{
const
result
=
await
loadRecords
(
groupSubject
,
0
,
100
);
const
next
=
result
?.
records
||
[];
setGroups
(
next
);
setActive
((
current
)
=>
current
||
next
[
0
]?.
name
||
''
);
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'流程分组加载失败'
);
}
};
useEffect
(()
=>
{
loadGroups
();
},
[
groupSubject
]);
const
save
=
async
()
=>
{
setSaving
(
true
);
setError
(
''
);
try
{
if
(
draft
.
group_name
!==
undefined
)
await
createRecord
({
subject
:
groupSubject
,
title
:
draft
.
group_name
,
metadata
:
{
group_name
:
draft
.
group_name
,
process_list
:
[]
}
});
else
if
(
draft
.
editName
)
await
updateRecord
(
draft
.
editName
,
{
title
:
draft
.
title
,
metadata
:
{
remark
:
draft
.
remark
,
source
:
draft
.
source
||
null
}
});
else
{
const
created
=
await
createRecord
({
subject
:
subjectName
,
title
:
draft
.
title
,
metadata
:
{
remark
:
draft
.
remark
,
source
:
draft
.
source
||
null
}
});
await
updateRecord
(
group
.
name
,
{
metadata
:
{
...
group
.
metadata
,
process_list
:
[...(
group
.
metadata
?.
process_list
||
[]).
map
(
valueOf
),
created
.
_id
]
}
});
}
setModal
(
false
);
setDraft
({});
await
loadGroups
();
await
reload
();
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'保存失败'
);
}
finally
{
setSaving
(
false
);
}
};
const
deleteProcess
=
async
(
item
)
=>
{
if
(
!
group
||
!
window
.
confirm
(
'确定删除该流程吗?'
))
return
;
await
removeRecord
(
item
.
name
||
item
.
_id
);
await
updateRecord
(
group
.
name
,
{
metadata
:
{
...
group
.
metadata
,
process_list
:
(
group
.
metadata
?.
process_list
||
[]).
map
(
valueOf
).
filter
((
id
)
=>
id
!==
(
item
.
_id
||
item
.
name
))
}
});
await
reload
();
};
return
<
div
className=
"rc-page-stack"
><
h2
className=
"mb-4 text-xl font-semibold"
>
流程管理
</
h2
>
{
error
||
groupError
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
||
groupError
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
<
GroupTabs
groups=
{
groups
}
active=
{
active
}
onChange=
{
setActive
}
onCreate=
{
()
=>
{
setDraft
({
group_name
:
''
});
setModal
(
true
);
}
}
label=
"流程分组"
/><
Card
><
Card
.
Header
><
Card
.
Title
className=
"mr-auto"
>
{
group
?.
title
||
'请选择流程分组'
}
</
Card
.
Title
>
{
group
?
<
Button
variant=
"primary"
onPress=
{
()
=>
{
setDraft
({});
setModal
(
true
);
}
}
>
新增流程
</
Button
>
:
null
}
</
Card
.
Header
><
Card
.
Content
>
{
loading
?
<
div
className=
"flex min-h-32 items-center justify-center"
><
Spinner
/></
div
>
:
!
items
.
length
?
<
div
className=
"py-16 text-center text-sm text-muted"
>
暂无流程
</
div
>
:
<
ol
className=
"space-y-3"
>
{
items
.
map
((
item
,
index
)
=>
<
li
className=
"flex items-center gap-3 rounded-lg border border-default p-3"
key=
{
item
.
name
||
item
.
_id
}
><
span
className=
"grid h-8 w-8 place-items-center rounded-full bg-accent/10 text-sm font-semibold text-accent"
>
{
index
+
1
}
</
span
><
div
className=
"min-w-0 flex-1"
><
div
className=
"truncate font-medium"
>
{
item
.
title
||
item
.
name
}
</
div
><
div
className=
"text-sm text-muted"
>
{
item
.
metadata
?.
remark
||
'暂无说明'
}
</
div
></
div
><
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
{
setDraft
({
...
item
.
metadata
,
title
:
item
.
title
,
editName
:
item
.
name
});
setModal
(
true
);
}
}
>
编辑
</
Button
><
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
history
.
push
(
`/${subjectName}/${item.name || item._id}`
)
}
>
查看
</
Button
><
Button
size=
"sm"
variant=
"danger"
onPress=
{
()
=>
deleteProcess
(
item
)
}
>
删除
</
Button
></
li
>)
}
</
ol
>
}
</
Card
.
Content
></
Card
><
XinyuanModal
isOpen=
{
modal
}
onOpenChange=
{
setModal
}
title=
{
draft
.
group_name
!==
undefined
?
'新建流程分组'
:
draft
.
editName
?
'编辑流程'
:
'新建流程'
}
footer=
{
<><
Button
variant=
"secondary"
onPress=
{
()
=>
setModal
(
false
)
}
>
取消
</
Button
><
Button
variant=
"primary"
isPending=
{
saving
}
onPress=
{
save
}
>
保存
</
Button
></>
}
><
div
className=
"grid gap-4"
>
{
draft
.
group_name
!==
undefined
?
<
Input
label=
"分组名称"
value=
{
draft
.
group_name
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
group_name
:
event
.
target
.
value
})
}
/>
:
<><
Input
label=
"流程名称"
value=
{
draft
.
title
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
title
:
event
.
target
.
value
})
}
/><
TextArea
label=
"流程说明"
value=
{
draft
.
remark
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
remark
:
event
.
target
.
value
})
}
/></>
}
</
div
></
XinyuanModal
></
div
>;
const
confirmDeleteProcess
=
async
()
=>
{
if
(
!
group
||
!
deleteTarget
)
return
;
setDeleting
(
true
);
setDeleteError
(
''
);
try
{
await
removeRecord
(
deleteTarget
.
name
||
deleteTarget
.
_id
);
await
updateRecord
(
group
.
name
,
{
metadata
:
{
...
group
.
metadata
,
process_list
:
(
group
.
metadata
?.
process_list
||
[]).
map
(
valueOf
).
filter
((
id
)
=>
id
!==
(
deleteTarget
.
_id
||
deleteTarget
.
name
))
}
});
setDeleteTarget
(
null
);
await
reload
();
}
catch
(
requestError
)
{
setDeleteError
(
requestError
?.
message
||
'删除失败'
);
}
finally
{
setDeleting
(
false
);
}
};
return
<
div
className=
"rc-page-stack"
><
h2
className=
"mb-4 text-xl font-semibold"
>
流程管理
</
h2
>
{
error
||
groupError
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
||
groupError
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
<
GroupTabs
groups=
{
groups
}
active=
{
active
}
onChange=
{
setActive
}
onCreate=
{
()
=>
{
setDraft
({
group_name
:
''
});
setModal
(
true
);
}
}
label=
"流程分组"
/><
Card
><
Card
.
Header
><
Card
.
Title
className=
"mr-auto"
>
{
group
?.
title
||
'请选择流程分组'
}
</
Card
.
Title
>
{
group
?
<
Button
variant=
"primary"
onPress=
{
()
=>
{
setDraft
({});
setModal
(
true
);
}
}
>
新增流程
</
Button
>
:
null
}
</
Card
.
Header
><
Card
.
Content
>
{
loading
?
<
div
className=
"flex min-h-32 items-center justify-center"
><
Spinner
/></
div
>
:
!
items
.
length
?
<
div
className=
"py-16 text-center text-sm text-muted"
>
暂无流程
</
div
>
:
<
ol
className=
"space-y-3"
>
{
items
.
map
((
item
,
index
)
=>
<
li
className=
"flex items-center gap-3 rounded-lg border border-default p-3"
key=
{
item
.
name
||
item
.
_id
}
><
span
className=
"grid h-8 w-8 place-items-center rounded-full bg-accent/10 text-sm font-semibold text-accent"
>
{
index
+
1
}
</
span
><
div
className=
"min-w-0 flex-1"
><
div
className=
"truncate font-medium"
>
{
item
.
title
||
item
.
name
}
</
div
><
div
className=
"text-sm text-muted"
>
{
item
.
metadata
?.
remark
||
'暂无说明'
}
</
div
></
div
><
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
{
setDraft
({
...
item
.
metadata
,
title
:
item
.
title
,
editName
:
item
.
name
});
setModal
(
true
);
}
}
>
编辑
</
Button
><
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
history
.
push
(
`/${subjectName}/${item.name || item._id}`
)
}
>
查看
</
Button
><
Button
size=
"sm"
variant=
"danger"
onPress=
{
()
=>
{
setDeleteError
(
''
);
setDeleteTarget
(
item
);
}
}
>
删除
</
Button
></
li
>)
}
</
ol
>
}
</
Card
.
Content
></
Card
><
XinyuanModal
isOpen=
{
Boolean
(
deleteTarget
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
&&
!
deleting
)
setDeleteTarget
(
null
);
}
}
title=
"删除流程"
footer=
{
<><
Button
variant=
"secondary"
isDisabled=
{
deleting
}
onPress=
{
()
=>
setDeleteTarget
(
null
)
}
>
取消
</
Button
><
Button
variant=
"danger"
isPending=
{
deleting
}
onPress=
{
confirmDeleteProcess
}
>
确认删除
</
Button
></>
}
><
p
>
确定删除“
{
titleOf
(
deleteTarget
)
}
”吗?
</
p
>
{
deleteError
?
<
p
className=
"mt-3 text-danger"
role=
"alert"
>
{
deleteError
}
</
p
>
:
null
}
</
XinyuanModal
><
XinyuanModal
isOpen=
{
modal
}
onOpenChange=
{
setModal
}
title=
{
draft
.
group_name
!==
undefined
?
'新建流程分组'
:
draft
.
editName
?
'编辑流程'
:
'新建流程'
}
footer=
{
<><
Button
variant=
"secondary"
onPress=
{
()
=>
setModal
(
false
)
}
>
取消
</
Button
><
Button
variant=
"primary"
isPending=
{
saving
}
onPress=
{
save
}
>
保存
</
Button
></>
}
><
div
className=
"grid gap-4"
>
{
draft
.
group_name
!==
undefined
?
<
Input
label=
"分组名称"
value=
{
draft
.
group_name
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
group_name
:
event
.
target
.
value
})
}
/>
:
<><
Input
label=
"流程名称"
value=
{
draft
.
title
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
title
:
event
.
target
.
value
})
}
/><
TextArea
label=
"流程说明"
value=
{
draft
.
remark
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
remark
:
event
.
target
.
value
})
}
/></>
}
</
div
></
XinyuanModal
></
div
>;
}
export
default
function
ConfiguredSpecialViews
({
view
,
subjectName
,
config
})
{
...
...
src/pages/LawRecordPage.jsx
View file @
86bbaf04
...
...
@@ -202,6 +202,9 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
const
value
=
record
?.
metadata
?.[
field
];
const
attachments
=
Array
.
isArray
(
value
)
?
value
:
value
?
[
value
]
:
[];
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
[
removing
,
setRemoving
]
=
useState
(
false
);
const
[
removeTarget
,
setRemoveTarget
]
=
useState
(
null
);
const
[
removeError
,
setRemoveError
]
=
useState
(
''
);
const
uploadFiles
=
async
(
files
)
=>
{
if
(
!
field
||
!
files
?.
length
)
return
;
...
...
@@ -217,10 +220,19 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
}
};
const
removeAttachment
=
async
(
attachment
)
=>
{
if
(
!
field
||
!
window
.
confirm
(
'确定移除这个附件吗?'
))
return
;
const
key
=
attachment
?.
_id
||
attachment
?.
url
||
attachment
?.
name
||
attachment
?.
title
;
const
confirmRemoveAttachment
=
async
()
=>
{
if
(
!
field
||
!
removeTarget
)
return
;
const
key
=
removeTarget
?.
_id
||
removeTarget
?.
url
||
removeTarget
?.
name
||
removeTarget
?.
title
;
setRemoving
(
true
);
setRemoveError
(
''
);
try
{
await
onPatchMetadata
(
field
,
attachments
.
filter
((
item
)
=>
(
item
?.
_id
||
item
?.
url
||
item
?.
name
||
item
?.
title
)
!==
key
));
setRemoveTarget
(
null
);
}
catch
(
error
)
{
setRemoveError
(
error
?.
message
||
'附件移除失败'
);
}
finally
{
setRemoving
(
false
);
}
};
return
(
...
...
@@ -235,7 +247,7 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
<
a
href=
{
item
?.
url
||
'#'
}
rel=
"noreferrer"
target=
"_blank"
>
{
item
?.
title
||
item
?.
name
||
`附件 ${index + 1}`
}
</
a
>
<
Button
isIconOnly
aria
-
label=
"移除附件"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
removeAttachment
(
item
)
}
>
<
Button
isIconOnly
aria
-
label=
"移除附件"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
{
setRemoveError
(
''
);
setRemoveTarget
(
item
);
}
}
>
<
Close
size=
{
14
}
/>
</
Button
>
</
div
>
...
...
@@ -251,6 +263,21 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
onFilesChange=
{
uploadFiles
}
/>
</
Card
.
Content
>
<
XinyuanModal
footer=
{
(
<>
<
Button
isDisabled=
{
removing
}
variant=
"secondary"
onPress=
{
()
=>
setRemoveTarget
(
null
)
}
>
取消
</
Button
>
<
Button
isPending=
{
removing
}
variant=
"danger"
onPress=
{
confirmRemoveAttachment
}
>
确认移除
</
Button
>
</>
)
}
isOpen=
{
Boolean
(
removeTarget
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
&&
!
removing
)
setRemoveTarget
(
null
);
}
}
size=
"sm"
title=
"移除附件"
>
<
p
>
确定移除“
{
removeTarget
?.
title
||
removeTarget
?.
name
||
removeTarget
?.
url
||
'该附件'
}
”吗?
</
p
>
{
removeError
?
<
p
className=
"mt-3 text-danger"
role=
"alert"
>
{
removeError
}
</
p
>
:
null
}
</
XinyuanModal
>
</
Card
>
);
}
...
...
@@ -339,6 +366,8 @@ function LawRecordDetail() {
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
editing
,
setEditing
]
=
useState
(
false
);
const
[
deletingRecord
,
setDeletingRecord
]
=
useState
(
false
);
const
[
deleteRecordOpen
,
setDeleteRecordOpen
]
=
useState
(
false
);
const
[
historyItems
,
setHistoryItems
]
=
useState
([]);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
collections
,
setCollections
]
=
useState
(()
=>
loadStorageArray
(
COLLECTION_STORAGE_KEY
));
...
...
@@ -569,9 +598,10 @@ function LawRecordDetail() {
};
const
deleteRecord
=
async
()
=>
{
if
(
!
recordName
||
!
window
.
confirm
(
'确定删除这条记录吗?'
)
)
return
;
if
(
!
recordName
)
return
;
try
{
await
removeRecord
(
recordName
);
setDeleteRecordOpen
(
false
);
history
.
push
(
`/
${
subjectName
}
`
);
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'删除失败'
);
...
...
@@ -614,7 +644,7 @@ function LawRecordDetail() {
<
TableActionButton
icon=
{
isCollected
?
StarSolid
:
Star
}
label=
{
isCollected
?
'取消收藏'
:
'收藏'
}
onPress=
{
toggleCollection
}
/>
<
TableActionButton
icon=
{
ArrowDownTrayIcon
}
isDisabled=
{
!
firstFile
}
label=
"下载"
onPress=
{
()
=>
firstFile
?.
url
&&
window
.
open
(
firstFile
.
url
,
'_blank'
,
'noopener,noreferrer'
)
}
/>
<
TableActionButton
icon=
{
Edit
}
label=
"编辑"
onPress=
{
openEditModal
}
/>
<
TableActionButton
icon=
{
Delete
}
label=
"删除"
tone=
"delete"
onPress=
{
deleteRecord
}
/>
<
TableActionButton
icon=
{
Delete
}
label=
"删除"
tone=
"delete"
onPress=
{
()
=>
{
setError
(
''
);
setDeleteRecordOpen
(
true
);
}
}
/>
</
TableActionGroup
>
)
:
null
}
</
div
>
...
...
@@ -790,6 +820,22 @@ function LawRecordDetail() {
</
div
>
</
section
>
</
XinyuanModal
>
<
XinyuanModal
footer=
{
(
<>
<
Button
isDisabled=
{
deletingRecord
}
variant=
"secondary"
onPress=
{
()
=>
setDeleteRecordOpen
(
false
)
}
>
取消
</
Button
>
<
Button
isPending=
{
deletingRecord
}
variant=
"danger"
onPress=
{
deleteRecord
}
>
确认删除
</
Button
>
</>
)
}
isOpen=
{
deleteRecordOpen
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
&&
!
deletingRecord
)
setDeleteRecordOpen
(
false
);
}
}
size=
"sm"
title=
"删除记录"
>
<
p
>
确定删除“
{
record
?.
title
||
record
?.
name
}
”吗?
</
p
>
{
error
?
<
p
className=
"mt-3 text-danger"
role=
"alert"
>
{
error
}
</
p
>
:
null
}
</
XinyuanModal
>
</
div
>
);
}
src/pages/LawSubjectPage.jsx
View file @
86bbaf04
...
...
@@ -1302,23 +1302,9 @@ function SearchTable({ records, fields, subjectName, onEdit, onRemove }) {
/>;
}
function
collectExpandableTreeKeys
(
nodes
=
[])
{
return
nodes
.
flatMap
((
node
)
=>
node
.
children
?.
length
?
[
node
.
key
,
...
collectExpandableTreeKeys
(
node
.
children
)]
:
[]);
}
function
FilterTreeCard
({
title
,
nodes
,
selectedKeys
,
onSelect
})
{
const
[
open
,
setOpen
]
=
useState
(
false
);
const
[
expandedKeys
,
setExpandedKeys
]
=
useState
(()
=>
collectExpandableTreeKeys
(
nodes
));
useEffect
(()
=>
{
const
availableKeys
=
new
Set
(
collectExpandableTreeKeys
(
nodes
));
setExpandedKeys
((
current
)
=>
{
const
retained
=
current
.
filter
((
key
)
=>
availableKeys
.
has
(
key
));
const
added
=
[...
availableKeys
].
filter
((
key
)
=>
!
current
.
includes
(
key
));
const
next
=
[...
retained
,
...
added
];
return
next
.
length
===
current
.
length
&&
next
.
every
((
key
,
index
)
=>
key
===
current
[
index
])
?
current
:
next
;
});
},
[
nodes
]);
const
[
open
,
setOpen
]
=
useState
(
true
);
const
[
expandedKeys
,
setExpandedKeys
]
=
useState
([]);
return
(
<
section
className=
"law-filter-tree-card"
>
...
...
src/pages/law-reference.business-flow.md
View file @
86bbaf04
...
...
@@ -6,7 +6,9 @@
## 浏览与检索
用户进入 Subject 列表后,页面调用
`POST /record/query`
,按配置中的
`list.view`
、排序和搜索 Subject 展示记录。
`search_view`
/
`search`
使用
`POST /search/:subject`
的全文检索结果,
`table`
使用 Record 查询结果渲染表格,
`column`
使用同一结果按
`metadata.level`
分组渲染可滚动列看板,
`card`
使用同一结果渲染卡片列表;
`search`
额外支持模糊搜索和聚合字段筛选,
`column`
对 Subject 的 select 字段提供筛选。法规模块的输入框、按钮、勾选框、卡片和反馈状态统一使用 HeroUI,搜索正文与模糊搜索使用显式 HeroUI Checkbox;搜索页桌面端沿用
`streams-admin`
的左右工作区,左侧约四分之一宽度显示聚合筛选,右侧约四分之三宽度依次显示搜索工具区和结果区,两个区域顶部对齐。搜索工具区将搜索框、检索选项和导入/导出/新建操作放在同一面板中,在右侧内容列内吸顶并停留在顶部导航栏下方;左侧筛选栏同步吸顶并独立滚动。中小屏切换为搜索工具区、筛选区、结果区的纵向结构,避免压缩搜索输入和操作按钮;导入、导出和新建使用统一尺寸的图标按钮,通过可访问名称和悬浮提示说明操作,操作区顶边与搜索框首行对齐。左侧聚合筛选分组默认收起,并与带子节点的目录项一样支持独立展开、折叠;面板不得压缩隐藏目录项。列表分页加载,前端页码从 0 开始传入接口,底部提供 20/50/100 条每页切换;切换页容量、关键词、模糊搜索或筛选条件后回到第一页重新查询,总数分别读取搜索接口的
`hits.total.value`
或 Record 查询接口返回的
`total_records`
/
`total`
/
`count`
。表格和卡片关键词输入采用延迟查询,Subject 搜索页通过回车或搜索按钮提交,搜索结果按标题、正文摘要和标签分层展示;每条结果使用紧凑的轻边框卡片,摘要展开按钮独立于正文段落,避免长列表展开时结构错位。记录标题和 metadata 字段统一展示,点击“查看”进入
`/:subject/:record`
详情,删除使用
`DELETE /record/:name`
软删除。
`/search`
为全局检索入口,调用
`GET /subject`
和
`GET /search/aggregate`
(通过
`aggregateSearch`
封装)按 Subject 分组展示结果,分组切换沿用历史应用的 Subject 标签页逻辑;点击全局搜索结果进入详情时携带
`keyword`
查询参数,详情页自动进入关键词查找和定位流程。
`audit_indicator_list`
使用指标分组、指标新增和执行结果记录流程;
`process_list`
使用流程分组、流程新增和流程记录详情流程。
用户进入 Subject 列表后,页面调用
`POST /record/query`
,按配置中的
`list.view`
、排序和搜索 Subject 展示记录。
`search_view`
/
`search`
使用
`POST /search/:subject`
的全文检索结果,
`table`
使用 Record 查询结果渲染表格,
`column`
使用同一结果按
`metadata.level`
分组渲染可滚动列看板,
`card`
使用同一结果渲染卡片列表;
`search`
额外支持模糊搜索和聚合字段筛选,
`column`
对 Subject 的 select 字段提供筛选。法规模块的输入框、按钮、勾选框、卡片和反馈状态统一使用 HeroUI,搜索正文与模糊搜索使用显式 HeroUI Checkbox;搜索页桌面端沿用
`streams-admin`
的左右工作区,左侧约四分之一宽度显示聚合筛选,右侧约四分之三宽度依次显示搜索工具区和结果区,两个区域顶部对齐。搜索工具区将搜索框、检索选项和导入/导出/新建操作放在同一面板中,在右侧内容列内吸顶并停留在顶部导航栏下方;左侧筛选栏同步吸顶并独立滚动。中小屏切换为搜索工具区、筛选区、结果区的纵向结构,避免压缩搜索输入和操作按钮;导入、导出和新建使用统一尺寸的图标按钮,通过可访问名称和悬浮提示说明操作,操作区顶边与搜索框首行对齐。左侧聚合筛选外层卡片默认展开,内部树节点默认只显示根级并支持逐级展开、折叠;面板不得压缩隐藏目录项。列表分页加载,前端页码从 0 开始传入接口,底部提供 20/50/100 条每页切换;切换页容量、关键词、模糊搜索或筛选条件后回到第一页重新查询,总数分别读取搜索接口的
`hits.total.value`
或 Record 查询接口返回的
`total_records`
/
`total`
/
`count`
。表格和卡片关键词输入采用延迟查询,Subject 搜索页通过回车或搜索按钮提交,搜索结果按标题、正文摘要和标签分层展示;每条结果使用紧凑的轻边框卡片,摘要展开按钮独立于正文段落,避免长列表展开时结构错位。记录标题和 metadata 字段统一展示,点击“查看”进入
`/:subject/:record`
详情,删除使用
`DELETE /record/:name`
软删除。
`/search`
为全局检索入口,调用
`GET /subject`
和
`GET /search/aggregate`
(通过
`aggregateSearch`
封装)按 Subject 分组展示结果,分组切换沿用历史应用的 Subject 标签页逻辑;点击全局搜索结果进入详情时携带
`keyword`
查询参数,详情页自动进入关键词查找和定位流程。
`audit_indicator_list`
使用指标分组、指标新增和执行结果记录流程;
`process_list`
使用流程分组、流程新增和流程记录详情流程。
配置为
`process_list`
时,删除流程前通过 HeroUI Modal 二次确认;确认后调用
`DELETE /record/:name`
软删除流程,并从当前分组的
`metadata.process_list`
中移除对应 ID。失败时保留确认弹窗并显示错误,成功后刷新分组关联和流程列表。
`table`
视图按历史 RecordTable 的常用操作对齐:表头字段可切换升序/降序排序,勾选框支持当前页批量选择,批量删除逐条调用
`DELETE /record/:name`
并刷新列表;导出会按当前 Subject 字段读取全量分页数据并生成 Excel。
`card`
视图保留卡片列表展示和查看、下载、编辑、删除操作,也支持 20/50/100 分页与 Excel 导出;卡片内部统一使用标题区、两列字段信息区和底部操作区,窄屏退化为单列字段,操作图标保留 tooltip 和可访问名称。
`table`
、
`card`
、
`search`
和
`search_view`
顶部均提供导入入口,其中“导入目录”读取 Excel/CSV 第一张表,以字段标签或字段别名映射到 Subject 字段后调用
`POST /record/import`
;“导入文档”上传 Word/PDF,优先转换正文并在 Word 文档可解析时写入
`metadata.document`
,再调用
`POST /record`
创建记录。
...
...
@@ -20,7 +22,7 @@
## 详情与附件
详情字段、正文模板和附件侧栏使用应用配置中的
`detail`
;
`article_view`
显示配置模板,其他详情模式使用结构化字段视图。详情页顶部提供返回列表、收藏、下载、编辑和删除入口,
顶部标题、操作按钮和正文阅读工具栏在滚动正文时保持吸顶,收藏状态保存在本地浏览器状态中,下载使用当前记录正文、文档或附件中的第一个可访问文件。
`detail.content.type`
为
`attachment`
时,正文区域按
`streams-admin`
的 ArticleView 规则从当前记录文件中选择首个 PDF 并直接内嵌预览,同时提供新窗口打开入口;没有可预览 PDF 时回退到配置模板中的附件链接或空状态。普通正文区域按模板安全渲染 HTML,并提供查找关键词、上一处/下一处命中、字号调整、字体色、背景色和引用识别开关。字号由正文容器统一继承并覆盖正文 HTML 中可能残留的内联字号,确保调整后段落、表格和标题同步变化。开启引用识别后,先从正文文本提取
`《…》`
标题,再通过
`POST /record/query`
在当前 Subject 中按
`title in [...]`
查询真实记录;只有匹配到的记录会渲染为新窗口链接,当前记录自身会排除,查询失败时保留正文并显示轻量错误提示。
`detail.side_info`
配置为
`attachment`
时显示右侧附件列表,可追加上传并移除附件;
`related`
、
`reference`
、
`history`
、
`category`
等配置按历史应用的侧栏信息区展示。日期、选择、长文本字段保留现有 HeroUI 控件风格;附件值按 Record API 的
`{name,type,size,url}`
结构保存。
详情字段、正文模板和附件侧栏使用应用配置中的
`detail`
;
`article_view`
显示配置模板,其他详情模式使用结构化字段视图。详情页顶部提供返回列表、收藏、下载、编辑和删除入口,
删除记录前通过 HeroUI Modal 二次确认;确认执行软删除,失败时保留弹窗并显示错误。顶部标题、操作按钮和正文阅读工具栏在滚动正文时保持吸顶,收藏状态保存在本地浏览器状态中,下载使用当前记录正文、文档或附件中的第一个可访问文件。
`detail.content.type`
为
`attachment`
时,正文区域按
`streams-admin`
的 ArticleView 规则从当前记录文件中选择首个 PDF 并直接内嵌预览,同时提供新窗口打开入口;没有可预览 PDF 时回退到配置模板中的附件链接或空状态。普通正文区域按模板安全渲染 HTML,并提供查找关键词、上一处/下一处命中、字号调整、字体色、背景色和引用识别开关。字号由正文容器统一继承并覆盖正文 HTML 中可能残留的内联字号,确保调整后段落、表格和标题同步变化。开启引用识别后,先从正文文本提取
`《…》`
标题,再通过
`POST /record/query`
在当前 Subject 中按
`title in [...]`
查询真实记录;只有匹配到的记录会渲染为新窗口链接,当前记录自身会排除,查询失败时保留正文并显示轻量错误提示。
`detail.side_info`
配置为
`attachment`
时显示右侧附件列表,可追加上传并移除附件;移除附件前通过 HeroUI Modal 二次确认,确认后更新对应 metadata 字段,失败时保留弹窗并显示错误。
`related`
、
`reference`
、
`history`
、
`category`
等配置按历史应用的侧栏信息区展示。日期、选择、长文本字段保留现有 HeroUI 控件风格;附件值按 Record API 的
`{name,type,size,url}`
结构保存。
## 权限与异常
...
...
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