Commit 15a1e978 authored by Andy-bubu's avatar Andy-bubu

feat: 对齐应用切换弹窗设计规范

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