This commit is contained in:
2026-05-13 00:28:07 +08:00
parent ef4f91a75e
commit 01c5ab985a
101 changed files with 10635 additions and 2292 deletions

View File

@@ -1,4 +1,4 @@
import { Loader2, Plus, Sparkles, WandSparkles, X } from 'lucide-react';
import { Loader2, Music2, Plus, Sparkles, WandSparkles, X } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import type {
@@ -26,13 +26,15 @@ type Match3DFormState = {
difficultyOptionId: Match3DDifficultyOptionId;
assetStyleId: Match3DAssetStyleOptionId;
customAssetStylePrompt: string;
generateClickSound: boolean;
};
const EMPTY_FORM_STATE: Match3DFormState = {
themeText: '',
difficultyOptionId: 'standard',
assetStyleId: 'clay-toy',
assetStyleId: 'flat-icon',
customAssetStylePrompt: '',
generateClickSound: false,
};
// 中文注释:入口页只暴露难度选项,消除次数和难度数值由选项稳定派生给后端。
@@ -40,7 +42,7 @@ const MATCH3D_DIFFICULTY_OPTIONS = [
{ id: 'easy', label: '轻松', clearCount: 8, difficulty: 2 },
{ id: 'standard', label: '标准', clearCount: 12, difficulty: 4 },
{ id: 'advanced', label: '进阶', clearCount: 16, difficulty: 6 },
{ id: 'hardcore', label: '硬核', clearCount: 20, difficulty: 8 },
{ id: 'hardcore', label: '硬核', clearCount: 21, difficulty: 8 },
] as const;
type Match3DDifficultyOptionId =
@@ -48,40 +50,46 @@ type Match3DDifficultyOptionId =
const MATCH3D_ASSET_STYLE_OPTIONS = [
{
id: 'clay-toy',
label: '黏土手作',
imageSrc: '/match3d-style-references/clay-toy.png',
prompt: '圆润、哑光、带轻微手捏痕迹的黏土手作 3D 素材风格。',
id: 'flat-icon',
label: '扁平图标',
imageSrc: '/match3d-style-references/flat-icon.png',
prompt:
'干净扁平的 2D 游戏道具图标风格,正面视角,色块清楚,边缘硬朗,高可读性,适合移动端休闲游戏素材。',
},
{
id: 'low-poly',
label: '低多边形',
imageSrc: '/match3d-style-references/low-poly.png',
prompt: '块面清晰、轮廓简洁、颜色分区明确的低多边形 3D 素材风格。',
id: 'cel-cartoon',
label: '赛璐璐卡通',
imageSrc: '/match3d-style-references/cel-cartoon.png',
prompt:
'明亮赛璐璐卡通 2D 游戏道具风格,清晰线稿,硬边阴影,饱和配色,轮廓醒目。',
},
{
id: 'toy-plastic',
label: '玩具塑料',
imageSrc: '/match3d-style-references/toy-plastic.png',
prompt: '亮面、光滑、有柔和高光的玩具塑料 3D 素材风格。',
id: 'pixel-retro',
label: '像素复古',
imageSrc: '/match3d-style-references/pixel-retro.png',
prompt:
'复古像素 2D 游戏道具素材风格,有限色板,清晰像素边缘,主体轮廓稳定,不使用真实 3D 渲染。',
},
{
id: 'wood-carved',
label: '木质雕刻',
imageSrc: '/match3d-style-references/wood-carved.png',
prompt: '保留木纹和手工雕刻感的温润木质 3D 素材风格。',
id: 'watercolor',
label: '手绘水彩',
imageSrc: '/match3d-style-references/watercolor.png',
prompt:
'手绘水彩 2D 道具素材风格,柔和纸张纹理,透明叠色,边缘轻微晕染,主体仍保持清楚可读。',
},
{
id: 'voxel-block',
label: '体素积木',
imageSrc: '/match3d-style-references/voxel-block.png',
prompt: '由小方块构成、边缘清晰、带游戏感的体素积木 3D 素材风格。',
id: 'sticker-outline',
label: '贴纸描边',
imageSrc: '/match3d-style-references/sticker-outline.png',
prompt:
'贴纸描边 2D 游戏道具素材风格,粗白边与深色外轮廓,柔和投影,色彩活泼,适合休闲消除游戏。',
},
{
id: 'metal-mecha',
label: '金属机甲',
imageSrc: '/match3d-style-references/metal-mecha.png',
prompt: '带金属拉丝、柔和高光和轻科幻感的金属机甲 3D 素材风格。',
id: 'painterly-icon',
label: '厚涂图标',
imageSrc: '/match3d-style-references/painterly-icon.png',
prompt:
'厚涂 2D 游戏道具图标风格,笔触细腻,体积光影明确,中心构图,保持图标级清晰剪影。',
},
{
id: 'custom',
@@ -149,7 +157,7 @@ function resolveAssetStyleOptionId(
return matchedOption.id;
}
return assetStylePrompt?.trim() ? 'custom' : 'clay-toy';
return assetStylePrompt?.trim() ? 'custom' : 'flat-icon';
}
function resolveInitialFormState(
@@ -164,21 +172,13 @@ function resolveInitialFormState(
initialFormPayload?.seedText?.trim() ||
'';
const clearCount =
initialFormPayload?.clearCount ??
config?.clearCount ??
null;
initialFormPayload?.clearCount ?? config?.clearCount ?? null;
const difficulty =
initialFormPayload?.difficulty ??
config?.difficulty ??
null;
initialFormPayload?.difficulty ?? config?.difficulty ?? null;
const assetStyleId =
initialFormPayload?.assetStyleId ??
config?.assetStyleId ??
null;
initialFormPayload?.assetStyleId ?? config?.assetStyleId ?? null;
const assetStylePrompt =
initialFormPayload?.assetStylePrompt ??
config?.assetStylePrompt ??
'';
initialFormPayload?.assetStylePrompt ?? config?.assetStylePrompt ?? '';
return {
...EMPTY_FORM_STATE,
@@ -186,6 +186,10 @@ function resolveInitialFormState(
difficultyOptionId: resolveDifficultyOptionId(difficulty, clearCount),
assetStyleId: resolveAssetStyleOptionId(assetStyleId, assetStylePrompt),
customAssetStylePrompt: assetStylePrompt,
generateClickSound:
initialFormPayload?.generateClickSound ??
config?.generateClickSound ??
false,
};
}
@@ -255,11 +259,13 @@ export function Match3DAgentWorkspace({
assetStyleId: formState.assetStyleId,
assetStyleLabel,
assetStylePrompt,
generateClickSound: formState.generateClickSound,
}),
[
assetStyleLabel,
assetStylePrompt,
formState.assetStyleId,
formState.generateClickSound,
selectedDifficultyOption,
themeText,
],
@@ -290,7 +296,10 @@ export function Match3DAgentWorkspace({
}
if (session) {
onExecuteAction({ action: 'match3d_compile_draft' });
onExecuteAction({
action: 'match3d_compile_draft',
generateClickSound: formState.generateClickSound,
});
}
};
@@ -350,11 +359,11 @@ export function Match3DAgentWorkspace({
<div className="flex min-h-0 flex-col gap-2 overflow-hidden">
<div className="min-h-0 rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/52 p-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.78)]">
<div className="mb-1.5 text-sm font-black text-[var(--platform-text-strong)]">
3D素材风格
2D素材风格
</div>
<div
className="flex snap-x gap-2.5 overflow-x-auto overscroll-x-contain pb-0.5 scrollbar-hide touch-pan-x [-webkit-overflow-scrolling:touch]"
aria-label="3D素材风格"
aria-label="2D素材风格"
>
{MATCH3D_ASSET_STYLE_OPTIONS.map((option) => {
const selected = formState.assetStyleId === option.id;
@@ -424,8 +433,7 @@ export function Match3DAgentWorkspace({
</div>
<div className="grid grid-cols-4 gap-1.5 sm:gap-2 lg:grid-cols-2">
{MATCH3D_DIFFICULTY_OPTIONS.map((option) => {
const selected =
formState.difficultyOptionId === option.id;
const selected = formState.difficultyOptionId === option.id;
return (
<button
key={option.id}
@@ -450,6 +458,43 @@ export function Match3DAgentWorkspace({
})}
</div>
</div>
<button
type="button"
disabled={isBusy}
onClick={() =>
setFormState((current) => ({
...current,
generateClickSound: !current.generateClickSound,
}))
}
className={`flex min-h-12 shrink-0 items-center justify-between gap-3 rounded-[1.05rem] border px-3 py-2.5 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-200 ${
formState.generateClickSound
? 'border-rose-200 bg-rose-50/80 text-rose-700 shadow-[0_8px_18px_rgba(244,63,94,0.10)]'
: 'border-[var(--platform-subpanel-border)] bg-white/58 text-[var(--platform-text-strong)] hover:border-rose-200 hover:bg-white/86'
} ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
aria-pressed={formState.generateClickSound}
aria-label="生成音效"
>
<span className="inline-flex min-w-0 items-center gap-2">
<Music2 className="h-4 w-4 shrink-0" />
<span className="truncate text-sm font-black"></span>
</span>
<span
className={`relative h-6 w-11 shrink-0 rounded-full transition ${
formState.generateClickSound
? 'bg-rose-400'
: 'bg-slate-200'
}`}
aria-hidden="true"
>
<span
className={`absolute top-1 h-4 w-4 rounded-full bg-white shadow-sm transition ${
formState.generateClickSound ? 'left-6' : 'left-1'
}`}
/>
</span>
</button>
</div>
</div>
@@ -511,10 +556,12 @@ export function Match3DAgentWorkspace({
</div>
<textarea
value={draftCustomStylePrompt}
onChange={(event) => setDraftCustomStylePrompt(event.target.value)}
onChange={(event) =>
setDraftCustomStylePrompt(event.target.value)
}
rows={4}
className="mt-4 h-[7.5rem] w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
aria-label="自定义3D素材风格描述"
aria-label="自定义2D素材风格描述"
/>
<div className="mt-5 grid grid-cols-2 gap-3">
<button