feat(card): 名片区块流与基础信息补齐(2.22/2.23) + 分享带名片ID
- 组件支持六个「可独立成块」模块任意排序(区块流): 资料行/快捷操作/社交/二维码/ 标签来源的位置可调, 头部恒最上、发名片恒最下; 二维码位置档位归并为 flow。 - 补齐「名片基础信息」遗漏字段: 简介区块、社交区块、名片附加信息(过期遮罩/ 地址细分/来源), 并与 wap 端展示口径对齐。 - 小程序分享给游客时携带名片 ID: 分享 path 追加 card_id, 访客打开「我的名片」 即看到分享者那张卡。新增 common/js/card-share.js 统一处理「取卡ID / 拼参数」 (dataset 通道 + storage 通道兜底, 朋友圈场景靠 storage)。
This commit is contained in:
@@ -101,63 +101,19 @@
|
||||
</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="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>
|
||||
<!-- 2.23 区块流: 按 c.blockOrder 渲染, 六个可独立成块的模块任意排序 -->
|
||||
<template v-for="blk in blockSeq">
|
||||
<!-- 简介区块(2.22: 补齐「名片基础信息」遗漏字段; 可独立成块, 与资料区同款) -->
|
||||
<view v-if="blk === 'bio' && showBioBlock" class="mini-bio" :style="bioFinalStyle" :key="blk">
|
||||
<view v-if="bioTitleShow" class="mini-bio-title">
|
||||
<view class="bio-title-bar"></view>
|
||||
<text class="bio-title-text">{{ bioTitle }}</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="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">
|
||||
<view class="qr-frame" :style="qrFrameStyle"><image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" show-menu-by-longpress="true" /></view>
|
||||
<text v-if="showQrTip" class="qr-tip" :style="labelStyle" @tap="previewQrImg"><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>
|
||||
<text class="mini-bio-text" :style="bioTextStyle" :class="{ 'bio-text-clamp': bioMaxLines > 0 }">{{ bioText }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作·图标宫格档(grid): 彩色浅底图标 + 下方文字 -->
|
||||
<view v-if="!actsAbove && showActs && acts.length && actsGrid" class="mini-acts-grid" :style="actsBlockFinalStyle">
|
||||
<view v-if="blk === 'acts' && showActs && acts.length && actsGrid" class="mini-acts-grid" :style="actsBlockFinalStyle" :key="blk">
|
||||
<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 同源) / 首字兜底 -->
|
||||
@@ -171,23 +127,75 @@
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作·胶囊芯片档(chips, 历史默认; 最小触达 72rpx; 首个为主动作) -->
|
||||
<view v-if="!actsAbove && showActs && acts.length && !actsGrid" class="mini-acts" :style="actsBlockFinalStyle">
|
||||
<view v-if="blk === 'acts' && showActs && acts.length && !actsGrid" class="mini-acts" :style="actsBlockFinalStyle" :key="blk">
|
||||
<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>
|
||||
: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>
|
||||
|
||||
<!-- 二维码·主按钮上方(bottom 档; 点击预览名片) -->
|
||||
<view v-if="showQrBottom" class="mini-qr mini-qr-center" :style="qrBlockFinalStyle">
|
||||
<view class="qr-frame" :style="qrFrameStyle"><image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" show-menu-by-longpress="true" /></view>
|
||||
<text v-if="showQrTip" class="qr-tip" :style="labelStyle" @tap="previewQrImg"><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 v-if="blk === 'rows' && showRowsBlock && shownRows.length" class="mini-rows" :style="rowsFinalStyle" :key="blk">
|
||||
<!-- 资料区标题(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>
|
||||
|
||||
<!-- 发名片(对齐名片详情页: 主题色大按钮; 点击跳原生转发中转页拉起微信转发面板) -->
|
||||
<view v-if="showSendBtn" class="mini-send" :style="sendBtnStyle" hover-class="btn-press" @tap.stop="onSend">
|
||||
<text class="send-text">{{ sendBtnText || '发名片' }}</text>
|
||||
<!-- 社交媒体区块(2.22: 抖音/小红书/微博/领英等; 可独立成块) -->
|
||||
<view v-if="blk === 'social' && showSocialBlock" class="mini-social" :style="socialFinalStyle" :key="blk">
|
||||
<view v-for="(s, i) in socialList" :key="i" class="social-item" hover-class="btn-press" @tap.stop="onSocialTap(s)">
|
||||
<text class="social-label">{{ s.label }}</text>
|
||||
<text v-if="s.value" class="social-value">{{ s.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 二维码·卡片流内(位置由区块顺序决定; 点击预览名片) -->
|
||||
<view v-if="blk === 'qr' && showQrFlow" class="mini-qr mini-qr-center" :style="qrBlockFinalStyle" :key="blk">
|
||||
<view class="qr-frame" :style="qrFrameStyle"><image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" show-menu-by-longpress="true" /></view>
|
||||
<text v-if="showQrTip" class="qr-tip" :style="labelStyle" @tap="previewQrImg"><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>
|
||||
|
||||
<!-- 名片标签(2.22) + 来源标注(2.22) -->
|
||||
<view v-if="blk === 'meta' && (showTagsRow || showSourceRow)" class="mini-meta" :key="blk">
|
||||
<view v-if="showTagsRow" class="mini-tags">
|
||||
<text v-for="(t, i) in cardTags" :key="'t' + i" class="mini-tag">{{ t }}</text>
|
||||
</view>
|
||||
<text v-if="showSourceRow" class="mini-source">{{ sourceName }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- 发名片(对齐名片详情页: 主题色大按钮; 点击跳原生转发中转页拉起微信转发面板) -->
|
||||
<view v-if="showSendBtn" class="mini-send" :style="sendBtnStyle" hover-class="btn-press" @tap.stop="onSend">
|
||||
<text class="send-text">{{ sendBtnText || '发名片' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 过期遮罩(2.22: 名片有效期到期; 与名片详情页同口径) -->
|
||||
<view v-if="cardExpired" class="mini-expired">
|
||||
<text class="expired-text">该名片已过期</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -196,6 +204,7 @@
|
||||
<!-- #ifdef MP-WEIXIN || MP-HARMONY -->
|
||||
<button v-if="showDetailBtn" class="mini-detail" :style="detailBtnStyle" hover-class="btn-press"
|
||||
open-type="share"
|
||||
@tap="rememberShareCard"
|
||||
: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>
|
||||
@@ -248,6 +257,22 @@ import {
|
||||
// 2.19: 显隐/文案/自定义动作归一化 + 素材图标库(与后端 CardIconLibrary 同源)
|
||||
normalizeActVis, normalizeActDeleted, normalizeActLabels, normalizeCustomActs, normalizeActOrder, libIconBg,
|
||||
} from './diy-card-common.js';
|
||||
// 2026-10-28: 分享带名片 ID —— 按钮 @tap 时记住当前卡片, 供页面级分享钩子取用
|
||||
// (微信只认页面级 onShareAppMessage; onShareTimeline 不接受事件参数, 只能走 storage 通道)
|
||||
import { rememberShareCardId } from '@/common/js/card-share.js';
|
||||
|
||||
/**
|
||||
* 2.23 区块流: 可独立成块的模块集合与默认顺序
|
||||
*
|
||||
* token: bio(简介) / rows(资料区) / acts(快捷操作) / social(社交媒体)
|
||||
* qr(二维码·流内) / meta(标签与来源标注)
|
||||
* 头部恒在最上、发名片按钮恒在最下, 不参与排序;
|
||||
* 二维码的 head/corner 两档是「锚点」语义(头部内 / 右上角浮层), 也不进区块流。
|
||||
* 默认顺序 = 简介 → 快捷操作(资料区上方, 兼容历史 actsPosition=above)
|
||||
* → 资料区 → 社交媒体 → 二维码 → 标签来源
|
||||
*/
|
||||
const BLOCK_TOKENS = ['bio', 'rows', 'acts', 'social', 'qr', 'meta'];
|
||||
const DEFAULT_BLOCK_ORDER = 'bio,acts,rows,social,qr,meta';
|
||||
|
||||
/**
|
||||
* 2.20 三块「独立成块」逐向间距: 开关开启时输出逐向 margin/padding 覆盖串。
|
||||
@@ -262,6 +287,47 @@ function blockCustomSpacing(c, prefix) {
|
||||
return 'margin: ' + m.join('rpx ') + 'rpx; padding: ' + p.join('rpx ') + 'rpx;';
|
||||
}
|
||||
|
||||
/**
|
||||
* 2.22 通用「独立成块」判定(简介/社交与资料区/二维码同款口径)
|
||||
*
|
||||
* 判定优先级: ① 显式模式串(follow/card/跟随卡片 → 跟随; block/indep/独立成块 → 独立)
|
||||
* ② 隐式开关(配了底色/圆角/内边距即独立成块, 与面板 chip 同款)
|
||||
* prefix 语义: <prefix>BlockBg / <prefix>BlockRadius / <prefix>BlockPad
|
||||
*/
|
||||
function blockIndependent(c, prefix) {
|
||||
const mode = String(
|
||||
c[prefix + 'BlockStyle'] || c[prefix + 'BlockMode'] || c[prefix + 'Mode'] ||
|
||||
c[prefix + 'Style'] || c[prefix + 'CardMode'] || c[prefix + 'IndepStyle'] ||
|
||||
c[prefix + 'BlockType'] || c[prefix + 'Type'] || ''
|
||||
).trim().toLowerCase();
|
||||
if (/^(follow|card|跟随卡片)$/.test(mode)) return false;
|
||||
if (/^(block|indep|independent|standalone|独立成块)$/.test(mode)) return true;
|
||||
return !!(String(c[prefix + 'BlockBg'] || '').trim() || num(c[prefix + 'BlockRadius'], 0) || num(c[prefix + 'BlockPad'], 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 2.22 通用「独立成块」最终样式串:
|
||||
* 独立成块 → 底色/圆角/内边距 + 逐向间距(纯数值, 不用 calc())
|
||||
* 跟随卡片 → 去掉白卡底(与上方同底连为一体)
|
||||
* 注: 纯字符串拼接(不依赖组件实例), WXSS 不支持 calc()。
|
||||
*/
|
||||
function blockFinalStyle(c, prefix) {
|
||||
const custom = blockCustomSpacing(c, prefix);
|
||||
if (blockIndependent(c, prefix)) {
|
||||
const bg = String(c[prefix + 'BlockBg'] || '').trim();
|
||||
const radius = num(c[prefix + 'BlockRadius'], 0);
|
||||
const pad = num(c[prefix + 'BlockPad'], 0);
|
||||
let text = '';
|
||||
if (bg) text += 'background: ' + bg + ';';
|
||||
if (radius > 0) text += 'border-radius: ' + px2rpx(radius) + 'rpx;';
|
||||
if (pad > 0) text += 'padding: ' + px2rpx(pad) + 'rpx;';
|
||||
if (custom) text = (text + ' ' + custom).trim();
|
||||
return text;
|
||||
}
|
||||
return ('background: transparent; border: 0; box-shadow: none; border-radius: 0; padding: 0; '
|
||||
+ (custom || '')).trim();
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'diy-card-mini',
|
||||
props: {
|
||||
@@ -367,6 +433,49 @@ export default {
|
||||
showQrBottom() { return this.showQrBase && this.qrPos === 'bottom'; },
|
||||
showQrHead() { return this.showQrBase && this.qrPos === 'head'; },
|
||||
showQrCorner() { return this.showQrBase && this.qrPos === 'corner'; },
|
||||
/**
|
||||
* 2.23 二维码在卡片流内(位置由区块顺序决定)。
|
||||
* rows/bottom 两个历史档位都归入流内; head/corner 是锚点档, 不进流内。
|
||||
*/
|
||||
showQrFlow() {
|
||||
return this.showQrBase && (this.qrPos === 'flow' || this.qrPos === 'rows' || this.qrPos === 'bottom');
|
||||
},
|
||||
/**
|
||||
* 2.23 区块流顺序: 配置 c.blockOrder 优先(逗号分隔 token);
|
||||
* 缺省回落默认顺序(并按历史 actsPosition 推导快捷操作在资料区上/下);
|
||||
* 未知 token 忽略, 缺失的已知 token 按默认顺序补齐。
|
||||
*/
|
||||
blockSeq() {
|
||||
const raw = String(this.c.blockOrder || '').trim();
|
||||
const list = [];
|
||||
if (raw) {
|
||||
raw.split(',').forEach((t) => {
|
||||
const token = t.trim();
|
||||
if (token && BLOCK_TOKENS.indexOf(token) >= 0 && list.indexOf(token) < 0) list.push(token);
|
||||
});
|
||||
}
|
||||
if (!list.length) {
|
||||
const actsAbove = normalizeActsPosition(this.c.actsPosition) === 'above';
|
||||
list.push('bio');
|
||||
if (actsAbove) list.push('acts');
|
||||
list.push('rows');
|
||||
if (!actsAbove) list.push('acts');
|
||||
list.push('social', 'qr', 'meta');
|
||||
return list;
|
||||
}
|
||||
// 补齐缺失的已知 token: 插到默认顺序里它前一个已存在 token 之后
|
||||
const def = DEFAULT_BLOCK_ORDER.split(',');
|
||||
def.forEach((t) => {
|
||||
if (list.indexOf(t) >= 0) return;
|
||||
let at = -1;
|
||||
for (let i = def.indexOf(t) - 1; i >= 0; i--) {
|
||||
const p = list.indexOf(def[i]);
|
||||
if (p >= 0) { at = p + 1; break; }
|
||||
}
|
||||
if (at < 0) list.unshift(t); else list.splice(at, 0, t);
|
||||
});
|
||||
return list;
|
||||
},
|
||||
/** 头像开关(历史配置无此字段时默认显示) */
|
||||
showAvatarBlock() {
|
||||
return flagOn(this.c.showAvatar, true);
|
||||
@@ -392,6 +501,8 @@ export default {
|
||||
*/
|
||||
cornerClearStyle() {
|
||||
if (this.qrPos !== 'corner' || !this.showQrBase) return {};
|
||||
// 2.23 区块流: 右上角浮层压住的是「紧邻头部的那块」, 资料区被排到别处时不再让位
|
||||
if (this.blockSeq[0] !== 'rows') 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;
|
||||
@@ -588,7 +699,7 @@ export default {
|
||||
},
|
||||
rowsInfo() {
|
||||
if (!this.card) return { shown: [], hasMore: false };
|
||||
return visibleRows(this.card.rows, this.expanded ? 0 : this.maxRows);
|
||||
return visibleRows(this.decorateRows(this.card.rows), this.expanded ? 0 : this.maxRows);
|
||||
},
|
||||
shownRows() {
|
||||
return this.rowsInfo.shown;
|
||||
@@ -856,8 +967,15 @@ export default {
|
||||
return this.acts.length > 4 ? '20%' : '25%';
|
||||
},
|
||||
/** 快捷操作区位置(2.18): above=资料区上方(below 历史默认零变化) */
|
||||
/**
|
||||
* 2.23 快捷操作是否排在资料区之上 —— 由区块流顺序决定
|
||||
* (旧 actsPosition 开关只作兜底: 区块流缺块时才回落到它)
|
||||
*/
|
||||
actsAbove() {
|
||||
return normalizeActsPosition(this.c.actsPosition) === 'above';
|
||||
const i = this.blockSeq.indexOf('acts');
|
||||
const r = this.blockSeq.indexOf('rows');
|
||||
if (i < 0 || r < 0) return normalizeActsPosition(this.c.actsPosition) === 'above';
|
||||
return i < r;
|
||||
},
|
||||
/**
|
||||
* 快捷操作区独立块样式(2.18): 背景/圆角/内边距, 全为默认(历史形态)时输出空串零变化
|
||||
@@ -985,6 +1103,66 @@ export default {
|
||||
// 跟随卡片: 去掉 .mini-rows 静态白卡(白底/描边/阴影/圆角/内边距), 与上方资料同底连为一体
|
||||
return ('background: transparent; border: 0; box-shadow: none; border-radius: 0; padding: 0; ' + extra).trim();
|
||||
},
|
||||
// ---- 2.22 简介区块(可独立成块, 与资料区同款) ----
|
||||
bioText() {
|
||||
return String((this.card && this.card.bio) || '').trim();
|
||||
},
|
||||
showBioBlock() {
|
||||
return flagOn(this.c.showBio, false) && !!this.bioText;
|
||||
},
|
||||
bioTitleShow() {
|
||||
return flagOn(this.c.bioTitleShow, false);
|
||||
},
|
||||
bioTitle() {
|
||||
return String(this.c.bioTitle || '简介');
|
||||
},
|
||||
/** 简介最多行数(0 = 不限); 超出部分由 CSS line-clamp 截断 */
|
||||
bioMaxLines() {
|
||||
const n = num(this.c.bioMaxLines, 3);
|
||||
return n > 0 ? n : 0;
|
||||
},
|
||||
bioFinalStyle() {
|
||||
return blockFinalStyle(this.c, 'bio');
|
||||
},
|
||||
bioTextStyle() {
|
||||
const c = this.c;
|
||||
const style = {
|
||||
fontSize: px2rpx(num(c.bioSize, 13)) + 'rpx',
|
||||
color: String(c.bioColor || c.infoColor || '#888888'),
|
||||
lineHeight: String(num(c.bioLineHeight, 22) / 10),
|
||||
};
|
||||
// 多行截断行数(WXSS 支持 -webkit-line-clamp, 需与 display:-webkit-box 同用)
|
||||
const lines = this.bioMaxLines;
|
||||
if (lines > 0) style['-webkit-line-clamp'] = lines;
|
||||
return this.$util.objToStyle(style);
|
||||
},
|
||||
// ---- 2.22 社交媒体区块(可独立成块) ----
|
||||
socialList() {
|
||||
return (this.card && Array.isArray(this.card.socials)) ? this.card.socials : [];
|
||||
},
|
||||
showSocialBlock() {
|
||||
return flagOn(this.c.showSocial, false) && this.socialList.length > 0;
|
||||
},
|
||||
socialFinalStyle() {
|
||||
return blockFinalStyle(this.c, 'social');
|
||||
},
|
||||
// ---- 2.22 名片标签 ----
|
||||
cardTags() {
|
||||
return (this.card && Array.isArray(this.card.tags)) ? this.card.tags : [];
|
||||
},
|
||||
showTagsRow() {
|
||||
return flagOn(this.c.showTags, false) && this.cardTags.length > 0;
|
||||
},
|
||||
// ---- 2.22 有效期 / 来源标注 ----
|
||||
cardExpired() {
|
||||
return !!(this.card && this.card.expired);
|
||||
},
|
||||
sourceName() {
|
||||
return String((this.card && this.card.source_name) || '').trim();
|
||||
},
|
||||
showSourceRow() {
|
||||
return flagOn(this.c.showSource, false) && !!this.sourceName;
|
||||
},
|
||||
/**
|
||||
* 二维码是否「独立成块」: 后台「二维码独立样式」开关(与资料区同套逻辑)。
|
||||
* 跟随卡片 → false(与上方资料同底连为一体, 无白卡/描边/阴影);
|
||||
@@ -1060,6 +1238,48 @@ export default {
|
||||
rowTapable(row) {
|
||||
return flagOn(this.c.rowTap, true) && !!rowActOf(row);
|
||||
},
|
||||
/**
|
||||
* 2.22 地址细分: address 行用完整地址(国家/省/市 + 街道)覆盖, 邮编独立成行。
|
||||
* 与名片详情页同口径([country, province, city, address] + 邮编一行)。
|
||||
*/
|
||||
decorateRows(rows) {
|
||||
const list = Array.isArray(rows) ? rows.map(r => Object.assign({}, r)) : [];
|
||||
const full = String((this.card && this.card.address_full) || '').trim();
|
||||
const postal = String((this.card && this.card.postal_code) || '').trim();
|
||||
let addrIdx = -1;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].field === 'address') {
|
||||
addrIdx = i;
|
||||
if (full) list[i].value = full;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (postal) {
|
||||
const row = { field: 'postal_code', label: '邮编', value: postal, href: '' };
|
||||
if (addrIdx >= 0) list.splice(addrIdx + 1, 0, row);
|
||||
else list.push(row);
|
||||
}
|
||||
return list;
|
||||
},
|
||||
/** 2.22 社交媒体点击: 有链接直接打开, 无链接则复制账号 */
|
||||
onSocialTap(s) {
|
||||
const href = String((s && s.href) || '').trim();
|
||||
if (href) {
|
||||
// #ifdef H5
|
||||
window.open(href, '_blank');
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({ data: href, success: () => uni.showToast({ title: '链接已复制', icon: 'none' }) });
|
||||
// #endif
|
||||
return;
|
||||
}
|
||||
const value = String((s && s.value) || '').trim();
|
||||
if (!value) return;
|
||||
uni.setClipboardData({
|
||||
data: value,
|
||||
success: () => uni.showToast({ title: '账号已复制', icon: 'none' }),
|
||||
});
|
||||
},
|
||||
onRowTap(row) {
|
||||
const act = rowActOf(row);
|
||||
if (act) this.$emit('row-act', act);
|
||||
@@ -1068,6 +1288,14 @@ export default {
|
||||
this.$emit('act', act);
|
||||
},
|
||||
/* ---------- 分享名片抽屉(「发送名片」弹出; 版式/逻辑与精选名片 show 完全同款) ---------- */
|
||||
/**
|
||||
* 2026-10-28: 记住"正在分享的卡片" —— 微信小程序只认页面级 onShareAppMessage,
|
||||
* 组件内的 dataset 需由页面钩子读取; 朋友圈(onShareTimeline)不接受事件参数,
|
||||
* 只能靠这个 storage 通道把 card_id 带到页面钩子。
|
||||
*/
|
||||
rememberShareCard() {
|
||||
rememberShareCardId(this.card && this.card.card_id);
|
||||
},
|
||||
openCardShare() {
|
||||
this.showShare = true;
|
||||
},
|
||||
@@ -1630,6 +1858,140 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 2.22 简介区块(可独立成块, 视觉与资料区同款白卡) ---- */
|
||||
.mini-bio {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: 24rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid #EEF0F3;
|
||||
box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.08);
|
||||
padding: 24rpx;
|
||||
|
||||
.mini-bio-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.bio-title-bar {
|
||||
width: 6rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 3rpx;
|
||||
background: #2F6BFF;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.bio-title-text {
|
||||
font-size: 26rpx;
|
||||
color: #1F2329;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.mini-bio-text {
|
||||
display: block;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 多行截断(最多 bioMaxLines 行; 0 = 不限) */
|
||||
.bio-text-clamp {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 2.22 社交媒体区块 ---- */
|
||||
.mini-social {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: 24rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid #EEF0F3;
|
||||
box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.08);
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.social-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.social-label {
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.social-value {
|
||||
font-size: 26rpx;
|
||||
color: #1F2329;
|
||||
margin-top: 4rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 2.22 标签 + 来源标注 ---- */
|
||||
.mini-meta {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.mini-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mini-tag {
|
||||
font-size: 22rpx;
|
||||
color: #6B7280;
|
||||
background: #F2F4F7;
|
||||
border-radius: 999rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
margin: 0 12rpx 8rpx 0;
|
||||
}
|
||||
|
||||
.mini-source {
|
||||
font-size: 22rpx;
|
||||
color: #B0B4BC;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 2.22 过期遮罩(覆盖整卡) ---- */
|
||||
.mini-expired {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.expired-text {
|
||||
font-size: 28rpx;
|
||||
color: #888888;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
border-radius: 999rpx;
|
||||
padding: 12rpx 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.mini-rows {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
Reference in New Issue
Block a user