收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { SquareHoleWorkProfile } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import { SquareHoleResultView } from './SquareHoleResultView';
|
||||
@@ -16,6 +22,11 @@ vi.mock('../../services/square-hole-works', () => ({
|
||||
updateSquareHoleWork: vi.fn(),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function createProfile(): SquareHoleWorkProfile {
|
||||
return {
|
||||
profileId: 'profile-1',
|
||||
@@ -88,6 +99,14 @@ test('square hole result view exposes test run and publish actions', async () =>
|
||||
expect(screen.getByRole('button', { name: '试玩' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '发布' })).toBeTruthy();
|
||||
|
||||
await user.clear(screen.getByLabelText('游戏名称'));
|
||||
await user.type(screen.getByLabelText('游戏名称'), '几何新挑战');
|
||||
const shapeNameInput = screen.getByLabelText('方块名称');
|
||||
const holePromptInput = screen.getByLabelText('洞口 1图片提示词');
|
||||
await user.clear(shapeNameInput);
|
||||
await user.type(shapeNameInput, '圆形钥匙');
|
||||
await user.clear(holePromptInput);
|
||||
await user.type(holePromptInput, '圆形洞口贴图');
|
||||
await user.click(screen.getByRole('button', { name: '试玩' }));
|
||||
await user.click(screen.getByRole('button', { name: '发布' }));
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
@@ -95,8 +114,217 @@ test('square hole result view exposes test run and publish actions', async () =>
|
||||
await waitFor(() => {
|
||||
expect(onStartTestRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(mockUpdateSquareHoleWork).toHaveBeenCalledWith(
|
||||
'profile-1',
|
||||
expect.objectContaining({
|
||||
gameName: '几何新挑战',
|
||||
shapeOptions: [expect.objectContaining({ label: '圆形钥匙' })],
|
||||
holeOptions: [expect.objectContaining({ imagePrompt: '圆形洞口贴图' })],
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(onPublished).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(onBack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('square hole autosave badges reuse PlatformPillBadge chrome', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { updateSquareHoleWork } = await import(
|
||||
'../../services/square-hole-works'
|
||||
);
|
||||
vi.mocked(updateSquareHoleWork).mockResolvedValue({
|
||||
item: createProfile(),
|
||||
} as Awaited<ReturnType<typeof updateSquareHoleWork>>);
|
||||
|
||||
render(
|
||||
<SquareHoleResultView
|
||||
profile={createProfile()}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('游戏名称'), {
|
||||
target: { value: '几何新挑战' },
|
||||
});
|
||||
|
||||
expect(screen.getByText('保存中').className).toContain(
|
||||
'border-[var(--platform-warm-border)]',
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(screen.getByText('已自动保存').className).toContain(
|
||||
'border-emerald-200',
|
||||
);
|
||||
});
|
||||
|
||||
test('square hole autosave failure badge uses PlatformPillBadge danger chrome', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { updateSquareHoleWork } = await import(
|
||||
'../../services/square-hole-works'
|
||||
);
|
||||
vi.mocked(updateSquareHoleWork).mockRejectedValue(new Error('保存失败'));
|
||||
|
||||
render(
|
||||
<SquareHoleResultView
|
||||
profile={createProfile()}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('游戏名称'), {
|
||||
target: { value: '几何异常挑战' },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
const failureBadge = screen
|
||||
.getAllByText('保存失败')
|
||||
.find((element) => element.tagName.toLowerCase() === 'span');
|
||||
expect(failureBadge?.className).toContain(
|
||||
'border-[var(--platform-button-danger-border)]',
|
||||
);
|
||||
});
|
||||
|
||||
test('square hole result view uses PlatformFieldLabel for section labels', () => {
|
||||
const { container } = render(
|
||||
<SquareHoleResultView
|
||||
profile={createProfile()}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
for (const label of ['游戏名称', '题材主题', '形状选项', '洞口选项']) {
|
||||
expect(screen.getByText(label).className).toContain('tracking-[0.18em]');
|
||||
}
|
||||
|
||||
const panels = Array.from(
|
||||
container.querySelectorAll('section.platform-subpanel'),
|
||||
);
|
||||
expect(panels).toHaveLength(4);
|
||||
for (const panel of panels) {
|
||||
expect(panel.className).toContain('rounded-[1.5rem]');
|
||||
expect(panel.className).toContain('sm:p-5');
|
||||
}
|
||||
|
||||
const optionCards = Array.from(
|
||||
container.querySelectorAll('div.bg-white\\/72'),
|
||||
).filter((element) => element.className.includes('p-3'));
|
||||
expect(optionCards).toHaveLength(2);
|
||||
for (const card of optionCards) {
|
||||
expect(card.className).toContain(
|
||||
'border-[var(--platform-subpanel-border)]',
|
||||
);
|
||||
expect(card.className).toContain('rounded-[1rem]');
|
||||
expect(card.className).toContain('p-3');
|
||||
}
|
||||
|
||||
for (const thumbnailButton of [
|
||||
screen.getByRole('button', { name: '查看方块贴图' }),
|
||||
screen.getByRole('button', { name: '查看洞口 1贴图' }),
|
||||
]) {
|
||||
expect(thumbnailButton.className).toContain('bg-white/82');
|
||||
expect(thumbnailButton.className).toContain('hover:bg-white');
|
||||
expect(thumbnailButton.className).toContain('disabled:cursor-not-allowed');
|
||||
}
|
||||
|
||||
for (const deleteButton of [
|
||||
screen.getByRole('button', { name: '删除形状选项' }),
|
||||
screen.getByRole('button', { name: '删除洞口选项' }),
|
||||
]) {
|
||||
expect(deleteButton.className).toContain('platform-icon-button');
|
||||
expect(deleteButton.className).toContain('p-2');
|
||||
}
|
||||
});
|
||||
|
||||
test('square hole shape and hole thumbnails use platform media frames', () => {
|
||||
render(
|
||||
<SquareHoleResultView
|
||||
profile={createProfile()}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
for (const thumbnailButton of [
|
||||
screen.getByRole('button', { name: '查看方块贴图' }),
|
||||
screen.getByRole('button', { name: '查看洞口 1贴图' }),
|
||||
]) {
|
||||
const mediaFrame = thumbnailButton.querySelector('div.relative');
|
||||
|
||||
expect(mediaFrame?.className).toContain('aspect-square');
|
||||
expect(mediaFrame?.className).toContain('bg-transparent');
|
||||
expect(mediaFrame?.className).toContain('rounded-none');
|
||||
expect(mediaFrame?.className).not.toContain(
|
||||
'bg-[var(--platform-subpanel-fill)]',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('square hole image dialog reuses platform media frame', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { squareHoleAssetClient } = await import(
|
||||
'../../services/square-hole-works'
|
||||
);
|
||||
vi.mocked(squareHoleAssetClient.listHistoryAssets).mockResolvedValue([]);
|
||||
|
||||
render(
|
||||
<SquareHoleResultView
|
||||
profile={createProfile()}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '查看封面图' }));
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: '封面图查看' });
|
||||
const image = screen.getByRole('img', { name: '封面图' });
|
||||
const mediaFrame = image.closest('div.relative');
|
||||
|
||||
expect(dialog).toBeTruthy();
|
||||
expect(mediaFrame?.className).toContain('aspect-[4/3]');
|
||||
expect(mediaFrame?.className).toContain('rounded-[1.35rem]');
|
||||
expect(mediaFrame?.className).toContain(
|
||||
'border-[var(--platform-subpanel-border)]',
|
||||
);
|
||||
expect(mediaFrame?.className).toContain('bg-[var(--platform-subpanel-fill)]');
|
||||
});
|
||||
|
||||
test('square hole cover and background buttons use platform media frames', () => {
|
||||
render(
|
||||
<SquareHoleResultView
|
||||
profile={createProfile()}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const coverFrame = screen
|
||||
.getByRole('button', { name: '查看封面图' })
|
||||
.querySelector('div.relative');
|
||||
const backgroundFrame = screen
|
||||
.getByRole('button', { name: '查看背景图' })
|
||||
.querySelector('div.relative');
|
||||
|
||||
expect(coverFrame?.className).toContain('aspect-[4/3]');
|
||||
expect(coverFrame?.className).toContain('rounded-none');
|
||||
expect(coverFrame?.className).toContain('bg-transparent');
|
||||
expect(coverFrame?.className).not.toContain(
|
||||
'bg-[var(--platform-subpanel-fill)]',
|
||||
);
|
||||
expect(backgroundFrame?.className).toContain('aspect-[16/9]');
|
||||
expect(backgroundFrame?.className).toContain('rounded-none');
|
||||
expect(backgroundFrame?.className).toContain('bg-transparent');
|
||||
expect(backgroundFrame?.className).not.toContain(
|
||||
'bg-[var(--platform-subpanel-fill)]',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
RefreshCw,
|
||||
Send,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { type ChangeEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
@@ -30,7 +29,20 @@ import {
|
||||
updateSquareHoleWork,
|
||||
} from '../../services/square-hole-works';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformAssetPickerGrid } from '../common/PlatformAssetPickerCard';
|
||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
|
||||
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformStatGrid } from '../common/PlatformStatGrid';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import {
|
||||
PlatformSelectField,
|
||||
PlatformTextField,
|
||||
} from '../common/PlatformTextField';
|
||||
|
||||
type SquareHoleResultViewProps = {
|
||||
profile: SquareHoleWorkProfile;
|
||||
@@ -440,32 +452,31 @@ function SquareHoleResultHeader({
|
||||
}) {
|
||||
const badge =
|
||||
autoSaveState === 'saving' ? (
|
||||
<div className="platform-pill platform-pill--warm px-3 py-1 text-[11px]">
|
||||
<PlatformPillBadge tone="warning" size="xs" className="px-3 py-1">
|
||||
保存中
|
||||
</div>
|
||||
</PlatformPillBadge>
|
||||
) : autoSaveState === 'saved' ? (
|
||||
<div className="platform-pill platform-pill--success px-3 py-1 text-[11px]">
|
||||
<PlatformPillBadge tone="success" size="xs" className="px-3 py-1">
|
||||
已自动保存
|
||||
</div>
|
||||
</PlatformPillBadge>
|
||||
) : autoSaveState === 'error' ? (
|
||||
<div className="platform-pill platform-pill--rose px-3 py-1 text-[11px]">
|
||||
<PlatformPillBadge tone="danger" size="xs" className="px-3 py-1">
|
||||
保存失败
|
||||
</div>
|
||||
</PlatformPillBadge>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onBack}
|
||||
disabled={isBusy}
|
||||
className={`platform-button platform-button--ghost min-h-0 self-start px-3 py-1.5 text-[11px] ${isBusy ? 'opacity-45' : ''}`}
|
||||
tone="ghost"
|
||||
size="xs"
|
||||
className="min-h-0 self-start gap-1.5 px-3 py-1.5 text-[11px]"
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
返回
|
||||
</span>
|
||||
</button>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
返回
|
||||
</PlatformActionButton>
|
||||
{badge}
|
||||
</div>
|
||||
);
|
||||
@@ -555,35 +566,33 @@ function SquareHoleImageSlotDialog({
|
||||
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
{slot.title}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
<PlatformModalCloseButton
|
||||
onClick={onClose}
|
||||
aria-label="关闭"
|
||||
className="platform-icon-button"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
label="关闭"
|
||||
variant="platformIcon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(16rem,0.78fr)]">
|
||||
<div className="space-y-3">
|
||||
<div className="aspect-[4/3] overflow-hidden rounded-[1.35rem] border border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)]">
|
||||
{currentImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={currentImageSrc}
|
||||
alt={slot.title}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center text-[var(--platform-text-soft)]">
|
||||
<Images className="h-10 w-10" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PlatformMediaFrame
|
||||
src={currentImageSrc}
|
||||
alt={slot.title}
|
||||
fallbackLabel={`${slot.title}占位图`}
|
||||
fallbackContent={<Images className="h-10 w-10" />}
|
||||
aspect="standard"
|
||||
surface="plain"
|
||||
className="rounded-[1.35rem]"
|
||||
fallbackClassName="tracking-normal text-[var(--platform-text-soft)]"
|
||||
/>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<label
|
||||
className={`platform-button platform-button--ghost flex min-h-11 cursor-pointer items-center justify-center gap-2 px-4 py-3 text-sm ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
<PlatformActionButton
|
||||
asChild="label"
|
||||
tone="ghost"
|
||||
size="md"
|
||||
aria-disabled={isBusy}
|
||||
className={`min-h-11 cursor-pointer gap-2 px-4 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
>
|
||||
<ImagePlus className="h-4 w-4" />
|
||||
上传图片
|
||||
@@ -594,12 +603,13 @@ function SquareHoleImageSlotDialog({
|
||||
disabled={isBusy}
|
||||
onChange={(event) => onUpload(slot, event)}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
onClick={onRegenerateImages}
|
||||
disabled={!canRegenerateImages || isBusy}
|
||||
className={`platform-button platform-button--secondary min-h-11 justify-center gap-2 px-4 py-3 text-sm ${!canRegenerateImages || isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
tone="secondary"
|
||||
size="md"
|
||||
className="min-h-11 gap-2 px-4"
|
||||
>
|
||||
{isRegeneratingImages ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -607,63 +617,34 @@ function SquareHoleImageSlotDialog({
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
AI生成图片
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 space-y-3">
|
||||
<div className="flex items-center gap-2 text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
<PlatformFieldLabel
|
||||
variant="section"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Images className="h-3.5 w-3.5" />
|
||||
历史生成
|
||||
</div>
|
||||
</PlatformFieldLabel>
|
||||
|
||||
{error ? (
|
||||
<div className="platform-banner platform-banner--danger text-sm leading-6">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex min-h-[14rem] items-center justify-center rounded-[1.25rem] border border-dashed border-[var(--platform-subpanel-border)] bg-white/52 px-6 text-center text-sm text-[var(--platform-text-base)]">
|
||||
读取中...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isLoading && !error && assets.length <= 0 ? (
|
||||
<div className="flex min-h-[14rem] items-center justify-center rounded-[1.25rem] border border-dashed border-[var(--platform-subpanel-border)] bg-white/52 px-6 text-center text-sm text-[var(--platform-text-base)]">
|
||||
暂无历史图片
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isLoading && assets.length > 0 ? (
|
||||
<div className="grid max-h-[24rem] grid-cols-2 gap-3 overflow-y-auto pr-1 sm:grid-cols-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{assets.map((asset) => (
|
||||
<button
|
||||
key={asset.assetObjectId}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => onSelectHistory(asset)}
|
||||
className={`overflow-hidden rounded-[1.1rem] 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={asset.ownerLabel || '历史图片'}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 px-3 py-3">
|
||||
<div className="truncate text-xs font-black text-[var(--platform-text-strong)]">
|
||||
{asset.ownerLabel || '未记录账号'}
|
||||
</div>
|
||||
<div className="text-[11px] leading-4 text-[var(--platform-text-base)]">
|
||||
{formatHistoryAssetDate(asset.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<PlatformAssetPickerGrid
|
||||
items={assets}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
loadingLabel="读取中..."
|
||||
emptyLabel="暂无历史图片"
|
||||
disabled={isBusy}
|
||||
getKey={(asset) => asset.assetObjectId}
|
||||
getImageSrc={(asset) => asset.imageSrc}
|
||||
getImageAlt={(asset) => asset.ownerLabel || '历史图片'}
|
||||
getTitle={(asset) => asset.ownerLabel || '未记录账号'}
|
||||
getSubtitle={(asset) => formatHistoryAssetDate(asset.createdAt)}
|
||||
onSelect={onSelectHistory}
|
||||
gridClassName="grid max-h-[24rem] grid-cols-2 gap-3 overflow-y-auto pr-1 sm:grid-cols-3 lg:grid-cols-2 xl:grid-cols-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -855,7 +836,9 @@ export function SquareHoleResultView({
|
||||
{
|
||||
visualAssetSlot: activeImageSlot.kind,
|
||||
visualAssetOptionId:
|
||||
activeImageSlot.shapeOptionId ?? activeImageSlot.holeOptionId ?? null,
|
||||
activeImageSlot.shapeOptionId ??
|
||||
activeImageSlot.holeOptionId ??
|
||||
null,
|
||||
},
|
||||
);
|
||||
setEditState(createEditState(item));
|
||||
@@ -894,7 +877,9 @@ export function SquareHoleResultView({
|
||||
setLocalError(null);
|
||||
} catch (caughtError) {
|
||||
setLocalError(
|
||||
caughtError instanceof Error ? caughtError.message : '套用历史图片失败。',
|
||||
caughtError instanceof Error
|
||||
? caughtError.message
|
||||
: '套用历史图片失败。',
|
||||
);
|
||||
} finally {
|
||||
setIsApplyingHistoryImage(false);
|
||||
@@ -947,7 +932,7 @@ export function SquareHoleResultView({
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
|
||||
<div className="grid gap-3 lg:grid-cols-[minmax(17rem,0.72fr)_minmax(0,1fr)]">
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||
<PlatformSubpanel radius="xl" padding="lg">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
@@ -958,32 +943,36 @@ export function SquareHoleResultView({
|
||||
assetKind: 'square_hole_cover_image',
|
||||
})
|
||||
}
|
||||
className={`group block aspect-[4/3] w-full overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-[radial-gradient(circle_at_42%_30%,rgba(125,211,252,0.28),transparent_34%),linear-gradient(135deg,rgba(15,23,42,0.16),rgba(20,184,166,0.18))] text-left ${busy ? 'cursor-not-allowed opacity-70' : ''}`}
|
||||
className={`group block w-full overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-[radial-gradient(circle_at_42%_30%,rgba(125,211,252,0.28),transparent_34%),linear-gradient(135deg,rgba(15,23,42,0.16),rgba(20,184,166,0.18))] text-left ${busy ? 'cursor-not-allowed opacity-70' : ''}`}
|
||||
aria-label="查看封面图"
|
||||
>
|
||||
{editState.coverImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={editState.coverImageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center text-slate-700">
|
||||
<ImagePlus className="h-10 w-10" />
|
||||
</div>
|
||||
)}
|
||||
<PlatformMediaFrame
|
||||
src={editState.coverImageSrc}
|
||||
alt=""
|
||||
fallbackLabel="封面图"
|
||||
fallbackContent={<ImagePlus className="h-10 w-10" />}
|
||||
aspect="standard"
|
||||
surface="none"
|
||||
className="rounded-none bg-transparent"
|
||||
fallbackClassName="tracking-normal text-slate-700"
|
||||
/>
|
||||
</button>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 text-center text-xs font-bold text-[var(--platform-text-base)]">
|
||||
<div className="rounded-[1rem] bg-white/68 px-2 py-2">
|
||||
{editState.shapeCountText || '-'} 个
|
||||
</div>
|
||||
<div className="rounded-[1rem] bg-white/68 px-2 py-2">
|
||||
{(draft?.publishReady ?? profile.publishReady)
|
||||
? '可发布'
|
||||
: '草稿'}
|
||||
</div>
|
||||
</div>
|
||||
<PlatformStatGrid
|
||||
items={[
|
||||
{ value: `${editState.shapeCountText || '-'} 个` },
|
||||
{
|
||||
value:
|
||||
(draft?.publishReady ?? profile.publishReady)
|
||||
? '可发布'
|
||||
: '草稿',
|
||||
},
|
||||
]}
|
||||
columns="two"
|
||||
density="compact"
|
||||
surface="plain"
|
||||
className="mt-3"
|
||||
itemClassName="border-0 bg-white/68"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
@@ -994,74 +983,71 @@ export function SquareHoleResultView({
|
||||
assetKind: 'square_hole_background_image',
|
||||
})
|
||||
}
|
||||
className={`mt-3 block aspect-[16/9] w-full overflow-hidden rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-[linear-gradient(135deg,rgba(15,23,42,0.12),rgba(34,197,94,0.16))] text-left ${busy ? 'cursor-not-allowed opacity-70' : ''}`}
|
||||
className={`mt-3 block w-full overflow-hidden rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-[linear-gradient(135deg,rgba(15,23,42,0.12),rgba(34,197,94,0.16))] text-left ${busy ? 'cursor-not-allowed opacity-70' : ''}`}
|
||||
aria-label="查看背景图"
|
||||
>
|
||||
{editState.backgroundImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={editState.backgroundImageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center text-slate-700">
|
||||
<ImagePlus className="h-8 w-8" />
|
||||
</div>
|
||||
)}
|
||||
<PlatformMediaFrame
|
||||
src={editState.backgroundImageSrc}
|
||||
alt=""
|
||||
fallbackLabel="背景图"
|
||||
fallbackContent={<ImagePlus className="h-8 w-8" />}
|
||||
aspect="landscape"
|
||||
surface="none"
|
||||
className="rounded-none bg-transparent"
|
||||
fallbackClassName="tracking-normal text-slate-700"
|
||||
/>
|
||||
</button>
|
||||
</section>
|
||||
</PlatformSubpanel>
|
||||
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||
<PlatformSubpanel radius="xl" padding="lg">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
<PlatformFieldLabel variant="section">
|
||||
游戏名称
|
||||
</span>
|
||||
<input
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
value={editState.gameName}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
setEditState({ ...editState, gameName: event.target.value })
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-base font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
size="lg"
|
||||
className="mt-2"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
标签
|
||||
</span>
|
||||
<input
|
||||
<PlatformFieldLabel variant="section">标签</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
value={editState.tagsText}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
setEditState({ ...editState, tagsText: event.target.value })
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
className="mt-2"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
简介
|
||||
</span>
|
||||
<textarea
|
||||
<PlatformFieldLabel variant="section">简介</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
value={editState.summary}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
setEditState({ ...editState, summary: event.target.value })
|
||||
}
|
||||
rows={3}
|
||||
className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
size="md"
|
||||
className="mt-2"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
<PlatformFieldLabel variant="section">
|
||||
题材主题
|
||||
</span>
|
||||
<input
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
value={editState.themeText}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
@@ -1070,15 +1056,15 @@ export function SquareHoleResultView({
|
||||
themeText: event.target.value,
|
||||
})
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
className="mt-2"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
<PlatformFieldLabel variant="section">
|
||||
反差规则
|
||||
</span>
|
||||
<input
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
value={editState.twistRule}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
@@ -1087,15 +1073,15 @@ export function SquareHoleResultView({
|
||||
twistRule: event.target.value,
|
||||
})
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
className="mt-2"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
<PlatformFieldLabel variant="section">
|
||||
背景提示
|
||||
</span>
|
||||
<input
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
value={editState.backgroundPrompt}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
@@ -1104,15 +1090,15 @@ export function SquareHoleResultView({
|
||||
backgroundPrompt: event.target.value,
|
||||
})
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
className="mt-2"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
<PlatformFieldLabel variant="section">
|
||||
形状数量
|
||||
</span>
|
||||
<input
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
value={editState.shapeCountText}
|
||||
inputMode="numeric"
|
||||
disabled={busy}
|
||||
@@ -1122,20 +1108,18 @@ export function SquareHoleResultView({
|
||||
shapeCountText: event.target.value,
|
||||
})
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
className="mt-2"
|
||||
/>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</PlatformSubpanel>
|
||||
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5 lg:col-span-2">
|
||||
<PlatformSubpanel radius="xl" padding="lg" className="lg:col-span-2">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
<PlatformFieldLabel variant="section">
|
||||
形状选项
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
</PlatformFieldLabel>
|
||||
<PlatformActionButton
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
setEditState((current) => ({
|
||||
@@ -1149,22 +1133,31 @@ export function SquareHoleResultView({
|
||||
],
|
||||
}))
|
||||
}
|
||||
className="platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px]"
|
||||
tone="ghost"
|
||||
size="xs"
|
||||
className="min-h-0 gap-1.5 px-3 py-1.5 text-[11px]"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
新增
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{editState.shapeOptions.map((option) => (
|
||||
<div
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
key={option.optionId}
|
||||
className="rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-3"
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="sm"
|
||||
>
|
||||
<div className="mb-3 flex items-start gap-3">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformSubpanel
|
||||
as="button"
|
||||
disabled={busy}
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="none"
|
||||
interactive
|
||||
onClick={() =>
|
||||
setActiveImageSlot({
|
||||
kind: 'shape',
|
||||
@@ -1173,24 +1166,26 @@ export function SquareHoleResultView({
|
||||
shapeOptionId: option.optionId,
|
||||
})
|
||||
}
|
||||
className={`relative grid h-20 w-20 shrink-0 place-items-center overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/82 text-slate-600 ${busy ? 'cursor-not-allowed opacity-70' : ''}`}
|
||||
className="relative grid h-20 w-20 shrink-0 place-items-center overflow-hidden bg-white/82 text-slate-600"
|
||||
aria-label={`查看${option.label || '形状'}贴图`}
|
||||
>
|
||||
{option.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={option.imageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<ImagePlus className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
<PlatformMediaFrame
|
||||
src={option.imageSrc}
|
||||
alt=""
|
||||
fallbackLabel={`${option.label || '形状'}贴图`}
|
||||
fallbackContent={<ImagePlus className="h-6 w-6" />}
|
||||
aspect="square"
|
||||
surface="none"
|
||||
className="h-full w-full rounded-none bg-transparent"
|
||||
fallbackClassName="tracking-normal text-slate-600"
|
||||
/>
|
||||
</PlatformSubpanel>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<input
|
||||
<PlatformTextField
|
||||
aria-label={`${option.label || '形状'}名称`}
|
||||
value={option.label}
|
||||
disabled={busy}
|
||||
density="compact"
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
@@ -1201,11 +1196,12 @@ export function SquareHoleResultView({
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
<select
|
||||
<PlatformSelectField
|
||||
aria-label={`${option.label || '形状'}目标洞口`}
|
||||
value={option.targetHoleId}
|
||||
disabled={busy}
|
||||
density="compact"
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
@@ -1216,7 +1212,6 @@ export function SquareHoleResultView({
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
>
|
||||
{editState.holeOptions.map((holeOption, holeIndex) => (
|
||||
<option
|
||||
@@ -1226,10 +1221,9 @@ export function SquareHoleResultView({
|
||||
{holeOption.label.trim() || `洞口 ${holeIndex + 1}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</PlatformSelectField>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
<PlatformIconButton
|
||||
disabled={busy || editState.shapeOptions.length <= 1}
|
||||
onClick={() =>
|
||||
setEditState((current) => ({
|
||||
@@ -1239,16 +1233,19 @@ export function SquareHoleResultView({
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="rounded-full p-2 text-slate-500 hover:bg-white/72 disabled:opacity-40"
|
||||
aria-label="删除形状选项"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
label="删除形状选项"
|
||||
icon={<Trash2 className="h-4 w-4" />}
|
||||
className="p-2 text-slate-500 hover:bg-white/72 disabled:opacity-40"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
aria-label={`${option.label || '形状'}图片提示词`}
|
||||
value={option.imagePrompt}
|
||||
disabled={busy}
|
||||
rows={2}
|
||||
size="xs"
|
||||
density="compact"
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
@@ -1259,20 +1256,18 @@ export function SquareHoleResultView({
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full resize-none rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm leading-5 text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</PlatformSubpanel>
|
||||
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5 lg:col-span-2">
|
||||
<PlatformSubpanel radius="xl" padding="lg" className="lg:col-span-2">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
<PlatformFieldLabel variant="section">
|
||||
洞口选项
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
</PlatformFieldLabel>
|
||||
<PlatformActionButton
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
setEditState((current) => ({
|
||||
@@ -1283,22 +1278,31 @@ export function SquareHoleResultView({
|
||||
],
|
||||
}))
|
||||
}
|
||||
className="platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px]"
|
||||
tone="ghost"
|
||||
size="xs"
|
||||
className="min-h-0 gap-1.5 px-3 py-1.5 text-[11px]"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
新增
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{editState.holeOptions.map((option) => (
|
||||
<div
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
key={option.holeId}
|
||||
className="rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-3"
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="sm"
|
||||
>
|
||||
<div className="mb-3 flex items-start gap-3">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformSubpanel
|
||||
as="button"
|
||||
disabled={busy}
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="none"
|
||||
interactive
|
||||
onClick={() =>
|
||||
setActiveImageSlot({
|
||||
kind: 'hole',
|
||||
@@ -1307,24 +1311,26 @@ export function SquareHoleResultView({
|
||||
holeOptionId: option.holeId,
|
||||
})
|
||||
}
|
||||
className={`relative grid h-20 w-20 shrink-0 place-items-center overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/82 text-slate-600 ${busy ? 'cursor-not-allowed opacity-70' : ''}`}
|
||||
className="relative grid h-20 w-20 shrink-0 place-items-center overflow-hidden bg-white/82 text-slate-600"
|
||||
aria-label={`查看${option.label || '洞口'}贴图`}
|
||||
>
|
||||
{option.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={option.imageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<ImagePlus className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
<PlatformMediaFrame
|
||||
src={option.imageSrc}
|
||||
alt=""
|
||||
fallbackLabel={`${option.label || '洞口'}贴图`}
|
||||
fallbackContent={<ImagePlus className="h-6 w-6" />}
|
||||
aspect="square"
|
||||
surface="none"
|
||||
className="h-full w-full rounded-none bg-transparent"
|
||||
fallbackClassName="tracking-normal text-slate-600"
|
||||
/>
|
||||
</PlatformSubpanel>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<input
|
||||
<PlatformTextField
|
||||
aria-label={`${option.label || '洞口'}名称`}
|
||||
value={option.label}
|
||||
disabled={busy}
|
||||
density="compact"
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
@@ -1335,12 +1341,15 @@ export function SquareHoleResultView({
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
<textarea
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
aria-label={`${option.label || '洞口'}图片提示词`}
|
||||
value={option.imagePrompt}
|
||||
disabled={busy}
|
||||
rows={2}
|
||||
size="xs"
|
||||
density="compact"
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
@@ -1351,11 +1360,9 @@ export function SquareHoleResultView({
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full resize-none rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm leading-5 text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
<PlatformIconButton
|
||||
disabled={busy || editState.holeOptions.length <= 1}
|
||||
onClick={() =>
|
||||
setEditState((current) => {
|
||||
@@ -1378,31 +1385,36 @@ export function SquareHoleResultView({
|
||||
};
|
||||
})
|
||||
}
|
||||
className="rounded-full p-2 text-slate-500 hover:bg-white/72 disabled:opacity-40"
|
||||
aria-label="删除洞口选项"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
label="删除洞口选项"
|
||||
icon={<Trash2 className="h-4 w-4" />}
|
||||
className="p-2 text-slate-500 hover:bg-white/72 disabled:opacity-40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</PlatformSubpanel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{displayError ? (
|
||||
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="platform"
|
||||
size="md"
|
||||
className="mt-3 rounded-2xl"
|
||||
>
|
||||
{displayError}
|
||||
</div>
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
|
||||
<div className="mt-3 flex flex-col gap-2 pb-[max(0.25rem,env(safe-area-inset-bottom))] sm:flex-row sm:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={handleStartTestRun}
|
||||
disabled={!canSubmit || busy}
|
||||
className={`platform-button platform-button--ghost min-h-11 justify-center gap-2 px-5 py-3 ${!canSubmit || busy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
tone="ghost"
|
||||
size="md"
|
||||
className="min-h-11 gap-2 px-5"
|
||||
>
|
||||
{isStartingTestRun ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -1410,12 +1422,12 @@ export function SquareHoleResultView({
|
||||
<Play className="h-4 w-4" />
|
||||
)}
|
||||
试玩
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
onClick={handlePublish}
|
||||
disabled={!canSubmit || busy}
|
||||
className={`platform-button platform-button--primary min-h-11 justify-center gap-2 px-5 py-3 ${!canSubmit || busy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
size="md"
|
||||
className="min-h-11 gap-2 px-5"
|
||||
>
|
||||
{isPublishing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -1425,7 +1437,7 @@ export function SquareHoleResultView({
|
||||
<Send className="h-4 w-4" />
|
||||
)}
|
||||
{profile.publicationStatus === 'published' ? '更新发布' : '发布'}
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
|
||||
{activeImageSlot ? (
|
||||
|
||||
Reference in New Issue
Block a user