Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
A
audit_manager_new
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
audit_manager_new
Commits
59f021c1
Commit
59f021c1
authored
Aug 26, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: align application switcher with Xinyuan baseline
parent
6a36f6ce
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
222 additions
and
191 deletions
+222
-191
XINYUAN_DESIGN_SYSTEM.md
docs/XINYUAN_DESIGN_SYSTEM.md
+1
-1
ui-architecture.md
docs/ui-architecture.md
+2
-0
ui-migration-checklist.md
docs/ui-migration-checklist.md
+3
-2
xinyuan-design-system.md
docs/xinyuan-design-system.md
+4
-1
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+7
-2
ApplicationSwitcherDialog.jsx
src/components/ApplicationSwitcherDialog.jsx
+184
-185
shell.css
src/design-system/shell.css
+16
-0
Layout.jsx
src/pages/audit_management/Layout.jsx
+3
-0
audit-management.business-flow.md
src/pages/audit_management/audit-management.business-flow.md
+2
-0
No files found.
docs/XINYUAN_DESIGN_SYSTEM.md
View file @
59f021c1
...
...
@@ -30,7 +30,7 @@
-
登录采用居中认证 Card。花形 Logo 约 36px,标题约 32px/600;不重复显示“登录”标题。
-
账号、密码、验证码可见高度统一为 40px,字段组可见间距 24px,提交按钮上方保留 24px。
-
登录字段有值或自动填充时使用
`#fbf1f1`
,Hover 使用
`#faeaea`
,Focus 使用鑫元红描边和浅红焦点环。
-
应用切换器固定为居中的紧凑
三列列表,标题“应用切换”、副标题“选择你可访问的应用”;最多一个“当前”标签,不显示数量、权限标签、外链图标或统计内容
。
-
应用切换器固定为居中的紧凑
两列 HeroUI Modal,移动端一列;标题“应用切换”、副标题“选择你可访问的应用”。分组与成员顺序读取站点
`site.group`
,组标题显示真实应用数量,空组隐藏,未分组可访问项归入“其他应用”,无配置时回退“全部应用”;最多一个“当前”标签,不显示权限标签或外链装饰
。
## 4. 页面、筛选与 Tab
...
...
docs/ui-architecture.md
View file @
59f021c1
...
...
@@ -26,6 +26,7 @@
-
多选:共享 Autocomplete composition 在
`Autocomplete.Value`
内使用 HeroUI
`TagGroup + Tag + Tag.RemoveButton`
;根
`selectedKeys`
是唯一选择状态,移除操作回写同一个
`onSelectionChange`
,业务页面只负责值协议映射。
-
列表:Pro DataGrid 原生 selection/sort/fixed columns + HeroUI Pagination + Pro ActionBar/EmptyState。
-
弹层:HeroUI Modal/AlertDialog/Dropdown/Tooltip,根主题覆盖 body Portal。
-
应用切换:共享
`ApplicationSwitcherDialog`
直接组合 HeroUI Modal/Card/Surface/Avatar/Chip 与 Pro EmptyState;
`SiteContext.siteInfo.group`
只负责动态分组与顺序,
`UserContext.applications`
和
`/application/info`
继续负责访问集合与详情。授权域
`audit_manager`
规范化为唯一当前项,并导航到部署基路径
`/audit_manager_new/`
。
-
上传:Pro DropZone,页面边界接收真实 File。
-
KPI/图表:仅业务存在时使用 Pro KPI/PieChart/BarChart/ComposedChart/ChartTooltip。
-
报告视图:一级 HeroUI Tabs 切换 current/history,二级内容宽度 Tabs 以稳定项目 ID 切换报告快照;当前项目只挂载工具栏和 DataGrid,不重复 Tabs 已表达的项目标题,整体进度由 KPI 承担。项目与控制点仍携带原数组索引进入现有更新/确认回调。
...
...
@@ -37,6 +38,7 @@
-
`theme.css`
提供 HeroUI 核心颜色、半径、阴影、字段和 dark token,必须与技能资产逐字节一致。
-
`tokens.css`
只能添加
`--rc-*`
别名、布局尺寸、业务语义 chip/chart tokens,不能重定义核心变量。
-
共享 Shell/layout CSS 只负责几何和技能批准的跨应用模式。
-
应用切换 CSS 仅允许
`46rem/42rem`
弹窗几何和 36px identity well 的共享 bridge class;Card、Surface、Avatar、Chip 与交互状态均由 HeroUI variant/compound API 管理。
-
页面 className 只负责 flex、grid、宽度、间距、对齐和响应式;禁止外观、状态、slot/BEM 和伪元素覆盖。
## 业务边界
...
...
docs/ui-migration-checklist.md
View file @
59f021c1
# UI 迁移清单
状态日期:2026-08-2
1
。
`完成`
仅表示代码、业务保护、双应用浏览器对照和扫描门槛全部通过。
状态日期:2026-08-2
6
。
`完成`
仅表示代码、业务保护、双应用浏览器对照和扫描门槛全部通过。
| 模块 | 路由/入口 | 代码迁移 | 浏览器对照 | 业务保护 |
| --- | --- | --- | --- | --- |
...
...
@@ -9,7 +9,7 @@
| Login |
`/login`
| 完成 | 四视口双主题及 filled 完成 | 完成 |
| AppShell/Navbar/Sidebar | 鉴权路由 | 完成 | 缺有效登录态 | 代码复核完成 |
| 用户菜单/退出 | Sidebar footer | 完成 | 缺有效登录态 | 回调保留 |
| 应用切换 | Shell Modal |
完成 | 缺有效登录态 | 数据/
导航保留 |
| 应用切换 | Shell Modal |
分组/数量/唯一当前态完成 | 2026-08-26 双应用均停在真实登录门禁 |
`/info`
分组、应用接口与部署
导航保留 |
| 全应用弹窗尺寸 | HeroUI Modal / AlertDialog | 完成 | 待有效登录态复验 | md 480 / lg 640 / 3xl 960 / 应用切换 736 |
| 稽核项目 |
`/audit_process`
| 完成 | 缺有效登录态 | 代码复核完成 |
| 多选字段 | 稽核项目表单 | 完成 | 缺有效登录态/打开态复验 | 框内 Tag;
`_id[]`
与请求时机保留 |
...
...
@@ -34,6 +34,7 @@
-
[
x
]
并列报告项目整合为内容宽度二级 HeroUI Tabs,单项目时隐藏轨道;搜索只过滤当前项目列表。
-
[
x
]
当前报告搜索位于二级 Tabs 下方的标准工具栏,移动端独占一行;项目区不重复 Tabs 标题和 KPI 进度。
-
[
x
]
报告附件内容使用紧凑 HeroUI Chip/Link,DataGrid 单元格内无 Card/Surface 或页面私有外观。
-
[
x
]
应用切换按站点配置动态分组,移动一列/桌面两列,使用 HeroUI 原生 Card/Surface/Avatar 并保持唯一当前态。
-
[
]
四视口、浅色/暗色、目标/基准同时打开完成(登录完成,鉴权页面待有效登录态)。
-
[
]
所有可达 popup、Modal、Tooltip、Dropdown、Sidebar 状态检查完成。
-
[
x
]
`npm run build`
与
`git diff --check`
通过;
`audit_project.sh`
退出码 0,并报告非活动遗留。
...
...
docs/xinyuan-design-system.md
View file @
59f021c1
...
...
@@ -16,7 +16,10 @@
-
HeroUI Pro Navbar 高 64px;
`Sidebar.Trigger`
使用 Pro 原生图形。
-
Navbar 与页面内容采用对称 48px/32px/16px gutter;主题操作在最右。
-
用户行由
`Dropdown.Trigger`
打开真实菜单,退出调用目标应用现有服务。
-
应用切换使用 HeroUI Modal,桌面宽约 46rem、高不超过 42rem,一列移动/两列桌面;最多一个“当前”。
-
应用切换使用 HeroUI Modal,标题固定为“应用切换”,副标题固定为“选择你可访问的应用”;弹窗宽
`min(46rem, 100vw - 2rem)`
、高不超过
`min(42rem, 100dvh - 2rem)`
,28px 内边距,只有 Modal Body 滚动。
-
应用按
`/info`
的
`site.group`
与组内成员顺序动态分区,隐藏无可访问应用的分组,未被配置引用的可访问应用进入“其他应用”;无分组配置时显示“全部应用”。分组标题右侧显示真实可访问数量,禁止硬编码分组或应用。
-
每个分组在移动端一列、
`sm`
及以上严格两列,组间 24px、标题到卡片 12px、卡片间 12px;应用项使用 HeroUI
`Card variant="secondary"`
、
`Surface variant="tertiary"`
与
`Avatar variant="soft"`
,identity well 固定 36px。
-
应用列表合并
`/application/info`
与当前用户授权应用;接口失败回退用户授权信息。授权域
`audit_manager`
映射到部署路径
`/audit_manager_new/`
,整个弹窗最多一个 accent“当前” Chip。
## 登录
...
...
docs/xinyuan-ui-audit.md
View file @
59f021c1
# 监察稽核管理 UI 审计
审计日期:2026-08-2
1
审计日期:2026-08-2
6
当前分支:
`codex/ui-ux-xinyuan`
唯一实现基准:
`/Users/liuchenlong/Desktop/job/problem_rectification`
...
...
@@ -49,7 +49,9 @@
-
登录已同步基准的居中 Card、双主题 Logo、字段节奏、验证码 Button、filled/autofill 与响应式几何。
-
鉴权 Shell 使用共享
`AppShell`
、Pro Navbar、Pro Sidebar offcanvas、用户 Dropdown、真实退出回调和应用切换 Modal。
-
应用切换使用基准两列结构、L2 Card 与 L3 Surface + Avatar identity well。
-
应用切换已按
`model_analysis`
基准改为动态分组结构:读取
`SiteContext.siteInfo.group`
保留后端分组及成员顺序,隐藏空组,将未配置的可访问应用归入“其他应用”,并显示真实组内数量。
-
应用项使用严格一列移动/两列桌面结构、L2 HeroUI Card 与 L3 Surface + Avatar identity well;弹窗保持 46rem/42rem、28px padding、Body 单独滚动及唯一“当前” Chip。
-
应用数据仍来自既有
`/application/info`
与
`userInfo.applications`
回退。仅将授权域
`audit_manager`
规范化为当前应用并导航至既有
`/audit_manager_new/`
部署基路径,未修改接口、权限或登录状态流。
-
活动列表使用 Pro DataGrid、Pagination、ActionBar、EmptyState;选择、排序、固定列及分页业务回调保留。
-
活动 DataGrid 操作列统一将
`tertiary`
Eye 查看按钮放在首位:项目控制点与报告控制点打开当前行的只读 HeroUI Modal,历史报告进入原详情路由;编辑、确认、删除权限和回调不变。
-
报告视图“进行中 / 历史报告”Tabs 已移除
`flex-1`
伸展,根节点和 ListContainer 按基准使用内容宽度;同行导出、填报链接和提交操作继续由外层布局靠右。
...
...
@@ -77,6 +79,9 @@
## 验收证据与剩余边界
-
`npm run build`
与
`git diff --check`
通过。
-
2026-08-26 应用切换变更后重新执行生产构建,Webpack 编译通过;目标独立开发服务运行于
`http://localhost:8011/audit_manager_new/`
,
`model_analysis`
基准运行于
`http://localhost:8005/model_analysis/`
,两者 HTTP 200。
-
使用全新 Playwright 会话分别进入目标
`/audit_process`
和基准
`/models/overview`
,两者都由真实鉴权重定向至各自登录页,登录页 console 均为 0 error / 0 warning。现存带
`browser-only-token`
的 QA 会话未用于验收,也未向目标注入用户、token 或业务数据。
-
因当前没有有效真实登录凭据,本次无法打开目标应用切换 Modal,动态分组、四视口双主题、Body 滚动、关闭交互与唯一当前态的浏览器对照仍待补齐;代码对照、生产构建、HeroUI Pro ESM 子路径导入和静态扫描已通过,不据此声明浏览器验收完成。
-
2026-08-21 重新打开
`http://localhost:8001/audit_manager_new/`
,应用正常跳转真实登录页,当前开发预览 console 为 0 error / 0 warning。
-
目标与“问题整改”登录页在 1440x900、1280x720、768x1024、390x844 的浅色/暗色几何逐项一致,所有视口横向溢出为 0,console 为 0 error / 0 warning。
-
已填写态三个字段均为 40px,目标与基准的 filled Surface 计算值一致,未提交测试登录请求。
...
...
src/components/ApplicationSwitcherDialog.jsx
View file @
59f021c1
import
xinyuanLogo
from
'@/assets/identity/xy-logo-red.png'
;
import
{
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
APP_BASE_PATH
,
APP_DISPLAY_NAME
,
APP_ROUTE_NAME
}
from
'@/constants/app'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
Close
,
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
...
...
@@ -13,152 +15,165 @@ import {
}
from
'@heroui/react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
function
normalizeLabelList
(
items
)
{
if
(
!
Array
.
isArray
(
items
))
{
return
[];
}
return
items
.
map
((
item
)
=>
{
if
(
typeof
item
===
'string'
)
{
return
item
;
}
return
item
?.
display_name
||
item
?.
title
||
item
?.
name
||
''
;
})
.
filter
(
Boolean
);
const
APPLICATION_PERMISSION_NAME
=
'audit_manager'
;
function
unwrapApplications
(
response
)
{
const
candidates
=
[
response
,
response
?.
data
,
response
?.
applications
,
response
?.
apps
,
response
?.
records
,
response
?.
rows
,
response
?.
list
,
response
?.
result
,
];
return
candidates
.
find
(
Array
.
isArray
)
||
[];
}
function
resolveApplicationUrl
(
application
)
{
if
(
application
?.
name
)
{
return
`/
${
application
.
name
}
`
;
}
return
'#'
;
function
isCurrentApplicationName
(
name
)
{
return
name
===
APPLICATION_PERMISSION_NAME
||
name
===
APP_ROUTE_NAME
;
}
function
normalizeApplication
(
application
,
fallback
=
{})
{
const
name
=
application
?.
name
||
fallback
?.
name
||
''
;
function
normalizeApplication
(
application
=
{})
{
const
sourceName
=
application
.
name
||
''
;
const
current
=
isCurrentApplicationName
(
sourceName
);
const
name
=
current
?
APPLICATION_PERMISSION_NAME
:
sourceName
;
return
{
name
,
title
:
application
?.
title
||
fallback
?.
title
||
application
?.
name
||
fallback
?.
name
||
'未命名应用'
,
title
:
current
?
APP_DISPLAY_NAME
:
application
.
title
||
name
||
'未命名应用'
,
description
:
application
?.
description
||
fallback
?.
description
||
fallback
?.
title
||
application
?.
title
||
application
.
description
||
application
.
subtitle
||
application
.
title
||
'进入对应应用继续处理业务'
,
logo
:
application
?.
logo
||
fallback
?.
logo
||
null
,
url
:
resolveApplicationUrl
({
name
,
}),
access
:
application
?.
access
||
fallback
?.
access
||
''
,
status
:
application
?.
status
||
fallback
?.
status
||
''
,
type
:
application
?.
type
||
fallback
?.
type
||
''
,
order
:
application
?.
order
??
fallback
?.
order
??
0
,
roles
:
normalizeLabelList
(
application
?.
roles
?.
length
?
application
.
roles
:
fallback
?.
roles
),
groups
:
normalizeLabelList
(
application
?.
groups
?.
length
?
application
.
groups
:
fallback
?.
groups
),
logo
:
current
?
xinyuanLogo
:
application
.
logo
||
application
.
icon
||
null
,
order
:
Number
(
application
.
order
||
0
),
url
:
current
?
APP_BASE_PATH
:
application
.
url
||
application
.
path
||
(
name
?
`/
${
name
}
`
:
'#'
),
};
}
function
mergeApplications
(
fetchedApplications
,
fallbackApplications
)
{
const
fallbackMap
=
new
Map
(
fallbackApplications
.
map
((
application
)
=>
[
application
.
name
,
application
]),
);
const
normalizedFetched
=
fetchedApplications
.
map
((
application
)
=>
normalizeApplication
(
application
,
fallbackMap
.
get
(
application
?.
name
)),
)
.
filter
((
application
)
=>
application
.
name
);
const
mergedMap
=
new
Map
(
normalizedFetched
.
map
((
application
)
=>
[
application
.
name
,
application
]),
);
fallbackApplications
.
forEach
((
application
)
=>
{
if
(
!
application
?.
name
||
mergedMap
.
has
(
application
.
name
))
{
return
;
}
mergedMap
.
set
(
application
.
name
,
normalizeApplication
(
application
,
application
));
});
return
Array
.
from
(
mergedMap
.
values
()).
sort
((
left
,
right
)
=>
{
const
orderDifference
=
Number
(
left
.
order
||
0
)
-
Number
(
right
.
order
||
0
);
if
(
orderDifference
!==
0
)
{
return
orderDifference
;
}
const
currentFallback
=
fallbackApplications
.
find
((
application
)
=>
isCurrentApplicationName
(
application
?.
name
),
)
||
{
name
:
APPLICATION_PERMISSION_NAME
,
title
:
APP_DISPLAY_NAME
,
};
const
merged
=
new
Map
();
[...
fallbackApplications
,
...
fetchedApplications
,
currentFallback
]
.
map
(
normalizeApplication
)
.
filter
((
application
)
=>
application
.
name
)
.
forEach
((
application
)
=>
{
merged
.
set
(
application
.
name
,
{
...
merged
.
get
(
application
.
name
),
...
application
,
});
});
return
left
.
title
.
localeCompare
(
right
.
title
,
'zh-CN'
);
return
Array
.
from
(
merged
.
values
()).
sort
((
left
,
right
)
=>
{
return
left
.
order
-
right
.
order
||
left
.
title
.
localeCompare
(
right
.
title
,
'zh-CN'
);
});
}
function
isCurrentApplication
(
application
)
{
if
(
typeof
window
===
'undefined'
)
{
return
false
;
function
buildApplicationSections
(
applications
,
applicationGroups
)
{
if
(
!
Array
.
isArray
(
applicationGroups
)
||
applicationGroups
.
length
===
0
)
{
return
[{
key
:
'all'
,
label
:
'全部应用'
,
applications
}]
;
}
const
currentPath
=
window
.
location
.
pathname
;
const
applicationRoot
=
application
?.
name
?
`/
${
application
.
name
}
`
:
''
;
const
applicationMap
=
new
Map
(
applications
.
map
((
application
)
=>
[
application
.
name
,
application
]),
);
const
groupedApplicationNames
=
new
Set
();
const
sections
=
applicationGroups
.
map
((
group
,
index
)
=>
{
const
groupApplications
=
Array
.
isArray
(
group
?.
applications
)
?
group
.
applications
.
map
((
name
)
=>
{
const
application
=
applicationMap
.
get
(
name
);
if
(
application
)
groupedApplicationNames
.
add
(
name
);
return
application
;
})
.
filter
(
Boolean
)
:
[];
return
{
key
:
group
?.
label
||
`group-
${
index
}
`
,
label
:
group
?.
label
||
'应用分组'
,
applications
:
groupApplications
,
};
})
.
filter
((
section
)
=>
section
.
applications
.
length
>
0
);
const
ungroupedApplications
=
applications
.
filter
(
(
application
)
=>
!
groupedApplicationNames
.
has
(
application
.
name
),
);
if
(
application
?.
url
?.
startsWith
(
'http'
))
{
try
{
const
url
=
new
URL
(
application
.
url
);
return
url
.
pathname
===
currentPath
;
}
catch
(
error
)
{
return
false
;
}
if
(
ungroupedApplications
.
length
>
0
)
{
sections
.
push
({
key
:
'ungrouped'
,
label
:
'其他应用'
,
applications
:
ungroupedApplications
,
});
}
return
(
application
?.
url
===
currentPath
||
(
!!
applicationRoot
&&
(
currentPath
===
applicationRoot
||
currentPath
.
startsWith
(
`
${
applicationRoot
}
/`
)))
);
return
sections
.
length
>
0
?
sections
:
[{
key
:
'all'
,
label
:
'全部应用'
,
applications
}];
}
function
ApplicationCard
({
application
,
current
})
{
const
target
=
application
?.
url
?.
startsWith
(
'http'
)
?
'_blank'
:
undefined
;
const
target
=
/^https
?
:
\/\/
/i
.
test
(
application
.
url
)
?
'_blank'
:
undefined
;
const
initial
=
application
.
title
.
trim
().
slice
(
0
,
1
).
toUpperCase
()
||
'A'
;
return
(
<
Card
className=
"h-full"
variant=
"secondary"
>
<
Card
.
Content
>
<
a
href=
{
application
.
url
}
target=
{
target
}
rel=
{
target
?
'noreferrer'
:
undefined
}
aria
-
current=
{
current
?
'page'
:
undefined
}
className=
"flex min-w-0 items-center gap-3 no-underline"
href=
{
application
.
url
}
rel=
{
target
?
'noreferrer'
:
undefined
}
target=
{
target
}
>
<
Surface
aria
-
hidden=
"true"
className=
"
grid size-9 shrink-0 place-items-center overflow-hidden rounded-[var(--rc-radius-control)]
"
className=
"
rc-app-identity-well grid size-9 shrink-0 place-items-center overflow-hidden
"
variant=
"tertiary"
>
<
Avatar
className=
"
size-full rounded-[var(--rc-radius-control)] bg-transparent
"
className=
"
rc-app-identity-avatar size-full
"
color=
"accent"
variant=
"soft"
>
{
application
.
logo
?
(
<
Avatar
.
Image
className=
"object-contain p-1.5"
src=
{
application
.
logo
}
alt=
""
/>
<
Avatar
.
Image
alt=
""
className=
"object-contain p-1.5"
src=
{
application
.
logo
}
/>
)
:
null
}
<
Avatar
.
Fallback
>
{
initial
}
</
Avatar
.
Fallback
>
</
Avatar
>
</
Surface
>
<
span
className=
"min-w-0 flex-1"
>
<
span
className=
"block
break-words font-medium leading-5 text-foreground"
title=
{
application
.
title
}
>
<
span
className=
"block
truncate text-sm font-medium leading-5"
>
{
application
.
title
}
</
span
>
<
span
className=
"block truncate text-sm text-muted"
title=
{
application
.
description
}
>
{
application
.
description
||
application
.
name
||
'应用'
}
</
spa
n
>
<
Description
className=
"mt-0.5 block truncate leading-4"
>
{
application
.
description
}
</
Descriptio
n
>
</
span
>
{
current
?
<
Chip
color=
"accent"
size=
"sm"
>
当前
</
Chip
>
:
null
}
{
current
?
(
<
Chip
color=
"accent"
size=
"sm"
>
当前
</
Chip
>
)
:
null
}
</
a
>
</
Card
.
Content
>
</
Card
>
...
...
@@ -167,7 +182,7 @@ function ApplicationCard({ application, current }) {
function
LoadingSkeleton
()
{
return
(
<
div
className=
"grid gap-3 sm:grid-cols-2"
aria
-
label=
"正在加载应用
"
>
<
div
aria
-
label=
"正在加载应用"
className=
"grid gap-3 sm:grid-cols-2
"
>
{
Array
.
from
({
length
:
6
}).
map
((
_
,
index
)
=>
(
<
Card
key=
{
index
}
variant=
"secondary"
>
<
Card
.
Content
className=
"flex items-center gap-3"
>
...
...
@@ -186,84 +201,53 @@ function LoadingSkeleton() {
export
default
function
ApplicationSwitcherDialog
({
open
,
onClose
,
siteInfo
,
userInfo
,
})
{
const
fallbackApplications
=
useMemo
(()
=>
{
if
(
!
Array
.
isArray
(
userInfo
?.
applications
))
{
return
[];
}
return
userInfo
.
applications
.
map
((
application
)
=>
normalizeApplication
(
application
,
{
...
application
,
description
:
application
?.
title
,
}),
)
.
filter
((
application
)
=>
application
.
name
);
},
[
userInfo
]);
const
[
applications
,
setApplications
]
=
useState
(
fallbackApplications
);
const
[
status
,
setStatus
]
=
useState
(
fallbackApplications
.
length
>
0
?
'success'
:
'idle'
,
const
fallbackApplications
=
useMemo
(
()
=>
(
Array
.
isArray
(
userInfo
?.
applications
)
?
userInfo
.
applications
:
[]),
[
userInfo
],
);
const
[
applications
,
setApplications
]
=
useState
(()
=>
mergeApplications
([],
fallbackApplications
),
);
const
[
status
,
setStatus
]
=
useState
(
'idle'
);
const
[
errorMessage
,
setErrorMessage
]
=
useState
(
''
);
const
currentApplicationName
=
useMemo
(()
=>
{
return
applications
.
find
(
isCurrentApplication
)?.
name
||
''
;
return
(
applications
.
find
(
(
application
)
=>
application
.
name
===
APPLICATION_PERMISSION_NAME
,
)?.
name
||
applications
.
find
((
application
)
=>
application
.
name
===
APP_ROUTE_NAME
)?.
name
||
''
);
},
[
applications
]);
const
applicationSections
=
useMemo
(
()
=>
buildApplicationSections
(
applications
,
siteInfo
?.
group
),
[
applications
,
siteInfo
?.
group
],
);
useEffect
(()
=>
{
if
(
fallbackApplications
.
length
===
0
)
{
return
;
}
setApplications
((
previous
)
=>
{
if
(
previous
.
length
>
0
)
{
return
previous
;
}
return
fallbackApplications
;
});
},
[
fallbackApplications
]);
useEffect
(()
=>
{
if
(
!
open
)
{
return
;
}
if
(
!
open
)
return
undefined
;
let
cancelled
=
false
;
const
loadApplications
=
async
()
=>
{
setStatus
((
previous
)
=>
(
previous
===
'success'
?
previous
:
'loading'
));
setErrorMessage
(
''
);
try
{
const
response
=
await
getApplications
();
const
fetchedApplications
=
Array
.
isArray
(
response
)
?
response
:
[];
if
(
cancelled
)
{
return
;
}
const
mergedApplications
=
mergeApplications
(
fetchedApplications
,
fallbackApplications
,
setStatus
((
previous
)
=>
(
previous
===
'success'
?
previous
:
'loading'
));
setErrorMessage
(
''
);
getApplications
()
.
then
((
response
)
=>
{
if
(
cancelled
)
return
;
setApplications
(
mergeApplications
(
unwrapApplications
(
response
),
fallbackApplications
),
);
setApplications
(
mergedApplications
);
setStatus
(
'success'
);
}
catch
(
error
)
{
if
(
cancelled
)
{
return
;
}
setApplications
(
fallbackApplications
);
})
.
catch
(()
=>
{
if
(
cancelled
)
return
;
setApplications
(
mergeApplications
([],
fallbackApplications
));
setStatus
(
'error'
);
setErrorMessage
(
'应用列表加载失败,已回退展示当前账号上的应用信息。'
);
}
};
loadApplications
();
});
return
()
=>
{
cancelled
=
true
;
...
...
@@ -275,52 +259,67 @@ export default function ApplicationSwitcherDialog({
<
Modal
.
Trigger
aria
-
hidden=
"true"
className=
"sr-only"
tabIndex=
{
-
1
}
/>
<
Modal
.
Backdrop
isOpen=
{
open
}
variant=
"blur"
onOpenChange=
{
(
isOpen
)
=>
{
if
(
!
isOpen
)
onClose
?.();
}
}
variant=
"blur"
>
<
Modal
.
Container
className=
"w-full sm:w-full"
placement=
"center"
scroll=
"inside"
size=
"lg"
>
<
Modal
.
Container
placement=
"center"
scroll=
"inside"
size=
"lg"
>
<
Modal
.
Dialog
className=
"max-h-[min(42rem,calc(100dvh-2rem))]
"
style=
{
{
maxWidth
:
'min(46rem, calc(100vw - 2rem))'
}
}
aria
-
label=
"应用切换
"
className=
"rc-app-switcher-dialog p-7"
>
<
Modal
.
CloseTrigger
aria
-
label=
"关闭"
>
<
Close
size=
{
16
}
/>
</
Modal
.
CloseTrigger
>
<
Modal
.
CloseTrigger
aria
-
label=
"关闭"
style=
{
{
insetInlineEnd
:
'1.75rem'
,
top
:
'1.75rem'
}
}
/>
<
Modal
.
Header
>
<
div
className=
"min-w-0"
>
<
Modal
.
Heading
>
应用切换
</
Modal
.
Heading
>
<
Description
>
选择你可访问的应用
</
Description
>
</
div
>
<
Modal
.
Heading
>
应用切换
</
Modal
.
Heading
>
<
Description
>
选择你可访问的应用
</
Description
>
</
Modal
.
Header
>
<
Modal
.
Body
>
<
Modal
.
Body
style=
{
{
marginTop
:
'0.75rem'
}
}
>
{
status
===
'error'
&&
errorMessage
?
(
<
Alert
status=
"warning"
>
<
Alert
.
Indicator
><
ExclamationTriangleIcon
size=
{
20
}
/></
Alert
.
Indicator
>
<
Alert
.
Content
><
Alert
.
Description
>
{
errorMessage
}
</
Alert
.
Description
></
Alert
.
Content
>
<
Alert
.
Indicator
>
<
ExclamationTriangleIcon
size=
{
20
}
/>
</
Alert
.
Indicator
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
errorMessage
}
</
Alert
.
Description
>
</
Alert
.
Content
>
</
Alert
>
)
:
null
}
{
status
===
'loading'
&&
applications
.
length
===
0
?
(
<
LoadingSkeleton
/>
)
:
applications
.
length
>
0
?
(
<
div
className=
"grid gap-3 sm:grid-cols-2"
>
{
applications
.
map
((
application
)
=>
(
<
ApplicationCard
key=
{
application
.
name
}
application=
{
application
}
current=
{
application
.
name
===
currentApplicationName
}
/>
<
div
className=
"space-y-6"
>
{
applicationSections
.
map
((
section
)
=>
(
<
section
key=
{
section
.
key
}
>
<
div
className=
"mb-3 flex items-center justify-between gap-3"
>
<
h3
className=
"text-sm font-medium leading-5"
>
{
section
.
label
}
</
h3
>
<
Description
>
{
section
.
applications
.
length
}
个应用
</
Description
>
</
div
>
<
div
className=
"grid gap-3 sm:grid-cols-2"
>
{
section
.
applications
.
map
((
application
)
=>
(
<
ApplicationCard
key=
{
application
.
name
}
application=
{
application
}
current=
{
application
.
name
===
currentApplicationName
}
/>
))
}
</
div
>
</
section
>
))
}
</
div
>
)
:
(
<
EmptyState
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
暂无可切换应用
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
当前账号还没有分配其它应用权限。
</
EmptyState
.
Description
>
<
EmptyState
.
Description
>
当前账号还没有分配其它应用权限。
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
</
EmptyState
>
)
}
...
...
src/design-system/shell.css
View file @
59f021c1
...
...
@@ -1397,6 +1397,22 @@
line-height
:
1rem
;
}
.rc-app-switcher-dialog
{
min-width
:
0
;
width
:
min
(
46rem
,
calc
(
100vw
-
2rem
));
max-width
:
min
(
46rem
,
calc
(
100vw
-
2rem
));
max-height
:
min
(
42rem
,
calc
(
100
dvh
-
2rem
));
}
.rc-app-identity-well
,
.rc-app-identity-avatar
{
border-radius
:
var
(
--rc-radius-control
);
}
.rc-app-identity-avatar
{
background
:
transparent
;
}
@media
(
max-width
:
768px
)
{
.rc-mobile-sidebar.app-sidebar-glass
{
width
:
100%
;
...
...
src/pages/audit_management/Layout.jsx
View file @
59f021c1
...
...
@@ -3,6 +3,7 @@ import { FileText, Project } from '@/components/AppIcons';
import
ApplicationSwitcherDialog
from
'@/components/ApplicationSwitcherDialog'
;
import
{
APP_DISPLAY_NAME
,
getAppRouteTitle
}
from
'@/constants/app'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
Outlet
,
useLocation
}
from
'umi'
;
...
...
@@ -20,6 +21,7 @@ const MENU_GROUPS = [
export
default
function
AuditManagementLayout
()
{
const
location
=
useLocation
();
const
userInfo
=
useContext
(
UserContext
);
const
siteInfo
=
useContext
(
SiteContext
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
true
);
const
[
isApplicationDialogOpen
,
setIsApplicationDialogOpen
]
=
useState
(
false
);
const
currentTitle
=
useMemo
(
...
...
@@ -49,6 +51,7 @@ export default function AuditManagementLayout() {
<
ApplicationSwitcherDialog
open=
{
isApplicationDialogOpen
}
onClose=
{
()
=>
setIsApplicationDialogOpen
(
false
)
}
siteInfo=
{
siteInfo
}
userInfo=
{
userInfo
}
/>
</>
...
...
src/pages/audit_management/audit-management.business-flow.md
View file @
59f021c1
...
...
@@ -9,6 +9,8 @@
应用部署基路径为
`/audit_manager_new/`
。主应用导航使用 HeroUI Pro Sidebar,桌面端采用 offcanvas 折叠,移动端使用
`Sidebar.Mobile`
。
侧栏应用切换入口打开 HeroUI Modal。弹窗按站点
`/info`
配置的
`site.group`
顺序展示当前用户可访问应用,并通过既有
`/application/info`
补全应用信息;加载失败时回退当前用户授权列表。选择其它应用沿用其既有 URL,选择当前授权域
`audit_manager`
返回部署基路径
`/audit_manager_new/`
,不改变权限或登录状态。
## 2. 权限
-
应用授权域为
`audit_manager`
...
...
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