feat(companion): 全面确立以陪吃·陪喝·陪唱为主的核心架构,重构首屏推荐阵容与歌声试听
这个提交包含在:
@@ -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
普通文件
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
普通文件
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
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户