Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
P
project_manager_xy
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
project_manager_xy
Commits
15a1e978
Commit
15a1e978
authored
Aug 26, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 对齐应用切换弹窗设计规范
parent
73208caf
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
129 additions
and
56 deletions
+129
-56
ui-architecture.md
docs/ui-architecture.md
+2
-2
ui-migration-checklist.md
docs/ui-migration-checklist.md
+3
-2
xinyuan-design-system.md
docs/xinyuan-design-system.md
+2
-2
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+3
-2
XinyuanModal.jsx
src/components/XinyuanModal.jsx
+15
-8
application.js
src/config/application.js
+1
-0
shell.css
src/design-system/shell.css
+6
-0
ApplicationSwitcherDialog.jsx
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
+96
-39
project-management.business-flow.md
...es/project_manager_xy/project-management.business-flow.md
+1
-1
No files found.
docs/ui-architecture.md
View file @
15a1e978
# UI 架构
更新日期:2026-08-2
0
更新日期:2026-08-2
6
## 目标边界
...
...
@@ -76,7 +76,7 @@ Umi routes
-
Autocomplete 统一在共享 HeroUI-native 组合中使用当前
`react-stately`
Select 的受控
`value/onChange`
契约:单选值为 ID 或
`null`
,多选值为 ID 数组;官方 ClearButton、触发器显示和选项勾选均由该值驱动。远程用户字段只在业务边界输出原有 ID、
`''`
、ID 数组或
`[]`
。
`selectedKeys/onSelectionChange`
仅用于 DataGrid 等集合选择,不用于 Autocomplete 根。
-
根
`html`
同时携带 class 与
`data-theme`
,Portal 从根继承主题。
-
查看态详情使用单一 L2 Surface;编辑态 controls 直接位于 Overlay 并使用
`secondary`
。
-
应用切换保留目标的应用 API、归一化和导航 callback
,只替换为批准的 Modal/identity
composition。
-
应用切换保留目标的应用 API、归一化和导航 callback
;
`ApplicationSwitcherDialog`
解包现有响应形态,按
`siteInfo.group`
以应用 name 匹配真实可访问应用,保持后端组序/组内序,隐藏空组并将未引用应用归入“其他应用”,无配置时回退“全部应用”。外层使用批准的
`XinyuanModal`
/HeroUI Modal compound
composition。
-
移动导航复用 HeroUI Pro Sidebar Provider 的
`isMobileOpen/setMobileOpen`
状态,并以同包 Sheet compound 建立具名 Dialog,规避当前 Sidebar.Mobile 未向 Dialog 根透传可访问名称的问题。
## 删除顺序
...
...
docs/ui-migration-checklist.md
View file @
15a1e978
# 鑫元 UI 迁移清单
更新日期:2026-08-2
0
更新日期:2026-08-2
6
状态说明:
`[ ]`
待处理,
`[x]`
已完成,
`[-]`
不适用或受外部条件阻塞。
...
...
@@ -32,7 +32,7 @@
-
[
x
]
Navbar 使用 Pro 默认 Sidebar.Trigger。
-
[
x
]
页面与 Navbar 对齐 48/32/16px 对称 gutter。
-
[
x
]
用户入口使用 Dropdown.Trigger,真实退出可用且 base-aware。
-
[
x
]
应用切换使用 HeroUI Modal、固定两列和 36px identity well。
-
[
x
]
应用切换使用 HeroUI Modal、
动态站点分组、移动单列/sm 起
固定两列和 36px identity well。
-
[
x
]
主题在刷新和路由切换后保持,全部 Portal 跟随根主题。
## 资源统计
...
...
@@ -92,6 +92,7 @@
-
目标与问题整改在四个规定视口、浅色和深色下同时打开比较;两端八组视口均无页面级横向溢出。
-
已打开并操作用户 Dropdown、应用切换 Modal、移动 Sheet、筛选 Disclosure、枚举与远程用户 Autocomplete、DatePicker、项目/周进度/导入 Modal、删除 AlertDialog、DataGrid、Pagination、ActionBar 和图表 Tooltip。
-
应用切换专项复验:1440x900 与
`model_analysis`
几何均为 736x484、24px 圆角、28px inset、24px 关闭按钮偏移 28px;390x844 为 x=16、width=358、单列且页面横向溢出 0;浅色/深色 Portal 控制台均 0 error/0 warning。
-
选择器专项回归:项目负责人和项目协作人均通过连续双选、重新打开保留勾选、单项取消、触发器即时回显及官方 ClearButton 全部清空;筛选状态/负责人及表单枚举通过选择回显、清除和实时查询。桌面浅色与 390x844 深色均通过,移动弹层无横向溢出。
-
退出菜单实际跳转到
`/project_manager_xy/login`
;取消周进度与删除确认不会触发保存/删除请求。
-
最后一次目标页面重载与退出验收均为 0 error、0 warning。
...
...
docs/xinyuan-design-system.md
View file @
15a1e978
# 部门任务管理鑫元设计系统
更新日期:2026-08-2
0
更新日期:2026-08-2
6
本项目直接继承鑫元 HeroUI 设计系统,以“问题整改”运行结果为唯一结构、尺寸、间距和交互基准。业务页面不得建立第二套视觉语言。
...
...
@@ -43,7 +43,7 @@
## 组件合同
-
登录:约 28rem 卡片、36px Logo、卡内品牌、24px 字段组节奏、40px 字段、46px提交按钮。
-
应用切换:46rem/42rem,
手机一列、sm 起固定两列,36px
`Surface tertiary + Avatar soft`
identity well
。
-
应用切换:46rem/42rem,
24px 圆角与 28px inset,手机一列、sm 起固定两列,36px
`Surface tertiary + Avatar soft`
identity well;分组来自
`site.group`
,按后端顺序渲染并将未引用应用归入“其他应用”
。
-
筛选:1-2 个字段不加容器;3 个以上使用
`Surface default rounded-3xl p-6`
,前三项常用字段,其余放 HeroUI Disclosure。
-
列表工具栏:搜索在左、列分组紧随、业务操作在右、主操作最右;手机搜索先独占一行。
-
DataGrid:原生选择/排序/固定列/滚动;20px 外圆角;固定每页 10 条;Pagination 与 DataGrid 间距 8px。
...
...
docs/xinyuan-ui-audit.md
View file @
15a1e978
# 鑫元 UI 迁移审计
更新日期:2026-08-2
0
更新日期:2026-08-2
6
## 范围与基准
...
...
@@ -48,7 +48,7 @@
| 鉴权 |
`Auth.jsx`
拉取用户并清理登录态 | 仅修正 base-aware 导航,不改调用与时机 |
| Sidebar | HeroUI Pro Sidebar,offcanvas 已启用 | 统一实际基准几何、单组标签、默认 Trigger、用户 Dropdown.Trigger |
| Navbar | HeroUI Pro Navbar | 对齐 48/32/16px gutter 和真实 Sidebar.Trigger |
| 应用切换 |
旧 Dialog 包装 + 三列 ItemCard | 直接 HeroUI Modal,
固定两列、36px identity well |
| 应用切换 |
平铺应用列表,未读取站点分组 | 基于
`GET /info -> site.group`
的 HeroUI Modal 分组,移动单列、sm 起
固定两列、36px identity well |
| 主题 | 根
`.light/.dark`
+ localStorage | 保留键与行为,改用权威 theme.css 驱动全部 Portal |
| 退出 | 现有
`userLogout`
+ clearUser | 保留真实回调,修正 base-aware 登录跳转 |
...
...
@@ -78,6 +78,7 @@
-
Dropdown、Tooltip、Popover、Modal、DatePicker、ActionBar 均需在 body Portal 下复核根主题。
-
Dropdown、Tooltip、Modal、Autocomplete、DatePicker、ActionBar 和图表 Tooltip 均通过根
`.light/.dark`
与
`data-theme`
驱动。
-
已验证远程用户 Autocomplete 打开/加载/选择、DatePicker Calendar、DataGrid Checkbox、Pagination、ActionBar、应用切换和用户菜单。
-
应用切换已同步
`model_analysis`
基准:按站点配置保持分组和组内顺序,空组隐藏,未分组应用归入“其他应用”,无配置回退“全部应用”;真实浏览器验证了分组数量、唯一当前 Chip、关闭/焦点与响应式几何。
-
390px 页首 Tabs 产生的 max-content 溢出已通过显式单列收缩边界修复;业务操作在手机视口内保持可达。
-
Pro
`Sidebar.Mobile`
当前版本未把可访问名称传给 Dialog 根,已改用同包 Sheet compound 与 Sidebar 状态组合,移动抽屉控制台零警告。
...
...
src/components/XinyuanModal.jsx
View file @
15a1e978
import
{
Close
}
from
'@/components/AppIcons'
;
import
{
Modal
}
from
'@heroui/react'
;
import
clsx
from
'clsx'
;
...
...
@@ -13,6 +12,7 @@ const DIALOG_WIDTH_CLASS = {
export
default
function
XinyuanModal
({
isOpen
,
onOpenChange
,
isDismissable
,
title
,
description
,
size
=
'lg'
,
...
...
@@ -26,29 +26,36 @@ export default function XinyuanModal({
return
(
<
Modal
.
Root
>
<
Modal
.
Trigger
aria
-
hidden=
"true"
className=
"sr-only"
tabIndex=
{
-
1
}
/>
<
Modal
.
Backdrop
isOpen=
{
isOpen
}
onOpenChange=
{
onOpenChange
}
variant=
"blur"
>
<
Modal
.
Backdrop
isDismissable=
{
isDismissable
}
isOpen=
{
isOpen
}
variant=
"blur"
onOpenChange=
{
onOpenChange
}
>
<
Modal
.
Container
placement=
"center"
scroll=
"inside"
size=
{
modalSize
}
>
<
Modal
.
Dialog
aria
-
label=
{
title
}
className=
{
clsx
(
DIALOG_WIDTH_CLASS
[
size
]
&&
'
p-7'
,
'rc-modal-dialog
p-7'
,
DIALOG_WIDTH_CLASS
[
size
],
dialogClassName
,
)
}
>
<
Modal
.
CloseTrigger
aria
-
label=
"关闭"
>
<
Close
size=
{
16
}
/>
</
Modal
.
CloseTrigger
>
<
Modal
.
CloseTrigger
aria
-
label=
"关闭"
style=
{
{
insetInlineEnd
:
'1.75rem'
,
top
:
'1.75rem'
}
}
/>
<
Modal
.
Header
>
<
Modal
.
Heading
>
{
title
}
</
Modal
.
Heading
>
{
description
?
(
<
div
className=
"text-sm text-muted"
>
{
description
}
</
div
>
)
:
null
}
</
Modal
.
Header
>
<
Modal
.
Body
className=
{
bodyClassName
}
>
<
Modal
.
Body
className=
{
bodyClassName
}
style=
{
{
marginTop
:
'0.75rem'
}
}
>
{
children
}
</
Modal
.
Body
>
{
footer
?
(
<
Modal
.
Footer
>
{
footer
}
</
Modal
.
Footer
>
<
Modal
.
Footer
style=
{
{
marginTop
:
'1.5rem'
}
}
>
{
footer
}
</
Modal
.
Footer
>
)
:
null
}
</
Modal
.
Dialog
>
</
Modal
.
Container
>
...
...
src/config/application.js
View file @
15a1e978
export
const
APPLICATION_DISPLAY_NAME
=
'部门任务管理'
;
export
const
APPLICATION_BASE_PATH
=
'/project_manager_xy/'
;
export
const
APPLICATION_ROUTE_NAME
=
APPLICATION_BASE_PATH
.
replace
(
/^
\/
+|
\/
+$/g
,
''
);
export
const
THEME_STORAGE_KEY
=
'xinyuan_color_theme'
;
export
const
ROUTE_TITLES
=
Object
.
freeze
({
...
...
src/design-system/shell.css
View file @
15a1e978
...
...
@@ -526,6 +526,12 @@
background
:
transparent
;
}
.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
));
}
/* HeroUI Pro KPI.Icon keeps its compound role; this class supplies Xinyuan tones. */
.rc-metric-icon-surface
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-neutral
);
...
...
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
View file @
15a1e978
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
Close
,
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
{
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
xyLogoRed
from
'@/assets/xy-logo-red.png'
;
import
{
APPLICATION_ROUTE_NAME
}
from
'@/config/application'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
...
...
@@ -7,12 +10,26 @@ import {
Card
,
Chip
,
Description
,
Modal
,
Skeleton
,
Surface
,
}
from
'@heroui/react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
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
)
{
if
(
!
Array
.
isArray
(
items
))
{
return
[];
...
...
@@ -49,7 +66,10 @@ function normalizeApplication(application, fallback = {}) {
fallback
?.
title
||
application
?.
title
||
'进入对应应用继续处理业务'
,
logo
:
application
?.
logo
||
fallback
?.
logo
||
null
,
logo
:
application
?.
logo
||
fallback
?.
logo
||
(
name
===
APPLICATION_ROUTE_NAME
?
xyLogoRed
:
null
),
url
:
resolveApplicationUrl
({
name
,
}),
...
...
@@ -95,6 +115,52 @@ 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
)
=>
{
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
)
{
if
(
typeof
window
===
'undefined'
)
{
return
false
;
...
...
@@ -134,11 +200,7 @@ function ApplicationCard({ application, current }) {
aria
-
current=
{
current
?
'page'
:
undefined
}
className=
"flex min-w-0 items-center gap-3 no-underline"
>
<
Surface
aria
-
hidden=
"true"
className=
"rc-application-identity-well grid size-9 shrink-0 place-items-center overflow-hidden"
variant=
"tertiary"
>
<
Surface
aria
-
hidden=
"true"
className=
"rc-application-identity-well grid size-9 shrink-0 place-items-center overflow-hidden"
variant=
"tertiary"
>
<
Avatar
className=
"rc-application-identity-avatar size-full"
color=
"accent"
...
...
@@ -151,12 +213,12 @@ function ApplicationCard({ application, current }) {
</
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
"
title=
{
application
.
title
}
>
{
application
.
title
}
</
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
||
'应用'
}
</
spa
n
>
</
Descriptio
n
>
</
span
>
{
current
?
<
Chip
color=
"accent"
size=
"sm"
>
当前
</
Chip
>
:
null
}
</
a
>
...
...
@@ -186,6 +248,7 @@ function LoadingSkeleton() {
export
default
function
ApplicationSwitcherDialog
({
open
,
onClose
,
siteInfo
,
userInfo
,
})
{
const
fallbackApplications
=
useMemo
(()
=>
{
...
...
@@ -211,6 +274,10 @@ export default function ApplicationSwitcherDialog({
const
currentApplicationName
=
useMemo
(()
=>
{
return
applications
.
find
(
isCurrentApplication
)?.
name
||
''
;
},
[
applications
]);
const
applicationSections
=
useMemo
(
()
=>
buildApplicationSections
(
applications
,
siteInfo
?.
group
),
[
applications
,
siteInfo
?.
group
],
);
useEffect
(()
=>
{
if
(
fallbackApplications
.
length
===
0
)
{
...
...
@@ -239,7 +306,7 @@ export default function ApplicationSwitcherDialog({
try
{
const
response
=
await
getApplications
();
const
fetchedApplications
=
Array
.
isArray
(
response
)
?
response
:
[]
;
const
fetchedApplications
=
unwrapApplications
(
response
)
;
if
(
cancelled
)
{
return
;
...
...
@@ -271,32 +338,16 @@ export default function ApplicationSwitcherDialog({
},
[
fallbackApplications
,
open
]);
return
(
<
Modal
.
Root
>
<
Modal
.
Trigger
aria
-
hidden=
"true"
className=
"sr-only"
tabIndex=
{
-
1
}
/>
<
Modal
.
Backdrop
<
XinyuanModal
description=
"选择你可访问的应用"
dialogClassName=
"rc-application-switcher-dialog"
isOpen=
{
open
}
size=
"xl"
title=
"应用切换"
onOpenChange=
{
(
isOpen
)
=>
{
if
(
!
isOpen
)
onClose
?.();
}
}
variant=
"blur"
>
<
Modal
.
Container
className=
"w-full sm:w-full"
placement=
"center"
scroll=
"inside"
size=
"lg"
>
<
Modal
.
Dialog
className=
"max-h-[min(42rem,calc(100dvh-2rem))]"
style=
{
{
maxWidth
:
'min(46rem, calc(100vw - 2rem))'
}
}
>
<
Modal
.
CloseTrigger
aria
-
label=
"关闭"
>
<
Close
size=
{
16
}
/>
</
Modal
.
CloseTrigger
>
<
Modal
.
Header
>
<
div
className=
"min-w-0"
>
<
Modal
.
Heading
>
应用切换
</
Modal
.
Heading
>
<
Description
>
选择你可访问的应用
</
Description
>
</
div
>
</
Modal
.
Header
>
<
Modal
.
Body
>
{
status
===
'error'
&&
errorMessage
?
(
<
Alert
status=
"warning"
>
<
Alert
.
Indicator
><
ExclamationTriangleIcon
size=
{
20
}
/></
Alert
.
Indicator
>
...
...
@@ -307,8 +358,15 @@ export default function ApplicationSwitcherDialog({
{
status
===
'loading'
&&
applications
.
length
===
0
?
(
<
LoadingSkeleton
/>
)
:
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"
>
{
applications
.
map
((
application
)
=>
(
{
section
.
applications
.
map
((
application
)
=>
(
<
ApplicationCard
key=
{
application
.
name
}
application=
{
application
}
...
...
@@ -316,6 +374,9 @@ export default function ApplicationSwitcherDialog({
/>
))
}
</
div
>
</
section
>
))
}
</
div
>
)
:
(
<
EmptyState
size=
"md"
>
<
EmptyState
.
Header
>
...
...
@@ -324,10 +385,6 @@ export default function ApplicationSwitcherDialog({
</
EmptyState
.
Header
>
</
EmptyState
>
)
}
</
Modal
.
Body
>
</
Modal
.
Dialog
>
</
Modal
.
Container
>
</
Modal
.
Backdrop
>
</
Modal
.
Root
>
</
XinyuanModal
>
);
}
src/pages/project_manager_xy/project-management.business-flow.md
View file @
15a1e978
...
...
@@ -14,7 +14,7 @@
## 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