收口创作入口契约后台表单
将统一创作契约泥点消耗改为数字字段并由前端格式化展示 将后台契约编辑从 JSON 文本改为结构化卡片与弹窗表单 隐藏玩法阶段等内部标识并按玩法默认映射自动带出 更新创作入口文档、团队记忆和回归测试
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Plus, RefreshCcw, Save, Trash2 } from 'lucide-react';
|
||||
import { Pencil, Plus, RefreshCcw, Save, Trash2, X } from 'lucide-react';
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
@@ -34,7 +34,100 @@ type AnnouncementFormBuildResult =
|
||||
| { ok: true; json: string }
|
||||
| { ok: false; message: string };
|
||||
|
||||
/** 统一创作契约字段的弹窗表单态。 */
|
||||
type UnifiedCreationSpecFieldFormItem = {
|
||||
id: string;
|
||||
fieldId: string;
|
||||
kind: UnifiedCreationFieldPayload['kind'];
|
||||
label: string;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
/** 统一创作契约弹窗表单态;保存入库前会重新组装为后端契约。 */
|
||||
type UnifiedCreationSpecFormState = {
|
||||
playId: string;
|
||||
title: string;
|
||||
mudPointCost: string;
|
||||
/** 内部阶段由已有契约或玩法默认映射带出,不在后台表单中开放编辑。 */
|
||||
workspaceStage: string;
|
||||
generationStage: string;
|
||||
resultStage: string;
|
||||
fields: UnifiedCreationSpecFieldFormItem[];
|
||||
};
|
||||
|
||||
type UnifiedCreationSpecStageState = Pick<
|
||||
UnifiedCreationSpecFormState,
|
||||
'workspaceStage' | 'generationStage' | 'resultStage'
|
||||
>;
|
||||
|
||||
const DEFAULT_UNIFIED_CREATION_STAGE_MAP: Record<
|
||||
string,
|
||||
UnifiedCreationSpecStageState
|
||||
> = {
|
||||
rpg: {
|
||||
workspaceStage: 'agent-workspace',
|
||||
generationStage: 'custom-world-generating',
|
||||
resultStage: 'custom-world-result',
|
||||
},
|
||||
'big-fish': {
|
||||
workspaceStage: 'big-fish-agent-workspace',
|
||||
generationStage: 'big-fish-generating',
|
||||
resultStage: 'big-fish-result',
|
||||
},
|
||||
puzzle: {
|
||||
workspaceStage: 'puzzle-agent-workspace',
|
||||
generationStage: 'puzzle-generating',
|
||||
resultStage: 'puzzle-result',
|
||||
},
|
||||
'puzzle-clear': {
|
||||
workspaceStage: 'puzzle-clear-workspace',
|
||||
generationStage: 'puzzle-clear-generating',
|
||||
resultStage: 'puzzle-clear-result',
|
||||
},
|
||||
match3d: {
|
||||
workspaceStage: 'match3d-agent-workspace',
|
||||
generationStage: 'match3d-generating',
|
||||
resultStage: 'match3d-result',
|
||||
},
|
||||
'jump-hop': {
|
||||
workspaceStage: 'jump-hop-workspace',
|
||||
generationStage: 'jump-hop-generating',
|
||||
resultStage: 'jump-hop-result',
|
||||
},
|
||||
'wooden-fish': {
|
||||
workspaceStage: 'wooden-fish-workspace',
|
||||
generationStage: 'wooden-fish-generating',
|
||||
resultStage: 'wooden-fish-result',
|
||||
},
|
||||
'square-hole': {
|
||||
workspaceStage: 'square-hole-agent-workspace',
|
||||
generationStage: 'square-hole-generating',
|
||||
resultStage: 'square-hole-result',
|
||||
},
|
||||
'bark-battle': {
|
||||
workspaceStage: 'bark-battle-workspace',
|
||||
generationStage: 'bark-battle-generating',
|
||||
resultStage: 'bark-battle-result',
|
||||
},
|
||||
'visual-novel': {
|
||||
workspaceStage: 'visual-novel-agent-workspace',
|
||||
generationStage: 'visual-novel-generating',
|
||||
resultStage: 'visual-novel-result',
|
||||
},
|
||||
'baby-object-match': {
|
||||
workspaceStage: 'baby-object-match-workspace',
|
||||
generationStage: 'baby-object-match-generating',
|
||||
resultStage: 'baby-object-match-result',
|
||||
},
|
||||
'creative-agent': {
|
||||
workspaceStage: 'creative-agent-workspace',
|
||||
generationStage: 'puzzle-generating',
|
||||
resultStage: 'puzzle-result',
|
||||
},
|
||||
};
|
||||
|
||||
let announcementFormItemSequence = 0;
|
||||
let unifiedCreationSpecFieldSequence = 0;
|
||||
|
||||
export function AdminCreationEntrySwitchPage({
|
||||
token,
|
||||
@@ -55,7 +148,12 @@ export function AdminCreationEntrySwitchPage({
|
||||
const [categoryId, setCategoryId] = useState('recommended');
|
||||
const [categoryLabel, setCategoryLabel] = useState('热门推荐');
|
||||
const [categorySortOrder, setCategorySortOrder] = useState('20');
|
||||
const [unifiedCreationSpecJson, setUnifiedCreationSpecJson] = useState('');
|
||||
const [unifiedCreationSpec, setUnifiedCreationSpec] =
|
||||
useState<UnifiedCreationSpecPayload | null>(null);
|
||||
const [unifiedCreationSpecForm, setUnifiedCreationSpecForm] =
|
||||
useState<UnifiedCreationSpecFormState | null>(null);
|
||||
const [unifiedCreationSpecFormError, setUnifiedCreationSpecFormError] =
|
||||
useState('');
|
||||
const [announcementItems, setAnnouncementItems] = useState<
|
||||
AnnouncementFormItem[]
|
||||
>([]);
|
||||
@@ -101,9 +199,9 @@ export function AdminCreationEntrySwitchPage({
|
||||
|
||||
const targetId = selectedId.trim();
|
||||
setErrorMessage('');
|
||||
const unifiedCreationSpecResult = parseUnifiedCreationSpecJson(
|
||||
const unifiedCreationSpecResult = validateUnifiedCreationSpecForEntry(
|
||||
targetId,
|
||||
unifiedCreationSpecJson,
|
||||
unifiedCreationSpec,
|
||||
);
|
||||
if (!unifiedCreationSpecResult.ok) {
|
||||
setErrorMessage(unifiedCreationSpecResult.message);
|
||||
@@ -193,9 +291,95 @@ export function AdminCreationEntrySwitchPage({
|
||||
setCategoryId(entry.categoryId);
|
||||
setCategoryLabel(entry.categoryLabel);
|
||||
setCategorySortOrder(String(entry.categorySortOrder));
|
||||
setUnifiedCreationSpecJson(
|
||||
formatUnifiedCreationSpecJson(entry.unifiedCreationSpec),
|
||||
setUnifiedCreationSpec(entry.unifiedCreationSpec ?? null);
|
||||
setUnifiedCreationSpecForm(null);
|
||||
setUnifiedCreationSpecFormError('');
|
||||
}
|
||||
|
||||
/** 打开统一创作契约弹窗;缺省时用当前入口 ID 和标题预填。 */
|
||||
function openUnifiedCreationSpecForm() {
|
||||
setUnifiedCreationSpecForm(
|
||||
buildUnifiedCreationSpecForm(
|
||||
unifiedCreationSpec,
|
||||
selectedId.trim(),
|
||||
title.trim(),
|
||||
),
|
||||
);
|
||||
setUnifiedCreationSpecFormError('');
|
||||
}
|
||||
|
||||
function closeUnifiedCreationSpecForm() {
|
||||
setUnifiedCreationSpecForm(null);
|
||||
setUnifiedCreationSpecFormError('');
|
||||
}
|
||||
|
||||
function applyUnifiedCreationSpecForm(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!unifiedCreationSpecForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = buildUnifiedCreationSpecFromForm(unifiedCreationSpecForm);
|
||||
if (!result.ok) {
|
||||
setUnifiedCreationSpecFormError(result.message);
|
||||
return;
|
||||
}
|
||||
if (result.spec.playId !== selectedId.trim()) {
|
||||
setUnifiedCreationSpecFormError('统一创作契约 playId 必须与入口 ID 一致');
|
||||
return;
|
||||
}
|
||||
setUnifiedCreationSpec(result.spec);
|
||||
closeUnifiedCreationSpecForm();
|
||||
}
|
||||
|
||||
function updateUnifiedCreationSpecForm(
|
||||
patch: Partial<Omit<UnifiedCreationSpecFormState, 'fields'>>,
|
||||
) {
|
||||
setUnifiedCreationSpecForm((currentForm) =>
|
||||
currentForm ? { ...currentForm, ...patch } : currentForm,
|
||||
);
|
||||
}
|
||||
|
||||
function updateUnifiedCreationSpecField(
|
||||
index: number,
|
||||
patch: Partial<Omit<UnifiedCreationSpecFieldFormItem, 'id'>>,
|
||||
) {
|
||||
setUnifiedCreationSpecForm((currentForm) =>
|
||||
currentForm
|
||||
? {
|
||||
...currentForm,
|
||||
fields: currentForm.fields.map((field, fieldIndex) =>
|
||||
fieldIndex === index ? { ...field, ...patch } : field,
|
||||
),
|
||||
}
|
||||
: currentForm,
|
||||
);
|
||||
}
|
||||
|
||||
function addUnifiedCreationSpecField() {
|
||||
setUnifiedCreationSpecForm((currentForm) =>
|
||||
currentForm
|
||||
? {
|
||||
...currentForm,
|
||||
fields: [...currentForm.fields, createUnifiedCreationSpecFieldFormItem()],
|
||||
}
|
||||
: currentForm,
|
||||
);
|
||||
}
|
||||
|
||||
function removeUnifiedCreationSpecField(index: number) {
|
||||
setUnifiedCreationSpecForm((currentForm) => {
|
||||
if (!currentForm) {
|
||||
return currentForm;
|
||||
}
|
||||
const fields = currentForm.fields.filter(
|
||||
(_, fieldIndex) => fieldIndex !== index,
|
||||
);
|
||||
return {
|
||||
...currentForm,
|
||||
fields: fields.length > 0 ? fields : [createUnifiedCreationSpecFieldFormItem()],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新单条公告表单字段,避免后台页面直接暴露 JSON 编辑。 */
|
||||
@@ -431,24 +615,34 @@ export function AdminCreationEntrySwitchPage({
|
||||
<div className="admin-subsection-heading">
|
||||
<span>统一创作契约</span>
|
||||
<span>
|
||||
{unifiedCreationSpecJson.trim() ? '已配置' : '未配置'}
|
||||
{unifiedCreationSpec ? '已配置' : '未配置'}
|
||||
</span>
|
||||
</div>
|
||||
{unifiedCreationSpecJson.trim() ? (
|
||||
<UnifiedCreationSpecSummary specJson={unifiedCreationSpecJson} />
|
||||
<div className="admin-form-actions">
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
type="button"
|
||||
onClick={openUnifiedCreationSpecForm}
|
||||
>
|
||||
<Pencil size={17} aria-hidden="true" />
|
||||
<span>{unifiedCreationSpec ? '修改契约' : '新增契约'}</span>
|
||||
</button>
|
||||
{unifiedCreationSpec ? (
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
type="button"
|
||||
onClick={() => setUnifiedCreationSpec(null)}
|
||||
>
|
||||
<Trash2 size={17} aria-hidden="true" />
|
||||
<span>清空契约</span>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{unifiedCreationSpec ? (
|
||||
<UnifiedCreationSpecCard spec={unifiedCreationSpec} />
|
||||
) : (
|
||||
<div className="admin-muted-text">未配置统一创作页契约</div>
|
||||
)}
|
||||
<label className="admin-field">
|
||||
<span>契约 JSON</span>
|
||||
<textarea
|
||||
rows={12}
|
||||
value={unifiedCreationSpecJson}
|
||||
onChange={(event) =>
|
||||
setUnifiedCreationSpecJson(event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{errorMessage ? (
|
||||
@@ -509,6 +703,171 @@ export function AdminCreationEntrySwitchPage({
|
||||
) : null}
|
||||
|
||||
{confirmDialog}
|
||||
{unifiedCreationSpecForm ? (
|
||||
<div
|
||||
className="admin-confirm-backdrop"
|
||||
role="presentation"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
closeUnifiedCreationSpecForm();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<form
|
||||
aria-labelledby="admin-unified-creation-spec-editor-title"
|
||||
aria-modal="true"
|
||||
className="admin-detail-panel admin-form admin-contract-dialog"
|
||||
role="dialog"
|
||||
onSubmit={applyUnifiedCreationSpecForm}
|
||||
>
|
||||
<div className="admin-panel-heading">
|
||||
<h3 id="admin-unified-creation-spec-editor-title">
|
||||
统一创作契约
|
||||
</h3>
|
||||
<button
|
||||
className="admin-ghost-button"
|
||||
title="关闭"
|
||||
type="button"
|
||||
onClick={closeUnifiedCreationSpecForm}
|
||||
>
|
||||
<X size={17} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>表头</span>
|
||||
<input
|
||||
value={unifiedCreationSpecForm.title}
|
||||
onChange={(event) =>
|
||||
updateUnifiedCreationSpecForm({
|
||||
title: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>泥点消耗</span>
|
||||
<input
|
||||
inputMode="numeric"
|
||||
min={1}
|
||||
step={1}
|
||||
type="number"
|
||||
value={unifiedCreationSpecForm.mudPointCost}
|
||||
onChange={(event) =>
|
||||
updateUnifiedCreationSpecForm({
|
||||
mudPointCost: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<section className="admin-subsection">
|
||||
<div className="admin-subsection-heading">
|
||||
<span>字段</span>
|
||||
<button
|
||||
className="admin-link-button"
|
||||
type="button"
|
||||
onClick={addUnifiedCreationSpecField}
|
||||
>
|
||||
<Plus size={15} aria-hidden="true" />
|
||||
新增字段
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-contract-field-editor-list">
|
||||
{unifiedCreationSpecForm.fields.map((field, index) => (
|
||||
<section className="admin-contract-field-editor" key={field.id}>
|
||||
<div className="admin-subsection-heading">
|
||||
<span>{`字段 ${index + 1}`}</span>
|
||||
<button
|
||||
className="admin-link-button"
|
||||
type="button"
|
||||
aria-label={`删除字段 ${index + 1}`}
|
||||
onClick={() => removeUnifiedCreationSpecField(index)}
|
||||
>
|
||||
<Trash2 size={15} aria-hidden="true" />
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-contract-field-editor-grid">
|
||||
<label className="admin-field">
|
||||
<span>{`字段 ${index + 1} ID`}</span>
|
||||
<input
|
||||
value={field.fieldId}
|
||||
onChange={(event) =>
|
||||
updateUnifiedCreationSpecField(index, {
|
||||
fieldId: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>{`字段 ${index + 1} 类型`}</span>
|
||||
<select
|
||||
value={field.kind}
|
||||
onChange={(event) =>
|
||||
updateUnifiedCreationSpecField(index, {
|
||||
kind: event.target
|
||||
.value as UnifiedCreationFieldPayload['kind'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="text">text</option>
|
||||
<option value="select">select</option>
|
||||
<option value="image">image</option>
|
||||
<option value="audio">audio</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>{`字段 ${index + 1} 标签`}</span>
|
||||
<input
|
||||
value={field.label}
|
||||
onChange={(event) =>
|
||||
updateUnifiedCreationSpecField(index, {
|
||||
label: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-switch-field admin-contract-required-toggle">
|
||||
<input
|
||||
checked={field.required}
|
||||
type="checkbox"
|
||||
onChange={(event) =>
|
||||
updateUnifiedCreationSpecField(index, {
|
||||
required: event.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span>必填</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{unifiedCreationSpecFormError ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{unifiedCreationSpecFormError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="admin-confirm-actions">
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
type="button"
|
||||
onClick={closeUnifiedCreationSpecForm}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button className="admin-primary-button" type="submit">
|
||||
应用修改
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -607,21 +966,100 @@ function escapeAnnouncementHtmlText(value: string): string {
|
||||
.replaceAll('"', '"');
|
||||
}
|
||||
|
||||
function formatUnifiedCreationSpecJson(
|
||||
spec: UnifiedCreationSpecPayload | null | undefined,
|
||||
) {
|
||||
return spec ? JSON.stringify(spec, null, 2) : '';
|
||||
function nextUnifiedCreationSpecFieldFormItemId() {
|
||||
unifiedCreationSpecFieldSequence += 1;
|
||||
return `unified-field-${unifiedCreationSpecFieldSequence}`;
|
||||
}
|
||||
|
||||
function parseUnifiedCreationSpecJson(entryId: string, value: string) {
|
||||
const parsed = parseUnifiedCreationSpecSummaryJson(value);
|
||||
if (!parsed.ok || !parsed.spec) {
|
||||
return parsed;
|
||||
function createUnifiedCreationSpecFieldFormItem(
|
||||
field?: UnifiedCreationFieldPayload,
|
||||
): UnifiedCreationSpecFieldFormItem {
|
||||
return {
|
||||
id: nextUnifiedCreationSpecFieldFormItemId(),
|
||||
fieldId: field?.id ?? '',
|
||||
kind: field?.kind ?? 'text',
|
||||
label: field?.label ?? '',
|
||||
required: field?.required ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
function buildUnifiedCreationSpecForm(
|
||||
spec: UnifiedCreationSpecPayload | null,
|
||||
entryId: string,
|
||||
entryTitle: string,
|
||||
): UnifiedCreationSpecFormState {
|
||||
const playId = spec?.playId ?? entryId;
|
||||
const stages = resolveUnifiedCreationSpecStages(spec, playId);
|
||||
return {
|
||||
playId,
|
||||
title: spec?.title ?? entryTitle,
|
||||
mudPointCost: String(normalizeMudPointCost(spec?.mudPointCost)),
|
||||
...stages,
|
||||
fields:
|
||||
spec?.fields.map((field) => createUnifiedCreationSpecFieldFormItem(field)) ??
|
||||
[createUnifiedCreationSpecFieldFormItem()],
|
||||
};
|
||||
}
|
||||
|
||||
function resolveUnifiedCreationSpecStages(
|
||||
spec: UnifiedCreationSpecPayload | null,
|
||||
playId: string,
|
||||
): UnifiedCreationSpecStageState {
|
||||
if (spec?.workspaceStage && spec.generationStage && spec.resultStage) {
|
||||
return {
|
||||
workspaceStage: spec.workspaceStage,
|
||||
generationStage: spec.generationStage,
|
||||
resultStage: spec.resultStage,
|
||||
};
|
||||
}
|
||||
if (parsed.spec.playId !== entryId) {
|
||||
|
||||
return (
|
||||
DEFAULT_UNIFIED_CREATION_STAGE_MAP[playId] ?? {
|
||||
workspaceStage: '',
|
||||
generationStage: '',
|
||||
resultStage: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function buildUnifiedCreationSpecFromForm(form: UnifiedCreationSpecFormState) {
|
||||
return validateUnifiedCreationSpec({
|
||||
playId: form.playId,
|
||||
title: form.title,
|
||||
mudPointCost: form.mudPointCost,
|
||||
workspaceStage: form.workspaceStage,
|
||||
generationStage: form.generationStage,
|
||||
resultStage: form.resultStage,
|
||||
fields: form.fields.map((field) => ({
|
||||
id: field.fieldId,
|
||||
kind: field.kind,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
function validateUnifiedCreationSpecForEntry(
|
||||
entryId: string,
|
||||
spec: UnifiedCreationSpecPayload | null,
|
||||
) {
|
||||
if (!spec) {
|
||||
return {ok: true as const, spec: null};
|
||||
}
|
||||
if (spec.playId !== entryId) {
|
||||
return {ok: false as const, message: '统一创作契约 playId 必须与入口 ID 一致'};
|
||||
}
|
||||
return parsed;
|
||||
return validateUnifiedCreationSpec(spec);
|
||||
}
|
||||
|
||||
function normalizeMudPointCost(value: number | null | undefined) {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
||||
? Math.trunc(value)
|
||||
: 10;
|
||||
}
|
||||
|
||||
function formatMudPointCostText(value: number | null | undefined) {
|
||||
return `${normalizeMudPointCost(value)}泥点数`;
|
||||
}
|
||||
|
||||
function validateUnifiedCreationSpec(value: unknown) {
|
||||
@@ -635,11 +1073,18 @@ function validateUnifiedCreationSpec(value: unknown) {
|
||||
}
|
||||
|
||||
const title = readRequiredString(value, 'title');
|
||||
const mudPointCost = readPositiveInteger(value, 'mudPointCost');
|
||||
const workspaceStage = readRequiredString(value, 'workspaceStage');
|
||||
const generationStage = readRequiredString(value, 'generationStage');
|
||||
const resultStage = readRequiredString(value, 'resultStage');
|
||||
if (!title || !workspaceStage || !generationStage || !resultStage) {
|
||||
return {ok: false as const, message: '统一创作契约标题和阶段不能为空'};
|
||||
if (!title) {
|
||||
return {ok: false as const, message: '统一创作契约标题不能为空'};
|
||||
}
|
||||
if (!workspaceStage || !generationStage || !resultStage) {
|
||||
return {ok: false as const, message: '该玩法缺少默认阶段配置,请先由开发接入'};
|
||||
}
|
||||
if (mudPointCost === null) {
|
||||
return {ok: false as const, message: '统一创作契约泥点消耗数量必须是大于 0 的整数'};
|
||||
}
|
||||
if (new Set([workspaceStage, generationStage, resultStage]).size !== 3) {
|
||||
return {ok: false as const, message: '统一创作契约阶段不能重复'};
|
||||
@@ -683,6 +1128,7 @@ function validateUnifiedCreationSpec(value: unknown) {
|
||||
spec: {
|
||||
playId,
|
||||
title,
|
||||
mudPointCost,
|
||||
workspaceStage,
|
||||
generationStage,
|
||||
resultStage,
|
||||
@@ -691,70 +1137,57 @@ function validateUnifiedCreationSpec(value: unknown) {
|
||||
};
|
||||
}
|
||||
|
||||
function UnifiedCreationSpecSummary({specJson}: {specJson: string}) {
|
||||
const parsed = parseUnifiedCreationSpecSummaryJson(specJson);
|
||||
if (!parsed.ok || !parsed.spec) {
|
||||
return (
|
||||
<div className="admin-alert" role="status">
|
||||
{'message' in parsed ? parsed.message : '未配置统一创作页契约'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UnifiedCreationSpecCard({spec}: {spec: UnifiedCreationSpecPayload}) {
|
||||
return (
|
||||
<dl className="admin-info-list">
|
||||
<div>
|
||||
<dt>玩法</dt>
|
||||
<dd>{parsed.spec.playId}</dd>
|
||||
<div className="admin-contract-card">
|
||||
<dl className="admin-info-list">
|
||||
<div>
|
||||
<dt>玩法</dt>
|
||||
<dd>{spec.playId}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>表头</dt>
|
||||
<dd>{spec.title}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>泥点消耗</dt>
|
||||
<dd>{formatMudPointCostText(spec.mudPointCost)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="admin-contract-field-list">
|
||||
{spec.fields.map((field) => (
|
||||
<div className="admin-contract-field-card" key={field.id}>
|
||||
<strong>{field.id}</strong>
|
||||
<span>{field.label}</span>
|
||||
<span>
|
||||
{field.kind} / {field.required ? '必填' : '选填'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<dt>表头</dt>
|
||||
<dd>{parsed.spec.title}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>阶段</dt>
|
||||
<dd>
|
||||
{parsed.spec.workspaceStage} / {parsed.spec.generationStage} /{' '}
|
||||
{parsed.spec.resultStage}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>字段</dt>
|
||||
<dd>{parsed.spec.fields.map((field) => field.id).join('、')}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function parseUnifiedCreationSpecSummaryJson(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return {ok: true as const, spec: null};
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false as const,
|
||||
message: error instanceof Error ? `契约 JSON 非法:${error.message}` : '契约 JSON 非法',
|
||||
};
|
||||
}
|
||||
|
||||
const validation = validateUnifiedCreationSpec(parsed);
|
||||
if (!validation.ok) {
|
||||
return validation;
|
||||
}
|
||||
|
||||
return {ok: true as const, spec: validation.spec};
|
||||
}
|
||||
|
||||
function readRequiredString(value: Record<string, unknown>, key: string) {
|
||||
const raw = value[key];
|
||||
return typeof raw === 'string' ? raw.trim() : '';
|
||||
}
|
||||
|
||||
function readPositiveInteger(value: Record<string, unknown>, key: string) {
|
||||
const raw = value[key];
|
||||
const numberValue =
|
||||
typeof raw === 'number'
|
||||
? raw
|
||||
: typeof raw === 'string'
|
||||
? Number(raw.trim())
|
||||
: NaN;
|
||||
if (!Number.isInteger(numberValue) || numberValue <= 0) {
|
||||
return null;
|
||||
}
|
||||
return numberValue;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user