插件市场
bug:浏览器上图片不显示,手机上就好了
组件poster.vue
<template
>
<view
><canvas
:canvas
-id
="id" :style
="'width:' + boardWidth + '; height:' + boardHeight + ';' + customStyle"></canvas
></view
>
</template
>
<script
>
const CHAR_WIDTH_SCALE_MAP = [0.296, 0.313, 0.436, 0.638, 0.586, 0.89, 0.87, 0.256, 0.334, 0.334, 0.455, 0.742, 0.241, 0.433, 0.241, 0.427, 0.586, 0.586, 0.586, 0.586, 0.586, 0.586, 0.586, 0.586, 0.586, 0.586, 0.241, 0.241, 0.742, 0.742, 0.742, 0.483, 1.031, 0.704, 0.627, 0.669, 0.762, 0.55, 0.531, 0.744, 0.773, 0.294, 0.396, 0.635, 0.513, 0.977, 0.813, 0.815, 0.612, 0.815, 0.653, 0.577, 0.573, 0.747, 0.676, 1.018, 0.645, 0.604, 0.62, 0.334, 0.416, 0.334, 0.742, 0.448, 0.295, 0.553, 0.639, 0.501, 0.64, 0.567, 0.347, 0.64, 0.616, 0.266, 0.267, 0.544, 0.266, 0.937, 0.616, 0.636, 0.639, 0.64, 0.382, 0.463, 0.373, 0.616, 0.525, 0.79, 0.507, 0.529, 0.492, 0.334, 0.269, 0.334, 0.742, 0.296];
const setStringPrototype = (screen
) => {
String
.prototype
.toPx = function (minus
, baseSize
) {
const reg
= minus
? (/^-?[0-9]+([.]{1}[0-9]+){0,1}(rpx|px|%)$/g) : (/^[0-9]+([.]{1}[0-9]+){0,1}(rpx|px|%)$/g)
const results
= reg
.exec(this);
if (!this || !results
) {
return 0;
}
const unit
= results
[2];
const value
= parseFloat(this);
let res
= 0;
if (unit
=== 'rpx') {
res
= Math
.round(value
* (screen
|| 0.5) * 1);
} else if (unit
=== 'px') {
res
= Math
.round(value
* 1);
} else if (unit
=== '%') {
res
= Math
.round(value
* baseSize
/ 100);
}
return res
;
}
}
export default {
props
:{
board
: {
type
: Object
,
},
isAsync
: {
type
: Boolean
,
default: true
},
pixelRatio
: Number
,
customStyle
: String
,
isRenderImage
: Boolean
},
data() {
return {
timer
: null,
id
: `painter_${Math.random()}`
id
: `painter`
}
},
watch
:{
board
: {
handler
: 'drawAll',
}
},
computed
:{
dpr() {
return this.pixelRatio
|| uni
.getSystemInfoSync().pixelRatio
},
windowWidth() {
return uni
.getSystemInfoSync().windowWidth
},
boardWidth() {
const {width
= 200} = this.board
|| {}
return width
},
boardHeight() {
const {height
= 200} = this.board
|| {}
return height
}
},
created() {
this.init()
},
mounted() {
if(this.context
) {
this.drawAll()
}
},
methods
: {
async initBoard() {
const { board
} = this
if(board
?.views
?.length
) {
let result
= await Promise
.all(board
.views
.map(async (item
) => {
if(item
.type
=== 'image') {
const {height
, width
, path
} = await this.getImageInfo(item
.url
)
return Object
.assign({}, item
, {height
, width
, url
: path
})
}
return item
}))
return result
|| []
}
return []
},
init() {
this.context
= uni
.createCanvasContext(this.id
, this)
setStringPrototype(this.windowWidth
/ 750)
},
draw(view
) {
this.context
.setFillStyle(view
.background
|| 'white')
this.context
.fillRect(view
.css
.left
.toPx(), view
.css
.top
.toPx(), view
.css
.width
.toPx(), view
.css
.height
.toPx())
this.context
.clip()
this.drawView(this.context
, view
)
this.context
.draw(true, () => {
if(this.isRenderImage
) {
setTimeout(() => {
this.saveImgToLocal();
}, 100)
}
})
},
async drawAll() {
let views
= this.isAsync
? await this.initBoard() : this.board
.views
if(!this.context
|| !views
.length
) {return}
const board
= this.drawRect(this.context
, {type
: 'view', css
: {left
: `${this.board?.left || 0}`, top
: `${this.board?.top || 0}`, width
: `${this.boardWidth}`, height
: `${this.boardHeight}`, background
: this.board
?.background
}})
const promises
= views
.map(item
=> this.drawView(this.context
, item
)) || [Promise
.resolve()]
Promise
.all([board
].concat(promises
)).then((res
) => {
this.context
.draw(true, () => {
if(this.isRenderImage
) {
clearTimeout(this.timer
)
this.timer
= setTimeout(() => {
this.saveImgToLocal();
}, 100)
}
})
})
},
saveImgToLocal() {
uni
.canvasToTempFilePath({
canvasId
: this.id
,
destWidth
: this.toNumber(this.boardWidth
) * this.dpr
,
destHeight
: this.toNumber(this.boardHeight
) * this.dpr
,
success
: async (res
) => {
const photo
= await this.getImageInfo(res
.tempFilePath
)
if(photo
.path
) {
this.$emit('success', photo
.path
)
}
},
fail
: (error
) => {
console
.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`);
this.$emit('fail', {
error
: error
})
}
}, this)
},
async drawView(context
, view
) {
if(view
.type
== 'view') {
return this.drawRect(context
, view
)
} else if(view
.type
== 'image') {
if(this.isAsync
) {
return this.drawRect(context
, view
)
} else {
const {height
= 0, width
= 0, path
: url
} = await this.getImageInfo(view
.url
)
return this.drawRect(context
, Object
.assign(view
, {height
, width
, url
}))
}
} else if(view
.type
== 'text'){
return this.drawText(context
, view
)
}
},
toNumber(value
, minus
= 0, baseSize
= 0) {
if(typeof value
=== 'string') {
return value
.toPx(minus
, baseSize
)
} else if(typeof value
=== 'number') {
return value
} else {
return 0
}
},
base64src(base64data
) {
return new Promise((resolve
, reject
) => {
const fs
= uni
.getFileSystemManager()
const [, format
, bodyData
] = /data:image\/(\w+);base64,(.*)/.exec(base64data
) || [];
if (!format
) {reject(new Error('ERROR_BASE64SRC_PARSE'))}
const time
= new Date().getTime();
const filePath
= `${wx.env.USER_DATA_PATH}/${time}.${format}`
const buffer
= uni
.base64ToArrayBuffer(bodyData
)
fs
.writeFile({
filePath
,
data
: buffer
,
encoding
: 'binary',
success() {
resolve(filePath
)
},
fail(err
) {
reject()
this.$emit('fail', {
error
: err
})
console
.log('获取base64图片失败', err
)
}
})
})
},
getImageInfo(imgSrc
){
return new Promise(async (resolve
, reject
) => {
if(/^data:image\/(\w+);base64/.test(imgSrc
)) {
imgSrc
= await this.base64src(imgSrc
)
}
uni
.getImageInfo({
src
: imgSrc
,
success
: (image
) => {
image
.path
= /^(http|\/\/|\/|wxfile|data:image\/(\w+);base64|file|bdfile)/.test(image
.path
) ? image
.path
: `/${image.path}`
resolve(image
)
},
fail
: (err
) => {
reject();
this.$emit('fail', {
error
: err
})
console
.log('获取图片失败', err
, imgSrc
)
}
});
})
},
downloadImage(url
) {
return new Promise((resolve
, reject
) => {
const downloadTask
= uni
.downloadFile({
url
,
success
: (res
) => {
if(res
.statusCode
!== 200) {
console
.error(`downloadFile ${url} failed res.statusCode is not 200`)
reject();
return;
} else {
resolve(res
.tempFilePath
)
}
},
fail
: (error
) => {
uni
.showToast({
title
: error
})
console
.error(`downloadFile ${url} failed ${JSON.stringify(error)}`);
resolve(url
);
}
})
})
},
measureText(context
, text
, fontSize
) {
return context
.measureText(text
).width
return text
.split("").reduce((widthScaleSum
, char
) => {
let code
= char
.charCodeAt(0);
let widthScale
= CHAR_WIDTH_SCALE_MAP[code
- 0x20] || 1;
return widthScaleSum
+ widthScale
;
}, 0) * fontSize
;
},
calcTextArrs(context
, view
) {
const textArray
= view
.text
.split('\n')
const fontWeight
= view
.css
.fontWeight
=== 'bold' ? 'bold' : 'normal'
const textStyle
= view
.css
.textStyle
=== 'italic' ? 'italic' : 'normal'
const fontWeight
= view
.css
.fontWeight
=== 'bold' ? 'bold' : ''
const textStyle
= view
.css
.textStyle
=== 'italic' ? 'italic' : ''
const fontSize
= view
.css
.fontSize
? this.toNumber(view
.css
.fontSize
) : '20rpx'.toPx()
const fontFamily
= view
.css
.fontFamily
|| 'sans-serif'
context
.font
= `${textStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
let width
= 0
let height
= 0
let lines
= 0
const linesArray
= []
for (let index
= 0; index
< textArray
.length
; index
++) {
const text
= textArray
[index
]
const textLength
= this.measureText(context
, text
, fontSize
)
const minWidth
= fontSize
let partWidth
= view
.css
.width
? this.toNumber(view
.css
.width
) : textLength
if(partWidth
< minWidth
) {
partWidth
= minWidth
}
const calLines
= Math
.ceil(textLength
/ partWidth
)
width
= partWidth
> width
? partWidth
: width
;
lines
+= calLines
;
linesArray
[index
] = calLines
;
}
lines
= view
.css
.maxLines
< lines
? view
.css
.maxLines
: lines
const lineHeight
= view
.css
.lineHeight
? (typeof view
.css
.lineHeight
=== 'number' ? this.toNumber(view
.css
.lineHeight
) * fontSize
: this.toNumber(view
.css
.lineHeight
)) : fontSize
* 1.2
height
= lineHeight
* lines
return {
fontSize
,
width
: width
,
height
: height
,
lines
: lines
,
lineHeight
: lineHeight
,
textArray
: textArray
,
linesArray
: linesArray
,
}
},
drawText(context
, view
) {
return new Promise( async (resolve
, reject
) => {
const {width
, height
, lines
, lineHeight
, textArray
, linesArray
, fontSize
} = this.calcTextArrs(context
, view
)
context
.fillStyle
= (view
.css
?.color
|| 'black')
let lineIndex
= 0
for (let i
= 0; i
< textArray
.length
; i
++) {
const preLineLength
= Math
.ceil(textArray
[i
].length
/ linesArray
[i
])
let start
= 0
let alreadyCount
= 0
for (let j
= 0; j
< linesArray
[i
]; j
++) {
context
.save()
if (lineIndex
>= lines
) {
break;
}
alreadyCount
= preLineLength
let text
= textArray
[i
].substr(start
, alreadyCount
)
let measuredWith
= this.measureText(context
, text
, fontSize
)
while ((start
+ alreadyCount
<= textArray
[i
].length
) && (width
- measuredWith
> fontSize
|| measuredWith
- width
> fontSize
)) {
if (measuredWith
< width
) {
text
= textArray
[i
].substr(start
, ++alreadyCount
);
} else {
if (text
.length
<= 1) {
break;
}
text
= textArray
[i
].substr(start
, --alreadyCount
);
}
measuredWith
= this.measureText(context
, text
, fontSize
)
}
start
+= text
.length
if (lineIndex
=== lines
- 1 && (i
< textArray
.length
- 1 || start
< textArray
[i
].length
)) {
while (this.measureText(context
, `${text}...`, fontSize
) > width
) {
if (text
.length
<= 1) {
break;
}
text
= text
.substring(0, text
.length
- 1);
}
text
+= '...';
measuredWith
= this.measureText(context
, text
, fontSize
)
}
context
.setTextAlign(view
.css
.textAlign
? view
.css
.textAlign
: 'left');
let x
= this.toNumber(view
.css
.left
);
let lineX
;
switch (view
.css
.textAlign
) {
case 'center':
x
= x
+ measuredWith
/ 2 + ((this.toNumber(view
.css
.width
) || this.toNumber(this.boardWidth
, 0 , this.windowWidth
)) - measuredWith
) / 2;
lineX
= x
- measuredWith
/ 2;
break;
case 'right':
x
= x
+ (this.toNumber(view
.css
.width
) || this.toNumber(this.boardWidth
, 0 , this.windowWidth
));
lineX
= x
- measuredWith
;
break;
default:
lineX
= x
;
break;
}
const y
= this.toNumber(view
.css
.top
) + (lineIndex
=== 0 ? fontSize
: (fontSize
+ lineIndex
* lineHeight
))
lineIndex
++;
if (view
.css
.textStyle
=== 'stroke') {
context
.strokeText(text
, x
, y
, measuredWith
)
} else {
context
.fillText(text
, x
, y
, measuredWith
* this.dpr
)
}
if (view
.css
.textDecoration
) {
context
.lineWidth
= fontSize
/ 13;
context
.beginPath();
if (/\bunderline\b/.test(view
.css
.textDecoration
)) {
context
.moveTo(lineX
, y
);
context
.lineTo(lineX
+ measuredWith
, y
);
}
if (/\boverline\b/.test(view
.css
.textDecoration
)) {
context
.moveTo(lineX
, y
- fontSize
);
context
.lineTo(lineX
+ measuredWith
, y
- fontSize
);
}
if (/\bline-through\b/.test(view
.css
.textDecoration
)) {
context
.moveTo(lineX
, y
- fontSize
/ 2.5);
context
.lineTo(lineX
+ measuredWith
, y
- fontSize
/ 2.5);
}
context
.closePath();
context
.strokeStyle
= view
.css
.color
;
context
.stroke();
}
context
.restore()
}
}
setTimeout(() => resolve('ok'), 100)
})
},
drawRect(context
, view
) {
return new Promise((resolve
, reject
) => {
let left
= view
.css
?.left
?.toPx() || 0
let top
= view
.css
?.top
?.toPx() || 0
const width
= view
.css
?.width
.toPx() || 0
const height
= view
.css
?.height
.toPx() || 0
let [topLeftRadius
, topRightRadius
, bottomRightRadius
, bottomLeftRadius
] = view
.css
?.radius
?.split(' ').map((item
) => /^\d/.test(item
) && item
.toPx(0, width
), []) || [0]
let radius
= topLeftRadius
topRightRadius
= topRightRadius
|| topLeftRadius
bottomRightRadius
= bottomRightRadius
|| topLeftRadius
bottomLeftRadius
= bottomLeftRadius
|| topRightRadius
const color
= view
.css
?.backgroundColor
|| view
.css
?.background
|| 'white'
const border
= view
.css
?.border
?.split(' ').map(item
=> /^\d/.test(item
) ? item
.toPx() : item
)
const shadow
= view
.css
?.shadow
const angle
= view
.css
?.rotate
context
.save()
context
.setFillStyle(color
)
if(angle
) {
context
.translate(left
+ width
/ 2, top
+ height
/ 2)
context
.rotate(angle
* Math
.PI / 180)
context
.translate(- left
- width
/ 2 , - top
- height
/ 2)
}
if(shadow
) {
const [x
, y
, b
, c
] = shadow
.split(' ')
context
.shadowOffsetX
= x
.toPx()
context
.shadowOffsetY
= y
.toPx()
context
.shadowBlur
= b
.toPx()
context
.shadowColor
= c
}
if(radius
) {
context
.beginPath()
context
.arc(left
+ width
- (bottomRightRadius
|| radius
), top
+ height
- (bottomRightRadius
|| radius
), (bottomRightRadius
|| radius
), 0, Math
.PI * 0.5)
context
.lineTo(left
+ (bottomLeftRadius
|| radius
), top
+ height
)
context
.arc(left
+ (bottomLeftRadius
|| radius
), top
+ height
- (bottomLeftRadius
|| radius
), (bottomLeftRadius
|| radius
), Math
.PI * 0.5, Math
.PI)
context
.lineTo(left
, top
+ radius
)
context
.arc(left
+ radius
, top
+ radius
, radius
, Math
.PI, Math
.PI * 1.5)
context
.lineTo(left
+ width
- (topRightRadius
|| radius
), top
)
context
.arc(left
+ width
- (topRightRadius
|| radius
), top
+ (topRightRadius
|| radius
), (topRightRadius
|| radius
), Math
.PI * 1.5, Math
.PI * 2)
context
.closePath()
context
.fill()
} else {
context
.fillRect(left
, top
, width
, height
)
}
if(view
?.type
== 'image') {
context
.fillStyle
= 'white'
radius
&& context
.clip()
let rWidth
= view
.width
let rHeight
= view
.height
let startX
= 0
let startY
= 0
const cp
= width
/ height
const op
= rWidth
/ rHeight
if (cp
>= op
) {
rHeight
= rWidth
/ cp
;
} else {
rWidth
= rHeight
* cp
;
startX
= Math
.round((view
.width
- rWidth
) / 2)
}
if (view
.css
&& view
.mode
=== 'scaleToFill') {
context
.drawImage(view
.url
, left
, top
, width
, height
);
} else {
context
.drawImage(view
.url
, startX
, startY
, rWidth
, rHeight
, left
, top
, width
, height
)
}
}
if(border
) {
const lineWidth
= border
[0]
context
.lineWidth
= lineWidth
if(border
[1] == 'dashed') {
context
.setLineDash([Math
.ceil(lineWidth
* 4 / 3), Math
.ceil(lineWidth
* 4 / 3)])
} else if(border
[1] == 'dotted') {
context
.setLineDash([lineWidth
, lineWidth
])
}
context
.setStrokeStyle(border
[2])
if(radius
) {
context
.stroke()
} else {
context
.strokeRect(left
, top
, width
, height
)
}
}
context
.restore()
setTimeout(() => resolve('ok'), 50)
})
},
}
}
</script
>
<style
></style
>
main.js全局注册组件
import poster
from './components/poster.vue'
Vue
.component('poster',poster
)
页面使用
<!-- 分享
-->
<u
-popup v
-model
="showShare" mode
="bottom" height
="100%" class="share">
<view
class="flex1 flex-column vh100">
<view
class="u-flex-1 w100 center" @click
="closePoster">
<poster isRenderImage
:board
="base" @success
="getImg"></poster
>
</view
>
<view
class="bg-white w100">
<view
class="flex1 u-p-30">
<view
class="u-flex-1 u-m-t-10 u-m-b-10 text-center" @click
="toWx(1)">
<image
class="icon-img" style
="width:70rpx;height:70rpx;" src
="../../static/vx-icon.png" mode
=""></image
>
<view
class="gray-1 u-font-24">微信好友
</view
>
</view
>
<view
class="u-flex-1 u-m-t-10 u-m-b-10 text-center" @click
="toWx(2)">
<image
class="icon-img" style
="width:70rpx;height:70rpx;" src
=".../../static/moments-icon.png" mode
=""></image
>
<view
class="gray-1 u-font-24">微信朋友圈
</view
>
</view
>
<view
class="u-flex-1 u-m-t-10 u-m-b-10 text-center" @click
="toSave()">
<image
class="icon-img" style
="width:70rpx;height:70rpx;" src
="../../static/share-save.png" mode
=""></image
>
<view
class="gray-1 u-font-24">保存图片
</view
>
</view
>
<view
class="u-flex-1 u-m-t-10 u-m-b-10 text-center" @click
="toCopy()">
<image
class="icon-img" style
="width:70rpx;height:70rpx;" src
="../../static/link-icon.png" mode
=""></image
>
<view
class="gray-1 u-font-24">复制链接
</view
>
</view
>
</view
>
</view
>
</view
>
</u
-popup
>
data数据
img
:'',
base
: {
width
: '487rpx',
height
: '867rpx',
background
: '#F6F7FB',
views
: [{
type
: 'image',
url
: '../../static/con_share2.png',
css
: {
left
: '0rpx',
top
: '0rpx',
width
: '487rpx',
height
: '867rpx',
}
},
{
type
: 'text',
text
: 'BTC/USDT永续',
css
: {
color
: '#0064E0',
left
: '0rpx',
top
: '500rpx',
fontSize
: '22rpx',
textAlign
: 'center'
}
},
{
type
: 'text',
text
: '+0.43 USDT',
css
: {
color
: '#58B27E',
left
: '0rpx',
top
: '540rpx',
fontSize
: '28rpx',
fontWeight
: 'bold',
textAlign
: 'center'
}
},
{
type
: 'image',
url
: '../../static/con_share2.png',
css
: {
left
: '153rpx',
top
: '590rpx',
width
: '180rpx',
height
: '180rpx',
}
}
]
},
方法
sharePop(i
) {
this.img
= ''
this.order
= this.list2
[i
]
console
.log(1111111111111,this.order
.earnings
);
this.domId
= '#poster'
this.base
.views
[0].url
= parseFloat(this.order
.earnings
) < 0 ? '../../static/con_share1.png' :
'../../static/con_share2.png'
this.base
.views
[1].text
= this.order
.contract_code
+ '永续'
this.base
.views
[2].text
= (parseFloat(this.order
.earnings
) >= 0 ? '+' : '') + parseFloat(this.order
.earnings
).toFixed(
4) + 'USDT'
this.base
.views
[3].url
= this.userInfo
.invite_code_img
this.showShare
= true
uni
.showLoading({
title
: '海报生成中...'
})
},
closePoster(){
this.showShare
= false
uni
.hideLoading()
},
getImg(e
) {
this.img
= e
;
uni
.hideLoading()
},
toWx(type
) {
let _this
= this
if (this.img
) {
uni
.share({
provider
: "weixin",
scene
: type
== 1 ? "WXSceneSession" : "WXSenceTimeline",
type
: 2,
imageUrl
: _this
.img
,
success
: function(res
) {
console
.log("success:" + JSON.stringify(res
));
},
fail
: function(err
) {
console
.log("fail111:" + JSON.stringify(err
));
}
});
} else {
uni
.showToast({
title
: '海报生成中...'
})
}
},
toSave() {
let _this
= this
uni
.saveImageToPhotosAlbum({
filePath
: _this
.img
,
success
: () => {
uni
.showToast({
title
: this.$t('contract.toast9')
})
},
fail
: (err
) => {
console
.log(err
);
uni
.showToast({
title
: this.$t('contract.toast10'),
icon
: 'none'
})
},
});
},
toCopy() {
uni
.setClipboardData({
data
: 'http://teex.aisi120.com/webTeex/?invite_link=6&invite=' + uni
.getStorageSync('userInfo').invite_code
,
success
: function() {
uni
.getClipboardData({
success
: function(res
) {}
});
}
});
},