diff --git a/src/api/admin-auth.ts b/src/api/admin-auth.ts index de1e8e2..3df8488 100644 --- a/src/api/admin-auth.ts +++ b/src/api/admin-auth.ts @@ -42,3 +42,11 @@ export async function postAdminLogin( export async function getAdminMe(): Promise { return adminRequest.get(`${API_V1_PREFIX}/admin/auth/me`); } + +/** `PUT /api/v1/admin/auth/me`(更新自身账号信息,需 Token) */ +export async function putAdminMe(body: { + nickname?: string; + password?: string; +}): Promise { + return adminRequest.put(`${API_V1_PREFIX}/admin/auth/me`, body); +} diff --git a/src/app/admin/(shell)/account/page.tsx b/src/app/admin/(shell)/account/page.tsx new file mode 100644 index 0000000..f7befcd --- /dev/null +++ b/src/app/admin/(shell)/account/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from "next"; + +import { AccountSettingsConsole } from "@/modules/account/account-settings-console"; + +export const metadata: Metadata = { + title: "账号设置 - 管理后台", +}; + +export default function AdminAccountPage() { + return ; +} diff --git a/src/app/globals.css b/src/app/globals.css index 03cce09..5ccd892 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -131,7 +131,7 @@ @layer components { .admin-list-card { - @apply overflow-hidden border-border/80 bg-card shadow-sm; + @apply border-border/80 bg-card shadow-sm; } .admin-list-header { diff --git a/src/components/admin/admin-language-switcher.tsx b/src/components/admin/admin-language-switcher.tsx index d0ee227..fdab11c 100644 --- a/src/components/admin/admin-language-switcher.tsx +++ b/src/components/admin/admin-language-switcher.tsx @@ -66,7 +66,7 @@ export function AdminLanguageSwitcher() { @@ -79,14 +79,14 @@ export function AdminLanguageSwitcher() { void onSelectLocale(code)} > - + {LOCALE_FLAGS[code]} diff --git a/src/components/admin/toolbar.tsx b/src/components/admin/toolbar.tsx index 5120db7..852c1ff 100644 --- a/src/components/admin/toolbar.tsx +++ b/src/components/admin/toolbar.tsx @@ -5,6 +5,7 @@ import { LogOutIcon, UserRoundIcon, } from "lucide-react"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; @@ -66,9 +67,11 @@ export function ShellToolbar() { - - - {t("toolbar.accountSettings")} + + + + {t("toolbar.accountSettings")} + diff --git a/src/modules/account/account-settings-console.tsx b/src/modules/account/account-settings-console.tsx new file mode 100644 index 0000000..c826ceb --- /dev/null +++ b/src/modules/account/account-settings-console.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; + +import { putAdminMe } from "@/api/admin-auth"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useAdminProfile, useAdminSessionStore } from "@/stores/admin-session"; +import { LotteryApiBizError } from "@/types/api/errors"; + +export function AccountSettingsConsole() { + const { t } = useTranslation(["common"]); + const adminProfile = useAdminProfile(); + const refreshAdminProfile = useAdminSessionStore((s) => s.refreshAdminProfile); + + const [nickname, setNickname] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (adminProfile) { + setNickname(adminProfile.nickname ?? ""); + } + }, [adminProfile]); + + async function handleUpdateProfile() { + if (!nickname.trim()) { + toast.error(t("validation.required", { field: t("fields.nickname", { defaultValue: "昵称" }) })); + return; + } + setLoading(true); + try { + await putAdminMe({ nickname: nickname.trim() }); + toast.success(t("actions.updateSuccess", { defaultValue: "更新成功" })); + void refreshAdminProfile(); + } catch (err) { + toast.error(err instanceof LotteryApiBizError ? err.message : t("actions.updateFailed", { defaultValue: "更新失败" })); + } finally { + setLoading(false); + } + } + + async function handleUpdatePassword() { + if (!password) { + toast.error(t("validation.required", { field: t("fields.newPassword", { defaultValue: "新密码" }) })); + return; + } + if (password !== confirmPassword) { + toast.error(t("validation.passwordMismatch", { defaultValue: "两次输入的密码不一致" })); + return; + } + setLoading(true); + try { + await putAdminMe({ password }); + toast.success(t("actions.updateSuccess", { defaultValue: "更新成功" })); + setPassword(""); + setConfirmPassword(""); + } catch (err) { + toast.error(err instanceof LotteryApiBizError ? err.message : t("actions.updateFailed", { defaultValue: "更新失败" })); + } finally { + setLoading(false); + } + } + + return ( +
+
+

+ {t("accountSettings", { defaultValue: "账号设置" })} +

+

+ {t("accountSettingsDesc", { defaultValue: "管理您的基本账号资料及安全设置。" })} +

+
+ + + + {t("profileSettings", { defaultValue: "基本资料" })} + + {t("profileSettingsDesc", { defaultValue: "更新您的显示名称。" })} + + + +
+ + setNickname(e.target.value)} + placeholder={t("placeholders.nickname", { defaultValue: "请输入昵称" })} + /> +
+ +
+
+ + + + {t("securitySettings", { defaultValue: "安全设置" })} + + {t("securitySettingsDesc", { defaultValue: "修改您的登录密码。如不修改请留空。" })} + + + +
+ + setPassword(e.target.value)} + placeholder={t("placeholders.password", { defaultValue: "请输入新密码" })} + /> +
+
+ + setConfirmPassword(e.target.value)} + placeholder={t("placeholders.confirmPassword", { defaultValue: "请再次输入新密码" })} + /> +
+ +
+
+
+ ); +} diff --git a/src/modules/jackpot/jackpot-pools-console.tsx b/src/modules/jackpot/jackpot-pools-console.tsx index c1f1268..c9727ae 100644 --- a/src/modules/jackpot/jackpot-pools-console.tsx +++ b/src/modules/jackpot/jackpot-pools-console.tsx @@ -239,7 +239,7 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro onValueChange={(v) => updateDraft(p.id, { status: v ?? "0" })} > - + {d.status === "1" ? t("enabled") : t("disabled")} {t("disabled")} diff --git a/src/modules/reports/reports-console.tsx b/src/modules/reports/reports-console.tsx index 90c2a92..b77712f 100644 --- a/src/modules/reports/reports-console.tsx +++ b/src/modules/reports/reports-console.tsx @@ -39,6 +39,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Select, SelectContent, @@ -773,11 +774,27 @@ export function ReportsConsole() { const value = kind === "draw" ? filters.drawNo : kind === "player" ? filters.player : filters.operator; const labelKey = kind === "draw" ? "drawNo" : kind === "player" ? "player" : "operator"; + const open = search.open === kind; return ( -
+
-
+ { + setSearch((prev) => + nextOpen + ? { + ...prev, + open: kind, + query: value, + } + : emptySearch, + ); + }} + modal={false} + > +
-
+ - - {t("searchPicker.select")} - -
- {search.open === kind ? ( -
-
- ) : null} + +
); }; @@ -1156,7 +1165,6 @@ export function ReportsConsole() {

{t("title")}

-

{t("subtitle")}

{resultRowCount(result)} {t("preview.exportableRows")} @@ -1168,7 +1176,7 @@ export function ReportsConsole() { {t("chooseReport")} - + {REPORTS.map((report) => { const Icon = report.icon; const active = report.key === selectedReport.key; @@ -1178,7 +1186,7 @@ export function ReportsConsole() { type="button" onClick={() => setSelectedKey(report.key)} className={cn( - "flex w-full min-w-0 items-start gap-3 rounded-md border px-3 py-3 text-left transition", + "flex w-full min-w-0 items-center gap-3 rounded-md border px-3 py-2.5 text-left transition", active ? "border-primary bg-primary/[0.05] shadow-sm ring-1 ring-primary/15" : "border-border/80 bg-card hover:border-primary/35 hover:bg-muted/30", @@ -1188,10 +1196,7 @@ export function ReportsConsole() { - {t(`items.${report.key}.title`)} - - {t(`categories.${report.category}`)} · {formatKind(report.filterKind, t)} - + {t(`items.${report.key}.title`)} ); @@ -1204,17 +1209,13 @@ export function ReportsConsole() {
{t("filterPanel")} -

{t(`items.${selectedReport.key}.summary`)}

{selectedReport.fields.map(renderField)}
-
-

- {selectedReport.connected ? t("queryHint") : t("backendPending")} -

+