feat: 完善对象存储上传与真实条码二维码引擎,彻底剥离保底Mock数据
这个提交包含在:
681
pwuniapp/common/api.js
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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') {
|
||||
|
||||
在新工单中引用
屏蔽一个用户