261 lines
8.0 KiB
TypeScript
261 lines
8.0 KiB
TypeScript
import {RefreshCcw, Save} from 'lucide-react';
|
|
import {FormEvent, useEffect, useState} from 'react';
|
|
|
|
import {
|
|
getAdminCreationEntryConfig,
|
|
upsertAdminCreationEntryConfig,
|
|
} from '../api/adminApiClient';
|
|
import type {AdminCreationEntryTypeConfigPayload} from '../api/adminApiTypes';
|
|
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
|
import {handlePageError} from './pageUtils';
|
|
|
|
interface AdminCreationEntrySwitchPageProps {
|
|
token: string;
|
|
onUnauthorized: (message?: string) => void;
|
|
}
|
|
|
|
export function AdminCreationEntrySwitchPage({
|
|
token,
|
|
onUnauthorized,
|
|
}: AdminCreationEntrySwitchPageProps) {
|
|
const [entries, setEntries] = useState<AdminCreationEntryTypeConfigPayload[]>([]);
|
|
const [selectedId, setSelectedId] = useState('puzzle');
|
|
const [title, setTitle] = useState('');
|
|
const [subtitle, setSubtitle] = useState('');
|
|
const [badge, setBadge] = useState('');
|
|
const [imageSrc, setImageSrc] = useState('');
|
|
const [visible, setVisible] = useState(true);
|
|
const [open, setOpen] = useState(true);
|
|
const [sortOrder, setSortOrder] = useState('30');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [listErrorMessage, setListErrorMessage] = useState('');
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
|
|
|
useEffect(() => {
|
|
void refreshEntries();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [token]);
|
|
|
|
async function refreshEntries() {
|
|
setIsLoading(true);
|
|
setListErrorMessage('');
|
|
try {
|
|
const response = await getAdminCreationEntryConfig(token);
|
|
const nextEntries = sortEntries(response.entries);
|
|
setEntries(nextEntries);
|
|
fillForm(
|
|
nextEntries.find((entry) => entry.id === selectedId) ?? nextEntries[0] ?? null,
|
|
);
|
|
} catch (error: unknown) {
|
|
handlePageError(error, onUnauthorized, setListErrorMessage);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleSave(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
if (isSaving) {
|
|
return;
|
|
}
|
|
|
|
const targetId = selectedId.trim();
|
|
setErrorMessage('');
|
|
const confirmed = await confirmWrite({
|
|
action: '保存创作入口开关',
|
|
target: targetId,
|
|
});
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
try {
|
|
const response = await upsertAdminCreationEntryConfig(token, {
|
|
id: targetId,
|
|
title: title.trim(),
|
|
subtitle: subtitle.trim(),
|
|
badge: badge.trim(),
|
|
imageSrc: imageSrc.trim(),
|
|
visible,
|
|
open,
|
|
sortOrder: parseInteger(sortOrder),
|
|
});
|
|
const nextEntries = sortEntries(response.entries);
|
|
setEntries(nextEntries);
|
|
fillForm(nextEntries.find((entry) => entry.id === targetId) ?? null);
|
|
} catch (error: unknown) {
|
|
handlePageError(error, onUnauthorized, setErrorMessage);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}
|
|
|
|
function fillForm(entry: AdminCreationEntryTypeConfigPayload | null) {
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
setSelectedId(entry.id);
|
|
setTitle(entry.title);
|
|
setSubtitle(entry.subtitle);
|
|
setBadge(entry.badge);
|
|
setImageSrc(entry.imageSrc);
|
|
setVisible(entry.visible);
|
|
setOpen(entry.open);
|
|
setSortOrder(String(entry.sortOrder));
|
|
}
|
|
|
|
return (
|
|
<section className="admin-page">
|
|
<div className="admin-page-heading">
|
|
<div>
|
|
<h2>创作入口开关</h2>
|
|
<p>控制创作中心入口展示与运行态路由可用性</p>
|
|
</div>
|
|
<button
|
|
className="admin-secondary-button"
|
|
disabled={isLoading}
|
|
type="button"
|
|
onClick={refreshEntries}
|
|
>
|
|
<RefreshCcw size={17} aria-hidden="true" />
|
|
<span>{isLoading ? '刷新中' : '刷新'}</span>
|
|
</button>
|
|
</div>
|
|
|
|
{listErrorMessage ? (
|
|
<div className="admin-alert" role="status">
|
|
{listErrorMessage}
|
|
</div>
|
|
) : null}
|
|
|
|
<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>
|
|
|
|
{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>
|
|
</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.sortOrder}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
{confirmDialog}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function sortEntries(entries: AdminCreationEntryTypeConfigPayload[]) {
|
|
return [...entries].sort((left, right) => {
|
|
if (left.sortOrder !== right.sortOrder) {
|
|
return left.sortOrder - right.sortOrder;
|
|
}
|
|
return left.id.localeCompare(right.id);
|
|
});
|
|
}
|
|
|
|
function parseInteger(value: string) {
|
|
const parsed = Number.parseInt(value, 10);
|
|
if (!Number.isFinite(parsed)) {
|
|
return 0;
|
|
}
|
|
return parsed;
|
|
}
|