🐞 fix(ID1000938): 【蜘蛛管家】邀请码- UI还原不足
【蜘蛛管家】邀请码- UI还原不足】 https://www.tapd.cn/53459131/bugtrace/bugs/view/1153459131001000938
This commit is contained in:
parent
fa267fa5a9
commit
e73c87cf6f
@ -5,3 +5,4 @@
|
|||||||
project.*.json
|
project.*.json
|
||||||
*.lock
|
*.lock
|
||||||
*.log
|
*.log
|
||||||
|
iconfont/
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
$am-ms: 200ms;
|
$am-ms: 200ms;
|
||||||
|
|
||||||
.dialog{
|
.dialog {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -14,7 +14,7 @@ $am-ms: 200ms;
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity $am-ms ease-in;
|
transition: opacity $am-ms ease-in;
|
||||||
|
|||||||
268
src/components/painter/index.tsx
Normal file
268
src/components/painter/index.tsx
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import { Canvas } from '@tarojs/components'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
|
||||||
|
import Downloader from './lib/downloader'
|
||||||
|
import Pen from './lib/pen'
|
||||||
|
import { equal, toPx } from './lib/util'
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 入参 palette 格式
|
||||||
|
* const imgDraw = {
|
||||||
|
width: '1124rpx',
|
||||||
|
height: '1578rpx',
|
||||||
|
background: getCDNSource('/user/inviteCodePopup.png'),
|
||||||
|
views: [
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg',
|
||||||
|
css: {
|
||||||
|
top: '32rpx',
|
||||||
|
left: '30rpx',
|
||||||
|
right: '32rpx',
|
||||||
|
width: '688rpx',
|
||||||
|
height: '420rpx',
|
||||||
|
borderRadius: '16rpx',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '青团子',
|
||||||
|
css: {
|
||||||
|
top: '532rpx',
|
||||||
|
fontSize: '28rpx',
|
||||||
|
left: '375rpx',
|
||||||
|
align: 'center',
|
||||||
|
color: '#3c3c3c',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const downloader = new Downloader()
|
||||||
|
|
||||||
|
let SystemInfo = Taro.getSystemInfoSync()
|
||||||
|
|
||||||
|
// 最大尝试的绘制次数
|
||||||
|
const MAX_PAINT_COUNT = 5
|
||||||
|
|
||||||
|
interface PropsType {
|
||||||
|
customStyle?: React.CSSProperties
|
||||||
|
painterStyle?: React.CSSProperties
|
||||||
|
dirty?: boolean
|
||||||
|
palette?: any // 参考上方
|
||||||
|
onImgErr: (result) => void
|
||||||
|
onImgOK: (result) => void
|
||||||
|
}
|
||||||
|
// 绘画
|
||||||
|
const Painter = (props: PropsType, ref) => {
|
||||||
|
const { customStyle, painterStyle: _painterStyle, dirty, palette, onImgErr, onImgOK } = props
|
||||||
|
const [painterStyle, setPainterStyle] = useState(_painterStyle)
|
||||||
|
const canvasWidthInPx = useRef(0)
|
||||||
|
const canvasHeightInPx = useRef(0)
|
||||||
|
const paintCount = useRef(0)
|
||||||
|
|
||||||
|
const [, setForceUpdate] = useState({})
|
||||||
|
const canvasNode = useRef<any>(null)
|
||||||
|
const ctx = useRef<any>(null)
|
||||||
|
/**
|
||||||
|
* 判断一个 object 是否为 空
|
||||||
|
* @param {object} object
|
||||||
|
*/
|
||||||
|
const isEmpty = (object) => {
|
||||||
|
for (const i in object) {
|
||||||
|
if (i) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const isNeedRefresh = (newVal, oldVal) => {
|
||||||
|
if (!newVal || isEmpty(newVal) || (dirty && equal(newVal, oldVal))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadImages = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let preCount = 0
|
||||||
|
let completeCount = 0
|
||||||
|
const paletteCopy = JSON.parse(JSON.stringify(palette))
|
||||||
|
if (paletteCopy.background) {
|
||||||
|
preCount++
|
||||||
|
downloader.download(paletteCopy.background).then((path) => {
|
||||||
|
paletteCopy.background = path
|
||||||
|
completeCount++
|
||||||
|
if (preCount === completeCount) {
|
||||||
|
resolve(paletteCopy)
|
||||||
|
}
|
||||||
|
}, () => {
|
||||||
|
completeCount++
|
||||||
|
if (preCount === completeCount) {
|
||||||
|
resolve(paletteCopy)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (paletteCopy.views) {
|
||||||
|
for (const view of paletteCopy.views) {
|
||||||
|
if (view && view.type === 'image' && view.url) {
|
||||||
|
preCount++
|
||||||
|
console.log('url', view.url)
|
||||||
|
/* eslint-disable no-loop-func */
|
||||||
|
downloader.download(view.url).then((path) => {
|
||||||
|
console.log('path', path)
|
||||||
|
view.url = path
|
||||||
|
Taro.getImageInfo({
|
||||||
|
src: view.url,
|
||||||
|
success: (res) => {
|
||||||
|
// 获得一下图片信息,供后续裁减使用
|
||||||
|
view.sWidth = res.width
|
||||||
|
view.sHeight = res.height
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.log(`imgDownloadErr failed, ${JSON.stringify(error)}`)
|
||||||
|
onImgErr({ error })
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
completeCount++
|
||||||
|
if (preCount === completeCount) {
|
||||||
|
resolve(paletteCopy)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, () => {
|
||||||
|
completeCount++
|
||||||
|
if (preCount === completeCount) {
|
||||||
|
resolve(paletteCopy)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (preCount === 0) {
|
||||||
|
resolve(paletteCopy)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 初始化 canvas
|
||||||
|
const initCanvas = async() => {
|
||||||
|
return new Promise<{ canvas: Taro.Canvas; ctx: Taro.RenderingContext }>((resolve, reject) => {
|
||||||
|
Taro.nextTick(() => {
|
||||||
|
const query = Taro.createSelectorQuery()
|
||||||
|
query.select('#canvas').node(({ node: canvas }: { node: Taro.Canvas }) => {
|
||||||
|
console.log('canvas==>', canvas)
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
console.log('ctx', context)
|
||||||
|
canvasNode.current = canvas
|
||||||
|
ctx.current = context
|
||||||
|
console.log('canvas', canvas)
|
||||||
|
setForceUpdate({})
|
||||||
|
resolve({ canvas: canvasNode.current, ctx: ctx.current })
|
||||||
|
}).exec()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 开始绘制
|
||||||
|
const _startPaint = () => {
|
||||||
|
if (isEmpty(palette)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('startPaint', palette)
|
||||||
|
if (!(SystemInfo && SystemInfo.screenWidth)) {
|
||||||
|
try {
|
||||||
|
SystemInfo = Taro.getSystemInfoSync()
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
const error = `Painter get system info failed, ${JSON.stringify(e)}`
|
||||||
|
// that.triggerEvent('imgErr', { error })
|
||||||
|
onImgErr({ error })
|
||||||
|
console.log(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadImages().then((palette: any) => {
|
||||||
|
const { width, height } = palette
|
||||||
|
canvasWidthInPx.current = toPx(width)
|
||||||
|
canvasHeightInPx.current = toPx(height)
|
||||||
|
if (!width || !height) {
|
||||||
|
console.log(`You should set width and height correctly for painter, width: ${width}, height: ${height}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('palette', palette, width, height)
|
||||||
|
setPainterStyle({
|
||||||
|
width: `${width}`,
|
||||||
|
height: `${height}`,
|
||||||
|
})
|
||||||
|
// 初始化canvas
|
||||||
|
initCanvas().then(({ ctx, canvas }) => {
|
||||||
|
// const ctx = Taro.createCanvasContext('k-canvas', this)
|
||||||
|
const pen = new Pen(ctx, canvas, palette)
|
||||||
|
pen.paint((canvas) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
|
saveImgToLocal(canvas)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getImageInfo = (filePath: string) => {
|
||||||
|
Taro.getImageInfo({
|
||||||
|
src: filePath,
|
||||||
|
success: (infoRes) => {
|
||||||
|
if (paintCount.current > MAX_PAINT_COUNT) {
|
||||||
|
const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`
|
||||||
|
console.log(error)
|
||||||
|
onImgErr({ error })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 比例相符时才证明绘制成功,否则进行强制重绘制
|
||||||
|
if (Math.abs((infoRes.width * canvasHeightInPx.current - canvasWidthInPx.current * infoRes.height) / (infoRes.height * canvasHeightInPx.current)) < 0.01) {
|
||||||
|
onImgOK({ path: filePath })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('infoRes filePath', infoRes, filePath, canvasHeightInPx.current, canvasWidthInPx.current)
|
||||||
|
_startPaint()
|
||||||
|
}
|
||||||
|
paintCount.current++
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.log(`getImageInfo failed, ${JSON.stringify(error)}`)
|
||||||
|
onImgErr({ error })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const saveImgToLocal = (canvas) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.canvasToTempFilePath({
|
||||||
|
canvas,
|
||||||
|
fileType: 'png',
|
||||||
|
success(res) {
|
||||||
|
console.log('tempFilePath', res.tempFilePath, canvas)
|
||||||
|
getImageInfo(res.tempFilePath)
|
||||||
|
},
|
||||||
|
fail(error) {
|
||||||
|
console.log(`canvasToTempFilePath failed, ${JSON.stringify(error)}`)
|
||||||
|
// that.triggerEvent('imgErr', { error })
|
||||||
|
onImgErr({ error })
|
||||||
|
},
|
||||||
|
}, this)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
// 暴露
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => {
|
||||||
|
return {
|
||||||
|
startPaint: () => {
|
||||||
|
paintCount.current = 0
|
||||||
|
_startPaint()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[paintCount.current],
|
||||||
|
)
|
||||||
|
return <Canvas style={{ ...customStyle, ...painterStyle }} id="canvas" type="2d" />
|
||||||
|
}
|
||||||
|
export default forwardRef(Painter)
|
||||||
239
src/components/painter/lib/downloader.ts
Normal file
239
src/components/painter/lib/downloader.ts
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
/**
|
||||||
|
* LRU 文件存储,使用该 downloader 可以让下载的文件存储在本地,下次进入小程序后可以直接使用
|
||||||
|
* 详细设计文档可查看 https://juejin.im/post/5b42d3ede51d4519277b6ce3
|
||||||
|
*/
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import {
|
||||||
|
equal,
|
||||||
|
isValidUrl,
|
||||||
|
} from './util'
|
||||||
|
|
||||||
|
const SAVED_FILES_KEY = 'savedFiles'
|
||||||
|
const KEY_TOTAL_SIZE = 'totalSize'
|
||||||
|
const KEY_PATH = 'path'
|
||||||
|
const KEY_TIME = 'time'
|
||||||
|
const KEY_SIZE = 'size'
|
||||||
|
|
||||||
|
// 可存储总共为 6M,目前小程序可允许的最大本地存储为 10M
|
||||||
|
const MAX_SPACE_IN_B = 6 * 1024 * 1024
|
||||||
|
let savedFiles: Record<string, any> = {}
|
||||||
|
|
||||||
|
export default class Dowloader {
|
||||||
|
constructor() {
|
||||||
|
// app 如果设置了最大存储空间,则使用 app 中的
|
||||||
|
// if (getApp().PAINTER_MAX_LRU_SPACE) {
|
||||||
|
// MAX_SPACE_IN_B = getApp().PAINTER_MAX_LRU_SPACE
|
||||||
|
// }
|
||||||
|
Taro.getStorage({
|
||||||
|
key: SAVED_FILES_KEY,
|
||||||
|
success(res) {
|
||||||
|
if (res.data) {
|
||||||
|
savedFiles = res.data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件,会用 lru 方式来缓存文件到本地
|
||||||
|
* @param {String} url 文件的 url
|
||||||
|
*/
|
||||||
|
download(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!(url && isValidUrl(url))) {
|
||||||
|
resolve(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const file = getFile(url)
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
// 检查文件是否正常,不正常需要重新下载
|
||||||
|
Taro.getSavedFileInfo({
|
||||||
|
filePath: file[KEY_PATH],
|
||||||
|
success: (res) => {
|
||||||
|
resolve(file[KEY_PATH])
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error(`the file is broken, redownload it, ${JSON.stringify(error)}`)
|
||||||
|
downloadFile(url).then((path) => {
|
||||||
|
resolve(path)
|
||||||
|
}, () => {
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
reject()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
downloadFile(url).then((path) => {
|
||||||
|
resolve(path)
|
||||||
|
}, () => {
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
reject()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Taro.downloadFile({
|
||||||
|
url,
|
||||||
|
success(res) {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
console.error(`downloadFile ${url} failed res.statusCode is not 200`)
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
reject()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { tempFilePath } = res
|
||||||
|
Taro.getFileInfo({
|
||||||
|
filePath: tempFilePath,
|
||||||
|
success: (tmpRes) => {
|
||||||
|
const newFileSize = tmpRes.size
|
||||||
|
doLru(newFileSize).then(() => {
|
||||||
|
saveFile(url, newFileSize, tempFilePath).then((filePath) => {
|
||||||
|
resolve(filePath)
|
||||||
|
})
|
||||||
|
}, () => {
|
||||||
|
resolve(tempFilePath)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
// 文件大小信息获取失败,则此文件也不要进行存储
|
||||||
|
console.error(`getFileInfo ${res.tempFilePath} failed, ${JSON.stringify(error)}`)
|
||||||
|
resolve(res.tempFilePath)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail(error) {
|
||||||
|
console.error(`downloadFile failed, ${JSON.stringify(error)} `)
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
reject()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFile(key, newFileSize, tempFilePath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Taro.saveFile({
|
||||||
|
tempFilePath,
|
||||||
|
success: (fileRes) => {
|
||||||
|
const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0
|
||||||
|
savedFiles[key] = {}
|
||||||
|
savedFiles[key][KEY_PATH] = fileRes.savedFilePath
|
||||||
|
savedFiles[key][KEY_TIME] = new Date().getTime()
|
||||||
|
savedFiles[key][KEY_SIZE] = newFileSize
|
||||||
|
savedFiles.totalSize = newFileSize + totalSize
|
||||||
|
Taro.setStorage({
|
||||||
|
key: SAVED_FILES_KEY,
|
||||||
|
data: savedFiles,
|
||||||
|
})
|
||||||
|
resolve(fileRes.savedFilePath)
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error(`saveFile ${key} failed, then we delete all files, ${JSON.stringify(error)}`)
|
||||||
|
// 由于 saveFile 成功后,res.tempFilePath 处的文件会被移除,所以在存储未成功时,我们还是继续使用临时文件
|
||||||
|
resolve(tempFilePath)
|
||||||
|
// 如果出现错误,就直接情况本地的所有文件,因为你不知道是不是因为哪次lru的某个文件未删除成功
|
||||||
|
reset()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有下载相关内容
|
||||||
|
*/
|
||||||
|
function reset() {
|
||||||
|
Taro.removeStorage({
|
||||||
|
key: SAVED_FILES_KEY,
|
||||||
|
success: () => {
|
||||||
|
Taro.getSavedFileList({
|
||||||
|
success: (listRes) => {
|
||||||
|
removeFiles(listRes.fileList)
|
||||||
|
},
|
||||||
|
fail: (getError) => {
|
||||||
|
console.error(`getSavedFileList failed, ${JSON.stringify(getError)}`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function doLru(size) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
let totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0
|
||||||
|
|
||||||
|
if (size + totalSize <= MAX_SPACE_IN_B) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 如果加上新文件后大小超过最大限制,则进行 lru
|
||||||
|
const pathsShouldDelete = []
|
||||||
|
// 按照最后一次的访问时间,从小到大排序
|
||||||
|
const allFiles = JSON.parse(JSON.stringify(savedFiles))
|
||||||
|
delete allFiles[KEY_TOTAL_SIZE]
|
||||||
|
const sortedKeys = Object.keys(allFiles).sort((a, b) => {
|
||||||
|
return allFiles[a][KEY_TIME] - allFiles[b][KEY_TIME]
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const sortedKey of sortedKeys) {
|
||||||
|
totalSize -= savedFiles[sortedKey].size
|
||||||
|
// @ts-expect-error sdfsf
|
||||||
|
pathsShouldDelete.push(savedFiles[sortedKey][KEY_PATH])
|
||||||
|
delete savedFiles[sortedKey]
|
||||||
|
if (totalSize + size < MAX_SPACE_IN_B) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
savedFiles.totalSize = totalSize
|
||||||
|
|
||||||
|
Taro.setStorage({
|
||||||
|
key: SAVED_FILES_KEY,
|
||||||
|
data: savedFiles,
|
||||||
|
success: () => {
|
||||||
|
// 保证 storage 中不会存在不存在的文件数据
|
||||||
|
if (pathsShouldDelete.length > 0) {
|
||||||
|
removeFiles(pathsShouldDelete)
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error(`doLru setStorage failed, ${JSON.stringify(error)}`)
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
reject()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFiles(pathsShouldDelete) {
|
||||||
|
for (const pathDel of pathsShouldDelete) {
|
||||||
|
let delPath = pathDel
|
||||||
|
if (typeof pathDel === 'object') {
|
||||||
|
delPath = pathDel.filePath
|
||||||
|
}
|
||||||
|
Taro.removeSavedFile({
|
||||||
|
filePath: delPath,
|
||||||
|
fail: (error) => {
|
||||||
|
console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFile(key) {
|
||||||
|
if (!savedFiles[key]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
savedFiles[key].time = new Date().getTime()
|
||||||
|
Taro.setStorage({
|
||||||
|
key: SAVED_FILES_KEY,
|
||||||
|
data: savedFiles,
|
||||||
|
})
|
||||||
|
return savedFiles[key]
|
||||||
|
}
|
||||||
400
src/components/painter/lib/pen.ts
Normal file
400
src/components/painter/lib/pen.ts
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { toPx } from './util'
|
||||||
|
|
||||||
|
const SystemInfo = Taro.getSystemInfoSync()
|
||||||
|
export default class Painter {
|
||||||
|
ctx: any
|
||||||
|
canvas: any
|
||||||
|
data: any
|
||||||
|
style: { width: any; height: any }
|
||||||
|
constructor(ctx, canvas, data: any) {
|
||||||
|
this.ctx = ctx
|
||||||
|
this.canvas = canvas
|
||||||
|
this.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
getImageObject = (canvas, src) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log('getImageObject param', canvas, src)
|
||||||
|
const img = canvas.createImage()
|
||||||
|
img.src = src
|
||||||
|
img.onload = () => {
|
||||||
|
console.log('image===>', img)
|
||||||
|
resolve(img)
|
||||||
|
}
|
||||||
|
img.onerror = (err) => {
|
||||||
|
console.log('image error===>', err)
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
paint(callback) {
|
||||||
|
this.style = {
|
||||||
|
width: toPx(this.data.width),
|
||||||
|
height: toPx(this.data.height),
|
||||||
|
}
|
||||||
|
this._background()
|
||||||
|
for (const view of this.data.views) {
|
||||||
|
this._drawAbsolute(view)
|
||||||
|
}
|
||||||
|
callback(this.canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
_background() {
|
||||||
|
this.ctx.save()
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} = this.style
|
||||||
|
// 绘制画布
|
||||||
|
this.canvas.width = width
|
||||||
|
this.canvas.height = height
|
||||||
|
console.log('background', this.canvas.width, this.data.background, width, height)
|
||||||
|
const bg = this.data.background
|
||||||
|
this.ctx.translate(width / 2, height / 2)
|
||||||
|
|
||||||
|
this._doClip(this.data.borderRadius, width, height)
|
||||||
|
if (!bg) {
|
||||||
|
// 如果未设置背景,则默认使用白色
|
||||||
|
this.ctx.fillStyle = '#fff'
|
||||||
|
this.ctx.fillRect(-(width / 2), -(height / 2), width, height)
|
||||||
|
}
|
||||||
|
else if (bg.startsWith('#') || bg.startsWith('rgba') || bg.toLowerCase() === 'transparent') {
|
||||||
|
// 背景填充颜色
|
||||||
|
this.ctx.fillStyle = bg
|
||||||
|
this.ctx.fillRect(-(width / 2), -(height / 2), width, height)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('bg', bg)
|
||||||
|
this.getImageObject(this.canvas, bg).then((image) => {
|
||||||
|
// 背景填充图片
|
||||||
|
this.ctx.drawImage(image, -(width / 2), -(height / 2), width, height)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawAbsolute(view) {
|
||||||
|
// 证明 css 为数组形式,需要合并
|
||||||
|
if (view.css && view.css.length) {
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
// @ts-expect-error sdfsf
|
||||||
|
view.css = Object.assign(...view.css)
|
||||||
|
}
|
||||||
|
switch (view.type) {
|
||||||
|
case 'image':
|
||||||
|
this._drawAbsImage(view)
|
||||||
|
break
|
||||||
|
case 'text':
|
||||||
|
this._fillAbsText(view)
|
||||||
|
break
|
||||||
|
case 'rect':
|
||||||
|
this._drawAbsRect(view)
|
||||||
|
break
|
||||||
|
// case 'qrcode':
|
||||||
|
// this._drawQRCode(view)
|
||||||
|
// break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 borderRadius 进行裁减
|
||||||
|
*/
|
||||||
|
_doClip(borderRadius, width, height) {
|
||||||
|
if (borderRadius && width && height) {
|
||||||
|
const r = Math.min(toPx(borderRadius), width / 2, height / 2)
|
||||||
|
// 防止在某些机型上周边有黑框现象,此处如果直接设置 setFillStyle 为透明,在 Android 机型上会导致被裁减的图片也变为透明, iOS 和 IDE 上不会
|
||||||
|
// setGlobalAlpha 在 1.9.90 起支持,低版本下无效,但把 setFillStyle 设为了 white,相对默认的 black 要好点
|
||||||
|
this.ctx.globalAlpha = 0
|
||||||
|
this.ctx.fillStyle = 'white'
|
||||||
|
this.ctx.beginPath()
|
||||||
|
this.ctx.arc(-width / 2 + r, -height / 2 + r, r, 1 * Math.PI, 1.5 * Math.PI)
|
||||||
|
this.ctx.lineTo(width / 2 - r, -height / 2)
|
||||||
|
this.ctx.arc(width / 2 - r, -height / 2 + r, r, 1.5 * Math.PI, 2 * Math.PI)
|
||||||
|
this.ctx.lineTo(width / 2, height / 2 - r)
|
||||||
|
this.ctx.arc(width / 2 - r, height / 2 - r, r, 0, 0.5 * Math.PI)
|
||||||
|
this.ctx.lineTo(-width / 2 + r, height / 2)
|
||||||
|
this.ctx.arc(-width / 2 + r, height / 2 - r, r, 0.5 * Math.PI, 1 * Math.PI)
|
||||||
|
this.ctx.closePath()
|
||||||
|
this.ctx.fill()
|
||||||
|
// 在 ios 的 6.6.6 版本上 clip 有 bug,禁掉此类型上的 clip,也就意味着,在此版本微信的 ios 设备下无法使用 border 属性
|
||||||
|
if (!(SystemInfo
|
||||||
|
&& SystemInfo?.version as string <= '6.6.6'
|
||||||
|
&& SystemInfo.platform === 'ios')) {
|
||||||
|
this.ctx.clip()
|
||||||
|
}
|
||||||
|
this.ctx.globalAlpha = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 画边框
|
||||||
|
*/
|
||||||
|
_doBorder(view, width, height) {
|
||||||
|
if (!view.css) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
borderRadius,
|
||||||
|
borderWidth,
|
||||||
|
borderColor,
|
||||||
|
} = view.css
|
||||||
|
if (!borderWidth) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.ctx.save()
|
||||||
|
this._preProcess(view, true)
|
||||||
|
let r
|
||||||
|
if (borderRadius) {
|
||||||
|
r = Math.min(toPx(borderRadius), width / 2, height / 2)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
r = 0
|
||||||
|
}
|
||||||
|
const lineWidth = toPx(borderWidth)
|
||||||
|
this.ctx.lineWidth = lineWidth
|
||||||
|
this.ctx.strokeStyle = borderColor || 'black'
|
||||||
|
this.ctx.beginPath()
|
||||||
|
this.ctx.arc(-width / 2 + r, -height / 2 + r, r + lineWidth / 2, 1 * Math.PI, 1.5 * Math.PI)
|
||||||
|
this.ctx.lineTo(width / 2 - r, -height / 2 - lineWidth / 2)
|
||||||
|
this.ctx.arc(width / 2 - r, -height / 2 + r, r + lineWidth / 2, 1.5 * Math.PI, 2 * Math.PI)
|
||||||
|
this.ctx.lineTo(width / 2 + lineWidth / 2, height / 2 - r)
|
||||||
|
this.ctx.arc(width / 2 - r, height / 2 - r, r + lineWidth / 2, 0, 0.5 * Math.PI)
|
||||||
|
this.ctx.lineTo(-width / 2 + r, height / 2 + lineWidth / 2)
|
||||||
|
this.ctx.arc(-width / 2 + r, height / 2 - r, r + lineWidth / 2, 0.5 * Math.PI, 1 * Math.PI)
|
||||||
|
this.ctx.closePath()
|
||||||
|
this.ctx.stroke()
|
||||||
|
this.ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
_preProcess(view, notClip?: boolean) {
|
||||||
|
let width
|
||||||
|
let height
|
||||||
|
let extra
|
||||||
|
switch (view.type) {
|
||||||
|
case 'text': {
|
||||||
|
const fontWeight = view.css.fontWeight === 'bold' ? 'bold' : 'normal'
|
||||||
|
view.css.fontSize = view.css.fontSize ? view.css.fontSize : '20rpx'
|
||||||
|
this.ctx.font = `normal ${fontWeight} ${toPx(view.css.fontSize)}px sans-serif`
|
||||||
|
// this.ctx.setFontSize(view.css.fontSize.toPx());
|
||||||
|
const textLength = this.ctx.measureText(view.text).width
|
||||||
|
width = view.css.width ? toPx(view.css.width) : textLength
|
||||||
|
// 计算行数
|
||||||
|
const calLines = Math.ceil(textLength / width)
|
||||||
|
const lines = view.css.maxLines < calLines ? view.css.maxLines : calLines
|
||||||
|
const lineHeight = view.css.lineHeight ? toPx(view.css.lineHeight) : toPx(view.css.fontSize)
|
||||||
|
height = lineHeight * lines
|
||||||
|
extra = { lines, lineHeight }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'image': {
|
||||||
|
// image 如果未设置长宽,则使用图片本身的长宽
|
||||||
|
const ratio = SystemInfo.pixelRatio ? SystemInfo.pixelRatio : 2
|
||||||
|
width = view.css && view.css.width ? toPx(view.css.width) : Math.round(view.sWidth / ratio)
|
||||||
|
height = view.css && view.css.height ? toPx(view.css.height) : Math.round(view.sHeight / ratio)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (!(view.css.width && view.css.height)) {
|
||||||
|
console.error('You should set width and height')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
width = toPx(view.css.width)
|
||||||
|
height = toPx(view.css.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const x = view.css && view.css.right ? this.style.width - toPx(view.css.right, true) : (view.css && view.css.left ? toPx(view.css.left, true) : 0)
|
||||||
|
const y = view.css && view.css.bottom ? this.style.height - height - toPx(view.css.bottom, true) : (view.css && view.css.top ? toPx(view.css.top, true) : 0)
|
||||||
|
|
||||||
|
const angle = view.css && view.css.rotate ? this._getAngle(view.css.rotate) : 0
|
||||||
|
// 当设置了 right 时,默认 align 用 right,反之用 left
|
||||||
|
const align = view.css && view.css.align ? view.css.align : (view.css && view.css.right ? 'right' : 'left')
|
||||||
|
switch (align) {
|
||||||
|
case 'center':
|
||||||
|
this.ctx.translate(x, y + height / 2)
|
||||||
|
break
|
||||||
|
case 'right':
|
||||||
|
this.ctx.translate(x - width / 2, y + height / 2)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.ctx.translate(x + width / 2, y + height / 2)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.ctx.rotate(angle)
|
||||||
|
if (!notClip && view.css && view.css.borderRadius) {
|
||||||
|
this._doClip(view.css.borderRadius, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
extra,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// _drawQRCode(view) {
|
||||||
|
// this.ctx.save()
|
||||||
|
// const {
|
||||||
|
// width,
|
||||||
|
// height,
|
||||||
|
// } = this._preProcess(view)
|
||||||
|
// QR.api.draw(view.content, this.ctx, -width / 2, -height / 2, width, height, view.css.background, view.css.color)
|
||||||
|
// this.ctx.restore()
|
||||||
|
// this._doBorder(view, width, height)
|
||||||
|
// }
|
||||||
|
|
||||||
|
_drawAbsImage(view) {
|
||||||
|
if (!view.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.ctx.save()
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} = this._preProcess(view)!
|
||||||
|
// 获得缩放到图片大小级别的裁减框
|
||||||
|
let rWidth
|
||||||
|
let rHeight
|
||||||
|
let startX = 0
|
||||||
|
let startY = 0
|
||||||
|
if (width > height) {
|
||||||
|
rHeight = Math.round((view.sWidth / width) * height)
|
||||||
|
rWidth = view.sWidth
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rWidth = Math.round((view.sHeight / height) * width)
|
||||||
|
rHeight = view.sHeight
|
||||||
|
}
|
||||||
|
if (view.sWidth > rWidth) {
|
||||||
|
startX = Math.round((view.sWidth - rWidth) / 2)
|
||||||
|
}
|
||||||
|
if (view.sHeight > rHeight) {
|
||||||
|
startY = Math.round((view.sHeight - rHeight) / 2)
|
||||||
|
}
|
||||||
|
if (view.css && view.css.mode === 'scaleToFill') {
|
||||||
|
this.getImageObject(this.canvas, view.url).then((image) => {
|
||||||
|
this.ctx.drawImage(image, -(width / 2), -(height / 2), width, height)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.getImageObject(this.canvas, view.url).then((image) => {
|
||||||
|
this.ctx.drawImage(image, startX, startY, rWidth, rHeight, -(width / 2), -(height / 2), width, height)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.ctx.restore()
|
||||||
|
this._doBorder(view, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
_fillAbsText(view) {
|
||||||
|
if (!view.text) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.ctx.save()
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
extra,
|
||||||
|
} = this._preProcess(view)!
|
||||||
|
this.ctx.fillStyle = view.css.color || 'black'
|
||||||
|
const { lines, lineHeight } = extra
|
||||||
|
const preLineLength = Math.round(view.text.length / lines)
|
||||||
|
let start = 0
|
||||||
|
let alreadyCount = 0
|
||||||
|
for (let i = 0; i < lines; ++i) {
|
||||||
|
alreadyCount = preLineLength
|
||||||
|
let text = view.text.substr(start, alreadyCount)
|
||||||
|
let measuredWith = this.ctx.measureText(text).width
|
||||||
|
// 如果测量大小小于width一个字符的大小,则进行补齐,如果测量大小超出 width,则进行减除
|
||||||
|
// 如果已经到文本末尾,也不要进行该循环
|
||||||
|
while ((start + alreadyCount <= view.text.length) && (width - measuredWith > toPx(view.css.fontSize) || measuredWith > width)) {
|
||||||
|
if (measuredWith < width) {
|
||||||
|
text = view.text.substr(start, ++alreadyCount)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (text.length <= 1) {
|
||||||
|
// 如果只有一个字符时,直接跳出循环
|
||||||
|
break
|
||||||
|
}
|
||||||
|
text = view.text.substr(start, --alreadyCount)
|
||||||
|
}
|
||||||
|
measuredWith = this.ctx.measureText(text).width
|
||||||
|
}
|
||||||
|
start += text.length
|
||||||
|
// 如果是最后一行了,发现还有未绘制完的内容,则加...
|
||||||
|
if (i === lines - 1 && start < view.text.length) {
|
||||||
|
while (this.ctx.measureText(`${text}...`).width > width) {
|
||||||
|
if (text.length <= 1) {
|
||||||
|
// 如果只有一个字符时,直接跳出循环
|
||||||
|
break
|
||||||
|
}
|
||||||
|
text = text.substring(0, text.length - 1)
|
||||||
|
}
|
||||||
|
text += '...'
|
||||||
|
measuredWith = this.ctx.measureText(text).width
|
||||||
|
}
|
||||||
|
this.ctx.textAlign = view.css.align ? view.css.align : 'left'
|
||||||
|
let x
|
||||||
|
switch (view.css.align) {
|
||||||
|
case 'center':
|
||||||
|
x = 0
|
||||||
|
break
|
||||||
|
case 'right':
|
||||||
|
x = (width / 2)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
x = -(width / 2)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const y = -(height / 2) + (i === 0 ? toPx(view.css.fontSize) : (toPx(view.css.fontSize) + i * lineHeight))
|
||||||
|
if (view.css.textStyle === 'stroke') {
|
||||||
|
this.ctx.strokeText(text, x, y, measuredWith)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.ctx.fillText(text, x, y, measuredWith)
|
||||||
|
}
|
||||||
|
const fontSize = toPx(view.css.fontSize)
|
||||||
|
if (view.css.textDecoration) {
|
||||||
|
this.ctx.beginPath()
|
||||||
|
if (/\bunderline\b/.test(view.css.textDecoration)) {
|
||||||
|
this.ctx.moveTo(x, y)
|
||||||
|
this.ctx.lineTo(x + measuredWith, y)
|
||||||
|
}
|
||||||
|
if (/\boverline\b/.test(view.css.textDecoration)) {
|
||||||
|
this.ctx.moveTo(x, y - fontSize)
|
||||||
|
this.ctx.lineTo(x + measuredWith, y - fontSize)
|
||||||
|
}
|
||||||
|
if (/\bline-through\b/.test(view.css.textDecoration)) {
|
||||||
|
this.ctx.moveTo(x, y - fontSize / 3)
|
||||||
|
this.ctx.lineTo(x + measuredWith, y - fontSize / 3)
|
||||||
|
}
|
||||||
|
this.ctx.closePath()
|
||||||
|
this.ctx.strokeStyle = view.css.color
|
||||||
|
this.ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.restore()
|
||||||
|
this._doBorder(view, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawAbsRect(view) {
|
||||||
|
this.ctx.save()
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} = this._preProcess(view)!
|
||||||
|
this.ctx.fillStyle = view.css.color
|
||||||
|
this.ctx.fillRect(-(width / 2), -(height / 2), width, height)
|
||||||
|
this.ctx.restore()
|
||||||
|
this._doBorder(view, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
_getAngle(angle) {
|
||||||
|
return Number(angle) * Math.PI / 180
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/components/painter/lib/util.ts
Normal file
94
src/components/painter/lib/util.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
|
const SystemInfo = Taro.getSystemInfoSync()
|
||||||
|
export const screenK = SystemInfo.screenWidth / 750
|
||||||
|
|
||||||
|
export function isValidUrl(url: string) {
|
||||||
|
return /(ht|f)tp(s?):\/\/([^ \\/]*\.)+[^ \\/]*(:[0-9]+)?\/?/.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深度对比两个对象是否一致
|
||||||
|
* from: https://github.com/epoberezkin/fast-deep-equal
|
||||||
|
* @param {Object} a 对象a
|
||||||
|
* @param {Object} b 对象b
|
||||||
|
* @return {Boolean} 是否相同
|
||||||
|
*/
|
||||||
|
export function equal(a, b) {
|
||||||
|
if (a === b) { return true }
|
||||||
|
|
||||||
|
if (a && b && typeof a == 'object' && typeof b == 'object') {
|
||||||
|
const arrA = Array.isArray(a)
|
||||||
|
const arrB = Array.isArray(b)
|
||||||
|
let i
|
||||||
|
let length
|
||||||
|
let key
|
||||||
|
|
||||||
|
if (arrA && arrB) {
|
||||||
|
length = a.length
|
||||||
|
if (length != b.length) { return false }
|
||||||
|
for (i = length; i-- !== 0;) { if (!equal(a[i], b[i])) { return false } }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arrA != arrB) { return false }
|
||||||
|
|
||||||
|
const dateA = a instanceof Date
|
||||||
|
const dateB = b instanceof Date
|
||||||
|
if (dateA != dateB) { return false }
|
||||||
|
if (dateA && dateB) { return a.getTime() == b.getTime() }
|
||||||
|
|
||||||
|
const regexpA = a instanceof RegExp
|
||||||
|
const regexpB = b instanceof RegExp
|
||||||
|
if (regexpA != regexpB) { return false }
|
||||||
|
if (regexpA && regexpB) { return a.toString() == b.toString() }
|
||||||
|
|
||||||
|
const keys = Object.keys(a)
|
||||||
|
length = keys.length
|
||||||
|
|
||||||
|
if (length !== Object.keys(b).length) { return false }
|
||||||
|
|
||||||
|
for (i = length; i-- !== 0;) { if (!Object.prototype.hasOwnProperty.call(b, keys[i])) { return false } }
|
||||||
|
|
||||||
|
for (i = length; i-- !== 0;) {
|
||||||
|
key = keys[i]
|
||||||
|
if (!equal(a[key], b[key])) { return false }
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-self-compare
|
||||||
|
return a !== a && b !== b
|
||||||
|
}
|
||||||
|
export /**
|
||||||
|
* 是否支持负数
|
||||||
|
* @param {Boolean} minus 是否支持负数
|
||||||
|
*/
|
||||||
|
const toPx = (num: string, minus = false) => {
|
||||||
|
let reg
|
||||||
|
if (minus) {
|
||||||
|
reg = /^-?[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reg = /^[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g
|
||||||
|
}
|
||||||
|
const results = reg.exec(num)
|
||||||
|
console.log('results', results)
|
||||||
|
if (!num || !results) {
|
||||||
|
console.log(`The size: ${num} is illegal`)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const unit = results[2]
|
||||||
|
const value = parseFloat(num)
|
||||||
|
|
||||||
|
let res = 0
|
||||||
|
if (unit === 'rpx') {
|
||||||
|
res = Math.round(value * screenK)
|
||||||
|
}
|
||||||
|
else if (unit === 'px') {
|
||||||
|
res = value
|
||||||
|
}
|
||||||
|
console.log('res', res)
|
||||||
|
return res
|
||||||
|
}
|
||||||
@ -7,11 +7,11 @@ page {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(to bottom, #E4EEFD 25%, $color_bg_one 42%);
|
background: linear-gradient(to bottom, #e4eefd 25%, $color_bg_one 42%);
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
.content{
|
.content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
@ -24,85 +24,88 @@ page {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
.left{
|
.left {
|
||||||
.title{
|
.title {
|
||||||
padding-bottom: 15px;
|
padding-bottom: 5px;
|
||||||
font-size: 60px;
|
font-size: 60px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.description{
|
.description {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
|
color: #848689;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.right{
|
.right {
|
||||||
.iconContainer{
|
.iconContainer {
|
||||||
width: 260px;
|
width: 260px;
|
||||||
position: relative;
|
position: relative;
|
||||||
bottom: -36px;
|
bottom: -36px;
|
||||||
.icon{
|
.icon {
|
||||||
width: 130%;
|
width: 130%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.codeBar{
|
.inviteCodeContent {
|
||||||
|
position: relative;
|
||||||
|
top: -20px;
|
||||||
|
}
|
||||||
|
.codeBar {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
background-color: #f7f8fa;
|
background-color: #f7f8fa;
|
||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
.inviteCodeBar{
|
.inviteCodeBar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
.invite{
|
.invite {
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
font-size: 46px;
|
font-size: 46px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #337FFF;
|
color: #337fff;
|
||||||
line-height: 65px;
|
line-height: 65px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.tips{
|
.tips {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #9fa0a1;
|
color: #9fa0a1;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
}
|
}
|
||||||
.inviteListTitle{
|
.inviteListTitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
padding-bottom: 30px;
|
padding-bottom: 30px;
|
||||||
.listTitle{
|
.listTitle {
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
.titleIconLeft{
|
.titleIconLeft {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: linear-gradient(270deg, #333333 0%, rgba(51,51,51,0) 100%);
|
background: linear-gradient(270deg, #333333 0%, rgba(51, 51, 51, 0) 100%);
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
.titleIconRight{
|
.titleIconRight {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: linear-gradient(270deg, rgba(51,51,51,0) 0%, #333333 100%);
|
background: linear-gradient(270deg, rgba(51, 51, 51, 0) 0%, #333333 100%);
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.bottomBar {
|
.bottomBar {
|
||||||
flex: none;
|
flex: none;
|
||||||
@ -111,27 +114,31 @@ page {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
padding-right: 24px;
|
padding-right: 48px;
|
||||||
padding-left: 24px;
|
padding-left: 48px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
padding-bottom: calc(20px + constant(safe-area-inset-bottom));
|
padding-bottom: calc(20px + constant(safe-area-inset-bottom));
|
||||||
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
||||||
|
&__text{
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.codePreview{
|
.codePreview {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
.imageContainer{
|
top: -120px;
|
||||||
|
.imageContainer {
|
||||||
width: 80vw;
|
width: 80vw;
|
||||||
height: auto;
|
height: auto;
|
||||||
.image{
|
.image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.previewTips{
|
.previewTips {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -80px;
|
bottom: -80px;
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Canvas, Image, Text, View } from '@tarojs/components'
|
import { Canvas, Image, Text, View } from '@tarojs/components'
|
||||||
import Taro, { useReady } from '@tarojs/taro'
|
import Taro, { useReady } from '@tarojs/taro'
|
||||||
import { useCallback, useRef, useState } from 'react'
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
import { imageAudit } from '@tarojs/taro-h5'
|
import { imageAudit } from '@tarojs/taro-h5'
|
||||||
import style from './index.module.scss'
|
import style from './index.module.scss'
|
||||||
import inviteCodePng from './inviteCode.png'
|
import inviteCodePng from './inviteCodePopupX2.png'
|
||||||
import QRcode from './inviteCodePopup.png'
|
import QRcode from './inviteCodePopup.png'
|
||||||
import Dialog from '@/components/Dialog'
|
import Dialog from '@/components/Dialog'
|
||||||
import LayoutBlock from '@/components/layoutBlock'
|
import LayoutBlock from '@/components/layoutBlock'
|
||||||
@ -14,6 +14,40 @@ import NormalButton from '@/components/normalButton'
|
|||||||
import { alert } from '@/common/common'
|
import { alert } from '@/common/common'
|
||||||
import { GenBarCodeOrQrCode, GetInvitationInfo } from '@/api'
|
import { GenBarCodeOrQrCode, GetInvitationInfo } from '@/api'
|
||||||
import { getCDNSource } from '@/common/constant'
|
import { getCDNSource } from '@/common/constant'
|
||||||
|
import Painter from '@/components/painter'
|
||||||
|
|
||||||
|
const SystemInfo = Taro.getSystemInfoSync()
|
||||||
|
let screenK = 5
|
||||||
|
/**
|
||||||
|
* 是否支持负数
|
||||||
|
* @param {Boolean} minus 是否支持负数
|
||||||
|
*/
|
||||||
|
const toPx = (num: string, minus = false) => {
|
||||||
|
let reg
|
||||||
|
if (minus) {
|
||||||
|
reg = /^-?[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reg = /^[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g
|
||||||
|
}
|
||||||
|
const results = reg.exec(num)
|
||||||
|
console.log('results', results)
|
||||||
|
if (!num || !results) {
|
||||||
|
console.log(`The size: ${num} is illegal`)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const unit = results[2]
|
||||||
|
const value = parseFloat(num)
|
||||||
|
|
||||||
|
let res = 0
|
||||||
|
if (unit === 'rpx') {
|
||||||
|
res = Math.round(value * screenK)
|
||||||
|
}
|
||||||
|
else if (unit === 'px') {
|
||||||
|
res = value
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
// 需要传进来的表头数据示例
|
// 需要传进来的表头数据示例
|
||||||
const inviteColumns = [
|
const inviteColumns = [
|
||||||
{
|
{
|
||||||
@ -29,8 +63,13 @@ const inviteColumns = [
|
|||||||
width: '50%',
|
width: '50%',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 邀请码
|
// 邀请码
|
||||||
const InviteCode = () => {
|
const InviteCode = () => {
|
||||||
|
screenK = SystemInfo.screenWidth / 750
|
||||||
|
|
||||||
const { fetchData } = GetInvitationInfo()
|
const { fetchData } = GetInvitationInfo()
|
||||||
const { fetchData: genCode } = GenBarCodeOrQrCode()
|
const { fetchData: genCode } = GenBarCodeOrQrCode()
|
||||||
const [inviteInfo, setInviteInfo] = useState<any>({})
|
const [inviteInfo, setInviteInfo] = useState<any>({})
|
||||||
@ -71,14 +110,14 @@ const InviteCode = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [, setForceUpdate] = useState({})
|
const [, setForceUpdate] = useState({})
|
||||||
const canvasNode = useRef<any>()
|
const canvasNode = useRef<Taro.Canvas | null>(null)
|
||||||
const ctx = useRef<any>(null)
|
const ctx = useRef<Taro.RenderingContext | null>(null)
|
||||||
const [showPopup, setShowPopup] = useState(false)
|
const [showPopup, setShowPopup] = useState(false)
|
||||||
|
|
||||||
const [targetImageUrl, setTargetImageUrl] = useState('')
|
const [targetImageUrl, setTargetImageUrl] = useState('')
|
||||||
|
|
||||||
const getImageObject = (canvas, src) => {
|
const getImageObject = (canvas: Taro.Canvas, src: string) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<Taro.Image>((resolve, reject) => {
|
||||||
console.log('getImageObject param', canvas, src)
|
console.log('getImageObject param', canvas, src)
|
||||||
const img = canvas.createImage()
|
const img = canvas.createImage()
|
||||||
img.src = src
|
img.src = src
|
||||||
@ -95,6 +134,7 @@ const InviteCode = () => {
|
|||||||
}
|
}
|
||||||
// canvas 生成 图片
|
// canvas 生成 图片
|
||||||
const saveCanvasToImage = (canvas) => {
|
const saveCanvasToImage = (canvas) => {
|
||||||
|
console.log('pixelRatio', SystemInfo.pixelRatio, canvas)
|
||||||
Taro.canvasToTempFilePath({
|
Taro.canvasToTempFilePath({
|
||||||
canvas,
|
canvas,
|
||||||
fileType: 'png',
|
fileType: 'png',
|
||||||
@ -102,6 +142,9 @@ const InviteCode = () => {
|
|||||||
console.log('tempFilePath', res.tempFilePath)
|
console.log('tempFilePath', res.tempFilePath)
|
||||||
setTargetImageUrl(res.tempFilePath)
|
setTargetImageUrl(res.tempFilePath)
|
||||||
},
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.log('error', error)
|
||||||
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
},
|
},
|
||||||
@ -133,44 +176,56 @@ const InviteCode = () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const startPaint = async(ctx, canvas, image) => {
|
|
||||||
console.log('startPaint param', ctx, canvas, image)
|
const doublePick = (num, minus = false) => {
|
||||||
|
if (minus) {
|
||||||
|
return num / 2
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return num * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [canvasStyle, setCanvasStyle] = useState<React.CSSProperties>({})
|
||||||
|
const painter = useRef<any>(null)
|
||||||
|
const startPaint = async(ctx: Taro.RenderingContext, canvas: Taro.Canvas, image: Taro.Image) => {
|
||||||
// 开始绘制
|
// 开始绘制
|
||||||
|
const { width, height } = canvas
|
||||||
|
console.log('startPaint param', ctx, canvas, image)
|
||||||
const cCanvasCtx = ctx as any
|
const cCanvasCtx = ctx as any
|
||||||
cCanvasCtx.clearRect(0, 0, canvas.width, canvas.height)
|
cCanvasCtx.clearRect(0, 0, width, height)
|
||||||
cCanvasCtx.drawImage(image, 0, 0, canvas.width, canvas.height)
|
cCanvasCtx.drawImage(image, 0, 0, width, height)
|
||||||
cCanvasCtx.save()
|
cCanvasCtx.save()
|
||||||
cCanvasCtx.font = `${40}px 微软雅黑`
|
cCanvasCtx.font = `${doublePick(40)}px 微软雅黑`
|
||||||
cCanvasCtx.fillStyle = '#000000'
|
cCanvasCtx.fillStyle = '#000000'
|
||||||
cCanvasCtx.fillText('蜘蛛管家', 40, 80) // text up above canvas
|
cCanvasCtx.fillText('蜘蛛管家', doublePick(40), doublePick(80)) // text up above canvas
|
||||||
cCanvasCtx.save()
|
cCanvasCtx.save()
|
||||||
cCanvasCtx.font = `${26}px 微软雅黑`
|
cCanvasCtx.font = `${doublePick(26)}px 微软雅黑`
|
||||||
cCanvasCtx.fillStyle = '#8f9398'
|
cCanvasCtx.fillStyle = '#8f9398'
|
||||||
cCanvasCtx.fillText('真挚邀请您建立合作关系', 40, 130) // text up above canvas
|
cCanvasCtx.fillText('真挚邀请您建立合作关系', doublePick(40), doublePick(130)) // text up above canvas
|
||||||
cCanvasCtx.save()
|
cCanvasCtx.save()
|
||||||
cCanvasCtx.font = `${24}px 微软雅黑`
|
cCanvasCtx.font = `${doublePick(24)}px 微软雅黑`
|
||||||
cCanvasCtx.fillStyle = '#a6a6a6'
|
cCanvasCtx.fillStyle = '#a6a6a6'
|
||||||
cCanvasCtx.fillText('请前往邀请码页面,进行扫描邀请', 100, 630) // text up above canvas
|
cCanvasCtx.fillText('请前往邀请码页面,进行扫描邀请', doublePick(100), doublePick(630)) // text up above canvas
|
||||||
cCanvasCtx.save()
|
cCanvasCtx.save()
|
||||||
cCanvasCtx.font = `${36}px 微软雅黑`
|
cCanvasCtx.font = `${doublePick(36)}px 微软雅黑`
|
||||||
cCanvasCtx.fillStyle = '#7f7f7f'
|
cCanvasCtx.fillStyle = '#7f7f7f'
|
||||||
cCanvasCtx.fillText('邀 请 码', 72, 730) // text up above canvas
|
cCanvasCtx.fillText('邀 请 码', doublePick(72), doublePick(730)) // text up above canvas
|
||||||
cCanvasCtx.save()
|
cCanvasCtx.save()
|
||||||
cCanvasCtx.font = `${24}px 微软雅黑`
|
cCanvasCtx.font = `${doublePick(24)}px 微软雅黑`
|
||||||
cCanvasCtx.fillStyle = '#cccccc'
|
cCanvasCtx.fillStyle = '#cccccc'
|
||||||
cCanvasCtx.fillText('|', 258, 724) // text up above canvas
|
cCanvasCtx.fillText('|', doublePick(258), doublePick(724)) // text up above canvas
|
||||||
cCanvasCtx.save()
|
cCanvasCtx.save()
|
||||||
cCanvasCtx.font = `${36}px 微软雅黑`
|
cCanvasCtx.font = `${doublePick(36)}px 微软雅黑`
|
||||||
cCanvasCtx.fillStyle = '#7f7f7f'
|
cCanvasCtx.fillStyle = '#7f7f7f'
|
||||||
cCanvasCtx.fillText(`${inviteInfo.invitation_code}`, 311, 730) // text up above canvas
|
cCanvasCtx.fillText(`${inviteInfo.invitation_code}`, doublePick(311), doublePick(730)) // text up above canvas
|
||||||
cCanvasCtx.save()
|
cCanvasCtx.save()
|
||||||
const codeUrl = await genQRcode()
|
const codeUrl = await genQRcode()
|
||||||
try {
|
try {
|
||||||
const code: any = await getImageObject(canvas, codeUrl)
|
const code = await getImageObject(canvas, codeUrl)
|
||||||
cCanvasCtx.drawImage(code, 110, 213, 342, 342)
|
cCanvasCtx.drawImage(code, doublePick(110), doublePick(213), doublePick(342), doublePick(342))
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('合成二维邀请码失败')
|
console.error('合成二维邀请码失败', err)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
throw new Error('合成二维邀请码失败')
|
throw new Error('合成二维邀请码失败')
|
||||||
}
|
}
|
||||||
@ -186,16 +241,21 @@ const InviteCode = () => {
|
|||||||
// 重新初始化canvas
|
// 重新初始化canvas
|
||||||
await initCanvas()
|
await initCanvas()
|
||||||
}
|
}
|
||||||
const canvas = canvasNode.current
|
const canvas = canvasNode.current!
|
||||||
Taro.getImageInfo({
|
Taro.getImageInfo({
|
||||||
src: getCDNSource('/user/inviteCodePopup.png'),
|
src: getCDNSource('/user/inviteCodePopup.png'),
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
canvas.width = res.width / 2
|
console.log('res==>', res)
|
||||||
canvas.height = res.height / 2
|
canvas.width = res.width
|
||||||
|
canvas.height = res.height
|
||||||
|
setCanvasStyle({
|
||||||
|
width: `${doublePick(canvas.width, true)}px`,
|
||||||
|
height: `${doublePick(canvas.height, true)}px`,
|
||||||
|
})
|
||||||
getImageObject(canvas, `${res.path}`).then(async(image) => {
|
getImageObject(canvas, `${res.path}`).then(async(image) => {
|
||||||
try {
|
try {
|
||||||
// 开始绘制
|
// 开始绘制
|
||||||
await startPaint(ctx.current, canvasNode.current, image)
|
await startPaint(ctx.current!, canvas, image)
|
||||||
resolve(true)
|
resolve(true)
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@ -217,6 +277,7 @@ const InviteCode = () => {
|
|||||||
}
|
}
|
||||||
const handleQRcodeShare = async() => {
|
const handleQRcodeShare = async() => {
|
||||||
try {
|
try {
|
||||||
|
// painter.current.startPaint()
|
||||||
const flag = await drawPictorial()
|
const flag = await drawPictorial()
|
||||||
if (flag) {
|
if (flag) {
|
||||||
setShowPopup(true)
|
setShowPopup(true)
|
||||||
@ -233,8 +294,6 @@ const InviteCode = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const handleChange = (value: boolean) => {
|
const handleChange = (value: boolean) => {
|
||||||
console.log('value', value)
|
|
||||||
|
|
||||||
setShowPopup(value)
|
setShowPopup(value)
|
||||||
}
|
}
|
||||||
useReady(() => {
|
useReady(() => {
|
||||||
@ -243,7 +302,19 @@ const InviteCode = () => {
|
|||||||
initCanvas()
|
initCanvas()
|
||||||
}, 200)
|
}, 200)
|
||||||
})
|
})
|
||||||
|
const onImgErr = (e) => {
|
||||||
|
Taro.hideLoading()
|
||||||
|
console.error('onImgErr', e.error)
|
||||||
|
Taro.showToast({
|
||||||
|
title: '生成分享图失败,请刷新页面重试',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onImgOK = (e) => {
|
||||||
|
console.log('onImgOK', e.path)
|
||||||
|
setTargetImageUrl(e.path)
|
||||||
|
setShowPopup(true)
|
||||||
|
Taro.hideLoading()
|
||||||
|
}
|
||||||
return <View className={style.main}>
|
return <View className={style.main}>
|
||||||
<View className={style.content}>
|
<View className={style.content}>
|
||||||
<View className={style.background}>
|
<View className={style.background}>
|
||||||
@ -257,34 +328,38 @@ const InviteCode = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<LayoutBlock circle customStyle={{ paddingTop: '10px', paddingBottom: '10px' }}>
|
<View className={style.inviteCodeContent}>
|
||||||
<View className={style.codeBar}>
|
<LayoutBlock circle>
|
||||||
<View className={style.inviteCodeBar}>
|
<View className={style.codeBar}>
|
||||||
<View className={style.invite}>邀请码</View>
|
<View className={style.inviteCodeBar}>
|
||||||
<Divider direction="vertical" />
|
<View className={style.invite}>邀请码</View>
|
||||||
<View className={style.invite}>{inviteInfo.invitation_code}</View>
|
<Divider direction="vertical" />
|
||||||
|
<View className={style.invite}>{inviteInfo.invitation_code}</View>
|
||||||
|
</View>
|
||||||
|
<View className={style.tips}>填写邀请码,即可在蜘蛛管家下单购物</View>
|
||||||
</View>
|
</View>
|
||||||
<View className={style.tips}>填写邀请码,即可在蜘蛛管家下单购物</View>
|
</LayoutBlock>
|
||||||
</View>
|
<LayoutBlock circle customStyle={{ paddingTop: '10px', paddingBottom: '10px' }}>
|
||||||
</LayoutBlock>
|
<View className={style.inviteListTitle}>
|
||||||
<LayoutBlock circle customStyle={{ paddingTop: '10px', paddingBottom: '10px' }}>
|
<View className={style.titleIconLeft}></View>
|
||||||
<View className={style.inviteListTitle}>
|
<Text className={style.listTitle}>成功邀请</Text>
|
||||||
<View className={style.titleIconLeft}></View>
|
<View className={style.titleIconRight}></View>
|
||||||
<Text className={style.listTitle}>成功邀请</Text>
|
</View>
|
||||||
<View className={style.titleIconRight}></View>
|
<View className={style.inviteList}>
|
||||||
</View>
|
<Table columns={currentTable.columns} emptyText="暂无邀请信息" safeAreaInsetBottom={false} dataSource={currentTable.dataSource} onLoadMore={handleLoadMore}></Table>
|
||||||
<View className={style.inviteList}>
|
</View>
|
||||||
<Table columns={currentTable.columns} emptyText="暂无邀请信息" safeAreaInsetBottom={false} dataSource={currentTable.dataSource} onLoadMore={handleLoadMore}></Table>
|
</LayoutBlock>
|
||||||
</View>
|
<View className={style.tips} style={{ justifyContent: 'flex-start' }}>温馨提示:邀请码确定绑定后,不支持解绑。</View>
|
||||||
</LayoutBlock>
|
</View>
|
||||||
<View className={style.tips} style={{ justifyContent: 'flex-start' }}>温馨提示:邀请码确定绑定后,不支持解绑。</View>
|
|
||||||
</View>
|
</View>
|
||||||
<Canvas style="position: absolute; left: -9999rpx" id="canvas" canvas-id="canvas" type="2d" />
|
{/* <Painter ref={painter} onImgErr={onImgErr} onImgOK={onImgOK} palette={imgDraw} customStyle={{ position: 'absolute', left: '-9999rpx' }}></Painter> */}
|
||||||
|
{/* 已踩坑,这里必须设置canvas的style的width和height,单单只设置canvas实例的width和height是不行的。会模糊! */}
|
||||||
|
<Canvas style={{ position: 'absolute', left: '-9999rpx', ...canvasStyle }} id="canvas" type="2d" />
|
||||||
<View className={style.bottomBar}>
|
<View className={style.bottomBar}>
|
||||||
<NormalButton loading={loading} plain type="primary" customStyles={{ width: '45%' }} round onClick={handleQRcodeShare}>
|
<NormalButton loading={loading} plain type="primary" customTextClassName={style.bottomBar__text} customStyles={{ width: '45%' }} round onClick={handleQRcodeShare}>
|
||||||
二维码分享
|
二维码分享
|
||||||
</NormalButton>
|
</NormalButton>
|
||||||
<NormalButton type="primary" round customStyles={{ width: '45%' }} onClick={handleCopyInviteCode}>
|
<NormalButton type="primary" round customTextClassName={style.bottomBar__text} customStyles={{ width: '45%' }} onClick={handleCopyInviteCode}>
|
||||||
复制邀请码
|
复制邀请码
|
||||||
</NormalButton>
|
</NormalButton>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
BIN
src/pages/inviteCode/inviteCodePopupX2.png
Normal file
BIN
src/pages/inviteCode/inviteCodePopupX2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
BIN
src/pages/inviteCode/inviteCodePopupX3.png
Normal file
BIN
src/pages/inviteCode/inviteCodePopupX3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
Loading…
x
Reference in New Issue
Block a user