提交 593878d8 作者: 方治民

feat: 添加 app-notice 全局通知模块 + 云函数 + 数据库表定义相关配置及示例

上级 81229b53
......@@ -64,7 +64,28 @@
"backgroundColorTop": "transparent",
"popGesture": "none",
"scrollIndicator": false,
"titleNView": false
"titleNView": false,
"bounce": "none"
},
"disableScroll": true
}
},
// === 应用全屏通知弹窗(场景:应用维护通知、用户通知公告等) ===
{
"path": "pages/common/notice/index",
"style": {
"navigationStyle": "custom",
"background": "transparent",
"app-plus": {
"animationDuration": 200,
"animationType": "fade-in",
"background": "transparent",
"backgroundColorTop": "transparent",
"popGesture": "none",
"scrollIndicator": false,
"webviewBGTransparent": true,
"titleNView": false,
"bounce": "none"
},
"disableScroll": true
}
......
import dayjs from 'dayjs'
import { useRuntime } from '@/hooks/app/useRuntime'
import { Storage } from '@/utils/storage'
export interface Notice {
/**
* 通知 ID
*/
id: number | string
/**
* 标题
*/
title?: string
/**
* 内容
*/
content: string
/**
* 按钮集合,confirm: 确认,exit: 退出
*/
buttons?: string[]
/**
* 是否已确认
*/
confirmed?: boolean
}
export interface NoticeMessage extends Notice {
/**
* 版本号(code)规则 JSON, 例如 { '>': '10000', '<': '20000' }
*/
version?: Recordable
/**
* 平台,默认为 all,支持 'android' | 'ios' | 'harmonyos' | 'windows' | 'macos' | 'linux' | 'all'
*/
os?: ['android' | 'ios' | 'harmonyos' | 'windows' | 'macos' | 'linux' | 'all']
/**
* 验证参数,用于校验当前用户是否需要弹出通知,例如:用户信息,租户信息等
*/
verify: Recordable
}
/**
* 检查是否有应用通知,如果有自动跳转到通知页面,没有则返回 false
* @param verify 扩展校验参数,校验与通知中的 verify 字段一致
* @returns Promise<boolean> 是否有应用通知
*/
export async function inspect(verify: Recordable = {}): Promise<boolean> {
const { app } = useRuntime()
const system = uni.getSystemInfoSync()
const params = {
datetime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
app: {
id: system.appId,
name: system.appName,
version: system.appVersion,
versionCode: system.appVersionCode,
wgetVersion: app.value.version || system.appWgtVersion || system.appVersion,
language: system.appLanguage,
},
os: {
name: system.osName,
version: system.osVersion,
},
net: {
type: (await uni.getNetworkType()).networkType,
},
rom: {
name: system.romName,
version: system.romVersion,
},
device: {
type: system.deviceType,
brand: system.deviceBrand,
model: system.deviceModel,
},
browser: {
name: system.browserName,
version: system.browserVersion,
ua: system.ua,
},
runtime: {
platform: system.platform,
uniPlatform: system.uniPlatform,
version: system.uniRuntimeVersion,
compileVersion: system.uniCompileVersion,
mode: import.meta.env.MODE,
lastBuildTime: $app.lastBuildTime,
},
// 自定义校验参数(例如:用户信息/租户信息,可参与校验是否需要弹窗,实现精确到指定用户的提醒)
verify,
}
try {
// 调用云函数查询
const result = await uniCloud.callFunction({
name: 'app-notice-inspect',
data: {
params,
},
})
const body = result.result as { id: number | string; params: typeof params; data: NoticeMessage }
console.log('[Notice] Response', params, body)
// 检查是否有通知
const id = body.id
if (!id) {
return false
}
// 缓存通知数据
const data: Notice = {
id,
confirmed: getConfirm(id),
...body.data,
}
getApp().globalData.notice = data
if (!data.confirmed) {
uni.navigateTo({ url: '/pages/common/notice/index' })
return true
}
} catch (e) {
console.warn(e)
}
return false
}
export function setConfirm(id: number | string) {
if (getApp().globalData.notice) {
Storage.set(`notice-${id}`, '1')
getApp().globalData.notice.confirmed = true
}
}
export function getConfirm(id: number | string) {
return Storage.get(`notice-${id}`) === '1'
}
<script setup lang="ts">
import type { Notice } from '.'
import { setConfirm } from '.'
// 获取通知信息
const notice = ref<Notice>(getApp().globalData.notice || {})
// 内容初始化高度
const height = ref(100)
onLoad(() => {
// 没有通知消息或者已经确认过
if (!notice.value.id || notice.value.confirmed) {
Message.toast('暂无通知消息 ~')
uni.navigateBack()
return
}
// 禁止页面侧滑返回
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentWebview = currentPage.$getAppWebview()
currentWebview.setStyle({ popGesture: 'none' })
nextTick(() => {
setTimeout(() => {
uni.createSelectorQuery()
.select('.notice-content')
.boundingClientRect((data: UniNamespace.NodeInfo) => {
// 获取高度, 默认为 100-340
height.value = Math.max(100, Math.min(340, data.height))
})
.exec()
}, 300)
})
})
// 预览图片
function preview(e: { src: string; imageUrls: string[] }) {
uni.previewImage({
urls: [e.src],
current: 0,
})
}
// 点击链接
function atap(href: string) {
if (href.startsWith('http')) {
// 打开外部链接
uni.setClipboardData({
data: href,
success: () => {
Message.toast('已复制链接,请在浏览器中粘贴打开')
},
})
} else if (href.startsWith('/pages/')) {
// 打开内部链接
uni.navigateTo({
url: href,
})
}
}
// 退出应用
function quit() {
// 退出应用
// #ifdef APP-PLUS
if (uni.getSystemInfoSync().platform === 'ios') {
try {
// @ts-expect-error
plus.ios.import('UIApplication').sharedApplication().performSelector('abort')
} catch (e) {
console.error(e)
}
} else {
plus.runtime.quit()
}
// #endif
}
// 确认知道到了
function confirm() {
notice.value.id && setConfirm(notice.value.id)
uni.navigateBack()
}
</script>
<template>
<view class="notice-mask">
<view class="w-80% p-3 bg-white rd-2 shadow shadow-2xl overflow-hidden">
<view class="flex-center mt-2" v-if="notice.title">
<text class="font-bold text-36">{{ notice.title }}</text>
</view>
<FuiDivider text="·" :height="50" v-if="notice.title" />
<view class="h-auto p-1">
<scroll-view scroll-y :style="{ height: `${height}px` }">
<FuiParseGroup class="notice-content" @preview="preview" @atap="atap">
<FuiParse :nodes="notice.content" language="md" />
</FuiParseGroup>
</scroll-view>
</view>
<view class="flex gap-3 mt-3" v-if="notice.buttons.length">
<FuiButton type="gray" @click="confirm" v-if="notice.buttons.includes('confirm')"> 我知道了 </FuiButton>
<FuiButton
color="#fff"
background="#ff6363"
borderColor="#ff6363"
@click="quit"
v-if="notice.buttons.includes('exit')"
>
退出应用
</FuiButton>
</view>
</view>
</view>
</template>
<style lang="less">
// ...
page {
background: transparent;
user-select: none;
}
.notice-mask {
position: fixed;
inset: 0;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
background-color: rgb(0 0 0 / 40%);
}
</style>
......@@ -3,6 +3,7 @@
import { checkUpgrade, closeSplashscreenAndChechUpgrade } from '@/utils/upgrade'
import { useRuntime } from '@/hooks/app/useRuntime'
import { useConcealedExit } from '@/hooks/app/useConcealedExit'
import * as Notice from '@/pages/common/notice'
const { exit } = useConcealedExit()
const { app } = useRuntime()
......@@ -13,27 +14,11 @@
onLoad(() => {
// 关闭启动页并检查更新
closeSplashscreenAndChechUpgrade()
})
// test API
API.example.hello
.request()
.then((body) => {
title.value = body
console.log('[API]', body, $app.name, $app.version)
Message.toast(body)
})
.catch((err) => {
console.error('[API]', err)
})
// test WebSocket(STOMP)
Stomp.connect(() => {
Stomp.send('/app/ping', 'ping')
})
// test uni-stat
uni.report('onLoad', `[Test] onLoad: ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`)
onShow(() => {
// 检查是否有全局通知
Notice.inspect()
})
function surprise() {
......
......@@ -3,23 +3,22 @@ import checkVersion from '@/uni_modules/uni-upgrade-center-app/utils/check-updat
// #endif
export function checkUpgrade(toast = false) {
export async function checkUpgrade(toast = false): Promise<void> {
// #ifdef APP-PLUS
// 版本更新检查
if (toast) {
Message.loading('正在检查更新...')
}
checkVersion()
.then((res) => {
if (toast && res?.code === 0) {
Message.toast('已是最新版本~')
}
})
.finally(() => {
if (toast) {
Message.hideLoading()
}
})
try {
const res = await checkVersion()
if (toast && res?.code === 0) {
Message.toast('已是最新版本~')
}
} finally {
if (toast) {
Message.hideLoading()
}
}
// #endif
}
......@@ -27,7 +26,7 @@ export function checkUpgrade(toast = false) {
* 关闭 splashscreen 并检查更新
*/
export function closeSplashscreenAndChechUpgrade() {
return new Promise<void>((resolve) => {
return new Promise<void>((resolve, reject) => {
// FIXED: pages 第一路由页面为登录页,加上 splashscreen 配置控制使得用户体验更好
// #ifdef APP-PLUS
nextTick(() => {
......@@ -35,11 +34,15 @@ export function closeSplashscreenAndChechUpgrade() {
const { platform } = uni.getSystemInfoSync()
const isAndroid = platform === 'android'
setTimeout(
() => {
async () => {
plus.navigator.closeSplashscreen()
// 检查更新
checkUpgrade()
resolve()
try {
// 检查更新
await checkUpgrade()
resolve()
} catch (e) {
reject(e)
}
},
isAndroid ? 1000 : 50,
)
......
......@@ -28,7 +28,9 @@
"@/*": ["src/*"],
"/@/*": ["src/*"],
"/#/*": ["types/*"]
}
},
"rootDir": ".",
"outDir": "dist"
},
"include": [
"src/**/*.ts",
......
'use strict'
exports.main = async (event, context) => {
// eslint-disable-next-line no-undef
const db = uniCloud.database()
const dbCmd = db.command
const { total } = await db.collection('opendb-app-list').where({ appid: context.appId }).count()
if (total < 1) {
return { errCode: 1, errMsg: '应用信息不存在' }
}
// 查询优先级最高且在生效中的 APP 通知
const { params } = event
const { data: notices } = await db
.collection('app-notice')
.where({
appid: context.appId,
enabled: true,
expired: dbCmd.gt(Date.now()),
})
.orderBy('priority', 'asc')
.limit(1)
.get()
if (notices.length < 1) {
return { errCode: 1, errMsg: '暂无生效中的通知' }
}
// 获取通知信息
const notice = notices[0]
console.log('notice', notice)
// 检查运行环境
if (notice.mode !== params.runtime.mode) {
return { errCode: 1, errMsg: '运行环境不匹配', message: `${params.runtime.mode} -> ${notice.mode}` }
}
// 检查平台
if (notice.os && notice.os.length > 0 && !notice.os.includes('all') && !notice.os.includes(params.os.name)) {
return { errCode: 1, errMsg: '平台不匹配', message: `${params.os.name} -> ${JSON.stringify(notice.os)}` }
}
// 检查版本号
if (notice.version) {
const keys = Object.keys(notice.version)
const code = Number(
context.appWgtVersion
.split('.')
.map((v) => String(v).padStart(2, '0'))
.join(''),
)
const mismatching = keys
.map((v) => {
switch (v) {
case '>':
if (code <= notice.version[v]) {
return false
}
break
case '<':
if (code >= notice.version[v]) {
return false
}
break
case '=':
if (code !== notice.version[v]) {
return false
}
break
case '>=':
if (code < notice.version[v]) {
return false
}
break
case '<=':
if (code > notice.version[v]) {
return false
}
break
default:
return false
}
return true
})
.some((v) => !v)
if (mismatching) {
return {
errCode: 1,
errMsg: '版本号不匹配',
message: `${context.appWgtVersion} -> ${code} -> ${JSON.stringify(notice.version)}`,
}
}
}
// 检查自定义参数
const verifyKeys = Object.keys(params.verify)
if (
verifyKeys.length < Object.keys(notice.verify).length ||
verifyKeys.some((v) => notice.verify[v] !== params.verify[v])
) {
return {
errCode: 1,
errMsg: '校验参数不匹配',
message: `${JSON.stringify(params.verify)} -> ${JSON.stringify(notice.verify)}`,
}
}
// 返回数据给客户端
return {
id: notice._id,
params: {
app: params.app,
verify: params.verify,
},
data: {
...notice,
},
}
}
{
"name": "app-notice-inspect",
"dependencies": {},
"extensions": {
"uni-cloud-jql": {}
}
}
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": true,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"created_at": {
"description": "创建时间"
},
"update_at": {
"description": "更新时间"
},
"appid": {
"description": "appId"
},
"title": {
"description": "标题"
},
"content": {
"description": "内容"
},
"buttons": {
"description": "按钮列表"
},
"priority": {
"description": "优先级,数值越小,优先级越高"
},
"enabled": {
"description": "是否启用"
},
"expired": {
"description": "过期时间"
},
"platform": {
"description": "平台"
},
"mode": {
"description": "运行模式,development:开发模式,preview:预览模式,production:生产模式"
},
"version": {
"description": "版本号规则 JSON, 例如 { '>': '1.0.0', '<': '2.0.0' }"
},
"verify": {
"description": "自定义校验参数 JSON, 例如 { 'userId': '1', 'tenant': 'YR' }"
}
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论