地址路径修改
This commit is contained in:
209
miniprogram/utils/avatarCache.ts
Normal file
209
miniprogram/utils/avatarCache.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 头像缓存管理工具类
|
||||
* 实现头像图片的KV存储和缓存机制,避免重复下载相同图片
|
||||
*/
|
||||
|
||||
class AvatarCache {
|
||||
private static instance: AvatarCache;
|
||||
private cache: Map<string, string> = new Map(); // KV存储:avatarUrl -> 本地图片路径
|
||||
private downloadingUrls: Set<string> = new Set(); // 正在下载的URL集合
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): AvatarCache {
|
||||
if (!AvatarCache.instance) {
|
||||
AvatarCache.instance = new AvatarCache();
|
||||
}
|
||||
return AvatarCache.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取头像图片路径
|
||||
* @param avatarUrl 头像URL
|
||||
* @returns 本地图片路径或Promise<string>
|
||||
*/
|
||||
public async getAvatarPath(avatarUrl: string): Promise<string> {
|
||||
if (!avatarUrl) {
|
||||
return this.getDefaultAvatarPath();
|
||||
}
|
||||
|
||||
// 如果缓存中已存在,直接返回
|
||||
if (this.cache.has(avatarUrl)) {
|
||||
return this.cache.get(avatarUrl)!;
|
||||
}
|
||||
|
||||
// 如果正在下载中,等待下载完成
|
||||
if (this.downloadingUrls.has(avatarUrl)) {
|
||||
return await this.waitForDownload(avatarUrl);
|
||||
}
|
||||
|
||||
// 开始下载图片
|
||||
return await this.downloadAndCacheAvatar(avatarUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取头像URL(用于存储和传递,不下载图片)
|
||||
* @param userId 用户ID
|
||||
* @param avatarUrl 头像URL
|
||||
* @returns 头像URL
|
||||
*/
|
||||
public getAvatarUrl(avatarUrl: string): string {
|
||||
if (!avatarUrl) {
|
||||
return '/images/user-avatar.png';
|
||||
}
|
||||
|
||||
// 如果缓存中已存在,直接返回URL(不下载)
|
||||
if (this.cache.has(avatarUrl)) {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
// 如果不在缓存中,异步下载但不等待结果
|
||||
if (!this.downloadingUrls.has(avatarUrl)) {
|
||||
this.downloadAndCacheAvatar(avatarUrl).catch(() => {
|
||||
// 下载失败不影响主流程
|
||||
});
|
||||
}
|
||||
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载并缓存头像图片
|
||||
* @param avatarUrl 头像URL
|
||||
* @returns 本地图片路径
|
||||
*/
|
||||
private async downloadAndCacheAvatar(avatarUrl: string): Promise<string> {
|
||||
this.downloadingUrls.add(avatarUrl);
|
||||
|
||||
try {
|
||||
// 下载图片到临时文件
|
||||
const tempFilePath = await this.downloadImage(avatarUrl);
|
||||
|
||||
// 缓存到KV存储
|
||||
this.cache.set(avatarUrl, tempFilePath);
|
||||
|
||||
return tempFilePath;
|
||||
} catch (error) {
|
||||
console.error(`下载头像失败: ${avatarUrl}`, error);
|
||||
return this.getDefaultAvatarPath();
|
||||
} finally {
|
||||
this.downloadingUrls.delete(avatarUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载图片到本地临时文件
|
||||
* @param avatarUrl 头像URL
|
||||
* @returns 本地临时文件路径
|
||||
*/
|
||||
private async downloadImage(avatarUrl: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.downloadFile({
|
||||
url: avatarUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.tempFilePath);
|
||||
} else {
|
||||
reject(new Error(`下载失败,状态码: ${res.statusCode}`));
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待下载完成
|
||||
* @param avatarUrl 头像URL
|
||||
* @returns 本地图片路径
|
||||
*/
|
||||
private async waitForDownload(avatarUrl: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
if (!this.downloadingUrls.has(avatarUrl) && this.cache.has(avatarUrl)) {
|
||||
clearInterval(checkInterval);
|
||||
resolve(this.cache.get(avatarUrl)!);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 设置超时时间
|
||||
setTimeout(() => {
|
||||
clearInterval(checkInterval);
|
||||
resolve(this.getDefaultAvatarPath());
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认头像路径
|
||||
* @returns 默认头像路径
|
||||
*/
|
||||
private getDefaultAvatarPath(): string {
|
||||
return '/images/user-avatar.png';
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载多个头像
|
||||
* @param avatarUrls 头像URL数组
|
||||
*/
|
||||
public async preloadAvatars(avatarUrls: string[]): Promise<void> {
|
||||
const uniqueUrls = [...new Set(avatarUrls)];
|
||||
|
||||
for (const url of uniqueUrls) {
|
||||
if (url && !this.cache.has(url) && !this.downloadingUrls.has(url)) {
|
||||
// 异步预加载,不等待结果
|
||||
this.downloadAndCacheAvatar(url).catch(() => {
|
||||
// 预加载失败不影响主流程
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载多个头像并等待所有加载完成
|
||||
* @param avatarUrls 头像URL数组
|
||||
*/
|
||||
public async preloadAvatarsAndWait(avatarUrls: string[]): Promise<void> {
|
||||
const uniqueUrls = [...new Set(avatarUrls)];
|
||||
const downloadPromises: Promise<string>[] = [];
|
||||
|
||||
for (const url of uniqueUrls) {
|
||||
if (url && !this.cache.has(url) && !this.downloadingUrls.has(url)) {
|
||||
// 同步预加载,等待结果
|
||||
downloadPromises.push(this.downloadAndCacheAvatar(url));
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadPromises.length > 0) {
|
||||
console.log(`🔄 [AvatarCache] 开始预加载 ${downloadPromises.length} 个头像`);
|
||||
await Promise.allSettled(downloadPromises);
|
||||
console.log(`✅ [AvatarCache] 头像预加载完成,缓存大小: ${this.cache.size}`);
|
||||
} else {
|
||||
console.log('ℹ️ [AvatarCache] 没有需要预加载的头像');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public clearCache(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
public getCacheStats(): { size: number; downloading: number } {
|
||||
return {
|
||||
size: this.cache.size,
|
||||
downloading: this.downloadingUrls.size
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const avatarCache = AvatarCache.getInstance();
|
||||
|
||||
export default AvatarCache;
|
||||
353
miniprogram/utils/avatarUtils.ts
Normal file
353
miniprogram/utils/avatarUtils.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
// 头像工具类 - 处理头像相关的功能
|
||||
|
||||
/**
|
||||
* 头像尺寸配置
|
||||
*/
|
||||
export const AvatarSizes = {
|
||||
// 小尺寸 - 用于列表、地图标记点等
|
||||
SMALL: 40,
|
||||
// 中等尺寸 - 用于用户面板、详情页等
|
||||
MEDIUM: 80,
|
||||
// 大尺寸 - 用于个人资料页等
|
||||
LARGE: 120,
|
||||
// 超大尺寸 - 用于编辑页面等
|
||||
XLARGE: 160
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 头像质量配置
|
||||
*/
|
||||
export const AvatarQuality = {
|
||||
// 缩略图质量
|
||||
THUMBNAIL: 60,
|
||||
// 标准质量
|
||||
STANDARD: 80,
|
||||
// 高质量
|
||||
HIGH: 90
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 默认头像配置
|
||||
*/
|
||||
export const DefaultAvatars = {
|
||||
// 默认用户头像
|
||||
USER: '/images/user-avatar.png',
|
||||
// 管理员头像
|
||||
ADMIN: '/images/admin-avatar.png',
|
||||
// 员工头像
|
||||
EMPLOYEE: '/images/employee-avatar.png',
|
||||
// 货运人员头像
|
||||
DELIVERY_PERSON: '/images/delivery-avatar.png'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 头像工具类
|
||||
*/
|
||||
export class AvatarUtils {
|
||||
|
||||
/**
|
||||
* 获取默认头像URL
|
||||
* @param role 用户角色
|
||||
* @returns 默认头像URL
|
||||
*/
|
||||
static getDefaultAvatar(role?: string): string {
|
||||
switch (role) {
|
||||
case 'ADMIN':
|
||||
return DefaultAvatars.ADMIN;
|
||||
case 'DELIVERY_PERSON':
|
||||
return DefaultAvatars.DELIVERY_PERSON;
|
||||
case 'EMPLOYEE':
|
||||
return DefaultAvatars.EMPLOYEE;
|
||||
default:
|
||||
return DefaultAvatars.USER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成头像占位符文本
|
||||
* @param name 用户名
|
||||
* @returns 占位符文本
|
||||
*/
|
||||
static generatePlaceholderText(name?: string): string {
|
||||
if (!name) return '👤';
|
||||
|
||||
// 如果是中文名字,取最后一个字符
|
||||
if (/[\u4e00-\u9fa5]/.test(name)) {
|
||||
return name.charAt(name.length - 1);
|
||||
}
|
||||
|
||||
// 如果是英文名字,取首字母
|
||||
return name.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取头像显示URL
|
||||
* @param avatarUrl 原始头像URL
|
||||
* @param size 目标尺寸
|
||||
* @param quality 图片质量
|
||||
* @returns 处理后的头像URL
|
||||
*/
|
||||
static getAvatarUrl(avatarUrl: string, _size: number = AvatarSizes.MEDIUM, _quality: number = AvatarQuality.STANDARD): string {
|
||||
if (!avatarUrl) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 如果是本地临时文件,直接返回
|
||||
if (avatarUrl.startsWith('http://tmp/') || avatarUrl.startsWith('wxfile://')) {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
// 如果是网络图片,可以添加尺寸参数(如果后端支持)
|
||||
if (avatarUrl.startsWith('http')) {
|
||||
// 这里可以根据后端API添加尺寸参数
|
||||
// 例如:return `${avatarUrl}?width=${size}&quality=${quality}`;
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放头像图片
|
||||
* @param src 原始图片路径
|
||||
* @param width 目标宽度
|
||||
* @param height 目标高度
|
||||
* @returns Promise<string> 缩放后的图片路径
|
||||
*/
|
||||
static async resizeAvatar(src: string, width: number, height: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!src) {
|
||||
reject(new Error('图片路径不能为空'));
|
||||
return;
|
||||
}
|
||||
|
||||
wx.getImageInfo({
|
||||
src,
|
||||
success: (_infoRes) => {
|
||||
const canvasId = `resize-canvas-${Date.now()}`;
|
||||
const ctx = wx.createCanvasContext(canvasId);
|
||||
|
||||
// 绘制缩放后的图片
|
||||
ctx.drawImage(src, 0, 0, width, height);
|
||||
|
||||
ctx.draw(false, () => {
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId,
|
||||
destWidth: width,
|
||||
destHeight: height,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath);
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪头像为圆形
|
||||
* @param src 原始图片路径
|
||||
* @param size 裁剪尺寸
|
||||
* @returns Promise<string> 裁剪后的圆形头像路径
|
||||
*/
|
||||
static async cropAvatarToCircle(src: string, size: number = AvatarSizes.MEDIUM): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!src) {
|
||||
reject(new Error('图片路径不能为空'));
|
||||
return;
|
||||
}
|
||||
|
||||
wx.getImageInfo({
|
||||
src,
|
||||
success: (_infoRes) => {
|
||||
const canvasId = `circle-canvas-${Date.now()}`;
|
||||
const ctx = wx.createCanvasContext(canvasId);
|
||||
|
||||
// 绘制圆形裁剪区域
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI);
|
||||
ctx.clip();
|
||||
|
||||
// 绘制图片
|
||||
ctx.drawImage(src, 0, 0, size, size);
|
||||
ctx.restore();
|
||||
|
||||
ctx.draw(false, () => {
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId,
|
||||
destWidth: size,
|
||||
destHeight: size,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath);
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩头像图片
|
||||
* @param src 原始图片路径
|
||||
* @param quality 压缩质量 (0-100)
|
||||
* @returns Promise<string> 压缩后的图片路径
|
||||
*/
|
||||
static async compressAvatar(src: string, quality: number = AvatarQuality.STANDARD): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.compressImage({
|
||||
src,
|
||||
quality,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath);
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存头像图片
|
||||
* @param avatarUrl 头像URL
|
||||
* @param cacheKey 缓存键
|
||||
* @returns Promise<string> 缓存后的图片路径
|
||||
*/
|
||||
static async cacheAvatar(avatarUrl: string, cacheKey?: string): Promise<string> {
|
||||
if (!avatarUrl) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 如果是本地文件,直接返回
|
||||
if (avatarUrl.startsWith('http://tmp/') || avatarUrl.startsWith('wxfile://')) {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
const key = cacheKey || `avatar_${avatarUrl.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
||||
|
||||
try {
|
||||
// 检查缓存
|
||||
const cachedPath = wx.getStorageSync(key);
|
||||
if (cachedPath) {
|
||||
// 验证缓存文件是否存在
|
||||
try {
|
||||
await this.checkFileExists(cachedPath);
|
||||
return cachedPath;
|
||||
} catch (error) {
|
||||
// 缓存文件不存在,重新下载
|
||||
console.log('缓存文件不存在,重新下载:', avatarUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// 下载头像
|
||||
const downloadRes = await this.downloadAvatar(avatarUrl);
|
||||
|
||||
// 缓存头像路径
|
||||
wx.setStorageSync(key, downloadRes);
|
||||
|
||||
return downloadRes;
|
||||
} catch (error) {
|
||||
console.error('缓存头像失败:', error);
|
||||
return avatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载头像图片
|
||||
* @param url 头像URL
|
||||
* @returns Promise<string> 下载后的临时文件路径
|
||||
*/
|
||||
private static downloadAvatar(url: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.downloadFile({
|
||||
url,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.tempFilePath);
|
||||
} else {
|
||||
reject(new Error(`下载失败,状态码: ${res.statusCode}`));
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
* @param filePath 文件路径
|
||||
* @returns Promise<boolean>
|
||||
*/
|
||||
private static checkFileExists(filePath: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
wx.getFileInfo({
|
||||
filePath,
|
||||
success: () => resolve(true),
|
||||
fail: () => resolve(false)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成头像样式类名
|
||||
* @param size 尺寸
|
||||
* @param shape 形状
|
||||
* @param bordered 是否有边框
|
||||
* @param shadow 是否有阴影
|
||||
* @returns 样式类名字符串
|
||||
*/
|
||||
static generateAvatarClass(size: number, shape: 'circle' | 'square' = 'circle', bordered: boolean = false, shadow: boolean = false): string {
|
||||
const classes = [];
|
||||
|
||||
// 尺寸类
|
||||
if (size <= AvatarSizes.SMALL) {
|
||||
classes.push('avatar-small');
|
||||
} else if (size <= AvatarSizes.MEDIUM) {
|
||||
classes.push('avatar-medium');
|
||||
} else if (size <= AvatarSizes.LARGE) {
|
||||
classes.push('avatar-large');
|
||||
} else {
|
||||
classes.push('avatar-xlarge');
|
||||
}
|
||||
|
||||
// 形状类
|
||||
classes.push(shape === 'circle' ? 'avatar-circle' : 'avatar-square');
|
||||
|
||||
// 边框类
|
||||
if (bordered) {
|
||||
classes.push('avatar-bordered');
|
||||
}
|
||||
|
||||
// 阴影类
|
||||
if (shadow) {
|
||||
classes.push('avatar-shadow');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理头像缓存
|
||||
* @param cacheKey 缓存键(可选,不传则清理所有头像缓存)
|
||||
*/
|
||||
static clearAvatarCache(cacheKey?: string) {
|
||||
if (cacheKey) {
|
||||
wx.removeStorageSync(cacheKey);
|
||||
} else {
|
||||
// 清理所有以 avatar_ 开头的缓存
|
||||
const storageInfo = wx.getStorageInfoSync();
|
||||
storageInfo.keys.forEach(key => {
|
||||
if (key.startsWith('avatar_')) {
|
||||
wx.removeStorageSync(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AvatarUtils;
|
||||
Reference in New Issue
Block a user