feat: update desktop workflows and app center
This commit is contained in:
143
src/pages/ProductCenter/index.tsx
Normal file
143
src/pages/ProductCenter/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ArrowLeft, ExternalLink, Loader2, RefreshCcw, ShoppingBag } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useYinianStore } from '@/stores/yinian';
|
||||
import { buildProductCenterSrc, getProductCenterBaseUrl } from './url';
|
||||
|
||||
const PRODUCT_CENTER_PARTITION = 'persist:yinian-travel-resource-ordering';
|
||||
|
||||
type ProductCenterWebviewElement = HTMLElement & {
|
||||
reload?: () => void;
|
||||
};
|
||||
|
||||
export function ProductCenter() {
|
||||
const { t } = useTranslation('appCenter');
|
||||
const navigate = useNavigate();
|
||||
const session = useYinianStore((state) => state.session);
|
||||
const webviewRef = useRef<ProductCenterWebviewElement | null>(null);
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const baseUrl = useMemo(() => getProductCenterBaseUrl(), []);
|
||||
const embeddedSrc = useMemo(() => buildProductCenterSrc({
|
||||
baseUrl,
|
||||
reloadKey,
|
||||
session,
|
||||
}), [baseUrl, reloadKey, session]);
|
||||
|
||||
useEffect(() => {
|
||||
const webview = webviewRef.current;
|
||||
if (!webview) return;
|
||||
|
||||
const handleStart = () => setIsLoading(true);
|
||||
const handleStop = () => setIsLoading(false);
|
||||
|
||||
webview.addEventListener('did-start-loading', handleStart);
|
||||
webview.addEventListener('did-stop-loading', handleStop);
|
||||
webview.addEventListener('did-fail-load', handleStop);
|
||||
|
||||
return () => {
|
||||
webview.removeEventListener('did-start-loading', handleStart);
|
||||
webview.removeEventListener('did-stop-loading', handleStop);
|
||||
webview.removeEventListener('did-fail-load', handleStop);
|
||||
};
|
||||
}, [reloadKey]);
|
||||
|
||||
const reloadApp = () => {
|
||||
setIsLoading(true);
|
||||
if (typeof webviewRef.current?.reload === 'function') {
|
||||
webviewRef.current.reload();
|
||||
return;
|
||||
}
|
||||
setReloadKey((value) => value + 1);
|
||||
};
|
||||
|
||||
const openExternal = () => {
|
||||
void window.electron.openExternal(baseUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="product-center-page" className="-m-6 flex h-[calc(100vh-2.5rem)] min-h-0 flex-col overflow-hidden bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50">
|
||||
<header className="grid h-14 shrink-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-3 border-b border-slate-200/80 bg-white/70 px-4 backdrop-blur dark:border-white/10 dark:bg-slate-950/80">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate('/app-center')}
|
||||
data-testid="product-center-back"
|
||||
className="shrink-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t('productCenter.back')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={reloadApp}
|
||||
aria-label={t('productCenter.reload')}
|
||||
title={t('productCenter.reload')}
|
||||
className="h-9 w-9 shrink-0"
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="min-w-0">
|
||||
<h1 className="truncate text-base font-semibold tracking-normal">
|
||||
{t('productCenter.title')}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center justify-self-center rounded-lg border border-slate-200/70 bg-white/60 px-3 py-1.5 text-xs font-medium text-slate-600 dark:border-white/10 dark:bg-white/5 dark:text-slate-300">
|
||||
<ShoppingBag className="mr-1.5 h-3.5 w-3.5 text-[#075985] dark:text-blue-200" />
|
||||
<span className="truncate">{t('productCenter.badge')}</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={openExternal}
|
||||
data-testid="product-center-open-external"
|
||||
className="shrink-0"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
{t('productCenter.openExternal')}
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="relative min-h-0 flex-1 overflow-hidden bg-white dark:bg-slate-950">
|
||||
<webview
|
||||
key={reloadKey}
|
||||
ref={webviewRef}
|
||||
data-testid="product-center-frame"
|
||||
src={embeddedSrc}
|
||||
partition={PRODUCT_CENTER_PARTITION}
|
||||
title={t('productCenter.frameTitle')}
|
||||
className="absolute inset-0 h-full w-full border-0 bg-white dark:bg-slate-950"
|
||||
allowpopups={false}
|
||||
webpreferences="contextIsolation=yes,nodeIntegration=no"
|
||||
/>
|
||||
{isLoading && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white/92 p-6 text-center dark:bg-slate-950/92">
|
||||
<div className="max-w-md">
|
||||
<div className="mx-auto flex h-11 w-11 items-center justify-center rounded-lg bg-[#EAF5FA] text-[#075985] dark:bg-[#1E3A8A]/30 dark:text-blue-100">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
<div className="mt-4 text-base font-semibold text-slate-950 dark:text-slate-50">
|
||||
{t('productCenter.loading')}
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
{t('productCenter.loadingDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProductCenter;
|
||||
Reference in New Issue
Block a user