feat(image-cache): 新增图片本地缓存并全站接入

引入 cached-image 组件与 image-cache 工具,实现签名 URL 稳定 key 剥离、downloadFile 落盘及 TTL/LRU 清理,替换 11 个页面与 clothes-card 共 22 处网络图片渲染点,减少 CDN 重复流量。
This commit is contained in:
2026-09-26 19:49:54 +08:00
parent 7cd6c169c2
commit f169777f4a
30 changed files with 754 additions and 25 deletions
@@ -0,0 +1,4 @@
{
"component": true,
"styleIsolation": "isolated"
}
@@ -0,0 +1,77 @@
// cached-image —— 通用图片组件:网络图经 utils/image-cache 落盘后展示(命中时零网络请求),
// 本地路径(wxfile:// / http://tmp / 包内路径等)原样直出;
// 未命中缓存时先骨架占位、下载完成后显示本地文件,失败自动回退网络地址,功能不受影响。
import { getCachedLocalPath, resolveImage, dropImageCache } from '../../utils/image-cache'
Component({
properties: {
src: { type: String, value: '' },
// 与 image 组件对齐:未显式传入时保持 scaleToFill 默认行为
mode: { type: String, value: 'scaleToFill' },
lazyLoad: { type: Boolean, value: false },
},
data: {
// 实际渲染地址:本地文件路径或回退的网络地址;空串 = 骨架占位中
shown: '',
},
observers: {
src() {
this.refresh()
},
},
lifetimes: {
attached() {
;(this as any)._alive = true
this.refresh()
},
detached() {
;(this as any)._alive = false
},
},
methods: {
refresh() {
const src = this.data.src
const seq = (((this as any)._seq as number) || 0) + 1
;(this as any)._seq = seq
if (!src) {
this.setData({ shown: '' })
return
}
if (src.indexOf('https://') !== 0) {
// 本地/包内路径直接渲染,不进入缓存流程
this.setData({ shown: src })
return
}
const hit = getCachedLocalPath(src)
if (hit) {
this.setData({ shown: hit })
return
}
// 未命中:先占位,下载落盘后显示本地文件(期间不渲染网络地址,避免额外请求抵消缓存收益)
if (this.data.shown) this.setData({ shown: '' })
resolveImage(src).then(
(localPath) => {
if ((this as any)._alive === false || (this as any)._seq !== seq) return
this.setData({ shown: localPath })
},
() => {
if ((this as any)._alive === false || (this as any)._seq !== seq) return
// 缓存不可用时回退网络地址,保证功能可用
this.setData({ shown: src })
}
)
},
onImgError() {
const shown = this.data.shown
const src = this.data.src
// 网络地址自身失败或非网络图:不再处理,避免循环回退
if (!shown || !src || shown.indexOf('https://') === 0) return
;(this as any)._seq = (((this as any)._seq as number) || 0) + 1
// 本地文件失效(被系统清理/损坏):清理缓存并回退网络地址一次
dropImageCache(src)
this.setData({ shown: src })
},
},
})
@@ -0,0 +1,10 @@
<!-- 通用图片:缓存命中直出本地文件,未命中先骨架占位,失败回退网络地址 -->
<view wx:if="{{!shown}}" class="ci-ph"></view>
<image
wx:if="{{shown}}"
class="ci-img"
src="{{shown}}"
mode="{{mode}}"
lazy-load="{{lazyLoad}}"
binderror="onImgError"
/>
@@ -0,0 +1,26 @@
/* 宿主节点:对齐 image 组件默认 inline-block 布局,尺寸与圆角由调用方 class 控制 */
:host {
display: inline-block;
overflow: hidden;
}
/* 内层图片:填满宿主并继承圆角,保证既有版式零回归 */
.ci-img {
display: block;
width: 100%;
height: 100%;
border-radius: inherit;
}
/* 骨架占位:与全局 .sk 同款视觉,组件内独立定义避免依赖外部样式加载顺序 */
.ci-ph {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #F2F2F0 25%, #E9E9E6 37%, #F2F2F0 63%);
background-size: 400% 100%;
animation: ciSkLoad 1.5s ease infinite;
}
@keyframes ciSkLoad {
0% { background-position: 100% 50%; }
100% { background-position: 0 50%; }
}
@@ -1,4 +1,7 @@
{
"component": true,
"styleIsolation": "apply-shared"
"styleIsolation": "apply-shared",
"usingComponents": {
"cached-image": "/components/cached-image/cached-image"
}
}
@@ -1,12 +1,12 @@
<!-- v2 衣物瓦片:无卡片容器,图下两行(衬线品名 + 灰色属性行) -->
<view class="tile {{selected ? 'is-picked' : ''}}" bindtap="onTap">
<view class="tile-img">
<image
<cached-image
wx:if="{{item.processed_image}}"
class="pic"
src="{{item.processed_image}}"
mode="aspectFill"
lazy-load
lazy-load="{{true}}"
/>
<view wx:else class="ph icon icon-hanger"></view>
<view wx:if="{{statusText}}" class="tile-flag {{statusType === 'ink' ? 'is-ink' : ''}}">{{statusText}}</view>