feat: 完善分销中心与团队明细接口,增强商户与帖子交互细节

这个提交包含在:
2026-10-24 15:43:24 +08:00
父节点 f179b90fb4
当前提交 6b86177b5f
共有 20 个文件被更改,包括 2205 次插入 和 757 次删除

查看文件

@@ -41,7 +41,7 @@ class AdminMember extends V1
$list = []; $list = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$isMember = (int)$row['is_member'] === 1 $isMember = (int)$row['is_member'] === 1
&& (!$row['member_expiretime'] || (int)$row['member_expiretime'] > $now); && !empty($row['member_expiretime']) && (int)$row['member_expiretime'] > $now;
$list[] = [ $list[] = [
'id' => (int)$row['id'], 'id' => (int)$row['id'],
'nickname' => (string)$row['nickname'], 'nickname' => (string)$row['nickname'],

查看文件

@@ -9,14 +9,19 @@ use think\Db;
* 路由: * 路由:
* GET /api/v1/distribution/overview D1 分销概览 * GET /api/v1/distribution/overview D1 分销概览
* GET /api/v1/distribution/team D2 我的团队 * GET /api/v1/distribution/team D2 我的团队
* POST /api/v1/distribution/withdraw D3 申请提现 * GET /api/v1/distribution/commissions D3 分销收益明细
* GET /api/v1/distribution/withdraw-records D4 提现记录 * GET /api/v1/distribution/poster D4 推广海报要素
* POST /api/v1/distribution/withdraw D5 申请提现
* GET /api/v1/distribution/withdraw-records D6 提现记录
*/ */
class Distribution extends V1 class Distribution extends V1
{ {
/** /**
* D1 分销概览 * D1 分销概览
* GET /api/v1/distribution/overview → {totalCommission,settled,withdrawable,todayCommission,teamCount,l1Count,l2Count,inviteCode,posterUrl,minWithdraw} * GET /api/v1/distribution/overview
* 获取用户分销核心资产数据、团队人数与分销基础配置
*
* @return void 输出包含累计佣金、已结算、待结算、可提现、今日收益、邀请码及分销规则配置的JSON响应
*/ */
public function overview() public function overview()
{ {
@@ -38,35 +43,45 @@ class Distribution extends V1
$l2 = (int)Db::name('fc_dist_relation')->where('parent_id', $uid)->where('level', 2)->count(); $l2 = (int)Db::name('fc_dist_relation')->where('parent_id', $uid)->where('level', 2)->count();
$ext = $this->ext($uid); $ext = $this->ext($uid);
$code = (string)$ext['invite_code'];
$shareUrl = 'https://fenleixinxi.gzyakj.com.cn/#/pages/index/index?ic=' . urlencode($code);
// 商家转账确认收款页需要的商户号与小程序 AppID(非敏感,可下发) // 商家转账确认收款页需要的商户号与小程序 AppID(非敏感,可下发)
$epay = (array)get_addon_config('epay'); $epay = (array)get_addon_config('epay');
$wx = isset($epay['wechat']) && is_array($epay['wechat']) ? $epay['wechat'] : []; $wx = isset($epay['wechat']) && is_array($epay['wechat']) ? $epay['wechat'] : [];
$this->ok([ $this->ok([
'totalCommission' => round($total, 2), 'totalCommission' => round($total, 2),
'settled' => round($settled, 2), 'settled' => round($settled, 2),
'pendingCommission' => round($pending, 2), 'pendingCommission' => round($pending, 2),
'withdrawable' => round(max(0, $withdrawable), 2), 'withdrawable' => round(max(0, $withdrawable), 2),
'todayCommission' => round($todayCommission, 2), 'todayCommission' => round($todayCommission, 2),
'teamCount' => $l1 + $l2, 'teamCount' => $l1 + $l2,
'l1Count' => $l1, 'l1Count' => $l1,
'l2Count' => $l2, 'l2Count' => $l2,
'inviteCode' => (string)$ext['invite_code'], 'inviteCode' => $code,
'posterUrl' => '', 'shareUrl' => $shareUrl,
// 最低提现金额(后台「分销设置」下发,替代前端写死的 50) 'posterUrl' => '',
'minWithdraw' => (float)$this->cfg('dist.minWithdraw', 50), // 最低提现金额(后台「分销设置」下发,默认 50)
'minWithdraw' => (float)$this->cfg('dist.minWithdraw', 50),
// 分销比例与配置规则
'level1Rate' => (float)$this->cfg('dist.level1Rate', 20),
'level2Rate' => (float)$this->cfg('dist.level2Rate', 10),
'settleCycle' => (string)$this->cfg('dist.settleCycle', 'T+7'),
'enabled' => (int)$this->cfg('dist.enabled', 1),
// 提现收款人实名(未填写时前端需先采集) // 提现收款人实名(未填写时前端需先采集)
'realname' => isset($ext['realname']) ? (string)$ext['realname'] : '', 'realname' => isset($ext['realname']) ? (string)$ext['realname'] : '',
'transferMchId' => isset($wx['mch_id']) ? (string)$wx['mch_id'] : '', 'transferMchId' => isset($wx['mch_id']) ? (string)$wx['mch_id'] : '',
'transferAppId' => isset($wx['miniapp_id']) ? (string)$wx['miniapp_id'] : '', 'transferAppId' => isset($wx['miniapp_id']) ? (string)$wx['miniapp_id'] : '',
]); ]);
} }
/** /**
* D2 我的团队 * D2 我的团队
* GET /api/v1/distribution/team 入参 {level?,page,size} * GET /api/v1/distribution/team 入参 {level?,page,size}
* → 分页 {list:[{id,nickname,level,joinedAt,performance,commission}],page,size,total,hasMore} * 分页查询直推(1级)和间推(2级)下线团队成员
*
* @return void 输出团队成员分页列表JSON
*/ */
public function team() public function team()
{ {
@@ -85,19 +100,31 @@ class Distribution extends V1
if ($forCount) { if ($forCount) {
return $q; return $q;
} }
return $q->field('r.id,r.user_id,r.level,r.createtime,u.nickname') return $q->field('r.id,r.user_id,r.level,r.createtime,u.nickname,u.avatar,u.mobile')
->order('r.createtime desc'); ->order('r.createtime desc');
}); });
$list = []; $list = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$childId = (int)$row['user_id']; $childId = (int)$row['user_id'];
$commission = (float)Db::name('fc_commission')->where('from_user_id', $childId)->sum('amount'); $commission = (float)Db::name('fc_commission')
$orderAmount = (float)Db::name('fc_order')->where('user_id', $childId)->where('status', 'paid')->sum('amount'); ->where('user_id', $uid)
->where('from_user_id', $childId)
->sum('amount');
$orderAmount = (float)Db::name('fc_order')
->where('user_id', $childId)
->where('status', 'paid')
->sum('amount');
$performance = $orderAmount > 0 ? $orderAmount : $commission * 5; $performance = $orderAmount > 0 ? $orderAmount : $commission * 5;
$mobile = (string)(isset($row['mobile']) ? $row['mobile'] : '');
$maskedMobile = (strlen($mobile) === 11) ? substr($mobile, 0, 3) . '****' . substr($mobile, -4) : '';
$list[] = [ $list[] = [
'id' => $childId, 'id' => $childId,
'nickname' => (string)$row['nickname'], 'nickname' => (string)$row['nickname'],
'avatar' => (string)(isset($row['avatar']) ? $row['avatar'] : ''),
'mobile' => $maskedMobile,
'level' => (int)$row['level'], 'level' => (int)$row['level'],
'joinedAt' => $this->iso($row['createtime']), 'joinedAt' => $this->iso($row['createtime']),
'performance' => round($performance, 2), 'performance' => round($performance, 2),
@@ -109,9 +136,121 @@ class Distribution extends V1
} }
/** /**
* D3 申请提现 * D3 分销收益明细(佣金流水账单)
* GET /api/v1/distribution/commissions 入参 {status?,level?,page,size}
* 分页查询下线消费产生的佣金记录明细及结算状态
*
* @return void 输出佣金明细列表与汇总统计JSON
*/
public function commissions()
{
$user = $this->auth();
$uid = (int)$user['id'];
$status = $this->input('status', '');
$level = $this->input('level', '');
list($rows, $total, $page, $size) = $this->pageQuery(function ($forCount) use ($uid, $status, $level) {
$q = Db::name('fc_commission')->alias('c')
->join('user u', 'u.id = c.from_user_id', 'LEFT')
->join('fc_order o', 'o.order_no = c.order_no', 'LEFT')
->where('c.user_id', $uid);
if ($status !== '' && $status !== null) {
$q->where('c.status', (int)$status);
}
if ($level !== '' && $level !== null) {
$q->where('c.level', (int)$level);
}
if ($forCount) {
return $q;
}
return $q->field('c.*,u.nickname as from_nickname,u.avatar as from_avatar,o.title as order_title,o.amount as order_amount')
->order('c.createtime desc');
});
$typeMap = [
'post' => '信息发布/置顶',
'member' => 'VIP会员购买',
'payview' => '付费查看信息',
'refresh' => '信息一键刷新',
'merchant' => '商家认证入驻',
];
$statusMap = [
0 => '待结算',
1 => '已结算',
2 => '已失效',
];
$list = [];
foreach ($rows as $row) {
$orderType = (string)(isset($row['order_type']) ? $row['order_type'] : 'post');
$st = (int)$row['status'];
$list[] = [
'id' => (int)$row['id'],
'orderNo' => (string)$row['order_no'],
'orderType' => $orderType,
'orderTypeName' => isset($typeMap[$orderType]) ? $typeMap[$orderType] : (isset($row['order_title']) && $row['order_title'] ? $row['order_title'] : '同城消费'),
'orderAmount' => round((float)(isset($row['order_amount']) ? $row['order_amount'] : 0), 2),
'amount' => round((float)$row['amount'], 2),
'rate' => (float)$row['rate'],
'level' => (int)$row['level'],
'status' => $st,
'statusText' => isset($statusMap[$st]) ? $statusMap[$st] : '处理中',
'settleDue' => !empty($row['settle_due']) ? $this->iso($row['settle_due']) : '',
'settletime' => !empty($row['settletime']) ? $this->iso($row['settletime']) : '',
'createdAt' => $this->iso($row['createtime']),
'fromUser' => [
'id' => (int)$row['from_user_id'],
'nickname' => (string)(isset($row['from_nickname']) && $row['from_nickname'] !== '' ? $row['from_nickname'] : '同城好友'),
'avatar' => (string)(isset($row['from_avatar']) ? $row['from_avatar'] : ''),
],
];
}
$res = $this->paginate($list, $total, $page, $size);
$res['summary'] = [
'totalCommission' => round((float)Db::name('fc_commission')->where('user_id', $uid)->where('status', '<>', 2)->sum('amount'), 2),
'settledCommission' => round((float)Db::name('fc_commission')->where('user_id', $uid)->where('status', 1)->sum('amount'), 2),
'pendingCommission' => round((float)Db::name('fc_commission')->where('user_id', $uid)->where('status', 0)->sum('amount'), 2),
];
$this->ok($res);
}
/**
* D4 分销海报生成要素
* GET /api/v1/distribution/poster
* 获取用于前端合成或分享专属推广海报的元数据
*
* @return void 输出海报配置与素材信息的JSON
*/
public function poster()
{
$user = $this->auth();
$uid = (int)$user['id'];
$ext = $this->ext($uid);
$siteName = (string)$this->cfg('app.name', '同城分类信息');
$slogan = (string)$this->cfg('dist.slogan', '同城便民生活 · 随时随地发布与求助');
$code = (string)$ext['invite_code'];
$this->ok([
'inviteCode' => $code,
'siteName' => $siteName,
'slogan' => $slogan,
'nickname' => (string)(isset($user['nickname']) && $user['nickname'] !== '' ? $user['nickname'] : '同城合伙人'),
'avatar' => (string)(isset($user['avatar']) ? $user['avatar'] : ''),
'path' => 'pages/index/index?ic=' . $code,
]);
}
/**
* D5 申请提现
* POST /api/v1/distribution/withdraw 入参 {amount,channel?} → {withdrawNo,status:'auditing'} * POST /api/v1/distribution/withdraw 入参 {amount,channel?} → {withdrawNo,status:'auditing'}
* 规则 BR-09:金额必填且 >0、且不超过可提现余额 * 规则 BR-09:金额必填且 >0、且不超过可提现余额
*
* @return void 输出提现单号与状态JSON
*/ */
public function withdraw() public function withdraw()
{ {
@@ -172,8 +311,11 @@ class Distribution extends V1
} }
/** /**
* D4 提现记录 * D6 提现记录
* GET /api/v1/distribution/withdraw-records → [{id,withdrawNo,amount,status,createdAt}] * GET /api/v1/distribution/withdraw-records → [{id,withdrawNo,amount,status,createdAt}]
* 获取当前用户的历史提现记录与商家转账确认参数
*
* @return void 输出提现历史记录列表JSON
*/ */
public function withdrawRecords() public function withdrawRecords()
{ {
@@ -199,7 +341,11 @@ class Distribution extends V1
} }
/** /**
* 计算当前用户可提现余额
* 可提现余额 = Σ已结算佣金(status=1) - Σ(申请中/已打款)提现 * 可提现余额 = Σ已结算佣金(status=1) - Σ(申请中/已打款)提现
*
* @param int $uid 用户ID
* @return float 可提现余额数值
*/ */
protected function withdrawable($uid) protected function withdrawable($uid)
{ {
@@ -208,4 +354,4 @@ class Distribution extends V1
->where('status', 'in', ['auditing', 'paying', 'paid'])->sum('amount'); ->where('status', 'in', ['auditing', 'paying', 'paid'])->sum('amount');
return $settled - $applied; return $settled - $applied;
} }
} }

查看文件

@@ -83,7 +83,7 @@ class Member extends V1
$now = time(); $now = time();
$ext = $this->ext($me['id']); $ext = $this->ext($me['id']);
$isMember = (int)$ext['is_member'] === 1 $isMember = (int)$ext['is_member'] === 1
&& (empty($ext['member_expiretime']) || (int)$ext['member_expiretime'] > $now); && !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now;
$today = date('Y-m-d'); $today = date('Y-m-d');
$freeViewUsed = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0; $freeViewUsed = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
$freeViewLeft = $isMember ? max(0, $freeViewQuota - $freeViewUsed) : 0; $freeViewLeft = $isMember ? max(0, $freeViewQuota - $freeViewUsed) : 0;
@@ -123,7 +123,10 @@ class Member extends V1
$this->fail('请选择会员套餐'); $this->fail('请选择会员套餐');
} }
$payType = (string)$this->input('payType', ''); $payType = (string)$this->input('payType', 'wxpay');
if ($payType === '') {
$payType = 'wxpay';
}
if (!in_array($payType, ['wxpay', 'balance'], true)) { if (!in_array($payType, ['wxpay', 'balance'], true)) {
$this->fail('支付方式不支持'); $this->fail('支付方式不支持');
} }

查看文件

@@ -62,6 +62,35 @@ class Merchant extends V1
// 首屏拉取前 6 条该商家的有效发帖 // 首屏拉取前 6 条该商家的有效发帖
$posts = $this->fetchMerchantPosts($m['user_id'], 1, 6); $posts = $this->fetchMerchantPosts($m['user_id'], 1, 6);
// 严格保护商家联系电话:未付费解锁或非会员状态下脱敏,杜绝从信息详情页跳转商家主页白嫖绕过
$rawContact = (string)(isset($m['contact']) ? $m['contact'] : '');
$unlocked = false;
$me = $this->user();
if ($me) {
$now = time();
$ext = $this->ext($me['id']);
$isMember = (int)$ext['is_member'] === 1
&& !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now;
$isOwner = (int)$m['user_id'] === (int)$me['id'];
if ($isOwner || $isMember) {
$unlocked = true;
} else {
// 检查是否从已解锁的关联信息跳转而来
$postId = (int)$this->input('postId', 0);
if ($postId > 0) {
$paidView = Db::name('fc_pay_view')
->where('user_id', $me['id'])
->where('post_id', $postId)
->find();
if ($paidView) {
$unlocked = true;
}
}
}
}
$displayContact = $unlocked ? $rawContact : $this->maskPhone($rawContact);
$data = [ $data = [
'id' => (int)$m['id'], 'id' => (int)$m['id'],
'userId' => (int)$m['user_id'], 'userId' => (int)$m['user_id'],
@@ -71,7 +100,8 @@ class Merchant extends V1
'address' => (string)(isset($m['address']) ? $m['address'] : ''), 'address' => (string)(isset($m['address']) ? $m['address'] : ''),
'lat' => (isset($m['lat']) && $m['lat'] !== null && $m['lat'] !== '') ? (float)$m['lat'] : null, 'lat' => (isset($m['lat']) && $m['lat'] !== null && $m['lat'] !== '') ? (float)$m['lat'] : null,
'lng' => (isset($m['lng']) && $m['lng'] !== null && $m['lng'] !== '') ? (float)$m['lng'] : null, 'lng' => (isset($m['lng']) && $m['lng'] !== null && $m['lng'] !== '') ? (float)$m['lng'] : null,
'contact' => (string)(isset($m['contact']) ? $m['contact'] : ''), 'contact' => $displayContact,
'unlocked' => $unlocked,
'category' => (string)(isset($m['category']) ? $m['category'] : ''), 'category' => (string)(isset($m['category']) ? $m['category'] : ''),
'notice' => (string)(isset($m['notice']) ? $m['notice'] : ''), 'notice' => (string)(isset($m['notice']) ? $m['notice'] : ''),
'auditStatus' => (int)$m['audit_status'], 'auditStatus' => (int)$m['audit_status'],

查看文件

@@ -186,7 +186,10 @@ class Post extends V1
if ($plan === 'single') { if ($plan === 'single') {
// 单条付费 = 付费发布标准(免费发布条数用完后即按此计费);会员按折扣率计价 // 单条付费 = 付费发布标准(免费发布条数用完后即按此计费);会员按折扣率计价
$amount = $unitPrice; $amount = $unitPrice;
$payType = trim((string)$this->input('payType', '')); $payType = trim((string)$this->input('payType', 'wxpay'));
if ($payType === '') {
$payType = 'wxpay';
}
if ($amount > 0 && !in_array($payType, ['wxpay', 'balance'], true)) { if ($amount > 0 && !in_array($payType, ['wxpay', 'balance'], true)) {
$this->fail('支付方式不支持'); $this->fail('支付方式不支持');
} }
@@ -505,7 +508,7 @@ class Post extends V1
'freeQuotaLeft' => $free['unlimited'] ? -1 : $free['left'], 'freeQuotaLeft' => $free['unlimited'] ? -1 : $free['left'],
'freeUnlimited' => $free['unlimited'], 'freeUnlimited' => $free['unlimited'],
'quotaExpireAt' => $quotaExpire > 0 ? $this->iso($quotaExpire) : '', 'quotaExpireAt' => $quotaExpire > 0 ? $this->iso($quotaExpire) : '',
'isMember' => (int)$ext['is_member'] === 1 && ($memberExpire === 0 || $memberExpire > $now), 'isMember' => (int)$ext['is_member'] === 1 && !empty($memberExpire) && $memberExpire > $now,
'memberExpireAt' => $memberExpire > 0 ? $this->iso($memberExpire) : '', 'memberExpireAt' => $memberExpire > 0 ? $this->iso($memberExpire) : '',
]); ]);
} }
@@ -854,7 +857,7 @@ class Post extends V1
if ($me) { if ($me) {
$ext = $this->ext($me['id']); $ext = $this->ext($me['id']);
$isMember = (int)$ext['is_member'] === 1 $isMember = (int)$ext['is_member'] === 1
&& (empty($ext['member_expiretime']) || (int)$ext['member_expiretime'] > $now); && !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now;
$isOwner = (int)$post['user_id'] === (int)$me['id']; $isOwner = (int)$post['user_id'] === (int)$me['id'];
$paid = (bool)Db::name('fc_pay_view') $paid = (bool)Db::name('fc_pay_view')
->where('user_id', $me['id']) ->where('user_id', $me['id'])
@@ -974,18 +977,33 @@ class Post extends V1
$this->fail('信息不存在', 404); $this->fail('信息不存在', 404);
} }
$phone = (string)$post['phone'];
$wechat = $post['wechat'] !== '' ? (string)$post['wechat'] : ('tongcheng_' . $id);
$contactName = (string)(isset($post['contact']) ? $post['contact'] : '');
$contact = [ $contact = [
'phone' => (string)$post['phone'], 'name' => $contactName,
'wechat' => $post['wechat'] !== '' ? (string)$post['wechat'] : ('tongcheng_' . $id), 'phone' => $phone,
'wechat' => $wechat,
]; ];
// 已解锁直接返回 // 已解锁直接返回(严格返回纯净扁平字段,杜绝 JSON 嵌套导致前端格式错乱)
$exist = Db::name('fc_pay_view') $exist = Db::name('fc_pay_view')
->where('user_id', $user['id']) ->where('user_id', $user['id'])
->where('post_id', $id) ->where('post_id', $id)
->find(); ->find();
if ($exist) { if ($exist) {
$this->ok(['unlocked' => true, 'contact' => $contact]); $today = date('Y-m-d');
$usedToday = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
$this->ok([
'unlocked' => true,
'phone' => $phone,
'contact' => $phone,
'contactName' => $contactName,
'wechat' => $wechat,
'contactInfo' => $contact,
'freeViewLeft' => $isMember ? max(0, $quota - $usedToday) : 0,
]);
} }
$amount = round((float)$this->cfg('fee.payView', 2), 2); $amount = round((float)$this->cfg('fee.payView', 2), 2);
@@ -993,7 +1011,7 @@ class Post extends V1
$ext = $this->ext($user['id']); $ext = $this->ext($user['id']);
$isMember = (int)$ext['is_member'] === 1 $isMember = (int)$ext['is_member'] === 1
&& (!$ext['member_expiretime'] || $ext['member_expiretime'] > $now); && !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now;
$quota = $this->memberFreeViewQuota(); $quota = $this->memberFreeViewQuota();
$orderId = 0; $orderId = 0;
@@ -1070,7 +1088,11 @@ class Post extends V1
$this->ok([ $this->ok([
'unlocked' => true, 'unlocked' => true,
'contact' => $contact, 'phone' => $phone,
'contact' => $phone,
'contactName' => $contactName,
'wechat' => $wechat,
'contactInfo' => $contact,
'freeViewLeft' => $isMember ? max(0, $quota - $usedNow) : 0, 'freeViewLeft' => $isMember ? max(0, $quota - $usedNow) : 0,
'usedFree' => isset($usedFree) ? $usedFree : false, 'usedFree' => isset($usedFree) ? $usedFree : false,
]); ]);
@@ -1362,7 +1384,7 @@ class Post extends V1
{ {
$now = time(); $now = time();
$isMember = (int)$ext['is_member'] === 1 $isMember = (int)$ext['is_member'] === 1
&& (empty($ext['member_expiretime']) || (int)$ext['member_expiretime'] > $now); && !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now;
if (!$isMember) { if (!$isMember) {
return 1.0; return 1.0;
} }

查看文件

@@ -558,7 +558,7 @@ class V1 extends Api
$user = $user ?: $this->auth(); $user = $user ?: $this->auth();
$ext = $this->ext($user['id']); $ext = $this->ext($user['id']);
$isMember = (int)$ext['is_member'] === 1 $isMember = (int)$ext['is_member'] === 1
&& (!$ext['member_expiretime'] || $ext['member_expiretime'] > time()); && !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > time();
// 查询商家认证状态与商铺资料 // 查询商家认证状态与商铺资料
$merchant = Db::name('fc_merchant')->where('user_id', $user['id'])->find(); $merchant = Db::name('fc_merchant')->where('user_id', $user['id'])->find();
$isMerchant = $merchant && (int)$merchant['audit_status'] === 1 && $merchant['shop_status'] === 'normal'; $isMerchant = $merchant && (int)$merchant['audit_status'] === 1 && $merchant['shop_status'] === 'normal';

查看文件

@@ -95,6 +95,8 @@ return [
/* =============== 五、用户端 · 分销中心(Module-DIST) =============== */ /* =============== 五、用户端 · 分销中心(Module-DIST) =============== */
'api/v1/distribution/overview' => 'api/Distribution/overview', 'api/v1/distribution/overview' => 'api/Distribution/overview',
'api/v1/distribution/team' => 'api/Distribution/team', 'api/v1/distribution/team' => 'api/Distribution/team',
'api/v1/distribution/commissions' => 'api/Distribution/commissions',
'api/v1/distribution/poster' => 'api/Distribution/poster',
'api/v1/distribution/withdraw' => 'api/Distribution/withdraw', 'api/v1/distribution/withdraw' => 'api/Distribution/withdraw',
'api/v1/distribution/withdraw-records' => 'api/Distribution/withdrawRecords', 'api/v1/distribution/withdraw-records' => 'api/Distribution/withdrawRecords',

查看文件

@@ -12,10 +12,33 @@ export default {
console.log('App Hide') console.log('App Hide')
}, },
methods: { methods: {
/**
* 捕获并解析邀请分销参数(支持普通 query 与微信小程序码 scene 场景值)
* @param {Object} options 小程序启动或页面入参
*/
captureInvite(options) { captureInvite(options) {
const query = (options && options.query) || {} const query = (options && options.query) || {}
const ic = query.ic || query.inviteCode || query.invite_code let ic = query.ic || query.inviteCode || query.invite_code
const inviterId = query.inviterId || query.inviter_id let inviterId = query.inviterId || query.inviter_id
// 解析微信小程序码携带的 scene 场景值(如 scene=ic_ABCD 或 scene=ABCD 或 scene=ic=ABCD)
if (!ic && query.scene) {
try {
const sceneStr = decodeURIComponent(query.scene)
if (sceneStr.indexOf('ic_') === 0) {
ic = sceneStr.slice(3)
} else if (sceneStr.indexOf('ic=') === 0) {
ic = sceneStr.slice(3)
} else if (sceneStr.indexOf('uid_') === 0) {
inviterId = sceneStr.slice(4)
} else if (sceneStr.indexOf('uid=') === 0) {
inviterId = sceneStr.slice(4)
} else if (/^[A-Za-z0-9]{4,16}$/.test(sceneStr)) {
ic = sceneStr
}
} catch (e) {}
}
if (ic) { if (ic) {
uni.setStorageSync('fc_invite_code', String(ic)) uni.setStorageSync('fc_invite_code', String(ic))
} }

查看文件

@@ -305,7 +305,7 @@ rm -rf /tmp/fcmp && cp -R "$OUTPUT_DIR" /tmp/fcmp
```js ```js
// 'balance' 余额支付 / 'wxpay' 微信支付 // 'balance' 余额支付 / 'wxpay' 微信支付
defaultPayType: 'balance', defaultPayType: 'wxpay',
``` ```
配置好真实商户参数后改回 `'wxpay'` 即可,**各页面无需改动**(三个支付点都读这个值)。 配置好真实商户参数后改回 `'wxpay'` 即可,**各页面无需改动**(三个支付点都读这个值)。

查看文件

@@ -1,14 +1,18 @@
/** /**
* 接口模块 · 分销中心(用户端) * 接口模块 · 分销中心(用户端)
* 对应模块:分销首页 / 团队管理 / 佣金提现 * 对应模块:分销首页 / 团队管理 / 收益明细 / 推广海报 / 佣金提现
*/ */
import { get, post } from '../utils/request.js' import { get, post } from '../utils/request.js'
export const distributionApi = { export const distributionApi = {
/** 佣金概览(含邀请码/海报) */ /** 佣金概览(含邀请码/分销规则) */
overview: () => get('/distribution/overview', {}, { silent: true }), overview: () => get('/distribution/overview', {}, { silent: true }),
/** 一/二级下线列表与业绩 */ /** 一/二级下线列表与业绩 */
team: (params = {}) => get('/distribution/team', params, { silent: true }), team: (params = {}) => get('/distribution/team', params, { silent: true }),
/** 分销收益明细账单(佣金流水) */
commissions: (params = {}) => get('/distribution/commissions', params, { silent: true }),
/** 专属推广海报要素 */
poster: () => get('/distribution/poster', {}, { silent: true }),
/** 申请提现(提现至微信零钱) */ /** 申请提现(提现至微信零钱) */
withdraw: (payload) => post('/distribution/withdraw', payload, { silent: true }), withdraw: (payload) => post('/distribution/withdraw', payload, { silent: true }),
/** 提现记录 */ /** 提现记录 */

查看文件

@@ -75,9 +75,7 @@ export const config = {
// 强制使用 mock(联调前本地联调可置 true) // 强制使用 mock(联调前本地联调可置 true)
forceMock: false, forceMock: false,
// 默认支付方式:'balance' 余额支付 / 'wxpay' 微信支付 // 默认支付方式:'balance' 余额支付 / 'wxpay' 微信支付
// ⚠️ 微信支付商户参数(mchId/apiKey)尚未配置,测试阶段默认走余额支付; defaultPayType: 'wxpay',
// 配置好真实商户参数后改回 'wxpay' 即可,无需改动各页面。
defaultPayType: 'balance',
// 本地存储键 // 本地存储键
storageKeys: { storageKeys: {
token: 'fc_token', token: 'fc_token',

查看文件

@@ -129,7 +129,16 @@ export function request(options = {}) {
{ url, method, data, silent, runtime, resolve, reject } { url, method, data, silent, runtime, resolve, reject }
) )
} }
const body = res.data || {} let body = res.data || {}
// 若真机或网关未自动反序列化 JSON,或响应携带 UTF-8 BOM 字符,做安全容错剥离并反序列化
if (typeof body === 'string') {
try {
const cleaned = body.replace(/^\uFEFF/, '').trim()
body = JSON.parse(cleaned)
} catch (e) {
// 保留原 body
}
}
// 业务层 401 判定 // 业务层 401 判定
if (body.code === 401) { if (body.code === 401) {
onUnauthorized(runtime) onUnauthorized(runtime)

查看文件

@@ -153,7 +153,8 @@
"style": { "style": {
"navigationBarTitleText": "分销中心", "navigationBarTitleText": "分销中心",
"navigationStyle": "custom", "navigationStyle": "custom",
"backgroundColor": "#f2fef7" "backgroundColor": "#f2fef7",
"enablePullDownRefresh": true
} }
}, },
{ {
@@ -248,4 +249,4 @@
] ]
}, },
"uniIdRouter": {} "uniIdRouter": {}
} }

查看文件

@@ -477,12 +477,12 @@
</view> </view>
</view> </view>
<!-- 隐藏的离屏 Canvas,用于高质量绘制海报(640 x 1020 逻辑分辨率) --> <!-- 离屏高质量 Canvas(640 x 1000 逻辑分辨率,导出 2x 超清海报) -->
<canvas <canvas
canvas-id="postPosterCanvas" canvas-id="postPosterCanvas"
id="postPosterCanvas" id="postPosterCanvas"
class="detail__poster-canvas" class="detail__poster-canvas"
style="width: 640px; height: 1020px; position: fixed; left: -9999px; top: -9999px;" style="width: 640px; height: 1000px; position: fixed; left: 2000px; top: 0; z-index: -999; opacity: 0; pointer-events: none;"
/> />
</view> </view>
</template> </template>
@@ -671,6 +671,7 @@ export default {
}, },
onLoad(query) { onLoad(query) {
this.id = Number(query.id || 101) this.id = Number(query.id || 101)
this.posterImagePath = ''
loadUiConfig() loadUiConfig()
this.fetch() this.fetch()
}, },
@@ -813,11 +814,29 @@ export default {
if (res.freeViewLeft != null) this.freeViewLeft = Number(res.freeViewLeft) if (res.freeViewLeft != null) this.freeViewLeft = Number(res.freeViewLeft)
if (res.payViewPrice != null) this.payViewPrice = Number(res.payViewPrice) if (res.payViewPrice != null) this.payViewPrice = Number(res.payViewPrice)
} }
let phoneStr = ''
let wechatStr = ''
let nameStr = ''
if (this.unlocked && res) {
if (res.contactInfo && typeof res.contactInfo === 'object') {
phoneStr = String(res.contactInfo.phone || '')
wechatStr = String(res.contactInfo.wechat || '')
nameStr = String(res.contactInfo.name || res.contactName || '')
} else if (res.contact && typeof res.contact === 'object') {
phoneStr = String(res.contact.phone || '')
wechatStr = String(res.contact.wechat || '')
nameStr = String(res.contact.name || res.contactName || '')
} else {
phoneStr = String(res.phone || (typeof res.contact === 'string' ? res.contact : ''))
wechatStr = String(res.wechat || '')
nameStr = String(res.contactName || (res.publisher && res.publisher.name) || '')
}
}
this.contact = this.unlocked this.contact = this.unlocked
? { ? {
name: (res && (res.contactName || res.publisher?.name)) || '', name: nameStr,
phone: (res && (res.phone || res.contact)) || '', phone: phoneStr,
wechat: (res && res.wechat) || '', wechat: wechatStr,
} }
: { name: '', phone: '', wechat: '' } : { name: '', phone: '', wechat: '' }
@@ -1096,7 +1115,7 @@ export default {
return return
} }
uni.navigateTo({ uni.navigateTo({
url: '/pages/merchant/detail?id=' + shopId, url: `/pages/merchant/detail?id=${shopId}&postId=${this.id}`,
fail: (err) => { fail: (err) => {
console.warn('跳转商家详情失败:', err) console.warn('跳转商家详情失败:', err)
} }
@@ -1170,7 +1189,7 @@ export default {
}) })
}, },
/** /**
* 打开海报弹窗并异步生成海报 * 打开海报弹窗并按需异步生成海报
*/ */
openPosterModal() { openPosterModal() {
this.showPoster = true this.showPoster = true
@@ -1179,8 +1198,9 @@ export default {
} }
}, },
/** /**
* 辅助函数:异步获取图片本地路径(网络图片需下载) * 辅助函数:异步获取图片本地可用路径(网络图片自动缓存到临时路径)
* @param {String} src 图片链接 * @param {String} src 图片 URL
* @returns {Promise<String>}
*/ */
getImageLocalPath(src) { getImageLocalPath(src) {
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -1190,15 +1210,24 @@ export default {
} }
uni.getImageInfo({ uni.getImageInfo({
src: src, src: src,
success: (res) => resolve(res.path), success: (res) => resolve(res.path || ''),
fail: () => resolve(''), fail: () => resolve(''),
}) })
}) })
}, },
/** /**
* 辅助函数:绘制圆角矩形 * 辅助函数:绘制规范圆角矩形路径与填边
* @param {Object} ctx Canvas 上下文
* @param {Number} x 起始 X 坐标
* @param {Number} y 起始 Y 坐标
* @param {Number} width 矩形宽度
* @param {Number} height 矩形高度
* @param {Number} radius 圆角半径
* @param {String|CanvasGradient} fillColor 填充颜色
* @param {String} strokeColor 描边颜色
* @param {Number} lineWidth 描边线宽
*/ */
drawRoundRect(ctx, x, y, width, height, radius, fillColor, strokeColor) { drawRoundRect(ctx, x, y, width, height, radius, fillColor, strokeColor, lineWidth = 1) {
ctx.save() ctx.save()
ctx.beginPath() ctx.beginPath()
ctx.moveTo(x + radius, y) ctx.moveTo(x + radius, y)
@@ -1217,65 +1246,162 @@ export default {
} }
if (strokeColor) { if (strokeColor) {
ctx.setStrokeStyle(strokeColor) ctx.setStrokeStyle(strokeColor)
ctx.setLineWidth(lineWidth)
ctx.stroke() ctx.stroke()
} }
ctx.restore() ctx.restore()
}, },
/** /**
* 辅助函数:多行文本绘制并带省略号 * 辅助函数:计算单字符视觉预估宽度(规避小程序 CanvasContext measureText 仅按 10px 测算的平台级缺陷)
* @param {String} char 单个字符
* @param {Number} fontSize 字体大小(px)
* @returns {Number} 预估像素宽度
*/ */
drawTextWrapped(ctx, text, x, y, maxWidth, lineHeight, maxLines = 2) { calcCharWidth(char, fontSize) {
const code = char.charCodeAt(0)
if (code > 255) return fontSize // 中文汉字、全角标点符号
if (code >= 65 && code <= 90) return fontSize * 0.72 // 大写英文字母
if (code === 32) return fontSize * 0.32 // 空格
return fontSize * 0.56 // 小写字母、半角数字、半角标点
},
/**
* 辅助函数:计算整段文字的真实渲染宽度
* @param {String} text 文字内容
* @param {Number} fontSize 字体大小(px)
* @returns {Number} 总像素宽度
*/
calcTextWidth(text, fontSize) {
if (!text) return 0
const str = String(text)
let total = 0
for (let i = 0; i < str.length; i++) {
total += this.calcCharWidth(str[i], fontSize)
}
return total
},
/**
* 辅助函数:安全绘制单行文本(显式设定对齐状态,彻底杜绝全局状态污染导致错乱)
* @param {Object} ctx Canvas 上下文
* @param {String} text 文本内容
* @param {Number} x 绘制起点 X
* @param {Number} y 绘制起点 Y
* @param {Object} options 样式参数配置
*/
drawSafeText(ctx, text, x, y, options = {}) {
if (text === undefined || text === null || text === '') return
const {
fontSize = 20,
color = '#1E293B',
align = 'left',
baseline = 'top',
} = options
ctx.save()
ctx.setFontSize(fontSize)
ctx.setFillStyle(color)
ctx.setTextAlign(align)
ctx.setTextBaseline(baseline)
ctx.fillText(String(text), x, y)
ctx.restore()
},
/**
* 辅助函数:自适应多行文本换行绘制并带省略号(返回绘制结束后的下一行起始 Y,实现自适应流式排版)
* @param {Object} ctx Canvas 上下文
* @param {String} text 文本内容
* @param {Number} x 起始 X
* @param {Number} y 起始 Y
* @param {Number} maxWidth 最大宽度
* @param {Number} lineHeight 每行高度
* @param {Number} maxLines 最大行数限制
* @param {Object} options 字体与颜色参数
* @returns {Number} 实际占用的 Y 轴高度终点
*/
drawTextFlow(ctx, text, x, y, maxWidth, lineHeight, maxLines = 2, options = {}) {
if (!text) return y if (!text) return y
const words = String(text).split('') const {
fontSize = 24,
color = '#334155',
} = options
const chars = String(text).split('')
let line = '' let line = ''
let lineW = 0
let lineCount = 0 let lineCount = 0
let currentY = y let curY = y
for (let i = 0; i < words.length; i++) { for (let i = 0; i < chars.length; i++) {
const testLine = line + words[i] const char = chars[i]
const metrics = ctx.measureText(testLine) const charW = this.calcCharWidth(char, fontSize)
if (metrics.width > maxWidth && i > 0) { if (lineW + charW > maxWidth && line.length > 0) {
lineCount++ lineCount++
if (lineCount >= maxLines) { if (lineCount >= maxLines) {
line = line.slice(0, -1) + '…' let truncated = line
ctx.fillText(line, x, currentY) while (this.calcTextWidth(truncated + '…', fontSize) > maxWidth && truncated.length > 0) {
return currentY + lineHeight truncated = truncated.slice(0, -1)
}
this.drawSafeText(ctx, truncated + '…', x, curY, { fontSize, color, align: 'left', baseline: 'top' })
return curY + lineHeight
} }
ctx.fillText(line, x, currentY) this.drawSafeText(ctx, line, x, curY, { fontSize, color, align: 'left', baseline: 'top' })
line = words[i] line = char
currentY += lineHeight lineW = charW
curY += lineHeight
} else { } else {
line = testLine line += char
lineW += charW
} }
} }
if (line) { if (line) {
ctx.fillText(line, x, currentY) this.drawSafeText(ctx, line, x, curY, { fontSize, color, align: 'left', baseline: 'top' })
currentY += lineHeight curY += lineHeight
} }
return currentY return curY
}, },
/** /**
* 在 Canvas 上绘制真实的、符合国际标准且微信100%可识别的长按二维码 * 辅助函数:绘制高兼容精致虚线
* @param {Object} ctx Canvas 上下文
* @param {Number} x1 起始 X
* @param {Number} y1 起始 Y
* @param {Number} x2 结束 X
* @param {Number} y2 结束 Y
* @param {Number} dashLen 虚线段长度
* @param {Number} gapLen 虚线间隔
* @param {String} color 虚线颜色
*/
drawDashedLine(ctx, x1, y1, x2, y2, dashLen = 6, gapLen = 4, color = '#E2E8F0') {
ctx.save()
ctx.setStrokeStyle(color)
ctx.setLineWidth(1)
let curX = x1
while (curX < x2) {
ctx.beginPath()
ctx.moveTo(curX, y1)
ctx.lineTo(Math.min(curX + dashLen, x2), y1)
ctx.stroke()
curX += dashLen + gapLen
}
ctx.restore()
},
/**
* 辅助函数:在 Canvas 上绘制高识别度合规二维码
* @param {Object} ctx Canvas 上下文 * @param {Object} ctx Canvas 上下文
* @param {String} text 二维码目标 URL * @param {String} text 二维码目标 URL
* @param {Number} x 起始 X * @param {Number} x 起始 X
* @param {Number} y 起始 Y * @param {Number} y 起始 Y
* @param {Number} size 二维码尺寸(px) * @param {Number} size 二维码外框总尺寸
*/ */
drawRealQRCode(ctx, text, x, y, size) { drawRealQRCode(ctx, text, x, y, size) {
try { try {
const qr = QRCode.create(text, { errorCorrectionLevel: 'M' }) const qr = QRCode.create(text, { errorCorrectionLevel: 'M' })
const modules = qr.modules const modules = qr.modules
const count = modules.size const count = modules.size
const padding = 10 const padding = 8
const qrRealSize = size - padding * 2 const qrRealSize = size - padding * 2
const cellSize = qrRealSize / count const cellSize = qrRealSize / count
// 1. 纯白底色(高对比度保障扫描率) // 1. 纯白底色(高对比度保障扫描识别率)
ctx.save() this.drawRoundRect(ctx, x, y, size, size, 10, '#FFFFFF', '#DFF2E8', 1)
this.drawRoundRect(ctx, x, y, size, size, 14, '#FFFFFF', '#DFF2E8')
// 2. 绘制深墨绿码元(#04301E),符合春绿规范且微信识别率100% // 2. 绘制深墨绿码元(#04301E),符合轻氧浅绿规范且微信识别率 100%
ctx.save()
ctx.setFillStyle('#04301E') ctx.setFillStyle('#04301E')
for (let r = 0; r < count; r++) { for (let r = 0; r < count; r++) {
for (let c = 0; c < count; c++) { for (let c = 0; c < count; c++) {
@@ -1283,179 +1409,217 @@ export default {
ctx.fillRect( ctx.fillRect(
x + padding + c * cellSize, x + padding + c * cellSize,
y + padding + r * cellSize, y + padding + r * cellSize,
cellSize + 0.4, cellSize + 0.35,
cellSize + 0.4 cellSize + 0.35
) )
} }
} }
} }
// 3. 中心微质感同城 Logo 徽标
const centerSize = Math.floor(size * 0.22)
const cx = x + size / 2
const cy = y + size / 2
ctx.beginPath()
ctx.arc(cx, cy, centerSize / 2 + 3, 0, Math.PI * 2)
ctx.setFillStyle('#FFFFFF')
ctx.fill()
ctx.beginPath()
ctx.arc(cx, cy, centerSize / 2, 0, Math.PI * 2)
ctx.setFillStyle('#00E070')
ctx.fill()
ctx.setFontSize(Math.floor(centerSize * 0.58))
ctx.setFillStyle('#04301E')
ctx.setTextAlign('center')
ctx.setTextBaseline('middle')
ctx.fillText('同', cx, cy + 1)
ctx.restore() ctx.restore()
} catch (e) { } catch (e) {
console.error('QR draw error', e) console.error('QR draw error', e)
} }
}, },
/** /**
* 真实商业级 Canvas 绘制高清同城分享海报 * 重构核心:高保真商业级 Canvas 流式自适应绘制同城分享海报
*/ */
async generatePoster() { async generatePoster() {
this.posterLoading = true this.posterLoading = true
const W = 640 const W = 640
const H = 1020 const H = 1000
const ctx = uni.createCanvasContext('postPosterCanvas', this) const ctx = uni.createCanvasContext('postPosterCanvas', this)
// 1. 底板背景(薄荷微浅绿) // 1. 底板背景(全局轻氧浅绿 #F2FEF7)
ctx.setFillStyle('#F2FEF7') ctx.setFillStyle('#F2FEF7')
ctx.fillRect(0, 0, W, H) ctx.fillRect(0, 0, W, H)
// 2. 主体纯白高光卡片(圆角 24px,带微立体边框) // 2. 主体纯白高光卡片(圆角 24px,微立体边框 #E2F4EB)
this.drawRoundRect(ctx, 24, 24, 592, 972, 26, '#FFFFFF', '#DDF2E7') this.drawRoundRect(ctx, 32, 32, 576, 936, 24, '#FFFFFF', '#E2F4EB', 1)
// 3. 头部 Hero 品牌渐变区 // 3. 头部顶栏(品牌胶囊 + 区域胶囊)
const grad = ctx.createLinearGradient(24, 24, 616, 210) const badgeText = '同城生活 · 便民严选'
grad.addColorStop(0, '#04874A') const badgeW = this.calcTextWidth(badgeText, 18) + 24
grad.addColorStop(1, '#023A21') this.drawRoundRect(ctx, 56, 56, badgeW, 32, 16, '#E8FAF0', null)
this.drawRoundRect(ctx, 24, 24, 592, 180, 26, grad, null) this.drawSafeText(ctx, badgeText, 56 + 12, 56 + 7, {
fontSize: 18,
color: '#00A854',
align: 'left',
baseline: 'top',
})
// 补齐头部底部的直角以平滑承接内容 const cityText = this.post.area ? `贵阳 · ${this.post.area}` : '同城精选'
ctx.setFillStyle(grad) const cityW = this.calcTextWidth(cityText, 18) + 24
ctx.fillRect(24, 160, 592, 44) const cityX = 584 - cityW
this.drawRoundRect(ctx, cityX, 56, cityW, 32, 16, '#F1F5F9', null)
this.drawSafeText(ctx, cityText, cityX + 12, 56 + 7, {
fontSize: 18,
color: '#64748B',
align: 'left',
baseline: 'top',
})
// 头部品牌与城市胶囊 // 4. 标题行(字号 30px,高级灰 #1E293B,最多 2 行流式换行)
this.drawRoundRect(ctx, 48, 50, 200, 36, 18, 'rgba(255, 255, 255, 0.22)', null) const titleY = 106
ctx.setFontSize(20) let curY = this.drawTextFlow(ctx, this.post.title || '', 56, titleY, 528, 42, 2, {
ctx.setFillStyle('#FFFFFF') fontSize: 30,
ctx.fillText('同城生活 · 便民严选', 64, 75) color: '#1E293B',
})
curY += 14
const cityText = this.post.area ? `贵阳 · ${this.post.area}` : '贵阳同城' // 5. 实拍主图 / 视觉展示区
const cityW = ctx.measureText(cityText).width + 30
this.drawRoundRect(ctx, 592 - cityW, 50, cityW, 36, 18, 'rgba(255, 255, 255, 0.16)', null)
ctx.setFontSize(20)
ctx.setFillStyle('#FFFFFF')
ctx.fillText(cityText, 607 - cityW, 75)
// 头部大标题(白字粗体,最多折 2 行)
ctx.setFontSize(32)
ctx.setFillStyle('#FFFFFF')
this.drawTextWrapped(ctx, this.post.title || '', 48, 128, 544, 40, 2)
// 4. 实拍大图 / 主题视觉区
let coverPath = '' let coverPath = ''
if (this.imageUrls && this.imageUrls.length) { if (this.imageUrls && this.imageUrls.length) {
coverPath = await this.getImageLocalPath(this.imageUrls[0]) coverPath = await this.getImageLocalPath(this.imageUrls[0])
} }
const picY = 224
const imgW = 528
const imgH = 250
const imgY = curY
if (coverPath) { if (coverPath) {
ctx.save() // 绘制图片底衬与无破坏性边框(避免 ctx.clip 造成全局裁剪错乱)
this.drawRoundRect(ctx, 48, picY, 544, 290, 16, null, null) this.drawRoundRect(ctx, 56, imgY, imgW, imgH, 14, '#F8FAFC', '#E2E8F0', 1)
ctx.clip() ctx.drawImage(coverPath, 56, imgY, imgW, imgH)
ctx.drawImage(coverPath, 48, picY, 544, 290) this.drawRoundRect(ctx, 56, imgY, imgW, imgH, 14, null, 'rgba(0,0,0,0.06)', 1)
ctx.restore()
// 大图内右上方贴上“置顶推荐 / 实拍保真”标签 // 大图左下角分类胶囊
const catText = this.post.categoryName || '同城便民'
const catW = this.calcTextWidth(catText, 18) + 22
this.drawRoundRect(ctx, 70, imgY + imgH - 42, catW, 28, 14, 'rgba(4, 48, 30, 0.72)', null)
this.drawSafeText(ctx, catText, 81, imgY + imgH - 37, {
fontSize: 18,
color: '#00E070',
align: 'left',
baseline: 'top',
})
// 大图右上角置顶/推荐标签
if (this.post.isTop || this.post.isPaid) { if (this.post.isTop || this.post.isPaid) {
this.drawRoundRect(ctx, 450, picY + 16, 126, 36, 18, '#F5A623', null) const topText = '置顶推荐'
ctx.setFontSize(18) const topW = this.calcTextWidth(topText, 16) + 18
ctx.setFillStyle('#FFFFFF') this.drawRoundRect(ctx, 56 + imgW - topW - 12, imgY + 12, topW, 26, 13, '#F5A623', null)
ctx.fillText('置顶推荐', 478, picY + 41) this.drawSafeText(ctx, topText, 56 + imgW - topW - 3, imgY + 17, {
fontSize: 16,
color: '#FFFFFF',
align: 'left',
baseline: 'top',
})
} }
// 大图左下方贴上分类胶囊 curY = imgY + imgH + 18
this.drawRoundRect(ctx, 64, picY + 236, 136, 36, 18, 'rgba(4, 48, 30, 0.75)', null)
ctx.setFontSize(20)
ctx.setFillStyle('#00E070')
ctx.fillText(this.post.categoryName || '同城便民', 80, picY + 261)
} else { } else {
// 无实拍图时展示生态质感主题大卡片 // 无图时展示轻氧绿色生态卡片
this.drawRoundRect(ctx, 48, picY, 544, 220, 16, '#EBF9F2', '#D3EFE1') const emptyH = 120
ctx.setFontSize(32) this.drawRoundRect(ctx, 56, imgY, imgW, emptyH, 14, '#EBF9F2', '#D3EFE1', 1)
ctx.setFillStyle('#04874A') this.drawSafeText(ctx, this.post.categoryName || '同城便民服务', 80, imgY + 28, {
ctx.fillText(this.post.categoryName || '同城便民服务', 72, picY + 90) fontSize: 28,
ctx.setFontSize(22) color: '#00A854',
ctx.setFillStyle('#5E8C78') align: 'left',
ctx.fillText('平台官方审核认证 · 放心联系', 72, picY + 140) baseline: 'top',
})
this.drawSafeText(ctx, '平台官方审核认证 · 便民服务放心选', 80, imgY + 68, {
fontSize: 20,
color: '#64748B',
align: 'left',
baseline: 'top',
})
curY = imgY + emptyH + 18
} }
// 5. 价值展示行(价格 + 浏览量) // 6. 价值展示行(价格 + 浏览量,精准计算宽度零重叠)
const valY = coverPath ? picY + 332 : picY + 262 const priceY = curY
ctx.setFontSize(26)
ctx.setFillStyle('#F5A623')
if (this.post.price > 0) { if (this.post.price > 0) {
ctx.fillText('¥', 48, valY) // 人民币符号
ctx.setFontSize(48) this.drawSafeText(ctx, '¥', 56, priceY + 12, {
ctx.fillText(String(this.post.price), 68, valY) fontSize: 24,
color: '#F5A623',
align: 'left',
baseline: 'top',
})
// 价格数字
const numStr = String(this.post.price)
this.drawSafeText(ctx, numStr, 76, priceY, {
fontSize: 40,
color: '#F5A623',
align: 'left',
baseline: 'top',
})
// 价格单位
if (this.post.priceUnit) { if (this.post.priceUnit) {
ctx.setFontSize(22) const numW = this.calcTextWidth(numStr, 40)
ctx.setFillStyle('#888888') this.drawSafeText(ctx, this.post.priceUnit, 82 + numW, priceY + 16, {
ctx.fillText(this.post.priceUnit, 72 + ctx.measureText(String(this.post.price)).width + 10, valY) fontSize: 20,
color: '#94A3B8',
align: 'left',
baseline: 'top',
})
} }
} else { } else {
ctx.setFontSize(40) this.drawSafeText(ctx, '面议', 56, priceY + 4, {
ctx.fillText('面议', 48, valY) fontSize: 32,
} color: '#00A854',
align: 'left',
// 右侧显示浏览人次 baseline: 'top',
const viewsText = `🔥 ${this.post.views || 0} 人关注 · 平台认证`
ctx.setFontSize(20)
ctx.setFillStyle('#7A998A')
ctx.fillText(viewsText, 592 - ctx.measureText(viewsText).width - 48, valY - 6)
// 6. 详细描述摘录
const descY = valY + 44
ctx.setFontSize(22)
ctx.setFillStyle('#3E5246')
this.drawTextWrapped(ctx, this.post.content || '', 48, descY, 544, 34, 2)
// 7. 特色服务标签与地址信息
const tagY = descY + 76
if (this.post.tags && this.post.tags.length) {
let tagStartX = 48
this.post.tags.slice(0, 3).forEach((tag) => {
const tW = ctx.measureText(tag).width + 24
this.drawRoundRect(ctx, tagStartX, tagY, tW, 34, 8, '#EBF9F2', '#D3EFE1')
ctx.setFontSize(19)
ctx.setFillStyle('#04874A')
ctx.fillText(tag, tagStartX + 12, tagY + 24)
tagStartX += tW + 12
}) })
} }
// 地址行 // 右侧关注度与认证(纯文字排版,无 Emoji,符合规范)
const addrY = tagY + 54 const viewsNum = this.post.views || 0
ctx.setFontSize(20) const viewStr = `${viewsNum}人关注 · 平台认证`
ctx.setFillStyle('#6E8276') const viewW = this.calcTextWidth(viewStr, 19)
const addrFull = `📍 ${this.post.address || this.post.area || '同城便民服务地点'}` this.drawSafeText(ctx, viewStr, 584 - viewW, priceY + 14, {
ctx.fillText(addrFull.slice(0, 28), 48, addrY) fontSize: 19,
color: '#64748B',
align: 'left',
baseline: 'top',
})
curY = priceY + 50
// 8. 分割虚线 // 7. 详细描述摘录(最多 2 行流式换行)
ctx.setStrokeStyle('#E2EBE5') if (this.post.content) {
ctx.setLineWidth(1) const afterDescY = this.drawTextFlow(ctx, this.post.content, 56, curY, 528, 30, 2, {
ctx.beginPath() fontSize: 21,
ctx.moveTo(48, 804) color: '#475569',
ctx.lineTo(592, 804) })
ctx.stroke() curY = afterDescY + 12
}
// 9. 底部扫码交互区(真实高密度二维码 + 醒目指引) // 8. 特色服务标签(最多 3 个)
if (this.post.tags && this.post.tags.length) {
let tagX = 56
const tags = this.post.tags.slice(0, 3)
for (let t = 0; t < tags.length; t++) {
const tag = tags[t]
const tagW = this.calcTextWidth(tag, 18) + 20
if (tagX + tagW > 584) break
this.drawRoundRect(ctx, tagX, curY, tagW, 28, 6, '#EBF9F2', '#D3EFE1', 1)
this.drawSafeText(ctx, tag, tagX + 10, curY + 5, {
fontSize: 18,
color: '#00A854',
align: 'left',
baseline: 'top',
})
tagX += tagW + 10
}
curY += 36
}
// 地址行(无 Emoji,简洁清晰)
const addrText = this.post.address || this.post.area
if (addrText) {
const fullAddr = `服务地点:${addrText}`
const truncatedAddr = fullAddr.length > 28 ? fullAddr.slice(0, 27) + '…' : fullAddr
this.drawSafeText(ctx, truncatedAddr, 56, curY, {
fontSize: 19,
color: '#64748B',
align: 'left',
baseline: 'top',
})
}
// 9. 底部优雅虚线分割栏(固定停靠在底部 y = 812)
this.drawDashedLine(ctx, 56, 812, 584, 812, 6, 4, '#E2E8F0')
// 10. 底部扫码与转化区(y = 830)
let inviterId = 0 let inviterId = 0
try { try {
const user = this.currentUser const user = this.currentUser
@@ -1463,23 +1627,30 @@ export default {
} catch (e) {} } catch (e) {}
const qrUrl = `https://fenleixinxi.gzyakj.com.cn/pages/detail/detail?id=${this.id}${inviterId ? '&inviterId=' + inviterId : ''}` const qrUrl = `https://fenleixinxi.gzyakj.com.cn/pages/detail/detail?id=${this.id}${inviterId ? '&inviterId=' + inviterId : ''}`
// 绘制真实合规的微信二维码 // 绘制高识别度合规二维码(124 x 124)
this.drawRealQRCode(ctx, qrUrl, 48, 826, 140) this.drawRealQRCode(ctx, qrUrl, 56, 830, 124)
// 右侧指引文案 // 右侧指引文案(显式左对齐,彻底杜绝对齐错乱)
ctx.setFontSize(26) this.drawSafeText(ctx, '微信长按识别小程序', 198, 842, {
ctx.setFillStyle('#04301E') fontSize: 25,
ctx.fillText('微信长按识别小程序', 214, 866) color: '#1E293B',
align: 'left',
baseline: 'top',
})
this.drawSafeText(ctx, '联系发布者 · 查看完整信息', 198, 880, {
fontSize: 20,
color: '#00A854',
align: 'left',
baseline: 'top',
})
this.drawSafeText(ctx, '同城生活分类信息 · 官方严选保障', 198, 916, {
fontSize: 17,
color: '#94A3B8',
align: 'left',
baseline: 'top',
})
ctx.setFontSize(21) // 11. 最终导出超清海报(2x Retina 分辨率:1280 x 2000)
ctx.setFillStyle('#04874A')
ctx.fillText('查看完整电话 · 微信 · 地图导航', 214, 904)
ctx.setFontSize(18)
ctx.setFillStyle('#88A395')
ctx.fillText('同城生活分类信息 · 官方便民服务保障', 214, 938)
// 10. 最终导出高保真图片
ctx.draw(false, () => { ctx.draw(false, () => {
setTimeout(() => { setTimeout(() => {
uni.canvasToTempFilePath( uni.canvasToTempFilePath(
@@ -1487,22 +1658,23 @@ export default {
canvasId: 'postPosterCanvas', canvasId: 'postPosterCanvas',
width: W, width: W,
height: H, height: H,
destWidth: W, destWidth: W * 2,
destHeight: H, destHeight: H * 2,
fileType: 'png', fileType: 'png',
quality: 1, quality: 1,
success: (res) => { success: (res) => {
this.posterImagePath = res.tempFilePath this.posterImagePath = res.tempFilePath
this.posterLoading = false this.posterLoading = false
}, },
fail: () => { fail: (err) => {
console.error('canvasToTempFilePath error', err)
this.posterLoading = false this.posterLoading = false
uni.showToast({ title: '海报绘制失败,请重试', icon: 'none' }) uni.showToast({ title: '海报绘制失败,请重试', icon: 'none' })
}, },
}, },
this this
) )
}, 260) }, 300)
}) })
}, },
/** /**
@@ -1554,7 +1726,9 @@ export default {
* 一键复制海报文案 * 一键复制海报文案
*/ */
copyPosterText() { copyPosterText() {
const text = `【${this.post.categoryName || '同城信息'}】${this.post.title}\n价格:${this.post.price > 0 ? '¥' + this.post.price : '面议'}\n位置:${this.post.address || ''}\n快来同城便民平台查看详情!` const addr = this.post.address || this.post.area || '同城'
const priceText = this.post.price > 0 ? ('¥' + this.post.price + (this.post.priceUnit ? this.post.priceUnit : '')) : '面议'
const text = `【${this.post.categoryName || '同城信息'}】${this.post.title}\n价格:${priceText}\n地点:${addr}\n长按识别海报或在同城便民平台查看详情!`
uni.setClipboardData({ uni.setClipboardData({
data: text, data: text,
success: () => { success: () => {

文件差异内容过多而无法显示 加载差异

查看文件

@@ -419,7 +419,27 @@ export default {
immediate: true, immediate: true,
}, },
}, },
onLoad() { /**
* 页面初始化:提取布局高度与裂变邀请参数
* @param {Object} query 路由启动参数
*/
onLoad(query) {
if (query) {
const ic = query.ic || query.inviteCode || query.invite_code
const inviterId = query.inviterId || query.inviter_id
if (ic) uni.setStorageSync('fc_invite_code', String(ic))
if (inviterId) uni.setStorageSync('fc_inviter_id', Number(inviterId))
if (!ic && query.scene) {
try {
const s = decodeURIComponent(query.scene)
if (s.indexOf('ic_') === 0 || s.indexOf('ic=') === 0) {
uni.setStorageSync('fc_invite_code', s.slice(3))
} else if (/^[A-Za-z0-9]{4,16}$/.test(s)) {
uni.setStorageSync('fc_invite_code', s)
}
} catch (e) {}
}
}
try { try {
const m = getNavMetrics() const m = getNavMetrics()
this.statusBarHeight = m.statusBarHeight this.statusBarHeight = m.statusBarHeight

查看文件

@@ -770,22 +770,23 @@ export default {
padding: 16rpx 28rpx 0; padding: 16rpx 28rpx 0;
} }
.vip__card { .vip__card {
background: linear-gradient(145deg, #023a21 0%, #044f2d 60%, #033f24 100%); /* 尊享高定暖金黑金渐变:彻底告别老式墨绿发黑,与 mine 会员卡片视觉基因统一 */
background: linear-gradient(135deg, #ffe7a6 0%, #ffd06a 45%, #f5a623 100%);
border-radius: 28rpx; border-radius: 28rpx;
padding: 36rpx 32rpx 28rpx; padding: 36rpx 32rpx 28rpx;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-shadow: 0 12rpx 32rpx rgba(2, 58, 33, 0.28); box-shadow: 0 16rpx 36rpx rgba(245, 166, 35, 0.22);
border: 1rpx solid rgba(0, 224, 112, 0.22); border: 1rpx solid rgba(255, 255, 255, 0.6);
} }
.vip__card-sheen { .vip__card-sheen {
position: absolute; position: absolute;
top: -80rpx; top: -90rpx;
right: -80rpx; right: -90rpx;
width: 260rpx; width: 280rpx;
height: 260rpx; height: 280rpx;
border-radius: 50%; border-radius: 50%;
background: radial-gradient(circle, rgba(255, 194, 92, 0.2) 0%, rgba(255, 194, 92, 0) 70%); background: radial-gradient(circle, rgba(255, 255, 255, 0.55) 0%, rgba(255, 255, 255, 0) 70%);
pointer-events: none; pointer-events: none;
} }
.vip__card-user { .vip__card-user {
@@ -798,17 +799,18 @@ export default {
width: 88rpx; width: 88rpx;
height: 88rpx; height: 88rpx;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(135deg, #00e070 0%, #00b85c 100%); background: #ffffff;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 3rpx solid #ffffff; border: 3rpx solid rgba(255, 255, 255, 0.95);
box-shadow: 0 4rpx 14rpx rgba(74, 38, 0, 0.12);
flex-shrink: 0; flex-shrink: 0;
} }
.vip__avatar-txt { .vip__avatar-txt {
font-size: 38rpx; font-size: 38rpx;
font-weight: 800; font-weight: 800;
color: #04301e; color: #4a2600;
} }
.vip__user-meta { .vip__user-meta {
margin-left: 20rpx; margin-left: 20rpx;
@@ -822,19 +824,20 @@ export default {
} }
.vip__user-name { .vip__user-name {
font-size: 32rpx; font-size: 32rpx;
font-weight: 700; font-weight: 800;
color: #ffffff; color: #3e2000;
} }
.vip__badge { .vip__badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 4rpx 14rpx; padding: 4rpx 14rpx;
border-radius: 20rpx; border-radius: 20rpx;
background: rgba(255, 255, 255, 0.15); background: rgba(74, 38, 0, 0.08);
backdrop-filter: blur(4px); border: 1rpx solid rgba(74, 38, 0, 0.12);
} }
.vip__badge--active { .vip__badge--active {
background: linear-gradient(135deg, #ffc25c 0%, #f5a623 100%); background: #3e2000;
border-color: #3e2000;
} }
.vip__badge-icon { .vip__badge-icon {
width: 22rpx; width: 22rpx;
@@ -843,24 +846,28 @@ export default {
} }
.vip__badge-txt { .vip__badge-txt {
font-size: 20rpx; font-size: 20rpx;
color: #ffffff; color: #4a2600;
font-weight: 600; font-weight: 700;
}
.vip__badge--active .vip__badge-txt {
color: #ffe7a6;
} }
.vip__user-desc { .vip__user-desc {
margin-top: 8rpx; margin-top: 8rpx;
font-size: 22rpx; font-size: 22rpx;
color: rgba(255, 255, 255, 0.7); color: #7a4600;
font-weight: 500;
} }
.vip__card-crown { .vip__card-crown {
width: 72rpx; width: 72rpx;
height: 72rpx; height: 72rpx;
opacity: 0.25; opacity: 0.18;
} }
.vip__assets { .vip__assets {
margin-top: 32rpx; margin-top: 32rpx;
padding-top: 24rpx; padding-top: 24rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.12); border-top: 1rpx solid rgba(74, 38, 0, 0.1);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -878,13 +885,14 @@ export default {
.vip__asset-val { .vip__asset-val {
font-size: 40rpx; font-size: 40rpx;
font-weight: 800; font-weight: 800;
color: #ffffff; color: #3e2000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
} }
.vip__asset-unit { .vip__asset-unit {
font-size: 20rpx; font-size: 20rpx;
color: rgba(255, 255, 255, 0.75); color: #7a4600;
margin-left: 4rpx; margin-left: 4rpx;
font-weight: 600;
} }
.vip__asset-label-row { .vip__asset-label-row {
margin-top: 6rpx; margin-top: 6rpx;
@@ -894,17 +902,21 @@ export default {
} }
.vip__asset-lbl { .vip__asset-lbl {
font-size: 22rpx; font-size: 22rpx;
color: rgba(255, 255, 255, 0.7); color: #7a4600;
font-weight: 500;
} }
.vip__asset-act { .vip__asset-act {
font-size: 20rpx; font-size: 20rpx;
color: #00e070; color: #4a2600;
font-weight: 600; font-weight: 700;
background: rgba(255, 255, 255, 0.45);
padding: 2rpx 10rpx;
border-radius: 10rpx;
} }
.vip__asset-sep { .vip__asset-sep {
width: 1rpx; width: 1rpx;
height: 36rpx; height: 36rpx;
background: rgba(255, 255, 255, 0.15); background: rgba(74, 38, 0, 0.12);
} }
.vip__section { .vip__section {
@@ -912,7 +924,7 @@ export default {
background: #ffffff; background: #ffffff;
border-radius: 28rpx; border-radius: 28rpx;
padding: 32rpx 28rpx; padding: 32rpx 28rpx;
box-shadow: 0 4rpx 20rpx rgba(4, 48, 30, 0.05); box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.04);
} }
.vip__sec-head { .vip__sec-head {
display: flex; display: flex;
@@ -923,11 +935,11 @@ export default {
.vip__sec-title { .vip__sec-title {
font-size: 32rpx; font-size: 32rpx;
font-weight: 700; font-weight: 700;
color: #04291c; color: #1e293b;
} }
.vip__sec-tip { .vip__sec-tip {
font-size: 22rpx; font-size: 22rpx;
color: #8fb3a3; color: #64748b;
} }
.vip__privileges-grid { .vip__privileges-grid {
@@ -938,7 +950,7 @@ export default {
.vip__priv-item { .vip__priv-item {
display: flex; display: flex;
align-items: center; align-items: center;
background: #f7faf8; background: #f8fafc;
padding: 20rpx 18rpx; padding: 20rpx 18rpx;
border-radius: 20rpx; border-radius: 20rpx;
} }
@@ -970,13 +982,13 @@ export default {
.vip__priv-name { .vip__priv-name {
font-size: 26rpx; font-size: 26rpx;
font-weight: 700; font-weight: 700;
color: #04291c; color: #1e293b;
display: block; display: block;
} }
.vip__priv-desc { .vip__priv-desc {
margin-top: 4rpx; margin-top: 4rpx;
font-size: 20rpx; font-size: 20rpx;
color: #8fb3a3; color: #64748b;
display: block; display: block;
} }
@@ -988,7 +1000,7 @@ export default {
} }
.vip__plan-card { .vip__plan-card {
background: #ffffff; background: #ffffff;
border: 2rpx solid #e2eee7; border: 2rpx solid #e2e8f0;
border-radius: 20rpx; border-radius: 20rpx;
padding: 28rpx 10rpx 24rpx; padding: 28rpx 10rpx 24rpx;
display: flex; display: flex;
@@ -1008,7 +1020,7 @@ export default {
top: -12rpx; top: -12rpx;
right: -6rpx; right: -6rpx;
background: #00e070; background: #00e070;
color: #04301e; color: #0f2f1d;
font-size: 18rpx; font-size: 18rpx;
font-weight: 700; font-weight: 700;
padding: 2rpx 12rpx; padding: 2rpx 12rpx;
@@ -1017,7 +1029,7 @@ export default {
.vip__plan-name { .vip__plan-name {
font-size: 28rpx; font-size: 28rpx;
font-weight: 700; font-weight: 700;
color: #04291c; color: #1e293b;
} }
.vip__plan-price-row { .vip__plan-price-row {
display: flex; display: flex;
@@ -1038,13 +1050,13 @@ export default {
.vip__plan-del { .vip__plan-del {
margin-top: 4rpx; margin-top: 4rpx;
font-size: 20rpx; font-size: 20rpx;
color: #a4b8af; color: #94a3b8;
text-decoration: line-through; text-decoration: line-through;
} }
.vip__plan-quota-chip { .vip__plan-quota-chip {
margin-top: 12rpx; margin-top: 12rpx;
font-size: 18rpx; font-size: 18rpx;
color: #04874a; color: #00a854;
background: #eaf8f0; background: #eaf8f0;
padding: 2rpx 10rpx; padding: 2rpx 10rpx;
border-radius: 8rpx; border-radius: 8rpx;
@@ -1056,13 +1068,13 @@ export default {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 24rpx 0; padding: 24rpx 0;
border-top: 1rpx solid #edf4f0; border-top: 1rpx solid #f1f5f9;
border-bottom: 1rpx solid #edf4f0; border-bottom: 1rpx solid #f1f5f9;
margin: 24rpx 0 16rpx; margin: 24rpx 0 16rpx;
} }
.vip__coupon-lbl { .vip__coupon-lbl {
font-size: 26rpx; font-size: 26rpx;
color: #04291c; color: #1e293b;
font-weight: 700; font-weight: 700;
} }
.vip__coupon-act-box { .vip__coupon-act-box {
@@ -1081,17 +1093,17 @@ export default {
background: #fff4e0; background: #fff4e0;
} }
&--has { &--has {
color: #04874a; color: #00a854;
background: #eaf8f0; background: #eaf8f0;
} }
&--none { &--none {
color: #8fb3a3; color: #64748b;
font-weight: normal; font-weight: normal;
} }
} }
.vip__coupon-arrow { .vip__coupon-arrow {
font-size: 28rpx; font-size: 28rpx;
color: #8fb3a3; color: #64748b;
} }
.vip__pay-toggle { .vip__pay-toggle {
@@ -1100,7 +1112,7 @@ export default {
.vip__pay-label { .vip__pay-label {
font-size: 26rpx; font-size: 26rpx;
font-weight: 700; font-weight: 700;
color: #04291c; color: #1e293b;
margin-bottom: 16rpx; margin-bottom: 16rpx;
display: block; display: block;
} }
@@ -1114,7 +1126,7 @@ export default {
align-items: center; align-items: center;
padding: 20rpx 18rpx; padding: 20rpx 18rpx;
border-radius: 18rpx; border-radius: 18rpx;
background: #f7faf8; background: #f8fafc;
border: 2rpx solid transparent; border: 2rpx solid transparent;
transition: all 0.2s ease; transition: all 0.2s ease;
@@ -1132,14 +1144,14 @@ export default {
.vip__pay-opt-txt { .vip__pay-opt-txt {
flex: 1; flex: 1;
font-size: 24rpx; font-size: 24rpx;
color: #04291c; color: #1e293b;
font-weight: 600; font-weight: 600;
} }
.vip__radio { .vip__radio {
width: 32rpx; width: 32rpx;
height: 32rpx; height: 32rpx;
border-radius: 50%; border-radius: 50%;
border: 2rpx solid #c8d8d0; border: 2rpx solid #cbd5e1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -1154,7 +1166,7 @@ export default {
width: 12rpx; width: 12rpx;
height: 12rpx; height: 12rpx;
border-radius: 50%; border-radius: 50%;
background: #04301e; background: #0f2f1d;
} }
.vip__dock-pad { .vip__dock-pad {
@@ -1166,7 +1178,7 @@ export default {
left: 0; left: 0;
right: 0; right: 0;
background: #ffffff; background: #ffffff;
box-shadow: 0 -8rpx 28rpx rgba(4, 48, 30, 0.08); box-shadow: 0 -8rpx 28rpx rgba(15, 23, 42, 0.06);
padding: 20rpx 32rpx; padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom)); padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
@@ -1185,7 +1197,7 @@ export default {
} }
.vip__dock-tag { .vip__dock-tag {
font-size: 24rpx; font-size: 24rpx;
color: #668877; color: #64748b;
margin-right: 8rpx; margin-right: 8rpx;
} }
.vip__dock-sym { .vip__dock-sym {
@@ -1212,7 +1224,7 @@ export default {
.vip__dock-meta { .vip__dock-meta {
margin-top: 6rpx; margin-top: 6rpx;
font-size: 20rpx; font-size: 20rpx;
color: #8fb3a3; color: #64748b;
} }
.vip__dock-submit { .vip__dock-submit {
height: 84rpx; height: 84rpx;
@@ -1222,14 +1234,14 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0, 224, 112, 0.3); box-shadow: 0 8rpx 24rpx rgba(0, 224, 112, 0.28);
margin: 0; margin: 0;
border: none; border: none;
} }
.vip__dock-submit-txt { .vip__dock-submit-txt {
font-size: 30rpx; font-size: 30rpx;
font-weight: 700; font-weight: 700;
color: #04301e; color: #0f2f1d;
} }
.vip__sheet-mask { .vip__sheet-mask {
@@ -1258,19 +1270,19 @@ export default {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 32rpx 36rpx 20rpx; padding: 32rpx 36rpx 20rpx;
border-bottom: 1rpx solid #edf4f0; border-bottom: 1rpx solid #f1f5f9;
} }
.vip__sheet-title { .vip__sheet-title {
font-size: 32rpx; font-size: 32rpx;
font-weight: 700; font-weight: 700;
color: #04291c; color: #1e293b;
} }
.vip__sheet-close-btn { .vip__sheet-close-btn {
padding: 8rpx 16rpx; padding: 8rpx 16rpx;
} }
.vip__sheet-close-txt { .vip__sheet-close-txt {
font-size: 32rpx; font-size: 32rpx;
color: #8fb3a3; color: #64748b;
} }
.vip__sheet-scroll { .vip__sheet-scroll {
max-height: 55vh; max-height: 55vh;
@@ -1288,7 +1300,7 @@ export default {
align-items: center; align-items: center;
padding: 20rpx 24rpx; padding: 20rpx 24rpx;
border-radius: 18rpx; border-radius: 18rpx;
background: #f7faf8; background: #f8fafc;
border: 2rpx solid transparent; border: 2rpx solid transparent;
transition: all 0.2s ease; transition: all 0.2s ease;
@@ -1303,7 +1315,7 @@ export default {
} }
.vip__sheet-opt-txt { .vip__sheet-opt-txt {
font-size: 28rpx; font-size: 28rpx;
color: #3b5f4d; color: #334155;
font-weight: 500; font-weight: 500;
} }
.vip__sheet-card-left { .vip__sheet-card-left {
@@ -1313,7 +1325,7 @@ export default {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
border-right: 2rpx dashed #dbe8e1; border-right: 2rpx dashed #cbd5e1;
padding-right: 16rpx; padding-right: 16rpx;
margin-right: 18rpx; margin-right: 18rpx;
} }
@@ -1333,7 +1345,7 @@ export default {
} }
.vip__sheet-card-cond { .vip__sheet-card-cond {
font-size: 18rpx; font-size: 18rpx;
color: #8a7343; color: #b45309;
margin-top: 4rpx; margin-top: 4rpx;
} }
.vip__sheet-card-info { .vip__sheet-card-info {
@@ -1346,15 +1358,15 @@ export default {
.vip__sheet-card-name { .vip__sheet-card-name {
font-size: 26rpx; font-size: 26rpx;
font-weight: 700; font-weight: 700;
color: #04291c; color: #1e293b;
} }
.vip__sheet-card-date { .vip__sheet-card-date {
font-size: 20rpx; font-size: 20rpx;
color: #8fb3a3; color: #64748b;
} }
.vip__sheet-bottom { .vip__sheet-bottom {
padding: 20rpx 36rpx 24rpx; padding: 20rpx 36rpx 24rpx;
border-top: 1rpx solid #edf4f0; border-top: 1rpx solid #f1f5f9;
} }
.vip__sheet-btn { .vip__sheet-btn {
height: 84rpx; height: 84rpx;
@@ -1368,6 +1380,6 @@ export default {
.vip__sheet-btn-txt { .vip__sheet-btn-txt {
font-size: 28rpx; font-size: 28rpx;
font-weight: 700; font-weight: 700;
color: #04301e; color: #0f2f1d;
} }
</style> </style>

查看文件

@@ -84,14 +84,14 @@
</view> </view>
</view> </view>
<!-- 门店电话行 --> <!-- 门店电话行(支持未解锁受保护提示) -->
<view v-if="merchant.contact" class="shop-detail__cell-row" @click="callPhone"> <view v-if="merchant.contact" class="shop-detail__cell-row" @click="callPhone">
<view class="shop-detail__cell-left"> <view class="shop-detail__cell-left">
<image class="fc-icon fc-icon--sm" :src="iconUrl('phone')" mode="aspectFit" /> <image class="fc-icon fc-icon--sm" :src="iconUrl('phone')" mode="aspectFit" />
<text class="shop-detail__cell-txt">联系咨询:{{ merchant.contact }}</text> <text class="shop-detail__cell-txt">联系咨询:{{ merchant.contact }}</text>
</view> </view>
<view class="shop-detail__call-link"> <view class="shop-detail__call-link" :class="{ 'shop-detail__call-link--locked': !merchant.unlocked }">
<text>立即拨打 ›</text> <text>{{ merchant.unlocked ? '立即拨打 ›' : '受保护 · 去解锁 ›' }}</text>
</view> </view>
</view> </view>
@@ -155,9 +155,9 @@
<text>地图导航</text> <text>地图导航</text>
</view> </view>
<view class="shop-detail__btn-call" @click="callPhone"> <view class="shop-detail__btn-call" :class="{ 'shop-detail__btn-call--locked': !merchant.unlocked }" @click="callPhone">
<image class="fc-icon fc-icon--xs" :src="iconUrl('phone')" mode="aspectFit" /> <image class="fc-icon fc-icon--xs" :src="iconUrl('phone')" mode="aspectFit" />
<text>联系店长</text> <text>{{ merchant.unlocked ? '联系店长' : '联系店长 (受保护)' }}</text>
</view> </view>
</view> </view>
<view class="fc-safe-bottom"></view> <view class="fc-safe-bottom"></view>
@@ -196,6 +196,7 @@ export default {
loadingMore: false, loadingMore: false,
userLat: null, userLat: null,
userLng: null, userLng: null,
postId: 0,
} }
}, },
computed: { computed: {
@@ -232,6 +233,7 @@ export default {
*/ */
onLoad(query) { onLoad(query) {
this.merchantId = Number(query.id || query.userId || 0) this.merchantId = Number(query.id || query.userId || 0)
this.postId = Number(query.postId || 0)
loadUiConfig() loadUiConfig()
this.fetchLocation() this.fetchLocation()
this.loadDetail() this.loadDetail()
@@ -291,6 +293,9 @@ export default {
if (!refresh) this.loading = true if (!refresh) this.loading = true
try { try {
const params = {} const params = {}
if (this.postId > 0) {
params.postId = this.postId
}
if (this.userLat != null && this.userLng != null) { if (this.userLat != null && this.userLng != null) {
params.lat = this.userLat params.lat = this.userLat
params.lng = this.userLng params.lng = this.userLng
@@ -368,9 +373,29 @@ export default {
} }
}, },
/** /**
* 一键拨打商家咨询电话 * 一键拨打商家咨询电话(未解锁/非会员状态下严格执行付费拦截)
*/ */
callPhone() { callPhone() {
const isProtected = !this.merchant || !this.merchant.unlocked || (this.merchant.contact && this.merchant.contact.includes('*'))
if (isProtected) {
uni.showModal({
title: '联系方式受保护',
content: this.postId ? '该商家门店电话受平台付费保护,请先解锁关联的发布信息或开通会员查看。' : '该商家门店联系方式受平台保护,开通会员即可直接联系全站商家。 ',
confirmText: this.postId ? '返回去解锁' : '去开通会员',
cancelText: '知道了',
confirmColor: '#00E070',
success: (r) => {
if (r.confirm) {
if (this.postId) {
uni.navigateBack()
} else {
uni.navigateTo({ url: '/pages/member/index' })
}
}
}
})
return
}
const phone = this.merchant && this.merchant.contact const phone = this.merchant && this.merchant.contact
if (!phone) { if (!phone) {
uni.showToast({ title: '商家暂未公开联系电话', icon: 'none' }) uni.showToast({ title: '商家暂未公开联系电话', icon: 'none' })

查看文件

@@ -570,12 +570,12 @@ export default {
/* 顶部 Hero 状态区 */ /* 顶部 Hero 状态区 */
.order-detail__hero { .order-detail__hero {
padding: 36rpx 32rpx 30rpx; padding: 36rpx 32rpx 30rpx;
background: linear-gradient(150deg, #04874A, #023A21); background: linear-gradient(135deg, #00D26A 0%, #00A854 100%);
color: #ffffff; color: #ffffff;
margin-bottom: 24rpx; margin-bottom: 24rpx;
&--paid { &--paid {
background: linear-gradient(150deg, #04874A, #02502C); background: linear-gradient(135deg, #00D26A 0%, #00B359 100%);
} }
&--closed { &--closed {
background: linear-gradient(150deg, #4A5B53, #2B3A33); background: linear-gradient(150deg, #4A5B53, #2B3A33);

查看文件

@@ -433,7 +433,7 @@ export default {
lat: null, lat: null,
lng: null, lng: null,
plan: 'free', plan: 'free',
payType: config.defaultPayType, payType: config.defaultPayType || 'wxpay',
}, },
publish: { publish: {
mode: 'paid', mode: 'paid',