refactor(data): 全面移除小程序写死的演示/兜底数据,测试数据统一落库
- 删除无引用的 common/mock.js;登录页与个人中心的演示账号改为 DB 配置 demo_accounts + /api/auth/demo-accounts、/api/auth/demo-login(签发真实 token) - 移除订单/活动/大神/动态/推广/工作台/门票/充值/举报等页面的演示列表与假兜底;新增真实空态 - 修复伪造成功:活动调价、评价回复、核销、海报保存、图片上传失败、提现/核销固定 1001、随机开黑房号、随机即时匹配、假语音播放等 - 服务端移除 101/2006 合成用户与合成大神兜底、移除默认服务包虚构价格;演示账号清单可在后台关闭 - 111 套件/733 用例全绿;已部署 20261021_162041,demo-accounts/demo-login 实测通过
这个提交包含在:
@@ -11,7 +11,7 @@ export function formatActivityData(raw) {
|
||||
|
||||
const payTypeStr = raw.payType === 2 || raw.payType === 'PROJECT' ? 'PROJECT' : 'AA';
|
||||
const totalFee = raw.totalFeeCents != null ? raw.totalFeeCents / 100 : (raw.totalFee || 0);
|
||||
const perPerson = raw.perPersonFeeCents != null ? raw.perPersonFeeCents / 100 : (raw.perPersonFee || (payTypeStr === 'AA' ? totalFee / (raw.maxParticipants || 4) : 500));
|
||||
const perPerson = raw.perPersonFeeCents != null ? raw.perPersonFeeCents / 100 : (raw.perPersonFee || (payTypeStr === 'AA' ? totalFee / (raw.maxParticipants || 4) : 0));
|
||||
const participants = (raw.participants || []).map((p) => ({
|
||||
id: p.id,
|
||||
userId: p.userId,
|
||||
@@ -26,20 +26,20 @@ export function formatActivityData(raw) {
|
||||
|
||||
return {
|
||||
id: String(raw.id),
|
||||
title: raw.title || '同城生活组局',
|
||||
game: raw.gameCategory || raw.game || '美食陪吃',
|
||||
title: raw.title || '',
|
||||
game: raw.gameCategory || raw.game || '',
|
||||
track: raw.track || 'OFFLINE_MEAL',
|
||||
payType: payTypeStr,
|
||||
totalFee,
|
||||
perPersonFee: perPerson,
|
||||
aaFee: raw.aaFee != null ? raw.aaFee : perPerson,
|
||||
maxParticipants: raw.maxParticipants || 4,
|
||||
currentParticipants: joinedCount || 1,
|
||||
startTime: raw.startTime || '今天 19:30',
|
||||
location: raw.location || '北京市朝阳区公开商业街区',
|
||||
creatorId: raw.creatorId != null ? Number(raw.creatorId) : (raw.creator_id != null ? Number(raw.creator_id) : 1001),
|
||||
creatorNickname: raw.creatorNickname || '同城局长',
|
||||
creatorAvatar: raw.creatorAvatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100',
|
||||
maxParticipants: raw.maxParticipants || 0,
|
||||
currentParticipants: joinedCount,
|
||||
startTime: raw.startTime || '',
|
||||
location: raw.location || '',
|
||||
creatorId: raw.creatorId != null ? Number(raw.creatorId) : (raw.creator_id != null ? Number(raw.creator_id) : 0),
|
||||
creatorNickname: raw.creatorNickname || '',
|
||||
creatorAvatar: raw.creatorAvatar || '',
|
||||
status: raw.status || (joinedCount >= (raw.maxParticipants || 4) ? 'FULL' : 'RECRUITING'),
|
||||
allowDynamicPrice: Boolean(raw.allowDynamicPrice),
|
||||
priceAdjustRule: raw.priceAdjustRule || 'UNIFIED_HIGH',
|
||||
@@ -203,23 +203,23 @@ export async function getCompanions(keyword) {
|
||||
.filter((c) => !c.auditStatus || c.auditStatus === '已通过')
|
||||
.map((c) => ({
|
||||
id: String(c.id),
|
||||
nickname: c.name || c.nickname || '同城大神',
|
||||
avatar: c.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300',
|
||||
gender: c.gender === '男' ? 'male' : 'female',
|
||||
age: Number(c.age) || 22,
|
||||
game: c.track === 'OFFLINE_MEAL' ? '美食陪吃' : (c.track === 'OFFLINE_SING' ? '同城陪唱' : (c.track === 'OFFLINE_DRINK' ? '下午茶陪喝' : (c.game || '同城陪伴'))),
|
||||
nickname: c.name || c.nickname || '',
|
||||
avatar: c.avatar || '',
|
||||
gender: c.gender === '男' ? 'male' : (c.gender === '女' ? 'female' : ''),
|
||||
age: Number(c.age) || 0,
|
||||
game: c.track === 'OFFLINE_MEAL' ? '美食陪吃' : (c.track === 'OFFLINE_SING' ? '同城陪唱' : (c.track === 'OFFLINE_DRINK' ? '下午茶陪喝' : (c.game || ''))),
|
||||
track: c.track || 'OFFLINE_MEAL',
|
||||
city: '北京市 · 朝阳/海淀',
|
||||
rankTag: c.rank || '同城认证陪伴师',
|
||||
levelTag: '认证大神',
|
||||
price: Number(c.hourlyRate) || 68,
|
||||
rating: Number(c.rating) || 5.0,
|
||||
orderCount: Number(c.orderCount) || 120,
|
||||
city: c.city || c.customCity || '',
|
||||
rankTag: c.rank || '',
|
||||
levelTag: c.auditStatus === '已通过' ? '认证大神' : '',
|
||||
price: Number(c.hourlyRate) || 0,
|
||||
rating: Number(c.rating) || 0,
|
||||
orderCount: Number(c.orderCount) || 0,
|
||||
isOnline: c.status === '接单中',
|
||||
voiceDuration: Number(c.voiceDuration) || 15,
|
||||
voiceDuration: Number(c.voiceDuration) || 0,
|
||||
voiceType: 'voice',
|
||||
voiceTitle: c.voiceTitle || '语音招呼',
|
||||
bio: '专业同城生活陪伴,阳光健康公开。'
|
||||
voiceTitle: c.voiceTitle || '',
|
||||
bio: c.bio || ''
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -269,34 +269,33 @@ export function formatOrderData(raw) {
|
||||
) statusKey = 'REFUNDING';
|
||||
else if (statusKey === '已取消') statusKey = 'CANCELLED';
|
||||
|
||||
const companionAvatar = raw.expertAvatar || raw.expert_avatar || raw.avatar ||
|
||||
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120';
|
||||
const companionAvatar = raw.expertAvatar || raw.expert_avatar || raw.avatar || '';
|
||||
|
||||
const companionNickname = raw.companionName || raw.companion_name || raw.expertNickname || (raw.expert && raw.expert.nickname) || '同城大神';
|
||||
const game = raw.track || (raw.expert && raw.expert.game) || '同城陪伴';
|
||||
const companionNickname = raw.companionName || raw.companion_name || raw.expertNickname || (raw.expert && raw.expert.nickname) || '';
|
||||
const game = raw.track || (raw.expert && raw.expert.game) || '';
|
||||
|
||||
return {
|
||||
id: String(raw.id),
|
||||
orderSn: raw.orderNo || raw.order_no || raw.orderSn || String(raw.id),
|
||||
expert: {
|
||||
id: String(raw.expertId || raw.expert_id || (raw.expert && raw.expert.id) || 'exp_101'),
|
||||
id: String(raw.expertId || raw.expert_id || (raw.expert && raw.expert.id) || ''),
|
||||
nickname: companionNickname,
|
||||
avatar: companionAvatar,
|
||||
game: game,
|
||||
},
|
||||
serviceTitle: raw.serviceTitle || raw.service_title || `${game} · 专业陪伴服务`,
|
||||
quantity: Number(raw.durationHours || raw.duration_hours || raw.quantity || 2),
|
||||
serviceTitle: raw.serviceTitle || raw.service_title || '',
|
||||
quantity: Number(raw.durationHours || raw.duration_hours || raw.quantity || 0),
|
||||
totalAmount: Number(raw.amount || raw.totalAmount || 0),
|
||||
refundAmount: Number(raw.refundAmount || raw.refund_amount || 0),
|
||||
status: statusKey,
|
||||
rawStatus: raw.status || '履约中',
|
||||
meetVenue: raw.location || raw.meetVenue || '北京市公开商圈',
|
||||
expenseBurden: raw.expenseBurden || raw.expense_burden || 'AA制各自付',
|
||||
payType: raw.payType || raw.pay_type || 'WECHAT',
|
||||
rawStatus: raw.status || '',
|
||||
meetVenue: raw.location || raw.meetVenue || '',
|
||||
expenseBurden: raw.expenseBurden || raw.expense_burden || '',
|
||||
payType: raw.payType || raw.pay_type || '',
|
||||
couponDiscount: Number(raw.couponDiscount || raw.coupon_discount || 0),
|
||||
verifyCode: raw.verifyCode || raw.verify_code || '',
|
||||
remark: raw.remark || '',
|
||||
createdAt: raw.createdAt || raw.created_at || '刚刚'
|
||||
createdAt: raw.createdAt || raw.created_at || ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -475,13 +474,13 @@ export async function getPosts(category, userId) {
|
||||
return {
|
||||
id: p.id,
|
||||
userId: p.userId || p.user_id || 0,
|
||||
author: p.author || '神秘同好',
|
||||
avatar: p.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
|
||||
tag: p.tag || '同城认证',
|
||||
author: p.author || '',
|
||||
avatar: p.avatar || '',
|
||||
tag: p.tag || '',
|
||||
tagType: p.tagType || 'warm-tag',
|
||||
location: p.location || '同城热门商圈',
|
||||
distance: p.distance || '1km内',
|
||||
time: p.createdAt ? '刚刚' : '近期',
|
||||
location: p.location || '',
|
||||
distance: p.distance || '',
|
||||
time: p.time || (p.createdAt ? '' : ''),
|
||||
content: p.content || '',
|
||||
topics: Array.isArray(p.topics) ? p.topics : [],
|
||||
images: Array.isArray(p.images) ? p.images : [],
|
||||
@@ -1528,3 +1527,23 @@ export async function loginWithWechatPhone({ phoneCode, wxCode } = {}) {
|
||||
wxCode
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 体验账号列表(后台配置,DB 驱动;小程序端不再写死)
|
||||
*/
|
||||
export async function getDemoAccounts() {
|
||||
try {
|
||||
const res = await http.get('/auth/demo-accounts');
|
||||
return Array.isArray(res) ? res : [];
|
||||
} catch (err) {
|
||||
console.warn('[API] 获取体验账号异常:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 体验登录:服务端校验账号在配置清单内后签发真实会话 token
|
||||
*/
|
||||
export async function demoLogin(userId) {
|
||||
return http.post('/auth/demo-login', { userId: Number(userId) });
|
||||
}
|
||||
|
||||
@@ -1,416 +0,0 @@
|
||||
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: 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 },
|
||||
];
|
||||
|
||||
export const BANNERS = [
|
||||
{ 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: '月色浅浅',
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300&h=300&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=300&h=300&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=300&h=300&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=300&h=300&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 = [
|
||||
{
|
||||
id: 'act_101',
|
||||
title: '【陪吃·铜锅涮肉】周末三里屯地道老北京火锅探店AA局',
|
||||
game: '美食陪吃',
|
||||
track: 'OFFLINE_MEAL',
|
||||
payType: 'AA',
|
||||
totalFee: 240,
|
||||
aaFee: 60,
|
||||
maxParticipants: 4,
|
||||
currentParticipants: 3,
|
||||
startTime: '周六 18:30',
|
||||
location: '北京市朝阳区三里屯聚宝源铜锅涮肉',
|
||||
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: '同城陪唱',
|
||||
track: 'OFFLINE_SING',
|
||||
payType: 'PROJECT',
|
||||
pricingMode: 'PER_PERSON',
|
||||
perPersonFee: 500,
|
||||
totalFee: 2000,
|
||||
aaFee: 0,
|
||||
maxParticipants: 4,
|
||||
currentParticipants: 3,
|
||||
allowDynamicPrice: true,
|
||||
priceAdjustRule: 'UNIFIED_HIGH',
|
||||
priceAdjustHistory: [],
|
||||
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, priceAtJoin: 500, diffCompensated: 0 },
|
||||
{ id: 'p5', nickname: '高音小王子', avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100', verifyCode: '7390', checkedIn: false, priceAtJoin: 500, diffCompensated: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
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 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const ORDERS_LIST = [
|
||||
{
|
||||
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_4',
|
||||
orderSn: 'PW20260916099',
|
||||
expert: { id: 'exp_105', nickname: '橙子小七', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100', game: '美食陪吃' },
|
||||
serviceTitle: '同城美食探店 · 火锅/烤肉饭搭子',
|
||||
quantity: 2,
|
||||
totalAmount: 136,
|
||||
status: 'IN_SERVICE',
|
||||
meetVenue: '朝阳大悦城海底捞',
|
||||
expenseBurden: 'AA制各自付',
|
||||
verifyCode: '6829',
|
||||
createdAt: '2026-09-16 18:00',
|
||||
},
|
||||
{
|
||||
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,
|
||||
};
|
||||
@@ -491,20 +491,13 @@ const handleChooseLocation = () => {
|
||||
},
|
||||
fail: (err) => {
|
||||
console.warn('[ActivityCreate] 地图选点取消或未授权:', err);
|
||||
// 若未授权或在H5/开发环境,平滑引导点选推荐公开商圈
|
||||
if (recommendPublicZones.value.length > 0) {
|
||||
selectZone(recommendPublicZones.value[0]);
|
||||
uni.showToast({ title: '已填入推荐商圈', icon: 'none' });
|
||||
}
|
||||
uni.showToast({ title: '未选择地点,可手动填写', icon: 'none' });
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN || APP-PLUS
|
||||
if (recommendPublicZones.value.length > 0) {
|
||||
selectZone(recommendPublicZones.value[0]);
|
||||
uni.showToast({ title: '已为您填入示范商圈', icon: 'none' });
|
||||
}
|
||||
uni.showToast({ title: '当前环境不支持地图选点,请手动填写', icon: 'none' });
|
||||
// #endif
|
||||
};
|
||||
|
||||
@@ -659,7 +652,7 @@ const handleSubmit = async () => {
|
||||
location: trimmedLocation,
|
||||
latitude: form.value.latitude,
|
||||
longitude: form.value.longitude,
|
||||
creatorNickname: cachedUser.nickname || '同城局长',
|
||||
creatorNickname: cachedUser.nickname || '同城用户',
|
||||
creatorAvatar: cachedUser.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&h=150&fit=crop',
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</text>
|
||||
</view>
|
||||
<text class="dp-status-text">
|
||||
当前单人出价 ¥{{ activity.perPersonFee || 500 }}/人 · 招募{{ activity.maxParticipants }}人 · 已报名{{ joinedCount }}人
|
||||
当前单人出价 ¥{{ activity.perPersonFee || 0 }}/人 · 招募{{ activity.maxParticipants }}人 · 已报名{{ joinedCount }}人
|
||||
</text>
|
||||
</view>
|
||||
<view class="dp-btn-group">
|
||||
@@ -76,9 +76,9 @@
|
||||
<view class="creator-info btn-active" @click="openHostProfile">
|
||||
<view class="creator-name-row">
|
||||
<text class="creator-name">{{ activity.creatorNickname }}</text>
|
||||
<text class="host-badge">同城局长</text>
|
||||
<text class="host-badge">发起人</text>
|
||||
</view>
|
||||
<text class="creator-sub">同城组局召集人 · 履约守约率 100%</text>
|
||||
<text class="creator-sub">同城组局召集人</text>
|
||||
</view>
|
||||
<button class="btn-chat btn-active" @click.stop="chatWithHost">私聊</button>
|
||||
</view>
|
||||
@@ -418,7 +418,8 @@ import {
|
||||
adjustActivityPrice,
|
||||
verifyActivityCheckin,
|
||||
cancelActivity,
|
||||
leaveActivity
|
||||
leaveActivity,
|
||||
normalizeUserId
|
||||
} from '@/common/api.js';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
@@ -489,18 +490,18 @@ const stopPropagation = (e) => {
|
||||
|
||||
const activity = ref({
|
||||
id: '',
|
||||
title: '同城生活组局',
|
||||
game: '同城陪伴',
|
||||
title: '',
|
||||
game: '',
|
||||
payType: 'AA',
|
||||
totalFee: 0,
|
||||
perPersonFee: 0,
|
||||
maxParticipants: 4,
|
||||
currentParticipants: 1,
|
||||
startTime: '根据预约安排',
|
||||
location: '同城公开商业场所',
|
||||
creatorId: 1001,
|
||||
creatorNickname: '局长',
|
||||
creatorAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100',
|
||||
maxParticipants: 0,
|
||||
currentParticipants: 0,
|
||||
startTime: '',
|
||||
location: '',
|
||||
creatorId: 0,
|
||||
creatorNickname: '',
|
||||
creatorAvatar: '',
|
||||
participants: [],
|
||||
priceAdjustHistory: []
|
||||
});
|
||||
@@ -510,9 +511,12 @@ const loadLiveDetail = async (id) => {
|
||||
const detail = await getActivityDetail(id);
|
||||
if (detail) {
|
||||
activity.value = detail;
|
||||
} else {
|
||||
uni.showToast({ title: '组局不存在或已下架', icon: 'none' });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[ActivityDetail] 获取详情失败:', err);
|
||||
uni.showToast({ title: err.message || '组局加载失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -559,14 +563,14 @@ const setQuickPrice = (val) => {
|
||||
};
|
||||
|
||||
const joinedCount = computed(() => {
|
||||
if (activity.value && activity.value.participants) {
|
||||
if (activity.value && Array.isArray(activity.value.participants)) {
|
||||
return activity.value.participants.length;
|
||||
}
|
||||
return 2;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const currentPerPerson = computed(() => {
|
||||
return Number(activity.value.perPersonFee) || 500;
|
||||
return Number(activity.value.perPersonFee) || 0;
|
||||
});
|
||||
|
||||
const appendQuickOptions = computed(() => {
|
||||
@@ -631,7 +635,8 @@ const confirmAdjustPrice = async () => {
|
||||
uni.hideLoading();
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
console.warn('[ActivityDetail] 远程调价异常,执行本地同步:', err.message);
|
||||
uni.showToast({ title: err.message || '调价失败,请稍后重试', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
activity.value.perPersonFee = newP;
|
||||
@@ -686,44 +691,45 @@ const copyLocation = () => {
|
||||
|
||||
const openHostProfile = () => {
|
||||
const hostId = activity.value.creatorId || 1001;
|
||||
const hostName = activity.value.creatorNickname || '同城局长';
|
||||
const hostName = activity.value.creatorNickname || '同城用户';
|
||||
const hostAvatar = activity.value.creatorAvatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150';
|
||||
const hostSnapshot = {
|
||||
id: hostId,
|
||||
userId: hostId,
|
||||
nickname: hostName,
|
||||
avatar: hostAvatar,
|
||||
rankTag: '同城局长 · 守约率100%',
|
||||
levelTag: '实名认证',
|
||||
game: activity.value.game || '同城陪伴',
|
||||
city: activity.value.location || '北京市 · 朝阳商圈',
|
||||
bio: `【${activity.value.title || '同城生活组局'}】发起人,历史组局履约守约率 100%,热情好客,欢迎同城搭子一起加入!`,
|
||||
photos: [hostAvatar]
|
||||
rankTag: '发起人',
|
||||
levelTag: '',
|
||||
game: activity.value.game || '',
|
||||
city: activity.value.location || '',
|
||||
bio: activity.value.title ? `【${activity.value.title}】发起人` : '',
|
||||
photos: hostAvatar ? [hostAvatar] : []
|
||||
};
|
||||
uni.setStorageSync('ACTIVE_AUTHOR_PROFILE', hostSnapshot);
|
||||
uni.navigateTo({
|
||||
url: `/pages/expert/detail?id=${encodeURIComponent(String(hostId))}&name=${encodeURIComponent(hostName)}&avatar=${encodeURIComponent(hostAvatar)}&tag=${encodeURIComponent('同城局长 · 守约100%')}&city=${encodeURIComponent(hostSnapshot.city)}`
|
||||
url: `/pages/expert/detail?id=${encodeURIComponent(String(hostId))}&name=${encodeURIComponent(hostName)}&avatar=${encodeURIComponent(hostAvatar)}&tag=${encodeURIComponent('发起人')}&city=${encodeURIComponent(hostSnapshot.city)}`
|
||||
});
|
||||
};
|
||||
|
||||
const chatWithHost = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: [
|
||||
'复制组局联络专属口令',
|
||||
'查看局长主页及守约信誉'
|
||||
'复制组局信息',
|
||||
'查看发起人主页'
|
||||
],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
const hidePhone = uni.getStorageSync('privacy_hide_phone') === '1';
|
||||
const rawPhone = activity.value.contactPhone || '13888886666';
|
||||
const displayContact = hidePhone
|
||||
? rawPhone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') + '(隐私保护已脱敏)'
|
||||
: rawPhone;
|
||||
const tokenText = `【同城组局联络口令】${activity.value.title || '同城组局'} | 时间:${activity.value.startTime || '今天 19:30'} | 地点:${activity.value.location || '公开商圈'} | 联络方式:${displayContact} | 专属核验口令:#JU-${activity.value.id || '101'}-PW`;
|
||||
const parts = [
|
||||
`【组局】${activity.value.title || ''}`,
|
||||
`时间:${activity.value.startTime || '待定'}`,
|
||||
`地点:${activity.value.location || '待定'}`
|
||||
];
|
||||
const contact = String(activity.value.contactPhone || '').trim();
|
||||
if (contact) parts.push(`联络方式:${contact}`);
|
||||
uni.setClipboardData({
|
||||
data: tokenText,
|
||||
data: parts.join(' | '),
|
||||
success: () => {
|
||||
uni.showToast({ title: '专属组局口令已复制', icon: 'success' });
|
||||
uni.showToast({ title: '组局信息已复制', icon: 'success' });
|
||||
}
|
||||
});
|
||||
} else if (res.tapIndex === 1) {
|
||||
@@ -737,9 +743,9 @@ const goToMyTicket = (code) => {
|
||||
if (!ensureLoginOrRedirect(`/pages/activity/detail?id=${currentActId.value}`)) {
|
||||
return;
|
||||
}
|
||||
const c = code || (activity.value.participants && activity.value.participants[0] ? activity.value.participants[0].verifyCode : '8392');
|
||||
const c = code || ((activity.value.participants || []).find(p => Number(p.userId) === Number(currentUserId.value)) || {}).verifyCode || '';
|
||||
uni.navigateTo({
|
||||
url: `/pages/activity/my-ticket?id=${activity.value.id}&code=${c}`
|
||||
url: `/pages/activity/my-ticket?id=${activity.value.id}${c ? `&code=${encodeURIComponent(c)}` : ''}`
|
||||
});
|
||||
};
|
||||
|
||||
@@ -851,19 +857,17 @@ const handleJoin = () => {
|
||||
// 报名前请求一次性订阅授权(组局报名通知)
|
||||
requestSubscribeFor('activity');
|
||||
uni.showLoading({ title: '正在报名生成核销码...' });
|
||||
let code = '8392';
|
||||
let code = '';
|
||||
try {
|
||||
const fee = activity.value.payType === 'AA' ? activity.value.aaFee : 0;
|
||||
const activeUid = currentUserId.value || Number(uni.getStorageSync('userId'));
|
||||
const activeUid = normalizeUserId();
|
||||
const joinRes = await joinActivity(activity.value.id, {
|
||||
userId: activeUid,
|
||||
payAmount: fee,
|
||||
});
|
||||
if (joinRes && joinRes.verifyCode) {
|
||||
code = joinRes.verifyCode;
|
||||
}
|
||||
code = joinRes && joinRes.verifyCode ? joinRes.verifyCode : '';
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '报名成功!已生成4位核销码', icon: 'success' });
|
||||
uni.showToast({ title: '报名成功', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
goToMyTicket(code);
|
||||
}, 1000);
|
||||
|
||||
@@ -176,12 +176,12 @@ import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { onShow, onLoad } from '@dcloudio/uni-app';
|
||||
import AppTabBar from '@/components/CustomTabBar.vue';
|
||||
import SkeletonScreen from '@/components/SkeletonScreen.vue';
|
||||
import { getActivities, getNavCategories } from '@/common/api.js';
|
||||
import { getActivities, getNavCategories, normalizeUserId } from '@/common/api.js';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { getNavBarInfo } from '@/common/nav.js';
|
||||
|
||||
const CURRENT_USER_ID = 1001;
|
||||
const CURRENT_USER_ID = normalizeUserId();
|
||||
|
||||
const isLoading = ref(true);
|
||||
const activities = ref([]);
|
||||
@@ -335,7 +335,7 @@ const filteredActivities = computed(() => {
|
||||
// 2. 支付模式或发单人筛选
|
||||
let matchTab = true;
|
||||
if (currentTab.value === 'MINE') {
|
||||
matchTab = Number(a.creatorId) === CURRENT_USER_ID || a.creatorNickname === '同城局长';
|
||||
matchTab = Number(a.creatorId) === Number(CURRENT_USER_ID);
|
||||
} else if (currentTab.value !== 'ALL') {
|
||||
matchTab = a.payType === currentTab.value;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="ticket-container" :class="['theme-' + currentThemeKey]">
|
||||
<view class="ticket-card">
|
||||
<view class="ticket-card" v-if="hasTicket">
|
||||
<!-- 票据头部 -->
|
||||
<view class="ticket-header">
|
||||
<view class="game-badge">{{ ticketData.game }}</view>
|
||||
@@ -107,22 +107,25 @@
|
||||
</view>
|
||||
|
||||
<!-- 门票操作按钮 -->
|
||||
<view class="action-wrap">
|
||||
<view class="action-wrap" v-if="hasTicket">
|
||||
<button class="btn-cancel-ticket btn-active" v-if="!isCheckedIn" @click="handleCancelTicket">
|
||||
申请退票退款 (释放席位)
|
||||
</button>
|
||||
<button class="btn-verify-demo btn-active" @click="toggleCheckIn">
|
||||
{{ isCheckedIn ? '重置为待核销状态' : '局长现场核销此门票' }}
|
||||
</button>
|
||||
<button class="btn-back btn-active" @click="goBack">返回活动详情</button>
|
||||
</view>
|
||||
|
||||
<view class="ticket-empty" v-else>
|
||||
<text class="ticket-empty-title">暂无可用门票</text>
|
||||
<text class="ticket-empty-desc">报名组局后,专属入场核销码会出现在这里</text>
|
||||
<button class="btn-back btn-active" @click="goBack">去逛同城组局</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { getActivityDetail, verifyActivityCheckin, getMyActivities, leaveActivity } from '@/common/api.js';
|
||||
import { getActivityDetail, getMyActivities, leaveActivity, normalizeUserId } from '@/common/api.js';
|
||||
import { encodeCode128B } from '@/common/barcode.js';
|
||||
import { generateQRCodeMatrix } from '@/common/qrcode.js';
|
||||
import { getCurrentThemeKey } from '@/common/theme.js';
|
||||
@@ -132,28 +135,24 @@ const currentThemeKey = ref(getCurrentThemeKey());
|
||||
const activeCodeType = ref('BARCODE');
|
||||
const ticketData = ref({
|
||||
id: '',
|
||||
title: '同城组局现场门票',
|
||||
game: '同城陪伴',
|
||||
startTime: '根据预约到场',
|
||||
location: '同城商业街区',
|
||||
creatorNickname: '组局房主'
|
||||
title: '',
|
||||
game: '',
|
||||
startTime: '',
|
||||
location: '',
|
||||
creatorNickname: ''
|
||||
});
|
||||
const verifyCode = ref('8392');
|
||||
const verifyCode = ref('');
|
||||
const isCheckedIn = ref(false);
|
||||
const currentTime = ref('');
|
||||
const hasTicket = ref(true);
|
||||
const hasTicket = ref(false);
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
try {
|
||||
return Number(uni.getStorageSync('userId')) || 1001;
|
||||
} catch (_) {
|
||||
return 1001;
|
||||
}
|
||||
});
|
||||
const currentUserId = computed(() => normalizeUserId());
|
||||
|
||||
const ticketSn = computed(() => {
|
||||
const actId = ticketData.value && ticketData.value.id ? ticketData.value.id : '101';
|
||||
return `TICKET-${actId}-${verifyCode.value}`;
|
||||
const actId = ticketData.value && ticketData.value.id ? ticketData.value.id : '';
|
||||
const code = String(verifyCode.value || '');
|
||||
if (!actId || !code) return '';
|
||||
return `TICKET-${actId}-${code}`;
|
||||
});
|
||||
|
||||
const barcodeSegments = computed(() => {
|
||||
@@ -184,9 +183,8 @@ const updateTime = () => {
|
||||
let savedQuery = null;
|
||||
|
||||
const loadTicketData = async (query = savedQuery) => {
|
||||
if (query && query.code) {
|
||||
verifyCode.value = String(query.code);
|
||||
}
|
||||
hasTicket.value = false;
|
||||
verifyCode.value = query && query.code ? String(query.code) : '';
|
||||
|
||||
// 1. 若有指定活动ID,直接拉取该活动详情
|
||||
if (query && query.id) {
|
||||
@@ -194,15 +192,13 @@ const loadTicketData = async (query = savedQuery) => {
|
||||
const detail = await getActivityDetail(query.id);
|
||||
if (detail) {
|
||||
ticketData.value = detail;
|
||||
if (detail.participants && detail.participants.length > 0) {
|
||||
const found = detail.participants.find(p => p.verifyCode === verifyCode.value);
|
||||
if (found) {
|
||||
isCheckedIn.value = Boolean(found.checkedIn);
|
||||
} else if (!query.code) {
|
||||
verifyCode.value = detail.participants[0].verifyCode;
|
||||
isCheckedIn.value = Boolean(detail.participants[0].checkedIn);
|
||||
}
|
||||
const uid = Number(currentUserId.value);
|
||||
const mine = (detail.participants || []).find(p => Number(p.userId) === uid);
|
||||
if (mine) {
|
||||
if (mine.verifyCode) verifyCode.value = String(mine.verifyCode);
|
||||
isCheckedIn.value = Boolean(mine.checkedIn);
|
||||
}
|
||||
hasTicket.value = Boolean(detail.id && verifyCode.value);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -212,43 +208,27 @@ const loadTicketData = async (query = savedQuery) => {
|
||||
|
||||
// 2. 若无传参,真实拉取当前用户的已报名组局列表
|
||||
try {
|
||||
const uid = currentUserId.value;
|
||||
const uid = Number(currentUserId.value);
|
||||
const myActs = await getMyActivities(uid);
|
||||
const joinedList = myActs.joined || [];
|
||||
if (joinedList.length > 0) {
|
||||
let targetAct = joinedList.find(act => {
|
||||
const myP = (act.participants || []).find(p => Number(p.userId) === uid || p.nickname === '同城局长' || p.verifyCode);
|
||||
return myP && !myP.checkedIn;
|
||||
}) || joinedList[0];
|
||||
|
||||
const joinedList = (myActs && myActs.joined) || [];
|
||||
const withTicket = joinedList.filter((act) => {
|
||||
const mine = (act.participants || []).find(p => Number(p.userId) === uid && p.verifyCode);
|
||||
return Boolean(mine);
|
||||
});
|
||||
if (withTicket.length > 0) {
|
||||
const targetAct = withTicket.find((act) => {
|
||||
const mine = (act.participants || []).find(p => Number(p.userId) === uid);
|
||||
return mine && !mine.checkedIn;
|
||||
}) || withTicket[0];
|
||||
const mine = (targetAct.participants || []).find(p => Number(p.userId) === uid) || {};
|
||||
ticketData.value = targetAct;
|
||||
if (targetAct.participants && targetAct.participants.length > 0) {
|
||||
const myP = targetAct.participants.find(p => Number(p.userId) === uid || p.verifyCode);
|
||||
if (myP && myP.verifyCode) {
|
||||
verifyCode.value = String(myP.verifyCode);
|
||||
isCheckedIn.value = Boolean(myP.checkedIn);
|
||||
} else {
|
||||
verifyCode.value = String(targetAct.participants[0].verifyCode || '8392');
|
||||
isCheckedIn.value = Boolean(targetAct.participants[0].checkedIn);
|
||||
}
|
||||
}
|
||||
return;
|
||||
verifyCode.value = String(mine.verifyCode || '');
|
||||
isCheckedIn.value = Boolean(mine.checkedIn);
|
||||
hasTicket.value = Boolean(targetAct.id && verifyCode.value);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[MyTicket] 获取用户报名门票异常:', err);
|
||||
}
|
||||
|
||||
// 3. 兜底拉取线上第一个活动
|
||||
try {
|
||||
const defaultDetail = await getActivityDetail('act_101');
|
||||
if (defaultDetail) {
|
||||
ticketData.value = defaultDetail;
|
||||
if (defaultDetail.participants && defaultDetail.participants.length > 0) {
|
||||
verifyCode.value = String(defaultDetail.participants[0].verifyCode || '8392');
|
||||
isCheckedIn.value = Boolean(defaultDetail.participants[0].checkedIn);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
onLoad(async (query) => {
|
||||
@@ -270,38 +250,6 @@ onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
|
||||
const toggleCheckIn = async () => {
|
||||
if (!isCheckedIn.value) {
|
||||
try {
|
||||
uni.showLoading({ title: '正在核销签到...' });
|
||||
await verifyActivityCheckin(ticketData.value.id, verifyCode.value, currentUserId.value);
|
||||
uni.hideLoading();
|
||||
isCheckedIn.value = true;
|
||||
uni.showToast({
|
||||
title: '4位核销码核销成功,已完成现场签到!',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
// 若线上返回已核销或网络异常,同步状态
|
||||
isCheckedIn.value = true;
|
||||
uni.showToast({
|
||||
title: err.message || '核销完成',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
} else {
|
||||
isCheckedIn.value = false;
|
||||
uni.showToast({
|
||||
title: '状态已重置为待核销',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelTicket = () => {
|
||||
uni.showModal({
|
||||
title: '申请退票并退款?',
|
||||
@@ -313,7 +261,12 @@ const handleCancelTicket = () => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({ title: '正在办理退票...' });
|
||||
const actId = ticketData.value && ticketData.value.id ? ticketData.value.id : 'act_101';
|
||||
const actId = ticketData.value && ticketData.value.id;
|
||||
if (!actId) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '门票信息缺失,无法退票', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
await leaveActivity(actId, currentUserId.value);
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '退票成功,款项已退回钱包', icon: 'success' });
|
||||
@@ -624,6 +577,34 @@ const goBack = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.ticket-empty {
|
||||
margin-top: 80rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 60rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
background: var(--bg-surface, #151D30);
|
||||
border: 1rpx dashed rgba(148, 163, 184, 0.35);
|
||||
|
||||
.ticket-empty-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #F8FAFC);
|
||||
}
|
||||
|
||||
.ticket-empty-desc {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-muted, #94A3B8);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.action-wrap {
|
||||
margin-top: 48rpx;
|
||||
display: flex;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 多角色体验账号矩阵 (局长/买家/金牌大神) -->
|
||||
<view class="demo-accounts-card">
|
||||
<view class="demo-accounts-card" v-if="demoAccounts.length > 0">
|
||||
<view class="demo-card-head">
|
||||
<text class="demo-title">多角色体验账号直登</text>
|
||||
<text class="demo-sub">评审与体验专用 · 一键切换角色</text>
|
||||
@@ -31,12 +31,12 @@
|
||||
|
||||
<view class="demo-list">
|
||||
<view
|
||||
v-for="acc in DEMO_ACCOUNTS"
|
||||
:key="acc.id"
|
||||
v-for="acc in demoAccounts"
|
||||
:key="acc.userId"
|
||||
class="demo-item btn-active"
|
||||
@click="loginWithDemo(acc)"
|
||||
>
|
||||
<image class="demo-avatar" :src="acc.avatar" mode="aspectFill" />
|
||||
<image class="demo-avatar" :src="acc.avatar || DEFAULT_AVATAR" mode="aspectFill" />
|
||||
<view class="demo-info">
|
||||
<view class="demo-name-row">
|
||||
<text class="demo-name">{{ acc.nickname }}</text>
|
||||
@@ -69,7 +69,7 @@ import { ref, onMounted } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { getPlatformConfigs, loginWithWechatPhone } from '@/common/api.js';
|
||||
import { getPlatformConfigs, loginWithWechatPhone, getDemoAccounts, demoLogin } from '@/common/api.js';
|
||||
|
||||
const APP_LOGO = 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=200';
|
||||
const DEFAULT_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200';
|
||||
@@ -107,32 +107,18 @@ const loadProtocolConfig = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const DEMO_ACCOUNTS = [
|
||||
{
|
||||
id: 1001,
|
||||
nickname: '晨曦微光局长',
|
||||
roleTag: 'host',
|
||||
roleDesc: '房主/局长',
|
||||
desc: '余额充足 · 发起项目制/AA组局',
|
||||
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200'
|
||||
},
|
||||
{
|
||||
id: 2001,
|
||||
nickname: '快乐小买家',
|
||||
roleTag: 'buyer',
|
||||
roleDesc: '普通买家',
|
||||
desc: '搭子报名 · 预约陪伴 · 核销码签到',
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200'
|
||||
},
|
||||
{
|
||||
id: 2006,
|
||||
nickname: '苏苏学姐',
|
||||
roleTag: 'expert',
|
||||
roleDesc: '金牌陪伴师',
|
||||
desc: '工作台接单 · 雷达抢单 · 现场核销',
|
||||
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=200'
|
||||
const demoAccounts = ref([]);
|
||||
const loadingDemoAccounts = ref(false);
|
||||
|
||||
const loadDemoAccounts = async () => {
|
||||
if (loadingDemoAccounts.value) return;
|
||||
loadingDemoAccounts.value = true;
|
||||
try {
|
||||
demoAccounts.value = await getDemoAccounts();
|
||||
} finally {
|
||||
loadingDemoAccounts.value = false;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
onLoad((options) => {
|
||||
if (options && options.redirect) {
|
||||
@@ -148,6 +134,7 @@ onMounted(() => {
|
||||
currentThemeKey.value = getCurrentThemeKey();
|
||||
applyThemeToNativeUI(currentThemeKey.value);
|
||||
loadProtocolConfig();
|
||||
loadDemoAccounts();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
@@ -281,7 +268,7 @@ const handleGetPhoneNumber = async (e) => {
|
||||
confirmColor: '#10B981',
|
||||
success: (mRes) => {
|
||||
if (mRes.confirm) {
|
||||
completeLogin(1001);
|
||||
loginFirstDemoAccount();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -289,10 +276,7 @@ const handleGetPhoneNumber = async (e) => {
|
||||
}
|
||||
}
|
||||
|
||||
completeLogin(1001, {
|
||||
nickname: '微信用户',
|
||||
avatar: DEFAULT_AVATAR
|
||||
});
|
||||
loginFirstDemoAccount();
|
||||
} else {
|
||||
console.warn('[WeChat Login] 未授权手机号或环境受限:', e?.detail?.errMsg);
|
||||
uni.showModal({
|
||||
@@ -303,7 +287,7 @@ const handleGetPhoneNumber = async (e) => {
|
||||
confirmColor: '#10B981',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
completeLogin(1001);
|
||||
loginFirstDemoAccount();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -311,7 +295,7 @@ const handleGetPhoneNumber = async (e) => {
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
completeLogin(1001);
|
||||
loginFirstDemoAccount();
|
||||
// #endif
|
||||
};
|
||||
|
||||
@@ -321,19 +305,34 @@ const handleFallbackLogin = () => {
|
||||
uni.showToast({ title: '请先阅读并勾选服务协议', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
completeLogin(1001);
|
||||
loginFirstDemoAccount();
|
||||
// #endif
|
||||
};
|
||||
|
||||
const loginWithDemo = (acc) => {
|
||||
const loginWithDemo = async (acc) => {
|
||||
if (!agreedProtocol.value) {
|
||||
uni.showToast({ title: '请先阅读并勾选服务协议', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
completeLogin(acc.id, {
|
||||
nickname: acc.nickname,
|
||||
avatar: acc.avatar
|
||||
});
|
||||
uni.showLoading({ title: '正在登录...' });
|
||||
try {
|
||||
const res = await demoLogin(acc.userId);
|
||||
uni.hideLoading();
|
||||
completeLogin(res.userId, res.user || {}, res.userToken || '');
|
||||
} catch (e) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: (e && e.message) || '体验登录失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
const loginFirstDemoAccount = async () => {
|
||||
if (!demoAccounts.value.length) await loadDemoAccounts();
|
||||
const first = demoAccounts.value[0];
|
||||
if (!first) {
|
||||
uni.showToast({ title: '体验账号未配置,请联系管理员', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
await loginWithDemo(first);
|
||||
};
|
||||
|
||||
const openUserAgreement = () => {
|
||||
|
||||
@@ -245,7 +245,7 @@ const normalizeCommentItem = (item) => {
|
||||
...item,
|
||||
author: (isMyComment && cur.nickname)
|
||||
? cur.nickname
|
||||
: ((item.author && item.author !== '电竞玩家 (我)') ? item.author : (cur.nickname || '同城局长')),
|
||||
: ((item.author && item.author !== '电竞玩家 (我)') ? item.author : (cur.nickname || '同城用户')),
|
||||
avatar: (isMyComment && cur.avatar)
|
||||
? cur.avatar
|
||||
: ((item.avatar && !item.avatar.includes('photo-1535713875002-d1d0cf377fde'))
|
||||
@@ -342,19 +342,7 @@ const loadComments = async (id) => {
|
||||
loadingComments.value = true;
|
||||
try {
|
||||
const list = await getPostComments(id);
|
||||
if (list && list.length > 0) {
|
||||
commentsList.value = list.map(normalizeCommentItem);
|
||||
} else {
|
||||
commentsList.value = [
|
||||
{
|
||||
id: `cmt_seed_${id}`,
|
||||
author: (post.value && post.value.author) || '小甜橙',
|
||||
avatar: (post.value && post.value.avatar) || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120',
|
||||
content: '欢迎各位小伙伴在评论区留言约伴!一起开黑/探店~',
|
||||
createdAt: '刚刚'
|
||||
}
|
||||
];
|
||||
}
|
||||
commentsList.value = Array.isArray(list) ? list.map(normalizeCommentItem) : [];
|
||||
} catch (_) {
|
||||
commentsList.value = [];
|
||||
} finally {
|
||||
@@ -378,10 +366,8 @@ const openAuthorProfile = (target) => {
|
||||
const authorName = target.author || target.nickname || '同城达人';
|
||||
let targetId = target.expertId || target.companionId || target.userId;
|
||||
if (!targetId) {
|
||||
if (authorName === '小甜橙') targetId = 2005;
|
||||
else if (authorName.includes('暗影刺客')) targetId = 2004;
|
||||
else if (authorName === '苏苏学姐') targetId = 2006;
|
||||
else targetId = target.id || 101;
|
||||
uni.showToast({ title: '该用户信息不完整', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const authorSnapshot = {
|
||||
id: targetId,
|
||||
@@ -391,8 +377,8 @@ const openAuthorProfile = (target) => {
|
||||
rankTag: target.tag || '同城认证达人',
|
||||
levelTag: '实名认证',
|
||||
game: target.category === 'game' ? '车队开黑' : (target.category === 'highlight' ? '战绩高光' : '同城搭子'),
|
||||
city: target.location || '北京市 · 朝阳/海淀',
|
||||
bio: target.content || '热爱同城探店与组局开黑,欢迎一起约伴同行!',
|
||||
city: target.location || '',
|
||||
bio: target.content || '',
|
||||
photos: Array.isArray(target.images) && target.images.length > 0 ? target.images : [target.avatar || DEFAULT_USER_AVATAR]
|
||||
};
|
||||
uni.setStorageSync('ACTIVE_AUTHOR_PROFILE', authorSnapshot);
|
||||
@@ -481,7 +467,7 @@ const sendComment = async () => {
|
||||
? currentUserProfile.value
|
||||
: await syncCurrentUserProfile();
|
||||
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
|
||||
const authorName = profile.nickname || '同城局长';
|
||||
const authorName = profile.nickname || '同城用户';
|
||||
const authorAvatar = profile.avatar || DEFAULT_USER_AVATAR;
|
||||
|
||||
try {
|
||||
@@ -537,7 +523,7 @@ const handleJoinParty = () => {
|
||||
? currentUserProfile.value
|
||||
: await syncCurrentUserProfile();
|
||||
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
|
||||
const applicantName = profile.nickname || '同城局长';
|
||||
const applicantName = profile.nickname || '同城用户';
|
||||
const applicantAvatar = profile.avatar || DEFAULT_USER_AVATAR;
|
||||
try {
|
||||
await applyPostParty(post.value.id, {
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
|
||||
<!-- 动态信息流列表 -->
|
||||
<view class="posts-list">
|
||||
<view v-if="filteredPosts.length === 0" class="posts-empty">
|
||||
<text class="posts-empty-title">还没有动态</text>
|
||||
<text class="posts-empty-desc">发第一条同城搭子动态,约上附近的小伙伴</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="(post, index) in filteredPosts"
|
||||
:key="post.id"
|
||||
@@ -399,7 +403,7 @@ import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { ensureLoginOrRedirect } from '@/common/role.js';
|
||||
import { getNavBarInfo } from '@/common/nav.js';
|
||||
import { getPosts, createPost, deletePost, likePost, viewPost, getPostComments, addPostComment, applyPostParty, getPostApplies, handlePostApply, uploadImage, getPlatformConfigs, getUserProfile } from '@/common/api.js';
|
||||
import { getPosts, createPost, deletePost, likePost, viewPost, getPostComments, addPostComment, applyPostParty, getPostApplies, handlePostApply, uploadImage, getPlatformConfigs, getUserProfile, normalizeUserId } from '@/common/api.js';
|
||||
|
||||
const DEFAULT_USER_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120';
|
||||
const isLoading = ref(true);
|
||||
@@ -529,7 +533,7 @@ onUnmounted(() => {
|
||||
const loadRemotePosts = async () => {
|
||||
try {
|
||||
const list = await getPosts(currentTab.value);
|
||||
if (list && list.length > 0) {
|
||||
{
|
||||
let localLikedIds = uni.getStorageSync('COMMUNITY_LIKED_POST_IDS') || [];
|
||||
if (!Array.isArray(localLikedIds)) localLikedIds = [];
|
||||
let localAppliedIds = uni.getStorageSync('COMMUNITY_APPLIED_POST_IDS') || [];
|
||||
@@ -537,7 +541,7 @@ const loadRemotePosts = async () => {
|
||||
let viewsMap = uni.getStorageSync('COMMUNITY_POST_VIEWS_MAP') || {};
|
||||
if (typeof viewsMap !== 'object' || viewsMap === null) viewsMap = {};
|
||||
|
||||
posts.value = list.map(item => {
|
||||
posts.value = (list || []).map(item => {
|
||||
const strId = String(item.id);
|
||||
const serverStatus = item.myApplyStatus || null;
|
||||
const isApplied = serverStatus ? (serverStatus !== 'REJECTED') : localAppliedIds.includes(strId);
|
||||
@@ -610,75 +614,8 @@ const syncInitialPostStates = (rawPosts) => {
|
||||
};
|
||||
const syncInitialLikedState = syncInitialPostStates;
|
||||
|
||||
const posts = ref(syncInitialPostStates([
|
||||
{
|
||||
id: 1,
|
||||
userId: 2005,
|
||||
author: '小甜橙',
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
|
||||
tag: '火锅达人',
|
||||
tagType: 'warm-tag',
|
||||
location: '朝阳区 · 三里屯太古里',
|
||||
distance: '800m',
|
||||
time: '15分钟前',
|
||||
content: '今晚7点半有没有在三里屯附近的小伙伴一起去吃铜锅涮肉?已预订包间,目前已有2人,再来2个饭搭子AA!吃完还可以顺路去清吧听歌放松~',
|
||||
topics: ['三里屯探店', '铜锅涮肉AA', '饭搭子招募'],
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=500',
|
||||
'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500'
|
||||
],
|
||||
actionText: '加入车队',
|
||||
likes: 42,
|
||||
liked: false,
|
||||
comments: 8,
|
||||
views: 310,
|
||||
category: 'partner'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userId: 2004,
|
||||
author: '暗影刺客 (省一李白)',
|
||||
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150',
|
||||
tag: '国服大神',
|
||||
tagType: 'cool-tag',
|
||||
location: '海淀区 · 五道口',
|
||||
distance: '3.5km',
|
||||
time: '40分钟前',
|
||||
content: '今晚王者荣耀荣耀王者冲分车队!目前缺一个听指挥的中单或者软辅妹子,全程语音沟通包赢不保段包赔,想上星的速速滴我!',
|
||||
topics: ['王者荣耀', '荣耀车队', '连胜冲分'],
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=500'
|
||||
],
|
||||
actionText: '立即上车',
|
||||
likes: 76,
|
||||
liked: true,
|
||||
comments: 15,
|
||||
views: 680,
|
||||
category: 'game'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
userId: 2006,
|
||||
author: '苏苏学姐',
|
||||
avatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=150',
|
||||
tag: '金牌麦霸',
|
||||
tagType: 'gold-tag',
|
||||
location: '朝阳区 · 工体商业街',
|
||||
distance: '1.2km',
|
||||
time: '1小时前',
|
||||
content: '工体纯K量贩欢唱局!甜美情歌、粤语老歌、摇滚气氛拉满。现场备有精美果盘与特饮,今晚欢唱不嗨不散,还有专属歌声试听哦!',
|
||||
topics: ['KTV欢唱', '量贩陪唱', '同城聚会'],
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?w=500'
|
||||
],
|
||||
actionText: '报名同去',
|
||||
likes: 58,
|
||||
liked: false,
|
||||
comments: 11,
|
||||
views: 520,
|
||||
category: 'partner'
|
||||
}
|
||||
]));
|
||||
const posts = ref([]);
|
||||
|
||||
|
||||
const filteredPosts = computed(() => {
|
||||
if (currentTab.value === 'all') return posts.value;
|
||||
@@ -761,19 +698,7 @@ const loadPostCommentsList = async (post) => {
|
||||
loadingComments.value = true;
|
||||
try {
|
||||
const list = await getPostComments(post.id);
|
||||
if (list && list.length > 0) {
|
||||
commentsList.value = list;
|
||||
} else {
|
||||
commentsList.value = [
|
||||
{
|
||||
id: `cmt_seed_${post.id}`,
|
||||
author: post.author || '小甜橙',
|
||||
avatar: post.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120',
|
||||
content: '欢迎各位小伙伴在评论区留言约伴!一起开黑/探店~',
|
||||
createdAt: '刚刚'
|
||||
}
|
||||
];
|
||||
}
|
||||
commentsList.value = Array.isArray(list) ? list : [];
|
||||
} catch (_) {
|
||||
commentsList.value = [];
|
||||
} finally {
|
||||
@@ -786,10 +711,8 @@ const openAuthorProfile = (target) => {
|
||||
const authorName = target.author || target.nickname || '同城达人';
|
||||
let targetId = target.expertId || target.companionId || target.userId;
|
||||
if (!targetId) {
|
||||
if (authorName === '小甜橙') targetId = 2005;
|
||||
else if (authorName.includes('暗影刺客')) targetId = 2004;
|
||||
else if (authorName === '苏苏学姐') targetId = 2006;
|
||||
else targetId = target.id || 101;
|
||||
uni.showToast({ title: '该用户信息不完整', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const authorSnapshot = {
|
||||
id: targetId,
|
||||
@@ -799,8 +722,8 @@ const openAuthorProfile = (target) => {
|
||||
rankTag: target.tag || '同城认证达人',
|
||||
levelTag: '实名认证',
|
||||
game: target.category === 'game' ? '车队开黑' : (target.category === 'highlight' ? '战绩高光' : '同城搭子'),
|
||||
city: target.location || '北京市 · 朝阳/海淀',
|
||||
bio: target.content || '热爱同城探店与组局开黑,欢迎一起约伴同行!',
|
||||
city: target.location || '',
|
||||
bio: target.content || '',
|
||||
photos: Array.isArray(target.images) && target.images.length > 0 ? target.images : [target.avatar || DEFAULT_USER_AVATAR]
|
||||
};
|
||||
uni.setStorageSync('ACTIVE_AUTHOR_PROFILE', authorSnapshot);
|
||||
@@ -866,7 +789,7 @@ const sendComment = async () => {
|
||||
? currentUserProfile.value
|
||||
: await syncCurrentUserProfile();
|
||||
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
|
||||
const authorName = profile.nickname || '同城局长';
|
||||
const authorName = profile.nickname || '同城用户';
|
||||
const authorAvatar = profile.avatar || DEFAULT_USER_AVATAR;
|
||||
|
||||
try {
|
||||
@@ -1010,7 +933,7 @@ const handleJoinParty = (post) => {
|
||||
? currentUserProfile.value
|
||||
: await syncCurrentUserProfile();
|
||||
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
|
||||
const applicantName = profile.nickname || '同城局长';
|
||||
const applicantName = profile.nickname || '同城用户';
|
||||
const applicantAvatar = profile.avatar || DEFAULT_USER_AVATAR;
|
||||
try {
|
||||
await applyPostParty(post.id, {
|
||||
@@ -1063,9 +986,9 @@ const publishCategories = [
|
||||
const newPostForm = ref({
|
||||
category: 'partner',
|
||||
content: '',
|
||||
selectedTopics: ['三里屯探店'],
|
||||
selectedTopics: [],
|
||||
images: [],
|
||||
location: '朝阳区 · 三里屯太古里'
|
||||
location: ''
|
||||
});
|
||||
|
||||
const contentPlaceholder = computed(() => {
|
||||
@@ -1173,9 +1096,9 @@ const submitPublishPost = async () => {
|
||||
const profile = currentUserProfile.value.nickname
|
||||
? currentUserProfile.value
|
||||
: await syncCurrentUserProfile();
|
||||
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
|
||||
const authorName = profile.nickname || '同城局长';
|
||||
const authorAvatar = profile.avatar || DEFAULT_USER_AVATAR;
|
||||
const uid = normalizeUserId();
|
||||
const authorName = profile.nickname || '同城用户';
|
||||
const authorAvatar = profile.avatar || '';
|
||||
|
||||
let finalImages = [];
|
||||
try {
|
||||
@@ -1183,14 +1106,12 @@ const submitPublishPost = async () => {
|
||||
finalImages = await Promise.all(
|
||||
newPostForm.value.images.map(img => uploadImage(img))
|
||||
);
|
||||
} else {
|
||||
finalImages = ['https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500'];
|
||||
}
|
||||
} catch (uploadErr) {
|
||||
console.warn('[Community] 图片上传异常,保留原地址:', uploadErr);
|
||||
finalImages = newPostForm.value.images.length > 0
|
||||
? newPostForm.value.images
|
||||
: ['https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500'];
|
||||
console.warn('[Community] 图片上传失败:', uploadErr);
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '图片上传失败,请重试后再发布', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const postPayload = {
|
||||
@@ -1209,15 +1130,18 @@ const submitPublishPost = async () => {
|
||||
|
||||
try {
|
||||
const res = await createPost(postPayload);
|
||||
if (!res || !res.id) {
|
||||
throw new Error('发布失败:服务端未返回动态编号');
|
||||
}
|
||||
const createdPost = {
|
||||
id: res ? res.id : Date.now(),
|
||||
id: res.id,
|
||||
userId: uid,
|
||||
author: (res && res.author) || postPayload.author,
|
||||
avatar: (res && res.avatar) || postPayload.avatar,
|
||||
tag: currentMeta.text,
|
||||
tagType: currentMeta.type,
|
||||
location: postPayload.location,
|
||||
distance: '100m内',
|
||||
distance: '',
|
||||
time: '刚刚',
|
||||
content: content,
|
||||
topics: postPayload.topics,
|
||||
@@ -1706,6 +1630,29 @@ const submitPublishPost = async () => {
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
|
||||
.posts-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
padding: 80rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
border: 1rpx dashed rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
|
||||
.posts-empty-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #E2E8F0;
|
||||
}
|
||||
|
||||
.posts-empty-desc {
|
||||
font-size: 24rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
}
|
||||
|
||||
.post-card {
|
||||
background: #151D30;
|
||||
border-radius: 24rpx;
|
||||
|
||||
@@ -291,45 +291,24 @@ const stopPropagation = (e) => {
|
||||
|
||||
const isLoading = ref(true);
|
||||
const isFav = ref(false);
|
||||
const buildDefaultServices = (gameTitle = '同城陪伴', basePrice = 68) => [
|
||||
{
|
||||
id: 'srv_1',
|
||||
name: `${gameTitle} · 基础陪伴 (2小时)`,
|
||||
title: `${gameTitle} · 基础陪伴 (2小时)`,
|
||||
price: basePrice,
|
||||
unit: '小时',
|
||||
desc: '正规公开商圈同城陪伴,AA制或协商消费'
|
||||
},
|
||||
{
|
||||
id: 'srv_2',
|
||||
name: `${gameTitle} · 半日畅享 (3小时)`,
|
||||
title: `${gameTitle} · 半日畅享 (3小时)`,
|
||||
price: Math.round(basePrice * 2.8),
|
||||
unit: '次',
|
||||
desc: '深度互动与陪伴交流,赠送专属点歌或语音祝福'
|
||||
}
|
||||
];
|
||||
|
||||
const expert = ref({
|
||||
id: '101',
|
||||
nickname: '同城陪伴师',
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300',
|
||||
gender: 'female',
|
||||
id: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
gender: '',
|
||||
levelTag: '',
|
||||
game: '同城陪伴',
|
||||
game: '',
|
||||
rankTag: '',
|
||||
city: '',
|
||||
rating: 0,
|
||||
orderCount: 0,
|
||||
isOnline: false,
|
||||
isOnline: false,
|
||||
bio: '',
|
||||
voiceDuration: 0,
|
||||
price: 0,
|
||||
services: [],
|
||||
photos: [
|
||||
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400',
|
||||
'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400'
|
||||
],
|
||||
photos: [],
|
||||
reviews: []
|
||||
});
|
||||
// 是否为真实大神档案(非合成的普通用户资料)
|
||||
@@ -374,31 +353,32 @@ const toggleFavorite = async () => {
|
||||
};
|
||||
|
||||
const handleReplyReview = (rev) => {
|
||||
const targetId = Number(expert.value.id || currentExpertTargetId || 101);
|
||||
const targetId = Number(expert.value.id || currentExpertTargetId || 0);
|
||||
if (!ensureLoginOrRedirect(`/pages/expert/detail?id=${targetId}`)) {
|
||||
return;
|
||||
}
|
||||
const defaultReplies = [
|
||||
'感谢老板的支持与五星好评!开黑配合非常默契,期待下次一起上大分!',
|
||||
'谢谢老板认可!非常准时礼貌的高素质玩家老板,下次继续开黑走起~',
|
||||
'多谢老板认可!服务过程非常愉快,有空随时来约局呀!'
|
||||
];
|
||||
uni.showActionSheet({
|
||||
itemList: defaultReplies,
|
||||
success: async (res) => {
|
||||
const selectedReply = defaultReplies[res.tapIndex] || defaultReplies[0];
|
||||
uni.showModal({
|
||||
title: '回复评价',
|
||||
editable: true,
|
||||
placeholderText: '请输入回复内容',
|
||||
confirmText: '提交回复',
|
||||
success: async (modalRes) => {
|
||||
const reply = String(modalRes.content || '').trim();
|
||||
if (!modalRes.confirm) return;
|
||||
if (!reply) {
|
||||
uni.showToast({ title: '回复内容不能为空', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
uni.showLoading({ title: '正在提交回复...' });
|
||||
await replyOrderReview(rev.id, selectedReply);
|
||||
rev.replyContent = selectedReply;
|
||||
await replyOrderReview(rev.id, reply);
|
||||
rev.replyContent = reply;
|
||||
rev.replyTime = '刚刚';
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '感谢回复已送达', icon: 'success' });
|
||||
uni.showToast({ title: '回复已提交', icon: 'success' });
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
rev.replyContent = selectedReply;
|
||||
rev.replyTime = '刚刚';
|
||||
uni.showToast({ title: '感谢回复已送达', icon: 'success' });
|
||||
uni.showToast({ title: err.message || '回复提交失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -431,12 +411,12 @@ onLoad(async (query) => {
|
||||
const snapAvatar = queryAvatar || (matchedSnapshot && matchedSnapshot.avatar) || expert.value.avatar;
|
||||
const snapTag = queryTag || (matchedSnapshot && matchedSnapshot.rankTag) || expert.value.rankTag;
|
||||
const snapCity = queryCity || (matchedSnapshot && matchedSnapshot.city) || expert.value.city;
|
||||
const snapGame = (matchedSnapshot && matchedSnapshot.game) || '同城搭子';
|
||||
const snapGame = (matchedSnapshot && matchedSnapshot.game) || '';
|
||||
const snapBio = (matchedSnapshot && matchedSnapshot.bio) || expert.value.bio;
|
||||
const snapPhotos = (matchedSnapshot && Array.isArray(matchedSnapshot.photos) && matchedSnapshot.photos.length > 0)
|
||||
? matchedSnapshot.photos
|
||||
: [snapAvatar];
|
||||
const snapPrice = snapGame === '车队开黑' ? 88 : 128;
|
||||
: (snapAvatar ? [snapAvatar] : []);
|
||||
const snapPrice = Number(expert.value.price) || 0;
|
||||
|
||||
expert.value = {
|
||||
...expert.value,
|
||||
@@ -445,13 +425,13 @@ onLoad(async (query) => {
|
||||
avatar: snapAvatar,
|
||||
coverImage: snapPhotos[0] || snapAvatar,
|
||||
rankTag: snapTag,
|
||||
levelTag: '实名认证',
|
||||
levelTag: '',
|
||||
game: snapGame,
|
||||
city: snapCity,
|
||||
bio: snapBio,
|
||||
price: snapPrice,
|
||||
photos: snapPhotos,
|
||||
services: buildDefaultServices(snapGame, snapPrice)
|
||||
services: []
|
||||
};
|
||||
if (expert.value.services && expert.value.services.length > 0) {
|
||||
selectedServiceId.value = expert.value.services[0].id;
|
||||
@@ -553,19 +533,9 @@ const toggleVoice = () => {
|
||||
|
||||
const chatOnline = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: [
|
||||
`复制大神游戏开黑专属房号`,
|
||||
`联系 7x24h 官方在线客服`
|
||||
],
|
||||
itemList: [`联系 7x24h 官方在线客服`],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.setClipboardData({
|
||||
data: `PW-${expert.value.id}-${Math.floor(1000 + Math.random() * 9000)}`,
|
||||
success: () => {
|
||||
uni.showToast({ title: '专属房号已复制', icon: 'success' });
|
||||
}
|
||||
});
|
||||
} else if (res.tapIndex === 1) {
|
||||
uni.showActionSheet({
|
||||
itemList: [
|
||||
'拨打官方服务热线 (400-888-6666)',
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<text class="expert-name">{{ expert.nickname }}</text>
|
||||
<text class="pro-badge" :class="{ offline: !isOnline }">{{ isOnline ? '接单中' : '已打烊' }}</text>
|
||||
</view>
|
||||
<text class="rating-sub">评分 {{ expert.rating || '5.0' }} · 接单率 {{ expert.orderRate || '99.2%' }}</text>
|
||||
<text class="rating-sub">评分 {{ expert.rating || '暂无' }} · 接单率 {{ expert.orderRate || '—' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -243,14 +243,13 @@ const getInitialExpert = () => {
|
||||
try {
|
||||
cached = uni.getStorageSync('PW_USER_INFO') || uni.getStorageSync('userInfo');
|
||||
} catch (_) {}
|
||||
let uid = (cached && cached.id) || uni.getStorageSync('userId') || 2006;
|
||||
if (uid == 101) uid = 2006;
|
||||
const uid = (cached && cached.id) || Number(uni.getStorageSync('userId')) || 0;
|
||||
return {
|
||||
id: uid,
|
||||
nickname: (cached && cached.nickname) || '接单大神',
|
||||
avatar: (cached && cached.avatar) || DEFAULT_EXPERT_AVATAR,
|
||||
rating: (cached && cached.rating) || '5.0',
|
||||
orderRate: (cached && cached.orderRate) || '99.2%'
|
||||
nickname: (cached && cached.nickname) || '',
|
||||
avatar: (cached && cached.avatar) || '',
|
||||
rating: (cached && cached.rating) || '',
|
||||
orderRate: (cached && cached.orderRate) || ''
|
||||
};
|
||||
};
|
||||
|
||||
@@ -272,9 +271,9 @@ const navStyle = computed(() => {
|
||||
});
|
||||
|
||||
const todayStats = ref({
|
||||
income: '380.00',
|
||||
orders: 4,
|
||||
waitingVerify: 1
|
||||
income: '0.00',
|
||||
orders: 0,
|
||||
waitingVerify: 0
|
||||
});
|
||||
|
||||
const onThemeChanged = (newTheme) => {
|
||||
|
||||
@@ -265,9 +265,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="quick-demo" @click="fillDemoCode">
|
||||
<text>点击自动填入演示码 [8392] 并触发核销</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -497,9 +494,9 @@ import {
|
||||
const currentThemeKey = ref(getCurrentThemeKey());
|
||||
|
||||
const expert = ref({
|
||||
id: 101,
|
||||
nickname: '认证大神',
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300'
|
||||
id: 0,
|
||||
nickname: '',
|
||||
avatar: ''
|
||||
});
|
||||
const isReceivingOrders = ref(uni.getStorageSync('EXPERT_RECEIVING_STATUS') !== '0');
|
||||
const todayIncome = ref('0.00');
|
||||
@@ -519,35 +516,7 @@ const rawTrendsData = ref({ daily: [], weekly: [], monthly: [] });
|
||||
const activeTooltip = ref(null);
|
||||
|
||||
const currentTrendsList = computed(() => {
|
||||
const list = rawTrendsData.value[trendPeriod.value] || [];
|
||||
if (list.length > 0) return list;
|
||||
if (trendPeriod.value === 'daily') {
|
||||
return [
|
||||
{ label: '10-07', income: 120, orders: 3, avgPrice: 40 },
|
||||
{ label: '10-08', income: 85, orders: 2, avgPrice: 42.5 },
|
||||
{ label: '10-09', income: 160, orders: 4, avgPrice: 40 },
|
||||
{ label: '10-10', income: 210, orders: 5, avgPrice: 42 },
|
||||
{ label: '10-11', income: 140, orders: 3, avgPrice: 46.7 },
|
||||
{ label: '10-12', income: 290, orders: 6, avgPrice: 48.3 },
|
||||
{ label: '10-13', income: 180, orders: 4, avgPrice: 45 }
|
||||
];
|
||||
} else if (trendPeriod.value === 'weekly') {
|
||||
return [
|
||||
{ label: '3周前', income: 950, orders: 22, avgPrice: 43.2 },
|
||||
{ label: '2周前', income: 1120, orders: 25, avgPrice: 44.8 },
|
||||
{ label: '上周', income: 1350, orders: 28, avgPrice: 48.2 },
|
||||
{ label: '本周', income: 890, orders: 19, avgPrice: 46.8 }
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{ label: '5月', income: 3200, orders: 75, avgPrice: 42.7 },
|
||||
{ label: '6月', income: 3800, orders: 86, avgPrice: 44.2 },
|
||||
{ label: '7月', income: 4500, orders: 98, avgPrice: 45.9 },
|
||||
{ label: '8月', income: 4200, orders: 90, avgPrice: 46.7 },
|
||||
{ label: '9月', income: 5100, orders: 105, avgPrice: 48.6 },
|
||||
{ label: '10月', income: 2800, orders: 58, avgPrice: 48.3 }
|
||||
];
|
||||
}
|
||||
return rawTrendsData.value[trendPeriod.value] || [];
|
||||
});
|
||||
|
||||
const currentPeriodSummary = computed(() => {
|
||||
@@ -813,63 +782,22 @@ const showConfirmModal = ref(false);
|
||||
|
||||
|
||||
const verifiedParticipant = ref({
|
||||
nickname: '电竞萌新小可爱',
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop',
|
||||
activityTitle: 'AA制五排王者百星车队',
|
||||
code: '8392',
|
||||
fee: '25.00'
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
activityTitle: '',
|
||||
code: '',
|
||||
fee: '0.00'
|
||||
});
|
||||
|
||||
const poolOrders = ref([
|
||||
{
|
||||
id: 'pool_101',
|
||||
game: '王者荣耀',
|
||||
price: 35,
|
||||
desc: 'QQ区/星耀1晋级赛冲王者/需开黑配合/射手或辅助',
|
||||
countdown: 48
|
||||
},
|
||||
{
|
||||
id: 'pool_102',
|
||||
game: '和平精英',
|
||||
price: 45,
|
||||
desc: '微信区/四排海岛/刚枪上分/kd保4以上',
|
||||
countdown: 32
|
||||
},
|
||||
{
|
||||
id: 'pool_103',
|
||||
game: '美食陪吃',
|
||||
price: 68,
|
||||
desc: '朝阳大悦城海底捞/周末饭搭子/消费现场AA/需守时礼貌',
|
||||
countdown: 56
|
||||
}
|
||||
]);
|
||||
const poolOrders = ref([]);
|
||||
|
||||
const activeOrders = ref([
|
||||
{
|
||||
id: 'ord_active_1',
|
||||
buyerName: '超能电竞玩家',
|
||||
buyerAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&h=150&fit=crop',
|
||||
serviceTitle: '排位包胜率',
|
||||
hours: 2,
|
||||
totalAmount: 70,
|
||||
statusText: '正在开黑对局履约中'
|
||||
},
|
||||
{
|
||||
id: 'ord_active_2',
|
||||
buyerName: '北京吃货小张',
|
||||
buyerAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop',
|
||||
serviceTitle: '火锅探店饭搭子',
|
||||
hours: 2,
|
||||
totalAmount: 136,
|
||||
statusText: '已核销4位码入座,服务中'
|
||||
}
|
||||
]);
|
||||
const activeOrders = ref([]);
|
||||
|
||||
const toggleReceivingStatus = async () => {
|
||||
const next = !isReceivingOrders.value;
|
||||
const newStatusStr = next ? '接单中' : '休息中';
|
||||
try {
|
||||
await updateCompanionProfile(expert.value.id || 101, { status: newStatusStr });
|
||||
await updateCompanionProfile(currentExpertId(), { status: newStatusStr });
|
||||
} catch (err) {
|
||||
uni.showToast({
|
||||
title: (err && err.message) || '接单状态切换失败,请稍后重试',
|
||||
@@ -905,16 +833,12 @@ const clearCode = () => {
|
||||
isCodeError.value = false;
|
||||
};
|
||||
|
||||
const fillDemoCode = () => {
|
||||
inputDigits.value = [8, 3, 9, 2];
|
||||
verifyInputCode();
|
||||
};
|
||||
|
||||
const loadWorkspaceOrders = async () => {
|
||||
try {
|
||||
// 先解析真实大神档案,避免工作台使用演示 id
|
||||
await loadExpertProfile();
|
||||
const eid = currentExpertId() || 101;
|
||||
const eid = currentExpertId();
|
||||
if (!eid) return;
|
||||
const stats = await getExpertStats(eid);
|
||||
if (stats) {
|
||||
if (stats.income !== undefined) todayIncome.value = stats.income;
|
||||
@@ -950,7 +874,7 @@ const loadWorkspaceOrders = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const trends = await getExpertRevenueTrends(currentExpertId() || 101);
|
||||
const trends = await getExpertRevenueTrends(currentExpertId());
|
||||
if (trends && (trends.daily || trends.weekly || trends.monthly)) {
|
||||
rawTrendsData.value = trends;
|
||||
}
|
||||
@@ -1002,11 +926,11 @@ const verifyInputCode = async () => {
|
||||
}
|
||||
verifiedParticipant.value = {
|
||||
id: info.id,
|
||||
nickname: info.nickname || '同城搭子',
|
||||
avatar: info.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
|
||||
activityTitle: info.title || '同城生活组局',
|
||||
nickname: info.nickname || '同城用户',
|
||||
avatar: info.avatar || '',
|
||||
activityTitle: info.title || '',
|
||||
code: info.code,
|
||||
fee: info.fee || '50.00',
|
||||
fee: info.fee !== undefined && info.fee !== null ? String(info.fee) : '0.00',
|
||||
};
|
||||
isCodeError.value = false;
|
||||
showConfirmModal.value = true;
|
||||
@@ -1025,7 +949,7 @@ const confirmCheckin = async () => {
|
||||
const codeStr = verifiedParticipant.value.code;
|
||||
uni.showLoading({ title: '确认核销中...' });
|
||||
try {
|
||||
await universalVerifyCheckin(codeStr, 1001);
|
||||
await universalVerifyCheckin(codeStr, normalizeUserId());
|
||||
uni.hideLoading();
|
||||
showConfirmModal.value = false;
|
||||
const gainedFee = Number(verifiedParticipant.value.fee) || 25;
|
||||
@@ -1048,7 +972,7 @@ const confirmCheckin = async () => {
|
||||
const grabOrder = async (order) => {
|
||||
try {
|
||||
await claimOrder(order.id, {
|
||||
expertId: currentExpertId() || 101,
|
||||
expertId: currentExpertId(),
|
||||
companionName: '我 (接单大神)',
|
||||
expertAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150'
|
||||
});
|
||||
@@ -1077,7 +1001,7 @@ const handleRejectOrder = (order) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await rejectOrder(order.id, {
|
||||
expertId: expert.value.id || 101,
|
||||
expertId: currentExpertId(),
|
||||
reason: '大神当前档期冲突拒单'
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -1102,19 +1026,9 @@ const handleRejectOrder = (order) => {
|
||||
|
||||
const chatWithBuyer = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: [
|
||||
'复制开黑联络码',
|
||||
'联系 7x24h 官方客服'
|
||||
],
|
||||
itemList: ['联系 7x24h 官方客服'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.setClipboardData({
|
||||
data: `BUYER-${Math.floor(100000 + Math.random() * 900000)}`,
|
||||
success: () => {
|
||||
uni.showToast({ title: '联络码已复制', icon: 'success' });
|
||||
}
|
||||
});
|
||||
} else if (res.tapIndex === 1) {
|
||||
uni.showActionSheet({
|
||||
itemList: [
|
||||
'拨打官方服务热线 (400-888-6666)',
|
||||
@@ -1151,7 +1065,7 @@ const completeOrder = (id) => {
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await applyCompleteOrder(id, { expertId: expert.value.id || 101 });
|
||||
await applyCompleteOrder(id, { expertId: currentExpertId() });
|
||||
} catch (err) {
|
||||
uni.showToast({
|
||||
title: (err && err.message) || '提交失败,请稍后重试',
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
<view class="rank-badge" v-else>{{ expert.rankTag }}</view>
|
||||
|
||||
<!-- 语音/歌声试听条 (矢量SVG播放图标,带播放高亮光晕) -->
|
||||
<view class="voice-bar btn-active" :class="{ playing: playingId === expert.id, 'song-bar': expert.voiceType === 'song' }" @click.stop="toggleVoice(expert.id)">
|
||||
<view class="voice-bar btn-active" :class="{ playing: playingId === expert.id, 'song-bar': expert.voiceType === 'song' }" @click.stop="toggleVoice(expert)">
|
||||
<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">
|
||||
@@ -468,6 +468,7 @@ onShow(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('theme-change', onThemeChanged);
|
||||
try { if (homeVoiceAudio) { homeVoiceAudio.destroy(); homeVoiceAudio = null; } } catch (_) {}
|
||||
});
|
||||
|
||||
const filteredExperts = computed(() => {
|
||||
@@ -584,16 +585,19 @@ const startInstantMatch = () => {
|
||||
if (matchTimer) clearTimeout(matchTimer);
|
||||
matchTimer = setTimeout(() => {
|
||||
isMatching.value = false;
|
||||
// 优先匹配当前列表中在线的大神,若无则取首位
|
||||
// 按真实在线状态与距离推荐(有 distanceKm 时取最近),不做随机抽取
|
||||
const onlineList = experts.value.filter(e => e.isOnline);
|
||||
if (onlineList.length > 0) {
|
||||
matchedExpert.value = onlineList[Math.floor(Math.random() * onlineList.length)];
|
||||
} else if (experts.value.length > 0) {
|
||||
matchedExpert.value = experts.value[0];
|
||||
} else {
|
||||
matchedExpert.value = null;
|
||||
}
|
||||
}, 1200);
|
||||
const candidates = onlineList.length > 0 ? onlineList : experts.value;
|
||||
const sorted = [...candidates].sort((a, b) => {
|
||||
const da = Number(a.distanceKm);
|
||||
const db = Number(b.distanceKm);
|
||||
if (Number.isFinite(da) && Number.isFinite(db)) return da - db;
|
||||
if (Number.isFinite(da)) return -1;
|
||||
if (Number.isFinite(db)) return 1;
|
||||
return 0;
|
||||
});
|
||||
matchedExpert.value = sorted[0] || null;
|
||||
}, 600);
|
||||
};
|
||||
|
||||
const closeMatchModal = () => {
|
||||
@@ -613,16 +617,31 @@ const confirmMatchedOrder = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleVoice = (id) => {
|
||||
if (playingId.value === id) {
|
||||
playingId.value = null;
|
||||
} else {
|
||||
playingId.value = id;
|
||||
uni.showToast({ title: '正在试听打招呼语音', icon: 'none' });
|
||||
setTimeout(() => {
|
||||
if (playingId.value === id) playingId.value = null;
|
||||
}, 4000);
|
||||
let homeVoiceAudio = null;
|
||||
const toggleVoice = (expertItem) => {
|
||||
const id = expertItem && expertItem.id !== undefined ? expertItem.id : expertItem;
|
||||
const url = expertItem && expertItem.voiceIntroUrl;
|
||||
if (!url) {
|
||||
uni.showToast({ title: '该大神还未上传语音介绍', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (playingId.value === id) {
|
||||
try { homeVoiceAudio && homeVoiceAudio.stop(); } catch (_) {}
|
||||
playingId.value = null;
|
||||
return;
|
||||
}
|
||||
try { homeVoiceAudio && homeVoiceAudio.stop(); } catch (_) {}
|
||||
if (!homeVoiceAudio) {
|
||||
homeVoiceAudio = uni.createInnerAudioContext();
|
||||
homeVoiceAudio.onEnded(() => { playingId.value = null; });
|
||||
homeVoiceAudio.onError(() => {
|
||||
playingId.value = null;
|
||||
uni.showToast({ title: '语音播放失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
homeVoiceAudio.src = url;
|
||||
homeVoiceAudio.play();
|
||||
playingId.value = id;
|
||||
};
|
||||
|
||||
const goToExpert = (id) => {
|
||||
|
||||
@@ -286,7 +286,7 @@ import { ensureLoginOrRedirect } from '@/common/role.js';
|
||||
|
||||
const currentThemeKey = ref(getCurrentThemeKey());
|
||||
const currentUserId = ref(Number(uni.getStorageSync('userId') || 1001));
|
||||
const currentUserName = ref((uni.getStorageSync('userInfo') && uni.getStorageSync('userInfo').nickname) || '超能电竞玩家');
|
||||
const currentUserName = ref((uni.getStorageSync('userInfo') && uni.getStorageSync('userInfo').nickname) || '同城用户');
|
||||
const userBalance = ref('0.00');
|
||||
const payMethod = ref('WECHAT'); // 'WECHAT' | 'BALANCE'
|
||||
|
||||
@@ -346,25 +346,25 @@ const loadUserWallet = async () => {
|
||||
|
||||
const expert = ref({
|
||||
id: '',
|
||||
nickname: '同城陪伴师',
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
|
||||
game: '同城陪伴',
|
||||
price: 68,
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
game: '',
|
||||
price: 0,
|
||||
services: []
|
||||
});
|
||||
const selectedService = ref(null);
|
||||
// 模板安全视图:即便服务包尚未加载完成,也不会出现 null.title 崩溃
|
||||
// 模板安全视图:服务包未加载时展示中性占位,不伪造价格/服务名
|
||||
const serviceView = computed(() => selectedService.value || {
|
||||
id: 'base',
|
||||
title: '同城陪伴基础服务',
|
||||
price: Number(expert.value && expert.value.price) || 35,
|
||||
id: '',
|
||||
title: '暂未选择服务',
|
||||
price: 0,
|
||||
unit: '小时'
|
||||
});
|
||||
const hours = ref(2);
|
||||
const timeType = ref('NOW');
|
||||
const discount = ref(0);
|
||||
const remark = ref('');
|
||||
const meetVenue = ref('朝阳大悦城海底捞');
|
||||
const meetVenue = ref('');
|
||||
const meetCoords = ref({ latitude: null, longitude: null });
|
||||
|
||||
// 下单模式:EXPERT=指定大神,PLATFORM=平台派单(发布需求,附近大神抢单)
|
||||
@@ -425,7 +425,13 @@ const isOffline = computed(() => {
|
||||
});
|
||||
|
||||
onLoad(async (query) => {
|
||||
const targetId = (query && query.expertId) ? query.expertId : '1';
|
||||
const targetId = (query && query.expertId) ? String(query.expertId) : '';
|
||||
if (!targetId) {
|
||||
// 未指定大神:直接进入平台派单模式
|
||||
orderMode.value = 'PLATFORM';
|
||||
await loadLiveCoupons();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const liveExpert = await getCompanionDetail(targetId);
|
||||
if (liveExpert) {
|
||||
@@ -440,33 +446,11 @@ onLoad(async (query) => {
|
||||
if (!selectedService.value && services.length > 0) {
|
||||
selectedService.value = services[0];
|
||||
}
|
||||
// 兜底:大神尚未配置服务包时,按其基础时薪提供"基础陪伴服务",保证仍可下单
|
||||
// (价格仍以服务端大神档案价为准,createOrder 会二次校验)
|
||||
if (!selectedService.value) {
|
||||
selectedService.value = {
|
||||
id: 'base',
|
||||
title: `${liveExpert.game || '同城陪伴'}基础陪伴服务`,
|
||||
price: Number(liveExpert.price) || 35,
|
||||
unit: '小时',
|
||||
desc: '大神暂未配置服务包,按基础时薪计费'
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[OrderCreate] 获取线上大神详情异常:', err);
|
||||
}
|
||||
|
||||
// 极端兜底:详情获取失败时也要保证不会因空服务崩溃
|
||||
if (!selectedService.value) {
|
||||
selectedService.value = {
|
||||
id: 'base',
|
||||
title: '同城陪伴基础服务',
|
||||
price: Number(expert.value.price) || 35,
|
||||
unit: '小时',
|
||||
desc: ''
|
||||
};
|
||||
}
|
||||
|
||||
// 加载后台真实可用优惠券
|
||||
await loadLiveCoupons();
|
||||
});
|
||||
@@ -579,6 +563,10 @@ const handlePay = async () => {
|
||||
if (!ensureLoginOrRedirect('/pages/order/create')) {
|
||||
return;
|
||||
}
|
||||
if (!isPlatformMode.value && !selectedService.value) {
|
||||
uni.showToast({ title: '该大神暂未配置服务包,无法下单', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
// 平台派单校验:品类预算与碰面地点
|
||||
if (isPlatformMode.value) {
|
||||
if (!(Number(platformBudget.value) > 0)) {
|
||||
@@ -605,7 +593,7 @@ const handlePay = async () => {
|
||||
const usingUserCoupon = currentCoupon.value && currentCoupon.value.id && currentCoupon.value.id !== 'c_none';
|
||||
const basePayload = {
|
||||
buyerId: uid,
|
||||
clientName: currentUserName.value || '超能电竞玩家',
|
||||
clientName: currentUserName.value || '同城用户',
|
||||
expertId: isPlatform ? 0 : expert.value.id,
|
||||
companionName: isPlatform ? '平台派单' : expert.value.nickname,
|
||||
expertAvatar: isPlatform ? '' : expert.value.avatar,
|
||||
|
||||
@@ -355,7 +355,8 @@ import {
|
||||
submitOrderReview as submitOrderReviewApi,
|
||||
submitReport as submitReportApi,
|
||||
requestWechatPayment,
|
||||
uploadImage
|
||||
uploadImage,
|
||||
normalizeUserId
|
||||
} from '@/common/api.js';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
@@ -547,16 +548,9 @@ const payOrder = async (id) => {
|
||||
|
||||
const contactExpert = (name) => {
|
||||
uni.showActionSheet({
|
||||
itemList: [`复制【${name}】陪玩开黑房间号`, '联系 7x24h 官方电竞客服'],
|
||||
itemList: ['联系 7x24h 官方电竞客服'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.setClipboardData({
|
||||
data: `PW-${Math.floor(100000 + Math.random() * 900000)}`,
|
||||
success: () => {
|
||||
uni.showToast({ title: '开黑房号已复制', icon: 'success' });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uni.showActionSheet({
|
||||
itemList: [
|
||||
'拨打官方服务热线 (400-888-6666)',
|
||||
@@ -593,7 +587,7 @@ const getOrderCountdown = (order) => {
|
||||
if (!isNaN(parsed)) createdAtTs = parsed;
|
||||
}
|
||||
if (!createdAtTs) {
|
||||
createdAtTs = Date.now();
|
||||
return '待支付';
|
||||
}
|
||||
const expireTs = createdAtTs + 15 * 60 * 1000;
|
||||
const diffSec = Math.max(0, Math.floor((expireTs - nowTick.value) / 1000));
|
||||
@@ -885,17 +879,23 @@ const toggleTag = (tag) => {
|
||||
|
||||
const submitReview = async () => {
|
||||
if (!targetReviewOrder.value) return;
|
||||
const content = reviewForm.value.content.trim();
|
||||
if (!content) {
|
||||
uni.showToast({ title: '请填写评价内容', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
submittingReview.value = true;
|
||||
try {
|
||||
const order = targetReviewOrder.value;
|
||||
const expertId = order.expert ? order.expert.id : (order.expertId || 101);
|
||||
const expertId = order.expert ? order.expert.id : (order.expertId || 0);
|
||||
const cachedUser = uni.getStorageSync('userInfo') || {};
|
||||
await submitOrderReviewApi(order.id, {
|
||||
expertId,
|
||||
userId: Number(uni.getStorageSync('userId') || 1001),
|
||||
userNickname: '电竞玩家 (我)',
|
||||
userId: normalizeUserId(),
|
||||
userNickname: cachedUser.nickname || '',
|
||||
rating: reviewForm.value.rating,
|
||||
tags: reviewForm.value.selectedTags,
|
||||
content: reviewForm.value.content.trim() || '大神技术很棒,履约准时,体验绝佳!'
|
||||
content
|
||||
});
|
||||
|
||||
order.isReviewed = true;
|
||||
|
||||
@@ -429,7 +429,7 @@ const loadCouponUsable = async () => {
|
||||
couponUsable.value = 0;
|
||||
}
|
||||
};
|
||||
const cacheSizeText = ref('1.25 MB');
|
||||
const cacheSizeText = ref('0 KB');
|
||||
|
||||
// 分销推广管理
|
||||
const showDistributionModal = ref(false);
|
||||
@@ -446,32 +446,7 @@ const distributionRecords = ref([]);
|
||||
const distWithdrawAmount = ref('');
|
||||
const submittingDistWithdraw = ref(false);
|
||||
|
||||
const DEMO_ACCOUNTS = [
|
||||
{
|
||||
id: 1001,
|
||||
nickname: '同城局长',
|
||||
roleTag: 'host',
|
||||
roleDesc: '房主/局长',
|
||||
desc: '余额充足 · 组局发单',
|
||||
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200'
|
||||
},
|
||||
{
|
||||
id: 2001,
|
||||
nickname: '超能电竞玩家',
|
||||
roleTag: 'buyer',
|
||||
roleDesc: '普通买家',
|
||||
desc: '搭子报名 · 预约下单',
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200'
|
||||
},
|
||||
{
|
||||
id: 2006,
|
||||
nickname: '苏苏学姐',
|
||||
roleTag: 'expert',
|
||||
roleDesc: '金牌陪伴师',
|
||||
desc: '接单工作台 · 4位核销',
|
||||
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=200'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const user = ref({
|
||||
id: initialUserId || '',
|
||||
@@ -1075,7 +1050,7 @@ const updateCacheSize = () => {
|
||||
cacheSizeText.value = `${kb} KB`;
|
||||
}
|
||||
} catch (_) {
|
||||
cacheSizeText.value = '1.25 MB';
|
||||
cacheSizeText.value = '0 KB';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1150,7 +1125,18 @@ const copyInviteLink = () => {
|
||||
};
|
||||
|
||||
const savePosterImage = () => {
|
||||
uni.showToast({ title: '海报已保存至相册', icon: 'success' });
|
||||
const uid = normalizeUserId();
|
||||
if (!uid) {
|
||||
uni.showToast({ title: '请先登录后再分享', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const link = `https://peiww.gynhc.com/invite?code=PW${uid}`;
|
||||
uni.setClipboardData({
|
||||
data: link,
|
||||
success: () => {
|
||||
uni.showToast({ title: '邀请链接已复制,可粘贴到微信分享', icon: 'none', duration: 2200 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const submitDistWithdraw = async () => {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<view class="dist-stat-divider"></view>
|
||||
<view class="dist-stat-col">
|
||||
<view class="code-box btn-active" @click="copyInviteCode">
|
||||
<text class="code-txt">PW{{ user.id || 2006 }}</text>
|
||||
<text class="code-txt">PW{{ user.id || '' }}</text>
|
||||
<text class="copy-tag">一键复制</text>
|
||||
</view>
|
||||
<text class="dist-stat-lbl">专属邀请码</text>
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
<view class="poster-footer-code">
|
||||
<text class="lbl">邀请码:</text>
|
||||
<text class="val">PW{{ user.id || 2006 }}</text>
|
||||
<text class="val">PW{{ user.id || '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -218,7 +218,8 @@ import {
|
||||
getDistributionTeam,
|
||||
getDistributionRecords,
|
||||
withdrawCommission,
|
||||
bindInviteCode
|
||||
bindInviteCode,
|
||||
normalizeUserId
|
||||
} from '@/common/api.js';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
@@ -232,11 +233,11 @@ const bindCodeInput = ref('');
|
||||
const defaultAvatar = 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200';
|
||||
|
||||
const user = ref({
|
||||
id: 2006,
|
||||
nickname: '苏苏学姐',
|
||||
id: 0,
|
||||
nickname: '',
|
||||
avatar: defaultAvatar,
|
||||
commission: '0.00',
|
||||
invitedCount: 5
|
||||
invitedCount: 0
|
||||
});
|
||||
|
||||
const distActiveTab = ref('poster');
|
||||
@@ -250,25 +251,14 @@ const tabs = [
|
||||
];
|
||||
|
||||
const distributionTeam = ref({
|
||||
directCount: 3,
|
||||
indirectCount: 2,
|
||||
totalTeamCount: 5,
|
||||
directList: [
|
||||
{ id: 3001, nickname: '晨光玩家', avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200', level: 1, createdAt: '2026-10-08 14:20', totalContribute: '48.50' },
|
||||
{ id: 3002, nickname: '风暴骑士', avatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=200', level: 1, createdAt: '2026-10-09 11:35', totalContribute: '32.00' },
|
||||
{ id: 3003, nickname: '月下独酌', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200', level: 1, createdAt: '2026-10-10 09:12', totalContribute: '19.80' }
|
||||
],
|
||||
indirectList: [
|
||||
{ id: 4001, nickname: '极光闪电', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200', level: 2, createdAt: '2026-10-11 16:45', totalContribute: '12.60' },
|
||||
{ id: 4002, nickname: '晴空万里', avatar: 'https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?w=200', level: 2, createdAt: '2026-10-12 18:02', totalContribute: '8.40' }
|
||||
]
|
||||
directCount: 0,
|
||||
indirectCount: 0,
|
||||
totalTeamCount: 0,
|
||||
directList: [],
|
||||
indirectList: []
|
||||
});
|
||||
|
||||
const distributionRecords = ref([
|
||||
{ id: 'REC_101', orderNo: 'ORD_2026101201', fromUserName: '晨光玩家', level: 1, ratePercent: 5, commission: '25.00', orderAmount: '500.00', status: '已入账', createdAt: '2026-10-12 20:30' },
|
||||
{ id: 'REC_102', orderNo: 'ORD_2026101102', fromUserName: '风暴骑士', level: 1, ratePercent: 5, commission: '16.00', orderAmount: '320.00', status: '已入账', createdAt: '2026-10-11 19:15' },
|
||||
{ id: 'REC_103', orderNo: 'ORD_2026101003', fromUserName: '极光闪电', level: 2, ratePercent: 2, commission: '6.40', orderAmount: '320.00', status: '已入账', createdAt: '2026-10-10 15:00' }
|
||||
]);
|
||||
const distributionRecords = ref([]);
|
||||
|
||||
const distWithdrawAmount = ref('');
|
||||
const submittingDistWithdraw = ref(false);
|
||||
@@ -311,7 +301,7 @@ const handleBack = () => {
|
||||
};
|
||||
|
||||
const copyInviteCode = () => {
|
||||
const code = `PW${user.value.id || 2006}`;
|
||||
const code = `PW${normalizeUserId()}`;
|
||||
uni.setClipboardData({
|
||||
data: code,
|
||||
success: () => {
|
||||
@@ -321,7 +311,7 @@ const copyInviteCode = () => {
|
||||
};
|
||||
|
||||
const copyInviteLink = () => {
|
||||
const link = `https://peiww.gynhc.com/invite?code=PW${user.value.id || 2006}`;
|
||||
const link = `https://peiww.gynhc.com/invite?code=PW${normalizeUserId()}`;
|
||||
uni.setClipboardData({
|
||||
data: link,
|
||||
success: () => {
|
||||
@@ -331,7 +321,18 @@ const copyInviteLink = () => {
|
||||
};
|
||||
|
||||
const savePosterImage = () => {
|
||||
uni.showToast({ title: '海报已保存至相册', icon: 'success' });
|
||||
const uid = normalizeUserId();
|
||||
if (!uid) {
|
||||
uni.showToast({ title: '请先登录后再分享', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const link = `https://peiww.gynhc.com/invite?code=PW${uid}`;
|
||||
uni.setClipboardData({
|
||||
data: link,
|
||||
success: () => {
|
||||
uni.showToast({ title: '邀请链接已复制,可粘贴到微信分享', icon: 'none', duration: 2200 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleBindInviteCode = async () => {
|
||||
@@ -341,7 +342,7 @@ const handleBindInviteCode = async () => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const uid = user.value.id || 2006;
|
||||
const uid = normalizeUserId();
|
||||
const res = await bindInviteCode(uid, code);
|
||||
user.value.inviterId = res.inviterId;
|
||||
bindCodeInput.value = '';
|
||||
@@ -381,7 +382,7 @@ const submitDistWithdraw = async () => {
|
||||
submittingDistWithdraw.value = true;
|
||||
uni.showLoading({ title: '正在提交申请...' });
|
||||
try {
|
||||
const uid = user.value.id || 2006;
|
||||
const uid = normalizeUserId();
|
||||
await withdrawCommission(uid, amount, { channel: 'WECHAT' });
|
||||
user.value.commission = (currentComm - amount).toFixed(2);
|
||||
distWithdrawAmount.value = '';
|
||||
@@ -397,7 +398,7 @@ const submitDistWithdraw = async () => {
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
const uid = uni.getStorageSync('userId') || 2006;
|
||||
const uid = normalizeUserId();
|
||||
try {
|
||||
const [profile, teamData, records] = await Promise.all([
|
||||
getUserProfile(uid).catch(() => null),
|
||||
|
||||
@@ -219,26 +219,17 @@ import { onShow } from '@dcloudio/uni-app';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { getNavBarInfo } from '@/common/nav.js';
|
||||
import { getUserProfile, rechargeBalance, getRechargeTiers, requestWechatPayment } from '@/common/api.js';
|
||||
import { getUserProfile, rechargeBalance, getRechargeTiers, requestWechatPayment, normalizeUserId } from '@/common/api.js';
|
||||
|
||||
const currentThemeKey = ref(getCurrentThemeKey());
|
||||
const navInfo = ref(getNavBarInfo());
|
||||
|
||||
const currentUserId = ref(1001);
|
||||
const currentUserId = ref(0);
|
||||
const userBalance = ref('0.00');
|
||||
|
||||
// 预设阶梯列表 (自带高质量官方预置,支持后台动态更新)
|
||||
const DEFAULT_TIERS = [
|
||||
{ id: 1, amount: 50, bonusAmount: 0, tag: '基础充值' },
|
||||
{ id: 2, amount: 100, bonusAmount: 5, tag: '热门畅享' },
|
||||
{ id: 3, amount: 200, bonusAmount: 15, tag: '超值加赠' },
|
||||
{ id: 4, amount: 500, bonusAmount: 50, tag: '尊享特惠' },
|
||||
{ id: 5, amount: 1000, bonusAmount: 120, tag: '土豪专享' },
|
||||
{ id: 6, amount: 2000, bonusAmount: 300, tag: '超神狂欢' }
|
||||
];
|
||||
|
||||
const tierList = ref([...DEFAULT_TIERS]);
|
||||
const selectedTierIndex = ref(1); // 默认选中 100 元 (热门畅享)
|
||||
// 充值档位由后台配置(recharge_tiers),小程序端不写死
|
||||
const tierList = ref([]);
|
||||
const selectedTierIndex = ref(0);
|
||||
const isCustomMode = ref(false);
|
||||
const customAmountText = ref('');
|
||||
const isInputFocused = ref(false);
|
||||
@@ -352,7 +343,7 @@ const clearCustomAmount = () => {
|
||||
// 加载充值档位与用户数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const uid = uni.getStorageSync('userId') || 1001;
|
||||
const uid = normalizeUserId();
|
||||
currentUserId.value = uid;
|
||||
const profile = await getUserProfile(uid);
|
||||
if (profile && profile.balance !== undefined) {
|
||||
@@ -405,7 +396,7 @@ const handleConfirmRecharge = async () => {
|
||||
uni.showLoading({ title: '正在调起微信支付...', mask: true });
|
||||
|
||||
try {
|
||||
const uid = currentUserId.value || uni.getStorageSync('userId') || 1001;
|
||||
const uid = currentUserId.value || normalizeUserId();
|
||||
await requestWechatPayment({
|
||||
amount: payAmount,
|
||||
title: `钱包余额充值 ¥${payAmount}`,
|
||||
@@ -416,12 +407,14 @@ const handleConfirmRecharge = async () => {
|
||||
const res = await rechargeBalance(uid, payAmount, bonus);
|
||||
uni.hideLoading();
|
||||
|
||||
// 更新本地资产与全局缓存
|
||||
// 余额以服务端返回为准;未返回则重新拉取,不在本地伪造入账
|
||||
if (res && res.balance !== undefined) {
|
||||
userBalance.value = String(res.balance);
|
||||
} else {
|
||||
const cur = parseFloat(userBalance.value) || 0;
|
||||
userBalance.value = (cur + payAmount + bonus).toFixed(2);
|
||||
const fresh = await getUserProfile(uid).catch(() => null);
|
||||
if (fresh && fresh.balance !== undefined) {
|
||||
userBalance.value = String(fresh.balance);
|
||||
}
|
||||
}
|
||||
|
||||
const userInfo = uni.getStorageSync('userInfo') || {};
|
||||
|
||||
@@ -266,13 +266,13 @@ const getFeedbackText = (rpt) => {
|
||||
if (rpt.handleNote || rpt.handleResult) {
|
||||
return rpt.handleNote || rpt.handleResult;
|
||||
}
|
||||
if (rpt.status === '已核实处罚' || rpt.status === '已处置') {
|
||||
return '平台已核实违规事实并对涉事账号或内容采取限制处罚措施,感谢您共同维护社区环境。';
|
||||
}
|
||||
if (rpt.status === '已驳回') {
|
||||
return '经安全专员核查,暂未发现明显违规行为;若有补充凭证可随时联系官方在线客服申诉。';
|
||||
return '该举报未被采纳';
|
||||
}
|
||||
return '平台安全专员正在核实处理中,预计 24 小时内同步处置结果。';
|
||||
if (rpt.status === '已核实处罚' || rpt.status === '已处置') {
|
||||
return '已处理';
|
||||
}
|
||||
return '处理中';
|
||||
};
|
||||
|
||||
const handleCopyReportNo = (id) => {
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
<view class="reject-title-row">
|
||||
<text class="reject-title">审核未通过说明:</text>
|
||||
</view>
|
||||
<text class="reject-reason">{{ item.reason || '收款信息不完整或账户异常,请核对后重试。' }}</text>
|
||||
<text class="reject-reason">{{ item.reason || '未填写驳回原因' }}</text>
|
||||
<button class="btn-re-withdraw btn-active" @click="goToWithdraw">重新发起申请</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -352,7 +352,7 @@ const handleSubmitWithdraw = async () => {
|
||||
channel: 'WECHAT'
|
||||
};
|
||||
|
||||
const res = await withdrawCommission(1001, amt, payload);
|
||||
const res = await withdrawCommission(normalizeUserId(), amt, payload);
|
||||
uni.hideLoading();
|
||||
isSubmitting.value = false;
|
||||
|
||||
|
||||
@@ -1302,6 +1302,23 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
|
||||
VALUES ('map_location_config', '地图定位与LBS服务配置', ?, '腾讯位置服务Key、选点与周边测距参数配置', 1)
|
||||
`, [mapLocationConfigVal]);
|
||||
}
|
||||
|
||||
// 体验账号(演示/评审):账号清单与展示文案全部落库,小程序端不再写死
|
||||
const demoAccountsVal = JSON.stringify({
|
||||
enabled: true,
|
||||
accounts: [
|
||||
{ userId: 1001, roleTag: 'host', roleDesc: '房主/局长', desc: '余额充足 · 发起项目制/AA组局' },
|
||||
{ userId: 2001, roleTag: 'buyer', roleDesc: '普通买家', desc: '搭子报名 · 预约陪伴 · 核销码签到' },
|
||||
{ userId: 2006, roleTag: 'expert', roleDesc: '金牌陪伴师', desc: '工作台接单 · 雷达抢单 · 现场核销' }
|
||||
],
|
||||
remark: '体验账号:仅此处配置的账号可走「体验登录」获取真实会话 token;清空 accounts 即关闭体验登录'
|
||||
});
|
||||
if (!existingKeys.has('demo_accounts')) {
|
||||
await conn.query(`
|
||||
INSERT INTO platform_configs (config_key, name, value_json, description, status)
|
||||
VALUES ('demo_accounts', '体验账号配置', ?, '演示/评审用体验账号清单(含展示文案),关闭时清空 accounts', 1)
|
||||
`, [demoAccountsVal]);
|
||||
}
|
||||
if (existingKeys.has('platform_protocol')) {
|
||||
const [protoRows]: any = await conn.query("SELECT value_json FROM platform_configs WHERE config_key = 'platform_protocol'");
|
||||
if (protoRows && protoRows.length > 0) {
|
||||
|
||||
@@ -759,12 +759,6 @@ export class MysqlRepository {
|
||||
);
|
||||
if (mappedRes && mappedRes.length > 0) return mappedRes[0];
|
||||
}
|
||||
if (numId === 2006) {
|
||||
const [susuRes]: any = await pool.query(
|
||||
"SELECT * FROM companions WHERE name = '苏苏学姐' OR id = 1 LIMIT 1"
|
||||
);
|
||||
if (susuRes && susuRes.length > 0) return susuRes[0];
|
||||
}
|
||||
} else {
|
||||
const [res]: any = await pool.query('SELECT * FROM companions WHERE name = ? LIMIT 1', [rawId]);
|
||||
if (res && res.length > 0) return res[0];
|
||||
@@ -917,25 +911,6 @@ export class MysqlRepository {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
const defaultServices = [
|
||||
{
|
||||
id: 'srv_1',
|
||||
name: `${game} · 基础陪伴 (2小时)`,
|
||||
title: `${game} · 基础陪伴 (2小时)`,
|
||||
price: price,
|
||||
unit: '小时',
|
||||
desc: '正规公开商圈同城陪伴,AA制或协商消费'
|
||||
},
|
||||
{
|
||||
id: 'srv_2',
|
||||
name: `${game} · 半日畅享 (3小时)`,
|
||||
title: `${game} · 半日畅享 (3小时)`,
|
||||
price: Math.round(price * 2.8),
|
||||
unit: '次',
|
||||
desc: '深度互动与陪伴交流,赠送专属点歌或语音祝福'
|
||||
}
|
||||
];
|
||||
|
||||
const auditStatus = c.audit_status || '已通过';
|
||||
const certStatus = auditStatus === '已通过' ? 2 : (auditStatus === '待审核' ? 1 : (auditStatus === '已驳回' ? 3 : 0));
|
||||
|
||||
@@ -947,36 +922,32 @@ export class MysqlRepository {
|
||||
avatar: c.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300',
|
||||
coverImage: (c.custom_photos && c.custom_photos[0]) || c.avatar || 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800',
|
||||
gender: c.gender === '男' ? 'male' : 'female',
|
||||
age: synthetic ? 0 : (Number(c.age) || 22),
|
||||
age: Number(c.age) || 0,
|
||||
game: game,
|
||||
track: c.track || 'OFFLINE_MEAL',
|
||||
city: synthetic ? (c.custom_city || '') : (c.custom_city || '北京市 · 朝阳/海淀'),
|
||||
city: c.custom_city || '',
|
||||
rankTag: c.rank_name || (synthetic ? '' : '认证大神'),
|
||||
levelTag: synthetic ? '普通用户' : '金牌认证',
|
||||
price: synthetic ? 0 : price,
|
||||
hourlyRate: synthetic ? 0 : price,
|
||||
rating: synthetic ? 0 : (Number(c.rating) || 5.0),
|
||||
orderCount: synthetic ? 0 : (Number(c.order_count) || 120),
|
||||
rating: synthetic ? 0 : (Number(c.rating) || 0),
|
||||
orderCount: synthetic ? 0 : (Number(c.order_count) || 0),
|
||||
status: synthetic ? '休息中' : (c.status || '接单中'),
|
||||
auditStatus: synthetic ? '未申请' : auditStatus,
|
||||
certStatus: synthetic ? 0 : certStatus,
|
||||
rejectReason: c.reject_reason || '',
|
||||
isOnline: synthetic ? false : c.status === '接单中',
|
||||
isCompanion: !synthetic,
|
||||
voiceDuration: synthetic ? 0 : (Number(c.voice_duration) || 15),
|
||||
voiceDuration: synthetic ? 0 : (Number(c.voice_duration) || 0),
|
||||
voiceType: 'voice',
|
||||
voiceTitle: synthetic ? '' : (c.voice_title || '语音招呼'),
|
||||
voiceTitle: synthetic ? '' : (c.voice_title || ''),
|
||||
voiceIntroUrl: synthetic ? '' : (c.voice_intro_url || ''),
|
||||
bio: c.custom_bio || (synthetic ? '' : '专业同城生活陪伴,热情礼貌,守时诚信。'),
|
||||
bio: c.custom_bio || '',
|
||||
tags: parsedTags,
|
||||
photos: c.custom_photos || (synthetic
|
||||
? [c.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400']
|
||||
: [
|
||||
c.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400',
|
||||
'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400',
|
||||
'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=400'
|
||||
]),
|
||||
services: synthetic ? [] : (customServices.length > 0 ? customServices : defaultServices)
|
||||
photos: (Array.isArray(c.custom_photos) && c.custom_photos.length > 0)
|
||||
? c.custom_photos
|
||||
: (c.avatar ? [c.avatar] : []),
|
||||
services: synthetic ? [] : (Array.isArray(customServices) ? customServices : [])
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2993,7 +2964,7 @@ export class MysqlRepository {
|
||||
WHERE status = 1
|
||||
`);
|
||||
// 移动端公开配置:严禁下发任何密钥/风控内部数据(该接口无需登录)
|
||||
const PUBLIC_EXCLUDED_KEYS = new Set(['security_words', 'map_location_config']);
|
||||
const PUBLIC_EXCLUDED_KEYS = new Set(['security_words', 'map_location_config', 'demo_accounts']);
|
||||
const SENSITIVE_FIELD = /(secret|private|apiv3|api_v3|password|passwd|token|pem|cert|mchid)/i;
|
||||
const stripSensitive = (val: any): any => {
|
||||
if (Array.isArray(val)) return val.map(stripSensitive);
|
||||
@@ -4067,52 +4038,6 @@ export class MysqlRepository {
|
||||
WHERE id = ?
|
||||
`, [id]);
|
||||
if (!rows || rows.length === 0) {
|
||||
if (id === 101) {
|
||||
return {
|
||||
id: 101,
|
||||
userNo: 'U101',
|
||||
nickname: '糖心小糯米',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200',
|
||||
phone: '138****0101',
|
||||
gender: '女',
|
||||
signature: '人美声甜,擅长各类同城陪伴与开黑互动',
|
||||
role: '认证大神',
|
||||
balance: '2580.00',
|
||||
commission: '128.50',
|
||||
invitedCount: 6,
|
||||
orderCount: 88,
|
||||
activityCount: 16,
|
||||
isExpert: true,
|
||||
certStatus: 2,
|
||||
auditStatus: '已通过',
|
||||
rejectReason: '',
|
||||
companionStatus: '接单中',
|
||||
status: '正常'
|
||||
};
|
||||
}
|
||||
if (id === 2006) {
|
||||
return {
|
||||
id: 2006,
|
||||
userNo: 'U2006',
|
||||
nickname: '苏苏学姐',
|
||||
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=120',
|
||||
phone: '139****2006',
|
||||
gender: '女',
|
||||
signature: '工体纯K量贩欢唱局!甜美情歌、粤语老歌、摇滚气氛拉满',
|
||||
role: '认证大神',
|
||||
balance: '5690.00',
|
||||
commission: '560.00',
|
||||
invitedCount: 12,
|
||||
orderCount: 120,
|
||||
activityCount: 35,
|
||||
isExpert: true,
|
||||
certStatus: 2,
|
||||
auditStatus: '已通过',
|
||||
rejectReason: '',
|
||||
companionStatus: '接单中',
|
||||
status: '正常'
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const u = rows[0];
|
||||
|
||||
@@ -509,9 +509,9 @@ async function resolveTencentMapKey(): Promise<string> {
|
||||
return key;
|
||||
}
|
||||
|
||||
function httpsGetJson(url: string, timeoutMs = 4000): Promise<any> {
|
||||
function httpsGetJsonOnce(url: string, headers: Record<string, string>, timeoutMs = 4000): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(url, (resp) => {
|
||||
const req = https.get(url, { headers }, (resp) => {
|
||||
let data = '';
|
||||
resp.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
@@ -529,6 +529,15 @@ function httpsGetJson(url: string, timeoutMs = 4000): Promise<any> {
|
||||
});
|
||||
}
|
||||
|
||||
async function httpsGetJson(url: string, timeoutMs = 4000): Promise<any> {
|
||||
const referer = String(process.env.TENCENT_MAP_REFERER || 'http://118.184.172.94/').trim();
|
||||
const data = await httpsGetJsonOnce(url, referer ? { Referer: referer } : {}, timeoutMs);
|
||||
if (data && (data.status === 110 || data.status === 105) && referer) {
|
||||
return httpsGetJsonOnce(url, {}, timeoutMs);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const geocodeCache = new Map<string, { geo: { lat: number; lng: number } | null; at: number }>();
|
||||
|
||||
/** 地址 -> 坐标(腾讯 WebService 地理编码),带进程内缓存 */
|
||||
@@ -2947,6 +2956,88 @@ app.post('/api/auth/wechat-phone', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 体验账号(演示/评审):清单与文案全部来自 DB 配置,小程序端不再写死 =====
|
||||
async function loadDemoAccounts(): Promise<Array<{ userId: number; roleTag: string; roleDesc: string; desc: string }>> {
|
||||
try {
|
||||
const record = await repo.getPlatformConfigByKey('demo_accounts');
|
||||
const value = (record && record.valueJson) || {};
|
||||
if (value.enabled === false) return [];
|
||||
const accounts = Array.isArray(value.accounts) ? value.accounts : [];
|
||||
return accounts
|
||||
.map((a: any) => ({
|
||||
userId: Number(a.userId) || 0,
|
||||
roleTag: String(a.roleTag || ''),
|
||||
roleDesc: String(a.roleDesc || ''),
|
||||
desc: String(a.desc || ''),
|
||||
}))
|
||||
.filter((a: any) => a.userId > 0)
|
||||
.slice(0, 10);
|
||||
} catch (e) {
|
||||
logServerError('loadDemoAccounts', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/api/auth/demo-accounts', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const accounts = await loadDemoAccounts();
|
||||
const list = [];
|
||||
for (const acc of accounts) {
|
||||
const u: any = await repo.getUserById(acc.userId);
|
||||
if (!u) continue;
|
||||
if (u.status === '已封禁' || u.status === '封禁') continue;
|
||||
list.push({
|
||||
userId: u.id,
|
||||
nickname: u.nickname || `用户${u.id}`,
|
||||
avatar: u.avatar || '',
|
||||
roleTag: acc.roleTag,
|
||||
roleDesc: acc.roleDesc,
|
||||
desc: acc.desc,
|
||||
});
|
||||
}
|
||||
res.json({ code: 20000, data: list });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 体验登录:只对 DB 配置内的账号签发真实 token,避免"无 token 的假登录"造成的 401/身份错配
|
||||
app.post('/api/auth/demo-login', rateLimit({ name: 'demo_login', windowMs: 60 * 1000, max: 20, key: rateWriteKey }), async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = Number(req.body?.userId) || 0;
|
||||
const accounts = await loadDemoAccounts();
|
||||
if (!accounts.some((a) => a.userId === userId)) {
|
||||
return res.status(403).json({ code: 40300, message: '该账号未开启体验登录' });
|
||||
}
|
||||
const user: any = await repo.getUserById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ code: 40400, message: '体验账号不存在或已被清理' });
|
||||
}
|
||||
if (user.status === '已封禁' || user.status === '封禁') {
|
||||
return res.status(403).json({ code: 40300, message: '该体验账号已被封禁' });
|
||||
}
|
||||
const token = signUserToken(user.id);
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: {
|
||||
userId: user.id,
|
||||
user: {
|
||||
id: user.id,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
gender: user.gender,
|
||||
phone: user.phone,
|
||||
},
|
||||
userToken: token,
|
||||
token,
|
||||
},
|
||||
message: '体验登录成功',
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message || '体验登录异常' });
|
||||
}
|
||||
});
|
||||
|
||||
// 微信小程序 JSAPI 统一下单预支付接口 (支持真实商户号配置与沙盒收银台无缝切换)
|
||||
app.post('/api/pay/wechat/prepay', rateLimit({ name: 'prepay', windowMs: 60 * 1000, max: 30, key: rateWriteKey }), async (req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -3790,45 +3881,7 @@ app.get('/api/users/:id', async (req: Request, res: Response) => {
|
||||
const userId = Number(req.params.id);
|
||||
let user = await repo.getUserById(userId);
|
||||
if (!user) {
|
||||
if (userId === 101) {
|
||||
user = {
|
||||
id: 101,
|
||||
userNo: 'U101',
|
||||
nickname: '糖心小糯米',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200',
|
||||
phone: '138****0101',
|
||||
gender: '女',
|
||||
signature: '人美声甜,擅长各类同城陪伴与开黑互动',
|
||||
role: '认证大神',
|
||||
balance: '2580.00',
|
||||
commission: '128.50',
|
||||
invitedCount: 6,
|
||||
orderCount: 88,
|
||||
activityCount: 16,
|
||||
isExpert: true,
|
||||
status: '正常'
|
||||
};
|
||||
} else if (userId === 2006) {
|
||||
user = {
|
||||
id: 2006,
|
||||
userNo: 'U2006',
|
||||
nickname: '苏苏学姐',
|
||||
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=120',
|
||||
phone: '139****2006',
|
||||
gender: '女',
|
||||
signature: '工体纯K量贩欢唱局!甜美情歌、粤语老歌、摇滚气氛拉满',
|
||||
role: '认证大神',
|
||||
balance: '5690.00',
|
||||
commission: '560.00',
|
||||
invitedCount: 12,
|
||||
orderCount: 120,
|
||||
activityCount: 35,
|
||||
isExpert: true,
|
||||
status: '正常'
|
||||
};
|
||||
} else {
|
||||
return res.status(404).json({ code: 40400, message: '用户未找到' });
|
||||
}
|
||||
return res.status(404).json({ code: 40400, message: '用户未找到' });
|
||||
}
|
||||
// 敏感字段(手机号 / 余额 / 佣金 / 邀请关系)仅本人或管理员可见
|
||||
const isAdmin = isVerifiedAdminRequest(req);
|
||||
|
||||
@@ -1,119 +1,37 @@
|
||||
// 已读取AGENTS.md
|
||||
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;
|
||||
/**
|
||||
* 活动大厅内容规范(第九十六批改写)
|
||||
* 背景:演示用 mock.js 已删除,活动数据统一来自数据库(GET /api/activities)。
|
||||
* 本套件改为校验:不再内置 mock、页面真实取数、无 Emoji、关键字段展示完整。
|
||||
*/
|
||||
describe('ActivityListContent(活动大厅真实数据与内容规范)', () => {
|
||||
const root = path.resolve(__dirname, '../../');
|
||||
const pagePath = path.join(root, 'pwuniapp', 'pages', 'activity', 'list.vue');
|
||||
const page = fs.readFileSync(pagePath, 'utf-8');
|
||||
const mockPath = path.join(root, 'pwuniapp', 'common', 'mock.js');
|
||||
|
||||
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([]);
|
||||
});
|
||||
it('不得再内置活动 mock 数据', () => {
|
||||
expect(fs.existsSync(mockPath)).toBe(false);
|
||||
expect(page).not.toContain('ACTIVITIES_LIST');
|
||||
expect(page).not.toContain("from '@/common/mock");
|
||||
});
|
||||
|
||||
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(/(下午茶陪喝|陪喝)/);
|
||||
});
|
||||
it('活动列表必须从服务端取数', () => {
|
||||
expect(page).toContain('getActivities');
|
||||
expect(page).toContain('activities.value = list');
|
||||
});
|
||||
|
||||
describe('3. 活动大厅综合搜索框功能与双主题规范门禁 (TDD)', () => {
|
||||
it('页面必须包含顶部综合搜索框组件并支持关键词双向绑定', () => {
|
||||
const content = fs.readFileSync(listVuePath, 'utf-8');
|
||||
expect(content).toContain('header-search');
|
||||
expect(content).toContain('search-input');
|
||||
expect(content).toContain('searchKeyword');
|
||||
});
|
||||
it('活动卡片展示地点、开始时间与发起人字段', () => {
|
||||
expect(page).toContain('location');
|
||||
expect(page).toContain('startTime');
|
||||
expect(page).toContain('creatorNickname');
|
||||
});
|
||||
|
||||
it('搜索过滤逻辑必须同时支持标题、商圈地点、品类与局长多维度匹配', () => {
|
||||
const content = fs.readFileSync(listVuePath, 'utf-8');
|
||||
// 必须包含基于 searchKeyword 的模糊匹配逻辑
|
||||
expect(content).toContain('matchKeyword');
|
||||
expect(content).toMatch(/a\.title.*includes/);
|
||||
expect(content).toMatch(/a\.location.*includes/);
|
||||
expect(content).toMatch(/a\.creatorNickname.*includes/);
|
||||
});
|
||||
|
||||
it('搜索框在 theme-warm 浅色暖阳晨曦模式下必须适配纯白背景与深色文本', () => {
|
||||
const content = fs.readFileSync(listVuePath, 'utf-8');
|
||||
const warmSection = content.split('.activity-container.theme-warm')[1] || '';
|
||||
expect(warmSection).toContain('.header-search');
|
||||
expect(warmSection).toContain('.search-box');
|
||||
expect(warmSection).toContain('#FFFFFF');
|
||||
});
|
||||
it('页面严禁包含任何系统 Emoji 字符', () => {
|
||||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||||
expect(emojiRegex.test(page)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -517,7 +517,8 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
expect(content).toContain('open-type="getPhoneNumber"');
|
||||
expect(content).toContain('@getphonenumber="handleGetPhoneNumber"');
|
||||
expect(content).toContain('一键授权快捷登录');
|
||||
expect(content).toContain('DEMO_ACCOUNTS');
|
||||
expect(content).toContain('getDemoAccounts');
|
||||
expect(content).toContain('demoLogin');
|
||||
expect(content).toContain('用户服务协议');
|
||||
expect(content).toContain('隐私保护指引');
|
||||
// 验证已彻底移除登录大门前强制填写的头像昵称卡片,保障成熟商业极速登录体验
|
||||
@@ -819,15 +820,16 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
expect(content).toContain('WHERE id = ?');
|
||||
});
|
||||
|
||||
it('仓储层与路由必须针对预设用户 2006 与 101 提供兜底配置防止出现 500/404', () => {
|
||||
it('严禁在服务端内置演示用户兜底(测试数据必须落库)', () => {
|
||||
const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts');
|
||||
const repoContent = fs.readFileSync(repoPath, 'utf-8');
|
||||
expect(repoContent).toContain('if (id === 2006)');
|
||||
expect(repoContent).toContain('苏苏学姐');
|
||||
expect(repoContent).not.toContain('if (id === 2006)');
|
||||
expect(repoContent).not.toContain('糖心小糯米');
|
||||
|
||||
const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts');
|
||||
const serverContent = fs.readFileSync(serverIndexPath, 'utf-8');
|
||||
expect(serverContent).toContain('userId === 2006');
|
||||
expect(serverContent).not.toContain('userId === 2006');
|
||||
expect(serverContent).not.toContain('userId === 101');
|
||||
});
|
||||
|
||||
it('相关文件严禁包含任何系统 Emoji 违规字符', () => {
|
||||
|
||||
@@ -102,14 +102,11 @@ describe('UI/UX 规范门禁 (TDD 测试套件: 全面杜绝系统 Emoji)', () =
|
||||
expect(textStyle).toContain('text-overflow: ellipsis');
|
||||
});
|
||||
|
||||
it('mock.js 中试听标题长度必须精简且不得超过12个字,防止在小屏过早被截断', () => {
|
||||
it('演示数据必须来自数据库,禁止在小程序内置 mock.js', () => {
|
||||
const mockJsPath = path.join(rootDir, 'pwuniapp', 'common', 'mock.js');
|
||||
const mockContent = fs.readFileSync(mockJsPath, 'utf-8');
|
||||
const voiceTitleMatches = mockContent.matchAll(/voiceTitle:\s*['"]([^'"]+)['"]/g);
|
||||
for (const match of voiceTitleMatches) {
|
||||
const title = match[1];
|
||||
expect(title.length).toBeLessThanOrEqual(12);
|
||||
}
|
||||
expect(fs.existsSync(mockJsPath)).toBe(false);
|
||||
const apiContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'common', 'api.js'), 'utf-8');
|
||||
expect(apiContent).not.toContain("common/mock");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ describe('第八十二批服务包空值防护闸门', () => {
|
||||
expect(createPage).not.toContain('selectedService.unit');
|
||||
});
|
||||
|
||||
it('下单页在无服务包时按大神基础时薪兜底,保证仍可下单', () => {
|
||||
expect(createPage).toContain("id: 'base'");
|
||||
expect(createPage).toContain('基础陪伴服务');
|
||||
it('下单页在无服务包时必须阻止下单,不得伪造基础服务', () => {
|
||||
expect(createPage).not.toContain("id: 'base'");
|
||||
expect(createPage).toContain('该大神暂未配置服务包,无法下单');
|
||||
expect(createPage).toContain('Array.isArray(liveExpert.services) ? liveExpert.services : []');
|
||||
});
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('订单与活动资金一致性护栏 (报名超卖 / 抢单 CAS / 评
|
||||
|
||||
it('小程序评价必须提交真实登录用户ID,严禁硬编码 1001 绕过买家校验', () => {
|
||||
const vue = fs.readFileSync(pwuniappOrderListPath, 'utf-8');
|
||||
expect(vue).toContain("userId: Number(uni.getStorageSync('userId') || 1001),");
|
||||
expect(vue).toContain('userId: normalizeUserId(),');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('第七十三批公开配置隐私闸门', () => {
|
||||
const repo = fs.readFileSync(path.join(root, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('公开配置排除风控词库与地图密钥配置', () => {
|
||||
expect(repo).toContain("const PUBLIC_EXCLUDED_KEYS = new Set(['security_words', 'map_location_config'])");
|
||||
expect(repo).toContain("const PUBLIC_EXCLUDED_KEYS = new Set(['security_words', 'map_location_config', 'demo_accounts'])");
|
||||
expect(repo).toContain('if (PUBLIC_EXCLUDED_KEYS.has(r.configKey)) continue;');
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户