chore:二维码
This commit is contained in:
@@ -42,9 +42,9 @@
|
||||
<image v-if="showLogo && card.logo" class="mini-logo" :src="$util.img(card.logo)" mode="aspectFit" />
|
||||
|
||||
<!-- 二维码·右上角浮层(corner 档: 外观由 qrStyle 决定, 资料行自动让位; 点击预览名片) -->
|
||||
<view v-if="showQrCorner" class="mini-qr mini-qr-corner" :style="qrBlockStyle" @tap.stop="onQrTap">
|
||||
<image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" />
|
||||
<text v-if="showQrTip" class="qr-tip" :style="labelStyle"><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="showQrCorner" class="mini-qr mini-qr-corner" :style="qrBlockStyle">
|
||||
<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>
|
||||
|
||||
<!-- 头部·主题渐变整块档(theme): 渐变通铺卡边 + 装饰圆, plain 档不渲染 -->
|
||||
@@ -70,8 +70,8 @@
|
||||
</view>
|
||||
<!-- 二维码·头部右侧(head 档: 头像|信息|二维码 三段 flex, 零遮挡) -->
|
||||
<view v-if="showQrHead" class="mini-qr mini-qr-head" :style="qrBlockStyle">
|
||||
<image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" />
|
||||
<text v-if="showQrTip" class="qr-tip" :style="labelStyle"><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 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>
|
||||
</view>
|
||||
</view>
|
||||
@@ -95,9 +95,9 @@
|
||||
<text v-if="subLine" class="mini-sub" :style="subTextStyle">{{ subLine }}</text>
|
||||
</view>
|
||||
<!-- 二维码·头部右侧(head 档: 头像|信息|二维码 三段 flex, 零遮挡; 点击预览名片) -->
|
||||
<view v-if="showQrHead" class="mini-qr mini-qr-head" :style="qrBlockStyle" @tap.stop="onQrTap">
|
||||
<image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" />
|
||||
<text v-if="showQrTip" class="qr-tip" :style="labelStyle"><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="showQrHead" class="mini-qr mini-qr-head" :style="qrBlockStyle">
|
||||
<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>
|
||||
</view>
|
||||
|
||||
@@ -151,9 +151,9 @@
|
||||
</view>
|
||||
|
||||
<!-- 二维码·资料行下方(rows 档, 默认; 点击预览名片); mini-qr-center: 块级居中 -->
|
||||
<view v-if="showQrRows" class="mini-qr mini-qr-center" :style="qrBlockFinalStyle" @tap.stop="onQrTap">
|
||||
<image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" />
|
||||
<text v-if="showQrTip" class="qr-tip" :style="labelStyle"><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="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>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作·图标宫格档(grid): 彩色浅底图标 + 下方文字 -->
|
||||
@@ -179,9 +179,9 @@
|
||||
</view>
|
||||
|
||||
<!-- 二维码·主按钮上方(bottom 档; 点击预览名片) -->
|
||||
<view v-if="showQrBottom" class="mini-qr mini-qr-center" :style="qrBlockFinalStyle" @tap.stop="onQrTap">
|
||||
<image class="qr-img" :style="qrImgStyle" :src="$util.img(qrSrc)" mode="aspectFit" />
|
||||
<text v-if="showQrTip" class="qr-tip" :style="labelStyle"><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="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>
|
||||
|
||||
<!-- 发名片(对齐名片详情页: 主题色大按钮; 点击跳原生转发中转页拉起微信转发面板) -->
|
||||
@@ -374,14 +374,14 @@ export default {
|
||||
}
|
||||
return this.$util.objToStyle(style);
|
||||
},
|
||||
/** 二维码图样式: 尺寸 + 外观衬底(plain 原图 / card 白卡 / theme 主题色 / gradient 渐变) + 微调位移(只动图, 白卡容器不动) */
|
||||
qrImgStyle() {
|
||||
const size = resolveQrSize(this.qrPos, this.c.qrSize);
|
||||
const style = {
|
||||
width: size + 'rpx',
|
||||
height: size + 'rpx',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
/**
|
||||
* 二维码衬底 · 外层框(qrStyle 装饰 + 微调位移都在这里)
|
||||
* 真实机长按「识别图中二维码」取的是这张图自身的渲染像素:
|
||||
* 主题色衬底/白卡边/内边距/transform 全部包在 <image> 上时微信会解码失败
|
||||
* (开发者工具按原始 src 判定所以必出, 真机必失败 —— 2026-10-26 定位)
|
||||
*/
|
||||
qrFrameStyle() {
|
||||
const style = {};
|
||||
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
|
||||
if (this.qrStyle === 'card') {
|
||||
style.background = '#ffffff';
|
||||
@@ -398,11 +398,29 @@ export default {
|
||||
style.borderRadius = '16rpx';
|
||||
style.padding = '6rpx';
|
||||
}
|
||||
// 微调位移(水平/垂直)只作用于二维码图本身, 外层独立白卡(.mini-qr-center)保持不动
|
||||
// 微调位移(水平/垂直)作用于整个二维码块, 外层独立白卡(.mini-qr-center)保持不动
|
||||
const transform = qrTransform(this.c.qrOffsetX, this.c.qrOffsetY);
|
||||
if (transform) style.transform = transform;
|
||||
return this.$util.objToStyle(style);
|
||||
},
|
||||
/** 衬底内边距数值(rpx): card/theme=10, gradient=6, plain=0(与 qrFrameStyle 同口径) */
|
||||
qrPad() {
|
||||
return this.qrStyle === 'plain' ? 0 : (this.qrStyle === 'gradient' ? 6 : 10);
|
||||
},
|
||||
/**
|
||||
* 二维码图: 只承载干净二维码像素(尺寸已扣除衬底内边距, 视觉与改造前完全一致),
|
||||
* 不带任何背景/边框/位移, 保证微信真机长按可识别
|
||||
*/
|
||||
qrImgStyle() {
|
||||
const size = resolveQrSize(this.qrPos, this.c.qrSize);
|
||||
const inner = size - this.qrPad * 2;
|
||||
const style = {
|
||||
width: inner + 'rpx',
|
||||
height: inner + 'rpx',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
return this.$util.objToStyle(style);
|
||||
},
|
||||
showDetailBtn() {
|
||||
return flagOn(this.c.showDetailBtn, true);
|
||||
},
|
||||
@@ -1017,7 +1035,11 @@ export default {
|
||||
// 小程序/鸿蒙: 下载二维码后存相册(长按图片也可存, 抽屉底部有提示)
|
||||
saveCardQr() {
|
||||
const qrUrl = (this.card && this.card.qr_url) || '';
|
||||
if (!qrUrl) return;
|
||||
if (!qrUrl) {
|
||||
// 不再静默返回: qr_url 为空说明后端 card/detail 未返回二维码地址, 必须可见否则无法定位
|
||||
uni.showToast({ title: '名片二维码未生成(接口未返回 qr_url)', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.downloadFile({
|
||||
url: this.$util.img(qrUrl),
|
||||
success: res => {
|
||||
@@ -1034,12 +1056,105 @@ export default {
|
||||
fail: () => uni.showToast({ title: '二维码下载失败', icon: 'none' }),
|
||||
});
|
||||
},
|
||||
// 分享抽屉二维码: 点击放大预览(预览页长按必出「识别图中二维码」菜单, 比 image 直接长按更可靠)
|
||||
// 分享抽屉二维码: 点击放大预览(同一张透明底码, 统一走垫白底通路)
|
||||
previewQr() {
|
||||
const url = (this.card && this.card.qr_url) || '';
|
||||
if (!url) return;
|
||||
const src = this.$util.img(url);
|
||||
uni.previewImage({ urls: [src], current: src });
|
||||
this.openQrPreview(this.$util.img(url));
|
||||
},
|
||||
/**
|
||||
* 名片二维码识别兜底通路(真机可用):
|
||||
* 真机实锤(2026-10-27 预览页长按菜单只有转发/保存/收藏/翻译, 无「识别图中二维码」):
|
||||
* 后端 qr 接口生成的二维码 PNG 是透明底 —— 白卡上看着正常, 但微信解码器把透明当深色,
|
||||
* 页面/预览页长按都解不出码。故先下载垫白底再预览, 微信即可解码(根治仍需后端出白底码)。
|
||||
*/
|
||||
previewQrImg() {
|
||||
if (!this.qrSrc) {
|
||||
uni.showToast({ title: '暂无可识别的二维码', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.openQrPreview(this.$util.img(this.qrSrc));
|
||||
},
|
||||
/** 二维码预览统一通路: 下载到本地(规避真机域名校验) → 垫白底 → 预览; 任一步失败回退原图预览 */
|
||||
openQrPreview(url) {
|
||||
if (!url) return;
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success: res => {
|
||||
if (res.statusCode === 200 && res.tempFilePath) {
|
||||
this.previewQrOnWhite(res.tempFilePath);
|
||||
} else {
|
||||
uni.previewImage({ urls: [url], current: url });
|
||||
}
|
||||
},
|
||||
fail: () => uni.previewImage({ urls: [url], current: url }),
|
||||
});
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.previewImage({ urls: [url], current: url });
|
||||
// #endif
|
||||
},
|
||||
/** 透明底二维码垫白底: 离屏 canvas 白底 + 四周留白重绘 → 导出临时图 → 预览(微信解码必需白底留白) */
|
||||
previewQrOnWhite(tempPath) {
|
||||
try {
|
||||
const probe = wx.createOffscreenCanvas({ type: '2d', width: 10, height: 10 });
|
||||
const img = probe.createImage();
|
||||
img.onload = () => {
|
||||
try {
|
||||
const w = img.width || 640;
|
||||
const h = img.height || 640;
|
||||
const side = Math.max(w, h);
|
||||
const pad = Math.round(side * 0.06); // 四周留白, 微信解码必要条件
|
||||
const size = side + pad * 2;
|
||||
const cv = wx.createOffscreenCanvas({ type: '2d', width: size, height: size });
|
||||
const ctx = cv.getContext('2d');
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
const scale = Math.min(size / w, size / h) * 0.88;
|
||||
const dw = w * scale;
|
||||
const dh = h * scale;
|
||||
ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
|
||||
const done = file => {
|
||||
console.log('[qr] 白底垫底成功 →', file);
|
||||
uni.previewImage({ urls: [file], current: file });
|
||||
};
|
||||
const fallbackRaw = tag => {
|
||||
console.warn('[qr] 垫白底失败(' + tag + '), 回退原图预览');
|
||||
uni.previewImage({ urls: [tempPath], current: tempPath });
|
||||
};
|
||||
// 通路1: canvasToTempFilePath; 通路2(更兼容): toDataURL + 文件系统写本地 png
|
||||
wx.canvasToTempFilePath({
|
||||
canvas: cv,
|
||||
fileType: 'png',
|
||||
success: r => done(r.tempFilePath),
|
||||
fail: err => {
|
||||
console.warn('[qr] canvasToTempFilePath 失败:', err && err.errMsg);
|
||||
try {
|
||||
const base64 = cv.toDataURL('image/png').replace(/^data:image\/\w+;base64,/, '');
|
||||
const file = wx.env.USER_DATA_PATH + '/qr_white_' + Date.now() + '.png';
|
||||
wx.getFileSystemManager().writeFile({
|
||||
filePath: file,
|
||||
data: base64,
|
||||
encoding: 'base64',
|
||||
success: () => done(file),
|
||||
fail: e2 => fallbackRaw('writeFile ' + (e2 && e2.errMsg)),
|
||||
});
|
||||
} catch (e3) {
|
||||
fallbackRaw('toDataURL ' + (e3 && e3.message));
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[qr] 垫白底异常:', e && e.message);
|
||||
uni.previewImage({ urls: [tempPath], current: tempPath });
|
||||
}
|
||||
};
|
||||
img.onerror = () => uni.previewImage({ urls: [tempPath], current: tempPath });
|
||||
img.src = tempPath;
|
||||
} catch (e) {
|
||||
uni.previewImage({ urls: [tempPath], current: tempPath });
|
||||
}
|
||||
},
|
||||
/** 二维码点击 → 预览名片(进详情页; qrTapPreview 开关控制, 默认开) */
|
||||
onQrTap() {
|
||||
|
||||
Reference in New Issue
Block a user