模板库卡片按安装状态收口并改用浮层提示
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- 已下载且版本一致的模板不再显示下载入口,只保留「使用模板」;版本落后显示「更新」,缺包显示「下载」 - 下载完成、开始建项目等过程提示改为浮层 toast(复用 PlatformRuntimeStatusToast,2.6 秒自动消失),不再占用页面内位置;页面内只保留错误与空态 - 补充/更新页面单测:已下载无下载按钮、落后显示更新、toast 渲染在页面外并自动消失 - 同步技术方案与里程碑的交互口径
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { PlatformRuntimeStatusToast } from '@genarrative/shared/components';
|
||||
import {
|
||||
ArrowLeft,
|
||||
BadgeCheck,
|
||||
@@ -9,10 +10,13 @@ import {
|
||||
SearchX,
|
||||
SlidersHorizontal,
|
||||
} from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { GameTemplateEntry } from '../../features/template-library/templateLibraryModel';
|
||||
import {
|
||||
formatGameTemplateSize,
|
||||
needsTemplateDownload,
|
||||
templateRuntimeLabel,
|
||||
} from '../../features/template-library/templateLibraryModel';
|
||||
import type { TemplateLibraryController } from '../../features/template-library/useTemplateLibrary';
|
||||
@@ -22,6 +26,51 @@ type TemplateLibraryViewProps = {
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
const TEMPLATE_LIBRARY_TOAST_MILLIS = 2600;
|
||||
|
||||
/**
|
||||
* 模板库提示统一走浮层 toast:下载完成、开始建项目这类过程提示不再占用页面内位置,
|
||||
* 页面里只保留可操作的错误与空态。
|
||||
*/
|
||||
function TemplateLibraryToast({
|
||||
message,
|
||||
onDismiss,
|
||||
}: {
|
||||
message: string;
|
||||
onDismiss: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
const timer = window.setTimeout(onDismiss, TEMPLATE_LIBRARY_TOAST_MILLIS);
|
||||
return () => {
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [message, onDismiss]);
|
||||
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
return createPortal(
|
||||
<div
|
||||
className="pointer-events-none fixed bottom-8 left-1/2 z-[1100] -translate-x-1/2"
|
||||
data-template-library-toast="true"
|
||||
>
|
||||
<PlatformRuntimeStatusToast
|
||||
tone="success"
|
||||
surface="solid"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
className="shadow-lg"
|
||||
>
|
||||
{message}
|
||||
</PlatformRuntimeStatusToast>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
const chipClass =
|
||||
'cursor-pointer rounded-full border border-(--platform-subpanel-border) bg-transparent px-2.5 py-1 text-[11px] text-(--platform-text-soft) transition hover:border-(--platform-warm-text) hover:text-(--platform-warm-text)';
|
||||
const activeChipClass =
|
||||
@@ -41,6 +90,7 @@ function TemplateCard({
|
||||
onUse: (template: GameTemplateEntry) => void;
|
||||
}) {
|
||||
const busy = busyTemplateId === template.id;
|
||||
const needsDownload = needsTemplateDownload(template);
|
||||
const busyLabel =
|
||||
busy && busyKind === 'download'
|
||||
? '正在下载模板'
|
||||
@@ -118,19 +168,26 @@ function TemplateCard({
|
||||
)}
|
||||
使用模板
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-(--platform-subpanel-border) bg-transparent px-2.5 py-1.5 text-[11px] text-(--platform-text-soft) disabled:cursor-default disabled:opacity-50"
|
||||
disabled={busy}
|
||||
onClick={() => onDownload(template)}
|
||||
>
|
||||
{busy && busyKind === 'download' ? (
|
||||
<Loader2 className="animate-spin" size={12} aria-hidden="true" />
|
||||
) : (
|
||||
<Download size={12} aria-hidden="true" />
|
||||
)}
|
||||
下载
|
||||
</button>
|
||||
{/* 已下载且版本一致时不再提供下载入口;只有缺包或版本落后才显示(落后时按「更新」)。 */}
|
||||
{needsDownload ? (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-(--platform-subpanel-border) bg-transparent px-2.5 py-1.5 text-[11px] text-(--platform-text-soft) disabled:cursor-default disabled:opacity-50"
|
||||
disabled={busy}
|
||||
onClick={() => onDownload(template)}
|
||||
>
|
||||
{busy && busyKind === 'download' ? (
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={12}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Download size={12} aria-hidden="true" />
|
||||
)}
|
||||
{template.installed ? '更新' : '下载'}
|
||||
</button>
|
||||
) : null}
|
||||
{busyLabel ? (
|
||||
<span className="text-[10px] text-(--platform-text-soft)">
|
||||
{busyLabel}
|
||||
@@ -167,6 +224,7 @@ export default function TemplateLibraryView({
|
||||
refresh,
|
||||
downloadTemplate,
|
||||
createProjectFromTemplate,
|
||||
clearNotice,
|
||||
} = controller;
|
||||
|
||||
const showEmptyLibrary = status === 'ready' && templates.length === 0;
|
||||
@@ -286,14 +344,7 @@ export default function TemplateLibraryView({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{notice ? (
|
||||
<p
|
||||
className="m-0 text-[12px] text-(--platform-text-soft)"
|
||||
role="status"
|
||||
>
|
||||
{notice}
|
||||
</p>
|
||||
) : null}
|
||||
<TemplateLibraryToast message={notice} onDismiss={clearNotice} />
|
||||
{error ? (
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-3 rounded-lg border border-(--platform-subpanel-border) px-3 py-2 text-[12px] text-(--platform-warm-text)"
|
||||
|
||||
@@ -117,12 +117,47 @@ describe('TemplateLibraryView', () => {
|
||||
expect(blankWebCard.textContent).toContain('网页 · none · v0.1.0 · 2.0 KB');
|
||||
expect(blankWebCard.textContent).toContain('空白');
|
||||
expect(blankWebCard.textContent).toContain('零依赖最小网页工程');
|
||||
// 已下载且版本一致:不再显示下载入口,只留「使用模板」。
|
||||
expect(
|
||||
within(blankWebCard).queryByRole('button', { name: /下载/ }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
within(blankWebCard).getByRole('button', { name: /使用模板/ }),
|
||||
).toBeTruthy();
|
||||
|
||||
const blankCanvasCard = cardFor('空白二维画布工程');
|
||||
expect(blankCanvasCard.textContent).not.toContain('已下载');
|
||||
expect(blankCanvasCard.querySelector('img')?.getAttribute('src')).toBe(
|
||||
`${OSS_BASE}/templates/v1/blank-2d-canvas/cover.svg`,
|
||||
);
|
||||
expect(
|
||||
within(blankCanvasCard).getByRole('button', { name: /下载/ }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('offers 更新 instead of 下载 when the installed version is stale', () => {
|
||||
const stale = template({
|
||||
id: 'blank-web',
|
||||
title: '空白网页工程',
|
||||
summary: '零依赖最小网页工程',
|
||||
tags: ['空白', '网页'],
|
||||
installed: true,
|
||||
installedVersion: '0.0.9',
|
||||
installedAtMillis: 1789000000000,
|
||||
});
|
||||
render(
|
||||
<TemplateLibraryView
|
||||
controller={controller({
|
||||
templates: [stale],
|
||||
visibleTemplates: [stale],
|
||||
})}
|
||||
onBack={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const card = cardFor('空白网页工程');
|
||||
expect(within(card).getByRole('button', { name: /更新/ })).toBeTruthy();
|
||||
expect(within(card).queryByRole('button', { name: /^下载/ })).toBeNull();
|
||||
});
|
||||
|
||||
it('routes search, tag, runtime and installed-only controls through the controller', () => {
|
||||
@@ -245,6 +280,40 @@ describe('TemplateLibraryView', () => {
|
||||
screen.getByText('远端清单暂时读不到,当前展示本机缓存'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders process notices as a floating toast outside the page and auto-dismisses it', () => {
|
||||
vi.useFakeTimers();
|
||||
const clearNotice = vi.fn();
|
||||
try {
|
||||
const { container } = render(
|
||||
<TemplateLibraryView
|
||||
controller={controller({
|
||||
notice: '已下载模板「空白网页工程」',
|
||||
clearNotice,
|
||||
})}
|
||||
onBack={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const toast = document.body.querySelector(
|
||||
'[data-template-library-toast="true"]',
|
||||
);
|
||||
expect(toast).not.toBeNull();
|
||||
expect(toast?.textContent).toContain('已下载模板「空白网页工程」');
|
||||
expect(toast?.querySelector('[role="status"]')?.textContent).toContain(
|
||||
'已下载模板「空白网页工程」',
|
||||
);
|
||||
// 提示不再占用页面内位置。
|
||||
expect(
|
||||
container.querySelector('[data-template-library-toast]'),
|
||||
).toBeNull();
|
||||
|
||||
vi.advanceTimersByTime(2600);
|
||||
expect(clearNotice).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('TemplateRecommendations', () => {
|
||||
|
||||
@@ -29,7 +29,7 @@ AGC 客户端能读取公共 OSS 上的游戏模板库,并把「浏览 → 筛
|
||||
2. `download_game_template` 对字节数与 SHA-256 不一致、越界对象键、非 `templates/` 前缀的包一律拒绝,且不落半成品目录。
|
||||
3. zip 解压拒绝绝对路径、`..`、盘符、符号链接;安装完成后才写 `installed.json` 作为已下载判据。
|
||||
4. `create_automatic_local_game_project_from_template` 建出的项目同时具备模板文件、`.agent` 清单与标准目录;失败时不留项目目录。
|
||||
5. 模板库页可按关键词、标签、运行时与「仅看已下载」筛选;卡片显示封面与已下载徽标;「使用模板」在版本落后时先重下再建项。
|
||||
5. 模板库页可按关键词、标签、运行时与「仅看已下载」筛选;卡片显示封面与已下载徽标;已下载且版本一致时不再显示下载入口(落后显示「更新」);过程提示以浮层 toast 呈现,不占页面内位置;「使用模板」在版本落后时先重下再建项。
|
||||
6. 首页推荐位展示模板库内容并可进入模板库页;左侧导航有模板库入口且为独立全屏页。
|
||||
7. 定向 Rust 单测 9 项(含线上清单 fixture)、前端模型 9 项 + 页面/推荐位 8 项、`tsc` 类型检查、`npm run check:encoding`、`git diff --check` 全部通过;可选真连检查能读线上清单、下载安装线上模板并据此建项目。
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ templates/
|
||||
- `src/features/template-library/templateLibraryModel.ts`:清单类型、搜索(空白分隔多关键词「与」)、标签/运行时/已下载筛选、标签选项聚合、体积格式化等纯函数。
|
||||
- `src/features/template-library/useTemplateLibrary.ts`:一次拉清单,暴露筛选状态、下载与「用模板建项目」;下载成功后只就地更新该条目的已下载状态。
|
||||
- `src/view/template-library/index.tsx`:模板库全屏页(返回、刷新、搜索、运行时/标签筛选、仅看已下载、卡片显示封面与已下载徽标、下载/使用模板)。
|
||||
- 卡片动作按安装状态收口:已下载且版本一致时**不再显示下载入口**,只留「使用模板」;版本落后才显示「更新」;缺包显示「下载」。
|
||||
- 过程提示(下载完成、开始建项目)走浮层 toast(复用 `packages/shared` 的 `PlatformRuntimeStatusToast`,`document.body` 浮层 + 2.6 秒自动消失),不再占用页面内位置;页面内只保留可操作的错误与空态。
|
||||
- 首页「灵感推荐」替换为「模板库」推荐位(`src/view/home/TemplateRecommendations.tsx`):只展示封面、标题、运行时与已下载徽标,点击进入模板库页面;首页不再直接触发建项目。
|
||||
- 左侧导航新增模板库入口(`LauncherView = 'template-library'`)。
|
||||
- `src-tauri/tauri.conf.json` 的 `csp` / `devCsp` 在 `img-src` 放行 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com`,用于封面图;`connect-src` 原本已放行同一域名。
|
||||
|
||||
Reference in New Issue
Block a user