feat: add plugin marketplace renderer
This commit is contained in:
1 parent
8b6824a8ba
commit
97c9ad1aad
27 files changed
+1585
-23
No files matched your search
@@ -0,0 +1,98 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AlertCircle, Download, PackageCheck, RefreshCw, Trash2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { MarketplaceInstallation, MarketplaceLibrarySnapshot } from '@/lib/plugin-marketplace';
|
||||
import { pluginMarketplaceStore, usePluginMarketplaceStore } from '@/stores/plugin-marketplace';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
function failureText(reason?: string): string | null {
|
||||
if (!reason) return null;
|
||||
const messages: string[] = [];
|
||||
if (reason.includes('signature')) messages.push('签名校验失败,旧版本仍保留。');
|
||||
if (reason.includes('yanked') || reason.includes('not_ready')) messages.push('版本已撤回或未就绪。');
|
||||
if (reason.includes('incompatible')) messages.push('当前 MakeLore 版本不兼容。');
|
||||
if (messages.length === 0) messages.push('当前版本暂不可用,旧版本仍保留。');
|
||||
return messages.join(' ');
|
||||
}
|
||||
|
||||
function actionErrorText(error?: string | null): string | null {
|
||||
if (!error) return null;
|
||||
if (error.includes('plugin_artifact_invalid')) return '插件签名或包校验失败,设备上此前可用版本仍保留。';
|
||||
if (error.includes('plugin_release_not_ready')) return '版本已撤回、尚未就绪或与当前 MakeLore 不兼容;不会替换此前可用版本。';
|
||||
return error;
|
||||
}
|
||||
|
||||
export type MyPluginsViewProps = {
|
||||
library: MarketplaceLibrarySnapshot | null;
|
||||
installations: Record<string, MarketplaceInstallation>;
|
||||
state: 'idle' | 'loading' | 'ready' | 'error';
|
||||
error?: string | null;
|
||||
pending: Record<string, true>;
|
||||
onRefresh(): void | Promise<void>;
|
||||
onInstall(pluginId: string): void | Promise<void>;
|
||||
onUpdate(pluginId: string): void | Promise<void>;
|
||||
onUninstall(pluginId: string): void | Promise<void>;
|
||||
onRemove(pluginId: string): void | Promise<void>;
|
||||
onReacquire(pluginId: string): void | Promise<void>;
|
||||
};
|
||||
|
||||
export function MyPluginsView(props: MyPluginsViewProps) {
|
||||
const visibleError = actionErrorText(props.error);
|
||||
return (
|
||||
<main data-testid="my-plugins-page" className="mx-auto w-full max-w-7xl text-foreground">
|
||||
<header className="flex flex-wrap items-start justify-between gap-4"><div><h1 className="text-balance text-3xl font-semibold tracking-[-0.03em]">我的插件</h1><p className="mt-2 max-w-2xl text-pretty text-sm text-muted-foreground">账号插件库与本机下载分开管理。下载不会自动启用到项目,也不会分配给伙伴。</p></div><Button type="button" variant="outline" className="min-h-10" onClick={() => void props.onRefresh()}><RefreshCw className="mr-2 h-4 w-4" />刷新</Button></header>
|
||||
{props.library?.stale ? <p role="status" className="mt-4 rounded-xl bg-warning/10 p-3 text-pretty text-sm">正在显示上次可信的插件库;服务恢复前,在线计量或托管能力保持不可用。</p> : null}
|
||||
{props.state === 'loading' && !props.library ? <p role="status" className="mt-8 text-sm">正在读取我的插件…</p> : null}
|
||||
{props.state === 'error' && !props.library ? <div role="alert" className="mt-8 rounded-2xl bg-warning/10 p-5"><h2 className="font-semibold">无法读取我的插件</h2><p className="mt-2 text-pretty text-sm">{props.error ?? '请稍后重试。'}</p></div> : null}
|
||||
{visibleError && props.library ? <p role="alert" className="mt-4 rounded-xl bg-warning/10 p-3 text-pretty text-sm">{visibleError}</p> : null}
|
||||
{props.library && props.library.items.length === 0 ? <div className="mt-8 rounded-2xl bg-surface-subtle p-6 text-center"><PackageCheck className="mx-auto h-8 w-8 text-muted-foreground" /><h2 className="mt-3 text-balance font-semibold">账号插件库还是空的</h2><p className="mt-1 text-pretty text-sm text-muted-foreground">前往插件中心免费获取需要的插件。</p><Button asChild className="mt-4 min-h-10"><Link to="/plugin-marketplace">浏览插件中心</Link></Button></div> : null}
|
||||
<section aria-label="账号插件库" className="mt-6 space-y-4">
|
||||
{props.library?.items.map((plugin) => {
|
||||
const installed = props.installations[plugin.pluginId];
|
||||
const removed = plugin.removedAt !== null;
|
||||
const suspended = plugin.runtimeStatus === 'suspended';
|
||||
const retired = plugin.catalogStatus === 'retired';
|
||||
const updateAvailable = Boolean(installed?.version && plugin.stableVersion && installed.version !== plugin.stableVersion);
|
||||
const busy = Object.keys(props.pending).some((key) => key.endsWith(`:${plugin.pluginId}`));
|
||||
const failure = failureText(installed?.reason);
|
||||
return <article key={plugin.pluginId} className="rounded-2xl bg-background p-5 shadow-soft ring-1 ring-border/70">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4"><div><div className="flex flex-wrap items-center gap-2"><h2 className="text-balance text-xl font-semibold">{plugin.title}</h2>{plugin.acquisition === 'system_included' ? <Badge variant="outline">系统内置</Badge> : removed ? <Badge variant="outline">已移除</Badge> : <Badge variant="secondary">已获取</Badge>}</div><p className="mt-2 text-pretty text-sm text-muted-foreground">{plugin.summary}</p></div><div className="text-right text-sm"><p className="tabular-nums">稳定版 {plugin.stableVersion ?? '不可用'}</p>{installed?.version ? <p className="mt-1 tabular-nums text-muted-foreground">设备版本 {installed.version}</p> : <p className="mt-1 text-muted-foreground">尚未下载到设备</p>}</div></div>
|
||||
<div className="mt-4 space-y-1 text-pretty text-sm">{suspended ? <p className="text-destructive">运行已暂停</p> : null}{retired ? <p>{removed ? '已退役,移除后不可重新获取' : '已退役;现有账号插件库仍可继续使用受支持版本'}</p> : null}{failure ? <p className="text-destructive"><AlertCircle className="mr-1 inline h-4 w-4" />{failure}</p> : null}</div>
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{removed ? <Button type="button" className="min-h-10" disabled={busy || retired || suspended} aria-label={`重新免费获取${plugin.title}`} onClick={() => void props.onReacquire(plugin.pluginId)}>重新免费获取</Button>
|
||||
: !installed ? <Button type="button" className="min-h-10" disabled={busy || suspended} aria-label={`下载${plugin.title}`} onClick={() => void props.onInstall(plugin.pluginId)}><Download className="mr-2 h-4 w-4" />下载</Button>
|
||||
: updateAvailable ? <Button type="button" className="min-h-10" disabled={busy || suspended} aria-label={`更新${plugin.title}`} onClick={() => void props.onUpdate(plugin.pluginId)}><RefreshCw className="mr-2 h-4 w-4" />更新</Button>
|
||||
: <span className="inline-flex min-h-10 items-center px-2 text-sm font-medium"><PackageCheck className="mr-2 h-4 w-4 text-brand" />设备版本已是最新</span>}
|
||||
{!removed && plugin.acquisition !== 'system_included' ? <Button type="button" variant="outline" className="min-h-10" disabled={busy} aria-label={`移除${plugin.title}`} onClick={() => void props.onRemove(plugin.pluginId)}><Trash2 className="mr-2 h-4 w-4" />移除</Button> : null}
|
||||
{installed ? <Button type="button" variant="ghost" className="min-h-10" disabled={busy} aria-label={`删除设备上的${plugin.title}`} onClick={() => void props.onUninstall(plugin.pluginId)}>删除设备包</Button> : null}
|
||||
<Button asChild variant="outline" className="min-h-10"><Link to="/project-plugins">启用到项目</Link></Button>
|
||||
<Button asChild variant="outline" className="min-h-10"><Link to="/project-config">分配给伙伴</Link></Button>
|
||||
</div>
|
||||
</article>;
|
||||
})}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export function MyPlugins() {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const accountKey = user?.userId ?? user?.username ?? null;
|
||||
const library = usePluginMarketplaceStore((state) => state.library);
|
||||
const installations = usePluginMarketplaceStore((state) => state.installations);
|
||||
const state = usePluginMarketplaceStore((value) => value.libraryState);
|
||||
const error = usePluginMarketplaceStore((value) => value.libraryError);
|
||||
const pending = usePluginMarketplaceStore((value) => value.pending);
|
||||
useEffect(() => {
|
||||
pluginMarketplaceStore.getState().activateAccount(accountKey);
|
||||
if (accountKey) void pluginMarketplaceStore.getState().loadLibrary().catch(() => undefined);
|
||||
}, [accountKey]);
|
||||
const safe = (operation: Promise<void>) => operation.catch((reason) => {
|
||||
toast.error(reason instanceof Error ? reason.message : String(reason));
|
||||
});
|
||||
const store = pluginMarketplaceStore.getState;
|
||||
return <MyPluginsView library={library} installations={installations} state={state} error={error} pending={pending} onRefresh={() => safe(store().loadLibrary())} onInstall={(id) => safe(store().install(id))} onUpdate={(id) => safe(store().update(id))} onUninstall={(id) => safe(store().uninstall(id))} onRemove={(id) => safe(store().remove(id))} onReacquire={(id) => safe(store().acquire(id))} />;
|
||||
}
|
||||
Reference in new issue
Block a user