From ce1ef491f8044a059978eb92c94e8517e312ac0a Mon Sep 17 00:00:00 2001 From: zheng020 Date: Tue, 7 Jul 2026 14:43:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0uniapp=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../specs/2026-07-02-preload-api-design.md | 593 +++++++++++++++ .../uni-upgrade-center-app/changelog.md | 129 ++++ .../uni-upgrade-center-app/package.json | 122 ++++ .../pages/uni-app-x/upgrade-popup.uvue | 546 ++++++++++++++ .../pages/upgrade-popup.vue | 679 ++++++++++++++++++ .../uni-upgrade-center-app/readme.md | 1 + .../static/app/app_update_close.png | Bin 0 -> 7644 bytes .../static/app/bg_top.png | Bin 0 -> 30486 bytes .../uniCloud/database/db_init.json | 1 + .../utils/call-check-version.ts | 120 ++++ .../utils/check-update-nvue.js | 184 +++++ .../utils/check-update.ts | 228 ++++++ .../uni-upgrade-center-app/utils/utils.ts | 46 ++ .../uni_modules/uts-openSchema/changelog.md | 13 + .../uni_modules/uts-openSchema/package.json | 124 ++++ frontend/uni_modules/uts-openSchema/readme.md | 59 ++ .../utssdk/app-android/config.json | 3 + .../utssdk/app-android/index.uts | 27 + .../utssdk/app-harmony/index.uts | 21 + .../uts-openSchema/utssdk/app-ios/config.json | 3 + .../uts-openSchema/utssdk/app-ios/index.uts | 22 + .../uts-openSchema/utssdk/interface.uts | 2 + .../uts-openSchema/utssdk/web/index.uts | 12 + .../uts-progressNotification/changelog.md | 28 + .../uts-progressNotification/package.json | 85 +++ .../uts-progressNotification/readme.md | 71 ++ .../utssdk/app-android/AndroidManifest.xml | 11 + .../app-android/TransparentActivity.uts | 62 ++ .../utssdk/app-android/callbacks.uts | 19 + .../utssdk/app-android/config.json | 3 + .../utssdk/app-android/constant.uts | 2 + .../utssdk/app-android/index.uts | 156 ++++ .../values/notification_progress_styles.xml | 11 + .../utssdk/interface.uts | 46 ++ .../utssdk/unierror.uts | 0 35 files changed, 3429 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-02-preload-api-design.md create mode 100644 frontend/uni_modules/uni-upgrade-center-app/changelog.md create mode 100644 frontend/uni_modules/uni-upgrade-center-app/package.json create mode 100644 frontend/uni_modules/uni-upgrade-center-app/pages/uni-app-x/upgrade-popup.uvue create mode 100644 frontend/uni_modules/uni-upgrade-center-app/pages/upgrade-popup.vue create mode 100644 frontend/uni_modules/uni-upgrade-center-app/readme.md create mode 100644 frontend/uni_modules/uni-upgrade-center-app/static/app/app_update_close.png create mode 100644 frontend/uni_modules/uni-upgrade-center-app/static/app/bg_top.png create mode 100644 frontend/uni_modules/uni-upgrade-center-app/uniCloud/database/db_init.json create mode 100644 frontend/uni_modules/uni-upgrade-center-app/utils/call-check-version.ts create mode 100644 frontend/uni_modules/uni-upgrade-center-app/utils/check-update-nvue.js create mode 100644 frontend/uni_modules/uni-upgrade-center-app/utils/check-update.ts create mode 100644 frontend/uni_modules/uni-upgrade-center-app/utils/utils.ts create mode 100644 frontend/uni_modules/uts-openSchema/changelog.md create mode 100644 frontend/uni_modules/uts-openSchema/package.json create mode 100644 frontend/uni_modules/uts-openSchema/readme.md create mode 100644 frontend/uni_modules/uts-openSchema/utssdk/app-android/config.json create mode 100644 frontend/uni_modules/uts-openSchema/utssdk/app-android/index.uts create mode 100644 frontend/uni_modules/uts-openSchema/utssdk/app-harmony/index.uts create mode 100644 frontend/uni_modules/uts-openSchema/utssdk/app-ios/config.json create mode 100644 frontend/uni_modules/uts-openSchema/utssdk/app-ios/index.uts create mode 100644 frontend/uni_modules/uts-openSchema/utssdk/interface.uts create mode 100644 frontend/uni_modules/uts-openSchema/utssdk/web/index.uts create mode 100644 frontend/uni_modules/uts-progressNotification/changelog.md create mode 100644 frontend/uni_modules/uts-progressNotification/package.json create mode 100644 frontend/uni_modules/uts-progressNotification/readme.md create mode 100644 frontend/uni_modules/uts-progressNotification/utssdk/app-android/AndroidManifest.xml create mode 100644 frontend/uni_modules/uts-progressNotification/utssdk/app-android/TransparentActivity.uts create mode 100644 frontend/uni_modules/uts-progressNotification/utssdk/app-android/callbacks.uts create mode 100644 frontend/uni_modules/uts-progressNotification/utssdk/app-android/config.json create mode 100644 frontend/uni_modules/uts-progressNotification/utssdk/app-android/constant.uts create mode 100644 frontend/uni_modules/uts-progressNotification/utssdk/app-android/index.uts create mode 100644 frontend/uni_modules/uts-progressNotification/utssdk/app-android/res/values/notification_progress_styles.xml create mode 100644 frontend/uni_modules/uts-progressNotification/utssdk/interface.uts create mode 100644 frontend/uni_modules/uts-progressNotification/utssdk/unierror.uts diff --git a/docs/superpowers/specs/2026-07-02-preload-api-design.md b/docs/superpowers/specs/2026-07-02-preload-api-design.md new file mode 100644 index 0000000..281048e --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-preload-api-design.md @@ -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`。任何入口(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 +// 在 +``` + +--- + +## 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 速查表) \ No newline at end of file diff --git a/frontend/uni_modules/uni-upgrade-center-app/changelog.md b/frontend/uni_modules/uni-upgrade-center-app/changelog.md new file mode 100644 index 0000000..f2ecf5c --- /dev/null +++ b/frontend/uni_modules/uni-upgrade-center-app/changelog.md @@ -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) +- 升级中心前台检查更新 diff --git a/frontend/uni_modules/uni-upgrade-center-app/package.json b/frontend/uni_modules/uni-upgrade-center-app/package.json new file mode 100644 index 0000000..56f2d7e --- /dev/null +++ b/frontend/uni_modules/uni-upgrade-center-app/package.json @@ -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" + } + } + } + } + } +} \ No newline at end of file diff --git a/frontend/uni_modules/uni-upgrade-center-app/pages/uni-app-x/upgrade-popup.uvue b/frontend/uni_modules/uni-upgrade-center-app/pages/uni-app-x/upgrade-popup.uvue new file mode 100644 index 0000000..b7e9b38 --- /dev/null +++ b/frontend/uni_modules/uni-upgrade-center-app/pages/uni-app-x/upgrade-popup.uvue @@ -0,0 +1,546 @@ + + + + + diff --git a/frontend/uni_modules/uni-upgrade-center-app/pages/upgrade-popup.vue b/frontend/uni_modules/uni-upgrade-center-app/pages/upgrade-popup.vue new file mode 100644 index 0000000..e9769d6 --- /dev/null +++ b/frontend/uni_modules/uni-upgrade-center-app/pages/upgrade-popup.vue @@ -0,0 +1,679 @@ + + + + + diff --git a/frontend/uni_modules/uni-upgrade-center-app/readme.md b/frontend/uni_modules/uni-upgrade-center-app/readme.md new file mode 100644 index 0000000..53ff781 --- /dev/null +++ b/frontend/uni_modules/uni-upgrade-center-app/readme.md @@ -0,0 +1 @@ +文档已移至 [uni-upgrade-center](https://uniapp.dcloud.net.cn/uniCloud/upgrade-center.html) \ No newline at end of file diff --git a/frontend/uni_modules/uni-upgrade-center-app/static/app/app_update_close.png b/frontend/uni_modules/uni-upgrade-center-app/static/app/app_update_close.png new file mode 100644 index 0000000000000000000000000000000000000000..8b2ffe62cba2466f184ea9f8ee4f9395ed8cf37a GIT binary patch literal 7644 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 U2>e_sb7~9VO{|RT486$z2cgKqd;kCd literal 0 HcmV?d00001 diff --git a/frontend/uni_modules/uni-upgrade-center-app/static/app/bg_top.png b/frontend/uni_modules/uni-upgrade-center-app/static/app/bg_top.png new file mode 100644 index 0000000000000000000000000000000000000000..015f698cdd8c8caa63486a2bd87f5a96f17df630 GIT binary patch literal 30486 zcmYgXby!sE*QHxJRYC-YZlneT0cD68B&9<_l$0DL1wle;kY+#xM!Fm68d{K$PLYNI zq~SZ<-}OG+uQYg<~W|3n|h4X?`}#pi)Xkmbe(oI7-IZ?uL>&)Z5%w%{{$^ zgZYE`{a<(To3fgnn|9U*d;8C9tXE3d+3(9OFx+n;62^(d0{%J_#W8CeIR8ebHuVjJ z-Q_LvzaIm?RXAo>FuhJzwJ%q|FL~SfVK0*?{br^&eOBlX! z71rj}^Y@NyIQtUwpKyN%hoNZ@Af)C$$K2cx#+FHwlRd=6Mr&w=)9KWJOyc%I{QYgf zBL%A5Y_C~SI*^7&rq%0}m*vm*pa1VX;Ml-HgQox8=e80X%XBsoVqI=74w|5mQRXv5 z5M8mFC(^o7z;yF!H%ECYw>(Z$8wtoV&ivt<)T6Bzms6{k&%7&lEH47@Wwvtq=VLdY z=BCBwbq%NCP6zXh(1KvOY?w#R<)1!^z+ZBn%{=&P=fBUW;l<%}&>6OQUAm=ALqQrR$wjc&*2&;1?T$Lf|2-J?<4ib_^*v~~ z>iRc@O+=VfGLbIKC?g3h0eex$_1|aoh-xW8{9pKCTs5ZP5(K;Y{;{_Le29vh3syw! z=9Ku?i!aD=>|LR*Ug>DxwadBG`3F~Zu=(1D)n5d|z(*H@vRtqMDwE>(|9u=EM-Y2y zUkY^TzdJ|h?}}CoWq~hCIKQ1{@;^@WuZG>%dVmRT-qc4S(dQj2d30HWj-r2q>Er!{ zM`=sN^{S;O49%FT`$ElIBmSn?&SOKY(^sux(KCl@Q7d_zk%gL^_(?rXkjYmMGFMb8jda9-!GJDCr z^0mVJNZP}U{S8iq??koKpwS%}OW&z7uRm^^pvUHUGEJpqdxvQh>Z=&sZfC|OtqQ@s zN_^FLcg&Abh*&b1P5?8*^4GsH%-9=)_4V0F(Q*w3ri9KZXYFzN2zFRc{LZ&meXAvP zzx!El7s6aIKPqmnwPCPmiu?QM+|)pnjR?S&4q&VoV>Y0>ZXKGw$ev@`re7kz><0-s zV5Fo6HR#vkpCq+kgiynB$JVsi)d2_u6edxVi1$c4|ryDQ;kObua8Q>PSPC%_+qnfr1 z&Qn`=_sU9-<12xJ&@*z#OTTKIQV)5TY2xP8-67u-W@ow8gN$zw&H)Yeq&&jtO{a!p zk9i>VxO#l({@zau59ii)q3lYG(%J6Y-v;Ka<`UjUQ|}(?U{B`8f*Q-Vr!y{l>Ygwk zq8xnlS#7ysDj(!)TK@vYFqO+aXxL8u(dNYQ?DbyB!z#<|l!C)_k6fpup3!jg*{j>o z6#KgpTrhXbUwr>ru#qZrr?8-p|Edp!gdbQA{q!--ARMpq^JlW?el*~S@^OZ_-b5Z; zU1nnF?^)v5*d(r^Tx~P2cX5Rto(3<=OKQE7PrTxYMUj98tT8L>3ML_cef{Ddj-l81 zd+ZhO$=w2>hQs-gvu{)r=c?>5?@Qk_@e1wadpbl%Wl=;fSC z`z;-3|Fwo)6A7)9>_h8a>XzcpH#N-TWl|6Qry;4*_PJ3Y1Ss6y)$lJ|usy^x!Rer( zu{87iX;X0ZE5f|V(Z_n>#Y=``e(WtM8*D-8D51n>am&kXq@Pls3#LoFjQKAw4yar> zp!u?60*l*%f{oqG)pW4lbKdjy`qY^_a}~(@lP!lozy#0l&XsL{v16?| z)kzaaplMX9Yx)1i(69!ciRX9@%b&NK+g@p&t?;$0J_X{lJL{7b2lD-fJvvoJvsY6F zY5xNk9iksG&J#q|5gC+`;l^d1roO zhL;9%a%=5vuj^Nd)gPWtesT6)=LwxB!#aa|Zp_GXKsUswbBg7Jlh8U}sJkb`{vxJ3 z-q+B}$5v@j@P0=t>(vS}Ba-}&W3HsBelNa#8;`AisqL^bN}B_~=zmRxN*fb_$EN1Z-sbmsrqN_guQ{L(lQpJI?MLFw;-f0cahTxE_!H0t5S| zZ1PgsniA|!H2#wK;(t_=aXq-P7}$=%aj(ZI^+iIF!uG$$oU^dco^zWsIGC0K@ zJWUPIi)bLV&s*BB?W`P5F2D?%-)sU*N!>BQLFc;BTm$`X;5WV_dzb%)&Y>W{fgn_z zIx_EicIxfCcHq}t=qhK2UxPBEE?Xvtmh;+s5z9^ur75rLE?OXRN&mCG!+*B-p?u+O zH{0Iqci~0uKX;FH^Wag9xx5^HKoO~Y|B}52Qx8pC(fQ8Y>(k9Q{5KV1rX!Zm_FrbG zh-cz8&S^s-G`s&LBkeLv40`?ApO_%LWj9Znl|Hcof$BUxorcP}Y&A1RIY4@)iV35; zrH!d-W?b$7wLc_=?_Z8-frp19*uf+5_GI4Y>R_nhY-7UT>+8@R0g};fTi<7oQxM-V zc=Ec}tG|TOE_Woa2Jc77?*1_JVt)IJ8G*{mMoj-3xW7H|mUxKw{5H#j`*wejHwY7K znC`xrn0i&xyg2`a@SL2Rn%hHM9e0H|_jvnyCV9yCZP9)B#z`f<1~NQe%PHc2V3TP~ zB__dy~W6|x~~DA&p5B3XkM#qK0`!r zxpHVRrvny7M7X;${mlwCCTxbK_>JP}Ca+()M>Nk%1m8TR|Jf;`XdARb{P}0qtF`XI z&DiW@m1r86{NIZ*Qz>tXk9zRw@6qYGH$A^TPW(!bYr#9yz_+;b%ljn`+$c}Kruiza z0@~o;M?cem;#dEjHF_;vuXe4;)Npu$s!pWff;m1^o>BZa%O0o%sHM4YB2IiJd>{I+ z^MJtDyXyGa9Kx2hcElFP+HO5G9AyhNF$3v_^>14i#9c^bHvPx{Im1E+! zu%g+{aC?M4^N0so&j!s*0#{|Exkdk1YIE6cn8x$8IX9rD?Vir>UPU*OpL=}kL#_Ed z(~7Fx$|F&B>%IO{@wWb zNuDvkk@`66u&0vsS!4>Oa4}WM#zX`7CIj@W~W*c zeDBu}7%+HZrvE^ltg7(wzZ^Gn_FqmB6?(~X+NdP|tLlZF>D`4on}DK&(-_eF@!G?K zvg7nW>kqFMcAPX)&?|YfD8A$9f1h-Ws21--ceHWXh5wqrgY;%U19E>%p5n~NS1kYX z(k3810=S98b1#M)s_{cmyZ`dv1bguW&*TFFt;awRGyfXkgWpyp&ET6B8ImJV$qyaG z#Jc1nu%!nDrZ-hnM;0P_{BI|aIZFxP;t#vEcE6J5t24Bh+4eT8Rns5k#CCy7 z=eI(nW|JSio_%}3M*=zRc6J`mQ3E$)S>NnndC!TfpzOg3d zVo?wb_F;k@Jzm{7WSjf@4Cvob)xW=>mbN4)d(>RxcHM8TSn*8e2GyZrr1|NdDzT^> zI@Y<10cK&HKiI@QZXmkBlR5_F`NahZQzgY9*hY*b2MpVi8c!{jlLK~Q=!g0+I*HYiC}>Rar1(|p z-K4zDqvvAsg}>K+N*p`LKA$#+yAU+}AzhI|vO(V|K&W%%N!w_t`8W)@GXrCcdUQrE zi?Lehv33*unh(d`79S!V2x5Ch9XF=ZYnxsRDjeP3`<8fz7~zMH0HIu8^&)8Bljo@7 zAV6F&526(-h%h7X*W2L&F#XH66&?aAoTc;E@bNH2<%9JO2qllbYd*m^7Y7WNGoab~ zr;*fk%ssxVZ7nPaO&sWmw+)DpZPUUwR-add{P*37$3Z2*E2Fr-b@X{S%feMT*?#Bp z#i-62VJ=}aZ&zEGKSsVcb|V(hFfo^AZMnw6N?V>G=7K@BMBs74boYcXkSTi$-J0_L zG|wuI9=}*8K#x%|dG|gUue7eQisTix=kyqbi2{2g8wTVuz$2N?4o&$4sofS|3SKRd zv%Bkeexk>b{KCdkBQngXAq!1Y?KKMf)5;7J#2e>S$#mb;$%EDJ4>M=?;*Fy$SZtok zKdsCW*o3lPZ0(3SfqMwjAN z!*>5toXio?f#^Xyt}(KZH(3C7TUj-(Ei{y^I*)#h<9V934~@>br71?}4&{ zh0C>QL|vIj=4<|dz$HfEITFm_7cSC+`;qb%SRdJqQwZ13VJ+r&6uYs}K>>soV5VS~Jc61W{aHHu0S`zf4ys z4``5Sm}5=b!P&R zU=jH1lpYNv4&LiB-3LKnNE^qqpczAyHuVQD0nWhiw-}#ia#(u<6HWmizcAarZgcTs z;x~H|Lc#&-F}+@MoVL7UD+N+=qCm z=_YM_PPz?Xt^0QfxG(A)dj$)zz_K7}f#8y?SHMlK2`}03B@$*heABykUf8}0OZHmwR1f;}6 z*-4DGK2P3~TFh!c1%(E(Fv5LSn6itrFs;5d*OVXTy2KG@MzBuM;neVKsjtPuphEQ? z`fR21)#9foWX7&e6F7|gO>fJNR`ME`+EYj3Y(w%$toNk6wZb_EBA;atz23?-Z+v^6 zpThv+oYZjIdE@UgQ!+z>B_u+OeG~VzHIBx8>I4N?H~)$66hT~!B3*%JO6`U)b*S{~ z=n+KriQ~PrGmVt`wvAt~_!0H-sV=LE1_@6@>rw@sl7mj5ldQ*=Jc!s)i)*rm4&Fyl z-A)P=i3MhL5<2%4^50W~u~;u#lrsC9pK@V&yXB+9hYc^oR7ytkNb(J5^F{~;hKXs$ zrYfrxGzeu9qGvufh6uod@sS_I5FniDd~WRLH6SHGdcs(n2D}}?#CslwOF5}4 z0%^Hk-*EVw>SQQd>26H70c|||cU+yrqkZ|e*V{HM+$@-_4?D6&^Wa+D}?BsB8-X!P~+(bRaP@Muc-5L`Jz88Ik zIxqg+-2ZiutGJ+X^4Mp_)XrA~ZiP`UQ=}jmVOeyg(JSCdGkDyd z0P5`_w6TAs_iDy7H{D}jY%_QjGPFxOYj}LBj#_x1Dl5hrXs?x#evCa-)ovgg3wl1H zG2@5=4d}u%DzlT{$AM_y&NuJP@ewo`ql6|JOMFm-F4cV+FE=}f4PQNGz{S{vkQRD8 zBMq+S`O|8nYt2+ZhX8n)pzNGF_lsc6;%ry3Ykt;;&f&cKWg=f%cfeO3W7_AopBRF+ zMBwZis(DLdQ4w=7Kp_rfk!GBHFO`>_$oq?@x?%@+ExKK=yC_ReHzqr4vQ0ICCABq8 zfBOkC5+b&oN76_gzk=PyYj114W{Ork`J&FTANCY1ezDNhcRmyrg9_W^TRUs=58ZST zgUu{N~UIjF3ar|H? ztN|}=+Q>|l-$4VJ1g_iY)oLU<;Dq9(6Mt5^bIX`_IvL!V{?tD7@@4DN8zd?sLy|1x z&H7Xn3__VW1T;3L5!>p>jX2xsBq~tT=IiX;h|>N z`GRwEPC@2|x%uHW10;5UpTYa#@d!(Lg|G4#!T1w5IKxd+Mp1lUr{V*rq|FIuj(ONa z={faZo$Uc-ItZ0#)M+x*cf@XQ4KB~`S#k5R&1yed{2@bh%Ua*b=+58(K(&~NWQ>sx zG9?dOmS~L9+DhKF`-$AI{jd{T28{*&NtH`PM!^qFvb?Cx^E+wFK?qL;hF(q$Ps#7L z7e;zfyVx^S@($@V4gozsubyJ0RA;mUv8h*QENb;--mb#Emo&Qq*PqSb-Emx)Fub2u zh48f5x{c!f+B=Xpa`yhCk0CU|Qh6v&wsQq-`%3bWTF1|QY!I)tG@29vs_=7V(a!fn zD+DXk3^M1-X>@y(-^85YN4R!zeSiv67~(Y=A0kZW8iy8X4gFXEKG~q#wA3MF`LkK> z`A}rJy}T=FDi*8~XPY3LfdHqd7^THXtwY4Vzh~eYpbcChr%}iVck0|+JZh{7Q=c#~ zXc|43!Rgc!NRC&0nh?w%0A#4^6=xfyrLJg|v%T$?)RScJn4D|fSGZ#!s3+;GA>XbK z4+2!981^wotcmA-w! zYV)j-+P`=m%V3!2QU{5k@zdIoVH&lT-C_{Xll>+SD*6*pwO~=9RcQSf6qPi2(`N=bjYC*@n-0I^pFLuMjuxZ}LvyoUmr| z2QL~a>9TR%+SbJ%E8NkwcMvR{7JMqr0eC4Ht>b6iV5$CYL^6K$9)miLF_K-$qGeKM zwGh9a`xk_CifV;e0QycD;+3u;%NAw$RCcp6$bY|~FPbTk!c-Xh3+oYOh{6XNpT~(sap&NpXL2G^yWycTEa{tqS3`=xHD)9TT(3lE6%jjpPx!vwGY0 zjG}NgQ^KDZ>wwU+(w`3H0{qZ?ZC0^|gtD8BlY=rIlO-gS6V#SVmd*ah#t7lz)qdij zj*K)F6PZwOSa$OK9Qm}c(5LXX;V9k@IRaQz7CVnKQ>@{<&e#EBNdPgm!mM2JuP^9J z3Jao2vfkfSy={yX?-3Win@K8|ZB)%%92V`))%U6)&Jlh+c#^yD$>_;Qv?a9$e`Zfy zrBr7TbU)oAPQ#HA`(%7mvMoh%#I%<0!vkKPrF7wD9l}^p@a{ZIa?sEm3SeBTZ^Ml; zgku_AFuHJ(mWMsL3MpA+WQZP!d!A-UH|O7703emMJ*R(~;B9zSgUeRYo#D2tw88M? zQ*;a(;qlWc$)V;JJgz3+bC6g&$A%Kd2IbsaY-s+5O>Uw&b>F75%9M2LSDPcvk|i~Q06y<5ay1XX13XAC2m^^*5l$M8rU z1?*@*EFPVP8M5l@awq9$G9Q3D&xPV_<-vEl&dM({DIzKfal)HEWa!5S=_0SMk09rG)AK9Fd2I+S+> zak>ih_C$U`2;OgUsKw?*n@V9PvdCF=el;eEFPMURGTXx2L?|I*&a>EkJs)khbY{+I z0SKU=)~80MiHlYEg&6{+#zsU56!U~HqmtgD4}lI(;QKka$JPQ+DjJ)>fy0|-AdDw! zWzK^GF3R#J;+Pwam@-6}coNGfGZh+9}{4_WL=EZ#m# z_i(kGda&Dm5q?pPD*&5xixofkWBN!5pAfsxD;Akql2ENszy0z_wd%SkcpabtnE11k zjKZWB(UoCGQrsIoM(W(GbI)#yND}xvGT>9xXOpoWtKY*=+tu8oK8XI^XJ5)#)_Ns^ zDECtup~AnpG4ZNt95gXuLY@2k>^l&oA<7VmY_`N|@?PgC+-Pg}8_lI2Q3nr^Kcy-m z=Y&ljZSGFqqS)r`y9)!jNi6NC{wUi6oAm7T)P1D}&KJsp=0>)oxwQ9}Qr zI10uC+MWDO3ru#Lty3b<6PbEiPT?dgk9MBL8#dn{zHX>OZ(COas2W_?ONv0zez_9} z=7QrE>OCVB4|K3A`vE%SD{EjkuC0^`leG5;zx&6ykBbHG{lo|zo9kJh`gm(yD48MJ zx`y7{c93w!apKkrarbZ8=)qRI-H%)Yk=X(Y7!FhK>K0cnnD&OTRfugLLh10Ora+j; zYptznm8m?Ee;Q;h+diGEgkFA(Go^09g)yl+7F^pEn&hbfg7UtV%H^Oemi7oS6)2%G zLJB*!R;?8V&dXa?8pC6cnuJRbQgNI_9&u8hzno;pd^J)|Z3XOY(}Aql(~9sV!Re=) zF3}|C0$LMo7Prw_5=}l}=Z3{L3a&=?jqKrLAvBQej&`Qcj4`F_FX1D&9jnqFfwF_{ zlg@i`JB6fHNSvD4JJ4tN6G+*)qtMITTjDI}a?)t)b#_ef@jD>lTeFib)o_3_GiKl@ zX8OA;A&(YyU>{oF0*Dtlh`mfg2Aq`^alk2MlovMosqpBA#98J= zzHsQ4&5~P|S{`Fsflz;mk?QP0^s)&qr-D!Y23v#nk^x&=IN$Vds?!WUN#^8!JM?`t zbEI4YKER*DAiSj5G}u|O?5D6av`&4weVC}KR*3$4M*+&wz^K*xM254%;zj*8K!ZRr z;&i}y!f8s@>vZw=EAd|Fk?2tNkS)CgnXa7Bfsvh|Yf*`S+N#;Q!6LOK>8PSy9xNz( z_|o$ZyzRVhedluT62!pVqZ1>J5P0c}d>?>oknK5@*_U>@P%zclHr0kzN=W=}LBVy?mH7)j? zb_8YxTex03L4;mq&#U-PmEiEG<*_0z8yh7DAe0L|sDmQ#{?gUqtls44m6=^1N9!OmcP9b=3SW zGx(3c04anD?`y)F)M;pqjHObqBWlfKL^PBTo{lx=zCm=c$W+j?XkDCLGumru2I}9< zZmy37f39uZ{^MJ!3DD%^?si>#iAOmm9{aVR#3Q;Qb%YYuau8#Z(=*%~Mgna4t%hZT zq<^6S3HegPTgLUNQx}U%KSD2RTJ%?#2GYm-)G}xfP1=nmfO|ngIcY#J+3@2MFy7H0 zP{KmVj(JOv5e{iQm{Dpn5M`MC$VLDk-)rIhuP<-VOxuLdB*}zrrAcFFJ#T+lkyaI3 znCw^FCzREOk9MMul{Terbj6Mv2&(#l;qaWz3vgv6H#dG~ENG0|eyNe_j)zP?wzTIA zhe#SB_sGBZ{z=&F$NE)C4ZG${9)-k$Ao(~22-|zYF(CfHeT~f!tdR5Bq<;h?{kX2b z1{Amlds$;zyEzyd>&JR^xiKazvQpi1MDoq(4>l5c>ks!vi4!NxJ6iHOpX~Z@Q!OS+ z;d<0-Jck3V{Cv!aL$jUbkVf+5O)=|OfxP8*)vLUT^H23@RO2$9)5)gm`b0(eQ|>kC z_bl#CG#h#Dvq{}Oke>BicTr?C*xRXg|T%geq{ zKu35B_VsQfo2_1GUwVCbQIDl^`e+E{={URQtvx_c;-SOwG4ZT_h|guMLhn>JbM_6rc(_9)s6D?K91&Xy@(6uadN%AgPU}F-$E074)%b) z1m-6<=Hh8@>vEDkc-Q9CKx^_fJf*s%T_##Wk71jm%KUXxjG$YY?qd_LT1sA>4;0Q) z)f96_@2-&YTaq8!EQiIpy0=s?!L>enJ2u`ggyI}bhruoBZZGVqW!EB43Cd}n-#mPw zS|V=OCW!GL1+l6x0rUj^oj4wAAk>Y5w-qJyo3L9`E8cQFrM%51v|;NF(5eJ^4?$Vw zPsOSRhJ(g%v2@z8?yJ3sqkXVW%=V|h6^`U!`;qNTJWjIt>=|iCZRGRQp3>e|aY8wA zBRB7)8yXP7dC!kZF+uuQUbhv}-S8=1FRqmPOpw2i&BI6xnlDUQ$o(h2NyXZX=TfzA zfY*gL{x4qD6fmQp!;iVO+S= zEP=K8q`pfbzVBnOU;_&mEbxeUp&BIOk|r!otdb1o3==X&QiC4+^k#Gia0S zF79kUd?(a%BV5Xa#+-;bHQ)5H37P4xN@~>Lv2#7c$mO#=(JPIPBmvk?0;vD+0)23{ zu!S4OM)S+LhE^qUcGS(t3B|$&^JWpZecIiuCeb5qW`_N&5a+@jMG-wj`M3s~>NyY* z=z7(%8_;$DBqE)s37|-~`b8B^*FzG)_>WE01;c)s5;}a_1_h-~*?6Ia_i|BAhq)_9 zNsKf@0uL8-2d5nd`i;N7EYnw*LqFb$5yCvVt8q}HKI*-P0>3bfLo~R zI&rtpXAo}JTPcgi!HXo02UWzM49zO* z%q8Vi7Tzex#c<$%pVZr5>+|}mzZl~7WHJ0pXqrt!GQMjfb6lK$3@V*e{$i7+ue5Zl zbtIfHLXYBCqOW2I{k}gWB!(z#RQs!>pb({WO340J9p1# zIV5V>R#m?d$6RuH)AZQfIAE5&cphx7AEm_YBBI z|418Bvy(E_>7-n6k=!m?-l{Gjj4S_qzcGcN;nNQ;d6%a)kJn{Mm@P2MihR$sMgQu9 z59Fo?gL99}!Z~0sTuF%HDfF;7U}L;#)p-<6CwG=P3}5Ik0*aXL>Ua$5XE=rP*$z9u z{&&;$z);ZEMzqDP7?ArwQTJWVq)E{~FC!f!lfjzbnzsyuwdZ-ieh(4j!41N3eL1fE z0hqJ4#aVtSTDr-Lx|e0PJT;{(ApFlq&>M+Hui2XIQSu?DV+#F~>I$&KuSeBDA%*CJ z+@thft)LGI_D~E(ArTbJ4)m<-V?p{LS2ia#6P+pNk7!D9-8iFr2nxz#<8C%mxBQFBd)w;28vq%zSt%|SbCJE z>;PxCCqo5}V z#Npt4I>)|c)~0wXN)F=jGbaWWM_B4fWHB2UHwI(-_<*$RlcsX4s3?LH!{6MI%k7nzA*qdNYOs$WT9 z*OuDZJ8?m)XloDcz@--+k1%+8R0r-qV}tf$4-9l9jC6W;4Kdh==hg4Pv->|pFFG&# zcJx8G@C_ks4RUM^GI%DLrbmm`kqyGVd=m>E4*r~(HM5n3J1M_xT=PK`-V3W!8Sf{+ zdnW+H+a#j0s|Hh*+Vtr>P?z)A=MHy3J)@>(K$`S;QmydVdQdm);9w4gN{|6xK6X3WJa`{6=MqkT3GoD+c^Iz@muxF!XV=;A)2$XB+k#C9y*6MYHx5Bi z#|44+D&s(7GbYk5s{%c2z{c|3gB47C!3qjcTZ2Yy`VjD+FA`q&IY5SG777`X!QZ8^ zJVg56J4k-VShK_4JaDz~z!dD&FNZ9Cvy0aL!OA=z%$KWKmrn9}diG>g^unb;$a5mu zbMjUK)^NJC$nUWRu{=y%CL%S*{E>J1r{G0wBMQcvkHSV0UIVxe?E$YNQ~O3-{?n%? zXMtn_Tz9baxhj?ncRTh$84J#!1`>0B+M`{8F>w!_DwFPJNcIy$`(3TlzBd8%&hO*P z>lUYFVs!*XlT3n&QOe<$XL|=;W;Nu^Sy-WL<@X|OYI*U z!Sqrbngyf$t}2Bky5ABF%~o{3#DF+mlGZFYD=FT7sQc`$!d#|0`3wl(JCDB4bDC)& ziN>jxI-d8@U>lEEN4sJz)&8b%A57FfQYXksU0)1DBUE-J&=`&{q3kO>tsCwaXYwZ- zIW%o2+J^-u_>)}G&v{b7zB_AlwMVYWv=zY#oUpAVr4jK+IH7|O7wkJossC0^1IFP5 z05ktayFNsGE<67xL;809-U?F&CBB~z`W>pl(s>O;&e^>h4ZRU*LfO-36|)ot|>A1rALp$gb-@}m~6 zM*U`Z-Dt<)({jsXLys1I*4s{Za`uH!SX%p?gc2wBCzrLfH%qV6GIimQq?WuQY0u7F zglYG~kMVF@-$K!4Q#Z?#D&<_2PXpJS})mw`&u zk16qJF`P*PORs02WGzQJ_g=izFuTnGP4G&J;j=_$YNQv7>|V`{jz(zjZ&y?%GKcLVJ7 zN+X2{azgKzXz160jgWBy1gH%M7SF^5!*kGkiqn<6ngIy1>BV;l+JIn@$nK*isfh%y z^K)>((E6w`TzQ}C8j$6uJw3YXsipSyWaiIFIU8(I5cpcnBdio#czDF< zRVUj+S{4gNf2;4;UeqcZh|fAXN0;p8kHr@7Zpl1|gYR2ltUD*A@S>KfF<4fo*=03= z9Mu9x#vMlBxXa@NXW8X7 z`E9N6srTjn{o;*=ye*QObt4}-2y!^}aK9nJqI&7QKFZ>EkS>0<0aX{VRU-Fh*}9qA zeVA1ywgn2zCj<@!c?Lm`fw40O>JKCR?M?OIdHcP}qcAObdu-SIcf3MO^bhiPEC5-3 zfs%86d8-alc|2{)i9`_;>u+@4qbr^ajBA zSY)#UT+XtGa>mWddhk~ zfqCF93a=wW+xS=Ifn6*JU0b}3%c=xA5m*_!gU1d)Ouy9(|0*z^PX-&_Xnk}q1rB7; zQlQv;g$kL)ML@cn^X%fjxZ)`=vhv99RdDS*4lf7adz8`mmiP}pTq~{&`gsiElgX=5 zppsO|FC%r;^ikY>&+(^!Gh)3gxBZ)pro4Q=pJ{@HZaKO*kl)6z*zDXUW zqY={@qj(bOsPEjq>=)1=26r`D&UgyvS|W9ukS1W%OgWllQcnczPH@3IeV^)(4`Qs? zVU%}I_^v9SKij*OM1pbrPu_{VH82U5QDUOwHsgS|^^~OUn7%7I0G8ye64S#ys98n1 zUvt5#z7Jt#REHC=e*akWd_43u_$ye@S_AoF6uQ)zaFZvrzxmI;I6i+?h_PxBVNidk z*i6~|UMzB2PlY*KH*&R|V7=UA<-IC0vV*utMq(tJh_*}a|E%WA)Bm%NObGVFm z#t6VX)XAvqfJC+GO+Yy$7)*&5y_M3;dMXD&E-5atr`ArLv*Lxl6)70KmU5)KXtghr z#$xOy$L(B8`~s(mV1xO?{h5$SigO%8Wv~8;xV~4ax&dF?bEF5ZDr2>>hLb=#c4L=T zTBm=026Lo3z_V@%BT%}HkxR;H-Zz~2DVGgb4WPo2cHjY{g^g@&njU6PcD+C{Msu;> z?L{T&@ABw7Wh^tLSaA49Jwv2cG?6T`H8}p47UCs3d9lwx+pS?=tv*_^0wmM za-cONCxEiOcdF;`4B9_e(c#?bkGLz-zk9;e$QFf;@xHjI_{sSAqHGo2( zI8k*6u03n=j9^k`yZtaOow>S$EWNW()gpaSj*7u!w!VAi|L=DIOV6>OS>5~E)Of@q z?O3k+{xNY1myRZ@t&+tl!4d+!o57>_MNr9^;|X+v4`^waU%CBX#TqiD;V3pOM@`GH?LLXFWx%3!Thx;{cvb zaQP^`&j`Mzo|oX*29DZ$43Mb;Ksx@;K=+}Dn`8i?VUf;K;1v=hw!1n~|zdNQTp>*pGuxt~skmfeSF08yWQ$tkh(G|*>Ts-#`QI4hEu|Q4x!kd(7m-8D>iavA;=d*J%Jw>~wl!W0w zU5Lm>r69T5$4?KH{DuogJ{T<|VpLb9)Q;_6En@L7h-E zjc*xB{-!&8^=UlTk-;lH(v4aX7|)>ZoGr5Yy%IRA;VMEf;`VdNn}_9qdXmA88OMt{ z4+TQu>FxzOdv(M@`Hj|w<`wSZeG=4BQ5>+B+;m}6_aM4b9fnvuRxZR5ioZb4MIWsh zUld5)pc=4vTh9281brJvUk<5&G@142^&r$^4B(ydIhB!ex>TS^dQ{CTtZ-K>Ji^;E zU3gUk*>vH;8@ksy=Nk?wWyPnZvd{(DM6j@V0 z7fX$d4VCfoEL~Ys9G-S^ba}LeK@3WUT4K#*Mg49C5cF(e4scP@n$ZjHwp+_TsLtQL zD(W)IBzAiBGek*;U{vdnMQ4*uJjU><>Oop6-y<_3w};B{wru2rZnf+Ggh7wkdU+cf zk&vKJ8f%2p-%_b_iyWJP_3Rt(%)acV<)!N8g@y?#R{Qi})aZ}fiQ$kB(d$p;!& zq69yU{~rVR-{TkNQRK-+-^Q1s3NRt^4 zKfSwW=ila$?*hk}((xYGwijplc;MGKDDz`!KB%7v*rS?c5;WkmjSh)x{$^ScMuO3!dM&ZiVN?04$f zB5H+}(tKQ_X0PiUnA1EJHs5vZlZ&8eSa9xkNz*AH|L~vfg*A*mPp3W8Et7~cx&Z;^ zf!CTs&yX5HUu;@6|Ly`v2^=Vo#eThkL4@X=+Zy2SC27{c97jBj(+oOkB+hG$)K=vr zwx|aGoV{?x(BhGWEgP&n<$uSI_D1bhfJb-Ut&z8(%)d{uxyI82CVp&&Qbu86I0~VR zo4Ob*wZNYOEStc4n|}Ngyoy%MO2G2`-Q6S%SBt0hoMw_@_PE#oUHhjC88D9275GW3 z&bipivlaQ-vS8AX)IE6`TEE#C^CoGqxFFpaR0tJq9!;2^CVCahjP*q>JVx!gMzKsd zfcmsX>B8g0XhhFycD#Zdi>Bh&e?x1uAOTfty_^hJ&C2c5ca9$8UCL%KcZay)x5aolcDk`yfe#B{6eR(X8umuH@Wx5=>vzdMG?ZYdWcrYsHAYGiIy z{f613{FWU)m|t9eAzt9B_eDfcUF@G;bm8C%x{*-^W*wpJ5YBloVrc+kcH4~XQzx<< zNBpfwWL5g^(Sm#c&L{4TAPsHTRjSYD|W3hC`d1JXi6Hv3HB7hrY@*LZFrbF|rQY-c2Hb3`1 ztP6~+BV4^zJ`MfojwAMXlcpZy*?(F}3v*LoHnAHeZrG0N2^zhy=%Z2mI6?b?rh3Xp zL}H{6q*Q&gBXz%Bgc3Akx`uA|{UW;&qdZm~@z5)aqUm&Me9gTLUSV#O9@$;GqKtI9 z%9*kRA*`}Kh`_Vf)h_Fhv!3CPr&zf}2C%zS1`=LPFlGpkhP1oB?*-rtPoRzm=x7SN z_s!Y_sd~lWY6&6*{ZCzi)mmfZS9PMXTL?A;M>+F7D0aIRFf)ZyV=uRg@B>3zTr;{R zkE1tI){W6`P?|!~`lxi^^#vH}U)+b)q<`O_J=uDB>*l77yf&Q95v& zwQQ)^Bu3XbJFouMO~ZZ8{y?o$#c;Ef-U37WR8cy{ahXyoDq`b~9iyZWErX8!w^HKv>@F% z64EU(qzDK~mqm1OTk`A3d?7E zW(^k_W)H;SePRGX*8i+nj@nCiro@y*hyxc*qCYpBWp+{V5WQb|gzFI!0#XsN?EW~wn)Gu$D*Qm4 zf8fPa{F0?9I15?<5$EX^e2)VmI#*WKW0L&wax@)B1Ro_#8otvKW&Ya4QucTr@$%m? zBx7?ghTHI`gt8@ot`kPmedlVDTU=G^795!|=_bS8i+Pl95sLyqRe`vduvs{2~=7EujIa6UvB=q{cA{$s!vU1 zoytY^)1PYH)j+j#Cc~fd1^Wg6Y_U=n^dQ;tI%BIj+MTkfHNcc}o}Vj$YaPj>CWRe2}`D!8Z#`_hG_Z z#Wa6_RQjdxE~22I;BxEJ_sm4nwnI&a&r&@*Is?f-lFz9v&W)fFg=n|fQ}4WUT^e5k z8rx+M7ejNrL%Zb4qiVYq*319p!~u6sU-$BpN0b*KZ<#<04r>1jV5Jb$J)er4#$xI7 zIkcla?U9LH;CQ8F4uH_8K&-ttJsO;w11}Yg_!4*utC62Afo$D}%wRJ+NUs{193`6= zIJc#qnH|+7rqR9Uoo9##^!M1Z?LF5PGYAfqB$(UIr)px4WoCJSf+(RyeIbDL6K&;A zj|*5a#fTf0AE`NHz2MX~@_fL#qBopMYeAq%IIN1=HH6f|8o{O~PM^Fk|bpSQq?Te#$2w60RpT z_wBiZUcBY2ROpBkS@;K401jY8b3n8#`Co>Kj>l1>i!7dUsqJUfL#qx#3>?N^ZMOe`%}%zcN=p2Hp^Qs#iNpftEpcg4wXw!x}C?V zx*RK2H19-rTsN!2;nBR-A5?j=NXD1=&UoOISF-^96-ORA$a_kSnt9PP81#|RL@~?N zo#W6zjMo`_TJmU&FAQi&n5va1ZcN>83RsW*opan%6l@_;oyhib2I?O^)vT$#9s^hj zPX6dSghv9aPE>~-YOF+Sb~B5SQ%x5`Sff#OgEeG#J5$IuulG4y(o64D>&zAAr;_iE zB5Eht6W~QgWoD zic2ZrW3iflqJ7&{&Wh<&&P0uCo7L?1%bdKW=XVU$4#k>+7kdARhjYK8{h*sDpV}(+ zvveKkP*%*o{!c^n=bi>cjlNx!Gotf|)>oKLYXFg_55?|N1ecim-&}?Bem~e5-aMch zs^>dwwTN{13{=QgZRIih;rWKw?Xds~q)rlucUU&~%;gsxIkcaT@0sN)FIqBKSD;gI zW;hc*Ou{bLDs*JWdi_iM3)&EK_tX4BLycwtb|i zySKLo>Ty5p%BqfZdDxV0b^1>|%Mc}`djz^aQKn z)dMS6l=?gYfROEdnn{P9XX-0#!#vEw{Ef#l3n<=3SGv>3?LtF|2gcl9jEqN?fR`uo;aLT$IQNf+sCIght+q_cZr9Z@lwt{SdV5+ zL_v1L{e+8kpMQkUYdiw722@ZC%e!nLl=GV%(@5fM6|*)97AUdJk_}0hJUh35sgr`Vcs%uD|5Pzs*`^~ z*~%M}7!O$^T(X@!p$IE_sa@&xvY63_Mnb}%lWlA)0m(X?Lp$*#g;D)m0U@MA*LHuf z6ssYHORas4M~2pGT5KNGN0+^3u}yM1E+!Knf{O*u?*G>FEqN>E{0S^)AHC*ZYsZS1 zbcKxuGTr-;!&>}aqx3Yu@{r`rwo_bQKz}fkb^O(?l}mqXD<9d{$xN4iwT>7^R%AwY zm!#=IVsx+0FMAqNeXR4^HX({qWU9iV`Gh^bEF)~1>`mZ4H3=KgVq7fX2(xbussvJ< zTag~sQ1ckvx&87Cb(`XJ&15I6Q7n~M_8)0`I^(mtJ0LnHOz-|-XIu3pKZiC9At$eR zIFbj^PwP;*OMCqXc<7E( zFB3CZxdiag73>(nv0l@>G=uSrb{yKjXYR!k@_5lLdnWPC`$aAbR>=(#ZR&q;eaOrx z035Gv2tfu6BpC>lfP1yxFN0iS8%kjA&Kz~H zilpsI)2*(jXG;02l2()JIG$VK%#84?B=>$5=_}lv&)qrzJ<54Li4+Cs)2Z0xz~ze=OtMYN63Ph z8<5HkmWsWt`jfDLA`GIH3VvP-gBvNQ*b~iCi~#yPCbSS!iB4x3M!02E%Jh)r!3)*- zcq3?$A~Cx*{gcaSej1aZr(BK2IlXWKl_eN{H7+B7AmAOpao&3ZMEsxaYx*KpumoUJ zkqr_kTiGQ`H;n>zrmKjjwzM2+Z%Y;H!V8NhBqz`lY8}_umrzl_c z!T+hiC7+vE%=3*?Et?A!K6sjCbcv73nwqgQBSB{v+F%eVUT52cLfDDvWw_z0ap?rj zMy&DC;44(PLhf?PIpZO_^YhpwLdwq%Q$X1Jkd4Q|_O_*Uc) zu>`0CBS)6Z7ywdy<6PQsR87M6mLgB%0Wr*PQHNl34qWiSfN(X0Vmt%r(~Sv@+n>I! zU$Ehyx_?&u`0eKEn=I)cLbwXHV1wZx!Z%?AyXmxdIYJ#o(r=(^)2!+Pt#z}-t$bp2 z0npd1j2Fo?PhFT6z%@|q8!ev2_0UGOcx$!%=TElcyjh2Xw9QEJ z7vHec_H(W#&il}W?a%o<_mIR-mQ%KzRa^g{?-?cFLJOj_bJV~G`wIO4F&)nZNs1%|jG-1H{oJ0dF ztE~GE3ODov^qF5D>0nNiAtS&q8a|6UcC)dEjYK|NkX&uCul2i!N)T+J_e_3qJ}%2= z1oqCn8QV@5GjL?0bP4~#uI-+Ln`Xn*gro63F( z@^??9r_8dElg(6No7(gjiJQsmCR)dc5)r~0$aa(wWdEan@0MDgl3YvZMMAy$==T@r zIl|YSvkQ&(>R?=~7>UApL}x-BT&CUw*S*by1Ac3alfb(>hN(cp$V{0`UQI4%tc}^3 z&%);2$~Kb5x5K4UJlp?7K=d8+6I$Noz?TY~4nv2{EoL=(Ydc3yg%HIv@*fs0?zazB zB(U=lBAMdXwAuw?Gz+|;+JV*rU-T(o%>0hw#wouC=p>-zyr#45>{C_p3~tdRqycP5 zI%aXOhm4O~TOxXO^ovy;i}v(X^(hK)R1JT=mDvY+bAG9%A}hO0G!ZyM;|}p1@)}w> z?|KcdzIqU8Qre$uZGYf&M%|6dJG5Dy^Oma~sqKQGQ#s9zoJa*o6Q#0JM4c>1D)gM* z@W*YCP*#+%IvC#$W|jSrs^W_;L@V(Zc{(#i;oY?wVtjnGI9GU(g#3sEu6x!x%|5Xh z0{d5`q;8`5@TE8^vsVU6sn5J9v!Kf5Q!oyGq#!JiejRlTt3zwFn~gQ$$O_pyN3;ba zrP_{;H*}-BLp-H4waBY!)VCg?V9&vux7cvWr%zMgKKx|7*mhJO7Q<-*!O|F)XN948 zd5Y=}1YE)=A=FF0xWknS*;g8E+;c$x1(fV!s<#yA z$|trW_}DrL%zf2TsS$eZM{`%#p<`Hzo@*MM@1zMDxX$z5_ew8A^{J?p3v^OKzd`Lk zx29VACBXfL7t$bb#)XGKYToG62-hOT&TD7bT-DZD9LfZ%9PqJY{oO!H|ESF=QsS&9-W9=Ut>MNNu`g>S|$ z8;s1lUA=SI-Rc%$dzL?65j#R<=Kbk2$aDP}S+d1p(?I7qh8r6y2M$Ze=QqePq{W^U zE=?HK7s+uxmCI&syGm7iwHo9nHeduq8Kl9hgpe9$`^&P4XaWBf8&-+jTkJ$V>p#Qa zMJS%dI~k`zU%y5&{iurQ6$2{Dnr60|6X#JIG9E{u;ENJ{_J_~gyQjd7#?ro>1ZPoR zhAe-)a7)KynEczU^)5@WSAi%hWVU`oZf5w{G27k?Tj9VW)hXChw<< zi3n+KdeU?%@s7$2bD*YI*4bAqFA) zLO)2AtIq^ybeA-gHAaXRNU`#KKxVJh>ssZS^J?0G9F#5NN4s+C7F7bf!FCA8^~Rvz zJTV3lOG}gLp4pf4h-%9-H!Mp*v%pqiZ5K!bRrb88UZ`cF$<@9l1DJcMvFdf?$qmkN6u$y z%SaoM2na%B#+tj1oz14cZ3VD%&}M!ibD!aO100x_-rwQ4+1{PsC#R(BNCHYq=$uxY zDx1gIMP@AT$&MRsK>qz+(vR@1{)zZ>kl-@)w=9c zgstJdIKyoU?$UKrx_&+;51jI$fR%Uy0(+J(Ss%raxH6()JLK8=4oOC09L0T$^jVAd zIly3X((g37gJhqd8Vt7_c*3WA!UUv)fx5*BeOAhbAr>5$Tu29AklXW?q}qARj5V^I z-$qN_`Jm1#%tsvM`Gu2gf#g}PC4Xz&sQ$w`hb>mpKIa2;LWP1}G;QVbUp)5dXbi$? zLi_*@I;JU=ea4DqDd~wbCnHlX$ zb*Z9?hT=H8DwwX?`Cg@-x>LuVpySVnH+Vo9(yaMR)=XMc=X>;0*$L3t{lqRc5o=sppYO zL^QZ&RRa$D%maT<1Nx+T{QjW@&?>y5=wa0Um;S&C$yYOkWyF>d7m{2|j|iqc?u;Df zO(kel=a#HS={JXVFiSA$P1>?WY*57$Y=T8Q2Gwkratk%gKXVD*GlKDUD@)6er`c$5 ze^Um|Sj|=C)1l2PZ59uNh~lDDz5!`G)xjY9=xkPv;q{E5@qs?o=L~IE&l+|IemSUi zi_^|rMVL^&B#lXC`{^L2B2D)nM61{gM~JXz4qa2J9#PesR3)||Jv)sM+GF|0*IE?( znWCcg^k^95eK8LRT3s%Ax{%)D=o0h%vy3tvGd1(*vSo*u|v?NqX~n!5mpRn zqJq_{rV_s9f9)AtL3lD{<6>EMeWZI?61ncf#m-!av`lfAB14*P{E^ghE5ryLCc%*rV+^J2K0IKa#{0?oZCR+s$ zzMas~y!u&G6#j}TdlW6ptyL$ntwG;QDN7l7^y2(lHqL-DV%ZMpYs98O_x1WM@nf%? zpNwj{x0&d)gbG;D!v!ot)bc{VH+r>vYY}z`sL|?5xMjKoQ_`0Z0g$th!!-NrMd+<{ zMA@#dNVxobHfg$S1p#Ggda2wuw6y&g)dQL5`*+Jf4T!5KezJL6Nu*mwjE+`KP#57q z_M~m#JE+a8C42IYZRqIlI4FnpeYfBBGu=q*8ND(175FW=9=R1{v6`_e#n{bJ^xcj_ z&GX`E8wUB*W>iHgNjepCE5uQrOW>w!GhRPH3$pyVLLF}ADXQd#S-Mn`3xuLHgfCN{R7P|r$_DQZ z(@g`-P}gn+;Xp( zh%u6`fiH%^U5}|XGK%Q0QbLqyfK`mR`7P5OG$5Mbv)#m%Vj5A6)^ak;MnpIt$@wk(Cd>y0Pfe=5F$wrVcvkGOQ4>+c9XqnD+6y9y^<2LX-svq| z+Sa=n0g`@-);6%v`k=@zi$x}VDNgNsMaC~aXaxXZsF%J#E5>cy7>w0GnipcA)0&`I zG>A2QW&t%YUG{8=xhv`Nq7qDA+kVhuUq+a{*+vMy9Iob)un-lxz1C9n{H?)hQ>;jO zt(7~yS3gXz7EEK0>hqUH8RHh@O-Br5rW8y=nPH0+Ty{fdqgWXLchi5R9@NE?IfF_eyl^ zjkEk#+o1sbJyQA^NBLMRG1-Igzdzsl@n{a)J2%=hG>U8O6k!sqoI59G&wLj@@&un2 znK*+Pxrn^_x+q6y*{ZOR7AdhO&rdqUNXJU~RcplZ@Am2#z##64*|@_Dr&TcA;M8w) zJT_ZrPqMzBHJo+Wcu`wu5R-cQr=ZRsZ^+@uJ;7YcNFD$kb(aIcHv09+?THH1lW_!F-&|+ z0X9XQ;hur=YoJ-$Psz!toE;8hH0zMYDfySS%BEe)x0TyOxXVGgQVbrt6vb(4N19b! z^myLFVSBwnmT*F-`~7;YmE>zC1POERTmLhj@ec>VmRV&UGHoOqp^Oc6!+KP9qXX`B z@R#>Md6Llrt*lNQ&whU~-XQ>7GmauBuoHPX+RrnQhQ3@Vku@oAsB)=J>y87&G)K&$ z^FRDjN2%8l(HnWqh;JVgaiWd;bD+wV?8mUk*T;3E@-4@MhcH-sKxsfN%s?Qh9*sb)W)La8leN!GYNFJrSCjFs1q+$U&_!k)V1CSg=UU^q$M~l;r_}H%YyYXIFV!^YV{`|fJ1Nirko(d# zJN3UlXu8hjR0D)Q;r~gLlnN- z&M%Bm51~FYV>4-#pHtMDmsUDqb^^etDzr28y?9e|7}+HEkO{r`%#;L}Ux5{YO zrvfSFeVB~ckYMm5ZYS{#>C8@RXMp7$oUf5})u zww^1AIpZ_;pkkAIo_sQ2IsN%s^WmdzbB|2^2>9=lC7#tEQG&^~_j`P}(jZ&$e8R?i z*W{EOWXX9T$>6@ot;tClXMGl?OQ1ll5(A)9fQJ%B)emRUFA5#>W!a~CqqAe$-77$m zPKRol5VJkbIek7M)L1+mF;jdZh8sydNwp*eefrwgj?mU!S{{&3DBtvv8(0rrfVFJa z8CvX?>OYJUd5Pg}nO)}fIE!+y|Fag!t21iVnCml9B(&}ggeN_(YoUb;P+8i}KT!R(`A`tx-4C_R&V>ia=+~x|0{5<4MmHKi zVDZ`h96^t+i$)()Y&;KsJC%36ne!DfFzG`CNE;pZC}=+wC*)VwQ3K>n5(}u#S}mdy z5JX9}`-wF@gP=95Yt(uw9|>Hvap}gilChTC1lS18e3q}h6Q$waQg3>k&djmY37e)N zd4ZF$&oot^{fM&@DJnKr zvVx8EJvPPoH~z<8CnrcC3yaHV=Z(C%$*(LFh(TsjexEl0oc+B4&ZIE{#;k{rTDA#DZ$v|Qt`%b*o9t7rZpDnckNX7Z_O<=4GstL?**PJ2KGm03xK7EAp9vyrH|+ zUiR!BV9z4B`R1a${^c_Oi@cK)kv)5@W2R9-?njH}Qm(`~o#~?L9?#*swY-%VRpE`F zJFsvak}>7iE|v3~ue;&TdczHo9pTC>;nsXiE~G^@N<^5G@a`-)5MZ5c=K3u7+3U2( zW^rNXWXVbL5_4+jECp%BvgB&MYFo1uhiIQZ7_6E2+e$o|LDv0wtN+pLhw?T~eo16P zd>cK?a5<_75Fy0=K)ADgC-<8R88;I0M$EUR!Qp_t%m9S(GC$HK$z$7i#vbg=~fY5aRgfMynA4%kPG zQ!Nxl^r_;3{_N%XoxF`yHFtlvGj1IwiFPG?8u%DxQed#V_j^FH^aB=>f9e!mye7wv zO4z{sQ6{gIkuUxDE9~!V$o1uv)V|7r?Vf=XRg5rjZ?_6YL({C)xHT)wmJ)rdWy`vB zb^-K<1qKji(e+petXj^FP~uL^GLJS~XI1X0YZ1ZV;}sXOQr3rY&S!n6;lJix#VsR9 z(PU%oTIAQDAE*AT54MF68d=fH)h?4P_cAIBdoV^wbb2yX_u(kWoai0H{Hp%>2q#SI z(KiaV`r<$o(cLcTTa91L`I9z(RKFv^?UC%QGhNSn|C^I>3t3J5of5lfAfQ5KircIL z3^_U1z7|PC06X*7%jQg}-$bbRhP5VeEL=zngl4f#&&n%}C~Tv`SQK1%4k<1rT6}n5DuM(y}ngaXQeAorp%-)C>YKJsA7mQ4mKY zz3ke}RavBo3MWk=;0^C9Z}g-8EE`_L|8srPw38ZNkLtjnD6c+9O}i-%K(i45In_%2 zd=e$f9q<4z$whwsnxW?Y*!}45UFtH0R2=-hB?Y|%beFSl9)aJ>+JIG_yGE;SL&O;z z->Edg3B|Us3iC-neKU;bKh3x5j0K^~7(nzcUuxckvA??6B@MGWQXtMGYq0krMm~T2 zpTj3T2^hT=hk7dry&h5efzuCmrm9aBf(9sHf#`s!)PJHLT^cv2=F=aaQ?~cF?X|$3 z&fHk>pT~~DAj_Z`9LBbf?a9U;$iQ-~M0JpmQtC5^65}WuqS@C5L8NQ6o0qmz$emqK zgBUZux)F(py`<$1Q~hy$@}@oH^COnVobi9p(=iZM{@1Pm3_=zFx$rD$*4!szWnknb z^DUFlCvLhtcc)6z0`Oad2F=m7%qI08?sh$q#>@?PlQO=B+C%H*YktmkGb2<9qLdu= z7JVihr^epqWsVKW8Ud1EwSteJ9tP!&;dNY=(9Q>&X=~TV{SC7*`~#KxbAwY((QbWA zKmjHE^MB36BwvAlyI%(Mv+_iwcCzU&t;CW(S;w6S zfwu8vi+e6HL5uH;SN%B8qEuiuhBJStZJa$z&v#|QfPJ_VM62mqjJ6L`D+ zp@egA(cP&iIPrTZBxfSd?zeOr-@1`IbZHO?-IC%ll zxPIJCkHrHl!a6ocLPv0-9gcTpGlPWarW_3hu=pd4tvZfiOM=cKm_~Z>B!D3R)Y;j_ zL;z`ITmq{2^saV+ZSO||B5dc=<8fi9h0~tCF0t3P0&HZK0W;2$sB=&4wmsjpVRK|o zl}^i+?Xe`M2K8=_eL6X?@!p_oBdDBc13)NID`(!ouQL&*FRkapGX();c*UB3t=_y| ze8850@R9$a`d5hHy#N^vU_`@ee0qWp(I<4`@mt~=wGS5FrJ{J3b@)mLC92gC#AHwA zFB$qQ99U(8c{TKDPyO9u$1eqdR~*6ueF{8oX~@%sZq;@TU#aUgNyO!gFrhNX$-e@x zw`zPR!xqo3WNMUc3Dp+ne+O+%!ilDe(^q?&;m1vT`|@(g=J8G;Tj2b3mG#@$om{TB zllBbZoNRTJf%74Mks!%-MCQlF+!Fh+vLC-rFtTCLfp)SL2XA%*z_~olw@*uM0!03v z{O1@`B%Pe!}M%$j-a%0t}LwhToA$$eLaxQ`1 z#W)_4HuO{5bJf4bWVwPl9!|{?@L~6aZ^17@{yqSCA_Nud&2f7k{VFKH3w#D+HMmBJ z@2|=Nsf?FX#6mzNz;XYXq5+6&h@hcjToj9WY>y2j1DShnrYX%sR>=W80QBS1`Q2;| zv*aELnNL9_$bw1SFln>(8u4#Ef0UKW!7enERY#5`B1)xCE?+mvQGrt3QghV-Hw3Nu zrByysK?M9WHgE9!KLZ&hm2;DNysnSN^{t=GXcvkxk}^Kj$q90tH6g5p`;(K(&nP|6 z0+vh&{FB|Y;45k)Wn$c&i18Dhbl5IUSWSbhYd)d>tqkwR>1PHwB`Z+jOgN8s2)$s?C372y<`<{#>JGNC8W`%kL9N9MTYgI8ju3 zwFU)UzL4VlW7C%x*o@spWR^D_p(YY&dm*mKf9!$bc7<3t0%ve-&*HXM2fzOCgFKzE zGtW!#aD)no_zm`s`>&XMK+?}51N^oFX2b3@LgLvqO&1O=IkmwnT!aa!WjJVRsNRku zNDx*|HVG?dQNicJ9{Tr_aS3kIzoN1${IxLqte|TtkY;0{kIWKgU75vyLP4#3%Dc&j zG^%Ng+SnfV->HCAsgAe+I(e6ajmCp1K7oQ8ejn2_&mnmw#UW{S?&uPI|nS8Z^h}L5(LXee{pSvde|-hKh!@%RJ=nKdK!~tqROIsE)5G{#&c?h7H=eN%E@QP zAEBIyp4C26=J~E(xft*2VpLuf8uke2I^2ArWE48UYCE_d1kNW@e9sjYC~{zY62l!> zeg?(b4B9(999j8-gtCXdniF1d{;seI9(bH@4glDSIw;N8Mb|c>67njo$Emc=^5tF{ zzVg({sx%)>ozcQ?~CH)Tl@DdBc(}M|Rs-}aD>%oLozdxOrg#ut zekIT_#n%(u!g2ndTGlHYHXn(_Z6-q?B)b=X6>k$JcKw(94@zkDQLO3oK)l8yutgY% zvW!^f(@L7JAkumE>bDr^DZ1R^xI?`40n|8b^`H)1GM zkdmI7(uRfu(encvsE+h&I2)T80LA&t3c~YvD5anf_4n|*BciYV))ttNSD?I!1YPrg zI{#73wplG6vcUoldyEzKc+XVr=gu zl5Bg};9AQEfA8N`_smHC<4pvb+!>OzW&{vDc)zglG?;Hw%FH2yt>8*ewcKR<*a42UagCgK;x z4v}YOZ~V4NOsH+eO%P#(v7Zlvj&1)nA>BA78@J^n$J2j^P#B-GegpmUm7WdCy`=z* z57OZSTLDXb2KV4CS<=KH#5RA)mI}9EfhJ2u#{kFbt_1odRz0FGo$8yJ?V=i3h^zTn z=zhS*hujUF)TaGM#|R8IRM^Z3ZJ9t{vGFTy;sUD>nn?K44d*%lSm-@HoKOPL_e(h0 zBcfERduh;@O8NMpMNrQ;+tldY&i{A#CtxP?$$R)Un!!E8GN0Ptr6r3+!<`@p)}Wqu z7AGeik62G|PVUbB|NKF?5o?(4d*FyX0c}3}bY9@90?kz~xFG%6m4dbF0Pb-T8U0cO z65=3ZWX1-`Ru?in9PtB&q27&N6#{p@1gR8Ef&pRoc483l^ND$13DE7xdB^X6VxSUd zbKjD^L;m}#|9)x21_J|^Y^Ynv{_l6Egn=sHK~B+j^8bz;OOlfY1WhZLRKkD#@0?e_ uGyrOPWyuUY$6Wt28?J}o|HlXZB@6lKQqLu$2?OTcf)L6`rCJ5+i2nlwem|@L literal 0 HcmV?d00001 diff --git a/frontend/uni_modules/uni-upgrade-center-app/uniCloud/database/db_init.json b/frontend/uni_modules/uni-upgrade-center-app/uniCloud/database/db_init.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/frontend/uni_modules/uni-upgrade-center-app/uniCloud/database/db_init.json @@ -0,0 +1 @@ +{} diff --git a/frontend/uni_modules/uni-upgrade-center-app/utils/call-check-version.ts b/frontend/uni_modules/uni-upgrade-center-app/utils/call-check-version.ts new file mode 100644 index 0000000..82b4de1 --- /dev/null +++ b/frontend/uni_modules/uni-upgrade-center-app/utils/call-check-version.ts @@ -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 // 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 { + // #ifdef APP + return new Promise((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(JSON.stringify(res.result)) as UniUpgradeCenterResult + resolve(result) + } + } + }).catch((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 +} diff --git a/frontend/uni_modules/uni-upgrade-center-app/utils/check-update-nvue.js b/frontend/uni_modules/uni-upgrade-center-app/utils/check-update-nvue.js new file mode 100644 index 0000000..6c0d4bc --- /dev/null +++ b/frontend/uni_modules/uni-upgrade-center-app/utils/check-update-nvue.js @@ -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 + }); + }); + } + }); + } + }); +} diff --git a/frontend/uni_modules/uni-upgrade-center-app/utils/check-update.ts b/frontend/uni_modules/uni-upgrade-center-app/utils/check-update.ts new file mode 100644 index 0000000..8c27d17 --- /dev/null +++ b/frontend/uni_modules/uni-upgrade-center-app/utils/check-update.ts @@ -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 { +// #endif +// #ifndef APP-HARMONY +export default function () : Promise { +// #endif + return new Promise((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 +} diff --git a/frontend/uni_modules/uni-upgrade-center-app/utils/utils.ts b/frontend/uni_modules/uni-upgrade-center-app/utils/utils.ts new file mode 100644 index 0000000..414e096 --- /dev/null +++ b/frontend/uni_modules/uni-upgrade-center-app/utils/utils.ts @@ -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; +} \ No newline at end of file diff --git a/frontend/uni_modules/uts-openSchema/changelog.md b/frontend/uni_modules/uts-openSchema/changelog.md new file mode 100644 index 0000000..de1703b --- /dev/null +++ b/frontend/uni_modules/uts-openSchema/changelog.md @@ -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 diff --git a/frontend/uni_modules/uts-openSchema/package.json b/frontend/uni_modules/uts-openSchema/package.json new file mode 100644 index 0000000..2225c04 --- /dev/null +++ b/frontend/uni_modules/uts-openSchema/package.json @@ -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" + } + } + } + } + } +} \ No newline at end of file diff --git a/frontend/uni_modules/uts-openSchema/readme.md b/frontend/uni_modules/uts-openSchema/readme.md new file mode 100644 index 0000000..e03fdf7 --- /dev/null +++ b/frontend/uni_modules/uts-openSchema/readme.md @@ -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) diff --git a/frontend/uni_modules/uts-openSchema/utssdk/app-android/config.json b/frontend/uni_modules/uts-openSchema/utssdk/app-android/config.json new file mode 100644 index 0000000..bf95925 --- /dev/null +++ b/frontend/uni_modules/uts-openSchema/utssdk/app-android/config.json @@ -0,0 +1,3 @@ +{ + "minSdkVersion": "21" +} \ No newline at end of file diff --git a/frontend/uni_modules/uts-openSchema/utssdk/app-android/index.uts b/frontend/uni_modules/uts-openSchema/utssdk/app-android/index.uts new file mode 100644 index 0000000..ae2f7de --- /dev/null +++ b/frontend/uni_modules/uts-openSchema/utssdk/app-android/index.uts @@ -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 + } +} diff --git a/frontend/uni_modules/uts-openSchema/utssdk/app-harmony/index.uts b/frontend/uni_modules/uts-openSchema/utssdk/app-harmony/index.uts new file mode 100644 index 0000000..21af83b --- /dev/null +++ b/frontend/uni_modules/uts-openSchema/utssdk/app-harmony/index.uts @@ -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 + } +} diff --git a/frontend/uni_modules/uts-openSchema/utssdk/app-ios/config.json b/frontend/uni_modules/uts-openSchema/utssdk/app-ios/config.json new file mode 100644 index 0000000..f272043 --- /dev/null +++ b/frontend/uni_modules/uts-openSchema/utssdk/app-ios/config.json @@ -0,0 +1,3 @@ +{ + "deploymentTarget": "12.0" +} \ No newline at end of file diff --git a/frontend/uni_modules/uts-openSchema/utssdk/app-ios/index.uts b/frontend/uni_modules/uts-openSchema/utssdk/app-ios/index.uts new file mode 100644 index 0000000..9adaf48 --- /dev/null +++ b/frontend/uni_modules/uts-openSchema/utssdk/app-ios/index.uts @@ -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 +} diff --git a/frontend/uni_modules/uts-openSchema/utssdk/interface.uts b/frontend/uni_modules/uts-openSchema/utssdk/interface.uts new file mode 100644 index 0000000..2b599ac --- /dev/null +++ b/frontend/uni_modules/uts-openSchema/utssdk/interface.uts @@ -0,0 +1,2 @@ +export type OpenSchema = (url : string) => void +export type CanOpenURL = (url : string) => boolean diff --git a/frontend/uni_modules/uts-openSchema/utssdk/web/index.uts b/frontend/uni_modules/uts-openSchema/utssdk/web/index.uts new file mode 100644 index 0000000..4a20902 --- /dev/null +++ b/frontend/uni_modules/uts-openSchema/utssdk/web/index.uts @@ -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; +} diff --git a/frontend/uni_modules/uts-progressNotification/changelog.md b/frontend/uni_modules/uts-progressNotification/changelog.md new file mode 100644 index 0000000..b7caf4e --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/changelog.md @@ -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通知栏显示进度插件 diff --git a/frontend/uni_modules/uts-progressNotification/package.json b/frontend/uni_modules/uts-progressNotification/package.json new file mode 100644 index 0000000..9f2adb1 --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/package.json @@ -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" + } + } + } + } +} diff --git a/frontend/uni_modules/uts-progressNotification/readme.md b/frontend/uni_modules/uts-progressNotification/readme.md new file mode 100644 index 0000000..70e8d79 --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/readme.md @@ -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 + +取消通知消息显示 + diff --git a/frontend/uni_modules/uts-progressNotification/utssdk/app-android/AndroidManifest.xml b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/AndroidManifest.xml new file mode 100644 index 0000000..bcc33ab --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/frontend/uni_modules/uts-progressNotification/utssdk/app-android/TransparentActivity.uts b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/TransparentActivity.uts new file mode 100644 index 0000000..06fe1bc --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/TransparentActivity.uts @@ -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); + } + } + } +} diff --git a/frontend/uni_modules/uts-progressNotification/utssdk/app-android/callbacks.uts b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/callbacks.uts new file mode 100644 index 0000000..5dacea6 --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/callbacks.uts @@ -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 +} diff --git a/frontend/uni_modules/uts-progressNotification/utssdk/app-android/config.json b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/config.json new file mode 100644 index 0000000..7ed4299 --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/config.json @@ -0,0 +1,3 @@ +{ + "minSdkVersion": "19" +} \ No newline at end of file diff --git a/frontend/uni_modules/uts-progressNotification/utssdk/app-android/constant.uts b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/constant.uts new file mode 100644 index 0000000..e89c210 --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/constant.uts @@ -0,0 +1,2 @@ +export const ACTION_DOWNLOAD_FINISH = "ACTION_DOWNLOAD_FINISH" +export const ACTION_DOWNLOAD_PROGRESS = "ACTION_DOWNLOAD_PROGRESS" \ No newline at end of file diff --git a/frontend/uni_modules/uts-progressNotification/utssdk/app-android/index.uts b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/index.uts new file mode 100644 index 0000000..76957c3 --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/index.uts @@ -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 +} diff --git a/frontend/uni_modules/uts-progressNotification/utssdk/app-android/res/values/notification_progress_styles.xml b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/res/values/notification_progress_styles.xml new file mode 100644 index 0000000..cc01105 --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/utssdk/app-android/res/values/notification_progress_styles.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/frontend/uni_modules/uts-progressNotification/utssdk/interface.uts b/frontend/uni_modules/uts-progressNotification/utssdk/interface.uts new file mode 100644 index 0000000..7d68ffe --- /dev/null +++ b/frontend/uni_modules/uts-progressNotification/utssdk/interface.uts @@ -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 diff --git a/frontend/uni_modules/uts-progressNotification/utssdk/unierror.uts b/frontend/uni_modules/uts-progressNotification/utssdk/unierror.uts new file mode 100644 index 0000000..e69de29