Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
A
audit_manager_new
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
audit_manager_new
Commits
a6c529a8
Commit
a6c529a8
authored
Oct 08, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: unify operation feedback and detail save notifications
parent
c4324c3a
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
33 changed files
with
301 additions
and
133 deletions
+301
-133
xinyuan-design-system.md
docs/xinyuan-design-system.md
+5
-1
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+17
-0
manifest.json
public/manifest.json
+1
-1
app.js
src/app.js
+7
-1
AppFeedback.jsx
src/components/AppFeedback.jsx
+54
-0
AppShell.jsx
src/components/AppShell.jsx
+0
-2
ApplicationSwitcherDialog.jsx
src/components/ApplicationSwitcherDialog.jsx
+4
-15
index.jsx
src/components/record-workbench/index.jsx
+15
-12
feedback.css
src/design-system/feedback.css
+11
-0
useFeedbackState.js
src/hooks/useFeedbackState.js
+33
-0
Login.jsx
src/pages/Login.jsx
+3
-10
ReferenceRecordsTable.jsx
...s/RecordDetailDrawer/components/ReferenceRecordsTable.jsx
+7
-6
ReferenceSearchSelect.jsx
...s/RecordDetailDrawer/components/ReferenceSearchSelect.jsx
+5
-1
TailwindEditableField.jsx
...s/RecordDetailDrawer/components/TailwindEditableField.jsx
+13
-4
useSubjectRecord.js
src/pages/RecordDetailDrawer/hooks/useSubjectRecord.js
+13
-12
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+3
-2
AuditProcess.jsx
src/pages/audit_management/AuditProcess.jsx
+6
-3
AuditReport.jsx
src/pages/audit_management/AuditReport.jsx
+3
-2
AuditReportContent.jsx
src/pages/audit_management/AuditReportContent.jsx
+2
-2
AuditReportDetail.jsx
src/pages/audit_management/AuditReportDetail.jsx
+3
-2
AuditReportFill.jsx
src/pages/audit_management/AuditReportFill.jsx
+3
-2
ControlPointDialog.jsx
src/pages/audit_management/ControlPointDialog.jsx
+4
-1
PageState.jsx
src/pages/audit_management/PageState.jsx
+11
-19
ReferenceSearchSelect.jsx
src/pages/audit_management/ReferenceSearchSelect.jsx
+13
-6
audit-management.business-flow.md
src/pages/audit_management/audit-management.business-flow.md
+12
-2
NotificationListPage.jsx
...s/audit_management/notifications/NotificationListPage.jsx
+16
-19
ProcessDetail.jsx
src/pages/process_monitor/ProcessDetail.jsx
+8
-5
ProcessMonitor.jsx
src/pages/process_monitor/ProcessMonitor.jsx
+0
-0
ProcessTemplateList.jsx
src/pages/process_monitor/ProcessTemplateList.jsx
+0
-0
process-monitor.business-flow.md
src/pages/process_monitor/process-monitor.business-flow.md
+11
-1
tailwind.css
src/tailwind.css
+1
-0
feedback.js
src/utils/feedback.js
+14
-0
Site.jsx
src/wrapper/Site.jsx
+3
-2
No files found.
docs/xinyuan-design-system.md
View file @
a6c529a8
...
...
@@ -62,7 +62,11 @@
-
Pagination 固定业务默认每页 10 条,不显示页容量选择器。
-
查看/表单 Modal 按统一尺寸阶梯选用宽度;普通详情和控制点详情使用 4xl(约 1088px)HeroUI Modal,字段按当前视图顺序单列纵向排列,普通标量字段直接落在 Dialog 背景上,不叠加同层级 Surface;编辑态控件直接在 Overlay 上使用 secondary。控制点查看与编辑共用同一 Modal,查看态底部使用“编辑”进入编辑态,编辑态使用“取消/保存”并保留现有保存回调。
-
结构化表单字段在查看态使用紧凑摘要显示“表单名称 · N 条记录”;后端返回“(无标题)”“(无标题)”等占位标题时回退到字段名称。不得直接把表单 JSON 序列化为详情正文,记录数量必须使用真实数据计算。
-
最终删除确认使用 AlertDialog/Modal + solid danger;例行成功提示使用浮动 Toast。
-
最终删除确认使用 AlertDialog/Modal + solid danger;接口成功、失败与降级警告统一使用应用根部
`AppFeedback`
的 HeroUI Toast,登录页、主布局与独立填报页共享唯一 Provider,不再渲染接口反馈横条。
-
操作提示通过 Portal 挂载到
`body`
,避开业务弹窗对页面根节点的
`inert`
限制,在视口顶部显示,层级 300 高于业务 Modal 和移动侧栏,不随页面滚动;成功默认 4 秒、错误/警告默认 8 秒后消失。长错误自动换行并限制正文滚动高度。
-
关闭按钮同步问题整改
`bfc5862`
:组合 HeroUI
`Button`
(
`ghost`
、
`sm`
、
`isIconOnly`
)和
`AppIcons.Close`
,通过官方
`slot="close"`
关闭提示,位于正文右侧参与布局。前排图标无需悬停即可操作,后排隐藏关闭按钮;使用官方默认 Hover/焦点样式,不再放大带边框的默认 Toast 关闭按钮,不设置绝对位置或正文右侧额外占位。
-
`src/design-system/feedback.css`
仅调整浮层层级、安全区和后排关闭按钮可见性,颜色、圆角、阴影与状态沿用 HeroUI 主题。关闭提示不改变请求失败状态、筛选、页码或表单草稿;加载失败保留可重试的页面状态,必填和 JSON 格式校验保留字段旁反馈。
-
通用记录详情逐字段保存仅在
`updateRecord`
成功后显示“详情已保存。”;失败继续保留编辑草稿。稽核控制点详情沿用父页面的“控制点已保存”反馈,避免重复提示。
## KPI 与图表
...
...
docs/xinyuan-ui-audit.md
View file @
a6c529a8
...
...
@@ -116,3 +116,20 @@
-
应用切换打开态确认 Pro
`ItemCardGroup + ItemCard`
、统一鑫元 Logo、分组数量、跨分组去重、搜索过滤和唯一当前态;详情弹窗宽度符合 4xl 级别。
-
重新加载后的当前页面控制台为 0 error / 0 warning;登录过程中两次错误验证码仅产生后端拒绝日志,不涉及代码或业务数据变更。
-
本轮截图保存在
`/tmp/audit-manager-1440.png`
、
`/tmp/audit-switcher-fixed.png`
、
`/tmp/controlpoint-modal-1440.png`
、
`/tmp/audit-1280x720.png`
、
`/tmp/audit-768x1024.png`
、
`/tmp/audit-390x844.png`
和
`/tmp/audit-mobile-nav.png`
。
## 2026-10-08 操作反馈回归
-
参考问题整改
`513706c`
:根部统一 HeroUI Toast,成功 4 秒、错误/警告 8 秒后消失,关闭按钮始终可见;登录页、主布局和独立填报页共享 Provider。
-
`AppFeedback`
通过 Portal 挂到
`body`
,避免 Modal 将应用根节点设为
`inert`
后导致提示只能看见、不能点击。浏览器验证关闭提示后 Modal、失败状态和草稿保留;相同错误重试重新显示。
-
使用实际源码组件的独立本地验证页完成 Modal 浮层、44px 关闭按钮、成功自动消失、旧 InlineNotice 入口、375px 长错误换行/正文滚动与暗色主题检查;验证页不连接业务 API、不写业务数据。
-
实际
`/audit_report_fill`
缺少参数场景验证:显示顶部错误提示,关闭后仍保留失败页面和重新加载入口,再次重试重新显示相同错误。浏览器未发现 warning/error 日志。
-
`npm run build`
、26 个变更 JavaScript 文件语法解析和
`git diff --check`
通过。当前未取得应用的有效真实登录态,登录后项目保存、报告归档与通知 API 操作未逐项实测;不以本地组件验证替代这些验收。
### 2026-10-08 同步 `bfc5862` 的关闭按钮与详情保存反馈
-
关闭按钮改用 HeroUI
`Button`
的
`ghost / sm / isIconOnly`
与官方
`slot="close"`
,实测尺寸为 32px、透明背景、参与正文右侧布局。前排按钮持续可见,后排隐藏;保留
`body`
Portal 与层级 300。
-
通用详情使用实际
`useSubjectRecord`
、
`TailwindEditableField`
和
`AppFeedback`
源码,在独立本地页面替换 DataService 验证:等待接口响应时无成功提示;响应成功后显示“详情已保存。”并更新快照;响应失败时不显示成功,字段草稿保留且可直接重试。重复成功可再次提示,切换记录后旧请求成功不会覆盖新详情或弹出旧反馈。
-
鼠标点击与 Enter 均可关闭前排提示,关闭后 Modal 和编辑草稿保持不变;三条堆叠提示关闭前排后,下一条显示可操作的关闭按钮。HeroUI 的 Toast View Transition 结束后检查按钮命中,不将过渡快照作为最终交互状态。
-
当前活动入口使用实际
`AuditProcess`
、
`ControlPointDialog`
与
`auditManagementService`
,仅在独立页面替换底层接口与权限 Hook:从“查看控制点 → 编辑 → 保存”验证等待响应无成功提示,失败保留详情及草稿,成功更新列表、关闭编辑弹窗并显示唯一“控制点已保存”,提示可关闭。未向实际应用注入登录状态或写入业务数据。
-
375px 窄屏下长错误换行、正文独立滚动、关闭按钮处于视口内且可命中;暗色主题成功提示可见可关闭。活动控制点表单仍有既存的无可访问标签 warning,本次不将其列为反馈改动引入的错误。
-
生产构建、源码语法解析与
`git diff --check`
通过。真实登录后的保存 API 尚未实测;本轮验证使用本地接口替身,不替代真实业务验收。接口仍为既有
`PUT /record/:name`
,Subject 字段和写入协议不变。
public/manifest.json
View file @
a6c529a8
{
"name"
:
"audit_manager_new"
,
"version"
:
"1.0.
1
"
,
"version"
:
"1.0.
3
"
,
"build"
:
"abc"
,
"description"
:
"监察稽核管理重构版"
,
"date"
:
"2025-07-14 19:27:20"
,
...
...
src/app.js
View file @
a6c529a8
import
'@/tailwind.css'
;
import
'@/global.less'
;
import
{
defineApp
}
from
'umi'
;
import
{
createElement
}
from
'react'
;
import
AppFeedback
from
'@/components/AppFeedback'
;
export
async
function
getInitialState
()
{
return
{
...
...
@@ -8,4 +10,8 @@ export async function getInitialState() {
};
}
export
default
defineApp
({});
export
function
rootContainer
(
container
)
{
return
createElement
(
AppFeedback
,
null
,
container
);
}
export
default
defineApp
({
rootContainer
});
src/components/AppFeedback.jsx
0 → 100644
View file @
a6c529a8
import
{
Button
,
Toast
}
from
"@heroui/react"
;
import
{
Close
}
from
"@/components/AppIcons"
;
import
{
createPortal
}
from
"react-dom"
;
export
default
function
AppFeedback
({
children
})
{
return
(
<>
{
children
}
{
typeof
document
!==
"undefined"
?
createPortal
(
<
Toast
.
Provider
aria
-
label=
"操作提示"
className=
"app-feedback-region"
maxVisibleToasts=
{
3
}
placement=
"top"
width=
"min(30rem, calc(100vw - 2rem))"
>
{
({
toast
})
=>
(
<
Toast
className=
"app-feedback-toast gap-2"
toast=
{
toast
}
variant=
{
toast
.
content
.
variant
}
>
<
Toast
.
Indicator
variant=
{
toast
.
content
.
variant
}
/>
<
Toast
.
Content
className=
"min-w-0"
>
<
Toast
.
Title
>
{
toast
.
content
.
title
}
</
Toast
.
Title
>
{
toast
.
content
.
description
?
(
<
Toast
.
Description
className=
"max-h-[40dvh] overflow-y-auto whitespace-pre-wrap break-words text-foreground [overflow-wrap:anywhere]"
>
{
toast
.
content
.
description
}
</
Toast
.
Description
>
)
:
null
}
{
toast
.
content
.
actionProps
?
(
<
Toast
.
ActionButton
{
...
toast
.
content
.
actionProps
}
/>
)
:
null
}
</
Toast
.
Content
>
<
Button
aria
-
label=
"关闭提示"
className=
"app-feedback-close shrink-0 self-start"
isIconOnly
size=
"sm"
slot=
"close"
variant=
"ghost"
>
<
Close
size=
{
16
}
/>
</
Button
>
</
Toast
>
)
}
</
Toast
.
Provider
>,
document
.
body
,
)
:
null
}
</>
);
}
src/components/AppShell.jsx
View file @
a6c529a8
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Toast
}
from
'@heroui/react'
;
import
{
createContext
,
useCallback
,
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
import
DashboardNavbar
from
'./DashboardNavbar'
;
...
...
@@ -53,7 +52,6 @@ export default function AppShell({
<
div
className=
"rc-app-page"
>
{
children
}
</
div
>
</
div
>
</
Sidebar
.
Main
>
<
Toast
.
Provider
maxVisibleToasts=
{
3
}
placement=
"top"
/>
</
Sidebar
.
Provider
>
</
AppShellHeaderContext
.
Provider
>
);
...
...
src/components/ApplicationSwitcherDialog.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
xinyuanLogo
from
'@/assets/identity/xy-logo-red.png'
;
import
{
ExclamationTriangleIcon
,
MagnifyingGlassIcon
}
from
'@/components/AppIcons'
;
import
{
MagnifyingGlassIcon
}
from
'@/components/AppIcons'
;
import
{
APP_BASE_PATH
,
APP_DISPLAY_NAME
,
APP_ROUTE_NAME
}
from
'@/constants/app'
;
import
{
EmptyStateSurface
}
from
'@/components/DataTable'
;
import
{
getApplications
}
from
'@/services/DataService'
;
...
...
@@ -7,7 +8,6 @@ import { EmptyState } from '@heroui-pro/react/empty-state';
import
{
ItemCard
}
from
'@heroui-pro/react/item-card'
;
import
{
ItemCardGroup
}
from
'@heroui-pro/react/item-card-group'
;
import
{
Alert
,
Avatar
,
Chip
,
Modal
,
...
...
@@ -199,7 +199,7 @@ export default function ApplicationSwitcherDialog({
mergeApplications
([],
fallbackApplications
),
);
const
[
status
,
setStatus
]
=
useState
(
'idle'
);
const
[
errorMessage
,
setErrorMessage
]
=
useState
(
'
'
);
const
[
,
setErrorMessage
]
=
useFeedbackState
(
'warning
'
);
const
[
applicationSearch
,
setApplicationSearch
]
=
useState
(
''
);
const
currentApplicationName
=
useMemo
(()
=>
{
return
(
...
...
@@ -254,7 +254,7 @@ export default function ApplicationSwitcherDialog({
return
()
=>
{
cancelled
=
true
;
};
},
[
fallbackApplications
,
open
]);
},
[
fallbackApplications
,
open
,
setErrorMessage
]);
return
(
<
Modal
.
Root
>
...
...
@@ -292,17 +292,6 @@ export default function ApplicationSwitcherDialog({
</
Modal
.
Header
>
<
Modal
.
Body
className=
"flex min-h-0 overflow-hidden"
style=
{
{
marginTop
:
'0.75rem'
}
}
>
<
ScrollShadow
className=
"h-full min-h-0 flex-1 overflow-y-auto pe-1 pb-1"
hideScrollBar=
{
false
}
offset=
{
12
}
size=
{
24
}
>
{
status
===
'error'
&&
errorMessage
?
(
<
Alert
status=
"warning"
>
<
Alert
.
Indicator
>
<
ExclamationTriangleIcon
size=
{
20
}
/>
</
Alert
.
Indicator
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
errorMessage
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
{
status
===
'loading'
&&
applications
.
length
===
0
?
(
<
LoadingSkeleton
/>
)
:
filteredApplicationSections
.
length
>
0
?
(
...
...
src/components/record-workbench/index.jsx
View file @
a6c529a8
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Checkbox
}
from
'@/catalyst-ui-kit/javascript/checkbox'
;
import
{
Input
}
from
'@/catalyst-ui-kit/javascript/input'
;
import
{
Select
}
from
'@/catalyst-ui-kit/javascript/select'
;
...
...
@@ -483,6 +484,15 @@ export default function RecordWorkbench({
const
[
expandedRowIds
,
setExpandedRowIds
]
=
useState
({});
const
loading
=
Boolean
(
states
?.
loading
);
const
error
=
states
?.
error
||
''
;
const
success
=
states
?.
success
||
''
;
const
warning
=
states
?.
warning
||
''
;
const
[,
setErrorFeedback
]
=
useFeedbackState
(
'error'
);
const
[,
setSuccessFeedback
]
=
useFeedbackState
(
'success'
);
const
[,
setWarningFeedback
]
=
useFeedbackState
(
'warning'
);
useEffect
(()
=>
{
setErrorFeedback
(
error
);
},
[
error
,
setErrorFeedback
]);
useEffect
(()
=>
{
setSuccessFeedback
(
success
);
},
[
success
,
setSuccessFeedback
]);
useEffect
(()
=>
{
setWarningFeedback
(
warning
);
},
[
warning
,
setWarningFeedback
]);
const
pageRecordCount
=
records
.
length
;
const
totalRecords
=
pagination
?.
totalRecords
??
records
.
length
;
const
columnGroupMeta
=
useMemo
(
...
...
@@ -801,18 +811,6 @@ export default function RecordWorkbench({
</
div
>
)
:
null
}
{
error
?
(
<
div
className=
{
clsx
(
'border-b border-zinc-100 bg-rose-50/70 px-6 py-4 text-sm text-rose-700'
,
fixedTablePanelClass
)
}
>
{
error
}
</
div
>
)
:
null
}
{
states
?.
success
?
(
<
div
className=
{
clsx
(
'border-b border-zinc-100 bg-emerald-50/70 px-6 py-4 text-sm text-emerald-700'
,
fixedTablePanelClass
)
}
>
{
states
.
success
}
</
div
>
)
:
null
}
{
loading
?
(
<
div
className=
{
stateContentClass
}
>
<
EmptyState
...
...
@@ -820,6 +818,11 @@ export default function RecordWorkbench({
description=
{
states
?.
loadingDescription
||
'正在读取对象结构、视图配置和记录数据,请稍候。'
}
/>
</
div
>
)
:
error
&&
pageRecordCount
===
0
?
(
<
div
className=
{
stateContentClass
}
>
<
EmptyState
title=
"列表加载失败"
description=
"请重新加载,或调整筛选条件后重试。"
/>
{
states
.
onRetry
?
<
Button
outline
onClick=
{
states
.
onRetry
}
>
重新加载
</
Button
>
:
null
}
</
div
>
)
:
pageRecordCount
===
0
?
(
<
div
className=
{
stateContentClass
}
>
<
EmptyState
...
...
src/design-system/feedback.css
0 → 100644
View file @
a6c529a8
/* Operation feedback stays above Modal and the mobile sidebar, outside page scrolling. */
.app-feedback-region.toast-region
{
z-index
:
300
;
top
:
max
(
1rem
,
env
(
safe-area-inset-top
));
}
/* Only the front toast is interactive; its ghost close button stays visible without hover. */
.app-feedback-toast
:not
([
data-frontmost
=
"true"
])
.app-feedback-close
{
visibility
:
hidden
;
pointer-events
:
none
;
}
src/hooks/useFeedbackState.js
0 → 100644
View file @
a6c529a8
import
{
toast
}
from
"@heroui/react"
;
import
{
showFeedback
}
from
"@/utils/feedback"
;
import
{
useCallback
,
useEffect
,
useRef
,
useState
}
from
"react"
;
// Closing a toast preserves the request's failure state and the form draft.
export
default
function
useFeedbackState
(
tone
=
"error"
)
{
const
[
message
,
setMessage
]
=
useState
(
""
);
const
toastKeyRef
=
useRef
(
null
);
const
isMountedRef
=
useRef
(
true
);
const
updateMessage
=
useCallback
(
(
nextMessage
)
=>
{
if
(
!
isMountedRef
.
current
)
return
;
if
(
toastKeyRef
.
current
)
toast
.
close
(
toastKeyRef
.
current
);
toastKeyRef
.
current
=
null
;
setMessage
(
nextMessage
);
// Retries with the same error must still display new feedback.
toastKeyRef
.
current
=
showFeedback
(
nextMessage
,
tone
);
},
[
tone
],
);
useEffect
(()
=>
{
isMountedRef
.
current
=
true
;
return
()
=>
{
isMountedRef
.
current
=
false
;
if
(
toastKeyRef
.
current
)
toast
.
close
(
toastKeyRef
.
current
);
toastKeyRef
.
current
=
null
;
};
},
[]);
return
[
message
,
updateMessage
];
}
src/pages/Login.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
xyLogoRed
from
'@/assets/identity/xy-logo-red.png'
;
import
xyLogoWhite
from
'@/assets/identity/xy-logo-white.png'
;
import
{
...
...
@@ -7,7 +8,7 @@ import {
import
authService
from
'@/services/authService'
;
import
{
getCaptcha
,
userLogin
}
from
'@/services/DataService'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
Alert
,
Button
,
Card
,
Input
,
Label
,
TextField
}
from
'@heroui/react'
;
import
{
Button
,
Card
,
Input
,
Label
,
TextField
}
from
'@heroui/react'
;
import
CryptoJS
from
'crypto-js'
;
import
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
history
,
useSearchParams
}
from
'umi'
;
...
...
@@ -55,7 +56,7 @@ export default function LoginPage() {
const
[
captchaData
,
setCaptchaData
]
=
useState
(
null
);
const
[
captchaLoading
,
setCaptchaLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
'
'
);
const
[
,
setError
]
=
useFeedbackState
(
'error
'
);
const
redirect
=
normalizeRedirect
(
searchParams
.
get
(
'redirect'
));
const
refreshCaptcha
=
async
()
=>
{
...
...
@@ -205,14 +206,6 @@ export default function LoginPage() {
</
div
>
</
div
>
{
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
<
Button
fullWidth
className=
"login-submit-button"
...
...
src/pages/RecordDetailDrawer/components/ReferenceRecordsTable.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
}
from
'@/catalyst-ui-kit/javascript/button'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
Table
,
...
...
@@ -106,7 +108,8 @@ export default function ReferenceRecordsTable({ fieldDef, value, pageSize = 1000
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
records
,
setRecords
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useFeedbackState
(
'error'
);
const
[
retryKey
,
setRetryKey
]
=
useState
(
0
);
useEffect
(()
=>
{
let
cancelled
=
false
;
...
...
@@ -170,7 +173,7 @@ export default function ReferenceRecordsTable({ fieldDef, value, pageSize = 1000
return
()
=>
{
cancelled
=
true
;
};
},
[
pageSize
,
populatedRecords
,
referenceIds
,
subjectName
]);
},
[
pageSize
,
populatedRecords
,
referenceIds
,
subjectName
,
retryKey
,
setError
]);
if
(
referenceIds
.
length
===
0
)
{
return
(
...
...
@@ -201,16 +204,14 @@ export default function ReferenceRecordsTable({ fieldDef, value, pageSize = 1000
return
(
<
div
className=
"space-y-2"
>
{
error
?
(
<
div
className=
"rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700"
>
{
error
}
</
div
>
<
Button
outline
onClick=
{
()
=>
setRetryKey
((
key
)
=>
key
+
1
)
}
>
重新加载关联明细
</
Button
>
)
:
null
}
<
div
className=
"overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm"
>
<
div
className=
"border-b border-zinc-100 px-4 py-3 text-xs text-zinc-500"
>
共
{
records
.
length
}
条关联记录
</
div
>
{
records
.
length
===
0
?
(
<
div
className=
"px-4 py-5 text-sm text-zinc-500"
>
暂无可展示的关联明细
</
div
>
<
div
className=
"px-4 py-5 text-sm text-zinc-500"
>
{
error
?
'关联明细加载失败,请重新加载'
:
'暂无可展示的关联明细'
}
</
div
>
)
:
(
<
Table
dense
grid
className=
"max-h-96 overflow-auto"
>
<
TableHead
>
...
...
src/pages/RecordDetailDrawer/components/ReferenceSearchSelect.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
Input
}
from
'@/catalyst-ui-kit/javascript/input'
;
import
{
queryRecords
}
from
'@/services/DataService'
;
...
...
@@ -16,6 +17,7 @@ export default function ReferenceSearchSelect({
})
{
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
options
,
setOptions
]
=
useState
([]);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
subjectName
=
getReferenceSubjectName
(
fieldDef
);
const
multiple
=
Boolean
(
fieldDef
?.
multiple
);
const
selectedIds
=
useMemo
(
...
...
@@ -27,6 +29,7 @@ export default function ReferenceSearchSelect({
let
cancelled
=
false
;
const
loadOptions
=
async
()
=>
{
setError
(
''
);
if
(
!
subjectName
)
{
setOptions
([]);
return
;
...
...
@@ -46,6 +49,7 @@ export default function ReferenceSearchSelect({
}
}
catch
(
requestError
)
{
if
(
!
cancelled
)
{
setError
(
requestError
?.
message
||
'引用记录加载失败,请重新搜索。'
);
setOptions
([]);
}
}
...
...
@@ -56,7 +60,7 @@ export default function ReferenceSearchSelect({
return
()
=>
{
cancelled
=
true
;
};
},
[
keyword
,
subjectName
]);
},
[
keyword
,
subjectName
,
setError
]);
const
toggleRecord
=
(
record
,
checked
)
=>
{
const
recordId
=
normalizeRecordId
(
record
);
...
...
src/pages/RecordDetailDrawer/components/TailwindEditableField.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
clsx
from
'clsx'
;
import
{
Checkbox
}
from
'@/catalyst-ui-kit/javascript/checkbox'
;
...
...
@@ -133,6 +134,7 @@ function FieldValue({ value, fieldDef }) {
function
UserSelector
({
value
,
onChange
,
multiple
})
{
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
options
,
setOptions
]
=
useState
([]);
const
[,
setRequestError
]
=
useFeedbackState
(
'error'
);
const
selectedIds
=
useMemo
(
()
=>
toArray
(
value
).
map
((
item
)
=>
item
?.
_id
||
item
).
filter
(
Boolean
),
[
value
],
...
...
@@ -142,6 +144,7 @@ function UserSelector({ value, onChange, multiple }) {
let
cancelled
=
false
;
const
loadOptions
=
async
()
=>
{
setRequestError
(
''
);
const
filter
=
keyword
.
trim
()
?
{
$or
:
[
...
...
@@ -158,6 +161,7 @@ function UserSelector({ value, onChange, multiple }) {
}
}
catch
(
error
)
{
if
(
!
cancelled
)
{
setRequestError
(
error
?.
message
||
'用户列表加载失败,请重新搜索。'
);
setOptions
([]);
}
}
...
...
@@ -168,7 +172,7 @@ function UserSelector({ value, onChange, multiple }) {
return
()
=>
{
cancelled
=
true
;
};
},
[
keyword
]);
},
[
keyword
,
setRequestError
]);
const
toggleUser
=
(
user
,
checked
)
=>
{
if
(
!
multiple
)
{
...
...
@@ -226,6 +230,7 @@ export default function TailwindEditableField({
const
[
editing
,
setEditing
]
=
useState
(
false
);
const
[
draft
,
setDraft
]
=
useState
(
value
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setRequestError
]
=
useFeedbackState
(
'error'
);
const
[
uploading
,
setUploading
]
=
useState
(
false
);
useEffect
(()
=>
{
...
...
@@ -246,6 +251,7 @@ export default function TailwindEditableField({
const
save
=
async
(
nextValue
=
draft
)
=>
{
setError
(
''
);
setRequestError
(
''
);
if
(
fieldType
===
'object'
&&
typeof
nextValue
===
'string'
&&
nextValue
.
trim
())
{
try
{
...
...
@@ -260,7 +266,7 @@ export default function TailwindEditableField({
await
onSave
(
asSaveValue
(
nextValue
,
fieldDef
));
setEditing
(
false
);
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败,请稍后重试。'
);
set
Request
Error
(
saveError
?.
message
||
'保存失败,请稍后重试。'
);
}
};
...
...
@@ -273,6 +279,7 @@ export default function TailwindEditableField({
const
cancel
=
()
=>
{
setDraft
(
value
);
setError
(
''
);
setRequestError
(
''
);
setEditing
(
false
);
};
...
...
@@ -284,6 +291,7 @@ export default function TailwindEditableField({
setUploading
(
true
);
setError
(
''
);
setRequestError
(
''
);
try
{
const
uploaded
=
await
uploadFile
(
file
);
...
...
@@ -293,7 +301,7 @@ export default function TailwindEditableField({
await
onSave
(
nextValue
);
setEditing
(
false
);
}
catch
(
uploadError
)
{
setError
(
uploadError
?.
message
||
'附件上传失败,请稍后重试。'
);
set
Request
Error
(
uploadError
?.
message
||
'附件上传失败,请稍后重试。'
);
}
finally
{
setUploading
(
false
);
}
...
...
@@ -301,6 +309,7 @@ export default function TailwindEditableField({
const
handleRemoveFile
=
async
(
indexToRemove
)
=>
{
setError
(
''
);
setRequestError
(
''
);
const
nextValue
=
fieldDef
?.
multiple
?
toArray
(
draft
).
filter
((
_
,
index
)
=>
index
!==
indexToRemove
)
...
...
@@ -314,7 +323,7 @@ export default function TailwindEditableField({
}
}
catch
(
saveError
)
{
setDraft
(
draft
);
setError
(
saveError
?.
message
||
'删除附件失败,请稍后重试。'
);
set
Request
Error
(
saveError
?.
message
||
'删除附件失败,请稍后重试。'
);
}
};
...
...
src/pages/RecordDetailDrawer/hooks/useSubjectRecord.js
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
useEffect
,
useRef
,
useState
}
from
'react'
;
import
{
getSubjectView
,
...
...
@@ -46,13 +47,15 @@ export function useSubjectRecord(recordName, refreshKey, readonly, subjectName)
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
views
,
setViews
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useFeedbackState
(
'error'
);
const
[,
setSaveSuccess
]
=
useFeedbackState
(
'success'
);
const
requestIdRef
=
useRef
(
0
);
useEffect
(()
=>
{
let
cancelled
=
false
;
const
requestId
=
requestIdRef
.
current
+
1
;
requestIdRef
.
current
=
requestId
;
setSaveSuccess
(
''
);
const
loadAll
=
async
()
=>
{
if
(
!
recordName
)
{
...
...
@@ -99,26 +102,24 @@ export function useSubjectRecord(recordName, refreshKey, readonly, subjectName)
return
()
=>
{
cancelled
=
true
;
};
},
[
recordName
,
refreshKey
,
subjectName
]);
},
[
recordName
,
refreshKey
,
subjectName
,
setError
,
setSaveSuccess
]);
const
handleFieldUpdate
=
async
(
fieldPath
,
newValue
,
fieldDef
)
=>
{
if
(
readonly
||
remoteRecord
?.
metadata
?.
is_locked
===
true
||
!
editableRecord
)
{
return
;
}
const
previousRecord
=
editableRecord
;
const
nextRecord
=
setFieldValue
(
editableRecord
,
fieldPath
,
newValue
);
setEditableRecord
(
nextRecord
);
try
{
setSaveSuccess
(
''
);
const
requestId
=
requestIdRef
.
current
;
const
saveValue
=
transformSaveValue
(
newValue
,
fieldDef
);
const
payload
=
fieldPath
.
split
(
'.'
).
reduceRight
((
value
,
key
)
=>
({
[
key
]:
value
}),
saveValue
);
// Keep the saved snapshot unchanged until the request succeeds, so failures preserve the field draft.
await
updateRecord
(
recordName
,
payload
);
setRemoteRecord
(
nextRecord
);
}
catch
(
updateError
)
{
setEditableRecord
(
previousRecord
);
throw
updateError
;
}
if
(
requestIdRef
.
current
!==
requestId
)
return
;
setEditableRecord
((
current
)
=>
setFieldValue
(
current
,
fieldPath
,
newValue
));
setRemoteRecord
((
current
)
=>
setFieldValue
(
current
,
fieldPath
,
newValue
));
setSaveSuccess
(
'详情已保存。'
);
};
const
isFieldDisabled
=
(
fieldPath
,
currentView
)
=>
{
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
a6c529a8
...
...
@@ -212,8 +212,9 @@ const RecordDetailDrawer = (props, ref) => {
正在加载详情...
</
div
>
)
:
error
?
(
<
div
className=
"rounded-[28px] border border-rose-200 bg-rose-50 px-6 py-5 text-sm text-rose-700 shadow-sm"
>
{
error
}
<
div
className=
"flex min-h-48 flex-col items-center justify-center gap-3 text-sm text-zinc-500"
>
<
span
>
详情加载失败,请重新加载。
</
span
>
<
Button
outline
onClick=
{
()
=>
setRefreshKey
((
key
)
=>
key
+
1
)
}
>
重新加载
</
Button
>
</
div
>
)
:
(
<
div
className=
"space-y-4"
>
...
...
src/pages/audit_management/AuditProcess.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
DataTable
,
DataTableChip
,
DataTableTextPair
}
from
'@/components/DataTable'
;
import
{
Delete
,
Edit
,
Eye
,
Search
}
from
'@/components/AppIcons'
;
import
{
TableActionButton
,
TableActionGroup
}
from
'@/components/TableActionButton'
;
...
...
@@ -29,6 +30,7 @@ function AuditProjectDialog({ open, onClose, onSaved }) {
const
[
form
,
setForm
]
=
useState
(
EMPTY_PROJECT_FORM
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setRequestError
]
=
useFeedbackState
(
'error'
);
const
handleSave
=
async
()
=>
{
if
(
!
form
.
title
.
trim
())
{
...
...
@@ -38,6 +40,7 @@ function AuditProjectDialog({ open, onClose, onSaved }) {
setSaving
(
true
);
setError
(
''
);
setRequestError
(
''
);
try
{
const
record
=
await
createAuditProcess
({
title
:
form
.
title
.
trim
(),
...
...
@@ -47,7 +50,7 @@ function AuditProjectDialog({ open, onClose, onSaved }) {
setForm
(
EMPTY_PROJECT_FORM
);
onSaved
(
record
);
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'添加稽核项目失败'
);
set
Request
Error
(
requestError
?.
message
||
'添加稽核项目失败'
);
}
finally
{
setSaving
(
false
);
}
...
...
@@ -97,7 +100,7 @@ export default function AuditProcess() {
const
[
definition
,
setDefinition
]
=
useState
(
null
);
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
[
notice
,
setNotice
]
=
useState
(
null
);
const
[
projectDialogOpen
,
setProjectDialogOpen
]
=
useState
(
false
);
const
[
projectToDelete
,
setProjectToDelete
]
=
useState
(
null
);
...
...
@@ -127,7 +130,7 @@ export default function AuditProcess() {
}
finally
{
setLoading
(
false
);
}
},
[]);
},
[
setError
]);
useEffect
(()
=>
{
loadData
();
...
...
src/pages/audit_management/AuditReport.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
DataTable
,
DataTablePrimaryText
}
from
'@/components/DataTable'
;
import
{
CheckCircleIcon
,
ClipboardDocumentListIcon
,
ClockIcon
,
Eye
,
Project
}
from
'@/components/AppIcons'
;
import
{
TableActionButton
,
TableActionGroup
}
from
'@/components/TableActionButton'
;
...
...
@@ -106,7 +107,7 @@ export default function AuditReport() {
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
[
notice
,
setNotice
]
=
useState
(
null
);
const
[
submitOpen
,
setSubmitOpen
]
=
useState
(
false
);
const
[
linksOpen
,
setLinksOpen
]
=
useState
(
false
);
...
...
@@ -132,7 +133,7 @@ export default function AuditReport() {
}
finally
{
setLoading
(
false
);
}
},
[]);
},
[
setError
]);
useEffect
(()
=>
{
loadData
();
...
...
src/pages/audit_management/AuditReportContent.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
DataTable
}
from
'@/components/DataTable'
;
import
FileUploadField
from
'@/components/FileUploadField'
;
import
FilterSelect
from
'@/components/FilterSelect'
;
...
...
@@ -33,7 +34,7 @@ function PointContent({
onUpdate
,
})
{
const
[
uploadingLabel
,
setUploadingLabel
]
=
useState
(
''
);
const
[
uploadError
,
setUploadError
]
=
useState
(
'
'
);
const
[
,
setUploadError
]
=
useFeedbackState
(
'error
'
);
if
(
point
.
type
===
'form'
)
{
if
(
!
editable
)
{
...
...
@@ -120,7 +121,6 @@ function PointContent({
</
div
>
);
})
}
{
uploadError
?
<
div
role=
"alert"
>
{
uploadError
}
</
div
>
:
null
}
</
div
>
);
}
...
...
src/pages/audit_management/AuditReportDetail.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
CheckCircleIcon
,
ClipboardDocumentListIcon
,
Project
}
from
'@/components/AppIcons'
;
import
{
useAppShellHeader
}
from
'@/components/AppShell'
;
import
{
KPI
}
from
'@heroui-pro/react/kpi'
;
...
...
@@ -19,7 +20,7 @@ export default function AuditReportDetail() {
const
[
report
,
setReport
]
=
useState
(
null
);
const
[
issueOptions
,
setIssueOptions
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
loadData
=
useCallback
(
async
()
=>
{
setLoading
(
true
);
...
...
@@ -38,7 +39,7 @@ export default function AuditReportDetail() {
}
finally
{
setLoading
(
false
);
}
},
[
reportId
]);
},
[
reportId
,
setError
]);
useEffect
(()
=>
{
loadData
();
...
...
src/pages/audit_management/AuditReportFill.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
xyLogo
from
'@/assets/identity/xy-logo-red.png'
;
import
xyLogoWhite
from
'@/assets/identity/xy-logo-white.png'
;
import
{
APP_DISPLAY_NAME
}
from
'@/constants/app'
;
...
...
@@ -56,7 +57,7 @@ export default function AuditReportFill() {
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
dirty
,
setDirty
]
=
useState
(
false
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
[
notice
,
setNotice
]
=
useState
(
null
);
const
[
resetOpen
,
setResetOpen
]
=
useState
(
false
);
...
...
@@ -92,7 +93,7 @@ export default function AuditReportFill() {
}
finally
{
setLoading
(
false
);
}
},
[
ownerId
,
reportId
,
ticket
]);
},
[
ownerId
,
reportId
,
ticket
,
setError
]);
useEffect
(()
=>
{
loadData
();
...
...
src/pages/audit_management/ControlPointDialog.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Close
,
Delete
}
from
'@/components/AppIcons'
;
import
FilterSelect
from
'@/components/FilterSelect'
;
import
{
Button
,
FieldError
,
Input
,
Label
,
Modal
,
TextArea
,
TextField
,
Tooltip
}
from
'@heroui/react'
;
...
...
@@ -49,6 +50,7 @@ export default function UnifiedControlPointDialog({
const
[
form
,
setForm
]
=
useState
(
EMPTY_POINT_FORM
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setRequestError
]
=
useFeedbackState
(
'error'
);
const
[
isEditing
,
setIsEditing
]
=
useState
(
initialMode
===
'edit'
||
!
point
);
const
loadDepartmentOptions
=
useCallback
((
keyword
)
=>
searchReferenceRecords
(
'department'
,
keyword
),
[]);
const
loadRegulationOptions
=
useCallback
((
keyword
)
=>
searchReferenceRecords
(
'internal_regulation'
,
keyword
),
[]);
...
...
@@ -92,6 +94,7 @@ export default function UnifiedControlPointDialog({
setSaving
(
true
);
setError
(
''
);
setRequestError
(
''
);
try
{
await
onSaved
({
...
form
,
...
...
@@ -105,7 +108,7 @@ export default function UnifiedControlPointDialog({
attachment_label
:
compactLabels
(
form
.
attachment_label
),
});
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'保存控制点失败'
);
set
Request
Error
(
requestError
?.
message
||
'保存控制点失败'
);
}
finally
{
setSaving
(
false
);
}
...
...
src/pages/audit_management/PageState.jsx
View file @
a6c529a8
import
{
CheckCircleIcon
,
Close
,
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
showFeedback
}
from
'@/utils/feedback'
;
import
{
EmptyState
as
HeroEmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
Button
,
Spinner
,
toast
}
from
'@heroui/react'
;
import
{
useEffect
}
from
'react'
;
import
{
Button
,
Spinner
}
from
'@heroui/react'
;
import
{
useEffect
,
useRef
}
from
'react'
;
export
function
LoadingState
({
label
=
'正在加载...'
})
{
return
<
div
className=
"flex min-h-56 items-center justify-center gap-2"
><
Spinner
size=
"sm"
/><
span
className=
"text-sm text-muted"
>
{
label
}
</
span
></
div
>;
...
...
@@ -30,24 +31,15 @@ export function EmptyState({ title, description, action }) {
}
export
function
InlineNotice
({
tone
=
'success'
,
children
,
onDismiss
})
{
const
reportedRef
=
useRef
(
null
);
useEffect
(()
=>
{
if
(
tone
!==
'success'
)
return
undefined
;
toast
.
success
(
String
(
children
||
''
),
{
timeout
:
4000
});
if
(
!
children
)
return
;
const
feedback
=
{
message
:
String
(
children
),
tone
};
if
(
reportedRef
.
current
?.
message
===
feedback
.
message
&&
reportedRef
.
current
?.
tone
===
tone
)
return
;
reportedRef
.
current
=
feedback
;
showFeedback
(
feedback
.
message
,
tone
);
onDismiss
?.();
return
undefined
;
},
[
children
,
onDismiss
,
tone
]);
if
(
tone
===
'success'
)
return
null
;
return
(
<
Alert
status=
"danger"
>
<
Alert
.
Indicator
><
ExclamationTriangleIcon
size=
{
16
}
/></
Alert
.
Indicator
>
<
Alert
.
Content
><
Alert
.
Description
>
{
children
}
</
Alert
.
Description
></
Alert
.
Content
>
{
onDismiss
?
(
<
Button
isIconOnly
aria
-
label=
"关闭提示"
size=
"sm"
variant=
"ghost"
onPress=
{
onDismiss
}
>
<
Close
size=
{
14
}
/>
</
Button
>
)
:
null
}
</
Alert
>
);
return
null
;
}
src/pages/audit_management/ReferenceSearchSelect.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
normalizeRecordId
,
normalizeRecordLabel
}
from
'./auditManagementService'
;
...
...
@@ -28,7 +29,7 @@ export default function ReferenceSearchSelect({
const
[
query
,
setQuery
]
=
useState
(
''
);
const
[
options
,
setOptions
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
selectedIds
=
useMemo
(
()
=>
(
Array
.
isArray
(
value
)
?
value
:
value
?
[
value
]
:
[])
.
map
(
normalizeRecordId
)
...
...
@@ -53,8 +54,9 @@ export default function ReferenceSearchSelect({
}
},
[
valueRecords
]);
const
missingSelectedIds
=
selectedIds
.
filter
(
(
id
)
=>
!
selectedRecordMap
.
get
(
id
),
const
missingSelectedIds
=
useMemo
(
()
=>
selectedIds
.
filter
((
id
)
=>
!
selectedRecordMap
.
get
(
id
)),
[
selectedIds
,
selectedRecordMap
],
);
useEffect
(()
=>
{
...
...
@@ -70,7 +72,7 @@ export default function ReferenceSearchSelect({
return
()
=>
{
cancelled
=
true
;
};
},
[
missingSelectedIds
,
resolveMissingIds
]);
},
[
missingSelectedIds
,
resolveMissingIds
,
setError
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
...
...
@@ -83,6 +85,11 @@ export default function ReferenceSearchSelect({
mergeRecords
(
records
);
setOptions
(
records
||
[]);
}
}
catch
(
requestError
)
{
if
(
!
cancelled
)
{
setOptions
([]);
setError
(
requestError
?.
message
||
'引用记录加载失败,请重新搜索'
);
}
}
finally
{
if
(
!
cancelled
)
setLoading
(
false
);
}
...
...
@@ -92,7 +99,7 @@ export default function ReferenceSearchSelect({
cancelled
=
true
;
window
.
clearTimeout
(
timer
);
};
},
[
loadOptions
,
query
]);
},
[
loadOptions
,
query
,
setError
]);
const
normalizedOptions
=
uniqueRecords
([
...
selectedIds
.
map
((
id
)
=>
selectedRecordMap
.
get
(
id
)).
filter
(
Boolean
),
...
...
@@ -113,9 +120,9 @@ export default function ReferenceSearchSelect({
return
(
<
div
className=
"space-y-1"
>
{
error
?
<
div
className=
"text-sm text-rose-600"
>
{
error
}
</
div
>
:
null
}
<
XinyuanAutocomplete
aria
-
label=
{
placeholder
}
errorText=
{
error
?
'引用记录加载失败,请重新搜索'
:
''
}
inputValue=
{
query
}
isLoading=
{
loading
}
options=
{
normalizedOptions
}
...
...
src/pages/audit_management/audit-management.business-flow.md
View file @
a6c529a8
...
...
@@ -94,10 +94,20 @@
异常与限制:
-
列表或操作失败时保留当前分类、筛选和页码,
并显示可重试错误或 Toast
。
-
列表或操作失败时保留当前分类、筛选和页码,
错误详情使用顶部操作提示;列表加载失败显示“重新加载”入口,关闭提示后仍保留加载失败状态
。
-
通知页不改变稽核项目、报告、权限、保存回调及请求时机。
## 8. 当前边界
## 8. 操作反馈
1.
登录/验证码、稽核项目、控制点弹窗、报告复核、独立填报、通知操作与应用切换共用根部
`AppFeedback`
;成功、失败和降级警告均以视口顶部 Toast 展示,不在页面或弹窗正文中留下接口反馈横条。
2.
浮层通过 Portal 挂到
`body`
,高于业务 Modal、删除确认弹窗和移动侧栏,不随内容滚动,也不受弹窗对页面背景的不可交互限制。关闭图标位于正文右侧,使用 HeroUI 小号透明按钮,通过 Toast 官方
`close`
slot 点击或键盘关闭;前排无需悬停即可操作,后排堆叠提示隐藏关闭按钮。关闭仅影响提示本身;成功默认 4 秒、错误与警告默认 8 秒后自动消失,最多同时显示 3 条。
3.
关闭提示只移除提示本身,不清空表单草稿、不重置查询条件/页码,也不将失败伪装为空数据。加载失败保留原有重新加载入口;重复尝试遇到相同错误时重新显示反馈。
4.
项目/控制点必填校验仍在表单字段附近展示;接口保存失败保留编辑内容和弹窗,上传失败不写入无效附件。引用选项查询或已选引用加载失败显示错误提示,用户可重新搜索;不会把接口失败当作成功选中。
5.
本次仅调整反馈呈现,继续使用
`record-api.md`
的查询、创建、更新、软删除与通知接口;
`audit_process`
、
`audit_process_control_point`
、
`audit_report`
等对象的
`metadata`
、引用 ID、状态与日期规则不变。
6.
通用
`RecordDetailDrawer`
详情按字段保存:仅在
`PUT /record/:name`
成功后更新详情快照并弹出“详情已保存。”;等待响应和失败时不覆盖已保存快照,失败保留字段草稿并显示错误,不提示成功。新一轮保存、重新加载或切换记录时清理上一条成功提示,旧记录的请求完成后不会给新记录弹出成功反馈;只读、锁定或尚未加载的详情不提交、不提示成功。字段保存、附件上传后保存和附件移除后保存共用此成功入口,不重复报成功。
7.
活动稽核控制点详情继续在
`saveAuditControlPoints`
成功后关闭编辑弹窗并显示唯一“控制点已保存”提示;校验失败不提交,接口失败保留弹窗及草稿。通用详情为存量复用组件,当前活动路由不新增详情入口。
## 9. 当前边界
-
本次重构提供按责任部门复制填报链接,不自动发送邮件。
-
旧版邮件发送依赖未收录于
`record-api.md`
的邮件接口;在补齐接口契约、发件身份、失败重试和审计要求前不接入。
...
...
src/pages/audit_management/notifications/NotificationListPage.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
showFeedback
}
from
'@/utils/feedback'
;
import
{
Bell
,
Close
,
MagnifyingGlassIcon
,
OpenExternal
}
from
'@/components/AppIcons'
;
import
{
DataTablePagination
}
from
'@/components/DataTable'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
Avatar
,
Button
,
Card
,
...
...
@@ -11,7 +12,6 @@ import {
SearchField
,
Spinner
,
Tabs
,
toast
,
}
from
'@heroui/react'
;
import
{
useCallback
,
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
...
...
@@ -150,7 +150,7 @@ export default function NotificationListPage() {
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
unreadTotal
,
setUnreadTotal
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
[
busyKey
,
setBusyKey
]
=
useState
(
''
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
...
...
@@ -192,7 +192,7 @@ export default function NotificationListPage() {
return
()
=>
{
cancelled
=
true
;
};
},
[
category
,
keyword
,
page
,
readStatus
,
reloadKey
]);
},
[
category
,
keyword
,
page
,
readStatus
,
reloadKey
,
setError
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
...
...
@@ -214,13 +214,11 @@ export default function NotificationListPage() {
setBusyKey
(
key
);
try
{
await
operation
();
toast
.
success
(
successMessage
,
{
timeout
:
3000
}
);
showFeedback
(
successMessage
,
'success'
);
refresh
();
return
true
;
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
timeout
:
4000
,
});
showFeedback
(
operationError
?.
message
||
'操作失败,请稍后重试'
);
return
false
;
}
finally
{
setBusyKey
(
''
);
...
...
@@ -271,9 +269,7 @@ export default function NotificationListPage() {
}
refresh
();
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
timeout
:
4000
,
});
showFeedback
(
operationError
?.
message
||
'无法打开通知关联流程'
);
}
finally
{
setBusyKey
(
''
);
}
...
...
@@ -356,14 +352,15 @@ export default function NotificationListPage() {
</
div
>
{
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
<
Button
size=
"sm"
variant=
"ghost"
onPress=
{
refresh
}
>
重试
</
Button
>
</
Alert
>
<
EmptyState
className=
"min-h-64"
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
通知列表加载失败
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
请重新加载,或调整筛选条件后重试。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Footer
>
<
Button
size=
"sm"
variant=
"secondary"
onPress=
{
refresh
}
>
重新加载
</
Button
>
</
EmptyState
.
Footer
>
</
EmptyState
>
)
:
null
}
{
loading
&&
notifications
.
length
===
0
?
(
...
...
src/pages/process_monitor/ProcessDetail.jsx
View file @
a6c529a8
import
{
showFeedback
}
from
'@/utils/feedback'
;
import
{
ErrorState
}
from
'@/pages/audit_management/PageState'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
ArrowLeft
,
Download
}
from
'@/components/AppIcons'
;
import
{
Description
,
Modal
,
Spinner
,
Surface
}
from
'@heroui/react'
;
import
{
Description
,
Spinner
}
from
'@heroui/react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
Link
,
useParams
,
useSearchParams
}
from
'umi'
;
import
{
buildDepartmentLookup
,
buildMemberLookup
,
downloadProcessAttachment
,
formatDateTime
,
getDisplayFieldMap
,
loadProcessInstanceDetail
}
from
'./processMonitorService'
;
function
resolveValue
(
field
,
fieldMap
,
lookups
)
{
const
definition
=
fieldMap
[
field
?.
label
];
const
value
=
field
?.
value
;
if
(
field
?.
type
===
'file'
)
{
const
files
=
Array
.
isArray
(
value
)
?
value
:
[];
return
files
.
length
?
<
div
className=
"flex flex-wrap gap-2"
>
{
files
.
map
((
file
,
index
)
=>
<
button
className=
"text-sm text-accent underline"
key=
{
`${file.fileUrl || file.url}-${index}`
}
type=
"button"
onClick=
{
()
=>
downloadProcessAttachment
(
file
)
}
>
{
file
.
filename
||
file
.
fileName
||
'附件'
}
</
button
>)
}
</
div
>
:
<
Description
>
暂无内容
</
Description
>;
}
if
(
definition
?.
type
===
'date'
)
return
formatDateTime
(
value
,
'YYYY-MM-DD HH:mm'
);
if
(
definition
?.
type
===
'member'
)
return
String
(
value
||
''
).
split
(
','
).
map
((
item
)
=>
lookups
.
members
[
item
.
trim
()]
||
item
.
trim
()).
filter
(
Boolean
).
join
(
'、'
)
||
'暂无内容'
;
if
(
definition
?.
type
===
'department'
)
return
String
(
value
||
''
).
split
(
','
).
map
((
item
)
=>
lookups
.
departments
[
item
.
trim
()]
||
item
.
trim
()).
filter
(
Boolean
).
join
(
'、'
)
||
'暂无内容'
;
return
value
||
<
Description
>
暂无内容
</
Description
>;
}
function
resolveValue
(
field
,
fieldMap
,
lookups
)
{
const
definition
=
fieldMap
[
field
?.
label
];
const
value
=
field
?.
value
;
if
(
field
?.
type
===
'file'
)
{
const
files
=
Array
.
isArray
(
value
)
?
value
:
[];
return
files
.
length
?
<
div
className=
"flex flex-wrap gap-2"
>
{
files
.
map
((
file
,
index
)
=>
<
button
className=
"text-sm text-accent underline"
key=
{
`${file.fileUrl || file.url}-${index}`
}
type=
"button"
onClick=
{
async
()
=>
{
try
{
await
downloadProcessAttachment
(
file
);
}
catch
(
requestError
)
{
showFeedback
(
requestError
?.
message
||
'附件下载失败'
);
}
}
}
>
{
file
.
filename
||
file
.
fileName
||
'附件'
}
</
button
>)
}
</
div
>
:
<
Description
>
暂无内容
</
Description
>;
}
if
(
definition
?.
type
===
'date'
)
return
formatDateTime
(
value
,
'YYYY-MM-DD HH:mm'
);
if
(
definition
?.
type
===
'member'
)
return
String
(
value
||
''
).
split
(
','
).
map
((
item
)
=>
lookups
.
members
[
item
.
trim
()]
||
item
.
trim
()).
filter
(
Boolean
).
join
(
'、'
)
||
'暂无内容'
;
if
(
definition
?.
type
===
'department'
)
return
String
(
value
||
''
).
split
(
','
).
map
((
item
)
=>
lookups
.
departments
[
item
.
trim
()]
||
item
.
trim
()).
filter
(
Boolean
).
join
(
'、'
)
||
'暂无内容'
;
return
value
||
<
Description
>
暂无内容
</
Description
>;
}
export
default
function
ProcessDetailPage
()
{
const
{
processId
,
application
}
=
useParams
();
const
[
searchParams
]
=
useSearchParams
();
const
tabName
=
searchParams
.
get
(
'tab'
)
||
''
;
const
[
record
,
setRecord
]
=
useState
(
null
);
const
[
fieldsDefinition
,
setFieldsDefinition
]
=
useState
([]);
const
[
members
,
setMembers
]
=
useState
([]);
const
[
departments
,
setDepartments
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
use
State
(
''
);
useEffect
(()
=>
{
let
cancelled
=
false
;
setLoading
(
true
);
loadProcessInstanceDetail
(
application
).
then
((
result
)
=>
{
if
(
cancelled
)
return
;
setRecord
(
result
.
record
);
setFieldsDefinition
(
result
.
fieldsDefinition
);
setMembers
(
result
.
members
);
setDepartments
(
result
.
departments
);
}).
catch
((
requestError
)
=>
{
if
(
!
cancelled
)
setError
(
requestError
?.
message
||
'加载流程详情失败'
);
}).
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
},
[
application
]);
const
{
processId
,
application
}
=
useParams
();
const
[
searchParams
]
=
useSearchParams
();
const
tabName
=
searchParams
.
get
(
'tab'
)
||
''
;
const
[
record
,
setRecord
]
=
useState
(
null
);
const
[
fieldsDefinition
,
setFieldsDefinition
]
=
useState
([]);
const
[
members
,
setMembers
]
=
useState
([]);
const
[
departments
,
setDepartments
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error'
);
const
[
retryKey
,
setRetryKey
]
=
useState
(
0
);
useEffect
(()
=>
{
let
cancelled
=
false
;
setLoading
(
true
);
setError
(
''
);
loadProcessInstanceDetail
(
application
).
then
((
result
)
=>
{
if
(
cancelled
)
return
;
setRecord
(
result
.
record
);
setFieldsDefinition
(
result
.
fieldsDefinition
);
setMembers
(
result
.
members
);
setDepartments
(
result
.
departments
);
}).
catch
((
requestError
)
=>
{
if
(
!
cancelled
)
setError
(
requestError
?.
message
||
'加载流程详情失败'
);
}).
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
},
[
application
,
retryKey
,
setError
]);
const
lookups
=
useMemo
(()
=>
({
members
:
buildMemberLookup
(
members
),
departments
:
buildDepartmentLookup
(
departments
)
}),
[
members
,
departments
]);
const
fieldMap
=
useMemo
(()
=>
getDisplayFieldMap
(
fieldsDefinition
),
[
fieldsDefinition
]);
const
details
=
record
?.
metadata
?.
detail
||
[];
return
<
div
className=
"space-y-6"
><
div
className=
"flex flex-wrap items-center gap-3"
><
Link
className=
"inline-flex items-center gap-2 text-sm text-muted"
to=
{
tabName
?
`/process_monitor/${processId}?tab=${tabName}`
:
`/process_monitor/${processId}`
}
><
ArrowLeft
size=
{
16
}
/>
返回实例列表
</
Link
></
div
><
h1
className=
"rc-section-title"
>
{
record
?.
title
||
'流程实例详情'
}
</
h1
>
{
error
?
<
Surface
className=
"p-4"
variant=
"secondary"
>
{
error
}
</
Surface
>
:
null
}{
loading
?
<
div
className=
"flex min-h-48 items-center justify-center"
><
Spinner
size=
"sm"
/></
div
>
:
<
section
className=
"grid min-w-0 grid-cols-1 gap-x-8 gap-y-2 md:grid-cols-2"
>
{
details
.
length
?
details
.
map
((
field
,
index
)
=>
<
div
className=
"min-w-0 py-3"
key=
{
`${field.label || 'field'}-${index}`
}
><
div
className=
"text-sm font-medium text-muted"
>
{
field
.
label
||
`字段 ${index + 1}`
}
</
div
><
div
className=
"mt-1 break-words text-sm text-foreground"
>
{
resolveValue
(
field
,
fieldMap
,
lookups
)
}
</
div
></
div
>)
:
<
Description
>
暂无内容
</
Description
>
}
</
section
>
}
</
div
>;
return
<
div
className=
"space-y-6"
><
div
className=
"flex flex-wrap items-center gap-3"
><
Link
className=
"inline-flex items-center gap-2 text-sm text-muted"
to=
{
tabName
?
`/process_monitor/${processId}?tab=${tabName}`
:
`/process_monitor/${processId}`
}
><
ArrowLeft
size=
{
16
}
/>
返回实例列表
</
Link
></
div
><
h1
className=
"rc-section-title"
>
{
record
?.
title
||
'流程实例详情'
}
</
h1
>
{
loading
?
<
div
className=
"flex min-h-48 items-center justify-center"
><
Spinner
size=
"sm"
/></
div
>
:
error
?
<
ErrorState
message=
"流程详情加载失败"
onRetry=
{
()
=>
setRetryKey
((
key
)
=>
key
+
1
)
}
/
>
:
<
section
className=
"grid min-w-0 grid-cols-1 gap-x-8 gap-y-2 md:grid-cols-2"
>
{
details
.
length
?
details
.
map
((
field
,
index
)
=>
<
div
className=
"min-w-0 py-3"
key=
{
`${field.label || 'field'}-${index}`
}
><
div
className=
"text-sm font-medium text-muted"
>
{
field
.
label
||
`字段 ${index + 1}`
}
</
div
><
div
className=
"mt-1 break-words text-sm text-foreground"
>
{
resolveValue
(
field
,
fieldMap
,
lookups
)
}
</
div
></
div
>)
:
<
Description
>
暂无内容
</
Description
>
}
</
section
>
}
</
div
>;
}
src/pages/process_monitor/ProcessMonitor.jsx
View file @
a6c529a8
This diff is collapsed.
Click to expand it.
src/pages/process_monitor/ProcessTemplateList.jsx
View file @
a6c529a8
This diff is collapsed.
Click to expand it.
src/pages/process_monitor/process-monitor.business-flow.md
View file @
a6c529a8
...
...
@@ -155,7 +155,17 @@
---
## 8. 当前项目对应文件
## 8. 操作反馈与异常处理
-
分组创建、模板添加/编辑/移除和显示字段保存沿用根部
`AppFeedback`
,成功默认 4 秒、接口错误默认 8 秒后消失;移除常驻成功横条和弹窗内的接口错误横条。
-
浮层位于视口顶部,层级高于配置 Modal 和移除确认弹窗,关闭按钮始终可见。关闭提示不清空分组名称、模板选择或字段草稿,不改变当前分组、关键字、部门筛选或分页。
-
加载失败保留失败状态和已有数据;无可展示数据时提供重新加载入口。实例详情加载失败可重新加载,附件下载失败通过操作提示报告,允许再次点击下载。
-
请求期间继续保持加载/保存状态与原有按钮禁用条件;重复失败重新显示提示。本次不修改
`process_monitor_group.metadata.progresses`
、
`audit_related_progress.metadata.fields_definition`
、
`audit_progress_info.metadata.detail`
或 Reference ID 写入规则。
-
本模块保留为复用实现,当前
`.umirc.ts`
未配置流程监控路由;本次不新增导航入口。
---
## 9. 当前项目对应文件
-
`src/pages/process_monitor/Layout.jsx`
-
`src/pages/process_monitor/ProcessMonitor.jsx`
...
...
src/tailwind.css
View file @
a6c529a8
...
...
@@ -7,6 +7,7 @@
@import
"./design-system/theme.css"
;
@import
"./design-system/tokens.css"
;
@import
"./design-system/shell.css"
;
@import
"./design-system/feedback.css"
;
@theme
{
--font-inter
:
...
...
src/utils/feedback.js
0 → 100644
View file @
a6c529a8
import
{
toast
}
from
"@heroui/react"
;
const
FEEDBACK_OPTIONS
=
{
error
:
{
title
:
"操作失败"
,
variant
:
"danger"
,
timeout
:
8000
},
success
:
{
title
:
"操作成功"
,
variant
:
"success"
,
timeout
:
4000
},
warning
:
{
title
:
"请注意"
,
variant
:
"warning"
,
timeout
:
8000
},
};
export
function
showFeedback
(
message
,
tone
=
"error"
)
{
if
(
!
message
)
return
null
;
const
{
title
,
variant
,
timeout
}
=
FEEDBACK_OPTIONS
[
tone
]
||
FEEDBACK_OPTIONS
.
error
;
return
toast
(
title
,
{
description
:
String
(
message
),
variant
,
timeout
});
}
src/wrapper/Site.jsx
View file @
a6c529a8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
getSystemInfo
}
from
'@/services/DataService'
;
import
dayjs
from
'dayjs'
;
import
customParseFormat
from
'dayjs/plugin/customParseFormat'
;
...
...
@@ -18,7 +19,7 @@ export default function SiteWrapper() {
const
[
siteInfo
,
setSiteInfo
]
=
useState
(
null
);
const
[
metadata
,
setMetadata
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loadError
,
setLoadError
]
=
use
State
(
'
'
);
const
[
loadError
,
setLoadError
]
=
use
FeedbackState
(
'warning
'
);
useEffect
(()
=>
{
let
cancelled
=
false
;
...
...
@@ -54,7 +55,7 @@ export default function SiteWrapper() {
return
()
=>
{
cancelled
=
true
;
};
},
[]);
},
[
setLoadError
]);
if
(
loading
)
{
return
(
...
...
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