feat(project): init
This commit is contained in:
411
components/admin-dashboard.tsx
Normal file
411
components/admin-dashboard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user