Files
lucky_shop/components-diy/diy-card-show.vue

973 lines
30 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 data-component-name="diy-card-show" class="diy-card-show">
<!-- 骨架屏: 首屏加载/切换名片 -->
<view v-if="loading" class="show-skeleton" :style="cardShellStyle">
<view class="sk sk-line" style="width:42%"></view>
<view class="sk sk-line" style="width:26%;margin-top:16rpx;"></view>
<view class="sk sk-line" style="width:70%;margin-top:40rpx;"></view>
<view class="sk sk-line" style="width:58%;margin-top:16rpx;"></view>
<view class="sk sk-line" style="width:40%;margin-top:16rpx;"></view>
</view>
<!-- 空态: 未拿到名片 -->
<view v-else-if="!card" class="show-empty" :style="cardShellStyle">
<text class="empty-icon">💳</text>
<text class="empty-tip">商家还未选择名片</text>
</view>
<!-- 名片: 主题色顶栏 + 白卡压边(参考图版式, 自绘不复用 mini 基件) -->
<view v-else class="show-block">
<!-- 包裹层: 顶带与白卡同容器, 顶带 top/bottom 拉伸 → 高度始终自适应白卡 -->
<view class="show-card-wrap">
<view class="show-band" :style="bandStyle"></view>
<view class="show-card" :style="cardShellStyle">
<!-- 封面图作背景(面板可关): 铺满整卡 + 压暗遮罩, 内容层 z-index 抬升防遮挡 -->
<image v-if="showCover" class="card-cover" :src="$util.img(card.cover)" mode="aspectFill" />
<view v-if="showCover" class="card-cover-scrim"></view>
<!-- 主题渐变饰条(面板可关): 名片主题渐变横条, 置于卡片顶部 -->
<view v-if="showGradBar" class="card-grad-bar" :style="gradBarStyle"></view>
<!-- 头部: 头像(面板可关, 关后只留姓名/职位) + 姓名/职位 左, 小二维码"预览名片" 右 -->
<view class="card-head">
<view v-if="showAvatarBlock" class="card-avatar" :style="avatarStyle">
<image v-if="card.avatar" class="avatar-img" :src="$util.img(card.avatar)" mode="aspectFill" />
<text v-else class="avatar-initial">{{ initialOf(card.name) }}</text>
</view>
<view class="head-info" :class="{ 'with-avatar': showAvatarBlock }">
<text class="card-name" :style="nameStyle">{{ card.name || '未命名名片' }}</text>
<text v-if="jobLine" class="card-job" :style="subStyle">{{ jobLine }}</text>
</view>
<!-- 头部右侧档 -->
<view v-if="showQrHead" class="card-qr" :style="qrBlockStyle" hover-class="btn-press" @tap.stop="openCardShare">
<image class="qr-img" :style="qrImgStyle" :src="$util.img(card.qr_url)" mode="aspectFit" />
<text class="qr-tip" :style="subStyle">{{ qrTipText }}</text>
</view>
<!-- 名片 Logo(面板可关): 头部右侧, 与头部档二维码同位自动避让 -->
<image v-if="showLogo" class="card-logo" :src="$util.img(card.logo)" mode="aspectFit" />
</view>
<!-- 资料行(面板「显示资料行」可关; 有字形字段显图标, 无字形退化为文字标签; href 派生点击动作) -->
<view v-if="showRowsBlock && shownRows.length" class="card-rows">
<view v-for="(row, i) in shownRows" :key="i" class="card-row"
:class="{ 'row-tappable': rowTapable(row) }"
:hover-class="rowTapable(row) ? 'btn-press' : 'none'"
@tap="onRowTap(row)">
<text v-if="rowIcon(row)" class="row-icon iconfont" :class="rowIcon(row)" :style="labelStyle"></text>
<text v-else class="row-label" :style="labelStyle">{{ row.label }}</text>
<text class="row-value" :style="valueStyle">{{ row.value }}</text>
</view>
<view v-if="hasMoreRows" class="row-toggle" hover-class="btn-press" @tap.stop="expanded = !expanded">
<text class="toggle-text" :style="themeTextStyle">{{ expanded ? '收起资料' : '展开全部资料(' + hiddenCount + ')' }}</text>
</view>
</view>
<!-- 资料行下方档(默认): 大码居中, 资料行随之下移 -->
<view v-if="showQrRows" class="qr-center" :style="qrBlockStyle" hover-class="btn-press" @tap.stop="openCardShare">
<image class="qr-img-c" :style="qrImgStyle" :src="$util.img(card.qr_url)" mode="aspectFit" />
<text class="qr-tip-c" :style="subStyle">{{ qrTipText }}</text>
</view>
<!-- 右上角浮层档: 白底描边小卡浮在名片右上角, 点击弹分享抽屉 -->
<view v-if="showQrCorner" class="card-qr-corner" :style="qrBlockStyle" hover-class="btn-press" @tap.stop="openCardShare">
<image class="qr-img-c" :style="qrImgStyle" :src="$util.img(card.qr_url)" mode="aspectFit" />
<text class="qr-tip-corner" :style="subStyle">{{ qrTipText }}</text>
</view>
<!-- 快捷操作(面板「显示快捷操作」可关; 服务端按资料派生;
2026-10-23 参考图定稿: 统一「发邮件」样式 白底+主题色描边) -->
<view v-if="showActsBlock && acts.length" class="card-acts">
<view v-for="(act, i) in acts" :key="i" class="act-chip"
:style="actChipStyle"
hover-class="btn-press" @tap.stop="doAct(act)">
<text class="act-text" :style="actChipTextStyle">{{ act.label }}</text>
</view>
</view>
</view>
</view>
<!-- 主按钮上方档: 大码居中卡于白卡与发名片按钮之间 -->
<view v-if="showQrBottom" class="qr-center qr-center-bottom" :style="qrBlockStyle" hover-class="btn-press" @tap.stop="openCardShare">
<image class="qr-img-c" :style="qrImgStyle" :src="$util.img(card.qr_url)" mode="aspectFit" />
<text class="qr-tip-c" :style="subStyle">{{ qrTipText }}</text>
</view>
<!-- 发名片: 卡片外下方胶囊(详情页 send-bar 同款观感, 主题渐变色; 面板「发送名片按钮」可关; 点击弹分享抽屉) -->
<view v-if="showSendBtn" class="show-send" :style="sendBtnStyle" hover-class="btn-press" @tap="openCardShare">
<text class="show-send-text">{{ sendBtnText }}</text>
</view>
</view>
<!-- 分享名片抽屉: 点二维码弹出(pages_tool/card/detail.vue 同款版式/文案) -->
<view class="cs-mask" v-if="showShare" @tap="closeShare"></view>
<view class="cs-sheet" :class="{ 'cs-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="card && card.qr_url" :src="$util.img(card.qr_url)" class="ss-qr" mode="widthFix"
show-menu-by-longpress="true" @tap="previewQr" />
</view>
<view class="ss-link">{{ (card && card.wap_url) || '' }}</view>
<view class="ss-btns">
<view class="ss-btn ss-btn-main" :style="shareBtnStyle" @tap="copyCardLink">复制链接</view>
<!-- #ifdef H5 -->
<view class="ss-btn" @tap="h5CardShare">浏览器分享</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN || MP-HARMONY -->
<view class="ss-btn" @tap="saveCardQr">保存二维码</view>
<!-- #endif -->
</view>
<view class="ss-tip">
<!-- #ifdef MP-WEIXIN -->长按二维码也可保存到相册<!-- #endif -->
<!-- #ifdef H5 -->长按二维码可保存到相册<!-- #endif -->
<!-- #ifdef MP-HARMONY -->点击「保存二维码」存入相册<!-- #endif -->
</view>
</view>
</view>
</template>
<script>
// 数字名片 · 精选名片(装修组件)
// 2026-10-21 换思路: 不再经 miniConfig 复用 diy-card-mini 通用布局, 改为本组件
// 自绘参考图版式 —— 主题色顶栏衬底 + 白卡压边 + 右上小二维码(预览名片) +
// 图标资料行 + 卡外「发名片」胶囊。面板仅颜色/底色/圆角/阴影/行数继续生效。
import {
CardTokens, px2rpx, num, flagOn, initialOf, avatarBg, themeGradient, darkenColor, visibleRows, rowIconFor, rowActOf,
resolveQrSize, normalizeQrPosition, qrTransform, normalizeQrStyle,
} from './diy-card-common.js';
export default {
name: 'diy-card-show',
props: {
value: { type: Object }
},
data() {
return {
card: null,
loading: false,
expanded: false,
showShare: false,
};
},
computed: {
c() {
return this.value || {};
},
/** 白卡外壳: 面板底色/圆角/阴影可调(内边距固定对齐参考图) */
cardShellStyle() {
const c = this.c;
return this.$util.objToStyle({
// 封面图作背景时白底让位(封面图铺满整卡, mini 基件同口径)
background: this.showCover ? 'transparent' : (c.bgColor || CardTokens.color.bg),
borderRadius: px2rpx(num(c.borderRadius, 16)) + 'rpx',
boxShadow: c.showShadow === false ? 'none' : CardTokens.shadow.card,
});
},
/** 封面图作背景: 面板「名片设计资产 → 封面图作背景」(默认关; 名片未配封面图时不显示) */
showCover() {
return flagOn(this.c.useCover, false) && !!(this.card && this.card.cover);
},
/** 主题渐变饰条: 面板「名片设计资产 → 主题渐变饰条」(默认关) */
showGradBar() {
return flagOn(this.c.showGradient, false);
},
/** 名片 Logo: 面板「名片设计资产 → 名片 Logo」(默认关; 名片未配 Logo 不显示;
* 与头部档二维码同在右上, 自动避让 —— mini 基件同口径) */
showLogo() {
return flagOn(this.c.showLogo, false) && !!(this.card && this.card.logo) && !this.showQrHead;
},
/** 饰条底色: 名片主题渐变(未配渐变副色时 themeGradient 退化为纯主题色) */
gradBarStyle() {
return this.$util.objToStyle({
background: themeGradient(this.card),
});
},
/** 主题色顶栏: 名片主题渐变, 无主题退化默认主色 */
bandStyle() {
return this.$util.objToStyle({ background: themeGradient(this.card) });
},
/** 头像开关: 面板「显示头像」(历史配置无此字段默认显示; 关后头部只留姓名/职位) */
showAvatarBlock() {
return flagOn(this.c.showAvatar, true);
},
/** 资料行总开关: 面板「显示资料行」(历史配置无此字段默认显示; 关后整块隐藏) */
showRowsBlock() {
return flagOn(this.c.showRows, true);
},
/** 二维码开关: 面板「显示二维码」(历史配置无此字段默认显示; 关后头部只留姓名/职位) */
showQrBlock() {
return flagOn(this.c.showQr, true);
},
/** 二维码位置(四档, 与面板下拉/mini 基件同口径): rows 资料行下方 / head 头部右侧 /
* corner 右上角浮层 / bottom 主按钮上方; 未配置回落 rows(面板默认档) */
qrPos() {
return normalizeQrPosition(this.c.qrPosition);
},
/** 位置分档显隐(共用 showQrBlock + qr_url 前置) */
showQrBase() {
return this.showQrBlock && !!(this.card && this.card.qr_url);
},
showQrHead() { return this.showQrBase && this.qrPos === 'head'; },
showQrRows() { return this.showQrBase && this.qrPos === 'rows'; },
showQrCorner() { return this.showQrBase && this.qrPos === 'corner'; },
showQrBottom() { return this.showQrBase && this.qrPos === 'bottom'; },
/** 二维码外观(四档): plain 原图 / card 白卡描边 / theme 主题色衬底 / gradient 渐变圆角。
* 多彩只装饰外围衬底, 码本身保持黑白保可扫(与 mini 基件 qrBlockStyle 同口径)。
* 注意: 必须输出单一字符串样式 —— uni-app 运行时不支持 :style="[字符串, 字符串]"
* 数组绑定(会被静默丢弃, 衬底/微调全部失效), 故 transform 合并在同一对象里 */
qrStyle() {
return normalizeQrStyle(this.c.qrStyle);
},
qrBlockStyle() {
const style = {};
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
if (this.qrStyle === 'card') {
style.background = '#ffffff';
style.border = '2rpx solid #EEF0F3';
style.borderRadius = '12rpx';
style.padding = '10rpx';
} else if (this.qrStyle === 'theme') {
style.background = theme;
style.borderRadius = '12rpx';
style.padding = '10rpx';
} else if (this.qrStyle === 'gradient') {
// 名片仅单主题色(无 gradient_end)时, themeGradient 返回纯色渐变(看不出渐变),
// 故用压暗一档的终止色保证「渐变圆角」始终可见明暗过渡
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
const end = (this.card && this.card.gradient_end) || darkenColor(theme, 0.18);
style.background = 'linear-gradient(135deg, ' + theme + ' 0%, ' + end + ' 100%)';
style.borderRadius = '16rpx';
style.padding = '6rpx';
}
// plain 不进任何分支(无衬底, 即原图)
// 微调位移(qrOffsetX/Y): 与外观合并成同一段内联样式
const t = qrTransform(this.c.qrOffsetX, this.c.qrOffsetY);
if (t) style.transform = t;
return this.$util.objToStyle(style);
},
/** 二维码提示文字: 面板 qrTip, 未配置回落「扫码查看完整名片」 */
qrTipText() {
return this.c.qrTip || '扫码查看完整名片';
},
/** 分享抽屉主按钮: 主题色(禁 CSS 变量, 小程序端内联具体值) */
shareBtnStyle() {
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
return this.$util.objToStyle({
background: theme,
border: '2rpx solid ' + theme,
});
},
/** 快捷操作开关: 面板「显示快捷操作」(历史配置无此字段默认显示; 关后芯片整块隐藏) */
showActsBlock() {
return flagOn(this.c.showQuick, true);
},
/** 快捷操作芯片: 服务端按名片实际资料派生(首个为主动作, 填充样式) */
acts() {
return this.card && Array.isArray(this.card.acts) ? this.card.acts : [];
},
actChipStyle() {
// 快捷芯片统一「发邮件」样式(2026-10-23 参考图定稿): 白底 + 主题色描边;
// 尺寸/圆角由 .act-chip CSS 类控制(避免内联锁死)
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
return this.$util.objToStyle({
background: '#FFFFFF',
border: '2rpx solid ' + theme,
});
},
actChipTextStyle() {
return this.$util.objToStyle({ color: (this.card && this.card.theme_color) || CardTokens.color.primary });
},
avatarStyle() {
return this.$util.objToStyle({ background: avatarBg(this.card) });
},
/** 职位行: 职位优先, 缺失退公司/名片标题(参考图只显一行) */
jobLine() {
const card = this.card || {};
return card.job_title || card.company || card.card_title || '';
},
/** 二维码边长: 面板 qrSize > 0 用自定义(px), 0/空 = 「按位自动」按位置档位取值
* (rows/bottom 180rpx, head/corner 120rpx, 见 common.QR_SIZES)。
* 之前用 num(qrSize, 48) 兜底 —— 面板默认 0 是合法数字不触发默认值,
* 算出 0rpx 宽高, 二维码图被渲染成 0 尺寸「看不见」 */
qrSide() {
return resolveQrSize(this.qrPos, this.c.qrSize);
},
qrImgStyle() {
return this.$util.objToStyle({
width: this.qrSide + 'rpx',
height: this.qrSide + 'rpx',
});
},
nameStyle() {
return this.$util.objToStyle({ color: this.c.nameColor || CardTokens.color.text });
},
subStyle() {
return this.$util.objToStyle({ color: this.c.infoColor || CardTokens.color.textSub });
},
labelStyle() {
return this.$util.objToStyle({ color: this.c.labelColor || CardTokens.color.textLabel });
},
valueStyle() {
return this.$util.objToStyle({ color: this.c.infoColor || CardTokens.color.textSub });
},
themeTextStyle() {
return this.$util.objToStyle({ color: (this.card && this.card.theme_color) || CardTokens.color.primary });
},
/** 发名片按钮开关: 面板「底部按钮 → 发送名片按钮」(历史配置无此字段默认显示; 关后白卡下不留按钮) */
showSendBtn() {
return flagOn(this.c.showDetailBtn, true);
},
/** 发名片按钮文案: 面板「按钮文案」(默认「发送名片」, 与面板提示一致) */
sendBtnText() {
return this.c.detailBtnText || '发送名片';
},
/** 发名片按钮: 名片主题渐变色(无主题退化为默认主色), 详情页 send-bar 同款胶囊 */
sendBtnStyle() {
return this.$util.objToStyle({
background: themeGradient(this.card),
height: '88rpx',
borderRadius: '44rpx',
});
},
maxRows() {
return num(this.c.maxRows, 4);
},
rowsInfo() {
if (!this.card) return { shown: [], hasMore: false };
return visibleRows(this.card.rows, this.expanded ? 0 : this.maxRows);
},
shownRows() {
return this.rowsInfo.shown;
},
hasMoreRows() {
return !this.expanded && this.rowsInfo.hasMore;
},
hiddenCount() {
return this.rowsInfo.rest.length;
},
},
watch: {
'value.cardId'() {
this.fetchData();
},
},
mounted() {
this.fetchData();
},
methods: {
// 模板无法访问模块导入, 需挂到实例(diy-card-mini 同款写法); 头像缺图时显示姓氏首字
initialOf,
fetchData() {
const cardId = String((this.value || {}).cardId || '').trim();
if (cardId === '') {
this.card = null;
return;
}
this.loading = true;
this.$api.sendRequest({
url: '/card/api/diy/card',
data: { card_id: cardId },
success: res => {
this.loading = false;
this.card = (res.code == 0 && res.data && res.data.card) ? res.data.card : null;
},
fail: () => {
this.loading = false;
this.card = null;
},
});
},
/** 资料行图标: 面板「名片信息展示 → 资料行图标」(默认开;
* 关后全部退化为文字标签, 无字形的字段(邮箱/网站/自定义)也退化为文字标签) */
rowIcon(row) {
return flagOn(this.c.showRowIcons, true) ? rowIconFor(row && row.field) : '';
},
/** 资料行是否可点: 服务端 href 派生得出动作 */
rowTapable(row) {
return !!rowActOf(row);
},
onRowTap(row) {
const act = rowActOf(row);
if (act) this.doAct(act);
},
doAct(act) {
if (!act || !act.value) return;
if (act.type === 'tel') {
uni.makePhoneCall({ phoneNumber: act.value, fail: () => {} });
} else if (act.type === 'wx') {
uni.setClipboardData({
data: act.value,
success: () => uni.showToast({ title: '微信号已复制', icon: 'none' }),
});
} else if (act.type === 'mail') {
// H5/公众号: 直接调起系统邮件客户端(满足「点击邮箱发邮件」)
// #ifdef H5
window.location.href = 'mailto:' + act.value;
// #endif
// 小程序/App(非 H5): 微信沙箱不支持 mailto 拉起, 复制地址并引导去邮件 App 粘贴
// #ifndef H5
uni.setClipboardData({
data: act.value,
success: () => uni.showToast({ title: '邮箱已复制,请前往邮件App粘贴发送', icon: 'none' }),
});
// #endif
} else if (act.type === 'web') {
// 网站行点击 → 跳转到该网址
// H5/公众号: 直接在本页打开(微信内置浏览器同)
// #ifdef H5
window.location.href = act.value;
// #endif
// 小程序/App(非 H5): 经 web-view 承载页在微信内打开(需微信后台配置业务域名)
// #ifndef H5
const title = (this.card && this.card.name) || '网站';
uni.navigateTo({
url: '/pages_tool/webview/webview?src=' + encodeURIComponent(act.value) +
'&title=' + encodeURIComponent(title) + '&enable_set_title=1',
fail: () => uni.showToast({ title: '打开网站失败', icon: 'none' }),
});
// #endif
} else {
uni.setClipboardData({
data: act.value,
success: () => uni.showToast({ title: '已复制', icon: 'none' }),
});
}
},
goDetail(card) {
if (!card || !card.card_id) return;
this.$util.redirectTo('/pages_tool/card/detail', { card_id: card.card_id });
},
/* ---------- 分享名片抽屉(点二维码弹出, 版式/文案对齐 pages_tool/card/detail.vue) ---------- */
openCardShare() {
this.showShare = true;
},
closeShare() {
this.showShare = false;
},
copyCardLink() {
const url = (this.card && this.card.wap_url) || '';
if (!url) {
uni.showToast({ title: '名片链接未返回,请稍后重试', icon: 'none' });
return;
}
uni.setClipboardData({
data: url,
success: () => uni.showToast({ title: '链接已复制', icon: 'success' }),
});
},
// H5: 优先 navigator.share(移动浏览器), 不支持则复制链接兜底
h5CardShare() {
// #ifdef H5
const url = (this.card && this.card.wap_url) || '';
if (navigator && typeof navigator.share === 'function' && url) {
navigator.share({
title: ((this.card && this.card.name) || '我的名片') + '的名片',
url: url
}).catch(() => {});
return;
}
// #endif
this.copyCardLink();
},
// 小程序/鸿蒙: 下载二维码后存相册(长按图片也可存, 抽屉底部有提示)
saveCardQr() {
const qrUrl = (this.card && this.card.qr_url) || '';
if (!qrUrl) return;
uni.downloadFile({
url: this.$util.img(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' }),
fail: () => uni.showToast({ title: '保存失败,可复制链接分享', icon: 'none' })
});
},
fail: () => uni.showToast({ title: '二维码下载失败', icon: 'none' })
});
},
// 分享抽屉二维码: 点击放大预览(预览页长按必出「识别图中二维码」菜单, 比 image 直接长按更可靠)
previewQr() {
const url = (this.card && this.card.qr_url) || '';
if (!url) return;
const src = this.$util.img(url);
uni.previewImage({ urls: [src], current: src });
},
},
};
</script>
<style lang="scss" scoped>
.diy-card-show {
width: 100%;
/* 顶栏 + 白卡压边版式 */
.show-block {
position: relative;
width: 100%;
/* 顶带+白卡同容器: 顶带 top/bottom 拉伸, 高度始终自适应白卡
(不管名片行数/二维码档位怎么变, 蓝色侧条弧线永远贴白卡底) */
.show-card-wrap {
position: relative;
/* 卡片上方露蓝沿 40rpx(由顶带 top:-40rpx 上伸形成) */
margin-top: 40rpx;
}
/* 主题色顶栏: 从露沿处拉伸, 底端始终距白卡底 88rpx(用户定稿:
参考图中弧线到白卡底的这段距离, 名片高度变化时保持不变);
顶部直角(2026-10-23 参考图定稿: 顶带两上角不再圆);
底部 28rpx 小圆弧(用户定稿弧度) */
.show-band {
position: absolute;
left: 0;
right: 0;
top: -40rpx;
bottom: 88rpx;
height: auto;
border-radius: 0 0 28rpx 28rpx;
}
.show-card {
position: relative;
z-index: 1;
margin: 0 24rpx;
padding: 40rpx;
box-sizing: border-box;
width: auto;
overflow: hidden;
/* 封面图作背景(面板「封面图作背景」开 + 名片已配封面图才渲染):
铺满整卡 + 半透明压暗遮罩(白字可读), mini 基件同款参数 */
.card-cover {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.card-cover-scrim {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.45);
z-index: 1;
}
/* 内容层压过封面/遮罩(绝对定位的封面默认盖住静态内容) */
.card-head,
.card-rows,
.card-acts,
.qr-center {
position: relative;
z-index: 2;
}
/* 主题渐变饰条: 卡片顶部横条(细高圆角, 底色走 gradBarStyle 内联主题渐变) */
.card-grad-bar {
position: relative;
z-index: 2;
height: 14rpx;
border-radius: 8rpx;
margin-bottom: 28rpx;
}
/* 名片 Logo: 头部右侧小图(自动推到行尾; 头部档二维码显示时不渲染, 自动避让) */
.card-logo {
position: relative;
z-index: 2;
width: 96rpx;
height: 96rpx;
border-radius: 12rpx;
margin-left: auto;
flex-shrink: 0;
}
.card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
/* 头像(面板「显示头像」可关): 圆形, 底色主题色, 缺图显示姓名首字 */
.card-avatar {
flex-shrink: 0;
width: 104rpx;
height: 104rpx;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
.avatar-img {
width: 100%;
height: 100%;
display: block;
}
.avatar-initial {
font-size: 42rpx;
font-weight: 700;
color: #ffffff;
}
}
.head-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
/* 与头像的间距只在头像显示时存在(关闭后姓名列与资料行左缘对齐) */
&.with-avatar {
margin-left: 24rpx;
}
.card-name {
font-size: 38rpx;
font-weight: 700;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-job {
font-size: 26rpx;
margin-top: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
/* 右上小二维码 + 「预览名片」: 点按进完整名片页 */
.card-qr {
flex-shrink: 0;
margin-left: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
.qr-img {
width: 96rpx;
height: 96rpx;
display: block;
}
.qr-tip {
font-size: 22rpx;
margin-top: 8rpx;
}
}
}
.card-rows {
margin-top: 36rpx;
.card-row {
display: flex;
align-items: center;
line-height: 2.1;
/* 图标/文字标签共槽, 值列左缘对齐(参考图口径) */
.row-icon {
flex-shrink: 0;
width: 88rpx;
text-align: left;
font-size: 28rpx;
}
.row-label {
flex-shrink: 0;
width: 88rpx;
font-size: 24rpx;
}
.row-value {
flex: 1;
font-size: 26rpx;
word-break: break-all;
}
&.row-tappable {
cursor: pointer;
}
}
.row-toggle {
margin-top: 8rpx;
display: flex;
justify-content: flex-end;
.toggle-text {
font-size: 22rpx;
font-weight: 500;
}
}
}
/* 快捷操作芯片: 高度/圆角对齐参考图(首个填充, 其余描边) */
.card-acts {
margin-top: 32rpx;
display: flex;
flex-wrap: wrap;
.act-chip {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
/* 小胶囊(2026-10-22 参考图定稿): 矮一点/收窄内距, 内容短时不再近似圆形 */
height: 56rpx;
padding: 0 24rpx;
margin: 0 16rpx 16rpx 0;
border-radius: 28rpx;
.act-text {
font-size: 24rpx;
font-weight: 500;
}
}
}
/* 右上角浮层档: 白底描边小卡浮在名片右上角(z-index 压过头部信息) */
.card-qr-corner {
position: absolute;
top: 20rpx;
right: 20rpx;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
padding: 10rpx;
background-color: #ffffff;
border: 2rpx solid #EEF0F3;
border-radius: 12rpx;
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.10);
.qr-tip-corner {
font-size: 20rpx;
margin-top: 4rpx;
white-space: nowrap;
}
}
}
/* 资料行下方 / 主按钮上方档: 大码居中(边长走 qrImgStyle 内联) */
.qr-center {
margin-top: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
.qr-tip-c {
font-size: 22rpx;
margin-top: 8rpx;
}
/* 卡外(主按钮上方): 与上下元素拉开间距 */
&.qr-center-bottom {
margin: 32rpx 24rpx 0;
}
}
/* 发名片: 卡片外下方胶囊(背景色由 sendBtnStyle 内联) */
.show-send {
position: relative;
z-index: 1;
margin: 32rpx 24rpx 0;
display: flex;
align-items: center;
justify-content: center;
.show-send-text {
font-size: 30rpx;
font-weight: 600;
color: #ffffff;
}
}
}
/* 空态 */
.show-empty {
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48rpx 24rpx;
.empty-icon {
font-size: 64rpx;
}
.empty-tip {
font-size: 24rpx;
margin-top: 16rpx;
color: #8a8f99;
text-align: center;
}
}
/* 骨架屏 */
.show-skeleton {
width: 100%;
box-sizing: border-box;
padding: 40rpx;
}
.sk {
background: linear-gradient(90deg, #f2f3f5 25%, #e8eaee 37%, #f2f3f5 63%);
background-size: 400rpx 100%;
animation: sk-shimmer 1.2s linear infinite;
border-radius: 8rpx;
&.sk-line {
height: 28rpx;
}
}
}
/* 按压反馈 */
.btn-press {
opacity: 0.72;
transform: scale(0.98);
}
/* 分享名片抽屉(复刻 pages_tool/card/detail.vue 的 share-mask/share-sheet, 类名加 cs- 前缀防撞) */
.cs-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 100;
}
.cs-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;
&.cs-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: 0;
padding: 0;
background-color: transparent;
overflow: visible;
&::after {
border: none;
}
& + .ss-btn {
margin-left: 20rpx;
}
}
// 主按钮颜色走 shareBtnStyle 内联(主题色), 此处只兜底默认蓝
.ss-btn-main {
background-color: #119dff;
border-color: #119dff;
color: #ffffff;
}
}
.ss-tip {
text-align: center;
font-size: 22rpx;
color: #c0c4cc;
margin-top: 20rpx;
}
}
@keyframes sk-shimmer {
0% {
background-position: -200rpx 0;
}
100% {
background-position: 200rpx 0;
}
}
</style>