feat(bet-providers): 添加开注商管理功能,包括增删改查接口及前端页面
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
This commit is contained in:
26
src/api/admin-bet-providers.ts
Normal file
26
src/api/admin-bet-providers.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { adminRequest } from "@/lib/admin-http";
|
||||||
|
import type {
|
||||||
|
AdminBetProviderCreatePayload,
|
||||||
|
AdminBetProviderListData,
|
||||||
|
AdminBetProviderRow,
|
||||||
|
AdminBetProviderUpdatePayload,
|
||||||
|
} from "@/types/api/admin-bet-provider";
|
||||||
|
|
||||||
|
const A = `/admin`;
|
||||||
|
|
||||||
|
export async function getAdminBetProviders(): Promise<AdminBetProviderListData> {
|
||||||
|
return adminRequest.get<AdminBetProviderListData>(`${A}/bet-providers`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAdminBetProvider(
|
||||||
|
body: AdminBetProviderCreatePayload,
|
||||||
|
): Promise<AdminBetProviderRow> {
|
||||||
|
return adminRequest.post<AdminBetProviderRow>(`${A}/bet-providers`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putAdminBetProvider(
|
||||||
|
id: number,
|
||||||
|
body: AdminBetProviderUpdatePayload,
|
||||||
|
): Promise<AdminBetProviderRow> {
|
||||||
|
return adminRequest.put<AdminBetProviderRow>(`${A}/bet-providers/${id}`, body);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ export type TicketItemsListQuery = {
|
|||||||
site_code?: string;
|
site_code?: string;
|
||||||
agent_node_id?: number;
|
agent_node_id?: number;
|
||||||
draw_no?: string;
|
draw_no?: string;
|
||||||
|
provider_code?: string;
|
||||||
status?: string[];
|
status?: string[];
|
||||||
number?: string;
|
number?: string;
|
||||||
start_date?: string;
|
start_date?: string;
|
||||||
|
|||||||
15
src/app/admin/(shell)/config/bet-providers/page.tsx
Normal file
15
src/app/admin/(shell)/config/bet-providers/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
|
import { BetProviderSettingsPanel } from "@/modules/config/bet-provider-settings-panel";
|
||||||
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
|
import { PRD_BET_PROVIDER_MANAGE } from "@/lib/admin-prd";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = buildPageMetadata("config", "betProviders.title");
|
||||||
|
|
||||||
|
export default function AdminBetProvidersPage() {
|
||||||
|
return (
|
||||||
|
<AdminPermissionGate requiredAny={[PRD_BET_PROVIDER_MANAGE]}>
|
||||||
|
<BetProviderSettingsPanel />
|
||||||
|
</AdminPermissionGate>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -58,6 +58,8 @@
|
|||||||
"currenciesTitle": "Currencies",
|
"currenciesTitle": "Currencies",
|
||||||
"jackpotDesc": "Pool parameters and ledger records",
|
"jackpotDesc": "Pool parameters and ledger records",
|
||||||
"jackpotTitle": "Jackpot",
|
"jackpotTitle": "Jackpot",
|
||||||
|
"betProvidersDesc": "Manage selectable betting providers",
|
||||||
|
"betProvidersTitle": "Bet providers",
|
||||||
"oddsDesc": "Odds matrix and base rebate in one version stream",
|
"oddsDesc": "Odds matrix and base rebate in one version stream",
|
||||||
"oddsTitle": "Odds & base rebate",
|
"oddsTitle": "Odds & base rebate",
|
||||||
"playsDesc": "Play switches, limits, and rule copy",
|
"playsDesc": "Play switches, limits, and rule copy",
|
||||||
@@ -68,6 +70,9 @@
|
|||||||
"settingsTitle": "System settings",
|
"settingsTitle": "System settings",
|
||||||
"title": "Operations configuration"
|
"title": "Operations configuration"
|
||||||
},
|
},
|
||||||
|
"betProviders": {
|
||||||
|
"title": "Bet providers"
|
||||||
|
},
|
||||||
"integrationSites": {
|
"integrationSites": {
|
||||||
"adminAccountCreated": "Created site admin account {{username}}",
|
"adminAccountCreated": "Created site admin account {{username}}",
|
||||||
"adminAccountSectionDescription": "Creating a site will also create one admin account bound to that site.",
|
"adminAccountSectionDescription": "Creating a site will also create one admin account bound to that site.",
|
||||||
|
|||||||
@@ -58,6 +58,8 @@
|
|||||||
"currenciesTitle": "मुद्रा व्यवस्थापन",
|
"currenciesTitle": "मुद्रा व्यवस्थापन",
|
||||||
"jackpotDesc": "पूल प्यारामिटर र लेजर",
|
"jackpotDesc": "पूल प्यारामिटर र लेजर",
|
||||||
"jackpotTitle": "ज्याकपट",
|
"jackpotTitle": "ज्याकपट",
|
||||||
|
"betProvidersDesc": "छनोट गर्न मिल्ने बेट प्रदायक व्यवस्थापन",
|
||||||
|
"betProvidersTitle": "बेट प्रदायक",
|
||||||
"oddsDesc": "बाधा म्याट्रिक्स र रिबेट, एउटै संस्करण",
|
"oddsDesc": "बाधा म्याट्रिक्स र रिबेट, एउटै संस्करण",
|
||||||
"oddsTitle": "बाधा र रिबेट",
|
"oddsTitle": "बाधा र रिबेट",
|
||||||
"playsDesc": "खेल स्विच, सीमा र नियम पाठ",
|
"playsDesc": "खेल स्विच, सीमा र नियम पाठ",
|
||||||
@@ -68,6 +70,9 @@
|
|||||||
"settingsTitle": "प्रणाली सेटिङ",
|
"settingsTitle": "प्रणाली सेटिङ",
|
||||||
"title": "सञ्चालन कन्फिगरेसन अवलोकन"
|
"title": "सञ्चालन कन्फिगरेसन अवलोकन"
|
||||||
},
|
},
|
||||||
|
"betProviders": {
|
||||||
|
"title": "बेट प्रदायक"
|
||||||
|
},
|
||||||
"integrationSites": {
|
"integrationSites": {
|
||||||
"adminAccountCreated": "साइट एडमिन खाता {{username}} सिर्जना भयो",
|
"adminAccountCreated": "साइट एडमिन खाता {{username}} सिर्जना भयो",
|
||||||
"adminAccountSectionDescription": "साइट सिर्जना गर्दा त्यस साइटसँग बाँधिएको एउटा एडमिन खाता पनि सिर्जना हुन्छ।",
|
"adminAccountSectionDescription": "साइट सिर्जना गर्दा त्यस साइटसँग बाँधिएको एउटा एडमिन खाता पनि सिर्जना हुन्छ।",
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
"oddsDesc": "赔率矩阵与基础回水,版本一体发布",
|
"oddsDesc": "赔率矩阵与基础回水,版本一体发布",
|
||||||
"jackpotTitle": "奖池",
|
"jackpotTitle": "奖池",
|
||||||
"jackpotDesc": "奖池参数与进账流水",
|
"jackpotDesc": "奖池参数与进账流水",
|
||||||
|
"betProvidersTitle": "开注商管理",
|
||||||
|
"betProvidersDesc": "维护玩家可勾选的下注公司",
|
||||||
"riskCapTitle": "限额版本",
|
"riskCapTitle": "限额版本",
|
||||||
"riskCapDesc": "号码赔付封顶与占用视图",
|
"riskCapDesc": "号码赔付封顶与占用视图",
|
||||||
"integrationTitle": "接入站点",
|
"integrationTitle": "接入站点",
|
||||||
@@ -38,6 +40,9 @@
|
|||||||
"currenciesTitle": "币种管理",
|
"currenciesTitle": "币种管理",
|
||||||
"currenciesDesc": "维护支持的币种与精度"
|
"currenciesDesc": "维护支持的币种与精度"
|
||||||
},
|
},
|
||||||
|
"betProviders": {
|
||||||
|
"title": "开注商管理"
|
||||||
|
},
|
||||||
"integrationSites": {
|
"integrationSites": {
|
||||||
"title": "接入站点",
|
"title": "接入站点",
|
||||||
"description": "由运营在后台维护各主站对接参数,并通过权限控制谁能查看或修改。site_code 创建后不可修改。",
|
"description": "由运营在后台维护各主站对接参数,并通过权限控制谁能查看或修改。site_code 创建后不可修改。",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const PRD_DRAW_RESULT_VIEW = "prd.draw_result.view" as const;
|
|||||||
export const PRD_DRAW_REOPEN_MANAGE = "prd.draw_reopen.manage" as const;
|
export const PRD_DRAW_REOPEN_MANAGE = "prd.draw_reopen.manage" as const;
|
||||||
|
|
||||||
export const PRD_PLAY_SWITCH_MANAGE = "prd.play_switch.manage" as const;
|
export const PRD_PLAY_SWITCH_MANAGE = "prd.play_switch.manage" as const;
|
||||||
|
export const PRD_BET_PROVIDER_MANAGE = "prd.bet_provider.manage" as const;
|
||||||
export const PRD_ODDS_MANAGE = "prd.odds.manage" as const;
|
export const PRD_ODDS_MANAGE = "prd.odds.manage" as const;
|
||||||
export const PRD_REBATE_MANAGE = "prd.rebate.manage" as const;
|
export const PRD_REBATE_MANAGE = "prd.rebate.manage" as const;
|
||||||
export const PRD_REBATE_VIEW = "prd.rebate.view" as const;
|
export const PRD_REBATE_VIEW = "prd.rebate.view" as const;
|
||||||
@@ -169,6 +170,7 @@ export const PRD_INTEGRATION_ACCESS_ANY = [PRD_INTEGRATION_VIEW, PRD_INTEGRATION
|
|||||||
/** 运营配置首页 */
|
/** 运营配置首页 */
|
||||||
export const PRD_CONFIG_HUB_ACCESS_ANY = [
|
export const PRD_CONFIG_HUB_ACCESS_ANY = [
|
||||||
...PRD_RULES_PLAYS_ACCESS_ANY,
|
...PRD_RULES_PLAYS_ACCESS_ANY,
|
||||||
|
PRD_BET_PROVIDER_MANAGE,
|
||||||
...PRD_RULES_ODDS_ACCESS_ANY,
|
...PRD_RULES_ODDS_ACCESS_ANY,
|
||||||
...PRD_JACKPOT_ACCESS_ANY,
|
...PRD_JACKPOT_ACCESS_ANY,
|
||||||
...PRD_RISK_CAP_ACCESS_ANY,
|
...PRD_RISK_CAP_ACCESS_ANY,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
Building2,
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
Coins,
|
Coins,
|
||||||
@@ -34,6 +35,7 @@ export const adminNavIconBySegment: Record<AdminNavItem["segment"], LucideIcon>
|
|||||||
players: Users,
|
players: Users,
|
||||||
draws: CalendarClock,
|
draws: CalendarClock,
|
||||||
rules_plays: ClipboardList,
|
rules_plays: ClipboardList,
|
||||||
|
bet_providers: Building2,
|
||||||
rules_odds: Percent,
|
rules_odds: Percent,
|
||||||
jackpot: Trophy,
|
jackpot: Trophy,
|
||||||
risk_cap: ShieldAlert,
|
risk_cap: ShieldAlert,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type AdminNavSegment =
|
|||||||
| "players"
|
| "players"
|
||||||
| "draws"
|
| "draws"
|
||||||
| "rules_plays"
|
| "rules_plays"
|
||||||
|
| "bet_providers"
|
||||||
| "rules_odds"
|
| "rules_odds"
|
||||||
| "jackpot"
|
| "jackpot"
|
||||||
| "risk_cap"
|
| "risk_cap"
|
||||||
|
|||||||
308
src/modules/config/bet-provider-settings-panel.tsx
Normal file
308
src/modules/config/bet-provider-settings-panel.tsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Pencil } from "lucide-react";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAdminBetProviders,
|
||||||
|
postAdminBetProvider,
|
||||||
|
putAdminBetProvider,
|
||||||
|
} from "@/api/admin-bet-providers";
|
||||||
|
import { AdminPageCard } from "@/components/admin/admin-page-card";
|
||||||
|
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||||
|
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||||
|
import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||||
|
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||||
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
|
import type { AdminBetProviderRow } from "@/types/api/admin-bet-provider";
|
||||||
|
|
||||||
|
type ProviderFormState = {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
is_enabled: boolean;
|
||||||
|
sort_order: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMPTY_FORM: ProviderFormState = {
|
||||||
|
code: "",
|
||||||
|
name: "",
|
||||||
|
is_enabled: true,
|
||||||
|
sort_order: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
function toFormState(row: AdminBetProviderRow): ProviderFormState {
|
||||||
|
return {
|
||||||
|
code: row.code,
|
||||||
|
name: row.name,
|
||||||
|
is_enabled: row.is_enabled,
|
||||||
|
sort_order: String(row.sort_order),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSortOrder(value: string): number {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BetProviderSettingsPanel() {
|
||||||
|
const { t } = useTranslation(["config", "adminUsers"]);
|
||||||
|
const tRef = useTranslationRef(["config", "adminUsers"]);
|
||||||
|
const [items, setItems] = useState<AdminBetProviderRow[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [mode, setMode] = useState<"create" | "edit">("create");
|
||||||
|
const [editingId, setEditingId] = useState<number | null>(null);
|
||||||
|
const [form, setForm] = useState<ProviderFormState>(EMPTY_FORM);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await getAdminBetProviders();
|
||||||
|
setItems(data.items);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof LotteryApiBizError
|
||||||
|
? error.message
|
||||||
|
: tRef.current("betProviders.loadFailed", {
|
||||||
|
ns: "config",
|
||||||
|
defaultValue: "加载开注商失败",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [tRef]);
|
||||||
|
|
||||||
|
useAsyncEffect(() => {
|
||||||
|
void load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function openCreate(): void {
|
||||||
|
setMode("create");
|
||||||
|
setEditingId(null);
|
||||||
|
setForm(EMPTY_FORM);
|
||||||
|
setDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row: AdminBetProviderRow): void {
|
||||||
|
setMode("edit");
|
||||||
|
setEditingId(row.id);
|
||||||
|
setForm(toFormState(row));
|
||||||
|
setDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateForm<K extends keyof ProviderFormState>(key: K, value: ProviderFormState[K]): void {
|
||||||
|
setForm((prev) => ({ ...prev, [key]: value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(): Promise<void> {
|
||||||
|
const name = form.name.trim();
|
||||||
|
const sortOrder = parseSortOrder(form.sort_order);
|
||||||
|
if (name === "" || (mode === "create" && form.code.trim() === "")) {
|
||||||
|
toast.error(t("betProviders.form.required", { ns: "config", defaultValue: "请填写代码和名称" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
if (mode === "create") {
|
||||||
|
await postAdminBetProvider({
|
||||||
|
code: form.code.trim().toUpperCase(),
|
||||||
|
name,
|
||||||
|
is_enabled: form.is_enabled,
|
||||||
|
sort_order: sortOrder,
|
||||||
|
});
|
||||||
|
toast.success(t("betProviders.createSuccess", { ns: "config", defaultValue: "已创建开注商" }));
|
||||||
|
} else if (editingId !== null) {
|
||||||
|
await putAdminBetProvider(editingId, {
|
||||||
|
name,
|
||||||
|
is_enabled: form.is_enabled,
|
||||||
|
sort_order: sortOrder,
|
||||||
|
});
|
||||||
|
toast.success(t("betProviders.updateSuccess", { ns: "config", defaultValue: "已更新开注商" }));
|
||||||
|
}
|
||||||
|
setDialogOpen(false);
|
||||||
|
await load();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof LotteryApiBizError
|
||||||
|
? error.message
|
||||||
|
: t("betProviders.saveFailed", { ns: "config", defaultValue: "保存开注商失败" }),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdminPageCard
|
||||||
|
title={t("betProviders.title", { ns: "config", defaultValue: "开注商管理" })}
|
||||||
|
description={t("betProviders.description", {
|
||||||
|
ns: "config",
|
||||||
|
defaultValue: "维护玩家下注时可勾选的公司;一期仅控制显示与启用状态,不独立配置赔率。",
|
||||||
|
})}
|
||||||
|
actions={<Button onClick={openCreate}>{t("betProviders.create", { ns: "config", defaultValue: "新增开注商" })}</Button>}
|
||||||
|
>
|
||||||
|
<div className="admin-table-shell">
|
||||||
|
<Table id="admin-bet-providers-table">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="whitespace-nowrap">{t("betProviders.table.code", { ns: "config", defaultValue: "代码" })}</TableHead>
|
||||||
|
<TableHead>{t("betProviders.table.name", { ns: "config", defaultValue: "名称" })}</TableHead>
|
||||||
|
<TableHead className="whitespace-nowrap">{t("betProviders.table.enabled", { ns: "config", defaultValue: "状态" })}</TableHead>
|
||||||
|
<TableHead className="text-center">{t("betProviders.table.sort", { ns: "config", defaultValue: "排序" })}</TableHead>
|
||||||
|
<TableHead className="sticky right-0 z-20 w-14 bg-muted text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
|
||||||
|
{t("betProviders.table.actions", { ns: "config", defaultValue: "操作" })}
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center text-sm text-muted-foreground">
|
||||||
|
{t("betProviders.loading", { ns: "config", defaultValue: "加载中..." })}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : items.length === 0 ? (
|
||||||
|
<AdminTableNoResourceRow colSpan={5} cellClassName="text-center" />
|
||||||
|
) : (
|
||||||
|
items.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
<TableCell className="font-mono font-semibold">{row.code}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{row.name}
|
||||||
|
{row.is_default ? (
|
||||||
|
<span className="ml-2 rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
|
||||||
|
{t("betProviders.default", { ns: "config", defaultValue: "默认" })}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<AdminStatusBadge status={row.is_enabled ? "enabled" : "disabled"}>
|
||||||
|
{row.is_enabled
|
||||||
|
? t("system.states.enabled", { ns: "config" })
|
||||||
|
: t("system.states.disabled", { ns: "config" })}
|
||||||
|
</AdminStatusBadge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center font-mono text-xs">{row.sort_order}</TableCell>
|
||||||
|
<TableCell className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
|
||||||
|
<AdminRowActionsMenu
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
key: "edit",
|
||||||
|
label: t("betProviders.edit", { ns: "config", defaultValue: "编辑" }),
|
||||||
|
icon: Pencil,
|
||||||
|
onClick: () => openEdit(row),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</AdminPageCard>
|
||||||
|
|
||||||
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
|
<DialogContent showCloseButton className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t(mode === "create" ? "betProviders.dialog.createTitle" : "betProviders.dialog.editTitle", {
|
||||||
|
ns: "config",
|
||||||
|
defaultValue: mode === "create" ? "新增开注商" : "编辑开注商",
|
||||||
|
})}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t("betProviders.dialog.description", {
|
||||||
|
ns: "config",
|
||||||
|
defaultValue: "代码创建后不可修改;停用后玩家端不可再选择。",
|
||||||
|
})}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="provider-code">{t("betProviders.form.code", { ns: "config", defaultValue: "代码" })}</Label>
|
||||||
|
<Input
|
||||||
|
id="provider-code"
|
||||||
|
value={form.code}
|
||||||
|
placeholder="SG"
|
||||||
|
onChange={(e) => updateForm("code", e.target.value.toUpperCase())}
|
||||||
|
disabled={saving || mode === "edit"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="provider-name">{t("betProviders.form.name", { ns: "config", defaultValue: "名称" })}</Label>
|
||||||
|
<Input
|
||||||
|
id="provider-name"
|
||||||
|
value={form.name}
|
||||||
|
placeholder="Singapore"
|
||||||
|
onChange={(e) => updateForm("name", e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="provider-sort">{t("betProviders.form.sort", { ns: "config", defaultValue: "排序" })}</Label>
|
||||||
|
<Input
|
||||||
|
id="provider-sort"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={form.sort_order}
|
||||||
|
onChange={(e) => updateForm("sort_order", e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between rounded-xl border border-border/70 p-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium">{t("betProviders.form.enabled", { ns: "config", defaultValue: "启用" })}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("betProviders.form.enabledHint", { ns: "config", defaultValue: "关闭后不会出现在玩家端可选公司里" })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={form.is_enabled}
|
||||||
|
onCheckedChange={(checked) => updateForm("is_enabled", checked)}
|
||||||
|
disabled={saving}
|
||||||
|
aria-label={t("betProviders.form.enabled", { ns: "config", defaultValue: "启用" })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button variant="outline" onClick={() => setDialogOpen(false)} disabled={saving}>
|
||||||
|
{t("actions.cancel", { ns: "adminUsers" })}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => void handleSubmit()} disabled={saving}>
|
||||||
|
{saving ? t("saving", { ns: "adminUsers" }) : t("actions.save", { ns: "adminUsers" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,12 @@ import { ChevronRight } from "lucide-react";
|
|||||||
|
|
||||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { PRD_INTEGRATION_ACCESS_ANY, PRD_SETTINGS_ACCESS_ANY, PRD_CURRENCY_MANAGE } from "@/lib/admin-prd";
|
import {
|
||||||
|
PRD_BET_PROVIDER_MANAGE,
|
||||||
|
PRD_CURRENCY_MANAGE,
|
||||||
|
PRD_INTEGRATION_ACCESS_ANY,
|
||||||
|
PRD_SETTINGS_ACCESS_ANY,
|
||||||
|
} from "@/lib/admin-prd";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
|
||||||
@@ -36,6 +41,12 @@ const HUB_CARDS: HubCard[] = [
|
|||||||
descKey: "hub.jackpotDesc",
|
descKey: "hub.jackpotDesc",
|
||||||
requiredAny: ["prd.jackpot.manage", "prd.jackpot.view"],
|
requiredAny: ["prd.jackpot.manage", "prd.jackpot.view"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "/admin/config/bet-providers",
|
||||||
|
titleKey: "hub.betProvidersTitle",
|
||||||
|
descKey: "hub.betProvidersDesc",
|
||||||
|
requiredAny: [PRD_BET_PROVIDER_MANAGE],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: "/admin/risk/cap",
|
href: "/admin/risk/cap",
|
||||||
titleKey: "hub.riskCapTitle",
|
titleKey: "hub.riskCapTitle",
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ function ticketStatusText(status: string, t: (key: string, opts?: { ns?: string
|
|||||||
return translated === key ? status : translated;
|
return translated === key ? status : translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function providerLabel(providerName?: string | null, providerCode?: string | null): string {
|
||||||
|
return providerName || providerCode || "—";
|
||||||
|
}
|
||||||
|
|
||||||
function walletStatusLabel(status: string, t: (key: string, opts?: { ns?: string }) => string): string {
|
function walletStatusLabel(status: string, t: (key: string, opts?: { ns?: string }) => string): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "processing":
|
case "processing":
|
||||||
@@ -438,6 +442,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
|||||||
<TableHead>{t("ticketNo", { ns: "tickets" })}</TableHead>
|
<TableHead>{t("ticketNo", { ns: "tickets" })}</TableHead>
|
||||||
<TableHead>{t("orderNo", { ns: "tickets" })}</TableHead>
|
<TableHead>{t("orderNo", { ns: "tickets" })}</TableHead>
|
||||||
<TableHead>{t("drawNo", { ns: "tickets" })}</TableHead>
|
<TableHead>{t("drawNo", { ns: "tickets" })}</TableHead>
|
||||||
|
<TableHead>{t("provider", { ns: "tickets", defaultValue: "开注商" })}</TableHead>
|
||||||
<TableHead>{t("playCode", { ns: "tickets" })}</TableHead>
|
<TableHead>{t("playCode", { ns: "tickets" })}</TableHead>
|
||||||
<TableHead>{t("number", { ns: "tickets" })}</TableHead>
|
<TableHead>{t("number", { ns: "tickets" })}</TableHead>
|
||||||
<TableHead className="text-center">{t("actualDeduct", { ns: "tickets" })}</TableHead>
|
<TableHead className="text-center">{t("actualDeduct", { ns: "tickets" })}</TableHead>
|
||||||
@@ -450,7 +455,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{ticketsLoading && tickets.length === 0 ? (
|
{ticketsLoading && tickets.length === 0 ? (
|
||||||
<AdminTableLoadingRow colSpan={10} />
|
<AdminTableLoadingRow colSpan={12} />
|
||||||
) : null}
|
) : null}
|
||||||
{tickets.map((row) => (
|
{tickets.map((row) => (
|
||||||
<TableRow key={row.ticket_no}>
|
<TableRow key={row.ticket_no}>
|
||||||
@@ -464,6 +469,9 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.order_no ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.order_no ?? "—"}</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.draw_no ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.draw_no ?? "—"}</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">
|
||||||
|
{providerLabel(row.provider_name, row.provider_code)}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-xs">{playCodeLabel(row.play_code)}</TableCell>
|
<TableCell className="text-xs">{playCodeLabel(row.play_code)}</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.original_number ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.original_number ?? "—"}</TableCell>
|
||||||
<TableCell className="text-center text-xs tabular-nums">
|
<TableCell className="text-center text-xs tabular-nums">
|
||||||
@@ -491,7 +499,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
{!ticketsLoading && tickets.length === 0 ? (
|
{!ticketsLoading && tickets.length === 0 ? (
|
||||||
<AdminTableNoResourceRow colSpan={10} className="text-muted-foreground" />
|
<AdminTableNoResourceRow colSpan={12} className="text-muted-foreground" />
|
||||||
) : null}
|
) : null}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const TICKET_STATUS_OPTIONS = [
|
|||||||
type TicketFilters = {
|
type TicketFilters = {
|
||||||
playerQuery: string;
|
playerQuery: string;
|
||||||
drawNo: string;
|
drawNo: string;
|
||||||
|
providerCode: string;
|
||||||
numberKeyword: string;
|
numberKeyword: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
@@ -70,6 +71,7 @@ type TicketFilters = {
|
|||||||
const emptyTicketFilters: TicketFilters = {
|
const emptyTicketFilters: TicketFilters = {
|
||||||
playerQuery: "",
|
playerQuery: "",
|
||||||
drawNo: "",
|
drawNo: "",
|
||||||
|
providerCode: "",
|
||||||
numberKeyword: "",
|
numberKeyword: "",
|
||||||
startDate: "",
|
startDate: "",
|
||||||
endDate: "",
|
endDate: "",
|
||||||
@@ -103,6 +105,10 @@ function ticketDetailPath(ticketNo: string): string {
|
|||||||
return `/admin/tickets/${encodeURIComponent(ticketNo)}`;
|
return `/admin/tickets/${encodeURIComponent(ticketNo)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function providerLabel(providerName?: string | null, providerCode?: string | null): string {
|
||||||
|
return providerName || providerCode || "—";
|
||||||
|
}
|
||||||
|
|
||||||
function TicketFilterField({
|
function TicketFilterField({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
@@ -164,6 +170,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
per_page: perPage,
|
per_page: perPage,
|
||||||
...query,
|
...query,
|
||||||
draw_no: applied.drawNo.trim() || undefined,
|
draw_no: applied.drawNo.trim() || undefined,
|
||||||
|
provider_code: applied.providerCode.trim().toUpperCase() || undefined,
|
||||||
status: applied.statuses.length > 0 ? applied.statuses : undefined,
|
status: applied.statuses.length > 0 ? applied.statuses : undefined,
|
||||||
number: applied.numberKeyword.trim() || undefined,
|
number: applied.numberKeyword.trim() || undefined,
|
||||||
start_date: applied.startDate || undefined,
|
start_date: applied.startDate || undefined,
|
||||||
@@ -188,6 +195,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
...draft,
|
...draft,
|
||||||
playerQuery: draft.playerQuery.trim(),
|
playerQuery: draft.playerQuery.trim(),
|
||||||
drawNo: draft.drawNo.trim(),
|
drawNo: draft.drawNo.trim(),
|
||||||
|
providerCode: draft.providerCode.trim().toUpperCase(),
|
||||||
numberKeyword: draft.numberKeyword.trim(),
|
numberKeyword: draft.numberKeyword.trim(),
|
||||||
});
|
});
|
||||||
setPage(1);
|
setPage(1);
|
||||||
@@ -263,6 +271,22 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TicketFilterField>
|
</TicketFilterField>
|
||||||
|
<TicketFilterField id="pt-provider" label={t("provider", { defaultValue: "开注商" })}>
|
||||||
|
<Input
|
||||||
|
id="pt-provider"
|
||||||
|
className="h-8 w-full font-mono uppercase"
|
||||||
|
placeholder="SG / MY / TH"
|
||||||
|
value={draft.providerCode}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDraft((current) => ({ ...current, providerCode: e.target.value.toUpperCase() }))
|
||||||
|
}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
runSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TicketFilterField>
|
||||||
<TicketFilterField id="pt-date-range" label={t("placedDateRange")}>
|
<TicketFilterField id="pt-date-range" label={t("placedDateRange")}>
|
||||||
<AdminDateRangeField
|
<AdminDateRangeField
|
||||||
id="pt-date-range"
|
id="pt-date-range"
|
||||||
@@ -322,7 +346,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{applied.playerQuery || applied.drawNo || applied.numberKeyword || applied.startDate || applied.endDate || applied.statuses.length > 0 ? (
|
{applied.playerQuery || applied.drawNo || applied.providerCode || applied.numberKeyword || applied.startDate || applied.endDate || applied.statuses.length > 0 ? (
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
{applied.playerQuery ? (
|
{applied.playerQuery ? (
|
||||||
<>
|
<>
|
||||||
@@ -337,6 +361,12 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
{t("drawNo")}:<span className="font-mono">{applied.drawNo}</span>
|
{t("drawNo")}:<span className="font-mono">{applied.drawNo}</span>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
{applied.providerCode ? (
|
||||||
|
<>
|
||||||
|
{" · "}
|
||||||
|
{t("provider", { defaultValue: "开注商" })}:<span className="font-mono">{applied.providerCode}</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -353,6 +383,7 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
<TableHead>{t("fundingMode", { ns: "players" })}</TableHead>
|
<TableHead>{t("fundingMode", { ns: "players" })}</TableHead>
|
||||||
<TableHead>{t("orderNo")}</TableHead>
|
<TableHead>{t("orderNo")}</TableHead>
|
||||||
<TableHead>{t("drawNo")}</TableHead>
|
<TableHead>{t("drawNo")}</TableHead>
|
||||||
|
<TableHead>{t("provider", { defaultValue: "开注商" })}</TableHead>
|
||||||
<TableHead>{t("playCode")}</TableHead>
|
<TableHead>{t("playCode")}</TableHead>
|
||||||
<TableHead>{t("number")}</TableHead>
|
<TableHead>{t("number")}</TableHead>
|
||||||
<TableHead className="text-center">{t("betAmount")}</TableHead>
|
<TableHead className="text-center">{t("betAmount")}</TableHead>
|
||||||
@@ -367,9 +398,9 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading && !data ? (
|
{loading && !data ? (
|
||||||
<AdminTableLoadingRow colSpan={18} />
|
<AdminTableLoadingRow colSpan={19} />
|
||||||
) : !data || data.items.length === 0 ? (
|
) : !data || data.items.length === 0 ? (
|
||||||
<AdminTableNoResourceRow colSpan={18} />
|
<AdminTableNoResourceRow colSpan={19} />
|
||||||
) : (
|
) : (
|
||||||
data.items.map((row) => {
|
data.items.map((row) => {
|
||||||
const winLabel = row.jackpot_win_amount > 0
|
const winLabel = row.jackpot_win_amount > 0
|
||||||
@@ -392,6 +423,9 @@ export function PlayerTicketsConsole(): React.ReactElement {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.order_no ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.order_no ?? "—"}</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.draw_no ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.draw_no ?? "—"}</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">
|
||||||
|
{providerLabel(row.provider_name, row.provider_code)}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-xs">{playCodeLabel(row.play_code)}</TableCell>
|
<TableCell className="text-xs">{playCodeLabel(row.play_code)}</TableCell>
|
||||||
<TableCell className="font-mono text-xs">{row.original_number ?? "—"}</TableCell>
|
<TableCell className="font-mono text-xs">{row.original_number ?? "—"}</TableCell>
|
||||||
<TableCell className="text-center tabular-nums text-xs">
|
<TableCell className="text-center tabular-nums text-xs">
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ function ticketStatusText(value: string, t: (key: string) => string): string {
|
|||||||
return translated === key ? value : translated;
|
return translated === key ? value : translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function providerLabel(providerName?: string | null, providerCode?: string | null): string {
|
||||||
|
return providerName || providerCode || "—";
|
||||||
|
}
|
||||||
|
|
||||||
export function TicketDetailConsole({ ticketNo }: { ticketNo: string }) {
|
export function TicketDetailConsole({ ticketNo }: { ticketNo: string }) {
|
||||||
const { t } = useTranslation(["tickets", "common"]);
|
const { t } = useTranslation(["tickets", "common"]);
|
||||||
const tRef = useTranslationRef(["tickets", "common"]);
|
const tRef = useTranslationRef(["tickets", "common"]);
|
||||||
@@ -126,6 +130,12 @@ export function TicketDetailConsole({ ticketNo }: { ticketNo: string }) {
|
|||||||
<span className="text-muted-foreground">{t("number")}:</span>
|
<span className="text-muted-foreground">{t("number")}:</span>
|
||||||
<span className="font-mono font-medium">{data.original_number ?? "—"}</span>
|
<span className="font-mono font-medium">{data.original_number ?? "—"}</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="text-muted-foreground">{t("provider", { defaultValue: "开注商" })}:</span>
|
||||||
|
<span className="font-mono font-medium">
|
||||||
|
{providerLabel(data.provider_name, data.provider_code)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-muted-foreground">{t("combinationCount")}:</span>
|
<span className="text-muted-foreground">{t("combinationCount")}:</span>
|
||||||
<span className="font-semibold tabular-nums">{data.combination_count}</span>
|
<span className="font-semibold tabular-nums">{data.combination_count}</span>
|
||||||
|
|||||||
27
src/types/api/admin-bet-provider.ts
Normal file
27
src/types/api/admin-bet-provider.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export type AdminBetProviderRow = {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
is_enabled: boolean;
|
||||||
|
sort_order: number;
|
||||||
|
is_default: boolean;
|
||||||
|
created_at: string | null;
|
||||||
|
updated_at: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminBetProviderListData = {
|
||||||
|
items: AdminBetProviderRow[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminBetProviderCreatePayload = {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
is_enabled?: boolean;
|
||||||
|
sort_order?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminBetProviderUpdatePayload = {
|
||||||
|
name?: string;
|
||||||
|
is_enabled?: boolean;
|
||||||
|
sort_order?: number;
|
||||||
|
};
|
||||||
@@ -3,6 +3,8 @@ export type AdminPlayerTicketItemRow = {
|
|||||||
order_no: string | null;
|
order_no: string | null;
|
||||||
draw_no: string | null;
|
draw_no: string | null;
|
||||||
currency_code: string | null;
|
currency_code: string | null;
|
||||||
|
provider_code?: string | null;
|
||||||
|
provider_name?: string | null;
|
||||||
play_code: string;
|
play_code: string;
|
||||||
original_number: string | null;
|
original_number: string | null;
|
||||||
/** @deprecated 历史字段,单位为 minor;优先使用 total_bet_amount_minor */
|
/** @deprecated 历史字段,单位为 minor;优先使用 total_bet_amount_minor */
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export type AdminTicketItemRow = {
|
|||||||
order_no: string | null;
|
order_no: string | null;
|
||||||
draw_no: string | null;
|
draw_no: string | null;
|
||||||
currency_code: string | null;
|
currency_code: string | null;
|
||||||
|
provider_code?: string | null;
|
||||||
|
provider_name?: string | null;
|
||||||
play_code: string;
|
play_code: string;
|
||||||
original_number: string | null;
|
original_number: string | null;
|
||||||
/** @deprecated 历史字段,单位为 minor;优先使用 total_bet_amount_minor */
|
/** @deprecated 历史字段,单位为 minor;优先使用 total_bet_amount_minor */
|
||||||
@@ -63,6 +65,8 @@ export type AdminTicketItemDetail = {
|
|||||||
agent_node_id: number | null;
|
agent_node_id: number | null;
|
||||||
agent_code: string | null;
|
agent_code: string | null;
|
||||||
agent_name: string | null;
|
agent_name: string | null;
|
||||||
|
provider_code?: string | null;
|
||||||
|
provider_name?: string | null;
|
||||||
play_code: string;
|
play_code: string;
|
||||||
dimension: number | null;
|
dimension: number | null;
|
||||||
digit_slot: number | null;
|
digit_slot: number | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user