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
8bb072d7
Commit
8bb072d7
authored
May 27, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: improve problem rectification workflow ui
parent
78c9be51
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1521 additions
and
323 deletions
+1521
-323
Layout.jsx
src/pages/problem_rectification/Layout.jsx
+0
-16
index.jsx
src/pages/problem_rectification/form/index.jsx
+540
-4
index.jsx
src/pages/problem_rectification/list/index.jsx
+369
-285
problem-rectification.business-flow.md
...blem_rectification/problem-rectification.business-flow.md
+20
-5
problemRectificationFormService.js
.../problem_rectification/problemRectificationFormService.js
+478
-0
problemRectificationListService.js
.../problem_rectification/problemRectificationListService.js
+94
-0
subjectsDefinition.md
src/services/subjectsDefinition.md
+20
-13
No files found.
src/pages/problem_rectification/Layout.jsx
View file @
8bb072d7
...
@@ -376,22 +376,6 @@ export default function ProblemRectificationLayout() {
...
@@ -376,22 +376,6 @@ export default function ProblemRectificationLayout() {
<
div
className=
"text-sm text-zinc-500"
>
{
currentGroupTitle
}
</
div
>
<
div
className=
"text-sm text-zinc-500"
>
{
currentGroupTitle
}
</
div
>
</
div
>
</
div
>
<
div
className=
"mb-4 rounded-3xl border border-white/80 bg-white/75 px-5 py-4 shadow-sm backdrop-blur"
>
<
div
className=
"flex flex-wrap items-center justify-between gap-3"
>
<
div
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.24em] text-rose-500"
>
Problem Rectification
</
div
>
<
div
className=
"mt-1 text-xl font-semibold text-slate-900"
>
问题整改模块重构
</
div
>
</
div
>
<
div
className=
"text-sm text-slate-500"
>
先保证业务流转通顺,再逐步补齐细节体验。
</
div
>
</
div
>
</
div
>
<
div
className=
"flex-1 overflow-y-auto rounded-[28px] border border-white/80 bg-white/80 p-4 shadow-sm backdrop-blur sm:p-6"
>
<
div
className=
"flex-1 overflow-y-auto rounded-[28px] border border-white/80 bg-white/80 p-4 shadow-sm backdrop-blur sm:p-6"
>
<
Outlet
/>
<
Outlet
/>
</
div
>
</
div
>
...
...
src/pages/problem_rectification/form/index.jsx
View file @
8bb072d7
import
ModuleEmptyState
from
'../ModuleEmptyState'
;
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
{
Dialog
,
DialogActions
,
DialogBody
,
DialogDescription
,
DialogTitle
,
}
from
'@/catalyst-ui-kit/javascript/dialog'
;
import
{
Table
,
TableBody
,
TableCell
,
TableHead
,
TableHeader
,
TableRow
,
}
from
'@/catalyst-ui-kit/javascript/table'
;
import
TailwindEditableField
from
'@/pages/RecordDetailDrawer/components/TailwindEditableField'
;
import
{
updateRecord
}
from
'@/services/DataService'
;
import
{
normalizeText
}
from
'@/utils/subjectView'
;
import
clsx
from
'clsx'
;
import
dayjs
from
'dayjs'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useSearchParams
}
from
'umi'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
approveByAuditor
,
approveBySender
,
loadProblemRectificationFlow
,
markEditorComplete
,
refreshRelatedProblemRecords
,
rejectByAuditor
,
rejectBySender
,
}
from
'../problemRectificationFormService'
;
const
EDITABLE_FIELD_NAMES
=
[
'rectification_progress'
,
'rectification_situation'
,
'the_time_of_rectification'
,
'remark'
,
'rectification_proof_materials'
,
];
function
formatUserName
(
user
)
{
if
(
!
user
)
{
return
'-'
;
}
return
user
.
display_name
||
user
.
username
||
user
.
name
||
user
.
_id
||
'-'
;
}
function
formatUserList
(
users
=
[])
{
if
(
!
users
.
length
)
{
return
'-'
;
}
return
users
.
map
((
item
)
=>
formatUserName
(
item
)).
join
(
'、'
);
}
function
formatDateText
(
value
)
{
if
(
!
value
)
{
return
'-'
;
}
const
parsed
=
dayjs
(
value
);
return
parsed
.
isValid
()
?
parsed
.
format
(
'YYYY-MM-DD'
)
:
normalizeText
(
value
)
||
'-'
;
}
function
StepBadge
({
title
,
status
,
active
})
{
return
(
<
div
className=
{
clsx
(
'rounded-[24px] border px-4 py-4 shadow-sm transition'
,
active
?
'border-amber-300 bg-amber-50/90'
:
status
===
'已完成'
?
'border-emerald-200 bg-emerald-50/90'
:
'border-white/80 bg-white/80'
,
)
}
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.24em] text-slate-400"
>
{
title
}
</
div
>
<
div
className=
"mt-2 text-lg font-semibold text-slate-900"
>
{
status
||
'未完成'
}
</
div
>
</
div
>
);
}
function
SummaryCard
({
label
,
value
,
hint
})
{
return
(
<
div
className=
"rounded-[24px] border border-white/80 bg-white/82 px-5 py-4 shadow-sm"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.22em] text-slate-400"
>
{
label
}
</
div
>
<
div
className=
"mt-2 text-base font-semibold text-slate-900"
>
{
value
}
</
div
>
{
hint
?
<
div
className=
"mt-1 text-sm text-slate-500"
>
{
hint
}
</
div
>
:
null
}
</
div
>
);
}
function
StatusTag
({
value
})
{
const
toneClass
=
value
===
'已完成'
||
value
===
'已审批'
?
'bg-emerald-50 text-emerald-700 ring-emerald-100'
:
value
===
'已驳回'
?
'bg-rose-50 text-rose-700 ring-rose-100'
:
'bg-amber-50 text-amber-700 ring-amber-100'
;
return
(
<
span
className=
{
clsx
(
'inline-flex rounded-full px-3 py-1 text-xs font-medium ring-1'
,
toneClass
)
}
>
{
value
||
'未完成'
}
</
span
>
);
}
function
EmptyState
({
title
,
description
})
{
return
(
<
div
className=
"rounded-[28px] border border-dashed border-zinc-300 bg-white/70 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
FieldBlock
({
label
,
fieldDef
,
value
,
readonly
,
disabled
,
onSave
})
{
return
(
<
div
className=
"rounded-[22px] border border-zinc-200 bg-zinc-50/70 p-4"
>
<
div
className=
"mb-3 text-sm font-medium text-slate-700"
>
{
label
}
</
div
>
<
TailwindEditableField
value=
{
value
}
fieldDef=
{
fieldDef
}
readonly=
{
readonly
}
disabled=
{
disabled
}
onSave=
{
onSave
}
/>
</
div
>
);
}
export
default
function
ProblemRectificationFormPage
()
{
export
default
function
ProblemRectificationFormPage
()
{
const
userInfo
=
useContext
(
UserContext
);
const
[
searchParams
]
=
useSearchParams
();
const
messageId
=
searchParams
.
get
(
'id'
)
||
''
;
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
submitting
,
setSubmitting
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
flowData
,
setFlowData
]
=
useState
(
null
);
const
[
selectedRecord
,
setSelectedRecord
]
=
useState
(
null
);
const
reload
=
async
()
=>
{
if
(
!
messageId
)
{
setFlowData
(
null
);
setLoading
(
false
);
setError
(
'缺少流程 ID,无法加载整改流程。'
);
return
;
}
setLoading
(
true
);
setError
(
''
);
try
{
const
result
=
await
loadProblemRectificationFlow
(
messageId
,
userInfo
);
setFlowData
(
result
);
}
catch
(
requestError
)
{
setFlowData
(
null
);
setError
(
requestError
?.
message
||
'整改流程加载失败,请稍后重试。'
);
}
finally
{
setLoading
(
false
);
}
};
useEffect
(()
=>
{
reload
();
},
[
messageId
,
userInfo
?.
_id
]);
const
messageRecord
=
flowData
?.
messageRecord
||
null
;
const
subject
=
flowData
?.
subject
||
null
;
const
participants
=
flowData
?.
participants
||
{};
const
relatedRecords
=
flowData
?.
relatedRecords
||
[];
const
currentStep
=
flowData
?.
currentStep
||
'editor'
;
const
viewerRole
=
flowData
?.
viewerRole
||
'guest'
;
const
canEditRecords
=
Boolean
(
flowData
?.
canEditRecords
);
const
metadata
=
messageRecord
?.
metadata
||
{};
const
fieldMap
=
useMemo
(()
=>
{
const
entries
=
(
subject
?.
fields
||
[]).
map
((
field
)
=>
[
field
.
name
,
field
]);
return
Object
.
fromEntries
(
entries
);
},
[
subject
]);
const
editableFields
=
useMemo
(
()
=>
EDITABLE_FIELD_NAMES
.
map
((
name
)
=>
fieldMap
[
name
]).
filter
(
Boolean
),
[
fieldMap
],
);
const
summary
=
useMemo
(()
=>
{
const
completedRectificationCount
=
relatedRecords
.
filter
(
(
record
)
=>
record
.
progress
===
'已整改'
||
record
.
progress
===
'已优化'
,
).
length
;
const
approvedCount
=
relatedRecords
.
filter
(
(
record
)
=>
normalizeText
(
record
.
auditStatus
)
===
'已审批'
,
).
length
;
return
{
total
:
relatedRecords
.
length
,
completedRectificationCount
,
approvedCount
,
};
},
[
relatedRecords
]);
const
refreshAfterMutation
=
async
()
=>
{
const
nextMessage
=
await
loadProblemRectificationFlow
(
messageId
,
userInfo
);
setFlowData
(
nextMessage
);
if
(
selectedRecord
)
{
const
refreshedRecords
=
await
refreshRelatedProblemRecords
(
nextMessage
.
messageRecord
);
const
matched
=
refreshedRecords
.
find
((
item
)
=>
item
.
id
===
selectedRecord
.
id
);
setSelectedRecord
(
matched
||
null
);
}
};
const
handleSaveField
=
async
(
fieldName
,
nextValue
)
=>
{
if
(
!
selectedRecord
)
{
return
;
}
await
updateRecord
(
selectedRecord
.
id
,
{
metadata
:
{
[
fieldName
]:
fieldMap
[
fieldName
]?.
type
===
'date'
&&
nextValue
?
dayjs
(
nextValue
).
isValid
()
?
dayjs
(
nextValue
).
toISOString
()
:
nextValue
:
nextValue
,
},
});
await
refreshAfterMutation
();
};
const
withSubmit
=
async
(
handler
)
=>
{
if
(
submitting
)
{
return
;
}
setSubmitting
(
true
);
setError
(
''
);
try
{
await
handler
();
await
refreshAfterMutation
();
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'流程处理失败,请稍后重试。'
);
}
finally
{
setSubmitting
(
false
);
}
};
const
handleEditorSubmit
=
async
()
=>
{
await
withSubmit
(
async
()
=>
{
await
markEditorComplete
({
messageRecord
,
currentUser
:
userInfo
,
auditors
:
participants
.
auditors
||
[],
});
});
};
const
handleAuditorApprove
=
async
()
=>
{
await
withSubmit
(
async
()
=>
{
await
approveByAuditor
({
messageRecord
,
currentUser
:
userInfo
,
sender
:
participants
.
sender
,
});
});
};
const
handleAuditorReject
=
async
()
=>
{
await
withSubmit
(
async
()
=>
{
await
rejectByAuditor
({
messageRecord
,
currentUser
:
userInfo
,
editors
:
participants
.
editors
||
[],
});
});
};
const
handleSenderApprove
=
async
()
=>
{
await
withSubmit
(
async
()
=>
{
await
approveBySender
({
messageRecord
,
});
});
};
const
handleSenderReject
=
async
()
=>
{
await
withSubmit
(
async
()
=>
{
await
rejectBySender
({
messageRecord
,
currentUser
:
userInfo
,
editors
:
participants
.
editors
||
[],
});
});
};
const
actionHint
=
viewerRole
===
'editor'
?
'你当前是填写人,请完善整改内容后点击送审。'
:
viewerRole
===
'auditor'
?
'你当前是复核人,请核对整改内容后进行通过或驳回。'
:
viewerRole
===
'sender'
?
'你当前是发送人,请在复核完成后执行最终审核。'
:
'你当前不是本流程参与人,仅可查看。'
;
return
(
return
(
<
ModuleEmptyState
<
div
className=
"space-y-6"
>
title=
"整改填写 / 复核 / 最终审核页"
<
section
className=
"overflow-hidden rounded-[34px] border border-white/70 bg-[linear-gradient(135deg,rgba(255,255,255,0.98),rgba(255,247,237,0.95)_45%,rgba(240,249,255,0.95))] px-6 py-6 shadow-[0_24px_60px_rgba(15,23,42,0.08)]"
>
description=
"按流程处理整改记录。"
<
div
className=
"flex flex-wrap items-start justify-between gap-5"
>
<
div
className=
"max-w-4xl"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.28em] text-amber-600"
>
Problem Rectification Flow
</
div
>
<
h1
className=
"mt-3 text-3xl font-semibold tracking-tight text-slate-950"
>
整改填写 / 复核 / 最终审核
</
h1
>
<
p
className=
"mt-3 max-w-3xl text-sm leading-6 text-slate-600"
>
当前流程通过
<
code
className=
"rounded bg-white/80 px-1.5 py-0.5 text-xs"
>
{
messageId
||
'-'
}
</
code
>
{
' '
}
绑定整改记录。页面会按照“填写人、审核人、发送人”三段式推进,并把流程状态回写到
<
code
className=
"mx-1 rounded bg-white/80 px-1.5 py-0.5 text-xs"
>
message_reference_id.metadata
</
code
>
中。
</
p
>
</
div
>
<
div
className=
"min-w-64 rounded-[26px] border border-white/80 bg-white/82 p-4 shadow-sm"
>
<
div
className=
"text-sm font-medium text-slate-500"
>
当前身份
</
div
>
<
div
className=
"mt-2 text-lg font-semibold text-slate-900"
>
{
viewerRole
===
'editor'
?
'填写人'
:
viewerRole
===
'auditor'
?
'审核人'
:
viewerRole
===
'sender'
?
'发送人'
:
'只读查看'
}
</
div
>
<
div
className=
"mt-2 text-sm leading-6 text-slate-500"
>
{
actionHint
}
</
div
>
</
div
>
</
div
>
</
section
>
{
loading
?
(
<
EmptyState
title=
"正在加载流程"
description=
"系统正在读取 message_reference_id 与关联整改记录。"
/>
)
:
error
?
(
<
div
className=
"rounded-[28px] border border-rose-200 bg-rose-50 px-6 py-5 text-sm text-rose-700 shadow-sm"
>
{
error
}
</
div
>
)
:
!
messageRecord
?
(
<
EmptyState
title=
"未找到流程数据"
description=
"请确认消息链接中的 id 是否正确,或检查流程主单是否已存在。"
/>
)
:
(
<>
<
section
className=
"grid gap-4 lg:grid-cols-3"
>
<
StepBadge
title=
"填写阶段"
status=
{
metadata
.
editor_status
||
'未完成'
}
active=
{
currentStep
===
'editor'
}
/>
<
StepBadge
title=
"复核阶段"
status=
{
metadata
.
auditor_status
||
'未完成'
}
active=
{
currentStep
===
'auditor'
}
/>
<
StepBadge
title=
"最终审核"
status=
{
metadata
.
sender_status
||
'未完成'
}
active=
{
currentStep
===
'sender'
}
/>
</
section
>
<
section
className=
"grid gap-4 lg:grid-cols-4"
>
<
SummaryCard
label=
"关联记录数"
value=
{
`${summary.total} 条`
}
hint=
{
`约束字段:${metadata.restriction_key || '_id'}`
}
/>
<
SummaryCard
label=
"已整改记录"
value=
{
`${summary.completedRectificationCount} 条`
}
hint=
"以整改进度为准"
/>
<
SummaryCard
label=
"复核通过"
value=
{
`${summary.approvedCount} 条`
}
hint=
"以 audit_status 为准"
/>
<
SummaryCard
label=
"当前对象"
value=
{
metadata
.
subject_name
||
'-'
}
hint=
{
`视图:${metadata.view_name || '-'}`
}
/>
</
section
>
<
section
className=
"grid gap-4 xl:grid-cols-[1.2fr,1fr]"
>
<
div
className=
"rounded-[30px] border border-white/80 bg-white/82 p-5 shadow-sm"
>
<
div
className=
"flex items-center justify-between gap-4"
>
<
div
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
关联整改记录
</
div
>
<
div
className=
"mt-1 text-sm text-slate-500"
>
点击“查看/填写”可进入单条整改详情维护。
</
div
>
</
div
>
</
div
>
{
relatedRecords
.
length
===
0
?
(
<
div
className=
"mt-4 rounded-[24px] border border-dashed border-zinc-300 bg-zinc-50/70 px-6 py-12 text-center text-sm text-slate-500"
>
当前流程还没有绑定整改记录。
</
div
>
)
:
(
<
div
className=
"mt-4"
>
<
Table
bleed
className=
"[--gutter:--spacing(5)]"
>
<
TableHead
>
<
TableRow
>
<
TableHeader
>
标题
</
TableHeader
>
<
TableHeader
>
反馈单位
</
TableHeader
>
<
TableHeader
>
整改进度
</
TableHeader
>
<
TableHeader
>
复核状态
</
TableHeader
>
<
TableHeader
>
最终审批
</
TableHeader
>
<
TableHeader
>
预计完成时间
</
TableHeader
>
<
TableHeader
>
操作
</
TableHeader
>
</
TableRow
>
</
TableHead
>
<
TableBody
>
{
relatedRecords
.
map
((
record
)
=>
(
<
TableRow
key=
{
record
.
id
}
>
<
TableCell
>
<
div
className=
"font-medium text-slate-900"
>
{
record
.
title
||
'-'
}
</
div
>
<
div
className=
"mt-1 text-xs text-slate-500"
>
{
record
.
projectName
||
'-'
}
</
div
>
</
TableCell
>
<
TableCell
>
{
record
.
feedbackUnit
||
'-'
}
</
TableCell
>
<
TableCell
>
<
StatusTag
value=
{
record
.
progress
||
'未填写'
}
/>
</
TableCell
>
<
TableCell
>
<
StatusTag
value=
{
record
.
auditStatus
||
'未完成'
}
/>
</
TableCell
>
<
TableCell
>
<
StatusTag
value=
{
record
.
finalAuditStatus
||
'未完成'
}
/>
</
TableCell
>
<
TableCell
>
{
record
.
dueDateLabel
||
'-'
}
</
TableCell
>
<
TableCell
>
<
Button
outline
onClick=
{
()
=>
setSelectedRecord
(
record
)
}
>
{
canEditRecords
?
'查看/填写'
:
'查看详情'
}
</
Button
>
</
TableCell
>
</
TableRow
>
))
}
</
TableBody
>
</
Table
>
</
div
>
)
}
</
div
>
<
div
className=
"space-y-4"
>
<
div
className=
"rounded-[30px] border border-white/80 bg-white/82 p-5 shadow-sm"
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
流程参与人
</
div
>
<
div
className=
"mt-4 grid gap-4"
>
<
SummaryCard
label=
"发送人"
value=
{
formatUserName
(
participants
.
sender
)
}
/>
<
SummaryCard
label=
"填写人"
value=
{
formatUserList
(
participants
.
editors
||
[])
}
/>
<
SummaryCard
label=
"审核人"
value=
{
formatUserList
(
participants
.
auditors
||
[])
}
/>
</
div
>
</
div
>
<
div
className=
"rounded-[30px] border border-white/80 bg-white/82 p-5 shadow-sm"
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
流程操作
</
div
>
<
div
className=
"mt-2 text-sm leading-6 text-slate-500"
>
页面根据当前步骤和你的身份自动开放可执行动作。
</
div
>
<
div
className=
"mt-5 flex flex-wrap gap-3"
>
{
currentStep
===
'editor'
&&
viewerRole
===
'editor'
?
(
<
Button
color=
"blue"
disabled=
{
submitting
||
relatedRecords
.
length
===
0
}
onClick=
{
handleEditorSubmit
}
>
{
submitting
?
'送审中...'
:
'填写完成并送审'
}
</
Button
>
)
:
null
}
{
currentStep
===
'auditor'
&&
viewerRole
===
'auditor'
?
(
<>
<
Button
color=
"emerald"
disabled=
{
submitting
||
relatedRecords
.
length
===
0
}
onClick=
{
handleAuditorApprove
}
>
{
submitting
?
'处理中...'
:
'复核通过'
}
</
Button
>
<
Button
color=
"red"
disabled=
{
submitting
||
relatedRecords
.
length
===
0
}
onClick=
{
handleAuditorReject
}
>
{
submitting
?
'处理中...'
:
'驳回复填'
}
</
Button
>
</>
)
:
null
}
{
currentStep
===
'sender'
&&
viewerRole
===
'sender'
?
(
<>
<
Button
color=
"blue"
disabled=
{
submitting
||
relatedRecords
.
length
===
0
}
onClick=
{
handleSenderApprove
}
>
{
submitting
?
'处理中...'
:
'最终审核通过'
}
</
Button
>
<
Button
color=
"red"
disabled=
{
submitting
||
relatedRecords
.
length
===
0
}
onClick=
{
handleSenderReject
}
>
{
submitting
?
'处理中...'
:
'最终审核驳回'
}
</
Button
>
</>
)
:
null
}
<
Button
outline
disabled=
{
submitting
}
onClick=
{
reload
}
>
刷新流程
</
Button
>
</
div
>
</
div
>
</
div
>
</
section
>
</>
)
}
<
Dialog
open=
{
Boolean
(
selectedRecord
)
}
onClose=
{
()
=>
setSelectedRecord
(
null
)
}
size=
"4xl"
>
<
DialogTitle
>
{
selectedRecord
?.
title
||
'整改记录详情'
}
</
DialogTitle
>
<
DialogDescription
>
当前记录仅开放整改填写阶段的核心字段编辑。所有写入仍通过
<
code
className=
"mx-1 rounded bg-zinc-100 px-1.5 py-0.5 text-xs"
>
metadata
</
code
>
提交。
</
DialogDescription
>
<
DialogBody
>
{
selectedRecord
?
(
<
div
className=
"space-y-4"
>
<
div
className=
"grid gap-4 md:grid-cols-2"
>
<
SummaryCard
label=
"问题标题"
value=
{
selectedRecord
.
title
||
'-'
}
/>
<
SummaryCard
label=
"反馈单位"
value=
{
selectedRecord
.
feedbackUnit
||
'-'
}
/>
<
SummaryCard
label=
"整改进度"
value=
{
selectedRecord
.
progress
||
'-'
}
/>
<
SummaryCard
label=
"预计完成时间"
value=
{
formatDateText
(
selectedRecord
.
raw
?.
metadata
?.
the_time_of_rectification
)
}
/>
</
div
>
<
div
className=
"grid gap-4 lg:grid-cols-2"
>
{
editableFields
.
map
((
fieldDef
)
=>
(
<
FieldBlock
key=
{
fieldDef
.
name
}
label=
{
fieldDef
.
label
||
fieldDef
.
name
}
fieldDef=
{
fieldDef
}
value=
{
selectedRecord
.
raw
?.
metadata
?.[
fieldDef
.
name
]
}
readonly=
{
!
canEditRecords
}
disabled=
{
!
canEditRecords
}
onSave=
{
(
nextValue
)
=>
handleSaveField
(
fieldDef
.
name
,
nextValue
)
}
/>
/>
))
}
</
div
>
</
div
>
)
:
null
}
</
DialogBody
>
<
DialogActions
>
<
Button
outline
onClick=
{
()
=>
setSelectedRecord
(
null
)
}
>
关闭
</
Button
>
</
DialogActions
>
</
Dialog
>
</
div
>
);
);
}
}
src/pages/problem_rectification/list/index.jsx
View file @
8bb072d7
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
{
Dialog
,
DialogActions
,
DialogBody
,
DialogDescription
,
DialogTitle
,
}
from
'@/catalyst-ui-kit/javascript/dialog'
;
import
{
Input
}
from
'@/catalyst-ui-kit/javascript/input'
;
import
{
Select
}
from
'@/catalyst-ui-kit/javascript/select'
;
import
{
Textarea
}
from
'@/catalyst-ui-kit/javascript/textarea'
;
import
{
import
{
batchRemoveRecords
,
batchRemoveRecords
,
batchUpdateRecords
,
batchUpdateRecords
,
createNotice
,
createNotice
,
createRecord
,
createRecord
,
executeAction
,
executeAction
,
loadSubject
,
loadUser
,
loadUser
,
queryUsers
,
queryUsers
,
sendNotice
,
sendNotice
,
updateRecord
,
updateRecord
,
}
from
'@/services/DataService'
;
}
from
'@/services/DataService'
;
import
*
as
Headless
from
'@headlessui/react'
;
import
clsx
from
'clsx'
;
import
clsx
from
'clsx'
;
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
{
emptyValueForField
,
getSelectOptions
,
normalizeText
,
}
from
'@/utils/subjectView'
;
import
{
DEPARTMENT_GROUPS
}
from
'../problemRectificationDashboardService'
;
import
{
DEPARTMENT_GROUPS
}
from
'../problemRectificationDashboardService'
;
import
{
import
{
PROBLEM_RECTIFICATION_SUBJECT
,
buildProblemRectificationCreatePayload
,
buildProblemRectificationCreatePayload
,
buildProblemRectificationListView
,
buildProblemRectificationListView
,
loadProblemRectificationListData
,
loadProblemRectificationListData
,
}
from
'../problemRectificationListService'
;
}
from
'../problemRectificationListService'
;
const
INITIAL_FORM_VALUES
=
{
const
CREATE_FIELD_ORDER
=
[
projectName
:
''
,
'project_name'
,
inspectUnit
:
''
,
'inspect_the_implementing_unit'
,
implementationTime
:
''
,
'check_the_implementation_time'
,
inspectionReportDate
:
''
,
'inspection_report_date'
,
brief
:
''
,
'rectification_progress'
,
detail
:
''
,
'problem_from'
,
responsibleDepartmentsText
:
''
,
'problem_type'
,
progress
:
''
,
'feedback_unit'
,
rectificationSituation
:
''
,
'related_issue_domain'
,
problemFrom
:
''
,
'the_time_of_rectification'
,
problemType
:
''
,
'responsible_department'
,
feedbackUnit
:
''
,
'problem_brief'
,
relatedIssueDomain
:
''
,
'problem_discovery_situation'
,
dueDate
:
''
,
'rectification_situation'
,
remark
:
''
,
'remark'
,
];
const
FIELD_HINTS
=
{
inspect_the_implementing_unit
:
'按对象配置的实施单位选项选择'
,
rectification_progress
:
'状态选项来自对象配置'
,
problem_from
:
'问题来源选项来自对象配置'
,
problem_type
:
'问题类型选项来自对象配置'
,
feedback_unit
:
'反馈单位选项来自对象配置'
,
responsible_department
:
'可多选责任部门'
,
the_time_of_rectification
:
'提交时会自动转成 ISO 8601 时间'
,
};
};
function
SummaryCard
({
label
,
value
,
hint
,
tone
=
'default'
})
{
function
SummaryCard
({
label
,
value
,
hint
,
tone
=
'default'
})
{
...
@@ -116,87 +145,235 @@ function formatUsers(users) {
...
@@ -116,87 +145,235 @@ function formatUsers(users) {
return
users
&&
users
.
length
>
0
?
users
.
join
(
'、'
)
:
'-'
;
return
users
&&
users
.
length
>
0
?
users
.
join
(
'、'
)
:
'-'
;
}
}
function
ModalCard
({
title
,
description
,
children
,
footer
})
{
function
SelectedUsers
({
users
,
onRemove
})
{
if
(
users
.
length
===
0
)
{
return
(
return
(
<
div
className=
"fixed inset-0 z-[160] flex items-center justify-center bg-slate-950/45 p-4 backdrop-blur-sm"
>
<
div
className=
"rounded-2xl border border-dashed border-zinc-200 bg-zinc-50/80 px-4 py-3 text-sm text-slate-500"
>
<
div
className=
"max-h-[92vh] w-full max-w-3xl overflow-y-auto rounded-[30px] border border-white/70 bg-white p-6 shadow-[0_30px_80px_rgba(15,23,42,0.22)]"
>
暂未选择
<
div
className=
"border-b border-zinc-100 pb-4"
>
<
div
className=
"text-xl font-semibold text-slate-900"
>
{
title
}
</
div
>
{
description
?
(
<
div
className=
"mt-2 text-sm leading-6 text-slate-500"
>
{
description
}
</
div
>
)
:
null
}
</
div
>
<
div
className=
"py-5"
>
{
children
}
</
div
>
{
footer
?
(
<
div
className=
"border-t border-zinc-100 pt-4"
>
{
footer
}
</
div
>
)
:
null
}
</
div
>
</
div
>
);
}
return
(
<
div
className=
"flex flex-wrap gap-2"
>
{
users
.
map
((
user
)
=>
(
<
span
key=
{
user
.
_id
}
className=
"inline-flex items-center gap-2 rounded-full bg-white px-3 py-1.5 text-sm font-medium text-slate-700 shadow-sm ring-1 ring-zinc-200"
>
<
span
>
{
user
.
display_name
||
user
.
username
}
</
span
>
<
button
type=
"button"
onClick=
{
()
=>
onRemove
(
user
.
_id
)
}
className=
"rounded-full text-slate-400 transition hover:text-rose-600"
aria
-
label=
{
`移除 ${user.display_name || user.username}`
}
>
×
</
button
>
</
span
>
))
}
</
div
>
</
div
>
);
);
}
}
function
UserMulti
Select
({
function
RemoteUser
Select
({
label
,
label
,
value
,
value
,
onChange
,
onChange
,
options
,
onSearch
,
placeholder
,
placeholder
,
onSearch
,
})
{
})
{
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
options
,
setOptions
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
requestIdRef
=
useRef
(
0
);
useEffect
(()
=>
{
let
cancelled
=
false
;
const
requestId
=
requestIdRef
.
current
+
1
;
requestIdRef
.
current
=
requestId
;
const
loadOptions
=
async
()
=>
{
setLoading
(
true
);
try
{
const
users
=
await
onSearch
(
keyword
);
if
(
!
cancelled
&&
requestIdRef
.
current
===
requestId
)
{
setOptions
(
users
);
}
}
catch
(
error
)
{
if
(
!
cancelled
&&
requestIdRef
.
current
===
requestId
)
{
setOptions
([]);
}
}
finally
{
if
(
!
cancelled
&&
requestIdRef
.
current
===
requestId
)
{
setLoading
(
false
);
}
}
};
loadOptions
();
return
()
=>
{
cancelled
=
true
;
};
},
[
keyword
,
onSearch
]);
const
selectedUsers
=
useMemo
(()
=>
value
.
filter
(
Boolean
),
[
value
]);
const
selectedIds
=
useMemo
(
()
=>
new
Set
(
selectedUsers
.
map
((
user
)
=>
user
.
_id
)),
[
selectedUsers
],
);
const
availableOptions
=
options
.
filter
((
user
)
=>
!
selectedIds
.
has
(
user
.
_id
));
return
(
return
(
<
div
>
<
div
className=
"space-y-3"
>
<
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
}
{
label
}
</
label
>
</
label
>
<
input
<
Headless
.
Combobox
type=
"text"
value=
{
null
}
onChange=
{
(
event
)
=>
onSearch
(
event
.
target
.
value
)
}
nullable
onChange=
{
(
user
)
=>
{
if
(
!
user
||
selectedIds
.
has
(
user
.
_id
))
{
return
;
}
onChange
([...
selectedUsers
,
user
]);
setKeyword
(
''
);
}
}
>
<
div
className=
"relative"
>
<
Headless
.
ComboboxInput
value=
{
keyword
}
onChange=
{
(
event
)
=>
setKeyword
(
event
.
target
.
value
)
}
displayValue=
{
()
=>
keyword
}
placeholder=
{
placeholder
}
placeholder=
{
placeholder
}
className=
"mb-3
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"
className=
"block
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
className=
"max-h-52 space-y-2 overflow-y-auto rounded-2xl border border-zinc-200 bg-zinc-50 p-3"
>
<
Headless
.
ComboboxButton
className=
"absolute inset-y-0 right-0 flex items-center px-3 text-slate-400"
>
{
options
.
length
>
0
?
(
<
svg
className=
"size-5"
viewBox=
"0 0 20 20"
fill=
"none"
aria
-
hidden=
"true"
>
options
.
map
((
user
)
=>
{
<
path
const
checked
=
value
.
includes
(
user
.
_id
);
d=
"M6 8l4 4 4-4"
return
(
stroke=
"currentColor"
<
label
strokeWidth=
"1.5"
strokeLinecap=
"round"
strokeLinejoin=
"round"
/>
</
svg
>
</
Headless
.
ComboboxButton
>
<
Headless
.
ComboboxOptions
className=
"absolute z-30 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-zinc-200 bg-white p-2 shadow-xl ring-1 ring-black/5 empty:hidden"
>
{
loading
?
(
<
div
className=
"rounded-xl px-3 py-3 text-sm text-slate-500"
>
搜索中...
</
div
>
)
:
availableOptions
.
length
>
0
?
(
availableOptions
.
map
((
user
)
=>
(
<
Headless
.
ComboboxOption
key=
{
user
.
_id
}
key=
{
user
.
_id
}
className=
"flex cursor-pointer items-center gap-3 rounded-xl bg-white px-3 py-2 text-sm text-slate-700 shadow-sm"
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"
>
>
<
input
type=
"checkbox"
checked=
{
checked
}
onChange=
{
(
event
)
=>
{
if
(
event
.
target
.
checked
)
{
onChange
([...
value
,
user
.
_id
]);
}
else
{
onChange
(
value
.
filter
((
item
)
=>
item
!==
user
.
_id
));
}
}
}
/>
<
div
>
<
div
className=
"font-medium text-slate-900"
>
<
div
className=
"font-medium text-slate-900"
>
{
user
.
display_name
||
user
.
username
}
{
user
.
display_name
||
user
.
username
}
</
div
>
</
div
>
<
div
className=
"text-xs text-slate-500"
>
{
user
.
username
}
</
div
>
<
div
className=
"text-xs text-slate-500"
>
{
user
.
username
}
</
div
>
</
div
>
</
Headless
.
ComboboxOption
>
</
label
>
))
);
})
)
:
(
)
:
(
<
div
className=
"rounded-xl bg-white px-3 py-4
text-sm text-slate-500"
>
<
div
className=
"rounded-xl px-3 py-3
text-sm text-slate-500"
>
暂无可选用户
{
keyword
.
trim
()
?
'没有匹配的用户'
:
'请输入关键词搜索用户'
}
</
div
>
</
div
>
)
}
)
}
</
Headless
.
ComboboxOptions
>
</
div
>
</
div
>
</
Headless
.
Combobox
>
<
SelectedUsers
users=
{
selectedUsers
}
onRemove=
{
(
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
})
{
const
options
=
getSelectOptions
(
fieldDef
);
const
isMultiple
=
Boolean
(
fieldDef
?.
multiple
);
const
fieldType
=
fieldDef
?.
type
||
'text'
;
const
placeholder
=
FIELD_HINTS
[
fieldDef
?.
name
]
||
`请输入
${
fieldDef
?.
label
||
fieldDef
?.
name
}
`;
if (fieldType === 'long_text') {
return (
<Textarea
rows={fieldDef?.name === 'problem_discovery_situation' ? 4 : 3}
value={value || ''}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
/>
);
}
if (fieldType === 'date') {
return (
<Input
type="date"
value={value || ''}
onChange={(event) => onChange(event.target.value)}
/>
);
}
if (fieldType === 'select') {
return (
<Select
multiple={isMultiple}
value={isMultiple ? value || [] : value || ''}
onChange={(event) => {
if (isMultiple) {
onChange(
Array.from(event.target.selectedOptions).map((option) => option.value),
);
return;
}
onChange(event.target.value);
}}
>
{!isMultiple ? <option value="">请选择</option> : null}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
);
}
return (
<Input
value={value || ''}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
/>
);
}
export default function ProblemRectificationListPage() {
export default function ProblemRectificationListPage() {
const [records, setRecords] = useState([]);
const [records, setRecords] = useState([]);
const [createSubject, setCreateSubject] = useState(null);
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);
...
@@ -209,10 +386,9 @@ export default function ProblemRectificationListPage() {
...
@@ -209,10 +386,9 @@ export default function ProblemRectificationListPage() {
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
(
INITIAL_FORM_VALUES
);
const [formValues, setFormValues] = useState({});
const
[
fillerIds
,
setFillerIds
]
=
useState
([]);
const [fillerUsers, setFillerUsers] = useState([]);
const
[
auditorIds
,
setAuditorIds
]
=
useState
([]);
const [auditorUsers, setAuditorUsers] = useState([]);
const
[
userOptions
,
setUserOptions
]
=
useState
([]);
const reloadList = async () => {
const reloadList = async () => {
setLoading(true);
setLoading(true);
...
@@ -237,13 +413,14 @@ export default function ProblemRectificationListPage() {
...
@@ -237,13 +413,14 @@ export default function ProblemRectificationListPage() {
(async () => {
(async () => {
try {
try {
const
result
=
await
queryUsers
(
0
,
20
,
{}
);
const
subject = await loadSubject(PROBLEM_RECTIFICATION_SUBJECT
);
if (!cancelled) {
if (!cancelled) {
setUserOptions
(
result
?.
users
||
[]);
setCreateSubject(subject);
setFormValues(buildInitialCreateValues(subject));
}
}
} catch (requestError) {
} catch (requestError) {
if (!cancelled) {
if (!cancelled) {
set
UserOptions
([]
);
set
CreateSubject(null
);
}
}
}
}
})();
})();
...
@@ -257,6 +434,14 @@ export default function ProblemRectificationListPage() {
...
@@ -257,6 +434,14 @@ export default function ProblemRectificationListPage() {
() => buildProblemRectificationListView(records, filters),
() => buildProblemRectificationListView(records, filters),
[records, filters],
[records, filters],
);
);
const createFields = useMemo(
() => normalizeFieldList(createSubject),
[createSubject],
);
const initialCreateValues = useMemo(
() => buildInitialCreateValues(createSubject),
[createSubject],
);
const filteredRecords = view.filteredRecords || [];
const filteredRecords = view.filteredRecords || [];
const groupCounts = view.groupCounts || {};
const groupCounts = view.groupCounts || {};
...
@@ -290,7 +475,10 @@ export default function ProblemRectificationListPage() {
...
@@ -290,7 +475,10 @@ export default function ProblemRectificationListPage() {
};
};
const handleCreate = async () => {
const handleCreate = async () => {
if
(
!
formValues
.
projectName
.
trim
()
&&
!
formValues
.
brief
.
trim
())
{
if (
!normalizeText(formValues.project_name) &&
!normalizeText(formValues.problem_brief)
) {
setError('新增记录至少需要填写检查项目名称或问题简述。');
setError('新增记录至少需要填写检查项目名称或问题简述。');
return;
return;
}
}
...
@@ -299,16 +487,10 @@ export default function ProblemRectificationListPage() {
...
@@ -299,16 +487,10 @@ export default function ProblemRectificationListPage() {
setError('');
setError('');
try {
try {
const
payload
=
buildProblemRectificationCreatePayload
({
const payload = buildProblemRectificationCreatePayload(formValues, createFields);
...
formValues
,
responsibleDepartments
:
formValues
.
responsibleDepartmentsText
.
split
(
/
[
,,
]
/
)
.
map
((
item
)
=>
item
.
trim
())
.
filter
(
Boolean
),
});
await createRecord(payload);
await createRecord(payload);
setShowCreateModal(false);
setShowCreateModal(false);
setFormValues
(
INITIAL_FORM_VALUES
);
setFormValues(
initialCreateValues
);
await reloadList();
await reloadList();
} catch (requestError) {
} catch (requestError) {
setError(requestError?.message || '新增问题整改记录失败,请稍后重试。');
setError(requestError?.message || '新增问题整改记录失败,请稍后重试。');
...
@@ -317,6 +499,11 @@ export default function ProblemRectificationListPage() {
...
@@ -317,6 +499,11 @@ export default function ProblemRectificationListPage() {
}
}
};
};
const handleCloseCreateModal = () => {
setShowCreateModal(false);
setFormValues(initialCreateValues);
};
const handleDelete = async (targetRecords) => {
const handleDelete = async (targetRecords) => {
if (!targetRecords || targetRecords.length === 0) {
if (!targetRecords || targetRecords.length === 0) {
setError('请先选择需要删除的记录。');
setError('请先选择需要删除的记录。');
...
@@ -356,24 +543,24 @@ export default function ProblemRectificationListPage() {
...
@@ -356,24 +543,24 @@ export default function ProblemRectificationListPage() {
}
}
};
};
const
handleSearchUsers
=
async
(
keyword
)
=>
{
const handleSearchUsers = useCallback(async (keyword) => {
try
{
const filter = keyword.trim()
const filter = keyword.trim()
? {
? {
$or: [
$or: [
{
username
:
{
$regex
:
keyword
.
trim
()
}
},
{ username: { $regex: keyword.trim(), $options: 'i'
} },
{
display_name
:
{
$regex
:
keyword
.
trim
()
}
},
{ display_name: { $regex: keyword.trim(), $options: 'i'
} },
],
],
}
}
: {};
: {};
const result = await queryUsers(0, 20, filter);
const result = await queryUsers(0, 20, filter);
setUserOptions
(
result
?.
users
||
[]);
return result?.users || [];
}
catch
(
requestError
)
{
}, []);
setUserOptions
([]);
}
};
const handlePush = async () => {
const handlePush = async () => {
const fillerIds = fillerUsers.map((user) => user._id).filter(Boolean);
const auditorIds = auditorUsers.map((user) => user._id).filter(Boolean);
if (selectedRecords.length === 0) {
if (selectedRecords.length === 0) {
setError('请先勾选需要推送的问题整改记录。');
setError('请先勾选需要推送的问题整改记录。');
return;
return;
...
@@ -401,8 +588,12 @@ export default function ProblemRectificationListPage() {
...
@@ -401,8 +588,12 @@ export default function ProblemRectificationListPage() {
auditor: auditorIds,
auditor: auditorIds,
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),
view_name: filters.groupKey,
view_name: filters.groupKey,
subject_name: 'problem_rectification',
subject_name: 'problem_rectification',
editor_status: '未完成',
auditor_status: '未完成',
sender_status: '未完成',
},
},
});
});
...
@@ -451,8 +642,8 @@ export default function ProblemRectificationListPage() {
...
@@ -451,8 +642,8 @@ export default function ProblemRectificationListPage() {
await
sendNotice
(
noticeResult
.
name
);
await
sendNotice
(
noticeResult
.
name
);
setShowPushModal
(
false
);
setShowPushModal
(
false
);
setFiller
Id
s
([]);
setFiller
User
s
([]);
setAuditor
Id
s
([]);
setAuditor
User
s
([]);
setSelectedIds
([]);
setSelectedIds
([]);
await
reloadList
();
await
reloadList
();
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
...
@@ -464,29 +655,6 @@ export default function ProblemRectificationListPage() {
...
@@ -464,29 +655,6 @@ export default function ProblemRectificationListPage() {
return
(
return
(
<
div
className=
"space-y-6"
>
<
div
className=
"space-y-6"
>
<
section
className=
"rounded-[34px] border border-white/80 bg-[linear-gradient(135deg,rgba(255,255,255,0.96),rgba(255,241,242,0.94))] px-6 py-6 shadow-[0_24px_60px_rgba(15,23,42,0.06)]"
>
<
div
className=
"flex flex-wrap items-end justify-between gap-5"
>
<
div
className=
"max-w-3xl"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.24em] text-rose-500"
>
Problem Rectification Records
</
div
>
<
div
className=
"mt-2 text-3xl font-semibold tracking-tight text-slate-900"
>
问题整改主列表
</
div
>
</
div
>
<
div
className=
"flex flex-wrap gap-2"
>
<
button
type=
"button"
onClick=
{
()
=>
setShowCreateModal
(
true
)
}
className=
"rounded-full bg-[linear-gradient(135deg,#7f1d1d,#be123c)] px-5 py-3 text-sm font-medium text-white shadow-lg transition hover:brightness-105"
>
新增记录
</
button
>
</
div
>
</
div
>
</
section
>
<
section
className=
"grid gap-4 md:grid-cols-3"
>
<
section
className=
"grid gap-4 md:grid-cols-3"
>
<
SummaryCard
<
SummaryCard
label=
"当前分组问题数"
label=
"当前分组问题数"
...
@@ -634,6 +802,13 @@ export default function ProblemRectificationListPage() {
...
@@ -634,6 +802,13 @@ export default function ProblemRectificationListPage() {
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"flex flex-wrap gap-2"
>
<
div
className=
"flex flex-wrap gap-2"
>
<
button
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
type=
"button"
type=
"button"
disabled=
{
selectedIds
.
length
===
0
||
actionLoading
}
disabled=
{
selectedIds
.
length
===
0
||
actionLoading
}
...
@@ -814,215 +989,124 @@ export default function ProblemRectificationListPage() {
...
@@ -814,215 +989,124 @@ export default function ProblemRectificationListPage() {
)
}
)
}
</
section
>
</
section
>
{
showCreateModal
?
(
<
Dialog
open=
{
showCreateModal
}
onClose=
{
handleCloseCreateModal
}
size=
"5xl"
>
<
ModalCard
<
DialogTitle
>
新增问题整改记录
</
DialogTitle
>
title=
"新增问题整改记录"
<
DialogDescription
>
footer=
{
表单字段已按
<
code
className=
"rounded bg-zinc-100 px-1.5 py-0.5 text-xs"
>
problem_rectification
</
code
>
{
' '
}
<
div
className=
"flex justify-end gap-3"
>
对象定义渲染,`select` 选项直接读取对象配置,日期字段会在提交时自动转成 ISO 8601。
<
button
</
DialogDescription
>
type=
"button"
<
DialogBody
>
onClick=
{
()
=>
{
<
div
className=
"rounded-[24px] border border-zinc-100 bg-zinc-50/50 px-4 py-3 text-sm text-slate-500"
>
setShowCreateModal
(
false
);
至少填写“检查项目名称”或“问题简述”之一。长文本字段已展开为更适合录入的输入区域。
setFormValues
(
INITIAL_FORM_VALUES
);
}
}
className=
"rounded-2xl border border-zinc-200 px-4 py-2 text-sm font-medium text-slate-600"
>
取消
</
button
>
<
button
type=
"button"
disabled=
{
actionLoading
}
onClick=
{
handleCreate
}
className=
"rounded-2xl bg-[linear-gradient(135deg,#7f1d1d,#be123c)] px-5 py-2 text-sm font-medium text-white shadow-lg disabled:opacity-60"
>
{
actionLoading
?
'提交中...'
:
'确认新增'
}
</
button
>
</
div
>
}
>
<
div
className=
"grid gap-4 md:grid-cols-2"
>
{
[
[
'检查项目名称'
,
'projectName'
,
'请输入检查项目名称'
],
[
'检查实施单位'
,
'inspectUnit'
,
'请输入检查实施单位'
],
[
'检查实施时间'
,
'implementationTime'
,
'例如 2026年第一季度'
],
[
'检查报告出具日'
,
'inspectionReportDate'
,
'例如 2026-04-30'
],
[
'整改进度'
,
'progress'
,
'例如 整改中 / 已整改'
],
[
'问题来源'
,
'problemFrom'
,
'请输入问题来源'
],
[
'问题类型'
,
'problemType'
,
'请输入问题类型'
],
[
'反馈单位'
,
'feedbackUnit'
,
'请输入反馈单位'
],
[
'问题涉及业务领域'
,
'relatedIssueDomain'
,
'请输入问题涉及业务领域'
],
[
'预计整改完成时间'
,
'dueDate'
,
'例如 2026-12-31T00:00:00.000Z'
],
].
map
(([
label
,
field
,
placeholder
])
=>
(
<
div
key=
{
field
}
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
{
label
}
</
label
>
<
input
value=
{
formValues
[
field
]
}
onChange=
{
(
event
)
=>
setFormValues
((
current
)
=>
({
...
current
,
[
field
]:
event
.
target
.
value
,
}))
}
placeholder=
{
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
className=
"md:col-span-2"
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
责任部门
</
label
>
<
input
value=
{
formValues
.
responsibleDepartmentsText
}
onChange=
{
(
event
)
=>
setFormValues
((
current
)
=>
({
...
current
,
responsibleDepartmentsText
:
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
className=
"md:col-span-2"
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
问题简述
</
label
>
<
textarea
rows=
{
3
}
value=
{
formValues
.
brief
}
onChange=
{
(
event
)
=>
setFormValues
((
current
)
=>
({
...
current
,
brief
:
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
className=
"md:col-span-2"
>
<
div
className=
"mt-6 grid gap-5 md:grid-cols-2"
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
{
createFields
.
map
((
fieldDef
)
=>
{
问题发现情况
const
isLongText
=
fieldDef
.
type
===
'long_text'
;
</
label
>
const
isSingleColumn
=
!
isLongText
&&
fieldDef
.
name
!==
'responsible_department'
;
<
textarea
rows=
{
4
}
value=
{
formValues
.
detail
}
onChange=
{
(
event
)
=>
setFormValues
((
current
)
=>
({
...
current
,
detail
:
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
className=
"md:col-span-2"
>
return
(
<
div
key=
{
fieldDef
.
name
}
className=
{
clsx
(
'rounded-[24px] border border-zinc-200 bg-white/90 p-4 shadow-sm'
,
isSingleColumn
?
''
:
'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"
>
整改情况
{
fieldDef
.
label
||
fieldDef
.
name
}
</
label
>
</
label
>
<
textarea
<
CreateField
rows=
{
3
}
fieldDef=
{
fieldDef
}
value=
{
formValues
.
rectificationSituation
}
value=
{
formValues
[
fieldDef
.
name
]
}
onChange=
{
(
event
)
=>
onChange=
{
(
nextValue
)
=>
setFormValues
((
current
)
=>
({
setFormValues
((
current
)
=>
({
...
current
,
...
current
,
rectificationSituation
:
event
.
target
.
v
alue
,
[
fieldDef
.
name
]:
nextV
alue
,
}))
}))
}
}
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"
/>
/>
{
FIELD_HINTS
[
fieldDef
.
name
]
?
(
<
div
className=
"mt-2 text-xs leading-5 text-slate-500"
>
{
FIELD_HINTS
[
fieldDef
.
name
]
}
</
div
>
</
div
>
)
:
null
}
<
div
className=
"md:col-span-2"
>
<
label
className=
"mb-2 block text-sm font-medium text-slate-700"
>
备注
</
label
>
<
textarea
rows=
{
2
}
value=
{
formValues
.
remark
}
onChange=
{
(
event
)
=>
setFormValues
((
current
)
=>
({
...
current
,
remark
:
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
>
</
ModalCard
>
</
DialogBody
>
)
:
null
}
<
DialogActions
>
<
Button
outline
onClick=
{
handleCloseCreateModal
}
>
取消
</
Button
>
<
Button
color=
"blue"
disabled=
{
actionLoading
||
!
createSubject
}
onClick=
{
handleCreate
}
>
{
actionLoading
?
'提交中...'
:
'确认新增'
}
</
Button
>
</
DialogActions
>
</
Dialog
>
{
showPushModal
?
(
{
showPushModal
?
(
<
ModalCard
<
Dialog
title=
"推送问题整改表"
open=
{
showPushModal
}
description=
{
`已选择 ${selectedRecords.length} 条记录。`
}
onClose=
{
()
=>
{
footer=
{
<
div
className=
"flex justify-end gap-3"
>
<
button
type=
"button"
onClick=
{
()
=>
{
setShowPushModal
(
false
);
setShowPushModal
(
false
);
setFillerId
s
([]);
setFillerUser
s
([]);
setAuditorId
s
([]);
setAuditorUser
s
([]);
}
}
}
}
className=
"rounded-2xl border border-zinc-200 px-4 py-2 text-sm font-medium text-slate-600"
size=
"4xl"
>
取消
</
button
>
<
button
type=
"button"
disabled=
{
actionLoading
}
onClick=
{
handlePush
}
className=
"rounded-2xl bg-[linear-gradient(135deg,#7f1d1d,#be123c)] px-5 py-2 text-sm font-medium text-white shadow-lg disabled:opacity-60"
>
{
actionLoading
?
'推送中...'
:
'确认推送'
}
</
button
>
</
div
>
}
>
>
<
DialogTitle
>
推送问题整改表
</
DialogTitle
>
<
DialogDescription
>
已选择
{
selectedRecords
.
length
}
条记录。请通过远程搜索选择填报人和复核人。
</
DialogDescription
>
<
DialogBody
>
<
div
className=
"grid gap-6 md:grid-cols-2"
>
<
div
className=
"grid gap-6 md:grid-cols-2"
>
<
UserMulti
Select
<
RemoteUser
Select
label=
"填报人"
label=
"填报人"
value=
{
fillerIds
}
value=
{
fillerUsers
}
onChange=
{
setFillerIds
}
onChange=
{
setFillerUsers
}
options=
{
userOptions
}
onSearch=
{
handleSearchUsers
}
onSearch=
{
handleSearchUsers
}
placeholder=
"搜索填报人用户名或显示名"
placeholder=
"搜索填报人用户名或显示名"
/>
/>
<
UserMulti
Select
<
RemoteUser
Select
label=
"复核人"
label=
"复核人"
value=
{
auditorIds
}
value=
{
auditorUsers
}
onChange=
{
setAuditorIds
}
onChange=
{
setAuditorUsers
}
options=
{
userOptions
}
onSearch=
{
handleSearchUsers
}
onSearch=
{
handleSearchUsers
}
placeholder=
"搜索复核人用户名或显示名"
placeholder=
"搜索复核人用户名或显示名"
/>
/>
</
div
>
</
div
>
<
div
className=
"mt-6 rounded-2xl border border-zinc-200 bg-zinc-5
0 p-4"
>
<
div
className=
"mt-6 rounded-[24px] border border-zinc-200 bg-zinc-50/8
0 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"
>
{
selectedRecords
.
map
((
record
)
=>
(
{
selectedRecords
.
map
((
record
)
=>
(
<
span
<
span
key=
{
record
.
id
}
key=
{
record
.
id
}
className=
"rounded-full bg-white px-3 py-1 text-xs font-medium text-slate-700 shadow-sm
"
className=
"rounded-full bg-white px-3 py-1 text-xs font-medium text-slate-700 shadow-sm ring-1 ring-zinc-200
"
>
>
{
record
.
projectName
}
{
record
.
projectName
}
</
span
>
</
span
>
))
}
))
}
</
div
>
</
div
>
</
div
>
</
div
>
</
ModalCard
>
</
DialogBody
>
<
DialogActions
>
<
Button
outline
onClick=
{
()
=>
{
setShowPushModal
(
false
);
setFillerUsers
([]);
setAuditorUsers
([]);
}
}
>
取消
</
Button
>
<
Button
color=
"blue"
disabled=
{
actionLoading
}
onClick=
{
handlePush
}
>
{
actionLoading
?
'推送中...'
:
'确认推送'
}
</
Button
>
</
DialogActions
>
</
Dialog
>
)
:
null
}
)
:
null
}
</
div
>
</
div
>
);
);
...
...
src/pages/problem_rectification/problem-rectification.business-flow.md
View file @
8bb072d7
...
@@ -225,8 +225,12 @@
...
@@ -225,8 +225,12 @@
-
`auditor`
-
`auditor`
-
`restriction_key = "_id"`
-
`restriction_key = "_id"`
-
`restriction_value = 选中问题记录 ID 列表`
-
`restriction_value = 选中问题记录 ID 列表`
-
`reference_record_id = 选中问题记录 ID 列表`
-
`subject_name = "problem_rectification"`
-
`subject_name = "problem_rectification"`
-
`view_name = 当前筛选视图名`
-
`view_name = 当前筛选视图名`
-
`editor_status = "未完成"`
-
`auditor_status = "未完成"`
-
`sender_status = "未完成"`
5.
批量更新选中问题的
`metadata.fill_in_the_people`
5.
批量更新选中问题的
`metadata.fill_in_the_people`
6.
发送内部通知与 OA 待办
6.
发送内部通知与 OA 待办
...
@@ -271,11 +275,12 @@
...
@@ -271,11 +275,12 @@
-
批量更新所有关联问题
`metadata.audit_status = "已审批"`
-
批量更新所有关联问题
`metadata.audit_status = "已审批"`
-
回写
`metadata.review_people`
-
回写
`metadata.review_people`
-
创建最终审核待办给发起人
-
创建最终审核待办给发起人
-
更新流程主单
`auditor_status = "已完成"`
、
`sender_status = "未
审批
"`
-
更新流程主单
`auditor_status = "已完成"`
、
`sender_status = "未
完成
"`
2.
驳回
2.
驳回
-
批量更新所有关联问题
`metadata.audit_status = "已驳回"`
-
批量更新所有关联问题
`metadata.audit_status = "已驳回"`
-
重新给填报人发通知和待办
-
重新给填报人发通知和待办
-
更新流程主单
`editor_status = "未完成"`
、
`auditor_status = "已完成"`
-
更新流程主单
`editor_status = "未完成"`
、
`auditor_status = "已完成"`
-
重新写入
`progress_id_editor`
,使流程回到填写阶段
推荐新交互:
推荐新交互:
...
@@ -294,6 +299,7 @@
...
@@ -294,6 +299,7 @@
-
批量更新所有关联问题
`metadata.final_audit_status = "已驳回"`
-
批量更新所有关联问题
`metadata.final_audit_status = "已驳回"`
-
重建填写人待办
-
重建填写人待办
-
更新流程主单
`editor_status = "未完成"`
、
`sender_status = "已完成"`
-
更新流程主单
`editor_status = "未完成"`
、
`sender_status = "已完成"`
-
重新写入
`progress_id_editor`
,使流程重新回到填写阶段
推荐新交互:
推荐新交互:
...
@@ -323,13 +329,21 @@
...
@@ -323,13 +329,21 @@
### 8.2 流程主单状态字段
### 8.2 流程主单状态字段
`message_reference_id`
中旧代码已使用
:
`message_reference_id`
在当前项目中的约定
:
-
`sender`
记录推送发起人
-
`editor`
记录填写人列表
-
`auditor`
记录审核人列表
-
`restriction_key`
默认固定为
`_id`
-
`restriction_value`
保存本次推送绑定的整改记录
`_id[]`
-
`reference_record_id`
作为关联记录 ID 冗余字段,和
`restriction_value`
一致
-
`subject_name`
保存被推送记录的对象名,当前为
`problem_rectification`
-
`progress_id_editor`
、
`progress_id_auditor`
、
`progress_id_sender`
记录每一步用户确认行为对应的流程 ID
-
`editor_status`
:
`未完成`
、
`已完成`
-
`editor_status`
:
`未完成`
、
`已完成`
-
`auditor_status`
:
`未
审批
`
、
`已完成`
-
`auditor_status`
:
`未
完成
`
、
`已完成`
-
`sender_status`
:
`未
审批`
、
`已完成`
、
`已驳回
`
-
`sender_status`
:
`未
完成`
、
`已完成
`
新项目可以继续兼容这些值,但 UI 层建议用更清晰的步骤条或状态标签展示,而不是直接暴露原字段名
。
当前新页面不再依赖旧版
`未审批`
、
`已驳回`
之类流程主单状态,而是通过“状态字段 + 当前 progress_id”共同判断流程走向。驳回场景通过重建
`progress_id_editor`
并将
`editor_status`
重置为
`未完成`
来回到填写阶段
。
---
---
...
@@ -649,6 +663,7 @@ const result = await queryUsers(0, 20, {
...
@@ -649,6 +663,7 @@ const result = await queryUsers(0, 20, {
-
功能入口
-
功能入口
-
关键交互步骤
-
关键交互步骤
-
推送、填报、复核、最终审核流程
-
推送、填报、复核、最终审核流程
-
`message_reference_id`
的当前字段协议与回流规则
-
驾驶舱统计口径
-
驾驶舱统计口径
-
归档流程
-
归档流程
-
状态变化与数据约束
-
状态变化与数据约束
...
...
src/pages/problem_rectification/problemRectificationFormService.js
0 → 100644
View file @
8bb072d7
import
dayjs
from
'dayjs'
;
import
{
batchUpdateRecords
,
createNotice
,
executeAction
,
loadRecord
,
loadSubject
,
loadUser
,
queryRecords
,
sendNotice
,
updateRecord
,
}
from
'@/services/DataService'
;
import
{
normalizeProblemRectificationRecord
}
from
'./problemRectificationListService'
;
import
{
normalizeText
,
toArray
}
from
'@/utils/subjectView'
;
const
MESSAGE_REFERENCE_SUBJECT
=
'message_reference_id'
;
const
DEFAULT_STATUS
=
'未完成'
;
const
COMPLETED_STATUS
=
'已完成'
;
function
toIdString
(
value
)
{
if
(
!
value
)
{
return
''
;
}
if
(
typeof
value
===
'string'
)
{
return
value
.
trim
();
}
return
String
(
value
.
_id
||
value
.
id
||
value
.
name
||
''
).
trim
();
}
function
normalizeUserIds
(
value
)
{
return
toArray
(
value
)
.
map
((
item
)
=>
toIdString
(
item
))
.
filter
(
Boolean
);
}
function
normalizeUserMap
(
users
=
[])
{
return
users
.
reduce
((
result
,
user
)
=>
{
const
id
=
toIdString
(
user
);
if
(
id
)
{
result
[
id
]
=
user
;
}
return
result
;
},
{});
}
function
normalizeRestrictionValue
(
value
)
{
if
(
Array
.
isArray
(
value
))
{
return
value
.
map
((
item
)
=>
normalizeText
(
item
)).
filter
(
Boolean
);
}
if
(
typeof
value
===
'string'
)
{
return
value
.
split
(
','
)
.
map
((
item
)
=>
normalizeText
(
item
))
.
filter
(
Boolean
);
}
return
[];
}
async
function
loadUsersByIds
(
ids
=
[])
{
const
uniqueIds
=
Array
.
from
(
new
Set
(
ids
.
map
((
item
)
=>
toIdString
(
item
)).
filter
(
Boolean
)));
const
users
=
await
Promise
.
all
(
uniqueIds
.
map
(
async
(
id
)
=>
{
try
{
return
await
loadUser
(
id
);
}
catch
(
error
)
{
return
{
_id
:
id
,
username
:
id
,
display_name
:
id
,
};
}
}),
);
return
users
.
filter
(
Boolean
);
}
function
buildProgressFallback
(
step
)
{
return
`
${
step
}
_
${
Date
.
now
()}
_
${
Math
.
random
().
toString
(
36
).
slice
(
2
,
10
)}
`
;
}
async
function
createProgressTasks
({
recipients
=
[],
title
,
currentUser
,
returnUrl
,
})
{
const
tasks
=
[];
for
(
const
recipient
of
recipients
)
{
try
{
const
result
=
await
executeAction
(
'oa_notice:createProgress'
,
{
title
,
classify
:
'流程待办'
,
contentType
:
'流程待办'
,
receiveUserId
:
recipient
.
_id
,
receiveUserName
:
recipient
.
username
,
receiveDisplayName
:
recipient
.
display_name
,
sendUserId
:
currentUser
?.
_id
||
''
,
sendUserName
:
currentUser
?.
username
||
''
,
sendDisplayName
:
currentUser
?.
display_name
||
''
,
returnUrl
,
});
tasks
.
push
(
result
?.
data
?.
taskId
||
buildProgressFallback
(
'progress'
));
}
catch
(
error
)
{
tasks
.
push
(
buildProgressFallback
(
'progress'
));
}
}
return
tasks
;
}
async
function
completeProgressTasks
(
progressIds
=
[])
{
for
(
const
progressId
of
toArray
(
progressIds
).
map
((
item
)
=>
normalizeText
(
item
)).
filter
(
Boolean
))
{
try
{
await
executeAction
(
'oa_notice:updateProgressStatus'
,
{
taskId
:
progressId
,
});
}
catch
(
error
)
{
// OA 更新失败时不阻断主流程,保留流程主单状态作为最终依据。
}
}
}
async
function
sendFlowNotice
({
title
,
content
,
fromUserId
,
toUserIds
=
[]
})
{
const
receivers
=
Array
.
from
(
new
Set
(
toUserIds
.
map
((
item
)
=>
toIdString
(
item
)).
filter
(
Boolean
)));
if
(
receivers
.
length
===
0
)
{
return
null
;
}
const
notice
=
await
createNotice
({
title
,
content
,
type
:
'internal'
,
from
:
fromUserId
||
''
,
to
:
receivers
,
settings
:
{},
});
await
sendNotice
(
notice
.
name
);
return
notice
;
}
function
buildReturnUrl
(
messageName
)
{
if
(
typeof
window
===
'undefined'
)
{
return
`/problem_rectification_form?id=
${
messageName
}
`
;
}
return
`
${
window
.
location
.
origin
}
/problem_rectification_form?id=
${
messageName
}
`
;
}
function
resolveCurrentStep
(
messageRecord
)
{
const
metadata
=
messageRecord
?.
metadata
||
{};
const
editorStatus
=
metadata
.
editor_status
||
DEFAULT_STATUS
;
const
auditorStatus
=
metadata
.
auditor_status
||
DEFAULT_STATUS
;
const
senderStatus
=
metadata
.
sender_status
||
DEFAULT_STATUS
;
if
(
editorStatus
!==
COMPLETED_STATUS
)
{
return
'editor'
;
}
if
(
auditorStatus
!==
COMPLETED_STATUS
)
{
return
'auditor'
;
}
if
(
senderStatus
!==
COMPLETED_STATUS
)
{
return
'sender'
;
}
return
'completed'
;
}
function
resolveViewerRole
(
messageRecord
,
currentUserId
)
{
if
(
!
currentUserId
)
{
return
'guest'
;
}
const
metadata
=
messageRecord
?.
metadata
||
{};
const
senderId
=
toIdString
(
metadata
.
sender
);
const
editorIds
=
normalizeUserIds
(
metadata
.
editor
);
const
auditorIds
=
normalizeUserIds
(
metadata
.
auditor
);
if
(
senderId
&&
senderId
===
currentUserId
)
{
return
'sender'
;
}
if
(
editorIds
.
includes
(
currentUserId
))
{
return
'editor'
;
}
if
(
auditorIds
.
includes
(
currentUserId
))
{
return
'auditor'
;
}
return
'guest'
;
}
export
async
function
loadProblemRectificationFlow
(
messageName
,
currentUser
)
{
const
messageRecord
=
await
loadRecord
(
messageName
);
const
metadata
=
messageRecord
?.
metadata
||
{};
const
subjectName
=
metadata
.
subject_name
||
'problem_rectification'
;
const
restrictionValue
=
normalizeRestrictionValue
(
metadata
.
restriction_value
);
const
[
subject
,
relatedResult
,
senderUsers
,
editorUsers
,
auditorUsers
]
=
await
Promise
.
all
([
loadSubject
(
subjectName
),
restrictionValue
.
length
?
queryRecords
({
subject
:
subjectName
,
page
:
0
,
page_size
:
1000
,
sort
:
'-updated_at -created_at'
,
mode
:
'full'
,
filter
:
{
_id
:
{
$in
:
restrictionValue
},
},
})
:
Promise
.
resolve
({
records
:
[]
}),
loadUsersByIds
([
metadata
.
sender
]),
loadUsersByIds
(
normalizeUserIds
(
metadata
.
editor
)),
loadUsersByIds
(
normalizeUserIds
(
metadata
.
auditor
)),
]);
const
sender
=
senderUsers
[
0
]
||
null
;
const
editorList
=
editorUsers
||
[];
const
auditorList
=
auditorUsers
||
[];
const
records
=
(
relatedResult
?.
records
||
[]).
map
((
record
)
=>
normalizeProblemRectificationRecord
(
record
));
const
currentUserId
=
toIdString
(
currentUser
?.
_id
);
const
currentStep
=
resolveCurrentStep
(
messageRecord
);
const
viewerRole
=
resolveViewerRole
(
messageRecord
,
currentUserId
);
return
{
messageRecord
,
subject
,
relatedRecords
:
records
,
participants
:
{
sender
,
editors
:
editorList
,
auditors
:
auditorList
,
senderMap
:
normalizeUserMap
(
sender
?
[
sender
]
:
[]),
editorMap
:
normalizeUserMap
(
editorList
),
auditorMap
:
normalizeUserMap
(
auditorList
),
},
currentStep
,
viewerRole
,
canEditRecords
:
currentStep
===
'editor'
&&
(
viewerRole
===
'editor'
||
viewerRole
===
'sender'
),
};
}
export
async
function
refreshRelatedProblemRecords
(
messageRecord
)
{
const
metadata
=
messageRecord
?.
metadata
||
{};
const
subjectName
=
metadata
.
subject_name
||
'problem_rectification'
;
const
restrictionValue
=
normalizeRestrictionValue
(
metadata
.
restriction_value
);
if
(
restrictionValue
.
length
===
0
)
{
return
[];
}
const
result
=
await
queryRecords
({
subject
:
subjectName
,
page
:
0
,
page_size
:
1000
,
sort
:
'-updated_at -created_at'
,
mode
:
'full'
,
filter
:
{
_id
:
{
$in
:
restrictionValue
},
},
});
return
(
result
?.
records
||
[]).
map
((
record
)
=>
normalizeProblemRectificationRecord
(
record
));
}
export
async
function
markEditorComplete
({
messageRecord
,
currentUser
,
auditors
,
})
{
const
metadata
=
messageRecord
?.
metadata
||
{};
const
messageName
=
messageRecord
?.
name
||
messageRecord
?.
_id
;
const
returnUrl
=
buildReturnUrl
(
messageName
);
const
auditorProgressIds
=
await
createProgressTasks
({
recipients
:
auditors
,
title
:
`
${
dayjs
().
format
(
'YYYY-MM-DD'
)}
问题整改表复核`
,
currentUser
,
returnUrl
,
});
await
completeProgressTasks
(
metadata
.
progress_id_editor
);
await
updateRecord
(
messageRecord
.
_id
||
messageRecord
.
name
,
{
metadata
:
{
progress_id_auditor
:
auditorProgressIds
,
editor_status
:
COMPLETED_STATUS
,
auditor_status
:
DEFAULT_STATUS
,
sender_status
:
metadata
.
sender_status
||
DEFAULT_STATUS
,
},
});
await
batchUpdateRecords
({
subject
:
metadata
.
subject_name
,
filter
:
{
_id
:
{
$in
:
normalizeRestrictionValue
(
metadata
.
restriction_value
)
},
},
data
:
{
'metadata.review_people'
:
auditors
.
map
((
item
)
=>
item
.
_id
),
},
});
await
sendFlowNotice
({
title
:
'问题整改表待复核'
,
content
:
`请点击链接复核问题整改表:
${
returnUrl
}
`
,
fromUserId
:
currentUser
?.
_id
,
toUserIds
:
auditors
.
map
((
item
)
=>
item
.
_id
),
});
}
export
async
function
approveByAuditor
({
messageRecord
,
currentUser
,
sender
,
})
{
const
metadata
=
messageRecord
?.
metadata
||
{};
const
messageName
=
messageRecord
?.
name
||
messageRecord
?.
_id
;
const
returnUrl
=
buildReturnUrl
(
messageName
);
const
senderProgressIds
=
await
createProgressTasks
({
recipients
:
sender
?
[
sender
]
:
[],
title
:
`
${
dayjs
().
format
(
'YYYY-MM-DD'
)}
问题整改表最终审核`
,
currentUser
,
returnUrl
,
});
await
batchUpdateRecords
({
subject
:
metadata
.
subject_name
,
filter
:
{
_id
:
{
$in
:
normalizeRestrictionValue
(
metadata
.
restriction_value
)
},
},
data
:
{
'metadata.review_people'
:
[
currentUser
?.
_id
].
filter
(
Boolean
),
'metadata.audit_status'
:
'已审批'
,
},
});
await
completeProgressTasks
(
metadata
.
progress_id_auditor
);
await
updateRecord
(
messageRecord
.
_id
||
messageRecord
.
name
,
{
metadata
:
{
progress_id_sender
:
senderProgressIds
[
0
]
||
buildProgressFallback
(
'sender'
),
auditor_status
:
COMPLETED_STATUS
,
sender_status
:
DEFAULT_STATUS
,
},
});
await
sendFlowNotice
({
title
:
'问题整改表待最终审核'
,
content
:
`问题整改表已完成复核,请点击链接最终审核:
${
returnUrl
}
`
,
fromUserId
:
currentUser
?.
_id
,
toUserIds
:
sender
?
[
sender
.
_id
]
:
[],
});
}
export
async
function
rejectByAuditor
({
messageRecord
,
currentUser
,
editors
,
})
{
const
metadata
=
messageRecord
?.
metadata
||
{};
const
messageName
=
messageRecord
?.
name
||
messageRecord
?.
_id
;
const
returnUrl
=
buildReturnUrl
(
messageName
);
const
editorProgressIds
=
await
createProgressTasks
({
recipients
:
editors
,
title
:
`复核驳回,请重新填写问题整改表
${
dayjs
().
format
(
'YYYY-MM-DD'
)}
`
,
currentUser
,
returnUrl
,
});
await
batchUpdateRecords
({
subject
:
metadata
.
subject_name
,
filter
:
{
_id
:
{
$in
:
normalizeRestrictionValue
(
metadata
.
restriction_value
)
},
},
data
:
{
'metadata.review_people'
:
[
currentUser
?.
_id
].
filter
(
Boolean
),
'metadata.audit_status'
:
'已驳回'
,
},
});
await
completeProgressTasks
(
metadata
.
progress_id_auditor
);
await
updateRecord
(
messageRecord
.
_id
||
messageRecord
.
name
,
{
metadata
:
{
progress_id_editor
:
editorProgressIds
,
editor_status
:
DEFAULT_STATUS
,
auditor_status
:
COMPLETED_STATUS
,
sender_status
:
DEFAULT_STATUS
,
},
});
await
sendFlowNotice
({
title
:
'问题整改表复核驳回'
,
content
:
`问题整改表复核未通过,请点击链接重新填写:
${
returnUrl
}
`
,
fromUserId
:
currentUser
?.
_id
,
toUserIds
:
editors
.
map
((
item
)
=>
item
.
_id
),
});
}
export
async
function
approveBySender
({
messageRecord
,
})
{
const
metadata
=
messageRecord
?.
metadata
||
{};
await
batchUpdateRecords
({
subject
:
metadata
.
subject_name
,
filter
:
{
_id
:
{
$in
:
normalizeRestrictionValue
(
metadata
.
restriction_value
)
},
},
data
:
{
'metadata.final_audit_status'
:
'已审批'
,
},
});
await
completeProgressTasks
(
metadata
.
progress_id_sender
);
await
updateRecord
(
messageRecord
.
_id
||
messageRecord
.
name
,
{
metadata
:
{
sender_status
:
COMPLETED_STATUS
,
},
});
}
export
async
function
rejectBySender
({
messageRecord
,
currentUser
,
editors
,
})
{
const
metadata
=
messageRecord
?.
metadata
||
{};
const
messageName
=
messageRecord
?.
name
||
messageRecord
?.
_id
;
const
returnUrl
=
buildReturnUrl
(
messageName
);
const
editorProgressIds
=
await
createProgressTasks
({
recipients
:
editors
,
title
:
`最终审核驳回,请重新填写问题整改表
${
dayjs
().
format
(
'YYYY-MM-DD'
)}
`
,
currentUser
,
returnUrl
,
});
await
batchUpdateRecords
({
subject
:
metadata
.
subject_name
,
filter
:
{
_id
:
{
$in
:
normalizeRestrictionValue
(
metadata
.
restriction_value
)
},
},
data
:
{
'metadata.final_audit_status'
:
'已驳回'
,
},
});
await
completeProgressTasks
(
metadata
.
progress_id_sender
);
await
updateRecord
(
messageRecord
.
_id
||
messageRecord
.
name
,
{
metadata
:
{
progress_id_editor
:
editorProgressIds
,
editor_status
:
DEFAULT_STATUS
,
auditor_status
:
COMPLETED_STATUS
,
sender_status
:
COMPLETED_STATUS
,
},
});
await
sendFlowNotice
({
title
:
'问题整改表最终审核驳回'
,
content
:
`问题整改表最终审核未通过,请点击链接重新填写:
${
returnUrl
}
`
,
fromUserId
:
currentUser
?.
_id
,
toUserIds
:
editors
.
map
((
item
)
=>
item
.
_id
),
});
}
src/pages/problem_rectification/problemRectificationListService.js
View file @
8bb072d7
...
@@ -4,6 +4,7 @@ import {
...
@@ -4,6 +4,7 @@ import {
loadSubject
,
loadSubject
,
queryRecords
,
queryRecords
,
}
from
'@/services/DataService'
;
}
from
'@/services/DataService'
;
import
{
DEPARTMENT_GROUPS
}
from
'./problemRectificationDashboardService'
;
import
{
import
{
buildViewColumns
,
buildViewColumns
,
emptyValueForField
,
emptyValueForField
,
...
@@ -60,16 +61,25 @@ function formatDateText(value) {
...
@@ -60,16 +61,25 @@ function formatDateText(value) {
export
function
normalizeProblemRectificationRecord
(
record
)
{
export
function
normalizeProblemRectificationRecord
(
record
)
{
const
metadata
=
record
?.
metadata
||
{};
const
metadata
=
record
?.
metadata
||
{};
const
dueDate
=
metadata
.
the_time_of_rectification
?
dayjs
(
metadata
.
the_time_of_rectification
)
:
null
;
return
{
return
{
id
:
record
?.
_id
||
record
?.
name
||
''
,
id
:
record
?.
_id
||
record
?.
name
||
''
,
name
:
record
?.
name
||
''
,
name
:
record
?.
name
||
''
,
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
),
progress
:
normalizeText
(
metadata
.
rectification_progress
),
progress
:
normalizeText
(
metadata
.
rectification_progress
),
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
),
problemType
:
normalizeText
(
metadata
.
problem_type
),
problemFrom
:
normalizeText
(
metadata
.
problem_from
),
relatedIssueDomain
:
normalizeText
(
metadata
.
related_issue_domain
),
brief
:
normalizeText
(
metadata
.
problem_brief
),
dueDate
:
dueDate
&&
dueDate
.
isValid
()
?
dueDate
:
null
,
fillUsers
:
toArray
(
metadata
.
fill_in_the_people
)
fillUsers
:
toArray
(
metadata
.
fill_in_the_people
)
.
map
(
normalizeUserLabel
)
.
map
(
normalizeUserLabel
)
.
filter
(
Boolean
),
.
filter
(
Boolean
),
...
@@ -190,6 +200,90 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
...
@@ -190,6 +200,90 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
};
};
}
}
function
matchesDepartmentGroup
(
record
,
groupKey
)
{
if
(
!
groupKey
||
groupKey
===
'all'
)
{
return
true
;
}
const
matchedGroup
=
DEPARTMENT_GROUPS
.
find
((
group
)
=>
group
.
key
===
groupKey
);
if
(
!
matchedGroup
)
{
return
true
;
}
if
(
!
matchedGroup
.
matchFeedbackUnits
||
matchedGroup
.
matchFeedbackUnits
.
length
===
0
)
{
return
true
;
}
return
matchedGroup
.
matchFeedbackUnits
.
includes
(
record
.
feedbackUnit
);
}
function
buildSearchText
(
record
)
{
return
[
record
.
projectName
,
record
.
inspectUnit
,
record
.
feedbackUnit
,
record
.
progress
,
record
.
auditStatus
,
record
.
finalAuditStatus
,
record
.
problemType
,
record
.
problemFrom
,
record
.
relatedIssueDomain
,
record
.
brief
,
record
.
dueDateLabel
,
record
.
fillUsers
.
join
(
' '
),
record
.
reviewUsers
.
join
(
' '
),
]
.
map
((
value
)
=>
normalizeText
(
value
))
.
filter
(
Boolean
)
.
join
(
' '
)
.
toLowerCase
();
}
export
function
buildProblemRectificationListView
(
records
=
[],
filters
=
{})
{
const
keyword
=
normalizeText
(
filters
.
keyword
).
toLowerCase
();
const
progress
=
normalizeText
(
filters
.
progress
);
const
auditStatus
=
normalizeText
(
filters
.
auditStatus
);
const
groupKey
=
normalizeText
(
filters
.
groupKey
)
||
'all'
;
const
scopedRecords
=
records
.
filter
((
record
)
=>
{
if
(
progress
&&
record
.
progress
!==
progress
)
{
return
false
;
}
if
(
auditStatus
&&
record
.
auditStatus
!==
auditStatus
)
{
return
false
;
}
if
(
keyword
&&
!
buildSearchText
(
record
).
includes
(
keyword
))
{
return
false
;
}
return
true
;
});
const
groupCounts
=
Object
.
fromEntries
(
DEPARTMENT_GROUPS
.
map
((
group
)
=>
[
group
.
key
,
scopedRecords
.
filter
((
record
)
=>
matchesDepartmentGroup
(
record
,
group
.
key
)).
length
,
]),
);
const
filteredRecords
=
scopedRecords
.
filter
((
record
)
=>
matchesDepartmentGroup
(
record
,
groupKey
),
);
return
{
filteredRecords
,
groupCounts
,
progressOptions
:
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
progress
).
filter
(
Boolean
)),
),
auditStatusOptions
:
Array
.
from
(
new
Set
(
records
.
map
((
item
)
=>
item
.
auditStatus
).
filter
(
Boolean
)),
),
};
}
export
function
buildProblemRectificationCreateFormFields
(
gridView
,
subject
)
{
export
function
buildProblemRectificationCreateFormFields
(
gridView
,
subject
)
{
const
columns
=
buildViewColumnsForList
(
gridView
,
subject
)
const
columns
=
buildViewColumnsForList
(
gridView
,
subject
)
.
filter
((
column
)
=>
column
.
field
!==
'title'
)
.
filter
((
column
)
=>
column
.
field
!==
'title'
)
...
...
src/services/subjectsDefinition.md
View file @
8bb072d7
...
@@ -239,25 +239,32 @@ type Record = {
...
@@ -239,25 +239,32 @@ type Record = {
| 字段标签 | 字段别名 | 字段类型 | 是否主键 | 是否多值 | 是否必填 |
| 字段标签 | 字段别名 | 字段类型 | 是否主键 | 是否多值 | 是否必填 |
| --- | --- | --- | --- | --- | --- |
| --- | --- | --- | --- | --- | --- |
| 发起人 |
`sender`
| user | 否 | 否 | 是 |
| 发送人 |
`sender`
| user | 否 | 否 | 否 |
| 填写人 |
`editor`
| user | 否 | 是 | 是 |
| 填写人 |
`editor`
| user | 否 | 是 | 否 |
| 审核人 |
`auditor`
| user | 否 | 是 | 是 |
| 审核人 |
`auditor`
| user | 否 | 是 | 否 |
| 限制字段 |
`restriction_key`
| text | 否 | 否 | 是 |
| 约束 key |
`restriction_key`
| text | 否 | 否 | 否 |
| 限制值列表 |
`restriction_value`
| text | 否 | 是 | 是 |
| 约束值 |
`restriction_value`
| text | 否 | 是 | 否 |
| 对象名 |
`subject_name`
| text | 否 | 否 | 否 |
| 视图名称 |
`view_name`
| text | 否 | 否 | 否 |
| 视图名称 |
`view_name`
| text | 否 | 否 | 否 |
|
主题名称 |
`subject_name`
| text | 否 | 否 | 是
|
|
关联记录 ID |
`reference_record_id`
| text | 否 | 是 | 否
|
| 填写待办 ID |
`progress_id_editor`
| text | 否 | 是 | 否 |
| 填写待办 ID |
`progress_id_editor`
| text | 否 | 是 | 否 |
| 审核待办 ID |
`progress_id_auditor`
| text | 否 | 是 | 否 |
| 审核待办 ID |
`progress_id_auditor`
| text | 否 | 是 | 否 |
| 最终审核待办 ID |
`progress_id_sender`
| text | 否 | 否 | 否 |
| 最终审核待办 ID |
`progress_id_sender`
| text | 否 | 否 | 否 |
| 填写状态 |
`editor_status`
| select | 否 | 否 | 否 |
| 填写状态 |
`editor_status`
| select | 否 | 否 | 否 |
| 审核状态 |
`auditor_status`
| select | 否 | 否 | 否 |
| 审核状态 |
`auditor_status`
| select | 否 | 否 | 否 |
| 发起人状态 |
`sender_status`
| select | 否 | 否 | 否 |
| 发送人状态 |
`sender_status`
| select | 否 | 否 | 否 |
### 3.4.1 旧流程状态值
### 3.4.1 当前流程写入约定
-
`editor_status`
:旧页面使用过
`未完成`
、
`已完成`
-
`restriction_key`
默认写
`_id`
-
`auditor_status`
:旧页面使用过
`未审批`
、
`已完成`
-
`restriction_value`
保存被推送整改记录的
`_id[]`
-
`sender_status`
:旧页面使用过
`未审批`
、
`已完成`
、
`已驳回`
-
`reference_record_id`
与
`restriction_value`
保持一致,作为显式的“关联记录 ID”冗余字段
-
`subject_name`
保存被推送记录所属对象名,当前整改流程固定为
`problem_rectification`
-
`progress_id_editor`
、
`progress_id_auditor`
、
`progress_id_sender`
用于记录每一步用户确认动作生成的流程 ID,用来区分当前流程推进到了哪一步
-
`editor_status`
:当前项目统一使用
`未完成`
、
`已完成`
-
`auditor_status`
:当前项目统一使用
`未完成`
、
`已完成`
-
`sender_status`
:当前项目统一使用
`未完成`
、
`已完成`
-
当复核或最终审核驳回时,流程主单会回写新的
`progress_id_editor`
,并将
`editor_status`
重置为
`未完成`
,从而重新进入填写阶段
## 3.5 项目管理
## 3.5 项目管理
...
...
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