Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
L
law_reference_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
law_reference_new
Commits
36617df8
Commit
36617df8
authored
Oct 08, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: unify operation feedback and loading error recovery
parent
86bbaf04
Hide whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
299 additions
and
126 deletions
+299
-126
xinyuan-design-system.md
docs/xinyuan-design-system.md
+4
-1
manifest.json
public/manifest.json
+1
-1
app.js
src/app.js
+7
-1
AppFeedback.jsx
src/components/AppFeedback.jsx
+42
-0
AppShell.jsx
src/components/AppShell.jsx
+0
-2
LawRecordRenderer.jsx
src/components/LawRecordRenderer.jsx
+27
-14
LoadErrorState.jsx
src/components/LoadErrorState.jsx
+18
-0
ReferenceSearchSelect.jsx
src/components/ReferenceSearchSelect.jsx
+2
-1
feedback.css
src/design-system/feedback.css
+11
-0
useFeedbackState.js
src/hooks/useFeedbackState.js
+38
-0
AuditIndicatorResultsPage.jsx
src/pages/AuditIndicatorResultsPage.jsx
+13
-9
AuditIndicatorView.jsx
src/pages/AuditIndicatorView.jsx
+13
-11
ConfiguredSpecialViews.jsx
src/pages/ConfiguredSpecialViews.jsx
+10
-6
GlobalSearchPage.jsx
src/pages/GlobalSearchPage.jsx
+5
-3
LawRecordPage.jsx
src/pages/LawRecordPage.jsx
+24
-12
LawSubjectPage.jsx
src/pages/LawSubjectPage.jsx
+44
-26
LawWorkbenchLayout.jsx
src/pages/LawWorkbenchLayout.jsx
+7
-8
Login.jsx
src/pages/Login.jsx
+2
-9
RecordGraphPage.jsx
src/pages/RecordGraphPage.jsx
+6
-5
law-reference.business-flow.md
src/pages/law-reference.business-flow.md
+10
-1
ApplicationSwitcherDialog.jsx
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
+4
-9
NotificationListPage.jsx
...project_manager_xy/notifications/NotificationListPage.jsx
+8
-6
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 @
36617df8
...
@@ -48,7 +48,10 @@
...
@@ -48,7 +48,10 @@
-
预览态普通标量字段使用无边框静态值:标签为
`text-muted`
,值为
`text-foreground`
,空值显示“暂无内容”;不得用灰色 disabled Input/Autocomplete 冒充预览。编辑态才显示 HeroUI
`secondary`
控件;权限禁用、锁定或只读字段在同一网格中继续显示静态只读值。
-
预览态普通标量字段使用无边框静态值:标签为
`text-muted`
,值为
`text-foreground`
,空值显示“暂无内容”;不得用灰色 disabled Input/Autocomplete 冒充预览。编辑态才显示 HeroUI
`secondary`
控件;权限禁用、锁定或只读字段在同一网格中继续显示静态只读值。
-
预览与编辑共用两列字段网格和字段顺序;长文本、附件等内容跨两列。编辑态使用 Footer 的 HeroUI
`secondary`
取消 +
`primary`
保存,保存只提交一次,取消丢弃草稿,不改变原有校验、权限和请求时机。
-
预览与编辑共用两列字段网格和字段顺序;长文本、附件等内容跨两列。编辑态使用 Footer 的 HeroUI
`secondary`
取消 +
`primary`
保存,保存只提交一次,取消丢弃草稿,不改变原有校验、权限和请求时机。
-
预览态 Footer 使用 HeroUI
`primary`
编辑按钮作为主操作;点击后在同一个 Modal 和编辑会话内切换为编辑态,保留当前记录标题、字段草稿和材料草稿,不关闭后重新打开;进入编辑态后显示
`secondary`
取消 +
`primary`
保存,取消关闭并丢弃未保存草稿。
-
预览态 Footer 使用 HeroUI
`primary`
编辑按钮作为主操作;点击后在同一个 Modal 和编辑会话内切换为编辑态,保留当前记录标题、字段草稿和材料草稿,不关闭后重新打开;进入编辑态后显示
`secondary`
取消 +
`primary`
保存,取消关闭并丢弃未保存草稿。
-
例行成功反馈使用顶部居中的自动消失 Toast;需要处理的错误保留上下文 Alert。
-
接口成功、失败和操作校验统一使用顶部居中的 HeroUI Toast,禁止在页面或弹窗中渲染常驻的操作反馈横条。全局
`AppFeedback`
在应用根层挂载唯一 Toast Provider,登录页、业务页与弹窗共用;提示层级高于 Modal 和移动侧栏,不受页面滚动影响。
-
成功提示 4 秒后自动消失,错误与警告提示 8 秒后自动消失;关闭按钮使用 HeroUI
`Button`
的
`ghost`
、
`isIconOnly`
、
`size="sm"`
与 Toast 原生
`slot="close"`
,统一 16px
`Close`
图标并参与内容布局,继承主题悬停和键盘焦点状态。最前方提示的关闭按钮始终可见,后方堆叠提示隐藏关闭按钮以避免误点;支持触屏和键盘。长错误内容可滚动阅读,提示最多同时展示 3 条。
-
详情编辑保存的成功提示由详情页持有,仅在更新接口成功后显示“详情已保存。”;编辑弹窗关闭后提示仍可见。进入新编辑会话、取消、再次保存或切换记录时清除旧成功提示,校验/请求失败和保存中的重复点击不触发成功提示。
-
关闭或自动隐藏提示只改变提示展示,不清除请求失败状态、表单草稿或已选记录;相同错误在重试失败后再次提示。数据加载失败使用 HeroUI Pro EmptyState 和重新加载入口,不能冒充无数据。操作失败不隐藏已加载数据;静态业务限制说明与字段错误仍可保留上下文展示。
-
应用切换严格继承
`model_analysis`
:从
`/info -> site.group`
动态生成分组,保持配置顺序并显示真实数量;未归组应用进入“其他应用”,无配置时使用“全部应用”。Modal 固定 46rem/42rem、28px inset、手机一列、
`sm`
起两列,间距 12px;加载骨架使用相同网格,桌面不得切成三列,长列表仅在 Modal Body 内滚动。
-
应用切换严格继承
`model_analysis`
:从
`/info -> site.group`
动态生成分组,保持配置顺序并显示真实数量;未归组应用进入“其他应用”,无配置时使用“全部应用”。Modal 固定 46rem/42rem、28px inset、手机一列、
`sm`
起两列,间距 12px;加载骨架使用相同网格,桌面不得切成三列,长列表仅在 Modal Body 内滚动。
## 字段后缀图标
## 字段后缀图标
...
...
public/manifest.json
View file @
36617df8
{
{
"name"
:
"law_reference_new"
,
"name"
:
"law_reference_new"
,
"version"
:
"1.0.
1
"
,
"version"
:
"1.0.
3
"
,
"build"
:
"law-reference"
,
"build"
:
"law-reference"
,
"description"
:
"内部制度与外部法规"
,
"description"
:
"内部制度与外部法规"
,
"date"
:
"2025-07-14 19:27:20"
,
"date"
:
"2025-07-14 19:27:20"
,
...
...
src/app.js
View file @
36617df8
import
'@/tailwind.css'
;
import
'@/tailwind.css'
;
import
'@/global.less'
;
import
'@/global.less'
;
import
{
defineApp
}
from
'umi'
;
import
{
defineApp
}
from
'umi'
;
import
{
createElement
}
from
'react'
;
import
AppFeedback
from
'@/components/AppFeedback'
;
export
async
function
getInitialState
()
{
export
async
function
getInitialState
()
{
return
{
return
{
...
@@ -8,4 +10,8 @@ export async function getInitialState() {
...
@@ -8,4 +10,8 @@ export async function getInitialState() {
};
};
}
}
export
default
defineApp
({});
export
function
rootContainer
(
container
)
{
return
createElement
(
AppFeedback
,
null
,
container
);
}
export
default
defineApp
({
rootContainer
});
src/components/AppFeedback.jsx
0 → 100644
View file @
36617df8
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
className=
"whitespace-pre-wrap break-words"
>
{
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 @
36617df8
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
ApplicationSwitcherDialog
from
'@/pages/project_manager_xy/ApplicationSwitcherDialog'
;
import
ApplicationSwitcherDialog
from
'@/pages/project_manager_xy/ApplicationSwitcherDialog'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
...
@@ -49,7 +48,6 @@ export default function AppShell({
...
@@ -49,7 +48,6 @@ export default function AppShell({
userInfo=
{
sidebarProps
.
userInfo
}
userInfo=
{
sidebarProps
.
userInfo
}
onOpenChange=
{
setIsApplicationDialogOpen
}
onOpenChange=
{
setIsApplicationDialogOpen
}
/>
/>
<
Toast
.
Provider
maxVisibleToasts=
{
3
}
placement=
"top"
/>
</
Sidebar
.
Provider
>
</
Sidebar
.
Provider
>
);
);
}
}
src/components/LawRecordRenderer.jsx
View file @
36617df8
import
{
Input
,
TextArea
}
from
'@heroui/react'
;
import
{
Input
,
TextArea
,
toast
}
from
'@heroui/react'
;
import
{
useState
}
from
'react'
;
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
import
DocumentService
from
'@/services/DocumentService'
;
import
DocumentService
from
'@/services/DocumentService'
;
import
{
uploadDocument
}
from
'@/services/DataService'
;
import
{
uploadDocument
}
from
'@/services/DataService'
;
...
@@ -273,6 +274,7 @@ export function LawRecordFieldValue({ field, value, className = '' }) {
...
@@ -273,6 +274,7 @@ export function LawRecordFieldValue({ field, value, className = '' }) {
}
}
export function LawRecordFieldEditor(
{
field
,
value
,
onChange
,
edit
=
false
}
)
{
export function LawRecordFieldEditor(
{
field
,
value
,
onChange
,
edit
=
false
}
)
{
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
label
=
getLawFieldLabel
(
field
);
const
label
=
getLawFieldLabel
(
field
);
const
disabled
=
edit
&&
isLawPrimaryField
(
field
);
const
disabled
=
edit
&&
isLawPrimaryField
(
field
);
const
options
=
normalizeLawOptions
(
field
?.
settings
?.
options
||
[]);
const
options
=
normalizeLawOptions
(
field
?.
settings
?.
options
||
[]);
...
@@ -301,26 +303,37 @@ export function LawRecordFieldEditor({ field, value, onChange, edit = false }) {
...
@@ -301,26 +303,37 @@ export function LawRecordFieldEditor({ field, value, onChange, edit = false }) {
return
(
return
(
<
FileUploadField
<
FileUploadField
ariaLabel=
{
label
}
ariaLabel=
{
label
}
disabled=
{
disabled
}
disabled=
{
disabled
||
uploading
}
fileName=
{
formatLawFieldValue
(
field
,
value
,
''
)
}
fileName=
{
formatLawFieldValue
(
field
,
value
,
''
)
}
multiple=
{
field
?.
multiple
!==
false
}
multiple=
{
field
?.
multiple
!==
false
}
triggerLabel=
{
uploading
?
'正在上传…'
:
'选择文件'
}
onFilesChange=
{
async
(
files
)
=>
{
onFilesChange=
{
async
(
files
)
=>
{
const
uploaded
=
await
Promise
.
all
(
files
.
map
((
file
)
=>
uploadDocument
(
file
)));
if
(
!
files
.
length
||
uploading
)
return
;
if
(
field
?.
mode
===
'document'
&&
files
[
0
])
{
setUploading
(
true
);
const
extension
=
files
[
0
].
name
?.
split
(
'.'
).
pop
()?.
toLowerCase
();
try
{
if
(
extension
===
'doc'
||
extension
===
'docx'
)
{
const
uploaded
=
await
Promise
.
all
(
files
.
map
((
file
)
=>
uploadDocument
(
file
)));
try
{
if
(
field
?.
mode
===
'document'
&&
files
[
0
])
{
const
documentModel
=
await
DocumentService
.
parse
(
files
[
0
]);
const
extension
=
files
[
0
].
name
?.
split
(
'.'
).
pop
()?.
toLowerCase
();
if
(
documentModel
)
{
if
(
extension
===
'doc'
||
extension
===
'docx'
)
{
onChange
(
documentModel
);
try
{
return
;
const
documentModel
=
await
DocumentService
.
parse
(
files
[
0
]);
if
(
documentModel
)
{
onChange
(
documentModel
);
toast
.
success
(
'文档上传成功'
,
{
timeout
:
4000
});
return
;
}
}
catch
{
// Preserve the uploaded file value when document parsing is unavailable.
}
}
}
catch
{
// Preserve the uploaded file value when document parsing is unavailable.
}
}
}
}
onChange
(
field
?.
multiple
===
false
?
uploaded
[
0
]
:
uploaded
);
toast
.
success
(
'附件上传成功'
,
{
timeout
:
4000
});
}
catch
(
error
)
{
toast
.
danger
(
error
?.
message
||
'附件上传失败,请稍后重试'
,
{
timeout
:
8000
});
}
finally
{
setUploading
(
false
);
}
}
onChange
(
field
?.
multiple
===
false
?
uploaded
[
0
]
:
uploaded
);
}
}
}
}
/>
/>
);
);
...
...
src/components/LoadErrorState.jsx
0 → 100644
View file @
36617df8
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Button
}
from
'@heroui/react'
;
export
default
function
LoadErrorState
({
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
.
Footer
>
<
Button
variant=
"secondary"
onPress=
{
onRetry
}
>
重新加载
</
Button
>
</
EmptyState
.
Footer
>
)
:
null
}
</
EmptyState
>
);
}
src/components/ReferenceSearchSelect.jsx
View file @
36617df8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
useEffect
,
useState
}
from
'react'
;
import
{
useEffect
,
useState
}
from
'react'
;
import
XinyuanAutocomplete
from
'./XinyuanAutocomplete'
;
import
XinyuanAutocomplete
from
'./XinyuanAutocomplete'
;
...
@@ -7,7 +8,7 @@ export default function ReferenceSearchSelect({ label, value, onChange, loadOpti
...
@@ -7,7 +8,7 @@ export default function ReferenceSearchSelect({ label, value, onChange, loadOpti
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
options
,
setOptions
]
=
useState
(()
=>
initialRecords
.
filter
((
item
)
=>
item
?.
_id
).
map
(
optionFrom
));
const
[
options
,
setOptions
]
=
useState
(()
=>
initialRecords
.
filter
((
item
)
=>
item
?.
_id
).
map
(
optionFrom
));
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
selected
=
new
Set
(
multiple
?
value
||
[]
:
value
?
[
value
]
:
[]);
const
selected
=
new
Set
(
multiple
?
value
||
[]
:
value
?
[
value
]
:
[]);
useEffect
(()
=>
{
useEffect
(()
=>
{
...
...
src/design-system/feedback.css
0 → 100644
View file @
36617df8
/* Operation feedback stays above dialogs and 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 @
36617df8
import
{
toast
}
from
'@heroui/react'
;
import
{
useCallback
,
useEffect
,
useRef
,
useState
}
from
'react'
;
const
FEEDBACK_OPTIONS
=
{
error
:
{
title
:
'操作失败'
,
variant
:
'danger'
,
timeout
:
8000
},
success
:
{
title
:
'操作成功'
,
variant
:
'success'
,
timeout
:
4000
},
warning
:
{
title
:
'请注意'
,
variant
:
'warning'
,
timeout
:
8000
},
};
// Closing a toast preserves the request's failure state and 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
;
// A retry must report feedback even when the server returns the same error.
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/AuditIndicatorResultsPage.jsx
View file @
36617df8
import
{
Alert
,
Button
,
Card
,
Input
,
Spinner
}
from
'@heroui/react'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
,
Card
,
Input
,
Spinner
,
toast
}
from
'@heroui/react'
;
import
LoadErrorState
from
'@/components/LoadErrorState'
;
import
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
import
{
history
}
from
'umi'
;
import
{
DataTable
,
DataTablePagination
}
from
'@/components/DataTable'
;
import
{
DataTable
,
DataTablePagination
}
from
'@/components/DataTable'
;
...
@@ -23,7 +25,8 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
...
@@ -23,7 +25,8 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
const
[
pageSize
,
setPageSize
]
=
useState
(
20
);
const
[
pageSize
,
setPageSize
]
=
useState
(
20
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
[
refresh
,
setRefresh
]
=
useState
(
0
);
const
[
refresh
,
setRefresh
]
=
useState
(
0
);
const
[
selected
,
setSelected
]
=
useState
(
null
);
const
[
selected
,
setSelected
]
=
useState
(
null
);
const
[
deleting
,
setDeleting
]
=
useState
(
null
);
const
[
deleting
,
setDeleting
]
=
useState
(
null
);
...
@@ -31,13 +34,13 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
...
@@ -31,13 +34,13 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
setLoading
(
true
);
setError
(
''
);
setRecords
([]);
setLoading
(
true
);
set
LoadFailed
(
false
);
set
Error
(
''
);
setRecords
([]);
(
async
()
=>
{
(
async
()
=>
{
try
{
try
{
const
record
=
await
loadRecord
(
recordName
);
const
record
=
await
loadRecord
(
recordName
);
const
response
=
await
queryRecords
({
subject
:
resultSubject
,
page
,
page_size
:
pageSize
,
filter
:
{
'metadata.related_indicator'
:
record
.
_id
,
...(
query
?
{
title
:
{
$regex
:
query
}
}
:
{})
}
});
const
response
=
await
queryRecords
({
subject
:
resultSubject
,
page
,
page_size
:
pageSize
,
filter
:
{
'metadata.related_indicator'
:
record
.
_id
,
...(
query
?
{
title
:
{
$regex
:
query
}
}
:
{})
}
});
if
(
!
cancelled
)
{
setIndicator
(
record
);
setRecords
(
response
?.
records
||
[]);
setTotal
(
response
?.
total_records
??
0
);
}
if
(
!
cancelled
)
{
setIndicator
(
record
);
setRecords
(
response
?.
records
||
[]);
setTotal
(
response
?.
total_records
??
0
);
}
}
catch
(
requestError
)
{
if
(
!
cancelled
)
setError
(
requestError
?.
message
||
'执行结果加载失败'
);
}
}
catch
(
requestError
)
{
if
(
!
cancelled
)
{
setLoadFailed
(
true
);
setError
(
requestError
?.
message
||
'执行结果加载失败'
);
}
}
finally
{
if
(
!
cancelled
)
setLoading
(
false
);
}
finally
{
if
(
!
cancelled
)
setLoading
(
false
);
}
})();
})();
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
...
@@ -51,6 +54,7 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
...
@@ -51,6 +54,7 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
const
rows
=
selected
.
metadata
.
execution_data
.
map
((
row
)
=>
Object
.
fromEntries
(
Object
.
entries
(
row
).
map
(([
key
,
value
])
=>
[
key
,
value
&&
typeof
value
===
'object'
?
JSON
.
stringify
(
value
)
:
value
])));
const
rows
=
selected
.
metadata
.
execution_data
.
map
((
row
)
=>
Object
.
fromEntries
(
Object
.
entries
(
row
).
map
(([
key
,
value
])
=>
[
key
,
value
&&
typeof
value
===
'object'
?
JSON
.
stringify
(
value
)
:
value
])));
XLSX
.
utils
.
book_append_sheet
(
workbook
,
XLSX
.
utils
.
json_to_sheet
(
rows
),
'执行结果'
);
XLSX
.
utils
.
book_append_sheet
(
workbook
,
XLSX
.
utils
.
json_to_sheet
(
rows
),
'执行结果'
);
XLSX
.
writeFile
(
workbook
,
`
${
indicator
?.
title
||
'指标'
}
执行结果
.
xlsx
`);
XLSX
.
writeFile
(
workbook
,
`
${
indicator
?.
title
||
'指标'
}
执行结果
.
xlsx
`);
toast.success('导出成功', { timeout: 4000 });
} catch (requestError) { setError(requestError?.message || '导出失败'); }
} catch (requestError) { setError(requestError?.message || '导出失败'); }
finally { setPending(false); }
finally { setPending(false); }
};
};
...
@@ -60,6 +64,7 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
...
@@ -60,6 +64,7 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
try {
try {
await removeRecord(deleting.name || deleting._id);
await removeRecord(deleting.name || deleting._id);
setDeleting(null);
setDeleting(null);
toast.success('执行结果已删除', { timeout: 4000 });
if (records.length === 1 && page > 0) setPage(page - 1);
if (records.length === 1 && page > 0) setPage(page - 1);
else setRefresh((value) => value + 1);
else setRefresh((value) => value + 1);
} catch (requestError) { setError(requestError?.message || '删除失败'); }
} catch (requestError) { setError(requestError?.message || '删除失败'); }
...
@@ -70,15 +75,14 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
...
@@ -70,15 +75,14 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
return <div className="rc-page-stack">
return <div className="rc-page-stack">
<div className="flex flex-wrap items-center gap-3"><Button variant="secondary" onPress={() => history.push(`
/
$
{
subjectName
}
`)}>返回指标列表</Button><h2 className="text-xl font-semibold">{indicator?.title || '指标'}执行结果</h2></div>
<div className="flex flex-wrap items-center gap-3"><Button variant="secondary" onPress={() => history.push(`
/
$
{
subjectName
}
`)}>返回指标列表</Button><h2 className="text-xl font-semibold">{indicator?.title || '指标'}执行结果</h2></div>
{error ? <Alert status="danger"><Alert.Content><Alert.Description>{error}</Alert.Description></Alert.Content><Button variant="ghost" onPress={() => setRefresh((value) => value + 1)}>重试</Button></Alert> : null}
<Card><Card.Header><form className="flex w-full gap-2 sm:max-w-md" onSubmit={(event) => { event.preventDefault(); setQuery(keyword.trim()); setPage(0); }}><Input aria-label="搜索执行结果" className="min-w-0 flex-1" placeholder="搜索结果标题" value={keyword} onChange={(event) => setKeyword(event.target.value)} /><Button variant="primary" type="submit">搜索</Button></form></Card.Header>
<Card><Card.Header><form className="flex w-full gap-2 sm:max-w-md" onSubmit={(event) => { event.preventDefault(); setQuery(keyword.trim()); setPage(0); }}><Input aria-label="搜索执行结果" className="min-w-0 flex-1" placeholder="搜索结果标题" value={keyword} onChange={(event) => setKeyword(event.target.value)} /><Button variant="primary" type="submit">搜索</Button></form></Card.Header>
<Card.Content className="space-y-4">
<Card.Content className="space-y-4">
{loading ? <div className="grid min-h-32 place-items-center"><Spinner /></div> :
!error ?
<DataTable ariaLabel="指标执行结果" rows={records} getRowKey={(item) => item._id} columns={[
{loading ? <div className="grid min-h-32 place-items-center"><Spinner /></div> :
loadFailed ? <LoadErrorState title="执行结果加载失败" onRetry={() => setRefresh((value) => value + 1)} /> :
<DataTable ariaLabel="指标执行结果" rows={records} getRowKey={(item) => item._id} columns={[
{ key: 'title', title: '标题' },
{ key: 'title', title: '标题' },
{ key: 'time', title: '执行时间', render: (item) => formatTime(item.metadata?.execution_time) },
{ key: 'time', title: '执行时间', render: (item) => formatTime(item.metadata?.execution_time) },
...(canManage ? [{ key: 'actions', title: '操作', isActions: true, width: 160, render: (item) => <div className="flex gap-2"><Button size="sm" variant="secondary" onPress={() => setSelected(item)}>查看</Button><Button size="sm" variant="danger-soft" onPress={() => setDeleting(item)}>删除</Button></div> }] : []),
...(canManage ? [{ key: 'actions', title: '操作', isActions: true, width: 160, render: (item) => <div className="flex gap-2"><Button size="sm" variant="secondary" onPress={() => setSelected(item)}>查看</Button><Button size="sm" variant="danger-soft" onPress={() => setDeleting(item)}>删除</Button></div> }] : []),
]} />
: null
}
]} />}
{!
error
? <DataTablePagination isDisabled={loading} page={page} pageSize={pageSize} pageSizeOptions={[20, 50, 100]} total={total} onPageChange={setPage} onPageSizeChange={(size) => { setPageSize(size); setPage(0); }} /> : null}
{!
loadFailed
? <DataTablePagination isDisabled={loading} page={page} pageSize={pageSize} pageSizeOptions={[20, 50, 100]} total={total} onPageChange={setPage} onPageSizeChange={(size) => { setPageSize(size); setPage(0); }} /> : null}
</Card.Content>
</Card.Content>
</Card>
</Card>
<XinyuanModal isOpen={Boolean(selected)} onOpenChange={(open) => { if (!open) setSelected(null); }} size="3xl" title={selected?.title || '执行结果'} footer={<Button variant="primary" onPress={() => setSelected(null)}>关闭</Button>}>
<XinyuanModal isOpen={Boolean(selected)} onOpenChange={(open) => { if (!open) setSelected(null); }} size="3xl" title={selected?.title || '执行结果'} footer={<Button variant="primary" onPress={() => setSelected(null)}>关闭</Button>}>
...
@@ -87,6 +91,6 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
...
@@ -87,6 +91,6 @@ export default function AuditIndicatorResultsPage({ subjectName, recordName, con
<p className="text-sm text-muted">执行时间:{formatTime(selected?.metadata?.execution_time)}</p>
<p className="text-sm text-muted">执行时间:{formatTime(selected?.metadata?.execution_time)}</p>
</div>
</div>
</XinyuanModal>
</XinyuanModal>
<XinyuanModal isOpen={Boolean(deleting)} onOpenChange={(open) => { if (!open && !pending) setDeleting(null); }} title="删除执行结果" footer={<><Button variant="secondary" isDisabled={pending} onPress={() => setDeleting(null)}>取消</Button><Button variant="danger" isPending={pending} onPress={deleteResult}>确认删除</Button></>}><p>确定删除“{deleting?.title}”吗?</p>
{error ? <p className="mt-3 text-danger" role="alert">{error}</p> : null}
</XinyuanModal>
<XinyuanModal isOpen={Boolean(deleting)} onOpenChange={(open) => { if (!open && !pending) setDeleting(null); }} title="删除执行结果" footer={<><Button variant="secondary" isDisabled={pending} onPress={() => setDeleting(null)}>取消</Button><Button variant="danger" isPending={pending} onPress={deleteResult}>确认删除</Button></>}><p>确定删除“{deleting?.title}”吗?</p></XinyuanModal>
</div>;
</div>;
}
}
src/pages/AuditIndicatorView.jsx
View file @
36617df8
import
{
Alert
,
Button
,
Card
,
Chip
,
Input
,
Spinner
}
from
'@heroui/react'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
,
Card
,
Chip
,
Input
,
Spinner
}
from
'@heroui/react'
;
import
LoadErrorState
from
'@/components/LoadErrorState'
;
import
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
import
{
history
}
from
'umi'
;
import
{
DataTable
}
from
'@/components/DataTable'
;
import
{
DataTable
}
from
'@/components/DataTable'
;
...
@@ -26,8 +28,10 @@ export default function AuditIndicatorView({ subjectName, config }) {
...
@@ -26,8 +28,10 @@ export default function AuditIndicatorView({ subjectName, config }) {
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
loadingGroups
,
setLoadingGroups
]
=
useState
(
true
);
const
[
loadingGroups
,
setLoadingGroups
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
notice
,
setNotice
]
=
useState
(
''
);
const
[
groupsLoadFailed
,
setGroupsLoadFailed
]
=
useState
(
false
);
const
[
indicatorLoadFailed
,
setIndicatorLoadFailed
]
=
useState
(
false
);
const
[,
setNotice
]
=
useFeedbackState
(
'success'
);
const
[
refresh
,
setRefresh
]
=
useState
(
0
);
const
[
refresh
,
setRefresh
]
=
useState
(
0
);
const
[
modal
,
setModal
]
=
useState
(
null
);
const
[
modal
,
setModal
]
=
useState
(
null
);
const
[
draft
,
setDraft
]
=
useState
({});
const
[
draft
,
setDraft
]
=
useState
({});
...
@@ -35,17 +39,17 @@ export default function AuditIndicatorView({ subjectName, config }) {
...
@@ -35,17 +39,17 @@ export default function AuditIndicatorView({ subjectName, config }) {
const
[
unlinked
,
setUnlinked
]
=
useState
(
null
);
const
[
unlinked
,
setUnlinked
]
=
useState
(
null
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
executing
,
setExecuting
]
=
useState
(
''
);
const
[
executing
,
setExecuting
]
=
useState
(
''
);
const
[
formError
,
setFormError
]
=
useState
(
'
'
);
const
[
,
setFormError
]
=
useFeedbackState
(
'error
'
);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
setLoadingGroups
(
true
);
setError
(
''
);
setLoadingGroups
(
true
);
set
GroupsLoadFailed
(
false
);
set
Error
(
''
);
loadRecords
(
groupSubject
,
0
,
100
).
then
((
response
)
=>
{
loadRecords
(
groupSubject
,
0
,
100
).
then
((
response
)
=>
{
if
(
cancelled
)
return
;
if
(
cancelled
)
return
;
const
next
=
response
?.
records
||
[];
const
next
=
response
?.
records
||
[];
setGroups
(
next
);
setGroups
(
next
);
setActive
((
current
)
=>
next
.
some
((
item
)
=>
(
item
.
name
||
item
.
_id
)
===
current
)
?
current
:
next
[
0
]?.
name
||
next
[
0
]?.
_id
||
''
);
setActive
((
current
)
=>
next
.
some
((
item
)
=>
(
item
.
name
||
item
.
_id
)
===
current
)
?
current
:
next
[
0
]?.
name
||
next
[
0
]?.
_id
||
''
);
}).
catch
((
requestError
)
=>
{
if
(
!
cancelled
)
setError
(
requestError
?.
message
||
'分组加载失败'
);
})
}).
catch
((
requestError
)
=>
{
if
(
!
cancelled
)
{
setGroupsLoadFailed
(
true
);
setError
(
requestError
?.
message
||
'分组加载失败'
);
}
})
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoadingGroups
(
false
);
});
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoadingGroups
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[
groupSubject
,
refresh
]);
},
[
groupSubject
,
refresh
]);
...
@@ -53,11 +57,12 @@ export default function AuditIndicatorView({ subjectName, config }) {
...
@@ -53,11 +57,12 @@ export default function AuditIndicatorView({ subjectName, config }) {
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
setGroup
(
null
);
setItems
([]);
setGroup
(
null
);
setItems
([]);
setIndicatorLoadFailed
(
false
);
if
(
!
active
)
{
setLoading
(
false
);
return
undefined
;
}
if
(
!
active
)
{
setLoading
(
false
);
return
undefined
;
}
setLoading
(
true
);
setError
(
''
);
setLoading
(
true
);
setError
(
''
);
loadIndicatorGroup
(
active
,
resultSubject
).
then
((
response
)
=>
{
loadIndicatorGroup
(
active
,
resultSubject
).
then
((
response
)
=>
{
if
(
!
cancelled
)
{
setGroup
(
response
.
group
);
setItems
(
response
.
items
);
}
if
(
!
cancelled
)
{
setGroup
(
response
.
group
);
setItems
(
response
.
items
);
}
}).
catch
((
requestError
)
=>
{
if
(
!
cancelled
)
setError
(
requestError
?.
message
||
'指标加载失败'
);
})
}).
catch
((
requestError
)
=>
{
if
(
!
cancelled
)
{
setIndicatorLoadFailed
(
true
);
setError
(
requestError
?.
message
||
'指标加载失败'
);
}
})
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[
active
,
resultSubject
,
refresh
]);
},
[
active
,
resultSubject
,
refresh
]);
...
@@ -118,16 +123,13 @@ export default function AuditIndicatorView({ subjectName, config }) {
...
@@ -118,16 +123,13 @@ export default function AuditIndicatorView({ subjectName, config }) {
return
<
div
className=
"rc-page-stack"
>
return
<
div
className=
"rc-page-stack"
>
<
div
className=
"flex flex-wrap items-center justify-between gap-3"
><
h2
className=
"text-xl font-semibold"
>
量化指标
</
h2
>
{
canManage
?
<
Button
variant=
"secondary"
onPress=
{
()
=>
{
setDraft
({});
setFormError
(
''
);
setModal
(
'group'
);
}
}
>
新建指标分组
</
Button
>
:
null
}
</
div
>
<
div
className=
"flex flex-wrap items-center justify-between gap-3"
><
h2
className=
"text-xl font-semibold"
>
量化指标
</
h2
>
{
canManage
?
<
Button
variant=
"secondary"
onPress=
{
()
=>
{
setDraft
({});
setFormError
(
''
);
setModal
(
'group'
);
}
}
>
新建指标分组
</
Button
>
:
null
}
</
div
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
><
Button
size=
"sm"
variant=
"ghost"
onPress=
{
()
=>
setRefresh
((
value
)
=>
value
+
1
)
}
>
重试
</
Button
></
Alert
>
:
null
}
{
notice
?
<
div
className=
"text-sm text-success"
role=
"status"
>
{
notice
}
</
div
>
:
null
}
{
loadingGroups
?
<
Spinner
aria
-
label=
"加载指标分组"
/>
:
<
div
className=
"flex flex-wrap gap-2"
aria
-
label=
"指标分组"
>
{
groups
.
map
((
item
)
=>
<
Button
key=
{
item
.
name
||
item
.
_id
}
aria
-
pressed=
{
active
===
(
item
.
name
||
item
.
_id
)
}
variant=
{
active
===
(
item
.
name
||
item
.
_id
)
?
'primary'
:
'secondary'
}
onPress=
{
()
=>
{
setActive
(
item
.
name
||
item
.
_id
);
setKeyword
(
''
);
}
}
>
{
item
.
title
}
<
Chip
size=
"sm"
>
{
item
.
metadata
?.
indicators
?.
length
||
0
}
</
Chip
></
Button
>)
}
</
div
>
}
{
loadingGroups
?
<
Spinner
aria
-
label=
"加载指标分组"
/>
:
<
div
className=
"flex flex-wrap gap-2"
aria
-
label=
"指标分组"
>
{
groups
.
map
((
item
)
=>
<
Button
key=
{
item
.
name
||
item
.
_id
}
aria
-
pressed=
{
active
===
(
item
.
name
||
item
.
_id
)
}
variant=
{
active
===
(
item
.
name
||
item
.
_id
)
?
'primary'
:
'secondary'
}
onPress=
{
()
=>
{
setActive
(
item
.
name
||
item
.
_id
);
setKeyword
(
''
);
}
}
>
{
item
.
title
}
<
Chip
size=
"sm"
>
{
item
.
metadata
?.
indicators
?.
length
||
0
}
</
Chip
></
Button
>)
}
</
div
>
}
<
Card
>
<
Card
>
<
Card
.
Header
className=
"flex-row flex-wrap items-center gap-3"
><
Card
.
Title
className=
"mr-auto"
>
{
group
?.
title
||
'指标列表'
}
</
Card
.
Title
><
Input
aria
-
label=
"搜索指标"
className=
"w-full sm:w-64"
placeholder=
"搜索指标名称"
value=
{
keyword
}
onChange=
{
(
event
)
=>
setKeyword
(
event
.
target
.
value
)
}
/>
{
canManage
&&
group
?
<
Button
variant=
"primary"
isDisabled=
{
Boolean
(
executing
)
}
onPress=
{
()
=>
openEditor
(
null
)
}
>
新增指标
</
Button
>
:
null
}
</
Card
.
Header
>
<
Card
.
Header
className=
"flex-row flex-wrap items-center gap-3"
><
Card
.
Title
className=
"mr-auto"
>
{
group
?.
title
||
'指标列表'
}
</
Card
.
Title
><
Input
aria
-
label=
"搜索指标"
className=
"w-full sm:w-64"
placeholder=
"搜索指标名称"
value=
{
keyword
}
onChange=
{
(
event
)
=>
setKeyword
(
event
.
target
.
value
)
}
/>
{
canManage
&&
group
?
<
Button
variant=
"primary"
isDisabled=
{
Boolean
(
executing
)
}
onPress=
{
()
=>
openEditor
(
null
)
}
>
新增指标
</
Button
>
:
null
}
</
Card
.
Header
>
<
Card
.
Content
>
<
DataTable
ariaLabel=
"量化指标"
columns=
{
columns
}
rows=
{
items
.
filter
((
item
)
=>
String
(
item
.
metadata
?.
indicator_name
||
item
.
title
||
''
).
includes
(
keyword
))
}
isLoading=
{
loading
||
loadingGroups
}
getRowKey=
{
(
item
)
=>
item
.
_id
}
emptyTitle=
{
groups
.
length
?
'暂无指标'
:
'暂无指标分组'
}
/>
</
Card
.
Content
>
<
Card
.
Content
>
{
!
loading
&&
!
loadingGroups
&&
(
groupsLoadFailed
||
indicatorLoadFailed
)
?
<
LoadErrorState
title=
"指标加载失败"
onRetry=
{
()
=>
setRefresh
((
value
)
=>
value
+
1
)
}
/>
:
<
DataTable
ariaLabel=
"量化指标"
columns=
{
columns
}
rows=
{
items
.
filter
((
item
)
=>
String
(
item
.
metadata
?.
indicator_name
||
item
.
title
||
''
).
includes
(
keyword
))
}
isLoading=
{
loading
||
loadingGroups
}
getRowKey=
{
(
item
)
=>
item
.
_id
}
emptyTitle=
{
groups
.
length
?
'暂无指标'
:
'暂无指标分组'
}
/>
}
</
Card
.
Content
>
</
Card
>
</
Card
>
<
XinyuanModal
isOpen=
{
Boolean
(
modal
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
)
closeModal
();
}
}
title=
{
modal
===
'group'
?
'新建指标分组'
:
modal
===
'delete'
?
'删除指标'
:
editing
?
'编辑量化指标'
:
'新建量化指标'
}
footer=
{
<><
Button
variant=
"secondary"
isDisabled=
{
saving
}
onPress=
{
closeModal
}
>
取消
</
Button
><
Button
variant=
{
modal
===
'delete'
?
'danger'
:
'primary'
}
isPending=
{
saving
}
onPress=
{
save
}
>
{
unlinked
?
'重试关联'
:
modal
===
'delete'
?
'确认删除'
:
'保存'
}
</
Button
></>
}
>
<
XinyuanModal
isOpen=
{
Boolean
(
modal
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
)
closeModal
();
}
}
title=
{
modal
===
'group'
?
'新建指标分组'
:
modal
===
'delete'
?
'删除指标'
:
editing
?
'编辑量化指标'
:
'新建量化指标'
}
footer=
{
<><
Button
variant=
"secondary"
isDisabled=
{
saving
}
onPress=
{
closeModal
}
>
取消
</
Button
><
Button
variant=
{
modal
===
'delete'
?
'danger'
:
'primary'
}
isPending=
{
saving
}
onPress=
{
save
}
>
{
unlinked
?
'重试关联'
:
modal
===
'delete'
?
'确认删除'
:
'保存'
}
</
Button
></>
}
>
<
div
className=
"space-y-4"
>
<
div
className=
"space-y-4"
>
{
formError
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
formError
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
{
modal
===
'delete'
?
<
p
>
确定删除“
{
editing
?.
metadata
?.
indicator_name
||
editing
?.
title
}
”吗?
</
p
>
:
modal
===
'group'
?
<
label
className=
"grid gap-2 text-sm"
>
分组名称
<
Input
aria
-
label=
"分组名称"
required
value=
{
draft
.
group_name
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
group_name
:
event
.
target
.
value
})
}
/></
label
>
:
!
unlinked
?
<>
{
modal
===
'delete'
?
<
p
>
确定删除“
{
editing
?.
metadata
?.
indicator_name
||
editing
?.
title
}
”吗?
</
p
>
:
modal
===
'group'
?
<
label
className=
"grid gap-2 text-sm"
>
分组名称
<
Input
aria
-
label=
"分组名称"
required
value=
{
draft
.
group_name
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
group_name
:
event
.
target
.
value
})
}
/></
label
>
:
!
unlinked
?
<>
<
label
className=
"grid gap-2 text-sm"
>
指标名称 *
<
Input
aria
-
label=
"指标名称"
required
value=
{
draft
.
indicator_name
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
indicator_name
:
event
.
target
.
value
})
}
/></
label
>
<
label
className=
"grid gap-2 text-sm"
>
指标名称 *
<
Input
aria
-
label=
"指标名称"
required
value=
{
draft
.
indicator_name
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
indicator_name
:
event
.
target
.
value
})
}
/></
label
>
<
ReferenceSearchSelect
label=
"责任人"
isRequired
value=
{
draft
.
owner
}
initialRecords=
{
[
editing
?.
metadata
?.
owner
]
}
loadOptions=
{
LOAD_OWNERS
}
onChange=
{
(
owner
)
=>
setDraft
({
...
draft
,
owner
})
}
/>
<
ReferenceSearchSelect
label=
"责任人"
isRequired
value=
{
draft
.
owner
}
initialRecords=
{
[
editing
?.
metadata
?.
owner
]
}
loadOptions=
{
LOAD_OWNERS
}
onChange=
{
(
owner
)
=>
setDraft
({
...
draft
,
owner
})
}
/>
...
...
src/pages/ConfiguredSpecialViews.jsx
View file @
36617df8
import
LoadErrorState
from
'@/components/LoadErrorState'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
AuditIndicatorView
from
'./AuditIndicatorView'
;
import
AuditIndicatorView
from
'./AuditIndicatorView'
;
import
{
Alert
,
Button
,
Card
,
Input
,
Spinner
,
TextArea
}
from
'@heroui/react'
;
import
{
Button
,
Card
,
Input
,
Spinner
,
TextArea
,
toast
}
from
'@heroui/react'
;
import
{
useEffect
,
useState
}
from
'react'
;
import
{
useEffect
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
import
{
history
}
from
'umi'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
...
@@ -16,7 +18,7 @@ function useSubjectRecords(subjectName, groupName) {
...
@@ -16,7 +18,7 @@ function useSubjectRecords(subjectName, groupName) {
const
[
group
,
setGroup
]
=
useState
(
null
);
const
[
group
,
setGroup
]
=
useState
(
null
);
const
[
items
,
setItems
]
=
useState
([]);
const
[
items
,
setItems
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
reload
=
async
()
=>
{
const
reload
=
async
()
=>
{
if
(
!
groupName
)
return
;
if
(
!
groupName
)
return
;
setLoading
(
true
);
setError
(
''
);
setLoading
(
true
);
setError
(
''
);
...
@@ -34,11 +36,12 @@ function useSubjectRecords(subjectName, groupName) {
...
@@ -34,11 +36,12 @@ function useSubjectRecords(subjectName, groupName) {
function
ProcessListView
({
subjectName
,
config
})
{
function
ProcessListView
({
subjectName
,
config
})
{
const
groupSubject
=
config
?.
group_subject
||
'process_flow_group'
;
const
groupSubject
=
config
?.
group_subject
||
'process_flow_group'
;
const
[
groups
,
setGroups
]
=
useState
([]);
const
[
active
,
setActive
]
=
useState
(
''
);
const
[
modal
,
setModal
]
=
useState
(
false
);
const
[
draft
,
setDraft
]
=
useState
({});
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
deleting
,
setDeleting
]
=
useState
(
false
);
const
[
deleteTarget
,
setDeleteTarget
]
=
useState
(
null
);
const
[
deleteError
,
setDeleteError
]
=
useState
(
''
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
groups
,
setGroups
]
=
useState
([]);
const
[
active
,
setActive
]
=
useState
(
''
);
const
[
modal
,
setModal
]
=
useState
(
false
);
const
[
draft
,
setDraft
]
=
useState
({});
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
deleting
,
setDeleting
]
=
useState
(
false
);
const
[
deleteTarget
,
setDeleteTarget
]
=
useState
(
null
);
const
[,
setDeleteError
]
=
useFeedbackState
(
'error'
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
groupsLoadFailed
,
setGroupsLoadFailed
]
=
useState
(
false
);
const
{
group
,
items
,
loading
,
error
:
groupError
,
reload
}
=
useSubjectRecords
(
subjectName
,
active
);
const
{
group
,
items
,
loading
,
error
:
groupError
,
reload
}
=
useSubjectRecords
(
subjectName
,
active
);
const
loadGroups
=
async
()
=>
{
try
{
const
result
=
await
loadRecords
(
groupSubject
,
0
,
100
);
const
next
=
result
?.
records
||
[];
setGroups
(
next
);
setActive
((
current
)
=>
current
||
next
[
0
]?.
name
||
''
);
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'流程分组加载失败'
);
}
};
const
loadGroups
=
async
()
=>
{
setGroupsLoadFailed
(
false
);
setError
(
''
);
try
{
const
result
=
await
loadRecords
(
groupSubject
,
0
,
100
);
const
next
=
result
?.
records
||
[];
setGroups
(
next
);
setActive
((
current
)
=>
current
||
next
[
0
]?.
name
||
''
);
}
catch
(
requestError
)
{
setGroupsLoadFailed
(
true
);
setError
(
requestError
?.
message
||
'流程分组加载失败'
);
}
};
useEffect
(()
=>
{
loadGroups
();
},
[
groupSubject
]);
useEffect
(()
=>
{
loadGroups
();
},
[
groupSubject
]);
const
save
=
async
()
=>
{
setSaving
(
true
);
setError
(
''
);
try
{
if
(
draft
.
group_name
!==
undefined
)
await
createRecord
({
subject
:
groupSubject
,
title
:
draft
.
group_name
,
metadata
:
{
group_name
:
draft
.
group_name
,
process_list
:
[]
}
});
else
if
(
draft
.
editName
)
await
updateRecord
(
draft
.
editName
,
{
title
:
draft
.
title
,
metadata
:
{
remark
:
draft
.
remark
,
source
:
draft
.
source
||
null
}
});
else
{
const
created
=
await
createRecord
({
subject
:
subjectName
,
title
:
draft
.
title
,
metadata
:
{
remark
:
draft
.
remark
,
source
:
draft
.
source
||
null
}
});
await
updateRecord
(
group
.
name
,
{
metadata
:
{
...
group
.
metadata
,
process_list
:
[...(
group
.
metadata
?.
process_list
||
[]).
map
(
valueOf
),
created
.
_id
]
}
});
}
setModal
(
false
);
setDraft
({});
await
loadGroups
();
await
reload
();
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'保存失败'
);
}
finally
{
setSaving
(
false
);
}
};
const
save
=
async
()
=>
{
setSaving
(
true
);
setError
(
''
);
try
{
if
(
draft
.
group_name
!==
undefined
)
await
createRecord
({
subject
:
groupSubject
,
title
:
draft
.
group_name
,
metadata
:
{
group_name
:
draft
.
group_name
,
process_list
:
[]
}
});
else
if
(
draft
.
editName
)
await
updateRecord
(
draft
.
editName
,
{
title
:
draft
.
title
,
metadata
:
{
remark
:
draft
.
remark
,
source
:
draft
.
source
||
null
}
});
else
{
const
created
=
await
createRecord
({
subject
:
subjectName
,
title
:
draft
.
title
,
metadata
:
{
remark
:
draft
.
remark
,
source
:
draft
.
source
||
null
}
});
await
updateRecord
(
group
.
name
,
{
metadata
:
{
...
group
.
metadata
,
process_list
:
[...(
group
.
metadata
?.
process_list
||
[]).
map
(
valueOf
),
created
.
_id
]
}
});
}
setModal
(
false
);
setDraft
({});
toast
.
success
(
'保存成功'
,
{
timeout
:
4000
});
await
loadGroups
();
await
reload
();
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'保存失败'
);
}
finally
{
setSaving
(
false
);
}
};
const
confirmDeleteProcess
=
async
()
=>
{
const
confirmDeleteProcess
=
async
()
=>
{
if
(
!
group
||
!
deleteTarget
)
return
;
if
(
!
group
||
!
deleteTarget
)
return
;
setDeleting
(
true
);
setDeleteError
(
''
);
setDeleting
(
true
);
setDeleteError
(
''
);
...
@@ -46,11 +49,12 @@ function ProcessListView({ subjectName, config }) {
...
@@ -46,11 +49,12 @@ function ProcessListView({ subjectName, config }) {
await
removeRecord
(
deleteTarget
.
name
||
deleteTarget
.
_id
);
await
removeRecord
(
deleteTarget
.
name
||
deleteTarget
.
_id
);
await
updateRecord
(
group
.
name
,
{
metadata
:
{
...
group
.
metadata
,
process_list
:
(
group
.
metadata
?.
process_list
||
[]).
map
(
valueOf
).
filter
((
id
)
=>
id
!==
(
deleteTarget
.
_id
||
deleteTarget
.
name
))
}
});
await
updateRecord
(
group
.
name
,
{
metadata
:
{
...
group
.
metadata
,
process_list
:
(
group
.
metadata
?.
process_list
||
[]).
map
(
valueOf
).
filter
((
id
)
=>
id
!==
(
deleteTarget
.
_id
||
deleteTarget
.
name
))
}
});
setDeleteTarget
(
null
);
setDeleteTarget
(
null
);
toast
.
success
(
'流程已删除'
,
{
timeout
:
4000
});
await
reload
();
await
reload
();
}
catch
(
requestError
)
{
setDeleteError
(
requestError
?.
message
||
'删除失败'
);
}
}
catch
(
requestError
)
{
setDeleteError
(
requestError
?.
message
||
'删除失败'
);
}
finally
{
setDeleting
(
false
);
}
finally
{
setDeleting
(
false
);
}
};
};
return
<
div
className=
"rc-page-stack"
><
h2
className=
"mb-4 text-xl font-semibold"
>
流程管理
</
h2
>
{
error
||
groupError
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
||
groupError
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
<
GroupTabs
groups=
{
groups
}
active=
{
active
}
onChange=
{
setActive
}
onCreate=
{
()
=>
{
setDraft
({
group_name
:
''
});
setModal
(
true
);
}
}
label=
"流程分组"
/><
Card
><
Card
.
Header
><
Card
.
Title
className=
"mr-auto"
>
{
group
?.
title
||
'请选择流程分组'
}
</
Card
.
Title
>
{
group
?
<
Button
variant=
"primary"
onPress=
{
()
=>
{
setDraft
({});
setModal
(
true
);
}
}
>
新增流程
</
Button
>
:
null
}
</
Card
.
Header
><
Card
.
Content
>
{
loading
?
<
div
className=
"flex min-h-32 items-center justify-center"
><
Spinner
/></
div
>
:
!
items
.
length
?
<
div
className=
"py-16 text-center text-sm text-muted"
>
暂无流程
</
div
>
:
<
ol
className=
"space-y-3"
>
{
items
.
map
((
item
,
index
)
=>
<
li
className=
"flex items-center gap-3 rounded-lg border border-default p-3"
key=
{
item
.
name
||
item
.
_id
}
><
span
className=
"grid h-8 w-8 place-items-center rounded-full bg-accent/10 text-sm font-semibold text-accent"
>
{
index
+
1
}
</
span
><
div
className=
"min-w-0 flex-1"
><
div
className=
"truncate font-medium"
>
{
item
.
title
||
item
.
name
}
</
div
><
div
className=
"text-sm text-muted"
>
{
item
.
metadata
?.
remark
||
'暂无说明'
}
</
div
></
div
><
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
{
setDraft
({
...
item
.
metadata
,
title
:
item
.
title
,
editName
:
item
.
name
});
setModal
(
true
);
}
}
>
编辑
</
Button
><
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
history
.
push
(
`/${subjectName}/${item.name || item._id}`
)
}
>
查看
</
Button
><
Button
size=
"sm"
variant=
"danger"
onPress=
{
()
=>
{
setDeleteError
(
''
);
setDeleteTarget
(
item
);
}
}
>
删除
</
Button
></
li
>)
}
</
ol
>
}
</
Card
.
Content
></
Card
><
XinyuanModal
isOpen=
{
Boolean
(
deleteTarget
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
&&
!
deleting
)
setDeleteTarget
(
null
);
}
}
title=
"删除流程"
footer=
{
<><
Button
variant=
"secondary"
isDisabled=
{
deleting
}
onPress=
{
()
=>
setDeleteTarget
(
null
)
}
>
取消
</
Button
><
Button
variant=
"danger"
isPending=
{
deleting
}
onPress=
{
confirmDeleteProcess
}
>
确认删除
</
Button
></>
}
><
p
>
确定删除“
{
titleOf
(
deleteTarget
)
}
”吗?
</
p
>
{
deleteError
?
<
p
className=
"mt-3 text-danger"
role=
"alert"
>
{
deleteError
}
</
p
>
:
null
}
</
XinyuanModal
><
XinyuanModal
isOpen=
{
modal
}
onOpenChange=
{
setModal
}
title=
{
draft
.
group_name
!==
undefined
?
'新建流程分组'
:
draft
.
editName
?
'编辑流程'
:
'新建流程'
}
footer=
{
<><
Button
variant=
"secondary"
onPress=
{
()
=>
setModal
(
false
)
}
>
取消
</
Button
><
Button
variant=
"primary"
isPending=
{
saving
}
onPress=
{
save
}
>
保存
</
Button
></>
}
><
div
className=
"grid gap-4"
>
{
draft
.
group_name
!==
undefined
?
<
Input
label=
"分组名称"
value=
{
draft
.
group_name
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
group_name
:
event
.
target
.
value
})
}
/>
:
<><
Input
label=
"流程名称"
value=
{
draft
.
title
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
title
:
event
.
target
.
value
})
}
/><
TextArea
label=
"流程说明"
value=
{
draft
.
remark
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
remark
:
event
.
target
.
value
})
}
/></>
}
</
div
></
XinyuanModal
></
div
>;
return
<
div
className=
"rc-page-stack"
><
h2
className=
"mb-4 text-xl font-semibold"
>
流程管理
</
h2
>
<
GroupTabs
groups=
{
groups
}
active=
{
active
}
onChange=
{
setActive
}
onCreate=
{
()
=>
{
setDraft
({
group_name
:
''
});
setModal
(
true
);
}
}
label=
"流程分组"
/><
Card
><
Card
.
Header
><
Card
.
Title
className=
"mr-auto"
>
{
group
?.
title
||
'请选择流程分组'
}
</
Card
.
Title
>
{
group
?
<
Button
variant=
"primary"
onPress=
{
()
=>
{
setDraft
({});
setModal
(
true
);
}
}
>
新增流程
</
Button
>
:
null
}
</
Card
.
Header
><
Card
.
Content
>
{
loading
?
<
div
className=
"flex min-h-32 items-center justify-center"
><
Spinner
/></
div
>
:
groupsLoadFailed
||
groupError
?
<
LoadErrorState
title=
"流程加载失败"
onRetry=
{
()
=>
{
loadGroups
();
reload
();
}
}
/>
:
!
items
.
length
?
<
div
className=
"py-16 text-center text-sm text-muted"
>
暂无流程
</
div
>
:
<
ol
className=
"space-y-3"
>
{
items
.
map
((
item
,
index
)
=>
<
li
className=
"flex items-center gap-3 rounded-lg border border-default p-3"
key=
{
item
.
name
||
item
.
_id
}
><
span
className=
"grid h-8 w-8 place-items-center rounded-full bg-accent/10 text-sm font-semibold text-accent"
>
{
index
+
1
}
</
span
><
div
className=
"min-w-0 flex-1"
><
div
className=
"truncate font-medium"
>
{
item
.
title
||
item
.
name
}
</
div
><
div
className=
"text-sm text-muted"
>
{
item
.
metadata
?.
remark
||
'暂无说明'
}
</
div
></
div
><
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
{
setDraft
({
...
item
.
metadata
,
title
:
item
.
title
,
editName
:
item
.
name
});
setModal
(
true
);
}
}
>
编辑
</
Button
><
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
history
.
push
(
`/${subjectName}/${item.name || item._id}`
)
}
>
查看
</
Button
><
Button
size=
"sm"
variant=
"danger"
onPress=
{
()
=>
{
setDeleteError
(
''
);
setDeleteTarget
(
item
);
}
}
>
删除
</
Button
></
li
>)
}
</
ol
>
}
</
Card
.
Content
></
Card
><
XinyuanModal
isOpen=
{
Boolean
(
deleteTarget
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
&&
!
deleting
)
setDeleteTarget
(
null
);
}
}
title=
"删除流程"
footer=
{
<><
Button
variant=
"secondary"
isDisabled=
{
deleting
}
onPress=
{
()
=>
setDeleteTarget
(
null
)
}
>
取消
</
Button
><
Button
variant=
"danger"
isPending=
{
deleting
}
onPress=
{
confirmDeleteProcess
}
>
确认删除
</
Button
></>
}
><
p
>
确定删除“
{
titleOf
(
deleteTarget
)
}
”吗?
</
p
>
</
XinyuanModal
><
XinyuanModal
isOpen=
{
modal
}
onOpenChange=
{
setModal
}
title=
{
draft
.
group_name
!==
undefined
?
'新建流程分组'
:
draft
.
editName
?
'编辑流程'
:
'新建流程'
}
footer=
{
<><
Button
variant=
"secondary"
onPress=
{
()
=>
setModal
(
false
)
}
>
取消
</
Button
><
Button
variant=
"primary"
isPending=
{
saving
}
onPress=
{
save
}
>
保存
</
Button
></>
}
><
div
className=
"grid gap-4"
>
{
draft
.
group_name
!==
undefined
?
<
Input
label=
"分组名称"
value=
{
draft
.
group_name
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
group_name
:
event
.
target
.
value
})
}
/>
:
<><
Input
label=
"流程名称"
value=
{
draft
.
title
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
title
:
event
.
target
.
value
})
}
/><
TextArea
label=
"流程说明"
value=
{
draft
.
remark
||
''
}
onChange=
{
(
event
)
=>
setDraft
({
...
draft
,
remark
:
event
.
target
.
value
})
}
/></>
}
</
div
></
XinyuanModal
></
div
>;
}
}
export
default
function
ConfiguredSpecialViews
({
view
,
subjectName
,
config
})
{
export
default
function
ConfiguredSpecialViews
({
view
,
subjectName
,
config
})
{
...
...
src/pages/GlobalSearchPage.jsx
View file @
36617df8
import
{
Alert
,
Button
,
Card
,
Chip
,
Input
,
Spinner
}
from
'@heroui/react'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Button
,
Card
,
Chip
,
Input
,
Spinner
}
from
'@heroui/react'
;
import
LoadErrorState
from
'@/components/LoadErrorState'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
history
,
useLocation
}
from
'umi'
;
import
{
history
,
useLocation
}
from
'umi'
;
import
{
APPLICATION_LAYOUT
}
from
'@/config/ApplicationConfig'
;
import
{
APPLICATION_LAYOUT
}
from
'@/config/ApplicationConfig'
;
...
@@ -51,7 +53,7 @@ export default function GlobalSearchPage() {
...
@@ -51,7 +53,7 @@ export default function GlobalSearchPage() {
const
[
groups
,
setGroups
]
=
useState
([]);
const
[
groups
,
setGroups
]
=
useState
([]);
const
[
selectedGroupName
,
setSelectedGroupName
]
=
useState
(
''
);
const
[
selectedGroupName
,
setSelectedGroupName
]
=
useState
(
''
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
[
searched
,
setSearched
]
=
useState
(
false
);
const
[
searched
,
setSearched
]
=
useState
(
false
);
const
selectedGroup
=
useMemo
(
const
selectedGroup
=
useMemo
(
...
@@ -111,7 +113,7 @@ export default function GlobalSearchPage() {
...
@@ -111,7 +113,7 @@ export default function GlobalSearchPage() {
</
Card
.
Header
>
</
Card
.
Header
>
<
Card
.
Content
>
<
Card
.
Content
>
{
loading
?
<
div
className=
"flex min-h-32 items-center justify-center"
><
Spinner
/></
div
>
:
null
}
{
loading
?
<
div
className=
"flex min-h-32 items-center justify-center"
><
Spinner
/></
div
>
:
null
}
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
{
!
loading
&&
error
?
<
LoadErrorState
title=
"搜索失败"
onRetry=
{
executeSearch
}
/
>
:
null
}
{
!
loading
&&
!
error
&&
searched
&&
!
groups
.
length
?
<
div
className=
"py-16 text-center text-sm text-muted"
>
暂无搜索结果
</
div
>
:
null
}
{
!
loading
&&
!
error
&&
searched
&&
!
groups
.
length
?
<
div
className=
"py-16 text-center text-sm text-muted"
>
暂无搜索结果
</
div
>
:
null
}
{
!
loading
&&
!
error
&&
groups
.
length
?
(
{
!
loading
&&
!
error
&&
groups
.
length
?
(
<
div
className=
"global-search-layout"
>
<
div
className=
"global-search-layout"
>
...
...
src/pages/LawRecordPage.jsx
View file @
36617df8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
import
{
Alert
,
Button
,
Button
,
Card
,
Card
,
Chip
,
Chip
,
Input
,
Input
,
Separator
,
Separator
,
Spinner
,
Spinner
,
toast
,
}
from
'@heroui/react'
;
}
from
'@heroui/react'
;
import
{
useContext
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
{
useContext
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
{
history
,
useLocation
,
useParams
,
useSearchParams
}
from
'umi'
;
import
{
history
,
useLocation
,
useParams
,
useSearchParams
}
from
'umi'
;
...
@@ -28,6 +29,7 @@ import HeroSwitch from '@/components/HeroSwitch';
...
@@ -28,6 +29,7 @@ import HeroSwitch from '@/components/HeroSwitch';
import
PdfAttachmentViewer
from
'@/components/PdfAttachmentViewer'
;
import
PdfAttachmentViewer
from
'@/components/PdfAttachmentViewer'
;
import
{
TableActionButton
,
TableActionGroup
}
from
'@/components/TableActionButton'
;
import
{
TableActionButton
,
TableActionGroup
}
from
'@/components/TableActionButton'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
LoadErrorState
from
'@/components/LoadErrorState'
;
import
{
import
{
LawRecordFieldEditor
as
EditField
,
LawRecordFieldEditor
as
EditField
,
LawRecordFieldValue
as
DetailField
,
LawRecordFieldValue
as
DetailField
,
...
@@ -204,7 +206,7 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
...
@@ -204,7 +206,7 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
[
uploading
,
setUploading
]
=
useState
(
false
);
const
[
removing
,
setRemoving
]
=
useState
(
false
);
const
[
removing
,
setRemoving
]
=
useState
(
false
);
const
[
removeTarget
,
setRemoveTarget
]
=
useState
(
null
);
const
[
removeTarget
,
setRemoveTarget
]
=
useState
(
null
);
const
[
removeError
,
setRemoveError
]
=
useState
(
'
'
);
const
[
,
setRemoveError
]
=
useFeedbackState
(
'error
'
);
const
uploadFiles
=
async
(
files
)
=>
{
const
uploadFiles
=
async
(
files
)
=>
{
if
(
!
field
||
!
files
?.
length
)
return
;
if
(
!
field
||
!
files
?.
length
)
return
;
...
@@ -213,6 +215,7 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
...
@@ -213,6 +215,7 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
try
{
try
{
const
uploaded
=
await
Promise
.
all
(
files
.
map
((
file
)
=>
uploadDocument
(
file
)));
const
uploaded
=
await
Promise
.
all
(
files
.
map
((
file
)
=>
uploadDocument
(
file
)));
await
onPatchMetadata
(
field
,
[...
attachments
,
...
uploaded
]);
await
onPatchMetadata
(
field
,
[...
attachments
,
...
uploaded
]);
toast
.
success
(
'附件上传成功'
,
{
timeout
:
4000
});
}
catch
(
error
)
{
}
catch
(
error
)
{
setError
(
error
?.
message
||
'附件上传失败'
);
setError
(
error
?.
message
||
'附件上传失败'
);
}
finally
{
}
finally
{
...
@@ -228,6 +231,7 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
...
@@ -228,6 +231,7 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
try
{
try
{
await
onPatchMetadata
(
field
,
attachments
.
filter
((
item
)
=>
(
item
?.
_id
||
item
?.
url
||
item
?.
name
||
item
?.
title
)
!==
key
));
await
onPatchMetadata
(
field
,
attachments
.
filter
((
item
)
=>
(
item
?.
_id
||
item
?.
url
||
item
?.
name
||
item
?.
title
)
!==
key
));
setRemoveTarget
(
null
);
setRemoveTarget
(
null
);
toast
.
success
(
'附件已移除'
,
{
timeout
:
4000
});
}
catch
(
error
)
{
}
catch
(
error
)
{
setRemoveError
(
error
?.
message
||
'附件移除失败'
);
setRemoveError
(
error
?.
message
||
'附件移除失败'
);
}
finally
{
}
finally
{
...
@@ -276,7 +280,6 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
...
@@ -276,7 +280,6 @@ function AttachmentPanel({ config, record, onPatchMetadata, setError }) {
title=
"移除附件"
title=
"移除附件"
>
>
<
p
>
确定移除“
{
removeTarget
?.
title
||
removeTarget
?.
name
||
removeTarget
?.
url
||
'该附件'
}
”吗?
</
p
>
<
p
>
确定移除“
{
removeTarget
?.
title
||
removeTarget
?.
name
||
removeTarget
?.
url
||
'该附件'
}
”吗?
</
p
>
{
removeError
?
<
p
className=
"mt-3 text-danger"
role=
"alert"
>
{
removeError
}
</
p
>
:
null
}
</
XinyuanModal
>
</
XinyuanModal
>
</
Card
>
</
Card
>
);
);
...
@@ -369,7 +372,9 @@ function LawRecordDetail() {
...
@@ -369,7 +372,9 @@ function LawRecordDetail() {
const
[
deletingRecord
,
setDeletingRecord
]
=
useState
(
false
);
const
[
deletingRecord
,
setDeletingRecord
]
=
useState
(
false
);
const
[
deleteRecordOpen
,
setDeleteRecordOpen
]
=
useState
(
false
);
const
[
deleteRecordOpen
,
setDeleteRecordOpen
]
=
useState
(
false
);
const
[
historyItems
,
setHistoryItems
]
=
useState
([]);
const
[
historyItems
,
setHistoryItems
]
=
useState
([]);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[,
setSaveSuccess
]
=
useFeedbackState
(
'success'
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
[
collections
,
setCollections
]
=
useState
(()
=>
loadStorageArray
(
COLLECTION_STORAGE_KEY
));
const
[
collections
,
setCollections
]
=
useState
(()
=>
loadStorageArray
(
COLLECTION_STORAGE_KEY
));
const
[
reading
,
setReading
]
=
useState
(()
=>
loadReadingPreferences
());
const
[
reading
,
setReading
]
=
useState
(()
=>
loadReadingPreferences
());
const
[
findText
,
setFindText
]
=
useState
(
searchParams
.
get
(
'keyword'
)
||
''
);
const
[
findText
,
setFindText
]
=
useState
(
searchParams
.
get
(
'keyword'
)
||
''
);
...
@@ -378,10 +383,11 @@ function LawRecordDetail() {
...
@@ -378,10 +383,11 @@ function LawRecordDetail() {
const
[
matchIndex
,
setMatchIndex
]
=
useState
(
0
);
const
[
matchIndex
,
setMatchIndex
]
=
useState
(
0
);
const
[
referenceRecords
,
setReferenceRecords
]
=
useState
([]);
const
[
referenceRecords
,
setReferenceRecords
]
=
useState
([]);
const
[
referenceLoading
,
setReferenceLoading
]
=
useState
(
false
);
const
[
referenceLoading
,
setReferenceLoading
]
=
useState
(
false
);
const
[
referenceError
,
setReferenceError
]
=
useState
(
'
'
);
const
[
,
setReferenceError
]
=
useFeedbackState
(
'error
'
);
const
contentRef
=
useRef
(
null
);
const
contentRef
=
useRef
(
null
);
useEffect
(()
=>
{
useEffect
(()
=>
{
setSaveSuccess
(
''
);
if
(
!
subjectName
||
!
recordName
||
subjectName
===
'search'
)
{
if
(
!
subjectName
||
!
recordName
||
subjectName
===
'search'
)
{
setError
(
'记录路径缺少业务对象或记录标识'
);
setError
(
'记录路径缺少业务对象或记录标识'
);
setLoading
(
false
);
setLoading
(
false
);
...
@@ -405,7 +411,7 @@ function LawRecordDetail() {
...
@@ -405,7 +411,7 @@ function LawRecordDetail() {
if
(
!
cancelled
)
setLoading
(
false
);
if
(
!
cancelled
)
setLoading
(
false
);
});
});
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[
recordName
,
subjectName
]);
},
[
recordName
,
subjectName
,
reloadKey
]);
const
historyConfig
=
useMemo
(()
=>
sideInfo
.
find
((
item
)
=>
item
.
type
===
'history'
)
||
null
,
[
sideInfo
]);
const
historyConfig
=
useMemo
(()
=>
sideInfo
.
find
((
item
)
=>
item
.
type
===
'history'
)
||
null
,
[
sideInfo
]);
...
@@ -560,8 +566,10 @@ function LawRecordDetail() {
...
@@ -560,8 +566,10 @@ function LawRecordDetail() {
};
};
const
save
=
async
()
=>
{
const
save
=
async
()
=>
{
if
(
saving
||
!
record
)
return
;
setSaving
(
true
);
setSaving
(
true
);
setError
(
''
);
setError
(
''
);
setSaveSuccess
(
''
);
try
{
try
{
for
(
const
field
of
editFields
)
{
for
(
const
field
of
editFields
)
{
if
(
isRequiredField
(
field
))
{
if
(
isRequiredField
(
field
))
{
...
@@ -590,6 +598,7 @@ function LawRecordDetail() {
...
@@ -590,6 +598,7 @@ function LawRecordDetail() {
setRecord
((
current
)
=>
({
...
current
,
title
,
content
,
metadata
}));
setRecord
((
current
)
=>
({
...
current
,
title
,
content
,
metadata
}));
setDraft
(
metadata
);
setDraft
(
metadata
);
setEditing
(
false
);
setEditing
(
false
);
setSaveSuccess
(
'详情已保存。'
);
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'保存失败'
);
setError
(
requestError
?.
message
||
'保存失败'
);
}
finally
{
}
finally
{
...
@@ -598,18 +607,24 @@ function LawRecordDetail() {
...
@@ -598,18 +607,24 @@ function LawRecordDetail() {
};
};
const
deleteRecord
=
async
()
=>
{
const
deleteRecord
=
async
()
=>
{
if
(
!
recordName
)
return
;
if
(
!
recordName
||
deletingRecord
)
return
;
setDeletingRecord
(
true
);
setError
(
''
);
try
{
try
{
await
removeRecord
(
recordName
);
await
removeRecord
(
recordName
);
setDeleteRecordOpen
(
false
);
setDeleteRecordOpen
(
false
);
toast
.
success
(
'记录已删除'
,
{
timeout
:
4000
});
history
.
push
(
`/
${
subjectName
}
`
);
history
.
push
(
`/
${
subjectName
}
`
);
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'删除失败'
);
setError
(
requestError
?.
message
||
'删除失败'
);
}
finally
{
setDeletingRecord
(
false
);
}
}
};
};
const
openEditModal
=
()
=>
{
const
openEditModal
=
()
=>
{
setError
(
''
);
setError
(
''
);
setSaveSuccess
(
''
);
setDraft
(
record
?.
metadata
||
{});
setDraft
(
record
?.
metadata
||
{});
setEditing
(
true
);
setEditing
(
true
);
};
};
...
@@ -618,6 +633,7 @@ function LawRecordDetail() {
...
@@ -618,6 +633,7 @@ function LawRecordDetail() {
if
(
saving
)
return
;
if
(
saving
)
return
;
setEditing
(
false
);
setEditing
(
false
);
setError
(
''
);
setError
(
''
);
setSaveSuccess
(
''
);
setDraft
(
record
?.
metadata
||
{});
setDraft
(
record
?.
metadata
||
{});
};
};
...
@@ -626,7 +642,7 @@ function LawRecordDetail() {
...
@@ -626,7 +642,7 @@ function LawRecordDetail() {
}
}
if
(
!
record
)
{
if
(
!
record
)
{
return
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
||
'记录不存在'
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>;
return
<
LoadErrorState
title=
"记录加载失败或不存在"
onRetry=
{
()
=>
setReloadKey
((
value
)
=>
value
+
1
)
}
/
>;
}
}
return
(
return
(
...
@@ -702,13 +718,11 @@ function LawRecordDetail() {
...
@@ -702,13 +718,11 @@ function LawRecordDetail() {
引用识别
引用识别
</
HeroSwitch
>
</
HeroSwitch
>
{
referenceLoading
?
<
span
className=
"text-sm text-muted"
role=
"status"
>
识别中…
</
span
>
:
null
}
{
referenceLoading
?
<
span
className=
"text-sm text-muted"
role=
"status"
>
识别中…
</
span
>
:
null
}
{
referenceError
?
<
span
className=
"text-sm text-danger"
role=
"status"
>
{
referenceError
}
</
span
>
:
null
}
</
div
>
</
div
>
</
div
>
</
div
>
)
:
null
}
)
:
null
}
</
div
>
</
div
>
{
error
&&
!
editing
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
<
div
className=
{
sideInfo
.
length
?
'law-record-layout'
:
'law-record-layout law-record-layout-single'
}
>
<
div
className=
{
sideInfo
.
length
?
'law-record-layout'
:
'law-record-layout law-record-layout-single'
}
>
<
main
className=
"law-record-main"
>
<
main
className=
"law-record-main"
>
...
@@ -799,7 +813,6 @@ function LawRecordDetail() {
...
@@ -799,7 +813,6 @@ function LawRecordDetail() {
size=
"5xl"
size=
"5xl"
title=
"编辑记录"
title=
"编辑记录"
>
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
<
section
aria
-
labelledby=
"law-record-edit-title"
>
<
section
aria
-
labelledby=
"law-record-edit-title"
>
<
h2
className=
"sr-only"
id=
"law-record-edit-title"
>
编辑内容
</
h2
>
<
h2
className=
"sr-only"
id=
"law-record-edit-title"
>
编辑内容
</
h2
>
<
div
className=
"grid gap-5 md:grid-cols-2"
>
<
div
className=
"grid gap-5 md:grid-cols-2"
>
...
@@ -834,7 +847,6 @@ function LawRecordDetail() {
...
@@ -834,7 +847,6 @@ function LawRecordDetail() {
title=
"删除记录"
title=
"删除记录"
>
>
<
p
>
确定删除“
{
record
?.
title
||
record
?.
name
}
”吗?
</
p
>
<
p
>
确定删除“
{
record
?.
title
||
record
?.
name
}
”吗?
</
p
>
{
error
?
<
p
className=
"mt-3 text-danger"
role=
"alert"
>
{
error
}
</
p
>
:
null
}
</
XinyuanModal
>
</
XinyuanModal
>
</
div
>
</
div
>
);
);
...
...
src/pages/LawSubjectPage.jsx
View file @
36617df8
import
LoadErrorState
from
'@/components/LoadErrorState'
;
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
import
{
Alert
,
Button
,
Button
,
Card
,
Card
,
Chip
,
Chip
,
...
@@ -9,6 +10,7 @@ import {
...
@@ -9,6 +10,7 @@ import {
Separator
,
Separator
,
Spinner
,
Spinner
,
Tooltip
,
Tooltip
,
toast
,
}
from
'@heroui/react'
;
}
from
'@heroui/react'
;
import
{
useCallback
,
useContext
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
{
useCallback
,
useContext
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
{
history
,
useLocation
,
useParams
}
from
'umi'
;
import
{
history
,
useLocation
,
useParams
}
from
'umi'
;
...
@@ -407,7 +409,8 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
...
@@ -407,7 +409,8 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
const
[
localRefreshSignal
,
setLocalRefreshSignal
]
=
useState
(
0
);
const
[
localRefreshSignal
,
setLocalRefreshSignal
]
=
useState
(
0
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
debouncedKeyword
=
useDebouncedValue
(
keyword
,
300
);
const
debouncedKeyword
=
useDebouncedValue
(
keyword
,
300
);
const
fields
=
useListFields
(
subject
,
config
);
const
fields
=
useListFields
(
subject
,
config
);
const
selectedKeySet
=
useMemo
(()
=>
new
Set
(
selectedKeys
),
[
selectedKeys
]);
const
selectedKeySet
=
useMemo
(()
=>
new
Set
(
selectedKeys
),
[
selectedKeys
]);
...
@@ -418,6 +421,7 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
...
@@ -418,6 +421,7 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
setLoadFailed
(
false
);
setLoading
(
true
);
setLoading
(
true
);
setError
(
''
);
setError
(
''
);
queryRecords
({
queryRecords
({
...
@@ -435,6 +439,7 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
...
@@ -435,6 +439,7 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
if
(
cancelled
)
return
;
if
(
cancelled
)
return
;
setRecords
([]);
setRecords
([]);
setTotal
(
0
);
setTotal
(
0
);
setLoadFailed
(
true
);
setError
(
requestError
?.
message
||
'记录加载失败'
);
setError
(
requestError
?.
message
||
'记录加载失败'
);
}).
finally
(()
=>
{
}).
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
if
(
!
cancelled
)
setLoading
(
false
);
...
@@ -452,6 +457,7 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
...
@@ -452,6 +457,7 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
await
Promise
.
all
(
records
.
filter
((
record
)
=>
selectedKeySet
.
has
(
recordKey
(
record
))).
map
((
record
)
=>
removeRecord
(
recordKey
(
record
))));
await
Promise
.
all
(
records
.
filter
((
record
)
=>
selectedKeySet
.
has
(
recordKey
(
record
))).
map
((
record
)
=>
removeRecord
(
recordKey
(
record
))));
setSelectedKeys
([]);
setSelectedKeys
([]);
setDeleteOpen
(
false
);
setDeleteOpen
(
false
);
toast
.
success
(
'已删除所选记录'
,
{
timeout
:
4000
});
setLocalRefreshSignal
((
value
)
=>
value
+
1
);
setLocalRefreshSignal
((
value
)
=>
value
+
1
);
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'批量删除失败'
);
setError
(
requestError
?.
message
||
'批量删除失败'
);
...
@@ -466,6 +472,7 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
...
@@ -466,6 +472,7 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
setError
(
''
);
setError
(
''
);
try
{
try
{
await
exportSubjectRecords
(
subject
,
config
?.
list
?.
sort
);
await
exportSubjectRecords
(
subject
,
config
?.
list
?.
sort
);
toast
.
success
(
'导出成功'
,
{
timeout
:
4000
});
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'导出失败'
);
setError
(
requestError
?.
message
||
'导出失败'
);
}
finally
{
}
finally
{
...
@@ -482,8 +489,8 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
...
@@ -482,8 +489,8 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
<
ToolbarIconButton
label=
"新建记录"
icon=
{
PlusIcon
}
className=
"law-toolbar-button-primary"
variant=
"primary"
onPress=
{
()
=>
onCreate
({})
}
/>
<
ToolbarIconButton
label=
"新建记录"
icon=
{
PlusIcon
}
className=
"law-toolbar-button-primary"
variant=
"primary"
onPress=
{
()
=>
onCreate
({})
}
/>
</
ListHeader
>
</
ListHeader
>
<
Card
.
Content
className=
"space-y-4"
>
<
Card
.
Content
className=
"space-y-4"
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
{
!
loading
&&
loadFailed
?
<
LoadErrorState
title=
"记录加载失败"
onRetry=
{
onRefresh
}
/
>
:
null
}
{
!
error
?
<
DataTable
{
!
loadFailed
?
<
DataTable
ariaLabel=
{
`${subject.title || subject.name}记录`
}
ariaLabel=
{
`${subject.title || subject.name}记录`
}
columns=
{
[
columns=
{
[
...(
subject
.
type
!==
'embedded'
&&
!
fields
.
some
((
field
)
=>
field
.
name
===
'title'
)
?
[{
name
:
'title'
,
label
:
'标题'
}]
:
[]),
...(
subject
.
type
!==
'embedded'
&&
!
fields
.
some
((
field
)
=>
field
.
name
===
'title'
)
?
[{
name
:
'title'
,
label
:
'标题'
}]
:
[]),
...
@@ -506,7 +513,7 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
...
@@ -506,7 +513,7 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
onSortChange=
{
({
column
,
direction
})
=>
{
setSortState
({
field
:
{
name
:
String
(
column
)
},
direction
:
direction
===
'ascending'
?
'asc'
:
'desc'
});
setPage
(
0
);
}
}
onSortChange=
{
({
column
,
direction
})
=>
{
setSortState
({
field
:
{
name
:
String
(
column
)
},
direction
:
direction
===
'ascending'
?
'asc'
:
'desc'
});
setPage
(
0
);
}
}
renderSelectionActions=
{
()
=>
<
Button
size=
"sm"
variant=
"danger"
isDisabled=
{
loading
}
onPress=
{
removeSelected
}
>
删除已选
</
Button
>
}
renderSelectionActions=
{
()
=>
<
Button
size=
"sm"
variant=
"danger"
isDisabled=
{
loading
}
onPress=
{
removeSelected
}
>
删除已选
</
Button
>
}
/>
:
null
}
/>
:
null
}
{
!
error
?
(
{
!
loadFailed
?
(
<
DataTablePagination
<
DataTablePagination
isDisabled=
{
loading
}
isDisabled=
{
loading
}
page=
{
page
}
page=
{
page
}
...
@@ -520,7 +527,6 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
...
@@ -520,7 +527,6 @@ function LawTableView({ subject, config, refreshSignal, onCreate, onEdit, onRemo
</
Card
.
Content
>
</
Card
.
Content
>
<
XinyuanModal
isOpen=
{
deleteOpen
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
deleting
)
setDeleteOpen
(
open
);
}
}
title=
"删除已选记录"
footer=
{
<><
Button
variant=
"secondary"
isDisabled=
{
deleting
}
onPress=
{
()
=>
setDeleteOpen
(
false
)
}
>
取消
</
Button
><
Button
variant=
"danger"
isPending=
{
deleting
}
onPress=
{
confirmRemoveSelected
}
>
确认删除
</
Button
></>
}
>
<
XinyuanModal
isOpen=
{
deleteOpen
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
deleting
)
setDeleteOpen
(
open
);
}
}
title=
"删除已选记录"
footer=
{
<><
Button
variant=
"secondary"
isDisabled=
{
deleting
}
onPress=
{
()
=>
setDeleteOpen
(
false
)
}
>
取消
</
Button
><
Button
variant=
"danger"
isPending=
{
deleting
}
onPress=
{
confirmRemoveSelected
}
>
确认删除
</
Button
></>
}
>
<
p
>
确定删除已选的
{
selectedKeys
.
length
}
条记录吗?
</
p
>
<
p
>
确定删除已选的
{
selectedKeys
.
length
}
条记录吗?
</
p
>
{
error
?
<
p
className=
"mt-3 text-danger"
role=
"alert"
>
{
error
}
</
p
>
:
null
}
</
XinyuanModal
>
</
XinyuanModal
>
</
Card
>
</
Card
>
);
);
...
@@ -533,7 +539,8 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
...
@@ -533,7 +539,8 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
const
[
pageSize
,
setPageSize
]
=
useState
(
20
);
const
[
pageSize
,
setPageSize
]
=
useState
(
20
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
debouncedKeyword
=
useDebouncedValue
(
keyword
,
300
);
const
debouncedKeyword
=
useDebouncedValue
(
keyword
,
300
);
const
fields
=
useListFields
(
subject
,
config
);
const
fields
=
useListFields
(
subject
,
config
);
...
@@ -541,6 +548,7 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
...
@@ -541,6 +548,7 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
setLoadFailed
(
false
);
setLoading
(
true
);
setLoading
(
true
);
setError
(
''
);
setError
(
''
);
queryRecords
({
queryRecords
({
...
@@ -558,6 +566,7 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
...
@@ -558,6 +566,7 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
if
(
cancelled
)
return
;
if
(
cancelled
)
return
;
setRecords
([]);
setRecords
([]);
setTotal
(
0
);
setTotal
(
0
);
setLoadFailed
(
true
);
setError
(
requestError
?.
message
||
'记录加载失败'
);
setError
(
requestError
?.
message
||
'记录加载失败'
);
}).
finally
(()
=>
{
}).
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
if
(
!
cancelled
)
setLoading
(
false
);
...
@@ -570,6 +579,7 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
...
@@ -570,6 +579,7 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
setError
(
''
);
setError
(
''
);
try
{
try
{
await
exportSubjectRecords
(
subject
,
config
?.
list
?.
sort
);
await
exportSubjectRecords
(
subject
,
config
?.
list
?.
sort
);
toast
.
success
(
'导出成功'
,
{
timeout
:
4000
});
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'导出失败'
);
setError
(
requestError
?.
message
||
'导出失败'
);
}
finally
{
}
finally
{
...
@@ -585,8 +595,8 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
...
@@ -585,8 +595,8 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
<
ToolbarIconButton
label=
"新建记录"
icon=
{
PlusIcon
}
className=
"law-toolbar-button-primary"
variant=
"primary"
onPress=
{
()
=>
onCreate
({})
}
/>
<
ToolbarIconButton
label=
"新建记录"
icon=
{
PlusIcon
}
className=
"law-toolbar-button-primary"
variant=
"primary"
onPress=
{
()
=>
onCreate
({})
}
/>
</
ListHeader
>
</
ListHeader
>
<
Card
.
Content
className=
"space-y-4"
>
<
Card
.
Content
className=
"space-y-4"
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
{
!
loading
&&
loadFailed
?
<
LoadErrorState
title=
"记录加载失败"
onRetry=
{
onRefresh
}
/
>
:
null
}
{
loading
?
<
div
className=
"flex min-h-32 items-center justify-center"
><
Spinner
/></
div
>
:
error
?
null
:
!
records
.
length
?
<
LawEmptyState
description=
{
debouncedKeyword
?
'没有找到匹配的法规,请调整搜索条件。'
:
undefined
}
/>
:
(
{
loading
?
<
div
className=
"flex min-h-32 items-center justify-center"
><
Spinner
/></
div
>
:
loadFailed
?
null
:
!
records
.
length
?
<
LawEmptyState
description=
{
debouncedKeyword
?
'没有找到匹配的法规,请调整搜索条件。'
:
undefined
}
/>
:
(
<
div
className=
"grid gap-4 md:grid-cols-2 xl:grid-cols-3"
>
<
div
className=
"grid gap-4 md:grid-cols-2 xl:grid-cols-3"
>
{
records
.
map
((
record
)
=>
(
{
records
.
map
((
record
)
=>
(
<
Card
className=
"rc-card-surface law-record-card"
key=
{
recordKey
(
record
)
}
>
<
Card
className=
"rc-card-surface law-record-card"
key=
{
recordKey
(
record
)
}
>
...
@@ -612,7 +622,7 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
...
@@ -612,7 +622,7 @@ function LawCardView({ subject, config, refreshSignal, onCreate, onEdit, onRemov
))
}
))
}
</
div
>
</
div
>
)
}
)
}
{
!
error
?
(
{
!
loadFailed
?
(
<
DataTablePagination
<
DataTablePagination
isDisabled=
{
loading
}
isDisabled=
{
loading
}
page=
{
page
}
page=
{
page
}
...
@@ -635,7 +645,7 @@ function LawColumnView({ subject, config, refreshSignal, onCreate, onEdit }) {
...
@@ -635,7 +645,7 @@ function LawColumnView({ subject, config, refreshSignal, onCreate, onEdit }) {
const
[
graphNodes
,
setGraphNodes
]
=
useState
([]);
const
[
graphNodes
,
setGraphNodes
]
=
useState
([]);
const
[
columnTotals
,
setColumnTotals
]
=
useState
({});
const
[
columnTotals
,
setColumnTotals
]
=
useState
({});
const
[
metaLoading
,
setMetaLoading
]
=
useState
(
false
);
const
[
metaLoading
,
setMetaLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
'
'
);
const
[
,
setError
]
=
useFeedbackState
(
'error
'
);
const
levelOptions
=
fieldOptions
(
subject
,
'level'
);
const
levelOptions
=
fieldOptions
(
subject
,
'level'
);
const
statusOptions
=
fieldOptions
(
subject
,
'effective_status'
);
const
statusOptions
=
fieldOptions
(
subject
,
'effective_status'
);
const
tagTree
=
useMemo
(()
=>
buildGraphTagTree
(
graphNodes
),
[
graphNodes
]);
const
tagTree
=
useMemo
(()
=>
buildGraphTagTree
(
graphNodes
),
[
graphNodes
]);
...
@@ -769,7 +779,6 @@ function LawColumnView({ subject, config, refreshSignal, onCreate, onEdit }) {
...
@@ -769,7 +779,6 @@ function LawColumnView({ subject, config, refreshSignal, onCreate, onEdit }) {
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
<
div
className=
"law-column-board law-column-board-fixed"
style=
{
{
gridTemplateColumns
:
`repeat(${Math.max(levelOptions.length, 1)}, minmax(16rem, 1fr))`
}
}
>
<
div
className=
"law-column-board law-column-board-fixed"
style=
{
{
gridTemplateColumns
:
`repeat(${Math.max(levelOptions.length, 1)}, minmax(16rem, 1fr))`
}
}
>
{
levelOptions
.
length
?
levelOptions
.
map
((
level
)
=>
(
{
levelOptions
.
length
?
levelOptions
.
map
((
level
)
=>
(
<
LawColumnLevel
<
LawColumnLevel
...
@@ -799,7 +808,7 @@ function LawColumnLevel({ subject, config, level, filterConfig, showDetail, refr
...
@@ -799,7 +808,7 @@ function LawColumnLevel({ subject, config, level, filterConfig, showDetail, refr
const
[
expanded
,
setExpanded
]
=
useState
([]);
const
[
expanded
,
setExpanded
]
=
useState
([]);
const
[
draggingKey
,
setDraggingKey
]
=
useState
(
''
);
const
[
draggingKey
,
setDraggingKey
]
=
useState
(
''
);
const
[
dragOverKey
,
setDragOverKey
]
=
useState
(
''
);
const
[
dragOverKey
,
setDragOverKey
]
=
useState
(
''
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
filterKey
=
JSON
.
stringify
(
filterConfig
);
const
filterKey
=
JSON
.
stringify
(
filterConfig
);
const
canLoadMore
=
records
.
length
<
total
;
const
canLoadMore
=
records
.
length
<
total
;
const
allRecordKeys
=
useMemo
(()
=>
records
.
map
((
record
)
=>
record
?.
_id
||
record
?.
name
).
filter
(
Boolean
),
[
records
]);
const
allRecordKeys
=
useMemo
(()
=>
records
.
map
((
record
)
=>
record
?.
_id
||
record
?.
name
).
filter
(
Boolean
),
[
records
]);
...
@@ -851,7 +860,7 @@ function LawColumnLevel({ subject, config, level, filterConfig, showDetail, refr
...
@@ -851,7 +860,7 @@ function LawColumnLevel({ subject, config, level, filterConfig, showDetail, refr
const
handleScroll
=
(
event
)
=>
{
const
handleScroll
=
(
event
)
=>
{
const
target
=
event
.
currentTarget
;
const
target
=
event
.
currentTarget
;
if
(
!
canLoadMore
||
loading
||
loadingMore
)
return
;
if
(
!
canLoadMore
||
loading
||
loadingMore
||
error
)
return
;
if
(
target
.
clientHeight
+
target
.
scrollTop
+
160
>=
target
.
scrollHeight
)
{
if
(
target
.
clientHeight
+
target
.
scrollTop
+
160
>=
target
.
scrollHeight
)
{
loadPage
(
page
+
1
);
loadPage
(
page
+
1
);
}
}
...
@@ -870,8 +879,9 @@ function LawColumnLevel({ subject, config, level, filterConfig, showDetail, refr
...
@@ -870,8 +879,9 @@ function LawColumnLevel({ subject, config, level, filterConfig, showDetail, refr
setRecords
(
nextRecords
.
map
((
record
)
=>
(
orderMap
[
recordKey
(
record
)]
!==
undefined
?
{
...
record
,
order
:
orderMap
[
recordKey
(
record
)]
}
:
record
)));
setRecords
(
nextRecords
.
map
((
record
)
=>
(
orderMap
[
recordKey
(
record
)]
!==
undefined
?
{
...
record
,
order
:
orderMap
[
recordKey
(
record
)]
}
:
record
)));
try
{
try
{
await
reorderRecords
(
orderMap
);
await
reorderRecords
(
orderMap
);
toast
.
success
(
'排序已保存'
,
{
timeout
:
4000
});
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'排序保存失败'
);
toast
.
danger
(
requestError
?.
message
||
'排序保存失败'
,
{
timeout
:
8000
}
);
loadPage
(
0
,
true
);
loadPage
(
0
,
true
);
}
}
};
};
...
@@ -889,7 +899,7 @@ function LawColumnLevel({ subject, config, level, filterConfig, showDetail, refr
...
@@ -889,7 +899,7 @@ function LawColumnLevel({ subject, config, level, filterConfig, showDetail, refr
</
header
>
</
header
>
<
div
className=
"law-column-list"
onScroll=
{
handleScroll
}
>
<
div
className=
"law-column-list"
onScroll=
{
handleScroll
}
>
{
loading
?
<
div
className=
"flex min-h-32 items-center justify-center"
><
Spinner
size=
"sm"
/></
div
>
:
null
}
{
loading
?
<
div
className=
"flex min-h-32 items-center justify-center"
><
Spinner
size=
"sm"
/></
div
>
:
null
}
{
!
loading
&&
error
?
<
div
className=
"px-3 py-6 text-sm text-danger"
>
{
error
}
</
div
>
:
null
}
{
!
loading
&&
error
?
<
LoadErrorState
title=
"栏目加载失败"
onRetry=
{
()
=>
loadPage
(
0
,
true
)
}
/
>
:
null
}
{
!
loading
&&
!
error
&&
records
.
map
((
record
)
=>
{
{
!
loading
&&
!
error
&&
records
.
map
((
record
)
=>
{
const
key
=
record
?.
_id
||
record
?.
name
;
const
key
=
record
?.
_id
||
record
?.
name
;
const
isActive
=
record
?.
metadata
?.
effective_status
===
'现行有效'
||
!
record
?.
metadata
?.
effective_status
;
const
isActive
=
record
?.
metadata
?.
effective_status
===
'现行有效'
||
!
record
?.
metadata
?.
effective_status
;
...
@@ -1034,7 +1044,8 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
...
@@ -1034,7 +1044,8 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
const
[
categoryList
,
setCategoryList
]
=
useState
([]);
const
[
categoryList
,
setCategoryList
]
=
useState
([]);
const
[
categoryFields
,
setCategoryFields
]
=
useState
([]);
const
[
categoryFields
,
setCategoryFields
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
loadFailed
,
setLoadFailed
]
=
useState
(
false
);
const
layout
=
config
?.
list
?.
layout
||
'list'
;
const
layout
=
config
?.
list
?.
layout
||
'list'
;
const
words
=
config
?.
list
?.
words
||
[];
const
words
=
config
?.
list
?.
words
||
[];
const
categoryEnabled
=
config
?.
list
?.
search
?.
category
||
false
;
const
categoryEnabled
=
config
?.
list
?.
search
?.
category
||
false
;
...
@@ -1073,6 +1084,7 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
...
@@ -1073,6 +1084,7 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
setLoadFailed
(
false
);
setLoading
(
true
);
setLoading
(
true
);
setError
(
''
);
setError
(
''
);
const
terms
=
Object
.
entries
(
filters
).
reduce
((
result
,
[
name
,
filter
])
=>
{
const
terms
=
Object
.
entries
(
filters
).
reduce
((
result
,
[
name
,
filter
])
=>
{
...
@@ -1118,6 +1130,7 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
...
@@ -1118,6 +1130,7 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
setRecords
([]);
setRecords
([]);
setTotal
(
0
);
setTotal
(
0
);
setFilterBuckets
([]);
setFilterBuckets
([]);
setLoadFailed
(
true
);
setError
(
requestError
?.
message
||
'搜索失败'
);
setError
(
requestError
?.
message
||
'搜索失败'
);
}
}
}).
finally
(()
=>
{
}).
finally
(()
=>
{
...
@@ -1151,6 +1164,7 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
...
@@ -1151,6 +1164,7 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
setError
(
''
);
setError
(
''
);
try
{
try
{
await
exportSubjectRecords
(
subject
,
config
?.
list
?.
sort
);
await
exportSubjectRecords
(
subject
,
config
?.
list
?.
sort
);
toast
.
success
(
'导出成功'
,
{
timeout
:
4000
});
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'下载失败'
);
setError
(
requestError
?.
message
||
'下载失败'
);
}
finally
{
}
finally
{
...
@@ -1198,7 +1212,7 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
...
@@ -1198,7 +1212,7 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
</
aside
>
</
aside
>
)
:
null
}
)
:
null
}
<
div
className=
"law-search-content"
>
<
div
className=
"law-search-content"
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
{
!
loading
&&
loadFailed
?
<
LoadErrorState
title=
"记录加载失败"
onRetry=
{
onRefresh
}
/
>
:
null
}
<
section
className=
"law-search-results"
>
<
section
className=
"law-search-results"
>
<
div
className=
"law-search-result-header"
>
<
div
className=
"law-search-result-header"
>
<
strong
>
搜索结果 (
{
total
}
)
</
strong
>
<
strong
>
搜索结果 (
{
total
}
)
</
strong
>
...
@@ -1227,8 +1241,8 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
...
@@ -1227,8 +1241,8 @@ function LawSearchView({ subject, config, application, refreshSignal, onCreate,
onToggle=
{
(
key
)
=>
setExpandedRecords
((
current
)
=>
(
current
.
includes
(
key
)
?
current
.
filter
((
item
)
=>
item
!==
key
)
:
[...
current
,
key
]))
}
onToggle=
{
(
key
)
=>
setExpandedRecords
((
current
)
=>
(
current
.
includes
(
key
)
?
current
.
filter
((
item
)
=>
item
!==
key
)
:
[...
current
,
key
]))
}
/>
/>
)
:
null
}
)
:
null
}
{
!
loading
&&
!
error
&&
!
records
.
length
?
<
LawEmptyState
description=
{
submittedKeyword
?
'没有找到匹配的法规,请调整关键词或筛选条件。'
:
undefined
}
className=
"law-search-empty-state"
/>
:
null
}
{
!
loading
&&
!
loadFailed
&&
!
records
.
length
?
<
LawEmptyState
description=
{
submittedKeyword
?
'没有找到匹配的法规,请调整关键词或筛选条件。'
:
undefined
}
className=
"law-search-empty-state"
/>
:
null
}
{
!
error
?
(
{
!
loadFailed
?
(
<
div
className=
"law-search-pagination"
>
<
div
className=
"law-search-pagination"
>
<
DataTablePagination
<
DataTablePagination
isDisabled=
{
loading
}
isDisabled=
{
loading
}
...
@@ -1394,6 +1408,7 @@ function ImportActions({ config, subject, onImported, setError }) {
...
@@ -1394,6 +1408,7 @@ function ImportActions({ config, subject, onImported, setError }) {
throw
new
Error
(
'导入文件没有可识别的数据行'
);
throw
new
Error
(
'导入文件没有可识别的数据行'
);
}
}
await
importRecords
(
subject
.
name
,
records
);
await
importRecords
(
subject
.
name
,
records
);
toast
.
success
(
'目录导入成功'
,
{
timeout
:
4000
});
onImported
?.();
onImported
?.();
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'导入目录失败'
);
setError
(
requestError
?.
message
||
'导入目录失败'
);
...
@@ -1414,6 +1429,7 @@ function ImportActions({ config, subject, onImported, setError }) {
...
@@ -1414,6 +1429,7 @@ function ImportActions({ config, subject, onImported, setError }) {
records
.
push
(
await
buildDocumentImportRecord
(
file
,
subject
,
config
));
records
.
push
(
await
buildDocumentImportRecord
(
file
,
subject
,
config
));
}
}
await
Promise
.
all
(
records
.
map
((
record
)
=>
createRecord
(
record
)));
await
Promise
.
all
(
records
.
map
((
record
)
=>
createRecord
(
record
)));
toast
.
success
(
'文档导入成功'
,
{
timeout
:
4000
});
onImported
?.();
onImported
?.();
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'导入文档失败'
);
setError
(
requestError
?.
message
||
'导入文档失败'
);
...
@@ -1878,7 +1894,8 @@ export default function LawSubjectPage() {
...
@@ -1878,7 +1894,8 @@ export default function LawSubjectPage() {
const
listView
=
config
?.
list
?.
view
||
'table'
;
const
listView
=
config
?.
list
?.
view
||
'table'
;
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
subject
,
setSubject
]
=
useState
(
null
);
const
[
loadingSubject
,
setLoadingSubject
]
=
useState
(
true
);
const
[
loadingSubject
,
setLoadingSubject
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[,
setError
]
=
useFeedbackState
(
'error'
);
const
[
subjectReloadKey
,
setSubjectReloadKey
]
=
useState
(
0
);
const
[
formOpen
,
setFormOpen
]
=
useState
(
false
);
const
[
formOpen
,
setFormOpen
]
=
useState
(
false
);
const
[
formRecord
,
setFormRecord
]
=
useState
(
null
);
const
[
formRecord
,
setFormRecord
]
=
useState
(
null
);
const
[
draft
,
setDraft
]
=
useState
({});
const
[
draft
,
setDraft
]
=
useState
({});
...
@@ -1905,7 +1922,7 @@ export default function LawSubjectPage() {
...
@@ -1905,7 +1922,7 @@ export default function LawSubjectPage() {
if
(
!
cancelled
)
setLoadingSubject
(
false
);
if
(
!
cancelled
)
setLoadingSubject
(
false
);
});
});
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[
canLoadSubject
,
subjectName
]);
},
[
canLoadSubject
,
subjectName
,
subjectReloadKey
]);
const
openCreate
=
(
prefill
=
{})
=>
{
const
openCreate
=
(
prefill
=
{})
=>
{
if
(
config
?.
list
?.
create
?.
path
)
{
if
(
config
?.
list
?.
create
?.
path
)
{
...
@@ -1937,6 +1954,7 @@ export default function LawSubjectPage() {
...
@@ -1937,6 +1954,7 @@ export default function LawSubjectPage() {
}
else
{
}
else
{
await
createRecord
(
payload
);
await
createRecord
(
payload
);
}
}
toast
.
success
(
'保存成功'
,
{
timeout
:
4000
});
setFormOpen
(
false
);
setFormOpen
(
false
);
setFormRecord
(
null
);
setFormRecord
(
null
);
setDraft
({});
setDraft
({});
...
@@ -1956,6 +1974,7 @@ export default function LawSubjectPage() {
...
@@ -1956,6 +1974,7 @@ export default function LawSubjectPage() {
try
{
try
{
await
removeRecord
(
name
);
await
removeRecord
(
name
);
setRemovingRecord
(
null
);
setRemovingRecord
(
null
);
toast
.
success
(
'记录已删除'
,
{
timeout
:
4000
});
setRefreshSignal
((
value
)
=>
value
+
1
);
setRefreshSignal
((
value
)
=>
value
+
1
);
}
catch
(
requestError
)
{
}
catch
(
requestError
)
{
setError
(
requestError
?.
message
||
'删除失败'
);
setError
(
requestError
?.
message
||
'删除失败'
);
...
@@ -1963,7 +1982,7 @@ export default function LawSubjectPage() {
...
@@ -1963,7 +1982,7 @@ export default function LawSubjectPage() {
};
};
if
(
!
canLoadSubject
)
{
if
(
!
canLoadSubject
)
{
return
<
Alert
status=
"warning"
><
Alert
.
Content
><
Alert
.
Description
>
当前路径未指定业务对象
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>;
return
<
LoadErrorState
title=
"当前路径未指定业务对象"
/
>;
}
}
if
(
loadingSubject
)
{
if
(
loadingSubject
)
{
...
@@ -1971,7 +1990,7 @@ export default function LawSubjectPage() {
...
@@ -1971,7 +1990,7 @@ export default function LawSubjectPage() {
}
}
if
(
!
subject
)
{
if
(
!
subject
)
{
return
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
||
'对象加载失败'
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>;
return
<
LoadErrorState
title=
"对象加载失败"
onRetry=
{
()
=>
setSubjectReloadKey
((
value
)
=>
value
+
1
)
}
/
>;
}
}
if
(
listView
===
'audit_indicator_list'
||
listView
===
'process_list'
)
{
if
(
listView
===
'audit_indicator_list'
||
listView
===
'process_list'
)
{
...
@@ -1991,13 +2010,12 @@ export default function LawSubjectPage() {
...
@@ -1991,13 +2010,12 @@ export default function LawSubjectPage() {
return
(
return
(
<
div
className=
"rc-page-stack"
>
<
div
className=
"rc-page-stack"
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
{
listView
===
'column'
?
<
LawColumnView
{
...
viewProps
}
/>
:
null
}
{
listView
===
'column'
?
<
LawColumnView
{
...
viewProps
}
/>
:
null
}
{
listView
===
'card'
?
<
LawCardView
{
...
viewProps
}
/>
:
null
}
{
listView
===
'card'
?
<
LawCardView
{
...
viewProps
}
/>
:
null
}
{
listView
===
'search'
?
<
LawSearchView
{
...
viewProps
}
advanced
/>
:
null
}
{
listView
===
'search'
?
<
LawSearchView
{
...
viewProps
}
advanced
/>
:
null
}
{
listView
===
'search_view'
?
<
LawSearchView
{
...
viewProps
}
/>
:
null
}
{
listView
===
'search_view'
?
<
LawSearchView
{
...
viewProps
}
/>
:
null
}
{
!
[
'column'
,
'card'
,
'search'
,
'search_view'
].
includes
(
listView
)
?
<
LawTableView
{
...
viewProps
}
/>
:
null
}
{
!
[
'column'
,
'card'
,
'search'
,
'search_view'
].
includes
(
listView
)
?
<
LawTableView
{
...
viewProps
}
/>
:
null
}
<
XinyuanModal
isOpen=
{
Boolean
(
removingRecord
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
&&
!
deleting
)
setRemovingRecord
(
null
);
}
}
title=
"删除记录"
footer=
{
<><
Button
variant=
"secondary"
isDisabled=
{
deleting
}
onPress=
{
()
=>
setRemovingRecord
(
null
)
}
>
取消
</
Button
><
Button
variant=
"danger"
isPending=
{
deleting
}
onPress=
{
confirmRemove
}
>
确认删除
</
Button
></>
}
><
p
>
确定删除“
{
removingRecord
?.
title
||
removingRecord
?.
name
}
”吗?
</
p
>
{
error
?
<
p
className=
"mt-3 text-danger"
role=
"alert"
>
{
error
}
</
p
>
:
null
}
</
XinyuanModal
>
<
XinyuanModal
isOpen=
{
Boolean
(
removingRecord
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
&&
!
deleting
)
setRemovingRecord
(
null
);
}
}
title=
"删除记录"
footer=
{
<><
Button
variant=
"secondary"
isDisabled=
{
deleting
}
onPress=
{
()
=>
setRemovingRecord
(
null
)
}
>
取消
</
Button
><
Button
variant=
"danger"
isPending=
{
deleting
}
onPress=
{
confirmRemove
}
>
确认删除
</
Button
></>
}
><
p
>
确定删除“
{
removingRecord
?.
title
||
removingRecord
?.
name
}
”吗?
</
p
></
XinyuanModal
>
<
SubjectRecordModal
<
SubjectRecordModal
config=
{
config
}
config=
{
config
}
draft=
{
draft
}
draft=
{
draft
}
...
...
src/pages/LawWorkbenchLayout.jsx
View file @
36617df8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
AppShell
from
'@/components/AppShell'
;
import
AppShell
from
'@/components/AppShell'
;
import
{
Alert
}
from
'@heroui/react
'
;
import
LoadErrorState
from
'@/components/LoadErrorState
'
;
import
{
FileText
,
Search
}
from
'@/components/AppIcons'
;
import
{
FileText
,
Search
}
from
'@/components/AppIcons'
;
import
{
APPLICATION_NAME
,
APP_NAME
}
from
'@/config/ApplicationConfig'
;
import
{
APPLICATION_NAME
,
APP_NAME
}
from
'@/config/ApplicationConfig'
;
import
{
loadApplication
}
from
'@/services/DataService'
;
import
{
loadApplication
}
from
'@/services/DataService'
;
...
@@ -47,11 +48,13 @@ export default function LawWorkbenchLayout() {
...
@@ -47,11 +48,13 @@ export default function LawWorkbenchLayout() {
const
userInfo
=
useContext
(
UserContext
);
const
userInfo
=
useContext
(
UserContext
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
true
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
true
);
const
[
application
,
setApplication
]
=
useState
(
null
);
const
[
application
,
setApplication
]
=
useState
(
null
);
const
[
applicationError
,
setApplicationError
]
=
useState
(
''
);
const
[
applicationError
,
setApplicationError
]
=
useFeedbackState
(
'error'
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
const
isLoadingApplication
=
!
application
&&
!
applicationError
;
const
isLoadingApplication
=
!
application
&&
!
applicationError
;
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
setApplicationError
(
''
);
loadApplication
(
APPLICATION_NAME
)
loadApplication
(
APPLICATION_NAME
)
.
then
((
result
)
=>
{
.
then
((
result
)
=>
{
if
(
cancelled
)
return
;
if
(
cancelled
)
return
;
...
@@ -64,7 +67,7 @@ export default function LawWorkbenchLayout() {
...
@@ -64,7 +67,7 @@ export default function LawWorkbenchLayout() {
if
(
!
cancelled
)
setApplicationError
(
error
?.
message
||
'应用配置加载失败'
);
if
(
!
cancelled
)
setApplicationError
(
error
?.
message
||
'应用配置加载失败'
);
});
});
return
()
=>
{
cancelled
=
true
;
};
return
()
=>
{
cancelled
=
true
;
};
},
[]);
},
[
reloadKey
]);
const
menuGroups
=
useMemo
(()
=>
[{
const
menuGroups
=
useMemo
(()
=>
[{
title
:
application
?.
title
||
APP_NAME
,
title
:
application
?.
title
||
APP_NAME
,
...
@@ -98,11 +101,7 @@ export default function LawWorkbenchLayout() {
...
@@ -98,11 +101,7 @@ export default function LawWorkbenchLayout() {
>
>
{
applicationError
?
(
{
applicationError
?
(
<
div
className=
"rc-page-stack"
>
<
div
className=
"rc-page-stack"
>
<
Alert
status=
"danger"
>
<
LoadErrorState
title=
"应用配置加载失败"
onRetry=
{
()
=>
setReloadKey
((
value
)
=>
value
+
1
)
}
/>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
applicationError
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
</
div
>
</
div
>
)
:
null
}
)
:
null
}
{
application
?
<
Outlet
/>
:
null
}
{
application
?
<
Outlet
/>
:
null
}
...
...
src/pages/Login.jsx
View file @
36617df8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
xyLogoRed
from
'@/assets/identity/xy-logo-red.png'
;
import
xyLogoRed
from
'@/assets/identity/xy-logo-red.png'
;
import
xyLogoWhite
from
'@/assets/identity/xy-logo-white.png'
;
import
xyLogoWhite
from
'@/assets/identity/xy-logo-white.png'
;
import
{
APP_BASE_PATH
,
APP_NAME
}
from
'@/config/ApplicationConfig'
;
import
{
APP_BASE_PATH
,
APP_NAME
}
from
'@/config/ApplicationConfig'
;
...
@@ -5,7 +6,6 @@ import authService from '@/services/authService';
...
@@ -5,7 +6,6 @@ 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
{
import
{
Alert
,
Button
,
Button
,
Card
,
Card
,
Input
,
Input
,
...
@@ -55,7 +55,7 @@ export default function LoginPage() {
...
@@ -55,7 +55,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
=
normalizeRedirect
(
searchParams
.
get
(
'redirect'
));
const
redirect
=
normalizeRedirect
(
searchParams
.
get
(
'redirect'
));
...
@@ -209,13 +209,6 @@ export default function LoginPage() {
...
@@ -209,13 +209,6 @@ export default function LoginPage() {
</
div
>
</
div
>
</
div
>
</
div
>
{
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
<
Button
<
Button
fullWidth
fullWidth
...
...
src/pages/RecordGraphPage.jsx
View file @
36617df8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
history
,
useLocation
,
useParams
}
from
'@umijs/max'
;
import
{
history
,
useLocation
,
useParams
}
from
'@umijs/max'
;
import
{
Button
,
Spinner
}
from
'@heroui/react'
;
import
{
Button
,
Spinner
}
from
'@heroui/react'
;
...
@@ -5,6 +6,7 @@ import { ArrowPathIcon } from '@/components/AppIcons';
...
@@ -5,6 +6,7 @@ import { ArrowPathIcon } from '@/components/AppIcons';
import
HeroCheckbox
from
'@/components/HeroCheckbox'
;
import
HeroCheckbox
from
'@/components/HeroCheckbox'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
RecordFlowGraph
from
'@/components/RecordFlowGraph/RecordFlowGraph'
;
import
RecordFlowGraph
from
'@/components/RecordFlowGraph/RecordFlowGraph'
;
import
LoadErrorState
from
'@/components/LoadErrorState'
;
import
{
loadRecordGraph
}
from
'@/services/RecordGraphService'
;
import
{
loadRecordGraph
}
from
'@/services/RecordGraphService'
;
import
{
ApplicationContext
}
from
'./LawWorkbenchLayout'
;
import
{
ApplicationContext
}
from
'./LawWorkbenchLayout'
;
...
@@ -18,9 +20,9 @@ export default function RecordGraphPage() {
...
@@ -18,9 +20,9 @@ export default function RecordGraphPage() {
const
groupsKey
=
JSON
.
stringify
(
subjectConfig
?.
tag
?.
group
?.
record
||
[]);
const
groupsKey
=
JSON
.
stringify
(
subjectConfig
?.
tag
?.
group
?.
record
||
[]);
const
[
data
,
setData
]
=
useState
(
null
);
const
[
data
,
setData
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
use
State
(
'
'
);
const
[
error
,
setError
]
=
use
FeedbackState
(
'error
'
);
const
[
operationError
,
setOperationError
]
=
useState
(
'
'
);
const
[
,
setOperationError
]
=
useFeedbackState
(
'error
'
);
const
[
success
,
setSuccess
]
=
useState
(
'
'
);
const
[
,
setSuccess
]
=
useFeedbackState
(
'success
'
);
const
[
centered
,
setCentered
]
=
useState
(
false
);
const
[
centered
,
setCentered
]
=
useState
(
false
);
const
[
refresh
,
setRefresh
]
=
useState
(
0
);
const
[
refresh
,
setRefresh
]
=
useState
(
0
);
const
filterConfig
=
{
_id
:
query
.
get
(
'node'
),
department
:
query
.
get
(
'dep'
),
tag
:
query
.
get
(
'tag'
)
};
const
filterConfig
=
{
_id
:
query
.
get
(
'node'
),
department
:
query
.
get
(
'dep'
),
tag
:
query
.
get
(
'tag'
)
};
...
@@ -59,8 +61,7 @@ export default function RecordGraphPage() {
...
@@ -59,8 +61,7 @@ export default function RecordGraphPage() {
<
HeroCheckbox
isSelected=
{
centered
}
onChange=
{
setCentered
}
>
居中
</
HeroCheckbox
>
<
HeroCheckbox
isSelected=
{
centered
}
onChange=
{
setCentered
}
>
居中
</
HeroCheckbox
>
<
Button
isIconOnly
variant=
"secondary"
aria
-
label=
"刷新关系图"
isDisabled=
{
loading
}
onPress=
{
()
=>
{
setRefresh
(
value
=>
value
+
1
);
setOperationError
(
''
);
setSuccess
(
''
);
}
}
><
ArrowPathIcon
size=
{
16
}
/></
Button
>
<
Button
isIconOnly
variant=
"secondary"
aria
-
label=
"刷新关系图"
isDisabled=
{
loading
}
onPress=
{
()
=>
{
setRefresh
(
value
=>
value
+
1
);
setOperationError
(
''
);
setSuccess
(
''
);
}
}
><
ArrowPathIcon
size=
{
16
}
/></
Button
>
</
div
>
</
div
>
{
(
error
||
operationError
)
&&
<
div
role=
"alert"
className=
"flex items-center justify-center gap-3 p-3 text-danger"
>
{
error
||
operationError
}
<
Button
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
setRefresh
(
value
=>
value
+
1
)
}
>
重试
</
Button
></
div
>
}
{
!
loading
&&
error
?
<
LoadErrorState
title=
"关系图加载失败"
onRetry=
{
()
=>
setRefresh
(
value
=>
value
+
1
)
}
/>
:
null
}
{
success
&&
!
operationError
&&
<
p
role=
"status"
className=
"px-4 py-2 text-sm text-success"
>
{
success
}
</
p
>
}
<
div
className=
"relative min-h-0 flex-1"
>
<
div
className=
"relative min-h-0 flex-1"
>
{
loading
?
<
div
className=
"absolute inset-0 flex items-center justify-center gap-2"
role=
"status"
><
Spinner
/>
正在加载关系图…
</
div
>
:
!
error
&&
data
?
<>
{
loading
?
<
div
className=
"absolute inset-0 flex items-center justify-center gap-2"
role=
"status"
><
Spinner
/>
正在加载关系图…
</
div
>
:
!
error
&&
data
?
<>
<
RecordFlowGraph
key=
{
`${subjectName}/${filterConfig._id}/${filterConfig.department}/${filterConfig.tag}/${centered}/${refresh}`
}
<
RecordFlowGraph
key=
{
`${subjectName}/${filterConfig._id}/${filterConfig.department}/${filterConfig.tag}/${centered}/${refresh}`
}
...
...
src/pages/law-reference.business-flow.md
View file @
36617df8
...
@@ -26,7 +26,16 @@
...
@@ -26,7 +26,16 @@
## 权限与异常
## 权限与异常
请求统一复用
`record-api.md`
中的鉴权和错误处理。接口失败时保留当前页面草稿并在页面内显示错误;无记录时显示明确的空状态;路由无法解析 Subject 时不会发送
`undefined`
请求。
请求统一复用
`record-api.md`
中的鉴权和错误处理。接口失败时保留当前页面草稿,通过全局操作提示显示具体错误;无记录时显示明确的空状态;路由无法解析 Subject 时不会发送
`undefined`
请求。
### 接口与操作反馈(2026-10-08)
-
登录、应用配置与切换、法规列表与搜索、编辑/删除弹窗、附件上传/移除、目录/文档导入、导出、指标及执行结果、关系图和通知入口统一使用根层 HeroUI Toast,不再显示常驻成功/失败横条。提示位于屏幕顶部中央,层级高于弹窗和移动侧栏,页面滚动不会遮挡。
-
成功提示 4 秒、失败/警告提示 8 秒后自动消失;最前方提示提供始终可见的“关闭提示”按钮,使用 HeroUI
`ghost`
图标按钮并参与提示内容布局,保留主题悬停和键盘焦点状态;后方堆叠提示隐藏关闭按钮,避免误点。最多同时显示 3 条,长错误信息可滚动阅读。相同请求重试后仍失败时再次显示提示。
-
保存、上传、移除和删除失败保留弹窗、草稿及选择,关闭提示不会关闭弹窗或触发成功流程。成功后按原流程刷新数据或返回列表,并显示相应操作反馈。
-
详情页通过顶部“编辑”进入编辑弹窗,点击“保存修改”后先校验当前 Subject 必填字段,再调用
`PUT /record/:name`
写入原有标题、正文和
`metadata`
。保存期间禁用重复提交;接口成功后更新详情数据、关闭编辑弹窗,并由详情页显示“详情已保存。”成功提示,提示不会随弹窗关闭而消失。校验或接口失败时不显示成功提示,保留编辑弹窗和草稿。重新编辑、取消编辑、再次保存或切换记录时清除上一轮保存成功提示。
-
对象、列表、搜索、详情、指标、关系图和通知加载失败保留独立失败状态,显示 EmptyState 和“重新加载”入口;提示关闭后不会显示“暂无数据”或无限加载。导入/导出、保存等操作失败不隐藏已经加载的数据。重新加载时清除旧失败状态,成功后恢复原数据视图。
-
本次仅调整反馈和重试展示,沿用
`GET /subject/:name`
、Record 查询/创建/更新/删除、搜索与通知的现有 service;业务对象和 metadata 字段协议不变。
## 六种配置视图与 streams-admin 对齐(2026-09-09)
## 六种配置视图与 streams-admin 对齐(2026-09-09)
...
...
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
View file @
36617df8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
xinyuanLogo
from
'@/assets/identity/xy-logo-red.png'
;
import
xinyuanLogo
from
'@/assets/identity/xy-logo-red.png'
;
import
{
import
{
APP_NAME
,
APP_NAME
,
...
@@ -5,12 +6,11 @@ import {
...
@@ -5,12 +6,11 @@ import {
}
from
'@/config/ApplicationConfig'
;
}
from
'@/config/ApplicationConfig'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
EmptyStateSurface
}
from
'@/components/DataTable'
;
import
{
EmptyStateSurface
}
from
'@/components/DataTable'
;
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
;
}
}
...
@@ -443,12 +444,6 @@ export default function ApplicationSwitcherDialog({
...
@@ -443,12 +444,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
/>
...
...
src/pages/project_manager_xy/notifications/NotificationListPage.jsx
View file @
36617df8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
Bell
,
Close
,
MagnifyingGlassIcon
,
OpenExternal
}
from
'@/components/AppIcons'
;
import
{
Bell
,
Close
,
MagnifyingGlassIcon
,
OpenExternal
}
from
'@/components/AppIcons'
;
import
{
DataTablePagination
}
from
'@/components/DataTable'
;
import
{
DataTablePagination
}
from
'@/components/DataTable'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
Avatar
,
Button
,
Card
,
Chip
,
SearchField
,
Spinner
,
Tabs
,
toast
}
from
'@heroui/react'
;
import
{
Avatar
,
Button
,
Card
,
Chip
,
SearchField
,
Spinner
,
Tabs
,
toast
}
from
'@heroui/react'
;
import
LoadErrorState
from
'@/components/LoadErrorState'
;
import
{
useCallback
,
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
useCallback
,
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
import
{
history
}
from
'umi'
;
import
{
import
{
...
@@ -75,7 +77,7 @@ export default function NotificationListPage() {
...
@@ -75,7 +77,7 @@ export default function NotificationListPage() {
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
unread
,
setUnread
]
=
useState
(
0
);
const
[
unread
,
setUnread
]
=
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
);
...
@@ -103,8 +105,8 @@ export default function NotificationListPage() {
...
@@ -103,8 +105,8 @@ export default function NotificationListPage() {
const
refresh
=
useCallback
(()
=>
setReloadKey
((
value
)
=>
value
+
1
),
[]);
const
refresh
=
useCallback
(()
=>
setReloadKey
((
value
)
=>
value
+
1
),
[]);
const
run
=
useCallback
(
async
(
key
,
operation
,
message
)
=>
{
const
run
=
useCallback
(
async
(
key
,
operation
,
message
)
=>
{
setBusyKey
(
key
);
setBusyKey
(
key
);
try
{
await
operation
();
toast
.
success
(
message
,
{
timeout
:
3
000
});
refresh
();
return
true
;
}
try
{
await
operation
();
toast
.
success
(
message
,
{
timeout
:
4
000
});
refresh
();
return
true
;
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
timeout
:
4
000
});
return
false
;
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
timeout
:
8
000
});
return
false
;
}
finally
{
setBusyKey
(
''
);
}
finally
{
setBusyKey
(
''
);
}
},
[
refresh
]);
},
[
refresh
]);
...
@@ -115,7 +117,7 @@ export default function NotificationListPage() {
...
@@ -115,7 +117,7 @@ export default function NotificationListPage() {
if
(
target
.
external
)
window
.
open
(
target
.
href
,
'_blank'
,
'noopener,noreferrer'
);
if
(
target
.
external
)
window
.
open
(
target
.
href
,
'_blank'
,
'noopener,noreferrer'
);
else
history
.
push
(
target
.
href
);
else
history
.
push
(
target
.
href
);
refresh
();
refresh
();
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
timeout
:
4
000
});
}
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
timeout
:
8
000
});
}
finally
{
setBusyKey
(
''
);
}
finally
{
setBusyKey
(
''
);
}
},
[
refresh
,
userInfo
?.
_id
]);
},
[
refresh
,
userInfo
?.
_id
]);
...
@@ -135,7 +137,7 @@ export default function NotificationListPage() {
...
@@ -135,7 +137,7 @@ export default function NotificationListPage() {
<
Tabs
.
ListContainer
><
Tabs
.
List
aria
-
label=
"通知状态"
>
{
READ_OPTIONS
.
map
((
option
)
=>
<
Tabs
.
Tab
id=
{
option
.
key
}
key=
{
option
.
key
}
>
{
option
.
label
}
<
Tabs
.
Indicator
/></
Tabs
.
Tab
>)
}
</
Tabs
.
List
></
Tabs
.
ListContainer
>
<
Tabs
.
ListContainer
><
Tabs
.
List
aria
-
label=
"通知状态"
>
{
READ_OPTIONS
.
map
((
option
)
=>
<
Tabs
.
Tab
id=
{
option
.
key
}
key=
{
option
.
key
}
>
{
option
.
label
}
<
Tabs
.
Indicator
/></
Tabs
.
Tab
>)
}
</
Tabs
.
List
></
Tabs
.
ListContainer
>
</
Tabs
>
</
Tabs
>
</
div
>
</
div
>
{
error
?
<
Alert
status=
"danger"
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
><
Button
size=
"sm"
variant=
"ghost"
onPress=
{
refresh
}
>
重试
</
Button
></
Alert
>
:
null
}
{
!
loading
&&
error
?
<
LoadErrorState
title=
"通知加载失败"
onRetry=
{
refresh
}
/
>
:
null
}
{
loading
&&
items
.
length
===
0
?
<
div
className=
"flex min-h-64 items-center justify-center"
><
Spinner
size=
"sm"
/></
div
>
:
null
}
{
loading
&&
items
.
length
===
0
?
<
div
className=
"flex min-h-64 items-center justify-center"
><
Spinner
size=
"sm"
/></
div
>
:
null
}
{
!
loading
&&
!
error
&&
items
.
length
===
0
?
<
EmptyState
className=
"min-h-64"
size=
"md"
><
EmptyState
.
Media
><
Bell
size=
{
22
}
/></
EmptyState
.
Media
><
EmptyState
.
Header
><
EmptyState
.
Title
>
{
category
===
'todo'
?
'暂无待办通知'
:
'暂无通知消息'
}
</
EmptyState
.
Title
><
EmptyState
.
Description
>
{
keyword
?
'没有找到匹配的通知,请调整搜索条件。'
:
readStatus
===
'unread'
?
'当前没有未读通知。'
:
'新的通知会显示在这里。'
}
</
EmptyState
.
Description
></
EmptyState
.
Header
></
EmptyState
>
:
null
}
{
!
loading
&&
!
error
&&
items
.
length
===
0
?
<
EmptyState
className=
"min-h-64"
size=
"md"
><
EmptyState
.
Media
><
Bell
size=
{
22
}
/></
EmptyState
.
Media
><
EmptyState
.
Header
><
EmptyState
.
Title
>
{
category
===
'todo'
?
'暂无待办通知'
:
'暂无通知消息'
}
</
EmptyState
.
Title
><
EmptyState
.
Description
>
{
keyword
?
'没有找到匹配的通知,请调整搜索条件。'
:
readStatus
===
'unread'
?
'当前没有未读通知。'
:
'新的通知会显示在这里。'
}
</
EmptyState
.
Description
></
EmptyState
.
Header
></
EmptyState
>
:
null
}
{
items
.
length
>
0
?
<
div
className=
"flex min-w-0 flex-col gap-3"
aria
-
busy=
{
loading
}
>
{
items
.
map
((
item
)
=>
<
NotificationItem
category=
{
category
}
currentUserId=
{
userInfo
?.
_id
}
isBusy=
{
busyKey
===
item
.
name
}
key=
{
item
.
name
||
item
.
_id
}
notification=
{
item
}
onAction=
{
(
notification
,
action
)
=>
run
(
notification
.
name
,
()
=>
executeNotificationAction
(
notification
,
action
),
'待办已处理'
)
}
onMarkRead=
{
(
notification
)
=>
run
(
notification
.
name
,
()
=>
markNotificationRead
(
notification
.
name
),
'已标记为已读'
)
}
onOpenTarget=
{
openTarget
}
/>)
}
</
div
>
:
null
}
{
items
.
length
>
0
?
<
div
className=
"flex min-w-0 flex-col gap-3"
aria
-
busy=
{
loading
}
>
{
items
.
map
((
item
)
=>
<
NotificationItem
category=
{
category
}
currentUserId=
{
userInfo
?.
_id
}
isBusy=
{
busyKey
===
item
.
name
}
key=
{
item
.
name
||
item
.
_id
}
notification=
{
item
}
onAction=
{
(
notification
,
action
)
=>
run
(
notification
.
name
,
()
=>
executeNotificationAction
(
notification
,
action
),
'待办已处理'
)
}
onMarkRead=
{
(
notification
)
=>
run
(
notification
.
name
,
()
=>
markNotificationRead
(
notification
.
name
),
'已标记为已读'
)
}
onOpenTarget=
{
openTarget
}
/>)
}
</
div
>
:
null
}
...
...
src/tailwind.css
View file @
36617df8
...
@@ -7,6 +7,7 @@
...
@@ -7,6 +7,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"
;
/* Keep Tailwind's watcher scoped to authored source. Umi's generated .umi
/* Keep Tailwind's watcher scoped to authored source. Umi's generated .umi
* files and hot-update artifacts must not trigger a CSS rebuild loop. */
* files and hot-update artifacts must not trigger a CSS rebuild loop. */
...
...
src/wrapper/Site.jsx
View file @
36617df8
import
useFeedbackState
from
'@/hooks/useFeedbackState'
;
import
{
getSystemInfo
}
from
'@/services/DataService'
;
import
{
getSystemInfo
}
from
'@/services/DataService'
;
import
{
APP_NAME
}
from
'@/config/ApplicationConfig'
;
import
{
APP_NAME
}
from
'@/config/ApplicationConfig'
;
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
...
@@ -19,7 +20,7 @@ export default function SiteWrapper() {
...
@@ -19,7 +20,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
(
'error
'
);
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