Files
image-prompt-studio/components/admin-dashboard.tsx
2026-04-25 11:26:33 +08:00

412 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import type {
AdminSnapshot,
AdminUserSummary,
SettingsSnapshot,
UserRole,
} from "@/lib/types";
interface SettingsResponse {
ok?: boolean;
error?: string;
settings?: SettingsSnapshot;
}
interface UserResponse {
ok?: boolean;
error?: string;
user?: AdminUserSummary;
}
interface SettingsFormState {
defaultMonthlyQuota: string;
}
interface CreateUserDraft {
username: string;
password: string;
role: UserRole;
monthlyQuota: string;
}
type EditableAdminUser = Omit<AdminUserSummary, "monthlyQuota"> & {
monthlyQuota: string | null;
passwordDraft: string;
};
function toEditableUser(user: AdminUserSummary): EditableAdminUser {
return {
...user,
monthlyQuota: user.monthlyQuota === null ? null : String(user.monthlyQuota),
passwordDraft: "",
};
}
function getErrorMessage(error: unknown, fallbackMessage: string): string {
return error instanceof Error ? error.message : fallbackMessage;
}
export default function AdminDashboard({
initialSnapshot,
}: {
initialSnapshot: AdminSnapshot;
}) {
const [settings, setSettings] = useState<SettingsFormState>({
defaultMonthlyQuota: String(initialSnapshot.settings.defaultMonthlyQuota),
});
const [users, setUsers] = useState<EditableAdminUser[]>(
initialSnapshot.users.map(toEditableUser),
);
const [feedback, setFeedback] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [isSavingSettings, setIsSavingSettings] = useState(false);
const [createDraft, setCreateDraft] = useState<CreateUserDraft>({
username: "",
password: "",
role: "user",
monthlyQuota: settings.defaultMonthlyQuota,
});
async function saveSettings(): Promise<void> {
setFeedback("");
setErrorMessage("");
setIsSavingSettings(true);
try {
const response = await fetch("/api/admin/settings", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
defaultMonthlyQuota: settings.defaultMonthlyQuota,
}),
});
const payload = (await response.json()) as SettingsResponse;
if (!response.ok || payload.ok !== true || !payload.settings) {
throw new Error(payload.error || "保存默认额度失败。");
}
setSettings({
defaultMonthlyQuota: String(payload.settings.defaultMonthlyQuota),
});
setFeedback("默认额度已更新。");
} catch (error) {
setErrorMessage(getErrorMessage(error, "保存默认额度失败。"));
} finally {
setIsSavingSettings(false);
}
}
async function createUser(): Promise<void> {
setFeedback("");
setErrorMessage("");
try {
const response = await fetch("/api/admin/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(createDraft),
});
const payload = (await response.json()) as UserResponse;
if (!response.ok || payload.ok !== true || !payload.user) {
throw new Error(payload.error || "创建用户失败。");
}
const createdUser = payload.user;
setUsers((currentUsers) => [
...currentUsers,
toEditableUser(createdUser),
]);
setCreateDraft({
username: "",
password: "",
role: "user",
monthlyQuota: settings.defaultMonthlyQuota,
});
setFeedback("用户已创建。");
} catch (error) {
setErrorMessage(getErrorMessage(error, "创建用户失败。"));
}
}
async function saveUser(user: EditableAdminUser): Promise<void> {
setFeedback("");
setErrorMessage("");
try {
const response = await fetch(`/api/admin/users/${user.id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
role: user.role,
monthlyQuota: user.role === "admin" ? null : user.monthlyQuota,
isActive: user.isActive,
password: user.passwordDraft || undefined,
}),
});
const payload = (await response.json()) as UserResponse;
if (!response.ok || payload.ok !== true || !payload.user) {
throw new Error(payload.error || "保存用户失败。");
}
const savedUser = payload.user;
setUsers((currentUsers) =>
currentUsers.map((item) =>
item.id === user.id ? toEditableUser(savedUser) : item,
),
);
setFeedback(`已保存 ${savedUser.username}`);
} catch (error) {
setErrorMessage(getErrorMessage(error, "保存用户失败。"));
}
}
return (
<div className="admin-grid">
<section className="card settings-card">
<div className="section-heading">
<div>
<p className="section-kicker">Policy</p>
<h3></h3>
</div>
<span className="subtle-pill">{initialSnapshot.periodKey}</span>
</div>
<label className="field">
<span></span>
<input
type="number"
min="0"
value={settings.defaultMonthlyQuota}
onChange={(event) =>
setSettings((current) => ({
...current,
defaultMonthlyQuota: event.target.value,
}))
}
/>
</label>
<p className="muted block-copy">
</p>
<button
type="button"
className="primary-button"
onClick={saveSettings}
disabled={isSavingSettings}
>
{isSavingSettings ? "保存中..." : "保存默认额度"}
</button>
</section>
<section className="card create-user-card">
<div className="section-heading">
<div>
<p className="section-kicker">Users</p>
<h3></h3>
</div>
</div>
<div className="form-grid">
<label className="field">
<span></span>
<input
type="text"
value={createDraft.username}
onChange={(event) =>
setCreateDraft((current) => ({
...current,
username: event.target.value,
}))
}
/>
</label>
<label className="field">
<span></span>
<input
type="password"
value={createDraft.password}
onChange={(event) =>
setCreateDraft((current) => ({
...current,
password: event.target.value,
}))
}
/>
</label>
<label className="field">
<span></span>
<select
value={createDraft.role}
onChange={(event) =>
setCreateDraft((current) => ({
...current,
role: event.target.value as UserRole,
}))
}
>
<option value="user"></option>
<option value="admin"></option>
</select>
</label>
<label className="field">
<span></span>
<input
type="number"
min="0"
value={createDraft.monthlyQuota}
onChange={(event) =>
setCreateDraft((current) => ({
...current,
monthlyQuota: event.target.value,
}))
}
/>
</label>
</div>
<button type="button" className="primary-button" onClick={createUser}>
</button>
</section>
{(feedback || errorMessage) && (
<div className={errorMessage ? "error-banner" : "success-banner"}>
{errorMessage || feedback}
</div>
)}
<section className="card user-table-card">
<div className="section-heading">
<div>
<p className="section-kicker">Manage</p>
<h3></h3>
</div>
</div>
<div className="user-table">
<div className="user-table-head">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
{users.map((user) => (
<div key={user.id} className="user-table-row">
<div className="user-cell user-cell-name">
<strong>{user.username}</strong>
<small>
{user.isUnlimited ? "无限额" : `剩余 ${user.remaining}`}
</small>
</div>
<select
value={user.role}
onChange={(event) =>
setUsers((currentUsers) =>
currentUsers.map((item) =>
item.id === user.id
? { ...item, role: event.target.value as UserRole }
: item,
),
)
}
>
<option value="user"></option>
<option value="admin"></option>
</select>
<input
type="number"
min="0"
value={user.monthlyQuota ?? ""}
placeholder={String(settings.defaultMonthlyQuota)}
onChange={(event) =>
setUsers((currentUsers) =>
currentUsers.map((item) =>
item.id === user.id
? {
...item,
monthlyQuota:
event.target.value === ""
? null
: event.target.value,
}
: item,
),
)
}
/>
<span>{user.usedThisPeriod}</span>
<select
value={String(user.isActive)}
onChange={(event) =>
setUsers((currentUsers) =>
currentUsers.map((item) =>
item.id === user.id
? {
...item,
isActive: event.target.value === "true",
}
: item,
),
)
}
>
<option value="true"></option>
<option value="false"></option>
</select>
<input
type="password"
value={user.passwordDraft}
placeholder="留空则不修改"
onChange={(event) =>
setUsers((currentUsers) =>
currentUsers.map((item) =>
item.id === user.id
? { ...item, passwordDraft: event.target.value }
: item,
),
)
}
/>
<button
type="button"
className="ghost-button"
onClick={() => saveUser(user)}
>
</button>
</div>
))}
</div>
</section>
</div>
);
}