chore:二维码精选名片后台控制开启显示

This commit is contained in:
2026-10-21 17:32:30 +08:00
parent 724e7ac134
commit a1d8ee8d7a
4 changed files with 318 additions and 13 deletions

View File

@@ -30,14 +30,14 @@
<text class="card-name" :style="nameStyle">{{ card.name || '未命名名片' }}</text>
<text v-if="jobLine" class="card-job" :style="subStyle">{{ jobLine }}</text>
</view>
<view v-if="card.qr_url" class="card-qr" hover-class="btn-press" @tap.stop="goDetail(card)">
<view v-if="showQrBlock && card.qr_url" class="card-qr" hover-class="btn-press" @tap.stop="openCardShare">
<image class="qr-img" :style="qrImgStyle" :src="$util.img(card.qr_url)" mode="aspectFit" />
<text class="qr-tip" :style="subStyle">预览名片</text>
<text class="qr-tip" :style="subStyle">{{ qrTipText }}</text>
</view>
</view>
<!-- 资料行(有字形字段显图标, 无字形退化为文字标签; href 派生点击动作) -->
<view v-if="shownRows.length" class="card-rows">
<!-- 资料行(面板「显示资料行」可关; 有字形字段显图标, 无字形退化为文字标签; href 派生点击动作) -->
<view v-if="showRowsBlock && shownRows.length" class="card-rows">
<view v-for="(row, i) in shownRows" :key="i" class="card-row"
:class="{ 'row-tappable': rowTapable(row) }"
:hover-class="rowTapable(row) ? 'btn-press' : 'none'"
@@ -50,6 +50,15 @@
<text class="toggle-text" :style="themeTextStyle">{{ expanded ? '收起资料' : '展开全部资料(' + hiddenCount + ')' }}</text>
</view>
</view>
<!-- 快捷操作(面板「显示快捷操作」可关; 服务端按资料派生, 首个主动作填充) -->
<view v-if="showActsBlock && acts.length" class="card-acts">
<view v-for="(act, i) in acts" :key="i" class="act-chip"
:style="i === 0 ? actPrimaryStyle : actGhostStyle"
hover-class="btn-press" @tap.stop="doAct(act)">
<text class="act-text" :style="i === 0 ? actPrimaryTextStyle : actGhostTextStyle">{{ act.label }}</text>
</view>
</view>
</view>
<!-- 发名片: 卡片外下方胶囊(详情页 send-bar 同款观感, 主题渐变色) -->
@@ -57,6 +66,32 @@
<text class="show-send-text">发名片</text>
</view>
</view>
<!-- 分享名片抽屉: 点二维码弹出(pages_tool/card/detail.vue 同款版式/文案) -->
<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" />
</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>
@@ -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;

View File

@@ -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"/);
});
// =============================================================
// 反例(防恒真)
// =============================================================

View File

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

View File

@@ -189,13 +189,33 @@ test('show: 头像开关(面板「显示头像」, 默认显示, 关后头部只
assert.ok(!showSrc.includes('<view class="card-avatar"'));
});
test('show: 自绘版式自带小二维码(预览名片 + 点击进详情, 尺寸面板可调)', () => {
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 class="ss-title">分享名片<\/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"));
});
// =============================================================