feat(agents, i18n): enhance agent management and settlement features with new translations and UI updates
Added new translations for agent management and settlement features in English, Nepali, and Chinese, improving multi-language support. Updated the agents console to reflect changes in funding modes and player details, enhancing user experience. Refactored the admin permission gate to include new logic for handling bound line agents, ensuring better permission management. Additionally, streamlined the UI for agent-related pages and improved navigation to the settlement center, consolidating related functionalities for better accessibility.
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { postAdminAgentLine } from "@/api/admin-agent-lines";
|
||||
import { getAdminIntegrationSites } from "@/api/admin-integration-sites";
|
||||
import { AdminPageCard } from "@/components/admin/admin-page-card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -17,21 +19,22 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||
import { percentUiToRatio } from "@/lib/admin-rate-percent";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type { AdminIntegrationSiteRow } from "@/types/api/admin-integration-site";
|
||||
|
||||
export function AgentLineProvisionWizard(): React.ReactElement {
|
||||
const { t } = useTranslation(["agents", "common"]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [secrets, setSecrets] = useState<{ sso: string; wallet: string } | null>(null);
|
||||
const [sitesLoading, setSitesLoading] = useState(true);
|
||||
const [sites, setSites] = useState<AdminIntegrationSiteRow[]>([]);
|
||||
const [form, setForm] = useState({
|
||||
site_code: "",
|
||||
code: "",
|
||||
name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
currency_code: "NPR",
|
||||
wallet_api_url: "",
|
||||
notes: "",
|
||||
total_share_rate: "0",
|
||||
credit_limit: "0",
|
||||
rebate_limit: "0",
|
||||
@@ -40,33 +43,51 @@ export function AgentLineProvisionWizard(): React.ReactElement {
|
||||
can_grant_extra_rebate: false,
|
||||
});
|
||||
|
||||
useAsyncEffect(() => {
|
||||
setSitesLoading(true);
|
||||
void getAdminIntegrationSites()
|
||||
.then((data) => setSites(data.items))
|
||||
.catch(() => setSites([]))
|
||||
.finally(() => setSitesLoading(false));
|
||||
}, []);
|
||||
|
||||
const unboundSites = useMemo(
|
||||
() => sites.filter((row) => !row.has_line_root),
|
||||
[sites],
|
||||
);
|
||||
|
||||
async function onSubmit(e: React.FormEvent): Promise<void> {
|
||||
e.preventDefault();
|
||||
if (!form.site_code.trim()) {
|
||||
toast.error(t("agents:lineProvision.siteRequired", { defaultValue: "请选择接入站点" }));
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
setSecrets(null);
|
||||
try {
|
||||
const result = await postAdminAgentLine({
|
||||
await postAdminAgentLine({
|
||||
site_code: form.site_code.trim().toLowerCase(),
|
||||
code: form.code.trim().toLowerCase(),
|
||||
name: form.name.trim(),
|
||||
username: form.username.trim(),
|
||||
password: form.password,
|
||||
currency_code: form.currency_code,
|
||||
wallet_api_url: form.wallet_api_url.trim() || null,
|
||||
notes: form.notes.trim() || null,
|
||||
total_share_rate: Number.parseFloat(form.total_share_rate) || 0,
|
||||
credit_limit: Number.parseInt(form.credit_limit, 10) || 0,
|
||||
rebate_limit: Number.parseFloat(form.rebate_limit) || 0,
|
||||
default_player_rebate: Number.parseFloat(form.default_player_rebate) || 0,
|
||||
rebate_limit: percentUiToRatio(form.rebate_limit),
|
||||
default_player_rebate: percentUiToRatio(form.default_player_rebate),
|
||||
settlement_cycle: form.settlement_cycle,
|
||||
can_grant_extra_rebate: form.can_grant_extra_rebate,
|
||||
});
|
||||
if (result.secrets) {
|
||||
setSecrets({
|
||||
sso: result.secrets.sso_jwt_secret,
|
||||
wallet: result.secrets.wallet_api_key,
|
||||
});
|
||||
}
|
||||
toast.success(t("agents:lineProvision.success", { defaultValue: "线路已开通" }));
|
||||
toast.success(t("agents:lineProvision.success", { defaultValue: "一级代理已创建" }));
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
site_code: "",
|
||||
code: "",
|
||||
name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
}));
|
||||
const data = await getAdminIntegrationSites();
|
||||
setSites(data.items);
|
||||
} catch (err) {
|
||||
const msg =
|
||||
err instanceof LotteryApiBizError ? err.message : t("common:error.generic");
|
||||
@@ -77,10 +98,61 @@ export function AgentLineProvisionWizard(): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminPageCard title={t("agents:lineProvision.title", { defaultValue: "开通代理线路" })}>
|
||||
<AdminPageCard title={t("agents:lineProvision.title", { defaultValue: "创建一级代理" })}>
|
||||
<p className="mb-2 max-w-xl text-sm text-muted-foreground">
|
||||
{t("agents:subnav.provisionHint", {
|
||||
defaultValue:
|
||||
"请先在「平台管理 → 接入配置」创建接入站点;对接密钥在站点创建时一次性展示。",
|
||||
})}
|
||||
</p>
|
||||
<p className="mb-4 max-w-xl text-sm text-muted-foreground">
|
||||
{t("agents:lineProvision.description", {
|
||||
defaultValue:
|
||||
"将一级代理绑定到已有接入站点,并配置后台登录账号与占成、授信、回水、结算周期。代理编码创建后不可修改。",
|
||||
})}{" "}
|
||||
<Link
|
||||
href="/admin/config/integration-sites"
|
||||
className="font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{t("agents:lineProvision.openIntegrationSites", {
|
||||
defaultValue: "前往接入站点",
|
||||
})}
|
||||
</Link>
|
||||
</p>
|
||||
<form className="grid max-w-xl gap-4" onSubmit={onSubmit}>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("agents:lineProvision.code", { defaultValue: "站点 code" })}</Label>
|
||||
<Label>{t("agents:lineProvision.siteCode", { defaultValue: "接入站点" })}</Label>
|
||||
<Select
|
||||
value={form.site_code}
|
||||
onValueChange={(value) => setForm((f) => ({ ...f, site_code: value ?? "" }))}
|
||||
disabled={sitesLoading || unboundSites.length === 0}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
sitesLoading
|
||||
? t("common:loading", { defaultValue: "加载中…" })
|
||||
: unboundSites.length === 0
|
||||
? t("agents:lineProvision.noUnboundSite", {
|
||||
defaultValue: "暂无未绑定一级代理的站点",
|
||||
})
|
||||
: t("agents:lineProvision.siteCodePlaceholder", {
|
||||
defaultValue: "选择站点",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{unboundSites.map((site) => (
|
||||
<SelectItem key={site.id} value={site.code}>
|
||||
{site.name} ({site.code})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("agents:lineProvision.code", { defaultValue: "代理编码" })}</Label>
|
||||
<Input
|
||||
value={form.code}
|
||||
onChange={(e) => setForm((f) => ({ ...f, code: e.target.value }))}
|
||||
@@ -89,7 +161,7 @@ export function AgentLineProvisionWizard(): React.ReactElement {
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("agents:lineProvision.name", { defaultValue: "线路名称" })}</Label>
|
||||
<Label>{t("agents:lineProvision.name", { defaultValue: "一级代理名称" })}</Label>
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
@@ -97,7 +169,7 @@ export function AgentLineProvisionWizard(): React.ReactElement {
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("agents:lineProvision.username", { defaultValue: "代理账号" })}</Label>
|
||||
<Label>{t("agents:lineProvision.username", { defaultValue: "后台登录账号" })}</Label>
|
||||
<Input
|
||||
value={form.username}
|
||||
onChange={(e) => setForm((f) => ({ ...f, username: e.target.value }))}
|
||||
@@ -114,13 +186,6 @@ export function AgentLineProvisionWizard(): React.ReactElement {
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("agents:lineProvision.walletUrl", { defaultValue: "钱包 API URL" })}</Label>
|
||||
<Input
|
||||
value={form.wallet_api_url}
|
||||
onChange={(e) => setForm((f) => ({ ...f, wallet_api_url: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-sm font-medium">
|
||||
{t("agents:profile.section", { defaultValue: "占成与授信" })}
|
||||
@@ -147,24 +212,26 @@ export function AgentLineProvisionWizard(): React.ReactElement {
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("agents:profile.rebateLimit", { defaultValue: "回水上限" })}</Label>
|
||||
<Label>{t("agents:profile.rebateLimit", { defaultValue: "回水上限 (%)" })}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={1}
|
||||
step="0.0001"
|
||||
max={100}
|
||||
step="0.01"
|
||||
value={form.rebate_limit}
|
||||
placeholder="0.5"
|
||||
onChange={(e) => setForm((f) => ({ ...f, rebate_limit: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("agents:profile.defaultPlayerRebate", { defaultValue: "默认玩家回水" })}</Label>
|
||||
<Label>{t("agents:profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={1}
|
||||
step="0.0001"
|
||||
max={100}
|
||||
step="0.01"
|
||||
value={form.default_player_rebate}
|
||||
placeholder="0.5"
|
||||
onChange={(e) => setForm((f) => ({ ...f, default_player_rebate: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
@@ -208,32 +275,12 @@ export function AgentLineProvisionWizard(): React.ReactElement {
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("common:notes", { defaultValue: "备注" })}</Label>
|
||||
<Textarea
|
||||
value={form.notes}
|
||||
onChange={(e) => setForm((f) => ({ ...f, notes: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={submitting}>
|
||||
<Button type="submit" disabled={submitting || unboundSites.length === 0}>
|
||||
{submitting
|
||||
? t("common:submitting", { defaultValue: "提交中…" })
|
||||
: t("agents:lineProvision.submit", { defaultValue: "开通线路" })}
|
||||
: t("agents:lineProvision.submit", { defaultValue: "创建一级代理" })}
|
||||
</Button>
|
||||
</form>
|
||||
{secrets ? (
|
||||
<div className="mt-6 rounded-md border border-amber-500/40 bg-amber-500/5 p-4 text-sm">
|
||||
<p className="font-medium text-amber-700">
|
||||
{t("agents:lineProvision.secretsOnce", { defaultValue: "密钥仅显示一次,请妥善保存" })}
|
||||
</p>
|
||||
<p className="mt-2 break-all">
|
||||
SSO: <code>{secrets.sso}</code>
|
||||
</p>
|
||||
<p className="mt-1 break-all">
|
||||
Wallet API Key: <code>{secrets.wallet}</code>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</AdminPageCard>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user