feat(companion): 全面确立以陪吃·陪喝·陪唱为主的核心架构,重构首屏推荐阵容与歌声试听

这个提交包含在:
wikiboss
2026-09-16 20:16:56 +08:00
父节点 341023c77b
当前提交 9fd7ea2c5a
共有 14 个文件被更改,包括 749 次插入 和 82 次删除

查看文件

@@ -24,12 +24,12 @@
## 1. 项目概述与业务架构
### 1.1 项目定位与业务范畴
本项目致力于打造高效、合规、有趣的泛娱乐线上电竞与同城生活社交陪伴平台。系统明确:**“陪玩”不仅仅包含线上游戏陪玩,更涵盖丰富的同城线下生活陪伴(陪玩·陪吃·陪喝)**:
- **线上电竞陪玩**:王者荣耀、和平精英、英雄联盟、无畏契约等上分、娱乐与连麦教学;
- **同城美食陪吃**:火锅烤肉、网红探店、商场餐饮搭子,前置约定现场消费分摊机制(AA制各自付 / 老板请客 / 大神自理);
- **同城下午茶陪喝**:精品咖啡手冲品鉴、新中式茶室、商业清吧微醺倾听解压;
- **同城线下陪玩**:剧本杀探案、密室逃脱、桌游互动、漫展同游、户外羽毛球运动等;
- **履约安全风控**:线下服务强制限定在公开正规商业场所,严禁私密幽闭空间,双方碰面后出示 **4 位数字核销码**现场到场签到,确保真实履约与人身财产安全。系统打破传统单向买卖壁垒,实现**用户与陪伴大神双身份一体化平滑切换**。
本项目致力于打造高效、合规、有趣的泛娱乐线上电竞与同城生活社交陪伴平台。系统明确:**以【陪吃】、【陪喝】、【陪唱】为主打核心业务**,全面覆盖同城生活陪伴与线上电竞开黑:
- **同城美食陪吃 (核心)**:火锅烤肉、网红餐厅探店、商场餐饮饭搭子,前置约定现场消费分摊机制(AA制各自付 / 老板请客 / 大神自理);
- **同城下午茶陪喝 (核心)**:精品咖啡手冲品鉴、新中式茶室、商业清吧微醺倾听解压;
- **同城KTV陪唱 (核心)**:量贩KTV麦霸对唱、甜美流行情歌、包厢气氛组担当,配套专属高保真歌声试听条;
- **同城陪玩与电竞上分 (补充)**:剧本杀探案、同城桌游、王者荣耀、和平精英等;
- **履约安全风控**:线下服务强制限定在公开正规商业场所(商场餐厅、品牌咖啡馆、正规量贩KTV等),严禁私密幽闭空间,双方碰面后出示 **4 位数字核销码**现场到场签到,确保真实履约与人身财产安全。系统打破传统单向买卖壁垒,实现**用户与陪伴大神双身份一体化平滑切换**。
### 1.2 角色权限与三端架构
```mermaid

查看文件

@@ -1,7 +1,10 @@
<script>
import { getCurrentThemeKey } from './common/theme.js';
export default {
onLaunch: function () {
console.log('App Launch: 电竞陪玩小程序初始化');
const currentTheme = getCurrentThemeKey();
console.log('App Launch: 电竞陪玩小程序初始化, 当前主题:', currentTheme);
},
onShow: function () {
console.log('App Show');
@@ -13,10 +16,51 @@ export default {
</script>
<style lang="scss">
/* 双主题 Design Tokens (CSS Variables 全局动态驱动) */
:root, page {
--bg-primary: #0A0F1D;
--bg-surface: #151D30;
--bg-surface-elevated: #1E2942;
--bg-surface-glass: rgba(21, 29, 48, 0.85);
--color-brand: #6366F1;
--color-brand-light: #818CF8;
--color-brand-gold: #F59E0B;
--color-brand-glow: rgba(99, 102, 241, 0.35);
--color-brand-gradient: linear-gradient(135deg, #6366F1, #8B5CF6);
--border-subtle: #1E293B;
--border-focus: #4F46E5;
--text-primary: #F8FAFC;
--text-secondary: #94A3B8;
--text-muted: #64748B;
--shadow-brand: 0 8rpx 24rpx rgba(99, 102, 241, 0.35);
}
/* 暖阳赤焰暖色系主题 (Sunset Amber & Warm Ember) */
page.theme-warm, .theme-warm {
--bg-primary: #140E0A;
--bg-surface: #221711;
--bg-surface-elevated: #2E1F17;
--bg-surface-glass: rgba(34, 23, 17, 0.85);
--color-brand: #F97316;
--color-brand-light: #FB923C;
--color-brand-gold: #F59E0B;
--color-brand-glow: rgba(249, 115, 22, 0.35);
--color-brand-gradient: linear-gradient(135deg, #F97316, #F59E0B);
--border-subtle: #342217;
--border-focus: #EA580C;
--text-primary: #FFF7ED;
--text-secondary: #C7B5A7;
--text-muted: #8C7565;
--shadow-brand: 0 8rpx 24rpx rgba(249, 115, 22, 0.35);
background-color: #140E0A;
color: #FFF7ED;
}
/* 全局暗黑电竞基础重置 (严格符合 UI/UX Pro Max 规范) */
page {
background-color: #0A0F1D;
color: #F8FAFC;
background-color: var(--bg-primary, #0A0F1D);
color: var(--text-primary, #F8FAFC);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', sans-serif;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
@@ -68,11 +112,11 @@ html, body {
max-width: 450px !important;
margin: 20px auto !important;
min-height: calc(100vh - 40px) !important;
box-shadow: 0 0 80rpx rgba(0, 0, 0, 0.95), 0 0 0 2rpx rgba(99, 102, 241, 0.25) !important;
box-shadow: 0 0 80rpx rgba(0, 0, 0, 0.95), 0 0 0 2rpx var(--color-brand-glow, rgba(99, 102, 241, 0.25)) !important;
border-radius: 40rpx !important;
overflow: hidden !important;
position: relative !important;
background-color: #0A0F1D !important;
background-color: var(--bg-primary, #0A0F1D) !important;
}
uni-page-head {
@@ -117,7 +161,7 @@ html, body {
}
uni-tabbar .uni-tabbar__bd {
background-color: #151D30 !important;
background-color: var(--bg-surface, #151D30) !important;
border-top: 1px solid rgba(255, 255, 255, 0.08) !important;
}
/* #endif */

查看文件

@@ -264,5 +264,34 @@ export const ICONS = {
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
`),
// 同城陪唱·KTV/麦霸
micVocal: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F43F5E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
`),
// 主题切换·调色板
palette: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="13.5" cy="6.5" r=".5" fill="#F97316"></circle>
<circle cx="17.5" cy="10.5" r=".5" fill="#F97316"></circle>
<circle cx="8.5" cy="7.5" r=".5" fill="#F97316"></circle>
<circle cx="6.5" cy="12.5" r=".5" fill="#F97316"></circle>
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"></path>
</svg>
`),
// 暖阳火苗·暖色流光
warmFlame: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path>
</svg>
`)
};

查看文件

@@ -3,21 +3,130 @@ import { ICONS } from './icons.js';
export const GAMES_LIST = [
{ id: 'all', name: '全部', icon: ICONS.gameAll, hot: false },
{ id: 'offline_meal', name: '美食陪吃', icon: ICONS.utensils, hot: true },
{ id: 'offline_drink', name: '下午茶陪喝', icon: ICONS.coffee, hot: false },
{ id: 'offline_play', name: '同城陪玩', icon: ICONS.dice, hot: true },
{ id: 'offline_drink', name: '下午茶陪喝', icon: ICONS.coffee, hot: true },
{ id: 'offline_sing', name: '同城陪唱', icon: ICONS.micVocal, hot: true },
{ id: 'offline_play', name: '同城陪玩', icon: ICONS.dice, hot: false },
{ id: 'wzry', name: '王者荣耀', icon: ICONS.gameCrown, hot: false },
{ id: 'hpjy', name: '和平精英', icon: ICONS.gameCrosshair, hot: false },
{ id: 'lol', name: '英雄联盟', icon: ICONS.gameSwords, hot: false },
{ id: 'val', name: '无畏契约', icon: ICONS.gameTarget, 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%)' },
{ id: '1', title: '同城聚会狂欢 | 网红火锅探店与量贩KTV嗨唱', tag: '热门陪伴', bg: 'linear-gradient(135deg, #F43F5E 0%, #FB7185 100%)' },
{ id: '2', title: '新用户专享 | 线下陪吃陪喝立减30元', tag: '新人特惠', bg: 'linear-gradient(135deg, #059669 0%, #10B981 100%)' },
{ id: '3', title: '天籁歌喉进驻 | 音乐学院陪唱达人歌声试听', tag: 'K歌对唱', bg: 'linear-gradient(135deg, #7C3AED 0%, #6366F1 100%)' },
];
export const EXPERTS_LIST = [
{
id: 'exp_105',
nickname: '橙子小七',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&h=400&fit=crop',
gender: 'female',
age: 22,
game: '美食陪吃',
track: 'OFFLINE_MEAL',
city: '北京市 · 朝阳/三里屯',
rankTag: '美食探店达人 · 火锅烤肉搭子',
levelTag: '同城红人',
price: 68,
rating: 5.0,
orderCount: 420,
isOnline: true,
voiceDuration: 14,
voiceType: 'voice',
bio: '资深美食探店博主,性格随和开朗,陪你吃遍同城网红餐厅与地道市井美味,消费约定AA或请客~',
photos: ['https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500&fit=crop'],
services: [
{ id: 'srv_7', title: '同城美食探店 · 火锅/烤肉饭搭子', price: 68, unit: '小时', desc: '公开商场餐厅,绝不踩雷,现场AA或买单' },
{ id: 'srv_8', title: '商圈早午餐/轻食下午茶陪伴', price: 58, unit: '小时', desc: '轻松闲聊,氛围愉快,阳光公开场所' },
],
reviews: [
{ id: 'r4', userNickname: '吃货小张', userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', rating: 5, tags: ['随和健谈', '推荐靠谱'], content: '火锅店推荐的太好吃了,聊得很投缘!', time: '昨天' },
],
},
{
id: 'exp_107',
nickname: '苏苏学姐',
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?w=800&h=400&fit=crop',
gender: 'female',
age: 21,
game: '同城陪唱',
track: 'OFFLINE_SING',
city: '北京市 · 工体/五道口',
rankTag: '音乐学院在读 · 甜美情歌对唱',
levelTag: '天籁歌喉',
price: 88,
rating: 5.0,
orderCount: 560,
isOnline: true,
voiceDuration: 18,
voiceType: 'song',
voiceTitle: '歌声试听 · 《晴天》甜美对唱',
bio: '音乐学院流行演唱在读,精通流行热门情歌与粤语金曲,KTV对唱超好听,包厢气氛组担当~限正规公开量贩KTV。',
photos: ['https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?w=500&fit=crop'],
services: [
{ id: 'srv_11', title: '量贩KTV麦霸对唱与包厢气氛', price: 88, unit: '小时', desc: '正规公开KTV,包厢房费与酒水消费约定AA或请客' },
{ id: 'srv_12', title: '同城Livehouse/音乐节同游陪伴', price: 98, unit: '小时', desc: '现场沉浸听歌,音乐狂欢好搭子' },
],
reviews: [
{ id: 'r5', userNickname: 'K歌达人阿伟', userAvatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=100', rating: 5, tags: ['歌声惊艳', '超高情商'], content: '歌声真的太好听了,完全是专业级水平,气氛特别好!', time: '3小时前' },
],
},
{
id: 'exp_106',
nickname: '莫吉托学长',
avatar: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=800&h=400&fit=crop',
gender: 'male',
age: 24,
game: '下午茶陪喝',
track: 'OFFLINE_DRINK',
city: '北京市 · 国贸/望京',
rankTag: '手冲咖啡师 · 清吧倾听解压',
levelTag: '暖心导师',
price: 58,
rating: 4.9,
orderCount: 310,
isOnline: true,
voiceDuration: 16,
voiceType: 'voice',
bio: 'SCA认证咖啡师与调酒师,善于倾听情绪解压,阳光透明,约定公开正规咖啡馆或清吧。',
photos: ['https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=500&fit=crop'],
services: [
{ id: 'srv_9', title: '精品咖啡品鉴 · 聊天解压倾听', price: 58, unit: '小时', desc: '星巴克臻选/独立咖啡馆,温和交流' },
{ id: 'srv_10', title: '正规清吧微醺特调陪伴', price: 78, unit: '小时', desc: '限公开正规商业清吧,严禁过量饮酒' },
],
reviews: [],
},
{
id: 'exp_108',
nickname: '阿泽学长',
avatar: 'https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=800&h=400&fit=crop',
gender: 'male',
age: 23,
game: '同城陪唱',
track: 'OFFLINE_SING',
city: '北京市 · 海淀/中关村',
rankTag: '流行摇滚主唱 · 吉他伴奏对唱',
levelTag: '全能麦霸',
price: 78,
rating: 4.9,
orderCount: 480,
isOnline: true,
voiceDuration: 20,
voiceType: 'song',
voiceTitle: '歌声试听 · 摇滚流行高音',
bio: '高校乐队主唱,吉他弹唱/流行摇滚,KTV气氛直接拉满,绝不冷场,带你体验演唱会包厢~',
photos: ['https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=500&fit=crop'],
services: [
{ id: 'srv_13', title: 'KTV流行摇滚对唱与吉他弹唱', price: 78, unit: '小时', desc: '气氛担当,带动全场,限公开正规量贩KTV' },
],
reviews: [],
},
{
id: 'exp_101',
nickname: '月色浅浅',
@@ -119,58 +228,6 @@ export const EXPERTS_LIST = [
],
reviews: [],
},
{
id: 'exp_105',
nickname: '橙子小七',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&h=400&fit=crop',
gender: 'female',
age: 22,
game: '美食陪吃',
track: 'OFFLINE_MEAL',
city: '北京市 · 朝阳/三里屯',
rankTag: '美食探店达人 · 火锅烤肉搭子',
levelTag: '同城红人',
price: 68,
rating: 5.0,
orderCount: 420,
isOnline: true,
voiceDuration: 14,
bio: '资深美食探店博主,性格随和开朗,陪你吃遍同城网红餐厅与地道市井美味,消费约定AA或请客~',
photos: ['https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500&fit=crop'],
services: [
{ id: 'srv_7', title: '同城美食探店 · 火锅/烤肉饭搭子', price: 68, unit: '小时', desc: '公开商场餐厅,绝不踩雷,现场AA或买单' },
{ id: 'srv_8', title: '商圈早午餐/轻食下午茶陪伴', price: 58, unit: '小时', desc: '轻松闲聊,氛围愉快,阳光公开场所' },
],
reviews: [
{ id: 'r4', userNickname: '吃货小张', userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', rating: 5, tags: ['随和健谈', '推荐靠谱'], content: '火锅店推荐的太好吃了,聊得很投缘!', time: '昨天' },
],
},
{
id: 'exp_106',
nickname: '莫吉托学长',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=800&h=400&fit=crop',
gender: 'male',
age: 24,
game: '下午茶陪喝',
track: 'OFFLINE_DRINK',
city: '北京市 · 国贸/望京',
rankTag: '手冲咖啡师 · 清吧倾听解压',
levelTag: '暖心导师',
price: 58,
rating: 4.9,
orderCount: 310,
isOnline: true,
voiceDuration: 16,
bio: '前五星酒店调酒师与SCA认证咖啡师,善于倾听情绪解压,阳光透明,约定公开正规咖啡馆或清吧。',
photos: ['https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=500&fit=crop'],
services: [
{ id: 'srv_9', title: '精品咖啡品鉴 · 聊天解压倾听', price: 58, unit: '小时', desc: '星巴克臻选/独立咖啡馆,温和交流' },
{ id: 'srv_10', title: '正规清吧微醺特调陪伴', price: 78, unit: '小时', desc: '限公开正规商业清吧,严禁过量饮酒' },
],
reviews: [],
},
];
export const ACTIVITIES_LIST = [

116
pwuniapp/common/theme.js 普通文件
查看文件

@@ -0,0 +1,116 @@
/**
* 前端双主题设计系统核心管理模块
* 严格遵照 UI/UX 规范:无 Emoji、纯矢量、高对比度与 WCAG AA 标准
*/
export const THEME_KEYS = {
COOL: 'cool',
WARM: 'warm'
};
export const THEMES = {
[THEME_KEYS.COOL]: {
key: THEME_KEYS.COOL,
name: '冷光暗夜',
description: '深邃暗夜黑与电竞靛蓝流光,专业沉浸质感',
tokens: {
bgPrimary: '#0A0F1D',
bgSurface: '#151D30',
bgSurfaceElevated: '#1E2942',
bgSurfaceGlass: 'rgba(21, 29, 48, 0.85)',
colorBrand: '#6366F1',
colorBrandLight: '#818CF8',
colorBrandGold: '#F59E0B',
colorBrandGlow: 'rgba(99, 102, 241, 0.35)',
colorBrandGradient: 'linear-gradient(135deg, #6366F1, #8B5CF6)',
borderSubtle: '#1E293B',
borderFocus: '#4F46E5',
textPrimary: '#F8FAFC',
textSecondary: '#94A3B8',
textMuted: '#64748B',
shadowBrand: '0 8rpx 24rpx rgba(99, 102, 241, 0.35)'
}
},
[THEME_KEYS.WARM]: {
key: THEME_KEYS.WARM,
name: '暖阳赤焰',
description: '深邃暖焦糖褐与暖阳曜橙流光,热情温馨有活力',
tokens: {
bgPrimary: '#140E0A',
bgSurface: '#221711',
bgSurfaceElevated: '#2E1F17',
bgSurfaceGlass: 'rgba(34, 23, 17, 0.85)',
colorBrand: '#F97316',
colorBrandLight: '#FB923C',
colorBrandGold: '#F59E0B',
colorBrandGlow: 'rgba(249, 115, 22, 0.35)',
colorBrandGradient: 'linear-gradient(135deg, #F97316, #F59E0B)',
borderSubtle: '#342217',
borderFocus: '#EA580C',
textPrimary: '#FFF7ED',
textSecondary: '#C7B5A7',
textMuted: '#8C7565',
shadowBrand: '0 8rpx 24rpx rgba(249, 115, 22, 0.35)'
}
}
};
const STORAGE_KEY = 'peiwan_app_theme';
/**
* 获取当前持久化的主题标识
*/
export function getCurrentThemeKey() {
try {
const saved = uni.getStorageSync(STORAGE_KEY);
if (saved && THEMES[saved]) {
return saved;
}
} catch (e) {
// 忽略异常,降级到默认
}
return THEME_KEYS.COOL;
}
/**
* 获取当前主题配置对象
*/
export function getCurrentTheme() {
const key = getCurrentThemeKey();
return THEMES[key];
}
/**
* 获取当前主题 Tokens
*/
export function getCurrentTokens() {
return getCurrentTheme().tokens;
}
/**
* 切换全局主题并持久化,广播全局事件
*/
export function setTheme(themeKey) {
const targetKey = THEMES[themeKey] ? themeKey : THEME_KEYS.COOL;
try {
uni.setStorageSync(STORAGE_KEY, targetKey);
} catch (e) {
// 忽略持久化异常
}
// 广播主题变更事件
try {
uni.$emit('theme-change', targetKey);
} catch (e) {
// 忽略事件抛出异常
}
return THEMES[targetKey];
}
/**
* 获取所有可选主题列表
*/
export function getAvailableThemes() {
return Object.values(THEMES);
}

查看文件

@@ -4,7 +4,7 @@
<view class="header-search">
<view class="search-box">
<image class="search-icon-img" :src="ICONS.search" mode="aspectFit" />
<input class="search-input" placeholder="搜索电竞陪玩、同城陪吃陪喝或大神" placeholder-class="placeholder-text" />
<input class="search-input" placeholder="搜索同城陪吃、陪喝、KTV陪唱或达人" placeholder-class="placeholder-text" />
</view>
</view>
@@ -116,10 +116,10 @@
</view>
<view class="rank-badge" v-else>{{ expert.rankTag }}</view>
<!-- 语音试听条 (矢量SVG播放图标,带播放高亮光晕) -->
<view class="voice-bar btn-active" :class="{ playing: playingId === expert.id }" @click.stop="toggleVoice(expert.id)">
<image class="voice-icon-svg" :src="playingId === expert.id ? ICONS.pause : ICONS.play" mode="aspectFit" />
<text class="voice-text">{{ playingId === expert.id ? '试听中...' : expert.voiceDuration + 's 原声试听' }}</text>
<!-- 语音/歌声试听条 (矢量SVG播放图标,带播放高亮光晕) -->
<view class="voice-bar btn-active" :class="{ playing: playingId === expert.id, 'song-bar': expert.voiceType === 'song' }" @click.stop="toggleVoice(expert.id)">
<image class="voice-icon-svg" :src="playingId === expert.id ? ICONS.pause : (expert.voiceType === 'song' ? ICONS.micVocal : ICONS.play)" mode="aspectFit" />
<text class="voice-text">{{ playingId === expert.id ? '播放中...' : (expert.voiceTitle || expert.voiceDuration + 's 原声试听') }}</text>
<view class="wave-lines" v-if="playingId === expert.id">
<view class="wave-item w1"></view>
<view class="wave-item w2"></view>
@@ -157,6 +157,7 @@ const filteredExperts = computed(() => {
const gameMap = {
offline_meal: '美食陪吃',
offline_drink: '下午茶陪喝',
offline_sing: '同城陪唱',
offline_play: '同城陪玩',
wzry: '王者荣耀',
hpjy: '和平精英',
@@ -616,6 +617,25 @@ const navTo = (url) => {
box-shadow: 0 0 14rpx rgba(99, 102, 241, 0.4);
}
&.song-bar {
background: rgba(244, 63, 94, 0.15);
border-color: rgba(244, 63, 94, 0.35);
.voice-text {
color: #FB7185;
}
&.playing {
background: rgba(244, 63, 94, 0.3);
border-color: #F43F5E;
box-shadow: 0 0 14rpx rgba(244, 63, 94, 0.4);
}
.wave-lines .wave-item {
background: #FB7185;
}
}
.voice-icon-svg {
width: 20rpx;
height: 20rpx;

查看文件

@@ -75,7 +75,7 @@
<block v-if="isOffline">
<view class="remark-row">
<text class="cell-label">碰面场所</text>
<input class="remark-input" v-model="meetVenue" placeholder="必须为公开正规场所,如:朝阳大悦城海底捞" placeholder-class="ph" />
<input class="remark-input" v-model="meetVenue" placeholder="正规场所,如:工体纯K量贩KTV/朝阳大悦城" placeholder-class="ph" />
</view>
<view class="expense-row">
<text class="cell-label">现场消费</text>
@@ -162,9 +162,11 @@ const isOffline = computed(() => {
return expert.value && (
expert.value.track === 'OFFLINE_MEAL' ||
expert.value.track === 'OFFLINE_DRINK' ||
expert.value.track === 'OFFLINE_SING' ||
expert.value.track === 'OFFLINE_PLAY' ||
expert.value.game === '美食陪吃' ||
expert.value.game === '下午茶陪喝' ||
expert.value.game === '同城陪唱' ||
expert.value.game === '同城陪玩'
);
});

查看文件

@@ -1,5 +1,5 @@
<template>
<view class="user-container">
<view class="user-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
<!-- 用户头部个人资料卡 -->
<view class="profile-card">
<image class="user-avatar" :src="user.avatar" mode="aspectFill" />
@@ -92,6 +92,14 @@
<!-- 列表式功能菜单 (纯矢量SVG图标) -->
<view class="list-menu-card">
<view class="list-row btn-active" @click="handleSwitchTheme">
<image class="row-svg" :src="ICONS.palette" mode="aspectFit" />
<text class="row-label">界面主题风格</text>
<view class="theme-pill" :class="{ warm: currentThemeKey === 'warm' }">
<text>{{ currentThemeKey === 'warm' ? '暖阳赤焰' : '冷光暗夜' }}</text>
</view>
<text class="row-arrow">></text>
</view>
<view class="list-row btn-active" @click="contactService">
<image class="row-svg" :src="ICONS.headphones" mode="aspectFit" />
<text class="row-label">官方在线客服</text>
@@ -112,12 +120,43 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import { USER_PROFILE } from '@/common/mock.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, setTheme, THEME_KEYS } from '@/common/theme.js';
const user = ref(USER_PROFILE);
const currentRole = ref('PLAYER');
const currentThemeKey = ref(getCurrentThemeKey());
const onThemeChanged = (key) => {
currentThemeKey.value = key;
};
onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
uni.$on('theme-change', onThemeChanged);
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
});
const handleSwitchTheme = () => {
uni.showActionSheet({
itemList: ['冷光暗夜 (经典暗夜蓝紫)', '暖阳赤焰 (热情暖橙金流光)'],
success: (res) => {
const selectedKey = res.tapIndex === 1 ? THEME_KEYS.WARM : THEME_KEYS.COOL;
currentThemeKey.value = selectedKey;
setTheme(selectedKey);
uni.showToast({
title: selectedKey === THEME_KEYS.WARM ? '已切换至【暖阳赤焰】暖色系' : '已切换至【冷光暗夜】经典款',
icon: 'none',
duration: 1500
});
}
});
};
const toggleRole = () => {
if (currentRole.value === 'PLAYER') {
@@ -522,6 +561,23 @@ const navTo = (url) => {
color: #E2E8F0;
}
.theme-pill {
margin-right: 16rpx;
font-size: 22rpx;
color: #818CF8;
background: rgba(99, 102, 241, 0.15);
border: 1px solid rgba(99, 102, 241, 0.3);
padding: 4rpx 16rpx;
border-radius: 20rpx;
font-weight: 500;
&.warm {
color: #FB923C;
background: rgba(249, 115, 22, 0.15);
border-color: rgba(249, 115, 22, 0.3);
}
}
.row-arrow {
font-size: 24rpx;
color: #64748B;
@@ -529,6 +585,25 @@ const navTo = (url) => {
}
}
.user-container.theme-warm {
background-color: #140E0A;
.profile-card .user-avatar {
border-color: #F97316;
}
.role-switch-card .role-glow-border {
background: conic-gradient(from 0deg, #F97316, #F59E0B, #EA580C, #F97316);
}
.role-switch-card .btn-switch-role {
background: linear-gradient(135deg, #F97316, #EA580C);
box-shadow: 0 4rpx 16rpx rgba(249, 115, 22, 0.4);
}
.wallet-card, .menu-grid-card, .list-menu-card {
background-color: #221711;
border-color: rgba(249, 115, 22, 0.12);
}
}
@keyframes glowSpin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }

查看文件

@@ -42,3 +42,23 @@ $shadow-accent: 0 8rpx 24rpx rgba(16, 185, 129, 0.3);
$uni-color-primary: #6366F1;
$uni-bg-color: #0A0F1D;
$uni-text-color: #F8FAFC;
/* 7. 暖阳赤焰暖色系专属 Tokens (Sunset Amber & Warm Ember) */
$warm-bg-primary: #140E0A; // 暖夜焦糖深褐黑
$warm-bg-surface: #221711; // 暖色卡片底色
$warm-bg-surface-elevated: #2E1F17; // 暖色浮层底色
$warm-bg-surface-glass: rgba(34, 23, 17, 0.85); // 暖色拟态磨砂玻璃
$warm-color-brand: #F97316; // 暖阳曜橙 (主品牌色)
$warm-color-brand-light: #FB923C; // 暖橙高光
$warm-color-brand-gold: #F59E0B; // 暖金流光
$warm-color-brand-glow: rgba(249, 115, 22, 0.35); // 暖橙发光光晕
$warm-color-brand-gradient: linear-gradient(135deg, #F97316, #F59E0B);
$warm-text-primary: #FFF7ED; // 暖白高光文本
$warm-text-secondary: #C7B5A7; // 暖灰次级文本
$warm-text-muted: #8C7565; // 暖咖三级文本
$warm-border-subtle: #342217; // 暖色边框
$warm-border-focus: #EA580C; // 暖色聚焦边框
$warm-shadow-brand: 0 8rpx 24rpx rgba(249, 115, 22, 0.35);

查看文件

@@ -8,6 +8,7 @@ export enum CompanionTrack {
OFFLINE_PLAY = 'OFFLINE_PLAY', // 线下同城陪玩 (桌游/剧本杀/运动/漫展)
OFFLINE_MEAL = 'OFFLINE_MEAL', // 线下同城陪吃 (网红探店/火锅/餐厅饭搭子)
OFFLINE_DRINK = 'OFFLINE_DRINK', // 线下同城陪喝 (咖啡下午茶/茶室清吧)
OFFLINE_SING = 'OFFLINE_SING', // 线下同城陪唱 (量贩KTV/麦霸对唱/音乐Live)
}
export enum ExpenseBurdenType {
@@ -45,10 +46,11 @@ export class ServiceCategoryManager {
getAllTracks(): CompanionTrack[] {
return [
CompanionTrack.ONLINE_GAME,
CompanionTrack.OFFLINE_PLAY,
CompanionTrack.OFFLINE_MEAL,
CompanionTrack.OFFLINE_DRINK,
CompanionTrack.OFFLINE_SING,
CompanionTrack.OFFLINE_PLAY,
CompanionTrack.ONLINE_GAME,
];
}

查看文件

@@ -0,0 +1,113 @@
/**
* 陪玩系统双主题色彩管理系统
* 严格遵照 UI/UX 规范:无 Emoji、全矢量的色彩 Token 体系
*/
export const THEME_KEYS = {
COOL: 'cool',
WARM: 'warm'
} as const;
export type ThemeKey = (typeof THEME_KEYS)[keyof typeof THEME_KEYS];
export interface ThemeTokens {
bgPrimary: string;
bgSurface: string;
bgSurfaceElevated: string;
bgSurfaceGlass: string;
colorBrand: string;
colorBrandLight: string;
colorBrandGold: string;
colorBrandGlow: string;
colorBrandGradient: string;
borderSubtle: string;
borderFocus: string;
textPrimary: string;
textSecondary: string;
textMuted: string;
shadowBrand: string;
}
export interface ThemeItem {
key: ThemeKey;
name: string;
description: string;
tokens: ThemeTokens;
}
export const THEMES: Record<ThemeKey, ThemeItem> = {
[THEME_KEYS.COOL]: {
key: THEME_KEYS.COOL,
name: '冷光暗夜',
description: '深邃暗夜黑与电竞靛蓝流光,专业沉浸质感',
tokens: {
bgPrimary: '#0A0F1D',
bgSurface: '#151D30',
bgSurfaceElevated: '#1E2942',
bgSurfaceGlass: 'rgba(21, 29, 48, 0.85)',
colorBrand: '#6366F1',
colorBrandLight: '#818CF8',
colorBrandGold: '#F59E0B',
colorBrandGlow: 'rgba(99, 102, 241, 0.35)',
colorBrandGradient: 'linear-gradient(135deg, #6366F1, #8B5CF6)',
borderSubtle: '#1E293B',
borderFocus: '#4F46E5',
textPrimary: '#F8FAFC',
textSecondary: '#94A3B8',
textMuted: '#64748B',
shadowBrand: '0 8rpx 24rpx rgba(99, 102, 241, 0.35)'
}
},
[THEME_KEYS.WARM]: {
key: THEME_KEYS.WARM,
name: '暖阳赤焰',
description: '深邃暖焦糖褐与暖阳曜橙流光,热情温馨有活力',
tokens: {
bgPrimary: '#140E0A',
bgSurface: '#221711',
bgSurfaceElevated: '#2E1F17',
bgSurfaceGlass: 'rgba(34, 23, 17, 0.85)',
colorBrand: '#F97316',
colorBrandLight: '#FB923C',
colorBrandGold: '#F59E0B',
colorBrandGlow: 'rgba(249, 115, 22, 0.35)',
colorBrandGradient: 'linear-gradient(135deg, #F97316, #F59E0B)',
borderSubtle: '#342217',
borderFocus: '#EA580C',
textPrimary: '#FFF7ED',
textSecondary: '#C7B5A7',
textMuted: '#8C7565',
shadowBrand: '0 8rpx 24rpx rgba(249, 115, 22, 0.35)'
}
}
};
export class ThemeManager {
private currentKey: ThemeKey = THEME_KEYS.COOL;
getCurrentThemeKey(): ThemeKey {
return this.currentKey;
}
getCurrentTheme(): ThemeItem {
return THEMES[this.currentKey];
}
getCurrentTokens(): ThemeTokens {
return this.getCurrentTheme().tokens;
}
switchTheme(key: ThemeKey): ThemeItem {
if (key in THEMES) {
this.currentKey = key;
} else {
// 非法或未知主题容错平滑降级至默认冷光暗夜
this.currentKey = THEME_KEYS.COOL;
}
return this.getCurrentTheme();
}
getAvailableThemes(): ThemeItem[] {
return Object.values(THEMES);
}
}

查看文件

@@ -13,13 +13,14 @@ describe('CompanionCategoryManager (TDD 测试套件: 陪玩·陪吃·陪喝品
});
describe('1. 品类赛道与元数据校验', () => {
it('应正确识别并列出包含线上游戏、线下陪玩、陪吃、陪喝的所有赛道', () => {
it('应正确识别并列出包含线上游戏、线下陪玩、陪吃、陪喝、陪唱的所有赛道', () => {
const tracks = manager.getAllTracks();
expect(tracks).toContain(CompanionTrack.ONLINE_GAME);
expect(tracks).toContain(CompanionTrack.OFFLINE_PLAY);
expect(tracks).toContain(CompanionTrack.OFFLINE_MEAL);
expect(tracks).toContain(CompanionTrack.OFFLINE_DRINK);
expect(tracks.length).toBe(4);
expect(tracks).toContain(CompanionTrack.OFFLINE_SING);
expect(tracks.length).toBe(5);
});
it('线上游戏陪玩服务必须包含有效游戏名称', () => {
@@ -74,6 +75,20 @@ describe('CompanionCategoryManager (TDD 测试套件: 陪玩·陪吃·陪喝品
expect(item.track).toBe(CompanionTrack.OFFLINE_DRINK);
});
it('线下陪唱服务必须指定服务城市与公开正规量贩KTV场所', () => {
const singDto: CreateServiceItemDto = {
track: CompanionTrack.OFFLINE_SING,
title: '量贩KTV百首流行曲库麦霸对唱',
city: '北京市',
venueType: '纯K/唱吧麦颂/温莎量贩KTV',
unitPriceCents: 8000,
unit: '小时',
};
const item = manager.createServiceItem(singDto);
expect(item.track).toBe(CompanionTrack.OFFLINE_SING);
expect(item.city).toBe('北京市');
});
it('线下服务未指定城市应抛出异常', () => {
expect(() => {
manager.createServiceItem({

查看文件

@@ -57,6 +57,28 @@ describe('CompanionOrderService (TDD 测试套件: 线上游戏与同城陪玩·
expect(order.offlineDetails?.expenseBurden).toBe(ExpenseBurdenType.AA_SHARE);
});
it('成功创建同城量贩KTV陪唱订单并前置约定包厢消费全包', () => {
const params: CreateOrderParams = {
buyerId: 1001,
expertId: 2003,
track: CompanionTrack.OFFLINE_SING,
serviceTitle: '量贩KTV麦霸对唱与气氛陪伴',
hours: 3,
totalAmountCents: 24000,
offlineDetails: {
city: '北京市',
venueName: '工体纯K量贩KTV',
meetTime: '今晚 20:00',
expenseBurden: ExpenseBurdenType.BUYER_TREAT,
},
};
const order = orderService.createOrder(params);
expect(order.track).toBe(CompanionTrack.OFFLINE_SING);
expect(order.offlineDetails?.venueName).toBe('工体纯K量贩KTV');
expect(order.offlineDetails?.expenseBurden).toBe(ExpenseBurdenType.BUYER_TREAT);
});
it('线下订单缺少商圈场所或城市时应抛出异常', () => {
expect(() => {
orderService.createOrder({

152
server/tests/theme.spec.ts 普通文件
查看文件

@@ -0,0 +1,152 @@
import { ThemeManager, THEME_KEYS, THEMES, ThemeKey, ThemeItem } from '../src/core/theme/theme.manager';
import * as fs from 'fs';
import * as path from 'path';
/**
* 辅助函数:计算 Hex 颜色的相对亮度 (Relative Luminance)
* 遵循 W3C WCAG 2.1 规范算法
*/
function getLuminance(hex: string): number {
const cleanHex = hex.replace('#', '');
const r = parseInt(cleanHex.substring(0, 2), 16) / 255;
const g = parseInt(cleanHex.substring(2, 4), 16) / 255;
const b = parseInt(cleanHex.substring(4, 6), 16) / 255;
const a = [r, g, b].map(v => {
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
/**
* 辅助函数:计算两组 Hex 颜色的对比度 (Contrast Ratio)
*/
function getContrastRatio(hex1: string, hex2: string): number {
const lum1 = getLuminance(hex1);
const lum2 = getLuminance(hex2);
const brightest = Math.max(lum1, lum2);
const darkest = Math.min(lum1, lum2);
return (brightest + 0.05) / (darkest + 0.05);
}
describe('ThemeManager (TDD 测试套件: 暖色系与双主题设计系统)', () => {
let themeManager: ThemeManager;
beforeEach(() => {
themeManager = new ThemeManager();
});
describe('1. 主题 Design Tokens 完备性与格式规范', () => {
it('应完整支持 cool (冷光暗夜) 与 warm (暖阳赤焰) 双套主题', () => {
const themes = themeManager.getAvailableThemes();
const keys = themes.map((t: ThemeItem) => t.key);
expect(keys).toContain(THEME_KEYS.COOL);
expect(keys).toContain(THEME_KEYS.WARM);
expect(themes.length).toBe(2);
});
it('暖色系 (warm) 必须包含全部核心色彩语义 Tokens', () => {
const warmTheme = THEMES[THEME_KEYS.WARM];
expect(warmTheme).toBeDefined();
const requiredTokens = [
'bgPrimary',
'bgSurface',
'bgSurfaceElevated',
'bgSurfaceGlass',
'colorBrand',
'colorBrandLight',
'colorBrandGlow',
'borderSubtle',
'textPrimary',
'textSecondary',
'textMuted'
];
for (const token of requiredTokens) {
expect(warmTheme.tokens[token as keyof typeof warmTheme.tokens]).toBeDefined();
}
});
it('暖色系关键颜色必须符合合法的 Hex 与 rgba 语法格式', () => {
const tokens = THEMES[THEME_KEYS.WARM].tokens;
const hexRegex = /^#([0-9A-Fa-f]{6})$/;
const rgbaRegex = /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/;
expect(hexRegex.test(tokens.bgPrimary)).toBe(true);
expect(hexRegex.test(tokens.bgSurface)).toBe(true);
expect(hexRegex.test(tokens.colorBrand)).toBe(true);
expect(hexRegex.test(tokens.textPrimary)).toBe(true);
expect(rgbaRegex.test(tokens.colorBrandGlow)).toBe(true);
});
});
describe('2. WCAG AA 级无障碍色彩对比度验证', () => {
it('暖色系一级主文本 (textPrimary) 对底色与卡片底色对比度必须 >= 7:1 (超越 AA 级 4.5:1)', () => {
const tokens = THEMES[THEME_KEYS.WARM].tokens;
const ratioOnBg = getContrastRatio(tokens.textPrimary, tokens.bgPrimary);
const ratioOnSurface = getContrastRatio(tokens.textPrimary, tokens.bgSurface);
expect(ratioOnBg).toBeGreaterThanOrEqual(7.0);
expect(ratioOnSurface).toBeGreaterThanOrEqual(7.0);
});
it('暖色系二级辅助文本 (textSecondary) 对底色对比度必须 >= 4.5:1 (符合 WCAG AA 标准)', () => {
const tokens = THEMES[THEME_KEYS.WARM].tokens;
const ratio = getContrastRatio(tokens.textSecondary, tokens.bgPrimary);
expect(ratio).toBeGreaterThanOrEqual(4.5);
});
});
describe('3. 主题切换状态机与防御性控制', () => {
it('默认初始主题应为 cool (冷光暗夜)', () => {
expect(themeManager.getCurrentThemeKey()).toBe(THEME_KEYS.COOL);
expect(themeManager.getCurrentTheme().name).toBe('冷光暗夜');
});
it('成功切换到 warm (暖阳赤焰) 主题', () => {
const current = themeManager.switchTheme(THEME_KEYS.WARM);
expect(current.key).toBe(THEME_KEYS.WARM);
expect(current.name).toBe('暖阳赤焰');
expect(themeManager.getCurrentThemeKey()).toBe(THEME_KEYS.WARM);
expect(themeManager.getCurrentTokens().colorBrand).toBe('#F97316');
});
it('传入非法主题标识时,应平滑回退到默认 cool 主题,确保系统不崩溃', () => {
const fallback = themeManager.switchTheme('non_existent_theme' as ThemeKey);
expect(fallback.key).toBe(THEME_KEYS.COOL);
});
});
describe('4. 前端代码纯净度与 Emoji 门禁校验', () => {
it('主题配置文件中严禁包含任何系统 Emoji', () => {
const rootDir = path.resolve(__dirname, '../../');
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
const filesToCheck = [
path.join(rootDir, 'server', 'src', 'core', 'theme', 'theme.manager.ts'),
path.join(rootDir, 'pwuniapp', 'common', 'theme.js')
];
for (const file of filesToCheck) {
if (fs.existsSync(file)) {
const content = fs.readFileSync(file, 'utf-8');
const lines = content.split('\n');
lines.forEach((line, idx) => {
expect({
file: path.relative(rootDir, file),
line: idx + 1,
hasEmoji: emojiRegex.test(line)
}).toEqual({
file: path.relative(rootDir, file),
line: idx + 1,
hasEmoji: false
});
});
}
}
});
});
});