topfans/frontend/utils/resource-manager.js
2026-04-07 23:08:49 +08:00

305 lines
7.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 资源管理器 - 处理图片预加载、重试和错误处理
*
* 功能:
* - 图片预加载
* - 自动重试机制 (最多3次)
* - 加载状态跟踪
* - 错误处理和降级
* - 占位符支持
*/
const DEFAULT_RETRY_COUNT = 3
const DEFAULT_TIMEOUT = 10000 // 10秒超时
// 使用 data URI 作为占位符 - 一个简单的灰色方块
const PLACEHOLDER_IMAGE = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iI2U1ZTVlNSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IiM5OTkiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7lm77niYfkuI3lj6/nlKg8L3RleHQ+PC9zdmc+'
/**
* 资源加载状态
*/
const LoadStatus = {
PENDING: 'pending',
LOADING: 'loading',
SUCCESS: 'success',
FAILED: 'failed',
TIMEOUT: 'timeout'
}
/**
* 资源缓存
*/
const resourceCache = new Map()
/**
* 预加载单个图片资源
* @param {string} src - 图片路径
* @param {Object} options - 配置选项
* @param {number} options.maxRetries - 最大重试次数
* @param {number} options.timeout - 超时时间(毫秒)
* @param {boolean} options.usePlaceholder - 失败时是否使用占位符
* @returns {Promise<Object>} 加载结果
*/
export async function preloadImage(src, options = {}) {
const {
maxRetries = DEFAULT_RETRY_COUNT,
timeout = DEFAULT_TIMEOUT,
usePlaceholder = true
} = options
// 检查缓存
if (resourceCache.has(src)) {
const cached = resourceCache.get(src)
if (cached.status === LoadStatus.SUCCESS) {
console.log(`[资源管理器] 使用缓存: ${src}`)
return cached
}
}
let lastError = null
let retryCount = 0
// 重试循环
while (retryCount <= maxRetries) {
try {
console.log(`[资源管理器] 加载资源 (尝试 ${retryCount + 1}/${maxRetries + 1}): ${src}`)
const result = await loadImageWithTimeout(src, timeout)
// 加载成功,缓存结果
const successResult = {
src,
status: LoadStatus.SUCCESS,
width: result.width,
height: result.height,
path: result.path,
retryCount,
timestamp: Date.now()
}
resourceCache.set(src, successResult)
console.log(`[资源管理器] 加载成功: ${src}`)
return successResult
} catch (error) {
lastError = error
retryCount++
console.warn(`[资源管理器] 加载失败 (尝试 ${retryCount}/${maxRetries + 1}): ${src}`, error)
// 如果还有重试机会,等待后重试
if (retryCount <= maxRetries) {
const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000) // 指数退避最多5秒
console.log(`[资源管理器] ${delay}ms 后重试...`)
await sleep(delay)
}
}
}
// 所有重试都失败
console.error(`[资源管理器] 资源加载最终失败: ${src}`, lastError)
const failedResult = {
src,
status: LoadStatus.FAILED,
error: lastError,
retryCount,
timestamp: Date.now(),
placeholder: usePlaceholder ? PLACEHOLDER_IMAGE : null
}
resourceCache.set(src, failedResult)
// 如果启用占位符,返回占位符路径
if (usePlaceholder) {
return {
...failedResult,
src: PLACEHOLDER_IMAGE,
usedPlaceholder: true
}
}
return failedResult
}
/**
* 带超时的图片加载
* @param {string} src - 图片路径
* @param {number} timeout - 超时时间
* @returns {Promise<Object>}
*/
function loadImageWithTimeout(src, timeout) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`加载超时: ${src}`))
}, timeout)
uni.getImageInfo({
src: src,
success: (res) => {
clearTimeout(timer)
resolve(res)
},
fail: (error) => {
clearTimeout(timer)
reject(error)
}
})
})
}
/**
* 批量预加载图片资源
* @param {Array<string>} sources - 图片路径数组
* @param {Object} options - 配置选项
* @param {Function} options.onProgress - 进度回调
* @param {boolean} options.parallel - 是否并行加载
* @returns {Promise<Array>} 加载结果数组
*/
export async function preloadImages(sources, options = {}) {
const {
onProgress = null,
parallel = true,
...imageOptions
} = options
if (!sources || sources.length === 0) {
return []
}
console.log(`[资源管理器] 开始批量加载 ${sources.length} 个资源`)
const results = []
let loadedCount = 0
if (parallel) {
// 并行加载
const promises = sources.map(async (src) => {
const result = await preloadImage(src, imageOptions)
loadedCount++
if (onProgress) {
onProgress({
loaded: loadedCount,
total: sources.length,
progress: (loadedCount / sources.length) * 100,
currentSrc: src,
currentResult: result
})
}
return result
})
const settled = await Promise.allSettled(promises)
results.push(...settled.map(r => r.status === 'fulfilled' ? r.value : r.reason))
} else {
// 串行加载
for (const src of sources) {
const result = await preloadImage(src, imageOptions)
results.push(result)
loadedCount++
if (onProgress) {
onProgress({
loaded: loadedCount,
total: sources.length,
progress: (loadedCount / sources.length) * 100,
currentSrc: src,
currentResult: result
})
}
}
}
const successCount = results.filter(r => r.status === LoadStatus.SUCCESS).length
const failedCount = results.filter(r => r.status === LoadStatus.FAILED).length
console.log(`[资源管理器] 批量加载完成: 成功 ${successCount}, 失败 ${failedCount}`)
return results
}
/**
* 预加载活动配置相关的所有资源
* @param {Object} config - 活动配置对象
* @param {Object} options - 配置选项
* @returns {Promise<Object>} 加载结果统计
*/
export async function preloadActivityAssets(config, options = {}) {
if (!config) {
throw new Error('配置对象不能为空')
}
// 收集所有需要预加载的资源
const sources = [
config.bgImage,
config.bannerImage,
config.mainAssetIdle,
config.mainAssetActive
].filter(Boolean) // 过滤掉 null/undefined
console.log(`[资源管理器] 预加载活动资源: ${config.title}`)
const results = await preloadImages(sources, options)
// 统计结果
const stats = {
total: results.length,
success: results.filter(r => r.status === LoadStatus.SUCCESS).length,
failed: results.filter(r => r.status === LoadStatus.FAILED).length,
usedPlaceholder: results.filter(r => r.usedPlaceholder).length,
results
}
return stats
}
/**
* 清除资源缓存
* @param {string} src - 可选,指定要清除的资源路径
*/
export function clearResourceCache(src = null) {
if (src) {
resourceCache.delete(src)
console.log(`[资源管理器] 清除缓存: ${src}`)
} else {
resourceCache.clear()
console.log(`[资源管理器] 清除所有缓存`)
}
}
/**
* 获取资源缓存状态
* @param {string} src - 资源路径
* @returns {Object|null}
*/
export function getResourceStatus(src) {
return resourceCache.get(src) || null
}
/**
* 检查资源是否已加载
* @param {string} src - 资源路径
* @returns {boolean}
*/
export function isResourceLoaded(src) {
const status = resourceCache.get(src)
return status && status.status === LoadStatus.SUCCESS
}
/**
* 辅助函数: 延迟
* @param {number} ms - 毫秒
* @returns {Promise}
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* 导出常量
*/
export { LoadStatus, PLACEHOLDER_IMAGE }