Commit 2abf70b5 authored by Andy-bubu's avatar Andy-bubu

fix: align application switcher with xinyuan baseline

parent 627f09e4
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
- Umi Max React 应用,base/publicPath 为 `/oa_process_monitor_new/`。 - Umi Max React 应用,base/publicPath 为 `/oa_process_monitor_new/`。
- `Site` 加载站点配置,`Auth` 维护用户和真实退出,再进入 `AppShell`。 - `Site` 加载站点配置,`Auth` 维护用户和真实退出,再进入 `AppShell`。
- 应用切换由 `SiteContext` 提供 `/info -> site.group`,按应用 `name` 与当前用户真实可访问应用匹配;分组仅负责展示顺序,不参与权限或跳转决策。
- 服务层继续拥有 API、Subject、payload、动态字段和业务转换;页面持有筛选、页码、选择、权限及回调。 - 服务层继续拥有 API、Subject、payload、动态字段和业务转换;页面持有筛选、页码、选择、权限及回调。
- 单个低频结构化筛选由列表工具栏的 HeroUI Popover 承载,Popover 内继续使用 HeroUI Autocomplete;父浮层保留子 Autocomplete Portal 的交互边界。 - 单个低频结构化筛选由列表工具栏的 HeroUI Popover 承载,Popover 内继续使用 HeroUI Autocomplete;父浮层保留子 Autocomplete Portal 的交互边界。
...@@ -11,6 +12,7 @@ ...@@ -11,6 +12,7 @@
- 基础组件直接来自 `@heroui/react`,Sidebar/Navbar/DataGrid/KPI/EmptyState 等直接来自 `@heroui-pro/react` 官方子路径。 - 基础组件直接来自 `@heroui/react`,Sidebar/Navbar/DataGrid/KPI/EmptyState 等直接来自 `@heroui-pro/react` 官方子路径。
- 共享组件只做业务无关组合:Shell、应用切换、Autocomplete、详情导航、标题说明和行操作。 - 共享组件只做业务无关组合:Shell、应用切换、Autocomplete、详情导航、标题说明和行操作。
- 应用切换直接使用 HeroUI Modal/Card/Surface/Avatar 与 HeroUI Pro EmptyState;Modal Body 是唯一滚动容器,桌面及平板固定两列、移动端一列。
- compound API 使用点语法,Button 使用 `onPress`;不提供 Catalyst/Headless 旧 props 兼容层。 - compound API 使用点语法,Button 使用 `onPress`;不提供 Catalyst/Headless 旧 props 兼容层。
- `XinyuanAutocomplete` 组合官方 Autocomplete、Popover Heading、SearchField 和 ListBox,不定义组件外观。 - `XinyuanAutocomplete` 组合官方 Autocomplete、Popover Heading、SearchField 和 ListBox,不定义组件外观。
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
- [x] 页面私有视觉 CSS 为零 - [x] 页面私有视觉 CSS 为零
- [x] 登录、Sidebar 真 offcanvas、Navbar 与 48/32/16 gutter - [x] 登录、Sidebar 真 offcanvas、Navbar 与 48/32/16 gutter
- [x] 用户 Dropdown 与原有真实 logout 回调 - [x] 用户 Dropdown 与原有真实 logout 回调
- [x] HeroUI Modal 应用切换、两列卡片、identity well 和当前状态 - [x] HeroUI Modal 应用切换、`site.group` 动态分组、真实数量、两列卡片、identity well 和唯一当前状态
## 页面 ## 页面
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
- Navbar 高 64px,直接使用 HeroUI Pro `Sidebar.Trigger` 默认图形。 - Navbar 高 64px,直接使用 HeroUI Pro `Sidebar.Trigger` 默认图形。
- Navbar 和页面共用左右 gutter:桌面 48px、平板 32px、手机 16px。 - Navbar 和页面共用左右 gutter:桌面 48px、平板 32px、手机 16px。
- Sidebar 用户入口通过 `Dropdown.Trigger` 打开菜单,退出必须调用现有真实 logout。 - Sidebar 用户入口通过 `Dropdown.Trigger` 打开菜单,退出必须调用现有真实 logout。
- 应用切换使用 HeroUI Modal,桌面宽 46rem/最大高 42rem,手机一列、`sm` 起固定两列。 - 应用切换使用 HeroUI Modal,桌面宽 46rem/最大高 42rem,手机一列、`sm` 起固定两列;分组来自 `/info -> site.group`,保留后端顺序并提供“其他应用/全部应用”回退。
## Surface 与布局 ## Surface 与布局
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
## 范围与基准 ## 范围与基准
- 目标仓库:`oa_process_monitor`,分支 `codex/xinyuan-ui-ux`。 - 目标仓库:`oa_process_monitor`,分支 `codex/xinyuan-ui-ux`。
- 唯一实现基准:本机 `problem_rectification` 的实际代码与运行页面。 - 整体实现基准:本机 `problem_rectification` 的实际代码与运行页面;应用切换专项基准为本机 `model_analysis` 当前实现。
- 路由基址:`/oa_process_monitor_new/`。 - 路由基址:`/oa_process_monitor_new/`。
- 活动路由:`/login`、`/process_monitor`、`/process_monitor/:processId`、`/process_monitor/:processId/:application`。 - 活动路由:`/login`、`/process_monitor`、`/process_monitor/:processId`、`/process_monitor/:processId/:application`。
- 业务依据:`src/services/record-api.md`、`src/services/subjectsDefinition.md`、`src/pages/process_monitor/process-monitor.business-flow.md`。 - 业务依据:`src/services/record-api.md`、`src/services/subjectsDefinition.md`、`src/pages/process_monitor/process-monitor.business-flow.md`。
...@@ -30,7 +30,8 @@ ...@@ -30,7 +30,8 @@
## 迁移后审计结论 ## 迁移后审计结论
- 登录、Shell、Sidebar、Navbar、用户退出菜单和应用切换均改为问题整改共享结构。 - 登录、Shell、Sidebar、Navbar 和用户退出菜单使用问题整改共享结构;应用切换同步 model_analysis 的 HeroUI Modal 几何、动态分组、真实数量和两列卡片结构。
- 应用切换消费现有 `SiteContext` 的 `/info -> site.group`,按应用名称匹配真实可访问应用,保持分组与组内顺序,并保留原有应用获取、合并、当前应用判断及跳转逻辑。
- 三个流程监控页面直接使用 HeroUI/HeroUI Pro;KPI 使用 Pro KPI,列表使用 Pro DataGrid,空状态使用 Pro EmptyState。 - 三个流程监控页面直接使用 HeroUI/HeroUI Pro;KPI 使用 Pro KPI,列表使用 Pro DataGrid,空状态使用 Pro EmptyState。
- 筛选使用 HeroUI SearchField/Autocomplete,弹窗使用 Modal/AlertDialog,分页固定 10 条并使用 compound Pagination。 - 筛选使用 HeroUI SearchField/Autocomplete,弹窗使用 Modal/AlertDialog,分页固定 10 条并使用 compound Pagination。
- 详情查看态只保留一个 `Surface secondary`;页面不再定义组件背景、颜色、描边、圆角、阴影或交互状态。 - 详情查看态只保留一个 `Surface secondary`;页面不再定义组件背景、颜色、描边、圆角、阴影或交互状态。
...@@ -48,6 +49,8 @@ ...@@ -48,6 +49,8 @@
## 浏览器证据与限制 ## 浏览器证据与限制
- 已覆盖 1440x900、1280x720、768x1024、390x844;包含浅色、深色、Sidebar offcanvas、移动 Sidebar、应用切换和创建分组 Modal。 - 已覆盖 1440x900、1280x720、768x1024、390x844;包含浅色、深色、Sidebar offcanvas、移动 Sidebar、应用切换和创建分组 Modal。
- 2026-08-26 应用切换专项复验同时打开目标应用与 `model_analysis`:四个视口的弹窗位置、宽高、24px 圆角、28px 内边距、Header/Body 起点、分组顶点、两列/单列断点及卡片尺寸逐项一致;深色 Portal 无浅色孤岛。
- 浏览器层覆盖动态分组顺序、组内顺序、空分组隐藏、未归组应用进入“其他应用”、无分组回退“全部应用”、真实数量、唯一“当前”、长列表 Body-only 滚动、失败/透明/缺失 Logo 稳定 identity well,以及 Escape、遮罩、关闭按钮和焦点顺序。
- 最终目标与基准应用均已在 1440x900、1280x720、768x1024、390x844 的浅色和深色模式完成本地截图比对;验收截图仅作本地临时证据,不纳入版本控制。 - 最终目标与基准应用均已在 1440x900、1280x720、768x1024、390x844 的浅色和深色模式完成本地截图比对;验收截图仅作本地临时证据,不纳入版本控制。
- 另记录 `final-production-app-switcher-1440x900-light.png`、`final-production-app-switcher-1440x900-three-apps.png` 和移动 Sidebar 截图;在相同视口逐项比较登录、Shell、页面 gutter、Surface、KPI、筛选、空 DataGrid、Pagination 和 overlay 几何。 - 另记录 `final-production-app-switcher-1440x900-light.png`、`final-production-app-switcher-1440x900-three-apps.png` 和移动 Sidebar 截图;在相同视口逐项比较登录、Shell、页面 gutter、Surface、KPI、筛选、空 DataGrid、Pagination 和 overlay 几何。
- 干净退出会话与应用切换会话的浏览器控制台均为 0 error / 0 warning;Autocomplete、Modal、应用切换和移动 Sidebar 的可访问名称已通过生产预览快照确认。 - 干净退出会话与应用切换会话的浏览器控制台均为 0 error / 0 warning;Autocomplete、Modal、应用切换和移动 Sidebar 的可访问名称已通过生产预览快照确认。
......
...@@ -96,6 +96,57 @@ function mergeApplications(fetchedApplications, fallbackApplications) { ...@@ -96,6 +96,57 @@ 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-${index}-${group?.label || 'application-group'}`,
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;
...@@ -153,12 +204,12 @@ function ApplicationCard({ application, current }) { ...@@ -153,12 +204,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>
...@@ -188,6 +239,7 @@ function LoadingSkeleton() { ...@@ -188,6 +239,7 @@ function LoadingSkeleton() {
export default function ApplicationSwitcherDialog({ export default function ApplicationSwitcherDialog({
open, open,
onClose, onClose,
siteInfo,
userInfo, userInfo,
}) { }) {
const fallbackApplications = useMemo(() => { const fallbackApplications = useMemo(() => {
...@@ -215,6 +267,10 @@ export default function ApplicationSwitcherDialog({ ...@@ -215,6 +267,10 @@ export default function ApplicationSwitcherDialog({
const acceptedNames = new Set([baseName, baseName.replace(/_new$/, '')]); const acceptedNames = new Set([baseName, baseName.replace(/_new$/, '')]);
return applications.find((application) => acceptedNames.has(application.name) || isCurrentApplication(application))?.name || ''; return applications.find((application) => acceptedNames.has(application.name) || isCurrentApplication(application))?.name || '';
}, [applications]); }, [applications]);
const applicationSections = useMemo(
() => buildApplicationSections(applications, siteInfo?.group),
[applications, siteInfo?.group],
);
useEffect(() => { useEffect(() => {
if (fallbackApplications.length === 0) { if (fallbackApplications.length === 0) {
...@@ -287,21 +343,24 @@ export default function ApplicationSwitcherDialog({ ...@@ -287,21 +343,24 @@ export default function ApplicationSwitcherDialog({
<Modal.Container className="w-full sm:w-full" placement="center" scroll="inside" size="lg"> <Modal.Container className="w-full sm:w-full" placement="center" scroll="inside" size="lg">
<Modal.Dialog <Modal.Dialog
aria-label="应用切换" aria-label="应用切换"
className="max-h-[min(42rem,calc(100dvh-2rem))]" className="p-7"
style={{ maxWidth: 'min(46rem, calc(100vw - 2rem))' }} style={{
width: 'min(46rem, calc(100vw - 2rem))',
maxWidth: 'min(46rem, calc(100vw - 2rem))',
maxHeight: 'min(42rem, calc(100dvh - 2rem))',
}}
> >
<Modal.CloseTrigger aria-label="关闭"> <Modal.CloseTrigger
aria-label="关闭"
style={{ insetInlineEnd: '1.75rem', top: '1.75rem' }}
>
<Close size={16} /> <Close 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><ExclamationTriangleIcon size={20} /></Alert.Indicator> <Alert.Indicator><ExclamationTriangleIcon size={20} /></Alert.Indicator>
...@@ -312,13 +371,23 @@ export default function ApplicationSwitcherDialog({ ...@@ -312,13 +371,23 @@ 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">{section.label}</h3>
current={application.name === currentApplicationName} <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>
) : ( ) : (
......
...@@ -36,6 +36,11 @@ ...@@ -36,6 +36,11 @@
3. `/process_monitor/:processId/:application` 3. `/process_monitor/:processId/:application`
单条流程实例详情 单条流程实例详情
Shell 中的应用切换入口继续读取当前登录用户可访问应用,并结合
`GET /info -> site.group` 动态分组:按后端分组及组内顺序展示非空分组,
未被引用的可访问应用归入“其他应用”,没有分组配置时回退为“全部应用”。
弹窗只改变展示结构,不改变应用权限、当前应用判断和跳转逻辑。
--- ---
## 4. 核心对象与动作 ## 4. 核心对象与动作
......
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