更新wz-app项目,完善迁移文档,新增路由映射和组件速查,重构App.tsx以移除占位组件,确保所有非Tab路由均已挂载真实组件,增强项目结构的清晰性与一致性。
This commit is contained in:
@@ -1,18 +1,37 @@
|
||||
# old C 端 → new/react-app 迁移清单
|
||||
# old C 端 → new/wz-app(Vite + React)迁移清单
|
||||
|
||||
> 权威路由注册见 [`src/App.tsx`](src/App.tsx)(`react-router`)。历史 uni 的 `pages.json` 仅作考古。
|
||||
|
||||
**说明(2026-04)**:堆叠子路由已从 `old/app/**/page.tsx` 全量迁入 `src/pages/**`(含带参路由 `useParams`)。**不再使用** `StackStubPage` / `ParamStubPage` 占位。
|
||||
|
||||
| 路由 | 来源(old) | 主要 wz-api | 状态 |
|
||||
|------|-------------|------------|------|
|
||||
| `/` | `old/app/page.tsx` | `GET /api/app/streamers`、`GET /api/app/user/me` | 已像素对接 |
|
||||
| `/login` | `old/app/login/page.tsx` | `POST /api/app/auth/login`、`/register`、`/user/me` | 已接 Bearer |
|
||||
| `/moments` | `old/app/moments/page.tsx` | 本地演示数据 + 链到 `/live/:id` | 已迁入 |
|
||||
| `/planet` | `old/app/planet/page.tsx` | 匹配/派对 UI 为本地 | 已迁入 |
|
||||
| `/mall` | `old/app/mall/page.tsx` | 支付走前端 `pay()` 模拟 | 已迁入 |
|
||||
| `/profile` | `old/app/profile/page.tsx` | `GET/POST` 认证同上 | 已迁入 |
|
||||
| 其它 | — | 见下表子包 | `NotFound` 占位 |
|
||||
| `/` | `old/app/page.tsx` | `GET /api/app/streamers`、`GET /api/app/user/me` | 已接入 |
|
||||
| `/login` | `old/app/login/page.tsx` | `POST /api/app/auth/*`、`/user/me` | 已接 Bearer |
|
||||
| `/moments` | `old/app/moments/page.tsx` | 见 hooks / 演示数据 | 已迁入 |
|
||||
| `/planet` | `old/app/planet/page.tsx` | 本地 | 已迁入 |
|
||||
| `/mall` | `old/app/mall/page.tsx` | 商城相关 API | 已迁入 |
|
||||
| `/profile` | `old/app/profile/page.tsx` | 同上 | 已迁入 |
|
||||
| `/orders`、`/messages`、`/settings`、`/profile/edit` | 对应 `old/app/*` | 见各页 | 已迁入 |
|
||||
| 其余 `App.tsx` 独立堆叠路由 | 见下表与 `App.tsx` | 见 [`迁移-API对照表`](../../开发文档/玩值/迁移-API对照表.md) | **UI 已迁**;未接 API 处在页面或本表标 **TODO: API** |
|
||||
|
||||
**子包/深链**(以 `new/wz-app/src/pages.json` `subPackages` 为权威清单,对应 old 同路径页面,未迁路由一律落 `/*` 占位):
|
||||
### 堆叠页组件速查(Vite)
|
||||
|
||||
- `subpkgs/stack/*`:pitch-deck、events-today、live、live-detail、cp、cp-detail、hotel、mall-*、orders、streamer-studio、recharge 等
|
||||
- 下一步:按业务优先级从 `old/app/**` 复制为 `src/pages/...` 并注册 `react-router` 路由,同时在对照表确认 ` /api/app/*` 或标 TODO
|
||||
| 路由 | 组件 |
|
||||
|------|------|
|
||||
| `/cp`、`/cp/:id` | `cp/CpListPage`、`cp/CpDetailPage` |
|
||||
| `/party/:id` | `PartyDetailPage` |
|
||||
| `/course/:id` | `CourseDetailPage`(本地 `courseData` + `pay`) |
|
||||
| `/guild`、`/guild/:id` | `guild/GuildListPage`、`guild/GuildDetailPage` |
|
||||
| `/zone/:id` | `ZoneDetailPage` |
|
||||
| `/star/:id` | `StarDetailPage` |
|
||||
| `/chat/:id` | `ChatRoomPage`(`cp-*` 与普通群聊两态) |
|
||||
| `/moments/:momentId` | `MomentsDetailPage` |
|
||||
| `/moments/create` | `MomentsCreatePage` |
|
||||
| `/mall/companion` 等子路径 | `mall/*Page` |
|
||||
| `/mall/points/:game` | `mall/MallPointsGamePage` |
|
||||
| `/streamer/audience`、`/streamer/studio` | `streamer/StreamerAudiencePage`、`StreamerStudioPage`(演示数据 / 本地态) |
|
||||
| `/screen` | `ScreenPage`(C 端轻量壳,运营大屏见 wz-admin) |
|
||||
| 其余优惠券、预订、帮助、今日活动、路演、充值、会员、足迹、装扮、服务与交易、酒店、陪练、创作者等 | 见 `src/pages` 下同名或目录 |
|
||||
|
||||
**完成度(主路径)**:五 Tab + 登录 + 请求层 ≈ **主里程碑 100%**;全站子页 ≈ 按上表子包行数续增。
|
||||
**完成度(主路径)**:五 Tab + 登录 + 直播 + 积分商城 + **计划内非 Tab 子页**均已挂真实页面;后续以 **接 wz-api 契约** 与动效/体验迭代为主。
|
||||
|
||||
4
new/wz-app/dist/index.html
vendored
4
new/wz-app/dist/index.html
vendored
@@ -11,8 +11,8 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>玩值电竞 - WanZhi Esports</title>
|
||||
<script type="module" crossorigin src="/assets/index-C-wxPala.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Caid84HJ.css">
|
||||
<script type="module" crossorigin src="/assets/index-BrzcGHzM.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-eem3Gmvl.css">
|
||||
</head>
|
||||
<body class="min-h-screen antialiased">
|
||||
<div id="root"></div>
|
||||
|
||||
106
new/wz-app/package-lock.json
generated
106
new/wz-app/package-lock.json
generated
@@ -10,10 +10,12 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^1.8.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
@@ -1000,6 +1002,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
|
||||
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
|
||||
@@ -1149,6 +1180,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-previous": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
|
||||
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||
@@ -2720,6 +2784,33 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.38.0.tgz",
|
||||
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.38.0",
|
||||
"motion-utils": "^12.36.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -3308,6 +3399,21 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmmirror.com/motion-dom/-/motion-dom-12.38.0.tgz",
|
||||
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.36.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.36.0",
|
||||
"resolved": "https://registry.npmmirror.com/motion-utils/-/motion-utils-12.36.0.tgz",
|
||||
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^1.8.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
|
||||
@@ -13,9 +13,47 @@ import { ProfilePage } from "@/pages/ProfilePage"
|
||||
import { NotFoundPage } from "@/pages/NotFoundPage"
|
||||
import { LivePage } from "@/pages/LivePage"
|
||||
import { AuthSessionListener } from "@/components/auth/AuthSessionListener"
|
||||
import { StackStubPage } from "@/pages/stack/StackStubPage"
|
||||
import { ParamStubPage } from "@/pages/stack/ParamStubPage"
|
||||
import { LiveListPage } from "@/pages/stack/LiveListPage"
|
||||
import { OrdersPage } from "@/pages/OrdersPage"
|
||||
import { MessagesPage } from "@/pages/MessagesPage"
|
||||
import { SettingsPage } from "@/pages/SettingsPage"
|
||||
import { ProfileEditPage } from "@/pages/ProfileEditPage"
|
||||
import { CouponsPage } from "@/pages/CouponsPage"
|
||||
import { BookingPage } from "@/pages/BookingPage"
|
||||
import { AccountHelpPage } from "@/pages/AccountHelpPage"
|
||||
import { CustomerServicePage } from "@/pages/CustomerServicePage"
|
||||
import { EventsTodayPage } from "@/pages/EventsTodayPage"
|
||||
import { PitchDeckPage } from "@/pages/PitchDeckPage"
|
||||
import { RechargePage } from "@/pages/RechargePage"
|
||||
import { VipPage } from "@/pages/VipPage"
|
||||
import { FootprintPage } from "@/pages/FootprintPage"
|
||||
import { DressUpPage } from "@/pages/DressUpPage"
|
||||
import { MallCompanionPage } from "@/pages/mall/MallCompanionPage"
|
||||
import { MallGearPage } from "@/pages/mall/MallGearPage"
|
||||
import { MallCoursesPage } from "@/pages/mall/MallCoursesPage"
|
||||
import { MallPointsGamePage } from "@/pages/mall/MallPointsGamePage"
|
||||
import { LevelingPage } from "@/pages/services/LevelingPage"
|
||||
import { AccountServicesPage } from "@/pages/services/AccountServicesPage"
|
||||
import { TransactionHistoryPage } from "@/pages/TransactionHistoryPage"
|
||||
import { MyCoursesPage } from "@/pages/MyCoursesPage"
|
||||
import { CreatorApplyPage } from "@/pages/CreatorApplyPage"
|
||||
import { HotelPage } from "@/pages/HotelPage"
|
||||
import { CoachListPage } from "@/pages/CoachListPage"
|
||||
import { CoachDetailPage } from "@/pages/CoachDetailPage"
|
||||
import { MomentsCreatePage } from "@/pages/MomentsCreatePage"
|
||||
import { CpListPage } from "@/pages/cp/CpListPage"
|
||||
import { CpDetailPage } from "@/pages/cp/CpDetailPage"
|
||||
import { GuildListPage } from "@/pages/guild/GuildListPage"
|
||||
import { GuildDetailPage } from "@/pages/guild/GuildDetailPage"
|
||||
import { PartyDetailPage } from "@/pages/PartyDetailPage"
|
||||
import { CourseDetailPage } from "@/pages/CourseDetailPage"
|
||||
import { ZoneDetailPage } from "@/pages/ZoneDetailPage"
|
||||
import { StarDetailPage } from "@/pages/StarDetailPage"
|
||||
import { ChatRoomPage } from "@/pages/ChatRoomPage"
|
||||
import { MomentsDetailPage } from "@/pages/MomentsDetailPage"
|
||||
import { StreamerAudiencePage } from "@/pages/streamer/StreamerAudiencePage"
|
||||
import { StreamerStudioPage } from "@/pages/streamer/StreamerStudioPage"
|
||||
import { ScreenPage } from "@/pages/ScreenPage"
|
||||
|
||||
/** 带底栏 Tab 的主区(与全屏/堆叠子页互斥) */
|
||||
function TabLayout() {
|
||||
@@ -46,54 +84,49 @@ export default function App() {
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route element={<AuthLayout />}>
|
||||
<Route path="moments/create" element={<StackStubPage title="发布动态" description="与 old 发布流程对齐;可接埋点/上传。" />} />
|
||||
<Route path="moments/create" element={<MomentsCreatePage />} />
|
||||
<Route path="live" element={<LiveListPage />} />
|
||||
<Route path="live/:streamerId" element={<LivePage />} />
|
||||
<Route path="cp" element={<StackStubPage title="CP" />} />
|
||||
<Route path="cp/:id" element={<ParamStubPage label="CP" />} />
|
||||
<Route path="party/:id" element={<ParamStubPage label="派对" />} />
|
||||
<Route path="course/:id" element={<ParamStubPage label="课程" />} />
|
||||
<Route path="guild" element={<StackStubPage title="公会" />} />
|
||||
<Route path="guild/:id" element={<ParamStubPage label="公会" />} />
|
||||
<Route path="zone/:id" element={<ParamStubPage label="游戏专区" />} />
|
||||
<Route path="star/:id" element={<ParamStubPage label="星级" />} />
|
||||
<Route path="chat/:id" element={<ParamStubPage label="聊天" />} />
|
||||
<Route path="moments/:momentId" element={<ParamStubPage label="动态" paramKey="momentId" />} />
|
||||
<Route path="cp" element={<CpListPage />} />
|
||||
<Route path="cp/:id" element={<CpDetailPage />} />
|
||||
<Route path="party/:id" element={<PartyDetailPage />} />
|
||||
<Route path="course/:id" element={<CourseDetailPage />} />
|
||||
<Route path="guild" element={<GuildListPage />} />
|
||||
<Route path="guild/:id" element={<GuildDetailPage />} />
|
||||
<Route path="zone/:id" element={<ZoneDetailPage />} />
|
||||
<Route path="star/:id" element={<StarDetailPage />} />
|
||||
<Route path="chat/:id" element={<ChatRoomPage />} />
|
||||
<Route path="moments/:momentId" element={<MomentsDetailPage />} />
|
||||
<Route path="mall/points" element={<MallPointsPage />} />
|
||||
<Route path="mall/points/:game" element={<ParamStubPage label="点卡" paramKey="game" />} />
|
||||
<Route path="mall/companion" element={<StackStubPage title="陪玩" />} />
|
||||
<Route path="mall/gear" element={<StackStubPage title="电竞外设" />} />
|
||||
<Route path="mall/courses" element={<StackStubPage title="课程商城" />} />
|
||||
<Route path="settings" element={<StackStubPage title="设置" />} />
|
||||
<Route path="recharge" element={<StackStubPage title="充值" description="与玩值币/支付对接前为占位。" />} />
|
||||
<Route path="messages" element={<StackStubPage title="消息" />} />
|
||||
<Route path="events/today" element={<StackStubPage title="今日赛事" />} />
|
||||
<Route path="pitch-deck" element={<StackStubPage title="路演" />} />
|
||||
<Route path="coupons" element={<StackStubPage title="卡券" />} />
|
||||
<Route
|
||||
path="hotel"
|
||||
element={
|
||||
<StackStubPage title="酒店/网咖" description="可用 ?tab=cafe 区分网咖,后续用 useSearchParams 分 Tab。" />
|
||||
}
|
||||
/>
|
||||
<Route path="screen" element={<StackStubPage title="大屏展示" description="C 端占位;运营大屏见 wz-admin /screen。" />} />
|
||||
<Route path="orders" element={<StackStubPage title="订单" />} />
|
||||
<Route path="services/leveling" element={<StackStubPage title="游戏代练" />} />
|
||||
<Route path="services/account" element={<StackStubPage title="账号服务" />} />
|
||||
<Route path="transaction-history" element={<StackStubPage title="流水" />} />
|
||||
<Route path="streamer/audience" element={<StackStubPage title="观众洞察" />} />
|
||||
<Route path="streamer/studio" element={<StackStubPage title="主播工作台" />} />
|
||||
<Route path="profile/edit" element={<StackStubPage title="编辑资料" description="可对接 PATCH 用户资料与头像上传。" />} />
|
||||
<Route path="coach" element={<StackStubPage title="约教练" />} />
|
||||
<Route path="coach/:id" element={<ParamStubPage label="教练" />} />
|
||||
<Route path="footprint" element={<StackStubPage title="足迹" />} />
|
||||
<Route path="dress-up" element={<StackStubPage title="装扮" />} />
|
||||
<Route path="account-help" element={<StackStubPage title="账号与帮助" />} />
|
||||
<Route path="my-courses" element={<StackStubPage title="我的课程" />} />
|
||||
<Route path="vip" element={<StackStubPage title="会员" />} />
|
||||
<Route path="creator/apply" element={<StackStubPage title="创作者申请" />} />
|
||||
<Route path="booking" element={<StackStubPage title="预订" />} />
|
||||
<Route path="customer-service" element={<StackStubPage title="客服" />} />
|
||||
<Route path="mall/points/:game" element={<MallPointsGamePage />} />
|
||||
<Route path="mall/companion" element={<MallCompanionPage />} />
|
||||
<Route path="mall/gear" element={<MallGearPage />} />
|
||||
<Route path="mall/courses" element={<MallCoursesPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="recharge" element={<RechargePage />} />
|
||||
<Route path="messages" element={<MessagesPage />} />
|
||||
<Route path="events/today" element={<EventsTodayPage />} />
|
||||
<Route path="pitch-deck" element={<PitchDeckPage />} />
|
||||
<Route path="coupons" element={<CouponsPage />} />
|
||||
<Route path="hotel" element={<HotelPage />} />
|
||||
<Route path="screen" element={<ScreenPage />} />
|
||||
<Route path="orders" element={<OrdersPage />} />
|
||||
<Route path="services/leveling" element={<LevelingPage />} />
|
||||
<Route path="services/account" element={<AccountServicesPage />} />
|
||||
<Route path="transaction-history" element={<TransactionHistoryPage />} />
|
||||
<Route path="streamer/audience" element={<StreamerAudiencePage />} />
|
||||
<Route path="streamer/studio" element={<StreamerStudioPage />} />
|
||||
<Route path="profile/edit" element={<ProfileEditPage />} />
|
||||
<Route path="coach" element={<CoachListPage />} />
|
||||
<Route path="coach/:id" element={<CoachDetailPage />} />
|
||||
<Route path="footprint" element={<FootprintPage />} />
|
||||
<Route path="dress-up" element={<DressUpPage />} />
|
||||
<Route path="account-help" element={<AccountHelpPage />} />
|
||||
<Route path="my-courses" element={<MyCoursesPage />} />
|
||||
<Route path="vip" element={<VipPage />} />
|
||||
<Route path="creator/apply" element={<CreatorApplyPage />} />
|
||||
<Route path="booking" element={<BookingPage />} />
|
||||
<Route path="customer-service" element={<CustomerServicePage />} />
|
||||
<Route element={<TabLayout />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="moments" element={<MomentsPage />} />
|
||||
|
||||
26
new/wz-app/src/components/ui/switch.tsx
Normal file
26
new/wz-app/src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
69
new/wz-app/src/pages/AccountHelpPage.tsx
Normal file
69
new/wz-app/src/pages/AccountHelpPage.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { ArrowLeft, Shield, Phone, HelpCircle, ChevronRight, Lock, Mail, FileText, MessageSquare } from "lucide-react"
|
||||
|
||||
export function AccountHelpPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: "账号安全",
|
||||
items: [
|
||||
{ icon: Lock, label: "修改密码", desc: "定期修改密码保障账号安全" },
|
||||
{ icon: Phone, label: "绑定手机", desc: "已绑定: 138****8888" },
|
||||
{ icon: Mail, label: "绑定邮箱", desc: "未绑定" },
|
||||
{ icon: Shield, label: "实名认证", desc: "已认证" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "帮助中心",
|
||||
items: [
|
||||
{ icon: HelpCircle, label: "常见问题", desc: "查看热门问题解答" },
|
||||
{ icon: FileText, label: "用户协议", desc: "查看服务条款" },
|
||||
{ icon: FileText, label: "隐私政策", desc: "了解隐私保护" },
|
||||
{ icon: MessageSquare, label: "意见反馈", desc: "告诉我们您的建议" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-24">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="text-white/70 hover:text-white">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">账号与帮助</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-6">
|
||||
{sections.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex}>
|
||||
<h2 className="text-sm font-bold text-white/60 mb-3 px-1">{section.title}</h2>
|
||||
<div className="bg-card border border-border rounded-xl overflow-hidden">
|
||||
{section.items.map((item, itemIndex) => (
|
||||
<button
|
||||
key={itemIndex}
|
||||
type="button"
|
||||
className="w-full flex items-center gap-3 p-4 hover:bg-white/5 transition-colors border-b border-border last:border-0"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<item.icon size={18} className="text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-medium text-sm">{item.label}</div>
|
||||
<div className="text-xs text-white/40">{item.desc}</div>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-white/30" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<p className="text-xs text-white/30">玩值电竞 v1.0.0</p>
|
||||
<p className="text-xs text-white/20 mt-1">© 2025 玩值电竞 All Rights Reserved</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
226
new/wz-app/src/pages/BookingPage.tsx
Normal file
226
new/wz-app/src/pages/BookingPage.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import { useState, Suspense } from "react"
|
||||
import { useSearchParams, useNavigate, Link } from "react-router-dom"
|
||||
import { ArrowLeft, Calendar, Clock, CreditCard, Shield, CheckCircle2 } from "lucide-react"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
|
||||
const bookingData: Record<string, { starName: string; starAvatar: string; serviceName: string; price: number; duration: string; type: string }> = {
|
||||
"1-1": { starName: "GM-远洋", starAvatar: "/streamer-1.jpg", serviceName: "单局陪玩", price: 50, duration: "1局", type: "陪玩" },
|
||||
"1-2": { starName: "GM-远洋", starAvatar: "/streamer-1.jpg", serviceName: "5局套餐", price: 220, duration: "5局", type: "陪玩" },
|
||||
"1-3": { starName: "GM-远洋", starAvatar: "/streamer-1.jpg", serviceName: "打野进阶课", price: 299, duration: "8节课", type: "课程" },
|
||||
"1-4": { starName: "GM-远洋", starAvatar: "/streamer-1.jpg", serviceName: "拜师学艺", price: 1980, duration: "30天", type: "拜师" },
|
||||
"2-1": { starName: "小柠檬", starAvatar: "/streamer-2.jpg", serviceName: "单局陪玩", price: 80, duration: "1局", type: "陪玩" },
|
||||
"3-1": { starName: "魔兽老张", starAvatar: "/streamer-3.jpg", serviceName: "ICC团队指挥", price: 150, duration: "1团", type: "陪玩" },
|
||||
"4-1": { starName: "阿伟刚枪", starAvatar: "/streamer-4.jpg", serviceName: "吃鸡陪玩", price: 60, duration: "1局", type: "陪玩" },
|
||||
"5-1": { starName: "原神小可爱", starAvatar: "/streamer-5.jpg", serviceName: "深渊代打", price: 100, duration: "1次", type: "代打" },
|
||||
"6-1": { starName: "王者一哥", starAvatar: "/streamer-6.jpg", serviceName: "上分陪玩", price: 70, duration: "1局", type: "陪玩" },
|
||||
}
|
||||
|
||||
function BookingContent() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const navigate = useNavigate()
|
||||
const starId = searchParams.get("starId") || "1"
|
||||
const serviceId = searchParams.get("serviceId") || "1"
|
||||
const bookingKey = `${starId}-${serviceId}`
|
||||
const booking = bookingData[bookingKey] ?? bookingData["1-1"]!
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState("")
|
||||
const [selectedTime, setSelectedTime] = useState("")
|
||||
const [note, setNote] = useState("")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
|
||||
const availableDates = Array.from({ length: 7 }, (_, i) => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + i)
|
||||
return {
|
||||
value: date.toISOString().split("T")[0]!,
|
||||
label: i === 0 ? "今天" : i === 1 ? "明天" : `${date.getMonth() + 1}/${date.getDate()}`,
|
||||
}
|
||||
})
|
||||
|
||||
const timeSlots = [
|
||||
"09:00", "10:00", "11:00", "14:00", "15:00", "16:00", "17:00", "18:00", "19:00", "20:00", "21:00", "22:00",
|
||||
]
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedDate || !selectedTime) {
|
||||
toast({ title: "请选择预约时间" })
|
||||
return
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
setIsSubmitting(false)
|
||||
setIsSuccess(true)
|
||||
setTimeout(() => {
|
||||
void navigate("/profile")
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
||||
<div className="text-center space-y-6 max-w-sm">
|
||||
<div className="w-20 h-20 rounded-full bg-primary/20 flex items-center justify-center mx-auto">
|
||||
<CheckCircle2 size={48} className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">预约成功!</h2>
|
||||
<p className="text-white/60">
|
||||
已向 {booking.starName} 发送预约请求
|
||||
<br />
|
||||
请等待确认
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/profile"
|
||||
className="inline-block px-6 py-3 rounded-full bg-primary text-black font-bold hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
查看我的订单
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3 border-b border-white/5">
|
||||
<Link to={`/star/${starId}`} className="p-2 -ml-2 rounded-full hover:bg-white/5">
|
||||
<ArrowLeft size={20} />
|
||||
</Link>
|
||||
<h1 className="font-bold text-lg">确认预约</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="glass-card p-4 rounded-xl flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-xl overflow-hidden shrink-0">
|
||||
<img
|
||||
src={booking.starAvatar || "/placeholder.svg"}
|
||||
alt={booking.starName}
|
||||
width={64}
|
||||
height={64}
|
||||
className="h-16 w-16 object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-bold mb-1">{booking.starName}</h3>
|
||||
<p className="text-sm text-white/60 mb-2">{booking.serviceName}</p>
|
||||
<div className="flex items-center gap-3 text-xs text-white/50">
|
||||
<span>{booking.duration}</span>
|
||||
<span>•</span>
|
||||
<span className="text-primary font-bold">{booking.price} 玩值币</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 mb-6">
|
||||
<h3 className="font-bold mb-3 flex items-center gap-2">
|
||||
<Calendar size={18} className="text-primary" />
|
||||
选择日期
|
||||
</h3>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{availableDates.map((date) => (
|
||||
<button
|
||||
key={date.value}
|
||||
type="button"
|
||||
onClick={() => setSelectedDate(date.value)}
|
||||
className={`p-3 rounded-xl text-sm font-medium transition-all ${
|
||||
selectedDate === date.value ? "bg-primary text-black" : "glass-card hover:bg-white/10"
|
||||
}`}
|
||||
>
|
||||
{date.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 mb-6">
|
||||
<h3 className="font-bold mb-3 flex items-center gap-2">
|
||||
<Clock size={18} className="text-secondary" />
|
||||
选择时间
|
||||
</h3>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{timeSlots.map((time) => (
|
||||
<button
|
||||
key={time}
|
||||
type="button"
|
||||
onClick={() => setSelectedTime(time)}
|
||||
className={`p-3 rounded-xl text-sm font-medium transition-all ${
|
||||
selectedTime === time ? "bg-secondary text-white" : "glass-card hover:bg-white/10"
|
||||
}`}
|
||||
>
|
||||
{time}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 mb-6">
|
||||
<h3 className="font-bold mb-3">备注说明</h3>
|
||||
<textarea
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder="告诉大神你的需求,比如想学习的英雄、想提升的技巧等..."
|
||||
className="w-full h-24 p-3 rounded-xl glass-card resize-none text-sm placeholder:text-white/30 focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-4 mb-6">
|
||||
<h3 className="font-bold mb-3 flex items-center gap-2">
|
||||
<CreditCard size={18} className="text-accent" />
|
||||
支付方式
|
||||
</h3>
|
||||
<div className="glass-card p-4 rounded-xl flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center">
|
||||
<CreditCard size={20} className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-sm">玩值币余额</div>
|
||||
<div className="text-xs text-white/50">当前余额: 1,205 币</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-primary font-bold">-{booking.price}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-4 mb-6 p-4 rounded-xl bg-gradient-to-r from-primary/10 to-transparent border-l-4 border-primary">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield size={24} className="text-primary" />
|
||||
<div>
|
||||
<h4 className="font-bold text-sm">平台保障</h4>
|
||||
<p className="text-xs text-white/60">服务完成后才会扣款,不满意全额退款</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-0 left-0 right-0 p-4 glass border-t border-white/10 flex justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleSubmit()}
|
||||
disabled={isSubmitting || !selectedDate || !selectedTime}
|
||||
className="w-full py-4 rounded-full bg-primary text-black font-bold text-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all active:scale-95"
|
||||
>
|
||||
{isSubmitting ? "提交中..." : `确认预约 (${booking.price} 玩值币)`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function BookingPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-white/50">加载中...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<BookingContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
246
new/wz-app/src/pages/ChatRoomPage.tsx
Normal file
246
new/wz-app/src/pages/ChatRoomPage.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useParams, useNavigate } from "react-router-dom"
|
||||
import { ArrowLeft, Mic, Plus, Video, MoreHorizontal, Users, Heart, Phone, VideoIcon, Send } from "lucide-react"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const cpData: Record<string, { name: string; avatar: string; game: string; intro: string }> = {
|
||||
"cp-1": { name: "甜心辅助", avatar: "/streamer-5.jpg", game: "王者荣耀", intro: "温柔妹子" },
|
||||
"cp-2": { name: "电竞萌妹", avatar: "/streamer-6.jpg", game: "和平精英", intro: "可爱软妹" },
|
||||
"cp-3": { name: "游戏小仙女", avatar: "/viewer-1.jpg", game: "原神", intro: "元气少女" },
|
||||
}
|
||||
|
||||
export function ChatRoomPage() {
|
||||
const { id = "" } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const isCPChat = id.startsWith("cp-")
|
||||
const cpInfo = isCPChat ? cpData[id] : null
|
||||
|
||||
const [message, setMessage] = useState("")
|
||||
const [messages, setMessages] = useState<{ text: string; isMe: boolean; time: string }[]>([])
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isCPChat && cpInfo) {
|
||||
const t = setTimeout(() => {
|
||||
setIsConnected(true)
|
||||
setMessages([{ text: "你好呀~很高兴认识你!", isMe: false, time: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }) }])
|
||||
}, 1000)
|
||||
return () => clearTimeout(t)
|
||||
}
|
||||
}, [isCPChat, cpInfo])
|
||||
|
||||
const sendMessage = () => {
|
||||
if (!message.trim()) return
|
||||
const now = new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
|
||||
setMessages((prev) => [...prev, { text: message, isMe: true, time: now }])
|
||||
setMessage("")
|
||||
|
||||
setTimeout(() => {
|
||||
const replies = [
|
||||
"哈哈,你也玩这个游戏呀~",
|
||||
"我们加个好友吧",
|
||||
"你是什么段位呀?",
|
||||
"一起开黑吗?",
|
||||
"你声音好好听~",
|
||||
"今晚有空吗?一起打游戏呀",
|
||||
]
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
text: replies[Math.floor(Math.random() * replies.length)]!,
|
||||
isMe: false,
|
||||
time: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
},
|
||||
])
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
if (isCPChat && cpInfo) {
|
||||
return (
|
||||
<div className="h-screen bg-black flex flex-col">
|
||||
<div className="px-4 py-3 flex items-center justify-between border-b border-white/10 bg-gradient-to-r from-pink-900/30 to-purple-900/30 backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button" onClick={() => void navigate(-1)} className="active:scale-90 transition-transform" aria-label="返回">
|
||||
<ArrowLeft size={20} className="text-white" />
|
||||
</button>
|
||||
<div className="w-10 h-10 rounded-full overflow-hidden border-2 border-pink-500 relative shrink-0">
|
||||
<FillImage src={cpInfo.avatar} alt={cpInfo.name} className="h-10 w-10" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-bold text-sm text-white flex items-center gap-2">
|
||||
{cpInfo.name}
|
||||
<Heart size={12} className="text-pink-500 fill-pink-500" />
|
||||
</h1>
|
||||
<div className="flex items-center gap-1 text-[10px] text-pink-400 font-bold">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||
<span>在线</span>
|
||||
<span className="text-white/30 mx-1">|</span>
|
||||
<span className="text-white/50">{cpInfo.game}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button" className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center" aria-label="语音">
|
||||
<Phone size={16} className="text-white" />
|
||||
</button>
|
||||
<button type="button" className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center" aria-label="视频">
|
||||
<VideoIcon size={16} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isConnected && (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-24 h-24 rounded-full border-4 border-pink-500/30 mx-auto mb-4 relative overflow-hidden animate-pulse">
|
||||
<FillImage src={cpInfo.avatar} alt={cpInfo.name} className="h-24 w-24" />
|
||||
</div>
|
||||
<p className="text-white font-bold mb-2">正在连线 {cpInfo.name}...</p>
|
||||
<p className="text-white/50 text-sm">请稍候</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isConnected && (
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
<div className="text-center py-4">
|
||||
<div className="inline-flex items-center gap-2 bg-pink-500/10 border border-pink-500/30 rounded-full px-4 py-2">
|
||||
<Heart size={14} className="text-pink-500 fill-pink-500" />
|
||||
<span className="text-xs text-pink-400">已成功匹配,开始聊天吧~</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i} className={`flex gap-3 ${msg.isMe ? "flex-row-reverse" : ""}`}>
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden flex-shrink-0 relative">
|
||||
<FillImage src={msg.isMe ? "/my-avatar.jpg" : cpInfo.avatar} alt="Avatar" className="h-8 w-8" />
|
||||
</div>
|
||||
<div className={`max-w-[70%] ${msg.isMe ? "flex flex-col items-end" : ""}`}>
|
||||
<div
|
||||
className={`p-3 rounded-2xl text-sm ${
|
||||
msg.isMe ? "bg-pink-500 text-white rounded-tr-none" : "bg-zinc-800 text-white/90 rounded-tl-none"
|
||||
}`}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
<span className="text-[10px] text-white/30 mt-1">{msg.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isConnected && (
|
||||
<div className="p-3 bg-zinc-900/90 border-t border-white/10 flex items-end gap-3 pb-safe">
|
||||
<button type="button" className="p-2 rounded-full hover:bg-white/5" aria-label="麦克风">
|
||||
<Mic size={20} className="text-white/60" />
|
||||
</button>
|
||||
<div className="flex-1 bg-black/50 rounded-2xl min-h-[36px] flex items-center px-3 border border-pink-500/30">
|
||||
<input
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
|
||||
className="bg-transparent w-full text-sm text-white placeholder:text-white/30 outline-none py-2"
|
||||
placeholder="说点什么..."
|
||||
/>
|
||||
</div>
|
||||
<button type="button" onClick={sendMessage} className="p-2 rounded-full bg-pink-500 hover:bg-pink-400 transition-colors" aria-label="发送">
|
||||
<Send size={20} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-black flex flex-col">
|
||||
<div className="px-4 py-3 flex items-center justify-between border-b border-white/10 bg-zinc-900/50 backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button" onClick={() => void navigate(-1)} className="active:scale-90 transition-transform" aria-label="返回">
|
||||
<ArrowLeft size={20} className="text-white" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="font-bold text-sm text-white">电竞明星派对 - 玩值杯备战</h1>
|
||||
<div className="flex items-center gap-1 text-[10px] text-primary font-bold">
|
||||
<Users size={10} />
|
||||
<span>8/10人</span>
|
||||
<span className="text-white/30">|</span>
|
||||
<span className="text-green-400">匹配成功,已自动入群</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center" aria-label="更多">
|
||||
<MoreHorizontal size={18} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="w-full bg-zinc-900/80 border-b border-white/5 p-3 flex items-center gap-3 cursor-pointer hover:bg-zinc-800 transition-colors backdrop-blur-sm"
|
||||
onClick={() => void navigate("/live/101")}
|
||||
onKeyDown={(e) => e.key === "Enter" && void navigate("/live/101")}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="w-24 h-14 rounded-lg bg-black relative overflow-hidden border border-white/10 shrink-0">
|
||||
<FillImage src="/live-lol.jpg" alt="Live" className="h-14" />
|
||||
<div className="absolute top-1 left-1 px-1 rounded bg-primary text-[8px] font-bold text-white animate-pulse">LIVE</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xs font-bold text-white mb-1">正在直播:国服第一盲僧冲分</h3>
|
||||
<p className="text-[10px] text-primary">点击进入直播间一起互动 ></p>
|
||||
</div>
|
||||
<div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center shrink-0">
|
||||
<Video size={16} className="text-white/60" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
<div className="text-center text-[10px] text-white/30 my-4">12:30</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-zinc-800 overflow-hidden flex-shrink-0 relative">
|
||||
<FillImage src="/avatar-1.jpg" alt="User" className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="max-w-[70%]">
|
||||
<p className="text-[10px] text-white/50 mb-1 ml-1">带粉狂魔</p>
|
||||
<div className="bg-zinc-800 p-2 rounded-xl rounded-tl-none text-xs text-white/90">有没有一起打灵活的?缺个中单,来个会玩的。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 flex-row-reverse">
|
||||
<div className="w-8 h-8 rounded-full bg-zinc-800 overflow-hidden flex-shrink-0 relative">
|
||||
<FillImage src="/my-avatar.jpg" alt="User" className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="max-w-[70%] flex flex-col items-end">
|
||||
<div className="bg-primary p-2 rounded-xl rounded-tr-none text-xs text-white">我来我来,我亚索贼溜!</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-zinc-800 overflow-hidden flex-shrink-0 relative">
|
||||
<FillImage src="/avatar-2.jpg" alt="User" className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="max-w-[70%]">
|
||||
<p className="text-[10px] text-white/50 mb-1 ml-1">软辅妹妹</p>
|
||||
<div className="bg-zinc-800 p-2 rounded-xl rounded-tl-none text-xs text-white/90">托儿索别来沾边</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-zinc-900/90 border-t border-white/10 flex items-end gap-3 pb-safe">
|
||||
<button type="button" className="p-2 rounded-full hover:bg-white/5" aria-label="麦克风">
|
||||
<Mic size={20} className="text-white/60" />
|
||||
</button>
|
||||
<div className="flex-1 bg-black/50 rounded-2xl min-h-[36px] flex items-center px-3 border border-white/5">
|
||||
<input
|
||||
type="text"
|
||||
className="bg-transparent w-full text-sm text-white placeholder:text-white/30 outline-none py-2"
|
||||
placeholder="发送消息..."
|
||||
/>
|
||||
</div>
|
||||
<button type="button" className="p-2 rounded-full hover:bg-white/5" aria-label="更多">
|
||||
<Plus size={20} className="text-white/60" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
277
new/wz-app/src/pages/CoachDetailPage.tsx
Normal file
277
new/wz-app/src/pages/CoachDetailPage.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import { useState } from "react"
|
||||
import { useParams, useNavigate } from "react-router-dom"
|
||||
import { ArrowLeft, Star, Trophy, Users, Shield, Gamepad2, Award, MessageCircle, Check } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const COACH_DATA: Record<
|
||||
string,
|
||||
{
|
||||
name: string
|
||||
title: string
|
||||
avatar: string
|
||||
cover: string
|
||||
games: string[]
|
||||
rank: string
|
||||
orders: number
|
||||
rating: number
|
||||
goodRate: string
|
||||
bio: string
|
||||
tags: string[]
|
||||
services: { name: string; price: number; desc: string; duration: string; hot?: boolean }[]
|
||||
online: boolean
|
||||
}
|
||||
> = {
|
||||
"1": {
|
||||
name: "职业打野-小明",
|
||||
title: "前LPL职业选手",
|
||||
avatar: "/coach-avatar-1.jpg",
|
||||
cover: "/coach-cover-1.jpg",
|
||||
games: ["英雄联盟"],
|
||||
rank: "王者2000分",
|
||||
orders: 2456,
|
||||
rating: 4.9,
|
||||
goodRate: "98%",
|
||||
bio: "前LPL职业选手 · 国服第一打野 · 5年教学经验",
|
||||
tags: ["职业", "打野", "带飞", "耐心教学"],
|
||||
services: [
|
||||
{ name: "单局陪练", price: 80, desc: "边玩边教,即时指导", duration: "1局" },
|
||||
{ name: "5局套餐", price: 360, desc: "系统提升,优惠45元", duration: "5局", hot: true },
|
||||
{ name: "10局特训", price: 650, desc: "深度培训,包上分", duration: "10局" },
|
||||
],
|
||||
online: true,
|
||||
},
|
||||
"2": {
|
||||
name: "上分小助手",
|
||||
title: "国服路人王",
|
||||
avatar: "/coach-avatar-2.jpg",
|
||||
cover: "/coach-cover-2.jpg",
|
||||
games: ["王者荣耀", "和平精英"],
|
||||
rank: "荣耀王者100星",
|
||||
orders: 1823,
|
||||
rating: 4.8,
|
||||
goodRate: "97%",
|
||||
bio: "多区路人王 · 全英雄精通 · 上分效率高",
|
||||
tags: ["温柔", "耐心", "指导", "效率高"],
|
||||
services: [
|
||||
{ name: "单局陪练", price: 50, desc: "轻松开黑", duration: "1局" },
|
||||
{ name: "3局套餐", price: 135, desc: "连续开黑优惠", duration: "3局", hot: true },
|
||||
{ name: "包天陪练", price: 300, desc: "全天候服务", duration: "不限局数" },
|
||||
],
|
||||
online: true,
|
||||
},
|
||||
"3": {
|
||||
name: "FPS大神",
|
||||
title: "前职业选手",
|
||||
avatar: "/coach-avatar-3.jpg",
|
||||
cover: "/coach-cover-3.jpg",
|
||||
games: ["无畏契约", "三角洲行动"],
|
||||
rank: "超凡入圣",
|
||||
orders: 987,
|
||||
rating: 4.9,
|
||||
goodRate: "99%",
|
||||
bio: "FPS专业选手 · 精准枪法 · 战术意识强",
|
||||
tags: ["枪法", "意识", "专业", "战术指导"],
|
||||
services: [
|
||||
{ name: "单局陪练", price: 100, desc: "职业级指导", duration: "1局" },
|
||||
{ name: "技术特训", price: 450, desc: "系统提升枪法", duration: "5局", hot: true },
|
||||
{ name: "冲分套餐", price: 800, desc: "保证上段", duration: "10局" },
|
||||
],
|
||||
online: false,
|
||||
},
|
||||
"4": {
|
||||
name: "温柔陪练师",
|
||||
title: "全能型选手",
|
||||
avatar: "/coach-avatar-4.jpg",
|
||||
cover: "/coach-cover-4.jpg",
|
||||
games: ["王者荣耀", "英雄联盟"],
|
||||
rank: "多区王者",
|
||||
orders: 3201,
|
||||
rating: 5.0,
|
||||
goodRate: "100%",
|
||||
bio: "全能选手 · 声音好听 · 超级耐心",
|
||||
tags: ["声音好听", "技术强", "幽默", "好评如潮"],
|
||||
services: [
|
||||
{ name: "欢乐开黑", price: 60, desc: "快乐游戏为主", duration: "1局" },
|
||||
{ name: "技术提升", price: 270, desc: "边玩边学", duration: "5局", hot: true },
|
||||
{ name: "专属陪练", price: 500, desc: "定制化服务", duration: "10局" },
|
||||
],
|
||||
online: true,
|
||||
},
|
||||
}
|
||||
|
||||
export function CoachDetailPage() {
|
||||
const { id = "1" } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [selectedService, setSelectedService] = useState<number | null>(null)
|
||||
const [isPaying, setIsPaying] = useState(false)
|
||||
|
||||
const coach = COACH_DATA[id] ?? COACH_DATA["1"]!
|
||||
|
||||
const handleBook = () => {
|
||||
if (selectedService === null) {
|
||||
toast({ title: "请选择服务", description: "请先选择一个陪练服务套餐" })
|
||||
return
|
||||
}
|
||||
setIsPaying(true)
|
||||
}
|
||||
|
||||
const handlePay = () => {
|
||||
if (selectedService === null) return
|
||||
toast({
|
||||
title: "支付成功",
|
||||
description: `已成功预约${coach.name}的${coach.services[selectedService]!.name}`,
|
||||
})
|
||||
setIsPaying(false)
|
||||
void navigate("/messages")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-24 relative">
|
||||
<div className="h-48 relative">
|
||||
<FillImage src={coach.cover || "/placeholder.svg"} alt="Cover" className="h-48" />
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-background" />
|
||||
<button
|
||||
onClick={() => void navigate(-1)}
|
||||
className="absolute top-4 left-4 p-2 rounded-full bg-black/20 backdrop-blur-md text-white z-10"
|
||||
type="button"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-4 -mt-12 relative z-10">
|
||||
<div className="flex items-end justify-between mb-4">
|
||||
<div className="w-24 h-24 rounded-2xl border-4 border-background bg-muted relative overflow-hidden shadow-xl">
|
||||
<FillImage src={coach.avatar || "/placeholder.svg"} alt="Coach" className="h-24 w-24 rounded-2xl" />
|
||||
{coach.online && <div className="absolute bottom-0 right-0 w-5 h-5 rounded-full bg-green-500 border-2 border-background z-10" />}
|
||||
</div>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Button size="sm" variant="outline" className="h-8 text-xs gap-1 bg-transparent" type="button">
|
||||
<MessageCircle size={14} />
|
||||
咨询
|
||||
</Button>
|
||||
<Button size="sm" className="h-8 text-xs bg-green-500 gap-1 hover:bg-green-600" type="button" onClick={handleBook}>
|
||||
<Gamepad2 size={14} />
|
||||
立即预约
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h1 className="text-2xl font-bold">{coach.name}</h1>
|
||||
{coach.rating >= 4.9 && <Award className="text-yellow-400" size={20} />}
|
||||
</div>
|
||||
<p className="text-sm text-white/60 mb-3">{coach.bio}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-white/40">
|
||||
<span className="flex items-center gap-1">
|
||||
<Star size={12} className="text-primary fill-primary" />
|
||||
{coach.rating} 评分
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Trophy size={12} className="text-accent" />
|
||||
{coach.orders} 订单
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={12} className="text-secondary" />
|
||||
{coach.goodRate} 好评
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<h3 className="font-bold text-sm flex items-center gap-2">
|
||||
<Gamepad2 size={16} className="text-green-500" />
|
||||
服务项目
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{coach.services.map((service, i) => (
|
||||
<div
|
||||
key={service.name}
|
||||
className={`glass-card p-4 rounded-xl relative cursor-pointer transition-all ${selectedService === i ? "ring-2 ring-green-500 bg-green-500/10" : "hover:bg-white/5"}`}
|
||||
onClick={() => setSelectedService(i)}
|
||||
onKeyDown={(e) => e.key === "Enter" && setSelectedService(i)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{service.hot && (
|
||||
<span className="absolute top-2 right-2 px-2 py-0.5 rounded-full bg-green-500/20 text-green-400 text-[10px] font-bold">最热门</span>
|
||||
)}
|
||||
{selectedService === i && (
|
||||
<div className="absolute top-2 left-2 w-5 h-5 rounded-full bg-green-500 flex items-center justify-center z-10">
|
||||
<Check size={12} className="text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className={selectedService === i ? "pl-6" : ""}>
|
||||
<h4 className="font-bold mb-1">{service.name}</h4>
|
||||
<p className="text-xs text-white/60">{service.desc}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-green-500">{service.price}</div>
|
||||
<div className="text-[10px] text-white/50">元/{service.duration}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<h3 className="font-bold text-sm">陪练标签</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{coach.tags.map((tag) => (
|
||||
<span key={tag} className="px-3 py-1.5 rounded-full bg-green-500/10 border border-green-500/30 text-xs text-green-400">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 rounded-xl bg-gradient-to-r from-green-500/10 to-transparent border-l-4 border-green-500">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield size={24} className="text-green-500" />
|
||||
<div>
|
||||
<h4 className="font-bold text-sm">平台认证陪练师</h4>
|
||||
<p className="text-xs text-white/60">服务不满意,全额退款</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isPaying && selectedService !== null && (
|
||||
<div className="fixed inset-0 z-50 bg-black/80 flex items-end justify-center">
|
||||
<div className="w-full max-w-lg bg-background rounded-t-3xl p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold">确认订单</h3>
|
||||
<button onClick={() => setIsPaying(false)} className="text-white/40" type="button">
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
<div className="glass-card p-4 rounded-xl">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-muted relative overflow-hidden">
|
||||
<FillImage src={coach.avatar || "/placeholder.svg"} alt={coach.name} className="h-12 w-12" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold">{coach.name}</div>
|
||||
<div className="text-xs text-white/60">{coach.services[selectedService]!.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-3 border-t border-white/10">
|
||||
<span className="text-white/60">服务费用</span>
|
||||
<span className="text-2xl font-bold text-green-500">¥{coach.services[selectedService]!.price}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full h-12 bg-green-500 hover:bg-green-600 text-lg font-bold" type="button" onClick={handlePay}>
|
||||
立即支付
|
||||
</Button>
|
||||
<p className="text-center text-xs text-white/40">支付即表示同意《玩值电竞服务协议》</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
170
new/wz-app/src/pages/CoachListPage.tsx
Normal file
170
new/wz-app/src/pages/CoachListPage.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { ArrowLeft, Trophy, Star, Gamepad2, MessageCircle, Award } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const COACH_LIST = [
|
||||
{ id: "1", name: "职业打野-小明", title: "前LPL职业选手", avatar: "/coach-avatar-1.jpg", games: ["英雄联盟"], rank: "王者2000分", orders: 2456, rating: 4.9, price: 80, tags: ["职业", "打野", "带飞"], online: true },
|
||||
{ id: "2", name: "上分小助手", title: "国服路人王", avatar: "/coach-avatar-2.jpg", games: ["王者荣耀", "和平精英"], rank: "荣耀王者100星", orders: 1823, rating: 4.8, price: 50, tags: ["温柔", "耐心", "指导"], online: true },
|
||||
{ id: "3", name: "FPS大神", title: "前职业选手", avatar: "/coach-avatar-3.jpg", games: ["无畏契约", "三角洲"], rank: "超凡入圣", orders: 987, rating: 4.9, price: 100, tags: ["枪法", "意识", "专业"], online: false },
|
||||
{ id: "4", name: "温柔陪练师", title: "全能型选手", avatar: "/coach-avatar-4.jpg", games: ["王者荣耀", "英雄联盟"], rank: "多区王者", orders: 3201, rating: 5.0, price: 60, tags: ["声音好听", "技术强", "幽默"], online: true },
|
||||
] as const
|
||||
|
||||
type Coach = (typeof COACH_LIST)[number]
|
||||
|
||||
function CoachCard({
|
||||
coach,
|
||||
navigate,
|
||||
onBook,
|
||||
}: {
|
||||
coach: Coach
|
||||
navigate: ReturnType<typeof useNavigate>
|
||||
onBook: (name: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="glass-card p-4 rounded-xl hover:bg-white/5 transition-colors cursor-pointer"
|
||||
onClick={() => void navigate(`/coach/${coach.id}`)}
|
||||
onKeyDown={(e) => e.key === "Enter" && void navigate(`/coach/${coach.id}`)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 rounded-lg bg-muted relative overflow-hidden">
|
||||
<FillImage src={coach.avatar || "/placeholder.svg"} alt={coach.name} className="h-16 w-16" />
|
||||
</div>
|
||||
{coach.online && <div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-green-500 border-2 border-background" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-bold truncate">{coach.name}</h4>
|
||||
{(coach.rating === 5.0 || coach.rating >= 4.9) && <Award className="w-4 h-4 text-yellow-400" />}
|
||||
</div>
|
||||
<p className="text-xs text-white/60 mb-2">{coach.title}</p>
|
||||
<div className="flex items-center gap-3 text-xs text-white/50 mb-2">
|
||||
<span className="flex items-center gap-1">
|
||||
<Star size={12} className="text-primary fill-primary" />
|
||||
{coach.rating}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Trophy size={12} className="text-accent" />
|
||||
{coach.orders}单
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{coach.tags.map((tag) => (
|
||||
<span key={tag} className="px-2 py-0.5 rounded-full bg-white/5 text-[10px] text-white/60">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-green-500">{coach.price}</div>
|
||||
<div className="text-[10px] text-white/50">元/局</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex-1 h-8 text-xs border-white/10 hover:bg-white/5 bg-transparent"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
void navigate(`/coach/${coach.id}`)
|
||||
}}
|
||||
>
|
||||
<MessageCircle size={14} className="mr-1" />
|
||||
查看详情
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 h-8 text-xs bg-green-500 hover:bg-green-600"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onBook(coach.name)
|
||||
}}
|
||||
>
|
||||
<Gamepad2 size={14} className="mr-1" />
|
||||
立即预约
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CoachListPage() {
|
||||
const navigate = useNavigate()
|
||||
const handleBookCoach = (coachName: string) => {
|
||||
toast({ title: "预约成功", description: `已成功预约${coachName}的陪练服务` })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="p-2 -ml-2 hover:bg-white/5 rounded-lg transition-colors" type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">游戏陪练</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="glass-card p-4 rounded-xl border-l-4 border-green-500">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-bold text-sm">专业陪练服务</h3>
|
||||
<span className="text-xs text-white/50">1000+ 陪练师</span>
|
||||
</div>
|
||||
<p className="text-xs text-white/60 mb-3">前职业选手、路人王陪你上分,技术提升看得见</p>
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
<div className="bg-white/5 rounded-lg p-2">
|
||||
<div className="text-xs font-bold text-green-500">专业认证</div>
|
||||
<div className="text-[10px] text-white/50 mt-0.5">实力保障</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2">
|
||||
<div className="text-xs font-bold text-blue-500">快速上分</div>
|
||||
<div className="text-[10px] text-white/50 mt-0.5">胜率提升</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2">
|
||||
<div className="text-xs font-bold text-purple-500">技术指导</div>
|
||||
<div className="text-[10px] text-white/50 mt-0.5">边玩边学</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="hot" className="w-full">
|
||||
<TabsList className="w-full bg-white/5 p-1 rounded-xl">
|
||||
<TabsTrigger value="hot" className="flex-1 text-xs">
|
||||
热门陪练
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pro" className="flex-1 text-xs">
|
||||
职业选手
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="king" className="flex-1 text-xs">
|
||||
王者大神
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="hot" className="mt-4 space-y-3">
|
||||
{COACH_LIST.map((coach) => (
|
||||
<CoachCard key={coach.id} coach={coach} navigate={navigate} onBook={handleBookCoach} />
|
||||
))}
|
||||
</TabsContent>
|
||||
<TabsContent value="pro" className="mt-4 space-y-3">
|
||||
{COACH_LIST.filter((c) => c.title.includes("职业")).map((coach) => (
|
||||
<CoachCard key={coach.id} coach={coach} navigate={navigate} onBook={handleBookCoach} />
|
||||
))}
|
||||
</TabsContent>
|
||||
<TabsContent value="king" className="mt-4 space-y-3">
|
||||
{COACH_LIST.filter((c) => c.rank.includes("王者") || c.rating === 5.0).map((coach) => (
|
||||
<CoachCard key={coach.id} coach={coach} navigate={navigate} onBook={handleBookCoach} />
|
||||
))}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
104
new/wz-app/src/pages/CouponsPage.tsx
Normal file
104
new/wz-app/src/pages/CouponsPage.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { ArrowLeft, Ticket, Clock, ChevronRight } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
export function CouponsPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const coupons = [
|
||||
{ id: 1, name: "电竞酒店满200减50", type: "酒店", discount: "50", condition: "满200可用", expires: "2025-02-28" },
|
||||
{ id: 2, name: "网咖通用8折券", type: "网咖", discount: "8折", condition: "无门槛", expires: "2025-03-15" },
|
||||
{ id: 3, name: "商城满100减20", type: "商城", discount: "20", condition: "满100可用", expires: "2025-02-20" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => void navigate(-1)}
|
||||
className="p-2 -ml-2 hover:bg-white/5 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">我的卡券</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4">
|
||||
<Tabs defaultValue="available" className="w-full">
|
||||
<TabsList className="w-full bg-white/5 p-1 rounded-xl mb-4">
|
||||
<TabsTrigger value="available" className="flex-1 text-xs">
|
||||
可使用
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="used" className="flex-1 text-xs">
|
||||
已使用
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="expired" className="flex-1 text-xs">
|
||||
已过期
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="available" className="space-y-3">
|
||||
{coupons.map((coupon) => (
|
||||
<div
|
||||
key={coupon.id}
|
||||
className="glass-card rounded-xl overflow-hidden border border-primary/20 hover:border-primary/40 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="w-28 bg-gradient-to-br from-primary to-purple-600 p-4 flex flex-col items-center justify-center text-white relative">
|
||||
<Ticket className="absolute top-2 right-2 opacity-20" size={40} />
|
||||
<div className="text-3xl font-bold z-10">
|
||||
{coupon.discount.includes("折") ? coupon.discount : `¥${coupon.discount}`}
|
||||
</div>
|
||||
<div className="text-[10px] mt-1 z-10">{coupon.type}</div>
|
||||
</div>
|
||||
<div className="flex-1 p-3 flex flex-col justify-between">
|
||||
<div>
|
||||
<h4 className="font-bold text-sm mb-1">{coupon.name}</h4>
|
||||
<p className="text-xs text-white/50">{coupon.condition}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-[10px] text-white/30">
|
||||
<Clock size={10} />
|
||||
{coupon.expires}到期
|
||||
</div>
|
||||
<Button size="sm" className="h-6 text-[10px] px-3">
|
||||
立即使用
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="used" className="text-center py-12">
|
||||
<Ticket className="w-12 h-12 text-white/20 mx-auto mb-3" />
|
||||
<p className="text-sm text-white/40">暂无已使用的卡券</p>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="expired" className="text-center py-12">
|
||||
<Ticket className="w-12 h-12 text-white/20 mx-auto mb-3" />
|
||||
<p className="text-sm text-white/40">暂无过期的卡券</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-6 glass-card p-4 rounded-xl border border-primary/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-bold text-sm mb-1">领取更多优惠券</h3>
|
||||
<p className="text-xs text-white/50">每日签到可领取专属福利</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-primary text-primary hover:bg-primary/10 bg-transparent"
|
||||
>
|
||||
去领取 <ChevronRight size={14} className="ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
295
new/wz-app/src/pages/CourseDetailPage.tsx
Normal file
295
new/wz-app/src/pages/CourseDetailPage.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import { useState } from "react"
|
||||
import { useParams, useNavigate, Link } from "react-router-dom"
|
||||
import { ArrowLeft, Star, Clock, Users, PlayCircle, Lock, Share2, Coins } from "lucide-react"
|
||||
import { useAppContext } from "@/components/providers/app-provider"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
|
||||
const courseData: Record<string, {
|
||||
id: string
|
||||
streamerId: string
|
||||
title: string
|
||||
cover: string
|
||||
streamer: { name: string; avatar: string; title: string }
|
||||
price: number
|
||||
originalPrice: number
|
||||
students: number
|
||||
duration: string
|
||||
rating: number
|
||||
lessons: { title: string; duration: string; isFree: boolean }[]
|
||||
}> = {
|
||||
"1": {
|
||||
id: "1",
|
||||
streamerId: "1",
|
||||
title: "打野进阶课:从入门到王者",
|
||||
cover: "/live-lol.jpg",
|
||||
streamer: { name: "GM-远洋", avatar: "/streamer-1.jpg", title: "国服第一盲僧" },
|
||||
price: 299,
|
||||
originalPrice: 499,
|
||||
students: 1205,
|
||||
duration: "8节课 (120分钟)",
|
||||
rating: 4.9,
|
||||
lessons: [
|
||||
{ title: "01. 打野基础思路与路线规划", duration: "15:00", isFree: true },
|
||||
{ title: "02. 如何高效刷野与反野", duration: "18:30", isFree: false },
|
||||
{ title: "03. Gank时机与路线选择", duration: "20:00", isFree: false },
|
||||
{ title: "04. 控龙与峡谷先锋运营", duration: "16:45", isFree: false },
|
||||
{ title: "05. 团战切入时机详解", duration: "22:10", isFree: false },
|
||||
{ title: "06. 逆风局如何翻盘", duration: "19:20", isFree: false },
|
||||
],
|
||||
},
|
||||
"2": {
|
||||
id: "2",
|
||||
streamerId: "2",
|
||||
title: "无畏契约枪法训练营",
|
||||
cover: "/live-valorant.jpg",
|
||||
streamer: { name: "卡若", avatar: "/streamer-2.jpg", title: "前职业选手" },
|
||||
price: 399,
|
||||
originalPrice: 599,
|
||||
students: 856,
|
||||
duration: "10节课 (150分钟)",
|
||||
rating: 4.8,
|
||||
lessons: [
|
||||
{ title: "01. 准星设置与灵敏度调整", duration: "12:00", isFree: true },
|
||||
{ title: "02. 爆头线与预瞄点位", duration: "20:00", isFree: false },
|
||||
{ title: "03. 各类枪械弹道控制", duration: "25:00", isFree: false },
|
||||
{ title: "04. 常用身法技巧详解", duration: "18:00", isFree: false },
|
||||
{ title: "05. 地图道具使用教学", duration: "22:00", isFree: false },
|
||||
],
|
||||
},
|
||||
"3": {
|
||||
id: "3",
|
||||
streamerId: "3",
|
||||
title: "和平精英四指操作教学",
|
||||
cover: "/live-pubg.jpg",
|
||||
streamer: { name: "小美", avatar: "/streamer-3.jpg", title: "全国冠军" },
|
||||
price: 199,
|
||||
originalPrice: 299,
|
||||
students: 2340,
|
||||
duration: "6节课 (90分钟)",
|
||||
rating: 4.9,
|
||||
lessons: [
|
||||
{ title: "01. 四指操作键位设置", duration: "10:00", isFree: true },
|
||||
{ title: "02. 基础移动与射击配合", duration: "15:00", isFree: false },
|
||||
{ title: "03. 压枪技巧与弹道控制", duration: "20:00", isFree: false },
|
||||
{ title: "04. 载具战斗与转移技巧", duration: "18:00", isFree: false },
|
||||
],
|
||||
},
|
||||
"4": {
|
||||
id: "4",
|
||||
streamerId: "4",
|
||||
title: "原神深渊满星攻略",
|
||||
cover: "/live-genshin.jpg",
|
||||
streamer: { name: "星辰", avatar: "/streamer-4.jpg", title: "原神攻略大神" },
|
||||
price: 149,
|
||||
originalPrice: 249,
|
||||
students: 3450,
|
||||
duration: "5节课 (75分钟)",
|
||||
rating: 4.7,
|
||||
lessons: [
|
||||
{ title: "01. 深渊机制与阵容搭配", duration: "18:00", isFree: true },
|
||||
{ title: "02. 各层怪物特性解析", duration: "20:00", isFree: false },
|
||||
{ title: "03. 圣遗物选择与词条优化", duration: "22:00", isFree: false },
|
||||
{ title: "04. 手法与连招技巧", duration: "15:00", isFree: false },
|
||||
],
|
||||
},
|
||||
"5": {
|
||||
id: "5",
|
||||
streamerId: "5",
|
||||
title: "王者荣耀上分秘籍",
|
||||
cover: "/live-hok.jpg",
|
||||
streamer: { name: "阿杰", avatar: "/streamer-5.jpg", title: "国服百星" },
|
||||
price: 249,
|
||||
originalPrice: 399,
|
||||
students: 4560,
|
||||
duration: "8节课 (120分钟)",
|
||||
rating: 4.8,
|
||||
lessons: [
|
||||
{ title: "01. 段位机制与上分心态", duration: "12:00", isFree: true },
|
||||
{ title: "02. 各路位选英雄推荐", duration: "18:00", isFree: false },
|
||||
{ title: "03. 对线技巧与兵线控制", duration: "20:00", isFree: false },
|
||||
{ title: "04. 节奏把控与支援时机", duration: "22:00", isFree: false },
|
||||
{ title: "05. 团战站位与技能释放", duration: "25:00", isFree: false },
|
||||
],
|
||||
},
|
||||
"6": {
|
||||
id: "6",
|
||||
streamerId: "6",
|
||||
title: "魔兽世界团本攻略",
|
||||
cover: "/live-wow.jpg",
|
||||
streamer: { name: "暗影", avatar: "/streamer-6.jpg", title: "首杀团队成员" },
|
||||
price: 499,
|
||||
originalPrice: 799,
|
||||
students: 890,
|
||||
duration: "12节课 (200分钟)",
|
||||
rating: 4.9,
|
||||
lessons: [
|
||||
{ title: "01. 团队配置与职责分工", duration: "15:00", isFree: true },
|
||||
{ title: "02. BOSS机制详细解析", duration: "30:00", isFree: false },
|
||||
{ title: "03. 治疗与坦克配合要点", duration: "25:00", isFree: false },
|
||||
{ title: "04. DPS输出循环优化", duration: "28:00", isFree: false },
|
||||
{ title: "05. 困难模式特殊机制", duration: "35:00", isFree: false },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export function CourseDetailPage() {
|
||||
const { id = "1" } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { pay } = useAppContext()
|
||||
const [showShareModal, setShowShareModal] = useState(false)
|
||||
const course = courseData[id] ?? courseData["1"]!
|
||||
|
||||
const handleBuy = () => {
|
||||
const ok = pay(course.price, course.title, "course", course.cover)
|
||||
if (ok) {
|
||||
toast({ title: "购买成功" })
|
||||
void navigate("/messages")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-28">
|
||||
<div className="relative aspect-video w-full">
|
||||
<FillImage src={course.cover} alt="Course Cover" className="aspect-video" />
|
||||
<div className="absolute inset-0 bg-black/30" />
|
||||
<Link
|
||||
to="/"
|
||||
className="absolute top-4 left-4 w-10 h-10 rounded-full bg-black/50 backdrop-blur-md flex items-center justify-center text-white hover:bg-black/70 transition-colors"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowShareModal(true)}
|
||||
className="absolute top-4 right-4 w-10 h-10 rounded-full bg-black/50 backdrop-blur-md flex items-center justify-center text-white hover:bg-black/70 transition-colors"
|
||||
>
|
||||
<Share2 size={20} />
|
||||
</button>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-16 h-16 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center cursor-pointer hover:scale-110 transition-transform">
|
||||
<PlayCircle size={48} className="text-white fill-white/20" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-4 right-4 px-3 py-1.5 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center gap-1">
|
||||
<span className="text-black font-bold text-lg">{course.price}</span>
|
||||
<Coins size={14} className="text-black/70" />
|
||||
<span className="text-black/70 text-xs">币</span>
|
||||
<span className="text-black/50 text-xs ml-2 line-through">{course.originalPrice}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="mb-4">
|
||||
<h1 className="text-xl font-bold mb-2">{course.title}</h1>
|
||||
<div className="flex items-center gap-4 text-xs text-white/60">
|
||||
<span className="flex items-center gap-1">
|
||||
<Star size={14} className="text-primary fill-primary" />
|
||||
{course.rating}分
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={14} /> {course.duration}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={14} /> {course.students}人已学
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to={`/live/${course.streamerId}`}
|
||||
className="flex items-center gap-3 p-3 rounded-xl bg-white/5 mb-6 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-full overflow-hidden border-2 border-primary relative shrink-0">
|
||||
<FillImage src={course.streamer.avatar} alt="Instructor" className="h-12 w-12 rounded-full" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-bold text-sm">{course.streamer.name}</h3>
|
||||
<p className="text-xs text-white/50">{course.streamer.title}</p>
|
||||
</div>
|
||||
<span className="px-4 py-1.5 rounded-full border border-primary text-primary text-xs font-bold">关注</span>
|
||||
</Link>
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="font-bold text-lg mb-4">课程目录</h3>
|
||||
<div className="space-y-3">
|
||||
{course.lessons.map((lesson, i) => (
|
||||
<div key={lesson.title} className="flex items-center gap-4 p-3 rounded-xl hover:bg-white/5 transition-colors cursor-pointer group">
|
||||
<div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-xs font-bold text-white/50 group-hover:bg-primary group-hover:text-black transition-colors">
|
||||
{i + 1}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-sm font-medium mb-1 group-hover:text-primary transition-colors">{lesson.title}</h4>
|
||||
<span className="text-xs text-white/40">{lesson.duration}</span>
|
||||
</div>
|
||||
{lesson.isFree ? (
|
||||
<div className="px-2 py-0.5 rounded bg-green-500/20 text-green-500 text-[10px] font-bold">试看</div>
|
||||
) : (
|
||||
<Lock size={16} className="text-white/30 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showShareModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center">
|
||||
<button type="button" className="absolute inset-0 bg-black/60 cursor-default" onClick={() => setShowShareModal(false)} aria-label="关闭" />
|
||||
<div className="relative w-full bg-zinc-900 rounded-t-3xl p-6 animate-in slide-in-from-bottom-4">
|
||||
<h3 className="text-lg font-bold mb-4 text-center">分享课程</h3>
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
{[
|
||||
{ name: "微信", icon: "💬", color: "bg-green-500" },
|
||||
{ name: "朋友圈", icon: "🌐", color: "bg-green-600" },
|
||||
{ name: "QQ", icon: "🐧", color: "bg-blue-500" },
|
||||
{ name: "微博", icon: "📱", color: "bg-red-500" },
|
||||
].map((platform) => (
|
||||
<button
|
||||
key={platform.name}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowShareModal(false)
|
||||
toast({ title: "已选择", description: platform.name })
|
||||
}}
|
||||
className="flex flex-col items-center gap-2"
|
||||
>
|
||||
<div className={`w-14 h-14 rounded-full ${platform.color} flex items-center justify-center text-2xl`}>{platform.icon}</div>
|
||||
<span className="text-xs text-white/70">{platform.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowShareModal(false)}
|
||||
className="w-full py-3 rounded-xl bg-white/10 text-white font-medium"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="fixed bottom-0 left-0 right-0 z-40 border-t border-white/10 bg-background/95 backdrop-blur-md px-4 py-3 flex items-center justify-between gap-3 safe-pb">
|
||||
<div>
|
||||
<div className="text-xs text-white/50">玩值价</div>
|
||||
<div className="text-lg font-bold text-primary">
|
||||
{course.price} <span className="text-sm font-normal text-white/50">币</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void navigate(`/chat/course-${id}`)}
|
||||
className="px-4 py-2 rounded-full border border-white/20 text-sm"
|
||||
>
|
||||
咨询
|
||||
</button>
|
||||
<button type="button" onClick={handleBuy} className="px-6 py-2 rounded-full bg-primary text-black text-sm font-bold">
|
||||
立即购买
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
176
new/wz-app/src/pages/CreatorApplyPage.tsx
Normal file
176
new/wz-app/src/pages/CreatorApplyPage.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import type { FormEvent } from "react"
|
||||
import { useState } from "react"
|
||||
import { ArrowLeft, Star, Upload, CheckCircle2 } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
|
||||
export function CreatorApplyPage() {
|
||||
const navigate = useNavigate()
|
||||
const [formData, setFormData] = useState({
|
||||
realName: "",
|
||||
phone: "",
|
||||
game: "",
|
||||
rank: "",
|
||||
experience: "",
|
||||
price: "",
|
||||
})
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
toast({
|
||||
title: "申请已提交",
|
||||
description: "我们会在3个工作日内审核您的申请",
|
||||
})
|
||||
setTimeout(() => void navigate("/profile"), 1500)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="p-2 -ml-2 hover:bg-white/5 rounded-lg transition-colors" type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">申请大神</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="glass-card p-4 rounded-xl border-l-4 border-primary">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
|
||||
<Star className="text-primary" size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-sm mb-1">成为大神,开启副业</h3>
|
||||
<p className="text-xs text-white/60 leading-relaxed">
|
||||
通过审核后,您可以在平台接单陪玩、开设课程、直播带货,月入过万不是梦!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ icon: "💰", label: "高额收益", desc: "自由定价" },
|
||||
{ icon: "🎯", label: "精准匹配", desc: "智能推荐" },
|
||||
{ icon: "🛡️", label: "平台保障", desc: "安全可靠" },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="glass-card p-3 rounded-xl text-center">
|
||||
<div className="text-2xl mb-1">{item.icon}</div>
|
||||
<h4 className="text-xs font-bold mb-0.5">{item.label}</h4>
|
||||
<p className="text-[10px] text-white/50">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="glass-card p-4 rounded-xl space-y-4">
|
||||
<h3 className="font-bold text-sm flex items-center gap-2">
|
||||
<CheckCircle2 size={16} className="text-green-400" />
|
||||
基本信息
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">真实姓名</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.realName}
|
||||
onChange={(e) => setFormData({ ...formData, realName: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-sm focus:border-primary focus:outline-none"
|
||||
placeholder="请输入真实姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">联系电话</label>
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-sm focus:border-primary focus:outline-none"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">擅长游戏</label>
|
||||
<select
|
||||
required
|
||||
value={formData.game}
|
||||
onChange={(e) => setFormData({ ...formData, game: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-sm focus:border-primary focus:outline-none"
|
||||
>
|
||||
<option value="">请选择游戏</option>
|
||||
<option value="lol">英雄联盟</option>
|
||||
<option value="valorant">无畏契约</option>
|
||||
<option value="hok">王者荣耀</option>
|
||||
<option value="csgo">CS:GO</option>
|
||||
<option value="wow">魔兽世界</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">游戏段位</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.rank}
|
||||
onChange={(e) => setFormData({ ...formData, rank: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-sm focus:border-primary focus:outline-none"
|
||||
placeholder="如:王者100星、传奇"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">从业经验</label>
|
||||
<textarea
|
||||
required
|
||||
value={formData.experience}
|
||||
onChange={(e) => setFormData({ ...formData, experience: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-sm focus:border-primary focus:outline-none resize-none"
|
||||
rows={3}
|
||||
placeholder="简单介绍您的游戏经历和成就"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">期望价格(币/局)</label>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
value={formData.price}
|
||||
onChange={(e) => setFormData({ ...formData, price: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-sm focus:border-primary focus:outline-none"
|
||||
placeholder="建议30-200币/局"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 rounded-xl">
|
||||
<h3 className="font-bold text-sm mb-3 flex items-center gap-2">
|
||||
<Upload size={16} className="text-blue-400" />
|
||||
上传证明材料
|
||||
</h3>
|
||||
<div className="border-2 border-dashed border-white/10 rounded-lg p-6 text-center hover:border-primary/50 transition-colors cursor-pointer">
|
||||
<Upload className="mx-auto mb-2 text-white/40" size={32} />
|
||||
<p className="text-xs text-white/60">点击上传游戏截图或段位证明</p>
|
||||
<p className="text-[10px] text-white/40 mt-1">支持 JPG、PNG 格式,最大 5MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full h-12 text-base font-bold bg-primary hover:bg-primary/90">
|
||||
提交申请
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-white/40 text-center leading-relaxed">
|
||||
提交申请即表示您同意《大神服务协议》和《平台规则》
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
136
new/wz-app/src/pages/CustomerServicePage.tsx
Normal file
136
new/wz-app/src/pages/CustomerServicePage.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import { useState, type KeyboardEvent } from "react"
|
||||
import { ArrowLeft, MessageCircle, Phone, Mail, Clock, ChevronRight, Send } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
type ChatMsg = { id: number; type: "system" | "user"; content: string; time: string }
|
||||
|
||||
export function CustomerServicePage() {
|
||||
const navigate = useNavigate()
|
||||
const [message, setMessage] = useState("")
|
||||
const [messages, setMessages] = useState<ChatMsg[]>([
|
||||
{ id: 1, type: "system", content: "您好!欢迎使用玩值电竞客服中心,请问有什么可以帮助您的?", time: "10:00" },
|
||||
])
|
||||
|
||||
const faqList = [
|
||||
{ q: "如何充值玩值币?", a: "点击首页右上角钱包图标,选择充值金额即可" },
|
||||
{ q: "如何申请退款?", a: "进入订单详情页,点击申请退款按钮" },
|
||||
{ q: "账号典当如何操作?", a: "进入商城-账号典当,填写账号信息提交评估" },
|
||||
{ q: "如何联系主播?", a: "在主播主页点击私信按钮即可发送消息" },
|
||||
]
|
||||
|
||||
const sendMessage = () => {
|
||||
if (!message.trim()) return
|
||||
|
||||
const newMsg: ChatMsg = {
|
||||
id: messages.length + 1,
|
||||
type: "user",
|
||||
content: message,
|
||||
time: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
}
|
||||
setMessages((prev) => [...prev, newMsg])
|
||||
setMessage("")
|
||||
|
||||
setTimeout(() => {
|
||||
const reply: ChatMsg = {
|
||||
id: messages.length + 2,
|
||||
type: "system",
|
||||
content: "感谢您的咨询,客服正在为您处理中,请稍候...",
|
||||
time: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
}
|
||||
setMessages((prev) => [...prev, reply])
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") sendMessage()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
<header className="sticky top-0 z-40 bg-background/95 backdrop-blur-sm border-b border-border px-4 py-3 flex items-center gap-4">
|
||||
<button onClick={() => void navigate(-1)} className="p-2 hover:bg-foreground/10 rounded-full transition-colors">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">客服中心</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 grid grid-cols-3 gap-3">
|
||||
<a href="tel:400-888-8888" className="bg-card rounded-xl p-4 border border-border text-center">
|
||||
<Phone size={24} className="mx-auto mb-2 text-primary" />
|
||||
<span className="text-xs text-foreground/70">电话客服</span>
|
||||
</a>
|
||||
<button type="button" className="bg-card rounded-xl p-4 border border-border text-center">
|
||||
<MessageCircle size={24} className="mx-auto mb-2 text-green-500" />
|
||||
<span className="text-xs text-foreground/70">在线客服</span>
|
||||
</button>
|
||||
<a href="mailto:support@wanzhi.com" className="bg-card rounded-xl p-4 border border-border text-center">
|
||||
<Mail size={24} className="mx-auto mb-2 text-blue-500" />
|
||||
<span className="text-xs text-foreground/70">邮件反馈</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="px-4 mb-4">
|
||||
<h3 className="text-sm font-bold mb-3 flex items-center gap-2">
|
||||
<Clock size={14} className="text-primary" />
|
||||
常见问题
|
||||
</h3>
|
||||
<div className="bg-card rounded-xl border border-border overflow-hidden">
|
||||
{faqList.map((faq, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
className="w-full flex items-center justify-between p-4 border-b border-border last:border-0 text-left"
|
||||
onClick={() => {
|
||||
setMessages((prev) => {
|
||||
const t = new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
|
||||
return [
|
||||
...prev,
|
||||
{ id: prev.length + 1, type: "user", content: faq.q, time: t },
|
||||
{ id: prev.length + 2, type: "system", content: faq.a, time: t },
|
||||
]
|
||||
})
|
||||
}}
|
||||
>
|
||||
<span className="text-sm text-foreground/80">{faq.q}</span>
|
||||
<ChevronRight size={16} className="text-foreground/30" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-4 space-y-3 overflow-y-auto pb-20 min-h-0">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.type === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl px-4 py-2 ${
|
||||
msg.type === "user" ? "bg-primary text-white" : "bg-card border border-border"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm">{msg.content}</p>
|
||||
<p className={`text-[10px] mt-1 ${msg.type === "user" ? "text-white/60" : "text-foreground/40"}`}>
|
||||
{msg.time}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-background border-t border-border p-4">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="请输入您的问题..."
|
||||
className="flex-1 bg-foreground/5 border-foreground/10"
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<Button type="button" onClick={sendMessage} className="bg-primary">
|
||||
<Send size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
140
new/wz-app/src/pages/DressUpPage.tsx
Normal file
140
new/wz-app/src/pages/DressUpPage.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { ArrowLeft, TrendingUp, ShoppingCart, Sparkles } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const avatars = [
|
||||
{ id: 1, name: "赛博朋克战士", price: 98, rarity: "传说", sales: 1205, image: "/avatar-cyberpunk.jpg" },
|
||||
{ id: 2, name: "甜心电竞少女", price: 68, rarity: "稀有", sales: 3420, image: "/avatar-cute.jpg" },
|
||||
{ id: 3, name: "暗影刺客", price: 128, rarity: "传说", sales: 856, image: "/avatar-assassin.jpg" },
|
||||
{ id: 4, name: "机甲战神", price: 88, rarity: "史诗", sales: 2100, image: "/avatar-mecha.jpg" },
|
||||
]
|
||||
|
||||
const decorations = [
|
||||
{ id: 101, name: "星空边框", price: 38, type: "边框", image: "/frame-starry.jpg" },
|
||||
{ id: 102, name: "炫光特效", price: 58, type: "特效", image: "/effect-neon.jpg" },
|
||||
{ id: 103, name: "王者徽章", price: 48, type: "徽章", image: "/badge-gold.jpg" },
|
||||
]
|
||||
|
||||
export function DressUpPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="p-2 -ml-2 hover:bg-white/5 rounded-lg transition-colors">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">我的装扮</h1>
|
||||
<div className="ml-auto flex items-center gap-2 text-xs bg-white/5 px-3 py-1.5 rounded-full">
|
||||
<Sparkles size={14} className="text-yellow-400" />
|
||||
<span className="text-white/80">数字头像商城</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="glass-card p-6 rounded-2xl mb-6 text-center border border-primary/20">
|
||||
<div className="w-24 h-24 mx-auto mb-3 rounded-full border-4 border-primary p-1 relative">
|
||||
<FillImage src="/avatar-cyberpunk.jpg" alt="Current Avatar" className="h-24 w-24 rounded-full" />
|
||||
<div className="absolute -bottom-2 left-1/2 -translate-x-1/2 bg-primary text-[10px] px-2 py-0.5 rounded-full text-white font-bold">
|
||||
LV.8
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="font-bold mb-1">当前形象</h3>
|
||||
<p className="text-xs text-white/50 mb-4">展示您的独特个性</p>
|
||||
<Button size="sm" variant="outline" className="border-white/20 hover:bg-white/5 bg-transparent">
|
||||
更换头像
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="avatar" className="w-full">
|
||||
<TabsList className="w-full bg-white/5 p-1 rounded-xl mb-4">
|
||||
<TabsTrigger value="avatar" className="flex-1 text-xs">
|
||||
数字头像
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="decoration" className="flex-1 text-xs">
|
||||
装饰道具
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="my" className="flex-1 text-xs">
|
||||
我的藏品
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="avatar" className="space-y-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-bold text-sm">热门头像</h3>
|
||||
<button type="button" className="text-xs text-primary">
|
||||
查看全部 <TrendingUp size={12} className="inline ml-1" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{avatars.map((avatar) => (
|
||||
<div
|
||||
key={avatar.id}
|
||||
className="glass-card p-3 rounded-xl hover:bg-white/10 transition-colors cursor-pointer"
|
||||
onClick={() =>
|
||||
toast({
|
||||
title: "购买成功",
|
||||
description: `已获得 ${avatar.name},消费 ¥${avatar.price}`,
|
||||
})
|
||||
}
|
||||
onKeyDown={(e) => e.key === "Enter" && e.currentTarget.click()}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="relative w-full aspect-square rounded-lg overflow-hidden mb-2 bg-zinc-800">
|
||||
<FillImage src={avatar.image || "/placeholder.svg"} alt={avatar.name} className="h-full" />
|
||||
<div className="absolute top-2 right-2 bg-yellow-500/90 text-black text-[9px] px-1.5 py-0.5 rounded font-bold">
|
||||
{avatar.rarity}
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="font-bold text-sm mb-1 truncate">{avatar.name}</h4>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-white/50">{avatar.sales}人购买</span>
|
||||
<span className="text-primary font-bold">¥{avatar.price}</span>
|
||||
</div>
|
||||
<Button size="sm" className="w-full mt-2 h-7 text-xs bg-primary hover:bg-primary/90">
|
||||
<ShoppingCart size={12} className="mr-1" /> 购买
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="decoration" className="space-y-3">
|
||||
<div className="grid gap-3">
|
||||
{decorations.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="glass-card p-3 rounded-xl flex items-center gap-3 hover:bg-white/10 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-lg overflow-hidden bg-zinc-800 relative">
|
||||
<FillImage src={item.image || "/placeholder.svg"} alt={item.name} className="h-full" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-bold text-sm mb-1">{item.name}</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] bg-white/10 px-2 py-0.5 rounded text-white/60">{item.type}</span>
|
||||
<span className="text-primary font-bold text-sm">¥{item.price}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" className="h-8 text-xs">
|
||||
购买
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="my" className="text-center py-12">
|
||||
<Sparkles className="w-12 h-12 text-white/20 mx-auto mb-3" />
|
||||
<p className="text-sm text-white/40">暂无藏品</p>
|
||||
<p className="text-xs text-white/30 mt-1">购买数字头像和装扮后会显示在这里</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
103
new/wz-app/src/pages/EventsTodayPage.tsx
Normal file
103
new/wz-app/src/pages/EventsTodayPage.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { ArrowLeft, Clock, Users, Trophy, MapPin } from "lucide-react"
|
||||
import { useNavigate, Link } from "react-router-dom"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const events = [
|
||||
{
|
||||
id: 1,
|
||||
title: "2025 玩值杯全明星邀请赛",
|
||||
game: "英雄联盟",
|
||||
time: "20:00",
|
||||
status: "即将开始" as const,
|
||||
teams: ["RNG", "EDG"] as [string, string],
|
||||
venue: "上海梅赛德斯奔驰中心",
|
||||
prize: "100万",
|
||||
viewers: "25.6w",
|
||||
image: "/tournament-event-today.jpg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "王者荣耀职业联赛",
|
||||
game: "王者荣耀",
|
||||
time: "18:30",
|
||||
status: "直播中" as const,
|
||||
teams: ["AG超玩会", "QGhappy"] as [string, string],
|
||||
venue: "线上赛",
|
||||
prize: "50万",
|
||||
viewers: "18.2w",
|
||||
image: "/tournament-event-today.jpg",
|
||||
},
|
||||
]
|
||||
|
||||
export function EventsTodayPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 bg-[#0D0D15]/90 backdrop-blur-md border-b border-white/5 px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="p-1 hover:bg-white/10 rounded-full transition-colors">
|
||||
<ArrowLeft size={20} className="text-white" />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg text-white">今日赛事</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
{events.map((event) => (
|
||||
<Link
|
||||
key={event.id}
|
||||
to={`/events/${String(event.id)}`}
|
||||
className="block bg-[#1A1A22] rounded-xl overflow-hidden border border-white/5 hover:border-primary/30 transition-all"
|
||||
>
|
||||
<div className="relative aspect-[16/9]">
|
||||
<FillImage src={event.image || "/placeholder.svg"} alt={event.title} className="h-full" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-transparent to-transparent" />
|
||||
{event.status === "直播中" && (
|
||||
<div className="absolute top-3 left-3 flex items-center gap-1 bg-secondary/90 backdrop-blur-md px-2 py-1 rounded-md text-xs font-bold text-white animate-pulse">
|
||||
<span className="w-2 h-2 rounded-full bg-white" />
|
||||
{event.status}
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-3 left-3 right-3">
|
||||
<h3 className="font-bold text-white mb-1">{event.title}</h3>
|
||||
<div className="flex items-center gap-2 text-xs text-white/70">
|
||||
<Clock size={14} />
|
||||
{event.time}
|
||||
<Users size={14} className="ml-2" />
|
||||
{event.viewers}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-center">
|
||||
<div className="font-bold text-white">{event.teams[0]}</div>
|
||||
<div className="text-xs text-white/50">战队</div>
|
||||
</div>
|
||||
<div className="text-primary font-bold">VS</div>
|
||||
<div className="text-center">
|
||||
<div className="font-bold text-white">{event.teams[1]}</div>
|
||||
<div className="text-xs text-white/50">战队</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="flex items-center gap-1 text-yellow-500 text-sm font-bold">
|
||||
<Trophy size={14} />
|
||||
{event.prize}
|
||||
</div>
|
||||
<div className="text-xs text-white/50">奖金池</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-xs text-white/50">
|
||||
<MapPin size={14} />
|
||||
{event.venue}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
114
new/wz-app/src/pages/FootprintPage.tsx
Normal file
114
new/wz-app/src/pages/FootprintPage.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { ArrowLeft, MapPin, Clock, Star, Heart } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const history: Array<{
|
||||
date: string
|
||||
type: "hotel" | "match" | "purchase" | "cafe"
|
||||
name: string
|
||||
rating?: number
|
||||
image?: string
|
||||
detail?: string
|
||||
}> = [
|
||||
{
|
||||
date: "2025-01-20",
|
||||
type: "hotel",
|
||||
name: "玩值电竞酒店(旗舰店)",
|
||||
rating: 5,
|
||||
image: "esports hotel room",
|
||||
},
|
||||
{
|
||||
date: "2025-01-18",
|
||||
type: "match",
|
||||
name: "与GM-远洋的粉丝群匹配成功",
|
||||
detail: "在线聊天2小时",
|
||||
},
|
||||
{
|
||||
date: "2025-01-15",
|
||||
type: "purchase",
|
||||
name: "购买了王者荣耀皮肤",
|
||||
detail: "白龙吟",
|
||||
},
|
||||
{
|
||||
date: "2025-01-10",
|
||||
type: "cafe",
|
||||
name: "网鱼网咖(万达店)",
|
||||
rating: 4,
|
||||
image: "internet cafe",
|
||||
},
|
||||
]
|
||||
|
||||
export function FootprintPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="p-2 -ml-2 hover:bg-white/5 rounded-lg transition-colors">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">我的足迹</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-3 gap-3 mb-6">
|
||||
<div className="glass-card p-3 rounded-xl text-center">
|
||||
<div className="text-2xl font-bold text-primary mb-1">12</div>
|
||||
<div className="text-xs text-white/50">总访问</div>
|
||||
</div>
|
||||
<div className="glass-card p-3 rounded-xl text-center">
|
||||
<div className="text-2xl font-bold text-green-500 mb-1">8</div>
|
||||
<div className="text-xs text-white/50">线下体验</div>
|
||||
</div>
|
||||
<div className="glass-card p-3 rounded-xl text-center">
|
||||
<div className="text-2xl font-bold text-yellow-500 mb-1">24</div>
|
||||
<div className="text-xs text-white/50">游戏局数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{history.map((item, i) => (
|
||||
<div key={i} className="flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary border-2 border-primary">
|
||||
{item.type === "hotel" && <MapPin size={18} />}
|
||||
{item.type === "match" && <Heart size={18} />}
|
||||
{item.type === "purchase" && <Star size={18} />}
|
||||
{item.type === "cafe" && <MapPin size={18} />}
|
||||
</div>
|
||||
{i < history.length - 1 && <div className="w-0.5 flex-1 bg-white/10 mt-2" />}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 pb-4">
|
||||
<div className="glass-card p-3 rounded-xl">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<h4 className="font-bold text-sm mb-1">{item.name}</h4>
|
||||
{item.detail && <p className="text-xs text-white/50">{item.detail}</p>}
|
||||
{item.rating && (
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
{Array.from({ length: item.rating }).map((_, j) => (
|
||||
<Star key={j} size={12} fill="currentColor" className="text-yellow-400" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.image && (
|
||||
<div className="w-14 h-14 rounded-lg overflow-hidden bg-zinc-800 relative flex-shrink-0">
|
||||
<FillImage src="/placeholder.svg" alt={item.name} className="h-full" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[10px] text-white/30">
|
||||
<Clock size={10} />
|
||||
{item.date}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
305
new/wz-app/src/pages/HotelPage.tsx
Normal file
305
new/wz-app/src/pages/HotelPage.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
import { useState, Suspense } from "react"
|
||||
import { Search, MapPin, Filter, Star, ArrowLeft, ChevronDown, CheckCircle2, X, Map, Navigation } from "lucide-react"
|
||||
import { useNavigate, useSearchParams } from "react-router-dom"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const hotels = [
|
||||
{ id: 1, name: "玩值电竞酒店(旗舰店)", rating: 4.9, reviews: 1205, price: 398, distance: "0.8km", tags: ["RTX4090", "电竞椅", "24h餐饮"], district: "思明区/SM广场", image: "/hotel-1.jpg", lat: 24.4798, lng: 118.0894 },
|
||||
{ id: 2, name: "极客空间电竞公寓", rating: 4.7, reviews: 856, price: 268, distance: "1.2km", tags: ["五黑房", "投影", "免费停车"], district: "湖里区/万达广场", image: "/hotel-2.jpg", lat: 24.5103, lng: 118.1287 },
|
||||
{ id: 3, name: "异次元电竞酒店", rating: 4.8, reviews: 2341, price: 328, distance: "2.5km", tags: ["情侣房", "PS5", "VR体验"], district: "集美区/软件园", image: "/hotel-3.jpg", lat: 24.5745, lng: 118.1012 },
|
||||
]
|
||||
|
||||
const cafes = [
|
||||
{ id: 101, name: "网鱼网咖(万达店)", rating: 4.8, reviews: 5620, price: 12, distance: "0.5km", tags: ["3080显卡", "特权公馆", "现磨咖啡"], district: "湖里区/万达广场", image: "/cafe-1.jpg", lat: 24.5089, lng: 118.1256 },
|
||||
{ id: 102, name: "杰拉电竞馆", rating: 4.6, reviews: 3210, price: 10, distance: "1.8km", tags: ["赛事级配置", "美女陪玩", "独立包间"], district: "思明区/中山路", image: "/cafe-2.jpg", lat: 24.4512, lng: 118.0823 },
|
||||
]
|
||||
|
||||
function HotelContent() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const tabFromUrl = searchParams.get("tab") === "cafe" ? "cafe" : "hotel"
|
||||
const [activeTab, setActiveTab] = useState(tabFromUrl)
|
||||
const [showPaymentSuccess, setShowPaymentSuccess] = useState(false)
|
||||
const [paymentInfo, setPaymentInfo] = useState({ name: "", price: 0, type: "" as "hotel" | "cafe" })
|
||||
const [showMap, setShowMap] = useState(false)
|
||||
const [selectedLocation, setSelectedLocation] = useState<{ name: string; lat: number; lng: number } | null>(null)
|
||||
|
||||
const handleBook = (name: string, price: number, type: "hotel" | "cafe") => {
|
||||
setPaymentInfo({ name, price, type })
|
||||
setShowPaymentSuccess(true)
|
||||
}
|
||||
|
||||
const handleShowMap = (item: { name: string; lat: number; lng: number }) => {
|
||||
setSelectedLocation(item)
|
||||
setShowMap(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-24">
|
||||
{showPaymentSuccess && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="bg-zinc-900 rounded-2xl p-6 mx-4 max-w-sm w-full border border-white/10 animate-in zoom-in-95 duration-200">
|
||||
<div className="flex justify-end mb-2">
|
||||
<button onClick={() => setShowPaymentSuccess(false)} className="p-1 hover:bg-white/10 rounded-full transition-colors" type="button">
|
||||
<X size={20} className="text-white/50" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-primary/20 flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle2 size={40} className="text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-2">{paymentInfo.type === "hotel" ? "预订成功" : "抢座成功"}</h3>
|
||||
<p className="text-white/60 mb-4">
|
||||
您已成功{paymentInfo.type === "hotel" ? "预订" : "抢座"} {paymentInfo.name}
|
||||
</p>
|
||||
<div className="bg-zinc-800/50 rounded-xl p-4 mb-4">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-white/60">支付金额</span>
|
||||
<span className="text-primary font-bold text-lg">
|
||||
¥{paymentInfo.price}
|
||||
{paymentInfo.type === "cafe" ? "/时" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowPaymentSuccess(false)
|
||||
void navigate("/orders")
|
||||
}}
|
||||
className="w-full py-3 rounded-full bg-primary text-black font-bold active:scale-95 transition-transform"
|
||||
type="button"
|
||||
>
|
||||
查看订单
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showMap && selectedLocation && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="bg-zinc-900 rounded-2xl mx-4 max-w-md w-full border border-white/10 animate-in zoom-in-95 duration-200 overflow-hidden">
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Map size={20} className="text-primary" />
|
||||
<h3 className="font-bold">{selectedLocation.name}</h3>
|
||||
</div>
|
||||
<button onClick={() => setShowMap(false)} className="p-1 hover:bg-white/10 rounded-full transition-colors" type="button">
|
||||
<X size={20} className="text-white/50" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative h-64 bg-zinc-800">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<FillImage src="/xiamen-city-map-with-location-pin.jpg" alt="厦门市地图" className="h-full opacity-50" />
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center z-10">
|
||||
<Navigation size={48} className="text-primary mb-2" />
|
||||
<p className="text-white font-bold">{selectedLocation.name}</p>
|
||||
<p className="text-xs text-white/60 mt-1">厦门市</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`https://uri.amap.com/marker?position=${selectedLocation.lng},${selectedLocation.lat}&name=${encodeURIComponent(selectedLocation.name)}&coordinate=gaode&callnative=1`,
|
||||
"_blank",
|
||||
)
|
||||
}}
|
||||
className="w-full py-3 rounded-full bg-primary text-black font-bold active:scale-95 transition-transform flex items-center justify-center gap-2"
|
||||
type="button"
|
||||
>
|
||||
<Navigation size={18} />
|
||||
打开导航
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="sticky top-0 z-40 bg-background/95 backdrop-blur-md border-b border-white/5 supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex items-center gap-3 px-4 py-2">
|
||||
<button onClick={() => void navigate(-1)} className="active:scale-90 transition-transform" type="button">
|
||||
<ArrowLeft size={22} />
|
||||
</button>
|
||||
<div className="flex-1 h-9 bg-zinc-800/50 rounded-full flex items-center px-3 gap-2 border border-white/5">
|
||||
<Search size={14} className="text-white/40" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索酒店、网咖、商圈"
|
||||
className="bg-transparent border-none outline-none text-xs w-full h-full text-white placeholder:text-white/30"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleShowMap({ name: "厦门市", lat: 24.4798, lng: 118.0894 })}
|
||||
className="flex items-center gap-1 text-primary hover:bg-primary/10 px-2 py-1 rounded-full transition-colors"
|
||||
type="button"
|
||||
>
|
||||
<MapPin size={18} />
|
||||
<span className="text-xs font-bold">地图</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<div className="px-4 pb-2">
|
||||
<TabsList className="grid w-full grid-cols-2 bg-zinc-800/50 h-9">
|
||||
<TabsTrigger value="hotel" className="text-xs data-[state=active]:bg-primary data-[state=active]:text-white">
|
||||
电竞酒店
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cafe" className="text-xs data-[state=active]:bg-primary data-[state=active]:text-white">
|
||||
附近网咖
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between px-4 py-2 border-t border-white/5">
|
||||
<div className="flex gap-6 text-xs text-white/60 font-medium">
|
||||
<button type="button" className="flex items-center gap-1 text-primary">
|
||||
综合排序 <ChevronDown size={10} />
|
||||
</button>
|
||||
<button type="button" className="flex items-center gap-1">
|
||||
距离 <ChevronDown size={10} />
|
||||
</button>
|
||||
<button type="button" className="flex items-center gap-1">
|
||||
价格 <ChevronDown size={10} />
|
||||
</button>
|
||||
<button type="button" className="flex items-center gap-1">
|
||||
筛选 <Filter size={10} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="hotel" className="mt-0 px-4 py-2 space-y-3 min-h-[50vh]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-sm font-bold text-white/90">热门推荐</span>
|
||||
</div>
|
||||
{hotels.map((hotel) => (
|
||||
<div key={hotel.id} className="flex gap-3 py-3 border-b border-white/5 last:border-0">
|
||||
<div className="relative w-24 h-24 rounded-lg overflow-hidden shrink-0 bg-muted">
|
||||
<FillImage src={hotel.image || "/placeholder.svg"} alt={hotel.name} className="h-24 w-24" />
|
||||
{hotel.id === 1 && (
|
||||
<div className="absolute top-0 left-0 bg-red-500 text-white text-[10px] px-1.5 py-0.5 rounded-br-lg font-bold z-10">热门</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between min-w-0">
|
||||
<div>
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<h3 className="font-bold text-base truncate pr-2">{hotel.name}</h3>
|
||||
<button
|
||||
onClick={() => handleShowMap(hotel)}
|
||||
className="text-primary hover:bg-primary/10 p-1 rounded transition-colors shrink-0"
|
||||
type="button"
|
||||
>
|
||||
<MapPin size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-white/60 mb-1">
|
||||
<div className="flex items-center text-orange-400 font-bold bg-orange-500/10 px-1 rounded">
|
||||
<Star size={10} fill="currentColor" />
|
||||
<span className="ml-0.5">{hotel.rating}</span>
|
||||
</div>
|
||||
<span className="text-primary font-bold">{hotel.reviews}条评论</span>
|
||||
</div>
|
||||
<div className="text-xs text-white/40 truncate mb-1">
|
||||
{hotel.district} | {hotel.distance}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{hotel.tags.map((tag) => (
|
||||
<span key={tag} className="px-1.5 py-0.5 rounded text-[10px] border border-white/10 text-white/50 bg-white/5">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end justify-between mt-1">
|
||||
<div className="flex items-baseline gap-0.5 text-red-500">
|
||||
<span className="text-xs font-bold">¥</span>
|
||||
<span className="text-xl font-bold leading-none">{hotel.price}</span>
|
||||
<span className="text-xs text-white/50 ml-1 font-normal">起</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleBook(hotel.name, hotel.price, "hotel")}
|
||||
className="px-3 py-1 bg-primary text-white text-xs rounded-full font-bold active:scale-95 transition-transform"
|
||||
type="button"
|
||||
>
|
||||
预订
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="cafe" className="mt-0 px-4 py-2 space-y-3 min-h-[50vh]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-sm font-bold text-white/90">附近好店</span>
|
||||
</div>
|
||||
{cafes.map((cafe) => (
|
||||
<div key={cafe.id} className="flex gap-3 py-3 border-b border-white/5 last:border-0">
|
||||
<div className="relative w-24 h-24 rounded-lg overflow-hidden shrink-0 bg-muted">
|
||||
<FillImage src={cafe.image || "/placeholder.svg"} alt={cafe.name} className="h-24 w-24" />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between min-w-0">
|
||||
<div>
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<h3 className="font-bold text-base truncate pr-2">{cafe.name}</h3>
|
||||
<button onClick={() => handleShowMap(cafe)} className="text-primary hover:bg-primary/10 p-1 rounded transition-colors shrink-0" type="button">
|
||||
<MapPin size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-white/60 mb-1">
|
||||
<div className="flex items-center text-orange-400 font-bold bg-orange-500/10 px-1 rounded">
|
||||
<Star size={10} fill="currentColor" />
|
||||
<span className="ml-0.5">{cafe.rating}</span>
|
||||
</div>
|
||||
<span className="text-white/40">人均 ¥{(cafe.price * 3).toFixed(0)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-white/40 truncate mb-1">
|
||||
{cafe.district} | {cafe.distance}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{cafe.tags.map((tag) => (
|
||||
<span key={tag} className="text-[10px] px-1.5 py-0.5 rounded bg-primary/5 text-primary border border-primary/20">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end justify-between mt-1">
|
||||
<div className="flex items-baseline gap-0.5">
|
||||
<span className="text-xs text-white/60">¥</span>
|
||||
<span className="text-lg font-bold text-primary leading-none">{cafe.price}</span>
|
||||
<span className="text-xs text-white/40">/时</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleBook(cafe.name, cafe.price, "cafe")}
|
||||
className="px-3 py-1 bg-white/10 text-white text-xs rounded-full font-bold active:scale-95 transition-transform hover:bg-white/20"
|
||||
type="button"
|
||||
>
|
||||
抢座
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function HotelPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<HotelContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
190
new/wz-app/src/pages/MessagesPage.tsx
Normal file
190
new/wz-app/src/pages/MessagesPage.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { ArrowLeft, Bell, MoreHorizontal, MessageCircle, Users } from "lucide-react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
export function MessagesPage() {
|
||||
const messages = [
|
||||
{
|
||||
id: 1,
|
||||
user: "GM-远洋",
|
||||
avatar: "/streamer-1.jpg",
|
||||
content: "大神,今晚有空带我上分吗?",
|
||||
time: "10:30",
|
||||
unread: 2,
|
||||
type: "chat",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: "系统通知",
|
||||
avatar: "/esports-logo.png",
|
||||
content: "您的陪玩申请已通过审核,快去接单吧!",
|
||||
time: "昨天",
|
||||
unread: 0,
|
||||
type: "system",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
user: "小柠檬",
|
||||
avatar: "/cp-avatar-2.jpg",
|
||||
content: "谢谢你的礼物!下次一起玩呀~",
|
||||
time: "昨天",
|
||||
unread: 0,
|
||||
type: "chat",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
user: "车队助手",
|
||||
avatar: "/esports-team-logo.jpg",
|
||||
content: "您申请加入的【王者荣耀五排车队】已同意",
|
||||
time: "前天",
|
||||
unread: 1,
|
||||
type: "system",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
user: "电竞酒店客服",
|
||||
avatar: "/hotel-icon.jpg",
|
||||
content: "您预订的电竞双人房已确认,期待您的光临~",
|
||||
time: "3天前",
|
||||
unread: 0,
|
||||
type: "system",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
user: "梦之队·队长",
|
||||
avatar: "/streamer-3.jpg",
|
||||
content: "欢迎加入我们的战队!有空一起训练",
|
||||
time: "3天前",
|
||||
unread: 0,
|
||||
type: "chat",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-20">
|
||||
<header className="glass sticky top-0 z-40 flex items-center justify-between px-4 py-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/" className="-ml-2 rounded-full p-2 transition-colors hover:bg-white/10">
|
||||
<ArrowLeft size={20} />
|
||||
</Link>
|
||||
<h1 className="text-lg font-bold">消息中心</h1>
|
||||
</div>
|
||||
<button type="button" className="rounded-full p-2 transition-colors hover:bg-white/10">
|
||||
<MoreHorizontal size={20} />
|
||||
</button>
|
||||
</header>
|
||||
<p className="text-foreground/45 border-border/50 border-b px-4 py-2 text-xs">
|
||||
当前为展示数据;正式会话列表以 wz-api 契约为准(见 开发文档/玩值/迁移-API对照表)。
|
||||
</p>
|
||||
|
||||
<Tabs defaultValue="chat" className="w-full">
|
||||
<div className="mt-2 px-4">
|
||||
<TabsList className="grid w-full grid-cols-2 rounded-xl bg-white/5 p-1">
|
||||
<TabsTrigger
|
||||
value="chat"
|
||||
className="data-[state=active]:bg-primary flex items-center gap-2 rounded-lg data-[state=active]:text-white"
|
||||
>
|
||||
<MessageCircle size={14} />
|
||||
聊天
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="notification"
|
||||
className="data-[state=active]:bg-primary flex items-center gap-2 rounded-lg data-[state=active]:text-white"
|
||||
>
|
||||
<Bell size={14} />
|
||||
通知
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="chat" className="mt-4 space-y-2 px-4">
|
||||
{messages
|
||||
.filter((m) => m.type === "chat")
|
||||
.map((msg) => (
|
||||
<Link
|
||||
key={msg.id}
|
||||
to={`/chat/${msg.id}`}
|
||||
className="hover:bg-white/5 flex cursor-pointer items-center gap-3 rounded-xl p-3 transition-colors"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="border-border relative h-12 w-12 overflow-hidden rounded-full border">
|
||||
<FillImage src={msg.avatar || "/favicon.svg"} alt={msg.user} className="h-12 w-12 rounded-full" />
|
||||
</div>
|
||||
{msg.unread > 0 && (
|
||||
<div className="border-background absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full border-2 bg-red-500 text-[10px] font-bold">
|
||||
{msg.unread}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold">{msg.user}</h3>
|
||||
<span className="text-foreground/40 text-xs">{msg.time}</span>
|
||||
</div>
|
||||
<p className="text-foreground/60 truncate text-xs">{msg.content}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="notification" className="mt-4 space-y-2 px-4">
|
||||
{messages
|
||||
.filter((m) => m.type === "system")
|
||||
.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className="hover:bg-white/5 flex cursor-pointer items-center gap-3 rounded-xl p-3 transition-colors"
|
||||
>
|
||||
<div className="border-primary/30 relative h-12 w-12 overflow-hidden rounded-full border bg-primary/20">
|
||||
<FillImage
|
||||
src={msg.avatar || "/favicon.svg"}
|
||||
alt={msg.user}
|
||||
className="h-12 w-12 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold">{msg.user}</h3>
|
||||
<span className="text-foreground/40 text-xs">{msg.time}</span>
|
||||
</div>
|
||||
<p className="text-foreground/60 truncate text-xs">{msg.content}</p>
|
||||
</div>
|
||||
{msg.unread > 0 && <div className="h-2 w-2 rounded-full bg-red-500" />}
|
||||
</div>
|
||||
))}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-6 px-4">
|
||||
<h3 className="text-foreground/80 mb-3 text-sm font-bold">快捷入口</h3>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Link to="/guild" className="flex flex-col items-center gap-2">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-blue-500/20">
|
||||
<Users size={20} className="text-blue-400" />
|
||||
</div>
|
||||
<span className="text-foreground/60 text-xs">公会群聊</span>
|
||||
</Link>
|
||||
<Link to="/cp" className="flex flex-col items-center gap-2">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-pink-500/20">
|
||||
<MessageCircle size={20} className="text-pink-400" />
|
||||
</div>
|
||||
<span className="text-foreground/60 text-xs">CP聊天</span>
|
||||
</Link>
|
||||
<Link to="/coach" className="flex flex-col items-center gap-2">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-green-500/20">
|
||||
<MessageCircle size={20} className="text-green-400" />
|
||||
</div>
|
||||
<span className="text-foreground/60 text-xs">陪练私信</span>
|
||||
</Link>
|
||||
<Link to="/star/1" className="flex flex-col items-center gap-2">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-purple-500/20">
|
||||
<MessageCircle size={20} className="text-purple-400" />
|
||||
</div>
|
||||
<span className="text-foreground/60 text-xs">明星社群</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
64
new/wz-app/src/pages/MomentsCreatePage.tsx
Normal file
64
new/wz-app/src/pages/MomentsCreatePage.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { ChevronLeft, ImageIcon, Hash, MapPin, Smile } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
|
||||
export function MomentsCreatePage() {
|
||||
const navigate = useNavigate()
|
||||
const [content, setContent] = useState("")
|
||||
|
||||
const handlePublish = () => {
|
||||
if (!content.trim()) {
|
||||
toast({ title: "内容不能为空" })
|
||||
return
|
||||
}
|
||||
toast({ title: "发布成功", description: "您的动态已发布" })
|
||||
setTimeout(() => void navigate(-1), 1000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground pb-20">
|
||||
<header className="sticky top-0 z-10 bg-background/80 backdrop-blur-md px-4 py-3 flex items-center justify-between border-b border-white/5">
|
||||
<Button variant="ghost" size="icon" onClick={() => void navigate(-1)} type="button">
|
||||
<ChevronLeft className="w-6 h-6" />
|
||||
</Button>
|
||||
<h1 className="font-bold text-lg">发布动态</h1>
|
||||
<Button size="sm" className="bg-primary hover:bg-primary/90 rounded-full px-4" type="button" onClick={handlePublish}>
|
||||
发布
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
<textarea
|
||||
placeholder="分享你的电竞生活..."
|
||||
className="w-full min-h-[200px] bg-transparent border-none text-lg resize-none focus-visible:ring-0 p-0 placeholder:text-muted-foreground/50 focus:outline-none"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="aspect-square rounded-xl bg-white/5 border border-white/10 flex flex-col items-center justify-center cursor-pointer hover:bg-white/10 transition-colors">
|
||||
<ImageIcon className="w-8 h-8 text-muted-foreground mb-2" />
|
||||
<span className="text-xs text-muted-foreground">添加图片/视频</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-white/5 space-y-0">
|
||||
<Button variant="ghost" className="w-full justify-start gap-3 text-muted-foreground hover:text-white px-2" type="button">
|
||||
<Hash className="w-5 h-5" />
|
||||
<span>添加话题</span>
|
||||
</Button>
|
||||
<Button variant="ghost" className="w-full justify-start gap-3 text-muted-foreground hover:text-white px-2" type="button">
|
||||
<MapPin className="w-5 h-5" />
|
||||
<span>你在哪里</span>
|
||||
</Button>
|
||||
<Button variant="ghost" className="w-full justify-start gap-3 text-muted-foreground hover:text-white px-2" type="button">
|
||||
<Smile className="w-5 h-5" />
|
||||
<span>表情/动图</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
128
new/wz-app/src/pages/MomentsDetailPage.tsx
Normal file
128
new/wz-app/src/pages/MomentsDetailPage.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Link, useNavigate, useParams } from "react-router-dom"
|
||||
import { Heart, MessageCircle, Share2, Gift, ArrowLeft, Send, BookOpen } from "lucide-react"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
|
||||
type MomentEntry = {
|
||||
id: string
|
||||
user: string
|
||||
avatar: string
|
||||
desc: string
|
||||
video: string
|
||||
likes: string
|
||||
comments: number
|
||||
hasCourse: boolean
|
||||
courseId: string
|
||||
courseTitle: string
|
||||
}
|
||||
|
||||
/** 与路由 `moments/:momentId` 对齐;接 wz-api 后可改为请求 `momentId` */
|
||||
const MOMENTS_BY_ID: Record<string, MomentEntry> = {
|
||||
"1": {
|
||||
id: "1",
|
||||
user: "GM-远洋",
|
||||
avatar: "/gamer-girl-headphones.jpg",
|
||||
desc: "可惜不是你喜欢的甜妹音 😷 #电竞少女 #英雄联盟 #日常",
|
||||
video: "/esports-gamer-girl-streaming-vertical.jpg",
|
||||
likes: "1.2w",
|
||||
comments: 458,
|
||||
hasCourse: true,
|
||||
courseId: "1",
|
||||
courseTitle: "打野进阶课:从入门到王者",
|
||||
},
|
||||
"2": {
|
||||
id: "2",
|
||||
user: "卡若",
|
||||
avatar: "/streamer-2.jpg",
|
||||
desc: "无畏契约练枪日常 #VALORANT #陪练",
|
||||
video: "/esports-streaming-vertical.jpg",
|
||||
likes: "8.3k",
|
||||
comments: 126,
|
||||
hasCourse: true,
|
||||
courseId: "2",
|
||||
courseTitle: "无畏契约枪法训练营",
|
||||
},
|
||||
}
|
||||
|
||||
export function MomentsDetailPage() {
|
||||
const { momentId = "1" } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const momentData = MOMENTS_BY_ID[momentId] ?? MOMENTS_BY_ID["1"]!
|
||||
|
||||
const handleLike = () => toast({ title: "点赞成功", description: "已添加到我喜欢的视频" })
|
||||
const handleShare = () => toast({ title: "分享成功", description: "链接已复制到剪贴板" })
|
||||
const handleGift = () => toast({ title: "礼物发送成功", description: "主播已收到您的心意" })
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-black text-white relative flex flex-col">
|
||||
<div className="absolute top-0 left-0 right-0 z-30 pt-safe px-4 py-3 flex items-center gap-4 bg-gradient-to-b from-black/60 to-transparent">
|
||||
<button type="button" onClick={() => void navigate(-1)} className="p-2 -ml-2 rounded-full hover:bg-white/10 transition-colors" aria-label="返回">
|
||||
<ArrowLeft size={24} />
|
||||
</button>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden border border-white/20 relative shrink-0">
|
||||
<FillImage src={momentData.avatar} alt={momentData.user} className="h-8 w-8" />
|
||||
</div>
|
||||
<span className="font-bold text-sm truncate">{momentData.user}</span>
|
||||
<button type="button" className="px-3 py-1 rounded-full bg-primary text-black text-xs font-bold shrink-0" onClick={() => toast({ title: "已关注" })}>
|
||||
关注
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 relative bg-zinc-900 min-h-0">
|
||||
<FillImage src={momentData.video} alt="Video" className="h-full w-full" />
|
||||
<div className="absolute right-2 bottom-32 flex flex-col items-center gap-6 z-20">
|
||||
<button type="button" className="flex flex-col items-center gap-1 group" onClick={handleLike}>
|
||||
<Heart size={32} className="text-white drop-shadow-lg group-hover:text-red-500 transition-colors" />
|
||||
<span className="text-xs font-medium drop-shadow-md">{momentData.likes}</span>
|
||||
</button>
|
||||
<div className="flex flex-col items-center gap-1 group">
|
||||
<MessageCircle size={32} className="text-white drop-shadow-lg group-hover:text-primary transition-colors" />
|
||||
<span className="text-xs font-medium drop-shadow-md">{momentData.comments}</span>
|
||||
</div>
|
||||
<button type="button" className="flex flex-col items-center gap-1 group" onClick={handleShare}>
|
||||
<Share2 size={32} className="text-white drop-shadow-lg group-hover:text-green-400 transition-colors" />
|
||||
<span className="text-xs font-medium drop-shadow-md">分享</span>
|
||||
</button>
|
||||
<button type="button" className="flex flex-col items-center gap-1 group" onClick={handleGift}>
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-secondary to-accent flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Gift size={20} />
|
||||
</div>
|
||||
<span className="text-xs font-medium text-secondary drop-shadow-md">礼物</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-0 bottom-0 w-full p-4 z-20 bg-gradient-to-t from-black via-black/60 to-transparent pt-20">
|
||||
{momentData.hasCourse && (
|
||||
<Link
|
||||
to={`/course/${momentData.courseId}`}
|
||||
className="flex items-center justify-between w-full p-3 rounded-xl bg-white/10 backdrop-blur-md border border-white/10 mb-4 hover:bg-white/20 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center text-primary shrink-0">
|
||||
<BookOpen size={20} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs text-primary font-bold mb-0.5">同款课程</div>
|
||||
<div className="text-sm font-bold text-white group-hover:text-primary transition-colors truncate">{momentData.courseTitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-3 py-1.5 rounded-lg bg-primary text-black text-xs font-bold shrink-0">去学习</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<h3 className="font-bold text-lg mb-2">@{momentData.user}</h3>
|
||||
<p className="text-sm text-white/90 leading-relaxed mb-4">{momentData.desc}</p>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 h-10 bg-white/10 rounded-full px-4 flex items-center text-white/50 text-sm">说点什么...</div>
|
||||
<button type="button" className="p-2 rounded-full bg-white/10 hover:bg-primary hover:text-black transition-colors" aria-label="发送">
|
||||
<Send size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
new/wz-app/src/pages/MyCoursesPage.tsx
Normal file
67
new/wz-app/src/pages/MyCoursesPage.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { ArrowLeft, PlayCircle, Clock, BookOpen } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useAppContext } from "@/components/providers/app-provider"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
export function MyCoursesPage() {
|
||||
const navigate = useNavigate()
|
||||
const { orders } = useAppContext()
|
||||
|
||||
const courses = orders.filter((o) => o.title.includes("课程") || o.title.includes("教学"))
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-24">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-4">
|
||||
<button onClick={() => void navigate(-1)} className="p-2 hover:bg-white/10 rounded-full transition-colors" type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">我的课程</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
{courses.length > 0 ? (
|
||||
courses.map((course) => (
|
||||
<div key={course.id} className="glass-card p-3 rounded-xl flex gap-3">
|
||||
<div className="relative w-24 h-16 rounded-lg overflow-hidden flex-shrink-0 bg-muted">
|
||||
<FillImage src={course.image || "/placeholder.svg"} alt={course.title} className="h-16 w-24" />
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30 z-10">
|
||||
<PlayCircle size={24} className="text-white/80" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
<h3 className="font-bold text-sm line-clamp-2">{course.title}</h3>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="flex items-center gap-2 text-[10px] text-white/50">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={10} /> 45分钟
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<BookOpen size={10} /> 3/12节
|
||||
</span>
|
||||
</div>
|
||||
<Button size="sm" className="h-6 text-xs px-3" type="button">
|
||||
继续学习
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-white/50">
|
||||
<BookOpen size={48} className="mb-4 opacity-20" />
|
||||
<p>暂无课程</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-4 border-white/10 hover:bg-white/5 bg-transparent"
|
||||
type="button"
|
||||
onClick={() => void navigate("/mall")}
|
||||
>
|
||||
去商城看看
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
178
new/wz-app/src/pages/OrdersPage.tsx
Normal file
178
new/wz-app/src/pages/OrdersPage.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { ArrowLeft, ShoppingBag, Clock, CheckCircle2, XCircle, RotateCcw } from "lucide-react"
|
||||
import { useNavigate, Link } from "react-router-dom"
|
||||
import { useAppContext } from "@/components/providers/app-provider"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
export function OrdersPage() {
|
||||
const navigate = useNavigate()
|
||||
const { orders } = useAppContext()
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return "text-green-500"
|
||||
case "pending":
|
||||
return "text-yellow-500"
|
||||
case "cancelled":
|
||||
return "text-red-500"
|
||||
case "refunded":
|
||||
return "text-blue-500"
|
||||
default:
|
||||
return "text-foreground/50"
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return <CheckCircle2 size={14} />
|
||||
case "pending":
|
||||
return <Clock size={14} />
|
||||
case "cancelled":
|
||||
return <XCircle size={14} />
|
||||
case "refunded":
|
||||
return <RotateCcw size={14} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return "已完成"
|
||||
case "pending":
|
||||
return "进行中"
|
||||
case "cancelled":
|
||||
return "已取消"
|
||||
case "refunded":
|
||||
return "已退款"
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
switch (type) {
|
||||
case "service":
|
||||
return "陪玩服务"
|
||||
case "product":
|
||||
return "商城商品"
|
||||
case "course":
|
||||
return "课程购买"
|
||||
case "hotel":
|
||||
return "酒店预订"
|
||||
case "cafe":
|
||||
return "网咖抢座"
|
||||
case "recharge":
|
||||
return "充值订单"
|
||||
case "pawn":
|
||||
return "账号典当"
|
||||
default:
|
||||
return "其他"
|
||||
}
|
||||
}
|
||||
|
||||
const displayOrders = orders.filter((o) => o.type !== "recharge")
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-24">
|
||||
<header className="border-border bg-background/95 supports-backdrop-filter:bg-background/80 sticky top-0 z-40 flex items-center gap-4 border-b px-4 py-3 backdrop-blur-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
className="hover:bg-foreground/10 rounded-full p-2 transition-colors"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="text-lg font-bold">订单中心</h1>
|
||||
</header>
|
||||
|
||||
<Tabs defaultValue="all" className="mt-2 min-w-0 w-full">
|
||||
<div className="min-w-0 px-4">
|
||||
<TabsList className="bg-foreground/5 w-full max-w-full min-w-0 rounded-xl p-1">
|
||||
<TabsTrigger value="all" className="min-w-0 flex-1 truncate text-xs">
|
||||
全部
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="service" className="min-w-0 flex-1 truncate text-xs">
|
||||
服务
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="product" className="min-w-0 flex-1 truncate text-xs">
|
||||
商品
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="hotel" className="min-w-0 flex-1 truncate text-xs">
|
||||
预订
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{["all", "service", "product", "hotel"].map((tab) => (
|
||||
<TabsContent key={tab} value={tab} className="mt-4 space-y-3 px-4">
|
||||
{displayOrders.filter(
|
||||
(o) => tab === "all" || o.type === tab || (tab === "hotel" && (o.type === "hotel" || o.type === "cafe")),
|
||||
).length > 0 ? (
|
||||
displayOrders
|
||||
.filter(
|
||||
(o) =>
|
||||
tab === "all" || o.type === tab || (tab === "hotel" && (o.type === "hotel" || o.type === "cafe")),
|
||||
)
|
||||
.map((order, i) => (
|
||||
<div key={i} className="bg-card border-border rounded-xl border p-4">
|
||||
<div className="mb-3 flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-foreground/50 text-xs">{order.date}</span>
|
||||
<span className="text-foreground/70 border-border bg-foreground/5 rounded px-1.5 py-0.5 text-[10px] border">
|
||||
{getTypeText(order.type)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 text-xs font-medium ${getStatusColor(order.status)}`}>
|
||||
{getStatusIcon(order.status)}
|
||||
{getStatusText(order.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="h-16 w-16 flex-shrink-0">
|
||||
<FillImage
|
||||
src={order.image || "/favicon.svg"}
|
||||
alt={order.title}
|
||||
className="h-16 w-16 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="mb-1 truncate text-sm font-bold">{order.title}</h3>
|
||||
<p className="text-foreground/50 mb-1 text-xs">订单号: {order.orderNo || order.id}</p>
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<span className="text-foreground/50 text-xs">数量: 1</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-primary text-sm font-bold">{order.price}</span>
|
||||
<span className="text-foreground/50 text-[10px]">币</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-border mt-3 flex justify-end gap-2 border-t pt-3">
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" asChild>
|
||||
<Link to="/customer-service">联系客服</Link>
|
||||
</Button>
|
||||
<Button size="sm" className="bg-foreground/10 border-border h-7 border text-xs hover:bg-foreground/20" asChild>
|
||||
<Link to={`/order/${order.id}`}>订单详情</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-foreground/50 flex flex-col items-center justify-center py-20">
|
||||
<ShoppingBag size={48} className="mb-4 opacity-20" />
|
||||
<p>暂无订单</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
250
new/wz-app/src/pages/PartyDetailPage.tsx
Normal file
250
new/wz-app/src/pages/PartyDetailPage.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import { useParams, useNavigate } from "react-router-dom"
|
||||
import {
|
||||
ArrowLeft,
|
||||
Mic,
|
||||
MicOff,
|
||||
MessageSquare,
|
||||
Gift,
|
||||
Send,
|
||||
UserPlus,
|
||||
Users,
|
||||
Star,
|
||||
Check,
|
||||
} from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { useAppContext } from "@/components/providers/app-provider"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const PARTY_DATA: Record<string, { name: string; host: string; avatar: string; game: string; background: string }> = {
|
||||
"1": {
|
||||
name: "GM-远洋的派对群",
|
||||
host: "GM-远洋",
|
||||
avatar: "/streamer-1.jpg",
|
||||
game: "英雄联盟",
|
||||
background: "/party-lol.jpg",
|
||||
},
|
||||
"2": {
|
||||
name: "甜心女王粉丝派对",
|
||||
host: "甜心女王",
|
||||
avatar: "/streamer-5.jpg",
|
||||
game: "王者荣耀",
|
||||
background: "/party-hok.jpg",
|
||||
},
|
||||
"3": {
|
||||
name: "带粉上分车队",
|
||||
host: "路人王",
|
||||
avatar: "/streamer-3.jpg",
|
||||
game: "和平精英",
|
||||
background: "/party-team.jpg",
|
||||
},
|
||||
}
|
||||
|
||||
const GIFTS = [
|
||||
{ id: 1, name: "小心心", price: 1, icon: "💕" },
|
||||
{ id: 2, name: "棒棒糖", price: 10, icon: "🍭" },
|
||||
{ id: 3, name: "玫瑰花", price: 52, icon: "🌹" },
|
||||
{ id: 4, name: "告白气球", price: 520, icon: "🎈" },
|
||||
{ id: 5, name: "跑车", price: 5000, icon: "🏎️" },
|
||||
{ id: 6, name: "火箭", price: 10000, icon: "🚀" },
|
||||
]
|
||||
|
||||
export function PartyDetailPage() {
|
||||
const { id = "1" } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { pay } = useAppContext()
|
||||
const partyInfo = useMemo(() => PARTY_DATA[id] ?? PARTY_DATA["1"]!, [id])
|
||||
const [isMicOn, setIsMicOn] = useState(false)
|
||||
const [showGifts, setShowGifts] = useState(false)
|
||||
const [showGiftSuccess, setShowGiftSuccess] = useState(false)
|
||||
const [lastGift, setLastGift] = useState("")
|
||||
const [messages, setMessages] = useState([
|
||||
{ user: "系统", content: "欢迎来到派对房间", type: "system" as const },
|
||||
{ user: "小柠檬", content: "大家好呀~", type: "user" as const },
|
||||
{ user: partyInfo.host, content: "今天带大家上分!", type: "user" as const },
|
||||
])
|
||||
const [inputValue, setInputValue] = useState("")
|
||||
|
||||
const seats = [
|
||||
{ id: 0, user: { name: partyInfo.host, avatar: partyInfo.avatar, isMuted: false, isHost: true } },
|
||||
{ id: 1, user: { name: "小柠檬", avatar: "/streamer-2.jpg", isMuted: false, isHost: false } },
|
||||
{ id: 2, user: { name: "电竞萌妹", avatar: "/streamer-6.jpg", isMuted: true, isHost: false } },
|
||||
{ id: 3, user: null },
|
||||
{ id: 4, user: null },
|
||||
{ id: 5, user: null },
|
||||
{ id: 6, user: null },
|
||||
{ id: 7, user: null },
|
||||
]
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!inputValue.trim()) return
|
||||
setMessages((m) => [...m, { user: "我", content: inputValue, type: "user" }])
|
||||
setInputValue("")
|
||||
}
|
||||
|
||||
const handleSendGift = (gift: (typeof GIFTS)[0]) => {
|
||||
pay(gift.price, `送出${gift.name}`, "product")
|
||||
setLastGift(`${gift.icon} ${gift.name}`)
|
||||
setShowGifts(false)
|
||||
setShowGiftSuccess(true)
|
||||
setTimeout(() => setShowGiftSuccess(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black relative flex flex-col">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<FillImage src={partyInfo.background} alt="Background" className="h-screen w-full object-cover opacity-40" />
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-black/30 to-black/90" />
|
||||
</div>
|
||||
|
||||
{showGiftSuccess && (
|
||||
<div className="absolute top-20 left-1/2 -translate-x-1/2 z-50">
|
||||
<div className="flex items-center gap-2 bg-gradient-to-r from-pink-500/90 to-purple-500/90 backdrop-blur-md px-4 py-2 rounded-full shadow-lg">
|
||||
<Check size={16} className="text-white" />
|
||||
<span className="text-white text-sm font-medium">已送出 {lastGift}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<header className="relative z-10 px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<Button variant="ghost" size="icon" className="text-white shrink-0" onClick={() => void navigate(-1)} type="button" aria-label="返回">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<div className="relative w-10 h-10 shrink-0 rounded-full overflow-hidden border-2 border-primary">
|
||||
<FillImage src={partyInfo.avatar} alt={partyInfo.host} className="h-10 w-10" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-white font-bold text-sm flex items-center gap-1 truncate">
|
||||
{partyInfo.name}
|
||||
<Star size={12} className="text-yellow-400 fill-yellow-400 shrink-0" />
|
||||
</h1>
|
||||
<p className="text-white/60 text-[10px] flex items-center gap-2">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={10} />
|
||||
125人
|
||||
</span>
|
||||
<span>{partyInfo.game}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="text-white" type="button" aria-label="更多">
|
||||
<UserPlus className="w-5 h-5" />
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div className="relative z-10 flex-1 px-4 py-6">
|
||||
<div className="grid grid-cols-4 gap-4 gap-y-8">
|
||||
{seats.map((seat) => (
|
||||
<div key={seat.id} className="flex flex-col items-center gap-2">
|
||||
<div className="relative w-14 h-14">
|
||||
<div className="w-14 h-14 rounded-full bg-white/10 border border-white/20 flex items-center justify-center overflow-hidden relative">
|
||||
{seat.user ? (
|
||||
<FillImage src={seat.user.avatar} alt={seat.user.name} className="h-14 w-14" />
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center">
|
||||
<UserPlus className="w-4 h-4 text-white/30" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{seat.user?.isMuted && (
|
||||
<div className="absolute bottom-0 right-0 bg-black/50 rounded-full p-0.5 border border-white/20">
|
||||
<MicOff className="w-3 h-3 text-red-500" />
|
||||
</div>
|
||||
)}
|
||||
{seat.user?.isHost && (
|
||||
<div className="absolute -top-1 -right-1 bg-yellow-500 rounded-full p-0.5">
|
||||
<Star className="w-3 h-3 text-white fill-white" />
|
||||
</div>
|
||||
)}
|
||||
{seat.id === 0 && <div className="absolute -inset-1 border-2 border-primary/50 rounded-full animate-pulse" />}
|
||||
</div>
|
||||
<span className="text-xs text-white/80 truncate max-w-[4rem]">{seat.user ? seat.user.name : `${seat.id + 1}号麦`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 h-48 px-4 mb-2 min-w-0 overflow-y-auto">
|
||||
<div className="space-y-2 flex flex-col justify-end min-h-full pb-2">
|
||||
{messages.map((msg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`bg-black/40 backdrop-blur-sm px-3 py-1.5 rounded-xl self-start max-w-full ${
|
||||
msg.type === "system" ? "text-yellow-400" : "text-white"
|
||||
}`}
|
||||
>
|
||||
<span className="text-white/60 text-xs mr-2">{msg.user}:</span>
|
||||
<span className="text-sm">{msg.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 bg-black/80 backdrop-blur-md px-4 py-3 border-t border-white/10 pb-8">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Input
|
||||
className="bg-white/10 border-none text-white h-9"
|
||||
placeholder="聊点什么..."
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSendMessage()}
|
||||
/>
|
||||
<Button size="icon" className="bg-primary hover:bg-primary/90 h-9 w-9" onClick={handleSendMessage} type="button">
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex gap-6">
|
||||
<button className="flex flex-col items-center gap-1 cursor-pointer" type="button" onClick={() => setIsMicOn(!isMicOn)}>
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${isMicOn ? "bg-white text-black" : "bg-white/10 text-white"}`}>
|
||||
{isMicOn ? <Mic className="w-5 h-5" /> : <MicOff className="w-5 h-5" />}
|
||||
</div>
|
||||
<span className="text-[10px] text-white/60">{isMicOn ? "开麦" : "闭麦"}</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center gap-1 cursor-pointer" type="button">
|
||||
<div className="w-10 h-10 rounded-full bg-white/10 text-white flex items-center justify-center">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-[10px] text-white/60">私信</span>
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
className="rounded-full px-6 bg-gradient-to-r from-pink-500 to-purple-500 border-none shadow-lg shadow-pink-500/20"
|
||||
type="button"
|
||||
onClick={() => setShowGifts(true)}
|
||||
>
|
||||
<Gift className="w-5 h-5 mr-2" />
|
||||
送礼
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={showGifts} onOpenChange={setShowGifts}>
|
||||
<DialogContent className="bg-zinc-900/95 backdrop-blur-xl border-white/10 text-white max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>送礼物给 {partyInfo.host}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-3 gap-3 py-4">
|
||||
{GIFTS.map((gift) => (
|
||||
<button
|
||||
key={gift.id}
|
||||
type="button"
|
||||
onClick={() => handleSendGift(gift)}
|
||||
className="flex flex-col items-center gap-2 p-3 bg-white/5 rounded-xl hover:bg-white/10 transition-colors border border-white/5 hover:border-pink-500/30 active:scale-95"
|
||||
>
|
||||
<span className="text-3xl">{gift.icon}</span>
|
||||
<span className="text-xs font-bold">{gift.name}</span>
|
||||
<span className="text-[10px] text-primary">{gift.price} 币</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
250
new/wz-app/src/pages/PitchDeckPage.tsx
Normal file
250
new/wz-app/src/pages/PitchDeckPage.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { useState } from "react"
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import { ChevronLeft, ChevronRight, TrendingUp, Users, Target, Zap, Award } from "lucide-react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const slides = [
|
||||
{
|
||||
id: 1,
|
||||
title: "玩值电竞",
|
||||
subtitle: "专注打造比体育明星价值高10倍的电竞俱乐部",
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center space-y-6">
|
||||
<div className="w-32 h-32 rounded-2xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-[0_0_50px_rgba(0,240,255,0.3)]">
|
||||
<span className="text-4xl font-bold text-black">WZ</span>
|
||||
</div>
|
||||
<h1 className="text-4xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-primary via-white to-secondary">
|
||||
玩值电竞
|
||||
</h1>
|
||||
<p className="text-xl text-white/80 font-light">
|
||||
让游戏玩家在电竞世界中
|
||||
<br />
|
||||
<span className="font-bold text-primary">玩得有价值</span>
|
||||
</p>
|
||||
<div className="flex gap-4 mt-8">
|
||||
<Badge text="14个签约IP" />
|
||||
<Badge text="46万铁粉" />
|
||||
<Badge text="10倍价值" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
bg: "bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-blue-900/20 via-background to-background",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "市场痛点与机会",
|
||||
subtitle: "电竞IP价值飞轮",
|
||||
content: (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card icon={Users} title="粘性更强" desc="比体育明星强10倍的用户粘性" />
|
||||
<Card icon={Zap} title="变现更多" desc="粉丝价值跃迁,多重变现渠道" />
|
||||
<Card icon={Target} title="流量更大" desc="线上线下全域流量分发" />
|
||||
<Card icon={TrendingUp} title="增长更快" desc="C2B2S 裂变增长模型" />
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-white/5 border border-white/10 mt-4">
|
||||
<h3 className="font-bold text-primary mb-2">超级中台运营模式</h3>
|
||||
<p className="text-sm text-white/70">搭建数字化生态体系,总部直接赋能门店及员工,实现粉丝资产数字化评估。</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
bg: "bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-purple-900/20 via-background to-background",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "商业模式",
|
||||
subtitle: "全方位价值变现体系",
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<ListItem number="01" title="游戏明星经纪" desc="选拔、签约、培养、包装,打造顶级电竞IP。" />
|
||||
<ListItem number="02" title="多元化变现" desc="陪玩服务、知识付费课程、拜师学艺、商业代言。" />
|
||||
<ListItem number="03" title="电竞赛事运营" desc="举办线上线下赛事,赞助、门票、周边全链条收入。" />
|
||||
<ListItem number="04" title="私域流量运营" desc="会员体系、粉丝社群、精准营销,沉淀私域资产。" />
|
||||
</div>
|
||||
),
|
||||
bg: "bg-[radial-gradient(ellipse_at_bottom_left,_var(--tw-gradient-stops))] from-pink-900/20 via-background to-background",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "核心优势",
|
||||
subtitle: "为什么选择玩值电竞?",
|
||||
content: (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4 p-4 rounded-xl bg-gradient-to-r from-primary/10 to-transparent border-l-4 border-primary">
|
||||
<div className="text-3xl font-black text-primary">3个</div>
|
||||
<div>
|
||||
<h4 className="font-bold">稳定内容赋能</h4>
|
||||
<p className="text-xs text-white/60">让你拥有3个AI员工:客服、企划、财务</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 p-4 rounded-xl bg-gradient-to-r from-secondary/10 to-transparent border-l-4 border-secondary">
|
||||
<div className="text-3xl font-black text-secondary">18年</div>
|
||||
<div>
|
||||
<h4 className="font-bold">服务沉淀</h4>
|
||||
<p className="text-xs text-white/60">深耕电竞行业,标准化的管理系统</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 p-4 rounded-xl bg-gradient-to-r from-accent/10 to-transparent border-l-4 border-accent">
|
||||
<div className="text-3xl font-black text-accent">3位</div>
|
||||
<div>
|
||||
<h4 className="font-bold">一体赋能</h4>
|
||||
<p className="text-xs text-white/60">内容 + AI + 服务,全方位支持</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
bg: "bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-blue-900/20 via-background to-background",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "未来愿景",
|
||||
subtitle: "构建全球领先的电竞生态平台",
|
||||
content: (
|
||||
<div className="text-center space-y-8 mt-8">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary via-accent to-secondary blur-3xl opacity-20" />
|
||||
<Award size={80} className="mx-auto text-white relative z-10" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-2xl font-bold">玩值电竞</h3>
|
||||
<p className="text-white/70 px-8">
|
||||
链接粉丝与游戏明星,让游戏玩家在电竞世界中玩得有价值。
|
||||
<br />
|
||||
<br />
|
||||
赋能超级个体,助力其发展壮大,共同推动电竞产业发展。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="button" className="px-8 py-3 rounded-full bg-white text-black font-bold hover:scale-105 transition-transform">
|
||||
加入我们
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
bg: "bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-green-900/20 via-background to-background",
|
||||
},
|
||||
]
|
||||
|
||||
export function PitchDeckPage() {
|
||||
const [currentSlide, setCurrentSlide] = useState(0)
|
||||
|
||||
const nextSlide = () => {
|
||||
setCurrentSlide((prev) => (prev + 1) % slides.length)
|
||||
}
|
||||
|
||||
const prevSlide = () => {
|
||||
setCurrentSlide((prev) => (prev - 1 + slides.length) % slides.length)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-background flex flex-col relative overflow-hidden">
|
||||
<div className={cn("absolute inset-0 transition-colors duration-1000", slides[currentSlide]!.bg)} />
|
||||
|
||||
<div className="relative z-10 p-4 flex justify-between items-center glass border-b border-white/5">
|
||||
<h1 className="font-bold text-sm tracking-widest uppercase text-white/50">Business Plan 2025</h1>
|
||||
<div className="text-xs font-mono text-primary">
|
||||
{currentSlide + 1} / {slides.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 relative z-10 flex flex-col p-6 overflow-hidden min-h-0">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentSlide}
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
className="flex-1 flex flex-col min-h-0"
|
||||
>
|
||||
<div className="mb-8 shrink-0">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="text-3xl font-bold mb-2"
|
||||
>
|
||||
{slides[currentSlide]!.title}
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="text-primary font-medium"
|
||||
>
|
||||
{slides[currentSlide]!.subtitle}
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="flex-1 min-h-0 overflow-y-auto"
|
||||
>
|
||||
{slides[currentSlide]!.content}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 p-6 pb-24 flex items-center justify-between gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevSlide}
|
||||
className="w-12 h-12 rounded-full glass flex items-center justify-center hover:bg-white/10 active:scale-95 transition-all"
|
||||
>
|
||||
<ChevronLeft />
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{slides.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn("w-2 h-2 rounded-full transition-all duration-300", i === currentSlide ? "w-6 bg-primary" : "bg-white/20")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextSlide}
|
||||
className="w-12 h-12 rounded-full bg-primary text-black flex items-center justify-center hover:bg-primary/90 active:scale-95 transition-all shadow-[0_0_20px_rgba(0,240,255,0.4)]"
|
||||
>
|
||||
<ChevronRight />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Badge({ text }: { text: string }) {
|
||||
return (
|
||||
<span className="px-3 py-1 rounded-full bg-white/10 border border-white/10 text-xs font-medium backdrop-blur-md">
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function Card({ icon: Icon, title, desc }: { icon: LucideIcon; title: string; desc: string }) {
|
||||
return (
|
||||
<div className="p-4 rounded-xl bg-white/5 border border-white/5 hover:border-primary/30 transition-colors">
|
||||
<Icon className="text-primary mb-2" size={24} />
|
||||
<h4 className="font-bold text-sm mb-1">{title}</h4>
|
||||
<p className="text-[10px] text-white/60 leading-tight">{desc}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ListItem({ number, title, desc }: { number: string; title: string; desc: string }) {
|
||||
return (
|
||||
<div className="flex gap-4 items-start p-3 rounded-xl hover:bg-white/5 transition-colors">
|
||||
<span className="text-2xl font-black text-white/10 font-mono">{number}</span>
|
||||
<div>
|
||||
<h4 className="font-bold text-white mb-1">{title}</h4>
|
||||
<p className="text-xs text-white/60">{desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
158
new/wz-app/src/pages/ProfileEditPage.tsx
Normal file
158
new/wz-app/src/pages/ProfileEditPage.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { useState } from "react"
|
||||
import { ArrowLeft, Camera, ChevronRight, Check } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useAppContext } from "@/components/providers/app-provider"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
export function ProfileEditPage() {
|
||||
const navigate = useNavigate()
|
||||
const { user, updateUser } = useAppContext()
|
||||
const [name, setName] = useState(user.name || "")
|
||||
const [bio] = useState("")
|
||||
const [gender, setGender] = useState<"male" | "female" | "unknown">("male")
|
||||
const [showAvatarPicker, setShowAvatarPicker] = useState(false)
|
||||
const [selectedAvatar, setSelectedAvatar] = useState(user.avatar || "/gamer-profile-avatar.jpg")
|
||||
|
||||
const avatarOptions = [
|
||||
"/gamer-profile-avatar.jpg",
|
||||
"/gamer-boy-esports-jersey.jpg",
|
||||
"/streamer-1.jpg",
|
||||
"/streamer-2.jpg",
|
||||
"/streamer-3.jpg",
|
||||
"/cp-avatar-1.jpg",
|
||||
"/cp-avatar-2.jpg",
|
||||
"/coach-avatar-1.jpg",
|
||||
]
|
||||
|
||||
const handleSave = () => {
|
||||
updateUser({
|
||||
name,
|
||||
avatar: selectedAvatar,
|
||||
})
|
||||
navigate(-1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="border-border bg-background/95 supports-backdrop-filter:bg-background/80 sticky top-0 z-50 border-b backdrop-blur-sm">
|
||||
<div className="flex h-14 items-center justify-between px-4">
|
||||
<Button type="button" variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ArrowLeft size={20} />
|
||||
</Button>
|
||||
<h1 className="font-bold">编辑资料</h1>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={handleSave} className="text-primary">
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 p-4">
|
||||
<div className="flex flex-col items-center py-6">
|
||||
<button
|
||||
type="button"
|
||||
className="relative"
|
||||
onClick={() => setShowAvatarPicker(true)}
|
||||
aria-label="选择头像"
|
||||
>
|
||||
<div className="border-primary h-24 w-24 overflow-hidden rounded-full border-2">
|
||||
<img
|
||||
src={selectedAvatar || "/favicon.svg"}
|
||||
alt="Avatar"
|
||||
width={96}
|
||||
height={96}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-primary absolute right-0 bottom-0 flex h-8 w-8 items-center justify-center rounded-full">
|
||||
<Camera size={16} className="text-white" />
|
||||
</div>
|
||||
</button>
|
||||
<p className="text-foreground/50 mt-2 text-sm">点击更换头像</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-card border-border overflow-hidden rounded-xl border">
|
||||
<div className="border-border flex items-center justify-between border-b p-4">
|
||||
<span className="text-foreground/70 text-sm">昵称</span>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="h-auto w-48 border-none bg-transparent p-0 text-right"
|
||||
placeholder="请输入昵称"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-border flex items-center justify-between border-b p-4">
|
||||
<span className="text-foreground/70 text-sm">玩值ID</span>
|
||||
<span className="text-foreground/50 text-sm">{user.odlId || "888888"}</span>
|
||||
</div>
|
||||
<div className="border-border flex items-center justify-between border-b p-4">
|
||||
<span className="text-foreground/70 text-sm">性别</span>
|
||||
<div className="flex gap-2">
|
||||
{["male", "female", "unknown"].map((g) => (
|
||||
<button
|
||||
type="button"
|
||||
key={g}
|
||||
onClick={() => setGender(g as typeof gender)}
|
||||
className={`rounded-full px-3 py-1 text-xs transition-colors ${
|
||||
gender === g ? "bg-primary text-white" : "text-foreground/60 bg-foreground/10"
|
||||
}`}
|
||||
>
|
||||
{g === "male" ? "男" : g === "female" ? "女" : "保密"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<span className="text-foreground/70 text-sm">个性签名</span>
|
||||
<div className="flex max-w-32 items-center gap-2">
|
||||
<span className="text-foreground/50 truncate text-sm">{bio || "这个人很懒,什么都没写"}</span>
|
||||
<ChevronRight size={16} className="text-foreground/30 shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAvatarPicker && (
|
||||
<div className="bg-black/80 fixed inset-0 z-50 flex items-end">
|
||||
<div className="bg-card w-full rounded-t-2xl p-4 pb-8">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="font-bold">选择头像</h3>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => setShowAvatarPicker(false)}>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{avatarOptions.map((avatar, i) => (
|
||||
<button
|
||||
type="button"
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setSelectedAvatar(avatar)
|
||||
setShowAvatarPicker(false)
|
||||
}}
|
||||
className="border-primary/50 relative aspect-square overflow-hidden rounded-full border-2 transition-colors"
|
||||
style={{
|
||||
borderColor: selectedAvatar === avatar ? "var(--color-primary, oklch(0.7 0.2 200))" : "transparent",
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0">
|
||||
<FillImage src={avatar || "/favicon.svg"} alt={`Avatar ${i}`} className="h-full w-full" />
|
||||
</div>
|
||||
{selectedAvatar === avatar && (
|
||||
<div className="bg-primary/30 absolute inset-0 flex items-center justify-center">
|
||||
<Check size={24} className="text-white" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
183
new/wz-app/src/pages/RechargePage.tsx
Normal file
183
new/wz-app/src/pages/RechargePage.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { ArrowLeft, Coins, Gem, Check, CreditCard } from "lucide-react"
|
||||
import { useAppContext } from "@/components/providers/app-provider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function RechargePage() {
|
||||
const navigate = useNavigate()
|
||||
const { recharge, wallet } = useAppContext()
|
||||
const [selectedAmount, setSelectedAmount] = useState<number | null>(null)
|
||||
const [selectedPayment, setSelectedPayment] = useState<"wechat" | "alipay">("wechat")
|
||||
const [showSuccess, setShowSuccess] = useState(false)
|
||||
const [purchasedCoins, setPurchasedCoins] = useState(0)
|
||||
|
||||
const amounts = [
|
||||
{ value: 6, coins: 60, bonus: 0 },
|
||||
{ value: 30, coins: 300, bonus: 20 },
|
||||
{ value: 68, coins: 680, bonus: 50 },
|
||||
{ value: 128, coins: 1280, bonus: 120 },
|
||||
{ value: 328, coins: 3280, bonus: 350 },
|
||||
{ value: 648, coins: 6480, bonus: 800 },
|
||||
]
|
||||
|
||||
const handleRecharge = () => {
|
||||
if (selectedAmount === null) return
|
||||
const item = amounts[selectedAmount]!
|
||||
const totalCoins = item.coins + item.bonus
|
||||
recharge(totalCoins)
|
||||
setPurchasedCoins(totalCoins)
|
||||
setShowSuccess(true)
|
||||
}
|
||||
|
||||
const selectedItem = selectedAmount !== null ? amounts[selectedAmount] : null
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-32">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="text-white/70 hover:text-white">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">充值中心</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="glass-card p-4 rounded-2xl flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center">
|
||||
<Coins size={24} className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-white/60">当前余额</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{wallet.balance} <span className="text-sm text-white/60">币</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-24 rounded-2xl bg-gradient-to-r from-purple-600 to-blue-600 relative overflow-hidden flex items-center px-6">
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-lg font-bold text-white mb-1">首充双倍</h2>
|
||||
<p className="text-xs text-white/80">限时特惠 赠送绝版头像框</p>
|
||||
</div>
|
||||
<div className="absolute right-0 bottom-0 w-24 h-24 opacity-50">
|
||||
<Gem className="w-full h-full text-white/20 rotate-12 translate-x-6 translate-y-6" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-bold text-sm text-white/70">选择充值金额</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{amounts.map((item, index) => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
className={`glass-card p-3 rounded-xl flex flex-col items-center justify-center gap-1.5 transition-all border ${
|
||||
selectedAmount === index
|
||||
? "border-primary bg-primary/20 ring-2 ring-primary/50"
|
||||
: "border-white/5 hover:bg-white/10 hover:border-primary/50"
|
||||
}`}
|
||||
onClick={() => setSelectedAmount(index)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Coins size={14} className="text-primary" />
|
||||
<span className="font-bold">{item.coins}</span>
|
||||
</div>
|
||||
<span className="text-xs text-white/70">¥{item.value}</span>
|
||||
{item.bonus > 0 && (
|
||||
<span className="text-[10px] text-yellow-400 bg-yellow-500/10 px-1.5 rounded-full">送{item.bonus}币</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-bold text-sm text-white/70">支付方式</h3>
|
||||
<div className="glass-card rounded-xl p-0 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedPayment("wechat")}
|
||||
className="w-full flex items-center gap-3 p-4 border-b border-white/5 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-green-500/20 flex items-center justify-center">
|
||||
<span className="text-green-500 text-lg font-bold">微</span>
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-bold text-sm">微信支付</div>
|
||||
<div className="text-[10px] text-white/40">推荐使用</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
||||
selectedPayment === "wechat" ? "border-primary bg-primary" : "border-white/20"
|
||||
}`}
|
||||
>
|
||||
{selectedPayment === "wechat" && <Check size={12} className="text-white" />}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedPayment("alipay")}
|
||||
className="w-full flex items-center gap-3 p-4 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center">
|
||||
<span className="text-blue-500 text-lg font-bold">支</span>
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-bold text-sm">支付宝</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
||||
selectedPayment === "alipay" ? "border-primary bg-primary" : "border-white/20"
|
||||
}`}
|
||||
>
|
||||
{selectedPayment === "alipay" && <Check size={12} className="text-white" />}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-white/30">充值即代表同意《用户充值协议》</p>
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-16 left-0 right-0 p-4 bg-gradient-to-t from-background via-background to-transparent">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm text-white/60">应付金额</span>
|
||||
<span className="text-xl font-bold text-primary">
|
||||
{selectedItem ? `¥${selectedItem.value}` : "请选择金额"}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleRecharge}
|
||||
disabled={selectedAmount === null}
|
||||
className="w-full h-12 bg-gradient-to-r from-primary to-cyan-500 hover:from-primary/90 hover:to-cyan-500/90 text-white font-bold text-base rounded-xl disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<CreditCard size={18} className="mr-2" />
|
||||
立即充值
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showSuccess && (
|
||||
<div className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
|
||||
<div className="bg-card rounded-2xl p-6 w-full max-w-sm border border-border animate-in zoom-in-95 duration-200">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-16 h-16 rounded-full bg-green-500/20 flex items-center justify-center mb-4">
|
||||
<Check size={32} className="text-green-500" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-foreground mb-2">充值成功</h3>
|
||||
<p className="text-sm text-foreground/60 mb-4">
|
||||
已充值 <span className="text-primary font-bold">{purchasedCoins}</span> 币
|
||||
</p>
|
||||
<p className="text-xs text-foreground/40 mb-6">当前余额: {wallet.balance} 币</p>
|
||||
<Button type="button" onClick={() => setShowSuccess(false)} className="w-full bg-primary hover:bg-primary/90">
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
new/wz-app/src/pages/ScreenPage.tsx
Normal file
12
new/wz-app/src/pages/ScreenPage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* C 端数据大屏轻量壳:与 old `/screen` 对应;完整运营大屏请使用 wz-admin 或独立部署同源 Dashboard。
|
||||
*/
|
||||
export function ScreenPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-900 text-white flex flex-col items-center justify-center p-8 text-center">
|
||||
<h1 className="text-2xl font-bold mb-2">玩值 · 数据大屏</h1>
|
||||
<p className="text-sm text-white/70 max-w-md mb-6">公网展示大屏与后台资产看板请在管理端或独立子域打开;此处为路由占位,避免引用 Next admin 包。</p>
|
||||
<p className="text-xs text-white/40">TODO: 接入 wz-api 公开只读指标或 iframe 嵌入运营大屏</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
125
new/wz-app/src/pages/SettingsPage.tsx
Normal file
125
new/wz-app/src/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useState } from "react"
|
||||
import { ArrowLeft, ChevronRight, Moon, Bell, Shield, Globe, Info, Trash2 } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
|
||||
export function SettingsPage() {
|
||||
const navigate = useNavigate()
|
||||
const [darkMode, setDarkMode] = useState(true)
|
||||
const [notifications, setNotifications] = useState(true)
|
||||
const [showClearConfirm, setShowClearConfirm] = useState(false)
|
||||
|
||||
const handleClearCache = () => {
|
||||
try {
|
||||
localStorage.clear()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setShowClearConfirm(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-24">
|
||||
<header className="border-border bg-background/95 supports-backdrop-filter:bg-background/80 sticky top-0 z-40 flex items-center gap-4 border-b px-4 py-3 backdrop-blur-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
className="hover:bg-foreground/10 rounded-full p-2 transition-colors"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="text-lg font-bold">设置</h1>
|
||||
</header>
|
||||
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="bg-card border-border overflow-hidden rounded-xl border">
|
||||
<div className="border-border flex items-center justify-between border-b p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Moon size={18} className="text-foreground/60" />
|
||||
<span className="text-sm">深色模式</span>
|
||||
</div>
|
||||
<Switch checked={darkMode} onCheckedChange={setDarkMode} />
|
||||
</div>
|
||||
<div className="border-border flex items-center justify-between border-b p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Bell size={18} className="text-foreground/60" />
|
||||
<span className="text-sm">消息通知</span>
|
||||
</div>
|
||||
<Switch checked={notifications} onCheckedChange={setNotifications} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/account-help")}
|
||||
className="flex w-full items-center justify-between p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield size={18} className="text-foreground/60" />
|
||||
<span className="text-sm">账号安全</span>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-foreground/30" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border-border overflow-hidden rounded-xl border">
|
||||
<button type="button" className="border-border flex w-full items-center justify-between border-b p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Globe size={18} className="text-foreground/60" />
|
||||
<span className="text-sm">语言设置</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-foreground/50 text-xs">简体中文</span>
|
||||
<ChevronRight size={16} className="text-foreground/30" />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowClearConfirm(true)}
|
||||
className="border-border flex w-full items-center justify-between border-b p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Trash2 size={18} className="text-foreground/60" />
|
||||
<span className="text-sm">清除缓存</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-foreground/50 text-xs">12.5MB</span>
|
||||
<ChevronRight size={16} className="text-foreground/30" />
|
||||
</div>
|
||||
</button>
|
||||
<button type="button" className="flex w-full items-center justify-between p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Info size={18} className="text-foreground/60" />
|
||||
<span className="text-sm">关于我们</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-foreground/50 text-xs">v1.0.0</span>
|
||||
<ChevronRight size={16} className="text-foreground/30" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showClearConfirm && (
|
||||
<div className="bg-black/80 fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-card border-border w-full max-w-sm rounded-2xl border p-6">
|
||||
<h3 className="text-foreground mb-2 text-center text-lg font-bold">清除缓存</h3>
|
||||
<p className="text-foreground/60 mb-6 text-center text-sm">确定要清除所有缓存数据吗?</p>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="border-border flex-1 bg-transparent"
|
||||
onClick={() => setShowClearConfirm(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="button" className="flex-1 bg-red-500 hover:bg-red-600" onClick={handleClearCache}>
|
||||
确认清除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
182
new/wz-app/src/pages/StarDetailPage.tsx
Normal file
182
new/wz-app/src/pages/StarDetailPage.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import { useState } from "react"
|
||||
import { useParams, useNavigate, Link } from "react-router-dom"
|
||||
import { ArrowLeft, Star, Trophy, Users, MessageCircle, Share2, Sparkles, PlayCircle, Heart } from "lucide-react"
|
||||
import { useAppContext } from "@/components/providers/app-provider"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
|
||||
const starData: Record<
|
||||
string,
|
||||
{
|
||||
name: string
|
||||
title: string
|
||||
avatar: string
|
||||
cover: string
|
||||
rating: number
|
||||
fans: string
|
||||
intro: string
|
||||
games: string[]
|
||||
services: { id: string; name: string; price: number; desc: string; courseId?: string; hot?: boolean }[]
|
||||
}
|
||||
> = {
|
||||
"1": {
|
||||
name: "GM-远洋",
|
||||
title: "国服第一盲僧",
|
||||
avatar: "/streamer-1.jpg",
|
||||
cover: "/live-lol.jpg",
|
||||
rating: 4.9,
|
||||
fans: "46.8万",
|
||||
intro: "前职业战队打野选手,擅长盲僧、千珏。教学耐心,带你轻松上分。",
|
||||
games: ["英雄联盟", "无畏契约"],
|
||||
services: [
|
||||
{ id: "s1", name: "单局陪玩", price: 50, desc: "一起开黑边玩边教" },
|
||||
{ id: "s2", name: "5局套餐", price: 220, desc: "连续5局享优惠" },
|
||||
{ id: "s3", name: "打野进阶课", price: 299, desc: "8节系统课", courseId: "1" },
|
||||
],
|
||||
},
|
||||
"2": {
|
||||
name: "卡若",
|
||||
title: "前职业选手",
|
||||
avatar: "/streamer-2.jpg",
|
||||
cover: "/live-valorant.jpg",
|
||||
rating: 4.8,
|
||||
fans: "12万",
|
||||
intro: "无畏契约、FPS 全精通,枪法与意识双修。",
|
||||
games: ["无畏契约", "CS:GO"],
|
||||
services: [
|
||||
{ id: "s1", name: "单局陪玩", price: 60, desc: "排位陪练" },
|
||||
{ id: "s2", name: "枪法训练营", price: 399, desc: "完整课程", courseId: "2", hot: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export function StarDetailPage() {
|
||||
const { id = "1" } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { pay } = useAppContext()
|
||||
const [followed, setFollowed] = useState(false)
|
||||
const star = starData[id] ?? starData["1"]!
|
||||
|
||||
const onBook = (svc: (typeof star.services)[0]) => {
|
||||
if (svc.courseId) {
|
||||
void navigate(`/course/${svc.courseId}`)
|
||||
return
|
||||
}
|
||||
const ok = pay(svc.price, `${star.name} · ${svc.name}`, "service", star.avatar)
|
||||
if (ok) toast({ title: "下单成功", description: "可在「消息」中查看" })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-28">
|
||||
<div className="relative h-56">
|
||||
<FillImage src={star.cover} alt="cover" className="h-56" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background to-transparent" />
|
||||
<div className="absolute top-0 left-0 right-0 p-4 flex items-center justify-between">
|
||||
<button type="button" onClick={() => void navigate(-1)} className="p-2 rounded-full bg-black/30 text-white" aria-label="返回">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded-full bg-black/30 text-white"
|
||||
onClick={() => toast({ title: "分享", description: "链接已复制(演示)" })}
|
||||
aria-label="分享"
|
||||
>
|
||||
<Share2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 -mt-16 relative z-10">
|
||||
<div className="flex items-end gap-4 mb-4">
|
||||
<div className="w-28 h-28 rounded-2xl border-4 border-background overflow-hidden relative shadow-xl">
|
||||
<FillImage src={star.avatar} alt={star.name} className="h-28 w-28" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 pb-1">
|
||||
<h1 className="text-xl font-bold flex items-center gap-2">
|
||||
{star.name}
|
||||
{star.services.some((s) => s.hot) && <Sparkles className="text-primary w-4 h-4" />}
|
||||
</h1>
|
||||
<p className="text-sm text-primary">{star.title}</p>
|
||||
<div className="flex flex-wrap gap-2 mt-2 text-xs text-white/50">
|
||||
<span className="flex items-center gap-1">
|
||||
<Star className="w-3 h-3 text-primary fill-primary" />
|
||||
{star.rating}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="w-3 h-3" />
|
||||
{star.fans} 粉丝
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mb-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant={followed ? "secondary" : "default"}
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setFollowed(!followed)
|
||||
toast({ title: followed ? "已取消关注" : "已关注" })
|
||||
}}
|
||||
>
|
||||
<Heart className="w-4 h-4 mr-1" />
|
||||
{followed ? "已关注" : "关注"}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" className="flex-1" onClick={() => void navigate("/messages")}>
|
||||
<MessageCircle className="w-4 h-4 mr-1" />
|
||||
私聊
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-white/70 mb-4 leading-relaxed">{star.intro}</p>
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{star.games.map((g) => (
|
||||
<span key={g} className="px-2 py-1 rounded-full bg-white/5 text-xs">
|
||||
{g}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3 className="font-bold mb-3 flex items-center gap-2">
|
||||
<Trophy className="w-4 h-4 text-primary" />
|
||||
服务与课程
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{star.services.map((svc) => (
|
||||
<div key={svc.id} className="glass-card p-4 rounded-xl flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="font-bold text-sm flex items-center gap-2">
|
||||
{svc.name}
|
||||
{svc.hot && <span className="text-[10px] px-1.5 py-0.5 rounded bg-primary/20 text-primary">热</span>}
|
||||
</div>
|
||||
<p className="text-xs text-white/50 mt-1">{svc.desc}</p>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<div className="text-primary font-bold">{svc.price} 币</div>
|
||||
<Button size="sm" className="mt-1 h-7 text-xs" type="button" onClick={() => onBook(svc)}>
|
||||
{svc.courseId ? "查看课程" : "下单"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link to={`/live/${id}`} className="mt-6 flex items-center gap-3 p-3 rounded-xl bg-white/5 hover:bg-white/10">
|
||||
<div className="w-20 h-12 rounded overflow-hidden relative shrink-0">
|
||||
<FillImage src={star.cover} alt="live" className="h-12" />
|
||||
<span className="absolute top-0.5 left-0.5 px-1 rounded bg-primary text-[8px] text-black font-bold">LIVE</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-bold">进入直播间</div>
|
||||
<div className="text-[10px] text-primary">与主播互动</div>
|
||||
</div>
|
||||
<PlayCircle className="w-6 h-6 text-white/60" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
94
new/wz-app/src/pages/TransactionHistoryPage.tsx
Normal file
94
new/wz-app/src/pages/TransactionHistoryPage.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { ArrowLeft, ArrowUpRight, ArrowDownLeft, Filter } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useAppContext } from "@/components/providers/app-provider"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
export function TransactionHistoryPage() {
|
||||
const navigate = useNavigate()
|
||||
const { orders, wallet } = useAppContext()
|
||||
|
||||
const incomeOrders = orders.filter((o) => o.type === "recharge")
|
||||
const expenseOrders = orders.filter((o) => o.type !== "recharge")
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-24">
|
||||
<header className="sticky top-0 z-40 bg-background/95 backdrop-blur-sm border-b border-border px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => void navigate(-1)} className="p-2 hover:bg-foreground/10 rounded-full transition-colors" type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">账单明细</h1>
|
||||
</div>
|
||||
<button className="p-2 hover:bg-foreground/10 rounded-full transition-colors" type="button">
|
||||
<Filter size={18} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="mx-4 mt-4 p-4 bg-gradient-to-r from-primary to-cyan-500 rounded-xl text-white">
|
||||
<p className="text-sm opacity-80 mb-1">当前余额</p>
|
||||
<p className="text-3xl font-bold">
|
||||
{wallet.balance} <span className="text-lg">币</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="all" className="w-full mt-4">
|
||||
<div className="px-4">
|
||||
<TabsList className="w-full bg-foreground/5 p-1 rounded-xl">
|
||||
<TabsTrigger value="all" className="flex-1 text-xs">
|
||||
全部
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="income" className="flex-1 text-xs">
|
||||
收入
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="expense" className="flex-1 text-xs">
|
||||
支出
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{(
|
||||
[
|
||||
{ key: "all" as const, data: orders },
|
||||
{ key: "income" as const, data: incomeOrders },
|
||||
{ key: "expense" as const, data: expenseOrders },
|
||||
] as const
|
||||
).map(({ key, data }) => (
|
||||
<TabsContent key={key} value={key} className="px-4 mt-4 space-y-3">
|
||||
{data.length > 0 ? (
|
||||
data.map((order) => (
|
||||
<div key={order.id} className="bg-card rounded-xl p-4 border border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
order.type === "recharge" ? "bg-green-500/10 text-green-500" : "bg-red-500/10 text-red-500"
|
||||
}`}
|
||||
>
|
||||
{order.type === "recharge" ? <ArrowDownLeft size={20} /> : <ArrowUpRight size={20} />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{order.title}</p>
|
||||
<p className="text-xs text-foreground/50">{order.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className={`font-bold ${order.type === "recharge" ? "text-green-500" : "text-foreground"}`}>
|
||||
{order.type === "recharge" ? "+" : "-"}
|
||||
{order.price}
|
||||
</p>
|
||||
<p className="text-[10px] text-foreground/40">币</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-foreground/50">
|
||||
<p>暂无记录</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
135
new/wz-app/src/pages/VipPage.tsx
Normal file
135
new/wz-app/src/pages/VipPage.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useState } from "react"
|
||||
import { ArrowLeft, Crown, Coins, Building2, Coffee, Gamepad2, Gift, Star } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useAppContext } from "@/components/providers/app-provider"
|
||||
|
||||
export function VipPage() {
|
||||
const navigate = useNavigate()
|
||||
const { pay, updateUser, wallet } = useAppContext()
|
||||
const [selectedPlan, setSelectedPlan] = useState<"month" | "quarter" | "year">("month")
|
||||
|
||||
type PlanKey = "month" | "quarter" | "year"
|
||||
type PlanItem = { price: number; coins: number; label: string; days: number; save?: string }
|
||||
const plans: Record<PlanKey, PlanItem> = {
|
||||
month: { price: 30, coins: 300, label: "月卡", days: 30 },
|
||||
quarter: { price: 80, coins: 800, label: "季卡", days: 90, save: "11%" },
|
||||
year: { price: 280, coins: 2800, label: "年卡", days: 365, save: "22%" },
|
||||
}
|
||||
|
||||
const benefits = [
|
||||
{ icon: Building2, title: "电竞酒店9折", desc: "全国合作酒店通用" },
|
||||
{ icon: Coffee, title: "网咖8折特权", desc: "特权公馆专属" },
|
||||
{ icon: Gamepad2, title: "陪玩优先匹配", desc: "专属匹配通道" },
|
||||
{ icon: Gift, title: "每月专属礼包", desc: "限定装扮+道具" },
|
||||
{ icon: Star, title: "专属客服", desc: "1对1VIP服务" },
|
||||
{ icon: Crown, title: "尊贵标识", desc: "个人主页专属展示" },
|
||||
]
|
||||
|
||||
const handleSubscribe = () => {
|
||||
const plan = plans[selectedPlan]
|
||||
const success = pay(plan.coins, `玩值黑卡会员${plan.label}`, "service")
|
||||
if (success) {
|
||||
updateUser({ isVip: true })
|
||||
void navigate("/profile")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-yellow-900/20 to-background pb-32">
|
||||
<header className="sticky top-0 z-40 bg-black/50 backdrop-blur-md px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="text-white/70 hover:text-white">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">玩值黑卡会员</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="relative bg-gradient-to-br from-yellow-600 via-yellow-700 to-yellow-900 rounded-2xl p-6 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-yellow-400/20 rounded-full blur-3xl" />
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-full bg-black/30 flex items-center justify-center">
|
||||
<Crown size={24} className="text-yellow-300" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-bold text-xl text-white">玩值黑卡</h2>
|
||||
<p className="text-xs text-yellow-200/70">尊享电竞特权</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-yellow-200/80 text-sm">
|
||||
<Coins size={16} />
|
||||
<span>当前余额: {wallet.balance} 币</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-bold text-sm text-white/70">选择套餐</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{(["month", "quarter", "year"] as const).map((key) => {
|
||||
const plan = plans[key]
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => setSelectedPlan(key)}
|
||||
className={`relative p-4 rounded-xl border-2 transition-all ${
|
||||
selectedPlan === key ? "border-yellow-500 bg-yellow-500/10" : "border-white/10 bg-white/5 hover:border-white/20"
|
||||
}`}
|
||||
>
|
||||
{plan.save && (
|
||||
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-[10px] px-1.5 py-0.5 rounded-full font-bold">
|
||||
省{plan.save}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-white/60 mb-1">{plan.label}</p>
|
||||
<p className="text-xl font-bold text-yellow-400">{plan.coins}</p>
|
||||
<p className="text-[10px] text-white/40">币</p>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-bold text-sm text-white/70">会员特权</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{benefits.map((benefit, i) => (
|
||||
<div key={i} className="bg-white/5 border border-white/5 rounded-xl p-3 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-yellow-500/10 flex items-center justify-center">
|
||||
<benefit.icon size={20} className="text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold">{benefit.title}</p>
|
||||
<p className="text-[10px] text-white/40">{benefit.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-16 left-0 right-0 px-4 py-3 bg-background/95 backdrop-blur-lg border-t border-white/10 z-30">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-white/60">已选: {plans[selectedPlan].label}</p>
|
||||
<p className="text-xl font-bold text-yellow-400 flex items-center gap-1">
|
||||
<Coins size={18} />
|
||||
{plans[selectedPlan].coins} 币
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubscribe}
|
||||
className="px-8 py-3 rounded-full bg-gradient-to-r from-yellow-500 to-yellow-600 text-black font-bold active:scale-95 transition-transform flex items-center gap-2"
|
||||
>
|
||||
<Coins size={18} />
|
||||
立即开通
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
88
new/wz-app/src/pages/ZoneDetailPage.tsx
Normal file
88
new/wz-app/src/pages/ZoneDetailPage.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ArrowLeft, Users, Trophy, Sword, Flame } from "lucide-react"
|
||||
import { useParams, useNavigate } from "react-router-dom"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
|
||||
type ZoneGame = { name: string; barClass: string; btnClass: string; image: string; query: string }
|
||||
|
||||
const GAMES: Record<string, ZoneGame> = {
|
||||
lol: { name: "英雄联盟", barClass: "bg-blue-500", btnClass: "bg-blue-600", image: "/game-lol.jpg", query: "lol" },
|
||||
val: { name: "无畏契约", barClass: "bg-red-500", btnClass: "bg-red-600", image: "/point-card-valorant.jpg", query: "valorant" },
|
||||
hok: { name: "王者荣耀", barClass: "bg-yellow-500", btnClass: "bg-yellow-600", image: "/live-hok.jpg", query: "hok" },
|
||||
csgo: { name: "CS:GO", barClass: "bg-orange-500", btnClass: "bg-orange-600", image: "/game-csgo.jpg", query: "csgo" },
|
||||
wow: { name: "魔兽世界", barClass: "bg-amber-600", btnClass: "bg-stone-600", image: "/wow-bg.jpg", query: "wow" },
|
||||
}
|
||||
|
||||
export function ZoneDetailPage() {
|
||||
const { id = "lol" } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const game = GAMES[id] ?? GAMES.lol!
|
||||
const teams = [1, 2, 3]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950 text-white pb-24">
|
||||
<div className="sticky top-0 z-40 p-4 flex items-center gap-4 bg-black/40 backdrop-blur-md">
|
||||
<button type="button" onClick={() => void navigate(-1)} className="p-2 bg-white/10 rounded-full hover:bg-white/20">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">{game.name}专区</h1>
|
||||
</div>
|
||||
|
||||
<div className="relative aspect-video w-full">
|
||||
<FillImage src={game.image} alt={game.name} className="aspect-video object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent" />
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<h2 className="text-2xl font-bold mb-2">{game.name}</h2>
|
||||
<div className="flex gap-2">
|
||||
<span className="px-2 py-1 bg-white/10 rounded text-xs backdrop-blur-md">热度 1205万</span>
|
||||
<span className="px-2 py-1 bg-white/10 rounded text-xs backdrop-blur-md">开黑 3.2万车队</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-2 p-4">
|
||||
{[
|
||||
{ icon: Sword, label: "找车队" },
|
||||
{ icon: Trophy, label: "打比赛" },
|
||||
{ icon: Users, label: "找陪练" },
|
||||
{ icon: Flame, label: "看直播" },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.label}
|
||||
type="button"
|
||||
onClick={() => toast({ title: item.label, description: "功能演示,待接 wz-api" })}
|
||||
className="bg-white/5 rounded-xl p-3 flex flex-col items-center gap-2 hover:bg-white/10"
|
||||
>
|
||||
<item.icon className="text-white/80" />
|
||||
<span className="text-xs">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="px-4 space-y-4">
|
||||
<h3 className="font-bold flex items-center gap-2">
|
||||
<div className={`w-1 h-4 ${game.barClass} rounded-full`} />
|
||||
热门车队
|
||||
</h3>
|
||||
{teams.map((i) => (
|
||||
<div key={i} className="bg-white/5 rounded-xl p-4 flex items-center gap-4 min-w-0">
|
||||
<div className="w-12 h-12 shrink-0 rounded-full bg-white/10 relative overflow-hidden">
|
||||
<FillImage src="/avatar-cute.jpg" alt="Avatar" className="h-12 w-12" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-bold text-sm truncate">钻石冲分车队 缺{i}</h4>
|
||||
<p className="text-xs text-white/50 truncate">胜率队 | 只要狠人 | {game.query}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toast({ title: "已申请", description: "等待队长通过" })}
|
||||
className={`shrink-0 px-4 py-1.5 rounded-full text-xs font-bold text-white ${game.btnClass}`}
|
||||
>
|
||||
申请
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
284
new/wz-app/src/pages/cp/CpDetailPage.tsx
Normal file
284
new/wz-app/src/pages/cp/CpDetailPage.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import { useState, type KeyboardEvent } from "react"
|
||||
import { useParams, useNavigate } from "react-router-dom"
|
||||
import { ArrowLeft, Heart, Star, MessageCircle, Shield, Trophy, Users, Gamepad2, Send, Phone, Video } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const CP_DATA: Record<
|
||||
string,
|
||||
{
|
||||
name: string
|
||||
gender: string
|
||||
age: number
|
||||
avatar: string
|
||||
cover: string
|
||||
games: { game: string; rank: string; role: string }[]
|
||||
voice: string
|
||||
bio: string
|
||||
tags: string[]
|
||||
friends: number
|
||||
matches: number
|
||||
online: boolean
|
||||
}
|
||||
> = {
|
||||
"1": {
|
||||
name: "甜心辅助",
|
||||
gender: "女",
|
||||
age: 22,
|
||||
avatar: "/cp-avatar-1.jpg",
|
||||
cover: "/cp-cover-1.jpg",
|
||||
games: [
|
||||
{ game: "王者荣耀", rank: "星耀I", role: "瑶、大乔、明世隐" },
|
||||
{ game: "和平精英", rank: "无敌战神", role: "四排辅助位" },
|
||||
],
|
||||
voice: "温柔甜美",
|
||||
bio: "喜欢玩辅助,找个靠谱的ADC一起开黑~",
|
||||
tags: ["声音好听", "技术不错", "温柔", "有耐心", "爱聊天"],
|
||||
friends: 68,
|
||||
matches: 385,
|
||||
online: true,
|
||||
},
|
||||
"2": {
|
||||
name: "职业打野",
|
||||
gender: "男",
|
||||
age: 24,
|
||||
avatar: "/cp-avatar-2.jpg",
|
||||
cover: "/cp-cover-2.jpg",
|
||||
games: [
|
||||
{ game: "英雄联盟", rank: "大师", role: "打野位" },
|
||||
{ game: "无畏契约", rank: "超凡入圣", role: "突破手" },
|
||||
],
|
||||
voice: "成熟稳重",
|
||||
bio: "前职业选手,带你上分不是梦",
|
||||
tags: ["技术流", "有耐心", "幽默", "靠谱", "Carry"],
|
||||
friends: 156,
|
||||
matches: 1230,
|
||||
online: true,
|
||||
},
|
||||
"3": {
|
||||
name: "软萌小姐姐",
|
||||
gender: "女",
|
||||
age: 20,
|
||||
avatar: "/cp-avatar-3.jpg",
|
||||
cover: "/cp-cover-3.jpg",
|
||||
games: [
|
||||
{ game: "王者荣耀", rank: "钻石II", role: "法师/辅助" },
|
||||
{ game: "原神", rank: "56级", role: "休闲探索" },
|
||||
],
|
||||
voice: "软萌可爱",
|
||||
bio: "游戏菜但是爱玩,找个不嫌弃的~",
|
||||
tags: ["萌妹子", "爱聊天", "佛系", "不坑", "快乐游戏"],
|
||||
friends: 42,
|
||||
matches: 198,
|
||||
online: false,
|
||||
},
|
||||
"4": {
|
||||
name: "中单刺客",
|
||||
gender: "男",
|
||||
age: 23,
|
||||
avatar: "/cp-avatar-4.jpg",
|
||||
cover: "/cp-cover-4.jpg",
|
||||
games: [{ game: "英雄联盟", rank: "王者", role: "中单刺客" }],
|
||||
voice: "阳光帅气",
|
||||
bio: "擅长刺客中单,带妹上分专业户",
|
||||
tags: ["Carry型", "稳定", "负责", "技术强", "有趣"],
|
||||
friends: 89,
|
||||
matches: 756,
|
||||
online: true,
|
||||
},
|
||||
}
|
||||
|
||||
export function CpDetailPage() {
|
||||
const { id = "1" } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const cp = CP_DATA[id] ?? CP_DATA["1"]!
|
||||
const [isFollowing, setIsFollowing] = useState(false)
|
||||
const [showChat, setShowChat] = useState(false)
|
||||
const [message, setMessage] = useState("")
|
||||
const [messages, setMessages] = useState<{ from: string; text: string }[]>([])
|
||||
|
||||
const handleFollow = () => {
|
||||
setIsFollowing(!isFollowing)
|
||||
toast({ title: isFollowing ? "已取消关注" : "关注成功", description: isFollowing ? "" : `你已关注${cp.name}` })
|
||||
}
|
||||
|
||||
const handleSendRequest = () => {
|
||||
setShowChat(true)
|
||||
setMessages([{ from: "system", text: `你已向${cp.name}发送了交友请求,对方已同意~` }])
|
||||
}
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!message.trim()) return
|
||||
setMessages((prev) => [...prev, { from: "me", text: message }])
|
||||
setMessage("")
|
||||
setTimeout(() => setMessages((prev) => [...prev, { from: "cp", text: "好呀,一起开黑吧~" }]), 1500)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") handleSendMessage()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-24 relative">
|
||||
<div className="h-48 relative">
|
||||
<FillImage src={cp.cover || "/placeholder.svg"} alt="Cover" className="h-48" />
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-background" />
|
||||
<button onClick={() => void navigate(-1)} className="absolute top-4 left-4 p-2 rounded-full bg-black/20 backdrop-blur-md text-white z-10" type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
{cp.online && (
|
||||
<div className="absolute top-4 right-4 px-3 py-1 rounded-full bg-green-500/20 border border-green-500/50 text-green-400 text-xs flex items-center gap-1 z-10">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
在线
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 -mt-12 relative z-10">
|
||||
<div className="flex items-end justify-between mb-4">
|
||||
<div className="w-24 h-24 rounded-2xl border-4 border-background bg-muted relative overflow-hidden shadow-xl">
|
||||
<FillImage src={cp.avatar || "/placeholder.svg"} alt="Avatar" className="h-24 w-24 rounded-2xl" />
|
||||
{cp.online && <div className="absolute bottom-0 right-0 w-5 h-5 rounded-full bg-green-500 border-2 border-background z-10" />}
|
||||
</div>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={`h-8 text-xs gap-1 bg-transparent ${isFollowing ? "border-white/20 text-white/60" : "border-pink-500/50 text-pink-400 hover:bg-pink-500/10"}`}
|
||||
type="button"
|
||||
onClick={handleFollow}
|
||||
>
|
||||
<Heart size={14} className={isFollowing ? "fill-current" : ""} />
|
||||
{isFollowing ? "已关注" : "关注"}
|
||||
</Button>
|
||||
<Button size="sm" className="h-8 text-xs bg-pink-500 gap-1 hover:bg-pink-600" type="button" onClick={handleSendRequest}>
|
||||
<MessageCircle size={14} />
|
||||
发送请求
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h1 className="text-2xl font-bold">{cp.name}</h1>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-[10px] border font-bold ${cp.gender === "女" ? "bg-pink-500/20 text-pink-400 border-pink-500/30" : "bg-blue-500/20 text-blue-400 border-blue-500/30"}`}
|
||||
>
|
||||
{cp.gender} · {cp.age}岁
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-white/60 mb-3">{cp.bio}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-white/40">
|
||||
<span className="flex items-center gap-1">
|
||||
<Star size={12} className="text-primary" />
|
||||
{cp.voice}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Trophy size={12} className="text-accent" />
|
||||
{cp.matches} 场游戏
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={12} className="text-secondary" />
|
||||
{cp.friends} 位好友
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<h3 className="font-bold text-sm flex items-center gap-2">
|
||||
<Gamepad2 size={16} className="text-primary" />
|
||||
擅长游戏
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{cp.games.map((item) => (
|
||||
<div key={item.game} className="glass-card p-3 rounded-xl">
|
||||
<div className="font-bold text-sm mb-1">{item.game}</div>
|
||||
<div className="text-xs text-white/50">{item.rank}</div>
|
||||
<div className="text-xs text-primary mt-1">{item.role}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<h3 className="font-bold text-sm">个人标签</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{cp.tags.map((tag) => (
|
||||
<span key={tag} className="px-3 py-1.5 rounded-full bg-pink-500/10 border border-pink-500/30 text-xs text-pink-400">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 rounded-xl bg-gradient-to-r from-pink-500/10 to-transparent border-l-4 border-pink-500">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield size={24} className="text-pink-500" />
|
||||
<div>
|
||||
<h4 className="font-bold text-sm">平台实名认证</h4>
|
||||
<p className="text-xs text-white/60">真实身份,安全交友</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showChat && (
|
||||
<div className="fixed inset-0 z-50 bg-background flex flex-col">
|
||||
<header className="glass px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={() => setShowChat(false)} className="p-2 -ml-2" type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-muted relative overflow-hidden">
|
||||
<FillImage src={cp.avatar || "/placeholder.svg"} alt={cp.name} className="h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm">{cp.name}</div>
|
||||
<div className="text-[10px] text-green-400">在线</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="p-2 hover:bg-white/5 rounded-full" type="button">
|
||||
<Phone size={18} />
|
||||
</button>
|
||||
<button className="p-2 hover:bg-white/5 rounded-full" type="button">
|
||||
<Video size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i} className={`flex ${msg.from === "me" ? "justify-end" : "justify-start"}`}>
|
||||
{msg.from === "system" ? (
|
||||
<div className="text-xs text-white/40 text-center w-full py-2">{msg.text}</div>
|
||||
) : (
|
||||
<div className={`max-w-[70%] px-4 py-2 rounded-2xl ${msg.from === "me" ? "bg-pink-500 text-white" : "bg-white/10 text-white"}`}>{msg.text}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 glass">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="输入消息..."
|
||||
className="flex-1 bg-white/5 rounded-full px-4 py-2 text-sm outline-none"
|
||||
/>
|
||||
<button onClick={handleSendMessage} className="p-2 bg-pink-500 rounded-full" type="button">
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
127
new/wz-app/src/pages/cp/CpListPage.tsx
Normal file
127
new/wz-app/src/pages/cp/CpListPage.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { ArrowLeft, Heart, Star, MessageCircle, Sparkles } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const CP_LIST = [
|
||||
{ id: "1", name: "甜心辅助", gender: "女" as const, age: 22, avatar: "/cp-avatar-1.jpg", games: ["王者荣耀", "和平精英"], voice: "温柔甜美", rank: "星耀", online: true, tags: ["声音好听", "技术不错", "温柔"], bio: "喜欢玩辅助,找个靠谱的ADC一起开黑~" },
|
||||
{ id: "2", name: "职业打野", gender: "男" as const, age: 24, avatar: "/cp-avatar-2.jpg", games: ["英雄联盟", "无畏契约"], voice: "成熟稳重", rank: "大师", online: true, tags: ["技术流", "有耐心", "幽默"], bio: "前职业选手,带你上分不是梦" },
|
||||
{ id: "3", name: "软萌小姐姐", gender: "女" as const, age: 20, avatar: "/cp-avatar-3.jpg", games: ["王者荣耀", "原神"], voice: "软萌可爱", rank: "钻石", online: false, tags: ["萌妹子", "爱聊天", "佛系"], bio: "游戏菜但是爱玩,找个不嫌弃的~" },
|
||||
{ id: "4", name: "中单刺客", gender: "男" as const, age: 23, avatar: "/cp-avatar-4.jpg", games: ["英雄联盟"], voice: "阳光帅气", rank: "王者", online: true, tags: ["Carry型", "稳定", "负责"], bio: "擅长刺客中单,带妹上分专业户" },
|
||||
]
|
||||
|
||||
function CpCard({
|
||||
cp,
|
||||
onOpen,
|
||||
onContact,
|
||||
}: {
|
||||
cp: (typeof CP_LIST)[number]
|
||||
onOpen: () => void
|
||||
onContact: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="glass-card p-4 rounded-xl hover:bg-white/5 transition-colors">
|
||||
<div className="flex items-start gap-3 mb-3 cursor-pointer" onClick={onOpen} onKeyDown={(e) => e.key === "Enter" && onOpen()} role="button" tabIndex={0}>
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 rounded-lg bg-muted relative overflow-hidden">
|
||||
<FillImage src={cp.avatar || "/placeholder.svg"} alt={cp.name} className="h-16 w-16" />
|
||||
</div>
|
||||
{cp.online && <div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-green-500 border-2 border-background" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-bold truncate">{cp.name}</h4>
|
||||
<span className={`px-1.5 py-0.5 rounded text-[10px] ${cp.gender === "女" ? "bg-pink-500/20 text-pink-400" : "bg-blue-500/20 text-blue-400"}`}>
|
||||
{cp.gender} · {cp.age}岁
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-white/50 mb-2">
|
||||
<span className="flex items-center gap-1">
|
||||
<Star size={12} className="text-primary" />
|
||||
{cp.rank}
|
||||
</span>
|
||||
<span>{cp.voice}</span>
|
||||
</div>
|
||||
<p className="text-xs text-white/60 mb-2 line-clamp-1">{cp.bio}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{cp.tags.map((tag) => (
|
||||
<span key={tag} className="px-2 py-0.5 rounded-full bg-white/5 text-[10px] text-white/60">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<Button size="sm" variant="outline" className="flex-1 h-8 text-xs border-white/10 hover:bg-white/5 bg-transparent" type="button" onClick={(e) => (e.stopPropagation(), onContact())}>
|
||||
<MessageCircle size={14} className="mr-1" />
|
||||
打个招呼
|
||||
</Button>
|
||||
<Button size="sm" className="flex-1 h-8 text-xs bg-pink-500 hover:bg-pink-600" type="button" onClick={(e) => (e.stopPropagation(), onOpen())}>
|
||||
<Heart size={14} className="mr-1" />
|
||||
查看详情
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CpListPage() {
|
||||
const navigate = useNavigate()
|
||||
const handleContact = (cpName: string) => {
|
||||
toast({ title: "发送成功", description: `已向${cpName}发送交友请求` })
|
||||
}
|
||||
|
||||
const filterTab = (tab: "all" | "girl" | "boy") => {
|
||||
if (tab === "girl") return CP_LIST.filter((c) => c.gender === "女")
|
||||
if (tab === "boy") return CP_LIST.filter((c) => c.gender === "男")
|
||||
return CP_LIST
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="p-2 -ml-2 hover:bg-white/5 rounded-lg transition-colors" type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">电竞CP</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="glass-card p-4 rounded-xl border-l-4 border-pink-500 bg-gradient-to-r from-pink-500/10 to-transparent">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-bold text-sm flex items-center gap-2">
|
||||
<Heart className="text-pink-500" size={18} />
|
||||
找个游戏CP
|
||||
</h3>
|
||||
<Sparkles className="text-pink-500 animate-pulse" size={18} />
|
||||
</div>
|
||||
<p className="text-xs text-white/60 mb-3">一起开黑,一起上分,遇见你的电竞知己</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="all" className="w-full">
|
||||
<TabsList className="w-full bg-white/5 p-1 rounded-xl">
|
||||
<TabsTrigger value="all" className="flex-1 text-xs">
|
||||
全部
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="girl" className="flex-1 text-xs">
|
||||
女生
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="boy" className="flex-1 text-xs">
|
||||
男生
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
{(["all", "girl", "boy"] as const).map((tab) => (
|
||||
<TabsContent key={tab} value={tab} className="mt-4 space-y-3">
|
||||
{filterTab(tab).map((cp) => (
|
||||
<CpCard key={cp.id} cp={cp} onOpen={() => void navigate(`/cp/${cp.id}`)} onContact={() => handleContact(cp.name)} />
|
||||
))}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
156
new/wz-app/src/pages/guild/GuildDetailPage.tsx
Normal file
156
new/wz-app/src/pages/guild/GuildDetailPage.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { useState } from "react"
|
||||
import { useParams, useNavigate } from "react-router-dom"
|
||||
import { ArrowLeft, Users, Trophy, Star, Shield, CheckCircle2, MessageCircle } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const guildData: Record<
|
||||
string,
|
||||
{
|
||||
name: string
|
||||
logo: string
|
||||
cover: string
|
||||
level: number
|
||||
members: number
|
||||
rank: number
|
||||
rating: number
|
||||
description: string
|
||||
benefits: { title: string; desc: string; color: string }[]
|
||||
requirements: { label: string; text: string }[]
|
||||
}
|
||||
> = {
|
||||
"1": {
|
||||
name: "东东电竞",
|
||||
logo: "/esports-team-logo.jpg",
|
||||
cover: "/esports-tournament-stadium.jpg",
|
||||
level: 10,
|
||||
members: 1205,
|
||||
rank: 1,
|
||||
rating: 4.9,
|
||||
description: "国服顶尖路人王聚集地,专注高端局陪练与代练业务。",
|
||||
benefits: [
|
||||
{ title: "专属培训体系", desc: "职业教练一对一指导", color: "border-primary" },
|
||||
{ title: "优先派单特权", desc: "优质高价单优先", color: "border-secondary" },
|
||||
],
|
||||
requirements: [
|
||||
{ label: "游戏段位", text: "荣耀王者50星 / LOL大师以上" },
|
||||
{ label: "在线时间", text: "每天稳定在线不低于4小时" },
|
||||
],
|
||||
},
|
||||
"2": {
|
||||
name: "星耀公会",
|
||||
logo: "/esports-team-logo.jpg",
|
||||
cover: "/esports-tournament-stadium.jpg",
|
||||
level: 8,
|
||||
members: 856,
|
||||
rank: 3,
|
||||
rating: 4.7,
|
||||
description: "专注王者荣耀高端陪练,新人友好。",
|
||||
benefits: [
|
||||
{ title: "新人培训", desc: "系统培训快速上手", color: "border-primary" },
|
||||
{ title: "稳定派单", desc: "收入有保障", color: "border-secondary" },
|
||||
],
|
||||
requirements: [
|
||||
{ label: "游戏段位", text: "王者荣耀荣耀王者以上" },
|
||||
{ label: "在线时间", text: "每天在线不低于3小时" },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export function GuildDetailPage() {
|
||||
const { id = "1" } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [isApplied, setIsApplied] = useState(false)
|
||||
const guild = guildData[id] ?? guildData["1"]!
|
||||
|
||||
const handleApply = () => {
|
||||
setIsApplied(true)
|
||||
toast({ title: "申请已提交", description: "公会管理员将在24小时内审核您的申请" })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-24 relative">
|
||||
<div className="h-48 relative">
|
||||
<FillImage src={guild.cover || "/placeholder.svg"} alt="Cover" className="h-48" />
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-background" />
|
||||
<button onClick={() => void navigate(-1)} className="absolute top-4 left-4 p-2 rounded-full bg-black/20 backdrop-blur-md text-white z-10" type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-4 -mt-12 relative z-10">
|
||||
<div className="flex items-end justify-between mb-4">
|
||||
<div className="w-24 h-24 rounded-2xl border-4 border-background bg-muted relative overflow-hidden shadow-xl">
|
||||
<FillImage src={guild.logo || "/placeholder.svg"} alt="Logo" className="h-24 w-24 rounded-2xl" />
|
||||
</div>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Button size="sm" variant="outline" className="h-8 text-xs gap-1 bg-transparent" type="button">
|
||||
<MessageCircle size={14} />
|
||||
咨询
|
||||
</Button>
|
||||
<Button size="sm" className={`h-8 text-xs gap-1 ${isApplied ? "bg-white/20 text-white/60" : "bg-primary"}`} type="button" onClick={handleApply} disabled={isApplied}>
|
||||
<CheckCircle2 size={14} />
|
||||
{isApplied ? "已申请" : "申请加入"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h1 className="text-2xl font-bold">{guild.name}</h1>
|
||||
<span className="px-2 py-0.5 rounded bg-yellow-500/20 text-yellow-400 text-[10px] border border-yellow-500/30 font-bold">LV.{guild.level} 顶级公会</span>
|
||||
</div>
|
||||
<p className="text-sm text-white/60 mb-3">{guild.description}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-white/40">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={12} />
|
||||
{guild.members.toLocaleString()} 成员
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Trophy size={12} />
|
||||
全区第 {guild.rank} 名
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Star size={12} />
|
||||
{guild.rating} 评分
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-bold text-base flex items-center gap-2">
|
||||
<Shield size={16} className="text-primary" />
|
||||
公会权益
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{guild.benefits.map((benefit) => (
|
||||
<div key={benefit.title} className={`glass-card p-3 rounded-xl border-l-2 ${benefit.color}`}>
|
||||
<div className="text-sm font-bold mb-1">{benefit.title}</div>
|
||||
<div className="text-xs text-white/50">{benefit.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mt-6">
|
||||
<h3 className="font-bold text-base flex items-center gap-2">
|
||||
<CheckCircle2 size={16} className="text-primary" />
|
||||
入驻要求
|
||||
</h3>
|
||||
<div className="glass-card p-4 rounded-xl space-y-3">
|
||||
{guild.requirements.map((req) => (
|
||||
<div key={req.label} className="flex items-start gap-3">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary mt-1.5" />
|
||||
<div className="flex-1 text-xs text-white/70">
|
||||
<span className="text-white font-bold">{req.label}:</span>
|
||||
{req.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
137
new/wz-app/src/pages/guild/GuildListPage.tsx
Normal file
137
new/wz-app/src/pages/guild/GuildListPage.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { ArrowLeft, Users, Trophy, Star, Crown, TrendingUp, MessageCircle } from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const GUILDS = [
|
||||
{ id: "1", name: "东东电竞", level: 10, members: 1205, income: "月入50万+", tags: ["全能", "陪玩", "培训"], rank: 1 },
|
||||
{ id: "2", name: "机枪电竞", level: 8, members: 680, income: "月入30万+", tags: ["LOL", "云顶", "代练"], rank: 2 },
|
||||
{ id: "3", name: "洲越电竞", level: 6, members: 320, income: "月入15万+", tags: ["三角洲", "无畏契约"], rank: 3 },
|
||||
{ id: "4", name: "星辰公会", level: 7, members: 450, income: "月入20万+", tags: ["王者", "吃鸡", "陪玩"], rank: 4 },
|
||||
]
|
||||
|
||||
export function GuildListPage() {
|
||||
const navigate = useNavigate()
|
||||
const handleJoinGuild = (guildName: string) => {
|
||||
toast({ title: "申请已发送", description: `您的加入申请已发送至${guildName},请等待审核` })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="p-2 -ml-2 hover:bg-white/5 rounded-lg transition-colors" type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">公会中心</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="glass-card p-4 rounded-xl border-l-4 border-primary">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-bold text-sm">我的公会</h3>
|
||||
<span className="text-xs text-white/50">暂未加入</span>
|
||||
</div>
|
||||
<p className="text-xs text-white/60 mb-3">加入公会,享受更多福利和资源支持</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="hot" className="w-full">
|
||||
<TabsList className="w-full bg-white/5 p-1 rounded-xl">
|
||||
<TabsTrigger value="hot" className="flex-1 text-xs">
|
||||
热门公会
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="new" className="flex-1 text-xs">
|
||||
新晋公会
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="top" className="flex-1 text-xs">
|
||||
排行榜
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="hot" className="mt-4 space-y-3">
|
||||
{GUILDS.map((guild, i) => (
|
||||
<div key={guild.id} className="glass-card p-4 rounded-xl hover:bg-white/5 transition-colors">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 rounded-lg bg-muted relative overflow-hidden">
|
||||
<FillImage src={`/guild-logo-${(i % 3) + 1}.jpg`} alt={guild.name} className="h-16 w-16" />
|
||||
</div>
|
||||
{guild.rank <= 3 && (
|
||||
<div className="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-gradient-to-br from-yellow-400 to-orange-500 flex items-center justify-center text-[10px] font-bold">
|
||||
{guild.rank}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-bold truncate">{guild.name}</h4>
|
||||
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-primary/20 text-primary text-[10px]">
|
||||
<Crown size={10} />
|
||||
Lv.{guild.level}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-white/50 mb-2">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={12} />
|
||||
{guild.members}人
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<TrendingUp size={12} />
|
||||
{guild.income}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{guild.tags.map((tag) => (
|
||||
<span key={tag} className="px-2 py-0.5 rounded-full bg-white/5 text-[10px] text-white/60">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex-1 h-8 text-xs border-white/10 hover:bg-white/5 bg-transparent"
|
||||
type="button"
|
||||
onClick={() => void navigate(`/guild/${guild.id}`)}
|
||||
>
|
||||
<MessageCircle size={14} className="mr-1" />
|
||||
了解详情
|
||||
</Button>
|
||||
<Button size="sm" className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90" type="button" onClick={() => handleJoinGuild(guild.name)}>
|
||||
<Star size={14} className="mr-1" />
|
||||
申请加入
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</TabsContent>
|
||||
<TabsContent value="new" className="mt-4">
|
||||
<div className="text-center py-12 text-white/40 text-sm">新晋公会列表开发中...</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="top" className="mt-4">
|
||||
<div className="text-center py-12 text-white/40 text-sm">公会排行榜开发中...</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="glass-card p-4 rounded-xl bg-gradient-to-r from-primary/10 to-purple-500/10 border border-primary/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
|
||||
<Trophy className="text-primary" size={24} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-bold text-sm mb-1">创建自己的公会</h3>
|
||||
<p className="text-xs text-white/60">招募成员,共创辉煌</p>
|
||||
</div>
|
||||
<Button size="sm" className="bg-primary hover:bg-primary/90 h-8 text-xs" type="button" onClick={() => toast({ title: "即将开放", description: "公会创建功能正在开发中" })}>
|
||||
立即创建
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
254
new/wz-app/src/pages/mall/MallCompanionPage.tsx
Normal file
254
new/wz-app/src/pages/mall/MallCompanionPage.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import { useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Star, Play, Mic, Video, Gamepad2, GraduationCap } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const games = [
|
||||
{ name: "全部", id: "all" },
|
||||
{ name: "英雄联盟", id: "lol" },
|
||||
{ name: "王者荣耀", id: "hok" },
|
||||
{ name: "无畏契约", id: "val" },
|
||||
{ name: "和平精英", id: "pubg" },
|
||||
{ name: "原神", id: "genshin" },
|
||||
]
|
||||
|
||||
const companions = [
|
||||
{
|
||||
id: 1,
|
||||
name: "GM-远洋",
|
||||
title: "国服第一盲僧",
|
||||
price: 50,
|
||||
unit: "局",
|
||||
image: "/avatar-1.jpg",
|
||||
tags: ["技术流", "风趣", "职业退役"],
|
||||
game: "lol",
|
||||
rating: 5.0,
|
||||
orders: 1205,
|
||||
voice: true,
|
||||
video: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "七七",
|
||||
title: "甜美声优",
|
||||
price: 40,
|
||||
unit: "小时",
|
||||
image: "/avatar-5.jpg",
|
||||
tags: ["声优", "聊天", "辅助"],
|
||||
game: "hok",
|
||||
rating: 4.9,
|
||||
orders: 890,
|
||||
voice: true,
|
||||
video: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "卡若",
|
||||
title: "前职业选手",
|
||||
price: 80,
|
||||
unit: "局",
|
||||
image: "/avatar-2.jpg",
|
||||
tags: ["包赢", "教学", "高分局"],
|
||||
game: "lol",
|
||||
rating: 5.0,
|
||||
orders: 3400,
|
||||
voice: true,
|
||||
video: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "小柠檬",
|
||||
title: "人皮话多",
|
||||
price: 30,
|
||||
unit: "局",
|
||||
image: "/avatar-3.jpg",
|
||||
tags: ["娱乐", "开心", "相声"],
|
||||
game: "val",
|
||||
rating: 4.8,
|
||||
orders: 560,
|
||||
voice: true,
|
||||
video: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "大魔王",
|
||||
title: "中单法王",
|
||||
price: 100,
|
||||
unit: "局",
|
||||
image: "/avatar-6.jpg",
|
||||
tags: ["技术教学", "上分", "严肃"],
|
||||
game: "lol",
|
||||
rating: 5.0,
|
||||
orders: 8800,
|
||||
voice: true,
|
||||
video: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "阿伟",
|
||||
title: "绝地刚枪王",
|
||||
price: 60,
|
||||
unit: "小时",
|
||||
image: "/avatar-4.jpg",
|
||||
tags: ["带妹", "吃鸡", "指挥"],
|
||||
game: "pubg",
|
||||
rating: 4.9,
|
||||
orders: 1100,
|
||||
voice: true,
|
||||
video: false,
|
||||
},
|
||||
]
|
||||
|
||||
const courseList = [
|
||||
{ id: 1, title: "中单瑞兹顶级教学", author: "Faker", price: 199, image: "/course-1.jpg", students: 12000, game: "lol" },
|
||||
{ id: 2, title: "下路对线细节全解", author: "Uzi", price: 199, image: "/course-2.jpg", students: 35000, game: "lol" },
|
||||
{ id: 3, title: "上单压制力养成", author: "TheShy", price: 159, image: "/course-3.jpg", students: 8900, game: "lol" },
|
||||
{ id: 4, title: "瓦罗兰特特工教学", author: "康康", price: 99, image: "/course-4.jpg", students: 4500, game: "val" },
|
||||
]
|
||||
|
||||
export function MallCompanionPage() {
|
||||
const navigate = useNavigate()
|
||||
const [activeTab, setActiveTab] = useState<"play" | "learn">("play")
|
||||
const [activeGame, setActiveGame] = useState("all")
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="text-white/70 hover:text-white">
|
||||
返回
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">明星陪玩 & 课程</h1>
|
||||
</header>
|
||||
|
||||
<div className="px-4 mt-2">
|
||||
<div className="bg-white/5 p-1 rounded-xl flex">
|
||||
<button
|
||||
type="button"
|
||||
className={`flex-1 py-2 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all ${activeTab === "play" ? "bg-primary text-black shadow-lg shadow-primary/20" : "text-white/50 hover:text-white"}`}
|
||||
onClick={() => setActiveTab("play")}
|
||||
>
|
||||
<Gamepad2 size={16} />
|
||||
明星陪玩
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex-1 py-2 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all ${activeTab === "learn" ? "bg-secondary text-white shadow-lg shadow-secondary/20" : "text-white/50 hover:text-white"}`}
|
||||
onClick={() => setActiveTab("learn")}
|
||||
>
|
||||
<GraduationCap size={16} />
|
||||
大师课程
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 mt-4 overflow-x-auto no-scrollbar pb-2">
|
||||
<div className="flex gap-2">
|
||||
{games.map((g) => (
|
||||
<button
|
||||
key={g.id}
|
||||
type="button"
|
||||
onClick={() => setActiveGame(g.id)}
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-bold whitespace-nowrap transition-colors ${activeGame === g.id ? "bg-white text-black" : "bg-white/5 text-white/50 hover:bg-white/10"}`}
|
||||
>
|
||||
{g.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 mt-2 space-y-4">
|
||||
{activeTab === "play" ? (
|
||||
<div className="grid gap-3">
|
||||
{companions
|
||||
.filter((c) => activeGame === "all" || c.game === activeGame)
|
||||
.map((c) => (
|
||||
<div
|
||||
key={c.id}
|
||||
className="glass-card p-3 rounded-xl flex gap-3 cursor-pointer hover:bg-white/5 transition-colors group"
|
||||
onClick={() => void navigate(`/live/${c.id}`)}
|
||||
onKeyDown={(e) => e.key === "Enter" && void navigate(`/live/${c.id}`)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="relative w-24 h-24 rounded-lg overflow-hidden shrink-0">
|
||||
<FillImage src={c.image || "/placeholder.svg"} alt={c.name} className="h-24 w-24" />
|
||||
<div className="absolute top-1 left-1 bg-black/60 backdrop-blur-sm px-1.5 py-0.5 rounded flex items-center gap-1 text-[10px] text-white font-bold z-10">
|
||||
<Star size={8} className="text-yellow-500 fill-yellow-500" />
|
||||
{c.rating}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between py-0.5">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold text-sm flex items-center gap-1">
|
||||
{c.name}
|
||||
{c.voice && <Mic size={10} className="text-green-400" />}
|
||||
{c.video && <Video size={10} className="text-blue-400" />}
|
||||
</h3>
|
||||
<span className="text-[10px] text-white/40">{c.orders}单</span>
|
||||
</div>
|
||||
<p className="text-xs text-primary font-medium mt-0.5">{c.title}</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1.5">
|
||||
{c.tags.map((tag, i) => (
|
||||
<span key={i} className="text-[10px] bg-white/5 px-1.5 py-0.5 rounded text-white/50">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="flex items-end gap-1">
|
||||
<span className="text-lg font-bold text-secondary">{c.price}</span>
|
||||
<span className="text-xs text-white/50 mb-1">币/{c.unit}</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs px-3 font-bold bg-white/10 hover:bg-primary hover:text-black"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
立即下单
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{courseList
|
||||
.filter((c) => activeGame === "all" || c.game === activeGame)
|
||||
.map((c) => (
|
||||
<div
|
||||
key={c.id}
|
||||
className="glass-card p-0 rounded-xl overflow-hidden cursor-pointer group"
|
||||
onClick={() => toast({ title: "购买成功", description: "课程已添加至学习中心" })}
|
||||
onKeyDown={(e) => e.key === "Enter" && e.currentTarget.click()}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="relative aspect-video">
|
||||
<FillImage src={c.image || "/placeholder.svg"} alt={c.title} className="h-full" />
|
||||
<div className="absolute inset-0 bg-black/30 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Play size={40} className="text-white fill-white" />
|
||||
</div>
|
||||
<div className="absolute bottom-2 left-2 px-2 py-1 bg-black/60 backdrop-blur-sm rounded text-[10px] font-bold">
|
||||
{c.author} 亲授
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<h3 className="font-bold text-sm mb-1">{c.title}</h3>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-white/50">{c.students} 人已学</span>
|
||||
<span className="text-primary font-bold">¥{c.price}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
61
new/wz-app/src/pages/mall/MallCoursesPage.tsx
Normal file
61
new/wz-app/src/pages/mall/MallCoursesPage.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Play, Users } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const courses = [
|
||||
{ id: 1, title: "Faker 中单瑞兹教学", author: "Faker", price: 199, image: "/course-1.jpg", students: 12000 },
|
||||
{ id: 2, title: "Uzi 下路对线细节", author: "Uzi", price: 199, image: "/course-2.jpg", students: 35000 },
|
||||
{ id: 3, title: "TheShy 上单压制力", author: "TheShy", price: 159, image: "/course-3.jpg", students: 8900 },
|
||||
{ id: 4, title: "10分钟学会云顶吃鸡", author: "云顶之弈", price: 49, image: "/course-4.jpg", students: 5000 },
|
||||
]
|
||||
|
||||
export function MallCoursesPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="text-white/70" type="button">
|
||||
返回
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">同款课程</h1>
|
||||
</header>
|
||||
<div className="p-4 space-y-4">
|
||||
{courses.map((c) => (
|
||||
<div
|
||||
key={c.id}
|
||||
className="glass-card p-3 rounded-xl flex gap-3 cursor-pointer hover:bg-white/10"
|
||||
onClick={() => toast({ title: "购买成功", description: "课程已添加至学习中心" })}
|
||||
onKeyDown={(e) => e.key === "Enter" && e.currentTarget.click()}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="relative w-32 aspect-video rounded-lg overflow-hidden shrink-0">
|
||||
<FillImage src={c.image || "/placeholder.svg"} alt={c.title} className="h-full" />
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
||||
<Play size={24} className="text-white/80" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
<h3 className="font-bold text-sm">{c.title}</h3>
|
||||
<div className="flex items-center gap-2 text-xs text-white/50">
|
||||
<span className="bg-white/10 px-1.5 py-0.5 rounded text-white/70">{c.author}</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={10} /> {c.students}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<span className="text-primary font-bold">¥{c.price}</span>
|
||||
<Button size="sm" className="h-6 text-[10px]" type="button" onClick={(e) => e.stopPropagation()}>
|
||||
购买
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
84
new/wz-app/src/pages/mall/MallGearPage.tsx
Normal file
84
new/wz-app/src/pages/mall/MallGearPage.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { ArrowLeft, ShoppingCart } from "lucide-react"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const products = [
|
||||
{ id: 1, name: "罗技 G Pro X Superlight", price: 899, image: "/gear-mouse.jpg", tag: "职业首选", category: "鼠标" },
|
||||
{ id: 2, name: "雷蛇 猎魂光蛛 V3", price: 1299, image: "/gear-keyboard.jpg", tag: "光轴科技", category: "键盘" },
|
||||
{ id: 3, name: "卓威 XL2546K", price: 3499, image: "/gear-monitor.jpg", tag: "360Hz", category: "显示器" },
|
||||
{ id: 4, name: "HyperX Cloud II", price: 599, image: "/gear-headset.jpg", tag: "7.1声道", category: "耳机" },
|
||||
{ id: 5, name: "赛睿 QcK Heavy", price: 199, image: "/gear-mousepad.jpg", tag: "加厚版", category: "鼠标垫" },
|
||||
{ id: 6, name: "电竞座椅 GT900", price: 1999, image: "/gear-chair.jpg", tag: "人体工学", category: "座椅" },
|
||||
]
|
||||
|
||||
export function MallGearPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="text-white/70 hover:text-white" type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">电竞外设</h1>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="rounded-full bg-white/5" type="button">
|
||||
<ShoppingCart size={18} />
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div className="px-4 py-3 flex gap-2 overflow-x-auto no-scrollbar">
|
||||
{["全部", "鼠标", "键盘", "耳机", "显示器", "鼠标垫", "座椅"].map((cat, i) => (
|
||||
<button
|
||||
key={cat}
|
||||
type="button"
|
||||
className={`whitespace-nowrap text-xs font-bold px-4 py-2 rounded-full transition-colors ${
|
||||
i === 0 ? "bg-primary text-black" : "bg-white/5 text-white/60 hover:bg-white/10"
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 grid grid-cols-2 gap-4">
|
||||
{products.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="glass-card rounded-xl overflow-hidden group cursor-pointer border border-white/5 hover:border-primary/30 transition-colors"
|
||||
>
|
||||
<div className="relative aspect-square bg-zinc-800 overflow-hidden">
|
||||
<FillImage src={p.image || "/placeholder.svg"} alt={p.name} className="h-full group-hover:scale-105 transition-transform" />
|
||||
<div className="absolute top-2 left-2 bg-red-500 text-white text-[10px] px-2 py-0.5 rounded font-bold z-10">
|
||||
{p.tag}
|
||||
</div>
|
||||
<div className="absolute top-2 right-2 bg-black/50 text-white/80 text-[10px] px-2 py-0.5 rounded z-10">
|
||||
{p.category}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<h3 className="font-bold text-sm text-white truncate mb-2">{p.name}</h3>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-primary font-bold">¥{p.price}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs px-3"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toast({ title: "已加入购物车", description: p.name })
|
||||
}}
|
||||
>
|
||||
购买
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
302
new/wz-app/src/pages/mall/MallPointsGamePage.tsx
Normal file
302
new/wz-app/src/pages/mall/MallPointsGamePage.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
import { useState } from "react"
|
||||
import { useParams, useNavigate } from "react-router-dom"
|
||||
import { ArrowLeft, Check, ShoppingCart, Zap, Flame } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useAppContext } from "@/components/providers/app-provider"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const gameData: Record<
|
||||
string,
|
||||
{
|
||||
name: string
|
||||
image: string
|
||||
description: string
|
||||
discount: string
|
||||
tag?: string
|
||||
options: { name: string; original: number; price: number; popular?: boolean }[]
|
||||
}
|
||||
> = {
|
||||
wow: {
|
||||
name: "魔兽世界",
|
||||
image: "/point-card-wow.jpg",
|
||||
description: "月卡/点卡充值",
|
||||
discount: "9折",
|
||||
tag: "经典",
|
||||
options: [
|
||||
{ name: "30天月卡", original: 75, price: 68, popular: true },
|
||||
{ name: "90天季卡", original: 198, price: 178 },
|
||||
{ name: "180天半年卡", original: 360, price: 320 },
|
||||
{ name: "2000点卡", original: 100, price: 90 },
|
||||
{ name: "5000点卡", original: 250, price: 225 },
|
||||
],
|
||||
},
|
||||
diablo: {
|
||||
name: "暗黑破坏神4",
|
||||
image: "/point-card-diablo.jpg",
|
||||
description: "白金币充值",
|
||||
discount: "85折",
|
||||
tag: "热卖",
|
||||
options: [
|
||||
{ name: "500白金币", original: 45, price: 38, popular: true },
|
||||
{ name: "1000白金币", original: 88, price: 75 },
|
||||
{ name: "2800白金币", original: 198, price: 168 },
|
||||
{ name: "5700白金币", original: 388, price: 330 },
|
||||
{ name: "11500白金币", original: 648, price: 550 },
|
||||
],
|
||||
},
|
||||
hearthstone: {
|
||||
name: "炉石传说",
|
||||
image: "/point-card-hearthstone.jpg",
|
||||
description: "战令/卡包充值",
|
||||
discount: "88折",
|
||||
tag: "新品",
|
||||
options: [
|
||||
{ name: "2卡包", original: 18, price: 16 },
|
||||
{ name: "7卡包", original: 58, price: 51, popular: true },
|
||||
{ name: "15卡包", original: 108, price: 95 },
|
||||
{ name: "40卡包", original: 258, price: 227 },
|
||||
{ name: "酒馆战令", original: 128, price: 113 },
|
||||
],
|
||||
},
|
||||
overwatch: {
|
||||
name: "守望先锋2",
|
||||
image: "/point-card-overwatch.jpg",
|
||||
description: "代币充值",
|
||||
discount: "9折",
|
||||
options: [
|
||||
{ name: "500代币", original: 35, price: 32 },
|
||||
{ name: "1000代币", original: 68, price: 61, popular: true },
|
||||
{ name: "2200代币", original: 128, price: 115 },
|
||||
{ name: "5700代币", original: 298, price: 268 },
|
||||
{ name: "11600代币", original: 518, price: 466 },
|
||||
],
|
||||
},
|
||||
lol: {
|
||||
name: "英雄联盟",
|
||||
image: "/point-card-lol.jpg",
|
||||
description: "点券充值",
|
||||
discount: "95折",
|
||||
tag: "热门",
|
||||
options: [
|
||||
{ name: "100点券", original: 10, price: 10 },
|
||||
{ name: "350点券", original: 35, price: 33, popular: true },
|
||||
{ name: "650点券", original: 65, price: 62 },
|
||||
{ name: "1380点券", original: 138, price: 131 },
|
||||
{ name: "3380点券", original: 338, price: 321 },
|
||||
],
|
||||
},
|
||||
hok: {
|
||||
name: "王者荣耀",
|
||||
image: "/point-card-hok.jpg",
|
||||
description: "点券充值",
|
||||
discount: "95折",
|
||||
tag: "热门",
|
||||
options: [
|
||||
{ name: "60点券", original: 6, price: 6 },
|
||||
{ name: "300点券", original: 30, price: 29, popular: true },
|
||||
{ name: "680点券", original: 68, price: 65 },
|
||||
{ name: "1280点券", original: 128, price: 122 },
|
||||
{ name: "2480点券", original: 248, price: 236 },
|
||||
],
|
||||
},
|
||||
pubg: {
|
||||
name: "和平精英",
|
||||
image: "/point-card-pubg.jpg",
|
||||
description: "点券充值",
|
||||
discount: "92折",
|
||||
tag: "热门",
|
||||
options: [
|
||||
{ name: "60点券", original: 6, price: 6 },
|
||||
{ name: "300点券", original: 30, price: 28, popular: true },
|
||||
{ name: "600点券", original: 60, price: 55 },
|
||||
{ name: "1500点券", original: 150, price: 138 },
|
||||
{ name: "3000点券", original: 300, price: 276 },
|
||||
],
|
||||
},
|
||||
genshin: {
|
||||
name: "原神",
|
||||
image: "/point-card-genshin.jpg",
|
||||
description: "创世结晶充值",
|
||||
discount: "9折",
|
||||
tag: "热卖",
|
||||
options: [
|
||||
{ name: "60创世结晶", original: 6, price: 6 },
|
||||
{ name: "300创世结晶", original: 30, price: 27, popular: true },
|
||||
{ name: "980创世结晶", original: 98, price: 88 },
|
||||
{ name: "1980创世结晶", original: 198, price: 178 },
|
||||
{ name: "3280创世结晶", original: 328, price: 295 },
|
||||
],
|
||||
},
|
||||
valorant: {
|
||||
name: "无畏契约",
|
||||
image: "/point-card-valorant.jpg",
|
||||
description: "VP点数充值",
|
||||
discount: "88折",
|
||||
tag: "新品",
|
||||
options: [
|
||||
{ name: "475VP", original: 35, price: 31 },
|
||||
{ name: "1000VP", original: 68, price: 60, popular: true },
|
||||
{ name: "2050VP", original: 128, price: 113 },
|
||||
{ name: "3650VP", original: 198, price: 174 },
|
||||
{ name: "5350VP", original: 288, price: 253 },
|
||||
],
|
||||
},
|
||||
fortnite: {
|
||||
name: "堡垒之夜",
|
||||
image: "/point-card-fortnite.jpg",
|
||||
description: "V-Bucks充值",
|
||||
discount: "85折",
|
||||
options: [
|
||||
{ name: "1000V-Bucks", original: 68, price: 58 },
|
||||
{ name: "2800V-Bucks", original: 168, price: 143, popular: true },
|
||||
{ name: "5000V-Bucks", original: 288, price: 245 },
|
||||
{ name: "13500V-Bucks", original: 648, price: 551 },
|
||||
],
|
||||
},
|
||||
apex: {
|
||||
name: "Apex英雄",
|
||||
image: "/point-card-apex.jpg",
|
||||
description: "Apex硬币充值",
|
||||
discount: "9折",
|
||||
options: [
|
||||
{ name: "1000硬币", original: 68, price: 61 },
|
||||
{ name: "2150硬币", original: 128, price: 115, popular: true },
|
||||
{ name: "4350硬币", original: 248, price: 223 },
|
||||
{ name: "6700硬币", original: 388, price: 349 },
|
||||
{ name: "11500硬币", original: 648, price: 583 },
|
||||
],
|
||||
},
|
||||
cs2: {
|
||||
name: "CS2",
|
||||
image: "/point-card-cs2.jpg",
|
||||
description: "Steam钱包充值",
|
||||
discount: "95折",
|
||||
options: [
|
||||
{ name: "¥50钱包", original: 50, price: 48 },
|
||||
{ name: "¥100钱包", original: 100, price: 95, popular: true },
|
||||
{ name: "¥200钱包", original: 200, price: 190 },
|
||||
{ name: "¥500钱包", original: 500, price: 475 },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export function MallPointsGamePage() {
|
||||
const { game: gameId = "" } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { pay } = useAppContext()
|
||||
const [selectedOption, setSelectedOption] = useState<number | null>(null)
|
||||
|
||||
const game = gameData[gameId]
|
||||
|
||||
if (!game) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-4">
|
||||
<p className="text-white/60 mb-4">游戏不存在</p>
|
||||
<Button type="button" onClick={() => void navigate(-1)}>
|
||||
返回
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handlePurchase = () => {
|
||||
if (selectedOption === null) {
|
||||
toast({ title: "请选择充值金额" })
|
||||
return
|
||||
}
|
||||
const option = game.options[selectedOption]!
|
||||
const ok = pay(option.price, `${game.name} ${option.name}`, "product", game.image)
|
||||
if (ok) void navigate("/mall")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-36 bg-background">
|
||||
<div className="relative h-56">
|
||||
<FillImage src={game.image || "/placeholder.svg"} alt={game.name} className="h-56" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/60 to-transparent" />
|
||||
<div className="absolute top-0 left-0 right-0 p-4 flex items-center justify-between z-10">
|
||||
<button type="button" onClick={() => void navigate(-1)} className="p-2 rounded-full bg-black/40 backdrop-blur-md">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
{game.tag && <div className="bg-secondary/90 text-white text-xs font-bold px-3 py-1 rounded-full">{game.tag}</div>}
|
||||
</div>
|
||||
<div className="absolute bottom-4 left-4 right-4 z-10">
|
||||
<div className="bg-yellow-500/90 text-black text-sm font-bold px-3 py-1 rounded-full mb-2 inline-block">
|
||||
<Flame size={14} className="inline mr-1" />
|
||||
{game.discount} 限时优惠
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">{game.name}</h1>
|
||||
<p className="text-sm text-white/60">{game.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 mt-6 space-y-3">
|
||||
<h3 className="font-bold text-sm flex items-center gap-2">
|
||||
<ShoppingCart size={16} className="text-primary" />
|
||||
选择充值金额
|
||||
</h3>
|
||||
|
||||
{game.options.map((option, i) => (
|
||||
<div
|
||||
key={i}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedOption(i)}
|
||||
onKeyDown={(e) => e.key === "Enter" && setSelectedOption(i)}
|
||||
className={`bg-[#1A1A22] rounded-xl p-4 flex items-center justify-between border-2 transition-all cursor-pointer ${
|
||||
selectedOption === i ? "border-primary bg-primary/5" : "border-transparent hover:border-white/10"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center ${
|
||||
selectedOption === i ? "border-primary bg-primary" : "border-white/30"
|
||||
}`}
|
||||
>
|
||||
{selectedOption === i && <Check size={14} className="text-black" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-bold">{option.name}</p>
|
||||
{option.popular && (
|
||||
<span className="bg-secondary/20 text-secondary text-[8px] px-1.5 py-0.5 rounded font-bold">推荐</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-xs text-white/40 line-through">¥{option.original}</span>
|
||||
<span className="text-[10px] text-green-400">省¥{option.original - option.price}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-primary">¥{option.price}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-16 left-0 right-0 px-4 py-3 bg-background/95 backdrop-blur-md border-t border-white/5 z-30">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
{selectedOption !== null ? (
|
||||
<>
|
||||
<p className="text-xs text-white/50 truncate">已选: {game.options[selectedOption]!.name}</p>
|
||||
<p className="text-xl font-bold text-primary">¥{game.options[selectedOption]!.price}</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-white/50">请选择充值金额</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
className="shrink-0 px-6 h-11 text-base font-bold"
|
||||
type="button"
|
||||
onClick={handlePurchase}
|
||||
disabled={selectedOption === null}
|
||||
>
|
||||
<Zap size={18} className="mr-2" />
|
||||
立即购买
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
310
new/wz-app/src/pages/services/AccountServicesPage.tsx
Normal file
310
new/wz-app/src/pages/services/AccountServicesPage.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import { Suspense, useState } from "react"
|
||||
import { Search, Filter, Shield, Wallet, Gamepad2, FileText, Clock } from "lucide-react"
|
||||
import { useNavigate, useSearchParams } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FillImage } from "@/components/media/FillImage"
|
||||
|
||||
const games = [
|
||||
{ name: "全部", icon: null as string | null },
|
||||
{ name: "英雄联盟", icon: "/account-icon-lol.jpg" },
|
||||
{ name: "无畏契约", icon: "/account-icon-val.jpg" },
|
||||
{ name: "王者荣耀", icon: "/account-icon-hok.jpg" },
|
||||
{ name: "原神", icon: "/account-icon-gen.jpg" },
|
||||
]
|
||||
|
||||
const accounts = [
|
||||
{
|
||||
id: 1,
|
||||
title: "英雄联盟-全英雄 全皮肤 龙瞎",
|
||||
price: 2888,
|
||||
server: "艾欧尼亚",
|
||||
security: "找回包赔",
|
||||
image: "/account-lol.jpg",
|
||||
tags: ["顶号", "满级"] as const,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "无畏契约-满特工 冠军套",
|
||||
price: 1500,
|
||||
server: "国服",
|
||||
security: "保险护航",
|
||||
image: "/account-val.jpg",
|
||||
tags: ["成品号"] as const,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "王者荣耀-V10 300皮肤",
|
||||
price: 5600,
|
||||
server: "安卓QQ",
|
||||
security: "官方验号",
|
||||
image: "/account-hok.jpg",
|
||||
tags: ["贵族10", "国标"] as const,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "原神-满命雷神 草神",
|
||||
price: 3200,
|
||||
server: "天空岛",
|
||||
security: "签署合同",
|
||||
image: "/account-genshin.jpg",
|
||||
tags: ["三神", "高练度"] as const,
|
||||
},
|
||||
]
|
||||
|
||||
function AccountTradeContent() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const defaultTab = searchParams.get("tab") === "pawn" ? "pawn" : "buy"
|
||||
const [pawnPhone, setPawnPhone] = useState("")
|
||||
const [pawnGame, setPawnGame] = useState("王者荣耀")
|
||||
const [pawnNote, setPawnNote] = useState("")
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="text-white/70 hover:text-white" type="button">
|
||||
返回
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">游戏账号交易</h1>
|
||||
<div className="ml-auto" />
|
||||
</header>
|
||||
|
||||
<Tabs defaultValue={defaultTab} className="flex flex-col min-h-[calc(100vh-60px)]">
|
||||
<div className="px-4 mt-2">
|
||||
<TabsList className="w-full bg-white/5 p-1 rounded-xl grid grid-cols-2">
|
||||
<TabsTrigger value="buy" className="text-sm font-bold data-[state=active]:bg-primary data-[state=active]:text-black">
|
||||
账号买卖
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pawn" className="text-sm font-bold data-[state=active]:bg-amber-500 data-[state=active]:text-black">
|
||||
账号典当
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="buy" className="flex-1 px-4 space-y-4 mt-4 data-[state=inactive]:hidden">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40" size={16} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索游戏、区服、英雄..."
|
||||
className="w-full bg-white/5 rounded-lg py-2.5 pl-9 pr-4 text-sm text-white focus:outline-none focus:ring-1 focus:ring-primary placeholder:text-white/30"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" className="border-white/10 bg-white/5 text-white/70 w-10 px-0" type="button">
|
||||
<Filter size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto no-scrollbar pb-2">
|
||||
<div className="flex gap-3">
|
||||
{games.map((g) => (
|
||||
<div key={g.name} className="flex flex-col items-center gap-1 min-w-[60px] cursor-pointer group">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-xl flex items-center justify-center border border-white/10 overflow-hidden transition-all group-hover:border-primary/50 ${!g.icon ? "bg-white/5" : "bg-black"}`}
|
||||
>
|
||||
{g.icon ? <img src={g.icon} alt={g.name} className="w-12 h-12 object-cover" /> : <Gamepad2 className="text-white/50" />}
|
||||
</div>
|
||||
<span className="text-[10px] text-white/60 group-hover:text-white">{g.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{accounts.map((acc) => (
|
||||
<div
|
||||
key={acc.id}
|
||||
className="glass-card p-3 rounded-xl flex gap-3 hover:bg-white/5 transition-colors cursor-pointer group"
|
||||
onClick={() => toast({ title: "即将跳转详情", description: "该功能为模拟演示" })}
|
||||
onKeyDown={(e) => e.key === "Enter" && e.currentTarget.click()}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="w-28 h-24 bg-muted rounded-lg relative overflow-hidden shrink-0 border border-white/5">
|
||||
<FillImage src={acc.image || "/placeholder.svg"} alt={acc.title} className="h-24 w-28" />
|
||||
<div className="absolute top-1 left-1 bg-black/60 text-[9px] px-1.5 py-0.5 rounded backdrop-blur-sm text-white/90 font-medium z-10">
|
||||
{acc.server}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between py-0.5">
|
||||
<div>
|
||||
<h3 className="font-bold text-sm line-clamp-2 leading-snug">{acc.title}</h3>
|
||||
<div className="flex flex-wrap gap-1 mt-1.5">
|
||||
{acc.tags.map((tag) => (
|
||||
<span key={tag} className="text-[9px] bg-white/5 text-white/50 px-1 rounded">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-between mt-auto">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] text-green-400 flex items-center gap-1 mb-0.5">
|
||||
<Shield size={10} /> {acc.security}
|
||||
</span>
|
||||
<span className="text-lg font-bold text-primary leading-none">¥{acc.price}</span>
|
||||
</div>
|
||||
<Button size="sm" className="h-7 text-xs px-3 font-bold bg-white/10 hover:bg-primary hover:text-black" type="button" onClick={(e) => e.stopPropagation()}>
|
||||
购买
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pawn" className="flex-1 px-4 space-y-4 mt-4 data-[state=inactive]:hidden">
|
||||
<div className="bg-gradient-to-br from-amber-900/30 to-orange-900/20 p-6 rounded-3xl border border-amber-500/20 relative overflow-hidden shadow-lg">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-amber-500/10 rounded-full blur-3xl -mr-10 -mt-10 pointer-events-none" />
|
||||
<div className="flex items-center justify-between mb-6 relative z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-amber-500/20 to-orange-500/20 flex items-center justify-center border border-white/10 shadow-inner">
|
||||
<Wallet size={24} className="text-amber-300" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white">游戏账号质押</h2>
|
||||
<p className="text-xs text-amber-200/60 mt-0.5">合法典当 · 随时赎回 · 安全保障</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-white/40 mb-1">已质押账号</div>
|
||||
<div className="text-xl font-mono font-bold text-white tracking-tight">8,526</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 relative z-10">
|
||||
<div className="bg-black/30 rounded-xl p-3 text-center border border-white/5">
|
||||
<div className="text-[10px] text-white/40 mb-1.5 uppercase tracking-wider">典当总额</div>
|
||||
<div className="text-sm font-bold text-amber-300">¥ 8600万</div>
|
||||
</div>
|
||||
<div className="bg-black/30 rounded-xl p-3 text-center border border-white/5">
|
||||
<div className="text-[10px] text-white/40 mb-1.5 uppercase tracking-wider">平均额度</div>
|
||||
<div className="text-sm font-bold text-amber-300">¥ 5,000+</div>
|
||||
</div>
|
||||
<div className="bg-black/30 rounded-xl p-3 text-center border border-white/5">
|
||||
<div className="text-[10px] text-white/40 mb-1.5 uppercase tracking-wider">赎回率</div>
|
||||
<div className="text-sm font-bold text-amber-300">98.5%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 rounded-2xl">
|
||||
<h3 className="font-bold text-sm mb-3 flex items-center gap-2">
|
||||
<FileText size={16} className="text-amber-400" />
|
||||
典当流程
|
||||
</h3>
|
||||
<div className="flex items-center justify-between text-center">
|
||||
{[
|
||||
{ step: "1", label: "提交账号" },
|
||||
{ step: "2", label: "价值评估" },
|
||||
{ step: "3", label: "签署协议" },
|
||||
{ step: "4", label: "获得资金" },
|
||||
].map((item, i) => (
|
||||
<div key={item.step} className="flex-1 relative">
|
||||
<div className="w-8 h-8 rounded-full bg-amber-500/20 border border-amber-500/30 flex items-center justify-center mx-auto mb-1.5">
|
||||
<span className="text-xs font-bold text-amber-400">{item.step}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-white/60">{item.label}</span>
|
||||
{i < 3 && <div className="absolute top-4 left-[60%] w-[80%] h-[1px] bg-gradient-to-r from-amber-500/30 to-transparent" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 rounded-xl space-y-3 border border-amber-500/20">
|
||||
<h3 className="font-bold text-sm">快速申请(完整评估请使用管理端/线下对接)</h3>
|
||||
<div>
|
||||
<label className="text-xs text-white/60">手机号</label>
|
||||
<input
|
||||
value={pawnPhone}
|
||||
onChange={(e) => setPawnPhone(e.target.value)}
|
||||
className="mt-1 w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-sm"
|
||||
placeholder="11位手机号"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-white/60">游戏</label>
|
||||
<select
|
||||
value={pawnGame}
|
||||
onChange={(e) => setPawnGame(e.target.value)}
|
||||
className="mt-1 w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-sm"
|
||||
>
|
||||
{["王者荣耀", "英雄联盟", "无畏契约", "原神"].map((g) => (
|
||||
<option key={g} value={g}>
|
||||
{g}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-white/60">备注</label>
|
||||
<textarea
|
||||
value={pawnNote}
|
||||
onChange={(e) => setPawnNote(e.target.value)}
|
||||
rows={2}
|
||||
className="mt-1 w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-sm resize-none"
|
||||
placeholder="区服、段位、资产等"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
if (!/^1[3-9]\d{9}$/.test(pawnPhone.trim())) {
|
||||
toast({ title: "请输入有效手机号" })
|
||||
return
|
||||
}
|
||||
toast({ title: "申请已提交", description: `${pawnGame} 典当需求已记录,客服将联系您` })
|
||||
setPawnNote("")
|
||||
}}
|
||||
>
|
||||
提交典当申请
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 pb-8">
|
||||
<div className="bg-white/5 border border-white/5 p-3 rounded-xl flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<Shield className="text-green-400" size={16} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-bold">合同保障</div>
|
||||
<div className="text-[10px] text-white/40">签署正规协议</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 border border-white/5 p-3 rounded-xl flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-amber-500/10 flex items-center justify-center">
|
||||
<Clock className="text-amber-400" size={16} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-bold">随时赎回</div>
|
||||
<div className="text-[10px] text-white/40">质押期内可赎回</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AccountServicesPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
<div className="text-white/50 text-sm">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AccountTradeContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
89
new/wz-app/src/pages/services/LevelingPage.tsx
Normal file
89
new/wz-app/src/pages/services/LevelingPage.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Star, Zap, Trophy } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
|
||||
const boosters = [
|
||||
{ id: 1, title: "王者荣耀-荣耀王者直通车", price: 25, unit: "星", tags: ["极速", "金牌打手"], sales: 5000 },
|
||||
{ id: 2, title: "LOL-钻石大师晋级赛", price: 88, unit: "局", tags: ["包赢", "职业退役"], sales: 1200 },
|
||||
{ id: 3, title: "无畏契约-赋能战神代打", price: 50, unit: "胜场", tags: ["MVP", "直播"], sales: 800 },
|
||||
{ id: 4, title: "原神-深渊满星/神瞳全收集", price: 10, unit: "层", tags: ["纯手工", "安全"], sales: 3000 },
|
||||
]
|
||||
|
||||
export function LevelingPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 bg-background">
|
||||
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
|
||||
<button onClick={() => void navigate(-1)} className="text-white/70" type="button">
|
||||
返回
|
||||
</button>
|
||||
<h1 className="font-bold text-lg">游戏代练</h1>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="relative w-full h-32 rounded-xl overflow-hidden bg-gradient-to-r from-orange-500 to-red-600 flex items-center px-6">
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-xl font-bold text-white mb-1">代练通严选</h2>
|
||||
<p className="text-xs text-white/80">官方认证打手 · 保证金赔付 · 效率保障</p>
|
||||
</div>
|
||||
<Trophy className="absolute right-4 bottom-[-10px] text-white/20 w-24 h-24 rotate-12" />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 overflow-x-auto no-scrollbar pb-2">
|
||||
{["全部", "王者荣耀", "英雄联盟", "无畏契约", "和平精英", "永劫无间"].map((tag, i) => (
|
||||
<button
|
||||
key={tag}
|
||||
type="button"
|
||||
className={`whitespace-nowrap px-4 py-1.5 rounded-full text-xs font-medium ${i === 0 ? "bg-primary text-black" : "bg-white/5 text-white/60"}`}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{boosters.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="glass-card p-4 rounded-xl flex justify-between items-center hover:bg-white/10 transition-colors"
|
||||
onClick={() => toast({ title: "下单成功", description: "打手将在5分钟内上号" })}
|
||||
onKeyDown={(e) => e.key === "Enter" && e.currentTarget.click()}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div>
|
||||
<h3 className="font-bold text-sm mb-1">{item.title}</h3>
|
||||
<div className="flex gap-2 mb-2">
|
||||
{item.tags.map((t) => (
|
||||
<span key={t} className="text-[10px] px-1.5 py-0.5 rounded bg-white/5 text-white/50 border border-white/5">
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-white/40">
|
||||
<span className="flex items-center gap-1">
|
||||
<Zap size={12} className="text-yellow-500" /> 15分钟接单
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Star size={12} className="text-primary" /> 销量 {item.sales}+
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-primary">
|
||||
¥{item.price}
|
||||
<span className="text-xs text-white/50 font-normal">/{item.unit}</span>
|
||||
</div>
|
||||
<Button size="sm" className="h-7 text-xs mt-1" type="button" onClick={(e) => e.stopPropagation()}>
|
||||
立即下单
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
93
new/wz-app/src/pages/streamer/StreamerAudiencePage.tsx
Normal file
93
new/wz-app/src/pages/streamer/StreamerAudiencePage.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useMemo } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ArrowLeft, Users } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
/** 与 old 观众洞察列表结构一致;后续可对接 wz-api /streamer 旅程接口 */
|
||||
const MOCK_ROWS = [
|
||||
{
|
||||
viewerKey: "u1",
|
||||
displayLabel: "观众 A**",
|
||||
engagePercent: 72,
|
||||
maxWatchMarkSec: 120,
|
||||
likeCount: 4,
|
||||
commentCount: 1,
|
||||
shareCount: 0,
|
||||
giftCount: 0,
|
||||
lastTouchAt: new Date().toISOString(),
|
||||
recentTouches: [{ eventName: "进入直播间", detail: "来源推荐", at: new Date().toISOString() }],
|
||||
},
|
||||
{
|
||||
viewerKey: "u2",
|
||||
displayLabel: "观众 B**",
|
||||
engagePercent: 45,
|
||||
maxWatchMarkSec: 45,
|
||||
likeCount: 1,
|
||||
commentCount: 0,
|
||||
shareCount: 1,
|
||||
giftCount: 0,
|
||||
lastTouchAt: new Date().toISOString(),
|
||||
recentTouches: [],
|
||||
},
|
||||
]
|
||||
|
||||
export function StreamerAudiencePage() {
|
||||
const meta = useMemo(() => ({ windowDays: 7, totalViewers: MOCK_ROWS.length, streamerId: "demo" }), [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground p-4 pb-24 max-w-3xl mx-auto">
|
||||
<div className="flex items-center gap-3 mb-6 flex-wrap">
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<Link to="/streamer/studio">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
观众触达与旅程
|
||||
</h1>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
近 {meta.windowDays} 天 · 来自直播间/推荐流的观看时长、点赞、评论、分享与送礼(脱敏展示)· 当前为本地演示数据
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="ml-auto" type="button" onClick={() => window.location.reload()}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
共 <span className="font-semibold text-foreground">{meta.totalViewers}</span> 位观众有行为数据;「触达分」为 0–100 的启发式汇总。
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{MOCK_ROWS.map((v) => (
|
||||
<div key={v.viewerKey} className="rounded-xl border border-border bg-card p-3 text-sm space-y-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="font-medium">{v.displayLabel}</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-primary/15 text-primary font-bold">触达 {v.engagePercent}%</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs text-muted-foreground">
|
||||
<span>观看峰值 {v.maxWatchMarkSec}s+</span>
|
||||
<span>赞 {v.likeCount}</span>
|
||||
<span>评 {v.commentCount}</span>
|
||||
<span>享 {v.shareCount}</span>
|
||||
<span className="col-span-2">礼 {v.giftCount}</span>
|
||||
<span className="col-span-2 text-right">最近 {new Date(v.lastTouchAt).toLocaleString()}</span>
|
||||
</div>
|
||||
{v.recentTouches?.length ? (
|
||||
<div className="text-[10px] text-muted-foreground border-t border-border pt-2 space-y-0.5">
|
||||
{v.recentTouches.map((t, i) => (
|
||||
<div key={`${t.at}-${i}`}>
|
||||
{t.eventName}
|
||||
{t.detail ? ` · ${t.detail}` : ""} · {new Date(t.at).toLocaleTimeString()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
131
new/wz-app/src/pages/streamer/StreamerStudioPage.tsx
Normal file
131
new/wz-app/src/pages/streamer/StreamerStudioPage.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useState } from "react"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ArrowLeft, Radio, Square, ExternalLink, Copy, Cloud } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
|
||||
function copyText(label: string, text: string) {
|
||||
void navigator.clipboard.writeText(text)
|
||||
toast({ title: "已复制", description: label })
|
||||
}
|
||||
|
||||
/**
|
||||
* 主播工作台:与 old 布局一致的核心操作;开播/云签名等接 wz-api 前为本地演示。
|
||||
*/
|
||||
export function StreamerStudioPage() {
|
||||
const navigate = useNavigate()
|
||||
const [isLive, setIsLive] = useState(false)
|
||||
const [title, setTitle] = useState("我的直播间")
|
||||
const [pullUrl, setPullUrl] = useState("")
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
const streamerId = "1"
|
||||
|
||||
const onStart = () => {
|
||||
setBusy(true)
|
||||
setTimeout(() => {
|
||||
setBusy(false)
|
||||
setIsLive(true)
|
||||
toast({ title: "已开播(演示)", description: "对接 API 后将同步服务端房间状态" })
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const onStop = () => {
|
||||
setBusy(true)
|
||||
setTimeout(() => {
|
||||
setBusy(false)
|
||||
setIsLive(false)
|
||||
toast({ title: "已关播(演示)" })
|
||||
}, 400)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-28 px-4 pt-safe max-w-md mx-auto">
|
||||
<div className="flex items-center justify-between h-12 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => void navigate(-1)} type="button">
|
||||
<ArrowLeft size={20} />
|
||||
</Button>
|
||||
<h1 className="font-bold text-lg">主播工作台</h1>
|
||||
</div>
|
||||
{isLive ? (
|
||||
<span className="text-xs font-bold text-red-500 flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />
|
||||
直播中
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">未开播</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
在此维护房间标题与拉流预览。云厂商推流地址需服务端签名,完整能力见 <code className="text-[10px]">wz-api</code> 与部署环境变量。
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">直播间标题</label>
|
||||
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="展示在直播广场与房间内" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">拉流预览 URL(可选)</label>
|
||||
<Input value={pullUrl} onChange={(e) => setPullUrl(e.target.value)} placeholder="https://…/xxx.m3u8" />
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-primary/25 bg-primary/5 p-4 space-y-3">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||
<Cloud size={18} className="text-primary" />
|
||||
云厂商推流 / 播放(演示文案)
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
正式环境由服务端生成 RTMP/HLS,勿在客户端硬编码密钥。
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => copyText("示例", "rtmp://demo.example.com/live/wanzhi_" + streamerId)}
|
||||
>
|
||||
<Copy size={14} className="mr-1" />
|
||||
复制示例推流地址
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button className="flex-1 gap-2" disabled={busy || isLive} type="button" onClick={onStart}>
|
||||
<Radio size={18} />
|
||||
开始直播
|
||||
</Button>
|
||||
<Button variant="destructive" className="flex-1 gap-2" disabled={busy || !isLive} type="button" onClick={onStop}>
|
||||
<Square size={18} />
|
||||
结束直播
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-card p-4 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">观众触达</span>
|
||||
<Link to="/streamer/audience">
|
||||
<Button size="sm" variant="secondary" className="gap-1" type="button">
|
||||
查看旅程与互动 <ExternalLink size={14} />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-card p-4 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">去我的直播间</span>
|
||||
<Link to={`/live/${streamerId}`}>
|
||||
<Button size="sm" variant="outline" className="gap-1" type="button">
|
||||
打开 <ExternalLink size={14} />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# 玩值 C 端路由映射(`old` 为真源,uni 为历史对照)
|
||||
# 玩值 C 端路由映射(`old` 为真源,uni 为历史对照)
|
||||
|
||||
最后更新:2026-04-22
|
||||
|
||||
@@ -37,22 +37,22 @@
|
||||
| `/coupons` | `subpkgs/stack/coupons/index` | — | **已实现**(示例券 + 去商城) |
|
||||
| `/hotel` | `subpkgs/stack/hotel/index` | — | **已实现**(示例酒店 → 预订意向) |
|
||||
| `/party/[id]` | `subpkgs/stack/party-detail/index` | — | **已实现**;`onLoad` 读 `id` |
|
||||
| `/screen` | `subpkgs/stack/screen/index` | 59–61 | **占位保留**(本轮不迁大屏;后续独立处理) |
|
||||
| `/moments/[id]` | `subpkgs/stack/moments-detail/index` | — | **已实现**;`onLoad` 读 `id` |
|
||||
| `/screen` | `subpkgs/stack/screen/index` | 59–61 | **Vite** `ScreenPage` 轻量壳;运营大屏 wz-admin |
|
||||
| `/moments/[id]` | `subpkgs/stack/moments-detail/index` | — | **Vite** `MomentsDetailPage`(`:momentId`) |
|
||||
| `/moments/create` | `subpkgs/stack/moments-create/index` | 62+ | **已实现**(表单 + `postJourneyEvent`) |
|
||||
| `/orders` | `subpkgs/stack/orders/index` | — | 占位 / 迭代中 |
|
||||
| `/course/[id]` | `subpkgs/stack/course-detail/index` | — | **已实现**(骨架:读 `id`、回我的课程/商城) |
|
||||
| `/orders` | `subpkgs/stack/orders/index` | — | **Vite `OrdersPage`(2026-04 首批自 old 迁 UI)**;uni 列仍为历史 |
|
||||
| `/course/[id]` | `subpkgs/stack/course-detail/index` | — | **Vite** `CourseDetailPage`(目录+试看+`pay` 购买) |
|
||||
| `/guild` | `subpkgs/stack/guild/index` | — | **已实现** |
|
||||
| `/guild/[id]` | `subpkgs/stack/guild-detail/index` | — | **已实现**;`onLoad` 读 `id` |
|
||||
| `/mall/companion` | `subpkgs/stack/mall-companion/index` | — | 占位 / 迭代中 |
|
||||
| `/mall/points/[game]` | `subpkgs/stack/mall-points-game/index` | — | 占位 / 迭代中;`onLoad` 读 `game` |
|
||||
| `/mall/companion` | `subpkgs/stack/mall-companion/index` | — | **Vite** `MallCompanionPage` |
|
||||
| `/mall/points/[game]` | `subpkgs/stack/mall-points-game/index` | — | **Vite** `MallPointsGamePage`;`:game` |
|
||||
| `/mall/points` | `subpkgs/stack/mall-points/index` | — | **已实现**(顶栏/搜索/品类 + 游戏栅格,图标为 emoji 占位) |
|
||||
| `/mall/gear` | `subpkgs/stack/mall-gear/index` | — | 占位 / 迭代中 |
|
||||
| `/mall/courses` | `subpkgs/stack/mall-courses/index` | — | 占位 / 迭代中 |
|
||||
| `/mall/gear` | `subpkgs/stack/mall-gear/index` | — | **Vite** `MallGearPage` |
|
||||
| `/mall/courses` | `subpkgs/stack/mall-courses/index` | — | **Vite** `MallCoursesPage` |
|
||||
| `/services/leveling` | `subpkgs/stack/services-leveling/index` | — | **已实现**(档位说明 + 客服) |
|
||||
| `/transaction-history` | `subpkgs/stack/transaction-history/index` | — | **已实现**(示例流水 + 订单入口) |
|
||||
| `/streamer/audience` | `subpkgs/stack/streamer-audience/index` | 55 | **已实现**(示例指标 + 可选 `getStreamers` 填充) |
|
||||
| `/profile/edit` | `subpkgs/stack/profile-edit/index` | PATCH `/api/app/user/me` | **已实现**(昵称 + `patchUserMe`) |
|
||||
| `/profile/edit` | `subpkgs/stack/profile-edit/index` | PATCH `/api/app/user/me` | **Vite `ProfileEditPage`(2026-04 首批)**;uni 行为见左 |
|
||||
| `/coach` | `subpkgs/stack/coach/index` | GET `/api/app/streamers` | **已实现**(列表 + 空列表本地示例) |
|
||||
| `/coach/[id]` | `subpkgs/stack/coach-detail/index` | GET `/api/app/streamers/:id` | **已实现**;`onLoad` 读 `id`(失败回落占位文案) |
|
||||
| `/streamer/studio` | `subpkgs/stack/streamer-studio/index` | — | **已实现**(工作台入口栅格) |
|
||||
@@ -62,9 +62,9 @@
|
||||
| `/account-help` | `subpkgs/stack/account-help/index` | — | **已实现**(FAQ + 客服) |
|
||||
| `/recharge` | `subpkgs/stack/recharge/index` | — | **已实现**(档位骨架 + 客服/订单) |
|
||||
| `/my-courses` | `subpkgs/stack/my-courses/index` | — | **已实现**(示例课表 → 课程详情) |
|
||||
| `/chat/[id]` | `subpkgs/stack/chat-detail/index` | — | 占位 / 迭代中;`onLoad` 读 `id` |
|
||||
| `/messages` | `subpkgs/stack/messages/index` | — | 占位 / 迭代中 |
|
||||
| `/settings` | `subpkgs/stack/settings/index` | — | **已实现**(子页导航 + 本地推送开关) |
|
||||
| `/chat/[id]` | `subpkgs/stack/chat-detail/index` | — | **Vite** `ChatRoomPage`;`cp-*` 与派对群两态 |
|
||||
| `/messages` | `subpkgs/stack/messages/index` | — | **Vite `MessagesPage`(2026-04 首批)**;展示数据 |
|
||||
| `/settings` | `subpkgs/stack/settings/index` | — | **Vite `SettingsPage`(2026-04 首批)**;`Switch`+清缓存;uni 为历史 |
|
||||
| `/star/[id]` | `subpkgs/stack/star-detail/index` | — | **已实现**;`onLoad` 读 `id` |
|
||||
| `/vip` | `subpkgs/stack/vip/index` | — | **已实现**(档位说明 + 充值/领券) |
|
||||
| `/creator/apply` | `subpkgs/stack/creator-apply/index` | 62+ | **已实现**(表单 + `postJourneyEvent` + 客服) |
|
||||
@@ -79,4 +79,5 @@
|
||||
- 新增/变更 **玩值 C 端**路由时:以 **`old/app/`** 为准在本表维护;若仍保留 uni 工程对照,可同步改 wz-app 列,**不强制**再注册 `pages.json`(新需求勿落 uni)。
|
||||
- 接口落地后:更新 **API #** 与 **状态**,并同步 [`进度-看板.md`](进度-看板.md)。
|
||||
- **大屏 `screen`**:公网页见 `old/app/screen/page.tsx`;与 wz-api `/api/screen/*` 关系见对照表。
|
||||
- **历史 uni 占位组件**:`new/wz-app` 中若仍有 `stub-page`,仅作旧构建对照。
|
||||
- **历史 uni 占位组件**:`new/wz-app` 中若仍有 `stub-page`,仅作旧构建对照。
|
||||
- **Vite+React**(`new/wz-app` 当前主工程,见 `package.json`):`App.tsx` 所注册**非 Tab** 路由均已挂 `src/pages` 下真实组件(**无** `StackStubPage` / `ParamStubPage` 占位),详见 [`MIGRATION.md`](../../new/wz-app/MIGRATION.md) 与 `App.tsx`。
|
||||
|
||||
Reference in New Issue
Block a user