Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
R
risk_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
risk_manager_new
Commits
a34ec141
Commit
a34ec141
authored
Oct 08, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: unify operation feedback and failure retries
parent
22d96165
Show whitespace changes
Inline
Side-by-side
Showing
34 changed files
with
467 additions
and
225 deletions
+467
-225
xinyuan-design-system.md
docs/xinyuan-design-system.md
+8
-0
manifest.json
public/manifest.json
+1
-1
app.js
src/app.js
+7
-1
AppFeedback.jsx
src/components/AppFeedback.jsx
+44
-0
AppShell.jsx
src/components/AppShell.jsx
+0
-2
DynamicFormValueEditor.jsx
src/components/DynamicFormValueEditor.jsx
+32
-14
DynamicRecordDialog.jsx
src/components/DynamicRecordDialog.jsx
+26
-7
ReferenceSearchSelect.jsx
src/components/ReferenceSearchSelect.jsx
+2
-1
SubjectRecordWorkbench.jsx
src/components/SubjectRecordWorkbench.jsx
+16
-11
UserSearchSelect.jsx
src/components/UserSearchSelect.jsx
+10
-3
index.jsx
src/components/record-workbench/index.jsx
+13
-19
feedback.css
src/design-system/feedback.css
+11
-0
useFeedbackState.js
src/hooks/useFeedbackState.js
+41
-0
CompareRecordField.jsx
src/pages/CompareRecordField.jsx
+19
-4
Login.jsx
src/pages/Login.jsx
+3
-10
ReferenceRecordsTable.jsx
...s/RecordDetailDrawer/components/ReferenceRecordsTable.jsx
+9
-9
TailwindEditableField.jsx
...s/RecordDetailDrawer/components/TailwindEditableField.jsx
+37
-8
useSubjectRecord.js
src/pages/RecordDetailDrawer/hooks/useSubjectRecord.js
+2
-1
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+19
-12
ApplicationSwitcherDialog.jsx
...pages/problem_rectification/ApplicationSwitcherDialog.jsx
+4
-10
index.jsx
src/pages/problem_rectification/archive/index.jsx
+8
-4
ProblemRectificationMaterialField.jsx
...fication/components/ProblemRectificationMaterialField.jsx
+5
-15
index.jsx
src/pages/problem_rectification/dashboard/index.jsx
+21
-13
index.jsx
src/pages/problem_rectification/flow-table/index.jsx
+13
-17
index.jsx
src/pages/problem_rectification/form/index.jsx
+10
-8
index.jsx
src/pages/problem_rectification/list/index.jsx
+11
-13
NotificationListPage.jsx
...blem_rectification/notifications/NotificationListPage.jsx
+21
-20
problem-rectification.business-flow.md
...blem_rectification/problem-rectification.business-flow.md
+10
-0
ImportPreflightDialog.jsx
src/pages/risk_management/ImportPreflightDialog.jsx
+11
-6
RiskManagementListPage.jsx
src/pages/risk_management/RiskManagementListPage.jsx
+27
-12
risk-management.business-flow.md
src/pages/risk_management/risk-management.business-flow.md
+15
-3
feedbackQueue.js
src/services/feedbackQueue.js
+8
-0
tailwind.css
src/tailwind.css
+1
-0
Site.jsx
src/wrapper/Site.jsx
+2
-1
No files found.
docs/xinyuan-design-system.md
View file @
a34ec141
...
...
@@ -31,6 +31,14 @@
-
共享组件只能组合 HeroUI/HeroUI Pro 的官方 compound API,不得复刻旧组件 props。
-
官方组件暂不覆盖的场景应优先组合 HeroUI/HeroUI Pro compound API,并在本设计系统中补充经过评审的约定;不得以 Catalyst 或可见原生控件作为降级方案。
### 1.1 操作反馈浮层
接口成功、失败和降级警告统一使用应用根部
`AppFeedback`
的 HeroUI
`Toast.Provider`
与官方 compound 组件;登录、独立历史比较/流程页和 AppShell 共用一个 Provider,业务页面不再渲染接口反馈横条。成功默认 4 秒、错误/警告默认 8 秒后消失,关闭仅改变提示展示,不修改请求结果、失败状态或表单草稿。
同步问题整改提交
`bfc5862`
,
`src/design-system/feedback.css`
仅调整 Toast 层级、安全区位置及后排提示关闭按钮的可见性,浮层层级 300,高于业务 Modal 和移动侧栏。关闭按钮直接组合 HeroUI
`Button`
(
`ghost`
、
`sm`
、
`isIconOnly`
),通过官方
`slot="close"`
接入 Toast 关闭行为,使用
`AppIcons.Close`
,位于内容右侧并随正文参与布局。前排关闭图标始终可见,使用官方透明按钮及默认 Hover/焦点样式;不再放大带边框的 Toast 默认关闭按钮,不设置额外的绝对位置和正文右侧占位。长文继续在 Toast 内容中换行和滚动。颜色、边框、圆角、阴影与 Hover 继续使用 HeroUI 默认主题,不在页面 CSS 或
`tokens.css`
中覆盖。
所有反馈来源使用
`feedbackQueue`
的官方
`ToastQueue`
;通过官方
`wrapUpdate`
即时更新队列,避免浏览器 View Transition 快照在提示进出期间拦截点击并误关下方弹窗。保留 HeroUI/React Aria 的计时、悬停/聚焦暂停、关闭、Portal 和键盘交互。
## 2. 主题权威
-
`src/design-system/theme.css`
是浅色、暗色、品牌色、语义色、圆角和字体的唯一主题权威,以 HeroUI 官方主题工具导出值为基础,并在文件内固化批准的 L1/L2/Overlay 映射。它必须与
`/Users/liuchenlong/Desktop/theme.css`
及共享 Skill 基线逐字节一致;升级 HeroUI 时重新导出基础值、复核三项语义映射并整体批准新基线。
...
...
public/manifest.json
View file @
a34ec141
{
"name"
:
"risk_manager_new"
,
"version"
:
"1.0.
2
"
,
"version"
:
"1.0.
4
"
,
"build"
:
"abc"
,
"description"
:
"风险管理应用"
,
"date"
:
"2025-07-14 19:27:20"
,
...
...
src/app.js
View file @
a34ec141
import
'@/tailwind.css'
;
import
{
defineApp
}
from
'umi'
;
import
{
createElement
}
from
'react'
;
import
AppFeedback
from
'@/components/AppFeedback'
;
import
{
APPLICATION_BASE_PATH
,
APPLICATION_NAME
,
...
...
@@ -40,4 +42,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 @
a34ec141
import
{
Button
,
Toast
}
from
'@heroui/react'
;
import
{
Close
}
from
'@/components/AppIcons'
;
import
{
feedbackQueue
}
from
'@/services/feedbackQueue'
;
export
default
function
AppFeedback
({
children
})
{
return
(
<>
{
children
}
<
Toast
.
Provider
aria
-
label=
"操作提示"
className=
"app-feedback-region"
maxVisibleToasts=
{
3
}
placement=
"top"
queue=
{
feedbackQueue
}
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
className=
"max-h-[40dvh] overflow-y-auto whitespace-pre-wrap break-words"
>
{
toast
.
content
.
title
}
</
Toast
.
Title
>
{
toast
.
content
.
description
?
(
<
Toast
.
Description
className=
"max-h-[40dvh] overflow-y-auto whitespace-pre-wrap break-words text-foreground"
>
{
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
>
</>
);
}
src/components/AppShell.jsx
View file @
a34ec141
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Toast
}
from
'@heroui/react'
;
import
{
history
}
from
'umi'
;
import
DashboardNavbar
from
'./DashboardNavbar'
;
import
DashboardSidebar
from
'./DashboardSidebar'
;
...
...
@@ -32,7 +31,6 @@ export default function AppShell({
<
div
className=
"rc-app-page"
>
{
children
}
</
div
>
</
div
>
</
Sidebar
.
Main
>
<
Toast
.
Provider
maxVisibleToasts=
{
3
}
placement=
"top"
/>
</
Sidebar
.
Provider
>
);
}
src/components/DynamicFormValueEditor.jsx
View file @
a34ec141
...
...
@@ -11,6 +11,7 @@ import {
TextField
,
}
from
'@heroui/react'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
DataTable
,
DataTableText
,
...
...
@@ -117,7 +118,8 @@ function useFormDefinition(instance) {
const
identifier
=
getFormIdentifier
(
instance
?.
form
);
const
[
definition
,
setDefinition
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useFeedbackState
(
'error'
);
const
[
retryKey
,
setRetryKey
]
=
useState
(
0
);
useEffect
(()
=>
{
let
cancelled
=
false
;
...
...
@@ -152,9 +154,9 @@ function useFormDefinition(instance) {
return
()
=>
{
cancelled
=
true
;
};
},
[
identifier
]);
},
[
identifier
,
retryKey
,
setError
]);
return
{
definition
,
loading
,
error
};
return
{
definition
,
loading
,
error
,
retry
:
()
=>
setRetryKey
((
key
)
=>
key
+
1
)
};
}
function
FormDataTable
({
instance
,
definition
,
loading
,
rowActions
=
null
})
{
...
...
@@ -240,17 +242,20 @@ function FormRowDialog({ definition, isOpen, row, onClose, onSave }) {
const
[
values
,
setValues
]
=
useState
({});
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setRequestError
]
=
useFeedbackState
(
'error'
);
useEffect
(()
=>
{
setRequestError
(
''
);
if
(
isOpen
)
{
setValues
(
row
&&
typeof
row
===
'object'
?
row
:
{});
setError
(
''
);
}
},
[
isOpen
,
row
]);
},
[
isOpen
,
row
,
setRequestError
]);
const
handleSave
=
async
()
=>
{
setSaving
(
true
);
setError
(
''
);
setRequestError
(
''
);
try
{
const
prepared
=
await
prepareValuesForSubmit
(
definition
,
values
);
if
(
prepared
.
errors
.
length
>
0
)
{
...
...
@@ -260,7 +265,7 @@ function FormRowDialog({ definition, isOpen, row, onClose, onSave }) {
onSave
(
prepared
.
metadata
);
onClose
();
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'表单行保存失败'
);
set
Request
Error
(
saveError
?.
message
||
'表单行保存失败'
);
}
finally
{
setSaving
(
false
);
}
...
...
@@ -336,7 +341,7 @@ function FormInstance({
onChange
,
onRequestDelete
,
})
{
const
{
definition
,
loading
,
error
}
=
useFormDefinition
(
instance
);
const
{
definition
,
loading
,
error
,
retry
}
=
useFormDefinition
(
instance
);
const
[
editingRowIndex
,
setEditingRowIndex
]
=
useState
(
null
);
const
formIdentifier
=
getFormIdentifier
(
instance
.
form
);
const
formOptions
=
forms
.
map
((
form
)
=>
({
...
...
@@ -423,9 +428,12 @@ function FormInstance({
</
EmptyState
.
Media
>
<
EmptyState
.
Title
>
表单模板不可用
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
{
error
}{
canEdit
?
' 请重新选择可用的表单模板。'
:
''
}
详细错误见操作提示,请重新加载或选择可用的表单模板。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Footer
>
<
Button
size=
"sm"
variant=
"secondary"
onPress=
{
retry
}
>
重新加载
</
Button
>
</
EmptyState
.
Footer
>
</
EmptyState
>
</
EmptyStateSurface
>
)
:
formIdentifier
?
(
...
...
@@ -492,7 +500,7 @@ function FormInstance({
}
function
ReadonlyFormInstance
({
instance
})
{
const
{
definition
,
loading
,
error
}
=
useFormDefinition
(
instance
);
const
{
definition
,
loading
,
error
,
retry
}
=
useFormDefinition
(
instance
);
return
(
<
Disclosure
defaultExpanded
>
...
...
@@ -516,8 +524,11 @@ function ReadonlyFormInstance({ instance }) {
<
ExclamationTriangleIcon
aria
-
hidden=
"true"
/>
</
EmptyState
.
Media
>
<
EmptyState
.
Title
>
表单模板不可用
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
{
error
}
</
EmptyState
.
Description
>
<
EmptyState
.
Description
>
详细错误见操作提示,请重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Footer
>
<
Button
size=
"sm"
variant=
"secondary"
onPress=
{
retry
}
>
重新加载
</
Button
>
</
EmptyState
.
Footer
>
</
EmptyState
>
</
EmptyStateSurface
>
)
:
(
...
...
@@ -546,7 +557,8 @@ export default function DynamicFormValueEditor({
const
instances
=
useMemo
(()
=>
normalizeInstances
(
value
,
multiple
),
[
value
,
multiple
]);
const
[
forms
,
setForms
]
=
useState
([]);
const
[
formsLoading
,
setFormsLoading
]
=
useState
(
false
);
const
[
formsError
,
setFormsError
]
=
useState
(
''
);
const
[
formsError
,
setFormsError
]
=
useFeedbackState
(
'error'
);
const
[
formsRetryKey
,
setFormsRetryKey
]
=
useState
(
0
);
const
[
activeGroup
,
setActiveGroup
]
=
useState
(
DEFAULT_GROUP
);
const
[
newGroupName
,
setNewGroupName
]
=
useState
(
''
);
const
[
isGroupDialogOpen
,
setGroupDialogOpen
]
=
useState
(
false
);
...
...
@@ -581,7 +593,7 @@ export default function DynamicFormValueEditor({
return
()
=>
{
cancelled
=
true
;
};
},
[
canEdit
]);
},
[
canEdit
,
formsRetryKey
,
setFormsError
]);
const
groups
=
useMemo
(()
=>
{
const
configured
=
Array
.
from
(
new
Set
(
...
...
@@ -770,9 +782,15 @@ export default function DynamicFormValueEditor({
<
Disclosure
.
Body
>
<
div
className=
"space-y-4"
>
{
formsError
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
><
Alert
.
Description
>
{
formsError
}
</
Alert
.
Description
></
Alert
.
Content
>
</
Alert
>
<
EmptyState
size=
"sm"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
表单模板列表加载失败
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
详细错误见操作提示,请重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Footer
>
<
Button
variant=
"secondary"
onPress=
{
()
=>
setFormsRetryKey
((
key
)
=>
key
+
1
)
}
>
重新加载
</
Button
>
</
EmptyState
.
Footer
>
</
EmptyState
>
)
:
null
}
{
multiple
?
(
...
...
src/components/DynamicRecordDialog.jsx
View file @
a34ec141
import
clsx
from
'clsx'
;
import
{
Alert
,
Button
,
Checkbox
,
Input
,
Label
,
Surface
,
TextArea
,
TextField
}
from
'@heroui/react'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
ReferenceSearchSelect
from
'@/components/ReferenceSearchSelect'
;
import
FileUploadField
from
'@/components/FileUploadField'
;
import
FormDatePicker
from
'@/components/FormDatePicker'
;
...
...
@@ -191,7 +193,7 @@ function JsonInput({ value, onChange, placeholder }) {
function
FileInput
({
fieldDef
,
value
,
onChange
})
{
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
'
'
);
const
[
,
setError
]
=
useFeedbackState
(
'error
'
);
const
handleUpload
=
async
(
files
)
=>
{
const
file
=
files
?.[
0
];
...
...
@@ -240,23 +242,26 @@ function FileInput({ fieldDef, value, onChange }) {
triggerLabel=
"选择附件"
/>
{
uploading
?
<
div
className=
"text-sm text-muted"
>
上传中...
</
div
>
:
null
}
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
</
div
>
);
}
function
NestedSubjectFields
({
fieldDef
,
value
,
onChange
,
disabled
,
depth
})
{
const
[
nestedSubject
,
setNestedSubject
]
=
useState
(
null
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useFeedbackState
(
'error'
);
const
[
retryKey
,
setRetryKey
]
=
useState
(
0
);
const
subjectName
=
getReferenceSubjectName
(
fieldDef
);
useEffect
(()
=>
{
let
cancelled
=
false
;
if
(
!
subjectName
||
depth
>=
MAX_SUBJECT_DEPTH
)
{
setNestedSubject
(
null
);
setError
(
''
);
return
undefined
;
}
setNestedSubject
(
null
);
setError
(
''
);
loadNestedSubjectDefinition
(
subjectName
)
.
then
((
result
)
=>
{
if
(
!
cancelled
)
{
...
...
@@ -273,7 +278,7 @@ function NestedSubjectFields({ fieldDef, value, onChange, disabled, depth }) {
return
()
=>
{
cancelled
=
true
;
};
},
[
subjectName
,
depth
]);
},
[
subjectName
,
depth
,
retryKey
,
setError
]);
if
(
!
subjectName
||
depth
>=
MAX_SUBJECT_DEPTH
)
{
const
fieldLabel
=
fieldDef
?.
label
||
fieldDef
?.
name
||
'字段'
;
...
...
@@ -287,7 +292,17 @@ function NestedSubjectFields({ fieldDef, value, onChange, disabled, depth }) {
}
if
(
error
)
{
return
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>;
return
(
<
EmptyState
size=
"sm"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
子对象结构加载失败
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
详细错误见操作提示,请重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Footer
>
<
Button
variant=
"secondary"
onPress=
{
()
=>
setRetryKey
((
key
)
=>
key
+
1
)
}
>
重新加载
</
Button
>
</
EmptyState
.
Footer
>
</
EmptyState
>
);
}
if
(
!
nestedSubject
)
{
...
...
@@ -593,6 +608,7 @@ export default function DynamicRecordDialog({
const
[
values
,
setValues
]
=
useState
({});
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setRequestError
]
=
useFeedbackState
(
'error'
);
const
editing
=
Boolean
(
record
);
const
hideTitle
=
shouldHideViewTitleColumn
(
view
);
const
dialogColumns
=
useMemo
(
...
...
@@ -626,6 +642,7 @@ export default function DynamicRecordDialog({
useEffect
(()
=>
{
if
(
!
open
)
{
setRequestError
(
''
);
return
;
}
setTitle
(
record
?.
title
||
record
?.
metadata
?.
project_name
||
(
...
...
@@ -633,11 +650,13 @@ export default function DynamicRecordDialog({
));
setValues
(
initialValues
);
setError
(
''
);
},
[
open
,
record
,
initialValues
,
editing
,
restrictionDefaults
]);
setRequestError
(
''
);
},
[
open
,
record
,
initialValues
,
editing
,
restrictionDefaults
,
setRequestError
]);
const
handleSubmit
=
async
()
=>
{
setSaving
(
true
);
setError
(
''
);
setRequestError
(
''
);
try
{
const
prepared
=
await
prepareValuesForSubmit
(
subject
,
values
);
const
validationErrors
=
[...
prepared
.
errors
];
...
...
@@ -673,7 +692,7 @@ export default function DynamicRecordDialog({
await
onSaved
?.();
onClose
?.();
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败,请稍后重试'
);
set
Request
Error
(
saveError
?.
message
||
'保存失败,请稍后重试'
);
}
finally
{
setSaving
(
false
);
}
...
...
src/components/ReferenceSearchSelect.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
{
queryRecords
}
from
'@/services/DataService'
;
import
{
Button
,
Label
}
from
'@heroui/react'
;
...
...
@@ -334,7 +335,7 @@ export default function ReferenceSearchSelect({
const
[
options
,
setOptions
]
=
useState
([]);
const
[
selectedById
,
setSelectedById
]
=
useState
({});
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
[
active
,
setActive
]
=
useState
(
false
);
const
lastSearchRequestId
=
useRef
(
0
);
...
...
src/components/SubjectRecordWorkbench.jsx
View file @
a34ec141
import
{
AlertDialog
,
Button
}
from
'@heroui/react'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
RecordWorkbench
from
'@/components/record-workbench'
;
import
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
{
...
...
@@ -146,10 +147,11 @@ export default function SubjectRecordWorkbench({
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
views
,
setViews
]
=
useState
([]);
const
[
tableDisplayConfig
,
setTableDisplayConfig
]
=
useState
({});
const
[
tableDisplayConfigError
,
setTableDisplayConfigError
]
=
useState
(
'
'
);
const
[
,
setTableDisplayConfigError
]
=
useFeedbackState
(
'warning
'
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
success
,
setSuccess
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[,
setSuccess
]
=
useFeedbackState
(
'success'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
activeViewName
,
setActiveViewName
]
=
useState
(
''
);
const
[
filters
,
setFilters
]
=
useState
({});
const
[
pendingFilters
,
setPendingFilters
]
=
useState
({});
...
...
@@ -167,8 +169,9 @@ export default function SubjectRecordWorkbench({
const
requestId
=
requestSequence
.
current
+
1
;
requestSequence
.
current
=
requestId
;
setLoading
(
true
);
set
Error
(
''
);
set
LoadFailed
(
false
);
if
(
!
keepFeedback
)
{
setError
(
''
);
setSuccess
(
''
);
}
...
...
@@ -189,6 +192,7 @@ export default function SubjectRecordWorkbench({
setTableDisplayConfigError
(
resources
.
displayConfigError
||
''
);
}
catch
(
requestError
)
{
if
(
requestId
!==
requestSequence
.
current
)
return
;
setLoadFailed
(
true
);
setRecords
([]);
setTotalRecords
(
0
);
setSubject
(
null
);
...
...
@@ -201,7 +205,7 @@ export default function SubjectRecordWorkbench({
setLoading
(
false
);
}
}
},
[
activeViewName
,
filters
,
hiddenViewNames
,
page
,
pageSize
,
query
,
subjectName
]);
},
[
activeViewName
,
filters
,
hiddenViewNames
,
page
,
pageSize
,
query
,
subjectName
,
setError
,
setSuccess
,
setTableDisplayConfigError
]);
useEffect
(()
=>
{
reload
();
...
...
@@ -278,7 +282,7 @@ export default function SubjectRecordWorkbench({
setSuccess
,
subject
,
subjectName
,
}),
[
reload
,
subject
,
subjectName
]);
}),
[
reload
,
subject
,
subjectName
,
setError
,
setSuccess
]);
const
wrapAction
=
(
action
)
=>
({
...
action
,
...
...
@@ -296,7 +300,8 @@ export default function SubjectRecordWorkbench({
setSuccess
(
''
);
try
{
if
(
onClone
)
{
await
onClone
({
record
,
reload
,
setError
,
setSuccess
,
subjectName
});
const
result
=
await
onClone
({
record
,
reload
,
setError
,
setSuccess
,
subjectName
});
if
(
result
===
false
)
return
;
}
else
{
await
cloneRecord
(
record
.
name
||
record
.
_id
);
}
...
...
@@ -318,7 +323,7 @@ export default function SubjectRecordWorkbench({
setError
(
''
);
try
{
if
(
onDelete
)
{
await
onDelete
({
const
result
=
await
onDelete
({
records
:
deleteTargets
,
selectedRecords
:
deleteTargets
,
reload
,
...
...
@@ -326,6 +331,7 @@ export default function SubjectRecordWorkbench({
setSuccess
,
subjectName
,
});
if
(
result
===
false
)
return
;
}
else
{
await
batchRemoveRecords
({
subject
:
subjectName
,
filter
:
{
_id
:
{
$in
:
ids
}
}
});
}
...
...
@@ -471,10 +477,9 @@ export default function SubjectRecordWorkbench({
),
}
:
null
}
states=
{
{
warning
:
tableDisplayConfigError
,
loading
,
error
,
success
,
loadFailed
,
onRetry
:
()
=>
reload
()
,
loadingTitle
:
`正在加载${title}`
,
loadingDescription
:
'正在读取对象结构、视图配置和记录数据,请稍候。'
,
emptyTitle
:
hasAccessibleView
?
'当前视图暂无数据'
:
'暂无可访问的视图'
,
...
...
src/components/UserSearchSelect.jsx
View file @
a34ec141
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Label
}
from
'@heroui/react'
;
import
{
loadUser
,
queryUsers
}
from
'@/services/DataService'
;
import
{
normalizeUserLabel
,
toArray
}
from
'@/utils/subjectView'
;
...
...
@@ -21,11 +22,13 @@ export default function UserSearchSelect({
const
[
users
,
setUsers
]
=
useState
([]);
const
[
profiles
,
setProfiles
]
=
useState
({});
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useFeedbackState
();
const
selectedIds
=
useMemo
(()
=>
toArray
(
value
).
map
(
getUserId
).
filter
(
Boolean
),
[
value
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
setLoading
(
true
);
setError
(
''
);
const
text
=
keyword
.
trim
();
queryUsers
(
0
,
30
,
text
?
{
$or
:
[
...
...
@@ -42,14 +45,17 @@ export default function UserSearchSelect({
...
Object
.
fromEntries
(
nextUsers
.
map
((
user
)
=>
[
getUserId
(
user
),
user
])),
}));
})
.
catch
(()
=>
{
if
(
!
cancelled
)
setUsers
([]);
.
catch
((
requestError
)
=>
{
if
(
!
cancelled
)
{
setUsers
([]);
setError
(
requestError
?.
message
||
'用户列表加载失败,请重新搜索。'
);
}
})
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
},
[
keyword
]);
},
[
keyword
,
setError
]);
useEffect
(()
=>
{
const
missing
=
selectedIds
.
filter
((
id
)
=>
!
profiles
[
id
]);
...
...
@@ -82,6 +88,7 @@ export default function UserSearchSelect({
<
XinyuanAutocomplete
aria
-
label=
{
ariaLabel
}
inputValue=
{
keyword
}
errorText=
{
error
}
isDisabled=
{
disabled
}
isClearable=
{
selectedIds
.
length
>
0
}
isLoading=
{
loading
}
...
...
src/components/record-workbench/index.jsx
View file @
a34ec141
...
...
@@ -26,11 +26,11 @@ import FilterSelect from '@/components/FilterSelect';
import
FormDatePicker
from
'@/components/FormDatePicker'
;
import
ReferenceSearchSelect
from
'@/pages/RecordDetailDrawer/components/ReferenceSearchSelect'
;
import
UserSearchSelect
from
'@/components/UserSearchSelect'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
TableFieldValue
from
'@/components/TableFieldValue'
;
import
{
getTableFieldDisplayConfig
}
from
'@/utils/tableFieldDisplay'
;
import
{
formatFieldValue
,
getFieldValue
,
normalizeText
}
from
'@/utils/subjectView'
;
import
{
Alert
,
Avatar
,
Button
,
Chip
,
...
...
@@ -43,7 +43,6 @@ import {
ToggleButton
,
Tooltip
,
buttonVariants
,
toast
,
}
from
'@heroui/react'
;
import
clsx
from
'clsx'
;
import
{
Fragment
,
useEffect
,
useLayoutEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
...
...
@@ -554,13 +553,8 @@ export default function RecordWorkbench({
const
[
expandedRowIds
,
setExpandedRowIds
]
=
useState
({});
const
filterFieldVariant
=
'secondary'
;
const
loading
=
Boolean
(
states
?.
loading
);
const
error
=
states
?.
error
||
''
;
const
loadFailed
=
Boolean
(
states
?.
loadFailed
)
;
const
totalRecords
=
pagination
?.
totalRecords
??
records
.
length
;
useEffect
(()
=>
{
if
(
states
?.
success
)
{
toast
.
success
(
states
.
success
,
{
timeout
:
4000
});
}
},
[
states
?.
success
]);
const
columnGroupMeta
=
useMemo
(
()
=>
buildColumnGroupMeta
(
columnGroups
,
columns
),
[
columnGroups
,
columns
],
...
...
@@ -1196,19 +1190,19 @@ export default function RecordWorkbench({
) : null}
</div>
{
error
? (
<
Alert status="danger
">
<
Alert.Content><Alert.Description>{error}</Alert.Description></Alert.Content
>
</Alert
>
) : null}
{states?.warning ? (
<Alert status="warning"
>
<
Alert.Content><Alert.Description>{states.warning}</Alert.Description></Alert.Content
>
</
Alert
>
{
!loading && loadFailed
? (
<
EmptyState className="min-h-48" size="md
">
<
EmptyState.Header
>
<EmptyState.Title>{title || '列表'}加载失败</EmptyState.Title
>
<EmptyState.Description>详细错误见操作提示,请重新加载。</EmptyState.Description>
</EmptyState.Header>
<EmptyState.Footer>
<Button variant="secondary" onPress={states.onRetry || onQuery}>重新加载</Button
>
<
/EmptyState.Footer
>
</
EmptyState
>
) : null}
<div ref={tableScrollRef} className={tableContentClass}>
<div ref={tableScrollRef} className={
!loading && loadFailed ? 'hidden' :
tableContentClass}>
<DataTable
ariaLabel={title || '业务数据列表'}
className={stickyTable ? 'rc-data-table-sticky' : ''}
...
...
src/design-system/feedback.css
0 → 100644
View file @
a34ec141
/* Keep operation feedback 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 @
a34ec141
import
{
feedbackQueue
}
from
'@/services/feedbackQueue'
;
import
{
useCallback
,
useEffect
,
useRef
,
useState
}
from
'react'
;
const
FEEDBACK_OPTIONS
=
{
error
:
{
title
:
'操作失败'
,
variant
:
'danger'
,
timeout
:
8000
},
success
:
{
title
:
'操作成功'
,
variant
:
'success'
,
timeout
:
4000
},
warning
:
{
title
:
'请注意'
,
variant
:
'warning'
,
timeout
:
8000
},
};
// Dismissing feedback must not clear the request's failure state or 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
)
feedbackQueue
.
close
(
toastKeyRef
.
current
);
toastKeyRef
.
current
=
null
;
setMessage
(
nextMessage
);
if
(
!
nextMessage
)
return
;
const
{
title
,
variant
,
timeout
}
=
FEEDBACK_OPTIONS
[
tone
]
||
FEEDBACK_OPTIONS
.
error
;
// Every attempt reports feedback, including identical errors on a retry.
toastKeyRef
.
current
=
feedbackQueue
.
add
(
{
title
,
description
:
nextMessage
,
variant
},
{
timeout
},
);
},
[
tone
]);
useEffect
(()
=>
{
isMountedRef
.
current
=
true
;
return
()
=>
{
isMountedRef
.
current
=
false
;
if
(
toastKeyRef
.
current
)
feedbackQueue
.
close
(
toastKeyRef
.
current
);
toastKeyRef
.
current
=
null
;
};
},
[]);
return
[
message
,
updateMessage
];
}
src/pages/CompareRecordField.jsx
View file @
a34ec141
import
{
loadRecord
}
from
'@/services/DataService'
;
import
{
Alert
,
Button
,
Spinner
,
Surface
}
from
'@heroui/react'
;
import
{
Button
,
Spinner
,
Surface
}
from
'@heroui/react'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
...
...
@@ -16,10 +18,13 @@ export default function CompareRecordField() {
const
[
oldRecord
,
setOldRecord
]
=
useState
(
null
);
const
[
newRecord
,
setNewRecord
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useFeedbackState
(
'error'
);
const
[
retryKey
,
setRetryKey
]
=
useState
(
0
);
useEffect
(()
=>
{
let
cancelled
=
false
;
setLoading
(
true
);
setError
(
''
);
(
async
()
=>
{
if
(
recordNames
.
length
<
2
||
!
field
)
{
setError
(
'缺少需要比较的记录或字段'
);
...
...
@@ -39,7 +44,7 @@ export default function CompareRecordField() {
}
})();
return
()
=>
{
cancelled
=
true
;
};
},
[
field
,
recordNames
]);
},
[
field
,
recordNames
,
retryKey
,
setError
]);
const
readField
=
(
record
)
=>
record
?.
metadata
?.[
field
]
??
record
?.[
field
];
...
...
@@ -58,7 +63,17 @@ export default function CompareRecordField() {
</
div
>
<
Button
variant=
"secondary"
onPress=
{
()
=>
history
.
back
()
}
>
返回
</
Button
>
</
div
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
{
error
?
(
<
EmptyState
className=
"min-h-48"
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
历史版本加载失败
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
详细错误见操作提示,请重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Footer
>
<
Button
variant=
"secondary"
onPress=
{
()
=>
setRetryKey
((
key
)
=>
key
+
1
)
}
>
重新加载
</
Button
>
</
EmptyState
.
Footer
>
</
EmptyState
>
)
:
null
}
{
!
error
?
(
<
div
className=
"grid gap-4 lg:grid-cols-2"
>
{
[{
label
:
'旧版本'
,
record
:
oldRecord
},
{
label
:
'新版本'
,
record
:
newRecord
}].
map
(({
label
,
record
})
=>
(
...
...
src/pages/Login.jsx
View file @
a34ec141
...
...
@@ -7,7 +7,8 @@ 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
useFeedbackState
from
'@/hooks/useFeedbackState'
;
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 @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
DataTable
,
DataTableEmptyState
}
from
'@/components/DataTable'
;
import
{
Alert
,
Spinner
}
from
'@heroui/react'
;
import
{
Alert
,
Button
,
Spinner
}
from
'@heroui/react'
;
import
{
loadSubject
,
queryRecords
}
from
'@/services/DataService'
;
import
{
formatDateText
,
...
...
@@ -131,7 +132,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
;
...
...
@@ -195,7 +197,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
(
...
...
@@ -232,15 +234,13 @@ export default function ReferenceRecordsTable({ fieldDef, value, pageSize = 1000
return
(
<
div
className=
"space-y-2"
>
{
error
?
(
<
Alert
status=
"warning"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
<
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
setRetryKey
((
key
)
=>
key
+
1
)
}
>
重新加载关联明细
</
Button
>
)
:
null
}
<
ReferenceRecordsFrame
action=
{
action
}
count=
{
records
.
length
}
>
{
records
.
length
===
0
?
(
<
DataTableEmptyState
title=
"暂无可展示的关联明细"
/>
<
DataTableEmptyState
title=
{
error
?
'关联明细加载失败,详细错误见操作提示'
:
'暂无可展示的关联明细'
}
/>
)
:
(
<
DataTable
ariaLabel=
"关联记录"
...
...
src/pages/RecordDetailDrawer/components/TailwindEditableField.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
clsx
from
'clsx'
;
import
{
...
...
@@ -191,7 +193,8 @@ function NestedSubjectEditor({
const
multiple
=
Boolean
(
fieldDef
?.
multiple
);
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useFeedbackState
(
'error'
);
const
[
retryKey
,
setRetryKey
]
=
useState
(
0
);
const
[
draft
,
setDraft
]
=
useState
(()
=>
multiple
?
(
Array
.
isArray
(
value
)
?
value
:
[])
:
value
||
{
metadata
:
{}
},
);
...
...
@@ -244,7 +247,7 @@ function NestedSubjectEditor({
return
()
=>
{
cancelled
=
true
;
};
},
[
multiple
,
subjectName
]);
},
[
multiple
,
subjectName
,
retryKey
,
setError
]);
const
handleNestedChange
=
(
childPath
,
nextValue
,
itemIndex
)
=>
{
const
currentItem
=
multiple
?
draft
[
itemIndex
]
||
{
metadata
:
{}
}
:
draft
;
...
...
@@ -296,7 +299,17 @@ function NestedSubjectEditor({
if
(
error
||
!
subject
)
{
return
(
<
div
className=
"space-y-2"
>
<
FieldError
>
{
error
||
'子对象字段定义不可用。'
}
</
FieldError
>
<
EmptyState
size=
"sm"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
子对象字段定义不可用
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
详细错误见操作提示,请检查 Subject 配置或重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
{
subjectName
?
(
<
EmptyState
.
Footer
>
<
Button
variant=
"secondary"
onPress=
{
()
=>
setRetryKey
((
key
)
=>
key
+
1
)
}
>
重新加载
</
Button
>
</
EmptyState
.
Footer
>
)
:
null
}
</
EmptyState
>
<
FieldValue
value=
{
value
}
fieldDef=
{
fieldDef
}
/>
</
div
>
);
...
...
@@ -461,6 +474,8 @@ function FieldValue({ value, fieldDef }) {
function
UserSelector
({
value
,
onChange
,
multiple
,
variant
})
{
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
options
,
setOptions
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useFeedbackState
(
'error'
);
const
selectedIds
=
useMemo
(
()
=>
toArray
(
value
).
map
((
item
)
=>
item
?.
_id
||
item
).
filter
(
Boolean
),
[
value
],
...
...
@@ -468,6 +483,8 @@ function UserSelector({ value, onChange, multiple, variant }) {
useEffect
(()
=>
{
let
cancelled
=
false
;
setLoading
(
true
);
setError
(
''
);
const
loadOptions
=
async
()
=>
{
const
filter
=
keyword
.
trim
()
...
...
@@ -487,7 +504,10 @@ function UserSelector({ value, onChange, multiple, variant }) {
}
catch
(
error
)
{
if
(
!
cancelled
)
{
setOptions
([]);
setError
(
error
?.
message
||
'用户列表加载失败,请重新搜索。'
);
}
}
finally
{
if
(
!
cancelled
)
setLoading
(
false
);
}
};
...
...
@@ -496,7 +516,7 @@ function UserSelector({ value, onChange, multiple, variant }) {
return
()
=>
{
cancelled
=
true
;
};
},
[
keyword
]);
},
[
keyword
,
setError
]);
const
handleSelectionChange
=
(
keys
)
=>
{
if
(
!
multiple
)
{
...
...
@@ -527,6 +547,8 @@ function UserSelector({ value, onChange, multiple, variant }) {
{
...
selectionProps
}
aria
-
label=
"用户"
emptyText=
"暂无用户"
errorText=
{
error
}
isLoading=
{
loading
}
inputValue=
{
keyword
}
isClearable=
{
selectedIds
.
length
>
0
}
options=
{
autocompleteOptions
}
...
...
@@ -608,6 +630,7 @@ function ControlledFieldEditor({
nestedDepth
=
0
,
})
{
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setRequestError
]
=
useFeedbackState
(
'error'
);
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
fieldType
=
fieldDef
?.
type
||
'text'
;
const
isDateField
=
isDateFieldType
(
fieldType
);
...
...
@@ -620,6 +643,7 @@ function ControlledFieldEditor({
setUploading
(
true
);
setError
(
''
);
setRequestError
(
''
);
onValidationChange
?.(
fieldPath
,
'附件正在上传,请稍候。'
,
editSessionId
);
try
{
...
...
@@ -627,7 +651,7 @@ function ControlledFieldEditor({
const
fileValue
=
uploaded
?.
file
||
uploaded
?.
data
||
uploaded
;
onChange
?.(
fieldDef
?.
multiple
?
[...
toArray
(
value
),
fileValue
]
:
fileValue
);
}
catch
(
uploadError
)
{
setError
(
uploadError
?.
message
||
'附件上传失败,请稍后重试。'
);
set
Request
Error
(
uploadError
?.
message
||
'附件上传失败,请稍后重试。'
);
}
finally
{
setUploading
(
false
);
onValidationChange
?.(
fieldPath
,
''
,
editSessionId
);
...
...
@@ -863,6 +887,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
(()
=>
{
...
...
@@ -906,6 +931,7 @@ export default function TailwindEditableField({
const
save
=
async
(
nextValue
=
draft
)
=>
{
setError
(
''
);
setRequestError
(
''
);
if
(
fieldType
===
'object'
&&
typeof
nextValue
===
'string'
&&
nextValue
.
trim
())
{
try
{
...
...
@@ -920,7 +946,7 @@ export default function TailwindEditableField({
await
onSave
(
asSaveValue
(
nextValue
,
fieldDef
));
setEditing
(
false
);
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败,请稍后重试。'
);
set
Request
Error
(
saveError
?.
message
||
'保存失败,请稍后重试。'
);
}
};
...
...
@@ -933,6 +959,7 @@ export default function TailwindEditableField({
const
cancel
=
()
=>
{
setDraft
(
value
);
setError
(
''
);
setRequestError
(
''
);
setEditing
(
false
);
};
...
...
@@ -944,6 +971,7 @@ export default function TailwindEditableField({
setUploading
(
true
);
setError
(
''
);
setRequestError
(
''
);
try
{
const
uploaded
=
await
uploadFile
(
file
);
...
...
@@ -953,7 +981,7 @@ export default function TailwindEditableField({
await
onSave
(
nextValue
);
setEditing
(
false
);
}
catch
(
uploadError
)
{
setError
(
uploadError
?.
message
||
'附件上传失败,请稍后重试。'
);
set
Request
Error
(
uploadError
?.
message
||
'附件上传失败,请稍后重试。'
);
}
finally
{
setUploading
(
false
);
}
...
...
@@ -961,6 +989,7 @@ export default function TailwindEditableField({
const
handleRemoveFile
=
async
(
indexToRemove
)
=>
{
setError
(
''
);
setRequestError
(
''
);
const
nextValue
=
fieldDef
?.
multiple
?
toArray
(
draft
).
filter
((
_
,
index
)
=>
index
!==
indexToRemove
)
...
...
@@ -974,7 +1003,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 @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
useEffect
,
useRef
,
useState
}
from
'react'
;
import
{
getSubjectView
,
...
...
@@ -75,7 +76,7 @@ export function useSubjectRecord(
const [subject, setSubject] = useState(null);
const [views, setViews] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = use
State('
');
const [error, setError] = use
FeedbackState('error
');
const requestIdRef = useRef(0);
useEffect(() => {
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Close
,
Fullscreen
,
FullscreenExit
}
from
'@/components/AppIcons'
;
import
{
Alert
,
Button
,
Modal
,
Spinner
,
Tabs
,
Tooltip
}
from
'@heroui/react'
;
import
React
,
{
...
...
@@ -84,7 +86,8 @@ const RecordDetailDrawer = (props, ref) => {
const
[
draftRecord
,
setDraftRecord
]
=
useState
(
null
);
const
[
dirtyFields
,
setDirtyFields
]
=
useState
({});
const
[
validationErrors
,
setValidationErrors
]
=
useState
({});
const
[
saveError
,
setSaveError
]
=
useState
(
''
);
const
[,
setSaveError
]
=
useFeedbackState
(
'error'
);
const
[,
setSaveSuccess
]
=
useFeedbackState
(
'success'
);
const
editSessionRef
=
useRef
(
0
);
const
pendingLocalMaterialsRef
=
useRef
({});
...
...
@@ -117,9 +120,10 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields
({});
setValidationErrors
({});
setSaveError
(
''
);
setSaveSuccess
(
''
);
pendingLocalMaterialsRef
.
current
=
{};
editSessionRef
.
current
+=
1
;
},
[
open
,
recordName
]);
},
[
open
,
recordName
,
setSaveError
,
setSaveSuccess
]);
useEffect
(()
=>
{
if
(
!
isEditing
)
{
...
...
@@ -229,6 +233,7 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields
({});
setValidationErrors
({});
setSaveError
(
''
);
setSaveSuccess
(
''
);
pendingLocalMaterialsRef
.
current
=
{};
};
...
...
@@ -245,6 +250,7 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields
({});
setValidationErrors
({});
setSaveError
(
''
);
setSaveSuccess
(
''
);
pendingLocalMaterialsRef
.
current
=
{};
setIsEditing
(
true
);
};
...
...
@@ -358,6 +364,7 @@ const RecordDetailDrawer = (props, ref) => {
setIsSaving
(
true
);
setSaveError
(
''
);
setSaveSuccess
(
''
);
try
{
const
pendingMaterials
=
Object
.
entries
(
pendingLocalMaterialsRef
.
current
)
...
...
@@ -389,6 +396,7 @@ const RecordDetailDrawer = (props, ref) => {
setValidationErrors
({});
pendingLocalMaterialsRef
.
current
=
{};
onDataChange
?.(
true
);
setSaveSuccess
(
'详情已保存。'
);
}
catch
(
saveFailure
)
{
setSaveError
(
saveFailure
?.
message
||
'保存失败,请稍后重试。'
);
}
finally
{
...
...
@@ -485,18 +493,17 @@ const RecordDetailDrawer = (props, ref) => {
<
Spinner
size=
"sm"
/>
</
div
>
)
:
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
>
</
Alert
>
<
EmptyState
className=
"min-h-48"
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
详情加载失败
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
详细错误见操作提示,请重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Footer
>
<
Button
variant=
"secondary"
onPress=
{
()
=>
setRefreshKey
((
key
)
=>
key
+
1
)
}
>
重新加载
</
Button
>
</
EmptyState
.
Footer
>
</
EmptyState
>
)
:
(
<
div
>
{
saveError
?
(
<
Alert
className=
"mb-4"
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
saveError
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
{
isRecordLocked
?
(
<
Alert
status=
"warning"
>
<
Alert
.
Content
>
...
...
src/pages/problem_rectification/ApplicationSwitcherDialog.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
xinyuanLogo
from
'@/assets/xy-logo-red.png'
;
import
{
APPLICATION_DISPLAY_NAME
,
...
...
@@ -5,12 +6,11 @@ import {
}
from
'@/config/application'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
EmptyStateSurface
}
from
'@/components/DataTable'
;
import
{
ExclamationTriangleIcon
,
Search
as
SearchIcon
}
from
'@/components/AppIcons'
;
import
{
Search
as
SearchIcon
}
from
'@/components/AppIcons'
;
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
,
...
...
@@ -285,7 +285,7 @@ export default function ApplicationSwitcherDialog({
const
[
status
,
setStatus
]
=
useState
(
fallbackApplications
.
length
>
0
?
'success'
:
'idle'
,
);
const
[
errorMessage
,
setErrorMessage
]
=
useState
(
'
'
);
const
[
,
setErrorMessage
]
=
useFeedbackState
(
'warning
'
);
const
[
applicationSearch
,
setApplicationSearch
]
=
useState
(
''
);
const
currentApplicationName
=
useMemo
(()
=>
{
return
applications
.
find
(
isCurrentApplication
)?.
name
||
CURRENT_APPLICATION_NAME
;
...
...
@@ -319,6 +319,7 @@ export default function ApplicationSwitcherDialog({
useEffect
(()
=>
{
if
(
!
open
)
{
setApplicationSearch
(
''
);
setErrorMessage
(
''
);
return
;
}
...
...
@@ -398,13 +399,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/pages/problem_rectification/archive/index.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
TableFieldValue
from
'@/components/TableFieldValue'
;
import
{
getTableFieldDisplayConfig
}
from
'@/utils/tableFieldDisplay'
;
import
{
getFieldValue
}
from
'@/utils/subjectView'
;
...
...
@@ -20,9 +21,10 @@ export default function ProblemRectificationArchivePage() {
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
views
,
setViews
]
=
useState
([]);
const
[
tableDisplayConfig
,
setTableDisplayConfig
]
=
useState
({});
const
[
tableDisplayConfigError
,
setTableDisplayConfigError
]
=
useState
(
'
'
);
const
[
,
setTableDisplayConfigError
]
=
useFeedbackState
(
'warning
'
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
filters
,
setFilters
]
=
useState
({
keyword
:
''
,
});
...
...
@@ -33,6 +35,7 @@ export default function ProblemRectificationArchivePage() {
const
reloadList
=
useCallback
(
async
()
=>
{
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
try
{
...
...
@@ -43,6 +46,7 @@ export default function ProblemRectificationArchivePage() {
setTableDisplayConfig
(
resources
.
displayConfig
||
{});
setTableDisplayConfigError
(
resources
.
displayConfigError
||
''
);
}
catch
(
requestError
)
{
setLoadFailed
(
true
);
setRecords
([]);
setSubject
(
null
);
setViews
([]);
...
...
@@ -172,9 +176,9 @@ export default function ProblemRectificationArchivePage() {
),
}
}
states=
{
{
warning
:
tableDisplayConfigError
,
loading
,
error
,
loadFailed
,
onRetry
:
reloadList
,
loadingTitle
:
'正在加载归档批次'
,
loadingDescription
:
'正在读取归档对象、视图配置和批次记录,请稍候。'
,
emptyTitle
:
'暂无归档批次'
,
...
...
src/pages/problem_rectification/components/ProblemRectificationMaterialField.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
ArrowPathIcon
,
Delete
,
...
...
@@ -9,7 +10,6 @@ import {
uploadProblemRectificationFile
,
}
from
'../problemRectificationMaterialService'
;
import
{
Alert
,
Button
,
Label
,
Tabs
,
...
...
@@ -60,10 +60,10 @@ export default function ProblemRectificationMaterialField({
const
[
debouncedKeyword
,
setDebouncedKeyword
]
=
useState
(
''
);
const
[
materials
,
setMaterials
]
=
useState
([]);
const
[
materialsLoading
,
setMaterialsLoading
]
=
useState
(
false
);
const
[
materialsError
,
setMaterialsError
]
=
use
State
(
'
'
);
const
[
materialsError
,
setMaterialsError
]
=
use
FeedbackState
(
'error
'
);
const
[
retryKey
,
setRetryKey
]
=
useState
(
0
);
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
[
uploadError
,
setUploadError
]
=
useState
(
'
'
);
const
[
,
setUploadError
]
=
useFeedbackState
(
'error
'
);
const
[
localFileKeys
,
setLocalFileKeys
]
=
useState
(()
=>
new
Set
());
const
fileValues
=
useMemo
(
()
=>
(
multiple
?
toFileArray
(
value
)
:
toFileArray
(
value
).
slice
(
0
,
1
)),
...
...
@@ -322,15 +322,10 @@ export default function ProblemRectificationMaterialField({
/>
{materialsError ? (
<Alert status="danger">
<Alert.Content>
<Alert.Description>{materialsError}</Alert.Description>
</Alert.Content>
<Button size="sm" variant="danger-soft" onPress={() => setRetryKey((key) => key + 1)}>
<Button size="sm" variant="secondary" onPress={() => setRetryKey((key) => key + 1)}>
<ArrowPathIcon size={14} />
重试
重新加载材料
</Button>
</Alert>
) : null}
</div>
</Tabs.Panel>
...
...
@@ -346,11 +341,6 @@ export default function ProblemRectificationMaterialField({
triggerLabel={uploading ? '上传中...' : '选择本地文件'}
onFilesChange={handleLocalUpload}
/>
{uploadError ? (
<Alert status="danger">
<Alert.Content><Alert.Description>{uploadError}</Alert.Description></Alert.Content>
</Alert>
) : null}
</div>
</Tabs.Panel>
</Tabs>
...
...
src/pages/problem_rectification/dashboard/index.jsx
View file @
a34ec141
import
{
DataTable
}
from
'@/components/DataTable'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
createResolvedXinyuanChartColorMap
,
getXinyuanChartColor
,
...
...
@@ -17,7 +18,6 @@ import {
ExclamationTriangleIcon
,
}
from
'@/components/AppIcons'
;
import
{
Alert
,
Button
,
Card
,
Chip
,
...
...
@@ -459,24 +459,28 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
isEmpty
:
false
,
});
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
useEffect
(()
=>
{
let
cancelled
=
false
;
(
async
()
=>
{
setLoading
(
true
);
setError
(
''
);
const
result
=
await
loadProblemRectificationDashboardData
();
if
(
cancelled
)
{
return
;
}
setDashboardData
(
result
);
setError
(
result
.
error
||
''
);
setLoading
(
false
);
})();
return
()
=>
{
cancelled
=
true
;
};
},
[]);
},
[
reloadKey
,
setError
]);
const
groups
=
dashboardData
.
groups
||
[];
const
yearOptions
=
dashboardData
.
yearOptions
||
[];
...
...
@@ -588,14 +592,6 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
</
div
>
</
div
>
{
dashboardData
.
error
?
(
<
Alert
status=
"warning"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
dashboardData
.
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
{
loading
?
(
<
div
className=
"flex min-h-48 items-center justify-center gap-2"
role=
"status"
>
<
Spinner
size=
"sm"
/>
...
...
@@ -603,15 +599,27 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
</
div
>
)
:
null
}
{
!
loading
&&
dashboardData
.
isEmpty
?
(
{
!
loading
&&
dashboardData
.
error
?
(
<
EmptyState
className=
"min-h-48"
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
{
dashboardTitle
}
加载失败
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
详细错误见操作提示,请重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Footer
>
<
Button
variant=
"secondary"
onPress=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
>
重新加载
</
Button
>
</
EmptyState
.
Footer
>
</
EmptyState
>
)
:
null
}
{
!
loading
&&
!
dashboardData
.
error
&&
dashboardData
.
isEmpty
?
(
<
EmptyBlock
title=
"暂无问题整改数据"
/>
)
:
null
}
{
!
loading
&&
!
dashboardData
.
isEmpty
&&
activeScope
.
isEmpty
?
(
{
!
loading
&&
!
dashboardData
.
error
&&
!
dashboardData
.
isEmpty
&&
activeScope
.
isEmpty
?
(
<
EmptyBlock
title=
{
`${yearLabel}暂无问题整改数据`
}
/>
)
:
null
}
{
!
loading
&&
!
dashboardData
.
isEmpty
&&
!
activeScope
.
isEmpty
?
(
{
!
loading
&&
!
dashboardData
.
error
&&
!
dashboardData
.
isEmpty
&&
!
activeScope
.
isEmpty
?
(
<>
{
!
isBusinessDashboard
?
(
<>
...
...
src/pages/problem_rectification/flow-table/index.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
ArrowPathIcon
,
CalendarDaysIcon
,
...
...
@@ -21,7 +22,7 @@ import {
import
{
normalizeText
,
toArray
}
from
'@/utils/subjectView'
;
import
clsx
from
'clsx'
;
import
dayjs
from
'dayjs'
;
import
{
Alert
,
Button
,
Card
,
Chip
,
Spinner
,
TextArea
,
Toast
,
Tooltip
,
toast
}
from
'@heroui/react'
;
import
{
Button
,
Card
,
Chip
,
Spinner
,
TextArea
,
Tooltip
}
from
'@heroui/react'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useSearchParams
}
from
'umi'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
...
...
@@ -416,8 +417,9 @@ export default function ProblemRectificationFlowTablePage() {
const
scopedRecordId
=
searchParams
.
get
(
'record'
)
||
''
;
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
submittingKey
,
setSubmittingKey
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
success
,
setSuccess
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[,
setSuccess
]
=
useFeedbackState
(
'success'
);
const
[
messageRecord
,
setMessageRecord
]
=
useState
(
null
);
const
[
records
,
setRecords
]
=
useState
([]);
const
[
userLabels
,
setUserLabels
]
=
useState
({});
...
...
@@ -463,12 +465,14 @@ export default function ProblemRectificationFlowTablePage() {
const
loadFlow
=
async
()
=>
{
if
(
!
messageId
)
{
setLoadFailed
(
true
);
setError
(
'缺少流程 ID。'
);
setLoading
(
false
);
return
;
}
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
try
{
...
...
@@ -481,6 +485,7 @@ export default function ProblemRectificationFlowTablePage() {
setRecords
(
relatedRecords
.
filter
(
Boolean
));
setDrafts
({});
}
catch
(
requestError
)
{
setLoadFailed
(
true
);
setError
(
requestError
?.
message
||
'流程表格加载失败,请稍后重试。'
);
setMessageRecord
(
null
);
setRecords
([]);
...
...
@@ -1778,12 +1783,6 @@ export default function ProblemRectificationFlowTablePage() {
},
];
useEffect
(()
=>
{
if
(
!
success
)
return
;
toast
.
success
(
success
,
{
timeout
:
4000
});
setSuccess
(
''
);
},
[
success
]);
useEffect
(()
=>
{
document
.
title
=
'整改流程 - 问题整改'
;
},
[]);
...
...
@@ -1835,13 +1834,6 @@ export default function ProblemRectificationFlowTablePage() {
</
Card
.
Content
>
</
Card
>
{
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
<
section
className=
"rc-list-stack"
>
<
div
className=
"rc-page-toolbar"
>
<
div
className=
"flex min-w-0 items-center gap-3"
>
...
...
@@ -1861,6 +1853,11 @@ export default function ProblemRectificationFlowTablePage() {
<
div
className=
"flex min-h-56 items-center justify-center"
role=
"status"
aria
-
label=
"正在加载流程表格"
>
<
Spinner
size=
"md"
/>
</
div
>
)
:
loadFailed
?
(
<
div
className=
"space-y-3"
>
<
DataTableEmptyState
title=
"流程表格加载失败,详细错误见操作提示"
/>
<
Button
variant=
"secondary"
onPress=
{
loadFlow
}
>
重新加载
</
Button
>
</
div
>
)
:
rows
.
length
===
0
?
(
<
DataTableEmptyState
title=
{
isDelayFlow
?
'当前流程没有可处理的延期申请'
:
'当前流程没有可处理的整改明细'
}
/>
)
:
(
...
...
@@ -1877,7 +1874,6 @@ export default function ProblemRectificationFlowTablePage() {
)
}
</
section
>
</
div
>
<
Toast
.
Provider
maxVisibleToasts=
{
3
}
placement=
"top"
/>
</
main
>
);
}
src/pages/problem_rectification/form/index.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
CheckCircleIcon
,
ClipboardDocumentListIcon
,
...
...
@@ -130,13 +131,14 @@ function StatusTag({ value }) {
);
}
function
EmptyState
({
title
,
description
})
{
function
EmptyState
({
title
,
description
,
action
})
{
return
(
<
ProEmptyState
className=
"min-h-64"
size=
"md"
>
<
ProEmptyState
.
Header
>
<
ProEmptyState
.
Title
>
{
title
}
</
ProEmptyState
.
Title
>
{
description
?
<
ProEmptyState
.
Description
>
{
description
}
</
ProEmptyState
.
Description
>
:
null
}
</
ProEmptyState
.
Header
>
{
action
?
<
ProEmptyState
.
Footer
>
{
action
}
</
ProEmptyState
.
Footer
>
:
null
}
</
ProEmptyState
>
);
}
...
...
@@ -163,12 +165,14 @@ export default function ProblemRectificationFormPage() {
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
submitting
,
setSubmitting
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
flowData
,
setFlowData
]
=
useState
(
null
);
const
[
selectedRecord
,
setSelectedRecord
]
=
useState
(
null
);
const
reload
=
async
()
=>
{
if
(
!
messageId
)
{
setLoadFailed
(
true
);
setFlowData
(
null
);
setLoading
(
false
);
setError
(
'缺少流程 ID,无法加载整改流程。'
);
...
...
@@ -176,12 +180,14 @@ export default function ProblemRectificationFormPage() {
}
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
try
{
const
result
=
await
loadProblemRectificationFlow
(
messageId
,
userInfo
);
setFlowData
(
result
);
}
catch
(
requestError
)
{
setLoadFailed
(
true
);
setFlowData
(
null
);
setError
(
requestError
?.
message
||
'整改流程加载失败,请稍后重试。'
);
}
finally
{
...
...
@@ -380,12 +386,8 @@ export default function ProblemRectificationFormPage() {
<
div
className=
"grid min-h-64 place-items-center"
role=
"status"
aria
-
label=
"正在加载整改流程"
>
<
Spinner
/>
</
div
>
)
:
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
loadFailed
?
(
<
EmptyState
title=
"整改流程加载失败"
description=
"详细错误见操作提示,请重新加载。"
action=
{
<
Button
variant=
"secondary"
onPress=
{
reload
}
>
重新加载
</
Button
>
}
/>
)
:
!
messageRecord
?
(
<
EmptyState
title=
"未找到流程数据"
description=
"请确认消息链接中的 id 是否正确,或检查流程主单是否已存在。"
/>
)
:
(
...
...
src/pages/problem_rectification/list/index.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
FormDatePicker
from
'@/components/FormDatePicker'
;
import
ResourceFilterMultiSelect
from
'@/components/ResourceFilterMultiSelect'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
...
...
@@ -48,7 +49,6 @@ import {
import
clsx
from
'clsx'
;
import
dayjs
from
'dayjs'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
toast
}
from
'@heroui/react'
;
import
{
formatFieldValue
,
getFieldNameFromPath
,
...
...
@@ -793,10 +793,11 @@ export default function ProblemRectificationListPage() {
const [subject, setSubject] = useState(null);
const [views, setViews] = useState([]);
const [tableDisplayConfig, setTableDisplayConfig] = useState({});
const [
tableDisplayConfigError, setTableDisplayConfigError] = useState('
');
const [
, setTableDisplayConfigError] = useFeedbackState('warning
');
const [loading, setLoading] = useState(true);
const [recordsLoading, setRecordsLoading] = useState(false);
const [error, setError] = useState('');
const [, setError] = useFeedbackState('error');
const [loadFailed, setLoadFailed] = useState(false);
const [actionLoading, setActionLoading] = useState(false);
const [filters, setFilters] = useState({});
const [pendingFilters, setPendingFilters] = useState({});
...
...
@@ -814,7 +815,7 @@ export default function ProblemRectificationListPage() {
const [auditorUsers, setAuditorUsers] = useState([]);
const [departmentAssignments, setDepartmentAssignments] = useState([]);
const [delayDepartmentAssignments, setDelayDepartmentAssignments] = useState([]);
const [
successMessage, setSuccessMessage] = useState('
');
const [
, setSuccessMessage] = useFeedbackState('success
');
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE);
const [pagination, setPagination] = useState({
...
...
@@ -838,6 +839,7 @@ export default function ProblemRectificationListPage() {
const reloadResources = useCallback(async () => {
setLoading(true);
setLoadFailed(false);
setError('');
setSuccessMessage('');
...
...
@@ -848,6 +850,7 @@ export default function ProblemRectificationListPage() {
setTableDisplayConfig(resources.displayConfig || {});
setTableDisplayConfigError(resources.displayConfigError || '');
} catch (requestError) {
setLoadFailed(true);
setRecords([]);
setSubject(null);
setViews([]);
...
...
@@ -863,12 +866,6 @@ export default function ProblemRectificationListPage() {
reloadResources();
}, [reloadResources]);
useEffect(() => {
if (!successMessage) return;
toast.success(successMessage, { timeout: 4000 });
setSuccessMessage('');
}, [successMessage]);
useEffect(() => {
const userIds = Array.from(
new Set(
...
...
@@ -1009,6 +1006,7 @@ export default function ProblemRectificationListPage() {
const nextFilters = overrides.filters ?? filters;
setRecordsLoading(true);
setLoadFailed(false);
setError('');
if (!overrides.keepSuccessMessage) {
setSuccessMessage('');
...
...
@@ -1049,6 +1047,7 @@ export default function ProblemRectificationListPage() {
setPage(nextPagination.totalPages - 1);
}
} catch (requestError) {
setLoadFailed(true);
setRecords([]);
setGridViewCounts({});
setPagination({
...
...
@@ -1958,9 +1957,8 @@ export default function ProblemRectificationListPage() {
} : null}
states={{
loading: loading || recordsLoading,
error,
warning: tableDisplayConfigError,
success: '',
loadFailed,
onRetry: () => subject && activeGridView ? reloadList() : reloadResources(),
loadingTitle: '正在加载问题整改列表',
loadingDescription: '正在读取对象结构、视图配置和记录数据,请稍候。',
emptyTitle: activeGridView ? '当前视图暂无数据' : '暂无可访问的视图',
...
...
src/pages/problem_rectification/notifications/NotificationListPage.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
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 +11,6 @@ import {
SearchField
,
Spinner
,
Tabs
,
toast
,
}
from
'@heroui/react'
;
import
{
useCallback
,
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
...
...
@@ -150,7 +149,9 @@ export default function NotificationListPage() {
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
unreadTotal
,
setUnreadTotal
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useFeedbackState
(
'error'
);
const
[,
setOperationError
]
=
useFeedbackState
(
'error'
);
const
[,
setSuccess
]
=
useFeedbackState
(
'success'
);
const
[
busyKey
,
setBusyKey
]
=
useState
(
''
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
...
...
@@ -212,21 +213,21 @@ export default function NotificationListPage() {
const
runItemOperation
=
useCallback
(
async
(
key
,
operation
,
successMessage
)
=>
{
setBusyKey
(
key
);
setOperationError
(
''
);
setSuccess
(
''
);
try
{
await
operation
();
toast
.
success
(
successMessage
,
{
timeout
:
3000
}
);
setSuccess
(
successMessage
);
refresh
();
return
true
;
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
timeout
:
4000
,
});
setOperationError
(
operationError
?.
message
||
'操作失败,请稍后重试'
);
return
false
;
}
finally
{
setBusyKey
(
''
);
}
},
[
refresh
],
[
refresh
,
setOperationError
,
setSuccess
],
);
const
handleMarkRead
=
useCallback
(
...
...
@@ -260,6 +261,7 @@ export default function NotificationListPage() {
const
handleOpenTarget
=
useCallback
(
async
(
notification
,
target
)
=>
{
setBusyKey
(
notification
.
name
);
setOperationError
(
''
);
try
{
if
(
!
isNotificationRead
(
notification
,
userInfo
?.
_id
))
{
await
markNotificationRead
(
notification
.
name
);
...
...
@@ -271,14 +273,12 @@ export default function NotificationListPage() {
}
refresh
();
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
timeout
:
4000
,
});
setOperationError
(
operationError
?.
message
||
'无法打开通知关联流程'
);
}
finally
{
setBusyKey
(
''
);
}
},
[
refresh
,
userInfo
?.
_id
],
[
refresh
,
userInfo
?.
_id
,
setOperationError
],
);
return
(
...
...
@@ -356,14 +356,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-48"
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
通知加载失败
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
详细错误见操作提示,请重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Footer
>
<
Button
variant=
"secondary"
onPress=
{
refresh
}
>
重试
</
Button
>
</
EmptyState
.
Footer
>
</
EmptyState
>
)
:
null
}
{
loading
&&
notifications
.
length
===
0
?
(
...
...
src/pages/problem_rectification/problem-rectification.business-flow.md
View file @
a34ec141
...
...
@@ -144,6 +144,16 @@
---
### 操作提示与失败重试
-
功能入口:登录、站点配置、应用切换、问题清单、归档批次、驾驶舱/整改分析、详情编辑、证明材料查询与上传、整改/延期流程表格、旧外链流程页和通知中心;与风险业务入口共用应用根部的
`AppFeedback`
。
-
查询、保存、复制、删除、锁定/解锁、推送、上传和审批的接口成功/失败统一显示 HeroUI Toast,配置和应用列表降级使用 warning。官方 Portal 在视口顶部呈现,层级高于业务 Modal、确认框和移动侧栏;弹窗打开或页面滚动时可看到并关闭,不抢走表单焦点。
-
成功默认 4 秒、错误/警告默认 8 秒后消失,悬停或键盘聚焦暂停计时;当前提示右侧的关闭图标始终可见,使用 HeroUI 默认小号透明按钮,点击或键盘激活均通过 Toast 的
`close`
slot 关闭。后排堆叠提示不显示关闭按钮,避免误关闭。相同错误再次发生时重新弹出,同一来源的新提示替换旧提示,清空反馈或离开来源组件时清理对应提示。
-
关闭提示不修改请求结果、草稿、附件、选中记录、
`metadata`
、
`is_locked`
或审批状态,也不重新执行写入。必填/JSON 校验、锁定说明与删除确认说明保留在对应位置。
-
问题详情弹窗点击“保存”后,只有本次待建档材料和
`updateRecord`
全部成功,才退出编辑并弹出“详情已保存”成功提示,弹窗保持打开并展示已保存内容;任一接口失败仅显示错误并保留草稿。重新开始编辑、再次保存、取消编辑、关闭弹窗或切换记录时清理上一条成功提示;无字段修改、校验未通过或正在保存时不提交、不弹出成功提示。
-
问题清单、归档批次、驾驶舱/整改分析、详情、通知和流程页读取失败时保留失败空状态与“重新加载/重试”;详细原因由浮层展示,不误显示“暂无数据”。重试只读取当前页面资源;问题清单保留当前视图、筛选与页码,通知保留分类、已读状态、关键词与页码。
-
人员、关联记录和材料查询失败时弹出提示,选择器保留字段级加载错误;关联明细和材料保留读取重试入口。详情保存或附件上传失败保留当前编辑内容,可修正后再次提交;批量附件部分失败保留已成功的附件并提示失败项。
## 4. 核心业务对象
### 4.1 主对象
...
...
src/pages/risk_management/ImportPreflightDialog.jsx
View file @
a34ec141
import
{
Alert
,
Button
,
Modal
,
Pagination
,
Spinner
}
from
'@heroui/react'
;
import
{
DataGrid
}
from
'@heroui-pro/react'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
exportImportIssues
}
from
'@/services/riskImportPreflight'
;
import
{
useEffect
,
useState
}
from
'react'
;
...
...
@@ -63,12 +64,16 @@ export default function ImportPreflightDialog({
</
div
>
)
:
null
}
{
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Title
>
操作未完成
</
Alert
.
Title
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
<
EmptyState
size=
"sm"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
操作未完成
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
{
report
?
'详细错误见操作提示。请先核对已写入数据,再重新选择文件;本次结果不能重复确认导入。'
:
'详细错误见操作提示。请重新选择文件进行预检。'
}
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
</
EmptyState
>
)
:
null
}
{
report
?
(
<>
...
...
src/pages/risk_management/RiskManagementListPage.jsx
View file @
a34ec141
...
...
@@ -2,8 +2,9 @@ import SubjectRecordWorkbench from '@/components/SubjectRecordWorkbench';
import
{
ArrowDownTrayIcon
}
from
'@/components/AppIcons'
;
import
{
ACTION_LABELS
,
RISK_MANAGEMENT_SUBJECTS
}
from
'@/config/riskManagementConfig'
;
import
RiskManagerService
from
'@/services/RiskManagerService'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
ImportPreflightDialog
from
'./ImportPreflightDialog'
;
import
{
Button
,
Tooltip
,
toast
}
from
'@heroui/react'
;
import
{
Button
,
Tooltip
}
from
'@heroui/react'
;
import
{
useCallback
,
useMemo
,
useRef
,
useState
}
from
'react'
;
function
selectedActionRecords
(
context
=
{})
{
...
...
@@ -21,6 +22,9 @@ function RiskManagementListPage({ subjectName }) {
const
importSessionRef
=
useRef
(
null
);
const
[
importState
,
setImportState
]
=
useState
(
null
);
const
[
actionLoading
,
setActionLoading
]
=
useState
(
false
);
const
[,
setRequestError
]
=
useFeedbackState
(
'error'
);
const
[,
setSuccess
]
=
useFeedbackState
(
'success'
);
const
[,
setWarning
]
=
useFeedbackState
(
'warning'
);
const
runAction
=
useCallback
(
async
(
actionKey
,
context
=
{},
file
)
=>
{
const
methodName
=
ACTION_LABELS
[
actionKey
];
...
...
@@ -28,12 +32,15 @@ function RiskManagementListPage({ subjectName }) {
const
records
=
selectedActionRecords
(
context
);
if
([
'copy'
,
'delete'
].
includes
(
actionKey
)
&&
records
.
length
===
0
)
{
toast
.
w
arning
(
'请选择要处理的记录'
);
return
;
setW
arning
(
'请选择要处理的记录'
);
return
false
;
}
setActionLoading
(
true
);
context
.
setError
?.(
''
);
setRequestError
(
''
);
setSuccess
(
''
);
setWarning
(
''
);
try
{
const
payload
=
{
records
,
...
...
@@ -44,16 +51,19 @@ function RiskManagementListPage({ subjectName }) {
payload
.
record
=
records
[
0
];
}
await
service
[
methodName
](
payload
);
toast
.
success
(
'操作已完成'
);
if
(
!
[
'copy'
,
'delete'
].
includes
(
actionKey
))
{
setSuccess
(
'操作已完成'
);
await
context
.
reload
?.({
keepFeedback
:
true
});
}
return
true
;
}
catch
(
actionError
)
{
const
message
=
actionError
?.
message
||
'操作失败,请稍后重试'
;
context
.
setError
?.
(
message
);
toast
.
danger
(
message
)
;
setRequestError
(
message
);
return
false
;
}
finally
{
setActionLoading
(
false
);
}
},
[
service
,
subjectName
]);
},
[
service
,
subjectName
,
setRequestError
,
setSuccess
,
setWarning
]);
const
runConfiguredAction
=
useCallback
((
actionKey
,
context
)
=>
{
if
(
actionKey
.
startsWith
(
'import-'
))
{
...
...
@@ -68,26 +78,31 @@ function RiskManagementListPage({ subjectName }) {
importSessionRef
.
current
=
session
;
setActionLoading
(
true
);
session
.
context
?.
setError
?.(
''
);
setRequestError
(
''
);
setSuccess
(
''
);
setImportState
({
isChecking
:
true
,
report
:
null
,
error
:
''
});
try
{
const
report
=
await
service
.
preflightImport
({
file
,
actionKey
:
session
.
actionKey
});
setImportState
({
isChecking
:
false
,
report
,
error
:
''
});
}
catch
(
error
)
{
setImportState
({
isChecking
:
false
,
report
:
null
,
error
:
error
?.
message
||
'预检失败,请稍后重试'
});
const
message
=
error
?.
message
||
'预检失败,请稍后重试'
;
setImportState
({
isChecking
:
false
,
report
:
null
,
error
:
message
});
setRequestError
(
message
);
}
finally
{
setActionLoading
(
false
);
}
},
[
service
]);
},
[
service
,
setRequestError
,
setSuccess
]);
const
confirmImport
=
useCallback
(
async
()
=>
{
if
(
!
importState
?.
report
?.
canImport
||
actionLoading
||
importState
.
error
)
return
;
const
context
=
importSessionRef
.
current
?.
context
||
{};
setActionLoading
(
true
);
setRequestError
(
''
);
setImportState
((
state
)
=>
({
...
state
,
isImporting
:
true
}));
try
{
const
result
=
await
service
.
executePreparedImport
(
importState
.
report
);
setImportState
(
null
);
toast
.
s
uccess
(
`成功导入
${
result
.
count
}
行数据`
);
setS
uccess
(
`成功导入
${
result
.
count
}
行数据`
);
try
{
await
context
.
reload
?.({
keepFeedback
:
true
});
}
catch
(
reloadError
)
{
...
...
@@ -96,7 +111,7 @@ function RiskManagementListPage({ subjectName }) {
}
catch
(
error
)
{
const
message
=
error
?.
message
||
'导入失败,请核对系统数据后重试'
;
setImportState
((
state
)
=>
({
...
state
,
isImporting
:
false
,
error
:
message
}));
context
.
setError
?.
(
message
);
setRequestError
(
message
);
try
{
await
context
.
reload
?.({
keepFeedback
:
true
});
}
catch
{
...
...
@@ -105,7 +120,7 @@ function RiskManagementListPage({ subjectName }) {
}
finally
{
setActionLoading
(
false
);
}
},
[
actionLoading
,
importState
,
service
]);
},
[
actionLoading
,
importState
,
service
,
setRequestError
,
setSuccess
]);
const
mapAction
=
useCallback
((
action
)
=>
({
...
action
,
...
...
src/pages/risk_management/risk-management.business-flow.md
View file @
a34ec141
...
...
@@ -25,7 +25,7 @@ HeroUI Pro `DataGrid` 和 `RecordDetailDrawer`。同一套多维表格组件也
1.
页面加载 Subject、Grid View 和当前页记录;查询使用
`POST /record/query`
,分页总数直接
使用查询响应中的
`total_records`
(兼容
`total_count`
、
`total`
等响应字段),不再为当前视图
额外调用
`POST /record/count`
。请求期间显示 loading,失败时
在列表上方显示可读错误
。
额外调用
`POST /record/count`
。请求期间显示 loading,失败时
通过操作提示展示原因,列表保留加载失败空状态和“重新加载”
。
2.
Grid View 的
`settings.columns`
决定表格列;应用配置中标记为
`filterView`
的视图继续
隐藏,不出现在列表 Tab 中;视图授权和
`settings.restrictions`
、
`settings.filters.conditions`
会在查询时转换为 Record API 的
`filter`
。可编辑筛选字段映射为 HeroUI 输入、选择、引用
...
...
@@ -81,7 +81,7 @@ HeroUI Pro `DataGrid` 和 `RecordDetailDrawer`。同一套多维表格组件也
-
基金初始化逐行检查“基金名称”,所有目标必须存在且唯一;允许同一文件包含多只基金。预检全通过后按目标基金分组导入
`indicator_library`
,将新记录
`_id`
合并到各基金的
`contract_terms`
,保留已有指标并避免同批多行覆盖。
-
模板关联指标逐行检查“模板标题 + 模板类别”,整份文件必须属于同一个模板及类别,不能仅使用第一行定位后忽略其余行。确认后导入指标,并沿用原规则更新目标模板的
`associated_indicators`
;导出反向生成含模板信息的 Excel。
-
托管清单逐行检查“公司名称 + 基金类型”,整批数据必须属于同一家公司和基金类型,且能定位唯一现有托管人记录。基金类型优先从“类别”读取,兼容旧文件仅使用“类型”的格式;当“类型”用于基金类型时,不误写到关联关系的管理类型字段。新版文件可同时使用“类别(公募/专户)”和“类型(管理人/托管人)”两列,列名分别保持“类别”和“类型”。按
`relation_related_party`
定义校验后创建关联关系,将新
`_id[]`
合并到公司的
`related_parties`
。
-
确认后先重新读取目标记录,确保目标仍可用;追加关联以确认时的最新引用列表为基础,避免覆盖预检等待期间其他用户新增的关联。正式写入检查接口返回的 ID 数量与待写行数是否一致,再更新关联。记录写入或关联更新失败时停止后续目标组,
在弹窗和工作台保留具体 Excel 行号、失败阶段、服务端原因和已完整完成的行数,并刷新列表供核对;
不把部分完成显示为成功,不提供同一结果直接重复确认,以免重复导入。服务端写入可能已部分执行,需核对系统数据后修正或清理再重试;预检不代表多接口事务回滚。
-
确认后先重新读取目标记录,确保目标仍可用;追加关联以确认时的最新引用列表为基础,避免覆盖预检等待期间其他用户新增的关联。正式写入检查接口返回的 ID 数量与待写行数是否一致,再更新关联。记录写入或关联更新失败时停止后续目标组,
通过操作提示展示具体 Excel 行号、失败阶段、服务端原因和已完整完成的行数;弹窗保留“操作未完成”和不能重复确认的状态,并刷新列表供核对。
不把部分完成显示为成功,不提供同一结果直接重复确认,以免重复导入。服务端写入可能已部分执行,需核对系统数据后修正或清理再重试;预检不代表多接口事务回滚。
## 5. 行操作与异常处理
...
...
@@ -91,6 +91,7 @@ HeroUI Pro `DataGrid` 和 `RecordDetailDrawer`。同一套多维表格组件也
-
所有异步动作都会禁用对应按钮并显示 loading;失败不会吞掉服务端错误,会通过 Toast
给出原因和恢复方向。删除动作先经过 HeroUI 确认弹窗;历史版本比较会过滤 removed
引用并要求至少两条可用版本。
-
复制和删除的业务回调返回失败时,工作台保留原操作状态,不再追加成功提示或关闭删除确认框;每次操作仅由对应反馈来源展示结果。
-
引用记录缺失、历史版本不足、导入文件为空或目标公司/模版不存在时,在写入前提示用户
检查数据后重试;写入阶段的接口失败会保留服务端错误,避免静默掩盖部分成功结果。
...
...
@@ -105,4 +106,15 @@ HeroUI Pro `DataGrid` 和 `RecordDetailDrawer`。同一套多维表格组件也
-
引用、选择、子对象与 form 字段适用上述规则。子对象优先读取
`titleField`
(支持 metadata 子字段),然后使用已有
`title/name/_id`
,无标题时显示“第 N 条”;
`{form,data}`
按
`data`
条目计数。空字符串、null 和 undefined 不计数,数字
`0`
保留。
-
例如基金产品的
`metadata.contract_terms`
、投监表模版的
`metadata.associated_indicators`
可配置
`displayMode: "count"`
;基金产品的
`metadata.investment_scope`
可配置
`displayMode: "labels"`
。字段仍以 Subject 定义为准。
-
未配置或配置非法时保留原展示;普通文本、数字、日期、布尔、人员和附件不受影响。首列仍可点击打开详情,行操作入口保持可用。计数只使用当前字段已有数据,不额外查询关联总数,不修改 metadata、筛选、排序、分页、导出或审批规则。
-
加载配置期间沿用列表 loading;应用配置失败时显示警告并继续使用视图配置和列表数据。共享工作台按 Subject 缓存配置,刷新页面重新读取;记录查询失败仍显示列表错误。
-
加载配置期间沿用列表 loading;应用配置失败时弹出警告并继续使用视图配置和列表数据。共享工作台按 Subject 缓存配置,刷新页面重新读取;记录查询失败保留失败空状态与读取重试入口。
## 7. 操作提示与失败重试
-
功能入口:登录、站点配置、应用切换、六个风险业务列表、详情、新建/编辑、子对象与动态表单、附件、导入预检、历史投监表版本比较和通知中心,以及保留的问题整改兼容路由。
-
查询、保存、复制、删除、导入、导出、创建历史版本和通知操作的成功/失败统一显示 HeroUI Toast。根部统一挂载,官方 Portal 在视口顶部呈现,层级高于 Modal、确认框和移动侧栏;弹窗打开或页面滚动时也可看到并关闭,不抢走表单焦点。
-
成功提示默认 4 秒、错误与警告默认 8 秒自动消失,悬停或键盘聚焦暂停计时;当前提示右侧的关闭图标始终可见,使用 HeroUI 默认小号透明按钮,点击或键盘激活均通过 Toast 的
`close`
slot 关闭。后排堆叠提示不显示关闭按钮,避免误关闭。相同错误再次发生时重新弹出,同一来源的新提示替换旧提示;清空反馈或离开来源组件时清理该来源提示。
-
提示使用官方 ToastQueue 即时更新,避免提示进出动画期间的点击被浏览器快照拦截并误关下方弹窗;鼠标和键盘关闭都只移除提示,弹窗保持打开。
-
提示关闭仅影响展示,不把失败改为成功,不清空草稿、选中记录、附件或导入预检结果,也不发起写入或修改
`metadata`
。必填/JSON 校验、锁定说明、删除确认与导入预检错误明细继续保留在对应位置。
-
六个风险业务列表共用的详情弹窗点击“保存”后,只有
`updateRecord`
成功才退出编辑并弹出“详情已保存”成功提示,弹窗保持打开并展示已保存内容;接口失败仅显示错误并保留草稿。重新开始编辑、再次保存、取消编辑、关闭弹窗或切换记录时清理上一条成功提示;无字段修改、校验未通过或正在保存时不提交、不弹出成功提示。兼容问题详情的待建档材料必须全部创建成功后再更新记录。
-
列表、详情、子对象结构、动态表单模板、历史版本比较和通知加载失败时保留失败空状态及“重新加载/重试”;详细原因由浮层展示,避免误显示“暂无数据”。重试只读取资源,列表沿用当前 Subject、Grid View、关键词/字段筛选与分页,历史比较保留 URL 中的记录和字段,通知保留分类、已读状态、关键词与分页。
-
导入预检的读取接口失败时保留弹窗并允许重新选择文件;正式写入失败时保留失败状态并禁用本次结果的“确认导入”。关闭提示不能重新启用该按钮,也不能清除已完成行数所对应的服务端数据。列表刷新不清除本次导入错误提示;附件上传或详情保存失败保留草稿,可修正后再次提交。
src/services/feedbackQueue.js
0 → 100644
View file @
a34ec141
import
{
ToastQueue
}
from
'@heroui/react'
;
// View-transition snapshots can intercept a close press and dismiss the Modal below.
// Apply queue updates immediately so the official live Toast remains interactive.
export
const
feedbackQueue
=
new
ToastQueue
({
maxVisibleToasts
:
3
,
wrapUpdate
:
(
update
)
=>
update
(),
});
src/tailwind.css
View file @
a34ec141
...
...
@@ -4,6 +4,7 @@
@import
"./design-system/theme.css"
;
@import
"./design-system/tokens.css"
;
@import
"./design-system/shell.css"
;
@import
"./design-system/feedback.css"
;
@source
"./*.{js,jsx,ts,tsx}"
;
@source
"./components/**/*.{js,jsx,ts,tsx}"
;
...
...
src/wrapper/Site.jsx
View file @
a34ec141
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
getSystemInfo
}
from
'@/services/DataService'
;
import
dayjs
from
'dayjs'
;
import
customParseFormat
from
'dayjs/plugin/customParseFormat'
;
...
...
@@ -20,7 +21,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
(
'error
'
);
useEffect
(()
=>
{
let
cancelled
=
false
;
...
...
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