初始化代码

This commit is contained in:
2025-12-22 17:13:05 +08:00
parent ed0de08e3a
commit 1f7e9d401b
2947 changed files with 526137 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
## 1.9.3.42022-06-20
- fix: 修复 因创建节点速度问题导致顺序出错。
- fix: 修复 微信小程序 PC 无法显示本地图片
- fix: 修复 flex-box 对齐问题
- feat: 增加 `text-shadow`
- feat: 重写 `text` 对齐方式
- chore: 更新文档
## 1.9.3.32022-06-17
- fix: 修复 支付宝小程序 canvas 2d 存在ctx.draw问题导致报错
- fix: 修复 支付宝小程序 toDataURL 存在权限问题改用 `toTempFilePath`
- fix: 修复 支付宝小程序 image size 问题导致 `objectFit` 无效
## 1.9.3.22022-06-14
- fix: 修复 image 设置背景色不生效问题
- fix: 修复 nvue 环境判断缺少参数问题
## 1.9.3.12022-06-14
- fix: 修复 bottom 定位不对问题
- fix: 修复 因小数导致计算出错换行问题
- feat: 增加 `useCORS` h5端图片跨域 在设置请求头无效果后试一下设置这个值
- chore: 更新文档
## 1.9.32022-06-13
- feat: 增加 `zIndex`
- feat: 增加 `flex-box` 该功能处于原始阶段,非常简陋。
- tips: QQ小程序 vue3 不支持, 为 uni 官方BUG
## 1.9.2.92022-06-10
- fix: 修复`text-align``margin`居中问题
## 1.9.2.82022-06-10
- fix: 修复 Nvue `canvasToTempFilePathSync` 不生效问题
## 1.9.2.72022-06-10
- fix: 修复 margin及padding的bug
- fix: 修复 Nvue `isCanvasToTempFilePath` 不生效问题
## 1.9.2.62022-06-09
- fix: 修复 Nvue 不显示
- feat: 增加支持字体渐变
```html
<l-painter-text
text="水调歌头\n明月几时有把酒问青天。不知天上宫阙今夕是何年。我欲乘风归去又恐琼楼玉宇高处不胜寒。起舞弄清影何似在人间。"
css="background: linear-gradient(,#ff971b 0%, #1989fa 100%); background-clip: text" />
```
## 1.9.2.52022-06-09
- chore: 更变获取父级宽度的设定
- chore: `pathType` 在canvas 2d 默认为 `url`
## 1.9.2.42022-06-08
- fix: 修复 `pathType` 不生效问题
## 1.9.2.32022-06-08
- fix: 修复 `canvasToTempFilePath` 漏写 `success` 参数
## 1.9.2.22022-06-07
- chore: 更新文档
## 1.9.2.12022-06-07
- fix: 修复 vue3 赋值给this再传入导致image无法绘制
- fix: 修复 `canvasToTempFilePathSync` 时机问题
- feat: canvas 2d 更改图片生成方式 `toDataURL`
## 1.9.22022-05-30
- fix: 修复 `canvasToTempFilePathSync` 在 vue3 下只生成一次
## 1.9.1.72022-05-28
- fix: 修复 `qrcode`显示不全问题
## 1.9.1.62022-05-28
- fix: 修复 `canvasToTempFilePathSync` 会重复多次问题
- fix: 修复 `view` css `backgroundImage` 图片下载失败导致 子节点不渲染
## 1.9.1.52022-05-27
- fix: 修正支付宝小程序 canvas 2d版本号 2.7.15
## 1.9.1.42022-05-22
- fix: 修复字节小程序无法使用xml方式
- fix: 修复字节小程序无法使用base64(非2D情况下工具上无法显示)
- fix: 修复支付宝小程序 `canvasToTempFilePath` 报错
## 1.9.1.32022-04-29
- fix: 修复vue3打包后uni对象为空后的报错
## 1.9.1.22022-04-25
- fix: 删除多余文件
## 1.9.1.12022-04-25
- fix: 修复图片不显示问题
## 1.9.12022-04-12
- fix: 因四舍五入导致有些机型错位
- fix: 修复无views报错
- chore: nvue下因ios无法读取插件内static文件改由下载方式
## 1.9.02022-03-20
- fix: 因无法固定尺寸导致生成图片不全
- fix: 特定情况下text判断无效
- chore: 本地化APP Nvue webview
## 1.8.92022-02-20
- fix: 修复 小程序下载最多10次并发的问题
- fix: 修复 APP端无法获取本地图片
- fix: 修复 APP Nvue端不执行问题
- chore: 增加图片缓存机制
## 1.8.8.82022-01-27
- fix: 修复 主动调用尺寸问题
## 1.8.8.62022-01-26
- fix: 修复 nvue 下无宽度时获取父级宽度
- fix: 修复 ios app 无法渲染问题
## 1.8.82022-01-23
- fix: 修复 主动调用时无节点问题
- fix: 修复 `box-shadow` 颜色问题
- fix: 修复 `transform:rotate` 角度位置问题
- feat: 增加 `overflow:hidden`
## 1.8.72022-01-07
- fix: 修复 image 方向为 `right` 时原始宽高问题
- feat: 支持 view 设置背景图 `background-image: url(xxx)`
- chore: 去掉可选链
## 1.8.62021-11-28
- feat: 支持`view``inline-block`的子集使用`text-align`
## 1.8.5.52021-08-17
- chore: 更新文档,删除 replace
- fix: 修复 text 值为 number时报错
## 1.8.5.42021-08-16
- fix: 字节小程序兼容
## 1.8.5.32021-08-15
- fix: 修复线性渐变与css现实效果不一致的问题
- chore: 更新文档
## 1.8.5.22021-08-13
- chore: 增加`background-image``background-repeat` 能力,主要用于背景纹理的绘制,并不是代替`image`。例如:大面积的重复平铺的水印
- 注意这个功能H5暂时无法使用因为[官方的API有BUG](https://ask.dcloud.net.cn/question/128793),待官方修复!!!
## 1.8.5.12021-08-10
- fix: 修复因`margin`报错问题
## 1.8.52021-08-09
- chore: 增加margin支持`auto`,以达到居中效果
## 1.8.42021-08-06
- chore: 增加判断缓存文件条件
- fix: 修复css 多余空格报错问题
## 1.8.32021-08-04
- tips: 1.6.x 以下的版本升级到1.8.x后要为每个元素都加上定位position: 'absolute'
- fix: 修复只有一个view子元素时不计算高度的问题
## 1.8.22021-08-03
- fix: 修复 path-type 为 `url` 无效问题
- fix: 修复 qrcode `text` 为空时报错问题
- fix: 修复 image `src` 动态设置时不生效问题
- feat: 增加 css 属性 `min-width` `max-width`
## 1.8.12021-08-02
- fix: 修复无法加载本地图片
## 1.8.02021-08-02
- chore 文档更新
- 使用旧版的同学不要升级!
## 1.8.0-beta2021-07-30
- ## 全新布局方式 不兼容旧版!
- chore: 布局方式变更
- tips: 微信canvas 2d 不支持真机调试
## 1.6.62021-07-09
- chore: 统一命名规范,无须主动引入组件
## 1.6.52021-06-08
- chore: 去掉console
## 1.6.42021-06-07
- fix: 修复 数字 为纯字符串时不转换的BUG
## 1.6.32021-06-06
- fix: 修复 PC 端放大的BUG
## 1.6.22021-05-31
- fix: 修复 报`adaptor is not a function`错误
- fix: 修复 text 多行高度
- fix: 优化 默认文字的基准线
- feat: `@progress`事件,监听绘制进度
## 1.6.12021-02-28
- 删除多余节点
## 1.6.02021-02-26
- 调整为uni_modules目录规范
- 修复transform的rotate不能为负数问题
- 新增:`pathType` 指定生成图片返回的路径类型,可选值有 `base64``url`

View File

@@ -0,0 +1,147 @@
const styles = (v ='') => v.split(';').filter(v => v && !/^[\n\s]+$/.test(v)).map(v => {
const key = v.slice(0, v.indexOf(':'))
const value = v.slice(v.indexOf(':')+1)
return {
[key
.replace(/-([a-z])/g, function() { return arguments[1].toUpperCase()})
.replace(/\s+/g, '')
]: value.replace(/^\s+/, '').replace(/\s+$/, '') || ''
}
})
export function parent(parent) {
return {
provide() {
return {
[parent]: this
}
},
data() {
return {
el: {
css: {},
views: []
},
}
},
watch: {
css: {
handler(v) {
if(this.canvasId) {
this.el.css = (typeof v == 'object' ? v : v && Object.assign(...styles(v))) || {}
this.canvasWidth = this.el.css && this.el.css.width || this.canvasWidth
this.canvasHeight = this.el.css && this.el.css.height || this.canvasHeight
}
},
immediate: true
}
}
}
}
export function children(parent, options = {}) {
const indexKey = options.indexKey || 'index'
return {
inject: {
[parent]: {
default: null
}
},
watch: {
el: {
handler(v, o) {
if(JSON.stringify(v) != JSON.stringify(o))
this.bindRelation()
},
deep: true,
immediate: true
},
src: {
handler(v, o) {
if(v != o)
this.bindRelation()
},
immediate: true
},
text: {
handler(v, o) {
if(v != o) this.bindRelation()
},
immediate: true
},
css: {
handler(v, o) {
if(v != o)
this.el.css = (typeof v == 'object' ? v : v && Object.assign(...styles(v))) || {}
},
immediate: true
},
replace: {
handler(v, o) {
if(JSON.stringify(v) != JSON.stringify(o))
this.bindRelation()
},
deep: true,
immediate: true
}
},
created() {
if(!this._uid) {
this._uid = this._.uid
}
Object.defineProperty(this, 'parent', {
get: () => this[parent] || [],
})
Object.defineProperty(this, 'index', {
get: () => {
this.bindRelation();
const {parent: {el: {views=[]}={}}={}} = this
return views.indexOf(this.el)
},
});
this.el.type = this.type
this.bindRelation()
},
// #ifdef VUE3
beforeUnmount() {
this.removeEl()
},
// #endif
// #ifdef VUE2
beforeDestroy() {
this.removeEl()
},
// #endif
methods: {
removeEl() {
if (this.parent) {
this.parent.el.views = this.parent.el.views.filter(
(item) => item._uid !== this._uid
);
}
},
bindRelation() {
if(!this.el._uid) {
this.el._uid = this._uid
}
if(['text','qrcode'].includes(this.type)) {
this.el.text = this.$slots && this.$slots.default && this.$slots.default[0].text || `${this.text || ''}`.replace(/\\n/g, '\n')
}
if(this.type == 'image') {
this.el.src = this.src
}
if (!this.parent) {
return;
}
let views = this.parent.el.views || [];
if(views.indexOf(this.el) !== -1) {
this.parent.el.views = views.map(v => v._uid == this._uid ? this.el : v)
} else {
this.parent.el.views = [...views, this.el];
}
}
},
mounted() {
// this.bindRelation()
},
}
}

View File

@@ -0,0 +1,27 @@
<template>
</template>
<script>
import {parent, children} from '../common/relation';
export default {
name: 'lime-painter-image',
mixins:[children('painter')],
props: {
css: [String, Object],
src: String
},
data() {
return {
type: 'image',
el: {
css: {},
src: null
},
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,26 @@
<template>
</template>
<script>
import {parent, children} from '../common/relation';
export default {
name: 'lime-painter-qrcode',
mixins:[children('painter')],
props: {
css: [String, Object],
text: String
},
data() {
return {
type: 'qrcode',
el: {
css: {},
text: null
},
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,28 @@
<template>
<text style="opacity: 0;height: 0;"><slot/></text>
</template>
<script>
import {parent, children} from '../common/relation';
export default {
name: 'lime-painter-text',
mixins:[children('painter')],
props: {
css: [String, Object],
text: [String, Number],
replace: Object,
},
data() {
return {
type: 'text',
el: {
css: {},
text: null
},
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,29 @@
<template>
<view><slot/></view>
</template>
<script>
import {parent, children} from '../common/relation';
export default {
name: 'lime-painter-view',
mixins:[children('painter'), parent('painter')],
props: {
css: [String, Object],
},
data() {
return {
type: 'view',
el: {
css: {},
views:[]
},
}
},
mounted() {
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,409 @@
<template>
<view class="lime-painter" ref="limepainter">
<view v-if="canvasId && size" :style="styles">
<!-- #ifndef APP-NVUE -->
<canvas class="lime-painter__canvas" v-if="use2dCanvas" :id="canvasId" type="2d" :style="size"></canvas>
<canvas class="lime-painter__canvas" v-else :canvas-id="canvasId" :style="size" :id="canvasId"
:width="boardWidth * dpr" :height="boardHeight * dpr"></canvas>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<web-view :style="size" ref="webview"
src="/uni_modules/lime-painter/static/index.html"
class="lime-painter__canvas" @pagefinish="onPageFinish" @error="onError" @onPostMessage="onMessage">
</web-view>
<!-- #endif -->
</view>
<slot />
</view>
</template>
<script>
import { parent } from '../common/relation'
import props from './props'
import {toPx, base64ToPath, pathToBase64, isBase64, sleep, getImageInfo}from './utils';
// #ifndef APP-NVUE
import { compareVersion } from './utils';
import Painter from './painter'
const nvue = {}
// #endif
// #ifdef APP-NVUE
import nvue from './nvue'
// #endif
export default {
name: 'lime-painter',
mixins: [props, parent('painter'), nvue],
data() {
return {
// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
use2dCanvas: true,
// #endif
// #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
use2dCanvas: false,
// #endif
canvasHeight: 150,
canvasWidth: null,
parentWidth: 0,
inited: false,
progress: 0,
firstRender: 0,
done: false
};
},
computed: {
styles() {
return `${this.size}${this.customStyle||''};`
},
canvasId() {
return `l-painter${this._uid || this._.uid}`
},
size() {
if (this.boardWidth && this.boardHeight) {
return `width:${this.boardWidth}px; height: ${this.boardHeight}px;`;
}
},
dpr() {
return this.pixelRatio || uni.getSystemInfoSync().pixelRatio;
},
boardWidth() {
const {width = 0} = (this.elements && this.elements.css) || this.elements || this
const w = toPx(width||this.width)
return w || Math.max(w, toPx(this.canvasWidth));
},
boardHeight() {
const {height = 0} = (this.elements && this.elements.css) || this.elements || this
const h = toPx(height||this.height)
return h || Math.max(h, toPx(this.canvasHeight));
},
hasBoard() {
return this.board && Object.keys(this.board).length
},
elements() {
return JSON.parse(JSON.stringify(this.hasBoard ? this.board : this.el))
}
},
watch: {
// #ifdef MP-WEIXIN || MP-ALIPAY
size(v) {
// #ifdef MP-WEIXIN
if (this.use2dCanvas) {
this.inited = false;
}
// #endif
// #ifdef MP-ALIPAY
this.inited = false;
// #endif
},
// #endif
},
created() {
const { SDKVersion, version, platform } = uni.getSystemInfoSync();
// #ifdef MP-WEIXIN
this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.9.2') >= 0 && !this.isPC;
// #endif
// #ifdef MP-TOUTIAO
this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '1.78.0') >= 0;
// #endif
// #ifdef MP-ALIPAY
this.use2dCanvas = this.type === '2d' && compareVersion(my.SDKVersion, '2.7.15') >= 0;
// #endif
},
async mounted() {
await sleep(30)
await this.getParentWeith()
this.$nextTick(() => {
setTimeout(() => {
this.$watch('elements', this.watchRender, {
deep: true,
immediate: true
});
}, 30)
})
},
methods: {
async watchRender(val, old) {
if (!val || !val.views || (!this.firstRender ? !val.views.length : !this.firstRender) || !Object.keys(val).length || val == old) return;
this.firstRender = 1
clearTimeout(this.rendertimer)
this.rendertimer = setTimeout(() => {
this.render(val);
}, this.beforeDelay)
},
async setFilePath(path, param) {
let filePath = path
const {pathType = this.pathType} = param || this
if (pathType == 'base64' && !isBase64(path)) {
filePath = await pathToBase64(path)
} else if (pathType == 'url' && isBase64(path)) {
filePath = await base64ToPath(path)
}
if (param && param.isEmit) {
this.$emit('success', filePath);
}
return filePath
},
async getSize(args) {
const {width} = args.css || args
const {height} = args.css || args
if (!this.size) {
if (width || height) {
this.canvasWidth = width || this.canvasWidth
this.canvasHeight = height || this.canvasHeight
await sleep(30);
} else {
await this.getParentWeith()
}
}
},
canvasToTempFilePathSync(args) {
this.stopWatch = this.$watch('done', (v) => {
if (v) {
this.canvasToTempFilePath(args)
this.stopWatch && this.stopWatch()
}
}, {
immediate: true
})
},
// #ifndef APP-NVUE
getParentWeith() {
return new Promise(resolve => {
uni.createSelectorQuery()
.in(this)
.select(`.lime-painter`)
.boundingClientRect()
.exec(res => {
const {width, height} = res[0]||{}
this.parentWidth = Math.ceil(width||0)
this.canvasWidth = this.parentWidth || 300
this.canvasHeight = height || this.canvasHeight||150
resolve(res[0])
})
})
},
async render(args = {}) {
if(!Object.keys(args).length) {
return console.error('空对象')
}
this.progress = 0
this.done = false
await this.getSize(args)
const ctx = await this.getContext();
let {
use2dCanvas,
boardWidth,
boardHeight,
canvas,
afterDelay
} = this;
if (use2dCanvas && !canvas) {
return Promise.reject(new Error('render: fail canvas has not been created'));
}
this.boundary = {
top: 0,
left: 0,
width: boardWidth,
height: boardHeight
};
this.painter = null
if (!this.painter) {
const {width} = args.css || args
const {height} = args.css || args
if(!width && this.parentWidth) {
Object.assign(args, {width: this.parentWidth})
}
const param = {
context: ctx,
canvas,
width: boardWidth,
height: boardHeight,
pixelRatio: this.dpr,
useCORS: this.useCORS,
listen: {
onProgress: (v) => {
this.progress = v
this.$emit('progress', v)
},
onEffectFail: (err) => {
this.$emit('faill', err)
}
}
}
if(!use2dCanvas) {
param.createImage = getImageInfo
}
this.painter = new Painter(param, this)
}
// vue3 赋值给data会引起图片无法绘制
const { width, height } = await this.painter.source(JSON.parse(JSON.stringify(args)))
this.boundary.height = this.canvasHeight = height
this.boundary.width = this.canvasWidth = width
await sleep(this.sleep);
if(!use2dCanvas) {
this.painter.init(this.ctx)
}
await this.painter.render()
await new Promise(resolve => this.$nextTick(resolve));
if (!use2dCanvas) {
await this.canvasDraw();
}
if (afterDelay && use2dCanvas) {
await sleep(afterDelay);
}
this.$emit('done');
this.done = true
if (this.isCanvasToTempFilePath) {
this.canvasToTempFilePath()
.then(res => {
this.$emit('success', res.tempFilePath)
})
.catch(err => {
this.$emit('fail', new Error(JSON.stringify(err)));
});
}
return Promise.resolve({
ctx,
draw: this.painter,
node: this.node
});
},
canvasDraw(flag = false) {
return new Promise((resolve, reject) => this.ctx.draw(flag, () => setTimeout(() => resolve(), this
.afterDelay)));
},
async getContext() {
if (!this.canvasWidth) {
this.$emit('fail', 'painter no size')
console.error('painter no size: 请给画板或父级设置尺寸')
return Promise.reject();
}
if (this.ctx && this.inited) {
return Promise.resolve(this.ctx);
}
const { type, use2dCanvas, dpr, boardWidth, boardHeight } = this;
const _getContext = () => {
return new Promise(resolve => {
uni.createSelectorQuery()
.in(this)
.select(`#${this.canvasId}`)
.boundingClientRect()
.exec(res => {
if (res) {
const ctx = uni.createCanvasContext(this.canvasId, this);
if (!this.inited) {
this.inited = true;
this.use2dCanvas = false;
this.canvas = res;
}
// #ifdef MP-ALIPAY
ctx.scale(dpr, dpr);
// #endif
this.ctx = ctx
resolve(this.ctx);
}
});
});
};
if (!use2dCanvas) {
return _getContext();
}
return new Promise(resolve => {
uni.createSelectorQuery()
.in(this)
.select(`#${this.canvasId}`)
.node()
.exec(res => {
let {node: canvas} = res[0];
if (!canvas) {
this.use2dCanvas = false;
resolve(this.getContext());
}
const ctx = canvas.getContext(type);
if (!this.inited) {
this.inited = true;
this.use2dCanvas = true;
this.canvas = canvas;
}
this.ctx = ctx
resolve(this.ctx);
});
});
},
canvasToTempFilePath(args = {}) {
return new Promise(async (resolve, reject) => {
const { use2dCanvas, canvasId, dpr, fileType, quality } = this;
const success = async (res) => {
try {
const tempFilePath = await this.setFilePath(res.tempFilePath || res)
resolve(Object.assign(res, {tempFilePath}))
} catch (e) {
this.$emit('fail', e)
}
}
let { top: y = 0, left: x = 0, width, height } = this.boundary || this;
let destWidth = width * dpr;
let destHeight = height * dpr;
// #ifdef MP-ALIPAY
width = destWidth;
height = destHeight;
// #endif
const copyArgs = Object.assign({
x,
y,
width,
height,
destWidth,
destHeight,
canvasId,
fileType,
quality,
success,
fail: reject
}, args);
if (use2dCanvas) {
try{
// #ifndef MP-ALIPAY
if(!args.pathType && !this.pathType) {args.pathType = 'url'}
const tempFilePath = await this.setFilePath(this.canvas.toDataURL(`image/${args.fileType||fileType}`.replace(/pg/, 'peg'), args.quality||quality), args)
args.success && args.success({tempFilePath})
resolve({tempFilePath})
// #endif
// #ifdef MP-ALIPAY
this.canvas.toTempFilePath(copyArgs)
// #endif
}catch(e){
args.fail && args.fail(e)
reject(e)
}
} else {
// #ifdef MP-ALIPAY
uni.canvasToTempFilePath(copyArgs);
// #endif
// #ifndef MP-ALIPAY
uni.canvasToTempFilePath(copyArgs, this);
// #endif
}
})
}
// #endif
}
};
</script>
<style>
.lime-painter,
.lime-painter__canvas {
// #ifndef APP-NVUE
width: 100%;
// #endif
// #ifdef APP-NVUE
flex: 1;
// #endif
}
</style>

View File

@@ -0,0 +1,218 @@
// #ifdef APP-NVUE
import { sleep, getImageInfo, isBase64, useNvue, networkReg } from './utils';
const dom = weex.requireModule('dom')
import {version } from '../../package.json'
export default {
data() {
return {
tempFilePath: [],
isInitFile: false,
osName: uni.getSystemInfoSync().osName
}
},
created() {
// if (this.hybrid) return
// useNvue('_doc/uni_modules/lime-painter/', version, this.timeout).then(res => {
// this.isInitFile = true
// })
},
methods: {
getParentWeith() {
return new Promise(resolve => {
dom.getComponentRect(this.$refs.limepainter, (res) => {
this.parentWidth = Math.ceil(res.size.width)
this.canvasWidth = this.canvasWidth || this.parentWidth ||300
this.canvasHeight = res.size.height || this.canvasHeight||150
resolve(res.size)
})
})
},
onPageFinish() {
this.webview = this.$refs.webview
this.webview.evalJS(`init(${this.dpr})`)
},
onMessage(e) {
const res = e.detail.data[0] || null;
if (res.event) {
if (res.event == 'inited') {
this.inited = true
}
if(res.event == 'fail'){
this.$emit('fail', res)
}
if (res.event == 'layoutChange') {
const data = typeof res.data == 'string' ? JSON.parse(res.data) : res.data
this.canvasWidth = Math.ceil(data.width);
this.canvasHeight = Math.ceil(data.height);
}
if (res.event == 'progressChange') {
this.progress = res.data * 1
}
if (res.event == 'file') {
this.tempFilePath.push(res.data)
if (this.tempFilePath.length > 7) {
this.tempFilePath.shift()
}
return
}
if (res.event == 'success') {
if (res.data) {
this.tempFilePath.push(res.data)
if (this.tempFilePath.length > 8) {
this.tempFilePath.shift()
}
if (this.isCanvasToTempFilePath) {
this.setFilePath(this.tempFilePath.join(''), {isEmit:true})
}
} else {
this.$emit('fail', 'canvas no data')
}
return
}
this.$emit(res.event, JSON.parse(res.data));
} else if (res.file) {
this.file = res.data;
} else{
console.info(res[0])
}
},
getWebViewInited() {
if (this.inited) return Promise.resolve(this.inited);
return new Promise((resolve) => {
this.$watch(
'inited',
async val => {
if (val) {
resolve(val)
}
}, {
immediate: true
}
);
})
},
getTempFilePath() {
if (this.tempFilePath.length == 8) return Promise.resolve(this.tempFilePath)
return new Promise((resolve) => {
this.$watch(
'tempFilePath',
async val => {
if (val.length == 8) {
resolve(val.join(''))
}
}
);
})
},
getWebViewDone() {
if (this.progress == 1) return Promise.resolve(this.progress);
return new Promise((resolve) => {
this.$watch(
'progress',
async val => {
if (val == 1) {
this.$emit('done')
this.done = true
resolve(val)
}
}, {
immediate: true
}
);
})
},
async render(args) {
try {
await this.getSize(args)
const {width} = args.css || args
if(!width && this.parentWidth) {
Object.assign(args, {width: this.parentWidth})
}
const newNode = await this.calcImage(args);
await this.getWebViewInited()
this.webview.evalJS(`source(${JSON.stringify(newNode)})`)
await this.getWebViewDone()
await sleep(this.afterDelay)
if (this.isCanvasToTempFilePath) {
const params = {
fileType: this.fileType,
quality: this.quality
}
this.webview.evalJS(`save(${JSON.stringify(params)})`)
}
return Promise.resolve()
} catch (e) {
this.$emit('fail', e)
}
},
getfile(e){
let url = plus.io.convertLocalFileSystemURL( e )
return new Promise((resolve,reject)=>{
plus.io.resolveLocalFileSystemURL(url, entry => {
var reader = null;
entry.file( file => {
reader = new plus.io.FileReader();
reader.onloadend = ( read )=> {
resolve(read.target.result)
};
reader.readAsDataURL( file );
}, function ( error ) {
alert( error.message );
} );
},err=>{
resolve(e)
})
})
},
async calcImage(args) {
let node = JSON.parse(JSON.stringify(args))
const urlReg = /url\((.+)\)/
const {backgroundImage} = node.css||{}
const isBG = backgroundImage && urlReg.exec(backgroundImage)[1]
const url = node.url || node.src || isBG
if(['text', 'qrcode'].includes(node.type)) {
return node
}
if ((node.type === "image" || isBG) && url && !isBase64(url) && (this.osName == 'ios' ? true : !networkReg.test(url))) {
let {path} = await getImageInfo(url)
if(this.osName == 'ios') {
path = await this.getfile(path)
}
if (isBG) {
node.css.backgroundImage = `url(${path})`
} else {
node.src = path
}
} else if (node.views && node.views.length) {
for (let i = 0; i < node.views.length; i++) {
node.views[i] = await this.calcImage(node.views[i])
}
}
return node
},
async canvasToTempFilePath(args = {}) {
if (!this.inited) {
return this.$emit('fail', 'no init')
}
this.tempFilePath = []
if (args.fileType == 'jpg') {
args.fileType = 'jpeg'
}
this.webview.evalJS(`save(${JSON.stringify(args)})`)
try {
let tempFilePath = await this.getTempFilePath()
tempFilePath = await this.setFilePath(tempFilePath)
args.success({
errMsg: "canvasToTempFilePath:ok",
tempFilePath
})
} catch (e) {
args.fail({
error: e
})
}
}
}
}
// #endif

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,50 @@
export default {
props: {
board: Object,
pathType: String, // 'base64'、'url'
fileType: {
type: String,
default: 'png'
},
quality: {
type: Number,
default: 1
},
css: [String, Object],
// styles: [String, Object],
width: [Number, String],
height: [Number, String],
pixelRatio: Number,
customStyle: String,
isCanvasToTempFilePath: Boolean,
// useCanvasToTempFilePath: Boolean,
sleep: {
type: Number,
default: 1000 / 30
},
beforeDelay: {
type: Number,
default: 100
},
afterDelay: {
type: Number,
default: 100
},
// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
type: {
type: String,
default: '2d'
},
// #endif
// #ifdef APP-NVUE
hybrid: Boolean,
timeout: {
type: Number,
default: 2000
},
// #endif
// #ifdef H5
useCORS: Boolean
// #endif
}
}

View File

@@ -0,0 +1,469 @@
export const networkReg = /^(http|\/\/)/;
export const isBase64 = (path) => /^data:image\/(\w+);base64/.test(path);
export function sleep(delay) {
return new Promise(resolve => setTimeout(resolve, delay))
}
const isDev = ['devtools'].includes(uni.getSystemInfoSync().platform)
// 缓存图片
let cache = {}
export function isNumber(value) {
return /^-?\d+(\.\d+)?$/.test(value);
}
export function toPx(value, baseSize, isDecimal = false) {
// 如果是数字
if (typeof value === 'number') {
return value
}
// 如果是字符串数字
if (isNumber(value)) {
return value * 1
}
// 如果有单位
if (typeof value === 'string') {
const reg = /^-?([0-9]+)?([.]{1}[0-9]+){0,1}(em|rpx|px|%)$/g
const results = reg.exec(value);
if (!value || !results) {
return 0;
}
const unit = results[3];
value = parseFloat(value);
let res = 0;
if (unit === 'rpx') {
res = uni.upx2px(value);
} else if (unit === 'px') {
res = value * 1;
} else if (unit === '%') {
res = value * toPx(baseSize) / 100;
} else if (unit === 'em') {
res = value * toPx(baseSize || 14);
}
return isDecimal ? res.toFixed(2) * 1 : Math.round(res);
}
return 0
}
// 计算版本
export function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
// #ifdef MP
export const prefix = () => {
// #ifdef MP-TOUTIAO
return tt
// #endif
// #ifdef MP-WEIXIN
return wx
// #endif
// #ifdef MP-BAIDU
return swan
// #endif
// #ifdef MP-ALIPAY
return my
// #endif
// #ifdef MP-QQ
return qq
// #endif
// #ifdef MP-360
return qh
// #endif
}
// #endif
const base64ToArrayBuffer = (data) => {
// #ifndef MP-WEIXIN || APP-PLUS
/**
* Base64Binary.decode(base64_string);
* Base64Binary.decodeArrayBuffer(base64_string);
*/
const Base64Binary = {
_keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
/* will return a Uint8Array type */
decodeArrayBuffer(input) {
const bytes = (input.length / 4) * 3;
const ab = new ArrayBuffer(bytes);
this.decode(input, ab);
return ab;
},
removePaddingChars(input) {
const lkey = this._keyStr.indexOf(input.charAt(input.length - 1));
if (lkey == 64) {
return input.substring(0, input.length - 1);
}
return input;
},
decode(input, arrayBuffer) {
//get last chars to see if are valid
input = this.removePaddingChars(input);
input = this.removePaddingChars(input);
const bytes = parseInt((input.length / 4) * 3, 10);
let uarray;
let chr1, chr2, chr3;
let enc1, enc2, enc3, enc4;
let i = 0;
let j = 0;
if (arrayBuffer)
uarray = new Uint8Array(arrayBuffer);
else
uarray = new Uint8Array(bytes);
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
for (i = 0; i < bytes; i += 3) {
//get the 3 octects in 4 ascii chars
enc1 = this._keyStr.indexOf(input.charAt(j++));
enc2 = this._keyStr.indexOf(input.charAt(j++));
enc3 = this._keyStr.indexOf(input.charAt(j++));
enc4 = this._keyStr.indexOf(input.charAt(j++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
uarray[i] = chr1;
if (enc3 != 64) uarray[i + 1] = chr2;
if (enc4 != 64) uarray[i + 2] = chr3;
}
return uarray;
}
}
return Base64Binary.decodeArrayBuffer(data)
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
return uni.base64ToArrayBuffer(data)
// #endif
}
/**
* base64转路径
* @param {Object} base64
*/
export function base64ToPath(base64) {
const [, format] = /^data:image\/(\w+);base64,/.exec(base64) || [];
return new Promise((resolve, reject) => {
// #ifdef MP
const fs = uni.getFileSystemManager()
//自定义文件名
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const time = new Date().getTime();
let pre = prefix()
const filePath = `${pre.env.USER_DATA_PATH}/${time}.${format}`
//let buffer = base64ToArrayBuffer(bodyData)
fs.writeFile({
filePath,
data: base64.split(',')[1], //base64.replace(/^data:\S+\/\S+;base64,/, ''),
encoding: 'base64',
// data: buffer,
// encoding: 'binary',
success() {
resolve(filePath)
},
fail(err) {
reject(err)
}
})
// #endif
// #ifdef H5
// mime类型
let mimeString = base64.split(',')[0].split(':')[1].split(';')[0];
//base64 解码
let byteString = atob(base64.split(',')[1]);
//创建缓冲数组
let arrayBuffer = new ArrayBuffer(byteString.length);
//创建视图
let intArray = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
intArray[i] = byteString.charCodeAt(i);
}
resolve(URL.createObjectURL(new Blob([intArray], {
type: mimeString
})))
// #endif
// #ifdef APP-PLUS
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
bitmap.loadBase64Data(base64, () => {
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const time = new Date().getTime();
const filePath = `_doc/uniapp_temp/${time}.${format}`
bitmap.save(filePath, {},
() => {
bitmap.clear()
resolve(filePath)
},
(error) => {
bitmap.clear()
reject(error)
})
}, (error) => {
bitmap.clear()
reject(error)
})
// #endif
})
}
/**
* 路径转base64
* @param {Object} string
*/
export function pathToBase64(path) {
if (/^data:/.test(path)) return path
return new Promise((resolve, reject) => {
// #ifdef H5
let image = new Image();
image.setAttribute("crossOrigin", 'Anonymous');
image.onload = function() {
let canvas = document.createElement('canvas');
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
canvas.getContext('2d').drawImage(image, 0, 0);
let result = canvas.toDataURL('image/png')
resolve(result);
canvas.height = canvas.width = 0
}
image.src = path + '?v=' + Math.random()
image.onerror = (error) => {
reject(error);
};
// #endif
// #ifdef MP
if (uni.canIUse('getFileSystemManager')) {
uni.getFileSystemManager().readFile({
filePath: path,
encoding: 'base64',
success: (res) => {
resolve('data:image/png;base64,' + res.data)
},
fail: (error) => {
reject(error)
}
})
}
// #endif
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), (entry) => {
entry.file((file) => {
const fileReader = new plus.io.FileReader()
fileReader.onload = (data) => {
resolve(data.target.result)
}
fileReader.onerror = (error) => {
reject(error)
}
fileReader.readAsDataURL(file)
}, reject)
}, reject)
// #endif
})
}
export function getImageInfo(path, useCORS) {
return new Promise(async (resolve, reject) => {
let src = path
if (cache[path] && cache[path].errMsg) {
resolve(cache[path])
} else {
try {
// if (!isBase64 && PLATFORM == UNI_PLATFORM.PLUS && !/^\/?(static|_doc)\//.test(src)) {
// src = await downloadFile(path) as string
// } else
// #ifdef MP || APP-PLUS
if (isBase64(path)) {
src = await base64ToPath(path)
}
// #endif
// #ifdef H5
if(useCORS) {
src = await pathToBase64(path)
}
// #endif
} catch (error) {
reject({
...error,
src
})
}
uni.getImageInfo({
src,
success: (image) => {
const localReg = /^\.|^\/(?=[^\/])/;
// #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-TOUTIAO
image.path = localReg.test(src) ? `/${image.path}` : image.path;
// #endif
// #ifdef H5
image.path = image.path.replace(/^\./, window.location.origin)
// #endif
if (isDev) {
resolve(image)
} else {
cache[path] = image
resolve(cache[path])
}
},
fail(err) {
reject({
err,
path
})
}
})
}
})
}
export function downloadFile(url) {
if (!url) return Promise.reject({
err: 'no url'
})
return new Promise((resolve, reject) => {
if (cache[url]) {
return reject()
}
cache[url] = 1
uni.downloadFile({
url,
success(res) {
resolve(res)
},
fail(err) {
reject(err)
}
})
})
}
// #ifdef APP-PLUS
const getLocalFilePath = (path) => {
if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path
.indexOf('_downloads') === 0) {
return path
}
if (path.indexOf('file://') === 0) {
return path
}
if (path.indexOf('/storage/emulated/0/') === 0) {
return path
}
if (path.indexOf('/') === 0) {
const localFilePath = plus.io.convertAbsoluteFileSystem(path)
if (localFilePath !== path) {
return localFilePath
} else {
path = path.substr(1)
}
}
return '_www/' + path
}
const getFile = (url) => {
return new Promise((resolve, rejcet) => {
plus.io.resolveLocalFileSystemURL(url, resolve, (err) => {
resolve(false)
})
})
}
const createFile = ({
fs,
url,
target,
name
}) => {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(url, res1 => {
fs.root.getDirectory(target, {
create: true
}, fileEntry => {
const success = () => {
res1.remove()
resolve()
}
getFile(target + name).then(res => {
if (res) {
res.remove((res2) => {
res1.moveTo(fileEntry, name, success, reject)
})
}
res1.moveTo(fileEntry, name, success, reject)
})
})
}, reject)
})
}
export function useNvue(target, version, timeout) {
return new Promise((resolve, reject) => {
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, async (fs) => {
try {
cache['lime-painter'] = 0
let names = ['uni.webview.1.5.3.js', 'painter.js', 'index.html']
let urls = ['https://gitee.com/dcloud/uni-app/raw/dev/dist/',
'https://static-6d65bd90-8508-4d6c-abbc-a4ef5c8e49e7.bspapp.com/lime-painter/'
]
const oldVersion = plus.storage.getItem('lime-painter')
const isFile = await getFile(`${target}${names[1]}`)
if (isFile && oldVersion && compareVersion(oldVersion, version) >= 0) {
resolve()
} else {
for (var i = 0; i < names.length; i++) {
const name = names[i]
const file = await downloadFile(urls[i >= 1 ? 1 : 0] + name)
await createFile({
fs,
url: file.tempFilePath,
target,
name: name.includes('uni.webview') ? 'uni.webview.js' : name
})
}
plus.storage.setItem('lime-painter', version)
cache['lime-painter'] = version
resolve()
}
} catch (e) {
let index = parseInt(timeout / 20)
while (!cache['lime-painter'] && index) {
await sleep(20)
index--
}
if (cache['lime-painter']) {
resolve()
} else {
reject(e)
}
}
}, reject)
})
}
// #endif

View File

@@ -0,0 +1,2 @@
<template>
</template>

View File

@@ -0,0 +1,96 @@
{
"id": "lime-painter",
"displayName": "海报画板",
"version": "1.9.3.4",
"description": "一款canvas海报组件更优雅的海报生成方案",
"keywords": [
"海报",
"canvas",
"生成海报",
"生成二维码",
"JSON"
],
"repository": "https://gitee.com/liangei/lime-painter",
"engines": {
"HBuilderX": "^3.4.14"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "305716444"
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u",
"小红书": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
},
"name": "lime-painter",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,910 @@
# Painter 画板 测试版
> uniapp 海报画板,更优雅的海报生成方案
> [查看更多 站点 1](https://limeui.qcoon.cn/#/painter)
> [查看更多 站点 2](http://liangei.gitee.io/limeui/#/painter)
> Q 群806744170
## 平台兼容
| H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App |
| --- | ---------- | ------------ | ---------- | ---------- | --------- | --- |
| √ | √ | √ | 未测 | √ | √ | √ |
## 安装
在市场导入[海报画板](https://ext.dcloud.net.cn/plugin?id=2389)uni_modules版本的即可无需`import`
## 代码演示
### 基本用法
- 插件提供 JSON 及 XML 的方式绘制海报
- 参考了 css 块状流布局模拟 css schema 方式。
- 使用JSON的方式时请使用驼峰key
#### 方式一 XML
- 提供`l-painter-view``l-painter-text``l-painter-image``l-painter-qrcode`四种类型组件
- 通过 `css` 属性绘制样式,与 style 使用方式保持一致。
```html
<l-painter>
<l-painter-view
css="background: #07c160; height: 120rpx; width: 120rpx; display: inline-block"
></l-painter-view>
<l-painter-view
css="background: #1989fa; height: 120rpx; width: 120rpx; border-top-right-radius: 60rpx; border-bottom-left-radius: 60rpx; display: inline-block; margin: 0 30rpx;"
></l-painter-view>
<l-painter-view
css="background: #ff9d00; height: 120rpx; width: 120rpx; border-radius: 50%; display: inline-block"
></l-painter-view>
</l-painter>
```
#### 方式二 JSON
- 在 json 里四种类型组件的`type``view``text``image``qrcode`
- 通过 `board` 设置海报所需的 JSON 数据进行绘制或`ref`获取组件实例调用组件内的`render(json)`
- 所有类型的 schema 都具有`css`字段css 的 key 值使用**驼峰**如:`lineHeight`
```html
<l-painter :board="poster"/>
```
```js
data() {
return {
poster: {
css: {
// 根节点若无尺寸,自动获取父级节点
width: '750rpx'
},
views: [
{
css: {
background: "#07c160",
height: "120rpx",
width: "120rpx",
display: "inline-block"
},
type: "view"
},
{
css: {
background: "#1989fa",
height: "120rpx",
width: "120rpx",
borderTopRightRadius: "60rpx",
borderBottomLeftRadius: "60rpx",
display: "inline-block",
margin: "0 30rpx"
},
views: [],
type: "view"
},
{
css: {
background: "#ff9d00",
height: "120rpx",
width: "120rpx",
borderRadius: "50%",
display: "inline-block"
},
views: [],
type: "view"
},
]
}
}
}
```
### View 容器
- 类似于 `div` 可以嵌套承载更多的 view、text、imageqrcode 共同构建一颗完整的节点树
- 在 JSON 里具有 `views` 的数组字段,用于嵌套承载节点。
#### 方式一 XML
```html
<l-painter>
<l-painter-view css="background: #f0f0f0; padding-top: 100rpx;">
<l-painter-view
css="background: #d9d9d9; width: 33.33%; height: 100rpx; display: inline-block"
></l-painter-view>
<l-painter-view
css="background: #bfbfbf; width: 66.66%; height: 100rpx; display: inline-block"
></l-painter-view>
</l-painter-view>
</l-painter>
```
#### 方式二 JSON
```js
{
css: {},
views: [
{
type: 'view',
css: {
background: '#f0f0f0',
paddingTop: '100rpx'
},
views: [
{
type: 'view',
css: {
background: '#d9d9d9',
width: '33.33%',
height: '100rpx',
display: 'inline-block'
}
},
{
type: 'view',
css: {
background: '#bfbfbf',
width: '66.66%',
height: '100rpx',
display: 'inline-block'
}
}
],
}
]
}
```
### Text 文本
- 通过 `text` 属性填写文本内容。
- 支持`\n`换行符
- 支持省略号,使用 css 的`line-clamp`设置行数,当文字内容超过会显示省略号。
- 支持`text-decoration`
#### 方式一 XML
```html
<l-painter>
<l-painter-view css="background: #e0e2db; padding: 30rpx; color: #222a29">
<l-painter-text
text="登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼"
/>
<l-painter-text
text="登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼"
css="text-align:center; padding-top: 20rpx; text-decoration: line-through "
/>
<l-painter-text
text="登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼"
css="text-align:right; padding-top: 20rpx"
/>
<l-painter-text
text="水调歌头\n明月几时有把酒问青天。不知天上宫阙今夕是何年。我欲乘风归去又恐琼楼玉宇高处不胜寒。起舞弄清影何似在人间。"
css="line-clamp: 3; padding-top: 20rpx; background: linear-gradient(,#ff971b 0%, #ff5000 100%); background-clip: text"
/>
</l-painter-view>
</l-painter>
```
#### 方式二 JSON
```js
// 基础用法
{
type: 'text',
text: '登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼',
},
{
type: 'text',
text: '登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼',
css: {
// 设置居中对齐
textAlign: 'center',
// 设置中划线
textDecoration: 'line-through'
}
},
{
type: 'text',
text: '登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼',
css: {
// 设置右对齐
textAlign: 'right',
}
},
{
type: 'text',
text: '登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼',
css: {
// 设置行数,超出显示省略号
lineClamp: 3,
// 渐变文字
background: 'linear-gradient(,#ff971b 0%, #1989fa 100%)',
backgroundClip: 'text'
}
}
```
### Image 图片
- 通过 `src` 属性填写图片路径。
- 图片路径支持:网络图片,本地 static 里的图片路径,缓存路径
- 通过 `css``object-fit`属性可以设置图片的填充方式,可选值见下方 CSS 表格。
- 通过 `css``object-position`配合 `object-fit` 可以设置图片的对齐方式,类似于`background-position`,详情见下方 CSS 表格。
- 使用网络图片时:小程序需要去公众平台配置 [downloadFile](https://mp.weixin.qq.com/) 域名
- 使用网络图片时:**H5 和 Nvue 需要决跨域问题**
#### 方式一 XML
```html
<l-painter>
<!-- 基础用法 -->
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="width: 200rpx; height: 200rpx"
/>
<!-- 填充方式 -->
<!-- css object-fit 设置 填充方式 见下方表格-->
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="width: 200rpx; height: 200rpx; object-fit: contain; background: #eee"
/>
<!-- css object-position 设置 图片的对齐方式-->
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="width: 200rpx; height: 200rpx; object-fit: contain; object-position: 50% 50%; background: #eee"
/>
</l-painter>
```
#### 方式二 JSON
```js
// 基础用法
{
type: 'image',
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
css: {
width: '200rpx',
height: '200rpx'
}
},
// 填充方式
// css objectFit 设置 填充方式 见下方表格
{
type: 'image',
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
css: {
width: '200rpx',
height: '200rpx',
objectFit: 'contain'
}
},
// css objectPosition 设置 图片的对齐方式
{
type: 'image',
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
css: {
width: '200rpx',
height: '200rpx',
objectFit: 'contain',
objectPosition: '50% 50%'
}
}
```
### Qrcode 二维码
- 通过`text`属性填写需要生成二维码的文本。
- 通过 `css` 里的 `color` 可设置生成码点的颜色。
- 通过 `css` 里的 `background`可设置背景色。
- 通过 `css `里的 `width``height`设置尺寸。
#### 方式一 XML
```html
<l-painter>
<l-painter-qrcode
text="limeui.qcoon.cn"
css="width: 200rpx; height: 200rpx"
/>
</l-painter>
```
#### 方式二 JSON
```js
{
type: 'qrcode',
text: 'limeui.qcoon.cn',
css: {
width: '200rpx',
height: '200rpx',
}
}
```
### 生成图片
- 1、通过设置`isCanvasToTempFilePath`自动生成图片并在 `@success` 事件里接收海报临时路径
- 2、通过调用内部方法生成图片
```html
<l-painter ref="painter">...code</l-painter>
```
```js
this.$refs.painter.canvasToTempFilePathSync({
fileType: "jpg",
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum需要设置 pathType为url
pathType: 'url',
quality: 1,
success: (res) => {
console.log(res.tempFilePath);
// 非H5 保存到相册
// H5 提示用户长按图另存
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: function () {
console.log('save success');
}
});
},
});
```
### 主动调用方式
- 通过获取组件实例内部的`render`函数 传递`JSON`即可
```html
<l-painter ref="painter" />
```
```js
// 渲染
this.$refs.painter.render(jsonSchema);
// 生成图片
this.$refs.painter.canvasToTempFilePathSync({
fileType: "jpg",
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum需要设置 pathType为url
pathType: 'url',
quality: 1,
success: (res) => {
console.log(res.tempFilePath);
// 非H5 保存到相册
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: function () {
console.log('save success');
}
});
},
});
```
### H5跨域
- 一般是需要后端或管理OSS资源的大佬处理
- 一般OSS的处理方式:
1、设置来源
```cmd
*
```
2、允许Methods
```html
GET
```
3、允许Headers
```html
access-control-allow-origin:*
```
4、最后如果还是不行,可试下给插件设置`useCORS`
```html
<l-painter useCORS>
```
### 海报示例
- 提供一份示例,只把插件当成生成图片的工具,非必要不要在弹窗里使用。
- 通过设置`isCanvasToTempFilePath`主动生成图片,再由 `@success` 事件接收海报临时路径
- 设置`custom-style="position: fixed; left: 200%"`样式把画板移到屏幕之外,达到隐藏画板的效果。
- **注意**:受平台影响海报画板最好不要隐藏,可能会无法生成图片。
#### 方式一 XML
```html
<image :src="path" mode="widthFix"></image>
<l-painter
isCanvasToTempFilePath
@success="path = $event"
custom-style="position: fixed; left: 200%"
css="width: 750rpx; padding-bottom: 40rpx; background: linear-gradient(,#ff971b 0%, #ff5000 100%)"
>
<l-painter-image
src="https://cdn.jsdelivr.net/gh/liangei/image@latest/avatar-1.jpeg"
css="margin-left: 40rpx; margin-top: 40rpx; width: 84rpx; height: 84rpx; border-radius: 50%;"
/>
<l-painter-view
css="margin-top: 40rpx; padding-left: 20rpx; display: inline-block"
>
<l-painter-text
text="隔壁老王"
css="display: block; padding-bottom: 10rpx; color: #fff; font-size: 32rpx; fontWeight: bold"
/>
<l-painter-text
text="为您挑选了一个好物"
css="color: rgba(255,255,255,.7); font-size: 24rpx"
/>
</l-painter-view>
<l-painter-view
css="margin-left: 40rpx; margin-top: 30rpx; padding: 32rpx; box-sizing: border-box; background: #fff; border-radius: 16rpx; width: 670rpx; box-shadow: 0 20rpx 58rpx rgba(0,0,0,.15)"
>
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="object-fit: cover; object-position: 50% 50%; width: 606rpx; height: 606rpx; border-radius: 12rpx;"
/>
<l-painter-view
css="margin-top: 32rpx; color: #FF0000; font-weight: bold; font-size: 28rpx; line-height: 1em;"
>
<l-painter-text text="¥" css="vertical-align: bottom" />
<l-painter-text
text="39"
css="vertical-align: bottom; font-size: 58rpx"
/>
<l-painter-text text=".39" css="vertical-align: bottom" />
<l-painter-text
text="¥59.99"
css="vertical-align: bottom; padding-left: 10rpx; font-weight: normal; text-decoration: line-through; color: #999999"
/>
</l-painter-view>
<l-painter-view css="margin-top: 32rpx; font-size: 26rpx; color: #8c5400">
<l-painter-text text="自营" css="color: #212121; background: #ffb400;" />
<l-painter-text
text="30天最低价"
css="margin-left: 16rpx; background: #fff4d9; text-decoration: line-through;"
/>
<l-painter-text
text="满减优惠"
css="margin-left: 16rpx; background: #fff4d9"
/>
<l-painter-text
text="超高好评"
css="margin-left: 16rpx; background: #fff4d9"
/>
</l-painter-view>
<l-painter-view css="margin-top: 30rpx">
<l-painter-text
css="line-clamp: 2; color: #333333; line-height: 1.8em; font-size: 36rpx; width: 478rpx; padding-right:32rpx; box-sizing: border-box"
text="360儿童电话手表9X 智能语音问答定位支付手表 4G全网通20米游泳级防水视频通话拍照手表男女孩星空蓝"
></l-painter-text>
<l-painter-qrcode
css="width: 128rpx; height: 128rpx;"
text="limeui.qcoon.cn"
></l-painter-qrcode>
</l-painter-view>
</l-painter-view>
</l-painter>
```
```js
data() {
return {
path: ''
}
}
```
#### 方式二 JSON
```html
<image :src="path" mode="widthFix"></image>
<l-painter
:board="poster"
isCanvasToTempFilePath
@success="path = $event"
custom-style="position: fixed; left: 200%"
/>
```
```js
data() {
return {
path: '',
poster: {
css: {
width: "750rpx",
paddingBottom: "40rpx",
background: "linear-gradient(,#000 0%, #ff5000 100%)"
},
views: [
{
src: "https://fastly.jsdelivr.net/gh/liangei/image@latest/avatar-1.jpeg",
type: "image",
css: {
background: "#fff",
objectFit: "cover",
marginLeft: "40rpx",
marginTop: "40rpx",
width: "84rpx",
border: "2rpx solid #fff",
boxSizing: "border-box",
height: "84rpx",
borderRadius: "50%"
}
},
{
type: "view",
css: {
marginTop: "40rpx",
paddingLeft: "20rpx",
display: "inline-block"
},
views: [
{
text: "隔壁老王",
type: "text",
css: {
display: "block",
paddingBottom: "10rpx",
color: "#fff",
fontSize: "32rpx",
fontWeight: "bold"
}
},
{
text: "为您挑选了一个好物",
type: "text",
css: {
color: "rgba(255,255,255,.7)",
fontSize: "24rpx"
},
}
],
},
{
css: {
marginLeft: "40rpx",
marginTop: "30rpx",
padding: "32rpx",
boxSizing: "border-box",
background: "#fff",
borderRadius: "16rpx",
width: "670rpx",
boxShadow: "0 20rpx 58rpx rgba(0,0,0,.15)"
},
views: [
{
src: "https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg",
type: "image",
css: {
objectFit: "cover",
objectPosition: "50% 50%",
width: "606rpx",
height: "606rpx"
},
}, {
css: {
marginTop: "32rpx",
color: "#FF0000",
fontWeight: "bold",
fontSize: "28rpx",
lineHeight: "1em"
},
views: [{
text: "¥",
type: "text",
css: {
verticalAlign: "bottom"
},
}, {
text: "39",
type: "text",
css: {
verticalAlign: "bottom",
fontSize: "58rpx"
},
}, {
text: ".39",
type: "text",
css: {
verticalAlign: "bottom"
},
}, {
text: "¥59.99",
type: "text",
css: {
verticalAlign: "bottom",
paddingLeft: "10rpx",
fontWeight: "normal",
textDecoration: "line-through",
color: "#999999"
}
}],
type: "view"
}, {
css: {
marginTop: "32rpx",
fontSize: "26rpx",
color: "#8c5400"
},
views: [{
text: "自营",
type: "text",
css: {
color: "#212121",
background: "#ffb400"
},
}, {
text: "30天最低价",
type: "text",
css: {
marginLeft: "16rpx",
background: "#fff4d9",
textDecoration: "line-through"
},
}, {
text: "满减优惠",
type: "text",
css: {
marginLeft: "16rpx",
background: "#fff4d9"
},
}, {
text: "超高好评",
type: "text",
css: {
marginLeft: "16rpx",
background: "#fff4d9"
},
}],
type: "view"
}, {
css: {
marginTop: "30rpx"
},
views: [
{
text: "360儿童电话手表9X 智能语音问答定位支付手表 4G全网通20米游泳级防水视频通话拍照手表男女孩星空蓝",
type: "text",
css: {
paddingRight: "32rpx",
boxSizing: "border-box",
lineClamp: 2,
color: "#333333",
lineHeight: "1.8em",
fontSize: "36rpx",
width: "478rpx"
},
}, {
text: "limeui.qcoon.cn",
type: "qrcode",
css: {
width: "128rpx",
height: "128rpx",
},
}],
type: "view"
}],
type: "view"
}
]
}
}
}
```
### Nvue
- 必须为HBX 3.4.11及以上
### 原生小程序
- 插件里的`painter.js`支持在原生小程序中使用
- new Painter 之后在`source`里传入 JSON
- 再调用`render`绘制海报
- 如需生成图片,请查看微信小程序 cavnas 的[canvasToTempFilePath](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/wx.canvasToTempFilePath.html)
```html
<canvas type="2d" id="painter" style="width: 100%"></canvas>
```
```js
import { Painter } from "./painter";
page({
data: {
poster: {
css: {
width: "750rpx",
},
views: [
{
type: "view",
css: {
background: "#d2d4c8",
paddingTop: "100rpx",
},
views: [
{
type: "view",
css: {
background: "#5f7470",
width: "33.33%",
height: "100rpx",
display: "inline-block",
},
},
{
type: "view",
css: {
background: "#889696",
width: "33.33%",
height: "100rpx",
display: "inline-block",
},
},
{
type: "view",
css: {
background: "#b8bdb5",
width: "33.33%",
height: "100rpx",
display: "inline-block",
},
},
],
},
],
},
},
async onLoad() {
const res = await this.getCentext();
const painter = new Painter(res);
// 返回计算布局后的整个内容尺寸
const { width, height } = await painter.source(this.data.poster);
// 得到计算后的尺寸后 可给canvas尺寸赋值达到动态响应效果
// 渲染
await painter.render();
},
// 获取canvas 2d
// 非2d也可以使用这里只是举个例子
getCentext() {
return new Promise((resolve) => {
wx.createSelectorQuery()
.select(`#painter`)
.node()
.exec((res) => {
let { node: canvas } = res[0];
resolve({
canvas,
context: canvas.getContext("2d"),
width: canvas.width,
height: canvas.height,
pixelRatio: 2,
});
});
});
},
});
```
### 旧版(1.6.x)更新
- 由于 1.8.x 版放弃了以定位的方式,所以 1.6.x 版更新之后要每个样式都加上`position: absolute`
- 旧版的 `image` mode 模式被放弃,使用`object-fit`
- 旧版的 `isRenderImage` 改成 `is-canvas-to-temp-filePath`
- 旧版的 `maxLines` 改成 `line-clamp`
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| -------------------------- | ------------------------------------------------------------ | ---------------- | ------------ |
| board | JSON 方式的海报元素对象集 | <em>object</em> | - |
| css | 海报最外层的样式,可以理解为`body` | <em>object</em> | 参数请向下看 |
| custom-style | canvas 自定义样式 | <em>string</em> | |
| is-canvas-to-temp-filePath | 是否生成图片,在`@success`事件接收图片地址 | <em>boolean</em> | `false` |
| after-delay | 生成图片错乱,可延时生成图片 | <em>number</em> | `100` |
| type | canvas 类型,对微信头条支付宝小程序可有效,可选值:`2d``''` | <em>string</em> | `2d` |
| file-type | 生成图片的后缀类型, 可选值:`png``jpg` | <em>string</em> | `png` |
| path-type | 生成图片路径类型,可选值`url``base64` | <em>string</em> | `-` |
| pixel-ratio | 生成图片的像素密度,默认为对应手机的像素密度,`nvue`无效 | <em>number</em> | `-` |
| width | **废弃** 画板的宽度,一般只用于通过内部方法时加上 | <em>number</em> | `` |
| height | **废弃** 画板的高度 ,同上 | <em>number</em> | `` |
### css
| 属性名 | 支持的值或类型 | 默认值 |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| (min\max)width | 支持`%`、`rpx`、`px` | - |
| height | 同上 | - |
| color | `string` | - |
| position | 定位,可选值:`absolute`、`fixed` | - |
| ↳ left、top、right、bottom | 配合`position`才生效,支持`%`、`rpx`、`px` | - |
| margin | 可简写或各方向分别写,如:`margin-top`,支持`auto`、`rpx`、`px` | - |
| padding | 可简写或各方向分别写,支持`rpx`、`px` | - |
| border | 可简写或各个值分开写:`border-width`、`border-style` 、`border-color`,简写请按顺序写 | - |
| line-clamp | `number`,超过行数显示省略号 | - |
| vertical-align | 文字垂直对齐,可选值:`bottom`、`top`、`middle` | `middle` |
| line-height | 文字行高,支持`rpx`、`px`、`em` | `1.4em` |
| font-weight | 文字粗细,可选值:`normal`、`bold` | `normal` |
| font-size | 文字大小,`string`,支持`rpx`、`px` | `14px` |
| text-decoration | 文本修饰,可选值:`underline` 、`line-through`、`overline` | - |
| text-align | 文本水平对齐,可选值:`right` 、`center` | `left` |
| display | 框类型,可选值:`block`、`inline-block`、`flex`、`none`,当为`none`时是不渲染该段, `flex`功能简陋。 | - |
| flex | 配合 display: flex; 属性定义了在分配多余空间,目前只用为数值如: flex: 1 | - |
| align-self | 配合 display: flex; 单个项目垂直轴对齐方式: `flex-start` `flex-end` `center` | `flex-start` |
| justify-content | 配合 display: flex; 水平轴对齐方式: `flex-start` `flex-end` `center` | `flex-start` |
| align-items | 配合 display: flex; 垂直轴对齐方式: `flex-start` `flex-end` `center` | `flex-start` |
| border-radius | 圆角边框,支持`%`、`rpx`、`px` | - |
| box-sizing | 可选值:`border-box` | - |
| box-shadow | 投影 | - |
| background(color) | 支持渐变,但必须写百分比!如:`linear-gradient(,#ff971b 0%, #ff5000 100%)`、`radial-gradient(#0ff 15%, #f0f 60%)`,目前 radial-gradient 渐变的圆心为元素中点,半径为最长边,不支持设置 | - |
| background-clip | 文字渐变,配合`background`背景渐变,设置`background-clip: text` 达到文字渐变效果 | - |
| background-image | view 元素背景:`url(src)`,若只是设置背景图,请不要设置`background-repeat` | - |
| background-repeat | 设置是否及如何重复背景纹理,可选值:`repeat`、`repeat-x`、`repeat-y`、`no-repeat` | `repeat` |
| [object-fit](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit/) | 图片元素适应容器方式,类似于`mode`,可选值:`cover`、 `contain`、 `fill`、 `none` | - |
| [object-position](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-position) | 图片的对齐方式,配合`object-fit`使用 | - |
### 图片填充模式 object-fit
| 名称 | 含义 |
| ------- | ------------------------------------------------------ |
| contain | 保持宽高缩放图片,使图片的长边能完全显示出来 |
| cover | 保持宽高缩放图片,使图片的短边能完全显示出来,裁剪长边 |
| fill | 拉伸图片,使图片填满元素 |
| none | 保持图片原有尺寸 |
### 事件 Events
| 事件名 | 说明 | 返回值 |
| -------- | ---------------------------------------------------------------- | ------ |
| success | 生成图片成功,若使用`is-canvas-to-temp-filePath` 可以接收图片地址 | path |
| fail | 生成图片失败 | error |
| done | 绘制成功 | |
| progress | 绘制进度 | number |
### 内部函数 Ref
| 事件名 | 说明 | 返回值 |
| -------- | ---------------------------------------------------------------- | ------ |
| render(object) | 渲染器传入JSON 绘制海报 | promise |
| [canvasToTempFilePath](https://uniapp.dcloud.io/api/canvas/canvasToTempFilePath.html#canvastotempfilepath)(object) | 把当前画布指定区域的内容导出生成指定大小的图片,并返回文件临时路径。 | |
| canvasToTempFilePathSync(object) | 同步接口,同上 | |
## 常见问题
- 1、H5 端使用网络图片需要解决跨域问题。
- 2、小程序使用网络图片需要去公众平台增加下载白名单二级域名也需要配
- 3、H5 端生成图片是 base64有时显示只有一半可以使用原生标签`<IMG/>`
- 4、发生保存图片倾斜变形或提示 native buffer exceed size limit 时,使用 pixel-ratio="2"参数,降分辨率。
- 5、h5 保存图片不需要调接口,提示用户长按图片保存。
- 6、画板不能隐藏包括`v-if``v-show`、`display:none`、`opacity:0`,另外也不要把画板放在弹窗里。如果需要隐藏画板请设置 `custom-style="position: fixed; left: 200%"`
- 7、微信小程序 canvas 2d **不支持真机调试**,请使用真机预览方式。
- 8、微信小程序打开调试时可以生但并闭无法生成时这种情况一般是没有在公众号配置download域名
- 9、HBX 3.4.5之前的版本不支持vue3
- 10、在微信开发工具上 canvas 层级最高无法zindex并不影响真机
- 11、请不要导入非uni_modules插件
- 华为手机 APP 上无法生成图片,请使用 HBX2.9.11++(已过时,忽略这条)
- IOS APP 请勿使用 HBX2.9.3.20201014 的版本!这个版本无法生成图片。(已过时,忽略这条)
- 苹果微信 7.0.20 存在闪退和图片无法 onload 为微信 bug已过时忽略这条
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![输入图片说明](https://static-6d65bd90-8508-4d6c-abbc-a4ef5c8e49e7.bspapp.com/image/222521_bb543f96_518581.jpeg "微信图片编辑_20201122220352.jpg")
![输入图片说明](https://static-6d65bd90-8508-4d6c-abbc-a4ef5c8e49e7.bspapp.com/image/wxplay.jpg "wxplay.jpg")

View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title></title>
<style type="text/css">
html,
body,
canvas {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
overflow-y: hidden;
background-color: transparent;
}
</style>
</head>
<body>
<canvas id="lime-painter"></canvas>
<script type="text/javascript" src="./uni.webview.1.5.3.js"></script>
<script type="text/javascript" src="./painter.js"></script>
<script>
var cache = [];
var painter = null;
var canvas = null;
var context = null;
var timer = null;
var pixelRatio = 1;
console.log = function (...args) {
postMessage(args);
};
// function stringify(key, value) {
// if (typeof value === 'object' && value !== null) {
// if (cache.indexOf(value) !== -1) {
// return;
// }
// cache.push(value);
// }
// return value;
// };
function emit(event, data) {
postMessage({
event,
data: (typeof data !== 'object' && data !== null ? data : JSON.stringify(data))
});
cache = [];
};
function postMessage(data) {
uni.postMessage({
data
});
};
function init(dpr) {
canvas = document.querySelector('#lime-painter');
context = canvas.getContext('2d');
pixelRatio = dpr || window.devicePixelRatio;
painter = new Painter({
id: 'lime-painter',
context,
canvas,
pixelRatio,
width: canvas.offsetWidth,
height: canvas.offsetHeight,
listen: {
onProgress(v) {
emit('progressChange', v);
},
onEffectFail(err) {
//console.error(err)
emit('fail', err);
}
}
});
emit('inited', true);
};
function save(args) {
delete args.success;
delete args.fail;
clearTimeout(timer);
timer = setTimeout(() => {
const path = painter.save(args);
if (typeof path == 'string') {
const index = Math.ceil(path.length / 8);
for (var i = 0; i < 8; i++) {
if (i == 7) {
emit('success', path.substr(i * index, index));
} else {
emit('file', path.substr(i * index, index));
}
};
} else {
// console.log('canvas no data')
emit('fail', 'canvas no data');
};
}, 30);
};
async function source(args) {
let size = await painter.source(args);
emit('layoutChange', size);
if(!canvas.height) {
console.log('canvas no size')
emit('fail', 'canvas no size');
}
painter.render().catch(err => {
// console.error(err)
emit('fail', err);
});
};
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,111 @@
## 3.3.282022-07-27
- 修复 app端微信登录返回的accessToken过期时间expired不正确的Bug
## 3.3.272022-07-27
- 短信发送失败、微信登录失败等场景下输出原始错误方便排查错误
## 3.3.262022-07-08
- 兼容配置放在uni-id下的逻辑但是仍推荐使用uni-config-center
## 3.3.252022-06-30
- 修复config文件不合法时未抛出具体错误的Bug
## 3.3.242022-06-28
- 修复3.3.12引出的使用多应用配置时报错的Bug
## 3.3.232022-06-13
- 修复上版本引出的部分依赖未找到的Bug
## 3.3.222022-06-13
- 新增 preferedWebPlatform 配置用于解决HBuilderX 3.4.9版本起web端platform不一致的问题 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=prefered-web-platform)
## 3.3.212022-05-24
- 修复createInstance传入clientInfo无效的Bug
## 3.3.202022-05-19
- 调整以下错误码(账号已注册[uni-id-account-exists]、账号不存在[uni-id-account-not-exists]、账号已绑定[uni-id-account-bound]
## 3.3.192022-05-19
- 修复 addUser 部分情况下会创建出重复账号的Bug
## 3.3.182022-05-12
- 调整绑定、解绑邮箱手机号接口只要传递code参数就进行验证码校验即使传递的值为undefined
## 3.3.172022-05-09
- register_env内增加os_name字段用于区分注册时的客户端系统类型
## 3.3.162022-05-09
- 修复 addUser接口添加的用户无法使用密码登录的Bug [详情](https://ask.dcloud.net.cn/question/144670)
## 3.3.152022-05-08
- 修复config文件语法错误时报`this.t is not a function`的Bug 感谢@寒暄
## 3.3.142022-05-08
- 新增 getWeixinUserInfo接口 用于获取app平台微信登录用户的用户信息 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#get-weixin-user-info)
- 新增 addUser接口 用于手动添加用户 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#add-user)
- 新增 resetPwdBySms接口 用于使用短信验证码重置密码 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#reset-pwd-by-sms)
- 新增 refreshToken接口 用于主动刷新用户token [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#refresh-token)
- 调整 用户注册时记录用户注册环境到 register_env 字段 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#user-table)
- 调整 用户注册时将注册 ip 移至 register_env 内
## 3.3.132022-03-04
- createInstance方法支持传递clientInfo [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id.html#create-instance)
- 修复`this.t is not a function`报错
## 3.3.122022-01-15
- 新增 preferedAppPlatform 配置用于解决uni-app vue2版本vue3版本获取platform不一致的问题 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=prefered-app-platform)
- 修复 checkToken 未返回自定义token内容的Bug
## 3.3.112022-01-11
- 修复用户名密码登录时多个应用出现重复用户名登录报错的Bug
## 3.3.102022-01-07
- 新增 自定义国际化语言支持 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=custom-i8n)
- 修复 一键登录时未校验重复手机号是否已验证的Bug
- 修复 Apple登录时用户邮箱为空时报错的Bug
- 修复 登录接口未传username时错误提示不正确的Bug
## 3.3.92021-11-09
- 去除重复的context.xxx未找到的提示语
## 3.3.82021-10-28
- 新增 用户账户封禁接口 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=ban-account)
- 新增 用户账户注销接口 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=close-account)
- 修复 未传appid时用户重复注册的Bug
## 3.3.72021-10-08
- 移除部分接口的废弃提示
## 3.3.62021-09-08
- 修复 邀请码可能重复的Bug
## 3.3.52021-08-10
- 修复版本号错误
## 3.3.42021-08-10
- 微信、QQ、支付宝登录新增type参数用于指定当前是登录还是注册
## 3.3.32021-08-04
- 修复使用数组形式的配置文件报错的Bug
## 3.3.22021-08-03
- 修复上3.3.0版本引出的createInstance接口传入配置不生效的Bug 感谢[hmh](https://gitee.com/hmh)
## 3.3.12021-07-30
- 修复 将设置用户允许登录的应用列表时传入空数组报错的Bug
## 3.3.02021-07-30
- 新增 不同端应用配置隔离 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=isolate-config)
- 新增 不同端用户隔离 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=isolate-user)
+ 此版本升级需要开发者处理一下用户数据,请参考 [补齐用户dcloud_appid字段](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=makeup-dcloud-appid)
- 新增 QQ登录、注册相关功能 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=qq)
- 调整 不再支持绑定手机、邮箱时不填验证码直接绑定
## 3.2.12021-07-09
- 撤销3.2.0版本所做的调整
## 3.2.02021-07-09
- 【重要】支持不同端(管理端、用户端等)用户隔离 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=isolate-user)
- 支持不同端(管理端、用户端等)配置文件隔离 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=isolate-config)
## 3.1.32021-07-08
- 移除插件内误传的node_modules
## 3.1.22021-07-08
- 修复 微信小程序绑定微信账号时报错的Bug
## 3.1.12021-07-01
- 使用新的错误码规范,兼容旧版 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=errcode)
- 修复微信登录、绑定时未返回用户accessToken的Bug
## 3.1.02021-04-19
- 增加对用户名、邮箱、密码字段的两端去空格
- 默认忽略用户名、邮箱的大小写 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=case-sensitive)
- 修复 customToken导出async方法报错的Bug
## 3.0.122021-04-13
- 调整bindTokenToDevice默认值为false
## 3.0.112021-04-12
- 修复3.0.7版本引出的多个用户访问时可能出现30201报错的Bug
## 3.0.102021-04-08
- 优化错误提示
## 3.0.92021-04-08
- bindMobile接口支持通过一键登录的方式绑定
- 优化错误提示
## 3.0.82021-03-19
- 修复 3.0.7版本某些情况下生成token报错的Bug
## 3.0.72021-03-19
- 新增 支持uni-config-center更新uni-id无须再担心配置被覆盖 [详情](https://uniapp.dcloud.io/uniCloud/uni-id?id=uni-config-center)
- 新增 自定义token内容可以缓存角色权限之外的更多信息到客户端 [详情](https://uniapp.dcloud.io/uniCloud/uni-id?id=custom-token)
- 新增 支持传入context获取uni-id实例防止单实例多并发时全局context混乱 [详情](https://uniapp.dcloud.io/uniCloud/uni-id?id=create-instance)
## 3.0.62021-03-05
- 新增[uniID.wxBizDataCrypt](https://uniapp.dcloud.io/uniCloud/uni-id?id=%e5%be%ae%e4%bf%a1%e6%95%b0%e6%8d%ae%e8%a7%a3%e5%af%86)方法
- 优化loginByApple方法提高接口响应速度
## 3.0.52021-02-03
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,85 @@
{
"id": "uni-id",
"displayName": "uni-id",
"version": "3.3.28",
"description": "简单、统一、可扩展的用户中心",
"keywords": [
"uniid",
"uni-id",
"用户管理",
"用户中心",
"短信验证码"
],
"repository": "https://gitee.com/dcloud/uni-id.git",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"uni_modules": {
"dependencies": ["uni-config-center"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
**文档已移至[uni-id文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id)**
> 一般uni-id升级大版本时为不兼容更新从低版本迁移到高版本请参考[uni-id迁移指南](https://uniapp.dcloud.net.cn/uniCloud/uni-id?id=migration)
## 重要升级说明
**uni-id 3.x版本搭配的uniCloud admin版本需大于1.2.10。**
### 缓存角色权限
`uni-id 3.0.0`支持在token内缓存用户的角色权限默认开启此功能各登录接口的needPermission参数不再生效。如需关闭请在config内配置`"removePermissionAndRoleFromToken": true`
为什么要缓存角色权限?要知道云数据库是按照读写次数来收取费用的,并且读写数据库会拖慢接口响应速度。未配置`"removePermissionAndRoleFromToken": true`的情况下可以在调用checkToken接口时不查询数据库获取用户角色权限。
详细checkToken流程如下
![](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/ed45d350-5a4d-11eb-b997-9918a5dda011.jpg)
可以看出旧版tokenremovePermissionAndRoleFromToken为true时生成的在checkToken时如需返回权限需要进行两次数据库查询。新版token不需要查库即可返回权限信息。
**注意**
- 由于角色权限缓存在token内可能会存在权限已经更新但是用户token未过期之前依然是旧版角色权限的情况。可以调短一些token过期时间来减少这种情况的影响。
- admin角色token内不包含permission如需自行判断用户是否有某个权限要注意admin角色需要额外判断一下写法如下
```js
const {
role,
permission
} = await uniID.checkToken(event.uniIdToken)
if(role.includes('admin') || permission.includes('your permission id')) {
// 当前角色拥有'your permission id'对应的权限
}
```

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
{
"name": "uni-id",
"version": "3.3.28",
"description": "uni-id for uniCloud",
"main": "index.js",
"homepage": "https://uniapp.dcloud.io/uniCloud/uni-id",
"repository": {
"type": "git",
"url": "git+https://gitee.com/dcloud/uni-id.git"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}

View File

@@ -0,0 +1,64 @@
## 0.6.12022-08-17
- 修复 后台添加应用市场但都没有启用的情况下报错的Bug (需要 uni-admin 1.9.3+
## 0.6.02022-07-19
- 新增 支持多应用商店配置(需要 uni-admin 1.9.3+
## 0.4.12022-05-27
- 修复 上版引出的报错问题
## 0.4.02022-05-27
- 新增 Android 支持跳转手机自带商店,填写升级包地址时请填写跳转商店链接
- 新增 改为云对象调用方式,使用更直观
## 0.3.32022-04-14
- 修复 调用 check-update当 code 为 0 时没有回调
## 0.3.22022-01-12
- 优化显示逻辑
## 0.3.12021-11-24
- 修复 vue3 上图片不显示的Bug
## 0.3.02021-11-18
- 移除 wgt 安装成功后提示,防止重启过快弹框不消失
## 0.2.22021-08-25
- 兼容vue3.0
## 0.2.12021-07-26
- 修复 使用腾讯云并手动填写地址时导致下载链接失效的bug
## 0.2.02021-07-13
- 更新文档 关于报错local_storage_key 为空请不要将页面路径设置为pages.json中第一项
## 0.1.92021-06-28
- 更新文档
- 修复 wgt安装失败时按钮状态不对
## 0.1.82021-06-16
- 修复 跳转安装时导致上次下载的apk还没安装就被删掉的bug
## 0.1.72021-06-03
- 修改 移除static中的图片
## 0.1.62021-06-03
- 修改 下载更新按钮使用CSS渐变色
## 0.1.52021-04-22
- 更新check-update函数。现在返回一个Promise有更新时成功回调其他情况错误回调
## 0.1.42021-04-13
- 更新文档。明确云函数调用结果
## 0.1.32021-04-13
- 解耦云函数与弹框处理。utils中新增 call-check-version.js可用于单独检测是否有更新
## 0.1.22021-04-07
- 更新版本对比函数 compare
## 0.1.12021-04-07
- 修复 腾讯云空间下载链接不能下载问题
## 0.1.02021-04-07
- 新增使用uni.showModal提示升级示例
- 修改iOS升级提示方式
## 0.0.72021-04-02
- 修复在iOS上打开弹框报错
## 0.0.62021-04-01
- 兼容旧版本安卓
## 0.0.52021-04-01
- 修复低版本安卓上进度条错位
## 0.0.42021-04-01
- 更新readme
- 修复check-update语法错误
## 0.0.32021-04-01
- 新增前台更新弹框详见readme
- 更新前台检查更新方法
## 0.0.22021-03-29
- 更新文档
- 移除 dependencies
## 0.0.12021-03-25
- 升级中心前台检查更新

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,81 @@
{
"id": "uni-upgrade-center-app",
"displayName": "升级中心 uni-upgrade-center - App",
"version": "0.6.1",
"description": "uni升级中心 - 客户端检查更新",
"keywords": [
"uniCloud",
"update",
"升级",
"wgt"
],
"repository": "https://gitee.com/dcloud/uni-upgrade-center/tree/master/uni_modules/uni-upgrade-center-app",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-page"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,539 @@
<template>
<view class="mask flex-center">
<view class="content botton-radius">
<view class="content-top">
<text class="content-top-text">{{title}}</text>
<image class="content-top" style="top: 0;" width="100%" height="100%" src="../images/bg_top.png">
</image>
</view>
<view class="content-header"></view>
<view class="content-body">
<view class="title">
<text>{{subTitle}}</text>
<!-- <text style="padding-left:20rpx;font-size: 0.5em;color: #666;">v.{{version}}</text> -->
</view>
<view class="body">
<scroll-view class="box-des-scroll" scroll-y="true">
<text class="box-des">
{{contents}}
</text>
</scroll-view>
</view>
<view class="footer flex-center">
<template v-if="isAppStore">
<button class="content-button" style="border: none;color: #fff;" plain @click="jumpToAppStore">
{{downLoadBtnTextiOS}}
</button>
</template>
<template v-else>
<template v-if="!downloadSuccess">
<view class="progress-box flex-column" v-if="downloading">
<progress class="progress" border-radius="35" :percent="downLoadPercent"
activeColor="#3DA7FF" show-info stroke-width="10" />
<view style="width:100%;font-size: 28rpx;display: flex;justify-content: space-around;">
<text>{{downLoadingText}}</text>
<text>({{downloadedSize}}/{{packageFileSize}}M)</text>
</view>
</view>
<button v-else class="content-button" style="border: none;color: #fff;" plain
@click="updateApp">
{{downLoadBtnText}}
</button>
</template>
<button v-else-if="downloadSuccess && !installed" class="content-button"
style="border: none;color: #fff;" plain :loading="installing" :disabled="installing"
@click="installPackage">
{{installing ? '正在安装' : '下载完成立即安装'}}
</button>
<button v-if="installed && isWGT" class="content-button" style="border: none;color: #fff;" plain
@click="restart">
安装完毕点击重启
</button>
</template>
</view>
</view>
<image v-if="!is_mandatory" class="close-img" src="../images/app_update_close.png"
@click.stop="closeUpdate"></image>
</view>
</view>
</template>
<script>
const localFilePathKey = 'UNI_ADMIN_UPGRADE_CENTER_LOCAL_FILE_PATH'
const platform_iOS = 'iOS';
let downloadTask = null;
let openSchemePromise
/**
* 对比版本号,如需要,请自行修改判断规则
* 支持比对 ("3.0.0.0.0.1.0.1", "3.0.0.0.0.1") ("3.0.0.1", "3.0") ("3.1.1", "3.1.1.1") 之类的
* @param {Object} v1
* @param {Object} v2
* v1 > v2 return 1
* v1 < v2 return -1
* v1 == v2 return 0
*/
function compare(v1 = '0', v2 = '0') {
v1 = String(v1).split('.')
v2 = String(v2).split('.')
const minVersionLens = Math.min(v1.length, v2.length);
let result = 0;
for (let i = 0; i < minVersionLens; i++) {
const curV1 = Number(v1[i])
const curV2 = Number(v2[i])
if (curV1 > curV2) {
result = 1
break;
} else if (curV1 < curV2) {
result = -1
break;
}
}
if (result === 0 && (v1.length !== v2.length)) {
const v1BiggerThenv2 = v1.length > v2.length;
const maxLensVersion = v1BiggerThenv2 ? v1 : v2;
for (let i = minVersionLens; i < maxLensVersion.length; i++) {
const curVersion = Number(maxLensVersion[i])
if (curVersion > 0) {
v1BiggerThenv2 ? result = 1 : result = -1
break;
}
}
}
return result;
}
export default {
data() {
return {
// 从之前下载安装
installForBeforeFilePath: '',
// 安装
installed: false,
installing: false,
// 下载
downloadSuccess: false,
downloading: false,
downLoadPercent: 0,
downloadedSize: 0,
packageFileSize: 0,
tempFilePath: '', // 要安装的本地包地址
// 默认安装包信息
title: '更新日志',
contents: '',
is_mandatory: false,
// 可自定义属性
subTitle: '发现新版本',
downLoadBtnTextiOS: '立即跳转更新',
downLoadBtnText: '立即下载更新',
downLoadingText: '安装包下载中,请稍后'
}
},
onLoad({
local_storage_key
}) {
if (!local_storage_key) {
console.error('local_storage_key为空请检查后重试')
uni.navigateBack()
return;
};
const localPackageInfo = uni.getStorageSync(local_storage_key);
if (!localPackageInfo) {
console.error('安装包信息为空,请检查后重试')
uni.navigateBack()
return;
};
const requiredKey = ['version', 'url', 'type']
for (let key in localPackageInfo) {
if (requiredKey.indexOf(key) !== -1 && !localPackageInfo[key]) {
console.error(`参数 ${key} 必填,请检查后重试`)
uni.navigateBack()
return;
}
}
Object.assign(this, localPackageInfo)
this.checkLocalStoragePackage()
},
onBackPress() {
// 强制更新不允许返回
if (this.is_mandatory) {
return true
}
downloadTask && downloadTask.abort()
},
onHide() {
openSchemePromise = null
},
computed: {
isWGT() {
return this.type === 'wgt'
},
isiOS() {
return !this.isWGT ? this.platform.includes(platform_iOS) : false;
},
isAppStore() {
return this.isiOS || (!this.isiOS && !this.isWGT && this.url.indexOf('.apk') === -1)
}
},
methods: {
checkLocalStoragePackage() {
// 如果已经有下载好的包,则直接提示安装
const localFilePathRecord = uni.getStorageSync(localFilePathKey)
if (localFilePathRecord) {
const {
version,
savedFilePath,
installed
} = localFilePathRecord
// 比对版本
if (!installed && compare(version, this.version) === 0) {
this.downloadSuccess = true;
this.installForBeforeFilePath = savedFilePath;
this.tempFilePath = savedFilePath
} else {
// 如果保存的包版本小 或 已安装过,则直接删除
this.deleteSavedFile(savedFilePath)
}
}
},
async closeUpdate() {
if (this.downloading) {
if (this.is_mandatory) {
return uni.showToast({
title: '下载中,请稍后……',
icon: 'none',
duration: 500
})
}
uni.showModal({
title: '是否取消下载?',
cancelText: '否',
confirmText: '是',
success: res => {
if (res.confirm) {
downloadTask && downloadTask.abort()
uni.navigateBack()
}
}
});
return;
}
if (this.downloadSuccess && this.tempFilePath) {
// 包已经下载完毕,稍后安装,将包保存在本地
await this.saveFile(this.tempFilePath, this.version)
uni.navigateBack()
return;
}
uni.navigateBack()
},
updateApp() {
this.checkStoreScheme().catch(() => {
this.downloadPackage()
})
},
// 跳转应用商店
checkStoreScheme() {
const storeList = (this.store_list || []).filter(item => item.enable)
if (storeList && storeList.length) {
storeList
.sort((cur, next) => next.priority - cur.priority)
.map(item => item.scheme)
.reduce((promise, cur, curIndex) => {
openSchemePromise = (promise || (promise = Promise.reject())).catch(() => {
return new Promise((resolve, reject) => {
plus.runtime.openURL(cur, (err) => {
reject(err)
})
})
})
return openSchemePromise
}, openSchemePromise)
return openSchemePromise
}
return Promise.reject()
},
downloadPackage() {
this.downloading = true;
//下载包
downloadTask = uni.downloadFile({
url: this.url,
success: res => {
if (res.statusCode == 200) {
this.downloadSuccess = true;
this.tempFilePath = res.tempFilePath
// 强制更新,直接安装
if (this.is_mandatory) {
this.installPackage();
}
}
},
complete: () => {
this.downloading = false;
this.downLoadPercent = 0
this.downloadedSize = 0
this.packageFileSize = 0
downloadTask = null;
}
});
downloadTask.onProgressUpdate(res => {
this.downLoadPercent = res.progress;
this.downloadedSize = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2);
this.packageFileSize = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2);
});
},
installPackage() {
// #ifdef APP-PLUS
// wgt资源包安装
if (this.isWGT) {
this.installing = true;
}
plus.runtime.install(this.tempFilePath, {
force: false
}, async res => {
this.installing = false;
this.installed = true;
// wgt包安装后会提示 安装成功,是否重启
if (this.isWGT) {
// 强制更新安装完成重启
if (this.is_mandatory) {
uni.showLoading({
icon: 'none',
title: '安装成功,正在重启……'
})
setTimeout(() => {
uni.hideLoading()
this.restart();
}, 1000)
}
} else {
const localFilePathRecord = uni.getStorageSync(localFilePathKey)
uni.setStorageSync(localFilePathKey, {
...localFilePathRecord,
installed: true
})
}
}, async err => {
// 如果是安装之前的包,安装失败后删除之前的包
if (this.installForBeforeFilePath) {
await this.deleteSavedFile(this.installForBeforeFilePath)
this.installForBeforeFilePath = '';
}
// 安装失败需要重新下载安装包
this.installing = false;
this.installed = false;
uni.showModal({
title: '更新失败,请重新下载',
content: err.message,
showCancel: false
});
});
// 非wgt包安装跳出覆盖安装此处直接返回上一页
if (!this.isWGT && !this.is_mandatory) {
uni.navigateBack()
}
// #endif
},
restart() {
this.installed = false;
// #ifdef APP-PLUS
//更新完重启app
plus.runtime.restart();
// #endif
},
saveFile(tempFilePath, version) {
return new Promise((resolve, reject) => {
uni.saveFile({
tempFilePath,
success({
savedFilePath
}) {
uni.setStorageSync(localFilePathKey, {
version,
savedFilePath
})
},
complete() {
resolve()
}
})
})
},
deleteSavedFile(filePath) {
uni.removeStorageSync(localFilePathKey)
return uni.removeSavedFile({
filePath
})
},
jumpToAppStore() {
plus.runtime.openURL(this.url);
}
}
}
</script>
<style>
page {
background: transparent;
}
.flex-center {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
}
.mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, .65);
}
.botton-radius {
border-bottom-left-radius: 30rpx;
border-bottom-right-radius: 30rpx;
}
.content {
position: relative;
top: 0;
width: 600rpx;
background-color: #fff;
box-sizing: border-box;
padding: 0 50rpx;
font-family: Source Han Sans CN;
}
.text {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
line-height: 200px;
text-align: center;
color: #FFFFFF;
}
.content-top {
position: absolute;
top: -195rpx;
left: 0;
width: 600rpx;
height: 270rpx;
}
.content-top-text {
font-size: 45rpx;
font-weight: bold;
color: #F8F8FA;
position: absolute;
top: 120rpx;
left: 50rpx;
z-index: 1;
}
.content-header {
height: 70rpx;
}
.title {
font-size: 33rpx;
font-weight: bold;
color: #3DA7FF;
line-height: 38px;
}
.footer {
height: 150rpx;
display: flex;
align-items: center;
justify-content: space-around;
}
.box-des-scroll {
box-sizing: border-box;
padding: 0 40rpx;
height: 200rpx;
text-align: left;
}
.box-des {
font-size: 26rpx;
color: #000000;
line-height: 50rpx;
}
.progress-box {
width: 100%;
}
.progress {
width: 90%;
height: 40rpx;
border-radius: 35px;
}
.close-img {
width: 70rpx;
height: 70rpx;
z-index: 1000;
position: absolute;
bottom: -120rpx;
left: calc(50% - 70rpx / 2);
}
.content-button {
text-align: center;
flex: 1;
font-size: 30rpx;
font-weight: 400;
color: #FFFFFF;
border-radius: 40rpx;
margin: 0 18rpx;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(to right, #1785ff, #3DA7FF);
}
.flex-column {
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View File

@@ -0,0 +1,18 @@
{
"pages": [{
"path": "uni_modules/uni-upgrade-center-app/pages/upgrade-popup",
"style": {
"disableScroll": true,
"app-plus": {
"backgroundColorTop": "transparent",
"background": "transparent",
"titleNView": false,
"scrollIndicator": false,
"popGesture": "none",
"animationType": "fade-in",
"animationDuration": 200
}
}
}]
}

View File

@@ -0,0 +1,126 @@
## 升级中心 - app插件与 `uni-admin` 版本关系
### `uni-admin >= 1.9.3`:云函数 `checkVersion` 废弃,使用 uni-admin 自带的 `uni-upgrade-center` 云函数。
# uni-upgrade-center - App
### 概述
> 统一管理App及App在`Android`、`iOS`平台上`App安装包`和`wgt资源包`的发布升级
> uni升级中心分为业务插件和后台管理插件。本插件为业务插件包括uni升级中心客户端检查更新的前后端逻辑。后台管理系统另见 [uni-upgrade-center - Admin](https://ext.dcloud.net.cn/plugin?id=4470)
### uni升级中心 - 客户端检查更新插件
- 一键式检查更新同时支持整包升级与wgt资源包更新
- 好看、实用、可自定义的客户端提示框
## 安装指引
1. 依赖数据库`opendb-app-versions`,如果没有此库,请在云服务空间中创建。
2. 使用`HBuilderX 3.1.0+`,因为要使用到`uni_modules`
3. 在插件市场打开本插件页面,在右侧点击`使用 HBuilderX 导入插件`,选择要导入的项目点击确定
4. 绑定一个服务空间。自 `0.6.0` 起,依赖 `uni-admin 1.9.3+``uni-upgrade-center 云函数`,请和 uni-admin 项目关联同一个服务空间
5. 找到`/uni_modules/uni-upgrade-center-app/uniCloud/cloudfunctions/check-version`,右键上传部署。自 `0.6.0` 起,依赖 `uni-admin 1.9.3+``uni-upgrade-center 云函数`,插件不再单独提供云函数,这样可以省下一个云函数名额。
6.`pages.json`中添加页面路径。**注请不要设置为pages.json中第一项**
```json
"pages": [
// ……其他页面配置
{
"path": "uni_modules/uni-upgrade-center-app/pages/upgrade-popup",
"style": {
"disableScroll": true,
"app-plus": {
"backgroundColorTop": "transparent",
"background": "transparent",
"titleNView": false,
"scrollIndicator": false,
"popGesture": "none",
"animationType": "fade-in",
"animationDuration": 200
}
}
}
]
```
7.`@/uni_modules/uni-upgrade-center-app/utils/check-update`import到需要用到的地方调用一下即可
1. 默认使用当前绑定的服务空间,如果要请求其他服务空间,可以使用其他服务空间的 `callFunction`。[详情](https://uniapp.dcloud.io/uniCloud/cf-functions.html#call-by-function-cross-space)
8. 升级弹框可自行编写,也可以使用`uni.showModal`或使用现有的升级弹框样式如果不满足UI需求请自行替换资源文件。在`utils/check-update.js`中都有实例。
9. wgt更新时打包前请务必将manifest.json中的版本修改为更高版本。
### 更新下载安装`check-update.js`
*该函数在utils目录下*
1. 如果是静默更新,则不会打开更新弹框,会在后台下载后安装,下次启动应用生效
2. 如果是 iOS则会直接打开AppStore的链接
3. 其他情况,会将`check-version`返回的结果保存在localStorage中并跳转进入`upgrade-popup.vue`打开更新弹框
### 检查更新函数`check-version`
*该函数在uniCloud/cloudfunctions目录下*
1. 使用检查更新需要传递三个参数 `appid``appVersion``wgtVersion`
2. `appid` 使用 plus.runtime.appid 获取,*注真机运行时为固定值HBuilder在调试的时候请使用本地调试云函数*
3. `appVersion` 使用 plus.runtime.version 获取
4. `wgtVersion` 使用 plus.runtime.getProperty(plus.runtime.appid,(wgtInfo) => { wgtInfo.version }) 获取
5. `check-version`云函数内部会自动获取 App 平台
**Tips**
1. `check-version`云函数内部有版本对比函数compare
- 使用多段式版本格式(如:"3.0.0.0.0.1.0.1", "3.0.0.0.0.1")。如果不满足对比规则,请自行修改。
- 如果修改,请将*pages/upgrade-popup.vue*中*compare*函数一并修改
## 项目代码说明
### 更新弹框
- `upgrade-popup.vue` - 更新应用:
- 如果云函数`check-version`返回的参数表明需要更新则将参数保存在localStorage中带着键值跳转该页面
- 进入时会先从localStorage中尝试取出之前存的安装包路径此包不会是强制安装类型的包
- 如果有已经保存的包,则和传进来的 `version` 进行比较如果相等则安装。大于和小于都不进行安装因为admin端可能会调整包的版本。不符合更新会将此包删除
- 如果本地没有包或者包不符合安装条件,则进行下载安装包
- 点击下载会有进度条、已下载大小和下载包的大小
- 下载完成会提示安装:
- 如果是 wgt 包,安装时则会提示 正在安装…… 和 安装完成。安装完成会提示是否重启
- 如果是 原生安装包,则直接跳出去覆盖安装
- 下载过程中,如果退出会提示是否取消下载。如果是强制更新,则只会提示正在下载请稍后,此时不可退出
- 如果是下载完成了没有安装就退出则会将下载完成的包保存在本地。将包的本地路径和包version保存在localStorage中
### 工具类 utils
- `call-check-version`
- 请求云函数`check-version`拿取版本检测结果
- `check-update`
- 调用`call-check-version`并根据结果判断是否显示更新弹框
### 云函数
- `check-version` - 检查应用更新:
- 根据传参先检测传参是否完整appid appVersion wgtVersion 必传
- 先从数据库取出所有该平台(会从上下文读取平台信息)的所有线上发行更新
- 再从所有线上发行更新中取出版本最大的一版。如果可以尽量先检测wgt的线上发行版更新
- 使用上一步取出的版本包的版本号 和传参 appVersion、wgtVersion 来检测是否有更新。必须同时大于这两项因为上一次可能是wgt热更新否则返回暂无更新
- 如果库中 wgt包 版本大于传参 appVersion但是不满足 min_uni_version < appVersion则不会使用wgt更新会接着判断库中 app包version 是否大于 appVersion
- 返回结果:
|code|message|
|:-:|:-:|
|0|当前版本已经是最新的,不需要更新|
|101|wgt更新|
|102|整包更新|
|-101|暂无更新或检查appid是否填写正确|
|-102|请检查传参是否填写正确|

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,33 @@
export default function() {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
const data = {
action: 'checkVersion',
appid: plus.runtime.appid,
appVersion: plus.runtime.version,
wgtVersion: widgetInfo.version
}
console.log("data: ", data);
uniCloud.callFunction({
name: 'uni-upgrade-center',
data,
success: (e) => {
console.log("e: ", e);
resolve(e)
},
fail: (error) => {
reject(error)
}
})
})
})
// #endif
// #ifndef APP-PLUS
return new Promise((resolve, reject) => {
reject({
message: '请在App中使用'
})
})
// #endif
}

View File

@@ -0,0 +1,158 @@
import callCheckVersion from './call-check-version'
// 推荐再App.vue中使用
const PACKAGE_INFO_KEY = '__package_info__'
export default function() {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
callCheckVersion().then(async (e) => {
if (!e.result) return;
const {
code,
message,
is_silently, // 是否静默更新
url, // 安装包下载地址
platform, // 安装包平台
type // 安装包类型
} = e.result;
// 此处逻辑仅为实例,可自行编写
if (code > 0) {
// 腾讯云和阿里云下载链接不同,需要处理一下,阿里云会原样返回
const {
fileList
} = await uniCloud.getTempFileURL({
fileList: [url]
});
if (fileList[0].tempFileURL)
e.result.url = fileList[0].tempFileURL;
resolve(e)
// 静默更新只有wgt有
if (is_silently) {
uni.downloadFile({
url: e.result.url,
success: res => {
if (res.statusCode == 200) {
// 下载好直接安装,下次启动生效
plus.runtime.install(res.tempFilePath, {
force: false
});
}
}
});
return;
}
/**
* 提示升级一
* 使用 uni.showModal
*/
// return updateUseModal(e.result)
/**
* 提示升级二
* 官方适配的升级弹窗可自行替换资源适配UI风格
*/
uni.setStorageSync(PACKAGE_INFO_KEY, e.result)
uni.navigateTo({
url: `/uni_modules/uni-upgrade-center-app/pages/upgrade-popup?local_storage_key=${PACKAGE_INFO_KEY}`,
fail: (err) => {
console.error('更新弹框跳转失败', err)
uni.removeStorageSync(PACKAGE_INFO_KEY)
}
})
return
} else if (code < 0) {
// TODO 云函数报错处理
console.error(message)
return reject(e)
}
return resolve(e)
}).catch(err => {
// TODO 云函数报错处理
console.error(err.message)
reject(err)
})
});
// #endif
}
/**
* 使用 uni.showModal 升级
*/
function updateUseModal(packageInfo) {
const {
title, // 标题
contents, // 升级内容
is_mandatory, // 是否强制更新
url, // 安装包下载地址
platform, // 安装包平台
type // 安装包类型
} = packageInfo;
let isWGT = type === 'wgt'
let isiOS = !isWGT ? platform.includes('iOS') : false;
let confirmText = isiOS ? '立即跳转更新' : '立即下载更新'
return uni.showModal({
title,
content: contents,
showCancel: !is_mandatory,
confirmText,
success: res => {
if (res.cancel) return;
// 安装包下载
if (isiOS) {
plus.runtime.openURL(url);
return;
}
uni.showToast({
title: '后台下载中……',
duration: 1000
});
// wgt 和 安卓下载更新
downloadTask = uni.downloadFile({
url,
success: res => {
if (res.statusCode !== 200) {
console.error('下载安装包失败', err);
return;
}
// 下载好直接安装,下次启动生效
plus.runtime.install(res.tempFilePath, {
force: false
}, () => {
if (is_mandatory) {
//更新完重启app
plus.runtime.restart();
return;
}
uni.showModal({
title: '安装成功是否重启?',
success: res => {
if (res.confirm) {
//更新完重启app
plus.runtime.restart();
}
}
});
}, err => {
uni.showModal({
title: '更新失败',
content: err
.message,
showCancel: false
});
});
}
});
}
});
}