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:
2026-06-04 18:01:05 +08:00
parent c2eac2fafc
commit 65eaeecf8c
139 changed files with 8852 additions and 1435 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import { Download, Link2, Pencil, ShieldAlert } from "lucide-react";
import { useCallback, useState } from "react";
import { Copy, Download, Link2, Pencil, ShieldAlert } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
@@ -11,12 +11,14 @@ import {
getAdminIntegrationSite,
getAdminIntegrationSiteExport,
getAdminIntegrationSites,
getAdminIntegrationSiteSecrets,
postAdminIntegrationSite,
postAdminIntegrationSiteConnectivityTest,
postAdminIntegrationSiteRotateSecrets,
putAdminIntegrationSite,
} from "@/api/admin-integration-sites";
import { AdminPageCard } from "@/components/admin/admin-page-card";
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Button } from "@/components/ui/button";
@@ -54,6 +56,60 @@ import type {
AdminIntegrationSiteWithSecrets,
} from "@/types/api/admin-integration-site";
function CopyIconButton({
label,
onClick,
disabled,
busy,
}: {
label: string;
onClick: () => void;
disabled?: boolean;
busy?: boolean;
}): React.ReactElement {
return (
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 shrink-0"
disabled={disabled || busy}
aria-label={label}
title={label}
onClick={onClick}
>
<Copy className="h-3.5 w-3.5" />
</Button>
);
}
function MaskedValueWithCopy({
configured,
masked,
copyLabel,
canCopy,
copying,
onCopy,
}: {
configured: boolean;
masked: string | null;
copyLabel: string;
canCopy: boolean;
copying: boolean;
onCopy: () => void;
}): React.ReactElement {
return (
<div className="flex min-w-[7.5rem] max-w-[11rem] items-center gap-0.5">
<span className="truncate font-mono text-xs text-muted-foreground">
{configured ? (masked ?? "••••••••") : "—"}
</span>
{configured && canCopy ? (
<CopyIconButton label={copyLabel} onClick={onCopy} busy={copying} />
) : null}
</div>
);
}
type FormState = {
code: string;
name: string;
@@ -140,7 +196,7 @@ function formToPayload(
}
type IntegrationSitesConsoleProps = {
/** 代理线路内站点列表:仅超管可新建站点,普通账号走「开通线路」。 */
/** 为 true 时仅超管可新建站点(默认有 integration.site.manage 即可创建)。 */
restrictCreateToSuperAdmin?: boolean;
};
@@ -180,9 +236,12 @@ export function IntegrationSitesConsole({
const [connectivityResult, setConnectivityResult] =
useState<AdminIntegrationSiteConnectivityResult | null>(null);
const [exportBusyId, setExportBusyId] = useState<number | null>(null);
const [secretCopyBusyKey, setSecretCopyBusyKey] = useState<string | null>(null);
const secretsCacheRef = useRef(new Map<number, AdminIntegrationSiteSecrets>());
const load = useCallback(async () => {
setLoading(true);
secretsCacheRef.current.clear();
try {
const data = await getAdminIntegrationSites();
setItems(data.items);
@@ -273,6 +332,7 @@ export function IntegrationSitesConsole({
const result = await postAdminIntegrationSiteRotateSecrets(rotateTarget.id);
toast.success(t("integrationSites.rotateSuccess", { code: rotateTarget.code }));
setRotateTarget(null);
secretsCacheRef.current.delete(rotateTarget.id);
showSecretsOnce(result);
await load();
} catch (error) {
@@ -350,6 +410,55 @@ export function IntegrationSitesConsole({
}
}
async function resolveSiteSecrets(siteId: number): Promise<AdminIntegrationSiteSecrets> {
const cached = secretsCacheRef.current.get(siteId);
if (cached) {
return cached;
}
const secrets = await getAdminIntegrationSiteSecrets(siteId);
secretsCacheRef.current.set(siteId, secrets);
return secrets;
}
async function copySiteSecret(
row: AdminIntegrationSiteRow,
field: "sso" | "wallet",
): Promise<void> {
if (!canManage) {
toast.error(t("integrationSites.secretCopyRequiresManage"));
return;
}
const configured = field === "sso" ? row.has_sso_secret : row.has_wallet_api_key;
if (!configured) {
toast.error(t("integrationSites.secretNotConfigured"));
return;
}
const busyKey = `${row.id}:${field}`;
setSecretCopyBusyKey(busyKey);
try {
const secrets = await resolveSiteSecrets(row.id);
const value = field === "sso" ? secrets.sso_jwt_secret : secrets.wallet_api_key;
if (!value) {
toast.error(t("integrationSites.secretNotConfigured"));
return;
}
await copyText(
field === "sso"
? t("integrationSites.fields.ssoSecret")
: t("integrationSites.fields.walletApiKey"),
value,
);
} catch (error) {
toast.error(
error instanceof LotteryApiBizError ? error.message : t("integrationSites.copyFailed"),
);
} finally {
setSecretCopyBusyKey(null);
}
}
return (
<>
<AdminPageCard
@@ -366,23 +475,38 @@ export function IntegrationSitesConsole({
{loading ? (
<AdminLoadingState minHeight="8rem" label={t("integrationSites.loading")} />
) : items.length === 0 ? (
<p className="text-sm text-muted-foreground">{t("integrationSites.empty")}</p>
<AdminNoResourceState />
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("integrationSites.columns.code")}</TableHead>
<TableHead>{t("integrationSites.columns.name")}</TableHead>
<TableHead>{t("integrationSites.columns.currency")}</TableHead>
<TableHead>{t("integrationSites.columns.status")}</TableHead>
<TableHead>{t("integrationSites.columns.lineRoot")}</TableHead>
<TableHead>{t("integrationSites.columns.walletUrl")}</TableHead>
<TableHead>{t("integrationSites.columns.h5Url")}</TableHead>
<TableHead>{t("integrationSites.columns.ssoSecret")}</TableHead>
<TableHead>{t("integrationSites.columns.walletApiKey")}</TableHead>
<TableHead className="sticky right-0 z-20 bg-muted w-14 text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("integrationSites.columns.actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((row) => (
<TableRow key={row.id}>
<TableCell className="font-mono text-xs">{row.code}</TableCell>
<TableCell>
<div className="flex max-w-[10rem] items-center gap-0.5">
<span className="truncate font-mono text-xs">{row.code}</span>
<CopyIconButton
label={t("integrationSites.copy")}
onClick={() => void copyText(t("integrationSites.columns.code"), row.code)}
/>
</div>
</TableCell>
<TableCell>{row.name}</TableCell>
<TableCell className="font-mono text-xs">{row.currency_code}</TableCell>
<TableCell>
<AdminStatusBadge
tone={row.status === 1 ? "success" : "neutral"}
@@ -392,8 +516,53 @@ export function IntegrationSitesConsole({
: t("integrationSites.statusDisabled")}
</AdminStatusBadge>
</TableCell>
<TableCell className="max-w-[240px] truncate text-xs text-muted-foreground">
{row.wallet_api_url ?? "—"}
<TableCell>
<AdminStatusBadge tone={row.has_line_root ? "success" : "neutral"}>
{row.has_line_root
? t("integrationSites.lineRootBound")
: t("integrationSites.lineRootUnbound")}
</AdminStatusBadge>
</TableCell>
<TableCell>
<div className="flex max-w-[14rem] items-center gap-0.5">
<span className="truncate text-xs text-muted-foreground">
{row.wallet_api_url ?? "—"}
</span>
{row.wallet_api_url ? (
<CopyIconButton
label={t("integrationSites.copy")}
onClick={() =>
void copyText(
t("integrationSites.columns.walletUrl"),
row.wallet_api_url ?? "",
)
}
/>
) : null}
</div>
</TableCell>
<TableCell className="max-w-[12rem] truncate text-xs text-muted-foreground">
{row.lottery_h5_base_url ?? "—"}
</TableCell>
<TableCell>
<MaskedValueWithCopy
configured={row.has_sso_secret}
masked={row.sso_secret_masked}
copyLabel={t("integrationSites.copy")}
canCopy={canManage}
copying={secretCopyBusyKey === `${row.id}:sso`}
onCopy={() => void copySiteSecret(row, "sso")}
/>
</TableCell>
<TableCell>
<MaskedValueWithCopy
configured={row.has_wallet_api_key}
masked={row.wallet_api_key_masked}
copyLabel={t("integrationSites.copy")}
canCopy={canManage}
copying={secretCopyBusyKey === `${row.id}:wallet`}
onCopy={() => void copySiteSecret(row, "wallet")}
/>
</TableCell>
<TableCell className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
<AdminRowActionsMenu
@@ -434,6 +603,7 @@ export function IntegrationSitesConsole({
))}
</TableBody>
</Table>
</div>
)}
</AdminPageCard>
@@ -645,36 +815,30 @@ export function IntegrationSitesConsole({
<Label>{t("integrationSites.fields.ssoSecret")}</Label>
<div className="flex gap-2">
<Input readOnly value={secretsDialog.secrets.sso_jwt_secret} className="font-mono text-xs" />
<Button
type="button"
variant="outline"
<CopyIconButton
label={t("integrationSites.copy")}
onClick={() =>
void copyText(
t("integrationSites.fields.ssoSecret"),
secretsDialog.secrets.sso_jwt_secret,
)
}
>
{t("integrationSites.copy")}
</Button>
/>
</div>
</div>
<div className="space-y-2">
<Label>{t("integrationSites.fields.walletApiKey")}</Label>
<div className="flex gap-2">
<Input readOnly value={secretsDialog.secrets.wallet_api_key} className="font-mono text-xs" />
<Button
type="button"
variant="outline"
<CopyIconButton
label={t("integrationSites.copy")}
onClick={() =>
void copyText(
t("integrationSites.fields.walletApiKey"),
secretsDialog.secrets.wallet_api_key,
)
}
>
{t("integrationSites.copy")}
</Button>
/>
</div>
</div>
</div>