Files
lucky_shop/pages_tool/card/detail.vue
2026-10-21 14:30:13 +08:00

1428 lines
47 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>
<!-- 根节点: 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;
background: rgba(0, 30, 80, 0.04);
.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>