feat: 完善对象存储上传与真实条码二维码引擎,彻底剥离保底Mock数据

这个提交包含在:
xcbyy
2026-10-09 17:18:14 +08:00
父节点 6d3076cdb8
当前提交 d0f4050cdc
共有 329 个文件被更改,包括 54416 次插入 和 926 次删除

681
pwuniapp/common/api.js 普通文件
查看文件

@@ -0,0 +1,681 @@
// pwuniapp/common/api.js
// 同城生活陪伴业务API接口层与数据适配器
import http from './request.js';
/**
* 格式化活动数据,使其在 MySQL 与前端展示组件间无缝互通
*/
export function formatActivityData(raw) {
if (!raw) return null;
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 participants = (raw.participants || []).map((p) => ({
id: p.id,
userId: p.userId,
nickname: p.nickname || `搭子_${p.userId || p.id}`,
avatar: p.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100',
verifyCode: p.verifyCode || '8392',
checkedIn: Boolean(p.checkinStatus || p.checkedIn),
payAmount: p.payAmountCents != null ? p.payAmountCents / 100 : (p.payAmount || 0),
}));
const joinedCount = raw.joinedCount != null ? raw.joinedCount : participants.length;
return {
id: String(raw.id),
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',
status: raw.status || (joinedCount >= (raw.maxParticipants || 4) ? 'FULL' : 'RECRUITING'),
allowDynamicPrice: Boolean(raw.allowDynamicPrice),
priceAdjustRule: raw.priceAdjustRule || 'UNIFIED_HIGH',
participants,
priceAdjustHistory: raw.priceAdjustHistory || [],
};
}
/**
* 获取组局活动列表 (真实请求 MySQL,异常时返回空列表)
*/
export async function getActivities() {
try {
const list = await http.get('/activities');
if (Array.isArray(list) && list.length > 0) {
return list.map(formatActivityData);
}
} catch (err) {
console.warn('[API] 获取后端活动列表异常:', err.message);
}
return [];
}
/**
* 获取指定活动详情 (真实请求 MySQL,不存在或异常时返回 null)
*/
export async function getActivityDetail(id) {
try {
const detail = await http.get(`/activities/${id}`);
if (detail) {
return formatActivityData(detail);
}
} catch (err) {
console.warn(`[API] 获取活动 [${id}] 详情异常:`, err.message);
}
return null;
}
/**
* 发起新组局活动 (实时写入 MySQL)
*/
export async function createActivity(payload) {
const body = {
creatorId: payload.creatorId || 1001,
title: payload.title,
gameCategory: payload.game || '美食陪吃',
location: payload.location,
payType: payload.payType === 'AA' ? 1 : 2,
totalFeeCents: Math.round((Number(payload.totalFee) || 0) * 100),
perPersonFeeCents: Math.round((Number(payload.perPersonFee || payload.aaFee) || 0) * 100),
maxParticipants: Number(payload.maxParticipants) || 4,
};
try {
const res = await http.post('/activities', body);
return formatActivityData(res);
} catch (err) {
console.warn('[API] 后端创建活动接口未响应:', err.message);
throw err;
}
}
/**
* 报名活动并获取 4 位唯一核销码
*/
export async function joinActivity(activityId, params = {}) {
const body = {
userId: params.userId || 2008,
payAmountCents: Math.round((Number(params.payAmount) || 50) * 100),
};
try {
const res = await http.post(`/activities/${activityId}/join`, body);
return res;
} catch (err) {
console.warn('[API] 报名接口异常:', err.message);
throw err;
}
}
/**
* 房主/管理员核销 4 位核销码
*/
export async function verifyActivityCheckin(activityId, verifyCode, operatorId = 1001) {
return http.post(`/activities/${activityId}/verify`, {
operatorId,
verifyCode,
});
}
/**
* 房主调整出价/追加金额
*/
export async function adjustActivityPrice(activityId, newPriceYuan, operatorId = 1001) {
const newPriceCents = Math.round(Number(newPriceYuan) * 100);
try {
const res = await http.post(`/activities/${activityId}/adjust-price`, {
operatorId,
newPriceCents,
});
return formatActivityData(res);
} catch (err) {
console.warn('[API] 调价请求异常:', err.message);
throw err;
}
}
/**
* 房主取消/解散组局 (原路退款已报名成员)
*/
export async function cancelActivity(activityId, hostId = 1001, reason = '房主主动解散') {
return http.post(`/activities/${activityId}/cancel`, {
hostId,
reason,
});
}
/**
* 成员主动退出组局 (原路退款)
*/
export async function leaveActivity(activityId, userId = 2001) {
return http.post(`/activities/${activityId}/leave`, {
userId,
});
}
/**
* 获取“我的组局”(发起的 + 报名的)
*/
export async function getMyActivities(userId = 1001) {
try {
const res = await http.get(`/users/${userId}/my-activities`);
if (res) {
return {
created: (res.created || []).map(formatActivityData),
joined: (res.joined || []).map(formatActivityData),
};
}
} catch (err) {
console.warn('[API] 获取我的组局异常:', err.message);
}
return { created: [], joined: [] };
}
/**
* 获取同城大神列表
*/
export async function getCompanions() {
try {
const list = await http.get('/companions');
if (Array.isArray(list) && list.length > 0) {
return list.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 || '同城陪伴'))),
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,
isOnline: c.status === '接单中',
voiceDuration: Number(c.voiceDuration) || 15,
voiceType: 'voice',
voiceTitle: c.voiceTitle || '语音招呼',
bio: '专业同城生活陪伴,阳光健康公开。'
}));
}
} catch (err) {
console.warn('[API] 获取大神列表异常:', err.message);
}
return [];
}
/**
* 获取单个大神详情 (真实从 MySQL 读取)
*/
export async function getCompanionDetail(id) {
try {
const res = await http.get(`/companions/${id}`);
if (res && res.id) {
return res;
}
} catch (err) {
console.warn(`[API] 获取大神 [${id}] 详情异常:`, err.message);
}
return null;
}
/**
* 格式化订单数据,使 MySQL 表字段与移动端视图无缝映射
*/
export function formatOrderData(raw) {
if (!raw) return null;
let statusKey = raw.status || 'IN_SERVICE';
if (statusKey === '待支付' || statusKey === '待付款') statusKey = 'PENDING_PAY';
else if (statusKey === '已到场核销' || statusKey === '履约中' || statusKey === '服务中') statusKey = 'IN_SERVICE';
else if (statusKey === '已完成') statusKey = 'COMPLETED';
else if (statusKey === '争议仲裁中' || statusKey === '退款中' || statusKey === '已全额退款') 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 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'),
nickname: companionNickname,
avatar: companionAvatar,
game: game,
},
serviceTitle: raw.serviceTitle || raw.service_title || `${game} · 专业陪伴服务`,
quantity: Number(raw.durationHours || raw.duration_hours || raw.quantity || 2),
totalAmount: Number(raw.amount || raw.totalAmount || 0),
status: statusKey,
meetVenue: raw.location || raw.meetVenue || '北京市公开商圈',
expenseBurden: raw.expenseBurden || raw.expense_burden || 'AA制各自付',
verifyCode: raw.verifyCode || raw.verify_code || '',
remark: raw.remark || '',
createdAt: raw.createdAt || raw.created_at || '刚刚'
};
}
/**
* 获取订单列表 (真实读取 MySQL)
*/
export async function getOrders() {
try {
const list = await http.get('/orders');
if (Array.isArray(list) && list.length > 0) {
return list.map(formatOrderData);
}
} catch (err) {
console.warn('[API] 获取订单列表异常:', err.message);
}
return [];
}
/**
* 创建新订单 (真实写入 MySQL orders 表)
*/
export async function createOrder(payload) {
try {
const res = await http.post('/orders', payload);
return formatOrderData(res);
} catch (err) {
console.warn('[API] 后端下单未响应:', err.message);
throw err;
}
}
/**
* 取消订单
*/
export async function cancelOrder(orderId) {
try {
const res = await http.post(`/orders/${orderId}/cancel`);
return formatOrderData(res);
} catch (err) {
console.warn('[API] 取消订单网络异常:', err.message);
}
return null;
}
/**
* 支付订单
*/
export async function payOrder(orderId) {
try {
const res = await http.post(`/orders/${orderId}/pay`);
return formatOrderData(res);
} catch (err) {
console.warn('[API] 支付订单网络异常:', err.message);
}
return null;
}
/**
* 确认验收订单
*/
export async function completeOrder(orderId) {
try {
const res = await http.post(`/orders/${orderId}/complete`);
return formatOrderData(res);
} catch (err) {
console.warn('[API] 确认完成网络异常:', err.message);
}
return null;
}
/**
* 申请退款/售后
*/
export async function refundOrder(orderId) {
try {
const res = await http.post(`/orders/${orderId}/refund`);
return formatOrderData(res);
} catch (err) {
console.warn('[API] 申请退款网络异常:', err.message);
}
return null;
}
/**
* 通用4位核销码反查信息 (支持组局活动与同城线下订单)
*/
export async function queryCheckinCode(code) {
try {
const res = await http.get(`/verify/query/${code}`);
return res;
} catch (err) {
console.warn('[API] 反查核销码失败:', err.message);
throw err;
}
}
/**
* 通用4位核销码现场核销 (支持组局活动与同城线下订单)
*/
export async function universalVerifyCheckin(code, operatorId = 1001) {
try {
const res = await http.post('/verify/universal', { verifyCode: code, operatorId });
return res;
} catch (err) {
console.warn('[API] 通用核销接口异常:', err.message);
throw err;
}
}
/**
* 获取友圈广场动态列表 (真实读取 MySQL posts 表)
*/
export async function getPosts(category) {
try {
const query = category && category !== 'all' ? `?category=${encodeURIComponent(category)}` : '';
const res = await http.get(`/posts${query}`);
if (Array.isArray(res) && res.length > 0) {
return res.map(p => ({
id: p.id,
author: p.author || '神秘同好',
avatar: p.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
tag: p.tag || '同城认证',
tagType: p.tagType || 'warm-tag',
location: p.location || '同城热门商圈',
distance: p.distance || '1km内',
time: p.createdAt ? '刚刚' : '近期',
content: p.content || '',
topics: Array.isArray(p.topics) ? p.topics : [],
images: Array.isArray(p.images) ? p.images : [],
actionText: p.actionText || '聊一聊',
likes: Number(p.likes) || 0,
liked: false,
comments: Number(p.comments) || 0,
views: Number(p.views) || 1,
category: p.category || 'partner'
}));
}
} catch (err) {
console.warn('[API] 获取友圈动态列表异常:', err.message);
}
return null;
}
/**
* 发布新动态 (真实写入 MySQL posts 表)
*/
export async function createPost(payload) {
try {
const res = await http.post('/posts', payload);
return res;
} catch (err) {
console.warn('[API] 发布动态异常:', err.message);
throw err;
}
}
/**
* 动态点赞 (落盘 MySQL)
*/
export async function likePost(id) {
try {
const res = await http.post(`/posts/${id}/like`);
return res;
} catch (err) {
console.warn('[API] 点赞动态异常:', err.message);
return null;
}
}
/**
* 获取用户个人资料与钱包资产 (真实读取 MySQL users 表)
*/
export async function getUserProfile(userId = 1001) {
try {
const res = await http.get(`/users/${userId}`);
if (res && res.id) {
return res;
}
} catch (err) {
console.warn('[API] 获取用户信息异常,降级读取本地默认配置:', err.message);
}
return null;
}
/**
* 修改用户个人资料 (昵称、头像、手机号)
*/
export async function updateUserProfile(userId = 1001, data = {}) {
try {
const res = await http.post(`/users/${userId}/profile`, data);
return res;
} catch (err) {
console.warn('[API] 修改个人资料网络异常:', err.message);
throw err;
}
}
/**
* 钱包充值 (落盘 MySQL 并记录财务入账流水)
*/
export async function rechargeBalance(userId = 1001, amount) {
try {
const res = await http.post(`/users/${userId}/recharge`, { amount });
return res;
} catch (err) {
console.warn('[API] 钱包充值接口异常:', err.message);
throw err;
}
}
/**
* 佣金提现申请 (扣减可提现金额,写入提现待审核表与出账流水)
*/
export async function withdrawCommission(userId = 1001, amount, channel = '微信商户零钱') {
try {
const res = await http.post(`/users/${userId}/withdraw`, { amount, channel });
return res;
} catch (err) {
console.warn('[API] 提现申请接口异常:', err.message);
throw err;
}
}
/**
* 查询用户财务收支流水
*/
export async function getUserFinanceRecords(userId = 1001) {
try {
const res = await http.get(`/users/${userId}/finance-records`);
return res || [];
} catch (err) {
console.warn('[API] 获取财务流水异常:', err.message);
return [];
}
}
/**
* 获取可抢订单池 (真实读取 MySQL orders 待接单列表)
*/
export async function getOrdersPool() {
try {
const res = await http.get('/orders/pool');
if (Array.isArray(res) && res.length > 0) {
return res;
}
} catch (err) {
console.warn('[API] 获取可抢订单池异常:', err.message);
}
return null;
}
/**
* 大神抢单认领
*/
export async function claimOrder(orderId, expertData = {}) {
try {
const res = await http.post(`/orders/${orderId}/claim`, expertData);
return res;
} catch (err) {
console.warn('[API] 大神抢单异常:', err.message);
throw err;
}
}
/**
* 获取大神今日接单数据汇总 (落盘 MySQL)
*/
export async function getExpertStats(expertId = 101) {
try {
const res = await http.get(`/expert/stats/${expertId}`);
if (res && typeof res.income !== 'undefined') {
return res;
}
} catch (err) {
console.warn('[API] 获取大神今日统计异常:', err.message);
}
return null;
}
/**
* 获取首页运营 Banner 列表 (配置化接口)
*/
export async function getBanners() {
try {
const res = await http.get('/banners');
if (Array.isArray(res) && res.length > 0) {
return res;
}
} catch (err) {
console.warn('[API] 获取Banner异常:', err.message);
}
return null;
}
/**
* 提交订单服务评价与星级打分 (落盘 MySQL order_reviews)
*/
export async function submitOrderReview(orderId, reviewData = {}) {
try {
const res = await http.post(`/orders/${orderId}/review`, reviewData);
return res;
} catch (err) {
console.warn('[API] 提交订单评价异常:', err.message);
throw err;
}
}
/**
* 获取大神真实历史评价列表
*/
export async function getExpertReviews(expertId) {
try {
const res = await http.get(`/companions/${expertId}/reviews`);
if (Array.isArray(res)) {
return res;
}
} catch (err) {
console.warn('[API] 获取大神评价列表异常:', err.message);
}
return [];
}
/**
* 获取友圈动态评论区列表 (落盘 MySQL post_comments)
*/
export async function getPostComments(postId) {
try {
const res = await http.get(`/posts/${postId}/comments`);
if (Array.isArray(res)) {
return res;
}
} catch (err) {
console.warn('[API] 获取动态评论异常:', err.message);
}
return [];
}
/**
* 发表友圈动态评论
*/
export async function addPostComment(postId, commentData = {}) {
try {
const res = await http.post(`/posts/${postId}/comments`, commentData);
return res;
} catch (err) {
console.warn('[API] 发表动态评论异常:', err.message);
throw err;
}
}
/**
* 申请友圈搭子互动上车
*/
export async function applyPostParty(postId, applyData = {}) {
try {
const res = await http.post(`/posts/${postId}/apply`, applyData);
return res;
} catch (err) {
console.warn('[API] 申请搭子上车异常:', err.message);
throw err;
}
}
/**
* 真实上传本地图片至服务端持久化静态存储
* @param {string} tempFilePath 本地图片临时路径 (如 wxfile://tmp_xxx 或 blob:xxx)
* @returns {Promise<string>} 返回持久化公网图片访问路径 (形如 /uploads/img_xxx.png)
*/
export function uploadImage(tempFilePath) {
return new Promise((resolve, reject) => {
// 若本身已经是网络公网链接,直接返回
if (tempFilePath.startsWith('http://') || tempFilePath.startsWith('https://') || tempFilePath.startsWith('/uploads/')) {
return resolve(tempFilePath);
}
// #ifdef H5
const uploadUrl = '/api/upload';
// #endif
// #ifndef H5
const uploadUrl = 'https://peiww.gynhc.com/api/upload';
// #endif
uni.uploadFile({
url: uploadUrl,
filePath: tempFilePath,
name: 'file',
success: (uploadRes) => {
try {
const data = JSON.parse(uploadRes.data);
if (data && data.code === 20000 && data.data && data.data.url) {
resolve(data.data.url);
} else {
reject(new Error((data && data.message) || '上传解析异常'));
}
} catch (e) {
reject(new Error('上传数据返回格式错误'));
}
},
fail: (err) => {
reject(new Error(err.errMsg || '图片上传网络异常'));
}
});
});
}

74
pwuniapp/common/barcode.js 普通文件
查看文件

@@ -0,0 +1,74 @@
// pwuniapp/common/barcode.js
// 轻量级原生 Code128 条形码编码与图形数据生成器 (零第三方依赖,跨端兼容小程序与H5)
const CODE128_PATTERNS = [
'212222', '222122', '222221', '121223', '121322', '131222', '122213', '122312', '132212', '221213', // 0-9
'221312', '231212', '112232', '122132', '122231', '113222', '123122', '123221', '223211', '221132', // 10-19
'221231', '213212', '223112', '312131', '311222', '321122', '321221', '312212', '322112', '322211', // 20-29
'212123', '212321', '232121', '111323', '131123', '131321', '112313', '132113', '132311', '211313', // 30-39
'231113', '231311', '112133', '112331', '132131', '113123', '113321', '133121', '313121', '211331', // 40-49
'231131', '213113', '213311', '213131', '311123', '311321', '331121', '312113', '312311', '332111', // 50-59
'314111', '221411', '431111', '111224', '111422', '121124', '121421', '141122', '141221', '112214', // 60-69
'112412', '122114', '122411', '142112', '142211', '241211', '221114', '413111', '241112', '134111', // 70-79
'111242', '121142', '121241', '114212', '124112', '124211', '411212', '421112', '421211', '212141', // 80-89
'214121', '412121', '111143', '111341', '131141', '114113', '114311', '411113', '411311', '113141', // 90-99
'114131', '311141', '411131', '211412', '211214', '211232', '2331112' // 100-106
];
// Start B 对应 index 104
const START_B_INDEX = 104;
// Stop 对应 index 106
const STOP_INDEX = 106;
/**
* 将字符串编码为 Code128B 的条块分布
* @param {string} text 要编码的文本字符串 (ASCII 32 - 126)
* @returns {Array<{ isBar: boolean, width: number }>} 条块序列
*/
export function encodeCode128B(text) {
if (!text) text = '0000';
const codes = [START_B_INDEX];
let checkSum = START_B_INDEX;
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i);
// Code 128B 映射: ASCII 32~127 对应 code 0~95
let val = charCode - 32;
if (val < 0 || val > 95) val = 0; // 越界字符转为空格
codes.push(val);
checkSum += val * (i + 1);
}
const checkVal = checkSum % 103;
codes.push(checkVal);
codes.push(STOP_INDEX);
// 转换为高精度条空阵列 (Bar and Space array)
const segments = [];
codes.forEach((codeIndex) => {
const pattern = CODE128_PATTERNS[codeIndex];
if (!pattern) return;
for (let p = 0; p < pattern.length; p++) {
const width = parseInt(pattern[p], 10);
const isBar = p % 2 === 0;
segments.push({ isBar, width });
}
});
return segments;
}
/**
* 转换为 0/1 位图序列 (用于像素级精确渲染)
* @param {string} text
* @returns {string} 由 '1' 和 '0' 组成的二进制序列
*/
export function getBarcodeBits(text) {
const segments = encodeCode128B(text);
let bits = '';
segments.forEach((seg) => {
bits += (seg.isBar ? '1' : '0').repeat(seg.width);
});
return bits;
}

查看文件

@@ -21,6 +21,79 @@ export const ICONS = {
<path d="M12 2c-.6 2.7-2.6 4.7-3.9 6.2C6.5 10 5 12.3 5 15c0 3.9 3.1 7 7 7s7-3.1 7-7c0-2.3-1.2-4.4-2.8-6-1.5-1.5-3.6-3.7-4.2-7zM12 19c-2.2 0-4-1.8-4-4 0-1.5.8-2.8 1.9-3.8.3-.3.8-.1.8.3.1 1.2.9 2.2 2.1 2.5.4.1.7.5.7.9 0 2.3-1.5 4.1-1.5 4.1z"/>
</svg>
`),
close: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
`),
camera: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
<circle cx="12" cy="13" r="4"></circle>
</svg>
`),
plus: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
`),
chevronRight: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#94A3B8" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
`),
starFilled: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#F59E0B">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
`),
starOutline: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
`),
messageSquare: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
`),
send: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
`),
ticket: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2z"></path>
<line x1="12" y1="5" x2="12" y2="19" stroke-dasharray="2 2"></line>
</svg>
`),
headphones: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#60A5FA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 18v-6a9 9 0 0 1 18 0v6"></path>
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path>
</svg>
`),
bell: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
`),
// 游戏分类
gameAll: svgToUri(`
@@ -276,6 +349,45 @@ export const ICONS = {
</svg>
`),
// 户外爬山·徒步登高
mountain: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 3l4 8 5-5 5 15H2L8 3z"></path>
<path d="M4 18l4-7 4 7"></path>
</svg>
`),
// 同城台球·桌球切磋
billiards: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#3B82F6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="5" fill="#3B82F6" fill-opacity="0.2"></circle>
<path d="M12 9.5a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5z"></path>
<path d="M12 12a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"></path>
</svg>
`),
// 运动健身·羽毛球/网球/挥拍暴汗
fitness: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 4h12v4H6V4z"></path>
<path d="M9 8v8m6-8v8"></path>
<path d="M6 16h12v4H6v-4z"></path>
<line x1="12" y1="2" x2="12" y2="4"></line>
<line x1="12" y1="20" x2="12" y2="22"></line>
</svg>
`),
// 全部活动·四宫格微标
gridAll: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#6366F1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7" rx="2"></rect>
<rect x="14" y="3" width="7" height="7" rx="2"></rect>
<rect x="14" y="14" width="7" height="7" rx="2"></rect>
<rect x="3" y="14" width="7" height="7" rx="2"></rect>
</svg>
`),
// 主题切换·调色板
palette: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -292,6 +404,153 @@ export const ICONS = {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path>
</svg>
`),
// 浅色暖阳·太阳图标 (Sun)
sun: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#EA580C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5" fill="#F59E0B" fill-opacity="0.25"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
`),
// 冷光暗夜·月亮图标 (Moon)
moon: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#818CF8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" fill="#6366F1" fill-opacity="0.25"></path>
</svg>
`),
// 底部导航栏专用图标
tabHome: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
`),
tabActivity: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path>
<line x1="4" y1="22" x2="4" y2="15"></line>
</svg>
`),
tabPlus: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
`),
tabCommunity: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M14.31 8l5.74 9.94M9.69 8h11.48M7.38 12l5.74-9.94M9.69 16L3.95 6.06M14.31 16H2.83M16.62 12l-5.74 9.94"></path>
</svg>
`),
tabUser: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`),
tabHall: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1"></rect>
<rect x="14" y="3" width="7" height="7" rx="1"></rect>
<rect x="14" y="14" width="7" height="7" rx="1"></rect>
<rect x="3" y="14" width="7" height="7" rx="1"></rect>
</svg>
`),
tabRadar: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="6"></circle>
<circle cx="12" cy="12" r="2"></circle>
<path d="M12 2a10 10 0 0 1 10 10h-10z" fill="currentColor" fill-opacity="0.2"></path>
</svg>
`),
radarWave: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="2"></circle>
<path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path>
</svg>
`),
locate: svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="8"></circle>
<line x1="12" y1="2" x2="12" y2="6"></line>
<line x1="12" y1="18" x2="12" y2="22"></line>
<line x1="2" y1="12" x2="6" y2="12"></line>
<line x1="18" y1="12" x2="22" y2="12"></line>
<circle cx="12" cy="12" r="2" fill="#10B981"></circle>
</svg>
`)
};
export function getTabIconUri(name, color = '#94A3B8') {
switch (name) {
case 'home':
return svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
`);
case 'activity':
return svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path>
<line x1="4" y1="22" x2="4" y2="15"></line>
</svg>
`);
case 'community':
return svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M14.31 8l5.74 9.94M9.69 8h11.48M7.38 12l5.74-9.94M9.69 16L3.95 6.06M14.31 16H2.83M16.62 12l-5.74 9.94"></path>
</svg>
`);
case 'user':
return svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`);
case 'hall':
return svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1"></rect>
<rect x="14" y="3" width="7" height="7" rx="1"></rect>
<rect x="14" y="14" width="7" height="7" rx="1"></rect>
<rect x="3" y="14" width="7" height="7" rx="1"></rect>
</svg>
`);
case 'radar':
return svgToUri(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="6"></circle>
<circle cx="12" cy="12" r="2"></circle>
<path d="M12 2a10 10 0 0 1 10 10h-10z" fill="${color}" fill-opacity="0.25"></path>
</svg>
`);
default:
return '';
}
}

查看文件

@@ -254,22 +254,27 @@ export const ACTIVITIES_LIST = [
},
{
id: 'act_102',
title: '【陪唱·KTV欢唱】周五晚工体纯K麦霸减压欢唱局·房主请客包厢',
title: '【同城组队·高额悬赏】工体欢唱减压局·急招欢唱搭子',
game: '同城陪唱',
track: 'OFFLINE_SING',
payType: 'PROJECT',
totalFee: 600,
pricingMode: 'PER_PERSON',
perPersonFee: 500,
totalFee: 2000,
aaFee: 0,
maxParticipants: 6,
currentParticipants: 4,
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 },
{ id: 'p5', nickname: '高音小王子', avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100', verifyCode: '7390', checkedIn: false },
{ 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 },
],
},
{

86
pwuniapp/common/nav.js 普通文件
查看文件

@@ -0,0 +1,86 @@
/**
* 多端顶部导航与安全区自适应计算模块
* 彻底解决微信小程序动态胶囊、iPhone灵动岛、刘海屏与系统状态栏遮挡重叠问题
*/
export function getNavBarInfo() {
let statusBarHeight = 44;
let navBarHeight = 44;
let capsuleTop = 48;
let capsuleHeight = 32;
let capsuleRightSpace = 0;
let safeHeaderGap = 20; // 默认 H5 顶部留白 (px)
let totalHeaderHeight = 88;
let capsuleBottom = 80;
// #ifdef H5
safeHeaderGap = 16;
capsuleRightSpace = 0;
totalHeaderHeight = 44;
capsuleBottom = 20;
// #endif
// #ifdef MP-WEIXIN
try {
let windowInfo = {};
if (typeof uni !== 'undefined' && typeof uni.getWindowInfo === 'function') {
windowInfo = uni.getWindowInfo();
} else if (typeof wx !== 'undefined' && typeof wx.getWindowInfo === 'function') {
windowInfo = wx.getWindowInfo();
} else if (typeof uni !== 'undefined' && typeof uni.getSystemInfoSync === 'function') {
windowInfo = uni.getSystemInfoSync();
}
statusBarHeight = windowInfo.statusBarHeight || 44;
let menuButton = null;
if (uni.getMenuButtonBoundingClientRect) {
menuButton = uni.getMenuButtonBoundingClientRect();
}
if (menuButton && menuButton.top && menuButton.height) {
capsuleTop = menuButton.top;
capsuleHeight = menuButton.height;
capsuleBottom = menuButton.bottom;
const margin = Math.max(0, menuButton.top - statusBarHeight);
navBarHeight = menuButton.height + margin * 2;
const screenWidth = windowInfo.screenWidth || 375;
// 胶囊避让右边距:屏幕最右侧到胶囊左边缘的距离 + 8px 缓冲
capsuleRightSpace = (screenWidth - menuButton.left) + 8;
// 搜索栏等单行导航条精准对齐胶囊 top 坐标
safeHeaderGap = menuButton.top;
// 整个导航栏底部总安全距离
totalHeaderHeight = menuButton.bottom + 10;
} else {
safeHeaderGap = statusBarHeight + 6;
capsuleHeight = 32;
capsuleBottom = safeHeaderGap + capsuleHeight;
capsuleRightSpace = 96;
totalHeaderHeight = statusBarHeight + 44;
}
} catch (e) {
safeHeaderGap = 54;
capsuleHeight = 32;
capsuleBottom = 86;
capsuleRightSpace = 96;
totalHeaderHeight = 90;
}
// #endif
return {
statusBarHeight,
navBarHeight,
capsuleTop,
capsuleHeight,
capsuleBottom,
capsuleRightSpace,
safeHeaderGap,
totalHeaderHeight,
capsuleBelowGap: capsuleBottom + 10,
headerGapPx: `${safeHeaderGap}px`,
capsuleSpacePx: `${capsuleRightSpace}px`,
capsuleHeightPx: `${capsuleHeight}px`,
capsuleBottomPx: `${capsuleBottom}px`,
capsuleBelowGapPx: `${capsuleBottom + 10}px`,
totalHeaderHeightPx: `${totalHeaderHeight}px`
};
}

359
pwuniapp/common/qrcode.js 普通文件
查看文件

@@ -0,0 +1,359 @@
// pwuniapp/common/qrcode.js
// 轻量级原生 QR Code 矩阵生成器 (支持 QR Code Version 1-4,纠错级别 L/M,零外部依赖,100% 支持小程序与H5)
// QR 纠错级别
export const QRErrorCorrectLevel = {
L: 1, // 约 7% 纠错能力
M: 0, // 约 15% 纠错能力
Q: 3, // 约 25% 纠错能力
H: 2 // 约 30% 纠错能力
};
// 伽罗华域 GF(256) 数学表
const QRMath = {
glog(n) {
if (n < 1) throw new Error('glog(' + n + ')');
return QRMath.LOG_TABLE[n];
},
gexp(n) {
while (n < 0) n += 255;
while (n >= 256) n -= 255;
return QRMath.EXP_TABLE[n];
},
EXP_TABLE: new Array(256),
LOG_TABLE: new Array(256)
};
for (let i = 0; i < 8; i++) QRMath.EXP_TABLE[i] = 1 << i;
for (let i = 8; i < 256; i++) {
QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8];
}
for (let i = 0; i < 255; i++) QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;
class QRPolynomial {
constructor(num, shift) {
let offset = 0;
while (offset < num.length && num[offset] === 0) offset++;
this.num = new Array(num.length - offset + shift);
for (let i = 0; i < num.length - offset; i++) this.num[i] = num[i + offset];
}
get(index) {
return this.num[index];
}
getLength() {
return this.num.length;
}
multiply(e) {
const num = new Array(this.getLength() + e.getLength() - 1).fill(0);
for (let i = 0; i < this.getLength(); i++) {
for (let j = 0; j < e.getLength(); j++) {
num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j)));
}
}
return new QRPolynomial(num, 0);
}
mod(e) {
if (this.getLength() - e.getLength() < 0) return this;
const ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0));
const num = new Array(this.getLength());
for (let i = 0; i < this.getLength(); i++) num[i] = this.get(i);
for (let i = 0; i < e.getLength(); i++) num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio);
return new QRPolynomial(num, 0).mod(e);
}
}
// 块定义与版本控制
const QRRSBlock = {
RS_BLOCK_TABLE: [
[1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9], // Ver 1 (L, M, Q, H)
[1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16], // Ver 2
[1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13], // Ver 3
[1, 100, 80], [2, 50, 32], [2, 50, 24], [4, 25, 9], // Ver 4
[1, 134, 108], [2, 67, 43], [2, 33, 15, 2, 34, 16], [2, 33, 11, 2, 34, 12] // Ver 5
],
getRSBlocks(typeNumber, errorCorrectLevel) {
const rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel);
const list = [];
for (let i = 0; i < rsBlock.length / 3; i++) {
const count = rsBlock[i * 3 + 0];
const totalCount = rsBlock[i * 3 + 1];
const dataCount = rsBlock[i * 3 + 2];
for (let j = 0; j < count; j++) {
list.push({ totalCount, dataCount });
}
}
return list;
},
getRsBlockTable(typeNumber, errorCorrectLevel) {
switch (errorCorrectLevel) {
case QRErrorCorrectLevel.L: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
case QRErrorCorrectLevel.M: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
case QRErrorCorrectLevel.Q: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
case QRErrorCorrectLevel.H: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
default: return undefined;
}
}
};
class QRBitBuffer {
constructor() {
this.buffer = [];
this.length = 0;
}
get(index) {
const bufIndex = Math.floor(index / 8);
return ((this.buffer[bufIndex] >>> (7 - (index % 8))) & 1) === 1;
}
put(num, length) {
for (let i = 0; i < length; i++) {
this.putBit(((num >>> (length - i - 1)) & 1) === 1);
}
}
putBit(bit) {
const bufIndex = Math.floor(this.length / 8);
if (this.buffer.length <= bufIndex) this.buffer.push(0);
if (bit) this.buffer[bufIndex] |= 0x80 >>> (this.length % 8);
this.length++;
}
}
export class QRCodeModel {
constructor(typeNumber, errorCorrectLevel) {
this.typeNumber = typeNumber || 2;
this.errorCorrectLevel = errorCorrectLevel !== undefined ? errorCorrectLevel : QRErrorCorrectLevel.M;
this.modules = null;
this.moduleCount = 0;
this.dataList = [];
}
addData(data) {
this.dataList.push(data);
}
isDark(row, col) {
if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) {
return false;
}
return this.modules[row][col];
}
getModuleCount() {
return this.moduleCount;
}
make() {
this.makeImpl(false, this.getBestMaskPattern());
}
makeImpl(test, maskPattern) {
this.moduleCount = this.typeNumber * 4 + 17;
this.modules = new Array(this.moduleCount);
for (let row = 0; row < this.moduleCount; row++) {
this.modules[row] = new Array(this.moduleCount).fill(null);
}
this.setupPositionProbePattern(0, 0);
this.setupPositionProbePattern(this.moduleCount - 7, 0);
this.setupPositionProbePattern(0, this.moduleCount - 7);
this.setupTimingPattern();
this.setupTypeInfo(test, maskPattern);
if (this.typeNumber >= 2) this.setupPositionAdjustPattern();
const data = this.createData(this.typeNumber, this.errorCorrectLevel, this.dataList);
this.mapData(data, maskPattern);
}
setupPositionProbePattern(row, col) {
for (let r = -1; r <= 7; r++) {
if (row + r <= -1 || this.moduleCount <= row + r) continue;
for (let c = -1; c <= 7; c++) {
if (col + c <= -1 || this.moduleCount <= col + c) continue;
if (
(0 <= r && r <= 6 && (c === 0 || c === 6)) ||
(0 <= c && c <= 6 && (r === 0 || r === 6)) ||
(2 <= r && r <= 4 && 2 <= c && c <= 4)
) {
this.modules[row + r][col + c] = true;
} else {
this.modules[row + r][col + c] = false;
}
}
}
}
setupTimingPattern() {
for (let r = 8; r < this.moduleCount - 8; r++) {
if (this.modules[r][6] !== null) continue;
this.modules[r][6] = r % 2 === 0;
}
for (let c = 8; c < this.moduleCount - 8; c++) {
if (this.modules[6][c] !== null) continue;
this.modules[6][c] = c % 2 === 0;
}
}
setupPositionAdjustPattern() {
const pos = [6, this.moduleCount - 7];
for (let i = 0; i < pos.length; i++) {
for (let j = 0; j < pos.length; j++) {
const row = pos[i];
const col = pos[j];
if (this.modules[row][col] !== null) continue;
for (let r = -2; r <= 2; r++) {
for (let c = -2; c <= 2; c++) {
if (r === -2 || r === 2 || c === -2 || c === 2 || (r === 0 && c === 0)) {
this.modules[row + r][col + c] = true;
} else {
this.modules[row + r][col + c] = false;
}
}
}
}
}
}
setupTypeInfo(test, maskPattern) {
const data = (this.errorCorrectLevel << 3) | maskPattern;
const bits = this.getBCHTypeInfo(data);
for (let i = 0; i < 15; i++) {
const mod = !test && ((bits >> i) & 1) === 1;
if (i < 6) this.modules[i][8] = mod;
else if (i < 8) this.modules[i + 1][8] = mod;
else this.modules[this.moduleCount - 15 + i][8] = mod;
if (i < 8) this.modules[8][this.moduleCount - i - 1] = mod;
else if (i < 9) this.modules[8][15 - i - 1 + 1] = mod;
else this.modules[8][15 - i - 1] = mod;
}
this.modules[this.moduleCount - 8][8] = !test;
}
getBCHTypeInfo(data) {
let d = data << 10;
while (this.getBCHDigit(d) - this.getBCHDigit(1335) >= 0) {
d ^= 1335 << (this.getBCHDigit(d) - this.getBCHDigit(1335));
}
return ((data << 10) | d) ^ 21522;
}
getBCHDigit(data) {
let digit = 0;
while (data !== 0) {
digit++;
data >>>= 1;
}
return digit;
}
getBestMaskPattern() {
return 0; // 标准棋盘掩膜 Pattern 0
}
createData(typeNumber, errorCorrectLevel, dataList) {
const rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
const buffer = new QRBitBuffer();
for (let i = 0; i < dataList.length; i++) {
const data = dataList[i];
// 默认 8bit 字节编码
buffer.put(4, 4);
buffer.put(data.length, typeNumber < 10 ? 8 : 16);
for (let j = 0; j < data.length; j++) {
buffer.put(data.charCodeAt(j), 8);
}
}
let totalDataCount = 0;
for (let i = 0; i < rsBlocks.length; i++) totalDataCount += rsBlocks[i].dataCount;
if (buffer.length + 4 <= totalDataCount * 8) buffer.put(0, 4);
while (buffer.length % 8 !== 0) buffer.putBit(false);
while (true) {
if (buffer.length >= totalDataCount * 8) break;
buffer.put(236, 8);
if (buffer.length >= totalDataCount * 8) break;
buffer.put(17, 8);
}
return this.createBytes(buffer, rsBlocks);
}
createBytes(buffer, rsBlocks) {
let offset = 0;
let maxDcCount = 0;
let maxEcCount = 0;
const dcdata = new Array(rsBlocks.length);
const ecdata = new Array(rsBlocks.length);
for (let r = 0; r < rsBlocks.length; r++) {
const dcCount = rsBlocks[r].dataCount;
const ecCount = rsBlocks[r].totalCount - dcCount;
maxDcCount = Math.max(maxDcCount, dcCount);
maxEcCount = Math.max(maxEcCount, ecCount);
dcdata[r] = new Array(dcCount);
for (let i = 0; i < dcdata[r].length; i++) {
dcdata[r][i] = 0xff & buffer.buffer[i + offset];
}
offset += dcCount;
const rsPoly = this.getErrorCorrectPolynomial(ecCount);
const rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);
const modPoly = rawPoly.mod(rsPoly);
ecdata[r] = new Array(rsPoly.getLength() - 1);
for (let i = 0; i < ecdata[r].length; i++) {
const modIndex = i + modPoly.getLength() - ecdata[r].length;
ecdata[r][i] = modIndex >= 0 ? modPoly.get(modIndex) : 0;
}
}
const totalCodeCount = rsBlocks.reduce((acc, b) => acc + b.totalCount, 0);
const data = new Array(totalCodeCount);
let index = 0;
for (let i = 0; i < maxDcCount; i++) {
for (let r = 0; r < rsBlocks.length; r++) {
if (i < dcdata[r].length) data[index++] = dcdata[r][i];
}
}
for (let i = 0; i < maxEcCount; i++) {
for (let r = 0; r < rsBlocks.length; r++) {
if (i < ecdata[r].length) data[index++] = ecdata[r][i];
}
}
return data;
}
getErrorCorrectPolynomial(errorCorrectLength) {
let a = new QRPolynomial([1], 0);
for (let i = 0; i < errorCorrectLength; i++) {
a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0));
}
return a;
}
mapData(data, maskPattern) {
let inc = -1;
let row = this.moduleCount - 1;
let bitIndex = 7;
let byteIndex = 0;
for (let col = this.moduleCount - 1; col > 0; col -= 2) {
if (col === 6) col--;
while (true) {
for (let c = 0; c < 2; c++) {
if (this.modules[row][col - c] === null) {
let dark = false;
if (byteIndex < data.length) {
dark = ((data[byteIndex] >>> bitIndex) & 1) === 1;
}
const mask = (row + col - c) % 2 === 0;
if (mask) dark = !dark;
this.modules[row][col - c] = dark;
bitIndex--;
if (bitIndex === -1) {
byteIndex++;
bitIndex = 7;
}
}
}
row += inc;
if (row < 0 || this.moduleCount <= row) {
row -= inc;
inc = -inc;
break;
}
}
}
}
}
/**
* 生成 QR Code 矩阵数据
* @param {string} text 要编码的文本
* @param {number} typeNumber 版本号 1-4 (默认 2: 25x25)
* @returns {Array<Array<boolean>>} 二维矩阵,true 为黑块,false 为白块
*/
export function generateQRCodeMatrix(text, typeNumber = 2) {
if (!text) text = '0000';
// 根据文本长度自动升版
if (text.length > 32) typeNumber = 4;
else if (text.length > 16) typeNumber = 3;
const qr = new QRCodeModel(typeNumber, QRErrorCorrectLevel.M);
qr.addData(text);
qr.make();
return qr.modules;
}

68
pwuniapp/common/request.js 普通文件
查看文件

@@ -0,0 +1,68 @@
// pwuniapp/common/request.js
// 同城生活陪伴小程序统一网络请求层
// 统一API基地址配置
const BASE_URL = (() => {
// #ifdef H5
// H5环境使用相对路径 /api,通过Nginx/本地代理转发,天然规避CORS跨域
return '/api';
// #endif
// 小程序端与App端使用HTTPS公网生产地址
return 'https://peiww.gynhc.com/api';
})();
/**
* 通用网络请求函数
* @param {Object} options 请求参数
* @returns {Promise} 返回请求结果
*/
export function request(options = {}) {
let url = options.url || '';
if (!url.startsWith('http')) {
const cleanUrl = url.startsWith('/') ? url : `/${url}`;
url = `${BASE_URL}${cleanUrl}`;
}
const method = (options.method || 'GET').toUpperCase();
const header = {
'Content-Type': 'application/json',
...options.header,
};
return new Promise((resolve, reject) => {
uni.request({
url,
method,
data: options.data || {},
header,
timeout: options.timeout || 10000,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
// 适配标准的业务封装 { code: 20000, data: ... }
if (res.data && typeof res.data === 'object' && 'code' in res.data) {
if (res.data.code === 20000) {
resolve(res.data.data);
} else {
reject(new Error(res.data.message || '业务请求失败'));
}
} else {
resolve(res.data);
}
} else {
const errMsg = (res.data && res.data.message) ? res.data.message : `HTTP请求失败: ${res.statusCode}`;
reject(new Error(errMsg));
}
},
fail: (err) => {
reject(new Error(err.errMsg || '网络请求异常,请检查网络连接'));
},
});
});
}
export default {
get: (url, data, header) => request({ url, method: 'GET', data, header }),
post: (url, data, header) => request({ url, method: 'POST', data, header }),
put: (url, data, header) => request({ url, method: 'PUT', data, header }),
delete: (url, data, header) => request({ url, method: 'DELETE', data, header }),
};

175
pwuniapp/common/role.js 普通文件
查看文件

@@ -0,0 +1,175 @@
/**
* 全局双身份与角色守卫管理模块 (Role Guard Manager)
* 覆盖四大核心角色:
* 1. PLAYER: 普通买家/玩家
* 2. EXPERT: 陪玩大神/接单人
* 3. HOST: 车队房主/局长
* 4. ADMIN: 平台管理员/运营端
*/
export const ROLE_TYPES = {
PLAYER: 'PLAYER', // 发单人端(普通玩家/发起活动/找陪玩)
EXPERT: 'EXPERT', // 接单人端(大神接单/5km雷达定位/核销)
HOST: 'HOST', // 车队房主/局长(发起AA组局/动态调价/解散退款/到场核销)
ADMIN: 'ADMIN', // 平台管理员(资质审核/争议仲裁/大盘监管)
};
export const CERT_STATUS = {
NONE: 0, // 未提交
PENDING: 1, // 待审核
APPROVED: 2, // 审核通过
REJECTED: 3, // 驳回
};
const STORAGE_KEY_ROLE = 'PW_CURRENT_USER_ROLE';
const STORAGE_KEY_CERT = 'PW_EXPERT_CERT_STATUS';
export function getExpertCertStatus() {
try {
const saved = uni.getStorageSync(STORAGE_KEY_CERT);
if (saved !== '' && saved !== null && saved !== undefined) {
return Number(saved);
}
return CERT_STATUS.NONE;
} catch (e) {
return CERT_STATUS.NONE;
}
}
export function setExpertCertStatus(status) {
const target = Number(status);
try {
uni.setStorageSync(STORAGE_KEY_CERT, target);
} catch (e) {
console.error('Failed to persist cert status', e);
}
uni.$emit('cert-status-change', target);
if (target !== CERT_STATUS.APPROVED) {
setCurrentRole(ROLE_TYPES.PLAYER);
}
return target;
}
export function isExpertCertified() {
return getExpertCertStatus() === CERT_STATUS.APPROVED;
}
export function canSwitchToRole(targetRole) {
if (targetRole === ROLE_TYPES.EXPERT) {
if (!isExpertCertified()) {
return {
allowed: false,
reason: '尚未开通或通过大神实名认证,无法切换至接单模式',
};
}
}
return { allowed: true };
}
export function getCurrentRole() {
try {
const saved = uni.getStorageSync(STORAGE_KEY_ROLE);
if (saved === ROLE_TYPES.EXPERT) {
if (isExpertCertified()) {
return ROLE_TYPES.EXPERT;
}
return ROLE_TYPES.PLAYER;
}
return ROLE_TYPES.PLAYER;
} catch (e) {
return ROLE_TYPES.PLAYER;
}
}
export function setCurrentRole(role) {
let target = role === ROLE_TYPES.EXPERT ? ROLE_TYPES.EXPERT : ROLE_TYPES.PLAYER;
if (target === ROLE_TYPES.EXPERT && !isExpertCertified()) {
console.warn('[RoleGuard] 未认证大神禁止切换至接单端,已自动降级回玩家端');
target = ROLE_TYPES.PLAYER;
}
try {
uni.setStorageSync(STORAGE_KEY_ROLE, target);
} catch (e) {
console.error('Failed to persist user role', e);
}
uni.$emit('role-change', target);
return target;
}
export function toggleRole() {
const current = getCurrentRole();
if (current === ROLE_TYPES.PLAYER) {
if (!isExpertCertified()) {
return {
success: false,
currentRole: ROLE_TYPES.PLAYER,
message: '尚未通过大神实名认证,请先提交入驻认证',
};
}
setCurrentRole(ROLE_TYPES.EXPERT);
return {
success: true,
currentRole: ROLE_TYPES.EXPERT,
message: '已切换至大神接单模式',
};
} else {
setCurrentRole(ROLE_TYPES.PLAYER);
return {
success: true,
currentRole: ROLE_TYPES.PLAYER,
message: '已切回发单人端',
};
}
}
// 非大神禁止访问的受保护页面清单
export const EXPERT_GUARD_PAGES = [
'/pages/expert/workspace',
'/pages/expert/radar',
'/pages/expert/hall',
];
export function checkRoleGuard(pageUrl) {
if (!pageUrl) return { allowed: true };
const pureUrl = pageUrl.split('?')[0];
if (EXPERT_GUARD_PAGES.includes(pureUrl)) {
if (!isExpertCertified()) {
return {
allowed: false,
code: 'FORBIDDEN_NOT_EXPERT',
redirectUrl: '/pages/user/index',
message: '非大神禁止访问接单工作台与雷达,请先在个人中心完成实名认证',
};
}
}
return { allowed: true };
}
let guardInstalled = false;
export function installRoleGuard() {
if (guardInstalled) return;
guardInstalled = true;
const routeMethods = ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'];
routeMethods.forEach((method) => {
try {
if (typeof uni !== 'undefined' && typeof uni.addInterceptor === 'function') {
uni.addInterceptor(method, {
invoke(args) {
if (!args || !args.url) return true;
const check = checkRoleGuard(args.url);
if (!check.allowed) {
uni.showToast({
title: check.message || '非大神禁止访问接单页面',
icon: 'none',
duration: 2000,
});
return false;
}
return true;
},
});
}
} catch (e) {}
});
}

查看文件

@@ -84,36 +84,78 @@ export function applyThemeToNativeUI(themeKey) {
const theme = THEMES[themeKey] || THEMES[THEME_KEYS.COOL];
const { navBar, tabBar } = theme.tokens;
// 1. 设置顶部系统导航栏颜色
// 1. 设置顶部系统导航栏颜色 (仅在小程序/App支持原生导航栏时生效,防御 H5 与自定义导航)
// #ifndef H5
try {
if (typeof uni !== 'undefined' && uni.setNavigationBarColor) {
uni.setNavigationBarColor({
if (typeof uni !== 'undefined' && typeof uni.setNavigationBarColor === 'function') {
const p = uni.setNavigationBarColor({
frontColor: navBar.frontColor,
backgroundColor: navBar.backgroundColor,
animation: {
duration: 200,
timingFunc: 'easeOut'
}
},
success: () => {},
fail: () => {}
});
if (p && typeof p.catch === 'function') {
p.catch(() => {});
}
}
} catch (e) {
// 忽略特定页面无系统导航栏异常
}
// #endif
// 2. 设置底部原生 TabBar 菜单颜色
// 2. 设置底部原生 TabBar 菜单颜色 (仅在原生 TabBar 激活时生效,防御使用自定义 TabBar 组件或原生已隐藏)
// #ifndef H5
try {
if (typeof uni !== 'undefined' && uni.setTabBarStyle) {
uni.setTabBarStyle({
if (typeof uni !== 'undefined' && typeof uni.setTabBarStyle === 'function') {
const p = uni.setTabBarStyle({
color: tabBar.color,
selectedColor: tabBar.selectedColor,
backgroundColor: tabBar.backgroundColor,
borderStyle: tabBar.borderStyle
borderStyle: tabBar.borderStyle,
success: () => {},
fail: () => {}
});
if (p && typeof p.catch === 'function') {
p.catch(() => {});
}
}
} catch (e) {
// 忽略非 TabBar 页面抛出的异常
}
// 3. 动态设置窗口底层原生背景色与下拉刷新底色 (彻底消除页面路由切换时的瞬间黑屏闪烁)
try {
if (typeof uni !== 'undefined' && typeof uni.setBackgroundColor === 'function') {
const p = uni.setBackgroundColor({
backgroundColor: theme.tokens.bgPrimary,
backgroundColorTop: theme.tokens.bgPrimary,
backgroundColorBottom: theme.tokens.bgPrimary,
success: () => {},
fail: () => {}
});
if (p && typeof p.catch === 'function') {
p.catch(() => {});
}
}
if (typeof uni !== 'undefined' && typeof uni.setBackgroundTextStyle === 'function') {
const p = uni.setBackgroundTextStyle({
textStyle: themeKey === THEME_KEYS.WARM ? 'dark' : 'light',
success: () => {},
fail: () => {}
});
if (p && typeof p.catch === 'function') {
p.catch(() => {});
}
}
} catch (e) {
// 忽略特定平台背景色设置异常
}
// #endif
// 3. 在 H5 环境下同步在 body 与 documentElement 上添加/移除主题 class,并同步设置 CSS 变量
try {
if (typeof document !== 'undefined') {