From afa592dd914005d3770e33eba07f2ae5a9bd94f0 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 11:17:57 +0800 Subject: [PATCH] feat(config): add wallet configuration management page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增钱包配置管理页面,包含转入转出限额配置功能,添加对应路由导航与API接口,完善配置元信息 --- src/api/admin-settings.ts | 30 +++ src/app/admin/(shell)/config/page.tsx | 1 + src/app/admin/(shell)/config/wallet/page.tsx | 16 ++ src/modules/config/config-subnav.tsx | 2 +- .../config/doc/wallet-config-doc-screen.tsx | 200 ++++++++++++++++++ src/modules/config/meta.ts | 5 + 6 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/api/admin-settings.ts create mode 100644 src/app/admin/(shell)/config/wallet/page.tsx create mode 100644 src/modules/config/doc/wallet-config-doc-screen.tsx diff --git a/src/api/admin-settings.ts b/src/api/admin-settings.ts new file mode 100644 index 0000000..562a8eb --- /dev/null +++ b/src/api/admin-settings.ts @@ -0,0 +1,30 @@ +import { adminRequest } from "@/lib/admin-http"; +import { API_V1_PREFIX } from "@/api/paths"; + +const A = `${API_V1_PREFIX}/admin`; + +export type AdminSettingItem = { + key: string; + value: unknown; + group: string; + description: string | null; +}; + +export type AdminSettingListResponse = { + items: AdminSettingItem[]; +}; + +export async function getAdminSettings( + group: string, +): Promise { + return adminRequest.get(`${A}/settings`, { + params: { group }, + }); +} + +export async function updateAdminSetting( + key: string, + value: unknown, +): Promise { + return adminRequest.put(`${A}/settings/${key}`, { value }); +} diff --git a/src/app/admin/(shell)/config/page.tsx b/src/app/admin/(shell)/config/page.tsx index fbdc2c7..641ae64 100644 --- a/src/app/admin/(shell)/config/page.tsx +++ b/src/app/admin/(shell)/config/page.tsx @@ -15,6 +15,7 @@ const SECTIONS = [ { href: "/admin/config/rebate", title: "佣金 / 回水" }, { href: "/admin/config/risk-cap", title: "风控封顶" }, { href: "/admin/config/versions", title: "配置版本历史" }, + { href: "/admin/config/wallet", title: "钱包配置" }, ] as const; export default function AdminConfigHubPage() { diff --git a/src/app/admin/(shell)/config/wallet/page.tsx b/src/app/admin/(shell)/config/wallet/page.tsx new file mode 100644 index 0000000..32d3e3c --- /dev/null +++ b/src/app/admin/(shell)/config/wallet/page.tsx @@ -0,0 +1,16 @@ +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { configWalletMeta } from "@/modules/config/meta"; +import { WalletConfigDocScreen } from "@/modules/config/doc/wallet-config-doc-screen"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: configWalletMeta.title, +}; + +export default function AdminConfigWalletPage() { + return ( + + + + ); +} diff --git a/src/modules/config/config-subnav.tsx b/src/modules/config/config-subnav.tsx index d485116..b95392a 100644 --- a/src/modules/config/config-subnav.tsx +++ b/src/modules/config/config-subnav.tsx @@ -6,11 +6,11 @@ import { usePathname } from "next/navigation"; import { cn } from "@/lib/utils"; const LINKS: { href: string; label: string; match?: "exact" | "prefix" }[] = [ - { href: "/admin/config", label: "概览", match: "exact" }, { href: "/admin/config/plays", label: "玩法配置" }, { href: "/admin/config/odds", label: "赔率配置" }, { href: "/admin/config/rebate", label: "佣金 / 回水" }, { href: "/admin/config/risk-cap", label: "风控封顶" }, + { href: "/admin/config/wallet", label: "钱包配置" }, ]; function linkActive(pathname: string, href: string, match: "exact" | "prefix"): boolean { diff --git a/src/modules/config/doc/wallet-config-doc-screen.tsx b/src/modules/config/doc/wallet-config-doc-screen.tsx new file mode 100644 index 0000000..01bca0a --- /dev/null +++ b/src/modules/config/doc/wallet-config-doc-screen.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { toast } from "sonner"; + +import { + getAdminSettings, + updateAdminSetting, +} from "@/api/admin-settings"; +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 { LotteryApiBizError } from "@/types/api/errors"; + +const WALLET_GROUP = "wallet"; + +const KEYS = { + IN_MIN: "wallet.transfer_in_min_minor", + IN_MAX: "wallet.transfer_in_max_minor", + OUT_MIN: "wallet.transfer_out_min_minor", + OUT_MAX: "wallet.transfer_out_max_minor", +} as const; + +function minorUnitsToDisplay(n: unknown, decimals = 2): string { + const num = Number(n); + if (!Number.isFinite(num)) return ""; + return (num / 100).toFixed(decimals); +} + +function displayToMinorUnits(s: string): number { + const n = parseFloat(s); + if (Number.isNaN(n) || n < 0) return 0; + return Math.round(n * 100); +} + +interface Draft { + inMin: string; + inMax: string; + outMin: string; + outMax: string; +} + +export function WalletConfigDocScreen() { + const [draft, setDraft] = useState({ + inMin: "", + inMax: "", + outMin: "", + outMax: "", + }); + const [saved, setSaved] = useState({ inMin: "", inMax: "", outMin: "", outMax: "" }); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [dirty, setDirty] = useState(false); + + const load = useCallback(async () => { + setLoading(true); + try { + const res = await getAdminSettings(WALLET_GROUP); + const kv: Record = {}; + for (const item of res.items) { + kv[item.key] = item.value; + } + const d: Draft = { + inMin: minorUnitsToDisplay(kv[KEYS.IN_MIN] ?? 100), + inMax: minorUnitsToDisplay(kv[KEYS.IN_MAX] ?? 0), + outMin: minorUnitsToDisplay(kv[KEYS.OUT_MIN] ?? 100), + outMax: minorUnitsToDisplay(kv[KEYS.OUT_MAX] ?? 0), + }; + setDraft(d); + setSaved(d); + setDirty(false); + } catch (e) { + toast.error("加载失败"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + void load(); + }, [load]); + + const handleChange = (field: keyof Draft, value: string) => { + setDraft((prev) => ({ ...prev, [field]: value })); + setDirty(true); + }; + + const handleSave = async () => { + setSaving(true); + try { + await updateAdminSetting(KEYS.IN_MIN, displayToMinorUnits(draft.inMin)); + await updateAdminSetting(KEYS.IN_MAX, displayToMinorUnits(draft.inMax)); + await updateAdminSetting(KEYS.OUT_MIN, displayToMinorUnits(draft.outMin)); + await updateAdminSetting(KEYS.OUT_MAX, displayToMinorUnits(draft.outMax)); + toast.success("保存成功"); + setSaved(draft); + setDirty(false); + } catch (e) { + toast.error(e instanceof LotteryApiBizError ? e.message : "保存失败"); + } finally { + setSaving(false); + } + }; + + return ( + + + 钱包转账限额配置 + + +

+ 金额单位为游戏币种最小单位(如 NPR 下 100 = 1.00 NPR)。最小金额至少为 1 最小单位。 +

+
+
+ + handleChange("inMin", e.target.value)} + disabled={loading || saving} + /> +

+ 主站钱包转入彩票钱包的单笔下限 +

+
+
+ + handleChange("inMax", e.target.value)} + disabled={loading || saving} + /> +

+ 主站钱包转入彩票钱包的单笔上限 +

+
+
+ + handleChange("outMin", e.target.value)} + disabled={loading || saving} + /> +

+ 彩票钱包转出主站钱包的单笔下限 +

+
+
+ + handleChange("outMax", e.target.value)} + disabled={loading || saving} + /> +

+ 彩票钱包转出主站钱包的单笔上限 +

+
+
+
+ + {dirty && ( + + )} +
+
+
+ ); +} diff --git a/src/modules/config/meta.ts b/src/modules/config/meta.ts index 993e709..ba33d21 100644 --- a/src/modules/config/meta.ts +++ b/src/modules/config/meta.ts @@ -27,3 +27,8 @@ export const configVersionsMeta = { title: "配置版本历史", description: "", } as const; + +export const configWalletMeta = { + title: "钱包配置", + description: "", +} as const;