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 { 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,17 +61,20 @@ function resolveApplicationUrl(application) { ...@@ -39,17 +61,20 @@ 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 ? xinyuanLogo : application?.logo || fallback?.logo || null,
url: resolveApplicationUrl({ url: resolveApplicationUrl({
name, name,
}), }),
...@@ -85,6 +110,15 @@ function mergeApplications(fetchedApplications, fallbackApplications) { ...@@ -85,6 +110,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,6 +129,60 @@ function mergeApplications(fetchedApplications, fallbackApplications) { ...@@ -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) { function isCurrentApplication(application) {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return false; return false;
...@@ -151,12 +239,12 @@ function ApplicationCard({ application, current }) { ...@@ -151,12 +239,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 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>
...@@ -186,6 +274,7 @@ function LoadingSkeleton() { ...@@ -186,6 +274,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,7 +292,9 @@ export default function ApplicationSwitcherDialog({ ...@@ -203,7 +292,9 @@ 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',
); );
...@@ -211,19 +302,19 @@ export default function ApplicationSwitcherDialog({ ...@@ -211,19 +302,19 @@ export default function ApplicationSwitcherDialog({
const currentApplicationName = useMemo(() => { const currentApplicationName = useMemo(() => {
return applications.find(isCurrentApplication)?.name || ''; return applications.find(isCurrentApplication)?.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 +330,7 @@ export default function ApplicationSwitcherDialog({ ...@@ -239,7 +330,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 +348,7 @@ export default function ApplicationSwitcherDialog({ ...@@ -257,7 +348,7 @@ export default function ApplicationSwitcherDialog({
return; return;
} }
setApplications(fallbackApplications); setApplications(mergeApplications([], fallbackApplications));
setStatus('error'); setStatus('error');
setErrorMessage('应用列表加载失败,已回退展示当前账号上的应用信息。'); setErrorMessage('应用列表加载失败,已回退展示当前账号上的应用信息。');
} }
...@@ -282,21 +373,21 @@ export default function ApplicationSwitcherDialog({ ...@@ -282,21 +373,21 @@ 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
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
<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>
<Description> <div className="text-sm text-muted">选择你可访问的应用</div>
选择你可访问的应用
</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,8 +398,19 @@ export default function ApplicationSwitcherDialog({ ...@@ -307,8 +398,19 @@ export default function ApplicationSwitcherDialog({
{status === 'loading' && applications.length === 0 ? ( {status === 'loading' && applications.length === 0 ? (
<LoadingSkeleton /> <LoadingSkeleton />
) : applications.length > 0 ? ( ) : 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 text-foreground">
{section.label}
</h3>
<Description>
{section.applications.length} 个应用
</Description>
</div>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
{applications.map((application) => ( {section.applications.map((application) => (
<ApplicationCard <ApplicationCard
key={application.name} key={application.name}
application={application} application={application}
...@@ -316,6 +418,9 @@ export default function ApplicationSwitcherDialog({ ...@@ -316,6 +418,9 @@ export default function ApplicationSwitcherDialog({
/> />
))} ))}
</div> </div>
</section>
))}
</div>
) : ( ) : (
<EmptyState size="md"> <EmptyState size="md">
<EmptyState.Header> <EmptyState.Header>
......
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