Commit f8a83c57 authored by Andy-bubu's avatar Andy-bubu

feat: align application switcher with Xinyuan baseline

parent 239971a9
...@@ -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` 合同。
......
...@@ -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` 胶囊轨道,按文字收缩且窄屏由官方容器滚动
- [ ] 页面数据集、详情阶段与动态表单分组的切换业务回调未变,本轮浏览器对照待补 - [ ] 页面数据集、详情阶段与动态表单分组的切换业务回调未变,本轮浏览器对照待补
......
...@@ -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
继承同一根主题。 继承同一根主题。
......
...@@ -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
......
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>
</> </>
); );
} }
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 || '应用'}
</span> </Description>
</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> <div className="text-sm text-muted">选择你可访问的应用</div>
<Description>
选择你可访问的应用
</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,13 +410,25 @@ export default function ApplicationSwitcherDialog({ ...@@ -307,13 +410,25 @@ export default function ApplicationSwitcherDialog({
{status === 'loading' && applications.length === 0 ? ( {status === 'loading' && applications.length === 0 ? (
<LoadingSkeleton /> <LoadingSkeleton />
) : applications.length > 0 ? ( ) : applications.length > 0 ? (
<div className="grid gap-3 sm:grid-cols-2"> <div className="space-y-6">
{applications.map((application) => ( {applicationSections.map((section) => (
<ApplicationCard <section key={section.key}>
key={application.name} <div className="mb-3 flex items-center justify-between gap-3">
application={application} <h3 className="text-sm font-medium leading-5">
current={application.name === currentApplicationName} {section.label}
/> </h3>
<Description>{section.applications.length} 个应用</Description>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{section.applications.map((application) => (
<ApplicationCard
key={application.name}
application={application}
current={application.name === currentApplicationName}
/>
))}
</div>
</section>
))} ))}
</div> </div>
) : ( ) : (
......
...@@ -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. 项目列表与详情
......
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