Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
P
process_management_new
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
process_management_new
Commits
091d72a3
Commit
091d72a3
authored
Oct 08, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: unify operation feedback and improve process retry states
parent
473ca7aa
Show whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
296 additions
and
85 deletions
+296
-85
xinyuan-design-system.md
docs/xinyuan-design-system.md
+9
-0
manifest.json
public/manifest.json
+1
-1
app.js
src/app.js
+6
-1
AppFeedback.jsx
src/components/AppFeedback.jsx
+48
-0
AppShell.jsx
src/components/AppShell.jsx
+0
-2
ApplicationSwitcherDialog.jsx
src/components/ApplicationSwitcherDialog.jsx
+4
-10
FeedbackErrorState.jsx
src/components/FeedbackErrorState.jsx
+20
-0
ProcessUi.css
src/components/process-ui/ProcessUi.css
+4
-0
ProcessUi.jsx
src/components/process-ui/ProcessUi.jsx
+23
-6
RecordFields.jsx
src/components/record/RecordFields.jsx
+10
-3
RecordPreviewModal.jsx
src/components/record/RecordPreviewModal.jsx
+16
-5
feedback.css
src/design-system/feedback.css
+11
-0
useFeedbackState.js
src/hooks/useFeedbackState.js
+46
-0
Login.jsx
src/pages/Login.jsx
+3
-10
ProcessFlow.jsx
src/pages/app/process_manager/ProcessFlow.jsx
+4
-6
ProcessFlowGraph.jsx
src/pages/app/process_manager/ProcessFlowGraph.jsx
+10
-9
ProcessList.jsx
src/pages/app/process_manager/ProcessList.jsx
+8
-9
Header.jsx
src/pages/app/process_manager/components/header/Header.jsx
+32
-4
process-management.business-flow.md
...s/app/process_manager/process-management.business-flow.md
+28
-2
useProcessFlow.js
src/pages/app/process_manager/useProcessFlow.js
+3
-3
NotificationListPage.jsx
src/pages/notifications/NotificationListPage.jsx
+7
-13
tailwind.css
src/tailwind.css
+1
-0
Site.jsx
src/wrapper/Site.jsx
+2
-1
No files found.
docs/xinyuan-design-system.md
View file @
091d72a3
# 流程管理鑫元设计系统
## 操作反馈(2026-10-08)
-
参考问题整改提交
`513706c`
,接口反馈统一由应用根部的
`AppFeedback`
承载,登录和业务页面共用一个 HeroUI Toast Provider。
-
Toast 通过 Portal 显示在视口顶部,层级为 300,高于业务 Modal、确认框和移动侧栏;不受页面滚动影响,不抢走表单焦点。
-
成功和信息提示默认 4 秒、错误和警告默认 8 秒后消失,悬停或键盘聚焦暂停计时。关闭按钮对齐问题整改提交
`bfc5862`
:使用 HeroUI
`Button`
(
`ghost`
、
`sm`
、
`isIconOnly`
)和
`AppIcons.Close`
,通过
`slot="close"`
接入官方关闭行为,在内容右侧参与布局;前排关闭图标始终可见,后排隐藏。移除绝对定位、放大的边框按钮和正文右侧占位,Hover/焦点样式沿用 HeroUI 默认主题。
-
页面不再使用常驻横条呈现接口错误;读取失败保留 HeroUI Pro EmptyState 和重试入口,表单字段校验继续在对应控件附近呈现。
-
`useFeedbackState`
保留请求失败状态;关闭提示只影响展示,相同错误重试时仍重新显示,同一来源的新提示替换旧提示,清空状态或组件卸载时清理该来源提示。
-
流程详情显式保存和标题修改成功后显示“详情已保存”,已有记录由 Header 的成功反馈状态统一管理,避免与通用画布保存提示重复;重新编辑、再次保存、取消或切换记录时清理旧提示。新建记录由通用画布保存入口发布同一文案,避免路由跳转卸载旧 Header 时清理成功提示。
## 2026-09-28 Shell 配色调整
当前 AppShell 使用低饱和深藏蓝侧栏与中性灰白内容画布。
`theme.css`
的
`--background`
和
`--xinyuan-sidebar-*`
是这组配色的唯一来源:侧栏背景为
`oklch(32% 0.037 255)`
,页面画布为
`oklch(97.4% 0.002 255)`
;侧栏文字为近白色,悬停项为稍亮的蓝色,选中项为提亮的蓝色底与白字。品牌主红
`#DA241A`
不变。侧栏红色 Logo 直接显示,不加底色。桌面与移动侧栏共用这些 Token,导航结构、交互及数据状态不变。
...
...
public/manifest.json
View file @
091d72a3
{
"name"
:
"process_management_new"
,
"version"
:
"1.0.
1
"
,
"version"
:
"1.0.
5
"
,
"build"
:
"abc"
,
"description"
:
"merge master"
,
"date"
:
"2025-07-14 19:27:20"
,
...
...
src/app.js
View file @
091d72a3
import
'@/global.less'
;
import
'@/tailwind.css'
;
import
AppFeedback
from
'@/components/AppFeedback'
;
import
{
I18nProvider
}
from
'@heroui/react'
;
import
{
createElement
}
from
'react'
;
import
{
defineApp
}
from
'umi'
;
...
...
@@ -12,6 +13,10 @@ export async function getInitialState() {
export
default
defineApp
({
rootContainer
(
container
)
{
return
createElement
(
I18nProvider
,
{
locale
:
'zh-CN'
},
container
);
return
createElement
(
I18nProvider
,
{
locale
:
'zh-CN'
},
createElement
(
AppFeedback
,
null
,
container
),
);
},
});
src/components/AppFeedback.jsx
0 → 100644
View file @
091d72a3
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 @
091d72a3
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Toast
}
from
'@heroui/react'
;
import
{
history
}
from
'umi'
;
import
DashboardNavbar
from
'./DashboardNavbar'
;
import
DashboardSidebar
from
'./DashboardSidebar'
;
...
...
@@ -36,7 +35,6 @@ export default function AppShell({
<
div
className=
"rc-app-page"
>
{
children
}
</
div
>
</
div
>
</
Sidebar
.
Main
>
<
Toast
.
Provider
maxVisibleToasts=
{
3
}
placement=
"top"
/>
</
Sidebar
.
Provider
>
);
}
src/components/ApplicationSwitcherDialog.jsx
View file @
091d72a3
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
xinyuanLogo
from
'@/assets/xy-logo-red.png'
;
import
{
APPLICATION_BASE_PATH
,
...
...
@@ -5,12 +6,11 @@ import {
APPLICATION_NAME
,
}
from
'@/config/application'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
ExclamationTriangleIcon
,
MagnifyingGlassIcon
}
from
'@/components/AppIcons'
;
import
{
MagnifyingGlassIcon
}
from
'@/components/AppIcons'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
ItemCard
}
from
'@heroui-pro/react/item-card'
;
import
{
ItemCardGroup
}
from
'@heroui-pro/react/item-card-group'
;
import
{
Alert
,
Avatar
,
Chip
,
Modal
,
...
...
@@ -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
(
!
open
)
{
setApplicationSearch
(
''
);
setErrorMessage
(
''
);
return
;
}
...
...
@@ -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/components/FeedbackErrorState.jsx
0 → 100644
View file @
091d72a3
import
{
EmptyState
}
from
"@heroui-pro/react/empty-state"
;
import
{
Button
}
from
"@heroui/react"
;
export
default
function
FeedbackErrorState
({
title
=
"加载失败"
,
onRetry
})
{
return
(
<
EmptyState
className=
"min-h-48"
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
{
title
}
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
请稍后重试,详细原因请查看操作提示。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
<
EmptyState
.
Content
>
<
Button
size=
"sm"
variant=
"secondary"
onPress=
{
onRetry
}
>
重试
</
Button
>
</
EmptyState
.
Content
>
</
EmptyState
>
);
}
src/components/process-ui/ProcessUi.css
View file @
091d72a3
.process-page
{
color
:
var
(
--foreground
);
}
.process-table
.process-table__action-content
{
justify-content
:
flex-start
;
gap
:
4px
;
}
.process-page--group-list
{
margin-top
:
calc
(
-1
*
var
(
--rc-page-padding-block-start
));
}
...
...
src/components/process-ui/ProcessUi.jsx
View file @
091d72a3
...
...
@@ -47,9 +47,9 @@ const TOKEN = {
};
export
const
theme
=
{
useToken
:
()
=>
({
token
:
TOKEN
})
};
export
const
message
=
{
success
:
(
text
)
=>
toast
.
success
(
text
),
error
:
(
text
)
=>
toast
.
danger
(
text
),
info
:
(
text
)
=>
toast
.
info
(
text
),
success
:
(
text
)
=>
toast
.
success
(
text
,
{
timeout
:
4000
}
),
error
:
(
text
)
=>
toast
.
danger
(
text
,
{
timeout
:
8000
}
),
info
:
(
text
)
=>
toast
.
info
(
text
,
{
timeout
:
4000
}
),
};
const
handleError
=
(
error
)
=>
message
.
error
(
error
?.
message
||
"操作失败,请重试"
);
...
...
@@ -237,6 +237,7 @@ function EditableText({ children, editable, type, style }) {
type=
"text"
style=
{
style
}
onClick=
{
()
=>
{
editable
.
onStart
?.();
setValue
(
typeof
children
===
"string"
?
children
:
""
);
setEditing
(
true
);
}
}
...
...
@@ -918,6 +919,10 @@ export function ProTable({
(
value
,
key
)
=>
value
?.[
key
],
record
);
const
getColumnWidthStyle
=
(
column
)
=>
column
.
width
?
{
width
:
column
.
width
,
minWidth
:
column
.
width
,
maxWidth
:
column
.
width
}
:
undefined
;
return
(
<
Card
>
<
div
className=
"mb-4 flex flex-wrap items-center justify-between gap-3"
>
...
...
@@ -964,7 +969,7 @@ export function ProTable({
{
loading
?
(
<
Spin
/>
)
:
(
<
Table
>
<
Table
className=
"process-table"
>
<
Table
.
ScrollContainer
>
<
Table
.
Content
aria
-
label=
"流程列表"
>
<
Table
.
Header
>
...
...
@@ -972,6 +977,8 @@ export function ProTable({
<
Table
.
Column
key=
{
String
(
col
.
key
||
col
.
dataIndex
||
index
)
}
isRowHeader=
{
index
===
0
}
className=
{
col
.
key
===
"action"
?
"process-table__action-column"
:
undefined
}
style=
{
getColumnWidthStyle
(
col
)
}
>
{
col
.
title
}
</
Table
.
Column
>
...
...
@@ -984,9 +991,19 @@ export function ProTable({
id=
{
record
.
_id
||
record
.
name
||
String
(
index
)
}
>
{
cols
.
map
((
col
,
cellIndex
)
=>
(
<
Table
.
Cell
key=
{
cellIndex
}
>
<
Table
.
Cell
key=
{
cellIndex
}
className=
{
col
.
key
===
"action"
?
"process-table__action-cell"
:
undefined
}
style=
{
getColumnWidthStyle
(
col
)
}
>
{
col
.
render
?
(
<
Space
>
<
Space
className=
{
col
.
key
===
"action"
?
"process-table__action-content"
:
undefined
}
>
{
col
.
render
(
getValue
(
record
,
col
.
dataIndex
),
record
,
...
...
src/components/record/RecordFields.jsx
View file @
091d72a3
import
FeedbackErrorState
from
"@/components/FeedbackErrorState"
;
import
useFeedbackState
from
"@/hooks/useFeedbackState"
;
import
{
Button
,
Modal
,
...
...
@@ -14,10 +16,12 @@ export default function RecordFields({ subjectConfig, subject, record }) {
const
[
history
,
setHistory
]
=
useState
([]);
const
[
open
,
setOpen
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
error
,
setError
]
=
useFeedbackState
(
"error"
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
useEffect
(()
=>
{
let
active
=
true
;
setLoading
(
true
);
setError
(
""
);
queryRecords
({
subject
:
"internal_regulation"
,
filter
:
{
"metadata.origin_regulation"
:
record
.
_id
},
...
...
@@ -44,7 +48,7 @@ export default function RecordFields({ subjectConfig, subject, record }) {
return
()
=>
{
active
=
false
;
};
},
[
record
.
_id
]);
},
[
record
.
_id
,
reloadKey
,
setError
]);
const
format
=
(
field
,
value
)
=>
{
if
(
value
==
null
||
value
.
length
===
0
)
return
"-"
;
if
([
"reference"
,
"document"
,
"user"
].
includes
(
field
.
type
))
...
...
@@ -93,7 +97,10 @@ export default function RecordFields({ subjectConfig, subject, record }) {
footer=
{
null
}
>
{
error
?
(
<
p
role=
"alert"
>
{
error
}
</
p
>
<
FeedbackErrorState
title=
"历史版本加载失败"
onRetry=
{
()
=>
setReloadKey
((
value
)
=>
value
+
1
)
}
/>
)
:
loading
?
(
<
Spin
/>
)
:
(
...
...
src/components/record/RecordPreviewModal.jsx
View file @
091d72a3
import
FeedbackErrorState
from
"@/components/FeedbackErrorState"
;
import
useFeedbackState
from
"@/hooks/useFeedbackState"
;
import
{
Modal
,
Flex
,
...
...
@@ -18,28 +20,34 @@ const DOCUMENT_PADDING = 30;
const
RecordPreviewModal
=
(
props
)
=>
{
const
{
record
,
onCancel
}
=
props
;
const
[
detail
,
setDetail
]
=
useState
(
null
);
const
[
error
,
setError
]
=
useState
(
""
);
const
[
error
,
setError
]
=
useFeedbackState
(
"error"
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
{
getSubjectConfig
}
=
useAppLayout
();
const
subjectConfig
=
getSubjectConfig
(
record
?.
type
);
useEffect
(()
=>
{
let
cancelled
=
false
;
(
async
()
=>
{
setError
(
""
);
setDetail
(
null
);
try
{
if
(
record
)
{
const
res
=
await
loadRecord
(
record
.
_id
);
setDetail
(
res
);
const
subjectDetail
=
await
loadSubject
(
record
?.
type
);
if
(
cancelled
)
return
;
setDetail
(
res
);
setSubject
(
subjectDetail
);
}
}
catch
(
error
)
{
setError
(
error
.
message
||
"详情加载失败,请关闭后
重试"
);
if
(
!
cancelled
)
setError
(
error
.
message
||
"详情加载失败,请
重试"
);
}
})();
},
[
record
]);
return
()
=>
{
cancelled
=
true
;
};
},
[
record
,
reloadKey
,
setError
]);
const
documentMeta
=
useMemo
(()
=>
detail
?.
metadata
?.
document
,
[
detail
]);
const
sections
=
useMemo
(()
=>
documentMeta
?.
sections
||
[],
[
documentMeta
]);
...
...
@@ -243,7 +251,10 @@ const RecordPreviewModal = (props) => {
footer={null}
>
{error ? (
<p role="alert">{error}</p>
<FeedbackErrorState
title="关联记录加载失败"
onRetry={() => setReloadKey((value) => value + 1)}
/>
) : !detail ? (
<Flex style={{ height: 400 }} align="center" justify="center">
<Spin spinning={true} />
...
...
src/design-system/feedback.css
0 → 100644
View file @
091d72a3
/* Feedback remains above business modals and the mobile sidebar. */
.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 @
091d72a3
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
},
};
// Closing a toast 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
;
// Identical errors on successive attempts must still show fresh feedback.
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 @
091d72a3
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
xyLogoRed
from
'@/assets/xy-logo-red.png'
;
import
xyLogoWhite
from
'@/assets/xy-logo-white.png'
;
import
{
...
...
@@ -7,7 +8,7 @@ import {
import
authService
from
'@/services/authService'
;
import
{
getCaptcha
,
userLogin
}
from
'@/services/DataService'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
Alert
,
Button
,
Card
,
Input
,
Label
,
TextField
}
from
'@heroui/react'
;
import
{
Button
,
Card
,
Input
,
Label
,
TextField
}
from
'@heroui/react'
;
import
CryptoJS
from
'crypto-js'
;
import
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
history
,
useSearchParams
}
from
'umi'
;
...
...
@@ -55,7 +56,7 @@ export default function LoginPage() {
const
[
captchaData
,
setCaptchaData
]
=
useState
(
null
);
const
[
captchaLoading
,
setCaptchaLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
'
'
);
const
[
,
setError
]
=
useFeedbackState
(
'error
'
);
const
redirect
=
normalizeRedirect
(
searchParams
.
get
(
'redirect'
));
const
refreshCaptcha
=
async
()
=>
{
...
...
@@ -205,14 +206,6 @@ export default function LoginPage() {
</
div
>
</
div
>
{
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
<
Button
fullWidth
className=
"login-submit-button"
...
...
src/pages/app/process_manager/ProcessFlow.jsx
View file @
091d72a3
import
{
Button
as
RetryButton
}
from
"@/components/process-ui/ProcessUi"
;
import
FeedbackErrorState
from
"@/components/FeedbackErrorState"
;
import
useFeedbackState
from
"@/hooks/useFeedbackState"
;
import
{
loadRecord
}
from
"@/services/DataService"
;
import
{
AppstoreOutlined
}
from
"@ant-design/icons"
;
import
{
Spin
}
from
"@/components/process-ui/ProcessUi"
;
...
...
@@ -214,7 +215,7 @@ function HistoryWrapper() {
const
{
takeSnapshot
,
...
undoProps
}
=
useUndoRedo
();
const
[
record
,
setRecord
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loadError
,
setLoadError
]
=
use
State
(
"
"
);
const
[
loadError
,
setLoadError
]
=
use
FeedbackState
(
"error
"
);
const
[
editing
,
setEditing
]
=
useState
(
false
);
const
params
=
useParams
();
...
...
@@ -261,10 +262,7 @@ function HistoryWrapper() {
editing=
{
editing
}
/>
{
loadError
?
(
<
div
role=
"alert"
className=
"p-5 text-danger"
>
{
loadError
}
<
RetryButton
onClick=
{
loadFlowRecord
}
>
重试
</
RetryButton
>
</
div
>
<
FeedbackErrorState
title=
"流程画布加载失败"
onRetry=
{
loadFlowRecord
}
/>
)
:
loading
?
(
<
div
style=
{
{
height
:
400
}
}
>
<
Spin
spinning=
{
true
}
/>
...
...
src/pages/app/process_manager/ProcessFlowGraph.jsx
View file @
091d72a3
import
{
Spin
,
Button
as
RetryButton
}
from
"@/components/process-ui/ProcessUi"
;
import
{
Spin
}
from
"@/components/process-ui/ProcessUi"
;
import
FeedbackErrorState
from
"@/components/FeedbackErrorState"
;
import
useFeedbackState
from
"@/hooks/useFeedbackState"
;
import
GraphEdge
from
"@/components/RecordFlowGraph/GraphEdge.jsx"
;
import
{
createRecord
,
...
...
@@ -53,7 +55,7 @@ const ProcessFlowGraphContent = () => {
const
[
view
,
setView
]
=
useState
(
"list"
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loadError
,
setLoadError
]
=
use
State
(
"
"
);
const
[
loadError
,
setLoadError
]
=
use
FeedbackState
(
"error
"
);
const
[
processForm
]
=
Form
.
useForm
();
const
{
check
}
=
useAuthorization
();
...
...
@@ -325,13 +327,11 @@ const ProcessFlowGraphContent = () => {
onTabChange={onTabChange}
tabActiveKey={activeTab}
>
{loadError && (
<div role="alert" className="text-danger">
{loadError}
<RetryButton onClick={loadGroups}>重试</RetryButton>
</div>
)}
{loading && <Spin />}
{loadError ? (
<FeedbackErrorState title="流程面板加载失败" onRetry={loadGroups} />
) : loading ? (
<Spin />
) : (
<ProCard style={{ height: "calc(100vh - 180px)" }}>
<ReactFlowProvider>
<ReactFlow
...
...
@@ -361,6 +361,7 @@ const ProcessFlowGraphContent = () => {
</ReactFlow>
</ReactFlowProvider>
</ProCard>
)}
{
<ModalForm
key="add"
...
...
src/pages/app/process_manager/ProcessList.jsx
View file @
091d72a3
import
{
Button
as
RetryButton
}
from
"@/components/process-ui/ProcessUi"
;
import
FeedbackErrorState
from
"@/components/FeedbackErrorState"
;
import
useFeedbackState
from
"@/hooks/useFeedbackState"
;
import
GraphEdge
from
"@/components/RecordFlowGraph/GraphEdge.jsx"
;
import
{
createRecord
,
...
...
@@ -70,7 +71,7 @@ export default function ProcessList() {
const
[
view
,
setView
]
=
useState
(
"list"
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loadError
,
setLoadError
]
=
use
State
(
"
"
);
const
[
loadError
,
setLoadError
]
=
use
FeedbackState
(
"error
"
);
const
[
processForm
]
=
Form
.
useForm
();
const
{
check
}
=
useAuthorization
();
const
flowRef
=
useRef
(
null
);
...
...
@@ -392,7 +393,7 @@ export default function ProcessList() {
columns
.
push
({
title
:
"操作"
,
key
:
"action"
,
width
:
1
40
,
width
:
1
12
,
valueType
:
"option"
,
render
:
(
text
,
item
,
_
,
action
)
=>
[
<
Popconfirm
...
...
@@ -496,12 +497,9 @@ export default function ProcessList() {
],
}
}
>
{
loadError
&&
(
<
div
role=
"alert"
className=
"text-danger"
>
{
loadError
}
<
RetryButton
onClick=
{
loadGroups
}
>
重试
</
RetryButton
>
</
div
>
)
}
{
loadError
?
(
<
FeedbackErrorState
title=
"流程列表加载失败"
onRetry=
{
loadGroups
}
/>
)
:
(
<
ProTable
loading=
{
loading
}
rowKey=
"key"
...
...
@@ -541,6 +539,7 @@ export default function ProcessList() {
])
}
/>
)
}
{
<
ModalForm
key=
"add"
...
...
src/pages/app/process_manager/components/header/Header.jsx
View file @
091d72a3
import
{
useAuthorization
}
from
"@/hooks/user"
;
import
useFeedbackState
from
"@/hooks/useFeedbackState"
;
import
{
ArrowLeftOutlined
,
FileJpgOutlined
,
...
...
@@ -16,7 +17,7 @@ import {
Typography
,
}
from
"@/components/process-ui/ProcessUi"
;
import
{
toJpeg
,
toPng
}
from
"html-to-image"
;
import
React
,
{
useContext
}
from
"react"
;
import
React
,
{
useContext
,
useEffect
}
from
"react"
;
import
{
getRectOfNodes
,
getTransformForBounds
,
...
...
@@ -43,6 +44,7 @@ const Header = ({
fresh
,
})
=>
{
const
{
saveFlow
}
=
useProcessFlow
();
const
[,
setSaveSuccess
]
=
useFeedbackState
(
"success"
);
const
{
getNodes
,
setNodes
}
=
useReactFlow
();
const
{
takeSnapshot
}
=
useContext
(
ProcessFlowContext
);
const
{
x
,
y
}
=
useViewport
();
...
...
@@ -50,6 +52,10 @@ const Header = ({
const
{
token
}
=
useToken
();
const
{
admin
}
=
useAuthorization
();
useEffect
(()
=>
{
setSaveSuccess
(
""
);
},
[
record
?.
_id
,
setSaveSuccess
]);
const
exportItems
=
[
{
key
:
"jpg"
,
...
...
@@ -172,9 +178,17 @@ const Header = ({
editable={
admin && {
triggerType: "text",
onStart: () => setSaveSuccess(""),
onChange: async (value) => {
await saveFlow(record?._id, value);
setSaveSuccess("");
await saveFlow(
record?._id,
value,
Boolean(record?._id),
"详情已保存。"
);
update({ ...record, title: value });
if (record?._id) setSaveSuccess("详情已保存。");
},
autoSize: { minRows: 1, maxRows: 1 },
}
...
...
@@ -243,6 +257,7 @@ const Header = ({
<>
<Button
onClick={async () => {
setSaveSuccess("");
trigger(false);
reset();
await fresh();
...
...
@@ -253,9 +268,16 @@ const Header = ({
<Button
type="primary"
onClick={async () => {
await saveFlow(record?._id, record?.title);
setSaveSuccess("");
await saveFlow(
record?._id,
record?.title,
Boolean(record?._id),
"详情已保存。"
);
reset();
trigger(false);
if (record?._id) setSaveSuccess("详情已保存。");
}}
>
保存
...
...
@@ -263,7 +285,13 @@ const Header = ({
</>
)}
{!editing && admin && (
<Button type="primary" onClick={() => trigger(true)}>
<Button
type="primary"
onClick={() => {
setSaveSuccess("");
trigger(true);
}}
>
编辑
</Button>
)}
...
...
src/pages/app/process_manager/process-management.business-flow.md
View file @
091d72a3
...
...
@@ -46,7 +46,7 @@
2.
管理员可进入编辑状态、修改标题、保存或取消。取消重新读取服务器记录并清理撤销记录。
3.
形状、颜色、位置、大小、节点名称和备注继续写入原节点数据;保留 Shift 等比缩放、缩略图及旧快捷键。
4.
连线工具保留线型、实虚线、起终点箭头和描述。
5.
保存使用原
`JSON.stringify({nodes,edges})`
;已有记录 updateRecord,新记录 createRecord 后跳转详情。
5.
保存使用原
`JSON.stringify({nodes,edges})`
;已有记录 updateRecord,新记录 createRecord 后跳转详情。
只有写入成功才退出编辑、清理撤销记录并显示“详情已保存”成功提示;标题修改成功也显示该提示。失败显示错误并保留编辑状态及画布内容。
6.
导出保留 html-to-image 的 JPG/PNG 格式与图形边界计算。
## 关系、标签、评论及预览
...
...
@@ -71,12 +71,38 @@
## 异常与限制
### 操作提示与失败重试
-
功能入口:登录/验证码、站点配置、应用切换、流程列表、分组视图面板、流程详情画布、关联制度/法规预览、历史版本和通知中心。
-
读取、新建、修改、保存、复制、删除、标签/关系查询及通知操作的成功/失败反馈统一显示 HeroUI Toast;站点配置和应用列表回退使用 warning。应用根部挂载,视口顶部 Portal 层级高于业务弹框、确认框和移动侧栏,弹框打开或页面滚动时仍可看到并关闭提示。
-
成功/信息默认 4 秒、错误/警告默认 8 秒后消失;悬停或键盘聚焦暂停计时。当前提示右侧的关闭图标始终可见,使用 HeroUI 默认小号透明按钮,通过
`close`
slot 支持点击和键盘关闭;后排堆叠提示隐藏关闭按钮,避免误关闭。相同错误再次发生时重新显示,同一读取来源的新提示替换旧提示,清空反馈或离开对应组件时清理该来源提示。
-
关闭提示只影响展示,不把失败改为成功,不清空表单草稿、选中记录或画布;必填校验与删除确认说明继续留在控件和确认框中。弹框提交失败保持打开,Loading 结束后可修正内容再次提交。
-
流程详情的“保存”和标题修改只在
`updateRecord`
/
`createRecord`
成功后显示“详情已保存”,不叠加通用“保存成功”提示;失败不显示成功。重新编辑、再次保存、取消、切换记录或离开详情时清理旧成功提示;画布自动保存继续沿用原静默调用,不增加提示。
-
已有记录的成功提示由 Header 管理;新建记录在
`createRecord`
返回后从通用保存入口发布,成功提示可跨过跳转到新记录详情的卸载过程。接口路径、请求顺序及
`title`
/
`metadata.flow`
协议不变。
-
流程列表、面板、画布、关联预览、历史版本和通知读取失败时显示失败空状态及“重试”;详细原因由浮层显示,避免错误地展示“暂无数据”或旧的流程图。重试只读取当前资源,不重复执行创建、保存、复制、删除或通知写入操作。
-
列表/面板重试沿用当前分组
`tab`
;详情/预览重试沿用当前记录
`_id`
/
`name`
及 Subject,历史版本仍按
`metadata.origin_regulation`
查询;通知仍按分类、已读状态、关键词和页码查询。关闭提示不改动
`metadata.process_list`
、
`metadata.source`
、
`metadata.flow`
、
`metadata.group_flow`
或任何业务状态。
### 接口与业务限制
-
列表、画布读取失败显示错误并支持重试;弹窗提交失败保持打开,防止重复提交。
-
记录、Subject、标签查询复用现有 DataService 与鉴权请求层,401 沿用现有登录处理。
-
创建流程、更新分组等组合操作不是事务;保留旧顺序,失败后可能需要刷新核对已写入记录。
-
本次未修改服务端字段定义、权限配置和业务规则。
`subjectsDefinition.md`
记录旧前端消费协议;完整 schema 以服务端为准。
-
修正迁入代码中阻止运行的两处变量初始化问题:图导出尺寸提前读取;连线组件用实际 store selector 读取 nodesDraggable。
##
验证状态
##
迁移阶段验证记录
迁移目录 43 个 JS/JSX 文件已通过语法、本地导入解析及禁止旧 UI 依赖检查。使用相同模拟上下文对比新旧处理函数,9 项用例通过:新增分组、列表流程新增/修改/复制/删除、面板流程新增/修改、画布新建/更新保存;API 参数和调用顺序相同,只归一化应用路由前缀。完整构建和浏览器交互测试尚未完成:离线依赖缓存不完整,联网安装被自动审批拒绝。当前 package.json 已补充所需依赖,但 node_modules 尚未恢复完整、锁文件仍待成功安装后更新。依赖/锁文件安装成功后需验证登录、分组、流程 CRUD、图编辑/导出、关系预览、标签、评论和普通用户权限,不应将静态检查等同于上线验收。
## 2026-10-08 操作反馈验证(513706c 阶段)
-
当前依赖可用,
`npm run build`
生产构建通过;上述依赖阻塞为迁移阶段的历史记录。
-
使用本地模拟接口验证:分组弹框保存失败时提示显示在遮罩上方,手动关闭不关闭弹框且保留输入;重复失败可重新提示;保存成功提示自动消失。
-
列表读取失败时显示失败空状态,关闭提示后仍保留重试,重试不会执行写入操作;键盘 Space 可关闭提示。应用切换查询失败时警告显示在弹框上方,并展示账号上的回退应用。
-
验证 375px 手机及 812×375 横屏显示,提示关闭按钮为 44×44px、始终可见,提示层级为 300,无页面横向溢出。此验证使用模拟数据,未执行真实业务写入。
## 2026-10-08 bfc5862 同步验证
-
生产构建通过;关闭按钮已改为官方
`ghost`
/
`sm`
图标按钮,替代上一阶段的 44×44px 绝对定位按钮。前排按钮可鼠标点击及键盘 Space 关闭,后排按钮为
`visibility: hidden`
且不可点击。
-
本地模拟
`PUT /record/:name`
失败时保留详情编辑状态且不显示成功;成功后退出编辑并只显示一条“详情已保存”,重新编辑清理旧成功提示。标题修改成功使用相同提示;新建流程
`POST /record`
成功跳转后提示仍保留。
-
保存仍传
`title`
和字符串化的
`metadata.flow`
,自动保存的静默调用不变;验证未执行真实业务写入。
src/pages/app/process_manager/useProcessFlow.js
View file @
091d72a3
...
...
@@ -77,7 +77,7 @@ export const useProcessFlow = () => {
);
const
saveFlow
=
useCallback
(
async
(
id
,
title
,
hideMessage
)
=>
{
async
(
id
,
title
,
hideMessage
,
successMessage
=
"保存成功"
)
=>
{
const
flow
=
JSON
.
stringify
({
nodes
:
getNodes
(),
edges
:
getEdges
(),
...
...
@@ -90,7 +90,7 @@ export const useProcessFlow = () => {
},
});
if
(
!
hideMessage
)
{
message
.
success
(
"保存成功"
);
message
.
success
(
successMessage
);
}
}
else
{
const
{
_id
}
=
await
createRecord
({
...
...
@@ -101,7 +101,7 @@ export const useProcessFlow = () => {
},
});
if
(
!
hideMessage
)
{
message
.
success
(
"保存成功"
);
message
.
success
(
successMessage
);
}
history
.
replace
(
`/
${
params
.
subject
}
/
${
_id
}
`
);
}
...
...
src/pages/notifications/NotificationListPage.jsx
View file @
091d72a3
import
FeedbackErrorState
from
'@/components/FeedbackErrorState'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Bell
,
Close
,
MagnifyingGlassIcon
}
from
'@/components/AppIcons'
;
import
{
useUserInfo
}
from
'@/hooks/user'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
Avatar
,
Button
,
Card
,
...
...
@@ -205,7 +206,7 @@ export default function NotificationListPage() {
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
unreadTotal
,
setUnreadTotal
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
[
busyKey
,
setBusyKey
]
=
useState
(
''
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
...
...
@@ -278,12 +279,12 @@ export default function NotificationListPage() {
setBusyKey
(
key
);
try
{
await
operation
();
toast
.
success
(
successMessage
,
{
timeout
:
3
000
});
toast
.
success
(
successMessage
,
{
timeout
:
4
000
});
refresh
();
return
true
;
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
timeout
:
4
000
,
timeout
:
8
000
,
});
return
false
;
}
finally
{
...
...
@@ -333,7 +334,7 @@ export default function NotificationListPage() {
refresh
();
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
timeout
:
4
000
,
timeout
:
8
000
,
});
}
finally
{
setBusyKey
(
''
);
...
...
@@ -418,14 +419,7 @@ 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
>
<
FeedbackErrorState
title=
"通知加载失败"
onRetry=
{
refresh
}
/>
)
:
null
}
{
loading
&&
notifications
.
length
===
0
?
(
...
...
src/tailwind.css
View file @
091d72a3
...
...
@@ -4,6 +4,7 @@
@import
"./design-system/theme.css"
;
@import
"./design-system/tokens.css"
;
@import
"./design-system/shell.css"
;
@import
"./design-system/feedback.css"
;
@source
"./*.{js,jsx,ts,tsx}"
;
@source
"./components/**/*.{js,jsx,ts,tsx}"
;
...
...
src/wrapper/Site.jsx
View file @
091d72a3
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
getSystemInfo
}
from
'@/services/DataService'
;
import
{
Spinner
}
from
'@heroui/react'
;
import
dayjs
from
'dayjs'
;
...
...
@@ -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
;
...
...
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