chore:微信小程序分享正常

This commit is contained in:
2026-10-23 14:01:20 +08:00
parent ecc81b9b31
commit a595a4ae1b
8 changed files with 109 additions and 14 deletions

View File

@@ -199,7 +199,25 @@ export default {
* 全局默认 onShareAppMessage: * 全局默认 onShareAppMessage:
* uni-app 规范:组件自身的钩子优先级 > mixin,因此已显式声明的页面不受影响。 * uni-app 规范:组件自身的钩子优先级 > mixin,因此已显式声明的页面不受影响。
*/ */
onShareAppMessage() { onShareAppMessage(options) {
// 名片「发送名片」按钮(open-type="share")触发: dataset 携带名片信息 → 精准转发该名片
// (覆盖默认 cfg 逻辑; 微信小程序无法在组件内直接定义转发, 只能由页面 onShareAppMessage 提供)
if (
options &&
options.from === 'button' &&
options.target &&
options.target.dataset &&
options.target.dataset.cardId
) {
const d = options.target.dataset;
const name = d.cardName || '我的名片';
const img = (this.$util && this.$util.img ? this.$util.img(d.cardQr) : d.cardQr) || '';
return {
title: name + ' 的电子名片',
path: '/pages_tool/card/detail?card_id=' + d.cardId,
imageUrl: img
};
}
// 命中禁用列表的页面:即使 wx.hideShareMenu 因某些原因没生效, // 命中禁用列表的页面:即使 wx.hideShareMenu 因某些原因没生效,
// 这里直接 return undefined / 也不主动拼 cfg,让微信回退到默认行为 // 这里直接 return undefined / 也不主动拼 cfg,让微信回退到默认行为
// (主防线是 wx.hideShareMenu,这里只是兜底) // (主防线是 wx.hideShareMenu,这里只是兜底)

View File

@@ -169,9 +169,9 @@ export function rowIconFor(field) {
/** /**
* 资料行点击动作: 由服务端下发的 href 派生(tel:/mailto:/url/空) * 资料行点击动作: 由服务端下发的 href 派生(tel:/mailto:/url/空)
* tel: → 拨号 mailto: → 邮箱(H5) / 复制(其它端) * tel: → 拨号 mailto: → 邮箱(H5 调起 / 其它端复制引导)
* http(s) → 复制链接 空 → 无动作(纯展示, 如地址/自定义) * http(s) → web(跳转网站, 经 web-view 承载页) 空 → 无动作(纯展示, 如地址/自定义)
* 返回 null 表示不可点; type 取值与父组件 doAct 的 tel/mail/copy 分支对齐 * 返回 null 表示不可点; type 取值与父组件 doAct 的 tel/mail/web/copy 分支对齐
*/ */
export function rowActOf(row) { export function rowActOf(row) {
const href = String((row && row.href) || '').trim(); const href = String((row && row.href) || '').trim();
@@ -179,6 +179,7 @@ export function rowActOf(row) {
if (!href || !value) return null; if (!href || !value) return null;
if (/^tel:/i.test(href)) return { type: 'tel', value: value }; if (/^tel:/i.test(href)) return { type: 'tel', value: value };
if (/^mailto:/i.test(href)) return { type: 'mail', value: value }; if (/^mailto:/i.test(href)) return { type: 'mail', value: value };
if (/^https?:\/\//i.test(href)) return { type: 'web', value: value };
return { type: 'copy', value: value }; return { type: 'copy', value: value };
} }

View File

@@ -22,7 +22,7 @@
<!-- 横滑布局 --> <!-- 横滑布局 -->
<scroll-view v-else-if="layout === 'scroll' && list.length" class="wall-scroll" scroll-x show-scrollbar="false"> <scroll-view v-else-if="layout === 'scroll' && list.length" class="wall-scroll" scroll-x show-scrollbar="false">
<view v-for="(item, index) in list" :key="'s' + index" class="scroll-item fade-up" :style="scrollItemStyle" hover-class="btn-press" @tap="goDetail(item)"> <view v-for="(item, index) in list" :key="item.card_id || index" class="scroll-item fade-up" :style="scrollItemStyle" hover-class="btn-press" @tap="goDetail(item)">
<view class="item-avatar" :style="avatarStyle(item)"> <view class="item-avatar" :style="avatarStyle(item)">
<image v-if="item.avatar" class="avatar-img" :src="$util.img(item.avatar)" mode="aspectFill" /> <image v-if="item.avatar" class="avatar-img" :src="$util.img(item.avatar)" mode="aspectFill" />
<text v-else class="avatar-initial">{{ initialOf(item.name) }}</text> <text v-else class="avatar-initial">{{ initialOf(item.name) }}</text>
@@ -35,7 +35,7 @@
<!-- 网格/列表布局 --> <!-- 网格/列表布局 -->
<view v-else-if="list.length" :class="['wall-grid', layout === 'list' ? 'is-list' : 'is-grid']"> <view v-else-if="list.length" :class="['wall-grid', layout === 'list' ? 'is-list' : 'is-grid']">
<view v-for="(item, index) in list" :key="'g' + index" class="wall-item fade-up" <view v-for="(item, index) in list" :key="item.card_id || index" class="wall-item fade-up"
:style="[itemStyle, { animationDelay: (index * 60) + 'ms' }]" hover-class="btn-press" @tap="goDetail(item)"> :style="[itemStyle, { animationDelay: (index * 60) + 'ms' }]" hover-class="btn-press" @tap="goDetail(item)">
<!-- 内层卡片: 布局盒(wall-item)只负责 50% 宽+左右 gutter, 视觉在此层 <!-- 内层卡片: 布局盒(wall-item)只负责 50% 宽+左右 gutter, 视觉在此层
(WXSS 不支持 calc(), 不能用 calc(50% - gap/2)) --> (WXSS 不支持 calc(), 不能用 calc(50% - gap/2)) -->

View File

@@ -108,10 +108,20 @@
</view> </view>
</view> </view>
<!-- 发送名片(白色名片外面下方, 与精选名片「发名片」同款布局; 点击弹分享抽屉) --> <!-- 发送名片: 小程序端用原生转发(open-type=share 直接拉起「发送给朋友」面板, 与微信分享一致);
H5 端保持弹自定义分享抽屉(原生分享在 H5 不可用) -->
<!-- #ifdef MP-WEIXIN || MP-HARMONY -->
<button v-if="showDetailBtn" class="mini-detail" :style="detailBtnStyle" hover-class="btn-press"
open-type="share"
:data-card-id="card && card.card_id" :data-card-name="card && card.name" :data-card-qr="card && card.qr_url">
<text class="detail-text">{{ detailBtnText || '发送名片' }}</text>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view v-if="showDetailBtn" class="mini-detail" :style="detailBtnStyle" hover-class="btn-press" @tap.stop="openCardShare"> <view v-if="showDetailBtn" class="mini-detail" :style="detailBtnStyle" hover-class="btn-press" @tap.stop="openCardShare">
<text class="detail-text">{{ detailBtnText || '发送名片' }}</text> <text class="detail-text">{{ detailBtnText || '发送名片' }}</text>
</view> </view>
<!-- #endif -->
<!-- 分享名片抽屉(2026-10-23 参考图定稿: 与精选名片 show 同款版式/文案, <!-- 分享名片抽屉(2026-10-23 参考图定稿: 与精选名片 show 同款版式/文案,
复刻 pages_tool/card/detail.vue 的 share-sheet; 类名 cs- 前缀, scoped 隔离) --> 复刻 pages_tool/card/detail.vue 的 share-sheet; 类名 cs- 前缀, scoped 隔离) -->
@@ -121,7 +131,8 @@
<view class="ss-title">分享名片</view> <view class="ss-title">分享名片</view>
<view class="ss-sub">客户打开链接即可查看名片并留言留资</view> <view class="ss-sub">客户打开链接即可查看名片并留言留资</view>
<view class="ss-qr-box"> <view class="ss-qr-box">
<image v-if="card && card.qr_url" :src="$util.img(card.qr_url)" class="ss-qr" mode="widthFix" show-menu-by-longpress="true" /> <image v-if="card && card.qr_url" :src="$util.img(card.qr_url)" class="ss-qr" mode="widthFix"
show-menu-by-longpress="true" @tap="previewQr" />
</view> </view>
<view class="ss-link">{{ (card && card.wap_url) || '' }}</view> <view class="ss-link">{{ (card && card.wap_url) || '' }}</view>
<view class="ss-btns"> <view class="ss-btns">
@@ -455,6 +466,13 @@ export default {
fail: () => uni.showToast({ title: '二维码下载失败', icon: 'none' }), fail: () => uni.showToast({ title: '二维码下载失败', icon: 'none' }),
}); });
}, },
// 分享抽屉二维码: 点击放大预览(预览页长按必出「识别图中二维码」菜单, 比 image 直接长按更可靠)
previewQr() {
const url = (this.card && this.card.qr_url) || '';
if (!url) return;
const src = this.$util.img(url);
uni.previewImage({ urls: [src], current: src });
},
/** 二维码点击 → 预览名片(进详情页; qrTapPreview 开关控制, 默认开) */ /** 二维码点击 → 预览名片(进详情页; qrTapPreview 开关控制, 默认开) */
onQrTap() { onQrTap() {
if (flagOn(this.c.qrTapPreview, true)) this.$emit('detail', this.card); if (flagOn(this.c.qrTapPreview, true)) this.$emit('detail', this.card);
@@ -779,6 +797,16 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
/* 小程序端为 <button open-type="share">: 去掉原生默认灰底/边框, 背景由 detailBtnStyle 内联主题色给出 */
padding: 0;
border: none;
background-color: transparent;
line-height: normal;
font-size: 26rpx;
&::after {
border: none;
}
.detail-text { .detail-text {
font-size: 26rpx; font-size: 26rpx;

View File

@@ -185,13 +185,30 @@ export default {
success: () => uni.showToast({ title: '微信号已复制', icon: 'none' }), success: () => uni.showToast({ title: '微信号已复制', icon: 'none' }),
}); });
} else if (act.type === 'mail') { } else if (act.type === 'mail') {
// H5/公众号: 直接调起系统邮件客户端(满足「点击邮箱发邮件」)
// #ifdef H5 // #ifdef H5
window.location.href = 'mailto:' + act.value; window.location.href = 'mailto:' + act.value;
// #endif // #endif
// 小程序/App(非 H5): 微信沙箱不支持 mailto 拉起, 复制地址并引导去邮件 App 粘贴
// #ifndef H5 // #ifndef H5
uni.setClipboardData({ uni.setClipboardData({
data: act.value, data: act.value,
success: () => uni.showToast({ title: '邮箱已复制', icon: 'none' }), success: () => uni.showToast({ title: '邮箱已复制,请前往邮件App粘贴发送', icon: 'none' }),
});
// #endif
} else if (act.type === 'web') {
// 网站行点击 → 跳转到该网址
// H5/公众号: 直接在本页打开(微信内置浏览器同)
// #ifdef H5
window.location.href = act.value;
// #endif
// 小程序/App(非 H5): 经 web-view 承载页在微信内打开(需微信后台配置业务域名)
// #ifndef H5
const title = (this.card && this.card.name) || '网站';
uni.navigateTo({
url: '/pages_tool/webview/webview?src=' + encodeURIComponent(act.value) +
'&title=' + encodeURIComponent(title) + '&enable_set_title=1',
fail: () => uni.showToast({ title: '打开网站失败', icon: 'none' }),
}); });
// #endif // #endif
} else { } else {

View File

@@ -104,7 +104,8 @@
<view class="ss-title">分享名片</view> <view class="ss-title">分享名片</view>
<view class="ss-sub">客户打开链接即可查看名片并留言留资</view> <view class="ss-sub">客户打开链接即可查看名片并留言留资</view>
<view class="ss-qr-box"> <view class="ss-qr-box">
<image v-if="card && card.qr_url" :src="$util.img(card.qr_url)" class="ss-qr" mode="widthFix" show-menu-by-longpress="true" /> <image v-if="card && card.qr_url" :src="$util.img(card.qr_url)" class="ss-qr" mode="widthFix"
show-menu-by-longpress="true" @tap="previewQr" />
</view> </view>
<view class="ss-link">{{ (card && card.wap_url) || '' }}</view> <view class="ss-link">{{ (card && card.wap_url) || '' }}</view>
<view class="ss-btns"> <view class="ss-btns">
@@ -399,13 +400,30 @@ export default {
success: () => uni.showToast({ title: '微信号已复制', icon: 'none' }), success: () => uni.showToast({ title: '微信号已复制', icon: 'none' }),
}); });
} else if (act.type === 'mail') { } else if (act.type === 'mail') {
// H5/公众号: 直接调起系统邮件客户端(满足「点击邮箱发邮件」)
// #ifdef H5 // #ifdef H5
window.location.href = 'mailto:' + act.value; window.location.href = 'mailto:' + act.value;
// #endif // #endif
// 小程序/App(非 H5): 微信沙箱不支持 mailto 拉起, 复制地址并引导去邮件 App 粘贴
// #ifndef H5 // #ifndef H5
uni.setClipboardData({ uni.setClipboardData({
data: act.value, data: act.value,
success: () => uni.showToast({ title: '邮箱已复制', icon: 'none' }), success: () => uni.showToast({ title: '邮箱已复制,请前往邮件App粘贴发送', icon: 'none' }),
});
// #endif
} else if (act.type === 'web') {
// 网站行点击 → 跳转到该网址
// H5/公众号: 直接在本页打开(微信内置浏览器同)
// #ifdef H5
window.location.href = act.value;
// #endif
// 小程序/App(非 H5): 经 web-view 承载页在微信内打开(需微信后台配置业务域名)
// #ifndef H5
const title = (this.card && this.card.name) || '网站';
uni.navigateTo({
url: '/pages_tool/webview/webview?src=' + encodeURIComponent(act.value) +
'&title=' + encodeURIComponent(title) + '&enable_set_title=1',
fail: () => uni.showToast({ title: '打开网站失败', icon: 'none' }),
}); });
// #endif // #endif
} else { } else {
@@ -472,6 +490,13 @@ export default {
fail: () => uni.showToast({ title: '二维码下载失败', icon: 'none' }) fail: () => uni.showToast({ title: '二维码下载失败', icon: 'none' })
}); });
}, },
// 分享抽屉二维码: 点击放大预览(预览页长按必出「识别图中二维码」菜单, 比 image 直接长按更可靠)
previewQr() {
const url = (this.card && this.card.qr_url) || '';
if (!url) return;
const src = this.$util.img(url);
uni.previewImage({ urls: [src], current: src });
},
}, },
}; };
</script> </script>

View File

@@ -58,7 +58,8 @@ test('common: rowActOf 由 href 派生点击动作', async () => {
const m = await import('../components-diy/diy-card-common.js'); const m = await import('../components-diy/diy-card-common.js');
assert.deepEqual(m.rowActOf({ field: 'phone', value: '22644212509', href: 'tel:22644212509' }), { type: 'tel', value: '22644212509' }); assert.deepEqual(m.rowActOf({ field: 'phone', value: '22644212509', href: 'tel:22644212509' }), { type: 'tel', value: '22644212509' });
assert.deepEqual(m.rowActOf({ field: 'email', value: 'a@b.com', href: 'mailto:a@b.com' }), { type: 'mail', value: 'a@b.com' }); assert.deepEqual(m.rowActOf({ field: 'email', value: 'a@b.com', href: 'mailto:a@b.com' }), { type: 'mail', value: 'a@b.com' });
assert.deepEqual(m.rowActOf({ field: 'website', value: 'https://x.com', href: 'https://x.com' }), { type: 'copy', value: 'https://x.com' }); // 网站行: http(s) 派生 web(点击经 web-view 承载页跳转, 2026-10-23 用户需求)
assert.deepEqual(m.rowActOf({ field: 'website', value: 'https://x.com', href: 'https://x.com' }), { type: 'web', value: 'https://x.com' });
// 空 href(地址/自定义)或空值 → 不可点 // 空 href(地址/自定义)或空值 → 不可点
assert.equal(m.rowActOf({ field: 'address', value: '上海', href: '' }), null); assert.equal(m.rowActOf({ field: 'address', value: '上海', href: '' }), null);
assert.equal(m.rowActOf({ field: 'phone', value: '', href: 'tel:' }), null); assert.equal(m.rowActOf({ field: 'phone', value: '', href: 'tel:' }), null);

View File

@@ -94,9 +94,14 @@ test('mini: showDetailBtn 默认开(历史配置无此字段时也显示, 开箱
assert.match(miniSrc, /flagOn\(this\.c\.showDetailBtn,\s*true\)/); assert.match(miniSrc, /flagOn\(this\.c\.showDetailBtn,\s*true\)/);
}); });
test('mini: 存在全宽主按钮节点并弹分享抽屉(2026-10-23 参考图定稿, 与精选 show 同款)', () => { test('mini: 发送名片按钮(小程序原生转发 + H5 弹抽屉, 双分支)', () => {
assert.match(miniSrc, /class="mini-detail"/); assert.match(miniSrc, /class="mini-detail"/);
// 主按钮点击 → 弹「分享名片」抽屉(不再 emit detail 跳详情) // 小程序端: <button open-type="share"> 直接拉起「发送给朋友」面板(与微信分享一致)
assert.match(miniSrc, /open-type="share"/);
assert.match(miniSrc, /:data-card-id="card && card\.card_id"/);
assert.match(miniSrc, /:data-card-name="card && card\.name"/);
assert.match(miniSrc, /:data-card-qr="card && card\.qr_url"/);
// H5 端: 仍弹「分享名片」抽屉(原生分享在 H5 不可用)
assert.match(miniSrc, /@tap\.stop="openCardShare"/); assert.match(miniSrc, /@tap\.stop="openCardShare"/);
assert.match(miniSrc, /openCardShare\(\)\s*\{\s*this\.showShare = true;/); assert.match(miniSrc, /openCardShare\(\)\s*\{\s*this\.showShare = true;/);
// 抽屉版式与 show 同款: 标题/副文案/二维码/复制链接 // 抽屉版式与 show 同款: 标题/副文案/二维码/复制链接