Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
A
ai-chat-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
ai-chat-xy
Commits
1da38cbc
Commit
1da38cbc
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
6345aa52
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
134 additions
and
38 deletions
+134
-38
ui-architecture.md
docs/ui-architecture.md
+2
-1
ui-migration-checklist.md
docs/ui-migration-checklist.md
+2
-1
xinyuan-design-system.md
docs/xinyuan-design-system.md
+1
-1
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+3
-2
index.jsx
src/components/ApplicationSwitcherDialog/index.jsx
+126
-33
No files found.
docs/ui-architecture.md
View file @
1da38cbc
...
...
@@ -68,7 +68,8 @@ Umi route /ai_chat/
-
退出仍删除
`current_user`
,不更改路由 base。
-
会话 CRUD、置顶顺序、当前会话选择和请求参数不变。
-
AI 请求 messages/context、知识库检索、MCP、附件和导出协议不变。
-
应用列表仍来自现有接口并沿用当前应用合并与跳转规则。
-
应用列表仍来自现有
`/api/application/info`
接口并沿用当前应用合并与跳转规则;
`/api/info`
的
`site.group`
仅负责显示分组、顺序与真实数量。站点配置失败回退“全部应用”,未被配置引用的授权应用进入“其他应用”,不改变权限或跳转协议。
-
应用切换数据只在弹窗打开时读取;两个接口并行且独立降级。应用列表失败沿用账号
`applications`
回退和原警告,站点配置失败只移除分组信息,不阻断授权应用展示。
## 不适用范围
...
...
docs/ui-migration-checklist.md
View file @
1da38cbc
...
...
@@ -30,7 +30,7 @@
-
[
x
]
HeroUI Pro Navbar、Sidebar.Trigger 与 48/32/16px 对称边距
-
[
x
]
主题切换及持久化
-
[
x
]
Sidebar 用户 Dropdown 与真实退出登录
-
[
x
]
应用切换 HeroUI Modal、两列应用 Card
和 identity well
-
[
x
]
应用切换 HeroUI Modal、两列应用 Card
、36px identity well,以及
`site.group`
动态分组、真实数量和“全部应用/其他应用”回退
-
[
x
]
移动端 Sidebar.Mobile
## AI 与业务模块
...
...
@@ -58,6 +58,7 @@
-
[
x
]
登录、filled/autofill、失败刷新验证码
-
[
x
]
Sidebar 开关、移动 offcanvas、用户菜单、退出
-
[
x
]
应用切换打开态、图片/失败/兜底状态
-
[
x
]
应用切换与
`model_analysis`
同数据同视口复验:动态分组/顺序/计数、空组过滤、其他/全部回退、长列表 Body 滚动、四视口双主题及弹窗控制台通过
-
[
~
]
会话菜单、删除确认、业务配置弹窗和上传打开态:配置弹窗/上传通过;空会话导致行菜单待真实记录复验
-
[
x
]
Tooltip、焦点、Escape、Console、页面溢出
-
[
x
]
长消息滚动时输入框坐标稳定;390×844 移动端无页面横向溢出
...
...
docs/xinyuan-design-system.md
View file @
1da38cbc
...
...
@@ -38,7 +38,7 @@ Inter 字体文件独立存放于 `public/fonts/`,并由 `.umirc.ts` 注入稳
-
Navbar 高 64px,使用 HeroUI Pro
`Sidebar.Trigger`
默认图形。
-
Navbar 与页面内容共享对称 gutter:桌面 48px、平板 32px、手机 16px。
-
Sidebar 用户整行通过
`Dropdown.Trigger`
打开菜单,
`退出登录`
调用现有清理流程。
-
应用切换为 HeroUI Modal,46rem/42rem 上限
,一列手机/两列
`sm`
以上,应用 identity well 为 36px
`Surface tertiary + Avatar soft`
。
-
应用切换为 HeroUI Modal,46rem/42rem 上限
、28px 内容内边距,一列手机/两列
`sm`
以上,应用 identity well 为 36px
`Surface tertiary + Avatar soft`
。分组必须来自
`/api/info`
的
`site.group`
,保持配置顺序并显示真实
`{count} 个应用`
;无配置使用“全部应用”,未被配置引用的授权应用使用“其他应用”
。
## Surface 与主题
...
...
docs/xinyuan-ui-audit.md
View file @
1da38cbc
...
...
@@ -38,7 +38,8 @@
-
退出:真实点击用户 Dropdown 的“退出登录”后,
`current_user`
为
`null`
并回到登录界面。
-
主题:切换后
`xinyuan_color_theme=dark`
,刷新后仍为 dark;Portal 弹窗计算为暗色主题背景与前景。
-
Sidebar:桌面折叠后内容可见宽度为 0,主区域回到 x=0;移动打开宽 312px,Escape 关闭。
-
应用切换:实际
`/api/application/info`
返回 3 个可访问应用;桌面 736px、两列、仅 1 个当前状态;手机 358px、单列;Escape 关闭。
-
应用切换:实际
`/api/application/info`
返回 3 个可访问应用;弹窗并行读取
`/api/info`
的
`site.group`
,按后端分组与组内顺序展示真实数量,未配置分组回退“全部应用”,未被分组引用的授权应用回退“其他应用”;桌面 736px、两列、仅 1 个当前状态;手机 358px、单列;Escape 关闭。
-
应用切换专项复验:与
`model_analysis`
在 1280×720 同屏数据下均为 736×484、28px inset、24px radius,Card 内容均为 38px;1440×900、1280×720、768×1024、390×844 的浅/深色共 8 组均无横向溢出,
`sm`
起严格两列、手机一列,弹窗自身控制台 0 error / 0 warning。长列表保持 Header 固定且仅 Body 滚动;空分组过滤、站点配置失败、全部应用和其他应用回退均通过内存 fixture 验证,fixture 未写入源码或业务数据。
-
弹窗:审计提示词和 Excel 配置均使用共享
`4xl`
HeroUI Modal 尺寸,最大宽度 1088px;手机宽度为视口减 32px,Footer 操作纵向满宽排列。DropZone 打开态、浅/深色和 Escape 已验证。
-
Modal 尺寸实测:目标在 1440×900 为
`x=176 / width=1088`
,在 3072×1200 仍为
`width=1088`
,在 390×844 为
`x=16 / width=358`
;“问题整改”基准在 1280×720 为
`width=1088`
,在 390×844 同为
`x=16 / width=358`
。目标弹窗
`scrollWidth === clientWidth`
,页面无横向溢出,控制台 0 error / 0 warning。
-
控制台:生产验收会话为 0 error / 0 warning;八种主题/视口组合均无页面横向溢出。
...
...
@@ -69,7 +70,7 @@
| AI 对话 | 普通问答、知识库问答、审计检查清单、附件和工具调用 | 消息结构、请求上下文、流式行为、错误回退不变 |
| 发票助手 | 由应用配置启用的对话分支 | 上传、识别、确认、下载和状态流不变 |
| 审计知识库 | Excel 导入、提示词配置、风险点结果和导出 | Excel 解析、保存、查询和导出逻辑不变 |
| 应用切换 | 调用系统信息与应用列表接口后跳转 | 数据解包、当前应用兜底、权限列表和跳转回调不变 |
| 应用切换 | 调用系统信息与应用列表接口后跳转 |
`site.group`
只控制展示分组;
数据解包、当前应用兜底、权限列表和跳转回调不变 |
| 退出登录 | 删除
`current_user`
并重新显示登录 | 真实清理行为不变,迁移到 Sidebar 用户菜单入口 |
`Access`
页面和已注释 Chat 路由当前不可达,记录为非活动代码;迁移完成前仍执行旧组件引用扫描,避免活动入口间接引用旧视觉组件。
...
...
src/components/ApplicationSwitcherDialog/index.jsx
View file @
1da38cbc
import
{
Close
,
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
getApplications
,
getSystemInfo
}
from
'@/services/DataService'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
...
...
@@ -41,13 +41,26 @@ function normalizeApplication(application, fallback = {}) {
}
function
extractApplications
(
response
)
{
if
(
Array
.
isArray
(
response
))
return
response
;
if
(
Array
.
isArray
(
response
?.
records
))
return
response
.
records
;
if
(
Array
.
isArray
(
response
?.
applications
))
return
response
.
applications
;
if
(
Array
.
isArray
(
response
?.
data
))
return
response
.
data
;
const
candidates
=
[
response
,
response
?.
data
,
response
?.
applications
,
response
?.
apps
,
response
?.
records
,
response
?.
rows
,
response
?.
list
,
response
?.
result
,
];
for
(
const
candidate
of
candidates
)
{
if
(
Array
.
isArray
(
candidate
))
return
candidate
;
}
return
[];
}
function
extractSiteInfo
(
response
)
{
return
response
?.
site
||
response
?.
data
?.
site
||
{};
}
function
mergeApplications
(
fetchedApplications
,
fallbackApplications
)
{
const
fallbackMap
=
new
Map
(
fallbackApplications
.
map
((
application
)
=>
[
application
.
name
,
application
]),
...
...
@@ -76,6 +89,49 @@ 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
;
...
...
@@ -121,15 +177,18 @@ function ApplicationCard({ application, current }) {
</
Avatar
>
</
Surface
>
<
span
className=
"min-w-0 flex-1"
>
<
Card
.
Title
className=
"break-words"
title=
{
application
.
title
}
>
<
span
className=
"block truncate text-sm font-medium leading-5"
title=
{
application
.
title
}
>
{
application
.
title
}
</
Card
.
Title
>
<
Card
.
Description
className=
"
truncate
"
</
span
>
<
Description
className=
"
mt-0.5 block truncate leading-4
"
title=
{
application
.
description
}
>
{
application
.
description
||
application
.
name
||
'应用'
}
</
Card
.
Description
>
</
Description
>
</
span
>
{
current
?
(
<
Chip
color=
"accent"
size=
"sm"
>
...
...
@@ -168,6 +227,7 @@ export default function ApplicationSwitcherDialog({ open, onClose, userInfo }) {
.
filter
((
application
)
=>
application
.
name
);
},
[
userInfo
]);
const
[
applications
,
setApplications
]
=
useState
(
fallbackApplications
);
const
[
siteInfo
,
setSiteInfo
]
=
useState
({});
const
[
status
,
setStatus
]
=
useState
(
fallbackApplications
.
length
>
0
?
'success'
:
'idle'
,
);
...
...
@@ -176,34 +236,48 @@ export default function ApplicationSwitcherDialog({ open, onClose, userInfo }) {
()
=>
applications
.
find
(
isCurrentApplication
)?.
name
||
''
,
[
applications
],
);
const
applicationSections
=
useMemo
(
()
=>
buildApplicationSections
(
applications
,
siteInfo
?.
group
),
[
applications
,
siteInfo
?.
group
],
);
useEffect
(()
=>
{
if
(
!
open
)
return
;
let
cancelled
=
false
;
const
load
Applications
=
async
()
=>
{
const
load
DialogData
=
async
()
=>
{
setStatus
((
previous
)
=>
(
previous
===
'success'
?
previous
:
'loading'
));
setErrorMessage
(
''
);
try
{
const
response
=
await
getApplications
();
const
[
applicationsResult
,
systemInfoResult
]
=
await
Promise
.
allSettled
([
getApplications
(),
getSystemInfo
(),
]);
if
(
cancelled
)
return
;
if
(
systemInfoResult
.
status
===
'fulfilled'
)
{
setSiteInfo
(
extractSiteInfo
(
systemInfoResult
.
value
));
}
else
{
setSiteInfo
({});
}
if
(
applicationsResult
.
status
===
'rejected'
)
{
setApplications
(
fallbackApplications
);
setStatus
(
'error'
);
setErrorMessage
(
'应用列表加载失败,已回退展示当前账号上的应用信息。'
);
return
;
}
setApplications
(
mergeApplications
(
extractApplications
(
respons
e
),
extractApplications
(
applicationsResult
.
valu
e
),
fallbackApplications
,
),
);
setStatus
(
'success'
);
}
catch
{
if
(
cancelled
)
return
;
setApplications
(
fallbackApplications
);
setStatus
(
'error'
);
setErrorMessage
(
'应用列表加载失败,已回退展示当前账号上的应用信息。'
);
}
};
load
Applications
();
load
DialogData
();
return
()
=>
{
cancelled
=
true
;
};
...
...
@@ -226,19 +300,22 @@ export default function ApplicationSwitcherDialog({ open, onClose, userInfo }) {
size=
"lg"
>
<
Modal
.
Dialog
className=
"max-h-[min(42rem,calc(100dvh-2rem))]"
style=
{
{
maxWidth
:
'min(46rem, calc(100vw - 2rem))'
}
}
aria
-
label=
"应用切换"
className=
"min-w-0 max-h-[min(42rem,calc(100dvh-2rem))] p-7"
style=
{
{
width
:
'min(46rem, calc(100vw - 2rem))'
,
maxWidth
:
'min(46rem, calc(100vw - 2rem))'
,
}
}
>
<
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
>
<
div
className=
"text-sm text-muted"
>
选择你可访问的应用
</
div
>
</
Modal
.
Header
>
<
Modal
.
Body
>
<
Modal
.
Body
style=
{
{
marginTop
:
'0.75rem'
}
}
>
{
status
===
'error'
&&
errorMessage
?
(
<
Alert
status=
"warning"
>
<
Alert
.
Indicator
>
...
...
@@ -252,15 +329,31 @@ export default function ApplicationSwitcherDialog({ open, onClose, userInfo }) {
{
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
application=
{
application
}
current=
{
application
.
name
===
currentApplicationName
}
current=
{
application
.
name
===
currentApplicationName
}
key=
{
application
.
name
}
/>
))
}
</
div
>
</
section
>
))
}
</
div
>
)
:
(
<
EmptyState
size=
"md"
>
<
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