feat(project): init

This commit is contained in:
zhangheng
2026-04-25 11:26:33 +08:00
commit 9bf212f67c
63 changed files with 11141 additions and 0 deletions

View File

@@ -0,0 +1,411 @@
"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>
);
}