Merge branch 'codex/web-admin'
# Conflicts: # server-rs/crates/api-server/src/admin.rs
This commit is contained in:
158
apps/admin-web/src/pages/AdminInviteCodePage.tsx
Normal file
158
apps/admin-web/src/pages/AdminInviteCodePage.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import {Save} from 'lucide-react';
|
||||
import {FormEvent, useState} from 'react';
|
||||
|
||||
import {upsertProfileInviteCode} from '../api/adminApiClient';
|
||||
import type {ProfileInviteCodeAdminResponse} from '../api/adminApiTypes';
|
||||
import {handlePageError} from './pageUtils';
|
||||
|
||||
interface AdminInviteCodePageProps {
|
||||
token: string;
|
||||
result: ProfileInviteCodeAdminResponse | null;
|
||||
onUnauthorized: (message?: string) => void;
|
||||
onResultChange: (result: ProfileInviteCodeAdminResponse) => void;
|
||||
}
|
||||
|
||||
export function AdminInviteCodePage({
|
||||
token,
|
||||
result,
|
||||
onUnauthorized,
|
||||
onResultChange,
|
||||
}: AdminInviteCodePageProps) {
|
||||
const [inviteCode, setInviteCode] = useState('');
|
||||
const [metadataText, setMetadataText] = useState('{}');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
async function handleSave(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (isSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage('');
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await upsertProfileInviteCode(token, {
|
||||
inviteCode: inviteCode.trim(),
|
||||
metadata: parseMetadata(metadataText),
|
||||
});
|
||||
onResultChange(response);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<div className="admin-page-heading">
|
||||
<div>
|
||||
<h2>邀请码</h2>
|
||||
<p>注册链路预置码</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-two-column admin-two-column-wide">
|
||||
<form className="admin-panel admin-form" onSubmit={handleSave}>
|
||||
<label className="admin-field">
|
||||
<span>Invite Code</span>
|
||||
<input
|
||||
autoComplete="off"
|
||||
value={inviteCode}
|
||||
onChange={(event) => setInviteCode(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>Metadata JSON</span>
|
||||
<textarea
|
||||
rows={10}
|
||||
spellCheck={false}
|
||||
value={metadataText}
|
||||
onChange={(event) => setMetadataText(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{errorMessage ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
className="admin-primary-button"
|
||||
disabled={isSaving || !inviteCode.trim() || !isMetadataReady(metadataText)}
|
||||
type="submit"
|
||||
>
|
||||
<Save size={17} aria-hidden="true" />
|
||||
<span>{isSaving ? '保存中' : '保存'}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<section className="admin-panel admin-result-panel">
|
||||
<div className="admin-panel-heading">
|
||||
<h3>记录</h3>
|
||||
<span>{result?.inviteCode ?? '-'}</span>
|
||||
</div>
|
||||
{result ? (
|
||||
<dl className="admin-info-list">
|
||||
<div>
|
||||
<dt>User ID</dt>
|
||||
<dd>{result.userId}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>邀请码</dt>
|
||||
<dd>{result.inviteCode}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>创建</dt>
|
||||
<dd>{result.createdAt}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>更新</dt>
|
||||
<dd>{result.updatedAt}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Metadata</dt>
|
||||
<dd>
|
||||
<pre className="admin-code-block">
|
||||
{JSON.stringify(result.metadata, null, 2)}
|
||||
</pre>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
) : (
|
||||
<div className="admin-empty-state">暂无记录</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function parseMetadata(value: string): Record<string, unknown> {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(trimmed) as unknown;
|
||||
if (!isRecord(parsed)) {
|
||||
throw new Error('Metadata 必须是 JSON 对象');
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function isMetadataReady(value: string) {
|
||||
try {
|
||||
parseMetadata(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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