1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00

chore: 批量新增项目依赖、工具函数、页面与组件资源

1. 新增mp-html、qs等生产依赖,补全项目基础库
2. 新增平台判断、缓存、工具函数等通用工具集
3. 新增标签页、网站浏览页、关于页等业务页面
4. 新增分类卡片、通知弹窗等业务组件
5. 新增uts-progressNotification、liu-poster、uhalo-upgrade等uni模块
6. 补充audio/video组件样式补件,修复uni-components路径缺失问题
7. 新增环境变量Halo个人令牌配置项
8. 重构store导出结构,新增appConfig/halo/setting三个状态模块
9. 新增tsconfig编译目标配置,适配更高版本ES语法
This commit is contained in:
小莫唐尼
2026-08-31 19:56:16 +08:00
parent ba5b77568b
commit 067f3ed98a
147 changed files with 20866 additions and 325 deletions
+22
View File
@@ -0,0 +1,22 @@
## 1.1.02023-07-05
优化APP端生成逻辑
## 1.0.92023-07-04
优化
## 1.0.82023-07-04
增加注意事项
## 1.0.72023-07-04
修改本地图片不显示问题
## 1.0.62023-06-26
优化
## 1.0.52023-06-09
增加子集绘制
## 1.0.42023-06-09
增加子集绘制
## 1.0.32023-06-08
增加预览二维码
## 1.0.22023-05-31
增加license
## 1.0.12023-05-30
增加示例
## 1.0.02023-05-30
初始化发布
@@ -0,0 +1,377 @@
<template>
<view class="canvas-main">
<canvas :style="'width:'+width+'rpx;height:'+height+'rpx;'" class="canvas-item" disable-scroll="true"
canvas-id="canvasId" @error="error"></canvas>
</view>
</template>
<script>
export default {
props: {
//画布宽度(rpx)
width: {
type: Number,
default: 750
},
//画布高度(rpx)
height: {
type: Number,
default: 750
},
//生成的图片格式(jpg或png)
fileType: {
type: String,
default: 'png'
},
},
data() {
return {
pixelRatio: 0,
context: null,
canvasList: []
}
},
methods: {
async init(list) {
uni.showLoading({
title: '正在绘制...'
})
if (this.context) {
await this.clear()
this.canvasList = []
this.context = null
}
this.canvasList = JSON.parse(JSON.stringify(list))
const systemInfo = uni.getSystemInfoSync()
this.pixelRatio = systemInfo.pixelRatio
this.context = uni.createCanvasContext('canvasId', this)
this.start()
},
clear() {
return new Promise(async (resolve, reject) => {
await this.context.clearRect(0, 0, this.width, this.height)
resolve()
})
},
async start() {
await Promise.all(this.canvasList.map(async res => {
if (res.type == 'color') {
await this.drawBg(res)
} else if (res.type == 'image') {
await this.drawImage(res)
} else if (res.type == 'text') {
await this.drawText(res)
} else if (res.type == 'line') {
await this.drawLine(res)
}
}))
this.save()
},
drawBg(item) {
return new Promise(async (resolve, reject) => {
item.width = uni.upx2px(item.width)
item.height = uni.upx2px(item.height)
item.x = uni.upx2px(item.x)
item.y = uni.upx2px(item.y)
item.radius = uni.upx2px(item.radius)
item.lineWidth = uni.upx2px(item.lineWidth)
let gradient = ''
if (item.colorObj && item.colorObj.colorList) {
if (item.colorObj.colorList.length == 1) {
this.context.fillStyle = item.colorObj.colorList[0]
} else {
if (item.colorObj.direction == 1) {
gradient = this.context.createLinearGradient(0, 0, item.height, 0)
} else if (item.colorObj.direction == 2) {
gradient = this.context.createLinearGradient(0, 0, 0, item.height)
} else if (item.colorObj.direction == 3) {
gradient = this.context.createLinearGradient(0, 0, item.width, item.height)
} else if (item.colorObj.direction == 4) {
gradient = this.context.createLinearGradient(item.width, 0, 0, item.height)
}
gradient.addColorStop(0, item.colorObj.colorList[0])
gradient.addColorStop(1, item.colorObj.colorList[1])
this.context.fillStyle = gradient
}
} else {
this.context.fillStyle = '#FFFFFF'
}
this.context.save()
if (item.radius > 0) {
this.context.beginPath()
this.context.moveTo(item.x + item.radius, item.y)
this.context.arcTo(item.x + item.width, item.y, item.x + item.width, item.y +
item.radius, item.radius)
this.context.lineTo(item.x + item.width, item.y + item.height - item.radius)
this.context.arcTo(item.x + item.width, item.y + item.height, item.x + item
.width - item.radius, item.y + item.height, item.radius)
this.context.lineTo(item.x + item.radius, item.y + item.height)
this.context.arcTo(item.x, item.y + item.height, item.x, item.y + item
.height - item.radius, item.radius)
this.context.lineTo(item.x, item.y + item.radius)
this.context.arcTo(item.x, item.y, item.x + item.radius, item.y, item.radius)
this.context.closePath()
this.context.clip()
}
this.context.fillRect(item.x, item.y, item.width, item.height)
if (item.lineWidth) {
this.context.setLineDash([])
this.context.lineWidth = item.lineWidth
this.context.strokeStyle = item.lineColor
this.context.beginPath()
this.context.moveTo(item.x + item.radius, item.y)
this.context.arcTo(item.x + item.width, item.y, item.x + item.width, item.y +
item.radius, item.radius)
this.context.lineTo(item.x + item.width, item.y + item.height - item.radius)
this.context.arcTo(item.x + item.width, item.y + item.height, item.x + item
.width - item.radius, item.y + item.height, item.radius)
this.context.lineTo(item.x + item.radius, item.y + item.height)
this.context.arcTo(item.x, item.y + item.height, item.x, item.y + item
.height - item.radius, item.radius)
this.context.lineTo(item.x, item.y + item.radius)
this.context.arcTo(item.x, item.y, item.x + item.radius, item.y, item.radius)
this.context.closePath()
this.context.stroke()
}
this.context.restore()
await this.context.draw(true)
if (item.childs && item.childs.length > 0) {
await Promise.all(item.childs.map(async res => {
if (res.type == 'color') {
await this.drawBg(res)
} else if (res.type == 'image') {
await this.drawImage(res)
} else if (res.type == 'text') {
await this.drawText(res)
} else if (res.type == 'line') {
await this.drawLine(res)
}
}))
}
resolve()
})
},
drawImage(item) {
return new Promise(async (resolve, reject) => {
item.width = uni.upx2px(item.width)
item.height = uni.upx2px(item.height)
item.x = uni.upx2px(item.x)
item.y = uni.upx2px(item.y)
item.radius = uni.upx2px(item.radius)
item.lineWidth = uni.upx2px(item.lineWidth)
await this.getImageInfo(item.path).then(async res => {
this.context.save()
if (item.radius > 0) {
this.context.beginPath()
this.context.moveTo(item.x + item.radius, item.y)
this.context.arcTo(item.x + item.width, item.y, item.x + item.width,
item.y +
item.radius, item.radius)
this.context.lineTo(item.x + item.width, item.y + item.height - item
.radius)
this.context.arcTo(item.x + item.width, item.y + item.height, item.x +
item
.width - item.radius, item.y + item.height, item.radius)
this.context.lineTo(item.x + item.radius, item.y + item.height)
this.context.arcTo(item.x, item.y + item.height, item.x, item.y + item
.height - item.radius, item.radius)
this.context.lineTo(item.x, item.y + item.radius)
this.context.arcTo(item.x, item.y, item.x + item.radius, item.y, item
.radius)
this.context.closePath()
this.context.clip()
}
await this.context.drawImage(res, item.x, item.y, item.width, item
.height)
if (item.lineWidth) {
this.context.setLineDash([])
this.context.lineWidth = item.lineWidth
this.context.strokeStyle = item.lineColor
this.context.beginPath()
this.context.moveTo(item.x + item.radius, item.y)
this.context.arcTo(item.x + item.width, item.y, item.x + item.width,
item.y +
item.radius, item.radius)
this.context.lineTo(item.x + item.width, item.y + item.height - item
.radius)
this.context.arcTo(item.x + item.width, item.y + item.height, item.x +
item
.width - item.radius, item.y + item.height, item.radius)
this.context.lineTo(item.x + item.radius, item.y + item.height)
this.context.arcTo(item.x, item.y + item.height, item.x, item.y + item
.height - item.radius, item.radius)
this.context.lineTo(item.x, item.y + item.radius)
this.context.arcTo(item.x, item.y, item.x + item.radius, item.y, item
.radius)
this.context.closePath()
this.context.stroke()
}
this.context.restore()
await this.context.draw(true)
if (item.childs && item.childs.length > 0) {
await Promise.all(item.childs.map(async res => {
if (res.type == 'color') {
await this.drawBg(res)
} else if (res.type == 'image') {
await this.drawImage(res)
} else if (res.type == 'text') {
await this.drawText(res)
} else if (res.type == 'line') {
await this.drawLine(res)
}
}))
}
resolve()
})
})
},
drawText(item) {
return new Promise(async (resolve, reject) => {
item.width = uni.upx2px(item.width)
item.height = uni.upx2px(item.height)
item.x = uni.upx2px(item.x)
item.y = uni.upx2px(item.y)
item.fontSize = uni.upx2px(item.fontSize)
item.lineHeight = uni.upx2px(item.lineHeight)
await this.drawTextInfo(item.content, item.x, item.y, item.fontSize, item.color, item
.width, item.height, item.lineHeight, item.bold, true)
resolve()
})
},
drawLine(item) {
return new Promise(async (resolve, reject) => {
item.width = uni.upx2px(item.width)
item.startX = uni.upx2px(item.startX)
item.startY = uni.upx2px(item.startY)
item.endX = uni.upx2px(item.endX)
item.endY = uni.upx2px(item.endY)
this.context.setStrokeStyle(item.color)
this.context.setLineWidth(item.width)
this.context.setLineCap('round')
if (item.lineType == 'dash') this.context.setLineDash([item.width * 5, item.width * 5], 0)
else this.context.setLineDash([])
this.context.beginPath()
this.context.moveTo(item.startX, item.startY)
this.context.lineTo(item.endX, item.endY)
this.context.stroke()
this.context.closePath()
resolve()
})
},
drawTextInfo(text, x, y, fontSize, color, width, height, lineHeight, bold, ellipsis) {
return new Promise(async (resolve, reject) => {
this.context.setFillStyle(color)
if (bold) this.context.font = 'bold ' + fontSize + 'px Arial'
else this.context.font = fontSize + 'px Arial'
this.context.setTextBaseline('bottom')
let textArray = text.split('')
let line = ''
let lines = []
for (let i = 0; i < textArray.length; i++) {
let testLine = line + textArray[i]
let testWidth = this.context.measureText(testLine).width
if (testWidth > width) {
lines.push(line)
line = textArray[i]
} else {
line = testLine
}
}
lines.push(line)
let firstWidth = this.context.measureText(lines[0]).width
if (height >= lineHeight * lines.length) {
await Promise.all(lines.map(async (res, i) => {
let lineText = res
let lineHeights = lineHeight * (i + 1)
await this.context.fillText(lineText, x, y + lineHeights)
}))
} else {
let sNum = parseInt(height / lineHeight)
lines = lines.slice(0, sNum)
await Promise.all(lines.map(async (res, i) => {
let lineText = res
let lineHeights = lineHeight * (i + 1)
if (i == lines.length - 1) {
if (this.context.measureText('...').width < fontSize) {
lineText = lineText.substring(0, lineText.length - 1)
lineText += '...'
} else {
lineText = lineText.substring(0, lineText.length - 2)
lineText += '...'
}
}
await this.context.fillText(lineText, x, y + lineHeights)
}))
}
resolve()
})
},
getImageInfo(src) {
return new Promise(async (resolve, reject) => {
if (src.indexOf('http') == -1) {
setTimeout(() => {
resolve(src)
})
} else {
// #ifdef APP-PLUS
uni.getImageInfo({
src: src,
success: (res) => {
resolve(res.path)
},
fail(err) {
resolve(src)
}
})
// #endif
// #ifndef APP-PLUS
uni.downloadFile({
url: src,
success: (res) => {
if (res.statusCode === 200) resolve(res.tempFilePath)
},
fail: (err) => {
resolve(src)
}
})
// #endif
}
})
},
save() {
let timer = setTimeout(async () => {
await this.context.draw(true, setTimeout(() => {
uni.canvasToTempFilePath({
canvasId: 'canvasId',
fileType: this.fileType,
quality: 1,
width: this.width,
height: this.height,
destWidth: this.width * this.pixelRatio,
destHeight: this.height * this.pixelRatio,
success: (res) => {
uni.hideLoading()
this.$emit('change', res.tempFilePath)
},
fail: (err) => {
console.log('生成图片失败:', err)
}
}, this)
}, 500))
clearTimeout(timer)
}, 500)
},
error(e) {
console.log('错误信息:', e)
}
}
}
</script>
</script>
<style lang="scss" scoped>
.canvas-main {
position: fixed;
z-index: -999999 !important;
opacity: 0;
top: -5000rpx;
}
</style>
+6
View File
@@ -0,0 +1,6 @@
### 1、本插件可免费下载使用;
### 2、未经许可,严禁复制本插件派生同类插件上传插件市场;
### 3、未经许可,严禁在插件市场恶意复制抄袭本插件进行违规获利;
### 4、对本软件的任何使用都必须遵守这些条款,违反这些条款的个人或组织将面临法律追究。
+100
View File
@@ -0,0 +1,100 @@
{
"id": "liu-poster",
"displayName": "canvas海报画板、海报生成、海报图",
"version": "1.1.0",
"description": "canvas海报画板、海报生成、海报图组件,配置简单,支持绘制背景色、绘制图片、绘制文本、绘制线条,自由生成海报图片",
"keywords": [
"海报",
"生成海报",
"canvas",
"图片合成",
"图片处理"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0",
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": "-",
"vue3": "-"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "-",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}
+260
View File
@@ -0,0 +1,260 @@
# liu-poster适用于uni-app项目的canvas海报画板、海报生成、海报图组件
### 本组件目前兼容微信小程序、H5
### 本组件是canvas海报画板、海报生成、海报图组件,配置简单,支持绘制背景色、绘制图片、绘制文本、绘制线条,自由生成海报图片
# --- 扫码预览、关注我们 ---
## 扫码关注公众号,查看更多插件信息,预览插件效果!
![](https://uni.ckapi.pro/uniapp/publicize.png)
### 属性说明
| 名称 | 类型 | 默认值 | 描述 |
| ----------------------------|--------------- | -------------------- | ---------------|
| width | Number | 750 | 画布宽度(rpx)
| height | Number | 750 | 画布高度(rpx)
| fileType | String | png | 生成的图片格式(jpg或png)
| @change | Function | | 海报绘制成功回调事件
### 使用示例
```
<template>
<view class="tab-box">
<view class="btn-complete" @click="open">一键生成海报</view>
<liu-poster ref="liuPoster" :width="750" :height="1300" @change="change"></liu-poster>
<image class="success-img" :src="url" @click="previewImg(url)"></image>
</view>
</template>
<script>
export default {
data() {
return {
canvasList: [{
type: 'color', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 750, //宽度(rpx)
height: 1300, //高度(rpx)
x: 0, //x轴位置(离左边的距离rpx)
y: 0, //y轴位置(离上边的距离rpx)
radius: 100, //圆角(rpx)
lineWidth: 40, //边框宽度(rpx)
lineColor: '#000000', //边框颜色
colorObj: {
colorList: ['#6900FF', '#FFFFFF'], //传入1个值为纯色,2个值为渐变色
direction: 2 //渐变色绘制方向(1:从左到右;2:从上到下;3:左上角到右下角;4:右上角到左下角)
}, //type为color时必填
}, {
type: 'image', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 132, //宽度(rpx)
height: 132, //高度(rpx)
x: 40, //x轴位置(离左边的距离rpx)
y: 120, //y轴位置(离上边的距离rpx)
radius: 66, //圆角(rpx)
lineWidth: 6, //边框宽度(rpx)
lineColor: '#FFFFFF', //边框颜色
path: 'https://img1.baidu.com/it/u=1471990434,2209509794&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400', //图片地址(type为image时必填)
}, {
type: 'text', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 400, //文本宽度(rpx)
height: 40, //文本高度(rpx)
x: 200, //x轴位置(离左边的距离rpx)
y: 145, //y轴位置(离上边的距离rpx)
color: '#FFFFFF', //文本颜色
fontSize: 36, //文字大小(rpx)
lineHeight: 36, //文字行高(rpx)
bold: true, //文字是否加粗
content: '好物分享猫猫虫', //文本内容(type为text时必填)
}, {
type: 'text', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 400, //文本宽度(rpx)
height: 40, //文本高度(rpx)
x: 200, //x轴位置(离左边的距离rpx)
y: 195, //y轴位置(离上边的距离rpx)
color: '#FFFFFF', //文本颜色
fontSize: 28, //文字大小(rpx)
lineHeight: 28, //文字行高(rpx)
bold: false, //文字是否加粗
content: '猫猫虫给你分享了一张美图', //文本内容(type为text时必填)
}, {
type: 'image', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 670, //宽度(rpx)
height: 670, //高度(rpx)
x: 40, //x轴位置(离左边的距离rpx)
y: 300, //y轴位置(离上边的距离rpx)
radius: 20, //圆角(rpx)
lineWidth: 12, //边框宽度(rpx)
lineColor: '#FFFFFF', //边框颜色
path: 'https://img1.baidu.com/it/u=1471990434,2209509794&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400', //图片地址(type为image时必填)
childs: [{
type: 'text', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 400, //文本宽度(rpx)
height: 40, //文本高度(rpx)
x: 100, //x轴位置(离左边的距离rpx)
y: 400, //y轴位置(离上边的距离rpx)
color: '#FFFFFF', //文本颜色
fontSize: 36, //文字大小(rpx)
lineHeight: 36, //文字行高(rpx)
bold: true, //文字是否加粗
content: '好物分享猫猫虫', //文本内容(type为text时必填)
}]
}, {
type: 'line', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 4, //线条宽度(rpx)
color: '#FFFFFF', //线条颜色
startX: 20, //起点x轴位置(离左边的距离rpx)
startY: 270, //起点y轴位置(离上边的距离rpx)
endX: 730, //终点x轴位置(离左边的距离rpx)
endY: 270, //终点y轴位置(离上边的距离rpx)
lineType: 'dash', //线条类型(solid:实线;dash:虚线)
}, {
type: 'line', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 4, //线条宽度(rpx)
color: '#FFFFFF', //线条颜色
startX: 20, //起点x轴位置(离左边的距离rpx)
startY: 1000, //起点y轴位置(离上边的距离rpx)
endX: 730, //终点x轴位置(离左边的距离rpx)
endY: 1000, //终点y轴位置(离上边的距离rpx)
lineType: 'dash', //线条类型(solid:实线;dash:虚线)
}, {
type: 'text', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 500, //文本宽度(rpx)
height: 150, //文本高度(rpx)
x: 40, //x轴位置(离左边的距离rpx)
y: 1050, //y轴位置(离上边的距离rpx)
color: '#9043FD', //文本颜色
fontSize: 32, //文字大小(rpx)
lineHeight: 45, //文字行高(rpx)
bold: true, //文字是否加粗
content: '这个是一段测试文字,这个是一段测试文字,这个是一段测试文字,这个是一段测试文字,这个是一段测试文字。', //文本内容(type为text时必填)
}, {
type: 'image', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 150, //宽度(rpx)
height: 150, //高度(rpx)
x: 550, //x轴位置(离左边的距离rpx)
y: 1050, //y轴位置(离上边的距离rpx)
radius: 4, //圆角(rpx)
lineWidth: 6, //边框宽度(rpx)
lineColor: '#FFFFFF', //边框颜色
path: 'https://img1.baidu.com/it/u=1471990434,2209509794&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400', //图片地址(type为image时必填)
}],
url: ''
};
},
methods: {
//开始绘制
open() {
this.$nextTick(() => {
this.$refs.liuPoster.init(this.canvasList)
})
},
//绘制成功返回生成的海报图片地址
change(e) {
this.url = e
},
//预览生成的海报图片
previewImg(url) {
if (!url) return
uni.previewImage({
urls: [url]
})
}
}
};
</script>
<style lang="scss" scoped>
.tab-box {
width: 100%;
height: 100vh;
box-sizing: border-box;
background-color: #f0f0f0;
padding-top: 20rpx;
.btn-reset {
width: 100%;
height: 72rpx;
background: #FFFFFF;
border-radius: 40rpx;
border: 2rpx solid #FD430E;
font-size: 30rpx;
color: #3E3E3E;
display: flex;
align-items: center;
justify-content: center;
}
.btn-complete {
width: 98%;
height: 76rpx;
border-radius: 40rpx;
font-size: 30rpx;
color: #FFFFFF;
background-color: #FD430E;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.success-img {
width: 100%;
height: 1300rpx;
margin-top: 20rpx;
}
}
</style>
```
### 传入的canvasList参数说明
### 绘制类型有4种:color:背景色;image:图片;text:文字;line:线条
```
canvasList: [{
type: 'color', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 750, //宽度(rpx)
height: 1500, //高度(rpx)
x: 0, //x轴位置(离左边的距离rpx)
y: 0, //y轴位置(离上边的距离rpx)
radius: 100, //圆角(rpx)
lineWidth: 40, //边框宽度(rpx)
lineColor: '#000000', //边框颜色
colorObj: {
colorList: ['#6900FF', '#FFFFFF'], //传入1个值为纯色,2个值为渐变色
direction: 2 //渐变色绘制方向(1:从左到右;2:从上到下;3:左上角到右下角;4:右上角到左下角)
}, //type为color时必填
childs:[],//在背景色上绘制的内容放在childs里面即可
}, {
type: 'image', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 132, //宽度(rpx)
height: 132, //高度(rpx)
x: 40, //x轴位置(离左边的距离rpx)
y: 150, //y轴位置(离上边的距离rpx)
radius: 66, //圆角(rpx)
lineWidth: 2, //边框宽度(rpx)
lineColor: '#FFFFFF', //边框颜色
path: 'https://img1.baidu.com/it/u=1471990434,2209509794&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400', //图片地址(type为image时必填)
childs:[],//如果在图片上绘制其他内容则将要绘制的内容放在childs里面即可
}, {
type: 'text', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 400, //文本宽度(rpx)
height: 100, //文本高度(rpx)
x: 200, //x轴位置(离左边的距离rpx)
y: 170, //y轴位置(离上边的距离rpx)
color: '#FFFFFF', //文本颜色
fontSize: 36, //文字大小(rpx)
lineHeight: 45, //文字行高(rpx)
bold: true, //文字是否加粗
content: '好物分享猫猫虫好物分享猫猫虫好物分享猫猫虫好物分享猫猫虫好物分享猫猫虫好物分享猫猫虫好物分享猫猫虫', //文本内容(type为text时必填)
}, {
type: 'line', //绘制类型(color:背景色;image:图片;text:文字;line:线条),
width: 2, //线条宽度(rpx)
color: '#FFFFFF', //线条颜色
startX: 0, //起点x轴位置(离左边的距离rpx)
startY: 310, //起点y轴位置(离上边的距离rpx)
endX: 750, //终点x轴位置(离左边的距离rpx)
endY: 310, //终点y轴位置(离上边的距离rpx)
lineType: 'dash', //线条类型(solid:实线;dash:虚线)
}]
```
### 注意
# 1、H5端使用网络图片需要解决跨域问题;
# 2、小程序使用网络图片需要在微信公众平台配置downloadFile合法域名。
+141
View File
@@ -0,0 +1,141 @@
## 1.0.02026-08-30
- 适配 uhalo:基于 uni-upgrade-center-app 0.9.12 复制改造,模块更名为 uhalo-upgrade
- 移除 uniCloud 依赖:checkVersion 由云函数调用改为 HTTP GET 请求 Halo 插件 plugin-uni-halo 公开接口
- 新增 baseUrl 参数:callCheckVersion(baseUrl) / checkUpdate(baseUrl) 需由调用方传入 Halo 站点地址(如 HaloTokenConfig.BASE_API
- 安装包地址为 Halo 附件直链,移除 cloud:// 临时链接处理逻辑
- 删除 uniCloud/database 目录(无云函数/云数据库依赖)
- 移除 UNI-APP-X 适配(条件编译分支、uni-app-x 弹窗页面、uts-openSchema 依赖),仅支持 uni-appvueApp 端
## 0.9.122026-08-03
- 修复 安卓平台、鸿蒙平台 蒸汽模式 下载/跳转按钮文字不居中
## 0.9.112026-08-01
- 修复 运行到 uni-app x Vapor 升级按钮样式上不可见
## 0.9.102026-04-27
- 修复 uni-app-x 项目编译时 warning
## 0.9.92026-02-03
- 修复 安卓端非强制更新 kotlin 报错 `onClick has not been intialized`
## 0.9.82026-01-05
- 更新 移除 vapor 模式不支持的 class 选择器
## 0.9.72025-07-28
- 修复 使用腾讯云时,wgt 更新报错的Bug
- 改进 uni-app-x 平台弹窗该用 script setup 实现
## 0.9.62025-04-01
- 新增 升级中心适配鸿蒙 uni-app x **需要 HBuilderX 4.61+**
## 0.9.52025-02-06
- 新增 完善下载失败时的处理逻辑
## 0.9.42024-12-28
- 修复 腾讯云在使用扩展存储时报错的 Bug
## 0.9.32024-12-23
- 修复 升级中心在大屏上的显示效果
## 0.9.22024-11-06
- 更新 部分 ts 类型
## 0.9.12024-11-01
- 更新 支持 HarmonyOS Next 设备整包更新、wgt 更新。需要 `HBuilderX 4.32+` [详情](https://doc.dcloud.net.cn/uniCloud/upgrade-center.html#uni-upgrade-center-app-harmonyos)
## 0.9.02024-10-30
- **重要更新** 在 uni-app x 项目中弃用之前弹窗方案使用[dialogPage](https://doc.dcloud.net.cn/uni-app-x/api/dialog-page.html)实现,需要 `HBuilderX 4.31+`
## 0.8.52024-10-26
- 优化 去除不必要代码
## 0.8.42024-10-26
- 修复 uni-app x 项目升级到 4.31 alpha 后中间有空隙的Bug
## 0.8.32024-07-31
- 修复 部分类型报错
## 0.8.22024-07-15
- 更新 static 下的静态图片放入 static/app 目录下,防止编译除 app 平台以外的平台时带入
## 0.8.12024-04-28
- 修复 在 HX 4.0.3+ uni-app x 项目运行到 Android 调不起安装的Bug
## 0.8.02024-04-15
- 修复 更新弹窗 data 中新增初始化字段
## 0.7.92024-03-15
- 移除无用代码
- 调整 is_silently 类型为可为 null
## 0.7.82024-01-04
- 新增 移除无用代码
## 0.7.72024-01-04
- 新增 uni-app x 项目中新增 @show 回调
## 0.7.62023-12-21
- 修复 iOS使用升级中心云打包时报错(使用新版的 [uts-progressNotification](https://ext.dcloud.net.cn/plugin?name=uts-progressNotification) 插件,如果之前下载过请删除 `uts-progressNotification\utssdk\app-ios` 文件夹)
## 0.7.52023-12-12
- 新增 通知栏进度条使用 uts-progressNotification 插件
- 新增 依赖 uni-installApk、uts-progressNotification。使用前要安装插件三方依赖
## 0.7.42023-11-29
- 修复 uni-app-x 项目中由上版引发的无法升级的Bug
## 0.7.32023-11-27
- 修复 在 uni-app x 中无更新时报错的Bug
## 0.7.22023-11-20
- 新增 插件根目录 utils 文件夹中新增 check-update-nvue.js 文件(vue2 的 nvue 页面请引用该文件)
## 0.7.12023-11-17
- 修复 运行至浏览器 ts 语法报错
## 0.7.02023-11-10
- 新增 兼容 uni-app x 项目 [详情](https://uniapp.dcloud.net.cn/uniCloud/upgrade-center.html)
## 0.6.52023-10-27
- 修复 安装 wgt 报错 manifest.json 文件不存在的Bug
## 0.6.42023-09-01
chore: 优化代码结构
## 0.6.32023-08-30
- 修复 下载 wgt 时如果后缀名不正确,重命名后安装
## 0.6.22022-11-21
- 处理 cloudfunctions 目录
## 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
- 升级中心前台检查更新
+110
View File
@@ -0,0 +1,110 @@
{
"id": "uhalo-upgrade",
"displayName": "uhalo 升级中心 - App",
"version": "1.0.0",
"description": "uhalo 升级中心 - 客户端检查更新(uni-upgrade-center-app 适配版,对接 Halo 插件 plugin-uni-halo 的 checkVersion 接口)",
"keywords": [
"uhalo",
"升级",
"update",
"wgt",
"halo"
],
"repository": "",
"engines": {
"HBuilderX": "^4.31",
"uni-app": "^4.35"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-page",
"darkmode": "x",
"i18n": "x",
"widescreen": "√"
},
"uni_modules": {
"dependencies": [
"uts-progressNotification"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": {
"extVersion": "1.0.0",
"minVersion": ""
},
"vue3": {
"extVersion": "1.0.0",
"minVersion": ""
}
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": {
"extVersion": "1.0.0",
"minVersion": ""
},
"nvue": {
"extVersion": "1.0.0",
"minVersion": ""
},
"android": {
"extVersion": "1.0.0",
"minVersion": "21"
},
"ios": {
"extVersion": "1.0.0",
"minVersion": "12"
},
"harmony": {
"extVersion": "1.0.0",
"minVersion": "12"
}
},
"mp": {
"weixin": "x",
"alipay": "x",
"toutiao": "x",
"baidu": "x",
"kuaishou": "x",
"jd": "x",
"harmony": "x",
"qq": "x",
"lark": "x",
"xhs": "-"
},
"quickapp": {
"huawei": "x",
"union": "x"
}
}
}
}
}
}
@@ -0,0 +1,679 @@
<template>
<view class="mask flex-center" v-if="shown">
<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="/uni_modules/uhalo-upgrade/static/app/bg_top.png"></image>
</view>
<view class="content-header"></view>
<view class="content-body">
<view class="title">
<text>{{ subTitle }}</text>
<text class="content-body-version">{{ 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="isApplicationStore">
<button class="content-button" style="border: none; color: #fff" plain @click="jumpToApplicationStore">
{{ downLoadBtnTextiOS }}
</button>
</template>
<template v-else>
<template v-if="!downloadSuccess">
<view class="progress-box flex-column" v-if="downloading">
<progress class="progress" :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-else-if="installed && !isWGT"
class="content-button"
style="border: none; color: #fff"
plain
:loading="installing"
:disabled="installing"
@click="installPackage"
>
安装未完成点击安装
</button>
<button v-else-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="/uni_modules/uhalo-upgrade/static/app/app_update_close.png" @click.stop="closeUpdate"></image>
</view>
</view>
</template>
<script>
// #ifdef APP-PLUS
import { createNotificationProgress, cancelNotificationProgress, finishNotificationProgress } from '@/uni_modules/uts-progressNotification';
// #endif
import { compare, platform_iOS, platform_Android, platform_Harmony } from '../utils/utils'
const localFilePathKey = 'UNI_ADMIN_UPGRADE_CENTER_LOCAL_FILE_PATH';
let downloadTask = null;
let openSchemePromise;
export default {
emits: ['close', 'show'],
data() {
return {
// 从之前下载安装
installForBeforeFilePath: '',
// 安装
installed: false,
installing: false,
// 下载
downloadSuccess: false,
downloading: false,
downLoadPercent: 0,
downloadedSize: 0,
packageFileSize: 0,
tempFilePath: '', // 要安装的本地包地址
// 默认安装包信息
title: '更新日志',
contents: '',
version: '',
is_mandatory: false,
url: '',
platform: [],
store_list: null,
// 可自定义属性
subTitle: '发现新版本',
downLoadBtnTextiOS: '立即跳转更新',
downLoadBtnText: '立即下载更新',
downLoadingText: '安装包下载中,请稍后',
// #ifdef APP-PLUS
shown: true,
// #endif
// #ifdef APP-HARMONY
shown: false,
// #endif
};
},
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;
}
this.setLocalPackageInfo(localPackageInfo)
},
onBackPress() {
// 强制更新不允许返回
if (this.is_mandatory) return true;
if (!this.needNotificationProgress) downloadTask && downloadTask.abort();
},
onHide() {
openSchemePromise = null;
},
computed: {
isWGT() {
return this.type === 'wgt';
},
isNativeApp() {
return this.type === 'native_app';
},
isiOS() {
return this.platform.indexOf(platform_iOS) !== -1;
},
isAndroid() {
return this.platform.indexOf(platform_Android) !== -1;
},
isHarmony() {
return this.platform.indexOf(platform_Harmony) !== -1;
},
isApplicationStore() {
return !this.isWGT && this.isNativeApp && (
this.isiOS ||
this.isHarmony
)
// return this.isiOS || (!this.isiOS && !this.isWGT && this.url.indexOf('.apk') === -1);
},
needNotificationProgress() {
return this.platform.indexOf(platform_iOS) === -1 && !this.is_mandatory && !this.isHarmony;
}
},
methods: {
show(shown, localPackageInfo) {
// #ifdef APP-HARMONY
this.$emit('show')
if (localPackageInfo) {
this.shown = shown
this.setLocalPackageInfo(localPackageInfo)
} else {
console.error(`安装包信息为空,请检查后重试`);
}
// #endif
},
setLocalPackageInfo(localPackageInfo) {
const requiredKey = ['version', 'url', 'type'];
for (let key in localPackageInfo) {
if (requiredKey.indexOf(key) !== -1 && !localPackageInfo[key]) {
console.error(`参数 ${key} 必填,请检查后重试`);
// #ifdef APP-PLUS
uni.navigateBack();
// #endif
// #ifdef APP-HARMONY
this.shown = false
// #endif
return;
}
}
Object.assign(this, localPackageInfo);
this.checkLocalStoragePackage();
},
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);
}
}
},
askAbortDownload() {
uni.showModal({
title: '是否取消下载?',
cancelText: '否',
confirmText: '是',
success: (res) => {
if (res.confirm) {
downloadTask && downloadTask.abort();
if (this.needNotificationProgress) {
cancelNotificationProgress();
}
uni.navigateBack();
}
}
});
},
async closeUpdate() {
if (this.downloading) {
if (this.is_mandatory) {
return uni.showToast({
title: '下载中,请稍后……',
icon: 'none',
duration: 500
});
}
if (!this.needNotificationProgress) {
this.askAbortDownload();
return;
}
}
if (!this.needNotificationProgress && this.downloadSuccess && this.tempFilePath) {
// 包已经下载完毕,稍后安装,将包保存在本地
await this.saveFile(this.tempFilePath, this.version);
}
// #ifdef APP-PLUS
uni.navigateBack();
// #endif
// #ifdef APP-HARMONY
this.shown = false
this.$emit('close')
// #endif
},
updateApp() {
this.checkStoreScheme()
.catch(() => {
this.downloadPackage();
})
.finally(() => {
openSchemePromise = null;
});
},
// 跳转应用商店
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) {
// fix: wgt 文件下载完成后后缀不是 wgt
if (this.isWGT && res.tempFilePath.split('.').slice(-1)[0] !== 'wgt') {
const failCallback = (e) => {
console.log('[FILE RENAME FAIL]', JSON.stringify(e));
};
// #ifndef APP-HARMONY
plus.io.resolveLocalFileSystemURL(
res.tempFilePath,
(entry) => {
entry.getParent((parent) => {
const newName = `new_wgt_${Date.now()}.wgt`;
entry.copyTo(
parent,
newName,
(res) => {
this.tempFilePath = res.fullPath;
this.downLoadComplete();
},
failCallback
);
}, failCallback);
},
failCallback
);
// #endif
// #ifdef APP-HARMONY
failCallback({code: -1, message: 'Download content error, is not wgt.'})
// #endif
} else {
this.tempFilePath = res.tempFilePath;
this.downLoadComplete();
}
} else {
console.log('下载错误:' + JSON.stringify(res))
this.downloadFail()
}
},
fail: (err) => {
console.log('下载错误:' + JSON.stringify(err))
this.downloadFail()
}
});
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);
if (this.needNotificationProgress && !this.downloadSuccess) {
createNotificationProgress({
title: '升级中心正在下载安装包……',
content: `${this.downLoadPercent}%`,
progress: this.downLoadPercent,
onClick: () => {
this.askAbortDownload();
}
});
}
});
if (this.needNotificationProgress) {
uni.navigateBack();
}
},
downloadFail() {
const errMsg = '下载失败,请点击重试'
this.downloadSuccess = false;
this.downloading = false;
this.downLoadPercent = 0;
this.downloadedSize = 0;
this.packageFileSize = 0;
this.downLoadBtnText = errMsg
downloadTask = null;
if (this.needNotificationProgress) {
finishNotificationProgress({
title: '升级包下载失败',
content: '请重新检查更新',
onClick: () => {}
});
}
},
downLoadComplete() {
this.downloadSuccess = true;
this.downloading = false;
this.downLoadPercent = 0;
this.downloadedSize = 0;
this.packageFileSize = 0;
downloadTask = null;
if (this.needNotificationProgress) {
finishNotificationProgress({
title: '安装升级包',
content: '下载完成',
onClick: () => {}
});
this.installPackage();
return;
}
// 强制更新,直接安装
if (this.is_mandatory) {
this.installPackage();
}
},
installPackage() {
// #ifdef APP-PLUS || APP-HARMONY
// 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) {
// #ifdef APP-PLUS
uni.showLoading({
icon: 'none',
title: '安装成功,正在重启……'
});
// #endif
setTimeout(() => {
// #ifdef APP-PLUS
uni.hideLoading();
// #endif
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-HARMONY
uni.showModal({
title: '更新完毕',
content: '请手动重启',
showCancel: false,
success(res) {
plus.runtime.quit()
}
})
// #endif
// #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
});
},
jumpToApplicationStore() {
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, 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;
}
.content-body {
width: 100%;
}
.content-body-version {
padding-left: 20rpx;
color: #fff;
font-size: 20rpx;
margin-left: 10rpx;
padding: 4rpx 8rpx;
border-radius: 20rpx;
background: #50aefd;
}
.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>
+98
View File
@@ -0,0 +1,98 @@
# uhalo-upgrade
基于 DCloud 官方 `uni-upgrade-center-app`v0.9.12)复制改造的 App 升级检测模块,适配 uni-halo 项目:
- **不再依赖 uniCloud**:原插件通过 `uniCloud.callFunction('uni-upgrade-center')` 检测升级,本插件改为 HTTP GET 请求 Halo 插件 [plugin-uni-halo](https://github.com/uhalo/plugin-uni-halo) 的公开接口 `checkVersion`
- **baseUrl 由调用方传入**uni_modules 插件无法直接读取项目配置文件(`config/uhalo.config.js`),因此调用时需显式传入 Halo 站点地址;
- 弹窗页面、下载安装、静默/强制更新、iOS 跳 AppStore 等逻辑与原插件保持一致。
- **不支持 uni-app x**:已移除 UNI-APP-X 适配(条件编译分支、uni-app-x 弹窗页面、uts-openSchema 依赖),仅支持 uni-appvueApp 端。
## 接口约定
检测升级请求地址(后端见 `.docs/app-upgrade-design.md` 第 3.2 节):
```
GET {baseUrl}/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/upgrade/checkVersion
?appid=xxx&appVersion=1.0.0&wgtVersion=1.0.0&platform=Android&isUniappX=false
```
返回结构与原插件 `UniUpgradeCenterResult` 完全一致(snake_case),app 端弹窗零改动复用。
## 使用方式
### 1. 页面注册
在项目 `pages.json` 中注册升级弹窗页面(App 端使用):
```json
{
"path": "uni_modules/uhalo-upgrade/pages/upgrade-popup",
"style": {
"disableScroll": true,
"app-plus": {
"backgroundColor": "rgba(0,0,0,0)",
"animationType": "fade-in",
"animationDuration": 200
}
}
}
```
### 2. 检测升级(推荐在 App.vue 中调用)
```js
import HaloTokenConfig from '@/config/uhalo.config.js'
import CheckAppUpdate from '@/uni_modules/uhalo-upgrade/utils/check-update'
// baseUrl 即 Halo 站点地址(config/uhalo.config.js 中的 BASE_API,域名后不带斜杠)
CheckAppUpdate(HaloTokenConfig.BASE_API)
```
> HarmonyOS Next 平台需要传递弹窗组件(vue3 组件),`check-update.ts` 中为 `checkUpdate(component, baseUrl)`
>
> ```js
> CheckAppUpdate(upgradePopupComponentRef, HaloTokenConfig.BASE_API)
> ```
若未传入 baseUrl,模块会 reject 并提示「未传入 baseUrl,无法检测升级」。
### 3. nvue 页面
nvue 工程使用 `@/uni_modules/uhalo-upgrade/utils/check-update-nvue.js`,用法相同:
```js
import CheckAppUpdate from '@/uni_modules/uhalo-upgrade/utils/check-update-nvue'
CheckAppUpdate(HaloTokenConfig.BASE_API)
```
## 依赖
- `uts-progressNotification`:通知栏下载进度(Android
依赖模块需随项目一并安装(项目 `uni_modules/` 目录中已包含)。
## 与 uni-upgrade-center-app 的差异
| 项 | uni-upgrade-center-app | uhalo-upgrade |
|---|---|---|
| 检测方式 | `uniCloud.callFunction('uni-upgrade-center')` | HTTP GET `checkVersion`Halo 插件) |
| baseUrl | 云函数自动获取 | 调用方传入(如 `HaloTokenConfig.BASE_API` |
| 安装包地址 | `cloud://` 云存储临时链接 | Halo 附件直链(`/upload/...`),无需换临时链接 |
| 模块名/路径 | `uni_modules/uni-upgrade-center-app/` | `uni_modules/uhalo-upgrade/` |
| 数据库 | uniCloud 集合 | Halo 插件自定义扩展(AppInfo / AppVersion |
## 完整升级链路
```
[Halo 控制台] 发布版本(标题/内容/平台/版本号/apk/wgt)→ 上线发行(stable_publish
[app 端] 启动 → checkUpdate(baseUrl) → GET checkVersion(匿名,见 role-anonymous.yaml
[Halo 插件] 按 appid+platform 查稳定版 → 选版本大者(wgt 优先)→ 101/102/0
[app 端] code>0 → 静默更新直接下载安装;否则弹窗(pages/upgrade-popup
→ 下载 → plus.runtime.install → 强制更新重启;iOS 跳 AppStore
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -0,0 +1,121 @@
export type StoreListItem = {
enable : boolean
id : string
name : string
scheme : string
priority : number // 优先级
}
export type UniUpgradeCenterResult = {
_id : string
appid : string
name : string
title : string
contents : string
url : string // 安装包下载地址
platform : Array<string> // Array<'Android' | 'iOS' | 'Harmony'>
version : string // 版本号 1.0.0
uni_platform : string // "android" | "ios" | 'harmony'
stable_publish : boolean // 是否是稳定版
is_mandatory : boolean // 是否强制更新
is_silently : boolean | null // 是否静默更新
create_env : string // "upgrade-center"
create_date : number
message : string
code : number
type : string // "native_app" | "wgt"
store_list : StoreListItem[] | null
min_uni_version : string | null // 升级 wgt 的最低 uni-app 版本
}
/**
* Halo 插件(plugin-uni-halo)公开的 checkVersion 接口路径,
* 由调用方传入的 baseUrl(Halo 站点地址)拼接而成
*/
const CHECK_VERSION_API = '/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/upgrade/checkVersion'
/**
* 拼接完整的 checkVersion 请求地址,自动去除 baseUrl 末尾多余的斜杠
*/
function buildCheckUrl(baseUrl : string) : string {
let url = baseUrl
while (url.endsWith('/')) {
url = url.substring(0, url.length - 1)
}
return url + CHECK_VERSION_API
}
/**
* 将 uni.getSystemInfoSync().platform 归一化为后端识别的平台名(Android/iOS/Harmony),
* 无法识别时返回空字符串(后端会按 User-Agent 自行推导)
*/
function normalizePlatform(platform : string) : string {
const p = platform.toLowerCase()
if (p.indexOf('ios') !== -1) {
return 'iOS'
}
if (p.indexOf('android') !== -1) {
return 'Android'
}
if (p.indexOf('harmony') !== -1) {
return 'Harmony'
}
return ''
}
/**
* 检测升级(uhalo 版)
* 原 uni-upgrade-center-app 通过 uniCloud.callFunction 调用云函数,
* 本插件改为 HTTP GET 请求 Halo 插件的 checkVersion 接口。
* uni_modules 插件无法直接读取项目配置文件,因此 baseUrl 必须由调用方传入。
* @param baseUrl Halo 站点地址,如 HaloTokenConfig.BASE_API(域名后不能带斜杠)
*/
export default function (baseUrl : string) : Promise<UniUpgradeCenterResult> {
// #ifdef APP
return new Promise<UniUpgradeCenterResult>((resolve, reject) => {
if (!baseUrl) {
reject('【uhalo-upgrade】未传入 baseUrl,无法检测升级。请调用 checkUpdate(baseUrl) 时传入 Halo 站点地址')
return
}
const systemInfo = uni.getSystemInfoSync()
const appId = systemInfo.appId
const appVersion = systemInfo.appVersion //systemInfo.appVersion
const platform = typeof systemInfo.platform === 'string' ? normalizePlatform(systemInfo.platform) : ''
const checkUrl = buildCheckUrl(baseUrl)
if (typeof appId === 'string' && typeof appVersion === 'string' && appId.length > 0 && appVersion.length > 0) {
plus.runtime.getProperty(appId, function (widgetInfo) {
if (widgetInfo.version) {
uni.request({
url: checkUrl,
method: 'GET',
data: {
appid: appId,
appVersion: appVersion,
wgtVersion: widgetInfo.version,
platform: platform
},
success: (e) => {
resolve(e.data as UniUpgradeCenterResult)
},
fail: (error) => {
reject(error)
}
})
} else {
reject('widgetInfo.version is EMPTY')
}
})
} else {
reject('plus.runtime.appid is EMPTY')
}
})
// #endif
// #ifndef APP
return new Promise((resolve, reject) => {
reject({
message: '请在App中使用'
})
})
// #endif
}
@@ -0,0 +1,180 @@
function callCheckVersion(baseUrl) {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
if (!baseUrl) {
reject('【uhalo-upgrade】未传入 baseUrl,无法检测升级。请调用 checkUpdate(baseUrl) 时传入 Halo 站点地址')
return
}
const checkUrl = baseUrl.replace(/\/+$/, '') + '/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/upgrade/checkVersion'
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
uni.request({
url: checkUrl,
method: 'GET',
data: {
appid: plus.runtime.appid,
appVersion: plus.runtime.version,
wgtVersion: widgetInfo.version
},
success: (e) => {
resolve(e.data)
},
fail: (error) => {
reject(error)
}
})
})
})
// #endif
// #ifndef APP-PLUS
return new Promise((resolve, reject) => {})
// #endif
}
// 推荐再App.vue中使用
const PACKAGE_INFO_KEY = '__package_info__'
export default function(baseUrl) {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
callCheckVersion(baseUrl).then((result) => {
if (!result) return;
const {
code,
message,
is_silently, // 是否静默更新
url, // 安装包下载地址
platform, // 安装包平台
type // 安装包类型
} = result;
// uhalo 版:url 为 Halo 附件直链,无需获取临时链接
// 此处逻辑仅为实例,可自行编写
if (code > 0) {
resolve(result)
// 静默更新,只有wgt有
if (is_silently) {
uni.downloadFile({
url: result.url,
success: res => {
if (res.statusCode == 200) {
// 下载好直接安装,下次启动生效
plus.runtime.install(res.tempFilePath, {
force: false
});
}
}
});
return;
}
/**
* 提示升级一
* 使用 uni.showModal
*/
// return updateUseModal(result)
/**
* 提示升级二
* 官方适配的升级弹窗,可自行替换资源适配UI风格
*/
uni.setStorageSync(PACKAGE_INFO_KEY, result)
uni.navigateTo({
url: `/uni_modules/uhalo-upgrade/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(result)
}
return resolve(result)
}).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
});
});
}
});
}
});
}
@@ -0,0 +1,185 @@
import callCheckVersion, { UniUpgradeCenterResult } from "./call-check-version"
import { platform_iOS } from './utils'
// 推荐再App.vue中使用
const PACKAGE_INFO_KEY = '__package_info__'
/**
* 升级检测实现(提取为内部函数:条件编译双函数声明共享函数体时 TS 无法解析,统一走这里)
* @param baseUrl Halo 站点地址
* @param component HarmonyOS Next 平台组件
*/
function checkUpdateImpl(baseUrl: string, component?: any) : Promise<UniUpgradeCenterResult> {
return new Promise<UniUpgradeCenterResult>((resolve, reject) => {
callCheckVersion(baseUrl).then((uniUpgradeCenterResult) => {
// NOTE uni-app x 3.96 解构有问题
const code = uniUpgradeCenterResult.code
const message = uniUpgradeCenterResult.message
// uhalo 版:url 为 Halo 附件直链,无需获取临时链接,下载与安装由升级弹窗处理
// 此处逻辑仅为示例,可自行编写
if (code > 0) {
/**
* 提示升级一
* 使用 uni.showModal
*/
// return updateUseModal(uniUpgradeCenterResult)
// 静默更新,只有wgt有
if (uniUpgradeCenterResult.is_silently) {
uni.downloadFile({
url: uniUpgradeCenterResult.url,
success: res => {
if (res.statusCode == 200) {
// 下载好直接安装,下次启动生效
plus.runtime.install(res.tempFilePath, {
force: false
});
}
}
});
return;
}
/**
* 提示升级二
* 官方适配的升级弹窗,可自行替换资源适配UI风格
*/
// #ifdef APP-PLUS
uni.setStorageSync(PACKAGE_INFO_KEY, uniUpgradeCenterResult)
uni.navigateTo({
url: `/uni_modules/uhalo-upgrade/pages/upgrade-popup?local_storage_key=${PACKAGE_INFO_KEY}`,
fail: (err) => {
console.error('更新弹框跳转失败', err)
uni.removeStorageSync(PACKAGE_INFO_KEY)
}
})
// #endif
// #ifdef APP-HARMONY
if (component) {
component.show(true, uniUpgradeCenterResult)
} else {
reject({
code: -1,
message: '在 HarmonyOS Next 平台请传递组件使用'
})
}
// #endif
return resolve(uniUpgradeCenterResult)
} else if (code < 0) {
console.error(message)
return reject(uniUpgradeCenterResult)
}
return resolve(uniUpgradeCenterResult)
}).catch((err) => {
reject(err)
})
});
}
// 平台差异仅在参数传法上:Harmony 传 (component, baseUrl),其余传 (baseUrl)
export default function (a?: any, b?: any) : Promise<UniUpgradeCenterResult> {
// #ifdef APP-HARMONY
return checkUpdateImpl(b || '', a)
// #endif
// #ifndef APP-HARMONY
return checkUpdateImpl(a)
// #endif
}
/**
* 使用 uni.showModal 升级
*/
function updateUseModal(packageInfo : UniUpgradeCenterResult) : void {
// #ifdef APP
const {
title, // 标题
contents, // 升级内容
is_mandatory, // 是否强制更新
url, // 安装包下载地址
type,
platform
} = packageInfo;
let isWGT = type === 'wgt'
let isiOS = !isWGT ? platform.includes(platform_iOS) : false;
let confirmText = isiOS ? '立即跳转更新' : '立即下载更新'
uni.showModal({
title,
content: contents,
showCancel: !is_mandatory,
confirmText,
success: res => {
if (res.cancel) return;
if (isiOS) {
// iOS 平台跳转 AppStore
plus.runtime.openURL(url);
return;
}
uni.showToast({
title: '后台下载中……',
duration: 1000
});
// wgt 和 安卓下载更新
uni.downloadFile({
url,
success: res => {
if (res.statusCode !== 200) {
console.error('下载安装包失败');
return;
}
// 下载好直接安装,下次启动生效
plus.runtime.install(res.tempFilePath, {
force: false
}, () => {
if (is_mandatory) {
//更新完重启app
// #ifdef APP-PLUS
plus.runtime.restart();
// #endif
// #ifdef APP-HARMONY
uni.showModal({
title: '安装成功',
content: '请手动重启应用',
showCancel: false,
success: res => {
plus.runtime.quit();
}
});
// #endif
return;
}
uni.showModal({
title: '安装成功是否重启?',
success: res => {
if (res.confirm) {
//更新完重启app
// #ifdef APP-PLUS
plus.runtime.restart();
// #endif
// #ifdef APP-HARMONY
plus.runtime.quit();
// #endif
}
}
});
}, err => {
uni.showModal({
title: '更新失败',
content: err
.message,
showCancel: false
});
});
}
});
}
});
// #endif
}
@@ -0,0 +1,46 @@
export const platform_iOS: string = 'iOS';
export const platform_Android: string = 'Android';
export const platform_Harmony: string = 'Harmony';
/**
* 对比版本号,如需要,请自行修改判断规则
* 支持比对 ("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
*/
export function compare(v_1: string = '0', v_2: string = '0') {
const v1: string[] = String(v_1).split('.');
const v2: string[] = String(v_2).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;
}
@@ -0,0 +1,28 @@
## 1.1.22025-02-10
修复某些情况通过点击通知消息无法拉起App的bug
## 1.1.12024-09-03
去除TypeScript警告
## 1.1.02024-03-08
修复uniapp打包报错问题
## 1.0.92024-02-29
去除代码过时警告
## 1.0.82023-12-21
去除app-ios目录
## 1.0.72023-12-11
去除无用代码
## 1.0.62023-12-11
修改文档
## 1.0.52023-12-11
1.修改插件名称
2.修改插件引入方式为import导入
## 1.0.42023-11-30
1. createNotificationProgress增加`onClick`回调
2.修复在小米部分系统上,通知消息会归类于不重要通知的bug
## 1.0.32023-11-28
更新截图
## 1.0.22023-11-28
修改资源的包名
## 1.0.12023-11-28
更新文档
## 1.0.02023-11-28
Android通知栏显示进度插件
@@ -0,0 +1,85 @@
{
"id": "uts-progressNotification",
"displayName": "uts-progressNotification",
"version": "1.1.2",
"description": "uts-progressNotification",
"keywords": [
"progressNotification"
],
"repository": "",
"engines": {
"HBuilderX": "^3.91"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "TargetSDKVersion33以上时需配置\n`android.permission.POST_NOTIFICATIONS`"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-android": {
"minVersion": "19"
},
"app-ios": "n",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "n",
"Android Browser": "n",
"微信浏览器(Android)": "n",
"QQ浏览器(Android)": "n"
},
"H5-pc": {
"Chrome": "n",
"IE": "n",
"Edge": "n",
"Firefox": "n",
"Safari": "n"
},
"小程序": {
"微信": "n",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "n",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}
@@ -0,0 +1,71 @@
# uts-progressNotification
## 使用说明
Android平台创建显示进度的通知栏消息
**注意: 需要自定义基座,否则点击通知栏消息不会拉起应用**
### 导入
需要import导入插件
### createNotificationProgress(options : CreateNotificationProgressOptions) : void,
创建显示进度的通知栏消息
参数说明
```
export type CreateNotificationProgressOptions = {
/**
* 通知标题
* @defaultValue 应用名称
*/
title ?: string | null
/**
* 通知内容
*/
content : string,
/**
* 进度
*/
progress : number,
/**
* 点击通知消息回调
* @defaultValue null
*/
onClick? : (() => void) | null
}
```
### finishNotificationProgress(options: FinishNotificationProgressOptions) : void
完成时调用的API,比如下载完成后需要显示下载完成并隐藏进度时调用。
参数说明
```
export type FinishNotificationProgressOptions = {
/**
* 通知标题
* @defaultValue 应用名称
*/
title ?: string | null
/**
* 通知内容
*/
content : string,
/**
* 点击通知消息回调
*/
onClick : () => void
}
```
### cancelNotificationProgress() : void
取消通知消息显示
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="uts.sdk.modules.utsProgressNotification">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application>
<activity android:name="uts.sdk.modules.utsProgressNotification.TransparentActivity"
android:theme="@style/DCNotificationProgressTranslucentTheme" android:hardwareAccelerated="true"
android:screenOrientation="user" android:exported="true">
</activity>
</application>
</manifest>
@@ -0,0 +1,62 @@
import Activity from "android.app.Activity";
import Bundle from 'android.os.Bundle';
import Build from 'android.os.Build';
import View from 'android.view.View';
import Color from 'android.graphics.Color';
import WindowManager from 'android.view.WindowManager';
import { getGlobalNotificationProgressCallBack, getGlobalNotificationProgressFinishCallBack, setGlobalNotificationProgressCallBack, setGlobalNotificationProgressFinishCallBack} from './callbacks.uts';
import { ACTION_DOWNLOAD_FINISH, ACTION_DOWNLOAD_PROGRESS } from "./constant.uts"
export class TransparentActivity extends Activity {
constructor() {
super()
}
@Suppress("DEPRECATION")
override onCreate(savedInstanceState : Bundle | null) {
super.onCreate(savedInstanceState)
this.fullScreen(this)
const action = this.getIntent().getAction()
if (action == ACTION_DOWNLOAD_FINISH) {
setTimeout(() => {
getGlobalNotificationProgressFinishCallBack()?.()
setGlobalNotificationProgressFinishCallBack(() => { })
}, 100)
this.overridePendingTransition(0, 0)
}
if (action == ACTION_DOWNLOAD_PROGRESS) {
setTimeout(() => {
getGlobalNotificationProgressCallBack()?.()
setGlobalNotificationProgressCallBack(() => { })
}, 100)
this.overridePendingTransition(0, 0)
}
setTimeout(() => {
this.finish()
}, 20)
}
@Suppress("DEPRECATION")
private fullScreen(activity : Activity) {
if (Build.VERSION.SDK_INT >= 19) {
if (Build.VERSION.SDK_INT >= 21) {
const window = activity.getWindow();
const decorView = window.getDecorView();
const option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(option);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
} else {
const window = activity.getWindow();
const attributes = window.getAttributes();
const flagTranslucentStatus = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
attributes.flags |= flagTranslucentStatus;
window.setAttributes(attributes);
}
}
}
}
@@ -0,0 +1,19 @@
let globalNotificationProgressCallBack : (() => void) | null = () => { }
let globalNotificationProgressFinishCallBack : (() => void) | null = () => { }
export function setGlobalNotificationProgressCallBack(callBack : (() => void) | null) : void {
globalNotificationProgressCallBack = callBack
}
export function getGlobalNotificationProgressCallBack() : (() => void) | null {
return globalNotificationProgressCallBack
}
export function setGlobalNotificationProgressFinishCallBack(callBack : (() => void) | null) : void {
globalNotificationProgressFinishCallBack = callBack
}
export function getGlobalNotificationProgressFinishCallBack() : (() => void) | null {
return globalNotificationProgressFinishCallBack
}
@@ -0,0 +1,3 @@
{
"minSdkVersion": "19"
}
@@ -0,0 +1,2 @@
export const ACTION_DOWNLOAD_FINISH = "ACTION_DOWNLOAD_FINISH"
export const ACTION_DOWNLOAD_PROGRESS = "ACTION_DOWNLOAD_PROGRESS"
@@ -0,0 +1,156 @@
import Build from 'android.os.Build';
import Context from 'android.content.Context';
import NotificationManager from 'android.app.NotificationManager';
import NotificationChannel from 'android.app.NotificationChannel';
import Notification from 'android.app.Notification';
import Intent from 'android.content.Intent';
import ComponentName from 'android.content.ComponentName';
import PendingIntent from 'android.app.PendingIntent';
import { CreateNotificationProgressOptions, FinishNotificationProgressOptions } from '../interface.uts';
import { ACTION_DOWNLOAD_FINISH, ACTION_DOWNLOAD_PROGRESS } from "./constant.uts"
import { setGlobalNotificationProgressCallBack, setGlobalNotificationProgressFinishCallBack } from './callbacks.uts';
export { TransparentActivity } from './TransparentActivity.uts';
const DOWNLOAD_PROGRESS_NOTIFICATION_ID : Int = 7890
const DC_DOWNLOAD_CHANNEL_ID = "下载文件"
const DC_DOWNLOAD_CHANNEL_NAME = "用于显示现在进度的渠道"
let notificationBuilder : Notification.Builder | null = null
let timeId = -1
let histroyProgress = 0
let isProgress = false
export function createNotificationProgress(options : CreateNotificationProgressOptions) : void {
const { content, progress, onClick } = options
if (progress == 100) {
clearTimeout(timeId)
const context = UTSAndroid.getAppContext() as Context
realCreateNotificationProgress(options.title ?? getAppName(context), content, progress, onClick)
reset()
return
}
histroyProgress = progress
if (timeId != -1) {
return
}
const context = UTSAndroid.getAppContext() as Context
if (!isProgress) {
realCreateNotificationProgress(options.title ?? getAppName(context), content, histroyProgress, onClick)
isProgress = true
} else {
timeId = setTimeout(() => {
realCreateNotificationProgress(options.title ?? getAppName(context), content, histroyProgress, onClick)
timeId = -1
}, 1000)
}
}
export function cancelNotificationProgress() : void {
const context = UTSAndroid.getAppContext() as Context
const notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(DOWNLOAD_PROGRESS_NOTIFICATION_ID)
reset()
}
function realCreateNotificationProgress(title : string, content : string, progress : number, cb : (() => void) | null) : void {
setGlobalNotificationProgressCallBack(cb)
const context = UTSAndroid.getAppContext() as Context
const notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
createDownloadChannel(notificationManager)
const builder = createNotificationBuilder(context)
builder.setProgress(100, progress.toInt(), false)
builder.setContentTitle(title)
builder.setContentText(content)
builder.setContentIntent(createPendingIntent(context, ACTION_DOWNLOAD_PROGRESS));
notificationManager.notify(DOWNLOAD_PROGRESS_NOTIFICATION_ID, builder.build())
}
export function finishNotificationProgress(options : FinishNotificationProgressOptions) {
setGlobalNotificationProgressFinishCallBack(options.onClick)
const context = UTSAndroid.getAppContext() as Context
const notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
createDownloadChannel(notificationManager)
const builder = createNotificationBuilder(context)
builder.setProgress(0, 0, false)
builder.setContentTitle(options.title ?? getAppName(context))
builder.setContentText(options.content)
//小米rom setOngoing未false的时候,会被通知管理器归为不重要通知
// builder.setOngoing(false)
builder.setAutoCancel(true);
builder.setContentIntent(createPendingIntent(context, ACTION_DOWNLOAD_FINISH));
notificationManager.notify(DOWNLOAD_PROGRESS_NOTIFICATION_ID, builder.build())
reset()
}
function reset() {
isProgress = false
notificationBuilder = null
histroyProgress = 0
if (timeId != -1) {
clearTimeout(timeId)
timeId = -1
}
}
function createPendingIntent(context : Context, action : string) : PendingIntent {
const intent = new Intent(action);
intent.setComponent(new ComponentName(context.getPackageName(), "uts.sdk.modules.utsProgressNotification.TransparentActivity"));
let flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= 23) {
flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
}
return PendingIntent.getActivity(context, DOWNLOAD_PROGRESS_NOTIFICATION_ID, intent, flags);
}
function createDownloadChannel(notificationManager : NotificationManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
const channel = new NotificationChannel(
DC_DOWNLOAD_CHANNEL_ID,
DC_DOWNLOAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(channel)
}
}
@Suppress("DEPRECATION")
function createNotificationBuilder(context : Context) : Notification.Builder {
if (notificationBuilder == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationBuilder = new Notification.Builder(context, DC_DOWNLOAD_CHANNEL_ID)
} else {
notificationBuilder = new Notification.Builder(context)
}
notificationBuilder!.setSmallIcon(context.getApplicationInfo().icon)
notificationBuilder!.setOngoing(true)
notificationBuilder!.setSound(null)
}
return notificationBuilder!
}
@Suppress("DEPRECATION")
function getAppName(context : Context) : string {
let appName = ""
try {
const packageManager = context.getPackageManager()
const applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0)
appName = packageManager.getApplicationLabel(applicationInfo) as string
} catch (e : Exception) {
e.printStackTrace()
}
return appName
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="DCNotificationProgressTranslucentTheme">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
</resources>
@@ -0,0 +1,46 @@
export type CreateNotificationProgressOptions = {
/**
* 通知标题
* @defaultValue 应用名称
*/
title ?: string | null
/**
* 通知内容
*/
content : string,
/**
* 进度
*/
progress : number,
/**
* 点击通知消息回调
* @defaultValue null
*/
onClick? : (() => void) | null
}
export type FinishNotificationProgressOptions = {
/**
* 通知标题
* @defaultValue 应用名称
*/
title ?: string | null
/**
* 通知内容
*/
content : string,
/**
* 点击通知消息回调
*/
onClick : () => void
}
export type CreateNotificationProgress = (options : CreateNotificationProgressOptions) => void;
export type CancelNotificationProgress = () => void;
export type FinishNotificationProgress = (options: FinishNotificationProgressOptions) => void