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() {
|
||||
|
||||
19
pages.json
19
pages.json
@@ -1220,20 +1220,11 @@
|
||||
"diy-text": "view",
|
||||
"diy-video": "view",
|
||||
"diy-video-list": "view"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "webview/webview",
|
||||
"style": {
|
||||
// #ifdef APP-PLUS
|
||||
"navigationStyle": "custom",
|
||||
// #endif
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "index/diy",
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "index/diy",
|
||||
"style": {
|
||||
// #ifdef APP-PLUS
|
||||
"navigationStyle": "custom",
|
||||
|
||||
@@ -769,7 +769,11 @@ export default {
|
||||
});
|
||||
},
|
||||
saveQr() {
|
||||
if (!this.qrUrl) return;
|
||||
if (!this.qrUrl) {
|
||||
// 不再静默返回: qr_url 为空说明后端接口未返回二维码地址, 提示出来便于定位
|
||||
uni.showToast({ title: '名片二维码未生成(接口未返回 qr_url)', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.downloadFile({
|
||||
url: this.qrUrl,
|
||||
success: res => {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<template>
|
||||
<view :style="themeColor">
|
||||
<!-- #ifndef MP-HARMONY -->
|
||||
<view class="iconfont iconshang navigate-back" @click="navigateBack"></view>
|
||||
<!-- #endif -->
|
||||
<web-view :src="src" @titleUpdate="updateNavbarTitle"></web-view>
|
||||
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<cover-view class="iconfont iconshang navigate-back" @click="navigateBack"></cover-view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 隐私弹窗 -->
|
||||
<privacy-popup ref="privacyPopup"></privacy-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { decodeEntity } from '@/common/js/util.js';
|
||||
|
||||
|
||||
/**
|
||||
* 必须真机调试
|
||||
* 在微信模拟器中,行为和真机不一样
|
||||
* 1. 模拟器中,设置标题不生效,但是,在真机是可以的。
|
||||
*/
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
src: '',
|
||||
title: '', // 标题栏标题
|
||||
enableSetTitle: false, // 是否强制设置标题,用来替换Webview中的默认标题
|
||||
};
|
||||
},
|
||||
onLoad(option) {
|
||||
this.title = decodeURIComponent(option?.title ?? '');
|
||||
this.enableSetTitle = decodeURIComponent(option?.enable_set_title) == 1;
|
||||
|
||||
// #ifdef H5
|
||||
this.src = decodeEntity(decodeURIComponent(option.src));
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
this.src = decodeEntity(decodeURIComponent(`${option.src}&from=mp-webview`));
|
||||
// #endif
|
||||
},
|
||||
onShow() {
|
||||
this.updateNavbarTitle();
|
||||
},
|
||||
watch: {
|
||||
title(newVal) {
|
||||
this.updateNavbarTitle(newVal);
|
||||
},
|
||||
enableSetTitle() {
|
||||
this.updateNavbarTitle();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
navigateBack() {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
});
|
||||
},
|
||||
updateNavbarTitle(newTitle) {
|
||||
const title = newTitle ?? this.title;
|
||||
if (this.enableSetTitle) {
|
||||
uni.setNavigationBarTitle({title});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.navigate-back {
|
||||
position: absolute;
|
||||
top: 34rpx;
|
||||
left: 34rpx;
|
||||
z-index: 5;
|
||||
font-size: $font-size-toolbar;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user