feat(claim-skin): 买家皮肤领取页(路径式 /claim/<token>)

买家从卡券/订单消息里的链接打开,看到物品图片与信息,填游戏账号即可领取。

- **路径式无 `#`**:新增独立 BrowserRouter 分支(照 `/redeem` 的做法),
  `isClaimSkinPath` 只认 `/claim/<token>` 两段精确匹配;
  打手领取页仍是主站 HashRouter 的 `#/claim/:token`(其 pathname 为 `/`),两者互不影响
- **物品卡片**:商品图(映射不到或加载失败都降级为占位块,不出现破图)、
  内部 SKU、数量 ×N、订单号、引流标题(灰色副信息)
- **领取表单**:按接口下发的 `claimFields` 动态渲染(游戏账号/角色ID),
  提交前二次确认让买家核对账号
- **状态**:待领取展示表单;已领取展示成功态(优先于 tokenStatus);
  token 失效/过期/关闭/已用分别给中文提示与联系客服引导
- 纯函数抽到 `utils/claim-skin.ts` 并补 20 个单测(路由判定、状态映射、字段校验、格式化)
This commit is contained in:
yml2213
2026-10-09 15:06:56 +08:00
parent b70741daec
commit eacca11768
9 changed files with 1214 additions and 0 deletions
+29
View File
@@ -11,6 +11,7 @@ import {
} from 'react-router'
import { getAdminRole, hasAdminSession } from '@/utils/admin-auth'
import { isClaimSkinPath } from '@/utils/claim-skin'
import { isRedeemPath } from '@/utils/redeem'
import { isWorkerSite, WORKER_SITE_ORIGIN } from '@/utils/site'
import { hasWorkerSession } from '@/utils/worker-auth'
@@ -35,6 +36,7 @@ const AdminUsersPage = lazy(() => import('@/pages/admin/AdminUsersPage'))
const AdminDevMockPage = lazy(() => import('@/pages/admin/AdminDevMockPage'))
const AdminWorkerPlatformPage = lazy(() => import('@/pages/admin/AdminWorkerPlatformPage'))
const ClaimPage = lazy(() => import('@/pages/claim/ClaimPage'))
const ClaimSkinPage = lazy(() => import('@/pages/claim-skin/ClaimSkinPage'))
const CollectPage = lazy(() => import('@/pages/collect/CollectPage'))
const NotFoundPage = lazy(() => import('@/pages/NotFoundPage'))
const RedeemPage = lazy(() => import('@/pages/redeem/RedeemPage'))
@@ -90,6 +92,14 @@ export default function App() {
return <WorkerSiteRedirect path={legacyWorkerPath} />
}
// 买家皮肤领取页是路径式(`https://<域名>/claim/<token>`,不带 `#`),必须早于站点判断,
// 用独立的 BrowserRouter 分支承载,主站 HashRouter 不受影响。
// 注意:打手领取页仍是主站 HashRouter 里的 `#/claim/:token`,它的 pathname 是 `/`,
// 不会命中这里(判定见 utils/claim-skin.isClaimSkinPath),两者互不影响。
if (isClaimSkinPath(window.location.pathname)) {
return <ClaimSkinApplication />
}
// 固定兑换链接是路径式(`https://<域名>/redeem`,不带 `#`),必须早于站点判断,
// 用独立的 BrowserRouter 分支承载,主站 HashRouter 不受影响。
if (isRedeemPath(window.location.pathname)) {
@@ -158,6 +168,25 @@ function RedeemApplication() {
)
}
/**
* 路径式皮肤领取页(`/claim/<token>`)的独立路由树。
*
* 只在 `isClaimSkinPath` 命中时才会挂载,所以这里的兜底路由正常走不到;
* 真走到也复用同一个页面(token 为空时页面自己展示「链接无效」),不会出现路由空屏。
*/
function ClaimSkinApplication() {
return (
<BrowserRouter>
<Suspense fallback={<RouteLoading />}>
<Routes>
<Route path="/claim/:token" element={<ClaimSkinPage />} />
<Route path="*" element={<ClaimSkinPage />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
function WorkerApplication() {
return (
<BrowserRouter>