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")); }); // =============================================================