feat(api): 按开注商支持获取赔率数据并完善相关功能
Some checks failed
lotteryfront CI / build (push) Has been cancelled

- 在getPlayEffective接口中增加provider_code参数支持
- 优化接口调用时对currency和provider_code的传递逻辑
- hall-bet-preview-dialog新增previewLineKey方法,解决列表渲染key冲突问题
- 修改投注和结果弹窗中开注商标题的国际化调用方式
- hall-betting-grid中增加选中开注商的默认逻辑,接口调用时传递provider_code
- 优化provider名称显示逻辑,完善开注商选择及摘要文本显示
- 订单详情页中显示开注商名称,提升显示准确性
- 补充i18n中开注商相关文案,支持多语言环境
- 更新PlayEffectiveOddsSlice和PlayEffectivePayload类型,加入provider_code字段支持
This commit is contained in:
2026-07-09 10:51:45 +08:00
parent add3d1fc05
commit 5cbabb5d0f
10 changed files with 70 additions and 23 deletions

View File

@@ -11,7 +11,7 @@
LOTTERY_API_UPSTREAM=http://127.0.0.1:8000 LOTTERY_API_UPSTREAM=http://127.0.0.1:8000
# 线上 # 线上
# LOTTERY_API_UPSTREAM=http://127.0.0.1:8000 # LOTTERY_API_UPSTREAM=http://127.0.0.1:8000
# LOTTERY_API_UPSTREAM=https://lotterylaravel.tanumo.com # LOTTERY_API_UPSTREAM=https://lottery-api.cjdhr.top
# Next 开发:局域网 IP 访问(逗号分隔 host无协议 # Next 开发:局域网 IP 访问(逗号分隔 host无协议
# ALLOWED_DEV_ORIGINS=192.168.0.101 # ALLOWED_DEV_ORIGINS=192.168.0.101

View File

@@ -4,6 +4,8 @@ import type { PlayEffectivePayload } from "@/types/api/play-effective";
export type GetPlayEffectiveParams = { export type GetPlayEffectiveParams = {
/** 不传则后端取首个可下注币种(通常为 NPR */ /** 不传则后端取首个可下注币种(通常为 NPR */
currency?: string; currency?: string;
/** 按开注商返回覆盖后的赔率;未传则返回通用赔率 */
provider_code?: string;
}; };
/** /**
@@ -17,8 +19,13 @@ export function getPlayEffective(
`/play/effective`, `/play/effective`,
{ {
params: params:
params?.currency !== undefined && params.currency !== "" params
? { currency: params.currency } ? {
...(params.currency !== undefined && params.currency !== "" ? { currency: params.currency } : {}),
...(params.provider_code !== undefined && params.provider_code !== ""
? { provider_code: params.provider_code }
: {}),
}
: undefined, : undefined,
}, },
); );

View File

@@ -23,7 +23,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { formatMinorAsCurrency } from "@/lib/money"; import { formatMinorAsCurrency } from "@/lib/money";
import { playLabel } from "@/lib/play-labels"; import { playLabel } from "@/lib/play-labels";
import type { TicketPreviewData, TicketPreviewWarning } from "@/types/api/ticket"; import type { TicketPreviewData, TicketPreviewLine, TicketPreviewWarning } from "@/types/api/ticket";
type HallBetPreviewDialogProps = { type HallBetPreviewDialogProps = {
open: boolean; open: boolean;
@@ -65,6 +65,16 @@ function providerLabel(providerName?: string | null, providerCode?: string | nul
return providerName || providerCode || "-"; return providerName || providerCode || "-";
} }
function previewLineKey(line: TicketPreviewLine, index: number): string {
return [
line.client_line_no,
line.provider_code ?? "provider",
line.play_code,
line.number,
index,
].join(":");
}
function SubmittingPanel() { function SubmittingPanel() {
const { t } = useTranslation("player"); const { t } = useTranslation("player");
@@ -221,7 +231,7 @@ export function HallBetPreviewDialog({
{t("orders.play")} {t("orders.play")}
</th> </th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black"> <th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("hall.providers.title", { defaultValue: "开注商" })} {t("hall.providers.title")}
</th> </th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black"> <th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("hall.preview.amount")} {t("hall.preview.amount")}
@@ -238,8 +248,8 @@ export function HallBetPreviewDialog({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{lines.map((ln) => ( {lines.map((ln, index) => (
<tr key={ln.client_line_no} className="border-t border-[#e8eef7] bg-white"> <tr key={previewLineKey(ln, index)} className="border-t border-[#e8eef7] bg-white">
<td className="border-r border-[#e8eef7] px-2 py-3 text-center"> <td className="border-r border-[#e8eef7] px-2 py-3 text-center">
<span className="inline-flex size-6 items-center justify-center rounded-full border border-[#dfe8f6] font-black text-[#304f86]"> <span className="inline-flex size-6 items-center justify-center rounded-full border border-[#dfe8f6] font-black text-[#304f86]">
{ln.client_line_no} {ln.client_line_no}

View File

@@ -219,7 +219,7 @@ export function HallBetResultDialog({
{t("orders.play")} {t("orders.play")}
</th> </th>
<th className="border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black"> <th className="border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">
{t("hall.providers.title", { defaultValue: "开注商" })} {t("hall.providers.title")}
</th> </th>
<th className="px-2 py-2.5 text-center font-black"> <th className="px-2 py-2.5 text-center font-black">
{t("hall.result.actualDeduct")} {t("hall.result.actualDeduct")}

View File

@@ -482,12 +482,19 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
}, []); }, []);
const catalogSeqRef = useRef(0); const catalogSeqRef = useRef(0);
const walletSeqRef = useRef(0); const walletSeqRef = useRef(0);
const selectedCatalogProviderCode =
selectedProviderCodes[0] ??
betProviders.find((provider) => provider.is_default)?.code ??
betProviders[0]?.code;
const loadCatalog = useCallback(async () => { const loadCatalog = useCallback(async () => {
const seq = ++catalogSeqRef.current; const seq = ++catalogSeqRef.current;
setCatalogState({ kind: "loading" }); setCatalogState({ kind: "loading" });
try { try {
const data = await getPlayEffective({ currency: currencyParam }); const data = await getPlayEffective({
currency: currencyParam,
provider_code: selectedCatalogProviderCode,
});
if (seq !== catalogSeqRef.current) return; if (seq !== catalogSeqRef.current) return;
setCatalogState({ kind: "ok", data }); setCatalogState({ kind: "ok", data });
} catch (e) { } catch (e) {
@@ -495,7 +502,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const msg = e instanceof LotteryApiBizError ? e.message : t("hall.loadingError"); const msg = e instanceof LotteryApiBizError ? e.message : t("hall.loadingError");
setCatalogState({ kind: "error", message: msg }); setCatalogState({ kind: "error", message: msg });
} }
}, [currencyParam, t]); }, [currencyParam, selectedCatalogProviderCode, t]);
const refreshWallet = useCallback(async () => { const refreshWallet = useCallback(async () => {
const seq = ++walletSeqRef.current; const seq = ++walletSeqRef.current;
@@ -1257,14 +1264,13 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const providerSummaryText = const providerSummaryText =
selectedProviderCodes.length > 0 selectedProviderCodes.length > 0
? t("hall.providers.summary", { ? t("hall.providers.summary", {
defaultValue: "已选 {{count}} 家,金额按每家公司计算",
count: providerSummaryCount, count: providerSummaryCount,
}) })
: t("hall.providers.platformDefaultSummary", { : implicitDefaultProvider
defaultValue: implicitDefaultProvider ? t("hall.providers.platformDefaultSummary", {
? `未选择时走平台默认(${implicitDefaultProvider.name}` provider: implicitDefaultProvider.name,
: "未选择时走平台默认", })
}); : t("hall.providers.platformDefaultSummaryNoProvider");
return ( return (
<> <>
@@ -1511,7 +1517,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-1.5"> <div className="flex flex-wrap items-center gap-1.5">
<span className="mr-1 text-[11px] font-black text-[#304f86]"> <span className="mr-1 text-[11px] font-black text-[#304f86]">
{t("hall.providers.title", { defaultValue: "开注商" })} {t("hall.providers.title")}
</span> </span>
{!isMobile ? betProviders.map((provider) => { {!isMobile ? betProviders.map((provider) => {
const checked = selectedProviderCodes.includes(provider.code); const checked = selectedProviderCodes.includes(provider.code);
@@ -1544,11 +1550,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
<span className="text-xs font-semibold text-[#58709d]"> <span className="text-xs font-semibold text-[#58709d]">
{selectedProviderNames.length > 0 {selectedProviderNames.length > 0
? selectedProviderNames.join(" / ") ? selectedProviderNames.join(" / ")
: t("hall.providers.platformDefault", { : implicitDefaultProvider
defaultValue: implicitDefaultProvider ? t("hall.providers.platformDefault", { provider: implicitDefaultProvider.name })
? `平台默认(${implicitDefaultProvider.name}` : t("hall.providers.platformDefaultNoProvider")}
: "平台默认",
})}
</span> </span>
)} )}
</div> </div>

View File

@@ -366,7 +366,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
</div> </div>
<div className="flex items-baseline justify-between gap-3"> <div className="flex items-baseline justify-between gap-3">
<span className="shrink-0 text-slate-500"> <span className="shrink-0 text-slate-500">
{t("hall.providers.title", { defaultValue: "开注商" })} {t("hall.providers.title")}
</span> </span>
<span className="text-right font-black text-[#0b56b7]"> <span className="text-right font-black text-[#0b56b7]">
{providerLabel(data.provider_name, data.provider_code)} {providerLabel(data.provider_name, data.provider_code)}

View File

@@ -169,6 +169,14 @@
"updated": "Play configuration updated. The betting table has been refreshed.", "updated": "Play configuration updated. The betting table has been refreshed.",
"oddsUpdated": "Odds updated. Please preview the ticket again." "oddsUpdated": "Odds updated. Please preview the ticket again."
}, },
"providers": {
"title": "Bet provider",
"summary": "{{count}} selected. Amount is calculated per provider.",
"platformDefault": "Platform default ({{provider}})",
"platformDefaultNoProvider": "Platform default",
"platformDefaultSummary": "If none is selected, platform default is used ({{provider}}).",
"platformDefaultSummaryNoProvider": "If none is selected, platform default is used."
},
"jackpotBurst": { "jackpotBurst": {
"title": "Jackpot Burst", "title": "Jackpot Burst",
"subtitle": "Issue {{drawNo}} triggered a pool payout", "subtitle": "Issue {{drawNo}} triggered a pool payout",

View File

@@ -169,6 +169,14 @@
"updated": "प्ले कन्फिगरेसन अपडेट भयो। बेटिङ तालिका रिफ्रेस गरियो।", "updated": "प्ले कन्फिगरेसन अपडेट भयो। बेटिङ तालिका रिफ्रेस गरियो।",
"oddsUpdated": "Odds अपडेट भयो। कृपया टिकट फेरि पूर्वावलोकन गर्नुहोस्।" "oddsUpdated": "Odds अपडेट भयो। कृपया टिकट फेरि पूर्वावलोकन गर्नुहोस्।"
}, },
"providers": {
"title": "Bet provider",
"summary": "{{count}} चयन गरियो। रकम प्रत्येक provider अनुसार गणना हुन्छ।",
"platformDefault": "Platform default ({{provider}})",
"platformDefaultNoProvider": "Platform default",
"platformDefaultSummary": "कुनै चयन नभए platform default प्रयोग हुन्छ ({{provider}})।",
"platformDefaultSummaryNoProvider": "कुनै चयन नभए platform default प्रयोग हुन्छ।"
},
"jackpotBurst": { "jackpotBurst": {
"title": "ज्याकपट बर्स्ट", "title": "ज्याकपट बर्स्ट",
"subtitle": "इश्यू {{drawNo}} मा पूल payout ट्रिगर भयो", "subtitle": "इश्यू {{drawNo}} मा पूल payout ट्रिगर भयो",

View File

@@ -168,6 +168,14 @@
"updated": "玩法配置已更新,已刷新下注表格。", "updated": "玩法配置已更新,已刷新下注表格。",
"oddsUpdated": "赔率已更新,请重新预览注单。" "oddsUpdated": "赔率已更新,请重新预览注单。"
}, },
"providers": {
"title": "开注商",
"summary": "已选 {{count}} 家,金额按每家公司计算",
"platformDefault": "平台默认({{provider}}",
"platformDefaultNoProvider": "平台默认",
"platformDefaultSummary": "未选择时走平台默认({{provider}}",
"platformDefaultSummaryNoProvider": "未选择时走平台默认"
},
"jackpotBurst": { "jackpotBurst": {
"title": "Jackpot 爆池", "title": "Jackpot 爆池",
"subtitle": "期号 {{drawNo}} 触发奖池派发", "subtitle": "期号 {{drawNo}} 触发奖池派发",

View File

@@ -18,6 +18,7 @@ export type PlayEffectiveConfigSlice = {
}; };
export type PlayEffectiveOddsSlice = { export type PlayEffectiveOddsSlice = {
provider_code?: string;
prize_scope: string; prize_scope: string;
odds_value: number; odds_value: number;
rebate_rate: string; rebate_rate: string;
@@ -50,6 +51,7 @@ export type PlayEffectiveRiskCapRow = {
export type PlayEffectivePayload = { export type PlayEffectivePayload = {
currency_code: string; currency_code: string;
provider_code?: string;
effective_versions: { effective_versions: {
play_config: PlayEffectiveVersionHead; play_config: PlayEffectiveVersionHead;
odds: PlayEffectiveVersionHead; odds: PlayEffectiveVersionHead;