From 8307cc3f884dbfcb9e32bd4605376329c547aca7 Mon Sep 17 00:00:00 2001 From: wchino Date: Tue, 21 Jul 2026 21:11:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(player):=20=E4=BC=98=E5=8C=96=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E6=8A=95=E6=B3=A8=E5=B8=83=E5=B1=80=E4=B8=8E=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design-qa.md | 148 +++++ package-lock.json | 12 + src/api/player-auth.ts | 13 + src/app/globals.css | 14 + .../layout/player-desktop-header.tsx | 4 +- src/components/layout/player-panel.tsx | 2 +- src/components/ui/field.tsx | 238 ++++++++ src/features/hall/hall-betting-grid.tsx | 517 +++++++++++++----- src/features/hall/hall-desktop-workspace.tsx | 184 +++++++ src/features/hall/hall-draw-panel.tsx | 10 +- src/features/hall/hall-screen.tsx | 2 +- src/features/hall/hall-wallet-strip.tsx | 23 +- .../orders/ticket-orders-list-screen.tsx | 19 +- src/features/player/player-account-dialog.tsx | 279 ++++++++++ src/features/player/player-login-screen.tsx | 12 +- .../results/draw-results-list-screen.tsx | 14 +- src/features/rules/play-rules-screen.tsx | 35 +- src/features/wallet/wallet-logs-block.tsx | 14 +- src/i18n/locales/en/entry.json | 3 +- src/i18n/locales/en/player.json | 52 ++ src/i18n/locales/ne/entry.json | 3 +- src/i18n/locales/ne/player.json | 52 ++ src/i18n/locales/zh/entry.json | 3 +- src/i18n/locales/zh/player.json | 52 ++ src/lib/player-spacing.ts | 4 +- 25 files changed, 1514 insertions(+), 195 deletions(-) create mode 100644 design-qa.md create mode 100644 src/components/ui/field.tsx create mode 100644 src/features/hall/hall-desktop-workspace.tsx create mode 100644 src/features/player/player-account-dialog.tsx diff --git a/design-qa.md b/design-qa.md new file mode 100644 index 0000000..e5e6465 --- /dev/null +++ b/design-qa.md @@ -0,0 +1,148 @@ +# 玩家端 PC 桌面化 Design QA + +final result: passed + +## 基准与实现证据 + +- 视觉基准(方案 3):`/Users/wchino/.codex/generated_images/019f8441-2864-75d2-a2a9-69bc25a3bc7a/exec-3297a599-f1a3-40a2-8845-e4d38b19bc52.png` +- 1728×907 最终实现:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/final-1728x907-populated.jpg` +- 同画布左右对比:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/design-comparison-option3-vs-implementation.jpg` +- 1024×768 最终实现:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/final-1024x768.jpg` +- 390×844 移动回归:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/full-390x844.jpg` +- 480×844 移动回归:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/viewport-480x844-full.jpg` +- 浏览器批注修正后 1567×899:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/followup-audit/hall-after-1567x899.jpg` + +## 验收状态 + +- 桌面基准状态:已登录、信用模式、开放期号;大厅使用 4D、四条有效草稿、一家开注商,用于检验工具区、表格密度和复核栏。 +- 1024×768:复核摘要位于表格下方,宽度 743px;表格宽度 741px。页面 `scrollWidth === clientWidth === 1024`,横向滚动仅在表格内部。 +- 1280×800、1440×900:复核摘要位于表格下方;1500px 以上复核栏位于表格右侧,避免中等桌面宽度压缩输入区。页面无全局横向滚动。 +- 390×844、480×844:继续使用原移动布局和底部提交栏;页面无全局横向滚动。 +- 主导航、玩法切换、号码和金额输入、预览、提交、注单展开、信用流水筛选均可操作。 +- Chrome 控制台没有新增 error;只有开发模式 HMR、TokenRefresh 和 React DevTools 提示。 + +## 真实数据闭环 + +- 测试订单号:`TO20260721111116994982` +- 票号:`TK20260721111116970261` +- 期号:`20260721-1039` +- 测试内容:号码 `3147`、玩法 `大`、Singapore、`NPR 1.00` +- 我的注单:总记录从 8 增至 9,订单立即出现并可展开查看票号、玩法、号码、开注商和金额。 +- 信用流水:新增“下注占用 −NPR 1.00”;提交时可用信用从 `NPR 95,891.00` 变为 `NPR 95,890.00`,已用信用从 `NPR 3,098.00` 变为 `NPR 3,099.00`,与授信 `NPR 98,989.00` 一致。 + +## 差异检查与迭代 + +1. 初版将三步工具区和复核区按方案 3 重组,保留真实表格列、开注商与提交预览。 +2. 1024px 首轮发现状态卡金额和第三步操作被压缩,调整状态区为 16rem 信用卡、隐藏窄屏次要授信摘要,并收窄玩法按钮。 +3. 复查 1024px 后期号、倒计时和信用金额完整显示,第三步操作不再裁切,复核区在表格后保持全宽。 +4. 1728px 同画布对比确认层级、三步工具区、投注台账、右侧复核栏、主操作位置与方案 3 一致;保留现有产品的真实列和业务文案。 +5. 最终 P0:0,P1:0,P2:0。 + +## 浏览器批注复查 + +- 大厅汇总条由单行滚动表改为自适应网格:1567、1440、1280、1024、480、390px 均为 `scrollWidth === clientWidth`;本轮进一步扩展为稳定的全玩法换行网格,移动端不显示横向滚动条。 +- 投注表格收窄桌面列宽并调整复核栏断点:1567、1440、1280px 表格内部无横向滚动;1024px 与 480/390px 只在投注表格内部保留必要横向滚动,页面本身无横向滚动。 +- 桌面复核区的可用信用、下注后可用和预计扣款已移除 `NPR` 前缀;“提交前仍会显示最终预览”提示已移除。 +- 开奖结果、我的注单、信用页复查后未发现新的 P0/P1/P2;规则页空数据状态改为有界居中提示,减少空白拉伸感。 +- 规则页 1567×899:`scrollWidth === clientWidth === 1567`;Chrome 控制台无新增 error。 + +## 浏览器批注复查 2 + +- 源视觉真值:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/followup-audit/hall-after-1567x899.jpg`,对应本轮三个浏览器批注前的 1567×899 大厅状态。 +- 实现截图:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-2/hall-after-open-1567x899.jpg`;状态为已登录、开放期号、4D、20 条空白行。 +- 全视图同画布对比:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-2/comparison-before-after-open.jpg`。 +- 汇总区聚焦对比:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-2/comparison-summary-focused.jpg`;密集表头和金额状态在原始尺寸下可读,因此无需更多局部裁切。 +- 非零高亮证据:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-2/hall-highlight-1567x899.jpg`。 +- 移动换行证据:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-2/hall-after-390x844.jpg`;31 个汇总单元固定六列换行,汇总区 `scrollWidth === clientWidth === 354`,页面 `scrollWidth === clientWidth === 390`。 + +### 比较历史与修复 + +1. P1:汇总区只显示当前玩法列,切换 4D/3D/2D 时内容变化。修复为稳定的 D4、D3、D2 全玩法集合;实测三个 Tab 的 31 个单元内容和顺序完全一致。 +2. P1:列同步复选框仅切换状态,关闭时不会清列。修复为开启后把该列输入同步到所有号码有效的行,关闭后清空该列;实测两行号码的 4C 金额同步为 `7`,取消后两行 4C 均为空。 +3. P1:批量清空会把 20 行缩减为 1 行。修复为只清号码和金额,保留行 ID、开注商和选择类型;实测清空前后均为 20 行,号码和金额为空,20 行默认开注商选择保留。 +4. P2:零值与非零值缺少视觉区分。零值使用灰色表头/数值,非零值使用蓝色表头与浅蓝底;实测 `4A 8.00` 与总计高亮,其他 `0.00` 保持灰态。 + +### 忠实度检查 + +- 字体与排版:沿用现有字体、字号和表格字重;长标签在单元格内保持清晰,不截断。 +- 间距与布局:1567px 使用两行自适应汇总网格,390px 使用六列换行;无新增全局或汇总区横向滚动。 +- 颜色与状态:灰态和蓝色高亮继续使用现有 slate/品牌蓝色 token,金额状态对比明确。 +- 图片与资产:本轮没有新增或替换图片、Logo、图标或装饰资产。 +- 文案与内容:保留全部现有玩法名称、金额格式和业务文案,只调整显示集合与交互行为。 +- Chrome 控制台无 error;本轮最终 P0:0,P1:0,P2:0,`final result` 保持 `passed`。 + +## 浏览器批注复查 3 + +- 源视觉真值:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-3/hall-before-1221x899.jpg`,对应汇总区仍位于三步工具栏下方的 1221px 大厅状态。 +- 桌面实现:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-3/hall-after-desktop-final-1221x899-cdp.jpg`;视口 1221×899、已登录、开放期号、4D。 +- 移动实现:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-3/hall-after-mobile-final-390x844-cdp.jpg`;视口 390×844。 +- 全视图同画布对比:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-3/comparison-before-after-1221.jpg`。 +- 汇总位置聚焦对比:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-3/comparison-summary-position-focused.jpg`。 + +### 比较历史与修复 + +1. P1:汇总区位于三步工具栏和投注表格之间,不符合“位于选择玩法上方”的信息顺序。修复为桌面端在三步工具栏之前渲染汇总区。 +2. P2:第一次移动后,移动端快捷填单位于汇总区和玩法 Tab 之间,汇总区与玩法 Tab 相距 84px。修复为移动端按“快捷填单、汇总区、玩法 Tab”排列,并统一使用 12px 垂直间距。 +3. Post-fix:1221px 下汇总区到三步工具栏为 12px,工具栏到投注表格为 13px;390px 下快捷填单到汇总区、汇总区到玩法 Tab 均为 12px。 +4. Post-fix:1221px 页面 `scrollWidth === clientWidth === 1221`;390px 页面 `scrollWidth === clientWidth === 390`,汇总区 `scrollWidth === clientWidth === 341`。 + +### 忠实度检查 + +- 字体与排版:玩法标签、金额字号、字重和换行规则保持不变。 +- 间距与布局:只改变汇总模块顺序并统一相邻模块 12px 节奏,没有改变状态区、工具栏或表格尺寸。 +- 颜色与状态:零值灰态和非零蓝色高亮保持不变。 +- 图片与资产:没有新增或替换 Logo、图标及图片资产。 +- 文案与内容:全部玩法名称、金额、快捷填单和工具栏文案保持不变。 +- Chrome 控制台无 error;本轮最终 P0:0,P1:0,P2:0,`final result` 保持 `passed`。 + +## 浏览器批注复查 4 + +- 源视觉真值:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-4/hall-before-390x844.png`,对应移动端快捷填单、汇总区和玩法 Tab 尚未形成独立上下间距的状态。 +- 移动实现:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-4/hall-after-390x844.png`;视口 390×844、已登录、开放期号、4D。 +- 桌面回归:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-4/hall-after-1221x899.png`;视口 1221×899。 +- 全视图同画布对比:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-4/mobile-before-after.png`。 +- 标注区域聚焦对比:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-4/mobile-before-after-focus.png`;快捷填单、玩法 Tab 与相邻模块的边界在原始尺寸下清晰可读,无需继续裁切。 + +### 比较历史与修复 + +1. P2:移动端快捷填单、汇总区和玩法 Tab 被放在同一垂直组内,快捷填单区域没有形成用户标注所需的独立上、下边界。修复为把全玩法汇总区移出快捷填单组,移动端顺序调整为“汇总区、快捷填单、玩法 Tab、投注表格”。 +2. Post-fix:390px 下汇总区到快捷填单组为 12px、快捷填单到玩法 Tab 为 12px、玩法组到投注表格为 12px;页面 `scrollWidth === clientWidth === 390`。 +3. Post-fix:1221px 页面 `scrollWidth === clientWidth === 1221`,桌面汇总区、三步工具栏和投注表格结构不变。 + +### 忠实度检查 + +- 字体与排版:快捷填单、玩法 Tab、汇总金额和表格字号、字重均未改变。 +- 间距与布局:只拆分移动端模块分组并统一 12px 垂直节奏,标注区域上下都有可见留白。 +- 颜色与状态:灰态汇总、品牌蓝和表格状态色保持不变。 +- 图片与资产:没有新增或替换 Logo、钱包背景、图标及图片资产。 +- 文案与内容:玩法、金额、快捷填单和业务文案保持不变。 +- Chrome 控制台无 error;本轮最终 P0:0,P1:0,P2:0,`final result` 保持 `passed`。 + +## 浏览器批注复查 5 + +- 源视觉真值:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-5/hall-before-390x844.png`,对应移动端期号状态卡和可用信用卡直接相贴的状态。 +- 移动实现:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-5/hall-after-390x844.png`;视口 390×844、已登录、开放期号、4D。 +- 桌面回归:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-5/hall-after-1221x899.png`;视口 1221×899。 +- 全视图同画布对比:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-5/mobile-before-after.png`。 +- 顶部模块聚焦对比:`/Users/wchino/.codex/visualizations/2026/07/21/019f8441-2864-75d2-a2a9-69bc25a3bc7a/annotation-round-5/mobile-before-after-focus.png`;状态卡和信用卡的边界与留白在原始尺寸下清晰可读。 + +### 比较历史与修复 + +1. P2:移动端期号状态卡与可用信用卡之间为 0px,两个独立信息模块视觉上粘连。修复为移动端容器使用 12px 垂直间距,并在桌面断点归零以继续使用并排网格。 +2. Post-fix:390px 下状态卡到信用卡为 12px,信用卡到全玩法汇总区为 12px;页面 `scrollWidth === clientWidth === 390`。 +3. Post-fix:1221px 下状态卡和信用卡顶部差为 0px,继续保持并排等高布局;页面 `scrollWidth === clientWidth === 1221`。 + +### 忠实度检查 + +- 字体与排版:期号、时间、倒计时、信用金额及辅助文案均未改变。 +- 间距与布局:只增加移动端两个顶部模块之间的 12px 间距,桌面网格保持原位。 +- 颜色与状态:状态色、红色信用卡和页面背景 token 保持不变。 +- 图片与资产:钱包背景、Logo 和现有图标均未新增或替换。 +- 文案与内容:期号状态、信用金额和导航文案保持不变。 +- Chrome 控制台无 error;本轮最终 P0:0,P1:0,P2:0,`final result` 保持 `passed`。 + +## 工程验证 + +- `npm run lint`:passed +- `npm run build`:passed +- `git diff --check`:passed diff --git a/package-lock.json b/package-lock.json index 72665f8..e7f8321 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/dompurify": "^3.2.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -2523,6 +2524,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.2.0.tgz", + "integrity": "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==", + "deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "dompurify": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", diff --git a/src/api/player-auth.ts b/src/api/player-auth.ts index 4fdc0a3..4be050b 100644 --- a/src/api/player-auth.ts +++ b/src/api/player-auth.ts @@ -27,6 +27,12 @@ export type PlayerAuthLoginData = { }; }; +export type PlayerPasswordUpdatePayload = { + current_password: string; + password: string; + password_confirmation: string; +}; + /** `GET /api/v1/player/auth/captcha`(公开) */ export function getPlayerAuthCaptcha(): Promise { return lotteryRequest.get(`/player/auth/captcha`, { @@ -42,3 +48,10 @@ export function postPlayerAuthLogin(body: PlayerAuthLoginPayload): Promise { + return lotteryRequest.put<{ password_changed: boolean }>(`/player/password`, body); +} diff --git a/src/app/globals.css b/src/app/globals.css index d30c38b..38e4330 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -170,6 +170,20 @@ .player-desktop-shell { --player-sidebar-width: 15rem; } + + .player-pc-page { + font-size: 0.9375rem; + line-height: 1.5; + } + + .player-pc-empty { + min-height: 18rem; + } + + .player-pc-table-head { + font-size: 0.875rem; + line-height: 1.25rem; + } } /* 玩家端 Toast:顶部居中、紧凑尺寸(位置见 components/ui/sonner.tsx) */ diff --git a/src/components/layout/player-desktop-header.tsx b/src/components/layout/player-desktop-header.tsx index 0b0e198..027f854 100644 --- a/src/components/layout/player-desktop-header.tsx +++ b/src/components/layout/player-desktop-header.tsx @@ -4,6 +4,7 @@ import Image from "next/image"; import Link from "next/link"; import { LanguageSwitcher } from "@/components/language-switcher"; +import { PlayerAccountDialog } from "@/features/player/player-account-dialog"; import { cn } from "@/lib/utils"; type PlayerDesktopHeaderProps = { @@ -26,7 +27,7 @@ export function PlayerDesktopHeader({ className }: PlayerDesktopHeaderProps) { alt="N lotto" width={243} height={84} - className="h-7 w-auto max-w-[160px] object-contain object-left" + className="h-7 w-auto max-w-[112px] object-contain object-left sm:max-w-[160px]" priority /> @@ -54,6 +55,7 @@ export function PlayerDesktopHeader({ className }: PlayerDesktopHeaderProps) { showFlag={false} className="rounded-full border border-[#e4eaf4] bg-[#f8fafc] [&_button]:h-8 [&_button]:gap-1 [&_button]:px-2 [&_button]:text-xs lg:[&_button]:h-9 lg:[&_button]:gap-1.5 lg:[&_button]:px-3 lg:[&_button]:text-sm" /> + ); diff --git a/src/components/layout/player-panel.tsx b/src/components/layout/player-panel.tsx index 0ffad70..ed5c03c 100644 --- a/src/components/layout/player-panel.tsx +++ b/src/components/layout/player-panel.tsx @@ -25,7 +25,7 @@ export function PlayerPanel({
) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +const fieldVariants = cva( + "group/field flex w-full gap-2 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: "flex-col *:w-full [&>.sr-only]:w-auto", + horizontal: + "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + responsive: + "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ )} + fillControls={( +
+ {favoriteChips.length > 0 ? ( + <> + + {t("hall.quickFill.favorites")} + + {favoriteChips.slice(0, 5).map((number) => ( + + ))} + + ) : null} + + {t("hall.quickFill.history")} + + {historyChips.length > 0 ? ( + historyChips.slice(0, 6).map((number) => ( + + )) + ) : ( + + {t("hall.quickFill.emptyHistory")} + + )} +
+ )} + reviewControls={( +
+ +
+ )} + /> + + +
{categoryTabs.map((tab) => { @@ -1885,7 +2173,13 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
) : null} -
+
-
- - - - - {allPlayColumns.map((column) => ( - - ))} - - - - - - {allPlayColumns.map((column) => { - const colTotalMinor = rows.reduce((sum, row) => { - const amtStr = row.amounts[column.key]; - if (!amtStr) return sum; - if ( - row.number.trim().length === 0 || - draftLineIssueReason(column.play.play_code, row.number, column.digitSlot) !== null - ) { - return sum; - } - const minorAmt = parseDecimalInputToMinor(amtStr, currencyCode); - return sum + (minorAmt ?? 0); - }, 0); - - return ( - - ); - })} - - -
- {t("hall.table.total", { defaultValue: "共" })} - - {playColumnHeaderLabel( - column.play, - playCategory(column.play.play_code), - column.digitSlot, - t, - )} -
- {formatMinorAmount(submitActualMinor)} - - {formatMinorAmount(colTotalMinor)} -
-
-
- ))} @@ -2087,49 +2329,35 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } {!isMobile ? ( - + : t("hall.table.submitBet"), + }} + /> ) : null} @@ -2377,7 +2605,8 @@ const DraftRowItem = memo(function DraftRowItem({ > ))} - diff --git a/src/features/hall/hall-desktop-workspace.tsx b/src/features/hall/hall-desktop-workspace.tsx new file mode 100644 index 0000000..6e22a62 --- /dev/null +++ b/src/features/hall/hall-desktop-workspace.tsx @@ -0,0 +1,184 @@ +import type { ReactNode } from "react"; +import { AlertTriangle, CheckCircle2, Lock, Ticket } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +type HallDesktopWorkflowBarProps = { + gameControls: ReactNode; + fillControls: ReactNode; + reviewControls: ReactNode; + stepGame: string; + stepFill: string; + stepReview: string; +}; + +export function HallDesktopWorkflowBar({ + gameControls, + fillControls, + reviewControls, + stepGame, + stepFill, + stepReview, +}: HallDesktopWorkflowBarProps) { + const steps = [ + { label: stepGame, content: gameControls }, + { label: stepFill, content: fillControls }, + { label: stepReview, content: reviewControls }, + ]; + + return ( +
+ {steps.map((step, index) => ( +
0 && "border-l border-[#e7edf6]", + )} + > +
+ + {index + 1} + +

{step.label}

+
+ {step.content} +
+ ))} +
+ ); +} + +export type HallDesktopReviewModel = { + lineCount: number; + providerCount: number; + actualAmount: string; + availableCredit: string; + remainingCredit: string; + warningCount: number; + creditSufficient: boolean; +}; + +type HallDesktopReviewPanelProps = { + model: HallDesktopReviewModel; + labels: { + title: string; + lineCount: string; + providerCount: string; + actualTotal: string; + availableCredit: string; + remainingCredit: string; + noWarnings: string; + warningCount: string; + creditEnough: string; + creditInsufficient: string; + submit: string; + }; + disabled: boolean; + loading: boolean; + isBettable: boolean; + onSubmit: () => void; +}; + +export function HallDesktopReviewPanel({ + model, + labels, + disabled, + loading, + isBettable, + onSubmit, +}: HallDesktopReviewPanelProps) { + return ( + + ); +} diff --git a/src/features/hall/hall-draw-panel.tsx b/src/features/hall/hall-draw-panel.tsx index 916394c..0061583 100644 --- a/src/features/hall/hall-draw-panel.tsx +++ b/src/features/hall/hall-draw-panel.tsx @@ -54,7 +54,7 @@ function ScheduleAnchorTime({ {time} - {date ? {date} : null} + {date ? {date} : null} ); } @@ -268,11 +268,11 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot }) {/* Desktop: 单行工具条 — 左期号状态,右时间/倒计时,与右侧钱包卡同高 */} -
+
- {t("draw.issueNo")} - + {t("draw.issueNo")} + {display.draw_no}
@@ -281,7 +281,7 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot }) {t(hud.labelKey, { defaultValue: hud.labelKey })}
-
+
{blockedNotice}
) : null} -
+
+ {t("hall.table.no", { defaultValue: "No." })} ( {playColumnHeaderLabel( @@ -1984,17 +2227,16 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } setSyncAmountColumns((current) => ({ - ...current, - [column.key]: checked === true, - }))} + onCheckedChange={(checked) => + toggleSyncAmountColumn(column, checked === true) + } aria-label={t("hall.table.syncColumnAmount", { column: playColumnHeaderLabel(column.play, playCategory(column.play.play_code), column.digitSlot, t), })} title={t("hall.table.syncColumnAmount", { column: playColumnHeaderLabel(column.play, playCategory(column.play.play_code), column.digitSlot, t), })} - className="mx-auto mt-1 size-3.5" + className={cn("mx-auto mt-1", isMobile ? "size-3.5" : "size-4")} /> setActiveRowId(row.id)} onChange={(event) => updateRowNumber(row.id, event.target.value)} className={cn( - "h-8 w-full rounded-md border-[#e1e8f3] bg-white px-1 text-center font-mono font-bold tabular-nums text-slate-950 shadow-sm focus-visible:ring-[#1d57b7]", - !isMobile ? "text-[13px]" : "text-sm", + "w-full rounded-md border-[#e1e8f3] bg-white px-1 text-center font-mono font-bold tabular-nums text-slate-950 shadow-sm focus-visible:ring-[#1d57b7]", + !isMobile ? "h-9 text-sm" : "h-8 text-sm", "tracking-[0.12em]", )} /> @@ -2431,7 +2661,8 @@ const DraftRowItem = memo(function DraftRowItem({ setActiveRowId(row.id)} onChange={(event) => updateAmount(row.id, column.key, event.target.value)} className={cn( - "h-8 w-full rounded-md border-[#e1e8f3] bg-white px-0.5 text-center font-bold tabular-nums shadow-sm focus-visible:ring-[#1d57b7]", - !isMobile ? "text-[13px]" : "text-sm", + "w-full rounded-md border-[#e1e8f3] bg-white px-0.5 text-center font-bold tabular-nums shadow-sm focus-visible:ring-[#1d57b7]", + !isMobile ? "h-9 text-sm" : "h-8 text-sm", hasAmount && "border-[#9bbcff] bg-[#f5f9ff] text-[#0b3f96]", status === "warning" && "border-amber-200 bg-amber-50 text-amber-800", status === "sold_out" && "border-slate-200 bg-slate-100 text-slate-400", )} /> ) : ( -
+
)} {isInputValidForPlay && status === "sold_out" ? (

@@ -2473,12 +2704,12 @@ const DraftRowItem = memo(function DraftRowItem({ ); })} {showSelectionTypeColumn ? ( -

+ toggleRowProvider(row.id, provider.code)} aria-label={`${provider.name} ${index + 1}`} - className="mx-auto border-slate-500 bg-white data-checked:border-slate-800 data-checked:bg-slate-800" + className={cn("mx-auto border-slate-500 bg-white data-checked:border-slate-800 data-checked:bg-slate-800", !isMobile && "size-4")} /> + {formatMinorAmount(rowTotalMinor)}
- + {t("results.detailTitle")} {RESULTS_TOP_PRIZE_KEYS.map((tier) => ( {t(resultsPrizeLabelKey(tier))} ))} - + @@ -391,7 +391,7 @@ export function DrawResultsListScreen() { } }} > - +

@@ -417,13 +417,13 @@ export function DrawResultsListScreen() {

{RESULTS_TOP_PRIZE_KEYS.map((tier) => ( - + {row.results[tier]} ))} - + diff --git a/src/features/rules/play-rules-screen.tsx b/src/features/rules/play-rules-screen.tsx index 2344e1d..a2c87b3 100644 --- a/src/features/rules/play-rules-screen.tsx +++ b/src/features/rules/play-rules-screen.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Loader2 } from "lucide-react"; +import { BookOpenText, Loader2, TriangleAlert } from "lucide-react"; import { getPublicSettings } from "@/api"; import { PlayerPanel } from "@/components/layout/player-panel"; @@ -12,6 +12,7 @@ import { Card, CardContent } from "@/components/ui/card"; export function PlayRulesScreen() { const { t, i18n } = useTranslation("player"); const [htmlContent, setHtmlContent] = useState(null); + const [contentState, setContentState] = useState<"html" | "empty" | "error">("empty"); const [loading, setLoading] = useState(true); useEffect(() => { @@ -21,11 +22,14 @@ export function PlayRulesScreen() { const html = resolvePlayRulesHtml(res.items, i18n.language); if (html) { setHtmlContent(html); + setContentState("html"); } else { - setHtmlContent(`
${t("rules.empty", { defaultValue: "暂无玩法规则说明" })}
`); + setHtmlContent(null); + setContentState("empty"); } } catch { - setHtmlContent(`
${t("rules.loadFailed", { defaultValue: "规则加载失败" })}
`); + setHtmlContent(null); + setContentState("error"); } finally { setLoading(false); } @@ -39,16 +43,31 @@ export function PlayRulesScreen() { backHref="/hall" className="bg-[#f7f9fd]" > -
- - +
+ + {loading ? ( -
+
+ ) : contentState !== "html" ? ( +
+ + {contentState === "error" ? ( + + ) : ( + + )} + +

+ {contentState === "error" + ? t("rules.loadFailed", { defaultValue: "规则加载失败" }) + : t("rules.empty", { defaultValue: "暂无玩法规则说明" })} +

+
) : (
)} diff --git a/src/features/wallet/wallet-logs-block.tsx b/src/features/wallet/wallet-logs-block.tsx index 1e6a5fb..2964d1f 100644 --- a/src/features/wallet/wallet-logs-block.tsx +++ b/src/features/wallet/wallet-logs-block.tsx @@ -189,7 +189,7 @@ export function WalletLogsBlock({

{logs.items.length === 0 ? ( -
+
{t(creditMode ? "wallet.emptyCreditLogs" : "wallet.emptyLogs", { defaultValue: creditMode ? "暂无信用流水" : "暂无流水", })} @@ -198,24 +198,24 @@ export function WalletLogsBlock({ <>
-

+

{t("wallet.logType", { defaultValue: "类型" })}

-

+

{t("wallet.logTime", { defaultValue: "时间" })}

-

+

{t("wallet.logRef", { defaultValue: "关联单号" })}

-

+

{t("wallet.logAmount", { defaultValue: "金额" })}

-

+

{creditMode ? t("wallet.creditAvailableAfter") : t("wallet.balanceAfter")}

-

+

{t("orders.status")}

diff --git a/src/i18n/locales/en/entry.json b/src/i18n/locales/en/entry.json index 1d7b9ed..fadcdb8 100644 --- a/src/i18n/locales/en/entry.json +++ b/src/i18n/locales/en/entry.json @@ -70,7 +70,8 @@ "captchaRefresh": "Refresh captcha", "captchaFetch": "Tap to load", "usernameInvalidCharset": "Username may only contain letters, numbers, dots (.), underscores, and hyphens", - "passwordMinLength": "Password must be at least 6 characters" + "passwordMinLength": "Password must be at least 6 characters", + "passwordChanged": "Password changed. Sign in again with your new password." }, "errors": { "noToken": "No authorization token found", diff --git a/src/i18n/locales/en/player.json b/src/i18n/locales/en/player.json index afa0dc0..528a05d 100644 --- a/src/i18n/locales/en/player.json +++ b/src/i18n/locales/en/player.json @@ -113,6 +113,44 @@ "player": { "fallback": "Player #{{id}}" }, + "account": { + "open": "View player profile", + "loading": "Player", + "title": "My account", + "description": "View the signed-in player profile and account security settings.", + "auth": { + "native": "Lottery account", + "sso": "Main-site API" + }, + "funding": { + "credit": "Credit mode", + "wallet": "Wallet mode" + }, + "fields": { + "username": "Username", + "site": "Site", + "playerId": "Player ID", + "currency": "Default currency" + }, + "ssoManaged": "This sign-in is managed by the main site. Return to the main site to sign out or change your credentials.", + "logout": "Sign out", + "password": { + "action": "Change password", + "title": "Change login password", + "description": "After the password is changed, all lottery sessions for this account will be signed out.", + "current": "Current password", + "new": "New password", + "confirm": "Confirm new password", + "hint": "Use at least 6 characters.", + "required": "Complete the current, new, and confirmation password fields", + "tooShort": "The new password must be at least 6 characters", + "mismatch": "The new passwords do not match", + "mustDiffer": "The new password must differ from the current password", + "submit": "Change password", + "saving": "Changing...", + "failed": "Failed to change password. Try again later." + } + }, "draw": { "currentIssue": "Current issue", "currentTime": "Current Time", @@ -164,6 +202,20 @@ "placeFailed": "Submission failed", "placeSuccess": "Bet submitted. Order {{orderNo}}, deducted {{amount}}.", "placePartialFailed": "{{success}} succeeded, {{failed}} failed", + "desktop": { + "stepGame": "Choose game", + "stepFill": "Enter number and stake", + "stepReview": "Review and submit", + "reviewTitle": "Review and submit", + "availableCredit": "Available credit", + "remainingCredit": "Available after bet", + "warnings": "Risk alerts", + "noWarnings": "No risk alerts", + "warningCount": "{{count}} rows near sold out", + "creditEnough": "Sufficient available credit", + "creditInsufficient": "Insufficient available credit", + "secureHint": "A final preview appears before submission" + }, "placeAllFailed": "All {{failed}} lines failed", "playConfig": { "playClosedDraftCleared": "{{playCode}} is closed. Related draft amounts have been cleared.", diff --git a/src/i18n/locales/ne/entry.json b/src/i18n/locales/ne/entry.json index 6f00b15..65fb231 100644 --- a/src/i18n/locales/ne/entry.json +++ b/src/i18n/locales/ne/entry.json @@ -70,7 +70,8 @@ "captchaRefresh": "क्याप्चा रिफ्रेस", "captchaFetch": "लोड गर्न ट्याप गर्नुहोस्", "usernameInvalidCharset": "प्रयोगकर्ता नाममा अक्षर, अंक, डट (.), अन्डरस्कोर र हाइफन मात्र हुन सक्छ", - "passwordMinLength": "पासवर्ड कम्तीमा ६ वर्ण हुनुपर्छ" + "passwordMinLength": "पासवर्ड कम्तीमा ६ वर्ण हुनुपर्छ", + "passwordChanged": "पासवर्ड बदलियो। नयाँ पासवर्डले फेरि लगइन गर्नुहोस्।" }, "errors": { "noToken": "कुनै प्राधिकरण टोकन फेला परेन", diff --git a/src/i18n/locales/ne/player.json b/src/i18n/locales/ne/player.json index 9869cf4..78af81d 100644 --- a/src/i18n/locales/ne/player.json +++ b/src/i18n/locales/ne/player.json @@ -113,6 +113,44 @@ "player": { "fallback": "खेलाडी #{{id}}" }, + "account": { + "open": "खेलाडी प्रोफाइल हेर्नुहोस्", + "loading": "खेलाडी", + "title": "मेरो खाता", + "description": "हाल लगइन भएको खेलाडी प्रोफाइल र खाता सुरक्षा सेटिङ हेर्नुहोस्।", + "auth": { + "native": "लटरी खाता", + "sso": "मुख्य साइट API" + }, + "funding": { + "credit": "क्रेडिट मोड", + "wallet": "वालेट मोड" + }, + "fields": { + "username": "प्रयोगकर्ता नाम", + "site": "साइट", + "playerId": "खेलाडी ID", + "currency": "पूर्वनिर्धारित मुद्रा" + }, + "ssoManaged": "यो लगइन मुख्य साइटले व्यवस्थापन गर्छ। लगआउट गर्न वा लगइन विवरण बदल्न मुख्य साइटमा फर्कनुहोस्।", + "logout": "लगआउट", + "password": { + "action": "पासवर्ड बदल्नुहोस्", + "title": "लगइन पासवर्ड बदल्नुहोस्", + "description": "पासवर्ड बदलेपछि यस खाताका सबै लटरी सत्रबाट लगआउट हुनेछ।", + "current": "हालको पासवर्ड", + "new": "नयाँ पासवर्ड", + "confirm": "नयाँ पासवर्ड पुष्टि", + "hint": "कम्तीमा ६ वर्ण प्रयोग गर्नुहोस्।", + "required": "हालको, नयाँ र पुष्टि पासवर्ड पूरा भर्नुहोस्", + "tooShort": "नयाँ पासवर्ड कम्तीमा ६ वर्ण हुनुपर्छ", + "mismatch": "नयाँ पासवर्डहरू मिलेनन्", + "mustDiffer": "नयाँ पासवर्ड हालको पासवर्डभन्दा फरक हुनुपर्छ", + "submit": "पासवर्ड बदल्नुहोस्", + "saving": "बदल्दै...", + "failed": "पासवर्ड बदल्न असफल भयो। पछि फेरि प्रयास गर्नुहोस्।" + } + }, "draw": { "currentIssue": "हालको इश्यू", "currentTime": "हालको समय", @@ -164,6 +202,20 @@ "placeFailed": "पेश गर्न असफल", "placeSuccess": "बेट पेश भयो। अर्डर {{orderNo}}, कट्टा {{amount}}।", "placePartialFailed": "{{success}} सफल, {{failed}} असफल", + "desktop": { + "stepGame": "खेल छान्नुहोस्", + "stepFill": "नम्बर र रकम भर्नुहोस्", + "stepReview": "जाँच गरी पेश गर्नुहोस्", + "reviewTitle": "जाँच र पेश", + "availableCredit": "उपलब्ध क्रेडिट", + "remainingCredit": "बेटपछि उपलब्ध", + "warnings": "जोखिम सूचना", + "noWarnings": "जोखिम सूचना छैन", + "warningCount": "{{count}} पङ्क्ति बिक्री सकिन नजिक", + "creditEnough": "पर्याप्त क्रेडिट उपलब्ध छ", + "creditInsufficient": "उपलब्ध क्रेडिट पर्याप्त छैन", + "secureHint": "पेश गर्नुअघि अन्तिम पूर्वावलोकन देखाइनेछ" + }, "placeAllFailed": "सबै {{failed}} लाइन असफल", "playConfig": { "playClosedDraftCleared": "{{playCode}} बन्द छ। सम्बन्धित ड्राफ्ट रकम हटाइएको छ।", diff --git a/src/i18n/locales/zh/entry.json b/src/i18n/locales/zh/entry.json index 65ab4bf..ab077d6 100644 --- a/src/i18n/locales/zh/entry.json +++ b/src/i18n/locales/zh/entry.json @@ -70,7 +70,8 @@ "captchaRefresh": "刷新验证码", "captchaFetch": "点击获取", "usernameInvalidCharset": "账号只能使用字母、数字、点(.)、下划线和连字符", - "passwordMinLength": "密码至少需要 6 个字符" + "passwordMinLength": "密码至少需要 6 个字符", + "passwordChanged": "密码已修改,请使用新密码重新登录" }, "errors": { "noToken": "未发现授权令牌", diff --git a/src/i18n/locales/zh/player.json b/src/i18n/locales/zh/player.json index 2ba74ca..625635f 100644 --- a/src/i18n/locales/zh/player.json +++ b/src/i18n/locales/zh/player.json @@ -113,6 +113,44 @@ "player": { "fallback": "玩家 #{{id}}" }, + "account": { + "open": "查看玩家资料", + "loading": "玩家", + "title": "我的账号", + "description": "查看当前登录的玩家资料与账号安全设置。", + "auth": { + "native": "彩票账号登录", + "sso": "主站 API 接入" + }, + "funding": { + "credit": "信用模式", + "wallet": "钱包模式" + }, + "fields": { + "username": "登录账号", + "site": "所属站点", + "playerId": "玩家编号", + "currency": "默认币种" + }, + "ssoManaged": "当前登录由主站管理。如需退出或修改登录凭证,请返回主站操作。", + "logout": "退出登录", + "password": { + "action": "修改密码", + "title": "修改登录密码", + "description": "修改成功后,当前账号的所有彩票端会话都会退出。", + "current": "当前密码", + "new": "新密码", + "confirm": "确认新密码", + "hint": "密码至少需要 6 个字符。", + "required": "请完整填写当前密码、新密码和确认密码", + "tooShort": "新密码至少需要 6 个字符", + "mismatch": "两次输入的新密码不一致", + "mustDiffer": "新密码不能与当前密码相同", + "submit": "确认修改", + "saving": "正在修改...", + "failed": "修改密码失败,请稍后重试" + } + }, "draw": { "currentIssue": "当前期号", "currentTime": "当前时间", @@ -164,6 +202,20 @@ "placeFailed": "提交失败", "placeSuccess": "下注成功,订单号 {{orderNo}},实扣 {{amount}}。", "placePartialFailed": "{{success}} 个成功,{{failed}} 个失败", + "desktop": { + "stepGame": "选择玩法", + "stepFill": "填写号码和金额", + "stepReview": "核对并提交", + "reviewTitle": "核对与提交", + "availableCredit": "可用信用", + "remainingCredit": "下注后可用", + "warnings": "风险提醒", + "noWarnings": "暂无风险提醒", + "warningCount": "{{count}} 行接近售罄", + "creditEnough": "可用信用充足", + "creditInsufficient": "可用信用不足", + "secureHint": "提交前仍会显示最终预览" + }, "playConfig": { "playClosedDraftCleared": "{{playCode}} 已关闭,相关草稿金额已清除。", "playClosed": "{{playCode}} 已关闭。", diff --git a/src/lib/player-spacing.ts b/src/lib/player-spacing.ts index 80c30ab..6595202 100644 --- a/src/lib/player-spacing.ts +++ b/src/lib/player-spacing.ts @@ -1,8 +1,8 @@ /** 主滚动区外边距(灰底区域与顶栏/底栏留白) */ -export const playerMainInset = "px-2 pt-2 pb-2 lg:px-5 lg:pt-5 lg:pb-5"; +export const playerMainInset = "px-2 pt-2 pb-2 lg:px-4 lg:pt-4 lg:pb-4"; /** 页内白卡片内边距(桌面主内容已是白卡片,减少二次 padding) */ -export const playerPageShellPadding = "px-2 pt-2 pb-4 lg:px-1 lg:pt-1 lg:pb-2"; +export const playerPageShellPadding = "px-2 pt-2 pb-4 lg:px-0 lg:pt-0 lg:pb-1"; /** 大厅等直接写在 section 上的内边距 */ export const playerPageInset = playerPageShellPadding;