Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
I
integrity_risk_prevention
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
integrity_risk_prevention
Commits
473e68e6
Commit
473e68e6
authored
Aug 26, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 同步应用切换动态分组规范
parent
7b1c7ebb
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
140 additions
and
28 deletions
+140
-28
ui-architecture.md
docs/ui-architecture.md
+3
-2
ui-migration-checklist.md
docs/ui-migration-checklist.md
+3
-2
xinyuan-design-system.md
docs/xinyuan-design-system.md
+2
-1
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+14
-1
application.js
src/config/application.js
+1
-0
shell.css
src/design-system/shell.css
+6
-0
ApplicationSwitcherDialog.jsx
...s/integrity_risk_prevention/ApplicationSwitcherDialog.jsx
+111
-22
No files found.
docs/ui-architecture.md
View file @
473e68e6
...
@@ -19,7 +19,8 @@
...
@@ -19,7 +19,8 @@
-
页面继续拥有查询条件、选择记录、弹窗开关、表单草稿和请求时机。
-
页面继续拥有查询条件、选择记录、弹窗开关、表单草稿和请求时机。
-
HeroUI 组件拥有打开态、焦点、键盘导航、选中态和 Portal 可访问性。
-
HeroUI 组件拥有打开态、焦点、键盘导航、选中态和 Portal 可访问性。
-
移动导航使用 HeroUI Pro Sheet 与 Sidebar compound 内容,并在打开应用切换前关闭移动 Sheet;Dialog 使用明确的可访问名称。
-
移动导航使用 HeroUI Pro Sheet 与 Sidebar compound 内容,并在打开应用切换前关闭移动 Sheet;Dialog 使用明确的可访问名称。
-
应用切换 Modal 的卡片列表和加载骨架共用响应式网格:移动端一列、
`sm`
起固定两列、间距 12px,长列表在 Modal Body 内滚动。
-
应用切换 Modal 从
`SiteContext`
消费
`/info -> site.group`
,仅在页面编排层把现有可访问应用按动态配置分组;请求、权限、应用合并、当前应用识别和导航仍由原有业务边界负责。空组/无权限成员被过滤,重复配置只展示一次,未分组应用进入
`其他应用`
,无配置时使用
`全部应用`
。
-
应用切换 Modal 的卡片列表和加载骨架共用响应式网格:移动端一列、
`sm`
起固定两列、间距 12px;专用共享布局类只负责 46rem 宽度、42rem 最大高度和视口安全边距,长列表仅在 Modal Body 内滚动,HeroUI 自身负责 Overlay 外观、圆角、阴影和交互。
-
数据转换仍在现有 service/util 边界完成;UI 迁移不改变接口协议。
-
数据转换仍在现有 service/util 边界完成;UI 迁移不改变接口协议。
-
Modal 是独立 Overlay/L1 平面;查看态字段组使用
`Surface secondary`
/L2。详情中的关联记录是 L2 内的静态 L3 子区域,使用 HeroUI
`Surface tertiary`
包裹共享
`DataTable`
,并通过 HeroUI Pro DataGrid 官方
`secondary`
variant 区分表头和 L3 背景;页面不得覆盖 DataGrid 的 BEM、slot、背景或圆角。
-
Modal 是独立 Overlay/L1 平面;查看态字段组使用
`Surface secondary`
/L2。详情中的关联记录是 L2 内的静态 L3 子区域,使用 HeroUI
`Surface tertiary`
包裹共享
`DataTable`
,并通过 HeroUI Pro DataGrid 官方
`secondary`
variant 区分表头和 L3 背景;页面不得覆盖 DataGrid 的 BEM、slot、背景或圆角。
-
业务列表页面标题由 Shell 的 HeroUI Pro Navbar 统一呈现,
`RecordWorkbench`
内容区不再重复渲染同名
`<h2>`
,以避免标题重复并让搜索/工具栏保持首屏优先级。
-
业务列表页面标题由 Shell 的 HeroUI Pro Navbar 统一呈现,
`RecordWorkbench`
内容区不再重复渲染同名
`<h2>`
,以避免标题重复并让搜索/工具栏保持首屏优先级。
...
@@ -33,4 +34,4 @@
...
@@ -33,4 +34,4 @@
## 验收证据
## 验收证据
浏览器截图、控制台检查、构建、依赖树和扫描结果已写入
`docs/xinyuan-ui-audit.md`
。
目标服务保持运行于
`http://localhost:8002/integrity_risk_prevention_new/`
;基准服务运行于
`http://localhost:8006/problem_rectification_new/`
。业务 API、参数、数据结构、路由、权限、校验、分页口径、保存回调、登录退出和请求时机均未改动;变更集中在 HeroUI 组件结构、共享布局、动作归位、响应式 gutter 和无效 DOM 属性清理
。
浏览器截图、控制台检查、构建、依赖树和扫描结果已写入
`docs/xinyuan-ui-audit.md`
。
本轮目标服务运行于
`http://localhost:8000/integrity_risk_prevention_new/`
,应用切换专项基准运行于
`http://localhost:8004/model_analysis/`
。业务 API、参数、数据结构、路由、权限、校验、分页口径、保存回调、登录退出和请求时机均未改动;变更集中在 HeroUI 组件结构、动态分组编排和共享布局几何
。
docs/ui-migration-checklist.md
View file @
473e68e6
...
@@ -7,7 +7,8 @@
...
@@ -7,7 +7,8 @@
-
[
x
]
记录未修改应用的原始启动状态。
-
[
x
]
记录未修改应用的原始启动状态。
-
[
x
]
同步权威 theme、Tokens、Logo 与 Shell 基线。
-
[
x
]
同步权威 theme、Tokens、Logo 与 Shell 基线。
-
[
x
]
Shell 使用 HeroUI Pro Sidebar/Navbar,支持 offcanvas、主题切换、用户菜单与退出。
-
[
x
]
Shell 使用 HeroUI Pro Sidebar/Navbar,支持 offcanvas、主题切换、用户菜单与退出。
-
[
x
]
应用切换使用 HeroUI Modal 和移动一列、
`sm`
起固定两列应用卡片与加载骨架。
-
[
x
]
应用切换使用 HeroUI Modal,并同步
`model_analysis`
的 46rem/42rem/28px 几何、Body 内滚动、移动一列及
`sm`
起固定两列应用卡片与加载骨架。
-
[
x
]
应用切换按
`site.group`
动态分组,保留配置顺序和真实数量,并覆盖空组过滤、无权限过滤、重复成员去重、
`其他应用`
及
`全部应用`
回退。
-
[
x
]
登录页使用 HeroUI Card/TextField/Input/Button/Alert。
-
[
x
]
登录页使用 HeroUI Card/TextField/Input/Button/Alert。
-
[
x
]
所有活动列表使用 DataGrid、ActionBar、EmptyState、Pagination。
-
[
x
]
所有活动列表使用 DataGrid、ActionBar、EmptyState、Pagination。
-
[
x
]
全局 DataGrid populated/loading/empty 高度链与宽表内部滚动复核完成;长文本统一最多两行 + HeroUI Tooltip。
-
[
x
]
全局 DataGrid populated/loading/empty 高度链与宽表内部滚动复核完成;长文本统一最多两行 + HeroUI Tooltip。
...
@@ -30,4 +31,4 @@
...
@@ -30,4 +31,4 @@
-
[
x
]
`pnpm run build`
、
`git diff --check`
、Skill audit 已执行;audit 的 pnpm store/CJS 子路径警告已人工复核为工具误报。
-
[
x
]
`pnpm run build`
、
`git diff --check`
、Skill audit 已执行;audit 的 pnpm store/CJS 子路径警告已人工复核为工具误报。
-
[
x
]
Umi watcher 已排除浏览器验收/构建产物;开发态
`umi.css`
/Inter 字体 HMR 循环已复测并记录。
-
[
x
]
Umi watcher 已排除浏览器验收/构建产物;开发态
`umi.css`
/Inter 字体 HMR 循环已复测并记录。
剩余说明:业务没有活动图表路由,因此没有凭空添加 KPI/图表数据;
`recharts`
仅保留原依赖且无源码 import。应用切换遵循
问题整改基准和鑫元规范,移动端一列、
`sm`
起固定两列。
剩余说明:业务没有活动图表路由,因此没有凭空添加 KPI/图表数据;
`recharts`
仅保留原依赖且无源码 import。应用切换遵循
`model_analysis`
专项基准和鑫元规范,使用动态分组、移动一列及
`sm`
起固定两列。
docs/xinyuan-design-system.md
View file @
473e68e6
...
@@ -36,7 +36,8 @@
...
@@ -36,7 +36,8 @@
-
批量动作只通过
`@heroui-pro/react/action-bar`
的浮动 ActionBar 呈现;普通工具栏保留导入、全量导出和新建,主操作在最右侧。
-
批量动作只通过
`@heroui-pro/react/action-bar`
的浮动 ActionBar 呈现;普通工具栏保留导入、全量导出和新建,主操作在最右侧。
-
共享 DataGrid 以真实列最小可读宽度之和与当前 viewport 比较决定内部滚动和固定列,不以列数阈值决定宽表。
-
共享 DataGrid 以真实列最小可读宽度之和与当前 viewport 比较决定内部滚动和固定列,不以列数阈值决定宽表。
-
应用切换遵循问题整改基准:移动端一列、
`sm`
起固定两列,卡片列表与加载骨架均使用 12px 间距;桌面不扩展为三列。
-
应用切换严格遵循
`model_analysis`
专项基准:HeroUI Modal 宽度为
`min(46rem, 100vw - 2rem)`
、最大高度为
`min(42rem, 100dvh - 2rem)`
、28px inset,仅 Body 滚动;移动端一列、
`sm`
起固定两列,卡片列表与加载骨架均使用 12px 间距,桌面不扩展为三列。
-
应用切换分组必须读取
`GET /info -> site.group`
的
`{ label, applications }`
动态配置,按用户真实可访问应用匹配并保持分组和组内顺序;空组和无权限应用不展示,未分组应用进入
`其他应用`
,无分组配置时展示单个
`全部应用`
,每组右侧显示真实
`{count} 个应用`
。禁止写死真实组名、成员或数量。
## 布局约定
## 布局约定
...
...
docs/xinyuan-ui-audit.md
View file @
473e68e6
...
@@ -2,7 +2,8 @@
...
@@ -2,7 +2,8 @@
## 基线与范围
## 基线与范围
-
唯一实现基准:
`/Users/liuchenlong/Desktop/job/problem_rectification`
-
整体唯一实现基准:
`/Users/liuchenlong/Desktop/job/problem_rectification`
-
应用切换专项唯一实现基准:
`/Users/liuchenlong/Desktop/job/model_analysis`
-
目标分支:
`ui/xinyuan-heroui-migration`
-
目标分支:
`ui/xinyuan-heroui-migration`
-
目标应用:廉洁风险防控
-
目标应用:廉洁风险防控
-
原始启动:2026-08-21,目标服务实际监听
`http://localhost:8002/integrity_risk_prevention_new/`
(Umi 未采用请求的 8005 参数);基准服务
`http://localhost:8006/problem_rectification_new/`
。
-
原始启动:2026-08-21,目标服务实际监听
`http://localhost:8002/integrity_risk_prevention_new/`
(Umi 未采用请求的 8005 参数);基准服务
`http://localhost:8006/problem_rectification_new/`
。
...
@@ -53,6 +54,18 @@
...
@@ -53,6 +54,18 @@
-
生产构建浏览器会话实测 6 个应用、两列、12px 间距、唯一当前态,控制台 0 error/0 warning。浏览器临时响应仅用于验收,没有写入应用源码或业务数据。
-
生产构建浏览器会话实测 6 个应用、两列、12px 间距、唯一当前态,控制台 0 error/0 warning。浏览器临时响应仅用于验收,没有写入应用源码或业务数据。
-
工具栏主操作复核(2026-08-21):展示表页面将
`导入廉洁风险防控表(过程表)`
、
`导出廉洁风险防控表(展示表)`
、
`导出廉洁风险防控`
精简为
`导入过程表`
、
`导出展示表`
、
`导出防控措施`
;1280x720 实测四个按钮均单行排列,
`新建记录`
保持最右侧。过程表和措施库同步使用
`导出过程表`
、
`导入防控措施`
、
`导出措施描述`
,仅调整可见文案,不改变 action key、请求、回调和业务状态。
-
工具栏主操作复核(2026-08-21):展示表页面将
`导入廉洁风险防控表(过程表)`
、
`导出廉洁风险防控表(展示表)`
、
`导出廉洁风险防控`
精简为
`导入过程表`
、
`导出展示表`
、
`导出防控措施`
;1280x720 实测四个按钮均单行排列,
`新建记录`
保持最右侧。过程表和措施库同步使用
`导出过程表`
、
`导入防控措施`
、
`导出措施描述`
,仅调整可见文案,不改变 action key、请求、回调和业务状态。
### 应用切换动态分组专项复核(2026-08-26)
-
审计发现旧实现仅渲染无标题的两列平铺网格,虽然两列几何已对齐,但没有消费
`GET /info -> site.group`
,也没有分组标题、真实数量及
`其他应用`
/
`全部应用`
回退,不符合
`model_analysis`
专项基准。
-
当前实现保留既有
`/application/info`
、
`userInfo.applications`
合并、权限结果、当前应用识别与跳转语义,仅按
`site.group`
的
`{ label, applications }`
配置组织可访问应用;保持分组与组内顺序,过滤空组、无权限应用和重复成员,并把未被配置引用的应用追加到
`其他应用`
。没有分组配置时仅显示
`全部应用`
。
-
结构使用 HeroUI
`Modal`
、
`Card secondary`
、
`Surface tertiary`
、
`Avatar soft`
、
`Chip accent`
、
`Alert warning`
与 HeroUI Pro
`EmptyState`
;弹窗专用布局为
`min(46rem, 100vw - 2rem)`
、最大高度
`min(42rem, 100dvh - 2rem)`
、28px inset、Body 内滚动、移动一列及
`sm`
起固定两列。布局 CSS 不设置组件背景、颜色、描边、圆角、阴影或交互状态。
-
本轮浏览器 fixture 只用于验证分组几何、Logo fallback、长列表滚动和响应式,不写入源码或业务数据。目标和
`model_analysis`
在 1440x900、1280x720、768x1024、390x844 的浅/深色计算值逐项一致:前三个视口 Dialog 为 736px、网格为
`334px 334px`
,390px 视口 Dialog 为 358px、左右各 16px、网格为单列 302px;全部视口均为 24px 圆角、28px padding、24px 关闭控件、12px gap、36px identity well 和 0 页面溢出。
-
390x844 下目标与基准的 Modal Body 均为
`clientHeight=548px`
、
`scrollHeight=732px`
、
`overflow-y=auto`
,确认长列表只在 Body 内滚动。动态分组 fixture 实测顺序为“核心应用 / 业务管理 / 协同应用 / 其他应用”,数量为
`2 / 2 / 1 / 2`
,空组和无权限项不渲染,重复配置只展示一次且
`aria-current`
唯一;无分组 fixture 实测仅展示“全部应用”。
-
Escape、官方关闭按钮和 Backdrop 均可关闭,Dialog 可访问名称为“应用切换”,打开后焦点进入 Dialog。移除旧的自定义 Close 子节点后,干净正常态会话控制台为 0 error / 0 warning;人为 404 Logo fixture 仅用于故障 fallback 验收,不计入正常态控制台。
-
截图:目标
`/tmp/integrity-app-switcher-grouped-{1440x900,1280x720,768x1024,390x844}-{light,dark}.png`
;基准
`/tmp/model-analysis-app-switcher-grouped-{1440x900,1280x720,768x1024,390x844}-{light,dark}.png`
;最终移动深色
`/tmp/integrity-app-switcher-final-390x844-dark.png`
。
-
最终命令:
`npm run build`
PASS;
`git diff --check`
PASS;权威
`theme.css`
byte-for-byte PASS;
`pnpm list @heroui-pro/react @heroui/react @heroui/styles react react-dom react-aria-components @react-aria/utils --depth 1`
PASS;Pro CSS 导出解析和六个 JS 子路径 ESM import PASS;package.json/package-lock/pnpm-lock 的 Pro 声明均为
`1.0.0-beta.8`
。旧组件、可见原生控件、硬编码视觉、内部 BEM/slot、
`file:`
、绝对本机路径和相邻
`node_modules`
扫描无命中。
-
Skill 审计脚本 PASS 构建性检查,但仍有三项静态误报:把当前仓库 pnpm
`.pnpm`
解析路径识别为机器依赖、以 CommonJS 方式检查 ESM Pro 子路径、以及没有跨文件追踪共享
`ApplicationIdentityWell`
中的
`Surface tertiary + Avatar soft`
。实际依赖声明、ESM 导入和浏览器 36px identity 计算值均已单独通过。
`npm ls`
还会把 pnpm 布局及 Umi 18 的旧 peer 声明标记为 invalid/extraneous;项目实际
`pnpm list`
显示 HeroUI 3.2.4、Pro 1.0.0-beta.8、React/React DOM 19.2.8、React Aria Components 1.20.0 为同一运行图。
### 弹窗 Surface 与圆角复核(2026-08-21)
### 弹窗 Surface 与圆角复核(2026-08-21)
-
全量扫描
`Modal.Dialog`
、
`XinyuanModal`
和弹窗内
`Surface`
后,确认 Modal 面板继续由 HeroUI Overlay 提供 L1 背景与 24px 圆角;所有弹窗内结构性
`Surface secondary`
统一为 16px 圆角。
-
全量扫描
`Modal.Dialog`
、
`XinyuanModal`
和弹窗内
`Surface`
后,确认 Modal 面板继续由 HeroUI Overlay 提供 L1 背景与 24px 圆角;所有弹窗内结构性
`Surface secondary`
统一为 16px 圆角。
...
...
src/config/application.js
View file @
473e68e6
export
const
APPLICATION_DISPLAY_NAME
=
'廉洁风险防控'
;
export
const
APPLICATION_DISPLAY_NAME
=
'廉洁风险防控'
;
export
const
APPLICATION_BASE_PATH
=
'/integrity_risk_prevention_new/'
;
export
const
APPLICATION_BASE_PATH
=
'/integrity_risk_prevention_new/'
;
export
const
APPLICATION_ROUTE_NAME
=
'integrity_risk_prevention_new'
;
export
const
THEME_STORAGE_KEY
=
'xinyuan_color_theme'
;
export
const
THEME_STORAGE_KEY
=
'xinyuan_color_theme'
;
export
const
ROUTE_TITLES
=
Object
.
freeze
({
export
const
ROUTE_TITLES
=
Object
.
freeze
({
...
...
src/design-system/shell.css
View file @
473e68e6
...
@@ -654,6 +654,12 @@
...
@@ -654,6 +654,12 @@
.rc-dialog-panel--4xl
{
--rc-dialog-width
:
68rem
;
}
.rc-dialog-panel--4xl
{
--rc-dialog-width
:
68rem
;
}
.rc-dialog-panel--5xl
{
--rc-dialog-width
:
76rem
;
}
.rc-dialog-panel--5xl
{
--rc-dialog-width
:
76rem
;
}
.rc-application-switcher-dialog
{
width
:
min
(
46rem
,
calc
(
100vw
-
2rem
));
max-width
:
min
(
46rem
,
calc
(
100vw
-
2rem
));
max-height
:
min
(
42rem
,
calc
(
100
dvh
-
2rem
));
}
@media
(
max-width
:
640px
)
{
@media
(
max-width
:
640px
)
{
.rc-dialog-panel
{
.rc-dialog-panel
{
width
:
calc
(
100vw
-
2rem
);
width
:
calc
(
100vw
-
2rem
);
...
...
src/pages/integrity_risk_prevention/ApplicationSwitcherDialog.jsx
View file @
473e68e6
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
Close
,
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
ApplicationIdentityWell
from
'@/components/ApplicationIdentityWell'
;
import
ApplicationIdentityWell
from
'@/components/ApplicationIdentityWell'
;
import
xinyuanLogo
from
'@/assets/xy-logo-red.png'
;
import
{
APPLICATION_DISPLAY_NAME
,
APPLICATION_ROUTE_NAME
,
}
from
'@/config/application'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
import
{
Alert
,
Alert
,
...
@@ -38,17 +43,20 @@ function resolveApplicationUrl(application) {
...
@@ -38,17 +43,20 @@ function resolveApplicationUrl(application) {
function
normalizeApplication
(
application
,
fallback
=
{})
{
function
normalizeApplication
(
application
,
fallback
=
{})
{
const
name
=
application
?.
name
||
fallback
?.
name
||
''
;
const
name
=
application
?.
name
||
fallback
?.
name
||
''
;
const
current
=
name
===
APPLICATION_ROUTE_NAME
;
return
{
return
{
name
,
name
,
title
:
application
?.
title
||
fallback
?.
title
||
application
?.
name
||
fallback
?.
name
||
'未命名应用'
,
title
:
current
?
APPLICATION_DISPLAY_NAME
:
application
?.
title
||
fallback
?.
title
||
application
?.
name
||
fallback
?.
name
||
'未命名应用'
,
description
:
description
:
application
?.
description
||
application
?.
description
||
fallback
?.
description
||
fallback
?.
description
||
fallback
?.
title
||
fallback
?.
title
||
application
?.
title
||
application
?.
title
||
'进入对应应用继续处理业务'
,
'进入对应应用继续处理业务'
,
logo
:
application
?.
logo
||
fallback
?.
logo
||
null
,
logo
:
current
?
xinyuanLogo
:
application
?.
logo
||
fallback
?.
logo
||
null
,
url
:
resolveApplicationUrl
({
url
:
resolveApplicationUrl
({
name
,
name
,
}),
}),
...
@@ -63,7 +71,14 @@ function normalizeApplication(application, fallback = {}) {
...
@@ -63,7 +71,14 @@ function normalizeApplication(application, fallback = {}) {
function
mergeApplications
(
fetchedApplications
,
fallbackApplications
)
{
function
mergeApplications
(
fetchedApplications
,
fallbackApplications
)
{
const
fallbackMap
=
new
Map
(
const
fallbackMap
=
new
Map
(
fallbackApplications
.
map
((
application
)
=>
[
application
.
name
,
application
]),
[
...
fallbackApplications
,
{
name
:
APPLICATION_ROUTE_NAME
,
title
:
APPLICATION_DISPLAY_NAME
,
logo
:
xinyuanLogo
,
},
].
map
((
application
)
=>
[
application
.
name
,
application
]),
);
);
const
normalizedFetched
=
fetchedApplications
const
normalizedFetched
=
fetchedApplications
...
@@ -76,7 +91,7 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
...
@@ -76,7 +91,7 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
normalizedFetched
.
map
((
application
)
=>
[
application
.
name
,
application
]),
normalizedFetched
.
map
((
application
)
=>
[
application
.
name
,
application
]),
);
);
fallback
Applications
.
forEach
((
application
)
=>
{
fallback
Map
.
forEach
((
application
)
=>
{
if
(
!
application
?.
name
||
mergedMap
.
has
(
application
.
name
))
{
if
(
!
application
?.
name
||
mergedMap
.
has
(
application
.
name
))
{
return
;
return
;
}
}
...
@@ -94,7 +109,62 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
...
@@ -94,7 +109,62 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
});
});
}
}
function
buildApplicationSections
(
applications
,
applicationGroups
)
{
if
(
!
Array
.
isArray
(
applicationGroups
)
||
applicationGroups
.
length
===
0
)
{
return
[{
key
:
'all'
,
label
:
'全部应用'
,
applications
}];
}
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
)
=>
{
if
(
groupedApplicationNames
.
has
(
name
))
{
return
null
;
}
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
(
ungroupedApplications
.
length
>
0
)
{
sections
.
push
({
key
:
'ungrouped'
,
label
:
'其他应用'
,
applications
:
ungroupedApplications
,
});
}
return
sections
;
}
function
isCurrentApplication
(
application
)
{
function
isCurrentApplication
(
application
)
{
if
(
application
?.
name
===
APPLICATION_ROUTE_NAME
)
{
return
true
;
}
if
(
typeof
window
===
'undefined'
)
{
if
(
typeof
window
===
'undefined'
)
{
return
false
;
return
false
;
}
}
...
@@ -135,10 +205,10 @@ function ApplicationCard({ application, current }) {
...
@@ -135,10 +205,10 @@ function ApplicationCard({ application, current }) {
>
>
<
ApplicationIdentityWell
initial=
{
initial
}
logo=
{
application
.
logo
}
/>
<
ApplicationIdentityWell
initial=
{
initial
}
logo=
{
application
.
logo
}
/>
<
span
className=
"min-w-0 flex-1"
>
<
span
className=
"min-w-0 flex-1"
>
<
span
className=
"block min-w-0 truncate
font-medium leading-5 text-foreground
"
>
<
span
className=
"block min-w-0 truncate
text-sm font-medium leading-5
"
>
{
application
.
title
}
{
application
.
title
}
</
span
>
</
span
>
<
Description
className=
"
block min-w-0 truncate
"
>
<
Description
className=
"
mt-0.5 block min-w-0 truncate leading-4
"
>
{
application
.
description
||
application
.
name
||
'应用'
}
{
application
.
description
||
application
.
name
||
'应用'
}
</
Description
>
</
Description
>
</
span
>
</
span
>
...
@@ -170,6 +240,7 @@ function LoadingSkeleton() {
...
@@ -170,6 +240,7 @@ function LoadingSkeleton() {
export
default
function
ApplicationSwitcherDialog
({
export
default
function
ApplicationSwitcherDialog
({
open
,
open
,
onClose
,
onClose
,
siteInfo
,
userInfo
,
userInfo
,
})
{
})
{
const
fallbackApplications
=
useMemo
(()
=>
{
const
fallbackApplications
=
useMemo
(()
=>
{
...
@@ -187,14 +258,20 @@ export default function ApplicationSwitcherDialog({
...
@@ -187,14 +258,20 @@ export default function ApplicationSwitcherDialog({
.
filter
((
application
)
=>
application
.
name
);
.
filter
((
application
)
=>
application
.
name
);
},
[
userInfo
]);
},
[
userInfo
]);
const
[
applications
,
setApplications
]
=
useState
(
fallbackApplications
);
const
[
applications
,
setApplications
]
=
useState
(()
=>
mergeApplications
([],
fallbackApplications
),
);
const
[
status
,
setStatus
]
=
useState
(
const
[
status
,
setStatus
]
=
useState
(
fallbackApplications
.
length
>
0
?
'success'
:
'idle'
,
fallbackApplications
.
length
>
0
?
'success'
:
'idle'
,
);
);
const
[
errorMessage
,
setErrorMessage
]
=
useState
(
''
);
const
[
errorMessage
,
setErrorMessage
]
=
useState
(
''
);
const
currentApplicationName
=
useMemo
(()
=>
{
const
currentApplicationName
=
useMemo
(()
=>
{
return
applications
.
find
(
isCurrentApplication
)?.
name
||
''
;
return
applications
.
find
(
isCurrentApplication
)?.
name
||
APPLICATION_ROUTE_NAME
;
},
[
applications
]);
},
[
applications
]);
const
applicationSections
=
useMemo
(
()
=>
buildApplicationSections
(
applications
,
siteInfo
?.
group
),
[
applications
,
siteInfo
?.
group
],
);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
fallbackApplications
.
length
===
0
)
{
if
(
fallbackApplications
.
length
===
0
)
{
...
@@ -241,7 +318,7 @@ export default function ApplicationSwitcherDialog({
...
@@ -241,7 +318,7 @@ export default function ApplicationSwitcherDialog({
return
;
return
;
}
}
setApplications
(
fallbackApplications
);
setApplications
(
mergeApplications
([],
fallbackApplications
)
);
setStatus
(
'error'
);
setStatus
(
'error'
);
setErrorMessage
(
'应用列表加载失败,已回退展示当前账号上的应用信息。'
);
setErrorMessage
(
'应用列表加载失败,已回退展示当前账号上的应用信息。'
);
}
}
...
@@ -264,20 +341,20 @@ export default function ApplicationSwitcherDialog({
...
@@ -264,20 +341,20 @@ export default function ApplicationSwitcherDialog({
}
}
}
}
variant=
"blur"
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=
"rc-dialog-panel rc-dialog-panel--xl max-h-[min(42rem,calc(100dvh-2rem))]"
>
<
Modal
.
Dialog
<
Modal
.
CloseTrigger
aria
-
label=
"关闭"
>
aria
-
label=
"应用切换"
<
Close
size=
{
16
}
/>
className=
"rc-application-switcher-dialog p-7"
</
Modal
.
CloseTrigger
>
>
<
Modal
.
CloseTrigger
aria
-
label=
"关闭"
style=
{
{
insetInlineEnd
:
'1.75rem'
,
top
:
'1.75rem'
}
}
/>
<
Modal
.
Header
>
<
Modal
.
Header
>
<
div
className=
"min-w-0"
>
<
Modal
.
Heading
>
应用切换
</
Modal
.
Heading
>
<
Modal
.
Heading
>
应用切换
</
Modal
.
Heading
>
<
Description
>
<
Description
className=
"leading-5"
>
选择你可访问的应用
</
Description
>
选择你可访问的应用
</
Description
>
</
div
>
</
Modal
.
Header
>
</
Modal
.
Header
>
<
Modal
.
Body
>
<
Modal
.
Body
className=
"mt-3"
>
{
status
===
'error'
&&
errorMessage
?
(
{
status
===
'error'
&&
errorMessage
?
(
<
Alert
status=
"warning"
>
<
Alert
status=
"warning"
>
<
Alert
.
Indicator
><
ExclamationTriangleIcon
size=
{
20
}
/></
Alert
.
Indicator
>
<
Alert
.
Indicator
><
ExclamationTriangleIcon
size=
{
20
}
/></
Alert
.
Indicator
>
...
@@ -288,8 +365,17 @@ export default function ApplicationSwitcherDialog({
...
@@ -288,8 +365,17 @@ export default function ApplicationSwitcherDialog({
{
status
===
'loading'
&&
applications
.
length
===
0
?
(
{
status
===
'loading'
&&
applications
.
length
===
0
?
(
<
LoadingSkeleton
/>
<
LoadingSkeleton
/>
)
:
applications
.
length
>
0
?
(
)
:
applications
.
length
>
0
?
(
<
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"
>
<
div
className=
"grid gap-3 sm:grid-cols-2"
>
{
applications
.
map
((
application
)
=>
(
{
section
.
applications
.
map
((
application
)
=>
(
<
ApplicationCard
<
ApplicationCard
key=
{
application
.
name
}
key=
{
application
.
name
}
application=
{
application
}
application=
{
application
}
...
@@ -297,6 +383,9 @@ export default function ApplicationSwitcherDialog({
...
@@ -297,6 +383,9 @@ export default function ApplicationSwitcherDialog({
/>
/>
))
}
))
}
</
div
>
</
div
>
</
section
>
))
}
</
div
>
)
:
(
)
:
(
<
EmptyState
size=
"md"
>
<
EmptyState
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Header
>
...
...
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