feat(card): add leads and share pages with platform-specific handling

- Implemented leads.vue for managing lead statuses and displaying lead information.
- Added share.vue for sharing card links with platform-specific web-view handling for WeChat Mini Programs, HarmonyOS, and H5.
- Introduced tests for cross-platform compatibility, ensuring proper handling of conditional compilation and API usage across WeChat, H5, and HarmonyOS.
- Ensured that sharing functionality correctly encodes and decodes URLs for different platforms.
This commit is contained in:
2026-10-12 14:32:20 +08:00
parent a957679957
commit 9e6dc8665f
8 changed files with 1822 additions and 4 deletions

View File

@@ -3,7 +3,8 @@
<!-- 名片头部 -->
<view class="detail-head" v-if="relation">
<view class="head-avatar">
<text>{{ avatarText(relation.name) }}</text>
<image v-if="relation.avatar" :src="$util.img(relation.avatar)" class="head-img" mode="aspectFill" />
<text v-else>{{ avatarText(relation.name) }}</text>
</view>
<view class="head-info">
<view class="head-name">{{ relation.name || '未命名名片' }}</view>
@@ -11,13 +12,17 @@
{{ [relation.job_title, relation.company].filter(Boolean).join(' · ') }}
</view>
</view>
<view class="head-actions">
<view class="head-btn" @tap="openShare">分享</view>
<view class="head-btn" @tap="goEdit">编辑</view>
</view>
</view>
<!-- 字段列表 -->
<view class="detail-fields" v-if="fieldRows.length > 0">
<view v-for="(row, index) in fieldRows" :key="index" class="field-row">
<view v-for="(row, index) in fieldRows" :key="index" class="field-row" :class="{ 'field-row-multi': row.multiline }">
<view class="field-label">{{ row.label }}</view>
<view class="field-value">{{ row.value }}</view>
<view class="field-value" :class="{ 'field-value-multi': row.multiline }">{{ row.value }}</view>
<view class="field-action" v-if="row.action"
@tap="row.action.type === 'call' ? callPhone(row.value) : copyText2(row.value)">
{{ row.action.text }}
@@ -25,6 +30,38 @@
</view>
</view>
<!-- 分享面板(微信式底部抽屉) -->
<view class="share-mask" v-if="showShare" @tap="closeShare"></view>
<view class="share-sheet" :class="{ 'share-sheet-show': showShare }">
<view class="ss-bar"></view>
<view class="ss-title">分享名片</view>
<view class="ss-sub">客户打开链接即可查看名片并留言留资</view>
<view class="ss-qr-box">
<image v-if="qrUrl" :src="qrUrl" class="ss-qr" mode="widthFix" show-menu-by-longpress="true" />
</view>
<view class="ss-link">{{ shareUrl }}</view>
<view class="ss-btns">
<view class="ss-btn ss-btn-main" @tap="copyLink">复制链接</view>
<!-- #ifdef MP-WEIXIN -->
<view class="ss-btn" @tap="saveQr">保存二维码</view>
<view class="ss-btn" @tap="shareToFriend">发给好友</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<view class="ss-btn" @tap="h5Share">浏览器分享</view>
<!-- #endif -->
<!-- #ifdef MP-HARMONY -->
<!-- 鸿蒙元服务:uni.saveImageToPhotosAlbum/downloadFile 均支持(先下载后保存的模式不变);
无小程序右上角转发菜单,系统级分享用「复制链接」代替;原生 image 无长按菜单 -->
<view class="ss-btn" @tap="saveQr">保存二维码</view>
<!-- #endif -->
</view>
<view class="ss-tip">
<!-- #ifdef MP-WEIXIN -->长按二维码也可保存到相册<!-- #endif -->
<!-- #ifdef H5 -->长按二维码可保存到相册<!-- #endif -->
<!-- #ifdef MP-HARMONY -->点击「保存二维码」存入相册<!-- #endif -->
</view>
</view>
<!-- 加载中 -->
<view class="detail-loading" v-if="isLoading">加载中…</view>
@@ -43,7 +80,10 @@ export default {
source: null,
shareList: [],
isLoading: true,
errorMsg: ''
errorMsg: '',
showShare: false,
shareUrl: '',
qrUrl: ''
};
},
computed: {
@@ -68,6 +108,27 @@ export default {
push('Telegram', r.telegram);
push('Messenger', r.messenger);
// 附加手机号(mobiles_list, 后端 CardRichFields 解码) → 可拨打
(r.mobiles_list || []).forEach((m, i) => {
push('手机' + (i + 2), m, { type: 'call', text: '拨打' });
});
// 社交账号(socials_list: {type,label,value}) → label 作行名, 值可复制
// (完整主页链接与纯账号统一复制 value, 链接可粘贴到浏览器打开)
(r.socials_list || []).forEach(s => {
push(s.label || s.type, s.value, { type: 'copy', text: '复制' });
});
// 自定义字段(custom_list: {label,value})
(r.custom_list || []).forEach(c => {
push(c.label, c.value);
});
// 简介(多行, 放最后)
if (r.bio && String(r.bio).trim() !== '') {
rows.push({ label: '简介', value: String(r.bio), action: null, multiline: true });
}
return rows;
}
},
@@ -75,7 +136,30 @@ export default {
this.cardId = decodeURIComponent(options.card_id || '');
this.loadDetail();
},
onShow() {
// 首次进入由 onLoad 负责加载; 从编辑页返回时(onShow 再次触发)刷新
if (this._loadedOnce) {
this.loadDetail();
}
this._loadedOnce = true;
},
// 小程序端转发: 分享 web-view 中转页(其内部加载 wap H5 名片页),
// 因为 onShareAppMessage 的 path 只能指向小程序页面, 不能直接分享 H5 链接
onShareAppMessage() {
const name = (this.relation && this.relation.name) || '我的名片';
return {
title: name + '的名片',
path: '/pages_tool/card/share?url=' + encodeURIComponent(this.shareUrl || ''),
imageUrl: (this.relation && this.relation.avatar) ? this.$util.img(this.relation.avatar) : ''
};
},
methods: {
goEdit() {
if (!this.cardId) return;
uni.navigateTo({
url: '/pages_tool/card/edit?card_id=' + encodeURIComponent(this.cardId)
});
},
avatarText(name) {
return (name || '?').trim().charAt(0).toUpperCase();
},
@@ -120,6 +204,68 @@ export default {
uni.showToast({ title: '已复制', icon: 'success' });
}
});
},
/* ---------- 分享 ---------- */
siteId() {
return uni.getStorageSync('uniacid') || this.$config.uniacid || 0;
},
openShare() {
if (!this.relation) return;
const sid = this.siteId();
const rid = this.relation.id;
const base = this.$config.baseUrl || '';
// wap 公开名片页(客户视角, 无需登录)
this.shareUrl = base + '/card/wap/index/detail?site_id=' + sid + '&id=' + rid;
// 二维码由后端同源生成(项目内置 extend\QRcode), 与链接一一对应
this.qrUrl = base + '/card/wap/index/qr?site_id=' + sid + '&id=' + rid;
this.showShare = true;
},
closeShare() {
this.showShare = false;
},
copyLink() {
if (!this.shareUrl) return;
uni.setClipboardData({
data: this.shareUrl,
success: () => {
uni.showToast({ title: '链接已复制', icon: 'success' });
}
});
},
saveQr() {
if (!this.qrUrl) return;
uni.downloadFile({
url: this.qrUrl,
success: res => {
if (res.statusCode !== 200 || !res.tempFilePath) {
uni.showToast({ title: '二维码下载失败', icon: 'none' });
return;
}
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => uni.showToast({ title: '已保存到相册', icon: 'success' }),
// 文案不提「长按保存」: 鸿蒙元服务的原生 image 无长按菜单, 只微信/H5 有
fail: () => uni.showToast({ title: '保存失败,可复制链接分享', icon: 'none' })
});
},
fail: () => uni.showToast({ title: '二维码下载失败', icon: 'none' })
});
},
shareToFriend() {
// 小程序右上角菜单转发(onshareappmessage 已配置)或提示用户点击右上角
uni.showToast({ title: '请点击右上角「···」转发给好友', icon: 'none' });
},
h5Share() {
// H5: 优先 navigator.share(移动浏览器), 不支持则复制链接兜底
if (navigator && typeof navigator.share === 'function') {
navigator.share({
title: ((this.relation && this.relation.name) || '我的名片') + '的名片',
url: this.shareUrl
}).catch(() => {});
return;
}
this.copyLink();
}
}
};
@@ -152,9 +298,16 @@ export default {
justify-content: center;
flex-shrink: 0;
margin-right: 28rpx;
overflow: hidden;
.head-img {
width: 100%;
height: 100%;
}
}
.head-info {
flex: 1;
min-width: 0;
.head-name {
@@ -168,6 +321,139 @@ export default {
margin-top: 12rpx;
}
}
.head-actions {
flex-shrink: 0;
margin-left: 20rpx;
display: flex;
align-items: center;
}
.head-btn {
font-size: 24rpx;
color: #ffffff;
border: 1rpx solid rgba(255, 255, 255, 0.8);
border-radius: 28rpx;
padding: 8rpx 24rpx;
& + .head-btn {
margin-left: 12rpx;
}
}
}
/* 分享面板(微信式底部抽屉, 与 wap 留资抽屉同风格) */
.share-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 100;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
}
.share-mask {
opacity: 1;
pointer-events: auto;
}
.share-sheet {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: #ffffff;
border-radius: 24rpx 24rpx 0 0;
padding: 20rpx 32rpx 40rpx;
z-index: 101;
transform: translateY(100%);
transition: transform 0.25s;
&.share-sheet-show {
transform: translateY(0);
}
.ss-bar {
width: 36rpx;
height: 4rpx;
border-radius: 2rpx;
background-color: #e3e7ee;
margin: 0 auto 20rpx;
}
.ss-title {
font-size: 32rpx;
font-weight: 600;
color: #1a1f36;
text-align: center;
}
.ss-sub {
font-size: 24rpx;
color: #9aa4b8;
text-align: center;
margin: 8rpx 0 24rpx;
}
.ss-qr-box {
display: flex;
justify-content: center;
margin-bottom: 20rpx;
.ss-qr {
width: 320rpx;
height: 320rpx;
}
}
.ss-link {
font-size: 22rpx;
color: #9aa4b8;
background-color: #f6f7f9;
border-radius: 8rpx;
padding: 14rpx 20rpx;
margin-bottom: 24rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.ss-btns {
display: flex;
justify-content: space-between;
.ss-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
font-size: 28rpx;
color: #333;
border: 1rpx solid #dcdfe6;
border-radius: 12rpx;
& + .ss-btn {
margin-left: 20rpx;
}
}
.ss-btn-main {
background-color: var(--main-color, #0054a5);
border-color: var(--main-color, #0054a5);
color: #ffffff;
}
}
.ss-tip {
text-align: center;
font-size: 22rpx;
color: #c0c4cc;
margin-top: 20rpx;
}
}
.detail-fields {
@@ -182,6 +468,10 @@ export default {
padding: 28rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
&.field-row-multi {
align-items: flex-start;
}
&:last-child {
border-bottom: none;
}
@@ -200,6 +490,11 @@ export default {
word-break: break-all;
}
.field-value-multi {
line-height: 1.6;
white-space: pre-wrap;
}
.field-action {
margin-left: 20rpx;
flex-shrink: 0;