Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
A
audit_project_manager
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
audit_project_manager
Commits
f4ec5abf
Commit
f4ec5abf
authored
Aug 03, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: refine audit project management experience
parent
24e78137
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
246 additions
and
130 deletions
+246
-130
DynamicFormManager.jsx
src/components/DynamicFormManager.jsx
+68
-38
SubjectRecordWorkbench.jsx
src/components/SubjectRecordWorkbench.jsx
+9
-2
index.jsx
src/components/record-workbench/index.jsx
+45
-3
global.less
src/global.less
+12
-2
AuditProjectListPage.jsx
src/pages/project_manager_xy/AuditProjectListPage.jsx
+8
-41
Layout.jsx
src/pages/project_manager_xy/Layout.jsx
+54
-29
project-management.business-flow.md
...es/project_manager_xy/project-management.business-flow.md
+15
-11
record-api.md
src/services/record-api.md
+5
-4
subjectView.js
src/utils/subjectView.js
+30
-0
No files found.
src/components/DynamicFormManager.jsx
View file @
f4ec5abf
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
{
Badge
}
from
'@/catalyst-ui-kit/javascript/badge'
;
import
{
Checkbox
}
from
'@/catalyst-ui-kit/javascript/checkbox'
;
import
{
Dialog
,
...
...
@@ -28,6 +29,7 @@ import {
ArrowDownIcon
,
ArrowUpIcon
,
PencilSquareIcon
,
TableCellsIcon
,
TrashIcon
,
}
from
'@heroicons/react/24/outline'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
...
...
@@ -52,33 +54,27 @@ const FIELD_TYPES = [
const
FIELD_TYPE_LABELS
=
Object
.
fromEntries
(
FIELD_TYPES
);
function
FormDialog
({
open
,
onClose
,
onSaved
,
form
})
{
const
[
values
,
setValues
]
=
useState
({
title
:
''
,
name
:
''
,
description
:
''
}
);
const
[
title
,
setTitle
]
=
useState
(
''
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
useEffect
(()
=>
{
if
(
open
)
{
setValues
({
title
:
form
?.
title
||
''
,
name
:
form
?.
name
||
''
,
description
:
form
?.
description
||
''
,
});
setTitle
(
form
?.
title
||
''
);
setError
(
''
);
}
},
[
open
,
form
]);
const
save
=
async
()
=>
{
if
(
!
values
.
title
.
trim
()
||
!
values
.
nam
e
.
trim
())
{
setError
(
'表单标题
和表单别名
为必填项。'
);
if
(
!
titl
e
.
trim
())
{
setError
(
'表单标题为必填项。'
);
return
;
}
setSaving
(
true
);
setError
(
''
);
try
{
const
payload
=
{
title
:
values
.
title
.
trim
(),
name
:
values
.
name
.
trim
(),
description
:
values
.
description
.
trim
(),
title
:
title
.
trim
(),
};
if
(
form
)
{
await
updateDForm
(
form
.
name
,
payload
);
...
...
@@ -97,20 +93,12 @@ function FormDialog({ open, onClose, onSaved, form }) {
return
(
<
Dialog
open=
{
Boolean
(
open
)
}
onClose=
{
saving
?
()
=>
{}
:
onClose
}
size=
"lg"
>
<
DialogTitle
>
{
form
?
'编辑表单'
:
'新建表单'
}
</
DialogTitle
>
<
DialogDescription
>
维护表单名称
、别名与说明
。
</
DialogDescription
>
<
DialogDescription
>
维护表单名称
,内部标识由系统自动生成
。
</
DialogDescription
>
<
DialogBody
>
<
div
className=
"space-y-5"
>
<
Field
>
<
Label
>
表单标题
<
span
className=
"text-red-600"
>
*
</
span
></
Label
>
<
Input
value=
{
values
.
title
}
onChange=
{
(
event
)
=>
setValues
((
current
)
=>
({
...
current
,
title
:
event
.
target
.
value
}))
}
/>
</
Field
>
<
Field
>
<
Label
>
表单别名
<
span
className=
"text-red-600"
>
*
</
span
></
Label
>
<
Input
disabled=
{
Boolean
(
form
)
}
value=
{
values
.
name
}
onChange=
{
(
event
)
=>
setValues
((
current
)
=>
({
...
current
,
name
:
event
.
target
.
value
}))
}
/>
</
Field
>
<
Field
>
<
Label
>
表单描述
</
Label
>
<
Textarea
rows=
{
4
}
value=
{
values
.
description
}
onChange=
{
(
event
)
=>
setValues
((
current
)
=>
({
...
current
,
description
:
event
.
target
.
value
}))
}
/>
<
Input
value=
{
title
}
onChange=
{
(
event
)
=>
setTitle
(
event
.
target
.
value
)
}
/>
</
Field
>
{
error
?
<
div
role=
"alert"
className=
"rounded-md bg-red-50 px-4 py-3 text-sm text-red-700"
>
{
error
}
</
div
>
:
null
}
</
div
>
...
...
@@ -426,6 +414,8 @@ export default function DynamicFormManager() {
const
[
error
,
setError
]
=
useState
(
''
);
const
[
success
,
setSuccess
]
=
useState
(
''
);
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
pageSize
,
setPageSize
]
=
useState
(
20
);
const
[
formDialogOpen
,
setFormDialogOpen
]
=
useState
(
false
);
const
[
editingForm
,
setEditingForm
]
=
useState
(
null
);
const
[
fieldsForm
,
setFieldsForm
]
=
useState
(
null
);
...
...
@@ -449,47 +439,87 @@ export default function DynamicFormManager() {
reload
();
},
[
reload
]);
const
r
ecords
=
useMemo
(()
=>
forms
.
filter
((
form
)
=>
`
${
form
.
title
}
${
form
.
name
}
${
form
.
description
||
''
}
`
.
toLowerCase
().
includes
(
keyword
.
trim
().
toLowerCase
()))
const
filteredR
ecords
=
useMemo
(()
=>
forms
.
filter
((
form
)
=>
`
${
form
.
title
}
`
.
toLowerCase
().
includes
(
keyword
.
trim
().
toLowerCase
()))
.
map
((
form
)
=>
({
id
:
form
.
_id
||
form
.
name
,
name
:
form
.
name
,
title
:
form
.
title
,
description
:
form
.
description
||
'-'
,
fieldCount
:
form
.
fields
?.
length
||
0
,
raw
:
form
,
})),
[
forms
,
keyword
]);
const
records
=
useMemo
(
()
=>
filteredRecords
.
slice
(
page
*
pageSize
,
(
page
+
1
)
*
pageSize
),
[
filteredRecords
,
page
,
pageSize
],
);
useEffect
(()
=>
{
const
lastPage
=
Math
.
max
(
0
,
Math
.
ceil
(
filteredRecords
.
length
/
pageSize
)
-
1
);
if
(
page
>
lastPage
)
{
setPage
(
lastPage
);
}
},
[
filteredRecords
.
length
,
page
,
pageSize
]);
const
handleKeywordChange
=
(
value
)
=>
{
setKeyword
(
value
);
setPage
(
0
);
};
return
(
<>
<
RecordWorkbench
summaryItems=
{
[
{
label
:
'表单总数'
,
value
:
forms
.
length
,
hint
:
'动态表单配置'
,
tone
:
'default'
},
{
label
:
'筛选结果'
,
value
:
records
.
length
,
hint
:
'当前搜索结果'
,
tone
:
'emerald'
},
{
label
:
'字段总数'
,
value
:
forms
.
reduce
((
sum
,
form
)
=>
sum
+
(
form
.
fields
?.
length
||
0
),
0
),
hint
:
'全部表单字段'
,
tone
:
'amber'
},
]
}
title=
"项目表单"
description=
"使用当前项目工作台维护动态表单和字段配置。"
description=
{
`共 ${filteredRecords.length} 个表单,可维护表单信息和字段结构。`
}
records=
{
records
}
columns=
{
[
{
field
:
'title'
,
label
:
'表单名称'
,
width
:
260
,
fieldDef
:
{
type
:
'text'
,
label
:
'表单名称'
}
},
{
field
:
'name'
,
label
:
'表单别名'
,
width
:
220
,
fieldDef
:
{
type
:
'text'
,
label
:
'表单别名'
}
},
{
field
:
'description'
,
label
:
'表单描述'
,
width
:
360
,
fieldDef
:
{
type
:
'text'
,
label
:
'表单描述'
}
},
{
field
:
'fieldCount'
,
label
:
'字段数量'
,
width
:
120
,
fieldDef
:
{
type
:
'number'
,
label
:
'字段数量'
}
},
{
field
:
'title'
,
label
:
'表单名称'
,
width
:
420
,
fieldDef
:
{
type
:
'text'
,
label
:
'表单名称'
},
render
:
(
record
)
=>
(
<
div
className=
"truncate text-sm font-medium text-slate-900"
title=
{
record
.
title
}
>
{
record
.
title
||
'未命名表单'
}
</
div
>
),
},
{
field
:
'fieldCount'
,
label
:
'字段数量'
,
width
:
140
,
fieldDef
:
{
type
:
'number'
,
label
:
'字段数量'
},
render
:
(
record
)
=>
(
<
Badge
color=
"zinc"
title=
{
`${record.fieldCount} 个字段`
}
>
{
record
.
fieldCount
}
</
Badge
>
),
},
]
}
selectable=
{
false
}
filters=
{
{
keyword
}
}
filterSchema=
{
[{
key
:
'keyword'
,
label
:
'搜索表单'
,
type
:
'text'
,
placeholder
:
'表单名称
、别名或说明
'
}]
}
onFiltersChange=
{
(
_
,
value
)
=>
setKeyword
(
value
)
}
onClearFilters=
{
()
=>
setKeyword
(
''
)
}
filterSchema=
{
[{
key
:
'keyword'
,
label
:
'搜索表单'
,
type
:
'text'
,
placeholder
:
'表单名称'
}]
}
onFiltersChange=
{
(
_
,
value
)
=>
handleKeywordChange
(
value
)
}
onClearFilters=
{
()
=>
handleKeywordChange
(
''
)
}
toolbarActions=
{
[
{
key
:
'create-form'
,
label
:
'新建表单'
,
variant
:
'solid'
,
tone
:
'primary'
,
onClick
:
()
=>
{
setEditingForm
(
null
);
setFormDialogOpen
(
true
);
}
},
]
}
rowActions=
{
[
{
key
:
'fields'
,
label
:
'字段管理'
,
onClick
:
({
record
})
=>
setFieldsForm
(
record
.
raw
)
},
{
key
:
'fields'
,
label
:
'字段管理'
,
icon
:
TableCellsIcon
,
onClick
:
({
record
})
=>
setFieldsForm
(
record
.
raw
)
},
{
key
:
'edit'
,
label
:
'编辑表单'
,
icon
:
PencilSquareIcon
,
onClick
:
({
record
})
=>
{
setEditingForm
(
record
.
raw
);
setFormDialogOpen
(
true
);
}
},
{
key
:
'delete'
,
label
:
'删除表单'
,
icon
:
TrashIcon
,
tone
:
'danger'
,
onClick
:
({
record
})
=>
setDeleteForm
(
record
.
raw
)
},
]
}
pagination=
{
{
page
,
pageSize
,
totalRecords
:
filteredRecords
.
length
,
pageSizeOptions
:
[
10
,
20
,
50
,
100
],
onPageChange
:
setPage
,
onPageSizeChange
:
(
nextPageSize
)
=>
{
setPageSize
(
nextPageSize
);
setPage
(
0
);
},
}
}
states=
{
{
loading
,
error
,
success
,
emptyTitle
:
'暂无项目表单'
,
emptyDescription
:
'使用“新建表单”开始配置。'
}
}
/>
...
...
src/components/SubjectRecordWorkbench.jsx
View file @
f4ec5abf
...
...
@@ -48,12 +48,14 @@ export default function SubjectRecordWorkbench({
description
,
readonly
=
false
,
allowCreate
=
true
,
allowEdit
=
true
,
allowClone
=
true
,
allowDelete
=
true
,
query
,
buildSummaryItems
,
toolbarActions
=
[],
rowActions
=
[],
hiddenColumnFields
=
[],
detailDrawerProps
=
{},
emptyDescription
,
})
{
...
...
@@ -120,6 +122,10 @@ export default function SubjectRecordWorkbench({
[
records
,
filters
,
activeView
,
subject
],
);
const
filteredRecords
=
configuredView
.
filteredRecords
||
[];
const
visibleColumns
=
useMemo
(()
=>
{
const
hiddenFields
=
new
Set
(
hiddenColumnFields
);
return
(
configuredView
.
columns
||
[]).
filter
((
column
)
=>
!
hiddenFields
.
has
(
column
.
field
));
},
[
configuredView
.
columns
,
hiddenColumnFields
]);
const
totalPages
=
Math
.
max
(
1
,
Math
.
ceil
(
filteredRecords
.
length
/
pageSize
));
const
safePage
=
Math
.
min
(
page
,
totalPages
-
1
);
const
pageRecords
=
filteredRecords
.
slice
(
safePage
*
pageSize
,
(
safePage
+
1
)
*
pageSize
);
...
...
@@ -254,7 +260,7 @@ export default function SubjectRecordWorkbench({
variant
:
'plain'
,
onClick
:
({
record
,
openDetail
})
=>
openDetail
(
record
),
},
...(
!
readonly
...(
allowEdit
&&
!
readonly
?
[{
key
:
'edit'
,
label
:
'编辑'
,
...
...
@@ -313,7 +319,8 @@ export default function SubjectRecordWorkbench({
}
}
title=
{
title
}
description=
{
description
}
columns=
{
configuredView
.
columns
||
[]
}
columns=
{
visibleColumns
}
columnGroups=
{
activeView
?.
settings
?.
groups
||
[]
}
records=
{
pageRecords
}
selectedIds=
{
selectedIds
}
onSelectionChange=
{
setSelectedIds
}
...
...
src/components/record-workbench/index.jsx
View file @
f4ec5abf
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
{
Badge
}
from
'@/catalyst-ui-kit/javascript/badge'
;
import
{
Checkbox
}
from
'@/catalyst-ui-kit/javascript/checkbox'
;
import
{
Input
}
from
'@/catalyst-ui-kit/javascript/input'
;
import
{
Select
}
from
'@/catalyst-ui-kit/javascript/select'
;
...
...
@@ -10,9 +11,10 @@ import {
TableHeader
,
TableRow
,
}
from
'@/catalyst-ui-kit/javascript/table'
;
import
{
formatFieldValue
,
getFieldValue
}
from
'@/utils/subjectView'
;
import
{
formatFieldValue
,
getFieldValue
,
getFormValueSummaries
}
from
'@/utils/subjectView'
;
import
{
ChevronRightIcon
,
ClipboardDocumentListIcon
,
DocumentDuplicateIcon
,
EyeIcon
,
TrashIcon
,
...
...
@@ -268,8 +270,48 @@ function defaultGetRecordLabel(record) {
}
function
defaultRenderCell
(
record
,
column
)
{
const
sourceRecord
=
record
?.
raw
||
record
;
const
value
=
getFieldValue
(
sourceRecord
,
column
.
field
);
const
mappedValue
=
getFieldValue
(
record
,
column
.
field
);
const
value
=
mappedValue
===
undefined
?
getFieldValue
(
record
?.
raw
,
column
.
field
)
:
mappedValue
;
const
fieldType
=
column
.
fieldDef
?.
type
;
if
(
fieldType
===
'long_text'
||
fieldType
===
'rich_text'
)
{
const
text
=
formatFieldValue
(
value
,
column
.
fieldDef
);
return
text
?
(
<
div
className=
"line-clamp-3 whitespace-pre-wrap break-words text-sm leading-5 text-slate-700"
title=
{
text
}
>
{
text
}
</
div
>
)
:
<
span
className=
"text-sm text-zinc-400"
>
-
</
span
>;
}
if
(
fieldType
===
'form'
)
{
const
summaries
=
getFormValueSummaries
(
value
,
column
.
fieldDef
?.
label
||
'表单'
);
if
(
summaries
.
length
===
0
)
{
return
<
span
className=
"text-sm text-zinc-400"
>
-
</
span
>;
}
return
(
<
div
className=
"flex max-w-full flex-wrap gap-1.5"
>
{
summaries
.
map
((
summary
)
=>
(
<
Badge
key=
{
summary
.
key
}
color=
"blue"
className=
"max-w-full py-1"
title=
{
`${summary.label},${summary.rowCount} 行数据`
}
>
<
ClipboardDocumentListIcon
className=
"size-3.5 shrink-0"
aria
-
hidden=
"true"
/>
<
span
className=
"truncate"
>
{
summary
.
label
}
</
span
>
<
span
className=
"shrink-0 text-blue-600/70"
>
(
{
summary
.
rowCount
}
)
</
span
>
</
Badge
>
))
}
</
div
>
);
}
return
(
<
div
className=
"whitespace-normal break-words text-sm text-slate-700"
>
{
formatFieldValue
(
value
,
column
.
fieldDef
)
||
'-'
}
...
...
src/global.less
View file @
f4ec5abf
:root {
--pm-brand: #
2563eb
;
--pm-brand-strong: #
1d4ed8
;
--pm-brand: #
b42318
;
--pm-brand-strong: #
8f1d14
;
--pm-accent: #059669;
--pm-surface: #ffffff;
--pm-bg: #f8fafc;
...
...
@@ -17,6 +17,16 @@ body,
}
body {
--color-blue-50: #fef2f2;
--color-blue-100: #fee2e2;
--color-blue-200: #fecaca;
--color-blue-300: #fca5a5;
--color-blue-400: #f87171;
--color-blue-500: #dc2626;
--color-blue-600: #b42318;
--color-blue-700: #991b1b;
--color-blue-800: #7f1d1d;
--color-blue-900: #450a0a;
margin: 0;
background: var(--pm-bg);
color: var(--pm-text);
...
...
src/pages/project_manager_xy/AuditProjectListPage.jsx
View file @
f4ec5abf
import
SubjectRecordWorkbench
from
'@/components/SubjectRecordWorkbench'
;
import
{
ArrowDownTrayIcon
,
ArchiveBoxArrowDownIcon
,
}
from
'@heroicons/react/24/outline'
;
import
{
ArrowDownTrayIcon
}
from
'@heroicons/react/24/outline'
;
import
{
archiveAuditProjects
,
AUDIT_PROJECT_SUBJECT
,
...
...
@@ -15,6 +12,8 @@ import {
transitionAuditProject
,
}
from
'./AuditProjectService'
;
const
HIDDEN_LIST_COLUMNS
=
[
'title'
];
function
hasStatus
(
record
,
statuses
)
{
return
statuses
.
includes
(
record
?.
metadata
?.
project_status
);
}
...
...
@@ -89,25 +88,11 @@ export default function AuditProjectListPage() {
<
SubjectRecordWorkbench
subjectName=
{
AUDIT_PROJECT_SUBJECT
}
title=
"审计项目"
description=
"按线上视图配置展示审计项目,支持详情维护、状态流转、导出和一键归档。"
buildSummaryItems=
{
({
records
,
filteredRecords
,
selectedIds
})
=>
{
const
completed
=
records
.
filter
((
record
)
=>
[
'整改'
,
'归档'
].
includes
(
record
.
raw
?.
metadata
?.
project_status
),
).
length
;
return
[
{
label
:
'项目总数'
,
value
:
records
.
length
,
hint
:
'全部审计项目'
,
tone
:
'default'
},
{
label
:
'已完成'
,
value
:
completed
,
hint
:
'整改或归档项目'
,
tone
:
'emerald'
},
{
label
:
'未完成'
,
value
:
records
.
length
-
completed
,
hint
:
'尚未进入整改或归档'
,
tone
:
'amber'
},
{
label
:
'完成率'
,
value
:
records
.
length
?
`${Math.round((completed / records.length) * 100)}%`
:
'0%'
,
hint
:
'已完成项目占比'
,
tone
:
'rose'
,
},
{
label
:
'筛选结果'
,
value
:
filteredRecords
.
length
,
hint
:
'当前视图和筛选'
,
tone
:
'default'
},
{
label
:
'当前已选'
,
value
:
selectedIds
.
length
,
hint
:
'可执行批量操作'
,
tone
:
'default'
},
];
}
}
description=
"按线上视图配置展示审计项目,支持详情维护、状态流转、导出和批量归档。"
allowEdit=
{
false
}
allowClone=
{
false
}
hiddenColumnFields=
{
HIDDEN_LIST_COLUMNS
}
buildSummaryItems=
{
()
=>
[]
}
toolbarActions=
{
[
{
key
:
'archive-selected'
,
...
...
@@ -126,24 +111,6 @@ export default function AuditProjectListPage() {
},
},
]
}
rowActions=
{
[
{
key
:
'archive'
,
label
:
'一键归档'
,
icon
:
ArchiveBoxArrowDownIcon
,
variant
:
'plain'
,
onClick
:
async
({
record
,
reload
,
setSuccess
,
setError
})
=>
{
setError
(
''
);
try
{
await
archiveAuditProjects
([
record
]);
setSuccess
(
'项目快照归档成功。'
);
await
reload
({
keepFeedback
:
true
});
}
catch
(
error
)
{
setError
(
error
?.
message
||
'归档失败,请稍后重试。'
);
}
},
},
]
}
detailDrawerProps=
{
{
width
:
'min(1180px, 96vw)'
,
isViewDisabled
:
isFutureAuditProjectView
,
...
...
src/pages/project_manager_xy/Layout.jsx
View file @
f4ec5abf
import
{
Avatar
}
from
'@/catalyst-ui-kit/javascript/avatar'
;
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
{
Navbar
,
NavbarSection
}
from
'@/catalyst-ui-kit/javascript/navbar'
;
import
*
as
Headless
from
'@headlessui/react'
;
import
{
Sidebar
,
SidebarBody
,
SidebarFooter
,
SidebarHeader
,
SidebarHeading
,
SidebarItem
,
SidebarLabel
,
SidebarSection
,
...
...
@@ -17,9 +17,9 @@ import xyLogoSmall from '@/assets/xy-logo_small.png';
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
ArchiveBoxIcon
,
ArrowRightStartOnRectangleIcon
,
ChartBarSquareIcon
,
ChevronDownIcon
,
ClipboardDocumentListIcon
,
DocumentDuplicateIcon
,
Squares2X2Icon
,
...
...
@@ -27,39 +27,76 @@ import {
import
{
useContext
}
from
'react'
;
import
{
Outlet
,
useLocation
}
from
'umi'
;
const
DASHBOARD_ITEM
=
{
label
:
'驾驶舱'
,
path
:
'/audit_project_table/dashboard'
,
icon
:
ChartBarSquareIcon
,
};
const
MENU_GROUPS
=
[
{
title
:
'审计项目管理'
,
title
:
'开展审计工作'
,
icon
:
ClipboardDocumentListIcon
,
items
:
[
{
label
:
'驾驶舱'
,
path
:
'/audit_project_table/dashboard'
,
icon
:
ChartBarSquareIcon
,
},
{
label
:
'审计项目管理'
,
path
:
'/audit_project'
,
icon
:
ClipboardDocumentListIcon
,
},
{
label
:
'往期项目归档'
,
path
:
'/audit_project_archive'
,
icon
:
ArchiveBoxIcon
,
},
],
},
{
title
:
'表单配置及维护'
,
icon
:
DocumentDuplicateIcon
,
items
:
[
{
label
:
'项目表单'
,
path
:
'/project_form'
,
icon
:
DocumentDuplicateIcon
,
},
],
},
];
function
NavigationItem
({
item
,
currentPath
})
{
const
Icon
=
item
.
icon
;
return
(
<
SidebarItem
href=
{
item
.
path
}
current=
{
currentPath
===
item
.
path
}
>
{
Icon
?
<
Icon
data
-
slot=
"icon"
className=
"!fill-none stroke-current"
/>
:
null
}
<
SidebarLabel
>
{
item
.
label
}
</
SidebarLabel
>
</
SidebarItem
>
);
}
function
NavigationGroup
({
group
,
currentPath
})
{
const
GroupIcon
=
group
.
icon
;
return
(
<
Headless
.
Disclosure
defaultOpen
>
{
({
open
})
=>
(
<>
<
Headless
.
DisclosureButton
className=
"flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left text-sm font-medium text-white transition hover:bg-white/5 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30"
>
<
GroupIcon
className=
"size-5 shrink-0 text-zinc-400"
aria
-
hidden=
"true"
/>
<
span
className=
"min-w-0 flex-1 truncate text-white"
>
{
group
.
title
}
</
span
>
<
ChevronDownIcon
className=
{
open
?
'size-4 shrink-0 rotate-180 text-zinc-500 transition-transform'
:
'size-4 shrink-0 text-zinc-500 transition-transform'
}
aria
-
hidden=
"true"
/>
</
Headless
.
DisclosureButton
>
<
Headless
.
DisclosurePanel
className=
"ml-5 flex flex-col gap-0.5 border-l border-white/10 pl-3"
>
{
group
.
items
.
map
((
item
)
=>
(
<
NavigationItem
key=
{
item
.
path
}
item=
{
item
}
currentPath=
{
currentPath
}
/>
))
}
</
Headless
.
DisclosurePanel
>
</>
)
}
</
Headless
.
Disclosure
>
);
}
function
Navigation
({
location
,
userInfo
,
siteInfo
})
{
const
appTitle
=
siteInfo
?.
title
||
siteInfo
?.
name
||
'审计项目管理'
;
const
currentPath
=
location
.
pathname
.
replace
(
/^
\/
audit_project_manager
(?=\/
|$
)
/
,
''
)
||
'/'
;
...
...
@@ -79,24 +116,12 @@ function Navigation({ location, userInfo, siteInfo }) {
</
SidebarHeader
>
<
SidebarBody
>
{
MENU_GROUPS
.
map
((
group
)
=>
(
<
SidebarSection
key=
{
group
.
title
}
>
<
SidebarHeading
className=
"text-zinc-500"
>
{
group
.
title
}
</
SidebarHeading
>
{
group
.
items
.
map
((
item
)
=>
{
const
Icon
=
item
.
icon
;
return
(
<
SidebarItem
key=
{
item
.
path
}
href=
{
item
.
path
}
current=
{
currentPath
===
item
.
path
}
>
<
Icon
data
-
slot=
"icon"
/>
<
SidebarLabel
>
{
item
.
label
}
</
SidebarLabel
>
</
SidebarItem
>
);
})
}
</
SidebarSection
>
))
}
<
SidebarSection
>
<
NavigationItem
item=
{
DASHBOARD_ITEM
}
currentPath=
{
currentPath
}
/>
{
MENU_GROUPS
.
map
((
group
)
=>
(
<
NavigationGroup
key=
{
group
.
title
}
group=
{
group
}
currentPath=
{
currentPath
}
/>
))
}
</
SidebarSection
>
<
SidebarSpacer
/>
</
SidebarBody
>
...
...
src/pages/project_manager_xy/project-management.business-flow.md
View file @
f4ec5abf
...
...
@@ -7,7 +7,7 @@
| 页面 | 路由 | 用途 |
| --- | --- | --- |
| 驾驶舱 |
`/audit_project_table/dashboard`
| 查看项目完成率、阶段分布、逾期和临期项目 |
| 审计项目管理 |
`/audit_project`
| 查询、维护、
复制、删除、归档
和推进项目 |
| 审计项目管理 |
`/audit_project`
| 查询、维护、
删除
和推进项目 |
| 往期项目归档 |
`/audit_project_archive`
| 只读查看审计项目历史快照 |
| 项目表单 |
`/project_form`
| 维护动态表单及字段 |
| 审计项目填写表 |
`/audit_project_form`
| 兼容外部填写和复核链接 |
...
...
@@ -15,14 +15,18 @@
配置中的
`audit_indicator`
、
`risk_indicator`
、
`audit_process`
、
`audit_report`
没有菜单入口,属于复制配置残留,不注册为本应用路由。
侧栏将“驾驶舱”作为独立入口;“审计项目管理”和“往期项目归档”归入可展开的“开展审计工作”,“项目表单”归入可展开的“表单配置及维护”。两个分组默认展开,并支持手动收起。模块主题色与
`problem_rectification`
保持一致,使用品牌红
`#b42318`
统一导航选中态、主按钮、复选框、当前视图和焦点反馈。
## 2. 项目列表与详情
1.
页面通过
`loadSubject`
、
`getSubjectView`
和
`POST /record/query`
读取字段、视图和记录。
2.
表格列、视图限制、排序
和选择字段选项来自服务端配置
。
2.
表格列、视图限制、排序
、选择字段选项和字段分组来自服务端配置;字段分组读取当前 Grid View 的
`settings.groups`
,按配置展示分组表头并支持展开或收起。审计项目管理列表不展示通用
`title`
列,项目名称及其他业务字段仍按 View 配置展示
。
3.
新建和编辑时业务字段统一写入
`metadata`
;引用和用户字段只提交 ID,日期提交 ISO 8601。
4.
编辑模式下主键字段禁用;必填字段按 Subject 配置校验。
5.
复制调用
`POST /record/clone`
;删除调用
`POST /record/batch/remove`
,默认软删除
。
5.
删除调用
`POST /record/batch/remove`
,默认软删除;列表行内不提供编辑、复制或归档按钮,字段维护统一从详情侧栏进入
。
6.
详情抽屉按可访问的 detail 视图显示字段,并支持字段内联维护。
7.
列表页不展示前端写死的项目总数、完成率等摘要卡片,仅展示由服务端 Subject/View 配置驱动的视图、筛选和表格内容;项目统计统一在驾驶舱查看。
8.
列表中的
`long_text`
和
`rich_text`
字段最多展示三行,超出内容省略;完整内容通过悬浮提示或项目详情查看,避免单条长文本拉高整行表格。
## 3. 状态流转
...
...
@@ -49,14 +53,12 @@ Word 导出分别读取 `audit_plain_template`、`audit_verification_template`
`audit_report_template`
文档。需求材料和问题清单导出为 Excel;核实书按数据行生成
多个文档并打包为 ZIP。缺少模板或可导出数据时展示错误,不生成空文件。
## 5. 一键归档
1.
用户在项目行或批量选择后执行“一键归档”。
2.
页面把项目
`metadata`
复制为
`audit_project_history`
记录;只按 Subject 定义把
`user/reference`
字段转换为 ID,保留表单和文件对象结构。
3.
页面通过
`POST /record/import`
批量写入历史快照;接口未返回任何历史记录 ID 时展示错误。
4.
`/audit_project_archive`
直接查询并展示
`audit_project_history`
,不再读写已废弃的
`audit_project_archive`
Subject。
## 5. 往期项目归档
归档操作生成快照,不删除原项目,也不自动修改原项目状态。
-
`/audit_project_archive`
直接查询并展示已有的
`audit_project_history`
历史快照,不再读写已废弃的
`audit_project_archive`
Subject。
-
审计项目列表右上角保留批量“一键归档”:用户选择一条或多条项目后,页面把项目
`metadata`
复制为
`audit_project_history`
记录,并通过
`POST /record/import`
写入历史快照。
-
批量归档未选择记录时按钮禁用;接口未返回任何历史记录 ID 时展示错误。归档只生成快照,不删除或修改原审计项目。
-
列表行内不提供复制或单条归档入口,已有历史快照继续保留并只读展示。
## 6. 驾驶舱口径
...
...
@@ -85,13 +87,15 @@ Word 导出分别读取 `audit_plain_template`、`audit_verification_template`
项目表单使用独立
`/form`
API。表单列表和字段列表复用
`RecordWorkbench`
,不再使用
旧
`form_grid`
多维表格。
-
表单支持新建、编辑、删除和进入字段管理。
-
表单支持新建、编辑、删除和进入字段管理;新建和编辑只维护表单标题,不提供描述或别名字段。新建时内部
`name`
由服务端自动生成,编辑时保留已有
`name`
。列表只展示表单标题,字段数量以标签展示,字段管理使用表格图标入口。
-
表单列表默认每页展示 20 条,搜索表单时自动回到第一页,并支持切换每页 10、20、50 或 100 条。
-
字段支持新建、编辑、删除、上移、下移、Excel 导入和导出。
-
字段类型、必填、多值、主键和
`settings`
由表单字段接口保存。
-
`settings`
必须是合法 JSON;选择字段选项存放在
`settings.options`
。
-
审计项目中的
`form`
字段在项目详情侧栏内默认完整展开:左侧按
`settings.group`
展示默认分组和自定义分组,右侧展示当前分组下的全部表单及数据表格。
-
表单字段支持在侧栏内添加/删除分组、添加/删除表单、切换表单模板和调整所属分组;删除自定义分组时,其中已配置的表单移入默认分组。
-
表单数据行使用通用表格展示,通过 Catalyst 弹窗新增或编辑;侧栏中的修改先作为草稿保留,点击“保存表单”后统一写回
`{ form, data, settings }`
,不向业务用户暴露 JSON。
-
业务列表中的
`form`
字段只显示“表单名称(数据行数)”摘要标签,不直接展示
`{ form, data, settings }`
JSON;多值表单按实例分别展示。
-
删除为不可恢复操作,执行前必须二次确认;接口失败时保留当前页面并展示错误。
## 9. 权限与限制
...
...
src/services/record-api.md
View file @
f4ec5abf
...
...
@@ -280,16 +280,17 @@ Query:`calculated=true|false`
-
PUT
`/form/:name`
:更新表单
-
DELETE
`/form/:name`
:删除表单
表单
创建和更新请求体:
项目表单页面
创建和更新请求体:
```
json
{
"title"
:
"审计项目表单"
,
"name"
:
"audit_project_form"
,
"description"
:
"表单说明"
"title"
:
"审计项目表单"
}
```
创建时只提交
`title`
,
`name`
由服务端自动生成;编辑时使用已有
`name`
作为 URL 路径参数,只更新
`title`
,页面不提供
`name`
编辑入口。
### 5.2 表单字段
-
GET
`/form/:name/field`
:查询全部字段
...
...
src/utils/subjectView.js
View file @
f4ec5abf
...
...
@@ -418,6 +418,30 @@ export function formatDateText(value) {
return
/^
\d{4}
-
\d{2}
-
\d{2}
/
.
test
(
text
)
?
text
.
slice
(
0
,
10
)
:
text
;
}
export
function
getFormValueSummaries
(
value
,
fallbackLabel
=
'表单'
)
{
return
toArray
(
value
)
.
filter
(
Boolean
)
.
map
((
instance
,
index
)
=>
{
const
normalizedInstance
=
typeof
instance
===
'object'
&&
!
Array
.
isArray
(
instance
)
?
instance
:
{
form
:
instance
,
data
:
[]
};
const
form
=
normalizedInstance
.
form
;
const
label
=
form
&&
typeof
form
===
'object'
?
form
.
title
||
form
.
label
||
form
.
name
||
fallbackLabel
:
fallbackLabel
;
const
formKey
=
form
&&
typeof
form
===
'object'
?
form
.
_id
||
form
.
name
||
label
:
label
;
return
{
key
:
`
${
formKey
}
-
${
index
}
`
,
label
,
rowCount
:
Array
.
isArray
(
normalizedInstance
.
data
)
?
normalizedInstance
.
data
.
length
:
0
,
};
});
}
export
function
formatFieldValue
(
value
,
fieldDef
)
{
if
(
value
===
null
||
value
===
undefined
||
value
===
''
)
{
return
''
;
...
...
@@ -464,6 +488,12 @@ export function formatFieldValue(value, fieldDef) {
.
join
(
'、'
);
}
if
(
fieldDef
?.
type
===
'form'
)
{
return
getFormValueSummaries
(
value
,
fieldDef
?.
label
||
'表单'
)
.
map
((
item
)
=>
`
${
item
.
label
}
(
${
item
.
rowCount
}
)`
)
.
join
(
'、'
);
}
if
(
typeof
value
===
'object'
)
{
return
JSON
.
stringify
(
value
);
}
...
...
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