更新wz-app项目,完善迁移文档,新增路由映射和组件速查,重构App.tsx以移除占位组件,确保所有非Tab路由均已挂载真实组件,增强项目结构的清晰性与一致性。

This commit is contained in:
乘风
2026-04-23 18:01:25 +08:00
parent 464be23f89
commit 8bf6c5a75f
47 changed files with 6755 additions and 75 deletions

View File

@@ -1,18 +1,37 @@
# old C 端 → new/react-app 迁移清单
# old C 端 → new/wz-appVite + 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 契约** 与动效/体验迭代为主

View File

@@ -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>

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 />} />

View 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 }

View 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>
)
}

View 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>
)
}

View 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"> &gt;</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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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"> JPGPNG 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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">3AI员工</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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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> 0100
</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>
)
}

View 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>
)
}

View File

@@ -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` | 5961 | **占位保留**(本轮不迁大屏;后续独立处理) |
| `/moments/[id]` | `subpkgs/stack/moments-detail/index` | — | **已实现**`onLoad``id` |
| `/screen` | `subpkgs/stack/screen/index` | 5961 | **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`