feat(config): add wallet configuration management page
新增钱包配置管理页面,包含转入转出限额配置功能,添加对应路由导航与API接口,完善配置元信息
This commit is contained in:
30
src/api/admin-settings.ts
Normal file
30
src/api/admin-settings.ts
Normal file
@@ -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<AdminSettingListResponse> {
|
||||
return adminRequest.get<AdminSettingListResponse>(`${A}/settings`, {
|
||||
params: { group },
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateAdminSetting(
|
||||
key: string,
|
||||
value: unknown,
|
||||
): Promise<AdminSettingItem> {
|
||||
return adminRequest.put<AdminSettingItem>(`${A}/settings/${key}`, { value });
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
16
src/app/admin/(shell)/config/wallet/page.tsx
Normal file
16
src/app/admin/(shell)/config/wallet/page.tsx
Normal file
@@ -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 (
|
||||
<ModuleScaffold className="max-w-3xl">
|
||||
<WalletConfigDocScreen />
|
||||
</ModuleScaffold>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
200
src/modules/config/doc/wallet-config-doc-screen.tsx
Normal file
200
src/modules/config/doc/wallet-config-doc-screen.tsx
Normal file
@@ -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<Draft>({
|
||||
inMin: "",
|
||||
inMax: "",
|
||||
outMin: "",
|
||||
outMax: "",
|
||||
});
|
||||
const [saved, setSaved] = useState<Draft>({ 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<string, unknown> = {};
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>钱包转账限额配置</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
金额单位为游戏币种最小单位(如 NPR 下 100 = 1.00 NPR)。最小金额至少为 1 最小单位。
|
||||
</p>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="in-min">转入最小金额</Label>
|
||||
<Input
|
||||
id="in-min"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="例如 1.00"
|
||||
value={draft.inMin}
|
||||
onChange={(e) => handleChange("inMin", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
主站钱包转入彩票钱包的单笔下限
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="in-max">转入最大金额</Label>
|
||||
<Input
|
||||
id="in-max"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="例如 10000.00"
|
||||
value={draft.inMax}
|
||||
onChange={(e) => handleChange("inMax", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
主站钱包转入彩票钱包的单笔上限
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="out-min">转出最小金额</Label>
|
||||
<Input
|
||||
id="out-min"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="例如 1.00"
|
||||
value={draft.outMin}
|
||||
onChange={(e) => handleChange("outMin", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
彩票钱包转出主站钱包的单笔下限
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="out-max">转出最大金额</Label>
|
||||
<Input
|
||||
id="out-max"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="例如 10000.00"
|
||||
value={draft.outMax}
|
||||
onChange={(e) => handleChange("outMax", e.target.value)}
|
||||
disabled={loading || saving}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
彩票钱包转出主站钱包的单笔上限
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button onClick={() => void handleSave()} disabled={!dirty || loading || saving}>
|
||||
{saving ? "保存中…" : "保存"}
|
||||
</Button>
|
||||
{dirty && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setDraft(saved);
|
||||
setDirty(false);
|
||||
}}
|
||||
>
|
||||
放弃更改
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -27,3 +27,8 @@ export const configVersionsMeta = {
|
||||
title: "配置版本历史",
|
||||
description: "",
|
||||
} as const;
|
||||
|
||||
export const configWalletMeta = {
|
||||
title: "钱包配置",
|
||||
description: "",
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user