生成图片方法更改

This commit is contained in:
chenhao 2026-05-21 09:09:54 +08:00
parent 5b37d7962d
commit 4a992c9847
19 changed files with 563 additions and 777 deletions

View File

@ -12,7 +12,6 @@
<body>
<div id="app" v-cloak></div>
<script src="/js/echarts.min.js"></script>
<script src="/js/html2canvas.min.js"></script>
<script type="module" src="/src/main.js"></script>
<style>
[v-cloak] {

7
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": {
"@vant/area-data": "^2.1.0",
"@zxing/library": "^0.21.3",
"html-to-image": "^1.11.13",
"image-conversion": "^2.1.1",
"pinia": "^3.0.4",
"postcss-px-to-viewport-8-plugin": "^1.2.5",
@ -822,6 +823,12 @@
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/html-to-image": {
"version": "1.11.13",
"resolved": "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.13.tgz",
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",

View File

@ -11,6 +11,7 @@
"dependencies": {
"@vant/area-data": "^2.1.0",
"@zxing/library": "^0.21.3",
"html-to-image": "^1.11.13",
"image-conversion": "^2.1.1",
"pinia": "^3.0.4",
"postcss-px-to-viewport-8-plugin": "^1.2.5",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,235 +0,0 @@
/*
* Canvas2Image v0.1
* Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk
* MIT License [http://www.opensource.org/licenses/mit-license.php]
*/
var Canvas2Image = (function() {
// check if we have canvas support
var bHasCanvas = false;
var oCanvas = document.createElement("canvas");
if (oCanvas.getContext("2d")) {
bHasCanvas = true;
}
// no canvas, bail out.
if (!bHasCanvas) {
return {
saveAsBMP : function(){},
saveAsPNG : function(){},
saveAsJPEG : function(){}
}
}
var bHasImageData = !!(oCanvas.getContext("2d").getImageData);
var bHasDataURL = !!(oCanvas.toDataURL);
var bHasBase64 = !!(window.btoa);
var strDownloadMime = "image/octet-stream";
// ok, we're good
var readCanvasData = function(oCanvas) {
var iWidth = parseInt(oCanvas.width);
var iHeight = parseInt(oCanvas.height);
return oCanvas.getContext("2d").getImageData(0,0,iWidth,iHeight);
}
// base64 encodes either a string or an array of charcodes
var encodeData = function(data) {
var strData = "";
if (typeof data == "string") {
strData = data;
} else {
var aData = data;
for (var i=0;i<aData.length;i++) {
strData += String.fromCharCode(aData[i]);
}
}
return btoa(strData);
}
// creates a base64 encoded string containing BMP data
// takes an imagedata object as argument
var createBMP = function(oData) {
var aHeader = [];
var iWidth = oData.width;
var iHeight = oData.height;
aHeader.push(0x42); // magic 1
aHeader.push(0x4D);
var iFileSize = iWidth*iHeight*3 + 54; // total header size = 54 bytes
aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256);
aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256);
aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256);
aHeader.push(iFileSize % 256);
aHeader.push(0); // reserved
aHeader.push(0);
aHeader.push(0); // reserved
aHeader.push(0);
aHeader.push(54); // dataoffset
aHeader.push(0);
aHeader.push(0);
aHeader.push(0);
var aInfoHeader = [];
aInfoHeader.push(40); // info header size
aInfoHeader.push(0);
aInfoHeader.push(0);
aInfoHeader.push(0);
var iImageWidth = iWidth;
aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256);
aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256);
aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256);
aInfoHeader.push(iImageWidth % 256);
var iImageHeight = iHeight;
aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256);
aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256);
aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256);
aInfoHeader.push(iImageHeight % 256);
aInfoHeader.push(1); // num of planes
aInfoHeader.push(0);
aInfoHeader.push(24); // num of bits per pixel
aInfoHeader.push(0);
aInfoHeader.push(0); // compression = none
aInfoHeader.push(0);
aInfoHeader.push(0);
aInfoHeader.push(0);
var iDataSize = iWidth*iHeight*3;
aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256);
aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256);
aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256);
aInfoHeader.push(iDataSize % 256);
for (var i=0;i<16;i++) {
aInfoHeader.push(0); // these bytes not used
}
var iPadding = (4 - ((iWidth * 3) % 4)) % 4;
var aImgData = oData.data;
var strPixelData = "";
var y = iHeight;
do {
var iOffsetY = iWidth*(y-1)*4;
var strPixelRow = "";
for (var x=0;x<iWidth;x++) {
var iOffsetX = 4*x;
strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX+2]);
strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX+1]);
strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX]);
}
for (var c=0;c<iPadding;c++) {
strPixelRow += String.fromCharCode(0);
}
strPixelData += strPixelRow;
} while (--y);
var strEncoded = encodeData(aHeader.concat(aInfoHeader)) + encodeData(strPixelData);
return strEncoded;
}
// sends the generated file to the client
var saveFile = function(strData) {
document.location.href = strData;
}
var makeDataURI = function(strData, strMime) {
return "data:" + strMime + ";base64," + strData;
}
// generates a <img> object containing the imagedata
var makeImageObject = function(strSource) {
var oImgElement = document.createElement("img");
oImgElement.src = strSource;
return oImgElement;
}
var scaleCanvas = function(oCanvas, iWidth, iHeight) {
if (iWidth && iHeight) {
var oSaveCanvas = document.createElement("canvas");
oSaveCanvas.width = iWidth;
oSaveCanvas.height = iHeight;
oSaveCanvas.style.width = iWidth+"px";
oSaveCanvas.style.height = iHeight+"px";
var oSaveCtx = oSaveCanvas.getContext("2d");
oSaveCtx.drawImage(oCanvas, 0, 0, oCanvas.width, oCanvas.height, 0, 0, iWidth, iHeight);
return oSaveCanvas;
}
return oCanvas;
}
return {
saveAsPNG : function(oCanvas, bReturnImg, iWidth, iHeight) {
if (!bHasDataURL) {
return false;
}
var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight);
var strData = oScaledCanvas.toDataURL("image/png");
if (bReturnImg) {
return makeImageObject(strData);
} else {
saveFile(strData.replace("image/png", strDownloadMime));
}
return true;
},
saveAsJPEG : function(oCanvas, bReturnImg, iWidth, iHeight) {
if (!bHasDataURL) {
return false;
}
var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight);
var strMime = "image/jpeg";
var strData = oScaledCanvas.toDataURL(strMime);
// check if browser actually supports jpeg by looking for the mime type in the data uri.
// if not, return false
if (strData.indexOf(strMime) != 5) {
return false;
}
if (bReturnImg) {
return makeImageObject(strData);
} else {
saveFile(strData.replace(strMime, strDownloadMime));
}
return true;
},
saveAsBMP : function(oCanvas, bReturnImg, iWidth, iHeight) {
if (!(bHasImageData && bHasBase64)) {
return false;
}
var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight);
var oData = readCanvasData(oScaledCanvas);
var strImgData = createBMP(oData);
if (bReturnImg) {
return makeImageObject(makeDataURI(strImgData, "image/bmp"));
} else {
saveFile(makeDataURI(strImgData, strDownloadMime));
}
return true;
}
};
})();

File diff suppressed because one or more lines are too long

View File

@ -268,6 +268,12 @@ const routes = [
component: () => import('./views/Merchant/MerchantIntroduction.vue'),
meta: { title: '商家资料' }
},
{
path: '/PayCode',
name: 'PayCode',
component: () => import('./views/Merchant/PayCode.vue'),
meta: { title: '收款码' }
},
{
path: '/QrReader',
name: 'QrReader',

View File

@ -5565,4 +5565,8 @@
font-weight: bold;
width: 100%;
}
}
.paycode-page {
.f5;
}

View File

@ -2298,26 +2298,37 @@ img {
.box;
.box-tb;
.box-align-center;
padding: 6.8vw 9.2vw;
width: 80vw;
min-width: 80vw;
max-width: 80vw;
max-height: 90vh;
overflow-y: auto;
background-color: #ffffff;
border-radius: 2.67vw;
.share_box {
.box;
.box-tb;
.box-align-center;
padding: 6.8vw 9.2vw;
width: 100%;
flex-shrink: 0;
background-image: linear-gradient(180deg,
#f5cfd8 0%,
#fff4f4 100%);
border-radius: 2.67vw;
}
.logo {
height: 10.53vw;
margin-bottom: 4vw;
width: 18.93vw;
height: 22.27vw;
}
.bottom_box {
.box;
.box-align-center;
.box-pack-around;
width: 100%;
}
.share_userinfo {
@ -2335,10 +2346,10 @@ img {
.name {
.box;
.box-center-center;
color: #222222;
.bs;
position: relative;
z-index: 1;
background-color: #fde5eb;
background-color: #841e36;
border-radius: 3.33vw;
// padding: .667vw 0;
// padding: 1.2vw 4vw 6vw;
@ -2350,7 +2361,7 @@ img {
span {
line-height: 8vw;
margin-bottom: 2.4vw;
// margin-bottom: 2.4vw;
}
}
}
@ -2377,9 +2388,25 @@ img {
height: 8vw;
text-align: center;
}
}
.price {
.box;
.box-align-center;
.box-pack-between;
width: 100%;
padding-bottom: 3.33vw;
border-bottom: 1px #fff solid;
.goods_price {
margin-top: 1.33vw;
font-size: 4vw;
font-weight: bold;
color: #ea3e23;
}
.o_price {
color: #b1b1b1;
text-decoration: line-through;
}
}
@ -2398,7 +2425,7 @@ img {
}
.share_result {
margin-top: 4vw;
// margin-top: 4vw;
text-align: center;
img {

View File

@ -1,159 +1,94 @@
/**
* HTML转图片工具
* 基于 html2canvas + canvas2image
* 基于 html-to-image 将DOM元素转换为图片
*/
import { toCanvas } from 'html-to-image'
/**
* 将HTML元素转换为canvas
* @param {HTMLElement} element - DOM元素
* @param {Object} options - 配置参数
* @param {number} [options.scale=2] - 缩放比例
* @param {number} [options.width] - 指定宽度
* @param {number} [options.height] - 指定高度
* @param {boolean} [options.useCORS=true] - 是否支持跨域
* @param {number} [options.dpi=window.devicePixelRatio] - DPI
* @returns {Promise<HTMLCanvasElement>}
* 将DOM元素转换为图片
* @param {string|HTMLElement} selector - DOM选择器或元素
* @param {Object} options - 配置选项
* @param {number} [options.width] - 自定义宽度会应用到元素上
* @param {number} [options.height] - 自定义高度会应用到元素上
* @param {number} [options.canvasWidth] - 输出画布宽度
* @param {number} [options.canvasHeight] - 输出画布高度
* @param {number} [options.pixelRatio] - 像素比例默认2
* @param {boolean} [options.useCORS] - 是否允许跨域默认true
* @param {string} [options.format] - 图片格式'png''jpeg'默认'png'
* @param {number} [options.quality] - 图片质量0-1仅jpeg有效默认1.0
* @returns {Promise<string>} - 返回base64图片数据URL
*/
export async function html2canvas(element, options = {}) {
const {
scale = 2,
width,
height,
useCORS = true,
dpi = window.devicePixelRatio,
onclone,
} = options;
export async function toDataURL(selector, options = {}) {
const {
width,
height,
canvasWidth,
canvasHeight,
pixelRatio = 2,
useCORS = true,
format = 'png',
quality = 1.0
} = options
const opts = {
scale,
useCORS,
dpi,
logging: false,
backgroundColor: '#ffffff',
};
const element = typeof selector === 'string' ? document.querySelector(selector) : selector
if (!element) {
throw new Error('Element not found')
}
if (width) opts.width = width;
if (height) opts.height = height;
const canvasEl = await toCanvas(element, {
width,
height,
canvasWidth,
canvasHeight,
pixelRatio,
cors: useCORS,
backgroundColor: '#ffffff'
})
// 添加 onclone 回调用于调试
if (onclone) {
opts.onclone = onclone;
}
const html2canvasFn = window.html2canvas;
if (!html2canvasFn) {
throw new Error('html2canvas not loaded, please include html2canvas.min.js');
}
return html2canvasFn(element, opts);
const mimeType = format === 'jpeg' ? 'image/jpeg' : 'image/png'
return canvasEl.toDataURL(mimeType, quality)
}
/**
* 将canvas转换为dataURL
* @param {HTMLCanvasElement} canvas
* @param {string} [type='image/png']
* @param {number} [quality=1.0]
* @returns {string}
* 将DOM元素转换为图片并下载
* @param {string|HTMLElement} selector - DOM选择器或元素
* @param {string} [filename] - 下载文件名不含扩展名
* @param {Object} options - 配置选项同toDataURL
*/
export function canvasToDataURL(canvas, type = 'image/png', quality = 1.0) {
return canvas.toDataURL(type, quality);
export async function toImageDownload(selector, filename = 'download', options = {}) {
const dataUrl = await toDataURL(selector, options)
const link = document.createElement('a')
link.download = filename + '.png'
link.href = dataUrl
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
/**
* 将canvas转换为Blob
* @param {HTMLCanvasElement} canvas
* @param {string} [type='image/png']
* @param {number} [quality=1.0]
* 将DOM元素转换为Blob
* @param {string|HTMLElement} selector - DOM选择器或元素
* @param {Object} options - 配置选项同toDataURL
* @returns {Promise<Blob>}
*/
export function canvasToBlob(canvas, type = 'image/png', quality = 1.0) {
return new Promise((resolve, reject) => {
canvas.toBlob(blob => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Canvas to Blob failed'));
}
}, type, quality);
});
export async function toBlob(selector, options = {}) {
const dataUrl = await toDataURL(selector, options)
const arr = dataUrl.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new Blob([u8arr], { type: mime })
}
/**
* 将HTML元素直接转成dataURL图片
* @param {HTMLElement} element - DOM元素
* @param {Object} options - html2canvas配置
* @param {string} [type='image/png'] - 图片类型
* @param {number} [quality=1.0] - 图片质量
* @returns {Promise<string>} dataURL
*/
export async function htmlToDataURL(element, options = {}, type = 'image/png', quality = 1.0) {
const canvas = await html2canvas(element, options);
return canvasToDataURL(canvas, type, quality);
}
/**
* 将HTML元素直接转成Blob
* @param {HTMLElement} element - DOM元素
* @param {Object} options - html2canvas配置
* @param {string} [type='image/png'] - 图片类型
* @param {number} [quality=1.0] - 图片质量
* @returns {Promise<Blob>}
*/
export async function htmlToBlob(element, options = {}, type = 'image/png', quality = 1.0) {
const canvas = await html2canvas(element, options);
return canvasToBlob(canvas, type, quality);
}
/**
* 下载canvas为图片
* @param {HTMLCanvasElement} canvas
* @param {string} filename - 文件名不带扩展名
* @param {string} [type='png'] - 图片格式 ('png' | 'jpeg' | 'webp')
*/
export function downloadCanvas(canvas, filename, type = 'png') {
const mimeType = `image/${type}`;
const dataURL = canvasToDataURL(canvas, mimeType, 1.0);
const link = document.createElement('a');
link.download = `${filename}.${type}`;
link.href = dataURL;
link.click();
}
/**
* 将HTML元素转成图片并下载
* @param {HTMLElement} element - DOM元素
* @param {string} filename - 文件名不带扩展名
* @param {Object} options - html2canvas配置
* @param {string} [type='png'] - 图片格式
*/
export async function htmlToImage(element, filename, options = {}, type = 'png') {
const canvas = await html2canvas(element, options);
downloadCanvas(canvas, filename, type);
}
/**
* 将dataURL保存为图片文件
* @param {string} dataURL - 图片dataURL
* @param {string} filename - 文件名不带扩展名
*/
export function downloadDataURL(dataURL, filename) {
const link = document.createElement('a');
link.download = filename;
link.href = dataURL;
link.click();
}
/**
* 将Blob保存为图片文件
* @param {Blob} blob - 图片Blob
* @param {string} filename - 文件名不带扩展名
*/
export async function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = filename;
link.href = url;
link.click();
setTimeout(() => URL.revokeObjectURL(url), 100);
}
export default {
toDataURL,
toImageDownload,
toBlob
}

View File

@ -125,7 +125,7 @@
<van-popup v-model:show="show" class="shareimgbox" style="max-width: 80vw;">
<div class="share_box" id="bsCard">
<img class="logo" src="/img/logo-lr.png" alt="">
<div class="share_userinfo">
<img :src="$file($userInfo.userimg)" alt="">
@ -135,18 +135,24 @@
<div class="goods_info">
<img class="goods_img" :src="$file(data.img)" alt="">
<div class="goods_name">{{ data.name }}</div>
<div class="goods_price">¥{{ data.saleprice?.toFixed(2) || '0.00' }}</div>
</div>
<div class="qrcode">
<vue-qr :margin="0" :text="shareLink" backgroundColor="rgb(255,255,255)"
colorLight="rgb(255,255,255)"></vue-qr>
长按扫码购买
<div class="price">
<div class="goods_price">¥{{ data.saleprice?.toFixed(2) || '0.00' }}</div>
<div class="o_price">¥{{ data.originalprice?.toFixed(2) || '0.00' }}</div>
</div>
<div class="bottom_box">
<div class="qrcode">
<vue-qr :margin="0" :text="shareLink" backgroundColor="#ffffff00" colorLight="rgb(255,255,255)"></vue-qr>
长按扫码购买
</div>
<img class="logo" src="/img/logo.png" alt="">
</div>
</div>
<div class="share_result" v-if="ShareImg">
<img :src="ShareImg" alt="分享海报">
<!-- <div class="tips">长按保存图片</div> -->
</div>
</van-popup>
@ -158,7 +164,8 @@
</template>
<script>
import { html2canvas, canvasToDataURL } from '@/utils/html2image';
import { toDataURL } from '@/utils/html2image.js'
export default {
name: 'GoodsDetail',
@ -182,7 +189,8 @@ export default {
],
shareLink: '',
ShareImg: '',
loading: false
loading: false,
transparent: 'rgba(0,0,0,0)'
}
},
methods: {
@ -242,7 +250,7 @@ export default {
// DOM
setTimeout(() => {
this.generateShareImg();
}, 200);
}, 500);
} else if (option.name == "复制链接") {
this.$copyText(location.href);
this.$showToast({ type: "success", message: "复制成功" });
@ -263,19 +271,29 @@ export default {
async generateShareImg() {
var shareContent = document.querySelector('#bsCard');
if (!shareContent) return;
var width = shareContent.offsetWidth;
var height = shareContent.offsetHeight;
try {
var canvas = await html2canvas(shareContent, {
scale: 2,
width: width,
height: height,
// Wait for all images inside shareContent to load
const images = shareContent.querySelectorAll('img');
for (const img of images) {
if (!img.complete) {
await new Promise((resolve) => {
img.onload = resolve;
img.onerror = resolve;
});
}
}
// Wait for vue-qr to render
await new Promise(resolve => setTimeout(resolve, 500));
this.ShareImg = await toDataURL(shareContent, {
pixelRatio: 2,
useCORS: true,
allowTaint: true,
format: 'png'
});
this.ShareImg = canvasToDataURL(canvas, "image/png", 1.0);
document.querySelector('.share_box').style = 'display: none';
this.loading = false;
this.loading = false;
document.querySelector('.share_box').style.display = 'none';
} catch (e) {
console.error(e);
this.loading = false;

View File

@ -49,7 +49,7 @@ export default {
Icon_Inactive: '/img/My_Inactive.png',
},
],
}
}
},
watch: {
'$route.path': {
@ -79,7 +79,8 @@ export default {
onTabClick(name) {
if (this.$route.name === name) {
//
this.$router.replace({ name })
// this.$router.replace({ name })
location.reload()
} else {
this.$router.push({ name })
}

View File

@ -1,53 +1,53 @@
<template>
<BasePage>
<ManagerPopup v-model="managerPopupVisible" />
<div class="merchant">
<div class="shopinfo">
<img class="tx" :src="$file(data.shopimg)">
<div class="info">
<b>{{ truncatedShopname }}</b>
<p>账号{{ data.shopphone }}</p>
</div>
<div class="service-box" @click="$showManagerPopup()">
管理中心
<van-icon name="arrow"></van-icon>
</div>
<!-- <BasePage> -->
<ManagerPopup v-model="managerPopupVisible" />
<div class="merchant">
<div class="shopinfo">
<img class="tx" :src="$file(data.shopimg)">
<div class="info">
<b>{{ truncatedShopname }}</b>
<p>账号{{ data.shopphone }}</p>
</div>
<div class="wallet">
<div class="tit">
<span @click="$navigate('MerchantIncome')"> 营业收入() <van-icon name="arrow"></van-icon>
<div class="service-box" @click="$showManagerPopup()">
管理中心
<van-icon name="arrow"></van-icon>
</div>
</div>
<div class="wallet">
<div class="tit">
<span @click="$navigate('MerchantIncome')"> 营业收入() <van-icon name="arrow"></van-icon>
</span>
<div class="line">
<b @click="$navigate('MerchantIncome')">
{{ data.shangjiashouru?.toFixed(2) }}
</b>
<!-- v-if="data.Income" -->
<div @click="$navigate('MerchantCashout')">
<span>提现中金额
</span>
<p>{{ data.withdrawing?.toFixed(2) }}</p>
</div>
</div>
</div>
<div class="cc">
<div class="ll" @click="$navigate('Balance')">
<span>
可用余额<van-icon name="arrow"></van-icon>
</span>
<div class="line">
<b @click="$navigate('MerchantIncome')">
{{ data.shangjiashouru?.toFixed(2) }}
</b>
<!-- v-if="data.Income" -->
<div @click="$navigate('MerchantCashout')">
<span>提现中金额
</span>
<p>{{ data.withdrawing?.toFixed(2) }}</p>
</div>
</div>
<b>{{ data.keyongyue?.toFixed(2) }}</b>
</div>
<div class="cc">
<div class="ll" @click="$navigate('Balance')">
<span>
可用余额<van-icon name="arrow"></van-icon>
</span>
<b>{{ data.keyongyue?.toFixed(2) }}</b>
</div>
<div class="ll" @click="$navigate('CV')">
<span>
会员卡额度<van-icon name="arrow"></van-icon>
</span>
<b>{{ data.huiyuanka?.toFixed(2) }}</b>
</div>
<div class="ll" @click="$navigate('CV')">
<span>
会员卡额度<van-icon name="arrow"></van-icon>
</span>
<b>{{ data.huiyuanka?.toFixed(2) }}</b>
</div>
</div>
</div>
<!-- <div class="block">
<!-- <div class="block">
<div class="d1" @click="$navigate('MerchantTrade')">
<b>订单记录</b>
<p>查看订单记录</p>
@ -73,97 +73,76 @@
</div> -->
<div class="fastTo">
<div class="ft1" @click="showBottom = true">
<span>收款码</span>
<img src="/img/ft_i1.png" alt="">
</div>
<div class="ft2" @click="$navigate('MerchantTrade')">
<span>订单记录</span>
<img src="/img/ft_i2.png" alt="">
</div>
<div class="ft3" @click="$navigate('MerchantIntroduction')">
<span>商家资料</span>
<img src="/img/ft_i3.png" alt="">
</div>
<div class="fastTo">
<div class="ft1" @click="$navigate('PayCode?id=' + id)">
<span>收款码</span>
<img src="/img/ft_i1.png" alt="">
</div>
<div class="countbox">
<div class="title">
<b>经营数据</b>
</div>
<div class="box line">
<span>截止{{ data.endTimes }}</span>
<p class="refresh" @click="getCount">刷新</p>
<p class="cou r" @click="$navigate('Statistics')">统计<van-icon name="arrow"></van-icon></p>
</div>
<div class="list">
<div class="item">
<span>
今日营业额
</span>
<b>{{ countData.todayyingyee?.toFixed(2) }}</b>
<p>昨日<span>{{ countData.yesterdayyingyee?.toFixed(2) }}</span></p>
</div>
<div class="item">
<span>
今日应收
</span>
<b>{{ countData.todayyingshou?.toFixed(2) }}</b>
<p>昨日<span>{{ countData.yesterdayingshou?.toFixed(2) }}</span></p>
</div>
<div class="item">
<span>
今日订单数
</span>
<b>{{ countData.todaydingdanshu }}</b>
<p>昨日<span>{{ countData.yesterdaydingdanshu }}</span></p>
</div>
<div class="item">
<span>
今日惠利金额
</span>
<b>{{ countData.todayyouhui?.toFixed(2) }}</b>
<p>昨日<span>{{ countData.yesterdayyouhui?.toFixed(2) }}</span></p>
</div>
</div>
<div class="ft2" @click="$navigate('MerchantTrade')">
<span>订单记录</span>
<img src="/img/ft_i2.png" alt="">
</div>
<div class="ft3" @click="$navigate('MerchantIntroduction')">
<span>商家资料</span>
<img src="/img/ft_i3.png" alt="">
</div>
</div>
<van-action-sheet v-model:show="showBottom" title="收款码" @click-overlay="onCloseBottom" @opened="onOpenBottom">
<div class="paycode-wrap">
<div class="paycode-original" ref="paycodeOriginal" v-show="!paycodeImg">
<img src="/img/paycode.jpg" alt="" style="width: 100%; display: block;">
<div class="info">
<img :src="$file(data.shopimg)" alt="" style="border-radius: 50%;">
</div>
<div class="name">
<span>{{ data.shopname }}</span>
</div>
<div class="code">
<vue-qr v-if="!loading" :margin="0" :text="link" backgroundColor="rgb(255,255,255)"
colorLight="rgb(255,255,255)"></vue-qr>
</div>
<div class="countbox">
<div class="title">
<b>经营数据</b>
</div>
<div class="box line">
<span>截止{{ data.endTimes }}</span>
<p class="refresh" @click="getCount">刷新</p>
<p class="cou r" @click="$navigate('Statistics')">统计<van-icon name="arrow"></van-icon></p>
</div>
<div class="list">
<div class="item">
<span>
今日营业额
</span>
<b>{{ countData.todayyingyee?.toFixed(2) }}</b>
<p>昨日<span>{{ countData.yesterdayyingyee?.toFixed(2) }}</span></p>
</div>
<div class="paycode-result" v-if="paycodeImg">
<img :src="paycodeImg" style="width: 100%;" alt="收款码">
<div class="item">
<span>
今日应收
</span>
<b>{{ countData.todayyingshou?.toFixed(2) }}</b>
<p>昨日<span>{{ countData.yesterdayingshou?.toFixed(2) }}</span></p>
</div>
<div class="item">
<span>
今日订单数
</span>
<b>{{ countData.todaydingdanshu }}</b>
<p>昨日<span>{{ countData.yesterdaydingdanshu }}</span></p>
</div>
<div class="item">
<span>
今日惠利金额
</span>
<b>{{ countData.todayyouhui?.toFixed(2) }}</b>
<p>昨日<span>{{ countData.yesterdayyouhui?.toFixed(2) }}</span></p>
</div>
</div>
</van-action-sheet>
</BasePage>
</div>
</div>
<!-- </BasePage> -->
</template>
<script>
import { html2canvas } from '@/utils/html2image'
import ManagerPopup from "@/components/ManagerPopup.vue"
export default {
@ -181,33 +160,11 @@ export default {
data() {
return {
managerPopupVisible: false,
showInfo: false,
showBottom: false,
paycodeImg: '',
isCapturing: false,
link: '',
id: this.$route.query.id,
data: {
id: 'merchant_001',
MerchantAvatar: '',
MerchantName: '泰古润直营店',
MerchantPhone: '13800138000',
Income: 12868.50,
Point: 5680.00,
TodayAmount: 5680.00,
YesterdayAmount: 4200.00,
TodayDeduct: 1280.00,
YesterdayDeduct: 980.00,
TodayQty: 45,
YesterdayQty: 38,
MonthIncome: 45600.00,
PrevMonthIncome: 38500.00,
endTimes: new Date().toLocaleString('zh-CN', { hour12: false })
},
countData: {},
States: 0,
expiring: 3,
loading: false,
}
},
computed: {
@ -226,87 +183,14 @@ export default {
init() {
Promise.all([
this.$get(`/v1/client/DShopsClient/${this.id}`).then(res => {
// console.log(res);
this.data = res.data;
this.data.endTimes = new Date().toLocaleString('zh-CN', { hour12: false });
this.$ls.set('merchant_shopname', res.data.shopname);
this.$ls.set('merchant_shopimg', res.data.shopimg);
this.$ls.set('merchant_cellphone', res.data.cellphone);
this.link = `${location.origin}${location.pathname}#/Checkout?id=${res.data.userid}`
}),
this.getCount()
]
)
},
async onOpenBottom() {
this.paycodeImg = ''
this.$nextTick(async () => {
const el = this.$refs.paycodeOriginal
if (!el) return
const scale = 2
const elRect = el.getBoundingClientRect()
//
//
const canvas = document.createElement('canvas')
const w = el.offsetWidth * scale
const h = el.offsetHeight * scale
canvas.width = w
canvas.height = h
const ctx = canvas.getContext('2d')
// img
const bg = el.querySelector('img')
const bgCanvas = await html2canvas(bg, { scale, useCORS: true, width: el.offsetWidth, height: el.offsetHeight })
ctx.drawImage(bgCanvas, 0, 0, w, h)
//
const infoEl = el.querySelector('.info')
const avatarEl = infoEl.querySelector('img')
const avatarCanvas = await html2canvas(avatarEl, { scale, useCORS: true, backgroundColor: 'transparent' })
const infoRect = infoEl.getBoundingClientRect()
const avatarSize = 30 * scale
const avatarX = (infoRect.left - elRect.left) * scale
const avatarY = (infoRect.top - elRect.top) * scale
ctx.save()
ctx.beginPath()
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2)
ctx.clip()
ctx.drawImage(avatarCanvas, avatarX, avatarY, avatarSize, avatarSize)
ctx.restore()
//
const codeEl = el.querySelector('.code')
const codeCanvas = await html2canvas(codeEl, { scale, useCORS: true, backgroundColor: '#ffffff' })
const codeRect = codeEl.getBoundingClientRect()
ctx.drawImage(
codeCanvas,
(codeRect.left - elRect.left) * scale,
(codeRect.top - elRect.top) * scale,
codeRect.width * scale,
codeRect.height * scale
)
// (.name)
const nameEl = el.querySelector('.name')
const nameRect = nameEl.getBoundingClientRect()
const fontSize = 20 * scale
ctx.font = `${fontSize}px 'PingFang SC'`
ctx.fillStyle = '#333'
ctx.textBaseline = 'middle'
if (this.data.shopname)
ctx.fillText(this.truncatedShopname, (nameRect.left - elRect.left) * scale, (nameRect.top - elRect.top) * scale + fontSize / 2)
this.paycodeImg = canvas.toDataURL('image/png')
})
},
onCloseBottom() {
this.showBottom = false
// this.paycodeImg = ''
])
},
getCount() {
this.$get('/v1/client/DShopsClient/statistics').then(res => {
@ -320,51 +204,4 @@ export default {
</script>
<style lang="less">
.paycode-original {
position: relative;
width: 100%;
font-family: 'PingFang SC';
>img {
width: 100%;
display: block;
}
.info {
display: flex;
align-items: center;
position: absolute;
left: 30.67vw;
top: 32.27vw;
img {
width: 6.4vw;
height: 6.4vw;
border-radius: 50%;
}
}
.name {
position: absolute;
left: 39.33vw;
top: 33.47vw;
span {
font-size: 4vw;
color: #333;
}
}
.code {
position: absolute;
top: 42.4vw;
left: 23.33vw;
canvas,
img {
width: 53.33vw;
height: 53.33vw;
}
}
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<BasePage>
<div class="paycode-page">
<div class="paycode-wrap">
<!-- 生成后的图片 -->
<div v-if="generatedImage" class="paycode-generated">
<img :src="generatedImage" alt="收款码">
</div>
<!-- 原始元素 -->
<div v-else class="paycode-original" ref="paycodeOriginalRef">
<img src="/img/paycode.jpg" alt="" style="width: 100%; display: block;" class="bg-img" @load="onBgLoad">
<div class="user-info">
<div class="info">
<img :src="$file(shopimg)" alt="" style="border-radius: 50%;">
</div>
<div class="name">
<span>{{ shopname?.length > 8 ? shopname.slice(0, 8) + '...' : shopname }}</span>
</div>
</div>
<div class="code">
<vue-qr v-if="link" :margin="0" :text="link" backgroundColor="rgb(255,255,255)"
colorLight="rgb(255,255,255)"></vue-qr>
</div>
</div>
</div>
<div class="save-btn">
<van-button type="primary" color="#ca2904" round block :loading="loading" @click="handleSave">{{ loading ?
'生成中...' : ('保存图片')
}}</van-button>
</div>
</div>
</BasePage>
</template>
<script>
import { toDataURL } from '@/utils/html2image'
export default {
name: 'PayCode',
data() {
return {
shopname: '',
shopimg: '',
link: '',
loading: false,
generatedImage: '',
bgLoaded: false,
}
},
mounted() {
this.init()
},
methods: {
init() {
const id = this.$route.query.id
this.$get(`/v1/client/DShopsClient/${id}`).then(res => {
this.shopname = res.data.shopname
this.shopimg = res.data.shopimg
this.link = `${location.origin}${location.pathname}#/Checkout?id=${res.data.userid}`
}).catch(err => {
this.$showFailToast('加载失败')
})
},
onBgLoad() {
this.bgLoaded = true
//
this.generateImage()
},
async generateImage() {
if (!this.bgLoaded || this.loading) return
this.loading = true
try {
await this.$nextTick()
await document.fonts.ready
await new Promise(resolve => setTimeout(resolve, 300))
const el = this.$refs.paycodeOriginalRef
this.generatedImage = await toDataURL(el, { format: 'png', pixelRatio: 3, useCORS: true })
} catch (e) {
this.$showFailToast('生成失败')
} finally {
this.loading = false
}
},
async handleSave() {
if (this.loading) return
//
if (this.generatedImage) {
this.downloadByBlob(this.generatedImage, `收款码_${this.shopname}.png`)
this.$showSuccessToast('保存成功')
return
}
//
await this.generateImage()
if (this.generatedImage) {
this.downloadByBlob(this.generatedImage, `收款码_${this.shopname}.png`)
this.$showSuccessToast('保存成功')
}
},
downloadByBlob(dataUrl, filename) {
const arr = dataUrl.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
const blob = new Blob([u8arr], { type: mime })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.download = filename
link.href = url
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
},
},
}
</script>
<style lang="less" scoped>
.paycode-page {
// min-height: 100vh;
background: #f5f5f5;
padding-bottom: 20px;
}
.paycode-generated {
img {
width: 100%;
display: block;
}
}
.paycode-original {
position: relative;
width: 100%;
font-family: 'PingFang SC';
>img {
width: 100%;
display: block;
}
.user-info {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: 32.27vw;
display: flex;
align-items: center;
}
.info {
display: flex;
align-items: center;
img {
width: 6.4vw;
height: 6.4vw;
border-radius: 50%;
}
}
.name {
margin-left: 2.67vw;
// margin-bottom: 2.4vw;
span {
font-size: 4vw;
color: #333;
}
}
.code {
position: absolute;
top: 42.4vw;
left: 23.33vw;
canvas,
img {
width: 53.33vw;
height: 53.33vw;
}
}
}
.save-btn {
margin: 20px;
}
</style>

View File

@ -1,90 +1,90 @@
<template>
<BasePage>
<ManagerPopup v-model="managerPopupVisible" />
<div class="opera">
<!-- <BasePage> -->
<ManagerPopup v-model="managerPopupVisible" />
<div class="opera">
<div class="top">
<div class="info_box">
<img class="icon" :src="$file(userimg || '/img/default-avatar.png')" alt="">
<div class="inf">
<b>{{ nickname || '未登录' }}</b>
<div>
<img src="/img/phone.png" alt="">
{{ cellphone || '暂无手机号' }}
</div>
<div class="top">
<div class="info_box">
<img class="icon" :src="$file(userimg || '/img/default-avatar.png')" alt="">
<div class="inf">
<b>{{ nickname || '未登录' }}</b>
<div>
<img src="/img/phone.png" alt="">
{{ cellphone || '暂无手机号' }}
</div>
</div>
<div class="shopcenter" @click="$showManagerPopup()">
<span>
管理中心
<van-icon name="arrow" />
</span>
</div>
</div>
<div class="containar">
<div class="wallet_box">
<div class="item" @click="$navigate('Balance')">
<div class="t">
<img src="/img/op-i1.png" alt="">
<span>余额</span>
<van-icon name="arrow"></van-icon>
</div>
<b>
{{ zijin?.toFixed(2) || '0.00' }}
</b>
</div>
<div class="shopcenter" @click="$showManagerPopup()">
<span>
管理中心
<van-icon name="arrow" />
</span>
</div>
</div>
<div class="item" @click="$navigate('CV')">
<div class="t">
<img src="/img/op-i2.png" alt="">
<span>会员卡</span>
<van-icon name="arrow"></van-icon>
</div>
<b>
{{ xiaofeiquan?.toFixed(2) || '0.00' }}
</b>
<div class="containar">
<div class="wallet_box">
<div class="item" @click="$navigate('Balance')">
<div class="t">
<img src="/img/op-i1.png" alt="">
<span>余额</span>
<van-icon name="arrow"></van-icon>
</div>
<b>
{{ zijin?.toFixed(2) || '0.00' }}
</b>
</div>
<div class="fun_box">
<div class="item" @click="$navigate('QrReader')">
<div class="left">
<b>
扫一扫
</b>
<p>核销礼品券</p>
</div>
<div class="item" @click="$navigate('CV')">
<div class="t">
<img src="/img/op-i2.png" alt="">
<span>会员卡</span>
<van-icon name="arrow"></van-icon>
</div>
<b>
{{ xiaofeiquan?.toFixed(2) || '0.00' }}
</b>
</div>
</div>
<img src="/img/op-i3.png" alt="">
<div class="fun_box">
<div class="item" @click="$navigate('QrReader')">
<div class="left">
<b>
扫一扫
</b>
<p>核销礼品券</p>
</div>
<div class="item" @click="$navigate('CertificateRecord?type=verif')">
<div class="left">
<b>
核销记录
</b>
<p>查看礼品券核销记录</p>
</div>
<img src="/img/op-i3.png" alt="">
</div>
<img src="/img/op-i4.png" alt="">
<div class="item" @click="$navigate('CertificateRecord?type=verif')">
<div class="left">
<b>
核销记录
</b>
<p>查看礼品券核销记录</p>
</div>
<div class="item" @click="$navigate('Allow')">
<div class="left">
<b>
业绩统计
</b>
<p>查看业绩统计数据</p>
</div>
<img src="/img/op-i4.png" alt="">
</div>
<img src="/img/op-i5.png" alt="">
<div class="item" @click="$navigate('Allow')">
<div class="left">
<b>
业绩统计
</b>
<p>查看业绩统计数据</p>
</div>
<img src="/img/op-i5.png" alt="">
</div>
</div>
</div>
</BasePage>
</div>
<!-- </BasePage> -->
</template>
<script>

View File

@ -5,7 +5,7 @@
<img src="/img/search.png" alt="">
<input v-model="searchParams.Key" placeholder="请输入关键词搜索"
@search="$navigate('Category?Key=' + searchParams.Key)">
<button @click="$navigate('Category?Key=' + searchParams.Key)">搜索</button>
<button @click="$navigate('Category?Key=' + searchParams.Key)" :style="btnStyle">搜索</button>
</div>
<div class="banner">
<van-swipe :autoplay="3000" indicator-color="white" @change="onSwipeChange">
@ -288,6 +288,14 @@ export default {
}
}
return {}
},
btnStyle() {
if (this.bgcolor) {
return {
background: this.bgcolor
}
}
return {}
}
},
}

View File

@ -27,8 +27,7 @@
</template>
<script>
import { html2canvas, canvasToDataURL } from '@/utils/html2image';
import { useUserStore } from '@/stores/user';
import { toDataURL } from '@/utils/html2image';
export default {
name: 'Invite',
@ -41,7 +40,7 @@ export default {
NickName: '',
},
link: '',
userStore: useUserStore(),
userStore: null,
};
},
mounted() {
@ -77,8 +76,6 @@ export default {
},
async CreatePoster(divID, targetID) {
var shareContent = document.querySelector(divID);
var width = shareContent.offsetWidth;
var height = shareContent.offsetHeight;
try {
//
@ -91,18 +88,11 @@ export default {
});
}
var canvas = await html2canvas(shareContent, {
scale: 2,
width: width,
height: height,
useCORS: true,
allowTaint: false,
});
var dataUrl = canvasToDataURL(canvas, "image/png", 1.0);
var dataUrl = await toDataURL(shareContent, { format: 'png', pixelRatio: 2, useCORS: true });
var newImg = document.createElement("img");
newImg.src = dataUrl;
newImg.width = width;
newImg.height = height;
newImg.width = shareContent.offsetWidth;
newImg.height = shareContent.offsetHeight;
document.querySelector(targetID).appendChild(newImg);
} catch (err) {
console.error('生成海报失败', err);
@ -128,7 +118,7 @@ export default {
.name {
position: absolute;
left: 20.27vw;
bottom: 25.6vw;
bottom: 23.6vw;
line-height: 6vw;
width: 100%;

View File

@ -3,7 +3,8 @@
<div class="verif">
<div class="search_box">
<img src="/img/search.png" alt="">
<input type="text" placeholder="输入用户账号/礼品券码搜索" v-model="keyword1">
<input type="text" placeholder="输入用户账号/礼品券码搜索" v-model="keyword1" v-if="type === 'verif'">
<input type="text" placeholder="输入用户账号/会员卡号搜索" v-model="keyword1" v-else>
</div>
<BaseList ref="listRef"
@ -17,9 +18,12 @@
<span>{{ item.nickname || item.username || '未知用户' }}</span>
<p>账号{{ item.cellphone || item.username }}</p>
</div>
<span class="r tt">
<span class="r tt" v-if="type === 'verif'">
已核销
</span>
<span class="r pre" v-else>
增送额度<b style="color: f00;">500.00</b>
</span>
</div>
<div class="info_box">
<div>
@ -28,17 +32,27 @@
src="/img/copy_b.png" alt=""></span>
</div>
<div>
<div v-if="type === 'verif'">
<span class="left">礼品券码</span>
<span class="r rg">{{ item.sysid }}<img @click="$copyText(item.sysid); $showSuccessToast('复制成功')"
src="/img/copy_b.png" alt=""></span>
</div>
<div>
<div v-else>
<span class="left">会员卡号</span>
<span class="r rg">{{ item.huiyuankaid }}<img
@click="$copyText(item.huiyuankaid); $showSuccessToast('复制成功')" src="/img/copy_b.png" alt=""></span>
</div>
<div v-if="type === 'verif'">
<span class="left">核销时间</span>
<span class="r rg">{{ formatTime(item.writeofftime) }}</span>
</div>
<div v-else>
<span class="left">赠送时间</span>
<span class="r rg">{{ formatTime(item.addtime) }}</span>
</div>
</div>
</div>
</template>