feat(activity): 活动大厅全面以陪吃·陪唱·陪喝为主,增加品类快捷胶囊筛选并完善TDD门禁

这个提交包含在:
wikiboss
2026-09-16 20:18:34 +08:00
父节点 9fd7ea2c5a
当前提交 57b8aeebe4
共有 5 个文件被更改,包括 359 次插入 和 72 次删除

查看文件

@@ -75,11 +75,15 @@ graph TD
## 3. 设计规范系统(Design Tokens)
### 3.1 色彩语义系统(Color Palette Tokens)
全套界面以暗夜深空蓝黑为底色,强化高对比度与电竞发光氛围:
### 3.1 色彩语义系统(Color Palette Tokens: 双主题架构)
系统支持【冷光暗夜 Cyber Cold (默认)】与【暖阳赤焰 Sunset Amber (暖色系)】双套色彩体系自由切换,兼顾硬核电竞沉浸质感与同城生活社交温馨活力:
#### 3.1.1 经典冷光暗夜 (Cyber Cold - Default)
以深邃夜空黑与电竞靛蓝为主色调:
```scss
// 主题色彩 Tokens (uni.scss / tailwind tokens)
// 经典冷光暗夜 Tokens
$bg-primary: #0A0F1D; // 页面全局最底色 (深邃暗夜黑)
$bg-surface: #151D30; // 卡片/模块容器底色 (暗夜深空蓝)
$bg-surface-elevated: #1E2942; // 次级浮层/下拉/弹窗底色
@@ -90,8 +94,8 @@ $color-brand: #6366F1; // 品牌核心蓝紫 (Primary CTA)
$color-brand-light: #818CF8;
$color-brand-glow: rgba(99, 102, 241, 0.35); // 发光阴影色
// 业务强调色 (特别为活动项目与状态指示设定)
$color-accent: #10B981; // 翠绿高光色 (活动项目标签、在线呼吸灯、成功状态)
// 业务强调色
$color-accent: #10B981; // 翠绿高光色 (活动标签、在线呼吸灯、成功状态)
$color-accent-glow: rgba(16, 185, 129, 0.3);
$color-warning: #F59E0B; // 暖橙金 (金牌大神等级标签、待支付倒计时)
$color-danger: #EF4444; // 警示红 (退款仲裁、违规警告)
@@ -104,6 +108,31 @@ $border-subtle: #1E293B; // 卡片外描边/分割线
$border-focus: #4F46E5; // 激活描边色
```
#### 3.1.2 暖阳赤焰暖色系 (Sunset Amber & Warm Ember)
以暖夜焦糖深褐黑为底色,以暖阳曜橙与琥珀金为流光,契合同城探店、陪吃、陪喝、陪唱等温暖治愈系生活场景:
```scss
// 暖阳赤焰专属 Tokens
$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; // 核心曜橙 (Primary CTA,极具温度与能量)
$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);
// 文本与无障碍对比色阶 (严格满足 WCAG AA 标准)
$warm-text-primary: #FFF7ED; // 一级主文本 (暖白,对比度 > 13:1)
$warm-text-secondary: #C7B5A7; // 二级描述文本 (暖灰褐,对比度 > 5.5:1)
$warm-text-muted: #8C7565; // 三级占位说明 (暗咖灰,对比度 > 3.5:1)
$warm-border-subtle: #342217; // 暖色分割线与卡片描边
$warm-border-focus: #EA580C; // 激活描边色
```
---
### 3.2 响应式栅格与间距阶梯(Spacing & Layout Scale)

查看文件

@@ -233,76 +233,120 @@ export const EXPERTS_LIST = [
export const ACTIVITIES_LIST = [
{
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: [],
},
{
id: 'act_104',
title: '【AA制火锅】周末三里屯地道铜锅涮肉探店搭子',
title: '【陪吃·铜锅涮肉】周末三里屯地道老北京火锅探店AA局',
game: '美食陪吃',
track: 'OFFLINE_MEAL',
payType: 'AA',
totalFee: 200,
aaFee: 50,
totalFee: 240,
aaFee: 60,
maxParticipants: 4,
currentParticipants: 2,
currentParticipants: 3,
startTime: '周六 18:30',
location: '朝阳区三里屯商圈聚宝源',
location: '北京市朝阳区三里屯聚宝源铜锅涮肉',
creatorNickname: '橙子小七',
creatorAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100',
status: 'RECRUITING',
participants: [
{ id: 'p6', nickname: '涮肉小行家', avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', verifyCode: '5821', checkedIn: false },
{ 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: '【陪唱·KTV欢唱】周五晚工体纯K麦霸减压欢唱局·房主请客包厢',
game: '同城陪唱',
track: 'OFFLINE_SING',
payType: 'PROJECT',
totalFee: 600,
aaFee: 0,
maxParticipants: 6,
currentParticipants: 4,
startTime: '周五 20:00',
location: '北京市朝阳区工人体育场纯K 888豪华大包',
creatorNickname: '苏苏学姐',
creatorAvatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=100',
status: 'RECRUITING',
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: '【陪喝·高空下午茶】国贸云端景观双人下午茶拍照闲聊',
game: '下午茶陪喝',
track: 'OFFLINE_DRINK',
payType: 'AA',
totalFee: 196,
aaFee: 98,
maxParticipants: 2,
currentParticipants: 1,
startTime: '周日 14:30',
location: '北京市朝阳区国贸商城高空云端景观咖啡',
creatorNickname: '清芷',
creatorAvatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=100',
status: 'RECRUITING',
participants: [
{ id: 'p6', nickname: '拿铁爱好者', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100', verifyCode: '5821', checkedIn: false },
],
},
{
id: 'act_104',
title: '【陪吃·炭火烤肉】望京韩国街地道厚切五花肉深夜食堂搭子',
game: '美食陪吃',
track: 'OFFLINE_MEAL',
payType: 'AA',
totalFee: 260,
aaFee: 65,
maxParticipants: 4,
currentParticipants: 2,
startTime: '周六 19:00',
location: '北京市朝阳区望京新天地地道炭火烤肉',
creatorNickname: '桃子汽水',
creatorAvatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=100',
status: 'RECRUITING',
participants: [
{ id: 'p7', nickname: '烤肉达人', avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', verifyCode: '3192', checkedIn: false },
],
},
{
id: 'act_105',
title: '【陪喝·精酿微醺】五道营胡同屋顶露台精酿清吧品酒局',
game: '同城微醺',
track: 'OFFLINE_DRINK',
payType: 'PROJECT',
totalFee: 480,
aaFee: 0,
maxParticipants: 4,
currentParticipants: 3,
startTime: '周六 21:00',
location: '北京市东城区五道营胡同露台精酿餐吧',
creatorNickname: '夜鹰',
creatorAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100',
status: 'RECRUITING',
participants: [
{ id: 'p8', nickname: '微醺客', avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100', verifyCode: '4912', checkedIn: true },
],
},
{
id: 'act_106',
title: '【陪唱·音乐清吧】五道口民谣现场驻唱与合唱拼桌',
game: '同城陪唱',
track: 'OFFLINE_SING',
payType: 'AA',
totalFee: 200,
aaFee: 50,
maxParticipants: 4,
currentParticipants: 4,
startTime: '周日 20:30',
location: '北京市海淀区五道口民谣音乐清吧',
creatorNickname: '月色浅浅',
creatorAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100',
status: 'RUNNING',
participants: [
{ id: 'p9', nickname: '吉他手阿杰', avatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=100', verifyCode: '7721', checkedIn: true },
{ id: 'p10', nickname: '民谣小调', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100', verifyCode: '8832', checkedIn: true },
],
},
];

查看文件

@@ -1,5 +1,5 @@
<template>
<view class="activity-container">
<view class="activity-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
<!-- 顶部模式切换选项卡 (矢量SVG模式图标) -->
<view class="tab-header">
<view
@@ -33,9 +33,24 @@
<!-- 顶部状态小提示 (矢量盾牌图标) -->
<view class="tips-bar">
<image class="tips-svg" :src="ICONS.shield" mode="aspectFit" />
<text class="tips-text">AA制活动人均AA平摊,项目制为房主全额悬赏或赛事奖池</text>
<text class="tips-text">同城组局主打美食陪吃、同城陪唱、下午茶陪喝,凭4位核销码现场签到履约</text>
</view>
<!-- 品类横滑筛选胶囊 -->
<scroll-view scroll-x class="cat-filter-scroll" :show-scrollbar="false">
<view class="cat-filter-list">
<view
v-for="cat in categoryOptions"
:key="cat.key"
class="cat-chip btn-active"
:class="{ active: currentCategory === cat.key }"
@click="selectCategory(cat.key)"
>
<text class="chip-text">{{ cat.label }}</text>
</view>
</view>
</scroll-view>
<!-- 活动列表 -->
<view class="activity-list">
<view
@@ -120,10 +135,38 @@ import { ICONS } from '@/common/icons.js';
const activities = ref(ACTIVITIES_LIST);
const currentTab = ref('ALL');
const currentCategory = ref('ALL');
const categoryOptions = [
{ key: 'ALL', label: '全部品类' },
{ key: 'MEAL', label: '美食陪吃' },
{ key: 'SING', label: '同城陪唱' },
{ key: 'DRINK', label: '下午茶陪喝' },
{ key: 'GAME', label: '电竞开黑' },
];
const selectCategory = (key) => {
currentCategory.value = key;
};
const filteredActivities = computed(() => {
if (currentTab.value === 'ALL') return activities.value;
return activities.value.filter(a => a.payType === currentTab.value);
let list = activities.value;
if (currentTab.value !== 'ALL') {
list = list.filter(a => a.payType === currentTab.value);
}
if (currentCategory.value !== 'ALL') {
const map = {
MEAL: '美食陪吃',
SING: '同城陪唱',
DRINK: '下午茶陪喝',
GAME: '王者荣耀',
};
const target = map[currentCategory.value];
if (target) {
list = list.filter(a => a.game === target);
}
}
return list;
});
const switchTab = (tab) => {
@@ -225,6 +268,40 @@ const createActivity = () => {
}
}
.cat-filter-scroll {
white-space: nowrap;
margin-bottom: 24rpx;
.cat-filter-list {
display: flex;
gap: 16rpx;
.cat-chip {
padding: 10rpx 24rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.2s ease;
.chip-text {
font-size: 24rpx;
color: #94A3B8;
font-weight: 500;
}
&.active {
background: rgba(99, 102, 241, 0.2);
border-color: #6366F1;
.chip-text {
color: #FFF;
font-weight: 700;
}
}
}
}
}
.activity-list {
display: flex;
flex-direction: column;

查看文件

@@ -1,5 +1,5 @@
<template>
<view class="index-container">
<view class="index-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
<!-- 顶部搜索栏 (矢量SVG搜索图标,交互聚焦微动效) -->
<view class="header-search">
<view class="search-box">
@@ -142,15 +142,30 @@
</template>
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { GAMES_LIST, BANNERS, EXPERTS_LIST } from '@/common/mock.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey } from '@/common/theme.js';
const games = ref(GAMES_LIST);
const banners = ref(BANNERS);
const experts = ref(EXPERTS_LIST);
const currentCategory = ref('all');
const playingId = ref(null);
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 filteredExperts = computed(() => {
if (currentCategory.value === 'all') return experts.value;
@@ -685,6 +700,35 @@ const navTo = (url) => {
}
}
.index-container.theme-warm {
background-color: #140E0A;
.header-search {
background-color: #140E0A;
.search-box {
background-color: #221711;
border-color: rgba(249, 115, 22, 0.15);
&:focus-within {
border-color: #F97316;
box-shadow: 0 0 16rpx rgba(249, 115, 22, 0.35);
}
}
}
.category-scroll .category-item.active {
background: linear-gradient(135deg, rgba(249, 115, 22, 0.25), rgba(245, 158, 11, 0.1));
border-color: #F97316;
.cat-name {
color: #FB923C;
}
}
.expert-grid .expert-card {
background-color: #221711;
border-color: rgba(249, 115, 22, 0.12);
}
}
@keyframes pulse {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 8rpx rgba(255, 255, 255, 0); }

查看文件

@@ -0,0 +1,93 @@
import * as fs from 'fs';
import * as path from 'path';
describe('ActivityListContent (TDD 测试套件: 活动大厅陪吃·陪唱·陪喝内容与零Emoji校验)', () => {
const rootDir = path.resolve(__dirname, '../../');
const mockFilePath = path.join(rootDir, 'pwuniapp', 'common', 'mock.js');
const listVuePath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'list.vue');
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
describe('1. mock.js 中 ACTIVITIES_LIST 内容规范校验', () => {
it('活动列表中陪吃·陪唱·陪喝三大核心品类应占主导地位 (>= 80%)', () => {
const mockContent = fs.readFileSync(mockFilePath, 'utf-8');
// 提取 ACTIVITIES_LIST 数组对象文本
const listMatch = mockContent.match(/export const ACTIVITIES_LIST\s*=\s*(\[[\s\S]*?\n\];)/);
expect(listMatch).not.toBeNull();
// 解析执行提取对象
const evalList = new Function(`return ${listMatch![1].replace(/;\s*$/, '')}`)();
expect(Array.isArray(evalList)).toBe(true);
expect(evalList.length).toBeGreaterThanOrEqual(4);
const mainCategories = ['美食陪吃', '同城陪唱', '下午茶陪喝', '同城微醺'];
const mainCategoryCount = evalList.filter((item: any) =>
mainCategories.includes(item.game || item.category)
).length;
const mainRatio = mainCategoryCount / evalList.length;
expect(mainRatio).toBeGreaterThanOrEqual(0.8);
});
it('所有活动必须包含合法公开场所、时间与发起人信息', () => {
const mockContent = fs.readFileSync(mockFilePath, 'utf-8');
const listMatch = mockContent.match(/export const ACTIVITIES_LIST\s*=\s*(\[[\s\S]*?\n\];)/);
const evalList = new Function(`return ${listMatch![1].replace(/;\s*$/, '')}`)();
for (const item of evalList) {
expect(item.id).toBeDefined();
expect(item.title).toBeDefined();
expect(item.location).toBeDefined();
expect(item.startTime).toBeDefined();
expect(item.creatorNickname).toBeDefined();
expect(item.creatorAvatar).toBeDefined();
expect(['AA', 'PROJECT']).toContain(item.payType);
// 验证地点均为公开合法商圈/门店,不能有私人住宅等违规词
expect(item.location).not.toMatch(/(酒店客房|私人住宅|私密公寓)/);
}
});
it('ACTIVITIES_LIST 数据中严禁包含任何系统 Emoji 字符', () => {
const mockContent = fs.readFileSync(mockFilePath, 'utf-8');
const listMatch = mockContent.match(/export const ACTIVITIES_LIST\s*=\s*(\[[\s\S]*?\n\];)/);
expect(listMatch).not.toBeNull();
const listContent = listMatch![1];
const lines = listContent.split('\n');
const violations: string[] = [];
lines.forEach((line, idx) => {
if (emojiRegex.test(line)) {
violations.push(`Line ${idx + 1}: ${line.trim()}`);
}
});
expect(violations).toEqual([]);
});
});
describe('2. pages/activity/list.vue 页面规范校验', () => {
it('活动列表页面模板与样式中严禁包含任何 Emoji 字符', () => {
const content = fs.readFileSync(listVuePath, 'utf-8');
const lines = content.split('\n');
const violations: string[] = [];
lines.forEach((line, idx) => {
if (emojiRegex.test(line)) {
violations.push(`Line ${idx + 1}: ${line.trim()}`);
}
});
expect(violations).toEqual([]);
});
it('页面应包含陪吃·陪唱·陪喝相关的分类快捷切换或展示逻辑', () => {
const content = fs.readFileSync(listVuePath, 'utf-8');
// 验证包含美食陪吃、同城陪唱等核心品类关键词
expect(content).toMatch(/(美食陪吃|陪吃)/);
expect(content).toMatch(/(同城陪唱|陪唱)/);
expect(content).toMatch(/(下午茶陪喝|陪喝)/);
});
});
});