feat(client): 完成uni-app用户端8大核心静态页面开发并验证微信小程序与H5双端构建成功

这个提交包含在:
wikiboss
2026-09-16 16:29:43 +08:00
父节点 14cc769fff
当前提交 4190070ff4
共有 21 个文件被更改,包括 12234 次插入 和 1 次删除

1
client/.npmrc 普通文件
查看文件

@@ -0,0 +1 @@
node-linker=hoisted

20
client/index.html 普通文件
查看文件

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title>电竞陪玩平台</title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

31
client/package.json 普通文件
查看文件

@@ -0,0 +1,31 @@
{
"name": "peiwan-client",
"version": "1.0.0",
"description": "游戏陪玩小程序客户端 (uni-app Vue 3 + TypeScript)",
"scripts": {
"dev:h5": "uni",
"build:h5": "uni build",
"dev:mp-weixin": "uni -p mp-weixin",
"build:mp-weixin": "uni build -p mp-weixin"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-alpha-5020620260914001",
"@dcloudio/uni-app-plus": "3.0.0-alpha-5020620260914001",
"@dcloudio/uni-components": "3.0.0-alpha-5020620260914001",
"@dcloudio/uni-h5": "3.0.0-alpha-5020620260914001",
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-5020620260914001",
"vue": "^3.4.21"
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-alpha-5020620260914001",
"@dcloudio/uni-cli-shared": "3.0.0-alpha-5020620260914001",
"@dcloudio/uni-stacktracey": "3.0.0-alpha-5020620260914001",
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-5020620260914001",
"@vue/runtime-core": "^3.4.21",
"@vue/tsconfig": "^0.5.1",
"sass": "^1.71.1",
"typescript": "^5.3.3",
"vite": "5.2.8"
}
}

8088
client/pnpm-lock.yaml 自动生成的 普通文件

文件差异内容过多而无法显示 加载差异

45
client/src/App.vue 普通文件
查看文件

@@ -0,0 +1,45 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
onLaunch(() => {
console.log('App Launch: 陪玩小程序初始化');
});
onShow(() => {
console.log('App Show');
});
onHide(() => {
console.log('App Hide');
});
</script>
<style lang="scss">
@import './uni.scss';
/* 全局应用基础重置 */
page {
background-color: $bg-primary;
color: $text-primary;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', sans-serif;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
}
view, text, image, input, button {
box-sizing: border-box;
}
/* 触控反馈标准类 (满足 UI/UX Pro Max 规范) */
.btn-active:active, .card-active:active {
transform: scale(0.98);
opacity: 0.9;
transition: transform 100ms ease-out, opacity 100ms ease-out;
}
/* 隐藏移动端滚动条 */
::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
</style>

9
client/src/main.ts 普通文件
查看文件

@@ -0,0 +1,9 @@
import { createSSRApp } from 'vue';
import App from './App.vue';
export function createApp() {
const app = createSSRApp(App);
return {
app,
};
}

24
client/src/manifest.json 普通文件
查看文件

@@ -0,0 +1,24 @@
{
"name": "电竞陪玩",
"appid": "",
"description": "电竞陪玩与活动约玩小程序",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"mp-weixin": {
"appid": "touristappid",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true
},
"usingComponents": true
},
"h5": {
"title": "电竞陪玩平台",
"router": {
"mode": "hash"
}
}
}

297
client/src/mock/data.ts 普通文件
查看文件

@@ -0,0 +1,297 @@
export interface GameCategory {
id: string;
name: string;
icon: string;
hot: boolean;
}
export interface ExpertServiceItem {
id: string;
title: string;
price: number;
unit: string;
desc: string;
}
export interface ExpertReview {
id: string;
userNickname: string;
userAvatar: string;
rating: number;
tags: string[];
content: string;
time: string;
}
export interface ExpertItem {
id: string;
nickname: string;
avatar: string;
gender: 'male' | 'female';
age: number;
game: string;
rankTag: string;
levelTag: string;
price: number;
rating: number;
orderCount: number;
isOnline: boolean;
voiceDuration: number;
bio: string;
coverImage: string;
photos: string[];
services: ExpertServiceItem[];
reviews: ExpertReview[];
}
export interface ActivityItem {
id: string;
title: string;
game: string;
payType: 'AA' | 'PROJECT';
totalFee: number;
aaFee: number;
maxParticipants: number;
currentParticipants: number;
startTime: string;
location: string;
creatorNickname: string;
creatorAvatar: string;
status: 'RECRUITING' | 'RUNNING' | 'FINISHED';
participants: { id: string; nickname: string; avatar: string; verifyCode: string; checkedIn: boolean }[];
}
export interface OrderItem {
id: string;
orderSn: string;
expert: { id: string; nickname: string; avatar: string; game: string };
serviceTitle: string;
quantity: number;
totalAmount: number;
status: 'PENDING_PAY' | 'PENDING_ACCEPT' | 'IN_SERVICE' | 'PENDING_CONFIRM' | 'COMPLETED';
createdAt: string;
}
export const GAMES_LIST: GameCategory[] = [
{ id: 'all', name: '全部', icon: '🎮', hot: true },
{ id: 'wzry', name: '王者荣耀', icon: '👑', hot: true },
{ id: 'hpjy', name: '和平精英', icon: '🎯', hot: true },
{ id: 'lol', name: '英雄联盟', icon: '⚔️', hot: true },
{ id: 'val', name: '无畏契约', icon: '🔫', hot: true },
{ id: 'yjjw', name: '永劫无间', icon: '🗡️', hot: false },
{ id: 'pubg', name: '绝地求生', icon: '🏆', hot: false },
];
export const BANNERS = [
{ id: '1', title: '周末车队狂欢 | AA制五排冲分', tag: '热门活动', bg: 'linear-gradient(135deg, #4F46E5 0%, #7C3AED 100%)' },
{ id: '2', title: '新用户专享 | 首单立减20元', tag: '新人福利', bg: 'linear-gradient(135deg, #059669 0%, #10B981 100%)' },
{ id: '3', title: '全国水友杯内战 | 项目制万元奖池', tag: '巅峰赛事', bg: 'linear-gradient(135deg, #D97706 0%, #F59E0B 100%)' },
];
export const EXPERTS_LIST: ExpertItem[] = [
{
id: 'exp_101',
nickname: '月色浅浅',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200&h=200&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=800&h=400&fit=crop',
gender: 'female',
age: 21,
game: '王者荣耀',
rankTag: '百星王者 · 国服瑶妹',
levelTag: '金牌导师',
price: 35,
rating: 4.9,
orderCount: 1280,
isOnline: true,
voiceDuration: 12,
bio: '全天在线可连麦,心态超好,软妹音/御姐音切换,带你轻松摘星~',
photos: [
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&fit=crop',
'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=500&fit=crop',
],
services: [
{ id: 'srv_1', title: '王者荣耀 · 排位包胜率', price: 35, unit: '小时', desc: '不包赢不退,高效稳定冲星' },
{ id: 'srv_2', title: '王者荣耀 · 娱乐连麦陪练', price: 28, unit: '小时', desc: '温柔互动,欢乐开黑' },
],
reviews: [
{ id: 'r1', userNickname: '电竞刺客', userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', rating: 5, tags: ['声音甜美', '技术过硬'], content: '小姐姐声音太好听了,操作也很稳,连赢5把!', time: '2小时前' },
{ id: 'r2', userNickname: '峡谷追梦人', userAvatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=100', rating: 5, tags: ['准时上号', '耐心教学'], content: '很耐心教辅助意识,体验绝佳。', time: '昨天' },
],
},
{
id: 'exp_102',
nickname: '夜鹰狙神',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200&h=200&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1511512578047-dfb367046420?w=800&h=400&fit=crop',
gender: 'male',
age: 23,
game: '和平精英',
rankTag: '无敌战神 · 15KD',
levelTag: '顶流大神',
price: 45,
rating: 5.0,
orderCount: 2360,
isOnline: true,
voiceDuration: 18,
bio: '前职业青训选手,意识拉满,带粉乱杀,绝不白给!',
photos: [
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=500&fit=crop',
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=500&fit=crop',
],
services: [
{ id: 'srv_3', title: '和平精英 · 战神四排车队', price: 45, unit: '小时', desc: '战神指引,全程高能护航' },
{ id: 'srv_4', title: '吃鸡指导 · 枪法与身法特训', price: 60, unit: '小时', desc: '1对1定制身法特训' },
],
reviews: [
{ id: 'r3', userNickname: 'AWM战狂', userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', rating: 5, tags: ['枪法如神', '落地包稳'], content: '真的太猛了,连吃三把鸡,KD直接飙升!', time: '前天' },
],
},
{
id: 'exp_103',
nickname: '软软喵',
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200&h=200&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1538481199705-c710c4e965fc?w=800&h=400&fit=crop',
gender: 'female',
age: 20,
game: '英雄联盟',
rankTag: '傲世宗师 · 中单法王',
levelTag: '新星大神',
price: 30,
rating: 4.8,
orderCount: 650,
isOnline: false,
voiceDuration: 9,
bio: '峡谷高分妹子,阿狸/辛德拉本命,能C能辅,幽默开心果~',
photos: ['https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=500&fit=crop'],
services: [
{ id: 'srv_5', title: 'LOL峡谷排位双排', price: 30, unit: '小时', desc: '双排上分,稳定Carry' },
],
reviews: [],
},
{
id: 'exp_104',
nickname: '瞬移者雷克',
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=200&h=200&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=800&h=400&fit=crop',
gender: 'male',
age: 22,
game: '无畏契约',
rankTag: '赋能战魂 · 芮娜绝活',
levelTag: '优质陪练',
price: 40,
rating: 4.9,
orderCount: 890,
isOnline: true,
voiceDuration: 15,
bio: '瓦罗兰特高分车头,定位准身法快,报点清晰,带你上神话!',
photos: ['https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=500&fit=crop'],
services: [
{ id: 'srv_6', title: '无畏契约冲分排位', price: 40, unit: '小时', desc: '带队突破,绝不拖泥带水' },
],
reviews: [],
},
];
export const ACTIVITIES_LIST: ActivityItem[] = [
{
id: 'act_101',
title: '【AA制五排】今晚8点王者百星车队冲分',
game: '王者荣耀',
payType: 'AA',
totalFee: 120,
aaFee: 30,
maxParticipants: 4,
currentParticipants: 3,
startTime: '今晚 20:00',
location: '开黑语音1号频道',
creatorNickname: '月色浅浅',
creatorAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100',
status: 'RECRUITING',
participants: [
{ id: 'p1', nickname: '峡谷追梦', avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', verifyCode: '8392', checkedIn: false },
{ id: 'p2', nickname: '无敌小刺客', avatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=100', verifyCode: '6147', checkedIn: true },
{ id: 'p3', nickname: '快乐小风男', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100', verifyCode: '9025', checkedIn: false },
],
},
{
id: 'act_102',
title: '【项目制·粉丝福利】夜鹰带队水友吃鸡四排',
game: '和平精英',
payType: 'PROJECT',
totalFee: 300,
aaFee: 0,
maxParticipants: 3,
currentParticipants: 3,
startTime: '周六 15:00',
location: 'YY频道 668899',
creatorNickname: '夜鹰狙神',
creatorAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100',
status: 'RUNNING',
participants: [
{ id: 'p4', nickname: '绝地菜鸟', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100', verifyCode: '2481', checkedIn: true },
{ id: 'p5', nickname: '伏地魔本魔', avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100', verifyCode: '7390', checkedIn: false },
],
},
{
id: 'act_103',
title: '【AA制】无畏契约五黑内战水友欢乐局',
game: '无畏契约',
payType: 'AA',
totalFee: 160,
aaFee: 20,
maxParticipants: 8,
currentParticipants: 5,
startTime: '周日 19:30',
location: '自定义内战房间',
creatorNickname: '瞬移者雷克',
creatorAvatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100',
status: 'RECRUITING',
participants: [],
},
];
export const ORDERS_LIST: OrderItem[] = [
{
id: 'ord_1',
orderSn: 'PW20260916001',
expert: { id: 'exp_101', nickname: '月色浅浅', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100', game: '王者荣耀' },
serviceTitle: '王者荣耀 · 排位包胜率',
quantity: 2,
totalAmount: 70,
status: 'IN_SERVICE',
createdAt: '2026-09-16 15:30',
},
{
id: 'ord_2',
orderSn: 'PW20260915088',
expert: { id: 'exp_102', nickname: '夜鹰狙神', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100', game: '和平精英' },
serviceTitle: '和平精英 · 战神四排车队',
quantity: 1,
totalAmount: 45,
status: 'COMPLETED',
createdAt: '2026-09-15 19:00',
},
{
id: 'ord_3',
orderSn: 'PW20260914032',
expert: { id: 'exp_103', nickname: '软软喵', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100', game: '英雄联盟' },
serviceTitle: 'LOL峡谷排位双排',
quantity: 3,
totalAmount: 90,
status: 'PENDING_PAY',
createdAt: '2026-09-14 21:10',
},
];
export const USER_PROFILE = {
id: 'u_888',
nickname: '超能电竞玩家',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200&h=200&fit=crop&crop=faces',
gender: 'male',
role: 'PLAYER',
balance: 380.5,
frozenBalance: 0,
commission: 128.0,
invitedCount: 16,
isCertifiedExpert: false,
};

106
client/src/pages.json 普通文件
查看文件

@@ -0,0 +1,106 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "电竞陪玩大厅",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/activity/list",
"style": {
"navigationBarTitleText": "组队活动大厅",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/activity/detail",
"style": {
"navigationBarTitleText": "活动详情",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/activity/my-ticket",
"style": {
"navigationBarTitleText": "我的活动门票",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/expert/detail",
"style": {
"navigationBarTitleText": "陪玩大神主页",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/order/create",
"style": {
"navigationBarTitleText": "确认预约订单",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/order/list",
"style": {
"navigationBarTitleText": "我的订单",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/user/index",
"style": {
"navigationBarTitleText": "个人中心",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "电竞陪玩",
"navigationBarBackgroundColor": "#0A0F1D",
"backgroundColor": "#0A0F1D"
},
"tabBar": {
"color": "#94A3B8",
"selectedColor": "#6366F1",
"backgroundColor": "#151D30",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页"
},
{
"pagePath": "pages/activity/list",
"text": "活动项目"
},
{
"pagePath": "pages/order/list",
"text": "订单中心"
},
{
"pagePath": "pages/user/index",
"text": "我的"
}
]
}
}

查看文件

@@ -0,0 +1,392 @@
<template>
<view class="detail-container" v-if="activity">
<!-- 头部活动卡片 -->
<view class="hero-card">
<view class="header-tags">
<view class="type-pill" :class="activity.payType === 'AA' ? 'pill-aa' : 'pill-project'">
{{ activity.payType === 'AA' ? 'AA制付费活动' : '项目制发起活动' }}
</view>
<view class="game-badge">{{ activity.game }}</view>
</view>
<view class="act-title">{{ activity.title }}</view>
<view class="time-location-box">
<view class="row">
<text class="label">活动时间:</text>
<text class="val">{{ activity.startTime }}</text>
</view>
<view class="row">
<text class="label">开黑频道:</text>
<text class="val">{{ activity.location }}</text>
</view>
</view>
</view>
<!-- 发起人卡片 -->
<view class="section-card creator-card">
<view class="section-title">活动发起人</view>
<view class="creator-info">
<image class="avatar" :src="activity.creatorAvatar" mode="aspectFill" />
<view class="text-wrap">
<view class="name">{{ activity.creatorNickname }}</view>
<view class="desc">活动房主 · 负责活动组织与现场核销</view>
</view>
<view class="contact-btn btn-active" @click="handleContact">
联系TA
</view>
</view>
</view>
<!-- 费用平摊与结算明细 -->
<view class="section-card fee-card">
<view class="section-title">费用结算规则</view>
<view class="fee-detail" v-if="activity.payType === 'AA'">
<view class="fee-row">
<text class="fee-name">活动总预算</text>
<text class="fee-val">¥{{ activity.totalFee }}</text>
</view>
<view class="fee-row">
<text class="fee-name">招募参与人数</text>
<text class="fee-val">{{ activity.maxParticipants }} 人</text>
</view>
<view class="divider"></view>
<view class="fee-row highlight">
<text class="fee-name">当前每人预估分摊</text>
<text class="fee-val total">¥{{ activity.aaFee }}</text>
</view>
<view class="rule-tip">* 报名后费用将锁定在平台监管账户,现场完成【4位核销码】签到后解冻。</view>
</view>
<view class="fee-detail" v-else>
<view class="fee-row">
<text class="fee-name">项目制总包款项</text>
<text class="fee-val">¥{{ activity.totalFee }}</text>
</view>
<view class="fee-row">
<text class="fee-name">发起人支付状态</text>
<text class="fee-val" style="color: #10B981;">已全额买断支付</text>
</view>
<view class="divider"></view>
<view class="fee-row highlight">
<text class="fee-name">水友参与报名费用</text>
<text class="fee-val total" style="color: #10B981;">0 元免费</text>
</view>
</view>
</view>
<!-- 当前已报名人员列表与核销情况 -->
<view class="section-card participants-card">
<view class="header-split">
<view class="section-title">已报名名单 ({{ activity.participants.length }})</view>
<view class="limit-text">上限 {{ activity.maxParticipants > 0 ? activity.maxParticipants : '不限' }} 人</view>
</view>
<view class="user-list">
<view
v-for="(p, index) in activity.participants"
:key="p.id"
class="user-row"
>
<image class="user-avatar" :src="p.avatar" mode="aspectFill" />
<view class="user-meta">
<text class="user-nickname">{{ p.nickname }}</text>
<text class="user-seq">第 {{ index + 1 }} 位加入</text>
</view>
<view class="checkin-tag" :class="p.checkedIn ? 'checked' : 'waiting'">
{{ p.checkedIn ? '已核销签到' : '已生成核销码' }}
</view>
</view>
</view>
</view>
<!-- 底部固定报名 CTA 栏 -->
<view class="bottom-bar">
<view class="price-box">
<text class="label">{{ activity.payType === 'AA' ? '应付平摊费:' : '报名费:' }}</text>
<text class="currency">¥</text>
<text class="price">{{ activity.payType === 'AA' ? activity.aaFee : 0 }}</text>
</view>
<button class="join-btn btn-active" @click="handleJoinActivity">
立即报名生成 4 位核销码
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { ACTIVITIES_LIST, ActivityItem } from '../../mock/data';
const activity = ref<ActivityItem | null>(null);
onLoad((options) => {
const actId = options?.id || 'act_101';
activity.value = ACTIVITIES_LIST.find((a) => a.id === actId) || ACTIVITIES_LIST[0];
});
const handleContact = () => {
uni.showToast({ title: '已进入与发起人的即时沟通会话', icon: 'none' });
};
const handleJoinActivity = () => {
uni.showModal({
title: '确认报名活动',
content: `报名成功后,系统将为您生成该活动专属的【4位纯数字核销码】作为入场凭证!`,
confirmText: '支付并生成',
confirmColor: '#6366F1',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '生成核销码中...' });
setTimeout(() => {
uni.hideLoading();
uni.navigateTo({
url: `/pages/activity/my-ticket?id=${activity.value?.id}&code=8392`
});
}, 500);
}
}
});
};
</script>
<style lang="scss" scoped>
@import '../../uni.scss';
.detail-container {
min-height: 100vh;
padding: 24rpx 24rpx 160rpx;
background-color: $bg-primary;
}
.hero-card {
background: linear-gradient(135deg, $bg-surface 0%, $bg-surface-elevated 100%);
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 28rpx;
margin-bottom: 24rpx;
.header-tags {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 16rpx;
.type-pill {
padding: 6rpx 18rpx;
border-radius: $radius-sm;
font-size: 22rpx;
font-weight: bold;
&.pill-aa {
background-color: rgba(16, 185, 129, 0.2);
color: $color-accent;
border: 1rpx solid rgba(16, 185, 129, 0.3);
}
&.pill-project {
background-color: rgba(99, 102, 241, 0.2);
color: $color-brand-light;
border: 1rpx solid rgba(99, 102, 241, 0.3);
}
}
.game-badge {
font-size: 22rpx;
color: $text-secondary;
}
}
.act-title {
font-size: 34rpx;
font-weight: bold;
color: $text-primary;
line-height: 1.4;
margin-bottom: 20rpx;
}
.time-location-box {
background-color: rgba(10, 15, 29, 0.6);
border-radius: $radius-md;
padding: 16rpx 20rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
.row {
font-size: 24rpx;
.label { color: $text-secondary; }
.val { color: $text-primary; font-weight: 500; }
}
}
}
.section-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 24rpx;
margin-bottom: 24rpx;
.section-title {
font-size: 28rpx;
font-weight: 600;
color: $text-primary;
margin-bottom: 20rpx;
}
}
.creator-card {
.creator-info {
display: flex;
align-items: center;
gap: 16rpx;
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.text-wrap {
flex: 1;
.name { font-size: 28rpx; font-weight: 600; color: $text-primary; }
.desc { font-size: 22rpx; color: $text-secondary; margin-top: 4rpx; }
}
.contact-btn {
padding: 12rpx 28rpx;
background-color: $bg-surface-elevated;
border: 1rpx solid $border-subtle;
border-radius: $radius-full;
font-size: 24rpx;
color: $color-brand-light;
}
}
}
.fee-card {
.fee-detail {
display: flex;
flex-direction: column;
gap: 16rpx;
.fee-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 26rpx;
.fee-name { color: $text-secondary; }
.fee-val { color: $text-primary; font-weight: 500; }
&.highlight {
.fee-name { color: #FFFFFF; font-weight: bold; }
.fee-val.total { font-size: 36rpx; font-weight: bold; color: $color-danger; }
}
}
.divider {
height: 1rpx;
background-color: $border-subtle;
margin: 8rpx 0;
}
.rule-tip {
font-size: 20rpx;
color: $text-muted;
line-height: 1.4;
}
}
}
.participants-card {
.header-split {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.section-title { margin-bottom: 0; }
.limit-text { font-size: 22rpx; color: $text-secondary; }
}
.user-list {
display: flex;
flex-direction: column;
gap: 16rpx;
.user-row {
display: flex;
align-items: center;
gap: 16rpx;
padding: 12rpx 0;
border-bottom: 1rpx solid rgba(30, 41, 59, 0.4);
.user-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
}
.user-meta {
flex: 1;
display: flex;
flex-direction: column;
.user-nickname { font-size: 26rpx; color: $text-primary; font-weight: 500; }
.user-seq { font-size: 20rpx; color: $text-muted; }
}
.checkin-tag {
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: $radius-sm;
&.checked {
background-color: rgba(16, 185, 129, 0.2);
color: $color-accent;
}
&.waiting {
background-color: rgba(99, 102, 241, 0.15);
color: $color-brand-light;
}
}
}
}
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 120rpx;
padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
background-color: $bg-surface;
border-top: 1rpx solid $border-subtle;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 100;
.price-box {
display: flex;
align-items: baseline;
gap: 6rpx;
.label { font-size: 24rpx; color: $text-secondary; }
.currency { font-size: 26rpx; color: $color-danger; font-weight: bold; }
.price { font-size: 44rpx; color: $color-danger; font-weight: bold; }
}
.join-btn {
height: 80rpx;
padding: 0 36rpx;
background: linear-gradient(135deg, $color-brand 0%, $color-brand-purple 100%);
border-radius: $radius-full;
font-size: 28rpx;
font-weight: 600;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-brand;
}
}
</style>

查看文件

@@ -0,0 +1,451 @@
<template>
<view class="activity-container">
<!-- 顶部 AA制 / 项目制 双模切换 -->
<view class="mode-tabs">
<view
class="tab-item btn-active"
:class="{ active: currentTab === 'ALL' }"
@click="currentTab = 'ALL'"
>
全部活动
</view>
<view
class="tab-item btn-active"
:class="{ active: currentTab === 'AA' }"
@click="currentTab = 'AA'"
>
<text class="tab-badge aa">AA制</text>
AA平摊拼单
</view>
<view
class="tab-item btn-active"
:class="{ active: currentTab === 'PROJECT' }"
@click="currentTab = 'PROJECT'"
>
<text class="tab-badge project">项目制</text>
包揽/水友赛
</view>
</view>
<!-- 活动规则提示微卡片 -->
<view class="rule-hint-card">
<text class="hint-icon">💡</text>
<view class="hint-text">
<text class="highlight">AA制</text>报名平摊服务费;<text class="highlight">项目制</text>由发起人买断总预算。报名均生成<text class="highlight-green">4位核销码</text>凭码签到!
</view>
</view>
<!-- 活动列表 -->
<view class="activity-list">
<view
v-for="act in filteredActivities"
:key="act.id"
class="activity-card card-active"
@click="navToDetail(act.id)"
>
<!-- 头部:类型标签与状态 -->
<view class="card-header">
<view class="type-wrap">
<view class="type-pill" :class="act.payType === 'AA' ? 'pill-aa' : 'pill-project'">
{{ act.payType === 'AA' ? 'AA制付费' : '项目制买断' }}
</view>
<view class="game-tag">{{ act.game }}</view>
</view>
<view class="status-pill" :class="act.status.toLowerCase()">
{{ act.status === 'RECRUITING' ? '招募中' : act.status === 'RUNNING' ? '进行中' : '已结束' }}
</view>
</view>
<!-- 标题 -->
<view class="card-title">{{ act.title }}</view>
<!-- 发起人与时间地点 -->
<view class="meta-row">
<view class="creator-box">
<image class="creator-avatar" :src="act.creatorAvatar" mode="aspectFill" />
<text class="creator-name">{{ act.creatorNickname }} (发起人)</text>
</view>
<view class="time-box">⏰ {{ act.startTime }}</view>
</view>
<!-- 费用与进度看板 -->
<view class="footer-row">
<view class="fee-info">
<template v-if="act.payType === 'AA'">
<text class="fee-label">每人预估:</text>
<text class="fee-currency">¥</text>
<text class="fee-amount">{{ act.aaFee }}</text>
<text class="fee-sub">(总包¥{{ act.totalFee }})</text>
</template>
<template v-else>
<text class="fee-label">项目预算:</text>
<text class="fee-currency">¥</text>
<text class="fee-amount">{{ act.totalFee }}</text>
<text class="fee-free">免费参团</text>
</template>
</view>
<!-- 报名进度与参与人头像群 -->
<view class="progress-box">
<view class="participant-avatars" v-if="act.participants.length > 0">
<image
v-for="(p, i) in act.participants.slice(0, 3)"
:key="p.id"
class="stacked-avatar"
:src="p.avatar"
:style="{ right: (i * 18) + 'rpx' }"
/>
</view>
<view class="progress-text">
已报 <text class="current-num">{{ act.currentParticipants }}</text>
<text v-if="act.maxParticipants > 0">/{{ act.maxParticipants }}人</text>
<text v-else>/不限人数</text>
</view>
</view>
</view>
</view>
</view>
<!-- 悬浮:发起活动按钮 -->
<view class="floating-create-btn btn-active" @click="handleCreateActivity">
<text class="create-icon">+</text>
<text class="create-text">发起活动</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ACTIVITIES_LIST } from '../../mock/data';
const currentTab = ref<'ALL' | 'AA' | 'PROJECT'>('ALL');
const filteredActivities = computed(() => {
if (currentTab.value === 'ALL') return ACTIVITIES_LIST;
return ACTIVITIES_LIST.filter((a) => a.payType === currentTab.value);
});
const navToDetail = (id: string) => {
uni.navigateTo({ url: `/pages/activity/detail?id=${id}` });
};
const handleCreateActivity = () => {
uni.showActionSheet({
itemList: ['发起 AA制 平摊车队', '发起 项目制 水友活动'],
success: (res) => {
const type = res.tapIndex === 0 ? 'AA制' : '项目制';
uni.showModal({
title: `发起${type}活动`,
content: `请确认发起模式为【${type}】。创建成功后参与者将自动生成 4 位核销码!`,
confirmText: '确定发起',
confirmColor: '#6366F1',
success: (mRes) => {
if (mRes.confirm) {
uni.showToast({ title: `${type}活动发起成功`, icon: 'success' });
}
}
});
}
});
};
</script>
<style lang="scss" scoped>
@import '../../uni.scss';
.activity-container {
min-height: 100vh;
padding: 24rpx 24rpx 140rpx;
background-color: $bg-primary;
}
/* 顶部 Tab 切换 */
.mode-tabs {
display: flex;
background-color: $bg-surface;
border-radius: $radius-lg;
padding: 8rpx;
margin-bottom: 20rpx;
border: 1rpx solid $border-subtle;
.tab-item {
flex: 1;
text-align: center;
padding: 18rpx 0;
font-size: 26rpx;
color: $text-secondary;
border-radius: $radius-md;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
.tab-badge {
font-size: 18rpx;
padding: 2rpx 8rpx;
border-radius: $radius-sm;
&.aa {
background-color: rgba(16, 185, 129, 0.2);
color: $color-accent;
}
&.project {
background-color: rgba(99, 102, 241, 0.2);
color: $color-brand-light;
}
}
&.active {
background: linear-gradient(135deg, $color-brand 0%, $color-brand-purple 100%);
color: #FFFFFF;
font-weight: 600;
.tab-badge {
background-color: rgba(255, 255, 255, 0.25);
color: #FFFFFF;
}
}
}
}
/* 规则微提示卡片 */
.rule-hint-card {
display: flex;
align-items: center;
gap: 12rpx;
padding: 16rpx 20rpx;
background-color: rgba(99, 102, 241, 0.1);
border: 1rpx solid rgba(99, 102, 241, 0.25);
border-radius: $radius-md;
margin-bottom: 24rpx;
.hint-icon {
font-size: 28rpx;
}
.hint-text {
font-size: 22rpx;
color: $text-secondary;
line-height: 1.4;
.highlight {
color: #FFFFFF;
font-weight: 600;
}
.highlight-green {
color: $color-accent;
font-weight: bold;
}
}
}
/* 活动卡片列表 */
.activity-list {
display: flex;
flex-direction: column;
gap: 24rpx;
.activity-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 24rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.type-wrap {
display: flex;
align-items: center;
gap: 12rpx;
.type-pill {
padding: 6rpx 16rpx;
border-radius: $radius-sm;
font-size: 20rpx;
font-weight: bold;
&.pill-aa {
background-color: rgba(16, 185, 129, 0.15);
color: $color-accent;
border: 1rpx solid rgba(16, 185, 129, 0.3);
}
&.pill-project {
background-color: rgba(99, 102, 241, 0.15);
color: $color-brand-light;
border: 1rpx solid rgba(99, 102, 241, 0.3);
}
}
.game-tag {
font-size: 22rpx;
color: $text-secondary;
}
}
.status-pill {
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: $radius-full;
&.recruiting {
background-color: rgba(16, 185, 129, 0.2);
color: $color-accent;
}
&.running {
background-color: rgba(245, 158, 11, 0.2);
color: $color-warning;
}
&.finished {
background-color: rgba(100, 116, 139, 0.2);
color: $text-muted;
}
}
}
.card-title {
font-size: 30rpx;
font-weight: 600;
color: $text-primary;
line-height: 1.4;
}
.meta-row {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 22rpx;
color: $text-secondary;
.creator-box {
display: flex;
align-items: center;
gap: 10rpx;
.creator-avatar {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
}
.creator-name {
color: $text-secondary;
}
}
}
.footer-row {
display: flex;
align-items: flex-end;
justify-content: space-between;
padding-top: 12rpx;
border-top: 1rpx solid rgba(30, 41, 59, 0.6);
.fee-info {
display: flex;
align-items: baseline;
gap: 6rpx;
.fee-label {
font-size: 22rpx;
color: $text-secondary;
}
.fee-currency {
font-size: 24rpx;
color: $color-danger;
font-weight: bold;
}
.fee-amount {
font-size: 36rpx;
color: $color-danger;
font-weight: bold;
}
.fee-sub {
font-size: 20rpx;
color: $text-muted;
}
.fee-free {
font-size: 22rpx;
color: $color-accent;
font-weight: bold;
margin-left: 8rpx;
}
}
.progress-box {
display: flex;
align-items: center;
gap: 12rpx;
.participant-avatars {
display: flex;
position: relative;
height: 40rpx;
width: 70rpx;
.stacked-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 2rpx solid $bg-surface;
position: absolute;
top: 0;
}
}
.progress-text {
font-size: 22rpx;
color: $text-secondary;
.current-num {
color: $color-brand-light;
font-weight: bold;
}
}
}
}
}
}
/* 悬浮发起活动按钮 */
.floating-create-btn {
position: fixed;
bottom: 160rpx;
right: 32rpx;
height: 88rpx;
padding: 0 36rpx;
background: linear-gradient(135deg, $color-accent 0%, #059669 100%);
border-radius: $radius-full;
display: flex;
align-items: center;
gap: 10rpx;
box-shadow: $shadow-accent;
z-index: 99;
.create-icon {
font-size: 36rpx;
color: #FFFFFF;
font-weight: bold;
}
.create-text {
font-size: 28rpx;
font-weight: 600;
color: #FFFFFF;
}
}
</style>

查看文件

@@ -0,0 +1,353 @@
<template>
<view class="ticket-container">
<!-- 电子凭证主卡片 -->
<view class="ticket-card">
<!-- 头部:活动基本信息 -->
<view class="ticket-header">
<view class="ticket-badge">活动签到凭证</view>
<view class="activity-title">{{ activityTitle }}</view>
<view class="activity-time">⏰ 活动时间:2026-09-20 20:00</view>
</view>
<!-- 撕裂齿线分割 -->
<view class="ticket-tear">
<view class="circle-notch left"></view>
<view class="dashed-line"></view>
<view class="circle-notch right"></view>
</view>
<!-- 核心核销区:4 位纯数字大字插槽 -->
<view class="verify-section">
<view class="section-hint">向活动发起人出示以下 4 位核销码</view>
<view class="code-slots">
<view class="slot-box" v-for="(digit, index) in codeDigits" :key="index">
<text class="digit-text">{{ digit }}</text>
</view>
</view>
<!-- 模拟条形码与二维码 -->
<view class="barcode-wrap">
<view class="barcode-bars">
<view class="bar" v-for="n in 36" :key="n" :style="{ width: (n % 3 === 0 ? 6 : 3) + 'rpx' }"></view>
</view>
<view class="barcode-num">TICKET-2026-{{ verifyCode }}</view>
</view>
<!-- 动态防伪时间戳 -->
<view class="security-timestamp">
<view class="radar-dot"></view>
<text class="time-text">动态防伪已激活:{{ currentTime }} ({{ refreshCountdown }}s刷新)</text>
</view>
<!-- 核销状态标签 -->
<view class="status-stamp" :class="isCheckedIn ? 'stamp-verified' : 'stamp-pending'">
{{ isCheckedIn ? '已核销签到' : '待核销使用' }}
</view>
</view>
</view>
<!-- 底部使用须知 -->
<view class="guide-card">
<view class="guide-title">核销使用说明</view>
<view class="guide-item">1. 到达活动现场或进入专属开黑连麦频道后,请向发起人提供此 4 位数字核销码。</view>
<view class="guide-item">2. 发起人输入核销码确认后即视为签到成功,平摊款项将在活动结束后自动清算。</view>
<view class="guide-item">3. 核销码仅可核销一次,请勿向第三方泄露,谨防被他人误核销。</view>
</view>
<!-- 模拟操作按钮 (供快速测试核销状态) -->
<view class="test-action-bar">
<button class="test-toggle-btn btn-active" @click="toggleCheckinStatus">
{{ isCheckedIn ? '重置为待核销状态' : '模拟已被发起人核销' }}
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
const activityTitle = ref('【AA制五排】今晚8点王者百星车队冲分');
const verifyCode = ref('8392');
const isCheckedIn = ref(false);
const currentTime = ref('');
const refreshCountdown = ref(30);
let timer: any = null;
const codeDigits = computed(() => {
return verifyCode.value.split('');
});
const updateTime = () => {
const now = new Date();
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
currentTime.value = `${h}:${m}:${s}`;
};
onLoad((options) => {
if (options?.code) {
verifyCode.value = options.code;
}
});
onMounted(() => {
updateTime();
timer = setInterval(() => {
updateTime();
refreshCountdown.value--;
if (refreshCountdown.value <= 0) {
refreshCountdown.value = 30;
}
}, 1000);
});
onUnmounted(() => {
if (timer) clearInterval(timer);
});
const toggleCheckinStatus = () => {
isCheckedIn.value = !isCheckedIn.value;
uni.showToast({
title: isCheckedIn.value ? '已模拟核销完成' : '已重置为待核销',
icon: 'none'
});
};
</script>
<style lang="scss" scoped>
@import '../../uni.scss';
.ticket-container {
min-height: 100vh;
padding: 32rpx 32rpx 60rpx;
background-color: $bg-primary;
display: flex;
flex-direction: column;
gap: 32rpx;
}
/* 门票主卡片 */
.ticket-card {
background: linear-gradient(180deg, #1A233A 0%, #151D30 100%);
border: 1rpx solid $border-subtle;
border-radius: $radius-xl;
box-shadow: $shadow-card;
position: relative;
overflow: hidden;
.ticket-header {
padding: 36rpx 32rpx 28rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
.ticket-badge {
align-self: flex-start;
padding: 6rpx 18rpx;
background: rgba(99, 102, 241, 0.2);
border: 1rpx solid rgba(99, 102, 241, 0.4);
border-radius: $radius-full;
font-size: 22rpx;
color: $color-brand-light;
font-weight: 600;
}
.activity-title {
font-size: 34rpx;
font-weight: bold;
color: $text-primary;
line-height: 1.4;
}
.activity-time {
font-size: 24rpx;
color: $text-secondary;
}
}
/* 门票打孔虚线 */
.ticket-tear {
position: relative;
height: 40rpx;
display: flex;
align-items: center;
.circle-notch {
position: absolute;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background-color: $bg-primary;
&.left { left: -20rpx; }
&.right { right: -20rpx; }
}
.dashed-line {
width: 100%;
height: 2rpx;
margin: 0 32rpx;
border-top: 2rpx dashed $border-subtle;
}
}
/* 核心核销区 */
.verify-section {
padding: 32rpx 32rpx 48rpx;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.section-hint {
font-size: 24rpx;
color: $text-secondary;
margin-bottom: 28rpx;
}
/* 4 个独立大字插槽 */
.code-slots {
display: flex;
gap: 20rpx;
margin-bottom: 36rpx;
.slot-box {
width: 120rpx;
height: 140rpx;
background-color: #0E1626;
border: 2rpx solid $color-accent;
border-radius: $radius-lg;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 20rpx $color-accent-glow;
.digit-text {
font-size: 72rpx;
font-weight: 900;
color: $color-accent;
font-family: monospace, 'Courier New';
}
}
}
/* 条形码 */
.barcode-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
margin-bottom: 28rpx;
.barcode-bars {
display: flex;
align-items: center;
height: 70rpx;
gap: 6rpx;
.bar {
height: 100%;
background-color: $text-primary;
border-radius: 2rpx;
}
}
.barcode-num {
font-size: 20rpx;
color: $text-secondary;
font-family: monospace;
letter-spacing: 2rpx;
}
}
/* 防伪时间戳 */
.security-timestamp {
display: flex;
align-items: center;
gap: 10rpx;
padding: 8rpx 20rpx;
background-color: rgba(16, 185, 129, 0.1);
border-radius: $radius-full;
.radar-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: $color-accent;
box-shadow: 0 0 10rpx $color-accent;
}
.time-text {
font-size: 20rpx;
color: $color-accent;
}
}
/* 状态印章水印 */
.status-stamp {
position: absolute;
top: 24rpx;
right: 24rpx;
padding: 8rpx 20rpx;
border-radius: $radius-sm;
font-size: 22rpx;
font-weight: bold;
border: 2rpx solid;
transform: rotate(-12deg);
&.stamp-pending {
color: $color-accent;
border-color: $color-accent;
background-color: rgba(16, 185, 129, 0.1);
}
&.stamp-verified {
color: $text-muted;
border-color: $text-muted;
background-color: rgba(100, 116, 139, 0.1);
}
}
}
}
/* 使用指南卡片 */
.guide-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 28rpx;
display: flex;
flex-direction: column;
gap: 14rpx;
.guide-title {
font-size: 26rpx;
font-weight: 600;
color: $text-primary;
margin-bottom: 4rpx;
}
.guide-item {
font-size: 22rpx;
color: $text-secondary;
line-height: 1.5;
}
}
.test-action-bar {
.test-toggle-btn {
height: 76rpx;
background-color: $bg-surface-elevated;
border: 1rpx solid $border-subtle;
border-radius: $radius-full;
font-size: 24rpx;
color: $color-brand-light;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

查看文件

@@ -0,0 +1,516 @@
<template>
<view class="expert-detail-container" v-if="expert">
<!-- 头部背景与大图 -->
<view class="hero-header">
<image class="cover-image" :src="expert.coverImage" mode="aspectFill" />
<view class="cover-mask"></view>
<view class="profile-summary">
<view class="avatar-wrap">
<image class="avatar" :src="expert.avatar" mode="aspectFill" />
<view class="online-indicator" v-if="expert.isOnline">在线</view>
</view>
<view class="info">
<view class="name-row">
<text class="nickname">{{ expert.nickname }}</text>
<text class="level-badge">{{ expert.levelTag }}</text>
</view>
<view class="game-rank">{{ expert.game }} · {{ expert.rankTag }}</view>
<view class="stats-row">
<text class="stat-item">⭐ {{ expert.rating }}分</text>
<text class="dot">·</text>
<text class="stat-item">已接 {{ expert.orderCount }} 单</text>
</view>
</view>
</view>
</view>
<!-- 语音自我介绍试听卡片 -->
<view class="voice-card">
<view class="voice-info">
<text class="voice-title">专属语音打招呼</text>
<text class="voice-time">{{ expert.voiceDuration }} 秒</text>
</view>
<view class="voice-player btn-active" @click="toggleVoice">
<view class="play-icon">{{ isPlayingVoice ? '⏸' : '▶' }}</view>
<view class="wave-bars" :class="{ playing: isPlayingVoice }">
<view class="wb" v-for="n in 16" :key="n" :style="{ height: (8 + (n * 3) % 24) + 'rpx' }"></view>
</view>
</view>
</view>
<!-- 个人介绍 -->
<view class="section-card">
<view class="section-title">大神简介</view>
<view class="bio-text">{{ expert.bio }}</view>
</view>
<!-- 服务项目列表 (选择服务包) -->
<view class="section-card">
<view class="section-title">选择陪玩服务</view>
<view class="service-list">
<view
v-for="srv in expert.services"
:key="srv.id"
class="service-item btn-active"
:class="{ selected: selectedServiceId === srv.id }"
@click="selectedServiceId = srv.id"
>
<view class="service-meta">
<view class="service-title">{{ srv.title }}</view>
<view class="service-desc">{{ srv.desc }}</view>
</view>
<view class="service-price">
<text class="currency">¥</text>
<text class="amount">{{ srv.price }}</text>
<text class="unit">/{{ srv.unit }}</text>
</view>
</view>
</view>
</view>
<!-- 高光战绩与写真相册 -->
<view class="section-card">
<view class="section-title">高光战绩与风采</view>
<scroll-view class="photo-scroll" scroll-x show-scrollbar="false">
<view class="photo-list">
<image
v-for="(img, idx) in expert.photos"
:key="idx"
class="photo-item"
:src="img"
mode="aspectFill"
@click="previewImage(img)"
/>
</view>
</scroll-view>
</view>
<!-- 买家评价列表 -->
<view class="section-card">
<view class="header-split">
<view class="section-title">玩家评价 ({{ expert.reviews.length }})</view>
<view class="rating-highlight">综合好评 99%</view>
</view>
<view class="review-list" v-if="expert.reviews.length > 0">
<view v-for="rev in expert.reviews" :key="rev.id" class="review-item">
<view class="review-user">
<image class="rev-avatar" :src="rev.userAvatar" mode="aspectFill" />
<view class="rev-meta">
<text class="rev-name">{{ rev.userNickname }}</text>
<text class="rev-time">{{ rev.time }}</text>
</view>
<view class="rev-stars">★★★★★</view>
</view>
<view class="tag-row">
<text v-for="(t, i) in rev.tags" :key="i" class="tag-pill">{{ t }}</text>
</view>
<view class="rev-content">{{ rev.content }}</view>
</view>
</view>
<view class="empty-review" v-else>
暂无评价,快来抢先体验并发表评价吧~
</view>
</view>
<!-- 底部预约下单结算固定操作条 -->
<view class="bottom-action-bar">
<view class="price-display">
<text class="label">当前服务:</text>
<text class="currency">¥</text>
<text class="amount">{{ currentSelectedPrice }}</text>
</view>
<button class="order-btn btn-active" @click="handleGoOrder">
立即预约下单
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { EXPERTS_LIST, ExpertItem } from '../../mock/data';
const expert = ref<ExpertItem | null>(null);
const selectedServiceId = ref('');
const isPlayingVoice = ref(false);
onLoad((options) => {
const expId = options?.id || 'exp_101';
expert.value = EXPERTS_LIST.find((e) => e.id === expId) || EXPERTS_LIST[0];
if (expert.value?.services?.length > 0) {
selectedServiceId.value = expert.value.services[0].id;
}
});
const currentSelectedPrice = computed(() => {
if (!expert.value) return 0;
const srv = expert.value.services.find((s) => s.id === selectedServiceId.value);
return srv ? srv.price : expert.value.price;
});
const toggleVoice = () => {
isPlayingVoice.value = !isPlayingVoice.value;
uni.showToast({
title: isPlayingVoice.value ? '播放大神语音介绍' : '已暂停',
icon: 'none'
});
};
const previewImage = (current: string) => {
if (expert.value?.photos) {
uni.previewImage({
urls: expert.value.photos,
current
});
}
};
const handleGoOrder = () => {
if (!selectedServiceId.value) {
uni.showToast({ title: '请选择陪玩服务包', icon: 'none' });
return;
}
uni.navigateTo({
url: `/pages/order/create?expertId=${expert.value?.id}&serviceId=${selectedServiceId.value}`
});
};
</script>
<style lang="scss" scoped>
@import '../../uni.scss';
.expert-detail-container {
min-height: 100vh;
padding-bottom: 160rpx;
background-color: $bg-primary;
}
.hero-header {
position: relative;
height: 420rpx;
.cover-image {
width: 100%;
height: 100%;
}
.cover-mask {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(10, 15, 29, 0.2) 0%, rgba(10, 15, 29, 0.95) 100%);
}
.profile-summary {
position: absolute;
bottom: 24rpx;
left: 28rpx;
right: 28rpx;
display: flex;
align-items: flex-end;
gap: 20rpx;
.avatar-wrap {
position: relative;
width: 128rpx;
height: 128rpx;
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
border: 4rpx solid $color-brand;
}
.online-indicator {
position: absolute;
bottom: 0;
right: 0;
padding: 2rpx 10rpx;
background-color: $color-accent;
border-radius: $radius-full;
font-size: 18rpx;
color: #000000;
font-weight: bold;
}
}
.info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
.name-row {
display: flex;
align-items: center;
gap: 12rpx;
.nickname {
font-size: 36rpx;
font-weight: bold;
color: #FFFFFF;
}
.level-badge {
padding: 4rpx 12rpx;
background: linear-gradient(135deg, $color-warning 0%, #D97706 100%);
border-radius: $radius-sm;
font-size: 18rpx;
font-weight: bold;
color: #000000;
}
}
.game-rank {
font-size: 24rpx;
color: $color-brand-light;
}
.stats-row {
font-size: 22rpx;
color: $text-secondary;
display: flex;
align-items: center;
gap: 8rpx;
}
}
}
}
.voice-card {
margin: 24rpx;
padding: 20rpx 28rpx;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%);
border: 1rpx solid rgba(99, 102, 241, 0.3);
border-radius: $radius-lg;
display: flex;
align-items: center;
justify-content: space-between;
.voice-info {
display: flex;
flex-direction: column;
.voice-title { font-size: 26rpx; color: #FFFFFF; font-weight: 500; }
.voice-time { font-size: 20rpx; color: $color-brand-light; margin-top: 4rpx; }
}
.voice-player {
display: flex;
align-items: center;
gap: 16rpx;
background-color: $bg-surface;
padding: 12rpx 24rpx;
border-radius: $radius-full;
.play-icon {
font-size: 24rpx;
color: $color-accent;
}
.wave-bars {
display: flex;
align-items: center;
gap: 4rpx;
height: 32rpx;
.wb {
width: 4rpx;
background-color: $color-accent;
border-radius: 2rpx;
}
&.playing .wb {
animation: soundWave 0.6s infinite alternate ease-in-out;
}
}
}
}
.section-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 24rpx;
margin: 0 24rpx 24rpx;
.section-title {
font-size: 28rpx;
font-weight: 600;
color: $text-primary;
margin-bottom: 16rpx;
}
.header-split {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
.section-title { margin-bottom: 0; }
.rating-highlight { font-size: 22rpx; color: $color-warning; }
}
.bio-text {
font-size: 26rpx;
color: $text-secondary;
line-height: 1.6;
}
}
.service-list {
display: flex;
flex-direction: column;
gap: 16rpx;
.service-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background-color: $bg-surface-elevated;
border: 2rpx solid transparent;
border-radius: $radius-md;
&.selected {
border-color: $color-brand;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
}
.service-meta {
display: flex;
flex-direction: column;
gap: 4rpx;
.service-title { font-size: 26rpx; font-weight: 500; color: $text-primary; }
.service-desc { font-size: 20rpx; color: $text-secondary; }
}
.service-price {
display: flex;
align-items: baseline;
.currency { font-size: 22rpx; color: $color-danger; font-weight: bold; }
.amount { font-size: 36rpx; color: $color-danger; font-weight: bold; }
.unit { font-size: 20rpx; color: $text-secondary; }
}
}
}
.photo-scroll {
width: 100%;
white-space: nowrap;
.photo-list {
display: inline-flex;
gap: 16rpx;
.photo-item {
width: 220rpx;
height: 160rpx;
border-radius: $radius-md;
}
}
}
.review-list {
display: flex;
flex-direction: column;
gap: 20rpx;
.review-item {
border-bottom: 1rpx solid rgba(30, 41, 59, 0.5);
padding-bottom: 16rpx;
.review-user {
display: flex;
align-items: center;
gap: 12rpx;
.rev-avatar {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
}
.rev-meta {
flex: 1;
.rev-name { font-size: 24rpx; color: $text-primary; font-weight: 500; }
.rev-time { font-size: 18rpx; color: $text-muted; margin-left: 12rpx; }
}
.rev-stars {
color: $color-warning;
font-size: 20rpx;
}
}
.tag-row {
display: flex;
gap: 10rpx;
margin: 10rpx 0;
.tag-pill {
padding: 2rpx 10rpx;
background-color: rgba(99, 102, 241, 0.15);
color: $color-brand-light;
font-size: 18rpx;
border-radius: $radius-sm;
}
}
.rev-content {
font-size: 24rpx;
color: $text-secondary;
line-height: 1.4;
}
}
}
.empty-review {
font-size: 24rpx;
color: $text-muted;
text-align: center;
padding: 24rpx 0;
}
.bottom-action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 120rpx;
padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
background-color: $bg-surface;
border-top: 1rpx solid $border-subtle;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 100;
.price-display {
display: flex;
align-items: baseline;
.label { font-size: 24rpx; color: $text-secondary; }
.currency { font-size: 26rpx; color: $color-danger; font-weight: bold; }
.amount { font-size: 40rpx; color: $color-danger; font-weight: bold; }
}
.order-btn {
height: 80rpx;
padding: 0 48rpx;
background: linear-gradient(135deg, $color-brand 0%, $color-brand-purple 100%);
border-radius: $radius-full;
font-size: 28rpx;
font-weight: 600;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-brand;
}
}
@keyframes soundWave {
0% { transform: scaleY(0.4); }
100% { transform: scaleY(1.5); }
}
</style>

查看文件

@@ -0,0 +1,659 @@
<template>
<view class="index-container">
<!-- 顶部搜索栏与即时匹配入口 -->
<view class="header-section">
<view class="search-bar">
<text class="search-icon">🔍</text>
<input
class="search-input"
placeholder="搜索大神ID / 昵称 / 游戏"
placeholder-style="color: #64748B;"
v-model="searchKey"
/>
<text v-if="searchKey" class="clear-btn" @click="searchKey = ''">×</text>
</view>
<view class="instant-match-btn btn-active" @click="handleInstantMatch">
<text class="instant-text">即时派单</text>
</view>
</view>
<!-- 轮播图 Banner -->
<swiper class="banner-swiper" circular autoplay indicator-dots indicator-active-color="#6366F1">
<swiper-item v-for="item in BANNERS" :key="item.id">
<view class="banner-item" :style="{ background: item.bg }">
<view class="banner-tag">{{ item.tag }}</view>
<view class="banner-title">{{ item.title }}</view>
</view>
</swiper-item>
</swiper>
<!-- 游戏分类横向滚动胶囊 -->
<scroll-view class="game-categories" scroll-x show-scrollbar="false">
<view class="category-list">
<view
v-for="game in GAMES_LIST"
:key="game.id"
class="category-capsule btn-active"
:class="{ active: selectedGameId === game.id }"
@click="selectGame(game.id)"
>
<text class="category-icon">{{ game.icon }}</text>
<text class="category-name">{{ game.name }}</text>
<view v-if="game.hot" class="hot-badge">HOT</view>
</view>
</view>
</scroll-view>
<!-- 筛选与排序工具栏 -->
<view class="filter-toolbar">
<view class="sort-tabs">
<view
class="sort-item"
:class="{ active: currentSort === 'recommend' }"
@click="currentSort = 'recommend'"
>
综合推荐
</view>
<view
class="sort-item"
:class="{ active: currentSort === 'rating' }"
@click="currentSort = 'rating'"
>
高评分
</view>
<view
class="sort-item"
:class="{ active: currentSort === 'price' }"
@click="togglePriceSort"
>
价格 {{ priceSortDirection === 'asc' ? '↑' : priceSortDirection === 'desc' ? '↓' : '' }}
</view>
</view>
<view
class="online-filter btn-active"
:class="{ active: onlyOnline }"
@click="onlyOnline = !onlyOnline"
>
<view class="dot" :class="{ 'dot-online': onlyOnline }"></view>
<text>仅看在线</text>
</view>
</view>
<!-- 陪玩大神卡片瀑布流 -->
<view class="expert-grid">
<view
v-for="expert in filteredExperts"
:key="expert.id"
class="expert-card card-active"
@click="navToExpertDetail(expert.id)"
>
<!-- 头像与在线呼吸灯 -->
<view class="avatar-wrap">
<image class="avatar-img" :src="expert.avatar" mode="aspectFill" />
<view class="online-pulse" v-if="expert.isOnline">
<view class="pulse-ring"></view>
<view class="pulse-core"></view>
</view>
<view class="offline-tag" v-else>离线</view>
<view class="level-badge">{{ expert.levelTag }}</view>
</view>
<!-- 信息区 -->
<view class="info-wrap">
<view class="name-row">
<text class="nickname">{{ expert.nickname }}</text>
<text class="gender-tag" :class="expert.gender">{{ expert.gender === 'female' ? '♀' : '♂' }} {{ expert.age }}</text>
</view>
<view class="rank-tag">{{ expert.rankTag }}</view>
<!-- 语音试听条 -->
<view class="voice-bar btn-active" @click.stop="togglePlayVoice(expert.id)">
<view class="voice-wave" :class="{ playing: playingVoiceId === expert.id }">
<view class="bar b1"></view>
<view class="bar b2"></view>
<view class="bar b3"></view>
</view>
<text class="voice-time">{{ expert.voiceDuration }}" 听声音</text>
</view>
<!-- 价格与评分 -->
<view class="price-row">
<view class="price-box">
<text class="currency">¥</text>
<text class="amount">{{ expert.price }}</text>
<text class="unit">/小时</text>
</view>
<view class="rating-box">
<text class="star">★</text>
<text class="score">{{ expert.rating }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { GAMES_LIST, BANNERS, EXPERTS_LIST } from '../../mock/data';
const searchKey = ref('');
const selectedGameId = ref('all');
const currentSort = ref('recommend');
const priceSortDirection = ref<'none' | 'asc' | 'desc'>('none');
const onlyOnline = ref(false);
const playingVoiceId = ref<string | null>(null);
const selectGame = (gameId: string) => {
selectedGameId.value = gameId;
};
const togglePriceSort = () => {
currentSort.value = 'price';
if (priceSortDirection.value === 'none') {
priceSortDirection.value = 'asc';
} else if (priceSortDirection.value === 'asc') {
priceSortDirection.value = 'desc';
} else {
priceSortDirection.value = 'none';
currentSort.value = 'recommend';
}
};
const togglePlayVoice = (id: string) => {
if (playingVoiceId.value === id) {
playingVoiceId.value = null;
} else {
playingVoiceId.value = id;
uni.showToast({ title: '播放专属打招呼语音', icon: 'none' });
}
};
const handleInstantMatch = () => {
uni.showModal({
title: '即时派单匹配',
content: '系统正根据您当前游戏偏好,为您广播给最近的在线金牌大神!',
confirmText: '立即寻找',
cancelText: '取消',
confirmColor: '#6366F1',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '匹配大神中...' });
setTimeout(() => {
uni.hideLoading();
uni.navigateTo({ url: `/pages/expert/detail?id=exp_101` });
}, 800);
}
}
});
};
const navToExpertDetail = (id: string) => {
uni.navigateTo({ url: `/pages/expert/detail?id=${id}` });
};
const filteredExperts = computed(() => {
let list = [...EXPERTS_LIST];
// 1. 游戏分类筛选
if (selectedGameId.value !== 'all') {
const targetGame = GAMES_LIST.find((g) => g.id === selectedGameId.value)?.name;
if (targetGame) {
list = list.filter((e) => e.game.includes(targetGame));
}
}
// 2. 在线筛选
if (onlyOnline.value) {
list = list.filter((e) => e.isOnline);
}
// 3. 搜索关键词
if (searchKey.value.trim()) {
const kw = searchKey.value.trim().toLowerCase();
list = list.filter((e) => e.nickname.toLowerCase().includes(kw) || e.game.includes(kw));
}
// 4. 排序
if (currentSort.value === 'rating') {
list.sort((a, b) => b.rating - a.rating);
} else if (currentSort.value === 'price') {
if (priceSortDirection.value === 'asc') {
list.sort((a, b) => a.price - b.price);
} else if (priceSortDirection.value === 'desc') {
list.sort((a, b) => b.price - a.price);
}
}
return list;
});
</script>
<style lang="scss" scoped>
@import '../../uni.scss';
.index-container {
min-height: 100vh;
padding: 24rpx 24rpx 48rpx;
background-color: $bg-primary;
}
/* 顶部搜索栏 */
.header-section {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 24rpx;
.search-bar {
flex: 1;
height: 76rpx;
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-full;
display: flex;
align-items: center;
padding: 0 24rpx;
gap: 12rpx;
.search-icon {
font-size: 28rpx;
}
.search-input {
flex: 1;
font-size: 26rpx;
color: $text-primary;
}
.clear-btn {
font-size: 32rpx;
color: $text-secondary;
padding: 0 8rpx;
}
}
.instant-match-btn {
height: 76rpx;
padding: 0 28rpx;
background: linear-gradient(135deg, $color-brand 0%, $color-brand-purple 100%);
border-radius: $radius-full;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-brand;
.instant-text {
font-size: 26rpx;
font-weight: 600;
color: #FFFFFF;
}
}
}
/* 轮播 Banner */
.banner-swiper {
height: 180rpx;
border-radius: $radius-lg;
overflow: hidden;
margin-bottom: 24rpx;
.banner-item {
height: 100%;
padding: 24rpx 32rpx;
display: flex;
flex-direction: column;
justify-content: center;
border-radius: $radius-lg;
.banner-tag {
align-self: flex-start;
padding: 4rpx 14rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: $radius-sm;
font-size: 20rpx;
color: #FFFFFF;
margin-bottom: 12rpx;
}
.banner-title {
font-size: 32rpx;
font-weight: bold;
color: #FFFFFF;
}
}
}
/* 游戏分类滑动栏 */
.game-categories {
width: 100%;
white-space: nowrap;
margin-bottom: 24rpx;
.category-list {
display: inline-flex;
gap: 16rpx;
.category-capsule {
display: inline-flex;
align-items: center;
gap: 10rpx;
padding: 14rpx 28rpx;
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-full;
position: relative;
.category-icon {
font-size: 28rpx;
}
.category-name {
font-size: 26rpx;
color: $text-secondary;
}
.hot-badge {
position: absolute;
top: -8rpx;
right: -8rpx;
background-color: $color-danger;
color: #FFFFFF;
font-size: 16rpx;
font-weight: bold;
padding: 2rpx 8rpx;
border-radius: $radius-full;
}
&.active {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
border-color: $color-brand;
.category-name {
color: #FFFFFF;
font-weight: 600;
}
}
}
}
}
/* 筛选与排序 */
.filter-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
.sort-tabs {
display: flex;
gap: 32rpx;
.sort-item {
font-size: 26rpx;
color: $text-secondary;
position: relative;
padding-bottom: 8rpx;
&.active {
color: $text-primary;
font-weight: 600;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 32rpx;
height: 4rpx;
background-color: $color-brand;
border-radius: 2rpx;
}
}
}
}
.online-filter {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
background-color: $bg-surface;
border-radius: $radius-full;
font-size: 22rpx;
color: $text-secondary;
border: 1rpx solid $border-subtle;
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: $text-muted;
&.dot-online {
background-color: $color-accent;
box-shadow: 0 0 8rpx $color-accent;
}
}
&.active {
color: $color-accent;
border-color: rgba(16, 185, 129, 0.3);
}
}
}
/* 瀑布流大神卡片 */
.expert-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.expert-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
overflow: hidden;
display: flex;
flex-direction: column;
.avatar-wrap {
width: 100%;
height: 320rpx;
position: relative;
.avatar-img {
width: 100%;
height: 100%;
}
.online-pulse {
position: absolute;
top: 16rpx;
left: 16rpx;
width: 24rpx;
height: 24rpx;
display: flex;
align-items: center;
justify-content: center;
.pulse-core {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background-color: $color-accent;
}
.pulse-ring {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
border: 2rpx solid $color-accent;
animation: pulse 2s infinite;
}
}
.offline-tag {
position: absolute;
top: 16rpx;
left: 16rpx;
padding: 2rpx 10rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: $radius-sm;
font-size: 18rpx;
color: $text-secondary;
}
.level-badge {
position: absolute;
bottom: 12rpx;
right: 12rpx;
padding: 4rpx 12rpx;
background: linear-gradient(135deg, $color-warning 0%, #D97706 100%);
border-radius: $radius-sm;
font-size: 18rpx;
font-weight: bold;
color: #000000;
}
}
.info-wrap {
padding: 16rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
.name-row {
display: flex;
align-items: center;
justify-content: space-between;
.nickname {
font-size: 28rpx;
font-weight: 600;
color: $text-primary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 180rpx;
}
.gender-tag {
font-size: 18rpx;
padding: 2rpx 8rpx;
border-radius: $radius-sm;
&.female {
background-color: rgba(236, 72, 153, 0.2);
color: #F472B6;
}
&.male {
background-color: rgba(59, 130, 246, 0.2);
color: #60A5FA;
}
}
}
.rank-tag {
font-size: 20rpx;
color: $color-brand-light;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.voice-bar {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 14rpx;
background-color: $bg-surface-elevated;
border-radius: $radius-full;
width: fit-content;
.voice-wave {
display: flex;
align-items: center;
gap: 3rpx;
height: 18rpx;
.bar {
width: 4rpx;
background-color: $color-accent;
border-radius: 2rpx;
&.b1 { height: 10rpx; }
&.b2 { height: 18rpx; }
&.b3 { height: 14rpx; }
}
&.playing .bar {
animation: soundWave 0.8s infinite alternate ease-in-out;
}
}
.voice-time {
font-size: 18rpx;
color: $color-accent;
}
}
.price-row {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-top: 6rpx;
.price-box {
.currency {
font-size: 22rpx;
font-weight: bold;
color: $color-danger;
}
.amount {
font-size: 32rpx;
font-weight: bold;
color: $color-danger;
}
.unit {
font-size: 20rpx;
color: $text-secondary;
}
}
.rating-box {
display: flex;
align-items: center;
gap: 4rpx;
.star {
color: $color-warning;
font-size: 20rpx;
}
.score {
font-size: 22rpx;
color: $text-secondary;
}
}
}
}
}
}
@keyframes pulse {
0% { transform: scale(0.9); opacity: 0.8; }
50% { transform: scale(1.6); opacity: 0; }
100% { transform: scale(0.9); opacity: 0; }
}
@keyframes soundWave {
0% { transform: scaleY(0.4); }
100% { transform: scaleY(1.4); }
}
</style>

查看文件

@@ -0,0 +1,405 @@
<template>
<view class="order-create-container">
<!-- 大神与服务包简要卡片 -->
<view class="section-card expert-card" v-if="expert">
<image class="avatar" :src="expert.avatar" mode="aspectFill" />
<view class="meta">
<view class="name-row">
<text class="name">{{ expert.nickname }}</text>
<text class="game">{{ expert.game }}</text>
</view>
<view class="service-name">{{ selectedService?.title || '陪玩服务' }}</view>
<view class="price-rate">¥{{ selectedService?.price || expert.price }}/{{ selectedService?.unit || '小时' }}</view>
</view>
</view>
<!-- 数量与时段配置 -->
<view class="section-card config-card">
<view class="config-row">
<text class="label">服务数量 ({{ selectedService?.unit || '小时' }})</text>
<view class="stepper">
<view class="step-btn btn-active" @click="changeQuantity(-1)">-</view>
<text class="step-val">{{ quantity }}</text>
<view class="step-btn btn-active" @click="changeQuantity(1)">+</view>
</view>
</view>
<view class="divider"></view>
<view class="config-row">
<text class="label">预约开始时间</text>
<view class="time-options">
<view
class="time-pill btn-active"
:class="{ active: timeType === 'NOW' }"
@click="timeType = 'NOW'"
>
即刻开始 (5分钟内)
</view>
<view
class="time-pill btn-active"
:class="{ active: timeType === 'LATER' }"
@click="timeType = 'LATER'"
>
预约今晚 21:00
</view>
</view>
</view>
</view>
<!-- 游戏信息与备注 -->
<view class="section-card form-card">
<view class="section-title">玩家游戏信息</view>
<view class="input-row">
<text class="input-label">游戏大区</text>
<input class="form-input" placeholder="例如:微信大区 / QQ大区" v-model="gameServer" />
</view>
<view class="input-row">
<text class="input-label">游戏角色名</text>
<input class="form-input" placeholder="输入游戏内ID方便拉进车队" v-model="gameId" />
</view>
<view class="input-row">
<text class="input-label">特殊打法要求</text>
<input class="form-input" placeholder="例如:主玩射手,需要硬辅贴身保护" v-model="gameNotes" />
</view>
</view>
<!-- 优惠券选择 -->
<view class="section-card coupon-card" @click="toggleCouponSelect">
<view class="coupon-left">
<text class="coupon-icon">🎟️</text>
<text class="coupon-label">平台优惠券</text>
</view>
<view class="coupon-right">
<text class="discount-text" v-if="appliedCoupon > 0">- ¥{{ appliedCoupon }} 元</text>
<text class="no-coupon" v-else>暂无可用券</text>
<text class="arrow">›</text>
</view>
</view>
<!-- 费用明细清单 -->
<view class="section-card fee-summary-card">
<view class="fee-row">
<text class="text">服务总计</text>
<text class="val">¥{{ rawTotal }}</text>
</view>
<view class="fee-row" v-if="appliedCoupon > 0">
<text class="text">优惠券抵扣</text>
<text class="val" style="color: #10B981;">- ¥{{ appliedCoupon }}</text>
</view>
<view class="divider"></view>
<view class="fee-row highlight">
<text class="text">实际支付金额</text>
<text class="val total">¥{{ finalPayAmount }}</text>
</view>
</view>
<!-- 底部支付操作栏 -->
<view class="bottom-pay-bar">
<view class="total-box">
<text class="label">实付:</text>
<text class="currency">¥</text>
<text class="price">{{ finalPayAmount }}</text>
</view>
<button class="pay-btn btn-active" @click="handleWechatPay">
微信安全支付
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { EXPERTS_LIST, ExpertItem } from '../../mock/data';
const expert = ref<ExpertItem | null>(null);
const selectedServiceId = ref('');
const quantity = ref(1);
const timeType = ref<'NOW' | 'LATER'>('NOW');
const gameServer = ref('微信大区');
const gameId = ref('超能战神007');
const gameNotes = ref('');
const appliedCoupon = ref(10); // 默认抵扣新人满减券 10 元
onLoad((options) => {
const expId = options?.expertId || 'exp_101';
selectedServiceId.value = options?.serviceId || 'srv_1';
expert.value = EXPERTS_LIST.find((e) => e.id === expId) || EXPERTS_LIST[0];
});
const selectedService = computed(() => {
return expert.value?.services.find((s) => s.id === selectedServiceId.value) || expert.value?.services[0];
});
const rawTotal = computed(() => {
const unitPrice = selectedService.value ? selectedService.value.price : 35;
return unitPrice * quantity.value;
});
const finalPayAmount = computed(() => {
return Math.max(0, rawTotal.value - appliedCoupon.value);
});
const changeQuantity = (delta: number) => {
const target = quantity.value + delta;
if (target >= 1 && target <= 10) {
quantity.value = target;
}
};
const toggleCouponSelect = () => {
uni.showActionSheet({
itemList: ['使用「新人首单满减券」-10元', '使用「周末冲分9折券」-5元', '不使用优惠券'],
success: (res) => {
if (res.tapIndex === 0) appliedCoupon.value = 10;
else if (res.tapIndex === 1) appliedCoupon.value = 5;
else appliedCoupon.value = 0;
}
});
};
const handleWechatPay = () => {
uni.showLoading({ title: '调起微信支付...' });
setTimeout(() => {
uni.hideLoading();
uni.showModal({
title: '支付成功',
content: `订单已创建并完成支付!大神接单后将第一时间通过微信服务通知提醒您。`,
showCancel: false,
confirmText: '查看我的订单',
confirmColor: '#6366F1',
success: () => {
uni.switchTab({ url: '/pages/order/list' });
}
});
}, 600);
};
</script>
<style lang="scss" scoped>
@import '../../uni.scss';
.order-create-container {
min-height: 100vh;
padding: 24rpx 24rpx 160rpx;
background-color: $bg-primary;
}
.section-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 24rpx;
margin-bottom: 24rpx;
}
.expert-card {
display: flex;
align-items: center;
gap: 20rpx;
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
}
.meta {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
.name-row {
display: flex;
align-items: center;
gap: 12rpx;
.name { font-size: 30rpx; font-weight: bold; color: $text-primary; }
.game { font-size: 22rpx; color: $color-brand-light; }
}
.service-name { font-size: 24rpx; color: $text-secondary; }
.price-rate { font-size: 24rpx; color: $color-danger; font-weight: bold; }
}
}
.config-card {
.config-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8rpx 0;
.label { font-size: 26rpx; color: $text-primary; }
.stepper {
display: flex;
align-items: center;
background-color: $bg-surface-elevated;
border-radius: $radius-md;
overflow: hidden;
.step-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: $color-brand-light;
}
.step-val {
width: 60rpx;
text-align: center;
font-size: 28rpx;
font-weight: bold;
color: #FFFFFF;
}
}
.time-options {
display: flex;
gap: 12rpx;
.time-pill {
padding: 10rpx 20rpx;
border-radius: $radius-full;
font-size: 22rpx;
color: $text-secondary;
background-color: $bg-surface-elevated;
border: 1rpx solid transparent;
&.active {
color: #FFFFFF;
border-color: $color-brand;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
font-weight: 500;
}
}
}
}
.divider {
height: 1rpx;
background-color: $border-subtle;
margin: 16rpx 0;
}
}
.form-card {
.section-title {
font-size: 28rpx;
font-weight: 600;
color: $text-primary;
margin-bottom: 20rpx;
}
.input-row {
margin-bottom: 16rpx;
.input-label {
font-size: 24rpx;
color: $text-secondary;
margin-bottom: 8rpx;
display: block;
}
.form-input {
height: 76rpx;
background-color: $bg-surface-elevated;
border: 1rpx solid $border-subtle;
border-radius: $radius-md;
padding: 0 20rpx;
font-size: 26rpx;
color: $text-primary;
}
}
}
.coupon-card {
display: flex;
align-items: center;
justify-content: space-between;
.coupon-left {
display: flex;
align-items: center;
gap: 12rpx;
.coupon-icon { font-size: 28rpx; }
.coupon-label { font-size: 26rpx; color: $text-primary; }
}
.coupon-right {
display: flex;
align-items: center;
gap: 8rpx;
.discount-text { font-size: 26rpx; font-weight: bold; color: $color-accent; }
.no-coupon { font-size: 24rpx; color: $text-muted; }
.arrow { font-size: 32rpx; color: $text-muted; }
}
}
.fee-summary-card {
display: flex;
flex-direction: column;
gap: 14rpx;
.fee-row {
display: flex;
justify-content: space-between;
font-size: 26rpx;
.text { color: $text-secondary; }
.val { color: $text-primary; }
&.highlight {
.text { font-weight: bold; color: #FFFFFF; }
.val.total { font-size: 36rpx; font-weight: bold; color: $color-danger; }
}
}
.divider {
height: 1rpx;
background-color: $border-subtle;
margin: 8rpx 0;
}
}
.bottom-pay-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 120rpx;
padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
background-color: $bg-surface;
border-top: 1rpx solid $border-subtle;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 100;
.total-box {
display: flex;
align-items: baseline;
.label { font-size: 24rpx; color: $text-secondary; }
.currency { font-size: 26rpx; color: $color-danger; font-weight: bold; }
.price { font-size: 44rpx; color: $color-danger; font-weight: bold; }
}
.pay-btn {
height: 80rpx;
padding: 0 48rpx;
background: linear-gradient(135deg, $color-brand 0%, $color-brand-purple 100%);
border-radius: $radius-full;
font-size: 28rpx;
font-weight: 600;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-brand;
}
}
</style>

查看文件

@@ -0,0 +1,327 @@
<template>
<view class="order-list-container">
<!-- 顶部状态切换 Tab -->
<view class="status-tabs">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item btn-active"
:class="{ active: currentTab === tab.key }"
@click="currentTab = tab.key"
>
{{ tab.label }}
</view>
</view>
<!-- 订单列表 -->
<view class="orders-wrap">
<view
v-for="order in filteredOrders"
:key="order.id"
class="order-card card-active"
>
<!-- 订单编号与状态 -->
<view class="card-header">
<text class="order-sn">单号:{{ order.orderSn }}</text>
<view class="status-pill" :class="order.status.toLowerCase()">
{{ getStatusText(order.status) }}
</view>
</view>
<!-- 大神与服务信息 -->
<view class="card-body">
<image class="expert-avatar" :src="order.expert.avatar" mode="aspectFill" />
<view class="detail">
<view class="name-row">
<text class="nickname">{{ order.expert.nickname }}</text>
<text class="game-tag">{{ order.expert.game }}</text>
</view>
<view class="service-title">{{ order.serviceTitle }}</view>
<view class="time-meta">下单时间:{{ order.createdAt }}</view>
</view>
<view class="price-col">
<view class="amount">¥{{ order.totalAmount }}</view>
<view class="qty">共 {{ order.quantity }} 小时/局</view>
</view>
</view>
<!-- 底部操作按钮组 (根据状态动态渲染) -->
<view class="card-actions">
<template v-if="order.status === 'PENDING_PAY'">
<button class="action-btn secondary btn-active" @click="handleCancel(order.id)">取消订单</button>
<button class="action-btn primary btn-active" @click="handlePay(order.id)">立即付款</button>
</template>
<template v-else-if="order.status === 'IN_SERVICE'">
<button class="action-btn secondary btn-active" @click="handleChat(order.expert.nickname)">联系大神</button>
<button class="action-btn danger btn-active" @click="handleRefund(order.id)">申请退款</button>
<button class="action-btn primary btn-active" @click="handleConfirm(order.id)">确认完成</button>
</template>
<template v-else-if="order.status === 'COMPLETED'">
<button class="action-btn secondary btn-active" @click="handleComplaint(order.id)">售后/申诉</button>
<button class="action-btn primary btn-active" @click="handleReview(order.id)">发表好评</button>
</template>
</view>
</view>
<view class="empty-state" v-if="filteredOrders.length === 0">
<view class="empty-icon">📂</view>
<view class="empty-text">暂无相关订单记录</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ORDERS_LIST, OrderItem } from '../../mock/data';
const tabs = [
{ key: 'ALL', label: '全部' },
{ key: 'IN_SERVICE', label: '进行中' },
{ key: 'COMPLETED', label: '已完成' },
{ key: 'PENDING_PAY', label: '待付款' },
];
const currentTab = ref('ALL');
const orders = ref<OrderItem[]>([...ORDERS_LIST]);
const filteredOrders = computed(() => {
if (currentTab.value === 'ALL') return orders.value;
return orders.value.filter((o) => o.status === currentTab.value);
});
const getStatusText = (status: string) => {
switch (status) {
case 'PENDING_PAY': return '待付款';
case 'PENDING_ACCEPT': return '待接单';
case 'IN_SERVICE': return '服务进行中';
case 'PENDING_CONFIRM': return '待买家确认';
case 'COMPLETED': return '已完成';
default: return '已取消';
}
};
const handlePay = (id: string) => {
uni.showLoading({ title: '拉起微信支付...' });
setTimeout(() => {
uni.hideLoading();
uni.showToast({ title: '支付成功,大神准备中', icon: 'success' });
const target = orders.value.find((o) => o.id === id);
if (target) target.status = 'IN_SERVICE';
}, 600);
};
const handleCancel = (id: string) => {
uni.showModal({
title: '确认取消订单',
content: '取消后不可恢复,是否确认?',
confirmColor: '#EF4444',
success: (res) => {
if (res.confirm) {
orders.value = orders.value.filter((o) => o.id !== id);
uni.showToast({ title: '订单已取消', icon: 'none' });
}
}
});
};
const handleChat = (nickname: string) => {
uni.showToast({ title: `进入与 [${nickname}] 的聊天室`, icon: 'none' });
};
const handleRefund = (id: string) => {
uni.showModal({
title: '申请平台介入退款',
content: '请简要说明退款理由(如大神迟到、挂机等),平台将在2小时内审核',
confirmColor: '#EF4444',
success: (res) => {
if (res.confirm) {
uni.showToast({ title: '退款申请已提交仲裁池', icon: 'success' });
}
}
});
};
const handleConfirm = (id: string) => {
uni.showModal({
title: '确认验收服务',
content: '确认大神已履约完成陪玩服务?款项将结算划入大神钱包。',
confirmColor: '#6366F1',
success: (res) => {
if (res.confirm) {
const target = orders.value.find((o) => o.id === id);
if (target) target.status = 'COMPLETED';
uni.showToast({ title: '服务确认完成', icon: 'success' });
}
}
});
};
const handleComplaint = (id: string) => {
uni.showToast({ title: '已进入售后申诉工单处理', icon: 'none' });
};
const handleReview = (id: string) => {
uni.showToast({ title: '感谢您的五星好评!', icon: 'success' });
};
</script>
<style lang="scss" scoped>
@import '../../uni.scss';
.order-list-container {
min-height: 100vh;
padding: 24rpx 24rpx 48rpx;
background-color: $bg-primary;
}
.status-tabs {
display: flex;
background-color: $bg-surface;
border-radius: $radius-lg;
padding: 8rpx;
margin-bottom: 24rpx;
border: 1rpx solid $border-subtle;
.tab-item {
flex: 1;
text-align: center;
padding: 18rpx 0;
font-size: 26rpx;
color: $text-secondary;
border-radius: $radius-md;
&.active {
background: linear-gradient(135deg, $color-brand 0%, $color-brand-purple 100%);
color: #FFFFFF;
font-weight: 600;
}
}
}
.orders-wrap {
display: flex;
flex-direction: column;
gap: 20rpx;
.order-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 24rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1rpx solid rgba(30, 41, 59, 0.5);
padding-bottom: 14rpx;
.order-sn {
font-size: 22rpx;
color: $text-muted;
}
.status-pill {
font-size: 20rpx;
font-weight: 600;
padding: 4rpx 14rpx;
border-radius: $radius-sm;
&.pending_pay { color: $color-warning; background-color: rgba(245, 158, 11, 0.15); }
&.in_service { color: $color-accent; background-color: rgba(16, 185, 129, 0.15); }
&.completed { color: $color-brand-light; background-color: rgba(99, 102, 241, 0.15); }
}
}
.card-body {
display: flex;
align-items: center;
gap: 20rpx;
.expert-avatar {
width: 90rpx;
height: 90rpx;
border-radius: 50%;
}
.detail {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
.name-row {
display: flex;
align-items: center;
gap: 12rpx;
.nickname { font-size: 28rpx; font-weight: 600; color: $text-primary; }
.game-tag { font-size: 20rpx; color: $color-brand-light; }
}
.service-title { font-size: 24rpx; color: $text-secondary; }
.time-meta { font-size: 20rpx; color: $text-muted; }
}
.price-col {
text-align: right;
.amount { font-size: 34rpx; font-weight: bold; color: $color-danger; }
.qty { font-size: 20rpx; color: $text-muted; }
}
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 16rpx;
padding-top: 10rpx;
.action-btn {
height: 64rpx;
padding: 0 28rpx;
font-size: 24rpx;
border-radius: $radius-full;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
&.primary {
background: linear-gradient(135deg, $color-brand 0%, $color-brand-purple 100%);
color: #FFFFFF;
font-weight: 600;
}
&.secondary {
background-color: $bg-surface-elevated;
color: $text-primary;
border: 1rpx solid $border-subtle;
}
&.danger {
background-color: rgba(239, 68, 68, 0.15);
color: $color-danger;
border: 1rpx solid rgba(239, 68, 68, 0.3);
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
gap: 16rpx;
.empty-icon { font-size: 64rpx; }
.empty-text { font-size: 26rpx; color: $text-muted; }
}
}
</style>

查看文件

@@ -0,0 +1,442 @@
<template>
<view class="user-container">
<!-- 用户基础信息卡片 -->
<view class="profile-card">
<image class="avatar" :src="user.avatar" mode="aspectFill" />
<view class="info">
<view class="name-row">
<text class="nickname">{{ user.nickname }}</text>
<text class="uid">ID: {{ user.id }}</text>
</view>
<view class="role-badge">当前身份:娱乐玩家</view>
</view>
<view class="edit-btn btn-active" @click="handleEditProfile">编辑资料</view>
</view>
<!-- 【核心重点】双身份切换流光卡片 -->
<view class="role-switch-card card-active" @click="handleSwitchToExpert">
<view class="switch-left">
<view class="switch-icon">⚡</view>
<view class="switch-text">
<view class="title">成为入驻大神 · 开启接单赚钱</view>
<view class="sub">实名认证通过后,即可一键切换至大神接单端</view>
</view>
</view>
<view class="switch-btn">
一键切换
<text class="arrow">›</text>
</view>
</view>
<!-- 资产钱包卡片 -->
<view class="wallet-card">
<view class="wallet-header">
<text class="title">我的钱包资产</text>
<text class="flow-link btn-active" @click="handleViewFlow">消费流水 ›</text>
</view>
<view class="balance-row">
<view class="balance-col">
<text class="label">可用余额 (元)</text>
<text class="val">¥{{ user.balance.toFixed(2) }}</text>
</view>
<view class="balance-col">
<text class="label">分销佣金 (元)</text>
<text class="val commission">¥{{ user.commission.toFixed(2) }}</text>
</view>
</view>
<view class="wallet-actions">
<button class="wallet-btn recharge btn-active" @click="handleRecharge">余额充值</button>
<button class="wallet-btn withdraw btn-active" @click="handleWithdraw">佣金提现</button>
</view>
</view>
<!-- 二级分销推广专区 -->
<view class="distribution-card">
<view class="dist-header">
<view class="title-box">
<text class="fire-icon">🔥</text>
<text class="title">邀请好友 · 赚二级佣金</text>
</view>
<text class="rate-tag">最高享 7% 永久返佣</text>
</view>
<view class="dist-stats">
<view class="stat-item">
<text class="num">{{ user.invitedCount }}</text>
<text class="desc">我的团队 (人)</text>
</view>
<view class="stat-item">
<text class="num">¥{{ user.commission.toFixed(2) }}</text>
<text class="desc">累计提现 (元)</text>
</view>
<view class="stat-item">
<text class="num">1 级</text>
<text class="desc">分销等级</text>
</view>
</view>
<button class="poster-btn btn-active" @click="handleGeneratePoster">
生成我的专属推广太阳码海报
</button>
</view>
<!-- 系统功能菜单列表 -->
<view class="menu-card">
<view class="menu-item btn-active" @click="handleMenu('我的收藏')">
<view class="item-left">
<text class="icon">⭐</text>
<text class="name">我收藏的大神</text>
</view>
<text class="arrow">›</text>
</view>
<view class="menu-item btn-active" @click="handleMenu('我的动态')">
<view class="item-left">
<text class="icon">📝</text>
<text class="name">我的战绩动态</text>
</view>
<text class="arrow">›</text>
</view>
<view class="menu-item btn-active" @click="handleMenu('优惠券中心')">
<view class="item-left">
<text class="icon">🎟️</text>
<text class="name">我的优惠券</text>
</view>
<text class="arrow">›</text>
</view>
<view class="menu-item btn-active" @click="handleMenu('隐私与通知设置')">
<view class="item-left">
<text class="icon">⚙️</text>
<text class="name">系统设置与隐私</text>
</view>
<text class="arrow">›</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { USER_PROFILE } from '../../mock/data';
const user = ref({ ...USER_PROFILE });
const handleEditProfile = () => {
uni.showToast({ title: '资料编辑暂未开放修改', icon: 'none' });
};
const handleSwitchToExpert = () => {
uni.showModal({
title: '切换至陪玩接单端',
content: '成为大神需要先完成实名认证与游戏段位核验,是否立即前往申请入驻?',
confirmText: '立即认证',
confirmColor: '#6366F1',
success: (res) => {
if (res.confirm) {
uni.showToast({ title: '已进入实名认证通道', icon: 'success' });
}
}
});
};
const handleViewFlow = () => {
uni.showToast({ title: '查看近期消费与充值账单', icon: 'none' });
};
const handleRecharge = () => {
uni.showActionSheet({
itemList: ['充值 50 元', '充值 100 元 (赠10元)', '充值 200 元 (赠30元)'],
success: (res) => {
const amounts = [50, 110, 230];
user.value.balance += amounts[res.tapIndex];
uni.showToast({ title: '充值成功', icon: 'success' });
}
});
};
const handleWithdraw = () => {
uni.showModal({
title: '申请佣金提现',
content: `当前可提现佣金为 ¥${user.value.commission} 元,平台将打款至您的微信零钱。`,
confirmText: '申请提现',
confirmColor: '#10B981',
success: (res) => {
if (res.confirm) {
uni.showToast({ title: '提现申请已提交,等待审核', icon: 'success' });
}
}
});
};
const handleGeneratePoster = () => {
uni.showLoading({ title: '正在合成太阳码海报...' });
setTimeout(() => {
uni.hideLoading();
uni.showToast({ title: '专属推广海报已保存至相册', icon: 'success' });
}, 700);
};
const handleMenu = (title: string) => {
uni.showToast({ title: `进入【${title}】`, icon: 'none' });
};
</script>
<style lang="scss" scoped>
@import '../../uni.scss';
.user-container {
min-height: 100vh;
padding: 24rpx 24rpx 60rpx;
background-color: $bg-primary;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.profile-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 28rpx;
display: flex;
align-items: center;
gap: 20rpx;
.avatar {
width: 110rpx;
height: 110rpx;
border-radius: 50%;
border: 2rpx solid $color-brand;
}
.info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.name-row {
display: flex;
align-items: baseline;
gap: 12rpx;
.nickname { font-size: 32rpx; font-weight: bold; color: $text-primary; }
.uid { font-size: 20rpx; color: $text-muted; }
}
.role-badge {
align-self: flex-start;
padding: 2rpx 12rpx;
background-color: rgba(99, 102, 241, 0.15);
border-radius: $radius-sm;
font-size: 20rpx;
color: $color-brand-light;
}
}
.edit-btn {
padding: 10rpx 24rpx;
background-color: $bg-surface-elevated;
border: 1rpx solid $border-subtle;
border-radius: $radius-full;
font-size: 22rpx;
color: $text-secondary;
}
}
/* 核心双身份切换卡片 */
.role-switch-card {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.25) 0%, rgba(16, 185, 129, 0.25) 100%);
border: 2rpx solid rgba(99, 102, 241, 0.4);
border-radius: $radius-lg;
padding: 24rpx 28rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: $shadow-card;
.switch-left {
display: flex;
align-items: center;
gap: 16rpx;
.switch-icon {
font-size: 40rpx;
}
.switch-text {
.title { font-size: 28rpx; font-weight: bold; color: #FFFFFF; }
.sub { font-size: 20rpx; color: $text-secondary; margin-top: 4rpx; }
}
}
.switch-btn {
display: flex;
align-items: center;
gap: 4rpx;
padding: 10rpx 20rpx;
background: linear-gradient(135deg, $color-brand 0%, $color-brand-purple 100%);
border-radius: $radius-full;
font-size: 22rpx;
font-weight: 600;
color: #FFFFFF;
.arrow { font-size: 26rpx; }
}
}
.wallet-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 28rpx;
.wallet-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.title { font-size: 28rpx; font-weight: 600; color: $text-primary; }
.flow-link { font-size: 22rpx; color: $text-secondary; }
}
.balance-row {
display: flex;
justify-content: space-around;
margin-bottom: 24rpx;
.balance-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
.label { font-size: 22rpx; color: $text-secondary; }
.val {
font-size: 44rpx;
font-weight: bold;
color: $text-primary;
&.commission { color: $color-accent; }
}
}
}
.wallet-actions {
display: flex;
gap: 20rpx;
.wallet-btn {
flex: 1;
height: 72rpx;
border-radius: $radius-full;
font-size: 26rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
&.recharge {
background: linear-gradient(135deg, $color-brand 0%, $color-brand-purple 100%);
color: #FFFFFF;
}
&.withdraw {
background-color: $bg-surface-elevated;
border: 1rpx solid $border-subtle;
color: $color-accent;
}
}
}
}
.distribution-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 28rpx;
.dist-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.title-box {
display: flex;
align-items: center;
gap: 8rpx;
.fire-icon { font-size: 28rpx; }
.title { font-size: 28rpx; font-weight: 600; color: $text-primary; }
}
.rate-tag {
font-size: 20rpx;
padding: 4rpx 14rpx;
background-color: rgba(245, 158, 11, 0.15);
color: $color-warning;
border-radius: $radius-full;
}
}
.dist-stats {
display: flex;
justify-content: space-around;
padding: 16rpx 0 24rpx;
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
.num { font-size: 32rpx; font-weight: bold; color: $color-brand-light; }
.desc { font-size: 20rpx; color: $text-secondary; }
}
}
.poster-btn {
height: 76rpx;
background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%);
border-radius: $radius-full;
font-size: 26rpx;
font-weight: 600;
color: #000000;
display: flex;
align-items: center;
justify-content: center;
}
}
.menu-card {
background-color: $bg-surface;
border: 1rpx solid $border-subtle;
border-radius: $radius-lg;
padding: 8rpx 24rpx;
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid rgba(30, 41, 59, 0.4);
&:last-child {
border-bottom: none;
}
.item-left {
display: flex;
align-items: center;
gap: 16rpx;
.icon { font-size: 28rpx; }
.name { font-size: 26rpx; color: $text-primary; }
}
.arrow {
font-size: 32rpx;
color: $text-muted;
}
}
}
</style>

39
client/src/uni.scss 普通文件
查看文件

@@ -0,0 +1,39 @@
/* ==========================================================================
Cyber-Esports Dark Elegance Design System Tokens (750rpx Standard)
========================================================================== */
// 1. 全局背景与表面色阶
$bg-primary: #0A0F1D; // 页面最底色(深邃夜空)
$bg-surface: #151D30; // 卡片/模块容器底色
$bg-surface-elevated: #1E2942; // 浮层/弹窗底色
$bg-surface-glass: rgba(21, 29, 48, 0.85); // 拟态磨砂玻璃
// 2. 品牌核心色与流光
$color-brand: #6366F1; // 核心电竞靛蓝
$color-brand-light: #818CF8; // 亮色高光
$color-brand-purple: #8B5CF6; // 紫罗兰流光
$color-brand-glow: rgba(99, 102, 241, 0.35);
// 3. 业务强调与状态色
$color-accent: #10B981; // 翠绿高光(活动项目、在线呼吸灯)
$color-accent-glow: rgba(16, 185, 129, 0.3);
$color-warning: #F59E0B; // 暖橙金(金牌段位、倒计时)
$color-danger: #EF4444; // 警示红(退款、仲裁)
// 4. 文本与排版无障碍色阶 (WCAG AA 4.5:1)
$text-primary: #F8FAFC; // 一级标题与主要文字 (纯白)
$text-secondary: #94A3B8; // 二级描述文字 (冷灰蓝)
$text-muted: #64748B; // 三级占位说明文字
$border-subtle: #1E293B; // 微弱分割线与描边
// 5. 圆角阶梯
$radius-sm: 8rpx;
$radius-md: 16rpx;
$radius-lg: 24rpx;
$radius-xl: 32rpx;
$radius-full: 9999rpx;
// 6. 统一阴影与电竞流光
$shadow-card: 0 8rpx 24rpx rgba(0, 0, 0, 0.4);
$shadow-brand: 0 8rpx 24rpx rgba(99, 102, 241, 0.35);
$shadow-accent: 0 8rpx 24rpx rgba(16, 185, 129, 0.3);

18
client/tsconfig.json 普通文件
查看文件

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": false,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"skipLibCheck": true,
"types": ["@dcloudio/types"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

10
client/vite.config.ts 普通文件
查看文件

@@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';
export default defineConfig({
plugins: [uni()],
server: {
port: 3000,
host: '0.0.0.0',
},
});

查看文件

@@ -1,6 +1,6 @@
# 游戏陪玩小程序 uni-app 静态 UI 架构规划与 UI/UX 约束规范文档
> **编制标准**:基于 `/ui-ux-pro-max` 移动端设计智能规范,面向 750rpx 移动端栅格系统,兼顾电竞潮酷视觉质感、WCAG AA 级色彩无障碍对比度与严苛的触控交互体验。
> **编制标准**:基于 `/ui-ux-pro-max` 符合中式审美 移动端设计智能规范,面向 750rpx 移动端栅格系统,兼顾电竞潮酷视觉质感、WCAG AA 级色彩无障碍对比度与严苛的触控交互体验。
---