Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
I
integrity_risk_prevention
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
integrity_risk_prevention
Commits
fbbe3f5a
Commit
fbbe3f5a
authored
Sep 08, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: support department item editing and risk workflow
parent
6f750b02
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
909 additions
and
150 deletions
+909
-150
AGENTS.md
AGENTS.md
+17
-16
ui-architecture.md
docs/ui-architecture.md
+1
-1
ui-migration-checklist.md
docs/ui-migration-checklist.md
+1
-1
xinyuan-design-system.md
docs/xinyuan-design-system.md
+1
-1
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+7
-5
DynamicRecordDialog.jsx
src/components/DynamicRecordDialog.jsx
+26
-5
SubjectRecordWorkbench.jsx
src/components/SubjectRecordWorkbench.jsx
+10
-0
index.jsx
src/components/record-workbench/index.jsx
+53
-19
ReferenceRecordsTable.jsx
...s/RecordDetailDrawer/components/ReferenceRecordsTable.jsx
+3
-2
SubjectViewContent.jsx
...ages/RecordDetailDrawer/components/SubjectViewContent.jsx
+2
-2
TailwindEditableField.jsx
...s/RecordDetailDrawer/components/TailwindEditableField.jsx
+14
-1
useSubjectRecord.js
src/pages/RecordDetailDrawer/hooks/useSubjectRecord.js
+5
-2
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+1
-1
ControlMeasureFormPage.jsx
...ages/integrity_risk_prevention/ControlMeasureFormPage.jsx
+60
-0
DepartmentControlItemDialog.jsx
...integrity_risk_prevention/DepartmentControlItemDialog.jsx
+13
-6
DepartmentControlMeasureFlow.jsx
...ntegrity_risk_prevention/DepartmentControlMeasureFlow.jsx
+45
-1
DepartmentPushDialog.jsx
src/pages/integrity_risk_prevention/DepartmentPushDialog.jsx
+1
-1
IntegrityRiskService.js
src/pages/integrity_risk_prevention/IntegrityRiskService.js
+262
-54
IntegrityRiskWorkbenchPage.jsx
.../integrity_risk_prevention/IntegrityRiskWorkbenchPage.jsx
+129
-0
integrity-risk-prevention.business-flow.md
...isk_prevention/integrity-risk-prevention.business-flow.md
+119
-29
DataService.js
src/services/DataService.js
+8
-3
SubjectWorkbenchService.js
src/services/SubjectWorkbenchService.js
+2
-0
subjectView.js
src/utils/subjectView.js
+129
-0
No files found.
AGENTS.md
View file @
fbbe3f5a
...
@@ -21,12 +21,13 @@
...
@@ -21,12 +21,13 @@
-
**常量**
:使用全大写下划线 (SNAKE_CASE)。
-
**常量**
:使用全大写下划线 (SNAKE_CASE)。
-
**技术栈偏好**
:
-
**技术栈偏好**
:
-
优先使用 React Hooks 和函数组件。
-
优先使用 React Hooks 和函数组件。
-
样式处理:
**
优先使用 Tailwind Plus / Catalyst UI Kit(见 `src/catalyst-ui-kit`)+ Tailwind CSS
**
。
-
样式处理:
**
统一使用 HeroUI 与 HeroUI Pro 的主题、Token 和组件样式,配合 Tailwind CSS 做布局
**
。
-
交互组件:
**优先使用 He
adless UI**
(已在 Catalyst 组件内封装,除非缺组件才直接用
`@headlessui/react`
)
。
-
交互组件:
**优先使用 He
roUI / HeroUI Pro 的 compound API**
,由组件自身提供焦点管理、键盘导航、Portal 和可访问性行为
。
-
组件选择优先级(从高到低):
-
组件选择优先级(从高到低):
1.
`src/catalyst-ui-kit/javascript/*`
(Button/Input/Dialog/Table/Combobox/SidebarLayout/...)
1.
`@heroui/react`
(Button/Input/Modal/Autocomplete/Table 等 OSS 组件)
2.
基于 Catalyst 样式规范的自定义组件(仅在 Kit 无现成组件时)
2.
`@heroui-pro/react/*`
(DataGrid/Sidebar/Navbar/DropZone/KPI/EmptyState 等 Pro 组件)
3.
原生 HTML + 自写样式(尽量避免)
3.
基于 HeroUI 语义 Token 的自定义组合组件(仅在 HeroUI/HeroUI Pro 无现成组件时)
4.
原生 HTML + 自写样式(尽量避免)
## 4. 接口请求规范
## 4. 接口请求规范
...
@@ -39,12 +40,12 @@
...
@@ -39,12 +40,12 @@
-
如果
`record-api.md`
或
`subjectsDefinition.md`
中的定义不清晰,请务必询问我,不要随意猜测字段名。
-
如果
`record-api.md`
或
`subjectsDefinition.md`
中的定义不清晰,请务必询问我,不要随意猜测字段名。
-
每次生成代码后,简要说明你引用了哪个 API 和哪些数据对象。
-
每次生成代码后,简要说明你引用了哪个 API 和哪些数据对象。
## 6. UI 规范(
Tailwind Plus / Headless UI
强制约束)
## 6. UI 规范(
HeroUI / HeroUI Pro
强制约束)
-
**全项目 UI 统一**
:默认使用
`
src/catalyst-ui-kit`
中的 Catalyst 组件与样式体系,避免引入其它 UI 框架
。
-
**全项目 UI 统一**
:默认使用
`
@heroui/react`
与
`@heroui-pro/react`
的组件和样式体系,禁止新增旧版 UI 框架或其它平行 UI 框架依赖
。
-
**He
adless UI 优先**
:弹窗/下拉/选择器等交互优先用 Headless UI(直接或通过 Catalyst 组件)
。
-
**He
roUI compound API 优先**
:弹窗、下拉、选择器、标签组等交互直接使用 HeroUI/HeroUI Pro 官方 compound API,不访问内部 slot/BEM 实现页面私有外观
。
-
**一致性**
:表单控件优先
用 Catalyst 的
`Input/Select/Checkbox/Textarea/Fieldset`
,表格用
`Table`
,对话框用
`
Dialog`
。
-
**一致性**
:表单控件优先
使用 HeroUI
`Input/TextArea/Checkbox/DatePicker/Autocomplete`
,表格优先使用 HeroUI Pro
`DataGrid`
,对话框使用 HeroUI
`Modal/Alert
Dialog`
。
-
**可访问性**
:移动端侧栏/弹层必须使用 He
adless UI 的 Dialog 等可访问性组件(focus trap / esc 关闭等)
。
-
**可访问性**
:移动端侧栏/弹层必须使用 He
roUI Pro
`Sheet/Sidebar`
、HeroUI
`Modal`
等组件提供的 focus trap、Esc 关闭、Portal 和键盘导航能力
。
# AI 开发执行规范
# AI 开发执行规范
...
@@ -54,12 +55,12 @@
...
@@ -54,12 +55,12 @@
| 字段类型 (Type) | UI 组件 (Component) | 交互逻辑 / 备注 |
| 字段类型 (Type) | UI 组件 (Component) | 交互逻辑 / 备注 |
| :-- | :-- | :-- |
| :-- | :-- | :-- |
|
**文本 (String)**
|
Catalyst
`Input`
| 标准文本输入 |
|
**文本 (String)**
|
HeroUI
`Input`
| 标准文本输入 |
|
**引用 (Reference)**
|
`ReferenceSearchSelect`
(He
adless UI Combobox
) | 必须通过 API 获取列表,选中后仅存储
`_id`
|
|
**引用 (Reference)**
|
`ReferenceSearchSelect`
(He
roUI
`Autocomplete`
compound API
) | 必须通过 API 获取列表,选中后仅存储
`_id`
|
|
**数字 (Number)**
|
Catalyst
`Input`
(
`type="number"`
) | 仅允许输入数字 |
|
**数字 (Number)**
|
HeroUI
`Input`
(
`type="number"`
) | 仅允许输入数字 |
|
**日期 (Date)**
|
Catalyst
`Input`
(
`type="date"`
/
`datetime-local`
)
| 提交时格式化为
`ISO 8601`
字符串 |
|
**日期 (Date)**
|
HeroUI
`DatePicker`
/
`DateField`
| 提交时格式化为
`ISO 8601`
字符串 |
|
**布尔 (Boolean)**
|
Catalyst
`Checkbox`
| 映射为 true/false |
|
**布尔 (Boolean)**
|
HeroUI
`Checkbox`
| 映射为 true/false |
|
**JSON**
|
Catalyst
`Texta
rea`
| 需要包含 JSON 校验逻辑 |
|
**JSON**
|
HeroUI
`TextA
rea`
| 需要包含 JSON 校验逻辑 |
|
**子对象 (Subject)**
|
**动态表单递归**
|
**核心逻辑:**
必须先调用
`loadSubject(subjectName)`
接口读取该子对象的字段定义,然后递归应用本映射表生成子表单。 |
|
**子对象 (Subject)**
|
**动态表单递归**
|
**核心逻辑:**
必须先调用
`loadSubject(subjectName)`
接口读取该子对象的字段定义,然后递归应用本映射表生成子表单。 |
---
---
...
...
docs/ui-architecture.md
View file @
fbbe3f5a
...
@@ -4,7 +4,7 @@
...
@@ -4,7 +4,7 @@
1.
业务控制层:现有 services、hooks、路由、权限、查询参数、分页口径和保存回调保持不变。
1.
业务控制层:现有 services、hooks、路由、权限、查询参数、分页口径和保存回调保持不变。
2.
页面编排层:页面只组合业务数据与 HeroUI 原生组件,
`className`
仅用于布局。
2.
页面编排层:页面只组合业务数据与 HeroUI 原生组件,
`className`
仅用于布局。
3.
共享组合层:
`AppShell`
、
`DataTable`
、
`XinyuanAutocomplete`
、
`XinyuanModal`
等均是 HeroUI-native 的业务无关组合,不兼容
旧 Catalyst
props。
3.
共享组合层:
`AppShell`
、
`DataTable`
、
`XinyuanAutocomplete`
、
`XinyuanModal`
等均是 HeroUI-native 的业务无关组合,不兼容
已废弃的旧 UI 适配层
props。
4.
主题层:HeroUI styles -> HeroUI Pro CSS -> 权威
`theme.css`
-> 批准的
`tokens.css`
-> Shell 布局样式。
4.
主题层:HeroUI styles -> HeroUI Pro CSS -> 权威
`theme.css`
-> 批准的
`tokens.css`
-> Shell 布局样式。
## 依赖边界
## 依赖边界
...
...
docs/ui-migration-checklist.md
View file @
fbbe3f5a
...
@@ -20,7 +20,7 @@
...
@@ -20,7 +20,7 @@
-
[
x
]
弹窗查看态为单一 L2 字段组且字段无框,编辑态移除字段组 Surface;只读字段不使用禁用输入框模拟。
-
[
x
]
弹窗查看态为单一 L2 字段组且字段无框,编辑态移除字段组 Surface;只读字段不使用禁用输入框模拟。
-
[
x
]
所有动态表单使用 HeroUI compound 控件。
-
[
x
]
所有动态表单使用 HeroUI compound 控件。
-
[
x
]
所有上传使用 HeroUI Pro DropZone。
-
[
x
]
所有上传使用 HeroUI Pro DropZone。
-
[
x
]
活动
Catalyst/Headless UI 引用为零
。
-
[
x
]
活动
业务源码统一使用 HeroUI/HeroUI Pro,未引入旧版 UI 框架或其它平行 UI 框架
。
-
[
x
]
活动原生 select/date/file 控件为零(DropZone 内部 input 为不可见技术输入)。
-
[
x
]
活动原生 select/date/file 控件为零(DropZone 内部 input 为不可见技术输入)。
-
[
x
]
页面私有视觉 CSS 扫描为零;页面 className 仅承担布局、间距和响应式。
-
[
x
]
页面私有视觉 CSS 扫描为零;页面 className 仅承担布局、间距和响应式。
-
[
x
]
全局复扫移除业务 JSX 的 Surface/KPI/DataGrid 圆角覆盖与旧 Disclosure BEM class。
-
[
x
]
全局复扫移除业务 JSX 的 Surface/KPI/DataGrid 圆角覆盖与旧 Disclosure BEM class。
...
...
docs/xinyuan-design-system.md
View file @
fbbe3f5a
...
@@ -13,7 +13,7 @@
...
@@ -13,7 +13,7 @@
-
页面
`className`
只处理布局、宽度、间距、对齐和响应式。
-
页面
`className`
只处理布局、宽度、间距、对齐和响应式。
-
组件外观由 HeroUI variant/size/compound API 与权威主题负责。
-
组件外观由 HeroUI variant/size/compound API 与权威主题负责。
-
不引入
Catalyst、Headless UI
、兼容旧 props 的 HeroUI 包装层或可见原生选择/日期/文件控件。
-
不引入
旧版 UI 框架
、兼容旧 props 的 HeroUI 包装层或可见原生选择/日期/文件控件。
-
列表使用 HeroUI Pro DataGrid、ActionBar、EmptyState 和 HeroUI Pagination。
-
列表使用 HeroUI Pro DataGrid、ActionBar、EmptyState 和 HeroUI Pagination。
-
筛选使用 HeroUI Surface、SearchField、Autocomplete、DatePicker 和 Disclosure。
-
筛选使用 HeroUI Surface、SearchField、Autocomplete、DatePicker 和 Disclosure。
-
受控 Autocomplete 的已选值必须可逆操作:单选使用官方
`Autocomplete.ClearButton`
清除当前值;多选在 Trigger 内使用官方
`TagGroup + Tag + Tag.RemoveButton`
将每个值显示为独立 Tag 并支持单项删除,同时保留官方清除入口清空集合;下拉中的官方
`ListBox.Item`
再次点选也可移除单项。Tag、清除按钮和选中态都从同一受控
`selectedKeys`
派生,不得合并为逗号文本、使用页面自制标签或维护第二套选择状态。已选 TagGroup 使用 HeroUI
`default`
variant,Tag 使用官方
`accent-soft-hover`
/
`accent-soft-foreground`
语义 Token,继承鑫元品牌辅助色并与字段背景拉开层级,避免纯白
`surface`
。
-
受控 Autocomplete 的已选值必须可逆操作:单选使用官方
`Autocomplete.ClearButton`
清除当前值;多选在 Trigger 内使用官方
`TagGroup + Tag + Tag.RemoveButton`
将每个值显示为独立 Tag 并支持单项删除,同时保留官方清除入口清空集合;下拉中的官方
`ListBox.Item`
再次点选也可移除单项。Tag、清除按钮和选中态都从同一受控
`selectedKeys`
派生,不得合并为逗号文本、使用页面自制标签或维护第二套选择状态。已选 TagGroup 使用 HeroUI
`default`
variant,Tag 使用官方
`accent-soft-hover`
/
`accent-soft-foreground`
语义 Token,继承鑫元品牌辅助色并与字段背景拉开层级,避免纯白
`surface`
。
...
...
docs/xinyuan-ui-audit.md
View file @
fbbe3f5a
...
@@ -30,11 +30,13 @@
...
@@ -30,11 +30,13 @@
|
`/integrity_risk_control_archive`
| 历史归档 | 只读行为不变 | 已验收 |
|
`/integrity_risk_control_archive`
| 历史归档 | 只读行为不变 | 已验收 |
|
`/control_measure_form`
| 部门填报与审核 | 保留流程状态、驳回与审核回调 | 已验收 |
|
`/control_measure_form`
| 部门填报与审核 | 保留流程状态、驳回与审核回调 | 已验收 |
##
旧 UI 清单
##
迁移前旧 UI 清单(历史基线)
-
Shell:Catalyst Sidebar/Navbar/Avatar/Button。
以下内容仅记录 HeroUI/HeroUI Pro 迁移前的实现状态,用于审计对照,不代表当前技术栈或新增代码规范。
-
页面:Catalyst Button、Dialog、Table、Checkbox、Input、Select、Textarea。
-
Portal:Headless UI Dialog、Combobox、Listbox。
-
Shell:旧版 Sidebar/Navbar/Avatar/Button 组件。
-
页面:旧版 Button、Dialog、Table、Checkbox、Input、Select、Textarea 组件。
-
Portal:旧版 Dialog、Combobox、Listbox 组件。
-
列表:自制 Table、选择列、分页、空状态和工具栏。
-
列表:自制 Table、选择列、分页、空状态和工具栏。
-
图表:未发现活动业务图表、ECharts 私有主题或自制 Tooltip;源码没有
`PieChart`
/
`BarChart`
/
`ComposedChart`
/
`ChartTooltip`
活动页面。
`recharts`
仍是原始依赖并保留在锁文件中,但没有活动 import,不制造测试数据。
-
图表:未发现活动业务图表、ECharts 私有主题或自制 Tooltip;源码没有
`PieChart`
/
`BarChart`
/
`ComposedChart`
/
`ChartTooltip`
活动页面。
`recharts`
仍是原始依赖并保留在锁文件中,但没有活动 import,不制造测试数据。
-
饼图 / 环形图专项审计:当前仍无真实 Pie/Donut 调用点,未新增伪造图表数据或页面。已新增
`--xinyuan-pie-chart-*`
/
`--rc-pie-chart-*`
专用 Token、稳定分类 key 冲突消解映射和
`applyXinyuanPieChartPalette`
共享适配器;未来接入时不得由页面直接传色或改动普通图表色板。
-
饼图 / 环形图专项审计:当前仍无真实 Pie/Donut 调用点,未新增伪造图表数据或页面。已新增
`--xinyuan-pie-chart-*`
/
`--rc-pie-chart-*`
专用 Token、稳定分类 key 冲突消解映射和
`applyXinyuanPieChartPalette`
共享适配器;未来接入时不得由页面直接传色或改动普通图表色板。
...
@@ -180,4 +182,4 @@
...
@@ -180,4 +182,4 @@
-
`shortTitle`
:现有
`buildViewColumns`
未按严格布尔值隐藏
`title`
,详情 Header 和字段区也未跟随当前激活 Detail View 隐藏记录标题;关键词搜索目前仅使用可见列,隐藏标题后会丢失标题检索语义。
-
`shortTitle`
:现有
`buildViewColumns`
未按严格布尔值隐藏
`title`
,详情 Header 和字段区也未跟随当前激活 Detail View 隐藏记录标题;关键词搜索目前仅使用可见列,隐藏标题后会丢失标题检索语义。
-
通知:目标 DataService 已保留平台
`queryUserNotice`
、
`readUserNotice`
、
`unReadNoticeCount`
和动态 action 接口,但目标应用尚无
`/notifications`
路由、通知页面/service 或 Navbar Badge;Sidebar 当前没有通知入口,符合要求。
-
通知:目标 DataService 已保留平台
`queryUserNotice`
、
`readUserNotice`
、
`unReadNoticeCount`
和动态 action 接口,但目标应用尚无
`/notifications`
路由、通知页面/service 或 Navbar Badge;Sidebar 当前没有通知入口,符合要求。
-
文件字段:运行时 Subject 协议支持
`file`
单值对象和
`multiple=true`
数组;现有详情仅直接上传业务附件。目标详情曾残留问题整改材料服务引用,必须改为廉洁风险防控自己的
`source_app`
,并保持
`{name,type,size,url}`
快照协议、延迟建档和重试去重语义。
-
文件字段:运行时 Subject 协议支持
`file`
单值对象和
`multiple=true`
数组;现有详情仅直接上传业务附件。目标详情曾残留问题整改材料服务引用,必须改为廉洁风险防控自己的
`source_app`
,并保持
`{name,type,size,url}`
快照协议、延迟建档和重试去重语义。
-
UI 边界:活动业务源码使用直接 HeroUI/HeroUI Pro 组合;DataGrid、Modal、DropZone、Autocomplete、Badge/Tooltip 的新增实现继续保持原生 compound API,不引入
Catalyst、Headless UI
、旧适配器或页面私有皮肤。
-
UI 边界:活动业务源码使用直接 HeroUI/HeroUI Pro 组合;DataGrid、Modal、DropZone、Autocomplete、Badge/Tooltip 的新增实现继续保持原生 compound API,不引入
旧版 UI 框架
、旧适配器或页面私有皮肤。
src/components/DynamicRecordDialog.jsx
View file @
fbbe3f5a
...
@@ -22,6 +22,8 @@ import {
...
@@ -22,6 +22,8 @@ import {
isReferenceFieldType
,
isReferenceFieldType
,
getViewRestrictionDefaults
,
getViewRestrictionDefaults
,
normalizeValueForSubmit
,
normalizeValueForSubmit
,
isFormulaFieldType
,
resolveFieldValue
,
shouldHideViewTitleColumn
,
shouldHideViewTitleColumn
,
toArray
,
toArray
,
}
from
'@/utils/subjectView'
;
}
from
'@/utils/subjectView'
;
...
@@ -223,7 +225,9 @@ function NestedSubjectFields({ fieldDef, value, onChange, disabled, depth }) {
...
@@ -223,7 +225,9 @@ function NestedSubjectFields({ fieldDef, value, onChange, disabled, depth }) {
<
DynamicField
<
DynamicField
key=
{
nestedField
.
name
}
key=
{
nestedField
.
name
}
fieldDef=
{
nestedField
}
fieldDef=
{
nestedField
}
value=
{
item
?.[
nestedField
.
name
]
}
value=
{
isFormulaFieldType
(
nestedField
.
type
)
?
resolveFieldValue
(
item
,
nestedField
.
name
,
nestedField
)
:
item
?.[
nestedField
.
name
]
}
disabled=
{
disabled
}
disabled=
{
disabled
}
onChange=
{
(
nextValue
)
=>
handleFieldChange
(
nestedField
.
name
,
nextValue
)
}
onChange=
{
(
nextValue
)
=>
handleFieldChange
(
nestedField
.
name
,
nextValue
)
}
depth=
{
depth
+
1
}
depth=
{
depth
+
1
}
...
@@ -294,7 +298,16 @@ export function DynamicField({ fieldDef, value, onChange, disabled = false, dept
...
@@ -294,7 +298,16 @@ export function DynamicField({ fieldDef, value, onChange, disabled = false, dept
const
fieldPlaceholder
=
getFieldPlaceholder
(
fieldDef
);
const
fieldPlaceholder
=
getFieldPlaceholder
(
fieldDef
);
let
control
=
null
;
let
control
=
null
;
if
(
type
===
'boolean'
)
{
if
(
isFormulaFieldType
(
type
))
{
control
=
(
<
Input
isReadOnly
value=
{
value
??
''
}
placeholder=
"保存后由公式自动计算"
variant=
"secondary"
/>
);
}
else
if
(
type
===
'boolean'
)
{
control
=
(
control
=
(
<
Checkbox
<
Checkbox
isSelected=
{
Boolean
(
value
)
}
isSelected=
{
Boolean
(
value
)
}
...
@@ -422,6 +435,10 @@ export async function prepareValuesForSubmit(subject, values, path = '') {
...
@@ -422,6 +435,10 @@ export async function prepareValuesForSubmit(subject, values, path = '') {
const
fieldLabel
=
`
${
path
}${
getFieldDisplayLabel
(
field
)}
`
;
const
fieldLabel
=
`
${
path
}${
getFieldDisplayLabel
(
field
)}
`
;
const
fieldType
=
String
(
field
.
type
||
'text'
).
toLowerCase
();
const
fieldType
=
String
(
field
.
type
||
'text'
).
toLowerCase
();
if
(
isFormulaFieldType
(
fieldType
))
{
continue
;
}
if
(
isRequiredField
(
field
)
&&
isEmptyValue
(
value
))
{
if
(
isRequiredField
(
field
)
&&
isEmptyValue
(
value
))
{
errors
.
push
(
`
${
fieldLabel
}
为必填项`
);
errors
.
push
(
`
${
fieldLabel
}
为必填项`
);
}
}
...
@@ -598,15 +615,19 @@ export default function DynamicRecordDialog({
...
@@ -598,15 +615,19 @@ export default function DynamicRecordDialog({
/>
/>
</
TextField
>
:
null
}
</
TextField
>
:
null
}
{
dialogFields
.
map
((
field
)
=>
{
{
dialogFields
.
map
((
field
)
=>
{
const
fieldType
=
String
(
field
.
type
||
''
).
toLowerCase
();
const
fieldValue
=
isFormulaFieldType
(
fieldType
)
?
resolveFieldValue
(
values
,
field
.
name
,
field
)
:
values
[
field
.
name
];
const
isFullWidth
=
[
'long_text'
,
'rich_text'
,
'object'
,
'form'
,
'subject'
,
'file'
].
includes
(
const
isFullWidth
=
[
'long_text'
,
'rich_text'
,
'object'
,
'form'
,
'subject'
,
'file'
].
includes
(
String
(
field
.
type
||
''
).
toLowerCase
()
,
fieldType
,
);
);
return
(
return
(
<
div
className=
{
isFullWidth
?
'md:col-span-2'
:
undefined
}
key=
{
field
.
name
}
>
<
div
className=
{
isFullWidth
?
'md:col-span-2'
:
undefined
}
key=
{
field
.
name
}
>
<
DynamicField
<
DynamicField
fieldDef=
{
field
}
fieldDef=
{
field
}
value=
{
values
[
field
.
name
]
}
value=
{
fieldValue
}
disabled=
{
saving
||
(
editing
&&
isPrimaryField
(
field
))
||
(
disabled=
{
saving
||
isFormulaFieldType
(
fieldType
)
||
(
editing
&&
isPrimaryField
(
field
))
||
(
!
editing
&&
Object
.
prototype
.
hasOwnProperty
.
call
(
restrictionDefaults
.
metadata
,
field
.
name
)
!
editing
&&
Object
.
prototype
.
hasOwnProperty
.
call
(
restrictionDefaults
.
metadata
,
field
.
name
)
)
}
)
}
onChange=
{
(
nextValue
)
=>
setValues
((
current
)
=>
({
onChange=
{
(
nextValue
)
=>
setValues
((
current
)
=>
({
...
...
src/components/SubjectRecordWorkbench.jsx
View file @
fbbe3f5a
...
@@ -161,6 +161,11 @@ function normalizeActionContext(context, helpers) {
...
@@ -161,6 +161,11 @@ function normalizeActionContext(context, helpers) {
...
context
,
...
context
,
selectedRecords
:
(
context
.
selectedRecords
||
[]).
map
((
record
)
=>
record
.
raw
||
record
),
selectedRecords
:
(
context
.
selectedRecords
||
[]).
map
((
record
)
=>
record
.
raw
||
record
),
record
:
context
.
record
?
context
.
record
.
raw
||
context
.
record
:
undefined
,
record
:
context
.
record
?
context
.
record
.
raw
||
context
.
record
:
undefined
,
parentRecord
:
context
.
parentRecord
?
context
.
parentRecord
.
raw
||
context
.
parentRecord
:
undefined
,
isExpandedRecord
:
Boolean
(
context
.
isExpandedRecord
),
isExpandedPanel
:
Boolean
(
context
.
isExpandedPanel
),
...
helpers
,
...
helpers
,
};
};
}
}
...
@@ -179,6 +184,7 @@ export default function SubjectRecordWorkbench({
...
@@ -179,6 +184,7 @@ export default function SubjectRecordWorkbench({
toolbarActions
=
[],
toolbarActions
=
[],
bulkActions
=
[],
bulkActions
=
[],
rowActions
=
[],
rowActions
=
[],
expandedRowActions
=
[],
compactBulkActions
=
false
,
compactBulkActions
=
false
,
hiddenColumnFields
=
[],
hiddenColumnFields
=
[],
detailDrawerProps
=
{},
detailDrawerProps
=
{},
...
@@ -474,6 +480,9 @@ export default function SubjectRecordWorkbench({
...
@@ -474,6 +480,9 @@ export default function SubjectRecordWorkbench({
}]
}]
:
[]),
:
[]),
]
:
[];
]
:
[];
const
builtExpandedRowActions
=
hasAccessibleGridView
?
expandedRowActions
.
map
(
wrapAction
)
:
[];
if
(
loading
&&
!
hasAccessibleGridView
)
{
if
(
loading
&&
!
hasAccessibleGridView
)
{
return
(
return
(
...
@@ -534,6 +543,7 @@ export default function SubjectRecordWorkbench({
...
@@ -534,6 +543,7 @@ export default function SubjectRecordWorkbench({
toolbarActions=
{
builtToolbarActions
}
toolbarActions=
{
builtToolbarActions
}
bulkActions=
{
builtBulkActions
}
bulkActions=
{
builtBulkActions
}
rowActions=
{
builtRowActions
}
rowActions=
{
builtRowActions
}
expandedRowActions=
{
builtExpandedRowActions
}
isRowExpandable=
{
isRowExpandable
}
isRowExpandable=
{
isRowExpandable
}
getExpandedRecordRows=
{
getExpandedRecordRows
?
resolvedExpandedRecordRows
:
undefined
}
getExpandedRecordRows=
{
getExpandedRecordRows
?
resolvedExpandedRecordRows
:
undefined
}
detailPanel=
{
{
detailPanel=
{
{
...
...
src/components/record-workbench/index.jsx
View file @
fbbe3f5a
...
@@ -27,7 +27,12 @@ import FilterSelect from '@/components/FilterSelect';
...
@@ -27,7 +27,12 @@ import FilterSelect from '@/components/FilterSelect';
import
FormDatePicker
from
'@/components/FormDatePicker'
;
import
FormDatePicker
from
'@/components/FormDatePicker'
;
import
ReferenceSearchSelect
from
'@/pages/RecordDetailDrawer/components/ReferenceSearchSelect'
;
import
ReferenceSearchSelect
from
'@/pages/RecordDetailDrawer/components/ReferenceSearchSelect'
;
import
UserSearchSelect
from
'@/components/UserSearchSelect'
;
import
UserSearchSelect
from
'@/components/UserSearchSelect'
;
import
{
formatFieldValue
,
getFieldValue
,
normalizeText
}
from
'@/utils/subjectView'
;
import
{
formatFieldValue
,
getFieldValue
,
normalizeText
,
resolveFieldValue
,
}
from
'@/utils/subjectView'
;
import
{
import
{
Alert
,
Alert
,
Avatar
,
Avatar
,
...
@@ -323,7 +328,7 @@ function getFilterPlaceholder(field) {
...
@@ -323,7 +328,7 @@ function getFilterPlaceholder(field) {
function
defaultRenderCell
(
record
,
column
)
{
function
defaultRenderCell
(
record
,
column
)
{
const
sourceRecord
=
record
?.
raw
||
record
;
const
sourceRecord
=
record
?.
raw
||
record
;
const
value
=
getFieldValue
(
sourceRecord
,
column
.
field
);
const
value
=
resolveFieldValue
(
sourceRecord
,
column
.
field
,
column
.
fieldDef
);
const
text
=
formatFieldValue
(
value
,
column
.
fieldDef
)
||
'-'
;
const
text
=
formatFieldValue
(
value
,
column
.
fieldDef
)
||
'-'
;
if
(
column
.
fieldDef
?.
type
===
'user'
)
{
if
(
column
.
fieldDef
?.
type
===
'user'
)
{
...
@@ -542,6 +547,7 @@ export default function RecordWorkbench({
...
@@ -542,6 +547,7 @@ export default function RecordWorkbench({
toolbarActions
=
[],
toolbarActions
=
[],
bulkActions
=
[],
bulkActions
=
[],
rowActions
=
[],
rowActions
=
[],
expandedRowActions
=
[],
detailPanel
=
null
,
detailPanel
=
null
,
states
=
{},
states
=
{},
renderCell
,
renderCell
,
...
@@ -605,21 +611,25 @@ export default function RecordWorkbench({
...
@@ -605,21 +611,25 @@ export default function RecordWorkbench({
[
records
,
getRecordId
],
[
records
,
getRecordId
],
);
);
const
resolvedActionColumnWidth
=
useMemo
(
const
resolvedActionColumnWidth
=
useMemo
(
()
=>
resolveActionColumnWidth
(
rowActions
,
actionColumnWidth
),
()
=>
resolveActionColumnWidth
(
[
rowActions
,
actionColumnWidth
],
rowActions
.
length
>=
expandedRowActions
.
length
?
rowActions
:
expandedRowActions
,
actionColumnWidth
,
),
[
rowActions
,
expandedRowActions
,
actionColumnWidth
],
);
);
const
hasRowActions
=
rowActions
.
length
>
0
||
expandedRowActions
.
length
>
0
;
const
stickyColumns
=
useMemo
(
const
stickyColumns
=
useMemo
(
()
=>
()
=>
buildStickyColumnMeta
(
buildStickyColumnMeta
(
orderedVisibleColumns
,
orderedVisibleColumns
,
selectable
,
selectable
,
rowActions
.
length
>
0
,
hasRowActions
,
resolvedActionColumnWidth
,
resolvedActionColumnWidth
,
),
),
[
[
orderedVisibleColumns
,
orderedVisibleColumns
,
selectable
,
selectable
,
rowActions
.
length
,
hasRowActions
,
resolvedActionColumnWidth
,
resolvedActionColumnWidth
,
],
],
);
);
...
@@ -801,6 +811,12 @@ export default function RecordWorkbench({
...
@@ -801,6 +811,12 @@ export default function RecordWorkbench({
});
});
if (Array.isArray(expandedRecords)) {
if (Array.isArray(expandedRecords)) {
expandedRecords.forEach((expandedRecord, index) => {
expandedRecords.forEach((expandedRecord, index) => {
const expandedRowContext = {
...rowContext,
record: expandedRecord,
parentRecord: record,
isExpandedRecord: true,
};
dataGridRows.push({
dataGridRows.push({
key:
key:
expandedRecord?.key ||
expandedRecord?.key ||
...
@@ -811,7 +827,7 @@ export default function RecordWorkbench({
...
@@ -811,7 +827,7 @@ export default function RecordWorkbench({
parentRecord: record,
parentRecord: record,
expandable: false,
expandable: false,
expanded: false,
expanded: false,
rowContext,
rowContext
: expandedRowContext
,
isExpandedRecord: true,
isExpandedRecord: true,
});
});
});
});
...
@@ -821,7 +837,11 @@ export default function RecordWorkbench({
...
@@ -821,7 +837,11 @@ export default function RecordWorkbench({
key: `
$
{
recordId
}
-
expanded
`,
key: `
$
{
recordId
}
-
expanded
`,
record,
record,
parentRecord: record,
parentRecord: record,
rowContext,
rowContext: {
...rowContext,
parentRecord: record,
isExpandedPanel: true,
},
isExpandedPanel: true,
isExpandedPanel: true,
expandedContent: renderExpandedRow({ record, rowContext }),
expandedContent: renderExpandedRow({ record, rowContext }),
});
});
...
@@ -914,13 +934,21 @@ export default function RecordWorkbench({
...
@@ -914,13 +934,21 @@ export default function RecordWorkbench({
}
}
if (columnIndex === 0 && detailPanel) {
if (columnIndex === 0 && detailPanel) {
const value = getFieldValue(row.record?.raw || row.record, column.field);
const value = resolveFieldValue(
row.record?.raw || row.record,
column.field,
column.fieldDef,
);
const text = formatFieldValue(value, column.fieldDef) || '-';
const text = formatFieldValue(value, column.fieldDef) || '-';
return (
return (
<DataTablePrimaryAction
<DataTablePrimaryAction
className="h-auto w-full min-w-0 justify-start px-0 text-left"
className="h-auto w-full min-w-0 justify-start px-0 text-left"
text={text}
text={text}
onPress={() => row.rowContext.openDetail(row.record)}
onPress={() => row.rowContext.openDetail(
row.isExpandedRecord || row.isExpandedPanel
? row.parentRecord
: row.record,
)}
/>
/>
);
);
}
}
...
@@ -929,7 +957,7 @@ export default function RecordWorkbench({
...
@@ -929,7 +957,7 @@ export default function RecordWorkbench({
},
},
};
};
}),
}),
...(
rowActions.length > 0
...(
hasRowActions
? [
? [
{
{
key: 'actions',
key: 'actions',
...
@@ -939,10 +967,10 @@ export default function RecordWorkbench({
...
@@ -939,10 +967,10 @@ export default function RecordWorkbench({
pin: 'end',
pin: 'end',
isActions: true,
isActions: true,
render: (row) => {
render: (row) => {
if (row.isExpandedRecord || row.isExpandedPanel) {
const actions = row.isExpandedRecord || row.isExpandedPanel
return null;
? expandedRowActions
}
: rowActions;
const visibleRowActions =
rowA
ctions.filter(
const visibleRowActions =
a
ctions.filter(
(action) => !isActionHidden(action, row.rowContext),
(action) => !isActionHidden(action, row.rowContext),
);
);
return (
return (
...
@@ -961,9 +989,15 @@ export default function RecordWorkbench({
...
@@ -961,9 +989,15 @@ export default function RecordWorkbench({
]
]
: []),
: []),
];
];
const disabledDataGridRowKeys = dataGridRows
// Expanded rows can expose their own actions (for example, editing a nested
.filter((row) => row.isExpandedRecord || row.isExpandedPanel)
// department item). Do not disable the whole row in that case: HeroUI
.map((row) => String(row.key));
// DataGrid's disabledKeys disables descendant buttons as well as selection.
// Selection is still filtered to parent record keys by the workbench.
const disabledDataGridRowKeys = expandedRowActions.length > 0
? []
: dataGridRows
.filter((row) => row.isExpandedRecord || row.isExpandedPanel)
.map((row) => String(row.key));
const toolbarSearchField = filterSchema.find((field) => field.icon === 'search');
const toolbarSearchField = filterSchema.find((field) => field.icon === 'search');
const structuredFilterFields = filterSchema.filter(
const structuredFilterFields = filterSchema.filter(
(field) => field !== toolbarSearchField,
(field) => field !== toolbarSearchField,
...
@@ -1229,7 +1263,7 @@ export default function RecordWorkbench({
...
@@ -1229,7 +1263,7 @@ export default function RecordWorkbench({
selectable
selectable
? (keys) =>
? (keys) =>
onSelectionChange?.(
onSelectionChange?.(
keys.filter((key) => currentPageRecordIds.includes(
key
)),
keys.filter((key) => currentPageRecordIds.includes(
String(key)
)),
)
)
: undefined
: undefined
}
}
...
...
src/pages/RecordDetailDrawer/components/ReferenceRecordsTable.jsx
View file @
fbbe3f5a
...
@@ -10,9 +10,9 @@ import { loadSubject, queryRecords } from '@/services/DataService';
...
@@ -10,9 +10,9 @@ import { loadSubject, queryRecords } from '@/services/DataService';
import
{
import
{
formatDateText
,
formatDateText
,
formatFieldValue
,
formatFieldValue
,
getFieldValue
,
getReferenceSubjectName
,
getReferenceSubjectName
,
normalizeRecordId
,
normalizeRecordId
,
resolveFieldValue
,
toArray
,
toArray
,
}
from
'@/utils/subjectView'
;
}
from
'@/utils/subjectView'
;
...
@@ -165,6 +165,7 @@ export default function ReferenceRecordsTable({ fieldDef, value, pageSize = 1000
...
@@ -165,6 +165,7 @@ export default function ReferenceRecordsTable({ fieldDef, value, pageSize = 1000
_id
:
{
$in
:
referenceIds
},
_id
:
{
$in
:
referenceIds
},
},
},
mode
:
'full'
,
mode
:
'full'
,
calculated
:
true
,
})
})
:
Promise
.
resolve
({
records
:
populatedRecords
}),
:
Promise
.
resolve
({
records
:
populatedRecords
}),
]);
]);
...
@@ -250,7 +251,7 @@ export default function ReferenceRecordsTable({ fieldDef, value, pageSize = 1000
...
@@ -250,7 +251,7 @@ export default function ReferenceRecordsTable({ fieldDef, value, pageSize = 1000
isPrimary
:
index
===
0
,
isPrimary
:
index
===
0
,
render
:
(
record
)
=>
(
render
:
(
record
)
=>
(
<
CellValue
<
CellValue
value=
{
getFieldValue
(
record
,
column
.
field
)
}
value=
{
resolveFieldValue
(
record
,
column
.
field
,
column
.
fieldDef
)
}
field=
{
column
}
field=
{
column
}
/>
/>
),
),
...
...
src/pages/RecordDetailDrawer/components/SubjectViewContent.jsx
View file @
fbbe3f5a
...
@@ -5,7 +5,7 @@ import {
...
@@ -5,7 +5,7 @@ import {
buildViewColumns
,
buildViewColumns
,
getFieldDisplayLabel
,
getFieldDisplayLabel
,
getFieldPlaceholder
,
getFieldPlaceholder
,
get
FieldValue
,
resolve
FieldValue
,
isFieldDisabledByView
,
isFieldDisabledByView
,
}
from
'@/utils/subjectView'
;
}
from
'@/utils/subjectView'
;
...
@@ -45,7 +45,7 @@ export default function SubjectViewContent({
...
@@ -45,7 +45,7 @@ export default function SubjectViewContent({
className=
"grid min-w-0 grid-cols-1 gap-y-2"
className=
"grid min-w-0 grid-cols-1 gap-y-2"
>
>
{
contentFields
.
map
((
field
)
=>
{
{
contentFields
.
map
((
field
)
=>
{
const
value
=
getFieldValue
(
editableRecord
,
field
.
field
);
const
value
=
resolveFieldValue
(
editableRecord
,
field
.
field
,
field
.
fieldDef
);
const
fieldLabel
=
getFieldDisplayLabel
(
field
.
fieldDef
,
field
.
field
);
const
fieldLabel
=
getFieldDisplayLabel
(
field
.
fieldDef
,
field
.
field
);
const
fieldDisabled
=
isFieldDisabled
const
fieldDisabled
=
isFieldDisabled
?
isFieldDisabled
(
field
.
field
,
view
)
?
isFieldDisabled
(
field
.
field
,
view
)
...
...
src/pages/RecordDetailDrawer/components/TailwindEditableField.jsx
View file @
fbbe3f5a
...
@@ -23,6 +23,7 @@ import {
...
@@ -23,6 +23,7 @@ import {
getFieldDisplayLabel
,
getFieldDisplayLabel
,
getFieldPlaceholder
,
getFieldPlaceholder
,
getSelectOptions
,
getSelectOptions
,
isFormulaFieldType
,
isDateFieldType
,
isDateFieldType
,
isReferenceFieldType
,
isReferenceFieldType
,
getReferenceSubjectName
,
getReferenceSubjectName
,
...
@@ -252,6 +253,17 @@ function ControlledFieldEditor({
...
@@ -252,6 +253,17 @@ function ControlledFieldEditor({
}
}
};
};
if
(
isFormulaFieldType
(
fieldType
))
{
return
(
<
Input
isReadOnly
value=
{
value
??
''
}
placeholder=
"保存后由公式自动计算"
variant=
{
fieldVariant
}
/>
);
}
if
(
fieldType
===
'boolean'
)
{
if
(
fieldType
===
'boolean'
)
{
return
(
return
(
<
Checkbox
isSelected=
{
Boolean
(
value
)
}
onChange=
{
(
nextValue
)
=>
onChange
?.(
nextValue
)
}
>
<
Checkbox
isSelected=
{
Boolean
(
value
)
}
onChange=
{
(
nextValue
)
=>
onChange
?.(
nextValue
)
}
>
...
@@ -433,12 +445,13 @@ export default function TailwindEditableField({
...
@@ -433,12 +445,13 @@ export default function TailwindEditableField({
const
canEdit
=
!
readonly
&&
!
disabled
;
const
canEdit
=
!
readonly
&&
!
disabled
;
const
fieldType
=
fieldDef
?.
type
||
'text'
;
const
fieldType
=
fieldDef
?.
type
||
'text'
;
const
isFormula
=
isFormulaFieldType
(
fieldType
);
const
isDateField
=
isDateFieldType
(
fieldType
);
const
isDateField
=
isDateFieldType
(
fieldType
);
const
isLongText
=
[
'long_text'
,
'rich_text'
].
includes
(
fieldType
);
const
isLongText
=
[
'long_text'
,
'rich_text'
].
includes
(
fieldType
);
const
isJsonLike
=
[
'object'
,
'form'
,
'subject'
].
includes
(
fieldType
);
const
isJsonLike
=
[
'object'
,
'form'
,
'subject'
].
includes
(
fieldType
);
const
textPlaceholder
=
getTextPlaceholder
(
placeholder
,
fieldDef
);
const
textPlaceholder
=
getTextPlaceholder
(
placeholder
,
fieldDef
);
if
(
mode
===
'view'
||
(
mode
===
'edit'
&&
(
readonly
||
disabled
)))
{
if
(
mode
===
'view'
||
isFormula
||
(
mode
===
'edit'
&&
(
readonly
||
disabled
)))
{
return
<
ReadonlyFieldValue
value=
{
value
}
fieldDef=
{
fieldDef
}
/>;
return
<
ReadonlyFieldValue
value=
{
value
}
fieldDef=
{
fieldDef
}
/>;
}
}
...
...
src/pages/RecordDetailDrawer/hooks/useSubjectRecord.js
View file @
fbbe3f5a
...
@@ -96,7 +96,7 @@ export function useSubjectRecord(
...
@@ -96,7 +96,7 @@ export function useSubjectRecord(
setError('');
setError('');
try {
try {
const record = await loadRecord(recordName);
const record = await loadRecord(recordName
, true
);
const resolvedSubjectName = subjectName || record?.__t || record?.subject?.name || record?.subject;
const resolvedSubjectName = subjectName || record?.__t || record?.subject?.name || record?.subject;
const [subjectData, viewSummaries] = resolvedSubjectName
const [subjectData, viewSummaries] = resolvedSubjectName
? await Promise.all([loadSubject(resolvedSubjectName), getSubjectView(resolvedSubjectName)])
? await Promise.all([loadSubject(resolvedSubjectName), getSubjectView(resolvedSubjectName)])
...
@@ -151,7 +151,10 @@ export function useSubjectRecord(
...
@@ -151,7 +151,10 @@ export function useSubjectRecord(
return;
return;
}
}
const normalizedChanges = changes.filter((change) => change?.fieldPath);
const normalizedChanges = changes.filter((change) => (
change?.fieldPath
&& String(change.fieldDef?.type || '').toLowerCase() !== 'formula'
));
if (normalizedChanges.length === 0) {
if (normalizedChanges.length === 0) {
return;
return;
}
}
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
fbbe3f5a
...
@@ -23,7 +23,7 @@ import {
...
@@ -23,7 +23,7 @@ import {
}
from
'@/utils/subjectView'
;
}
from
'@/utils/subjectView'
;
const
EMPTY_ARRAY
=
[];
const
EMPTY_ARRAY
=
[];
const
NON_EDITABLE_FIELD_TYPES
=
new
Set
([
'form'
,
'subject'
]);
const
NON_EDITABLE_FIELD_TYPES
=
new
Set
([
'form'
,
'subject'
,
'formula'
]);
function
cloneRecord
(
record
)
{
function
cloneRecord
(
record
)
{
return
record
?
JSON
.
parse
(
JSON
.
stringify
(
record
))
:
null
;
return
record
?
JSON
.
parse
(
JSON
.
stringify
(
record
))
:
null
;
...
...
src/pages/integrity_risk_prevention/ControlMeasureFormPage.jsx
View file @
fbbe3f5a
...
@@ -16,6 +16,7 @@ import {
...
@@ -16,6 +16,7 @@ import {
normalizeIds
,
normalizeIds
,
rejectControlMeasureFlow
,
rejectControlMeasureFlow
,
submitControlMeasureFlow
,
submitControlMeasureFlow
,
withdrawControlMeasureFlow
,
}
from
'./IntegrityRiskService'
;
}
from
'./IntegrityRiskService'
;
function
normalizeRecord
(
record
)
{
function
normalizeRecord
(
record
)
{
...
@@ -58,6 +59,34 @@ function RejectConfirmDialog({ type, loading, onClose, onConfirm }) {
...
@@ -58,6 +59,34 @@ function RejectConfirmDialog({ type, loading, onClose, onConfirm }) {
);
);
}
}
function
WithdrawConfirmDialog
({
open
,
loading
,
onClose
,
onConfirm
})
{
return
(
<
XinyuanModal
isOpen=
{
open
}
onOpenChange=
{
(
isOpen
)
=>
{
if
(
!
isOpen
&&
!
loading
)
onClose
?.();
}
}
size=
"sm"
title=
"确认撤回审批"
description=
"撤回后审批人的 OA 待办会完成,表单恢复为填报人可填写状态。"
footer=
{
(
<>
<
Button
isDisabled=
{
loading
}
variant=
"secondary"
onPress=
{
onClose
}
>
取消
</
Button
>
<
Button
isDisabled=
{
loading
}
isPending=
{
loading
}
onPress=
{
onConfirm
}
>
{
loading
?
'撤回中...'
:
'确认撤回'
}
</
Button
>
</>
)
}
>
<
Alert
status=
"warning"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
已填写的数据会保留,审批人尚未处理时才可以撤回。
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
</
XinyuanModal
>
);
}
export
default
function
ControlMeasureFormPage
()
{
export
default
function
ControlMeasureFormPage
()
{
const
location
=
useLocation
();
const
location
=
useLocation
();
const
userInfo
=
useUserInfo
();
const
userInfo
=
useUserInfo
();
...
@@ -67,6 +96,7 @@ export default function ControlMeasureFormPage() {
...
@@ -67,6 +96,7 @@ export default function ControlMeasureFormPage() {
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
success
,
setSuccess
]
=
useState
(
''
);
const
[
success
,
setSuccess
]
=
useState
(
''
);
const
[
rejectType
,
setRejectType
]
=
useState
(
''
);
const
[
rejectType
,
setRejectType
]
=
useState
(
''
);
const
[
withdrawOpen
,
setWithdrawOpen
]
=
useState
(
false
);
const
messageId
=
useMemo
(
const
messageId
=
useMemo
(
()
=>
new
URLSearchParams
(
location
.
search
).
get
(
'id'
)
||
''
,
()
=>
new
URLSearchParams
(
location
.
search
).
get
(
'id'
)
||
''
,
...
@@ -158,6 +188,21 @@ export default function ControlMeasureFormPage() {
...
@@ -158,6 +188,21 @@ export default function ControlMeasureFormPage() {
});
});
}
}
if
(
isEditor
&&
editorStatus
===
FLOW_STATUS
.
complete
&&
auditorStatus
===
FLOW_STATUS
.
pending
)
{
toolbarActions
.
push
({
key
:
'withdraw'
,
label
:
actionLoading
===
'withdraw'
?
'撤回中...'
:
'撤回审批'
,
variant
:
'secondary'
,
tone
:
'warning'
,
disabled
:
Boolean
(
actionLoading
)
||
records
.
length
===
0
,
onClick
:
()
=>
setWithdrawOpen
(
true
),
});
}
if
(
if
(
isAuditor
isAuditor
&&
editorStatus
===
FLOW_STATUS
.
complete
&&
editorStatus
===
FLOW_STATUS
.
complete
...
@@ -228,6 +273,15 @@ export default function ControlMeasureFormPage() {
...
@@ -228,6 +273,15 @@ export default function ControlMeasureFormPage() {
if
(
succeeded
)
setRejectType
(
''
);
if
(
succeeded
)
setRejectType
(
''
);
};
};
const
confirmWithdraw
=
async
()
=>
{
const
succeeded
=
await
runFlowAction
(
'withdraw'
,
()
=>
withdrawControlMeasureFlow
(
messageRecord
,
userInfo
,
returnUrl
),
'已撤回审批,表单已恢复为可填写状态。'
,
);
if
(
succeeded
)
setWithdrawOpen
(
false
);
};
if
(
workspace
?.
isDepartmentFlow
)
{
if
(
workspace
?.
isDepartmentFlow
)
{
return
(
return
(
<
DepartmentControlMeasureFlow
<
DepartmentControlMeasureFlow
...
@@ -290,6 +344,12 @@ export default function ControlMeasureFormPage() {
...
@@ -290,6 +344,12 @@ export default function ControlMeasureFormPage() {
onClose=
{
()
=>
setRejectType
(
''
)
}
onClose=
{
()
=>
setRejectType
(
''
)
}
onConfirm=
{
confirmReject
}
onConfirm=
{
confirmReject
}
/>
/>
<
WithdrawConfirmDialog
open=
{
withdrawOpen
}
loading=
{
actionLoading
===
'withdraw'
}
onClose=
{
()
=>
setWithdrawOpen
(
false
)
}
onConfirm=
{
confirmWithdraw
}
/>
</
main
>
</
main
>
);
);
}
}
src/pages/integrity_risk_prevention/DepartmentControlItemDialog.jsx
View file @
fbbe3f5a
...
@@ -2,7 +2,11 @@ import { Alert, Button, Surface } from '@heroui/react';
...
@@ -2,7 +2,11 @@ import { Alert, Button, Surface } from '@heroui/react';
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
{
DynamicField
,
prepareValuesForSubmit
}
from
'@/components/DynamicRecordDialog'
;
import
{
DynamicField
,
prepareValuesForSubmit
}
from
'@/components/DynamicRecordDialog'
;
import
TailwindEditableField
from
'@/pages/RecordDetailDrawer/components/TailwindEditableField'
;
import
TailwindEditableField
from
'@/pages/RecordDetailDrawer/components/TailwindEditableField'
;
import
{
getFieldDisplayLabel
}
from
'@/utils/subjectView'
;
import
{
getFieldDisplayLabel
,
isFormulaFieldType
,
resolveFieldValue
,
}
from
'@/utils/subjectView'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
const
HIDDEN_FIELD_NAMES
=
new
Set
([
const
HIDDEN_FIELD_NAMES
=
new
Set
([
...
@@ -50,8 +54,7 @@ export default function DepartmentControlItemDialog({
...
@@ -50,8 +54,7 @@ export default function DepartmentControlItemDialog({
const
fields
=
useMemo
(
const
fields
=
useMemo
(
()
=>
(
subject
?.
fields
||
[]).
filter
((
field
)
=>
()
=>
(
subject
?.
fields
||
[]).
filter
((
field
)
=>
!
HIDDEN_FIELD_NAMES
.
has
(
field
.
name
)
!
HIDDEN_FIELD_NAMES
.
has
(
field
.
name
)),
&&
String
(
field
.
type
||
''
).
toLowerCase
()
!==
'formula'
),
[
subject
],
[
subject
],
);
);
...
@@ -74,7 +77,11 @@ export default function DepartmentControlItemDialog({
...
@@ -74,7 +77,11 @@ export default function DepartmentControlItemDialog({
<
div
className=
"mt-1.5 text-foreground"
>
{
item
?.
business_unit
||
'暂无内容'
}
</
div
>
<
div
className=
"mt-1.5 text-foreground"
>
{
item
?.
business_unit
||
'暂无内容'
}
</
div
>
</
div
>
</
div
>
{
fields
.
map
((
field
)
=>
{
{
fields
.
map
((
field
)
=>
{
const
isFullWidth
=
[
'long_text'
,
'rich_text'
,
'subject'
,
'object'
,
'form'
].
includes
(
field
.
type
);
const
fieldType
=
String
(
field
.
type
||
''
).
toLowerCase
();
const
fieldValue
=
isFormulaFieldType
(
fieldType
)
?
resolveFieldValue
(
values
,
field
.
name
,
field
)
:
values
[
field
.
name
];
const
isFullWidth
=
[
'long_text'
,
'rich_text'
,
'subject'
,
'object'
,
'form'
].
includes
(
fieldType
);
return
(
return
(
<
div
<
div
...
@@ -93,14 +100,14 @@ export default function DepartmentControlItemDialog({
...
@@ -93,14 +100,14 @@ export default function DepartmentControlItemDialog({
fieldDef=
{
field
}
fieldDef=
{
field
}
mode=
"view"
mode=
"view"
readonly
readonly
value=
{
values
[
field
.
name
]
}
value=
{
fieldValue
}
/>
/>
</
div
>
</
div
>
</>
</>
)
:
(
)
:
(
<
DynamicField
<
DynamicField
fieldDef=
{
field
}
fieldDef=
{
field
}
value=
{
values
[
field
.
name
]
}
value=
{
fieldValue
}
disabled=
{
loading
}
disabled=
{
loading
}
onChange=
{
(
nextValue
)
=>
setValues
((
current
)
=>
({
onChange=
{
(
nextValue
)
=>
setValues
((
current
)
=>
({
...
current
,
...
current
,
...
...
src/pages/integrity_risk_prevention/DepartmentControlMeasureFlow.jsx
View file @
fbbe3f5a
...
@@ -3,7 +3,7 @@ import XinyuanModal from '@/components/XinyuanModal';
...
@@ -3,7 +3,7 @@ import XinyuanModal from '@/components/XinyuanModal';
import
RecordWorkbench
from
'@/components/record-workbench'
;
import
RecordWorkbench
from
'@/components/record-workbench'
;
import
{
queryUsers
}
from
'@/services/DataService'
;
import
{
queryUsers
}
from
'@/services/DataService'
;
import
{
normalizeUserLabel
}
from
'@/utils/subjectView'
;
import
{
normalizeUserLabel
}
from
'@/utils/subjectView'
;
import
{
CheckCircleIcon
,
Close
,
Edit
,
Eye
,
Send
}
from
'@/components/AppIcons'
;
import
{
ArrowPathIcon
,
CheckCircleIcon
,
Close
,
Edit
,
Eye
,
Send
}
from
'@/components/AppIcons'
;
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
DepartmentControlItemDialog
from
'./DepartmentControlItemDialog'
;
import
DepartmentControlItemDialog
from
'./DepartmentControlItemDialog'
;
...
@@ -17,6 +17,7 @@ import {
...
@@ -17,6 +17,7 @@ import {
rejectDepartmentControlItem
,
rejectDepartmentControlItem
,
saveDepartmentControlItem
,
saveDepartmentControlItem
,
submitDepartmentControlItem
,
submitDepartmentControlItem
,
withdrawDepartmentControlItem
,
}
from
'./IntegrityRiskService'
;
}
from
'./IntegrityRiskService'
;
const
COLUMNS
=
[
const
COLUMNS
=
[
...
@@ -42,6 +43,7 @@ export default function DepartmentControlMeasureFlow({ workspace, userInfo, onRe
...
@@ -42,6 +43,7 @@ export default function DepartmentControlMeasureFlow({ workspace, userInfo, onRe
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
success
,
setSuccess
]
=
useState
(
''
);
const
[
success
,
setSuccess
]
=
useState
(
''
);
const
[
rejectContext
,
setRejectContext
]
=
useState
(
null
);
const
[
rejectContext
,
setRejectContext
]
=
useState
(
null
);
const
[
withdrawContext
,
setWithdrawContext
]
=
useState
(
null
);
const
[
userLabels
,
setUserLabels
]
=
useState
({});
const
[
userLabels
,
setUserLabels
]
=
useState
({});
const
rows
=
useMemo
(
const
rows
=
useMemo
(
...
@@ -130,6 +132,14 @@ export default function DepartmentControlMeasureFlow({ workspace, userInfo, onRe
...
@@ -130,6 +132,14 @@ export default function DepartmentControlMeasureFlow({ workspace, userInfo, onRe
`
${
record
.
business_unit
}
部门明细已送审。`
,
`
${
record
.
business_unit
}
部门明细已送审。`
,
),
),
},
},
{
key
:
'withdraw-department-item'
,
label
:
'撤回审批'
,
icon
:
ArrowPathIcon
,
hidden
:
({
record
})
=>
!
getDepartmentItemPermissions
(
record
,
userInfo
).
canWithdraw
,
disabled
:
({
record
})
=>
Boolean
(
actionKey
)
||
!
record
.
auditor
,
onClick
:
({
record
})
=>
setWithdrawContext
(
record
),
},
{
{
key
:
'approve-department-item'
,
key
:
'approve-department-item'
,
label
:
'审批通过'
,
label
:
'审批通过'
,
...
@@ -204,6 +214,17 @@ export default function DepartmentControlMeasureFlow({ workspace, userInfo, onRe
...
@@ -204,6 +214,17 @@ export default function DepartmentControlMeasureFlow({ workspace, userInfo, onRe
if
(
succeeded
)
setRejectContext
(
null
);
if
(
succeeded
)
setRejectContext
(
null
);
};
};
const
confirmWithdraw
=
async
()
=>
{
const
row
=
withdrawContext
;
if
(
!
row
)
return
;
const
succeeded
=
await
runAction
(
`withdraw-
${
row
.
id
}
`
,
()
=>
withdrawDepartmentControlItem
(
actionArgs
(
row
)),
`
${
row
.
business_unit
}
部门明细已撤回审批,可继续填写。`
,
);
if
(
succeeded
)
setWithdrawContext
(
null
);
};
const
activePermissions
=
activeItem
const
activePermissions
=
activeItem
?
getDepartmentItemPermissions
(
activeItem
,
userInfo
)
?
getDepartmentItemPermissions
(
activeItem
,
userInfo
)
:
{
canEdit
:
false
};
:
{
canEdit
:
false
};
...
@@ -260,6 +281,29 @@ export default function DepartmentControlMeasureFlow({ workspace, userInfo, onRe
...
@@ -260,6 +281,29 @@ export default function DepartmentControlMeasureFlow({ workspace, userInfo, onRe
</>
</>
)
}
)
}
/>
/>
<
XinyuanModal
isOpen=
{
Boolean
(
withdrawContext
)
}
onOpenChange=
{
(
isOpen
)
=>
{
if
(
!
isOpen
&&
!
actionKey
)
setWithdrawContext
(
null
);
}
}
size=
"sm"
title=
"确认撤回审批"
description=
{
`撤回“${withdrawContext?.business_unit || '-'}”部门明细的审批申请。`
}
footer=
{
(
<>
<
Button
isDisabled=
{
Boolean
(
actionKey
)
}
variant=
"secondary"
onPress=
{
()
=>
setWithdrawContext
(
null
)
}
>
取消
</
Button
>
<
Button
isDisabled=
{
Boolean
(
actionKey
)
}
isPending=
{
Boolean
(
actionKey
)
}
onPress=
{
confirmWithdraw
}
>
{
actionKey
?
'撤回中...'
:
'确认撤回'
}
</
Button
>
</>
)
}
>
<
div
className=
"space-y-2 text-sm text-muted"
>
<
p
>
撤回后将完成审批人的 OA 待办,已填写内容会保留。
</
p
>
<
p
>
当前部门明细会恢复为填报人可编辑状态,其他部门不受影响。
</
p
>
</
div
>
</
XinyuanModal
>
</
main
>
</
main
>
);
);
}
}
src/pages/integrity_risk_prevention/DepartmentPushDialog.jsx
View file @
fbbe3f5a
...
@@ -55,7 +55,7 @@ export default function DepartmentPushDialog({ open, targets, loading, onClose,
...
@@ -55,7 +55,7 @@ export default function DepartmentPushDialog({ open, targets, loading, onClose,
primary=
{
assignment
.
business_unit
}
primary=
{
assignment
.
business_unit
}
secondary=
{
showParentTitle
?
assignment
.
parent_title
:
'将生成 1 条部门明细'
}
secondary=
{
showParentTitle
?
assignment
.
parent_title
:
'将生成 1 条部门明细'
}
/>
/>
{
assignment
.
reuse_existing
?
<
Chip
className=
"mt-1"
color=
"warning"
size=
"sm"
>
沿用
未完成子项
</
Chip
>
:
null
}
{
assignment
.
reuse_existing
?
<
Chip
className=
"mt-1"
color=
"warning"
size=
"sm"
>
沿用
部门子项原始数据
</
Chip
>
:
null
}
</
div
>
</
div
>
),
),
},
},
...
...
src/pages/integrity_risk_prevention/IntegrityRiskService.js
View file @
fbbe3f5a
...
@@ -160,19 +160,17 @@ export function buildDepartmentPushAssignments(records = []) {
...
@@ -160,19 +160,17 @@ export function buildDepartmentPushAssignments(records = []) {
return
records
.
flatMap
((
record
)
=>
{
return
records
.
flatMap
((
record
)
=>
{
const
rawRecord
=
record
?.
raw
||
record
;
const
rawRecord
=
record
?.
raw
||
record
;
const
existingItems
=
normalizeDepartmentControlItems
(
rawRecord
);
const
existingItems
=
normalizeDepartmentControlItems
(
rawRecord
);
const
shouldReuseItems
=
existingItems
.
length
>
0
&&
!
allDepartmentItemsApproved
(
existingItems
);
const
existingDepartments
=
existingItems
.
map
((
item
)
=>
item
.
business_unit
).
filter
(
Boolean
);
const
departments
=
shouldReuseItems
const
parentDepartments
=
uniqueValues
(
?
existingItems
.
map
((
item
)
=>
item
.
business_unit
)
toArray
(
rawRecord
?.
metadata
?.
business_unit
).
map
((
item
)
=>
String
(
item
||
''
).
trim
()),
:
uniqueValues
(
toArray
(
rawRecord
?.
metadata
?.
business_unit
).
map
((
item
)
=>
String
(
item
||
''
).
trim
()));
);
const
sourceItems
=
shouldReuseItems
const
departments
=
uniqueValues
([...
existingDepartments
,
...
parentDepartments
]);
?
existingItems
const
sourceItems
=
departments
.
map
((
department
,
index
)
=>
existingItems
.
find
(
:
departments
.
map
((
department
,
index
)
=>
{
(
item
)
=>
item
.
business_unit
===
department
,
const
previousItem
=
existingItems
.
find
((
item
)
=>
item
.
business_unit
===
department
);
)
||
{
return
previousItem
||
{
item_id
:
makeDepartmentItemId
(
rawRecord
,
department
,
index
),
item_id
:
makeDepartmentItemId
(
rawRecord
,
department
,
index
),
business_unit
:
department
,
business_unit
:
department
,
});
};
});
return
sourceItems
.
map
((
item
,
index
)
=>
({
return
sourceItems
.
map
((
item
,
index
)
=>
({
key
:
`
${
recordId
(
rawRecord
)}
-
${
item
.
item_id
||
index
}
`
,
key
:
`
${
recordId
(
rawRecord
)}
-
${
item
.
item_id
||
index
}
`
,
...
@@ -182,7 +180,7 @@ export function buildDepartmentPushAssignments(records = []) {
...
@@ -182,7 +180,7 @@ export function buildDepartmentPushAssignments(records = []) {
business_unit
:
item
.
business_unit
||
'未指定责任单位'
,
business_unit
:
item
.
business_unit
||
'未指定责任单位'
,
editor
:
item
.
editor
||
''
,
editor
:
item
.
editor
||
''
,
auditor
:
item
.
auditor
||
''
,
auditor
:
item
.
auditor
||
''
,
reuse_existing
:
shouldReuseItems
,
reuse_existing
:
existingItems
.
some
((
existingItem
)
=>
existingItem
.
item_id
===
item
.
item_id
)
,
editor_status
:
item
.
editor_status
||
FLOW_STATUS
.
incomplete
,
editor_status
:
item
.
editor_status
||
FLOW_STATUS
.
incomplete
,
auditor_status
:
item
.
auditor_status
||
FLOW_STATUS
.
pending
,
auditor_status
:
item
.
auditor_status
||
FLOW_STATUS
.
pending
,
sender_status
:
item
.
sender_status
||
FLOW_STATUS
.
pending
,
sender_status
:
item
.
sender_status
||
FLOW_STATUS
.
pending
,
...
@@ -363,8 +361,10 @@ export async function exportShowTable(records) {
...
@@ -363,8 +361,10 @@ export async function exportShowTable(records) {
return
sourceRecords
.
length
;
return
sourceRecords
.
length
;
}
}
export
async
function
exportPreventionMeasures
()
{
export
async
function
exportPreventionMeasures
(
const
records
=
await
queryAllIntegrityRecords
(
INTEGRITY_RISK_SUBJECTS
.
show
);
subject
=
INTEGRITY_RISK_SUBJECTS
.
show
,
)
{
const
records
=
await
queryAllIntegrityRecords
(
subject
);
const
measures
=
sortByOriginalIndex
(
records
)
const
measures
=
sortByOriginalIndex
(
records
)
.
flatMap
((
record
)
=>
String
(
.
flatMap
((
record
)
=>
String
(
record
?.
metadata
?.
description_of_main_prevention_and_control_measures
||
''
,
record
?.
metadata
?.
description_of_main_prevention_and_control_measures
||
''
,
...
@@ -515,10 +515,31 @@ function processArchiveRecord(record) {
...
@@ -515,10 +515,31 @@ function processArchiveRecord(record) {
approved_by
:
normalizeIds
(
metadata
.
approved_by
),
approved_by
:
normalizeIds
(
metadata
.
approved_by
),
approval_status
:
metadata
.
approval_status
,
approval_status
:
metadata
.
approval_status
,
final_approval_status
:
metadata
.
final_approval_status
,
final_approval_status
:
metadata
.
final_approval_status
,
[
DEPARTMENT_CONTROL_ITEMS_FIELD
]:
normalizeDepartmentControlItems
(
record
).
map
(
normalizeDepartmentArchiveItem
,
),
},
},
};
};
}
}
function
normalizeDepartmentArchiveItem
(
item
)
{
return
{
...
item
,
business_unit
:
String
(
item
.
business_unit
||
''
).
trim
(),
file_name_and_document_number
:
normalizeIds
(
item
.
file_name_and_document_number
),
subsidiary_system
:
normalizeIds
(
item
.
subsidiary_system
),
editor
:
normalizeId
(
item
.
editor
),
auditor
:
normalizeId
(
item
.
auditor
),
sender
:
normalizeId
(
item
.
sender
),
progress_id_editor
:
item
.
progress_id_editor
||
''
,
progress_id_auditor
:
item
.
progress_id_auditor
||
''
,
progress_id_sender
:
item
.
progress_id_sender
||
''
,
flow_record_id
:
normalizeId
(
item
.
flow_record_id
),
created_by
:
normalizeId
(
item
.
created_by
),
updated_by
:
normalizeId
(
item
.
updated_by
),
};
}
function
showArchiveRecord
(
record
)
{
function
showArchiveRecord
(
record
)
{
const
metadata
=
record
?.
metadata
||
{};
const
metadata
=
record
?.
metadata
||
{};
return
{
return
{
...
@@ -569,9 +590,13 @@ async function createArchiveBatch({ records, historySubject, mapRecord, referenc
...
@@ -569,9 +590,13 @@ async function createArchiveBatch({ records, historySubject, mapRecord, referenc
return
historyIds
.
length
;
return
historyIds
.
length
;
}
}
export
function
archiveProcessRecords
(
records
)
{
export
async
function
archiveProcessRecords
(
records
)
{
const
recordIds
=
normalizeIds
(
records
);
const
latestRecords
=
recordIds
.
length
>
0
?
await
Promise
.
all
(
recordIds
.
map
((
id
)
=>
loadRecord
(
id
)))
:
[];
return
createArchiveBatch
({
return
createArchiveBatch
({
records
,
records
:
latestRecords
,
historySubject
:
INTEGRITY_RISK_SUBJECTS
.
processArchive
,
historySubject
:
INTEGRITY_RISK_SUBJECTS
.
processArchive
,
mapRecord
:
processArchiveRecord
,
mapRecord
:
processArchiveRecord
,
referenceField
:
'reference_record'
,
referenceField
:
'reference_record'
,
...
@@ -641,8 +666,14 @@ async function createProgressTasks(receiverIds, sender, title, returnUrl) {
...
@@ -641,8 +666,14 @@ async function createProgressTasks(receiverIds, sender, title, returnUrl) {
}
}
async
function
completeProgressTasks
(
value
)
{
async
function
completeProgressTasks
(
value
)
{
const
taskIds
=
(
Array
.
isArray
(
value
)
?
value
:
value
?
[
value
]
:
[])
const
flattenTaskValues
=
(
currentValue
)
=>
(
.
flatMap
((
item
)
=>
typeof
item
===
'string'
?
item
.
split
(
','
)
:
[
item
]);
(
Array
.
isArray
(
currentValue
)
?
currentValue
:
currentValue
?
[
currentValue
]
:
[])
.
flatMap
((
item
)
=>
{
if
(
Array
.
isArray
(
item
))
return
flattenTaskValues
(
item
);
return
typeof
item
===
'string'
?
item
.
split
(
','
)
:
[
item
];
})
);
const
taskIds
=
flattenTaskValues
(
value
);
for
(
const
taskId
of
taskIds
for
(
const
taskId
of
taskIds
.
map
((
item
)
=>
typeof
item
===
'object'
.
map
((
item
)
=>
typeof
item
===
'object'
?
item
.
taskId
||
item
.
task_id
||
item
.
id
||
item
.
_id
||
item
.
name
?
item
.
taskId
||
item
.
task_id
||
item
.
id
||
item
.
_id
||
item
.
name
...
@@ -668,6 +699,7 @@ async function sendInternalNotice({ title, content, from, to }) {
...
@@ -668,6 +699,7 @@ async function sendInternalNotice({ title, content, from, to }) {
}
}
let
departmentSubjectPromise
;
let
departmentSubjectPromise
;
let
processSubjectPromise
;
async
function
loadDepartmentSubject
()
{
async
function
loadDepartmentSubject
()
{
if
(
!
departmentSubjectPromise
)
{
if
(
!
departmentSubjectPromise
)
{
...
@@ -680,6 +712,21 @@ async function loadDepartmentSubject() {
...
@@ -680,6 +712,21 @@ async function loadDepartmentSubject() {
return
departmentSubjectPromise
;
return
departmentSubjectPromise
;
}
}
export
async
function
loadDepartmentControlItemSubject
()
{
return
loadDepartmentSubject
();
}
async
function
loadProcessSubject
()
{
if
(
!
processSubjectPromise
)
{
processSubjectPromise
=
loadSubject
(
INTEGRITY_RISK_SUBJECTS
.
process
)
.
catch
((
error
)
=>
{
processSubjectPromise
=
null
;
throw
error
;
});
}
return
processSubjectPromise
;
}
function
departmentBusinessFields
(
subject
)
{
function
departmentBusinessFields
(
subject
)
{
return
(
subject
?.
fields
||
[]).
filter
((
field
)
=>
{
return
(
subject
?.
fields
||
[]).
filter
((
field
)
=>
{
const
fieldName
=
String
(
field
?.
name
||
''
).
trim
();
const
fieldName
=
String
(
field
?.
name
||
''
).
trim
();
...
@@ -699,6 +746,22 @@ function normalizeSnapshotValue(value, field) {
...
@@ -699,6 +746,22 @@ function normalizeSnapshotValue(value, field) {
return
value
===
undefined
?
null
:
value
;
return
value
===
undefined
?
null
:
value
;
}
}
/**
* Normalize a nested department item before writing it back to the parent
* record. Query responses may contain populated user/reference objects, while
* Record writes must contain only their IDs.
*/
function
normalizeDepartmentItemForWrite
(
item
,
subject
)
{
if
(
!
subject
)
return
item
;
return
departmentBusinessFields
(
subject
).
reduce
((
result
,
field
)
=>
{
if
(
field
.
name
===
'business_unit'
)
return
result
;
if
(
Object
.
prototype
.
hasOwnProperty
.
call
(
item
,
field
.
name
))
{
result
[
field
.
name
]
=
normalizeSnapshotValue
(
item
[
field
.
name
],
field
);
}
return
result
;
},
{
...
item
});
}
function
hasFieldValue
(
item
,
fieldName
)
{
function
hasFieldValue
(
item
,
fieldName
)
{
return
Object
.
prototype
.
hasOwnProperty
.
call
(
item
,
fieldName
)
return
Object
.
prototype
.
hasOwnProperty
.
call
(
item
,
fieldName
)
&&
item
[
fieldName
]
!==
undefined
&&
item
[
fieldName
]
!==
undefined
...
@@ -714,6 +777,20 @@ function mergeTextValues(items, fieldName) {
...
@@ -714,6 +777,20 @@ function mergeTextValues(items, fieldName) {
.
join
(
'
\
n'
);
.
join
(
'
\
n'
);
}
}
function
mergeUniqueTextValues
(
items
,
fieldName
)
{
const
seen
=
new
Set
();
return
items
.
map
((
item
)
=>
item
[
fieldName
])
.
filter
((
value
)
=>
value
!==
undefined
&&
value
!==
null
&&
value
!==
''
)
.
map
((
value
)
=>
String
(
value
).
trim
())
.
filter
((
value
)
=>
{
if
(
!
value
||
seen
.
has
(
value
))
return
false
;
seen
.
add
(
value
);
return
true
;
})
.
join
(
'
\
n'
);
}
function
mergeReferenceValues
(
items
,
fieldName
)
{
function
mergeReferenceValues
(
items
,
fieldName
)
{
return
uniqueValues
(
items
.
flatMap
((
item
)
=>
normalizeIds
(
item
[
fieldName
])));
return
uniqueValues
(
items
.
flatMap
((
item
)
=>
normalizeIds
(
item
[
fieldName
])));
}
}
...
@@ -728,6 +805,9 @@ function mergeDepartmentBusinessValue(items, field) {
...
@@ -728,6 +805,9 @@ function mergeDepartmentBusinessValue(items, field) {
}
}
if
([
'text'
,
'long_text'
,
'rich_text'
].
includes
(
fieldType
))
{
if
([
'text'
,
'long_text'
,
'rich_text'
].
includes
(
fieldType
))
{
if
(
fieldName
===
'integrity_risk_points'
)
{
return
mergeUniqueTextValues
(
valuedItems
,
fieldName
);
}
return
mergeTextValues
(
valuedItems
,
fieldName
);
return
mergeTextValues
(
valuedItems
,
fieldName
);
}
}
...
@@ -776,6 +856,31 @@ function buildDepartmentSnapshot(record, assignment, subject, currentUserId, now
...
@@ -776,6 +856,31 @@ function buildDepartmentSnapshot(record, assignment, subject, currentUserId, now
};
};
}
}
function
buildDepartmentResubmissionSnapshot
(
item
,
assignment
,
subject
,
currentUserId
,
now
)
{
return
{
...
normalizeDepartmentItemForWrite
(
item
,
subject
),
item_id
:
item
.
item_id
,
business_unit
:
assignment
.
business_unit
||
item
.
business_unit
,
editor
:
normalizeId
(
assignment
.
editor
),
auditor
:
normalizeId
(
assignment
.
auditor
),
sender
:
currentUserId
,
editor_status
:
FLOW_STATUS
.
incomplete
,
auditor_status
:
FLOW_STATUS
.
pending
,
sender_status
:
FLOW_STATUS
.
pending
,
progress_id_editor
:
''
,
progress_id_auditor
:
''
,
progress_id_sender
:
''
,
flow_record_id
:
''
,
last_pushed_at
:
now
,
submitted_at
:
null
,
auditor_decided_at
:
null
,
sender_decided_at
:
null
,
merged_at
:
null
,
updated_by
:
currentUserId
,
updated_at
:
now
,
};
}
function
summarizeApprovalStatus
(
items
,
statusField
)
{
function
summarizeApprovalStatus
(
items
,
statusField
)
{
if
(
items
.
some
((
item
)
=>
item
[
statusField
]
===
FLOW_STATUS
.
rejected
))
{
if
(
items
.
some
((
item
)
=>
item
[
statusField
]
===
FLOW_STATUS
.
rejected
))
{
return
FLOW_STATUS
.
rejected
;
return
FLOW_STATUS
.
rejected
;
...
@@ -795,6 +900,16 @@ function departmentParentSummary(items = []) {
...
@@ -795,6 +900,16 @@ function departmentParentSummary(items = []) {
};
};
}
}
function
departmentBusinessPatch
(
items
,
subject
)
{
const
businessPatch
=
{};
departmentBusinessFields
(
subject
).
forEach
((
field
)
=>
{
if
(
field
.
name
===
'business_unit'
)
return
;
businessPatch
[
field
.
name
]
=
mergeDepartmentBusinessValue
(
items
,
field
);
});
businessPatch
.
business_unit
=
uniqueValues
(
items
.
map
((
item
)
=>
item
.
business_unit
));
return
businessPatch
;
}
function
departmentFlowSummary
(
items
=
[])
{
function
departmentFlowSummary
(
items
=
[])
{
return
{
return
{
editor_status
:
items
.
length
>
0
&&
items
.
every
((
item
)
=>
item
.
editor_status
===
FLOW_STATUS
.
complete
)
editor_status
:
items
.
length
>
0
&&
items
.
every
((
item
)
=>
item
.
editor_status
===
FLOW_STATUS
.
complete
)
...
@@ -823,9 +938,14 @@ async function updateDepartmentFlowSummary(messageRecord) {
...
@@ -823,9 +938,14 @@ async function updateDepartmentFlowSummary(messageRecord) {
});
});
}
}
async
function
mutateDepartmentItem
(
parentId
,
itemId
,
currentUser
,
mutate
)
{
async
function
mutateDepartmentItem
(
parentId
,
itemId
,
currentUser
,
mutate
,
departmentSubject
=
null
)
{
const
[
resolvedDepartmentSubject
,
processSubject
]
=
await
Promise
.
all
([
departmentSubject
||
loadDepartmentSubject
(),
loadProcessSubject
(),
]);
const
latestParent
=
await
loadRecord
(
parentId
);
const
latestParent
=
await
loadRecord
(
parentId
);
const
items
=
normalizeDepartmentControlItems
(
latestParent
);
const
items
=
normalizeDepartmentControlItems
(
latestParent
)
.
map
((
item
)
=>
normalizeDepartmentItemForWrite
(
item
,
resolvedDepartmentSubject
));
const
itemIndex
=
items
.
findIndex
((
item
)
=>
item
.
item_id
===
itemId
);
const
itemIndex
=
items
.
findIndex
((
item
)
=>
item
.
item_id
===
itemId
);
if
(
itemIndex
<
0
)
throw
new
Error
(
'部门填报明细不存在,请刷新后重试。'
);
if
(
itemIndex
<
0
)
throw
new
Error
(
'部门填报明细不存在,请刷新后重试。'
);
const
currentItem
=
items
[
itemIndex
];
const
currentItem
=
items
[
itemIndex
];
...
@@ -841,6 +961,7 @@ async function mutateDepartmentItem(parentId, itemId, currentUser, mutate) {
...
@@ -841,6 +961,7 @@ async function mutateDepartmentItem(parentId, itemId, currentUser, mutate) {
await
updateRecord
(
latestParent
.
_id
||
latestParent
.
name
,
{
await
updateRecord
(
latestParent
.
_id
||
latestParent
.
name
,
{
metadata
:
{
metadata
:
{
...
departmentBusinessPatch
(
nextItems
,
processSubject
),
[
DEPARTMENT_CONTROL_ITEMS_FIELD
]:
nextItems
,
[
DEPARTMENT_CONTROL_ITEMS_FIELD
]:
nextItems
,
...
departmentParentSummary
(
nextItems
),
...
departmentParentSummary
(
nextItems
),
},
},
...
@@ -855,11 +976,13 @@ function requireDepartmentRole(item, role, user) {
...
@@ -855,11 +976,13 @@ function requireDepartmentRole(item, role, user) {
}
}
async
function
mergeDepartmentItemsIntoParent
(
parentId
,
messageRecord
)
{
async
function
mergeDepartmentItemsIntoParent
(
parentId
,
messageRecord
)
{
const
[
latestParent
,
departmentSubject
]
=
await
Promise
.
all
([
const
[
latestParent
,
departmentSubject
,
processSubject
]
=
await
Promise
.
all
([
loadRecord
(
parentId
),
loadRecord
(
parentId
),
loadDepartmentSubject
(),
loadDepartmentSubject
(),
loadProcessSubject
(),
]);
]);
const
items
=
normalizeDepartmentControlItems
(
latestParent
);
const
items
=
normalizeDepartmentControlItems
(
latestParent
)
.
map
((
item
)
=>
normalizeDepartmentItemForWrite
(
item
,
departmentSubject
));
const
scopedItemIds
=
new
Set
(
normalizeIds
(
flowMetadata
(
messageRecord
).
department_item_ids
));
const
scopedItemIds
=
new
Set
(
normalizeIds
(
flowMetadata
(
messageRecord
).
department_item_ids
));
const
scopedItems
=
scopedItemIds
.
size
>
0
const
scopedItems
=
scopedItemIds
.
size
>
0
?
items
.
filter
((
item
)
=>
scopedItemIds
.
has
(
normalizeId
(
item
.
item_id
)))
?
items
.
filter
((
item
)
=>
scopedItemIds
.
has
(
normalizeId
(
item
.
item_id
)))
...
@@ -873,17 +996,12 @@ async function mergeDepartmentItemsIntoParent(parentId, messageRecord) {
...
@@ -873,17 +996,12 @@ async function mergeDepartmentItemsIntoParent(parentId, messageRecord) {
return
false
;
return
false
;
}
}
const
sortedItems
=
[...
scopedI
tems
].
sort
((
left
,
right
)
=>
{
const
sortedItems
=
[...
i
tems
].
sort
((
left
,
right
)
=>
{
const
leftTime
=
dayjs
(
left
.
submitted_at
||
0
).
valueOf
();
const
leftTime
=
dayjs
(
left
.
submitted_at
||
0
).
valueOf
();
const
rightTime
=
dayjs
(
right
.
submitted_at
||
0
).
valueOf
();
const
rightTime
=
dayjs
(
right
.
submitted_at
||
0
).
valueOf
();
return
leftTime
-
rightTime
;
return
leftTime
-
rightTime
;
});
});
const
businessPatch
=
{};
const
businessPatch
=
departmentBusinessPatch
(
sortedItems
,
processSubject
);
departmentBusinessFields
(
departmentSubject
).
forEach
((
field
)
=>
{
if
(
field
.
name
===
'business_unit'
)
return
;
businessPatch
[
field
.
name
]
=
mergeDepartmentBusinessValue
(
sortedItems
,
field
);
});
businessPatch
.
business_unit
=
uniqueValues
(
sortedItems
.
map
((
item
)
=>
item
.
business_unit
));
const
mergedAt
=
dayjs
().
toISOString
();
const
mergedAt
=
dayjs
().
toISOString
();
const
mergedItems
=
items
.
map
((
item
)
=>
(
const
mergedItems
=
items
.
map
((
item
)
=>
(
scopedItemIds
.
size
===
0
||
scopedItemIds
.
has
(
normalizeId
(
item
.
item_id
))
scopedItemIds
.
size
===
0
||
scopedItemIds
.
has
(
normalizeId
(
item
.
item_id
))
...
@@ -895,7 +1013,7 @@ async function mergeDepartmentItemsIntoParent(parentId, messageRecord) {
...
@@ -895,7 +1013,7 @@ async function mergeDepartmentItemsIntoParent(parentId, messageRecord) {
metadata
:
{
metadata
:
{
...
businessPatch
,
...
businessPatch
,
[
DEPARTMENT_CONTROL_ITEMS_FIELD
]:
mergedItems
,
[
DEPARTMENT_CONTROL_ITEMS_FIELD
]:
mergedItems
,
...
departmentParentSummary
(
scop
edItems
),
...
departmentParentSummary
(
merg
edItems
),
},
},
});
});
return
true
;
return
true
;
...
@@ -927,34 +1045,40 @@ async function pushDepartmentControlMeasureForm({
...
@@ -927,34 +1045,40 @@ async function pushDepartmentControlMeasureForm({
recordIds
.
forEach
((
parentId
)
=>
{
recordIds
.
forEach
((
parentId
)
=>
{
const
parent
=
recordsById
.
get
(
parentId
);
const
parent
=
recordsById
.
get
(
parentId
);
const
existingItems
=
normalizeDepartmentControlItems
(
parent
)
;
const
existingItems
=
normalizeDepartmentControlItems
(
parent
)
const
replaceAll
=
existingItems
.
length
===
0
||
allDepartmentItemsApproved
(
existingItems
);
.
map
((
item
)
=>
normalizeDepartmentItemForWrite
(
item
,
departmentSubject
)
);
const
selectedAssignments
=
assignments
.
filter
((
item
)
=>
item
.
parent_id
===
parentId
);
const
selectedAssignments
=
assignments
.
filter
((
item
)
=>
item
.
parent_id
===
parentId
);
if
(
selectedAssignments
.
length
===
0
)
return
;
if
(
selectedAssignments
.
length
===
0
)
return
;
const
selectedItems
=
selectedAssignments
.
map
((
assignment
)
=>
{
const
selectedItems
=
selectedAssignments
.
map
((
assignment
)
=>
{
const
existingItem
=
existingItems
.
find
((
item
)
=>
item
.
item_id
===
assignment
.
item_id
);
const
existingItem
=
existingItems
.
find
((
item
)
=>
if
(
!
replaceAll
&&
existingItem
)
{
item
.
item_id
===
assignment
.
item_id
return
{
||
item
.
business_unit
===
assignment
.
business_unit
,
...
existingItem
,
);
editor
:
normalizeId
(
assignment
.
editor
),
if
(
existingItem
)
{
auditor
:
normalizeId
(
assignment
.
auditor
),
return
buildDepartmentResubmissionSnapshot
(
sender
:
senderId
,
existingItem
,
flow_record_id
:
''
,
assignment
,
last_pushed_at
:
now
,
departmentSubject
,
updated_by
:
senderId
,
senderId
,
updated_at
:
now
,
now
,
}
;
)
;
}
}
return
buildDepartmentSnapshot
(
parent
,
assignment
,
departmentSubject
,
senderId
,
now
);
return
buildDepartmentSnapshot
(
parent
,
assignment
,
departmentSubject
,
senderId
,
now
);
});
});
const
selectedIds
=
new
Set
(
selectedItems
.
map
((
item
)
=>
item
.
item_id
));
const
selectedById
=
new
Map
(
selectedItems
.
map
((
item
)
=>
[
item
.
item_id
,
item
]));
const
nextItems
=
replaceAll
const
nextItems
=
existingItems
.
map
((
item
)
=>
selectedById
.
get
(
item
.
item_id
)
||
item
);
?
selectedItems
selectedItems
.
forEach
((
item
)
=>
{
:
existingItems
.
map
((
item
)
=>
selectedIds
.
has
(
item
.
item_id
)
if
(
!
existingItems
.
some
((
existingItem
)
=>
existingItem
.
item_id
===
item
.
item_id
))
{
?
selectedItems
.
find
((
selected
)
=>
selected
.
item_id
===
item
.
item_id
)
nextItems
.
push
(
item
);
:
item
);
}
plannedByParent
.
set
(
parentId
,
{
parent
,
nextItems
,
selectedItems
});
});
plannedByParent
.
set
(
parentId
,
{
parent
,
nextItems
,
selectedItems
,
previousItemsById
:
new
Map
(
existingItems
.
map
((
item
)
=>
[
item
.
item_id
,
item
])),
});
});
});
if
(
plannedByParent
.
size
===
0
)
{
if
(
plannedByParent
.
size
===
0
)
{
...
@@ -1002,6 +1126,14 @@ async function pushDepartmentControlMeasureForm({
...
@@ -1002,6 +1126,14 @@ async function pushDepartmentControlMeasureForm({
nextItems
.
push
(
item
);
nextItems
.
push
(
item
);
continue
;
continue
;
}
}
const
previousItem
=
plan
.
previousItemsById
.
get
(
item
.
item_id
);
if
(
previousItem
)
{
await
completeProgressTasks
([
previousItem
.
progress_id_editor
,
previousItem
.
progress_id_auditor
,
previousItem
.
progress_id_sender
,
]);
}
const
nextItem
=
{
...
item
,
flow_record_id
:
flowId
,
last_pushed_at
:
now
};
const
nextItem
=
{
...
item
,
flow_record_id
:
flowId
,
last_pushed_at
:
now
};
if
(
item
.
editor_status
!==
FLOW_STATUS
.
complete
)
{
if
(
item
.
editor_status
!==
FLOW_STATUS
.
complete
)
{
await
completeProgressTasks
(
item
.
progress_id_editor
);
await
completeProgressTasks
(
item
.
progress_id_editor
);
...
@@ -1236,6 +1368,7 @@ export async function loadControlMeasureWorkspace(messageId) {
...
@@ -1236,6 +1368,7 @@ export async function loadControlMeasureWorkspace(messageId) {
}
}
function
requireCurrentDepartmentFlow
(
item
,
messageRecord
)
{
function
requireCurrentDepartmentFlow
(
item
,
messageRecord
)
{
if
(
!
messageRecord
)
return
;
const
flowIds
=
[
messageRecord
?.
_id
,
messageRecord
?.
name
].
map
(
normalizeId
).
filter
(
Boolean
);
const
flowIds
=
[
messageRecord
?.
_id
,
messageRecord
?.
name
].
map
(
normalizeId
).
filter
(
Boolean
);
if
(
item
.
flow_record_id
&&
!
flowIds
.
includes
(
normalizeId
(
item
.
flow_record_id
)))
{
if
(
item
.
flow_record_id
&&
!
flowIds
.
includes
(
normalizeId
(
item
.
flow_record_id
)))
{
throw
new
Error
(
'该部门明细已被重新推送,请使用最新流程链接。'
);
throw
new
Error
(
'该部门明细已被重新推送,请使用最新流程链接。'
);
...
@@ -1247,11 +1380,16 @@ export function getDepartmentItemPermissions(item, user) {
...
@@ -1247,11 +1380,16 @@ export function getDepartmentItemPermissions(item, user) {
const
isEditor
=
normalizeId
(
item
?.
editor
)
===
userId
;
const
isEditor
=
normalizeId
(
item
?.
editor
)
===
userId
;
const
isAuditor
=
normalizeId
(
item
?.
auditor
)
===
userId
;
const
isAuditor
=
normalizeId
(
item
?.
auditor
)
===
userId
;
const
isSender
=
normalizeId
(
item
?.
sender
)
===
userId
;
const
isSender
=
normalizeId
(
item
?.
sender
)
===
userId
;
const
canWithdraw
=
isEditor
&&
item
?.
editor_status
===
FLOW_STATUS
.
complete
&&
item
?.
auditor_status
===
FLOW_STATUS
.
pending
&&
!
item
?.
auditor_decided_at
;
return
{
return
{
isEditor
,
isEditor
,
isAuditor
,
isAuditor
,
isSender
,
isSender
,
canEdit
:
(
isSender
&&
item
?.
sender_status
!==
FLOW_STATUS
.
approved
)
canWithdraw
,
canEdit
:
isSender
||
(
isEditor
&&
item
?.
editor_status
!==
FLOW_STATUS
.
complete
)
||
(
isEditor
&&
item
?.
editor_status
!==
FLOW_STATUS
.
complete
)
||
(
||
(
isAuditor
isAuditor
...
@@ -1290,7 +1428,43 @@ export async function saveDepartmentControlItem({
...
@@ -1290,7 +1428,43 @@ export async function saveDepartmentControlItem({
Object
.
entries
(
values
||
{}).
filter
(([
fieldName
])
=>
allowedFields
.
has
(
fieldName
)),
Object
.
entries
(
values
||
{}).
filter
(([
fieldName
])
=>
allowedFields
.
has
(
fieldName
)),
);
);
return
{
...
item
,
...
patch
};
return
{
...
item
,
...
patch
};
},
departmentSubject
);
}
export
async
function
withdrawDepartmentControlItem
({
messageRecord
,
parentId
,
itemId
,
user
,
})
{
const
result
=
await
mutateDepartmentItem
(
parentId
,
itemId
,
user
,
async
(
item
)
=>
{
requireCurrentDepartmentFlow
(
item
,
messageRecord
);
requireDepartmentRole
(
item
,
'editor'
,
user
);
if
(
item
.
editor_status
!==
FLOW_STATUS
.
complete
||
item
.
auditor_status
!==
FLOW_STATUS
.
pending
||
item
.
auditor_decided_at
)
{
throw
new
Error
(
'审批人已处理或当前明细不在可撤回阶段。'
);
}
await
completeProgressTasks
(
item
.
progress_id_auditor
);
return
{
...
item
,
editor_status
:
FLOW_STATUS
.
incomplete
,
auditor_status
:
FLOW_STATUS
.
pending
,
sender_status
:
FLOW_STATUS
.
pending
,
progress_id_editor
:
''
,
progress_id_auditor
:
''
,
progress_id_sender
:
''
,
submitted_at
:
null
,
auditor_decided_at
:
null
,
sender_decided_at
:
null
,
merged_at
:
null
,
};
});
});
await
updateDepartmentFlowSummary
(
messageRecord
);
return
result
;
}
}
export
async
function
submitDepartmentControlItem
({
export
async
function
submitDepartmentControlItem
({
...
@@ -1486,6 +1660,40 @@ export async function finalRejectDepartmentControlItem({
...
@@ -1486,6 +1660,40 @@ export async function finalRejectDepartmentControlItem({
return
result
;
return
result
;
}
}
export
async
function
withdrawControlMeasureFlow
(
record
,
user
)
{
const
latest
=
await
latestFlowRecord
(
record
);
requireFlowRole
(
latest
,
'editor'
,
user
);
const
metadata
=
flowMetadata
(
latest
);
requireFlowStage
(
metadata
.
editor_status
===
FLOW_STATUS
.
complete
&&
metadata
.
auditor_status
===
FLOW_STATUS
.
pending
,
'审批人已处理或当前流程不在可撤回阶段。'
,
);
await
completeProgressTasks
(
metadata
.
progress_id_auditor
);
const
recordIds
=
flowRecordIds
(
latest
);
if
(
recordIds
.
length
>
0
)
{
await
batchUpdateRecords
({
subject
:
metadata
.
subject_name
||
INTEGRITY_RISK_SUBJECTS
.
process
,
filter
:
{
_id
:
{
$in
:
recordIds
}
},
data
:
{
'metadata.approval_status'
:
FLOW_STATUS
.
pending
,
'metadata.final_approval_status'
:
FLOW_STATUS
.
pending
,
},
});
}
await
updateRecord
(
latest
.
_id
||
latest
.
name
,
{
metadata
:
{
editor_status
:
FLOW_STATUS
.
incomplete
,
auditor_status
:
FLOW_STATUS
.
pending
,
sender_status
:
FLOW_STATUS
.
pending
,
progress_id_editor
:
[],
progress_id_auditor
:
[],
progress_id_sender
:
''
,
},
});
}
export
async
function
submitControlMeasureFlow
(
record
,
user
,
returnUrl
)
{
export
async
function
submitControlMeasureFlow
(
record
,
user
,
returnUrl
)
{
const
latest
=
await
latestFlowRecord
(
record
);
const
latest
=
await
latestFlowRecord
(
record
);
requireFlowRole
(
latest
,
'editor'
,
user
);
requireFlowRole
(
latest
,
'editor'
,
user
);
...
...
src/pages/integrity_risk_prevention/IntegrityRiskWorkbenchPage.jsx
View file @
fbbe3f5a
...
@@ -2,6 +2,7 @@ import { Button, Surface } from '@heroui/react';
...
@@ -2,6 +2,7 @@ import { Button, Surface } from '@heroui/react';
import
FileUploadField
from
'@/components/FileUploadField'
;
import
FileUploadField
from
'@/components/FileUploadField'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
SubjectRecordWorkbench
from
'@/components/SubjectRecordWorkbench'
;
import
SubjectRecordWorkbench
from
'@/components/SubjectRecordWorkbench'
;
import
DepartmentControlItemDialog
from
'./DepartmentControlItemDialog'
;
import
{
useUserInfo
}
from
'@/hooks/user'
;
import
{
useUserInfo
}
from
'@/hooks/user'
;
import
{
useMemo
,
useState
}
from
'react'
;
import
{
useMemo
,
useState
}
from
'react'
;
import
{
import
{
...
@@ -13,11 +14,15 @@ import {
...
@@ -13,11 +14,15 @@ import {
exportProcessTable
,
exportProcessTable
,
exportShowTable
,
exportShowTable
,
INTEGRITY_RISK_SUBJECTS
,
INTEGRITY_RISK_SUBJECTS
,
getDepartmentItemPermissions
,
loadDepartmentControlItemSubject
,
saveDepartmentControlItem
,
pushControlMeasureForm
,
pushControlMeasureForm
,
replaceMeasureDescriptionsFromWorkbook
,
replaceMeasureDescriptionsFromWorkbook
,
replaceShowTableFromWorkbook
,
replaceShowTableFromWorkbook
,
normalizeDepartmentControlItems
,
normalizeDepartmentControlItems
,
}
from
'./IntegrityRiskService'
;
}
from
'./IntegrityRiskService'
;
import
{
Edit
,
Eye
}
from
'@/components/AppIcons'
;
import
DepartmentPushDialog
from
'./DepartmentPushDialog'
;
import
DepartmentPushDialog
from
'./DepartmentPushDialog'
;
const
PAGE_CONFIG
=
{
const
PAGE_CONFIG
=
{
...
@@ -90,6 +95,10 @@ export default function IntegrityRiskWorkbenchPage({ pageType }) {
...
@@ -90,6 +95,10 @@ export default function IntegrityRiskWorkbenchPage({ pageType }) {
const
[
importContext
,
setImportContext
]
=
useState
(
null
);
const
[
importContext
,
setImportContext
]
=
useState
(
null
);
const
[
importFile
,
setImportFile
]
=
useState
(
null
);
const
[
importFile
,
setImportFile
]
=
useState
(
null
);
const
[
importType
,
setImportType
]
=
useState
(
''
);
const
[
importType
,
setImportType
]
=
useState
(
''
);
const
[
departmentSubject
,
setDepartmentSubject
]
=
useState
(
null
);
const
[
departmentEditor
,
setDepartmentEditor
]
=
useState
(
null
);
const
[
departmentEditorLoading
,
setDepartmentEditorLoading
]
=
useState
(
false
);
const
[
departmentEditorActionKey
,
setDepartmentEditorActionKey
]
=
useState
(
''
);
const
runAction
=
async
({
key
,
context
,
action
,
successMessage
,
reload
=
false
})
=>
{
const
runAction
=
async
({
key
,
context
,
action
,
successMessage
,
reload
=
false
})
=>
{
setBusyKey
(
key
);
setBusyKey
(
key
);
...
@@ -154,6 +163,17 @@ export default function IntegrityRiskWorkbenchPage({ pageType }) {
...
@@ -154,6 +163,17 @@ export default function IntegrityRiskWorkbenchPage({ pageType }) {
action
:
exportProcessTable
,
action
:
exportProcessTable
,
successMessage
:
(
count
)
=>
`已导出
${
count
}
条过程表记录。`
,
successMessage
:
(
count
)
=>
`已导出
${
count
}
条过程表记录。`
,
}),
}),
},
{
key
:
'export-process-measures'
,
label
:
busyKey
===
'export-process-measures'
?
'导出中...'
:
'导出防控措施'
,
variant
:
'secondary'
,
disabled
:
disabledWhileBusy
,
onClick
:
(
context
)
=>
runAction
({
key
:
'export-process-measures'
,
context
,
action
:
()
=>
exportPreventionMeasures
(
INTEGRITY_RISK_SUBJECTS
.
process
),
successMessage
:
(
count
)
=>
`已导出
${
count
}
条去重后的防控措施。`
,
}),
}],
}],
bulkActions
:
[
bulkActions
:
[
{
{
...
@@ -303,6 +323,105 @@ export default function IntegrityRiskWorkbenchPage({ pageType }) {
...
@@ -303,6 +323,105 @@ export default function IntegrityRiskWorkbenchPage({ pageType }) {
if
(
succeeded
)
setPushContext
(
null
);
if
(
succeeded
)
setPushContext
(
null
);
};
};
const
resolveDepartmentItemContext
=
(
context
)
=>
{
const
parentRecord
=
context
?.
parentRecord
?.
raw
||
context
?.
parentRecord
||
context
?.
record
?.
parentRecord
?.
raw
||
context
?.
record
?.
parentRecord
||
context
?.
record
?.
raw
||
context
?.
record
;
const
parentId
=
parentRecord
?.
_id
||
parentRecord
?.
id
||
parentRecord
?.
name
;
const
expandedRecord
=
context
?.
record
?.
raw
||
context
?.
record
;
const
itemId
=
expandedRecord
?.
metadata
?.
item_id
||
expandedRecord
?.
item_id
;
const
item
=
normalizeDepartmentControlItems
(
parentRecord
)
.
find
((
candidate
)
=>
candidate
.
item_id
===
itemId
);
return
{
parentRecord
,
parentId
,
item
};
};
const
openDepartmentItem
=
async
(
context
,
readonly
=
false
)
=>
{
const
{
parentId
,
item
}
=
resolveDepartmentItemContext
(
context
);
if
(
!
parentId
||
!
item
)
{
context
?.
setError
?.(
'找不到对应的部门子项,请刷新主界面后重试。'
);
return
;
}
const
permissions
=
getDepartmentItemPermissions
(
item
,
userInfo
);
if
(
!
readonly
&&
!
permissions
.
canEdit
)
{
context
?.
setError
?.(
'当前用户没有编辑该部门子项的权限。'
);
return
;
}
setDepartmentEditorLoading
(
true
);
context
?.
setError
?.(
''
);
try
{
const
subject
=
departmentSubject
||
await
loadDepartmentControlItemSubject
();
setDepartmentSubject
(
subject
);
setDepartmentEditor
({
item
,
parentId
,
readonly
:
readonly
||
!
permissions
.
canEdit
,
setError
:
context
?.
setError
,
setSuccess
:
context
?.
setSuccess
,
reload
:
context
?.
reload
,
});
}
catch
(
error
)
{
context
?.
setError
?.(
error
?.
message
||
'部门子项结构加载失败,请稍后重试。'
);
}
finally
{
setDepartmentEditorLoading
(
false
);
}
};
const
expandedRowActions
=
pageType
===
'process'
?
[
{
key
:
'edit-department-item'
,
label
:
'编辑子项'
,
icon
:
Edit
,
hidden
:
(
context
)
=>
{
const
{
item
}
=
resolveDepartmentItemContext
(
context
);
return
!
item
||
!
getDepartmentItemPermissions
(
item
,
userInfo
).
canEdit
;
},
disabled
:
()
=>
departmentEditorLoading
||
Boolean
(
departmentEditorActionKey
),
onClick
:
(
context
)
=>
openDepartmentItem
(
context
,
false
),
},
{
key
:
'view-department-item'
,
label
:
'查看子项'
,
icon
:
Eye
,
hidden
:
(
context
)
=>
{
const
{
item
}
=
resolveDepartmentItemContext
(
context
);
return
!
item
||
getDepartmentItemPermissions
(
item
,
userInfo
).
canEdit
;
},
disabled
:
()
=>
departmentEditorLoading
||
Boolean
(
departmentEditorActionKey
),
onClick
:
(
context
)
=>
openDepartmentItem
(
context
,
true
),
},
]
:
[];
const
closeDepartmentEditor
=
()
=>
{
if
(
departmentEditorActionKey
)
return
;
setDepartmentEditor
(
null
);
};
const
handleDepartmentItemSave
=
async
(
values
)
=>
{
if
(
!
departmentEditor
)
return
;
const
actionKey
=
`save-department-
${
departmentEditor
.
item
.
item_id
}
`
;
setDepartmentEditorActionKey
(
actionKey
);
departmentEditor
.
setError
?.(
''
);
departmentEditor
.
setSuccess
?.(
''
);
try
{
await
saveDepartmentControlItem
({
parentId
:
departmentEditor
.
parentId
,
itemId
:
departmentEditor
.
item
.
item_id
,
values
,
user
:
userInfo
,
});
departmentEditor
.
setSuccess
?.(
'部门子项已保存,主表数据已自动合并。'
);
await
departmentEditor
.
reload
?.({
keepFeedback
:
true
});
setDepartmentEditor
(
null
);
}
catch
(
error
)
{
departmentEditor
.
setError
?.(
error
?.
message
||
'部门子项保存失败,请稍后重试。'
);
}
finally
{
setDepartmentEditorActionKey
(
''
);
}
};
return
(
return
(
<>
<>
<
SubjectRecordWorkbench
<
SubjectRecordWorkbench
...
@@ -319,6 +438,7 @@ export default function IntegrityRiskWorkbenchPage({ pageType }) {
...
@@ -319,6 +438,7 @@ export default function IntegrityRiskWorkbenchPage({ pageType }) {
hiddenColumnFields=
{
pageConfig
.
hiddenColumnFields
||
[]
}
hiddenColumnFields=
{
pageConfig
.
hiddenColumnFields
||
[]
}
toolbarActions=
{
toolbarActions
}
toolbarActions=
{
toolbarActions
}
bulkActions=
{
bulkActions
}
bulkActions=
{
bulkActions
}
expandedRowActions=
{
expandedRowActions
}
detailDrawerProps=
{
{
detailDrawerProps=
{
{
title
:
`${pageConfig.title}详情`
,
title
:
`${pageConfig.title}详情`
,
readonly
:
pageConfig
.
readonly
,
readonly
:
pageConfig
.
readonly
,
...
@@ -357,6 +477,15 @@ export default function IntegrityRiskWorkbenchPage({ pageType }) {
...
@@ -357,6 +477,15 @@ export default function IntegrityRiskWorkbenchPage({ pageType }) {
onClose=
{
()
=>
setPushContext
(
null
)
}
onClose=
{
()
=>
setPushContext
(
null
)
}
onSubmit=
{
confirmPush
}
onSubmit=
{
confirmPush
}
/>
/>
<
DepartmentControlItemDialog
open=
{
Boolean
(
departmentEditor
)
}
item=
{
departmentEditor
?.
item
}
subject=
{
departmentSubject
}
readonly=
{
Boolean
(
departmentEditor
?.
readonly
)
}
loading=
{
Boolean
(
departmentEditorActionKey
)
}
onClose=
{
closeDepartmentEditor
}
onSave=
{
handleDepartmentItemSave
}
/>
<
ImportConfirmDialog
<
ImportConfirmDialog
open=
{
Boolean
(
importContext
)
}
open=
{
Boolean
(
importContext
)
}
file=
{
importFile
}
file=
{
importFile
}
...
...
src/pages/integrity_risk_prevention/integrity-risk-prevention.business-flow.md
View file @
fbbe3f5a
...
@@ -29,8 +29,8 @@
...
@@ -29,8 +29,8 @@
点击只切换该组列的显示状态。分组字段表头同时展示分组名和字段名,不改变查询或记录数据。
点击只切换该组列的显示状态。分组字段表头同时展示分组名和字段名,不改变查询或记录数据。
3.
详情继续使用当前
`RecordDetailDrawer`
,列表页不复制或改写详情抽屉逻辑;可编辑页面
3.
详情继续使用当前
`RecordDetailDrawer`
,列表页不复制或改写详情抽屉逻辑;可编辑页面
的字段维护统一从详情侧栏进入。
的字段维护统一从详情侧栏进入。
4.
新建、复制和软删除沿用工作台既有行为;列表行内不提供独立编辑按钮,业务字段写入
4.
新建、复制和软删除沿用工作台既有行为;
主对象
列表行内不提供独立编辑按钮,业务字段写入
`metadata`
。
`metadata`
。
过程表展开后的部门子项行提供“编辑子项”或“查看子项”操作,不复用主对象详情编辑。
5.
归档页为只读,不提供新建、编辑、复制或删除。
5.
归档页为只读,不提供新建、编辑、复制或删除。
6.
页面请求必须展示加载、成功和错误状态;删除与替换导入必须提供确认。删除确认弹窗
6.
页面请求必须展示加载、成功和错误状态;删除与替换导入必须提供确认。删除确认弹窗
仅展示选中记录数量和软删除说明,不展示记录标题列表。
仅展示选中记录数量和软删除说明,不展示记录标题列表。
...
@@ -50,13 +50,17 @@
...
@@ -50,13 +50,17 @@
`file_name_and_document_number`
多值引用
`internal_regulation`
,
`file_name_and_document_number`
多值引用
`internal_regulation`
,
`subsidiary_system`
多值引用
`subsidiary_system`
,写入时均只保存
`_id[]`
。
`subsidiary_system`
多值引用
`subsidiary_system`
,写入时均只保存
`_id[]`
。
`risk_level`
是公式字段,由
`probability * hazard_degree`
计算,页面不手工写入。
`risk_level`
是公式字段,由
`probability * hazard_degree`
计算,页面不手工写入。
列表和详情请求必须显式使用
`calculated=true`
,公式字段在表格、详情和新建/编辑弹窗中以只读结果展示;
弹窗中的概率或危害程度变更会即时刷新风险等级预览,提交时不会把公式结果写回
`metadata`
。
12.
展示表
`risk_prevention_control_measures_table_show`
的真实定义已确认包含 18 个字段;
12.
展示表
`risk_prevention_control_measures_table_show`
的真实定义已确认包含 18 个字段;
不包含填报/审批用户字段,也不包含引用字段。导入后的制度名称、子公司制度、责任单位
不包含填报/审批用户字段,也不包含引用字段。导入后的制度名称、子公司制度、责任单位
均保存展示文本或长文本,
`risk_level`
同样为公式字段,页面不手工写入。
均保存展示文本或长文本,
`risk_level`
同样为公式字段,页面不手工写入;列表和详情读取
公式结果时同样必须显式使用
`calculated=true`
。
13.
措施库
`measure_description`
的真实定义已确认只包含
`description`
一个文本字段,
13.
措施库
`measure_description`
的真实定义已确认只包含
`description`
一个文本字段,
导入、导出、归档均以
`metadata.description`
作为措施正文。
导入、导出、归档均以
`metadata.description`
作为措施正文。
14.
过程表归档对象
`risk_prevention_control_measures_table_archive`
的真实定义已确认包含
14.
过程表归档对象
`risk_prevention_control_measures_table_archive`
的主对象快照定义已确认包含
22 个字段;部分过程表多值字段在快照中保存为展示文本,制度引用和用户字段仍写
`_id[]`
。
22 个字段;归档业务在此基础上同时保留对应的
`department_control_items[]`
子对象快照。
主对象部分的多值字段在快照中保存为展示文本,制度引用和用户字段仍写
`_id[]`
。
15.
展示表归档对象
`risk_prevention_control_measures_table_show_archive`
的真实定义已确认包含
15.
展示表归档对象
`risk_prevention_control_measures_table_show_archive`
的真实定义已确认包含
18 个字段;制度名称、子公司制度、责任单位和涉及范围均保存展示文本,不写引用或用户 ID。
18 个字段;制度名称、子公司制度、责任单位和涉及范围均保存展示文本,不写引用或用户 ID。
16.
措施库历史对象
`measure_description_history`
的真实定义已确认只包含
`description`
16.
措施库历史对象
`measure_description_history`
的真实定义已确认只包含
`description`
...
@@ -70,9 +74,16 @@
...
@@ -70,9 +74,16 @@
`reference_record_id`
与当前页面兼容写入的
`reference_id`
需保持注意。
`reference_record_id`
与当前页面兼容写入的
`reference_id`
需保持注意。
19.
过程表列表不直接展示内部嵌套字段
`department_control_items`
;部门展开行中的用户和引用
19.
过程表列表不直接展示内部嵌套字段
`department_control_items`
;部门展开行中的用户和引用
字段仍保存 ID,但列表批量解析并显示用户名称、记录标题,解析失败时才回退显示 ID。
字段仍保存 ID,但列表批量解析并显示用户名称、记录标题,解析失败时才回退显示 ID。
用户点击父行左侧展开按钮后,每个责任单位显示为一条部门子项行。
20.
详情编辑中的用户字段会按已保存的用户 ID 补查用户信息,不受首屏用户列表范围限制;
20.
详情编辑中的用户字段会按已保存的用户 ID 补查用户信息,不受首屏用户列表范围限制;
展开下拉框时已选用户始终置顶并保持原选中顺序。补查失败时保留原 ID 选择值并显示不可用状态,
展开下拉框时已选用户始终置顶并保持原选中顺序。补查失败时保留原 ID 选择值并显示不可用状态,
不因回显失败自动清空;保存时仍只写入
`_id`
/
`_id[]`
。
不因回显失败自动清空;保存时仍只写入
`_id`
/
`_id[]`
。
21.
部门展开行的操作列按当前用户和子项流程阶段显示“编辑子项”或“查看子项”。编辑入口按
`item_id`
从父记录最新的
`department_control_items[]`
中定位真实子项,使用运行时加载的
`risk_prevention_control_measures_table_department_item`
Subject 动态渲染字段;保存后立即
合并全部子项到主对象并刷新过程表。找不到对应
`item_id`
时终止操作并提示刷新,禁止误用
展开行包装数据、其他部门子项或主对象合并结果。
>>>>>>> eaef1ed (feat: support department item editing and risk workflow)
### 2.1 Subject View 授权
### 2.1 Subject View 授权
...
@@ -117,27 +128,48 @@
...
@@ -117,27 +128,48 @@
### 3.1 推送风险防控表
### 3.1 推送风险防控表
1.
用户勾选一条或多条过程表记录,点击“推送风险防控表”。
1.
用户勾选一条或多条过程表记录,点击“推送风险防控表”。
2.
系统按每条记录的
`business_unit`
生成责任单位配置行;每行分别选择 1 名填报人和
2.
系统按每条风险记录的
`business_unit`
拆分推送明细:1 个责任单位对应 1 条
1 名审批人,可删除无需参与本次推送的责任单位。
`department_control_items[]`
子项和 1 条独立的填报/审批链路,3 个责任单位即生成 3 条
部门明细。一次操作可以用同一个
`message_reference_id`
流程主单关联多条部门明细,但
不合并部门的权限、填报数据或审批状态。
3.
每行分别选择 1 名填报人和 1 名审批人,可删除无需参与本次推送的责任单位。所有参与人
都可以查看本次推送涉及的其他部门明细;编辑和流程操作只对当前用户承担角色的子项开放,
不得越权修改其他人员负责的子项。
推送弹窗采用与问题整改推送一致的紧凑配置方式:责任单位列表在弹窗内独立滚动,用户
推送弹窗采用与问题整改推送一致的紧凑配置方式:责任单位列表在弹窗内独立滚动,用户
选择使用搜索下拉浮层,所选风险记录在下方摘要展示,避免用户列表或多条责任单位把弹窗
选择使用搜索下拉浮层,所选风险记录在下方摘要展示,避免用户列表或多条责任单位把弹窗
直接撑长。
直接撑长。
3.
首次推送或历史子项全部终审通过时,从主表业务字段生成新的
4.
推送数据来源按场景区分:
`metadata.department_control_items[]`
快照;存在任意未终审子项时沿用当前子项数据,
-
首次推送且不存在对应部门子项时,使用父过程表当前业务字段仅初始化该责任单位的新
只更新本次配置的人员、流程主单 ID 和待办。
部门快照,并将新子项追加到现有
`department_control_items[]`
;不得因为创建新子项而替换、
4.
创建
`message_reference_id`
流程主单:
`restriction_value`
保存父过程表 ID,
清空或重置其他部门子项。
-
第二年重新推送、同一风险点再次推送,或只向某个人/某个责任单位单独推送时,必须先按
本次推送的责任单位匹配
`metadata.department_control_items[]`
中的对应部门子项,再使用
该部门子项保存的原始业务数据;不得跨部门取数,也不得从已经合并的主对象反向生成,
避免把其他部门的合并结果带入本次推送。
-
如果本次责任单位没有匹配到已有部门子项,不能误用其他部门子项或将其他部门数据带入;
应将该责任单位视为首次推送,使用父过程表当前业务字段创建新的部门子项,并保留父记录
中其他部门子项的全部业务数据、人员配置、审批状态、流程关联和时间字段。
-
只重新推送选定的子项;未选中的部门子项及其流程状态不受影响。
5.
创建
`message_reference_id`
流程主单:
`restriction_value`
保存父过程表 ID,
`department_item_ids`
保存本次子项
`item_id[]`
,参与人字段保存去重后的用户 ID。
`department_item_ids`
保存本次子项
`item_id[]`
,参与人字段保存去重后的用户 ID。
5.
根据每条子项当前阶段创建填报、审批或最终审批 OA 待办,并将待办 ID 写入对应子项。
6.
根据每条子项当前阶段创建填报、审批或最终审批 OA 待办,并将待办 ID 写入对应子项。
6.
主表
`filled_by`
、
`approved_by`
、
`approval_status`
、
`final_approval_status`
由全部子项汇总,
7.
主表
`filled_by`
、
`approved_by`
、
`approval_status`
、
`final_approval_status`
由全部子项汇总;
业务字段在全部子项最终审批完成前不修改。
每次子项保存都会将全部子项当前已保存的业务字段合并到主对象,最终审批只负责完成本次流程
7.
创建并发送站内通知,链接指向
`/control_measure_form?id=<流程主单>`
。
的合并标记。
8.
任一步失败时页面显示错误,不显示成功状态;已成功创建的外部 OA/通知不具备事务
8.
创建并发送站内通知,链接指向
`/control_measure_form?id=<流程主单>`
。
9.
任一步失败时页面显示错误,不显示成功状态;已成功创建的外部 OA/通知不具备事务
回滚能力,需要管理员按错误信息核查。
回滚能力,需要管理员按错误信息核查。
### 3.2 导出
### 3.2 导出
-
“导出廉洁风险防控表(过程表)”查询全部过程表记录,按
`original_index`
升序导出;
-
“导出廉洁风险防控表(过程表)”查询全部过程表记录,按
`original_index`
升序导出;
空序号记录排在最后。
空序号记录排在最后。
-
过程表工具栏提供“导出防控措施”,查询全部过程表记录,按与展示表相同的规则从“主要防控措施描述”
中按行拆分、去除行首
`数字.`
、去空并去重,生成“廉洁风险防控措施.xlsx”。
-
导出数据以过程表主对象当前
`metadata`
为准。部门子项是填报和审批的原始来源,不能
直接替代主对象作为导出数据源。
-
导出不读取或展开
`metadata.department_control_items[]`
;子对象中的分部门填报、审批和
流程控制字段不会单独生成导出行。
-
引用和多值字段导出为显示文本,不把对象写入 Excel。
-
引用和多值字段导出为显示文本,不把对象写入 Excel。
### 3.3 归档
### 3.3 归档
...
@@ -149,7 +181,11 @@
...
@@ -149,7 +181,11 @@
`subsidiary_system`
、
`filled_by`
、
`approved_by`
保存
`_id[]`
。
`subsidiary_system`
、
`filled_by`
、
`approved_by`
保存
`_id[]`
。
4.
创建
`integrity_risk_control_archive`
批次,通过
`metadata.type=廉洁风险点`
4.
创建
`integrity_risk_control_archive`
批次,通过
`metadata.type=廉洁风险点`
和
`metadata.reference_record`
关联快照。
和
`metadata.reference_record`
关联快照。
5.
归档只创建快照,不删除过程表原记录。
5.
归档前,业务老师可以修改已经合并到主对象的内容。归档时同时保存两部分快照:
过程表主对象当前业务数据,以及该记录当时的
`metadata.department_control_items[]`
子对象完整数据(包括各部门原始业务字段、人员、
权限范围、审批状态、流程主单/待办 ID 和时间字段)。归档只创建快照,不删除过程表原记录,
也不把主对象的人工修改反写到部门子项。
## 4. 展示表操作
## 4. 展示表操作
...
@@ -196,46 +232,89 @@
...
@@ -196,46 +232,89 @@
```
text
```
text
按责任单位发起推送
按责任单位发起推送
-> 各部门填报人分别送审
-> 各部门填报人分别送审
-> 审批人尚未处理时,填报人可撤回审批
-> 完成审批人 OA 待办,当前部门明细恢复为填报人可填写
-> 各部门审批人分别审批
-> 各部门审批人分别审批
-> 发起人分别完成各部门最终审批
-> 发起人分别完成各部门最终审批
-> 全部当前子项终审通过后自动合并主表
-> 任一子项保存时,将全部子项当前已保存内容同步合并到主表
-> 全部当前子项终审通过后完成本次流程的最终合并标记
-> 推送人(最终审批人)可修订子项业务字段,保存时再次自动合并主表
-> 业务老师修改主对象后归档:同时保存主对象和部门子对象快照
-> 导出时只读取主对象数据,不读取部门子对象
一级审核驳回 -> 填报人重新填写 -> 再次送审
一级审核驳回 -> 填报人重新填写 -> 再次送审
最终审核驳回 -> 填报人重新填写 -> 再次送审 -> 再次一级审核
最终审核驳回 -> 填报人重新填写 -> 再次送审 -> 再次一级审核
```
```
### 6.1 填报人送审
### 6.1 保存与主数据合并
-
每次保存部门子项时,先读取最新父记录,再仅替换当前
`item_id`
对应的子项,避免覆盖
其他部门刚刚保存的数据;保存完成后,系统基于父记录中全部部门子项的最新内容同步更新
主对象业务字段。
-
合并只处理业务字段;责任单位、填报/审批人员、状态、流程主单 ID、OA 待办 ID 和各类
时间字段仍由流程控制,不因业务字段合并而被覆盖。
-
文本、长文本和富文本按子项内容合并;引用字段合并
`_id[]`
并去重;多值普通字段合并
后去重;其他单值字段取最后一个有值子项。
-
同一父级风险记录下,如果多个部门子项提交的
`integrity_risk_points`
内容完全一致,
主对象只保留一份风险点内容,不重复拼接;去重不影响各部门子项本身的人员、权限和审批
状态。内容不一致的风险点仍按上述规则合并。
-
主对象是合并结果和业务老师后续维护的对象;业务老师直接修改主对象时不反写部门子项。
但推送人(最终审批人)对部门子项业务字段的修订,必须通过子项保存流程自动再次同步到
主对象。
### 6.2 填报人送审
-
仅子项
`editor`
可操作该行,填写范围为子 Subject 中的业务字段;责任单位、人员、
-
仅子项
`editor`
可操作该行,填写范围为子 Subject 中的业务字段;责任单位、人员、
状态和待办字段不开放编辑。
状态和待办字段不开放编辑。
-
保存时先读取最新父记录,再按
`item_id`
替换当前子项,避免覆盖其他部门数据。
-
保存时先读取最新父记录,再按
`item_id`
替换当前子项,避免覆盖其他部门数据。
-
送审后完成该子项填报待办,创建对应
`auditor`
的审批待办。
-
送审后完成该子项填报待办,创建对应
`auditor`
的审批待办。
### 6.2 一级审核
### 6.3 填报人撤回审批
-
仅当前子项的
`editor`
可以撤回自己提交的审批;其他部门填报人、审批人和普通查看者
不得代为撤回。
-
只有在该子项已送审且审批人尚未作出审批决定时才允许撤回,即
`editor_status=已完成`
且
`auditor_status=未审批`
。审批人已通过或已驳回后不能再撤回,
应按对应的审批结果继续后续流程。
-
撤回操作先将
`progress_id_auditor`
指向的审批人 OA 待办设置为已完成,再更新当前子项
状态:
`editor_status`
恢复为
`未完成`
,
`auditor_status`
保持/重置为
`未审批`
,表单重新
对填报人开放业务字段编辑。已保存的业务数据保留,不影响其他部门子项。
-
撤回不创建新的填报人 OA 待办,也不启动新的 OA 流程;只恢复填报状态并清空当前审批待办
引用。重新填写完成后,填报人可以再次送审,由送审操作创建新的审批待办。
-
若关闭审批待办或更新子项状态任一步失败,页面显示错误,不得提示撤回成功;管理员需核查
OA 待办和当前子项状态是否一致。
-
兼容的非部门流程提供同样的撤回入口:撤回成功后同步将关联主记录的一级/最终审批状态
恢复为
`未审批`
,并恢复填报流程状态;同样不创建新的填报人 OA 待办。
### 6.4 一级审核
-
仅当前子项
`auditor`
可操作,且该子项填写状态必须为已完成。
-
仅当前子项
`auditor`
可操作,且该子项填写状态必须为已完成。
-
通过只更新当前子项并创建
`sender`
的最终审批待办;驳回只把当前子项退回填报人,
-
通过只更新当前子项并创建
`sender`
的最终审批待办;驳回只把当前子项退回填报人,
其他部门状态不受影响。
其他部门状态不受影响。
### 6.
3
最终审核
### 6.
5
最终审核
-
仅当前子项
`sender`
可操作,且该子项一级审批必须通过。
-
仅当前子项
`sender`
可操作,且该子项一级审批必须通过。
-
单个部门终审通过时只更新当前子项;本次流程
`department_item_ids`
对应的全部子项
-
单个部门终审通过时只更新当前子项;本次流程
`department_item_ids`
对应的全部子项
`sender_status=已审批`
后才合并主表,历史或未参与本次推送的子项不阻塞本次合并。
`sender_status=已审批`
后完成本次流程的最终合并检查和
`merged_at`
标记,历史或未参与
本次推送的子项不阻塞本次流程。
-
历史流程若已终审但未写入
`merged_at`
,发起人可再次触发最终审批动作补偿合并;
-
历史流程若已终审但未写入
`merged_at`
,发起人可再次触发最终审批动作补偿合并;
已审批状态不会被重复改写。
已审批状态不会被重复改写。
-
合并按
`submitted_at`
升序应用子项业务字段:文本、长文本和富文本按子项值用换行符
`\n`
拼接;
`reference`
字段拼接各子项
`_id[]`
并去重;多值普通字段拼接去重;
其他单值字段取最后一个有值子项。
-
`business_unit`
按全部子项责任单位汇总,公式、人员、状态和待办字段不参与业务字段覆盖;
-
`business_unit`
按全部子项责任单位汇总,公式、人员、状态和待办字段不参与业务字段覆盖;
主表审批状态只由子项审批状态映射汇总,按最落后状态展示。
主表审批状态只由子项审批状态映射汇总,按最落后状态展示。
-
合并后为所有子项写入同一个
`merged_at`
;主表后续修改不会回写子项。
-
完成本次终审后,推送人发现描述或其他业务字段不准确时,可以直接修改其负责的部门子项;
保存会重新执行“保存与主数据合并”,不需要从主对象反向拆分,也不允许修改其他部门子项。
-
合并完成后为本次流程涉及的子项写入同一个
`merged_at`
;主表后续修改不会回写子项。
-
终审驳回只把当前子项退回填报人并重新经历一级审批。
-
终审驳回只把当前子项退回填报人并重新经历一级审批。
### 6.
4
编辑权限
### 6.
6
编辑权限
-
发起人在当前子项终审前可编辑该子项,终审完成后只读。
-
所有参与人都可以查看本次推送的全部部门明细,但只能编辑自己在该子项中承担角色范围内
-
填报人在当前子项送审前可编辑;送审后只读。
的业务字段,不得越权修改其他人的子项或流程控制字段。
-
发起人/最终审批人在当前子项终审阶段及终审完成后的校正场景可编辑该子项业务字段,
保存自动触发主对象合并;责任单位、人员、状态和待办字段仍只读。
-
填报人在当前子项送审前可编辑;送审后,在审批人尚未处理的情况下仍可执行“撤回审批”,
撤回成功后恢复编辑;审批人已通过或已驳回后不能撤回。
-
审批人在当前子项送审后、审批通过前可编辑;审批通过后只读。
-
审批人在当前子项送审后、审批通过前可编辑;审批通过后只读。
-
其他用户只读,且不显示该行流程操作按钮。
-
其他用户只读,且不显示该行流程操作按钮。
...
@@ -243,6 +322,17 @@
...
@@ -243,6 +322,17 @@
-
外部流程链接必须包含流程主单
`id`
;缺失、无权限或记录不存在时显示错误状态。
-
外部流程链接必须包含流程主单
`id`
;缺失、无权限或记录不存在时显示错误状态。
-
推送记录为空、填报人为空、审核人为空时不创建流程。
-
推送记录为空、填报人为空、审核人为空时不创建流程。
-
填报人撤回审批仅限当前子项审批人尚未处理的阶段;必须同步完成审批人的 OA 待办并恢复
`editor_status=未完成`
,不得创建新的填报人 OA 待办,否则不得部分成功。
-
同一父级风险记录下完全相同的
`integrity_risk_points`
只在主对象保留一份;不得因多个
部门提交相同内容而重复拼接。
-
第二年重新推送、同一风险点再次推送或单独推送给某个人时,必须先按责任单位匹配对应的
部门子项,再以该子项保存的原始业务数据为推送来源;不得跨部门取数,也不得以主对象的
合并结果作为已有子项的新数据来源。没有对应部门子项时,将该责任单位视为首次推送,
使用父过程表当前业务字段创建新子项并追加保存;不得因此覆盖、清空或重置其他部门子项
的任何数据或流程状态。
-
归档同时保存主对象当前数据和部门子对象快照;导出只以主对象当前数据为准,不读取或
展开部门子对象。
-
所有用户、引用和归档关联字段只写 ID 字符串或 ID 数组。
-
所有用户、引用和归档关联字段只写 ID 字符串或 ID 数组。
-
选择字段的选项、必填规则和主键规则只使用
`loadSubject`
返回配置,不在页面硬编码。
-
选择字段的选项、必填规则和主键规则只使用
`loadSubject`
返回配置,不在页面硬编码。
-
过程表当前 Subject 定义中
`approval_status`
选项只有
`未审批`
、
`已审批`
;一级审核驳回
-
过程表当前 Subject 定义中
`approval_status`
选项只有
`未审批`
、
`已审批`
;一级审核驳回
...
...
src/services/DataService.js
View file @
fbbe3f5a
...
@@ -167,8 +167,9 @@ function changeSearchIndex(params) {
...
@@ -167,8 +167,9 @@ function changeSearchIndex(params) {
});
});
}
}
function
loadRecord
(
name
)
{
function
loadRecord
(
name
,
calculated
=
true
)
{
return
request
(
`/record/
${
name
}
`
,
{
const
query
=
calculated
?
'?calculated=true'
:
''
;
return
request
(
`/record/
${
name
}${
query
}
`
,
{
method
:
'get'
,
method
:
'get'
,
});
});
}
}
...
@@ -219,6 +220,7 @@ function loadRecords(
...
@@ -219,6 +220,7 @@ function loadRecords(
keyword
,
keyword
,
full
,
full
,
select
,
select
,
calculated
:
true
,
},
},
});
});
}
}
...
@@ -226,7 +228,10 @@ function loadRecords(
...
@@ -226,7 +228,10 @@ function loadRecords(
function
queryRecords
(
query
)
{
function
queryRecords
(
query
)
{
return
request
(
'/record/query'
,
{
return
request
(
'/record/query'
,
{
method
:
'post'
,
method
:
'post'
,
data
:
query
,
data
:
{
...(
query
||
{}),
calculated
:
query
?.
calculated
??
true
,
},
});
});
}
}
...
...
src/services/SubjectWorkbenchService.js
View file @
fbbe3f5a
...
@@ -96,6 +96,7 @@ export async function queryAllSubjectRecords(subjectName, query = {}) {
...
@@ -96,6 +96,7 @@ export async function queryAllSubjectRecords(subjectName, query = {}) {
page
:
0
,
page
:
0
,
page_size
:
QUERY_PAGE_SIZE
,
page_size
:
QUERY_PAGE_SIZE
,
mode
:
'full'
,
mode
:
'full'
,
calculated
:
true
,
sort
:
'-updated_at -created_at'
,
sort
:
'-updated_at -created_at'
,
...
query
,
...
query
,
});
});
...
@@ -111,6 +112,7 @@ export async function queryAllSubjectRecords(subjectName, query = {}) {
...
@@ -111,6 +112,7 @@ export async function queryAllSubjectRecords(subjectName, query = {}) {
page
,
page
,
page_size
:
QUERY_PAGE_SIZE
,
page_size
:
QUERY_PAGE_SIZE
,
mode
:
'full'
,
mode
:
'full'
,
calculated
:
true
,
sort
:
'-updated_at -created_at'
,
sort
:
'-updated_at -created_at'
,
...
query
,
...
query
,
});
});
...
...
src/utils/subjectView.js
View file @
fbbe3f5a
...
@@ -185,6 +185,135 @@ export function getFieldValue(record, fieldPath) {
...
@@ -185,6 +185,135 @@ export function getFieldValue(record, fieldPath) {
return
fieldPath
.
split
(
'.'
).
reduce
((
current
,
key
)
=>
current
?.[
key
],
record
);
return
fieldPath
.
split
(
'.'
).
reduce
((
current
,
key
)
=>
current
?.[
key
],
record
);
}
}
function
isFormulaEmpty
(
value
)
{
return
value
===
undefined
||
value
===
null
||
value
===
''
;
}
function
splitFormulaArguments
(
value
)
{
const
parts
=
[];
let
current
=
''
;
let
depth
=
0
;
let
quote
=
''
;
for
(
const
character
of
String
(
value
||
''
))
{
if
(
quote
)
{
current
+=
character
;
if
(
character
===
quote
)
quote
=
''
;
continue
;
}
if
(
character
===
"'"
||
character
===
'"'
)
{
quote
=
character
;
current
+=
character
;
}
else
if
(
character
===
'('
)
{
depth
+=
1
;
current
+=
character
;
}
else
if
(
character
===
')'
)
{
depth
=
Math
.
max
(
0
,
depth
-
1
);
current
+=
character
;
}
else
if
(
character
===
','
&&
depth
===
0
)
{
parts
.
push
(
current
.
trim
());
current
=
''
;
}
else
{
current
+=
character
;
}
}
if
(
current
.
trim
()
||
parts
.
length
>
0
)
parts
.
push
(
current
.
trim
());
return
parts
;
}
function
unwrapFormulaCall
(
expression
,
functionName
)
{
const
match
=
String
(
expression
||
''
).
trim
().
match
(
new
RegExp
(
`^
${
functionName
}
\\s*\\((.*)\\)$`
,
'i'
),
);
return
match
?
match
[
1
]
:
null
;
}
function
formulaOperandValue
(
record
,
expression
)
{
const
source
=
String
(
expression
||
''
).
trim
();
if
(
!
source
)
return
undefined
;
const
quoted
=
source
.
match
(
/^
([
'"
])(
.*
)\1
$/
s
);
if
(
quoted
)
return
quoted
[
2
];
if
(
/^null$/i
.
test
(
source
))
return
null
;
if
(
/^true$/i
.
test
(
source
))
return
true
;
if
(
/^false$/i
.
test
(
source
))
return
false
;
if
(
/^-
?(?:\d
+
\.?\d
*|
\.\d
+
)
$/
.
test
(
source
))
return
Number
(
source
);
const
productArguments
=
unwrapFormulaCall
(
source
,
'PRODUCT'
);
if
(
productArguments
!==
null
)
{
const
values
=
splitFormulaArguments
(
productArguments
).
map
((
item
)
=>
formulaOperandValue
(
record
,
item
),
);
if
(
values
.
some
((
value
)
=>
isFormulaEmpty
(
value
)))
return
undefined
;
const
numbers
=
values
.
map
(
Number
);
if
(
numbers
.
some
((
value
)
=>
!
Number
.
isFinite
(
value
)))
return
undefined
;
return
numbers
.
reduce
((
result
,
value
)
=>
result
*
value
,
1
);
}
const
fieldReference
=
source
.
match
(
/^
\[([^\]]
+
)\]
$/
);
const
fieldName
=
fieldReference
?
fieldReference
[
1
].
trim
()
:
source
;
const
value
=
getFieldValue
(
record
,
fieldName
)
??
getFieldValue
(
record
,
`metadata.
${
fieldName
}
`
);
return
value
&&
typeof
value
===
'object'
&&
!
Array
.
isArray
(
value
)
?
value
.
value
??
value
.
_id
??
value
.
name
:
value
;
}
/**
* Evaluate the small, declarative formula grammar used by Subject fields.
* It intentionally supports only function calls and literals, never arbitrary
* JavaScript, so formulas from the API cannot execute code in the browser.
*/
export
function
evaluateFormula
(
formula
,
record
)
{
const
source
=
String
(
formula
||
''
).
trim
();
if
(
!
source
)
return
undefined
;
const
switchArguments
=
unwrapFormulaCall
(
source
,
'SWITCH'
);
if
(
switchArguments
!==
null
)
{
const
parts
=
splitFormulaArguments
(
switchArguments
);
if
(
parts
.
length
<
3
)
return
undefined
;
const
switchValue
=
formulaOperandValue
(
record
,
parts
[
0
]);
for
(
let
index
=
1
;
index
+
1
<
parts
.
length
;
index
+=
2
)
{
const
caseValue
=
formulaOperandValue
(
record
,
parts
[
index
]);
if
(
String
(
caseValue
)
===
String
(
switchValue
))
{
return
formulaOperandValue
(
record
,
parts
[
index
+
1
]);
}
}
return
parts
.
length
%
2
===
0
?
formulaOperandValue
(
record
,
parts
[
parts
.
length
-
1
])
:
undefined
;
}
return
formulaOperandValue
(
record
,
source
);
}
export
function
isFormulaFieldType
(
fieldType
)
{
return
normalizeText
(
fieldType
).
toLowerCase
()
===
'formula'
;
}
export
function
getFormulaExpression
(
fieldDef
)
{
return
fieldDef
?.
settings
?.
formula
||
fieldDef
?.
formula
||
''
;
}
export
function
resolveFieldValue
(
record
,
fieldPath
,
fieldDef
)
{
const
currentValue
=
getFieldValue
(
record
,
fieldPath
);
if
(
!
isFormulaFieldType
(
fieldDef
?.
type
))
return
currentValue
;
const
calculatedValue
=
evaluateFormula
(
getFormulaExpression
(
fieldDef
),
record
);
return
calculatedValue
===
undefined
?
currentValue
:
calculatedValue
;
}
export
function
applyFormulaFields
(
record
,
fields
=
[])
{
return
(
fields
||
[]).
reduce
((
nextRecord
,
field
)
=>
{
if
(
!
isFormulaFieldType
(
field
?.
type
))
return
nextRecord
;
const
fieldPath
=
getFieldPath
(
field
);
const
value
=
evaluateFormula
(
getFormulaExpression
(
field
),
nextRecord
);
return
value
===
undefined
?
nextRecord
:
setFieldValue
(
nextRecord
,
fieldPath
,
value
);
},
JSON
.
parse
(
JSON
.
stringify
(
record
||
{})));
}
export
function
setFieldValue
(
record
,
fieldPath
,
value
)
{
export
function
setFieldValue
(
record
,
fieldPath
,
value
)
{
const
nextRecord
=
JSON
.
parse
(
JSON
.
stringify
(
record
||
{}));
const
nextRecord
=
JSON
.
parse
(
JSON
.
stringify
(
record
||
{}));
const
keys
=
fieldPath
.
split
(
'.'
);
const
keys
=
fieldPath
.
split
(
'.'
);
...
...
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