Commit 045f8d34 authored by Andy-bubu's avatar Andy-bubu

feat: align application switcher and responsive shell

parent a4fa4a39
...@@ -58,6 +58,18 @@ The removed portable/app-token layers must not be reintroduced. ...@@ -58,6 +58,18 @@ The removed portable/app-token layers must not be reintroduced.
- Shared UI owns layout, accessibility and HeroUI composition only. - Shared UI owns layout, accessibility and HeroUI composition only.
- Routes keep the Umi base `/audit_material_manager/` and existing auth wrappers. - Routes keep the Umi base `/audit_material_manager/` and existing auth wrappers.
Application switching keeps `/application/info` plus the current user's
applications as the access-controlled data source. `SiteContext` supplies
`GET /info -> site.group` for presentation grouping only; it never grants
access or invents applications. Configured order is preserved, empty or
inaccessible groups are omitted, unreferenced accessible apps append to
`其他应用`, and absent configuration uses one `全部应用` section.
The desktop navigation remains HeroUI Pro Sidebar. At mobile width it reuses
the same Sidebar content inside a controlled HeroUI Pro Sheet so the Dialog can
carry the explicit accessible name `主导航`; `useSidebar` remains the single
source of open/close state and route actions keep the existing close behavior.
## Legacy UI Policy ## Legacy UI Policy
1. Do not add or restore Catalyst imports, components, styles, dependencies, 1. Do not add or restore Catalyst imports, components, styles, dependencies,
......
# UI Migration Checklist # UI Migration Checklist
Updated: 2026-08-20 Updated: 2026-08-25
Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`. Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`.
...@@ -8,12 +8,12 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`. ...@@ -8,12 +8,12 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`.
| ----------------------------- | ----------- | ----------------------------------------------------------------------------------------------------- | | ----------------------------- | ----------- | ----------------------------------------------------------------------------------------------------- |
| Repository onboarding | done | Remote verified, fast-forwarded, dedicated branch created | | Repository onboarding | done | Remote verified, fast-forwarded, dedicated branch created |
| Business/API reading | done | Record, Subject and audit-material flow documents reviewed | | Business/API reading | done | Record, Subject and audit-material flow documents reviewed |
| Baseline startup | done | Umi running at localhost port 8002 with real base path | | Baseline startup | done | Umi running at `http://localhost:8000/audit_material_manager/` with the real base path |
| Baseline login QA | superseded | Earlier viewport evidence predates the corrected problem-rectification login parity pass | | Baseline login QA | superseded | Earlier viewport evidence predates the corrected problem-rectification login parity pass |
| Authoritative theme | done | Exact baseline `theme.css`, Xinyuan assets, Inter and app-only extension Tokens installed | | Authoritative theme | done | Exact baseline `theme.css`, Xinyuan assets, Inter and app-only extension Tokens installed |
| Login | done | Baseline structure, assets and approved 12px label / 24px field rhythm synchronized | | Login | done | Baseline structure, assets and approved 12px label / 24px field rhythm synchronized |
| Shell | done | Pro AppLayout, Sidebar, Navbar and official Sidebar.Trigger; desktop offcanvas and mobile sheet | | Shell | done | Pro AppLayout/Sidebar/Navbar; desktop offcanvas plus an accessible Pro Sheet mobile sidebar |
| Application switcher | done | HeroUI Modal/Surface/Avatar/Skeleton/Alert; API fallback and current-app detection preserved | | Application switcher | done | model_analysis parity; `/info -> site.group`, counts, empty-group filtering, 其他应用/全部应用 fallbacks, two-column grid |
| Dashboard | done | Pro KPI uses the approved 24px Card and 48px toned icon Surface; analytical modules follow the same radius contract | | Dashboard | done | Pro KPI uses the approved 24px Card and 48px toned icon Surface; analytical modules follow the same radius contract |
| Material archive filters | done | HeroUI Surface, Disclosure, InputGroup, Autocomplete and DatePicker compounds | | Material archive filters | done | HeroUI Surface, Disclosure, InputGroup, Autocomplete and DatePicker compounds |
| Material DataGrid | done | Pro DataGrid with native current-page multiple selection and fixed overflow columns | | Material DataGrid | done | Pro DataGrid with native current-page multiple selection and fixed overflow columns |
...@@ -27,7 +27,31 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`. ...@@ -27,7 +27,31 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`.
| Real-data-only states | done | Demo material/project fallbacks removed; empty/error states reflect the API | | Real-data-only states | done | Demo material/project fallbacks removed; empty/error states reflect the API |
| Private visual CSS | done | Duplicate portable/app-token layers removed; approved shared shell/login baseline retained | | Private visual CSS | done | Duplicate portable/app-token layers removed; approved shared shell/login baseline retained |
| Production build | done | `npm run build` completed after core migration | | Production build | done | `npm run build` completed after core migration |
| Browser acceptance | blocked | Browser discovery currently returns no available browser; prior evidence is not final acceptance | | Browser acceptance | in progress | Four switcher viewports, dark theme, desktop/mobile sidebar and user menu pass; Autocomplete library warnings remain |
## 2026-08-25 Application Switcher Correction
- Fast-forwarded the clean migration branch to the remote documentation commit
before editing.
- Passed `SiteContext` from the existing `GET /info` wrapper into the switcher.
- Preserved `/application/info`, account fallback, merge ordering, current-app
detection, links, auth and error fallback while adding the model-analysis
section builder and robust response-array unwrapping.
- Verified configured order, inaccessible/empty-group removal, `其他应用`, real
counts and one `当前` marker with browser-only responses.
- Computed switcher results are 358x648 and one column at 390x844, then
736x484 and exactly two columns at 768x1024, 1280x720 and 1440x900. Every
viewport resolves 24px radius, 28px padding and zero page-level overflow.
- Shell gutters resolve to 16px, 32px and 48px at the approved mobile, tablet
and desktop breakpoints. The 1440x900 dark pass uses the dark Surface rather
than a white overlay island.
- Desktop offcanvas collapse moves the complete 240px sidebar outside the
viewport. The 390px Pro Sheet sidebar is 312px wide, exposes
`aria-label="主导航"`, and closes through Escape, backdrop and route action.
- Application switcher, dashboard, sidebar and user Dropdown report zero
console errors/warnings. Material booking still emits a HeroUI 3.2.2
Autocomplete internal Dialog-title warning for each mounted field; the
public compound API does not expose the internal Dialog label.
## 2026-08-20 Parity Correction ## 2026-08-20 Parity Correction
...@@ -83,8 +107,8 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`. ...@@ -83,8 +107,8 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`.
## Acceptance Evidence ## Acceptance Evidence
- Historical screenshots are regression context only. Current 1440x900, - Playwright acceptance ran at 1440x900, 1280x720, 768x1024 and 390x844. No
1280x720, 768x1024 and 390x844 acceptance is blocked until a browser connects. screenshot artifacts are retained in the repository.
- Login inputs resolve to the same 40px height at every target viewport. - Login inputs resolve to the same 40px height at every target viewport.
- Login source parity now covers the centered 28rem shell, mobile top - Login source parity now covers the centered 28rem shell, mobile top
placement, light/dark Xinyuan logo switching, direct captcha input, disabled placement, light/dark Xinyuan logo switching, direct captcha input, disabled
...@@ -94,6 +118,8 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`. ...@@ -94,6 +118,8 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`.
close with Escape after their exit transition. close with Escape after their exit transition.
- Light and dark roots, dark Modal surfaces and page-level overflow were checked - Light and dark roots, dark Modal surfaces and page-level overflow were checked
from computed styles. Every target viewport reported zero horizontal overflow. from computed styles. Every target viewport reported zero horizontal overflow.
- The evidence above predates the final native Pro Shell/DataGrid/DropZone pass. - The application switcher, dark overlay, Shell gutters, desktop offcanvas,
It remains useful regression context but is not claimed as final Pro browser mobile Sheet, user Dropdown and login are current post-migration evidence.
acceptance. - Full material booking approval remains in progress because the official
Autocomplete Popover's internal React Aria Dialog emits accessibility
warnings before the popup is opened.
# XinYuan Design System # XinYuan Design System
Updated: 2026-08-19 Updated: 2026-08-25
## Authority ## Authority
...@@ -62,6 +62,19 @@ DataGrid. Cards are reserved for discrete business objects and analytical tools. ...@@ -62,6 +62,19 @@ DataGrid. Cards are reserved for discrete business objects and analytical tools.
- Empty results stay empty and request failures stay errors. UI code must never - Empty results stay empty and request failures stay errors. UI code must never
populate dashboard or list surfaces with fabricated records or numbers. populate dashboard or list surfaces with fabricated records or numbers.
## Application Switcher
- `GET /info -> site.group` is the display-group source; each entry is
`{ label, applications: string[] }` and is matched to real accessible apps.
- `/application/info` and the current user's `applications` remain the app
source and fallback. Empty and inaccessible groups are omitted, unreferenced
accessible apps go to `其他应用`, and missing group configuration uses
`全部应用`.
- The Modal is 46rem wide and at most 42rem high, with 24px computed radius,
28px inset, body-only scrolling, one mobile column and exactly two columns
from `sm`. Cards use HeroUI `secondary`; identity wells use `Surface
tertiary + Avatar soft` at 36px.
## Geometry ## Geometry
- Shell gutters: 48px desktop, 32px tablet, 16px mobile. - Shell gutters: 48px desktop, 32px tablet, 16px mobile.
......
# XinYuan UI Audit # XinYuan UI Audit
Updated: 2026-08-20 Updated: 2026-08-25
## Repository Baseline ## Repository Baseline
...@@ -9,7 +9,7 @@ Updated: 2026-08-20 ...@@ -9,7 +9,7 @@ Updated: 2026-08-20
- Framework: Umi Max 4, React 19, Tailwind CSS 4 - Framework: Umi Max 4, React 19, Tailwind CSS 4
- Package manager: npm (`.umirc.ts` declares `npmClient: 'npm'`) - Package manager: npm (`.umirc.ts` declares `npmClient: 'npm'`)
- Base and public path: `/audit_material_manager/` - Base and public path: `/audit_material_manager/`
- Development baseline: `http://localhost:8002/audit_material_manager/` - Development baseline: `http://localhost:8000/audit_material_manager/`
- Application display name: `审计材料中心` - Application display name: `审计材料中心`
## Business Protection Boundary ## Business Protection Boundary
...@@ -36,7 +36,7 @@ The authoritative references are `src/services/record-api.md`, ...@@ -36,7 +36,7 @@ The authoritative references are `src/services/record-api.md`,
| `/resource/stat` | Material dashboard and charts | Pro KPI/Charts/DataGrid; 24px L1 Cards, toned KPI icon wells and three peer L1 compliance Cards | | `/resource/stat` | Material dashboard and charts | Pro KPI/Charts/DataGrid; 24px L1 Cards, toned KPI icon wells and three peer L1 compliance Cards |
| `/resource/booking` | Material archive, filters, table, upload and dialogs | HeroUI controls/Modal plus Pro DataGrid, ActionBar and DropZone | | `/resource/booking` | Material archive, filters, table, upload and dialogs | HeroUI controls/Modal plus Pro DataGrid, ActionBar and DropZone |
| Global shell | Sidebar, navbar, theme, user menu | Pro AppLayout, Sidebar and Navbar with offcanvas/mobile navigation | | Global shell | Sidebar, navbar, theme, user menu | Pro AppLayout, Sidebar and Navbar with offcanvas/mobile navigation |
| Application switcher | Cross-application modal | HeroUI Modal with loading, error, empty and fallback states | | Application switcher | Cross-application modal | HeroUI Modal with `/info -> site.group` sections, counts, loading, error, empty and fallback states |
## Dependency Audit ## Dependency Audit
...@@ -66,7 +66,8 @@ Production routes contain no Catalyst, Headless UI, ECharts, visible native ...@@ -66,7 +66,8 @@ Production routes contain no Catalyst, Headless UI, ECharts, visible native
select/date/file controls, legacy UI compatibility imports or page-owned select/date/file controls, legacy UI compatibility imports or page-owned
HeroUI appearance styles. The historical kit, unreachable workspace and unused HeroUI appearance styles. The historical kit, unreachable workspace and unused
decorative empty-state page were removed after production references reached decorative empty-state page were removed after production references reached
zero. Final interactive browser comparison remains outstanding. zero. The 2026-08-25 audit found and corrected the remaining application
switcher grouping gap without changing its application source or navigation.
## Theme Audit ## Theme Audit
...@@ -79,8 +80,9 @@ zero. Final interactive browser comparison remains outstanding. ...@@ -79,8 +80,9 @@ zero. Final interactive browser comparison remains outstanding.
## Browser Evidence ## Browser Evidence
- Earlier pre-correction Playwright evidence is superseded and is not current - Playwright is available. The synchronized unmodified target was recorded at
visual acceptance evidence. `http://localhost:8000/audit_material_manager/`; the login page loaded the
real captcha and reported zero console errors or warnings at 1440x900.
- The login document title is aligned in source to `审计材料中心 - 登录`. - The login document title is aligned in source to `审计材料中心 - 登录`.
- The login parity correction now uses the approved centered `rc-login-*` - The login parity correction now uses the approved centered `rc-login-*`
shell, 28rem panel, responsive mobile placement, red/white theme logos, shell, 28rem panel, responsive mobile placement, red/white theme logos,
...@@ -94,14 +96,27 @@ zero. Final interactive browser comparison remains outstanding. ...@@ -94,14 +96,27 @@ zero. Final interactive browser comparison remains outstanding.
icon geometry without changing API calls, record fields or callbacks. icon geometry without changing API calls, record fields or callbacks.
- Pre-Pro migration QA covered four target viewports, light/dark, representative - Pre-Pro migration QA covered four target viewports, light/dark, representative
populated data and zero final console errors/warnings. populated data and zero final console errors/warnings.
- After the Pro Shell/DataGrid/DropZone migration, the in-app Browser runtime - The target, `problem_rectification` and `model_analysis` were opened together
exposed no browser instance. New interactive browser acceptance remains to be on ports 8000, 8001 and 8002. Browser-only responses exercise authorized
repeated when that runtime is available; build and endpoint checks pass. application grouping without writing fixture data to the repository.
- The switcher computes to 358x648 with one column at 390x844, then 736x484
with exactly two columns at 768x1024, 1280x720 and 1440x900. Every viewport
keeps 24px radius, 28px inset and zero page overflow. The 1440x900 dark pass
resolves a dark theme Surface.
- Shell gutters compute to 16px mobile, 32px tablet and 48px desktop. Desktop
offcanvas moves the full 240px Sidebar outside the viewport. The 390px Pro
Sheet mobile sidebar is 312px wide, is named `主导航`, and closes with Escape,
backdrop or route action. The user Dropdown exposes the real logout action.
- Dashboard, shell and application-switcher passes report zero console errors
or warnings. Material booking reports zero errors but emits the known HeroUI
3.2.2 Autocomplete Popover internal Dialog-title warning for each mounted
Autocomplete. The public compound API does not expose that internal Dialog.
## Risks ## Risks
1. Browser discovery currently returns no available browser. Final light/dark 1. Full material-route approval remains in progress until the upstream HeroUI
comparison against `problem_rectification` is blocked until a browser is connected. Autocomplete internal Dialog warning can be removed without patching package
internals or bypassing the required public compound API.
2. UI migration must not change pagination query keys, record metadata shape, 2. UI migration must not change pagination query keys, record metadata shape,
soft-delete behavior, configurable options or login/captcha callbacks. soft-delete behavior, configurable options or login/captcha callbacks.
3. Empty and request-failure states now expose real API state only; no demo 3. Empty and request-failure states now expose real API state only; no demo
......
...@@ -300,7 +300,8 @@ ...@@ -300,7 +300,8 @@
padding: 0 var(--rc-navbar-inline-padding); padding: 0 var(--rc-navbar-inline-padding);
} }
.rc-navbar [data-slot='sidebar-trigger'] { .rc-navbar [data-slot='sidebar-trigger'],
.rc-navbar [data-slot='app-layout-menu-toggle'] {
width: var(--rc-navbar-trigger-size); width: var(--rc-navbar-trigger-size);
min-width: var(--rc-navbar-trigger-size); min-width: var(--rc-navbar-trigger-size);
max-width: var(--rc-navbar-trigger-size); max-width: var(--rc-navbar-trigger-size);
...@@ -740,8 +741,12 @@ ...@@ -740,8 +741,12 @@
} }
@media (max-width: 1023px) { @media (max-width: 1023px) {
.rc-app-shell,
.rc-app-main { .rc-app-main {
--rc-shell-inline-gutter: var(--rc-space-8); --rc-shell-inline-gutter: var(--rc-space-8);
--rc-navbar-inline-padding: var(--rc-space-8);
--rc-page-inline-start: var(--rc-space-8);
--rc-page-inline-end: var(--rc-space-8);
} }
.rc-sidebar { .rc-sidebar {
...@@ -761,8 +766,12 @@ ...@@ -761,8 +766,12 @@
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.rc-app-shell,
.rc-app-main { .rc-app-main {
--rc-shell-inline-gutter: var(--rc-space-4); --rc-shell-inline-gutter: var(--rc-space-4);
--rc-navbar-inline-padding: var(--rc-space-4);
--rc-page-inline-start: var(--rc-space-4);
--rc-page-inline-end: var(--rc-space-4);
--rc-page-padding-block-start: var(--rc-space-4); --rc-page-padding-block-start: var(--rc-space-4);
--rc-page-padding-block-end: var(--rc-space-6); --rc-page-padding-block-end: var(--rc-space-6);
--rc-page-section-gap: var(--rc-space-6); --rc-page-section-gap: var(--rc-space-6);
...@@ -1055,7 +1064,8 @@ ...@@ -1055,7 +1064,8 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.rc-navbar [data-slot='sidebar-trigger'] { .rc-navbar [data-slot='sidebar-trigger'],
.rc-navbar [data-slot='app-layout-menu-toggle'] {
width: var(--rc-navbar-icon-button-size); width: var(--rc-navbar-icon-button-size);
min-width: var(--rc-navbar-icon-button-size); min-width: var(--rc-navbar-icon-button-size);
max-width: var(--rc-navbar-icon-button-size); max-width: var(--rc-navbar-icon-button-size);
......
import { getApplications } from '@/services/DataService'; import { getApplications } from '@/services/DataService';
import { ExclamationCircleIcon, XMarkIcon } from '@/components/AppIcons'; import { ExclamationCircleIcon, XMarkIcon } from '@/components/AppIcons';
import xinyuanLogo from '@/assets/xy-logo-red.png';
import {
APPLICATION_BASE_PATH,
APPLICATION_DISPLAY_NAME,
} from '@/config/application';
import { EmptyState } from '@heroui-pro/react/empty-state'; import { EmptyState } from '@heroui-pro/react/empty-state';
import { import {
Alert, Alert,
...@@ -13,6 +18,8 @@ import { ...@@ -13,6 +18,8 @@ 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 normalizeLabelList(items) { function normalizeLabelList(items) {
if (!Array.isArray(items)) { if (!Array.isArray(items)) {
return []; return [];
...@@ -29,6 +36,21 @@ function normalizeLabelList(items) { ...@@ -29,6 +36,21 @@ function normalizeLabelList(items) {
.filter(Boolean); .filter(Boolean);
} }
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) { function resolveApplicationUrl(application) {
if (application?.name) { if (application?.name) {
return `/${application.name}`; return `/${application.name}`;
...@@ -39,22 +61,24 @@ function resolveApplicationUrl(application) { ...@@ -39,22 +61,24 @@ 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: title: current
application?.title || ? APPLICATION_DISPLAY_NAME
fallback?.title || : application?.title ||
application?.name || fallback?.title ||
fallback?.name || 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 ? xinyuanLogo : application?.logo || fallback?.logo || null,
url: resolveApplicationUrl({ url: resolveApplicationUrl({
name, name,
}), }),
...@@ -107,6 +131,60 @@ function mergeApplications(fetchedApplications, fallbackApplications) { ...@@ -107,6 +131,60 @@ 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;
...@@ -181,12 +259,12 @@ function ApplicationCard({ application }) { ...@@ -181,12 +259,12 @@ function ApplicationCard({ application }) {
</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 text-foreground" 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>
...@@ -216,6 +294,7 @@ function LoadingSkeleton() { ...@@ -216,6 +294,7 @@ function LoadingSkeleton() {
export default function ApplicationSwitcherDialog({ export default function ApplicationSwitcherDialog({
open, open,
onClose, onClose,
siteInfo,
userInfo, userInfo,
}) { }) {
const fallbackApplications = useMemo(() => { const fallbackApplications = useMemo(() => {
...@@ -240,6 +319,13 @@ export default function ApplicationSwitcherDialog({ ...@@ -240,6 +319,13 @@ export default function ApplicationSwitcherDialog({
fallbackApplications.length > 0 ? 'success' : 'idle', fallbackApplications.length > 0 ? 'success' : 'idle',
); );
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const currentApplicationName = useMemo(() => {
return applications.find(isCurrentApplication)?.name || '';
}, [applications]);
const applicationSections = useMemo(
() => buildApplicationSections(applications, siteInfo?.group),
[applications, siteInfo?.group],
);
useEffect(() => { useEffect(() => {
if (fallbackApplications.length === 0) { if (fallbackApplications.length === 0) {
...@@ -268,7 +354,7 @@ export default function ApplicationSwitcherDialog({ ...@@ -268,7 +354,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;
...@@ -300,29 +386,37 @@ export default function ApplicationSwitcherDialog({ ...@@ -300,29 +386,37 @@ export default function ApplicationSwitcherDialog({
}, [fallbackApplications, open]); }, [fallbackApplications, open]);
return ( return (
<Modal.Root isOpen={open} onOpenChange={(isOpen) => !isOpen && onClose?.()}> <Modal.Root>
<Modal.Trigger <Modal.Trigger
aria-hidden="true" aria-hidden="true"
className="sr-only" className="sr-only"
tabIndex={-1} tabIndex={-1}
/> />
<Modal.Backdrop variant="blur"> <Modal.Backdrop
<Modal.Container placement="center" scroll="inside" size="lg"> isOpen={open}
variant="blur"
onOpenChange={(isOpen) => !isOpen && onClose?.()}
>
<Modal.Container className="w-full sm:w-full" placement="center" scroll="inside" size="lg">
<Modal.Dialog <Modal.Dialog
className="max-h-[min(42rem,calc(100dvh-2rem))]" className="max-h-[min(42rem,calc(100dvh-2rem))] p-7"
style={{ maxWidth: 'min(46rem, calc(100vw - 2rem))' }} style={{
maxWidth: 'min(46rem, calc(100vw - 2rem))',
width: 'min(46rem, calc(100vw - 2rem))',
}}
> >
<Modal.CloseTrigger aria-label="关闭应用切换"> <Modal.CloseTrigger
aria-label="关闭应用切换"
style={{ insetInlineEnd: '1.75rem', top: '1.75rem' }}
>
<XMarkIcon size={16} /> <XMarkIcon size={16} />
</Modal.CloseTrigger> </Modal.CloseTrigger>
<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><ExclamationCircleIcon size={20} /></Alert.Indicator> <Alert.Indicator><ExclamationCircleIcon size={20} /></Alert.Indicator>
...@@ -335,12 +429,29 @@ export default function ApplicationSwitcherDialog({ ...@@ -335,12 +429,29 @@ 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 text-foreground">
/> {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>
) : ( ) : (
......
...@@ -5,7 +5,9 @@ import { ...@@ -5,7 +5,9 @@ import {
APPLICATION_DISPLAY_NAME, APPLICATION_DISPLAY_NAME,
} from "@/config/application"; } from "@/config/application";
import ApplicationSwitcherDialog from "@/pages/project_manager_xy/ApplicationSwitcherDialog"; import ApplicationSwitcherDialog from "@/pages/project_manager_xy/ApplicationSwitcherDialog";
import { SidebarPanel } from "@/components/AppIcons";
import { UserContext } from "@/wrapper/Auth"; import { UserContext } from "@/wrapper/Auth";
import { SiteContext } from "@/wrapper/Site";
import { import {
AppstoreOutlined, AppstoreOutlined,
DatabaseOutlined, DatabaseOutlined,
...@@ -17,6 +19,7 @@ import { ...@@ -17,6 +19,7 @@ import {
import { Avatar, Button, Dropdown, Label, Tooltip } from "@heroui/react"; import { Avatar, Button, Dropdown, Label, Tooltip } from "@heroui/react";
import { AppLayout } from "@heroui-pro/react/app-layout"; import { AppLayout } from "@heroui-pro/react/app-layout";
import { Navbar } from "@heroui-pro/react/navbar"; import { Navbar } from "@heroui-pro/react/navbar";
import { Sheet } from "@heroui-pro/react/sheet";
import { Sidebar, useSidebar } from "@heroui-pro/react/sidebar"; import { Sidebar, useSidebar } from "@heroui-pro/react/sidebar";
import { useContext, useEffect, useMemo, useState } from "react"; import { useContext, useEffect, useMemo, useState } from "react";
import { history, Outlet, useLocation } from "umi"; import { history, Outlet, useLocation } from "umi";
...@@ -198,8 +201,41 @@ function SidebarContent({ currentPath, onOpenApplicationDialog, userInfo }) { ...@@ -198,8 +201,41 @@ function SidebarContent({ currentPath, onOpenApplicationDialog, userInfo }) {
); );
} }
function MobileSidebar({ children }) {
const { isMobile, isMobileOpen, setMobileOpen, side } = useSidebar();
if (!isMobile) {
return null;
}
return (
<Sheet
isOpen={isMobileOpen}
placement={side}
onOpenChange={setMobileOpen}
>
<Sheet.Backdrop variant="blur">
<Sheet.Content className="h-full w-4/5">
<Sheet.Dialog
aria-label="主导航"
className="h-full"
>
<div
className="app-sidebar-glass rc-mobile-sidebar"
data-slot="sidebar-mobile"
>
{children}
</div>
</Sheet.Dialog>
</Sheet.Content>
</Sheet.Backdrop>
</Sheet>
);
}
export default function ProjectManagerLayout() { export default function ProjectManagerLayout() {
const location = useLocation(); const location = useLocation();
const siteInfo = useContext(SiteContext);
const userInfo = useContext(UserContext); const userInfo = useContext(UserContext);
const [theme, setTheme] = useState(() => getPreferredTheme()); const [theme, setTheme] = useState(() => getPreferredTheme());
const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false); const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false);
...@@ -256,7 +292,15 @@ export default function ProjectManagerLayout() { ...@@ -256,7 +292,15 @@ export default function ProjectManagerLayout() {
<Sidebar.Trigger <Sidebar.Trigger
className="app-navbar-trigger rc-navbar-trigger" className="app-navbar-trigger rc-navbar-trigger"
aria-label={isSidebarOpen ? "收起侧栏" : "展开侧栏"} aria-label={isSidebarOpen ? "收起侧栏" : "展开侧栏"}
/> >
<SidebarPanel size={16} />
</Sidebar.Trigger>
<AppLayout.MenuToggle
aria-label="打开导航"
className="app-navbar-trigger rc-navbar-trigger"
>
<SidebarPanel size={16} />
</AppLayout.MenuToggle>
<h1 className="app-navbar-page-title rc-navbar-title">{currentRoute.name}</h1> <h1 className="app-navbar-page-title rc-navbar-title">{currentRoute.name}</h1>
<Navbar.Spacer /> <Navbar.Spacer />
<div className="rc-navbar-actions"> <div className="rc-navbar-actions">
...@@ -286,9 +330,9 @@ export default function ProjectManagerLayout() { ...@@ -286,9 +330,9 @@ export default function ProjectManagerLayout() {
<Sidebar.Root className="app-sidebar-glass rc-sidebar"> <Sidebar.Root className="app-sidebar-glass rc-sidebar">
{sidebarNavigation} {sidebarNavigation}
</Sidebar.Root> </Sidebar.Root>
<Sidebar.Mobile className="app-sidebar-glass rc-mobile-sidebar"> <MobileSidebar>
{sidebarNavigation} {sidebarNavigation}
</Sidebar.Mobile> </MobileSidebar>
</> </>
} }
sidebarCollapsible="offcanvas" sidebarCollapsible="offcanvas"
...@@ -303,6 +347,7 @@ export default function ProjectManagerLayout() { ...@@ -303,6 +347,7 @@ export default function ProjectManagerLayout() {
<ApplicationSwitcherDialog <ApplicationSwitcherDialog
open={isApplicationDialogOpen} open={isApplicationDialogOpen}
onClose={() => setIsApplicationDialogOpen(false)} onClose={() => setIsApplicationDialogOpen(false)}
siteInfo={siteInfo}
userInfo={userInfo} userInfo={userInfo}
/> />
</AppLayout> </AppLayout>
......
...@@ -41,6 +41,9 @@ ...@@ -41,6 +41,9 @@
- 桌面端侧栏收起时完全移出视口,不保留图标栏;通过顶部导航栏侧栏按钮重新展开 - 桌面端侧栏收起时完全移出视口,不保留图标栏;通过顶部导航栏侧栏按钮重新展开
- 移动端通过顶部导航栏侧栏按钮打开抽屉,再进入应用切换 - 移动端通过顶部导航栏侧栏按钮打开抽屉,再进入应用切换
- 打开弹框后调用 `/application/info` 获取当前账号可访问应用,并与登录用户信息中的 `applications` 合并展示 - 打开弹框后调用 `/application/info` 获取当前账号可访问应用,并与登录用户信息中的 `applications` 合并展示
- 从现有 `GET /info -> site.group` 读取 `{ label, applications }` 展示分组;分组只组织已具备访问权限的真实应用,不改变鉴权结果
- 保持后端分组及组内应用顺序,隐藏空分组和无权限应用;未被分组引用的可访问应用进入“其他应用”,无分组配置时使用“全部应用”
- 每个分组显示真实可访问应用数量,移动端一列,`sm` 及以上固定两列
- 应用列表加载失败时,回退展示当前账号上的应用信息,并显示失败提示 - 应用列表加载失败时,回退展示当前账号上的应用信息,并显示失败提示
- 应用卡片与加载骨架在窄屏使用单列,从 `sm` 断点起固定使用两列,与“问题整改”应用保持一致 - 应用卡片与加载骨架在窄屏使用单列,从 `sm` 断点起固定使用两列,与“问题整改”应用保持一致
- 点击应用卡片后进入对应应用工作区 - 点击应用卡片后进入对应应用工作区
......
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