- onLoad action=mail 时: uni.setClipboardData 复制邮箱(showToast:false) + 自定义 toast「邮箱已复制,去邮件 App 粘贴发送」+ 1.4s 后 navigateBack 返回 wap 名片页 - 复制失败时把邮箱原文带进 toast, 便于用户手动抄写 - 与后端 _mini_bridge.html 双环境分发配套(backend ebf355a)
252 lines
7.1 KiB
Vue
252 lines
7.1 KiB
Vue
<template>
|
|
<view class="save-contact">
|
|
<!-- 名片摘要卡 -->
|
|
<view class="sc-card">
|
|
<view class="sc-avatar">{{ initial }}</view>
|
|
<view class="sc-name">{{ form.name || '未命名名片' }}</view>
|
|
<view class="sc-job" v-if="form.job_title">{{ form.job_title }}</view>
|
|
<view class="sc-company" v-if="form.company">{{ form.company }}</view>
|
|
<view class="sc-mobile" v-if="form.mobile" @tap="callNow">
|
|
<text class="sc-mobile-icon">📞</text>
|
|
<text>{{ form.mobile }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 保存按钮: 调系统 API 直接写入手机通讯录(系统会弹出确认框) -->
|
|
<button class="sc-btn" :disabled="saving" @tap="saveToContacts">
|
|
{{ saving ? '保存中…' : '保存到通讯录' }}
|
|
</button>
|
|
<view class="sc-tip">保存后可在手机通讯录中直接拨号</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
/**
|
|
* 保存名片到手机通讯录(原生页)
|
|
*
|
|
* 背景: 分享出去的名片是后端 wap H5 页, 经 web-view 打开。H5 原来的
|
|
* 「保存到通讯录」是下载 .vcf 文件, 而微信小程序 web-view 禁止下载文件,
|
|
* 会提示「可在浏览器打开此网页来下载文件」。
|
|
*
|
|
* 方案: wap H5 检测到运行在小程序 web-view 内(/miniProgram/i.test(UA))时,
|
|
* 不再触发 .vcf 下载, 改为调 wx.miniProgram.navigateTo 跳到本页,
|
|
* 由本页(小程序原生)调 uni.addPhoneContact 直接写入系统通讯录。
|
|
*
|
|
* 入参(query, 均需 encodeURIComponent):
|
|
* name/mobile/company/job_title/email/website/address/remark/wechat 名片字段
|
|
* action=call 时进入页面直接拨号(wap 页「打电话」按钮也可走本页)
|
|
* action=mail 时复制 email 到剪贴板并返回(web-view 内 mailto: 不生效的兜底)
|
|
*/
|
|
export default {
|
|
data() {
|
|
return {
|
|
form: {
|
|
name: '',
|
|
mobile: '',
|
|
company: '',
|
|
job_title: '',
|
|
email: '',
|
|
website: '',
|
|
address: '',
|
|
remark: '',
|
|
wechat: ''
|
|
},
|
|
saving: false
|
|
};
|
|
},
|
|
computed: {
|
|
initial() {
|
|
const n = this.form.name || this.form.mobile || '?';
|
|
return n.slice(0, 1);
|
|
}
|
|
},
|
|
onLoad(options) {
|
|
const keys = ['name', 'mobile', 'company', 'job_title', 'email', 'website', 'address', 'remark', 'wechat'];
|
|
keys.forEach(k => {
|
|
if (options && options[k] != null) {
|
|
this.form[k] = decodeURIComponent(options[k]);
|
|
}
|
|
});
|
|
// 打电话动作: 直接唤起拨号盘并退出本页
|
|
if (options && options.action === 'call' && this.form.mobile) {
|
|
this.callNow();
|
|
}
|
|
// 发邮件动作: web-view 内 mailto: 不生效, 复制邮箱后返回名片页
|
|
if (options && options.action === 'mail' && this.form.email) {
|
|
this.copyEmail();
|
|
}
|
|
},
|
|
onReady() {
|
|
// 微信真机才生效(模拟器 bug 见 docs/weixin-bug.md), onShow 兜底见下
|
|
uni.setNavigationBarTitle({ title: '保存到通讯录' });
|
|
// 进入页面即自动调起系统"添加联系人"确认框:
|
|
// web-view 内 H5 无法直接调 addPhoneContact, 必须经本原生页中转,
|
|
// 若再让用户手动点一次等于两步操作, 所以这里自动触发, 用户零点击直接看到系统弹窗
|
|
this.saveToContacts();
|
|
},
|
|
onShow() {
|
|
uni.setNavigationBarTitle({ title: '保存到通讯录' });
|
|
},
|
|
methods: {
|
|
callNow() {
|
|
if (!this.form.mobile) return;
|
|
uni.makePhoneCall({
|
|
phoneNumber: this.form.mobile,
|
|
// 用户取消拨号会走 fail, 不提示
|
|
fail: () => {}
|
|
});
|
|
},
|
|
// 发邮件兜底: 小程序无唤起邮件 App 的 API, 复制邮箱 + 提示后返回原名片页
|
|
copyEmail() {
|
|
uni.setClipboardData({
|
|
data: this.form.email,
|
|
showToast: false,
|
|
success: () => {
|
|
uni.showToast({ title: '邮箱已复制,去邮件 App 粘贴发送', icon: 'none', duration: 2000 });
|
|
setTimeout(() => {
|
|
uni.navigateBack({ fail: () => {} });
|
|
}, 1400);
|
|
},
|
|
fail: () => {
|
|
uni.showToast({ title: '复制失败,邮箱:' + this.form.email, icon: 'none', duration: 3000 });
|
|
}
|
|
});
|
|
},
|
|
saveToContacts() {
|
|
if (this.saving) return;
|
|
if (!this.form.name && !this.form.mobile) {
|
|
uni.showToast({ title: '名片资料缺失,无法保存', icon: 'none' });
|
|
return;
|
|
}
|
|
this.saving = true;
|
|
uni.addPhoneContact({
|
|
firstName: this.form.name || this.form.mobile,
|
|
mobilePhoneNumber: this.form.mobile || '',
|
|
organization: this.form.company || '',
|
|
title: this.form.job_title || '',
|
|
email: this.form.email || '',
|
|
url: this.form.website || '',
|
|
weChatNumber: this.form.wechat || '',
|
|
remark: [this.form.address, this.form.remark].filter(Boolean).join(' '),
|
|
success: () => {
|
|
uni.showToast({ title: '已保存到通讯录', icon: 'success' });
|
|
setTimeout(() => {
|
|
uni.navigateBack({ fail: () => {} });
|
|
}, 1200);
|
|
},
|
|
fail: err => {
|
|
// 细分失败原因, 便于真机上快速定位(系统确认框点「取消」不算错误)
|
|
const msg = (err && err.errMsg) || '';
|
|
if (msg.indexOf('cancel') > -1) return;
|
|
if (msg.indexOf('privacy') > -1 || msg.indexOf('declared') > -1) {
|
|
// 小程序后台「用户隐私保护指引」未声明 addPhoneContact(见 docs/CARD_SAVE_CONTACT_BACKEND.md)
|
|
uni.showToast({ title: '小程序隐私协议未声明该能力,请管理员配置', icon: 'none', duration: 3000 });
|
|
} else if (msg.indexOf('auth') > -1 || msg.indexOf('deny') > -1) {
|
|
uni.showToast({ title: '未获通讯录权限,请在系统设置中允许微信访问通讯录', icon: 'none', duration: 3000 });
|
|
} else {
|
|
// 未知错误把 errMsg 原样带出, 截图即可定位
|
|
uni.showToast({ title: '保存失败: ' + (msg || '未知'), icon: 'none', duration: 3000 });
|
|
}
|
|
},
|
|
complete: () => {
|
|
this.saving = false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.save-contact {
|
|
min-height: 100vh;
|
|
box-sizing: border-box;
|
|
padding: 40rpx 32rpx;
|
|
background: linear-gradient(160deg, #29a3ff 0%, #1f8fef 40%, #f5f7fa 40.2%);
|
|
}
|
|
|
|
.sc-card {
|
|
background-color: #ffffff;
|
|
border-radius: 24rpx;
|
|
padding: 48rpx 32rpx 40rpx;
|
|
box-shadow: 0 8rpx 30rpx rgba(31, 143, 239, 0.12);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
|
|
.sc-avatar {
|
|
width: 128rpx;
|
|
height: 128rpx;
|
|
border-radius: 50%;
|
|
background: linear-gradient(150deg, #29a3ff, #1f8fef);
|
|
color: #ffffff;
|
|
font-size: 56rpx;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.sc-name {
|
|
margin-top: 24rpx;
|
|
font-size: 40rpx;
|
|
font-weight: 600;
|
|
color: #222;
|
|
}
|
|
|
|
.sc-job {
|
|
margin-top: 8rpx;
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
}
|
|
|
|
.sc-company {
|
|
margin-top: 4rpx;
|
|
font-size: 26rpx;
|
|
color: #888;
|
|
}
|
|
|
|
.sc-mobile {
|
|
margin-top: 24rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
font-size: 32rpx;
|
|
color: #1f8fef;
|
|
font-weight: 500;
|
|
|
|
.sc-mobile-icon {
|
|
margin-right: 8rpx;
|
|
font-size: 28rpx;
|
|
}
|
|
}
|
|
}
|
|
|
|
.sc-btn {
|
|
margin-top: 64rpx;
|
|
height: 92rpx;
|
|
line-height: 92rpx;
|
|
border-radius: 46rpx;
|
|
color: #ffffff;
|
|
font-size: 32rpx;
|
|
font-weight: 600;
|
|
background: linear-gradient(150deg, #29a3ff, #1f8fef);
|
|
border: none;
|
|
|
|
&::after {
|
|
border: none;
|
|
}
|
|
|
|
&[disabled] {
|
|
opacity: 0.6;
|
|
color: #ffffff;
|
|
}
|
|
}
|
|
|
|
.sc-tip {
|
|
margin-top: 24rpx;
|
|
text-align: center;
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
}
|
|
</style>
|