Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
P
problem_rectification
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
problem_rectification
Commits
9b9085cc
Commit
9b9085cc
authored
Jul 21, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update: ui
parent
2d3757b2
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
2455 additions
and
380 deletions
+2455
-380
.umirc.ts
.umirc.ts
+9
-2
table.jsx
src/catalyst-ui-kit/javascript/table.jsx
+4
-1
index.jsx
src/components/record-workbench/index.jsx
+327
-78
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+13
-1
index.jsx
src/pages/problem_rectification/flow-table/index.jsx
+848
-0
index.jsx
src/pages/problem_rectification/list/index.jsx
+575
-161
problem-rectification.business-flow.md
...blem_rectification/problem-rectification.business-flow.md
+77
-54
problemRectificationFormService.js
.../problem_rectification/problemRectificationFormService.js
+5
-6
problemRectificationListService.js
.../problem_rectification/problemRectificationListService.js
+502
-25
subjectsDefinition.md
src/services/subjectsDefinition.md
+95
-52
No files found.
.umirc.ts
View file @
9b9085cc
...
@@ -12,13 +12,15 @@ const ROUTE_PATHS = {
...
@@ -12,13 +12,15 @@ const ROUTE_PATHS = {
problemRectification
:
'/problem_rectification'
,
problemRectification
:
'/problem_rectification'
,
issueRectificationArchive
:
'/issue_rectification_archive'
,
issueRectificationArchive
:
'/issue_rectification_archive'
,
problemRectificationForm
:
'/problem_rectification_form'
,
problemRectificationForm
:
'/problem_rectification_form'
,
problemRectificationFlowTable
:
'/problem_rectification_flow_table'
,
}
as
const
;
}
as
const
;
const
APP_BASE_PATH
=
'/problem_rectification_new/'
;
export
default
defineConfig
({
export
default
defineConfig
({
layout
:
false
,
layout
:
false
,
fastRefresh
:
true
,
fastRefresh
:
true
,
base
:
'/problem_rectification_new/'
,
base
:
APP_BASE_PATH
,
publicPath
:
'/problem_rectification_new/'
,
publicPath
:
APP_BASE_PATH
,
alias
:
{
alias
:
{
react
:
resolvePackageRoot
(
'react'
),
react
:
resolvePackageRoot
(
'react'
),
'react-dom'
:
resolvePackageRoot
(
'react-dom'
),
'react-dom'
:
resolvePackageRoot
(
'react-dom'
),
...
@@ -58,6 +60,10 @@ export default defineConfig({
...
@@ -58,6 +60,10 @@ export default defineConfig({
path
:
ROUTE_PATHS
.
problemRectificationForm
,
path
:
ROUTE_PATHS
.
problemRectificationForm
,
component
:
'./problem_rectification/form/index'
,
component
:
'./problem_rectification/form/index'
,
},
},
{
path
:
ROUTE_PATHS
.
problemRectificationFlowTable
,
component
:
'./problem_rectification/flow-table/index'
,
},
],
],
},
},
],
],
...
@@ -88,5 +94,6 @@ export default defineConfig({
...
@@ -88,5 +94,6 @@ export default defineConfig({
define
:
{
define
:
{
STREAMS_API_URL
:
'/api'
,
STREAMS_API_URL
:
'/api'
,
STREAMS_PARSER_URL
:
'/parser'
,
STREAMS_PARSER_URL
:
'/parser'
,
PROBLEM_RECTIFICATION_APP_BASE_PATH
:
APP_BASE_PATH
,
},
},
});
});
src/catalyst-ui-kit/javascript/table.jsx
View file @
9b9085cc
...
@@ -16,6 +16,7 @@ export function Table({
...
@@ -16,6 +16,7 @@ export function Table({
dense
=
false
,
dense
=
false
,
grid
=
false
,
grid
=
false
,
striped
=
false
,
striped
=
false
,
scrollable
=
true
,
className
,
className
,
children
,
children
,
...
props
...
props
...
@@ -27,7 +28,9 @@ export function Table({
...
@@ -27,7 +28,9 @@ export function Table({
{
...
props
}
{
...
props
}
className=
{
clsx
(
className=
{
clsx
(
className
,
className
,
'-mx-(--gutter) overflow-x-auto whitespace-nowrap'
,
'-mx-(--gutter)'
,
scrollable
&&
'overflow-x-auto'
,
'whitespace-nowrap'
,
)
}
)
}
>
>
<
div
<
div
...
...
src/components/record-workbench/index.jsx
View file @
9b9085cc
...
@@ -12,16 +12,18 @@ import {
...
@@ -12,16 +12,18 @@ import {
}
from
'@/catalyst-ui-kit/javascript/table'
;
}
from
'@/catalyst-ui-kit/javascript/table'
;
import
{
formatFieldValue
,
getFieldValue
}
from
'@/utils/subjectView'
;
import
{
formatFieldValue
,
getFieldValue
}
from
'@/utils/subjectView'
;
import
{
import
{
ChevronRightIcon
,
DocumentDuplicateIcon
,
DocumentDuplicateIcon
,
EyeIcon
,
EyeIcon
,
TrashIcon
,
TrashIcon
,
}
from
'@heroicons/react/24/outline'
;
}
from
'@heroicons/react/24/outline'
;
import
clsx
from
'clsx'
;
import
clsx
from
'clsx'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
Fragment
,
useEffect
,
useLayoutEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
const
SELECTION_COLUMN_WIDTH
=
56
;
const
SELECTION_COLUMN_WIDTH
=
56
;
const
DEFAULT_ACTION_COLUMN_WIDTH
=
168
;
const
DEFAULT_ACTION_COLUMN_WIDTH
=
168
;
const
EMPTY_ARRAY
=
[];
const
EMPTY_ARRAY
=
[];
const
DEFAULT_PAGE_SIZE_OPTIONS
=
[
10
,
20
,
50
,
100
];
const
DEFAULT_ROW_ACTION_ICONS
=
{
const
DEFAULT_ROW_ACTION_ICONS
=
{
clone
:
DocumentDuplicateIcon
,
clone
:
DocumentDuplicateIcon
,
delete
:
TrashIcon
,
delete
:
TrashIcon
,
...
@@ -275,6 +277,33 @@ function defaultRenderCell(record, column) {
...
@@ -275,6 +277,33 @@ function defaultRenderCell(record, column) {
);
);
}
}
function
buildPaginationPageItems
(
currentPage
,
totalPages
)
{
if
(
totalPages
<=
7
)
{
return
Array
.
from
({
length
:
totalPages
},
(
_
,
index
)
=>
index
);
}
const
pageSet
=
new
Set
([
0
,
totalPages
-
1
,
currentPage
-
1
,
currentPage
,
currentPage
+
1
,
]);
const
pages
=
Array
.
from
(
pageSet
)
.
filter
((
page
)
=>
page
>=
0
&&
page
<
totalPages
)
.
sort
((
left
,
right
)
=>
left
-
right
);
return
pages
.
reduce
((
items
,
page
,
index
)
=>
{
const
previousPage
=
pages
[
index
-
1
];
if
(
index
>
0
&&
page
-
previousPage
>
1
)
{
items
.
push
(
`gap-
${
previousPage
}
-
${
page
}
`
);
}
items
.
push
(
page
);
return
items
;
},
[]);
}
function
ColumnGroupButton
({
group
,
collapsed
,
onToggle
})
{
function
ColumnGroupButton
({
group
,
collapsed
,
onToggle
})
{
return
(
return
(
<
button
<
button
...
@@ -327,6 +356,93 @@ function RowActionButton({ action, disabled, onClick }) {
...
@@ -327,6 +356,93 @@ function RowActionButton({ action, disabled, onClick }) {
);
);
}
}
function
WorkbenchPagination
({
pagination
})
{
if
(
!
pagination
)
{
return
null
;
}
const
pageSizeOptions
=
pagination
.
pageSizeOptions
||
DEFAULT_PAGE_SIZE_OPTIONS
;
const
pageSize
=
Number
(
pagination
.
pageSize
||
pageSizeOptions
[
0
]
||
20
);
const
totalRecords
=
Math
.
max
(
0
,
Number
(
pagination
.
totalRecords
||
0
));
const
totalPages
=
Math
.
max
(
1
,
Number
(
pagination
.
totalPages
||
Math
.
ceil
(
totalRecords
/
pageSize
)
||
1
),
);
const
currentPage
=
Math
.
min
(
Math
.
max
(
0
,
Number
(
pagination
.
page
||
0
)),
totalPages
-
1
,
);
const
startRecord
=
totalRecords
===
0
?
0
:
currentPage
*
pageSize
+
1
;
const
endRecord
=
Math
.
min
(
totalRecords
,
(
currentPage
+
1
)
*
pageSize
);
const
pageItems
=
buildPaginationPageItems
(
currentPage
,
totalPages
);
return
(
<
div
className=
"flex flex-col gap-3 border-t border-zinc-100 bg-zinc-50/70 px-4 py-3 sm:flex-row sm:items-center sm:justify-between"
>
<
div
className=
"flex flex-wrap items-center gap-3 text-sm text-slate-500"
>
<
span
>
共
{
totalRecords
}
条,显示
{
startRecord
}
-
{
endRecord
}
</
span
>
<
label
className=
"flex items-center gap-2"
>
<
span
>
每页
</
span
>
<
Select
className=
"w-24"
value=
{
String
(
pageSize
)
}
onChange=
{
(
event
)
=>
pagination
.
onPageSizeChange
?.(
Number
(
event
.
target
.
value
))
}
>
{
pageSizeOptions
.
map
((
option
)
=>
(
<
option
key=
{
option
}
value=
{
option
}
>
{
option
}
</
option
>
))
}
</
Select
>
</
label
>
</
div
>
<
div
className=
"flex flex-wrap items-center gap-1.5"
>
<
Button
plain
disabled=
{
currentPage
<=
0
}
onClick=
{
()
=>
pagination
.
onPageChange
?.(
currentPage
-
1
)
}
>
上一页
</
Button
>
{
pageItems
.
map
((
item
)
=>
typeof
item
===
'number'
?
(
<
Button
key=
{
item
}
plain
aria
-
current=
{
item
===
currentPage
?
'page'
:
undefined
}
className=
{
clsx
(
'min-w-9'
,
item
===
currentPage
&&
'bg-zinc-950/5 text-slate-950'
,
)
}
onClick=
{
()
=>
pagination
.
onPageChange
?.(
item
)
}
>
{
item
+
1
}
</
Button
>
)
:
(
<
span
key=
{
item
}
className=
"flex h-9 min-w-9 items-center justify-center text-sm font-semibold text-slate-400"
>
...
</
span
>
),
)
}
<
Button
plain
disabled=
{
currentPage
>=
totalPages
-
1
}
onClick=
{
()
=>
pagination
.
onPageChange
?.(
currentPage
+
1
)
}
>
下一页
</
Button
>
</
div
>
</
div
>
);
}
export
default
function
RecordWorkbench
({
export
default
function
RecordWorkbench
({
summaryItems
=
[],
summaryItems
=
[],
views
=
[],
views
=
[],
...
@@ -349,17 +465,26 @@ export default function RecordWorkbench({
...
@@ -349,17 +465,26 @@ export default function RecordWorkbench({
detailPanel
=
null
,
detailPanel
=
null
,
states
=
{},
states
=
{},
renderCell
,
renderCell
,
renderExpandedRow
,
isRowExpandable
,
getRecordId
=
defaultGetRecordId
,
getRecordId
=
defaultGetRecordId
,
getRecordLabel
=
defaultGetRecordLabel
,
getRecordLabel
=
defaultGetRecordLabel
,
selectable
=
true
,
selectable
=
true
,
actionColumnWidth
=
DEFAULT_ACTION_COLUMN_WIDTH
,
actionColumnWidth
=
DEFAULT_ACTION_COLUMN_WIDTH
,
pagination
=
null
,
stickyTable
=
false
,
showExpandColumn
=
true
,
})
{
})
{
const
tableScrollRef
=
useRef
(
null
);
const
pendingTableScrollRef
=
useRef
(
null
);
const
[
drawerRecordName
,
setDrawerRecordName
]
=
useState
(
''
);
const
[
drawerRecordName
,
setDrawerRecordName
]
=
useState
(
''
);
const
[
drawerDirty
,
setDrawerDirty
]
=
useState
(
false
);
const
[
drawerDirty
,
setDrawerDirty
]
=
useState
(
false
);
const
[
collapsedColumnGroups
,
setCollapsedColumnGroups
]
=
useState
({});
const
[
collapsedColumnGroups
,
setCollapsedColumnGroups
]
=
useState
({});
const
[
expandedRowIds
,
setExpandedRowIds
]
=
useState
({});
const
loading
=
Boolean
(
states
?.
loading
);
const
loading
=
Boolean
(
states
?.
loading
);
const
error
=
states
?.
error
||
''
;
const
error
=
states
?.
error
||
''
;
const
totalRecords
=
records
.
length
;
const
pageRecordCount
=
records
.
length
;
const
totalRecords
=
pagination
?.
totalRecords
??
records
.
length
;
const
columnGroupMeta
=
useMemo
(
const
columnGroupMeta
=
useMemo
(
()
=>
buildColumnGroupMeta
(
columnGroups
,
columns
),
()
=>
buildColumnGroupMeta
(
columnGroups
,
columns
),
[
columnGroups
,
columns
],
[
columnGroups
,
columns
],
...
@@ -385,6 +510,14 @@ export default function RecordWorkbench({
...
@@ -385,6 +510,14 @@ export default function RecordWorkbench({
()
=>
records
.
filter
((
record
)
=>
selectedIds
.
includes
(
getRecordId
(
record
))),
()
=>
records
.
filter
((
record
)
=>
selectedIds
.
includes
(
getRecordId
(
record
))),
[
records
,
selectedIds
,
getRecordId
],
[
records
,
selectedIds
,
getRecordId
],
);
);
const
currentPageRecordIds
=
useMemo
(
()
=>
records
.
map
((
record
)
=>
getRecordId
(
record
)).
filter
(
Boolean
),
[
records
,
getRecordId
],
);
const
selectedCurrentPageIds
=
useMemo
(
()
=>
currentPageRecordIds
.
filter
((
id
)
=>
selectedIds
.
includes
(
id
)),
[
currentPageRecordIds
,
selectedIds
],
);
const
stickyColumns
=
useMemo
(
const
stickyColumns
=
useMemo
(
()
=>
buildStickyColumnMeta
(
orderedVisibleColumns
,
selectable
,
rowActions
.
length
>
0
,
actionColumnWidth
),
()
=>
buildStickyColumnMeta
(
orderedVisibleColumns
,
selectable
,
rowActions
.
length
>
0
,
actionColumnWidth
),
...
@@ -401,6 +534,13 @@ export default function RecordWorkbench({
...
@@ -401,6 +534,13 @@ export default function RecordWorkbench({
);
);
const
hasColumnGroups
=
columnGroupMeta
.
groups
.
length
>
0
;
const
hasColumnGroups
=
columnGroupMeta
.
groups
.
length
>
0
;
const
showColumnGroupHeader
=
columnGroupSegments
.
some
((
segment
)
=>
segment
.
group
);
const
showColumnGroupHeader
=
columnGroupSegments
.
some
((
segment
)
=>
segment
.
group
);
const
hasExpandableRows
=
typeof
renderExpandedRow
===
'function'
;
const
hasExpandColumn
=
hasExpandableRows
&&
showExpandColumn
;
const
tableColSpan
=
stickyColumns
.
orderedColumns
.
length
+
(
selectable
?
1
:
0
)
+
(
hasExpandColumn
?
1
:
0
)
+
(
rowActions
.
length
>
0
?
1
:
0
);
useEffect
(()
=>
{
useEffect
(()
=>
{
const
validGroupIds
=
new
Set
(
columnGroupMeta
.
groups
.
map
((
group
)
=>
group
.
id
));
const
validGroupIds
=
new
Set
(
columnGroupMeta
.
groups
.
map
((
group
)
=>
group
.
id
));
...
@@ -417,13 +557,46 @@ export default function RecordWorkbench({
...
@@ -417,13 +557,46 @@ export default function RecordWorkbench({
});
});
},
[
columnGroupMeta
.
groups
]);
},
[
columnGroupMeta
.
groups
]);
const
captureTableScroll
=
()
=>
{
const
scrollNode
=
tableScrollRef
.
current
;
if
(
!
scrollNode
)
{
return
;
}
pendingTableScrollRef
.
current
=
{
top
:
scrollNode
.
scrollTop
,
left
:
scrollNode
.
scrollLeft
,
};
};
useLayoutEffect
(()
=>
{
const
scrollPosition
=
pendingTableScrollRef
.
current
;
const
scrollNode
=
tableScrollRef
.
current
;
if
(
!
scrollPosition
||
!
scrollNode
)
{
return
;
}
scrollNode
.
scrollTop
=
scrollPosition
.
top
;
scrollNode
.
scrollLeft
=
scrollPosition
.
left
;
pendingTableScrollRef
.
current
=
null
;
},
[
collapsedColumnGroups
]);
const
toggleColumnGroup
=
(
groupId
)
=>
{
const
toggleColumnGroup
=
(
groupId
)
=>
{
captureTableScroll
();
setCollapsedColumnGroups
((
current
)
=>
({
setCollapsedColumnGroups
((
current
)
=>
({
...
current
,
...
current
,
[
groupId
]:
!
current
[
groupId
],
[
groupId
]:
!
current
[
groupId
],
}));
}));
};
};
const
toggleExpandedRow
=
(
recordId
)
=>
{
setExpandedRowIds
((
current
)
=>
({
...
current
,
[
recordId
]:
!
current
[
recordId
],
}));
};
const
openDetail
=
(
recordOrName
)
=>
{
const
openDetail
=
(
recordOrName
)
=>
{
const
recordName
=
const
recordName
=
typeof
recordOrName
===
'string'
typeof
recordOrName
===
'string'
...
@@ -472,6 +645,20 @@ export default function RecordWorkbench({
...
@@ -472,6 +645,20 @@ export default function RecordWorkbench({
:
filterSchema
.
length
>
1
:
filterSchema
.
length
>
1
?
'xl:grid-cols-4'
?
'xl:grid-cols-4'
:
'xl:grid-cols-[1fr_auto]'
;
:
'xl:grid-cols-[1fr_auto]'
;
const
tableSectionClass
=
clsx
(
'overflow-hidden rounded-[32px] border border-white/80 bg-white/96 shadow-[0_18px_48px_rgba(15,23,42,0.08)] ring-1 ring-zinc-950/5'
,
stickyTable
&&
'sticky top-0 z-10 flex h-[calc(100dvh-6rem)] min-h-[32rem] flex-col'
,
);
const
fixedTablePanelClass
=
stickyTable
?
'shrink-0'
:
''
;
const
tableContentClass
=
stickyTable
?
'min-h-0 flex-1 overflow-auto overscroll-contain bg-white px-3 pt-0 [scrollbar-gutter:stable]'
:
'px-3 pb-3 pt-2'
;
const
tableHeadClass
=
stickyTable
?
'sticky top-0 z-40 bg-zinc-50 shadow-[0_10px_24px_rgba(15,23,42,0.08)]'
:
undefined
;
const
stateContentClass
=
stickyTable
?
'min-h-0 flex-1 overflow-auto px-6 py-6'
:
'px-6 py-6'
;
return
(
return
(
<
div
className=
"space-y-6 pb-4"
>
<
div
className=
"space-y-6 pb-4"
>
...
@@ -569,8 +756,8 @@ export default function RecordWorkbench({
...
@@ -569,8 +756,8 @@ export default function RecordWorkbench({
</
section
>
</
section
>
)
:
null
}
)
:
null
}
<
section
className=
"overflow-hidden rounded-[32px] border border-white/80 bg-white/96 shadow-[0_18px_48px_rgba(15,23,42,0.08)] ring-1 ring-zinc-950/5"
>
<
section
className=
{
tableSectionClass
}
>
<
div
className=
"flex flex-wrap items-start justify-between gap-4 border-b border-zinc-100 px-6 py-5"
>
<
div
className=
{
clsx
(
'flex flex-wrap items-start justify-between gap-4 border-b border-zinc-100 px-6 py-5'
,
fixedTablePanelClass
)
}
>
<
div
>
<
div
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
{
title
}
</
div
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
{
title
}
</
div
>
<
div
className=
"mt-1 text-sm text-slate-500"
>
<
div
className=
"mt-1 text-sm text-slate-500"
>
...
@@ -600,7 +787,7 @@ export default function RecordWorkbench({
...
@@ -600,7 +787,7 @@ export default function RecordWorkbench({
</
div
>
</
div
>
{
hasColumnGroups
?
(
{
hasColumnGroups
?
(
<
div
className=
"flex flex-wrap items-center gap-2 border-b border-zinc-100 bg-zinc-50/50 px-6 py-3"
>
<
div
className=
{
clsx
(
'flex flex-wrap items-center gap-2 border-b border-zinc-100 bg-zinc-50/50 px-6 py-3'
,
fixedTablePanelClass
)
}
>
<
span
className=
"mr-1 text-xs font-medium text-slate-400"
>
列分组
</
span
>
<
span
className=
"mr-1 text-xs font-medium text-slate-400"
>
列分组
</
span
>
{
columnGroupMeta
.
groups
.
map
((
group
)
=>
(
{
columnGroupMeta
.
groups
.
map
((
group
)
=>
(
<
ColumnGroupButton
<
ColumnGroupButton
...
@@ -614,95 +801,108 @@ export default function RecordWorkbench({
...
@@ -614,95 +801,108 @@ export default function RecordWorkbench({
)
:
null
}
)
:
null
}
{
error
?
(
{
error
?
(
<
div
className=
"border-b border-zinc-100 bg-rose-50/70 px-6 py-4 text-sm text-rose-700"
>
<
div
className=
{
clsx
(
'border-b border-zinc-100 bg-rose-50/70 px-6 py-4 text-sm text-rose-700'
,
fixedTablePanelClass
)
}
>
{
error
}
{
error
}
</
div
>
</
div
>
)
:
null
}
)
:
null
}
{
states
?.
success
?
(
{
states
?.
success
?
(
<
div
className=
"border-b border-zinc-100 bg-emerald-50/70 px-6 py-4 text-sm text-emerald-700"
>
<
div
className=
{
clsx
(
'border-b border-zinc-100 bg-emerald-50/70 px-6 py-4 text-sm text-emerald-700'
,
fixedTablePanelClass
)
}
>
{
states
.
success
}
{
states
.
success
}
</
div
>
</
div
>
)
:
null
}
)
:
null
}
{
loading
?
(
{
loading
?
(
<
div
className=
"px-6 py-6"
>
<
div
className=
{
stateContentClass
}
>
<
EmptyState
<
EmptyState
title=
{
states
?.
loadingTitle
||
'正在加载列表'
}
title=
{
states
?.
loadingTitle
||
'正在加载列表'
}
description=
{
states
?.
loadingDescription
||
'正在读取对象结构、视图配置和记录数据,请稍候。'
}
description=
{
states
?.
loadingDescription
||
'正在读取对象结构、视图配置和记录数据,请稍候。'
}
/>
/>
</
div
>
</
div
>
)
:
totalRecords
===
0
?
(
)
:
pageRecordCount
===
0
?
(
<
div
className=
"px-6 py-6"
>
<
div
className=
{
stateContentClass
}
>
<
EmptyState
<
EmptyState
title=
{
states
?.
emptyTitle
||
'当前暂无数据'
}
title=
{
states
?.
emptyTitle
||
'当前暂无数据'
}
description=
{
states
?.
emptyDescription
||
'可以切换视图或调整筛选条件后重试。'
}
description=
{
states
?.
emptyDescription
||
'可以切换视图或调整筛选条件后重试。'
}
/>
/>
</
div
>
</
div
>
)
:
(
)
:
(
<
div
className=
"px-3 pb-3 pt-2"
>
<>
<
Table
bleed
dense
className=
"[--gutter:--spacing(3)]"
>
<
div
ref=
{
tableScrollRef
}
className=
{
tableContentClass
}
>
<
TableHead
>
<
Table
bleed
dense
scrollable=
{
!
stickyTable
}
className=
"[--gutter:--spacing(3)]"
>
{
showColumnGroupHeader
?
(
<
TableHead
className=
{
tableHeadClass
}
>
<
TableRow
className=
"bg-zinc-50/90"
>
{
showColumnGroupHeader
?
(
{
selectable
?
(
<
TableRow
className=
"bg-zinc-50"
>
<
TableHeader
{
selectable
?
(
rowSpan=
{
2
}
className=
"w-14 sticky left-0 z-40 bg-zinc-50/95 backdrop-blur shadow-[10px_0_20px_rgba(15,23,42,0.06)]"
style=
{
{
width
:
SELECTION_COLUMN_WIDTH
,
minWidth
:
SELECTION_COLUMN_WIDTH
}
}
>
<
div
className=
"flex justify-center"
>
<
Checkbox
checked=
{
totalRecords
>
0
&&
selectedIds
.
length
===
totalRecords
}
onChange=
{
(
checked
)
=>
{
if
(
!
checked
)
{
onSelectionChange
?.([]);
return
;
}
onSelectionChange
?.(
records
.
map
((
record
)
=>
getRecordId
(
record
)));
}
}
color=
"rose"
aria
-
label=
"全选当前视图记录"
/>
</
div
>
</
TableHeader
>
)
:
null
}
{
columnGroupSegments
.
map
((
segment
)
=>
{
const
firstColumn
=
segment
.
columns
[
0
];
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
firstColumn
.
field
];
return
(
<
TableHeader
<
TableHeader
key=
{
segment
.
key
}
rowSpan=
{
2
}
colSpan=
{
segment
.
columns
.
length
}
className=
"w-14 sticky left-0 z-40 bg-zinc-50/95 backdrop-blur shadow-[10px_0_20px_rgba(15,23,42,0.06)]"
style=
{
{
style=
{
{
width
:
SELECTION_COLUMN_WIDTH
,
minWidth
:
SELECTION_COLUMN_WIDTH
}
}
left
:
stickyMeta
?.
side
===
'left'
?
`${stickyMeta.offset}px`
:
undefined
,
right
:
stickyMeta
?.
side
===
'right'
?
`${stickyMeta.offset}px`
:
undefined
,
}
}
className=
{
clsx
(
'text-xs font-semibold text-slate-500'
,
segment
.
group
?
'bg-rose-50/80 text-rose-700'
:
'bg-zinc-50/95 text-slate-300'
,
getStickyCellClass
(
stickyMeta
,
'header'
),
)
}
>
>
{
segment
.
group
?
(
<
div
className=
"flex justify-center"
>
<
button
<
Checkbox
type=
"button"
checked=
{
pageRecordCount
>
0
&&
selectedCurrentPageIds
.
length
===
pageRecordCount
}
onClick=
{
()
=>
toggleColumnGroup
(
segment
.
group
.
id
)
}
onChange=
{
(
checked
)
=>
{
className=
"inline-flex min-h-8 items-center gap-2 rounded-full px-2 py-1 transition hover:bg-white/70"
if
(
!
checked
)
{
aria
-
label=
{
`折叠${segment.group.name}`
}
onSelectionChange
?.(
>
selectedIds
.
filter
((
id
)
=>
!
currentPageRecordIds
.
includes
(
id
)),
<
span
aria
-
hidden=
"true"
>
-
</
span
>
);
<
span
>
{
segment
.
group
.
name
}
</
span
>
return
;
<
span
className=
"rounded-full bg-white/80 px-2 py-0.5 text-[11px] text-rose-500"
>
}
{
segment
.
group
.
children
.
length
}
onSelectionChange
?.(
</
span
>
Array
.
from
(
new
Set
([...
selectedIds
,
...
currentPageRecordIds
])),
</
button
>
);
)
:
null
}
}
}
color=
"rose"
aria
-
label=
"全选当前页记录"
/>
</
div
>
</
TableHeader
>
</
TableHeader
>
);
)
:
null
}
})
}
{
hasExpandColumn
?
(
<
TableHeader
rowSpan=
{
2
}
className=
"w-12 bg-zinc-50/95 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400"
style=
{
{
width
:
48
,
minWidth
:
48
}
}
/>
)
:
null
}
{
columnGroupSegments
.
map
((
segment
)
=>
{
const
firstColumn
=
segment
.
columns
[
0
];
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
firstColumn
.
field
];
return
(
<
TableHeader
key=
{
segment
.
key
}
colSpan=
{
segment
.
columns
.
length
}
style=
{
{
left
:
stickyMeta
?.
side
===
'left'
?
`${stickyMeta.offset}px`
:
undefined
,
right
:
stickyMeta
?.
side
===
'right'
?
`${stickyMeta.offset}px`
:
undefined
,
}
}
className=
{
clsx
(
'text-xs font-semibold text-slate-500'
,
segment
.
group
?
'bg-rose-50/80 text-rose-700'
:
'bg-zinc-50/95 text-slate-300'
,
getStickyCellClass
(
stickyMeta
,
'header'
),
)
}
>
{
segment
.
group
?
(
<
button
type=
"button"
onClick=
{
()
=>
toggleColumnGroup
(
segment
.
group
.
id
)
}
className=
"inline-flex min-h-8 items-center gap-2 rounded-full px-2 py-1 transition hover:bg-white/70"
aria
-
label=
{
`折叠${segment.group.name}`
}
>
<
span
aria
-
hidden=
"true"
>
-
</
span
>
<
span
>
{
segment
.
group
.
name
}
</
span
>
<
span
className=
"rounded-full bg-white/80 px-2 py-0.5 text-[11px] text-rose-500"
>
{
segment
.
group
.
children
.
length
}
</
span
>
</
button
>
)
:
null
}
</
TableHeader
>
);
})
}
{
rowActions
.
length
>
0
?
(
{
rowActions
.
length
>
0
?
(
<
TableHeader
<
TableHeader
...
@@ -716,7 +916,7 @@ export default function RecordWorkbench({
...
@@ -716,7 +916,7 @@ export default function RecordWorkbench({
</
TableRow
>
</
TableRow
>
)
:
null
}
)
:
null
}
<
TableRow
className=
"bg-zinc-50
/80
"
>
<
TableRow
className=
"bg-zinc-50"
>
{
selectable
&&
!
showColumnGroupHeader
?
(
{
selectable
&&
!
showColumnGroupHeader
?
(
<
TableHeader
<
TableHeader
className=
"w-14 sticky left-0 z-40 bg-zinc-50/95 backdrop-blur shadow-[10px_0_20px_rgba(15,23,42,0.06)]"
className=
"w-14 sticky left-0 z-40 bg-zinc-50/95 backdrop-blur shadow-[10px_0_20px_rgba(15,23,42,0.06)]"
...
@@ -724,21 +924,32 @@ export default function RecordWorkbench({
...
@@ -724,21 +924,32 @@ export default function RecordWorkbench({
>
>
<
div
className=
"flex justify-center"
>
<
div
className=
"flex justify-center"
>
<
Checkbox
<
Checkbox
checked=
{
totalRecords
>
0
&&
selectedIds
.
length
===
totalRecords
}
checked=
{
pageRecordCount
>
0
&&
selectedCurrentPageIds
.
length
===
pageRecordCount
}
onChange=
{
(
checked
)
=>
{
onChange=
{
(
checked
)
=>
{
if
(
!
checked
)
{
if
(
!
checked
)
{
onSelectionChange
?.([]);
onSelectionChange
?.(
selectedIds
.
filter
((
id
)
=>
!
currentPageRecordIds
.
includes
(
id
)),
);
return
;
return
;
}
}
onSelectionChange
?.(
records
.
map
((
record
)
=>
getRecordId
(
record
)));
onSelectionChange
?.(
Array
.
from
(
new
Set
([...
selectedIds
,
...
currentPageRecordIds
])),
);
}
}
}
}
color=
"rose"
color=
"rose"
aria
-
label=
"全选当前
视图
记录"
aria
-
label=
"全选当前
页
记录"
/>
/>
</
div
>
</
div
>
</
TableHeader
>
</
TableHeader
>
)
:
null
}
)
:
null
}
{
hasExpandColumn
&&
!
showColumnGroupHeader
?
(
<
TableHeader
className=
"w-12 bg-zinc-50 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400"
style=
{
{
width
:
48
,
minWidth
:
48
}
}
/>
)
:
null
}
{
stickyColumns
.
orderedColumns
.
map
((
column
)
=>
{
{
stickyColumns
.
orderedColumns
.
map
((
column
)
=>
{
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
column
.
field
];
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
column
.
field
];
...
@@ -752,7 +963,7 @@ export default function RecordWorkbench({
...
@@ -752,7 +963,7 @@ export default function RecordWorkbench({
right
:
stickyMeta
?.
side
===
'right'
?
`${stickyMeta.offset}px`
:
undefined
,
right
:
stickyMeta
?.
side
===
'right'
?
`${stickyMeta.offset}px`
:
undefined
,
}
}
}
}
className=
{
clsx
(
className=
{
clsx
(
'text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'
,
'
bg-zinc-50
text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'
,
getStickyCellClass
(
stickyMeta
,
'header'
),
getStickyCellClass
(
stickyMeta
,
'header'
),
)
}
)
}
>
>
...
@@ -775,6 +986,8 @@ export default function RecordWorkbench({
...
@@ -775,6 +986,8 @@ export default function RecordWorkbench({
<
TableBody
>
<
TableBody
>
{
records
.
map
((
record
)
=>
{
{
records
.
map
((
record
)
=>
{
const
recordId
=
getRecordId
(
record
);
const
recordId
=
getRecordId
(
record
);
const
expandable
=
hasExpandableRows
&&
(
isRowExpandable
?
isRowExpandable
(
record
)
:
true
);
const
expanded
=
Boolean
(
expandedRowIds
[
recordId
]);
const
rowContext
=
{
const
rowContext
=
{
record
,
record
,
records
,
records
,
...
@@ -785,7 +998,8 @@ export default function RecordWorkbench({
...
@@ -785,7 +998,8 @@ export default function RecordWorkbench({
const
visibleRowActions
=
rowActions
.
filter
((
action
)
=>
!
isActionHidden
(
action
,
rowContext
));
const
visibleRowActions
=
rowActions
.
filter
((
action
)
=>
!
isActionHidden
(
action
,
rowContext
));
return
(
return
(
<
TableRow
key=
{
recordId
}
className=
"align-top"
>
<
Fragment
key=
{
recordId
}
>
<
TableRow
className=
"align-top"
>
{
selectable
?
(
{
selectable
?
(
<
TableCell
<
TableCell
className=
"align-top sticky left-0 z-30 bg-white shadow-[10px_0_20px_rgba(15,23,42,0.05)]"
className=
"align-top sticky left-0 z-30 bg-white shadow-[10px_0_20px_rgba(15,23,42,0.05)]"
...
@@ -808,6 +1022,25 @@ export default function RecordWorkbench({
...
@@ -808,6 +1022,25 @@ export default function RecordWorkbench({
</
TableCell
>
</
TableCell
>
)
:
null
}
)
:
null
}
{
hasExpandColumn
?
(
<
TableCell
className=
"w-12 align-top"
style=
{
{
width
:
48
,
minWidth
:
48
}
}
>
{
expandable
?
(
<
button
type=
"button"
onClick=
{
()
=>
toggleExpandedRow
(
recordId
)
}
className=
"mt-1 inline-flex size-8 items-center justify-center rounded-full text-slate-500 transition hover:bg-zinc-100 hover:text-slate-900"
aria
-
label=
{
`${expanded ? '收起' : '展开'} ${getRecordLabel(record)}`
}
aria
-
expanded=
{
expanded
}
>
<
ChevronRightIcon
className=
{
clsx
(
'size-4 transition-transform'
,
expanded
&&
'rotate-90'
)
}
aria
-
hidden=
"true"
/>
</
button
>
)
:
null
}
</
TableCell
>
)
:
null
}
{
stickyColumns
.
orderedColumns
.
map
((
column
)
=>
{
{
stickyColumns
.
orderedColumns
.
map
((
column
)
=>
{
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
column
.
field
];
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
column
.
field
];
return
(
return
(
...
@@ -827,7 +1060,13 @@ export default function RecordWorkbench({
...
@@ -827,7 +1060,13 @@ export default function RecordWorkbench({
}
}
if
(
renderCell
)
{
if
(
renderCell
)
{
const
customNode
=
renderCell
({
record
,
column
});
const
customNode
=
renderCell
({
record
,
column
,
expandable
,
expanded
,
toggleExpanded
:
()
=>
toggleExpandedRow
(
recordId
),
});
if
(
customNode
!==
undefined
)
{
if
(
customNode
!==
undefined
)
{
return
customNode
;
return
customNode
;
}
}
...
@@ -861,11 +1100,21 @@ export default function RecordWorkbench({
...
@@ -861,11 +1100,21 @@ export default function RecordWorkbench({
</
TableCell
>
</
TableCell
>
)
:
null
}
)
:
null
}
</
TableRow
>
</
TableRow
>
{
expanded
&&
expandable
?
(
<
TableRow
key=
{
`${recordId}-expanded`
}
className=
"bg-zinc-50/70"
>
<
TableCell
colSpan=
{
tableColSpan
}
className=
"px-6 py-4"
>
{
renderExpandedRow
({
record
,
rowContext
})
}
</
TableCell
>
</
TableRow
>
)
:
null
}
</
Fragment
>
);
);
})
}
})
}
</
TableBody
>
</
TableBody
>
</
Table
>
</
Table
>
</
div
>
</
div
>
<
WorkbenchPagination
pagination=
{
pagination
}
/>
</>
)
}
)
}
</
section
>
</
section
>
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
9b9085cc
...
@@ -15,6 +15,8 @@ import {
...
@@ -15,6 +15,8 @@ import {
isViewAccessible
,
isViewAccessible
,
}
from
'@/utils/subjectView'
;
}
from
'@/utils/subjectView'
;
const
EMPTY_ARRAY
=
[];
function
CloseIcon
()
{
function
CloseIcon
()
{
return
(
return
(
<
svg
viewBox=
"0 0 20 20"
fill=
"none"
aria
-
hidden=
"true"
className=
"size-5"
>
<
svg
viewBox=
"0 0 20 20"
fill=
"none"
aria
-
hidden=
"true"
className=
"size-5"
>
...
@@ -46,6 +48,7 @@ const RecordDetailDrawer = (props, ref) => {
...
@@ -46,6 +48,7 @@ const RecordDetailDrawer = (props, ref) => {
readonly
=
false
,
readonly
=
false
,
filterView
=
[],
filterView
=
[],
preferredViewName
,
preferredViewName
,
disabledFields
=
EMPTY_ARRAY
,
}
=
props
;
}
=
props
;
const
[
refreshKey
,
setRefreshKey
]
=
useState
(()
=>
Date
.
now
());
const
[
refreshKey
,
setRefreshKey
]
=
useState
(()
=>
Date
.
now
());
...
@@ -122,6 +125,15 @@ const RecordDetailDrawer = (props, ref) => {
...
@@ -122,6 +125,15 @@ const RecordDetailDrawer = (props, ref) => {
const
activeView
=
detailViews
.
find
((
view
)
=>
view
.
name
===
activeTabKey
)
||
detailViews
[
0
];
const
activeView
=
detailViews
.
find
((
view
)
=>
view
.
name
===
activeTabKey
)
||
detailViews
[
0
];
const
drawerWidth
=
isFullscreen
?
'100vw'
:
typeof
width
===
'number'
?
`
${
width
}
px`
:
width
;
const
drawerWidth
=
isFullscreen
?
'100vw'
:
typeof
width
===
'number'
?
`
${
width
}
px`
:
width
;
const
disabledFieldSet
=
useMemo
(
()
=>
new
Set
(
disabledFields
.
flatMap
((
fieldName
)
=>
[
fieldName
,
`metadata.
${
fieldName
}
`
]),
),
[
disabledFields
],
);
const
isDrawerFieldDisabled
=
(
fieldPath
,
currentView
)
=>
disabledFieldSet
.
has
(
fieldPath
)
||
isFieldDisabled
(
fieldPath
,
currentView
);
const
closeDrawer
=
()
=>
{
const
closeDrawer
=
()
=>
{
onClose
?.();
onClose
?.();
...
@@ -209,7 +221,7 @@ const RecordDetailDrawer = (props, ref) => {
...
@@ -209,7 +221,7 @@ const RecordDetailDrawer = (props, ref) => {
onFieldUpdate=
{
handleFieldUpdate
}
onFieldUpdate=
{
handleFieldUpdate
}
onDataChange=
{
onDataChange
}
onDataChange=
{
onDataChange
}
readonly=
{
readonly
}
readonly=
{
readonly
}
isFieldDisabled=
{
isFieldDisabled
}
isFieldDisabled=
{
is
Drawer
FieldDisabled
}
/>
/>
)
}
)
}
</
main
>
</
main
>
...
...
src/pages/problem_rectification/flow-table/index.jsx
0 → 100644
View file @
9b9085cc
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
{
Input
}
from
'@/catalyst-ui-kit/javascript/input'
;
import
{
Select
}
from
'@/catalyst-ui-kit/javascript/select'
;
import
{
Table
,
TableBody
,
TableCell
,
TableHead
,
TableHeader
,
TableRow
,
}
from
'@/catalyst-ui-kit/javascript/table'
;
import
{
Textarea
}
from
'@/catalyst-ui-kit/javascript/textarea'
;
import
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
{
batchUpdateRecords
,
createNotice
,
executeAction
,
loadRecord
,
queryRecords
,
queryUsers
,
sendNotice
,
updateRecord
,
uploadFile
,
}
from
'@/services/DataService'
;
import
{
normalizeText
,
toArray
}
from
'@/utils/subjectView'
;
import
clsx
from
'clsx'
;
import
dayjs
from
'dayjs'
;
import
{
PencilSquareIcon
}
from
'@heroicons/react/24/outline'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useSearchParams
}
from
'umi'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
AUDIT_STATUS_APPROVED
,
AUDIT_STATUS_PENDING
,
AUDIT_STATUS_REJECTED
,
EDITOR_STATUS_COMPLETED
,
EDITOR_STATUS_PENDING
,
FLOW_MODE_BATCH
,
FLOW_MODE_DEPARTMENT
,
PROBLEM_RECTIFICATION_SUBJECT
,
buildDepartmentParentBatchUpdateData
,
getProblemRectificationFlowTableUrl
,
normalizeDepartmentRectificationItems
,
}
from
'../problemRectificationListService'
;
const
FLOW_MANAGED_PARENT_FIELDS
=
[
'fill_in_the_people'
,
'review_people'
,
'audit_status'
,
'final_audit_status'
,
'department_rectification_items'
,
];
const
DEPARTMENT_MANAGED_PARENT_FIELDS
=
[
...
FLOW_MANAGED_PARENT_FIELDS
,
'rectification_progress'
,
'rectification_situation'
,
'the_time_of_rectification'
,
'rectification_proof_materials'
,
];
function
toId
(
value
)
{
if
(
!
value
)
{
return
''
;
}
if
(
typeof
value
===
'string'
)
{
return
value
;
}
return
value
.
_id
||
value
.
id
||
value
.
name
||
''
;
}
function
normalizeRestrictionValue
(
value
)
{
if
(
Array
.
isArray
(
value
))
{
return
value
.
map
((
item
)
=>
normalizeText
(
item
)).
filter
(
Boolean
);
}
if
(
typeof
value
===
'string'
)
{
return
value
.
split
(
','
).
map
((
item
)
=>
normalizeText
(
item
)).
filter
(
Boolean
);
}
return
[];
}
function
buildProgressFallback
(
step
)
{
return
`
${
step
}
_
${
Date
.
now
()}
_
${
Math
.
random
().
toString
(
36
).
slice
(
2
,
10
)}
`
;
}
function
formatDateInput
(
value
)
{
if
(
!
value
)
{
return
''
;
}
const
parsed
=
dayjs
(
value
);
return
parsed
.
isValid
()
?
parsed
.
format
(
'YYYY-MM-DD'
)
:
normalizeText
(
value
).
slice
(
0
,
10
);
}
function
formatDateDisplay
(
value
)
{
if
(
!
value
)
{
return
'-'
;
}
const
parsed
=
dayjs
(
value
);
return
parsed
.
isValid
()
?
parsed
.
format
(
'YYYY-MM-DD'
)
:
normalizeText
(
value
)
||
'-'
;
}
function
StatusPill
({
value
})
{
const
toneClass
=
value
===
EDITOR_STATUS_COMPLETED
||
value
===
AUDIT_STATUS_APPROVED
?
'bg-emerald-50 text-emerald-700 ring-emerald-100'
:
value
===
AUDIT_STATUS_REJECTED
?
'bg-rose-50 text-rose-700 ring-rose-100'
:
'bg-amber-50 text-amber-700 ring-amber-100'
;
return
(
<
span
className=
{
clsx
(
'inline-flex rounded-full px-2.5 py-1 text-xs font-medium ring-1'
,
toneClass
)
}
>
{
value
||
AUDIT_STATUS_PENDING
}
</
span
>
);
}
function
collectUsers
(
ids
=
[])
{
return
Array
.
from
(
new
Set
(
ids
.
map
((
item
)
=>
toId
(
item
)).
filter
(
Boolean
)));
}
async
function
createProgressTask
({
recipientId
,
title
,
currentUser
,
returnUrl
})
{
if
(
!
recipientId
)
{
return
''
;
}
try
{
const
result
=
await
executeAction
(
'oa_notice:createProgress'
,
{
title
,
classify
:
'流程待办'
,
contentType
:
'流程待办'
,
receiveUserId
:
recipientId
,
receiveUserName
:
recipientId
,
receiveDisplayName
:
recipientId
,
sendUserId
:
currentUser
?.
_id
||
''
,
sendUserName
:
currentUser
?.
username
||
''
,
sendDisplayName
:
currentUser
?.
display_name
||
''
,
returnUrl
,
});
return
result
?.
data
?.
taskId
||
buildProgressFallback
(
'progress'
);
}
catch
(
error
)
{
return
buildProgressFallback
(
'progress'
);
}
}
async
function
completeProgressTask
(
progressId
)
{
if
(
!
progressId
)
{
return
;
}
try
{
await
executeAction
(
'oa_notice:updateProgressStatus'
,
{
taskId
:
progressId
,
});
}
catch
(
error
)
{
// OA 失败不阻断业务状态回写。
}
}
async
function
sendNoticeToUsers
({
title
,
content
,
from
,
to
})
{
const
receivers
=
collectUsers
(
to
);
if
(
receivers
.
length
===
0
)
{
return
;
}
const
notice
=
await
createNotice
({
title
,
content
,
type
:
'internal'
,
from
:
from
||
''
,
to
:
receivers
,
settings
:
{},
});
await
sendNotice
(
notice
.
name
);
}
function
buildDepartmentRows
(
records
=
[])
{
return
records
.
flatMap
((
record
)
=>
{
const
items
=
normalizeDepartmentRectificationItems
(
record
);
return
items
.
map
((
item
)
=>
({
key
:
`
${
record
.
_id
||
record
.
name
}
-
${
item
.
item_id
}
`
,
flowMode
:
FLOW_MODE_DEPARTMENT
,
record
,
item
,
itemId
:
item
.
item_id
,
recordId
:
record
.
_id
||
record
.
name
,
title
:
record
.
title
||
record
.
metadata
?.
project_name
||
'-'
,
projectName
:
record
.
metadata
?.
project_name
||
'-'
,
problemBrief
:
record
.
metadata
?.
problem_brief
||
''
,
relatedIssueDomain
:
toArray
(
record
.
metadata
?.
related_issue_domain
).
join
(
'、'
),
department
:
item
.
responsible_department
||
'-'
,
editor
:
item
.
editor
,
auditor
:
item
.
auditor
,
progress
:
item
.
rectification_progress
||
''
,
situation
:
item
.
rectification_situation
||
''
,
dueDate
:
item
.
the_time_of_rectification
||
''
,
proof
:
item
.
rectification_proof_materials
||
null
,
editorStatus
:
item
.
editor_status
||
EDITOR_STATUS_PENDING
,
auditStatus
:
item
.
audit_status
||
AUDIT_STATUS_PENDING
,
finalAuditStatus
:
item
.
final_audit_status
||
AUDIT_STATUS_PENDING
,
progressIdEditor
:
item
.
progress_id_editor
||
''
,
progressIdAuditor
:
item
.
progress_id_auditor
||
''
,
progressIdSender
:
item
.
progress_id_sender
||
''
,
}));
});
}
function
buildBatchRows
(
records
=
[],
metadata
=
{})
{
const
editor
=
toId
(
toArray
(
metadata
.
editor
)[
0
]);
const
auditor
=
toId
(
toArray
(
metadata
.
auditor
)[
0
]);
return
records
.
map
((
record
)
=>
{
const
recordMetadata
=
record
.
metadata
||
{};
return
{
key
:
record
.
_id
||
record
.
name
,
flowMode
:
FLOW_MODE_BATCH
,
record
,
item
:
null
,
itemId
:
record
.
_id
||
record
.
name
,
recordId
:
record
.
_id
||
record
.
name
,
title
:
record
.
title
||
recordMetadata
.
project_name
||
'-'
,
projectName
:
recordMetadata
.
project_name
||
'-'
,
problemBrief
:
recordMetadata
.
problem_brief
||
''
,
relatedIssueDomain
:
toArray
(
recordMetadata
.
related_issue_domain
).
join
(
'、'
),
department
:
toArray
(
recordMetadata
.
responsible_department
).
join
(
'、'
)
||
'-'
,
editor
,
auditor
,
progress
:
recordMetadata
.
rectification_progress
||
''
,
situation
:
recordMetadata
.
rectification_situation
||
''
,
dueDate
:
recordMetadata
.
the_time_of_rectification
||
''
,
proof
:
recordMetadata
.
rectification_proof_materials
||
null
,
editorStatus
:
metadata
.
editor_status
||
EDITOR_STATUS_PENDING
,
auditStatus
:
recordMetadata
.
audit_status
||
AUDIT_STATUS_PENDING
,
finalAuditStatus
:
recordMetadata
.
final_audit_status
||
AUDIT_STATUS_PENDING
,
progressIdEditor
:
toArray
(
metadata
.
progress_id_editor
)[
0
]
||
''
,
progressIdAuditor
:
toArray
(
metadata
.
progress_id_auditor
)[
0
]
||
''
,
progressIdSender
:
metadata
.
progress_id_sender
||
''
,
};
});
}
export
default
function
ProblemRectificationFlowTablePage
()
{
const
userInfo
=
useContext
(
UserContext
);
const
[
searchParams
]
=
useSearchParams
();
const
messageId
=
searchParams
.
get
(
'id'
)
||
''
;
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
submittingKey
,
setSubmittingKey
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
success
,
setSuccess
]
=
useState
(
''
);
const
[
messageRecord
,
setMessageRecord
]
=
useState
(
null
);
const
[
records
,
setRecords
]
=
useState
([]);
const
[
userLabels
,
setUserLabels
]
=
useState
({});
const
[
drafts
,
setDrafts
]
=
useState
({});
const
[
editingRecordName
,
setEditingRecordName
]
=
useState
(
''
);
const
metadata
=
useMemo
(()
=>
messageRecord
?.
metadata
||
{},
[
messageRecord
]);
const
flowMode
=
metadata
.
flow_mode
||
FLOW_MODE_BATCH
;
const
rows
=
useMemo
(
()
=>
flowMode
===
FLOW_MODE_DEPARTMENT
?
buildDepartmentRows
(
records
)
:
buildBatchRows
(
records
,
metadata
),
[
flowMode
,
metadata
,
records
],
);
const
currentUserId
=
toId
(
userInfo
?.
_id
);
const
senderId
=
toId
(
metadata
.
sender
);
const
isSender
=
Boolean
(
currentUserId
&&
senderId
===
currentUserId
);
const
returnUrl
=
getProblemRectificationFlowTableUrl
(
messageRecord
?.
name
||
messageRecord
?.
_id
||
messageId
,
);
const
loadFlow
=
async
()
=>
{
if
(
!
messageId
)
{
setError
(
'缺少流程 ID。'
);
setLoading
(
false
);
return
;
}
setLoading
(
true
);
setError
(
''
);
try
{
const
nextMessage
=
await
loadRecord
(
messageId
);
const
nextMetadata
=
nextMessage
?.
metadata
||
{};
const
recordIds
=
normalizeRestrictionValue
(
nextMetadata
.
restriction_value
);
const
result
=
recordIds
.
length
?
await
queryRecords
({
subject
:
nextMetadata
.
subject_name
||
PROBLEM_RECTIFICATION_SUBJECT
,
page
:
0
,
page_size
:
1000
,
mode
:
'full'
,
filter
:
{
_id
:
{
$in
:
recordIds
},
},
})
:
{
records
:
[]
};
setMessageRecord
(
nextMessage
);
setRecords
(
result
?.
records
||
[]);
setDrafts
({});
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'流程表格加载失败,请稍后重试。'
);
setMessageRecord
(
null
);
setRecords
([]);
}
finally
{
setLoading
(
false
);
}
};
useEffect
(()
=>
{
loadFlow
();
},
[
messageId
]);
useEffect
(()
=>
{
const
userIds
=
Array
.
from
(
new
Set
(
rows
.
flatMap
((
row
)
=>
[
row
.
editor
,
row
.
auditor
]).
map
(
toId
).
filter
(
Boolean
)),
);
if
(
userIds
.
length
===
0
)
{
setUserLabels
({});
return
undefined
;
}
let
cancelled
=
false
;
queryUsers
(
0
,
1000
,
{
_id
:
{
$in
:
userIds
}
})
.
then
((
result
)
=>
{
if
(
cancelled
)
{
return
;
}
setUserLabels
(
Object
.
fromEntries
(
(
result
?.
users
||
[]).
map
((
user
)
=>
[
user
.
_id
,
user
.
display_name
||
user
.
username
||
user
.
_id
,
]),
),
);
})
.
catch
(()
=>
{
if
(
!
cancelled
)
{
setUserLabels
({});
}
});
return
()
=>
{
cancelled
=
true
;
};
},
[
rows
]);
const
getUserLabel
=
(
value
)
=>
{
const
userId
=
toId
(
value
);
return
userLabels
[
userId
]
||
userId
||
'-'
;
};
const
getDraft
=
(
row
)
=>
({
progress
:
row
.
progress
,
situation
:
row
.
situation
,
dueDate
:
formatDateInput
(
row
.
dueDate
),
proof
:
row
.
proof
,
...(
drafts
[
row
.
key
]
||
{}),
});
const
setDraftValue
=
(
rowKey
,
patch
)
=>
{
setDrafts
((
current
)
=>
({
...
current
,
[
rowKey
]:
{
...(
current
[
rowKey
]
||
{}),
...
patch
,
},
}));
};
const
updateDepartmentItem
=
async
(
row
,
patch
)
=>
{
const
nextItems
=
normalizeDepartmentRectificationItems
(
row
.
record
).
map
((
item
)
=>
item
.
item_id
===
row
.
itemId
?
{
...
item
,
...
patch
,
updated_by
:
currentUserId
,
updated_at
:
dayjs
().
toISOString
(),
}
:
item
,
);
await
batchUpdateRecords
({
subject
:
PROBLEM_RECTIFICATION_SUBJECT
,
filter
:
{
_id
:
row
.
recordId
,
},
data
:
buildDepartmentParentBatchUpdateData
(
nextItems
),
});
};
const
updateBatchRow
=
async
(
row
,
patch
)
=>
{
await
updateRecord
(
row
.
recordId
,
{
metadata
:
{
rectification_progress
:
patch
.
rectification_progress
??
row
.
progress
,
rectification_situation
:
patch
.
rectification_situation
??
row
.
situation
,
the_time_of_rectification
:
patch
.
the_time_of_rectification
??
row
.
dueDate
,
rectification_proof_materials
:
patch
.
rectification_proof_materials
??
row
.
proof
,
audit_status
:
patch
.
audit_status
??
row
.
auditStatus
,
final_audit_status
:
patch
.
final_audit_status
??
row
.
finalAuditStatus
,
},
});
};
const
withRowSubmit
=
async
(
row
,
handler
)
=>
{
setSubmittingKey
(
row
.
key
);
setError
(
''
);
setSuccess
(
''
);
try
{
await
handler
();
await
loadFlow
();
setSuccess
(
'操作已完成。'
);
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'操作失败,请稍后重试。'
);
}
finally
{
setSubmittingKey
(
''
);
}
};
const
saveRowDraft
=
async
(
row
)
=>
{
const
draft
=
getDraft
(
row
);
const
patch
=
{
rectification_progress
:
draft
.
progress
,
rectification_situation
:
draft
.
situation
,
the_time_of_rectification
:
draft
.
dueDate
?
dayjs
(
draft
.
dueDate
).
toISOString
()
:
null
,
rectification_proof_materials
:
draft
.
proof
||
null
,
};
if
(
row
.
flowMode
===
FLOW_MODE_DEPARTMENT
)
{
await
updateDepartmentItem
(
row
,
patch
);
return
patch
;
}
await
updateBatchRow
(
row
,
patch
);
return
patch
;
};
const
handleSave
=
async
(
row
)
=>
{
await
withRowSubmit
(
row
,
async
()
=>
{
await
saveRowDraft
(
row
);
});
};
const
handleUpload
=
async
(
row
,
event
)
=>
{
const
file
=
event
.
target
.
files
?.[
0
];
if
(
!
file
)
{
return
;
}
await
withRowSubmit
(
row
,
async
()
=>
{
const
uploaded
=
await
uploadFile
(
file
);
const
fileValue
=
uploaded
?.
file
||
uploaded
?.
data
||
uploaded
;
setDraftValue
(
row
.
key
,
{
proof
:
fileValue
});
if
(
row
.
flowMode
===
FLOW_MODE_DEPARTMENT
)
{
await
updateDepartmentItem
(
row
,
{
rectification_proof_materials
:
fileValue
});
return
;
}
await
updateBatchRow
(
row
,
{
rectification_progress
:
row
.
progress
,
rectification_situation
:
row
.
situation
,
the_time_of_rectification
:
row
.
dueDate
||
null
,
rectification_proof_materials
:
fileValue
,
audit_status
:
row
.
auditStatus
,
final_audit_status
:
row
.
finalAuditStatus
,
});
});
};
const
handleEditorSubmit
=
async
(
row
)
=>
{
await
withRowSubmit
(
row
,
async
()
=>
{
const
savedPatch
=
await
saveRowDraft
(
row
);
const
progressIdAuditor
=
await
createProgressTask
({
recipientId
:
row
.
auditor
,
title
:
`
${
dayjs
().
format
(
'YYYY-MM-DD'
)}
问题整改表复核 -
${
row
.
department
}
`
,
currentUser
:
userInfo
,
returnUrl
,
});
await
completeProgressTask
(
row
.
progressIdEditor
);
if
(
row
.
flowMode
===
FLOW_MODE_DEPARTMENT
)
{
await
updateDepartmentItem
(
row
,
{
...
savedPatch
,
editor_status
:
EDITOR_STATUS_COMPLETED
,
audit_status
:
AUDIT_STATUS_PENDING
,
progress_id_auditor
:
progressIdAuditor
,
});
}
else
{
await
updateBatchRow
(
row
,
{
...
savedPatch
,
audit_status
:
AUDIT_STATUS_PENDING
,
final_audit_status
:
row
.
finalAuditStatus
,
});
await
updateRecord
(
messageRecord
.
_id
||
messageRecord
.
name
,
{
metadata
:
{
progress_id_auditor
:
[
progressIdAuditor
],
editor_status
:
EDITOR_STATUS_COMPLETED
,
auditor_status
:
EDITOR_STATUS_PENDING
,
},
});
}
await
sendNoticeToUsers
({
title
:
'问题整改表待复核'
,
content
:
`请点击链接复核问题整改表:
${
returnUrl
}
`
,
from
:
currentUserId
,
to
:
[
row
.
auditor
],
});
});
};
const
handleAuditorDecision
=
async
(
row
,
approved
)
=>
{
await
withRowSubmit
(
row
,
async
()
=>
{
await
completeProgressTask
(
row
.
progressIdAuditor
);
const
nextAuditStatus
=
approved
?
AUDIT_STATUS_APPROVED
:
AUDIT_STATUS_REJECTED
;
const
progressIdSender
=
approved
?
await
createProgressTask
({
recipientId
:
senderId
,
title
:
`
${
dayjs
().
format
(
'YYYY-MM-DD'
)}
问题整改表最终审批 -
${
row
.
department
}
`
,
currentUser
:
userInfo
,
returnUrl
,
})
:
''
;
const
progressIdEditor
=
approved
?
''
:
await
createProgressTask
({
recipientId
:
row
.
editor
,
title
:
`复核驳回,请重新填写问题整改表
${
dayjs
().
format
(
'YYYY-MM-DD'
)}
-
${
row
.
department
}
`
,
currentUser
:
userInfo
,
returnUrl
,
});
if
(
row
.
flowMode
===
FLOW_MODE_DEPARTMENT
)
{
await
updateDepartmentItem
(
row
,
{
audit_status
:
nextAuditStatus
,
editor_status
:
approved
?
EDITOR_STATUS_COMPLETED
:
EDITOR_STATUS_PENDING
,
final_audit_status
:
approved
?
AUDIT_STATUS_PENDING
:
row
.
finalAuditStatus
,
progress_id_sender
:
progressIdSender
,
progress_id_editor
:
approved
?
row
.
progressIdEditor
:
progressIdEditor
,
});
}
else
{
await
updateBatchRow
(
row
,
{
rectification_progress
:
row
.
progress
,
rectification_situation
:
row
.
situation
,
the_time_of_rectification
:
row
.
dueDate
,
rectification_proof_materials
:
row
.
proof
,
audit_status
:
nextAuditStatus
,
final_audit_status
:
approved
?
AUDIT_STATUS_PENDING
:
row
.
finalAuditStatus
,
});
await
updateRecord
(
messageRecord
.
_id
||
messageRecord
.
name
,
{
metadata
:
{
progress_id_sender
:
progressIdSender
,
auditor_status
:
approved
?
EDITOR_STATUS_COMPLETED
:
AUDIT_STATUS_REJECTED
,
editor_status
:
approved
?
EDITOR_STATUS_COMPLETED
:
EDITOR_STATUS_PENDING
,
progress_id_editor
:
approved
?
metadata
.
progress_id_editor
:
[
progressIdEditor
],
},
});
}
await
sendNoticeToUsers
({
title
:
approved
?
'问题整改表待最终审批'
:
'问题整改表复核驳回'
,
content
:
approved
?
`请点击链接最终审批问题整改表:
${
returnUrl
}
`
:
`问题整改表复核未通过,请点击链接重新填写:
${
returnUrl
}
`
,
from
:
currentUserId
,
to
:
approved
?
[
senderId
]
:
[
row
.
editor
],
});
});
};
const
handleSenderDecision
=
async
(
row
,
approved
)
=>
{
await
withRowSubmit
(
row
,
async
()
=>
{
await
completeProgressTask
(
row
.
progressIdSender
);
const
nextFinalStatus
=
approved
?
AUDIT_STATUS_APPROVED
:
AUDIT_STATUS_REJECTED
;
const
progressIdEditor
=
approved
?
''
:
await
createProgressTask
({
recipientId
:
row
.
editor
,
title
:
`最终审批驳回,请重新填写问题整改表
${
dayjs
().
format
(
'YYYY-MM-DD'
)}
-
${
row
.
department
}
`
,
currentUser
:
userInfo
,
returnUrl
,
});
if
(
row
.
flowMode
===
FLOW_MODE_DEPARTMENT
)
{
await
updateDepartmentItem
(
row
,
{
final_audit_status
:
nextFinalStatus
,
editor_status
:
approved
?
row
.
editorStatus
:
EDITOR_STATUS_PENDING
,
progress_id_editor
:
approved
?
row
.
progressIdEditor
:
progressIdEditor
,
});
}
else
{
await
updateBatchRow
(
row
,
{
rectification_progress
:
row
.
progress
,
rectification_situation
:
row
.
situation
,
the_time_of_rectification
:
row
.
dueDate
,
rectification_proof_materials
:
row
.
proof
,
audit_status
:
row
.
auditStatus
,
final_audit_status
:
nextFinalStatus
,
});
if
(
!
approved
)
{
await
updateRecord
(
messageRecord
.
_id
||
messageRecord
.
name
,
{
metadata
:
{
progress_id_editor
:
[
progressIdEditor
],
editor_status
:
EDITOR_STATUS_PENDING
,
},
});
}
}
if
(
!
approved
)
{
await
sendNoticeToUsers
({
title
:
'问题整改表最终审批驳回'
,
content
:
`问题整改表最终审批未通过,请点击链接重新填写:
${
returnUrl
}
`
,
from
:
currentUserId
,
to
:
[
row
.
editor
],
});
}
});
};
const
canEditRow
=
(
row
)
=>
isSender
||
(
row
.
editor
===
currentUserId
&&
row
.
editorStatus
!==
EDITOR_STATUS_COMPLETED
)
||
(
row
.
auditor
===
currentUserId
&&
row
.
auditStatus
===
AUDIT_STATUS_REJECTED
);
const
canSubmitRow
=
(
row
)
=>
row
.
editor
===
currentUserId
&&
row
.
editorStatus
!==
EDITOR_STATUS_COMPLETED
;
const
canAuditRow
=
(
row
)
=>
row
.
auditor
===
currentUserId
&&
row
.
editorStatus
===
EDITOR_STATUS_COMPLETED
&&
row
.
auditStatus
!==
AUDIT_STATUS_APPROVED
;
const
canFinalAuditRow
=
(
row
)
=>
senderId
===
currentUserId
&&
row
.
auditStatus
===
AUDIT_STATUS_APPROVED
&&
row
.
finalAuditStatus
!==
AUDIT_STATUS_APPROVED
;
return
(
<
div
className=
"space-y-6 p-6"
>
<
section
className=
"rounded-[30px] border border-white/80 bg-white/95 px-6 py-5 shadow-sm"
>
<
div
className=
"flex flex-wrap items-start justify-between gap-4"
>
<
div
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.24em] text-rose-600"
>
Problem Rectification Flow
</
div
>
<
h1
className=
"mt-2 text-2xl font-semibold tracking-tight text-slate-950"
>
问题整改表格处理
</
h1
>
<
p
className=
"mt-2 text-sm text-slate-500"
>
当前流程:
{
messageId
||
'-'
}
;流程模式:
{
flowMode
===
FLOW_MODE_DEPARTMENT
?
'按责任部门'
:
'批量简单推送'
}
</
p
>
</
div
>
<
Button
outline
onClick=
{
loadFlow
}
disabled=
{
loading
||
Boolean
(
submittingKey
)
}
>
刷新
</
Button
>
</
div
>
</
section
>
{
error
?
(
<
div
className=
"rounded-[24px] border border-rose-200 bg-rose-50 px-5 py-4 text-sm text-rose-700"
>
{
error
}
</
div
>
)
:
null
}
{
success
?
(
<
div
className=
"rounded-[24px] border border-emerald-200 bg-emerald-50 px-5 py-4 text-sm text-emerald-700"
>
{
success
}
</
div
>
)
:
null
}
<
section
className=
"overflow-hidden rounded-[30px] border border-white/80 bg-white/96 shadow-sm"
>
{
loading
?
(
<
div
className=
"px-6 py-14 text-center text-sm text-slate-500"
>
正在加载流程表格...
</
div
>
)
:
rows
.
length
===
0
?
(
<
div
className=
"px-6 py-14 text-center text-sm text-slate-500"
>
当前流程没有可处理的整改明细。
</
div
>
)
:
(
<
Table
bleed
dense
className=
"[--gutter:--spacing(4)]"
>
<
TableHead
>
<
TableRow
className=
"bg-zinc-50"
>
<
TableHeader
>
问题
</
TableHeader
>
<
TableHeader
>
问题简述
</
TableHeader
>
<
TableHeader
>
涉及业务领域
</
TableHeader
>
<
TableHeader
>
责任部门
</
TableHeader
>
<
TableHeader
>
填写人 / 审批人
</
TableHeader
>
<
TableHeader
>
整改进度
</
TableHeader
>
<
TableHeader
>
预计完成
</
TableHeader
>
<
TableHeader
>
整改情况
</
TableHeader
>
<
TableHeader
>
证明材料
</
TableHeader
>
<
TableHeader
>
状态
</
TableHeader
>
<
TableHeader
>
操作
</
TableHeader
>
</
TableRow
>
</
TableHead
>
<
TableBody
>
{
rows
.
map
((
row
)
=>
{
const
draft
=
getDraft
(
row
);
const
editable
=
canEditRow
(
row
);
const
submitting
=
submittingKey
===
row
.
key
;
return
(
<
TableRow
key=
{
row
.
key
}
className=
"align-top"
>
<
TableCell
className=
"min-w-64"
>
<
div
className=
"font-medium text-slate-900"
>
{
row
.
title
}
</
div
>
<
div
className=
"mt-1 text-xs text-slate-500"
>
{
row
.
projectName
}
</
div
>
{
isSender
?
(
<
Button
plain
className=
"mt-2"
disabled=
{
Boolean
(
submittingKey
)
}
onClick=
{
()
=>
setEditingRecordName
(
row
.
record
?.
name
||
row
.
recordId
)
}
>
<
PencilSquareIcon
data
-
slot=
"icon"
/>
编辑问题记录
</
Button
>
)
:
null
}
</
TableCell
>
<
TableCell
className=
"min-w-72 max-w-96"
>
<
div
className=
"whitespace-pre-wrap break-words text-sm text-slate-700"
>
{
row
.
problemBrief
||
'-'
}
</
div
>
</
TableCell
>
<
TableCell
className=
"min-w-40 max-w-64"
>
<
div
className=
"whitespace-pre-wrap break-words text-sm text-slate-700"
>
{
row
.
relatedIssueDomain
||
'-'
}
</
div
>
</
TableCell
>
<
TableCell
>
{
row
.
department
}
</
TableCell
>
<
TableCell
className=
"min-w-48"
>
<
div
className=
"text-xs text-slate-500"
>
填:
{
getUserLabel
(
row
.
editor
)
}
</
div
>
<
div
className=
"mt-1 text-xs text-slate-500"
>
审:
{
getUserLabel
(
row
.
auditor
)
}
</
div
>
</
TableCell
>
<
TableCell
className=
"min-w-36"
>
{
editable
?
(
<
Select
value=
{
draft
.
progress
}
onChange=
{
(
event
)
=>
setDraftValue
(
row
.
key
,
{
progress
:
event
.
target
.
value
})
}
>
<
option
value=
""
>
请选择
</
option
>
<
option
value=
"已整改"
>
已整改
</
option
>
<
option
value=
"整改中"
>
整改中
</
option
>
<
option
value=
"部分整改"
>
部分整改
</
option
>
<
option
value=
"不适用"
>
不适用
</
option
>
<
option
value=
"已优化"
>
已优化
</
option
>
</
Select
>
)
:
(
draft
.
progress
||
'-'
)
}
</
TableCell
>
<
TableCell
className=
"min-w-40"
>
{
editable
?
(
<
Input
type=
"date"
value=
{
draft
.
dueDate
}
onChange=
{
(
event
)
=>
setDraftValue
(
row
.
key
,
{
dueDate
:
event
.
target
.
value
})
}
/>
)
:
(
formatDateDisplay
(
row
.
dueDate
)
)
}
</
TableCell
>
<
TableCell
className=
"min-w-72"
>
{
editable
?
(
<
Textarea
rows=
{
3
}
value=
{
draft
.
situation
}
onChange=
{
(
event
)
=>
setDraftValue
(
row
.
key
,
{
situation
:
event
.
target
.
value
})
}
/>
)
:
(
<
span
className=
"whitespace-pre-wrap"
>
{
row
.
situation
||
'-'
}
</
span
>
)
}
</
TableCell
>
<
TableCell
className=
"min-w-52"
>
{
row
.
proof
?.
url
?
(
<
a
href=
{
row
.
proof
.
url
}
target=
"_blank"
rel=
"noreferrer"
className=
"text-sm font-medium text-sky-700"
>
{
row
.
proof
.
name
||
'查看附件'
}
</
a
>
)
:
(
<
div
className=
"text-sm text-slate-400"
>
未上传
</
div
>
)
}
{
editable
?
(
<
input
type=
"file"
className=
"mt-2 block w-full text-xs text-slate-500"
disabled=
{
submitting
}
onChange=
{
(
event
)
=>
handleUpload
(
row
,
event
)
}
/>
)
:
null
}
</
TableCell
>
<
TableCell
className=
"min-w-44"
>
<
div
className=
"space-y-1.5"
>
<
StatusPill
value=
{
row
.
editorStatus
}
/>
<
StatusPill
value=
{
row
.
auditStatus
}
/>
<
StatusPill
value=
{
row
.
finalAuditStatus
}
/>
</
div
>
</
TableCell
>
<
TableCell
className=
"min-w-52"
>
<
div
className=
"flex flex-wrap gap-2"
>
{
editable
?
(
<
Button
outline
disabled=
{
submitting
}
onClick=
{
()
=>
handleSave
(
row
)
}
>
保存
</
Button
>
)
:
null
}
{
canSubmitRow
(
row
)
?
(
<
Button
color=
"blue"
disabled=
{
submitting
}
onClick=
{
()
=>
handleEditorSubmit
(
row
)
}
>
送审
</
Button
>
)
:
null
}
{
canAuditRow
(
row
)
?
(
<>
<
Button
color=
"emerald"
disabled=
{
submitting
}
onClick=
{
()
=>
handleAuditorDecision
(
row
,
true
)
}
>
审批通过
</
Button
>
<
Button
color=
"red"
disabled=
{
submitting
}
onClick=
{
()
=>
handleAuditorDecision
(
row
,
false
)
}
>
驳回
</
Button
>
</>
)
:
null
}
{
canFinalAuditRow
(
row
)
?
(
<>
<
Button
color=
"blue"
disabled=
{
submitting
}
onClick=
{
()
=>
handleSenderDecision
(
row
,
true
)
}
>
最终通过
</
Button
>
<
Button
color=
"red"
disabled=
{
submitting
}
onClick=
{
()
=>
handleSenderDecision
(
row
,
false
)
}
>
最终驳回
</
Button
>
</>
)
:
null
}
{
!
editable
&&
!
canAuditRow
(
row
)
&&
!
canFinalAuditRow
(
row
)
?
(
<
span
className=
"text-xs text-slate-400"
>
当前无可执行操作
</
span
>
)
:
null
}
</
div
>
</
TableCell
>
</
TableRow
>
);
})
}
</
TableBody
>
</
Table
>
)
}
</
section
>
<
RecordDetailDrawer
open=
{
Boolean
(
editingRecordName
)
}
recordName=
{
editingRecordName
}
subjectName=
{
PROBLEM_RECTIFICATION_SUBJECT
}
readonly=
{
!
isSender
}
disabledFields=
{
flowMode
===
FLOW_MODE_DEPARTMENT
?
DEPARTMENT_MANAGED_PARENT_FIELDS
:
FLOW_MANAGED_PARENT_FIELDS
}
onClose=
{
()
=>
{
setEditingRecordName
(
''
);
loadFlow
();
}
}
/>
</
div
>
);
}
src/pages/problem_rectification/list/index.jsx
View file @
9b9085cc
...
@@ -8,6 +8,14 @@ import {
...
@@ -8,6 +8,14 @@ import {
}
from
'@/catalyst-ui-kit/javascript/dialog'
;
}
from
'@/catalyst-ui-kit/javascript/dialog'
;
import
{
Input
}
from
'@/catalyst-ui-kit/javascript/input'
;
import
{
Input
}
from
'@/catalyst-ui-kit/javascript/input'
;
import
{
Select
}
from
'@/catalyst-ui-kit/javascript/select'
;
import
{
Select
}
from
'@/catalyst-ui-kit/javascript/select'
;
import
{
Table
,
TableBody
,
TableCell
,
TableHead
,
TableHeader
,
TableRow
,
}
from
'@/catalyst-ui-kit/javascript/table'
;
import
{
Textarea
}
from
'@/catalyst-ui-kit/javascript/textarea'
;
import
{
Textarea
}
from
'@/catalyst-ui-kit/javascript/textarea'
;
import
RecordWorkbench
from
'@/components/record-workbench'
;
import
RecordWorkbench
from
'@/components/record-workbench'
;
import
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
...
@@ -18,12 +26,14 @@ import {
...
@@ -18,12 +26,14 @@ import {
createNotice
,
createNotice
,
createRecord
,
createRecord
,
executeAction
,
executeAction
,
loadRecord
,
loadUser
,
loadUser
,
queryUsers
,
queryUsers
,
sendNotice
,
sendNotice
,
updateRecord
,
updateRecord
,
}
from
'@/services/DataService'
;
}
from
'@/services/DataService'
;
import
*
as
Headless
from
'@headlessui/react'
;
import
*
as
Headless
from
'@headlessui/react'
;
import
{
ChevronRightIcon
}
from
'@heroicons/react/20/solid'
;
import
clsx
from
'clsx'
;
import
clsx
from
'clsx'
;
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
...
@@ -35,15 +45,28 @@ import {
...
@@ -35,15 +45,28 @@ import {
normalizeText
,
normalizeText
,
}
from
'@/utils/subjectView'
;
}
from
'@/utils/subjectView'
;
import
{
import
{
DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE
,
PROBLEM_RECTIFICATION_SUBJECT
,
PROBLEM_RECTIFICATION_SUBJECT
,
buildGridViewCountMap
,
AUDIT_STATUS_PENDING
,
DEPARTMENT_RECTIFICATION_FIELD
,
EDITOR_STATUS_PENDING
,
FLOW_MODE_BATCH
,
FLOW_MODE_DEPARTMENT
,
PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS
,
buildDepartmentAssignmentsFromRecord
,
buildDepartmentParentBatchUpdateData
,
buildDepartmentRectificationItems
,
buildGridViews
,
buildGridViews
,
buildInitialCreateValues
,
buildInitialCreateValues
,
buildProblemRectificationConfiguredListView
,
buildProblemRectificationConfiguredListView
,
buildProblemRectificationCreateFormFields
,
buildProblemRectificationCreateFormFields
,
buildProblemRectificationCreatePayload
,
buildProblemRectificationCreatePayload
,
getProblemRectificationFlowTableUrl
,
getRecordCellText
,
getRecordCellText
,
loadProblemRectificationListData
,
loadProblemRectificationListResources
,
loadProblemRectificationListResources
,
loadProblemRectificationViewCountMap
,
normalizeDepartmentRectificationItems
,
}
from
'../problemRectificationListService'
;
}
from
'../problemRectificationListService'
;
const
FIELD_HINTS
=
{
const
FIELD_HINTS
=
{
...
@@ -284,6 +307,89 @@ function RemoteUserSelect({ label, value, onChange, placeholder, onSearch }) {
...
@@ -284,6 +307,89 @@ function RemoteUserSelect({ label, value, onChange, placeholder, onSearch }) {
);
);
}
}
function
MultiSelectDropdown
({
value
,
options
,
onChange
,
placeholder
=
'请选择'
})
{
const
selectedValues
=
Array
.
isArray
(
value
)
?
value
:
[];
const
optionByValue
=
useMemo
(
()
=>
new
Map
(
options
.
map
((
option
)
=>
[
option
.
value
,
option
])),
[
options
],
);
const
selectedLabels
=
selectedValues
.
map
((
item
)
=>
optionByValue
.
get
(
item
)?.
label
||
item
)
.
filter
(
Boolean
);
return
(
<
div
className=
"space-y-2"
>
<
Headless
.
Listbox
value=
{
selectedValues
}
onChange=
{
onChange
}
multiple
>
<
div
className=
"relative"
>
<
Headless
.
ListboxButton
className=
"relative block min-h-11 w-full rounded-lg border border-zinc-950/10 bg-white py-[calc(--spacing(2.5)-1px)] pl-[calc(--spacing(3.5)-1px)] pr-10 text-left text-base/6 text-zinc-950 shadow-sm outline-none transition data-focus:ring-2 data-focus:ring-blue-500 sm:min-h-9 sm:py-[calc(--spacing(1.5)-1px)] sm:pl-[calc(--spacing(3)-1px)] sm:text-sm/6"
>
{
selectedLabels
.
length
>
0
?
(
<
span
className=
"block truncate"
>
已选择
{
selectedLabels
.
length
}
项:
{
selectedLabels
.
join
(
'、'
)
}
</
span
>
)
:
(
<
span
className=
"block truncate text-zinc-500"
>
{
placeholder
}
</
span
>
)
}
<
span
className=
"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<
svg
className=
"size-5 stroke-zinc-500 sm:size-4"
viewBox=
"0 0 16 16"
aria
-
hidden=
"true"
fill=
"none"
>
<
path
d=
"M5.75 10.75L8 13L10.25 10.75"
strokeWidth=
{
1.5
}
strokeLinecap=
"round"
strokeLinejoin=
"round"
/>
<
path
d=
"M10.25 5.25L8 3L5.75 5.25"
strokeWidth=
{
1.5
}
strokeLinecap=
"round"
strokeLinejoin=
"round"
/>
</
svg
>
</
span
>
</
Headless
.
ListboxButton
>
<
Headless
.
ListboxOptions
className=
"absolute z-50 mt-2 max-h-72 w-full overflow-y-auto overscroll-contain rounded-xl border border-zinc-200 bg-white p-1 shadow-xl ring-1 ring-black/5 focus:outline-none"
>
{
options
.
map
((
option
)
=>
(
<
Headless
.
ListboxOption
key=
{
option
.
value
}
value=
{
option
.
value
}
className=
"group flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-slate-700 data-focus:bg-rose-50 data-focus:text-slate-950"
>
{
({
selected
})
=>
(
<>
<
span
className=
{
clsx
(
'flex size-4 shrink-0 items-center justify-center rounded border'
,
selected
?
'border-rose-500 bg-rose-500 text-white'
:
'border-zinc-300 bg-white text-transparent'
,
)
}
aria
-
hidden=
"true"
>
✓
</
span
>
<
span
className=
"min-w-0 flex-1 truncate"
>
{
option
.
label
}
</
span
>
</>
)
}
</
Headless
.
ListboxOption
>
))
}
</
Headless
.
ListboxOptions
>
</
div
>
</
Headless
.
Listbox
>
{
selectedLabels
.
length
>
0
?
(
<
div
className=
"flex flex-wrap gap-2"
>
{
selectedValues
.
map
((
item
)
=>
(
<
button
key=
{
item
}
type=
"button"
onClick=
{
()
=>
onChange
(
selectedValues
.
filter
((
selected
)
=>
selected
!==
item
))
}
className=
"inline-flex items-center gap-1 rounded-full bg-rose-50 px-2.5 py-1 text-xs font-medium text-rose-700 ring-1 ring-rose-100 transition hover:bg-rose-100"
aria
-
label=
{
`移除 ${optionByValue.get(item)?.label || item}`
}
>
<
span
>
{
optionByValue
.
get
(
item
)?.
label
||
item
}
</
span
>
<
span
aria
-
hidden=
"true"
>
×
</
span
>
</
button
>
))
}
</
div
>
)
:
null
}
</
div
>
);
}
function
CreateField
({
fieldDef
,
value
,
onChange
})
{
function
CreateField
({
fieldDef
,
value
,
onChange
})
{
const
options
=
getSelectOptions
(
fieldDef
);
const
options
=
getSelectOptions
(
fieldDef
);
const
isMultiple
=
Boolean
(
fieldDef
?.
multiple
);
const
isMultiple
=
Boolean
(
fieldDef
?.
multiple
);
...
@@ -307,19 +413,23 @@ function CreateField({ fieldDef, value, onChange }) {
...
@@ -307,19 +413,23 @@ function CreateField({ fieldDef, value, onChange }) {
}
}
if (fieldType === 'select') {
if (fieldType === 'select') {
if (isMultiple) {
return (
<MultiSelectDropdown
value={value || []}
options={options}
onChange={onChange}
placeholder="请选择"
/>
);
}
return (
return (
<Select
<Select
multiple={isMultiple}
value={value || ''}
value={isMultiple ? value || [] : value || ''}
onChange={(event) => onChange(event.target.value)}
onChange={(event) => {
if (isMultiple) {
onChange(Array.from(event.target.selectedOptions).map((option) => option.value));
return;
}
onChange(event.target.value);
}}
>
>
{!isMultiple ? <option value="">请选择</option> : null}
<option value="">请选择</option>
{options.map((option) => (
{options.map((option) => (
<option key={option.value} value={option.value}>
<option key={option.value} value={option.value}>
{option.label}
{option.label}
...
@@ -358,30 +468,7 @@ function FieldValuePreview({ column, record }) {
...
@@ -358,30 +468,7 @@ function FieldValuePreview({ column, record }) {
const value = getFieldValue(rawRecord, column.field);
const value = getFieldValue(rawRecord, column.field);
if (fieldName === 'title') {
if (fieldName === 'title') {
const tags = [
return <div className="font-medium text-slate-900">{record.title || '(无标题)'}</div>;
record.severityLevel ? { label: `
严重程度
$
{
record
.
severityLevel
}
`, tone: resolveLevelTone(record.severityLevel) } : null,
record.overallRating ? { label: `
整改评级
$
{
record
.
overallRating
}
`, tone: resolveLevelTone(record.overallRating, true) } : null,
record.primaryResponsibleDepartment
? { label: `
主责
$
{
record
.
primaryResponsibleDepartment
}
`, tone: 'default' }
: null,
record.isOverdue ? { label: '已逾期', tone: 'rose' } : null,
record.hasEconomicLoss || record.hasExternalImpact || record.hasEmployeeAccountability
? { label: '重点后果问题', tone: 'amber' }
: null,
].filter(Boolean);
return (
<div className="space-y-2">
<div className="font-medium text-slate-900">{record.title || '(无标题)'}</div>
{tags.length > 0 ? (
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<StatusPill key={tag.label} text={tag.label} tone={tag.tone} />
))}
</div>
) : null}
</div>
);
}
}
if (fieldName === 'rectification_progress') {
if (fieldName === 'rectification_progress') {
...
@@ -470,11 +557,21 @@ function FieldValuePreview({ column, record }) {
...
@@ -470,11 +557,21 @@ function FieldValuePreview({ column, record }) {
);
);
}
}
function DepartmentDetailField({ label, children }) {
return (
<div className="min-w-0">
<div className="text-xs font-medium text-slate-400">{label}</div>
<div className="mt-1.5 break-words text-sm text-slate-800">{children || '-'}</div>
</div>
);
}
export default function ProblemRectificationListPage() {
export default function ProblemRectificationListPage() {
const [records, setRecords] = useState([]);
const [records, setRecords] = useState([]);
const [subject, setSubject] = useState(null);
const [subject, setSubject] = useState(null);
const [views, setViews] = useState([]);
const [views, setViews] = useState([]);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(true);
const [recordsLoading, setRecordsLoading] = useState(false);
const [error, setError] = useState('');
const [error, setError] = useState('');
const [actionLoading, setActionLoading] = useState(false);
const [actionLoading, setActionLoading] = useState(false);
const [filters, setFilters] = useState({
const [filters, setFilters] = useState({
...
@@ -496,16 +593,26 @@ export default function ProblemRectificationListPage() {
...
@@ -496,16 +593,26 @@ export default function ProblemRectificationListPage() {
const [formValues, setFormValues] = useState({});
const [formValues, setFormValues] = useState({});
const [fillerUsers, setFillerUsers] = useState([]);
const [fillerUsers, setFillerUsers] = useState([]);
const [auditorUsers, setAuditorUsers] = useState([]);
const [auditorUsers, setAuditorUsers] = useState([]);
const [departmentAssignments, setDepartmentAssignments] = useState([]);
const [successMessage, setSuccessMessage] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE);
const [pagination, setPagination] = useState({
page: 0,
pageSize: DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE,
totalRecords: 0,
totalPages: 1,
});
const [gridViewCounts, setGridViewCounts] = useState({});
const [departmentUserLabels, setDepartmentUserLabels] = useState({});
const reload
List
= useCallback(async () => {
const reload
Resources
= useCallback(async () => {
setLoading(true);
setLoading(true);
setError('');
setError('');
setSuccessMessage('');
setSuccessMessage('');
try {
try {
const resources = await loadProblemRectificationListResources();
const resources = await loadProblemRectificationListResources();
setRecords(resources.records || []);
setSubject(resources.subject || null);
setSubject(resources.subject || null);
setViews(resources.views || []);
setViews(resources.views || []);
} catch (requestError) {
} catch (requestError) {
...
@@ -519,8 +626,48 @@ export default function ProblemRectificationListPage() {
...
@@ -519,8 +626,48 @@ export default function ProblemRectificationListPage() {
}, []);
}, []);
useEffect(() => {
useEffect(() => {
reloadList();
reloadResources();
}, [reloadList]);
}, [reloadResources]);
useEffect(() => {
const userIds = Array.from(
new Set(
records.flatMap((record) =>
(record.departmentItems || []).flatMap((item) => [item.editor, item.auditor]),
).filter(Boolean),
),
);
if (userIds.length === 0) {
setDepartmentUserLabels({});
return;
}
let cancelled = false;
queryUsers(0, 1000, { _id: { $in: userIds } })
.then((result) => {
if (cancelled) {
return;
}
setDepartmentUserLabels(
Object.fromEntries(
(result?.users || []).map((user) => [
user._id,
user.display_name || user.username || user._id,
]),
),
);
})
.catch(() => {
if (!cancelled) {
setDepartmentUserLabels({});
}
});
return () => {
cancelled = true;
};
}, [records]);
const gridViews = useMemo(() => buildGridViews(views), [views]);
const gridViews = useMemo(() => buildGridViews(views), [views]);
...
@@ -541,16 +688,82 @@ export default function ProblemRectificationListPage() {
...
@@ -541,16 +688,82 @@ export default function ProblemRectificationListPage() {
[gridViews, activeViewName],
[gridViews, activeViewName],
);
);
const reloadList = useCallback(
async (overrides = {}) => {
if (!subject || !activeGridView) {
return;
}
const nextPage = overrides.page ?? page;
const nextPageSize = overrides.pageSize ?? pageSize;
const nextFilters = overrides.filters ?? filters;
setRecordsLoading(true);
setError('');
if (!overrides.keepSuccessMessage) {
setSuccessMessage('');
}
try {
const [pageResult, countMap] = await Promise.all([
loadProblemRectificationListData({
page: nextPage,
pageSize: nextPageSize,
filters: nextFilters,
gridView: activeGridView,
subject,
}),
loadProblemRectificationViewCountMap({
filters: nextFilters,
views: gridViews,
subject,
}),
]);
const nextPagination = pageResult.pagination || {
page: nextPage,
pageSize: nextPageSize,
totalRecords: pageResult.records?.length || 0,
totalPages: 1,
};
setRecords(pageResult.records || []);
setPagination(nextPagination);
setGridViewCounts(countMap || {});
if (
nextPagination.totalRecords > 0 &&
nextPagination.page >= nextPagination.totalPages &&
nextPagination.totalPages > 0
) {
setPage(nextPagination.totalPages - 1);
}
} catch (requestError) {
setRecords([]);
setGridViewCounts({});
setPagination({
page: nextPage,
pageSize: nextPageSize,
totalRecords: 0,
totalPages: 1,
});
setError(requestError?.message || '问题整改列表加载失败,请稍后重试。');
} finally {
setRecordsLoading(false);
}
},
[activeGridView, filters, gridViews, page, pageSize, subject],
);
useEffect(() => {
reloadList();
}, [reloadList]);
const configuredView = useMemo(
const configuredView = useMemo(
() => buildProblemRectificationConfiguredListView(records, filters, activeGridView, subject),
() => buildProblemRectificationConfiguredListView(records, filters, activeGridView, subject),
[records, filters, activeGridView, subject],
[records, filters, activeGridView, subject],
);
);
const gridViewCounts = useMemo(
() => buildGridViewCountMap(records, filters, gridViews, subject),
[records, filters, gridViews, subject],
);
const createFields = useMemo(
const createFields = useMemo(
() => buildProblemRectificationCreateFormFields(activeGridView, subject),
() => buildProblemRectificationCreateFormFields(activeGridView, subject),
[activeGridView, subject],
[activeGridView, subject],
...
@@ -578,23 +791,24 @@ export default function ProblemRectificationListPage() {
...
@@ -578,23 +791,24 @@ export default function ProblemRectificationListPage() {
setSelectedIds((current) => current.filter((id) => visibleIds.has(id)));
setSelectedIds((current) => current.filter((id) => visibleIds.has(id)));
}, [filteredRecords]);
}, [filteredRecords]);
const fixedCount = filteredRecords.filter(
(item) => item.progress === '已整改' || item.progress === '已优化',
).length;
const highSeverityCount = filteredRecords.filter((item) => item.severityLevel === '高').length;
const consequenceCount = filteredRecords.filter(
(item) => item.hasEconomicLoss || item.hasExternalImpact || item.hasEmployeeAccountability,
).length;
const repeatedOrHistoricalCount = filteredRecords.filter(
(item) => item.isHistoricalProblem || item.isRepeatedProblem,
).length;
const overdueCount = filteredRecords.filter((item) => item.isOverdue).length;
const lowRatingCount = filteredRecords.filter((item) => item.overallRating === '低').length;
const pendingAuditCount = filteredRecords.filter(
(item) => item.auditStatus && item.auditStatus !== '已审批',
).length;
const quickSelectEnabled = activeGridView?.settings?.quickSelect !== false;
const quickSelectEnabled = activeGridView?.settings?.quickSelect !== false;
const totalMatchingRecords = pagination.totalRecords || 0;
const isDepartmentPushMode = showPushModal && selectedRecords.length === 1;
useEffect(() => {
if (!showPushModal) {
return;
}
if (selectedRecords.length === 1) {
setDepartmentAssignments(buildDepartmentAssignmentsFromRecord(selectedRecords[0].raw || selectedRecords[0]));
setFillerUsers([]);
setAuditorUsers([]);
return;
}
setDepartmentAssignments([]);
}, [showPushModal, selectedRecords]);
const handleCreate = async () => {
const handleCreate = async () => {
if (
if (
...
@@ -613,7 +827,8 @@ export default function ProblemRectificationListPage() {
...
@@ -613,7 +827,8 @@ export default function ProblemRectificationListPage() {
await createRecord(payload);
await createRecord(payload);
setShowCreateModal(false);
setShowCreateModal(false);
setFormValues(initialCreateValues);
setFormValues(initialCreateValues);
await reloadList();
setPage(0);
await reloadList({ page: 0 });
} catch (requestError) {
} catch (requestError) {
setError(requestError?.message || '新增问题整改记录失败,请稍后重试。');
setError(requestError?.message || '新增问题整改记录失败,请稍后重试。');
} finally {
} finally {
...
@@ -681,7 +896,8 @@ export default function ProblemRectificationListPage() {
...
@@ -681,7 +896,8 @@ export default function ProblemRectificationListPage() {
await cloneRecord(recordName);
await cloneRecord(recordName);
const title = cloneTargetRecord.title || cloneTargetRecord.projectName || cloneTargetRecord.name || '当前记录';
const title = cloneTargetRecord.title || cloneTargetRecord.projectName || cloneTargetRecord.name || '当前记录';
setCloneTargetRecord(null);
setCloneTargetRecord(null);
await reloadList();
setPage(0);
await reloadList({ page: 0 });
setSuccessMessage(`
已复制“
$
{
title
}
”,新记录已加入当前列表。
`);
setSuccessMessage(`
已复制“
$
{
title
}
”,新记录已加入当前列表。
`);
} catch (requestError) {
} catch (requestError) {
setError(requestError?.message || '复制问题整改记录失败,请稍后重试。');
setError(requestError?.message || '复制问题整改记录失败,请稍后重试。');
...
@@ -704,47 +920,123 @@ export default function ProblemRectificationListPage() {
...
@@ -704,47 +920,123 @@ export default function ProblemRectificationListPage() {
return result?.users || [];
return result?.users || [];
}, []);
}, []);
const updateDepartmentAssignment = (itemId, patch) => {
setDepartmentAssignments((current) =>
current.map((item) =>
item.item_id === itemId
? {
...item,
...patch,
}
: item,
),
);
};
const handlePush = async () => {
const handlePush = async () => {
const fillerIds = fillerUsers.map((user) => user._id).filter(Boolean);
const fillerIds = fillerUsers.map((user) => user._id).filter(Boolean);
const auditorIds = auditorUsers.map((user) => user._id).filter(Boolean);
const auditorIds = auditorUsers.map((user) => user._id).filter(Boolean);
const isDepartmentFlow = selectedRecords.length === 1;
if (selectedRecords.length === 0) {
if (selectedRecords.length === 0) {
setError('请先勾选需要推送的问题整改记录。');
setError('请先勾选需要推送的问题整改记录。');
return;
return;
}
}
if (fillerIds.length === 0 || auditorIds.length === 0) {
if (isDepartmentFlow) {
setError('请先选择填报人和复核人。');
const hasMissingDepartmentUser = departmentAssignments.some(
return;
(item) => !item.editor?._id || !item.auditor?._id,
);
if (departmentAssignments.length === 0 || hasMissingDepartmentUser) {
setError('请为每个责任部门选择 1 个填报人和 1 个复核人。');
return;
}
} else {
if (fillerIds.length !== 1 || auditorIds.length !== 1) {
setError('多选问题整改时只能选择 1 个填报人和 1 个复核人。');
return;
}
}
}
setActionLoading(true);
setActionLoading(true);
setError('');
setError('');
try {
try {
const fillerProfiles = await Promise.all(fillerIds.map((id) => loadUser(id)));
const currentUserInfo = JSON.parse(window.localStorage.getItem('current_user') || '{}') || {};
const currentUserInfo = JSON.parse(window.localStorage.getItem('current_user') || '{}') || {};
const selectedRecordIds = selectedRecords.map((item) => item.id);
const nextFillerIds = isDepartmentFlow
? Array.from(new Set(departmentAssignments.map((item) => item.editor?._id).filter(Boolean)))
: fillerIds;
const nextAuditorIds = isDepartmentFlow
? Array.from(new Set(departmentAssignments.map((item) => item.auditor?._id).filter(Boolean)))
: auditorIds;
const messageRecord = await createRecord({
const messageRecord = await createRecord({
subject: 'message_reference_id',
subject: 'message_reference_id',
title: `
推送问题整改表填写
$
{
dayjs
().
format
(
'YYYY-MM-DD HH:mm'
)}
`,
title: `
推送问题整改表填写
$
{
dayjs
().
format
(
'YYYY-MM-DD HH:mm'
)}
`,
metadata: {
metadata: {
sender: currentUserInfo?._id || currentUserInfo?.id || '',
sender: currentUserInfo?._id || currentUserInfo?.id || '',
editor:
f
illerIds,
editor:
nextF
illerIds,
auditor:
a
uditorIds,
auditor:
nextA
uditorIds,
restriction_key: '_id',
restriction_key: '_id',
restriction_value: selectedRecord
s.map((item) => item.id)
,
restriction_value: selectedRecord
Ids
,
reference_record_id: selectedRecord
s.map((item) => item.id)
,
reference_record_id: selectedRecord
Ids
,
view_name: activeGridView?.name || '',
view_name: activeGridView?.name || '',
subject_name: PROBLEM_RECTIFICATION_SUBJECT,
subject_name: PROBLEM_RECTIFICATION_SUBJECT,
editor_status: '未完成',
flow_mode: isDepartmentFlow ? FLOW_MODE_DEPARTMENT : FLOW_MODE_BATCH,
auditor_status: '未完成',
department_item_ids: isDepartmentFlow ? departmentAssignments.map((item) => item.item_id) : [],
sender_status: '未完成',
editor_status: EDITOR_STATUS_PENDING,
auditor_status: EDITOR_STATUS_PENDING,
sender_status: EDITOR_STATUS_PENDING,
},
},
});
});
const returnUrl = getProblemRectificationFlowTableUrl(messageRecord.name);
const progressIdEditor = [];
const progressIdEditor = [];
for (const fillerProfile of fillerProfiles) {
if (isDepartmentFlow) {
const departmentItems = buildDepartmentRectificationItems(
selectedRecords[0].raw || selectedRecords[0],
departmentAssignments,
currentUserInfo,
);
for (const item of departmentItems) {
const fillerProfile = item.editor ? await loadUser(item.editor) : null;
const oaProcessResult = await executeAction('oa_notice:createProgress', {
title: `
$
{
dayjs
().
format
(
'YYYY-MM-DD'
)}
填写问题整改表
-
$
{
item
.
responsible_department
}
`,
classify: '流程待办',
contentType: '流程待办',
receiveUserId: fillerProfile?._id || item.editor,
receiveUserName: fillerProfile?.username || item.editor,
receiveDisplayName: fillerProfile?.display_name || fillerProfile?.username || item.editor,
sendUserId: currentUserInfo?._id || currentUserInfo?.id || '',
sendUserName: currentUserInfo?.username || '',
sendDisplayName: currentUserInfo?.display_name || '',
returnUrl,
});
item.progress_id_editor = oaProcessResult?.data?.taskId || '';
progressIdEditor.push(item.progress_id_editor);
}
await batchUpdateRecords({
subject: PROBLEM_RECTIFICATION_SUBJECT,
filter: {
_id: selectedRecords[0].id,
},
data: buildDepartmentParentBatchUpdateData(departmentItems),
});
const savedRecord = await loadRecord(selectedRecords[0].id);
const savedItems = normalizeDepartmentRectificationItems(savedRecord);
if (savedItems.length !== departmentItems.length) {
throw new Error(
`
部门整改明细写入失败:预期
$
{
departmentItems
.
length
}
条,后台实际返回
$
{
savedItems
.
length
}
条。
`,
);
}
} else {
const fillerProfile = await loadUser(nextFillerIds[0]);
const oaProcessResult = await executeAction('oa_notice:createProgress', {
const oaProcessResult = await executeAction('oa_notice:createProgress', {
title: `
$
{
dayjs
().
format
(
'YYYY-MM-DD'
)}
填写问题整改表
`,
title: `
$
{
dayjs
().
format
(
'YYYY-MM-DD'
)}
填写问题整改表
`,
classify: '流程待办',
classify: '流程待办',
...
@@ -755,9 +1047,23 @@ export default function ProblemRectificationListPage() {
...
@@ -755,9 +1047,23 @@ export default function ProblemRectificationListPage() {
sendUserId: currentUserInfo?._id || currentUserInfo?.id || '',
sendUserId: currentUserInfo?._id || currentUserInfo?.id || '',
sendUserName: currentUserInfo?.username || '',
sendUserName: currentUserInfo?.username || '',
sendDisplayName: currentUserInfo?.display_name || '',
sendDisplayName: currentUserInfo?.display_name || '',
returnUrl
: `
$
{
window
.
location
.
origin
}
/problem_rectification_form
?
id=${messageRecord.name}`
,
returnUrl,
});
});
progressIdEditor.push(oaProcessResult?.data?.taskId);
progressIdEditor.push(oaProcessResult?.data?.taskId);
await batchUpdateRecords({
subject: PROBLEM_RECTIFICATION_SUBJECT,
filter: {
_id: { $in: selectedRecordIds },
},
data: {
'metadata.fill_in_the_people': nextFillerIds,
'metadata.review_people': nextAuditorIds,
'metadata.audit_status': AUDIT_STATUS_PENDING,
'metadata.final_audit_status': AUDIT_STATUS_PENDING,
[`
metadata
.
$
{
DEPARTMENT_RECTIFICATION_FIELD
}
`]: [],
},
});
}
}
await updateRecord(messageRecord._id || messageRecord.name, {
await updateRecord(messageRecord._id || messageRecord.name, {
...
@@ -766,22 +1072,12 @@ export default function ProblemRectificationListPage() {
...
@@ -766,22 +1072,12 @@ export default function ProblemRectificationListPage() {
},
},
});
});
await
batchUpdateRecords
({
subject
:
PROBLEM_RECTIFICATION_SUBJECT
,
filter
:
{
_id
:
{
$in
:
selectedRecords
.
map
((
item
)
=>
item
.
id
)
},
},
data
:
{
'metadata.fill_in_the_people'
:
fillerIds
,
},
});
const noticeResult = await createNotice({
const noticeResult = await createNotice({
title: '推送问题整改表填写',
title: '推送问题整改表填写',
content
:
`请点击链接填写问题整改表:
${
window
.
location
.
origin
}
/problem_rectification_form?id=
${
messageRecord
.
name
}
`
,
content: `
请点击链接填写问题整改表:
$
{
returnUrl
}
`,
type: 'internal',
type: 'internal',
from: currentUserInfo?._id || currentUserInfo?.id || '',
from: currentUserInfo?._id || currentUserInfo?.id || '',
to
:
f
illerIds
,
to:
nextF
illerIds,
settings: {},
settings: {},
});
});
...
@@ -790,8 +1086,14 @@ export default function ProblemRectificationListPage() {
...
@@ -790,8 +1086,14 @@ export default function ProblemRectificationListPage() {
setShowPushModal(false);
setShowPushModal(false);
setFillerUsers([]);
setFillerUsers([]);
setAuditorUsers([]);
setAuditorUsers([]);
setDepartmentAssignments([]);
setSelectedIds([]);
setSelectedIds([]);
await reloadList();
await reloadList();
setSuccessMessage(
isDepartmentFlow
? `
推送成功,已生成
$
{
departmentAssignments
.
length
}
条部门整改明细,可从记录标题右侧展开查看。
`
: `
推送成功,已将
$
{
selectedRecords
.
length
}
条问题发送给填写人。
`,
);
} catch (requestError) {
} catch (requestError) {
setError(requestError?.message || '推送问题整改表失败,请稍后重试。');
setError(requestError?.message || '推送问题整改表失败,请稍后重试。');
} finally {
} finally {
...
@@ -799,61 +1101,9 @@ export default function ProblemRectificationListPage() {
...
@@ -799,61 +1101,9 @@ export default function ProblemRectificationListPage() {
}
}
};
};
const
summaryItems
=
[
{
label
:
'当前视图记录数'
,
value
:
filteredRecords
.
length
,
hint
:
activeGridView
?.
title
?
`
${
activeGridView
.
title
}
视图`
:
'筛选结果'
,
tone
:
'rose'
,
},
{
label
:
'高严重程度'
,
value
:
highSeverityCount
,
hint
:
'问题综合严重程度 = 高'
,
tone
:
'rose'
,
},
{
label
:
'重点后果问题'
,
value
:
consequenceCount
,
hint
:
'含经济损失 / 外部影响 / 员工问责'
,
tone
:
'amber'
,
},
{
label
:
'往期或重复问题'
,
value
:
repeatedOrHistoricalCount
,
hint
:
'往期发现或再次出现的问题'
,
tone
:
'amber'
,
},
{
label
:
'已逾期未完成'
,
value
:
overdueCount
,
hint
:
'预计整改完成时间已到且未闭环'
,
tone
:
'rose'
,
},
{
label
:
'整改评级偏低'
,
value
:
lowRatingCount
,
hint
:
'综合整改评定等级 = 低'
,
tone
:
'rose'
,
},
{
label
:
'已整改 / 已优化'
,
value
:
fixedCount
,
hint
:
'按当前视图与筛选统计'
,
tone
:
'emerald'
,
},
{
label
:
'待复核项'
,
value
:
pendingAuditCount
,
hint
:
'审批状态未通过的记录'
,
tone
:
'amber'
,
},
];
return (
return (
<>
<>
<RecordWorkbench
<RecordWorkbench
summaryItems=
{
summaryItems
}
views={gridViews.map((view) => ({
views={gridViews.map((view) => ({
...view,
...view,
count: gridViewCounts[view.name] || 0,
count: gridViewCounts[view.name] || 0,
...
@@ -861,6 +1111,7 @@ export default function ProblemRectificationListPage() {
...
@@ -861,6 +1111,7 @@ export default function ProblemRectificationListPage() {
activeViewName={activeGridView?.name || ''}
activeViewName={activeGridView?.name || ''}
onViewChange={(viewName) => {
onViewChange={(viewName) => {
setActiveViewName(viewName);
setActiveViewName(viewName);
setPage(0);
setSelectedIds([]);
setSelectedIds([]);
}}
}}
filters={filters}
filters={filters}
...
@@ -931,8 +1182,13 @@ export default function ProblemRectificationListPage() {
...
@@ -931,8 +1182,13 @@ export default function ProblemRectificationListPage() {
options: configuredView.focusOptions,
options: configuredView.focusOptions,
},
},
]}
]}
onFiltersChange=
{
(
key
,
value
)
=>
setFilters
((
current
)
=>
({
...
current
,
[
key
]:
value
}))
}
onFiltersChange={(key, value) => {
setPage(0);
setSelectedIds([]);
setFilters((current) => ({ ...current, [key]: value }));
}}
onClearFilters={() => {
onClearFilters={() => {
setPage(0);
setFilters({
setFilters({
keyword: '',
keyword: '',
progress: '',
progress: '',
...
@@ -947,13 +1203,76 @@ export default function ProblemRectificationListPage() {
...
@@ -947,13 +1203,76 @@ export default function ProblemRectificationListPage() {
setSelectedIds([]);
setSelectedIds([]);
}}
}}
title="问题清单"
title="问题清单"
description=
{
`当前
视图显示 ${filteredRecords.length} 条记录,已选中 ${selectedIds.length} 条,其中重点后果问题 ${consequenceCount} 条、逾期未完成 ${overdueCount
} 条。`
}
description={`
当前
筛选共
$
{
totalMatchingRecords
}
条记录,本页显示
$
{
filteredRecords
.
length
}
条,已选中
$
{
selectedIds
.
length
}
条。
`}
columns={configuredView.columns || []}
columns={configuredView.columns || []}
columnGroups={activeGridView?.settings?.groups || []}
columnGroups={activeGridView?.settings?.groups || []}
records={filteredRecords}
records={filteredRecords}
selectedIds={selectedIds}
selectedIds={selectedIds}
onSelectionChange={setSelectedIds}
onSelectionChange={setSelectedIds}
selectable={quickSelectEnabled}
selectable={quickSelectEnabled}
showExpandColumn={false}
isRowExpandable={(record) => record.departmentItems?.length > 0}
renderExpandedRow={({ record }) => (
<div className="sticky left-4 w-[calc(100vw-8rem)] max-w-[1320px] overflow-hidden border-y border-rose-100 bg-rose-50/25">
<div className="flex items-center justify-between gap-4 border-b border-rose-100 px-5 py-3">
<div className="text-sm font-semibold text-slate-900">部门整改明细</div>
<div className="text-xs text-slate-500">共 {record.departmentItems.length} 条</div>
</div>
<div className="divide-y divide-rose-100/80">
{(record.departmentItems || []).map((item) => (
<div
key={item.item_id}
className="grid gap-x-5 gap-y-4 px-5 py-4 sm:grid-cols-2 lg:grid-cols-4 2xl:grid-cols-8"
>
<DepartmentDetailField label="责任部门">
{item.responsible_department || '-'}
</DepartmentDetailField>
<DepartmentDetailField label="填写人">
{departmentUserLabels[item.editor] || item.editor || '-'}
</DepartmentDetailField>
<DepartmentDetailField label="审批人">
{departmentUserLabels[item.auditor] || item.auditor || '-'}
</DepartmentDetailField>
<DepartmentDetailField label="整改进度">
<StatusPill
text={item.rectification_progress || '未填写'}
tone={resolveProgressTone(item.rectification_progress)}
/>
</DepartmentDetailField>
<DepartmentDetailField label="预计完成时间">
{item.the_time_of_rectification
? dayjs(item.the_time_of_rectification).format('YYYY-MM-DD')
: '-'}
</DepartmentDetailField>
<DepartmentDetailField label="审批状态">
<StatusPill
text={item.audit_status || '未审批'}
tone={resolveAuditTone(item.audit_status)}
/>
</DepartmentDetailField>
<DepartmentDetailField label="最终审批">
<StatusPill
text={item.final_audit_status || '未审批'}
tone={resolveAuditTone(item.final_audit_status)}
/>
</DepartmentDetailField>
<DepartmentDetailField label="整改附件">
{item.rectification_proof_materials?.url ? (
<a
href={item.rectification_proof_materials.url}
target="_blank"
rel="noreferrer"
className="font-medium text-sky-700 hover:text-sky-900"
>
{item.rectification_proof_materials.name || '查看附件'}
</a>
) : '未上传'}
</DepartmentDetailField>
</div>
))}
</div>
</div>
)}
toolbarActions={[
toolbarActions={[
{
{
key: 'create',
key: 'create',
...
@@ -1028,7 +1347,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1028,7 +1347,7 @@ export default function ProblemRectificationListPage() {
),
),
}}
}}
states={{
states={{
loading
,
loading
: loading || recordsLoading
,
error,
error,
success: successMessage,
success: successMessage,
loadingTitle: '正在加载问题整改列表',
loadingTitle: '正在加载问题整改列表',
...
@@ -1036,7 +1355,49 @@ export default function ProblemRectificationListPage() {
...
@@ -1036,7 +1355,49 @@ export default function ProblemRectificationListPage() {
emptyTitle: '当前视图暂无数据',
emptyTitle: '当前视图暂无数据',
emptyDescription: '可以切换其他视图,或调整严重程度、整改评级、主责部门和重点关注筛选。',
emptyDescription: '可以切换其他视图,或调整严重程度、整改评级、主责部门和重点关注筛选。',
}}
}}
renderCell=
{
({
record
,
column
})
=>
<
FieldValuePreview
record=
{
record
}
column=
{
column
}
/>
}
pagination={{
page,
pageSize,
totalRecords: totalMatchingRecords,
totalPages: pagination.totalPages,
pageSizeOptions: PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS,
onPageChange: (nextPage) => {
setPage(nextPage);
setSelectedIds([]);
},
onPageSizeChange: (nextPageSize) => {
setPageSize(nextPageSize);
setPage(0);
setSelectedIds([]);
},
}}
stickyTable
renderCell={({ record, column, expandable, expanded, toggleExpanded }) => {
const fieldPreview = <FieldValuePreview record={record} column={column} />;
const fieldName = getFieldNameFromPath(column.field);
if (fieldName !== 'title' || !expandable) {
return fieldPreview;
}
return (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
{fieldPreview}
<Button
plain
className="text-rose-700"
onClick={toggleExpanded}
aria-expanded={expanded}
>
<ChevronRightIcon
data-slot="icon"
className={clsx('transition-transform', expanded && 'rotate-90')}
/>
{expanded ? '收起明细' : `
$
{
record
.
departmentItems
.
length
}
条部门明细
`}
</Button>
</div>
);
}}
/>
/>
<Dialog open={showCreateModal} onClose={handleCloseCreateModal} size="5xl">
<Dialog open={showCreateModal} onClose={handleCloseCreateModal} size="5xl">
...
@@ -1102,31 +1463,83 @@ export default function ProblemRectificationListPage() {
...
@@ -1102,31 +1463,83 @@ export default function ProblemRectificationListPage() {
setShowPushModal(false);
setShowPushModal(false);
setFillerUsers([]);
setFillerUsers([]);
setAuditorUsers([]);
setAuditorUsers([]);
setDepartmentAssignments([]);
}}
}}
size=
"4xl"
size=
{isDepartmentPushMode ? '5xl' : '4xl'}
>
>
<DialogTitle>推送问题整改表</DialogTitle>
<DialogTitle>推送问题整改表</DialogTitle>
<DialogDescription>
<DialogDescription>
已从视图 <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-xs">{activeGridView?.title || activeGridView?.name || '-'}</code>{' '}
已从视图 <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-xs">{activeGridView?.title || activeGridView?.name || '-'}</code>{' '}
中选择
{
selectedRecords
.
length
}
条记录,请为本次流程配置填报人和复核人。
中选择 {selectedRecords.length} 条记录,
{isDepartmentPushMode ? '请按责任部门分别配置填报人和复核人。' : '多选推送只允许配置 1 个填报人和 1 个复核人。'}
</DialogDescription>
</DialogDescription>
<DialogBody>
<DialogBody>
<
div
className=
"grid gap-6 md:grid-cols-2"
>
{isDepartmentPushMode ? (
<
RemoteUserSelect
<div className="overflow-hidden rounded-[24px] border border-zinc-200 bg-white">
label=
"填报人"
<Table bleed dense className="[--gutter:--spacing(4)]">
value=
{
fillerUsers
}
<TableHead>
onChange=
{
setFillerUsers
}
<TableRow className="bg-zinc-50">
onSearch=
{
handleSearchUsers
}
<TableHeader>责任部门</TableHeader>
placeholder=
"搜索填报人用户名或显示名"
<TableHeader>填报人</TableHeader>
/>
<TableHeader>复核人</TableHeader>
<
RemoteUserSelect
</TableRow>
label=
"复核人"
</TableHead>
value=
{
auditorUsers
}
<TableBody>
onChange=
{
setAuditorUsers
}
{departmentAssignments.map((assignment) => (
onSearch=
{
handleSearchUsers
}
<TableRow key={assignment.item_id}>
placeholder=
"搜索复核人用户名或显示名"
<TableCell className="w-56 align-top">
/>
<div className="font-medium text-slate-900">{assignment.responsible_department}</div>
</
div
>
<div className="mt-1 text-xs text-slate-500">将生成 1 条部门明细</div>
</TableCell>
<TableCell className="min-w-72 align-top">
<RemoteUserSelect
label="填报人"
value={assignment.editor ? [assignment.editor] : []}
onChange={(users) =>
updateDepartmentAssignment(assignment.item_id, {
editor: users[users.length - 1] || null,
})
}
onSearch={handleSearchUsers}
placeholder="搜索填报人"
/>
</TableCell>
<TableCell className="min-w-72 align-top">
<RemoteUserSelect
label="复核人"
value={assignment.auditor ? [assignment.auditor] : []}
onChange={(users) =>
updateDepartmentAssignment(assignment.item_id, {
auditor: users[users.length - 1] || null,
})
}
onSearch={handleSearchUsers}
placeholder="搜索复核人"
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="grid gap-6 md:grid-cols-2">
<RemoteUserSelect
label="填报人"
value={fillerUsers}
onChange={(users) => setFillerUsers(users.slice(-1))}
onSearch={handleSearchUsers}
placeholder="搜索填报人用户名或显示名"
/>
<RemoteUserSelect
label="复核人"
value={auditorUsers}
onChange={(users) => setAuditorUsers(users.slice(-1))}
onSearch={handleSearchUsers}
placeholder="搜索复核人用户名或显示名"
/>
</div>
)}
<div className="mt-6 rounded-[24px] border border-zinc-200 bg-zinc-50/80 p-4">
<div className="mt-6 rounded-[24px] border border-zinc-200 bg-zinc-50/80 p-4">
<div className="text-sm font-medium text-slate-900">本次推送记录</div>
<div className="text-sm font-medium text-slate-900">本次推送记录</div>
...
@@ -1149,6 +1562,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1149,6 +1562,7 @@ export default function ProblemRectificationListPage() {
setShowPushModal(false);
setShowPushModal(false);
setFillerUsers([]);
setFillerUsers([]);
setAuditorUsers([]);
setAuditorUsers([]);
setDepartmentAssignments([]);
}}
}}
>
>
取消
取消
...
...
src/pages/problem_rectification/problem-rectification.business-flow.md
View file @
9b9085cc
...
@@ -73,6 +73,10 @@
...
@@ -73,6 +73,10 @@
-
`problem_rectification`
与
`issue_rectification_archive`
列表页统一复用
`RecordWorkbench`
作为“结构化表格 + 工具栏 + 详情抽屉入口”的工作台容器
-
`problem_rectification`
与
`issue_rectification_archive`
列表页统一复用
`RecordWorkbench`
作为“结构化表格 + 工具栏 + 详情抽屉入口”的工作台容器
-
页面本身只负责提供 subject 对应的数据加载、统计口径、筛选 schema、行操作与批量操作
-
页面本身只负责提供 subject 对应的数据加载、统计口径、筛选 schema、行操作与批量操作
-
`problem_rectification`
主列表采用传统分页表格,不一次性加载全部数据;每次按当前视图、筛选条件、关键词和页码调用
`/record/query`
,分页参数使用
`page`
与
`page_size`
-
主列表支持每页
`10 / 20 / 50 / 100`
条,切换视图、修改筛选或清空筛选时回到第 1 页;表头勾选只选择当前页记录,跨页批量处理需用户逐页确认选择范围
-
主列表视图页签数量通过
`/record/count`
按各视图限制和当前筛选条件计算,表格标题区展示服务端筛选后的总数与当前页记录数
-
主列表滚动到表格区域后,表格工作台吸顶展示;标题栏、操作区、列分组和分页固定在工作台内,数据表格区域独立滚动,表头列名在数据区域内继续吸顶,折叠 / 展开列分组时保留当前表格滚动位置
-
详情侧栏统一复用
`RecordDetailDrawer`
,通过
`recordName`
、
`subjectName`
、
`preferredViewName`
驱动,不再在各页面重复实现抽屉骨架
-
详情侧栏统一复用
`RecordDetailDrawer`
,通过
`recordName`
、
`subjectName`
、
`preferredViewName`
驱动,不再在各页面重复实现抽屉骨架
-
详情侧栏顶部不展示记录标题、状态和摘要卡片,只保留详情视图切换与全屏 / 关闭操作,字段主体作为主要阅读区域
-
详情侧栏顶部不展示记录标题、状态和摘要卡片,只保留详情视图切换与全屏 / 关闭操作,字段主体作为主要阅读区域
-
详情侧栏内
`reference`
类型字段只读态统一展示为关联明细表:按目标 Subject 的
`title`
与全部 metadata 字段展开,字段值按各自类型格式化;编辑态仍使用引用选择器,保存时只写入关联记录
`_id`
或
`_id[]`
-
详情侧栏内
`reference`
类型字段只读态统一展示为关联明细表:按目标 Subject 的
`title`
与全部 metadata 字段展开,字段值按各自类型格式化;编辑态仍使用引用选择器,保存时只写入关联记录
`_id`
或
`_id[]`
...
@@ -134,7 +138,7 @@
...
@@ -134,7 +138,7 @@
### 4.3 流程辅助对象
### 4.3 流程辅助对象
-
`message_reference_id`
:旧应用中用于承载“推送填写、复核、最终审核”状态的消息流程主单
-
`message_reference_id`
(信息关联id)
:旧应用中用于承载“推送填写、复核、最终审核”状态的消息流程主单
新项目如继续兼容旧链接
`/problem_rectification_form?id=...`
,则仍需读取该对象。
新项目如继续兼容旧链接
`/problem_rectification_form?id=...`
,则仍需读取该对象。
...
@@ -173,7 +177,7 @@
...
@@ -173,7 +177,7 @@
### 5.4 推送问题整改表
### 5.4 推送问题整改表
-
从已选问题记录中选择填报人和审核人
-
从已选问题记录中选择填报人和审核人
-
创建一条
`message_reference_id`
流程主单
-
创建一条
`message_reference_id`
(信息关联id)
流程主单
-
记录被推送问题的
`_id`
列表到
`restriction_value`
-
记录被推送问题的
`_id`
列表到
`restriction_value`
-
将选中问题的
`metadata.fill_in_the_people`
批量更新为填报人
-
将选中问题的
`metadata.fill_in_the_people`
批量更新为填报人
-
给填报人发送内部通知
-
给填报人发送内部通知
...
@@ -222,6 +226,7 @@
...
@@ -222,6 +226,7 @@
替代实现约定:
替代实现约定:
-
列表页保留表格浏览、搜索、筛选、批量选择
-
列表页保留表格浏览、搜索、筛选、批量选择
-
列表页采用分页加载,默认每页 20 条,可切换为 10、50、100;分页、筛选、视图切换均由服务端查询结果驱动
-
字段编辑统一进入右侧详情抽屉
-
字段编辑统一进入右侧详情抽屉
-
长文本、附件、状态类字段在详情抽屉中直接维护
-
长文本、附件、状态类字段在详情抽屉中直接维护
-
详情抽屉内字段顺序与显示权限跟随
`detail view`
配置
-
详情抽屉内字段顺序与显示权限跟随
`detail view`
配置
...
@@ -230,7 +235,7 @@
...
@@ -230,7 +235,7 @@
## 6. 流程角色
## 6. 流程角色
-
发起人:通常为问题整改列表操作人,负责发起填报和最终审核
-
发起人:通常为问题整改列表操作人,负责发起填报和最终审核
;可编辑本次推送关联的父问题记录以及全部部门整改明细业务字段
-
填报人:负责补充整改情况、附件、预计完成时间等
-
填报人:负责补充整改情况、附件、预计完成时间等
-
复核人:负责复核填报内容,决定通过或驳回
-
复核人:负责复核填报内容,决定通过或驳回
-
最终审核人:旧流程中通常仍由发起人承担最终审核
-
最终审核人:旧流程中通常仍由发起人承担最终审核
...
@@ -250,12 +255,12 @@
...
@@ -250,12 +255,12 @@
-
检查实施时间
-
检查实施时间
-
检查报告出具日
-
检查报告出具日
-
问题简述
-
问题简述
-
问题发现情况
-
责任部门
-
责任部门
-
问题来源
-
问题来源
-
问题类型
-
问题类型
-
反馈单位
-
反馈单位
-
问题涉及业务领域
-
问题涉及业务领域
-
若线上 Subject 配置存在
`problem_discovery_situation`
,可兼容展示“问题发现情况”,但当前主对象字段定义不再将其作为固定字段
3.
根据 2026 年整改驾驶舱要求,补录扩展治理字段:
3.
根据 2026 年整改驾驶舱要求,补录扩展治理字段:
-
问题后果:是否造成经济损失、对应经济损失、是否造成外部影响、处罚机构、是否涉及员工问责、问责人数
-
问题后果:是否造成经济损失、对应经济损失、是否造成外部影响、处罚机构、是否涉及员工问责、问责人数
-
问题画像:问题综合严重程度、问题一级至四级分类、问题原因
-
问题画像:问题综合严重程度、问题一级至四级分类、问题原因
...
@@ -267,18 +272,17 @@
...
@@ -267,18 +272,17 @@
推荐新交互:
推荐新交互:
-
工作台筛选区增加严重程度、整改评级、主责部门、问题来源、问题原因、重点关注等维度
-
工作台筛选区增加严重程度、整改评级、主责部门、问题来源、问题原因、重点关注等维度
-
列表标题列
增加风险标签,优先暴露“高严重程度”“重点后果问题”“已逾期”等重点状态
-
列表标题列
只显示标题文本,不在标题下方追加严重程度、整改评级、主责部门、逾期或重点后果标签;相关信息由对应业务列展示
-
新增弹窗除基础字段外,还应补充严重程度、分类、原因和整改质效字段,避免驾驶舱后续无数据可用
-
新增弹窗除基础字段外,还应补充严重程度、分类、原因和整改质效字段,避免驾驶舱后续无数据可用
-
摘要卡优先展示高严重程度、重点后果问题、往期/重复问题、逾期未完成、整改评级偏低等指标
-
新增弹窗中
`select`
且
`multiple=true`
的字段使用 Headless UI 下拉多选,不使用原生多行选择框;选项仍从 Subject
`settings.options`
读取,提交时保存选项值数组
### 7.2 发起整改填报
### 7.2 发起整改填报
1.
在问题整改列表中勾选一条或多条问题
1.
在问题整改列表中勾选一条或多条问题
2.
点击“推送问题整改表”
2.
点击“推送问题整改表”
3.
通过用户搜索选择:
3.
如果只选择 1 条问题,系统按该问题的
`metadata.responsible_department`
生成部门配置行;每个责任部门必须选择 1 个填写人和 1 个审批人
-
填报人
`fill_in_the_people`
4.
如果选择多条问题,不拆分责任部门;本次推送只允许选择 1 个填写人和 1 个审批人,并批量应用到选中问题
-
复核人
`review_people`
5.
创建
`message_reference_id`
(信息关联id)流程主单,写入:
4.
创建
`message_reference_id`
流程主单,写入:
-
`sender`
-
`sender`
-
`editor`
-
`editor`
-
`auditor`
-
`auditor`
...
@@ -287,83 +291,88 @@
...
@@ -287,83 +291,88 @@
-
`reference_record_id = 选中问题记录 ID 列表`
-
`reference_record_id = 选中问题记录 ID 列表`
-
`subject_name = "problem_rectification"`
-
`subject_name = "problem_rectification"`
-
`view_name = 当前筛选视图名`
-
`view_name = 当前筛选视图名`
-
`flow_mode = "department"`
或
`flow_mode = "batch"`
-
`department_item_ids = 本次部门明细 item_id 列表`
-
`editor_status = "未完成"`
-
`editor_status = "未完成"`
-
`auditor_status = "未完成"`
-
`auditor_status = "未完成"`
-
`sender_status = "未完成"`
-
`sender_status = "未完成"`
5.
批量更新选中问题的
`metadata.fill_in_the_people`
6.
单条部门推送时,回写该问题的
`metadata.department_rectification_items`
;每个部门明细独立保存责任部门、填写人、审批人、整改字段、审批状态和待办 ID
6.
发送内部通知与 OA 待办
-
写入后重新读取父问题并校验部门明细数量;后台未返回预期明细时,本次推送显示失败,不展示成功提示
7.
多选简单推送时,批量更新选中问题的
`metadata.fill_in_the_people`
、
`metadata.review_people`
8.
发送内部通知与 OA 待办,链接指向
`/problem_rectification_new/problem_rectification_flow_table?id=<message_record_name>`
推荐新交互:
推荐新交互:
-
顶部批量操作栏出现“推送整改表”
-
顶部批量操作栏出现“推送整改表”
-
点击后打开
“选择填报人 / 复核人”弹窗
-
点击后打开
推送弹窗;单条问题展示部门配置表格,多选问题展示单填写人 / 单复核人配置
-
弹窗底部明确展示本次选中记录数量
-
弹窗底部明确展示本次选中记录数量
和生成的部门明细数量
-
成功后列表即时刷新,并给出已推送结果提示
-
成功后列表即时刷新,并给出已推送结果提示
### 7.3 填报人补录整改内容
### 7.3 填报人补录整改内容
1.
填报人通过消息链接进入
整改填写页
1.
填报人通过消息链接进入
新表格处理页
`/problem_rectification_new/problem_rectification_flow_table`
2.
页面
只展示本次流程绑定的记录范围
2.
页面
展示本次流程绑定的所有部门明细行;参与人可查看全部行,填写人只能编辑自己负责的行,流程推送人可以编辑全部部门明细行。表格同时只读展示父问题的
`problem_brief`
(问题简述)和
`related_issue_domain`
(涉及业务领域);填写人和审批人字段保存用户
`_id`
,表格展示用户姓名,姓名查询不到时才回退显示 ID
3.
填报人可编辑:
3.
填报人可
在表格行内
编辑:
-
`rectification_progress`
-
`rectification_progress`
-
`rectification_situation`
-
`rectification_situation`
-
`the_time_of_rectification`
-
`the_time_of_rectification`
-
`remark`
-
`rectification_proof_materials`
-
`rectification_proof_materials`
4.
如有新增问题,也可能被补充进本次流程绑定范围
4.
填写人送审后,该行填写业务字段锁定;只有审批驳回后,填写人恢复编辑
5.
填写完成后点击“送审”
5.
审批驳回后,原审批人也可以编辑该行整改业务字段,但不能代替填写人点击“送审”;待填写人重新送审后,审批人恢复审批操作
6.
流程推送人可以从问题列打开父问题详情抽屉,编辑本次推送关联的
`problem_rectification`
主记录;填写人和审批人只能查看父问题记录。部门流程的父级汇总字段、部门明细对象、审批状态和流程人员字段不在详情抽屉中直接编辑
推荐新交互:
推荐新交互:
-
页面顶部显示本次流程步骤和当前身份
-
页面顶部显示本次流程步骤和当前身份
-
主体使用结构化表格 + 详情抽屉,不直接暴露底层 JSON
-
主体统一使用表格,不再使用旧填写页的详情式编辑
-
长文本、附件、备注在详情抽屉内集中编辑
-
长文本、附件、预计完成时间和整改进度均在表格行内维护
-
“送审”按钮固定在页面顶部操作区,避免滚动后找不到
-
“保存”“送审”“审批通过”“驳回”“最终通过”“最终驳回”作为行级操作展示
-
推送人具有部门明细“保存”权限,但不会因此获得填写人的“送审”权限;推送人同时是该行填写人时,才按填写人身份送审
### 7.4 复核人审核
### 7.4 复核人审核
送审后,系统应:
部门明细
送审后,系统应:
1.
将
`message_reference_id.metadata.
editor_status`
更新为
`已完成`
1.
将
该部门明细的
`
editor_status`
更新为
`已完成`
2.
创建
复核
人的 OA 待办
2.
创建
该部门审批
人的 OA 待办
3.
给
复核
人发送通知
3.
给
该部门审批
人发送通知
复核人进入
页面后
有两个结果:
复核人进入
新表格处理页后,对自己负责的部门明细
有两个结果:
1.
一键复核通过
1.
审批通过
-
批量更新所有关联问题
`metadata.audit_status = "已审批"`
-
更新该部门明细
`audit_status = "已审批"`
-
回写
`metadata.review_people`
-
立即创建该部门明细的最终审批待办给发起人,不等待其他部门审批完成
-
创建最终审核待办给发起人
-
写入该部门明细
`progress_id_sender`
-
更新流程主单
`auditor_status = "已完成"`
、
`sender_status = "未完成"`
2.
驳回
2.
驳回
-
批量更新所有关联问题
`metadata.audit_status = "已驳回"`
-
更新该部门明细
`audit_status = "已驳回"`
-
重新给填报人发通知和待办
-
将该部门明细
`editor_status`
重置为
`未完成`
-
更新流程主单
`editor_status = "未完成"`
、
`auditor_status = "已完成"`
-
允许该审批人编辑该部门明细整改业务字段;填写人同步恢复编辑,审批人不获得“送审”权限
-
重新写入
`progress_id_editor`
,使流程回到填写阶段
-
重新给该部门填写人发通知和待办
复核人审批通过后,该部门明细业务字段锁定;只有审批驳回状态下,复核人拥有该行“保存”权限。填写人重新送审后,复核人的编辑权限随行重新进入审批阶段而关闭。
推荐新交互:
推荐新交互:
-
顶部提供“复核通过”“驳回”两个主
操作
-
表格行内提供“审批通过”“驳回”两个
操作
-
驳回时建议补充驳回原因输入框,为后续完善流程留接口
-
驳回时建议补充驳回原因输入框,为后续完善流程留接口
-
审核操作前显示
本次关联问题数量和当前填报人 / 复核人信息
-
审核操作前显示
当前行的责任部门、填写人、审批人和整改材料
### 7.5 发起人最终审核
### 7.5 发起人最终审核
最终审核人进入
页面后也有两个结果
:
最终审核人进入
新表格处理页后,按部门明细逐行最终审批
:
1.
一键最终审核通过
1.
最终审核通过
-
批量更新所有关联问题
`metadata.final_audit_status = "已审批"`
-
更新该部门明细
`final_audit_status = "已审批"`
-
更新流程主单
`sender_status = "已完成"`
2.
最终审核驳回
2.
驳回审批
-
更新该部门明细
`final_audit_status = "已驳回"`
-
批量更新所有关联问题
`metadata.final_audit_status = "已驳回"`
-
将该部门明细
`editor_status`
重置为
`未完成`
-
重建填写人待办
-
重新通知该部门填写人处理
-
更新流程主单
`editor_status = "未完成"`
、
`sender_status = "已完成"`
-
重新写入
`progress_id_editor`
,使流程重新回到填写阶段
推荐新交互:
推荐新交互:
-
页面头部展示“填写完成 / 复核完成 / 最终审核”三段式状态
-
表格中分开展示“审批状态”和“最终审批状态”
-
发起人在最终审核时只需要看到汇总结果和关键问题,不必再回到多维表格编辑体验
-
最终审批行级处理,不要求本次推送所有部门都先审批通过
-
发起人编辑已经送审或审批完成的父问题、部门整改内容时,不自动重置审批状态,也不自动重新生成 OA 待办
---
---
...
@@ -388,11 +397,13 @@
...
@@ -388,11 +397,13 @@
### 8.2 流程主单状态字段
### 8.2 流程主单状态字段
`message_reference_id`
在当前项目中的约定:
`message_reference_id`
(信息关联id)
在当前项目中的约定:
-
`sender`
记录推送发起人
-
`sender`
记录推送发起人
-
`editor`
记录填写人列表
-
`editor`
记录填写人列表
-
`auditor`
记录审核人列表
-
`auditor`
记录审核人列表
-
`flow_mode`
记录流程模式:
`department`
表示单条问题按责任部门拆分明细,
`batch`
表示多选问题的单填写人 / 单审批人简单推送
-
`department_item_ids`
记录本次部门流程关联的
`department_rectification_items[].item_id`
-
`restriction_key`
默认固定为
`_id`
-
`restriction_key`
默认固定为
`_id`
-
`restriction_value`
保存本次推送绑定的整改记录
`_id[]`
-
`restriction_value`
保存本次推送绑定的整改记录
`_id[]`
-
`reference_record_id`
作为关联记录 ID 冗余字段,和
`restriction_value`
一致
-
`reference_record_id`
作为关联记录 ID 冗余字段,和
`restriction_value`
一致
...
@@ -402,7 +413,19 @@
...
@@ -402,7 +413,19 @@
-
`auditor_status`
:
`未完成`
、
`已完成`
-
`auditor_status`
:
`未完成`
、
`已完成`
-
`sender_status`
:
`未完成`
、
`已完成`
-
`sender_status`
:
`未完成`
、
`已完成`
当前新页面不再依赖旧版
`未审批`
、
`已驳回`
之类流程主单状态,而是通过“状态字段 + 当前 progress_id”共同判断流程走向。驳回场景通过重建
`progress_id_editor`
并将
`editor_status`
重置为
`未完成`
来回到填写阶段。
当前新页面不再依赖旧版
`未审批`
、
`已驳回`
之类流程主单状态,而是通过“状态字段 + 当前 progress_id”共同判断流程走向。新部门流程中,部门明细上的
`editor_status`
、
`audit_status`
、
`final_audit_status`
是事实来源;流程主单状态仅用于兼容和整体提示。驳回场景只重置对应部门明细,不影响其他部门明细。
### 8.3 部门明细汇总约定
`department_rectification_items`
存放在父问题
`metadata`
中:
-
问题整改主列表父行仍显示一条问题
-
父行的整改进度、审批状态、最终审批状态按部门明细汇总;全部一致则显示该状态,不一致则显示“部分整改”或“部分审批”
-
父行可展开显示部门明细,明细中分开展示责任部门、填写人、审批人、整改进度、预计完成时间、审批状态、最终审批状态和附件状态
-
存在部门明细的父行在标题右侧显示“N 条部门明细”入口;单部门推送同样生成 1 条明细。展开区使用贴合当前视口的响应式扁平信息网格,不嵌套横向子表格,并将填写人、审批人 ID 转为用户显示名
-
转发后的整改填写、审批和最终审批均优先更新部门明细,父字段仅做兼容汇总展示
-
新流程页中,填写人只能维护本人负责行的整改业务字段,送审后锁定,复核驳回后恢复;复核驳回状态下对应审批人可以维护该行整改业务字段,审批通过后锁定,且审批人不能点击填写人的“送审”;流程推送人可以维护本次推送关联的父问题记录和全部部门明细整改业务字段,但不能直接编辑流程状态与待办 ID
-
推送人的内容修改保持当前审批状态不变,不触发重新送审
---
---
...
@@ -595,7 +618,7 @@
...
@@ -595,7 +618,7 @@
-
基础信息
-
基础信息
`project_name`
、
`inspect_the_implementing_unit`
、
`check_the_implementation_time`
、
`inspection_report_date`
`project_name`
、
`inspect_the_implementing_unit`
、
`check_the_implementation_time`
、
`inspection_report_date`
-
问题信息
-
问题信息
`problem_brief`
、
`problem_
discovery_situation`
、
`problem_
from`
、
`problem_type`
、
`related_issue_domain`
`problem_brief`
、
`problem_from`
、
`problem_type`
、
`related_issue_domain`
-
责任与流转
-
责任与流转
`responsible_department`
、
`fill_in_the_people`
、
`review_people`
、
`audit_status`
、
`final_audit_status`
`responsible_department`
、
`fill_in_the_people`
、
`review_people`
、
`audit_status`
、
`final_audit_status`
-
整改信息
-
整改信息
...
@@ -771,7 +794,7 @@ const result = await queryUsers(0, 20, {
...
@@ -771,7 +794,7 @@ const result = await queryUsers(0, 20, {
-
功能入口
-
功能入口
-
关键交互步骤
-
关键交互步骤
-
推送、填报、复核、最终审核流程
-
推送、填报、复核、最终审核流程
-
`message_reference_id`
的当前字段协议与回流规则
-
`message_reference_id`
(信息关联id)
的当前字段协议与回流规则
-
驾驶舱统计口径
-
驾驶舱统计口径
-
归档流程
-
归档流程
-
状态变化与数据约束
-
状态变化与数据约束
...
...
src/pages/problem_rectification/problemRectificationFormService.js
View file @
9b9085cc
...
@@ -10,7 +10,10 @@ import {
...
@@ -10,7 +10,10 @@ import {
sendNotice
,
sendNotice
,
updateRecord
,
updateRecord
,
}
from
'@/services/DataService'
;
}
from
'@/services/DataService'
;
import
{
normalizeProblemRectificationRecord
}
from
'./problemRectificationListService'
;
import
{
getProblemRectificationFormUrl
,
normalizeProblemRectificationRecord
,
}
from
'./problemRectificationListService'
;
import
{
normalizeText
,
toArray
}
from
'@/utils/subjectView'
;
import
{
normalizeText
,
toArray
}
from
'@/utils/subjectView'
;
const
MESSAGE_REFERENCE_SUBJECT
=
'message_reference_id'
;
const
MESSAGE_REFERENCE_SUBJECT
=
'message_reference_id'
;
...
@@ -147,11 +150,7 @@ async function sendFlowNotice({ title, content, fromUserId, toUserIds = [] }) {
...
@@ -147,11 +150,7 @@ async function sendFlowNotice({ title, content, fromUserId, toUserIds = [] }) {
}
}
function
buildReturnUrl
(
messageName
)
{
function
buildReturnUrl
(
messageName
)
{
if
(
typeof
window
===
'undefined'
)
{
return
getProblemRectificationFormUrl
(
messageName
);
return
`/problem_rectification_form?id=
${
messageName
}
`
;
}
return
`
${
window
.
location
.
origin
}
/problem_rectification_form?id=
${
messageName
}
`
;
}
}
function
resolveCurrentStep
(
messageRecord
)
{
function
resolveCurrentStep
(
messageRecord
)
{
...
...
src/pages/problem_rectification/problemRectificationListService.js
View file @
9b9085cc
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
import
{
import
{
countRecords
,
getSubjectView
,
getSubjectView
,
loadSubject
,
loadSubject
,
queryRecords
,
queryRecords
,
...
@@ -12,6 +13,7 @@ import {
...
@@ -12,6 +13,7 @@ import {
formatFieldValue
,
formatFieldValue
,
getFieldNameFromPath
,
getFieldNameFromPath
,
getFieldValue
,
getFieldValue
,
getSelectOptions
,
isViewAccessible
,
isViewAccessible
,
normalizeValueForSubmit
,
normalizeValueForSubmit
,
normalizeText
,
normalizeText
,
...
@@ -20,8 +22,29 @@ import {
...
@@ -20,8 +22,29 @@ import {
}
from
'@/utils/subjectView'
;
}
from
'@/utils/subjectView'
;
export
const
PROBLEM_RECTIFICATION_SUBJECT
=
'problem_rectification'
;
export
const
PROBLEM_RECTIFICATION_SUBJECT
=
'problem_rectification'
;
export
const
PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS
=
[
10
,
20
,
50
,
100
];
export
const
DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE
=
20
;
export
const
DEPARTMENT_RECTIFICATION_FIELD
=
'department_rectification_items'
;
export
const
FLOW_MODE_DEPARTMENT
=
'department'
;
export
const
FLOW_MODE_BATCH
=
'batch'
;
export
const
PROBLEM_RECTIFICATION_FLOW_TABLE_PATH
=
'/problem_rectification_flow_table'
;
export
const
EDITOR_STATUS_PENDING
=
'未完成'
;
export
const
EDITOR_STATUS_COMPLETED
=
'已完成'
;
export
const
AUDIT_STATUS_PENDING
=
'未审批'
;
export
const
AUDIT_STATUS_APPROVED
=
'已审批'
;
export
const
AUDIT_STATUS_REJECTED
=
'已驳回'
;
const
FIXED_PROGRESS_VALUES
=
new
Set
([
'已整改'
,
'已优化'
]);
const
FIXED_PROGRESS_VALUES
=
new
Set
([
'已整改'
,
'已优化'
]);
const
LEVEL_ORDER
=
[
'高'
,
'中'
,
'低'
];
const
LEVEL_ORDER
=
[
'高'
,
'中'
,
'低'
];
const
FILTER_FIELD_PATHS
=
{
progress
:
'metadata.rectification_progress'
,
auditStatus
:
'metadata.audit_status'
,
severityLevel
:
'metadata.problem_severity_level'
,
overallRating
:
'metadata.rectification_overall_rating'
,
primaryResponsibleDepartment
:
'metadata.primary_responsible_department'
,
problemFrom
:
'metadata.problem_from'
,
rootCause
:
'metadata.problem_root_cause'
,
};
const
AFFIRMATIVE_FILTER_VALUES
=
[
'是'
,
'有'
,
'涉及'
,
'true'
,
'1'
,
'Y'
,
'YES'
,
true
];
const
WORKBENCH_PRIORITY_CREATE_FIELD_NAMES
=
[
const
WORKBENCH_PRIORITY_CREATE_FIELD_NAMES
=
[
'problem_severity_level'
,
'problem_severity_level'
,
'problem_category_level_1'
,
'problem_category_level_1'
,
...
@@ -84,6 +107,190 @@ function normalizeUserLabel(value) {
...
@@ -84,6 +107,190 @@ function normalizeUserLabel(value) {
);
);
}
}
function
normalizeUserId
(
value
)
{
if
(
!
value
)
{
return
''
;
}
if
(
typeof
value
===
'string'
)
{
return
value
.
trim
();
}
return
String
(
value
.
_id
||
value
.
id
||
value
.
name
||
''
).
trim
();
}
function
uniqueTruthy
(
values
=
[])
{
return
Array
.
from
(
new
Set
(
values
.
map
((
item
)
=>
normalizeText
(
item
)).
filter
(
Boolean
)));
}
function
makeDepartmentItemId
(
record
,
department
,
index
)
{
const
recordId
=
normalizeText
(
record
?.
_id
||
record
?.
name
||
record
?.
id
||
'record'
);
const
departmentSlug
=
normalizeText
(
department
)
||
`department-
${
index
+
1
}
`
;
const
randomPart
=
Math
.
random
().
toString
(
36
).
slice
(
2
,
8
);
return
`dept_
${
recordId
}
_
${
index
+
1
}
_
${
departmentSlug
}
_
${
Date
.
now
()}
_
${
randomPart
}
`
;
}
function
summarizeTextStatus
(
values
=
[],
partialText
=
'部分审批'
,
emptyText
=
''
)
{
const
normalizedValues
=
uniqueTruthy
(
values
);
if
(
normalizedValues
.
length
===
0
)
{
return
emptyText
;
}
if
(
normalizedValues
.
length
===
1
)
{
return
normalizedValues
[
0
];
}
return
partialText
;
}
function
summarizeDate
(
values
=
[])
{
const
normalizedValues
=
uniqueTruthy
(
values
);
return
normalizedValues
.
length
===
1
?
normalizedValues
[
0
]
:
null
;
}
export
function
getProblemRectificationFlowTableUrl
(
messageName
)
{
const
query
=
messageName
?
`?id=
${
encodeURIComponent
(
messageName
)}
`
:
''
;
const
basePath
=
String
(
PROBLEM_RECTIFICATION_APP_BASE_PATH
||
'/'
)
.
replace
(
/
\/
+$/
,
''
);
const
appPath
=
`
${
basePath
}${
PROBLEM_RECTIFICATION_FLOW_TABLE_PATH
}${
query
}
`
;
if
(
typeof
window
===
'undefined'
)
{
return
appPath
;
}
return
`
${
window
.
location
.
origin
}${
appPath
}
`
;
}
export
function
getProblemRectificationFormUrl
(
messageName
)
{
const
query
=
messageName
?
`?id=
${
encodeURIComponent
(
messageName
)}
`
:
''
;
const
basePath
=
String
(
PROBLEM_RECTIFICATION_APP_BASE_PATH
||
'/'
)
.
replace
(
/
\/
+$/
,
''
);
const
appPath
=
`
${
basePath
}
/problem_rectification_form
${
query
}
`
;
if
(
typeof
window
===
'undefined'
)
{
return
appPath
;
}
return
`
${
window
.
location
.
origin
}${
appPath
}
`
;
}
export
function
normalizeDepartmentRectificationItems
(
record
)
{
const
metadata
=
record
?.
metadata
||
record
?.
raw
?.
metadata
||
{};
const
rawItems
=
metadata
[
DEPARTMENT_RECTIFICATION_FIELD
];
let
parsedItems
=
rawItems
;
if
(
typeof
rawItems
===
'string'
)
{
try
{
parsedItems
=
JSON
.
parse
(
rawItems
);
}
catch
(
error
)
{
parsedItems
=
[];
}
}
return
toArray
(
parsedItems
)
.
filter
((
item
)
=>
item
&&
typeof
item
===
'object'
)
.
map
((
item
,
index
)
=>
({
item_id
:
normalizeText
(
item
.
item_id
)
||
makeDepartmentItemId
(
record
,
item
.
responsible_department
,
index
),
responsible_department
:
normalizeText
(
item
.
responsible_department
),
editor
:
normalizeUserId
(
item
.
editor
),
auditor
:
normalizeUserId
(
item
.
auditor
),
rectification_progress
:
normalizeText
(
item
.
rectification_progress
),
rectification_situation
:
normalizeText
(
item
.
rectification_situation
),
the_time_of_rectification
:
item
.
the_time_of_rectification
||
null
,
rectification_proof_materials
:
item
.
rectification_proof_materials
||
null
,
editor_status
:
normalizeText
(
item
.
editor_status
)
||
EDITOR_STATUS_PENDING
,
audit_status
:
normalizeText
(
item
.
audit_status
)
||
AUDIT_STATUS_PENDING
,
final_audit_status
:
normalizeText
(
item
.
final_audit_status
)
||
AUDIT_STATUS_PENDING
,
progress_id_editor
:
item
.
progress_id_editor
||
''
,
progress_id_auditor
:
item
.
progress_id_auditor
||
''
,
progress_id_sender
:
item
.
progress_id_sender
||
''
,
updated_by
:
item
.
updated_by
||
''
,
updated_at
:
item
.
updated_at
||
''
,
created_by
:
item
.
created_by
||
''
,
created_at
:
item
.
created_at
||
''
,
}));
}
export
function
buildDepartmentAssignmentsFromRecord
(
record
)
{
const
metadata
=
record
?.
metadata
||
record
?.
raw
?.
metadata
||
{};
const
departments
=
uniqueTruthy
(
toArray
(
metadata
.
responsible_department
).
length
>
0
?
toArray
(
metadata
.
responsible_department
)
:
[
metadata
.
primary_responsible_department
||
'未指定责任部门'
],
);
return
(
departments
.
length
>
0
?
departments
:
[
'未指定责任部门'
]).
map
((
department
,
index
)
=>
({
item_id
:
makeDepartmentItemId
(
record
?.
raw
||
record
,
department
,
index
),
responsible_department
:
department
,
editor
:
null
,
auditor
:
null
,
}));
}
export
function
buildDepartmentRectificationItems
(
record
,
assignments
=
[],
currentUser
=
null
)
{
const
now
=
dayjs
().
toISOString
();
const
currentUserId
=
normalizeUserId
(
currentUser
);
return
assignments
.
map
((
assignment
,
index
)
=>
({
item_id
:
assignment
.
item_id
||
makeDepartmentItemId
(
record
?.
raw
||
record
,
assignment
.
responsible_department
,
index
),
responsible_department
:
normalizeText
(
assignment
.
responsible_department
)
||
'未指定责任部门'
,
editor
:
normalizeUserId
(
assignment
.
editor
),
auditor
:
normalizeUserId
(
assignment
.
auditor
),
rectification_progress
:
''
,
rectification_situation
:
''
,
the_time_of_rectification
:
null
,
rectification_proof_materials
:
null
,
editor_status
:
EDITOR_STATUS_PENDING
,
audit_status
:
AUDIT_STATUS_PENDING
,
final_audit_status
:
AUDIT_STATUS_PENDING
,
progress_id_editor
:
''
,
progress_id_auditor
:
''
,
progress_id_sender
:
''
,
created_by
:
currentUserId
,
created_at
:
now
,
updated_by
:
currentUserId
,
updated_at
:
now
,
}));
}
export
function
buildDepartmentParentMetadataPatch
(
items
=
[])
{
const
normalizedItems
=
items
.
filter
(
Boolean
);
const
editorIds
=
uniqueTruthy
(
normalizedItems
.
map
((
item
)
=>
item
.
editor
));
const
auditorIds
=
uniqueTruthy
(
normalizedItems
.
map
((
item
)
=>
item
.
auditor
));
return
{
[
DEPARTMENT_RECTIFICATION_FIELD
]:
normalizedItems
,
fill_in_the_people
:
editorIds
,
review_people
:
auditorIds
,
rectification_progress
:
summarizeTextStatus
(
normalizedItems
.
map
((
item
)
=>
item
.
rectification_progress
),
'部分整改'
,
''
,
),
audit_status
:
summarizeTextStatus
(
normalizedItems
.
map
((
item
)
=>
item
.
audit_status
),
'部分审批'
,
AUDIT_STATUS_PENDING
,
),
final_audit_status
:
summarizeTextStatus
(
normalizedItems
.
map
((
item
)
=>
item
.
final_audit_status
),
'部分审批'
,
AUDIT_STATUS_PENDING
,
),
the_time_of_rectification
:
summarizeDate
(
normalizedItems
.
map
((
item
)
=>
item
.
the_time_of_rectification
),
),
};
}
export
function
buildDepartmentParentBatchUpdateData
(
items
=
[])
{
return
Object
.
fromEntries
(
Object
.
entries
(
buildDepartmentParentMetadataPatch
(
items
)).
map
(([
fieldName
,
value
])
=>
[
`metadata.
${
fieldName
}
`
,
value
,
]),
);
}
function
normalizeFlagText
(
value
)
{
function
normalizeFlagText
(
value
)
{
const
text
=
normalizeText
(
value
);
const
text
=
normalizeText
(
value
);
return
text
||
'否'
;
return
text
||
'否'
;
...
@@ -132,12 +339,227 @@ function findSubjectField(subject, fieldName) {
...
@@ -132,12 +339,227 @@ function findSubjectField(subject, fieldName) {
return
(
subject
?.
fields
||
[]).
find
((
field
)
=>
field
?.
name
===
fieldName
)
||
null
;
return
(
subject
?.
fields
||
[]).
find
((
field
)
=>
field
?.
name
===
fieldName
)
||
null
;
}
}
function
isEmptyFilterValue
(
value
)
{
if
(
value
===
undefined
||
value
===
null
||
value
===
''
)
{
return
true
;
}
return
Array
.
isArray
(
value
)
&&
value
.
length
===
0
;
}
function
compactFilterClauses
(
clauses
=
[])
{
return
clauses
.
filter
((
clause
)
=>
clause
&&
Object
.
keys
(
clause
).
length
>
0
);
}
function
combineFilterClauses
(
clauses
=
[])
{
const
nextClauses
=
compactFilterClauses
(
clauses
);
if
(
nextClauses
.
length
===
0
)
{
return
{};
}
if
(
nextClauses
.
length
===
1
)
{
return
nextClauses
[
0
];
}
return
{
$and
:
nextClauses
};
}
function
buildComparatorFilter
(
field
,
comparator
=
'$eq'
,
value
)
{
if
(
!
field
)
{
return
null
;
}
if
(
comparator
===
'$eq'
)
{
return
{
[
field
]:
value
};
}
if
(
comparator
===
'$contains'
)
{
return
{
[
field
]:
{
$regex
:
normalizeText
(
value
),
$options
:
'i'
}
};
}
if
(
comparator
===
'$exists'
)
{
return
{
[
field
]:
{
$exists
:
Boolean
(
value
)
}
};
}
return
{
[
field
]:
{
[
comparator
]:
value
}
};
}
function
buildRestrictionsFilter
(
restrictions
=
[])
{
const
clauses
=
toArray
(
restrictions
)
.
filter
((
restriction
)
=>
restriction
?.
type
===
'field'
&&
restriction
.
field
)
.
map
((
restriction
)
=>
buildComparatorFilter
(
restriction
.
field
,
restriction
.
comparator
||
'$in'
,
restriction
.
comparator
===
'$in'
||
restriction
.
comparator
===
'$nin'
?
toArray
(
restriction
.
value
)
:
restriction
.
value
,
),
);
return
combineFilterClauses
(
clauses
);
}
function
buildViewConditionsFilter
(
filters
=
{})
{
const
rules
=
Array
.
isArray
(
filters
?.
conditions
)
?
filters
.
conditions
:
[];
const
clauses
=
rules
.
filter
((
rule
)
=>
rule
?.
field
)
.
map
((
rule
)
=>
buildComparatorFilter
(
rule
.
field
,
rule
.
comparator
||
'$eq'
,
rule
.
comparator
===
'$in'
||
rule
.
comparator
===
'$nin'
?
toArray
(
rule
.
value
)
:
rule
.
value
,
),
);
const
nextClauses
=
compactFilterClauses
(
clauses
);
if
(
nextClauses
.
length
===
0
)
{
return
{};
}
if
(
filters
?.
relationship
===
'any'
||
filters
?.
relationship
===
'or'
)
{
return
{
$or
:
nextClauses
};
}
return
combineFilterClauses
(
nextClauses
);
}
function
buildKeywordFilter
(
keyword
,
columns
=
[])
{
const
text
=
normalizeText
(
keyword
);
if
(
!
text
)
{
return
{};
}
const
fields
=
Array
.
from
(
new
Set
([
'title'
,
...
columns
.
map
((
column
)
=>
column
.
field
)
.
filter
((
field
)
=>
field
&&
field
!==
'title'
),
]),
);
return
{
$or
:
fields
.
map
((
field
)
=>
({
[
field
]:
{
$regex
:
text
,
$options
:
'i'
},
})),
};
}
function
buildFocusFlagFilter
(
focusFlag
)
{
if
(
focusFlag
===
'consequence'
)
{
return
{
$or
:
[
{
'metadata.has_economic_loss'
:
{
$in
:
AFFIRMATIVE_FILTER_VALUES
}
},
{
'metadata.has_external_impact'
:
{
$in
:
AFFIRMATIVE_FILTER_VALUES
}
},
{
'metadata.has_employee_accountability'
:
{
$in
:
AFFIRMATIVE_FILTER_VALUES
}
},
],
};
}
if
(
focusFlag
===
'high_severity'
)
{
return
{
'metadata.problem_severity_level'
:
'高'
};
}
if
(
focusFlag
===
'low_rating'
)
{
return
{
'metadata.rectification_overall_rating'
:
'低'
};
}
if
(
focusFlag
===
'repeated_or_historical'
)
{
return
{
$or
:
[
{
'metadata.is_historical_problem'
:
{
$in
:
AFFIRMATIVE_FILTER_VALUES
}
},
{
'metadata.is_repeated_problem'
:
{
$in
:
AFFIRMATIVE_FILTER_VALUES
}
},
],
};
}
if
(
focusFlag
===
'overdue'
)
{
return
{
'metadata.the_time_of_rectification'
:
{
$lt
:
dayjs
().
startOf
(
'day'
).
toISOString
()
},
'metadata.rectification_progress'
:
{
$nin
:
Array
.
from
(
FIXED_PROGRESS_VALUES
)
},
};
}
if
(
focusFlag
===
'policy_or_system'
)
{
return
{
$or
:
[
{
'metadata.involves_policy_revision'
:
{
$in
:
AFFIRMATIVE_FILTER_VALUES
}
},
{
'metadata.involves_system_construction'
:
{
$in
:
AFFIRMATIVE_FILTER_VALUES
}
},
],
};
}
return
{};
}
export
function
buildProblemRectificationQueryFilter
(
filters
=
{},
gridView
,
subject
)
{
const
columns
=
buildViewColumnsForList
(
gridView
,
subject
);
const
uiFieldClauses
=
Object
.
entries
(
FILTER_FIELD_PATHS
).
map
(([
filterKey
,
fieldPath
])
=>
{
const
value
=
filters
?.[
filterKey
];
if
(
isEmptyFilterValue
(
value
))
{
return
null
;
}
return
{
[
fieldPath
]:
value
};
});
return
combineFilterClauses
([
buildRestrictionsFilter
(
gridView
?.
settings
?.
restrictions
),
buildViewConditionsFilter
(
gridView
?.
settings
?.
filters
),
...
uiFieldClauses
,
buildFocusFlagFilter
(
filters
?.
focusFlag
),
buildKeywordFilter
(
filters
?.
keyword
,
columns
),
]);
}
function
buildProblemRectificationSort
(
gridView
)
{
const
sortColumns
=
toArray
(
gridView
?.
settings
?.
columns
)
.
filter
((
column
)
=>
column
.
sort
&&
column
.
field
)
.
sort
((
left
,
right
)
=>
(
left
.
sortIndex
??
0
)
-
(
right
.
sortIndex
??
0
));
const
viewSort
=
sortColumns
.
map
((
column
)
=>
column
.
sort
===
'desc'
?
`-
${
column
.
field
}
`
:
column
.
field
,
);
return
[...
viewSort
,
'-updated_at'
,
'-created_at'
,
'_id'
].
join
(
' '
);
}
export
function
normalizeProblemRectificationRecord
(
record
)
{
export
function
normalizeProblemRectificationRecord
(
record
)
{
const
metadata
=
record
?.
metadata
||
{};
const
metadata
=
record
?.
metadata
||
{};
const
dueDate
=
metadata
.
the_time_of_rectification
const
departmentItems
=
normalizeDepartmentRectificationItems
(
record
);
?
dayjs
(
metadata
.
the_time_of_rectification
)
const
hasDepartmentItems
=
departmentItems
.
length
>
0
;
const
summarizedProgress
=
hasDepartmentItems
?
summarizeTextStatus
(
departmentItems
.
map
((
item
)
=>
item
.
rectification_progress
),
'部分整改'
,
''
,
)
:
normalizeText
(
metadata
.
rectification_progress
);
const
summarizedAuditStatus
=
hasDepartmentItems
?
summarizeTextStatus
(
departmentItems
.
map
((
item
)
=>
item
.
audit_status
),
'部分审批'
,
AUDIT_STATUS_PENDING
,
)
:
normalizeText
(
metadata
.
audit_status
);
const
summarizedFinalAuditStatus
=
hasDepartmentItems
?
summarizeTextStatus
(
departmentItems
.
map
((
item
)
=>
item
.
final_audit_status
),
'部分审批'
,
AUDIT_STATUS_PENDING
,
)
:
normalizeText
(
metadata
.
final_audit_status
);
const
summarizedDueDate
=
hasDepartmentItems
?
summarizeDate
(
departmentItems
.
map
((
item
)
=>
item
.
the_time_of_rectification
))
:
metadata
.
the_time_of_rectification
;
const
dueDate
=
summarizedDueDate
?
dayjs
(
summarizedDueDate
)
:
null
;
:
null
;
const
normalizedProgress
=
normalizeText
(
metadata
.
rectification_progress
)
;
const
normalizedProgress
=
summarizedProgress
;
const
isFixed
=
FIXED_PROGRESS_VALUES
.
has
(
normalizedProgress
);
const
isFixed
=
FIXED_PROGRESS_VALUES
.
has
(
normalizedProgress
);
const
hasEconomicLoss
=
isAffirmative
(
metadata
.
has_economic_loss
);
const
hasEconomicLoss
=
isAffirmative
(
metadata
.
has_economic_loss
);
const
hasExternalImpact
=
isAffirmative
(
metadata
.
has_external_impact
);
const
hasExternalImpact
=
isAffirmative
(
metadata
.
has_external_impact
);
...
@@ -158,8 +580,8 @@ export function normalizeProblemRectificationRecord(record) {
...
@@ -158,8 +580,8 @@ export function normalizeProblemRectificationRecord(record) {
inspectUnit
:
normalizeText
(
metadata
.
inspect_the_implementing_unit
),
inspectUnit
:
normalizeText
(
metadata
.
inspect_the_implementing_unit
),
progress
:
normalizedProgress
,
progress
:
normalizedProgress
,
feedbackUnit
:
normalizeText
(
metadata
.
feedback_unit
),
feedbackUnit
:
normalizeText
(
metadata
.
feedback_unit
),
auditStatus
:
normalizeText
(
metadata
.
audit_status
)
,
auditStatus
:
summarizedAuditStatus
,
finalAuditStatus
:
normalizeText
(
metadata
.
final_audit_status
)
,
finalAuditStatus
:
summarizedFinalAuditStatus
,
problemType
:
normalizeText
(
metadata
.
problem_type
),
problemType
:
normalizeText
(
metadata
.
problem_type
),
problemFrom
:
normalizeText
(
metadata
.
problem_from
),
problemFrom
:
normalizeText
(
metadata
.
problem_from
),
relatedIssueDomain
:
normalizeText
(
metadata
.
related_issue_domain
),
relatedIssueDomain
:
normalizeText
(
metadata
.
related_issue_domain
),
...
@@ -196,46 +618,93 @@ export function normalizeProblemRectificationRecord(record) {
...
@@ -196,46 +618,93 @@ export function normalizeProblemRectificationRecord(record) {
actualRectificationCompletedAt
:
formatDateText
(
metadata
.
actual_rectification_completed_at
),
actualRectificationCompletedAt
:
formatDateText
(
metadata
.
actual_rectification_completed_at
),
inspectionCompletedAt
:
formatDateText
(
metadata
.
inspection_completed_at
),
inspectionCompletedAt
:
formatDateText
(
metadata
.
inspection_completed_at
),
dueDate
:
dueDate
&&
dueDate
.
isValid
()
?
dueDate
:
null
,
dueDate
:
dueDate
&&
dueDate
.
isValid
()
?
dueDate
:
null
,
fillUsers
:
toArray
(
metadata
.
fill_in_the_people
)
fillUsers
:
(
hasDepartmentItems
?
departmentItems
.
map
((
item
)
=>
item
.
editor
)
:
toArray
(
metadata
.
fill_in_the_people
)
)
.
map
(
normalizeUserLabel
)
.
map
(
normalizeUserLabel
)
.
filter
(
Boolean
),
.
filter
(
Boolean
),
reviewUsers
:
toArray
(
metadata
.
review_people
)
reviewUsers
:
(
hasDepartmentItems
?
departmentItems
.
map
((
item
)
=>
item
.
auditor
)
:
toArray
(
metadata
.
review_people
)
)
.
map
(
normalizeUserLabel
)
.
map
(
normalizeUserLabel
)
.
filter
(
Boolean
),
.
filter
(
Boolean
),
dueDateLabel
:
formatDateText
(
metadata
.
the_time_of_rectification
)
||
'-'
,
dueDateLabel
:
formatDateText
(
summarizedDueDate
)
||
(
hasDepartmentItems
?
'见部门明细'
:
'-'
),
departmentItems
,
hasDepartmentItems
,
isFixed
,
isFixed
,
isOverdue
,
isOverdue
,
raw
:
record
,
raw
:
record
,
};
};
}
}
export
async
function
loadProblemRectificationListData
()
{
export
async
function
loadProblemRectificationListData
({
page
=
0
,
pageSize
=
DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE
,
filters
=
{},
gridView
=
null
,
subject
=
null
,
}
=
{})
{
const
requestPage
=
Math
.
max
(
0
,
Number
(
page
)
||
0
);
const
requestPageSize
=
PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS
.
includes
(
Number
(
pageSize
))
?
Number
(
pageSize
)
:
DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE
;
const
result
=
await
queryRecords
({
const
result
=
await
queryRecords
({
subject
:
PROBLEM_RECTIFICATION_SUBJECT
,
subject
:
PROBLEM_RECTIFICATION_SUBJECT
,
page
:
0
,
page
:
requestPage
,
page_size
:
1000
,
page_size
:
requestPageSize
,
sort
:
'-updated_at -created_at'
,
filter
:
buildProblemRectificationQueryFilter
(
filters
,
gridView
,
subject
),
sort
:
buildProblemRectificationSort
(
gridView
),
mode
:
'full'
,
mode
:
'full'
,
});
});
const
records
=
(
result
?.
records
||
[]).
map
(
normalizeProblemRectificationRecord
);
const
records
=
(
result
?.
records
||
[]).
map
(
normalizeProblemRectificationRecord
);
return
records
;
const
totalRecords
=
Number
(
result
?.
total_records
??
result
?.
total
??
records
.
length
);
const
totalPages
=
Number
(
result
?.
total_pages
??
Math
.
max
(
1
,
Math
.
ceil
(
totalRecords
/
requestPageSize
)),
);
return
{
records
,
pagination
:
{
page
:
Number
(
result
?.
page
??
requestPage
),
pageSize
:
Number
(
result
?.
page_size
??
requestPageSize
),
totalRecords
,
totalPages
:
Math
.
max
(
1
,
totalPages
),
},
};
}
}
export
async
function
loadProblemRectificationListResources
()
{
export
async
function
loadProblemRectificationListResources
()
{
const
[
records
,
subject
,
views
]
=
await
Promise
.
all
([
const
[
subject
,
views
]
=
await
Promise
.
all
([
loadProblemRectificationListData
(),
loadSubject
(
PROBLEM_RECTIFICATION_SUBJECT
),
loadSubject
(
PROBLEM_RECTIFICATION_SUBJECT
),
getSubjectView
(
PROBLEM_RECTIFICATION_SUBJECT
),
getSubjectView
(
PROBLEM_RECTIFICATION_SUBJECT
),
]);
]);
return
{
return
{
records
,
subject
,
subject
,
views
:
views
||
[],
views
:
views
||
[],
};
};
}
}
export
async
function
loadProblemRectificationViewCountMap
({
filters
=
{},
views
=
[],
subject
=
null
}
=
{})
{
const
entries
=
await
Promise
.
all
(
views
.
map
(
async
(
view
)
=>
{
const
result
=
await
countRecords
({
subject
:
PROBLEM_RECTIFICATION_SUBJECT
,
filter
:
buildProblemRectificationQueryFilter
(
filters
,
view
,
subject
),
});
return
[
view
.
name
,
Number
(
result
?.
count
??
result
?.
total
??
0
)];
}),
);
return
Object
.
fromEntries
(
entries
);
}
export
function
buildGridViews
(
views
=
[])
{
export
function
buildGridViews
(
views
=
[])
{
const
gridViews
=
views
const
gridViews
=
views
.
filter
((
view
)
=>
view
.
type
===
'grid'
)
.
filter
((
view
)
=>
view
.
type
===
'grid'
)
...
@@ -383,13 +852,20 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
...
@@ -383,13 +852,20 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
const
sortedRecords
=
sortRecordsByView
(
filteredRecords
,
gridView
);
const
sortedRecords
=
sortRecordsByView
(
filteredRecords
,
gridView
);
const
progressOptions
=
Array
.
from
(
const
subjectOptionValues
=
(
fieldName
)
=>
new
Set
(
records
.
map
((
item
)
=>
item
.
progress
).
filter
(
Boolean
)),
getSelectOptions
(
findSubjectField
(
subject
,
fieldName
))
);
.
map
((
option
)
=>
option
.
value
)
.
filter
(
Boolean
);
const
auditStatusOptions
=
Array
.
from
(
const
progressOptions
=
new
Set
(
records
.
map
((
item
)
=>
item
.
auditStatus
).
filter
(
Boolean
)),
subjectOptionValues
(
'rectification_progress'
).
length
>
0
);
?
subjectOptionValues
(
'rectification_progress'
)
:
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
progress
).
filter
(
Boolean
)));
const
auditStatusOptions
=
subjectOptionValues
(
'audit_status'
).
length
>
0
?
subjectOptionValues
(
'audit_status'
)
:
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
auditStatus
).
filter
(
Boolean
)));
const
severityOptions
=
sortLevelOptions
(
const
severityOptions
=
sortLevelOptions
(
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
severityLevel
).
filter
(
Boolean
))),
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
severityLevel
).
filter
(
Boolean
))),
...
@@ -403,9 +879,10 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
...
@@ -403,9 +879,10 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
new
Set
(
records
.
map
((
item
)
=>
item
.
primaryResponsibleDepartment
).
filter
(
Boolean
)),
new
Set
(
records
.
map
((
item
)
=>
item
.
primaryResponsibleDepartment
).
filter
(
Boolean
)),
).
sort
((
left
,
right
)
=>
left
.
localeCompare
(
right
,
'zh-CN'
));
).
sort
((
left
,
right
)
=>
left
.
localeCompare
(
right
,
'zh-CN'
));
const
problemFromOptions
=
Array
.
from
(
const
problemFromOptions
=
new
Set
(
records
.
map
((
item
)
=>
item
.
problemFrom
).
filter
(
Boolean
)),
subjectOptionValues
(
'problem_from'
).
length
>
0
);
?
subjectOptionValues
(
'problem_from'
)
:
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
problemFrom
).
filter
(
Boolean
)));
const
rootCauseOptions
=
Array
.
from
(
const
rootCauseOptions
=
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
rootCause
).
filter
(
Boolean
)),
new
Set
(
records
.
map
((
item
)
=>
item
.
rootCause
).
filter
(
Boolean
)),
...
...
src/services/subjectsDefinition.md
View file @
9b9085cc
...
@@ -103,64 +103,103 @@ type Record = {
...
@@ -103,64 +103,103 @@ type Record = {
一条记录表示一条待整改问题主记录,是本模块最核心的数据对象。
一条记录表示一条待整改问题主记录,是本模块最核心的数据对象。
| 字段标签 | 字段别名 | 字段类型 | 是否主键 | 是否多值 | 是否必填 |
当前字段定义来自线上 Subject 配置,共 43 个字段。字段值写入时均放入
`metadata`
。
| --- | --- | --- | --- | --- | --- |
| 检查项目名称 |
`project_name`
| text | 否 | 否 | 否 |
| 字段标签 | 字段别名 | 字段类型 | 默认值 | 是否主键 | 是否多值 | 是否必填 | 可搜索 | 可聚合 |
| 检查实施单位 |
`inspect_the_implementing_unit`
| select | 否 | 否 | 否 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 检查实施时间 |
`check_the_implementation_time`
| text | 否 | 否 | 否 |
| 检查项目名称 |
`project_name`
| text |
`null`
| 否 | 否 | 否 | 是 | 否 |
| 检查报告出具日 |
`inspection_report_date`
| text | 否 | 否 | 否 |
| 检查实施单位 |
`inspect_the_implementing_unit`
| select |
`""`
| 否 | 否 | 否 | 是 | 否 |
| 问题简述 |
`problem_brief`
| long_text | 否 | 否 | 否 |
| 检查实施时间 |
`check_the_implementation_time`
| text |
`null`
| 否 | 否 | 否 | 是 | 否 |
| 问题发现情况 |
`problem_discovery_situation`
| long_text | 否 | 否 | 否 |
| 检查报告出具日 |
`inspection_report_date`
| text |
`null`
| 否 | 否 | 否 | 是 | 否 |
| 责任部门 |
`responsible_department`
| select | 否 | 是 | 否 |
| 问题简述 |
`problem_brief`
| long_text |
`""`
| 否 | 否 | 否 | 是 | 否 |
| 整改进度 |
`rectification_progress`
| select | 否 | 否 | 否 |
| 责任部门 |
`responsible_department`
| select |
`""`
| 否 | 是 | 否 | 是 | 否 |
| 整改情况 |
`rectification_situation`
| long_text | 否 | 否 | 否 |
| 整改进度 |
`rectification_progress`
| select |
`""`
| 否 | 否 | 否 | 是 | 否 |
| 问题来源 |
`problem_from`
| select | 否 | 否 | 否 |
| 整改情况 |
`rectification_situation`
| long_text |
`""`
| 否 | 否 | 否 | 是 | 否 |
| 问题类型 |
`problem_type`
| select | 否 | 否 | 否 |
| 问题来源 |
`problem_from`
| select |
`""`
| 否 | 否 | 否 | 是 | 否 |
| 反馈单位 |
`feedback_unit`
| select | 否 | 否 | 否 |
| 问题类型 |
`problem_type`
| select |
`""`
| 否 | 否 | 否 | 是 | 否 |
| 问题涉及业务领域 |
`related_issue_domain`
| text | 否 | 否 | 否 |
| 反馈单位 |
`feedback_unit`
| select |
`鑫元基金风险管理部`
| 否 | 否 | 否 | 是 | 否 |
| 预计整改完成时间 |
`the_time_of_rectification`
| date | 否 | 否 | 否 |
| 问题涉及业务领域 |
`related_issue_domain`
| text |
`""`
| 否 | 否 | 否 | 是 | 是 |
| 填写人 |
`fill_in_the_people`
| user | 否 | 是 | 否 |
| 预计整改完成时间 |
`the_time_of_rectification`
| date |
`null`
| 否 | 否 | 否 | 是 | 否 |
| 复核人 |
`review_people`
| user | 否 | 是 | 否 |
| 填写人 |
`fill_in_the_people`
| user |
`""`
| 否 | 是 | 否 | 是 | 否 |
| 审批状态 |
`audit_status`
| select | 否 | 否 | 否 |
| 复核人 |
`review_people`
| user |
`""`
| 否 | 是 | 否 | 是 | 否 |
| 最终审批状态 |
`final_audit_status`
| select | 否 | 否 | 否 |
| 审批状态 |
`audit_status`
| select |
`未审批`
| 否 | 否 | 否 | 是 | 否 |
| 备注 |
`remark`
| long_text | 否 | 否 | 否 |
| 最终审批状态 |
`final_audit_status`
| select |
`未审批`
| 否 | 否 | 否 | 是 | 否 |
| 整改证明材料 |
`rectification_proof_materials`
| file | 否 | 否 | 否 |
| 备注 |
`remark`
| long_text |
`null`
| 否 | 否 | 否 | 是 | 否 |
| 是否造成经济损失 |
`has_economic_loss`
| text | 否 | 否 | 否 |
| 整改证明材料 |
`rectification_proof_materials`
| file |
`null`
| 否 | 否 | 否 | 是 | 否 |
| 对应经济损失 |
`economic_loss_amount`
| text | 否 | 否 | 否 |
| 是否造成经济损失 |
`has_economic_loss`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 是否造成外部影响 |
`has_external_impact`
| text | 否 | 否 | 否 |
| 对应经济损失 |
`economic_loss_amount`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 处罚机构 |
`penalty_organization`
| text | 否 | 否 | 否 |
| 是否造成外部影响 |
`has_external_impact`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 是否涉及员工问责 |
`has_employee_accountability`
| text | 否 | 否 | 否 |
| 处罚机构 |
`penalty_organization`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 问责人数 |
`accountability_person_count`
| text | 否 | 否 | 否 |
| 是否涉及员工问责 |
`has_employee_accountability`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 问题综合严重程度 |
`problem_severity_level`
| text | 否 | 否 | 否 |
| 问责人数 |
`accountability_person_count`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 问题一级分类 |
`problem_category_level_1`
| text | 否 | 否 | 否 |
| 问题综合严重程度 |
`problem_severity_level`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 问题二级分类 |
`problem_category_level_2`
| text | 否 | 否 | 否 |
| 问题一级分类 |
`problem_category_level_1`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 问题三级分类 |
`problem_category_level_3`
| text | 否 | 否 | 否 |
| 问题二级分类 |
`problem_category_level_2`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 问题四级分类 |
`problem_category_level_4`
| text | 否 | 否 | 否 |
| 问题三级分类 |
`problem_category_level_3`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 问题原因 |
`problem_root_cause`
| text | 否 | 否 | 否 |
| 问题四级分类 |
`problem_category_level_4`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 整改时间评估 |
`rectification_timeliness_level`
| text | 否 | 否 | 否 |
| 问题原因 |
`problem_root_cause`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 整改证明材料质量 |
`rectification_material_quality_level`
| text | 否 | 否 | 否 |
| 整改时间评估 |
`rectification_timeliness_level`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 是否涉及制度修订 |
`involves_policy_revision`
| text | 否 | 否 | 否 |
| 整改证明材料质量 |
`rectification_material_quality_level`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 制度名称 |
`policy_revision_name`
| text | 否 | 否 | 否 |
| 是否涉及制度修订 |
`involves_policy_revision`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 是否涉及系统建设 |
`involves_system_construction`
| text | 否 | 否 | 否 |
| 制度名称 |
`policy_revision_name`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 系统名称 |
`system_construction_name`
| text | 否 | 否 | 否 |
| 是否涉及系统建设 |
`involves_system_construction`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 是否为往期发现问题 |
`is_historical_problem`
| text | 否 | 否 | 否 |
| 系统名称 |
`system_construction_name`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 问题是否重复出现 |
`is_repeated_problem`
| text | 否 | 否 | 否 |
| 是否为往期发现问题 |
`is_historical_problem`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 综合整改评定等级 |
`rectification_overall_rating`
| text | 否 | 否 | 否 |
| 问题是否重复出现 |
`is_repeated_problem`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 实际整改完成时间 |
`actual_rectification_completed_at`
| text | 否 | 否 | 否 |
| 综合整改评定等级 |
`rectification_overall_rating`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 实际检查完成日期 |
`inspection_completed_at`
| text | 否 | 否 | 否 |
| 实际整改完成时间 |
`actual_rectification_completed_at`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 主责部门 |
`primary_responsible_department`
| text | 否 | 否 | 否 |
| 实际检查完成日期 |
`inspection_completed_at`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 主责部门 |
`primary_responsible_department`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
### 3.1.1 字段说明
| 部门整改明细 |
`department_rectification_items`
| object |
`[]`
| 否 | 是 | 否 | 否 | 否 |
### 3.1.1 select 字段选项
-
`inspect_the_implementing_unit`
(检查实施单位):
`法律合规部`
、
`源泰律师事务所(合规有效性评估)`
、
`普华永道会计师事务所`
、
`风险管理部`
、
`内部审计`
、
`内部合规检查`
、
`外部审计`
、
`外部检查`
、
`总行审计`
、
`工会经费审査`
、
`国家审计署`
、
`鑫沅资产`
、
`鑫元基金`
、
`其他`
-
`responsible_department`
(责任部门):
`固定收益部`
、
`权益投资部`
、
`多元资产投资部`
、
`量化投资部`
、
`市场研究部`
、
`交易部`
、
`机构销售部`
、
`渠道销售部`
、
`电商销售部`
、
`战略与品宣部`
、
`客户服务部`
、
`产品研发部`
、
`法律合规部`
、
`审计部(纪委办公室)`
、
`风险管理部`
、
`信息技术部`
、
`基金运营部`
、
`财务会计部`
、
`人力资源部(党委组织部)`
、
`办公室(党委办公室)`
、
`北京分公司`
、
`广州分公司`
、
`(子)投资部`
、
`(子)研究部`
、
`(子)交易室`
、
`(子)市场营销部`
、
`(子)风控合规部`
、
`(子)运营部`
、
`(子)办公室`
-
`rectification_progress`
(整改进度):
`已整改`
(颜色
`#a9ef95`
)、
`整改中`
(颜色
`#ea6e60`
)、
`部分整改`
(颜色
`#efa659`
)、
`不适用`
(颜色
`#b2b0a9`
)、
`已优化`
-
`problem_from`
(问题来源):
`内部检查`
、
`总行检查`
、
`监管检查`
、
`其他外部机构检查`
、
`内部转办`
、
`总行转办`
、
`监管转办`
、
`其他外部机构转办`
-
`problem_type`
(问题类型):
`业务问题类`
、
`线索类`
-
`feedback_unit`
(反馈单位):
`鑫元基金风险管理部`
(颜色
`#f55353`
)、
`鑫元基金法律合规部`
(颜色
`#63ac52`
)、
`鑫元基金审计部`
(颜色
`#9b2727`
)、
`鑫沅资产风控合规部`
(颜色
`#948686`
)
-
`audit_status`
(审批状态):
`未审批`
、
`已审批`
、
`已驳回`
-
`final_audit_status`
(最终审批状态):
`未审批`
、
`已审批`
、
`已驳回`
### 3.1.2 字段说明
-
`responsible_department`
为多值选择字段,旧应用导出/归档时会转成文本或逗号串
-
`responsible_department`
为多值选择字段,旧应用导出/归档时会转成文本或逗号串
-
`fill_in_the_people`
、
`review_people`
为多值用户字段,写入时仅保存用户
`_id[]`
-
`fill_in_the_people`
、
`review_people`
为多值用户字段,写入时仅保存用户
`_id[]`
-
`audit_status`
用于复核阶段结果,旧应用已使用
`已审批`
、
`已驳回`
-
`audit_status`
用于复核阶段结果,旧应用已使用
`已审批`
、
`已驳回`
-
`final_audit_status`
用于最终审批阶段结果,旧应用已使用
`已审批`
、
`已驳回`
-
`final_audit_status`
用于最终审批阶段结果,旧应用已使用
`已审批`
、
`已驳回`
-
`rectification_progress`
在旧驾驶舱统计中已使用
`已整改`
、
`已优化`
两个值判断“已整改问题”
-
`rectification_progress`
在旧驾驶舱统计中已使用
`已整改`
、
`已优化`
两个值判断“已整改问题”
-
`related_issue_domain`
当前配置开启聚合,可用于业务领域统计
-
新增驾驶舱扩展字段当前在线上 Subject 中大多仍为
`text`
类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析
-
新增驾驶舱扩展字段当前在线上 Subject 中大多仍为
`text`
类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析
-
其余
`select`
类型的可选值必须以线上 Subject 配置中的
`settings.options`
为准,禁止在页面内硬编码
-
其余
`select`
类型的可选值必须以线上 Subject 配置中的
`settings.options`
为准,禁止在页面内硬编码
-
`problem_discovery_situation`
曾在历史文档和归档对象中出现,但本次主对象字段定义未包含;新页面只应在 Subject View 或线上字段配置存在时展示,不能作为
`problem_rectification`
固定字段依赖
-
`department_rectification_items`
用于新转发流程的部门级整改明细。单条问题即使只有 1 个责任部门,也生成 1 条明细;转发后
`rectification_progress`
、
`rectification_situation`
、
`the_time_of_rectification`
、
`rectification_proof_materials`
、
`audit_status`
、
`final_audit_status`
以明细为事实来源,父字段仅做兼容汇总展示
-
部门明细的填写人只能编辑自己负责明细的整改业务字段,送审后(
`editor_status = "已完成"`
)不再允许编辑;审批人审批通过后(
`audit_status = "已审批"`
)不再允许编辑,审批驳回后(
`audit_status = "已驳回"`
)审批人可以编辑该明细的整改业务字段,填写人也恢复编辑。审批人恢复的只是业务字段编辑权限,不获得填写人的送审权限。流程推送人可以编辑本次推送关联的父问题记录及全部部门明细整改业务字段。审批状态、待办 ID、明细 ID 等流程控制字段不作为普通表单字段开放
-
推送人修改父问题记录或部门明细时,不自动重置该明细已经形成的审批状态,也不自动重新生成流程待办
### 3.1.2 metadata 示例
### 3.1.3 `department_rectification_items` 子结构
| 字段标签 | 字段别名 | 字段类型 | 是否必填 | 说明 |
| --- | --- | --- | --- | --- |
| 明细 ID |
`item_id`
| text | 是 | 部门明细唯一标识,用于行级保存、审批和待办回写 |
| 责任部门 |
`responsible_department`
| text | 是 | 当前明细对应的责任部门 |
| 填写人 |
`editor`
| user | 是 | 保存用户
`_id`
,每条明细 1 个填写人 |
| 审批人 |
`auditor`
| user | 是 | 保存用户
`_id`
,每条明细 1 个审批人 |
| 整改进度 |
`rectification_progress`
| select/text | 否 | 填写人维护,优先使用主对象整改进度选项 |
| 整改情况 |
`rectification_situation`
| long_text | 否 | 填写人维护 |
| 预计整改完成时间 |
`the_time_of_rectification`
| date | 否 | 填写人维护,提交时保存 ISO 8601 |
| 整改证明材料 |
`rectification_proof_materials`
| file | 否 | 填写人上传,按部门明细独立保存 |
| 填写状态 |
`editor_status`
| text | 否 |
`未完成`
、
`已完成`
|
| 审批状态 |
`audit_status`
| text | 否 |
`未审批`
、
`已审批`
、
`已驳回`
|
| 最终审批状态 |
`final_audit_status`
| text | 否 |
`未审批`
、
`已审批`
、
`已驳回`
|
| 填写待办 ID |
`progress_id_editor`
| text | 否 | 当前明细填写待办 ID |
| 审批待办 ID |
`progress_id_auditor`
| text | 否 | 当前明细审批待办 ID |
| 最终审批待办 ID |
`progress_id_sender`
| text | 否 | 当前明细最终审批待办 ID |
| 更新人 |
`updated_by`
| user | 否 | 最近更新用户
`_id`
|
| 更新时间 |
`updated_at`
| date | 否 | 最近更新时间 |
### 3.1.4 metadata 示例
```
json
```
json
{
{
...
@@ -169,7 +208,6 @@ type Record = {
...
@@ -169,7 +208,6 @@ type Record = {
"check_the_implementation_time"
:
"2026年第一季度"
,
"check_the_implementation_time"
:
"2026年第一季度"
,
"inspection_report_date"
:
"2026-04-30"
,
"inspection_report_date"
:
"2026-04-30"
,
"problem_brief"
:
"部分整改项未按期闭环。"
,
"problem_brief"
:
"部分整改项未按期闭环。"
,
"problem_discovery_situation"
:
"检查发现两项制度执行存在时效性缺口。"
,
"responsible_department"
:
[
"responsible_department"
:
[
"<责任部门选项值 A>"
,
"<责任部门选项值 A>"
,
"<责任部门选项值 B>"
"<责任部门选项值 B>"
...
@@ -256,11 +294,12 @@ type Record = {
...
@@ -256,11 +294,12 @@ type Record = {
}
}
```
```
## 3.4
问题整改流程消息主单
## 3.4
信息关联id(问题整改流程消息主单)
-
**subject**
:
`message_reference_id`
-
**subject**
:
`message_reference_id`
-
**对象名称**
: 信息关联id
这是旧应用中用于承载“推送填写 / 复核 / 最终审批”流程的辅助对象,不是业务展示主表,但新项目若需要兼容既有流程链接,应保留该协议说明。
这是旧应用中用于承载“推送填写 / 复核 / 最终审批”流程的辅助对象,
线上对象名称为“信息关联id”。它
不是业务展示主表,但新项目若需要兼容既有流程链接,应保留该协议说明。
| 字段标签 | 字段别名 | 字段类型 | 是否主键 | 是否多值 | 是否必填 |
| 字段标签 | 字段别名 | 字段类型 | 是否主键 | 是否多值 | 是否必填 |
| --- | --- | --- | --- | --- | --- |
| --- | --- | --- | --- | --- | --- |
...
@@ -272,6 +311,8 @@ type Record = {
...
@@ -272,6 +311,8 @@ type Record = {
| 对象名 |
`subject_name`
| text | 否 | 否 | 否 |
| 对象名 |
`subject_name`
| text | 否 | 否 | 否 |
| 视图名称 |
`view_name`
| text | 否 | 否 | 否 |
| 视图名称 |
`view_name`
| text | 否 | 否 | 否 |
| 关联记录 ID |
`reference_record_id`
| text | 否 | 是 | 否 |
| 关联记录 ID |
`reference_record_id`
| text | 否 | 是 | 否 |
| 流程模式 |
`flow_mode`
| text | 否 | 否 | 否 |
| 部门明细 ID |
`department_item_ids`
| text | 否 | 是 | 否 |
| 填写待办 ID |
`progress_id_editor`
| text | 否 | 是 | 否 |
| 填写待办 ID |
`progress_id_editor`
| text | 否 | 是 | 否 |
| 审核待办 ID |
`progress_id_auditor`
| text | 否 | 是 | 否 |
| 审核待办 ID |
`progress_id_auditor`
| text | 否 | 是 | 否 |
| 最终审核待办 ID |
`progress_id_sender`
| text | 否 | 否 | 否 |
| 最终审核待办 ID |
`progress_id_sender`
| text | 否 | 否 | 否 |
...
@@ -285,6 +326,8 @@ type Record = {
...
@@ -285,6 +326,8 @@ type Record = {
-
`restriction_value`
保存被推送整改记录的
`_id[]`
-
`restriction_value`
保存被推送整改记录的
`_id[]`
-
`reference_record_id`
与
`restriction_value`
保持一致,作为显式的“关联记录 ID”冗余字段
-
`reference_record_id`
与
`restriction_value`
保持一致,作为显式的“关联记录 ID”冗余字段
-
`subject_name`
保存被推送记录所属对象名,当前整改流程固定为
`problem_rectification`
-
`subject_name`
保存被推送记录所属对象名,当前整改流程固定为
`problem_rectification`
-
`flow_mode`
区分新旧流程:
`department`
表示单条问题按责任部门生成明细行,
`batch`
表示多选问题的单填写人 / 单审批人简单推送;历史数据可能为空
-
`department_item_ids`
保存本次部门流程涉及的
`department_rectification_items[].item_id`
-
`progress_id_editor`
、
`progress_id_auditor`
、
`progress_id_sender`
用于记录每一步用户确认动作生成的流程 ID,用来区分当前流程推进到了哪一步
-
`progress_id_editor`
、
`progress_id_auditor`
、
`progress_id_sender`
用于记录每一步用户确认动作生成的流程 ID,用来区分当前流程推进到了哪一步
-
`editor_status`
:当前项目统一使用
`未完成`
、
`已完成`
-
`editor_status`
:当前项目统一使用
`未完成`
、
`已完成`
-
`auditor_status`
:当前项目统一使用
`未完成`
、
`已完成`
-
`auditor_status`
:当前项目统一使用
`未完成`
、
`已完成`
...
...
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