This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions

View File

@@ -0,0 +1,287 @@
import { Check, Puzzle, SlidersHorizontal, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import type { PuzzleCreativeTemplateSelection } from '../../../packages/shared/src/contracts/puzzleCreativeTemplate';
import { useAuthUi } from '../auth/AuthUiContext';
type CreativeAgentTemplateConfirmPanelProps = {
selection: PuzzleCreativeTemplateSelection;
isBusy: boolean;
onConfirm: (selection: PuzzleCreativeTemplateSelection) => void;
onCancel: () => void;
};
function clampLevelCount(value: number, selection: PuzzleCreativeTemplateSelection) {
const { min, max } = resolveLevelCountBounds(selection);
return Math.max(min, Math.min(max, value));
}
function resolveLevelCountBounds(selection: PuzzleCreativeTemplateSelection) {
if (selection.selectedLevelMode === 'single_level') {
return {
min: 1,
max: 1,
};
}
return {
min: 2,
max: 6,
};
}
function canUseLevelMode(
selection: PuzzleCreativeTemplateSelection,
mode: PuzzleCreativeTemplateSelection['selectedLevelMode'],
) {
if (selection.supportedLevelMode === 'single') {
return mode === 'single_level';
}
if (selection.supportedLevelMode === 'multi') {
return mode === 'multi_level';
}
return true;
}
export function CreativeAgentTemplateConfirmPanel({
selection,
isBusy,
onConfirm,
onCancel,
}: CreativeAgentTemplateConfirmPanelProps) {
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
const [isAdjustOpen, setIsAdjustOpen] = useState(false);
const [draftSelection, setDraftSelection] = useState(selection);
const levelCountBounds = resolveLevelCountBounds(draftSelection);
useEffect(() => {
setDraftSelection(selection);
}, [selection]);
const pointsText = `${draftSelection.costRange.minPoints}${draftSelection.costRange.maxPoints} 光点`;
const panel = (
<div
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[136] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
onClick={(event) => {
if (event.target === event.currentTarget && !isBusy) {
onCancel();
}
}}
>
<section
role="dialog"
aria-modal="true"
aria-label="确认拼图模板"
className="platform-modal-shell platform-remap-surface flex max-h-[min(90vh,42rem)] w-full max-w-xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
<div className="min-w-0">
<div className="truncate text-lg font-black text-[var(--platform-text-strong)]">
{draftSelection.title}
</div>
<div className="mt-1 text-sm font-semibold text-[var(--platform-text-base)]">
{pointsText}
</div>
</div>
<button
type="button"
disabled={isBusy}
onClick={onCancel}
className="platform-icon-button"
aria-label="取消模板"
title="取消"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
<div className="space-y-3">
<div className="overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/68">
<div className="aspect-[16/9] bg-[radial-gradient(circle_at_28%_20%,rgba(255,255,255,0.92),transparent_32%),linear-gradient(135deg,rgba(255,194,123,0.86),rgba(255,93,132,0.82)_52%,rgba(92,186,255,0.78))]">
{'previewImageSrc' in draftSelection &&
typeof draftSelection.previewImageSrc === 'string' &&
draftSelection.previewImageSrc.trim() ? (
<img
src={draftSelection.previewImageSrc}
alt={draftSelection.title}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full items-center justify-center">
<span className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-white/84 text-[var(--platform-text-strong)] shadow-sm">
<Puzzle className="h-6 w-6" />
</span>
</div>
)}
</div>
</div>
<div className="platform-subpanel rounded-[1.25rem] p-4">
<div className="text-sm font-semibold leading-6 text-[var(--platform-text-base)]">
{draftSelection.reason}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="platform-subpanel rounded-[1.15rem] p-4">
<div className="text-xs font-bold tracking-[0.16em] text-[var(--platform-text-soft)]">
</div>
<div className="mt-2 text-base font-black text-[var(--platform-text-strong)]">
{draftSelection.selectedLevelMode === 'single_level'
? '单关卡'
: '多关卡'}
</div>
</div>
<div className="platform-subpanel rounded-[1.15rem] p-4">
<div className="text-xs font-bold tracking-[0.16em] text-[var(--platform-text-soft)]">
</div>
<div className="mt-2 text-base font-black text-[var(--platform-text-strong)]">
{draftSelection.plannedLevelCount}
</div>
</div>
</div>
</div>
</div>
<div className="flex flex-col-reverse gap-3 border-t border-[var(--platform-subpanel-border)] px-5 py-4 pb-[calc(env(safe-area-inset-bottom,0px)+1rem)] sm:flex-row sm:justify-end">
<button
type="button"
disabled={isBusy}
onClick={() => setIsAdjustOpen((current) => !current)}
className="platform-button platform-button--ghost"
>
<span className="inline-flex items-center gap-2">
<SlidersHorizontal className="h-4 w-4" />
</span>
</button>
<button
type="button"
disabled={isBusy}
onClick={() => onConfirm(draftSelection)}
className="platform-button platform-button--primary"
>
<span className="inline-flex items-center gap-2">
<Check className="h-4 w-4" />
</span>
</button>
</div>
</section>
{isAdjustOpen ? (
<section
role="dialog"
aria-modal="true"
aria-label="调整拼图模板"
className="platform-modal-shell platform-remap-surface fixed inset-x-3 bottom-3 z-[138] mx-auto w-auto max-w-lg overflow-hidden rounded-[1.5rem] shadow-[0_18px_64px_rgba(0,0,0,0.42)] sm:inset-x-4 sm:bottom-auto sm:top-1/2 sm:-translate-y-1/2"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
<div className="text-base font-black text-[var(--platform-text-strong)]">
</div>
<button
type="button"
disabled={isBusy}
onClick={() => setIsAdjustOpen(false)}
className="platform-icon-button"
aria-label="关闭调整"
title="关闭"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="space-y-3 px-5 py-4">
<div className="grid grid-cols-2 gap-2 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/62 p-1">
{[
{ value: 'single_level' as const, label: '单关卡' },
{ value: 'multi_level' as const, label: '多关卡' },
].map((item) => (
<button
key={item.value}
type="button"
disabled={isBusy || !canUseLevelMode(draftSelection, item.value)}
onClick={() => {
setDraftSelection((current) => ({
...current,
selectedLevelMode: item.value,
plannedLevelCount:
item.value === 'single_level'
? 1
: Math.max(2, current.plannedLevelCount),
}));
}}
className={`min-h-10 rounded-[0.8rem] px-3 text-sm font-bold ${
draftSelection.selectedLevelMode === item.value
? 'bg-white text-[var(--platform-text-strong)] shadow-sm'
: 'text-[var(--platform-text-base)]'
}`}
>
{item.label}
</button>
))}
</div>
<label className="flex min-h-11 items-center gap-3">
<span className="shrink-0 text-sm font-bold text-[var(--platform-text-base)]">
</span>
<input
type="number"
min={levelCountBounds.min}
max={levelCountBounds.max}
disabled={
isBusy || draftSelection.selectedLevelMode === 'single_level'
}
value={draftSelection.plannedLevelCount}
onChange={(event) => {
const nextValue = Number.parseInt(
event.target.value || '1',
10,
);
setDraftSelection((current) => ({
...current,
plannedLevelCount: clampLevelCount(
Number.isNaN(nextValue) ? 1 : nextValue,
current,
),
}));
}}
className="min-h-11 min-w-0 flex-1 rounded-[0.9rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 text-sm font-bold text-[var(--platform-text-strong)] outline-none"
aria-label="计划关卡数"
/>
</label>
</div>
<div className="flex justify-end border-t border-[var(--platform-subpanel-border)] px-5 py-4 pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]">
<button
type="button"
disabled={isBusy}
onClick={() => setIsAdjustOpen(false)}
className="platform-button platform-button--primary"
>
</button>
</div>
</section>
) : null}
</div>
);
if (typeof document === 'undefined') {
return null;
}
return createPortal(panel, document.body);
}