feat: 完善分销中心与团队明细接口,增强商户与帖子交互细节
这个提交包含在:
@@ -41,7 +41,7 @@ class AdminMember extends V1
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$isMember = (int)$row['is_member'] === 1
|
||||
&& (!$row['member_expiretime'] || (int)$row['member_expiretime'] > $now);
|
||||
&& !empty($row['member_expiretime']) && (int)$row['member_expiretime'] > $now;
|
||||
$list[] = [
|
||||
'id' => (int)$row['id'],
|
||||
'nickname' => (string)$row['nickname'],
|
||||
|
||||
@@ -9,14 +9,19 @@ use think\Db;
|
||||
* 路由:
|
||||
* GET /api/v1/distribution/overview D1 分销概览
|
||||
* GET /api/v1/distribution/team D2 我的团队
|
||||
* POST /api/v1/distribution/withdraw D3 申请提现
|
||||
* GET /api/v1/distribution/withdraw-records D4 提现记录
|
||||
* GET /api/v1/distribution/commissions D3 分销收益明细
|
||||
* GET /api/v1/distribution/poster D4 推广海报要素
|
||||
* POST /api/v1/distribution/withdraw D5 申请提现
|
||||
* GET /api/v1/distribution/withdraw-records D6 提现记录
|
||||
*/
|
||||
class Distribution extends V1
|
||||
{
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
@@ -38,35 +43,45 @@ class Distribution extends V1
|
||||
$l2 = (int)Db::name('fc_dist_relation')->where('parent_id', $uid)->where('level', 2)->count();
|
||||
|
||||
$ext = $this->ext($uid);
|
||||
$code = (string)$ext['invite_code'];
|
||||
$shareUrl = 'https://fenleixinxi.gzyakj.com.cn/#/pages/index/index?ic=' . urlencode($code);
|
||||
|
||||
// 商家转账确认收款页需要的商户号与小程序 AppID(非敏感,可下发)
|
||||
$epay = (array)get_addon_config('epay');
|
||||
$wx = isset($epay['wechat']) && is_array($epay['wechat']) ? $epay['wechat'] : [];
|
||||
|
||||
$this->ok([
|
||||
'totalCommission' => round($total, 2),
|
||||
'settled' => round($settled, 2),
|
||||
'totalCommission' => round($total, 2),
|
||||
'settled' => round($settled, 2),
|
||||
'pendingCommission' => round($pending, 2),
|
||||
'withdrawable' => round(max(0, $withdrawable), 2),
|
||||
'todayCommission' => round($todayCommission, 2),
|
||||
'teamCount' => $l1 + $l2,
|
||||
'l1Count' => $l1,
|
||||
'l2Count' => $l2,
|
||||
'inviteCode' => (string)$ext['invite_code'],
|
||||
'posterUrl' => '',
|
||||
// 最低提现金额(后台「分销设置」下发,替代前端写死的 50)
|
||||
'minWithdraw' => (float)$this->cfg('dist.minWithdraw', 50),
|
||||
'withdrawable' => round(max(0, $withdrawable), 2),
|
||||
'todayCommission' => round($todayCommission, 2),
|
||||
'teamCount' => $l1 + $l2,
|
||||
'l1Count' => $l1,
|
||||
'l2Count' => $l2,
|
||||
'inviteCode' => $code,
|
||||
'shareUrl' => $shareUrl,
|
||||
'posterUrl' => '',
|
||||
// 最低提现金额(后台「分销设置」下发,默认 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'] : '',
|
||||
'transferMchId' => isset($wx['mch_id']) ? (string)$wx['mch_id'] : '',
|
||||
'transferAppId' => isset($wx['miniapp_id']) ? (string)$wx['miniapp_id'] : '',
|
||||
'realname' => isset($ext['realname']) ? (string)$ext['realname'] : '',
|
||||
'transferMchId' => isset($wx['mch_id']) ? (string)$wx['mch_id'] : '',
|
||||
'transferAppId' => isset($wx['miniapp_id']) ? (string)$wx['miniapp_id'] : '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* D2 我的团队
|
||||
* 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()
|
||||
{
|
||||
@@ -85,19 +100,31 @@ class Distribution extends V1
|
||||
if ($forCount) {
|
||||
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');
|
||||
});
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$childId = (int)$row['user_id'];
|
||||
$commission = (float)Db::name('fc_commission')->where('from_user_id', $childId)->sum('amount');
|
||||
$orderAmount = (float)Db::name('fc_order')->where('user_id', $childId)->where('status', 'paid')->sum('amount');
|
||||
$commission = (float)Db::name('fc_commission')
|
||||
->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;
|
||||
|
||||
$mobile = (string)(isset($row['mobile']) ? $row['mobile'] : '');
|
||||
$maskedMobile = (strlen($mobile) === 11) ? substr($mobile, 0, 3) . '****' . substr($mobile, -4) : '';
|
||||
|
||||
$list[] = [
|
||||
'id' => $childId,
|
||||
'nickname' => (string)$row['nickname'],
|
||||
'avatar' => (string)(isset($row['avatar']) ? $row['avatar'] : ''),
|
||||
'mobile' => $maskedMobile,
|
||||
'level' => (int)$row['level'],
|
||||
'joinedAt' => $this->iso($row['createtime']),
|
||||
'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'}
|
||||
* 规则 BR-09:金额必填且 >0、且不超过可提现余额
|
||||
*
|
||||
* @return void 输出提现单号与状态JSON
|
||||
*/
|
||||
public function withdraw()
|
||||
{
|
||||
@@ -172,8 +311,11 @@ class Distribution extends V1
|
||||
}
|
||||
|
||||
/**
|
||||
* D4 提现记录
|
||||
* D6 提现记录
|
||||
* GET /api/v1/distribution/withdraw-records → [{id,withdrawNo,amount,status,createdAt}]
|
||||
* 获取当前用户的历史提现记录与商家转账确认参数
|
||||
*
|
||||
* @return void 输出提现历史记录列表JSON
|
||||
*/
|
||||
public function withdrawRecords()
|
||||
{
|
||||
@@ -199,7 +341,11 @@ class Distribution extends V1
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前用户可提现余额
|
||||
* 可提现余额 = Σ已结算佣金(status=1) - Σ(申请中/已打款)提现
|
||||
*
|
||||
* @param int $uid 用户ID
|
||||
* @return float 可提现余额数值
|
||||
*/
|
||||
protected function withdrawable($uid)
|
||||
{
|
||||
@@ -208,4 +354,4 @@ class Distribution extends V1
|
||||
->where('status', 'in', ['auditing', 'paying', 'paid'])->sum('amount');
|
||||
return $settled - $applied;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class Member extends V1
|
||||
$now = time();
|
||||
$ext = $this->ext($me['id']);
|
||||
$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');
|
||||
$freeViewUsed = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
|
||||
$freeViewLeft = $isMember ? max(0, $freeViewQuota - $freeViewUsed) : 0;
|
||||
@@ -123,7 +123,10 @@ class Member extends V1
|
||||
$this->fail('请选择会员套餐');
|
||||
}
|
||||
|
||||
$payType = (string)$this->input('payType', '');
|
||||
$payType = (string)$this->input('payType', 'wxpay');
|
||||
if ($payType === '') {
|
||||
$payType = 'wxpay';
|
||||
}
|
||||
if (!in_array($payType, ['wxpay', 'balance'], true)) {
|
||||
$this->fail('支付方式不支持');
|
||||
}
|
||||
|
||||
@@ -62,6 +62,35 @@ class Merchant extends V1
|
||||
// 首屏拉取前 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 = [
|
||||
'id' => (int)$m['id'],
|
||||
'userId' => (int)$m['user_id'],
|
||||
@@ -71,7 +100,8 @@ class Merchant extends V1
|
||||
'address' => (string)(isset($m['address']) ? $m['address'] : ''),
|
||||
'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,
|
||||
'contact' => (string)(isset($m['contact']) ? $m['contact'] : ''),
|
||||
'contact' => $displayContact,
|
||||
'unlocked' => $unlocked,
|
||||
'category' => (string)(isset($m['category']) ? $m['category'] : ''),
|
||||
'notice' => (string)(isset($m['notice']) ? $m['notice'] : ''),
|
||||
'auditStatus' => (int)$m['audit_status'],
|
||||
|
||||
@@ -186,7 +186,10 @@ class Post extends V1
|
||||
if ($plan === 'single') {
|
||||
// 单条付费 = 付费发布标准(免费发布条数用完后即按此计费);会员按折扣率计价
|
||||
$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)) {
|
||||
$this->fail('支付方式不支持');
|
||||
}
|
||||
@@ -505,7 +508,7 @@ class Post extends V1
|
||||
'freeQuotaLeft' => $free['unlimited'] ? -1 : $free['left'],
|
||||
'freeUnlimited' => $free['unlimited'],
|
||||
'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) : '',
|
||||
]);
|
||||
}
|
||||
@@ -854,7 +857,7 @@ class Post extends V1
|
||||
if ($me) {
|
||||
$ext = $this->ext($me['id']);
|
||||
$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'];
|
||||
$paid = (bool)Db::name('fc_pay_view')
|
||||
->where('user_id', $me['id'])
|
||||
@@ -974,18 +977,33 @@ class Post extends V1
|
||||
$this->fail('信息不存在', 404);
|
||||
}
|
||||
|
||||
$phone = (string)$post['phone'];
|
||||
$wechat = $post['wechat'] !== '' ? (string)$post['wechat'] : ('tongcheng_' . $id);
|
||||
$contactName = (string)(isset($post['contact']) ? $post['contact'] : '');
|
||||
|
||||
$contact = [
|
||||
'phone' => (string)$post['phone'],
|
||||
'wechat' => $post['wechat'] !== '' ? (string)$post['wechat'] : ('tongcheng_' . $id),
|
||||
'name' => $contactName,
|
||||
'phone' => $phone,
|
||||
'wechat' => $wechat,
|
||||
];
|
||||
|
||||
// 已解锁直接返回
|
||||
// 已解锁直接返回(严格返回纯净扁平字段,杜绝 JSON 嵌套导致前端格式错乱)
|
||||
$exist = Db::name('fc_pay_view')
|
||||
->where('user_id', $user['id'])
|
||||
->where('post_id', $id)
|
||||
->find();
|
||||
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);
|
||||
@@ -993,7 +1011,7 @@ class Post extends V1
|
||||
|
||||
$ext = $this->ext($user['id']);
|
||||
$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();
|
||||
|
||||
$orderId = 0;
|
||||
@@ -1070,7 +1088,11 @@ class Post extends V1
|
||||
|
||||
$this->ok([
|
||||
'unlocked' => true,
|
||||
'contact' => $contact,
|
||||
'phone' => $phone,
|
||||
'contact' => $phone,
|
||||
'contactName' => $contactName,
|
||||
'wechat' => $wechat,
|
||||
'contactInfo' => $contact,
|
||||
'freeViewLeft' => $isMember ? max(0, $quota - $usedNow) : 0,
|
||||
'usedFree' => isset($usedFree) ? $usedFree : false,
|
||||
]);
|
||||
@@ -1362,7 +1384,7 @@ class Post extends V1
|
||||
{
|
||||
$now = time();
|
||||
$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) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
@@ -558,7 +558,7 @@ class V1 extends Api
|
||||
$user = $user ?: $this->auth();
|
||||
$ext = $this->ext($user['id']);
|
||||
$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();
|
||||
$isMerchant = $merchant && (int)$merchant['audit_status'] === 1 && $merchant['shop_status'] === 'normal';
|
||||
|
||||
@@ -95,6 +95,8 @@ return [
|
||||
/* =============== 五、用户端 · 分销中心(Module-DIST) =============== */
|
||||
'api/v1/distribution/overview' => 'api/Distribution/overview',
|
||||
'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-records' => 'api/Distribution/withdrawRecords',
|
||||
|
||||
|
||||
@@ -12,10 +12,33 @@ export default {
|
||||
console.log('App Hide')
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 捕获并解析邀请分销参数(支持普通 query 与微信小程序码 scene 场景值)
|
||||
* @param {Object} options 小程序启动或页面入参
|
||||
*/
|
||||
captureInvite(options) {
|
||||
const query = (options && options.query) || {}
|
||||
const ic = query.ic || query.inviteCode || query.invite_code
|
||||
const inviterId = query.inviterId || query.inviter_id
|
||||
let ic = query.ic || query.inviteCode || query.invite_code
|
||||
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) {
|
||||
uni.setStorageSync('fc_invite_code', String(ic))
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ rm -rf /tmp/fcmp && cp -R "$OUTPUT_DIR" /tmp/fcmp
|
||||
|
||||
```js
|
||||
// 'balance' 余额支付 / 'wxpay' 微信支付
|
||||
defaultPayType: 'balance',
|
||||
defaultPayType: 'wxpay',
|
||||
```
|
||||
|
||||
配置好真实商户参数后改回 `'wxpay'` 即可,**各页面无需改动**(三个支付点都读这个值)。
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
/**
|
||||
* 接口模块 · 分销中心(用户端)
|
||||
* 对应模块:分销首页 / 团队管理 / 佣金提现
|
||||
* 对应模块:分销首页 / 团队管理 / 收益明细 / 推广海报 / 佣金提现
|
||||
*/
|
||||
import { get, post } from '../utils/request.js'
|
||||
|
||||
export const distributionApi = {
|
||||
/** 佣金概览(含邀请码/海报) */
|
||||
/** 佣金概览(含邀请码/分销规则) */
|
||||
overview: () => get('/distribution/overview', {}, { 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 }),
|
||||
/** 提现记录 */
|
||||
|
||||
@@ -75,9 +75,7 @@ export const config = {
|
||||
// 强制使用 mock(联调前本地联调可置 true)
|
||||
forceMock: false,
|
||||
// 默认支付方式:'balance' 余额支付 / 'wxpay' 微信支付
|
||||
// ⚠️ 微信支付商户参数(mchId/apiKey)尚未配置,测试阶段默认走余额支付;
|
||||
// 配置好真实商户参数后改回 'wxpay' 即可,无需改动各页面。
|
||||
defaultPayType: 'balance',
|
||||
defaultPayType: 'wxpay',
|
||||
// 本地存储键
|
||||
storageKeys: {
|
||||
token: 'fc_token',
|
||||
|
||||
@@ -129,7 +129,16 @@ export function request(options = {}) {
|
||||
{ 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 判定
|
||||
if (body.code === 401) {
|
||||
onUnauthorized(runtime)
|
||||
|
||||
@@ -153,7 +153,8 @@
|
||||
"style": {
|
||||
"navigationBarTitleText": "分销中心",
|
||||
"navigationStyle": "custom",
|
||||
"backgroundColor": "#f2fef7"
|
||||
"backgroundColor": "#f2fef7",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -248,4 +249,4 @@
|
||||
]
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
}
|
||||
@@ -477,12 +477,12 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 隐藏的离屏 Canvas,用于高质量绘制海报(640 x 1020 逻辑分辨率) -->
|
||||
<!-- 离屏高质量 Canvas(640 x 1000 逻辑分辨率,导出 2x 超清海报) -->
|
||||
<canvas
|
||||
canvas-id="postPosterCanvas"
|
||||
id="postPosterCanvas"
|
||||
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>
|
||||
</template>
|
||||
@@ -671,6 +671,7 @@ export default {
|
||||
},
|
||||
onLoad(query) {
|
||||
this.id = Number(query.id || 101)
|
||||
this.posterImagePath = ''
|
||||
loadUiConfig()
|
||||
this.fetch()
|
||||
},
|
||||
@@ -813,11 +814,29 @@ export default {
|
||||
if (res.freeViewLeft != null) this.freeViewLeft = Number(res.freeViewLeft)
|
||||
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
|
||||
? {
|
||||
name: (res && (res.contactName || res.publisher?.name)) || '',
|
||||
phone: (res && (res.phone || res.contact)) || '',
|
||||
wechat: (res && res.wechat) || '',
|
||||
name: nameStr,
|
||||
phone: phoneStr,
|
||||
wechat: wechatStr,
|
||||
}
|
||||
: { name: '', phone: '', wechat: '' }
|
||||
|
||||
@@ -1096,7 +1115,7 @@ export default {
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/pages/merchant/detail?id=' + shopId,
|
||||
url: `/pages/merchant/detail?id=${shopId}&postId=${this.id}`,
|
||||
fail: (err) => {
|
||||
console.warn('跳转商家详情失败:', err)
|
||||
}
|
||||
@@ -1170,7 +1189,7 @@ export default {
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 打开海报弹窗并异步生成海报
|
||||
* 打开海报弹窗并按需异步生成海报
|
||||
*/
|
||||
openPosterModal() {
|
||||
this.showPoster = true
|
||||
@@ -1179,8 +1198,9 @@ export default {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 辅助函数:异步获取图片本地路径(网络图片需下载)
|
||||
* @param {String} src 图片链接
|
||||
* 辅助函数:异步获取图片本地可用路径(网络图片自动缓存到临时路径)
|
||||
* @param {String} src 图片 URL
|
||||
* @returns {Promise<String>}
|
||||
*/
|
||||
getImageLocalPath(src) {
|
||||
return new Promise((resolve) => {
|
||||
@@ -1190,15 +1210,24 @@ export default {
|
||||
}
|
||||
uni.getImageInfo({
|
||||
src: src,
|
||||
success: (res) => resolve(res.path),
|
||||
success: (res) => resolve(res.path || ''),
|
||||
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.beginPath()
|
||||
ctx.moveTo(x + radius, y)
|
||||
@@ -1217,65 +1246,162 @@ export default {
|
||||
}
|
||||
if (strokeColor) {
|
||||
ctx.setStrokeStyle(strokeColor)
|
||||
ctx.setLineWidth(lineWidth)
|
||||
ctx.stroke()
|
||||
}
|
||||
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
|
||||
const words = String(text).split('')
|
||||
const {
|
||||
fontSize = 24,
|
||||
color = '#334155',
|
||||
} = options
|
||||
const chars = String(text).split('')
|
||||
let line = ''
|
||||
let lineW = 0
|
||||
let lineCount = 0
|
||||
let currentY = y
|
||||
let curY = y
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const testLine = line + words[i]
|
||||
const metrics = ctx.measureText(testLine)
|
||||
if (metrics.width > maxWidth && i > 0) {
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const char = chars[i]
|
||||
const charW = this.calcCharWidth(char, fontSize)
|
||||
if (lineW + charW > maxWidth && line.length > 0) {
|
||||
lineCount++
|
||||
if (lineCount >= maxLines) {
|
||||
line = line.slice(0, -1) + '…'
|
||||
ctx.fillText(line, x, currentY)
|
||||
return currentY + lineHeight
|
||||
let truncated = line
|
||||
while (this.calcTextWidth(truncated + '…', fontSize) > maxWidth && truncated.length > 0) {
|
||||
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)
|
||||
line = words[i]
|
||||
currentY += lineHeight
|
||||
this.drawSafeText(ctx, line, x, curY, { fontSize, color, align: 'left', baseline: 'top' })
|
||||
line = char
|
||||
lineW = charW
|
||||
curY += lineHeight
|
||||
} else {
|
||||
line = testLine
|
||||
line += char
|
||||
lineW += charW
|
||||
}
|
||||
}
|
||||
if (line) {
|
||||
ctx.fillText(line, x, currentY)
|
||||
currentY += lineHeight
|
||||
this.drawSafeText(ctx, line, x, curY, { fontSize, color, align: 'left', baseline: 'top' })
|
||||
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 {String} text 二维码目标 URL
|
||||
* @param {Number} x 起始 X
|
||||
* @param {Number} y 起始 Y
|
||||
* @param {Number} size 二维码尺寸(px)
|
||||
* @param {Number} size 二维码外框总尺寸
|
||||
*/
|
||||
drawRealQRCode(ctx, text, x, y, size) {
|
||||
try {
|
||||
const qr = QRCode.create(text, { errorCorrectionLevel: 'M' })
|
||||
const modules = qr.modules
|
||||
const count = modules.size
|
||||
const padding = 10
|
||||
const padding = 8
|
||||
const qrRealSize = size - padding * 2
|
||||
const cellSize = qrRealSize / count
|
||||
|
||||
// 1. 纯白底色(高对比度保障扫描率)
|
||||
ctx.save()
|
||||
this.drawRoundRect(ctx, x, y, size, size, 14, '#FFFFFF', '#DFF2E8')
|
||||
// 1. 纯白底色(高对比度保障扫描识别率)
|
||||
this.drawRoundRect(ctx, x, y, size, size, 10, '#FFFFFF', '#DFF2E8', 1)
|
||||
|
||||
// 2. 绘制深墨绿码元(#04301E),符合春绿规范且微信识别率100%
|
||||
// 2. 绘制深墨绿码元(#04301E),符合轻氧浅绿规范且微信识别率 100%
|
||||
ctx.save()
|
||||
ctx.setFillStyle('#04301E')
|
||||
for (let r = 0; r < count; r++) {
|
||||
for (let c = 0; c < count; c++) {
|
||||
@@ -1283,179 +1409,217 @@ export default {
|
||||
ctx.fillRect(
|
||||
x + padding + c * cellSize,
|
||||
y + padding + r * cellSize,
|
||||
cellSize + 0.4,
|
||||
cellSize + 0.4
|
||||
cellSize + 0.35,
|
||||
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()
|
||||
} catch (e) {
|
||||
console.error('QR draw error', e)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 真实商业级 Canvas 绘制高清同城分享海报
|
||||
* 重构核心:高保真商业级 Canvas 流式自适应绘制同城分享海报
|
||||
*/
|
||||
async generatePoster() {
|
||||
this.posterLoading = true
|
||||
const W = 640
|
||||
const H = 1020
|
||||
const H = 1000
|
||||
const ctx = uni.createCanvasContext('postPosterCanvas', this)
|
||||
|
||||
// 1. 底板背景(薄荷微浅绿)
|
||||
// 1. 底板背景(全局轻氧浅绿 #F2FEF7)
|
||||
ctx.setFillStyle('#F2FEF7')
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
|
||||
// 2. 主体纯白高光卡片(圆角 24px,带微立体边框)
|
||||
this.drawRoundRect(ctx, 24, 24, 592, 972, 26, '#FFFFFF', '#DDF2E7')
|
||||
// 2. 主体纯白高光卡片(圆角 24px,微立体边框 #E2F4EB)
|
||||
this.drawRoundRect(ctx, 32, 32, 576, 936, 24, '#FFFFFF', '#E2F4EB', 1)
|
||||
|
||||
// 3. 头部 Hero 品牌渐变区
|
||||
const grad = ctx.createLinearGradient(24, 24, 616, 210)
|
||||
grad.addColorStop(0, '#04874A')
|
||||
grad.addColorStop(1, '#023A21')
|
||||
this.drawRoundRect(ctx, 24, 24, 592, 180, 26, grad, null)
|
||||
// 3. 头部顶栏(品牌胶囊 + 区域胶囊)
|
||||
const badgeText = '同城生活 · 便民严选'
|
||||
const badgeW = this.calcTextWidth(badgeText, 18) + 24
|
||||
this.drawRoundRect(ctx, 56, 56, badgeW, 32, 16, '#E8FAF0', null)
|
||||
this.drawSafeText(ctx, badgeText, 56 + 12, 56 + 7, {
|
||||
fontSize: 18,
|
||||
color: '#00A854',
|
||||
align: 'left',
|
||||
baseline: 'top',
|
||||
})
|
||||
|
||||
// 补齐头部底部的直角以平滑承接内容
|
||||
ctx.setFillStyle(grad)
|
||||
ctx.fillRect(24, 160, 592, 44)
|
||||
const cityText = this.post.area ? `贵阳 · ${this.post.area}` : '同城精选'
|
||||
const cityW = this.calcTextWidth(cityText, 18) + 24
|
||||
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',
|
||||
})
|
||||
|
||||
// 头部品牌与城市胶囊
|
||||
this.drawRoundRect(ctx, 48, 50, 200, 36, 18, 'rgba(255, 255, 255, 0.22)', null)
|
||||
ctx.setFontSize(20)
|
||||
ctx.setFillStyle('#FFFFFF')
|
||||
ctx.fillText('同城生活 · 便民严选', 64, 75)
|
||||
// 4. 标题行(字号 30px,高级灰 #1E293B,最多 2 行流式换行)
|
||||
const titleY = 106
|
||||
let curY = this.drawTextFlow(ctx, this.post.title || '', 56, titleY, 528, 42, 2, {
|
||||
fontSize: 30,
|
||||
color: '#1E293B',
|
||||
})
|
||||
curY += 14
|
||||
|
||||
const cityText = this.post.area ? `贵阳 · ${this.post.area}` : '贵阳同城'
|
||||
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. 实拍大图 / 主题视觉区
|
||||
// 5. 实拍主图 / 视觉展示区
|
||||
let coverPath = ''
|
||||
if (this.imageUrls && this.imageUrls.length) {
|
||||
coverPath = await this.getImageLocalPath(this.imageUrls[0])
|
||||
}
|
||||
const picY = 224
|
||||
|
||||
const imgW = 528
|
||||
const imgH = 250
|
||||
const imgY = curY
|
||||
|
||||
if (coverPath) {
|
||||
ctx.save()
|
||||
this.drawRoundRect(ctx, 48, picY, 544, 290, 16, null, null)
|
||||
ctx.clip()
|
||||
ctx.drawImage(coverPath, 48, picY, 544, 290)
|
||||
ctx.restore()
|
||||
// 绘制图片底衬与无破坏性边框(避免 ctx.clip 造成全局裁剪错乱)
|
||||
this.drawRoundRect(ctx, 56, imgY, imgW, imgH, 14, '#F8FAFC', '#E2E8F0', 1)
|
||||
ctx.drawImage(coverPath, 56, imgY, imgW, imgH)
|
||||
this.drawRoundRect(ctx, 56, imgY, imgW, imgH, 14, null, 'rgba(0,0,0,0.06)', 1)
|
||||
|
||||
// 大图内右上方贴上“置顶推荐 / 实拍保真”标签
|
||||
// 大图左下角分类胶囊
|
||||
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) {
|
||||
this.drawRoundRect(ctx, 450, picY + 16, 126, 36, 18, '#F5A623', null)
|
||||
ctx.setFontSize(18)
|
||||
ctx.setFillStyle('#FFFFFF')
|
||||
ctx.fillText('置顶推荐', 478, picY + 41)
|
||||
const topText = '置顶推荐'
|
||||
const topW = this.calcTextWidth(topText, 16) + 18
|
||||
this.drawRoundRect(ctx, 56 + imgW - topW - 12, imgY + 12, topW, 26, 13, '#F5A623', null)
|
||||
this.drawSafeText(ctx, topText, 56 + imgW - topW - 3, imgY + 17, {
|
||||
fontSize: 16,
|
||||
color: '#FFFFFF',
|
||||
align: 'left',
|
||||
baseline: 'top',
|
||||
})
|
||||
}
|
||||
// 大图左下方贴上分类胶囊
|
||||
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)
|
||||
curY = imgY + imgH + 18
|
||||
} else {
|
||||
// 无实拍图时展示生态质感主题大卡片
|
||||
this.drawRoundRect(ctx, 48, picY, 544, 220, 16, '#EBF9F2', '#D3EFE1')
|
||||
ctx.setFontSize(32)
|
||||
ctx.setFillStyle('#04874A')
|
||||
ctx.fillText(this.post.categoryName || '同城便民服务', 72, picY + 90)
|
||||
ctx.setFontSize(22)
|
||||
ctx.setFillStyle('#5E8C78')
|
||||
ctx.fillText('平台官方审核认证 · 放心联系', 72, picY + 140)
|
||||
// 无图时展示轻氧绿色生态卡片
|
||||
const emptyH = 120
|
||||
this.drawRoundRect(ctx, 56, imgY, imgW, emptyH, 14, '#EBF9F2', '#D3EFE1', 1)
|
||||
this.drawSafeText(ctx, this.post.categoryName || '同城便民服务', 80, imgY + 28, {
|
||||
fontSize: 28,
|
||||
color: '#00A854',
|
||||
align: 'left',
|
||||
baseline: 'top',
|
||||
})
|
||||
this.drawSafeText(ctx, '平台官方审核认证 · 便民服务放心选', 80, imgY + 68, {
|
||||
fontSize: 20,
|
||||
color: '#64748B',
|
||||
align: 'left',
|
||||
baseline: 'top',
|
||||
})
|
||||
curY = imgY + emptyH + 18
|
||||
}
|
||||
|
||||
// 5. 价值展示行(价格 + 浏览量)
|
||||
const valY = coverPath ? picY + 332 : picY + 262
|
||||
ctx.setFontSize(26)
|
||||
ctx.setFillStyle('#F5A623')
|
||||
// 6. 价值展示行(价格 + 浏览量,精准计算宽度零重叠)
|
||||
const priceY = curY
|
||||
if (this.post.price > 0) {
|
||||
ctx.fillText('¥', 48, valY)
|
||||
ctx.setFontSize(48)
|
||||
ctx.fillText(String(this.post.price), 68, valY)
|
||||
// 人民币符号
|
||||
this.drawSafeText(ctx, '¥', 56, priceY + 12, {
|
||||
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) {
|
||||
ctx.setFontSize(22)
|
||||
ctx.setFillStyle('#888888')
|
||||
ctx.fillText(this.post.priceUnit, 72 + ctx.measureText(String(this.post.price)).width + 10, valY)
|
||||
const numW = this.calcTextWidth(numStr, 40)
|
||||
this.drawSafeText(ctx, this.post.priceUnit, 82 + numW, priceY + 16, {
|
||||
fontSize: 20,
|
||||
color: '#94A3B8',
|
||||
align: 'left',
|
||||
baseline: 'top',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ctx.setFontSize(40)
|
||||
ctx.fillText('面议', 48, valY)
|
||||
}
|
||||
|
||||
// 右侧显示浏览人次
|
||||
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
|
||||
this.drawSafeText(ctx, '面议', 56, priceY + 4, {
|
||||
fontSize: 32,
|
||||
color: '#00A854',
|
||||
align: 'left',
|
||||
baseline: 'top',
|
||||
})
|
||||
}
|
||||
|
||||
// 地址行
|
||||
const addrY = tagY + 54
|
||||
ctx.setFontSize(20)
|
||||
ctx.setFillStyle('#6E8276')
|
||||
const addrFull = `📍 ${this.post.address || this.post.area || '同城便民服务地点'}`
|
||||
ctx.fillText(addrFull.slice(0, 28), 48, addrY)
|
||||
// 右侧关注度与认证(纯文字排版,无 Emoji,符合规范)
|
||||
const viewsNum = this.post.views || 0
|
||||
const viewStr = `${viewsNum}人关注 · 平台认证`
|
||||
const viewW = this.calcTextWidth(viewStr, 19)
|
||||
this.drawSafeText(ctx, viewStr, 584 - viewW, priceY + 14, {
|
||||
fontSize: 19,
|
||||
color: '#64748B',
|
||||
align: 'left',
|
||||
baseline: 'top',
|
||||
})
|
||||
curY = priceY + 50
|
||||
|
||||
// 8. 分割虚线
|
||||
ctx.setStrokeStyle('#E2EBE5')
|
||||
ctx.setLineWidth(1)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(48, 804)
|
||||
ctx.lineTo(592, 804)
|
||||
ctx.stroke()
|
||||
// 7. 详细描述摘录(最多 2 行流式换行)
|
||||
if (this.post.content) {
|
||||
const afterDescY = this.drawTextFlow(ctx, this.post.content, 56, curY, 528, 30, 2, {
|
||||
fontSize: 21,
|
||||
color: '#475569',
|
||||
})
|
||||
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
|
||||
try {
|
||||
const user = this.currentUser
|
||||
@@ -1463,23 +1627,30 @@ export default {
|
||||
} catch (e) {}
|
||||
const qrUrl = `https://fenleixinxi.gzyakj.com.cn/pages/detail/detail?id=${this.id}${inviterId ? '&inviterId=' + inviterId : ''}`
|
||||
|
||||
// 绘制真实合规的微信二维码
|
||||
this.drawRealQRCode(ctx, qrUrl, 48, 826, 140)
|
||||
// 绘制高识别度合规二维码(124 x 124)
|
||||
this.drawRealQRCode(ctx, qrUrl, 56, 830, 124)
|
||||
|
||||
// 右侧指引文案
|
||||
ctx.setFontSize(26)
|
||||
ctx.setFillStyle('#04301E')
|
||||
ctx.fillText('微信长按识别小程序', 214, 866)
|
||||
// 右侧指引文案(显式左对齐,彻底杜绝对齐错乱)
|
||||
this.drawSafeText(ctx, '微信长按识别小程序', 198, 842, {
|
||||
fontSize: 25,
|
||||
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)
|
||||
ctx.setFillStyle('#04874A')
|
||||
ctx.fillText('查看完整电话 · 微信 · 地图导航', 214, 904)
|
||||
|
||||
ctx.setFontSize(18)
|
||||
ctx.setFillStyle('#88A395')
|
||||
ctx.fillText('同城生活分类信息 · 官方便民服务保障', 214, 938)
|
||||
|
||||
// 10. 最终导出高保真图片
|
||||
// 11. 最终导出超清海报(2x Retina 分辨率:1280 x 2000)
|
||||
ctx.draw(false, () => {
|
||||
setTimeout(() => {
|
||||
uni.canvasToTempFilePath(
|
||||
@@ -1487,22 +1658,23 @@ export default {
|
||||
canvasId: 'postPosterCanvas',
|
||||
width: W,
|
||||
height: H,
|
||||
destWidth: W,
|
||||
destHeight: H,
|
||||
destWidth: W * 2,
|
||||
destHeight: H * 2,
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
success: (res) => {
|
||||
this.posterImagePath = res.tempFilePath
|
||||
this.posterLoading = false
|
||||
},
|
||||
fail: () => {
|
||||
fail: (err) => {
|
||||
console.error('canvasToTempFilePath error', err)
|
||||
this.posterLoading = false
|
||||
uni.showToast({ title: '海报绘制失败,请重试', icon: 'none' })
|
||||
},
|
||||
},
|
||||
this
|
||||
)
|
||||
}, 260)
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
/**
|
||||
@@ -1554,7 +1726,9 @@ export default {
|
||||
* 一键复制海报文案
|
||||
*/
|
||||
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({
|
||||
data: text,
|
||||
success: () => {
|
||||
|
||||
文件差异内容过多而无法显示
加载差异
@@ -419,7 +419,27 @@ export default {
|
||||
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 {
|
||||
const m = getNavMetrics()
|
||||
this.statusBarHeight = m.statusBarHeight
|
||||
|
||||
@@ -770,22 +770,23 @@ export default {
|
||||
padding: 16rpx 28rpx 0;
|
||||
}
|
||||
.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;
|
||||
padding: 36rpx 32rpx 28rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12rpx 32rpx rgba(2, 58, 33, 0.28);
|
||||
border: 1rpx solid rgba(0, 224, 112, 0.22);
|
||||
box-shadow: 0 16rpx 36rpx rgba(245, 166, 35, 0.22);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.vip__card-sheen {
|
||||
position: absolute;
|
||||
top: -80rpx;
|
||||
right: -80rpx;
|
||||
width: 260rpx;
|
||||
height: 260rpx;
|
||||
top: -90rpx;
|
||||
right: -90rpx;
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
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;
|
||||
}
|
||||
.vip__card-user {
|
||||
@@ -798,17 +799,18 @@ export default {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #00e070 0%, #00b85c 100%);
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: 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;
|
||||
}
|
||||
.vip__avatar-txt {
|
||||
font-size: 38rpx;
|
||||
font-weight: 800;
|
||||
color: #04301e;
|
||||
color: #4a2600;
|
||||
}
|
||||
.vip__user-meta {
|
||||
margin-left: 20rpx;
|
||||
@@ -822,19 +824,20 @@ export default {
|
||||
}
|
||||
.vip__user-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
color: #3e2000;
|
||||
}
|
||||
.vip__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(4px);
|
||||
background: rgba(74, 38, 0, 0.08);
|
||||
border: 1rpx solid rgba(74, 38, 0, 0.12);
|
||||
}
|
||||
.vip__badge--active {
|
||||
background: linear-gradient(135deg, #ffc25c 0%, #f5a623 100%);
|
||||
background: #3e2000;
|
||||
border-color: #3e2000;
|
||||
}
|
||||
.vip__badge-icon {
|
||||
width: 22rpx;
|
||||
@@ -843,24 +846,28 @@ export default {
|
||||
}
|
||||
.vip__badge-txt {
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
color: #4a2600;
|
||||
font-weight: 700;
|
||||
}
|
||||
.vip__badge--active .vip__badge-txt {
|
||||
color: #ffe7a6;
|
||||
}
|
||||
.vip__user-desc {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
color: #7a4600;
|
||||
font-weight: 500;
|
||||
}
|
||||
.vip__card-crown {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
opacity: 0.25;
|
||||
opacity: 0.18;
|
||||
}
|
||||
|
||||
.vip__assets {
|
||||
margin-top: 32rpx;
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -878,13 +885,14 @@ export default {
|
||||
.vip__asset-val {
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
color: #3e2000;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
.vip__asset-unit {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
color: #7a4600;
|
||||
margin-left: 4rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.vip__asset-label-row {
|
||||
margin-top: 6rpx;
|
||||
@@ -894,17 +902,21 @@ export default {
|
||||
}
|
||||
.vip__asset-lbl {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
color: #7a4600;
|
||||
font-weight: 500;
|
||||
}
|
||||
.vip__asset-act {
|
||||
font-size: 20rpx;
|
||||
color: #00e070;
|
||||
font-weight: 600;
|
||||
color: #4a2600;
|
||||
font-weight: 700;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
.vip__asset-sep {
|
||||
width: 1rpx;
|
||||
height: 36rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: rgba(74, 38, 0, 0.12);
|
||||
}
|
||||
|
||||
.vip__section {
|
||||
@@ -912,7 +924,7 @@ export default {
|
||||
background: #ffffff;
|
||||
border-radius: 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 {
|
||||
display: flex;
|
||||
@@ -923,11 +935,11 @@ export default {
|
||||
.vip__sec-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #04291c;
|
||||
color: #1e293b;
|
||||
}
|
||||
.vip__sec-tip {
|
||||
font-size: 22rpx;
|
||||
color: #8fb3a3;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.vip__privileges-grid {
|
||||
@@ -938,7 +950,7 @@ export default {
|
||||
.vip__priv-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f7faf8;
|
||||
background: #f8fafc;
|
||||
padding: 20rpx 18rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
@@ -970,13 +982,13 @@ export default {
|
||||
.vip__priv-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #04291c;
|
||||
color: #1e293b;
|
||||
display: block;
|
||||
}
|
||||
.vip__priv-desc {
|
||||
margin-top: 4rpx;
|
||||
font-size: 20rpx;
|
||||
color: #8fb3a3;
|
||||
color: #64748b;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -988,7 +1000,7 @@ export default {
|
||||
}
|
||||
.vip__plan-card {
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e2eee7;
|
||||
border: 2rpx solid #e2e8f0;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 10rpx 24rpx;
|
||||
display: flex;
|
||||
@@ -1008,7 +1020,7 @@ export default {
|
||||
top: -12rpx;
|
||||
right: -6rpx;
|
||||
background: #00e070;
|
||||
color: #04301e;
|
||||
color: #0f2f1d;
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
padding: 2rpx 12rpx;
|
||||
@@ -1017,7 +1029,7 @@ export default {
|
||||
.vip__plan-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #04291c;
|
||||
color: #1e293b;
|
||||
}
|
||||
.vip__plan-price-row {
|
||||
display: flex;
|
||||
@@ -1038,13 +1050,13 @@ export default {
|
||||
.vip__plan-del {
|
||||
margin-top: 4rpx;
|
||||
font-size: 20rpx;
|
||||
color: #a4b8af;
|
||||
color: #94a3b8;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.vip__plan-quota-chip {
|
||||
margin-top: 12rpx;
|
||||
font-size: 18rpx;
|
||||
color: #04874a;
|
||||
color: #00a854;
|
||||
background: #eaf8f0;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
@@ -1056,13 +1068,13 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
border-top: 1rpx solid #edf4f0;
|
||||
border-bottom: 1rpx solid #edf4f0;
|
||||
border-top: 1rpx solid #f1f5f9;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
margin: 24rpx 0 16rpx;
|
||||
}
|
||||
.vip__coupon-lbl {
|
||||
font-size: 26rpx;
|
||||
color: #04291c;
|
||||
color: #1e293b;
|
||||
font-weight: 700;
|
||||
}
|
||||
.vip__coupon-act-box {
|
||||
@@ -1081,17 +1093,17 @@ export default {
|
||||
background: #fff4e0;
|
||||
}
|
||||
&--has {
|
||||
color: #04874a;
|
||||
color: #00a854;
|
||||
background: #eaf8f0;
|
||||
}
|
||||
&--none {
|
||||
color: #8fb3a3;
|
||||
color: #64748b;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
.vip__coupon-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #8fb3a3;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.vip__pay-toggle {
|
||||
@@ -1100,7 +1112,7 @@ export default {
|
||||
.vip__pay-label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #04291c;
|
||||
color: #1e293b;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
@@ -1114,7 +1126,7 @@ export default {
|
||||
align-items: center;
|
||||
padding: 20rpx 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f7faf8;
|
||||
background: #f8fafc;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
@@ -1132,14 +1144,14 @@ export default {
|
||||
.vip__pay-opt-txt {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
color: #04291c;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
.vip__radio {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #c8d8d0;
|
||||
border: 2rpx solid #cbd5e1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1154,7 +1166,7 @@ export default {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #04301e;
|
||||
background: #0f2f1d;
|
||||
}
|
||||
|
||||
.vip__dock-pad {
|
||||
@@ -1166,7 +1178,7 @@ export default {
|
||||
left: 0;
|
||||
right: 0;
|
||||
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-bottom: calc(20rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
@@ -1185,7 +1197,7 @@ export default {
|
||||
}
|
||||
.vip__dock-tag {
|
||||
font-size: 24rpx;
|
||||
color: #668877;
|
||||
color: #64748b;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
.vip__dock-sym {
|
||||
@@ -1212,7 +1224,7 @@ export default {
|
||||
.vip__dock-meta {
|
||||
margin-top: 6rpx;
|
||||
font-size: 20rpx;
|
||||
color: #8fb3a3;
|
||||
color: #64748b;
|
||||
}
|
||||
.vip__dock-submit {
|
||||
height: 84rpx;
|
||||
@@ -1222,14 +1234,14 @@ export default {
|
||||
display: flex;
|
||||
align-items: 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;
|
||||
border: none;
|
||||
}
|
||||
.vip__dock-submit-txt {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #04301e;
|
||||
color: #0f2f1d;
|
||||
}
|
||||
|
||||
.vip__sheet-mask {
|
||||
@@ -1258,19 +1270,19 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx 36rpx 20rpx;
|
||||
border-bottom: 1rpx solid #edf4f0;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
}
|
||||
.vip__sheet-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #04291c;
|
||||
color: #1e293b;
|
||||
}
|
||||
.vip__sheet-close-btn {
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
.vip__sheet-close-txt {
|
||||
font-size: 32rpx;
|
||||
color: #8fb3a3;
|
||||
color: #64748b;
|
||||
}
|
||||
.vip__sheet-scroll {
|
||||
max-height: 55vh;
|
||||
@@ -1288,7 +1300,7 @@ export default {
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f7faf8;
|
||||
background: #f8fafc;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
@@ -1303,7 +1315,7 @@ export default {
|
||||
}
|
||||
.vip__sheet-opt-txt {
|
||||
font-size: 28rpx;
|
||||
color: #3b5f4d;
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
}
|
||||
.vip__sheet-card-left {
|
||||
@@ -1313,7 +1325,7 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-right: 2rpx dashed #dbe8e1;
|
||||
border-right: 2rpx dashed #cbd5e1;
|
||||
padding-right: 16rpx;
|
||||
margin-right: 18rpx;
|
||||
}
|
||||
@@ -1333,7 +1345,7 @@ export default {
|
||||
}
|
||||
.vip__sheet-card-cond {
|
||||
font-size: 18rpx;
|
||||
color: #8a7343;
|
||||
color: #b45309;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.vip__sheet-card-info {
|
||||
@@ -1346,15 +1358,15 @@ export default {
|
||||
.vip__sheet-card-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #04291c;
|
||||
color: #1e293b;
|
||||
}
|
||||
.vip__sheet-card-date {
|
||||
font-size: 20rpx;
|
||||
color: #8fb3a3;
|
||||
color: #64748b;
|
||||
}
|
||||
.vip__sheet-bottom {
|
||||
padding: 20rpx 36rpx 24rpx;
|
||||
border-top: 1rpx solid #edf4f0;
|
||||
border-top: 1rpx solid #f1f5f9;
|
||||
}
|
||||
.vip__sheet-btn {
|
||||
height: 84rpx;
|
||||
@@ -1368,6 +1380,6 @@ export default {
|
||||
.vip__sheet-btn-txt {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #04301e;
|
||||
color: #0f2f1d;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -84,14 +84,14 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 门店电话行 -->
|
||||
<!-- 门店电话行(支持未解锁受保护提示) -->
|
||||
<view v-if="merchant.contact" class="shop-detail__cell-row" @click="callPhone">
|
||||
<view class="shop-detail__cell-left">
|
||||
<image class="fc-icon fc-icon--sm" :src="iconUrl('phone')" mode="aspectFit" />
|
||||
<text class="shop-detail__cell-txt">联系咨询:{{ merchant.contact }}</text>
|
||||
</view>
|
||||
<view class="shop-detail__call-link">
|
||||
<text>立即拨打 ›</text>
|
||||
<view class="shop-detail__call-link" :class="{ 'shop-detail__call-link--locked': !merchant.unlocked }">
|
||||
<text>{{ merchant.unlocked ? '立即拨打 ›' : '受保护 · 去解锁 ›' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -155,9 +155,9 @@
|
||||
<text>地图导航</text>
|
||||
</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" />
|
||||
<text>联系店长</text>
|
||||
<text>{{ merchant.unlocked ? '联系店长' : '联系店长 (受保护)' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="fc-safe-bottom"></view>
|
||||
@@ -196,6 +196,7 @@ export default {
|
||||
loadingMore: false,
|
||||
userLat: null,
|
||||
userLng: null,
|
||||
postId: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -232,6 +233,7 @@ export default {
|
||||
*/
|
||||
onLoad(query) {
|
||||
this.merchantId = Number(query.id || query.userId || 0)
|
||||
this.postId = Number(query.postId || 0)
|
||||
loadUiConfig()
|
||||
this.fetchLocation()
|
||||
this.loadDetail()
|
||||
@@ -291,6 +293,9 @@ export default {
|
||||
if (!refresh) this.loading = true
|
||||
try {
|
||||
const params = {}
|
||||
if (this.postId > 0) {
|
||||
params.postId = this.postId
|
||||
}
|
||||
if (this.userLat != null && this.userLng != null) {
|
||||
params.lat = this.userLat
|
||||
params.lng = this.userLng
|
||||
@@ -368,9 +373,29 @@ export default {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 一键拨打商家咨询电话
|
||||
* 一键拨打商家咨询电话(未解锁/非会员状态下严格执行付费拦截)
|
||||
*/
|
||||
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
|
||||
if (!phone) {
|
||||
uni.showToast({ title: '商家暂未公开联系电话', icon: 'none' })
|
||||
|
||||
@@ -570,12 +570,12 @@ export default {
|
||||
/* 顶部 Hero 状态区 */
|
||||
.order-detail__hero {
|
||||
padding: 36rpx 32rpx 30rpx;
|
||||
background: linear-gradient(150deg, #04874A, #023A21);
|
||||
background: linear-gradient(135deg, #00D26A 0%, #00A854 100%);
|
||||
color: #ffffff;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
&--paid {
|
||||
background: linear-gradient(150deg, #04874A, #02502C);
|
||||
background: linear-gradient(135deg, #00D26A 0%, #00B359 100%);
|
||||
}
|
||||
&--closed {
|
||||
background: linear-gradient(150deg, #4A5B53, #2B3A33);
|
||||
|
||||
@@ -433,7 +433,7 @@ export default {
|
||||
lat: null,
|
||||
lng: null,
|
||||
plan: 'free',
|
||||
payType: config.defaultPayType,
|
||||
payType: config.defaultPayType || 'wxpay',
|
||||
},
|
||||
publish: {
|
||||
mode: 'paid',
|
||||
|
||||
在新工单中引用
屏蔽一个用户