diff --git a/application/api/controller/AdminMember.php b/application/api/controller/AdminMember.php
index 3daf99c..2c2c58d 100644
--- a/application/api/controller/AdminMember.php
+++ b/application/api/controller/AdminMember.php
@@ -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'],
diff --git a/application/api/controller/Distribution.php b/application/api/controller/Distribution.php
index 567bdfd..5f1432d 100644
--- a/application/api/controller/Distribution.php
+++ b/application/api/controller/Distribution.php
@@ -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;
}
-}
+}
\ No newline at end of file
diff --git a/application/api/controller/Member.php b/application/api/controller/Member.php
index e057070..15d8390 100644
--- a/application/api/controller/Member.php
+++ b/application/api/controller/Member.php
@@ -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('支付方式不支持');
}
diff --git a/application/api/controller/Merchant.php b/application/api/controller/Merchant.php
index 1496894..4192d3d 100644
--- a/application/api/controller/Merchant.php
+++ b/application/api/controller/Merchant.php
@@ -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'],
diff --git a/application/api/controller/Post.php b/application/api/controller/Post.php
index d53ba0a..c22f1ac 100644
--- a/application/api/controller/Post.php
+++ b/application/api/controller/Post.php
@@ -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;
}
diff --git a/application/api/controller/V1.php b/application/api/controller/V1.php
index 1510763..fa04d4d 100644
--- a/application/api/controller/V1.php
+++ b/application/api/controller/V1.php
@@ -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';
diff --git a/application/route.php b/application/route.php
index b1eb482..a33589c 100755
--- a/application/route.php
+++ b/application/route.php
@@ -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',
diff --git a/uniapp/App.vue b/uniapp/App.vue
index 33371d9..55bfa59 100644
--- a/uniapp/App.vue
+++ b/uniapp/App.vue
@@ -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))
}
diff --git a/uniapp/README.md b/uniapp/README.md
index 725e177..5c9a237 100644
--- a/uniapp/README.md
+++ b/uniapp/README.md
@@ -305,7 +305,7 @@ rm -rf /tmp/fcmp && cp -R "$OUTPUT_DIR" /tmp/fcmp
```js
// 'balance' 余额支付 / 'wxpay' 微信支付
-defaultPayType: 'balance',
+defaultPayType: 'wxpay',
```
配置好真实商户参数后改回 `'wxpay'` 即可,**各页面无需改动**(三个支付点都读这个值)。
diff --git a/uniapp/common/api/distribution.js b/uniapp/common/api/distribution.js
index df37dd2..252c37b 100644
--- a/uniapp/common/api/distribution.js
+++ b/uniapp/common/api/distribution.js
@@ -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 }),
/** 提现记录 */
diff --git a/uniapp/common/config/index.js b/uniapp/common/config/index.js
index 824815c..7c87cd0 100644
--- a/uniapp/common/config/index.js
+++ b/uniapp/common/config/index.js
@@ -75,9 +75,7 @@ export const config = {
// 强制使用 mock(联调前本地联调可置 true)
forceMock: false,
// 默认支付方式:'balance' 余额支付 / 'wxpay' 微信支付
- // ⚠️ 微信支付商户参数(mchId/apiKey)尚未配置,测试阶段默认走余额支付;
- // 配置好真实商户参数后改回 'wxpay' 即可,无需改动各页面。
- defaultPayType: 'balance',
+ defaultPayType: 'wxpay',
// 本地存储键
storageKeys: {
token: 'fc_token',
diff --git a/uniapp/common/utils/request.js b/uniapp/common/utils/request.js
index 8969edd..7354b7e 100644
--- a/uniapp/common/utils/request.js
+++ b/uniapp/common/utils/request.js
@@ -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)
diff --git a/uniapp/pages.json b/uniapp/pages.json
index fd34142..a4df566 100644
--- a/uniapp/pages.json
+++ b/uniapp/pages.json
@@ -153,7 +153,8 @@
"style": {
"navigationBarTitleText": "分销中心",
"navigationStyle": "custom",
- "backgroundColor": "#f2fef7"
+ "backgroundColor": "#f2fef7",
+ "enablePullDownRefresh": true
}
},
{
@@ -248,4 +249,4 @@
]
},
"uniIdRouter": {}
-}
+}
\ No newline at end of file
diff --git a/uniapp/pages/detail/detail.vue b/uniapp/pages/detail/detail.vue
index 81411c6..a729bc9 100644
--- a/uniapp/pages/detail/detail.vue
+++ b/uniapp/pages/detail/detail.vue
@@ -477,12 +477,12 @@
-
+
@@ -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}
*/
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: () => {
diff --git a/uniapp/pages/distribution/index.vue b/uniapp/pages/distribution/index.vue
index 414c24d..2220e31 100644
--- a/uniapp/pages/distribution/index.vue
+++ b/uniapp/pages/distribution/index.vue
@@ -1,13 +1,20 @@
-
-
+
+
+
+
+
+ 规则
+
+
+
- 分享好内容,赚取佣金
- 好友消费,佣金自动到账
+ 分享好内容,天天赚佣金
+ 好友消费自动返佣 · {{ overview.settleCycle || 'T+7' }} 自动结算提现
@@ -31,13 +38,17 @@
{{ money(overview.totalCommission) }}
累计佣金
+
+ {{ money(overview.pendingCommission) }}
+ 待结算
+
{{ money(overview.settled) }}
已结算
{{ money(overview.todayCommission) }}
- 今日佣金
+ 今日收益
@@ -48,21 +59,22 @@
数据加载失败,点击重试
-
+
- 邀请好友
+ 邀请好友推广
+
- 我的邀请码
+ 我的专属邀请码
{{ overview.inviteCode || '—' }}
@@ -70,13 +82,35 @@
+
+
+
+
+
+
+
+
- 分享给好友
+ 分享好友
+
+
+
+
+
+ 专属海报
+
+
+
复制链接
@@ -84,18 +118,33 @@
-
-
+
+
+
+ {{ tab.name }}
+
+
+
+
+
+
- 我的团队
+ 团队成员
+
- 一级 {{ overview.l1Count || 0 }} 人
+ 一级直推 {{ overview.l1Count || 0 }} 人(返佣 {{ overview.level1Rate || 20 }}%)
·
- 二级 {{ overview.l2Count || 0 }} 人
+ 二级间推 {{ overview.l2Count || 0 }} 人(返佣 {{ overview.level2Rate || 10 }}%)
-
+
@@ -125,29 +174,39 @@
+
- 还没有团队成员
-
- 邀请好友
+ 还没有团队成员,立即邀请好友加入
+
+ 生成邀请海报
+
-
- {{ firstChar(t.nickname) }}
+
+
+
+ {{ firstChar(t.nickname) }}
+
- {{ t.nickname || '匿名用户' }}
+ {{ t.nickname || '同城合伙人' }}
- {{ t.level === 1 ? '一级' : '二级' }}
+ {{ t.level === 1 ? '一级直推' : '二级间推' }}
- {{ joinText(t) }} · 业绩 ¥{{ money(t.performance) }}
+ {{ joinText(t) }} · 累计消费 ¥{{ money(t.performance) }}
+¥{{ money(t.commission) }}
@@ -155,15 +214,89 @@
+
- {{ teamExpanded ? '收起' : '展开全部 ' + team.length + ' 人' }}
+ {{ teamExpanded ? '收起' : '展开全部 ' + team.length + ' 位成员' }}
-
-
+
+
+
+
+
+
+
+ 佣金账单
+
+
+
+
+
+
+
+ {{ s.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无收益记录,好友消费后自动入账
+
+
+
+
+
+
+
+ {{ c.orderTypeName || '同城消费' }}
+
+ {{ c.statusText || (c.status === 1 ? '已结算' : (c.status === 2 ? '已失效' : '待结算')) }}
+
+
+ {{ timeText(c.createdAt) }} · {{ c.level === 1 ? '一级好友' : '二级好友' }}:{{ (c.fromUser && c.fromUser.nickname) || '同城好友' }}
+ 消费 ¥{{ money(c.orderAmount) }} · 分佣比例 {{ c.rate }}% · 单号 {{ c.orderNo }}
+
+
+ +¥{{ money(c.amount) }}
+
+
+
+
+
+ {{ commissionsExpanded ? '收起' : '查看全部收益账单' }}
+
+
+
+
+
+
+
@@ -213,7 +346,7 @@
- {{ recordsExpanded ? '收起' : '查看全部记录' }}
+ {{ recordsExpanded ? '收起' : '查看全部提现记录' }}
@@ -221,7 +354,7 @@
-
+
@@ -237,6 +370,7 @@
{{ money(overview.withdrawable) }}
+
+
¥
全部
+
- 最低提现 ¥{{ minWithdrawText }},预计 T+7 到账微信零钱
+ 最低提现 ¥{{ minWithdrawText }},预计 24 小时内直转微信零钱
+
+
+
+
+
+
+ 分销推广奖励规则
+
+
+
+
+
+
+
+ 1
+
+ 一级直推返佣:{{ overview.level1Rate || 20 }}%
+ 好友通过您的专属海报或链接注册,其在平台发布信息、置顶、开通会员或购买便民服务,您立享订单金额 {{ overview.level1Rate || 20 }}% 的现金佣金。
+
+
+
+
+ 2
+
+ 二级间推返佣:{{ overview.level2Rate || 10 }}%
+ 您的一级好友再次邀请新朋友消费,您仍可获得订单金额 {{ overview.level2Rate || 10 }}% 的间接分销奖励,裂变无忧。
+
+
+
+
+ 3
+
+ 结算周期与到账:{{ overview.settleCycle || 'T+7' }}
+ 订单支付完成后佣金即刻进入待结算,满 {{ overview.settleCycle || 'T+7' }} 天后自动转入可提现余额,随时提现至微信零钱。
+
+
+
+
+ 4
+
+ 提现规则与到账保障
+ 最低提现金额为 ¥{{ minWithdrawText }},平台支持微信商家转账直接到账微信零钱,无任何手续费。
+
+
+
+
+
+ 我知道了
+
+
+
+
+
+
+
+
+
+ 专属合伙人海报
+
+
+
+
+
+
+
+
+ 正在生成专属推广海报…
+
+
+
+
+
+ 点击大图可全屏预览,长按图片直接转发
+
+
+
+
+
+
+ 保存到相册
+
+
+
+
+
+ 复制文案
+
+
+
+
+
+
+
diff --git a/uniapp/pages/index/index.vue b/uniapp/pages/index/index.vue
index 041481e..fe601d4 100644
--- a/uniapp/pages/index/index.vue
+++ b/uniapp/pages/index/index.vue
@@ -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
diff --git a/uniapp/pages/member/index.vue b/uniapp/pages/member/index.vue
index 623ffdf..880a3d2 100644
--- a/uniapp/pages/member/index.vue
+++ b/uniapp/pages/member/index.vue
@@ -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;
}
-
+
\ No newline at end of file
diff --git a/uniapp/pages/merchant/detail.vue b/uniapp/pages/merchant/detail.vue
index 40a5621..2c141c2 100644
--- a/uniapp/pages/merchant/detail.vue
+++ b/uniapp/pages/merchant/detail.vue
@@ -84,14 +84,14 @@
-
+
联系咨询:{{ merchant.contact }}
-
- 立即拨打 ›
+
+ {{ merchant.unlocked ? '立即拨打 ›' : '受保护 · 去解锁 ›' }}
@@ -155,9 +155,9 @@
地图导航
-
+
- 联系店长
+ {{ merchant.unlocked ? '联系店长' : '联系店长 (受保护)' }}
@@ -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' })
diff --git a/uniapp/pages/order/detail.vue b/uniapp/pages/order/detail.vue
index ebdcda3..cd03aa3 100644
--- a/uniapp/pages/order/detail.vue
+++ b/uniapp/pages/order/detail.vue
@@ -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);
diff --git a/uniapp/pages/post/create.vue b/uniapp/pages/post/create.vue
index 73bc06b..8fc541d 100644
--- a/uniapp/pages/post/create.vue
+++ b/uniapp/pages/post/create.vue
@@ -433,7 +433,7 @@ export default {
lat: null,
lng: null,
plan: 'free',
- payType: config.defaultPayType,
+ payType: config.defaultPayType || 'wxpay',
},
publish: {
mode: 'paid',