Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
I
incorrupt_party_build
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
incorrupt_party_build
Commits
b2ccff62
Commit
b2ccff62
authored
Oct 08, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: unify operation feedback and refine dashboard distribution
parent
31252990
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
283 additions
and
86 deletions
+283
-86
xinyuan-design-system.md
docs/xinyuan-design-system.md
+10
-1
manifest.json
public/manifest.json
+1
-1
app.js
src/app.js
+7
-1
AppFeedback.jsx
src/components/AppFeedback.jsx
+42
-0
AppShell.jsx
src/components/AppShell.jsx
+0
-2
feedback.css
src/design-system/feedback.css
+11
-0
useFeedbackState.js
src/hooks/useFeedbackState.js
+38
-0
Login.jsx
src/pages/Login.jsx
+2
-10
ApplicationSwitcherDialog.jsx
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
+5
-11
integrity-party.business-flow.md
...pages/project_manager_xy/integrity-party.business-flow.md
+14
-2
IntegrityPartyMaterialField.jsx
...anager_xy/integrity-party/IntegrityPartyMaterialField.jsx
+27
-30
index.jsx
src/pages/project_manager_xy/integrity-party/index.jsx
+0
-0
NotificationListPage.jsx
...project_manager_xy/notifications/NotificationListPage.jsx
+16
-7
index.jsx
src/pages/project_manager_xy/resource-stat/index.jsx
+94
-17
resource-stat.business-flow.md
...t_manager_xy/resource-stat/resource-stat.business-flow.md
+12
-2
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 @
b2ccff62
...
...
@@ -50,9 +50,18 @@
-
预览态普通标量字段使用无边框静态值:标签为
`text-muted`
,值为
`text-foreground`
,空值显示“暂无内容”;不得用灰色 disabled Input/Autocomplete 冒充预览。编辑态才显示 HeroUI
`secondary`
控件;权限禁用、锁定或只读字段在同一网格中继续显示静态只读值。
-
预览与编辑共用两列字段网格和字段顺序;长文本、附件等内容跨两列。编辑态使用 Footer 的 HeroUI
`secondary`
取消 +
`primary`
保存,保存只提交一次,取消丢弃草稿,不改变原有校验、权限和请求时机。
-
预览态 Footer 使用 HeroUI
`primary`
编辑按钮作为主操作;点击后在同一个 Modal 和编辑会话内切换为编辑态,保留当前记录标题、字段草稿和材料草稿,不关闭后重新打开;进入编辑态后显示
`secondary`
取消 +
`primary`
保存,取消关闭并丢弃未保存草稿。
-
例行成功反馈使用顶部居中的自动消失 Toast;需要处理的错误保留上下文 Alert
。
-
接口成功、失败及降级警告统一使用应用根部
`AppFeedback`
的 HeroUI Toast,不在页面或弹窗正文渲染接口反馈横条。表单校验摘要、字段错误及操作说明保留在对应上下文
。
-
应用切换严格继承
`model_analysis`
:从
`/info -> site.group`
动态生成分组,保持配置顺序并显示真实数量;未归组应用进入“其他应用”,无配置时使用“全部应用”。Modal 固定 46rem/42rem、28px inset、手机一列、
`sm`
起两列,间距 12px;加载骨架使用相同网格,桌面不得切成三列,长列表仅在 Modal Body 内滚动。
## 操作反馈浮层
-
参考问题整改提交
`513706c`
、
`bfc5862`
:根部唯一的
`Toast.Provider`
覆盖登录页、站点配置、AppShell 及业务弹窗;通过 Portal 在视口顶部居中呈现,最多展示 3 条。
-
成功默认 4 秒消失,错误及警告默认 8 秒消失;悬停或键盘聚焦时暂停计时。关闭按钮使用 HeroUI
`Button`
(
`ghost`
、
`sm`
、
`isIconOnly`
)和
`AppIcons.Close`
,位于正文右侧并参与布局,通过官方
`slot="close"`
支持点击和键盘关闭,提供“关闭提示”可访问名称;前排按钮无需悬停即可操作,后排堆叠提示隐藏关闭按钮以避免误关闭。
-
`src/design-system/feedback.css`
仅调整浮层层级(300,高于业务 Modal 和移动侧栏)、安全区位置和后排关闭按钮可见性;不再放大默认 Toast 关闭按钮或设置绝对定位、正文右侧占位。颜色、边框、圆角、阴影和 Hover/焦点样式继续使用 HeroUI 主题。
-
`useFeedbackState`
让同一来源的新提示替换旧提示,相同错误再次发生时重新显示;清空反馈或来源卸载时清理对应提示。关闭 Toast 不清除请求失败状态或表单草稿,不触发写入操作。
-
查询失败保留失败空状态与只读重试入口,避免将接口错误显示为“暂无数据”;部分统计失败时保留已成功返回的数据与重新加载入口。
-
台账详情的保存成功提示由页面级独立来源维护,材料建档及记录写入全部成功才触发;编辑保存显示“详情已保存”。提示在关闭详情弹窗及刷新列表后保留至自动消失或手动关闭,开启下一次新建、预览、编辑或再次保存时清理。
## 字段后缀图标
-
Autocomplete 下拉箭头与 DatePicker 日历图标使用 16px Ant Design 图标;可选的选择清除图标与日期清除图标使用统一的 14px Ant Design
`Close`
图标。页容量选择器按上一条规则关闭清除图标。
...
...
public/manifest.json
View file @
b2ccff62
{
"name"
:
"incorrupt_party_build"
,
"version"
:
"1.0.
3
"
,
"version"
:
"1.0.
4
"
,
"build"
:
"abc"
,
"description"
:
"党风廉政建设"
,
"date"
:
"2025-07-14 19:27:20"
,
...
...
src/app.js
View file @
b2ccff62
import
'@/tailwind.css'
;
import
'@/global.less'
;
import
{
defineApp
}
from
'umi'
;
import
{
createElement
}
from
'react'
;
import
AppFeedback
from
'@/components/AppFeedback'
;
export
async
function
getInitialState
()
{
return
{
...
...
@@ -8,4 +10,8 @@ export async function getInitialState() {
};
}
export
default
defineApp
({});
export
function
rootContainer
(
container
)
{
return
createElement
(
AppFeedback
,
null
,
container
);
}
export
default
defineApp
({
rootContainer
});
src/components/AppFeedback.jsx
0 → 100644
View file @
b2ccff62
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/components/AppShell.jsx
View file @
b2ccff62
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Toast
}
from
'@heroui/react'
;
import
{
history
}
from
'umi'
;
import
ApplicationSwitcherDialog
from
'@/pages/project_manager_xy/ApplicationSwitcherDialog'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
...
...
@@ -47,7 +46,6 @@ export default function AppShell({
userInfo=
{
sidebarProps
.
userInfo
}
onOpenChange=
{
setIsApplicationDialogOpen
}
/>
<
Toast
.
Provider
maxVisibleToasts=
{
3
}
placement=
"top"
/>
</
Sidebar
.
Provider
>
);
}
src/design-system/feedback.css
0 → 100644
View file @
b2ccff62
/* 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 @
b2ccff62
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 @
b2ccff62
import
xyLogoRed
from
'@/assets/identity/xy-logo-red.png'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
xyLogoWhite
from
'@/assets/identity/xy-logo-white.png'
;
import
{
APP_BASE_PATH
,
APP_NAME
}
from
'@/config/ApplicationConfig'
;
import
authService
from
'@/services/authService'
;
import
{
getCaptcha
,
userLogin
}
from
'@/services/DataService'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
Alert
,
Button
,
Card
,
Input
,
...
...
@@ -55,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 +209,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 @
b2ccff62
...
...
@@ -5,12 +5,12 @@ import {
}
from
'@/config/ApplicationConfig'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
EmptyStateSurface
}
from
'@/components/DataTable'
;
import
{
ExclamationTriangleIcon
,
MagnifyingGlassIcon
}
from
'@/components/AppIcons'
;
import
{
MagnifyingGlassIcon
}
from
'@/components/AppIcons'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
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
,
...
...
@@ -312,7 +312,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
;
...
...
@@ -352,6 +352,7 @@ export default function ApplicationSwitcherDialog({
useEffect
(()
=>
{
if
(
!
isOpen
)
{
setApplicationSearch
(
''
);
setErrorMessage
(
''
);
return
;
}
...
...
@@ -392,7 +393,7 @@ export default function ApplicationSwitcherDialog({
return
()
=>
{
cancelled
=
true
;
};
},
[
fallbackApplications
,
isOpen
]);
},
[
fallbackApplications
,
isOpen
,
setErrorMessage
]);
return
(
<
Modal
.
Root
>
...
...
@@ -443,13 +444,6 @@ export default function ApplicationSwitcherDialog({
offset=
{
12
}
size=
{
24
}
>
{
status
===
'error'
&&
errorMessage
?
(
<
Alert
className=
"mb-4"
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/project_manager_xy/integrity-party.business-flow.md
View file @
b2ccff62
...
...
@@ -130,6 +130,18 @@
5.
前端逐条调用
`PUT /record/:name`
6.
附件、长文本主要内容等字段一期不参与批量修改
### 4.9 操作提示与失败重试
-
功能入口:登录、站点配置、应用切换、6 类台账的查询/保存/删除/Excel 导入导出/批量修改、详情材料搜索与上传、驾驶舱和通知中心。
-
接口成功、失败及降级警告统一显示根部 HeroUI Toast,Portal 在视口顶部展示,层级高于详情弹窗、删除确认框和移动侧栏;页面滚动或弹窗打开时也可看到并关闭。
-
成功提示默认 4 秒、错误和警告默认 8 秒后消失;悬停或键盘聚焦暂停计时。当前提示右侧的关闭图标始终可见,使用 HeroUI 小号透明图标按钮,通过 Toast 的
`close`
slot 支持点击或键盘关闭;后排堆叠提示隐藏关闭按钮以避免误关闭。同一来源的新提示替换旧提示,相同错误再次发生时重新弹出;清空反馈或离开对应页面时清理来源提示。
-
关闭提示仅影响展示,不改变请求结果、字段校验、表单输入、附件草稿、选中记录或分页筛选,不重新执行保存/删除/导入/批量修改/通知操作。必填校验摘要和字段错误仍保留在表单内,并定位第一个错误字段;批量修改和删除确认说明保留。
-
台账与通知查询失败时显示失败空状态及“重新加载/重试”,详细原因由 Toast 展示,关闭提示后不会误显示“暂无数据”。台账重试沿用当前分类、页码、页容量及页面筛选;通知重试沿用分类、已读状态、关键词和页码。重试仅重新读取资源,不重放写入操作。
-
驾驶舱全部查询失败时显示失败空状态和重试;部分失败时保留成功统计,标明统计可能不完整并提供重新加载入口。
-
材料查询失败时保留选择器内的加载错误与“重新加载材料”;上传错误单独提示,不影响材料搜索状态。多附件上传部分失败时保留已成功上传的附件草稿,提示已上传数量和未完成原因,保存详情时仍按原流程创建
`am_material`
。
-
保存失败保留详情弹窗和草稿;Excel 导入、批量修改及批量删除部分失败时使用 warning 展示成功/失败条数。站点配置与应用列表降级使用 warning 并沿用原有回退信息。
-
台账详情点击“保存”后,只有待建档材料和记录创建/更新全部成功才显示独立的成功提示:编辑显示“详情已保存”,新建显示记录创建结果。沿用保存成功后关闭详情弹窗并刷新列表的交互;提示来源保留在台账页面,不随弹窗关闭或列表重载被清除,默认 4 秒消失且可手动关闭。重新开启新建、预览、编辑或再次保存时清理上一条成功提示;校验未通过、正在保存或写入失败时不显示成功提示,失败保留草稿和错误反馈。
## 5. 字段与数据规则
-
日期字段页面和 Excel 统一展示为
`YYYY-MM-DD`
...
...
@@ -146,8 +158,8 @@
## 6. 异常处理与限制
-
保存失败时保留当前表单输入,弹窗保持打开,
并在弹窗内提示用户;必填校验同时
标记对应字段并定位到第一个错误字段
-
Excel 导入、批量修改和删除确认在弹窗内失败时,
错误信息在当前弹窗内展
示,不要求用户先关闭弹窗
-
保存失败时保留当前表单输入,弹窗保持打开,
通过高于弹窗的 Toast 提示用户;必填校验仍在弹窗内
标记对应字段并定位到第一个错误字段
-
Excel 导入、批量修改和删除确认在弹窗内失败时,
通过 Toast 展示可关闭的错误提
示,不要求用户先关闭弹窗
-
Excel 解析失败时不发起创建请求
-
Excel 批量导入单条失败不影响其它条目继续导入
-
批量删除和单条删除均需要二次确认
...
...
src/pages/project_manager_xy/integrity-party/IntegrityPartyMaterialField.jsx
View file @
b2ccff62
import
{
ArrowPathIcon
,
Delete
}
from
'@/components/AppIcons'
;
import
FileUploadField
from
'@/components/FileUploadField'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
{
Alert
,
Button
,
Label
,
Tabs
,
Tooltip
}
from
'@heroui/react'
;
import
{
Button
,
Label
,
Tabs
,
Tooltip
}
from
'@heroui/react'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
queryAuditMaterials
,
uploadIntegrityPartyFile
}
from
'../integrityPartyMaterialService'
;
...
...
@@ -16,19 +17,20 @@ export default function IntegrityPartyMaterialField({ editSessionId, fieldPath,
const
[
materials
,
setMaterials
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
materialsError
,
setMaterialsError
]
=
useFeedbackState
(
'error'
);
const
[,
setUploadError
]
=
useFeedbackState
(
'error'
);
const
[
retry
,
setRetry
]
=
useState
(
0
);
const
[
localKeys
,
setLocalKeys
]
=
useState
(()
=>
new
Set
());
const
files
=
useMemo
(()
=>
(
multiple
?
toFiles
(
value
)
:
toFiles
(
value
).
slice
(
0
,
1
)),
[
multiple
,
value
]);
useEffect
(()
=>
{
setTab
(
'material-center'
);
setKeyword
(
''
);
setDebouncedKeyword
(
''
);
set
Error
(
''
);
setLocalKeys
(
new
Set
());
},
[
editSessionId
]);
useEffect
(()
=>
{
setTab
(
'material-center'
);
setKeyword
(
''
);
setDebouncedKeyword
(
''
);
set
MaterialsError
(
''
);
setUploadError
(
''
);
setLocalKeys
(
new
Set
());
},
[
editSessionId
,
setMaterialsError
,
setUploadError
]);
useEffect
(()
=>
{
const
timer
=
window
.
setTimeout
(()
=>
setDebouncedKeyword
(
keyword
),
300
);
return
()
=>
window
.
clearTimeout
(
timer
);
},
[
keyword
]);
useEffect
(()
=>
{
if
(
tab
!==
'material-center'
)
return
undefined
;
let
cancelled
=
false
;
setLoading
(
true
);
setError
(
''
);
queryAuditMaterials
(
debouncedKeyword
).
then
((
items
)
=>
{
if
(
!
cancelled
)
setMaterials
(
items
);
}).
catch
((
requestError
)
=>
{
if
(
!
cancelled
)
{
setMaterials
([]);
setError
(
requestError
?.
message
||
'审计材料加载失败,请稍后重试。'
);
}
}).
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
let
cancelled
=
false
;
setLoading
(
true
);
set
Materials
Error
(
''
);
queryAuditMaterials
(
debouncedKeyword
).
then
((
items
)
=>
{
if
(
!
cancelled
)
setMaterials
(
items
);
}).
catch
((
requestError
)
=>
{
if
(
!
cancelled
)
{
setMaterials
([]);
set
Materials
Error
(
requestError
?.
message
||
'审计材料加载失败,请稍后重试。'
);
}
}).
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
},
[
debouncedKeyword
,
retry
,
tab
]);
},
[
debouncedKeyword
,
retry
,
tab
,
setMaterialsError
]);
function
selectMaterial
(
key
)
{
const
material
=
materials
.
find
((
item
)
=>
String
(
item
.
id
)
===
String
(
key
||
''
));
...
...
@@ -41,16 +43,22 @@ export default function IntegrityPartyMaterialField({ editSessionId, fieldPath,
async
function
uploadLocal
(
selected
)
{
const
inputFiles
=
multiple
?
toFiles
(
selected
)
:
toFiles
(
selected
).
slice
(
0
,
1
);
if
(
inputFiles
.
length
===
0
)
return
;
setUploading
(
true
);
setError
(
''
);
setUploading
(
true
);
set
Upload
Error
(
''
);
const
uploaded
=
[];
try
{
for
(
const
file
of
inputFiles
)
uploaded
.
push
(
await
uploadIntegrityPartyFile
(
file
));
const
pending
=
uploaded
.
map
((
asset
)
=>
({
clientId
:
fileKey
(
asset
),
fileAsset
:
asset
,
materialRecordId
:
null
}));
setLocalKeys
((
current
)
=>
new
Set
(
multiple
?
[...
current
,
...
pending
.
map
((
item
)
=>
item
.
clientId
)]
:
pending
.
map
((
item
)
=>
item
.
clientId
)));
onPendingChange
(
fieldPath
,
{
type
:
multiple
?
'add'
:
'replace'
,
materials
:
pending
});
onChange
(
multiple
?
uniqueFiles
([...
files
,
...
uploaded
])
:
uploaded
[
uploaded
.
length
-
1
]);
}
catch
(
uploadError
)
{
setError
(
uploadError
?.
message
||
'附件上传失败,请稍后重试。'
);
}
finally
{
setUploading
(
false
);
}
}
catch
(
uploadError
)
{
const
message
=
uploadError
?.
message
||
'附件上传失败,请稍后重试。'
;
setUploadError
(
uploaded
.
length
>
0
?
`已上传
${
uploaded
.
length
}
个附件,其余附件未完成上传:
${
message
}
`
:
message
);
}
finally
{
if
(
uploaded
.
length
>
0
)
{
const
pending
=
uploaded
.
map
((
asset
)
=>
({
clientId
:
fileKey
(
asset
),
fileAsset
:
asset
,
materialRecordId
:
null
}));
setLocalKeys
((
current
)
=>
new
Set
(
multiple
?
[...
current
,
...
pending
.
map
((
item
)
=>
item
.
clientId
)]
:
pending
.
map
((
item
)
=>
item
.
clientId
)));
onPendingChange
(
fieldPath
,
{
type
:
multiple
?
'add'
:
'replace'
,
materials
:
pending
});
onChange
(
multiple
?
uniqueFiles
([...
files
,
...
uploaded
])
:
uploaded
[
uploaded
.
length
-
1
]);
}
setUploading
(
false
);
}
}
function
removeFile
(
file
,
index
)
{
...
...
@@ -87,6 +95,7 @@ export default function IntegrityPartyMaterialField({ editSessionId, fieldPath,
ariaLabel="选择审计材料"
className="w-full min-w-0"
emptyText="没有找到可选择的审计材料"
errorText={materialsError}
inputValue={keyword}
isDisabled={disabled || uploading}
isLoading={loading}
...
...
@@ -106,16 +115,11 @@ export default function IntegrityPartyMaterialField({ editSessionId, fieldPath,
</span>
)}
/>
{error ? (
<Alert className="mt-3" status="danger">
<Alert.Content>
<Alert.Description>{error}</Alert.Description>
</Alert.Content>
<Button size="sm" variant="danger-soft" onPress={() => setRetry((value) => value + 1)}>
<ArrowPathIcon size={14} />
重试
</Button>
</Alert>
{materialsError ? (
<Button className="mt-3" size="sm" variant="secondary" onPress={() => setRetry((value) => value + 1)}>
<ArrowPathIcon size={14} />
重新加载材料
</Button>
) : null}
</Tabs.Panel>
<Tabs.Panel className="mt-3 w-full min-w-0 p-0" id="local-upload">
...
...
@@ -126,13 +130,6 @@ export default function IntegrityPartyMaterialField({ editSessionId, fieldPath,
triggerLabel={uploading ? '上传中...' : '选择本地文件'}
onFilesChange={uploadLocal}
/>
{error ? (
<Alert className="mt-3" status="danger">
<Alert.Content>
<Alert.Description>{error}</Alert.Description>
</Alert.Content>
</Alert>
) : null}
</Tabs.Panel>
</Tabs>
</div>;
...
...
src/pages/project_manager_xy/integrity-party/index.jsx
View file @
b2ccff62
This diff is collapsed.
Click to expand it.
src/pages/project_manager_xy/notifications/NotificationListPage.jsx
View file @
b2ccff62
import
{
Bell
,
Close
,
MagnifyingGlassIcon
,
OpenExternal
}
from
'@/components/AppIcons'
;
import
{
DataTablePagination
}
from
'@/components/DataTable'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
A
lert
,
A
vatar
,
Button
,
Card
,
Chip
,
SearchField
,
Spinner
,
Tabs
,
toast
}
from
'@heroui/react'
;
import
{
Avatar
,
Button
,
Card
,
Chip
,
SearchField
,
Spinner
,
Tabs
,
toast
}
from
'@heroui/react'
;
import
{
useCallback
,
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
import
{
...
...
@@ -75,7 +76,7 @@ export default function NotificationListPage() {
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
unread
,
setUnread
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
[
busyKey
,
setBusyKey
]
=
useState
(
''
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
...
...
@@ -92,7 +93,7 @@ export default function NotificationListPage() {
.
catch
((
requestError
)
=>
{
if
(
!
cancelled
)
{
setItems
([]);
setTotal
(
0
);
setError
(
requestError
?.
message
||
'通知列表加载失败,请稍后重试'
);
}
})
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
},
[
category
,
keyword
,
page
,
readStatus
,
reloadKey
]);
},
[
category
,
keyword
,
page
,
readStatus
,
reloadKey
,
setError
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
...
...
@@ -103,8 +104,8 @@ export default function NotificationListPage() {
const
refresh
=
useCallback
(()
=>
setReloadKey
((
value
)
=>
value
+
1
),
[]);
const
run
=
useCallback
(
async
(
key
,
operation
,
message
)
=>
{
setBusyKey
(
key
);
try
{
await
operation
();
toast
.
success
(
message
,
{
timeout
:
3
000
});
refresh
();
return
true
;
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
timeout
:
4
000
});
return
false
;
}
try
{
await
operation
();
toast
.
success
(
message
,
{
timeout
:
4
000
});
refresh
();
return
true
;
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
timeout
:
8
000
});
return
false
;
}
finally
{
setBusyKey
(
''
);
}
},
[
refresh
]);
...
...
@@ -115,7 +116,7 @@ export default function NotificationListPage() {
if
(
target
.
external
)
window
.
open
(
target
.
href
,
'_blank'
,
'noopener,noreferrer'
);
else
history
.
push
(
target
.
href
);
refresh
();
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
timeout
:
4
000
});
}
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
timeout
:
8
000
});
}
finally
{
setBusyKey
(
''
);
}
},
[
refresh
,
userInfo
?.
_id
]);
...
...
@@ -135,7 +136,15 @@ export default function NotificationListPage() {
<
Tabs
.
ListContainer
><
Tabs
.
List
aria
-
label=
"通知状态"
>
{
READ_OPTIONS
.
map
((
option
)
=>
<
Tabs
.
Tab
id=
{
option
.
key
}
key=
{
option
.
key
}
>
{
option
.
label
}
<
Tabs
.
Indicator
/></
Tabs
.
Tab
>)
}
</
Tabs
.
List
></
Tabs
.
ListContainer
>
</
Tabs
>
</
div
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
><
Button
size=
"sm"
variant=
"ghost"
onPress=
{
refresh
}
>
重试
</
Button
></
Alert
>
:
null
}
{
error
?
(
<
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=
{
refresh
}
>
重试
</
Button
></
EmptyState
.
Content
>
</
EmptyState
>
)
:
null
}
{
loading
&&
items
.
length
===
0
?
<
div
className=
"flex min-h-64 items-center justify-center"
><
Spinner
size=
"sm"
/></
div
>
:
null
}
{
!
loading
&&
!
error
&&
items
.
length
===
0
?
<
EmptyState
className=
"min-h-64"
size=
"md"
><
EmptyState
.
Media
><
Bell
size=
{
22
}
/></
EmptyState
.
Media
><
EmptyState
.
Header
><
EmptyState
.
Title
>
{
category
===
'todo'
?
'暂无待办通知'
:
'暂无通知消息'
}
</
EmptyState
.
Title
><
EmptyState
.
Description
>
{
keyword
?
'没有找到匹配的通知,请调整搜索条件。'
:
readStatus
===
'unread'
?
'当前没有未读通知。'
:
'新的通知会显示在这里。'
}
</
EmptyState
.
Description
></
EmptyState
.
Header
></
EmptyState
>
:
null
}
{
items
.
length
>
0
?
<
div
className=
"flex min-w-0 flex-col gap-3"
aria
-
busy=
{
loading
}
>
{
items
.
map
((
item
)
=>
<
NotificationItem
category=
{
category
}
currentUserId=
{
userInfo
?.
_id
}
isBusy=
{
busyKey
===
item
.
name
}
key=
{
item
.
name
||
item
.
_id
}
notification=
{
item
}
onAction=
{
(
notification
,
action
)
=>
run
(
notification
.
name
,
()
=>
executeNotificationAction
(
notification
,
action
),
'待办已处理'
)
}
onMarkRead=
{
(
notification
)
=>
run
(
notification
.
name
,
()
=>
markNotificationRead
(
notification
.
name
),
'已标记为已读'
)
}
onOpenTarget=
{
openTarget
}
/>)
}
</
div
>
:
null
}
...
...
src/pages/project_manager_xy/resource-stat/index.jsx
View file @
b2ccff62
...
...
@@ -6,16 +6,18 @@ import {
import
{
XINYUAN_CHART_CATEGORY_PALETTE
,
createResolvedXinyuanChartColorMap
,
createResolvedXinyuanPieChartColorMap
,
getXinyuanEChartsTheme
,
resolveXinyuanChartColor
,
XINYUAN_CHART_SEMANTIC
,
}
from
'@/design-system/chartPalette'
;
import
XinyuanEChart
from
'@/components/XinyuanEChart'
;
import
{
Alert
,
Card
,
Spinner
}
from
'@heroui/react'
;
import
{
Button
,
Card
,
Spinner
}
from
'@heroui/react'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
KPI
}
from
'@heroui-pro/react/kpi'
;
import
{
useEffect
,
useState
}
from
'react'
;
import
{
loadIntegrityPartyDashboard
}
from
'../integrityPartyService'
;
import
{
INTEGRITY_LEDGER_CONFIGS
,
loadIntegrityPartyDashboard
}
from
'../integrityPartyService'
;
function
formatCount
(
value
)
{
const
numericValue
=
Number
(
value
||
0
);
...
...
@@ -87,13 +89,75 @@ function LedgerDistributionChart({ groups }) {
name
:
item
.
label
,
value
:
Number
(
item
.
count
||
0
),
}));
const
total
=
data
.
reduce
((
sum
,
item
)
=>
sum
+
item
.
value
,
0
);
const
populatedCount
=
data
.
filter
((
item
)
=>
item
.
value
>
0
).
length
;
const
colorMap
=
createResolvedXinyuanPieChartColorMap
(
data
.
map
((
item
)
=>
item
.
name
));
const
formatPercent
=
(
value
)
=>
`
${
Number
(((
value
/
total
)
*
100
).
toFixed
(
1
))}
%`
;
if
(
data
.
length
===
0
)
return
<
EmptyBlock
title=
"暂无台账分类数据"
/>;
if
(
total
===
0
)
return
<
EmptyBlock
title=
"暂无台账分类数据"
/>;
return
<
XinyuanEChart
ariaLabel=
"台账分类分布图"
option=
{
{
legend
:
{
bottom
:
0
,
data
:
data
.
map
((
item
)
=>
item
.
name
)
},
tooltip
:
{
trigger
:
'item'
,
formatter
:
({
name
,
value
,
percent
})
=>
`${name}<br/>数量:${value} 条(${percent}%)`
},
series
:
[{
type
:
'pie'
,
radius
:
[
'52%'
,
'72%'
],
center
:
[
'50%'
,
'44%'
],
avoidLabelOverlap
:
true
,
data
:
data
.
map
((
item
)
=>
({
name
:
item
.
name
,
value
:
item
.
value
})),
label
:
{
alignTo
:
'labelLine'
,
distanceToLabelLine
:
6
,
formatter
:
({
name
,
value
})
=>
`{name|${name}}\n{value|${value} 条}`
,
lineHeight
:
18
,
rich
:
{
name
:
{
fontSize
:
13
,
fontWeight
:
500
,
lineHeight
:
18
},
value
:
{
fontSize
:
14
,
fontWeight
:
600
,
lineHeight
:
20
}
}
},
labelLine
:
{
length
:
20
,
length2
:
22
,
lineStyle
:
{
width
:
1.5
}
},
labelLayout
:
(
params
)
=>
{
const
points
=
params
?.
labelLinePoints
;
if
(
!
Array
.
isArray
(
points
)
||
points
.
length
<
3
)
return
null
;
const
endpoint
=
points
[
2
];
const
isRight
=
endpoint
[
0
]
>=
points
[
0
][
0
];
return
{
align
:
isRight
?
'left'
:
'right'
,
hideOverlap
:
true
,
moveOverlap
:
'none'
,
verticalAlign
:
'middle'
,
x
:
endpoint
[
0
]
+
(
isRight
?
8
:
-
8
),
y
:
endpoint
[
1
]
};
}
}]
}
}
height=
{
320
}
/>;
return
(
<
div
className=
"@container min-w-0"
>
<
div
className=
"grid min-h-80 items-center gap-6 py-2 @min-[30rem]:grid-cols-[minmax(0,1fr)_minmax(0,1.15fr)]"
>
<
div
className=
"relative mx-auto w-full max-w-64"
>
<
XinyuanEChart
ariaLabel=
{
`台账分类分布图,共 ${formatCount(total)} 条,分类数量和占比详见分类明细表`
}
height=
{
248
}
option=
{
{
animation
:
!
window
.
matchMedia
(
'(prefers-reduced-motion: reduce)'
).
matches
,
tooltip
:
{
trigger
:
'item'
,
formatter
:
({
name
,
value
})
=>
`${name}<br/>数量:${formatCount(value)} 条(${formatPercent(value)})`
,
},
series
:
[{
type
:
'pie'
,
radius
:
[
'66%'
,
'86%'
],
center
:
[
'50%'
,
'50%'
],
padAngle
:
populatedCount
>
1
?
2
:
0
,
stillShowZeroSum
:
false
,
data
,
label
:
{
show
:
false
},
labelLine
:
{
show
:
false
},
emphasis
:
{
scaleSize
:
4
},
}],
}
}
/>
<
div
aria
-
hidden=
"true"
className=
"pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-1"
>
<
span
className=
"text-sm text-muted"
>
台账总数
</
span
>
<
span
className=
"max-w-[60%] truncate text-4xl font-semibold tracking-tight text-foreground tabular-nums"
title=
{
formatCount
(
total
)
}
>
{
formatCount
(
total
)
}
</
span
>
<
span
className=
"text-xs text-muted"
>
条记录
</
span
>
</
div
>
</
div
>
<
table
className=
"w-full table-fixed text-sm"
>
<
caption
className=
"sr-only"
>
台账分类数量及占比明细
</
caption
>
<
thead
>
<
tr
className=
"border-b border-border text-xs text-muted"
>
<
th
className=
"pb-3 text-left font-normal"
scope=
"col"
>
分类
</
th
>
<
th
className=
"w-16 pb-3 text-right font-normal"
scope=
"col"
>
数量
</
th
>
<
th
className=
"w-16 pb-3 text-right font-normal"
scope=
"col"
>
占比
</
th
>
</
tr
>
</
thead
>
<
tbody
>
{
data
.
map
((
item
)
=>
(
<
tr
className=
"h-11"
key=
{
item
.
name
}
>
<
th
className=
"py-2 pr-2 text-left font-normal"
scope=
"row"
>
<
span
className=
"flex items-center gap-2.5 text-foreground"
>
<
span
aria
-
hidden=
"true"
className=
"size-2.5 shrink-0 rounded-full"
style=
{
{
backgroundColor
:
colorMap
.
get
(
item
.
name
)
}
}
/>
<
span
>
{
item
.
name
}
</
span
>
</
span
>
</
th
>
<
td
className=
"text-right font-medium text-foreground tabular-nums"
>
{
formatCount
(
item
.
value
)
}
</
td
>
<
td
className=
"text-right text-muted tabular-nums"
>
{
formatPercent
(
item
.
value
)
}
</
td
>
</
tr
>
))
}
</
tbody
>
</
table
>
</
div
>
</
div
>
);
}
function
YearDistributionChart
({
groups
})
{
...
...
@@ -219,21 +283,26 @@ function TalkOverview({ summary }) {
export
default
function
ResourceStatPage
()
{
const
[
integrityDashboard
,
setIntegrityDashboard
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
useEffect
(()
=>
{
let
cancelled
=
false
;
(
async
()
=>
{
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
try
{
const
integrityResult
=
await
loadIntegrityPartyDashboard
();
if
(
cancelled
)
return
;
setIntegrityDashboard
(
integrityResult
);
setLoadFailed
(
integrityResult
.
errors
?.
length
===
INTEGRITY_LEDGER_CONFIGS
.
length
);
setError
((
integrityResult
.
errors
||
[]).
filter
(
Boolean
).
join
(
';'
));
}
catch
(
loadError
)
{
if
(
cancelled
)
return
;
setIntegrityDashboard
(
null
);
setLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'党风廉政驾驶舱加载失败'
);
}
finally
{
if
(
!
cancelled
)
setLoading
(
false
);
...
...
@@ -243,7 +312,7 @@ export default function ResourceStatPage() {
return
()
=>
{
cancelled
=
true
;
};
},
[]);
},
[
reloadKey
,
setError
]);
const
dashboard
=
integrityDashboard
||
{
totalCount
:
0
,
...
...
@@ -261,21 +330,29 @@ export default function ResourceStatPage() {
return
(
<
div
className=
"rc-page-stack"
>
{
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
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"
/>
<
span
className=
"text-sm text-muted"
>
正在加载驾驶舱...
</
span
>
</
div
>
)
:
loadFailed
?
(
<
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
>
)
:
(
<>
{
error
?
(
<
div
className=
"flex flex-wrap items-center gap-3"
>
<
span
className=
"text-sm text-muted"
>
部分台账加载失败,当前统计可能不完整。
</
span
>
<
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
>
重新加载统计
</
Button
>
</
div
>
)
:
null
}
<
section
className=
"rc-page-section"
>
<
div
className=
"rc-kpi-grid"
data
-
count=
"3"
>
<
MetricCard
icon=
{
ClipboardDocumentListIcon
}
label=
"台账总数"
tone=
"info"
value=
{
dashboard
.
totalCount
}
/>
...
...
src/pages/project_manager_xy/resource-stat/resource-stat.business-flow.md
View file @
b2ccff62
...
...
@@ -9,9 +9,18 @@
1.
页面进入后并行查询培训、谈话、调研、专题会议、廉洁主题活动、节前廉洁提醒 6 类台账
2.
加载期间展示加载状态;全部请求结束后展示指标卡、台账分类分布、年度分布和本年度模块概览
3.
部分或全部请求失败时,
在页面顶部展示错误信息;成功返回的数据仍按现有结果展示
3.
部分或全部请求失败时,
通过根部 Toast 展示详细错误,默认 8 秒消失且可手动关闭;部分失败保留成功统计并标明统计可能不完整,提供“重新加载统计”;全部失败展示失败空状态与“重新加载”,不显示零值统计
4.
查询结果为空时,对应图表或概览区域展示空状态
### 台账分类分布展示
-
圆环中心显示当前统计的台账总数,右侧明细表按既有 6 类台账顺序展示分类、数量与占比;卡片内容宽度不足 30rem 时改为圆环在上、明细表在下
-
数量为 0 的分类保留在明细表中,显示
`0`
和
`0%`
,圆环不显示外置标签及引导线,避免单分类有数据时零值标签堆叠
-
明细表色点与圆环共用共享饼图色板和完整分类集合,同一分类颜色保持一致;仅一个分类有数据时显示完整圆环,多个分类时保留扇区间隙
-
占比使用分类查询总数除以 6 类查询总数之和计算,最多显示一位小数;各分类数量直接展示查询总数,不受单类读取 500 条的限制
-
悬停扇区展示分类、数量和占比;明细表提供相同信息,支持无需悬停及屏幕阅读器读取。系统开启减少动态效果时关闭此圆环的入场与更新动画
-
6 类总数均为 0 时展示“暂无台账分类数据”,不绘制无业务含义的等分圆环;请求失败仍按上文失败与重试流程处理
## 3. 年度分布规则
-
年份取自各台账配置的年度字段:日期字段截取年份,数字年份字段直接转为文本
...
...
@@ -34,6 +43,7 @@
## 5. 异常与限制
-
API 错误信息统一展示在页面顶部,不隐藏已成功加载的统计结果
-
API 错误信息使用高于弹窗和移动侧栏的 Toast;关闭提示不改变失败状态,不隐藏已成功加载的统计结果。悬停或键盘聚焦暂停计时,相同错误在重试失败时重新显示
-
重新加载仅并行读取当前 6 类台账,不执行写入操作;重试成功后移除失败状态或统计不完整标记
-
单类台账超过 500 条时,当前年度分布可能不包含第 501 条及之后的记录
-
年度字段为空时归入非年份分组,并排在有效年份之后
src/tailwind.css
View file @
b2ccff62
...
...
@@ -7,6 +7,7 @@
@import
"./design-system/theme.css"
;
@import
"./design-system/tokens.css"
;
@import
"./design-system/shell.css"
;
@import
"./design-system/feedback.css"
;
/* Keep Tailwind's watcher scoped to authored source. Umi's generated .umi
* files and hot-update artifacts must not trigger a CSS rebuild loop. */
...
...
src/wrapper/Site.jsx
View file @
b2ccff62
import
{
getSystemInfo
}
from
'@/services/DataService'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
APP_NAME
}
from
'@/config/ApplicationConfig'
;
import
dayjs
from
'dayjs'
;
import
customParseFormat
from
'dayjs/plugin/customParseFormat'
;
...
...
@@ -19,7 +20,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
;
...
...
@@ -55,7 +56,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