|
|
@@ -1,9 +1,31 @@
|
|
|
// ---- 图标本地缓存(App 端文件系统 + H5 端 base64/uni.Storage) ----
|
|
|
|
|
|
const CACHE_MAP_KEY = 'app_icon_cache_map'
|
|
|
+const APP_ICON_DIR_NAME = 'app-icons'
|
|
|
+const APP_ICON_CACHE_DIR = '_doc/' + APP_ICON_DIR_NAME
|
|
|
+const H5_ICON_MAX_SIZE = 128
|
|
|
+const H5_ICON_WEBP_QUALITY = 0.82
|
|
|
+const H5_RECOMPRESS_MIN_LENGTH = 32 * 1024
|
|
|
|
|
|
-function hasFileSystem() {
|
|
|
- return typeof uni.getFileSystemManager === 'function'
|
|
|
+const pendingIconDownloads = new Map()
|
|
|
+
|
|
|
+function getCacheRuntime() {
|
|
|
+ let runtime = 'other'
|
|
|
+ // #ifdef H5
|
|
|
+ runtime = 'h5'
|
|
|
+ // #endif
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ runtime = 'app'
|
|
|
+ // #endif
|
|
|
+ return runtime
|
|
|
+}
|
|
|
+
|
|
|
+function isH5CacheEnabled() {
|
|
|
+ return getCacheRuntime() === 'h5'
|
|
|
+}
|
|
|
+
|
|
|
+function isAppCacheEnabled() {
|
|
|
+ return getCacheRuntime() === 'app'
|
|
|
}
|
|
|
|
|
|
function getIconCacheMap() {
|
|
|
@@ -17,7 +39,32 @@ function setIconCacheMap(map) {
|
|
|
try { uni.setStorageSync(CACHE_MAP_KEY, map || {}) } catch (e) {}
|
|
|
}
|
|
|
|
|
|
-// H5 环境:用 uni.request 下载图片转 base64 缓存到 uni.Storage
|
|
|
+function isAppLocalIconPath(path) {
|
|
|
+ const value = String(path || '')
|
|
|
+ return value.startsWith('_doc/')
|
|
|
+ || value.startsWith('file://')
|
|
|
+ || value.startsWith('/')
|
|
|
+ || /^[a-zA-Z]:\\/.test(value)
|
|
|
+}
|
|
|
+
|
|
|
+function isValidCachedPathForRuntime(path) {
|
|
|
+ const value = String(path || '')
|
|
|
+ if (!value) return false
|
|
|
+ if (isH5CacheEnabled()) return value.startsWith('data:image/')
|
|
|
+ if (isAppCacheEnabled()) return isAppLocalIconPath(value)
|
|
|
+ return false
|
|
|
+}
|
|
|
+
|
|
|
+export function getCachedIconPath(iconObjectKey) {
|
|
|
+ const key = String(iconObjectKey || '')
|
|
|
+ if (!key) return ''
|
|
|
+
|
|
|
+ const cacheMap = getIconCacheMap()
|
|
|
+ const cachedPath = cacheMap[key]
|
|
|
+ return isValidCachedPathForRuntime(cachedPath) ? String(cachedPath) : ''
|
|
|
+}
|
|
|
+
|
|
|
+// H5 环境:用 uni.request 下载图片,先压缩再转成 base64 缓存到 uni.Storage
|
|
|
function arrayBufferToBase64(buffer) {
|
|
|
let binary = ''
|
|
|
const bytes = new Uint8Array(buffer)
|
|
|
@@ -27,15 +74,119 @@ function arrayBufferToBase64(buffer) {
|
|
|
return btoa(binary)
|
|
|
}
|
|
|
|
|
|
+function getImageMimeType(res) {
|
|
|
+ const headers = res.header || res.headers || {}
|
|
|
+ const contentType = headers['content-type'] || headers['Content-Type'] || ''
|
|
|
+ const mimeType = String(contentType).split(';')[0].trim()
|
|
|
+ return mimeType.startsWith('image/') ? mimeType : 'image/png'
|
|
|
+}
|
|
|
+
|
|
|
+function canUseCanvasCompressor() {
|
|
|
+ return typeof window !== 'undefined'
|
|
|
+ && typeof document !== 'undefined'
|
|
|
+ && typeof Blob !== 'undefined'
|
|
|
+ && typeof URL !== 'undefined'
|
|
|
+ && typeof URL.createObjectURL === 'function'
|
|
|
+ && typeof URL.revokeObjectURL === 'function'
|
|
|
+ && typeof Image !== 'undefined'
|
|
|
+}
|
|
|
+
|
|
|
+function getRawDataUrl(buffer, mimeType) {
|
|
|
+ return 'data:' + mimeType + ';base64,' + arrayBufferToBase64(buffer)
|
|
|
+}
|
|
|
+
|
|
|
+function getCanvasOutputType(canvas) {
|
|
|
+ const webp = canvas.toDataURL('image/webp', H5_ICON_WEBP_QUALITY)
|
|
|
+ return webp.startsWith('data:image/webp') ? 'image/webp' : 'image/png'
|
|
|
+}
|
|
|
+
|
|
|
+function compressImageSourceToDataUrl(src, rawDataUrl) {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ if (!canUseCanvasCompressor()) {
|
|
|
+ resolve(rawDataUrl)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const img = new Image()
|
|
|
+ img.onload = () => {
|
|
|
+ try {
|
|
|
+ const sourceWidth = img.naturalWidth || img.width
|
|
|
+ const sourceHeight = img.naturalHeight || img.height
|
|
|
+ if (!sourceWidth || !sourceHeight) {
|
|
|
+ resolve(rawDataUrl)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const scale = Math.min(1, H5_ICON_MAX_SIZE / Math.max(sourceWidth, sourceHeight))
|
|
|
+ const targetWidth = Math.max(1, Math.round(sourceWidth * scale))
|
|
|
+ const targetHeight = Math.max(1, Math.round(sourceHeight * scale))
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
+ canvas.width = targetWidth
|
|
|
+ canvas.height = targetHeight
|
|
|
+
|
|
|
+ const ctx = canvas.getContext('2d')
|
|
|
+ if (!ctx) {
|
|
|
+ resolve(rawDataUrl)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.clearRect(0, 0, targetWidth, targetHeight)
|
|
|
+ ctx.drawImage(img, 0, 0, targetWidth, targetHeight)
|
|
|
+
|
|
|
+ const outputType = getCanvasOutputType(canvas)
|
|
|
+ const compressed = outputType === 'image/webp'
|
|
|
+ ? canvas.toDataURL(outputType, H5_ICON_WEBP_QUALITY)
|
|
|
+ : canvas.toDataURL(outputType)
|
|
|
+ resolve(compressed && compressed.length < rawDataUrl.length ? compressed : rawDataUrl)
|
|
|
+ } catch (e) {
|
|
|
+ resolve(rawDataUrl)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ img.onerror = () => resolve(rawDataUrl)
|
|
|
+ img.src = src
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+async function compressImageBufferToDataUrl(buffer, mimeType) {
|
|
|
+ const rawDataUrl = getRawDataUrl(buffer, mimeType)
|
|
|
+ if (!canUseCanvasCompressor()) return rawDataUrl
|
|
|
+
|
|
|
+ const blob = new Blob([buffer], { type: mimeType })
|
|
|
+ const objectUrl = URL.createObjectURL(blob)
|
|
|
+ try {
|
|
|
+ return await compressImageSourceToDataUrl(objectUrl, rawDataUrl)
|
|
|
+ } finally {
|
|
|
+ URL.revokeObjectURL(objectUrl)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function shouldRecompressCachedDataUrl(dataUrl) {
|
|
|
+ const value = String(dataUrl || '')
|
|
|
+ return canUseCanvasCompressor()
|
|
|
+ && value.length > H5_RECOMPRESS_MIN_LENGTH
|
|
|
+ && value.startsWith('data:image/')
|
|
|
+ && !value.startsWith('data:image/webp')
|
|
|
+}
|
|
|
+
|
|
|
+function recompressCachedDataUrl(dataUrl) {
|
|
|
+ const value = String(dataUrl || '')
|
|
|
+ return compressImageSourceToDataUrl(value, value)
|
|
|
+}
|
|
|
+
|
|
|
function downloadAsBase64(url) {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
uni.request({
|
|
|
url,
|
|
|
responseType: 'arraybuffer',
|
|
|
- success: (res) => {
|
|
|
+ success: async (res) => {
|
|
|
if (res.statusCode === 200 && res.data) {
|
|
|
- const base64 = arrayBufferToBase64(res.data)
|
|
|
- resolve('data:image/png;base64,' + base64)
|
|
|
+ try {
|
|
|
+ const mimeType = getImageMimeType(res)
|
|
|
+ const base64 = await compressImageBufferToDataUrl(res.data, mimeType)
|
|
|
+ resolve(base64)
|
|
|
+ } catch (e) {
|
|
|
+ reject(e)
|
|
|
+ }
|
|
|
} else {
|
|
|
reject(new Error('request failed: ' + res.statusCode))
|
|
|
}
|
|
|
@@ -45,6 +196,267 @@ function downloadAsBase64(url) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+function formatError(err) {
|
|
|
+ if (err == null) return 'unknown error'
|
|
|
+ if (typeof err === 'string') return err
|
|
|
+ if (err instanceof Error && err.message) return err.message
|
|
|
+ if (typeof err.errMsg === 'string' && err.errMsg) return err.errMsg
|
|
|
+ if (typeof err.message === 'string' && err.message) return err.message
|
|
|
+ try {
|
|
|
+ return JSON.stringify(err)
|
|
|
+ } catch (e) {
|
|
|
+ return String(err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function canUsePlusIo() {
|
|
|
+ return isAppCacheEnabled() && typeof plus !== 'undefined' && plus.io
|
|
|
+}
|
|
|
+
|
|
|
+function normalizePlusPath(path) {
|
|
|
+ const value = String(path || '').trim()
|
|
|
+ if (!value) return ''
|
|
|
+ if (/^file:\/\//i.test(value)) return value
|
|
|
+ if (/^_(doc|downloads|www)/i.test(value)) return value
|
|
|
+ if (/^\//.test(value)) return 'file://' + value
|
|
|
+ return value
|
|
|
+}
|
|
|
+
|
|
|
+function buildPathResolveCandidates(path) {
|
|
|
+ const raw = String(path || '').trim()
|
|
|
+ const set = new Set()
|
|
|
+ const add = (value) => {
|
|
|
+ const s = String(value || '').trim()
|
|
|
+ if (s) set.add(s)
|
|
|
+ }
|
|
|
+
|
|
|
+ add(normalizePlusPath(raw))
|
|
|
+ add(raw)
|
|
|
+ if (canUsePlusIo() && typeof plus.io.convertLocalFileSystemURL === 'function') {
|
|
|
+ try {
|
|
|
+ if (/^_(doc|downloads|www)/i.test(raw)) {
|
|
|
+ const converted = plus.io.convertLocalFileSystemURL(raw)
|
|
|
+ add(converted)
|
|
|
+ add(normalizePlusPath(converted))
|
|
|
+ }
|
|
|
+ const noProtocol = raw.replace(/^file:\/\//i, '')
|
|
|
+ if (noProtocol !== raw && /^(?:\/|_)/.test(noProtocol)) {
|
|
|
+ const converted = plus.io.convertLocalFileSystemURL(noProtocol)
|
|
|
+ add(converted)
|
|
|
+ add(normalizePlusPath(converted))
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ }
|
|
|
+ return Array.from(set)
|
|
|
+}
|
|
|
+
|
|
|
+function savedFileExists(filePath) {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ if (typeof uni.getSavedFileInfo !== 'function') {
|
|
|
+ resolve(false)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.getSavedFileInfo({
|
|
|
+ filePath,
|
|
|
+ success: () => resolve(true),
|
|
|
+ fail: () => resolve(false)
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function appFileExists(filePath) {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const raw = String(filePath || '').trim()
|
|
|
+ if (!isAppCacheEnabled() || !raw) {
|
|
|
+ resolve(false)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!canUsePlusIo()) {
|
|
|
+ savedFileExists(raw).then(resolve)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const candidates = buildPathResolveCandidates(raw)
|
|
|
+ let index = 0
|
|
|
+ const tryNext = () => {
|
|
|
+ if (index >= candidates.length) {
|
|
|
+ savedFileExists(raw).then(resolve)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ plus.io.resolveLocalFileSystemURL(
|
|
|
+ candidates[index++],
|
|
|
+ (entry) => resolve(!!(entry && entry.isFile)),
|
|
|
+ () => tryNext()
|
|
|
+ )
|
|
|
+ }
|
|
|
+ tryNext()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function unlinkAppFileIfExists(filePath) {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const raw = String(filePath || '').trim()
|
|
|
+ if (!canUsePlusIo() || !raw) {
|
|
|
+ resolve()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ plus.io.resolveLocalFileSystemURL(
|
|
|
+ normalizePlusPath(raw),
|
|
|
+ (entry) => {
|
|
|
+ if (entry && entry.isFile) {
|
|
|
+ entry.remove(() => resolve(), () => resolve())
|
|
|
+ } else {
|
|
|
+ resolve()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ () => resolve()
|
|
|
+ )
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function ensureAppIconDir() {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ if (!canUsePlusIo()) {
|
|
|
+ reject(new Error('plus.io unavailable'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ plus.io.resolveLocalFileSystemURL(
|
|
|
+ '_doc',
|
|
|
+ (rootEntry) => {
|
|
|
+ rootEntry.getDirectory(
|
|
|
+ APP_ICON_DIR_NAME,
|
|
|
+ { create: true },
|
|
|
+ resolve,
|
|
|
+ (e) => reject(new Error(formatError(e) || 'create icon cache dir failed'))
|
|
|
+ )
|
|
|
+ },
|
|
|
+ (e) => reject(new Error(formatError(e) || 'resolve _doc failed'))
|
|
|
+ )
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function hashIconKey(key) {
|
|
|
+ const value = String(key || '')
|
|
|
+ let hash = 5381
|
|
|
+ for (let i = 0; i < value.length; i++) {
|
|
|
+ hash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0
|
|
|
+ }
|
|
|
+ return hash.toString(36)
|
|
|
+}
|
|
|
+
|
|
|
+function getIconExtension(iconUrl) {
|
|
|
+ try {
|
|
|
+ const clean = String(iconUrl || '').split('?')[0].split('#')[0]
|
|
|
+ const match = clean.match(/\.([a-zA-Z0-9]{2,5})$/)
|
|
|
+ const ext = match ? match[1].toLowerCase() : ''
|
|
|
+ if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'].includes(ext)) return ext
|
|
|
+ } catch (e) {}
|
|
|
+ return 'png'
|
|
|
+}
|
|
|
+
|
|
|
+function getAppIconFileName(iconObjectKey, iconUrl) {
|
|
|
+ const key = String(iconObjectKey || '')
|
|
|
+ const suffix = key.replace(/[^a-zA-Z0-9_-]/g, '_').slice(-36)
|
|
|
+ const stem = suffix ? hashIconKey(key) + '-' + suffix : hashIconKey(key)
|
|
|
+ return stem + '.' + getIconExtension(iconUrl)
|
|
|
+}
|
|
|
+
|
|
|
+function getAppIconPath(iconObjectKey, iconUrl) {
|
|
|
+ return APP_ICON_CACHE_DIR + '/' + getAppIconFileName(iconObjectKey, iconUrl)
|
|
|
+}
|
|
|
+
|
|
|
+function downloadToTempFile(iconUrl) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.downloadFile({
|
|
|
+ url: iconUrl,
|
|
|
+ success: (res) => {
|
|
|
+ if (res.statusCode === 200 && res.tempFilePath) {
|
|
|
+ resolve(res.tempFilePath)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ reject(new Error('download failed: ' + (res.statusCode || 'unknown')))
|
|
|
+ },
|
|
|
+ fail: reject
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function copyTempFileToAppCache(tempFilePath, destPath) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ if (!canUsePlusIo()) {
|
|
|
+ reject(new Error('plus.io unavailable'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const fileName = destPath.substring(destPath.lastIndexOf('/') + 1)
|
|
|
+ plus.io.resolveLocalFileSystemURL(
|
|
|
+ normalizePlusPath(tempFilePath),
|
|
|
+ (srcEntry) => {
|
|
|
+ if (!srcEntry || !srcEntry.isFile) {
|
|
|
+ reject(new Error('temp icon file invalid'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ensureAppIconDir()
|
|
|
+ .then((dirEntry) => {
|
|
|
+ srcEntry.copyTo(
|
|
|
+ dirEntry,
|
|
|
+ fileName,
|
|
|
+ () => resolve(destPath),
|
|
|
+ (e) => reject(new Error(formatError(e) || 'copy icon file failed'))
|
|
|
+ )
|
|
|
+ })
|
|
|
+ .catch(reject)
|
|
|
+ },
|
|
|
+ (e) => reject(new Error(formatError(e) || 'resolve temp icon file failed'))
|
|
|
+ )
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function saveTempFile(tempFilePath) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ if (typeof uni.saveFile !== 'function') {
|
|
|
+ reject(new Error('uni.saveFile unavailable'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.saveFile({
|
|
|
+ tempFilePath,
|
|
|
+ success: (res) => resolve(res.savedFilePath || res.filePath || ''),
|
|
|
+ fail: reject
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+async function downloadAndCacheH5Icon(iconUrl, iconObjectKey) {
|
|
|
+ const LOG = '[IconCache]'
|
|
|
+ console.log(LOG + ' H5下载并缓存(base64):', iconObjectKey)
|
|
|
+ const base64 = await downloadAsBase64(iconUrl)
|
|
|
+ console.log(LOG + ' H5缓存完成:', iconObjectKey, base64.substring(0, 50) + '...')
|
|
|
+ return base64
|
|
|
+}
|
|
|
+
|
|
|
+async function downloadAndCacheAppIcon(iconUrl, iconObjectKey) {
|
|
|
+ const LOG = '[IconCache]'
|
|
|
+ const localPath = getAppIconPath(iconObjectKey, iconUrl)
|
|
|
+ console.log(LOG + ' App下载并缓存文件:', iconObjectKey)
|
|
|
+ const tempFilePath = await downloadToTempFile(iconUrl)
|
|
|
+
|
|
|
+ try {
|
|
|
+ await unlinkAppFileIfExists(localPath)
|
|
|
+ const copiedPath = await copyTempFileToAppCache(tempFilePath, localPath)
|
|
|
+ console.log(LOG + ' App缓存完成:', iconObjectKey, '->', copiedPath)
|
|
|
+ return copiedPath
|
|
|
+ } catch (e) {
|
|
|
+ console.warn(LOG + ' App文件缓存失败,尝试 saveFile:', iconObjectKey, e)
|
|
|
+ const savedPath = await saveTempFile(tempFilePath)
|
|
|
+ if (savedPath) {
|
|
|
+ console.log(LOG + ' App缓存完成(saveFile):', iconObjectKey, '->', savedPath)
|
|
|
+ return savedPath
|
|
|
+ }
|
|
|
+ throw new Error('save icon file failed')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 下载图标并缓存
|
|
|
* @param {string} iconUrl - 远程图标 URL
|
|
|
@@ -53,129 +465,141 @@ function downloadAsBase64(url) {
|
|
|
*/
|
|
|
export async function downloadAndCacheIcon(iconUrl, iconObjectKey) {
|
|
|
const LOG = '[IconCache]'
|
|
|
+ const url = String(iconUrl || '').trim()
|
|
|
+ const key = String(iconObjectKey || '').trim()
|
|
|
+ if (!url || !key) return ''
|
|
|
|
|
|
- if (!hasFileSystem()) {
|
|
|
+ const pendingKey = getCacheRuntime() + ':' + key
|
|
|
+ if (pendingIconDownloads.has(pendingKey)) {
|
|
|
+ return pendingIconDownloads.get(pendingKey)
|
|
|
+ }
|
|
|
+
|
|
|
+ const task = (async () => {
|
|
|
try {
|
|
|
- console.log(LOG + ' H5下载并缓存(base64):', iconObjectKey)
|
|
|
- const base64 = await downloadAsBase64(iconUrl)
|
|
|
- console.log(LOG + ' H5缓存完成:', iconObjectKey, base64.substring(0, 50) + '...')
|
|
|
- return base64
|
|
|
+ if (isH5CacheEnabled()) return await downloadAndCacheH5Icon(url, key)
|
|
|
+ if (isAppCacheEnabled()) return await downloadAndCacheAppIcon(url, key)
|
|
|
} catch (e) {
|
|
|
- console.error(LOG + ' H5下载异常:', iconObjectKey, e)
|
|
|
+ console.error(LOG + ' 下载异常:', key, e)
|
|
|
}
|
|
|
return ''
|
|
|
- }
|
|
|
+ })()
|
|
|
|
|
|
- const CACHE_DIR = '_app_icons_'
|
|
|
+ pendingIconDownloads.set(pendingKey, task)
|
|
|
try {
|
|
|
- const fs = uni.getFileSystemManager()
|
|
|
- try { fs.accessSync(CACHE_DIR) } catch (e) { fs.mkdirSync(CACHE_DIR) }
|
|
|
-
|
|
|
- const localPath = CACHE_DIR + '/' + encodeURIComponent(iconObjectKey) + '.png'
|
|
|
- try { fs.unlinkSync(localPath) } catch (e) {}
|
|
|
-
|
|
|
- console.log(LOG + ' 开始下载图标:', iconObjectKey)
|
|
|
- const res = await uni.downloadFile({ url: iconUrl })
|
|
|
- if (res.statusCode === 200) {
|
|
|
- fs.copyFileSync(res.tempFilePath, localPath)
|
|
|
- console.log(LOG + ' 下载完成并缓存:', iconObjectKey, '->', localPath)
|
|
|
- return localPath
|
|
|
- } else {
|
|
|
- console.warn(LOG + ' 下载失败, statusCode:', res.statusCode, iconObjectKey)
|
|
|
+ return await task
|
|
|
+ } finally {
|
|
|
+ pendingIconDownloads.delete(pendingKey)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function applyIconPath(items, iconPath) {
|
|
|
+ for (const item of items) {
|
|
|
+ item.iconPath = iconPath
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function getCacheGroups(items) {
|
|
|
+ const groups = new Map()
|
|
|
+ for (const item of Array.isArray(items) ? items : []) {
|
|
|
+ if (!item || !item.iconObjectKey) continue
|
|
|
+ const key = String(item.iconObjectKey)
|
|
|
+ if (!key) continue
|
|
|
+
|
|
|
+ const useRealIcon = item.useRealIcon !== false
|
|
|
+ const iconUrl = String(item.iconUrl || item.remoteIconUrl || '').trim()
|
|
|
+ if (!useRealIcon && !iconUrl) continue
|
|
|
+
|
|
|
+ if (!groups.has(key)) {
|
|
|
+ groups.set(key, { key, iconUrl, items: [] })
|
|
|
}
|
|
|
- } catch (e) {
|
|
|
- console.error(LOG + ' 下载异常:', iconObjectKey, e)
|
|
|
+ const group = groups.get(key)
|
|
|
+ if (!group.iconUrl && iconUrl) group.iconUrl = iconUrl
|
|
|
+ group.items.push(item)
|
|
|
}
|
|
|
- return ''
|
|
|
+ return Array.from(groups.values())
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 为一批图标处理缓存:命中则使用缓存,未命中则下载
|
|
|
* @param {Array} items - 待处理列表,每项需含 iconUrl / iconObjectKey / useRealIcon
|
|
|
- * @param {Function} onUpdate - 全部下载完成后回调,用于触发 UI 刷新
|
|
|
+ * @param {Function} onUpdate - 缓存命中或下载完成后回调,用于触发 UI 刷新
|
|
|
*/
|
|
|
-export function processIconCache(items, onUpdate) {
|
|
|
+export async function processIconCache(items, onUpdate) {
|
|
|
const LOG = '[IconCache]'
|
|
|
+ const groups = getCacheGroups(items)
|
|
|
+ if (groups.length === 0) return
|
|
|
+ if (!isH5CacheEnabled() && !isAppCacheEnabled()) return
|
|
|
+
|
|
|
const cacheMap = getIconCacheMap()
|
|
|
- const newCacheMap = {}
|
|
|
+ const newCacheMap = { ...cacheMap }
|
|
|
const iconDownloadTasks = []
|
|
|
+ let shouldNotifyUpdate = false
|
|
|
+ let shouldSaveMap = false
|
|
|
|
|
|
- for (const item of items) {
|
|
|
- if (!item.useRealIcon || !item.iconObjectKey) {
|
|
|
- if (!item.useRealIcon) {
|
|
|
- // 没有真实图标的项,不做任何处理
|
|
|
- }
|
|
|
- continue
|
|
|
- }
|
|
|
+ for (const group of groups) {
|
|
|
+ const key = group.key
|
|
|
+ const cachedPath = cacheMap[key]
|
|
|
+ newCacheMap[key] = cachedPath || ''
|
|
|
|
|
|
- const key = String(item.iconObjectKey)
|
|
|
- newCacheMap[key] = cacheMap[key] || ''
|
|
|
-
|
|
|
- if (!hasFileSystem()) {
|
|
|
- // H5:检查缓存值是否为 base64 data URL
|
|
|
- const isBase64 = cacheMap[key] && String(cacheMap[key]).startsWith('data:image/')
|
|
|
- if (isBase64) {
|
|
|
- item.iconPath = cacheMap[key]
|
|
|
- console.log(LOG + ' H5缓存命中:', key)
|
|
|
- } else {
|
|
|
- if (cacheMap[key]) {
|
|
|
- console.log(LOG + ' H5缓存值无效(非base64),重新下载:', key)
|
|
|
- } else {
|
|
|
- console.log(LOG + ' H5首次下载:', key)
|
|
|
- }
|
|
|
+ if (isH5CacheEnabled() && cachedPath && String(cachedPath).startsWith('data:image/')) {
|
|
|
+ applyIconPath(group.items, cachedPath)
|
|
|
+ shouldNotifyUpdate = true
|
|
|
+ if (shouldRecompressCachedDataUrl(cachedPath)) {
|
|
|
+ console.log(LOG + ' H5缓存较大,重新压缩:', key)
|
|
|
iconDownloadTasks.push(
|
|
|
- downloadAndCacheIcon(item.iconUrl || item.iconPath, key).then(result => {
|
|
|
+ recompressCachedDataUrl(cachedPath).then((result) => {
|
|
|
if (result) {
|
|
|
- item.iconPath = result
|
|
|
+ applyIconPath(group.items, result)
|
|
|
newCacheMap[key] = result
|
|
|
+ shouldSaveMap = true
|
|
|
}
|
|
|
})
|
|
|
)
|
|
|
}
|
|
|
- } else if (cacheMap[key]) {
|
|
|
- // App:检查缓存文件是否存在
|
|
|
- let fileExists = false
|
|
|
- try {
|
|
|
- const fs = uni.getFileSystemManager()
|
|
|
- fs.accessSync(cacheMap[key])
|
|
|
- fileExists = true
|
|
|
- } catch (e) {}
|
|
|
-
|
|
|
- if (fileExists) {
|
|
|
- item.iconPath = cacheMap[key]
|
|
|
- console.log(LOG + ' 命中缓存:', key)
|
|
|
- } else {
|
|
|
- console.log(LOG + ' 缓存文件被清理,重新下载:', key)
|
|
|
- iconDownloadTasks.push(
|
|
|
- downloadAndCacheIcon(item.iconUrl || item.iconPath, key).then(result => {
|
|
|
- if (result) {
|
|
|
- item.iconPath = result
|
|
|
- newCacheMap[key] = result
|
|
|
- }
|
|
|
- })
|
|
|
- )
|
|
|
+ console.log(LOG + ' H5缓存命中:', key)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isAppCacheEnabled() && cachedPath && isAppLocalIconPath(cachedPath)) {
|
|
|
+ if (await appFileExists(cachedPath)) {
|
|
|
+ applyIconPath(group.items, cachedPath)
|
|
|
+ shouldNotifyUpdate = true
|
|
|
+ console.log(LOG + ' App缓存命中:', key)
|
|
|
+ continue
|
|
|
}
|
|
|
- } else if (hasFileSystem()) {
|
|
|
- console.log(LOG + ' 首次下载:', key)
|
|
|
- iconDownloadTasks.push(
|
|
|
- downloadAndCacheIcon(item.iconUrl || item.iconPath, key).then(result => {
|
|
|
- if (result) {
|
|
|
- item.iconPath = result
|
|
|
- newCacheMap[key] = result
|
|
|
- }
|
|
|
- })
|
|
|
- )
|
|
|
+ console.log(LOG + ' App缓存文件丢失,重新下载:', key)
|
|
|
+ delete newCacheMap[key]
|
|
|
+ shouldSaveMap = true
|
|
|
+ } else if (cachedPath) {
|
|
|
+ console.log(LOG + ' 缓存值不适用于当前端,重新下载:', key)
|
|
|
+ delete newCacheMap[key]
|
|
|
+ shouldSaveMap = true
|
|
|
}
|
|
|
+
|
|
|
+ if (!group.iconUrl) continue
|
|
|
+ console.log(LOG + ' 首次下载:', key)
|
|
|
+ iconDownloadTasks.push(
|
|
|
+ downloadAndCacheIcon(group.iconUrl, key).then((result) => {
|
|
|
+ if (result) {
|
|
|
+ applyIconPath(group.items, result)
|
|
|
+ newCacheMap[key] = result
|
|
|
+ shouldSaveMap = true
|
|
|
+ }
|
|
|
+ })
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
if (iconDownloadTasks.length > 0) {
|
|
|
console.log(LOG + ' 共 ' + iconDownloadTasks.length + ' 个图标需要下载')
|
|
|
- Promise.all(iconDownloadTasks).then(() => {
|
|
|
- setIconCacheMap(newCacheMap)
|
|
|
- if (onUpdate) onUpdate()
|
|
|
- console.log(LOG + ' 全部下载完成,UI已刷新')
|
|
|
- })
|
|
|
- } else {
|
|
|
- console.log(LOG + ' 所有图标均已缓存,无需下载')
|
|
|
+ if (shouldNotifyUpdate && onUpdate) onUpdate()
|
|
|
+ await Promise.all(iconDownloadTasks)
|
|
|
+ setIconCacheMap(newCacheMap)
|
|
|
+ if (onUpdate) onUpdate()
|
|
|
+ console.log(LOG + ' 全部下载完成,UI已刷新')
|
|
|
+ return
|
|
|
}
|
|
|
+
|
|
|
+ if (shouldSaveMap) setIconCacheMap(newCacheMap)
|
|
|
+ if (shouldNotifyUpdate && onUpdate) onUpdate()
|
|
|
+ console.log(LOG + ' 所有图标均已缓存,无需下载')
|
|
|
}
|