This commit is contained in:
2026-05-14 14:21:17 +08:00
parent 7a75f5d612
commit d33c937ebc
191 changed files with 1916 additions and 1549 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import {
import {
type ReactNode,
useDeferredValue,
useEffect,
@@ -1199,7 +1199,7 @@ export function CustomWorldEntityCatalog({
<div className="flex flex-wrap items-center gap-2 px-1">
{lockedCharacterNames.has(role.name.trim()) ? (
<span className="platform-pill platform-pill--warm px-2.5 py-1 text-[10px]">
百梦主锁定
陶泥儿主锁定
</span>
) : null}
<span className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]">
+1 -1
View File
@@ -62,7 +62,7 @@ export function BindPhoneScreen({
<div className="platform-auth-card grid w-full max-w-4xl overflow-hidden rounded-[28px] md:grid-cols-[1.05fr_0.95fr]">
<div className="border-b border-[var(--platform-subpanel-border)] bg-[linear-gradient(135deg,rgba(255,79,139,0.18),rgba(255,155,120,0.14))] px-6 py-8 md:border-b-0 md:border-r md:px-10 md:py-12">
<div className="selection-hero-brand selection-hero-brand--left">
<div className="selection-hero-brand__title">百梦</div>
<div className="selection-hero-brand__title">陶泥儿</div>
<div className="selection-hero-brand__subtitle">视觉叙事 RPG</div>
</div>
<p className="mt-8 text-[11px] font-semibold tracking-[0.32em] text-[var(--platform-cool-text)]">
@@ -0,0 +1,444 @@
import {
type CSSProperties,
type PointerEvent,
useMemo,
useRef,
useState,
} from 'react';
export type SquareImageCropRect = {
x: number;
y: number;
size: number;
};
export type SquareImageCropModalLabels = {
title: string;
close: string;
editor: string;
previewAlt: string;
cancel: string;
submit: string;
saving: string;
};
type SquareCropDragHandle =
| 'move'
| 'north'
| 'northEast'
| 'east'
| 'southEast'
| 'south'
| 'southWest'
| 'west'
| 'northWest';
type SquareCropDragSnapshot = {
pointerId: number;
handle: SquareCropDragHandle;
clientX: number;
clientY: number;
cropRect: SquareImageCropRect;
previewWidth: number;
previewHeight: number;
};
const SQUARE_CROP_RESIZE_HANDLES: Array<{
handle: Exclude<SquareCropDragHandle, 'move'>;
label: string;
className: string;
}> = [
{
handle: 'northWest',
label: '拖拽左上角裁剪边界',
className:
'left-0 top-0 -translate-x-1/2 -translate-y-1/2 cursor-nwse-resize',
},
{
handle: 'north',
label: '拖拽上边裁剪边界',
className:
'left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 cursor-ns-resize',
},
{
handle: 'northEast',
label: '拖拽右上角裁剪边界',
className:
'right-0 top-0 translate-x-1/2 -translate-y-1/2 cursor-nesw-resize',
},
{
handle: 'east',
label: '拖拽右边裁剪边界',
className:
'right-0 top-1/2 -translate-y-1/2 translate-x-1/2 cursor-ew-resize',
},
{
handle: 'southEast',
label: '拖拽右下角裁剪边界',
className:
'bottom-0 right-0 translate-x-1/2 translate-y-1/2 cursor-nwse-resize',
},
{
handle: 'south',
label: '拖拽下边裁剪边界',
className:
'bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 cursor-ns-resize',
},
{
handle: 'southWest',
label: '拖拽左下角裁剪边界',
className:
'bottom-0 left-0 -translate-x-1/2 translate-y-1/2 cursor-nesw-resize',
},
{
handle: 'west',
label: '拖拽左边裁剪边界',
className:
'left-0 top-1/2 -translate-x-1/2 -translate-y-1/2 cursor-ew-resize',
},
];
function clampNumber(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
}
function getSquareCropSizeBounds(imageSize: { width: number; height: number }) {
const maxSize = Math.max(1, Math.min(imageSize.width, imageSize.height));
const minSize = Math.min(maxSize, Math.max(48, maxSize * 0.18));
return { minSize, maxSize };
}
export function buildCenteredSquareImageCropRect(imageSize: {
width: number;
height: number;
}): SquareImageCropRect {
const size = Math.max(1, Math.min(imageSize.width, imageSize.height));
return {
x: Math.max(0, (imageSize.width - size) / 2),
y: Math.max(0, (imageSize.height - size) / 2),
size,
};
}
export function clampSquareImageCropRect(
imageSize: { width: number; height: number },
crop: SquareImageCropRect,
): SquareImageCropRect {
const { minSize, maxSize } = getSquareCropSizeBounds(imageSize);
const size = clampNumber(crop.size, minSize, maxSize);
return {
x: clampNumber(crop.x, 0, Math.max(0, imageSize.width - size)),
y: clampNumber(crop.y, 0, Math.max(0, imageSize.height - size)),
size,
};
}
function buildSquareCropPreviewStyle(
crop: SquareImageCropRect,
imageSize: { width: number; height: number },
) {
return {
left: `${(crop.x / imageSize.width) * 100}%`,
top: `${(crop.y / imageSize.height) * 100}%`,
width: `${(crop.size / imageSize.width) * 100}%`,
height: `${(crop.size / imageSize.height) * 100}%`,
} satisfies CSSProperties;
}
function resizeSquareCropRectFromHandle(
snapshot: SquareCropDragSnapshot,
deltaX: number,
deltaY: number,
imageSize: { width: number; height: number },
) {
const start = snapshot.cropRect;
const startRight = start.x + start.size;
const startBottom = start.y + start.size;
const startCenterX = start.x + start.size / 2;
const startCenterY = start.y + start.size / 2;
const { minSize, maxSize } = getSquareCropSizeBounds(imageSize);
const chooseSize = (sizeFromX: number, sizeFromY: number) => {
const xDistance = Math.abs(sizeFromX - start.size);
const yDistance = Math.abs(sizeFromY - start.size);
return xDistance >= yDistance ? sizeFromX : sizeFromY;
};
const clampSize = (size: number, maxByAnchor = maxSize) =>
clampNumber(
size,
minSize,
Math.max(minSize, Math.min(maxSize, maxByAnchor)),
);
if (snapshot.handle === 'move') {
return clampSquareImageCropRect(imageSize, {
...start,
x: start.x + deltaX,
y: start.y + deltaY,
});
}
// 中文注释:边缘手柄保持正方形比例,锚点逻辑与拼图图片上传裁剪一致。
if (snapshot.handle === 'east' || snapshot.handle === 'west') {
const isEast = snapshot.handle === 'east';
const anchorX = isEast ? start.x : startRight;
const maxByAnchorX = isEast ? imageSize.width - anchorX : anchorX;
const maxByCenterY =
2 * Math.min(startCenterY, imageSize.height - startCenterY);
const size = clampSize(
start.size + (isEast ? deltaX : -deltaX),
Math.min(maxByAnchorX, maxByCenterY),
);
return clampSquareImageCropRect(imageSize, {
x: isEast ? anchorX : anchorX - size,
y: startCenterY - size / 2,
size,
});
}
if (snapshot.handle === 'north' || snapshot.handle === 'south') {
const isSouth = snapshot.handle === 'south';
const anchorY = isSouth ? start.y : startBottom;
const maxByAnchorY = isSouth ? imageSize.height - anchorY : anchorY;
const maxByCenterX =
2 * Math.min(startCenterX, imageSize.width - startCenterX);
const size = clampSize(
start.size + (isSouth ? deltaY : -deltaY),
Math.min(maxByAnchorY, maxByCenterX),
);
return clampSquareImageCropRect(imageSize, {
x: startCenterX - size / 2,
y: isSouth ? anchorY : anchorY - size,
size,
});
}
const isEast =
snapshot.handle === 'northEast' || snapshot.handle === 'southEast';
const isSouth =
snapshot.handle === 'southEast' || snapshot.handle === 'southWest';
const anchorX = isEast ? start.x : startRight;
const anchorY = isSouth ? start.y : startBottom;
const maxByAnchorX = isEast ? imageSize.width - anchorX : anchorX;
const maxByAnchorY = isSouth ? imageSize.height - anchorY : anchorY;
const sizeFromX = start.size + (isEast ? deltaX : -deltaX);
const sizeFromY = start.size + (isSouth ? deltaY : -deltaY);
const size = clampSize(
chooseSize(sizeFromX, sizeFromY),
Math.min(maxByAnchorX, maxByAnchorY),
);
return clampSquareImageCropRect(imageSize, {
x: isEast ? anchorX : anchorX - size,
y: isSouth ? anchorY : anchorY - size,
size,
});
}
export function SquareImageCropModal({
source,
imageSize,
cropRect,
labels,
titleId = 'square-image-crop-title',
error = null,
isSaving = false,
onCropRectChange,
onClose,
onSubmit,
}: {
source: string;
imageSize: { width: number; height: number };
cropRect: SquareImageCropRect;
labels: SquareImageCropModalLabels;
titleId?: string;
error?: string | null;
isSaving?: boolean;
onCropRectChange: (nextCrop: SquareImageCropRect) => void;
onClose: () => void;
onSubmit: () => void;
}) {
const previewRef = useRef<HTMLDivElement | null>(null);
const dragSnapshotRef = useRef<SquareCropDragSnapshot | null>(null);
const [activeDragHandle, setActiveDragHandle] =
useState<SquareCropDragHandle | null>(null);
const normalizedCropRect = useMemo(
() => clampSquareImageCropRect(imageSize, cropRect),
[cropRect, imageSize],
);
const previewStyle = useMemo(
() => buildSquareCropPreviewStyle(normalizedCropRect, imageSize),
[normalizedCropRect, imageSize],
);
const editorPreviewStyle = useMemo(
() =>
({
aspectRatio: `${imageSize.width} / ${imageSize.height}`,
width: `min(100%, calc(min(52vh, 22rem) * ${
imageSize.width / Math.max(1, imageSize.height)
}))`,
}) satisfies CSSProperties,
[imageSize],
);
const beginCropDrag = (
handle: SquareCropDragHandle,
event: PointerEvent<HTMLElement>,
) => {
if (isSaving) {
return;
}
const preview = previewRef.current;
if (!preview) {
return;
}
const rect = preview.getBoundingClientRect();
dragSnapshotRef.current = {
pointerId: event.pointerId,
handle,
clientX: event.clientX,
clientY: event.clientY,
cropRect: normalizedCropRect,
previewWidth: rect.width,
previewHeight: rect.height,
};
setActiveDragHandle(handle);
event.preventDefault();
event.stopPropagation();
event.currentTarget.setPointerCapture(event.pointerId);
};
const updateCropDrag = (event: PointerEvent<HTMLElement>) => {
const snapshot = dragSnapshotRef.current;
if (!snapshot || snapshot.pointerId !== event.pointerId) {
return;
}
const deltaX =
((event.clientX - snapshot.clientX) * imageSize.width) /
Math.max(1, snapshot.previewWidth);
const deltaY =
((event.clientY - snapshot.clientY) * imageSize.height) /
Math.max(1, snapshot.previewHeight);
onCropRectChange(
resizeSquareCropRectFromHandle(snapshot, deltaX, deltaY, imageSize),
);
};
const stopCropDrag = (event: PointerEvent<HTMLElement>) => {
if (dragSnapshotRef.current?.pointerId !== event.pointerId) {
return;
}
dragSnapshotRef.current = null;
setActiveDragHandle(null);
event.currentTarget.releasePointerCapture(event.pointerId);
};
return (
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="platform-modal-shell platform-remap-surface w-full max-w-sm overflow-hidden rounded-[1.4rem]"
>
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div id={titleId} className="text-base font-black">
{labels.title}
</div>
<button
type="button"
aria-label={labels.close}
onClick={onClose}
className="platform-profile-icon-button flex h-8 w-8 items-center justify-center rounded-full"
>
×
</button>
</div>
<div className="px-5 py-5">
<div
ref={previewRef}
className="relative mx-auto overflow-hidden rounded-[1.2rem] border border-white/12 bg-black/35 select-none touch-none"
style={editorPreviewStyle}
aria-label={labels.editor}
>
<img
src={source}
alt={labels.previewAlt}
draggable={false}
className="h-full w-full object-fill"
/>
<div
className={`absolute border-2 border-sky-200/95 shadow-[0_0_0_9999px_rgba(0,0,0,0.38)] outline outline-1 outline-black/35 ${
activeDragHandle === 'move' ? 'cursor-grabbing' : 'cursor-grab'
}`}
style={previewStyle}
onPointerDown={(event) => beginCropDrag('move', event)}
onPointerMove={updateCropDrag}
onPointerUp={stopCropDrag}
onPointerCancel={stopCropDrag}
/>
<div
className="pointer-events-none absolute border border-white/70"
style={previewStyle}
>
<div className="absolute inset-x-0 top-1/3 border-t border-white/35" />
<div className="absolute inset-x-0 top-2/3 border-t border-white/35" />
<div className="absolute inset-y-0 left-1/3 border-l border-white/35" />
<div className="absolute inset-y-0 left-2/3 border-l border-white/35" />
</div>
<div className="pointer-events-none absolute" style={previewStyle}>
{SQUARE_CROP_RESIZE_HANDLES.map((handleConfig) => (
<button
key={handleConfig.handle}
type="button"
aria-label={handleConfig.label}
disabled={isSaving}
className={`pointer-events-auto absolute z-10 h-10 w-10 rounded-full border border-white/15 bg-black/5 p-0 disabled:cursor-not-allowed disabled:opacity-45 ${handleConfig.className}`}
onPointerDown={(event) =>
beginCropDrag(handleConfig.handle, event)
}
onPointerMove={updateCropDrag}
onPointerUp={stopCropDrag}
onPointerCancel={stopCropDrag}
>
<span className="absolute left-1/2 top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white bg-sky-300 shadow-[0_0_0_3px_rgba(2,132,199,0.32)]" />
</button>
))}
</div>
</div>
{error ? (
<div className="mt-4 rounded-2xl border border-rose-400/25 bg-rose-500/10 px-3 py-2 text-sm text-rose-600">
{error}
</div>
) : null}
<div className="mt-5 grid grid-cols-2 gap-3">
<button
type="button"
onClick={onClose}
className="platform-button platform-button--secondary justify-center"
>
{labels.cancel}
</button>
<button
type="button"
onClick={onSubmit}
disabled={isSaving}
className="platform-button platform-button--primary justify-center disabled:cursor-not-allowed disabled:opacity-60"
>
{isSaving ? labels.saving : labels.submit}
</button>
</div>
</div>
</div>
</div>
);
}
@@ -382,7 +382,7 @@ export function CreativeAgentHome({
<CreativeAgentInputComposer
variant="floating"
isBusy={isBusy}
placeholder="问一问百梦"
placeholder="问一问陶泥儿"
onSubmit={(payload) => {
const content = buildCreativeHomeInputParts(payload);
if (content.length === 0) {
@@ -40,7 +40,7 @@ test('shows cost range and opens an independent adjustment dialog', () => {
);
const confirmDialog = screen.getByRole('dialog', { name: '确认拼图模板' });
expect(within(confirmDialog).getByText('预计 2 到 12 光点')).toBeTruthy();
expect(within(confirmDialog).getByText('预计 2 到 12 泥点')).toBeTruthy();
expect(within(confirmDialog).getByText('创意拼图')).toBeTruthy();
fireEvent.click(within(confirmDialog).getByRole('button', { name: /调整/u }));
@@ -61,7 +61,7 @@ export function CreativeAgentTemplateConfirmPanel({
setDraftSelection(selection);
}, [selection]);
const pointsText = `${draftSelection.costRange.minPoints} 到 ${draftSelection.costRange.maxPoints} 光点`;
const pointsText = `${draftSelection.costRange.minPoints} 到 ${draftSelection.costRange.maxPoints} 泥点`;
const panel = (
<div
@@ -119,7 +119,7 @@ test('target ready session exposes the puzzle result entry action', () => {
expect(screen.getByText('拼图草稿已就绪')).toBeTruthy();
expect(screen.getByText('可以进入结果页继续编辑')).toBeTruthy();
expect(screen.getByText('预计 2-12 光点')).toBeTruthy();
expect(screen.getByText('预计 2-12 泥点')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '打开草稿' }));
@@ -168,7 +168,7 @@ test('waiting confirmation shows template catalog before template config dialog'
fireEvent.click(screen.getByRole('button', { name: /旅行记忆拼图/u }));
expect(screen.getByRole('dialog', { name: '确认拼图模板' })).toBeTruthy();
expect(screen.getByText('预计 4 到 16 光点')).toBeTruthy();
expect(screen.getByText('预计 4 到 16 泥点')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /确认/u }));
@@ -85,7 +85,7 @@ function CreativeAgentTemplateCatalogPanel({
{template.summary}
</div>
<div className="mt-3 text-xs font-bold text-[var(--platform-text-soft)]">
{`${template.defaultLevelCount} 关 · ${template.costRange.minPoints}-${template.costRange.maxPoints} 光点`}
{`${template.defaultLevelCount} 关 · ${template.costRange.minPoints}-${template.costRange.maxPoints} 泥点`}
</div>
</button>
))}
@@ -323,7 +323,7 @@ function buildEventProcessItem(
return {
id: `${index}-cost-${event.data.costRange.minPoints}-${event.data.costRange.maxPoints}`,
meta: '消耗',
title: `预计 ${event.data.costRange.minPoints}-${event.data.costRange.maxPoints} 光点`,
title: `预计 ${event.data.costRange.minPoints}-${event.data.costRange.maxPoints} 泥点`,
detail: event.data.costRange.reason,
detailLines: [],
tone: 'info',
@@ -482,7 +482,7 @@ function buildSessionFallbackItems(
id: `session-level-plan-${plan.templateId}`,
meta: '关卡',
title: `规划 ${plan.levels.length} 个关卡`,
detail: `${formatPuzzleLevelMode(plan.mode)} · ${plan.estimatedCostRange.minPoints}-${plan.estimatedCostRange.maxPoints} 光点`,
detail: `${formatPuzzleLevelMode(plan.mode)} · ${plan.estimatedCostRange.minPoints}-${plan.estimatedCostRange.maxPoints} 泥点`,
detailLines: plan.levels.slice(0, 4).map((level) =>
[
level.levelName,
@@ -345,9 +345,9 @@ test('creation hub shows puzzle point incentive and claims without opening card'
profileId: 'puzzle-profile-incentive',
ownerUserId: 'user-1',
authorDisplayName: '拼图作者',
levelName: '百梦灯塔',
levelName: '陶泥儿灯塔',
summary: '拼图作品会展示积分激励。',
themeTags: ['灯塔', '百梦'],
themeTags: ['灯塔', '陶泥儿'],
coverImageSrc: null,
publicationStatus: 'published',
updatedAt: new Date('2026-05-01T12:00:00.000Z').toISOString(),
@@ -375,8 +375,8 @@ test('creation hub shows puzzle point incentive and claims without opening card'
/>,
);
expect(screen.getByLabelText('积分激励总数 2.5 光点')).toBeTruthy();
expect(screen.getByLabelText('待领取积分 1 光点')).toBeTruthy();
expect(screen.getByLabelText('积分激励总数 2.5 泥点')).toBeTruthy();
expect(screen.getByLabelText('待领取积分 1 泥点')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '领取积分' }));
@@ -255,7 +255,7 @@ export function CustomWorldWorkCard({
event.preventDefault();
onOpen();
}}
className={`platform-surface platform-interactive-card relative min-h-[9.5rem] cursor-pointer overflow-hidden px-2.5 py-2.5 text-left sm:min-h-[12.5rem] sm:px-4 sm:py-4 xl:min-h-[11.25rem] xl:px-4 xl:py-3.5 ${isPublished ? 'col-span-2 sm:col-span-1' : ''}`}
className={`creation-work-card platform-surface platform-interactive-card relative min-h-[9.5rem] cursor-pointer overflow-hidden px-2.5 py-2.5 text-left sm:min-h-[12.5rem] sm:px-4 sm:py-4 xl:min-h-[11.25rem] xl:px-4 xl:py-3.5 ${isPublished ? 'col-span-2 sm:col-span-1' : ''}`}
>
<CustomWorldCoverArtwork
imageSrc={item.coverImageSrc}
@@ -263,10 +263,9 @@ export function CustomWorldWorkCard({
fallbackLabel="封面"
renderMode={item.coverRenderMode}
characterImageSrcs={item.coverCharacterImageSrcs}
className="platform-cover-artwork absolute inset-0 opacity-70 saturate-[1.08]"
className="platform-cover-artwork absolute inset-0"
/>
<div className="absolute inset-0 bg-[var(--platform-card-overlay-strong)]" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_100%_100%,rgba(255,255,255,0.18),transparent_34%),linear-gradient(180deg,rgba(255,255,255,0.08),rgba(0,0,0,0.08))]" />
<div className="creation-work-card__overlay absolute inset-0" />
{item.hasUnreadUpdate ? (
<span
aria-label="新生成完成"
@@ -288,7 +287,7 @@ export function CustomWorldWorkCard({
disabled={deleteBusy}
aria-label={deleteBusy ? '删除中' : '删除'}
title={deleteBusy ? '删除中' : '删除作品'}
className="grid h-7 w-7 place-items-center text-[var(--platform-text-soft)] transition hover:text-[var(--platform-button-danger-text)] disabled:cursor-not-allowed disabled:opacity-55 sm:h-8 sm:w-8"
className="grid h-7 w-7 place-items-center rounded-full bg-black/22 text-white/78 transition hover:bg-red-500/22 hover:text-white disabled:cursor-not-allowed disabled:opacity-55 sm:h-8 sm:w-8"
>
{deleteBusy ? (
<span className="text-xs leading-none">…</span>
@@ -326,7 +325,7 @@ export function CustomWorldWorkCard({
? '分享内容复制失败'
: '分享'
}
className="inline-flex h-7 min-w-7 items-center justify-center gap-1 whitespace-nowrap px-1.5 text-[var(--platform-text-soft)] transition hover:text-[var(--platform-cool-text)] disabled:cursor-not-allowed disabled:opacity-55 sm:h-8 sm:min-w-8"
className="inline-flex h-7 min-w-7 items-center justify-center gap-1 whitespace-nowrap rounded-full bg-black/22 px-1.5 text-white/78 transition hover:bg-white/18 hover:text-white disabled:cursor-not-allowed disabled:opacity-55 sm:h-8 sm:min-w-8"
>
{shareState === 'idle' ? (
<Share2 aria-hidden="true" className="h-3.5 w-3.5" />
@@ -358,10 +357,10 @@ export function CustomWorldWorkCard({
</div>
<div className="mt-3 min-h-0 sm:mt-4 xl:mt-3">
<div className="line-clamp-1 break-words text-base font-black leading-tight text-[var(--platform-text-strong)] sm:text-2xl xl:text-xl">
<div className="line-clamp-1 break-words text-base font-black leading-tight text-white [text-shadow:0_2px_12px_rgba(0,0,0,0.52)] sm:text-2xl xl:text-xl">
{displayTitle}
</div>
<div className="mt-2 line-clamp-2 break-words text-[11px] leading-4 text-[color:color-mix(in_srgb,var(--platform-text-base)_92%,transparent)] sm:mt-3 sm:text-sm sm:leading-6 xl:mt-2">
<div className="mt-2 line-clamp-2 break-words text-[11px] leading-4 text-white/84 [text-shadow:0_1px_8px_rgba(0,0,0,0.5)] sm:mt-3 sm:text-sm sm:leading-6 xl:mt-2">
{item.summary}
</div>
</div>
@@ -371,7 +370,7 @@ export function CustomWorldWorkCard({
{item.pointIncentive ? (
<div className="creation-work-card-incentive">
<div
aria-label={`积分激励总数 ${formatCreationPointIncentiveTotal(item.pointIncentive.totalPoints)} 光点`}
aria-label={`积分激励总数 ${formatCreationPointIncentiveTotal(item.pointIncentive.totalPoints)} 泥点`}
className="creation-work-card-incentive__metric"
>
<span className="creation-work-card-incentive__label">
@@ -384,7 +383,7 @@ export function CustomWorldWorkCard({
</span>
</div>
<div
aria-label={`待领取积分 ${item.pointIncentive.claimablePoints} 光点`}
aria-label={`待领取积分 ${item.pointIncentive.claimablePoints} 泥点`}
className="creation-work-card-incentive__metric"
>
<span className="creation-work-card-incentive__label">
@@ -187,6 +187,109 @@ test('buildCreationWorkShelfItems sorts works by latest updatedAt across timesta
]);
});
test('buildCreationWorkShelfItems falls back to available gameplay images as covers', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [
{
workId: 'puzzle:level-cover',
profileId: 'puzzle-profile-level-cover',
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
levelName: '关卡封面拼图',
summary: '作品自身封面为空时使用关卡正式图。',
themeTags: [],
coverImageSrc: null,
publicationStatus: 'draft',
updatedAt: '2026-05-08T00:00:00.000Z',
publishedAt: null,
publishReady: false,
levels: [
{
levelId: 'level-1',
levelName: '第一关',
pictureDescription: '港口雨夜。',
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle-candidate.png',
assetId: 'asset-1',
prompt: '港口雨夜',
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'ready',
},
],
},
],
match3dItems: [
{
workId: 'match3d:asset-cover',
profileId: 'match3d-profile-asset-cover',
ownerUserId: 'user-1',
gameName: '素材封面抓鹅',
themeText: '糖果厨房',
summary: '作品自身封面为空时使用素材图。',
tags: [],
coverImageSrc: null,
clearCount: 18,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-07T00:00:00.000Z',
publishReady: false,
generatedItemAssets: [
{
itemId: 'item-1',
itemName: '糖果',
imageSrc: '/match3d-item.png',
status: 'image_ready',
},
],
},
],
squareHoleItems: [
{
workId: 'square-hole:background-cover',
profileId: 'square-hole-profile-background-cover',
ownerUserId: 'user-1',
gameName: '背景封面方洞',
themeText: '星空玩具箱',
twistRule: '旋转洞口',
summary: '作品自身封面为空时使用背景图。',
tags: [],
coverImageSrc: null,
backgroundPrompt: '星空玩具箱',
backgroundImageSrc: '/square-hole-background.png',
shapeOptions: [],
holeOptions: [],
shapeCount: 3,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-06T00:00:00.000Z',
publishReady: false,
},
],
});
expect(items.find((item) => item.kind === 'puzzle')?.coverImageSrc).toBe(
'/puzzle-candidate.png',
);
expect(items.find((item) => item.kind === 'match3d')?.coverImageSrc).toBe(
'/match3d-item.png',
);
expect(items.find((item) => item.kind === 'square-hole')?.coverImageSrc).toBe(
'/square-hole-background.png',
);
});
test('getCreationWorkShelfItemTime parses backend seconds.microsZ values', () => {
expect(getCreationWorkShelfItemTime('1778457601.234567Z')).toBe(
1778457601234.567,
@@ -364,6 +364,7 @@ function mapMatch3DWorkToShelfItem(
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
const publicWorkCode =
status === 'published' ? buildMatch3DPublicWorkCode(item.profileId) : null;
const coverImageSrc = resolveMatch3DWorkCoverImageSrc(item);
return {
id: item.workId,
@@ -372,7 +373,7 @@ function mapMatch3DWorkToShelfItem(
title: item.gameName,
summary: item.summary,
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverImageSrc,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
publicWorkCode,
@@ -408,6 +409,7 @@ function mapPuzzleWorkToShelfItem(
const status = item.publicationStatus;
const publicWorkCode =
status === 'published' ? buildPuzzlePublicWorkCode(item.profileId) : null;
const coverImageSrc = resolvePuzzleWorkCoverImageSrc(item);
return {
id: item.workId,
@@ -419,7 +421,7 @@ function mapPuzzleWorkToShelfItem(
item.summary.trim() ||
(status === 'draft' ? '未填写作品描述' : ''),
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverImageSrc,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
publicWorkCode,
@@ -571,6 +573,7 @@ function mapSquareHoleWorkToShelfItem(
status === 'published'
? buildSquareHolePublicWorkCode(item.profileId)
: null;
const coverImageSrc = resolveSquareHoleWorkCoverImageSrc(item);
return {
id: item.workId,
@@ -579,7 +582,7 @@ function mapSquareHoleWorkToShelfItem(
title: item.gameName,
summary: item.summary,
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverImageSrc,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
publicWorkCode,
@@ -607,6 +610,90 @@ function mapSquareHoleWorkToShelfItem(
};
}
function normalizeCoverImageSrc(value?: string | null) {
return value?.trim() || null;
}
function resolvePuzzleWorkCoverImageSrc(item: PuzzleWorkSummary) {
const directCoverImageSrc = normalizeCoverImageSrc(item.coverImageSrc);
if (directCoverImageSrc) {
return directCoverImageSrc;
}
for (const level of item.levels ?? []) {
const selectedCandidateImageSrc =
level.selectedCandidateId && level.candidates.length > 0
? normalizeCoverImageSrc(
level.candidates.find(
(candidate) => candidate.candidateId === level.selectedCandidateId,
)?.imageSrc,
)
: null;
const fallbackCandidateImageSrc = normalizeCoverImageSrc(
level.candidates[level.candidates.length - 1]?.imageSrc,
);
const levelCoverImageSrc =
selectedCandidateImageSrc ||
normalizeCoverImageSrc(level.coverImageSrc) ||
fallbackCandidateImageSrc;
if (levelCoverImageSrc) {
return levelCoverImageSrc;
}
}
return null;
}
function resolveMatch3DWorkCoverImageSrc(item: Match3DWorkSummary) {
const directCoverImageSrc = normalizeCoverImageSrc(item.coverImageSrc);
if (directCoverImageSrc) {
return directCoverImageSrc;
}
const backgroundImageSrc =
normalizeCoverImageSrc(item.backgroundImageSrc) ||
normalizeCoverImageSrc(item.generatedBackgroundAsset?.imageSrc) ||
normalizeCoverImageSrc(item.generatedBackgroundAsset?.containerImageSrc);
if (backgroundImageSrc) {
return backgroundImageSrc;
}
for (const asset of item.generatedItemAssets ?? []) {
const imageViewSrc = normalizeCoverImageSrc(
asset.imageViews?.find((view) => normalizeCoverImageSrc(view.imageSrc))
?.imageSrc,
);
const itemImageSrc = normalizeCoverImageSrc(asset.imageSrc);
if (imageViewSrc || itemImageSrc) {
return imageViewSrc || itemImageSrc;
}
}
return null;
}
function resolveSquareHoleWorkCoverImageSrc(item: SquareHoleWorkSummary) {
const directCoverImageSrc = normalizeCoverImageSrc(item.coverImageSrc);
if (directCoverImageSrc) {
return directCoverImageSrc;
}
const backgroundImageSrc = normalizeCoverImageSrc(item.backgroundImageSrc);
if (backgroundImageSrc) {
return backgroundImageSrc;
}
for (const option of [...item.shapeOptions, ...item.holeOptions]) {
const optionImageSrc = normalizeCoverImageSrc(option.imageSrc);
if (optionImageSrc) {
return optionImageSrc;
}
}
return null;
}
function buildWorkShelfActions<TItem>(
item: TItem,
adapter: WorkShelfAdapter<TItem>,
@@ -75,7 +75,7 @@ test('match3d workspace submits derived entry form payload instead of agent chat
expect(screen.getByText('2D素材风格')).toBeTruthy();
expect(screen.getByRole('button', { name: '扁平图标' })).toBeTruthy();
expect(screen.getByRole('button', { name: '自定义' })).toBeTruthy();
expect(screen.getByText('消耗10光点')).toBeTruthy();
expect(screen.getByText('消耗10泥点')).toBeTruthy();
expect(screen.queryByRole('button', { name: '生成音效' })).toBeNull();
expect(screen.queryByText('参考图')).toBeNull();
expect(screen.queryByLabelText('上传抓大鹅参考图')).toBeNull();
@@ -480,7 +480,7 @@ export function Match3DAgentWorkspace({
)}
<span>生成抓大鹅草稿</span>
<span className="rounded-full bg-white/24 px-2 py-0.5 text-[11px] font-bold">
消耗10光点
消耗10泥点
</span>
</span>
</button>
@@ -504,7 +504,7 @@ describe('Match3DResultView', () => {
expect(screen.getByRole('dialog', { name: /水果核心物件/u })).toBeTruthy();
expect(screen.getByText('素材名称')).toBeTruthy();
expect(screen.getByText('暂无音效')).toBeTruthy();
expect(screen.getByLabelText('生成点击音效,10光点')).toBeTruthy();
expect(screen.getByLabelText('生成点击音效,10泥点')).toBeTruthy();
expect(screen.queryByRole('button', { name: '重新生成' })).toBeNull();
expect(screen.queryByText('用途')).toBeNull();
});
@@ -638,7 +638,7 @@ describe('Match3DResultView', () => {
fireEvent.change(screen.getByLabelText('物品名称 4'), {
target: { value: '苹果' },
});
expect(screen.getByRole('button', { name: /生成物品素材 · 2光点/u })).toBeTruthy();
expect(screen.getByRole('button', { name: /生成物品素材 · 2泥点/u })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /生成物品素材/u }));
await waitFor(() => {
@@ -1109,7 +1109,7 @@ describe('Match3DResultView', () => {
fireEvent.change(screen.getByLabelText('UI背景图画面描述提示词'), {
target: { value: '新背景提示词' },
});
expect(screen.getByRole('button', { name: /重新生成 · 2光点/u })).toBeTruthy();
expect(screen.getByRole('button', { name: /重新生成 · 2泥点/u })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /重新生成/u }));
await waitFor(() => {
@@ -1369,7 +1369,7 @@ describe('Match3DResultView', () => {
'轻快, 休闲',
);
expect(screen.queryByLabelText('抓大鹅背景音乐提示词')).toBeNull();
expect(screen.getByRole('button', { name: /生成音乐 · 5光点/u })).toBeTruthy();
expect(screen.getByRole('button', { name: /生成音乐 · 5泥点/u })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /生成音乐/u }));
@@ -1,4 +1,4 @@
import {
import {
ArrowLeft,
CheckCircle2,
Eye,
@@ -1952,8 +1952,8 @@ function Match3DItemAssetDetail({
disabled={busy || soundBusy}
onClick={() => onGenerateClickSound(asset)}
className={`platform-icon-button h-9 w-9 ${busy || soundBusy ? 'cursor-not-allowed opacity-55' : ''}`}
aria-label={`生成点击音效,${MATCH3D_CLICK_SOUND_POINTS_COST}光点`}
title={`生成点击音效 · ${MATCH3D_CLICK_SOUND_POINTS_COST}光点`}
aria-label={`生成点击音效,${MATCH3D_CLICK_SOUND_POINTS_COST}泥点`}
title={`生成点击音效 · ${MATCH3D_CLICK_SOUND_POINTS_COST}泥点`}
>
{soundBusy ? (
<Loader2 className="h-4 w-4 animate-spin" />
@@ -2154,7 +2154,7 @@ function Match3DBatchAddItemsPanel({
) : (
<Plus className="h-4 w-4" />
)}
生成物品素材 · {pointsCost}光点
生成物品素材 · {pointsCost}泥点
</button>
</div>
</Match3DModalShell>
@@ -2332,7 +2332,7 @@ function Match3DMusicTab({
) : (
<Music className="h-4 w-4" />
)}
{currentMusic ? '重新生成音乐' : '生成音乐'} · {MATCH3D_BACKGROUND_MUSIC_POINTS_COST}光点
{currentMusic ? '重新生成音乐' : '生成音乐'} · {MATCH3D_BACKGROUND_MUSIC_POINTS_COST}泥点
</button>
</section>
@@ -2451,7 +2451,7 @@ function Match3DUIAssetsTab({
) : (
<Wand2 className="h-4 w-4" />
)}
重新生成 · {MATCH3D_UI_BACKGROUND_POINTS_COST}光点
重新生成 · {MATCH3D_UI_BACKGROUND_POINTS_COST}泥点
</button>
</div>
</div>
@@ -1003,7 +1003,7 @@ function escapePuzzleOnboardingSvgText(value: string) {
function buildPuzzleOnboardingFallbackImage(promptText: string) {
const trimmedPrompt = promptText.trim();
const displayPrompt = escapePuzzleOnboardingSvgText(
trimmedPrompt.slice(0, 12) || '百梦拼图',
trimmedPrompt.slice(0, 12) || '陶泥儿拼图',
);
return (
'data:image/svg+xml;utf8,' +
@@ -1065,7 +1065,7 @@ function buildPuzzleOnboardingFallbackWork(
profileId: `onboarding-local-profile-${seed}`,
ownerUserId: 'onboarding-guest',
sourceSessionId: null,
authorDisplayName: '百梦主',
authorDisplayName: '陶泥儿主',
workTitle: '梦境拼图',
workDescription: promptText,
levelName: level.levelName,
@@ -9731,7 +9731,7 @@ export function PlatformEntryFlowShellImpl({
}
setPublicSearchError(
resolveRpgCreationErrorMessage(error, '未找到对应的百梦号或作品号。'),
resolveRpgCreationErrorMessage(error, '未找到对应的陶泥号或作品号。'),
);
} finally {
setIsSearchingPublicCode(false);
@@ -12233,7 +12233,7 @@ export function PlatformEntryFlowShellImpl({
{searchedPublicUser.displayName}
</div>
<div className="mt-2 text-sm text-[var(--platform-text-soft)]">
百梦号 {searchedPublicUser.publicUserCode}
陶泥号 {searchedPublicUser.publicUserCode}
</div>
</div>
) : null}
@@ -66,7 +66,7 @@ function createBabyObjectMatchEntry(): PlatformEdutainmentGalleryCard {
profileId: 'baby-object-match-profile-1',
publicWorkCode: 'EDU-BABY01',
ownerUserId: 'user-1',
authorDisplayName: '百梦主',
authorDisplayName: '陶泥儿主',
worldName: '宝贝识物水果篮',
subtitle: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
summaryText: '将物品放入对应的篮子里。',
@@ -181,7 +181,7 @@ test('puzzle workspace submits the work form instead of agent chat', () => {
imageModel: 'gpt-image-2',
aiRedraw: true,
});
expect(screen.getByText('消耗2光点')).toBeTruthy();
expect(screen.getByText('消耗2泥点')).toBeTruthy();
expect(screen.queryByRole('button', { name: '补充剩余设定' })).toBeNull();
expect(screen.queryByText('旧会话消息不再渲染为聊天入口。')).toBeNull();
});
@@ -498,7 +498,7 @@ test('puzzle workspace hides prompt and cost when AI redraw is off', async () =>
expect((aiRedrawSwitch as HTMLInputElement).checked).toBe(true);
fireEvent.click(aiRedrawSwitch);
expect(screen.queryByLabelText('画面AI重绘要求(提示词)')).toBeNull();
expect(screen.queryByText('消耗2光点')).toBeNull();
expect(screen.queryByText('消耗2泥点')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: /生成拼图游戏草稿/u }));
File diff suppressed because it is too large Load Diff
@@ -288,9 +288,9 @@ describe('PuzzleResultView', () => {
within(dialog).getByRole('button', { name: /重新生成画面/u }),
);
const confirmDialog = screen.getByRole('dialog', {
name: '确认消耗光点',
name: '确认消耗泥点',
});
expect(within(confirmDialog).getByText('消耗 2 光点')).toBeTruthy();
expect(within(confirmDialog).getByText('消耗 2 泥点')).toBeTruthy();
fireEvent.click(within(confirmDialog).getByRole('button', { name: '确定' }));
expect(onExecuteAction).toHaveBeenCalledWith({
@@ -371,7 +371,7 @@ describe('PuzzleResultView', () => {
expect(
within(dialog).getByRole('button', { name: /生成画面/u }),
).toBeTruthy();
expect(within(dialog).getByText('消耗2光点')).toBeTruthy();
expect(within(dialog).getByText('消耗2泥点')).toBeTruthy();
expect(
within(dialog).getByText('等待时间可以制作更多关卡哦~'),
).toBeTruthy();
@@ -434,7 +434,7 @@ describe('PuzzleResultView', () => {
});
fireEvent.click(within(dialog).getByRole('button', { name: /生成画面/u }));
fireEvent.click(
within(screen.getByRole('dialog', { name: '确认消耗光点' })).getByRole(
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
'button',
{ name: '确定' },
),
@@ -481,7 +481,7 @@ describe('PuzzleResultView', () => {
fireEvent.click(screen.getByText('雨夜猫街'));
fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u }));
fireEvent.click(
within(screen.getByRole('dialog', { name: '确认消耗光点' })).getByRole(
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
'button',
{ name: '确定' },
),
@@ -517,7 +517,7 @@ describe('PuzzleResultView', () => {
fireEvent.click(screen.getByText('雨夜猫街'));
fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u }));
fireEvent.click(
within(screen.getByRole('dialog', { name: '确认消耗光点' })).getByRole(
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
'button',
{ name: '确定' },
),
@@ -604,7 +604,7 @@ describe('PuzzleResultView', () => {
rerender(
<PuzzleResultView
session={createSession()}
error="光点余额不足"
error="泥点余额不足"
isBusy={false}
onBack={() => {}}
onExecuteAction={onExecuteAction}
@@ -615,7 +615,7 @@ describe('PuzzleResultView', () => {
name: '发布拼图作品',
});
expect(publishDialog).toBeTruthy();
expect(within(publishDialog).getByText('光点余额不足')).toBeTruthy();
expect(within(publishDialog).getByText('泥点余额不足')).toBeTruthy();
});
test('generates six tags after work title and description are filled', () => {
@@ -730,7 +730,7 @@ describe('PuzzleResultView', () => {
fireEvent.change(screen.getByLabelText('拼图UI背景提示词'), {
target: { value: '新拼图UI背景提示词' },
});
expect(screen.getByRole('button', { name: /生成UI背景 · 2光点/u })).toBeTruthy();
expect(screen.getByRole('button', { name: /生成UI背景 · 2泥点/u })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /生成UI背景/u }));
expect(onExecuteAction).toHaveBeenCalledWith({
@@ -785,7 +785,7 @@ describe('PuzzleResultView', () => {
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
fireEvent.click(screen.getByRole('button', { name: '背景音乐' }));
expect(screen.getByRole('button', { name: /重新生成音乐 · 5光点/u })).toBeTruthy();
expect(screen.getByRole('button', { name: /重新生成音乐 · 5泥点/u })).toBeTruthy();
expect(screen.getByLabelText('拼图背景音乐').getAttribute('src')).toBe(
'https://signed.example.com/generated-puzzle-assets/session/audio/music.mp3',
@@ -965,7 +965,7 @@ describe('PuzzleResultView', () => {
fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u }));
fireEvent.click(
within(screen.getByRole('dialog', { name: '确认消耗光点' })).getByRole(
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
'button',
{ name: '确定' },
),
@@ -1011,7 +1011,7 @@ describe('PuzzleResultView', () => {
fireEvent.click(screen.getByText('雨夜猫街'));
fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u }));
fireEvent.click(
within(screen.getByRole('dialog', { name: '确认消耗光点' })).getByRole(
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
'button',
{ name: '确定' },
),
@@ -1048,7 +1048,7 @@ describe('PuzzleResultView', () => {
within(dialog).getByRole('button', { name: /重新生成画面/u }),
);
fireEvent.click(
within(screen.getByRole('dialog', { name: '确认消耗光点' })).getByRole(
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
'button',
{ name: '确定' },
),
@@ -955,7 +955,7 @@ function PuzzleLevelDetailDialog({
<Sparkles className="h-4 w-4" />
<span>{hasFormalImage ? '重新生成画面' : '生成画面'}</span>
<span className="rounded-full bg-white/24 px-2 py-0.5 text-[11px] font-bold">
消耗{PUZZLE_IMAGE_GENERATION_POINT_COST}光点
消耗{PUZZLE_IMAGE_GENERATION_POINT_COST}泥点
</span>
</span>
<span className="text-[11px] font-semibold leading-none text-white/78">
@@ -973,7 +973,7 @@ function PuzzleLevelDetailDialog({
<section
role="dialog"
aria-modal="true"
aria-label="确认消耗光点"
aria-label="确认消耗泥点"
className="platform-modal-shell platform-remap-surface w-full max-w-sm overflow-hidden rounded-[1.5rem] shadow-[0_24px_80px_rgba(0,0,0,0.45)]"
onClick={(event) => event.stopPropagation()}
>
@@ -982,11 +982,11 @@ function PuzzleLevelDetailDialog({
<Sparkles className="h-4 w-4" />
</span>
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
确认消耗光点
确认消耗泥点
</div>
</div>
<div className="px-5 py-4 text-sm font-semibold text-[var(--platform-text-base)]">
消耗 {PUZZLE_IMAGE_GENERATION_POINT_COST} 光点
消耗 {PUZZLE_IMAGE_GENERATION_POINT_COST} 泥点
</div>
<div className="flex items-center justify-end gap-3 border-t border-[var(--platform-subpanel-border)] px-5 py-4">
<button
@@ -1508,7 +1508,7 @@ function PuzzleUiAssetsTab({
) : (
<Wand2 className="h-4 w-4" />
)}
{firstLevel?.uiBackgroundImageSrc ? '重新生成' : '生成UI背景'} · {PUZZLE_IMAGE_GENERATION_POINT_COST}光点
{firstLevel?.uiBackgroundImageSrc ? '重新生成' : '生成UI背景'} · {PUZZLE_IMAGE_GENERATION_POINT_COST}泥点
</button>
</div>
</div>
@@ -1809,7 +1809,7 @@ function PuzzleMusicTab({
) : (
<Music className="h-4 w-4" />
)}
{currentMusic ? '重新生成音乐' : '生成音乐'} · {PUZZLE_BACKGROUND_MUSIC_POINT_COST}光点
{currentMusic ? '重新生成音乐' : '生成音乐'} · {PUZZLE_BACKGROUND_MUSIC_POINT_COST}泥点
</button>
</section>
@@ -1184,7 +1184,7 @@ test('道具确认弹窗暂停时间,提示只演示不直接移动拼块', as
fireEvent.click(screen.getByRole('button', { name: '提示' }));
expect(screen.getByRole('dialog', { name: '使用提示' })).toBeTruthy();
expect(screen.getByText('消耗 1 光点')).toBeTruthy();
expect(screen.getByText('消耗 1 泥点')).toBeTruthy();
expect(onPauseChange).toHaveBeenLastCalledWith(true);
await act(async () => {
@@ -1201,7 +1201,7 @@ test('道具确认弹窗暂停时间,提示只演示不直接移动拼块', as
test('道具使用失败时保留确认弹窗和暂停态', async () => {
const onPauseChange = vi.fn();
const onUseProp = vi.fn().mockRejectedValue(new Error('光点余额不足'));
const onUseProp = vi.fn().mockRejectedValue(new Error('泥点余额不足'));
const playingRun: PuzzleRunSnapshot = {
...clearedRun,
currentLevel: {
@@ -1234,7 +1234,7 @@ test('道具使用失败时保留确认弹窗和暂停态', async () => {
});
expect(screen.getByRole('dialog', { name: '冻结时间' })).toBeTruthy();
expect(screen.getByText('光点余额不足')).toBeTruthy();
expect(screen.getByText('泥点余额不足')).toBeTruthy();
expect(onPauseChange).toHaveBeenLastCalledWith(true);
});
@@ -1386,7 +1386,7 @@ test('失败弹窗支持重开当前关和续时确认', async () => {
within(failedDialog).getByRole('button', { name: '继续1分钟' }),
);
expect(screen.getByRole('dialog', { name: '继续1分钟' })).toBeTruthy();
expect(screen.getByText('消耗 1 光点')).toBeTruthy();
expect(screen.getByText('消耗 1 泥点')).toBeTruthy();
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: '确定' }));
@@ -1396,7 +1396,7 @@ test('失败弹窗支持重开当前关和续时确认', async () => {
});
test('失败续时扣费失败时保留确认弹窗', async () => {
const onUseProp = vi.fn().mockRejectedValue(new Error('光点余额不足'));
const onUseProp = vi.fn().mockRejectedValue(new Error('泥点余额不足'));
const failedRun: PuzzleRunSnapshot = {
...clearedRun,
currentLevel: {
@@ -1428,7 +1428,7 @@ test('失败续时扣费失败时保留确认弹窗', async () => {
});
expect(screen.getByRole('dialog', { name: '继续1分钟' })).toBeTruthy();
expect(screen.getByText('光点余额不足')).toBeTruthy();
expect(screen.getByText('泥点余额不足')).toBeTruthy();
});
test('查看原图开关打开覆盖层并在关闭后恢复计时', async () => {
@@ -2007,7 +2007,7 @@ export function PuzzleRuntimeShell({
</h2>
</header>
<div className="puzzle-runtime-dialog__body px-5 py-4 text-sm">
消耗 1 光点
消耗 1 泥点
{propConfirmError ? (
<div className="puzzle-runtime-error-chip mt-3 rounded-[0.9rem] border px-3 py-2 text-xs leading-5">
{propConfirmError}

Some files were not shown because too many files have changed in this diff Show More