From 7bed43ac96d5602a47b01cb9e28fc6cccbf35f93 Mon Sep 17 00:00:00 2001 From: kang Date: Sat, 9 May 2026 09:25:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=B0=81=E8=A3=85=E8=AF=B7?= =?UTF-8?q?=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/health.ts | 9 +++ src/api/index.ts | 4 ++ src/api/paths.ts | 2 + src/api/player.ts | 15 +++++ src/api/wallet.ts | 19 +++++++ src/app/(player)/layout.tsx | 9 +++ src/app/(player)/page.tsx | 23 ++++++++ src/app/layout.tsx | 4 +- src/app/page.tsx | 65 ---------------------- src/components/layout/player-app-shell.tsx | 24 ++++++++ src/lib/lottery-http.ts | 48 +++------------- src/types/api/envelope.ts | 18 ++++++ src/types/api/errors.ts | 20 +++++++ src/types/api/health.ts | 7 +++ src/types/api/index.ts | 11 ++++ src/types/api/ping.ts | 4 ++ src/types/api/player-me.ts | 10 ++++ src/types/api/wallet-balance.ts | 9 +++ src/types/index.ts | 2 + 19 files changed, 195 insertions(+), 108 deletions(-) create mode 100644 src/api/health.ts create mode 100644 src/api/index.ts create mode 100644 src/api/paths.ts create mode 100644 src/api/player.ts create mode 100644 src/api/wallet.ts create mode 100644 src/app/(player)/layout.tsx create mode 100644 src/app/(player)/page.tsx delete mode 100644 src/app/page.tsx create mode 100644 src/components/layout/player-app-shell.tsx create mode 100644 src/types/api/envelope.ts create mode 100644 src/types/api/errors.ts create mode 100644 src/types/api/health.ts create mode 100644 src/types/api/index.ts create mode 100644 src/types/api/ping.ts create mode 100644 src/types/api/player-me.ts create mode 100644 src/types/api/wallet-balance.ts create mode 100644 src/types/index.ts diff --git a/src/api/health.ts b/src/api/health.ts new file mode 100644 index 0000000..b232bae --- /dev/null +++ b/src/api/health.ts @@ -0,0 +1,9 @@ +import { lotteryRequest } from "@/lib/lottery-http"; +import type { HealthData } from "@/types/api/health"; + +import { API_V1_PREFIX } from "@/api/paths"; + +/** `GET /api/v1/health` */ +export function getHealth(): Promise { + return lotteryRequest.get(`${API_V1_PREFIX}/health`); +} diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..036768f --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,4 @@ +export { API_V1_PREFIX } from "@/api/paths"; +export { getHealth } from "@/api/health"; +export { getPlayerPing, getPlayerMe } from "@/api/player"; +export { getWalletBalance, type GetWalletBalanceParams } from "@/api/wallet"; diff --git a/src/api/paths.ts b/src/api/paths.ts new file mode 100644 index 0000000..fd23d1a --- /dev/null +++ b/src/api/paths.ts @@ -0,0 +1,2 @@ +/** Laravel `routes/api.php`:`api` 前缀 + `v1` 分组 */ +export const API_V1_PREFIX = "/api/v1"; diff --git a/src/api/player.ts b/src/api/player.ts new file mode 100644 index 0000000..25e8cbd --- /dev/null +++ b/src/api/player.ts @@ -0,0 +1,15 @@ +import { lotteryRequest } from "@/lib/lottery-http"; +import type { PlayerMeData } from "@/types/api/player-me"; +import type { ScopePingData } from "@/types/api/ping"; + +import { API_V1_PREFIX } from "@/api/paths"; + +/** `GET /api/v1/player/ping`(无需登录) */ +export function getPlayerPing(): Promise { + return lotteryRequest.get(`${API_V1_PREFIX}/player/ping`); +} + +/** `GET /api/v1/player/me`(需玩家 Token) */ +export function getPlayerMe(): Promise { + return lotteryRequest.get(`${API_V1_PREFIX}/player/me`); +} diff --git a/src/api/wallet.ts b/src/api/wallet.ts new file mode 100644 index 0000000..c5cf03a --- /dev/null +++ b/src/api/wallet.ts @@ -0,0 +1,19 @@ +import { lotteryRequest } from "@/lib/lottery-http"; +import type { WalletBalanceData } from "@/types/api/wallet-balance"; + +import { API_V1_PREFIX } from "@/api/paths"; + +export type GetWalletBalanceParams = { + /** Query `currency`,不传则用玩家默认币种 */ + currency?: string; +}; + +/** `GET /api/v1/wallet/balance`(需玩家 Token) */ +export function getWalletBalance( + params?: GetWalletBalanceParams, +): Promise { + return lotteryRequest.get( + `${API_V1_PREFIX}/wallet/balance`, + { params }, + ); +} diff --git a/src/app/(player)/layout.tsx b/src/app/(player)/layout.tsx new file mode 100644 index 0000000..744671a --- /dev/null +++ b/src/app/(player)/layout.tsx @@ -0,0 +1,9 @@ +import { PlayerAppShell } from "@/components/layout/player-app-shell"; + +export default function PlayerLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return {children}; +} diff --git a/src/app/(player)/page.tsx b/src/app/(player)/page.tsx new file mode 100644 index 0000000..82d0d01 --- /dev/null +++ b/src/app/(player)/page.tsx @@ -0,0 +1,23 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +export default function PlayerHomePage() { + return ( + + + 玩家端 + + 基础 layout 已就绪;后续在此挂钱包、大厅等路由。 + + + + 顶栏与主区域宽度由 PlayerAppShell 统一控制。 + + + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3252991..1186a86 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,8 +15,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Lottery", + description: "Lottery player", }; export default function RootLayout({ diff --git a/src/app/page.tsx b/src/app/page.tsx deleted file mode 100644 index 3f36f7c..0000000 --- a/src/app/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
- ); -} diff --git a/src/components/layout/player-app-shell.tsx b/src/components/layout/player-app-shell.tsx new file mode 100644 index 0000000..13b724f --- /dev/null +++ b/src/components/layout/player-app-shell.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from "react"; + +type PlayerAppShellProps = { + children: ReactNode; +}; + +/** + * 玩家端业务区外壳:顶栏 + 主内容(移动端宽度上限,桌面居中)。 + * 标题 / 底部导航等后续再接 i18n、路由。 + */ +export function PlayerAppShell({ children }: PlayerAppShellProps): ReactNode { + return ( +
+
+
+ Lottery +
+
+
+ {children} +
+
+ ); +} diff --git a/src/lib/lottery-http.ts b/src/lib/lottery-http.ts index da3e11c..818c4fd 100644 --- a/src/lib/lottery-http.ts +++ b/src/lib/lottery-http.ts @@ -5,33 +5,11 @@ import axios, { type AxiosResponse, } from "axios"; -/** 后端 `{ code, msg, data }`;约定 `code === 0` 表示成功 */ -export type ApiEnvelope = { - code: number; - msg: string; - data: T; -}; - -/** HTTP 状态非 2xx(或 Axios 报错)但其 body 仍是业务信封且 `code !== 0` 时抛出 */ -export class LotteryApiBizError extends Error { - readonly code: number; - readonly data: unknown; - - constructor(message: string, code: number, data: unknown) { - super(message); - this.name = "LotteryApiBizError"; - this.code = code; - this.data = data; - } -} - -/** body 不是约定的信封结构 */ -export class LotteryApiEnvelopeError extends Error { - constructor(message = "响应不是约定的 { code, msg, data }") { - super(message); - this.name = "LotteryApiEnvelopeError"; - } -} +import { + LotteryApiBizError, + LotteryApiEnvelopeError, +} from "@/types/api/errors"; +import { isApiEnvelope } from "@/types/api/envelope"; export function setLotteryRequestLocale(locale: string | null): void { if (locale === null) { @@ -69,18 +47,6 @@ function acceptLanguage(loc: ReturnType): string { return "en-US,en;q=0.9"; } -function isEnvelope(v: unknown): v is ApiEnvelope { - if (v === null || typeof v !== "object") { - return false; - } - const o = v as Record; - return ( - typeof o.code === "number" && - typeof o.msg === "string" && - "data" in o - ); -} - const baseURL = process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim(); /** @@ -112,7 +78,7 @@ function mergeLocaleHeaders( * `code !== 0` 抛 {@link LotteryApiBizError}。 */ export function unwrapData(payload: unknown): T { - if (!isEnvelope(payload)) { + if (!isApiEnvelope(payload)) { throw new LotteryApiEnvelopeError(); } if (payload.code !== 0) { @@ -138,7 +104,7 @@ export async function request(config: AxiosRequestConfig): Promise { } catch (err: unknown) { if (isAxiosError(err) && err.response?.data !== undefined) { const body = err.response.data; - if (isEnvelope(body) && body.code !== 0) { + if (isApiEnvelope(body) && body.code !== 0) { throw new LotteryApiBizError(body.msg, body.code, body.data); } } diff --git a/src/types/api/envelope.ts b/src/types/api/envelope.ts new file mode 100644 index 0000000..a9b553c --- /dev/null +++ b/src/types/api/envelope.ts @@ -0,0 +1,18 @@ +/** 后端 `{ code, msg, data }`;约定 `code === 0` 表示成功 */ +export type ApiEnvelope = { + code: number; + msg: string; + data: T; +}; + +export function isApiEnvelope(v: unknown): v is ApiEnvelope { + if (v === null || typeof v !== "object") { + return false; + } + const o = v as Record; + return ( + typeof o.code === "number" && + typeof o.msg === "string" && + "data" in o + ); +} diff --git a/src/types/api/errors.ts b/src/types/api/errors.ts new file mode 100644 index 0000000..db0ed93 --- /dev/null +++ b/src/types/api/errors.ts @@ -0,0 +1,20 @@ +/** HTTP 非成功或其 body 为业务信封且 `code !== 0` 时抛出 */ +export class LotteryApiBizError extends Error { + readonly code: number; + readonly data: unknown; + + constructor(message: string, code: number, data: unknown) { + super(message); + this.name = "LotteryApiBizError"; + this.code = code; + this.data = data; + } +} + +/** body 不是约定的信封结构 */ +export class LotteryApiEnvelopeError extends Error { + constructor(message = "响应不是约定的 { code, msg, data }") { + super(message); + this.name = "LotteryApiEnvelopeError"; + } +} diff --git a/src/types/api/health.ts b/src/types/api/health.ts new file mode 100644 index 0000000..c4fa08b --- /dev/null +++ b/src/types/api/health.ts @@ -0,0 +1,7 @@ +/** `GET /api/v1/health` → `data` */ +export type HealthData = { + app: string; + default_currency: string; + /** 仅 `APP_DEBUG=true` 时出现 */ + laravel?: string; +}; diff --git a/src/types/api/index.ts b/src/types/api/index.ts new file mode 100644 index 0000000..3acdcb9 --- /dev/null +++ b/src/types/api/index.ts @@ -0,0 +1,11 @@ +export type { ApiEnvelope } from "./envelope"; +export { isApiEnvelope } from "./envelope"; +export { + LotteryApiBizError, + LotteryApiEnvelopeError, +} from "./errors"; + +export type { HealthData } from "./health"; +export type { ScopePingData } from "./ping"; +export type { PlayerMeData } from "./player-me"; +export type { WalletBalanceData } from "./wallet-balance"; diff --git a/src/types/api/ping.ts b/src/types/api/ping.ts new file mode 100644 index 0000000..6169f74 --- /dev/null +++ b/src/types/api/ping.ts @@ -0,0 +1,4 @@ +/** `GET /api/v1/player/ping` → `data` */ +export type ScopePingData = { + scope: string; +}; diff --git a/src/types/api/player-me.ts b/src/types/api/player-me.ts new file mode 100644 index 0000000..3ffff16 --- /dev/null +++ b/src/types/api/player-me.ts @@ -0,0 +1,10 @@ +/** `GET /api/v1/player/me` → `data`(需 `lottery.player`) */ +export type PlayerMeData = { + id: number; + site_code: string; + site_player_id: string; + username: string; + nickname: string | null; + default_currency: string; + status: number; +}; diff --git a/src/types/api/wallet-balance.ts b/src/types/api/wallet-balance.ts new file mode 100644 index 0000000..5ca8003 --- /dev/null +++ b/src/types/api/wallet-balance.ts @@ -0,0 +1,9 @@ +/** `GET /api/v1/wallet/balance` → `data`(需 `lottery.player`) */ +export type WalletBalanceData = { + /** 最小货币单位 bigint,序列化可能为 string */ + balance: string | number; + main_balance: null; + currency_code: string; + wallet_type: string; + frozen_balance: string | number; +}; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..33e5ff7 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,2 @@ +/** 对外类型出口;API 契约与接口 DTO 见 `./api` */ +export * from "./api";