Files
lucky_shop/pages_tool/card/detail.vue

532 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>