From a1d8ee8d7aaa83f672df7aafb50a9c0fd2f603fe Mon Sep 17 00:00:00 2001
From: jing <1683105490@qq.com>
Date: Wed, 21 Oct 2026 17:32:30 +0800
Subject: [PATCH] =?UTF-8?q?chore=EF=BC=9A=E4=BA=8C=E7=BB=B4=E7=A0=81?=
=?UTF-8?q?=E7=B2=BE=E9=80=89=E5=90=8D=E7=89=87=E5=90=8E=E5=8F=B0=E6=8E=A7?=
=?UTF-8?q?=E5=88=B6=E5=BC=80=E5=90=AF=E6=98=BE=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
components-diy/diy-card-show.vue | 291 ++++++++++++++++++++++++++++-
tests/diy-card-ab-visual.test.js | 6 +
tests/diy-card-detail-jump.test.js | 4 +-
tests/diy-card-qr-style.test.js | 30 ++-
4 files changed, 318 insertions(+), 13 deletions(-)
diff --git a/components-diy/diy-card-show.vue b/components-diy/diy-card-show.vue
index 363f37c..ba99689 100644
--- a/components-diy/diy-card-show.vue
+++ b/components-diy/diy-card-show.vue
@@ -30,14 +30,14 @@
{{ card.name || '未命名名片' }}
{{ jobLine }}
-
+
- 预览名片
+ {{ qrTipText }}
-
-
+
+
{{ expanded ? '收起资料' : '展开全部资料(' + hiddenCount + ')' }}
+
+
+
+
+ {{ act.label }}
+
+
@@ -57,6 +66,32 @@
发名片
+
+
+
+
+
+ 分享名片
+ 客户打开链接即可查看名片并留言留资
+
+
+
+ {{ (card && card.wap_url) || '' }}
+
+ 复制链接
+
+ 浏览器分享
+
+
+ 保存二维码
+
+
+
+ 长按二维码也可保存到相册
+ 长按二维码可保存到相册
+ 点击「保存二维码」存入相册
+
+
@@ -67,6 +102,7 @@
// 图标资料行 + 卡外「发名片」胶囊。面板仅颜色/底色/圆角/阴影/行数继续生效。
import {
CardTokens, px2rpx, num, flagOn, initialOf, avatarBg, themeGradient, visibleRows, rowIconFor, rowActOf,
+ resolveQrSize,
} from './diy-card-common.js';
export default {
@@ -79,6 +115,7 @@ export default {
card: null,
loading: false,
expanded: false,
+ showShare: false,
};
},
computed: {
@@ -102,6 +139,54 @@ export default {
showAvatarBlock() {
return flagOn(this.c.showAvatar, true);
},
+ /** 资料行总开关: 面板「显示资料行」(历史配置无此字段默认显示; 关后整块隐藏) */
+ showRowsBlock() {
+ return flagOn(this.c.showRows, true);
+ },
+ /** 二维码开关: 面板「显示二维码」(历史配置无此字段默认显示; 关后头部只留姓名/职位) */
+ showQrBlock() {
+ return flagOn(this.c.showQr, true);
+ },
+ /** 二维码提示文字: 面板 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 : [];
+ },
+ actPrimaryStyle() {
+ const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
+ return this.$util.objToStyle({
+ background: theme,
+ border: '2rpx solid ' + theme,
+ });
+ },
+ actGhostStyle() {
+ const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
+ return this.$util.objToStyle({
+ background: 'transparent',
+ border: '2rpx solid ' + theme,
+ });
+ },
+ actPrimaryTextStyle() {
+ return this.$util.objToStyle({ color: '#FFFFFF' });
+ },
+ actGhostTextStyle() {
+ return this.$util.objToStyle({ color: (this.card && this.card.theme_color) || CardTokens.color.primary });
+ },
avatarStyle() {
return this.$util.objToStyle({ background: avatarBg(this.card) });
},
@@ -110,9 +195,11 @@ export default {
const card = this.card || {};
return card.job_title || card.company || card.card_title || '';
},
- /** 二维码边长: 面板自定义 qrSize(px) > 0 时生效, 否则固定 48px */
+ /** 二维码边长: 面板 qrSize > 0 用自定义(px), 0/空 = 「按位自动」走头部右侧档(120rpx)。
+ * 之前用 num(qrSize, 48) 兜底 —— 面板默认 0 是合法数字不触发默认值,
+ * 算出 0rpx 宽高, 二维码图被渲染成 0 尺寸「看不见」 */
qrSide() {
- return px2rpx(num(this.c.qrSize, 48));
+ return resolveQrSize('head', this.c.qrSize);
},
qrImgStyle() {
return this.$util.objToStyle({
@@ -233,6 +320,59 @@ export default {
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' })
+ });
+ },
/**
* 发名片: 跳原生转发中转页 /pages_tool/card/forward(内含 open-type=share 按钮,
* 直接拉起微信转发面板, 接收方落地 card/share 的 web-view 名片页)。
@@ -411,6 +551,29 @@ export default {
}
}
}
+
+ /* 快捷操作芯片: 高度/圆角对齐参考图(首个填充, 其余描边) */
+ .card-acts {
+ margin-top: 32rpx;
+ display: flex;
+ flex-wrap: wrap;
+
+ .act-chip {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ height: 64rpx;
+ padding: 0 30rpx;
+ margin: 0 16rpx 16rpx 0;
+ border-radius: 32rpx;
+
+ .act-text {
+ font-size: 24rpx;
+ font-weight: 500;
+ }
+ }
+ }
}
/* 发名片: 卡片外下方胶囊(背景色由 sendBtnStyle 内联) */
@@ -477,6 +640,122 @@ export default {
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;
diff --git a/tests/diy-card-ab-visual.test.js b/tests/diy-card-ab-visual.test.js
index 4b2a3a9..2d11ff8 100644
--- a/tests/diy-card-ab-visual.test.js
+++ b/tests/diy-card-ab-visual.test.js
@@ -144,6 +144,12 @@ test('show: 自绘资料行(图标/文字标签退化 + href 派生动作 + doAc
assert.match(showSrc, /onRowTap\(row\)\s*\{[\s\S]*?this\.doAct\(act\)/);
});
+test('show: 资料行总开关(面板「显示资料行」, 默认显示, 关后整块隐藏)', () => {
+ assert.match(showSrc, /showRowsBlock\(\)\s*\{\s*return flagOn\(this\.c\.showRows,\s*true\)/);
+ // 模板资料行块受开关控制(与头像开关同口径: v-if 直接消费)
+ assert.match(showSrc, /v-if="showRowsBlock && shownRows\.length" class="card-rows"/);
+});
+
// =============================================================
// 反例(防恒真)
// =============================================================
diff --git a/tests/diy-card-detail-jump.test.js b/tests/diy-card-detail-jump.test.js
index 08966bf..8fd430d 100644
--- a/tests/diy-card-detail-jump.test.js
+++ b/tests/diy-card-detail-jump.test.js
@@ -57,8 +57,8 @@ test('self: miniConfig 透传 showDetailBtn 与 detailBtnText', () => {
assert.match(selfSrc, /detailBtnText:\s*v\.detailBtnText/);
});
-test('show: 自绘二维码块挂 @tap.stop="goDetail(card)"(点「预览名片」进详情)', () => {
- assert.match(showSrc, /@tap\.stop="goDetail\(card\)"/);
+test('show: 自绘二维码块挂 @tap.stop="openCardShare"(点二维码弹「分享名片」抽屉)', () => {
+ assert.match(showSrc, /@tap\.stop="openCardShare"/);
});
// =============================================================
diff --git a/tests/diy-card-qr-style.test.js b/tests/diy-card-qr-style.test.js
index 59badc9..936adad 100644
--- a/tests/diy-card-qr-style.test.js
+++ b/tests/diy-card-qr-style.test.js
@@ -189,13 +189,33 @@ test('show: 头像开关(面板「显示头像」, 默认显示, 关后头部只
assert.ok(!showSrc.includes(' {
+test('show: 快捷操作开关(面板「显示快捷操作」, 默认显示, 关后芯片整块隐藏)', () => {
+ assert.match(showSrc, /showActsBlock\(\)\s*\{\s*return flagOn\(this\.c\.showQuick,\s*true\)/);
+ assert.match(showSrc, /v-if="showActsBlock && acts\.length" class="card-acts"/);
+ // 芯片点击直连 doAct(自绘版式不再经 $emit('act') 中转)
+ assert.match(showSrc, /@tap\.stop="doAct\(act\)"/);
+});
+
+test('show: 自绘版式自带小二维码(开关可关 + 点击进详情, 尺寸/提示文字面板可调)', () => {
assert.match(showSrc, /class="card-qr"/);
assert.match(showSrc, /\$util\.img\(card\.qr_url\)/);
- assert.match(showSrc, /预览名片/);
- assert.match(showSrc, /@tap\.stop="goDetail\(card\)"/);
- // 尺寸仍消费面板 qrSize(默认 48px)
- assert.match(showSrc, /qrSide\(\)\s*\{\s*return px2rpx\(num\(this\.c\.qrSize,\s*48\)\)/);
+ // 二维码块受面板「显示二维码」开关控制(与头像/资料行/快捷操作同口径: flagOn 默认显示)
+ assert.match(showSrc, /showQrBlock\(\)\s*\{\s*return flagOn\(this\.c\.showQr,\s*true\)/);
+ assert.match(showSrc, /v-if="showQrBlock && card\.qr_url" class="card-qr"/);
+ // 提示文字消费面板 qrTip, 未配置回落「扫码查看完整名片」(与 mini 基件同口径)
+ assert.match(showSrc, /qrTipText\(\)\s*\{\s*return this\.c\.qrTip \|\| '扫码查看完整名片';/);
+ // 点二维码弹「分享名片」抽屉(复刻 detail.vue: 码 + 链接 + 复制链接/浏览器分享/保存二维码)
+ assert.match(showSrc, /@tap\.stop="openCardShare"/);
+ assert.match(showSrc, /v-if="showShare" @tap="closeShare"/);
+ assert.match(showSrc, /class="cs-sheet" :class="\{ 'cs-sheet-show': showShare \}"/);
+ assert.match(showSrc, /分享名片<\/view>/);
+ assert.match(showSrc, /客户打开链接即可查看名片并留言留资/);
+ assert.match(showSrc, /copyCardLink\(\)\s*\{\s*const url = \(this\.card && this\.card\.wap_url\) \|\| '';/);
+ // 尺寸走 resolveQrSize: 面板 qrSize>0 用自定义, 0/空 = 自动档(head 120rpx)。
+ // 反例形态: 不允许 num(qrSize, 48) 兜底 —— 面板默认 0 是合法数字, 会算出 0rpx 宽高,
+ // 二维码图渲染成 0 尺寸「看不见」(2026-10-21 线上问题)
+ assert.match(showSrc, /qrSide\(\)\s*\{\s*return resolveQrSize\('head',\s*this\.c\.qrSize\)/);
+ assert.ok(!showSrc.includes("num(this.c.qrSize"));
});
// =============================================================