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/rebate", title: "佣金 / 回水" },
|
||||||
{ href: "/admin/config/risk-cap", title: "风控封顶" },
|
{ href: "/admin/config/risk-cap", title: "风控封顶" },
|
||||||
{ href: "/admin/config/versions", title: "配置版本历史" },
|
{ href: "/admin/config/versions", title: "配置版本历史" },
|
||||||
|
{ href: "/admin/config/wallet", title: "钱包配置" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export default function AdminConfigHubPage() {
|
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";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const LINKS: { href: string; label: string; match?: "exact" | "prefix" }[] = [
|
const LINKS: { href: string; label: string; match?: "exact" | "prefix" }[] = [
|
||||||
{ href: "/admin/config", label: "概览", match: "exact" },
|
|
||||||
{ href: "/admin/config/plays", label: "玩法配置" },
|
{ href: "/admin/config/plays", label: "玩法配置" },
|
||||||
{ href: "/admin/config/odds", label: "赔率配置" },
|
{ href: "/admin/config/odds", label: "赔率配置" },
|
||||||
{ href: "/admin/config/rebate", label: "佣金 / 回水" },
|
{ href: "/admin/config/rebate", label: "佣金 / 回水" },
|
||||||
{ href: "/admin/config/risk-cap", label: "风控封顶" },
|
{ href: "/admin/config/risk-cap", label: "风控封顶" },
|
||||||
|
{ href: "/admin/config/wallet", label: "钱包配置" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function linkActive(pathname: string, href: string, match: "exact" | "prefix"): boolean {
|
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: "配置版本历史",
|
title: "配置版本历史",
|
||||||
description: "",
|
description: "",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const configWalletMeta = {
|
||||||
|
title: "钱包配置",
|
||||||
|
description: "",
|
||||||
|
} as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user