feat:增加uniapp更新页面
This commit is contained in:
parent
139d91d08b
commit
ce1ef491f8
593
docs/superpowers/specs/2026-07-02-preload-api-design.md
Normal file
593
docs/superpowers/specs/2026-07-02-preload-api-design.md
Normal file
@ -0,0 +1,593 @@
|
||||
# uniapp+vue3 API 预加载通用方案 — 设计
|
||||
|
||||
- **日期**:2026-07-02
|
||||
- **作者**:Claude Fable 5(与项目 owner 协作)
|
||||
- **范围**:`frontend/`(uniapp + Vue 3 + Vite),主要服务于 App 端
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
为 uniapp + Vue 3 项目提供一套**通用、可配置、按场景分层的 API 预加载方案**,覆盖 4 类典型场景:
|
||||
|
||||
1. **页面切换前预先拉数据**:进入 A 页时按目标页 B 的清单提前拉好 B 的接口
|
||||
2. **应用启动时批量预热常用数据**:`onLaunch` 期间并发拉启动清单
|
||||
3. **通用响应缓存层**:相同 `url+params` 在 TTL 内复用结果
|
||||
4. **idle 时机预拉**:首屏渲染完成后用空闲时间拉「可能下一屏」的数据
|
||||
|
||||
**非目标**:
|
||||
- 不重写 `request()` 本身(保持 `utils/api.js` 签名和返回值不变,仅新增 `.abort()` 方法)
|
||||
- 不做请求合并 / dedup beyond 同一 cacheKey(避免引入 dispatcher complexity)
|
||||
- 不做离线缓存(断网时不读文件缓存兜底)
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构与模块
|
||||
|
||||
### 2.1 目录结构
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── utils/preloadApi/
|
||||
│ ├── core.js # 命令式核心(零 Vue 依赖)
|
||||
│ ├── config.js # 默认 config 导入与合并
|
||||
│ ├── storage.js # storage 适配器(用户隔离、namespace 隔离)
|
||||
│ ├── scheduler.js # idle / 启动期调度
|
||||
│ ├── navigate.js # wrap uni.navigateTo / switchTab / reLaunch
|
||||
│ └── index.js # 统一导出 preloadApi
|
||||
├── composables/
|
||||
│ └── usePreload.js # Vue 3 包装:暴露响应式 state
|
||||
└── config/
|
||||
└── preload.config.js # 用户声明:key / fetcher / ttl / persistence / 触发时机
|
||||
```
|
||||
|
||||
### 2.2 模块职责
|
||||
|
||||
| 模块 | 职责 |
|
||||
| --- | --- |
|
||||
| `core.js` | 注册/执行/查询/失效;维护内存 Map + LRU;并发去重 | **零外部依赖**(不依赖 Vue / uni / api.js / store;fetcher 返回的 Promise 自带 `.abort()`,core 仅存储调用)
|
||||
| `config.js` | 把用户 `preload.config.js` 与内置默认值合并成一个运行时 config |
|
||||
| `storage.js` | 文件缓存适配器:通过 `plus.io`(APP-PLUS)/ `uni.getFileSystemManager`(兜底)读写 `_doc/preload/{userId}/` 目录;按 userId + namespace 隔离 |
|
||||
| `scheduler.js` | `warmStartup()`:跑 startup 清单;`warmIdle()`:用 `requestIdleCallback` 兜底跑 idle 清单 |
|
||||
| `navigate.js` | 重写 `uni.navigateTo/switchTab/reLaunch`,命中目标页时按 pages 映射触发预拉 |
|
||||
| `usePreload.js` | 包装 `core.get()`,对组件返回 `{ data, loading, error, refresh }` |
|
||||
|
||||
### 2.3 调用关系
|
||||
|
||||
```
|
||||
App.vue onLaunch
|
||||
└─► scheduler.warmStartup() ─┐
|
||||
├─► core.run(key) ─► request() ─► api.js
|
||||
preloadApi.navigateTo('/pages/foo?id=1') ─┘
|
||||
└─► prefetchFor(targetPath, params) ─┐
|
||||
├─► core.run(key)
|
||||
Page onLoad (composable) ─┘
|
||||
└─► usePreload(key, params) ─► core.get(key)
|
||||
```
|
||||
|
||||
### 2.4 对现有代码的侵入
|
||||
|
||||
| 文件 | 改动 |
|
||||
| --- | --- |
|
||||
| `App.vue` | ~10 行:import + `warmStartup()` 到 `onLaunch` + `warmIdle()` 到 `onShow` + patch `SET_USER_INFO` / `CLEAR_AUTH` 注入失效调用 |
|
||||
| `pages/*/...vue` | 把原本 `request({...})` 改成 `usePreload('key')`(setup / store action / watch 中的请求均适用);把 `uni.navigateTo/switchTab/reLaunch` 全量替换为 `preloadApi.navigateTo/switchTab/reLaunch`(见 §2.5) |
|
||||
| `utils/api.js` | `request()` 返回的 Promise 上挂 `.abort()` 方法(约 3 行改动,不影响现有调用方) |
|
||||
| 其余 `utils/*` | **不动** |
|
||||
|
||||
### 2.5 wrappedNavigateTo 替换策略(已确定方案 A)
|
||||
|
||||
**严格 wrap,预拉 fire-and-forget 不 await**:
|
||||
|
||||
- `preloadApi.navigateTo(opts)` 内部:解析 `opts.url` 的 query string → 按目标页路径查 `config.pages` 映射 → **如果命中映射**对每个匹配项调用 `core.run(key, params)`(**不 await**)→ 立即调用原生 `uni.navigateTo(opts)
|
||||
- **如果未命中 `config.pages` 映射**:直接透传调用原生 `uni.navigateTo(opts)`,不做任何预拉(预拉是 opt-in,不是 opt-out)
|
||||
- 用户点击 → 立刻跳转;预拉在后台并发进行;目标页 `onLoad` 用 `usePreload(key, params)` 时大概率命中缓存
|
||||
- **绝不**在 `navigateTo` 内 await 预拉结果,否则跳转延迟与设计目标矛盾
|
||||
- 不保留 `uni.navigateTo` 兼容入口:要么用 `preloadApi.navigateTo`(触发预拉),要么不要预拉;保留兼容会让 "哪些跳转走预拉" 变得不可见
|
||||
|
||||
**全量替换规则**:用 ESLint 规则或 PR review checklist 强制 —— 全仓 `uni.navigateTo` → `preloadApi.navigateTo`(可用 codemod 一次性替换)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置 schema
|
||||
|
||||
```js
|
||||
// frontend/config/preload.config.js
|
||||
import {
|
||||
getCastloveConfigApi,
|
||||
getUserProfileApi,
|
||||
getHotRankingApi,
|
||||
getAssetLikersApi,
|
||||
getActivityDetailApi,
|
||||
getActivityItemsApi,
|
||||
// ...按需 import
|
||||
} from '@/utils/api'
|
||||
|
||||
export const preloadConfig = {
|
||||
// ── 全局默认 ──
|
||||
defaults: {
|
||||
ttl: 5 * 60 * 1000, // 5 分钟
|
||||
persistence: 'memory', // 'memory' | 'file'
|
||||
concurrency: 4, // 单次批量最多并发数
|
||||
timeout: 10000, // 单接口超时(ms),超时后取消预拉
|
||||
silent: true, // 失败是否静默(true = 仅 warn,不抛)
|
||||
// ── 上限保护阈值(硬编码默认值,可在下面覆盖)──
|
||||
limits: {
|
||||
maxEntrySizeKB: 1024, // 单 key 数据超过此值只写内存,不写文件(1 MB 上限,防止单文件过大拖慢读取)
|
||||
maxMemoryEntries: 100, // 内存总条目上限,超出按 LRU 淘汰
|
||||
maxFileCacheMB: 50 // 文件缓存总容量上限(uni.storage 仅 4 MB 且与其他业务共享;本地 _doc/ 容量远超此值,50 MB 足够中等规模 API 响应缓存)
|
||||
}
|
||||
},
|
||||
|
||||
// ── 启动期预热清单(App.vue onLaunch 跑)──
|
||||
startup: [
|
||||
{ key: 'castlove.config', fetcher: getCastloveConfigApi,
|
||||
ttl: 60 * 60 * 1000, persistence: 'file' },
|
||||
{ key: 'me.profile', fetcher: getUserProfileApi,
|
||||
ttl: 10 * 60 * 1000 }
|
||||
],
|
||||
|
||||
// ── idle 预拉清单(首屏渲染完后跑)──
|
||||
idle: [
|
||||
{ key: 'ranking.hot', fetcher: () => getHotRankingApi('total', null, 1, 10),
|
||||
ttl: 10 * 60 * 1000 }
|
||||
],
|
||||
|
||||
// ── 页面切换预拉映射(wrappedNavigateTo 命中时触发)──
|
||||
pages: {
|
||||
'/pages/asset-detail/asset-detail': [
|
||||
{ key: 'asset.likers', fetcher: (params) => getAssetLikersApi(Number(params.id)) }
|
||||
],
|
||||
'/pages/activity-detail/activity-detail': [
|
||||
{ key: 'activity.detail', fetcher: (params) => getActivityDetailApi(params.id) },
|
||||
{ key: 'activity.items', fetcher: (params) => getActivityItemsApi(params.id) }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
- `key`:逻辑 key,业务引用缓存的唯一标识
|
||||
- `fetcher(params)`:返回 Promise 的请求函数
|
||||
- `ttl`:缓存有效期(ms)
|
||||
- `persistence`:`'memory'`(仅内存 Map)/ `'file'`(持久化到本地文件缓存 `_doc/preload/`,走 `plus.io`)
|
||||
- `concurrency`:单次批量内最大并发
|
||||
- `timeout`:单接口超时(ms)
|
||||
- `silent`:失败是否静默(`run` 时生效,`get` 始终抛错)
|
||||
- `limits.maxEntrySizeKB / maxMemoryEntries / maxFileCacheMB`:上限保护阈值(详见 §7),默认 1024 KB / 100 条 / 50 MB;可在 `defaults.limits` 覆盖
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心 API
|
||||
|
||||
### 4.1 命令式 API(utils/preloadApi/index.js 导出)
|
||||
|
||||
```js
|
||||
preloadApi.run(key, params?) // 触发一次预拉(写缓存):先查内存,未过期则跳过;过期/未命中才拉取。401/7/16 静默吞掉。不返回数据
|
||||
preloadApi.get(key, params?) // 读缓存:命中直接返回;未命中/过期则拉取;401/7/16 业务码静默吞掉(不抛给调用方)
|
||||
preloadApi.refresh(key, params?, force?) // 命令式刷新:force=true 跳过 TTL。非组件上下文使用;组件上下文用 usePreload.refresh()
|
||||
preloadApi.prefetchFor(targetPath, params?) // wrappedNavigateTo 内部调用
|
||||
preloadApi.invalidate(key?) // 失效单 key(仅内存)
|
||||
preloadApi.invalidatePrefix(prefix) // 失效某前缀(命中 logicalKey 前缀,仅内存)
|
||||
preloadApi.invalidateAll() // 清空全部内存缓存(不动文件缓存)
|
||||
preloadApi.clearForUser(userId) // 删除指定用户的文件缓存目录(`_doc/preload/{userId}/`)
|
||||
preloadApi.clearUser(userId) // 清空全部内存 + 删除指定用户的文件缓存目录(登出用)
|
||||
preloadApi.warmStartup() // App.vue onLaunch 调用
|
||||
preloadApi.warmIdle() // App.vue onShow 调用(每次回前台跑一次)
|
||||
preloadApi.navigateTo(opts) // 替代 uni.navigateTo(预拉 fire-and-forget)
|
||||
preloadApi.switchTab(opts) // 替代 uni.switchTab
|
||||
preloadApi.reLaunch(opts) // 替代 uni.reLaunch
|
||||
```
|
||||
|
||||
### 4.2 key 命名规则与物理 cache key
|
||||
|
||||
- 配置里写的 `key` 是"逻辑 key"
|
||||
- 真正缓存的物理 key:
|
||||
```
|
||||
${userId || 'guest'}::${namespace}::${logicalKey}::${hash(params)}
|
||||
```
|
||||
- 同 key 不同 params 是不同缓存条目
|
||||
- `namespace`:**硬编码为常量 `'preload'`**(与 §6.5 的文件目录名保持一致;预留扩展位,当前不在 schema 暴露)
|
||||
- `hash(params)`:**先对 params 的 key 排序**再 `JSON.stringify` → djb2 → 16 进制(key 排序保证 `{a:1,b:2}` 与 `{b:2,a:1}` 生成相同 hash,避免命中率下降)
|
||||
|
||||
### 4.3 读缓存流程(`get`)
|
||||
|
||||
```
|
||||
get(key, params)
|
||||
├─ 计算 cacheKey
|
||||
├─ 查内存 → 命中且未过期 → 同步 resolve(LRU touchLRU 标记访问)→ return
|
||||
├─ 查文件缓存(async I/O)→ 命中且未过期 → 写回内存 + resolve
|
||||
├─ 未命中 → 调 fetcher(params)
|
||||
│ ├─ 成功 → 写内存 + 异步写文件缓存(fire-and-forget)+ resolve
|
||||
│ ├─ "已登出"信号 → swallow 错误 + resolve(null)(见 _swallowAuth)
|
||||
│ └─ 其他错误 → reject(原错误)(业务方决定 toast)
|
||||
└─ 注:内存命中是唯一同步路径;文件缓存命中 / fetcher 路径均返回 async Promise,
|
||||
因此 composable §4.5 中只有内存命中能让首次渲染时 data 同步有值
|
||||
```
|
||||
|
||||
**"已登出"信号匹配规则**(见 §5 `_swallowAuth`):
|
||||
- `err.code === 7`(业务 401:token 失效)
|
||||
- `err.code === 16`(业务 403:账号被封)
|
||||
- `err.message` 含 "登录已过期"(匹配 `utils/api.js` 第 86-122 行 HTTP 401 reject 时构造的 message)
|
||||
|
||||
**关键约束**:`run` 与 `get` 都必须 swallow "已登出"信号。理由:`utils/api.js` 第 86-122 行在检测到这些码时**已经同步调用 `uni.reLaunch` 跳登录页**,相当于"已处理";预加载层再抛错会造成双重跳转或冷启动被中断。
|
||||
|
||||
### 4.3.1 `prefetchFor` 的入参约定
|
||||
|
||||
`preloadApi.prefetchFor(targetPath, params)`:
|
||||
- `targetPath`:目标页的完整路径,如 `/pages/asset-detail/asset-detail`
|
||||
- `params`:**只接受基本类型 key-value**(string / number / boolean),由 `navigate.js` 从跳转 URL 的 query string 解析得到
|
||||
- 解析规则:
|
||||
- 用 `URLSearchParams` 解析 query string
|
||||
- 每个 value 用 `decodeURIComponent` 解码
|
||||
- 所有 value 统一为 string(fetcher 内部按需 `Number()` / `Boolean()` 转换)
|
||||
- 示例:`uni.navigateTo({ url: '/pages/foo/bar?id=123&type=hot' })` → `{ id: '123', type: 'hot' }`
|
||||
- 数组 / 嵌套对象走 `?arr=1,2,3` 这种字符串协议;不在本次 spec 范围内
|
||||
- **URL 编码约束**:业务方拼 URL 时必须对 value 做 `encodeURIComponent`(特别是含 `&` / `=` / 中文 的 value)
|
||||
|
||||
调用链示例:
|
||||
```js
|
||||
navigateTo({ url: '/pages/asset-detail/asset-detail?id=123' })
|
||||
// → prefetchFor('/pages/asset-detail/asset-detail', { id: '123' })
|
||||
// → core.run('asset.likers', { id: '123' })
|
||||
// → fetcher({ id: '123' }) = getAssetLikersApi('123')
|
||||
```
|
||||
|
||||
### 4.4 并发去重
|
||||
|
||||
**适用范围**:`run` 与 `get` 共用同一套 inFlight 去重。同一个 cacheKey in-flight 时,第二个 `run` 或 `get` 共享同一个 Promise,**不重复发请求**。
|
||||
|
||||
**实现**:内部维护 `inFlight: Map<cacheKey, { promise, abort }>`。任何入口(run / get / prefetchFor / warmStartup / warmIdle)触发 fetch 时,先查 inFlight —— 命中则直接返回共享 Promise,未命中则创建新 Promise 并写入 inFlight。
|
||||
|
||||
**清理时机**:fetch **无论成功失败都在 finally 阶段清掉 inFlight**。否则失败后该 cacheKey 会永久 stuck,后续调用永远拿到同一个 reject 的 Promise。401 swallow 路径也是 finally 清掉。
|
||||
|
||||
**请求取消(abort)**:uniapp App 端 `uni.request` 返回的 `requestTask` 可调用 `.abort()`。当 composable 组件 unmount 或 `usePreload` 的 key/params 变化时,应 abort 前一个未完成的请求以避免浪费带宽。实现:
|
||||
- `inFlight` 每个条目存 `{ promise, abort: () => { promise.abort?.(); } }`
|
||||
- composable 在 `onBeforeUnmount` / watcher 中调 `abort()`
|
||||
- abort 后清掉 inFlight 条目(abort 不走 finally 路径,需显式清理)
|
||||
|
||||
**`request()` 改造**(唯一对 `utils/api.js` 的改动,约 3 行):
|
||||
```js
|
||||
// utils/api.js — request() 内部
|
||||
export function request(options) {
|
||||
const requestTask = uni.request({...}) // 已有
|
||||
let abortFn = () => requestTask.abort()
|
||||
const p = new Promise((resolve, reject) => {
|
||||
// ... 现有 success/fail 逻辑不变 ...
|
||||
})
|
||||
p.abort = abortFn // 新增这一行
|
||||
return p
|
||||
}
|
||||
```
|
||||
所有现有调用方 `await request(...)` 语法不变;preload 的 `core.js` 通过 `fetcher` 拿到这个 Promise 后,将其 `.abort` 存入 inFlight 条目。fetcher 无需改造 — fetcher 负责调用 `request()` 并返回其 Promise。当组件 unmount / params 变化时调用 `abort()`,`requestTask.abort()` 会终止 TCP 连接并释放带宽。
|
||||
|
||||
### 4.5 composable API(composables/usePreload.js)
|
||||
|
||||
```js
|
||||
// 在 <script setup> 中:
|
||||
const { data, loading, error, refresh } = usePreload('asset.detail', { id: 123 })
|
||||
```
|
||||
|
||||
返回:
|
||||
- `data: Ref<any | null>`:命中缓存为缓存值;未命中拉到为 fetcher 返回值;401 swallow 为 null;其他失败为上一次值或 null
|
||||
- `loading: Ref<boolean>`:true 表示当前有 in-flight 请求
|
||||
- `error: Ref<Error | null>`:401 swallow 时为 null;其他错误为 Error 对象
|
||||
- `refresh(force?: boolean)`:手动刷新(force=true 跳过 TTL 复用)
|
||||
|
||||
**初次渲染行为**(关键):
|
||||
- `core.get()` **命中内存缓存时**返回**已 resolved 的 Promise**。composable 在 `setup()` 同步阶段立即 `data.value = cached.data`,首次渲染时 `data` 就已有值、`loading` 为 false。这依赖 Vue 3 同步赋值而非下一 tick 更新。
|
||||
- `core.get()` **未命中 / 文件缓存命中 / 未命中**时返回**未 resolved 的 Promise**。composable 在 `setup()` 同步阶段 `data` 初始为 null、`loading` 为 true;Promise resolve 后 **用 `.then()` 异步更新 `data.value`**,触发下一次渲染。
|
||||
- 实现:`get` 内部命中内存路径直接 `return Promise.resolve(cached.data)`,未命中路径返回 `inFlightEntry.promise`
|
||||
- **硬约束**:composable 内 data.value 的赋值必须包裹在 `.then()` 中(不能 `await` 后在 setup 同步代码中赋值),否则未命中路径会阻塞整个 setup 导致白屏。这等同于**不能阻塞 setup 的同步执行**。
|
||||
|
||||
**典型用法**:
|
||||
```vue
|
||||
<script setup>
|
||||
const { data, loading, error, refresh } = usePreload('asset.detail', { id: route.params.id })
|
||||
// 模板中:v-if="data" / v-else-if="error" / v-else="loading"
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 错误处理
|
||||
|
||||
| 场景 | 行为 | 日志 |
|
||||
| --- | --- | --- |
|
||||
| 启动预热失败 | 静默(启动期不能阻塞) | `console.warn('[preload] startup fail:', key, err.message)` |
|
||||
| idle 预拉失败 | 静默 | 同上 |
|
||||
| 页面切换预拉失败 | 静默(用户可能不去目标页) | 同上 |
|
||||
| `usePreload.get()` 在组件内失败(非 401 类) | 抛错给调用方,组件 `error` ref 更新 | 无(业务自行决定 toast) |
|
||||
| **401 / 业务码 7 / 16("已登出"信号)** | **所有链路(run / get / prefetchFor)必须 swallow,resolve(null)** | `console.warn('[preload] auth-expired, swallowed:', key)` |
|
||||
| 网络断开 / 请求超时 | 抛错给调用方;**已存在的未过期缓存保持不变**(不会因为 fetch 失败而被清掉);下次 `get` 重新尝试 | `console.warn('[preload] network error:', key, err.message)` |
|
||||
|
||||
**核心原则:预拉失败绝不能阻塞主链路;401 / 业务码 7 / 16 必须 swallow(避免与 `request()` 的 reLaunch 行为冲突导致冷启动被中断或双重跳转)。**
|
||||
|
||||
**实现位置**:`core.js` 的 `run / get` 共享一个内部 helper `_swallowAuth(err)`:
|
||||
|
||||
```js
|
||||
function _swallowAuth(err) {
|
||||
if (err && (err.code === 7 || err.code === 16)) return null
|
||||
if (err && /登录已过期/.test(err.message || '')) return null
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 失效策略
|
||||
|
||||
### 6.1 自动失效
|
||||
|
||||
- **TTL 到期**(per-key 配置)
|
||||
- **用户登出** → `clearUser(oldUserId)`(清该用户的内存 + 删文件缓存目录)
|
||||
- **切换粉丝身份** → `invalidatePrefix('me.')`(仅内存)
|
||||
- **App.vue onShow** → `warmIdle()` 触发时**只对 TTL 到期的 key 自然过期**,不主动失效任何 cache
|
||||
|
||||
### 6.2 手动失效(业务调用)
|
||||
|
||||
- 点赞后:`invalidate('ranking.hot')`、`invalidatePrefix('asset.')`
|
||||
- 提交评论后:`invalidatePrefix('activity.messages')`
|
||||
- 进入个人页前可选主动失效 `me.*`
|
||||
|
||||
### 6.3 关键澄清:`invalidateAll()` vs `clearUser(userId)` vs `clearForUser(userId)`
|
||||
|
||||
| API | 内存 | 文件缓存 | 触发场景 |
|
||||
| --- | --- | --- | --- |
|
||||
| `invalidateAll()` | 清空全部 | **不动** | 极少使用;保留作为"内存全清"逃生口 |
|
||||
| `clearForUser(userId)` | 不动 | 仅删指定用户的 `_doc/preload/{userId}/` 目录 | 单独清理某用户文件缓存(如切换账号但保留另一账号会话的边缘场景) |
|
||||
| `clearUser(userId)` | 清空全部内存缓存(所有 userId 命名空间) + 删指定用户的文件缓存目录 | — | **登出专用** |
|
||||
|
||||
**核心约束**:
|
||||
1. `invalidateAll()` **永远不动文件缓存**。理由:避免把当前会话(登录态改变前)的 persistence 缓存白白清掉;文件清理必须显式按 userId 走。
|
||||
2. `clearUser(userId)` 的 `userId` **必须等于当前登录用户**。否则会清空当前用户的内存缓存(与文件删的目标用户不一致)。调用方需保证 userId 正确性;不在 API 层做防御。
|
||||
|
||||
### 6.4 数据一致性
|
||||
|
||||
```
|
||||
写:先写内存 → 再异步写文件缓存(fire-and-forget,不 await)
|
||||
读:先查内存 → 未命中查文件缓存 → 仍未命中才 fetch
|
||||
清:组合清空(如 clearUser)时,先清内存 → 再删文件目录;仅清内存或仅删文件的 API(invalidateAll / clearForUser)按各自定义执行
|
||||
```
|
||||
|
||||
文件写入失败仅 `console.warn`,不影响内存中的可用数据。文件读取失败(如文件被手动删除、权限异常)视为未命中,走 fetch 路径。
|
||||
|
||||
### 6.5 用户隔离
|
||||
|
||||
文件缓存目录结构:
|
||||
```
|
||||
_doc/preload/
|
||||
├── guest/ # 未登录用户
|
||||
│ ├── {hash1}.json
|
||||
│ └── {hash2}.json
|
||||
├── 1001/ # userId = 1001
|
||||
│ ├── {hash1}.json
|
||||
│ └── {hash2}.json
|
||||
└── 1002/ # userId = 1002
|
||||
└── {hash1}.json
|
||||
```
|
||||
|
||||
一个缓存条目 = 一个 JSON 文件,文件名 = `{hash(cacheKey)}.json`,内容 = `{ data, ts, ttl }`。
|
||||
|
||||
```js
|
||||
// storage.js — 文件缓存适配器
|
||||
const BASE_DIR = '_doc/preload'
|
||||
|
||||
// 获取某个用户缓存目录下的所有条目
|
||||
function listUserEntries(userId) {
|
||||
const dir = `${BASE_DIR}/${userId || 'guest'}`
|
||||
// plus.io.resolveLocalFileSystemURL 读取目录
|
||||
// 失败(目录不存在)视为空
|
||||
}
|
||||
|
||||
// 读:解析条目 JSON 文件
|
||||
async function readEntry(userId, cacheKey) {
|
||||
const path = `${BASE_DIR}/${userId || 'guest'}/${hash(cacheKey)}.json`
|
||||
// plus.io.FileReader 或 uni.getFileSystemManager().readFile
|
||||
try {
|
||||
const raw = await readFile(path, 'utf-8')
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return null // 文件不存在 / 损坏 → 视为未命中
|
||||
}
|
||||
}
|
||||
|
||||
// 写:异步写 JSON 文件(fire-and-forget 调用,因此必须内建 .catch)
|
||||
async function writeEntry(userId, cacheKey, data, ts, ttl) {
|
||||
const dir = `${BASE_DIR}/${userId || 'guest'}`
|
||||
const path = `${dir}/${hash(cacheKey)}.json`
|
||||
// 先确保目录存在(plus.io.File.createDirectory 或 mkdir)
|
||||
// 再写文件(plus.io.File.writeFile)
|
||||
// 注意:调用方不 await,因此必须在内部 .catch(e => console.warn(...))
|
||||
// 否则未捕获的文件写入异常会变成 unhandled promise rejection
|
||||
}
|
||||
|
||||
// 登出时:删除整个用户目录
|
||||
function clearForUser(userId) {
|
||||
const dir = `${BASE_DIR}/${userId || 'guest'}`
|
||||
// plus.io.resolveLocalFileSystemURL(dir, (entry) => { entry.removeRecursively(...) })
|
||||
// 或 uni.getFileSystemManager().rmdir(dir, { recursive: true })
|
||||
}
|
||||
```
|
||||
|
||||
**App.vue** 包装 `store/user` mutation,按 userId 变化分三种动作:
|
||||
|
||||
| 事件 | 动作 |
|
||||
| --- | --- |
|
||||
| `onLogin(userId)` | **不**主动清缓存。注意:**memory 总是按 userId 物理隔离**(物理 key 含 userId),新用户访问任何 key(包括 `ranking.hot` 这种"公共"数据)都需要重新拉取,**不会**自动复用前一会话或 guest 的内存条目。这是用户隔离的 by design 代价。 |
|
||||
| `onSwitchUser(oldId, newId)` | `invalidatePrefix('me.')` + `clearForUser(oldId)` |
|
||||
| `onLogout(userId)` | `clearUser(userId)` |
|
||||
|
||||
**实施注意**:
|
||||
1. **store/user.js 当前没有事件总线**(只有 action / mutation,没有 emit/on)。实施方式:**包装 mutation**,在 `SET_USER_INFO` mutation 里 patch 一下:如果旧用户存在(`oldUserId != null`),调用 `preloadApi.clearForUser(oldUserId)`;在 `CLEAR_AUTH` mutation 里 patch 一下(注入 `preloadApi.clearUser(userId)` 调用)。注意:**登录场景下 `oldUserId` 为 null/undefined,必须 guard 住**,否则会误删 `_doc/preload/undefined/`。
|
||||
2. **调用顺序约束**(重要):CLEAR_AUTH 时**先 `clearUser(userId)` 再清 token / user**。原因:token 清掉之后任何业务发起的请求都会 401;preload 缓存如果在 token 清之前清理完,业务不会有 401 + 缓存不一致的窗口。
|
||||
3. **CLEAR_AUTH 内部已经清了一堆 key**(`access_token / user / star_id` 等),与 `clearUser` 职责正交(preload 只在 `_doc/preload/` 下操作,不碰 uni.storage)。两者互不冲突,组合使用即可。
|
||||
4. **API 选型**:`#ifdef APP-PLUS` 主路径走 `plus.io`(项目已有先例,`useShare.js` 里用了 `plus.io.copyTo` 操作 `_doc/`);`#ifdef H5` / 小程序等降级用 `uni.getFileSystemManager`(仅当 persist=file 时走文件,memory 不变)。
|
||||
|
||||
### 6.6 与 `useBackgroundRefresh` 的协作
|
||||
|
||||
两个模块**完全独立**、可并存:
|
||||
- `useBackgroundRefresh(refreshFn)`:从后台切回时调 `refreshFn`(通常是重新拉当前页数据)
|
||||
- `preloadApi.invalidate(prefix)`:手动失效缓存
|
||||
|
||||
业务页面常用组合:
|
||||
```js
|
||||
useBackgroundRefresh(() => {
|
||||
// 从后台切回时,强制刷当前页 + 失效短期缓存
|
||||
preloadApi.invalidate('ranking.hot')
|
||||
preloadApi.invalidatePrefix('asset.')
|
||||
refresh(true) // usePreload 的 refresh
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 内存与文件缓存上限
|
||||
|
||||
内存泄漏防护 + 文件缓存容量控制(文件系统远比 uni.storage 大):
|
||||
|
||||
| 限制 | 值 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| 单 key 数据体积 | > 1024 KB | 只写内存,不写文件(即使 config 里设了 `persistence: 'file'` 也会降级,避免单 JSON 文件过大拖慢 I/O)。不报错、静默降级,下次冷启动走 fetch |
|
||||
| 总内存条目数 | > 100 条 | 按 LRU 淘汰 |
|
||||
| 总文件缓存容量 | > 50 MB | 按插入顺序淘汰最旧文件(FIFO) |
|
||||
|
||||
### 7.1 LRU 实现约束
|
||||
|
||||
**内存与文件缓存淘汰策略不同**:
|
||||
|
||||
| 层 | 淘汰策略 | 触发时机 |
|
||||
| --- | --- | --- |
|
||||
| 内存 | LRU(最近访问优先) | 命中时重排(delete + set) |
|
||||
| 文件缓存 | FIFO(最早写入优先) | 不在读时重排;淘汰时一次性 scan 目录,按文件 `stat.mtime` 排序找出最旧文件删除。文件数不大(≤ 几百),scan 开销可接受。也可维护一个轻量 `_index.json` 记录插入顺序,但 scan 方式更简单更健壮 |
|
||||
|
||||
JS `Map` 的迭代顺序是**插入顺序**而非**访问顺序**。内存 LRU 正确实现:
|
||||
|
||||
```js
|
||||
// 命中访问时:删除再插入,重排到最新
|
||||
function touchLRU(map, k, v) {
|
||||
if (map.has(k)) map.delete(k)
|
||||
map.set(k, v)
|
||||
if (map.size > MAX_ENTRIES) {
|
||||
// Map 迭代顺序 = 插入顺序,最久未访问的是第一个
|
||||
const oldestKey = map.keys().next().value
|
||||
map.delete(oldestKey)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 并发粒度
|
||||
|
||||
`defaults.concurrency`(默认 4)按 **scheduler 入口粒度**生效:
|
||||
|
||||
| 入口 | 并发限制 |
|
||||
| --- | --- |
|
||||
| `warmStartup()` | 整个 startup 数组最多 4 并发 |
|
||||
| `warmIdle()` | 整个 idle 数组最多 4 并发 |
|
||||
| `prefetchFor(targetPath)` | 单目标页内的多个 key 最多 4 并发 |
|
||||
|
||||
实现:`scheduler.js` 用一个简单的 semaphore(计数器 + 等待队列)控制并发。
|
||||
|
||||
### 7.3 idle 调度触发点与 App 端 fallback
|
||||
|
||||
`warmIdle()` 调用点:**`App.vue` 的 `onShow` 中**(每次从后台回前台都跑一次;首次冷启动 onLaunch 之后 onShow 也会触发,等价于"首屏渲染完后")。
|
||||
|
||||
**首次冷启动 onLaunch→onShow 连续触发**:onLaunch 里 `warmStartup` 跑 startup 清单,紧接着 onShow 里 `warmIdle` 跑 idle 清单。如果 idle 清单与 startup 清单有重叠 key,`run` 的"查内存 → 未过期则跳过"机制自动处理(run 看到 startup 已写入的未过期缓存直接返回,不重拉)。如果无重叠,两者并行进行,composable 在首次渲染时可能受益于 startup 的预热而 idle 还在后台跑。
|
||||
|
||||
**去重 / 防抖**:warmIdle 必须**幂等**——同一进程内多次调用,对已在 in-flight 的 key 直接跳过、不重发;对已在内存且未过期的 key 也跳过(靠 `run` 语义)。理由:用户高频切前后台会触发多次 onShow,warmIdle 不应每次都发起新请求。
|
||||
|
||||
App 端 fallback(`#ifdef APP-PLUS`):uniapp App 端**没有** `requestIdleCallback`,scheduler 内部统一用:
|
||||
```js
|
||||
const idle = typeof requestIdleCallback === 'function'
|
||||
? requestIdleCallback
|
||||
: (cb) => setTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 测试策略
|
||||
|
||||
### 8.1 层级与覆盖目标
|
||||
|
||||
| 层 | 范围 | 工具 |
|
||||
| --- | --- | --- |
|
||||
| 核心单元测试 | `core.js` 的 run/get/invalidate/并发去重/LRU/TTL | 手动 mock `uni.*` / `plus.io` |
|
||||
| 文件缓存适配器测试 | 目录创建 / 读写条目 / 清空逻辑 / 大小阈值 | 同上(mock `plus.io` File API) |
|
||||
| navigate 测试 | wrappedNavigateTo 命中/未命中均正确 | mock `uni.navigateTo` |
|
||||
| composable 测试 | `usePreload` 暴露的响应式 state 正确性 | mock core |
|
||||
| 集成冒烟 | App.vue 启动预热、wrappedNavigateTo 跳转 | HBuilderX 真机 |
|
||||
|
||||
### 8.3 调试与可观测性
|
||||
|
||||
**开发者工具**(`import.meta.env.DEV` 时启用):
|
||||
|
||||
```js
|
||||
// 实时查看缓存命中率、top-N hot keys
|
||||
window.__PRELOAD_DEBUG__ = {
|
||||
dumpMemory: () => Array.from(memoryMap.entries()).map(([k, v]) => ({
|
||||
key: k, age: Date.now() - v.ts, ttl: v.ttl, persistence: v.persistence
|
||||
})),
|
||||
stats: () => ({
|
||||
hits: hitCount, misses: missCount, memorySize: memoryMap.size, fileCacheBytes: totalFileCacheBytes
|
||||
}),
|
||||
// 一键 dump 所有 key + inFlight 当前几条
|
||||
all: () => ({ ... }, inFlight: Array.from(inFlightMap.keys()))
|
||||
}
|
||||
```
|
||||
|
||||
**日志规范**(生产环境统一前缀 `[preload]`):
|
||||
- 缓存命中:不记日志(高频、无意义)
|
||||
- 缓存过期 / 未命中:不记日志(正常路径)
|
||||
- fetch 完成:`console.log('[preload] fetch done:', key, elapsedMs)`
|
||||
- fetch 失败(swallow):`console.warn('[preload] fetch fail (swallowed):', key, err.message)`
|
||||
- fetch 失败(throw):不记日志(业务方自行处理)
|
||||
- 清理事件:不记日志(正常路径)
|
||||
- LRU 淘汰:`console.log('[preload] LRU evict:', oldestKey)`
|
||||
|
||||
### 8.2 验收清单
|
||||
|
||||
- [ ] `preloadApi.run` / `get` / `invalidate*` 单测通过
|
||||
- [ ] wrappedNavigateTo 命中/未命中均通过单测
|
||||
- [ ] usePreload composable 暴露响应式 state 通过单测
|
||||
- [ ] 401 / 业务码 7、16 被 `_swallowAuth` 正确吞掉(核心 + navigate + composable 各 1 个 case)
|
||||
- [ ] query string 含 `encodeURIComponent` 字符(如中文)解析正确
|
||||
- [ ] App.vue 真机:onLaunch 后立即访问 startup 项 key,命中内存缓存
|
||||
- [ ] wrappedNavigateTo 真机:列表页跳详情页,详情页 onLoad 时立即命中预拉缓存(loading 闪烁 < 50ms)
|
||||
- [ ] 登出后 `_doc/preload/{userId}/` 目录被删除
|
||||
- [ ] LRU 淘汰边界:
|
||||
- 塞 100 个 key:第 1 个**不**被淘汰
|
||||
- 塞 101 个 key:第 1 个被淘汰
|
||||
- [ ] warmIdle 幂等:同进程内连续调 3 次,fetcher 只被调用 1 次
|
||||
- [ ] 用户切换(onSwitchUser):me.* 前缀全失效、oldUser 的文件缓存目录已删除、newUser 缓存不受影响
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与缓解
|
||||
|
||||
| 风险 | 缓解 |
|
||||
| --- | --- |
|
||||
| 401 链路被 `request()` 同步 reLaunch 中断冷启动 | `core.run / get` 通过 `_swallowAuth` swallow:业务码 `code === 7 \|\| 16` **或** message 含 "登录已过期"(HTTP 401 走此路径,见 §5) |
|
||||
| 用户隔离在登出 / 切账号时混乱 | `clearUser / clearForUser / invalidateAll` 三层职责严格分离(见 §6.3) |
|
||||
| `wrappedNavigateTo` 全量替换回归风险 | 严格方案 A:预拉 fire-and-forget 不 await(见 §2.5) |
|
||||
| 预拉时机太早导致请求用户未登录 | 401 swallow(§5);启动清单里依赖登录态的 key 必须 `silent=true` |
|
||||
| 启动期批量预拉阻塞冷启动 | `startup` 全部 `silent=true`、并发受 `concurrency` 控制、超时 `timeout` |
|
||||
| 文件缓存体积膨胀 | 1024 KB / 100 条 / 50 MB 三层上限;登出 `clearUser` 删除目录 |
|
||||
| `usePreload` 在非 `setup()` 上下文误用 | composable 内部 `getCurrentInstance()` 检查 + 警告 |
|
||||
| 与 `avatarCache` / `screen-cache` 命名冲突 | 文件缓存统一放在 `_doc/preload/{userId}/` 目录下,与 uni.storage 隔离(见 §6.5) |
|
||||
| 与 `utils/api.js` 的 mock import 耦合(bundle 体积) | `preload.config.js` 的 fetcher 引用按需 import;必要时在 vite 分包配置里隔离 |
|
||||
| `JSON.stringify(params)` key 顺序敏感导致命中率下降 | 实现里对 params key 排序后再 stringify(见 §4.2 hash 约定) |
|
||||
|
||||
---
|
||||
|
||||
## 10. 实施清单(落到 writing-plans 阶段再细化)
|
||||
|
||||
0. `frontend/utils/api.js` — `request()` 返回的 Promise 上挂 `.abort()` 方法(约 3 行改动)
|
||||
1. `frontend/utils/preloadApi/storage.js`
|
||||
2. `frontend/utils/preloadApi/core.js`
|
||||
3. `frontend/utils/preloadApi/scheduler.js`
|
||||
4. `frontend/utils/preloadApi/navigate.js`
|
||||
5. `frontend/utils/preloadApi/index.js`
|
||||
6. `frontend/composables/usePreload.js`
|
||||
7. `frontend/config/preload.config.js`(初始内容,含 castlove.config / me.profile / ranking.hot / asset-detail / activity-detail)
|
||||
8. `frontend/App.vue`(注入启动 + 登出监听)
|
||||
9. `frontend/utils/preloadApi/__tests__/`(核心 + 文件缓存适配器 + navigate + composable)
|
||||
10. README 文档片段(放在 `frontend/utils/preloadApi/README.md`,供业务开发查阅 config schema + API 速查表)
|
||||
129
frontend/uni_modules/uni-upgrade-center-app/changelog.md
Normal file
129
frontend/uni_modules/uni-upgrade-center-app/changelog.md
Normal file
@ -0,0 +1,129 @@
|
||||
## 0.9.10(2026-04-27)
|
||||
- 修复 uni-app-x 项目编译时 warning
|
||||
## 0.9.9(2026-02-03)
|
||||
- 修复 安卓端非强制更新 kotlin 报错 `onClick has not been intialized`
|
||||
## 0.9.8(2026-01-05)
|
||||
- 更新 移除 vapor 模式不支持的 class 选择器
|
||||
## 0.9.7(2025-07-28)
|
||||
- 修复 使用腾讯云时,wgt 更新报错的Bug
|
||||
- 改进 uni-app-x 平台弹窗该用 script setup 实现
|
||||
## 0.9.6(2025-04-01)
|
||||
- 新增 升级中心适配鸿蒙 uni-app x **需要 HBuilderX 4.61+**
|
||||
## 0.9.5(2025-02-06)
|
||||
- 新增 完善下载失败时的处理逻辑
|
||||
## 0.9.4(2024-12-28)
|
||||
- 修复 腾讯云在使用扩展存储时报错的 Bug
|
||||
## 0.9.3(2024-12-23)
|
||||
- 修复 升级中心在大屏上的显示效果
|
||||
## 0.9.2(2024-11-06)
|
||||
- 更新 部分 ts 类型
|
||||
## 0.9.1(2024-11-01)
|
||||
- 更新 支持 HarmonyOS Next 设备整包更新、wgt 更新。需要 `HBuilderX 4.32+` [详情](https://doc.dcloud.net.cn/uniCloud/upgrade-center.html#uni-upgrade-center-app-harmonyos)
|
||||
## 0.9.0(2024-10-30)
|
||||
- **重要更新** 在 uni-app x 项目中弃用之前弹窗方案使用[dialogPage](https://doc.dcloud.net.cn/uni-app-x/api/dialog-page.html)实现,需要 `HBuilderX 4.31+`
|
||||
## 0.8.5(2024-10-26)
|
||||
- 优化 去除不必要代码
|
||||
## 0.8.4(2024-10-26)
|
||||
- 修复 uni-app x 项目升级到 4.31 alpha 后中间有空隙的Bug
|
||||
## 0.8.3(2024-07-31)
|
||||
- 修复 部分类型报错
|
||||
## 0.8.2(2024-07-15)
|
||||
- 更新 static 下的静态图片放入 static/app 目录下,防止编译除 app 平台以外的平台时带入
|
||||
## 0.8.1(2024-04-28)
|
||||
- 修复 在 HX 4.0.3+ uni-app x 项目运行到 Android 调不起安装的Bug
|
||||
## 0.8.0(2024-04-15)
|
||||
- 修复 更新弹窗 data 中新增初始化字段
|
||||
## 0.7.9(2024-03-15)
|
||||
- 移除无用代码
|
||||
- 调整 is_silently 类型为可为 null
|
||||
## 0.7.8(2024-01-04)
|
||||
- 新增 移除无用代码
|
||||
## 0.7.7(2024-01-04)
|
||||
- 新增 uni-app x 项目中新增 @show 回调
|
||||
## 0.7.6(2023-12-21)
|
||||
- 修复 iOS使用升级中心云打包时报错(使用新版的 [uts-progressNotification](https://ext.dcloud.net.cn/plugin?name=uts-progressNotification) 插件,如果之前下载过请删除 `uts-progressNotification\utssdk\app-ios` 文件夹)
|
||||
## 0.7.5(2023-12-12)
|
||||
- 新增 通知栏进度条使用 uts-progressNotification 插件
|
||||
- 新增 依赖 uni-installApk、uts-progressNotification。使用前要安装插件三方依赖
|
||||
## 0.7.4(2023-11-29)
|
||||
- 修复 uni-app-x 项目中由上版引发的无法升级的Bug
|
||||
## 0.7.3(2023-11-27)
|
||||
- 修复 在 uni-app x 中无更新时报错的Bug
|
||||
## 0.7.2(2023-11-20)
|
||||
- 新增 插件根目录 utils 文件夹中新增 check-update-nvue.js 文件(vue2 的 nvue 页面请引用该文件)
|
||||
## 0.7.1(2023-11-17)
|
||||
- 修复 运行至浏览器 ts 语法报错
|
||||
## 0.7.0(2023-11-10)
|
||||
- 新增 兼容 uni-app x 项目 [详情](https://uniapp.dcloud.net.cn/uniCloud/upgrade-center.html)
|
||||
## 0.6.5(2023-10-27)
|
||||
- 修复 安装 wgt 报错 manifest.json 文件不存在的Bug
|
||||
## 0.6.4(2023-09-01)
|
||||
chore: 优化代码结构
|
||||
## 0.6.3(2023-08-30)
|
||||
- 修复 下载 wgt 时如果后缀名不正确,重命名后安装
|
||||
## 0.6.2(2022-11-21)
|
||||
- 处理 cloudfunctions 目录
|
||||
## 0.6.1(2022-08-17)
|
||||
- 修复 后台添加应用市场,但都没有启用的情况下报错的Bug (需要 uni-admin 1.9.3+)
|
||||
## 0.6.0(2022-07-19)
|
||||
- 新增 支持多应用商店配置(需要 uni-admin 1.9.3+)
|
||||
## 0.4.1(2022-05-27)
|
||||
- 修复 上版引出的报错问题
|
||||
## 0.4.0(2022-05-27)
|
||||
- 新增 Android 支持跳转手机自带商店,填写升级包地址时请填写跳转商店链接
|
||||
- 新增 改为云对象调用方式,使用更直观
|
||||
## 0.3.3(2022-04-14)
|
||||
- 修复 调用 check-update,当 code 为 0 时没有回调
|
||||
## 0.3.2(2022-01-12)
|
||||
- 优化显示逻辑
|
||||
## 0.3.1(2021-11-24)
|
||||
- 修复 vue3 上图片不显示的Bug
|
||||
## 0.3.0(2021-11-18)
|
||||
- 移除 wgt 安装成功后提示,防止重启过快弹框不消失
|
||||
## 0.2.2(2021-08-25)
|
||||
- 兼容vue3.0
|
||||
## 0.2.1(2021-07-26)
|
||||
- 修复 使用腾讯云并手动填写地址时,导致下载链接失效的bug
|
||||
## 0.2.0(2021-07-13)
|
||||
- 更新文档 关于报错local_storage_key 为空,请不要将页面路径设置为pages.json中第一项
|
||||
## 0.1.9(2021-06-28)
|
||||
- 更新文档
|
||||
- 修复 wgt安装失败时,按钮状态不对
|
||||
## 0.1.8(2021-06-16)
|
||||
- 修复 跳转安装时,导致上次下载的apk还没安装就被删掉的bug
|
||||
## 0.1.7(2021-06-03)
|
||||
- 修改 移除static中的图片
|
||||
## 0.1.6(2021-06-03)
|
||||
- 修改 下载更新按钮使用CSS渐变色
|
||||
## 0.1.5(2021-04-22)
|
||||
- 更新check-update函数。现在返回一个Promise,有更新时成功回调,其他情况错误回调
|
||||
## 0.1.4(2021-04-13)
|
||||
- 更新文档。明确云函数调用结果
|
||||
## 0.1.3(2021-04-13)
|
||||
- 解耦云函数与弹框处理。utils中新增 call-check-version.js,可用于单独检测是否有更新
|
||||
## 0.1.2(2021-04-07)
|
||||
- 更新版本对比函数 compare
|
||||
## 0.1.1(2021-04-07)
|
||||
- 修复 腾讯云空间下载链接不能下载问题
|
||||
## 0.1.0(2021-04-07)
|
||||
- 新增使用uni.showModal提示升级示例
|
||||
- 修改iOS升级提示方式
|
||||
## 0.0.7(2021-04-02)
|
||||
- 修复在iOS上打开弹框报错
|
||||
## 0.0.6(2021-04-01)
|
||||
- 兼容旧版本安卓
|
||||
## 0.0.5(2021-04-01)
|
||||
- 修复低版本安卓上进度条错位
|
||||
## 0.0.4(2021-04-01)
|
||||
- 更新readme
|
||||
- 修复check-update语法错误
|
||||
## 0.0.3(2021-04-01)
|
||||
- 新增前台更新弹框,详见readme
|
||||
- 更新前台检查更新方法
|
||||
|
||||
## 0.0.2(2021-03-29)
|
||||
- 更新文档
|
||||
- 移除 dependencies
|
||||
|
||||
## 0.0.1(2021-03-25)
|
||||
- 升级中心前台检查更新
|
||||
122
frontend/uni_modules/uni-upgrade-center-app/package.json
Normal file
122
frontend/uni_modules/uni-upgrade-center-app/package.json
Normal file
@ -0,0 +1,122 @@
|
||||
{
|
||||
"id": "uni-upgrade-center-app",
|
||||
"displayName": "升级中心 uni-upgrade-center - App",
|
||||
"version": "0.9.10",
|
||||
"description": "uni升级中心 - 客户端检查更新",
|
||||
"keywords": [
|
||||
"uniCloud",
|
||||
"update",
|
||||
"升级",
|
||||
"wgt"
|
||||
],
|
||||
"repository": "https://gitee.com/dcloud/uni-upgrade-center/tree/master/uni_modules/uni-upgrade-center-app",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.31",
|
||||
"uni-app": "^4.35",
|
||||
"uni-app-x": "^4.65"
|
||||
},
|
||||
"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",
|
||||
"uts-openSchema"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "-",
|
||||
"chrome": "-"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": {
|
||||
"extVersion": "0.8.3",
|
||||
"minVersion": ""
|
||||
},
|
||||
"android": {
|
||||
"extVersion": "0.8.3",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": {
|
||||
"extVersion": "0.8.3",
|
||||
"minVersion": "12"
|
||||
},
|
||||
"harmony": {
|
||||
"extVersion": "0.9.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "x",
|
||||
"chrome": "x"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "0.9.0",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": {
|
||||
"extVersion": "0.9.0",
|
||||
"minVersion": "12"
|
||||
},
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "x"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,546 @@
|
||||
<template>
|
||||
<view class="mask flex-center">
|
||||
<view class="content">
|
||||
<view class="content-top">
|
||||
<text class="content-top-text">{{title}}</text>
|
||||
<image class="content-top-image" mode="widthFix"
|
||||
src="/uni_modules/uni-upgrade-center-app/static/app/bg_top.png"></image>
|
||||
</view>
|
||||
|
||||
<view class="content-space"></view>
|
||||
|
||||
<view class="content-body">
|
||||
<view class="content-body-title">
|
||||
<text class="text title content-body-title_title">{{subTitle}}</text>
|
||||
<text class="text version content-body-title_version">v{{version}}</text>
|
||||
</view>
|
||||
<view class="body">
|
||||
<scroll-view class="box-des-scroll">
|
||||
<text class="text box-des">
|
||||
{{contents}}
|
||||
</text>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="footer flex-center">
|
||||
<template v-if="isiOS || isHarmony">
|
||||
<button class="content-button" style="border: none;color: #fff;" type="primary" 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" :percent="downLoadPercent" activeColor="#3DA7FF" :show-info="true"
|
||||
:stroke-width="10" />
|
||||
<view style="width:100%;display: flex;justify-content: space-around;flex-direction: row;">
|
||||
<text class="text" style="font-size: 14px;">{{downLoadingText}}</text>
|
||||
<text class="text" style="font-size: 14px;">({{downloadedSize}}/{{packageFileSize}}M)</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button v-else class="content-button" @click="updateApp">
|
||||
{{downLoadBtnText}}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<button v-else-if="downloadSuccess && !installed" class="content-button" :loading="installing"
|
||||
:disabled="installing" @click="installPackage">
|
||||
{{installing ? '正在安装……' : '下载完成,立即安装'}}
|
||||
</button>
|
||||
|
||||
<button v-else-if="installed" class="content-button" @click="installPackage">
|
||||
安装未完成,点击安装
|
||||
</button>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content-bottom">
|
||||
<image v-if="!is_mandatory" class="close-img" mode="widthFix"
|
||||
src="/uni_modules/uni-upgrade-center-app/static/app/app_update_close.png" @click="closeUpdate">
|
||||
</image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { openSchema as utsOpenSchema } from '@/uni_modules/uts-openSchema'
|
||||
import { UniUpgradeCenterResult, StoreListItem } from '../../utils/call-check-version'
|
||||
import { platform_iOS, platform_Android, platform_Harmony } from '../../utils/utils'
|
||||
// #ifdef APP-ANDROID
|
||||
import { createNotificationProgress, cancelNotificationProgress, finishNotificationProgress, CreateNotificationProgressOptions, FinishNotificationProgressOptions } from '@/uni_modules/uts-progressNotification'
|
||||
// #endif
|
||||
|
||||
const requiredKey = ['version', 'url', 'type']
|
||||
let downloadTask : DownloadTask | null = null;
|
||||
let openSchemePromise : Promise<boolean> | null = null;
|
||||
|
||||
const openSchema = (url : string) : Promise<boolean> => new Promise<boolean>((resolve, reject) => {
|
||||
try {
|
||||
utsOpenSchema(url)
|
||||
resolve(true)
|
||||
} catch (e) {
|
||||
reject(false)
|
||||
}
|
||||
})
|
||||
|
||||
// 从之前下载安装
|
||||
const installForBeforeFilePath = ref<string>('')
|
||||
|
||||
// 安装
|
||||
const installed = ref<boolean>(false)
|
||||
const installing = ref<boolean>(false)
|
||||
|
||||
// 下载
|
||||
const downloadSuccess = ref<boolean>(false)
|
||||
const downloading = ref<boolean>(false)
|
||||
|
||||
const downLoadPercent = ref<number>(0)
|
||||
const downloadedSize = ref<number>(0)
|
||||
const packageFileSize = ref<number>(0)
|
||||
|
||||
// 要安装的本地包地址
|
||||
const tempFilePath = ref<string>('')
|
||||
|
||||
// 默认安装包信息
|
||||
const title = ref<string>('更新日志')
|
||||
const contents = ref<string>('')
|
||||
const version = ref<string>('')
|
||||
const is_mandatory = ref<boolean>(false)
|
||||
const url = ref<string>("")
|
||||
const platform = ref<string[]>([])
|
||||
const store_list = ref<StoreListItem[] | null>(null)
|
||||
|
||||
// 可自定义属性
|
||||
const subTitle = ref<string>('发现新版本')
|
||||
const downLoadBtnTextiOS = ref<string>('立即跳转更新')
|
||||
const downLoadBtnText = ref<string>('立即下载更新')
|
||||
const downLoadingText = ref<string>('安装包下载中,请稍后')
|
||||
|
||||
const isiOS = computed(() : boolean => platform.value.includes(platform_iOS))
|
||||
const isHarmony = computed(() : boolean => platform.value.includes(platform_Harmony))
|
||||
const isAndroid = computed(() : boolean => platform.value.includes(platform_Android))
|
||||
const needNotificationProgress = computed(() : boolean => isAndroid.value && !is_mandatory.value)
|
||||
|
||||
function getCurrentDialogPage() : UniPage | null {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0) {
|
||||
const dialogPages = pages[pages.length - 1].getDialogPages()
|
||||
if (dialogPages.length > 0) {
|
||||
return dialogPages[dialogPages.length - 1]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function closePopup() {
|
||||
downloadSuccess.value = false
|
||||
downloading.value = false
|
||||
downLoadPercent.value = 0
|
||||
downloadedSize.value = 0
|
||||
packageFileSize.value = 0
|
||||
tempFilePath.value = ''
|
||||
|
||||
installing.value = false
|
||||
installed.value = false
|
||||
|
||||
uni.closeDialogPage({
|
||||
dialogPage: getCurrentDialogPage(),
|
||||
fail(e) {
|
||||
console.log('e: ', e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function askAbortDownload() {
|
||||
uni.showModal({
|
||||
title: '是否取消下载?',
|
||||
cancelText: '否',
|
||||
confirmText: '是',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
if (downloadTask !== null) downloadTask!.abort()
|
||||
if (needNotificationProgress.value) {
|
||||
// #ifdef APP-ANDROID
|
||||
cancelNotificationProgress();
|
||||
// #endif
|
||||
}
|
||||
closePopup()
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeUpdate() {
|
||||
if (downloading.value && !needNotificationProgress.value) {
|
||||
askAbortDownload()
|
||||
return;
|
||||
}
|
||||
closePopup()
|
||||
}
|
||||
|
||||
function jumpToAppStore() {
|
||||
openSchema(url.value)
|
||||
}
|
||||
|
||||
function show(localPackageInfo : UniUpgradeCenterResult | null) {
|
||||
if (localPackageInfo === null) return;
|
||||
for (let key in localPackageInfo) {
|
||||
if (requiredKey.indexOf(key) != -1 && localPackageInfo[key] === null) {
|
||||
console.error(`参数 ${key} 必填,请检查后重试`)
|
||||
closePopup()
|
||||
return;
|
||||
}
|
||||
}
|
||||
title.value = localPackageInfo.title
|
||||
url.value = localPackageInfo.url
|
||||
contents.value = localPackageInfo.contents
|
||||
is_mandatory.value = localPackageInfo.is_mandatory
|
||||
platform.value = localPackageInfo.platform
|
||||
version.value = localPackageInfo.version
|
||||
store_list.value = localPackageInfo.store_list
|
||||
}
|
||||
|
||||
function checkStoreScheme() : Promise<boolean> | null {
|
||||
if (store_list.value !== null) {
|
||||
const storeList : StoreListItem[] = store_list.value!.filter((item : StoreListItem) : boolean => item.enable)
|
||||
if (storeList.length > 0) {
|
||||
if (openSchemePromise === null) {
|
||||
openSchemePromise = Promise.reject() as Promise<boolean>
|
||||
}
|
||||
storeList
|
||||
.sort((cur : StoreListItem, next : StoreListItem) : number => next.priority - cur.priority)
|
||||
.map((item : StoreListItem) : string => item.scheme)
|
||||
.reduce((promise : Promise<boolean>, cur : string) : Promise<boolean> => {
|
||||
openSchemePromise = promise.catch<boolean>(() : Promise<boolean> => openSchema(cur))
|
||||
return openSchemePromise!
|
||||
}, openSchemePromise!)
|
||||
return openSchemePromise!
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function installPackage() {
|
||||
installing.value = true;
|
||||
// #ifdef APP
|
||||
uni.installApk({
|
||||
filePath: tempFilePath.value,
|
||||
success: _ => {
|
||||
installing.value = false;
|
||||
installed.value = true;
|
||||
},
|
||||
fail: err => {
|
||||
console.error('installApk fail', err);
|
||||
// 安装失败需要重新下载安装包
|
||||
installing.value = false;
|
||||
installed.value = false;
|
||||
uni.showModal({
|
||||
title: '更新失败,请重新下载',
|
||||
content: `uni.installApk 错误码 ${err.errCode}`,
|
||||
showCancel: false
|
||||
});
|
||||
}
|
||||
});
|
||||
// 安装跳出覆盖安装,此处直接返回上一页
|
||||
if (!is_mandatory.value) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
function downloadFail() {
|
||||
const errMsg = '下载失败,请点击重试'
|
||||
downloadSuccess.value = false;
|
||||
downloading.value = false;
|
||||
downLoadPercent.value = 0;
|
||||
downloadedSize.value = 0;
|
||||
packageFileSize.value = 0;
|
||||
downLoadBtnText.value = errMsg
|
||||
downloadTask = null;
|
||||
if (needNotificationProgress.value) {
|
||||
// #ifdef APP-ANDROID
|
||||
finishNotificationProgress({
|
||||
title: '升级包下载失败',
|
||||
content: '请重新检查更新',
|
||||
onClick() { }
|
||||
} as FinishNotificationProgressOptions);
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
function downLoadComplete() {
|
||||
downloadSuccess.value = true;
|
||||
downloading.value = false;
|
||||
downLoadPercent.value = 0
|
||||
downloadedSize.value = 0
|
||||
packageFileSize.value = 0
|
||||
downloadTask = null;
|
||||
if (needNotificationProgress.value) {
|
||||
// #ifdef APP-ANDROID
|
||||
finishNotificationProgress({
|
||||
title: "安装升级包",
|
||||
content: "下载完成",
|
||||
onClick() { }
|
||||
} as FinishNotificationProgressOptions)
|
||||
installPackage();
|
||||
// #endif
|
||||
return
|
||||
}
|
||||
// 强制更新,直接安装
|
||||
if (is_mandatory.value) {
|
||||
installPackage();
|
||||
}
|
||||
}
|
||||
|
||||
function downloadPackage() {
|
||||
//下载包
|
||||
downloadTask = uni.downloadFile({
|
||||
url: url.value,
|
||||
success: res => {
|
||||
if (res.statusCode == 200) {
|
||||
tempFilePath.value = res.tempFilePath
|
||||
downLoadComplete()
|
||||
} else {
|
||||
console.log('downloadFile err: ', res);
|
||||
downloadFail()
|
||||
}
|
||||
},
|
||||
fail: err => {
|
||||
console.log('downloadFile err: ', err);
|
||||
downloadFail()
|
||||
}
|
||||
});
|
||||
if (downloadTask !== null) {
|
||||
downloading.value = true;
|
||||
if (needNotificationProgress.value) {
|
||||
closePopup()
|
||||
}
|
||||
downloadTask!.onProgressUpdate(res => {
|
||||
downLoadPercent.value = parseFloat(res.progress.toFixed(0));
|
||||
downloadedSize.value = parseFloat((res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2));
|
||||
packageFileSize.value = parseFloat((res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2));
|
||||
if (needNotificationProgress.value) {
|
||||
// #ifdef APP-ANDROID
|
||||
createNotificationProgress({
|
||||
title: "升级中心正在下载安装包……",
|
||||
content: `${downLoadPercent.value}%`,
|
||||
progress: downLoadPercent.value,
|
||||
onClick: () => {
|
||||
if (!downloadSuccess.value) {
|
||||
askAbortDownload()
|
||||
}
|
||||
}
|
||||
} as CreateNotificationProgressOptions)
|
||||
// #endif
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateApp() {
|
||||
const checkStoreSchemeResult = checkStoreScheme()
|
||||
if (checkStoreSchemeResult !== null) {
|
||||
checkStoreSchemeResult
|
||||
.then(_ => { })
|
||||
.catch(() => { downloadPackage() })
|
||||
.finally(() => {
|
||||
openSchemePromise = null
|
||||
})
|
||||
} else { downloadPackage() }
|
||||
}
|
||||
|
||||
onUnload(() => {
|
||||
if (needNotificationProgress.value) {
|
||||
// #ifdef APP-ANDROID
|
||||
cancelNotificationProgress()
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
|
||||
onLoad((onLoadOptions : OnLoadOptions) => {
|
||||
const local_storage_key : string | null = onLoadOptions['local_storage_key']
|
||||
if (local_storage_key == null) {
|
||||
console.error('local_storage_key为空,请检查后重试')
|
||||
closePopup()
|
||||
return;
|
||||
};
|
||||
const localPackageInfo = uni.getStorageSync(local_storage_key);
|
||||
if (localPackageInfo == null) {
|
||||
console.error('安装包信息为空,请检查后重试')
|
||||
closePopup()
|
||||
return;
|
||||
};
|
||||
show(JSON.parse<UniUpgradeCenterResult>(JSON.stringify(localPackageInfo)) as UniUpgradeCenterResult)
|
||||
})
|
||||
|
||||
onBackPress((options : OnBackPressOptions) : boolean | null => {
|
||||
if (is_mandatory.value) return true
|
||||
if (!needNotificationProgress.value) {
|
||||
if (downloadTask !== null) {
|
||||
downloadTask!.abort()
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.flex-center {
|
||||
/* #ifndef APP-NVUE | UNI-APP-X */
|
||||
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);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
top: 0;
|
||||
width: 600rpx;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-family: Source Han Sans CN;
|
||||
}
|
||||
|
||||
.content-top {
|
||||
width: 100%;
|
||||
border-bottom-color: #fff;
|
||||
border-bottom-width: 15px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
.content-space {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.content-top-image {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
bottom: -10%;
|
||||
}
|
||||
|
||||
.content-top-text {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #F8F8FA;
|
||||
position: absolute;
|
||||
width: 65%;
|
||||
top: 50%;
|
||||
left: 25px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
box-sizing: border-box;
|
||||
padding: 0 25px;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
|
||||
.content-body-title {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content-body-title_version {
|
||||
padding-left: 10px;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
margin-left: 5px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 10px;
|
||||
background: #50aefd;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #3DA7FF;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 75px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.box-des-scroll {
|
||||
box-sizing: border-box;
|
||||
padding: 0 15px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.box-des {
|
||||
font-size: 13px;
|
||||
color: #000000;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.progress-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 90%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.content-bottom {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.close-img {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
z-index: 1000;
|
||||
position: relative;
|
||||
bottom: -30%;
|
||||
left: 50%;
|
||||
margin-left: -17px;
|
||||
}
|
||||
|
||||
.content-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
|
||||
text-align: center;
|
||||
|
||||
background-color: #1785ff;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@ -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/uni-upgrade-center-app/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/uni-upgrade-center-app/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>
|
||||
1
frontend/uni_modules/uni-upgrade-center-app/readme.md
Normal file
1
frontend/uni_modules/uni-upgrade-center-app/readme.md
Normal file
@ -0,0 +1 @@
|
||||
文档已移至 [uni-upgrade-center](https://uniapp.dcloud.net.cn/uniCloud/upgrade-center.html)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@ -0,0 +1 @@
|
||||
{}
|
||||
@ -0,0 +1,120 @@
|
||||
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 版本
|
||||
}
|
||||
|
||||
export default function () : Promise<UniUpgradeCenterResult> {
|
||||
// #ifdef APP
|
||||
return new Promise<UniUpgradeCenterResult>((resolve, reject) => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
const appId = systemInfo.appId
|
||||
const appVersion = systemInfo.appVersion //systemInfo.appVersion
|
||||
// #ifndef UNI-APP-X
|
||||
if (typeof appId === 'string' && typeof appVersion === 'string' && appId.length > 0 && appVersion.length > 0) {
|
||||
plus.runtime.getProperty(appId, function (widgetInfo) {
|
||||
if (widgetInfo.version) {
|
||||
let data = {
|
||||
action: 'checkVersion',
|
||||
appid: appId,
|
||||
appVersion: appVersion,
|
||||
wgtVersion: widgetInfo.version
|
||||
}
|
||||
uniCloud.callFunction({
|
||||
name: 'uni-upgrade-center',
|
||||
data,
|
||||
success: (e) => {
|
||||
resolve(e.result as UniUpgradeCenterResult)
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
reject('widgetInfo.version is EMPTY')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
reject('plus.runtime.appid is EMPTY')
|
||||
}
|
||||
// #endif
|
||||
// #ifdef UNI-APP-X
|
||||
if (typeof appId === 'string' && typeof appVersion === 'string' && appId.length > 0 && appVersion.length > 0) {
|
||||
let data = {
|
||||
action: 'checkVersion',
|
||||
appid: appId,
|
||||
appVersion: appVersion,
|
||||
is_uniapp_x: true,
|
||||
wgtVersion: '0.0.0.0.0.1'
|
||||
}
|
||||
try {
|
||||
uniCloud.callFunction({
|
||||
name: 'uni-upgrade-center',
|
||||
data: data
|
||||
}).then(res => {
|
||||
const code = res.result['code']
|
||||
const codeIsNumber = ['Int', 'Long', 'number'].includes(typeof code)
|
||||
if (codeIsNumber) {
|
||||
if ((code as number) == 0) {
|
||||
reject({
|
||||
code: res.result['code'],
|
||||
message: res.result['message']
|
||||
})
|
||||
} else if ((code as number) < 0) {
|
||||
reject({
|
||||
code: res.result['code'],
|
||||
message: res.result['message']
|
||||
})
|
||||
} else {
|
||||
const result = JSON.parse<UniUpgradeCenterResult>(JSON.stringify(res.result)) as UniUpgradeCenterResult
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
}).catch<void>((err : any | null) => {
|
||||
const error = err as UniCloudError
|
||||
if (error.errMsg == '未匹配到云函数[uni-upgrade-center]')
|
||||
error.errMsg = '【uni-upgrade-center-app】未配置uni-upgrade-center,无法升级。参考: https://uniapp.dcloud.net.cn/uniCloud/upgrade-center.html'
|
||||
reject(error.errMsg)
|
||||
})
|
||||
} catch (e) {
|
||||
reject(e.message)
|
||||
}
|
||||
} else {
|
||||
reject('invalid appid or appVersion')
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
return new Promise((resolve, reject) => {
|
||||
reject({
|
||||
message: '请在App中使用'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
@ -0,0 +1,184 @@
|
||||
function callCheckVersion() {
|
||||
// #ifdef APP-PLUS
|
||||
return new Promise((resolve, reject) => {
|
||||
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
|
||||
let data = {
|
||||
action: 'checkVersion',
|
||||
appid: plus.runtime.appid,
|
||||
appVersion: plus.runtime.version,
|
||||
wgtVersion: widgetInfo.version
|
||||
}
|
||||
uniCloud.callFunction({
|
||||
name: 'uni-upgrade-center',
|
||||
data,
|
||||
success: (e) => {
|
||||
resolve(e)
|
||||
},
|
||||
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() {
|
||||
// #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
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,228 @@
|
||||
import callCheckVersion, { UniUpgradeCenterResult } from "./call-check-version"
|
||||
import { platform_iOS } from './utils'
|
||||
// #ifdef UNI-APP-X
|
||||
import { openSchema } from '@/uni_modules/uts-openSchema'
|
||||
// #endif
|
||||
|
||||
// 推荐再App.vue中使用
|
||||
const PACKAGE_INFO_KEY = '__package_info__'
|
||||
// #ifdef APP-HARMONY
|
||||
export default function (component?: any) : Promise<UniUpgradeCenterResult> {
|
||||
// #endif
|
||||
// #ifndef APP-HARMONY
|
||||
export default function () : Promise<UniUpgradeCenterResult> {
|
||||
// #endif
|
||||
return new Promise<UniUpgradeCenterResult>((resolve, reject) => {
|
||||
callCheckVersion().then(async (uniUpgradeCenterResult) => {
|
||||
// NOTE uni-app x 3.96 解构有问题
|
||||
const code = uniUpgradeCenterResult.code
|
||||
const message = uniUpgradeCenterResult.message
|
||||
const url = uniUpgradeCenterResult.url // 安装包下载地址
|
||||
// 此处逻辑仅为示例,可自行编写
|
||||
if (code > 0) {
|
||||
// 腾讯云获取下载链接
|
||||
if (/^cloud:\/\//.test(url)) {
|
||||
const tcbRes = await uniCloud.getTempFileURL({ fileList: [url] });
|
||||
if (typeof tcbRes.fileList[0].tempFileURL !== 'undefined') uniUpgradeCenterResult.url = tcbRes.fileList[0].tempFileURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提示升级一
|
||||
* 使用 uni.showModal
|
||||
*/
|
||||
// return updateUseModal(uniUpgradeCenterResult)
|
||||
|
||||
// #ifndef UNI-APP-X
|
||||
// 静默更新,只有wgt有
|
||||
if (uniUpgradeCenterResult.is_silently) {
|
||||
uni.downloadFile({
|
||||
url: uniUpgradeCenterResult.url,
|
||||
success: res => {
|
||||
if (res.statusCode == 200) {
|
||||
// 下载好直接安装,下次启动生效
|
||||
plus.runtime.install(res.tempFilePath, {
|
||||
force: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 提示升级二
|
||||
* 官方适配的升级弹窗,可自行替换资源适配UI风格
|
||||
*/
|
||||
// #ifndef UNI-APP-X
|
||||
// #ifdef APP-PLUS
|
||||
uni.setStorageSync(PACKAGE_INFO_KEY, uniUpgradeCenterResult)
|
||||
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)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
if (component) {
|
||||
component.show(true, uniUpgradeCenterResult)
|
||||
} else {
|
||||
reject({
|
||||
code: -1,
|
||||
message: '在 HarmonyOS Next 平台请传递组件使用'
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
// #endif
|
||||
// #ifdef UNI-APP-X
|
||||
uni.setStorageSync(PACKAGE_INFO_KEY, uniUpgradeCenterResult)
|
||||
uni.openDialogPage({
|
||||
url: `/uni_modules/uni-upgrade-center-app/pages/uni-app-x/upgrade-popup?local_storage_key=${PACKAGE_INFO_KEY}`,
|
||||
disableEscBack: true,
|
||||
fail: (err) => {
|
||||
console.error('更新弹框跳转失败', err)
|
||||
uni.removeStorageSync(PACKAGE_INFO_KEY)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
return resolve(uniUpgradeCenterResult)
|
||||
} else if (code < 0) {
|
||||
console.error(message)
|
||||
return reject(uniUpgradeCenterResult)
|
||||
}
|
||||
return resolve(uniUpgradeCenterResult)
|
||||
}).catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 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;
|
||||
|
||||
// #ifndef UNI-APP-X
|
||||
let confirmText = isiOS ? '立即跳转更新' : '立即下载更新'
|
||||
// #endif
|
||||
// #ifdef UNI-APP-X
|
||||
let confirmText = '立即下载更新'
|
||||
// #endif
|
||||
|
||||
uni.showModal({
|
||||
title,
|
||||
content: contents,
|
||||
showCancel: !is_mandatory,
|
||||
confirmText,
|
||||
success: res => {
|
||||
if (res.cancel) return;
|
||||
|
||||
if (isiOS) {
|
||||
// iOS 平台跳转 AppStore
|
||||
// #ifndef UNI-APP-X
|
||||
plus.runtime.openURL(url);
|
||||
// #endif
|
||||
// #ifdef UNI-APP-X
|
||||
openSchema(url)
|
||||
// #endif
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '后台下载中……',
|
||||
duration: 1000
|
||||
});
|
||||
|
||||
// wgt 和 安卓下载更新
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success: res => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.error('下载安装包失败');
|
||||
return;
|
||||
}
|
||||
// 下载好直接安装,下次启动生效
|
||||
// uni-app x 项目没有 plus5+ 故使用条件编译
|
||||
// #ifndef UNI-APP-X
|
||||
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
|
||||
|
||||
// #ifdef UNI-APP-X
|
||||
uni.installApk({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
uni.showModal({
|
||||
title: '安装成功请手动重启'
|
||||
});
|
||||
},
|
||||
fail: err => {
|
||||
uni.showModal({
|
||||
title: '更新失败',
|
||||
content: err.errMsg,
|
||||
showCancel: false
|
||||
});
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
46
frontend/uni_modules/uni-upgrade-center-app/utils/utils.ts
Normal file
46
frontend/uni_modules/uni-upgrade-center-app/utils/utils.ts
Normal file
@ -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;
|
||||
}
|
||||
13
frontend/uni_modules/uts-openSchema/changelog.md
Normal file
13
frontend/uni_modules/uts-openSchema/changelog.md
Normal file
@ -0,0 +1,13 @@
|
||||
## 1.1.3(2025-09-12)
|
||||
- 鸿蒙平台 新增 打开 url 错误时输出
|
||||
- iOS平台 修复 语法报黄问题
|
||||
## 1.1.2(2025-03-20)
|
||||
- 更新 支持鸿蒙
|
||||
## 1.1.1(2024-12-16)
|
||||
- 修复 canOpenURL 在安卓端可能会报类型错误的问题
|
||||
## 1.1.0(2024-12-06)
|
||||
- 新增 canOpenURL UTS API,可用此API判断url是否可以跳转
|
||||
## 1.0.1(2024-11-13)
|
||||
- 修复 Android 打开部分 schema 时没有跳转到目标应用的 Bug
|
||||
## 1.0.0(2024-04-25)
|
||||
- 更新 在 Android 和 iOS 上打开链接的 UTS API
|
||||
124
frontend/uni_modules/uts-openSchema/package.json
Normal file
124
frontend/uni_modules/uts-openSchema/package.json
Normal file
@ -0,0 +1,124 @@
|
||||
{
|
||||
"id": "uts-openSchema",
|
||||
"displayName": "uts-openSchema",
|
||||
"version": "1.1.3",
|
||||
"description": "在 Android、iOS、HarmonyOS 上打开链接的 UTS API",
|
||||
"keywords": [
|
||||
"uts-openSchema"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.0",
|
||||
"uni-app": "^4.75",
|
||||
"uni-app-x": "^4.75"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "uts",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": {
|
||||
"extVersion": "1.0.0",
|
||||
"minVersion": ""
|
||||
},
|
||||
"vue3": {
|
||||
"extVersion": "1.0.0",
|
||||
"minVersion": ""
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"safari": "x",
|
||||
"chrome": "x"
|
||||
},
|
||||
"app": {
|
||||
"vue": {
|
||||
"extVersion": "1.0.0",
|
||||
"minVersion": ""
|
||||
},
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "1.0.0",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": {
|
||||
"extVersion": "1.0.0",
|
||||
"minVersion": "12"
|
||||
},
|
||||
"harmony": {
|
||||
"extVersion": "1.1.2",
|
||||
"minVersion": "5.0.0"
|
||||
}
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "x",
|
||||
"alipay": "x",
|
||||
"toutiao": "x",
|
||||
"baidu": "x",
|
||||
"kuaishou": "x",
|
||||
"jd": "x",
|
||||
"harmony": "x",
|
||||
"qq": "x",
|
||||
"lark": "x"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "x",
|
||||
"union": "x"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "x",
|
||||
"chrome": "x"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "1.0.0",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": {
|
||||
"extVersion": "1.0.0",
|
||||
"minVersion": "12"
|
||||
},
|
||||
"harmony": {
|
||||
"extVersion": "1.1.2",
|
||||
"minVersion": "5.0.0"
|
||||
}
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "x"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
frontend/uni_modules/uts-openSchema/readme.md
Normal file
59
frontend/uni_modules/uts-openSchema/readme.md
Normal file
@ -0,0 +1,59 @@
|
||||
# uts-openSchema
|
||||
|
||||
打开链接,支持:
|
||||
|
||||
1. 打开外部 App
|
||||
2. 使用浏览器打开链接
|
||||
3. 打开地图到指定地点
|
||||
4. ...
|
||||
|
||||
## 使用
|
||||
|
||||
1. 安装此插件
|
||||
2. 在要使用的地方 `import` 导入
|
||||
```ts
|
||||
import { openSchema, canOpenURL } from '@/uni_modules/uts-openSchema'
|
||||
```
|
||||
3. 直接调用 `openSchema` 方法:
|
||||
```ts
|
||||
// #ifdef UNI-APP-X
|
||||
// 使用外部浏览器打开指定URL
|
||||
openSchema('https://uniapp.dcloud.io/uni-app-x')
|
||||
|
||||
// #ifdef APP-ANDROID
|
||||
// Android 使用应用商店打开指定App
|
||||
openSchema('market://details?id=com.tencent.mm')
|
||||
|
||||
// Android 打开地图坐标
|
||||
// 可以先用canOpenURL判断是否安装了地图软件
|
||||
if (canOpenURL('androidamap://')) {
|
||||
openSchema('androidamap://viewMap?sourceApplication=Hello%20uni-app&poiname=DCloud&lat=39.9631018208&lon=116.3406135236&dev=0')
|
||||
} else {
|
||||
console.log('未安装高德地图')
|
||||
}
|
||||
// #endif -->
|
||||
|
||||
// #ifdef APP-IOS
|
||||
// 打开 AppStore 到搜索页
|
||||
openSchema('itms-apps://search.itunes.apple.com//WebObjects//MZSearch.woa/wa/search?media=software<erm=')
|
||||
|
||||
// 打开 iOS 地图坐标
|
||||
openSchema('http://maps.apple.com/?q=Mexican+Restaurant&sll=50.894967,4.341626&z=10&t=s')
|
||||
// #endif -->
|
||||
|
||||
// #endif -->
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
- openSchema(url: string) // `url`:要打开的链接 `必填` `不为空字符串`
|
||||
|
||||
## 相关开发文档
|
||||
|
||||
[UTS 语法](https://uniapp.dcloud.net.cn/tutorial/syntax-uts.html)
|
||||
|
||||
[UTS API插件](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html)
|
||||
|
||||
[UTS 组件插件](https://uniapp.dcloud.net.cn/plugin/uts-component.html)
|
||||
|
||||
[Hello UTS](https://gitcode.net/dcloud/hello-uts)
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"minSdkVersion": "21"
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import Intent from 'android.content.Intent'
|
||||
import Uri from 'android.net.Uri'
|
||||
import { OpenSchema, CanOpenURL } from '../interface.uts'
|
||||
|
||||
export const openSchema : OpenSchema = function (url : string) {
|
||||
if (canOpenURL(url)) {
|
||||
const context = UTSAndroid.getUniActivity()!
|
||||
const uri = Uri.parse(url)
|
||||
const intent = new Intent(Intent.ACTION_VIEW, uri)
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.setData(uri)
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
console.error('[uts-openSchema] url param Error:', JSON.stringify(url))
|
||||
}
|
||||
}
|
||||
|
||||
export const canOpenURL : CanOpenURL = function (url : string) : boolean {
|
||||
if (typeof url === 'string' && url.length > 0) {
|
||||
const context = UTSAndroid.getUniActivity()!
|
||||
const uri = Uri.parse(url)
|
||||
const intent = new Intent(Intent.ACTION_VIEW, uri)
|
||||
return intent.resolveActivity(context.packageManager) != null ? true : false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { bundleManager, common } from '@kit.AbilityKit';
|
||||
import OpenLinkOptions from '@ohos.app.ability.OpenLinkOptions'
|
||||
import { getAbilityContext } from '@dcloudio/uni-runtime'
|
||||
import { OpenSchema, CanOpenURL } from '../interface.uts'
|
||||
|
||||
|
||||
export const openSchema : OpenSchema = function (url : string) : void {
|
||||
(getAbilityContext() as common.UIAbilityContext)?.openLink(url, {
|
||||
appLinkingOnly: false
|
||||
} as OpenLinkOptions)
|
||||
}
|
||||
|
||||
|
||||
export const canOpenURL : CanOpenURL = function (url : string) : boolean {
|
||||
try {
|
||||
return bundleManager.canOpenLink(url)
|
||||
} catch (error) {
|
||||
console.error('[uts-openSchema] url param Error:', JSON.stringify(url))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"deploymentTarget": "12.0"
|
||||
}
|
||||
22
frontend/uni_modules/uts-openSchema/utssdk/app-ios/index.uts
Normal file
22
frontend/uni_modules/uts-openSchema/utssdk/app-ios/index.uts
Normal file
@ -0,0 +1,22 @@
|
||||
import { UIApplication } from 'UIKit'
|
||||
import { URL } from 'Foundation'
|
||||
import { OpenSchema, CanOpenURL } from '../interface.uts'
|
||||
|
||||
export const openSchema : OpenSchema = function (url : string) : void {
|
||||
if (canOpenURL(url)) {
|
||||
let uri = new URL(string = url)
|
||||
UIApplication.shared.open(uri!)
|
||||
} else {
|
||||
console.error('[uts-openSchema] url param Error: ', url)
|
||||
}
|
||||
}
|
||||
|
||||
export const canOpenURL : CanOpenURL = function (url : string) : boolean {
|
||||
if (typeof url == 'string' && url.length > 0) {
|
||||
let uri = new URL(string = url)
|
||||
if (uri != null && UIApplication.shared.canOpenURL(uri!)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
2
frontend/uni_modules/uts-openSchema/utssdk/interface.uts
Normal file
2
frontend/uni_modules/uts-openSchema/utssdk/interface.uts
Normal file
@ -0,0 +1,2 @@
|
||||
export type OpenSchema = (url : string) => void
|
||||
export type CanOpenURL = (url : string) => boolean
|
||||
12
frontend/uni_modules/uts-openSchema/utssdk/web/index.uts
Normal file
12
frontend/uni_modules/uts-openSchema/utssdk/web/index.uts
Normal file
@ -0,0 +1,12 @@
|
||||
import { OpenSchema, CanOpenURL } from '../interface.uts'
|
||||
|
||||
export const openSchema : OpenSchema = function (url : string) : void {
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
export const canOpenURL : CanOpenURL = function (url : string) : boolean {
|
||||
if (url != "") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
28
frontend/uni_modules/uts-progressNotification/changelog.md
Normal file
28
frontend/uni_modules/uts-progressNotification/changelog.md
Normal file
@ -0,0 +1,28 @@
|
||||
## 1.1.2(2025-02-10)
|
||||
修复某些情况通过点击通知消息无法拉起App的bug
|
||||
## 1.1.1(2024-09-03)
|
||||
去除TypeScript警告
|
||||
## 1.1.0(2024-03-08)
|
||||
修复uniapp打包报错问题
|
||||
## 1.0.9(2024-02-29)
|
||||
去除代码过时警告
|
||||
## 1.0.8(2023-12-21)
|
||||
去除app-ios目录
|
||||
## 1.0.7(2023-12-11)
|
||||
去除无用代码
|
||||
## 1.0.6(2023-12-11)
|
||||
修改文档
|
||||
## 1.0.5(2023-12-11)
|
||||
1.修改插件名称
|
||||
2.修改插件引入方式为import导入
|
||||
## 1.0.4(2023-11-30)
|
||||
1. createNotificationProgress增加`onClick`回调
|
||||
2.修复在小米部分系统上,通知消息会归类于不重要通知的bug
|
||||
## 1.0.3(2023-11-28)
|
||||
更新截图
|
||||
## 1.0.2(2023-11-28)
|
||||
修改资源的包名
|
||||
## 1.0.1(2023-11-28)
|
||||
更新文档
|
||||
## 1.0.0(2023-11-28)
|
||||
Android通知栏显示进度插件
|
||||
85
frontend/uni_modules/uts-progressNotification/package.json
Normal file
85
frontend/uni_modules/uts-progressNotification/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
frontend/uni_modules/uts-progressNotification/readme.md
Normal file
71
frontend/uni_modules/uts-progressNotification/readme.md
Normal file
@ -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
|
||||
Loading…
Reference in New Issue
Block a user