收口创作流程统一总计划并修复等待页窄屏裁切
This commit is contained in:
@@ -5,7 +5,11 @@ import {
|
||||
getAdminCreationEntryConfig,
|
||||
upsertAdminCreationEntryConfig,
|
||||
} from '../api/adminApiClient';
|
||||
import type {AdminCreationEntryTypeConfigPayload} from '../api/adminApiTypes';
|
||||
import type {
|
||||
AdminCreationEntryTypeConfigPayload,
|
||||
UnifiedCreationFieldPayload,
|
||||
UnifiedCreationSpecPayload,
|
||||
} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {handlePageError} from './pageUtils';
|
||||
|
||||
@@ -30,6 +34,7 @@ export function AdminCreationEntrySwitchPage({
|
||||
const [categoryId, setCategoryId] = useState('recent');
|
||||
const [categoryLabel, setCategoryLabel] = useState('最近创作');
|
||||
const [categorySortOrder, setCategorySortOrder] = useState('10');
|
||||
const [unifiedCreationSpecJson, setUnifiedCreationSpecJson] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [listErrorMessage, setListErrorMessage] = useState('');
|
||||
@@ -66,6 +71,14 @@ export function AdminCreationEntrySwitchPage({
|
||||
|
||||
const targetId = selectedId.trim();
|
||||
setErrorMessage('');
|
||||
const unifiedCreationSpecResult = parseUnifiedCreationSpecJson(
|
||||
targetId,
|
||||
unifiedCreationSpecJson,
|
||||
);
|
||||
if (!unifiedCreationSpecResult.ok) {
|
||||
setErrorMessage(unifiedCreationSpecResult.message);
|
||||
return;
|
||||
}
|
||||
const confirmed = await confirmWrite({
|
||||
action: '保存创作入口开关',
|
||||
target: targetId,
|
||||
@@ -88,6 +101,7 @@ export function AdminCreationEntrySwitchPage({
|
||||
categoryId: categoryId.trim(),
|
||||
categoryLabel: categoryLabel.trim(),
|
||||
categorySortOrder: parseInteger(categorySortOrder),
|
||||
unifiedCreationSpec: unifiedCreationSpecResult.spec,
|
||||
});
|
||||
const nextEntries = sortEntries(response.entries);
|
||||
setEntries(nextEntries);
|
||||
@@ -114,6 +128,7 @@ export function AdminCreationEntrySwitchPage({
|
||||
setCategoryId(entry.categoryId);
|
||||
setCategoryLabel(entry.categoryLabel);
|
||||
setCategorySortOrder(String(entry.categorySortOrder));
|
||||
setUnifiedCreationSpecJson(formatUnifiedCreationSpecJson(entry.unifiedCreationSpec));
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -224,6 +239,26 @@ export function AdminCreationEntrySwitchPage({
|
||||
/>
|
||||
</label>
|
||||
|
||||
<section className="admin-subsection">
|
||||
<div className="admin-subsection-heading">
|
||||
<span>统一创作契约</span>
|
||||
<span>{unifiedCreationSpecJson.trim() ? '已配置' : '未配置'}</span>
|
||||
</div>
|
||||
{unifiedCreationSpecJson.trim() ? (
|
||||
<UnifiedCreationSpecSummary specJson={unifiedCreationSpecJson} />
|
||||
) : (
|
||||
<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 ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{errorMessage}
|
||||
@@ -246,6 +281,7 @@ export function AdminCreationEntrySwitchPage({
|
||||
<th>入口</th>
|
||||
<th>展示</th>
|
||||
<th>开放</th>
|
||||
<th>统一契约</th>
|
||||
<th>分类</th>
|
||||
<th>排序</th>
|
||||
</tr>
|
||||
@@ -264,6 +300,7 @@ export function AdminCreationEntrySwitchPage({
|
||||
</td>
|
||||
<td>{entry.visible ? '是' : '否'}</td>
|
||||
<td>{entry.open ? '是' : '否'}</td>
|
||||
<td>{entry.unifiedCreationSpec ? '是' : '否'}</td>
|
||||
<td>{entry.categoryLabel || entry.categoryId}</td>
|
||||
<td>{entry.sortOrder}</td>
|
||||
</tr>
|
||||
@@ -295,3 +332,162 @@ function parseInteger(value: string) {
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function formatUnifiedCreationSpecJson(
|
||||
spec: UnifiedCreationSpecPayload | null | undefined,
|
||||
) {
|
||||
return spec ? JSON.stringify(spec, null, 2) : '';
|
||||
}
|
||||
|
||||
function parseUnifiedCreationSpecJson(entryId: string, value: string) {
|
||||
const parsed = parseUnifiedCreationSpecSummaryJson(value);
|
||||
if (!parsed.ok || !parsed.spec) {
|
||||
return parsed;
|
||||
}
|
||||
if (parsed.spec.playId !== entryId) {
|
||||
return {ok: false as const, message: '统一创作契约 playId 必须与入口 ID 一致'};
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function validateUnifiedCreationSpec(value: unknown) {
|
||||
if (!isRecord(value)) {
|
||||
return {ok: false as const, message: '统一创作契约必须是对象'};
|
||||
}
|
||||
|
||||
const playId = readRequiredString(value, 'playId');
|
||||
if (!playId) {
|
||||
return {ok: false as const, message: '统一创作契约 playId 不能为空'};
|
||||
}
|
||||
|
||||
const title = readRequiredString(value, 'title');
|
||||
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 (new Set([workspaceStage, generationStage, resultStage]).size !== 3) {
|
||||
return {ok: false as const, message: '统一创作契约阶段不能重复'};
|
||||
}
|
||||
|
||||
if (!Array.isArray(value.fields) || value.fields.length === 0) {
|
||||
return {ok: false as const, message: '统一创作契约 fields 不能为空'};
|
||||
}
|
||||
|
||||
const fieldIds = new Set<string>();
|
||||
const fields: UnifiedCreationFieldPayload[] = [];
|
||||
for (const item of value.fields) {
|
||||
if (!isRecord(item)) {
|
||||
return {ok: false as const, message: '统一创作契约字段必须是对象'};
|
||||
}
|
||||
const id = readRequiredString(item, 'id');
|
||||
const label = readRequiredString(item, 'label');
|
||||
if (!id || !label) {
|
||||
return {ok: false as const, message: '统一创作契约字段 id 和 label 不能为空'};
|
||||
}
|
||||
if (fieldIds.has(id)) {
|
||||
return {ok: false as const, message: `统一创作契约字段 id 重复:${id}`};
|
||||
}
|
||||
fieldIds.add(id);
|
||||
if (!isUnifiedCreationFieldKind(item.kind)) {
|
||||
return {ok: false as const, message: `统一创作契约字段 kind 非法:${id}`};
|
||||
}
|
||||
if (typeof item.required !== 'boolean') {
|
||||
return {ok: false as const, message: `统一创作契约字段 required 非法:${id}`};
|
||||
}
|
||||
fields.push({
|
||||
id,
|
||||
kind: item.kind,
|
||||
label,
|
||||
required: item.required,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true as const,
|
||||
spec: {
|
||||
playId,
|
||||
title,
|
||||
workspaceStage,
|
||||
generationStage,
|
||||
resultStage,
|
||||
fields,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<dl className="admin-info-list">
|
||||
<div>
|
||||
<dt>玩法</dt>
|
||||
<dd>{parsed.spec.playId}</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>
|
||||
);
|
||||
}
|
||||
|
||||
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 isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isUnifiedCreationFieldKind(
|
||||
value: unknown,
|
||||
): value is UnifiedCreationFieldPayload['kind'] {
|
||||
return (
|
||||
value === 'text' ||
|
||||
value === 'select' ||
|
||||
value === 'image' ||
|
||||
value === 'audio'
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user