Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
A
audit_project_manager
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_project_manager
Commits
f8a83c57
Commit
f8a83c57
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
239971a9
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
234 additions
and
52 deletions
+234
-52
ui-architecture.md
docs/ui-architecture.md
+5
-0
ui-migration-checklist.md
docs/ui-migration-checklist.md
+6
-2
xinyuan-design-system.md
docs/xinyuan-design-system.md
+5
-2
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+24
-2
DashboardSidebar.jsx
src/components/DashboardSidebar.jsx
+35
-7
ApplicationSwitcherDialog.jsx
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
+154
-39
project-management.business-flow.md
...es/project_manager_xy/project-management.business-flow.md
+5
-0
No files found.
docs/ui-architecture.md
View file @
f8a83c57
...
@@ -14,6 +14,11 @@ Hover/Focus/Selected/Disabled。`src/pages` 下没有页面私有 CSS/Less/SCSS
...
@@ -14,6 +14,11 @@ Hover/Focus/Selected/Disabled。`src/pages` 下没有页面私有 CSS/Less/SCSS
-
`AppShell`
:根主题、Pro Sidebar Provider、Navbar 和 base-aware 页面滚动容器。
-
`AppShell`
:根主题、Pro Sidebar Provider、Navbar 和 base-aware 页面滚动容器。
-
`DashboardSidebar`
/
`DashboardNavbar`
:导航、offcanvas、应用切换、用户菜单与主题动作。
-
`DashboardSidebar`
/
`DashboardNavbar`
:导航、offcanvas、应用切换、用户菜单与主题动作。
移动 Sidebar 直接组合 Pro Sheet,并在真实
`Sheet.Dialog`
上提供可访问名称,继续复用
`useSidebar`
的开关状态和同一份 Sidebar 内容。
-
`ApplicationSwitcherDialog`
:直接组合 HeroUI Modal/Card/Surface/Avatar 与 Pro EmptyState;
从既有
`/application/info`
和用户应用合并可访问集合,再按
`SiteContext`
的
`/info -> site.group`
配置生成有序分组,不拥有接口、权限、会话或跳转逻辑。
-
`DataTable`
:Pro DataGrid 原生选择、排序、固定列、内部滚动、EmptyState、ActionBar。
-
`DataTable`
:Pro DataGrid 原生选择、排序、固定列、内部滚动、EmptyState、ActionBar。
-
`XinyuanAutocomplete`
/
`FilterSelect`
/
`ReferenceSearchSelect`
:当前 HeroUI
-
`XinyuanAutocomplete`
/
`FilterSelect`
/
`ReferenceSearchSelect`
:当前 HeroUI
`selectedKey(s)`
、
`onSelectionChange`
、
`inputValue`
合同。
`selectedKey(s)`
、
`onSelectionChange`
、
`inputValue`
合同。
...
...
docs/ui-migration-checklist.md
View file @
f8a83c57
...
@@ -24,8 +24,11 @@
...
@@ -24,8 +24,11 @@
-
[
x
]
居中登录 Card、鑫元 Logo、标题、统一字段高度和 24px 节奏
-
[
x
]
居中登录 Card、鑫元 Logo、标题、统一字段高度和 24px 节奏
-
[
x
]
登录账号/密码、真实验证码与 token 提交,密码加密、失败刷新、失败反馈和 redirect
-
[
x
]
登录账号/密码、真实验证码与 token 提交,密码加密、失败刷新、失败反馈和 redirect
-
[
x
]
HeroUI Pro Sidebar 真 offcanvas,移动面板与基准同为 80vw
-
[
x
]
HeroUI Pro Sidebar 真 offcanvas,移动面板与基准同为 80vw
-
[
x
]
移动 Sidebar 使用官方 Sheet 结构,真实 Dialog 具备可访问名称且控制台无警告
-
[
x
]
Pro Navbar/Sidebar.Trigger 与 48/32/16px 对称边距
-
[
x
]
Pro Navbar/Sidebar.Trigger 与 48/32/16px 对称边距
-
[
x
]
HeroUI Modal 应用切换,一列/两列和唯一当前状态
-
[
x
]
应用切换同步
`model_analysis`
HeroUI Modal 的 46rem/42rem、28px inset、Body 滚动
-
[
x
]
`/info -> site.group`
动态分组、真实数量、空组过滤、“其他应用/全部应用”回退
-
[
x
]
应用卡移动一列/
`sm+`
两列、36px identity well 和唯一当前状态
-
[
x
]
Dropdown 用户菜单、真实退出、token 清理和 base-aware 跳转
-
[
x
]
Dropdown 用户菜单、真实退出、token 清理和 base-aware 跳转
-
[
x
]
浅深色主题切换、刷新持久化与深色 Portal
-
[
x
]
浅深色主题切换、刷新持久化与深色 Portal
...
@@ -52,7 +55,8 @@
...
@@ -52,7 +55,8 @@
-
[
x
]
768x1024 浅色/深色目标与基准对照
-
[
x
]
768x1024 浅色/深色目标与基准对照
-
[
x
]
390x844 浅色/深色目标与基准对照
-
[
x
]
390x844 浅色/深色目标与基准对照
-
[
]
验证码恢复后的首次加载、点击刷新、失败重载及四视口浅深色对照(本轮浏览器实例不可用,待重验)
-
[
]
验证码恢复后的首次加载、点击刷新、失败重载及四视口浅深色对照(本轮浏览器实例不可用,待重验)
-
[
x
]
登录、退出、Sidebar、Navbar、应用切换和用户菜单
-
[
x
]
应用切换四视口浅/深色与
`model_analysis`
打开态重新验收
-
[
x
]
登录、退出、Sidebar、Navbar 和用户菜单
-
[
x
]
Tabs、筛选展开/收起、Dropdown/ListBox、DatePicker、Tooltip
-
[
x
]
Tabs、筛选展开/收起、Dropdown/ListBox、DatePicker、Tooltip
-
[
x
]
三处活动 Tabs 均显式使用 HeroUI
`primary`
胶囊轨道,按文字收缩且窄屏由官方容器滚动
-
[
x
]
三处活动 Tabs 均显式使用 HeroUI
`primary`
胶囊轨道,按文字收缩且窄屏由官方容器滚动
-
[
]
页面数据集、详情阶段与动态表单分组的切换业务回调未变,本轮浏览器对照待补
-
[
]
页面数据集、详情阶段与动态表单分组的切换业务回调未变,本轮浏览器对照待补
...
...
docs/xinyuan-design-system.md
View file @
f8a83c57
...
@@ -24,8 +24,11 @@ Inter 与系统回退字体之间切换。
...
@@ -24,8 +24,11 @@ Inter 与系统回退字体之间切换。
-
HeroUI Pro Navbar 高 64px,直接使用
`Sidebar.Trigger`
;Navbar 与页面共享桌面/平板/
-
HeroUI Pro Navbar 高 64px,直接使用
`Sidebar.Trigger`
;Navbar 与页面共享桌面/平板/
手机 48/32/16px 对称 gutter。
手机 48/32/16px 对称 gutter。
-
Sidebar 用户入口必须使用
`Dropdown.Trigger`
,退出调用既有 auth service 和 base-aware 路由。
-
Sidebar 用户入口必须使用
`Dropdown.Trigger`
,退出调用既有 auth service 和 base-aware 路由。
-
应用切换使用 HeroUI Modal:46rem/42rem 几何,移动一列、
`sm`
起始终两列;每个 Logo
-
应用切换以
`model_analysis`
为唯一基准,使用 HeroUI Modal:46rem/42rem 几何、28px
使用 36px
`Surface tertiary + Avatar soft`
identity well,只允许一个当前应用。
inset、24px 官方关闭控件,移动一列、
`sm`
起始终两列;每个 Logo 使用 36px
`Surface tertiary + Avatar soft`
identity well,只允许一个当前应用。
-
应用分组读取
`/info -> site.group`
,按配置和账号可访问应用的真实顺序匹配,隐藏空组,
显示真实数量,未引用应用进入“其他应用”,无分组配置时使用“全部应用”。
-
根主题写入
`xinyuan_color_theme`
,所有 Portal、Sidebar.Mobile、ActionBar 和图表 Tooltip
-
根主题写入
`xinyuan_color_theme`
,所有 Portal、Sidebar.Mobile、ActionBar 和图表 Tooltip
继承同一根主题。
继承同一根主题。
...
...
docs/xinyuan-ui-audit.md
View file @
f8a83c57
...
@@ -7,7 +7,8 @@
...
@@ -7,7 +7,8 @@
-
分支:
`codex/ui-ux-xinyuan-migration`
-
分支:
`codex/ui-ux-xinyuan-migration`
-
唯一实现基准:
`problem_rectification`
-
唯一实现基准:
`problem_rectification`
-
应用基路径:
`/audit_project_manager_new/`
-
应用基路径:
`/audit_project_manager_new/`
-
目标验收地址:
`http://127.0.0.1:8014/audit_project_manager_new/`
-
本轮启动命令:
`npm run dev -- --port 8014`
(Umi 自动选择实际空闲端口
`8005`
)
-
目标验收地址:
`http://127.0.0.1:8005/audit_project_manager_new/`
-
基准验收地址:
`http://127.0.0.1:8006/problem_rectification_new/problem_rectification`
-
基准验收地址:
`http://127.0.0.1:8006/problem_rectification_new/problem_rectification`
迁移前已启动未修改项目并记录业务基线。原登录页为蓝色营销式左右分栏,Shell、表格、
迁移前已启动未修改项目并记录业务基线。原登录页为蓝色营销式左右分栏,Shell、表格、
...
@@ -64,6 +65,9 @@ redirect 与失败反馈保持原流程。详情查看/编辑/取消期间资源
...
@@ -64,6 +65,9 @@ redirect 与失败反馈保持原流程。详情查看/编辑/取消期间资源
-
登录:居中 Card、鑫元 Logo、统一字段高度、24px 字段节奏、filled/autofill 与真实验证码加载/刷新/提交。
-
登录:居中 Card、鑫元 Logo、统一字段高度、24px 字段节奏、filled/autofill 与真实验证码加载/刷新/提交。
-
Shell:HeroUI Pro Sidebar/Navbar、offcanvas 真折叠、应用切换、主题、用户菜单和真实退出。
-
Shell:HeroUI Pro Sidebar/Navbar、offcanvas 真折叠、应用切换、主题、用户菜单和真实退出。
-
应用切换已按
`model_analysis`
基准同步 HeroUI Modal 结构和固定几何;保留既有
`/application/info`
、用户应用合并、鉴权和跳转,新增消费已有
`/info -> site.group`
配置,
按真实权限和后端顺序显示非空分组、数量及“其他应用/全部应用”回退。
-
页面:HeroUI Tabs、Surface、Autocomplete、DatePicker、Disclosure、DropZone。
-
页面:HeroUI Tabs、Surface、Autocomplete、DatePicker、Disclosure、DropZone。
-
列表:HeroUI Pro DataGrid 原生选择/排序/固定列/内部横向滚动、Pagination、ActionBar、
-
列表:HeroUI Pro DataGrid 原生选择/排序/固定列/内部横向滚动、Pagination、ActionBar、
EmptyState;固定每页 10 条,无页容量控件。
EmptyState;固定每页 10 条,无页容量控件。
...
@@ -103,7 +107,8 @@ redirect 与失败反馈保持原流程。详情查看/编辑/取消期间资源
...
@@ -103,7 +107,8 @@ redirect 与失败反馈保持原流程。详情查看/编辑/取消期间资源
-
移动 Sidebar:目标与基准均为 HeroUI Pro 官方
`80vw`
,390px 视口下为
`312x844`
,
-
移动 Sidebar:目标与基准均为 HeroUI Pro 官方
`80vw`
,390px 视口下为
`312x844`
,
Sheet/Dialog/Sidebar 内容同宽且无空白条;桌面折叠后为零宽。
Sheet/Dialog/Sidebar 内容同宽且无空白条;桌面折叠后为零宽。
-
应用切换:移动端 358px 单列,桌面 736px 两列,只有一个“当前”。
-
应用切换:移动端 358px 单列,桌面 736px 两列,只有一个“当前”;本次动态分组同步后的
四视口浅/深色打开态将重新验收,不复用旧平铺版本结论。
-
Modal:移动端 4xl 为
`358x812`
,桌面 4xl 为 1088px;查看态 1 个 secondary Surface、
-
Modal:移动端 4xl 为
`358x812`
,桌面 4xl 为 1088px;查看态 1 个 secondary Surface、
0 个输入,编辑态 0 个组 Surface、7 个官方控件,取消后恢复原查看态。
0 个输入,编辑态 0 个组 Surface、7 个官方控件,取消后恢复原查看态。
-
DataGrid:滚动层
`clientWidth=1096`
、
`scrollWidth=1624`
、最大滚动 528px;首列与操作列
-
DataGrid:滚动层
`clientWidth=1096`
、
`scrollWidth=1624`
、最大滚动 528px;首列与操作列
...
@@ -114,6 +119,23 @@ redirect 与失败反馈保持原流程。详情查看/编辑/取消期间资源
...
@@ -114,6 +119,23 @@ redirect 与失败反馈保持原流程。详情查看/编辑/取消期间资源
-
Checkbox 选中/半选、ActionBar、Pagination、Tabs、筛选展开态、Dropdown、用户菜单、
-
Checkbox 选中/半选、ActionBar、Pagination、Tabs、筛选展开态、Dropdown、用户菜单、
退出和删除取消均通过。目标控制台最终为
`0 errors / 0 warnings`
。
退出和删除取消均通过。目标控制台最终为
`0 errors / 0 warnings`
。
### 2026-08-26 应用切换同步复核
-
以
`model_analysis`
当前运行实现为唯一基准,目标和基准使用同一浏览器会话数据,在
`1440x900`
、
`1280x720`
、
`768x1024`
、
`390x844`
的浅色与深色模式逐项对照。
-
两者桌面/平板弹窗均为 736px 宽,390px 视口均为
`358px`
宽且左右各 16px;计算圆角
`24px`
、padding
`28px`
、关闭控件
`24px`
,关闭控件与顶部/右侧均相距 28px。
-
`sm`
起两列计算宽度均为
`334px 334px`
,390px 为单列
`302px`
;浅深色均无页面横向
溢出,深色 Dialog/Card/identity well 计算色值与基准逐项一致。
-
动态分组场景显示“演示 3 个应用”和“其他应用 1 个应用”,空分组被过滤且当前状态唯一;
无分组配置回退为“全部应用”。17 个应用的移动端长列表中,Modal Body 为
`clientHeight=548`
、
`scrollHeight=1508`
,只有 Body 滚动,全部 identity well 为 36x36px。
-
Escape 可关闭弹窗并把焦点还给触发按钮;目标应用切换本身控制台为 0 error / 0 warning。
验收响应只存在于临时 Playwright 会话,未写入源码、文档数据或后端。
-
移动端入口将 HeroUI Pro
`Sidebar.Mobile`
的等价 Sheet compound 结构显式展开,只为在真实
`Sheet.Dialog`
上提供“审计项目管理导航”可访问名称;390x844 下 Sidebar 仍为 312x844,
开关状态、遮罩、位置、内容和应用切换入口未变,控制台复验为 0 error / 0 warning。
### 2026-08-21 筛选模块复核
### 2026-08-21 筛选模块复核
-
活动路由的筛选入口统一收敛到
`RecordWorkbench`
:关键词使用列表工具栏的 HeroUI
-
活动路由的筛选入口统一收敛到
`RecordWorkbench`
:关键词使用列表工具栏的 HeroUI
...
...
src/components/DashboardSidebar.jsx
View file @
f8a83c57
import
xyLogoRed
from
'@/assets/xy-logo-red.png'
;
import
xyLogoRed
from
'@/assets/xy-logo-red.png'
;
import
xyLogoWhite
from
'@/assets/xy-logo-white.png'
;
import
xyLogoWhite
from
'@/assets/xy-logo-white.png'
;
import
{
APPLICATION_DISPLAY_NAME
}
from
'@/config/application'
;
import
{
APPLICATION_DISPLAY_NAME
}
from
'@/config/application'
;
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Sheet
}
from
'@heroui-pro/react'
;
import
{
Sidebar
,
useSidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Avatar
,
Button
,
Dropdown
,
Label
,
Tooltip
}
from
'@heroui/react'
;
import
{
Avatar
,
Button
,
Dropdown
,
Label
,
Tooltip
}
from
'@heroui/react'
;
import
{
AppGrid
,
LogOut
}
from
'./AppIcons'
;
import
{
AppGrid
,
LogOut
}
from
'./AppIcons'
;
...
@@ -147,18 +148,45 @@ function SidebarContents({
...
@@ -147,18 +148,45 @@ function SidebarContents({
);
);
}
}
function
AccessibleMobileSidebar
(
props
)
{
const
{
isMobile
,
isMobileOpen
,
setMobileOpen
,
side
}
=
useSidebar
();
if
(
!
isMobile
)
{
return
null
;
}
return
(
<
Sheet
.
Root
isOpen=
{
isMobileOpen
}
placement=
{
side
}
onOpenChange=
{
setMobileOpen
}
>
<
Sheet
.
Backdrop
variant=
"blur"
>
<
Sheet
.
Content
className=
"sidebar__mobile-sheet"
>
<
Sheet
.
Dialog
aria
-
label=
{
`${APPLICATION_DISPLAY_NAME}导航`
}
className=
"sidebar__mobile-dialog"
>
<
div
className=
"sidebar__mobile app-sidebar-glass rc-mobile-sidebar"
data
-
slot=
"sidebar-mobile"
>
<
SidebarContents
{
...
props
}
/>
</
div
>
</
Sheet
.
Dialog
>
</
Sheet
.
Content
>
</
Sheet
.
Backdrop
>
</
Sheet
.
Root
>
);
}
export
default
function
DashboardSidebar
(
props
)
{
export
default
function
DashboardSidebar
(
props
)
{
return
(
return
(
<>
<>
<
Sidebar
className=
"app-sidebar-glass rc-sidebar"
>
<
Sidebar
className=
"app-sidebar-glass rc-sidebar"
>
<
SidebarContents
{
...
props
}
/>
<
SidebarContents
{
...
props
}
/>
</
Sidebar
>
</
Sidebar
>
<
Sidebar
.
Mobile
className=
"app-sidebar-glass rc-mobile-sidebar"
>
<
AccessibleMobileSidebar
{
...
props
}
/>
<
h2
slot=
"title"
className=
"sr-only"
>
{
APPLICATION_DISPLAY_NAME
}
导航
</
h2
>
<
SidebarContents
{
...
props
}
/>
</
Sidebar
.
Mobile
>
</>
</>
);
);
}
}
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
View file @
f8a83c57
import
xinyuanLogo
from
'@/assets/xy-logo-red.png'
;
import
{
APPLICATION_BASE_PATH
,
APPLICATION_DISPLAY_NAME
,
}
from
'@/config/application'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
Close
,
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
import
{
Alert
,
Alert
,
...
@@ -13,6 +18,23 @@ import {
...
@@ -13,6 +18,23 @@ import {
}
from
'@heroui/react'
;
}
from
'@heroui/react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
const
CURRENT_APPLICATION_NAME
=
APPLICATION_BASE_PATH
.
replace
(
/^
\/
+|
\/
+$/g
,
''
);
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
normalizeLabelList
(
items
)
{
function
normalizeLabelList
(
items
)
{
if
(
!
Array
.
isArray
(
items
))
{
if
(
!
Array
.
isArray
(
items
))
{
return
[];
return
[];
...
@@ -39,20 +61,27 @@ function resolveApplicationUrl(application) {
...
@@ -39,20 +61,27 @@ 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
===
CURRENT_APPLICATION_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
url
:
resolveApplicationUrl
({
?
xinyuanLogo
name
,
:
application
?.
logo
||
}),
application
?.
icon
||
fallback
?.
logo
||
fallback
?.
icon
||
null
,
url
:
resolveApplicationUrl
({
name
}),
access
:
application
?.
access
||
fallback
?.
access
||
''
,
access
:
application
?.
access
||
fallback
?.
access
||
''
,
status
:
application
?.
status
||
fallback
?.
status
||
''
,
status
:
application
?.
status
||
fallback
?.
status
||
''
,
type
:
application
?.
type
||
fallback
?.
type
||
''
,
type
:
application
?.
type
||
fallback
?.
type
||
''
,
...
@@ -85,6 +114,15 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
...
@@ -85,6 +114,15 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
mergedMap
.
set
(
application
.
name
,
normalizeApplication
(
application
,
application
));
mergedMap
.
set
(
application
.
name
,
normalizeApplication
(
application
,
application
));
});
});
const
currentApplication
=
mergedMap
.
get
(
CURRENT_APPLICATION_NAME
);
mergedMap
.
set
(
CURRENT_APPLICATION_NAME
,
normalizeApplication
(
{
name
:
CURRENT_APPLICATION_NAME
},
currentApplication
||
{
title
:
APPLICATION_DISPLAY_NAME
},
),
);
return
Array
.
from
(
mergedMap
.
values
()).
sort
((
left
,
right
)
=>
{
return
Array
.
from
(
mergedMap
.
values
()).
sort
((
left
,
right
)
=>
{
const
orderDifference
=
Number
(
left
.
order
||
0
)
-
Number
(
right
.
order
||
0
);
const
orderDifference
=
Number
(
left
.
order
||
0
)
-
Number
(
right
.
order
||
0
);
if
(
orderDifference
!==
0
)
{
if
(
orderDifference
!==
0
)
{
...
@@ -95,7 +133,62 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
...
@@ -95,7 +133,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
===
CURRENT_APPLICATION_NAME
)
{
return
true
;
}
if
(
typeof
window
===
'undefined'
)
{
if
(
typeof
window
===
'undefined'
)
{
return
false
;
return
false
;
}
}
...
@@ -134,29 +227,35 @@ function ApplicationCard({ application, current }) {
...
@@ -134,29 +227,35 @@ function ApplicationCard({ application, current }) {
aria
-
current=
{
current
?
'page'
:
undefined
}
aria
-
current=
{
current
?
'page'
:
undefined
}
className=
"flex min-w-0 items-center gap-3 no-underline"
className=
"flex min-w-0 items-center gap-3 no-underline"
>
>
<
Surface
<
Surface
aria
-
hidden=
"true"
variant=
"tertiary"
className=
"rc-application-identity-well"
>
aria
-
hidden=
"true"
className=
"rc-application-identity-well"
variant=
"tertiary"
>
<
Avatar
<
Avatar
className=
"rc-application-identity-avatar"
className=
"rc-application-identity-avatar"
color=
"accent"
color=
"accent"
variant=
"soft"
variant=
"soft"
>
>
{
application
.
logo
?
(
{
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
}
)
:
null
}
<
Avatar
.
Fallback
>
{
initial
}
</
Avatar
.
Fallback
>
<
Avatar
.
Fallback
>
{
initial
}
</
Avatar
.
Fallback
>
</
Avatar
>
</
Avatar
>
</
Surface
>
</
Surface
>
<
span
className=
"min-w-0 flex-1"
>
<
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"
title=
{
application
.
title
}
>
{
application
.
title
}
{
application
.
title
}
</
span
>
</
span
>
<
span
className=
"block truncate text-sm text-muted"
title=
{
application
.
description
}
>
<
Description
className=
"mt-0.5 block truncate leading-4"
title=
{
application
.
description
}
>
{
application
.
description
||
application
.
name
||
'应用'
}
{
application
.
description
||
application
.
name
||
'应用'
}
</
spa
n
>
</
Descriptio
n
>
</
span
>
</
span
>
{
current
?
<
Chip
color=
"accent"
size=
"sm"
>
当前
</
Chip
>
:
null
}
{
current
?
<
Chip
color=
"accent"
size=
"sm"
>
当前
</
Chip
>
:
null
}
</
a
>
</
a
>
...
@@ -186,6 +285,7 @@ function LoadingSkeleton() {
...
@@ -186,6 +285,7 @@ function LoadingSkeleton() {
export
default
function
ApplicationSwitcherDialog
({
export
default
function
ApplicationSwitcherDialog
({
open
,
open
,
onClose
,
onClose
,
siteInfo
,
userInfo
,
userInfo
,
})
{
})
{
const
fallbackApplications
=
useMemo
(()
=>
{
const
fallbackApplications
=
useMemo
(()
=>
{
...
@@ -203,27 +303,29 @@ export default function ApplicationSwitcherDialog({
...
@@ -203,27 +303,29 @@ 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
||
CURRENT_APPLICATION_NAME
;
},
[
applications
]);
},
[
applications
]);
const
applicationSections
=
useMemo
(
()
=>
buildApplicationSections
(
applications
,
siteInfo
?.
group
),
[
applications
,
siteInfo
?.
group
],
);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
fallbackApplications
.
length
===
0
)
{
if
(
fallbackApplications
.
length
===
0
)
{
return
;
return
;
}
}
setApplications
((
previous
)
=>
{
setApplications
((
previous
)
=>
if
(
previous
.
length
>
0
)
{
mergeApplications
(
previous
,
fallbackApplications
),
return
previous
;
);
}
return
fallbackApplications
;
});
},
[
fallbackApplications
]);
},
[
fallbackApplications
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
...
@@ -239,7 +341,7 @@ export default function ApplicationSwitcherDialog({
...
@@ -239,7 +341,7 @@ export default function ApplicationSwitcherDialog({
try
{
try
{
const
response
=
await
getApplications
();
const
response
=
await
getApplications
();
const
fetchedApplications
=
Array
.
isArray
(
response
)
?
response
:
[]
;
const
fetchedApplications
=
unwrapApplications
(
response
)
;
if
(
cancelled
)
{
if
(
cancelled
)
{
return
;
return
;
...
@@ -257,7 +359,7 @@ export default function ApplicationSwitcherDialog({
...
@@ -257,7 +359,7 @@ export default function ApplicationSwitcherDialog({
return
;
return
;
}
}
setApplications
(
fallbackApplications
);
setApplications
(
mergeApplications
([],
fallbackApplications
)
);
setStatus
(
'error'
);
setStatus
(
'error'
);
setErrorMessage
(
'应用列表加载失败,已回退展示当前账号上的应用信息。'
);
setErrorMessage
(
'应用列表加载失败,已回退展示当前账号上的应用信息。'
);
}
}
...
@@ -280,23 +382,24 @@ export default function ApplicationSwitcherDialog({
...
@@ -280,23 +382,24 @@ 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
<
Modal
.
Dialog
className=
"max-h-[min(42rem,calc(100dvh-2rem))]"
aria
-
label=
"应用切换"
style=
{
{
maxWidth
:
'min(46rem, calc(100vw - 2rem))'
}
}
className=
"max-h-[min(42rem,calc(100dvh-2rem))] p-7"
style=
{
{
maxWidth
:
'min(46rem, calc(100vw - 2rem))'
,
width
:
'min(46rem, calc(100vw - 2rem))'
,
}
}
>
>
<
Modal
.
CloseTrigger
aria
-
label=
"关闭"
>
<
Modal
.
CloseTrigger
<
Close
size=
{
16
}
/>
aria
-
label=
"关闭"
</
Modal
.
CloseTrigger
>
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
>
<
div
className=
"text-sm text-muted"
>
选择你可访问的应用
</
div
>
选择你可访问的应用
</
Description
>
</
div
>
</
Modal
.
Header
>
</
Modal
.
Header
>
<
Modal
.
Body
>
<
Modal
.
Body
style=
{
{
marginTop
:
'0.75rem'
}
}
>
{
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
>
...
@@ -307,8 +410,17 @@ export default function ApplicationSwitcherDialog({
...
@@ -307,8 +410,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
}
...
@@ -316,6 +428,9 @@ export default function ApplicationSwitcherDialog({
...
@@ -316,6 +428,9 @@ export default function ApplicationSwitcherDialog({
/>
/>
))
}
))
}
</
div
>
</
div
>
</
section
>
))
}
</
div
>
)
:
(
)
:
(
<
EmptyState
size=
"md"
>
<
EmptyState
size=
"md"
>
<
EmptyState
.
Header
>
<
EmptyState
.
Header
>
...
...
src/pages/project_manager_xy/project-management.business-flow.md
View file @
f8a83c57
...
@@ -16,6 +16,11 @@
...
@@ -16,6 +16,11 @@
配置中的
`audit_indicator`
、
`risk_indicator`
、
`audit_process`
、
`audit_report`
配置中的
`audit_indicator`
、
`risk_indicator`
、
`audit_process`
、
`audit_report`
没有菜单入口,属于复制配置残留,不注册为本应用路由。
没有菜单入口,属于复制配置残留,不注册为本应用路由。
侧栏“切换应用”打开 HeroUI Modal。可访问应用仍由既有
`/application/info`
与当前用户
`applications`
合并,分组读取
`/info -> site.group`
的
`{ label, applications }`
配置;
按后端分组及成员顺序过滤无权限和空分组,未被配置引用的可访问应用归入“其他应用”,
无分组配置时显示“全部应用”。应用点击仍使用既有名称到根路径的跳转规则。
侧栏将“驾驶舱”作为独立入口;“审计项目管理”和“往期项目归档”归入可展开的“开展审计工作”,“项目表单”归入可展开的“表单配置及维护”。两个分组默认展开,并支持手动收起。模块主题色与
`problem_rectification`
保持一致,使用品牌红
`#b42318`
统一导航选中态、主按钮、复选框、当前视图和焦点反馈。
侧栏将“驾驶舱”作为独立入口;“审计项目管理”和“往期项目归档”归入可展开的“开展审计工作”,“项目表单”归入可展开的“表单配置及维护”。两个分组默认展开,并支持手动收起。模块主题色与
`problem_rectification`
保持一致,使用品牌红
`#b42318`
统一导航选中态、主按钮、复选框、当前视图和焦点反馈。
## 2. 项目列表与详情
## 2. 项目列表与详情
...
...
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