feat(cdn-image): 实现CDN静态图片统一版本缓存管理机制
新增`src/utils/assetUrl.js`工具文件,封装CDN图片版本化处理逻辑 在全局App启动及支付确认页预加载图片版本配置,缓存版本信息到本地 替换支付确认页内硬编码的CDN图片地址,改用工具生成带版本的URL 更新README.md与AGENTS.md文档,说明缓存更新流程与开发约定
Showing
5 changed files
with
194 additions
and
8 deletions
| ... | @@ -20,7 +20,7 @@ | ... | @@ -20,7 +20,7 @@ |
| 20 | - `src/hooks/`:较轻量的通用 hooks,目前主要是 `useGo.js` 这类导航辅助。 | 20 | - `src/hooks/`:较轻量的通用 hooks,目前主要是 `useGo.js` 这类导航辅助。 |
| 21 | - `src/api/`:接口封装层。`fn.js` 是统一返回格式与 mock 接入的总入口,`index.js`、`tabbar.js`、`wx/pay.js` 分别承接首页、底部导航、微信支付等当前主链路接口;`message.js` 目前更偏旧版原生资讯列表/详情演示接口。 | 21 | - `src/api/`:接口封装层。`fn.js` 是统一返回格式与 mock 接入的总入口,`index.js`、`tabbar.js`、`wx/pay.js` 分别承接首页、底部导航、微信支付等当前主链路接口;`message.js` 目前更偏旧版原生资讯列表/详情演示接口。 |
| 22 | - `src/mock/`:本地 mock 体系。目录拆分规则是 `index.js` 统一入口、`modules/` 放 handler、`shared/` 放公共解析能力、`stores/` 放有状态 mock 数据、`fixtures/` 放静态样本。 | 22 | - `src/mock/`:本地 mock 体系。目录拆分规则是 `index.js` 统一入口、`modules/` 放 handler、`shared/` 放公共解析能力、`stores/` 放有状态 mock 数据、`fixtures/` 放静态样本。 |
| 23 | -- `src/utils/`:公共工具层。当前高频核心文件是 `authRedirect.js`、`request.js`、`config.js`、`webview.js`、`tabbar.js`、`paySuccessRedirect.js`、`wechatPay.js`;改动授权、环境、WebView 路由或支付时优先先看这里。若是“小程序内支付成功后该跳到哪里”这类问题,先看 `paySuccessRedirect.js`,不要在页面里各自重复写一份 `app_menu -> data.user.link -> webview-preview / 首页` 判断。 | 23 | +- `src/utils/`:公共工具层。当前高频核心文件是 `authRedirect.js`、`request.js`、`config.js`、`webview.js`、`tabbar.js`、`paySuccessRedirect.js`、`wechatPay.js`、`assetUrl.js`;改动授权、环境、WebView 路由、支付或 CDN 静态图缓存时优先先看这里。若是“小程序内支付成功后该跳到哪里”这类问题,先看 `paySuccessRedirect.js`,不要在页面里各自重复写一份 `app_menu -> data.user.link -> webview-preview / 首页` 判断。 |
| 24 | - `src/stores/`:Pinia 状态目录。当前重点是 `tabbar.js`(底部导航配置)和 `router.js`(授权回跳来源页),其他 store 多为基础能力或历史保留。 | 24 | - `src/stores/`:Pinia 状态目录。当前重点是 `tabbar.js`(底部导航配置)和 `router.js`(授权回跳来源页),其他 store 多为基础能力或历史保留。 |
| 25 | - `src/assets/`、`src/constants/`:分别承接静态资源和常量;其中 `src/constants/` 目前较轻,新增共享枚举或键名时再往这里收。 | 25 | - `src/assets/`、`src/constants/`:分别承接静态资源和常量;其中 `src/constants/` 目前较轻,新增共享枚举或键名时再往这里收。 |
| 26 | - `config/`:构建与环境配置目录,`dev.js` / `prod.js` 控制 `API_RUNTIME_ENV`,不要把环境切换逻辑重新分散回页面层。 | 26 | - `config/`:构建与环境配置目录,`dev.js` / `prod.js` 控制 `API_RUNTIME_ENV`,不要把环境切换逻辑重新分散回页面层。 |
| ... | @@ -48,6 +48,17 @@ Mock 敶&極嚗src/mock/index.js` | ... | @@ -48,6 +48,17 @@ Mock 敶&極嚗src/mock/index.js` |
| 48 | 48 | ||
| 49 | 仓库实现上,共享支付能力核心在 `src/composables/useWechatMiniPay.js` 与 `src/api/index.js`;调试入口在 `src/pages/pay-test/index.vue`,正式确认页在 `src/pages/pay-confirm/index.vue`,H5 桥接页在 `src/pages/pay-bridge/index.vue`。另一条是历史保留的通用支付封装,位于 `src/utils/wechatPay.js` 与 `src/api/wx/pay.js`,通过 `pay_id` 调用 `/srv/?a=icbc_pay_wxamp`。当前若处理支付问题,默认先看 `order_id -> useWechatMiniPay -> /srv/?a=pay` 这条主链路;其中 `f`、`client_id` 由请求层公共参数统一补齐,不要在单个支付接口 URL 上重复手写。若没有用户明确点名 `pay_id`、`wechatPay.js` 或 `src/api/wx/pay.js`,暂时不要把排查范围扩到这条历史链路,也不要顺手改它。修改支付逻辑时务必先确认当前页面接的是哪一条接口链路,不要混用 `order_id` 与 `pay_id`,也不要在未拿到后端有效支付参数时直接调用 `requestPayment`。涉及 H5/WebView 唤起支付时,应优先保持 `pages/pay-bridge` 的桥接职责与返回参数约定稳定;涉及小程序内直接支付时,应优先保持 `pages/pay-confirm` 的“展示金额 -> 用户点击 -> 调用共享支付能力 -> 成功后按 `app_menu.data.user.link` 跳 `webview-preview`,无 `user` 则回首页”职责单一。这个成功跳转规则当前集中在 `src/utils/paySuccessRedirect.js`,如果以后还有别的小程序内支付入口要复用同样落点,优先复用它,不要在各页面里各写一版菜单判断。无论改哪条链路,都需要区分成功、取消、失败三类状态,并至少在微信开发者工具或真机中验证一次“授权状态检查 -> 拉起支付 -> 返回结果展示/回跳”的流程。 | 49 | 仓库实现上,共享支付能力核心在 `src/composables/useWechatMiniPay.js` 与 `src/api/index.js`;调试入口在 `src/pages/pay-test/index.vue`,正式确认页在 `src/pages/pay-confirm/index.vue`,H5 桥接页在 `src/pages/pay-bridge/index.vue`。另一条是历史保留的通用支付封装,位于 `src/utils/wechatPay.js` 与 `src/api/wx/pay.js`,通过 `pay_id` 调用 `/srv/?a=icbc_pay_wxamp`。当前若处理支付问题,默认先看 `order_id -> useWechatMiniPay -> /srv/?a=pay` 这条主链路;其中 `f`、`client_id` 由请求层公共参数统一补齐,不要在单个支付接口 URL 上重复手写。若没有用户明确点名 `pay_id`、`wechatPay.js` 或 `src/api/wx/pay.js`,暂时不要把排查范围扩到这条历史链路,也不要顺手改它。修改支付逻辑时务必先确认当前页面接的是哪一条接口链路,不要混用 `order_id` 与 `pay_id`,也不要在未拿到后端有效支付参数时直接调用 `requestPayment`。涉及 H5/WebView 唤起支付时,应优先保持 `pages/pay-bridge` 的桥接职责与返回参数约定稳定;涉及小程序内直接支付时,应优先保持 `pages/pay-confirm` 的“展示金额 -> 用户点击 -> 调用共享支付能力 -> 成功后按 `app_menu.data.user.link` 跳 `webview-preview`,无 `user` 则回首页”职责单一。这个成功跳转规则当前集中在 `src/utils/paySuccessRedirect.js`,如果以后还有别的小程序内支付入口要复用同样落点,优先复用它,不要在各页面里各写一版菜单判断。无论改哪条链路,都需要区分成功、取消、失败三类状态,并至少在微信开发者工具或真机中验证一次“授权状态检查 -> 拉起支付 -> 返回结果展示/回跳”的流程。 |
| 50 | 50 | ||
| 51 | +## CDN 静态图片缓存约定 | ||
| 52 | +仓库里如果有直接写死的 `https://cdn.ipadbiz.cn/jls_weapp/images/` 图片地址,不要再在页面里裸写完整 URL,也不要每张图各自手工拼时间戳。当前约定是统一走 `src/utils/assetUrl.js`:应用启动时会预加载 `https://cdn.ipadbiz.cn/jls_weapp/version.json`,并给本地写死的 CDN 图片自动补上 `?v=<image_version>`。 | ||
| 53 | + | ||
| 54 | +这里的边界要守住: | ||
| 55 | + | ||
| 56 | +- 当前这套版本参数机制只用于“仓库里本地写死的 CDN 图片地址”,主要目的是 CDN 覆盖同名图后,让小程序端也能跟着拿到新图。 | ||
| 57 | +- 接口动态返回的图片 URL 默认先不要动,除非用户明确要求把某一类接口图也接入同一套版本机制。 | ||
| 58 | +- 新增这类本地写死图片时,优先用 `getVersionedImageAssetByName('xxx.png')`,不要继续手抄完整 CDN 地址。 | ||
| 59 | +- 如果只是替换同名图片内容,标准流程应该是:上传同名图片到 CDN -> 手动刷新图片 CDN 缓存 -> 更新 `version.json` 里的 `image_version` -> 再刷新 `version.json` 自己的 CDN 缓存。 | ||
| 60 | +- 如果后续发现某个页面用了 `jls_weapp/images` 下的本地写死图但没有走 `assetUrl.js`,应视为漏接入,优先补到这一层,而不是在页面里临时各写一版缓存参数。 | ||
| 61 | + | ||
| 51 | ## 提交与合并请求规范 | 62 | ## 提交与合并请求规范 |
| 52 | 当前 Git 历史以简短中文提交为主,例如 `初始化觉林寺小程序项目`。后续提交信息也请保持中文、简洁、祈使语气,并聚焦单一改动。提交 PR 时请附上:变更背景与解决方案、关联任务或问题、影响的页面或模块、手工验证步骤;涉及界面改动时,需补充截图或录屏。 | 63 | 当前 Git 历史以简短中文提交为主,例如 `初始化觉林寺小程序项目`。后续提交信息也请保持中文、简洁、祈使语气,并聚焦单一改动。提交 PR 时请附上:变更背景与解决方案、关联任务或问题、影响的页面或模块、手工验证步骤;涉及界面改动时,需补充截图或录屏。 |
| 53 | 64 | ||
| ... | @@ -55,4 +66,4 @@ Mock 敶&極嚗src/mock/index.js` | ... | @@ -55,4 +66,4 @@ Mock 敶&極嚗src/mock/index.js` |
| 55 | 本仓库默认使用中文沟通与书写。今后新增或修改的说明性文字请统一使用中文,包括但不限于文档、注释说明、提交说明、PR 描述、变更摘要和协作备注;如必须保留英文术语,请同时提供中文语义,避免只写英文描述。 | 66 | 本仓库默认使用中文沟通与书写。今后新增或修改的说明性文字请统一使用中文,包括但不限于文档、注释说明、提交说明、PR 描述、变更摘要和协作备注;如必须保留英文术语,请同时提供中文语义,避免只写英文描述。 |
| 56 | 67 | ||
| 57 | ## 安全与配置提示 | 68 | ## 安全与配置提示 |
| 58 | -不要提交真实的 AppID、令牌或生产环境域名。首次接手项目时,请优先检查 `src/utils/config.js`、`config/dev.js`、`config/prod.js` 与 `project.config.json`。授权与支付测试环境当前会复用既有后端配置,调整域名、`client_id`、支付接口地址或 WebView 跳转地址前,必须先确认不会影响 `openid`、会话续期和微信支付参数生成。除非明确要重构认证链路,否则不要随意删除或绕过 `src/pages/auth/index` 认证页;同理,除非明确要废弃本地 mock 联调能力,否则不要把 `API_RUNTIME_ENV`、`src/mock/index.js` 或 `src/mock/modules/` 里的统一分发能力改回页面内局部开关。 | 69 | +不要提交真实的 AppID、令牌或生产环境域名。首次接手项目时,请优先检查 `src/utils/config.js`、`config/dev.js`、`config/prod.js` 与 `project.config.json`。授权与支付测试环境当前会复用既有后端配置,调整域名、`client_id`、支付接口地址或 WebView 跳转地址前,必须先确认不会影响 `openid`、会话续期和微信支付参数生成。若调整 CDN 静态图片更新策略,也要同时确认 `https://cdn.ipadbiz.cn/jls_weapp/version.json` 仍然返回 `{ "image_version": "..." }` 这类结构,避免 `assetUrl.js` 读不到版本号。除非明确要重构认证链路,否则不要随意删除或绕过 `src/pages/auth/index` 认证页;同理,除非明确要废弃本地 mock 联调能力,否则不要把 `API_RUNTIME_ENV`、`src/mock/index.js` 或 `src/mock/modules/` 里的统一分发能力改回页面内局部开关。 | ... | ... |
| ... | @@ -174,6 +174,45 @@ API_RUNTIME_ENV: '"production"' | ... | @@ -174,6 +174,45 @@ API_RUNTIME_ENV: '"production"' |
| 174 | 174 | ||
| 175 | 详细约定见 [src/mock/README.md](/Users/huyirui/program/itomix/jls_weapp/src/mock/README.md)。 | 175 | 详细约定见 [src/mock/README.md](/Users/huyirui/program/itomix/jls_weapp/src/mock/README.md)。 |
| 176 | 176 | ||
| 177 | +### 1.3 CDN 静态图片缓存更新 | ||
| 178 | + | ||
| 179 | +如果仓库里的页面直接写死了 `https://cdn.ipadbiz.cn/jls_weapp/images/` 下的图片地址,不建议每次换图都改代码重新提审,也不要在每个页面里临时手工拼时间戳。当前项目已经约定用 `version.json` 统一控制这类本地写死 CDN 图片的缓存版本。 | ||
| 180 | + | ||
| 181 | +当前固定地址: | ||
| 182 | + | ||
| 183 | +- `https://cdn.ipadbiz.cn/jls_weapp/version.json` | ||
| 184 | + | ||
| 185 | +当前文件结构: | ||
| 186 | + | ||
| 187 | +```json | ||
| 188 | +{ | ||
| 189 | + "image_version": "20260515-1" | ||
| 190 | +} | ||
| 191 | +``` | ||
| 192 | + | ||
| 193 | +运行方式: | ||
| 194 | + | ||
| 195 | +- 应用启动时会预加载这份 `version.json` | ||
| 196 | +- 本地写死的 `https://cdn.ipadbiz.cn/jls_weapp/images/xxx.png` 会自动变成 `https://cdn.ipadbiz.cn/jls_weapp/images/xxx.png?v=<image_version>` | ||
| 197 | +- 这样当 CDN 覆盖同名图片后,只要版本值变化,小程序就会把它当成新资源重新拉取 | ||
| 198 | + | ||
| 199 | +当前边界: | ||
| 200 | + | ||
| 201 | +- 这套机制只默认作用于“仓库里本地写死的 CDN 图片地址” | ||
| 202 | +- 接口动态返回的图片 URL 默认不处理,避免把首页/后台内容图也一起卷进缓存策略 | ||
| 203 | + | ||
| 204 | +开发约定: | ||
| 205 | + | ||
| 206 | +- 新增这类本地写死图片时,优先通过 `src/utils/assetUrl.js` 里的 `getVersionedImageAssetByName('xxx.png')` 接入 | ||
| 207 | +- 不要继续在页面里裸写完整 CDN 地址后再各自补 `?v=` 或时间戳 | ||
| 208 | + | ||
| 209 | +当你要替换同名图片时,推荐流程是: | ||
| 210 | + | ||
| 211 | +1. 上传同名图片覆盖 CDN 旧文件 | ||
| 212 | +2. 手动刷新图片 CDN 缓存 | ||
| 213 | +3. 修改 `version.json` 里的 `image_version` | ||
| 214 | +4. 再刷新 `version.json` 自己的 CDN 缓存 | ||
| 215 | + | ||
| 177 | ### 2. 定义 API 接口 | 216 | ### 2. 定义 API 接口 |
| 178 | 217 | ||
| 179 | 编辑 `src/api/index.js`,添加您的业务接口: | 218 | 编辑 `src/api/index.js`,添加您的业务接口: | ... | ... |
| ... | @@ -3,12 +3,17 @@ import { createPinia } from 'pinia' | ... | @@ -3,12 +3,17 @@ import { createPinia } from 'pinia' |
| 3 | import './utils/polyfill' | 3 | import './utils/polyfill' |
| 4 | import './app.less' | 4 | import './app.less' |
| 5 | import { saveCurrentPagePath, hasAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect' | 5 | import { saveCurrentPagePath, hasAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect' |
| 6 | +import { preloadImageAssetVersion } from '@/utils/assetUrl' | ||
| 6 | import { useTabbarStore } from '@/stores/tabbar' | 7 | import { useTabbarStore } from '@/stores/tabbar' |
| 7 | 8 | ||
| 8 | const pinia = createPinia() | 9 | const pinia = createPinia() |
| 9 | 10 | ||
| 10 | const App = createApp({ | 11 | const App = createApp({ |
| 11 | async onLaunch(options) { | 12 | async onLaunch(options) { |
| 13 | + preloadImageAssetVersion().catch((error) => { | ||
| 14 | + console.error('预加载图片版本配置失败:', error) | ||
| 15 | + }) | ||
| 16 | + | ||
| 12 | const path = options?.path || '' | 17 | const path = options?.path || '' |
| 13 | const query = options?.query || {} | 18 | const query = options?.query || {} |
| 14 | 19 | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | <view class="pay-confirm-page"> | 2 | <view class="pay-confirm-page"> |
| 3 | - <view class="hero-panel"> | 3 | + <view class="hero-panel" :style="hero_panel_style"> |
| 4 | <text class="page-title">常住随用</text> | 4 | <text class="page-title">常住随用</text> |
| 5 | 5 | ||
| 6 | - <view class="amount-card"> | 6 | + <view class="amount-card" :style="amount_card_style"> |
| 7 | <text class="amount-label">支付金额:</text> | 7 | <text class="amount-label">支付金额:</text> |
| 8 | <view class="amount-value-group"> | 8 | <view class="amount-value-group"> |
| 9 | <text class="amount-value">{{ display_amount }}</text> | 9 | <text class="amount-value">{{ display_amount }}</text> |
| ... | @@ -19,6 +19,7 @@ | ... | @@ -19,6 +19,7 @@ |
| 19 | 19 | ||
| 20 | <button | 20 | <button |
| 21 | class="pay-button" | 21 | class="pay-button" |
| 22 | + :style="pay_button_style" | ||
| 22 | hover-class="pay-button-hover" | 23 | hover-class="pay-button-hover" |
| 23 | :loading="pay_loading" | 24 | :loading="pay_loading" |
| 24 | :disabled="!order_id || pay_loading" | 25 | :disabled="!order_id || pay_loading" |
| ... | @@ -34,6 +35,7 @@ | ... | @@ -34,6 +35,7 @@ |
| 34 | import { computed, ref, watch } from 'vue' | 35 | import { computed, ref, watch } from 'vue' |
| 35 | import { useLoad } from '@tarojs/taro' | 36 | import { useLoad } from '@tarojs/taro' |
| 36 | import { useWechatMiniPay } from '@/composables/useWechatMiniPay' | 37 | import { useWechatMiniPay } from '@/composables/useWechatMiniPay' |
| 38 | +import { getVersionedImageAssetByName, preloadImageAssetVersion } from '@/utils/assetUrl' | ||
| 37 | import { redirectAfterPaySuccess } from '@/utils/paySuccessRedirect' | 39 | import { redirectAfterPaySuccess } from '@/utils/paySuccessRedirect' |
| 38 | 40 | ||
| 39 | const order_id = ref('') | 41 | const order_id = ref('') |
| ... | @@ -53,6 +55,15 @@ const display_amount = computed(() => { | ... | @@ -53,6 +55,15 @@ const display_amount = computed(() => { |
| 53 | }) | 55 | }) |
| 54 | 56 | ||
| 55 | const has_amount = computed(() => display_amount.value !== '--') | 57 | const has_amount = computed(() => display_amount.value !== '--') |
| 58 | +const hero_panel_style = computed(() => ({ | ||
| 59 | + background: `url('${getVersionedImageAssetByName('bgg@2x.png')}') center top / 100% 100% no-repeat`, | ||
| 60 | +})) | ||
| 61 | +const amount_card_style = computed(() => ({ | ||
| 62 | + background: `url('${getVersionedImageAssetByName('money_bg@2x.png')}') center / 100% 100% no-repeat`, | ||
| 63 | +})) | ||
| 64 | +const pay_button_style = computed(() => ({ | ||
| 65 | + background: `url('${getVersionedImageAssetByName('btnn@2x.png')}') center / 100% 100% no-repeat`, | ||
| 66 | +})) | ||
| 56 | 67 | ||
| 57 | watch(last_result_text, (value) => { | 68 | watch(last_result_text, (value) => { |
| 58 | if (!pay_loading.value) { | 69 | if (!pay_loading.value) { |
| ... | @@ -107,6 +118,10 @@ const handlePay = async () => { | ... | @@ -107,6 +118,10 @@ const handlePay = async () => { |
| 107 | } | 118 | } |
| 108 | 119 | ||
| 109 | useLoad((options) => { | 120 | useLoad((options) => { |
| 121 | + preloadImageAssetVersion().catch((error) => { | ||
| 122 | + console.error('支付确认页预加载图片版本配置失败:', error) | ||
| 123 | + }) | ||
| 124 | + | ||
| 110 | order_id.value = String(options?.order_id || '').trim() | 125 | order_id.value = String(options?.order_id || '').trim() |
| 111 | amount.value = String(options?.amount || options?.money || '').trim() | 126 | amount.value = String(options?.amount || options?.money || '').trim() |
| 112 | 127 | ||
| ... | @@ -131,7 +146,6 @@ useLoad((options) => { | ... | @@ -131,7 +146,6 @@ useLoad((options) => { |
| 131 | min-height: 612rpx; | 146 | min-height: 612rpx; |
| 132 | padding: 132rpx 48rpx 88rpx; | 147 | padding: 132rpx 48rpx 88rpx; |
| 133 | box-sizing: border-box; | 148 | box-sizing: border-box; |
| 134 | - background: url('https://cdn.ipadbiz.cn/jls_weapp/images/bgg@2x.png') center top / 100% 100% no-repeat; | ||
| 135 | } | 149 | } |
| 136 | 150 | ||
| 137 | .page-title { | 151 | .page-title { |
| ... | @@ -149,7 +163,6 @@ useLoad((options) => { | ... | @@ -149,7 +163,6 @@ useLoad((options) => { |
| 149 | min-height: 145rpx; | 163 | min-height: 145rpx; |
| 150 | padding: 0 40rpx; | 164 | padding: 0 40rpx; |
| 151 | box-sizing: border-box; | 165 | box-sizing: border-box; |
| 152 | - background: url('https://cdn.ipadbiz.cn/jls_weapp/images/money_bg@2x.png') center / 100% 100% no-repeat; | ||
| 153 | display: flex; | 166 | display: flex; |
| 154 | align-items: center; | 167 | align-items: center; |
| 155 | justify-content: space-between; | 168 | justify-content: space-between; |
| ... | @@ -221,7 +234,6 @@ useLoad((options) => { | ... | @@ -221,7 +234,6 @@ useLoad((options) => { |
| 221 | font-size: 34rpx; | 234 | font-size: 34rpx; |
| 222 | font-weight: 500; | 235 | font-weight: 500; |
| 223 | color: #fff6eb; | 236 | color: #fff6eb; |
| 224 | - background: url('https://cdn.ipadbiz.cn/jls_weapp/images/btnn@2x.png') center / 100% 100% no-repeat; | ||
| 225 | 237 | ||
| 226 | &::after { | 238 | &::after { |
| 227 | border: 0; | 239 | border: 0; |
| ... | @@ -230,7 +242,6 @@ useLoad((options) => { | ... | @@ -230,7 +242,6 @@ useLoad((options) => { |
| 230 | &[disabled] { | 242 | &[disabled] { |
| 231 | opacity: 0.72; | 243 | opacity: 0.72; |
| 232 | color: #fff6eb; | 244 | color: #fff6eb; |
| 233 | - background: url('https://cdn.ipadbiz.cn/jls_weapp/images/btnn@2x.png') center / 100% 100% no-repeat; | ||
| 234 | } | 245 | } |
| 235 | } | 246 | } |
| 236 | 247 | ... | ... |
src/utils/assetUrl.js
0 → 100644
| 1 | +import { ref } from 'vue' | ||
| 2 | +import Taro from '@tarojs/taro' | ||
| 3 | + | ||
| 4 | +const IMAGE_CDN_PREFIX = 'https://cdn.ipadbiz.cn/jls_weapp/images/' | ||
| 5 | +const ASSET_VERSION_URL = 'https://cdn.ipadbiz.cn/jls_weapp/version.json' | ||
| 6 | +const ASSET_VERSION_STORAGE_KEY = 'jls_weapp_image_asset_version' | ||
| 7 | +const ASSET_VERSION_FIELD = 'image_version' | ||
| 8 | + | ||
| 9 | +const normalizeValue = (value) => String(value || '').trim() | ||
| 10 | + | ||
| 11 | +const getStoredAssetVersion = () => { | ||
| 12 | + try { | ||
| 13 | + return normalizeValue(Taro.getStorageSync(ASSET_VERSION_STORAGE_KEY)) | ||
| 14 | + } catch (error) { | ||
| 15 | + console.error('读取图片版本缓存失败:', error) | ||
| 16 | + return '' | ||
| 17 | + } | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +export const imageAssetVersion = ref(getStoredAssetVersion()) | ||
| 21 | + | ||
| 22 | +let assetVersionRequestPromise = null | ||
| 23 | + | ||
| 24 | +const saveAssetVersion = (version) => { | ||
| 25 | + const normalizedVersion = normalizeValue(version) | ||
| 26 | + imageAssetVersion.value = normalizedVersion | ||
| 27 | + | ||
| 28 | + try { | ||
| 29 | + if (normalizedVersion) { | ||
| 30 | + Taro.setStorageSync(ASSET_VERSION_STORAGE_KEY, normalizedVersion) | ||
| 31 | + return | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + Taro.removeStorageSync(ASSET_VERSION_STORAGE_KEY) | ||
| 35 | + } catch (error) { | ||
| 36 | + console.error('缓存图片版本失败:', error) | ||
| 37 | + } | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +const parseAssetVersionResponse = (data) => { | ||
| 41 | + if (typeof data === 'string') { | ||
| 42 | + try { | ||
| 43 | + const parsedData = JSON.parse(data) | ||
| 44 | + return normalizeValue(parsedData?.[ASSET_VERSION_FIELD]) | ||
| 45 | + } catch (error) { | ||
| 46 | + return '' | ||
| 47 | + } | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + return normalizeValue(data?.[ASSET_VERSION_FIELD]) | ||
| 51 | +} | ||
| 52 | + | ||
| 53 | +const appendOrReplaceQueryParam = (url, key, value) => { | ||
| 54 | + if (!value) { | ||
| 55 | + return url | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + const hashIndex = url.indexOf('#') | ||
| 59 | + const hash = hashIndex >= 0 ? url.slice(hashIndex) : '' | ||
| 60 | + const baseUrl = hashIndex >= 0 ? url.slice(0, hashIndex) : url | ||
| 61 | + const queryPattern = new RegExp(`([?&])${key}=[^&#]*`) | ||
| 62 | + | ||
| 63 | + if (queryPattern.test(baseUrl)) { | ||
| 64 | + return `${baseUrl.replace(queryPattern, `$1${key}=${encodeURIComponent(value)}`)}${hash}` | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + const joiner = baseUrl.includes('?') ? '&' : '?' | ||
| 68 | + return `${baseUrl}${joiner}${key}=${encodeURIComponent(value)}${hash}` | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +export const buildImageCdnUrl = (fileName = '') => `${IMAGE_CDN_PREFIX}${normalizeValue(fileName)}` | ||
| 72 | + | ||
| 73 | +export const shouldUseVersionedImageAsset = (url) => normalizeValue(url).startsWith(IMAGE_CDN_PREFIX) | ||
| 74 | + | ||
| 75 | +export const getVersionedImageAssetUrl = (url) => { | ||
| 76 | + const normalizedUrl = normalizeValue(url) | ||
| 77 | + if (!shouldUseVersionedImageAsset(normalizedUrl)) { | ||
| 78 | + return normalizedUrl | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + const version = normalizeValue(imageAssetVersion.value) | ||
| 82 | + if (!version) { | ||
| 83 | + return normalizedUrl | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + return appendOrReplaceQueryParam(normalizedUrl, 'v', version) | ||
| 87 | +} | ||
| 88 | + | ||
| 89 | +export const getVersionedImageAssetByName = (fileName = '') => ( | ||
| 90 | + getVersionedImageAssetUrl(buildImageCdnUrl(fileName)) | ||
| 91 | +) | ||
| 92 | + | ||
| 93 | +export const preloadImageAssetVersion = async (force = false) => { | ||
| 94 | + if (!force && assetVersionRequestPromise) { | ||
| 95 | + return assetVersionRequestPromise | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + assetVersionRequestPromise = Taro.request({ | ||
| 99 | + url: appendOrReplaceQueryParam(ASSET_VERSION_URL, '_t', String(Date.now())), | ||
| 100 | + method: 'GET', | ||
| 101 | + enableCache: false, | ||
| 102 | + }) | ||
| 103 | + .then((response) => { | ||
| 104 | + const version = parseAssetVersionResponse(response?.data) | ||
| 105 | + if (version) { | ||
| 106 | + saveAssetVersion(version) | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + return version | ||
| 110 | + }) | ||
| 111 | + .catch((error) => { | ||
| 112 | + console.error('加载图片版本配置失败:', error) | ||
| 113 | + return imageAssetVersion.value | ||
| 114 | + }) | ||
| 115 | + .finally(() => { | ||
| 116 | + assetVersionRequestPromise = null | ||
| 117 | + }) | ||
| 118 | + | ||
| 119 | + return assetVersionRequestPromise | ||
| 120 | +} |
-
Please register or login to post a comment