chore:微信小程序分享名片给好友正常

This commit is contained in:
2026-10-17 17:22:57 +08:00
parent 21b3247547
commit 7daa3afdc2
8 changed files with 177 additions and 37 deletions

View File

@@ -1530,8 +1530,13 @@
{ {
"path": "card/share", "path": "card/share",
"style": { "style": {
"navigationBarTitleText": "名片分享", // #ifdef MP-HARMONY
"navigationStyle": "custom" // 鸿蒙元服务的 web-view 是原生全屏组件, 只能自绘头部(见 share.vue)
"navigationStyle": "custom",
// #endif
// 微信小程序必须用原生导航栏: web-view 会自动排在导航栏下方,
// 若用 custom + cover-view 自绘头部, 头部会浮在 web-view 上遮住名片顶部
"navigationBarTitleText": "名片分享"
} }
}, },
{ {

View File

@@ -27,7 +27,7 @@
<!-- 人脉列表 --> <!-- 人脉列表 -->
<view class="contact-list"> <view class="contact-list">
<view v-for="item in list" :key="item.id" class="contact-item"> <view v-for="(item, index) in list" :key="item.id" class="contact-item">
<view class="item-top" @tap="callPhone(item.mobile)"> <view class="item-top" @tap="callPhone(item.mobile)">
<view class="item-name">{{ item.name }}</view> <view class="item-name">{{ item.name }}</view>
<view class="item-source" :class="'src-' + item.source">{{ item.source_text }}</view> <view class="item-source" :class="'src-' + item.source">{{ item.source_text }}</view>
@@ -42,9 +42,9 @@
<text class="meta-status" v-if="item.status == 1">已归档</text> <text class="meta-status" v-if="item.status == 1">已归档</text>
</view> </view>
<view class="item-actions"> <view class="item-actions">
<view class="action-btn plain" @tap="goEdit(item.id)">编辑</view> <view class="action-btn plain" :data-index="index" @tap="goEdit">编辑</view>
<view class="action-btn primary" @tap="convertToLead(item)">转为潜在客户</view> <view class="action-btn primary" :data-index="index" @tap="convertToLead">转为潜在客户</view>
<view class="action-btn danger" @tap="delContact(item)">删除</view> <view class="action-btn danger" :data-index="index" @tap="delContact">删除</view>
</view> </view>
</view> </view>
@@ -116,7 +116,27 @@ export default {
}, },
goSelf() {}, goSelf() {},
// 编辑页共用 contact-edit; from=admin 时保存后回本页(见 contact-edit.vue#backToList) // 编辑页共用 contact-edit; from=admin 时保存后回本页(见 contact-edit.vue#backToList)
goEdit(id) { /**
* 取事件对应的行数据。小程序端 v-for 内联传对象会丢参, 统一 :data-index + 不传参。
* 兼容: 无参(新建) / 事件对象(data-index) / 直接 id。
*/
resolveItem(payload) {
if (payload == null) return null;
if (typeof payload === 'number' || typeof payload === 'string') return null;
const ds = payload.currentTarget && payload.currentTarget.dataset;
if (ds) {
const i = Number(ds.index);
if (!isNaN(i) && this.list[i]) return this.list[i];
}
return payload.id ? payload : null;
},
goEdit(payload) {
let id = payload;
if (payload && payload.currentTarget) {
const i = Number(payload.currentTarget.dataset.index);
const item = this.list[i];
id = item ? item.id : '';
}
const url = id const url = id
? '/pages_tool/card/contact-edit?from=admin&id=' + id ? '/pages_tool/card/contact-edit?from=admin&id=' + id
: '/pages_tool/card/contact-edit?from=admin'; : '/pages_tool/card/contact-edit?from=admin';
@@ -163,7 +183,9 @@ export default {
} }
}); });
}, },
convertToLead(item) { convertToLead(payload) {
const item = this.resolveItem(payload);
if (!item) return;
uni.showModal({ uni.showModal({
title: '转为潜在客户', title: '转为潜在客户',
content: '将「' + (item.name || '') + '」转入潜在客户跟进?人脉记录会保留。', content: '将「' + (item.name || '') + '」转入潜在客户跟进?人脉记录会保留。',
@@ -192,7 +214,9 @@ export default {
} }
}); });
}, },
delContact(item) { delContact(payload) {
const item = this.resolveItem(payload);
if (!item) return;
// 真删不可恢复, 二次确认 // 真删不可恢复, 二次确认
uni.showModal({ uni.showModal({
title: '删除人脉', title: '删除人脉',

View File

@@ -39,7 +39,7 @@
</view> </view>
<view class="lead-actions"> <view class="lead-actions">
<view class="action-btn copy" v-if="item.wechat" @tap="copyText(item.wechat)">复制微信</view> <view class="action-btn copy" v-if="item.wechat" @tap="copyText(item.wechat)">复制微信</view>
<view class="action-btn primary" @tap="changeStatus(item, index)">更新状态</view> <view class="action-btn primary" :data-index="index" @tap="changeStatus">更新状态</view>
</view> </view>
</view> </view>
@@ -143,7 +143,33 @@ export default {
} }
}); });
}, },
changeStatus(item, index) { /**
* 取事件对应的行数据。小程序端 v-for 内联传对象会丢参, 统一 :data-index + 不传参。
* 兼容: 事件对象(data-index) / 数字索引 / 行对象本身。
*/
resolveItem(payload) {
if (payload == null) return null;
if (typeof payload === 'number') return this.list[payload] || null;
const ds = payload.currentTarget && payload.currentTarget.dataset;
if (ds) {
const i = Number(ds.index);
if (!isNaN(i) && this.list[i]) return this.list[i];
}
return payload.id ? payload : null;
},
changeStatus(payload) {
let item, index;
if (payload && payload.currentTarget) {
index = Number(payload.currentTarget.dataset.index);
item = this.list[index];
} else if (typeof payload === 'number') {
index = payload;
item = this.list[index];
} else {
item = payload;
index = this.list.indexOf(item);
}
if (!item) return;
uni.showActionSheet({ uni.showActionSheet({
itemList: LEAD_STATUS.map(s => s.label), itemList: LEAD_STATUS.map(s => s.label),
success: res => { success: res => {

View File

@@ -25,8 +25,8 @@
<!-- 名片列表 --> <!-- 名片列表 -->
<view class="card-list"> <view class="card-list">
<view v-for="(item, index) in list" :key="item.card_id || index" class="card-item" <view v-for="(item, index) in list" :key="item.card_id || index" :data-index="index" class="card-item"
@tap="goDetail(item)"> @tap="goDetail">
<view class="card-avatar"> <view class="card-avatar">
<image v-if="item.avatar" :src="$util.img(item.avatar)" class="avatar-img" mode="aspectFill" /> <image v-if="item.avatar" :src="$util.img(item.avatar)" class="avatar-img" mode="aspectFill" />
<text v-else>{{ avatarText(item.name) }}</text> <text v-else>{{ avatarText(item.name) }}</text>
@@ -41,8 +41,8 @@
<view class="card-side"> <view class="card-side">
<text v-if="sourceLabel(item)" class="source-tag">{{ sourceLabel(item) }}</text> <text v-if="sourceLabel(item)" class="source-tag">{{ sourceLabel(item) }}</text>
<view class="card-actions" @tap.stop=""> <view class="card-actions" @tap.stop="">
<text class="act-btn" @tap="goEdit(item)">编辑</text> <text class="act-btn" :data-index="index" @tap="goEdit">编辑</text>
<text class="act-btn primary" @tap="goDetail(item)">详情</text> <text class="act-btn primary" :data-index="index" @tap="goDetail">详情</text>
</view> </view>
</view> </view>
</view> </view>
@@ -159,14 +159,30 @@ export default {
}, },
/* ---------- 导航 ---------- */ /* ---------- 导航 ---------- */
/**
* 取事件对应的行数据。小程序端 v-for 内联传对象(如 goDetail(item))会丢参,
* 统一改为 :data-index + 不传参, 这里从 dataset 取索引回表。
*/
resolveItem(payload) {
if (payload == null) return null;
if (typeof payload === 'number') return this.list[payload] || null;
const ds = payload.currentTarget && payload.currentTarget.dataset;
if (ds) {
const i = Number(ds.index);
if (!isNaN(i) && this.list[i]) return this.list[i];
}
return payload.card_id ? payload : null;
},
// 名片路由参数一律 card_id(字符串), encodeURIComponent 幂等 // 名片路由参数一律 card_id(字符串), encodeURIComponent 幂等
goDetail(item) { goDetail(payload) {
const item = this.resolveItem(payload);
if (!item || !item.card_id) return; if (!item || !item.card_id) return;
uni.navigateTo({ uni.navigateTo({
url: '/pages_tool/card/detail?card_id=' + encodeURIComponent(item.card_id) url: '/pages_tool/card/detail?card_id=' + encodeURIComponent(item.card_id)
}); });
}, },
goEdit(item) { goEdit(payload) {
const item = this.resolveItem(payload);
if (!item || !item.card_id) return; if (!item || !item.card_id) return;
uni.navigateTo({ uni.navigateTo({
url: '/pages_tool/card/edit?card_id=' + encodeURIComponent(item.card_id) url: '/pages_tool/card/edit?card_id=' + encodeURIComponent(item.card_id)

View File

@@ -41,9 +41,9 @@
<text class="meta-status" v-if="item.status == 1">已归档</text> <text class="meta-status" v-if="item.status == 1">已归档</text>
</view> </view>
<view class="item-actions"> <view class="item-actions">
<view class="action-btn plain" @tap="goEdit(item.id)">编辑</view> <view class="action-btn plain" :data-index="index" @tap="goEdit">编辑</view>
<view class="action-btn primary" @tap="convertToLead(item)">转为潜在客户</view> <view class="action-btn primary" :data-index="index" @tap="convertToLead">转为潜在客户</view>
<view class="action-btn danger" @tap="delContact(item)">删除</view> <view class="action-btn danger" :data-index="index" @tap="delContact">删除</view>
</view> </view>
</view> </view>
@@ -102,7 +102,28 @@ export default {
goLeads() { goLeads() {
this.$util.redirectTo('/pages_tool/card/leads'); this.$util.redirectTo('/pages_tool/card/leads');
}, },
goEdit(id) { /**
* 取事件对应的行数据。小程序端 v-for 内联传对象(如 convertToLead(item))会丢参,
* 统一改为 :data-index + 不传参, 这里从 dataset 取索引回表。
* 兼容: 无参(新建) / 事件对象(data-index) / 直接 id。
*/
resolveItem(payload) {
if (payload == null) return null;
if (typeof payload === 'number') return this.list[payload] || null;
const ds = payload.currentTarget && payload.currentTarget.dataset;
if (ds) {
const i = Number(ds.index);
if (!isNaN(i) && this.list[i]) return this.list[i];
}
return payload.id ? payload : null;
},
goEdit(payload) {
let id = payload;
if (payload && payload.currentTarget) {
const i = Number(payload.currentTarget.dataset.index);
const item = this.list[i];
id = item ? item.id : '';
}
const url = id ? '/pages_tool/card/contact-edit?id=' + id : '/pages_tool/card/contact-edit'; const url = id ? '/pages_tool/card/contact-edit?id=' + id : '/pages_tool/card/contact-edit';
this.$util.redirectTo(url); this.$util.redirectTo(url);
}, },
@@ -150,7 +171,9 @@ export default {
} }
}); });
}, },
convertToLead(item) { convertToLead(payload) {
const item = this.resolveItem(payload);
if (!item) return;
uni.showModal({ uni.showModal({
title: '转为潜在客户', title: '转为潜在客户',
content: '将「' + (item.name || '') + '」转入潜在客户跟进?人脉记录会保留。', content: '将「' + (item.name || '') + '」转入潜在客户跟进?人脉记录会保留。',
@@ -178,7 +201,9 @@ export default {
} }
}); });
}, },
delContact(item) { delContact(payload) {
const item = this.resolveItem(payload);
if (!item) return;
uni.showModal({ uni.showModal({
title: '删除人脉', title: '删除人脉',
content: '删除后不可恢复,确定删除「' + (item.name || '') + '」?', content: '删除后不可恢复,确定删除「' + (item.name || '') + '」?',

View File

@@ -10,9 +10,9 @@
<!-- 交换请求列表 --> <!-- 交换请求列表 -->
<view class="ex-list"> <view class="ex-list">
<view v-for="item in shownList" :key="item.id" class="ex-item"> <view v-for="(item, index) in shownList" :key="item.id" class="ex-item">
<view class="ex-top"> <view class="ex-top">
<view class="ex-avatar-wrap" @tap.stop="viewCard(item)"> <view class="ex-avatar-wrap" :data-index="index" @tap.stop="viewCard">
<image v-if="item.avatar" class="ex-avatar" :src="item.avatar" mode="aspectFill" /> <image v-if="item.avatar" class="ex-avatar" :src="item.avatar" mode="aspectFill" />
<view v-else class="ex-avatar ex-avatar-ph">{{ item.name.charAt(0) }}</view> <view v-else class="ex-avatar ex-avatar-ph">{{ item.name.charAt(0) }}</view>
</view> </view>
@@ -26,7 +26,7 @@
<text class="ex-company">{{ item.company }}</text> <text class="ex-company">{{ item.company }}</text>
</view> </view>
</view> </view>
<view v-if="item.status === 'pending'" class="ex-agree" @tap.stop="onAgree(item)">同意</view> <view v-if="item.status === 'pending'" class="ex-agree" :data-index="index" @tap.stop="onAgree">同意</view>
<text v-else class="ex-agreed">已同意</text> <text v-else class="ex-agreed">已同意</text>
</view> </view>
@@ -110,8 +110,23 @@ export default {
} }
}); });
}, },
/**
* 取事件对应的行数据。小程序端 v-for 内联传对象(viewCard(item))会丢参,
* 统一改为 :data-index + 不传参, 这里从 dataset 取索引回表。
*/
resolveItem(payload) {
if (payload == null) return null;
if (typeof payload === 'number') return this.shownList[payload] || null;
const ds = payload.currentTarget && payload.currentTarget.dataset;
if (ds) {
const i = Number(ds.index);
if (!isNaN(i) && this.shownList[i]) return this.shownList[i];
}
return payload.id ? payload : null;
},
// 点对方名片 → 跳转对方名片详情(用 from_card_id 字符串) // 点对方名片 → 跳转对方名片详情(用 from_card_id 字符串)
viewCard(item) { viewCard(payload) {
const item = this.resolveItem(payload);
if (!item || !item.from_card_id) return; if (!item || !item.from_card_id) return;
uni.navigateTo({ uni.navigateTo({
url: '/pages_tool/card/detail?card_id=' + encodeURIComponent(item.from_card_id) url: '/pages_tool/card/detail?card_id=' + encodeURIComponent(item.from_card_id)
@@ -154,7 +169,9 @@ export default {
return ''; return '';
// #endif // #endif
}, },
onAgree(item) { onAgree(payload) {
const item = this.resolveItem(payload);
if (!item) return;
this.$cardApi.exchangeAgree({ this.$cardApi.exchangeAgree({
data: { id: item.id }, data: { id: item.id },
success: res => { success: res => {

View File

@@ -19,7 +19,7 @@
<!-- 客户列表 --> <!-- 客户列表 -->
<view class="lead-list"> <view class="lead-list">
<!-- 点「别人的名片」= 打开对方名片; 拨号/按钮一律 .stop, 避免误触成打电话 --> <!-- 点「别人的名片」= 打开对方名片; 拨号/按钮一律 .stop, 避免误触成打电话 -->
<view v-for="(item, index) in list" :key="item.id || index" class="lead-item" @tap="viewCard(item)"> <view v-for="(item, index) in list" :key="item.id || index" :data-index="index" class="lead-item" @tap="viewCard">
<view class="lead-top"> <view class="lead-top">
<view class="lead-name">{{ item.name || '未留姓名' }}</view> <view class="lead-name">{{ item.name || '未留姓名' }}</view>
<view class="lead-status" :class="'st-' + item.status">{{ item.status_text || '新客户' }}</view> <view class="lead-status" :class="'st-' + item.status">{{ item.status_text || '新客户' }}</view>
@@ -36,7 +36,7 @@
</view> </view>
<view class="lead-actions"> <view class="lead-actions">
<view class="action-btn copy" v-if="item.wechat" @tap.stop="copyText(item.wechat)">复制微信</view> <view class="action-btn copy" v-if="item.wechat" @tap.stop="copyText(item.wechat)">复制微信</view>
<view class="action-btn primary" @tap.stop="changeStatus(item, index)">更新状态</view> <view class="action-btn primary" :data-index="index" @tap.stop="changeStatus">更新状态</view>
</view> </view>
</view> </view>
@@ -127,7 +127,33 @@ export default {
} }
}); });
}, },
changeStatus(item, index) { /**
* 取事件对应的行数据。小程序端 v-for 内联传对象会丢参, 统一 :data-index + 不传参。
* 兼容: 事件对象(data-index) / 数字索引 / 行对象本身。
*/
resolveItem(payload) {
if (payload == null) return null;
if (typeof payload === 'number') return this.list[payload] || null;
const ds = payload.currentTarget && payload.currentTarget.dataset;
if (ds) {
const i = Number(ds.index);
if (!isNaN(i) && this.list[i]) return this.list[i];
}
return payload.id ? payload : null;
},
changeStatus(payload) {
let item, index;
if (payload && payload.currentTarget) {
index = Number(payload.currentTarget.dataset.index);
item = this.list[index];
} else if (typeof payload === 'number') {
index = payload;
item = this.list[index];
} else {
item = payload;
index = this.list.indexOf(item);
}
if (!item) return;
uni.showActionSheet({ uni.showActionSheet({
itemList: STATUS_MAP.map(s => s.label), itemList: STATUS_MAP.map(s => s.label),
success: res => { success: res => {
@@ -173,13 +199,14 @@ export default {
// 点整条客户卡片 → 打开对方名片 // 点整条客户卡片 → 打开对方名片
// 与交换名片(exchange.vue)同款写法: detail 接收 card_id(卡片编号字符串); // 与交换名片(exchange.vue)同款写法: detail 接收 card_id(卡片编号字符串);
// 本人名下的卡 → 正常详情页; 非本人名下 → detail 兜底跳公开名片页(后端返回的 wap_url) // 本人名下的卡 → 正常详情页; 非本人名下 → detail 兜底跳公开名片页(后端返回的 wap_url)
viewCard(item) { viewCard(payload) {
const item = this.resolveItem(payload);
const cardId = this.cardIdOf(item); const cardId = this.cardIdOf(item);
if (!cardId) { if (!cardId) {
// 后端 lead/lists 目前只下发 card_name(展示名), 没有名片编号 → 无法跳转。 // 后端 lead/lists 目前只下发 card_name(展示名), 没有名片编号 → 无法跳转。
// 这里把该行真实字段打到控制台, 便于确认后端是否用了别的字段名(如 relation_id) // 这里把该行真实字段打到控制台, 便于确认后端是否用了别的字段名(如 relation_id)
console.warn('[card/leads] 该行缺少名片编号(card_id/from_card_id/source_card_id), 无法打开对方名片; 可用字段:', console.warn('[card/leads] 该行缺少名片编号(card_id/from_card_id/source_card_id), 无法打开对方名片; 可用字段:',
item ? Object.keys(item) : item, item); item ? Object.keys(item) : 'event', item);
uni.showToast({ title: '该客户未关联名片,无法查看', icon: 'none' }); uni.showToast({ title: '该客户未关联名片,无法查看', icon: 'none' });
return; return;
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<view class="card-share"> <view class="card-share">
<!-- #ifdef MP-WEIXIN || MP-HARMONY --> <!-- #ifdef MP-HARMONY -->
<!-- 微信小程序/鸿蒙元服务:web-view 是原生组件,必须用 cover-view 盖在上面(cover-view 不支持 iconfont,用文本) --> <!-- 鸿蒙元服务:web-view 是原生全屏组件,必须用 cover-view 盖在上面(cover-view 不支持 iconfont,用文本) -->
<cover-view class="mp-header"> <cover-view class="mp-header">
<cover-view class="mp-status-bar" :style="{ height: statusBarHeight + 'px' }"></cover-view> <cover-view class="mp-status-bar" :style="{ height: statusBarHeight + 'px' }"></cover-view>
<cover-view class="mp-header-inner"> <cover-view class="mp-header-inner">
@@ -59,7 +59,7 @@ export default {
}; };
}, },
computed: { computed: {
// #ifdef MP-WEIXIN || MP-HARMONY // #ifdef MP-HARMONY
headerTotalHeightPx() { headerTotalHeightPx() {
const rpxToPx = this.windowWidth / 750; const rpxToPx = this.windowWidth / 750;
return this.statusBarHeight + Math.round(88 * rpxToPx); return this.statusBarHeight + Math.round(88 * rpxToPx);
@@ -121,7 +121,7 @@ export default {
font-size: 28rpx; font-size: 28rpx;
} }
/* #ifdef MP-WEIXIN || MP-HARMONY */ /* #ifdef MP-HARMONY */
.mp-header { .mp-header {
position: fixed; position: fixed;
top: 0; top: 0;