新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
291 lines
10 KiB
TypeScript
291 lines
10 KiB
TypeScript
import { Check, Puzzle, SlidersHorizontal } 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';
|
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
|
import { PlatformIconBadge } from '../common/PlatformIconBadge';
|
|
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
|
|
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
|
|
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
|
|
import { PlatformStatGrid } from '../common/PlatformStatGrid';
|
|
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
|
import { PlatformTextField } from '../common/PlatformTextField';
|
|
|
|
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>
|
|
<PlatformModalCloseButton
|
|
label="取消模板"
|
|
variant="platformIcon"
|
|
disabled={isBusy}
|
|
onClick={onCancel}
|
|
title="取消"
|
|
/>
|
|
</div>
|
|
|
|
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
<div className="space-y-3">
|
|
<PlatformMediaFrame
|
|
src={
|
|
'previewImageSrc' in draftSelection &&
|
|
typeof draftSelection.previewImageSrc === 'string'
|
|
? draftSelection.previewImageSrc
|
|
: ''
|
|
}
|
|
alt={draftSelection.title}
|
|
fallbackLabel={draftSelection.title}
|
|
fallbackContent={
|
|
<PlatformIconBadge
|
|
icon={<Puzzle className="h-6 w-6" />}
|
|
size="xl"
|
|
tone="softBright"
|
|
/>
|
|
}
|
|
aspect="landscape"
|
|
surface="soft"
|
|
className="rounded-[1.25rem]"
|
|
fallbackShellClassName="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))]"
|
|
fallbackClassName="tracking-normal"
|
|
/>
|
|
|
|
<PlatformSubpanel as="div" radius="md">
|
|
<div className="text-sm font-semibold leading-6 text-[var(--platform-text-base)]">
|
|
{draftSelection.reason}
|
|
</div>
|
|
</PlatformSubpanel>
|
|
|
|
<PlatformStatGrid
|
|
items={[
|
|
{
|
|
label: '关卡模式',
|
|
value:
|
|
draftSelection.selectedLevelMode === 'single_level'
|
|
? '单关卡'
|
|
: '多关卡',
|
|
},
|
|
{
|
|
label: '计划关卡',
|
|
value: `${draftSelection.plannedLevelCount} 关`,
|
|
},
|
|
]}
|
|
columns="two"
|
|
order="labelFirst"
|
|
surface="plain"
|
|
textAlign="left"
|
|
itemClassName="rounded-[1.15rem] p-4"
|
|
/>
|
|
</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">
|
|
<PlatformActionButton
|
|
tone="ghost"
|
|
disabled={isBusy}
|
|
onClick={() => setIsAdjustOpen((current) => !current)}
|
|
>
|
|
<span className="inline-flex items-center gap-2">
|
|
<SlidersHorizontal className="h-4 w-4" />
|
|
调整
|
|
</span>
|
|
</PlatformActionButton>
|
|
<PlatformActionButton
|
|
disabled={isBusy}
|
|
onClick={() => onConfirm(draftSelection)}
|
|
>
|
|
<span className="inline-flex items-center gap-2">
|
|
<Check className="h-4 w-4" />
|
|
确认
|
|
</span>
|
|
</PlatformActionButton>
|
|
</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>
|
|
<PlatformModalCloseButton
|
|
label="关闭调整"
|
|
variant="platformIcon"
|
|
disabled={isBusy}
|
|
onClick={() => setIsAdjustOpen(false)}
|
|
title="关闭"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3 px-5 py-4">
|
|
<PlatformSegmentedTabs
|
|
items={[
|
|
{
|
|
id: 'single_level',
|
|
label: '单关卡',
|
|
disabled:
|
|
isBusy || !canUseLevelMode(draftSelection, 'single_level'),
|
|
},
|
|
{
|
|
id: 'multi_level',
|
|
label: '多关卡',
|
|
disabled:
|
|
isBusy || !canUseLevelMode(draftSelection, 'multi_level'),
|
|
},
|
|
]}
|
|
activeId={draftSelection.selectedLevelMode}
|
|
onChange={(nextLevelMode) => {
|
|
setDraftSelection((current) => ({
|
|
...current,
|
|
selectedLevelMode: nextLevelMode,
|
|
plannedLevelCount:
|
|
nextLevelMode === 'single_level'
|
|
? 1
|
|
: Math.max(2, current.plannedLevelCount),
|
|
}));
|
|
}}
|
|
radius="md"
|
|
size="compact"
|
|
/>
|
|
|
|
<label className="flex min-h-11 items-center gap-3">
|
|
<span className="shrink-0 text-sm font-bold text-[var(--platform-text-base)]">
|
|
关卡数
|
|
</span>
|
|
<PlatformTextField
|
|
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,
|
|
),
|
|
}));
|
|
}}
|
|
density="compact"
|
|
className="min-h-11 min-w-0 flex-1 font-bold"
|
|
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)]">
|
|
<PlatformActionButton
|
|
disabled={isBusy}
|
|
onClick={() => setIsAdjustOpen(false)}
|
|
>
|
|
完成
|
|
</PlatformActionButton>
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
</div>
|
|
);
|
|
|
|
if (typeof document === 'undefined') {
|
|
return null;
|
|
}
|
|
|
|
return createPortal(panel, document.body);
|
|
}
|