Commit 59f021c1 authored by Andy-bubu's avatar Andy-bubu

feat: align application switcher with Xinyuan baseline

parent 6a36f6ce
......@@ -30,7 +30,7 @@
- 登录采用居中认证 Card。花形 Logo 约 36px,标题约 32px/600;不重复显示“登录”标题。
- 账号、密码、验证码可见高度统一为 40px,字段组可见间距 24px,提交按钮上方保留 24px。
- 登录字段有值或自动填充时使用 `#fbf1f1`,Hover 使用 `#faeaea`,Focus 使用鑫元红描边和浅红焦点环。
- 应用切换器固定为居中的紧凑三列列表,标题“应用切换”、副标题“选择你可访问的应用”;最多一个“当前”标签,不显示数量、权限标签、外链图标或统计内容。
- 应用切换器固定为居中的紧凑两列 HeroUI Modal,移动端一列;标题“应用切换”、副标题“选择你可访问的应用”。分组与成员顺序读取站点 `site.group`,组标题显示真实应用数量,空组隐藏,未分组可访问项归入“其他应用”,无配置时回退“全部应用”;最多一个“当前”标签,不显示权限标签或外链装饰。
## 4. 页面、筛选与 Tab
......
......@@ -26,6 +26,7 @@
- 多选:共享 Autocomplete composition 在 `Autocomplete.Value` 内使用 HeroUI `TagGroup + Tag + Tag.RemoveButton`;根 `selectedKeys` 是唯一选择状态,移除操作回写同一个 `onSelectionChange`,业务页面只负责值协议映射。
- 列表:Pro DataGrid 原生 selection/sort/fixed columns + HeroUI Pagination + Pro ActionBar/EmptyState。
- 弹层:HeroUI Modal/AlertDialog/Dropdown/Tooltip,根主题覆盖 body Portal。
- 应用切换:共享 `ApplicationSwitcherDialog` 直接组合 HeroUI Modal/Card/Surface/Avatar/Chip 与 Pro EmptyState;`SiteContext.siteInfo.group` 只负责动态分组与顺序,`UserContext.applications` 和 `/application/info` 继续负责访问集合与详情。授权域 `audit_manager` 规范化为唯一当前项,并导航到部署基路径 `/audit_manager_new/`。
- 上传:Pro DropZone,页面边界接收真实 File。
- KPI/图表:仅业务存在时使用 Pro KPI/PieChart/BarChart/ComposedChart/ChartTooltip。
- 报告视图:一级 HeroUI Tabs 切换 current/history,二级内容宽度 Tabs 以稳定项目 ID 切换报告快照;当前项目只挂载工具栏和 DataGrid,不重复 Tabs 已表达的项目标题,整体进度由 KPI 承担。项目与控制点仍携带原数组索引进入现有更新/确认回调。
......@@ -37,6 +38,7 @@
- `theme.css` 提供 HeroUI 核心颜色、半径、阴影、字段和 dark token,必须与技能资产逐字节一致。
- `tokens.css` 只能添加 `--rc-*` 别名、布局尺寸、业务语义 chip/chart tokens,不能重定义核心变量。
- 共享 Shell/layout CSS 只负责几何和技能批准的跨应用模式。
- 应用切换 CSS 仅允许 `46rem/42rem` 弹窗几何和 36px identity well 的共享 bridge class;Card、Surface、Avatar、Chip 与交互状态均由 HeroUI variant/compound API 管理。
- 页面 className 只负责 flex、grid、宽度、间距、对齐和响应式;禁止外观、状态、slot/BEM 和伪元素覆盖。
## 业务边界
......
# UI 迁移清单
状态日期:2026-08-21。`完成` 仅表示代码、业务保护、双应用浏览器对照和扫描门槛全部通过。
状态日期:2026-08-26。`完成` 仅表示代码、业务保护、双应用浏览器对照和扫描门槛全部通过。
| 模块 | 路由/入口 | 代码迁移 | 浏览器对照 | 业务保护 |
| --- | --- | --- | --- | --- |
......@@ -9,7 +9,7 @@
| Login | `/login` | 完成 | 四视口双主题及 filled 完成 | 完成 |
| AppShell/Navbar/Sidebar | 鉴权路由 | 完成 | 缺有效登录态 | 代码复核完成 |
| 用户菜单/退出 | Sidebar footer | 完成 | 缺有效登录态 | 回调保留 |
| 应用切换 | Shell Modal | 完成 | 缺有效登录态 | 数据/导航保留 |
| 应用切换 | Shell Modal | 分组/数量/唯一当前态完成 | 2026-08-26 双应用均停在真实登录门禁 | `/info` 分组、应用接口与部署导航保留 |
| 全应用弹窗尺寸 | HeroUI Modal / AlertDialog | 完成 | 待有效登录态复验 | md 480 / lg 640 / 3xl 960 / 应用切换 736 |
| 稽核项目 | `/audit_process` | 完成 | 缺有效登录态 | 代码复核完成 |
| 多选字段 | 稽核项目表单 | 完成 | 缺有效登录态/打开态复验 | 框内 Tag;`_id[]` 与请求时机保留 |
......@@ -34,6 +34,7 @@
- [x] 并列报告项目整合为内容宽度二级 HeroUI Tabs,单项目时隐藏轨道;搜索只过滤当前项目列表。
- [x] 当前报告搜索位于二级 Tabs 下方的标准工具栏,移动端独占一行;项目区不重复 Tabs 标题和 KPI 进度。
- [x] 报告附件内容使用紧凑 HeroUI Chip/Link,DataGrid 单元格内无 Card/Surface 或页面私有外观。
- [x] 应用切换按站点配置动态分组,移动一列/桌面两列,使用 HeroUI 原生 Card/Surface/Avatar 并保持唯一当前态。
- [ ] 四视口、浅色/暗色、目标/基准同时打开完成(登录完成,鉴权页面待有效登录态)。
- [ ] 所有可达 popup、Modal、Tooltip、Dropdown、Sidebar 状态检查完成。
- [x] `npm run build` 与 `git diff --check` 通过;`audit_project.sh` 退出码 0,并报告非活动遗留。
......
......@@ -16,7 +16,10 @@
- HeroUI Pro Navbar 高 64px;`Sidebar.Trigger` 使用 Pro 原生图形。
- Navbar 与页面内容采用对称 48px/32px/16px gutter;主题操作在最右。
- 用户行由 `Dropdown.Trigger` 打开真实菜单,退出调用目标应用现有服务。
- 应用切换使用 HeroUI Modal,桌面宽约 46rem、高不超过 42rem,一列移动/两列桌面;最多一个“当前”。
- 应用切换使用 HeroUI Modal,标题固定为“应用切换”,副标题固定为“选择你可访问的应用”;弹窗宽 `min(46rem, 100vw - 2rem)`、高不超过 `min(42rem, 100dvh - 2rem)`,28px 内边距,只有 Modal Body 滚动。
- 应用按 `/info` 的 `site.group` 与组内成员顺序动态分区,隐藏无可访问应用的分组,未被配置引用的可访问应用进入“其他应用”;无分组配置时显示“全部应用”。分组标题右侧显示真实可访问数量,禁止硬编码分组或应用。
- 每个分组在移动端一列、`sm` 及以上严格两列,组间 24px、标题到卡片 12px、卡片间 12px;应用项使用 HeroUI `Card variant="secondary"`、`Surface variant="tertiary"` 与 `Avatar variant="soft"`,identity well 固定 36px。
- 应用列表合并 `/application/info` 与当前用户授权应用;接口失败回退用户授权信息。授权域 `audit_manager` 映射到部署路径 `/audit_manager_new/`,整个弹窗最多一个 accent“当前” Chip。
## 登录
......
# 监察稽核管理 UI 审计
审计日期:2026-08-21
审计日期:2026-08-26
当前分支:`codex/ui-ux-xinyuan`
唯一实现基准:`/Users/liuchenlong/Desktop/job/problem_rectification`
......@@ -49,7 +49,9 @@
- 登录已同步基准的居中 Card、双主题 Logo、字段节奏、验证码 Button、filled/autofill 与响应式几何。
- 鉴权 Shell 使用共享 `AppShell`、Pro Navbar、Pro Sidebar offcanvas、用户 Dropdown、真实退出回调和应用切换 Modal。
- 应用切换使用基准两列结构、L2 Card 与 L3 Surface + Avatar identity well。
- 应用切换已按 `model_analysis` 基准改为动态分组结构:读取 `SiteContext.siteInfo.group` 保留后端分组及成员顺序,隐藏空组,将未配置的可访问应用归入“其他应用”,并显示真实组内数量。
- 应用项使用严格一列移动/两列桌面结构、L2 HeroUI Card 与 L3 Surface + Avatar identity well;弹窗保持 46rem/42rem、28px padding、Body 单独滚动及唯一“当前” Chip。
- 应用数据仍来自既有 `/application/info` 与 `userInfo.applications` 回退。仅将授权域 `audit_manager` 规范化为当前应用并导航至既有 `/audit_manager_new/` 部署基路径,未修改接口、权限或登录状态流。
- 活动列表使用 Pro DataGrid、Pagination、ActionBar、EmptyState;选择、排序、固定列及分页业务回调保留。
- 活动 DataGrid 操作列统一将 `tertiary` Eye 查看按钮放在首位:项目控制点与报告控制点打开当前行的只读 HeroUI Modal,历史报告进入原详情路由;编辑、确认、删除权限和回调不变。
- 报告视图“进行中 / 历史报告”Tabs 已移除 `flex-1` 伸展,根节点和 ListContainer 按基准使用内容宽度;同行导出、填报链接和提交操作继续由外层布局靠右。
......@@ -77,6 +79,9 @@
## 验收证据与剩余边界
- `npm run build` 与 `git diff --check` 通过。
- 2026-08-26 应用切换变更后重新执行生产构建,Webpack 编译通过;目标独立开发服务运行于 `http://localhost:8011/audit_manager_new/`,`model_analysis` 基准运行于 `http://localhost:8005/model_analysis/`,两者 HTTP 200。
- 使用全新 Playwright 会话分别进入目标 `/audit_process` 和基准 `/models/overview`,两者都由真实鉴权重定向至各自登录页,登录页 console 均为 0 error / 0 warning。现存带 `browser-only-token` 的 QA 会话未用于验收,也未向目标注入用户、token 或业务数据。
- 因当前没有有效真实登录凭据,本次无法打开目标应用切换 Modal,动态分组、四视口双主题、Body 滚动、关闭交互与唯一当前态的浏览器对照仍待补齐;代码对照、生产构建、HeroUI Pro ESM 子路径导入和静态扫描已通过,不据此声明浏览器验收完成。
- 2026-08-21 重新打开 `http://localhost:8001/audit_manager_new/`,应用正常跳转真实登录页,当前开发预览 console 为 0 error / 0 warning。
- 目标与“问题整改”登录页在 1440x900、1280x720、768x1024、390x844 的浅色/暗色几何逐项一致,所有视口横向溢出为 0,console 为 0 error / 0 warning。
- 已填写态三个字段均为 40px,目标与基准的 filled Surface 计算值一致,未提交测试登录请求。
......
import xinyuanLogo from '@/assets/identity/xy-logo-red.png';
import { ExclamationTriangleIcon } from '@/components/AppIcons';
import { APP_BASE_PATH, APP_DISPLAY_NAME, APP_ROUTE_NAME } from '@/constants/app';
import { getApplications } from '@/services/DataService';
import { Close, ExclamationTriangleIcon } from '@/components/AppIcons';
import { EmptyState } from '@heroui-pro/react/empty-state';
import {
Alert,
......@@ -13,152 +15,165 @@ import {
} from '@heroui/react';
import { useEffect, useMemo, useState } from 'react';
function normalizeLabelList(items) {
if (!Array.isArray(items)) {
return [];
}
return items
.map((item) => {
if (typeof item === 'string') {
return item;
}
return item?.display_name || item?.title || item?.name || '';
})
.filter(Boolean);
const APPLICATION_PERMISSION_NAME = 'audit_manager';
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 resolveApplicationUrl(application) {
if (application?.name) {
return `/${application.name}`;
}
return '#';
function isCurrentApplicationName(name) {
return name === APPLICATION_PERMISSION_NAME || name === APP_ROUTE_NAME;
}
function normalizeApplication(application, fallback = {}) {
const name = application?.name || fallback?.name || '';
function normalizeApplication(application = {}) {
const sourceName = application.name || '';
const current = isCurrentApplicationName(sourceName);
const name = current ? APPLICATION_PERMISSION_NAME : sourceName;
return {
name,
title: application?.title || fallback?.title || application?.name || fallback?.name || '未命名应用',
title: current ? APP_DISPLAY_NAME : application.title || name || '未命名应用',
description:
application?.description ||
fallback?.description ||
fallback?.title ||
application?.title ||
application.description ||
application.subtitle ||
application.title ||
'进入对应应用继续处理业务',
logo: application?.logo || fallback?.logo || null,
url: resolveApplicationUrl({
name,
}),
access: application?.access || fallback?.access || '',
status: application?.status || fallback?.status || '',
type: application?.type || fallback?.type || '',
order: application?.order ?? fallback?.order ?? 0,
roles: normalizeLabelList(application?.roles?.length ? application.roles : fallback?.roles),
groups: normalizeLabelList(application?.groups?.length ? application.groups : fallback?.groups),
logo: current ? xinyuanLogo : application.logo || application.icon || null,
order: Number(application.order || 0),
url: current
? APP_BASE_PATH
: application.url || application.path || (name ? `/${name}` : '#'),
};
}
function mergeApplications(fetchedApplications, fallbackApplications) {
const fallbackMap = new Map(
fallbackApplications.map((application) => [application.name, application]),
);
const normalizedFetched = fetchedApplications
.map((application) =>
normalizeApplication(application, fallbackMap.get(application?.name)),
)
.filter((application) => application.name);
const mergedMap = new Map(
normalizedFetched.map((application) => [application.name, application]),
);
fallbackApplications.forEach((application) => {
if (!application?.name || mergedMap.has(application.name)) {
return;
}
mergedMap.set(application.name, normalizeApplication(application, application));
const currentFallback = fallbackApplications.find((application) =>
isCurrentApplicationName(application?.name),
) || {
name: APPLICATION_PERMISSION_NAME,
title: APP_DISPLAY_NAME,
};
const merged = new Map();
[...fallbackApplications, ...fetchedApplications, currentFallback]
.map(normalizeApplication)
.filter((application) => application.name)
.forEach((application) => {
merged.set(application.name, {
...merged.get(application.name),
...application,
});
});
return Array.from(mergedMap.values()).sort((left, right) => {
const orderDifference = Number(left.order || 0) - Number(right.order || 0);
if (orderDifference !== 0) {
return orderDifference;
}
return left.title.localeCompare(right.title, 'zh-CN');
return Array.from(merged.values()).sort((left, right) => {
return left.order - right.order || left.title.localeCompare(right.title, 'zh-CN');
});
}
function isCurrentApplication(application) {
if (typeof window === 'undefined') {
return false;
function buildApplicationSections(applications, applicationGroups) {
if (!Array.isArray(applicationGroups) || applicationGroups.length === 0) {
return [{ key: 'all', label: '全部应用', applications }];
}
const currentPath = window.location.pathname;
const applicationRoot = application?.name ? `/${application.name}` : '';
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)
: [];
if (application?.url?.startsWith('http')) {
try {
const url = new URL(application.url);
return url.pathname === currentPath;
} catch (error) {
return false;
}
}
return {
key: group?.label || `group-${index}`,
label: group?.label || '应用分组',
applications: groupApplications,
};
})
.filter((section) => section.applications.length > 0);
return (
application?.url === currentPath ||
(!!applicationRoot &&
(currentPath === applicationRoot ||
currentPath.startsWith(`${applicationRoot}/`)))
const ungroupedApplications = applications.filter(
(application) => !groupedApplicationNames.has(application.name),
);
if (ungroupedApplications.length > 0) {
sections.push({
key: 'ungrouped',
label: '其他应用',
applications: ungroupedApplications,
});
}
return sections.length > 0
? sections
: [{ key: 'all', label: '全部应用', applications }];
}
function ApplicationCard({ application, current }) {
const target = application?.url?.startsWith('http') ? '_blank' : undefined;
const target = /^https?:\/\//i.test(application.url) ? '_blank' : undefined;
const initial = application.title.trim().slice(0, 1).toUpperCase() || 'A';
return (
<Card className="h-full" variant="secondary">
<Card.Content>
<a
href={application.url}
target={target}
rel={target ? 'noreferrer' : undefined}
aria-current={current ? 'page' : undefined}
className="flex min-w-0 items-center gap-3 no-underline"
href={application.url}
rel={target ? 'noreferrer' : undefined}
target={target}
>
<Surface
aria-hidden="true"
className="grid size-9 shrink-0 place-items-center overflow-hidden rounded-[var(--rc-radius-control)]"
className="rc-app-identity-well grid size-9 shrink-0 place-items-center overflow-hidden"
variant="tertiary"
>
<Avatar
className="size-full rounded-[var(--rc-radius-control)] bg-transparent"
className="rc-app-identity-avatar size-full"
color="accent"
variant="soft"
>
{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}
<Avatar.Fallback>{initial}</Avatar.Fallback>
</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">
{application.title}
</span>
<span className="block truncate text-sm text-muted" title={application.description}>
{application.description || application.name || '应用'}
</span>
<Description className="mt-0.5 block truncate leading-4">
{application.description}
</Description>
</span>
{current ? <Chip color="accent" size="sm">当前</Chip> : null}
{current ? (
<Chip color="accent" size="sm">
当前
</Chip>
) : null}
</a>
</Card.Content>
</Card>
......@@ -167,7 +182,7 @@ function ApplicationCard({ application, current }) {
function LoadingSkeleton() {
return (
<div className="grid gap-3 sm:grid-cols-2" aria-label="正在加载应用">
<div aria-label="正在加载应用" className="grid gap-3 sm:grid-cols-2">
{Array.from({ length: 6 }).map((_, index) => (
<Card key={index} variant="secondary">
<Card.Content className="flex items-center gap-3">
......@@ -186,84 +201,53 @@ function LoadingSkeleton() {
export default function ApplicationSwitcherDialog({
open,
onClose,
siteInfo,
userInfo,
}) {
const fallbackApplications = useMemo(() => {
if (!Array.isArray(userInfo?.applications)) {
return [];
}
return userInfo.applications
.map((application) =>
normalizeApplication(application, {
...application,
description: application?.title,
}),
)
.filter((application) => application.name);
}, [userInfo]);
const [applications, setApplications] = useState(fallbackApplications);
const [status, setStatus] = useState(
fallbackApplications.length > 0 ? 'success' : 'idle',
const fallbackApplications = useMemo(
() => (Array.isArray(userInfo?.applications) ? userInfo.applications : []),
[userInfo],
);
const [applications, setApplications] = useState(() =>
mergeApplications([], fallbackApplications),
);
const [status, setStatus] = useState('idle');
const [errorMessage, setErrorMessage] = useState('');
const currentApplicationName = useMemo(() => {
return applications.find(isCurrentApplication)?.name || '';
return (
applications.find(
(application) => application.name === APPLICATION_PERMISSION_NAME,
)?.name ||
applications.find((application) => application.name === APP_ROUTE_NAME)?.name ||
''
);
}, [applications]);
const applicationSections = useMemo(
() => buildApplicationSections(applications, siteInfo?.group),
[applications, siteInfo?.group],
);
useEffect(() => {
if (fallbackApplications.length === 0) {
return;
}
setApplications((previous) => {
if (previous.length > 0) {
return previous;
}
return fallbackApplications;
});
}, [fallbackApplications]);
useEffect(() => {
if (!open) {
return;
}
if (!open) return undefined;
let cancelled = false;
const loadApplications = async () => {
setStatus((previous) => (previous === 'success' ? previous : 'loading'));
setErrorMessage('');
try {
const response = await getApplications();
const fetchedApplications = Array.isArray(response) ? response : [];
if (cancelled) {
return;
}
const mergedApplications = mergeApplications(
fetchedApplications,
fallbackApplications,
getApplications()
.then((response) => {
if (cancelled) return;
setApplications(
mergeApplications(unwrapApplications(response), fallbackApplications),
);
setApplications(mergedApplications);
setStatus('success');
} catch (error) {
if (cancelled) {
return;
}
setApplications(fallbackApplications);
})
.catch(() => {
if (cancelled) return;
setApplications(mergeApplications([], fallbackApplications));
setStatus('error');
setErrorMessage('应用列表加载失败,已回退展示当前账号上的应用信息。');
}
};
loadApplications();
});
return () => {
cancelled = true;
......@@ -275,40 +259,50 @@ export default function ApplicationSwitcherDialog({
<Modal.Trigger aria-hidden="true" className="sr-only" tabIndex={-1} />
<Modal.Backdrop
isOpen={open}
variant="blur"
onOpenChange={(isOpen) => {
if (!isOpen) onClose?.();
}}
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
className="max-h-[min(42rem,calc(100dvh-2rem))]"
style={{ maxWidth: 'min(46rem, calc(100vw - 2rem))' }}
aria-label="应用切换"
className="rc-app-switcher-dialog p-7"
>
<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>
<Description>选择你可访问的应用</Description>
</Modal.Header>
<Modal.Body>
<Modal.Body style={{ marginTop: '0.75rem' }}>
{status === 'error' && errorMessage ? (
<Alert status="warning">
<Alert.Indicator><ExclamationTriangleIcon size={20} /></Alert.Indicator>
<Alert.Content><Alert.Description>{errorMessage}</Alert.Description></Alert.Content>
<Alert.Indicator>
<ExclamationTriangleIcon size={20} />
</Alert.Indicator>
<Alert.Content>
<Alert.Description>{errorMessage}</Alert.Description>
</Alert.Content>
</Alert>
) : null}
{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,11 +310,16 @@ export default function ApplicationSwitcherDialog({
/>
))}
</div>
</section>
))}
</div>
) : (
<EmptyState size="md">
<EmptyState.Header>
<EmptyState.Title>暂无可切换应用</EmptyState.Title>
<EmptyState.Description>当前账号还没有分配其它应用权限。</EmptyState.Description>
<EmptyState.Description>
当前账号还没有分配其它应用权限。
</EmptyState.Description>
</EmptyState.Header>
</EmptyState>
)}
......
......@@ -1397,6 +1397,22 @@
line-height: 1rem;
}
.rc-app-switcher-dialog {
min-width: 0;
width: min(46rem, calc(100vw - 2rem));
max-width: min(46rem, calc(100vw - 2rem));
max-height: min(42rem, calc(100dvh - 2rem));
}
.rc-app-identity-well,
.rc-app-identity-avatar {
border-radius: var(--rc-radius-control);
}
.rc-app-identity-avatar {
background: transparent;
}
@media (max-width: 768px) {
.rc-mobile-sidebar.app-sidebar-glass {
width: 100%;
......
......@@ -3,6 +3,7 @@ import { FileText, Project } from '@/components/AppIcons';
import ApplicationSwitcherDialog from '@/components/ApplicationSwitcherDialog';
import { APP_DISPLAY_NAME, getAppRouteTitle } from '@/constants/app';
import { UserContext } from '@/wrapper/Auth';
import { SiteContext } from '@/wrapper/Site';
import { useContext, useEffect, useMemo, useState } from 'react';
import { Outlet, useLocation } from 'umi';
......@@ -20,6 +21,7 @@ const MENU_GROUPS = [
export default function AuditManagementLayout() {
const location = useLocation();
const userInfo = useContext(UserContext);
const siteInfo = useContext(SiteContext);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false);
const currentTitle = useMemo(
......@@ -49,6 +51,7 @@ export default function AuditManagementLayout() {
<ApplicationSwitcherDialog
open={isApplicationDialogOpen}
onClose={() => setIsApplicationDialogOpen(false)}
siteInfo={siteInfo}
userInfo={userInfo}
/>
</>
......
......@@ -9,6 +9,8 @@
应用部署基路径为 `/audit_manager_new/`。主应用导航使用 HeroUI Pro Sidebar,桌面端采用 offcanvas 折叠,移动端使用 `Sidebar.Mobile`。
侧栏应用切换入口打开 HeroUI Modal。弹窗按站点 `/info` 配置的 `site.group` 顺序展示当前用户可访问应用,并通过既有 `/application/info` 补全应用信息;加载失败时回退当前用户授权列表。选择其它应用沿用其既有 URL,选择当前授权域 `audit_manager` 返回部署基路径 `/audit_manager_new/`,不改变权限或登录状态。
## 2. 权限
- 应用授权域为 `audit_manager`
......
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