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
b52ff11f
Commit
b52ff11f
authored
May 28, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: bugs
parent
40f35588
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
587 additions
and
408 deletions
+587
-408
TailwindEditableField.jsx
...s/RecordDetailDrawer/components/TailwindEditableField.jsx
+79
-3
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+12
-1
Layout.jsx
src/pages/problem_rectification/Layout.jsx
+5
-14
index.jsx
src/pages/problem_rectification/list/index.jsx
+445
-390
problem-rectification.business-flow.md
...blem_rectification/problem-rectification.business-flow.md
+21
-0
problemRectificationListService.js
.../problem_rectification/problemRectificationListService.js
+25
-0
No files found.
src/pages/RecordDetailDrawer/components/TailwindEditableField.jsx
View file @
b52ff11f
...
@@ -27,6 +27,10 @@ function asSaveValue(value, fieldDef) {
...
@@ -27,6 +27,10 @@ function asSaveValue(value, fieldDef) {
return
value
;
return
value
;
}
}
function
getClearedValue
(
fieldDef
)
{
return
fieldDef
?.
multiple
?
[]
:
null
;
}
function
Pill
({
children
,
tone
=
'zinc'
})
{
function
Pill
({
children
,
tone
=
'zinc'
})
{
const
toneClass
=
const
toneClass
=
tone
===
'rose'
tone
===
'rose'
...
@@ -241,6 +245,12 @@ export default function TailwindEditableField({
...
@@ -241,6 +245,12 @@ export default function TailwindEditableField({
}
}
};
};
const
clearValue
=
async
()
=>
{
const
clearedValue
=
getClearedValue
(
fieldDef
);
setDraft
(
clearedValue
);
await
save
(
clearedValue
);
};
const
cancel
=
()
=>
{
const
cancel
=
()
=>
{
setDraft
(
value
);
setDraft
(
value
);
setError
(
''
);
setError
(
''
);
...
@@ -259,7 +269,8 @@ export default function TailwindEditableField({
...
@@ -259,7 +269,8 @@ export default function TailwindEditableField({
try
{
try
{
const
uploaded
=
await
uploadFile
(
file
);
const
uploaded
=
await
uploadFile
(
file
);
const
fileValue
=
uploaded
?.
file
||
uploaded
?.
data
||
uploaded
;
const
fileValue
=
uploaded
?.
file
||
uploaded
?.
data
||
uploaded
;
const
nextValue
=
fieldDef
?.
multiple
?
[...
toArray
(
value
),
fileValue
]
:
fileValue
;
const
nextValue
=
fieldDef
?.
multiple
?
[...
toArray
(
draft
),
fileValue
]
:
fileValue
;
setDraft
(
nextValue
);
await
onSave
(
nextValue
);
await
onSave
(
nextValue
);
setEditing
(
false
);
setEditing
(
false
);
}
catch
(
uploadError
)
{
}
catch
(
uploadError
)
{
...
@@ -269,6 +280,25 @@ export default function TailwindEditableField({
...
@@ -269,6 +280,25 @@ export default function TailwindEditableField({
}
}
};
};
const
handleRemoveFile
=
async
(
indexToRemove
)
=>
{
setError
(
''
);
const
nextValue
=
fieldDef
?.
multiple
?
toArray
(
draft
).
filter
((
_
,
index
)
=>
index
!==
indexToRemove
)
:
null
;
try
{
setDraft
(
nextValue
);
await
onSave
(
nextValue
);
if
(
!
fieldDef
?.
multiple
)
{
setEditing
(
false
);
}
}
catch
(
saveError
)
{
setDraft
(
draft
);
setError
(
saveError
?.
message
||
'删除附件失败,请稍后重试。'
);
}
};
if
(
!
editing
)
{
if
(
!
editing
)
{
return
(
return
(
<
button
<
button
...
@@ -300,7 +330,10 @@ export default function TailwindEditableField({
...
@@ -300,7 +330,10 @@ export default function TailwindEditableField({
<
div
className=
"flex items-center gap-3 rounded-2xl border border-zinc-200 bg-white px-3 py-2"
>
<
div
className=
"flex items-center gap-3 rounded-2xl border border-zinc-200 bg-white px-3 py-2"
>
<
Checkbox
checked=
{
Boolean
(
draft
)
}
onChange=
{
(
checked
)
=>
setDraft
(
checked
)
}
color=
"rose"
/>
<
Checkbox
checked=
{
Boolean
(
draft
)
}
onChange=
{
(
checked
)
=>
setDraft
(
checked
)
}
color=
"rose"
/>
<
span
className=
"text-sm text-zinc-700"
>
{
draft
?
'是'
:
'否'
}
</
span
>
<
span
className=
"text-sm text-zinc-700"
>
{
draft
?
'是'
:
'否'
}
</
span
>
<
button
type=
"button"
onClick=
{
()
=>
save
(
Boolean
(
draft
))
}
className=
"ml-auto text-sm font-medium text-rose-700"
>
<
button
type=
"button"
onClick=
{
clearValue
}
className=
"ml-auto text-sm font-medium text-rose-600"
>
清空
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
(
Boolean
(
draft
))
}
className=
"text-sm font-medium text-rose-700"
>
保存
保存
</
button
>
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
...
@@ -334,6 +367,7 @@ export default function TailwindEditableField({
...
@@ -334,6 +367,7 @@ export default function TailwindEditableField({
))
}
))
}
</
Select
>
</
Select
>
<
div
className=
"flex justify-end gap-2"
>
<
div
className=
"flex justify-end gap-2"
>
<
button
type=
"button"
onClick=
{
clearValue
}
className=
"text-sm font-medium text-rose-600"
>
清空
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
()
}
className=
"text-sm font-medium text-rose-700"
>
保存
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
()
}
className=
"text-sm font-medium text-rose-700"
>
保存
</
button
>
</
div
>
</
div
>
...
@@ -347,6 +381,7 @@ export default function TailwindEditableField({
...
@@ -347,6 +381,7 @@ export default function TailwindEditableField({
<
div
className=
"space-y-3"
>
<
div
className=
"space-y-3"
>
<
UserSelector
value=
{
draft
}
onChange=
{
setDraft
}
multiple=
{
fieldDef
?.
multiple
}
/>
<
UserSelector
value=
{
draft
}
onChange=
{
setDraft
}
multiple=
{
fieldDef
?.
multiple
}
/>
<
div
className=
"flex justify-end gap-2"
>
<
div
className=
"flex justify-end gap-2"
>
<
button
type=
"button"
onClick=
{
clearValue
}
className=
"text-sm font-medium text-rose-600"
>
清空
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
()
}
className=
"text-sm font-medium text-rose-700"
>
保存
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
()
}
className=
"text-sm font-medium text-rose-700"
>
保存
</
button
>
</
div
>
</
div
>
...
@@ -364,6 +399,7 @@ export default function TailwindEditableField({
...
@@ -364,6 +399,7 @@ export default function TailwindEditableField({
onChange=
{
setDraft
}
onChange=
{
setDraft
}
/>
/>
<
div
className=
"flex justify-end gap-2"
>
<
div
className=
"flex justify-end gap-2"
>
<
button
type=
"button"
onClick=
{
clearValue
}
className=
"text-sm font-medium text-rose-600"
>
清空
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
()
}
className=
"text-sm font-medium text-rose-700"
>
保存
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
()
}
className=
"text-sm font-medium text-rose-700"
>
保存
</
button
>
</
div
>
</
div
>
...
@@ -375,10 +411,47 @@ export default function TailwindEditableField({
...
@@ -375,10 +411,47 @@ export default function TailwindEditableField({
if
(
fieldType
===
'file'
)
{
if
(
fieldType
===
'file'
)
{
return
(
return
(
<
div
className=
"space-y-3 rounded-2xl border border-zinc-200 bg-white p-3"
>
<
div
className=
"space-y-3 rounded-2xl border border-zinc-200 bg-white p-3"
>
<
FieldValue
value=
{
value
}
fieldDef=
{
fieldDef
}
/>
<
div
className=
"space-y-2"
>
{
toArray
(
draft
).
length
===
0
?
(
<
span
className=
"text-zinc-400"
>
未上传
</
span
>
)
:
(
toArray
(
draft
).
map
((
file
,
index
)
=>
{
const
label
=
file
?.
name
||
file
?.
title
||
file
?.
url
||
`附件 ${index + 1}`
;
return
(
<
div
key=
{
`${label}-${index}`
}
className=
"flex items-center justify-between gap-3 rounded-2xl border border-zinc-100 bg-zinc-50 px-3 py-2"
>
<
div
className=
"min-w-0 flex-1"
>
{
file
?.
url
?
(
<
a
href=
{
file
.
url
}
target=
"_blank"
rel=
"noreferrer"
className=
"text-sm font-medium text-sky-700 transition hover:text-sky-800"
>
{
label
}
</
a
>
)
:
(
<
div
className=
"text-sm font-medium text-zinc-700"
>
{
label
}
</
div
>
)
}
</
div
>
<
button
type=
"button"
onClick=
{
()
=>
handleRemoveFile
(
index
)
}
className=
"shrink-0 text-sm font-medium text-rose-600"
>
删除
</
button
>
</
div
>
);
})
)
}
</
div
>
<
input
type=
"file"
onChange=
{
handleFileUpload
}
disabled=
{
uploading
}
className=
"block w-full text-sm text-zinc-600"
/>
<
input
type=
"file"
onChange=
{
handleFileUpload
}
disabled=
{
uploading
}
className=
"block w-full text-sm text-zinc-600"
/>
{
error
?
<
div
className=
"text-sm text-rose-600"
>
{
error
}
</
div
>
:
null
}
{
error
?
<
div
className=
"text-sm text-rose-600"
>
{
error
}
</
div
>
:
null
}
<
div
className=
"flex justify-end gap-2"
>
<
div
className=
"flex justify-end gap-2"
>
<
button
type=
"button"
onClick=
{
clearValue
}
className=
"text-sm font-medium text-rose-600"
>
清空
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
span
className=
"text-sm text-zinc-400"
>
{
uploading
?
'上传中...'
:
'选择文件后自动保存'
}
</
span
>
<
span
className=
"text-sm text-zinc-400"
>
{
uploading
?
'上传中...'
:
'选择文件后自动保存'
}
</
span
>
</
div
>
</
div
>
...
@@ -393,6 +466,7 @@ export default function TailwindEditableField({
...
@@ -393,6 +466,7 @@ export default function TailwindEditableField({
<
Textarea
rows=
{
8
}
value=
{
jsonValue
}
onChange=
{
(
event
)
=>
setDraft
(
event
.
target
.
value
)
}
resizable
/>
<
Textarea
rows=
{
8
}
value=
{
jsonValue
}
onChange=
{
(
event
)
=>
setDraft
(
event
.
target
.
value
)
}
resizable
/>
{
error
?
<
div
className=
"text-sm text-rose-600"
>
{
error
}
</
div
>
:
null
}
{
error
?
<
div
className=
"text-sm text-rose-600"
>
{
error
}
</
div
>
:
null
}
<
div
className=
"flex justify-end gap-2"
>
<
div
className=
"flex justify-end gap-2"
>
<
button
type=
"button"
onClick=
{
clearValue
}
className=
"text-sm font-medium text-rose-600"
>
清空
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
<
button
type=
"button"
type=
"button"
...
@@ -411,6 +485,7 @@ export default function TailwindEditableField({
...
@@ -411,6 +485,7 @@ export default function TailwindEditableField({
<
div
className=
"space-y-3"
>
<
div
className=
"space-y-3"
>
<
Textarea
rows=
{
5
}
value=
{
draft
||
''
}
onChange=
{
(
event
)
=>
setDraft
(
event
.
target
.
value
)
}
autoFocus
/>
<
Textarea
rows=
{
5
}
value=
{
draft
||
''
}
onChange=
{
(
event
)
=>
setDraft
(
event
.
target
.
value
)
}
autoFocus
/>
<
div
className=
"flex justify-end gap-2"
>
<
div
className=
"flex justify-end gap-2"
>
<
button
type=
"button"
onClick=
{
clearValue
}
className=
"text-sm font-medium text-rose-600"
>
清空
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
()
}
className=
"text-sm font-medium text-rose-700"
>
保存
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
()
}
className=
"text-sm font-medium text-rose-700"
>
保存
</
button
>
</
div
>
</
div
>
...
@@ -433,6 +508,7 @@ export default function TailwindEditableField({
...
@@ -433,6 +508,7 @@ export default function TailwindEditableField({
autoFocus
autoFocus
/>
/>
<
div
className=
"flex justify-end gap-2"
>
<
div
className=
"flex justify-end gap-2"
>
<
button
type=
"button"
onClick=
{
clearValue
}
className=
"text-sm font-medium text-rose-600"
>
清空
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
cancel
}
className=
"text-sm text-zinc-500"
>
取消
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
()
}
className=
"text-sm font-medium text-rose-700"
>
保存
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
save
()
}
className=
"text-sm font-medium text-rose-700"
>
保存
</
button
>
</
div
>
</
div
>
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
b52ff11f
...
@@ -63,6 +63,7 @@ const RecordDetailDrawer = (props, ref) => {
...
@@ -63,6 +63,7 @@ const RecordDetailDrawer = (props, ref) => {
onDataChange
,
onDataChange
,
readonly
=
false
,
readonly
=
false
,
filterView
=
[],
filterView
=
[],
preferredViewName
,
}
=
props
;
}
=
props
;
const
[
refreshKey
,
setRefreshKey
]
=
useState
(()
=>
Date
.
now
());
const
[
refreshKey
,
setRefreshKey
]
=
useState
(()
=>
Date
.
now
());
...
@@ -112,11 +113,21 @@ const RecordDetailDrawer = (props, ref) => {
...
@@ -112,11 +113,21 @@ const RecordDetailDrawer = (props, ref) => {
return
;
return
;
}
}
if
(
preferredViewName
)
{
const
matchedView
=
detailViews
.
find
(
(
view
)
=>
view
.
name
===
preferredViewName
||
view
.
title
===
preferredViewName
,
);
if
(
matchedView
&&
matchedView
.
name
!==
activeTabKey
)
{
setActiveTabKey
(
matchedView
.
name
);
return
;
}
}
const
currentTab
=
detailViews
.
find
((
view
)
=>
view
.
name
===
activeTabKey
);
const
currentTab
=
detailViews
.
find
((
view
)
=>
view
.
name
===
activeTabKey
);
if
(
!
currentTab
)
{
if
(
!
currentTab
)
{
setActiveTabKey
(
detailViews
[
0
].
name
);
setActiveTabKey
(
detailViews
[
0
].
name
);
}
}
},
[
detailViews
,
activeTabKey
]);
},
[
detailViews
,
activeTabKey
,
preferredViewName
]);
useImperativeHandle
(
useImperativeHandle
(
ref
,
ref
,
...
...
src/pages/problem_rectification/Layout.jsx
View file @
b52ff11f
import
xyLogo
from
'@/assets/xy-logo.png'
;
import
xyLogoSmall
from
'@/assets/xy-logo_small.png'
;
import
xyLogoSmall
from
'@/assets/xy-logo_small.png'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
import
{
...
@@ -244,16 +243,13 @@ export default function ProblemRectificationLayout() {
...
@@ -244,16 +243,13 @@ export default function ProblemRectificationLayout() {
sidebarCollapsed
?
'justify-center'
:
'gap-3 px-1'
,
sidebarCollapsed
?
'justify-center'
:
'gap-3 px-1'
,
)
}
)
}
>
>
{
sidebarCollapsed
?
(
<
img
<
img
src=
{
sidebarCollapsed
?
xyLogoSmall
:
xyLogo
}
src=
{
xyLogoSmall
}
alt=
"XY Logo"
alt=
"XY Logo"
className=
{
clsx
(
className=
"h-8 w-auto max-w-8 shrink-0 object-contain"
'shrink-0 object-contain'
,
sidebarCollapsed
?
'h-8 w-auto max-w-8'
:
'h-8 w-auto max-w-[140px]'
,
)
}
/>
/>
)
:
null
}
{
!
sidebarCollapsed
?
(
{
!
sidebarCollapsed
?
(
<
div
className=
"min-w-0"
>
<
div
className=
"min-w-0"
>
<
h1
className=
"truncate text-sm font-semibold tracking-[0.02em] text-white/95"
>
<
h1
className=
"truncate text-sm font-semibold tracking-[0.02em] text-white/95"
>
...
@@ -327,12 +323,7 @@ export default function ProblemRectificationLayout() {
...
@@ -327,12 +323,7 @@ export default function ProblemRectificationLayout() {
close=
{
()
=>
setIsMobileSidebarOpen
(
false
)
}
close=
{
()
=>
setIsMobileSidebarOpen
(
false
)
}
>
>
<
div
className=
"px-5 pt-6"
>
<
div
className=
"px-5 pt-6"
>
<
div
className=
"flex items-center gap-2"
>
<
div
>
<
img
src=
{
xyLogo
}
alt=
"XY Logo"
className=
"h-8 w-auto max-w-[140px] shrink-0 object-contain"
/>
<
div
>
<
div
>
<
h1
className=
"text-base font-semibold tracking-[0.02em] text-white"
>
<
h1
className=
"text-base font-semibold tracking-[0.02em] text-white"
>
{
APP_NAME
}
{
APP_NAME
}
...
...
src/pages/problem_rectification/list/index.jsx
View file @
b52ff11f
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
,
...
@@ -8,14 +9,22 @@ import {
...
@@ -8,14 +9,22 @@ 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
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
{
import
{
batchRemoveRecords
,
batchRemoveRecords
,
batchUpdateRecords
,
batchUpdateRecords
,
createNotice
,
createNotice
,
createRecord
,
createRecord
,
executeAction
,
executeAction
,
loadSubject
,
loadUser
,
loadUser
,
queryUsers
,
queryUsers
,
sendNotice
,
sendNotice
,
...
@@ -24,37 +33,28 @@ import {
...
@@ -24,37 +33,28 @@ import {
import
*
as
Headless
from
'@headlessui/react'
;
import
*
as
Headless
from
'@headlessui/react'
;
import
clsx
from
'clsx'
;
import
clsx
from
'clsx'
;
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
import
{
useCallback
,
useEffect
,
useMemo
,
use
Ref
,
use
State
}
from
'react'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
import
{
emptyValueForField
,
formatFieldValue
,
getFieldNameFromPath
,
getFieldValue
,
getSelectOptions
,
getSelectOptions
,
normalizeText
,
normalizeText
,
}
from
'@/utils/subjectView'
;
}
from
'@/utils/subjectView'
;
import
{
DEPARTMENT_GROUPS
}
from
'../problemRectificationDashboardService'
;
import
{
import
{
PROBLEM_RECTIFICATION_SUBJECT
,
PROBLEM_RECTIFICATION_SUBJECT
,
buildGridViewCountMap
,
buildGridViews
,
buildInitialCreateValues
,
buildProblemRectificationConfiguredListView
,
buildProblemRectificationCreateFormFields
,
buildProblemRectificationCreatePayload
,
buildProblemRectificationCreatePayload
,
buildProblemRectificationListView
,
getRecordCellText
,
loadProblemRectificationList
Data
,
loadProblemRectificationList
Resources
,
}
from
'../problemRectificationListService'
;
}
from
'../problemRectificationListService'
;
const
CREATE_FIELD_ORDER
=
[
const
SELECTION_COLUMN_WIDTH
=
56
;
'project_name'
,
const
ACTION_COLUMN_WIDTH
=
132
;
'inspect_the_implementing_unit'
,
'check_the_implementation_time'
,
'inspection_report_date'
,
'rectification_progress'
,
'problem_from'
,
'problem_type'
,
'feedback_unit'
,
'related_issue_domain'
,
'the_time_of_rectification'
,
'responsible_department'
,
'problem_brief'
,
'problem_discovery_situation'
,
'rectification_situation'
,
'remark'
,
];
const
FIELD_HINTS
=
{
const
FIELD_HINTS
=
{
inspect_the_implementing_unit
:
'按对象配置的实施单位选项选择'
,
inspect_the_implementing_unit
:
'按对象配置的实施单位选项选择'
,
...
@@ -66,20 +66,86 @@ const FIELD_HINTS = {
...
@@ -66,20 +66,86 @@ const FIELD_HINTS = {
the_time_of_rectification
:
'提交时会自动转成 ISO 8601 时间'
,
the_time_of_rectification
:
'提交时会自动转成 ISO 8601 时间'
,
};
};
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'
})
{
function
SummaryCard
({
label
,
value
,
hint
,
tone
=
'default'
})
{
const
toneClass
=
const
toneClass
=
tone
===
'rose'
tone
===
'rose'
?
'border-rose-200
bg-rose-50/80
'
?
'border-rose-200
/80 bg-rose-50/85
'
:
tone
===
'amber'
:
tone
===
'amber'
?
'border-amber-200 bg-amber-50/80
'
?
'border-amber-200/80 bg-amber-50/85
'
:
tone
===
'emerald'
:
tone
===
'emerald'
?
'border-emerald-200 bg-emerald-50/80
'
?
'border-emerald-200/80 bg-emerald-50/85
'
:
'border-zinc-200 bg-zinc-50/80
'
;
:
'border-zinc-200/80 bg-white/88
'
;
return
(
return
(
<
div
className=
{
clsx
(
'rounded-[24px] border p-4 shadow-sm'
,
toneClass
)
}
>
<
div
className=
{
clsx
(
'rounded-[26px] border p-5 shadow-sm ring-1 ring-zinc-950/5'
,
toneClass
)
}
>
<
div
className=
"text-sm font-medium text-slate-500"
>
{
label
}
</
div
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.22em] text-slate-400"
>
<
div
className=
"mt-3 text-3xl font-semibold tracking-tight text-slate-900"
>
{
label
}
</
div
>
<
div
className=
"mt-3 text-3xl font-semibold tracking-tight text-slate-950"
>
{
value
}
{
value
}
</
div
>
</
div
>
<
div
className=
"mt-2 text-sm text-slate-500"
>
{
hint
}
</
div
>
<
div
className=
"mt-2 text-sm text-slate-500"
>
{
hint
}
</
div
>
...
@@ -89,7 +155,7 @@ function SummaryCard({ label, value, hint, tone = 'default' }) {
...
@@ -89,7 +155,7 @@ function SummaryCard({ label, value, hint, tone = 'default' }) {
function
EmptyState
({
title
,
description
})
{
function
EmptyState
({
title
,
description
})
{
return
(
return
(
<
div
className=
"rounded-[28px] border border-dashed border-zinc-300 bg-white/
70 px-6 py-12
text-center"
>
<
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=
"text-xl font-semibold text-slate-900"
>
{
title
}
</
div
>
<
div
className=
"mt-3 text-sm leading-6 text-slate-500"
>
{
description
}
</
div
>
<
div
className=
"mt-3 text-sm leading-6 text-slate-500"
>
{
description
}
</
div
>
</
div
>
</
div
>
...
@@ -99,20 +165,15 @@ function EmptyState({ title, description }) {
...
@@ -99,20 +165,15 @@ function EmptyState({ title, description }) {
function
StatusPill
({
text
,
tone
=
'default'
})
{
function
StatusPill
({
text
,
tone
=
'default'
})
{
const
toneClass
=
const
toneClass
=
tone
===
'rose'
tone
===
'rose'
?
'bg-rose-50 text-rose-700'
?
'bg-rose-50 text-rose-700
ring-rose-100
'
:
tone
===
'emerald'
:
tone
===
'emerald'
?
'bg-emerald-50 text-emerald-7
00'
?
'bg-emerald-50 text-emerald-700 ring-emerald-1
00'
:
tone
===
'amber'
:
tone
===
'amber'
?
'bg-amber-50 text-amber-7
00'
?
'bg-amber-50 text-amber-700 ring-amber-1
00'
:
'bg-slate-100 text-slate-6
00'
;
:
'bg-slate-100 text-slate-600 ring-slate-2
00'
;
return
(
return
(
<
span
<
span
className=
{
clsx
(
'inline-flex rounded-full px-3 py-1 text-xs font-medium ring-1'
,
toneClass
)
}
>
className=
{
clsx
(
'inline-flex rounded-full px-3 py-1 text-xs font-medium'
,
toneClass
,
)
}
>
{
text
||
'-'
}
{
text
||
'-'
}
</
span
>
</
span
>
);
);
...
@@ -141,10 +202,6 @@ function resolveAuditTone(status = '') {
...
@@ -141,10 +202,6 @@ function resolveAuditTone(status = '') {
return
'default'
;
return
'default'
;
}
}
function
formatUsers
(
users
)
{
return
users
&&
users
.
length
>
0
?
users
.
join
(
'、'
)
:
'-'
;
}
function
SelectedUsers
({
users
,
onRemove
})
{
function
SelectedUsers
({
users
,
onRemove
})
{
if
(
users
.
length
===
0
)
{
if
(
users
.
length
===
0
)
{
return
(
return
(
...
@@ -176,37 +233,28 @@ function SelectedUsers({ users, onRemove }) {
...
@@ -176,37 +233,28 @@ function SelectedUsers({ users, onRemove }) {
);
);
}
}
function
RemoteUserSelect
({
function
RemoteUserSelect
({
label
,
value
,
onChange
,
placeholder
,
onSearch
})
{
label
,
value
,
onChange
,
placeholder
,
onSearch
,
})
{
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
options
,
setOptions
]
=
useState
([]);
const
[
options
,
setOptions
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
requestIdRef
=
useRef
(
0
);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
const
requestId
=
requestIdRef
.
current
+
1
;
requestIdRef
.
current
=
requestId
;
const
loadOptions
=
async
()
=>
{
const
loadOptions
=
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
try
{
try
{
const
users
=
await
onSearch
(
keyword
);
const
users
=
await
onSearch
(
keyword
);
if
(
!
cancelled
&&
requestIdRef
.
current
===
requestId
)
{
if
(
!
cancelled
)
{
setOptions
(
users
);
setOptions
(
users
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
if
(
!
cancelled
&&
requestIdRef
.
current
===
requestId
)
{
if
(
!
cancelled
)
{
setOptions
([]);
setOptions
([]);
}
}
}
finally
{
}
finally
{
if
(
!
cancelled
&&
requestIdRef
.
current
===
requestId
)
{
if
(
!
cancelled
)
{
setLoading
(
false
);
setLoading
(
false
);
}
}
}
}
...
@@ -219,18 +267,12 @@ function RemoteUserSelect({
...
@@ -219,18 +267,12 @@ function RemoteUserSelect({
};
};
},
[
keyword
,
onSearch
]);
},
[
keyword
,
onSearch
]);
const
selectedUsers
=
useMemo
(()
=>
value
.
filter
(
Boolean
),
[
value
]);
const
selectedIds
=
useMemo
(()
=>
new
Set
(
value
.
map
((
item
)
=>
item
.
_id
)),
[
value
]);
const
selectedIds
=
useMemo
(
()
=>
new
Set
(
selectedUsers
.
map
((
user
)
=>
user
.
_id
)),
[
selectedUsers
],
);
const
availableOptions
=
options
.
filter
((
user
)
=>
!
selectedIds
.
has
(
user
.
_id
));
const
availableOptions
=
options
.
filter
((
user
)
=>
!
selectedIds
.
has
(
user
.
_id
));
return
(
return
(
<
div
className=
"space-y-3"
>
<
div
className=
"space-y-3"
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
<
label
className=
"block text-sm font-medium text-slate-700"
>
{
label
}
</
label
>
{
label
}
</
label
>
<
Headless
.
Combobox
<
Headless
.
Combobox
value=
{
null
}
value=
{
null
}
nullable
nullable
...
@@ -238,7 +280,7 @@ function RemoteUserSelect({
...
@@ -238,7 +280,7 @@ function RemoteUserSelect({
if
(
!
user
||
selectedIds
.
has
(
user
.
_id
))
{
if
(
!
user
||
selectedIds
.
has
(
user
.
_id
))
{
return
;
return
;
}
}
onChange
([...
selectedUsers
,
user
]);
onChange
([...
value
,
user
]);
setKeyword
(
''
);
setKeyword
(
''
);
}
}
}
}
>
>
...
@@ -271,9 +313,7 @@ function RemoteUserSelect({
...
@@ -271,9 +313,7 @@ function RemoteUserSelect({
value=
{
user
}
value=
{
user
}
className=
"cursor-pointer rounded-xl px-3 py-2 text-sm text-slate-700 data-focus:bg-rose-50 data-focus:text-slate-900"
className=
"cursor-pointer rounded-xl px-3 py-2 text-sm text-slate-700 data-focus:bg-rose-50 data-focus:text-slate-900"
>
>
<
div
className=
"font-medium text-slate-900"
>
<
div
className=
"font-medium text-slate-900"
>
{
user
.
display_name
||
user
.
username
}
</
div
>
{
user
.
display_name
||
user
.
username
}
</
div
>
<
div
className=
"text-xs text-slate-500"
>
{
user
.
username
}
</
div
>
<
div
className=
"text-xs text-slate-500"
>
{
user
.
username
}
</
div
>
</
Headless
.
ComboboxOption
>
</
Headless
.
ComboboxOption
>
))
))
...
@@ -286,30 +326,13 @@ function RemoteUserSelect({
...
@@ -286,30 +326,13 @@ function RemoteUserSelect({
</
div
>
</
div
>
</
Headless
.
Combobox
>
</
Headless
.
Combobox
>
<
SelectedUsers
<
SelectedUsers
users=
{
selectedUsers
}
users=
{
value
}
onRemove=
{
(
userId
)
=>
onRemove=
{
(
userId
)
=>
onChange
(
value
.
filter
((
user
)
=>
user
.
_id
!==
userId
))
}
onChange
(
selectedUsers
.
filter
((
user
)
=>
user
.
_id
!==
userId
))
}
/>
/>
</
div
>
</
div
>
);
);
}
}
function
buildInitialCreateValues
(
subject
)
{
return
(
subject
?.
fields
||
[])
.
filter
((
field
)
=>
CREATE_FIELD_ORDER
.
includes
(
field
.
name
))
.
reduce
((
result
,
field
)
=>
{
result
[
field
.
name
]
=
emptyValueForField
(
field
);
return
result
;
},
{});
}
function
normalizeFieldList
(
subject
)
{
const
fieldMap
=
new
Map
((
subject
?.
fields
||
[]).
map
((
field
)
=>
[
field
.
name
,
field
]));
return
CREATE_FIELD_ORDER
.
map
((
fieldName
)
=>
fieldMap
.
get
(
fieldName
)).
filter
(
Boolean
);
}
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
);
...
@@ -328,13 +351,7 @@ function CreateField({ fieldDef, value, onChange }) {
...
@@ -328,13 +351,7 @@ function CreateField({ fieldDef, value, onChange }) {
}
}
if (fieldType === 'date') {
if (fieldType === 'date') {
return (
return <Input type="date" value={value || ''} onChange={(event) => onChange(event.target.value)} />;
<Input
type="date"
value={value || ''}
onChange={(event) => onChange(event.target.value)}
/>
);
}
}
if (fieldType === 'select') {
if (fieldType === 'select') {
...
@@ -344,9 +361,7 @@ function CreateField({ fieldDef, value, onChange }) {
...
@@ -344,9 +361,7 @@ function CreateField({ fieldDef, value, onChange }) {
value={isMultiple ? value || [] : value || ''}
value={isMultiple ? value || [] : value || ''}
onChange={(event) => {
onChange={(event) => {
if (isMultiple) {
if (isMultiple) {
onChange(
onChange(Array.from(event.target.selectedOptions).map((option) => option.value));
Array.from(event.target.selectedOptions).map((option) => option.value),
);
return;
return;
}
}
onChange(event.target.value);
onChange(event.target.value);
...
@@ -364,6 +379,7 @@ function CreateField({ fieldDef, value, onChange }) {
...
@@ -364,6 +379,7 @@ function CreateField({ fieldDef, value, onChange }) {
return (
return (
<Input
<Input
type={fieldType === 'number' ? 'number' : 'text'}
value={value || ''}
value={value || ''}
onChange={(event) => onChange(event.target.value)}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
placeholder={placeholder}
...
@@ -371,89 +387,178 @@ function CreateField({ fieldDef, value, onChange }) {
...
@@ -371,89 +387,178 @@ function CreateField({ fieldDef, value, onChange }) {
);
);
}
}
function FieldValuePreview({ column, record }) {
const fieldName = getFieldNameFromPath(column.field);
const rawRecord = record.raw || record;
const value = getFieldValue(rawRecord, column.field);
if (fieldName === 'rectification_progress') {
return <StatusPill text={formatFieldValue(value, column.fieldDef) || '未填写'} tone={resolveProgressTone(formatFieldValue(value, column.fieldDef) || '')} />;
}
if (fieldName === 'audit_status' || fieldName === 'final_audit_status') {
return <StatusPill text={formatFieldValue(value, column.fieldDef) || '未填写'} tone={resolveAuditTone(formatFieldValue(value, column.fieldDef) || '')} />;
}
if (fieldName === 'fill_in_the_people' || fieldName === 'review_people') {
const text = getRecordCellText(record, column);
return <div className="whitespace-normal break-words text-sm text-slate-700">{text}</div>;
}
if (column.fieldDef?.type === 'file') {
const fileValue = Array.isArray(value) ? value.filter(Boolean) : value ? [value] : [];
if (fileValue.length === 0) {
return <span className="text-slate-400">未上传</span>;
}
return (
<div className="flex flex-wrap gap-2">
{fileValue.map((file, index) => {
const label = file?.name || file?.title || `
附件
$
{
index
+
1
}
`;
if (!file?.url) {
return (
<span key={`
$
{
label
}
-
$
{
index
}
`} className="inline-flex rounded-full bg-zinc-100 px-2.5 py-1 text-xs font-medium text-zinc-700">
{label}
</span>
);
}
return (
<a
key={`
$
{
label
}
-
$
{
index
}
`}
href={file.url}
target="_blank"
rel="noreferrer"
className="inline-flex rounded-full bg-sky-50 px-2.5 py-1 text-xs font-medium text-sky-700 transition hover:bg-sky-100"
>
{label}
</a>
);
})}
</div>
);
}
const text = getRecordCellText(record, column);
const isLongText = ['long_text', 'rich_text'].includes(column.fieldDef?.type);
return (
<div className={clsx('whitespace-normal break-words text-sm text-slate-700', isLongText && 'line-clamp-3')}>
{text}
</div>
);
}
export default function ProblemRectificationListPage() {
export default function ProblemRectificationListPage() {
const [records, setRecords] = useState([]);
const [records, setRecords] = useState([]);
const [createSubject, setCreateSubject] = useState(null);
const [subject, setSubject] = useState(null);
const [views, setViews] = useState([]);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [error, setError] = useState('');
const [actionLoading, setActionLoading] = useState(false);
const [actionLoading, setActionLoading] = useState(false);
const [filters, setFilters] = useState({
const [filters, setFilters] = useState({
groupKey: 'all',
keyword: '',
keyword: '',
progress: '',
progress: '',
auditStatus: '',
auditStatus: '',
});
});
const [activeViewName, setActiveViewName] = useState('');
const [selectedIds, setSelectedIds] = useState([]);
const [selectedIds, setSelectedIds] = useState([]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showPushModal, setShowPushModal] = useState(false);
const [showPushModal, setShowPushModal] = useState(false);
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 = async () => {
const reloadList =
useCallback(
async () => {
setLoading(true);
setLoading(true);
setError('');
setError('');
try {
try {
const nextRecords = await loadProblemRectificationListData();
const resources = await loadProblemRectificationListResources();
setRecords(nextRecords);
setRecords(resources.records || []);
setSubject(resources.subject || null);
setViews(resources.views || []);
} catch (requestError) {
} catch (requestError) {
setRecords([]);
setRecords([]);
setSubject(null);
setViews([]);
setError(requestError?.message || '问题整改列表加载失败,请稍后重试。');
setError(requestError?.message || '问题整改列表加载失败,请稍后重试。');
} finally {
} finally {
setLoading(false);
setLoading(false);
}
}
};
}
, [])
;
useEffect(() => {
useEffect(() => {
reloadList();
reloadList();
}, []);
}, [
reloadList
]);
useEffect(() => {
const gridViews = useMemo(() => buildGridViews(views), [views]);
let cancelled = false;
(async () => {
useEffect(() => {
try {
if (gridViews.length === 0) {
const subject = await loadSubject(PROBLEM_RECTIFICATION_SUBJECT);
setActiveViewName('');
if (!cancelled) {
return;
setCreateSubject(subject);
setFormValues(buildInitialCreateValues(subject));
}
} catch (requestError) {
if (!cancelled) {
setCreateSubject(null);
}
}
const currentView = gridViews.find((view) => view.name === activeViewName);
if (!currentView) {
setActiveViewName(gridViews[0].name);
}
}
})(
);
}, [gridViews, activeViewName]
);
return () => {
const activeGridView = useMemo(
cancelled = true;
() => gridViews.find((view) => view.name === activeViewName) || gridViews[0] || null,
};
[gridViews, activeViewName],
}, []);
);
const configuredView = useMemo(
() => buildProblemRectificationConfiguredListView(records, filters, activeGridView, subject),
[records, filters, activeGridView, subject],
);
const
view
= useMemo(
const
stickyColumns
= useMemo(
() => build
ProblemRectificationListView(records, filters
),
() => build
StickyColumnMeta(configuredView.columns || []
),
[
records, filter
s],
[
configuredView.column
s],
);
);
const gridViewCounts = useMemo(
() => buildGridViewCountMap(records, filters, gridViews, subject),
[records, filters, gridViews, subject],
);
const createFields = useMemo(
const createFields = useMemo(
() =>
normalizeFieldList(createS
ubject),
() =>
buildProblemRectificationCreateFormFields(activeGridView, s
ubject),
[
createS
ubject],
[
activeGridView, s
ubject],
);
);
const initialCreateValues = useMemo(
const initialCreateValues = useMemo(
() => buildInitialCreateValues(create
Subject
),
() => buildInitialCreateValues(create
Fields
),
[create
Subject
],
[create
Fields
],
);
);
const filteredRecords = view.filteredRecords || [];
useEffect(() => {
const groupCounts = view.groupCounts || {};
if (!showCreateModal) {
setFormValues(initialCreateValues);
}
}, [initialCreateValues, showCreateModal]);
const filteredRecords = configuredView.filteredRecords || [];
const selectedRecords = useMemo(
const selectedRecords = useMemo(
() => filteredRecords.filter((item) => selectedIds.includes(item.id)),
() => filteredRecords.filter((item) => selectedIds.includes(item.id)),
[filteredRecords, selectedIds],
[filteredRecords, selectedIds],
);
);
useEffect(() => {
const visibleIds = new Set(filteredRecords.map((item) => item.id));
setSelectedIds((current) => current.filter((id) => visibleIds.has(id)));
}, [filteredRecords]);
const fixedCount = filteredRecords.filter(
const fixedCount = filteredRecords.filter(
(item) => item.progress === '已整改' || item.progress === '已优化',
(item) => item.progress === '已整改' || item.progress === '已优化',
).length;
).length;
const pendingAuditCount = filteredRecords.filter(
const pendingAuditCount = filteredRecords.filter(
(item) => item.auditStatus && item.auditStatus !== '已审批',
(item) => item.auditStatus && item.auditStatus !== '已审批',
).length;
).length;
...
@@ -474,6 +579,20 @@ export default function ProblemRectificationListPage() {
...
@@ -474,6 +579,20 @@ export default function ProblemRectificationListPage() {
setSelectedIds((current) => current.filter((item) => item !== recordId));
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) &&
...
@@ -510,9 +629,7 @@ export default function ProblemRectificationListPage() {
...
@@ -510,9 +629,7 @@ export default function ProblemRectificationListPage() {
return;
return;
}
}
const confirmed = window.confirm(
const confirmed = window.confirm(`
确认删除
$
{
targetRecords
.
length
}
条问题整改记录吗?此操作不可撤销。
`);
`
确认删除
$
{
targetRecords
.
length
}
条问题整改记录吗?此操作不可撤销。
`,
);
if (!confirmed) {
if (!confirmed) {
return;
return;
}
}
...
@@ -527,14 +644,13 @@ export default function ProblemRectificationListPage() {
...
@@ -527,14 +644,13 @@ export default function ProblemRectificationListPage() {
}
}
await batchRemoveRecords({
await batchRemoveRecords({
subject:
'problem_rectification'
,
subject:
PROBLEM_RECTIFICATION_SUBJECT
,
filter: {
filter: {
name: { $in: names },
name: { $in: names },
},
},
});
});
setSelectedIds((current) =>
current.filter((id) => !targetRecords.some((item) => item.id === id)),
setSelectedIds((current) => current.filter((id) => !targetRecords.some((item) => item.id === id)));
);
await reloadList();
await reloadList();
} catch (requestError) {
} catch (requestError) {
setError(requestError?.message || '删除问题整改记录失败,请稍后重试。');
setError(requestError?.message || '删除问题整改记录失败,请稍后重试。');
...
@@ -565,6 +681,7 @@ export default function ProblemRectificationListPage() {
...
@@ -565,6 +681,7 @@ export default function ProblemRectificationListPage() {
setError('请先勾选需要推送的问题整改记录。');
setError('请先勾选需要推送的问题整改记录。');
return;
return;
}
}
if (fillerIds.length === 0 || auditorIds.length === 0) {
if (fillerIds.length === 0 || auditorIds.length === 0) {
setError('请先选择填报人和复核人。');
setError('请先选择填报人和复核人。');
return;
return;
...
@@ -574,10 +691,8 @@ export default function ProblemRectificationListPage() {
...
@@ -574,10 +691,8 @@ export default function ProblemRectificationListPage() {
setError('');
setError('');
try {
try {
const fillerUsers = await Promise.all(fillerIds.map((id) => loadUser(id)));
const fillerProfiles = await Promise.all(fillerIds.map((id) => loadUser(id)));
const auditorUsers = await Promise.all(auditorIds.map((id) => loadUser(id)));
const currentUserInfo = JSON.parse(window.localStorage.getItem('current_user') || '{}') || {};
const currentUserInfo =
JSON.parse(window.localStorage.getItem('current_user') || '{}') || {};
const messageRecord = await createRecord({
const messageRecord = await createRecord({
subject: 'message_reference_id',
subject: 'message_reference_id',
...
@@ -589,8 +704,8 @@ export default function ProblemRectificationListPage() {
...
@@ -589,8 +704,8 @@ export default function ProblemRectificationListPage() {
restriction_key: '_id',
restriction_key: '_id',
restriction_value: selectedRecords.map((item) => item.id),
restriction_value: selectedRecords.map((item) => item.id),
reference_record_id: selectedRecords.map((item) => item.id),
reference_record_id: selectedRecords.map((item) => item.id),
view_name:
filters.groupKey
,
view_name:
activeGridView?.name || ''
,
subject_name:
'problem_rectification'
,
subject_name:
PROBLEM_RECTIFICATION_SUBJECT
,
editor_status: '未完成',
editor_status: '未完成',
auditor_status: '未完成',
auditor_status: '未完成',
sender_status: '未完成',
sender_status: '未完成',
...
@@ -598,14 +713,14 @@ export default function ProblemRectificationListPage() {
...
@@ -598,14 +713,14 @@ export default function ProblemRectificationListPage() {
});
});
const progressIdEditor = [];
const progressIdEditor = [];
for (const filler
User of fillerUser
s) {
for (const filler
Profile of fillerProfile
s) {
const oaProcessResult = await executeAction('oa_notice:createProgress', {
const oaProcessResult = await executeAction('oa_notice:createProgress', {
title: `
$
{
dayjs
().
format
(
'YYYY-MM-DD'
)}
填写问题整改表
`,
title: `
$
{
dayjs
().
format
(
'YYYY-MM-DD'
)}
填写问题整改表
`,
classify: '流程待办',
classify: '流程待办',
contentType: '流程待办',
contentType: '流程待办',
receiveUserId: filler
User
._id,
receiveUserId: filler
Profile
._id,
receiveUserName: filler
User
.username,
receiveUserName: filler
Profile
.username,
receiveDisplayName: filler
User
.display_name,
receiveDisplayName: filler
Profile
.display_name,
sendUserId: currentUserInfo?._id || currentUserInfo?.id || '',
sendUserId: currentUserInfo?._id || currentUserInfo?.id || '',
sendUserName: currentUserInfo?.username || '',
sendUserName: currentUserInfo?.username || '',
sendDisplayName: currentUserInfo?.display_name || '',
sendDisplayName: currentUserInfo?.display_name || '',
...
@@ -621,7 +736,7 @@ export default function ProblemRectificationListPage() {
...
@@ -621,7 +736,7 @@ export default function ProblemRectificationListPage() {
});
});
await
batchUpdateRecords
({
await
batchUpdateRecords
({
subject
:
'problem_rectification'
,
subject
:
PROBLEM_RECTIFICATION_SUBJECT
,
filter
:
{
filter
:
{
_id
:
{
$in
:
selectedRecords
.
map
((
item
)
=>
item
.
id
)
},
_id
:
{
$in
:
selectedRecords
.
map
((
item
)
=>
item
.
id
)
},
},
},
...
@@ -654,337 +769,266 @@ export default function ProblemRectificationListPage() {
...
@@ -654,337 +769,266 @@ export default function ProblemRectificationListPage() {
};
};
return
(
return
(
<
div
className=
"space-y-6"
>
<
div
className=
"space-y-6
pb-4
"
>
<
section
className=
"grid gap-4 md:grid-cols-3"
>
<
section
className=
"grid gap-4 md:grid-cols-3"
>
<
SummaryCard
<
SummaryCard
label=
"当前
分组问题
数"
label=
"当前
视图记录
数"
value=
{
filteredRecords
.
length
}
value=
{
filteredRecords
.
length
}
hint=
"筛选结果"
hint=
{
activeGridView
?.
title
?
`${activeGridView.title} 视图`
:
'筛选结果'
}
tone=
"rose"
tone=
"rose"
/>
/>
<
SummaryCard
<
SummaryCard
label=
"已整改 / 已优化"
label=
"已整改 / 已优化"
value=
{
fixedCount
}
value=
{
fixedCount
}
hint=
"
已完成数量
"
hint=
"
按当前视图与筛选统计
"
tone=
"emerald"
tone=
"emerald"
/>
/>
<
SummaryCard
<
SummaryCard
label=
"待复核项"
label=
"待复核项"
value=
{
pendingAuditCount
}
value=
{
pendingAuditCount
}
hint=
"
待处理
记录"
hint=
"
审批状态未通过的
记录"
tone=
"amber"
tone=
"amber"
/>
/>
</
section
>
</
section
>
<
section
className=
"overflow-x-auto"
>
<
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=
"inline-flex min-w-full gap-3 rounded-[28px] border border-zinc-200 bg-white/90 p-2 shadow-sm"
>
<
div
className=
"flex flex-wrap gap-2"
>
{
DEPARTMENT_GROUPS
.
map
((
group
)
=>
(
{
gridViews
.
map
((
view
)
=>
{
const
active
=
view
.
name
===
activeGridView
?.
name
;
return
(
<
button
<
button
key=
{
group
.
key
}
key=
{
view
.
name
}
type=
"button"
type=
"button"
onClick=
{
()
=>
onClick=
{
()
=>
{
setFilters
((
current
)
=>
({
...
current
,
groupKey
:
group
.
key
}))
setActiveViewName
(
view
.
name
);
}
setSelectedIds
([]);
}
}
className=
{
clsx
(
className=
{
clsx
(
'rounded-[18px] px-4 py-3 text-sm font-medium transition-all duration-200
'
,
'inline-flex min-h-11 items-center gap-2 rounded-full px-4 py-2.5 text-sm font-medium transition
'
,
filters
.
groupKey
===
group
.
key
active
?
'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg
'
?
'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg shadow-rose-200/60
'
:
'text-slate-6
00 hover:bg-rose-50 hover:text-rose-700'
,
:
'bg-white text-slate-600 ring-1 ring-zinc-2
00 hover:bg-rose-50 hover:text-rose-700'
,
)
}
)
}
>
>
{
group
.
label
}
<
span
>
{
view
.
title
||
view
.
name
}
</
span
>
<
span
<
span
className=
{
clsx
(
className=
{
clsx
(
'ml-2 rounded-full px-2 py-0.5 text-xs'
,
'rounded-full px-2 py-0.5 text-xs'
,
filters
.
groupKey
===
group
.
key
active
?
'bg-white/20 text-white'
:
'bg-zinc-100 text-slate-500'
,
?
'bg-white/18 text-white'
:
'bg-zinc-100 text-slate-500'
,
)
}
)
}
>
>
{
groupCounts
[
group
.
key
]
||
0
}
{
gridViewCounts
[
view
.
name
]
||
0
}
</
span
>
</
span
>
</
button
>
</
button
>
))
}
);
})
}
</
div
>
</
div
>
</
section
>
</
section
>
<
section
className=
"rounded-[30px] border border-zinc-200 bg-white/95 p-5 shadow-sm"
>
<
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
className=
"grid gap-4 xl:grid-cols-[1.4fr_0.8fr_0.8fr_auto]"
>
<
div
>
<
div
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
关键词搜索
</
label
>
关键词搜索
<
Input
</
label
>
<
input
value=
{
filters
.
keyword
}
value=
{
filters
.
keyword
}
onChange=
{
(
event
)
=>
onChange=
{
(
event
)
=>
setFilters
((
current
)
=>
({
...
current
,
keyword
:
event
.
target
.
value
}))
}
setFilters
((
current
)
=>
({
placeholder=
"搜索当前视图中的项目名称、状态、问题简述、责任人..."
...
current
,
keyword
:
event
.
target
.
value
,
}))
}
placeholder=
"搜索项目名称、问题简述、问题类型、反馈单位..."
className=
"w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-300 focus:bg-white"
/>
/>
</
div
>
</
div
>
<
div
>
<
div
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
整改进度
</
label
>
整改进度
<
Select
</
label
>
<
select
value=
{
filters
.
progress
}
value=
{
filters
.
progress
}
onChange=
{
(
event
)
=>
onChange=
{
(
event
)
=>
setFilters
((
current
)
=>
({
...
current
,
progress
:
event
.
target
.
value
}))
}
setFilters
((
current
)
=>
({
...
current
,
progress
:
event
.
target
.
value
,
}))
}
className=
"w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-300 focus:bg-white"
>
>
<
option
value=
""
>
全部进度
</
option
>
<
option
value=
""
>
全部进度
</
option
>
{
v
iew
.
progressOptions
.
map
((
option
)
=>
(
{
configuredV
iew
.
progressOptions
.
map
((
option
)
=>
(
<
option
key=
{
option
}
value=
{
option
}
>
<
option
key=
{
option
}
value=
{
option
}
>
{
option
}
{
option
}
</
option
>
</
option
>
))
}
))
}
</
s
elect
>
</
S
elect
>
</
div
>
</
div
>
<
div
>
<
div
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
审批状态
</
label
>
审批状态
<
Select
</
label
>
<
select
value=
{
filters
.
auditStatus
}
value=
{
filters
.
auditStatus
}
onChange=
{
(
event
)
=>
onChange=
{
(
event
)
=>
setFilters
((
current
)
=>
({
...
current
,
auditStatus
:
event
.
target
.
value
}))
}
setFilters
((
current
)
=>
({
...
current
,
auditStatus
:
event
.
target
.
value
,
}))
}
className=
"w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-300 focus:bg-white"
>
>
<
option
value=
""
>
全部状态
</
option
>
<
option
value=
""
>
全部状态
</
option
>
{
v
iew
.
auditStatusOptions
.
map
((
option
)
=>
(
{
configuredV
iew
.
auditStatusOptions
.
map
((
option
)
=>
(
<
option
key=
{
option
}
value=
{
option
}
>
<
option
key=
{
option
}
value=
{
option
}
>
{
option
}
{
option
}
</
option
>
</
option
>
))
}
))
}
</
s
elect
>
</
S
elect
>
</
div
>
</
div
>
<
div
className=
"flex items-end"
>
<
div
className=
"flex items-end"
>
<
button
<
Button
type=
"button"
outline
className=
"w-full justify-center"
onClick=
{
()
=>
{
onClick=
{
()
=>
{
setFilters
({
setFilters
({
groupKey
:
'all'
,
keyword
:
''
,
keyword
:
''
,
progress
:
''
,
progress
:
''
,
auditStatus
:
''
,
auditStatus
:
''
,
});
});
setSelectedIds
([]);
setSelectedIds
([]);
}
}
}
}
className=
"w-full rounded-2xl border border-zinc-200 bg-white px-4 py-3 text-sm font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-700"
>
>
重置
筛选
清空
筛选
</
b
utton
>
</
B
utton
>
</
div
>
</
div
>
</
div
>
</
div
>
</
section
>
</
section
>
<
section
className=
"
rounded-[30px] border border-zinc-200 bg-white/95 shadow-sm
"
>
<
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-
center justify-between gap-3
border-b border-zinc-100 px-6 py-5"
>
<
div
className=
"flex flex-wrap items-
start justify-between gap-4
border-b border-zinc-100 px-6 py-5"
>
<
div
>
<
div
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
问题清单
</
div
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
问题清单
</
div
>
<
div
className=
"mt-1 text-sm text-slate-500"
>
<
div
className=
"mt-1 text-sm text-slate-500"
>
当前
选中
{
selectedIds
.
length
}
条记录
。
当前
视图显示
{
filteredRecords
.
length
}
条记录,已选中
{
selectedIds
.
length
}
条
。
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"flex flex-wrap gap-2"
>
<
div
className=
"flex flex-wrap gap-2"
>
<
button
<
Button
color=
"rose"
onClick=
{
()
=>
setShowCreateModal
(
true
)
}
>
type=
"button"
onClick=
{
()
=>
setShowCreateModal
(
true
)
}
className=
"rounded-full bg-[linear-gradient(135deg,#7f1d1d,#be123c)] px-4 py-2 text-sm font-medium text-white shadow-lg transition hover:brightness-105"
>
新增记录
新增记录
</
button
>
</
Button
>
<
button
<
Button
outline
disabled=
{
selectedIds
.
length
===
0
||
actionLoading
}
onClick=
{
()
=>
setShowPushModal
(
true
)
}
>
type=
"button"
disabled=
{
selectedIds
.
length
===
0
||
actionLoading
}
onClick=
{
()
=>
setShowPushModal
(
true
)
}
className=
"rounded-full border border-zinc-200 px-4 py-2 text-sm font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-700 disabled:cursor-not-allowed disabled:opacity-50"
>
推送整改表
推送整改表
</
button
>
</
Button
>
<
button
<
Button
outline
disabled
>
type=
"button"
className=
"rounded-full border border-zinc-200 px-4 py-2 text-sm font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-700"
>
导出
导出
</
button
>
</
Button
>
<
button
<
Button
outline
disabled
>
type=
"button"
className=
"rounded-full border border-zinc-200 px-4 py-2 text-sm font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-700"
>
一键归档
一键归档
</
button
>
</
Button
>
<
button
<
Button
color=
"red"
disabled=
{
selectedIds
.
length
===
0
||
actionLoading
}
onClick=
{
()
=>
handleDelete
(
selectedRecords
)
}
>
type=
"button"
disabled=
{
selectedIds
.
length
===
0
||
actionLoading
}
onClick=
{
()
=>
handleDelete
(
selectedRecords
)
}
className=
"rounded-full border border-rose-200 bg-rose-50 px-4 py-2 text-sm font-medium text-rose-700 transition hover:bg-rose-100 disabled:cursor-not-allowed disabled:opacity-50"
>
删除选中
删除选中
</
b
utton
>
</
B
utton
>
</
div
>
</
div
>
</
div
>
</
div
>
{
error
?
(
{
error
?
(
<
div
className=
"border-b border-zinc-100
px-6 py-4 text-sm text-rose-6
00"
>
<
div
className=
"border-b border-zinc-100
bg-rose-50/70 px-6 py-4 text-sm text-rose-7
00"
>
{
error
}
{
error
}
</
div
>
</
div
>
)
:
null
}
)
:
null
}
{
loading
?
(
{
loading
?
(
<
div
className=
"px-6 py-5"
>
<
div
className=
"px-6 py-6"
>
<
EmptyState
<
EmptyState
title=
"正在加载问题整改列表"
description=
"正在读取对象结构、视图配置和记录数据,请稍候。"
/>
title=
"正在加载问题整改列表"
description=
"请稍候。"
/>
</
div
>
</
div
>
)
:
filteredRecords
.
length
===
0
?
(
)
:
filteredRecords
.
length
===
0
?
(
<
div
className=
"px-6 py-
5
"
>
<
div
className=
"px-6 py-
6
"
>
<
EmptyState
<
EmptyState
title=
"暂无数据"
title=
"
当前视图
暂无数据"
description=
"
未查询到记录
。"
description=
"
可以切换其他视图,或调整关键词、审批状态和整改进度筛选
。"
/>
/>
</
div
>
</
div
>
)
:
(
)
:
(
<
div
className=
"overflow-x-auto"
>
<
div
className=
"px-3 pb-3 pt-2"
>
<
table
className=
"min-w-full border-separate border-spacing-0"
>
<
Table
bleed
dense
className=
"[--gutter:--spacing(3)]"
>
<
thead
>
<
TableHead
>
<
tr
className=
"text-left text-sm text-slate-500"
>
<
TableRow
className=
"bg-zinc-50/80"
>
<
th
className=
"border-b border-zinc-100 bg-zinc-50/80 px-4 py-4 font-medium"
>
<
TableHeader
<
input
className=
"w-14 sticky left-0 z-40 bg-zinc-50/95 backdrop-blur shadow-[10px_0_20px_rgba(15,23,42,0.06)]"
type=
"checkbox"
style=
{
{
width
:
SELECTION_COLUMN_WIDTH
,
minWidth
:
SELECTION_COLUMN_WIDTH
}
}
checked=
{
filteredRecords
.
length
>
0
&&
selectedIds
.
length
===
filteredRecords
.
length
}
onChange=
{
(
event
)
=>
toggleAll
(
event
.
target
.
checked
)
}
/>
</
th
>
{
[
'检查项目名称'
,
'反馈单位'
,
'整改进度'
,
'审批状态'
,
'最终审批'
,
'预计整改完成时间'
,
'问题类型'
,
'填写人 / 复核人'
,
'问题简述'
,
'操作'
,
].
map
((
header
)
=>
(
<
th
key=
{
header
}
className=
"border-b border-zinc-100 bg-zinc-50/80 px-6 py-4 font-medium"
>
>
{
header
}
<
div
className=
"flex justify-center"
>
</
th
>
<
Checkbox
))
}
checked=
{
filteredRecords
.
length
>
0
&&
selectedIds
.
length
===
filteredRecords
.
length
}
</
tr
>
onChange=
{
toggleAll
}
</
thead
>
color=
"rose"
<
tbody
>
aria
-
label=
"全选当前视图记录"
{
filteredRecords
.
map
((
record
,
index
)
=>
(
/>
<
tr
</
div
>
key=
{
record
.
id
}
</
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
(
className=
{
clsx
(
'align-top text-sm text-slate-7
00'
,
'text-xs font-semibold uppercase tracking-[0.18em] text-slate-4
00'
,
index
%
2
===
0
?
'bg-white'
:
'bg-zinc-50/45'
,
getStickyCellClass
(
stickyMeta
,
'header'
)
,
)
}
)
}
>
>
<
td
className=
"border-b border-zinc-100 px-4 py-5"
>
{
column
.
label
}
<
input
</
TableHeader
>
type=
"checkbox"
);
})
}
<
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
)
}
checked=
{
selectedIds
.
includes
(
record
.
id
)
}
onChange=
{
(
event
)
=>
onChange=
{
(
checked
)
=>
toggleRow
(
record
.
id
,
checked
)
}
toggleRow
(
record
.
id
,
event
.
target
.
checked
)
color=
"rose"
}
aria
-
label=
{
`选择 ${record.title || record.projectName || '当前记录'}`
}
/>
</
td
>
<
td
className=
"max-w-[220px] border-b border-zinc-100 px-6 py-5"
>
<
div
className=
"font-semibold text-slate-900"
>
{
record
.
projectName
}
</
div
>
<
div
className=
"mt-2 text-xs leading-5 text-slate-500"
>
{
record
.
inspectUnit
||
'未填写实施单位'
}
</
div
>
</
td
>
<
td
className=
"max-w-[180px] border-b border-zinc-100 px-6 py-5"
>
{
record
.
feedbackUnit
||
'-'
}
</
td
>
<
td
className=
"border-b border-zinc-100 px-6 py-5"
>
<
StatusPill
text=
{
record
.
progress
||
'未填写'
}
tone=
{
resolveProgressTone
(
record
.
progress
)
}
/>
</
td
>
<
td
className=
"border-b border-zinc-100 px-6 py-5"
>
<
StatusPill
text=
{
record
.
auditStatus
||
'未填写'
}
tone=
{
resolveAuditTone
(
record
.
auditStatus
)
}
/>
</
td
>
<
td
className=
"border-b border-zinc-100 px-6 py-5"
>
<
StatusPill
text=
{
record
.
finalAuditStatus
||
'未填写'
}
tone=
{
resolveAuditTone
(
record
.
finalAuditStatus
)
}
/>
/>
</
td
>
<
td
className=
"border-b border-zinc-100 px-6 py-5"
>
{
record
.
dueDateLabel
}
</
td
>
<
td
className=
"max-w-[180px] border-b border-zinc-100 px-6 py-5"
>
<
div
>
{
record
.
problemType
||
'-'
}
</
div
>
<
div
className=
"mt-2 text-xs text-slate-500"
>
{
record
.
problemFrom
||
'未填写来源'
}
</
div
>
</
div
>
</
td
>
</
TableCell
>
<
td
className=
"max-w-[220px] border-b border-zinc-100 px-6 py-5"
>
<
div
className=
"text-xs uppercase tracking-[0.18em] text-slate-400"
>
{
stickyColumns
.
orderedColumns
.
map
((
column
)
=>
{
填写人
const
stickyMeta
=
stickyColumns
.
stickyMeta
[
column
.
field
];
</
div
>
return
(
<
div
className=
"mt-1"
>
{
formatUsers
(
record
.
fillUsers
)
}
</
div
>
<
TableCell
<
div
className=
"mt-3 text-xs uppercase tracking-[0.18em] text-slate-400"
>
key=
{
column
.
field
}
复核人
style=
{
{
</
div
>
width
:
column
.
width
||
180
,
<
div
className=
"mt-1"
>
{
formatUsers
(
record
.
reviewUsers
)
}
</
div
>
minWidth
:
Math
.
min
(
Math
.
max
(
column
.
width
||
180
,
150
),
360
),
</
td
>
left
:
stickyMeta
?.
side
===
'left'
?
`${stickyMeta.offset}px`
:
undefined
,
<
td
className=
"max-w-[340px] border-b border-zinc-100 px-6 py-5"
>
right
:
stickyMeta
?.
side
===
'right'
?
`${stickyMeta.offset}px`
:
undefined
,
<
div
className=
"line-clamp-2 text-slate-700"
>
}
}
{
record
.
brief
||
'暂无问题简述'
}
className=
{
clsx
(
'align-top'
,
getStickyCellClass
(
stickyMeta
))
}
</
div
>
<
div
className=
"mt-2 text-xs leading-5 text-slate-500"
>
{
record
.
relatedIssueDomain
||
'未填写业务领域'
}
</
div
>
</
td
>
<
td
className=
"border-b border-zinc-100 px-6 py-5"
>
<
div
className=
"flex flex-col gap-2"
>
<
button
type=
"button"
onClick=
{
()
=>
handleDelete
([
record
])
}
className=
"rounded-full border border-rose-200 px-3 py-1 text-xs font-medium text-rose-700 transition hover:bg-rose-50"
>
>
<
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
])
}
>
删除
删除
</
b
utton
>
</
B
utton
>
</
div
>
</
div
>
</
td
>
</
TableCell
>
</
tr
>
</
TableRow
>
))
}
))
}
</
tb
ody
>
</
TableB
ody
>
</
t
able
>
</
T
able
>
</
div
>
</
div
>
)
}
)
}
</
section
>
</
section
>
...
@@ -992,29 +1036,30 @@ export default function ProblemRectificationListPage() {
...
@@ -992,29 +1036,30 @@ export default function ProblemRectificationListPage() {
<
Dialog
open=
{
showCreateModal
}
onClose=
{
handleCloseCreateModal
}
size=
"5xl"
>
<
Dialog
open=
{
showCreateModal
}
onClose=
{
handleCloseCreateModal
}
size=
"5xl"
>
<
DialogTitle
>
新增问题整改记录
</
DialogTitle
>
<
DialogTitle
>
新增问题整改记录
</
DialogTitle
>
<
DialogDescription
>
<
DialogDescription
>
表单字段已按
<
code
className=
"rounded bg-zinc-100 px-1.5 py-0.5 text-xs"
>
problem_rectification
</
code
>
{
' '
}
当前新增表单按视图
<
code
className=
"rounded bg-zinc-100 px-1.5 py-0.5 text-xs"
>
{
activeGridView
?.
title
||
activeGridView
?.
name
||
'默认视图'
}
</
code
>
{
' '
}
对象定义渲染,`select` 选项直接读取对象配置,日期字段会在提交时自动转成
ISO 8601。
的字段顺序渲染,`select` 选项来自 Subject 配置,日期提交时会自动转为
ISO 8601。
</
DialogDescription
>
</
DialogDescription
>
<
DialogBody
>
<
DialogBody
>
<
div
className=
"rounded-[24px] border border-zinc-100 bg-zinc-50/
5
0 px-4 py-3 text-sm text-slate-500"
>
<
div
className=
"rounded-[24px] border border-zinc-100 bg-zinc-50/
6
0 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"
>
{
createFields
.
map
((
fieldDef
)
=>
{
{
createFields
.
map
((
field
)
=>
{
const
isLongText
=
fieldDef
.
type
===
'long_text'
;
const
fieldDef
=
field
.
fieldDef
;
const
isSingleColumn
=
!
isLongText
&&
fieldDef
.
name
!==
'responsible_department'
;
const
isLongText
=
fieldDef
?.
type
===
'long_text'
;
const
fullWidth
=
isLongText
||
fieldDef
?.
multiple
;
return
(
return
(
<
div
<
div
key=
{
field
Def
.
name
}
key=
{
field
.
field
}
className=
{
clsx
(
className=
{
clsx
(
'rounded-[24px] border border-zinc-200 bg-white/9
0
p-4 shadow-sm'
,
'rounded-[24px] border border-zinc-200 bg-white/9
2
p-4 shadow-sm'
,
isSingleColumn
?
''
:
'md:col-span-2'
,
fullWidth
&&
'md:col-span-2'
,
)
}
)
}
>
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
{
field
Def
.
label
||
fieldDef
.
name
}
{
field
.
label
}
</
label
>
</
label
>
<
CreateField
<
CreateField
fieldDef=
{
fieldDef
}
fieldDef=
{
fieldDef
}
...
@@ -1027,9 +1072,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1027,9 +1072,7 @@ export default function ProblemRectificationListPage() {
}
}
/>
/>
{
FIELD_HINTS
[
fieldDef
.
name
]
?
(
{
FIELD_HINTS
[
fieldDef
.
name
]
?
(
<
div
className=
"mt-2 text-xs leading-5 text-slate-500"
>
<
div
className=
"mt-2 text-xs leading-5 text-slate-500"
>
{
FIELD_HINTS
[
fieldDef
.
name
]
}
</
div
>
{
FIELD_HINTS
[
fieldDef
.
name
]
}
</
div
>
)
:
null
}
)
:
null
}
</
div
>
</
div
>
);
);
...
@@ -1040,7 +1083,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1040,7 +1083,7 @@ export default function ProblemRectificationListPage() {
<
Button
outline
onClick=
{
handleCloseCreateModal
}
>
<
Button
outline
onClick=
{
handleCloseCreateModal
}
>
取消
取消
</
Button
>
</
Button
>
<
Button
color=
"blue"
disabled=
{
actionLoading
||
!
createS
ubject
}
onClick=
{
handleCreate
}
>
<
Button
color=
"blue"
disabled=
{
actionLoading
||
!
s
ubject
}
onClick=
{
handleCreate
}
>
{
actionLoading
?
'提交中...'
:
'确认新增'
}
{
actionLoading
?
'提交中...'
:
'确认新增'
}
</
Button
>
</
Button
>
</
DialogActions
>
</
DialogActions
>
...
@@ -1058,7 +1101,8 @@ export default function ProblemRectificationListPage() {
...
@@ -1058,7 +1101,8 @@ export default function ProblemRectificationListPage() {
>
>
<
DialogTitle
>
推送问题整改表
</
DialogTitle
>
<
DialogTitle
>
推送问题整改表
</
DialogTitle
>
<
DialogDescription
>
<
DialogDescription
>
已选择
{
selectedRecords
.
length
}
条记录。请通过远程搜索选择填报人和复核人。
已从视图
<
code
className=
"rounded bg-zinc-100 px-1.5 py-0.5 text-xs"
>
{
activeGridView
?.
title
||
activeGridView
?.
name
||
'-'
}
</
code
>
{
' '
}
中选择
{
selectedRecords
.
length
}
条记录,请为本次流程配置填报人和复核人。
</
DialogDescription
>
</
DialogDescription
>
<
DialogBody
>
<
DialogBody
>
<
div
className=
"grid gap-6 md:grid-cols-2"
>
<
div
className=
"grid gap-6 md:grid-cols-2"
>
...
@@ -1077,6 +1121,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1077,6 +1121,7 @@ export default function ProblemRectificationListPage() {
placeholder=
"搜索复核人用户名或显示名"
placeholder=
"搜索复核人用户名或显示名"
/>
/>
</
div
>
</
div
>
<
div
className=
"mt-6 rounded-[24px] border border-zinc-200 bg-zinc-50/80 p-4"
>
<
div
className=
"mt-6 rounded-[24px] border border-zinc-200 bg-zinc-50/80 p-4"
>
<
div
className=
"text-sm font-medium text-slate-900"
>
本次推送记录
</
div
>
<
div
className=
"text-sm font-medium text-slate-900"
>
本次推送记录
</
div
>
<
div
className=
"mt-3 flex flex-wrap gap-2"
>
<
div
className=
"mt-3 flex flex-wrap gap-2"
>
...
@@ -1085,7 +1130,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1085,7 +1130,7 @@ export default function ProblemRectificationListPage() {
key=
{
record
.
id
}
key=
{
record
.
id
}
className=
"rounded-full bg-white px-3 py-1 text-xs font-medium text-slate-700 shadow-sm ring-1 ring-zinc-200"
className=
"rounded-full bg-white px-3 py-1 text-xs font-medium text-slate-700 shadow-sm ring-1 ring-zinc-200"
>
>
{
record
.
projectN
ame
}
{
record
.
title
||
record
.
projectName
||
record
.
n
ame
}
</
span
>
</
span
>
))
}
))
}
</
div
>
</
div
>
...
@@ -1108,6 +1153,16 @@ export default function ProblemRectificationListPage() {
...
@@ -1108,6 +1153,16 @@ 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
>
</
div
>
);
);
}
}
src/pages/problem_rectification/problem-rectification.business-flow.md
View file @
b52ff11f
...
@@ -51,6 +51,19 @@
...
@@ -51,6 +51,19 @@
-
新增、编辑、查看详情优先采用抽屉或弹窗,不走重型嵌套表格交互
-
新增、编辑、查看详情优先采用抽屉或弹窗,不走重型嵌套表格交互
-
批量操作放在顶部工具栏,不再依赖右键菜单作为唯一入口
-
批量操作放在顶部工具栏,不再依赖右键菜单作为唯一入口
### 3.3 视图配置驱动约定
问题整改主列表与详情抽屉统一以
`getSubjectView(subjectName)`
返回的视图配置驱动,不再在页面内写死部门标签和字段顺序:
-
`type = "grid"`
的视图用于主列表顶部视图切换,替代原先写死的 tab
-
`settings.columns`
决定列表列顺序、宽度和排序优先级
-
`settings.restrictions`
与
`settings.filters.conditions`
共同决定该视图下可见的数据范围
-
`settings.viewAuth`
决定当前用户是否能看到该视图
-
`type = "detail"`
的视图用于详情抽屉字段编排,按
`columns`
顺序渲染字段
-
`settings.auth`
与
`disableColumns`
用于控制详情抽屉内字段是否允许编辑
因此前端新增字段展示、部门视图调整、列顺序变化时,应优先改 Subject View 配置,而不是直接修改页面常量。
### 3.1 目标页面拆分
### 3.1 目标页面拆分
为便于当前项目直接重构,建议把模块拆成以下 4 个页面入口:
为便于当前项目直接重构,建议把模块拆成以下 4 个页面入口:
...
@@ -179,6 +192,13 @@
...
@@ -179,6 +192,13 @@
-
表格内分组拖拽
-
表格内分组拖拽
-
全屏容器能力
-
全屏容器能力
替代实现约定:
-
列表页保留表格浏览、搜索、筛选、批量选择
-
字段编辑统一进入右侧详情抽屉
-
长文本、附件、状态类字段在详情抽屉中直接维护
-
详情抽屉内字段顺序与显示权限跟随
`detail view`
配置
---
---
## 6. 流程角色
## 6. 流程角色
...
@@ -624,6 +644,7 @@ const result = await queryUsers(0, 20, {
...
@@ -624,6 +644,7 @@ const result = await queryUsers(0, 20, {
布局层负责:
布局层负责:
-
左侧菜单
-
左侧菜单
-
桌面展开态仅展示模块名称,去掉大 Logo;仅在桌面折叠态展示小 Logo
-
顶部用户区
-
顶部用户区
-
内容容器
-
内容容器
-
移动端侧栏开关
-
移动端侧栏开关
...
...
src/pages/problem_rectification/problemRectificationListService.js
View file @
b52ff11f
...
@@ -7,6 +7,7 @@ import {
...
@@ -7,6 +7,7 @@ import {
import
{
DEPARTMENT_GROUPS
}
from
'./problemRectificationDashboardService'
;
import
{
DEPARTMENT_GROUPS
}
from
'./problemRectificationDashboardService'
;
import
{
import
{
buildViewColumns
,
buildViewColumns
,
checkViewCondition
,
emptyValueForField
,
emptyValueForField
,
formatFieldValue
,
formatFieldValue
,
getFieldNameFromPath
,
getFieldNameFromPath
,
...
@@ -162,6 +163,18 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
...
@@ -162,6 +163,18 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
return
false
;
return
false
;
}
}
if
(
!
checkViewCondition
(
{
relationship
:
gridView
?.
settings
?.
filters
?.
relationship
||
'all'
,
rules
:
gridView
?.
settings
?.
filters
?.
conditions
||
[],
},
rawRecord
,
)
)
{
return
false
;
}
if
(
nextProgress
&&
record
.
progress
!==
nextProgress
)
{
if
(
nextProgress
&&
record
.
progress
!==
nextProgress
)
{
return
false
;
return
false
;
}
}
...
@@ -200,6 +213,18 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
...
@@ -200,6 +213,18 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
};
};
}
}
export
function
buildGridViewCountMap
(
records
,
filters
,
views
,
subject
)
{
return
views
.
reduce
((
counts
,
view
)
=>
{
counts
[
view
.
name
]
=
buildProblemRectificationConfiguredListView
(
records
,
filters
,
view
,
subject
,
).
filteredRecords
.
length
;
return
counts
;
},
{});
}
function
matchesDepartmentGroup
(
record
,
groupKey
)
{
function
matchesDepartmentGroup
(
record
,
groupKey
)
{
if
(
!
groupKey
||
groupKey
===
'all'
)
{
if
(
!
groupKey
||
groupKey
===
'all'
)
{
return
true
;
return
true
;
...
...
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