Browse Source

Update 优化前端资源展示的组件,重构用户头像(修改为字符串)

获取资源在统一应地方utils\resource.ts
Yue 2 days ago
parent
commit
37eb93740e
18 changed files with 153 additions and 104 deletions
  1. 1 1
      SERVER/VberAdminPlusV3/.script/docker/sql/init.sql
  2. 1 1
      SERVER/VberAdminPlusV3/.script/sql/admin.sql
  3. 2 2
      SERVER/VberAdminPlusV3/vber-admin/src/main/resources/application-dev.yml
  4. 3 3
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/controller/system/SysProfileController.java
  5. 1 1
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/domain/SysUser.java
  6. 1 2
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/domain/vo/ProfileUserVo.java
  7. 1 2
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/domain/vo/SysUserVo.java
  8. 1 0
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/ISysOssService.java
  9. 1 1
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/ISysUserService.java
  10. 6 5
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/impl/SysOssServiceImpl.java
  11. 1 1
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/impl/SysUserServiceImpl.java
  12. 9 15
      UI/VAP_V3.VUE/src/components/symbol/VbSymbol.vue
  13. 7 27
      UI/VAP_V3.VUE/src/components/upload/VbImagePreview.vue
  14. 5 26
      UI/VAP_V3.VUE/src/components/upload/VbOfficePreview.vue
  15. 103 0
      UI/VAP_V3.VUE/src/core/utils/resource.ts
  16. 1 6
      UI/VAP_V3.VUE/src/layouts/main/header/navbar/UserAccountMenu.vue
  17. 8 9
      UI/VAP_V3.VUE/src/views/account/profile/_avatar.vue
  18. 1 2
      UI/VAP_V3.VUE/src/views/system/oss/index.vue

+ 1 - 1
SERVER/VberAdminPlusV3/.script/docker/sql/init.sql

@@ -44,7 +44,7 @@ CREATE TABLE sys_user
     email       VARCHAR(50)  DEFAULT '' COMMENT '用户邮箱',
     email       VARCHAR(50)  DEFAULT '' COMMENT '用户邮箱',
     phonenumber VARCHAR(11)  DEFAULT '' COMMENT '手机号码',
     phonenumber VARCHAR(11)  DEFAULT '' COMMENT '手机号码',
     sex         CHAR(1)      DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
     sex         CHAR(1)      DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
-    avatar      BIGINT(20) COMMENT '头像地址',
+    avatar      VARCHAR(50) COMMENT '头像地址',
     password    VARCHAR(100) DEFAULT '' COMMENT '密码',
     password    VARCHAR(100) DEFAULT '' COMMENT '密码',
     status      CHAR(1)      DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
     status      CHAR(1)      DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
     del_flag    CHAR(1)      DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
     del_flag    CHAR(1)      DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',

+ 1 - 1
SERVER/VberAdminPlusV3/.script/sql/admin.sql

@@ -44,7 +44,7 @@ CREATE TABLE sys_user
     email       VARCHAR(50)  DEFAULT '' COMMENT '用户邮箱',
     email       VARCHAR(50)  DEFAULT '' COMMENT '用户邮箱',
     phonenumber VARCHAR(11)  DEFAULT '' COMMENT '手机号码',
     phonenumber VARCHAR(11)  DEFAULT '' COMMENT '手机号码',
     sex         CHAR(1)      DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
     sex         CHAR(1)      DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
-    avatar      BIGINT(20) COMMENT '头像地址',
+    avatar      VARCHAR(50) COMMENT '头像地址',
     password    VARCHAR(100) DEFAULT '' COMMENT '密码',
     password    VARCHAR(100) DEFAULT '' COMMENT '密码',
     status      CHAR(1)      DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
     status      CHAR(1)      DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
     del_flag    CHAR(1)      DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
     del_flag    CHAR(1)      DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',

+ 2 - 2
SERVER/VberAdminPlusV3/vber-admin/src/main/resources/application-dev.yml

@@ -23,9 +23,9 @@ spring:
           driverClassName: com.mysql.cj.jdbc.Driver
           driverClassName: com.mysql.cj.jdbc.Driver
           # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
           # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
           # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
           # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
-          url: jdbc:mysql://192.168.0.104:3316/VberAdminPlusV3?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
+          url: jdbc:mysql://192.168.0.104:3317/VberAdminPlusV3DB?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username: root
           username: root
-          password: 123456
+          password: root
         #        # 从库数据源
         #        # 从库数据源
         #        slave:
         #        slave:
         #          lazy: true
         #          lazy: true

+ 3 - 3
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/controller/system/SysProfileController.java

@@ -123,9 +123,9 @@ public class SysProfileController extends BaseController {
                 return R.fail("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式");
                 return R.fail("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式");
             }
             }
             SysOssVo oss = ossService.upload(avatarfile, "avatar");
             SysOssVo oss = ossService.upload(avatarfile, "avatar");
-            String avatar = oss.getUrl();
+            String avatar = oss.getObjectId();
             boolean updateSuccess = DataPermissionHelper
             boolean updateSuccess = DataPermissionHelper
-                    .ignore(() -> userService.updateUserAvatar(LoginHelper.getUserId(), oss.getOssId()));
+                    .ignore(() -> userService.updateUserAvatar(LoginHelper.getUserId(), oss.getObjectId()));
             if (updateSuccess) {
             if (updateSuccess) {
                 return R.ok(new AvatarVo(avatar));
                 return R.ok(new AvatarVo(avatar));
             }
             }
@@ -133,7 +133,7 @@ public class SysProfileController extends BaseController {
         return R.fail("上传图片异常,请联系管理员");
         return R.fail("上传图片异常,请联系管理员");
     }
     }
 
 
-    public record AvatarVo(String imgUrl) {
+    public record AvatarVo(String objectId) {
     }
     }
 
 
     public record ProfileVo(ProfileUserVo user, String roleGroup, String postGroup) {
     public record ProfileVo(ProfileUserVo user, String roleGroup, String postGroup) {

+ 1 - 1
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/domain/SysUser.java

@@ -65,7 +65,7 @@ public class SysUser extends TenantEntity {
     /**
     /**
      * 用户头像
      * 用户头像
      */
      */
-    private Long avatar;
+    private String avatar;
 
 
     /**
     /**
      * 密码
      * 密码

+ 1 - 2
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/domain/vo/ProfileUserVo.java

@@ -68,8 +68,7 @@ public class ProfileUserVo implements Serializable {
     /**
     /**
      * 头像地址
      * 头像地址
      */
      */
-    @Translation(type = TransConstant.OSS_ID_TO_URL)
-    private Long avatar;
+    private String avatar;
 
 
     /**
     /**
      * 最后登录IP
      * 最后登录IP

+ 1 - 2
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/domain/vo/SysUserVo.java

@@ -77,8 +77,7 @@ public class SysUserVo implements Serializable {
     /**
     /**
      * 头像地址
      * 头像地址
      */
      */
-    @Translation(type = TransConstant.OSS_ID_TO_URL)
-    private Long avatar;
+    private String avatar;
 
 
     /**
     /**
      * 密码
      * 密码

+ 1 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/ISysOssService.java

@@ -19,6 +19,7 @@ import java.util.List;
  */
  */
 public interface ISysOssService {
 public interface ISysOssService {
 
 
+
     /**
     /**
      * 查询OSS对象存储列表
      * 查询OSS对象存储列表
      *
      *

+ 1 - 1
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/ISysUserService.java

@@ -201,7 +201,7 @@ public interface ISysUserService {
      * @param avatar 头像地址
      * @param avatar 头像地址
      * @return 结果
      * @return 结果
      */
      */
-    boolean updateUserAvatar(Long userId, Long avatar);
+    boolean updateUserAvatar(Long userId, String avatar);
 
 
     /**
     /**
      * 重置用户密码
      * 重置用户密码

+ 6 - 5
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/impl/SysOssServiceImpl.java

@@ -3,11 +3,11 @@ package com.vber.system.service.impl;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.vber.common.core.constant.CacheNames;
 import com.vber.common.core.domain.dto.OssDTO;
 import com.vber.common.core.domain.dto.OssDTO;
 import com.vber.common.core.exception.ServiceException;
 import com.vber.common.core.exception.ServiceException;
 import com.vber.common.core.service.OssService;
 import com.vber.common.core.service.OssService;
@@ -34,6 +34,7 @@ import com.vber.system.service.ISysOssService;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.ResponseEntity;
@@ -60,7 +61,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
 
 
     private final SysOssMapper baseMapper;
     private final SysOssMapper baseMapper;
 
 
-    private final String[] SysFileSuffix = new String[] { "avatar", "system" };
+    private final String[] SysFileSuffix = new String[]{"avatar", "system"};
 
 
     /**
     /**
      * 查询OSS对象存储列表
      * 查询OSS对象存储列表
@@ -177,7 +178,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
      * @param ossId 文件在数据库中的唯一标识
      * @param ossId 文件在数据库中的唯一标识
      * @return SysOssVo 对象,包含文件信息
      * @return SysOssVo 对象,包含文件信息
      */
      */
-    // @Cacheable(cacheNames = CacheNames.SYS_OSS, key = "#ossId")
+    @Cacheable(cacheNames = CacheNames.SYS_OSS, key = "#ossId")
     @Override
     @Override
     public SysOssVo getById(Long ossId) {
     public SysOssVo getById(Long ossId) {
         return baseMapper.selectVoById(ossId);
         return baseMapper.selectVoById(ossId);
@@ -189,7 +190,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
      * @param objectId 文件唯一标识
      * @param objectId 文件唯一标识
      * @return SysOssVo 对象,包含文件信息
      * @return SysOssVo 对象,包含文件信息
      */
      */
-    // @Cacheable(cacheNames = CacheNames.SYS_OSS, key = "#objectId")
+    @Cacheable(cacheNames = CacheNames.SYS_OSS, key = "#objectId")
     @Override
     @Override
     public SysOssVo getByObjectId(String objectId) {
     public SysOssVo getByObjectId(String objectId) {
         TenantHelper.enableIgnore();
         TenantHelper.enableIgnore();
@@ -340,7 +341,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
 
 
     @NotNull
     @NotNull
     private SysOssVo buildResultEntity(String originalFileName, String suffix, String configKey,
     private SysOssVo buildResultEntity(String originalFileName, String suffix, String configKey,
-            PutObjectResult uploadResult, SysOssExt ext1) {
+                                       PutObjectResult uploadResult, SysOssExt ext1) {
         SysOss oss = new SysOss();
         SysOss oss = new SysOss();
         oss.setUrl(uploadResult.url());
         oss.setUrl(uploadResult.url());
         oss.setFileSuffix(suffix);
         oss.setFileSuffix(suffix);

+ 1 - 1
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/impl/SysUserServiceImpl.java

@@ -429,7 +429,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
      * @return 结果
      * @return 结果
      */
      */
     @Override
     @Override
-    public boolean updateUserAvatar(Long userId, Long avatar) {
+    public boolean updateUserAvatar(Long userId, String avatar) {
         return baseMapper.update(null,
         return baseMapper.update(null,
                 new LambdaUpdateWrapper<SysUser>()
                 new LambdaUpdateWrapper<SysUser>()
                         .set(SysUser::getAvatar, avatar)
                         .set(SysUser::getAvatar, avatar)

+ 9 - 15
UI/VAP_V3.VUE/src/components/symbol/VbSymbol.vue

@@ -4,7 +4,6 @@ const props = withDefaults(
 	defineProps<{
 	defineProps<{
 		text?: string
 		text?: string
 		src?: string
 		src?: string
-		url?: string
 		size?: number
 		size?: number
 		shape?: "" | "circle" | "square"
 		shape?: "" | "circle" | "square"
 		isRatio?: boolean
 		isRatio?: boolean
@@ -42,25 +41,20 @@ const _style = computed(() => {
 })
 })
 const srcUrl = ref<string>("")
 const srcUrl = ref<string>("")
 function init() {
 function init() {
-	if (props.url && props.src) {
-		Rs.get({
-			url: props.url + props.src,
-			loading: false,
-			responseType: "blob"
-		}).then((res: any) => {
-			if (res.type === "application/json") {
-				return
-			}
-			const data = new Blob([res])
-			const url = URL.createObjectURL(data)
-			srcUrl.value = url
-		})
-	} else if (props.src) {
+	if (!props.src) {
+		return
+	}
+	if (props.src.startsWith("http") || props.src.includes("/")) {
 		srcUrl.value = props.src
 		srcUrl.value = props.src
+		return
 	}
 	}
+	getResourceLocalUrl(props.src).then((url) => {
+		srcUrl.value = url
+	})
 }
 }
 
 
 onMounted(init)
 onMounted(init)
+watch(() => props.src, init)
 </script>
 </script>
 
 
 <template>
 <template>

+ 7 - 27
UI/VAP_V3.VUE/src/components/upload/VbImagePreview.vue

@@ -3,12 +3,10 @@ import Rs from "@@/services/RequestService"
 const props = withDefaults(
 const props = withDefaults(
 	defineProps<{
 	defineProps<{
 		src: string
 		src: string
-		prefixSrc?: string
 		fit?: "" | "fill" | "none" | "contain" | "cover" | "scale-down"
 		fit?: "" | "fill" | "none" | "contain" | "cover" | "scale-down"
 		width?: string | number
 		width?: string | number
 		height?: string | number
 		height?: string | number
 		imageStyle?: any
 		imageStyle?: any
-		url?: string
 		load?: (e: any) => void
 		load?: (e: any) => void
 		error?: (e: any) => void
 		error?: (e: any) => void
 	}>(),
 	}>(),
@@ -16,7 +14,6 @@ const props = withDefaults(
 		width: 75,
 		width: 75,
 		height: 50,
 		height: 50,
 		prefixSrc: "/resource/oss/preview/",
 		prefixSrc: "/resource/oss/preview/",
-		url: "/resource/oss/preview/",
 		fit: "cover",
 		fit: "cover",
 		imageStyle: () => {
 		imageStyle: () => {
 			return {
 			return {
@@ -25,13 +22,7 @@ const props = withDefaults(
 		}
 		}
 	}
 	}
 )
 )
-function formatUrl(url: string) {
-	if (isExternal(url)) {
-		return url
-	}
-	url = import.meta.env.VITE_APP_BASE_API + (props.prefixSrc ? props.prefixSrc : "") + url
-	return url
-}
+
 const srcList = ref<any>([])
 const srcList = ref<any>([])
 
 
 const imgStyle = computed(() => {
 const imgStyle = computed(() => {
@@ -51,24 +42,13 @@ function load() {
 		return
 		return
 	}
 	}
 	const src_list = props.src.split(",")
 	const src_list = props.src.split(",")
-	src_list.forEach((v) => {
-		if (props.url) {
-			Rs.get({
-				url: props.url + v.split("$")[0],
-				loading: false,
-				responseType: "blob"
-			}).then((res: any) => {
-				if (res.type === "application/json") {
-					return
-				}
-				const data = new Blob([res])
-				const url = URL.createObjectURL(data)
-				srcList.value.push(url)
-			})
-		} else {
-			const url = formatUrl(v)
-			srcList.value.push(url)
+	src_list.forEach(async (v) => {
+		if (v.startsWith("http") || v.includes("/")) {
+			srcList.value.push(v)
+			return
 		}
 		}
+		const url = await getResourceLocalUrl(v)
+		srcList.value.push(url)
 	})
 	})
 }
 }
 
 

+ 5 - 26
UI/VAP_V3.VUE/src/components/upload/VbOfficePreview.vue

@@ -5,7 +5,6 @@ import { isExternal } from "@@/utils/validate"
 const props = withDefaults(
 const props = withDefaults(
 	defineProps<{
 	defineProps<{
 		src?: string
 		src?: string
-		previewUrl?: string
 		fileId?: string
 		fileId?: string
 		fileName?: string
 		fileName?: string
 		fileExt?: string
 		fileExt?: string
@@ -73,32 +72,12 @@ function load(fileId?: string, src?: string) {
 		fileUrl.value = ""
 		fileUrl.value = ""
 		return
 		return
 	}
 	}
-
-	if (props.previewUrl && targetFileId) {
-		let requestUrl = props.previewUrl
-		if (!requestUrl.endsWith("/")) {
-			requestUrl += "/"
-		}
-
-		loading.value = true
-		Rs.get({
-			url: requestUrl + targetFileId,
-			loading: false,
-			responseType: "blob"
-		})
-			.then((res: any) => {
-				loading.value = false
-				if (res.type === "application/json") {
-					return
-				}
-				const data = new Blob([res], { type: `application/${fileExt.value}` })
-				fileUrl.value = URL.createObjectURL(data)
-			})
-			.catch(() => {
-				loading.value = false
-			})
-	} else if (targetFileId) {
+	if (targetFileId.startsWith("http") || targetFileId.includes("/")) {
 		fileUrl.value = formatUrl(targetFileId)
 		fileUrl.value = formatUrl(targetFileId)
+	} else if (targetFileId) {
+		getResourceLocalUrl(targetFileId).then((url) => {
+			fileUrl.value = url
+		})
 	} else {
 	} else {
 		fileUrl.value = formatUrl(targetSrc || "")
 		fileUrl.value = formatUrl(targetSrc || "")
 	}
 	}

+ 103 - 0
UI/VAP_V3.VUE/src/core/utils/resource.ts

@@ -0,0 +1,103 @@
+import Rs from "@@/services/RequestService"
+
+// 资源缓存字典:objectId -> objectUrl
+const ResourceDict = new Map<string, string>()
+
+/**
+ * 缓存最大数量限制
+ * 当缓存超过此数量时,会自动清理最旧的缓存项
+ */
+export const MAX_RESOURCE_CACHE_SIZE = 100
+
+/**
+ * 获取资源本地 URL(带缓存)
+ * @param objectId - 资源对象 ID
+ * @returns 资源的 Blob URL,失败返回 undefined
+ */
+export async function getResourceLocalUrl(objectId: string): Promise<string | undefined> {
+	// 1. 检查缓存
+	if (ResourceDict.has(objectId)) {
+		return ResourceDict.get(objectId)
+	}
+
+	// 2. 创建新的请求
+	const requestPromise = (async () => {
+		try {
+			const url = `/resource/oss/preview/${objectId}`
+			const res: any = await Rs.get({
+				url,
+				loading: false,
+				responseType: "blob"
+			})
+
+			// 检查响应类型
+			if (res.type === "application/json") {
+				console.warn(`[Resource] 获取资源失败: ${objectId}`)
+				return undefined
+			}
+
+			// 创建 Blob URL
+			const data = new Blob([res])
+			const objectUrl = URL.createObjectURL(data)
+
+			// 存入缓存
+			ResourceDict.set(objectId, objectUrl)
+
+			// 检查缓存大小,超过限制时清理最旧的缓存
+			if (ResourceDict.size > MAX_RESOURCE_CACHE_SIZE) {
+				cleanupOldestCache()
+			}
+
+			return objectUrl
+		} catch (error) {
+			console.error(`[Resource] 获取资源异常: ${objectId}`, error)
+			return undefined
+		}
+	})()
+
+	return requestPromise
+}
+
+/**
+ * 清理最旧的缓存项(LRU 策略)
+ * Map 的迭代顺序是插入顺序,第一个是最旧的
+ */
+function cleanupOldestCache(): void {
+	// 获取第一个(最旧的)缓存项
+	const oldestKey = ResourceDict.keys().next().value
+	if (oldestKey) {
+		const oldestUrl = ResourceDict.get(oldestKey)
+		if (oldestUrl) {
+			URL.revokeObjectURL(oldestUrl) // 释放 Blob URL
+		}
+		ResourceDict.delete(oldestKey)
+		console.log(`[Resource] 缓存已满,清理最旧缓存: ${oldestKey}`)
+	}
+}
+
+/**
+ * 清除指定资源的缓存并释放内存
+ * @param objectId - 资源对象 ID,不传则清空所有缓存
+ */
+export function clearResourceCache(objectId?: string): void {
+	if (objectId) {
+		const url = ResourceDict.get(objectId)
+		if (url) {
+			URL.revokeObjectURL(url) // 释放 Blob URL
+			ResourceDict.delete(objectId)
+		}
+	} else {
+		// 清空所有缓存
+		ResourceDict.forEach((url) => {
+			URL.revokeObjectURL(url)
+		})
+		ResourceDict.clear()
+	}
+}
+
+/**
+ * 获取缓存大小
+ */
+export function getResourceCacheSize(): number {
+	return ResourceDict.size
+}

+ 1 - 6
UI/VAP_V3.VUE/src/layouts/main/header/navbar/UserAccountMenu.vue

@@ -37,12 +37,7 @@ function signOut() {
 			data-vb-menu-trigger="{default: 'click', lg: 'hover'}"
 			data-vb-menu-trigger="{default: 'click', lg: 'hover'}"
 			data-vb-menu-attach="parent"
 			data-vb-menu-attach="parent"
 			data-vb-menu-placement="bottom-end">
 			data-vb-menu-placement="bottom-end">
-			<VbSymbol
-				url="/resource/oss"
-				:src="user.avatar"
-				:text="user.userName"
-				:size="40"
-				alt="user" />
+			<VbSymbol :src="user.avatar" :text="user.userName" :size="40" alt="user" />
 		</div>
 		</div>
 		<div
 		<div
 			class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary py-4 fs-6 w-275px"
 			class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary py-4 fs-6 w-275px"

+ 8 - 9
UI/VAP_V3.VUE/src/views/account/profile/_avatar.vue

@@ -7,10 +7,10 @@ const user = appStore.authStore.user
 const avatarModalRef = ref()
 const avatarModalRef = ref()
 const cropperRef = ref()
 const cropperRef = ref()
 const visible = ref(false)
 const visible = ref(false)
-
+const avatar = computed(() => user.avatar)
 // 图片裁剪数据
 // 图片裁剪数据
 const options = reactive<any>({
 const options = reactive<any>({
-	img: user.avatar, // 裁剪图片的地址
+	img: avatar.value, // 裁剪图片的地址
 	autoCrop: true, // 是否默认生成截图框
 	autoCrop: true, // 是否默认生成截图框
 	autoCropWidth: 200, // 默认生成截图框宽度
 	autoCropWidth: 200, // 默认生成截图框宽度
 	autoCropHeight: 200, // 默认生成截图框高度
 	autoCropHeight: 200, // 默认生成截图框高度
@@ -62,9 +62,9 @@ function uploadImg() {
 	cropperRef.value.getCropBlob((data: any) => {
 	cropperRef.value.getCropBlob((data: any) => {
 		const formData = new FormData()
 		const formData = new FormData()
 		formData.append("avatar", data, options.fileName)
 		formData.append("avatar", data, options.fileName)
-		apis.system.userApi.uploadAvatar(formData).then((res: any) => {
-			appStore.authStore.changeAvatar(res.imgUrl)
-			options.img = appStore.authStore.user.avatar
+		apis.system.userApi.uploadAvatar(formData).then(async (res: any) => {
+			appStore.authStore.changeAvatar(res.objectId)
+			options.img = await getResourceLocalUrl(res.objectId)
 			message.msgSuccess("修改成功")
 			message.msgSuccess("修改成功")
 			avatarModalRef.value.hide()
 			avatarModalRef.value.hide()
 		})
 		})
@@ -75,12 +75,12 @@ function realTime(data: any) {
 	options.previews = data
 	options.previews = data
 }
 }
 /** 打开弹出层结束时的回调 */
 /** 打开弹出层结束时的回调 */
-function modalOpened() {
+async function modalOpened() {
+	options.img = await getResourceLocalUrl(avatar.value)
 	visible.value = true
 	visible.value = true
 }
 }
 /** 关闭窗口 */
 /** 关闭窗口 */
 function modalHide() {
 function modalHide() {
-	options.img = user.avatar
 	visible.value = false
 	visible.value = false
 }
 }
 const previewDivStyle = computed(() => {
 const previewDivStyle = computed(() => {
@@ -94,8 +94,7 @@ const previewDivStyle = computed(() => {
 			<VbSymbol
 			<VbSymbol
 				class="position-relative cursor-pointer"
 				class="position-relative cursor-pointer"
 				:size="100"
 				:size="100"
-				url="/resource/oss"
-				:src="user.avatar"
+				:src="avatar"
 				:text="user.userName"
 				:text="user.userName"
 				@click="avatarModalRef.show()"></VbSymbol>
 				@click="avatarModalRef.show()"></VbSymbol>
 		</vb-tooltip>
 		</vb-tooltip>

+ 1 - 2
UI/VAP_V3.VUE/src/views/system/oss/index.vue

@@ -252,8 +252,7 @@ getServiceOptions()
 			<template #url="{ row }">
 			<template #url="{ row }">
 				<template v-if="checkFileType(row.originalName)">
 				<template v-if="checkFileType(row.originalName)">
 					<VbImagePreview
 					<VbImagePreview
-						url="/resource/oss/preview"
-						:src="`/${row.objectId}`"
+						:src="row.objectId"
 						:image-style="{ marginTop: '6px', marginBottom: '' }"></VbImagePreview>
 						:image-style="{ marginTop: '6px', marginBottom: '' }"></VbImagePreview>
 				</template>
 				</template>
 				<span v-else>
 				<span v-else>