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

1867 lines
62 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-mini" class="card-mini">
<!-- 骨架屏: 首屏加载/切换名片 -->
<view v-if="loading" class="mini-skeleton" :style="wrapStyle">
<view class="sk-row">
<view class="sk sk-avatar"></view>
<view class="sk-col">
<view class="sk sk-line" style="width:55%"></view>
<view class="sk sk-line" style="width:35%;margin-top:16rpx;"></view>
</view>
</view>
<view class="sk sk-line" style="width:100%;margin-top:28rpx;"></view>
<view class="sk sk-line" style="width:72%;margin-top:16rpx;"></view>
<view class="sk sk-btn" style="margin-top:28rpx;"></view>
</view>
<!-- 空态: 未拿到名片 -->
<view v-else-if="!card" class="mini-empty" :style="wrapStyle">
<text class="empty-icon">💳</text>
<text class="empty-tip" :style="emptyTipStyle">{{ emptyTip || '暂无名片数据' }}</text>
<view v-if="emptyActionText" class="empty-btn" :style="emptyBtnStyle" hover-class="btn-press" @tap="onEmptyAction">
<text class="empty-btn-text">{{ emptyActionText }}</text>
</view>
</view>
<!-- 名片卡(精品名片 show 同款版式: 蓝底顶带 + 白卡压边; 角标压在左上) -->
<view v-else class="mini-card-wrap">
<view v-if="bandShow" class="mini-band" :style="bandStyle"></view>
<view class="mini-card fade-up" :style="wrapStyle" :class="{ 'qr-at-corner': showQrCorner }">
<!-- 角标插槽(如 self 组件的「我的名片」) -->
<view class="mini-badge-slot">
<slot name="badge"></slot>
</view>
<!-- 封面图(可选) + 遮罩 -->
<image v-if="showCover" class="mini-cover" :src="$util.img(card.cover)" mode="aspectFill" />
<view v-if="showCover" class="mini-cover-scrim"></view>
<!-- 主题渐变饰条(可选) -->
<view v-if="showGradient" class="mini-gradient" :style="gradientStyle"></view>
<!-- Logo(可选, 右上; corner 档二维码占同一位, 自动避让) -->
<image v-if="showLogo && card.logo" class="mini-logo" :src="$util.img(card.logo)" mode="aspectFit" />
<!-- 二维码·右上角浮层(corner 档: 外观由 qrStyle 决定, 资料行自动让位; 点击预览名片) -->
<view v-if="showQrCorner" class="mini-qr mini-qr-corner" :style="qrBlockStyle" @tap.stop="onQrTap">
<image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" />
<text v-if="showQrTip" class="qr-tip" :style="labelStyle"><text v-if="qrTipEm" class="qr-tip-em" :style="qrTipEmStyle">{{ qrTipEm }}</text>{{ qrTipLine1 }}</text><text v-if="qrTipSub" class="qr-tip-sub" :style="labelStyle">{{ qrTipSub }}</text><text v-if="qrUrlShow && qrUrlText" class="qr-url" :style="qrUrlStyle">{{ qrUrlText }}</text>
</view>
<!-- 头部·主题渐变整块档(theme): 渐变通铺卡边 + 装饰圆, plain 档不渲染 -->
<view v-if="headTheme" class="mini-headtheme" :style="headThemeStyle">
<view class="headtheme-decor headtheme-decor--big"></view>
<view class="headtheme-decor headtheme-decor--small"></view>
<view v-if="badgeShow" class="mini-hero-badge" :style="heroBadgeStyle">
<view class="hero-badge-dot"></view>
<text class="hero-badge-text" :style="heroBadgeTextStyle">{{ badgeText }}</text>
</view>
<view class="mini-head" :style="cornerHeadStyle">
<view v-if="showAvatarBlock" class="mini-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="mini-head-info">
<view class="name-row">
<text class="mini-name" :style="nameStyle">{{ card.name || '未命名名片' }}</text>
<text v-if="isEnterprise" class="mini-badge" :style="badgeStyle">企业</text>
</view>
<text v-if="card.card_title" class="mini-card-title" :style="cardTitleStyle">{{ card.card_title }}</text>
<text v-if="subLine" class="mini-sub" :style="subTextStyle">{{ subLine }}</text>
</view>
<!-- 二维码·头部右侧(head 档: 头像|信息|二维码 三段 flex, 零遮挡) -->
<view v-if="showQrHead" class="mini-qr mini-qr-head" :style="qrBlockStyle">
<image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" />
<text v-if="showQrTip" class="qr-tip" :style="labelStyle"><text v-if="qrTipEm" class="qr-tip-em" :style="qrTipEmStyle">{{ qrTipEm }}</text>{{ qrTipLine1 }}</text><text v-if="qrTipSub" class="qr-tip-sub" :style="labelStyle">{{ qrTipSub }}</text><text v-if="qrUrlShow && qrUrlText" class="qr-url" :style="qrUrlStyle">{{ qrUrlText }}</text>
</view>
</view>
</view>
<!-- 头部·白卡档(plain, 历史默认) -->
<view v-if="!headTheme && badgeShow" class="mini-hero-badge" :style="heroBadgeStyle">
<view class="hero-badge-dot"></view>
<text class="hero-badge-text" :style="heroBadgeTextStyle">{{ badgeText }}</text>
</view>
<view v-if="!headTheme" class="mini-head" :style="plainHeadFinalStyle">
<view v-if="showAvatarBlock" class="mini-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="mini-head-info">
<view class="name-row">
<text class="mini-name" :style="nameStyle">{{ card.name || '未命名名片' }}</text>
<text v-if="isEnterprise" class="mini-badge" :style="badgeStyle">企业</text>
</view>
<text v-if="card.card_title" class="mini-card-title" :style="cardTitleStyle">{{ card.card_title }}</text>
<text v-if="subLine" class="mini-sub" :style="subTextStyle">{{ subLine }}</text>
</view>
<!-- 二维码·头部右侧(head 档: 头像|信息|二维码 三段 flex, 零遮挡; 点击预览名片) -->
<view v-if="showQrHead" class="mini-qr mini-qr-head" :style="qrBlockStyle" @tap.stop="onQrTap">
<image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" />
<text v-if="showQrTip" class="qr-tip" :style="labelStyle"><text v-if="qrTipEm" class="qr-tip-em" :style="qrTipEmStyle">{{ qrTipEm }}</text>{{ qrTipLine1 }}</text><text v-if="qrTipSub" class="qr-tip-sub" :style="labelStyle">{{ qrTipSub }}</text><text v-if="qrUrlShow && qrUrlText" class="qr-url" :style="qrUrlStyle">{{ qrUrlText }}</text>
</view>
</view>
<!-- 快捷操作·资料区上方档(2.18 actsPosition=above): below 时由下方同构块渲染 -->
<view v-if="actsAbove && showActs && acts.length && actsGrid" class="mini-acts-grid" :style="actsBlockFinalStyle">
<view v-for="(act, i) in acts" :key="'ta' + i" class="act-grid-item" :style="{ width: gridItemWidth }" hover-class="btn-press" @tap.stop="onAct(act)">
<view class="act-grid-icon" :style="actGridIconStyle(act)">
<!-- 自定义图标图片(2.18 媒体库, 优先) / SVG 线稿(2.18.1 与 wap detail 同源) / 首字兜底 -->
<image v-if="actImgOf(act)" class="act-grid-img" :src="$util.img(actImgOf(act))" mode="aspectFit" />
<view v-else-if="actLibOf(act)" class="act-grid-svg" :style="actLibStyle(act)"></view>
<view v-else-if="actSvgOf(act)" class="act-grid-svg" :style="actSvgStyle(act)"></view>
<text v-else class="act-grid-initial" :style="actGridGlyphStyle(act)">{{ actInitialOf(act) }}</text>
</view>
<text class="act-grid-label" :style="actGridLabelStyle(act)">{{ act.label }}</text>
</view>
</view>
<view v-if="actsAbove && showActs && acts.length && !actsGrid" class="mini-acts" :style="actsBlockFinalStyle">
<view v-for="(act, i) in acts" :key="'ta' + i" class="act-chip"
:style="i === 0 ? actPrimaryStyle : actGhostStyle" hover-class="btn-press" @tap.stop="onAct(act)">
<text class="act-text" :style="i === 0 ? actPrimaryTextStyle : actGhostTextStyle">{{ act.label }}</text>
</view>
</view>
<!-- 资料行(可折叠; 有字形字段显图标, 无字形退化为文字标签) -->
<view v-if="showRowsBlock && shownRows.length" class="mini-rows" :style="rowsFinalStyle">
<!-- 资料区标题(2.17 可选): 主题色竖条 + 标题 -->
<view v-if="rowsTitleShow" class="mini-rows-title">
<view class="rows-title-bar" :style="rowsTitleBarStyle"></view>
<text class="rows-title-text" :style="rowsTitleStyle">{{ rowsTitle }}</text>
</view>
<view v-for="(row, i) in shownRows" :key="i" class="mini-row"
:class="{ 'row-tappable': rowTapable(row), 'row-3col': showRowLabels }" :hover-class="rowTapable(row) ? 'btn-press' : 'none'" @tap="onRowTap(row)">
<!-- CSS 描边图标档(2.17.1): 邮箱/网站无 iconfont 字形, 纯 view 绘制 -->
<view v-if="rowCssIcon(row)" class="row-css-wrap">
<view v-if="rowCssIcon(row) === 'mail'" class="css-ico-mail css-ico-mail--row" :style="rowCssBorderStyle(row)">
<view class="css-ico-mail-flap" :style="rowCssBorderStyle(row)"></view>
</view>
<view v-else class="css-ico-web css-ico-web--row" :style="rowCssBorderStyle(row)">
<view class="css-ico-web-h" :style="rowCssFillStyle(row)"></view>
<view class="css-ico-web-v" :style="rowCssBorderStyle(row)"></view>
</view>
</view>
<text v-else-if="rowIcon(row)" class="row-icon iconfont" :class="rowIcon(row)" :style="labelStyle"></text>
<text v-if="showRowLabels || (!rowCssIcon(row) && !rowIcon(row))" class="row-label" :style="rowLabelStyle">{{ row.label }}</text>
<text class="row-value" :style="valueStyle">{{ row.value }}</text>
<text v-if="copyBtnVisible(row)" class="row-copy" :style="copyBtnStyle" @tap.stop="onCopyRow(row)">复制</text>
</view>
<view v-if="hasMoreRows" class="row-toggle" hover-class="btn-press" @tap="expanded = !expanded">
<text class="toggle-text" :style="toggleStyle">{{ expanded ? '收起资料' : '展开全部资料(' + hiddenCount + ')' }}</text>
</view>
</view>
<!-- 二维码·资料行下方(rows 档, 默认; 点击预览名片); mini-qr-center: 块级居中 -->
<view v-if="showQrRows" class="mini-qr mini-qr-center" :style="qrBlockFinalStyle" @tap.stop="onQrTap">
<image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" />
<text v-if="showQrTip" class="qr-tip" :style="labelStyle"><text v-if="qrTipEm" class="qr-tip-em" :style="qrTipEmStyle">{{ qrTipEm }}</text>{{ qrTipLine1 }}</text><text v-if="qrTipSub" class="qr-tip-sub" :style="labelStyle">{{ qrTipSub }}</text><text v-if="qrUrlShow && qrUrlText" class="qr-url" :style="qrUrlStyle">{{ qrUrlText }}</text>
</view>
<!-- 快捷操作·图标宫格档(grid): 彩色浅底图标 + 下方文字 -->
<view v-if="!actsAbove && showActs && acts.length && actsGrid" class="mini-acts-grid" :style="actsBlockFinalStyle">
<view v-for="(act, i) in acts" :key="i" class="act-grid-item" :style="{ width: gridItemWidth }" hover-class="btn-press" @tap.stop="onAct(act)">
<view class="act-grid-icon" :style="actGridIconStyle(act)">
<!-- 自定义图标图片(2.18 媒体库, 优先) / SVG 线稿(2.18.1 与 wap detail 同源) / 首字兜底 -->
<image v-if="actImgOf(act)" class="act-grid-img" :src="$util.img(actImgOf(act))" mode="aspectFit" />
<view v-else-if="actLibOf(act)" class="act-grid-svg" :style="actLibStyle(act)"></view>
<view v-else-if="actSvgOf(act)" class="act-grid-svg" :style="actSvgStyle(act)"></view>
<text v-else class="act-grid-initial" :style="actGridGlyphStyle(act)">{{ actInitialOf(act) }}</text>
</view>
<text class="act-grid-label" :style="actGridLabelStyle(act)">{{ act.label }}</text>
</view>
</view>
<!-- 快捷操作·胶囊芯片档(chips, 历史默认; 最小触达 72rpx; 首个为主动作) -->
<view v-if="!actsAbove && showActs && acts.length && !actsGrid" class="mini-acts" :style="actsBlockFinalStyle">
<view v-for="(act, i) in acts" :key="i" class="act-chip"
:style="actStyle" hover-class="btn-press" @tap.stop="onAct(act)">
<text class="act-text" :style="actTextStyle">{{ act.label }}</text>
</view>
</view>
<!-- 二维码·主按钮上方(bottom 档; 点击预览名片) -->
<view v-if="showQrBottom" class="mini-qr mini-qr-center" :style="qrBlockFinalStyle" @tap.stop="onQrTap">
<image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" />
<text v-if="showQrTip" class="qr-tip" :style="labelStyle"><text v-if="qrTipEm" class="qr-tip-em" :style="qrTipEmStyle">{{ qrTipEm }}</text>{{ qrTipLine1 }}</text><text v-if="qrTipSub" class="qr-tip-sub" :style="labelStyle">{{ qrTipSub }}</text><text v-if="qrUrlShow && qrUrlText" class="qr-url" :style="qrUrlStyle">{{ qrUrlText }}</text>
</view>
<!-- 发名片(对齐名片详情页: 主题色大按钮; 点击跳原生转发中转页拉起微信转发面板) -->
<view v-if="showSendBtn" class="mini-send" :style="sendBtnStyle" hover-class="btn-press" @tap.stop="onSend">
<text class="send-text">{{ sendBtnText || '发名片' }}</text>
</view>
</view>
</view>
<!-- 发送名片: 小程序端用原生转发(open-type=share 直接拉起「发送给朋友」面板, 与微信分享一致);
H5 端保持弹自定义分享抽屉(原生分享在 H5 不可用) -->
<!-- #ifdef MP-WEIXIN || MP-HARMONY -->
<button v-if="showDetailBtn" class="mini-detail" :style="detailBtnStyle" hover-class="btn-press"
open-type="share"
:data-card-id="card && card.card_id" :data-card-name="card && card.name" :data-card-qr="card && card.qr_url">
<text class="detail-text">{{ detailBtnText || '发送名片' }}</text>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view v-if="showDetailBtn" class="mini-detail" :style="detailBtnStyle" hover-class="btn-press" @tap.stop="openCardShare">
<text class="detail-text">{{ detailBtnText || '发送名片' }}</text>
</view>
<!-- #endif -->
<!-- 分享名片抽屉(2026-10-23 参考图定稿: 与精选名片 show 同款版式/文案,
复刻 pages_tool/card/detail.vue 的 share-sheet; 类名 cs- 前缀, scoped 隔离) -->
<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>
// 数字名片 · 名片迷你卡基件(show/self 共用, 设计系统单一事实来源)
// 消费名片自身设计资产: cover 封面 / logo / theme_color / 渐变末色与角度 / card_title
import {
CardTokens, px2rpx, flagOn, num, initialOf, avatarBg, themeGradient, darkenColor, visibleRows,
rowIconFor, rowCssIconFor, rowActOf, normalizeQrPosition, normalizeQrStyle, resolveQrSize, qrTransform, QR_SIZES,
normalizeHeadStyle, normalizeActsLayout, normalizeActGridColorMode, normalizeActsPosition, actIconCfgOf,
WAP_LINE_ICONS, svgLineIconBg,
ACT_GRID_PRESETS, ACT_GRID_PRESET_FALLBACK, hexToRgba,
// 2.19: 显隐/文案/自定义动作归一化 + 素材图标库(与后端 CardIconLibrary 同源)
normalizeActVis, normalizeActDeleted, normalizeActLabels, normalizeCustomActs, normalizeActOrder, libIconBg,
} from './diy-card-common.js';
export default {
name: 'diy-card-mini',
props: {
card: { type: Object, default: null },
config: { type: Object, default: () => ({}) },
loading: { type: Boolean, default: false },
emptyTip: { type: String, default: '' },
emptyActionText: { type: String, default: '' },
},
data() {
return { expanded: false, showShare: false };
},
computed: {
c() {
return this.config || {};
},
wrapStyle() {
const c = this.c;
return this.$util.objToStyle({
background: flagOn(c.useCover, false) && this.card && this.card.cover ? 'transparent' : (c.bgColor || CardTokens.color.bg),
borderRadius: px2rpx(num(c.borderRadius, CardTokens.radius.md)) + 'rpx',
padding: px2rpx(num(c.padding, CardTokens.space.lg)) + 'rpx',
marginTop: px2rpx(num(c.wrapMarginTop, 0)) + 'rpx',
marginRight: px2rpx(num(c.wrapMarginRight, 0)) + 'rpx',
marginBottom: px2rpx(num(c.wrapMarginBottom, 0)) + 'rpx',
marginLeft: px2rpx(num(c.wrapMarginLeft, 0)) + 'rpx',
boxShadow: flagOn(c.showShadow, true) ? CardTokens.shadow.card : 'none',
});
},
showCover() {
return flagOn(this.c.useCover, false) && !!(this.card && this.card.cover);
},
showGradient() {
return flagOn(this.c.showGradient, false);
},
showLogo() {
// 只有 corner 二维码实际显示时才避让;关闭二维码或无码源不隐藏 Logo。
return flagOn(this.c.showLogo, false) && !this.showQrCorner;
},
showActs() {
return flagOn(this.c.showQuick, true);
},
showRowsBlock() {
// 资料行总开关(面板「显示资料行」); 历史配置无此字段时默认显示, 兼容 1.0
return flagOn(this.c.showRows, true);
},
/**
* 二维码总条件: 开关 + 有码源(自定义图 qrImage 优先, 回落名片码 qr_url)
*/
showQrBase() {
return flagOn(this.c.showQr, false) && !!this.qrSrc;
},
/** 二维码码源(2.18): 用户自定义图片(媒体库)优先, 空=名片自动生成的码 */
qrSrc() {
const custom = String(this.c.qrImage || '').trim();
if (custom) return custom;
return (this.card && this.card.qr_url) || '';
},
qrPos() {
return normalizeQrPosition(this.c.qrPosition);
},
qrStyle() {
return normalizeQrStyle(this.c.qrStyle);
},
showQrTip() {
return flagOn(this.c.qrTipShow, true);
},
showQrRows() { return this.showQrBase && this.qrPos === 'rows'; },
showQrBottom() { return this.showQrBase && this.qrPos === 'bottom'; },
showQrHead() { return this.showQrBase && this.qrPos === 'head'; },
showQrCorner() { return this.showQrBase && this.qrPos === 'corner'; },
/** 头像开关(历史配置无此字段时默认显示) */
showAvatarBlock() {
return flagOn(this.c.showAvatar, true);
},
/**
* corner 档头部水平让位: 按浮层实际宽度反算(浮层宽[尺寸+衬底] + 右偏移20 + 呼吸20),
* 自定义大尺寸时自动跟随 —— 修复静态 180rpx 在 qrSize>120 时让位不足压住姓名区;
* 非 corner 档 / 二维码未启用时输出空对象(浮层不存在无需让位)
*/
cornerHeadStyle() {
if (this.qrPos !== 'corner' || !this.showQrBase) return {};
const size = resolveQrSize(this.qrPos, this.c.qrSize);
const pad = this.qrStyle === 'plain' ? 0 : (this.qrStyle === 'gradient' ? 12 : 20);
return { paddingRight: (size + pad + 40) + 'rpx' };
},
/**
* corner 档资料行/操作区让位量: 按二维码实际占高反算
* (浮层 top 20 + 块高[尺寸+样式衬底+tip] - 头部实际高 + 间隙 10), 防遮挡;
* 头部实际高: plain=104 / theme 档=32+104+28=164(上下 padding 增高, 2.17);
* 自定义大尺寸/关 tip 时自动跟随(不用 calc, 纯数值)
*/
cornerClearStyle() {
if (this.qrPos !== 'corner' || !this.showQrBase) return {};
const size = resolveQrSize(this.qrPos, this.c.qrSize);
const pad = this.qrStyle === 'plain' ? 0 : (this.qrStyle === 'gradient' ? 6 : 10) * 2;
const tip = this.showQrTip ? 42 : 0;
const total = 20 + size + pad + tip;
const headHeight = this.headTheme ? 164 : 104;
return { marginTop: Math.max(24, total - headHeight + 10) + 'rpx' };
},
/**
* 二维码容器样式: 只管尺寸(自定义>档位自动) + 微调位移(transform, WXSS 支持)。
* 外观衬底(白卡/主题色/渐变)在 qrImgStyle 上只包二维码图 ——
* 「扫码查看完整名片」提示(2026-10-23 用户要求)在衬底外, 无蓝底
*/
qrBlockStyle() {
const size = resolveQrSize(this.qrPos, this.c.qrSize);
const style = {
width: size + 'rpx',
};
const transform = qrTransform(this.c.qrOffsetX, this.c.qrOffsetY);
if (transform) style.transform = transform;
return this.$util.objToStyle(style);
},
/** 二维码图样式: 尺寸 + 外观衬底(plain 原图 / card 白卡 / theme 主题色 / gradient 渐变) */
qrImgStyle() {
const size = resolveQrSize(this.qrPos, this.c.qrSize);
const style = {
width: size + 'rpx',
height: size + 'rpx',
boxSizing: 'border-box',
};
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') {
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';
}
return this.$util.objToStyle(style);
},
showDetailBtn() {
return flagOn(this.c.showDetailBtn, true);
},
/** 发名片按钮(名片详情页同款主按钮; 默认关, 由 show 组件打开) */
showSendBtn() {
return flagOn(this.c.showSendBtn, false);
},
sendBtnText() {
return this.c.sendBtnText || '发名片';
},
sendBtnStyle() {
return this.$util.objToStyle({
background: themeGradient(this.card),
height: px2rpx(CardTokens.touch.btnHeight) + 'rpx',
borderRadius: px2rpx(CardTokens.radius.pill) + 'rpx',
});
},
/** 头部形态(2.17): theme=渐变整块头部(历史配置无此字段时为 plain, 兼容 1.0) */
headTheme() {
return normalizeHeadStyle(this.c.headStyle) === 'theme';
},
/**
* 头部容器样式(plain/theme 共用)。未开启自定义间距时完整保留历史布局:
* theme 负外边距抵消卡片 padding,plain 不额外设置间距。
*/
headBoxStyle() {
const c = this.c;
const custom = flagOn(c.headSpacingCustom, false);
const style = {
background: c.headBgMode === 'custom'
? (String(c.headBg || '').trim() || 'transparent')
: (this.headTheme ? themeGradient(this.card) : 'transparent'),
marginTop: px2rpx(custom ? num(c.headMarginTop, 0) : (this.headTheme ? -num(c.padding, CardTokens.space.lg) : 0)) + 'rpx',
marginRight: px2rpx(custom ? num(c.headMarginRight, 0) : (this.headTheme ? -num(c.padding, CardTokens.space.lg) : 0)) + 'rpx',
marginBottom: px2rpx(custom ? num(c.headMarginBottom, 0) : 0) + 'rpx',
marginLeft: px2rpx(custom ? num(c.headMarginLeft, 0) : (this.headTheme ? -num(c.padding, CardTokens.space.lg) : 0)) + 'rpx',
paddingTop: px2rpx(custom ? num(c.headPaddingTop, 16) : (this.headTheme ? 16 : 0)) + 'rpx',
paddingRight: px2rpx(custom ? num(c.headPaddingRight, 16) : (this.headTheme ? num(c.padding, CardTokens.space.lg) : 0)) + 'rpx',
paddingBottom: px2rpx(custom ? num(c.headPaddingBottom, 14) : (this.headTheme ? 14 : 0)) + 'rpx',
paddingLeft: px2rpx(custom ? num(c.headPaddingLeft, 16) : (this.headTheme ? num(c.padding, CardTokens.space.lg) : 0)) + 'rpx',
borderRadius: c.headRadiusMode === 'custom'
? px2rpx(num(c.headRadius, 8)) + 'rpx'
: (this.headTheme ? '0 0 16rpx 16rpx' : '0'),
};
return this.$util.objToStyle(style);
},
headThemeStyle() {
return this.headBoxStyle;
},
/** 资料区标题(2.17): 默认关(历史配置无此字段时不出标题) */
rowsTitleShow() {
return flagOn(this.c.showRowsTitle, false);
},
rowsTitle() {
return String(this.c.rowsTitle || '联系资料');
},
/** 快捷操作布局(2.17): 默认 chips 胶囊(历史配置无此字段时不变) */
actsGrid() {
return normalizeActsLayout(this.c.actsLayout) === 'grid';
},
/** 复制按钮(2.17): 默认关; 手机等资料行右侧浅底小按钮, 点击复制原文 */
showCopyBtn() {
return flagOn(this.c.showCopyBtn, false);
},
qrTip() {
return this.c.qrTip || '';
},
// ===== 2.19.10 detail 版式对齐字段 =====
/** 资料行三列(图标+灰标签+值, detail 同款): 默认 0 = 历史图标/标签二选一 */
showRowLabels() {
return flagOn(this.c.showRowLabels, false);
},
/** hero 徽章(左上「数字名片」): 默认关 */
badgeShow() {
return flagOn(this.c.badgeShow, false);
},
badgeText() {
return String(this.c.badgeText || '数字名片');
},
/** 提示加粗段(detail <b>扫码保存名片</b>); 空 = 整行走 qrTip(历史形态) */
qrTipEm() {
return String(this.c.qrTipEm || '').trim();
},
/** 提示第一行(加粗段之后): 有 em 时前置「 · 」分隔符 */
qrTipLine1() {
const tip = String(this.c.qrTip || '').trim();
if (this.qrTipEm) return tip ? (' · ' + tip) : '';
return tip || '扫码查看完整名片';
},
/** 提示第二行(detail「同一二维码可写入 NFC 名片贴」): 空 = 不出 */
qrTipSub() {
return String(this.c.qrTipSub || '').trim();
},
/** 二维码下 URL 文本行(detail qr-url 同款): 默认关 */
qrUrlShow() {
return flagOn(this.c.qrUrlShow, false);
},
qrUrlText() {
return (this.card && this.card.wap_url) || '';
},
/** 复制按钮范围: all=全部行(历史) / mobile=仅手机微信行(detail 口径) */
copyBtnScope() {
return String(this.c.copyBtnScope || 'all');
},
/** 复制按钮外观: theme=主题浅底(历史) / gray=浅灰描边胶囊(detail 同款) */
copyBtnLook() {
return String(this.c.copyBtnLook || 'theme');
},
detailBtnText() {
return this.c.detailBtnText || '';
},
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;
},
/**
* 快捷操作(2.19 重构): 服务端 acts(资料驱动) + 组件配置合成
* 1. actVis 显隐过滤(缺省全显, 历史形态零变化)
* 2. actLabels 文案覆盖(空/缺 = 服务端 label)
* 3. customActs 自定义动作尾接(vis=0 隐藏)
* 每项合并 cfgImg/cfgKey/cfgColor/cfgBg 四字段 —— 图标渲染统一走
* 「媒体库图 > 素材库图标 > SVG 线稿 > 首字」优先级, 免去方法内重复取配置
*/
acts() {
const raw = this.card && Array.isArray(this.card.acts) ? this.card.acts : [];
const vis = normalizeActVis(this.c.actVis);
const del = normalizeActDeleted(this.c.actDeleted);
const labels = normalizeActLabels(this.c.actLabels);
const icons = this.c.actIcons && typeof this.c.actIcons === 'object' ? this.c.actIcons : {};
const order = normalizeActOrder(this.c.actOrder);
const merge = (a) => {
const t = String(a.type);
const cfg = icons[t] && typeof icons[t] === 'object' ? icons[t] : {};
return Object.assign({}, a, {
label: labels[t] || a.label,
cfgImg: String(cfg.img || '').trim(),
cfgKey: String(cfg.iconKey || '').trim(),
cfgColor: String(cfg.color || '').trim(),
cfgBg: String(cfg.bg || '').trim(),
});
};
// 2.19.1: 按 actOrder 次序输出默认动作; 后端 quickActs 扩展类型(order 外)尾接兜底
const byType = {};
raw.forEach(a => {
if (a && a.type) byType[String(a.type)] = a;
});
const out = [];
order.forEach(t => {
if (byType[t] && vis[t] !== 0 && del[t] !== 1) out.push(merge(byType[t]));
});
raw.forEach(a => {
if (!a || !a.type) return;
const t = String(a.type);
if (order.indexOf(t) >= 0 || vis[t] === 0 || del[t] === 1) return;
out.push(merge(a));
});
normalizeCustomActs(this.c.customActs).forEach(ca => {
if (ca.vis === 0) return;
out.push(Object.assign({}, ca, {
cfgImg: ca.img,
cfgKey: ca.iconKey,
cfgColor: ca.color,
cfgBg: ca.bg,
}));
});
return out;
},
isEnterprise() {
return this.card && this.card.card_type === 'enterprise';
},
subLine() {
const c = this.card || {};
return [c.job_title, c.company].filter(Boolean).join(' · ');
},
gradientStyle() {
return this.$util.objToStyle({ background: themeGradient(this.card) });
},
/** 顶部色带默认隐藏;背景可跟随名片设计或使用独立颜色。 */
bandShow() {
return flagOn(this.c.bandShow, false);
},
bandStyle() {
const custom = this.c.bandBgMode === 'custom';
return this.$util.objToStyle({
background: custom ? (String(this.c.bandBg || '').trim() || 'transparent') : themeGradient(this.card),
});
},
shareBtnStyle() {
// 分享抽屉主按钮(复制链接): 主题色
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
return this.$util.objToStyle({
background: theme,
border: '2rpx solid ' + theme,
});
},
avatarStyle() {
return this.$util.objToStyle({ background: avatarBg(this.card) });
},
nameStyle() {
// theme 渐变头部档: 深底上默认白字(headNameColor 可覆盖); plain 档沿用历史配色
const autoColor = this.headTheme ? (this.c.headNameColor || '#FFFFFF') : (this.c.nameColor || CardTokens.color.text);
return this.$util.objToStyle({
color: autoColor,
fontSize: px2rpx(num(this.c.nameSize, 17)) + 'rpx',
});
},
/** 名片头衔(设计器 card_title): 默认 13px = 26rpx, 与副行 24rpx 区分 */
cardTitleStyle() {
const autoColor = this.headTheme ? (this.c.headSubColor || 'rgba(255,255,255,0.9)') : (this.c.infoColor || CardTokens.color.textSub);
return this.$util.objToStyle({
color: autoColor,
fontSize: px2rpx(num(this.c.cardTitleSize, 13)) + 'rpx',
});
},
/** 副行(公司/职位)与空态提示共用: 默认 12px = 24rpx */
subTextStyle() {
// theme 档副行默认白 85% 透明度(渐变深底可读); plain 档沿用历史配色
const autoColor = this.headTheme ? (this.c.headSubColor || 'rgba(255,255,255,0.85)') : (this.c.infoColor || CardTokens.color.textSub);
return this.$util.objToStyle({
color: autoColor,
fontSize: px2rpx(num(this.c.subSize, 12)) + 'rpx',
});
},
/** labelColor 纯色(资料行图标/二维码提示用, 不带字号) */
labelStyle() {
return this.$util.objToStyle({ color: this.c.labelColor || CardTokens.color.textLabel });
},
/** 资料行文字标签: 默认 12px = 24rpx */
rowLabelStyle() {
return this.$util.objToStyle({
color: this.c.labelColor || CardTokens.color.textLabel,
fontSize: px2rpx(num(this.c.labelSize, 12)) + 'rpx',
});
},
valueStyle() {
return this.$util.objToStyle({
color: this.c.infoColor || CardTokens.color.textSub,
fontSize: px2rpx(num(this.c.valueSize, 12)) + 'rpx',
});
},
/** 空态提示: 固定历史配色(不随 theme 头部变白 —— 空态无渐变头) */
emptyTipStyle() {
return this.$util.objToStyle({
color: this.c.infoColor || CardTokens.color.textSub,
fontSize: px2rpx(num(this.c.subSize, 12)) + 'rpx',
});
},
/** 资料区标题(2.17): 主题色竖条 + 主文字色标题 */
rowsTitleBarStyle() {
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
return this.$util.objToStyle({ background: theme });
},
rowsTitleStyle() {
return this.$util.objToStyle({
color: this.c.nameColor || CardTokens.color.text,
fontSize: px2rpx(14) + 'rpx',
fontWeight: '600',
});
},
/** 复制按钮: theme=主题浅底+主题色字(历史); gray=detail 同款浅灰描边胶囊 */
copyBtnStyle() {
if (this.copyBtnLook === 'gray') {
return this.$util.objToStyle({
background: '#F7F9FC',
color: '#5A6478',
border: '2rpx solid #EEF1F6',
});
}
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
return this.$util.objToStyle({
background: hexToRgba(theme, 0.12) || '#F2F3F5',
color: theme,
});
},
/** 提示加粗段样式(detail qr-tip b 同款: ink-2 深灰 + 加粗) */
qrTipEmStyle() {
return this.$util.objToStyle({
color: this.c.infoColor || '#5A6478',
fontWeight: '600',
});
},
/** 二维码 URL 行样式(detail qr-url 同款: 小字灰 + 断行) */
qrUrlStyle() {
return this.$util.objToStyle({
color: this.c.labelColor || CardTokens.color.textLabel,
fontSize: px2rpx(10) + 'rpx',
marginTop: px2rpx(8) + 'rpx',
wordBreak: 'break-all',
textAlign: 'center',
});
},
/** hero 徽章胶囊(theme 档白描边 / plain 档主题描边; 避让企业徽标 badgeStyle) */
heroBadgeStyle() {
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
return this.$util.objToStyle({
border: '2rpx solid ' + (this.headTheme ? 'rgba(255,255,255,0.35)' : (hexToRgba(theme, 0.35) || theme)),
});
},
heroBadgeTextStyle() {
return this.$util.objToStyle({
color: this.headTheme ? 'rgba(255,255,255,0.85)' : (this.c.labelColor || CardTokens.color.textLabel),
fontSize: px2rpx(11) + 'rpx',
letterSpacing: '2rpx',
});
},
badgeStyle() {
// 企业徽标固定红底(参考图 2026-10-22 口径), 与头像底色解耦
return this.$util.objToStyle({ background: '#FA5151' });
},
toggleStyle() {
return this.$util.objToStyle({ color: (this.card && this.card.theme_color) || CardTokens.color.primary });
},
actStyle() {
// 快捷芯片统一「发邮件」样式(2026-10-23 参考图定稿): 白底 + 主题色描边;
// 尺寸/圆角由 .act-chip CSS 类控制(内联只出底色+描边, 不锁死尺寸)
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
return this.$util.objToStyle({
background: '#FFFFFF',
height: px2rpx(CardTokens.touch.chipHeight) + 'rpx',
padding: '0 ' + px2rpx(CardTokens.space.xl) + 'rpx',
borderRadius: px2rpx(CardTokens.radius.pill) + 'rpx',
});
},
actPrimaryTextStyle() {
return this.$util.objToStyle({
color: '#FFFFFF',
fontSize: px2rpx(num(this.c.actSize, 12)) + 'rpx',
});
},
/** 首动作(索引 0)实心底: 主题色 + 胶囊尺寸(与 actStyle/actGhostStyle 同口径) */
actPrimaryStyle() {
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
return this.$util.objToStyle({
background: theme,
height: px2rpx(CardTokens.touch.chipHeight) + 'rpx',
padding: '0 ' + px2rpx(CardTokens.space.xl) + 'rpx',
borderRadius: px2rpx(CardTokens.radius.pill) + 'rpx',
});
},
actGhostStyle() {
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
return this.$util.objToStyle({
background: 'transparent',
border: '2rpx solid ' + theme,
});
},
actTextStyle() {
return this.$util.objToStyle({ color: (this.card && this.card.theme_color) || CardTokens.color.primary });
},
actGhostTextStyle() {
return this.$util.objToStyle({
color: (this.card && this.card.theme_color) || CardTokens.color.primary,
fontSize: px2rpx(num(this.c.actSize, 12)) + 'rpx',
});
},
/** 发送名片按钮文字: 默认 13px = 26rpx */
detailTextStyle() {
return this.$util.objToStyle({
color: '#FFFFFF',
fontSize: px2rpx(num(this.c.btnSize, 13)) + 'rpx',
});
},
/** 宫格配色模式(2.17): preset 多彩预设 / theme 主题色派生 */
gridColorMode() {
return normalizeActGridColorMode(this.c.actGridColorMode);
},
/** 宫格列宽: 5 个动作一行(存通讯录入列后), 4 个及以下仍四列均分 */
gridItemWidth() {
return this.acts.length > 4 ? '20%' : '25%';
},
/** 快捷操作区位置(2.18): above=资料区上方(below 历史默认零变化) */
actsAbove() {
return normalizeActsPosition(this.c.actsPosition) === 'above';
},
/**
* 快捷操作区独立块样式(2.18): 背景/圆角/内边距, 全为默认(历史形态)时输出空串零变化
*/
actsBlockStyle() {
const c = this.c;
const bg = String(c.actsBlockBg || '').trim();
const radius = num(c.actsBlockRadius, 0);
const pad = num(c.actsBlockPad, 0);
const style = {};
if (bg) style.background = bg;
if (radius > 0) style.borderRadius = px2rpx(radius) + 'rpx';
if (pad > 0) style.padding = px2rpx(pad) + 'rpx';
return this.$util.objToStyle(style);
},
/** 资料行让位: 操作区在上方时由操作区让位, 资料行不再重复位移 */
rowsClearStyle() {
return this.actsAbove ? {} : this.cornerClearStyle;
},
/**
* 操作区最终样式(2.18 独立成块 + corner 让位): 合并输出单字符串。
* uni-app 运行时会静默丢弃 :style="[字符串, 对象]" 数组绑定 ——
* 2026-10-22 线上问题(qrBlockStyle)同根因, 2026-10-24 H5 实测复现于操作区
* (actsBlockBg/Radius/Pad 与 corner 让位同时失效, style 属性整体为 null)
*/
actsBlockFinalStyle() {
const clear = this.cornerClearStyle || {};
const extra = clear.marginTop ? 'margin-top: ' + clear.marginTop + ';' : '';
return ((this.actsBlockStyle || '') + ' ' + extra).trim();
},
/** 资料区独立块样式(与操作区同款: Bg/Radius/Pad) */
rowsBlockStyle() {
const c = this.c;
const bg = String(c.rowsBlockBg || '').trim();
const radius = num(c.rowsBlockRadius, 0);
const pad = num(c.rowsBlockPad, 0);
const style = {};
if (bg) style.background = bg;
if (radius > 0) style.borderRadius = px2rpx(radius) + 'rpx';
if (pad > 0) style.padding = px2rpx(pad) + 'rpx';
return this.$util.objToStyle(style);
},
/** 二维码独立块样式(与操作区同款: Bg/Radius/Pad) */
qrBlockCustomStyle() {
const c = this.c;
const bg = String(c.qrBlockBg || '').trim();
const radius = num(c.qrBlockRadius, 0);
const pad = num(c.qrBlockPad, 0);
const style = {};
if (bg) style.background = bg;
if (radius > 0) style.borderRadius = px2rpx(radius) + 'rpx';
if (pad > 0) style.padding = px2rpx(pad) + 'rpx';
return this.$util.objToStyle(style);
},
/** 资料区最终样式: 独立块样式 + corner 让位合并 */
rowsFinalStyle() {
const clear = this.rowsClearStyle || {};
const extra = clear.marginTop ? 'margin-top: ' + clear.marginTop + ';' : '';
return ((this.rowsBlockStyle || '') + ' ' + extra).trim();
},
/** 二维码(rows/bottom 流内档)最终样式: 独立块样式 + 尺寸/位移合并 */
qrBlockFinalStyle() {
const transform = qrTransform(this.c.qrOffsetX, this.c.qrOffsetY);
const parts = [this.qrBlockStyle, this.qrBlockCustomStyle];
if (transform) parts.push('transform:' + transform + ';');
return parts.filter(Boolean).join(' ').trim();
},
/** plain 头最终样式:头部容器配置与 corner 二维码让位合并为单字符串。 */
plainHeadFinalStyle() {
const corner = this.$util.objToStyle(this.cornerHeadStyle || {});
return [this.headBoxStyle, corner].filter(Boolean).join(' ').trim();
},
detailBtnStyle() {
// 发送名片大按钮: 纯色主题实心(2026-10-22 参考图定稿, 不再用渐变);
// 高度 104rpx(用户反馈: 比触达标准 80rpx 加高一档, 参考图观感)
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
return this.$util.objToStyle({
background: theme,
height: '95rpx',
borderRadius: px2rpx(CardTokens.radius.pill) + 'rpx',
});
},
emptyBtnStyle() {
return this.$util.objToStyle({
background: CardTokens.color.primary,
height: px2rpx(CardTokens.touch.btnHeight) + 'rpx',
borderRadius: px2rpx(CardTokens.radius.pill) + 'rpx',
padding: '0 ' + px2rpx(CardTokens.space.xxl) + 'rpx',
});
},
},
methods: {
// 模板无法访问模块导入, 需挂到实例(list.vue 同款写法); 头像缺图时显示姓氏首字
initialOf,
/** 资料行图标(开关关时退化为文字标签; field 无字形同样退化) */
rowIcon(row) {
return flagOn(this.c.showRowIcons, true) ? rowIconFor(row && row.field) : '';
},
/** 资料行 CSS 描边图标(2.17.1): 邮箱=信封 / 网站=线框地球, 同受行图标开关 */
rowCssIcon(row) {
return flagOn(this.c.showRowIcons, true) ? rowCssIconFor(row && row.field) : '';
},
/** 资料行 CSS 图标颜色: 跟随 labelColor(与 iconfont 行图标同色口径) */
rowCssColor() {
return this.c.labelColor || CardTokens.color.textLabel;
},
rowCssBorderStyle() {
return this.$util.objToStyle({ borderColor: this.rowCssColor() });
},
rowCssFillStyle() {
return this.$util.objToStyle({ background: this.rowCssColor() });
},
/** 资料行是否可点: 行点击开关 + 服务端 href 派生得出动作 */
rowTapable(row) {
return flagOn(this.c.rowTap, true) && !!rowActOf(row);
},
onRowTap(row) {
const act = rowActOf(row);
if (act) this.$emit('row-act', act);
},
onAct(act) {
this.$emit('act', act);
},
/* ---------- 分享名片抽屉(「发送名片」弹出; 版式/逻辑与精选名片 show 完全同款) ---------- */
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 });
},
/** 二维码点击 → 预览名片(进详情页; qrTapPreview 开关控制, 默认开) */
onQrTap() {
if (flagOn(this.c.qrTapPreview, true)) this.$emit('detail', this.card);
},
onSend() {
this.$emit('send', this.card);
},
/** 每按钮自定义图标图片(2.18 媒体库, 优先级最高; acts 已合并 cfgImg) */
actImgOf(act) {
return String((act && act.cfgImg) || '').trim() || String(actIconCfgOf(this.c.actIcons, act && act.type).img || '').trim();
},
/**
* 素材库图标(2.19): acts 已合并 cfgKey(iconKey), 库缺 key 返回空串回落线稿档
*/
actLibOf(act) {
return String((act && act.cfgKey) || '').trim();
},
/** 素材库图标背景(实时配色: color 空回落 actIconColor 四级口径) */
actLibStyle(act) {
return this.$util.objToStyle({ backgroundImage: libIconBg(this.actLibOf(act), this.actIconColor(act)) });
},
/** 宫格 SVG 线稿(2.18.1): 与 wap detail 同源, 素材库/媒体库优先时不再进入 */
actSvgOf(act) {
return !this.actImgOf(act) && !this.actLibOf(act) && !!WAP_LINE_ICONS[String((act && act.type) || '')];
},
/**
* 图标前景色: act 合并的每按钮 color > detail 同源固定色(wx 微信绿/save 通讯录橙)
* > theme 档主题色 > preset 档预设深字
*/
actIconColor(act) {
const custom = String((act && act.cfgColor) || '').trim();
if (custom) return custom;
const ico = WAP_LINE_ICONS[String((act && act.type) || '')];
if (ico && ico.color) return ico.color;
if (this.gridColorMode === 'theme') {
return (this.card && this.card.theme_color) || CardTokens.color.primary;
}
const preset = ACT_GRID_PRESETS[(act && act.type) || ''] || ACT_GRID_PRESET_FALLBACK;
return preset.fg;
},
/** SVG 线稿背景(data URI, 颜色内嵌) */
actSvgStyle(act) {
return this.$util.objToStyle({ backgroundImage: svgLineIconBg(act && act.type, this.actIconColor(act)) });
},
actInitialOf(act) {
return String((act && act.label) || '·').charAt(0);
},
/** 宫格配色(每按钮 bg 覆盖 / preset 多彩预设 / theme 主题渐变) —— 带参, 走 methods */
actGridIconStyle(act) {
// 每按钮自定义底色优先(2.18), 空回落 preset/theme 档(acts 已合并 cfgBg)
const cfgBg = String((act && act.cfgBg) || '').trim() || String(actIconCfgOf(this.c.actIcons, act && act.type).bg || '').trim();
if (cfgBg) return this.$util.objToStyle({ background: cfgBg });
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
if (this.gridColorMode === 'theme') {
// detail 同款浅主题渐变底(145deg, 12%→20% 主题色混白)
return this.$util.objToStyle({
background: 'linear-gradient(145deg, ' + (hexToRgba(theme, 0.12) || '#F2F3F5') + ', ' + (hexToRgba(theme, 0.2) || '#F2F3F5') + ')',
});
}
const preset = ACT_GRID_PRESETS[(act && act.type) || ''] || ACT_GRID_PRESET_FALLBACK;
return this.$util.objToStyle({ background: preset.bg });
},
/** 首字兜底字色(未知类型), 与每按钮 color/固定色同口径 */
actGridGlyphStyle(act) {
return this.$util.objToStyle({ color: this.actIconColor(act) });
},
actGridLabelStyle() {
return this.$util.objToStyle({
color: this.c.labelColor || CardTokens.color.textSub,
fontSize: px2rpx(num(this.c.actSize, 12)) + 'rpx',
});
},
/** 复制按钮可见性: 开关 + 值非空 + 范围(mobile=仅手机/微信行, detail 口径) */
copyBtnVisible(row) {
if (!this.showCopyBtn || !row || !String(row.value || '').trim()) return false;
if (this.copyBtnScope === 'mobile') {
const f = String(row.field || '').toLowerCase();
return f === 'mobile' || f === 'phone' || f === 'wechat' || f === 'wx';
}
return true;
},
/** 复制按钮: 复用 row-act 的 copy 分支(rowTap 关闭时仍可独立复制) */
onCopyRow(row) {
this.$emit('row-act', { type: 'copy', value: row.value });
},
onDetail() {
this.$emit('detail', this.card);
},
onEmptyAction() {
this.$emit('empty-action');
},
},
};
</script>
<style lang="scss" scoped>
.card-mini {
width: 100%;
/* 顶带+白卡同容器: 顶带 top/bottom 拉伸 → 高度自适应白卡,
弧线始终距白卡底 88rpx(2026-10-23 参考图定稿, 与 show 同口径) */
.mini-card-wrap {
position: relative;
margin-top: 40rpx;
}
/* 主题色顶带: 从露沿处拉伸, 底端距白卡底 88rpx; 顶部直角, 底部 28rpx 小圆弧 */
.mini-band {
position: absolute;
left: 0;
right: 0;
top: -40rpx;
bottom: 88rpx;
height: auto;
border-radius: 0 0 28rpx 28rpx;
}
.mini-card {
position: relative;
overflow: hidden;
width: auto;
margin: 0 24rpx;
box-sizing: border-box;
z-index: 1;
.mini-badge-slot {
position: absolute;
left: 0;
top: 0;
z-index: 4;
}
.mini-cover {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.mini-cover-scrim {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.45);
z-index: 1;
}
.mini-gradient {
/* 流内饰条(2026-10-22 参考图定稿, show 的 .card-grad-bar 同款):
不再 absolute 贴死卡顶, 改为排在头像行上方并与其拉开 28rpx */
position: relative;
z-index: 2;
width: 100%;
height: 14rpx;
border-radius: 8rpx;
margin-bottom: 28rpx;
}
.mini-logo {
position: absolute;
right: 24rpx;
top: 24rpx;
width: 64rpx;
height: 64rpx;
z-index: 3;
}
.mini-head {
position: relative;
z-index: 2;
display: flex;
align-items: center;
.mini-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;
}
}
/* theme 渐变头部档: 白描边白底字头像更贴合深底(仅 theme 档, plain 零变化) */
.mini-headtheme & {
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.10);
}
}
/* 头部·主题渐变整块档(2.17): 通铺卡边 + 装饰圆; 颜色固定白透明度, 任意主题色可见
圆角只给底部 —— 顶部贴卡边, 避免与卡片圆角叠加出双圆角 */
.mini-headtheme {
position: relative;
z-index: 2;
overflow: hidden;
border-radius: 0 0 16rpx 16rpx;
.headtheme-decor {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
pointer-events: none;
&--big {
width: 240rpx;
height: 240rpx;
right: -60rpx;
top: -80rpx;
}
&--small {
width: 120rpx;
height: 120rpx;
left: -40rpx;
bottom: -50rpx;
background: rgba(255, 255, 255, 0.08);
}
}
}
/* 资料区标题(2.17): 主题色竖条(尺寸内联) + 600 字重标题 */
.mini-rows-title {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.rows-title-bar {
flex-shrink: 0;
width: 8rpx;
height: 28rpx;
border-radius: 4rpx;
margin-right: 12rpx;
}
}
/* 2.19.10 hero 徽章(detail hero-badge 同款: 绿点 + 描边胶囊) */
.mini-hero-badge {
display: inline-flex;
align-items: center;
position: relative;
z-index: 2;
align-self: flex-start;
margin-bottom: 20rpx;
padding: 6rpx 18rpx;
border-radius: 999rpx;
.hero-badge-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: #07c160;
margin-right: 10rpx;
}
}
/* 提示多行(detail qr-tip 同款: 加粗段 + 换行) */
.qr-tip-em {
font-weight: 600;
}
.qr-tip-sub {
font-size: 22rpx;
margin-top: 4rpx;
}
/* 二维码 URL 行(detail qr-url 同款) */
.qr-url {
display: block;
width: 100%;
}
/* 复制按钮(2.17): 浅底主题色小按钮(色内联), 不挤压资料值换行 */
.row-copy {
flex-shrink: 0;
margin-left: 16rpx;
padding: 4rpx 20rpx;
border-radius: 999rpx;
font-size: 22rpx;
line-height: 1.4;
}
/* 快捷操作·图标宫格档(2.17): 均分多列, 图标浅底块 88rpx
列宽由内联 gridItemWidth 动态给出(5 动作 20% / 4 及以下 25%), 此处为兜底 */
.mini-acts-grid {
position: relative;
z-index: 2;
margin-top: 24rpx;
display: flex;
flex-wrap: wrap;
.act-grid-item {
width: 25%;
box-sizing: border-box;
padding: 8rpx 0;
display: flex;
flex-direction: column;
align-items: center;
.act-grid-icon {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
/* SVG 线稿图标(2.18.1): data URI 背景呈现, 与 wap detail 同源 */
.act-grid-svg {
width: 44rpx;
height: 44rpx;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
/* 自定义图标图片(2.18): 铺满图标块, 圆角继承 */
.act-grid-img {
width: 100%;
height: 100%;
display: block;
border-radius: inherit;
}
.act-grid-initial {
font-size: 36rpx;
font-weight: 600;
}
}
.act-grid-label {
margin-top: 12rpx;
line-height: 1.4;
}
}
}
.mini-head-info {
flex: 1;
min-width: 0;
margin-left: 24rpx;
display: flex;
flex-direction: column;
.name-row {
display: flex;
align-items: center;
.mini-name {
font-size: 34rpx;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mini-badge {
flex-shrink: 0;
margin-left: 12rpx;
font-size: 18rpx;
color: #ffffff;
padding: 2rpx 12rpx;
border-radius: 8rpx;
}
}
.mini-card-title {
font-size: 26rpx;
margin-top: 6rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mini-sub {
font-size: 24rpx;
margin-top: 6rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.mini-rows {
position: relative;
z-index: 2;
margin-top: 24rpx;
.mini-row {
display: flex;
line-height: 2;
/* CSS 描边图标容器(2.17.1): 与 iconfont 图标列/文字标签列同宽 96rpx, 值列左缘对齐 */
.row-css-wrap {
flex-shrink: 0;
width: 96rpx;
display: flex;
align-items: center;
justify-content: center;
align-self: center;
}
/* 行内缩小版信封/地球(宫格同构图形, 边框色内联) */
.css-ico-mail--row {
width: 30rpx;
height: 22rpx;
border-width: 3rpx;
border-radius: 4rpx;
.css-ico-mail-flap {
width: 10rpx;
height: 10rpx;
border-width: 3rpx;
top: -2rpx;
}
}
.css-ico-web--row {
width: 26rpx;
height: 26rpx;
border-width: 3rpx;
.css-ico-web-h {
height: 3rpx;
}
.css-ico-web-v {
width: 10rpx;
border-width: 3rpx;
}
}
.row-label {
flex-shrink: 0;
width: 96rpx;
font-size: 24rpx;
}
/* 三列档(2.19.10): 图标+灰标签+值, detail 资料行同款列宽 */
&.row-3col {
.row-icon,
.row-css-wrap {
width: 56rpx;
}
.row-label {
width: 88rpx;
}
}
/* 图标列与文字标签列同宽, 保证值列左缘对齐(参考图口径) */
.row-icon {
flex-shrink: 0;
width: 96rpx;
text-align: center;
font-size: 28rpx;
}
.row-value {
flex: 1;
font-size: 24rpx;
word-break: break-all;
}
/* 可点行: hover-class 已给按压反馈, 此处仅标识可点(光标语义小程序无效, 保留 H5) */
&.row-tappable {
cursor: pointer;
}
}
.row-toggle {
margin-top: 8rpx;
display: flex;
justify-content: flex-end;
.toggle-text {
font-size: 22rpx;
font-weight: 500;
}
}
}
.mini-acts {
position: relative;
z-index: 2;
margin-top: 24rpx;
display: flex;
flex-wrap: wrap;
.act-chip {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
/* 小胶囊(2026-10-22 定稿): 显式给高/内距/圆角(内联只出背景色, 尺寸走本类);
72rpx 触达标准, 按用户反馈放大一档 */
height: 60rpx;
padding: 0 32rpx;
margin: 0 16rpx 16rpx 0;
border-radius: 36rpx;
.act-text {
font-size: 26rpx;
font-weight: 500;
}
}
}
.mini-qr {
position: relative;
z-index: 2;
margin-top: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
/* rows/bottom 档(流内块级)水平居中; corner/head 档不带本类, 定位不受影响 */
&.mini-qr-center {
margin-left: auto;
margin-right: auto;
}
/* 块尺寸由 qrBlockStyle 内联给出(自定义/档位), 图片填满内容盒 */
.qr-img {
width: 100%;
height: 100%;
display: block;
}
.qr-tip {
font-size: 22rpx;
margin-top: 12rpx;
}
/* head 档: 头部第三段(头像|信息|二维码), 不占额外垂直节奏 */
&.mini-qr-head {
margin-top: 0;
margin-left: 20rpx;
flex-shrink: 0;
align-self: center;
.qr-tip {
font-size: 20rpx;
margin-top: 8rpx;
}
}
/* corner 档: 右上角浮层(外观衬底由 qrStyle 内联决定, 此处只定位于层次) */
&.mini-qr-corner {
position: absolute;
top: 20rpx;
right: 20rpx;
z-index: 3;
margin-top: 0;
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.12);
}
}
/* corner 档: 头部右侧让位由 cornerHeadStyle 按浮层实际宽度动态给出
(静态 180rpx 在自定义大尺寸 qrSize 下不足, 已废弃); WXSS 不支持 calc() */
&.qr-at-corner {
.mini-head {
position: relative;
z-index: 2;
}
}
/* 发名片主按钮(名片详情页 send-bar 同款观感) */
.mini-send {
position: relative;
z-index: 2;
margin-top: 24rpx;
display: flex;
align-items: center;
justify-content: center;
.send-text {
font-size: 30rpx;
font-weight: 600;
color: #ffffff;
}
}
}
/* 发送名片大按钮: 白卡外下方(2026-10-23 参考图定稿),
与精选名片「发名片」同款布局; 左右 24rpx 与白卡对齐 */
.mini-detail {
position: relative;
z-index: 2;
margin: 24rpx 24rpx 0;
display: flex;
align-items: center;
justify-content: center;
/* 小程序端为 <button open-type="share">: 去掉原生默认灰底/边框, 背景由 detailBtnStyle 内联主题色给出 */
padding: 0;
border: none;
background-color: transparent;
line-height: normal;
font-size: 26rpx;
&::after {
border: none;
}
.detail-text {
font-size: 26rpx;
font-weight: 600;
color: #ffffff;
}
}
/* 空态 */
.mini-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;
text-align: center;
}
.empty-btn {
margin-top: 28rpx;
display: flex;
align-items: center;
justify-content: center;
.empty-btn-text {
font-size: 26rpx;
font-weight: 600;
color: #ffffff;
}
}
}
/* 骨架屏 */
.mini-skeleton {
width: 100%;
box-sizing: border-box;
.sk-row {
display: flex;
align-items: center;
.sk-avatar {
width: 104rpx;
height: 104rpx;
border-radius: 50%;
flex-shrink: 0;
}
.sk-col {
flex: 1;
margin-left: 24rpx;
}
}
.sk-btn {
height: 80rpx;
border-radius: 999rpx;
}
}
.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;
}
}
/* 入场动效(逐项延迟由父组件传 animation-delay) */
.fade-up {
animation: card-fade-up 240ms ease both;
}
/* 按压反馈(芯片/按钮/空态按钮共用) */
.btn-press {
opacity: 0.72;
transform: scale(0.98);
}
/* CSS 描边图标(2.17.1): iconfont 无邮件/网站字形, 纯 view 边框绘制,
颜色由内联 style 跟随 preset/theme 配色档(H5+小程序通用)。
顶层定义 —— 宫格(88rpx 图标块内)与资料行(--row 缩小版)共用同一套图形 */
.css-ico-mail {
position: relative;
width: 44rpx;
height: 32rpx;
border: 4rpx solid #B0B4BC;
border-radius: 6rpx;
box-sizing: border-box;
/* 封面折角: 只留左+下边的方块旋转 -45° 成 V 形折线 */
.css-ico-mail-flap {
position: absolute;
left: 50%;
top: -2rpx;
width: 16rpx;
height: 16rpx;
border-left: 4rpx solid #B0B4BC;
border-bottom: 4rpx solid #B0B4BC;
transform: translateX(-50%) rotate(-45deg);
}
}
.css-ico-web {
position: relative;
width: 40rpx;
height: 40rpx;
border: 4rpx solid #B0B4BC;
border-radius: 50%;
box-sizing: border-box;
overflow: hidden;
/* 赤道横线(色由内联 background 给出) */
.css-ico-web-h {
position: absolute;
left: 0;
top: 50%;
width: 100%;
height: 4rpx;
transform: translateY(-50%);
}
/* 经线椭圆: 左右双边框 + 椭圆圆角 */
.css-ico-web-v {
position: absolute;
left: 50%;
top: -2rpx;
bottom: -2rpx;
width: 14rpx;
transform: translateX(-50%);
border-left: 4rpx solid #B0B4BC;
border-right: 4rpx solid #B0B4BC;
border-radius: 50%;
}
}
@keyframes sk-shimmer {
0% {
background-position: -200rpx 0;
}
100% {
background-position: 200rpx 0;
}
}
@keyframes card-fade-up {
from {
opacity: 0;
transform: translateY(12rpx);
}
to {
opacity: 1;
transform: none;
}
}
/* 分享名片抽屉(「发送名片」弹出; 与精选名片 show 同款, 复刻 detail.vue share-sheet) */
.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;
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: #b8c0cc;
margin-top: 20rpx;
}
}
</style>