隐藏历史图片名称

历史图片选择弹窗只展示缩略图和生成时间

历史素材兜底文案统一为历史素材

更新拼图历史图片展示口径文档

同步调整拼图历史图片选择流程测试
This commit is contained in:
2026-06-08 14:53:07 +08:00
parent 59b5bd1f83
commit 8f991a4ac2
5 changed files with 24 additions and 55 deletions

View File

@@ -110,6 +110,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列
- 图像输入复用 `CreativeImageInputPanel`
- 结果页每关画面编辑复用 `CreativeImageInputPanel`;入口页和关卡画面只共享受控 UI 模块不共享数据源、状态、action 或存储位置:入口页继续写 `formDraft` 与草稿编译 payload关卡画面写 `levels[].pictureReference/pictureDescription` 并触发 `generate_puzzle_images`。结果页删除独立“素材配置”Tab不再提供单独 UI 背景生成入口。通用图片面板的展示图和 AI 重绘参考图能力必须分开控制:结果页正式关卡图只作为预览图,不因存在正式图自动暴露 AI 重绘开关;只有本地上传、历史选择或已保存 `pictureReference` 可作为重绘参考图时,才显示 AI 重绘开关并把状态带入 `generate_puzzle_images`。用户在本次编辑中上传或选择历史图后,该图优先占据主图卡片,可删除、切换 AI 重绘,也可关闭 AI 重绘直用;仅有正式图预览时,画面描述框仍可上传多张参考图。关卡详情弹窗应使用加宽面板,关卡名称、画面图和画面描述合并在同一个纵向列表中,名称输入和画面编辑模块外层不再包独立 `platform-subpanel`;画面图卡仍必须保留稳定最小高度,避免弹窗内 `flex-1` 布局坍缩后只剩标题、描述输入和操作按钮。
- 历史图片选择弹窗只展示缩略图与生成时间,不展示从对象路径或文件名解析出的图片名称;选中历史图后内部兜底文案统一使用“历史素材”。
- 支持画面描述生图、多参考图生图、上传或历史生成主图后 AI 重绘、上传或历史生成主图后不重绘;主链要求浏览器先经 `/api/assets/direct-upload-tickets` 直传 OSS 并确认 `asset_object`,创作 action 只提交 `referenceImageAssetObjectId(s)`,由后端校验 owner / bucket / kind / MIME / size 后签发 OSS 只读 URL 并下载为 VectorEngine `/v1/images/edits` 的 multipart `image` part。本地上传 Data URL 与历史 `/generated-*` 图片路径仅保留为旧草稿、旧入口或未迁移客户端的兼容输入;关闭 AI 重绘时,后端统一解析为首关或当前关卡正式图后再持久化,不调用第一段拼图首图生成。
- 草稿生成会先持久化 `generationStatus=generating` 的作品摘要生成完成并回写关卡拼图画面、关卡画面参考图、UI spritesheet 和关卡背景图后再变为 `ready`;当前不自动生成背景音乐。生成页步骤推进必须跟随后端 session `progressPercent` 的真实里程碑:`88` 表示草稿编译完成并进入出图步骤,`94` 表示生成图已保存并进入 UI / 背景步骤,`96` 表示正式图与 UI 背景已确认并进入写入步骤,最终 action 成功或发布才进入完成态;每个步骤内部可以按实际等待时间使用假进度平滑推进。`88/94/96` 只负责切换当前步骤,不作为总进度地板;总进度按已完成步骤权重加当前步骤内假进度推导,非完成态最多停在 `98%`。文字直创的 `compile_puzzle_draft` 同步回包只表示已编译首关草稿并启动后台首图 / UI 资产生成;只要回包 session 仍缺正式 `draft.coverImageSrc`、首关 `coverImageSrc` 和候选图,前端必须继续保持生成中,不弹完成通知、不把草稿卡标记为 ready也不得自动进入结果页或试玩。
- 作品架拼图草稿的“生成中”遮罩只表示初始草稿还没有可查看结果;只要作品摘要、首关封面或任一关卡候选图已经可用,后续 UI 背景重生成和追加关卡生图都必须作为结果页局部生成态处理,不能阻止打开草稿结果页。生成失败后,同一浏览器会话内的失败 notice 必须覆盖后端可能仍短暂返回的 `generationStatus=generating` 摘要,作品架保留对应草稿卡但不再显示“生成中”,点击后回到失败 / 重试状态。

View File

@@ -1237,11 +1237,13 @@ describe('PuzzleResultView', () => {
const picker = await screen.findByRole('dialog', {
name: '选择历史图片',
});
expect(await within(picker).findByText('image.png')).toBeTruthy();
expect(await within(picker).findByText(/2024\/04\/21/u)).toBeTruthy();
expect(within(picker).queryByText('image.png')).toBeNull();
expect(within(picker).queryByText('账号 user-1')).toBeNull();
fireEvent.click(
await within(picker).findByRole('button', { name: /image\.png/u }),
await within(picker).findByRole('button', {
name: /选择2024\/04\/21.*的历史图片/u,
}),
);
await waitFor(() => {

View File

@@ -6,10 +6,7 @@ import {
puzzleAssetClient,
type PuzzleHistoryAsset,
} from '../../../services/puzzle-works/puzzleAssetClient';
import {
formatPuzzleHistoryAssetCreatedAt,
getPuzzleHistoryAssetDisplayName,
} from '../../../services/puzzle-works/puzzleHistoryAsset';
import { formatPuzzleHistoryAssetCreatedAt } from '../../../services/puzzle-works/puzzleHistoryAsset';
import { useAuthUi } from '../../auth/AuthUiContext';
import { ResolvedAssetImage } from '../../ResolvedAssetImage';
@@ -116,30 +113,28 @@ export function PuzzleHistoryAssetPickerDialog({
{!isLoading && assets.length > 0 ? (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{assets.map((asset) => {
const displayName = getPuzzleHistoryAssetDisplayName(
asset.imageSrc,
const createdAtText = formatPuzzleHistoryAssetCreatedAt(
asset.createdAt,
);
return (
<button
key={asset.assetObjectId}
type="button"
disabled={isBusy}
aria-label={`选择${createdAtText}的历史图片`}
onClick={() => onSelect(asset)}
className={`overflow-hidden rounded-[1.25rem] border bg-white/82 text-left transition hover:border-amber-300/70 hover:bg-white ${isBusy ? 'cursor-not-allowed opacity-55' : 'border-[var(--platform-subpanel-border)]'}`}
>
<div className="aspect-square overflow-hidden bg-[var(--platform-subpanel-fill)]">
<ResolvedAssetImage
src={asset.imageSrc}
alt={displayName}
alt=""
className="h-full w-full object-cover"
/>
</div>
<div className="space-y-1 px-4 py-4">
<div className="truncate text-sm font-black text-[var(--platform-text-strong)]">
{displayName}
</div>
<div className="px-4 py-3">
<div className="text-xs leading-5 text-[var(--platform-text-base)]">
{formatPuzzleHistoryAssetCreatedAt(asset.createdAt)}
{createdAtText}
</div>
</div>
</button>

View File

@@ -344,11 +344,13 @@ test('puzzle workspace selects a history image from the upload card', async () =
const picker = await screen.findByRole('dialog', {
name: '选择历史图片',
});
expect(await within(picker).findByText('image.png')).toBeTruthy();
expect(await within(picker).findByText(/2024\/04\/21/u)).toBeTruthy();
expect(within(picker).queryByText('image.png')).toBeNull();
expect(within(picker).queryByText('账号 user-1')).toBeNull();
fireEvent.click(
await within(picker).findByRole('button', { name: /image\.png/u }),
await within(picker).findByRole('button', {
name: /2024\/04\/21.*/u,
}),
);
await waitFor(() => {
@@ -622,7 +624,9 @@ test('puzzle workspace submits history image when AI redraw is off', async () =>
name: '选择历史图片',
});
fireEvent.click(
await within(picker).findByRole('button', { name: /image\.png/u }),
await within(picker).findByRole('button', {
name: /2024\/04\/21.*/u,
}),
);
await waitFor(() => {
expect(screen.queryByRole('dialog', { name: '选择历史图片' })).toBeNull();
@@ -636,9 +640,9 @@ test('puzzle workspace submits history image when AI redraw is off', async () =>
fireEvent.click(screen.getByRole('button', { name: /稿/u }));
expect(onCreateFromForm).toHaveBeenCalledWith({
seedText: '历史素材 · image.png',
workDescription: '历史素材 · image.png',
pictureDescription: '历史素材 · image.png',
seedText: '历史素材',
workDescription: '历史素材',
pictureDescription: '历史素材',
referenceImageSrc: '/generated-puzzle-assets/history/image.png',
referenceImageSrcs: [],
referenceImageAssetObjectId: 'asset-history-1',

View File

@@ -1,13 +1,3 @@
const PUZZLE_HISTORY_ASSET_FALLBACK_NAME = '历史拼图素材';
function safeDecodePathSegment(value: string) {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function parsePuzzleHistoryTimestamp(value: string) {
const trimmed = value.trim();
if (!trimmed) {
@@ -47,24 +37,6 @@ function parsePuzzleHistoryTimestamp(value: string) {
return new Date(timestampMs);
}
export function getPuzzleHistoryAssetDisplayName(
imageSrc: string | null | undefined,
) {
const trimmed = imageSrc?.trim() ?? '';
if (!trimmed) {
return PUZZLE_HISTORY_ASSET_FALLBACK_NAME;
}
const pathOnly = trimmed.split(/[?#]/u)[0]?.trim() ?? '';
if (!pathOnly) {
return PUZZLE_HISTORY_ASSET_FALLBACK_NAME;
}
const fileName = pathOnly.replace(/^\/+/u, '').split('/').filter(Boolean).pop();
const displayName = safeDecodePathSegment(fileName ?? '').trim();
return displayName || PUZZLE_HISTORY_ASSET_FALLBACK_NAME;
}
export function formatPuzzleHistoryAssetCreatedAt(value: string) {
const parsedDate = parsePuzzleHistoryTimestamp(value);
if (!parsedDate) {
@@ -82,12 +54,7 @@ export function formatPuzzleHistoryAssetCreatedAt(value: string) {
}
export function getPuzzleHistoryAssetReferenceLabel(
imageSrc: string | null | undefined,
_imageSrc: string | null | undefined,
) {
const displayName = getPuzzleHistoryAssetDisplayName(imageSrc);
if (displayName === PUZZLE_HISTORY_ASSET_FALLBACK_NAME) {
return '历史素材';
}
return `历史素材 · ${displayName}`;
return '历史素材';
}