hookehuyr

feat(cdn-image): 实现CDN静态图片统一版本缓存管理机制

新增`src/utils/assetUrl.js`工具文件,封装CDN图片版本化处理逻辑
在全局App启动及支付确认页预加载图片版本配置,缓存版本信息到本地
替换支付确认页内硬编码的CDN图片地址,改用工具生成带版本的URL
更新README.md与AGENTS.md文档,说明缓存更新流程与开发约定
...@@ -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
......
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 +}