Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
I
internal_regulation_manager_new
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
internal_regulation_manager_new
Commits
3e6db798
Commit
3e6db798
authored
Oct 08, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 统一操作反馈与失败重试并优化拓扑节点详情
parent
ba3d23a8
Show whitespace changes
Inline
Side-by-side
Showing
30 changed files
with
531 additions
and
154 deletions
+531
-154
xinyuan-design-system.md
docs/xinyuan-design-system.md
+6
-0
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
RequestFailure.jsx
src/components/RequestFailure.jsx
+18
-0
feedback.css
src/design-system/feedback.css
+11
-0
useFeedbackState.js
src/hooks/useFeedbackState.js
+38
-0
Login.jsx
src/pages/Login.jsx
+3
-10
ArchiveList.jsx
src/pages/internal_regulation_manager/ArchiveList.jsx
+19
-4
GraphNodeDetails.jsx
src/pages/internal_regulation_manager/GraphNodeDetails.jsx
+138
-0
InternalRegulationDetail.jsx
.../internal_regulation_manager/InternalRegulationDetail.jsx
+36
-6
InternalRegulationManager.jsx
...internal_regulation_manager/InternalRegulationManager.jsx
+7
-3
RegulationGraph.jsx
src/pages/internal_regulation_manager/RegulationGraph.jsx
+39
-39
RegulationList.jsx
src/pages/internal_regulation_manager/RegulationList.jsx
+13
-7
RevisionDetail.jsx
src/pages/internal_regulation_manager/RevisionDetail.jsx
+9
-5
RevisionList.jsx
src/pages/internal_regulation_manager/RevisionList.jsx
+16
-9
internal-regulation.business-flow.md
...l_regulation_manager/internal-regulation.business-flow.md
+18
-4
ApplicationSwitcherDialog.jsx
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
+4
-10
ForwardApprovals.jsx
src/pages/project_manager_xy/ForwardApprovals.jsx
+17
-7
ModelBatchData.jsx
src/pages/project_manager_xy/ModelBatchData.jsx
+12
-7
ModelBatches.jsx
src/pages/project_manager_xy/ModelBatches.jsx
+13
-7
ModelMonitorShared.jsx
src/pages/project_manager_xy/ModelMonitorShared.jsx
+11
-1
ModelOverview.jsx
src/pages/project_manager_xy/ModelOverview.jsx
+11
-5
Models.jsx
src/pages/project_manager_xy/Models.jsx
+18
-10
model-monitor.business-flow.md
src/pages/project_manager_xy/model-monitor.business-flow.md
+8
-0
NotificationListPage.jsx
...project_manager_xy/notifications/NotificationListPage.jsx
+7
-15
notifications.business-flow.md
...t_manager_xy/notifications/notifications.business-flow.md
+6
-0
tailwind.css
src/tailwind.css
+1
-0
Site.jsx
src/wrapper/Site.jsx
+2
-1
No files found.
docs/xinyuan-design-system.md
View file @
3e6db798
...
@@ -53,3 +53,9 @@ AppShell 采用低饱和深藏蓝侧栏与中性灰白内容画布。`theme.css`
...
@@ -53,3 +53,9 @@ AppShell 采用低饱和深藏蓝侧栏与中性灰白内容画布。`theme.css`
-
Reference 写入仍只保存 ID 或现有页面协议对象;枚举来自
`settings.options`
。
-
Reference 写入仍只保存 ID 或现有页面协议对象;枚举来自
`settings.options`
。
-
DatePicker 保持既有 service 转 ISO 流程;动态 Subject 继续使用现有
`loadSubject`
/subject definition。
-
DatePicker 保持既有 service 转 ISO 流程;动态 Subject 继续使用现有
`loadSubject`
/subject definition。
-
UI 迁移不得改变 API、路由、权限、校验、保存回调、请求时机、分页口径或审批状态。
-
UI 迁移不得改变 API、路由、权限、校验、保存回调、请求时机、分页口径或审批状态。
## 操作反馈浮层
-
全应用统一由根部
`AppFeedback`
挂载 HeroUI
`Toast.Provider`
和官方 compound 组件,登录和 AppShell 共用一个 Provider;接口成功/失败/降级警告不再渲染页面横条。
-
成功默认 4 秒、错误和警告默认 8 秒后消失;关闭只改变提示展示。详细加载错误通过 Toast 展示,失败空状态使用 HeroUI Pro EmptyState,并提供读取重试。
-
操作提示沿用问题整改
`bfc5862`
:关闭按钮直接组合 HeroUI
`Button`
(
`ghost`
、
`sm`
、
`isIconOnly`
),通过官方
`slot="close"`
接入关闭行为,使用 16px
`AppIcons.Close`
,位于正文右侧并参与正常布局。当前提示关闭图标始终可见,后排堆叠提示隐藏关闭按钮,避免误关闭;移除旧关闭按钮的边框、绝对定位和正文右侧占位。
`src/design-system/feedback.css`
只保留层级 300、安全区顶部和后排关闭按钮可见性约定;颜色、圆角、阴影、Hover 与焦点样式使用 HeroUI 默认主题。
public/manifest.json
View file @
3e6db798
{
{
"name"
:
"internal_regulation_manager_new"
,
"name"
:
"internal_regulation_manager_new"
,
"version"
:
"1.0.
2
"
,
"version"
:
"1.0.
4
"
,
"build"
:
"abc"
,
"build"
:
"abc"
,
"description"
:
"内部制度管理"
,
"description"
:
"内部制度管理"
,
"date"
:
"2025-07-14 19:27:20"
,
"date"
:
"2025-07-14 19:27:20"
,
...
...
src/app.js
View file @
3e6db798
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 @
3e6db798
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 @
3e6db798
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/components/RequestFailure.jsx
0 → 100644
View file @
3e6db798
import
{
Button
}
from
'@heroui/react'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
export
default
function
RequestFailure
({
title
=
'加载失败'
,
onRetry
})
{
return
(
<
EmptyState
className=
"min-h-48"
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
{
title
}
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
详细错误见操作提示,请重新加载。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
{
onRetry
?
(
<
EmptyState
.
Content
>
<
Button
variant=
"secondary"
onPress=
{
onRetry
}
>
重新加载
</
Button
>
</
EmptyState
.
Content
>
)
:
null
}
</
EmptyState
>
);
}
src/design-system/feedback.css
0 → 100644
View file @
3e6db798
/* 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 @
3e6db798
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 @
3e6db798
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
xinyuanLogo
from
'@/assets/xy-logo-red.png'
;
import
xinyuanLogo
from
'@/assets/xy-logo-red.png'
;
import
xinyuanLogoWhite
from
'@/assets/xy-logo-white.png'
;
import
xinyuanLogoWhite
from
'@/assets/xy-logo-white.png'
;
import
{
APP_DISPLAY_NAME
}
from
'@/constants/app'
;
import
{
APP_DISPLAY_NAME
}
from
'@/constants/app'
;
import
authService
from
'@/services/authService'
;
import
authService
from
'@/services/authService'
;
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
{
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'
;
...
@@ -26,7 +27,7 @@ export default function LoginPage() {
...
@@ -26,7 +27,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
(
'error
'
);
const
redirect
=
searchParams
.
get
(
'redirect'
)
||
'/'
;
const
redirect
=
searchParams
.
get
(
'redirect'
)
||
'/'
;
const
refreshCaptcha
=
async
()
=>
{
const
refreshCaptcha
=
async
()
=>
{
...
@@ -171,14 +172,6 @@ export default function LoginPage() {
...
@@ -171,14 +172,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/internal_regulation_manager/ArchiveList.jsx
View file @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
,
Input
,
Label
,
Spinner
,
TextField
}
from
'@heroui/react'
;
import
{
Button
,
Input
,
Label
,
Spinner
,
TextField
}
from
'@heroui/react'
;
import
{
Download
,
Search
}
from
'@/components/AppIcons'
;
import
{
Download
,
Search
}
from
'@/components/AppIcons'
;
import
ListPagination
from
'@/components/ListPagination'
;
import
ListPagination
from
'@/components/ListPagination'
;
...
@@ -18,16 +20,29 @@ export default function ArchiveList() {
...
@@ -18,16 +20,29 @@ export default function ArchiveList() {
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
totalPages
,
setTotalPages
]
=
useState
(
1
);
const
[
totalPages
,
setTotalPages
]
=
useState
(
1
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[,
setOptionsError
]
=
useFeedbackState
(
'warning'
);
const
refresh
=
async
()
=>
{
const
refresh
=
async
()
=>
{
try
{
try
{
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
const
result
=
await
loadArchivedRegulations
({
archive
,
keyword
,
page
,
pageSize
});
const
result
=
await
loadArchivedRegulations
({
archive
,
keyword
,
page
,
pageSize
});
setRecords
(
result
.
records
);
setTotal
(
result
.
total
);
setTotalPages
(
result
.
totalPages
);
setError
(
''
);
setRecords
(
result
.
records
);
setTotal
(
result
.
total
);
setTotalPages
(
result
.
totalPages
);
setError
(
''
);
}
catch
(
loadError
)
{
setError
(
loadError
?.
message
||
'归档历史加载失败。'
);
}
finally
{
setLoading
(
false
);
}
}
catch
(
loadError
)
{
set
LoadFailed
(
true
);
set
Error
(
loadError
?.
message
||
'归档历史加载失败。'
);
}
finally
{
setLoading
(
false
);
}
};
};
useEffect
(()
=>
{
loadArchiveOptions
().
then
((
values
)
=>
setArchiveOptions
(
values
.
map
((
value
)
=>
({
value
,
label
:
value
})))).
catch
(()
=>
setArchiveOptions
([]));
},
[]);
const
refreshOptions
=
async
()
=>
{
try
{
const
values
=
await
loadArchiveOptions
();
setArchiveOptions
(
values
.
map
((
value
)
=>
({
value
,
label
:
value
})));
setOptionsError
(
''
);
}
catch
(
requestError
)
{
setOptionsError
(
requestError
?.
message
||
'归档批次列表加载失败。'
);
}
};
useEffect
(()
=>
{
refreshOptions
();
},
[]);
const
changePageSize
=
(
nextPageSize
)
=>
{
setPageSize
(
nextPageSize
);
setPage
(
0
);
};
const
changePageSize
=
(
nextPageSize
)
=>
{
setPageSize
(
nextPageSize
);
setPage
(
0
);
};
useEffect
(()
=>
{
refresh
();
},
[
archive
,
keyword
,
page
,
pageSize
]);
useEffect
(()
=>
{
refresh
();
},
[
archive
,
keyword
,
page
,
pageSize
]);
...
@@ -53,5 +68,5 @@ export default function ArchiveList() {
...
@@ -53,5 +68,5 @@ export default function ArchiveList() {
}
catch
(
exportError
)
{
setError
(
exportError
?.
message
||
'导出失败。'
);
}
}
catch
(
exportError
)
{
setError
(
exportError
?.
message
||
'导出失败。'
);
}
};
};
return
<
div
className=
"space-y-4"
><
div
className=
"flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between"
><
div
className=
"flex flex-col gap-3 sm:flex-row sm:items-end"
><
XinyuanAutocomplete
className=
"w-full sm:w-56"
label=
"归档批次"
options=
{
[{
value
:
''
,
label
:
'全部归档'
},
...
archiveOptions
]
}
selectedKey=
{
archive
}
onSelectionChange=
{
(
value
)
=>
{
setArchive
(
String
(
value
||
''
));
setPage
(
0
);
}
}
/><
div
className=
"flex w-full items-end gap-2 sm:w-72"
><
TextField
className=
"min-w-0 flex-1"
fullWidth
value=
{
searchText
}
onChange=
{
setSearchText
}
onKeyDown=
{
(
event
)
=>
{
if
(
event
.
key
===
'Enter'
)
{
event
.
preventDefault
();
submitSearch
();
}
}
}
><
Label
className=
"sr-only"
>
搜索归档制度
</
Label
><
div
className=
"relative"
><
Search
className=
"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted"
size=
{
16
}
/><
Input
className=
"pl-9"
fullWidth
placeholder=
"搜索制度名称"
variant=
"secondary"
/></
div
></
TextField
><
Button
isDisabled=
{
loading
}
variant=
"secondary"
onPress=
{
submitSearch
}
>
查询
</
Button
></
div
></
div
><
div
className=
"flex items-center gap-3"
><
span
className=
"text-sm text-muted"
>
归档记录只读展示,不会修改现行制度
</
span
><
Button
variant=
"tertiary"
onPress=
{
exportList
}
><
Download
size=
{
16
}
/>
批量下载
</
Button
></
div
></
div
>
{
error
?
<
div
className=
"rounded-xl border border-danger/30 bg-danger/5 p-3 text-sm text-danger"
>
{
error
}
</
div
>
:
null
}{
loading
?
<
div
className=
"flex min-h-48 items-center justify-center"
><
Spinner
label=
"正在加载归档记录..."
/></
div
>
:
records
.
length
===
0
?
<
div
className=
"rounded-2xl border border-dashed border-separator p-10 text-center text-sm text-muted"
>
暂无归档记录
</
div
>
:
<
div
className=
"overflow-x-auto rounded-2xl border border-separator"
><
table
className=
"w-full min-w-[900px] text-left text-sm"
><
thead
className=
"bg-surface-secondary text-muted"
><
tr
>
{
[
'制度名称'
,
'时效性'
,
'效力位阶'
,
'审批主体'
,
'制定部门'
,
'发文日期'
,
'归档批次'
,
'操作'
].
map
((
title
)
=>
<
th
className=
"px-4 py-3 font-medium"
key=
{
title
}
>
{
title
}
</
th
>)
}
</
tr
></
thead
><
tbody
className=
"divide-y divide-separator"
>
{
records
.
map
((
record
)
=>
{
const
metadata
=
record
.
metadata
||
{};
return
<
tr
key=
{
record
.
_id
||
record
.
name
}
><
td
className=
"px-4 py-3"
><
button
className=
"font-medium text-accent hover:underline"
onClick=
{
()
=>
history
.
push
(
`/internal_regulation/${metadata.origin_regulation_id || record.name}`
)
}
>
{
metadata
.
regulation_name
||
record
.
title
||
'-'
}
</
button
></
td
><
td
className=
"px-4 py-3"
>
{
metadata
.
effective_status
||
'-'
}
</
td
><
td
className=
"px-4 py-3"
>
{
metadata
.
level
||
'-'
}
</
td
><
td
className=
"max-w-48 truncate px-4 py-3"
>
{
Array
.
isArray
(
metadata
.
approve
)
?
metadata
.
approve
.
join
(
'、'
)
:
metadata
.
approve
||
'-'
}
</
td
><
td
className=
"max-w-48 truncate px-4 py-3"
>
{
Array
.
isArray
(
metadata
.
departments
)
?
metadata
.
departments
.
join
(
'、'
)
:
metadata
.
departments
||
'-'
}
</
td
><
td
className=
"px-4 py-3"
>
{
metadata
.
public_date
?
new
Date
(
metadata
.
public_date
).
toLocaleDateString
(
'zh-CN'
)
:
'-'
}
</
td
><
td
className=
"px-4 py-3"
>
{
metadata
.
archive
||
'-'
}
</
td
><
td
className=
"px-4 py-3"
><
Button
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
history
.
push
(
`/internal_regulation/${metadata.origin_regulation_id || record.name}`
)
}
>
查看详情
</
Button
></
td
></
tr
>;
})
}
</
tbody
></
table
></
div
>
}
<
ListPagination
isDisabled=
{
loading
}
page=
{
page
}
pageSize=
{
pageSize
}
total=
{
total
}
totalPages=
{
totalPages
}
onPageChange=
{
setPage
}
onPageSizeChange=
{
changePageSize
}
></
ListPagination
></
div
>;
return
<
div
className=
"space-y-4"
><
div
className=
"flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between"
><
div
className=
"flex flex-col gap-3 sm:flex-row sm:items-end"
><
XinyuanAutocomplete
className=
"w-full sm:w-56"
label=
"归档批次"
options=
{
[{
value
:
''
,
label
:
'全部归档'
},
...
archiveOptions
]
}
selectedKey=
{
archive
}
onSelectionChange=
{
(
value
)
=>
{
setArchive
(
String
(
value
||
''
));
setPage
(
0
);
}
}
/><
div
className=
"flex w-full items-end gap-2 sm:w-72"
><
TextField
className=
"min-w-0 flex-1"
fullWidth
value=
{
searchText
}
onChange=
{
setSearchText
}
onKeyDown=
{
(
event
)
=>
{
if
(
event
.
key
===
'Enter'
)
{
event
.
preventDefault
();
submitSearch
();
}
}
}
><
Label
className=
"sr-only"
>
搜索归档制度
</
Label
><
div
className=
"relative"
><
Search
className=
"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted"
size=
{
16
}
/><
Input
className=
"pl-9"
fullWidth
placeholder=
"搜索制度名称"
variant=
"secondary"
/></
div
></
TextField
><
Button
isDisabled=
{
loading
}
variant=
"secondary"
onPress=
{
submitSearch
}
>
查询
</
Button
></
div
></
div
><
div
className=
"flex items-center gap-3"
><
span
className=
"text-sm text-muted"
>
归档记录只读展示,不会修改现行制度
</
span
><
Button
variant=
"tertiary"
onPress=
{
exportList
}
><
Download
size=
{
16
}
/>
批量下载
</
Button
></
div
></
div
>
{
loading
?
<
div
className=
"flex min-h-48 items-center justify-center"
><
Spinner
label=
"正在加载归档记录..."
/></
div
>
:
loadFailed
?
<
RequestFailure
title=
"归档历史加载失败"
onRetry=
{
refresh
}
/>
:
records
.
length
===
0
?
<
div
className=
"rounded-2xl border border-dashed border-separator p-10 text-center text-sm text-muted"
>
暂无归档记录
</
div
>
:
<
div
className=
"overflow-x-auto rounded-2xl border border-separator"
><
table
className=
"w-full min-w-[900px] text-left text-sm"
><
thead
className=
"bg-surface-secondary text-muted"
><
tr
>
{
[
'制度名称'
,
'时效性'
,
'效力位阶'
,
'审批主体'
,
'制定部门'
,
'发文日期'
,
'归档批次'
,
'操作'
].
map
((
title
)
=>
<
th
className=
"px-4 py-3 font-medium"
key=
{
title
}
>
{
title
}
</
th
>)
}
</
tr
></
thead
><
tbody
className=
"divide-y divide-separator"
>
{
records
.
map
((
record
)
=>
{
const
metadata
=
record
.
metadata
||
{};
return
<
tr
key=
{
record
.
_id
||
record
.
name
}
><
td
className=
"px-4 py-3"
><
button
className=
"font-medium text-accent hover:underline"
onClick=
{
()
=>
history
.
push
(
`/internal_regulation/${metadata.origin_regulation_id || record.name}`
)
}
>
{
metadata
.
regulation_name
||
record
.
title
||
'-'
}
</
button
></
td
><
td
className=
"px-4 py-3"
>
{
metadata
.
effective_status
||
'-'
}
</
td
><
td
className=
"px-4 py-3"
>
{
metadata
.
level
||
'-'
}
</
td
><
td
className=
"max-w-48 truncate px-4 py-3"
>
{
Array
.
isArray
(
metadata
.
approve
)
?
metadata
.
approve
.
join
(
'、'
)
:
metadata
.
approve
||
'-'
}
</
td
><
td
className=
"max-w-48 truncate px-4 py-3"
>
{
Array
.
isArray
(
metadata
.
departments
)
?
metadata
.
departments
.
join
(
'、'
)
:
metadata
.
departments
||
'-'
}
</
td
><
td
className=
"px-4 py-3"
>
{
metadata
.
public_date
?
new
Date
(
metadata
.
public_date
).
toLocaleDateString
(
'zh-CN'
)
:
'-'
}
</
td
><
td
className=
"px-4 py-3"
>
{
metadata
.
archive
||
'-'
}
</
td
><
td
className=
"px-4 py-3"
><
Button
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
history
.
push
(
`/internal_regulation/${metadata.origin_regulation_id || record.name}`
)
}
>
查看详情
</
Button
></
td
></
tr
>;
})
}
</
tbody
></
table
></
div
>
}
<
ListPagination
isDisabled=
{
loading
||
loadFailed
}
page=
{
page
}
pageSize=
{
pageSize
}
total=
{
total
}
totalPages=
{
totalPages
}
onPageChange=
{
setPage
}
onPageSizeChange=
{
changePageSize
}
></
ListPagination
></
div
>;
}
}
src/pages/internal_regulation_manager/GraphNodeDetails.jsx
0 → 100644
View file @
3e6db798
import
{
Button
,
Chip
,
Input
,
Label
,
Tabs
,
TextArea
,
TextField
}
from
'@heroui/react'
;
import
{
ChevronDown
,
ChevronRight
,
Close
,
Delete
,
OpenExternal
,
PlusIcon
,
Reload
}
from
'@/components/AppIcons'
;
function
NodePath
({
items
,
emptyText
})
{
if
(
!
items
.
length
)
return
<
p
className=
"py-3 text-sm text-muted"
>
{
emptyText
}
</
p
>;
return
(
<
ol
className=
"space-y-2"
>
{
items
.
map
((
item
,
index
)
=>
(
<
li
className=
"flex items-start gap-2 text-sm leading-6"
key=
{
item
.
id
}
>
<
span
className=
"grid size-6 shrink-0 place-items-center rounded-md bg-surface-secondary text-xs text-muted"
>
{
index
+
1
}
</
span
>
<
span
className=
"min-w-0 break-words"
>
{
item
.
title
}
</
span
>
</
li
>
))
}
</
ol
>
);
}
export
default
function
GraphNodeDetails
({
node
,
typeLabel
,
status
,
isCollapsed
,
isArchived
,
isSaving
,
hasDescendants
,
onClearSelection
,
onFieldChange
,
onToggleCollapsed
,
onAddNode
,
onDeleteNode
,
onRestoreNode
,
onOpenRegulation
,
})
{
const
ancestors
=
node
.
ancestors
||
[];
const
historyRegulations
=
node
.
historyRegulation
||
[];
const
relatedId
=
node
.
relatedRegulation
||
node
.
relatedReviseRegulation
;
const
isMutationDisabled
=
isArchived
||
isSaving
;
return
(
<
div
className=
"flex min-h-0 flex-1 flex-col bg-surface"
>
<
header
className=
"flex shrink-0 items-start gap-3 border-b border-separator p-4"
>
<
div
className=
"min-w-0 flex-1"
>
<
div
className=
"mb-2 flex flex-wrap items-center gap-2"
>
<
span
className=
"text-xs text-muted"
>
{
typeLabel
}
</
span
>
<
Chip
aria
-
label=
{
`节点状态:${status.label}`
}
color=
{
status
.
color
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
status
.
label
}
</
Chip
.
Label
></
Chip
>
{
node
.
removed
?
<
Chip
color=
"danger"
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
已删除
</
Chip
.
Label
></
Chip
>
:
null
}
</
div
>
<
h2
className=
"break-words text-base font-semibold leading-6"
>
{
node
.
title
}
</
h2
>
</
div
>
<
Button
isIconOnly
aria
-
label=
"清除选择"
className=
"shrink-0 rounded-lg"
size=
"sm"
variant=
"ghost"
onPress=
{
onClearSelection
}
><
Close
size=
{
16
}
/></
Button
>
</
header
>
<
div
className=
"min-h-0 flex-1 overflow-y-auto overscroll-contain p-4"
aria
-
label=
"节点详情内容"
>
<
div
className=
"space-y-5"
>
<
section
aria
-
label=
"基本信息"
className=
"space-y-3"
>
<
h3
className=
"text-sm font-semibold"
>
基本信息
</
h3
>
<
TextField
fullWidth
isReadOnly=
{
isArchived
}
isDisabled=
{
isSaving
}
value=
{
node
.
title
}
onChange=
{
(
value
)
=>
onFieldChange
(
'title'
,
value
)
}
>
<
Label
className=
"text-sm text-muted"
>
标题
</
Label
>
<
Input
className=
"min-h-10 rounded-lg"
fullWidth
variant=
"secondary"
/>
</
TextField
>
<
TextField
fullWidth
isReadOnly=
{
isArchived
}
isDisabled=
{
isSaving
}
value=
{
node
.
content
||
''
}
onChange=
{
(
value
)
=>
onFieldChange
(
'content'
,
value
)
}
>
<
Label
className=
"text-sm text-muted"
>
备注
</
Label
>
<
TextArea
className=
"min-h-20 max-h-48 resize-y rounded-lg"
fullWidth
placeholder=
"补充节点说明"
rows=
{
3
}
variant=
"secondary"
/>
</
TextField
>
</
section
>
<
section
aria
-
label=
"节点操作"
className=
"space-y-3 border-t border-separator pt-4"
>
<
h3
className=
"text-sm font-semibold"
>
节点操作
</
h3
>
<
div
className=
"grid grid-cols-2 gap-2"
>
<
Button
className=
"min-h-10 min-w-0 gap-1.5 rounded-lg px-2 text-sm"
fullWidth
variant=
"tertiary"
onPress=
{
onToggleCollapsed
}
>
{
isCollapsed
?
<
ChevronRight
size=
{
15
}
/>
:
<
ChevronDown
size=
{
15
}
/>
}{
isCollapsed
?
'展开子节点'
:
'折叠子节点'
}
</
Button
>
<
Button
className=
"min-h-10 min-w-0 gap-1.5 rounded-lg px-2 text-sm"
fullWidth
isDisabled=
{
isMutationDisabled
||
node
.
removed
}
variant=
"tertiary"
onPress=
{
onAddNode
}
>
<
PlusIcon
size=
{
15
}
/>
添加流程节点
</
Button
>
</
div
>
{
relatedId
?
(
<
Button
className=
"min-h-10 justify-between rounded-lg px-3 text-sm"
fullWidth
variant=
"outline"
onPress=
{
()
=>
onOpenRegulation
(
relatedId
)
}
>
查看关联制度
<
OpenExternal
size=
{
15
}
/>
</
Button
>
)
:
null
}
</
section
>
<
section
aria
-
label=
"关联信息"
className=
"space-y-3 border-t border-separator pt-4"
>
<
h3
className=
"text-sm font-semibold"
>
关联信息
</
h3
>
<
Tabs
className=
"min-w-0 gap-3"
defaultSelectedKey=
"process"
variant=
"secondary"
>
<
Tabs
.
ListContainer
className=
"w-full min-w-0"
>
<
Tabs
.
List
aria
-
label=
"节点关联信息"
style=
{
{
display
:
'grid'
,
gridTemplateColumns
:
'repeat(3, minmax(0, 1fr))'
,
width
:
'100%'
}
}
>
<
Tabs
.
Tab
className=
"min-w-0 px-1"
id=
"process"
>
流程路径
<
Tabs
.
Indicator
/></
Tabs
.
Tab
>
<
Tabs
.
Tab
className=
"min-w-0 px-1"
id=
"regulation"
>
制度路径
<
Tabs
.
Indicator
/></
Tabs
.
Tab
>
<
Tabs
.
Tab
className=
"min-w-0 px-1"
id=
"history"
>
历史版本
<
Tabs
.
Indicator
/></
Tabs
.
Tab
>
</
Tabs
.
List
>
</
Tabs
.
ListContainer
>
<
Tabs
.
Panel
style=
{
{
marginTop
:
0
,
padding
:
0
}
}
id=
"process"
>
<
NodePath
items=
{
ancestors
.
filter
((
item
)
=>
item
.
type
===
'groupNode'
)
}
emptyText=
"暂无流程路径"
/>
</
Tabs
.
Panel
>
<
Tabs
.
Panel
style=
{
{
marginTop
:
0
,
padding
:
0
}
}
id=
"regulation"
>
<
NodePath
items=
{
ancestors
.
filter
((
item
)
=>
item
.
type
===
'regulationNode'
)
}
emptyText=
"暂无制度路径"
/>
</
Tabs
.
Panel
>
<
Tabs
.
Panel
style=
{
{
marginTop
:
0
,
padding
:
0
}
}
id=
"history"
>
<
div
className=
"space-y-1"
>
{
historyRegulations
.
map
((
item
)
=>
(
<
Button
className=
"h-auto min-h-10 justify-start whitespace-normal rounded-lg px-2 py-2 text-left text-sm text-accent"
fullWidth
key=
{
item
.
_id
||
item
.
id
||
item
.
name
}
variant=
"ghost"
onPress=
{
()
=>
onOpenRegulation
(
item
.
_id
||
item
.
name
)
}
>
{
item
.
metadata
?.
regulation_name
||
item
.
title
}
</
Button
>
))
}
{
!
historyRegulations
.
length
?
<
p
className=
"py-3 text-sm text-muted"
>
暂无历史版本
</
p
>
:
null
}
</
div
>
</
Tabs
.
Panel
>
</
Tabs
>
</
section
>
</
div
>
</
div
>
<
footer
className=
"shrink-0 space-y-2 border-t border-separator bg-background p-4"
>
{
isArchived
?
<
p
className=
"text-xs text-muted"
>
归档版本仅供查看,无法保存修改。
</
p
>
:
null
}
{
node
.
removed
?
(
<>
<
Button
className=
"min-h-10 rounded-lg"
fullWidth
isDisabled=
{
isMutationDisabled
}
variant=
"secondary"
onPress=
{
onRestoreNode
}
>
<
Reload
size=
{
16
}
/>
{
hasDescendants
?
'恢复节点及子节点'
:
'恢复节点'
}
</
Button
>
<
p
className=
"text-xs leading-5 text-muted"
>
恢复后点击顶部“保存”使修改生效。
</
p
>
</>
)
:
(
<>
<
Button
aria
-
keyshortcuts=
"Delete Backspace"
className=
"min-h-10 rounded-lg"
fullWidth
isDisabled=
{
isMutationDisabled
}
variant=
"danger-soft"
onPress=
{
onDeleteNode
}
>
<
Delete
size=
{
16
}
/>
删除节点
</
Button
>
<
p
className=
"text-xs leading-5 text-muted"
>
{
hasDescendants
?
'同时移除所有子节点,可在“删除节点”列表恢复。'
:
'快捷键 Delete(Mac ⌫),可在“删除节点”列表恢复。'
}
</
p
>
</>
)
}
</
footer
>
</
div
>
);
}
src/pages/internal_regulation_manager/InternalRegulationDetail.jsx
View file @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
,
Card
,
Chip
,
Spinner
,
TextArea
,
TextField
,
Label
,
Input
}
from
'@heroui/react'
;
import
{
Button
,
Card
,
Chip
,
Spinner
,
TextArea
,
TextField
,
Label
,
Input
}
from
'@heroui/react'
;
import
{
Back
,
Edit
,
FileText
,
PaperClipIcon
}
from
'@/components/AppIcons'
;
import
{
Back
,
Edit
,
FileText
,
PaperClipIcon
}
from
'@/components/AppIcons'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
...
@@ -15,21 +17,49 @@ export default function InternalRegulationDetail() {
...
@@ -15,21 +17,49 @@ export default function InternalRegulationDetail() {
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
options
,
setOptions
]
=
useState
({});
const
[
options
,
setOptions
]
=
useState
({});
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[,
setSaveSuccess
]
=
useFeedbackState
(
'success'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
editOpen
,
setEditOpen
]
=
useState
(
false
);
const
[
editOpen
,
setEditOpen
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
form
,
setForm
]
=
useState
({});
const
[
form
,
setForm
]
=
useState
({});
const
[
historyRecords
,
setHistoryRecords
]
=
useState
([]);
const
[
historyRecords
,
setHistoryRecords
]
=
useState
([]);
const
load
=
async
()
=>
{
try
{
setLoading
(
true
);
const
[
loaded
,
loadedSubject
,
loadedOptions
]
=
await
Promise
.
all
([
loadRecord
(
name
),
loadRegulationSubject
(),
loadSubjectOptions
(
'internal_regulation'
)]);
const
histories
=
await
loadRegulationHistory
(
loaded
);
setRecord
(
loaded
);
setHistoryRecords
(
histories
);
setSubject
(
loadedSubject
);
setOptions
(
loadedOptions
.
options
);
setForm
(
loaded
?.
metadata
||
{});
setError
(
''
);
}
catch
(
loadError
)
{
setError
(
loadError
?.
message
||
'制度详情加载失败。'
);
}
finally
{
setLoading
(
false
);
}
};
const
load
=
async
()
=>
{
try
{
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
const
[
loaded
,
loadedSubject
,
loadedOptions
]
=
await
Promise
.
all
([
loadRecord
(
name
),
loadRegulationSubject
(),
loadSubjectOptions
(
'internal_regulation'
)]);
const
histories
=
await
loadRegulationHistory
(
loaded
);
setRecord
(
loaded
);
setHistoryRecords
(
histories
);
setSubject
(
loadedSubject
);
setOptions
(
loadedOptions
.
options
);
setForm
(
loaded
?.
metadata
||
{});
setError
(
''
);
}
catch
(
loadError
)
{
setLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'制度详情加载失败。'
);
}
finally
{
setLoading
(
false
);
}
};
useEffect
(()
=>
{
load
();
},
[
name
]);
useEffect
(()
=>
{
setSaveSuccess
(
''
);
load
();
},
[
name
,
setSaveSuccess
]);
const
fields
=
useMemo
(()
=>
(
subject
?.
fields
||
[]).
filter
((
field
)
=>
[
'regulation_name'
,
'effective_status'
,
'level'
,
'public_date'
,
'departments'
,
'approve'
,
'effective_date'
,
'comment'
].
includes
(
field
.
name
)),
[
subject
]);
const
fields
=
useMemo
(()
=>
(
subject
?.
fields
||
[]).
filter
((
field
)
=>
[
'regulation_name'
,
'effective_status'
,
'level'
,
'public_date'
,
'departments'
,
'approve'
,
'effective_date'
,
'comment'
].
includes
(
field
.
name
)),
[
subject
]);
const
save
=
async
()
=>
{
try
{
setSaving
(
true
);
await
updateRecord
(
record
.
name
||
name
,
{
name
:
record
.
name
||
name
,
metadata
:
normalizeMetadata
({
...
record
.
metadata
,
...
form
})
});
setEditOpen
(
false
);
await
load
();
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败。'
);
}
finally
{
setSaving
(
false
);
}
};
const
openEdit
=
()
=>
{
setError
(
''
);
setSaveSuccess
(
''
);
setEditOpen
(
true
);
};
const
save
=
async
()
=>
{
if
(
saving
||
!
record
)
return
;
setSaving
(
true
);
setError
(
''
);
setSaveSuccess
(
''
);
try
{
await
updateRecord
(
record
.
name
||
name
,
{
name
:
record
.
name
||
name
,
metadata
:
normalizeMetadata
({
...
record
.
metadata
,
...
form
}),
});
setEditOpen
(
false
);
setSaveSuccess
(
'详情已保存。'
);
await
load
();
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败。'
);
}
finally
{
setSaving
(
false
);
}
};
if
(
loading
)
return
<
div
className=
"flex min-h-72 items-center justify-center"
><
Spinner
label=
"正在加载制度详情..."
/></
div
>;
if
(
loading
)
return
<
div
className=
"flex min-h-72 items-center justify-center"
><
Spinner
label=
"正在加载制度详情..."
/></
div
>;
if
(
error
||
!
record
)
return
<
div
className=
"space-y-4"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
history
.
back
()
}
><
Back
size=
{
16
}
/>
返回
</
Button
><
div
className=
"rounded-2xl border border-danger/30 bg-danger/5 p-6 text-sm text-danger"
>
{
error
||
'制度不存在。'
}
</
div
></
div
>;
if
(
loadFailed
||
!
record
)
return
<
div
className=
"space-y-4"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
history
.
back
()
}
><
Back
size=
{
16
}
/>
返回
</
Button
><
RequestFailure
title=
{
loadFailed
?
'制度详情加载失败'
:
'制度不存在'
}
onRetry=
{
load
}
/
></
div
>;
const
metadata
=
record
.
metadata
||
{};
const
metadata
=
record
.
metadata
||
{};
const
fieldOptions
=
(
fieldName
)
=>
(
options
[
fieldName
]
||
[]).
map
((
item
)
=>
({
value
:
item
.
value
??
item
.
label
??
item
,
label
:
item
.
label
??
item
.
value
??
item
}));
const
fieldOptions
=
(
fieldName
)
=>
(
options
[
fieldName
]
||
[]).
map
((
item
)
=>
({
value
:
item
.
value
??
item
.
label
??
item
,
label
:
item
.
label
??
item
.
value
??
item
}));
const
referenceFields
=
new
Set
([
'departments'
,
'approve'
]);
const
referenceFields
=
new
Set
([
'departments'
,
'approve'
]);
return
<
div
className=
"space-y-5"
><
div
className=
"flex flex-wrap items-center justify-between gap-3"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
history
.
back
()
}
><
Back
size=
{
16
}
/>
返回制度目录
</
Button
><
Button
variant=
"secondary"
onPress=
{
()
=>
setEditOpen
(
true
)
}
><
Edit
size=
{
16
}
/>
编辑信息
</
Button
></
div
><
Card
className=
"rc-card-surface"
><
Card
.
Header
><
div
className=
"flex flex-wrap items-start justify-between gap-4"
><
div
><
p
className=
"text-sm text-muted"
>
内部制度
</
p
><
Card
.
Title
className=
"mt-1 text-2xl"
>
{
metadata
.
regulation_name
||
record
.
title
}
</
Card
.
Title
><
div
className=
"mt-3 flex flex-wrap gap-2"
>
{
metadata
.
effective_status
?
<
Chip
color=
{
metadata
.
effective_status
===
'现行有效'
?
'success'
:
'default'
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
metadata
.
effective_status
}
</
Chip
.
Label
></
Chip
>
:
null
}{
metadata
.
level
?
<
Chip
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
metadata
.
level
}
</
Chip
.
Label
></
Chip
>
:
null
}
</
div
></
div
><
div
className=
"text-right text-sm text-muted"
>
发文日期
<
br
/><
span
className=
"font-medium text-foreground"
>
{
dateText
(
metadata
.
public_date
)
}
</
span
></
div
></
div
></
Card
.
Header
><
Card
.
Content
><
dl
className=
"grid gap-x-8 gap-y-5 sm:grid-cols-2"
>
{
fields
.
map
((
field
)
=>
{
const
value
=
metadata
[
field
.
name
];
return
<
div
key=
{
field
.
name
}
><
dt
className=
"text-sm font-medium text-muted"
>
{
field
.
label
||
field
.
name
}
</
dt
><
dd
className=
"mt-1 whitespace-pre-wrap text-sm"
>
{
Array
.
isArray
(
value
)
?
value
.
map
(
referenceId
).
join
(
'、'
)
:
field
.
type
===
'date'
?
dateText
(
value
)
:
typeof
value
===
'object'
?
referenceId
(
value
)
:
value
||
'-'
}
</
dd
></
div
>;
})
}
</
dl
></
Card
.
Content
></
Card
><
div
className=
"grid gap-5 xl:grid-cols-[minmax(0,1fr)_20rem]"
><
Card
className=
"rc-card-surface"
><
Card
.
Header
><
Card
.
Title
className=
"flex items-center gap-2"
><
FileText
size=
{
18
}
/>
制度正文
</
Card
.
Title
></
Card
.
Header
><
Card
.
Content
><
article
className=
"prose prose-slate max-w-none whitespace-pre-wrap text-sm leading-7"
dangerouslySetInnerHTML=
{
{
__html
:
record
.
content
||
'<p class="text-muted">暂无正文内容</p>'
}
}
/></
Card
.
Content
></
Card
><
Card
className=
"rc-card-surface"
><
Card
.
Header
><
Card
.
Title
>
关联信息
</
Card
.
Title
></
Card
.
Header
><
Card
.
Content
className=
"space-y-4 text-sm"
><
div
><
p
className=
"mb-1 text-muted"
>
附件
</
p
>
{
(
metadata
.
attachments
||
[]).
length
?
metadata
.
attachments
.
map
((
attachment
,
index
)
=>
{
const
href
=
typeof
attachment
===
'string'
?
attachment
:
attachment
.
url
;
return
href
?
<
a
className=
"flex items-center gap-2 text-accent hover:underline"
href=
{
href
}
key=
{
referenceId
(
attachment
)
||
index
}
rel=
"noreferrer"
target=
"_blank"
><
PaperClipIcon
size=
{
15
}
/>
{
attachment
.
title
||
attachment
.
name
||
`附件 ${index + 1}`
}
</
a
>
:
<
span
className=
"text-muted"
key=
{
referenceId
(
attachment
)
||
index
}
>
附件
{
index
+
1
}
</
span
>;
})
:
<
span
className
=
"text-muted"
>
暂无附件
<
/span>
}
</
div
><
div
><
p
className=
"mb-1 text-muted"
>
相关制度
</
p
><
span
>
{
metadata
.
related_regulations
?.
length
?
`${metadata.related_regulations.length} 条`
:
'暂无关联'
}
</
span
></
div
><
div
><
p
className=
"mb-1 text-muted"
>
引用法规
</
p
><
span
>
{
metadata
.
reference_regulation
?
'已关联'
:
'暂无引用'
}
</
span
></
div
><
div
><
p
className=
"mb-1 text-muted"
>
历史沿革
</
p
>
{
historyRecords
.
length
?
<
div
className=
"space-y-2"
>
{
historyRecords
.
map
((
item
)
=>
<
div
className=
"border-t border-separator pt-2"
key=
{
item
.
_id
||
item
.
name
}
><
div
className=
"font-medium"
>
{
item
.
metadata
?.
regulation_name
||
item
.
title
||
'-'
}
</
div
><
div
className=
"text-muted"
>
{
dateText
(
item
.
metadata
?.
public_date
)
}
·
{
item
.
metadata
?.
effective_status
||
'历史版本'
}
</
div
></
div
>)
}
</
div
>
:
<
span
className=
"text-muted"
>
暂无历史版本
</
span
>
}
</
div
></
Card
.
Content
></
Card
></
div
><
XinyuanModal
isOpen=
{
editOpen
}
onOpenChange=
{
setEditOpen
}
size=
"2xl"
title=
"编辑制度信息"
footer=
{
<
div
className=
"flex justify-end gap-2"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
setEditOpen
(
false
)
}
>
取消
</
Button
><
Button
isDisabled=
{
saving
}
variant=
"primary"
onPress=
{
save
}
>
{
saving
?
'保存中...'
:
'保存'
}
</
Button
></
div
>
}
><
div
className=
"grid gap-4 sm:grid-cols-2"
>
{
fields
.
map
((
field
)
=>
{
const
value
=
form
[
field
.
name
];
const
selectedValues
=
Array
.
isArray
(
value
)
?
value
.
map
(
referenceId
)
:
value
?
[
referenceId
(
value
)]
:
[];
if
(
field
.
name
===
'regulation_name'
)
return
<
TextField
className=
"sm:col-span-2"
fullWidth
isRequired
key=
{
field
.
name
}
value=
{
value
||
''
}
onChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
next
}))
}
><
Label
>
{
field
.
label
||
field
.
name
}
</
Label
><
Input
fullWidth
variant=
"secondary"
/></
TextField
>;
if
(
field
.
type
===
'long_text'
||
field
.
name
===
'comment'
)
return
<
TextField
className=
"sm:col-span-2"
fullWidth
key=
{
field
.
name
}
value=
{
value
||
''
}
onChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
next
}))
}
><
Label
>
{
field
.
label
||
field
.
name
}
</
Label
><
TextArea
fullWidth
rows=
{
4
}
variant=
"secondary"
/></
TextField
>;
if
(
referenceFields
.
has
(
field
.
name
))
return
<
XinyuanAutocomplete
key=
{
field
.
name
}
label=
{
field
.
label
||
field
.
name
}
options=
{
fieldOptions
(
field
.
name
)
}
selectedKeys=
{
new
Set
(
selectedValues
)
}
selectionMode=
"multiple"
onSelectionChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
next
===
'all'
?
fieldOptions
(
field
.
name
).
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
next
||
[])
}))
}
/>;
if
(
field
.
name
===
'level'
||
field
.
name
===
'effective_status'
)
return
<
XinyuanAutocomplete
key=
{
field
.
name
}
label=
{
field
.
label
||
field
.
name
}
options=
{
fieldOptions
(
field
.
name
)
}
selectedKey=
{
value
||
''
}
onSelectionChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
String
(
next
)
}))
}
/>;
if
(
field
.
type
===
'date'
||
[
'public_date'
,
'effective_date'
].
includes
(
field
.
name
))
return
<
TextField
fullWidth
key=
{
field
.
name
}
value=
{
dateInputValue
(
value
)
}
onChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
next
}))
}
><
Label
>
{
field
.
label
||
field
.
name
}
</
Label
><
Input
fullWidth
type=
"date"
variant=
"secondary"
/></
TextField
>;
return
<
TextField
fullWidth
key=
{
field
.
name
}
value=
{
Array
.
isArray
(
value
)
?
value
.
join
(
'、'
)
:
value
||
''
}
onChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
next
}))
}
><
Label
>
{
field
.
label
||
field
.
name
}
</
Label
><
Input
fullWidth
variant=
"secondary"
/></
TextField
>;
})
}
</
div
></
XinyuanModal
></
div
>;
return
<
div
className=
"space-y-5"
><
div
className=
"flex flex-wrap items-center justify-between gap-3"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
history
.
back
()
}
><
Back
size=
{
16
}
/>
返回制度目录
</
Button
><
Button
variant=
"secondary"
onPress=
{
openEdit
}
><
Edit
size=
{
16
}
/>
编辑信息
</
Button
></
div
><
Card
className=
"rc-card-surface"
><
Card
.
Header
><
div
className=
"flex flex-wrap items-start justify-between gap-4"
><
div
><
p
className=
"text-sm text-muted"
>
内部制度
</
p
><
Card
.
Title
className=
"mt-1 text-2xl"
>
{
metadata
.
regulation_name
||
record
.
title
}
</
Card
.
Title
><
div
className=
"mt-3 flex flex-wrap gap-2"
>
{
metadata
.
effective_status
?
<
Chip
color=
{
metadata
.
effective_status
===
'现行有效'
?
'success'
:
'default'
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
metadata
.
effective_status
}
</
Chip
.
Label
></
Chip
>
:
null
}{
metadata
.
level
?
<
Chip
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
metadata
.
level
}
</
Chip
.
Label
></
Chip
>
:
null
}
</
div
></
div
><
div
className=
"text-right text-sm text-muted"
>
发文日期
<
br
/><
span
className=
"font-medium text-foreground"
>
{
dateText
(
metadata
.
public_date
)
}
</
span
></
div
></
div
></
Card
.
Header
><
Card
.
Content
><
dl
className=
"grid gap-x-8 gap-y-5 sm:grid-cols-2"
>
{
fields
.
map
((
field
)
=>
{
const
value
=
metadata
[
field
.
name
];
return
<
div
key=
{
field
.
name
}
><
dt
className=
"text-sm font-medium text-muted"
>
{
field
.
label
||
field
.
name
}
</
dt
><
dd
className=
"mt-1 whitespace-pre-wrap text-sm"
>
{
Array
.
isArray
(
value
)
?
value
.
map
(
referenceId
).
join
(
'、'
)
:
field
.
type
===
'date'
?
dateText
(
value
)
:
typeof
value
===
'object'
?
referenceId
(
value
)
:
value
||
'-'
}
</
dd
></
div
>;
})
}
</
dl
></
Card
.
Content
></
Card
><
div
className=
"grid gap-5 xl:grid-cols-[minmax(0,1fr)_20rem]"
><
Card
className=
"rc-card-surface"
><
Card
.
Header
><
Card
.
Title
className=
"flex items-center gap-2"
><
FileText
size=
{
18
}
/>
制度正文
</
Card
.
Title
></
Card
.
Header
><
Card
.
Content
><
article
className=
"prose prose-slate max-w-none whitespace-pre-wrap text-sm leading-7"
dangerouslySetInnerHTML=
{
{
__html
:
record
.
content
||
'<p class="text-muted">暂无正文内容</p>'
}
}
/></
Card
.
Content
></
Card
><
Card
className=
"rc-card-surface"
><
Card
.
Header
><
Card
.
Title
>
关联信息
</
Card
.
Title
></
Card
.
Header
><
Card
.
Content
className=
"space-y-4 text-sm"
><
div
><
p
className=
"mb-1 text-muted"
>
附件
</
p
>
{
(
metadata
.
attachments
||
[]).
length
?
metadata
.
attachments
.
map
((
attachment
,
index
)
=>
{
const
href
=
typeof
attachment
===
'string'
?
attachment
:
attachment
.
url
;
return
href
?
<
a
className=
"flex items-center gap-2 text-accent hover:underline"
href=
{
href
}
key=
{
referenceId
(
attachment
)
||
index
}
rel=
"noreferrer"
target=
"_blank"
><
PaperClipIcon
size=
{
15
}
/>
{
attachment
.
title
||
attachment
.
name
||
`附件 ${index + 1}`
}
</
a
>
:
<
span
className=
"text-muted"
key=
{
referenceId
(
attachment
)
||
index
}
>
附件
{
index
+
1
}
</
span
>;
})
:
<
span
className
=
"text-muted"
>
暂无附件
<
/span>
}
</
div
><
div
><
p
className=
"mb-1 text-muted"
>
相关制度
</
p
><
span
>
{
metadata
.
related_regulations
?.
length
?
`${metadata.related_regulations.length} 条`
:
'暂无关联'
}
</
span
></
div
><
div
><
p
className=
"mb-1 text-muted"
>
引用法规
</
p
><
span
>
{
metadata
.
reference_regulation
?
'已关联'
:
'暂无引用'
}
</
span
></
div
><
div
><
p
className=
"mb-1 text-muted"
>
历史沿革
</
p
>
{
historyRecords
.
length
?
<
div
className=
"space-y-2"
>
{
historyRecords
.
map
((
item
)
=>
<
div
className=
"border-t border-separator pt-2"
key=
{
item
.
_id
||
item
.
name
}
><
div
className=
"font-medium"
>
{
item
.
metadata
?.
regulation_name
||
item
.
title
||
'-'
}
</
div
><
div
className=
"text-muted"
>
{
dateText
(
item
.
metadata
?.
public_date
)
}
·
{
item
.
metadata
?.
effective_status
||
'历史版本'
}
</
div
></
div
>)
}
</
div
>
:
<
span
className=
"text-muted"
>
暂无历史版本
</
span
>
}
</
div
></
Card
.
Content
></
Card
></
div
><
XinyuanModal
isOpen=
{
editOpen
}
onOpenChange=
{
setEditOpen
}
size=
"2xl"
title=
"编辑制度信息"
footer=
{
<
div
className=
"flex justify-end gap-2"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
setEditOpen
(
false
)
}
>
取消
</
Button
><
Button
isDisabled=
{
saving
}
variant=
"primary"
onPress=
{
save
}
>
{
saving
?
'保存中...'
:
'保存'
}
</
Button
></
div
>
}
><
div
className=
"grid gap-4 sm:grid-cols-2"
>
{
fields
.
map
((
field
)
=>
{
const
value
=
form
[
field
.
name
];
const
selectedValues
=
Array
.
isArray
(
value
)
?
value
.
map
(
referenceId
)
:
value
?
[
referenceId
(
value
)]
:
[];
if
(
field
.
name
===
'regulation_name'
)
return
<
TextField
className=
"sm:col-span-2"
fullWidth
isRequired
key=
{
field
.
name
}
value=
{
value
||
''
}
onChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
next
}))
}
><
Label
>
{
field
.
label
||
field
.
name
}
</
Label
><
Input
fullWidth
variant=
"secondary"
/></
TextField
>;
if
(
field
.
type
===
'long_text'
||
field
.
name
===
'comment'
)
return
<
TextField
className=
"sm:col-span-2"
fullWidth
key=
{
field
.
name
}
value=
{
value
||
''
}
onChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
next
}))
}
><
Label
>
{
field
.
label
||
field
.
name
}
</
Label
><
TextArea
fullWidth
rows=
{
4
}
variant=
"secondary"
/></
TextField
>;
if
(
referenceFields
.
has
(
field
.
name
))
return
<
XinyuanAutocomplete
key=
{
field
.
name
}
label=
{
field
.
label
||
field
.
name
}
options=
{
fieldOptions
(
field
.
name
)
}
selectedKeys=
{
new
Set
(
selectedValues
)
}
selectionMode=
"multiple"
onSelectionChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
next
===
'all'
?
fieldOptions
(
field
.
name
).
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
next
||
[])
}))
}
/>;
if
(
field
.
name
===
'level'
||
field
.
name
===
'effective_status'
)
return
<
XinyuanAutocomplete
key=
{
field
.
name
}
label=
{
field
.
label
||
field
.
name
}
options=
{
fieldOptions
(
field
.
name
)
}
selectedKey=
{
value
||
''
}
onSelectionChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
String
(
next
)
}))
}
/>;
if
(
field
.
type
===
'date'
||
[
'public_date'
,
'effective_date'
].
includes
(
field
.
name
))
return
<
TextField
fullWidth
key=
{
field
.
name
}
value=
{
dateInputValue
(
value
)
}
onChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
next
}))
}
><
Label
>
{
field
.
label
||
field
.
name
}
</
Label
><
Input
fullWidth
type=
"date"
variant=
"secondary"
/></
TextField
>;
return
<
TextField
fullWidth
key=
{
field
.
name
}
value=
{
Array
.
isArray
(
value
)
?
value
.
join
(
'、'
)
:
value
||
''
}
onChange=
{
(
next
)
=>
setForm
((
current
)
=>
({
...
current
,
[
field
.
name
]:
next
}))
}
><
Label
>
{
field
.
label
||
field
.
name
}
</
Label
><
Input
fullWidth
variant=
"secondary"
/></
TextField
>;
})
}
</
div
></
XinyuanModal
></
div
>;
}
}
src/pages/internal_regulation_manager/InternalRegulationManager.jsx
View file @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Card
,
Spinner
,
Tabs
}
from
'@heroui/react'
;
import
{
Card
,
Spinner
,
Tabs
}
from
'@heroui/react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useSearchParams
}
from
'umi'
;
import
{
useSearchParams
}
from
'umi'
;
...
@@ -18,7 +20,9 @@ export default function InternalRegulationManager() {
...
@@ -18,7 +20,9 @@ export default function InternalRegulationManager() {
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
options
,
setOptions
]
=
useState
({});
const
[
options
,
setOptions
]
=
useState
({});
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useFeedbackState
(
'error'
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
...
@@ -38,7 +42,7 @@ export default function InternalRegulationManager() {
...
@@ -38,7 +42,7 @@ export default function InternalRegulationManager() {
}
}
})();
})();
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[]);
},
[
reloadKey
,
setError
]);
const
changeTab
=
(
key
)
=>
{
const
changeTab
=
(
key
)
=>
{
const
value
=
String
(
key
);
const
value
=
String
(
key
);
...
@@ -52,7 +56,7 @@ export default function InternalRegulationManager() {
...
@@ -52,7 +56,7 @@ export default function InternalRegulationManager() {
const
content
=
useMemo
(()
=>
{
const
content
=
useMemo
(()
=>
{
if
(
loading
)
return
<
div
className=
"flex min-h-72 items-center justify-center"
><
Spinner
label=
"正在加载制度配置..."
/></
div
>;
if
(
loading
)
return
<
div
className=
"flex min-h-72 items-center justify-center"
><
Spinner
label=
"正在加载制度配置..."
/></
div
>;
if
(
error
)
return
<
div
className=
"rounded-2xl border border-danger/30 bg-danger/5 p-6 text-sm text-danger"
>
{
error
}
</
div
>;
if
(
error
)
return
<
RequestFailure
title=
"制度配置加载失败"
onRetry=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
/
>;
if
(
activeTab
===
'process'
)
return
<
RevisionList
options=
{
options
}
/>;
if
(
activeTab
===
'process'
)
return
<
RevisionList
options=
{
options
}
/>;
if
(
activeTab
===
'archive'
)
return
<
ArchiveList
options=
{
options
}
/>;
if
(
activeTab
===
'archive'
)
return
<
ArchiveList
options=
{
options
}
/>;
return
<
RegulationList
subject=
{
subject
}
options=
{
options
}
/>;
return
<
RegulationList
subject=
{
subject
}
options=
{
options
}
/>;
...
...
src/pages/internal_regulation_manager/RegulationGraph.jsx
View file @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
,
Checkbox
,
Chip
,
Dropdown
,
Input
,
Label
,
Spinner
,
Tabs
,
TextArea
,
TextField
,
Tooltip
,
toast
}
from
'@heroui/react'
;
import
{
Button
,
Checkbox
,
Chip
,
Dropdown
,
Input
,
Label
,
Spinner
,
Tabs
,
TextArea
,
TextField
,
Tooltip
,
toast
}
from
'@heroui/react'
;
import
dagre
from
'dagre'
;
import
dagre
from
'dagre'
;
import
JSZip
from
'jszip'
;
import
JSZip
from
'jszip'
;
...
@@ -9,7 +11,6 @@ import {
...
@@ -9,7 +11,6 @@ import {
ArrowPathIcon
,
ArrowPathIcon
,
ChevronDown
,
ChevronDown
,
ChevronRight
,
ChevronRight
,
Delete
,
Download
,
Download
,
Edit
,
Edit
,
Expand
,
Expand
,
...
@@ -27,6 +28,7 @@ import { attachGraphAncestors } from '@/services/regulationGraphData';
...
@@ -27,6 +28,7 @@ import { attachGraphAncestors } from '@/services/regulationGraphData';
import
{
isRegulationHistory
,
isSameRegulation
}
from
'@/services/regulationHistory'
;
import
{
isRegulationHistory
,
isSameRegulation
}
from
'@/services/regulationHistory'
;
import
{
createGraphXmindArchive
}
from
'@/services/regulationGraphXmind'
;
import
{
createGraphXmindArchive
}
from
'@/services/regulationGraphXmind'
;
import
GraphNode
from
'./GraphNode'
;
import
GraphNode
from
'./GraphNode'
;
import
GraphNodeDetails
from
'./GraphNodeDetails'
;
import
{
getGraphRecordId
,
getNextGraphRecordNode
,
getSelectedGraphRecordId
}
from
'./graphSelection'
;
import
{
getGraphRecordId
,
getNextGraphRecordNode
,
getSelectedGraphRecordId
}
from
'./graphSelection'
;
import
{
findGraphDropTarget
,
REGULATION_DRAG_TYPE
}
from
'./graphDrop'
;
import
{
findGraphDropTarget
,
REGULATION_DRAG_TYPE
}
from
'./graphDrop'
;
import
{
getGraphBranchIds
,
isGraphDeleteShortcut
,
setGraphBranchRemoved
}
from
'./graphRemoval'
;
import
{
getGraphBranchIds
,
isGraphDeleteShortcut
,
setGraphBranchRemoved
}
from
'./graphRemoval'
;
...
@@ -251,8 +253,9 @@ export default function RegulationGraph() {
...
@@ -251,8 +253,9 @@ export default function RegulationGraph() {
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
exporting
,
setExporting
]
=
useState
(
false
);
const
[
exporting
,
setExporting
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
notice
,
setNotice
]
=
useState
(
''
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[,
setNotice
]
=
useFeedbackState
(
'success'
);
const
[
contextMenu
,
setContextMenu
]
=
useState
(
null
);
const
[
contextMenu
,
setContextMenu
]
=
useState
(
null
);
const
[
nodeDialog
,
setNodeDialog
]
=
useState
(
null
);
const
[
nodeDialog
,
setNodeDialog
]
=
useState
(
null
);
const
[
nodeForm
,
setNodeForm
]
=
useState
({
title
:
''
,
content
:
''
});
const
[
nodeForm
,
setNodeForm
]
=
useState
({
title
:
''
,
content
:
''
});
...
@@ -285,6 +288,8 @@ export default function RegulationGraph() {
...
@@ -285,6 +288,8 @@ export default function RegulationGraph() {
const
refresh
=
useCallback
(
async
()
=>
{
const
refresh
=
useCallback
(
async
()
=>
{
try
{
try
{
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
const
[
nextGraph
,
nextArchives
,
nextRegulations
,
nextRevisions
]
=
await
Promise
.
all
([
loadGraph
(),
loadGraphArchives
(),
queryAll
(
REGULATION_SUBJECT
,
{}),
queryAll
(
REVISION_SUBJECT
,
{
filter
:
{
'metadata.status'
:
{
$in
:
[
'起草中'
,
'已提交'
,
'已发文'
]
}
}
})]);
const
[
nextGraph
,
nextArchives
,
nextRegulations
,
nextRevisions
]
=
await
Promise
.
all
([
loadGraph
(),
loadGraphArchives
(),
queryAll
(
REGULATION_SUBJECT
,
{}),
queryAll
(
REVISION_SUBJECT
,
{
filter
:
{
'metadata.status'
:
{
$in
:
[
'起草中'
,
'已提交'
,
'已发文'
]
}
}
})]);
setRegulations
(
nextRegulations
);
setRegulations
(
nextRegulations
);
setRevisions
(
nextRevisions
);
setRevisions
(
nextRevisions
);
...
@@ -292,15 +297,11 @@ export default function RegulationGraph() {
...
@@ -292,15 +297,11 @@ export default function RegulationGraph() {
setArchives
([...
new
Set
([
'当前版本'
,
...
nextArchives
.
filter
((
value
)
=>
value
!==
'当前版本'
)])]);
setArchives
([...
new
Set
([
'当前版本'
,
...
nextArchives
.
filter
((
value
)
=>
value
!==
'当前版本'
)])]);
setError
(
''
);
setError
(
''
);
}
catch
(
loadError
)
{
}
catch
(
loadError
)
{
setLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'拓扑图加载失败,请稍后重试。'
);
setError
(
loadError
?.
message
||
'拓扑图加载失败,请稍后重试。'
);
}
finally
{
setLoading
(
false
);
}
}
finally
{
setLoading
(
false
);
}
},
[
applyGraph
]);
},
[
applyGraph
,
setError
]);
useEffect
(()
=>
{
refresh
();
},
[
refresh
]);
useEffect
(()
=>
{
refresh
();
},
[
refresh
]);
useEffect
(()
=>
{
if
(
!
notice
)
return
undefined
;
const
timer
=
window
.
setTimeout
(()
=>
setNotice
(
''
),
4000
);
return
()
=>
window
.
clearTimeout
(
timer
);
},
[
notice
]);
const
openContextMenu
=
useCallback
((
event
,
nodeId
=
null
)
=>
{
const
openContextMenu
=
useCallback
((
event
,
nodeId
=
null
)
=>
{
event
.
preventDefault
();
event
.
preventDefault
();
...
@@ -490,7 +491,7 @@ export default function RegulationGraph() {
...
@@ -490,7 +491,7 @@ export default function RegulationGraph() {
const
focusRecordNode
=
useCallback
((
record
)
=>
{
const
focusRecordNode
=
useCallback
((
record
)
=>
{
const
node
=
getNextGraphRecordNode
(
record
,
graph
.
nodes
,
selectedId
,
selectedRecordId
);
const
node
=
getNextGraphRecordNode
(
record
,
graph
.
nodes
,
selectedId
,
selectedRecordId
);
if
(
!
node
)
{
if
(
!
node
)
{
toast
.
warning
(
'该制度在当前拓扑图中暂无关联节点。'
);
toast
.
warning
(
'该制度在当前拓扑图中暂无关联节点。'
,
{
timeout
:
8000
}
);
return
;
return
;
}
}
focusNode
(
node
,
getGraphRecordId
(
record
));
focusNode
(
node
,
getGraphRecordId
(
record
));
...
@@ -608,7 +609,7 @@ export default function RegulationGraph() {
...
@@ -608,7 +609,7 @@ export default function RegulationGraph() {
if
(
archive
!==
'当前版本'
||
saving
)
return
;
if
(
archive
!==
'当前版本'
||
saving
)
return
;
const
target
=
getDropTarget
(
event
);
const
target
=
getDropTarget
(
event
);
if
(
!
target
)
{
if
(
!
target
)
{
toast
.
warning
(
'请拖到目标节点右侧的连接区域。'
);
toast
.
warning
(
'请拖到目标节点右侧的连接区域。'
,
{
timeout
:
8000
}
);
return
;
return
;
}
}
try
{
try
{
...
@@ -645,6 +646,8 @@ export default function RegulationGraph() {
...
@@ -645,6 +646,8 @@ export default function RegulationGraph() {
setSelectedGroupIds
([]);
setSelectedGroupIds
([]);
setSelectedStatuses
([]);
setSelectedStatuses
([]);
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
const
archivedGraph
=
await
loadGraphArchive
(
value
);
const
archivedGraph
=
await
loadGraphArchive
(
value
);
// The current version can have a stale/empty graph record after a fresh
// The current version can have a stale/empty graph record after a fresh
// deployment. Reuse the same regulation-reference fallback as the first
// deployment. Reuse the same regulation-reference fallback as the first
...
@@ -655,6 +658,7 @@ export default function RegulationGraph() {
...
@@ -655,6 +658,7 @@ export default function RegulationGraph() {
applyGraph
(
enrichGraph
(
nextGraph
,
regulations
,
revisions
));
applyGraph
(
enrichGraph
(
nextGraph
,
regulations
,
revisions
));
setError
(
''
);
setError
(
''
);
}
catch
(
loadError
)
{
}
catch
(
loadError
)
{
setLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'归档拓扑图加载失败。'
);
setError
(
loadError
?.
message
||
'归档拓扑图加载失败。'
);
}
finally
{
}
finally
{
setLoading
(
false
);
setLoading
(
false
);
...
@@ -662,13 +666,12 @@ export default function RegulationGraph() {
...
@@ -662,13 +666,12 @@ export default function RegulationGraph() {
};
};
const
listNodes
=
activePanel
===
'all'
?
allNodes
:
activePanel
===
'pending'
?
pendingNodes
:
activePanel
===
'removed'
?
removedNodes
:
filteredNodes
;
const
listNodes
=
activePanel
===
'all'
?
allNodes
:
activePanel
===
'pending'
?
pendingNodes
:
activePanel
===
'removed'
?
removedNodes
:
filteredNodes
;
if
(
loadFailed
&&
!
loading
)
return
<
RequestFailure
title=
"拓扑图加载失败"
onRetry=
{
()
=>
archive
===
'当前版本'
?
refresh
()
:
loadArchive
(
archive
)
}
/>;
if
(
loading
)
return
<
div
className=
"flex min-h-72 items-center justify-center"
><
Spinner
label=
"正在加载制度拓扑图..."
/></
div
>;
if
(
loading
)
return
<
div
className=
"flex min-h-72 items-center justify-center"
><
Spinner
label=
"正在加载制度拓扑图..."
/></
div
>;
return
(
return
(
<
div
className=
"flex h-[calc(100dvh-7.5rem)] min-h-0 flex-col gap-4 overflow-hidden"
onClick=
{
()
=>
setContextMenu
(
null
)
}
>
<
div
className=
"flex h-[calc(100dvh-7.5rem)] min-h-0 flex-col gap-4 overflow-hidden"
onClick=
{
()
=>
setContextMenu
(
null
)
}
>
{
error
?
<
div
role=
"alert"
className=
"rounded-xl border border-danger/30 bg-danger/5 p-3 text-sm text-danger"
>
{
error
}
</
div
>
:
null
}
{
notice
?
<
div
role=
"status"
className=
"rounded-xl border border-success/30 bg-success/5 p-3 text-sm text-success"
>
{
notice
}
</
div
>
:
null
}
<
div
className=
"relative flex min-h-0 flex-1 overflow-hidden rounded-2xl border border-separator bg-background shadow-sm"
ref=
{
canvasRef
}
>
<
div
className=
"relative flex min-h-0 flex-1 overflow-hidden rounded-2xl border border-separator bg-background shadow-sm"
ref=
{
canvasRef
}
>
<
aside
className=
{
`
shrink-0 overflow-hidden border-e border-separator transition-[width] ${leftOpen ? 'w-72' : 'w-0'}`
}
><
div
className=
"flex h-full w-72
flex-col"
><
div
className=
"border-b border-separator p-3"
><
TextField
fullWidth
value=
{
searchText
}
onChange=
{
setSearchText
}
><
Label
className=
"sr-only"
>
搜索节点
</
Label
><
div
className=
"relative"
><
Search
className=
"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted"
size=
{
16
}
/><
Input
className=
"pl-9"
fullWidth
placeholder=
"搜索制度或节点"
variant=
"secondary"
/></
div
></
TextField
></
div
><
Tabs
className=
"min-w-0"
selectedKey=
{
activePanel
}
variant=
"primary"
onSelectionChange=
{
(
key
)
=>
setActivePanel
(
String
(
key
))
}
><
Tabs
.
ListContainer
className=
"w-full min-w-0 overflow-hidden border-b border-separator px-2"
><
Tabs
.
List
className=
"grid w-full min-w-0 grid-cols-4"
style=
{
{
display
:
'grid'
,
gridTemplateColumns
:
'repeat(4, minmax(0, 1fr))'
,
width
:
'100%'
}
}
aria
-
label=
"拓扑图节点列表"
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"all"
>
全部
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"pending"
>
待分类
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"nodes"
>
节点
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"removed"
>
删除节点
<
Tabs
.
Indicator
/></
Tabs
.
Tab
></
Tabs
.
List
></
Tabs
.
ListContainer
></
Tabs
><
div
className=
"min-h-0 flex-1 overflow-y-auto p-2"
>
{
listNodes
.
map
((
node
)
=>
{
const
isRecordList
=
activePanel
===
'all'
||
activePanel
===
'pending'
;
const
isSelected
=
isRecordList
?
highlightedRecordId
!==
null
&&
getGraphRecordId
(
node
)
===
highlightedRecordId
:
selectedId
===
node
.
id
;
const
title
=
isRecordList
?
metadataOf
(
node
).
regulation_name
||
node
.
title
:
node
.
title
;
const
type
=
isRecordList
?
node
.
__pendingStatus
||
(
metadataOf
(
node
).
type
===
'新增'
?
'addition'
:
metadataOf
(
node
).
type
===
'废止'
?
'forbidden'
:
'revise'
)
:
nodeStatus
(
node
);
return
<
button
draggable=
{
isRecordList
&&
archive
===
'当前版本'
&&
!
saving
}
onDragStart=
{
(
event
)
=>
{
event
.
dataTransfer
.
effectAllowed
=
'copy'
;
event
.
dataTransfer
.
setData
(
REGULATION_DRAG_TYPE
,
JSON
.
stringify
(
node
));
}
}
onDragEnd=
{
clearDropPreview
}
className=
{
`flex min-h-11 w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm hover:bg-surface-secondary ${isSelected ? 'bg-accent/10 text-accent' : ''}`
}
aria
-
current=
{
isSelected
?
'true'
:
undefined
}
key=
{
node
.
_id
||
node
.
id
||
node
.
name
}
onClick=
{
()
=>
isRecordList
?
focusRecordNode
(
node
)
:
focusNode
(
node
)
}
><
span
className=
"min-w-0 flex-1 truncate"
>
{
title
}
</
span
><
Chip
color=
{
STATUS_OPTIONS
.
find
((
item
)
=>
item
.
value
===
type
)?.
color
||
'default'
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
isRecordList
?
(
type
===
'active'
?
'现行有效'
:
`待${metadataOf(node).type || '处理'}`
)
:
nodeTypeLabel
(
node
.
type
)
}
</
Chip
.
Label
></
Chip
></
button
>;
})
}{
listNodes
.
length
===
0
?
<
p
className=
"p-4 text-center text-sm text-muted"
>
暂无节点
</
p
>
:
null
}
</
div
></
div
></
aside
>
<
aside
className=
{
`
min-w-0 shrink overflow-hidden border-e border-separator transition-[width] ${leftOpen ? 'w-72' : 'w-0'}`
}
><
div
className=
"flex h-full w-full min-w-0
flex-col"
><
div
className=
"border-b border-separator p-3"
><
TextField
fullWidth
value=
{
searchText
}
onChange=
{
setSearchText
}
><
Label
className=
"sr-only"
>
搜索节点
</
Label
><
div
className=
"relative"
><
Search
className=
"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted"
size=
{
16
}
/><
Input
className=
"pl-9"
fullWidth
placeholder=
"搜索制度或节点"
variant=
"secondary"
/></
div
></
TextField
></
div
><
Tabs
className=
"min-w-0"
selectedKey=
{
activePanel
}
variant=
"primary"
onSelectionChange=
{
(
key
)
=>
setActivePanel
(
String
(
key
))
}
><
Tabs
.
ListContainer
className=
"w-full min-w-0 overflow-hidden border-b border-separator px-2"
><
Tabs
.
List
className=
"grid w-full min-w-0 grid-cols-4"
style=
{
{
display
:
'grid'
,
gridTemplateColumns
:
'repeat(4, minmax(0, 1fr))'
,
width
:
'100%'
}
}
aria
-
label=
"拓扑图节点列表"
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"all"
>
全部
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"pending"
>
待分类
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"nodes"
>
节点
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
className=
"min-w-0 whitespace-nowrap px-1 text-xs"
style=
{
{
minWidth
:
0
,
whiteSpace
:
'nowrap'
,
paddingInline
:
4
}
}
id=
"removed"
>
删除节点
<
Tabs
.
Indicator
/></
Tabs
.
Tab
></
Tabs
.
List
></
Tabs
.
ListContainer
></
Tabs
><
div
className=
"min-h-0 flex-1 overflow-y-auto p-2"
>
{
listNodes
.
map
((
node
)
=>
{
const
isRecordList
=
activePanel
===
'all'
||
activePanel
===
'pending'
;
const
isSelected
=
isRecordList
?
highlightedRecordId
!==
null
&&
getGraphRecordId
(
node
)
===
highlightedRecordId
:
selectedId
===
node
.
id
;
const
title
=
isRecordList
?
metadataOf
(
node
).
regulation_name
||
node
.
title
:
node
.
title
;
const
type
=
isRecordList
?
node
.
__pendingStatus
||
(
metadataOf
(
node
).
type
===
'新增'
?
'addition'
:
metadataOf
(
node
).
type
===
'废止'
?
'forbidden'
:
'revise'
)
:
nodeStatus
(
node
);
return
<
button
draggable=
{
isRecordList
&&
archive
===
'当前版本'
&&
!
saving
}
onDragStart=
{
(
event
)
=>
{
event
.
dataTransfer
.
effectAllowed
=
'copy'
;
event
.
dataTransfer
.
setData
(
REGULATION_DRAG_TYPE
,
JSON
.
stringify
(
node
));
}
}
onDragEnd=
{
clearDropPreview
}
className=
{
`flex min-h-11 w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm hover:bg-surface-secondary ${isSelected ? 'bg-accent/10 text-accent' : ''}`
}
aria
-
current=
{
isSelected
?
'true'
:
undefined
}
key=
{
node
.
_id
||
node
.
id
||
node
.
name
}
onClick=
{
()
=>
isRecordList
?
focusRecordNode
(
node
)
:
focusNode
(
node
)
}
><
span
className=
"min-w-0 flex-1 truncate"
>
{
title
}
</
span
><
Chip
color=
{
STATUS_OPTIONS
.
find
((
item
)
=>
item
.
value
===
type
)?.
color
||
'default'
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
isRecordList
?
(
type
===
'active'
?
'现行有效'
:
`待${metadataOf(node).type || '处理'}`
)
:
nodeTypeLabel
(
node
.
type
)
}
</
Chip
.
Label
></
Chip
></
button
>;
})
}{
listNodes
.
length
===
0
?
<
p
className=
"p-4 text-center text-sm text-muted"
>
暂无节点
</
p
>
:
null
}
</
div
></
div
></
aside
>
<
main
ref=
{
graphCanvasRef
}
className=
"relative min-h-0 min-w-0 flex-1 bg-surface-secondary/30"
onDragOver=
{
previewRecordDrop
}
onDragLeave=
{
leaveRecordDrop
}
onDrop=
{
dropPendingRevision
}
><
div
className=
"absolute inset-x-3 top-3 z-10 flex flex-wrap items-center justify-between gap-2"
><
div
className=
"flex flex-wrap items-center gap-2 rounded-xl border border-separator bg-background/95 p-2 shadow-sm backdrop-blur"
><
XinyuanAutocomplete
className=
"w-40"
aria
-
label=
"归档版本"
options=
{
archives
.
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
archive
}
onSelectionChange=
{
(
value
)
=>
loadArchive
(
String
(
value
))
}
/><
ProcessCascade
tree=
{
processTree
}
value=
{
selectedGroupIds
}
onChange=
{
setSelectedGroupIds
}
/><
Checkbox
isSelected=
{
showCurrent
}
variant=
"secondary"
onChange=
{
setShowCurrent
}
><
Checkbox
.
Content
><
Checkbox
.
Control
><
Checkbox
.
Indicator
/></
Checkbox
.
Control
></
Checkbox
.
Content
><
Label
>
显示现行
</
Label
></
Checkbox
><
Checkbox
isSelected=
{
showRemoved
}
variant=
"secondary"
onChange=
{
setShowRemoved
}
><
Checkbox
.
Content
><
Checkbox
.
Control
><
Checkbox
.
Indicator
/></
Checkbox
.
Control
></
Checkbox
.
Content
><
Label
>
显示已移除
</
Label
></
Checkbox
>
{
showCurrent
?
<
div
className=
"flex items-center gap-1"
>
{
STATUS_OPTIONS
.
map
((
option
)
=>
<
button
className=
{
`rounded-md px-2 py-1 text-xs ${selectedStatuses.includes(option.value) ? 'bg-accent/10 text-accent' : 'text-muted hover:bg-surface-secondary'}`
}
key=
{
option
.
value
}
onClick=
{
()
=>
setSelectedStatuses
((
current
)
=>
current
.
includes
(
option
.
value
)
?
current
.
filter
((
value
)
=>
value
!==
option
.
value
)
:
[...
current
,
option
.
value
])
}
>
{
option
.
label
}
</
button
>)
}
</
div
>
:
null
}
</
div
><
div
className=
"flex items-center gap-1 rounded-xl border border-separator bg-background/95 p-1 shadow-sm backdrop-blur"
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"恢复视图"
size=
"sm"
variant=
"tertiary"
onPress=
{
restoreView
}
><
Reload
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
恢复视图
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"展开全部"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
setCollapsedIds
(
new
Set
())
}
><
Expand
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
展开全部
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"折叠全部"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
setCollapsedIds
(
defaultCollapsedIds
(
graph
.
nodes
))
}
><
ArrowPathIcon
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
折叠到第一层
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"全屏查看"
size=
"sm"
variant=
"tertiary"
onPress=
{
toggleFullscreen
}
><
Fullscreen
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
全屏查看
</
Tooltip
.
Content
></
Tooltip
></
div
>
<
main
ref=
{
graphCanvasRef
}
className=
"relative min-h-0 min-w-0 flex-1 bg-surface-secondary/30"
onDragOver=
{
previewRecordDrop
}
onDragLeave=
{
leaveRecordDrop
}
onDrop=
{
dropPendingRevision
}
><
div
className=
"absolute inset-x-3 top-3 z-10 flex flex-wrap items-center justify-between gap-2"
><
div
className=
"flex flex-wrap items-center gap-2 rounded-xl border border-separator bg-background/95 p-2 shadow-sm backdrop-blur"
><
XinyuanAutocomplete
className=
"w-40"
aria
-
label=
"归档版本"
options=
{
archives
.
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
archive
}
onSelectionChange=
{
(
value
)
=>
loadArchive
(
String
(
value
))
}
/><
ProcessCascade
tree=
{
processTree
}
value=
{
selectedGroupIds
}
onChange=
{
setSelectedGroupIds
}
/><
Checkbox
isSelected=
{
showCurrent
}
variant=
"secondary"
onChange=
{
setShowCurrent
}
><
Checkbox
.
Content
><
Checkbox
.
Control
><
Checkbox
.
Indicator
/></
Checkbox
.
Control
></
Checkbox
.
Content
><
Label
>
显示现行
</
Label
></
Checkbox
><
Checkbox
isSelected=
{
showRemoved
}
variant=
"secondary"
onChange=
{
setShowRemoved
}
><
Checkbox
.
Content
><
Checkbox
.
Control
><
Checkbox
.
Indicator
/></
Checkbox
.
Control
></
Checkbox
.
Content
><
Label
>
显示已移除
</
Label
></
Checkbox
>
{
showCurrent
?
<
div
className=
"flex items-center gap-1"
>
{
STATUS_OPTIONS
.
map
((
option
)
=>
<
button
className=
{
`rounded-md px-2 py-1 text-xs ${selectedStatuses.includes(option.value) ? 'bg-accent/10 text-accent' : 'text-muted hover:bg-surface-secondary'}`
}
key=
{
option
.
value
}
onClick=
{
()
=>
setSelectedStatuses
((
current
)
=>
current
.
includes
(
option
.
value
)
?
current
.
filter
((
value
)
=>
value
!==
option
.
value
)
:
[...
current
,
option
.
value
])
}
>
{
option
.
label
}
</
button
>)
}
</
div
>
:
null
}
</
div
><
div
className=
"flex items-center gap-1 rounded-xl border border-separator bg-background/95 p-1 shadow-sm backdrop-blur"
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"恢复视图"
size=
"sm"
variant=
"tertiary"
onPress=
{
restoreView
}
><
Reload
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
恢复视图
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"展开全部"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
setCollapsedIds
(
new
Set
())
}
><
Expand
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
展开全部
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"折叠全部"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
setCollapsedIds
(
defaultCollapsedIds
(
graph
.
nodes
))
}
><
ArrowPathIcon
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
折叠到第一层
</
Tooltip
.
Content
></
Tooltip
><
Tooltip
delay=
{
0
}
><
Button
isIconOnly
aria
-
label=
"全屏查看"
size=
"sm"
variant=
"tertiary"
onPress=
{
toggleFullscreen
}
><
Fullscreen
size=
{
16
}
/></
Button
><
Tooltip
.
Content
>
全屏查看
</
Tooltip
.
Content
></
Tooltip
></
div
>
</
div
><
div
className=
"relative h-full min-h-0 w-full"
onContextMenu=
{
(
event
)
=>
event
.
preventDefault
()
}
>
{
visibleNodes
.
length
?
<
ReactFlowProvider
><
ReactFlow
nodes=
{
flowNodes
}
edges=
{
visibleEdges
}
deleteKeyCode=
{
null
}
nodeTypes=
{
NODE_TYPES
}
fitView
onInit=
{
(
instance
)
=>
{
flowRef
.
current
=
instance
;
instance
.
fitView
({
padding
:
0.22
});
}
}
onNodeClick=
{
(
_
,
node
)
=>
selectNode
(
nodeById
.
get
(
String
(
node
.
id
)))
}
onNodeDoubleClick=
{
(
_
,
node
)
=>
{
const
current
=
nodeById
.
get
(
String
(
node
.
id
));
const
relatedId
=
current
?.
relatedRegulation
||
current
?.
relatedReviseRegulation
;
if
(
relatedId
)
history
.
push
(
`/internal_regulation/${relatedId}`
);
}
}
onNodeDragStop=
{
(
_
,
node
)
=>
{
setDraggedPositions
((
current
)
=>
({
...
current
,
[
node
.
id
]:
node
.
position
}));
setGraph
((
current
)
=>
({
...
current
,
nodes
:
current
.
nodes
.
map
((
item
)
=>
item
.
id
===
node
.
id
?
{
...
item
,
position
:
node
.
position
}
:
item
)
}));
}
}
onNodeContextMenu=
{
(
event
,
node
)
=>
openContextMenu
(
event
,
String
(
node
.
id
))
}
onPaneContextMenu=
{
openContextMenu
}
onMoveStart=
{
()
=>
setContextMenu
(
null
)
}
onNodeDragStart=
{
()
=>
setContextMenu
(
null
)
}
onPaneClick=
{
()
=>
{
setContextMenu
(
null
);
clearNodeSelection
();
}
}
proOptions=
{
{
hideAttribution
:
true
}
}
><
MiniMap
pannable
zoomable
/><
Controls
showInteractive=
{
false
}
/><
Background
color=
"#e2e8f0"
gap=
{
24
}
/></
ReactFlow
></
ReactFlowProvider
>
:
<
div
className=
"flex h-full min-h-0 items-center justify-center text-sm text-muted"
>
暂无可展示的拓扑节点
</
div
>
}
</
div
><
div
className=
"relative h-full min-h-0 w-full"
onContextMenu=
{
(
event
)
=>
event
.
preventDefault
()
}
>
{
visibleNodes
.
length
?
<
ReactFlowProvider
><
ReactFlow
nodes=
{
flowNodes
}
edges=
{
visibleEdges
}
deleteKeyCode=
{
null
}
nodeTypes=
{
NODE_TYPES
}
fitView
onInit=
{
(
instance
)
=>
{
flowRef
.
current
=
instance
;
instance
.
fitView
({
padding
:
0.22
});
}
}
onNodeClick=
{
(
_
,
node
)
=>
selectNode
(
nodeById
.
get
(
String
(
node
.
id
)))
}
onNodeDoubleClick=
{
(
_
,
node
)
=>
{
const
current
=
nodeById
.
get
(
String
(
node
.
id
));
const
relatedId
=
current
?.
relatedRegulation
||
current
?.
relatedReviseRegulation
;
if
(
relatedId
)
history
.
push
(
`/internal_regulation/${relatedId}`
);
}
}
onNodeDragStop=
{
(
_
,
node
)
=>
{
setDraggedPositions
((
current
)
=>
({
...
current
,
[
node
.
id
]:
node
.
position
}));
setGraph
((
current
)
=>
({
...
current
,
nodes
:
current
.
nodes
.
map
((
item
)
=>
item
.
id
===
node
.
id
?
{
...
item
,
position
:
node
.
position
}
:
item
)
}));
}
}
onNodeContextMenu=
{
(
event
,
node
)
=>
openContextMenu
(
event
,
String
(
node
.
id
))
}
onPaneContextMenu=
{
openContextMenu
}
onMoveStart=
{
()
=>
setContextMenu
(
null
)
}
onNodeDragStart=
{
()
=>
setContextMenu
(
null
)
}
onPaneClick=
{
()
=>
{
setContextMenu
(
null
);
clearNodeSelection
();
}
}
proOptions=
{
{
hideAttribution
:
true
}
}
><
MiniMap
pannable
zoomable
/><
Controls
showInteractive=
{
false
}
/><
Background
color=
"#e2e8f0"
gap=
{
24
}
/></
ReactFlow
></
ReactFlowProvider
>
:
<
div
className=
"flex h-full min-h-0 items-center justify-center text-sm text-muted"
>
暂无可展示的拓扑节点
</
div
>
}
...
@@ -698,8 +701,8 @@ export default function RegulationGraph() {
...
@@ -698,8 +701,8 @@ export default function RegulationGraph() {
</
div
>
</
div
>
)
:
null
}
)
:
null
}
<
button
aria
-
label=
{
leftOpen
?
'收起左侧节点列表'
:
'展开左侧节点列表'
}
className=
"absolute left-1 top-1/2 z-20 grid size-8 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-separator bg-background shadow-sm"
onClick=
{
()
=>
setLeftOpen
((
value
)
=>
!
value
)
}
><
ChevronRight
className=
{
leftOpen
?
'rotate-180'
:
''
}
size=
{
15
}
/></
button
></
main
>
<
button
aria
-
label=
{
leftOpen
?
'收起左侧节点列表'
:
'展开左侧节点列表'
}
className=
"absolute left-1 top-1/2 z-20 grid size-8 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-separator bg-background shadow-sm"
onClick=
{
()
=>
setLeftOpen
((
value
)
=>
!
value
)
}
><
ChevronRight
className=
{
leftOpen
?
'rotate-180'
:
''
}
size=
{
15
}
/></
button
></
main
>
<
aside
aria
-
label=
"拓扑图操作与节点详情"
className=
{
`relative shrink-0 transition-[width] ${rightOpen ? 'w-80 border-s border-separator' : 'w-0'}`
}
>
<
aside
aria
-
label=
"拓扑图操作与节点详情"
className=
{
`relative
max-w-full
shrink-0 transition-[width] ${rightOpen ? 'w-80 border-s border-separator' : 'w-0'}`
}
>
<
div
className=
{
`absolute right-0 top-0 z-10 flex
w-80 flex-col bg-background ${rightOpen ? 'bottom-0' : '
rounded-bl-xl border-s border-separator shadow-sm'}`
}
>
<
div
className=
{
`absolute right-0 top-0 z-10 flex
flex-col bg-background ${rightOpen ? 'bottom-0 w-full' : 'w-80 max-w-[calc(100vw-2rem)]
rounded-bl-xl border-s border-separator shadow-sm'}`
}
>
<
div
aria
-
label=
"拓扑图操作"
role=
"group"
className=
"grid shrink-0 grid-cols-3 items-center gap-2 border-b border-separator p-3"
>
<
div
aria
-
label=
"拓扑图操作"
role=
"group"
className=
"grid shrink-0 grid-cols-3 items-center gap-2 border-b border-separator p-3"
>
<
Button
aria
-
label=
"导入 XMind"
className=
"h-9 min-w-0 gap-1.5 whitespace-nowrap px-2"
fullWidth
isDisabled=
{
saving
||
archive
!==
'当前版本'
}
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
importInputRef
.
current
?.
click
()
}
><
Upload
className=
"shrink-0"
size=
{
15
}
/><
span
>
导入
</
span
></
Button
>
<
Button
aria
-
label=
"导入 XMind"
className=
"h-9 min-w-0 gap-1.5 whitespace-nowrap px-2"
fullWidth
isDisabled=
{
saving
||
archive
!==
'当前版本'
}
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
importInputRef
.
current
?.
click
()
}
><
Upload
className=
"shrink-0"
size=
{
15
}
/><
span
>
导入
</
span
></
Button
>
<
Dropdown
>
<
Dropdown
>
...
@@ -714,32 +717,29 @@ export default function RegulationGraph() {
...
@@ -714,32 +717,29 @@ export default function RegulationGraph() {
<
Button
className=
"h-9 min-w-0 gap-1.5 whitespace-nowrap px-2"
fullWidth
isDisabled=
{
saving
||
archive
!==
'当前版本'
}
size=
"sm"
variant=
"primary"
onPress=
{
save
}
><
Save
className=
"shrink-0"
size=
{
15
}
/><
span
>
保存
</
span
></
Button
>
<
Button
className=
"h-9 min-w-0 gap-1.5 whitespace-nowrap px-2"
fullWidth
isDisabled=
{
saving
||
archive
!==
'当前版本'
}
size=
"sm"
variant=
"primary"
onPress=
{
save
}
><
Save
className=
"shrink-0"
size=
{
15
}
/><
span
>
保存
</
span
></
Button
>
</
div
>
</
div
>
<
input
accept=
".xmind,.json"
aria
-
label=
"导入 XMind 文件"
disabled=
{
saving
||
archive
!==
'当前版本'
}
hidden
ref=
{
importInputRef
}
type=
"file"
onChange=
{
importGraph
}
/>
<
input
accept=
".xmind,.json"
aria
-
label=
"导入 XMind 文件"
disabled=
{
saving
||
archive
!==
'当前版本'
}
hidden
ref=
{
importInputRef
}
type=
"file"
onChange=
{
importGraph
}
/>
<
div
className=
{
`min-h-0 flex-1 flex-col ${rightOpen ? 'flex' : 'hidden'}`
}
>
{
selectedNode
?
<><
div
className=
"flex shrink-0 items-center justify-between border-b border-separator p-4"
><
div
><
p
className=
"text-xs text-muted"
>
{
nodeTypeLabel
(
selectedNode
.
type
)
}
</
p
><
h2
className=
"mt-1 max-w-56 truncate font-semibold"
>
{
selectedNode
.
title
}
</
h2
></
div
><
Button
isIconOnly
aria
-
label=
"清除选择"
size=
"sm"
variant=
"ghost"
onPress=
{
clearNodeSelection
}
>
×
</
Button
></
div
><
div
className=
"min-h-0 flex-1 overflow-y-auto p-4"
><
div
className=
"space-y-4"
><
TextField
fullWidth
value=
{
selectedNode
.
title
}
onChange=
{
(
value
)
=>
setNodeField
(
'title'
,
value
)
}
><
Label
>
标题
</
Label
><
Input
fullWidth
variant=
"secondary"
/></
TextField
><
TextField
fullWidth
value=
{
selectedNode
.
content
||
''
}
onChange=
{
(
value
)
=>
setNodeField
(
'content'
,
value
)
}
><
Label
>
备注
</
Label
><
TextArea
fullWidth
rows=
{
5
}
variant=
"secondary"
/></
TextField
><
div
><
Label
>
节点状态
</
Label
><
div
className=
"mt-2 flex flex-wrap gap-2"
>
{
selectedNode
.
removed
?
<
Chip
color=
"danger"
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
已删除
</
Chip
.
Label
></
Chip
>
:
null
}
<
Chip
color=
{
STATUS_OPTIONS
.
find
((
item
)
=>
item
.
value
===
nodeStatus
(
selectedNode
))?.
color
||
'default'
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
STATUS_OPTIONS
.
find
((
item
)
=>
item
.
value
===
nodeStatus
(
selectedNode
))?.
label
||
nodeTypeLabel
(
selectedNode
.
type
)
}
</
Chip
.
Label
></
Chip
></
div
></
div
>
<
div
className=
{
`min-h-0 flex-1 flex-col ${rightOpen ? 'flex' : 'hidden'}`
}
>
<
div
>
{
selectedNode
?
(
<
Label
>
节点操作
</
Label
>
<
GraphNodeDetails
<
div
className=
"mt-2 grid gap-2"
>
key=
{
selectedNode
.
id
}
<
Button
variant=
"secondary"
onPress=
{
()
=>
toggleCollapsed
(
selectedNode
.
id
)
}
>
{
collapsedIds
.
has
(
selectedNode
.
id
)
?
'展开子节点'
:
'折叠子节点'
}
</
Button
>
node=
{
selectedNode
}
<
Button
isDisabled=
{
archive
!==
'当前版本'
||
saving
||
selectedNode
.
removed
}
variant=
"tertiary"
onPress=
{
()
=>
openAddNode
(
selectedNode
.
id
,
'group'
)
}
><
PlusIcon
size=
{
16
}
/>
添加流程节点
</
Button
>
typeLabel=
{
nodeTypeLabel
(
selectedNode
.
type
)
}
{
selectedNode
.
removed
?
(
status=
{
STATUS_OPTIONS
.
find
((
item
)
=>
item
.
value
===
nodeStatus
(
selectedNode
))
||
{
label
:
nodeTypeLabel
(
selectedNode
.
type
),
color
:
'default'
}
}
<
Button
isDisabled=
{
archive
!==
'当前版本'
||
saving
}
variant=
"secondary"
onPress=
{
restoreSelectedNode
}
>
isCollapsed=
{
collapsedIds
.
has
(
selectedNode
.
id
)
}
<
Reload
size=
{
16
}
/>
{
selectedBranchIds
.
size
>
1
?
'恢复节点及子节点'
:
'恢复节点'
}
isArchived=
{
archive
!==
'当前版本'
}
</
Button
>
isSaving=
{
saving
}
hasDescendants=
{
selectedBranchIds
.
size
>
1
}
onClearSelection=
{
clearNodeSelection
}
onFieldChange=
{
setNodeField
}
onToggleCollapsed=
{
()
=>
toggleCollapsed
(
selectedNode
.
id
)
}
onAddNode=
{
()
=>
openAddNode
(
selectedNode
.
id
,
'group'
)
}
onDeleteNode=
{
deleteSelectedNode
}
onRestoreNode=
{
restoreSelectedNode
}
onOpenRegulation=
{
(
id
)
=>
history
.
push
(
`/internal_regulation/${id}`
)
}
/>
)
:
(
)
:
(
<>
<
div
className=
"flex h-full items-center justify-center p-6 text-center text-sm text-muted"
>
点击中间节点查看编辑和流程信息
</
div
>
<
Button
aria
-
keyshortcuts=
"Delete Backspace"
isDisabled=
{
archive
!==
'当前版本'
||
saving
}
variant=
"danger-soft"
onPress=
{
deleteSelectedNode
}
>
<
Delete
size=
{
16
}
/>
删除节点
</
Button
>
<
p
className=
"text-xs text-muted"
>
{
selectedBranchIds
.
size
>
1
?
'删除会同时移除所有子节点,可在“删除节点”列表恢复。'
:
'选中后可按 Delete(Mac ⌫)删除,可在“删除节点”列表恢复。'
}
</
p
>
</>
)
}
)
}
{
selectedNode
.
relatedRegulation
||
selectedNode
.
relatedReviseRegulation
?
(
<
Button
variant=
"tertiary"
onPress=
{
()
=>
history
.
push
(
`/internal_regulation/${selectedNode.relatedRegulation || selectedNode.relatedReviseRegulation}`
)
}
><
Edit
size=
{
16
}
/>
查看关联制度
</
Button
>
)
:
null
}
</
div
>
</
div
>
</
div
>
<
Tabs
defaultSelectedKey=
"process"
variant=
"primary"
><
Tabs
.
ListContainer
className=
"border-b border-separator"
><
Tabs
.
List
aria
-
label=
"节点信息"
><
Tabs
.
Tab
id=
"process"
>
流程
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
id=
"regulation"
>
制度
<
Tabs
.
Indicator
/></
Tabs
.
Tab
><
Tabs
.
Tab
id=
"history"
>
历史
<
Tabs
.
Indicator
/></
Tabs
.
Tab
></
Tabs
.
List
></
Tabs
.
ListContainer
><
Tabs
.
Panel
className=
"pt-4"
id=
"process"
><
div
className=
"space-y-2 text-sm text-muted"
>
{
selectedNode
.
ancestors
.
filter
((
item
)
=>
item
.
type
===
'groupNode'
).
map
((
item
)
=>
<
div
className=
"flex items-center gap-1"
key=
{
item
.
id
}
><
ChevronRight
size=
{
14
}
/>
{
item
.
title
}
</
div
>)
}{
!
selectedNode
.
ancestors
.
some
((
item
)
=>
item
.
type
===
'groupNode'
)
?
'暂无流程路径'
:
null
}
</
div
></
Tabs
.
Panel
><
Tabs
.
Panel
className=
"pt-4"
id=
"regulation"
><
div
className=
"space-y-2 text-sm text-muted"
>
{
selectedNode
.
ancestors
.
filter
((
item
)
=>
item
.
type
===
'regulationNode'
).
map
((
item
)
=>
<
div
className=
"flex items-center gap-1"
key=
{
item
.
id
}
><
ChevronRight
size=
{
14
}
/>
{
item
.
title
}
</
div
>)
}{
!
selectedNode
.
ancestors
.
some
((
item
)
=>
item
.
type
===
'regulationNode'
)
?
'暂无制度路径'
:
null
}
</
div
></
Tabs
.
Panel
><
Tabs
.
Panel
className=
"pt-4"
id=
"history"
><
div
className=
"space-y-2 text-sm text-muted"
>
{
selectedNode
.
historyRegulation
.
map
((
item
)
=>
<
button
className=
"block text-left text-accent hover:underline"
key=
{
item
.
_id
||
item
.
id
||
item
.
name
}
onClick=
{
()
=>
history
.
push
(
`/internal_regulation/${item._id || item.name}`
)
}
>
{
metadataOf
(
item
).
regulation_name
||
item
.
title
}
</
button
>)
}{
!
selectedNode
.
historyRegulation
.
length
?
'暂无历史版本'
:
null
}
</
div
></
Tabs
.
Panel
></
Tabs
></
div
></
div
></>
:
<
div
className=
"flex h-full items-center justify-center p-6 text-center text-sm text-muted"
>
点击中间节点查看编辑和流程信息
</
div
>
}
</
div
>
</
div
>
</
div
>
</
aside
>
</
aside
>
<
button
aria
-
label=
{
rightOpen
?
'收起右侧节点详情'
:
'展开右侧节点详情'
}
className=
"absolute right-1 top-1/2 z-20 grid size-8 translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-separator bg-background shadow-sm"
onClick=
{
()
=>
setRightOpen
((
value
)
=>
!
value
)
}
><
ChevronRight
className=
{
rightOpen
?
''
:
'rotate-180'
}
size=
{
15
}
/></
button
>
<
button
aria
-
label=
{
rightOpen
?
'收起右侧节点详情'
:
'展开右侧节点详情'
}
className=
"absolute right-1 top-1/2 z-20 grid size-8 translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-separator bg-background shadow-sm"
onClick=
{
()
=>
setRightOpen
((
value
)
=>
!
value
)
}
><
ChevronRight
className=
{
rightOpen
?
''
:
'rotate-180'
}
size=
{
15
}
/></
button
>
...
...
src/pages/internal_regulation_manager/RegulationList.jsx
View file @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
,
Chip
,
Input
,
Label
,
Spinner
,
TextArea
,
TextField
,
toast
}
from
'@heroui/react'
;
import
{
Button
,
Chip
,
Input
,
Label
,
Spinner
,
TextArea
,
TextField
,
toast
}
from
'@heroui/react'
;
import
{
Close
,
Download
,
Edit
,
Eye
,
FileText
,
PlusIcon
,
Search
}
from
'@/components/AppIcons'
;
import
{
Close
,
Download
,
Edit
,
Eye
,
FileText
,
PlusIcon
,
Search
}
from
'@/components/AppIcons'
;
import
ListPagination
from
'@/components/ListPagination'
;
import
ListPagination
from
'@/components/ListPagination'
;
...
@@ -41,7 +43,8 @@ export default function RegulationList({ options = {} }) {
...
@@ -41,7 +43,8 @@ export default function RegulationList({ options = {} }) {
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
totalPages
,
setTotalPages
]
=
useState
(
1
);
const
[
totalPages
,
setTotalPages
]
=
useState
(
1
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
dialogOpen
,
setDialogOpen
]
=
useState
(
false
);
const
[
dialogOpen
,
setDialogOpen
]
=
useState
(
false
);
const
[
form
,
setForm
]
=
useState
(
INITIAL_FORM
);
const
[
form
,
setForm
]
=
useState
(
INITIAL_FORM
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
...
@@ -52,6 +55,8 @@ export default function RegulationList({ options = {} }) {
...
@@ -52,6 +55,8 @@ export default function RegulationList({ options = {} }) {
const
refresh
=
async
()
=>
{
const
refresh
=
async
()
=>
{
try
{
try
{
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
const
activeFilters
=
Object
.
fromEntries
(
Object
.
entries
(
filters
).
filter
(([,
value
])
=>
value
).
map
(([
key
,
value
])
=>
[
`metadata.
${
key
}
`
,
[
'departments'
,
'approve'
].
includes
(
key
)
?
{
$in
:
[
value
]
}
:
value
]));
const
activeFilters
=
Object
.
fromEntries
(
Object
.
entries
(
filters
).
filter
(([,
value
])
=>
value
).
map
(([
key
,
value
])
=>
[
`metadata.
${
key
}
`
,
[
'departments'
,
'approve'
].
includes
(
key
)
?
{
$in
:
[
value
]
}
:
value
]));
const
result
=
await
loadRegulations
({
page
,
pageSize
,
keyword
,
filters
:
activeFilters
});
const
result
=
await
loadRegulations
({
page
,
pageSize
,
keyword
,
filters
:
activeFilters
});
setRecords
(
result
.
records
);
setRecords
(
result
.
records
);
...
@@ -59,6 +64,7 @@ export default function RegulationList({ options = {} }) {
...
@@ -59,6 +64,7 @@ export default function RegulationList({ options = {} }) {
setTotalPages
(
result
.
totalPages
);
setTotalPages
(
result
.
totalPages
);
setError
(
''
);
setError
(
''
);
}
catch
(
loadError
)
{
}
catch
(
loadError
)
{
setLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'制度目录加载失败。'
);
setError
(
loadError
?.
message
||
'制度目录加载失败。'
);
}
finally
{
}
finally
{
setLoading
(
false
);
setLoading
(
false
);
...
@@ -110,7 +116,7 @@ export default function RegulationList({ options = {} }) {
...
@@ -110,7 +116,7 @@ export default function RegulationList({ options = {} }) {
setSaving
(
true
);
setSaving
(
true
);
await
createRevision
(
form
);
await
createRevision
(
form
);
setDialogOpen
(
false
);
setDialogOpen
(
false
);
toast
.
success
(
'制度创建成功'
);
toast
.
success
(
'制度创建成功'
,
{
timeout
:
4000
}
);
await
refresh
();
await
refresh
();
}
catch
(
saveError
)
{
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败,请检查字段后重试。'
);
setError
(
saveError
?.
message
||
'保存失败,请检查字段后重试。'
);
...
@@ -137,7 +143,7 @@ export default function RegulationList({ options = {} }) {
...
@@ -137,7 +143,7 @@ export default function RegulationList({ options = {} }) {
origin_regulation
:
record
.
_id
,
origin_regulation
:
record
.
_id
,
});
});
setConfirmState
({
open
:
false
,
type
:
''
,
record
:
null
});
setConfirmState
({
open
:
false
,
type
:
''
,
record
:
null
});
toast
.
success
(
type
===
'修订'
?
'修订制度创建成功'
:
'废除制度创建成功'
);
toast
.
success
(
type
===
'修订'
?
'修订制度创建成功'
:
'废除制度创建成功'
,
{
timeout
:
4000
}
);
await
refresh
();
await
refresh
();
}
catch
(
saveError
)
{
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败,请检查字段后重试。'
);
setError
(
saveError
?.
message
||
'保存失败,请检查字段后重试。'
);
...
@@ -157,7 +163,7 @@ export default function RegulationList({ options = {} }) {
...
@@ -157,7 +163,7 @@ export default function RegulationList({ options = {} }) {
setSaving
(
true
);
setSaving
(
true
);
await
updateRegulationComment
(
record
,
commentState
.
comment
);
await
updateRegulationComment
(
record
,
commentState
.
comment
);
setCommentState
({
open
:
false
,
record
:
null
,
comment
:
''
});
setCommentState
({
open
:
false
,
record
:
null
,
comment
:
''
});
toast
.
success
(
'已成功添加备注'
);
toast
.
success
(
'已成功添加备注'
,
{
timeout
:
4000
}
);
await
refresh
();
await
refresh
();
}
catch
(
saveError
)
{
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'备注保存失败,请稍后重试。'
);
setError
(
saveError
?.
message
||
'备注保存失败,请稍后重试。'
);
...
@@ -184,8 +190,8 @@ export default function RegulationList({ options = {} }) {
...
@@ -184,8 +190,8 @@ export default function RegulationList({ options = {} }) {
<
XinyuanAutocomplete
className=
"w-52"
label=
"制定部门"
options=
{
[{
value
:
''
,
label
:
'全部'
},
...
departmentOptions
]
}
selectedKey=
{
filters
.
departments
}
onSelectionChange=
{
(
value
)
=>
{
setFilters
((
current
)
=>
({
...
current
,
departments
:
String
(
value
||
''
)
}));
setPage
(
0
);
}
}
/>
<
XinyuanAutocomplete
className=
"w-52"
label=
"制定部门"
options=
{
[{
value
:
''
,
label
:
'全部'
},
...
departmentOptions
]
}
selectedKey=
{
filters
.
departments
}
onSelectionChange=
{
(
value
)
=>
{
setFilters
((
current
)
=>
({
...
current
,
departments
:
String
(
value
||
''
)
}));
setPage
(
0
);
}
}
/>
<
XinyuanAutocomplete
className=
"w-52"
label=
"审批主体"
options=
{
[{
value
:
''
,
label
:
'全部'
},
...
approveOptions
]
}
selectedKey=
{
filters
.
approve
}
onSelectionChange=
{
(
value
)
=>
{
setFilters
((
current
)
=>
({
...
current
,
approve
:
String
(
value
||
''
)
}));
setPage
(
0
);
}
}
/>
<
XinyuanAutocomplete
className=
"w-52"
label=
"审批主体"
options=
{
[{
value
:
''
,
label
:
'全部'
},
...
approveOptions
]
}
selectedKey=
{
filters
.
approve
}
onSelectionChange=
{
(
value
)
=>
{
setFilters
((
current
)
=>
({
...
current
,
approve
:
String
(
value
||
''
)
}));
setPage
(
0
);
}
}
/>
</
div
>
</
div
>
{
error
?
<
div
className=
"rounded-xl border border-danger/30 bg-danger/5 p-3 text-sm text-danger"
>
{
error
}
</
div
>
:
null
}
{
loading
?
<
div
className=
"flex min-h-48 items-center justify-center"
><
Spinner
label=
"正在加载制度..."
/></
div
>
:
records
.
length
===
0
?
<
div
className=
"rounded-2xl border border-dashed border-separator p-10 text-center text-sm text-muted"
>
暂无制度记录
</
div
>
:
(
{
loading
?
<
div
className=
"flex min-h-48 items-center justify-center"
><
Spinner
label=
"正在加载制度..."
/></
div
>
:
loadFailed
?
<
RequestFailure
title=
"制度目录加载失败"
onRetry=
{
refresh
}
/>
:
records
.
length
===
0
?
<
div
className=
"rounded-2xl border border-dashed border-separator p-10 text-center text-sm text-muted"
>
暂无制度记录
</
div
>
:
(
<
div
className=
"overflow-x-auto rounded-2xl border border-separator"
>
<
div
className=
"overflow-x-auto rounded-2xl border border-separator"
>
<
table
className=
"w-full min-w-[1040px] text-left text-sm"
>
<
table
className=
"w-full min-w-[1040px] text-left text-sm"
>
<
thead
className=
"bg-surface-secondary text-muted"
><
tr
>
{
[
'制度名称'
,
'修订状态'
,
'时效性'
,
'效力位阶'
,
'制定部门'
,
'审批主体'
,
'发文日期'
,
'备注'
,
'操作'
].
map
((
title
)
=>
<
th
className=
"px-4 py-3 font-medium"
key=
{
title
}
>
{
title
}
</
th
>)
}
</
tr
></
thead
>
<
thead
className=
"bg-surface-secondary text-muted"
><
tr
>
{
[
'制度名称'
,
'修订状态'
,
'时效性'
,
'效力位阶'
,
'制定部门'
,
'审批主体'
,
'发文日期'
,
'备注'
,
'操作'
].
map
((
title
)
=>
<
th
className=
"px-4 py-3 font-medium"
key=
{
title
}
>
{
title
}
</
th
>)
}
</
tr
></
thead
>
...
@@ -209,7 +215,7 @@ export default function RegulationList({ options = {} }) {
...
@@ -209,7 +215,7 @@ export default function RegulationList({ options = {} }) {
</
table
>
</
table
>
</
div
>
</
div
>
)
}
)
}
<
ListPagination
isDisabled=
{
loading
}
page=
{
page
}
pageSize=
{
pageSize
}
total=
{
total
}
totalPages=
{
totalPages
}
onPageChange=
{
setPage
}
onPageSizeChange=
{
changePageSize
}
/>
<
ListPagination
isDisabled=
{
loading
||
loadFailed
}
page=
{
page
}
pageSize=
{
pageSize
}
total=
{
total
}
totalPages=
{
totalPages
}
onPageChange=
{
setPage
}
onPageSizeChange=
{
changePageSize
}
/>
<
XinyuanModal
isOpen=
{
dialogOpen
}
onOpenChange=
{
setDialogOpen
}
size=
"2xl"
title=
"创建修订计划"
footer=
{
<
div
className=
"flex justify-end gap-2"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
setDialogOpen
(
false
)
}
>
取消
</
Button
><
Button
isDisabled=
{
saving
||
!
form
.
regulation_name
.
trim
()
}
variant=
"primary"
onPress=
{
submit
}
>
{
saving
?
'保存中...'
:
'保存'
}
</
Button
></
div
>
}
>
<
XinyuanModal
isOpen=
{
dialogOpen
}
onOpenChange=
{
setDialogOpen
}
size=
"2xl"
title=
"创建修订计划"
footer=
{
<
div
className=
"flex justify-end gap-2"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
setDialogOpen
(
false
)
}
>
取消
</
Button
><
Button
isDisabled=
{
saving
||
!
form
.
regulation_name
.
trim
()
}
variant=
"primary"
onPress=
{
submit
}
>
{
saving
?
'保存中...'
:
'保存'
}
</
Button
></
div
>
}
>
<
div
className=
"grid gap-4 sm:grid-cols-2"
>
<
div
className=
"grid gap-4 sm:grid-cols-2"
>
<
TextField
className=
"sm:col-span-2"
fullWidth
isRequired
value=
{
form
.
regulation_name
}
onChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
regulation_name
:
value
}))
}
><
Label
>
制度名称
</
Label
><
Input
fullWidth
placeholder=
"请输入制度名称"
variant=
"secondary"
/></
TextField
>
<
TextField
className=
"sm:col-span-2"
fullWidth
isRequired
value=
{
form
.
regulation_name
}
onChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
regulation_name
:
value
}))
}
><
Label
>
制度名称
</
Label
><
Input
fullWidth
placeholder=
"请输入制度名称"
variant=
"secondary"
/></
TextField
>
...
...
src/pages/internal_regulation_manager/RevisionDetail.jsx
View file @
3e6db798
import
{
Button
,
Card
,
Input
,
Label
,
Spinner
,
TextArea
,
TextField
}
from
'@heroui/react'
;
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
,
Card
,
Input
,
Label
,
Spinner
,
TextArea
,
TextField
,
toast
}
from
'@heroui/react'
;
import
{
Back
}
from
'@/components/AppIcons'
;
import
{
Back
}
from
'@/components/AppIcons'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
{
useEffect
,
useState
}
from
'react'
;
import
{
useEffect
,
useState
}
from
'react'
;
...
@@ -12,11 +14,13 @@ export default function RevisionDetail() {
...
@@ -12,11 +14,13 @@ export default function RevisionDetail() {
const
[
form
,
setForm
]
=
useState
({});
const
[
form
,
setForm
]
=
useState
({});
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
useEffect
(()
=>
{
Promise
.
all
([
loadRecord
(
name
),
loadSubjectOptions
(
'internal_regulation'
)]).
then
(([
loaded
,
loadedOptions
])
=>
{
setRecord
(
loaded
);
setForm
(
loaded
?.
metadata
||
{});
setOptions
(
loadedOptions
.
options
);
}).
catch
((
loadError
)
=>
setError
(
loadError
?.
message
||
'修订详情加载失败。'
)).
finally
(()
=>
setLoading
(
false
));
},
[
name
]);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
save
=
async
()
=>
{
try
{
setSaving
(
true
);
await
updateRevision
(
record
.
name
||
name
,
form
);
history
.
push
(
'/internal_regulation_manager?tab=process'
);
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败。'
);
}
finally
{
setSaving
(
false
);
}
};
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
useEffect
(()
=>
{
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
Promise
.
all
([
loadRecord
(
name
),
loadSubjectOptions
(
'internal_regulation'
)]).
then
(([
loaded
,
loadedOptions
])
=>
{
setRecord
(
loaded
);
setForm
(
loaded
?.
metadata
||
{});
setOptions
(
loadedOptions
.
options
);
}).
catch
((
loadError
)
=>
{
setLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'修订详情加载失败。'
);
}).
finally
(()
=>
setLoading
(
false
));
},
[
name
,
reloadKey
,
setError
]);
const
save
=
async
()
=>
{
try
{
setSaving
(
true
);
await
updateRevision
(
record
.
name
||
name
,
form
);
toast
.
success
(
'修订计划已保存。'
,
{
timeout
:
4000
});
history
.
push
(
'/internal_regulation_manager?tab=process'
);
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败。'
);
}
finally
{
setSaving
(
false
);
}
};
if
(
loading
)
return
<
div
className=
"flex min-h-72 items-center justify-center"
><
Spinner
label=
"正在加载修订详情..."
/></
div
>;
if
(
loading
)
return
<
div
className=
"flex min-h-72 items-center justify-center"
><
Spinner
label=
"正在加载修订详情..."
/></
div
>;
if
(
error
||
!
record
)
return
<
div
className=
"space-y-4"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
history
.
back
()
}
><
Back
size=
{
16
}
/>
返回
</
Button
><
div
className=
"rounded-2xl border border-danger/30 bg-danger/5 p-6 text-sm text-danger"
>
{
error
||
'修订计划不存在。'
}
</
div
></
div
>;
if
(
loadFailed
||
!
record
)
return
<
div
className=
"space-y-4"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
history
.
back
()
}
><
Back
size=
{
16
}
/>
返回
</
Button
><
RequestFailure
title=
{
loadFailed
?
'修订详情加载失败'
:
'修订计划不存在'
}
onRetry=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
/
></
div
>;
const
selectOptions
=
(
field
)
=>
(
options
[
field
]
||
[]).
map
((
item
)
=>
({
value
:
item
.
value
??
item
.
label
??
item
,
label
:
item
.
label
??
item
.
value
??
item
}));
const
selectOptions
=
(
field
)
=>
(
options
[
field
]
||
[]).
map
((
item
)
=>
({
value
:
item
.
value
??
item
.
label
??
item
,
label
:
item
.
label
??
item
.
value
??
item
}));
return
<
div
className=
"mx-auto max-w-4xl space-y-5"
><
div
className=
"flex items-center justify-between"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
history
.
push
(
'/internal_regulation_manager?tab=process'
)
}
><
Back
size=
{
16
}
/>
返回修订计划
</
Button
><
Button
isDisabled=
{
saving
||
!
form
.
regulation_name
}
variant=
"primary"
onPress=
{
save
}
>
{
saving
?
'保存中...'
:
'保存'
}
</
Button
></
div
><
Card
className=
"rc-card-surface"
><
Card
.
Header
><
Card
.
Title
>
编辑修订计划
</
Card
.
Title
></
Card
.
Header
><
Card
.
Content
><
div
className=
"grid gap-4 sm:grid-cols-2"
><
TextField
className=
"sm:col-span-2"
fullWidth
isRequired
value=
{
form
.
regulation_name
||
''
}
onChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
regulation_name
:
value
}))
}
><
Label
>
制度名称
</
Label
><
Input
fullWidth
variant=
"secondary"
/></
TextField
><
XinyuanAutocomplete
label=
"修订类型"
options=
{
[
'新增'
,
'修订'
,
'废止'
].
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
form
.
type
||
''
}
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
type
:
String
(
value
)
}))
}
/><
XinyuanAutocomplete
label=
"效力位阶"
options=
{
REGULATION_LEVELS
.
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
form
.
level
||
''
}
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
level
:
String
(
value
)
}))
}
/><
XinyuanAutocomplete
label=
"审批主体"
options=
{
selectOptions
(
'approve'
)
}
selectedKeys=
{
new
Set
(
form
.
approve
||
[])
}
selectionMode=
"multiple"
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
approve
:
value
===
'all'
?
selectOptions
(
'approve'
).
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
value
||
[])
}))
}
/><
XinyuanAutocomplete
label=
"制定部门"
options=
{
selectOptions
(
'departments'
)
}
selectedKeys=
{
new
Set
(
form
.
departments
||
[])
}
selectionMode=
"multiple"
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
departments
:
value
===
'all'
?
selectOptions
(
'departments'
).
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
value
||
[])
}))
}
/><
TextField
className=
"sm:col-span-2"
fullWidth
value=
{
form
.
comment
||
''
}
onChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
comment
:
value
}))
}
><
Label
>
备注
</
Label
><
TextArea
fullWidth
rows=
{
6
}
variant=
"secondary"
/></
TextField
></
div
></
Card
.
Content
></
Card
></
div
>;
return
<
div
className=
"mx-auto max-w-4xl space-y-5"
><
div
className=
"flex items-center justify-between"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
history
.
push
(
'/internal_regulation_manager?tab=process'
)
}
><
Back
size=
{
16
}
/>
返回修订计划
</
Button
><
Button
isDisabled=
{
saving
||
!
form
.
regulation_name
}
variant=
"primary"
onPress=
{
save
}
>
{
saving
?
'保存中...'
:
'保存'
}
</
Button
></
div
><
Card
className=
"rc-card-surface"
><
Card
.
Header
><
Card
.
Title
>
编辑修订计划
</
Card
.
Title
></
Card
.
Header
><
Card
.
Content
><
div
className=
"grid gap-4 sm:grid-cols-2"
><
TextField
className=
"sm:col-span-2"
fullWidth
isRequired
value=
{
form
.
regulation_name
||
''
}
onChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
regulation_name
:
value
}))
}
><
Label
>
制度名称
</
Label
><
Input
fullWidth
variant=
"secondary"
/></
TextField
><
XinyuanAutocomplete
label=
"修订类型"
options=
{
[
'新增'
,
'修订'
,
'废止'
].
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
form
.
type
||
''
}
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
type
:
String
(
value
)
}))
}
/><
XinyuanAutocomplete
label=
"效力位阶"
options=
{
REGULATION_LEVELS
.
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
form
.
level
||
''
}
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
level
:
String
(
value
)
}))
}
/><
XinyuanAutocomplete
label=
"审批主体"
options=
{
selectOptions
(
'approve'
)
}
selectedKeys=
{
new
Set
(
form
.
approve
||
[])
}
selectionMode=
"multiple"
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
approve
:
value
===
'all'
?
selectOptions
(
'approve'
).
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
value
||
[])
}))
}
/><
XinyuanAutocomplete
label=
"制定部门"
options=
{
selectOptions
(
'departments'
)
}
selectedKeys=
{
new
Set
(
form
.
departments
||
[])
}
selectionMode=
"multiple"
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
departments
:
value
===
'all'
?
selectOptions
(
'departments'
).
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
value
||
[])
}))
}
/><
TextField
className=
"sm:col-span-2"
fullWidth
value=
{
form
.
comment
||
''
}
onChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
comment
:
value
}))
}
><
Label
>
备注
</
Label
><
TextArea
fullWidth
rows=
{
6
}
variant=
"secondary"
/></
TextField
></
div
></
Card
.
Content
></
Card
></
div
>;
}
}
src/pages/internal_regulation_manager/RevisionList.jsx
View file @
3e6db798
import
{
Button
,
Chip
,
Input
,
Label
,
Spinner
,
TextArea
,
TextField
}
from
'@heroui/react'
;
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
,
Chip
,
Input
,
Label
,
Spinner
,
TextArea
,
TextField
,
toast
}
from
'@heroui/react'
;
import
{
Archive
,
Check
,
Close
,
Download
,
Edit
,
FileText
,
PaperClipIcon
,
Search
,
Upload
}
from
'@/components/AppIcons'
;
import
{
Archive
,
Check
,
Close
,
Download
,
Edit
,
FileText
,
PaperClipIcon
,
Search
,
Upload
}
from
'@/components/AppIcons'
;
import
ListPagination
from
'@/components/ListPagination'
;
import
ListPagination
from
'@/components/ListPagination'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
...
@@ -45,7 +47,8 @@ export default function RevisionList({ options = {} }) {
...
@@ -45,7 +47,8 @@ export default function RevisionList({ options = {} }) {
const
[
pageSize
,
setPageSize
]
=
useState
(
20
);
const
[
pageSize
,
setPageSize
]
=
useState
(
20
);
const
[
totalPages
,
setTotalPages
]
=
useState
(
1
);
const
[
totalPages
,
setTotalPages
]
=
useState
(
1
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
editing
,
setEditing
]
=
useState
(
null
);
const
[
editing
,
setEditing
]
=
useState
(
null
);
const
[
editOpen
,
setEditOpen
]
=
useState
(
false
);
const
[
editOpen
,
setEditOpen
]
=
useState
(
false
);
const
[
publishOpen
,
setPublishOpen
]
=
useState
(
false
);
const
[
publishOpen
,
setPublishOpen
]
=
useState
(
false
);
...
@@ -58,12 +61,15 @@ export default function RevisionList({ options = {} }) {
...
@@ -58,12 +61,15 @@ export default function RevisionList({ options = {} }) {
const
load
=
async
()
=>
{
const
load
=
async
()
=>
{
try
{
try
{
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
const
result
=
await
loadRevisions
({
status
,
keyword
,
page
,
pageSize
});
const
result
=
await
loadRevisions
({
status
,
keyword
,
page
,
pageSize
});
setRecords
(
result
.
records
);
setRecords
(
result
.
records
);
setTotal
(
result
.
total
);
setTotal
(
result
.
total
);
setTotalPages
(
result
.
totalPages
);
setTotalPages
(
result
.
totalPages
);
setError
(
''
);
setError
(
''
);
}
catch
(
loadError
)
{
}
catch
(
loadError
)
{
setLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'修订计划加载失败。'
);
setError
(
loadError
?.
message
||
'修订计划加载失败。'
);
}
finally
{
setLoading
(
false
);
}
}
finally
{
setLoading
(
false
);
}
};
};
...
@@ -84,18 +90,18 @@ export default function RevisionList({ options = {} }) {
...
@@ -84,18 +90,18 @@ export default function RevisionList({ options = {} }) {
};
};
const
saveEdit
=
async
()
=>
{
const
saveEdit
=
async
()
=>
{
if
(
!
form
.
regulation_name
.
trim
())
return
;
if
(
!
form
.
regulation_name
.
trim
())
return
;
try
{
setSaving
(
true
);
if
(
editing
)
await
updateRevision
(
editing
.
name
,
form
);
else
await
createRevision
(
form
);
setEditOpen
(
false
);
await
load
();
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败。'
);
}
finally
{
setSaving
(
false
);
}
try
{
setSaving
(
true
);
if
(
editing
)
await
updateRevision
(
editing
.
name
,
form
);
else
await
createRevision
(
form
);
setEditOpen
(
false
);
toast
.
success
(
'修订计划已保存。'
,
{
timeout
:
4000
});
await
load
();
}
catch
(
saveError
)
{
setError
(
saveError
?.
message
||
'保存失败。'
);
}
finally
{
setSaving
(
false
);
}
};
};
const
submitStatus
=
async
(
record
,
nextStatus
)
=>
{
const
submitStatus
=
async
(
record
,
nextStatus
)
=>
{
try
{
setSaving
(
true
);
await
setRevisionStatus
(
record
.
name
,
nextStatus
);
await
load
();
}
catch
(
actionError
)
{
setError
(
actionError
?.
message
||
'状态更新失败。'
);
}
finally
{
setSaving
(
false
);
}
try
{
setSaving
(
true
);
await
setRevisionStatus
(
record
.
name
,
nextStatus
);
toast
.
success
(
'修订状态已更新。'
,
{
timeout
:
4000
});
await
load
();
}
catch
(
actionError
)
{
setError
(
actionError
?.
message
||
'状态更新失败。'
);
}
finally
{
setSaving
(
false
);
}
};
};
const
handlePublish
=
async
()
=>
{
const
handlePublish
=
async
()
=>
{
if
(
!
editing
||
!
publishForm
.
public_date
)
return
;
if
(
!
editing
||
!
publishForm
.
public_date
)
return
;
try
{
setSaving
(
true
);
await
publishRevision
(
editing
,
publishForm
);
setPublishOpen
(
false
);
await
load
();
}
catch
(
publishError
)
{
setError
(
publishError
?.
message
||
'发文失败。'
);
}
finally
{
setSaving
(
false
);
}
try
{
setSaving
(
true
);
await
publishRevision
(
editing
,
publishForm
);
setPublishOpen
(
false
);
toast
.
success
(
'发文成功。'
,
{
timeout
:
4000
});
await
load
();
}
catch
(
publishError
)
{
setError
(
publishError
?.
message
||
'发文失败。'
);
}
finally
{
setSaving
(
false
);
}
};
};
const
handleArchive
=
async
()
=>
{
const
handleArchive
=
async
()
=>
{
if
(
!
archiveTag
.
trim
())
return
;
if
(
!
archiveTag
.
trim
())
return
;
try
{
setSaving
(
true
);
await
archivePublishedRegulations
(
archiveTag
.
trim
());
setArchiveOpen
(
false
);
setArchiveTag
(
''
);
}
catch
(
archiveError
)
{
setError
(
archiveError
?.
message
||
'归档失败。'
);
}
finally
{
setSaving
(
false
);
}
try
{
setSaving
(
true
);
await
archivePublishedRegulations
(
archiveTag
.
trim
());
setArchiveOpen
(
false
);
setArchiveTag
(
''
);
toast
.
success
(
'现行有效制度已归档。'
,
{
timeout
:
4000
});
}
catch
(
archiveError
)
{
setError
(
archiveError
?.
message
||
'归档失败。'
);
}
finally
{
setSaving
(
false
);
}
};
};
const
onUpload
=
async
(
event
)
=>
{
const
onUpload
=
async
(
event
)
=>
{
const
file
=
event
.
target
.
files
?.[
0
];
const
file
=
event
.
target
.
files
?.[
0
];
...
@@ -105,6 +111,7 @@ export default function RevisionList({ options = {} }) {
...
@@ -105,6 +111,7 @@ export default function RevisionList({ options = {} }) {
const
body
=
new
FormData
();
body
.
set
(
'title'
,
file
.
name
.
replace
(
/
\.[^
.
]
+$/
,
''
));
body
.
append
(
'file'
,
file
);
const
body
=
new
FormData
();
body
.
set
(
'title'
,
file
.
name
.
replace
(
/
\.[^
.
]
+$/
,
''
));
body
.
append
(
'file'
,
file
);
const
uploaded
=
await
createDocument
(
body
);
const
uploaded
=
await
createDocument
(
body
);
setPublishForm
((
current
)
=>
({
...
current
,
attachments
:
[...
current
.
attachments
,
uploaded
]
}));
setPublishForm
((
current
)
=>
({
...
current
,
attachments
:
[...
current
.
attachments
,
uploaded
]
}));
toast
.
success
(
'附件上传成功。'
,
{
timeout
:
4000
});
}
catch
(
uploadError
)
{
setError
(
uploadError
?.
message
||
'附件上传失败。'
);
}
finally
{
setSaving
(
false
);
}
}
catch
(
uploadError
)
{
setError
(
uploadError
?.
message
||
'附件上传失败。'
);
}
finally
{
setSaving
(
false
);
}
};
};
const
exportList
=
async
()
=>
{
const
exportList
=
async
()
=>
{
...
@@ -132,11 +139,11 @@ export default function RevisionList({ options = {} }) {
...
@@ -132,11 +139,11 @@ export default function RevisionList({ options = {} }) {
</
div
>
</
div
>
<
div
className=
"flex w-full max-w-xl items-end gap-2"
><
div
className=
"min-w-0 flex-1"
><
TextField
fullWidth
value=
{
searchText
}
onChange=
{
setSearchText
}
onKeyDown=
{
(
event
)
=>
{
if
(
event
.
key
===
'Enter'
)
{
event
.
preventDefault
();
submitSearch
();
}
}
}
><
Label
className=
"sr-only"
>
搜索修订计划
</
Label
><
div
className=
"relative"
><
Search
className=
"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted"
size=
{
16
}
/><
Input
className=
"pl-9"
fullWidth
placeholder=
"搜索制度名称"
variant=
"secondary"
/></
div
></
TextField
></
div
><
Button
isDisabled=
{
loading
}
variant=
"secondary"
onPress=
{
submitSearch
}
>
查询
</
Button
><
Button
variant=
"tertiary"
onPress=
{
exportList
}
><
Download
size=
{
16
}
/>
下载
</
Button
><
Button
variant=
"secondary"
onPress=
{
()
=>
setArchiveOpen
(
true
)
}
><
Archive
size=
{
16
}
/>
归档
</
Button
></
div
>
<
div
className=
"flex w-full max-w-xl items-end gap-2"
><
div
className=
"min-w-0 flex-1"
><
TextField
fullWidth
value=
{
searchText
}
onChange=
{
setSearchText
}
onKeyDown=
{
(
event
)
=>
{
if
(
event
.
key
===
'Enter'
)
{
event
.
preventDefault
();
submitSearch
();
}
}
}
><
Label
className=
"sr-only"
>
搜索修订计划
</
Label
><
div
className=
"relative"
><
Search
className=
"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted"
size=
{
16
}
/><
Input
className=
"pl-9"
fullWidth
placeholder=
"搜索制度名称"
variant=
"secondary"
/></
div
></
TextField
></
div
><
Button
isDisabled=
{
loading
}
variant=
"secondary"
onPress=
{
submitSearch
}
>
查询
</
Button
><
Button
variant=
"tertiary"
onPress=
{
exportList
}
><
Download
size=
{
16
}
/>
下载
</
Button
><
Button
variant=
"secondary"
onPress=
{
()
=>
setArchiveOpen
(
true
)
}
><
Archive
size=
{
16
}
/>
归档
</
Button
></
div
>
</
div
>
</
div
>
{
error
?
<
div
className=
"rounded-xl border border-danger/30 bg-danger/5 p-3 text-sm text-danger"
>
{
error
}
</
div
>
:
null
}
{
loading
?
<
div
className=
"flex min-h-48 items-center justify-center"
><
Spinner
label=
"正在加载修订计划..."
/></
div
>
:
records
.
length
===
0
?
<
div
className=
"rounded-2xl border border-dashed border-separator p-10 text-center text-sm text-muted"
>
当前状态暂无修订计划
</
div
>
:
(
{
loading
?
<
div
className=
"flex min-h-48 items-center justify-center"
><
Spinner
label=
"正在加载修订计划..."
/></
div
>
:
loadFailed
?
<
RequestFailure
title=
"修订计划加载失败"
onRetry=
{
load
}
/>
:
records
.
length
===
0
?
<
div
className=
"rounded-2xl border border-dashed border-separator p-10 text-center text-sm text-muted"
>
当前状态暂无修订计划
</
div
>
:
(
<
div
className=
"overflow-x-auto rounded-2xl border border-separator"
><
table
className=
"w-full min-w-[1120px] text-left text-sm"
><
thead
className=
"bg-surface-secondary text-muted"
><
tr
>
{
[
'类型'
,
'制度名称'
,
'OA流程制度'
,
'发起日期'
,
'审批主体'
,
'制定部门'
,
'附件'
,
'状态'
,
'操作'
].
map
((
title
)
=>
<
th
className=
"px-4 py-3 font-medium"
key=
{
title
}
>
{
title
}
</
th
>)
}
</
tr
></
thead
><
tbody
className=
"divide-y divide-separator"
>
{
records
.
map
((
record
)
=>
{
const
metadata
=
record
.
metadata
||
{};
const
attachments
=
Array
.
isArray
(
metadata
.
attachments
)
?
metadata
.
attachments
:
metadata
.
attachments
?
[
metadata
.
attachments
]
:
[];
return
<
tr
className=
"hover:bg-surface-secondary/50"
key=
{
record
.
_id
||
record
.
name
}
><
td
className=
"px-4 py-3"
><
Chip
color=
{
metadata
.
type
===
'修订'
?
'warning'
:
metadata
.
type
===
'废止'
?
'default'
:
'accent'
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
待
{
metadata
.
type
||
'-'
}
</
Chip
.
Label
></
Chip
></
td
><
td
className=
"px-4 py-3 font-medium"
>
{
metadata
.
regulation_name
||
record
.
title
||
'-'
}
</
td
><
td
className=
"max-w-48 truncate px-4 py-3 text-muted"
>
{
metadata
.
regulation_name_oa
||
metadata
.
regulation_name_oa_submitted
||
'-'
}
</
td
><
td
className=
"px-4 py-3"
>
{
formatDate
(
metadata
.
created_at
)
}
</
td
><
td
className=
"max-w-48 truncate px-4 py-3"
>
{
Array
.
isArray
(
metadata
.
approve
)
?
metadata
.
approve
.
join
(
'、'
)
:
metadata
.
approve
||
'-'
}
</
td
><
td
className=
"max-w-48 truncate px-4 py-3"
>
{
Array
.
isArray
(
metadata
.
departments
)
?
metadata
.
departments
.
join
(
'、'
)
:
metadata
.
departments
||
'-'
}
</
td
><
td
className=
"px-4 py-3"
>
{
attachments
.
length
?
<
div
className=
"flex flex-col gap-1"
>
{
attachments
.
map
((
attachment
,
index
)
=>
{
const
href
=
typeof
attachment
===
'string'
?
attachment
:
attachment
.
url
;
const
title
=
typeof
attachment
===
'string'
?
`附件 ${index + 1}`
:
attachment
.
title
||
attachment
.
name
||
`附件 ${index + 1}`
;
return
href
?
<
a
className=
"flex items-center gap-1 text-accent hover:underline"
href=
{
href
}
key=
{
attachment
.
_id
||
attachment
.
name
||
index
}
rel=
"noreferrer"
target=
"_blank"
><
PaperClipIcon
size=
{
14
}
/>
{
title
}
</
a
>
:
<
span
className=
"text-muted"
key=
{
attachment
.
_id
||
attachment
.
name
||
index
}
>
{
title
}
</
span
>;
})
}
</
div
>
:
'-'
}
</
td
><
td
className=
"px-4 py-3"
><
Chip
color=
{
statusColor
(
metadata
.
status
)
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
metadata
.
status
}
</
Chip
.
Label
></
Chip
></
td
><
td
className=
"px-4 py-3"
><
div
className=
"flex items-center gap-1"
>
{
status
===
'起草中'
?
<><
Button
isIconOnly
aria
-
label=
"取消修订"
isDisabled=
{
saving
}
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
submitStatus
(
record
,
'已取消'
)
}
><
Close
size=
{
16
}
/></
Button
><
Button
isIconOnly
aria
-
label=
"提交审核"
isDisabled=
{
saving
}
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
submitStatus
(
record
,
'已提交'
)
}
><
Check
size=
{
16
}
/></
Button
><
Button
isIconOnly
aria
-
label=
"编辑修订"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
openEdit
(
record
)
}
><
Edit
size=
{
16
}
/></
Button
></>
:
null
}{
status
===
'已提交'
?
<><
Button
isIconOnly
aria
-
label=
"取消审核"
isDisabled=
{
saving
}
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
submitStatus
(
record
,
'已取消'
)
}
><
Close
size=
{
16
}
/></
Button
><
Button
isIconOnly
aria
-
label=
"提交发文"
isDisabled=
{
saving
}
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
{
setEditing
(
record
);
setPublishForm
({
public_date
:
''
,
attachments
:
[],
content
:
''
});
setPublishOpen
(
true
);
}
}
><
FileText
size=
{
16
}
/></
Button
></>
:
null
}{
status
===
'已取消'
?
<
Button
isIconOnly
aria
-
label=
"查看修订"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
openEdit
(
record
)
}
><
Edit
size=
{
16
}
/></
Button
>
:
null
}
</
div
></
td
></
tr
>;
})
}
</
tbody
></
table
></
div
>
<
div
className=
"overflow-x-auto rounded-2xl border border-separator"
><
table
className=
"w-full min-w-[1120px] text-left text-sm"
><
thead
className=
"bg-surface-secondary text-muted"
><
tr
>
{
[
'类型'
,
'制度名称'
,
'OA流程制度'
,
'发起日期'
,
'审批主体'
,
'制定部门'
,
'附件'
,
'状态'
,
'操作'
].
map
((
title
)
=>
<
th
className=
"px-4 py-3 font-medium"
key=
{
title
}
>
{
title
}
</
th
>)
}
</
tr
></
thead
><
tbody
className=
"divide-y divide-separator"
>
{
records
.
map
((
record
)
=>
{
const
metadata
=
record
.
metadata
||
{};
const
attachments
=
Array
.
isArray
(
metadata
.
attachments
)
?
metadata
.
attachments
:
metadata
.
attachments
?
[
metadata
.
attachments
]
:
[];
return
<
tr
className=
"hover:bg-surface-secondary/50"
key=
{
record
.
_id
||
record
.
name
}
><
td
className=
"px-4 py-3"
><
Chip
color=
{
metadata
.
type
===
'修订'
?
'warning'
:
metadata
.
type
===
'废止'
?
'default'
:
'accent'
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
待
{
metadata
.
type
||
'-'
}
</
Chip
.
Label
></
Chip
></
td
><
td
className=
"px-4 py-3 font-medium"
>
{
metadata
.
regulation_name
||
record
.
title
||
'-'
}
</
td
><
td
className=
"max-w-48 truncate px-4 py-3 text-muted"
>
{
metadata
.
regulation_name_oa
||
metadata
.
regulation_name_oa_submitted
||
'-'
}
</
td
><
td
className=
"px-4 py-3"
>
{
formatDate
(
metadata
.
created_at
)
}
</
td
><
td
className=
"max-w-48 truncate px-4 py-3"
>
{
Array
.
isArray
(
metadata
.
approve
)
?
metadata
.
approve
.
join
(
'、'
)
:
metadata
.
approve
||
'-'
}
</
td
><
td
className=
"max-w-48 truncate px-4 py-3"
>
{
Array
.
isArray
(
metadata
.
departments
)
?
metadata
.
departments
.
join
(
'、'
)
:
metadata
.
departments
||
'-'
}
</
td
><
td
className=
"px-4 py-3"
>
{
attachments
.
length
?
<
div
className=
"flex flex-col gap-1"
>
{
attachments
.
map
((
attachment
,
index
)
=>
{
const
href
=
typeof
attachment
===
'string'
?
attachment
:
attachment
.
url
;
const
title
=
typeof
attachment
===
'string'
?
`附件 ${index + 1}`
:
attachment
.
title
||
attachment
.
name
||
`附件 ${index + 1}`
;
return
href
?
<
a
className=
"flex items-center gap-1 text-accent hover:underline"
href=
{
href
}
key=
{
attachment
.
_id
||
attachment
.
name
||
index
}
rel=
"noreferrer"
target=
"_blank"
><
PaperClipIcon
size=
{
14
}
/>
{
title
}
</
a
>
:
<
span
className=
"text-muted"
key=
{
attachment
.
_id
||
attachment
.
name
||
index
}
>
{
title
}
</
span
>;
})
}
</
div
>
:
'-'
}
</
td
><
td
className=
"px-4 py-3"
><
Chip
color=
{
statusColor
(
metadata
.
status
)
}
size=
"sm"
variant=
"soft"
><
Chip
.
Label
>
{
metadata
.
status
}
</
Chip
.
Label
></
Chip
></
td
><
td
className=
"px-4 py-3"
><
div
className=
"flex items-center gap-1"
>
{
status
===
'起草中'
?
<><
Button
isIconOnly
aria
-
label=
"取消修订"
isDisabled=
{
saving
}
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
submitStatus
(
record
,
'已取消'
)
}
><
Close
size=
{
16
}
/></
Button
><
Button
isIconOnly
aria
-
label=
"提交审核"
isDisabled=
{
saving
}
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
submitStatus
(
record
,
'已提交'
)
}
><
Check
size=
{
16
}
/></
Button
><
Button
isIconOnly
aria
-
label=
"编辑修订"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
openEdit
(
record
)
}
><
Edit
size=
{
16
}
/></
Button
></>
:
null
}{
status
===
'已提交'
?
<><
Button
isIconOnly
aria
-
label=
"取消审核"
isDisabled=
{
saving
}
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
submitStatus
(
record
,
'已取消'
)
}
><
Close
size=
{
16
}
/></
Button
><
Button
isIconOnly
aria
-
label=
"提交发文"
isDisabled=
{
saving
}
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
{
setEditing
(
record
);
setPublishForm
({
public_date
:
''
,
attachments
:
[],
content
:
''
});
setPublishOpen
(
true
);
}
}
><
FileText
size=
{
16
}
/></
Button
></>
:
null
}{
status
===
'已取消'
?
<
Button
isIconOnly
aria
-
label=
"查看修订"
size=
"sm"
variant=
"tertiary"
onPress=
{
()
=>
openEdit
(
record
)
}
><
Edit
size=
{
16
}
/></
Button
>
:
null
}
</
div
></
td
></
tr
>;
})
}
</
tbody
></
table
></
div
>
)
}
)
}
<
ListPagination
isDisabled=
{
loading
}
page=
{
page
}
pageSize=
{
pageSize
}
total=
{
total
}
totalPages=
{
totalPages
}
onPageChange=
{
setPage
}
onPageSizeChange=
{
changePageSize
}
/>
<
ListPagination
isDisabled=
{
loading
||
loadFailed
}
page=
{
page
}
pageSize=
{
pageSize
}
total=
{
total
}
totalPages=
{
totalPages
}
onPageChange=
{
setPage
}
onPageSizeChange=
{
changePageSize
}
/>
<
XinyuanModal
isOpen=
{
editOpen
}
onOpenChange=
{
setEditOpen
}
size=
"2xl"
title=
{
editing
?
'编辑修订计划'
:
'创建修订计划'
}
footer=
{
<
div
className=
"flex justify-end gap-2"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
setEditOpen
(
false
)
}
>
取消
</
Button
><
Button
isDisabled=
{
saving
||
!
form
.
regulation_name
.
trim
()
}
variant=
"primary"
onPress=
{
saveEdit
}
>
{
saving
?
'保存中...'
:
'保存'
}
</
Button
></
div
>
}
>
<
XinyuanModal
isOpen=
{
editOpen
}
onOpenChange=
{
setEditOpen
}
size=
"2xl"
title=
{
editing
?
'编辑修订计划'
:
'创建修订计划'
}
footer=
{
<
div
className=
"flex justify-end gap-2"
><
Button
variant=
"tertiary"
onPress=
{
()
=>
setEditOpen
(
false
)
}
>
取消
</
Button
><
Button
isDisabled=
{
saving
||
!
form
.
regulation_name
.
trim
()
}
variant=
"primary"
onPress=
{
saveEdit
}
>
{
saving
?
'保存中...'
:
'保存'
}
</
Button
></
div
>
}
>
<
div
className=
"grid gap-4 sm:grid-cols-2"
><
TextField
className=
"sm:col-span-2"
fullWidth
isRequired
value=
{
form
.
regulation_name
}
onChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
regulation_name
:
value
}))
}
><
Label
>
制度名称
</
Label
><
Input
fullWidth
variant=
"secondary"
/></
TextField
><
XinyuanAutocomplete
label=
"类型"
options=
{
[
'新增'
,
'修订'
,
'废止'
].
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
form
.
type
}
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
type
:
String
(
value
)
}))
}
/><
XinyuanAutocomplete
label=
"层级"
options=
{
REGULATION_LEVELS
.
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
form
.
level
}
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
level
:
String
(
value
)
}))
}
/><
XinyuanAutocomplete
label=
"审批主体"
options=
{
optionsFor
(
'approve'
)
}
selectedKeys=
{
new
Set
(
form
.
approve
||
[])
}
selectionMode=
"multiple"
onSelectionChange=
{
(
values
)
=>
setForm
((
current
)
=>
({
...
current
,
approve
:
values
===
'all'
?
optionsFor
(
'approve'
).
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
values
||
[])
}))
}
/><
XinyuanAutocomplete
label=
"制定部门"
options=
{
optionsFor
(
'departments'
)
}
selectedKeys=
{
new
Set
(
form
.
departments
||
[])
}
selectionMode=
"multiple"
onSelectionChange=
{
(
values
)
=>
setForm
((
current
)
=>
({
...
current
,
departments
:
values
===
'all'
?
optionsFor
(
'departments'
).
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
values
||
[])
}))
}
/><
TextField
className=
"sm:col-span-2"
fullWidth
value=
{
form
.
comment
}
onChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
comment
:
value
}))
}
><
Label
>
备注
</
Label
><
TextArea
fullWidth
rows=
{
4
}
variant=
"secondary"
/></
TextField
></
div
>
<
div
className=
"grid gap-4 sm:grid-cols-2"
><
TextField
className=
"sm:col-span-2"
fullWidth
isRequired
value=
{
form
.
regulation_name
}
onChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
regulation_name
:
value
}))
}
><
Label
>
制度名称
</
Label
><
Input
fullWidth
variant=
"secondary"
/></
TextField
><
XinyuanAutocomplete
label=
"类型"
options=
{
[
'新增'
,
'修订'
,
'废止'
].
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
form
.
type
}
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
type
:
String
(
value
)
}))
}
/><
XinyuanAutocomplete
label=
"层级"
options=
{
REGULATION_LEVELS
.
map
((
value
)
=>
({
value
,
label
:
value
}))
}
selectedKey=
{
form
.
level
}
onSelectionChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
level
:
String
(
value
)
}))
}
/><
XinyuanAutocomplete
label=
"审批主体"
options=
{
optionsFor
(
'approve'
)
}
selectedKeys=
{
new
Set
(
form
.
approve
||
[])
}
selectionMode=
"multiple"
onSelectionChange=
{
(
values
)
=>
setForm
((
current
)
=>
({
...
current
,
approve
:
values
===
'all'
?
optionsFor
(
'approve'
).
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
values
||
[])
}))
}
/><
XinyuanAutocomplete
label=
"制定部门"
options=
{
optionsFor
(
'departments'
)
}
selectedKeys=
{
new
Set
(
form
.
departments
||
[])
}
selectionMode=
"multiple"
onSelectionChange=
{
(
values
)
=>
setForm
((
current
)
=>
({
...
current
,
departments
:
values
===
'all'
?
optionsFor
(
'departments'
).
map
((
item
)
=>
item
.
value
)
:
Array
.
from
(
values
||
[])
}))
}
/><
TextField
className=
"sm:col-span-2"
fullWidth
value=
{
form
.
comment
}
onChange=
{
(
value
)
=>
setForm
((
current
)
=>
({
...
current
,
comment
:
value
}))
}
><
Label
>
备注
</
Label
><
TextArea
fullWidth
rows=
{
4
}
variant=
"secondary"
/></
TextField
></
div
>
...
...
src/pages/internal_regulation_manager/internal-regulation.business-flow.md
View file @
3e6db798
...
@@ -40,16 +40,20 @@
...
@@ -40,16 +40,20 @@
-
“全部 / 待分类”列表首次点击制度记录时定位第一个关联节点,连续点击同一条记录时按图谱节点顺序循环定位(A → B → C → A);只有一个关联节点时保持定位该节点。记录
`_id/name`
的直接关联节点合并后参与循环,只有没有直接关联时才按
`metadata.origin_regulation`
查找。优先在未移除节点中循环;全部关联节点已移除时才在已移除节点中循环,并自动开启“显示已移除”。点击其他制度、选中画布或“节点 / 删除节点”列表、重新加载或切换拓扑版本后,再点击该制度从第一个关联节点重新开始。循环过程仍只高亮点击的制度记录,自动展开目标祖先、保持缩放并打开详情;快速连续点击时取消尚未执行的旧定位回调,画布最终定位到最新选中的节点。
-
“全部 / 待分类”列表首次点击制度记录时定位第一个关联节点,连续点击同一条记录时按图谱节点顺序循环定位(A → B → C → A);只有一个关联节点时保持定位该节点。记录
`_id/name`
的直接关联节点合并后参与循环,只有没有直接关联时才按
`metadata.origin_regulation`
查找。优先在未移除节点中循环;全部关联节点已移除时才在已移除节点中循环,并自动开启“显示已移除”。点击其他制度、选中画布或“节点 / 删除节点”列表、重新加载或切换拓扑版本后,再点击该制度从第一个关联节点重新开始。循环过程仍只高亮点击的制度记录,自动展开目标祖先、保持缩放并打开详情;快速连续点击时取消尚未执行的旧定位回调,画布最终定位到最新选中的节点。
-
当前所查看的拓扑版本没有关联节点时,点击制度记录显示 HeroUI 提示“该制度在当前拓扑图中暂无关联节点。”,保持画布、筛选和已选节点状态;待分类列表仍支持拖拽归类。
-
当前所查看的拓扑版本没有关联节点时,点击制度记录显示 HeroUI 提示“该制度在当前拓扑图中暂无关联节点。”,保持画布、筛选和已选节点状态;待分类列表仍支持拖拽归类。
-
画布内单击节点只选中节点并打开右侧详情,保持当前缩放和画布位置,不自动放大或居中;左侧列表点击和拖入制度后的定位可平移到目标节点,但保持操作前的缩放比例。“恢复视图”及缩放控件仍由用户主动调整视角。
-
画布内单击节点只选中节点并打开右侧详情,保持当前缩放和画布位置,不自动放大或居中;左侧列表点击和拖入制度后的定位可平移到目标节点,但保持操作前的缩放比例。“恢复视图”及缩放控件仍由用户主动调整视角。
-
当前版本选中节点后,可按
`Delete`
(兼容 Mac 的
`⌫ / Backspace`
)或点击右侧详情
“节点操作 →
删除节点”。删除按节点 ID 将该节点及全部下级节点的
`removed`
、
`data.removed`
设为
`true`
,保留节点引用、父子层级及连线;同一制度在其他分支的节点不受影响。删除后清除节点和记录选择、取消旧定位回调与拖入预览、移除被删除的流程筛选路径,并关闭“显示已移除”,让删除分支立即从画布隐藏;节点保留在支持搜索的“删除节点”列表。只有已删除关联的制度重新进入“待分类”,仍有未移除关联节点的制度保持已分类。
-
当前版本选中节点后,可按
`Delete`
(兼容 Mac 的
`⌫ / Backspace`
)或点击右侧详情
底部“
删除节点”。删除按节点 ID 将该节点及全部下级节点的
`removed`
、
`data.removed`
设为
`true`
,保留节点引用、父子层级及连线;同一制度在其他分支的节点不受影响。删除后清除节点和记录选择、取消旧定位回调与拖入预览、移除被删除的流程筛选路径,并关闭“显示已移除”,让删除分支立即从画布隐藏;节点保留在支持搜索的“删除节点”列表。只有已删除关联的制度重新进入“待分类”,仍有未移除关联节点的制度保持已分类。
-
在“删除节点”列表选择节点后,右侧详情显示“已删除”,提供“恢复节点”或“恢复节点及子节点”。恢复将所选分支及其祖先的
`removed`
、
`data.removed`
设为
`false`
,保留其他分支的删除状态;自动展开目标祖先、保持缩放定位并打开详情。删除和恢复均只修改当前内存图谱,点击“保存”后通过
`POST /record/import`
写入
`internal_regulation_graph.metadata.graph_info`
;不删除制度或修订计划 Record,保存失败保留修改。
-
在“删除节点”列表选择节点后,右侧详情显示“已删除”,提供“恢复节点”或“恢复节点及子节点”。恢复将所选分支及其祖先的
`removed`
、
`data.removed`
设为
`false`
,保留其他分支的删除状态;自动展开目标祖先、保持缩放定位并打开详情。删除和恢复均只修改当前内存图谱,点击“保存”后通过
`POST /record/import`
写入
`internal_regulation_graph.metadata.graph_info`
;不删除制度或修订计划 Record,保存失败保留修改。
-
删除快捷键仅对当前页面已选中且未删除的节点生效;输入框、备注、搜索框、可编辑区域、选择器、菜单和弹窗内的
`Delete / Backspace`
保持原有编辑行为,带修饰键、输入法组合中或长按重复事件不触发节点删除。归档版本、加载/导入/保存期间、节点编辑弹窗打开时禁用删除和恢复;已删除节点不重复删除。点击画布空白处或详情“清除选择”会同时清除节点和记录选择,后续删除快捷键不操作之前的节点。React Flow 默认删除快捷键关闭,统一由上述逻辑处理。
-
删除快捷键仅对当前页面已选中且未删除的节点生效;输入框、备注、搜索框、可编辑区域、选择器、菜单和弹窗内的
`Delete / Backspace`
保持原有编辑行为,带修饰键、输入法组合中或长按重复事件不触发节点删除。归档版本、加载/导入/保存期间、节点编辑弹窗打开时禁用删除和恢复;已删除节点不重复删除。点击画布空白处或详情“清除选择”会同时清除节点和记录选择,后续删除快捷键不操作之前的节点。React Flow 默认删除快捷键关闭,统一由上述逻辑处理。
-
拓扑页高度锁定为当前视口可用高度,工具栏和画布保持在当前屏幕内;节点列表和详情区域在容器内部滚动,页面外层不因节点数量增长而整体向下延伸。
-
拓扑页高度锁定为当前视口可用高度,工具栏和画布保持在当前屏幕内;节点列表和详情区域在容器内部滚动,页面外层不因节点数量增长而整体向下延伸
;窄窗口下节点列表可收缩,详情宽度不超过拓扑容器,避免右侧按钮或表单被裁切
。
-
工具栏支持现行制度显示、已移除节点显示、现行/待新增/待修订/待废止筛选、当前版本和归档版本切换;归档版本只读,不能导入或保存。
-
工具栏支持现行制度显示、已移除节点显示、现行/待新增/待修订/待废止筛选、当前版本和归档版本切换;归档版本只读,不能导入或保存。
-
流程筛选使用多层级联路径,不将全部节点平铺到单层下拉;用户从第一层流程开始逐级展开并勾选路径。筛选保留选中路径、路径祖先和最后一个节点的子孙,不会因保留根节点路径而放行同一父级下的其他流程分支。
-
流程筛选使用多层级联路径,不将全部节点平铺到单层下拉;用户从第一层流程开始逐级展开并勾选路径。筛选保留选中路径、路径祖先和最后一个节点的子孙,不会因保留根节点路径而放行同一父级下的其他流程分支。
-
“导入 / 导出 / 保存”是图谱级操作,常驻拓扑容器右上角,与节点详情共用右侧面板;操作栏在顶部,三个按钮等宽横向排列、统一高度与间距,图标和文字保持同一行;导出使用 HeroUI Button 作为下拉触发器,保存保留主操作样式。下方节点详情以分隔线隔开并独立滚动。未选中节点、清除选择或图谱为空时操作栏仍可见;收起右侧详情时仅隐藏下方详情并释放画布宽度,顶部操作栏保留在右上角。导入使用 HeroUI Button 打开
`.xmind/.json`
文件选择,导出菜单使用 HeroUI Dropdown 提供“制度列表(.csv)/ 拓扑图(.xmind)”,保存当前整张图谱;归档版本禁用导入和保存,保留导出;导入或保存期间禁用重复导入、保存。
-
“导入 / 导出 / 保存”是图谱级操作,常驻拓扑容器右上角,与节点详情共用右侧面板;操作栏在顶部,三个按钮等宽横向排列、统一高度与间距,图标和文字保持同一行;导出使用 HeroUI Button 作为下拉触发器,保存保留主操作样式。下方节点详情以分隔线隔开并独立滚动。未选中节点、清除选择或图谱为空时操作栏仍可见;收起右侧详情时仅隐藏下方详情并释放画布宽度,顶部操作栏保留在右上角。导入使用 HeroUI Button 打开
`.xmind/.json`
文件选择,导出菜单使用 HeroUI Dropdown 提供“制度列表(.csv)/ 拓扑图(.xmind)”,保存当前整张图谱;归档版本禁用导入和保存,保留导出;导入或保存期间禁用重复导入、保存。
-
XMind 导出基于当前所查看版本的完整内存图谱,包括尚未保存的修改、折叠/筛选隐藏和已移除节点;导出不触发保存请求,归档版本也可导出。单根图沿用原根节点,多根或缺失父节点的分支归入“制度拓扑图”统一根节点,按
`parent`
保留层级;非父子连线转为 XMind 关联线。制度主题使用
`《制度名称》`
,节点备注写入 XMind Notes,制度状态和“已移除”写入标签;XMind 使用向右的逻辑图布局,不承诺复制画布的手工坐标、缩放或筛选状态。
-
XMind 导出基于当前所查看版本的完整内存图谱,包括尚未保存的修改、折叠/筛选隐藏和已移除节点;导出不触发保存请求,归档版本也可导出。单根图沿用原根节点,多根或缺失父节点的分支归入“制度拓扑图”统一根节点,按
`parent`
保留层级;非父子连线转为 XMind 关联线。制度主题使用
`《制度名称》`
,节点备注写入 XMind Notes,制度状态和“已移除”写入标签;XMind 使用向右的逻辑图布局,不承诺复制画布的手工坐标、缩放或筛选状态。
-
XMind 文件在浏览器内生成,使用现有 JSZip 打包
`content.json/metadata.json/manifest.json`
,下载名称为
`制度拓扑图-版本.xmind`
。生成期间显示“正在导出”并禁用重复导出;无节点、重复节点 ID 或循环父子关系时显示明确错误,失败保留当前图谱。CSV 继续按原规则导出制度列表。
-
XMind 文件在浏览器内生成,使用现有 JSZip 打包
`content.json/metadata.json/manifest.json`
,下载名称为
`制度拓扑图-版本.xmind`
。生成期间显示“正在导出”并禁用重复导出;无节点、重复节点 ID 或循环父子关系时显示明确错误,失败保留当前图谱。CSV 继续按原规则导出制度列表。
-
右侧详情支持编辑标题和备注、查看流程路径/制度路径/历史版本、折叠子节点和打开关联制度;“添加流程节点”位于详情的节点操作区,右键也可添加流程节点或备注。
-
点击画布或左侧列表中的节点打开右侧详情。详情标题栏固定展示节点类型、完整标题(较长时换行)和状态,已删除节点另显示“已删除”;点击关闭按钮清除节点和记录选择。顶部图谱操作栏和详情标题栏不随正文滚动。
-
详情正文按“基本信息 / 节点操作 / 关联信息”分组,在单一内容区域内滚动。基本信息使用 HeroUI 表单编辑标题与备注,备注默认三行并支持调整高度;修改实时更新当前图谱,点击顶部“保存”后持久化,保存失败保留编辑内容。归档版本标题和备注只读,保存期间禁用输入。
-
节点操作区将“展开/折叠子节点”和“添加流程节点”两列等宽排列;有制度或修订计划引用时,下方提供整行“查看关联制度”入口。归档版本、保存期间和已删除节点禁用添加;展开/折叠仍可调整查看范围。右键也可添加流程节点或备注。
-
关联信息区提供等宽“流程路径 / 制度路径 / 历史版本”三个 HeroUI Tab;路径按祖先顺序展示并支持长名称换行,历史条目进入对应制度详情,无数据时显示明确空状态。切换所选节点时,详情滚动位置和关联 Tab 重置到顶部及“流程路径”。
-
删除或恢复操作固定在详情底部,与常用操作分隔,并保留快捷键及恢复说明;内容滚动时始终可见。已删除节点显示“恢复节点”或“恢复节点及子节点”。归档版本显示只读提示,禁用删除、恢复;保存期间也禁用重复操作。删除、恢复状态变化及保存规则沿用上文,不修改制度 Record。
-
右键节点时,在鼠标点击位置显示“添加流程节点 / 添加备注”菜单;右键画布空白处只显示“添加流程节点”。菜单坐标以中间画布的实际定位容器为基准,不计入左侧列表宽度,不使用图谱缩放或平移后的节点坐标;菜单渲染后按实际宽高限制在画布内,靠近右侧或底部时自动向内调整,避免覆盖详情或被裁切。菜单项使用 HeroUI Button;点击外部、按 Esc、开始平移/缩放或拖动节点、窗口尺寸变化时关闭菜单,选择菜单项后按原流程打开节点编辑弹窗。
-
右键节点时,在鼠标点击位置显示“添加流程节点 / 添加备注”菜单;右键画布空白处只显示“添加流程节点”。菜单坐标以中间画布的实际定位容器为基准,不计入左侧列表宽度,不使用图谱缩放或平移后的节点坐标;菜单渲染后按实际宽高限制在画布内,靠近右侧或底部时自动向内调整,避免覆盖详情或被裁切。菜单项使用 HeroUI Button;点击外部、按 Esc、开始平移/缩放或拖动节点、窗口尺寸变化时关闭菜单,选择菜单项后按原流程打开节点编辑弹窗。
-
当前版本支持上传
`.xmind`
(兼容
`content.json`
与
`content.xml`
),会将
`《制度名称》`
主题匹配到制度记录,生成流程节点及连线,并同步
`graph_process`
分类。
-
当前版本支持上传
`.xmind`
(兼容
`content.json`
与
`content.xml`
),会将
`《制度名称》`
主题匹配到制度记录,生成流程节点及连线,并同步
`graph_process`
分类。
-
“全部 / 待分类”的制度记录可拖到画布中任意未移除可见节点的右侧连接区域:区域向右延伸 220 个画布单位,上下各扩展 32 个单位;相邻区域重叠时优先选择离鼠标最近的节点。命中后高亮父节点、显示连接区域与横向连线预览,并提示“松开连接到「节点名称」”;缩放、平移及手动移动节点后仍按当前画布坐标判断。
-
“全部 / 待分类”的制度记录可拖到画布中任意未移除可见节点的右侧连接区域:区域向右延伸 220 个画布单位,上下各扩展 32 个单位;相邻区域重叠时优先选择离鼠标最近的节点。命中后高亮父节点、显示连接区域与横向连线预览,并提示“松开连接到「节点名称」”;缩放、平移及手动移动节点后仍按当前画布坐标判断。
...
@@ -60,7 +64,17 @@
...
@@ -60,7 +64,17 @@
## 加载、错误与限制
## 加载、错误与限制
-
Subject、Record、附件请求均显示 loading、success 或 error 状态;
错误保留在当前页面
,不使用演示数据或 localStorage 回退。
-
Subject、Record、附件请求均显示 loading、success 或 error 状态;
请求错误统一显示可关闭的浮层,加载失败保留失败空状态和读取重试
,不使用演示数据或 localStorage 回退。
-
制度目录、修订计划和归档历史列表使用服务端分页;每页条数可选
`20 / 50 / 100`
,切换条数后回到第 1 页。页码和页数读取
`/record/query`
返回的
`current_page`
与
`total_pages`
,列表总数读取当前筛选条件对应的
`total_records`
。
-
制度目录、修订计划和归档历史列表使用服务端分页;每页条数可选
`20 / 50 / 100`
,切换条数后回到第 1 页。页码和页数读取
`/record/query`
返回的
`current_page`
与
`total_pages`
,列表总数读取当前筛选条件对应的
`total_records`
。
-
保存、提交、发文期间禁用重复提交按钮;失败时保留表单内容。
-
保存、提交、发文期间禁用重复提交按钮;失败时保留表单内容。
-
归档记录不修改现行制度;不存在的制度或修订计划显示明确空状态。
-
归档记录不修改现行制度;不存在的制度或修订计划显示明确空状态。
## 操作提示与失败重试
-
功能入口:登录、站点配置、应用切换、制度目录、修订计划编辑/提交/发文、现行制度归档、归档历史、制度详情,以及拓扑图加载、节点操作、导入、导出和保存。
-
接口成功、失败和降级警告统一通过应用根部
`AppFeedback`
的 HeroUI Toast 显示;浮层固定在视口顶部,层级 300,高于业务 Modal、确认框和移动侧栏,页面滚动或弹窗打开时均可看见并关闭。页面不再展示常驻操作横条。
-
成功默认 4 秒、错误和警告默认 8 秒后消失;悬停或键盘聚焦暂停计时。当前提示正文右侧的小号透明关闭图标始终可见,点击或键盘激活通过 HeroUI Toast 的
`close`
slot 关闭;后排堆叠提示隐藏关闭按钮,避免误关闭。同一来源的新提示替换旧提示,相同错误再次发生时重新显示;清空反馈或离开对应页面时清理该来源提示。
-
制度详情在“编辑信息”弹窗点击“保存”,仅
`PUT /record/:name`
成功后关闭编辑弹窗并弹出“详情已保存。”,然后沿用原逻辑重新读取详情。提示挂在页面根部,关闭编辑弹窗或显示加载状态不使其消失;重新开始编辑、再次保存、切换记录或离开页面时清理上一条成功提示。写入失败只显示错误并保留弹窗与草稿,正在保存时不重复提交;修订详情与修订计划编辑弹窗保留现有保存成功提示。
-
关闭提示仅影响展示,不改变请求成功/失败状态、修订状态、筛选条件、页码、所选记录、编辑草稿或
`metadata`
,也不会触发保存、发文、归档或图谱导入。保存/上传失败保留弹窗及填写内容,图谱保存失败保留内存节点和连线。
-
制度配置、制度目录、修订计划、归档历史、制度详情、修订详情及拓扑图加载失败时保留明确的失败空状态和“重新加载”,详细原因由 Toast 展示;不误显示为暂无数据。重试按当前名称、归档版本、筛选和分页读取资源,不重复执行写入操作。详情保存失败继续保留原编辑页面和弹框。
-
必填字段、确认说明、归档只读及节点未保存说明继续保留原业务规则;成功反馈不替代数据重新读取。现有
`internal_regulation/internal_regulation_revision/internal_regulation_archive/internal_regulation_graph`
字段及 API 协议保持不变。
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
View file @
3e6db798
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
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
{
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
;
...
@@ -352,6 +352,7 @@ export default function ApplicationSwitcherDialog({
...
@@ -352,6 +352,7 @@ export default function ApplicationSwitcherDialog({
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
!
isOpen
)
{
if
(
!
isOpen
)
{
setApplicationSearch
(
''
);
setApplicationSearch
(
''
);
setErrorMessage
(
''
);
return
;
return
;
}
}
...
@@ -442,13 +443,6 @@ export default function ApplicationSwitcherDialog({
...
@@ -442,13 +443,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 @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
{
Eye
}
from
'@/components/AppIcons'
;
import
{
Eye
}
from
'@/components/AppIcons'
;
...
@@ -91,9 +93,12 @@ export default function ForwardApprovals() {
...
@@ -91,9 +93,12 @@ export default function ForwardApprovals() {
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
editLoading
,
setEditLoading
]
=
useState
(
false
);
const
[
editLoading
,
setEditLoading
]
=
useState
(
false
);
const
[
editLoadFailed
,
setEditLoadFailed
]
=
useState
(
false
);
const
[
editSaving
,
setEditSaving
]
=
useState
(
false
);
const
[
editSaving
,
setEditSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
notice
,
setNotice
]
=
useState
(
''
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[,
setNotice
]
=
useFeedbackState
(
'success'
);
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
);
...
@@ -107,6 +112,8 @@ export default function ForwardApprovals() {
...
@@ -107,6 +112,8 @@ export default function ForwardApprovals() {
(
async
()
=>
{
(
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
try
{
try
{
await
ensureModelMonitorSubjects
();
await
ensureModelMonitorSubjects
();
const
result
=
await
loadForwardRecords
();
const
result
=
await
loadForwardRecords
();
...
@@ -122,6 +129,7 @@ export default function ForwardApprovals() {
...
@@ -122,6 +129,7 @@ export default function ForwardApprovals() {
}
}
}
catch
(
loadError
)
{
}
catch
(
loadError
)
{
if
(
!
cancelled
)
{
if
(
!
cancelled
)
{
setLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'转发审批列表加载失败'
);
setError
(
loadError
?.
message
||
'转发审批列表加载失败'
);
}
}
}
finally
{
}
finally
{
...
@@ -134,7 +142,7 @@ export default function ForwardApprovals() {
...
@@ -134,7 +142,7 @@ export default function ForwardApprovals() {
return
()
=>
{
return
()
=>
{
cancelled
=
true
;
cancelled
=
true
;
};
};
},
[
currentUserId
]);
},
[
currentUserId
,
reloadKey
,
setError
]);
const
editableInvolvedCount
=
useMemo
(
const
editableInvolvedCount
=
useMemo
(
()
=>
()
=>
...
@@ -212,6 +220,7 @@ export default function ForwardApprovals() {
...
@@ -212,6 +220,7 @@ export default function ForwardApprovals() {
setEditingRecord
(
record
);
setEditingRecord
(
record
);
setIsEditDialogOpen
(
true
);
setIsEditDialogOpen
(
true
);
setEditLoading
(
true
);
setEditLoading
(
true
);
setEditLoadFailed
(
false
);
setError
(
''
);
setError
(
''
);
try
{
try
{
...
@@ -220,8 +229,8 @@ export default function ForwardApprovals() {
...
@@ -220,8 +229,8 @@ export default function ForwardApprovals() {
setEditableRecords
(
result
.
records
);
setEditableRecords
(
result
.
records
);
setActiveEditableRecordId
(
result
.
records
[
0
]?.
id
||
''
);
setActiveEditableRecordId
(
result
.
records
[
0
]?.
id
||
''
);
}
catch
(
loadError
)
{
}
catch
(
loadError
)
{
setEditLoadFailed
(
true
);
setError
(
loadError
?.
message
||
'填写数据加载失败'
);
setError
(
loadError
?.
message
||
'填写数据加载失败'
);
setEditingRecord
(
null
);
setEditableSubjectDefinition
(
null
);
setEditableSubjectDefinition
(
null
);
setEditableRecords
([]);
setEditableRecords
([]);
setActiveEditableRecordId
(
''
);
setActiveEditableRecordId
(
''
);
...
@@ -457,8 +466,7 @@ export default function ForwardApprovals() {
...
@@ -457,8 +466,7 @@ export default function ForwardApprovals() {
/>
/>
{
loading
?
<
NoticeBanner
>
正在加载转发审批记录...
</
NoticeBanner
>
:
null
}
{
loading
?
<
NoticeBanner
>
正在加载转发审批记录...
</
NoticeBanner
>
:
null
}
{
!
loading
&&
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
loadFailed
?
<
RequestFailure
title=
"转发审批列表加载失败"
onRetry=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
/>
:
null
}
{
!
error
&&
notice
?
<
NoticeBanner
tone=
"success"
>
{
notice
}
</
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
);
}
}
>
...
@@ -477,7 +485,7 @@ export default function ForwardApprovals() {
...
@@ -477,7 +485,7 @@ export default function ForwardApprovals() {
{
editableInvolvedCount
>
0
?
<
NoticeBanner
>
你当前有
{
editableInvolvedCount
}
条待填写记录。
</
NoticeBanner
>
:
null
}
{
editableInvolvedCount
>
0
?
<
NoticeBanner
>
你当前有
{
editableInvolvedCount
}
条待填写记录。
</
NoticeBanner
>
:
null
}
<
section
className=
"min-w-0"
>
<
section
className=
"min-w-0"
hidden=
{
loading
||
loadFailed
}
>
<
SectionHeader
title=
"转发记录列表"
/>
<
SectionHeader
title=
"转发记录列表"
/>
<
DataGrid
<
DataGrid
aria
-
label=
"转发审批列表"
aria
-
label=
"转发审批列表"
...
@@ -573,6 +581,8 @@ export default function ForwardApprovals() {
...
@@ -573,6 +581,8 @@ export default function ForwardApprovals() {
>
>
{
editLoading
?
(
{
editLoading
?
(
<
NoticeBanner
>
正在加载可填写数据...
</
NoticeBanner
>
<
NoticeBanner
>
正在加载可填写数据...
</
NoticeBanner
>
)
:
editLoadFailed
?
(
<
RequestFailure
title=
"填写数据加载失败"
onRetry=
{
()
=>
openEditDialog
(
editingRecord
)
}
/>
)
:
editableRecords
.
length
>
0
&&
editableSubjectDefinition
?
(
)
:
editableRecords
.
length
>
0
&&
editableSubjectDefinition
?
(
<
div
className=
"grid gap-5 xl:grid-cols-[22rem_minmax(0,1fr)]"
>
<
div
className=
"grid gap-5 xl:grid-cols-[22rem_minmax(0,1fr)]"
>
<
Surface
className=
"rounded-2xl p-4"
variant=
"secondary"
>
<
Surface
className=
"rounded-2xl p-4"
variant=
"secondary"
>
...
...
src/pages/project_manager_xy/ModelBatchData.jsx
View file @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
{
import
{
...
@@ -58,8 +60,10 @@ export default function ModelBatchData() {
...
@@ -58,8 +60,10 @@ export default function ModelBatchData() {
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
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
notice
,
setNotice
]
=
useState
(
''
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[,
setNotice
]
=
useFeedbackState
(
'success'
);
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
);
...
@@ -82,6 +86,8 @@ export default function ModelBatchData() {
...
@@ -82,6 +86,8 @@ export default function ModelBatchData() {
let
cancelled
=
false
;
let
cancelled
=
false
;
(
async
()
=>
{
(
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
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
)]);
...
@@ -98,13 +104,13 @@ export default function ModelBatchData() {
...
@@ -98,13 +104,13 @@ export default function ModelBatchData() {
setForwardItems
(
itemList
);
setForwardItems
(
itemList
);
}
}
}
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
;
};
},
[
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
]);
...
@@ -225,11 +231,10 @@ export default function ModelBatchData() {
...
@@ -225,11 +231,10 @@ export default function ModelBatchData() {
<
div
className=
"space-y-5"
>
<
div
className=
"space-y-5"
>
<
PageHeader
eyebrow=
{
batch
?.
batchCode
||
'模型批次'
}
title=
{
batch
?.
batchName
||
'批次数据'
}
/>
<
PageHeader
eyebrow=
{
batch
?.
batchCode
||
'模型批次'
}
title=
{
batch
?.
batchName
||
'批次数据'
}
/>
{
loading
?
<
NoticeBanner
>
正在加载批次数据和转发状态...
</
NoticeBanner
>
:
null
}
{
loading
?
<
NoticeBanner
>
正在加载批次数据和转发状态...
</
NoticeBanner
>
:
null
}
{
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
loadFailed
?
<
RequestFailure
title=
"模型批次数据加载失败"
onRetry=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
/>
:
null
}
{
!
error
&&
notice
?
<
NoticeBanner
tone=
"success"
>
{
notice
}
</
NoticeBanner
>
:
null
}
{
batch
?.
syncStatus
===
'partial'
||
batch
?.
syncStatus
===
'failed'
?
<
NoticeBanner
tone=
"error"
>
最近一次同步未完整成功:
{
batch
.
errorMessage
||
'该批次不可重试,请联系管理员排查。'
}
</
NoticeBanner
>
:
null
}
{
batch
?.
syncStatus
===
'partial'
||
batch
?.
syncStatus
===
'failed'
?
<
NoticeBanner
tone=
"error"
>
最近一次同步未完整成功:
{
batch
.
errorMessage
||
'该批次不可重试,请联系管理员排查。'
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
batch
?
(
{
!
loading
&&
!
loadFailed
&&
batch
?
(
<
section
className=
"min-w-0 space-y-4"
>
<
section
className=
"min-w-0 space-y-4"
>
<
SectionHeader
<
SectionHeader
title=
"批次数据"
title=
"批次数据"
...
...
src/pages/project_manager_xy/ModelBatches.jsx
View file @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
...
@@ -35,7 +37,9 @@ export default function ModelBatches() {
...
@@ -35,7 +37,9 @@ 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
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
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 +58,8 @@ export default function ModelBatches() {
...
@@ -54,6 +58,8 @@ export default function ModelBatches() {
let
cancelled
=
false
;
let
cancelled
=
false
;
(
async
()
=>
{
(
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
try
{
try
{
await
ensureModelMonitorSubjects
();
await
ensureModelMonitorSubjects
();
const
nextModel
=
await
loadModel
(
modelId
);
const
nextModel
=
await
loadModel
(
modelId
);
...
@@ -63,13 +69,13 @@ export default function ModelBatches() {
...
@@ -63,13 +69,13 @@ export default function ModelBatches() {
setBatches
(
batchList
);
setBatches
(
batchList
);
}
}
}
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
;
};
},
[
modelId
]);
},
[
modelId
,
reloadKey
,
setError
]);
const
filteredBatches
=
useMemo
(()
=>
{
const
filteredBatches
=
useMemo
(()
=>
{
const
search
=
keyword
.
trim
().
toLowerCase
();
const
search
=
keyword
.
trim
().
toLowerCase
();
...
@@ -115,7 +121,7 @@ export default function ModelBatches() {
...
@@ -115,7 +121,7 @@ export default function ModelBatches() {
}
catch
(
loadError
)
{
}
catch
(
loadError
)
{
const
message
=
loadError
?.
message
||
'外部批次列表加载失败。'
;
const
message
=
loadError
?.
message
||
'外部批次列表加载失败。'
;
if
(
message
===
'外部模型列表中已找不到当前模型。'
)
{
if
(
message
===
'外部模型列表中已找不到当前模型。'
)
{
toast
.
info
(
message
);
toast
.
info
(
message
,
{
timeout
:
8000
}
);
setError
(
''
);
setError
(
''
);
}
else
{
}
else
{
setError
(
message
);
setError
(
message
);
...
@@ -137,7 +143,7 @@ export default function ModelBatches() {
...
@@ -137,7 +143,7 @@ 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
);
toast
.
success
(
`批次“
${
sourceBatch
.
batch_name
}
”同步完成:新增
${
result
.
insertedCount
}
条。`
);
toast
.
success
(
`批次“
${
sourceBatch
.
batch_name
}
”同步完成:新增
${
result
.
insertedCount
}
条。`
,
{
timeout
:
4000
}
);
setError
(
''
);
setError
(
''
);
setIsSyncDialogOpen
(
false
);
setIsSyncDialogOpen
(
false
);
}
catch
(
syncError
)
{
}
catch
(
syncError
)
{
...
@@ -152,9 +158,9 @@ export default function ModelBatches() {
...
@@ -152,9 +158,9 @@ export default function ModelBatches() {
<
div
className=
"space-y-5"
>
<
div
className=
"space-y-5"
>
<
PageHeader
eyebrow=
{
model
?.
modelCode
||
'模型'
}
title=
{
model
?.
modelName
||
'模型批次'
}
/>
<
PageHeader
eyebrow=
{
model
?.
modelCode
||
'模型'
}
title=
{
model
?.
modelName
||
'模型批次'
}
/>
{
loading
?
<
NoticeBanner
>
正在加载模型批次...
</
NoticeBanner
>
:
null
}
{
loading
?
<
NoticeBanner
>
正在加载模型批次...
</
NoticeBanner
>
:
null
}
{
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
loadFailed
?
<
RequestFailure
title=
"模型批次加载失败"
onRetry=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
/
>
:
null
}
{
!
loading
&&
model
?
(
{
!
loading
&&
!
loadFailed
&&
model
?
(
<
section
className=
"min-w-0 space-y-4"
>
<
section
className=
"min-w-0 space-y-4"
>
<
div
className=
"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<
div
className=
"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<
div
className=
"min-w-0 flex-1 sm:max-w-96"
>
<
div
className=
"min-w-0 flex-1 sm:max-w-96"
>
...
...
src/pages/project_manager_xy/ModelMonitorShared.jsx
View file @
3e6db798
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
import
{
Archive
,
Archive
,
Back
,
Back
,
...
@@ -183,15 +184,23 @@ export function UserSelectField({ value, onChange, label, required = false, plac
...
@@ -183,15 +184,23 @@ 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
(
'error'
);
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
((
requestError
)
=>
{
if
(
!
cancelled
)
{
setOptions
([]);
setError
(
requestError
?.
message
||
'用户列表加载失败,请重新搜索。'
);
}
})
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[
query
]);
},
[
query
,
setError
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
value
?.
label
&&
value
.
label
!==
query
)
setQuery
(
value
.
label
);
if
(
value
?.
label
&&
value
.
label
!==
query
)
setQuery
(
value
.
label
);
...
@@ -203,6 +212,7 @@ export function UserSelectField({ value, onChange, label, required = false, plac
...
@@ -203,6 +212,7 @@ export function UserSelectField({ value, onChange, label, required = false, plac
:
options
;
:
options
;
return
<
XinyuanAutocomplete
return
<
XinyuanAutocomplete
emptyText=
{
loading
?
'正在加载...'
:
'没有匹配用户'
}
emptyText=
{
loading
?
'正在加载...'
:
'没有匹配用户'
}
errorText=
{
error
}
inputValue=
{
query
}
inputValue=
{
query
}
isLoading=
{
loading
}
isLoading=
{
loading
}
isRequired=
{
required
}
isRequired=
{
required
}
...
...
src/pages/project_manager_xy/ModelOverview.jsx
View file @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
import
{
AppGrid
,
AppGrid
,
Clock
,
Clock
,
...
@@ -32,12 +34,16 @@ export default function ModelOverview() {
...
@@ -32,12 +34,16 @@ 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
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
(
async
()
=>
{
(
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
setLoadFailed
(
false
);
setError
(
''
);
try
{
try
{
await
ensureModelMonitorSubjects
();
await
ensureModelMonitorSubjects
();
const
[
modelList
,
batchList
,
forwardList
]
=
await
Promise
.
all
([
const
[
modelList
,
batchList
,
forwardList
]
=
await
Promise
.
all
([
...
@@ -52,13 +58,13 @@ export default function ModelOverview() {
...
@@ -52,13 +58,13 @@ export default function ModelOverview() {
setError
(
''
);
setError
(
''
);
}
}
}
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
totalRecords
=
useMemo
(
const
totalRecords
=
useMemo
(
()
=>
batches
.
reduce
((
total
,
batch
)
=>
total
+
batch
.
totalRecordCount
,
0
),
()
=>
batches
.
reduce
((
total
,
batch
)
=>
total
+
batch
.
totalRecordCount
,
0
),
...
@@ -123,9 +129,9 @@ export default function ModelOverview() {
...
@@ -123,9 +129,9 @@ export default function ModelOverview() {
/>
/>
{
loading
?
<
NoticeBanner
>
正在初始化模型监控对象并加载概览...
</
NoticeBanner
>
:
null
}
{
loading
?
<
NoticeBanner
>
正在初始化模型监控对象并加载概览...
</
NoticeBanner
>
:
null
}
{
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
loadFailed
?
<
RequestFailure
title=
"模型总览加载失败"
onRetry=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
/
>
:
null
}
{
!
loading
&&
!
error
?
(
{
!
loading
&&
!
loadFailed
?
(
<>
<>
<
div
className=
"rc-kpi-grid"
data
-
count=
"4"
>
<
div
className=
"rc-kpi-grid"
data
-
count=
"4"
>
{
metrics
.
map
(({
icon
:
Icon
,
iconTone
,
label
,
value
})
=>
(
{
metrics
.
map
(({
icon
:
Icon
,
iconTone
,
label
,
value
})
=>
(
...
...
src/pages/project_manager_xy/Models.jsx
View file @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
...
@@ -144,8 +146,11 @@ export default function Models() {
...
@@ -144,8 +146,11 @@ 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
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
notice
,
setNotice
]
=
useState
(
''
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[,
setNotice
]
=
useFeedbackState
(
'success'
);
const
[,
setWarning
]
=
useFeedbackState
(
'warning'
);
const
[
isGroupDialogOpen
,
setIsGroupDialogOpen
]
=
useState
(
false
);
const
[
isGroupDialogOpen
,
setIsGroupDialogOpen
]
=
useState
(
false
);
const
[
isDeleteGroupDialogOpen
,
setIsDeleteGroupDialogOpen
]
=
useState
(
false
);
const
[
isDeleteGroupDialogOpen
,
setIsDeleteGroupDialogOpen
]
=
useState
(
false
);
...
@@ -164,7 +169,7 @@ export default function Models() {
...
@@ -164,7 +169,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
]
=
use
State
(
'
'
);
const
[
syncDialogError
,
setSyncDialogError
]
=
use
FeedbackState
(
'error
'
);
async
function
refresh
()
{
async
function
refresh
()
{
const
[
modelList
,
groupList
]
=
await
Promise
.
all
([
loadModels
(),
loadModelGroups
()]);
const
[
modelList
,
groupList
]
=
await
Promise
.
all
([
loadModels
(),
loadModelGroups
()]);
...
@@ -176,17 +181,19 @@ export default function Models() {
...
@@ -176,17 +181,19 @@ 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 +447,9 @@ export default function Models() {
...
@@ -440,7 +447,9 @@ export default function Models() {
failedRecordCount
,
failedRecordCount
,
failures
,
failures
,
});
});
setNotice
(
`已完成
${
completedCodes
.
length
}
/
${
selectedEntries
.
length
}
个批次同步,共新增
${
insertedCount
}
条数据。`
);
const
summary
=
`已完成
${
selectedEntries
.
length
-
failures
.
length
}
/
${
selectedEntries
.
length
}
个批次同步,共新增
${
insertedCount
}
条数据。`
;
if
(
failures
.
length
)
setWarning
(
`
${
summary
}
${
failures
.
length
}
个批次出现异常,请查看同步结果。`
);
else
setNotice
(
summary
);
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 +467,9 @@ export default function Models() {
...
@@ -458,10 +467,9 @@ export default function Models() {
<
PageHeader
eyebrow=
"模型监控"
title=
"全部模型"
/>
<
PageHeader
eyebrow=
"模型监控"
title=
"全部模型"
/>
<
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
?
<
RequestFailure
title=
"模型列表加载失败"
onRetry=
{
()
=>
setReloadKey
((
key
)
=>
key
+
1
)
}
/>
:
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 +599,7 @@ export default function Models() {
...
@@ -591,7 +599,7 @@ 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
}
{
syncDialogError
?
<
RequestFailure
title=
{
syncStep
===
1
?
'来源模型与批次加载失败'
:
'同步结果读取失败'
}
onRetry=
{
syncStep
===
1
?
openSyncDialog
:
undefined
}
/
>
:
null
}
{
syncStep
===
1
&&
syncLoading
?
<
NoticeBanner
>
正在加载全部来源模型、批次和本地同步状态...
</
NoticeBanner
>
:
null
}
{
syncStep
===
1
&&
syncLoading
?
<
NoticeBanner
>
正在加载全部来源模型、批次和本地同步状态...
</
NoticeBanner
>
:
null
}
{
syncStep
===
2
?
(
{
syncStep
===
2
?
(
...
...
src/pages/project_manager_xy/model-monitor.business-flow.md
View file @
3e6db798
...
@@ -182,3 +182,11 @@ MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql`
...
@@ -182,3 +182,11 @@ MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql`
-
批量同步模型批次弹窗使用紧凑步骤条、年份筛选与全选工具行;年份下按来源模型分段展示批次,同一模型的批次集中在同一列表区,并在分段标题展示模型名称、编码和批次数量。各分段使用 HeroUI Pro ListView 原生多选,切换年份继续保留其它年份已选项,已同步批次继续禁用,选择和同步口径不变。
-
批量同步模型批次弹窗使用紧凑步骤条、年份筛选与全选工具行;年份下按来源模型分段展示批次,同一模型的批次集中在同一列表区,并在分段标题展示模型名称、编码和批次数量。各分段使用 HeroUI Pro ListView 原生多选,切换年份继续保留其它年份已选项,已同步批次继续禁用,选择和同步口径不变。
-
查看态详情统一放在单一 secondary Surface;编辑态字段直接位于 Modal Overlay,字段必填、枚举来源、主键禁用和完成填写校验保持原规则。
-
查看态详情统一放在单一 secondary Surface;编辑态字段直接位于 Modal Overlay,字段必填、枚举来源、主键禁用和完成填写校验保持原规则。
-
Loading、错误和空状态由 HeroUI/HeroUI Pro 语义组件展示;不得生成测试记录或用伪造数值填充空页面。
-
Loading、错误和空状态由 HeroUI/HeroUI Pro 语义组件展示;不得生成测试记录或用伪造数值填充空页面。
## 8. 操作提示与失败重试
-
功能入口:模型总览、模型分组/归档信息、批次同步、批次数据隐藏/恢复、转发提交、填写保存、完成填写和两级审批,以及弹窗中的用户查询。
-
成功、失败和降级警告共用应用根部 HeroUI Toast,位于视口顶部且高于业务弹窗和侧栏,当前提示使用正文右侧的小号透明关闭按钮,点击或键盘激活通过
`close`
slot 关闭,后排提示隐藏关闭按钮;成功 4 秒、错误/警告 8 秒自动消失,悬停或聚焦暂停计时,同一错误重试时重新弹出。
-
关闭提示不改变同步/填写/审批状态,也不清空选择或草稿。保存失败保留原页面和弹窗;用户搜索失败保留字段级错误并可重新搜索。请求写入协议与
`metadata`
字段保持不变。
-
总览、模型列表、批次、批次数据、转发审批和填写数据加载失败时显示失败空状态及读取重试;重试只读取当前页面资源,不重复同步、转发或审批。批量同步来源加载失败可重新读取,同步结果刷新失败仍保留结果供查看。
-
批量同步出现部分失败时使用警告浮层并保留逐批结果;已经创建的批次继续禁止重复同步。历史同步失败摘要、同步进度、待填写数量属于业务状态,继续在原位置展示。
src/pages/project_manager_xy/notifications/NotificationListPage.jsx
View file @
3e6db798
import
RequestFailure
from
'@/components/RequestFailure'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
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
{
import
{
Alert
,
Avatar
,
Avatar
,
Button
,
Button
,
Card
,
Card
,
...
@@ -200,7 +201,7 @@ export default function NotificationListPage() {
...
@@ -200,7 +201,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
(
'error
'
);
const
[
busyKey
,
setBusyKey
]
=
useState
(
''
);
const
[
busyKey
,
setBusyKey
]
=
useState
(
''
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
...
@@ -264,12 +265,12 @@ export default function NotificationListPage() {
...
@@ -264,12 +265,12 @@ export default function NotificationListPage() {
setBusyKey
(
key
);
setBusyKey
(
key
);
try
{
try
{
await
operation
();
await
operation
();
toast
.
success
(
successMessage
,
{
timeout
:
3
000
});
toast
.
success
(
successMessage
,
{
timeout
:
4
000
});
refresh
();
refresh
();
return
true
;
return
true
;
}
catch
(
operationError
)
{
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
timeout
:
4
000
,
timeout
:
8
000
,
});
});
return
false
;
return
false
;
}
finally
{
}
finally
{
...
@@ -319,7 +320,7 @@ export default function NotificationListPage() {
...
@@ -319,7 +320,7 @@ export default function NotificationListPage() {
refresh
();
refresh
();
}
catch
(
operationError
)
{
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
timeout
:
4
000
,
timeout
:
8
000
,
});
});
}
finally
{
}
finally
{
setBusyKey
(
''
);
setBusyKey
(
''
);
...
@@ -403,16 +404,7 @@ export default function NotificationListPage() {
...
@@ -403,16 +404,7 @@ export default function NotificationListPage() {
</
Tabs
>
</
Tabs
>
</
div
>
</
div
>
{
error
?
(
{
!
loading
&&
error
?
<
RequestFailure
title=
"通知加载失败"
onRetry=
{
refresh
}
/>
:
null
}
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
<
Button
size=
"sm"
variant=
"ghost"
onPress=
{
refresh
}
>
重试
</
Button
>
</
Alert
>
)
:
null
}
{
loading
&&
notifications
.
length
===
0
?
(
{
loading
&&
notifications
.
length
===
0
?
(
<
div
className=
"flex min-h-64 items-center justify-center"
aria
-
label=
"通知加载中"
>
<
div
className=
"flex min-h-64 items-center justify-center"
aria
-
label=
"通知加载中"
>
...
...
src/pages/project_manager_xy/notifications/notifications.business-flow.md
View file @
3e6db798
...
@@ -35,3 +35,9 @@
...
@@ -35,3 +35,9 @@
-
接口 401/403、网络失败或动态 action 配置缺失时不得伪造成功状态或未读数量。
-
接口 401/403、网络失败或动态 action 配置缺失时不得伪造成功状态或未读数量。
-
通知中心不读取或修改模型监控 Subject、Record、审批字段及用户
`applications`
授权结构。
-
通知中心不读取或修改模型监控 Subject、Record、审批字段及用户
`applications`
授权结构。
-
本应用不在五项完整功能白名单中,不实现
`shortTitle`
、Subject View 权限或审计材料中心文件工作流。
-
本应用不在五项完整功能白名单中,不实现
`shortTitle`
、Subject View 权限或审计材料中心文件工作流。
## 操作提示与失败重试
-
通知查询、已读操作、待办动作和关联流程打开失败均使用根部 HeroUI Toast,弹窗和移动侧栏上方可见;成功 4 秒、失败 8 秒自动消失,当前提示正文右侧的透明关闭图标始终可见,点击或键盘激活通过
`close`
slot 关闭,后排提示隐藏关闭按钮;悬停或聚焦暂停计时。
-
列表加载失败保留“通知加载失败”空状态与“重新加载”,重试保留当前分类、已读状态、关键词及页码,只重新读取通知。
-
关闭提示不会把请求失败改为成功,不修改通知已读/待办状态或未读数量,不重复执行待办动作或打开关联链接;接口协议保持不变。
src/tailwind.css
View file @
3e6db798
...
@@ -4,6 +4,7 @@
...
@@ -4,6 +4,7 @@
@import
"./design-system/theme.css"
;
@import
"./design-system/theme.css"
;
@import
"./design-system/tokens.css"
;
@import
"./design-system/tokens.css"
;
@import
"./design-system/shell.css"
;
@import
"./design-system/shell.css"
;
@import
"./design-system/feedback.css"
;
@source
"./*.{js,jsx,ts,tsx}"
;
@source
"./*.{js,jsx,ts,tsx}"
;
@source
"./components/**/*.{js,jsx,ts,tsx}"
;
@source
"./components/**/*.{js,jsx,ts,tsx}"
;
...
...
src/wrapper/Site.jsx
View file @
3e6db798
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
getSystemInfo
}
from
'@/services/DataService'
;
import
{
getSystemInfo
}
from
'@/services/DataService'
;
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
import
customParseFormat
from
'dayjs/plugin/customParseFormat'
;
import
customParseFormat
from
'dayjs/plugin/customParseFormat'
;
...
@@ -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
(
'warning
'
);
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