From a841af8f5b2522204a5fbef0ee978820f721184f Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Wed, 21 Oct 2026 11:44:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(diy-card):=20A+B=20=E8=A7=86=E8=A7=89?= =?UTF-8?q?=E5=8D=87=E7=BA=A7(=E8=B5=84=E6=96=99=E8=A1=8C=E5=9B=BE?= =?UTF-8?q?=E6=A0=87/=E5=8F=AF=E7=82=B9=20+=20=E4=BA=8C=E7=BB=B4=E7=A0=81?= =?UTF-8?q?=E5=9B=9B=E6=A1=A3=E4=BB=BB=E6=84=8F=E4=BD=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配合后端 f031476, 组件内直接复刻参考图视觉, 功能仍归详情页(路径A): - common.js: rowIconFor(field→iconfont glyph, 无字形退化空串) + rowActOf(href 派生 tel/mail/copy, 空 href 不可点) + normalizeQrPosition/QR_SIZES(四档归一化+尺寸表) - mini.vue: 资料行图标列(与文字标签列同宽, 值列左缘对齐) + 行点击 emit row-act; 二维码四档渲染: rows 行下(默认) / head 头部右侧(三段 flex 零遮挡) / corner 右上浮层(白底衬底+Logo 避让+资料行让位 108rpx) / bottom 主按钮上方; 二维码尺寸随档位(180/120/120/180) - show/self: miniConfig 透传 showRowIcons/rowTap/qrPosition, 模板挂 @row-act="doAct" 复用既有动作分发 - 新增 tests/diy-card-ab-visual.test.js 13 项(真函数+源码断言, 7 项变异验证全抓住) node --test tests/diy-card-ab-visual.test.js → 13/13 --- components-diy/diy-card-common.js | 38 ++++++++ components-diy/diy-card-mini.vue | 118 ++++++++++++++++++++--- components-diy/diy-card-self.vue | 5 + components-diy/diy-card-show.vue | 5 + tests/diy-card-ab-visual.test.js | 149 ++++++++++++++++++++++++++++++ 5 files changed, 304 insertions(+), 11 deletions(-) create mode 100644 tests/diy-card-ab-visual.test.js diff --git a/components-diy/diy-card-common.js b/components-diy/diy-card-common.js index d12c7a5..8c0f3f8 100644 --- a/components-diy/diy-card-common.js +++ b/components-diy/diy-card-common.js @@ -128,3 +128,41 @@ export function visibleRows(rows, max) { } return { shown: list, rest: [], hasMore: false }; } + +/** + * 资料行图标(参考图: 电话/微信/地址带头像, 邮箱/网站/自定义无字形→退化为文字标签) + * 口径与 pages_tool/card/detail.vue 的 fieldRows 映射一致(同 glyph 同规则) + */ +const ROW_ICONS = { + mobile: 'icon-dianhua', + phone: 'icon-dianhua', + wechat: 'icon-weixin', + address: 'icon-dizhi', +}; +export function rowIconFor(field) { + return ROW_ICONS[String(field || '')] || ''; +} + +/** + * 资料行点击动作: 由服务端下发的 href 派生(tel:/mailto:/url/空) + * tel: → 拨号 mailto: → 邮箱(H5) / 复制(其它端) + * http(s) → 复制链接 空 → 无动作(纯展示, 如地址/自定义) + * 返回 null 表示不可点; type 取值与父组件 doAct 的 tel/mail/copy 分支对齐 + */ +export function rowActOf(row) { + const href = String((row && row.href) || '').trim(); + const value = String((row && row.value) || '').trim(); + if (!href || !value) return null; + if (/^tel:/i.test(href)) return { type: 'tel', value: value }; + if (/^mailto:/i.test(href)) return { type: 'mail', value: value }; + return { type: 'copy', value: value }; +} + +/** 二维码位置(四档): rows 资料行下方(默认) / head 头部右侧 / corner 右上角浮层 / bottom 主按钮上方 */ +export const QR_POSITIONS = ['rows', 'head', 'corner', 'bottom']; +export function normalizeQrPosition(v) { + const s = String(v || '').trim(); + return QR_POSITIONS.indexOf(s) >= 0 ? s : 'rows'; +} +/** 各档位的二维码边长(rpx): 头部右侧紧凑, 浮层带衬底, 行下/底部大图便于扫 */ +export const QR_SIZES = { rows: 180, head: 120, corner: 120, bottom: 180 }; diff --git a/components-diy/diy-card-mini.vue b/components-diy/diy-card-mini.vue index ce8ff18..c9e8819 100644 --- a/components-diy/diy-card-mini.vue +++ b/components-diy/diy-card-mini.vue @@ -24,7 +24,7 @@ - + @@ -36,9 +36,15 @@ - + + + + + {{ qrTip || '扫码查看完整名片' }} + + @@ -52,12 +58,19 @@ {{ card.card_title }} {{ subLine }} + + + + {{ qrTip || '扫码查看完整名片' }} + - + - - {{ row.label }} + + + {{ row.label }} {{ row.value }} @@ -65,6 +78,12 @@ + + + + {{ qrTip || '扫码查看完整名片' }} + + - - - + + + {{ qrTip || '扫码查看完整名片' }} @@ -92,6 +111,7 @@ // 消费名片自身设计资产: cover 封面 / logo / theme_color / 渐变末色与角度 / card_title import { CardTokens, px2rpx, flagOn, num, initialOf, avatarBg, themeGradient, visibleRows, + rowIconFor, rowActOf, normalizeQrPosition, QR_SIZES, } from './diy-card-common.js'; export default { @@ -126,7 +146,8 @@ export default { return flagOn(this.c.showGradient, false); }, showLogo() { - return flagOn(this.c.showLogo, false); + // corner 档二维码占右上同一位, Logo 自动避让(面板有提示) + return flagOn(this.c.showLogo, false) && this.qrPos !== 'corner'; }, showActs() { return flagOn(this.c.showQuick, true); @@ -135,8 +156,20 @@ export default { // 资料行总开关(面板「显示资料行」); 历史配置无此字段时默认显示, 兼容 1.0 return flagOn(this.c.showRows, true); }, - showQr() { - return flagOn(this.c.showQr, false); + /** 二维码总条件: 开关 + 名片有二维码地址 */ + showQrBase() { + return flagOn(this.c.showQr, false) && !!(this.card && this.card.qr_url); + }, + qrPos() { + return normalizeQrPosition(this.c.qrPosition); + }, + 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'; }, + qrImgStyle() { + const s = QR_SIZES[this.qrPos]; + return this.$util.objToStyle({ width: s + 'rpx', height: s + 'rpx' }); }, showDetailBtn() { return flagOn(this.c.showDetailBtn, true); @@ -239,6 +272,18 @@ export default { }, }, methods: { + /** 资料行图标(开关关时退化为文字标签; field 无字形同样退化) */ + rowIcon(row) { + return flagOn(this.c.showRowIcons, true) ? rowIconFor(row && row.field) : ''; + }, + /** 资料行是否可点: 行点击开关 + 服务端 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); }, @@ -397,11 +442,24 @@ export default { font-size: 24rpx; } + /* 图标列与文字标签列同宽, 保证值列左缘对齐(参考图口径) */ + .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 { @@ -455,6 +513,44 @@ export default { 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 档: 右上角浮层, 白底衬底保证封面/纯色上都可扫 */ + &.mini-qr-corner { + position: absolute; + top: 20rpx; + right: 20rpx; + z-index: 3; + margin-top: 0; + background: #ffffff; + border-radius: 12rpx; + padding: 10rpx; + box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.12); + } + } + + /* corner 档: 浮层二维码(120rpx+衬底+tip)下探约 202rpx, 资料行/操作区让位防遮挡 */ + &.qr-at-corner { + .mini-rows, + .mini-acts { + margin-top: 108rpx; + } + + .mini-head-info { + max-width: calc(100% - 170rpx); + } } .mini-detail { diff --git a/components-diy/diy-card-self.vue b/components-diy/diy-card-self.vue index d149192..0a9d48b 100644 --- a/components-diy/diy-card-self.vue +++ b/components-diy/diy-card-self.vue @@ -15,6 +15,7 @@ :config="miniConfig" :loading="loading" @act="doAct" + @row-act="doAct" @detail="goDetail" >