1427 lines
47 KiB
Vue
1427 lines
47 KiB
Vue
<template>
|
||
<!-- 根节点: designVars 注入 CSS 变量(H5 生效); 但微信小程序会丢弃内联 style 里的
|
||
自定义属性, 故字体等必须再单独内联具体值(见各元素 :style 兜底)。 -->
|
||
<view class="card-detail" :style="[designVars, { fontFamily: ds.fontFamily }]" :class="{ 'qs-square': ds.quickStyle === 'square' }">
|
||
<view class="page-layer">
|
||
<!-- 名片区: 蓝色背景 + 白卡。蓝色自导航栏下沿一路向下延伸到卡片内部后渐隐 -->
|
||
<view class="card-wrap" v-if="relation">
|
||
<!-- 蓝色背景层: 下沿锚定在卡片底部上方, 故卡片高度变化时蓝色下沿跟着走 -->
|
||
<view class="card-wrap-bg" :style="cardWrapStyle"></view>
|
||
|
||
<!-- 名片卡: 对齐设计稿 —— 姓名/职位/公司与全部联系方式都收在同一张白卡内, 不再拆成上下两块 -->
|
||
<!-- 背景色/圆角内联具体值: 小程序端 var(--card-bg/--card-radius) 失效, 不内联会回退默认白/20rpx -->
|
||
<view class="detail-card" :style="{ backgroundColor: ds.bgColor, borderRadius: ds.cardRadius + 'px' }"
|
||
:class="['layout-' + ds.layout, ds.cover && ds.layout !== 'classic' ? 'has-cover' : '']">
|
||
<!-- 封面(hero/compact): 有 cover 时全幅主视觉, 身份信息压在其下沿; classic 不展示封面 -->
|
||
<view v-if="ds.cover && ds.layout !== 'classic'" class="dc-cover"
|
||
:style="{ borderTopLeftRadius: ds.cardRadius + 'px', borderTopRightRadius: ds.cardRadius + 'px' }"
|
||
:class="ds.layout === 'compact' ? 'dc-cover-compact' : 'dc-cover-hero'">
|
||
<image class="dc-cover-img" :src="$util.img(ds.cover)" mode="aspectFill"></image>
|
||
</view>
|
||
<!-- 名片二维码入口: 名片卡右上角(2026-10-21 从底部宫格移入; 点击打开名片二维码弹层) -->
|
||
<view class="dc-qr" @tap="openShare">
|
||
<text class="iconfont icon-erweima" :style="{ color: '#000000' }"></text>
|
||
</view>
|
||
<!-- 预览名片: 二维码块下方, 纯文字四个字(2026-10-21 去背景去药丸, 只留「预览名片」;
|
||
点击打开本卡公开页, 后端 wap 模板完整渲染画布, 永远跟后台一致) -->
|
||
<view class="dc-preview" @tap="previewCard" v-if="shareUrl">预览名片</view>
|
||
<!-- 文字色/字号内联: 小程序端 var(--card-text / --card-font-scale) 失效 -->
|
||
<view class="dc-name" :style="{ color: ds.textColor }">{{ relation.name || '未命名名片' }}</view>
|
||
<view class="dc-job" v-if="relation.job_title" :style="{ color: ds.textSoft, fontSize: (28 * ds.fontScaleNum) + 'rpx' }">{{ relation.job_title }}</view>
|
||
<view class="dc-company" v-if="relation.company" :style="{ color: ds.textSoft, fontSize: (26 * ds.fontScaleNum) + 'rpx' }">{{ relation.company }}</view>
|
||
|
||
<view class="dc-rows" v-if="fieldRows.length > 0">
|
||
<view v-for="(row, index) in fieldRows" :key="index" class="field-row"
|
||
:class="{ 'field-row-multi': row.multiline }" @tap="onRowTap(row)">
|
||
<text v-if="row.icon" class="field-icon iconfont" :class="row.icon" :style="{ color: ds.textSoft }"></text>
|
||
<!-- 没有对应图标字形的字段(邮箱/网站/社交/自定义)退化为文字标签, 保持与图标列同宽 -->
|
||
<text v-else class="field-tag" :style="{ color: ds.textSoft, fontSize: (26 * ds.fontScaleNum) + 'rpx' }">{{ row.label }}</text>
|
||
<view class="field-value" :class="{ 'field-value-multi': row.multiline }" :style="{ color: ds.textColor, fontSize: (28 * ds.fontScaleNum) + 'rpx' }">{{ row.value }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 卡下操作条(参考图: 三项等分, 项与项之间用浅灰竖线分隔)
|
||
2026-10-21: 编辑资料/名片样式/切换名片 暂隐藏(showLegacyActions=false), 代码保留可随时恢复;
|
||
预览名片已移入名片卡右侧(.dc-preview), 此处不再重复放置 -->
|
||
<view class="card-actions" v-if="relation && showLegacyActions">
|
||
<view class="ca-item" @tap="goEdit">
|
||
<text class="ca-icon iconfont icon-bianji"></text>
|
||
<text>编辑资料</text>
|
||
</view>
|
||
<view class="ca-divider"></view>
|
||
<view class="ca-item" @tap="onStyleTap" v-if="designEnabled">
|
||
<text class="ca-icon iconfont icon-erweima"></text>
|
||
<text>名片样式</text>
|
||
</view>
|
||
<view class="ca-divider" v-if="designEnabled"></view>
|
||
<view class="ca-item" @tap="goList">
|
||
<text class="ca-icon iconfont icon-shuaxin"></text>
|
||
<text>切换名片</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 发名片: 微信小程序端必须用原生 <button open-type="share"> ——
|
||
微信未开放「主动唤起转发面板」的 API, 普通 view 的 @tap 只能弹提示,
|
||
原生 button 点一下即拉起「发送给朋友」选择器, 内容取本页 onShareAppMessage。
|
||
其它端无等价能力, 退化为打开分享面板(复制链接 / 浏览器分享)。 -->
|
||
<view class="send-bar" v-if="relation">
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<!-- 不加 :plain —— plain 会强制背景透明, 主按钮需要渐变底, 故用 CSS 完整重置原生样式 -->
|
||
<button class="send-btn" open-type="share">发名片</button>
|
||
<!-- #endif -->
|
||
<!-- #ifdef H5 || APP-PLUS || MP-HARMONY -->
|
||
<view class="send-btn" @tap="openShare">发名片</view>
|
||
<!-- #endif -->
|
||
</view>
|
||
|
||
<!-- 功能入口宫格(参考图: 发名片下方四等分图标入口)
|
||
2026-10-21: 潜在客户/我的人脉 暂隐藏(showLegacyActions=false), 代码保留可随时恢复;
|
||
名片二维码入口已移至名片卡右上角(.dc-qr), 宫格不再重复放置 -->
|
||
<view class="entry-grid" v-if="relation && showLegacyActions">
|
||
<view class="eg-item" @tap="goList">
|
||
<text class="eg-icon iconfont icon-list" :style="{ color: ds.themeColor }"></text>
|
||
<text class="eg-text">名片夹</text>
|
||
</view>
|
||
<view class="eg-item" @tap="goLeads" v-if="showLegacyActions">
|
||
<text class="eg-icon iconfont icon-yonghu3" :style="{ color: ds.themeColor }"></text>
|
||
<text class="eg-text">潜在客户</text>
|
||
</view>
|
||
<view class="eg-item" @tap="goContacts" v-if="showLegacyActions">
|
||
<text class="eg-icon iconfont icon-pengyouquan" :style="{ color: ds.themeColor }"></text>
|
||
<text class="eg-text">我的人脉</text>
|
||
</view>
|
||
<!-- 名片二维码入口已移至名片卡右上角(.dc-qr), 宫格不再重复放置 -->
|
||
</view>
|
||
|
||
<!-- 交换名片提醒条(参考图: 宫格下方出现对方要交换的名片) -->
|
||
<!-- 数据来源: exchange/lists 的待处理请求(只含 status=0, 已同意/已忽略不可见), 不做其它兜底 -->
|
||
<!-- 头部文案与忽略/同意按钮按参考图固定渲染, 不改动名片数据 -->
|
||
<view class="exchange-bar" v-if="relation && exchangeItem" @tap="onExchangeBarTap"
|
||
:style="{ borderColor: ds.themeBorder }">
|
||
<view class="eb-head" :style="{ backgroundColor: ds.themeSoft }">
|
||
<!-- 参考图: 蓝色喇叭图标 + 「有 N 人请求与您交换名片」, 数字更大更粗 -->
|
||
<text class="eb-head-icon iconfont icon-gonggao" :style="{ color: ds.themeColor }"></text>
|
||
<view class="eb-head-text">
|
||
<text>有</text>
|
||
<text class="eb-num" :style="{ color: ds.themeColor }">{{ exchangeCount }}</text>
|
||
<text>人请求与您交换名片</text>
|
||
</view>
|
||
<text class="eb-arrow iconfont icon-right" :style="{ color: ds.themeBorder }"></text>
|
||
</view>
|
||
<view class="eb-card" @tap.stop="viewExchangeCard(exchangeItem)">
|
||
<image v-if="exchangeItem.avatar" class="eb-avatar" :src="exchangeItem.avatar" mode="aspectFill"
|
||
:style="{ backgroundColor: ds.themeColor }" />
|
||
<view v-else class="eb-avatar eb-avatar-ph" :style="{ backgroundColor: ds.themeColor }">{{ (exchangeItem.name || '?').charAt(0) }}</view>
|
||
<view class="eb-info">
|
||
<view class="eb-name-row">
|
||
<text class="eb-name">{{ exchangeItem.name }}</text>
|
||
<text class="eb-badge iconfont icon-dui"></text>
|
||
</view>
|
||
<!-- 参考图: 职位、公司分行显示 -->
|
||
<text class="eb-job" v-if="exchangeItem.job_title">{{ exchangeItem.job_title }}</text>
|
||
<text class="eb-company" v-if="exchangeItem.company">{{ exchangeItem.company }}</text>
|
||
<text class="eb-time">{{ exchangeItem.create_time_txt }} 向您发起名片交换</text>
|
||
</view>
|
||
<view class="eb-actions">
|
||
<view class="eb-act-item" @tap.stop="onIgnoreExchange(exchangeItem)">
|
||
<view class="eb-act eb-act-ignore">
|
||
<!-- ✕ 用 CSS 画, 避免字体图标自带圆圈 -->
|
||
<view class="eb-x">
|
||
<view class="eb-x-line"></view>
|
||
<view class="eb-x-line"></view>
|
||
</view>
|
||
</view>
|
||
<text class="eb-act-text">忽略</text>
|
||
</view>
|
||
<view class="eb-act-item" @tap.stop="onAgreeExchange(exchangeItem)">
|
||
<view class="eb-act eb-act-agree">
|
||
<!-- ✓ 用 CSS 画, 避免字体图标自带圆圈 -->
|
||
<view class="eb-check"></view>
|
||
</view>
|
||
<text class="eb-act-text eb-act-text-main">同意</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载中 -->
|
||
<view class="detail-loading" v-if="isLoading">加载中…</view>
|
||
|
||
<!-- 不存在 / 无权查看 -->
|
||
<ns-empty v-if="!isLoading && !relation" :isIndex="false"
|
||
:text="errorMsg || '卡片不存在或已被删除'"></ns-empty>
|
||
</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>
|
||
<!-- 发给好友: 必须用原生 button(open-type=share) 才能直接拉起转发面板;
|
||
纯 @tap 的 view 调不起该面板, 只能弹一句「去点右上角」的提示 -->
|
||
<button class="ss-btn" :plain="true" open-type="share">发给好友</button>
|
||
<!-- #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>
|
||
</template>
|
||
|
||
<script>
|
||
/**
|
||
* 把两个 hex 颜色按比例混合(返回 hex)。t=0 取 a, t=1 取 b。
|
||
* 用于从后端下发的主色/文字色派生浅色调(提示条底色、次级文字色等),
|
||
* 避免在前端硬编码与主题色脱节的灰色。
|
||
*/
|
||
function mixHex(a, b, t) {
|
||
const toRgb = h => {
|
||
h = (h || '').replace('#', '');
|
||
if (h.length === 3) h = h.split('').map(c => c + c).join('');
|
||
const n = parseInt(h, 16);
|
||
if (isNaN(n)) return [0, 0, 0];
|
||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||
};
|
||
const [r1, g1, b1] = toRgb(a);
|
||
const [r2, g2, b2] = toRgb(b);
|
||
const r = Math.round(r1 + (r2 - r1) * t);
|
||
const g = Math.round(g1 + (g2 - g1) * t);
|
||
const bl = Math.round(b1 + (b2 - b1) * t);
|
||
return '#' + [r, g, bl].map(x => x.toString(16).padStart(2, '0')).join('');
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
cardId: '',
|
||
relation: null,
|
||
source: null,
|
||
// 名片设计摘要(后端 /card/api/card/detail 顶层 design, 旧后端无此键时为 null)
|
||
// 仅做有限主题化(色/字/圆角/布局/封面), 不渲染画布块
|
||
design: null,
|
||
shareList: [],
|
||
isLoading: true,
|
||
errorMsg: '',
|
||
showShare: false,
|
||
shareUrl: '',
|
||
qrUrl: '',
|
||
// 交换名片: 待处理请求(真实, exchange/lists, 只含 status=0)
|
||
exchangeList: [],
|
||
exchangeCount: 0,
|
||
// 商家是否开启「员工端样式自选」(GET design/presets 的 enabled); 默认 true(失败开放)
|
||
designEnabled: true,
|
||
// 旧入口显隐开关(2026-10-21 需求: 隐藏「编辑资料/名片样式/切换名片」「潜在客户/我的人脉」,
|
||
// 隐藏非删除——改回 true 即全部恢复; 对应方法 goEdit/onStyleTap/goLeads/goContacts 均保留)
|
||
showLegacyActions: false
|
||
};
|
||
},
|
||
computed: {
|
||
fieldRows() {
|
||
const r = this.relation;
|
||
if (!r) return [];
|
||
|
||
const rows = [];
|
||
// icon: 图标字形见 common/css/iconfont.css; 邮箱/网站无对应 glyph, 留空 → 模板退化为文字标签
|
||
const push = (label, value, action, icon) => {
|
||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||
rows.push({ label, value: String(value), action: action || null, icon: icon || '' });
|
||
}
|
||
};
|
||
|
||
push('手机', r.mobile, { type: 'call', text: '拨打' }, 'icon-dianhua');
|
||
push('电话', r.phone, { type: 'call', text: '拨打' }, 'icon-dianhua');
|
||
push('邮箱', r.email, { type: 'copy', text: '复制' });
|
||
push('微信', r.wechat, { type: 'copy', text: '复制' }, 'icon-weixin');
|
||
push('网站', r.website);
|
||
push('地址', [r.country, r.province, r.city, r.address].filter(Boolean).join(' '), null, 'icon-dizhi');
|
||
push('邮编', r.postal_code);
|
||
// Telegram / Messenger 已于 2026-10-13 软下线为独立标量:
|
||
// 旧值由后端自动合并进 socials_list, 此处不再单独渲染(避免重复行)
|
||
|
||
// 附加手机号(mobiles_list, 后端 CardRichFields 解码) → 可拨打
|
||
(r.mobiles_list || []).forEach((m, i) => {
|
||
push('手机' + (i + 2), m, { type: 'call', text: '拨打' }, 'icon-dianhua');
|
||
});
|
||
|
||
// 社交账号(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;
|
||
},
|
||
/* ============ 名片设计摘要(后端下发, 仅做有限主题化) ============ */
|
||
/**
|
||
* 归一化 design: 字段缺失/非法一律回落文档默认值, 任何异常都不应阻断页面。
|
||
* 后端已对颜色 safeColor 校验、cover 转绝对地址、layout/font_family/font_scale 钳位,
|
||
* 这里再做一层前端兜底(旧后端无 design 键时 all-default)。
|
||
*/
|
||
ds() {
|
||
const d = this.design || {};
|
||
const FONTS = {
|
||
system: '-apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue",Arial,sans-serif',
|
||
sans: '"Source Han Sans SC","Noto Sans SC","PingFang SC",Helvetica,Arial,sans-serif',
|
||
serif: '"Source Han Serif SC","Songti SC",SimSun,Georgia,serif',
|
||
rounded: '"Yuanti SC","YouYuan","Quicksand",-apple-system,sans-serif'
|
||
};
|
||
const isHex = v => /^#[0-9a-fA-F]{3,8}$/.test(v || '');
|
||
const theme = isHex(d.theme_color) ? d.theme_color : '#119dff';
|
||
const text = isHex(d.text_color) ? d.text_color : '#1f2329';
|
||
const bg = isHex(d.bg_color) ? d.bg_color : '#ffffff';
|
||
const fontKey = FONTS[d.font_family] ? d.font_family : 'system';
|
||
const fontScale = Math.min(160, Math.max(60, Number(d.font_scale) || 100));
|
||
const cardRadius = Number(d.card_radius) || 16;
|
||
const layout = ['hero', 'compact', 'classic'].includes(d.layout) ? d.layout : 'classic';
|
||
const headerEnd = typeof d.header_gradient_end === 'string' ? d.header_gradient_end : '';
|
||
const headerAngle = Number(d.header_gradient_angle) || 150;
|
||
return {
|
||
themeColor: theme,
|
||
textColor: text,
|
||
bgColor: bg,
|
||
layout: layout,
|
||
cover: d.cover || '',
|
||
fontFamily: FONTS[fontKey],
|
||
fontScale: fontScale,
|
||
cardRadius: cardRadius,
|
||
headerGradientEnd: headerEnd,
|
||
headerGradientAngle: headerAngle,
|
||
quickStyle: d.quick_style || '',
|
||
templateId: d.template_id || '',
|
||
source: d.source || 'none',
|
||
// 派生(前端按主题色/文字色混合得到, 避免硬编码脱节灰)
|
||
textSoft: mixHex(text, bg, 0.55),
|
||
themeSoft: mixHex(theme, '#ffffff', 0.85),
|
||
themeBorder: mixHex(theme, '#ffffff', 0.55),
|
||
fontScaleNum: fontScale / 100
|
||
};
|
||
},
|
||
/** 根节点注入的 CSS 变量(所有卡片视觉从变量取值) */
|
||
designVars() {
|
||
const s = this.ds;
|
||
return {
|
||
'--card-theme': s.themeColor,
|
||
'--card-theme-end': s.headerGradientEnd || s.themeColor,
|
||
// 主题色浅色调(提示条底色) / 主题色中浅调(细描边、次级图标)
|
||
// 之前只算了 themeSoft 却没注入, 导致提醒条一直落回硬编码浅蓝, 不跟随后台主题色
|
||
'--card-theme-soft': s.themeSoft,
|
||
'--card-theme-border': s.themeBorder,
|
||
'--card-text': s.textColor,
|
||
'--card-text-soft': s.textSoft,
|
||
'--card-bg': s.bgColor,
|
||
'--card-radius': s.cardRadius + 'px',
|
||
'--card-font': s.fontFamily,
|
||
'--card-font-scale': s.fontScaleNum
|
||
};
|
||
},
|
||
/** 头部蓝色背景层: 主题色渐变(header_gradient_end 为空时按文档算法向黑加深 28%) */
|
||
headerBg() {
|
||
const s = this.ds;
|
||
if (s.headerGradientEnd) {
|
||
return 'linear-gradient(' + s.headerGradientAngle + 'deg, ' + s.themeColor + ', ' + s.headerGradientEnd + ')';
|
||
}
|
||
return 'linear-gradient(150deg, ' + s.themeColor + ' 0%, ' + s.themeColor + ' 62%, rgba(0,0,0,0.28) 160%)';
|
||
},
|
||
/** 头部背景层内联样式(覆盖静态 background-color) */
|
||
cardWrapStyle() {
|
||
return { background: this.headerBg };
|
||
},
|
||
// 是否「待处理请求」(决定头部文案与是否显示忽略/同意)
|
||
exchangeIsRequest() {
|
||
return this.exchangeList.length > 0;
|
||
},
|
||
// 提醒条展示对象: 最新的那条待处理请求(交换成功后该条从列表消失, 提醒条自动收起)
|
||
exchangeItem() {
|
||
return this.exchangeList[0] || null;
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
this.cardId = decodeURIComponent(options.card_id || '');
|
||
this.applyPageTitle();
|
||
this.loadDetail();
|
||
this.loadExchange();
|
||
this.loadDesignEnabled();
|
||
},
|
||
// H5 直接刷新/深链进入时, uni-page-head 可能在路由 meta 就绪前就挂载,
|
||
// 此时标题会回落到 document.title(uni-app H5 默认值 "title");
|
||
// onLoad 设得过早会被这次回落覆盖, 故 onReady(已挂载)再设一次兜死。
|
||
onReady() {
|
||
this.applyPageTitle();
|
||
},
|
||
onShow() {
|
||
this.applyPageTitle();
|
||
// 首次进入由 onLoad 负责加载; 从编辑页返回时(onShow 再次触发)刷新
|
||
if (this._loadedOnce) {
|
||
this.loadDetail();
|
||
this.loadExchange();
|
||
this.loadDesignEnabled();
|
||
}
|
||
this._loadedOnce = true;
|
||
},
|
||
// 小程序端转发: 分享 web-view 中转页(其内部加载 wap H5 名片页),
|
||
// 因为 onShareAppMessage 的 path 只能指向小程序页面, 不能直接分享 H5 链接
|
||
onShareAppMessage() {
|
||
// 右上角「···」转发与原生转发按钮都会直接走这里, 不经过 openShare;
|
||
// shareUrl 已由 loadDetail 从后端 data.wap_url 取得(此处仅做兜底校验),
|
||
// 空值会让中转页显示「链接无效」
|
||
this.buildShareInfo();
|
||
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: {
|
||
// 导航栏标题统一出口: 小程序/鸿蒙/App 走原生导航栏;
|
||
// H5 额外同步 document.title, 避免 uni-page-head 标题回落到默认值 "title"。
|
||
applyPageTitle() {
|
||
const title = '名片';
|
||
uni.setNavigationBarTitle({ title });
|
||
// #ifdef H5
|
||
document.title = title;
|
||
// #endif
|
||
},
|
||
goEdit() {
|
||
if (!this.cardId) return;
|
||
uni.navigateTo({
|
||
url: '/pages_tool/card/edit?card_id=' + encodeURIComponent(this.cardId)
|
||
});
|
||
},
|
||
// 切换名片: 回名片夹选另一张
|
||
goList() {
|
||
this.$util.redirectTo('/pages_tool/card/list');
|
||
},
|
||
goLeads() {
|
||
this.$util.redirectTo('/pages_tool/card/leads');
|
||
},
|
||
goContacts() {
|
||
this.$util.redirectTo('/pages_tool/card/contacts');
|
||
},
|
||
/**
|
||
* 资料行整行可点: 电话类拨打, 其余复制
|
||
* (设计稿行内没有独立的「拨打/复制」按钮, 交互收到整行上)
|
||
*/
|
||
onRowTap(row) {
|
||
if (!row || !row.action) return;
|
||
if (row.action.type === 'call') {
|
||
this.callPhone(row.value);
|
||
return;
|
||
}
|
||
this.copyText2(row.value);
|
||
},
|
||
loadDetail(retry) {
|
||
if (!this.cardId) {
|
||
this.isLoading = false;
|
||
this.errorMsg = '缺少名片编号';
|
||
return;
|
||
}
|
||
this.isLoading = true;
|
||
this.$cardApi.cardDetail({
|
||
data: { card_id: this.cardId },
|
||
success: res => {
|
||
const data = res.data || {};
|
||
if (res.code == 0) {
|
||
this.isLoading = false;
|
||
this.relation = data.relation || null;
|
||
this.source = data.source || null;
|
||
// 名片设计摘要: 旧后端无 design 键时为 null, 全页面按默认值渲染(不报错/不阻断)
|
||
this.design = data.design || null;
|
||
this.shareList = data.share_list || [];
|
||
// 公开页/二维码地址由后端统一拼装返回(含 site_id/域名), 前端不自行拼接
|
||
this.shareUrl = data.wap_url || '';
|
||
this.qrUrl = data.qr_url || '';
|
||
} else if (res.code == -1 && !retry) {
|
||
// -1 = 尚未登录: 冷启动进入本页时(含从对方名片页点返回回到本页)
|
||
// store 里的 token 可能还没恢复完成, 与 loadExchange 的瞬时态同源。
|
||
// 若不重试就会走下面的 fallbackToPublicPage → 本页刚显示又被弹去公开页,
|
||
// 表现为「点返回后中间又跳走了」, 故先静默重试一次再决定是否兜底。
|
||
setTimeout(() => this.loadDetail(true), 800);
|
||
} else {
|
||
// 未登录 / 非本人名下 / 卡片不存在: 后端报错信封同样返回 wap_url,
|
||
// 据此匿名落地到公开页(可看、可提交交换请求), 不出现死路
|
||
if (data.wap_url) {
|
||
this.fallbackToPublicPage(data.wap_url);
|
||
} else {
|
||
this.isLoading = false;
|
||
this.errorMsg = res.message || '卡片不存在或已被删除';
|
||
this.relation = null;
|
||
}
|
||
}
|
||
},
|
||
fail: () => {
|
||
this.isLoading = false;
|
||
this.errorMsg = '网络异常,请稍后重试';
|
||
}
|
||
});
|
||
},
|
||
callPhone(mobile) {
|
||
if (!mobile) return;
|
||
uni.makePhoneCall({ phoneNumber: mobile });
|
||
},
|
||
copyText2(text) {
|
||
uni.setClipboardData({
|
||
data: text,
|
||
success: () => {
|
||
uni.showToast({ title: '已复制', icon: 'success' });
|
||
}
|
||
});
|
||
},
|
||
|
||
/* ---------- 分享 / 公开页 ---------- */
|
||
/**
|
||
* 分享链接与二维码由 loadDetail 时后端返回的 data.wap_url / qr_url 提供,
|
||
* 这里只做兜底校验, 不再本地拼接(域名/site_id/路径结构由后端统一控制, 换环境不失效)。
|
||
*/
|
||
buildShareInfo() {
|
||
if (!this.shareUrl || !this.qrUrl) {
|
||
uni.showToast({ title: '分享链接未返回,请稍后重试', icon: 'none' });
|
||
}
|
||
},
|
||
openShare() {
|
||
if (!this.relation) return;
|
||
this.buildShareInfo();
|
||
this.showShare = true;
|
||
},
|
||
/**
|
||
* 跳对方公开名片页(客户视角 H5, 含「企业介绍 / 收下名片」面板)
|
||
* 契约: 公开页地址由后端 card/detail 按对方 card_id 返回, 前端不自行拼接。
|
||
* 故先拉一次对方名片详情拿到 wap_url, 再按平台跳转;
|
||
* card/detail 对非本人/不存在也会返回 wap_url(防探测), 同样可直达。
|
||
*/
|
||
openPublicCard(cardId) {
|
||
if (!cardId) return;
|
||
this.$cardApi.cardDetail({
|
||
data: { card_id: cardId },
|
||
success: res => {
|
||
const data = res.data || {};
|
||
// 同样带 back_url: 对方名片页左上角返回键据此回到本页(提醒条点对方名片 → 返回详情页)
|
||
this.gotoPublic(this.withBackUrl(data.wap_url || ''));
|
||
},
|
||
fail: () => {
|
||
uni.showToast({ title: '打开名片失败,请稍后重试', icon: 'none' });
|
||
}
|
||
});
|
||
},
|
||
// 预览名片: 打开本卡公开页(后端 wap 模板, 完整渲染画布设计, 永远跟后台最新保存一致)。
|
||
// 小/App 经 share.vue 的 web-view 中转; H5 直接 location。附 back_url 便于公开页返回本页。
|
||
previewCard() {
|
||
this.gotoPublic(this.withBackUrl(this.shareUrl));
|
||
},
|
||
/**
|
||
* 按平台打开公开页: H5 直接 location; 小程序/鸿蒙/App 经 share.vue 的 web-view 中转。
|
||
* 拿不到地址(老后端/网络异常)时降级报错, 禁止本地拼 URL。
|
||
*/
|
||
gotoPublic(wapUrl) {
|
||
if (!wapUrl) {
|
||
uni.showToast({ title: '名片链接未返回', icon: 'none' });
|
||
return;
|
||
}
|
||
// #ifdef H5
|
||
window.location.href = wapUrl;
|
||
// #endif
|
||
// #ifndef H5
|
||
uni.navigateTo({
|
||
url: '/pages_tool/card/share?url=' + encodeURIComponent(wapUrl)
|
||
});
|
||
// #endif
|
||
},
|
||
/**
|
||
* 契约 §4(2026-10-15): 同意后跳对方公开名片页。
|
||
* wap_url 原样使用(后端已带 ?site_id=..&card_id=..), 前端唯一允许的改动是追加
|
||
* &back_url=encodeURIComponent(我方名片页完整地址); wap_url 为空(对方刚删卡等)
|
||
* 则交给调用方: 人脉已互建, 不跳转只 toast 成功。
|
||
*/
|
||
jumpToOtherPublic(wapUrl) {
|
||
if (!wapUrl) return;
|
||
this.gotoPublic(this.withBackUrl(wapUrl));
|
||
},
|
||
/**
|
||
* 给公开页地址追加 back_url=本页完整地址(契约 §4 前端唯一允许的改动)。
|
||
* 对方名片页(PHP wap 模板)左上角返回键据此跳回本页; 已带 back_url 的原样返回, 不重复追加。
|
||
*/
|
||
withBackUrl(wapUrl) {
|
||
if (!wapUrl) return '';
|
||
if (wapUrl.indexOf('back_url=') >= 0) return wapUrl;
|
||
const back = this.myPageFullUrl();
|
||
if (!back) return wapUrl;
|
||
return wapUrl + (wapUrl.indexOf('?') >= 0 ? '&' : '?') + 'back_url=' + encodeURIComponent(back);
|
||
},
|
||
// 我方名片页完整地址: H5 运行时取 window.location.href(禁止写死域名, 部署/测试环境不同);
|
||
// 非 H5(小程序/App) 无等价 H5 地址, 返回 '' 由 share.vue 中转页原生返回。
|
||
myPageFullUrl() {
|
||
// #ifdef H5
|
||
return window.location.href;
|
||
// #endif
|
||
// #ifndef H5
|
||
return '';
|
||
// #endif
|
||
},
|
||
/**
|
||
* 详情接口非 0(未登录/非本人名下/卡片不存在): 后端报错信封照样返回 wap_url,
|
||
* 据此匿名落地到公开页(可看、可提交交换请求), 不出现死路。
|
||
* 用 redirectTo 替换掉当前详情页, 避免回退又回到打不开的死页。
|
||
*/
|
||
fallbackToPublicPage(wapUrl) {
|
||
if (!wapUrl) {
|
||
this.errorMsg = this.errorMsg || '卡片不存在或已被删除';
|
||
return;
|
||
}
|
||
// #ifdef H5
|
||
window.location.href = wapUrl;
|
||
// #endif
|
||
// #ifndef H5
|
||
uni.redirectTo({
|
||
url: '/pages_tool/card/share?url=' + encodeURIComponent(wapUrl)
|
||
});
|
||
// #endif
|
||
},
|
||
// 查询商家是否开启「员工端样式自选」(控制名片样式入口显隐)
|
||
loadDesignEnabled() {
|
||
this.$cardApi.cardDesignPresets({
|
||
success: res => {
|
||
const d = res.data || {};
|
||
if (res.code == 0) this.designEnabled = d.enabled !== false;
|
||
},
|
||
fail: () => {} // 失败开放(默认 true, 入口保留)
|
||
});
|
||
},
|
||
// 「名片样式」: 进入样式选择页(名片模板 / 分享封面)
|
||
onStyleTap() {
|
||
const url = this.cardId
|
||
? '/pages_tool/card/style?card_id=' + encodeURIComponent(this.cardId)
|
||
: '/pages_tool/card/style';
|
||
uni.navigateTo({ url: url });
|
||
},
|
||
// 「交换名片」提醒条: 点右侧箭头/整条 → 进入交换请求列表页
|
||
goExchangeReq() {
|
||
const url = this.cardId
|
||
? '/pages_tool/card/exchange?card_id=' + encodeURIComponent(this.cardId)
|
||
: '/pages_tool/card/exchange';
|
||
uni.navigateTo({ url: url });
|
||
},
|
||
// 拉取待处理交换请求(只含 status=0), 回填 exchangeList / exchangeCount
|
||
// 已同意/已忽略的请求后端不再返回, 列表为空时提醒条自然不渲染(不兜底其它数据源)
|
||
loadExchange(retry) {
|
||
this.$cardApi.exchangeLists({
|
||
data: { page: 1, page_size: 20 },
|
||
success: res => {
|
||
if (res.code != 0) {
|
||
// -1 = 尚未登录: 首次进入本页时 store 里的 token 可能还没恢复完成, 静默重试一次
|
||
if (res.code == -1 && !retry) {
|
||
setTimeout(() => this.loadExchange(true), 800);
|
||
return;
|
||
}
|
||
// 其它失败: 契约允许直接透出后端 message, 便于定位(-1 是上面的瞬时态, 不打扰用户)
|
||
if (res.code != -1 && res.message) {
|
||
uni.showToast({ title: res.message, icon: 'none' });
|
||
}
|
||
console.warn('[card/detail] exchange/lists 返回失败:', res);
|
||
this.exchangeList = [];
|
||
this.exchangeCount = 0;
|
||
return;
|
||
}
|
||
// 兼容 { list, count, page_count } 与裸数组两种返回体
|
||
const data = res.data || {};
|
||
const list = Array.isArray(data) ? data : (data.list || []);
|
||
// id 降序: 新的在前(后端已是 id desc, 这里兜底防不一致)
|
||
const ordered = list.slice().sort((a, b) => Number(b.id) - Number(a.id));
|
||
this.exchangeList = ordered;
|
||
this.exchangeCount = (Array.isArray(data) ? 0 : data.count) || ordered.length;
|
||
},
|
||
fail: () => {
|
||
this.exchangeList = [];
|
||
this.exchangeCount = 0;
|
||
}
|
||
});
|
||
},
|
||
// 提醒条整条点击 → 交换请求列表页(这里只展示真实待处理请求)
|
||
onExchangeBarTap() {
|
||
this.goExchangeReq();
|
||
},
|
||
// 点对方名片 → 打开对方公开名片页(非本人卡)
|
||
viewExchangeCard(item) {
|
||
if (!item) return;
|
||
this.openPublicCard(item.from_card_id);
|
||
},
|
||
onIgnoreExchange(item) {
|
||
// 提醒条只展示真实待处理请求, item.id 即请求 id(非名片 id)
|
||
if (!item || !item.id) return;
|
||
this.$cardApi.exchangeIgnore({
|
||
data: { id: item.id },
|
||
success: res => {
|
||
if (res.code == 0) {
|
||
uni.showToast({ title: '已忽略', icon: 'none' });
|
||
this.loadExchange();
|
||
} else if (res.message) {
|
||
uni.showToast({ title: res.message, icon: 'none' });
|
||
}
|
||
},
|
||
fail: () => {}
|
||
});
|
||
},
|
||
onAgreeExchange(item) {
|
||
// 提醒条只展示真实待处理请求, item.id 即请求 id(非名片 id)
|
||
if (!item || !item.id) return;
|
||
this.$cardApi.exchangeAgree({
|
||
data: { id: item.id },
|
||
success: res => {
|
||
if (res.code == 0) {
|
||
// receiver_created=0 也是正常成功(同人脉已存在被跳过), 不当失败
|
||
const d = res.data || {};
|
||
const txt = d.receiver_created === 0 ? '已交换(人脉已存在)' : '已交换,人脉已互加';
|
||
uni.showToast({ title: txt, icon: 'none' });
|
||
this.loadExchange();
|
||
// 需求: 点「同意」后对方名片必须出现在我的人脉中。
|
||
// 后端虽声明 agree 双向建人脉, 实测出现「同意成功、人脉里却没有对方」,
|
||
// 故不再用 receiver_created 判断, 一律在同意后校验/补建(幂等, 不会重复):
|
||
// 取对方名片详情 → 按手机号查我的人脉 → 没有才以 source=exchange 新建。
|
||
// 兜底完成后再跳转: H5 跳转会中断进行中的请求, 先跳会导致兜底漏执行。
|
||
this.ensureExchangeContact(item.from_card_id, ensureRes => {
|
||
// 命中同手机号人脉但姓名不同(多为对方改过名片名): 提示实际名称,
|
||
// 避免「交换后对方没出现」的误判(该联系人其实已在人脉里, 只是显示旧名)
|
||
if (ensureRes && ensureRes.existed && ensureRes.existed.name
|
||
&& item.name && ensureRes.existed.name !== item.name) {
|
||
uni.showToast({
|
||
title: '人脉中已有同手机号联系人「' + ensureRes.existed.name + '」',
|
||
icon: 'none',
|
||
duration: 2500
|
||
});
|
||
}
|
||
// 契约 §4(2026-10-15): 同意后用响应里的 wap_url 跳对方公开名片页,
|
||
// 仅追加 &back_url=(encodeURIComponent 我方名片页完整地址); wap_url 为空不跳转
|
||
if (d.wap_url) {
|
||
this.jumpToOtherPublic(d.wap_url);
|
||
}
|
||
});
|
||
} else if (res.message) {
|
||
// 请求已被处理/不存在/对方名片失效: 直接提示后端文案, 不二次弹「没有待处理请求」
|
||
uni.showToast({ title: res.message, icon: 'none' });
|
||
}
|
||
},
|
||
fail: () => {}
|
||
});
|
||
},
|
||
/**
|
||
* 同意交换后校验并兜底建人脉: 保证对方名片出现在我的人脉中。
|
||
* 走 card-api 的 ensureExchangeContact(取对方名片详情 → 按手机号去重 → 没有才 source=exchange 新建), 幂等。
|
||
* 无论成败都执行 done(res) —— 兜底失败不阻塞同意后的跳转(交换本身已成功), 人脉页仍可手动补录。
|
||
*/
|
||
ensureExchangeContact(fromCardId, done) {
|
||
if (!fromCardId) { typeof done === 'function' && done(null); return; }
|
||
this.$cardApi.ensureExchangeContact({
|
||
fromCardId: fromCardId,
|
||
success: res => {
|
||
if (res && res.created) {
|
||
console.log('[card/detail] 兜底新建人脉成功:', fromCardId);
|
||
}
|
||
typeof done === 'function' && done(res);
|
||
},
|
||
fail: err => {
|
||
console.warn('[card/detail] 兜底建人脉失败(不影响交换结果):', err);
|
||
typeof done === 'function' && done(null);
|
||
}
|
||
});
|
||
},
|
||
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' })
|
||
});
|
||
},
|
||
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: #ffffff;
|
||
padding: 0 0 60rpx;
|
||
box-sizing: border-box;
|
||
// 设计摘要: 全局字体键(后端 font_family → CSS 字体栈)
|
||
font-family: var(--card-font, -apple-system, "PingFang SC", "Helvetica Neue", Arial, sans-serif);
|
||
}
|
||
|
||
// 内容层
|
||
.page-layer {
|
||
position: relative;
|
||
}
|
||
|
||
// 名片区: 高度 = 上留白 + 卡片高度, 蓝色背景层锚在其中
|
||
.card-wrap {
|
||
position: relative;
|
||
padding-top: 40rpx;
|
||
}
|
||
|
||
// 蓝色背景层: 纯色(无渐变), 顶部与导航栏 #2aa5ff 无缝衔接, 下沿是一条向下凸的弧线
|
||
// 弧度做法: 底部两角用「横向 240rpx + 纵向 44rpx」的椭圆圆角, 两侧各一条大弧线
|
||
// (中段被白卡完全盖住, 所以漏出来的只有左右两条弧边, 和参考图一致)
|
||
// bottom: 48rpx —— 弧线最低点比卡片下沿上提 48rpx(向上移一点); 想再高/再低改这个值即可
|
||
.card-wrap-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 48rpx;
|
||
z-index: 0;
|
||
background-color: var(--card-theme, #2aa5ff);
|
||
border-bottom-left-radius: 240rpx 44rpx;
|
||
border-bottom-right-radius: 240rpx 44rpx;
|
||
pointer-events: none;
|
||
}
|
||
|
||
// 名片卡: 设计稿式白卡 —— 姓名/职位/公司与全部联系方式同卡
|
||
// position/z-index: 保证白卡压在蓝色背景层之上
|
||
.detail-card {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin: 0 24rpx;
|
||
// 设计摘要: 名片底色 + 圆角(后端 bg_color / card_radius)
|
||
background-color: var(--card-bg, #ffffff);
|
||
border-radius: var(--card-radius, 20rpx);
|
||
padding: 40rpx 36rpx 32rpx;
|
||
box-shadow: 0 6rpx 20rpx rgba(0, 30, 80, 0.06);
|
||
|
||
// 名片二维码入口: 卡片右上角放大块(2026-10-21 从底部宫格移入后按需求放大到右侧整块区域)
|
||
.dc-qr {
|
||
position: absolute;
|
||
top: 24rpx;
|
||
right: 24rpx;
|
||
width: 170rpx;
|
||
height: 170rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 16rpx;
|
||
|
||
.iconfont {
|
||
font-size: 110rpx;
|
||
}
|
||
}
|
||
|
||
// 预览名片: 二维码块下方, 纯文字四个字(2026-10-21 去背景去药丸, 无底色无边框无 padding)
|
||
.dc-preview {
|
||
position: absolute;
|
||
top: 206rpx;
|
||
right: 24rpx;
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
}
|
||
|
||
// 封面(hero/compact): 顶部全幅出血, 身份信息压在其下沿; classic 不展示
|
||
.dc-cover {
|
||
margin: -40rpx -36rpx 24rpx;
|
||
border-top-left-radius: var(--card-radius, 20rpx);
|
||
border-top-right-radius: var(--card-radius, 20rpx);
|
||
overflow: hidden;
|
||
|
||
.dc-cover-img {
|
||
display: block;
|
||
width: 100%;
|
||
}
|
||
}
|
||
.dc-cover-hero .dc-cover-img { height: 200rpx; }
|
||
.dc-cover-compact .dc-cover-img { height: 90rpx; }
|
||
|
||
.dc-name {
|
||
font-size: 44rpx;
|
||
font-weight: 700;
|
||
// 设计摘要: 正文字色(姓名保持固定字号, 不随 font_scale 放大, 避免溢出)
|
||
color: var(--card-text, #1a1a1a);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.dc-job {
|
||
margin-top: 6rpx;
|
||
// 设计摘要: 次级文字 = 文字色向底色混合; 字号随 font_scale 缩放(正文)
|
||
font-size: calc(28rpx * var(--card-font-scale, 1));
|
||
line-height: 1.3;
|
||
color: var(--card-text-soft, #4a4a4a);
|
||
}
|
||
|
||
.dc-company {
|
||
margin-top: 2rpx;
|
||
font-size: calc(26rpx * var(--card-font-scale, 1));
|
||
line-height: 1.3;
|
||
color: var(--card-text-soft, #9aa4b2);
|
||
}
|
||
|
||
.dc-rows {
|
||
// 参考图: 公司名与资料行之间留一大段空白
|
||
margin-top: 56rpx;
|
||
}
|
||
|
||
// 紧凑布局: 间距收紧(封面压扁由 .dc-cover-compact 控制)
|
||
&.layout-compact {
|
||
padding-top: 28rpx;
|
||
|
||
.dc-rows { margin-top: 28rpx; }
|
||
.field-row { padding: 8rpx 0; }
|
||
.dc-name { font-size: 40rpx; }
|
||
}
|
||
|
||
// 资料行: 全局 view{line-height:1.8} 会把每行撑高, 这里显式收紧到参考图行距
|
||
.field-row {
|
||
display: flex;
|
||
align-items: center;
|
||
line-height: 1.2;
|
||
padding: 12rpx 0;
|
||
|
||
&.field-row-multi {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.field-icon {
|
||
width: 40rpx;
|
||
margin-right: 18rpx;
|
||
font-size: 32rpx;
|
||
line-height: 1;
|
||
color: var(--card-text-soft, #8a94a6);
|
||
text-align: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.field-tag {
|
||
min-width: 40rpx;
|
||
margin-right: 18rpx;
|
||
font-size: calc(26rpx * var(--card-font-scale, 1));
|
||
line-height: 1.2;
|
||
color: var(--card-text-soft, #9aa4b2);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.field-value {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: calc(28rpx * var(--card-font-scale, 1));
|
||
line-height: 1.25;
|
||
color: var(--card-text, #333333);
|
||
word-break: break-all;
|
||
}
|
||
|
||
.field-value-multi {
|
||
line-height: 1.4;
|
||
white-space: pre-wrap;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 卡下操作条: 多项等分, 无白底无边框, 项与项之间浅灰竖线分隔(对齐参考图)
|
||
.card-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
margin: 28rpx 24rpx 0;
|
||
|
||
.ca-item {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 26rpx;
|
||
line-height: 1.2;
|
||
color: #333333;
|
||
|
||
.ca-icon {
|
||
margin-right: 8rpx;
|
||
font-size: 30rpx;
|
||
color: #8a94a6;
|
||
}
|
||
}
|
||
|
||
// 参考图: 项与项之间一根浅灰竖线(约半个字高)
|
||
.ca-divider {
|
||
width: 2rpx;
|
||
height: 28rpx;
|
||
flex-shrink: 0;
|
||
background-color: #e5e7eb;
|
||
}
|
||
}
|
||
|
||
// 发名片下方的功能入口宫格: 五等分, 图标在上、文字在下
|
||
.entry-grid {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin: 40rpx 24rpx 0;
|
||
|
||
.eg-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.eg-icon {
|
||
font-size: 52rpx;
|
||
line-height: 1;
|
||
color: var(--card-theme, #2aa5ff);
|
||
}
|
||
|
||
.eg-text {
|
||
margin-top: 14rpx;
|
||
font-size: 22rpx;
|
||
line-height: 1.2;
|
||
color: #7b8794;
|
||
}
|
||
}
|
||
|
||
// 交换名片请求提醒条(参考图: 宫格下方出现对方要交换的名片)
|
||
// 顶部提醒行为主题色浅底 + 主题色图标, 名片区白底, 整条主题色浅描边
|
||
// (所有颜色取自 --card-theme 派生, 后台改主题色后整块跟着变)
|
||
.exchange-bar {
|
||
margin: 24rpx 24rpx 0;
|
||
background-color: #ffffff;
|
||
border: 2rpx solid var(--card-theme-border, #cfe3ff);
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
|
||
// 提醒行高度按需求收窄(上下内边距 20rpx → 12rpx)
|
||
.eb-head {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12rpx 24rpx;
|
||
// 设计摘要: 提示条底色 = 主题色向白混合的浅色调
|
||
background-color: var(--card-theme-soft, #eaf3ff);
|
||
}
|
||
|
||
// 参考图: 蓝色喇叭图标(替换原来的小圆点)
|
||
.eb-head-icon {
|
||
margin-right: 10rpx;
|
||
font-size: 26rpx;
|
||
line-height: 1;
|
||
color: var(--card-theme, #2aa5ff);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.eb-head-text {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: baseline;
|
||
font-size: 24rpx;
|
||
line-height: 1.2;
|
||
color: #333333;
|
||
}
|
||
|
||
// 参考图: 「有 1 人请求与您交换名片」里的数字更大更粗
|
||
.eb-num {
|
||
margin: 0 6rpx;
|
||
font-size: 26rpx;
|
||
line-height: 1.2;
|
||
font-weight: 600;
|
||
color: var(--card-theme, #2aa5ff);
|
||
}
|
||
|
||
.eb-arrow {
|
||
margin-left: 12rpx;
|
||
font-size: 24rpx;
|
||
// 右侧箭头用主题色中浅调, 不再固定灰蓝
|
||
color: var(--card-theme-border, #9fb4cc);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.eb-card {
|
||
display: flex;
|
||
align-items: center;
|
||
// 名片区独立白底, 与浅蓝提醒行分块(参考图)
|
||
padding: 24rpx;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.eb-avatar {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
// 头像占位底(无头像时显示姓名首字)跟随主题色
|
||
background-color: var(--card-theme, #2aa5ff);
|
||
}
|
||
|
||
.eb-avatar-ph {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #ffffff;
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.eb-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin-left: 20rpx;
|
||
}
|
||
|
||
.eb-name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.eb-name {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #333333;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.eb-badge {
|
||
margin-left: 8rpx;
|
||
font-size: 24rpx;
|
||
color: var(--card-theme, #2aa5ff);
|
||
}
|
||
|
||
// 参考图: 职位一行、公司一行
|
||
.eb-job {
|
||
display: block;
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
color: #8a94a6;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.eb-company {
|
||
display: block;
|
||
margin-top: 6rpx;
|
||
font-size: 22rpx;
|
||
color: #5a6572;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.eb-time {
|
||
display: block;
|
||
margin-top: 8rpx;
|
||
font-size: 20rpx;
|
||
color: #a9b4c0;
|
||
}
|
||
|
||
.eb-actions {
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
margin-left: 20rpx;
|
||
}
|
||
|
||
// 参考图: 上方图标圆 + 下方文字(忽略/同意)
|
||
.eb-act-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
|
||
&:first-child {
|
||
margin-right: 26rpx;
|
||
}
|
||
}
|
||
|
||
.eb-act {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
|
||
// 忽略: 白底细描边圆 + 灰色 ✕
|
||
&.eb-act-ignore {
|
||
background-color: #ffffff;
|
||
border: 2rpx solid #e3e8ef;
|
||
}
|
||
|
||
// 同意: 蓝底实心圆 + 白色 ✓
|
||
&.eb-act-agree {
|
||
background-color: var(--card-theme, #2aa5ff);
|
||
}
|
||
}
|
||
|
||
// 参考图: ✕ 用 CSS 画(两条交叉短线), 不用字体图标以免自带圆圈
|
||
.eb-x {
|
||
position: relative;
|
||
width: 34rpx;
|
||
height: 34rpx;
|
||
|
||
.eb-x-line {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 3rpx;
|
||
margin-top: -1.5rpx;
|
||
border-radius: 3rpx;
|
||
background-color: #5a6572;
|
||
|
||
&:first-child {
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
&:last-child {
|
||
transform: rotate(-45deg);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 参考图: ✓ 用 CSS 画(左下两边框的方块旋转 -45°)
|
||
.eb-check {
|
||
width: 24rpx;
|
||
height: 12rpx;
|
||
margin-top: -6rpx;
|
||
border-left: 5rpx solid #ffffff;
|
||
border-bottom: 5rpx solid #ffffff;
|
||
transform: rotate(-45deg);
|
||
}
|
||
|
||
.eb-act-text {
|
||
margin-top: 8rpx;
|
||
font-size: 20rpx;
|
||
line-height: 1.2;
|
||
color: #8a94a6;
|
||
}
|
||
|
||
// 参考图: 「同意」文字为蓝色, 「忽略」为灰色
|
||
.eb-act-text-main {
|
||
color: var(--card-theme, #2aa5ff);
|
||
}
|
||
}
|
||
|
||
/* 分享面板(微信式底部抽屉, 与 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;
|
||
// 「发给好友」在小程序端是原生 <button>: 清掉其默认 margin/padding/背景与 ::after 边框
|
||
margin: 0;
|
||
padding: 0;
|
||
background-color: transparent;
|
||
overflow: visible;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
|
||
& + .ss-btn {
|
||
margin-left: 20rpx;
|
||
}
|
||
}
|
||
|
||
.ss-btn-main {
|
||
background-color: var(--card-theme, #119dff);
|
||
border-color: var(--card-theme, #119dff);
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
|
||
.ss-tip {
|
||
text-align: center;
|
||
font-size: 22rpx;
|
||
color: #c0c4cc;
|
||
margin-top: 20rpx;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 「发名片」主按钮: 全宽胶囊, 与名片卡左右对齐(24rpx)
|
||
.send-bar {
|
||
padding: 0 24rpx;
|
||
margin-top: 44rpx;
|
||
|
||
.send-btn {
|
||
display: block;
|
||
width: 100%;
|
||
height: 96rpx;
|
||
line-height: 96rpx;
|
||
text-align: center;
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
// 设计摘要: 主题色渐变胶囊(135° 由主题色 → 头部渐变第二色)
|
||
background: linear-gradient(135deg, var(--card-theme, #2aa5ff) 0%, var(--card-theme-end, #0d7bf0) 100%);
|
||
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.12);
|
||
border-radius: 48rpx;
|
||
// 小程序原生 <button> 自带 margin/padding/背景/::after 边框, 需清掉才能当普通块用
|
||
margin: 0;
|
||
padding: 0;
|
||
border: none;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 快捷操作区: quick_style=square → 方形快捷图标(设计摘要 §3 / 自测矩阵) */
|
||
.qs-square .eb-act {
|
||
border-radius: 20%;
|
||
}
|
||
|
||
.detail-loading {
|
||
text-align: center;
|
||
color: #999999;
|
||
font-size: 26rpx;
|
||
padding: 120rpx 0;
|
||
}
|
||
</style>
|