Commit 5206d42b authored by Andy-bubu's avatar Andy-bubu

feat: align application switcher with Xinyuan standard

parent 0b9490a0
import xinyuanLogo from '@/assets/xy-logo-red.png';
import {
APPLICATION_BASE_PATH,
APPLICATION_DISPLAY_NAME,
} from '@/config/application';
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 {
Alert,
......@@ -13,6 +18,23 @@ import {
} from '@heroui/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) {
if (!Array.isArray(items)) {
return [];
......@@ -39,17 +61,20 @@ function resolveApplicationUrl(application) {
function normalizeApplication(application, fallback = {}) {
const name = application?.name || fallback?.name || '';
const current = name === CURRENT_APPLICATION_NAME;
return {
name,
title: application?.title || fallback?.title || application?.name || fallback?.name || '未命名应用',
title: current
? APPLICATION_DISPLAY_NAME
: application?.title || fallback?.title || application?.name || fallback?.name || '未命名应用',
description:
application?.description ||
fallback?.description ||
fallback?.title ||
application?.title ||
'进入对应应用继续处理业务',
logo: application?.logo || fallback?.logo || null,
logo: current ? xinyuanLogo : application?.logo || fallback?.logo || null,
url: resolveApplicationUrl({
name,
}),
......@@ -85,6 +110,15 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
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) => {
const orderDifference = Number(left.order || 0) - Number(right.order || 0);
if (orderDifference !== 0) {
......@@ -95,6 +129,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) {
if (typeof window === 'undefined') {
return false;
......@@ -151,12 +239,12 @@ function ApplicationCard({ application, current }) {
</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 text-foreground" title={application.title}>
{application.title}
</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 || '应用'}
</span>
</Description>
</span>
{current ? <Chip color="accent" size="sm">当前</Chip> : null}
</a>
......@@ -186,6 +274,7 @@ function LoadingSkeleton() {
export default function ApplicationSwitcherDialog({
open,
onClose,
siteInfo,
userInfo,
}) {
const fallbackApplications = useMemo(() => {
......@@ -203,7 +292,9 @@ export default function ApplicationSwitcherDialog({
.filter((application) => application.name);
}, [userInfo]);
const [applications, setApplications] = useState(fallbackApplications);
const [applications, setApplications] = useState(() =>
mergeApplications([], fallbackApplications),
);
const [status, setStatus] = useState(
fallbackApplications.length > 0 ? 'success' : 'idle',
);
......@@ -211,19 +302,19 @@ export default function ApplicationSwitcherDialog({
const currentApplicationName = useMemo(() => {
return applications.find(isCurrentApplication)?.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;
});
setApplications((previous) =>
mergeApplications(previous, fallbackApplications),
);
}, [fallbackApplications]);
useEffect(() => {
......@@ -239,7 +330,7 @@ export default function ApplicationSwitcherDialog({
try {
const response = await getApplications();
const fetchedApplications = Array.isArray(response) ? response : [];
const fetchedApplications = unwrapApplications(response);
if (cancelled) {
return;
......@@ -257,7 +348,7 @@ export default function ApplicationSwitcherDialog({
return;
}
setApplications(fallbackApplications);
setApplications(mergeApplications([], fallbackApplications));
setStatus('error');
setErrorMessage('应用列表加载失败,已回退展示当前账号上的应用信息。');
}
......@@ -282,21 +373,21 @@ export default function ApplicationSwitcherDialog({
>
<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))' }}
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="关闭">
<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>
<Modal.Heading>应用切换</Modal.Heading>
<div className="text-sm text-muted">选择你可访问的应用</div>
</Modal.Header>
<Modal.Body>
<Modal.Body style={{ marginTop: '0.75rem' }}>
{status === 'error' && errorMessage ? (
<Alert status="warning">
<Alert.Indicator><ExclamationTriangleIcon size={20} /></Alert.Indicator>
......@@ -307,13 +398,27 @@ export default function ApplicationSwitcherDialog({
{status === 'loading' && applications.length === 0 ? (
<LoadingSkeleton />
) : applications.length > 0 ? (
<div className="grid gap-3 sm:grid-cols-2">
{applications.map((application) => (
<ApplicationCard
key={application.name}
application={application}
current={application.name === currentApplicationName}
/>
<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 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>
) : (
......
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