532 lines
14 KiB
Vue
532 lines
14 KiB
Vue
<template>
|
||
<view class="card-detail">
|
||
<!-- 名片头部 -->
|
||
<view class="detail-head" v-if="relation">
|
||
<view class="head-avatar">
|
||
<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>
|
||
<view class="head-sub" v-if="relation.job_title || relation.company">
|
||
{{ [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" :class="{ 'field-row-multi': row.multiline }">
|
||
<view class="field-label">{{ row.label }}</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 }}
|
||
</view>
|
||
</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>
|
||
|
||
<!-- 不存在 / 无权查看 -->
|
||
<ns-empty v-if="!isLoading && !relation" :isIndex="false"
|
||
:text="errorMsg || '卡片不存在或已被删除'"></ns-empty>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
cardId: '',
|
||
relation: null,
|
||
source: null,
|
||
shareList: [],
|
||
isLoading: true,
|
||
errorMsg: '',
|
||
showShare: false,
|
||
shareUrl: '',
|
||
qrUrl: ''
|
||
};
|
||
},
|
||
computed: {
|
||
fieldRows() {
|
||
const r = this.relation;
|
||
if (!r) return [];
|
||
|
||
const rows = [];
|
||
const push = (label, value, action) => {
|
||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||
rows.push({ label, value: String(value), action: action || null });
|
||
}
|
||
};
|
||
|
||
push('手机', r.mobile, { type: 'call', text: '拨打' });
|
||
push('电话', r.phone, { type: 'call', text: '拨打' });
|
||
push('邮箱', r.email, { type: 'copy', text: '复制' });
|
||
push('微信', r.wechat, { type: 'copy', text: '复制' });
|
||
push('网站', r.website);
|
||
push('地址', [r.country, r.province, r.city, r.address].filter(Boolean).join(' '));
|
||
push('邮编', r.postal_code);
|
||
// Telegram/Messenger 已软下线为社交流, 经 socials_list 统一渲染
|
||
|
||
// 附加手机号(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;
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
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();
|
||
},
|
||
loadDetail() {
|
||
if (!this.cardId) {
|
||
this.isLoading = false;
|
||
this.errorMsg = '缺少名片编号';
|
||
return;
|
||
}
|
||
this.isLoading = true;
|
||
this.$api.sendRequest({
|
||
url: '/card/api/card/detail',
|
||
data: { card_id: this.cardId },
|
||
success: res => {
|
||
this.isLoading = false;
|
||
const data = res.data || {};
|
||
if (res.code == 0) {
|
||
this.relation = data.relation || null;
|
||
this.source = data.source || null;
|
||
this.shareList = data.share_list || [];
|
||
// 公开页/二维码地址由后端统一拼装返回, 前端不自行拼接
|
||
this.shareUrl = data.wap_url || '';
|
||
this.qrUrl = data.qr_url || '';
|
||
} else {
|
||
// 未登录 / 卡片不存在 / 非本人名下(详情接口按归属校验, 同事打开
|
||
// 别人分享的链接会命中) —— 后端在报错 data 里同样返回公开页地址,
|
||
// 据此匿名落到公开名片页(可看、可提交交换/留资), 不出现死路
|
||
this.fallbackToPublicPage(data.wap_url || '');
|
||
}
|
||
},
|
||
fail: () => {
|
||
this.isLoading = false;
|
||
this.errorMsg = '网络异常,请稍后重试';
|
||
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
|
||
}
|
||
});
|
||
},
|
||
fallbackToPublicPage(wapUrl) {
|
||
// 公开页地址完全由后端 detail 接口返回(成功/报错路径都带), 前端零拼装
|
||
if (!wapUrl) {
|
||
this.isLoading = false;
|
||
this.errorMsg = this.errorMsg || '卡片不存在或已被删除';
|
||
return;
|
||
}
|
||
// #ifdef H5
|
||
window.location.href = wapUrl;
|
||
// #endif
|
||
// #ifndef H5
|
||
// 小程序/鸿蒙/APP: 复用 share.vue 中转页(web-view 加载 wap 页)
|
||
uni.redirectTo({
|
||
url: '/pages_tool/card/share?url=' + encodeURIComponent(wapUrl)
|
||
});
|
||
// #endif
|
||
},
|
||
callPhone(mobile) {
|
||
if (!mobile) return;
|
||
uni.makePhoneCall({ phoneNumber: mobile });
|
||
},
|
||
copyText2(text) {
|
||
uni.setClipboardData({
|
||
data: text,
|
||
success: () => {
|
||
uni.showToast({ title: '已复制', icon: 'success' });
|
||
}
|
||
});
|
||
},
|
||
|
||
/* ---------- 分享 ---------- */
|
||
openShare() {
|
||
if (!this.relation) return;
|
||
// 链接/二维码地址已在 loadDetail 时由后端返回(wap_url/qr_url), 这里只做兜底校验
|
||
if (!this.shareUrl || !this.qrUrl) {
|
||
uni.showToast({ title: '分享链接未返回,请稍后重试', icon: 'none' });
|
||
return;
|
||
}
|
||
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();
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.card-detail {
|
||
min-height: 100vh;
|
||
background-color: #f6f7f9;
|
||
padding-bottom: 60rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.detail-head {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 48rpx 32rpx;
|
||
background: linear-gradient(135deg, var(--main-color, #0054a5), var(--main-color-shallow, #3a7bc8));
|
||
color: #ffffff;
|
||
|
||
.head-avatar {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 50%;
|
||
background-color: rgba(255, 255, 255, 0.25);
|
||
font-size: 52rpx;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
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 {
|
||
font-size: 40rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.head-sub {
|
||
font-size: 26rpx;
|
||
opacity: 0.85;
|
||
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 {
|
||
margin: 24rpx;
|
||
background-color: #ffffff;
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
|
||
.field-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 28rpx 24rpx;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
|
||
&.field-row-multi {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.field-label {
|
||
width: 140rpx;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.field-value {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.field-value-multi {
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.field-action {
|
||
margin-left: 20rpx;
|
||
flex-shrink: 0;
|
||
font-size: 24rpx;
|
||
color: var(--main-color, #0054a5);
|
||
border: 1rpx solid var(--main-color, #0054a5);
|
||
border-radius: 28rpx;
|
||
padding: 6rpx 24rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.detail-loading {
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 26rpx;
|
||
padding: 120rpx 0;
|
||
}
|
||
</style>
|