Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
A
audit_material_manage
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_material_manage
Commits
5bc7b82c
Commit
5bc7b82c
authored
Oct 08, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: unify HeroUI operation feedback and retry states
parent
07785e81
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
208 additions
and
89 deletions
+208
-89
xinyuan-design-system.md
docs/xinyuan-design-system.md
+13
-1
manifest.json
public/manifest.json
+1
-1
app.js
src/app.js
+3
-8
AppFeedback.jsx
src/components/AppFeedback.jsx
+42
-0
feedback.css
src/design-system/feedback.css
+11
-0
useFeedbackState.js
src/hooks/useFeedbackState.js
+38
-0
Login.jsx
src/pages/Login.jsx
+3
-10
ApplicationSwitcherDialog.jsx
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
+5
-13
audit-material-center.business-flow.md
...project_manager_xy/audit-material-center.business-flow.md
+13
-2
NotificationListPage.jsx
...project_manager_xy/notifications/NotificationListPage.jsx
+22
-21
index.jsx
src/pages/project_manager_xy/resource-booking/index.jsx
+32
-20
index.jsx
src/pages/project_manager_xy/resource-stat/index.jsx
+21
-11
tailwind.css
src/tailwind.css
+1
-0
Site.jsx
src/wrapper/Site.jsx
+3
-2
No files found.
docs/xinyuan-design-system.md
View file @
5bc7b82c
...
...
@@ -35,6 +35,7 @@ Global CSS order:
@import
"./design-system/theme.css"
;
@import
"./design-system/tokens.css"
;
@import
"./design-system/shell.css"
;
@import
"./design-system/feedback.css"
;
```
The official licensed package is installed independently in this repository.
...
...
@@ -105,7 +106,18 @@ typing alone never reloads the list.
ActionBar and compound Pagination.
-
All ECharts Pie/Donut series use the shared
`XinyuanEChart`
adapter; pages do
not provide pie
`itemStyle.color`
values or replace the dedicated palette.
-
Routine success feedback is a fixed, auto-dismissing Toast.
-
API success, error and fallback warnings use the root
`AppFeedback`
HeroUI
`Toast.Provider`
with official compound components. Success lasts 4 seconds;
errors and warnings last 8 seconds. Closing feedback does not clear request
failure state or form drafts; failed reads retain an EmptyState and retry.
-
`feedback.css`
only adjusts Toast stacking, safe-area positioning and hiding
close buttons on background toasts, following problem rectification commit
`bfc5862`
. The region uses z-index 300 above business Modals and mobile Sidebar.
The close icon is a HeroUI
`Button`
(
`ghost`
,
`sm`
,
`isIconOnly`
) using the
official
`slot="close"`
and
`AppIcons.Close`
; it stays visible on the frontmost
toast and participates in layout to the right of the content. No absolute
positioning, enlarged bordered close button or extra right padding is added.
Colors, borders, radius, shadow, hover and focus use HeroUI theme defaults.
-
Modal view mode uses one secondary Surface for the complete read-only field
group; edit mode removes that Surface and renders secondary fields directly.
-
Detail view modal footers expose only the contextual edit action; destructive
...
...
public/manifest.json
View file @
5bc7b82c
{
"name"
:
"audit_material_manager"
,
"version"
:
"1.
1
.4"
,
"version"
:
"1.
0
.4"
,
"build"
:
"abc"
,
"description"
:
"merge master"
,
"date"
:
"2025-07-14 19:27:20"
,
...
...
src/app.js
View file @
5bc7b82c
import
"@/global.less"
;
import
"@/tailwind.css"
;
import
{
Toast
}
from
"@heroui/react
"
;
import
{
createElement
,
Fragment
}
from
"react"
;
import
AppFeedback
from
"@/components/AppFeedback
"
;
import
{
createElement
}
from
"react"
;
import
{
defineApp
}
from
"umi"
;
export
async
function
getInitialState
()
{
...
...
@@ -12,11 +12,6 @@ export async function getInitialState() {
export
default
defineApp
({
rootContainer
(
container
)
{
return
createElement
(
Fragment
,
null
,
container
,
createElement
(
Toast
.
Provider
,
{
placement
:
"top"
}),
);
return
createElement
(
AppFeedback
,
null
,
container
);
},
});
src/components/AppFeedback.jsx
0 → 100644
View file @
5bc7b82c
import
{
Button
,
Toast
}
from
'@heroui/react'
;
import
{
Close
}
from
'@/components/AppIcons'
;
export
default
function
AppFeedback
({
children
})
{
return
(
<>
{
children
}
<
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"
>
{
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/design-system/feedback.css
0 → 100644
View file @
5bc7b82c
/* 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 @
5bc7b82c
import
{
toast
}
from
'@heroui/react'
;
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
)
toast
.
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
=
toast
(
title
,
{
description
:
nextMessage
,
variant
,
timeout
});
},
[
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 @
5bc7b82c
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
authService
from
'@/services/authService'
;
import
{
APPLICATION_BASE_PATH
,
...
...
@@ -5,7 +6,7 @@ import {
}
from
'@/config/application'
;
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'
;
...
...
@@ -54,7 +55,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'
));
...
...
@@ -209,14 +210,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/project_manager_xy/ApplicationSwitcherDialog.jsx
View file @
5bc7b82c
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
ExclamationCircleIcon
,
MagnifyingGlassIcon
,
XMarkIcon
}
from
'@/components/AppIcons'
;
import
{
MagnifyingGlassIcon
,
XMarkIcon
}
from
'@/components/AppIcons'
;
import
xinyuanLogo
from
'@/assets/xy-logo-red.png'
;
import
{
APPLICATION_BASE_PATH
,
...
...
@@ -9,7 +10,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
,
Description
,
...
...
@@ -291,7 +291,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
||
''
;
...
...
@@ -332,6 +332,7 @@ export default function ApplicationSwitcherDialog({
useEffect
(()
=>
{
if
(
!
open
)
{
setApplicationSearch
(
''
);
setErrorMessage
(
''
);
return
;
}
...
...
@@ -372,7 +373,7 @@ export default function ApplicationSwitcherDialog({
return
()
=>
{
cancelled
=
true
;
};
},
[
fallbackApplications
,
open
]);
},
[
fallbackApplications
,
open
,
setErrorMessage
]);
return
(
<
Modal
.
Root
>
...
...
@@ -418,15 +419,6 @@ export default function ApplicationSwitcherDialog({
<
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
><
ExclamationCircleIcon
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/project_manager_xy/audit-material-center.business-flow.md
View file @
5bc7b82c
...
...
@@ -142,11 +142,11 @@
-
提供部门、提供时间
-
涉密标记、审计部材料标记
13.
创建、修改、批量操作、下载和删除
成功后,以视口顶部居中的 Toast 提示结果,并在 4 秒后自动消失;失败信息保留在当前业务区域,便于用户定位和重试
13.
创建、修改、批量操作、下载和删除
的成功、失败反馈统一使用视口顶部居中的 Toast;成功默认 4 秒、错误默认 8 秒后消失,关闭按钮始终可见,弹框打开时也可查看和关闭
14.
列表或统计请求失败时保留真实错误态并展示重试提示;真实查询结果为空时展示空状态,不注入演示材料、测试数字或伪造项目记录
15.
用户点击“编辑”后进入编辑态,可修改材料标题、所属周期/年份、来源应用、来源系统、分类、标签、提供信息与合规属性;来源应用使用文本输入,来源系统和材料分类从配置下拉选择
16.
编辑态下点击“取消编辑”放弃未保存修改并恢复当前材料原值
17.
编辑态下点击“保存修改”后调用材料更新接口
,保存成功后生效并回到查看态
17.
编辑态下点击“保存修改”后调用材料更新接口
;保存成功并完成列表刷新后回到查看态,弹框继续展示已保存内容,并弹出“详情已保存”成功提示
18.
详情弹框提供单个删除入口,删除前需二次确认,确认后对当前材料执行软删除并关闭弹框
19.
详情弹框中的编辑区域按“基础信息 / 提供信息 / 合规属性”三段式维护,减少字段混排
...
...
@@ -156,6 +156,17 @@
2.
默认优先软删除,不建议直接物理删除文件
3.
如需保留历史,可通过分类、标签或状态进行归档管理
### 4.5 操作提示与失败重试
-
功能入口:登录与验证码、站点配置、应用切换、工作台、材料归档(上传、编辑、复制链接、下载、删除及批量操作)和通知中心。
-
根部统一挂载 HeroUI
`Toast.Provider`
,接口成功、失败及配置降级警告以浮层显示;Portal 位于视口顶部,层级高于业务 Modal、确认框和移动侧栏,页面滚动时仍可见,不抢走表单焦点。接口反馈不再渲染为页面常驻横条;字段配置错误、必填规则及删除确认等说明保留在对应控件或确认框。
-
成功默认 4 秒、错误和警告默认 8 秒后消失,悬停或键盘聚焦暂停计时。当前提示右侧的关闭图标始终可见,使用 HeroUI 小号透明按钮,通过 Toast 的
`close`
slot 支持点击和键盘关闭;后排堆叠提示隐藏关闭按钮,避免误关闭。相同错误再次发生时重新提示,同一来源的新提示替换旧提示;清空反馈或离开对应页面时清理该来源提示,关闭应用切换弹框时清理其降级提示。
-
关闭或自动消失仅影响提示展示,不把失败改为成功,不修改
`am_material.metadata`
,不清空上传文件、编辑草稿或选中记录,不触发写入操作。保存、上传和批量操作失败时,用户可修正内容后再次提交。
-
材料详情弹框仅在
`updateMaterialRecord`
成功后显示“详情已保存”;提示在列表刷新结束并回到查看态时开始计时,避免刷新等待耗尽提示时长。更新失败仅显示错误并保留编辑草稿;材料标题为空或正在保存时不提交、不显示成功。重新开始编辑、取消编辑、再次保存、切换材料或关闭详情弹框时清理上一条成功提示。
-
材料列表、工作台和通知加载失败时显示失败空状态及“重新加载/重试”,详细原因通过 Toast 展示,避免将加载失败误显示为“暂无数据”或零值统计。关闭 Toast 后失败空状态仍保留,只有读取成功后才恢复数据展示。
-
列表重试保留已提交的字段筛选、材料分类和页码;通知重试保留消息/待办、全部/未读、关键词和页码。重试仅重新读取资源,不重新执行上传、保存、删除或待办操作。
-
来源系统和材料分类仍从 Subject
`settings.options`
读取;配置读取异常以 warning 提示,并在选择器中保留字段错误与禁用状态。应用列表失败继续回退到账号应用信息,站点配置失败继续使用现有降级配置。
---
## 5. 联动规则
...
...
src/pages/project_manager_xy/notifications/NotificationListPage.jsx
View file @
5bc7b82c
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
);
...
...
@@ -192,7 +193,7 @@ export default function NotificationListPage() {
return
()
=>
{
cancelled
=
true
;
};
},
[
category
,
keyword
,
page
,
readStatus
,
reloadKey
]);
},
[
category
,
keyword
,
page
,
readStatus
,
reloadKey
,
setError
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
...
...
@@ -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
,
setOperationError
,
userInfo
?.
_id
],
);
return
(
...
...
@@ -357,14 +357,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
.
Content
>
<
Button
variant=
"secondary"
onPress=
{
refresh
}
>
重试
</
Button
>
</
EmptyState
.
Content
>
</
EmptyState
>
)
:
null
}
{
loading
&&
notifications
.
length
===
0
?
(
...
...
src/pages/project_manager_xy/resource-booking/index.jsx
View file @
5bc7b82c
import
useFeedbackState
from
"@/hooks/useFeedbackState"
;
import
{
DataTable
,
DataTableChip
,
...
...
@@ -45,9 +46,9 @@ import {
Tabs
,
TextArea
,
Tooltip
,
toast
,
}
from
"@heroui/react"
;
import
{
DropZone
}
from
"@heroui-pro/react/drop-zone"
;
import
{
EmptyState
}
from
"@heroui-pro/react/empty-state"
;
import
{
startTransition
,
useContext
,
...
...
@@ -385,8 +386,11 @@ export default function ResourceBookingPage() {
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
[
exporting
,
setExporting
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
notice
,
setNotice
]
=
useState
(
""
);
const
[,
setError
]
=
useFeedbackState
(
"error"
);
const
[,
setNotice
]
=
useFeedbackState
(
"success"
);
const
[,
setOptionsWarning
]
=
useFeedbackState
(
"warning"
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[
sourceSystemOptions
,
setSourceSystemOptions
]
=
useState
([]);
const
[
sourceSystemOptionsLoading
,
setSourceSystemOptionsLoading
]
=
useState
(
true
);
...
...
@@ -416,12 +420,6 @@ export default function ResourceBookingPage() {
[
selectedFile
],
);
useEffect
(()
=>
{
if
(
notice
)
{
toast
.
success
(
notice
,
{
timeout
:
4000
});
}
},
[
notice
]);
const
activeMaterial
=
useMemo
(()
=>
{
return
materials
.
find
((
item
)
=>
item
.
id
===
activeMaterialId
)
||
null
;
},
[
activeMaterialId
,
materials
]);
...
...
@@ -449,6 +447,9 @@ export default function ResourceBookingPage() {
setArchiveCategoryOptions
(
result
.
archiveCategoryOptions
||
[]);
setArchiveCategoryOptionsError
(
result
.
archiveCategoryError
||
""
);
setArchiveCategoryOptionsLoading
(
false
);
setOptionsWarning
(
[...
new
Set
([
result
.
sourceSystemError
,
result
.
archiveCategoryError
].
filter
(
Boolean
))].
join
(
"
\n
"
),
);
}
run
();
...
...
@@ -456,13 +457,15 @@ export default function ResourceBookingPage() {
return
()
=>
{
cancelled
=
true
;
};
},
[]);
},
[
setOptionsWarning
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
async
function
runFirstPage
()
{
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
""
);
const
result
=
await
loadMaterialPage
({
...
submittedMaterialSearchForm
,
page
,
...
...
@@ -480,6 +483,7 @@ export default function ResourceBookingPage() {
}),
);
setTotal
(
result
.
total
||
0
);
setLoadFailed
(
Boolean
(
result
.
error
));
setError
(
result
.
error
||
""
);
setLoading
(
false
);
...
...
@@ -496,7 +500,7 @@ export default function ResourceBookingPage() {
return
()
=>
{
cancelled
=
true
;
};
},
[
activeMaterialId
,
submittedMaterialSearchForm
,
page
,
pageSize
]);
},
[
activeMaterialId
,
submittedMaterialSearchForm
,
page
,
pageSize
,
reloadKey
,
setError
]);
const
categoryTabs
=
useMemo
(
()
=>
{
...
...
@@ -749,6 +753,7 @@ export default function ResourceBookingPage() {
}),
);
setTotal
(
result
.
total
||
0
);
setLoadFailed
(
Boolean
(
result
.
error
));
setError
(
result
.
error
||
""
);
return
result
;
...
...
@@ -823,6 +828,7 @@ export default function ResourceBookingPage() {
setIsDetailOpen
(
false
);
setIsDetailEditing
(
false
);
setDetailForm
(
buildDetailForm
(
activeMaterial
));
setNotice
(
""
);
}
function
handleStartDetailEdit
()
{
...
...
@@ -840,6 +846,7 @@ export default function ResourceBookingPage() {
setDetailForm
(
buildDetailForm
(
activeMaterial
));
setIsDetailEditing
(
false
);
setError
(
""
);
setNotice
(
""
);
}
async
function
handleUploadSubmit
()
{
...
...
@@ -902,7 +909,7 @@ export default function ResourceBookingPage() {
}
async
function
handleSaveDetail
()
{
if
(
!
activeMaterial
)
{
if
(
!
activeMaterial
||
saving
)
{
return
;
}
...
...
@@ -917,10 +924,10 @@ export default function ResourceBookingPage() {
try
{
const
updated
=
await
updateMaterialRecord
(
activeMaterial
,
detailForm
);
setNotice
(
"材料信息已更新。"
);
await
reloadMaterials
();
setActiveMaterialId
(
updated
.
id
||
activeMaterial
.
id
);
setIsDetailEditing
(
false
);
setNotice
(
"详情已保存。"
);
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
"材料信息保存失败,请稍后重试。"
);
}
finally
{
...
...
@@ -1226,13 +1233,6 @@ export default function ResourceBookingPage() {
</
Tabs
>
</
div
>
{
error
?
(
<
Alert
className=
"mt-4"
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
</
div
>
<
Surface
className=
"rounded-3xl p-6"
variant=
"default"
>
...
...
@@ -1448,6 +1448,18 @@ export default function ResourceBookingPage() {
<
Spinner
size=
"sm"
/>
<
span
className=
"text-sm text-muted"
>
正在加载材料归档工作台...
</
span
>
</
div
>
)
:
loadFailed
?
(
<
EmptyState
className=
"min-h-48"
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
材料列表加载失败
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
详细错误见操作提示,请重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Content
>
<
Button
variant=
"secondary"
onPress=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
>
重新加载
</
Button
>
</
EmptyState
.
Content
>
</
EmptyState
>
)
:
(
<
div
className=
"space-y-4"
>
<
div
className=
"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
...
...
src/pages/project_manager_xy/resource-stat/index.jsx
View file @
5bc7b82c
import
useFeedbackState
from
"@/hooks/useFeedbackState"
;
import
{
UserContext
}
from
"@/wrapper/Auth"
;
import
{
DataTable
,
...
...
@@ -22,7 +23,6 @@ import {
}
from
"@/design-system/chartPalette"
;
import
XinyuanEChart
from
"@/components/XinyuanEChart"
;
import
{
Alert
,
Button
,
Card
,
Chip
,
...
...
@@ -440,13 +440,15 @@ export default function ResourceStatPage() {
const
[
materials
,
setMaterials
]
=
useState
([]);
const
[
dashboard
,
setDashboard
]
=
useState
(()
=>
buildMaterialDashboard
([]));
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
error
,
setError
]
=
useFeedbackState
(
"error"
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
useEffect
(()
=>
{
let
cancelled
=
false
;
(
async
()
=>
{
setLoading
(
true
);
setError
(
""
);
const
result
=
await
loadMaterialRecords
({});
if
(
cancelled
)
{
...
...
@@ -462,7 +464,7 @@ export default function ResourceStatPage() {
return
()
=>
{
cancelled
=
true
;
};
},
[]);
},
[
reloadKey
,
setError
]);
const
welcomeLabel
=
useMemo
(()
=>
{
return
(
...
...
@@ -495,6 +497,22 @@ export default function ResourceStatPage() {
dashboard
.
totalCount
,
],
);
if
(
!
loading
&&
error
)
{
return
(
<
EmptyState
className=
"min-h-64"
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
工作台加载失败
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
详细错误见操作提示,请重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Content
>
<
Button
variant=
"secondary"
onPress=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
>
重新加载
</
Button
>
</
EmptyState
.
Content
>
</
EmptyState
>
);
}
return
(
<
div
className=
"rc-page-stack"
>
<
Panel
className=
"p-5 sm:p-6"
>
...
...
@@ -523,14 +541,6 @@ export default function ResourceStatPage() {
</
div
>
</
div
>
{
error
?
(
<
Alert
className=
"mt-6"
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
{
loading
?
(
<
div
className=
"mt-6 flex min-h-20 items-center justify-center gap-2"
role=
"status"
>
<
Spinner
size=
"sm"
/>
...
...
src/tailwind.css
View file @
5bc7b82c
...
...
@@ -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/wrapper/Site.jsx
View file @
5bc7b82c
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