refactor(hall): 优化投注格子行数与号码输入逻辑
Some checks failed
lotteryfront CI / build (push) Has been cancelled
Some checks failed
lotteryfront CI / build (push) Has been cancelled
- 引入默认草稿行数常量及批量生成草稿行函数
- 根据玩法类别调整号码最大输入长度和清洗规则
- 更新号码输入组件以适应动态最大长度和格式规范
- 修正相关回调依赖,确保玩法类别变更时生效
- 改善行状态、占位符和样式传递,增强代码可读性
feat(results): 添加多提供商开奖结果切换功能
- 新增状态管理并展示多提供商按钮切换激活项
- 根据选中的提供商动态展示对应开奖结果数据
- 细化UI显示,支持多提供商名称并列渲染
feat(results): 在结果列表中显示多提供商名称
- 当存在多个提供商结果时展示其名称或代码
- 改善界面信息丰富度,方便用户辨识来源
feat(wallet): 支持账期还款释额类型的流水显示
- 调整账期流水类型文案,精准反映释额含义
- 引入账期时区参数以支持多时区时间格式化
- 自定义信用盘账期释额流水的颜色和文案显示
- 在流水明细和列表中根据时区格式化展示时间
feat(wallet): 支持钱包账期账单区间时区格式化
- 允许传入账期时区参数格式化账单区间时间
- 在账期账单区间显示组件中显示对应时区格式
- 根据玩家配置自动获取并传递账期时区信息
- 条件渲染中信用盘区块支持账期时区参数传递
chore(i18n): 更新中文钱包模块多处文案
- 新增“释额 {{amount}}”等账期释额相关文案
- 修改“账期收款”为“账期还款释额”以符合业务描述
refactor(types): 扩展开奖结果支持多提供商结构
- 新增DrawResultProviderRow类型定义以描述提供商结果
- 在DrawResultListItem中增加provider_results字段支持多结果
- 在玩家信息类型中新增settlement_timezone字段表示账期时区
This commit is contained in:
@@ -150,6 +150,11 @@ const D4_PLAY_ORDER = [
|
|||||||
"digit_small",
|
"digit_small",
|
||||||
] as const;
|
] as const;
|
||||||
const MOBILE_QUICK_AMOUNT_PRESETS = ["10", "50", "100"] as const;
|
const MOBILE_QUICK_AMOUNT_PRESETS = ["10", "50", "100"] as const;
|
||||||
|
const DEFAULT_DRAFT_ROW_COUNT = 20;
|
||||||
|
|
||||||
|
function newDraftRows(count = DEFAULT_DRAFT_ROW_COUNT): DraftRow[] {
|
||||||
|
return Array.from({ length: count }, newDraftRow);
|
||||||
|
}
|
||||||
|
|
||||||
function newDraftRow(): DraftRow {
|
function newDraftRow(): DraftRow {
|
||||||
const id =
|
const id =
|
||||||
@@ -215,8 +220,18 @@ function playColumnsForCategory(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeNumber(raw: string): string {
|
function numberMaxCharsForCategory(category: PlayHallCategory): number {
|
||||||
return raw.replace(/[^0-9Rr]/g, "").toUpperCase().slice(0, 4);
|
return category === "D2" ? 2 : category === "D3" ? 3 : 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeNumber(raw: string, category: PlayHallCategory): string {
|
||||||
|
const normalized =
|
||||||
|
category === "D4"
|
||||||
|
? raw.replace(/[^0-9Rr]/g, "").toUpperCase()
|
||||||
|
: raw.replace(/\D/g, "");
|
||||||
|
const maxChars = numberMaxCharsForCategory(category);
|
||||||
|
|
||||||
|
return category === "D4" ? normalized.slice(0, maxChars) : normalized.slice(-maxChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeAmount(raw: string): string {
|
function sanitizeAmount(raw: string): string {
|
||||||
@@ -439,7 +454,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|||||||
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
|
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const [rows, setRows] = useState<DraftRow[]>(() => Array.from({ length: 20 }, newDraftRow));
|
const [rows, setRows] = useState<DraftRow[]>(() => newDraftRows());
|
||||||
const [activeRowId, setActiveRowId] = useState<string | null>(null);
|
const [activeRowId, setActiveRowId] = useState<string | null>(null);
|
||||||
const [catalogState, setCatalogState] = useState<
|
const [catalogState, setCatalogState] = useState<
|
||||||
| { kind: "loading" }
|
| { kind: "loading" }
|
||||||
@@ -642,15 +657,16 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|||||||
const sealedBetUi = Boolean(display && isHallSealedCountdownUi(display.status));
|
const sealedBetUi = Boolean(display && isHallSealedCountdownUi(display.status));
|
||||||
const numberPlaceholder =
|
const numberPlaceholder =
|
||||||
activeCategory === "D2" ? "00" : activeCategory === "D3" ? "000" : "0000";
|
activeCategory === "D2" ? "00" : activeCategory === "D3" ? "000" : "0000";
|
||||||
|
const numberMaxChars = numberMaxCharsForCategory(activeCategory);
|
||||||
|
|
||||||
const updateRowNumber = useCallback((id: string, value: string) => {
|
const updateRowNumber = useCallback((id: string, value: string) => {
|
||||||
setRows((current) =>
|
setRows((current) =>
|
||||||
current.map((row) =>
|
current.map((row) =>
|
||||||
row.id === id ? { ...row, number: sanitizeNumber(value) } : row,
|
row.id === id ? { ...row, number: sanitizeNumber(value, activeCategory) } : row,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setActiveRowId(id);
|
setActiveRowId(id);
|
||||||
}, []);
|
}, [activeCategory]);
|
||||||
|
|
||||||
const updateAmount = useCallback((rowId: string, playCode: string, value: string) => {
|
const updateAmount = useCallback((rowId: string, playCode: string, value: string) => {
|
||||||
setRows((current) =>
|
setRows((current) =>
|
||||||
@@ -1151,7 +1167,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|||||||
setPreviewData(null);
|
setPreviewData(null);
|
||||||
setResultData(data);
|
setResultData(data);
|
||||||
setResultOpen(true);
|
setResultOpen(true);
|
||||||
setRows([newDraftRow()]);
|
setRows(newDraftRows());
|
||||||
setActiveRowId(null);
|
setActiveRowId(null);
|
||||||
triggerWalletPollingAfterBet();
|
triggerWalletPollingAfterBet();
|
||||||
void refreshWallet();
|
void refreshWallet();
|
||||||
@@ -1752,6 +1768,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|||||||
rowActive={activeRowId === row.id}
|
rowActive={activeRowId === row.id}
|
||||||
tableDisabled={tableDisabled}
|
tableDisabled={tableDisabled}
|
||||||
numberPlaceholder={numberPlaceholder}
|
numberPlaceholder={numberPlaceholder}
|
||||||
|
activeCategory={activeCategory}
|
||||||
|
numberMaxChars={numberMaxChars}
|
||||||
playColumns={playColumns}
|
playColumns={playColumns}
|
||||||
alertRows={alertRows}
|
alertRows={alertRows}
|
||||||
liveSoldOutNumbers={liveSoldOutNumbers}
|
liveSoldOutNumbers={liveSoldOutNumbers}
|
||||||
@@ -1905,6 +1923,8 @@ const DraftRowItem = memo(function DraftRowItem({
|
|||||||
rowActive,
|
rowActive,
|
||||||
tableDisabled,
|
tableDisabled,
|
||||||
numberPlaceholder,
|
numberPlaceholder,
|
||||||
|
activeCategory,
|
||||||
|
numberMaxChars,
|
||||||
playColumns,
|
playColumns,
|
||||||
alertRows,
|
alertRows,
|
||||||
liveSoldOutNumbers,
|
liveSoldOutNumbers,
|
||||||
@@ -1919,16 +1939,18 @@ const DraftRowItem = memo(function DraftRowItem({
|
|||||||
indexColClass,
|
indexColClass,
|
||||||
numberColClass,
|
numberColClass,
|
||||||
stickyNumberLeftClass,
|
stickyNumberLeftClass,
|
||||||
}: {
|
}: {
|
||||||
row: DraftRow;
|
row: DraftRow;
|
||||||
index: number;
|
index: number;
|
||||||
rowActive: boolean;
|
rowActive: boolean;
|
||||||
tableDisabled: boolean;
|
tableDisabled: boolean;
|
||||||
numberPlaceholder: string;
|
numberPlaceholder: string;
|
||||||
playColumns: PlayColumn[];
|
activeCategory: PlayHallCategory;
|
||||||
alertRows: DrawCurrentRiskPoolAlert[];
|
numberMaxChars: number;
|
||||||
liveSoldOutNumbers: Set<string>;
|
playColumns: PlayColumn[];
|
||||||
liveWarningNumbers: Set<string>;
|
alertRows: DrawCurrentRiskPoolAlert[];
|
||||||
|
liveSoldOutNumbers: Set<string>;
|
||||||
|
liveWarningNumbers: Set<string>;
|
||||||
updateRowNumber: (id: string, value: string) => void;
|
updateRowNumber: (id: string, value: string) => void;
|
||||||
updateAmount: (rowId: string, playCode: string, value: string) => void;
|
updateAmount: (rowId: string, playCode: string, value: string) => void;
|
||||||
setActiveRowId: (id: string) => void;
|
setActiveRowId: (id: string) => void;
|
||||||
@@ -1937,10 +1959,12 @@ const DraftRowItem = memo(function DraftRowItem({
|
|||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
removable: boolean;
|
removable: boolean;
|
||||||
indexColClass: string;
|
indexColClass: string;
|
||||||
numberColClass: string;
|
numberColClass: string;
|
||||||
stickyNumberLeftClass: string;
|
stickyNumberLeftClass: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
const displayNumber = sanitizeNumber(row.number, activeCategory);
|
||||||
|
|
||||||
|
return (
|
||||||
<tr
|
<tr
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b border-[#f0f3f8] last:border-b-0",
|
"border-b border-[#f0f3f8] last:border-b-0",
|
||||||
@@ -1964,13 +1988,14 @@ const DraftRowItem = memo(function DraftRowItem({
|
|||||||
rowActive ? "bg-[#f5f9ff]" : "bg-white",
|
rowActive ? "bg-[#f5f9ff]" : "bg-white",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
value={row.number}
|
value={displayNumber}
|
||||||
disabled={tableDisabled}
|
disabled={tableDisabled}
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
pattern="[0-9Rr]*"
|
pattern="[0-9Rr]*"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder={numberPlaceholder}
|
placeholder={numberPlaceholder}
|
||||||
|
maxLength={numberMaxChars}
|
||||||
onFocus={() => setActiveRowId(row.id)}
|
onFocus={() => setActiveRowId(row.id)}
|
||||||
onClick={() => setActiveRowId(row.id)}
|
onClick={() => setActiveRowId(row.id)}
|
||||||
onChange={(event) => updateRowNumber(row.id, event.target.value)}
|
onChange={(event) => updateRowNumber(row.id, event.target.value)}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
|
|||||||
const [data, setData] = useState<DrawResultDetailPayload | null>(null);
|
const [data, setData] = useState<DrawResultDetailPayload | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeProviderCode, setActiveProviderCode] = useState<string>("");
|
||||||
const [highlightSet, setHighlightSet] = useState<ReadonlySet<string> | null>(null);
|
const [highlightSet, setHighlightSet] = useState<ReadonlySet<string> | null>(null);
|
||||||
const [myTotals, setMyTotals] = useState<{
|
const [myTotals, setMyTotals] = useState<{
|
||||||
win: number;
|
win: number;
|
||||||
@@ -60,6 +61,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
|
|||||||
try {
|
try {
|
||||||
const row = await getDrawResultByNo(drawNo, { currency: activeCurrency });
|
const row = await getDrawResultByNo(drawNo, { currency: activeCurrency });
|
||||||
setData(row);
|
setData(row);
|
||||||
|
setActiveProviderCode(String(row.provider_code ?? row.provider_results?.[0]?.provider_code ?? ""));
|
||||||
} catch {
|
} catch {
|
||||||
setData(null);
|
setData(null);
|
||||||
setError(t("results.unavailable"));
|
setError(t("results.unavailable"));
|
||||||
@@ -144,6 +146,12 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currency = data.jackpot?.currency_code ?? activeCurrency;
|
const currency = data.jackpot?.currency_code ?? activeCurrency;
|
||||||
|
const providerResults = data.provider_results ?? [];
|
||||||
|
const activeProviderResult =
|
||||||
|
providerResults.find((row) => String(row.provider_code ?? "") === activeProviderCode)
|
||||||
|
?? providerResults[0]
|
||||||
|
?? null;
|
||||||
|
const activeResults = activeProviderResult?.results ?? data.results;
|
||||||
const showMyPayout =
|
const showMyPayout =
|
||||||
myTotals &&
|
myTotals &&
|
||||||
myTotals.hasBets &&
|
myTotals.hasBets &&
|
||||||
@@ -222,6 +230,29 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
|
|||||||
{t("results.detail")}
|
{t("results.detail")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{providerResults.length > 1 ? (
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{providerResults.map((row) => {
|
||||||
|
const code = String(row.provider_code ?? "");
|
||||||
|
const active = code === String(activeProviderResult?.provider_code ?? "");
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={code || row.provider_name || row.result_version}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveProviderCode(code)}
|
||||||
|
className={cn(
|
||||||
|
"rounded-full border px-3 py-1 text-xs font-bold transition-colors",
|
||||||
|
active
|
||||||
|
? "border-[#0b56b7] bg-[#0b56b7] text-white"
|
||||||
|
: "border-[#dce7f7] bg-white text-[#0b56b7] hover:bg-[#f1f6ff]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{row.provider_name || row.provider_code || "Provider"}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="mt-3 grid grid-cols-3 gap-2 text-center">
|
<div className="mt-3 grid grid-cols-3 gap-2 text-center">
|
||||||
{RESULTS_TOP_PRIZE_KEYS.map((tier) => (
|
{RESULTS_TOP_PRIZE_KEYS.map((tier) => (
|
||||||
<div
|
<div
|
||||||
@@ -232,7 +263,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
|
|||||||
{t(resultsPrizeLabelKey(tier))}
|
{t(resultsPrizeLabelKey(tier))}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 font-mono text-lg font-black tabular-nums text-[#e5002c]">
|
<p className="mt-1 font-mono text-lg font-black tabular-nums text-[#e5002c]">
|
||||||
{data.results[tier]}
|
{activeResults[tier]}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -240,7 +271,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TwentyThreeResultsGrid
|
<TwentyThreeResultsGrid
|
||||||
numbers={data.results}
|
numbers={activeResults}
|
||||||
highlighted4d={highlightSet ?? undefined}
|
highlighted4d={highlightSet ?? undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,11 @@ export function DrawResultsListScreen() {
|
|||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
{(featured.provider_results?.length ?? 0) > 1 ? (
|
||||||
|
<p className="mt-1 text-[11px] font-semibold text-[#0b56b7]">
|
||||||
|
{featured.provider_results?.map((row) => row.provider_name || row.provider_code).filter(Boolean).join(" / ")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={`/results/${encodeURIComponent(featured.draw_no)}`}
|
href={`/results/${encodeURIComponent(featured.draw_no)}`}
|
||||||
@@ -291,6 +296,11 @@ export function DrawResultsListScreen() {
|
|||||||
<p className="mt-1 text-[11px] text-slate-500">
|
<p className="mt-1 text-[11px] text-slate-500">
|
||||||
{formatPlayerInstant(row.draw_time_iso ?? row.draw_time ?? null)}
|
{formatPlayerInstant(row.draw_time_iso ?? row.draw_time ?? null)}
|
||||||
</p>
|
</p>
|
||||||
|
{(row.provider_results?.length ?? 0) > 1 ? (
|
||||||
|
<p className="mt-1 text-[11px] font-semibold text-[#0b56b7]">
|
||||||
|
{row.provider_results?.map((item) => item.provider_name || item.provider_code).filter(Boolean).join(" / ")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 rounded-full bg-[#f2f6ff] px-2.5 py-1 text-xs font-bold text-[#0b56b7]">
|
<span className="shrink-0 rounded-full bg-[#f2f6ff] px-2.5 py-1 text-xs font-bold text-[#0b56b7]">
|
||||||
{t("results.detail")}
|
{t("results.detail")}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { formatPlayerInstant } from "@/lib/player-datetime";
|
import { formatLotteryInstantInTimeZone, formatPlayerInstant } from "@/lib/player-datetime";
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
import type { WalletLogItem, WalletLogsData } from "@/types/api/wallet-logs";
|
import type { WalletLogItem, WalletLogsData } from "@/types/api/wallet-logs";
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ const CREDIT_BIZ_LABEL_FALLBACKS: Record<string, string> = {
|
|||||||
game_settlement_loss: "亏损入账",
|
game_settlement_loss: "亏损入账",
|
||||||
game_settlement_win: "中奖释额",
|
game_settlement_win: "中奖释额",
|
||||||
game_settlement_reversal: "结算冲正",
|
game_settlement_reversal: "结算冲正",
|
||||||
settlement_confirm: "账期收款",
|
settlement_confirm: "账期还款释额",
|
||||||
settlement_payout: "账期付款",
|
settlement_payout: "账期付款",
|
||||||
rebate_accrued: "账期回水",
|
rebate_accrued: "账期回水",
|
||||||
rebate_in_bill: "回水已入账单",
|
rebate_in_bill: "回水已入账单",
|
||||||
@@ -108,6 +108,7 @@ type WalletLogsBlockProps = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
/** 信用盘:专用筛选项与列表文案 */
|
/** 信用盘:专用筛选项与列表文案 */
|
||||||
creditMode?: boolean;
|
creditMode?: boolean;
|
||||||
|
settlementTimeZone?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 类型筛选 + 列表(钱包盘待对账见钱包页横幅) */
|
/** 类型筛选 + 列表(钱包盘待对账见钱包页横幅) */
|
||||||
@@ -123,6 +124,7 @@ export function WalletLogsBlock({
|
|||||||
currency,
|
currency,
|
||||||
title,
|
title,
|
||||||
creditMode = false,
|
creditMode = false,
|
||||||
|
settlementTimeZone,
|
||||||
}: WalletLogsBlockProps) {
|
}: WalletLogsBlockProps) {
|
||||||
const { t } = useTranslation("player");
|
const { t } = useTranslation("player");
|
||||||
const resolvedTitle =
|
const resolvedTitle =
|
||||||
@@ -189,6 +191,7 @@ export function WalletLogsBlock({
|
|||||||
item={row}
|
item={row}
|
||||||
currency={currency}
|
currency={currency}
|
||||||
creditMode={creditMode}
|
creditMode={creditMode}
|
||||||
|
settlementTimeZone={settlementTimeZone}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -226,14 +229,27 @@ export function LogRow({
|
|||||||
item,
|
item,
|
||||||
currency,
|
currency,
|
||||||
creditMode = false,
|
creditMode = false,
|
||||||
|
settlementTimeZone,
|
||||||
}: {
|
}: {
|
||||||
item: WalletLogItem;
|
item: WalletLogItem;
|
||||||
currency: string;
|
currency: string;
|
||||||
creditMode?: boolean;
|
creditMode?: boolean;
|
||||||
|
settlementTimeZone?: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("player");
|
const { t } = useTranslation("player");
|
||||||
const ccy = item.currency_code || currency;
|
const ccy = item.currency_code || currency;
|
||||||
const isIn = item.direction === "in";
|
const isIn = item.direction === "in";
|
||||||
|
const isCreditSettlementConfirm = creditMode && item.biz_type === "settlement_confirm";
|
||||||
|
const amountTone = isCreditSettlementConfirm
|
||||||
|
? "text-[#32518d]"
|
||||||
|
: isIn
|
||||||
|
? "text-emerald-600"
|
||||||
|
: "text-[#e5002c]";
|
||||||
|
const dotTone = isCreditSettlementConfirm
|
||||||
|
? "bg-[#2f6fca]"
|
||||||
|
: isIn
|
||||||
|
? "bg-emerald-500"
|
||||||
|
: "bg-[#e5002c]";
|
||||||
const statusTone = item.status === "posted"
|
const statusTone = item.status === "posted"
|
||||||
? "border-emerald-200 bg-emerald-50 text-emerald-700"
|
? "border-emerald-200 bg-emerald-50 text-emerald-700"
|
||||||
: item.status === "pending_reconcile"
|
: item.status === "pending_reconcile"
|
||||||
@@ -247,13 +263,15 @@ export function LogRow({
|
|||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={isIn ? "size-2.5 rounded-full bg-emerald-500" : "size-2.5 rounded-full bg-[#e5002c]"} aria-hidden />
|
<span className={`size-2.5 rounded-full ${dotTone}`} aria-hidden />
|
||||||
<p className="truncate text-base font-black leading-tight text-[#101a33]">
|
<p className="truncate text-base font-black leading-tight text-[#101a33]">
|
||||||
{logTypeLabel(item.type, t, creditMode, item.biz_type)}
|
{logTypeLabel(item.type, t, creditMode, item.biz_type)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1.5 text-xs font-medium text-slate-500">
|
<p className="mt-1.5 text-xs font-medium text-slate-500">
|
||||||
{formatPlayerInstant(item.created_at)}
|
{creditMode && settlementTimeZone
|
||||||
|
? formatLotteryInstantInTimeZone(item.created_at, settlementTimeZone)
|
||||||
|
: formatPlayerInstant(item.created_at)}
|
||||||
</p>
|
</p>
|
||||||
{item.ref_id ? (
|
{item.ref_id ? (
|
||||||
<p className="mt-1 truncate font-mono text-[11px] text-slate-400">
|
<p className="mt-1 truncate font-mono text-[11px] text-slate-400">
|
||||||
@@ -263,9 +281,13 @@ export function LogRow({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 flex-col items-end gap-2 text-right">
|
<div className="flex shrink-0 flex-col items-end gap-2 text-right">
|
||||||
<p className={isIn ? "text-lg font-black tabular-nums text-emerald-600" : "text-lg font-black tabular-nums text-[#e5002c]"}>
|
<p className={`text-lg font-black tabular-nums ${amountTone}`}>
|
||||||
{isIn ? "+" : "−"}
|
{isCreditSettlementConfirm
|
||||||
{formatMinorAsCurrency(item.amount_abs, ccy)}
|
? t("wallet.creditReleasedAmount", {
|
||||||
|
defaultValue: "释额 {{amount}}",
|
||||||
|
amount: formatMinorAsCurrency(item.amount_abs, ccy),
|
||||||
|
})
|
||||||
|
: `${isIn ? "+" : "−"}${formatMinorAsCurrency(item.amount_abs, ccy)}`}
|
||||||
</p>
|
</p>
|
||||||
<span className={`inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-black ${statusTone}`}>
|
<span className={`inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-black ${statusTone}`}>
|
||||||
{txnStatusLabel(item.status, t)}
|
{txnStatusLabel(item.status, t)}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
|
|||||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||||
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
import { formatPlayerInstant } from "@/lib/player-datetime";
|
import { formatLotteryInstantInTimeZone, formatPlayerInstant } from "@/lib/player-datetime";
|
||||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||||
import { formatWalletClientError } from "@/lib/wallet-api-error";
|
import { formatWalletClientError } from "@/lib/wallet-api-error";
|
||||||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
||||||
@@ -69,18 +69,31 @@ function settlementStatusLabel(
|
|||||||
return t(`wallet.settlementStatus.${status}`, { defaultValue: fallback[status] ?? status });
|
return t(`wallet.settlementStatus.${status}`, { defaultValue: fallback[status] ?? status });
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPeriodRange(item: WalletSettlementBillItem): string {
|
function formatPeriodRange(
|
||||||
const start = item.period_start ? formatPlayerInstant(item.period_start).slice(0, 10) : "—";
|
item: WalletSettlementBillItem,
|
||||||
const end = item.period_end ? formatPlayerInstant(item.period_end).slice(0, 10) : "—";
|
settlementTimeZone?: string,
|
||||||
|
): string {
|
||||||
|
const start = item.period_start
|
||||||
|
? (settlementTimeZone
|
||||||
|
? formatLotteryInstantInTimeZone(item.period_start, settlementTimeZone)
|
||||||
|
: formatPlayerInstant(item.period_start)).slice(0, 10)
|
||||||
|
: "—";
|
||||||
|
const end = item.period_end
|
||||||
|
? (settlementTimeZone
|
||||||
|
? formatLotteryInstantInTimeZone(item.period_end, settlementTimeZone)
|
||||||
|
: formatPlayerInstant(item.period_end)).slice(0, 10)
|
||||||
|
: "—";
|
||||||
return `${start} / ${end}`;
|
return `${start} / ${end}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreditSettlementBillsBlock({
|
function CreditSettlementBillsBlock({
|
||||||
data,
|
data,
|
||||||
currency,
|
currency,
|
||||||
|
settlementTimeZone,
|
||||||
}: {
|
}: {
|
||||||
data: WalletSettlementBillsData | null;
|
data: WalletSettlementBillsData | null;
|
||||||
currency: string;
|
currency: string;
|
||||||
|
settlementTimeZone?: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("player");
|
const { t } = useTranslation("player");
|
||||||
const summary = data?.summary;
|
const summary = data?.summary;
|
||||||
@@ -157,7 +170,9 @@ function CreditSettlementBillsBlock({
|
|||||||
? t("wallet.agentPaysPlayer", { defaultValue: "代理应付玩家" })
|
? t("wallet.agentPaysPlayer", { defaultValue: "代理应付玩家" })
|
||||||
: t("wallet.playerPaysAgent", { defaultValue: "玩家应付代理" })}
|
: t("wallet.playerPaysAgent", { defaultValue: "玩家应付代理" })}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-0.5 text-[11px] text-slate-500">{formatPeriodRange(item)}</p>
|
<p className="mt-0.5 text-[11px] text-slate-500">
|
||||||
|
{formatPeriodRange(item, settlementTimeZone)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 text-right">
|
<div className="shrink-0 text-right">
|
||||||
@@ -199,6 +214,7 @@ export function WalletScreen() {
|
|||||||
const sectionDeepLinkHandledRef = useRef(false);
|
const sectionDeepLinkHandledRef = useRef(false);
|
||||||
|
|
||||||
const profile = usePlayerSessionStore((s) => s.profile);
|
const profile = usePlayerSessionStore((s) => s.profile);
|
||||||
|
const settlementTimeZone = profile?.settlement_timezone || "UTC";
|
||||||
const fundingModeView = resolveFundingModeView(balance, profile);
|
const fundingModeView = resolveFundingModeView(balance, profile);
|
||||||
const fundingModeKnown = fundingModeView !== "unknown";
|
const fundingModeKnown = fundingModeView !== "unknown";
|
||||||
const isCreditPlayer = fundingModeView === "credit";
|
const isCreditPlayer = fundingModeView === "credit";
|
||||||
@@ -478,7 +494,11 @@ export function WalletScreen() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{fundingModeKnown && isCreditPlayer ? (
|
{fundingModeKnown && isCreditPlayer ? (
|
||||||
<CreditSettlementBillsBlock data={settlementBills} currency={currency} />
|
<CreditSettlementBillsBlock
|
||||||
|
data={settlementBills}
|
||||||
|
currency={currency}
|
||||||
|
settlementTimeZone={settlementTimeZone}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{fundingModeKnown && !isCreditPlayer ? (
|
{fundingModeKnown && !isCreditPlayer ? (
|
||||||
@@ -562,6 +582,7 @@ export function WalletScreen() {
|
|||||||
filter={filter}
|
filter={filter}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
currency={currency}
|
currency={currency}
|
||||||
|
settlementTimeZone={isCreditPlayer ? settlementTimeZone : undefined}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="h-40 w-full rounded-lg" />
|
<Skeleton className="h-40 w-full rounded-lg" />
|
||||||
|
|||||||
@@ -451,6 +451,7 @@
|
|||||||
"wallet": "主站钱包玩家(划转与钱包余额流水)"
|
"wallet": "主站钱包玩家(划转与钱包余额流水)"
|
||||||
},
|
},
|
||||||
"creditAvailableAfter": "变更后可用信用",
|
"creditAvailableAfter": "变更后可用信用",
|
||||||
|
"creditReleasedAmount": "释额 {{amount}}",
|
||||||
"creditPaymentRecordOnly": "账期收付记账,不计入可用信用",
|
"creditPaymentRecordOnly": "账期收付记账,不计入可用信用",
|
||||||
"totalRecords": "共 {{total}} 条记录",
|
"totalRecords": "共 {{total}} 条记录",
|
||||||
"emptyLogs": "暂无流水",
|
"emptyLogs": "暂无流水",
|
||||||
@@ -501,7 +502,7 @@
|
|||||||
"game_settlement_loss": "亏损入账",
|
"game_settlement_loss": "亏损入账",
|
||||||
"game_settlement_win": "中奖释额",
|
"game_settlement_win": "中奖释额",
|
||||||
"game_settlement_reversal": "结算冲正",
|
"game_settlement_reversal": "结算冲正",
|
||||||
"settlement_confirm": "账期收款",
|
"settlement_confirm": "账期还款释额",
|
||||||
"settlement_payout": "账期付款",
|
"settlement_payout": "账期付款",
|
||||||
"rebate_accrued": "账期回水",
|
"rebate_accrued": "账期回水",
|
||||||
"rebate_in_bill": "回水已入账单",
|
"rebate_in_bill": "回水已入账单",
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ export type DrawResultsNumbers = {
|
|||||||
consolation: string[];
|
consolation: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DrawResultProviderRow = {
|
||||||
|
provider_code?: string | null;
|
||||||
|
provider_name?: string | null;
|
||||||
|
result_version: number;
|
||||||
|
results: DrawResultsNumbers;
|
||||||
|
result_items: Array<{
|
||||||
|
prize_type: string;
|
||||||
|
prize_index: number;
|
||||||
|
number_4d: string;
|
||||||
|
suffix_3d: string | null;
|
||||||
|
suffix_2d: string | null;
|
||||||
|
head_digit: number | null;
|
||||||
|
tail_digit: number | null;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
export type DrawResultListItem = {
|
export type DrawResultListItem = {
|
||||||
draw_id: string;
|
draw_id: string;
|
||||||
draw_no: string;
|
draw_no: string;
|
||||||
@@ -15,6 +31,8 @@ export type DrawResultListItem = {
|
|||||||
draw_time_iso?: string | null;
|
draw_time_iso?: string | null;
|
||||||
result_version: number;
|
result_version: number;
|
||||||
result_source: string | null;
|
result_source: string | null;
|
||||||
|
provider_code?: string | null;
|
||||||
|
provider_name?: string | null;
|
||||||
jackpot?: {
|
jackpot?: {
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -33,6 +51,7 @@ export type DrawResultListItem = {
|
|||||||
head_digit: number | null;
|
head_digit: number | null;
|
||||||
tail_digit: number | null;
|
tail_digit: number | null;
|
||||||
}>;
|
}>;
|
||||||
|
provider_results?: DrawResultProviderRow[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DrawResultsListPayload = {
|
export type DrawResultsListPayload = {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type PlayerMeData = {
|
|||||||
username: string | null;
|
username: string | null;
|
||||||
nickname: string | null;
|
nickname: string | null;
|
||||||
default_currency: string;
|
default_currency: string;
|
||||||
|
settlement_timezone: string;
|
||||||
status: number;
|
status: number;
|
||||||
/** 当次请求解析后的界面语言(zh / en / ne) */
|
/** 当次请求解析后的界面语言(zh / en / ne) */
|
||||||
locale: string;
|
locale: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user