Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
M
model_analysis
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
model_analysis
Commits
f167d242
Commit
f167d242
authored
Oct 08, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: make operation feedback visible and dismissible
parent
346c685a
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
210 additions
and
70 deletions
+210
-70
app.js
src/app.js
+7
-1
AppFeedback.jsx
src/components/AppFeedback.jsx
+32
-0
AppShell.jsx
src/components/AppShell.jsx
+0
-2
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
+3
-10
ForwardApprovals.jsx
src/pages/project_manager_xy/ForwardApprovals.jsx
+12
-8
ModelBatchData.jsx
src/pages/project_manager_xy/ModelBatchData.jsx
+9
-8
ModelBatches.jsx
src/pages/project_manager_xy/ModelBatches.jsx
+16
-4
ModelMonitorShared.jsx
src/pages/project_manager_xy/ModelMonitorShared.jsx
+19
-1
ModelOverview.jsx
src/pages/project_manager_xy/ModelOverview.jsx
+8
-5
Models.jsx
src/pages/project_manager_xy/Models.jsx
+20
-10
model-monitor.business-flow.md
src/pages/project_manager_xy/model-monitor.business-flow.md
+11
-0
NotificationListPage.jsx
...project_manager_xy/notifications/NotificationListPage.jsx
+9
-10
notifications.business-flow.md
...t_manager_xy/notifications/notifications.business-flow.md
+1
-0
tailwind.css
src/tailwind.css
+20
-0
Site.jsx
src/wrapper/Site.jsx
+2
-1
No files found.
src/app.js
View file @
f167d242
import
'@/global.less'
;
import
'@/global.less'
;
import
'@/tailwind.css'
;
import
'@/tailwind.css'
;
import
{
defineApp
}
from
'umi'
;
import
{
defineApp
}
from
'umi'
;
import
{
createElement
}
from
'react'
;
import
AppFeedback
from
'@/components/AppFeedback'
;
if
(
typeof
document
!==
'undefined'
)
{
if
(
typeof
document
!==
'undefined'
)
{
document
.
documentElement
.
classList
.
remove
(
'dark'
);
document
.
documentElement
.
classList
.
remove
(
'dark'
);
...
@@ -15,4 +17,8 @@ export async function getInitialState() {
...
@@ -15,4 +17,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 @
f167d242
import
{
Toast
}
from
'@heroui/react'
;
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"
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
>
<
Toast
.
CloseButton
aria
-
label=
"关闭提示"
className=
"app-feedback-close"
/>
</
Toast
>
)
}
</
Toast
.
Provider
>
</>
);
}
src/components/AppShell.jsx
View file @
f167d242
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Toast
}
from
'@heroui/react'
;
import
{
history
}
from
'umi'
;
import
{
history
}
from
'umi'
;
import
DashboardNavbar
from
'./DashboardNavbar'
;
import
DashboardNavbar
from
'./DashboardNavbar'
;
import
DashboardSidebar
from
'./DashboardSidebar'
;
import
DashboardSidebar
from
'./DashboardSidebar'
;
...
@@ -34,7 +33,6 @@ export default function AppShell({
...
@@ -34,7 +33,6 @@ export default function AppShell({
<
div
className=
"rc-app-page"
>
{
children
}
</
div
>
<
div
className=
"rc-app-page"
>
{
children
}
</
div
>
</
div
>
</
div
>
</
Sidebar
.
Main
>
</
Sidebar
.
Main
>
<
Toast
.
Provider
maxVisibleToasts=
{
3
}
placement=
"top"
/>
</
Sidebar
.
Provider
>
</
Sidebar
.
Provider
>
);
);
}
}
src/hooks/useFeedbackState.js
0 → 100644
View file @
f167d242
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
},
};
// Keep request state separate from dismissal: closing a prompt does not turn a failed load into success.
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
;
// Report every attempt, including repeated failures with identical messages.
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 @
f167d242
...
@@ -5,7 +5,8 @@ import authService from '@/services/authService';
...
@@ -5,7 +5,8 @@ import authService from '@/services/authService';
import
{
resolveLoginRedirect
}
from
'@/utils/authRedirect'
;
import
{
resolveLoginRedirect
}
from
'@/utils/authRedirect'
;
import
{
getCaptcha
,
userLogin
}
from
'@/services/DataService'
;
import
{
getCaptcha
,
userLogin
}
from
'@/services/DataService'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
Alert
,
Button
,
Card
,
Input
,
Label
,
TextField
}
from
'@heroui/react'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
,
Card
,
Input
,
Label
,
TextField
}
from
'@heroui/react'
;
import
CryptoJS
from
'crypto-js'
;
import
CryptoJS
from
'crypto-js'
;
import
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
history
,
useSearchParams
}
from
'umi'
;
import
{
history
,
useSearchParams
}
from
'umi'
;
...
@@ -27,7 +28,7 @@ export default function LoginPage() {
...
@@ -27,7 +28,7 @@ export default function LoginPage() {
const
[
captchaData
,
setCaptchaData
]
=
useState
(
null
);
const
[
captchaData
,
setCaptchaData
]
=
useState
(
null
);
const
[
captchaLoading
,
setCaptchaLoading
]
=
useState
(
false
);
const
[
captchaLoading
,
setCaptchaLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
,
setError
]
=
useFeedbackState
(
);
const
redirect
=
resolveLoginRedirect
(
searchParams
.
get
(
'redirect'
));
const
redirect
=
resolveLoginRedirect
(
searchParams
.
get
(
'redirect'
));
const
refreshCaptcha
=
async
()
=>
{
const
refreshCaptcha
=
async
()
=>
{
...
@@ -172,14 +173,6 @@ export default function LoginPage() {
...
@@ -172,14 +173,6 @@ export default function LoginPage() {
</
div
>
</
div
>
</
div
>
</
div
>
{
error
?
(
<
Alert
className=
"login-error"
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
<
Button
<
Button
className=
"login-submit-button"
className=
"login-submit-button"
fullWidth
fullWidth
...
...
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
View file @
f167d242
import
xinyuanLogo
from
'@/assets/xy-logo-red.png'
;
import
xinyuanLogo
from
'@/assets/xy-logo-red.png'
;
import
{
APP_DISPLAY_NAME
,
APP_ROUTE_NAME
}
from
'@/constants/app'
;
import
{
APP_DISPLAY_NAME
,
APP_ROUTE_NAME
}
from
'@/constants/app'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
ExclamationTriangleIcon
,
MagnifyingGlassIcon
}
from
'@/components/AppIcons'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
MagnifyingGlassIcon
}
from
'@/components/AppIcons'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
ItemCard
}
from
'@heroui-pro/react/item-card'
;
import
{
ItemCard
}
from
'@heroui-pro/react/item-card'
;
import
{
ItemCardGroup
}
from
'@heroui-pro/react/item-card-group'
;
import
{
ItemCardGroup
}
from
'@heroui-pro/react/item-card-group'
;
import
{
import
{
Alert
,
Avatar
,
Avatar
,
Chip
,
Chip
,
Modal
,
Modal
,
...
@@ -312,7 +312,7 @@ export default function ApplicationSwitcherDialog({
...
@@ -312,7 +312,7 @@ export default function ApplicationSwitcherDialog({
const
[
status
,
setStatus
]
=
useState
(
const
[
status
,
setStatus
]
=
useState
(
fallbackApplications
.
length
>
0
?
'success'
:
'idle'
,
fallbackApplications
.
length
>
0
?
'success'
:
'idle'
,
);
);
const
[
errorMessage
,
setErrorMessage
]
=
useState
(
'
'
);
const
[
,
setErrorMessage
]
=
useFeedbackState
(
'warning
'
);
const
[
applicationSearch
,
setApplicationSearch
]
=
useState
(
''
);
const
[
applicationSearch
,
setApplicationSearch
]
=
useState
(
''
);
const
currentApplicationName
=
useMemo
(()
=>
{
const
currentApplicationName
=
useMemo
(()
=>
{
return
applications
.
find
(
isCurrentApplication
)?.
name
||
CURRENT_APPLICATION_NAME
;
return
applications
.
find
(
isCurrentApplication
)?.
name
||
CURRENT_APPLICATION_NAME
;
...
@@ -442,13 +442,6 @@ export default function ApplicationSwitcherDialog({
...
@@ -442,13 +442,6 @@ export default function ApplicationSwitcherDialog({
offset=
{
12
}
offset=
{
12
}
size=
{
24
}
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
?
(
{
status
===
'loading'
&&
applications
.
length
===
0
?
(
<
LoadingSkeleton
/>
<
LoadingSkeleton
/>
)
:
filteredApplicationSections
.
length
>
0
?
(
)
:
filteredApplicationSections
.
length
>
0
?
(
...
...
src/pages/project_manager_xy/ForwardApprovals.jsx
View file @
f167d242
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
{
Eye
}
from
'@/components/AppIcons'
;
import
{
Eye
}
from
'@/components/AppIcons'
;
import
{
Button
,
Description
,
Surface
,
Tabs
}
from
'@heroui/react'
;
import
{
Button
,
Description
,
Surface
,
Tabs
}
from
'@heroui/react'
;
...
@@ -93,12 +94,14 @@ export default function ForwardApprovals() {
...
@@ -93,12 +94,14 @@ export default function ForwardApprovals() {
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
pageSize
,
setPageSize
]
=
useState
(
DEFAULT_PAGE_SIZE
);
const
[
pageSize
,
setPageSize
]
=
useState
(
DEFAULT_PAGE_SIZE
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
editLoading
,
setEditLoading
]
=
useState
(
false
);
const
[
editLoading
,
setEditLoading
]
=
useState
(
false
);
const
[
editSaving
,
setEditSaving
]
=
useState
(
false
);
const
[
editSaving
,
setEditSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
,
setError
]
=
useFeedbackState
(
);
const
[
notice
,
setNotice
]
=
useState
(
'
'
);
const
[
,
setNotice
]
=
useFeedbackState
(
'success
'
);
const
[
notificationWarning
,
setNotificationWarning
]
=
useState
(
'
'
);
const
[
,
setNotificationWarning
]
=
useFeedbackState
(
'warning
'
);
const
[
activeRecordId
,
setActiveRecordId
]
=
useState
(
''
);
const
[
activeRecordId
,
setActiveRecordId
]
=
useState
(
''
);
const
[
isDetailDialogOpen
,
setIsDetailDialogOpen
]
=
useState
(
false
);
const
[
isDetailDialogOpen
,
setIsDetailDialogOpen
]
=
useState
(
false
);
const
[
isEditDialogOpen
,
setIsEditDialogOpen
]
=
useState
(
false
);
const
[
isEditDialogOpen
,
setIsEditDialogOpen
]
=
useState
(
false
);
...
@@ -112,6 +115,8 @@ export default function ForwardApprovals() {
...
@@ -112,6 +115,8 @@ export default function ForwardApprovals() {
(
async
()
=>
{
(
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
setIsDetailDialogOpen
(
false
);
setIsDetailDialogOpen
(
false
);
setIsEditDialogOpen
(
false
);
setIsEditDialogOpen
(
false
);
try
{
try
{
...
@@ -148,6 +153,7 @@ export default function ForwardApprovals() {
...
@@ -148,6 +153,7 @@ export default function ForwardApprovals() {
}
}
}
catch
(
loadError
)
{
}
catch
(
loadError
)
{
if
(
!
cancelled
)
{
if
(
!
cancelled
)
{
setLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'转发审批列表加载失败'
);
setError
(
loadError
?.
message
||
'转发审批列表加载失败'
);
}
}
}
finally
{
}
finally
{
...
@@ -160,7 +166,7 @@ export default function ForwardApprovals() {
...
@@ -160,7 +166,7 @@ export default function ForwardApprovals() {
return
()
=>
{
return
()
=>
{
cancelled
=
true
;
cancelled
=
true
;
};
};
},
[
currentUserId
,
forwardId
]);
},
[
currentUserId
,
forwardId
,
reloadKey
,
setError
]);
const
editableInvolvedCount
=
useMemo
(
const
editableInvolvedCount
=
useMemo
(
()
=>
()
=>
...
@@ -505,9 +511,7 @@ export default function ForwardApprovals() {
...
@@ -505,9 +511,7 @@ export default function ForwardApprovals() {
/>
/>
{
loading
?
<
NoticeBanner
>
正在加载转发审批记录...
</
NoticeBanner
>
:
null
}
{
loading
?
<
NoticeBanner
>
正在加载转发审批记录...
</
NoticeBanner
>
:
null
}
{
!
loading
&&
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
loadFailed
?
<
EmptyCollection
title=
"转发审批加载失败"
description=
"请重新加载,详细错误见操作提示。"
action=
{
<
Button
variant=
"secondary"
onPress=
{
()
=>
setReloadKey
((
current
)
=>
current
+
1
)
}
>
重新加载
</
Button
>
}
/>
:
null
}
{
!
error
&&
notice
?
<
NoticeBanner
tone=
"success"
>
{
notice
}
</
NoticeBanner
>
:
null
}
{
notificationWarning
?
<
NoticeBanner
>
{
notificationWarning
}
</
NoticeBanner
>
:
null
}
<
div
className=
"min-w-0 max-w-full"
>
<
div
className=
"min-w-0 max-w-full"
>
<
Tabs
className=
"w-fit max-w-full min-w-0"
selectedKey=
{
activeFilter
}
variant=
"primary"
onSelectionChange=
{
(
key
)
=>
{
setActiveFilter
(
String
(
key
));
setPage
(
0
);
}
}
>
<
Tabs
className=
"w-fit max-w-full min-w-0"
selectedKey=
{
activeFilter
}
variant=
"primary"
onSelectionChange=
{
(
key
)
=>
{
setActiveFilter
(
String
(
key
));
setPage
(
0
);
}
}
>
...
@@ -526,7 +530,7 @@ export default function ForwardApprovals() {
...
@@ -526,7 +530,7 @@ export default function ForwardApprovals() {
{
editableInvolvedCount
>
0
?
<
NoticeBanner
>
你当前有
{
editableInvolvedCount
}
条待填写记录。
</
NoticeBanner
>
:
null
}
{
editableInvolvedCount
>
0
?
<
NoticeBanner
>
你当前有
{
editableInvolvedCount
}
条待填写记录。
</
NoticeBanner
>
:
null
}
<
section
className=
"min-w-0"
>
<
section
className=
{
loadFailed
?
'hidden'
:
'min-w-0'
}
>
<
SectionHeader
title=
"转发记录列表"
/>
<
SectionHeader
title=
"转发记录列表"
/>
<
DataGrid
<
DataGrid
aria
-
label=
"转发审批列表"
aria
-
label=
"转发审批列表"
...
...
src/pages/project_manager_xy/ModelBatchData.jsx
View file @
f167d242
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
import
{
Button
,
Button
,
Checkbox
,
Checkbox
,
...
@@ -58,10 +59,11 @@ export default function ModelBatchData() {
...
@@ -58,10 +59,11 @@ export default function ModelBatchData() {
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
pageSize
,
setPageSize
]
=
useState
(
DEFAULT_PAGE_SIZE
);
const
[
pageSize
,
setPageSize
]
=
useState
(
DEFAULT_PAGE_SIZE
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
,
setError
]
=
useFeedbackState
(
);
const
[
notice
,
setNotice
]
=
useState
(
'
'
);
const
[
,
setNotice
]
=
useFeedbackState
(
'success
'
);
const
[
notificationWarning
,
setNotificationWarning
]
=
useState
(
'
'
);
const
[
,
setNotificationWarning
]
=
useFeedbackState
(
'warning
'
);
const
[
isHiddenDialogOpen
,
setIsHiddenDialogOpen
]
=
useState
(
false
);
const
[
isHiddenDialogOpen
,
setIsHiddenDialogOpen
]
=
useState
(
false
);
const
[
isForwardDialogOpen
,
setIsForwardDialogOpen
]
=
useState
(
false
);
const
[
isForwardDialogOpen
,
setIsForwardDialogOpen
]
=
useState
(
false
);
const
[
forwardForm
,
setForwardForm
]
=
useState
(
createEmptyForwardDraft
);
const
[
forwardForm
,
setForwardForm
]
=
useState
(
createEmptyForwardDraft
);
...
@@ -84,6 +86,7 @@ export default function ModelBatchData() {
...
@@ -84,6 +86,7 @@ export default function ModelBatchData() {
let
cancelled
=
false
;
let
cancelled
=
false
;
(
async
()
=>
{
(
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
setError
(
''
);
try
{
try
{
await
ensureModelMonitorSubjects
();
await
ensureModelMonitorSubjects
();
const
[
nextModel
,
nextBatch
]
=
await
Promise
.
all
([
loadModel
(
modelId
),
loadModelBatch
(
batchId
)]);
const
[
nextModel
,
nextBatch
]
=
await
Promise
.
all
([
loadModel
(
modelId
),
loadModelBatch
(
batchId
)]);
...
@@ -106,7 +109,7 @@ export default function ModelBatchData() {
...
@@ -106,7 +109,7 @@ export default function ModelBatchData() {
}
}
})();
})();
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[
batchId
,
modelId
]);
},
[
batchId
,
modelId
,
reloadKey
,
setError
]);
const
forwardStatusIndex
=
useMemo
(()
=>
buildForwardStatusIndex
(
forwards
),
[
forwards
]);
const
forwardStatusIndex
=
useMemo
(()
=>
buildForwardStatusIndex
(
forwards
),
[
forwards
]);
const
forwardItemByRecordId
=
useMemo
(()
=>
buildForwardItemIndex
(
forwardItems
),
[
forwardItems
]);
const
forwardItemByRecordId
=
useMemo
(()
=>
buildForwardItemIndex
(
forwardItems
),
[
forwardItems
]);
...
@@ -229,10 +232,8 @@ export default function ModelBatchData() {
...
@@ -229,10 +232,8 @@ export default function ModelBatchData() {
<
div
className=
"space-y-5"
>
<
div
className=
"space-y-5"
>
<
PageHeader
eyebrow=
{
batch
?.
batchCode
||
'模型批次'
}
title=
{
batch
?.
batchName
||
'批次数据'
}
description=
"展示当前批次已同步的全部业务字段。页面不提供字段筛选或图表分析,勾选记录后可隐藏或进入转发审批。"
/>
<
PageHeader
eyebrow=
{
batch
?.
batchCode
||
'模型批次'
}
title=
{
batch
?.
batchName
||
'批次数据'
}
description=
"展示当前批次已同步的全部业务字段。页面不提供字段筛选或图表分析,勾选记录后可隐藏或进入转发审批。"
/>
{
loading
?
<
NoticeBanner
>
正在加载批次数据和转发状态...
</
NoticeBanner
>
:
null
}
{
loading
?
<
NoticeBanner
>
正在加载批次数据和转发状态...
</
NoticeBanner
>
:
null
}
{
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
!
batch
?
<
EmptyCollection
title=
"批次数据加载失败"
description=
"请重新加载,详细错误见操作提示。"
action=
{
<
Button
variant=
"secondary"
onPress=
{
()
=>
setReloadKey
((
current
)
=>
current
+
1
)
}
>
重新加载
</
Button
>
}
/>
:
null
}
{
!
error
&&
notice
?
<
NoticeBanner
tone=
"success"
>
{
notice
}
</
NoticeBanner
>
:
null
}
{
batch
?.
syncStatus
===
'partial'
||
batch
?.
syncStatus
===
'failed'
?
<
NoticeBanner
key=
{
`${batch.id}:${batch.syncStatus}:${batch.errorMessage}`
}
tone=
"error"
>
最近一次同步未完整成功:
{
batch
.
errorMessage
||
'该批次不可重试,请联系管理员排查。'
}
</
NoticeBanner
>
:
null
}
{
notificationWarning
?
<
NoticeBanner
>
{
notificationWarning
}
</
NoticeBanner
>
:
null
}
{
batch
?.
syncStatus
===
'partial'
||
batch
?.
syncStatus
===
'failed'
?
<
NoticeBanner
tone=
"error"
>
最近一次同步未完整成功:
{
batch
.
errorMessage
||
'该批次不可重试,请联系管理员排查。'
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
batch
?
(
{
!
loading
&&
batch
?
(
<
section
className=
"min-w-0 space-y-4"
>
<
section
className=
"min-w-0 space-y-4"
>
...
...
src/pages/project_manager_xy/ModelBatches.jsx
View file @
f167d242
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
Eye
}
from
'@/components/AppIcons'
;
import
{
Eye
}
from
'@/components/AppIcons'
;
import
{
Button
,
Description
,
Label
,
ProgressBar
,
toast
}
from
'@heroui/react'
;
import
{
Button
,
Description
,
Label
,
ProgressBar
,
toast
}
from
'@heroui/react'
;
...
@@ -35,7 +36,8 @@ export default function ModelBatches() {
...
@@ -35,7 +36,8 @@ export default function ModelBatches() {
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
pageSize
,
setPageSize
]
=
useState
(
20
);
const
[
pageSize
,
setPageSize
]
=
useState
(
20
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[,
setError
]
=
useFeedbackState
();
const
[
isSyncDialogOpen
,
setIsSyncDialogOpen
]
=
useState
(
false
);
const
[
isSyncDialogOpen
,
setIsSyncDialogOpen
]
=
useState
(
false
);
const
[
sourceModel
,
setSourceModel
]
=
useState
(
null
);
const
[
sourceModel
,
setSourceModel
]
=
useState
(
null
);
const
[
sourceBatches
,
setSourceBatches
]
=
useState
([]);
const
[
sourceBatches
,
setSourceBatches
]
=
useState
([]);
...
@@ -54,6 +56,7 @@ export default function ModelBatches() {
...
@@ -54,6 +56,7 @@ export default function ModelBatches() {
let
cancelled
=
false
;
let
cancelled
=
false
;
(
async
()
=>
{
(
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
setError
(
''
);
try
{
try
{
await
ensureModelMonitorSubjects
();
await
ensureModelMonitorSubjects
();
const
nextModel
=
await
loadModel
(
modelId
);
const
nextModel
=
await
loadModel
(
modelId
);
...
@@ -69,7 +72,7 @@ export default function ModelBatches() {
...
@@ -69,7 +72,7 @@ export default function ModelBatches() {
}
}
})();
})();
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[
modelId
]);
},
[
modelId
,
reloadKey
,
setError
]);
const
filteredBatches
=
useMemo
(()
=>
{
const
filteredBatches
=
useMemo
(()
=>
{
const
search
=
keyword
.
trim
().
toLowerCase
();
const
search
=
keyword
.
trim
().
toLowerCase
();
...
@@ -137,12 +140,21 @@ export default function ModelBatches() {
...
@@ -137,12 +140,21 @@ export default function ModelBatches() {
try
{
try
{
const
result
=
await
synchronizeModelBatch
(
sourceModel
,
sourceBatch
,
userInfo
,
setProgress
);
const
result
=
await
synchronizeModelBatch
(
sourceModel
,
sourceBatch
,
userInfo
,
setProgress
);
await
refresh
(
result
.
model
);
await
refresh
(
result
.
model
);
if
(
result
.
failedCount
)
{
setError
(
`批次“
${
sourceBatch
.
batch_name
}
”同步存在异常:
${
result
.
failedCount
}
条数据写入失败。该批次不可再次同步,请查看批次数据中的错误摘要。`
);
}
else
{
toast
.
success
(
`批次“
${
sourceBatch
.
batch_name
}
”同步完成:新增
${
result
.
insertedCount
}
条。`
);
toast
.
success
(
`批次“
${
sourceBatch
.
batch_name
}
”同步完成:新增
${
result
.
insertedCount
}
条。`
);
setError
(
''
);
setError
(
''
);
}
setIsSyncDialogOpen
(
false
);
setIsSyncDialogOpen
(
false
);
}
catch
(
syncError
)
{
}
catch
(
syncError
)
{
setError
(
syncError
?.
message
||
'模型批次同步失败。'
);
const
message
=
syncError
?.
message
||
'模型批次同步失败。'
;
setError
(
message
);
try
{
await
refresh
();
await
refresh
();
}
catch
(
refreshError
)
{
setError
(
`
${
message
}
刷新批次状态失败:
${
refreshError
?.
message
||
'请稍后刷新页面确认。'
}
`);
}
} finally {
} finally {
setSyncing(false);
setSyncing(false);
}
}
...
@@ -152,7 +164,7 @@ export default function ModelBatches() {
...
@@ -152,7 +164,7 @@ export default function ModelBatches() {
<div className="space-y-5">
<div className="space-y-5">
<PageHeader eyebrow={model?.modelCode || '模型'} title={model?.modelName || '模型批次'} description="这里仅展示已经同步到本系统的批次。外部未同步批次通过右侧同步入口选择。" />
<PageHeader eyebrow={model?.modelCode || '模型'} title={model?.modelName || '模型批次'} description="这里仅展示已经同步到本系统的批次。外部未同步批次通过右侧同步入口选择。" />
{loading ? <NoticeBanner>正在加载模型批次...</NoticeBanner> : null}
{loading ? <NoticeBanner>正在加载模型批次...</NoticeBanner> : null}
{
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
!loading && !model ? <EmptyCollection title="模型批次加载失败" description="请重新加载,详细错误见操作提示。" action={<Button variant="secondary" onPress={() => setReloadKey((current) => current + 1)}>重新加载</Button>} /
> : null}
{!loading && model ? (
{!loading && model ? (
<section className="min-w-0 space-y-4">
<section className="min-w-0 space-y-4">
...
...
src/pages/project_manager_xy/ModelMonitorShared.jsx
View file @
f167d242
...
@@ -12,6 +12,7 @@ import {
...
@@ -12,6 +12,7 @@ import {
Calendar
,
Calendar
,
Checkbox
,
Checkbox
,
Chip
,
Chip
,
CloseButton
,
CloseIcon
,
CloseIcon
,
DateField
,
DateField
,
DatePicker
,
DatePicker
,
...
@@ -32,6 +33,7 @@ import { parseDateTime } from '@internationalized/date';
...
@@ -32,6 +33,7 @@ import { parseDateTime } from '@internationalized/date';
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
{
Link
}
from
'umi'
;
import
{
Link
}
from
'umi'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
import
{
APPROVAL_STATUS_OPTIONS
,
APPROVAL_STATUS_OPTIONS
,
formatDateTime
,
formatDateTime
,
...
@@ -129,8 +131,15 @@ export function WorkflowStatusTag({ status }) {
...
@@ -129,8 +131,15 @@ export function WorkflowStatusTag({ status }) {
}
}
export
function
NoticeBanner
({
tone
=
'info'
,
children
})
{
export
function
NoticeBanner
({
tone
=
'info'
,
children
})
{
const
[
isDismissed
,
setIsDismissed
]
=
useState
(
false
);
const
status
=
tone
===
'error'
?
'danger'
:
tone
===
'success'
?
'success'
:
'accent'
;
const
status
=
tone
===
'error'
?
'danger'
:
tone
===
'success'
?
'success'
:
'accent'
;
return
<
Alert
status=
{
status
}
><
Alert
.
Content
><
Alert
.
Description
>
{
children
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>;
if
(
isDismissed
)
return
null
;
return
(
<
Alert
status=
{
status
}
>
<
Alert
.
Content
className=
"min-w-0"
><
Alert
.
Description
className=
"break-words"
>
{
children
}
</
Alert
.
Description
></
Alert
.
Content
>
{
tone
===
'error'
?
<
CloseButton
aria
-
label=
"关闭错误摘要"
className=
"size-11 shrink-0"
onPress=
{
()
=>
setIsDismissed
(
true
)
}
/>
:
null
}
</
Alert
>
);
}
}
export
function
EmptyCollection
({
title
,
description
,
action
})
{
export
function
EmptyCollection
({
title
,
description
,
action
})
{
...
@@ -183,12 +192,20 @@ export function UserSelectField({ value, onChange, label, required = false, plac
...
@@ -183,12 +192,20 @@ export function UserSelectField({ value, onChange, label, required = false, plac
const
[
query
,
setQuery
]
=
useState
(
value
?.
label
||
''
);
const
[
query
,
setQuery
]
=
useState
(
value
?.
label
||
''
);
const
[
options
,
setOptions
]
=
useState
([]);
const
[
options
,
setOptions
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useFeedbackState
();
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
setLoading
(
true
);
setLoading
(
true
);
setError
(
''
);
searchUsers
(
query
)
searchUsers
(
query
)
.
then
((
result
)
=>
{
if
(
!
cancelled
)
setOptions
(
result
);
})
.
then
((
result
)
=>
{
if
(
!
cancelled
)
setOptions
(
result
);
})
.
catch
((
loadError
)
=>
{
if
(
!
cancelled
)
{
setOptions
([]);
setError
(
loadError
?.
message
||
'用户列表加载失败,请重新搜索。'
);
}
})
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[
query
]);
},
[
query
]);
...
@@ -202,6 +219,7 @@ export function UserSelectField({ value, onChange, label, required = false, plac
...
@@ -202,6 +219,7 @@ export function UserSelectField({ value, onChange, label, required = false, plac
?
[
value
,
...
options
]
?
[
value
,
...
options
]
:
options
;
:
options
;
return
<
XinyuanAutocomplete
return
<
XinyuanAutocomplete
errorText=
{
error
}
emptyText=
{
loading
?
'正在加载...'
:
'没有匹配用户'
}
emptyText=
{
loading
?
'正在加载...'
:
'没有匹配用户'
}
inputValue=
{
query
}
inputValue=
{
query
}
isLoading=
{
loading
}
isLoading=
{
loading
}
...
...
src/pages/project_manager_xy/ModelOverview.jsx
View file @
f167d242
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
import
{
AppGrid
,
AppGrid
,
Clock
,
Clock
,
...
@@ -10,7 +11,7 @@ import { ItemCardGroup } from '@heroui-pro/react/item-card-group';
...
@@ -10,7 +11,7 @@ import { ItemCardGroup } from '@heroui-pro/react/item-card-group';
import
{
KPI
}
from
'@heroui-pro/react/kpi'
;
import
{
KPI
}
from
'@heroui-pro/react/kpi'
;
import
{
ListView
}
from
'@heroui-pro/react/list-view'
;
import
{
ListView
}
from
'@heroui-pro/react/list-view'
;
import
{
NumberValue
}
from
'@heroui-pro/react/number-value'
;
import
{
NumberValue
}
from
'@heroui-pro/react/number-value'
;
import
{
Card
,
ScrollShadow
}
from
'@heroui/react'
;
import
{
Button
,
Card
,
ScrollShadow
}
from
'@heroui/react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
import
{
history
}
from
'umi'
;
import
{
import
{
...
@@ -32,12 +33,14 @@ export default function ModelOverview() {
...
@@ -32,12 +33,14 @@ export default function ModelOverview() {
const
[
batches
,
setBatches
]
=
useState
([]);
const
[
batches
,
setBatches
]
=
useState
([]);
const
[
forwards
,
setForwards
]
=
useState
([]);
const
[
forwards
,
setForwards
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useFeedbackState
();
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
(
async
()
=>
{
(
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
setError
(
''
);
try
{
try
{
await
ensureModelMonitorSubjects
();
await
ensureModelMonitorSubjects
();
const
[
modelList
,
batchList
,
forwardList
]
=
await
Promise
.
all
([
const
[
modelList
,
batchList
,
forwardList
]
=
await
Promise
.
all
([
...
@@ -58,7 +61,7 @@ export default function ModelOverview() {
...
@@ -58,7 +61,7 @@ export default function ModelOverview() {
}
}
})();
})();
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[]);
},
[
reloadKey
,
setError
]);
const
totalRecords
=
useMemo
(
const
totalRecords
=
useMemo
(
()
=>
batches
.
reduce
((
total
,
batch
)
=>
total
+
batch
.
totalRecordCount
,
0
),
()
=>
batches
.
reduce
((
total
,
batch
)
=>
total
+
batch
.
totalRecordCount
,
0
),
...
@@ -124,7 +127,7 @@ export default function ModelOverview() {
...
@@ -124,7 +127,7 @@ export default function ModelOverview() {
/>
/>
{
loading
?
<
NoticeBanner
>
正在初始化模型监控对象并加载概览...
</
NoticeBanner
>
:
null
}
{
loading
?
<
NoticeBanner
>
正在初始化模型监控对象并加载概览...
</
NoticeBanner
>
:
null
}
{
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
error
?
<
EmptyCollection
title=
"模型总览加载失败"
description=
"请重新加载,详细错误见操作提示。"
action=
{
<
Button
variant=
"secondary"
onPress=
{
()
=>
setReloadKey
((
current
)
=>
current
+
1
)
}
>
重新加载
</
Button
>
}
/
>
:
null
}
{
!
loading
&&
!
error
?
(
{
!
loading
&&
!
error
?
(
<>
<>
...
@@ -145,7 +148,7 @@ export default function ModelOverview() {
...
@@ -145,7 +148,7 @@ export default function ModelOverview() {
</
div
>
</
div
>
{
failedBatches
.
length
?
(
{
failedBatches
.
length
?
(
<
NoticeBanner
tone=
"error"
>
<
NoticeBanner
key=
{
failedBatches
.
map
((
batch
)
=>
batch
.
id
).
join
(
','
)
}
tone=
"error"
>
有
{
failedBatches
.
length
}
个批次同步异常。请进入对应模型批次查看错误摘要;一次性同步批次不允许重试。
有
{
failedBatches
.
length
}
个批次同步异常。请进入对应模型批次查看错误摘要;一次性同步批次不允许重试。
</
NoticeBanner
>
</
NoticeBanner
>
)
:
null
}
)
:
null
}
...
...
src/pages/project_manager_xy/Models.jsx
View file @
f167d242
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
Delete
,
Edit
,
Eye
,
FolderOpenIcon
,
More
,
PlusIcon
}
from
'@/components/AppIcons'
;
import
{
Delete
,
Edit
,
Eye
,
FolderOpenIcon
,
More
,
PlusIcon
}
from
'@/components/AppIcons'
;
import
{
import
{
...
@@ -144,8 +145,10 @@ export default function Models() {
...
@@ -144,8 +145,10 @@ export default function Models() {
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
pageSize
,
setPageSize
]
=
useState
(
DEFAULT_PAGE_SIZE
);
const
[
pageSize
,
setPageSize
]
=
useState
(
DEFAULT_PAGE_SIZE
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
notice
,
setNotice
]
=
useState
(
''
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[,
setError
]
=
useFeedbackState
();
const
[,
setNotice
]
=
useFeedbackState
(
'success'
);
const
[
isGroupDialogOpen
,
setIsGroupDialogOpen
]
=
useState
(
false
);
const
[
isGroupDialogOpen
,
setIsGroupDialogOpen
]
=
useState
(
false
);
const
[
isDeleteGroupDialogOpen
,
setIsDeleteGroupDialogOpen
]
=
useState
(
false
);
const
[
isDeleteGroupDialogOpen
,
setIsDeleteGroupDialogOpen
]
=
useState
(
false
);
...
@@ -164,7 +167,7 @@ export default function Models() {
...
@@ -164,7 +167,7 @@ export default function Models() {
const
[
syncLoading
,
setSyncLoading
]
=
useState
(
false
);
const
[
syncLoading
,
setSyncLoading
]
=
useState
(
false
);
const
[
syncProgress
,
setSyncProgress
]
=
useState
({
completed
:
0
,
total
:
0
});
const
[
syncProgress
,
setSyncProgress
]
=
useState
({
completed
:
0
,
total
:
0
});
const
[
syncResult
,
setSyncResult
]
=
useState
(
null
);
const
[
syncResult
,
setSyncResult
]
=
useState
(
null
);
const
[
syncDialogError
,
setSyncDialogError
]
=
useState
(
''
);
const
[
,
setSyncDialogError
]
=
useFeedbackState
(
);
async
function
refresh
()
{
async
function
refresh
()
{
const
[
modelList
,
groupList
]
=
await
Promise
.
all
([
loadModels
(),
loadModelGroups
()]);
const
[
modelList
,
groupList
]
=
await
Promise
.
all
([
loadModels
(),
loadModelGroups
()]);
...
@@ -176,17 +179,22 @@ export default function Models() {
...
@@ -176,17 +179,22 @@ export default function Models() {
let
cancelled
=
false
;
let
cancelled
=
false
;
(
async
()
=>
{
(
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
try
{
try
{
await
ensureModelMonitorSubjects
();
await
ensureModelMonitorSubjects
();
if
(
!
cancelled
)
await
refresh
();
if
(
!
cancelled
)
await
refresh
();
}
catch
(
loadError
)
{
}
catch
(
loadError
)
{
if
(
!
cancelled
)
setError
(
loadError
?.
message
||
'模型列表初始化失败。'
);
if
(
!
cancelled
)
{
setLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'模型列表初始化失败。'
);
}
}
finally
{
}
finally
{
if
(
!
cancelled
)
setLoading
(
false
);
if
(
!
cancelled
)
setLoading
(
false
);
}
}
})();
})();
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[]);
},
[
reloadKey
,
setError
]);
const
groupTree
=
useMemo
(()
=>
buildGroupTree
(
groups
),
[
groups
]);
const
groupTree
=
useMemo
(()
=>
buildGroupTree
(
groups
),
[
groups
]);
const
groupOptions
=
useMemo
(()
=>
flattenGroupOptions
(
groupTree
),
[
groupTree
]);
const
groupOptions
=
useMemo
(()
=>
flattenGroupOptions
(
groupTree
),
[
groupTree
]);
...
@@ -440,7 +448,11 @@ export default function Models() {
...
@@ -440,7 +448,11 @@ export default function Models() {
failedRecordCount
,
failedRecordCount
,
failures
,
failures
,
});
});
if
(
failures
.
length
)
{
setSyncDialogError
(
`同步结束:
${
failures
.
length
}
个批次存在异常,共新增
${
insertedCount
}
条数据。请查看弹窗中的逐项错误摘要。`
);
}
else
{
setNotice
(
`已完成
${
completedCodes
.
length
}
/
${
selectedEntries
.
length
}
个批次同步,共新增
${
insertedCount
}
条数据。`
);
setNotice
(
`已完成
${
completedCodes
.
length
}
/
${
selectedEntries
.
length
}
个批次同步,共新增
${
insertedCount
}
条数据。`
);
}
setError
(
''
);
setError
(
''
);
const
[,
localBatchList
]
=
await
Promise
.
all
([
refresh
(),
loadModelBatches
()]);
const
[,
localBatchList
]
=
await
Promise
.
all
([
refresh
(),
loadModelBatches
()]);
const
nextSyncedCodes
=
localBatchList
.
map
((
batch
)
=>
String
(
batch
.
batchCode
));
const
nextSyncedCodes
=
localBatchList
.
map
((
batch
)
=>
String
(
batch
.
batchCode
));
...
@@ -458,10 +470,9 @@ export default function Models() {
...
@@ -458,10 +470,9 @@ export default function Models() {
<
PageHeader
eyebrow=
"模型监控"
title=
"全部模型"
description=
"模型由同步自动创建。你可以用无限级分组、标签和说明整理模型,点击模型后进入独立批次页面。"
/>
<
PageHeader
eyebrow=
"模型监控"
title=
"全部模型"
description=
"模型由同步自动创建。你可以用无限级分组、标签和说明整理模型,点击模型后进入独立批次页面。"
/>
<
div
className=
"space-y-8"
>
<
div
className=
"space-y-8"
>
{
loading
?
<
NoticeBanner
>
正在初始化模型对象并加载列表...
</
NoticeBanner
>
:
null
}
{
loading
?
<
NoticeBanner
>
正在初始化模型对象并加载列表...
</
NoticeBanner
>
:
null
}
{
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
loadFailed
?
<
EmptyCollection
title=
"模型列表加载失败"
description=
"请重新加载,详细错误见操作提示。"
action=
{
<
Button
variant=
"secondary"
onPress=
{
()
=>
setReloadKey
((
current
)
=>
current
+
1
)
}
>
重新加载
</
Button
>
}
/>
:
null
}
{
!
error
&&
notice
?
<
NoticeBanner
tone=
"success"
>
{
notice
}
</
NoticeBanner
>
:
null
}
{
!
loading
?
(
{
!
loading
&&
!
loadFailed
?
(
<>
<>
<
div
className=
"grid min-w-0 gap-5 lg:grid-cols-[15rem_minmax(0,1fr)] lg:items-start"
>
<
div
className=
"grid min-w-0 gap-5 lg:grid-cols-[15rem_minmax(0,1fr)] lg:items-start"
>
<
Surface
className=
"min-w-0 rounded-3xl p-4"
variant=
"default"
>
<
Surface
className=
"min-w-0 rounded-3xl p-4"
variant=
"default"
>
...
@@ -591,7 +602,6 @@ export default function Models() {
...
@@ -591,7 +602,6 @@ export default function Models() {
<
div
className=
"space-y-5"
>
<
div
className=
"space-y-5"
>
<
Stepper
aria
-
label=
"同步步骤"
className=
"mx-auto max-w-4xl"
currentStep=
{
syncStep
-
1
}
size=
"sm"
>
{
[
'加载批次'
,
'选择批次'
,
'执行同步'
].
map
((
label
)
=>
<
Stepper
.
Step
key=
{
label
}
><
Stepper
.
Indicator
/><
Stepper
.
Content
><
Stepper
.
Title
>
{
label
}
</
Stepper
.
Title
></
Stepper
.
Content
><
Stepper
.
Separator
/></
Stepper
.
Step
>)
}
</
Stepper
>
<
Stepper
aria
-
label=
"同步步骤"
className=
"mx-auto max-w-4xl"
currentStep=
{
syncStep
-
1
}
size=
"sm"
>
{
[
'加载批次'
,
'选择批次'
,
'执行同步'
].
map
((
label
)
=>
<
Stepper
.
Step
key=
{
label
}
><
Stepper
.
Indicator
/><
Stepper
.
Content
><
Stepper
.
Title
>
{
label
}
</
Stepper
.
Title
></
Stepper
.
Content
><
Stepper
.
Separator
/></
Stepper
.
Step
>)
}
</
Stepper
>
{
syncDialogError
?
<
NoticeBanner
tone=
"error"
>
{
syncDialogError
}
</
NoticeBanner
>
:
null
}
{
syncStep
===
1
&&
syncLoading
?
<
NoticeBanner
>
正在加载全部来源模型、批次和本地同步状态...
</
NoticeBanner
>
:
null
}
{
syncStep
===
1
&&
syncLoading
?
<
NoticeBanner
>
正在加载全部来源模型、批次和本地同步状态...
</
NoticeBanner
>
:
null
}
{
syncStep
===
2
?
(
{
syncStep
===
2
?
(
...
@@ -663,7 +673,7 @@ export default function Models() {
...
@@ -663,7 +673,7 @@ export default function Models() {
<
Surface
className=
"rounded-2xl p-4"
variant=
"secondary"
>
<
Surface
className=
"rounded-2xl p-4"
variant=
"secondary"
>
<
ProgressBar
aria
-
label=
"批量同步进度"
value=
{
syncProgress
.
total
?
Math
.
round
(
syncProgress
.
completed
/
syncProgress
.
total
*
100
)
:
syncLoading
?
10
:
0
}
><
div
className=
"flex items-start justify-between gap-4 text-sm"
><
div
className=
"min-w-0"
><
Label
>
{
syncLoading
?
'正在逐批校验并写入 Record...'
:
'批量同步已结束'
}
</
Label
>
{
syncProgress
.
currentBatch
?
<
Description
className=
"mt-1 truncate"
>
{
syncProgress
.
currentBatch
}{
syncProgress
.
currentRecordsTotal
?
` · ${syncProgress.currentRecordsCompleted}/${syncProgress.currentRecordsTotal} 条`
:
''
}
</
Description
>
:
null
}
</
div
><
ProgressBar
.
Output
>
{
syncProgress
.
completed
}
/
{
syncProgress
.
total
}
</
ProgressBar
.
Output
></
div
><
ProgressBar
.
Track
><
ProgressBar
.
Fill
/></
ProgressBar
.
Track
></
ProgressBar
>
<
ProgressBar
aria
-
label=
"批量同步进度"
value=
{
syncProgress
.
total
?
Math
.
round
(
syncProgress
.
completed
/
syncProgress
.
total
*
100
)
:
syncLoading
?
10
:
0
}
><
div
className=
"flex items-start justify-between gap-4 text-sm"
><
div
className=
"min-w-0"
><
Label
>
{
syncLoading
?
'正在逐批校验并写入 Record...'
:
'批量同步已结束'
}
</
Label
>
{
syncProgress
.
currentBatch
?
<
Description
className=
"mt-1 truncate"
>
{
syncProgress
.
currentBatch
}{
syncProgress
.
currentRecordsTotal
?
` · ${syncProgress.currentRecordsCompleted}/${syncProgress.currentRecordsTotal} 条`
:
''
}
</
Description
>
:
null
}
</
div
><
ProgressBar
.
Output
>
{
syncProgress
.
completed
}
/
{
syncProgress
.
total
}
</
ProgressBar
.
Output
></
div
><
ProgressBar
.
Track
><
ProgressBar
.
Fill
/></
ProgressBar
.
Track
></
ProgressBar
>
{
syncResult
?
<
div
className=
"mt-4 grid grid-cols-2 gap-3 text-center text-sm sm:grid-cols-4"
><
div
><
div
className=
"font-medium"
>
{
syncResult
.
completedBatchCount
}
</
div
><
Description
>
完成批次
</
Description
></
div
><
div
><
div
className=
"font-medium"
>
{
syncResult
.
failedBatchCount
}
</
div
><
Description
>
异常批次
</
Description
></
div
><
div
><
div
className=
"font-medium"
>
{
syncResult
.
insertedCount
}
</
div
><
Description
>
新增数据
</
Description
></
div
><
div
><
div
className=
"font-medium"
>
{
syncResult
.
failedRecordCount
}
</
div
><
Description
>
失败数据
</
Description
></
div
></
div
>
:
null
}
{
syncResult
?
<
div
className=
"mt-4 grid grid-cols-2 gap-3 text-center text-sm sm:grid-cols-4"
><
div
><
div
className=
"font-medium"
>
{
syncResult
.
completedBatchCount
}
</
div
><
Description
>
完成批次
</
Description
></
div
><
div
><
div
className=
"font-medium"
>
{
syncResult
.
failedBatchCount
}
</
div
><
Description
>
异常批次
</
Description
></
div
><
div
><
div
className=
"font-medium"
>
{
syncResult
.
insertedCount
}
</
div
><
Description
>
新增数据
</
Description
></
div
><
div
><
div
className=
"font-medium"
>
{
syncResult
.
failedRecordCount
}
</
div
><
Description
>
失败数据
</
Description
></
div
></
div
>
:
null
}
{
syncResult
?.
failures
?.
length
?
<
div
className=
"mt-4"
><
NoticeBanner
tone=
"error"
>
{
syncResult
.
failures
.
map
((
failure
)
=>
`${failure.label}:${failure.message}`
).
join
(
';'
)
}
</
NoticeBanner
></
div
>
:
null
}
{
syncResult
?.
failures
?.
length
?
<
div
className=
"mt-4"
><
NoticeBanner
key=
{
syncResult
.
failures
.
map
((
failure
)
=>
`${failure.label}:${failure.message}`
).
join
(
';'
)
}
tone=
"error"
>
{
syncResult
.
failures
.
map
((
failure
)
=>
`${failure.label}:${failure.message}`
).
join
(
';'
)
}
</
NoticeBanner
></
div
>
:
null
}
</
Surface
>
</
Surface
>
)
:
null
}
)
:
null
}
</
div
>
</
div
>
...
...
src/pages/project_manager_xy/model-monitor.business-flow.md
View file @
f167d242
...
@@ -169,6 +169,17 @@ MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql`
...
@@ -169,6 +169,17 @@ MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql`
-
本期不提供模型或批次删除,也不限制同步和分组维护权限。
-
本期不提供模型或批次删除,也不限制同步和分组维护权限。
-
模型列表、批次页和数据页必须支持直接 URL 访问及稳定面包屑返回。
-
模型列表、批次页和数据页必须支持直接 URL 访问及稳定面包屑返回。
### 5.1 操作提示与关闭
-
登录、站点配置、模型加载、分组/模型保存、同步、隐藏/恢复、转发、填写和审批失败时,统一显示 HeroUI Toast 浮层;用户查询失败同时在人员选择器内显示加载错误,保留当前表单和选中记录。
-
提示挂载在应用根部,通过 Portal 呈现在视口顶部,层级高于业务 Modal、删除确认和移动侧栏;弹窗打开时也能看到并关闭提示,提示出现不抢走表单焦点。
-
错误和通知处理异常提示默认 8 秒后消失,成功提示默认 4 秒后消失;悬停或键盘聚焦时暂停计时。关闭按钮始终可见,移动端也可直接点击,无需先悬停。
-
再次提交发生同样错误时重新弹出提示,并替换同一来源的上一条提示。清空错误或离开对应页面时清理该来源的提示;关闭提示只影响展示,不把失败状态改为成功、不清空表单、不触发保存或再次同步。
-
模型总览、模型列表、批次、批次数据和转发审批加载失败时显示可重新加载的空状态,详细原因由浮层提示;重新加载只读取页面数据,不重新同步批次。通知中心的重试方式见对应流程文档。
-
单批次同步返回部分写入失败时显示异常提示,不显示完整成功;批次继续保留错误摘要和不可再次同步限制。
-
批量同步结束存在失败批次时,额外弹出异常汇总,逐项错误仍保留在同步弹窗的结果区;结果区、概览和批次数据的同步异常摘要均可关闭。关闭摘要不修改
`sync_status/error_message`
,一次性同步批次仍不能重试。
-
转发或审批状态已写入但站内通知/OA 处理失败时使用 warning 提示,保留“业务状态已更新”的说明,不回滚业务状态。
## 6. 文档与历史数据
## 6. 文档与历史数据
-
旧
`analysis_*`
Subject 和 Record 保留,不迁移、不删除。
-
旧
`analysis_*`
Subject 和 Record 保留,不迁移、不删除。
...
...
src/pages/project_manager_xy/notifications/NotificationListPage.jsx
View file @
f167d242
import
{
Bell
,
Close
,
OpenExternal
,
Search
}
from
'@/components/AppIcons'
;
import
{
Bell
,
Close
,
OpenExternal
,
Search
}
from
'@/components/AppIcons'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
import
{
Alert
,
Avatar
,
Avatar
,
Button
,
Button
,
Card
,
Card
,
...
@@ -200,7 +200,7 @@ export default function NotificationListPage() {
...
@@ -200,7 +200,7 @@ export default function NotificationListPage() {
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
unreadTotal
,
setUnreadTotal
]
=
useState
(
0
);
const
[
unreadTotal
,
setUnreadTotal
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
use
State
(
''
);
const
[
error
,
setError
]
=
use
FeedbackState
(
);
const
[
busyKey
,
setBusyKey
]
=
useState
(
''
);
const
[
busyKey
,
setBusyKey
]
=
useState
(
''
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
...
@@ -404,14 +404,13 @@ export default function NotificationListPage() {
...
@@ -404,14 +404,13 @@ export default function NotificationListPage() {
</
div
>
</
div
>
{
error
?
(
{
error
?
(
<
Alert
status=
"danger"
>
<
EmptyState
className=
"min-h-64"
size=
"md"
>
<
Alert
.
Content
>
<
EmptyState
.
Header
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
<
EmptyState
.
Title
>
通知列表加载失败
</
EmptyState
.
Title
>
</
Alert
.
Content
>
<
EmptyState
.
Description
>
请重新加载通知,详细错误见操作提示。
</
EmptyState
.
Description
>
<
Button
size=
"sm"
variant=
"ghost"
onPress=
{
refresh
}
>
</
EmptyState
.
Header
>
重试
<
EmptyState
.
Content
><
Button
variant=
"secondary"
onPress=
{
refresh
}
>
重试
</
Button
></
EmptyState
.
Content
>
</
Button
>
</
EmptyState
>
</
Alert
>
)
:
null
}
)
:
null
}
{
loading
&&
notifications
.
length
===
0
?
(
{
loading
&&
notifications
.
length
===
0
?
(
...
...
src/pages/project_manager_xy/notifications/notifications.business-flow.md
View file @
f167d242
...
@@ -35,5 +35,6 @@
...
@@ -35,5 +35,6 @@
-
所有请求继续使用现有登录 Token、请求拦截器和退出处理,不新增通知专用鉴权。
-
所有请求继续使用现有登录 Token、请求拦截器和退出处理,不新增通知专用鉴权。
-
接口 401/403、网络失败或动态 action 配置缺失时不得伪造成功状态或未读数量。
-
接口 401/403、网络失败或动态 action 配置缺失时不得伪造成功状态或未读数量。
-
加载或操作错误使用应用统一 HeroUI Toast 浮层,覆盖在弹窗和侧栏上方;关闭按钮始终可见,加载错误默认 8 秒自动消失,操作错误默认 4 秒自动消失,悬停/聚焦暂停计时。列表加载失败时保留“重试”空状态,关闭提示不清除失败状态,点击重试后重新请求列表。
-
通知中心不读取或修改模型监控 Subject、Record、审批字段及用户
`applications`
授权结构。
-
通知中心不读取或修改模型监控 Subject、Record、审批字段及用户
`applications`
授权结构。
-
本应用不在五项完整功能白名单中,不实现
`shortTitle`
、Subject View 权限或审计材料中心文件工作流。
-
本应用不在五项完整功能白名单中,不实现
`shortTitle`
、Subject View 权限或审计材料中心文件工作流。
src/tailwind.css
View file @
f167d242
...
@@ -31,6 +31,26 @@ html {
...
@@ -31,6 +31,26 @@ html {
color-scheme
:
light
!important
;
color-scheme
:
light
!important
;
}
}
/* Toasts must stay above dialogs and the mobile sidebar, outside page scroll containers. */
.app-feedback-region.toast-region
{
z-index
:
300
;
top
:
max
(
1rem
,
env
(
safe-area-inset-top
));
}
.app-feedback-toast.toast
{
gap
:
0.5rem
;
padding-inline-end
:
3.5rem
;
}
.app-feedback-toast
[
data-frontmost
=
"true"
]
.app-feedback-close
{
top
:
0.25rem
;
right
:
0.25rem
;
width
:
2.75rem
;
height
:
2.75rem
;
opacity
:
1
;
pointer-events
:
auto
;
}
body
{
body
{
margin
:
0
;
margin
:
0
;
font-family
:
var
(
--rc-font-sans
);
font-family
:
var
(
--rc-font-sans
);
...
...
src/wrapper/Site.jsx
View file @
f167d242
import
{
getSystemInfo
}
from
'@/services/DataService'
;
import
{
getSystemInfo
}
from
'@/services/DataService'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
import
customParseFormat
from
'dayjs/plugin/customParseFormat'
;
import
customParseFormat
from
'dayjs/plugin/customParseFormat'
;
import
localeData
from
'dayjs/plugin/localeData'
;
import
localeData
from
'dayjs/plugin/localeData'
;
...
@@ -18,7 +19,7 @@ export default function SiteWrapper() {
...
@@ -18,7 +19,7 @@ export default function SiteWrapper() {
const
[
siteInfo
,
setSiteInfo
]
=
useState
(
null
);
const
[
siteInfo
,
setSiteInfo
]
=
useState
(
null
);
const
[
metadata
,
setMetadata
]
=
useState
(
null
);
const
[
metadata
,
setMetadata
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loadError
,
setLoadError
]
=
use
State
(
''
);
const
[
loadError
,
setLoadError
]
=
use
FeedbackState
(
);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
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