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
dcdc9689
Commit
dcdc9689
authored
Jun 01, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 动态详情页
parent
b52ff11f
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
2935 additions
and
889 deletions
+2935
-889
index.jsx
src/components/record-workbench/index.jsx
+576
-0
useSubjectRecord.js
src/pages/RecordDetailDrawer/hooks/useSubjectRecord.js
+5
-5
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+4
-3
archiveWorkbenchService.js
.../problem_rectification/archive/archiveWorkbenchService.js
+144
-0
index.jsx
src/pages/problem_rectification/archive/index.jsx
+191
-4
index.jsx
src/pages/problem_rectification/dashboard/index.jsx
+801
-245
index.jsx
src/pages/problem_rectification/list/index.jsx
+375
-422
problem-rectification.business-flow.md
...blem_rectification/problem-rectification.business-flow.md
+85
-35
problemRectificationDashboardService.js
...lem_rectification/problemRectificationDashboardService.js
+468
-172
problemRectificationListService.js
.../problem_rectification/problemRectificationListService.js
+261
-3
subjectsDefinition.md
src/services/subjectsDefinition.md
+25
-0
No files found.
src/components/record-workbench/index.jsx
0 → 100644
View file @
dcdc9689
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
{
Checkbox
}
from
'@/catalyst-ui-kit/javascript/checkbox'
;
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
{
formatFieldValue
,
getFieldValue
}
from
'@/utils/subjectView'
;
import
clsx
from
'clsx'
;
import
{
useMemo
,
useState
}
from
'react'
;
const
SELECTION_COLUMN_WIDTH
=
56
;
const
DEFAULT_ACTION_COLUMN_WIDTH
=
132
;
function
getPinnedSide
(
column
)
{
const
pinnedValue
=
column
?.
lockPosition
||
column
?.
pinned
;
if
(
pinnedValue
===
'left'
||
pinnedValue
===
'right'
)
{
return
pinnedValue
;
}
return
null
;
}
function
buildStickyColumnMeta
(
columns
=
[],
selectable
,
hasRowActions
,
actionColumnWidth
)
{
const
leftColumns
=
columns
.
filter
((
column
)
=>
getPinnedSide
(
column
)
===
'left'
);
const
centerColumns
=
columns
.
filter
((
column
)
=>
!
getPinnedSide
(
column
));
const
rightColumns
=
columns
.
filter
((
column
)
=>
getPinnedSide
(
column
)
===
'right'
);
const
orderedColumns
=
[...
leftColumns
,
...
centerColumns
,
...
rightColumns
];
let
leftOffset
=
selectable
?
SELECTION_COLUMN_WIDTH
:
0
;
let
rightOffset
=
hasRowActions
?
actionColumnWidth
:
0
;
const
stickyMeta
=
{};
leftColumns
.
forEach
((
column
)
=>
{
stickyMeta
[
column
.
field
]
=
{
side
:
'left'
,
offset
:
leftOffset
,
isEdge
:
column
.
field
===
leftColumns
[
leftColumns
.
length
-
1
]?.
field
,
};
leftOffset
+=
column
.
width
||
180
;
});
[...
rightColumns
].
reverse
().
forEach
((
column
)
=>
{
stickyMeta
[
column
.
field
]
=
{
side
:
'right'
,
offset
:
rightOffset
,
isEdge
:
column
.
field
===
rightColumns
[
0
]?.
field
,
};
rightOffset
+=
column
.
width
||
180
;
});
return
{
orderedColumns
,
stickyMeta
,
};
}
function
getStickyCellClass
(
meta
,
kind
=
'body'
)
{
if
(
!
meta
)
{
return
''
;
}
const
baseClass
=
kind
===
'header'
?
'sticky z-30 bg-zinc-50/95 backdrop-blur'
:
'sticky z-20 bg-white'
;
const
sideClass
=
meta
.
side
===
'left'
?
'left-0'
:
'right-0'
;
const
edgeShadow
=
meta
.
side
===
'left'
&&
meta
.
isEdge
?
'shadow-[10px_0_20px_rgba(15,23,42,0.06)]'
:
meta
.
side
===
'right'
&&
meta
.
isEdge
?
'shadow-[-10px_0_20px_rgba(15,23,42,0.06)]'
:
''
;
return
clsx
(
baseClass
,
sideClass
,
edgeShadow
);
}
function
EmptyState
({
title
,
description
})
{
return
(
<
div
className=
"rounded-[28px] border border-dashed border-zinc-300 bg-white/80 px-6 py-14 text-center"
>
<
div
className=
"text-xl font-semibold text-slate-900"
>
{
title
}
</
div
>
<
div
className=
"mt-3 text-sm leading-6 text-slate-500"
>
{
description
}
</
div
>
</
div
>
);
}
function
SummaryCard
({
label
,
value
,
hint
,
tone
=
'default'
})
{
const
toneClass
=
tone
===
'rose'
?
'border-rose-200/80 bg-rose-50/85'
:
tone
===
'amber'
?
'border-amber-200/80 bg-amber-50/85'
:
tone
===
'emerald'
?
'border-emerald-200/80 bg-emerald-50/85'
:
'border-zinc-200/80 bg-white/88'
;
return
(
<
div
className=
{
clsx
(
'rounded-[26px] border p-5 shadow-sm ring-1 ring-zinc-950/5'
,
toneClass
)
}
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.22em] text-slate-400"
>
{
label
}
</
div
>
<
div
className=
"mt-3 text-3xl font-semibold tracking-tight text-slate-950"
>
{
value
}
</
div
>
<
div
className=
"mt-2 text-sm text-slate-500"
>
{
hint
}
</
div
>
</
div
>
);
}
function
isActionDisabled
(
action
,
context
)
{
if
(
typeof
action
?.
disabled
===
'function'
)
{
return
Boolean
(
action
.
disabled
(
context
));
}
return
Boolean
(
action
?.
disabled
);
}
function
isActionHidden
(
action
,
context
)
{
if
(
typeof
action
?.
hidden
===
'function'
)
{
return
Boolean
(
action
.
hidden
(
context
));
}
return
Boolean
(
action
?.
hidden
);
}
function
renderButtonProps
(
action
)
{
const
props
=
{};
if
(
action
?.
variant
===
'plain'
)
{
props
.
plain
=
true
;
}
else
if
(
action
?.
variant
===
'solid'
)
{
props
.
color
=
action
?.
color
||
(
action
?.
tone
===
'danger'
?
'red'
:
action
?.
tone
===
'primary'
?
'rose'
:
undefined
);
}
else
{
props
.
outline
=
true
;
if
(
action
?.
tone
===
'danger'
)
{
props
.
color
=
action
?.
color
||
'red'
;
}
else
if
(
action
?.
tone
===
'primary'
)
{
props
.
color
=
action
?.
color
||
'rose'
;
}
}
return
props
;
}
function
defaultGetRecordId
(
record
)
{
return
record
?.
id
||
record
?.
_id
||
record
?.
name
||
''
;
}
function
defaultGetRecordName
(
record
)
{
return
record
?.
name
||
record
?.
id
||
record
?.
_id
||
''
;
}
function
defaultGetRecordLabel
(
record
)
{
return
record
?.
title
||
record
?.
name
||
record
?.
id
||
'当前记录'
;
}
function
defaultRenderCell
(
record
,
column
)
{
const
sourceRecord
=
record
?.
raw
||
record
;
const
value
=
getFieldValue
(
sourceRecord
,
column
.
field
);
return
(
<
div
className=
"whitespace-normal break-words text-sm text-slate-700"
>
{
formatFieldValue
(
value
,
column
.
fieldDef
)
||
'-'
}
</
div
>
);
}
export
default
function
RecordWorkbench
({
summaryItems
=
[],
views
=
[],
activeViewName
=
''
,
onViewChange
,
filters
=
{},
filterSchema
=
[],
onFiltersChange
,
onClearFilters
,
title
,
description
,
columns
=
[],
records
=
[],
selectedIds
=
[],
onSelectionChange
,
toolbarActions
=
[],
bulkActions
=
[],
rowActions
=
[],
detailPanel
=
null
,
states
=
{},
renderCell
,
getRecordId
=
defaultGetRecordId
,
getRecordLabel
=
defaultGetRecordLabel
,
selectable
=
true
,
actionColumnWidth
=
DEFAULT_ACTION_COLUMN_WIDTH
,
})
{
const
[
drawerRecordName
,
setDrawerRecordName
]
=
useState
(
''
);
const
[
drawerDirty
,
setDrawerDirty
]
=
useState
(
false
);
const
loading
=
Boolean
(
states
?.
loading
);
const
error
=
states
?.
error
||
''
;
const
totalRecords
=
records
.
length
;
const
selectedRecords
=
useMemo
(
()
=>
records
.
filter
((
record
)
=>
selectedIds
.
includes
(
getRecordId
(
record
))),
[
records
,
selectedIds
,
getRecordId
],
);
const
stickyColumns
=
useMemo
(
()
=>
buildStickyColumnMeta
(
columns
,
selectable
,
rowActions
.
length
>
0
,
actionColumnWidth
),
[
columns
,
selectable
,
rowActions
.
length
,
actionColumnWidth
],
);
const
openDetail
=
(
recordOrName
)
=>
{
const
recordName
=
typeof
recordOrName
===
'string'
?
recordOrName
:
detailPanel
?.
getRecordName
?.(
recordOrName
)
||
defaultGetRecordName
(
recordOrName
);
if
(
!
recordName
)
{
return
;
}
setDrawerDirty
(
false
);
setDrawerRecordName
(
recordName
);
};
const
closeDetail
=
async
()
=>
{
setDrawerRecordName
(
''
);
detailPanel
?.
onClose
?.();
if
(
drawerDirty
)
{
setDrawerDirty
(
false
);
await
detailPanel
?.
onDirtyClose
?.();
}
};
const
toolbarContext
=
{
records
,
selectedIds
,
selectedRecords
,
openDetail
,
};
const
visibleToolbarActions
=
[...
toolbarActions
,
...
bulkActions
].
filter
(
(
action
)
=>
!
isActionHidden
(
action
,
toolbarContext
),
);
const
summaryGridClass
=
summaryItems
.
length
>=
6
?
'md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-8'
:
summaryItems
.
length
>=
4
?
'md:grid-cols-2 xl:grid-cols-4'
:
'md:grid-cols-3'
;
const
filterGridClass
=
filterSchema
.
length
>=
7
?
'xl:grid-cols-3 2xl:grid-cols-5'
:
filterSchema
.
length
>=
4
?
'xl:grid-cols-2 2xl:grid-cols-4'
:
filterSchema
.
length
>
1
?
'xl:grid-cols-4'
:
'xl:grid-cols-[1fr_auto]'
;
return
(
<
div
className=
"space-y-6 pb-4"
>
{
summaryItems
.
length
>
0
?
(
<
section
className=
{
clsx
(
'grid gap-4'
,
summaryGridClass
)
}
>
{
summaryItems
.
map
((
item
)
=>
(
<
SummaryCard
key=
{
item
.
label
}
label=
{
item
.
label
}
value=
{
item
.
value
}
hint=
{
item
.
hint
}
tone=
{
item
.
tone
}
/>
))
}
</
section
>
)
:
null
}
{
views
.
length
>
0
?
(
<
section
className=
"rounded-[30px] border border-white/80 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(250,250,250,0.92))] p-3 shadow-sm ring-1 ring-zinc-950/5"
>
<
div
className=
"flex flex-wrap gap-2"
>
{
views
.
map
((
view
)
=>
{
const
active
=
view
.
name
===
activeViewName
;
return
(
<
button
key=
{
view
.
name
}
type=
"button"
onClick=
{
()
=>
onViewChange
?.(
view
.
name
)
}
className=
{
clsx
(
'inline-flex min-h-11 items-center gap-2 rounded-full px-4 py-2.5 text-sm font-medium transition'
,
active
?
'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg shadow-rose-200/60'
:
'bg-white text-slate-600 ring-1 ring-zinc-200 hover:bg-rose-50 hover:text-rose-700'
,
)
}
>
<
span
>
{
view
.
title
||
view
.
name
}
</
span
>
{
view
.
count
!==
undefined
?
(
<
span
className=
{
clsx
(
'rounded-full px-2 py-0.5 text-xs'
,
active
?
'bg-white/20 text-white'
:
'bg-zinc-100 text-slate-500'
,
)
}
>
{
view
.
count
}
</
span
>
)
:
null
}
</
button
>
);
})
}
</
div
>
</
section
>
)
:
null
}
{
filterSchema
.
length
>
0
?
(
<
section
className=
"rounded-[30px] border border-zinc-200 bg-white/95 p-5 shadow-sm ring-1 ring-zinc-950/5"
>
<
div
className=
{
clsx
(
'grid gap-4'
,
filterGridClass
)
}
>
{
filterSchema
.
map
((
field
)
=>
(
<
div
key=
{
field
.
key
}
className=
{
clsx
(
field
.
type
===
'text'
&&
filterSchema
.
length
>
1
&&
'xl:col-span-2'
,
filterSchema
.
length
>=
7
&&
field
.
type
===
'text'
&&
'2xl:col-span-2'
,
)
}
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
{
field
.
label
}
</
label
>
{
field
.
type
===
'select'
?
(
<
Select
value=
{
filters
[
field
.
key
]
||
''
}
onChange=
{
(
event
)
=>
onFiltersChange
?.(
field
.
key
,
event
.
target
.
value
)
}
>
<
option
value=
""
>
{
field
.
placeholder
||
`全部${field.label}`
}
</
option
>
{
(
field
.
options
||
[]).
map
((
option
)
=>
(
<
option
key=
{
option
.
value
}
value=
{
option
.
value
}
>
{
option
.
label
}
</
option
>
))
}
</
Select
>
)
:
(
<
Input
value=
{
filters
[
field
.
key
]
||
''
}
onChange=
{
(
event
)
=>
onFiltersChange
?.(
field
.
key
,
event
.
target
.
value
)
}
placeholder=
{
field
.
placeholder
}
/>
)
}
</
div
>
))
}
{
onClearFilters
?
(
<
div
className=
"flex items-end"
>
<
Button
outline
className=
"w-full justify-center"
onClick=
{
onClearFilters
}
>
清空筛选
</
Button
>
</
div
>
)
:
null
}
</
div
>
</
section
>
)
:
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"
>
<
div
className=
"flex flex-wrap items-start justify-between gap-4 border-b border-zinc-100 px-6 py-5"
>
<
div
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
{
title
}
</
div
>
<
div
className=
"mt-1 text-sm text-slate-500"
>
{
description
||
(
selectable
?
`当前视图显示 ${totalRecords} 条记录,已选中 ${selectedIds.length} 条。`
:
`当前视图显示 ${totalRecords} 条记录。`
)
}
</
div
>
</
div
>
{
visibleToolbarActions
.
length
>
0
?
(
<
div
className=
"flex flex-wrap gap-2"
>
{
visibleToolbarActions
.
map
((
action
)
=>
{
const
disabled
=
isActionDisabled
(
action
,
toolbarContext
);
const
buttonProps
=
renderButtonProps
(
action
);
return
(
<
Button
key=
{
action
.
key
}
{
...
buttonProps
}
disabled=
{
disabled
}
onClick=
{
()
=>
action
.
onClick
?.(
toolbarContext
)
}
>
{
action
.
label
}
</
Button
>
);
})
}
</
div
>
)
:
null
}
</
div
>
{
error
?
(
<
div
className=
"border-b border-zinc-100 bg-rose-50/70 px-6 py-4 text-sm text-rose-700"
>
{
error
}
</
div
>
)
:
null
}
{
loading
?
(
<
div
className=
"px-6 py-6"
>
<
EmptyState
title=
{
states
?.
loadingTitle
||
'正在加载列表'
}
description=
{
states
?.
loadingDescription
||
'正在读取对象结构、视图配置和记录数据,请稍候。'
}
/>
</
div
>
)
:
totalRecords
===
0
?
(
<
div
className=
"px-6 py-6"
>
<
EmptyState
title=
{
states
?.
emptyTitle
||
'当前暂无数据'
}
description=
{
states
?.
emptyDescription
||
'可以切换视图或调整筛选条件后重试。'
}
/>
</
div
>
)
:
(
<
div
className=
"px-3 pb-3 pt-2"
>
<
Table
bleed
dense
className=
"[--gutter:--spacing(3)]"
>
<
TableHead
>
<
TableRow
className=
"bg-zinc-50/80"
>
{
selectable
?
(
<
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)]"
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
}
{
stickyColumns
.
orderedColumns
.
map
((
column
)
=>
{
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
column
.
field
];
return
(
<
TableHeader
key=
{
column
.
field
}
style=
{
{
width
:
column
.
width
||
180
,
minWidth
:
Math
.
min
(
Math
.
max
(
column
.
width
||
180
,
150
),
360
),
left
:
stickyMeta
?.
side
===
'left'
?
`${stickyMeta.offset}px`
:
undefined
,
right
:
stickyMeta
?.
side
===
'right'
?
`${stickyMeta.offset}px`
:
undefined
,
}
}
className=
{
clsx
(
'text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'
,
getStickyCellClass
(
stickyMeta
,
'header'
),
)
}
>
{
column
.
label
}
</
TableHeader
>
);
})
}
{
rowActions
.
length
>
0
?
(
<
TableHeader
className=
"sticky right-0 z-40 bg-zinc-50/95 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400 shadow-[-10px_0_20px_rgba(15,23,42,0.06)] backdrop-blur"
style=
{
{
width
:
actionColumnWidth
,
minWidth
:
actionColumnWidth
}
}
>
操作
</
TableHeader
>
)
:
null
}
</
TableRow
>
</
TableHead
>
<
TableBody
>
{
records
.
map
((
record
)
=>
{
const
recordId
=
getRecordId
(
record
);
const
rowContext
=
{
record
,
records
,
selectedIds
,
selectedRecords
,
openDetail
,
};
const
visibleRowActions
=
rowActions
.
filter
((
action
)
=>
!
isActionHidden
(
action
,
rowContext
));
return
(
<
TableRow
key=
{
recordId
}
className=
"align-top"
>
{
selectable
?
(
<
TableCell
className=
"align-top sticky left-0 z-30 bg-white shadow-[10px_0_20px_rgba(15,23,42,0.05)]"
style=
{
{
width
:
SELECTION_COLUMN_WIDTH
,
minWidth
:
SELECTION_COLUMN_WIDTH
}
}
>
<
div
className=
"flex justify-center pt-1"
>
<
Checkbox
checked=
{
selectedIds
.
includes
(
recordId
)
}
onChange=
{
(
checked
)
=>
{
if
(
checked
)
{
onSelectionChange
?.(
Array
.
from
(
new
Set
([...
selectedIds
,
recordId
])));
return
;
}
onSelectionChange
?.(
selectedIds
.
filter
((
id
)
=>
id
!==
recordId
));
}
}
color=
"rose"
aria
-
label=
{
`选择 ${getRecordLabel(record)}`
}
/>
</
div
>
</
TableCell
>
)
:
null
}
{
stickyColumns
.
orderedColumns
.
map
((
column
)
=>
{
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
column
.
field
];
return
(
<
TableCell
key=
{
column
.
field
}
style=
{
{
width
:
column
.
width
||
180
,
minWidth
:
Math
.
min
(
Math
.
max
(
column
.
width
||
180
,
150
),
360
),
left
:
stickyMeta
?.
side
===
'left'
?
`${stickyMeta.offset}px`
:
undefined
,
right
:
stickyMeta
?.
side
===
'right'
?
`${stickyMeta.offset}px`
:
undefined
,
}
}
className=
{
clsx
(
'align-top'
,
getStickyCellClass
(
stickyMeta
))
}
>
{
(()
=>
{
if
(
column
.
render
)
{
return
column
.
render
(
record
,
column
);
}
if
(
renderCell
)
{
const
customNode
=
renderCell
({
record
,
column
});
if
(
customNode
!==
undefined
)
{
return
customNode
;
}
}
return
defaultRenderCell
(
record
,
column
);
})()
}
</
TableCell
>
);
})
}
{
visibleRowActions
.
length
>
0
?
(
<
TableCell
className=
"align-top sticky right-0 z-30 bg-white shadow-[-10px_0_20px_rgba(15,23,42,0.05)]"
style=
{
{
width
:
actionColumnWidth
,
minWidth
:
actionColumnWidth
}
}
>
<
div
className=
"flex min-w-28 flex-col gap-2"
>
{
visibleRowActions
.
map
((
action
)
=>
{
const
disabled
=
isActionDisabled
(
action
,
rowContext
);
const
buttonProps
=
renderButtonProps
(
action
);
return
(
<
Button
key=
{
action
.
key
}
{
...
buttonProps
}
disabled=
{
disabled
}
onClick=
{
()
=>
action
.
onClick
?.(
rowContext
)
}
>
{
action
.
label
}
</
Button
>
);
})
}
</
div
>
</
TableCell
>
)
:
null
}
</
TableRow
>
);
})
}
</
TableBody
>
</
Table
>
</
div
>
)
}
</
section
>
{
drawerRecordName
&&
detailPanel
?.
render
?
detailPanel
.
render
({
open
:
Boolean
(
drawerRecordName
),
recordName
:
drawerRecordName
,
onClose
:
closeDetail
,
onDataChange
:
()
=>
setDrawerDirty
(
true
),
})
:
null
}
</
div
>
);
}
src/pages/RecordDetailDrawer/hooks/useSubjectRecord.js
View file @
dcdc9689
...
@@ -39,7 +39,7 @@ function transformSaveValue(newValue, fieldDef) {
...
@@ -39,7 +39,7 @@ function transformSaveValue(newValue, fieldDef) {
return
newValue
;
return
newValue
;
}
}
export
function
useSubjectRecord
(
recordName
,
refreshKey
,
readonly
)
{
export
function
useSubjectRecord
(
recordName
,
refreshKey
,
readonly
,
subjectName
)
{
const
[
remoteRecord
,
setRemoteRecord
]
=
useState
(
null
);
const
[
remoteRecord
,
setRemoteRecord
]
=
useState
(
null
);
const
[
editableRecord
,
setEditableRecord
]
=
useState
(
null
);
const
[
editableRecord
,
setEditableRecord
]
=
useState
(
null
);
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
subject
,
setSubject
]
=
useState
(
null
);
...
@@ -67,9 +67,9 @@ export function useSubjectRecord(recordName, refreshKey, readonly) {
...
@@ -67,9 +67,9 @@ export function useSubjectRecord(recordName, refreshKey, readonly) {
try
{
try
{
const
record
=
await
loadRecord
(
recordName
);
const
record
=
await
loadRecord
(
recordName
);
const
subjectName
=
record
?.
__t
||
record
?.
subject
?.
name
||
record
?.
subject
;
const
resolvedSubjectName
=
subjectName
||
record
?.
__t
||
record
?.
subject
?.
name
||
record
?.
subject
;
const
[
subjectData
,
viewsData
]
=
s
ubjectName
const
[
subjectData
,
viewsData
]
=
resolvedS
ubjectName
?
await
Promise
.
all
([
loadSubject
(
subjectName
),
getSubjectView
(
s
ubjectName
)])
?
await
Promise
.
all
([
loadSubject
(
resolvedSubjectName
),
getSubjectView
(
resolvedS
ubjectName
)])
:
[
null
,
[]];
:
[
null
,
[]];
if
(
!
cancelled
&&
requestIdRef
.
current
===
requestId
)
{
if
(
!
cancelled
&&
requestIdRef
.
current
===
requestId
)
{
...
@@ -98,7 +98,7 @@ export function useSubjectRecord(recordName, refreshKey, readonly) {
...
@@ -98,7 +98,7 @@ export function useSubjectRecord(recordName, refreshKey, readonly) {
return
()
=>
{
return
()
=>
{
cancelled
=
true
;
cancelled
=
true
;
};
};
},
[
recordName
,
refreshKey
]);
},
[
recordName
,
refreshKey
,
subjectName
]);
const
handleFieldUpdate
=
async
(
fieldPath
,
newValue
,
fieldDef
)
=>
{
const
handleFieldUpdate
=
async
(
fieldPath
,
newValue
,
fieldDef
)
=>
{
if
(
readonly
||
!
editableRecord
)
{
if
(
readonly
||
!
editableRecord
)
{
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
dcdc9689
...
@@ -59,7 +59,8 @@ const RecordDetailDrawer = (props, ref) => {
...
@@ -59,7 +59,8 @@ const RecordDetailDrawer = (props, ref) => {
width
=
860
,
width
=
860
,
open
,
open
,
recordName
,
recordName
,
setCustomComponentClose
,
subjectName
,
onClose
,
onDataChange
,
onDataChange
,
readonly
=
false
,
readonly
=
false
,
filterView
=
[],
filterView
=
[],
...
@@ -79,7 +80,7 @@ const RecordDetailDrawer = (props, ref) => {
...
@@ -79,7 +80,7 @@ const RecordDetailDrawer = (props, ref) => {
handleFieldUpdate
,
handleFieldUpdate
,
isFieldDisabled
,
isFieldDisabled
,
loading
,
loading
,
}
=
useSubjectRecord
(
recordName
,
refreshKey
,
readonly
);
}
=
useSubjectRecord
(
recordName
,
refreshKey
,
readonly
,
subjectName
);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
open
)
{
if
(
open
)
{
...
@@ -156,7 +157,7 @@ const RecordDetailDrawer = (props, ref) => {
...
@@ -156,7 +157,7 @@ const RecordDetailDrawer = (props, ref) => {
];
];
const
closeDrawer
=
()
=>
{
const
closeDrawer
=
()
=>
{
setCustomComponent
Close
?.();
on
Close
?.();
};
};
return
(
return
(
...
...
src/pages/problem_rectification/archive/archiveWorkbenchService.js
0 → 100644
View file @
dcdc9689
import
{
countRecords
,
getSubjectView
,
loadRecord
,
loadSubject
,
queryRecords
}
from
'@/services/DataService'
;
import
{
buildViewColumns
,
checkViewCondition
,
formatFieldValue
,
getFieldValue
,
isViewAccessible
,
normalizeText
,
recordMatchesRestrictions
,
sortRecordsByView
,
}
from
'@/utils/subjectView'
;
export
const
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
=
'issue_rectification_archive'
;
export
const
PROBLEM_RECTIFICATION_ARCHIVE_RECORDS_SUBJECT
=
'problem_rectification_archive_records'
;
export
function
normalizeArchiveBatchRecord
(
record
)
{
const
referenceRecords
=
Array
.
isArray
(
record
?.
metadata
?.
reference_records
)
?
record
.
metadata
.
reference_records
:
[];
return
{
id
:
record
?.
_id
||
record
?.
name
||
''
,
name
:
record
?.
name
||
''
,
title
:
record
?.
title
||
record
?.
name
||
'归档批次'
,
archiveCount
:
referenceRecords
.
length
,
referenceRecords
,
raw
:
record
,
};
}
export
async
function
loadArchiveWorkbenchResources
()
{
const
[
archiveRecordsResult
,
archiveSubject
,
archiveViews
]
=
await
Promise
.
all
([
queryRecords
({
subject
:
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
,
page
:
0
,
page_size
:
1000
,
sort
:
'-updated_at -created_at'
,
mode
:
'full'
,
}),
loadSubject
(
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
),
getSubjectView
(
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
),
]);
const
records
=
(
archiveRecordsResult
?.
records
||
[]).
map
(
normalizeArchiveBatchRecord
);
return
{
records
,
subject
:
archiveSubject
,
views
:
archiveViews
||
[],
};
}
export
function
buildArchiveGridViews
(
views
=
[])
{
const
gridViews
=
views
.
filter
((
view
)
=>
view
.
type
===
'grid'
)
.
filter
((
view
)
=>
isViewAccessible
(
view
));
if
(
gridViews
.
length
>
0
)
{
return
gridViews
;
}
return
[
{
title
:
'全部批次'
,
type
:
'grid'
,
name
:
'all'
,
settings
:
{},
},
];
}
export
function
buildArchiveColumns
(
view
,
subject
)
{
return
buildViewColumns
(
view
,
subject
).
filter
((
column
)
=>
column
.
field
!==
'title'
||
!
column
.
hide
);
}
export
function
buildArchiveConfiguredView
(
records
,
filters
,
gridView
,
subject
)
{
const
keyword
=
normalizeText
(
filters
?.
keyword
).
toLowerCase
();
const
columns
=
buildArchiveColumns
(
gridView
,
subject
);
const
filteredRecords
=
records
.
filter
((
record
)
=>
{
const
rawRecord
=
record
.
raw
||
record
;
if
(
!
recordMatchesRestrictions
(
rawRecord
,
gridView
?.
settings
?.
restrictions
))
{
return
false
;
}
if
(
!
checkViewCondition
(
{
relationship
:
gridView
?.
settings
?.
filters
?.
relationship
||
'all'
,
rules
:
gridView
?.
settings
?.
filters
?.
conditions
||
[],
},
rawRecord
,
)
)
{
return
false
;
}
if
(
!
keyword
)
{
return
true
;
}
const
searchText
=
columns
.
map
((
column
)
=>
formatFieldValue
(
getFieldValue
(
rawRecord
,
column
.
field
),
column
.
fieldDef
))
.
join
(
' '
)
.
toLowerCase
();
return
searchText
.
includes
(
keyword
);
});
return
{
filteredRecords
:
sortRecordsByView
(
filteredRecords
,
gridView
),
columns
,
};
}
export
function
buildArchiveViewCountMap
(
records
,
filters
,
views
,
subject
)
{
return
views
.
reduce
((
counts
,
view
)
=>
{
counts
[
view
.
name
]
=
buildArchiveConfiguredView
(
records
,
filters
,
view
,
subject
).
filteredRecords
.
length
;
return
counts
;
},
{});
}
export
async
function
loadArchiveDetailMetrics
(
recordName
)
{
const
batchRecord
=
await
loadRecord
(
recordName
);
const
referenceRecordIds
=
Array
.
isArray
(
batchRecord
?.
metadata
?.
reference_records
)
?
batchRecord
.
metadata
.
reference_records
.
map
((
item
)
=>
(
typeof
item
===
'object'
?
item
.
_id
||
item
.
name
:
item
))
.
filter
(
Boolean
)
:
[];
const
detailCount
=
referenceRecordIds
.
length
>
0
?
await
countRecords
({
subject
:
PROBLEM_RECTIFICATION_ARCHIVE_RECORDS_SUBJECT
,
filter
:
{
_id
:
{
$in
:
referenceRecordIds
},
},
})
:
{
count
:
0
};
return
{
batchRecord
,
detailCount
:
detailCount
?.
count
||
0
,
referenceRecordIds
,
};
}
src/pages/problem_rectification/archive/index.jsx
View file @
dcdc9689
import
ModuleEmptyState
from
'../ModuleEmptyState'
;
import
RecordWorkbench
from
'@/components/record-workbench'
;
import
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
buildArchiveConfiguredView
,
buildArchiveGridViews
,
buildArchiveViewCountMap
,
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
,
loadArchiveWorkbenchResources
,
}
from
'./archiveWorkbenchService'
;
export
default
function
ProblemRectificationArchivePage
()
{
export
default
function
ProblemRectificationArchivePage
()
{
const
[
records
,
setRecords
]
=
useState
([]);
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
views
,
setViews
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
filters
,
setFilters
]
=
useState
({
keyword
:
''
,
});
const
[
activeViewName
,
setActiveViewName
]
=
useState
(
''
);
const
[
selectedIds
,
setSelectedIds
]
=
useState
([]);
const
reloadList
=
useCallback
(
async
()
=>
{
setLoading
(
true
);
setError
(
''
);
try
{
const
resources
=
await
loadArchiveWorkbenchResources
();
setRecords
(
resources
.
records
||
[]);
setSubject
(
resources
.
subject
||
null
);
setViews
(
resources
.
views
||
[]);
}
catch
(
requestError
)
{
setRecords
([]);
setSubject
(
null
);
setViews
([]);
setError
(
requestError
?.
message
||
'归档批次列表加载失败,请稍后重试。'
);
}
finally
{
setLoading
(
false
);
}
},
[]);
useEffect
(()
=>
{
reloadList
();
},
[
reloadList
]);
const
gridViews
=
useMemo
(()
=>
buildArchiveGridViews
(
views
),
[
views
]);
useEffect
(()
=>
{
if
(
gridViews
.
length
===
0
)
{
setActiveViewName
(
''
);
return
;
}
if
(
!
gridViews
.
some
((
view
)
=>
view
.
name
===
activeViewName
))
{
setActiveViewName
(
gridViews
[
0
].
name
);
}
},
[
gridViews
,
activeViewName
]);
const
activeGridView
=
useMemo
(
()
=>
gridViews
.
find
((
view
)
=>
view
.
name
===
activeViewName
)
||
gridViews
[
0
]
||
null
,
[
gridViews
,
activeViewName
],
);
const
configuredView
=
useMemo
(
()
=>
buildArchiveConfiguredView
(
records
,
filters
,
activeGridView
,
subject
),
[
records
,
filters
,
activeGridView
,
subject
],
);
const
filteredRecords
=
configuredView
.
filteredRecords
||
[];
useEffect
(()
=>
{
const
visibleIds
=
new
Set
(
filteredRecords
.
map
((
item
)
=>
item
.
id
));
setSelectedIds
((
current
)
=>
current
.
filter
((
id
)
=>
visibleIds
.
has
(
id
)));
},
[
filteredRecords
]);
const
gridViewCounts
=
useMemo
(
()
=>
buildArchiveViewCountMap
(
records
,
filters
,
gridViews
,
subject
),
[
records
,
filters
,
gridViews
,
subject
],
);
const
summaryItems
=
useMemo
(
()
=>
[
{
label
:
'归档批次数'
,
value
:
filteredRecords
.
length
,
hint
:
activeGridView
?.
title
?
`
${
activeGridView
.
title
}
视图`
:
'当前筛选结果'
,
tone
:
'amber'
,
},
{
label
:
'已归档明细数'
,
value
:
filteredRecords
.
reduce
((
total
,
item
)
=>
total
+
(
item
.
archiveCount
||
0
),
0
),
hint
:
'按当前视图统计关联快照数量'
,
tone
:
'emerald'
,
},
{
label
:
'当前已选批次'
,
value
:
selectedIds
.
length
,
hint
:
'支持后续批量操作扩展'
,
tone
:
'rose'
,
},
],
[
filteredRecords
,
selectedIds
.
length
,
activeGridView
],
);
return
(
return
(
<
ModuleEmptyState
<
RecordWorkbench
title=
"往期材料归档"
summaryItems=
{
summaryItems
}
description=
"查看归档批次和归档明细。"
views=
{
gridViews
.
map
((
view
)
=>
({
...
view
,
count
:
gridViewCounts
[
view
.
name
]
||
0
,
}))
}
activeViewName=
{
activeGridView
?.
name
||
''
}
onViewChange=
{
(
viewName
)
=>
{
setActiveViewName
(
viewName
);
setSelectedIds
([]);
}
}
filters=
{
filters
}
filterSchema=
{
[
{
key
:
'keyword'
,
label
:
'关键词搜索'
,
type
:
'text'
,
placeholder
:
'搜索归档批次标题、编码或关联字段...'
,
},
]
}
onFiltersChange=
{
(
key
,
value
)
=>
setFilters
((
current
)
=>
({
...
current
,
[
key
]:
value
}))
}
onClearFilters=
{
()
=>
{
setFilters
({
keyword
:
''
});
setSelectedIds
([]);
}
}
title=
"归档批次"
description=
{
`当前视图显示 ${filteredRecords.length} 条批次记录,已选中 ${selectedIds.length} 条。`
}
columns=
{
configuredView
.
columns
||
[]
}
records=
{
filteredRecords
}
selectedIds=
{
selectedIds
}
onSelectionChange=
{
setSelectedIds
}
toolbarActions=
{
[
{
key
:
'archive-export'
,
label
:
'导出批次'
,
variant
:
'outline'
,
disabled
:
true
,
onClick
:
()
=>
{},
},
{
key
:
'archive-compare'
,
label
:
'批次对比'
,
variant
:
'outline'
,
disabled
:
true
,
onClick
:
()
=>
{},
},
]
}
rowActions=
{
[
{
key
:
'view-detail'
,
label
:
'查看详情'
,
variant
:
'plain'
,
onClick
:
({
openDetail
,
record
})
=>
openDetail
(
record
),
},
]
}
detailPanel=
{
{
getRecordName
:
(
record
)
=>
record
.
name
||
record
.
id
,
onDirtyClose
:
reloadList
,
render
:
({
open
,
recordName
,
onClose
,
onDataChange
})
=>
(
<
RecordDetailDrawer
open=
{
open
}
recordName=
{
recordName
}
subjectName=
{
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
}
onClose=
{
onClose
}
onDataChange=
{
onDataChange
}
preferredViewName=
{
activeGridView
?.
title
||
activeGridView
?.
name
}
/>
),
}
}
states=
{
{
loading
,
error
,
loadingTitle
:
'正在加载归档批次'
,
loadingDescription
:
'正在读取归档对象、视图配置和批次记录,请稍候。'
,
emptyTitle
:
'暂无归档批次'
,
emptyDescription
:
'当前还没有归档批次记录,后续完成“一键归档”后会在这里展示。'
,
}
}
renderCell=
{
({
record
,
column
})
=>
{
if
(
column
.
field
===
'metadata.reference_records'
)
{
return
(
<
div
className=
"whitespace-normal break-words text-sm text-slate-700"
>
{
record
.
archiveCount
||
0
}
条明细
</
div
>
);
}
return
column
.
render
?
column
.
render
(
record
,
column
)
:
null
;
}
}
/>
/>
);
);
}
}
src/pages/problem_rectification/dashboard/index.jsx
View file @
dcdc9689
...
@@ -4,6 +4,8 @@ import dayjs from 'dayjs';
...
@@ -4,6 +4,8 @@ import dayjs from 'dayjs';
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
loadProblemRectificationDashboardData
}
from
'../problemRectificationDashboardService'
;
import
{
loadProblemRectificationDashboardData
}
from
'../problemRectificationDashboardService'
;
const
CHART_COLOR_SET
=
[
'#e11d48'
,
'#fb7185'
,
'#f97316'
,
'#f59e0b'
,
'#22c55e'
,
'#0ea5e9'
,
'#8b5cf6'
];
function
formatPercent
(
value
)
{
function
formatPercent
(
value
)
{
const
numericValue
=
Number
(
value
||
0
);
const
numericValue
=
Number
(
value
||
0
);
if
(
!
Number
.
isFinite
(
numericValue
))
{
if
(
!
Number
.
isFinite
(
numericValue
))
{
...
@@ -20,6 +22,14 @@ function formatCount(value) {
...
@@ -20,6 +22,14 @@ function formatCount(value) {
return
`
${
numericValue
}
`
;
return
`
${
numericValue
}
`
;
}
}
function
formatScore
(
value
)
{
const
numericValue
=
Number
(
value
||
0
);
if
(
!
Number
.
isFinite
(
numericValue
))
{
return
'0.00'
;
}
return
numericValue
.
toFixed
(
2
);
}
function
toneCardClass
(
tone
)
{
function
toneCardClass
(
tone
)
{
if
(
tone
===
'rose'
)
{
if
(
tone
===
'rose'
)
{
return
'border-rose-200 bg-[linear-gradient(135deg,rgba(255,241,242,0.95),rgba(255,255,255,0.9))]'
;
return
'border-rose-200 bg-[linear-gradient(135deg,rgba(255,241,242,0.95),rgba(255,255,255,0.9))]'
;
...
@@ -36,6 +46,20 @@ function toneCardClass(tone) {
...
@@ -36,6 +46,20 @@ function toneCardClass(tone) {
return
'border-zinc-200 bg-[linear-gradient(135deg,rgba(250,250,250,0.95),rgba(255,255,255,0.92))]'
;
return
'border-zinc-200 bg-[linear-gradient(135deg,rgba(250,250,250,0.95),rgba(255,255,255,0.92))]'
;
}
}
function
SectionHeading
({
eyebrow
,
title
,
description
})
{
return
(
<
div
className=
"space-y-2"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.24em] text-rose-500"
>
{
eyebrow
}
</
div
>
<
div
className=
"text-2xl font-semibold tracking-tight text-slate-950"
>
{
title
}
</
div
>
{
description
?
(
<
div
className=
"max-w-4xl text-sm leading-6 text-slate-500"
>
{
description
}
</
div
>
)
:
null
}
</
div
>
);
}
function
StatCard
({
label
,
value
,
hint
,
tone
=
'default'
})
{
function
StatCard
({
label
,
value
,
hint
,
tone
=
'default'
})
{
return
(
return
(
<
div
<
div
...
@@ -45,27 +69,28 @@ function StatCard({ label, value, hint, tone = 'default' }) {
...
@@ -45,27 +69,28 @@ function StatCard({ label, value, hint, tone = 'default' }) {
)
}
)
}
>
>
<
div
className=
"text-sm font-medium text-slate-500"
>
{
label
}
</
div
>
<
div
className=
"text-sm font-medium text-slate-500"
>
{
label
}
</
div
>
<
div
className=
"mt-4 text-4xl font-semibold tracking-tight text-slate-900"
>
<
div
className=
"mt-4 text-4xl font-semibold tracking-tight text-slate-900"
>
{
value
}
</
div
>
{
value
}
</
div
>
<
div
className=
"mt-2 text-sm leading-6 text-slate-500"
>
{
hint
}
</
div
>
<
div
className=
"mt-2 text-sm leading-6 text-slate-500"
>
{
hint
}
</
div
>
</
div
>
</
div
>
);
);
}
}
function
Panel
({
title
,
description
,
children
,
aside
})
{
function
NumberCard
({
label
,
value
,
hint
})
{
return
(
<
div
className=
"rounded-[24px] border border-zinc-200 bg-zinc-50/70 p-5"
>
<
div
className=
"text-sm font-medium text-slate-500"
>
{
label
}
</
div
>
<
div
className=
"mt-3 text-3xl font-semibold tracking-tight text-slate-950"
>
{
value
}
</
div
>
{
hint
?
<
div
className=
"mt-2 text-sm text-slate-500"
>
{
hint
}
</
div
>
:
null
}
</
div
>
);
}
function
Panel
({
title
,
description
,
children
})
{
return
(
return
(
<
section
className=
"rounded-[30px] border border-zinc-200 bg-white/95 shadow-[0_20px_50px_rgba(15,23,42,0.06)]"
>
<
section
className=
"rounded-[30px] border border-zinc-200 bg-white/95 shadow-[0_20px_50px_rgba(15,23,42,0.06)]"
>
<
div
className=
"flex flex-wrap items-start justify-between gap-4 border-b border-zinc-100 px-6 py-5"
>
<
div
className=
"border-b border-zinc-100 px-6 py-5"
>
<
div
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
{
title
}
</
div
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
{
title
}
</
div
>
{
description
?
<
div
className=
"mt-1 text-sm leading-6 text-slate-500"
>
{
description
}
</
div
>
:
null
}
{
description
?
(
<
div
className=
"mt-1 text-sm leading-6 text-slate-500"
>
{
description
}
</
div
>
)
:
null
}
</
div
>
{
aside
?
<
div
>
{
aside
}
</
div
>
:
null
}
</
div
>
</
div
>
<
div
className=
"px-6 py-6"
>
{
children
}
</
div
>
<
div
className=
"px-6 py-6"
>
{
children
}
</
div
>
</
section
>
</
section
>
...
@@ -80,69 +105,189 @@ function EmptyBlock({ title }) {
...
@@ -80,69 +105,189 @@ function EmptyBlock({ title }) {
);
);
}
}
function
ProblemListCard
({
title
,
description
,
items
=
[],
emptyText
})
{
function
FocusIssueList
({
title
,
description
,
items
=
[],
tone
})
{
const
toneClass
=
tone
===
'rose'
?
'bg-rose-50 text-rose-700'
:
tone
===
'amber'
?
'bg-amber-50 text-amber-700'
:
'bg-sky-50 text-sky-700'
;
return
(
return
(
<
Panel
title=
{
title
}
description=
{
description
}
>
<
Panel
title=
{
title
}
description=
{
description
}
>
{
items
.
length
>
0
?
(
{
items
.
length
>
0
?
(
<
div
className=
"
grid gap-4 xl:grid-cols-2
"
>
<
div
className=
"
space-y-3
"
>
{
items
.
map
((
item
)
=>
(
{
items
.
map
((
item
)
=>
(
<
article
<
article
key=
{
item
.
id
}
key=
{
item
.
id
}
className=
"rounded-[2
4px] border border-zinc-200 bg-[linear-gradient(180deg,#ffffff,rgba(248,250,252,0.92))] p-5
shadow-sm"
className=
"rounded-[2
2px] border border-zinc-200 bg-[linear-gradient(180deg,#ffffff,rgba(248,250,252,0.92))] p-4
shadow-sm"
>
>
<
div
className=
"flex items-start justify-between gap-
4
"
>
<
div
className=
"flex items-start justify-between gap-
3
"
>
<
div
className=
"min-w-0"
>
<
div
className=
"min-w-0"
>
<
div
className=
"truncate text-lg font-semibold text-slate-900"
>
<
div
className=
"truncate text-base font-semibold text-slate-900"
>
{
item
.
title
}
</
div
>
{
item
.
title
}
<
div
className=
"mt-1 text-sm text-slate-500"
>
{
item
.
feedbackUnit
}
</
div
>
</
div
>
<
div
className=
"mt-1 text-sm text-slate-500"
>
{
item
.
feedbackUnit
||
'未归类单位'
}
</
div
>
</
div
>
</
div
>
<
div
className=
"rounded-full border border-rose-100 bg-rose-50 px-3 py-1 text-xs font-medium text-rose-700"
>
<
div
className=
{
clsx
(
'rounded-full px-3 py-1 text-xs font-medium'
,
toneClass
)
}
>
{
item
.
dueDate
Label
}
{
item
.
extra
Label
}
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"mt-3 line-clamp-2 text-sm leading-6 text-slate-600"
>
{
item
.
brief
}
</
div
>
<
div
className=
"mt-4 line-clamp-2 text-sm leading-6 text-slate-600"
>
<
div
className=
"mt-4 flex flex-wrap gap-2"
>
{
item
.
brief
}
<
span
className=
"rounded-full bg-zinc-100 px-3 py-1 text-xs font-medium text-zinc-700"
>
</
div
>
严重程度
{
item
.
severityLevel
}
</
span
>
<
div
className=
"mt-5 flex flex-wrap gap-2"
>
<
span
className=
"rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700"
>
<
span
className=
"rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-600"
>
截止
{
item
.
dueDateLabel
}
{
item
.
progress
||
'未填写进度'
}
</
span
>
</
span
>
{
item
.
problemType
?
(
<
span
className=
"rounded-full bg-amber-50 px-3 py-1 text-xs font-medium text-amber-700"
>
{
item
.
problemType
}
</
span
>
)
:
null
}
{
item
.
relatedIssueDomain
?
(
<
span
className=
"rounded-full bg-sky-50 px-3 py-1 text-xs font-medium text-sky-700"
>
{
item
.
relatedIssueDomain
}
</
span
>
)
:
null
}
</
div
>
</
div
>
</
article
>
</
article
>
))
}
))
}
</
div
>
</
div
>
)
:
(
)
:
(
<
EmptyBlock
title=
{
emptyText
}
/>
<
EmptyBlock
title=
"当前没有相关重点问题"
/>
)
}
)
}
</
Panel
>
</
Panel
>
);
);
}
}
function
build
HorizontalBar
Option
(
data
,
colors
)
{
function
build
Donut
Option
(
data
,
colors
)
{
return
{
return
{
color
:
colors
,
color
:
colors
,
tooltip
:
{
trigger
:
'item'
,
backgroundColor
:
'rgba(15,23,42,0.9)'
,
borderWidth
:
0
,
textStyle
:
{
color
:
'#fff'
},
},
legend
:
{
bottom
:
0
,
icon
:
'circle'
,
textStyle
:
{
color
:
'#475569'
},
},
series
:
[
{
type
:
'pie'
,
radius
:
[
'52%'
,
'74%'
],
center
:
[
'50%'
,
'42%'
],
itemStyle
:
{
borderRadius
:
12
,
borderColor
:
'#fff'
,
borderWidth
:
4
,
},
label
:
{
color
:
'#334155'
,
formatter
:
'{b|{b}}
\
n{c|{c} 条}'
,
rich
:
{
b
:
{
fontSize
:
12
,
fontWeight
:
600
,
color
:
'#334155'
},
c
:
{
fontSize
:
11
,
color
:
'#64748b'
},
},
},
labelLine
:
{
lineStyle
:
{
color
:
'#cbd5e1'
},
},
data
,
},
],
};
}
function
buildTreemapOption
(
data
=
[])
{
return
{
tooltip
:
{
formatter
:
(
params
)
=>
`
${
params
.
name
}
:
${
params
.
value
||
0
}
条`
,
backgroundColor
:
'rgba(15,23,42,0.9)'
,
borderWidth
:
0
,
textStyle
:
{
color
:
'#fff'
},
},
series
:
[
{
type
:
'treemap'
,
roam
:
false
,
breadcrumb
:
{
show
:
false
},
nodeClick
:
false
,
upperLabel
:
{
show
:
true
,
height
:
26
,
color
:
'#0f172a'
,
},
label
:
{
show
:
true
,
color
:
'#fff'
,
fontSize
:
12
,
formatter
:
'{b}'
,
},
itemStyle
:
{
borderColor
:
'#fff'
,
borderWidth
:
3
,
gapWidth
:
3
,
},
levels
:
[
{
color
:
[
'#e11d48'
,
'#f97316'
,
'#0ea5e9'
,
'#22c55e'
,
'#8b5cf6'
,
'#f59e0b'
],
colorMappingBy
:
'index'
,
},
{
colorSaturation
:
[
0.4
,
0.75
],
itemStyle
:
{
borderColorSaturation
:
0.7
,
gapWidth
:
2
,
borderWidth
:
2
,
},
},
],
data
,
},
],
};
}
function
buildStackedSourceOption
(
sourceComparison
)
{
return
{
color
:
CHART_COLOR_SET
,
tooltip
:
{
trigger
:
'axis'
,
axisPointer
:
{
type
:
'shadow'
},
backgroundColor
:
'rgba(15,23,42,0.9)'
,
borderWidth
:
0
,
textStyle
:
{
color
:
'#fff'
},
},
legend
:
{
top
:
0
,
textStyle
:
{
color
:
'#475569'
},
},
grid
:
{
grid
:
{
left
:
8
,
left
:
8
,
right
:
20
,
right
:
16
,
top
:
12
,
top
:
48
,
bottom
:
8
,
bottom
:
8
,
containLabel
:
true
,
containLabel
:
true
,
},
},
xAxis
:
{
type
:
'category'
,
data
:
sourceComparison
.
categories
,
axisTick
:
{
show
:
false
},
axisLine
:
{
lineStyle
:
{
color
:
'#e2e8f0'
}
},
axisLabel
:
{
color
:
'#64748b'
},
},
yAxis
:
{
type
:
'value'
,
splitLine
:
{
lineStyle
:
{
color
:
'rgba(148,163,184,0.18)'
}
},
axisLabel
:
{
color
:
'#64748b'
},
},
series
:
sourceComparison
.
series
.
map
((
series
)
=>
({
name
:
series
.
name
,
type
:
'bar'
,
stack
:
'total'
,
barMaxWidth
:
34
,
emphasis
:
{
focus
:
'series'
},
data
:
series
.
data
,
})),
};
}
function
buildHorizontalBarOption
(
data
,
color
)
{
return
{
color
:
[
color
],
tooltip
:
{
tooltip
:
{
trigger
:
'axis'
,
trigger
:
'axis'
,
axisPointer
:
{
type
:
'shadow'
},
axisPointer
:
{
type
:
'shadow'
},
...
@@ -150,33 +295,30 @@ function buildHorizontalBarOption(data, colors) {
...
@@ -150,33 +295,30 @@ function buildHorizontalBarOption(data, colors) {
borderWidth
:
0
,
borderWidth
:
0
,
textStyle
:
{
color
:
'#fff'
},
textStyle
:
{
color
:
'#fff'
},
},
},
grid
:
{
left
:
8
,
right
:
20
,
top
:
12
,
bottom
:
8
,
containLabel
:
true
,
},
xAxis
:
{
xAxis
:
{
type
:
'value'
,
type
:
'value'
,
splitLine
:
{
splitLine
:
{
lineStyle
:
{
color
:
'rgba(148,163,184,0.18)'
}
},
lineStyle
:
{
axisLabel
:
{
color
:
'#64748b'
},
color
:
'rgba(148,163,184,0.18)'
,
},
},
axisLabel
:
{
color
:
'#64748b'
,
},
},
},
yAxis
:
{
yAxis
:
{
type
:
'category'
,
type
:
'category'
,
data
:
data
.
map
((
item
)
=>
item
.
name
),
data
:
data
.
map
((
item
)
=>
item
.
name
),
axisTick
:
{
show
:
false
},
axisTick
:
{
show
:
false
},
axisLine
:
{
show
:
false
},
axisLine
:
{
show
:
false
},
axisLabel
:
{
axisLabel
:
{
color
:
'#334155'
,
fontSize
:
12
},
color
:
'#334155'
,
fontSize
:
12
,
},
},
},
series
:
[
series
:
[
{
{
type
:
'bar'
,
type
:
'bar'
,
data
:
data
.
map
((
item
)
=>
item
.
value
),
data
:
data
.
map
((
item
)
=>
item
.
value
),
barWidth
:
16
,
barWidth
:
18
,
roundCap
:
true
,
showBackground
:
true
,
showBackground
:
true
,
backgroundStyle
:
{
backgroundStyle
:
{
color
:
'rgba(148,163,184,0.08)'
,
color
:
'rgba(148,163,184,0.08)'
,
...
@@ -196,110 +338,275 @@ function buildHorizontalBarOption(data, colors) {
...
@@ -196,110 +338,275 @@ function buildHorizontalBarOption(data, colors) {
};
};
}
}
function
buildDonutOption
(
data
,
colors
)
{
function
buildBubbleMatrixOption
(
data
=
[])
{
const
maxTotal
=
Math
.
max
(...
data
.
map
((
item
)
=>
item
.
totalCount
),
1
);
const
maxOverdue
=
Math
.
max
(...
data
.
map
((
item
)
=>
item
.
overdueCount
),
1
);
return
{
return
{
color
:
colors
,
tooltip
:
{
tooltip
:
{
trigger
:
'item'
,
formatter
:
(
params
)
=>
{
const
item
=
params
.
data
;
return
[
`<strong>
${
item
.
name
}
</strong>`
,
`问题总数:
${
item
.
totalCount
}
`
,
`整改完成率:
${
formatPercent
(
item
.
completionRate
)}
`
,
`逾期未完成:
${
item
.
overdueCount
}
`
,
].
join
(
'<br/>'
);
},
backgroundColor
:
'rgba(15,23,42,0.9)'
,
backgroundColor
:
'rgba(15,23,42,0.9)'
,
borderWidth
:
0
,
borderWidth
:
0
,
textStyle
:
{
color
:
'#fff'
},
textStyle
:
{
color
:
'#fff'
},
},
},
legend
:
{
grid
:
{
bottom
:
0
,
left
:
8
,
icon
:
'circle'
,
right
:
16
,
textStyle
:
{
top
:
12
,
color
:
'#475569'
,
bottom
:
10
,
},
containLabel
:
true
,
},
xAxis
:
{
type
:
'value'
,
name
:
'问题总数'
,
nameTextStyle
:
{
color
:
'#64748b'
},
splitLine
:
{
lineStyle
:
{
color
:
'rgba(148,163,184,0.18)'
}
},
axisLabel
:
{
color
:
'#64748b'
},
},
yAxis
:
{
type
:
'value'
,
name
:
'整改完成率(%)'
,
nameTextStyle
:
{
color
:
'#64748b'
},
min
:
0
,
max
:
100
,
splitLine
:
{
lineStyle
:
{
color
:
'rgba(148,163,184,0.18)'
}
},
axisLabel
:
{
color
:
'#64748b'
},
},
},
series
:
[
series
:
[
{
{
type
:
'pie'
,
type
:
'scatter'
,
radius
:
[
'54%'
,
'74%'
],
data
:
data
.
map
((
item
)
=>
({
center
:
[
'50%'
,
'44%'
],
...
item
,
itemStyle
:
{
value
:
[
item
.
totalCount
,
item
.
completionRate
],
borderRadius
:
12
,
symbolSize
:
16
+
(
item
.
overdueCount
/
maxOverdue
)
*
34
,
borderColor
:
'#fff'
,
itemStyle
:
{
borderWidth
:
4
,
color
:
},
item
.
completionRate
>=
75
?
'#22c55e'
:
item
.
completionRate
>=
45
?
'#f59e0b'
:
'#e11d48'
,
opacity
:
0.85
,
},
})),
label
:
{
label
:
{
show
:
true
,
formatter
:
(
params
)
=>
params
.
data
.
name
,
position
:
'top'
,
color
:
'#334155'
,
color
:
'#334155'
,
formatter
:
'{b|{b}}
\
n{c|{c} 条}'
,
fontSize
:
12
,
rich
:
{
b
:
{
fontSize
:
12
,
fontWeight
:
600
,
color
:
'#334155'
,
},
c
:
{
fontSize
:
11
,
color
:
'#64748b'
,
},
},
},
},
labelLine
:
{
},
lineStyle
:
{
],
color
:
'#cbd5e1'
,
graphic
:
[
},
{
type
:
'text'
,
right
:
14
,
top
:
8
,
style
:
{
text
:
`气泡大小 = 逾期数,最大问题数
${
maxTotal
}
`
,
fill
:
'#94a3b8'
,
fontSize
:
12
,
},
},
data
,
},
},
],
],
};
};
}
}
function
buildTrendAreaOption
(
progressItems
=
[])
{
function
buildTimelinessScatterOption
(
data
=
[])
{
const
maxValue
=
Math
.
max
(
...
data
.
flatMap
((
item
)
=>
[
item
.
value
?.[
0
]
||
0
,
item
.
value
?.[
1
]
||
0
]),
1
,
);
return
{
return
{
color
:
[
'#e11d48'
],
tooltip
:
{
tooltip
:
{
trigger
:
'axis'
,
formatter
:
(
params
)
=>
{
const
item
=
params
.
data
;
return
[
`<strong>
${
item
.
name
}
</strong>`
,
`预计整改时长:
${
item
.
value
[
0
]}
天`
,
`实际整改时长:
${
item
.
value
[
1
]}
天`
,
`严重程度:
${
item
.
value
[
2
]}
`
,
item
.
isDelayed
?
'判定:已逾期'
:
'判定:按期或提前完成'
,
].
join
(
'<br/>'
);
},
backgroundColor
:
'rgba(15,23,42,0.9)'
,
backgroundColor
:
'rgba(15,23,42,0.9)'
,
borderWidth
:
0
,
borderWidth
:
0
,
textStyle
:
{
color
:
'#fff'
},
textStyle
:
{
color
:
'#fff'
},
},
},
grid
:
{
grid
:
{
left
:
8
,
left
:
8
,
right
:
1
8
,
right
:
1
6
,
top
:
16
,
top
:
16
,
bottom
:
10
,
bottom
:
10
,
containLabel
:
true
,
containLabel
:
true
,
},
},
xAxis
:
{
xAxis
:
{
type
:
'category'
,
type
:
'value'
,
data
:
progressItems
.
map
((
item
)
=>
item
.
name
),
name
:
'预计整改时长(天)'
,
boundaryGap
:
false
,
nameTextStyle
:
{
color
:
'#64748b'
},
axisLine
:
{
lineStyle
:
{
color
:
'#e2e8f0'
}
},
splitLine
:
{
lineStyle
:
{
color
:
'rgba(148,163,184,0.18)'
}
},
axisTick
:
{
show
:
false
},
axisLabel
:
{
color
:
'#64748b'
},
axisLabel
:
{
color
:
'#64748b'
},
},
},
yAxis
:
{
yAxis
:
{
type
:
'value'
,
type
:
'value'
,
splitLine
:
{
name
:
'实际整改时长(天)'
,
lineStyle
:
{
color
:
'rgba(148,163,184,0.18)
'
},
nameTextStyle
:
{
color
:
'#64748b
'
},
},
splitLine
:
{
lineStyle
:
{
color
:
'rgba(148,163,184,0.18)'
}
},
axisLabel
:
{
color
:
'#64748b'
},
axisLabel
:
{
color
:
'#64748b'
},
},
},
series
:
[
series
:
[
{
type
:
'scatter'
,
data
:
data
.
map
((
item
)
=>
({
...
item
,
symbolSize
:
item
.
value
[
2
]
===
'高'
?
18
:
item
.
value
[
2
]
===
'中'
?
14
:
10
,
itemStyle
:
{
color
:
item
.
isDelayed
?
'#e11d48'
:
'#0ea5e9'
,
opacity
:
0.82
,
},
})),
},
{
{
type
:
'line'
,
type
:
'line'
,
data
:
progressItems
.
map
((
item
)
=>
item
.
value
),
data
:
[
smooth
:
true
,
[
0
,
0
],
symbol
:
'circle'
,
[
maxValue
,
maxValue
],
symbolSize
:
10
,
],
silent
:
true
,
symbol
:
'none'
,
lineStyle
:
{
lineStyle
:
{
width
:
4
,
color
:
'#94a3b8'
,
type
:
'dashed'
,
width
:
1.5
,
},
},
],
};
}
function
buildScoreBarOption
(
data
,
color
)
{
return
{
color
:
[
color
],
tooltip
:
{
trigger
:
'axis'
,
axisPointer
:
{
type
:
'shadow'
},
backgroundColor
:
'rgba(15,23,42,0.9)'
,
borderWidth
:
0
,
textStyle
:
{
color
:
'#fff'
},
},
grid
:
{
left
:
8
,
right
:
20
,
top
:
12
,
bottom
:
8
,
containLabel
:
true
,
},
xAxis
:
{
type
:
'value'
,
min
:
0
,
max
:
3
,
splitLine
:
{
lineStyle
:
{
color
:
'rgba(148,163,184,0.18)'
}
},
axisLabel
:
{
color
:
'#64748b'
},
},
yAxis
:
{
type
:
'category'
,
data
:
data
.
map
((
item
)
=>
item
.
name
),
axisTick
:
{
show
:
false
},
axisLine
:
{
show
:
false
},
axisLabel
:
{
color
:
'#334155'
,
fontSize
:
12
},
},
series
:
[
{
type
:
'bar'
,
data
:
data
.
map
((
item
)
=>
item
.
value
),
barWidth
:
18
,
showBackground
:
true
,
backgroundStyle
:
{
color
:
'rgba(148,163,184,0.08)'
,
borderRadius
:
99
,
},
itemStyle
:
{
borderRadius
:
[
99
,
99
,
99
,
99
],
},
label
:
{
show
:
true
,
position
:
'right'
,
formatter
:
(
params
)
=>
formatScore
(
params
.
value
),
color
:
'#0f172a'
,
fontWeight
:
600
,
},
},
areaStyle
:
{
},
color
:
{
],
type
:
'linear'
,
};
x
:
0
,
}
y
:
0
,
x2
:
0
,
function
buildHeatmapOption
(
heatmap
)
{
y2
:
1
,
return
{
colorStops
:
[
tooltip
:
{
{
offset
:
0
,
color
:
'rgba(225,29,72,0.32)'
},
formatter
:
(
params
)
=>
{
offset
:
1
,
color
:
'rgba(225,29,72,0.04)'
},
`严重程度
${
heatmap
.
xLabels
[
params
.
value
[
0
]]}
<br/>整改评级
${
heatmap
.
yLabels
[
params
.
value
[
1
]]}
<br/>问题数
${
params
.
value
[
2
]}
`
,
],
backgroundColor
:
'rgba(15,23,42,0.9)'
,
borderWidth
:
0
,
textStyle
:
{
color
:
'#fff'
},
},
grid
:
{
left
:
8
,
right
:
16
,
top
:
12
,
bottom
:
10
,
containLabel
:
true
,
},
xAxis
:
{
type
:
'category'
,
data
:
heatmap
.
xLabels
,
splitArea
:
{
show
:
true
},
axisLabel
:
{
color
:
'#334155'
},
axisTick
:
{
show
:
false
},
axisLine
:
{
show
:
false
},
},
yAxis
:
{
type
:
'category'
,
data
:
heatmap
.
yLabels
,
splitArea
:
{
show
:
true
},
axisLabel
:
{
color
:
'#334155'
},
axisTick
:
{
show
:
false
},
axisLine
:
{
show
:
false
},
},
visualMap
:
{
min
:
0
,
max
:
Math
.
max
(...
heatmap
.
data
.
map
((
item
)
=>
item
[
2
]),
1
),
calculable
:
false
,
orient
:
'horizontal'
,
left
:
'center'
,
bottom
:
0
,
textStyle
:
{
color
:
'#64748b'
},
inRange
:
{
color
:
[
'#fef3c7'
,
'#fb7185'
,
'#be123c'
],
},
},
series
:
[
{
type
:
'heatmap'
,
data
:
heatmap
.
data
,
label
:
{
show
:
true
,
color
:
'#0f172a'
,
fontWeight
:
600
,
},
emphasis
:
{
itemStyle
:
{
shadowBlur
:
12
,
shadowColor
:
'rgba(15,23,42,0.18)'
,
},
},
},
},
},
},
...
@@ -307,6 +614,83 @@ function buildTrendAreaOption(progressItems = []) {
...
@@ -307,6 +614,83 @@ function buildTrendAreaOption(progressItems = []) {
};
};
}
}
function
buildRatingStackOption
(
data
)
{
return
{
color
:
[
'#fda4af'
,
'#fbbf24'
,
'#4ade80'
],
tooltip
:
{
trigger
:
'axis'
,
axisPointer
:
{
type
:
'shadow'
},
backgroundColor
:
'rgba(15,23,42,0.9)'
,
borderWidth
:
0
,
textStyle
:
{
color
:
'#fff'
},
formatter
:
(
params
)
=>
{
const
title
=
params
[
0
]?.
axisValueLabel
||
''
;
const
lines
=
params
.
map
(
(
item
)
=>
`
${
item
.
seriesName
}
:
${
Number
(
item
.
value
).
toFixed
(
2
)}
%`
,
);
return
[
title
,
...
lines
].
join
(
'<br/>'
);
},
},
legend
:
{
top
:
0
,
textStyle
:
{
color
:
'#475569'
},
},
grid
:
{
left
:
8
,
right
:
16
,
top
:
44
,
bottom
:
8
,
containLabel
:
true
,
},
xAxis
:
{
type
:
'value'
,
max
:
100
,
axisLabel
:
{
color
:
'#64748b'
,
formatter
:
'{value}%'
,
},
splitLine
:
{
lineStyle
:
{
color
:
'rgba(148,163,184,0.18)'
}
},
},
yAxis
:
{
type
:
'category'
,
data
:
data
.
map
((
item
)
=>
item
.
name
),
axisTick
:
{
show
:
false
},
axisLine
:
{
show
:
false
},
axisLabel
:
{
color
:
'#334155'
,
fontSize
:
12
},
},
series
:
[
{
name
:
'低评级'
,
type
:
'bar'
,
stack
:
'rating'
,
barMaxWidth
:
24
,
data
:
data
.
map
((
item
)
=>
Number
(
item
.
lowPercent
.
toFixed
(
2
))),
},
{
name
:
'中评级'
,
type
:
'bar'
,
stack
:
'rating'
,
barMaxWidth
:
24
,
data
:
data
.
map
((
item
)
=>
Number
(
item
.
mediumPercent
.
toFixed
(
2
))),
},
{
name
:
'高评级'
,
type
:
'bar'
,
stack
:
'rating'
,
barMaxWidth
:
24
,
label
:
{
show
:
true
,
position
:
'right'
,
formatter
:
(
params
)
=>
`
${
params
.
value
}
%`
,
color
:
'#0f172a'
,
fontWeight
:
600
,
},
data
:
data
.
map
((
item
)
=>
Number
(
item
.
highPercent
.
toFixed
(
2
))),
},
],
};
}
export
default
function
ProblemRectificationDashboardPage
()
{
export
default
function
ProblemRectificationDashboardPage
()
{
const
[
activeGroupKey
,
setActiveGroupKey
]
=
useState
(
'all'
);
const
[
activeGroupKey
,
setActiveGroupKey
]
=
useState
(
'all'
);
const
[
dashboardData
,
setDashboardData
]
=
useState
({
const
[
dashboardData
,
setDashboardData
]
=
useState
({
...
@@ -339,83 +723,101 @@ export default function ProblemRectificationDashboardPage() {
...
@@ -339,83 +723,101 @@ export default function ProblemRectificationDashboardPage() {
const
summary
=
dashboardData
.
summaries
?.[
activeGroupKey
]
||
{
const
summary
=
dashboardData
.
summaries
?.[
activeGroupKey
]
||
{
totalCount
:
0
,
totalCount
:
0
,
fixedCount
:
0
,
fixedCount
:
0
,
historyRate
:
0
,
completionRate
:
0
,
currentYearRate
:
0
,
overdueCount
:
0
,
currentYearTotal
:
0
,
overdueRate
:
0
,
upcoming
:
[],
overdue
:
[],
feedbackUnitDistribution
:
[],
problemTypeDistribution
:
[],
progressDistribution
:
[],
progressDistribution
:
[],
domainDistribution
:
[],
categoryTreemap
:
[],
rootCauseDistribution
:
[],
sourceComparison
:
{
categories
:
[],
series
:
[]
},
focusIssueGroups
:
{
economicLoss
:
[],
externalImpact
:
[],
accountability
:
[],
},
overdueRanking
:
[],
departmentMatrix
:
[],
timelinessScatter
:
[],
timelinessScoreRanking
:
[],
qualityScoreRanking
:
[],
policyRevisionCount
:
0
,
systemConstructionCount
:
0
,
severityRatingHeatmap
:
{
xLabels
:
[],
yLabels
:
[],
data
:
[],
},
departmentRatingDistribution
:
[],
};
};
const
feedbackChartOption
=
useMemo
(
const
progressOption
=
useMemo
(
()
=>
()
=>
buildDonutOption
(
summary
.
progressDistribution
,
CHART_COLOR_SET
),
buildHorizontalBarOption
(
summary
.
feedbackUnitDistribution
.
slice
(
0
,
6
),
[
[
summary
.
progressDistribution
],
'#ef4444'
,
'#f97316'
,
'#fb7185'
,
'#f59e0b'
,
]),
[
summary
.
feedbackUnitDistribution
],
);
);
const
categoryOption
=
useMemo
(
const
problemTypeOption
=
useMemo
(
()
=>
buildTreemapOption
(
summary
.
categoryTreemap
),
()
=>
[
summary
.
categoryTreemap
],
buildHorizontalBarOption
(
summary
.
problemTypeDistribution
.
slice
(
0
,
6
),
[
'#0ea5e9'
,
'#14b8a6'
,
'#38bdf8'
,
'#8b5cf6'
,
]),
[
summary
.
problemTypeDistribution
],
);
);
const
rootCauseOption
=
useMemo
(
const
domainOption
=
useMemo
(
()
=>
buildDonutOption
(
summary
.
rootCauseDistribution
,
CHART_COLOR_SET
),
()
=>
[
summary
.
rootCauseDistribution
],
buildDonutOption
(
summary
.
domainDistribution
.
slice
(
0
,
6
),
[
'#f43f5e'
,
'#fb7185'
,
'#f97316'
,
'#f59e0b'
,
'#38bdf8'
,
'#22c55e'
,
]),
[
summary
.
domainDistribution
],
);
);
const
sourceComparisonOption
=
useMemo
(
const
progressTrendOption
=
useMemo
(
()
=>
buildStackedSourceOption
(
summary
.
sourceComparison
),
()
=>
buildTrendAreaOption
(
summary
.
progressDistribution
),
[
summary
.
sourceComparison
],
[
summary
.
progressDistribution
],
);
const
overdueRankingOption
=
useMemo
(
()
=>
buildHorizontalBarOption
(
summary
.
overdueRanking
,
'#e11d48'
),
[
summary
.
overdueRanking
],
);
const
departmentMatrixOption
=
useMemo
(
()
=>
buildBubbleMatrixOption
(
summary
.
departmentMatrix
),
[
summary
.
departmentMatrix
],
);
const
timelinessScatterOption
=
useMemo
(
()
=>
buildTimelinessScatterOption
(
summary
.
timelinessScatter
),
[
summary
.
timelinessScatter
],
);
const
timelinessScoreOption
=
useMemo
(
()
=>
buildScoreBarOption
(
summary
.
timelinessScoreRanking
,
'#f59e0b'
),
[
summary
.
timelinessScoreRanking
],
);
const
qualityScoreOption
=
useMemo
(
()
=>
buildScoreBarOption
(
summary
.
qualityScoreRanking
,
'#22c55e'
),
[
summary
.
qualityScoreRanking
],
);
const
heatmapOption
=
useMemo
(
()
=>
buildHeatmapOption
(
summary
.
severityRatingHeatmap
),
[
summary
.
severityRatingHeatmap
],
);
const
ratingStackOption
=
useMemo
(
()
=>
buildRatingStackOption
(
summary
.
departmentRatingDistribution
),
[
summary
.
departmentRatingDistribution
],
);
);
const
activeGroup
=
groups
.
find
((
item
)
=>
item
.
key
===
activeGroupKey
);
const
activeGroup
=
groups
.
find
((
item
)
=>
item
.
key
===
activeGroupKey
);
return
(
return
(
<
div
className=
"space-y-
6
"
>
<
div
className=
"space-y-
8
"
>
<
section
className=
"relative overflow-hidden rounded-[34px] border border-white/80 bg-[linear-gradient(135deg,rgba(124,24,24,0.96),rgba(190,24,93,0.92)_54%,rgba(244,63,94,0.9))] px-6 py-7 text-white shadow-[0_28px_80px_rgba(159,18,57,0.24)]"
>
<
section
className=
"relative overflow-hidden rounded-[34px] border border-white/80 bg-[linear-gradient(135deg,rgba(124,24,24,0.96),rgba(190,24,93,0.92)_54%,rgba(244,63,94,0.9))] px-6 py-7 text-white shadow-[0_28px_80px_rgba(159,18,57,0.24)]"
>
<
div
className=
"absolute inset-y-0 right-0 w-[36%] bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.28),transparent_45%)]"
/>
<
div
className=
"absolute inset-y-0 right-0 w-[36%] bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.28),transparent_45%)]"
/>
<
div
className=
"absolute -bottom-16 left-1/3 h-40 w-40 rounded-full bg-white/10 blur-3xl"
/>
<
div
className=
"absolute -bottom-16 left-1/3 h-40 w-40 rounded-full bg-white/10 blur-3xl"
/>
<
div
className=
"relative flex flex-wrap items-end justify-between gap-6"
>
<
div
className=
"relative flex flex-wrap items-end justify-between gap-6"
>
<
div
className=
"max-w-
3
xl"
>
<
div
className=
"max-w-
4
xl"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.28em] text-white/75"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.28em] text-white/75"
>
Integrity Risk Dashboard
Integrity Risk Dashboard
</
div
>
</
div
>
<
h1
className=
"mt-3 text-3xl font-semibold tracking-tight md:text-[2.55rem]"
>
<
h1
className=
"mt-3 text-3xl font-semibold tracking-tight md:text-[2.55rem]"
>
问题整改驾驶舱
问题整改驾驶舱
</
h1
>
</
h1
>
<
div
className=
"mt-4 max-w-3xl text-sm leading-6 text-white/78"
>
围绕整改完成进度、问题风险定位、部门责任压实和整改质效评估,统一展示今年问题整改全景。
</
div
>
</
div
>
</
div
>
<
div
className=
"rounded-[28px] border border-white/15 bg-white/10 px-5 py-4 backdrop-blur"
>
<
div
className=
"rounded-[28px] border border-white/15 bg-white/10 px-5 py-4 backdrop-blur"
>
<
div
className=
"text-xs uppercase tracking-[0.22em] text-white/65"
>
<
div
className=
"text-xs uppercase tracking-[0.22em] text-white/65"
>
当前分组
</
div
>
当前分组
<
div
className=
"mt-2 text-2xl font-semibold"
>
{
activeGroup
?.
label
||
'全部'
}
</
div
>
</
div
>
<
div
className=
"mt-1 text-sm text-white/72"
>
{
dayjs
().
format
(
'YYYY 年 MM 月 DD 日'
)
}
</
div
>
<
div
className=
"mt-2 text-2xl font-semibold"
>
{
activeGroup
?.
label
||
'全部'
}
</
div
>
<
div
className=
"mt-1 text-sm text-white/72"
>
{
dayjs
().
format
(
'YYYY 年 MM 月 DD 日'
)
}
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
section
>
</
section
>
...
@@ -454,101 +856,255 @@ export default function ProblemRectificationDashboardPage() {
...
@@ -454,101 +856,255 @@ export default function ProblemRectificationDashboardPage() {
{
!
loading
&&
dashboardData
.
isEmpty
?
(
{
!
loading
&&
dashboardData
.
isEmpty
?
(
<
div
className=
"rounded-[30px] border border-dashed border-zinc-300 bg-white/80 px-6 py-12 text-center"
>
<
div
className=
"rounded-[30px] border border-dashed border-zinc-300 bg-white/80 px-6 py-12 text-center"
>
<
div
className=
"text-xl font-semibold text-slate-900"
>
<
div
className=
"text-xl font-semibold text-slate-900"
>
暂无问题整改数据
</
div
>
暂无问题整改数据
<
div
className=
"mt-3 text-sm leading-6 text-slate-500"
>
当前没有可用于驾驶舱展示的整改记录。
</
div
>
</
div
>
<
div
className=
"mt-3 text-sm leading-6 text-slate-500"
>
暂无记录。
</
div
>
</
div
>
</
div
>
)
:
null
}
)
:
null
}
{
!
loading
&&
!
dashboardData
.
isEmpty
?
(
{
!
loading
&&
!
dashboardData
.
isEmpty
?
(
<>
<>
<
section
className=
"grid gap-4 md:grid-cols-2 xl:grid-cols-4"
>
<
section
className=
"space-y-5"
>
<
StatCard
<
SectionHeading
label=
"已整改问题"
eyebrow=
"Overview"
value=
{
formatCount
(
summary
.
fixedCount
)
}
title=
"宏观总览"
hint=
"状态为已整改或已优化的问题数量"
description=
"先看整体整改健康度,快速判断问题总量、闭环情况和逾期风险。"
tone=
"emerald"
/>
<
StatCard
label=
"问题总数"
value=
{
formatCount
(
summary
.
totalCount
)
}
hint=
"当前分组内全部问题记录"
tone=
"rose"
/>
<
StatCard
label=
"历史累计整改率"
value=
{
formatPercent
(
summary
.
historyRate
)
}
hint=
"已整改或已优化 / 问题总数"
tone=
"amber"
/>
<
StatCard
label=
"本年度整改率"
value=
{
formatPercent
(
summary
.
currentYearRate
)
}
hint=
{
`按检查实施时间归属本年度,共 ${formatCount(summary.currentYearTotal)} 条`
}
tone=
"sky"
/>
/>
<
div
className=
"grid gap-4 md:grid-cols-2 xl:grid-cols-5"
>
<
StatCard
label=
"问题总数"
value=
{
formatCount
(
summary
.
totalCount
)
}
hint=
"当前分组内全部问题记录"
tone=
"rose"
/>
<
StatCard
label=
"已完成整改数"
value=
{
formatCount
(
summary
.
fixedCount
)
}
hint=
"整改进度为已整改或已优化"
tone=
"emerald"
/>
<
StatCard
label=
"整改完成率"
value=
{
formatPercent
(
summary
.
completionRate
)
}
hint=
"已完成整改数 / 总问题数"
tone=
"sky"
/>
<
StatCard
label=
"逾期未完成数"
value=
{
formatCount
(
summary
.
overdueCount
)
}
hint=
"截止今天已超期且未闭环"
tone=
"amber"
/>
<
StatCard
label=
"逾期率"
value=
{
formatPercent
(
summary
.
overdueRate
)
}
hint=
"逾期未完成数 / 总问题数"
tone=
"amber"
/>
</
div
>
<
div
className=
"grid gap-6 xl:grid-cols-[0.9fr_1.1fr]"
>
<
Panel
title=
"整改进度分布"
description=
"按整改进度统计数量与占比,直观看当前积压主要落在哪个状态。"
>
{
summary
.
progressDistribution
.
length
>
0
?
(
<
ReactECharts
option=
{
progressOption
}
style=
{
{
height
:
340
}
}
/>
)
:
(
<
EmptyBlock
title=
"暂无整改进度分布数据"
/>
)
}
</
Panel
>
<
Panel
title=
"进度口径说明"
description=
"当前逾期判定规则采用“预计整改完成时间早于今天且尚未闭环”。如果后续上线显式“是否延期”字段,可直接切换为配置化口径。"
>
<
div
className=
"grid gap-4 md:grid-cols-2"
>
<
NumberCard
label=
"已闭环问题"
value=
{
formatCount
(
summary
.
fixedCount
)
}
hint=
"适合用于经营层快速判断整体闭环能力"
/>
<
NumberCard
label=
"待重点督办"
value=
{
formatCount
(
summary
.
overdueCount
)
}
hint=
"适合作为督办清单和预警入口"
/>
</
div
>
</
Panel
>
</
div
>
</
section
>
</
section
>
<
section
className=
"grid gap-6 xl:grid-cols-[1.15fr_0.85fr]"
>
<
section
className=
"space-y-5"
>
<
Panel
<
SectionHeading
title=
"整改进度分布"
eyebrow=
"Risk"
>
title=
"问题分析与风险定位"
{
summary
.
progressDistribution
.
length
>
0
?
(
description=
"把问题集中区域、根因和来源差异拉出来,支撑制度修订和内审效能判断。"
<
ReactECharts
option=
{
progressTrendOption
}
style=
{
{
height
:
320
}
}
/>
/>
)
:
(
<
div
className=
"grid gap-6 xl:grid-cols-[1.05fr_0.95fr]"
>
<
EmptyBlock
title=
"暂无整改进度分布数据"
/>
<
Panel
)
}
title=
"问题类型 / 业务领域分布"
</
Panel
>
description=
"按问题一级至四级分类构造矩形树图,帮助定位高发领域。"
>
{
summary
.
categoryTreemap
.
length
>
0
?
(
<
ReactECharts
option=
{
categoryOption
}
style=
{
{
height
:
360
}
}
/>
)
:
(
<
EmptyBlock
title=
"暂无问题领域统计数据"
/>
)
}
</
Panel
>
<
Panel
title=
"问题根本原因分析"
description=
"按问题原因分类统计,便于识别制度、流程或执行层面的共性成因。"
>
{
summary
.
rootCauseDistribution
.
length
>
0
?
(
<
ReactECharts
option=
{
rootCauseOption
}
style=
{
{
height
:
360
}
}
/>
)
:
(
<
EmptyBlock
title=
"暂无问题原因统计数据"
/>
)
}
</
Panel
>
</
div
>
<
Panel
<
Panel
title=
"涉及问题领域"
title=
"问题来源对比"
description=
"横轴为问题来源,堆叠展示检查实施单位,观察内外部检查发现问题的结构差异。"
>
>
{
summary
.
domainDistribution
.
length
>
0
?
(
{
summary
.
sourceComparison
.
categories
.
length
>
0
?
(
<
ReactECharts
option=
{
domainOption
}
style=
{
{
height
:
32
0
}
}
/>
<
ReactECharts
option=
{
sourceComparisonOption
}
style=
{
{
height
:
36
0
}
}
/>
)
:
(
)
:
(
<
EmptyBlock
title=
"暂无问题
领域统计
数据"
/>
<
EmptyBlock
title=
"暂无问题
来源对比
数据"
/>
)
}
)
}
</
Panel
>
</
Panel
>
<
div
className=
"grid gap-6 xl:grid-cols-3"
>
<
FocusIssueList
title=
"经济损失问题"
description=
"优先展示造成经济损失且严重程度更高的问题。"
items=
{
summary
.
focusIssueGroups
.
economicLoss
}
tone=
"rose"
/>
<
FocusIssueList
title=
"外部影响问题"
description=
"直观看到涉及处罚机构或外部影响的问题。"
items=
{
summary
.
focusIssueGroups
.
externalImpact
}
tone=
"amber"
/>
<
FocusIssueList
title=
"员工问责问题"
description=
"展示已触发员工问责后果的问题。"
items=
{
summary
.
focusIssueGroups
.
accountability
}
tone=
"sky"
/>
</
div
>
</
section
>
</
section
>
<
section
className=
"grid gap-6 xl:grid-cols-2"
>
<
section
className=
"space-y-5"
>
<
SectionHeading
eyebrow=
"Execution"
title=
"问题责任追踪与执行"
description=
"围绕责任部门看执行难点,快速识别逾期堆积、完成率偏低和预估时长失真的对象。"
/>
<
div
className=
"grid gap-6 xl:grid-cols-[0.9fr_1.1fr]"
>
<
Panel
title=
"逾期问题排行"
description=
"按责任部门统计逾期未完成问题数,用于识别长期挂账和重点督办对象。"
>
{
summary
.
overdueRanking
.
length
>
0
?
(
<
ReactECharts
option=
{
overdueRankingOption
}
style=
{
{
height
:
340
}
}
/>
)
:
(
<
EmptyBlock
title=
"当前没有逾期问题排行数据"
/>
)
}
</
Panel
>
<
Panel
title=
"部门整改责任矩阵"
description=
"X 轴为问题总数,Y 轴为整改完成率,气泡大小表示逾期问题数。"
>
{
summary
.
departmentMatrix
.
length
>
0
?
(
<
ReactECharts
option=
{
departmentMatrixOption
}
style=
{
{
height
:
340
}
}
/>
)
:
(
<
EmptyBlock
title=
"暂无部门责任矩阵数据"
/>
)
}
</
Panel
>
</
div
>
<
Panel
<
Panel
title=
"反馈单位分布"
title=
"整改时效分析"
description=
"X 轴为预计整改时长,Y 轴为实际整改时长。虚线右上方的点代表实际时长高于预估。"
>
>
{
summary
.
feedbackUnitDistribution
.
length
>
0
?
(
{
summary
.
timelinessScatter
.
length
>
0
?
(
<
ReactECharts
option=
{
feedbackChartOption
}
style=
{
{
height
:
34
0
}
}
/>
<
ReactECharts
option=
{
timelinessScatterOption
}
style=
{
{
height
:
38
0
}
}
/>
)
:
(
)
:
(
<
EmptyBlock
title=
"
暂无反馈单位分布数据
"
/>
<
EmptyBlock
title=
"
缺少预计整改时间、实际整改完成时间或检查完成日期,暂无法形成时效散点图
"
/>
)
}
)
}
</
Panel
>
</
Panel
>
</
section
>
<
section
className=
"space-y-5"
>
<
SectionHeading
eyebrow=
"Quality"
title=
"审计整改质效"
description=
"从整改时间评估、材料质量、制度与系统助力,以及风险和整改评级匹配度四个角度审视整改成效。"
/>
<
div
className=
"grid gap-6 xl:grid-cols-2"
>
<
Panel
title=
"问题整改时效"
description=
"按责任部门计算整改时间评估平均分,低=1,中=2,高=3。"
>
{
summary
.
timelinessScoreRanking
.
length
>
0
?
(
<
ReactECharts
option=
{
timelinessScoreOption
}
style=
{
{
height
:
360
}
}
/>
)
:
(
<
EmptyBlock
title=
"暂无整改时间评估数据"
/>
)
}
</
Panel
>
<
Panel
title=
"问题整改质量"
description=
"按责任部门计算整改材料质量平均分,便于识别形式整改和敷衍整改。"
>
{
summary
.
qualityScoreRanking
.
length
>
0
?
(
<
ReactECharts
option=
{
qualityScoreOption
}
style=
{
{
height
:
360
}
}
/>
)
:
(
<
EmptyBlock
title=
"暂无整改质量评估数据"
/>
)
}
</
Panel
>
</
div
>
<
div
className=
"grid gap-6 xl:grid-cols-[0.8fr_1.2fr]"
>
<
Panel
title=
"问题整改助力总结"
description=
"统计本年问题整改直接助力制度修订和系统建设的记录数量。"
>
<
div
className=
"grid gap-4 md:grid-cols-2"
>
<
NumberCard
label=
"助力制度修订"
value=
{
formatCount
(
summary
.
policyRevisionCount
)
}
hint=
"标记为涉及制度修订的问题数"
/>
<
NumberCard
label=
"助力系统建设"
value=
{
formatCount
(
summary
.
systemConstructionCount
)
}
hint=
"标记为涉及系统建设的问题数"
/>
</
div
>
</
Panel
>
<
Panel
title=
"综合整改评定等级 vs 问题综合严重程度"
description=
"观察重大问题是否得到了相匹配的整改投入和结果。"
>
{
summary
.
severityRatingHeatmap
.
data
.
length
>
0
?
(
<
ReactECharts
option=
{
heatmapOption
}
style=
{
{
height
:
360
}
}
/>
)
:
(
<
EmptyBlock
title=
"暂无严重程度与整改评级对照数据"
/>
)
}
</
Panel
>
</
div
>
<
Panel
<
Panel
title=
"问题类型排行"
title=
"部门综合整改评定等级排行"
description=
"按高评级占比降序展示各责任部门低、中、高评级结构,便于横向比较整改质量。"
>
>
{
summary
.
problemType
Distribution
.
length
>
0
?
(
{
summary
.
departmentRating
Distribution
.
length
>
0
?
(
<
ReactECharts
option=
{
problemTypeOption
}
style=
{
{
height
:
34
0
}
}
/>
<
ReactECharts
option=
{
ratingStackOption
}
style=
{
{
height
:
42
0
}
}
/>
)
:
(
)
:
(
<
EmptyBlock
title=
"暂无
问题类型
分布数据"
/>
<
EmptyBlock
title=
"暂无
部门整改评级
分布数据"
/>
)
}
)
}
</
Panel
>
</
Panel
>
</
section
>
</
section
>
<
ProblemListCard
title=
"临近整改问题"
description=
"预计整改完成时间在未来 1 个月内。"
items=
{
summary
.
upcoming
.
slice
(
0
,
6
)
}
emptyText=
"当前没有临近整改问题"
/>
<
ProblemListCard
title=
"整改逾期问题"
description=
"预计整改完成时间早于今天。"
items=
{
summary
.
overdue
.
slice
(
0
,
6
)
}
emptyText=
"当前没有整改逾期问题"
/>
</>
</>
)
:
null
}
)
:
null
}
</
div
>
</
div
>
...
...
src/pages/problem_rectification/list/index.jsx
View file @
dcdc9689
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
{
Checkbox
}
from
'@/catalyst-ui-kit/javascript/checkbox'
;
import
{
import
{
Dialog
,
Dialog
,
DialogActions
,
DialogActions
,
...
@@ -9,15 +8,8 @@ import {
...
@@ -9,15 +8,8 @@ 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
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
{
import
{
batchRemoveRecords
,
batchRemoveRecords
,
...
@@ -53,9 +45,6 @@ import {
...
@@ -53,9 +45,6 @@ import {
loadProblemRectificationListResources
,
loadProblemRectificationListResources
,
}
from
'../problemRectificationListService'
;
}
from
'../problemRectificationListService'
;
const
SELECTION_COLUMN_WIDTH
=
56
;
const
ACTION_COLUMN_WIDTH
=
132
;
const
FIELD_HINTS
=
{
const
FIELD_HINTS
=
{
inspect_the_implementing_unit
:
'按对象配置的实施单位选项选择'
,
inspect_the_implementing_unit
:
'按对象配置的实施单位选项选择'
,
rectification_progress
:
'状态选项来自对象配置'
,
rectification_progress
:
'状态选项来自对象配置'
,
...
@@ -64,103 +53,44 @@ const FIELD_HINTS = {
...
@@ -64,103 +53,44 @@ const FIELD_HINTS = {
feedback_unit
:
'反馈单位选项来自对象配置'
,
feedback_unit
:
'反馈单位选项来自对象配置'
,
responsible_department
:
'可多选责任部门'
,
responsible_department
:
'可多选责任部门'
,
the_time_of_rectification
:
'提交时会自动转成 ISO 8601 时间'
,
the_time_of_rectification
:
'提交时会自动转成 ISO 8601 时间'
,
has_economic_loss
:
'建议统一填写“是”或“否”'
,
economic_loss_amount
:
'建议填写数字金额,单位可在备注中说明'
,
has_external_impact
:
'建议统一填写“是”或“否”'
,
has_employee_accountability
:
'建议统一填写“是”或“否”'
,
accountability_person_count
:
'建议填写数字人数'
,
problem_severity_level
:
'建议统一填写“高 / 中 / 低”'
,
rectification_timeliness_level
:
'建议统一填写“高 / 中 / 低”'
,
rectification_material_quality_level
:
'建议统一填写“高 / 中 / 低”'
,
rectification_overall_rating
:
'建议统一填写“高 / 中 / 低”'
,
involves_policy_revision
:
'建议统一填写“是”或“否”'
,
involves_system_construction
:
'建议统一填写“是”或“否”'
,
is_historical_problem
:
'建议统一填写“是”或“否”'
,
is_repeated_problem
:
'建议统一填写“是”或“否”'
,
actual_rectification_completed_at
:
'建议使用 YYYY-MM-DD'
,
inspection_completed_at
:
'建议使用 YYYY-MM-DD'
,
};
const
CREATE_FIELD_TYPE_OVERRIDES
=
{
economic_loss_amount
:
'number'
,
accountability_person_count
:
'number'
,
actual_rectification_completed_at
:
'date'
,
inspection_completed_at
:
'date'
,
};
const
CREATE_FIELD_OPTION_OVERRIDES
=
{
has_economic_loss
:
[
'是'
,
'否'
],
has_external_impact
:
[
'是'
,
'否'
],
has_employee_accountability
:
[
'是'
,
'否'
],
problem_severity_level
:
[
'高'
,
'中'
,
'低'
],
problem_category_level_1
:
[
'内控建设'
,
'内控运行'
,
'内控建设及运行'
,
'其他'
],
problem_category_level_2
:
[
'公司治理'
,
'投资管理'
,
'销售管理'
,
'员工行为管理'
,
'基金运营'
,
'财务管理'
,
'信息技术'
],
problem_root_cause
:
[
'内外规执行不到位'
,
'系统化程度低'
,
'规矩意识淡薄导致操作风险'
,
'现有流程设计缺陷'
,
'制度建设有待完善'
],
rectification_timeliness_level
:
[
'高'
,
'中'
,
'低'
],
rectification_material_quality_level
:
[
'高'
,
'中'
,
'低'
],
involves_policy_revision
:
[
'是'
,
'否'
],
involves_system_construction
:
[
'是'
,
'否'
],
is_historical_problem
:
[
'是'
,
'否'
],
is_repeated_problem
:
[
'是'
,
'否'
],
rectification_overall_rating
:
[
'高'
,
'中'
,
'低'
],
};
};
function
getPinnedSide
(
column
)
{
const
pinnedValue
=
column
?.
lockPosition
||
column
?.
pinned
;
if
(
pinnedValue
===
'left'
||
pinnedValue
===
'right'
)
{
return
pinnedValue
;
}
return
null
;
}
function
buildStickyColumnMeta
(
columns
=
[])
{
const
leftColumns
=
columns
.
filter
((
column
)
=>
getPinnedSide
(
column
)
===
'left'
);
const
centerColumns
=
columns
.
filter
((
column
)
=>
!
getPinnedSide
(
column
));
const
rightColumns
=
columns
.
filter
((
column
)
=>
getPinnedSide
(
column
)
===
'right'
);
const
orderedColumns
=
[...
leftColumns
,
...
centerColumns
,
...
rightColumns
];
let
leftOffset
=
SELECTION_COLUMN_WIDTH
;
let
rightOffset
=
ACTION_COLUMN_WIDTH
;
const
stickyMeta
=
{};
leftColumns
.
forEach
((
column
)
=>
{
stickyMeta
[
column
.
field
]
=
{
side
:
'left'
,
offset
:
leftOffset
,
isEdge
:
column
.
field
===
leftColumns
[
leftColumns
.
length
-
1
]?.
field
,
};
leftOffset
+=
column
.
width
||
180
;
});
[...
rightColumns
].
reverse
().
forEach
((
column
)
=>
{
stickyMeta
[
column
.
field
]
=
{
side
:
'right'
,
offset
:
rightOffset
,
isEdge
:
column
.
field
===
rightColumns
[
0
]?.
field
,
};
rightOffset
+=
column
.
width
||
180
;
});
return
{
orderedColumns
,
stickyMeta
,
};
}
function
getStickyCellClass
(
meta
,
kind
=
'body'
)
{
if
(
!
meta
)
{
return
''
;
}
const
baseClass
=
kind
===
'header'
?
'sticky z-30 bg-zinc-50/95 backdrop-blur'
:
'sticky z-20 bg-white'
;
const
sideClass
=
meta
.
side
===
'left'
?
'left-0'
:
'right-0'
;
const
edgeShadow
=
meta
.
side
===
'left'
&&
meta
.
isEdge
?
'shadow-[10px_0_20px_rgba(15,23,42,0.06)]'
:
meta
.
side
===
'right'
&&
meta
.
isEdge
?
'shadow-[-10px_0_20px_rgba(15,23,42,0.06)]'
:
''
;
return
clsx
(
baseClass
,
sideClass
,
edgeShadow
);
}
function
SummaryCard
({
label
,
value
,
hint
,
tone
=
'default'
})
{
const
toneClass
=
tone
===
'rose'
?
'border-rose-200/80 bg-rose-50/85'
:
tone
===
'amber'
?
'border-amber-200/80 bg-amber-50/85'
:
tone
===
'emerald'
?
'border-emerald-200/80 bg-emerald-50/85'
:
'border-zinc-200/80 bg-white/88'
;
return
(
<
div
className=
{
clsx
(
'rounded-[26px] border p-5 shadow-sm ring-1 ring-zinc-950/5'
,
toneClass
)
}
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.22em] text-slate-400"
>
{
label
}
</
div
>
<
div
className=
"mt-3 text-3xl font-semibold tracking-tight text-slate-950"
>
{
value
}
</
div
>
<
div
className=
"mt-2 text-sm text-slate-500"
>
{
hint
}
</
div
>
</
div
>
);
}
function
EmptyState
({
title
,
description
})
{
return
(
<
div
className=
"rounded-[28px] border border-dashed border-zinc-300 bg-white/80 px-6 py-14 text-center"
>
<
div
className=
"text-xl font-semibold text-slate-900"
>
{
title
}
</
div
>
<
div
className=
"mt-3 text-sm leading-6 text-slate-500"
>
{
description
}
</
div
>
</
div
>
);
}
function
StatusPill
({
text
,
tone
=
'default'
})
{
function
StatusPill
({
text
,
tone
=
'default'
})
{
const
toneClass
=
const
toneClass
=
...
@@ -202,6 +132,26 @@ function resolveAuditTone(status = '') {
...
@@ -202,6 +132,26 @@ function resolveAuditTone(status = '') {
return
'default'
;
return
'default'
;
}
}
function
resolveLevelTone
(
value
=
''
,
invert
=
false
)
{
if
(
!
value
)
{
return
'default'
;
}
if
(
value
===
'高'
)
{
return
invert
?
'emerald'
:
'rose'
;
}
if
(
value
===
'中'
)
{
return
'amber'
;
}
if
(
value
===
'低'
)
{
return
invert
?
'rose'
:
'emerald'
;
}
return
'default'
;
}
function
SelectedUsers
({
users
,
onRemove
})
{
function
SelectedUsers
({
users
,
onRemove
})
{
if
(
users
.
length
===
0
)
{
if
(
users
.
length
===
0
)
{
return
(
return
(
...
@@ -336,8 +286,9 @@ function RemoteUserSelect({ label, value, onChange, placeholder, onSearch }) {
...
@@ -336,8 +286,9 @@ function RemoteUserSelect({ label, value, onChange, placeholder, onSearch }) {
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
);
const
fieldType
=
fieldDef
?.
type
||
'text'
;
const
fieldType
=
CREATE_FIELD_TYPE_OVERRIDES
[
fieldDef
?.
name
]
||
fieldDef
?.
type
||
'text'
;
const
placeholder
=
FIELD_HINTS
[
fieldDef
?.
name
]
||
`请输入
${
fieldDef
?.
label
||
fieldDef
?.
name
}
`;
const
placeholder
=
FIELD_HINTS
[
fieldDef
?.
name
]
||
`请输入
${
fieldDef
?.
label
||
fieldDef
?.
name
}
`;
const overrideOptions = CREATE_FIELD_OPTION_OVERRIDES[fieldDef?.name] || [];
if (fieldType === 'long_text') {
if (fieldType === 'long_text') {
return (
return (
...
@@ -377,6 +328,19 @@ function CreateField({ fieldDef, value, onChange }) {
...
@@ -377,6 +328,19 @@ function CreateField({ fieldDef, value, onChange }) {
);
);
}
}
if (overrideOptions.length > 0) {
return (
<Select value={value || ''} onChange={(event) => onChange(event.target.value)}>
<option value="">请选择</option>
{overrideOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</Select>
);
}
return (
return (
<Input
<Input
type={fieldType === 'number' ? 'number' : 'text'}
type={fieldType === 'number' ? 'number' : 'text'}
...
@@ -392,12 +356,68 @@ function FieldValuePreview({ column, record }) {
...
@@ -392,12 +356,68 @@ function FieldValuePreview({ column, record }) {
const rawRecord = record.raw || record;
const rawRecord = record.raw || record;
const value = getFieldValue(rawRecord, column.field);
const value = getFieldValue(rawRecord, column.field);
if (fieldName === 'title') {
const tags = [
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') {
return <StatusPill text={formatFieldValue(value, column.fieldDef) || '未填写'} tone={resolveProgressTone(formatFieldValue(value, column.fieldDef) || '')} />;
const text = formatFieldValue(value, column.fieldDef) || '未填写';
return <StatusPill text={text} tone={resolveProgressTone(text)} />;
}
}
if (fieldName === 'audit_status' || fieldName === 'final_audit_status') {
if (fieldName === 'audit_status' || fieldName === 'final_audit_status') {
return <StatusPill text={formatFieldValue(value, column.fieldDef) || '未填写'} tone={resolveAuditTone(formatFieldValue(value, column.fieldDef) || '')} />;
const text = formatFieldValue(value, column.fieldDef) || '未填写';
return <StatusPill text={text} tone={resolveAuditTone(text)} />;
}
if (
fieldName === 'problem_severity_level' ||
fieldName === 'rectification_timeliness_level' ||
fieldName === 'rectification_material_quality_level'
) {
const text = formatFieldValue(value, column.fieldDef) || '未填写';
return <StatusPill text={text} tone={resolveLevelTone(text)} />;
}
if (fieldName === 'rectification_overall_rating') {
const text = formatFieldValue(value, column.fieldDef) || '未填写';
return <StatusPill text={text} tone={resolveLevelTone(text, true)} />;
}
if (
fieldName === 'has_economic_loss' ||
fieldName === 'has_external_impact' ||
fieldName === 'has_employee_accountability' ||
fieldName === 'involves_policy_revision' ||
fieldName === 'involves_system_construction' ||
fieldName === 'is_historical_problem' ||
fieldName === 'is_repeated_problem'
) {
const text = formatFieldValue(value, column.fieldDef) || '否';
return <StatusPill text={text} tone={text === '是' ? 'amber' : 'default'} />;
}
}
if (fieldName === 'fill_in_the_people' || fieldName === 'review_people') {
if (fieldName === 'fill_in_the_people' || fieldName === 'review_people') {
...
@@ -460,6 +480,12 @@ export default function ProblemRectificationListPage() {
...
@@ -460,6 +480,12 @@ export default function ProblemRectificationListPage() {
keyword: '',
keyword: '',
progress: '',
progress: '',
auditStatus: '',
auditStatus: '',
severityLevel: '',
overallRating: '',
primaryResponsibleDepartment: '',
problemFrom: '',
rootCause: '',
focusFlag: '',
});
});
const [activeViewName, setActiveViewName] = useState('');
const [activeViewName, setActiveViewName] = useState('');
const [selectedIds, setSelectedIds] = useState([]);
const [selectedIds, setSelectedIds] = useState([]);
...
@@ -468,8 +494,6 @@ export default function ProblemRectificationListPage() {
...
@@ -468,8 +494,6 @@ 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 [drawerRecordName, setDrawerRecordName] = useState('');
const [drawerDirty, setDrawerDirty] = useState(false);
const reloadList = useCallback(async () => {
const reloadList = useCallback(async () => {
setLoading(true);
setLoading(true);
...
@@ -518,11 +542,6 @@ export default function ProblemRectificationListPage() {
...
@@ -518,11 +542,6 @@ export default function ProblemRectificationListPage() {
[records, filters, activeGridView, subject],
[records, filters, activeGridView, subject],
);
);
const stickyColumns = useMemo(
() => buildStickyColumnMeta(configuredView.columns || []),
[configuredView.columns],
);
const gridViewCounts = useMemo(
const gridViewCounts = useMemo(
() => buildGridViewCountMap(records, filters, gridViews, subject),
() => buildGridViewCountMap(records, filters, gridViews, subject),
[records, filters, gridViews, subject],
[records, filters, gridViews, subject],
...
@@ -558,41 +577,20 @@ export default function ProblemRectificationListPage() {
...
@@ -558,41 +577,20 @@ export default function ProblemRectificationListPage() {
const fixedCount = filteredRecords.filter(
const fixedCount = filteredRecords.filter(
(item) => item.progress === '已整改' || item.progress === '已优化',
(item) => item.progress === '已整改' || item.progress === '已优化',
).length;
).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(
const pendingAuditCount = filteredRecords.filter(
(item) => item.auditStatus && item.auditStatus !== '已审批',
(item) => item.auditStatus && item.auditStatus !== '已审批',
).length;
).length;
const toggleAll = (checked) => {
if (checked) {
setSelectedIds(filteredRecords.map((item) => item.id));
return;
}
setSelectedIds([]);
};
const toggleRow = (recordId, checked) => {
if (checked) {
setSelectedIds((current) => Array.from(new Set([...current, recordId])));
return;
}
setSelectedIds((current) => current.filter((item) => item !== recordId));
};
const handleOpenDrawer = (record) => {
setDrawerDirty(false);
setDrawerRecordName(record.name || record.id);
};
const handleCloseDrawer = async () => {
setDrawerRecordName('');
if (drawerDirty) {
setDrawerDirty(false);
await reloadList();
}
};
const handleCreate = async () => {
const handleCreate = async () => {
if (
if (
!normalizeText(formValues.project_name) &&
!normalizeText(formValues.project_name) &&
...
@@ -768,270 +766,235 @@ export default function ProblemRectificationListPage() {
...
@@ -768,270 +766,235 @@ export default function ProblemRectificationListPage() {
}
}
};
};
return
(
const
summaryItems
=
[
<
div
className=
"space-y-6 pb-4"
>
{
<
section
className=
"grid gap-4 md:grid-cols-3"
>
label
:
'当前视图记录数'
,
<
SummaryCard
value
:
filteredRecords
.
length
,
label=
"当前视图记录数"
hint
:
activeGridView
?.
title
?
`
${
activeGridView
.
title
}
视图`
:
'筛选结果'
,
value=
{
filteredRecords
.
length
}
tone
:
'rose'
,
hint=
{
activeGridView
?.
title
?
`${activeGridView.title} 视图`
:
'筛选结果'
}
},
tone=
"rose"
{
/>
label
:
'高严重程度'
,
<
SummaryCard
value
:
highSeverityCount
,
label=
"已整改 / 已优化"
hint
:
'问题综合严重程度 = 高'
,
value=
{
fixedCount
}
tone
:
'rose'
,
hint=
"按当前视图与筛选统计"
},
tone=
"emerald"
{
/>
label
:
'重点后果问题'
,
<
SummaryCard
value
:
consequenceCount
,
label=
"待复核项"
hint
:
'含经济损失 / 外部影响 / 员工问责'
,
value=
{
pendingAuditCount
}
tone
:
'amber'
,
hint=
"审批状态未通过的记录"
},
tone=
"amber"
{
/>
label
:
'往期或重复问题'
,
</
section
>
value
:
repeatedOrHistoricalCount
,
hint
:
'往期发现或再次出现的问题'
,
<
section
className=
"rounded-[30px] border border-white/80 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(250,250,250,0.92))] p-3 shadow-sm ring-1 ring-zinc-950/5"
>
tone
:
'amber'
,
<
div
className=
"flex flex-wrap gap-2"
>
},
{
gridViews
.
map
((
view
)
=>
{
{
const
active
=
view
.
name
===
activeGridView
?.
name
;
label
:
'已逾期未完成'
,
return
(
value
:
overdueCount
,
<
button
hint
:
'预计整改完成时间已到且未闭环'
,
key=
{
view
.
name
}
tone
:
'rose'
,
type=
"button"
},
onClick=
{
()
=>
{
{
setActiveViewName
(
view
.
name
);
label
:
'整改评级偏低'
,
setSelectedIds
([]);
value
:
lowRatingCount
,
}
}
hint
:
'综合整改评定等级 = 低'
,
className=
{
clsx
(
tone
:
'rose'
,
'inline-flex min-h-11 items-center gap-2 rounded-full px-4 py-2.5 text-sm font-medium transition'
,
},
active
{
?
'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg shadow-rose-200/60'
label
:
'已整改 / 已优化'
,
:
'bg-white text-slate-600 ring-1 ring-zinc-200 hover:bg-rose-50 hover:text-rose-700'
,
value
:
fixedCount
,
)
}
hint
:
'按当前视图与筛选统计'
,
>
tone
:
'emerald'
,
<
span
>
{
view
.
title
||
view
.
name
}
</
span
>
},
<
span
{
className=
{
clsx
(
label
:
'待复核项'
,
'rounded-full px-2 py-0.5 text-xs'
,
value
:
pendingAuditCount
,
active
?
'bg-white/20 text-white'
:
'bg-zinc-100 text-slate-500'
,
hint
:
'审批状态未通过的记录'
,
)
}
tone
:
'amber'
,
>
},
{
gridViewCounts
[
view
.
name
]
||
0
}
];
</
span
>
</
button
>
);
})
}
</
div
>
</
section
>
<
section
className=
"rounded-[30px] border border-zinc-200 bg-white/95 p-5 shadow-sm ring-1 ring-zinc-950/5"
>
<
div
className=
"grid gap-4 xl:grid-cols-[1.4fr_0.8fr_0.8fr_auto]"
>
<
div
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
关键词搜索
</
label
>
<
Input
value=
{
filters
.
keyword
}
onChange=
{
(
event
)
=>
setFilters
((
current
)
=>
({
...
current
,
keyword
:
event
.
target
.
value
}))
}
placeholder=
"搜索当前视图中的项目名称、状态、问题简述、责任人..."
/>
</
div
>
<
div
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
整改进度
</
label
>
<
Select
value=
{
filters
.
progress
}
onChange=
{
(
event
)
=>
setFilters
((
current
)
=>
({
...
current
,
progress
:
event
.
target
.
value
}))
}
>
<
option
value=
""
>
全部进度
</
option
>
{
configuredView
.
progressOptions
.
map
((
option
)
=>
(
<
option
key=
{
option
}
value=
{
option
}
>
{
option
}
</
option
>
))
}
</
Select
>
</
div
>
<
div
>
return
(
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
审批状态
</
label
>
<>
<
Select
<
RecordWorkbench
value=
{
filters
.
auditStatus
}
summaryItems=
{
summaryItems
}
onChange=
{
(
event
)
=>
setFilters
((
current
)
=>
({
...
current
,
auditStatus
:
event
.
target
.
value
}))
}
views=
{
gridViews
.
map
((
view
)
=>
({
>
...
view
,
<
option
value=
""
>
全部状态
</
option
>
count
:
gridViewCounts
[
view
.
name
]
||
0
,
{
configuredView
.
auditStatusOptions
.
map
((
option
)
=>
(
}))
}
<
option
key=
{
option
}
value=
{
option
}
>
activeViewName=
{
activeGridView
?.
name
||
''
}
{
option
}
onViewChange=
{
(
viewName
)
=>
{
</
option
>
setActiveViewName
(
viewName
);
))
}
setSelectedIds
([]);
</
Select
>
}
}
</
div
>
filters=
{
filters
}
filterSchema=
{
[
<
div
className=
"flex items-end"
>
{
<
Button
key
:
'keyword'
,
outline
label
:
'关键词搜索'
,
className=
"w-full justify-center"
type
:
'text'
,
onClick=
{
()
=>
{
placeholder
:
'搜索项目名称、严重程度、分类、责任部门、问题原因...'
,
setFilters
({
},
keyword
:
''
,
{
progress
:
''
,
key
:
'progress'
,
auditStatus
:
''
,
label
:
'整改进度'
,
});
type
:
'select'
,
setSelectedIds
([]);
placeholder
:
'全部进度'
,
}
}
options
:
configuredView
.
progressOptions
.
map
((
option
)
=>
({
label
:
option
,
value
:
option
})),
>
},
清空筛选
{
</
Button
>
key
:
'auditStatus'
,
</
div
>
label
:
'审批状态'
,
</
div
>
type
:
'select'
,
</
section
>
placeholder
:
'全部状态'
,
options
:
configuredView
.
auditStatusOptions
.
map
((
option
)
=>
({
label
:
option
,
value
:
option
})),
<
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"
>
},
<
div
className=
"flex flex-wrap items-start justify-between gap-4 border-b border-zinc-100 px-6 py-5"
>
{
<
div
>
key
:
'severityLevel'
,
<
div
className=
"text-lg font-semibold text-slate-900"
>
问题清单
</
div
>
label
:
'严重程度'
,
<
div
className=
"mt-1 text-sm text-slate-500"
>
type
:
'select'
,
当前视图显示
{
filteredRecords
.
length
}
条记录,已选中
{
selectedIds
.
length
}
条。
placeholder
:
'全部严重程度'
,
</
div
>
options
:
configuredView
.
severityOptions
.
map
((
option
)
=>
({
label
:
option
,
value
:
option
})),
</
div
>
},
{
<
div
className=
"flex flex-wrap gap-2"
>
key
:
'overallRating'
,
<
Button
color=
"rose"
onClick=
{
()
=>
setShowCreateModal
(
true
)
}
>
label
:
'整改评级'
,
新增记录
type
:
'select'
,
</
Button
>
placeholder
:
'全部评级'
,
<
Button
outline
disabled=
{
selectedIds
.
length
===
0
||
actionLoading
}
onClick=
{
()
=>
setShowPushModal
(
true
)
}
>
options
:
configuredView
.
overallRatingOptions
.
map
((
option
)
=>
({
label
:
option
,
value
:
option
})),
推送整改表
},
</
Button
>
{
<
Button
outline
disabled
>
key
:
'primaryResponsibleDepartment'
,
导出
label
:
'主责部门'
,
</
Button
>
type
:
'select'
,
<
Button
outline
disabled
>
placeholder
:
'全部主责部门'
,
一键归档
options
:
configuredView
.
primaryResponsibleDepartmentOptions
.
map
((
option
)
=>
({
</
Button
>
label
:
option
,
<
Button
color=
"red"
disabled=
{
selectedIds
.
length
===
0
||
actionLoading
}
onClick=
{
()
=>
handleDelete
(
selectedRecords
)
}
>
value
:
option
,
删除选中
})),
</
Button
>
},
</
div
>
{
</
div
>
key
:
'problemFrom'
,
label
:
'问题来源'
,
{
error
?
(
type
:
'select'
,
<
div
className=
"border-b border-zinc-100 bg-rose-50/70 px-6 py-4 text-sm text-rose-700"
>
placeholder
:
'全部来源'
,
{
error
}
options
:
configuredView
.
problemFromOptions
.
map
((
option
)
=>
({
label
:
option
,
value
:
option
})),
</
div
>
},
)
:
null
}
{
key
:
'rootCause'
,
{
loading
?
(
label
:
'问题原因'
,
<
div
className=
"px-6 py-6"
>
type
:
'select'
,
<
EmptyState
title=
"正在加载问题整改列表"
description=
"正在读取对象结构、视图配置和记录数据,请稍候。"
/>
placeholder
:
'全部原因'
,
</
div
>
options
:
configuredView
.
rootCauseOptions
.
map
((
option
)
=>
({
label
:
option
,
value
:
option
})),
)
:
filteredRecords
.
length
===
0
?
(
},
<
div
className=
"px-6 py-6"
>
{
<
EmptyState
key
:
'focusFlag'
,
title=
"当前视图暂无数据"
label
:
'重点关注'
,
description=
"可以切换其他视图,或调整关键词、审批状态和整改进度筛选。"
type
:
'select'
,
placeholder
:
'全部类型'
,
options
:
configuredView
.
focusOptions
,
},
]
}
onFiltersChange=
{
(
key
,
value
)
=>
setFilters
((
current
)
=>
({
...
current
,
[
key
]:
value
}))
}
onClearFilters=
{
()
=>
{
setFilters
({
keyword
:
''
,
progress
:
''
,
auditStatus
:
''
,
severityLevel
:
''
,
overallRating
:
''
,
primaryResponsibleDepartment
:
''
,
problemFrom
:
''
,
rootCause
:
''
,
focusFlag
:
''
,
});
setSelectedIds
([]);
}
}
title=
"问题清单"
description=
{
`当前视图显示 ${filteredRecords.length} 条记录,已选中 ${selectedIds.length} 条,其中重点后果问题 ${consequenceCount} 条、逾期未完成 ${overdueCount} 条。`
}
columns=
{
configuredView
.
columns
||
[]
}
records=
{
filteredRecords
}
selectedIds=
{
selectedIds
}
onSelectionChange=
{
setSelectedIds
}
toolbarActions=
{
[
{
key
:
'create'
,
label
:
'新增记录'
,
variant
:
'solid'
,
tone
:
'primary'
,
onClick
:
()
=>
setShowCreateModal
(
true
),
},
{
key
:
'push'
,
label
:
'推送整改表'
,
variant
:
'outline'
,
disabled
:
()
=>
selectedIds
.
length
===
0
||
actionLoading
,
onClick
:
()
=>
setShowPushModal
(
true
),
},
{
key
:
'export'
,
label
:
'导出'
,
variant
:
'outline'
,
disabled
:
true
,
onClick
:
()
=>
{},
},
{
key
:
'archive'
,
label
:
'一键归档'
,
variant
:
'outline'
,
disabled
:
true
,
onClick
:
()
=>
{},
},
{
key
:
'delete-selected'
,
label
:
'删除选中'
,
variant
:
'solid'
,
tone
:
'danger'
,
disabled
:
()
=>
selectedIds
.
length
===
0
||
actionLoading
,
onClick
:
()
=>
handleDelete
(
selectedRecords
),
},
]
}
rowActions=
{
[
{
key
:
'view-detail'
,
label
:
'查看详情'
,
variant
:
'plain'
,
onClick
:
({
openDetail
,
record
})
=>
openDetail
(
record
),
},
{
key
:
'delete'
,
label
:
'删除'
,
variant
:
'outline'
,
tone
:
'danger'
,
onClick
:
({
record
})
=>
handleDelete
([
record
]),
},
]
}
detailPanel=
{
{
getRecordName
:
(
record
)
=>
record
.
name
||
record
.
id
,
onDirtyClose
:
reloadList
,
render
:
({
open
,
recordName
,
onClose
,
onDataChange
})
=>
(
<
RecordDetailDrawer
open=
{
open
}
recordName=
{
recordName
}
onClose=
{
onClose
}
onDataChange=
{
onDataChange
}
preferredViewName=
{
activeGridView
?.
title
||
activeGridView
?.
name
}
/>
/>
</
div
>
),
)
:
(
}
}
<
div
className=
"px-3 pb-3 pt-2"
>
states=
{
{
<
Table
bleed
dense
className=
"[--gutter:--spacing(3)]"
>
loading
,
<
TableHead
>
error
,
<
TableRow
className=
"bg-zinc-50/80"
>
loadingTitle
:
'正在加载问题整改列表'
,
<
TableHeader
loadingDescription
:
'正在读取对象结构、视图配置和记录数据,请稍候。'
,
className=
"w-14 sticky left-0 z-40 bg-zinc-50/95 backdrop-blur shadow-[10px_0_20px_rgba(15,23,42,0.06)]"
emptyTitle
:
'当前视图暂无数据'
,
style=
{
{
width
:
SELECTION_COLUMN_WIDTH
,
minWidth
:
SELECTION_COLUMN_WIDTH
}
}
emptyDescription
:
'可以切换其他视图,或调整严重程度、整改评级、主责部门和重点关注筛选。'
,
>
}
}
<
div
className=
"flex justify-center"
>
renderCell=
{
({
record
,
column
})
=>
<
FieldValuePreview
record=
{
record
}
column=
{
column
}
/>
}
<
Checkbox
/>
checked=
{
filteredRecords
.
length
>
0
&&
selectedIds
.
length
===
filteredRecords
.
length
}
onChange=
{
toggleAll
}
color=
"rose"
aria
-
label=
"全选当前视图记录"
/>
</
div
>
</
TableHeader
>
{
stickyColumns
.
orderedColumns
.
map
((
column
)
=>
{
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
column
.
field
];
return
(
<
TableHeader
key=
{
column
.
field
}
style=
{
{
width
:
column
.
width
||
180
,
minWidth
:
Math
.
min
(
Math
.
max
(
column
.
width
||
180
,
150
),
360
),
left
:
stickyMeta
?.
side
===
'left'
?
`${stickyMeta.offset}px`
:
undefined
,
right
:
stickyMeta
?.
side
===
'right'
?
`${stickyMeta.offset}px`
:
undefined
,
}
}
className=
{
clsx
(
'text-xs font-semibold uppercase tracking-[0.18em] text-slate-400'
,
getStickyCellClass
(
stickyMeta
,
'header'
),
)
}
>
{
column
.
label
}
</
TableHeader
>
);
})
}
<
TableHeader
className=
"sticky right-0 z-40 bg-zinc-50/95 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400 shadow-[-10px_0_20px_rgba(15,23,42,0.06)] backdrop-blur"
style=
{
{
width
:
ACTION_COLUMN_WIDTH
,
minWidth
:
ACTION_COLUMN_WIDTH
}
}
>
操作
</
TableHeader
>
</
TableRow
>
</
TableHead
>
<
TableBody
>
{
filteredRecords
.
map
((
record
)
=>
(
<
TableRow
key=
{
record
.
id
}
className=
"align-top"
>
<
TableCell
className=
"align-top sticky left-0 z-30 bg-white shadow-[10px_0_20px_rgba(15,23,42,0.05)]"
style=
{
{
width
:
SELECTION_COLUMN_WIDTH
,
minWidth
:
SELECTION_COLUMN_WIDTH
}
}
>
<
div
className=
"flex justify-center pt-1"
>
<
Checkbox
checked=
{
selectedIds
.
includes
(
record
.
id
)
}
onChange=
{
(
checked
)
=>
toggleRow
(
record
.
id
,
checked
)
}
color=
"rose"
aria
-
label=
{
`选择 ${record.title || record.projectName || '当前记录'}`
}
/>
</
div
>
</
TableCell
>
{
stickyColumns
.
orderedColumns
.
map
((
column
)
=>
{
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
column
.
field
];
return
(
<
TableCell
key=
{
column
.
field
}
style=
{
{
width
:
column
.
width
||
180
,
minWidth
:
Math
.
min
(
Math
.
max
(
column
.
width
||
180
,
150
),
360
),
left
:
stickyMeta
?.
side
===
'left'
?
`${stickyMeta.offset}px`
:
undefined
,
right
:
stickyMeta
?.
side
===
'right'
?
`${stickyMeta.offset}px`
:
undefined
,
}
}
className=
{
clsx
(
'align-top'
,
getStickyCellClass
(
stickyMeta
))
}
>
<
FieldValuePreview
column=
{
column
}
record=
{
record
}
/>
</
TableCell
>
);
})
}
<
TableCell
className=
"align-top sticky right-0 z-30 bg-white shadow-[-10px_0_20px_rgba(15,23,42,0.05)]"
style=
{
{
width
:
ACTION_COLUMN_WIDTH
,
minWidth
:
ACTION_COLUMN_WIDTH
}
}
>
<
div
className=
"flex min-w-28 flex-col gap-2"
>
<
Button
plain
onClick=
{
()
=>
handleOpenDrawer
(
record
)
}
>
查看详情
</
Button
>
<
Button
outline
color=
"red"
onClick=
{
()
=>
handleDelete
([
record
])
}
>
删除
</
Button
>
</
div
>
</
TableCell
>
</
TableRow
>
))
}
</
TableBody
>
</
Table
>
</
div
>
)
}
</
section
>
<
Dialog
open=
{
showCreateModal
}
onClose=
{
handleCloseCreateModal
}
size=
"5xl"
>
<
Dialog
open=
{
showCreateModal
}
onClose=
{
handleCloseCreateModal
}
size=
"5xl"
>
<
DialogTitle
>
新增问题整改记录
</
DialogTitle
>
<
DialogTitle
>
新增问题整改记录
</
DialogTitle
>
...
@@ -1041,7 +1004,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1041,7 +1004,7 @@ export default function ProblemRectificationListPage() {
</
DialogDescription
>
</
DialogDescription
>
<
DialogBody
>
<
DialogBody
>
<
div
className=
"rounded-[24px] border border-zinc-100 bg-zinc-50/60 px-4 py-3 text-sm text-slate-500"
>
<
div
className=
"rounded-[24px] border border-zinc-100 bg-zinc-50/60 px-4 py-3 text-sm text-slate-500"
>
至少填写“检查项目名称”或“问题简述”之一。
当前只展示本视图所关心的字段,减少一次性录入负担
。
至少填写“检查项目名称”或“问题简述”之一。
基础字段沿视图顺序渲染,新增的严重程度、分类、原因和整改评估字段会自动补充到表单末尾,便于后续驾驶舱统计
。
</
div
>
</
div
>
<
div
className=
"mt-6 grid gap-5 md:grid-cols-2"
>
<
div
className=
"mt-6 grid gap-5 md:grid-cols-2"
>
...
@@ -1153,16 +1116,6 @@ export default function ProblemRectificationListPage() {
...
@@ -1153,16 +1116,6 @@ export default function ProblemRectificationListPage() {
</
DialogActions
>
</
DialogActions
>
</
Dialog
>
</
Dialog
>
)
:
null
}
)
:
null
}
</>
{
drawerRecordName
?
(
<
RecordDetailDrawer
open=
{
Boolean
(
drawerRecordName
)
}
recordName=
{
drawerRecordName
}
setCustomComponentClose=
{
handleCloseDrawer
}
onDataChange=
{
()
=>
setDrawerDirty
(
true
)
}
preferredViewName=
{
activeGridView
?.
title
||
activeGridView
?.
name
}
/>
)
:
null
}
</
div
>
);
);
}
}
src/pages/problem_rectification/problem-rectification.business-flow.md
View file @
dcdc9689
...
@@ -64,6 +64,13 @@
...
@@ -64,6 +64,13 @@
因此前端新增字段展示、部门视图调整、列顺序变化时,应优先改 Subject View 配置,而不是直接修改页面常量。
因此前端新增字段展示、部门视图调整、列顺序变化时,应优先改 Subject View 配置,而不是直接修改页面常量。
当前项目实现约定补充:
-
`problem_rectification`
与
`issue_rectification_archive`
列表页统一复用
`RecordWorkbench`
作为“结构化表格 + 工具栏 + 详情抽屉入口”的工作台容器
-
页面本身只负责提供 subject 对应的数据加载、统计口径、筛选 schema、行操作与批量操作
-
详情侧栏统一复用
`RecordDetailDrawer`
,通过
`recordName`
、
`subjectName`
、
`preferredViewName`
驱动,不再在各页面重复实现抽屉骨架
-
新增同类列表页时,优先复用工作台与详情抽屉组合,避免再次复制问题整改页 JSX
### 3.1 目标页面拆分
### 3.1 目标页面拆分
为便于当前项目直接重构,建议把模块拆成以下 4 个页面入口:
为便于当前项目直接重构,建议把模块拆成以下 4 个页面入口:
...
@@ -229,8 +236,20 @@
...
@@ -229,8 +236,20 @@
-
问题类型
-
问题类型
-
反馈单位
-
反馈单位
-
问题涉及业务领域
-
问题涉及业务领域
3.
可预先填写预计整改完成时间
3.
根据 2026 年整改驾驶舱要求,补录扩展治理字段:
4.
初始时可不指定填报人、复核人
-
问题后果:是否造成经济损失、对应经济损失、是否造成外部影响、处罚机构、是否涉及员工问责、问责人数
-
问题画像:问题综合严重程度、问题一级至四级分类、问题原因
-
整改质效:整改时间评估、整改证明材料质量、是否涉及制度修订、制度名称、是否涉及系统建设、系统名称、是否为往期发现问题、问题是否重复出现、综合整改评定等级
-
责任识别:主责部门、实际整改完成时间、实际检查完成日期
4.
可预先填写预计整改完成时间
5.
初始时可不指定填报人、复核人
推荐新交互:
-
工作台筛选区增加严重程度、整改评级、主责部门、问题来源、问题原因、重点关注等维度
-
列表标题列增加风险标签,优先暴露“高严重程度”“重点后果问题”“已逾期”等重点状态
-
新增弹窗除基础字段外,还应补充严重程度、分类、原因和整改质效字段,避免驾驶舱后续无数据可用
-
摘要卡优先展示高严重程度、重点后果问题、往期/重复问题、逾期未完成、整改评级偏低等指标
### 7.2 发起整改填报
### 7.2 发起整改填报
...
@@ -369,12 +388,12 @@
...
@@ -369,12 +388,12 @@
## 9. 驾驶舱展示规则
## 9. 驾驶舱展示规则
旧驾驶舱页面以
`problem_rectification`
为唯一数据源,主要分为三部分
:
当前驾驶舱页面以
`problem_rectification`
为唯一数据源,围绕 2026 年整改驾驶舱需求分为四层
:
1.
按反馈单位分组的统计看板
1.
宏观总览层
2.
临近整改问题列表
2.
问题分析与风险定位层
3.
整改逾期问题列表
3.
问题责任追踪与执行层
4.
问题涉及业务领域词云
4.
审计整改质效层
### 9.1 顶部标签分组
### 9.1 顶部标签分组
...
@@ -399,47 +418,78 @@
...
@@ -399,47 +418,78 @@
-
页面文案可以更贴近业务,例如“资产管理部”与“鑫沅资产”统一命名
-
页面文案可以更贴近业务,例如“资产管理部”与“鑫沅资产”统一命名
-
分组不必写死在组件内部,建议抽为配置映射
-
分组不必写死在组件内部,建议抽为配置映射
推荐新实现:
当前实现约定:
-
顶部继续保留按
`feedback_unit`
分组的轻量 tabs
-
tabs 下方分为 KPI 卡片、ECharts 图表区和重点问题清单区
-
驾驶舱仅负责统计展示,不承接编辑动作
-
如果线上 Subject 后续把新增字段从
`text`
升级为
`select`
/
`date`
/
`boolean`
,驾驶舱统计口径无需调整,只需复用标准字段值
### 9.2 宏观总览层
当前 KPI 统计口径:
-
问题总数:当前分组内问题总数
-
已完成整改数:
`rectification_progress`
为
`已整改`
或
`已优化`
-
整改完成率:
`已完成整改数 / 问题总数`
-
逾期未完成数:
`the_time_of_rectification`
早于当前日期且未闭环的问题数
-
逾期率:
`逾期未完成数 / 问题总数`
-
顶部使用轻量 tabs
整改进度分布:
-
tabs 下方展示 KPI 卡片
-
页面中下部拆成“临近整改”“整改逾期”“问题领域分布”三个区块
-
若词云阅读性差,可直接换成条形排名,不影响业务含义
### 9.2 统计指标口径
-
以
`rectification_progress`
为维度统计数量和占比
-
当前页面使用环形图表达,不再沿用旧版折线趋势表达
旧
`MainPanel`
已确认的统计口径:
### 9.3 问题分析与风险定位层
-
已整改问题:
`rectification_progress`
为
`已整改`
或
`已优化`
当前图表与字段映射:
-
问题数量:当前分组内问题总数
-
历史问题累计整改率:
`(已整改或已优化数量 / 问题总数) * 100`
-
本年度问题整改率:
当年
`check_the_implementation_time`
所属问题中,已整改或已优化的占比
其中“当年”是通过
`check_the_implementation_time`
中提取四位年份数字判断,不是通过日期字段直接计算。
-
问题类型 / 业务领域分布:优先使用
`problem_category_level_1`
至
`problem_category_level_4`
生成矩形树图;若分类未填写,则回退到
`related_issue_domain`
-
问题根本原因分析:使用
`problem_root_cause`
聚合
-
问题来源对比:以
`problem_from`
为横轴,并按
`inspect_the_implementing_unit`
做堆叠比较
-
重点问题集中展示:
-
经济损失问题:
`has_economic_loss`
+
`economic_loss_amount`
-
外部影响问题:
`has_external_impact`
+
`penalty_organization`
-
员工问责问题:
`has_employee_accountability`
+
`accountability_person_count`
-
重点问题清单中同时展示
`problem_severity_level`
### 9.
3 临近整改问题
### 9.
4 问题责任追踪与执行层
筛选逻辑
:
当前图表与字段映射
:
-
`the_time_of_rectification`
在当前时间之后
-
逾期问题排行:按
`responsible_department`
聚合逾期未完成问题数
-
`the_time_of_rectification`
在未来 1 个月内
-
部门整改责任矩阵:
-
`rectification_progress !== "已整改"`
-
X 轴:责任部门问题总数
-
Y 轴:整改完成率
-
气泡大小:逾期问题数
-
整改时效分析散点图:
-
X 轴:
`the_time_of_rectification - inspection_completed_at`
-
Y 轴:
`actual_rectification_completed_at - inspection_completed_at`
-
仅统计同时填写了预计整改时间、实际整改完成时间、实际检查完成日期且天数非负的问题
### 9.4 整改逾期问题
当前逾期 / 延期判定说明:
筛选逻辑:
-
当前项目线上尚无稳定使用的显式“是否延期”字段,因此页面暂以三类规则综合判断:
-
整改进度文案包含“延期”
-
实际整改完成时间晚于预计整改完成时间
-
截止当前日期预计整改完成时间已到且问题尚未闭环
-
其中 KPI 和逾期排行的“逾期未完成”使用第三条规则
-
`the_time_of_rectification`
早于当前时间
### 9.5 审计整改质效层
-
`rectification_progress !== "已整改"`
### 9.5 问题涉及业务领域统计
当前图表与字段映射:
-
使用
`related_issue_domain`
做聚合统计
-
问题整改时效:按责任部门计算
`rectification_timeliness_level`
平均分,低=1、中=2、高=3
-
旧应用通过
`subjectSearch('problem_rectification', ...)`
获取 bucket
-
问题整改质量:按责任部门计算
`rectification_material_quality_level`
平均分,低=1、中=2、高=3
-
新项目可以保留词云,也可以替换成更轻量的条形列表或标签热度图
-
问题整改助力总结:
-
如果追求性能和可读性,优先推荐横向条形图或排名卡片,而不是强依赖词云
-
制度修订数量:
`involves_policy_revision = 是`
-
系统建设数量:
`involves_system_construction = 是`
-
风险-质量热力图:
-
X 轴:
`problem_severity_level`
-
Y 轴:
`rectification_overall_rating`
-
部门综合整改评定等级排行:
-
以责任部门为维度统计
`rectification_overall_rating`
的低/中/高占比
-
按高评级占比降序展示
---
---
...
...
src/pages/problem_rectification/problemRectificationDashboardService.js
View file @
dcdc9689
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
import
{
queryRecords
,
subjectSearch
}
from
'@/services/DataService'
;
import
{
queryRecords
}
from
'@/services/DataService'
;
export
const
PROBLEM_RECTIFICATION_SUBJECT
=
'problem_rectification'
;
export
const
PROBLEM_RECTIFICATION_SUBJECT
=
'problem_rectification'
;
...
@@ -32,6 +32,14 @@ export const DEPARTMENT_GROUPS = [
...
@@ -32,6 +32,14 @@ export const DEPARTMENT_GROUPS = [
];
];
const
FIXED_PROGRESS_VALUES
=
new
Set
([
'已整改'
,
'已优化'
]);
const
FIXED_PROGRESS_VALUES
=
new
Set
([
'已整改'
,
'已优化'
]);
const
POSITIVE_FLAGS
=
new
Set
([
'是'
,
'有'
,
'涉及'
,
'TRUE'
,
'1'
,
'Y'
,
'YES'
]);
const
SEVERITY_LEVELS
=
[
'低'
,
'中'
,
'高'
];
const
RATING_LEVELS
=
[
'低'
,
'中'
,
'高'
];
const
SCORE_LEVEL_MAP
=
{
低
:
1
,
中
:
2
,
高
:
3
,
};
function
normalizeText
(
value
)
{
function
normalizeText
(
value
)
{
return
String
(
value
??
''
).
trim
();
return
String
(
value
??
''
).
trim
();
...
@@ -49,43 +57,37 @@ function toArray(value) {
...
@@ -49,43 +57,37 @@ function toArray(value) {
return
[
value
];
return
[
value
];
}
}
function
formatDateText
(
value
)
{
function
parseDate
(
value
)
{
if
(
!
value
)
{
if
(
!
value
)
{
return
''
;
return
null
;
}
}
const
parsed
=
dayjs
(
value
);
const
parsed
=
dayjs
(
value
);
if
(
parsed
.
isValid
())
{
if
(
parsed
.
isValid
())
{
return
parsed
.
format
(
'YYYY-MM-DD'
)
;
return
parsed
;
}
}
const
text
=
normalizeText
(
value
);
if
(
/^
\d{4}
-
\d{2}
-
\d{2}
/
.
test
(
text
))
{
return
text
.
slice
(
0
,
10
);
}
return
text
;
}
function
extractYear
(
value
)
{
const
text
=
normalizeText
(
value
);
const
text
=
normalizeText
(
value
);
if
(
!
text
)
{
if
(
!
text
)
{
return
null
;
return
null
;
}
}
const
directDate
=
dayjs
(
text
);
const
fallbackParsed
=
dayjs
(
text
);
if
(
directDate
.
isValid
())
{
return
fallbackParsed
.
isValid
()
?
fallbackParsed
:
null
;
return
directDate
.
year
();
}
function
formatDateText
(
value
)
{
const
parsed
=
parseDate
(
value
);
if
(
parsed
)
{
return
parsed
.
format
(
'YYYY-MM-DD'
);
}
}
const
match
=
text
.
match
(
/
\d{4}
/
);
const
text
=
normalizeText
(
value
);
return
match
?
Number
(
match
[
0
])
:
null
;
return
/^
\d{4}
-
\d{2}
-
\d{2}
/
.
test
(
text
)
?
text
.
slice
(
0
,
10
)
:
text
;
}
}
function
isFixedProblem
(
record
)
{
function
isAffirmative
(
value
)
{
return
FIXED_PROGRESS_VALUES
.
has
(
return
POSITIVE_FLAGS
.
has
(
normalizeText
(
value
).
toUpperCase
());
normalizeText
(
record
?.
metadata
?.
rectification_progress
),
);
}
}
function
normalizeUserLabel
(
value
)
{
function
normalizeUserLabel
(
value
)
{
...
@@ -107,36 +109,458 @@ function normalizeUserLabel(value) {
...
@@ -107,36 +109,458 @@ function normalizeUserLabel(value) {
);
);
}
}
function
isFixedProblem
(
record
)
{
return
FIXED_PROGRESS_VALUES
.
has
(
normalizeText
(
record
?.
metadata
?.
rectification_progress
));
}
function
extractPrimaryDepartment
(
metadata
)
{
const
primary
=
normalizeText
(
metadata
.
primary_responsible_department
);
if
(
primary
)
{
return
primary
;
}
const
departments
=
toArray
(
metadata
.
responsible_department
)
.
map
((
item
)
=>
normalizeText
(
item
))
.
filter
(
Boolean
);
return
departments
[
0
]
||
'未指定'
;
}
function
sortByCountDesc
(
items
=
[])
{
return
[...
items
].
sort
((
left
,
right
)
=>
right
.
value
-
left
.
value
);
}
function
diffDays
(
later
,
earlier
)
{
if
(
!
later
||
!
earlier
)
{
return
null
;
}
return
later
.
startOf
(
'day'
).
diff
(
earlier
.
startOf
(
'day'
),
'day'
);
}
function
buildDistributionMap
(
records
,
getKey
)
{
const
map
=
new
Map
();
records
.
forEach
((
record
)
=>
{
const
key
=
normalizeText
(
getKey
(
record
))
||
'未分类'
;
map
.
set
(
key
,
(
map
.
get
(
key
)
||
0
)
+
1
);
});
return
sortByCountDesc
(
Array
.
from
(
map
.
entries
()).
map
(([
name
,
value
])
=>
({
name
,
value
})),
);
}
function
upsertTreeNode
(
nodes
,
name
)
{
let
existing
=
nodes
.
find
((
node
)
=>
node
.
name
===
name
);
if
(
!
existing
)
{
existing
=
{
name
,
value
:
0
,
children
:
[]
};
nodes
.
push
(
existing
);
}
existing
.
value
+=
1
;
return
existing
;
}
function
buildCategoryTreemap
(
records
)
{
const
root
=
[];
records
.
forEach
((
record
)
=>
{
const
path
=
[
record
.
categoryLevel1
,
record
.
categoryLevel2
,
record
.
categoryLevel3
,
record
.
categoryLevel4
,
].
filter
(
Boolean
);
if
(
path
.
length
===
0
)
{
path
.
push
(
record
.
relatedIssueDomain
||
'未分类'
);
}
let
currentNodes
=
root
;
path
.
forEach
((
name
)
=>
{
const
node
=
upsertTreeNode
(
currentNodes
,
name
);
currentNodes
=
node
.
children
;
});
});
return
root
;
}
function
buildSourceComparison
(
records
)
{
const
sourceMap
=
new
Map
();
const
unitSet
=
new
Set
();
records
.
forEach
((
record
)
=>
{
const
source
=
record
.
problemFrom
||
'未分类来源'
;
const
inspectUnit
=
record
.
inspectUnit
||
'未分类单位'
;
if
(
!
sourceMap
.
has
(
source
))
{
sourceMap
.
set
(
source
,
new
Map
());
}
const
unitMap
=
sourceMap
.
get
(
source
);
unitMap
.
set
(
inspectUnit
,
(
unitMap
.
get
(
inspectUnit
)
||
0
)
+
1
);
unitSet
.
add
(
inspectUnit
);
});
const
categories
=
Array
.
from
(
sourceMap
.
keys
());
const
unitTotals
=
Array
.
from
(
unitSet
).
map
((
unit
)
=>
({
unit
,
total
:
categories
.
reduce
(
(
sum
,
source
)
=>
sum
+
(
sourceMap
.
get
(
source
)?.
get
(
unit
)
||
0
),
0
,
),
}));
const
topUnits
=
unitTotals
.
sort
((
left
,
right
)
=>
right
.
total
-
left
.
total
)
.
slice
(
0
,
6
)
.
map
((
item
)
=>
item
.
unit
);
const
series
=
topUnits
.
map
((
unit
)
=>
({
name
:
unit
,
data
:
categories
.
map
((
source
)
=>
sourceMap
.
get
(
source
)?.
get
(
unit
)
||
0
),
}));
return
{
categories
,
series
,
};
}
function
buildFocusIssueGroups
(
records
)
{
const
sortBySeverityAndDueDate
=
(
left
,
right
)
=>
{
const
leftSeverity
=
SEVERITY_LEVELS
.
indexOf
(
left
.
severityLevel
);
const
rightSeverity
=
SEVERITY_LEVELS
.
indexOf
(
right
.
severityLevel
);
if
(
leftSeverity
!==
rightSeverity
)
{
return
rightSeverity
-
leftSeverity
;
}
if
(
left
.
dueDate
&&
right
.
dueDate
)
{
return
left
.
dueDate
.
valueOf
()
-
right
.
dueDate
.
valueOf
();
}
if
(
left
.
dueDate
)
{
return
-
1
;
}
if
(
right
.
dueDate
)
{
return
1
;
}
return
left
.
title
.
localeCompare
(
right
.
title
,
'zh-CN'
);
};
const
formatItem
=
(
record
,
extraLabel
)
=>
({
id
:
record
.
id
,
title
:
record
.
title
,
severityLevel
:
record
.
severityLevel
||
'未评定'
,
feedbackUnit
:
record
.
feedbackUnit
||
'未归类单位'
,
extraLabel
,
dueDateLabel
:
record
.
dueDateLabel
,
brief
:
record
.
brief
,
});
return
{
economicLoss
:
records
.
filter
((
record
)
=>
record
.
hasEconomicLoss
)
.
sort
(
sortBySeverityAndDueDate
)
.
slice
(
0
,
6
)
.
map
((
record
)
=>
formatItem
(
record
,
record
.
economicLossAmount
?
`损失
${
record
.
economicLossAmount
}
`
:
'已标记经济损失'
),
),
externalImpact
:
records
.
filter
((
record
)
=>
record
.
hasExternalImpact
)
.
sort
(
sortBySeverityAndDueDate
)
.
slice
(
0
,
6
)
.
map
((
record
)
=>
formatItem
(
record
,
record
.
penaltyOrganization
||
'已标记外部影响'
)),
accountability
:
records
.
filter
((
record
)
=>
record
.
hasEmployeeAccountability
)
.
sort
(
sortBySeverityAndDueDate
)
.
slice
(
0
,
6
)
.
map
((
record
)
=>
formatItem
(
record
,
record
.
accountabilityPersonCount
?
`问责
${
record
.
accountabilityPersonCount
}
人`
:
'已标记员工问责'
,
),
),
};
}
function
collectDepartmentStats
(
records
)
{
const
departmentMap
=
new
Map
();
records
.
forEach
((
record
)
=>
{
const
departments
=
record
.
responsibleDepartments
.
length
>
0
?
record
.
responsibleDepartments
:
[
record
.
primaryResponsibleDepartment
||
'未指定'
];
departments
.
forEach
((
department
)
=>
{
const
key
=
department
||
'未指定'
;
if
(
!
departmentMap
.
has
(
key
))
{
departmentMap
.
set
(
key
,
{
name
:
key
,
totalCount
:
0
,
fixedCount
:
0
,
overdueCount
:
0
,
timelinessScoreTotal
:
0
,
timelinessScoreCount
:
0
,
qualityScoreTotal
:
0
,
qualityScoreCount
:
0
,
ratingCounts
:
{
低
:
0
,
中
:
0
,
高
:
0
,
},
});
}
const
stats
=
departmentMap
.
get
(
key
);
stats
.
totalCount
+=
1
;
if
(
record
.
isFixed
)
{
stats
.
fixedCount
+=
1
;
}
if
(
record
.
isOverdue
)
{
stats
.
overdueCount
+=
1
;
}
const
timelinessScore
=
SCORE_LEVEL_MAP
[
record
.
timelinessLevel
];
if
(
timelinessScore
)
{
stats
.
timelinessScoreTotal
+=
timelinessScore
;
stats
.
timelinessScoreCount
+=
1
;
}
const
qualityScore
=
SCORE_LEVEL_MAP
[
record
.
materialQualityLevel
];
if
(
qualityScore
)
{
stats
.
qualityScoreTotal
+=
qualityScore
;
stats
.
qualityScoreCount
+=
1
;
}
if
(
record
.
overallRating
&&
stats
.
ratingCounts
[
record
.
overallRating
]
!==
undefined
)
{
stats
.
ratingCounts
[
record
.
overallRating
]
+=
1
;
}
});
});
return
Array
.
from
(
departmentMap
.
values
()).
map
((
item
)
=>
({
...
item
,
completionRate
:
item
.
totalCount
>
0
?
(
item
.
fixedCount
/
item
.
totalCount
)
*
100
:
0
,
timelinessAverage
:
item
.
timelinessScoreCount
>
0
?
item
.
timelinessScoreTotal
/
item
.
timelinessScoreCount
:
0
,
qualityAverage
:
item
.
qualityScoreCount
>
0
?
item
.
qualityScoreTotal
/
item
.
qualityScoreCount
:
0
,
}));
}
function
buildDepartmentMatrix
(
departmentStats
)
{
return
departmentStats
.
filter
((
item
)
=>
item
.
totalCount
>
0
)
.
sort
((
left
,
right
)
=>
right
.
totalCount
-
left
.
totalCount
)
.
map
((
item
)
=>
({
name
:
item
.
name
,
totalCount
:
item
.
totalCount
,
completionRate
:
Number
(
item
.
completionRate
.
toFixed
(
2
)),
overdueCount
:
item
.
overdueCount
,
}));
}
function
buildOverdueRanking
(
departmentStats
)
{
return
sortByCountDesc
(
departmentStats
.
filter
((
item
)
=>
item
.
overdueCount
>
0
)
.
map
((
item
)
=>
({
name
:
item
.
name
,
value
:
item
.
overdueCount
,
})),
).
slice
(
0
,
10
);
}
function
buildTimelinessScatter
(
records
)
{
return
records
.
map
((
record
)
=>
{
const
plannedDays
=
diffDays
(
record
.
dueDate
,
record
.
inspectionCompletedAt
);
const
actualDays
=
diffDays
(
record
.
actualRectificationCompletedAt
,
record
.
inspectionCompletedAt
,
);
if
(
plannedDays
===
null
||
actualDays
===
null
||
plannedDays
<
0
||
actualDays
<
0
)
{
return
null
;
}
return
{
name
:
record
.
title
,
value
:
[
plannedDays
,
actualDays
,
record
.
severityLevel
||
'未评定'
],
isDelayed
:
actualDays
>
plannedDays
,
};
})
.
filter
(
Boolean
)
.
slice
(
0
,
120
);
}
function
buildScoreRanking
(
departmentStats
,
key
)
{
return
[...
departmentStats
]
.
filter
((
item
)
=>
item
[
key
]
>
0
)
.
sort
((
left
,
right
)
=>
right
[
key
]
-
left
[
key
])
.
map
((
item
)
=>
({
name
:
item
.
name
,
value
:
Number
(
item
[
key
].
toFixed
(
2
)),
}));
}
function
buildSeverityRatingHeatmap
(
records
)
{
const
matrix
=
[];
RATING_LEVELS
.
forEach
((
rating
,
yIndex
)
=>
{
SEVERITY_LEVELS
.
forEach
((
severity
,
xIndex
)
=>
{
const
count
=
records
.
filter
(
(
record
)
=>
record
.
severityLevel
===
severity
&&
record
.
overallRating
===
rating
,
).
length
;
matrix
.
push
([
xIndex
,
yIndex
,
count
]);
});
});
return
{
xLabels
:
SEVERITY_LEVELS
,
yLabels
:
RATING_LEVELS
,
data
:
matrix
,
};
}
function
buildDepartmentRatingDistribution
(
departmentStats
)
{
return
[...
departmentStats
]
.
filter
((
item
)
=>
item
.
totalCount
>
0
)
.
map
((
item
)
=>
{
const
high
=
item
.
ratingCounts
.
高
||
0
;
const
medium
=
item
.
ratingCounts
.
中
||
0
;
const
low
=
item
.
ratingCounts
.
低
||
0
;
const
ratedTotal
=
high
+
medium
+
low
;
return
{
name
:
item
.
name
,
low
,
medium
,
high
,
lowPercent
:
ratedTotal
>
0
?
(
low
/
ratedTotal
)
*
100
:
0
,
mediumPercent
:
ratedTotal
>
0
?
(
medium
/
ratedTotal
)
*
100
:
0
,
highPercent
:
ratedTotal
>
0
?
(
high
/
ratedTotal
)
*
100
:
0
,
highShare
:
ratedTotal
>
0
?
high
/
ratedTotal
:
0
,
};
})
.
sort
((
left
,
right
)
=>
right
.
highShare
-
left
.
highShare
);
}
function
summarizeRecords
(
records
)
{
const
totalCount
=
records
.
length
;
const
fixedCount
=
records
.
filter
((
item
)
=>
item
.
isFixed
).
length
;
const
overdueCount
=
records
.
filter
((
item
)
=>
item
.
isOverdue
).
length
;
const
completionRate
=
totalCount
>
0
?
(
fixedCount
/
totalCount
)
*
100
:
0
;
const
overdueRate
=
totalCount
>
0
?
(
overdueCount
/
totalCount
)
*
100
:
0
;
const
departmentStats
=
collectDepartmentStats
(
records
);
return
{
totalCount
,
fixedCount
,
completionRate
,
overdueCount
,
overdueRate
,
progressDistribution
:
buildDistributionMap
(
records
,
(
item
)
=>
item
.
progress
||
'未填写'
),
categoryTreemap
:
buildCategoryTreemap
(
records
),
rootCauseDistribution
:
buildDistributionMap
(
records
,
(
item
)
=>
item
.
rootCause
||
'未分类原因'
),
sourceComparison
:
buildSourceComparison
(
records
),
focusIssueGroups
:
buildFocusIssueGroups
(
records
),
overdueRanking
:
buildOverdueRanking
(
departmentStats
),
departmentMatrix
:
buildDepartmentMatrix
(
departmentStats
),
timelinessScatter
:
buildTimelinessScatter
(
records
),
timelinessScoreRanking
:
buildScoreRanking
(
departmentStats
,
'timelinessAverage'
),
qualityScoreRanking
:
buildScoreRanking
(
departmentStats
,
'qualityAverage'
),
policyRevisionCount
:
records
.
filter
((
item
)
=>
item
.
involvesPolicyRevision
).
length
,
systemConstructionCount
:
records
.
filter
((
item
)
=>
item
.
involvesSystemConstruction
).
length
,
severityRatingHeatmap
:
buildSeverityRatingHeatmap
(
records
),
departmentRatingDistribution
:
buildDepartmentRatingDistribution
(
departmentStats
),
};
}
function
normalizeProblemRecord
(
record
)
{
function
normalizeProblemRecord
(
record
)
{
const
metadata
=
record
?.
metadata
||
{};
const
metadata
=
record
?.
metadata
||
{};
const
dueDate
=
metadata
.
the_time_of_rectification
const
dueDate
=
parseDate
(
metadata
.
the_time_of_rectification
);
?
dayjs
(
metadata
.
the_time_of_rectification
)
const
actualRectificationCompletedAt
=
parseDate
(
metadata
.
actual_rectification_completed_at
);
:
null
;
const
inspectionCompletedAt
=
parseDate
(
metadata
.
inspection_completed_at
);
const
progress
=
normalizeText
(
metadata
.
rectification_progress
);
const
isFixed
=
isFixedProblem
(
record
);
const
now
=
dayjs
();
const
delayedByProgress
=
progress
.
includes
(
'延期'
);
const
delayedByActual
=
dueDate
&&
actualRectificationCompletedAt
&&
actualRectificationCompletedAt
.
isAfter
(
dueDate
,
'day'
);
const
delayedByPending
=
dueDate
&&
dueDate
.
isBefore
(
now
,
'day'
)
&&
!
isFixed
;
const
isDelayed
=
Boolean
(
delayedByProgress
||
delayedByActual
||
delayedByPending
);
return
{
return
{
id
:
record
?.
_id
||
record
?.
name
||
''
,
id
:
record
?.
_id
||
record
?.
name
||
''
,
title
:
normalizeText
(
metadata
.
project_name
)
||
normalizeText
(
record
?.
titl
e
)
||
'-'
,
title
:
normalizeText
(
record
?.
title
)
||
normalizeText
(
metadata
.
project_nam
e
)
||
'-'
,
projectName
:
normalizeText
(
metadata
.
project_name
),
projectName
:
normalizeText
(
metadata
.
project_name
),
inspectUnit
:
normalizeText
(
metadata
.
inspect_the_implementing_unit
),
feedbackUnit
:
normalizeText
(
metadata
.
feedback_unit
),
feedbackUnit
:
normalizeText
(
metadata
.
feedback_unit
),
implementationYear
:
extractYear
(
metadata
.
check_the_implementation_time
),
progress
,
implementationTime
:
normalizeText
(
metadata
.
check_the_implementation_time
),
progress
:
normalizeText
(
metadata
.
rectification_progress
),
auditStatus
:
normalizeText
(
metadata
.
audit_status
),
finalAuditStatus
:
normalizeText
(
metadata
.
final_audit_status
),
problemType
:
normalizeText
(
metadata
.
problem_type
),
problemFrom
:
normalizeText
(
metadata
.
problem_from
),
problemFrom
:
normalizeText
(
metadata
.
problem_from
),
problemType
:
normalizeText
(
metadata
.
problem_type
),
relatedIssueDomain
:
normalizeText
(
metadata
.
related_issue_domain
)
||
'未分类'
,
relatedIssueDomain
:
normalizeText
(
metadata
.
related_issue_domain
)
||
'未分类'
,
brief
:
normalizeText
(
metadata
.
problem_brief
)
||
'暂无问题简述'
,
severityLevel
:
normalizeText
(
metadata
.
problem_severity_level
),
categoryLevel1
:
normalizeText
(
metadata
.
problem_category_level_1
),
categoryLevel2
:
normalizeText
(
metadata
.
problem_category_level_2
),
categoryLevel3
:
normalizeText
(
metadata
.
problem_category_level_3
),
categoryLevel4
:
normalizeText
(
metadata
.
problem_category_level_4
),
rootCause
:
normalizeText
(
metadata
.
problem_root_cause
),
timelinessLevel
:
normalizeText
(
metadata
.
rectification_timeliness_level
),
materialQualityLevel
:
normalizeText
(
metadata
.
rectification_material_quality_level
),
overallRating
:
normalizeText
(
metadata
.
rectification_overall_rating
),
primaryResponsibleDepartment
:
extractPrimaryDepartment
(
metadata
),
responsibleDepartments
:
toArray
(
metadata
.
responsible_department
)
responsibleDepartments
:
toArray
(
metadata
.
responsible_department
)
.
map
((
item
)
=>
normalizeText
(
item
))
.
map
((
item
)
=>
normalizeText
(
item
))
.
filter
(
Boolean
),
.
filter
(
Boolean
),
brief
:
normalizeText
(
metadata
.
problem_brief
)
||
'暂无问题简述'
,
hasEconomicLoss
:
isAffirmative
(
metadata
.
has_economic_loss
),
detail
:
normalizeText
(
metadata
.
problem_discovery_situation
),
economicLossAmount
:
normalizeText
(
metadata
.
economic_loss_amount
),
rectificationSituation
:
normalizeText
(
metadata
.
rectification_situation
),
hasExternalImpact
:
isAffirmative
(
metadata
.
has_external_impact
),
dueDate
:
dueDate
&&
dueDate
.
isValid
()
?
dueDate
:
null
,
penaltyOrganization
:
normalizeText
(
metadata
.
penalty_organization
),
hasEmployeeAccountability
:
isAffirmative
(
metadata
.
has_employee_accountability
),
accountabilityPersonCount
:
normalizeText
(
metadata
.
accountability_person_count
),
involvesPolicyRevision
:
isAffirmative
(
metadata
.
involves_policy_revision
),
involvesSystemConstruction
:
isAffirmative
(
metadata
.
involves_system_construction
),
isHistoricalProblem
:
isAffirmative
(
metadata
.
is_historical_problem
),
isRepeatedProblem
:
isAffirmative
(
metadata
.
is_repeated_problem
),
dueDate
,
dueDateLabel
:
formatDateText
(
metadata
.
the_time_of_rectification
)
||
'未填写'
,
dueDateLabel
:
formatDateText
(
metadata
.
the_time_of_rectification
)
||
'未填写'
,
actualRectificationCompletedAt
,
actualRectificationCompletedLabel
:
formatDateText
(
metadata
.
actual_rectification_completed_at
)
||
'未填写'
,
inspectionCompletedAt
,
inspectionCompletedLabel
:
formatDateText
(
metadata
.
inspection_completed_at
)
||
'未填写'
,
fillUsers
:
toArray
(
metadata
.
fill_in_the_people
).
map
(
normalizeUserLabel
),
fillUsers
:
toArray
(
metadata
.
fill_in_the_people
).
map
(
normalizeUserLabel
),
reviewUsers
:
toArray
(
metadata
.
review_people
).
map
(
normalizeUserLabel
),
reviewUsers
:
toArray
(
metadata
.
review_people
).
map
(
normalizeUserLabel
),
isFixed
:
isFixedProblem
(
record
),
isFixed
,
isDelayed
,
isOverdue
:
delayedByPending
,
};
};
}
}
...
@@ -161,114 +585,12 @@ function buildDepartmentBuckets(records) {
...
@@ -161,114 +585,12 @@ function buildDepartmentBuckets(records) {
return
buckets
;
return
buckets
;
}
}
function
summarizeRecords
(
records
,
domainBuckets
=
[])
{
const
totalCount
=
records
.
length
;
const
fixedCount
=
records
.
filter
((
item
)
=>
item
.
isFixed
).
length
;
const
now
=
dayjs
();
const
nextMonth
=
dayjs
().
add
(
1
,
'month'
);
const
currentYear
=
now
.
year
();
const
currentYearRecords
=
records
.
filter
(
(
item
)
=>
item
.
implementationYear
===
currentYear
,
);
const
currentYearFixedCount
=
currentYearRecords
.
filter
(
(
item
)
=>
item
.
isFixed
,
).
length
;
const
upcoming
=
records
.
filter
((
item
)
=>
{
if
(
!
item
.
dueDate
||
item
.
isFixed
)
{
return
false
;
}
return
item
.
dueDate
.
isAfter
(
now
)
&&
item
.
dueDate
.
isBefore
(
nextMonth
);
})
.
sort
((
left
,
right
)
=>
left
.
dueDate
.
valueOf
()
-
right
.
dueDate
.
valueOf
());
const
overdue
=
records
.
filter
((
item
)
=>
{
if
(
!
item
.
dueDate
||
item
.
isFixed
)
{
return
false
;
}
return
item
.
dueDate
.
isBefore
(
now
);
})
.
sort
((
left
,
right
)
=>
left
.
dueDate
.
valueOf
()
-
right
.
dueDate
.
valueOf
());
const
feedbackUnitDistribution
=
Object
.
values
(
records
.
reduce
((
accumulator
,
item
)
=>
{
const
key
=
item
.
feedbackUnit
||
'未归类'
;
if
(
!
accumulator
[
key
])
{
accumulator
[
key
]
=
{
name
:
key
,
value
:
0
};
}
accumulator
[
key
].
value
+=
1
;
return
accumulator
;
},
{}),
).
sort
((
left
,
right
)
=>
right
.
value
-
left
.
value
);
const
problemTypeDistribution
=
Object
.
values
(
records
.
reduce
((
accumulator
,
item
)
=>
{
const
key
=
item
.
problemType
||
'未分类'
;
if
(
!
accumulator
[
key
])
{
accumulator
[
key
]
=
{
name
:
key
,
value
:
0
};
}
accumulator
[
key
].
value
+=
1
;
return
accumulator
;
},
{}),
).
sort
((
left
,
right
)
=>
right
.
value
-
left
.
value
);
const
progressDistribution
=
Object
.
values
(
records
.
reduce
((
accumulator
,
item
)
=>
{
const
key
=
item
.
progress
||
'未填写'
;
if
(
!
accumulator
[
key
])
{
accumulator
[
key
]
=
{
name
:
key
,
value
:
0
};
}
accumulator
[
key
].
value
+=
1
;
return
accumulator
;
},
{}),
).
sort
((
left
,
right
)
=>
right
.
value
-
left
.
value
);
const
domainDistribution
=
domainBuckets
&&
domainBuckets
.
length
>
0
?
domainBuckets
.
filter
((
item
)
=>
normalizeText
(
item
?.
key
))
.
map
((
item
)
=>
({
name
:
item
.
key
,
value
:
Number
(
item
.
doc_count
||
0
),
}))
.
sort
((
left
,
right
)
=>
right
.
value
-
left
.
value
)
:
Object
.
values
(
records
.
reduce
((
accumulator
,
item
)
=>
{
const
key
=
item
.
relatedIssueDomain
||
'未分类'
;
if
(
!
accumulator
[
key
])
{
accumulator
[
key
]
=
{
name
:
key
,
value
:
0
};
}
accumulator
[
key
].
value
+=
1
;
return
accumulator
;
},
{}),
).
sort
((
left
,
right
)
=>
right
.
value
-
left
.
value
);
return
{
totalCount
,
fixedCount
,
historyRate
:
totalCount
>
0
?
(
fixedCount
/
totalCount
)
*
100
:
0
,
currentYearRate
:
currentYearRecords
.
length
>
0
?
(
currentYearFixedCount
/
currentYearRecords
.
length
)
*
100
:
0
,
currentYearTotal
:
currentYearRecords
.
length
,
upcoming
,
overdue
,
feedbackUnitDistribution
,
problemTypeDistribution
,
progressDistribution
,
domainDistribution
,
};
}
async
function
fetchAllProblemRectificationRecords
()
{
async
function
fetchAllProblemRectificationRecords
()
{
const
firstPage
=
await
queryRecords
({
const
firstPage
=
await
queryRecords
({
subject
:
PROBLEM_RECTIFICATION_SUBJECT
,
subject
:
PROBLEM_RECTIFICATION_SUBJECT
,
page
:
0
,
page
:
0
,
page_size
:
1000
,
page_size
:
1000
,
mode
:
'full'
,
});
});
const
totalRecords
=
Number
(
firstPage
?.
total_records
||
0
);
const
totalRecords
=
Number
(
firstPage
?.
total_records
||
0
);
...
@@ -281,6 +603,7 @@ async function fetchAllProblemRectificationRecords() {
...
@@ -281,6 +603,7 @@ async function fetchAllProblemRectificationRecords() {
subject
:
PROBLEM_RECTIFICATION_SUBJECT
,
subject
:
PROBLEM_RECTIFICATION_SUBJECT
,
page
,
page
,
page_size
:
1000
,
page_size
:
1000
,
mode
:
'full'
,
});
});
records
.
push
(...(
pageResult
?.
records
||
[]));
records
.
push
(...(
pageResult
?.
records
||
[]));
}
}
...
@@ -289,44 +612,17 @@ async function fetchAllProblemRectificationRecords() {
...
@@ -289,44 +612,17 @@ async function fetchAllProblemRectificationRecords() {
return
records
;
return
records
;
}
}
async
function
fetchDomainBuckets
()
{
try
{
const
result
=
await
subjectSearch
(
PROBLEM_RECTIFICATION_SUBJECT
,
{
page
:
0
,
page_size
:
1000
,
content
:
false
,
exact
:
true
,
source
:
true
,
});
return
result
?.
aggregations
?.
related_issue_domain
?.
buckets
||
[];
}
catch
(
error
)
{
return
[];
}
}
export
async
function
loadProblemRectificationDashboardData
()
{
export
async
function
loadProblemRectificationDashboardData
()
{
try
{
try
{
const
[
rawRecords
,
domainBuckets
]
=
await
Promise
.
all
([
const
rawRecords
=
await
fetchAllProblemRectificationRecords
();
fetchAllProblemRectificationRecords
(),
fetchDomainBuckets
(),
]);
const
records
=
rawRecords
.
map
(
normalizeProblemRecord
);
const
records
=
rawRecords
.
map
(
normalizeProblemRecord
);
const
departmentBuckets
=
buildDepartmentBuckets
(
records
);
const
departmentBuckets
=
buildDepartmentBuckets
(
records
);
const
summaries
=
Object
.
fromEntries
(
const
summaries
=
Object
.
fromEntries
(
DEPARTMENT_GROUPS
.
map
((
group
)
=>
{
DEPARTMENT_GROUPS
.
map
((
group
)
=>
[
const
scopedRecords
=
departmentBuckets
[
group
.
key
]
||
[];
group
.
key
,
const
scopedDomains
=
summarizeRecords
(
departmentBuckets
[
group
.
key
]
||
[]),
group
.
key
===
'all'
]),
?
domainBuckets
:
summarizeRecords
(
scopedRecords
).
domainDistribution
;
return
[
group
.
key
,
summarizeRecords
(
scopedRecords
,
scopedDomains
),
];
}),
);
);
return
{
return
{
...
...
src/pages/problem_rectification/problemRectificationListService.js
View file @
dcdc9689
...
@@ -20,6 +20,49 @@ import {
...
@@ -20,6 +20,49 @@ import {
}
from
'@/utils/subjectView'
;
}
from
'@/utils/subjectView'
;
export
const
PROBLEM_RECTIFICATION_SUBJECT
=
'problem_rectification'
;
export
const
PROBLEM_RECTIFICATION_SUBJECT
=
'problem_rectification'
;
const
FIXED_PROGRESS_VALUES
=
new
Set
([
'已整改'
,
'已优化'
]);
const
LEVEL_ORDER
=
[
'高'
,
'中'
,
'低'
];
const
WORKBENCH_ENHANCED_COLUMN_FIELDS
=
[
'problem_severity_level'
,
'rectification_overall_rating'
,
'primary_responsible_department'
,
'problem_root_cause'
,
'actual_rectification_completed_at'
,
];
const
WORKBENCH_PRIORITY_CREATE_FIELD_NAMES
=
[
'problem_severity_level'
,
'problem_category_level_1'
,
'problem_category_level_2'
,
'problem_category_level_3'
,
'problem_category_level_4'
,
'problem_root_cause'
,
'rectification_timeliness_level'
,
'rectification_material_quality_level'
,
'has_economic_loss'
,
'economic_loss_amount'
,
'has_external_impact'
,
'penalty_organization'
,
'has_employee_accountability'
,
'accountability_person_count'
,
'involves_policy_revision'
,
'policy_revision_name'
,
'involves_system_construction'
,
'system_construction_name'
,
'is_historical_problem'
,
'is_repeated_problem'
,
'rectification_overall_rating'
,
'actual_rectification_completed_at'
,
'inspection_completed_at'
,
'primary_responsible_department'
,
];
const
FOCUS_FILTER_OPTIONS
=
[
{
label
:
'重点后果问题'
,
value
:
'consequence'
},
{
label
:
'高严重程度'
,
value
:
'high_severity'
},
{
label
:
'综合评级较低'
,
value
:
'low_rating'
},
{
label
:
'往期或重复问题'
,
value
:
'repeated_or_historical'
},
{
label
:
'已逾期未完成'
,
value
:
'overdue'
},
{
label
:
'制度/系统整改类'
,
value
:
'policy_or_system'
},
];
function
toArray
(
value
)
{
function
toArray
(
value
)
{
if
(
!
value
)
{
if
(
!
value
)
{
...
@@ -48,6 +91,17 @@ function normalizeUserLabel(value) {
...
@@ -48,6 +91,17 @@ function normalizeUserLabel(value) {
);
);
}
}
function
normalizeFlagText
(
value
)
{
const
text
=
normalizeText
(
value
);
return
text
||
'否'
;
}
function
isAffirmative
(
value
)
{
return
[
'是'
,
'有'
,
'涉及'
,
'true'
,
'1'
,
'Y'
,
'YES'
].
includes
(
normalizeText
(
value
).
toUpperCase
(),
);
}
function
formatDateText
(
value
)
{
function
formatDateText
(
value
)
{
if
(
!
value
)
{
if
(
!
value
)
{
return
''
;
return
''
;
...
@@ -60,11 +114,48 @@ function formatDateText(value) {
...
@@ -60,11 +114,48 @@ function formatDateText(value) {
return
/^
\d{4}
-
\d{2}
-
\d{2}
/
.
test
(
text
)
?
text
.
slice
(
0
,
10
)
:
text
;
return
/^
\d{4}
-
\d{2}
-
\d{2}
/
.
test
(
text
)
?
text
.
slice
(
0
,
10
)
:
text
;
}
}
function
sortLevelOptions
(
values
=
[])
{
return
[...
values
].
sort
((
left
,
right
)
=>
{
const
leftIndex
=
LEVEL_ORDER
.
indexOf
(
left
);
const
rightIndex
=
LEVEL_ORDER
.
indexOf
(
right
);
if
(
leftIndex
===
-
1
&&
rightIndex
===
-
1
)
{
return
left
.
localeCompare
(
right
,
'zh-CN'
);
}
if
(
leftIndex
===
-
1
)
{
return
1
;
}
if
(
rightIndex
===
-
1
)
{
return
-
1
;
}
return
leftIndex
-
rightIndex
;
});
}
function
findSubjectField
(
subject
,
fieldName
)
{
return
(
subject
?.
fields
||
[]).
find
((
field
)
=>
field
?.
name
===
fieldName
)
||
null
;
}
export
function
normalizeProblemRectificationRecord
(
record
)
{
export
function
normalizeProblemRectificationRecord
(
record
)
{
const
metadata
=
record
?.
metadata
||
{};
const
metadata
=
record
?.
metadata
||
{};
const
dueDate
=
metadata
.
the_time_of_rectification
const
dueDate
=
metadata
.
the_time_of_rectification
?
dayjs
(
metadata
.
the_time_of_rectification
)
?
dayjs
(
metadata
.
the_time_of_rectification
)
:
null
;
:
null
;
const
normalizedProgress
=
normalizeText
(
metadata
.
rectification_progress
);
const
isFixed
=
FIXED_PROGRESS_VALUES
.
has
(
normalizedProgress
);
const
hasEconomicLoss
=
isAffirmative
(
metadata
.
has_economic_loss
);
const
hasExternalImpact
=
isAffirmative
(
metadata
.
has_external_impact
);
const
hasEmployeeAccountability
=
isAffirmative
(
metadata
.
has_employee_accountability
);
const
involvesPolicyRevision
=
isAffirmative
(
metadata
.
involves_policy_revision
);
const
involvesSystemConstruction
=
isAffirmative
(
metadata
.
involves_system_construction
);
const
isHistoricalProblem
=
isAffirmative
(
metadata
.
is_historical_problem
);
const
isRepeatedProblem
=
isAffirmative
(
metadata
.
is_repeated_problem
);
const
now
=
dayjs
();
const
isOverdue
=
dueDate
&&
dueDate
.
isValid
()
?
dueDate
.
isBefore
(
now
,
'day'
)
&&
!
isFixed
:
false
;
return
{
return
{
id
:
record
?.
_id
||
record
?.
name
||
''
,
id
:
record
?.
_id
||
record
?.
name
||
''
,
...
@@ -72,7 +163,7 @@ export function normalizeProblemRectificationRecord(record) {
...
@@ -72,7 +163,7 @@ export function normalizeProblemRectificationRecord(record) {
title
:
normalizeText
(
record
?.
title
)
||
normalizeText
(
metadata
.
project_name
),
title
:
normalizeText
(
record
?.
title
)
||
normalizeText
(
metadata
.
project_name
),
projectName
:
normalizeText
(
metadata
.
project_name
)
||
'-'
,
projectName
:
normalizeText
(
metadata
.
project_name
)
||
'-'
,
inspectUnit
:
normalizeText
(
metadata
.
inspect_the_implementing_unit
),
inspectUnit
:
normalizeText
(
metadata
.
inspect_the_implementing_unit
),
progress
:
normalize
Text
(
metadata
.
rectification_progress
)
,
progress
:
normalize
dProgress
,
feedbackUnit
:
normalizeText
(
metadata
.
feedback_unit
),
feedbackUnit
:
normalizeText
(
metadata
.
feedback_unit
),
auditStatus
:
normalizeText
(
metadata
.
audit_status
),
auditStatus
:
normalizeText
(
metadata
.
audit_status
),
finalAuditStatus
:
normalizeText
(
metadata
.
final_audit_status
),
finalAuditStatus
:
normalizeText
(
metadata
.
final_audit_status
),
...
@@ -80,6 +171,37 @@ export function normalizeProblemRectificationRecord(record) {
...
@@ -80,6 +171,37 @@ export function normalizeProblemRectificationRecord(record) {
problemFrom
:
normalizeText
(
metadata
.
problem_from
),
problemFrom
:
normalizeText
(
metadata
.
problem_from
),
relatedIssueDomain
:
normalizeText
(
metadata
.
related_issue_domain
),
relatedIssueDomain
:
normalizeText
(
metadata
.
related_issue_domain
),
brief
:
normalizeText
(
metadata
.
problem_brief
),
brief
:
normalizeText
(
metadata
.
problem_brief
),
severityLevel
:
normalizeText
(
metadata
.
problem_severity_level
),
categoryLevel1
:
normalizeText
(
metadata
.
problem_category_level_1
),
categoryLevel2
:
normalizeText
(
metadata
.
problem_category_level_2
),
categoryLevel3
:
normalizeText
(
metadata
.
problem_category_level_3
),
categoryLevel4
:
normalizeText
(
metadata
.
problem_category_level_4
),
rootCause
:
normalizeText
(
metadata
.
problem_root_cause
),
timelinessLevel
:
normalizeText
(
metadata
.
rectification_timeliness_level
),
materialQualityLevel
:
normalizeText
(
metadata
.
rectification_material_quality_level
),
overallRating
:
normalizeText
(
metadata
.
rectification_overall_rating
),
primaryResponsibleDepartment
:
normalizeText
(
metadata
.
primary_responsible_department
),
hasEconomicLoss
,
hasEconomicLossLabel
:
normalizeFlagText
(
metadata
.
has_economic_loss
),
economicLossAmount
:
normalizeText
(
metadata
.
economic_loss_amount
),
hasExternalImpact
,
hasExternalImpactLabel
:
normalizeFlagText
(
metadata
.
has_external_impact
),
penaltyOrganization
:
normalizeText
(
metadata
.
penalty_organization
),
hasEmployeeAccountability
,
hasEmployeeAccountabilityLabel
:
normalizeFlagText
(
metadata
.
has_employee_accountability
),
accountabilityPersonCount
:
normalizeText
(
metadata
.
accountability_person_count
),
involvesPolicyRevision
,
involvesPolicyRevisionLabel
:
normalizeFlagText
(
metadata
.
involves_policy_revision
),
policyRevisionName
:
normalizeText
(
metadata
.
policy_revision_name
),
involvesSystemConstruction
,
involvesSystemConstructionLabel
:
normalizeFlagText
(
metadata
.
involves_system_construction
),
systemConstructionName
:
normalizeText
(
metadata
.
system_construction_name
),
isHistoricalProblem
,
isHistoricalProblemLabel
:
normalizeFlagText
(
metadata
.
is_historical_problem
),
isRepeatedProblem
,
isRepeatedProblemLabel
:
normalizeFlagText
(
metadata
.
is_repeated_problem
),
actualRectificationCompletedAt
:
formatDateText
(
metadata
.
actual_rectification_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
:
toArray
(
metadata
.
fill_in_the_people
)
.
map
(
normalizeUserLabel
)
.
map
(
normalizeUserLabel
)
...
@@ -88,6 +210,8 @@ export function normalizeProblemRectificationRecord(record) {
...
@@ -88,6 +210,8 @@ export function normalizeProblemRectificationRecord(record) {
.
map
(
normalizeUserLabel
)
.
map
(
normalizeUserLabel
)
.
filter
(
Boolean
),
.
filter
(
Boolean
),
dueDateLabel
:
formatDateText
(
metadata
.
the_time_of_rectification
)
||
'-'
,
dueDateLabel
:
formatDateText
(
metadata
.
the_time_of_rectification
)
||
'-'
,
isFixed
,
isOverdue
,
raw
:
record
,
raw
:
record
,
};
};
}
}
...
@@ -139,7 +263,28 @@ export function buildGridViews(views = []) {
...
@@ -139,7 +263,28 @@ export function buildGridViews(views = []) {
}
}
export
function
buildViewColumnsForList
(
view
,
subject
)
{
export
function
buildViewColumnsForList
(
view
,
subject
)
{
return
buildViewColumns
(
view
,
subject
).
filter
((
column
)
=>
column
.
field
!==
'title'
||
!
column
.
hide
);
const
baseColumns
=
buildViewColumns
(
view
,
subject
).
filter
(
(
column
)
=>
column
.
field
!==
'title'
||
!
column
.
hide
,
);
const
existingFields
=
new
Set
(
baseColumns
.
map
((
column
)
=>
getFieldNameFromPath
(
column
.
field
)));
const
extraColumns
=
WORKBENCH_ENHANCED_COLUMN_FIELDS
.
filter
((
fieldName
)
=>
!
existingFields
.
has
(
fieldName
))
.
map
((
fieldName
)
=>
{
const
fieldDef
=
findSubjectField
(
subject
,
fieldName
);
if
(
!
fieldDef
)
{
return
null
;
}
return
{
field
:
`metadata.
${
fieldDef
.
name
}
`
,
label
:
fieldDef
.
label
||
fieldDef
.
name
,
width
:
168
,
fieldDef
,
};
})
.
filter
(
Boolean
);
return
[...
baseColumns
,
...
extraColumns
];
}
}
export
function
getRecordCellText
(
record
,
column
)
{
export
function
getRecordCellText
(
record
,
column
)
{
...
@@ -152,6 +297,12 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
...
@@ -152,6 +297,12 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
const
nextKeyword
=
filters
?.
keyword
||
''
;
const
nextKeyword
=
filters
?.
keyword
||
''
;
const
nextProgress
=
filters
?.
progress
||
''
;
const
nextProgress
=
filters
?.
progress
||
''
;
const
nextAuditStatus
=
filters
?.
auditStatus
||
''
;
const
nextAuditStatus
=
filters
?.
auditStatus
||
''
;
const
nextSeverityLevel
=
filters
?.
severityLevel
||
''
;
const
nextOverallRating
=
filters
?.
overallRating
||
''
;
const
nextPrimaryResponsibleDepartment
=
filters
?.
primaryResponsibleDepartment
||
''
;
const
nextProblemFrom
=
filters
?.
problemFrom
||
''
;
const
nextRootCause
=
filters
?.
rootCause
||
''
;
const
nextFocusFlag
=
filters
?.
focusFlag
||
''
;
const
columns
=
buildViewColumnsForList
(
gridView
,
subject
);
const
columns
=
buildViewColumnsForList
(
gridView
,
subject
);
const
searchableFields
=
columns
.
map
((
column
)
=>
column
.
field
);
const
searchableFields
=
columns
.
map
((
column
)
=>
column
.
field
);
...
@@ -183,6 +334,67 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
...
@@ -183,6 +334,67 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
return
false
;
return
false
;
}
}
if
(
nextSeverityLevel
&&
record
.
severityLevel
!==
nextSeverityLevel
)
{
return
false
;
}
if
(
nextOverallRating
&&
record
.
overallRating
!==
nextOverallRating
)
{
return
false
;
}
if
(
nextPrimaryResponsibleDepartment
&&
record
.
primaryResponsibleDepartment
!==
nextPrimaryResponsibleDepartment
)
{
return
false
;
}
if
(
nextProblemFrom
&&
record
.
problemFrom
!==
nextProblemFrom
)
{
return
false
;
}
if
(
nextRootCause
&&
record
.
rootCause
!==
nextRootCause
)
{
return
false
;
}
if
(
nextFocusFlag
===
'consequence'
)
{
if
(
!
record
.
hasEconomicLoss
&&
!
record
.
hasExternalImpact
&&
!
record
.
hasEmployeeAccountability
)
{
return
false
;
}
}
if
(
nextFocusFlag
===
'high_severity'
&&
record
.
severityLevel
!==
'高'
)
{
return
false
;
}
if
(
nextFocusFlag
===
'low_rating'
&&
record
.
overallRating
!==
'低'
)
{
return
false
;
}
if
(
nextFocusFlag
===
'repeated_or_historical'
&&
!
record
.
isHistoricalProblem
&&
!
record
.
isRepeatedProblem
)
{
return
false
;
}
if
(
nextFocusFlag
===
'overdue'
&&
!
record
.
isOverdue
)
{
return
false
;
}
if
(
nextFocusFlag
===
'policy_or_system'
&&
!
record
.
involvesPolicyRevision
&&
!
record
.
involvesSystemConstruction
)
{
return
false
;
}
if
(
!
nextKeyword
)
{
if
(
!
nextKeyword
)
{
return
true
;
return
true
;
}
}
...
@@ -205,11 +417,37 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
...
@@ -205,11 +417,37 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
new
Set
(
records
.
map
((
item
)
=>
item
.
auditStatus
).
filter
(
Boolean
)),
new
Set
(
records
.
map
((
item
)
=>
item
.
auditStatus
).
filter
(
Boolean
)),
);
);
const
severityOptions
=
sortLevelOptions
(
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
severityLevel
).
filter
(
Boolean
))),
);
const
overallRatingOptions
=
sortLevelOptions
(
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
overallRating
).
filter
(
Boolean
))),
);
const
primaryResponsibleDepartmentOptions
=
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
primaryResponsibleDepartment
).
filter
(
Boolean
)),
).
sort
((
left
,
right
)
=>
left
.
localeCompare
(
right
,
'zh-CN'
));
const
problemFromOptions
=
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
problemFrom
).
filter
(
Boolean
)),
);
const
rootCauseOptions
=
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
rootCause
).
filter
(
Boolean
)),
).
sort
((
left
,
right
)
=>
left
.
localeCompare
(
right
,
'zh-CN'
));
return
{
return
{
filteredRecords
:
sortedRecords
,
filteredRecords
:
sortedRecords
,
columns
,
columns
,
progressOptions
,
progressOptions
,
auditStatusOptions
,
auditStatusOptions
,
severityOptions
,
overallRatingOptions
,
primaryResponsibleDepartmentOptions
,
problemFromOptions
,
rootCauseOptions
,
focusOptions
:
FOCUS_FILTER_OPTIONS
,
};
};
}
}
...
@@ -326,7 +564,27 @@ export function buildProblemRectificationCreateFormFields(gridView, subject) {
...
@@ -326,7 +564,27 @@ export function buildProblemRectificationCreateFormFields(gridView, subject) {
});
});
if
(
fields
.
length
>
0
)
{
if
(
fields
.
length
>
0
)
{
return
fields
;
const
nextFields
=
[...
fields
];
WORKBENCH_PRIORITY_CREATE_FIELD_NAMES
.
forEach
((
fieldName
)
=>
{
if
(
seen
.
has
(
fieldName
))
{
return
;
}
const
fieldDef
=
findSubjectField
(
subject
,
fieldName
);
if
(
!
fieldDef
)
{
return
;
}
seen
.
add
(
fieldName
);
nextFields
.
push
({
field
:
`metadata.
${
fieldDef
.
name
}
`
,
label
:
fieldDef
.
label
||
fieldDef
.
name
,
fieldDef
,
});
});
return
nextFields
;
}
}
return
(
subject
?.
fields
||
[]).
map
((
field
)
=>
({
return
(
subject
?.
fields
||
[]).
map
((
field
)
=>
({
...
...
src/services/subjectsDefinition.md
View file @
dcdc9689
...
@@ -125,6 +125,30 @@ type Record = {
...
@@ -125,6 +125,30 @@ type Record = {
| 最终审批状态 |
`final_audit_status`
| select | 否 | 否 | 否 |
| 最终审批状态 |
`final_audit_status`
| select | 否 | 否 | 否 |
| 备注 |
`remark`
| long_text | 否 | 否 | 否 |
| 备注 |
`remark`
| long_text | 否 | 否 | 否 |
| 整改证明材料 |
`rectification_proof_materials`
| file | 否 | 否 | 否 |
| 整改证明材料 |
`rectification_proof_materials`
| file | 否 | 否 | 否 |
| 是否造成经济损失 |
`has_economic_loss`
| text | 否 | 否 | 否 |
| 对应经济损失 |
`economic_loss_amount`
| text | 否 | 否 | 否 |
| 是否造成外部影响 |
`has_external_impact`
| text | 否 | 否 | 否 |
| 处罚机构 |
`penalty_organization`
| text | 否 | 否 | 否 |
| 是否涉及员工问责 |
`has_employee_accountability`
| text | 否 | 否 | 否 |
| 问责人数 |
`accountability_person_count`
| text | 否 | 否 | 否 |
| 问题综合严重程度 |
`problem_severity_level`
| text | 否 | 否 | 否 |
| 问题一级分类 |
`problem_category_level_1`
| text | 否 | 否 | 否 |
| 问题二级分类 |
`problem_category_level_2`
| text | 否 | 否 | 否 |
| 问题三级分类 |
`problem_category_level_3`
| text | 否 | 否 | 否 |
| 问题四级分类 |
`problem_category_level_4`
| text | 否 | 否 | 否 |
| 问题原因 |
`problem_root_cause`
| text | 否 | 否 | 否 |
| 整改时间评估 |
`rectification_timeliness_level`
| text | 否 | 否 | 否 |
| 整改证明材料质量 |
`rectification_material_quality_level`
| text | 否 | 否 | 否 |
| 是否涉及制度修订 |
`involves_policy_revision`
| text | 否 | 否 | 否 |
| 制度名称 |
`policy_revision_name`
| text | 否 | 否 | 否 |
| 是否涉及系统建设 |
`involves_system_construction`
| text | 否 | 否 | 否 |
| 系统名称 |
`system_construction_name`
| text | 否 | 否 | 否 |
| 是否为往期发现问题 |
`is_historical_problem`
| text | 否 | 否 | 否 |
| 问题是否重复出现 |
`is_repeated_problem`
| text | 否 | 否 | 否 |
| 综合整改评定等级 |
`rectification_overall_rating`
| text | 否 | 否 | 否 |
| 实际整改完成时间 |
`actual_rectification_completed_at`
| text | 否 | 否 | 否 |
| 实际检查完成日期 |
`inspection_completed_at`
| text | 否 | 否 | 否 |
| 主责部门 |
`primary_responsible_department`
| text | 否 | 否 | 否 |
### 3.1.1 字段说明
### 3.1.1 字段说明
...
@@ -133,6 +157,7 @@ type Record = {
...
@@ -133,6 +157,7 @@ type Record = {
-
`audit_status`
用于复核阶段结果,旧应用已使用
`已审批`
、
`已驳回`
-
`audit_status`
用于复核阶段结果,旧应用已使用
`已审批`
、
`已驳回`
-
`final_audit_status`
用于最终审批阶段结果,旧应用已使用
`已审批`
、
`已驳回`
-
`final_audit_status`
用于最终审批阶段结果,旧应用已使用
`已审批`
、
`已驳回`
-
`rectification_progress`
在旧驾驶舱统计中已使用
`已整改`
、
`已优化`
两个值判断“已整改问题”
-
`rectification_progress`
在旧驾驶舱统计中已使用
`已整改`
、
`已优化`
两个值判断“已整改问题”
-
新增驾驶舱扩展字段当前在线上 Subject 中大多仍为
`text`
类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析
-
其余
`select`
类型的可选值必须以线上 Subject 配置中的
`settings.options`
为准,禁止在页面内硬编码
-
其余
`select`
类型的可选值必须以线上 Subject 配置中的
`settings.options`
为准,禁止在页面内硬编码
### 3.1.2 metadata 示例
### 3.1.2 metadata 示例
...
...
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