点赞和改造开关加入后台配置
This commit is contained in:
@@ -5,10 +5,12 @@ import {
|
||||
getAdminCreationEntryConfig,
|
||||
upsertAdminCreationEntryBanners,
|
||||
upsertAdminCreationEntryConfig,
|
||||
upsertAdminPublicWorkInteractions,
|
||||
} from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminCreationEntryEventBannerPayload,
|
||||
AdminCreationEntryTypeConfigPayload,
|
||||
PublicWorkInteractionConfigPayload,
|
||||
UnifiedCreationFieldPayload,
|
||||
UnifiedCreationSpecPayload,
|
||||
} from '../api/adminApiTypes';
|
||||
@@ -129,14 +131,28 @@ const DEFAULT_UNIFIED_CREATION_STAGE_MAP: Record<
|
||||
let announcementFormItemSequence = 0;
|
||||
let unifiedCreationSpecFieldSequence = 0;
|
||||
|
||||
const PUBLIC_WORK_SOURCE_LABELS: Record<string, string> = {
|
||||
'custom-world': 'RPG',
|
||||
'big-fish': '摸鱼',
|
||||
puzzle: '拼图',
|
||||
'puzzle-clear': '拼消消',
|
||||
'jump-hop': '跳一跳',
|
||||
'wooden-fish': '敲木鱼',
|
||||
match3d: '抓大鹅',
|
||||
'square-hole': '方洞挑战',
|
||||
'visual-novel': '视觉小说',
|
||||
'bark-battle': '汪汪声浪',
|
||||
edutainment: '宝贝识物',
|
||||
};
|
||||
|
||||
export function AdminCreationEntrySwitchPage({
|
||||
token,
|
||||
onUnauthorized,
|
||||
mode = 'switches',
|
||||
}: AdminCreationEntrySwitchPageProps) {
|
||||
const [entries, setEntries] = useState<
|
||||
AdminCreationEntryTypeConfigPayload[]
|
||||
>([]);
|
||||
const [entries, setEntries] = useState<AdminCreationEntryTypeConfigPayload[]>(
|
||||
[],
|
||||
);
|
||||
const [selectedId, setSelectedId] = useState('puzzle');
|
||||
const [title, setTitle] = useState('');
|
||||
const [subtitle, setSubtitle] = useState('');
|
||||
@@ -157,12 +173,17 @@ export function AdminCreationEntrySwitchPage({
|
||||
const [announcementItems, setAnnouncementItems] = useState<
|
||||
AnnouncementFormItem[]
|
||||
>([]);
|
||||
const [publicWorkInteractions, setPublicWorkInteractions] = useState<
|
||||
PublicWorkInteractionConfigPayload[]
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isSavingBanners, setIsSavingBanners] = useState(false);
|
||||
const [isSavingInteractions, setIsSavingInteractions] = useState(false);
|
||||
const [listErrorMessage, setListErrorMessage] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [bannerErrorMessage, setBannerErrorMessage] = useState('');
|
||||
const [interactionErrorMessage, setInteractionErrorMessage] = useState('');
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
const isAnnouncementMode = mode === 'announcements';
|
||||
|
||||
@@ -179,6 +200,7 @@ export function AdminCreationEntrySwitchPage({
|
||||
const nextEntries = sortEntries(response.entries);
|
||||
setEntries(nextEntries);
|
||||
setAnnouncementItems(formatEventBannersFormItems(response.eventBanners));
|
||||
setPublicWorkInteractions(response.publicWorkInteractions ?? []);
|
||||
fillForm(
|
||||
nextEntries.find((entry) => entry.id === selectedId) ??
|
||||
nextEntries[0] ??
|
||||
@@ -234,6 +256,7 @@ export function AdminCreationEntrySwitchPage({
|
||||
const nextEntries = sortEntries(response.entries);
|
||||
setEntries(nextEntries);
|
||||
setAnnouncementItems(formatEventBannersFormItems(response.eventBanners));
|
||||
setPublicWorkInteractions(response.publicWorkInteractions ?? []);
|
||||
fillForm(nextEntries.find((entry) => entry.id === targetId) ?? null);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||
@@ -269,6 +292,7 @@ export function AdminCreationEntrySwitchPage({
|
||||
});
|
||||
setEntries(sortEntries(response.entries));
|
||||
setAnnouncementItems(formatEventBannersFormItems(response.eventBanners));
|
||||
setPublicWorkInteractions(response.publicWorkInteractions ?? []);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setBannerErrorMessage);
|
||||
} finally {
|
||||
@@ -276,6 +300,41 @@ export function AdminCreationEntrySwitchPage({
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存公开作品详情页点赞 / 改造能力开关。 */
|
||||
async function handleSavePublicWorkInteractions() {
|
||||
if (isSavingInteractions) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInteractionErrorMessage('');
|
||||
const confirmed = await confirmWrite({
|
||||
action: '保存作品互动配置',
|
||||
target: 'public-work-interactions',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSavingInteractions(true);
|
||||
try {
|
||||
const response = await upsertAdminPublicWorkInteractions(token, {
|
||||
publicWorkInteractions: publicWorkInteractions.map((item) => ({
|
||||
...item,
|
||||
sourceType: item.sourceType.trim(),
|
||||
likeDisabledMessage: item.likeDisabledMessage.trim(),
|
||||
remixDisabledMessage: item.remixDisabledMessage.trim(),
|
||||
})),
|
||||
});
|
||||
setEntries(sortEntries(response.entries));
|
||||
setAnnouncementItems(formatEventBannersFormItems(response.eventBanners));
|
||||
setPublicWorkInteractions(response.publicWorkInteractions ?? []);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setInteractionErrorMessage);
|
||||
} finally {
|
||||
setIsSavingInteractions(false);
|
||||
}
|
||||
}
|
||||
|
||||
function fillForm(entry: AdminCreationEntryTypeConfigPayload | null) {
|
||||
if (!entry) {
|
||||
return;
|
||||
@@ -361,7 +420,10 @@ export function AdminCreationEntrySwitchPage({
|
||||
currentForm
|
||||
? {
|
||||
...currentForm,
|
||||
fields: [...currentForm.fields, createUnifiedCreationSpecFieldFormItem()],
|
||||
fields: [
|
||||
...currentForm.fields,
|
||||
createUnifiedCreationSpecFieldFormItem(),
|
||||
],
|
||||
}
|
||||
: currentForm,
|
||||
);
|
||||
@@ -377,7 +439,10 @@ export function AdminCreationEntrySwitchPage({
|
||||
);
|
||||
return {
|
||||
...currentForm,
|
||||
fields: fields.length > 0 ? fields : [createUnifiedCreationSpecFieldFormItem()],
|
||||
fields:
|
||||
fields.length > 0
|
||||
? fields
|
||||
: [createUnifiedCreationSpecFieldFormItem()],
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -414,6 +479,26 @@ export function AdminCreationEntrySwitchPage({
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新单条公开作品互动配置。 */
|
||||
function updatePublicWorkInteraction(
|
||||
index: number,
|
||||
patch: Partial<
|
||||
Pick<
|
||||
PublicWorkInteractionConfigPayload,
|
||||
| 'likeEnabled'
|
||||
| 'remixEnabled'
|
||||
| 'likeDisabledMessage'
|
||||
| 'remixDisabledMessage'
|
||||
>
|
||||
>,
|
||||
) {
|
||||
setPublicWorkInteractions((currentItems) =>
|
||||
currentItems.map((item, itemIndex) =>
|
||||
itemIndex === index ? { ...item, ...patch } : item,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<div className="admin-page-heading">
|
||||
@@ -515,191 +600,288 @@ export function AdminCreationEntrySwitchPage({
|
||||
) : null}
|
||||
|
||||
{!isAnnouncementMode ? (
|
||||
<div className="admin-two-column admin-two-column-wide">
|
||||
<form className="admin-panel admin-form" onSubmit={handleSave}>
|
||||
<div className="admin-form-row">
|
||||
<label className="admin-field admin-field-fill">
|
||||
<span>入口 ID</span>
|
||||
<input
|
||||
value={selectedId}
|
||||
onChange={(event) => setSelectedId(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-switch-field">
|
||||
<input
|
||||
checked={visible}
|
||||
type="checkbox"
|
||||
onChange={(event) => setVisible(event.target.checked)}
|
||||
/>
|
||||
<span>展示</span>
|
||||
</label>
|
||||
<label className="admin-switch-field">
|
||||
<input
|
||||
checked={open}
|
||||
type="checkbox"
|
||||
onChange={(event) => setOpen(event.target.checked)}
|
||||
/>
|
||||
<span>开放</span>
|
||||
</label>
|
||||
<>
|
||||
<section className="admin-panel admin-form">
|
||||
<div className="admin-subsection-heading">
|
||||
<h3>作品互动</h3>
|
||||
<span>{`${publicWorkInteractions.length} 类`}</span>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<label className="admin-field">
|
||||
<span>标题</span>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>角标</span>
|
||||
<input
|
||||
value={badge}
|
||||
onChange={(event) => setBadge(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>副标题</span>
|
||||
<input
|
||||
value={subtitle}
|
||||
onChange={(event) => setSubtitle(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>图片路径</span>
|
||||
<input
|
||||
value={imageSrc}
|
||||
onChange={(event) => setImageSrc(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>排序</span>
|
||||
<input
|
||||
inputMode="numeric"
|
||||
value={sortOrder}
|
||||
onChange={(event) => setSortOrder(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<label className="admin-field">
|
||||
<span>分类 ID</span>
|
||||
<input
|
||||
value={categoryId}
|
||||
onChange={(event) => setCategoryId(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>分类名称</span>
|
||||
<input
|
||||
value={categoryLabel}
|
||||
onChange={(event) => setCategoryLabel(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>分类排序</span>
|
||||
<input
|
||||
inputMode="numeric"
|
||||
value={categorySortOrder}
|
||||
onChange={(event) => setCategorySortOrder(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<section className="admin-subsection">
|
||||
<div className="admin-subsection-heading">
|
||||
<span>统一创作契约</span>
|
||||
<span>
|
||||
{unifiedCreationSpec ? '已配置' : '未配置'}
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{errorMessage ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="admin-form-actions">
|
||||
<button
|
||||
className="admin-primary-button"
|
||||
disabled={isSaving}
|
||||
type="submit"
|
||||
>
|
||||
<Save size={17} aria-hidden="true" />
|
||||
<span>{isSaving ? '保存中' : '保存入库'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section className="admin-panel">
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table admin-table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>入口</th>
|
||||
<th>展示</th>
|
||||
<th>开放</th>
|
||||
<th>统一契约</th>
|
||||
<th>分类</th>
|
||||
<th>排序</th>
|
||||
<th>作品类型</th>
|
||||
<th>点赞</th>
|
||||
<th>点赞关闭提示</th>
|
||||
<th>改造</th>
|
||||
<th>改造关闭提示</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry) => (
|
||||
<tr key={entry.id}>
|
||||
{publicWorkInteractions.map((item, index) => (
|
||||
<tr key={item.sourceType}>
|
||||
<td>{formatPublicWorkSourceLabel(item.sourceType)}</td>
|
||||
<td>
|
||||
<button
|
||||
className="admin-link-button"
|
||||
type="button"
|
||||
onClick={() => fillForm(entry)}
|
||||
>
|
||||
{entry.title || entry.id}
|
||||
</button>
|
||||
<label className="admin-switch-field">
|
||||
<input
|
||||
checked={item.likeEnabled}
|
||||
type="checkbox"
|
||||
onChange={(event) =>
|
||||
updatePublicWorkInteraction(index, {
|
||||
likeEnabled: event.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span>{item.likeEnabled ? '开' : '关'}</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
aria-label={`${formatPublicWorkSourceLabel(
|
||||
item.sourceType,
|
||||
)} 点赞关闭提示`}
|
||||
value={item.likeDisabledMessage}
|
||||
onChange={(event) =>
|
||||
updatePublicWorkInteraction(index, {
|
||||
likeDisabledMessage: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<label className="admin-switch-field">
|
||||
<input
|
||||
checked={item.remixEnabled}
|
||||
type="checkbox"
|
||||
onChange={(event) =>
|
||||
updatePublicWorkInteraction(index, {
|
||||
remixEnabled: event.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span>{item.remixEnabled ? '开' : '关'}</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
aria-label={`${formatPublicWorkSourceLabel(
|
||||
item.sourceType,
|
||||
)} 改造关闭提示`}
|
||||
value={item.remixDisabledMessage}
|
||||
onChange={(event) =>
|
||||
updatePublicWorkInteraction(index, {
|
||||
remixDisabledMessage: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>{entry.visible ? '是' : '否'}</td>
|
||||
<td>{entry.open ? '是' : '否'}</td>
|
||||
<td>{entry.unifiedCreationSpec ? '是' : '否'}</td>
|
||||
<td>{entry.categoryLabel || entry.categoryId}</td>
|
||||
<td>{entry.sortOrder}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{interactionErrorMessage ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{interactionErrorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="admin-form-actions">
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
disabled={isSavingInteractions}
|
||||
type="button"
|
||||
onClick={handleSavePublicWorkInteractions}
|
||||
>
|
||||
<Save size={17} aria-hidden="true" />
|
||||
<span>{isSavingInteractions ? '保存中' : '保存作品互动'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="admin-two-column admin-two-column-wide">
|
||||
<form className="admin-panel admin-form" onSubmit={handleSave}>
|
||||
<div className="admin-form-row">
|
||||
<label className="admin-field admin-field-fill">
|
||||
<span>入口 ID</span>
|
||||
<input
|
||||
value={selectedId}
|
||||
onChange={(event) => setSelectedId(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-switch-field">
|
||||
<input
|
||||
checked={visible}
|
||||
type="checkbox"
|
||||
onChange={(event) => setVisible(event.target.checked)}
|
||||
/>
|
||||
<span>展示</span>
|
||||
</label>
|
||||
<label className="admin-switch-field">
|
||||
<input
|
||||
checked={open}
|
||||
type="checkbox"
|
||||
onChange={(event) => setOpen(event.target.checked)}
|
||||
/>
|
||||
<span>开放</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<label className="admin-field">
|
||||
<span>标题</span>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>角标</span>
|
||||
<input
|
||||
value={badge}
|
||||
onChange={(event) => setBadge(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>副标题</span>
|
||||
<input
|
||||
value={subtitle}
|
||||
onChange={(event) => setSubtitle(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>图片路径</span>
|
||||
<input
|
||||
value={imageSrc}
|
||||
onChange={(event) => setImageSrc(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>排序</span>
|
||||
<input
|
||||
inputMode="numeric"
|
||||
value={sortOrder}
|
||||
onChange={(event) => setSortOrder(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<label className="admin-field">
|
||||
<span>分类 ID</span>
|
||||
<input
|
||||
value={categoryId}
|
||||
onChange={(event) => setCategoryId(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>分类名称</span>
|
||||
<input
|
||||
value={categoryLabel}
|
||||
onChange={(event) => setCategoryLabel(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>分类排序</span>
|
||||
<input
|
||||
inputMode="numeric"
|
||||
value={categorySortOrder}
|
||||
onChange={(event) => setCategorySortOrder(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<section className="admin-subsection">
|
||||
<div className="admin-subsection-heading">
|
||||
<span>统一创作契约</span>
|
||||
<span>{unifiedCreationSpec ? '已配置' : '未配置'}</span>
|
||||
</div>
|
||||
<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>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{errorMessage ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="admin-form-actions">
|
||||
<button
|
||||
className="admin-primary-button"
|
||||
disabled={isSaving}
|
||||
type="submit"
|
||||
>
|
||||
<Save size={17} aria-hidden="true" />
|
||||
<span>{isSaving ? '保存中' : '保存入库'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section className="admin-panel">
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table admin-table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>入口</th>
|
||||
<th>展示</th>
|
||||
<th>开放</th>
|
||||
<th>统一契约</th>
|
||||
<th>分类</th>
|
||||
<th>排序</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry) => (
|
||||
<tr key={entry.id}>
|
||||
<td>
|
||||
<button
|
||||
className="admin-link-button"
|
||||
type="button"
|
||||
onClick={() => fillForm(entry)}
|
||||
>
|
||||
{entry.title || entry.id}
|
||||
</button>
|
||||
</td>
|
||||
<td>{entry.visible ? '是' : '否'}</td>
|
||||
<td>{entry.open ? '是' : '否'}</td>
|
||||
<td>{entry.unifiedCreationSpec ? '是' : '否'}</td>
|
||||
<td>{entry.categoryLabel || entry.categoryId}</td>
|
||||
<td>{entry.sortOrder}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{confirmDialog}
|
||||
@@ -776,7 +958,10 @@ export function AdminCreationEntrySwitchPage({
|
||||
</div>
|
||||
<div className="admin-contract-field-editor-list">
|
||||
{unifiedCreationSpecForm.fields.map((field, index) => (
|
||||
<section className="admin-contract-field-editor" key={field.id}>
|
||||
<section
|
||||
className="admin-contract-field-editor"
|
||||
key={field.id}
|
||||
>
|
||||
<div className="admin-subsection-heading">
|
||||
<span>{`字段 ${index + 1}`}</span>
|
||||
<button
|
||||
@@ -881,6 +1066,11 @@ function sortEntries(entries: AdminCreationEntryTypeConfigPayload[]) {
|
||||
});
|
||||
}
|
||||
|
||||
function formatPublicWorkSourceLabel(sourceType: string) {
|
||||
const label = PUBLIC_WORK_SOURCE_LABELS[sourceType];
|
||||
return label ? `${label} / ${sourceType}` : sourceType;
|
||||
}
|
||||
|
||||
function parseInteger(value: string) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
@@ -933,7 +1123,7 @@ function buildEventBannersJsonFromForm(
|
||||
htmlCode: item.htmlCode.trim(),
|
||||
}));
|
||||
if (banners.length === 0) {
|
||||
return {ok: false as const, message: '至少需要一条公告'};
|
||||
return { ok: false as const, message: '至少需要一条公告' };
|
||||
}
|
||||
const emptyIndex = banners.findIndex(
|
||||
(banner) => !banner.title || !banner.htmlCode,
|
||||
@@ -945,7 +1135,7 @@ function buildEventBannersJsonFromForm(
|
||||
};
|
||||
}
|
||||
|
||||
return {ok: true as const, json: JSON.stringify(banners, null, 2)};
|
||||
return { ok: true as const, json: JSON.stringify(banners, null, 2) };
|
||||
}
|
||||
|
||||
/** 将旧结构化公告字段转成可编辑 HTML,避免后台表单丢失历史公告内容。 */
|
||||
@@ -995,9 +1185,9 @@ function buildUnifiedCreationSpecForm(
|
||||
title: spec?.title ?? entryTitle,
|
||||
mudPointCost: String(normalizeMudPointCost(spec?.mudPointCost)),
|
||||
...stages,
|
||||
fields:
|
||||
spec?.fields.map((field) => createUnifiedCreationSpecFieldFormItem(field)) ??
|
||||
[createUnifiedCreationSpecFieldFormItem()],
|
||||
fields: spec?.fields.map((field) =>
|
||||
createUnifiedCreationSpecFieldFormItem(field),
|
||||
) ?? [createUnifiedCreationSpecFieldFormItem()],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1044,10 +1234,13 @@ function validateUnifiedCreationSpecForEntry(
|
||||
spec: UnifiedCreationSpecPayload | null,
|
||||
) {
|
||||
if (!spec) {
|
||||
return {ok: true as const, spec: null};
|
||||
return { ok: true as const, spec: null };
|
||||
}
|
||||
if (spec.playId !== entryId) {
|
||||
return {ok: false as const, message: '统一创作契约 playId 必须与入口 ID 一致'};
|
||||
return {
|
||||
ok: false as const,
|
||||
message: '统一创作契约 playId 必须与入口 ID 一致',
|
||||
};
|
||||
}
|
||||
return validateUnifiedCreationSpec(spec);
|
||||
}
|
||||
@@ -1064,12 +1257,12 @@ function formatMudPointCostText(value: number | null | undefined) {
|
||||
|
||||
function validateUnifiedCreationSpec(value: unknown) {
|
||||
if (!isRecord(value)) {
|
||||
return {ok: false as const, message: '统一创作契约必须是对象'};
|
||||
return { ok: false as const, message: '统一创作契约必须是对象' };
|
||||
}
|
||||
|
||||
const playId = readRequiredString(value, 'playId');
|
||||
if (!playId) {
|
||||
return {ok: false as const, message: '统一创作契约 playId 不能为空'};
|
||||
return { ok: false as const, message: '统一创作契约 playId 不能为空' };
|
||||
}
|
||||
|
||||
const title = readRequiredString(value, 'title');
|
||||
@@ -1078,42 +1271,57 @@ function validateUnifiedCreationSpec(value: unknown) {
|
||||
const generationStage = readRequiredString(value, 'generationStage');
|
||||
const resultStage = readRequiredString(value, 'resultStage');
|
||||
if (!title) {
|
||||
return {ok: false as const, message: '统一创作契约标题不能为空'};
|
||||
return { ok: false as const, message: '统一创作契约标题不能为空' };
|
||||
}
|
||||
if (!workspaceStage || !generationStage || !resultStage) {
|
||||
return {ok: false as const, message: '该玩法缺少默认阶段配置,请先由开发接入'};
|
||||
return {
|
||||
ok: false as const,
|
||||
message: '该玩法缺少默认阶段配置,请先由开发接入',
|
||||
};
|
||||
}
|
||||
if (mudPointCost === null) {
|
||||
return {ok: false as const, message: '统一创作契约泥点消耗数量必须是大于 0 的整数'};
|
||||
return {
|
||||
ok: false as const,
|
||||
message: '统一创作契约泥点消耗数量必须是大于 0 的整数',
|
||||
};
|
||||
}
|
||||
if (new Set([workspaceStage, generationStage, resultStage]).size !== 3) {
|
||||
return {ok: false as const, message: '统一创作契约阶段不能重复'};
|
||||
return { ok: false as const, message: '统一创作契约阶段不能重复' };
|
||||
}
|
||||
|
||||
if (!Array.isArray(value.fields) || value.fields.length === 0) {
|
||||
return {ok: false as const, message: '统一创作契约 fields 不能为空'};
|
||||
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: '统一创作契约字段必须是对象'};
|
||||
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 不能为空'};
|
||||
return {
|
||||
ok: false as const,
|
||||
message: '统一创作契约字段 id 和 label 不能为空',
|
||||
};
|
||||
}
|
||||
if (fieldIds.has(id)) {
|
||||
return {ok: false as const, message: `统一创作契约字段 id 重复:${id}`};
|
||||
return { ok: false as const, message: `统一创作契约字段 id 重复:${id}` };
|
||||
}
|
||||
fieldIds.add(id);
|
||||
if (!isUnifiedCreationFieldKind(item.kind)) {
|
||||
return {ok: false as const, message: `统一创作契约字段 kind 非法:${id}`};
|
||||
return {
|
||||
ok: false as const,
|
||||
message: `统一创作契约字段 kind 非法:${id}`,
|
||||
};
|
||||
}
|
||||
if (typeof item.required !== 'boolean') {
|
||||
return {ok: false as const, message: `统一创作契约字段 required 非法:${id}`};
|
||||
return {
|
||||
ok: false as const,
|
||||
message: `统一创作契约字段 required 非法:${id}`,
|
||||
};
|
||||
}
|
||||
fields.push({
|
||||
id,
|
||||
@@ -1137,7 +1345,11 @@ function validateUnifiedCreationSpec(value: unknown) {
|
||||
};
|
||||
}
|
||||
|
||||
function UnifiedCreationSpecCard({spec}: {spec: UnifiedCreationSpecPayload}) {
|
||||
function UnifiedCreationSpecCard({
|
||||
spec,
|
||||
}: {
|
||||
spec: UnifiedCreationSpecPayload;
|
||||
}) {
|
||||
return (
|
||||
<div className="admin-contract-card">
|
||||
<dl className="admin-info-list">
|
||||
|
||||
Reference in New Issue
Block a user