提交
This commit is contained in:
1 parent
375a2c8df7
commit
ba7b7b8497
9 files changed
+343
-57
No files matched your search
+7
-2
@@ -75,11 +75,16 @@ public class SysUserVo implements Serializable {
|
|||||||
private String sex;
|
private String sex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 头像地址
|
* 头像 OSS ID
|
||||||
*/
|
*/
|
||||||
@Translation(type = TransConstant.OSS_ID_TO_URL)
|
|
||||||
private Long avatar;
|
private Long avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像访问地址
|
||||||
|
*/
|
||||||
|
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "avatar")
|
||||||
|
private String avatarUrl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 密码
|
* 密码
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -395,7 +395,8 @@ loginWithEncryption()
|
|||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"phonenumber": "13800138000",
|
"phonenumber": "13800138000",
|
||||||
"sex": "1",
|
"sex": "1",
|
||||||
"avatar": "/profile/avatar/default.jpg",
|
"avatar": "1938120323613728770",
|
||||||
|
"avatarUrl": "http://example.com/ruoyi/2025/08/31/xxxx.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"loginIp": "127.0.0.1",
|
"loginIp": "127.0.0.1",
|
||||||
"loginDate": "2025-01-26 10:30:00",
|
"loginDate": "2025-01-26 10:30:00",
|
||||||
@@ -409,6 +410,13 @@ loginWithEncryption()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**字段说明补充**:
|
||||||
|
|
||||||
|
- `avatar`:始终返回 `sys_user.avatar` 中存储的 OSS ID(字符串),用于鉴权下载接口 `/resource/oss/download/{ossId}`。
|
||||||
|
- `avatarUrl`:系统通过 `TransConstant.OSS_ID_TO_URL` 规则实时翻译得到的可直接访问地址(MinIO 预签名 URL),供无需链路改造的场景快速显示;有效期受 OSS 策略控制。
|
||||||
|
|
||||||
|
前端如需在带 Token 的受保护组件(如 `ImagePreview`)中展示,请优先使用 `avatar` 并通过统一的认证下载逻辑获取图片,避免直接暴露临时 URL。
|
||||||
|
|
||||||
**超级管理员权限说明**:
|
**超级管理员权限说明**:
|
||||||
|
|
||||||
admin 用户具有系统最高权限`*:*:*`,可以访问所有功能模块和执行所有操作。
|
admin 用户具有系统最高权限`*:*:*`,可以访问所有功能模块和执行所有操作。
|
||||||
@@ -663,7 +671,9 @@ methods: {
|
|||||||
---
|
---
|
||||||
|
|
||||||
**📝 维护**: 用户认证登录 API 文档
|
**📝 维护**: 用户认证登录 API 文档
|
||||||
**🔄 更新**: 2025-01-03
|
**🔄 更新记录**:
|
||||||
|
- 2025-11-28:`/system/user/getInfo` 增加 `avatarUrl` 字段,并明确 `avatar` 返回 OSS ID
|
||||||
|
- 2025-01-03:文档初始化
|
||||||
**📁 文件**: 用户认证登录 API.md
|
**📁 文件**: 用户认证登录 API.md
|
||||||
|
|
||||||
## 获取具有指定权限的用户列表
|
## 获取具有指定权限的用户列表
|
||||||
@@ -693,7 +703,8 @@ methods: {
|
|||||||
deptId: string; // 部门ID
|
deptId: string; // 部门ID
|
||||||
deptName: string; // 部门名称
|
deptName: string; // 部门名称
|
||||||
};
|
};
|
||||||
avatar: string; // 头像URL
|
avatar: string; // 头像OSS ID
|
||||||
|
avatarUrl?: string; // 头像访问URL(可选,预签名)
|
||||||
}[];
|
}[];
|
||||||
total: number; // 总记录数
|
total: number; // 总记录数
|
||||||
}
|
}
|
||||||
@@ -713,7 +724,8 @@ methods: {
|
|||||||
"deptId": "1",
|
"deptId": "1",
|
||||||
"deptName": "后勤部"
|
"deptName": "后勤部"
|
||||||
},
|
},
|
||||||
"avatar": "http://example.com/avatar.jpg"
|
"avatar": "1938120323613728770",
|
||||||
|
"avatarUrl": "http://example.com/avatar.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&..."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total": 1
|
"total": 1
|
||||||
|
|||||||
@@ -65,6 +65,36 @@ ImagePreview 是一个图片预览组件,支持两种模式:
|
|||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 4. 顶部个人头像(自动识别OSS ID)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import ImagePreview from '@/components/ImagePreview/index.vue';
|
||||||
|
import { useUserStore } from '@/store/modules/user';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const avatarIsOssId = computed(() => {
|
||||||
|
const avatar = userStore.avatar;
|
||||||
|
if (!avatar) return false;
|
||||||
|
return !/^https?:\/\//i.test(avatar) && !avatar.startsWith('/');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ImagePreview
|
||||||
|
v-if="userStore.avatar"
|
||||||
|
class="user-avatar"
|
||||||
|
:src="userStore.avatar"
|
||||||
|
:width="24"
|
||||||
|
:height="24"
|
||||||
|
:is-oss-id="avatarIsOssId"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
> ✅ 用于首页 `Navbar` 头像区域时,`avatarIsOssId` 会自动识别当前值是否为 OSS ID,从而在带 Token 的 `/resource/oss/download/{ossId}` 接口与传统 URL 之间无缝切换,保证头像始终通过认证通道加载。
|
||||||
|
|
||||||
## 技术实现
|
## 技术实现
|
||||||
|
|
||||||
### 核心功能
|
### 核心功能
|
||||||
@@ -126,6 +156,7 @@ const urlCache = ref<Map<string, string>>(new Map());
|
|||||||
|
|
||||||
## 更新记录
|
## 更新记录
|
||||||
|
|
||||||
|
- **2025-11-28**:补充顶部栏个人头像场景示例,强调 OSS ID 自动识别策略,确保 `Navbar` 等公共区域也复用统一的图片安全加载方案。
|
||||||
- **2025-08-02**:修复组件,支持OSS ID模式,添加缓存机制和加载状态
|
- **2025-08-02**:修复组件,支持OSS ID模式,添加缓存机制和加载状态
|
||||||
- 修复了导入错误和computed未定义的问题
|
- 修复了导入错误和computed未定义的问题
|
||||||
- 添加了完整的错误处理和日志记录
|
- 添加了完整的错误处理和日志记录
|
||||||
@@ -737,6 +737,13 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 头像鉴权加载
|
||||||
|
|
||||||
|
- 统一从 `/system/user/getInfo` 获取 `avatar`(OSS ID)与 `avatarUrl`(预签名地址)。
|
||||||
|
- 页面中优先识别 `avatar` 是否为 OSS ID,使用 `uni.request`(`responseType: 'arraybuffer'`)访问 `/resource/oss/download/{ossId}` 并转换为 `Blob URL/Base64`,确保 Token 认证生效。
|
||||||
|
- 若历史账号仍存储直接 URL,则回退到 `avatar` 或 `avatarUrl` 原始值,最后兜底 `/static/111.png`。
|
||||||
|
- 首页、详情页等通用区域必需复用该策略,避免直接暴露预签名链接导致过期或越权。
|
||||||
|
|
||||||
#### 列表优化
|
#### 列表优化
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
|
|||||||
@@ -681,7 +681,8 @@ import Card from '@/components/Card.vue';
|
|||||||
import ServiceItem from '@/components/ServiceItem.vue';
|
import ServiceItem from '@/components/ServiceItem.vue';
|
||||||
import GridLayout from '@/components/GridLayout.vue';
|
import GridLayout from '@/components/GridLayout.vue';
|
||||||
import { userManager, userState } from '@/utils/user';
|
import { userManager, userState } from '@/utils/user';
|
||||||
import { api } from '@/common/request';
|
import { api, request } from '@/common/request';
|
||||||
|
import { REQUEST_CONFIG } from '@/common/config';
|
||||||
import { hasPermi, permissionMixin } from '@/utils/permission';
|
import { hasPermi, permissionMixin } from '@/utils/permission';
|
||||||
import RouterManager from '@/utils/router';
|
import RouterManager from '@/utils/router';
|
||||||
|
|
||||||
@@ -698,8 +699,10 @@ export default {
|
|||||||
userInfo: {
|
userInfo: {
|
||||||
name: '用户',
|
name: '用户',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
|
avatarOssId: '',
|
||||||
deptName: '',
|
deptName: '',
|
||||||
},
|
},
|
||||||
|
avatarBlobUrl: '',
|
||||||
// 轮播图片数据
|
// 轮播图片数据
|
||||||
bannerList: [
|
bannerList: [
|
||||||
{
|
{
|
||||||
@@ -817,6 +820,12 @@ export default {
|
|||||||
this.checkLoginStatus();
|
this.checkLoginStatus();
|
||||||
this.loadUserInfo();
|
this.loadUserInfo();
|
||||||
},
|
},
|
||||||
|
onUnload() {
|
||||||
|
this.cleanupAvatarBlob();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.cleanupAvatarBlob();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 全局权限校验方法
|
// 全局权限校验方法
|
||||||
hasPermi(permissions) {
|
hasPermi(permissions) {
|
||||||
@@ -831,15 +840,220 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 加载用户信息
|
// 加载用户信息
|
||||||
loadUserInfo() {
|
async loadUserInfo() {
|
||||||
const displayUser = userManager.formatUserForDisplay();
|
try {
|
||||||
if (displayUser) {
|
const displayUser = userManager.formatUserForDisplay();
|
||||||
|
if (!displayUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.userInfo.name = displayUser.name;
|
this.userInfo.name = displayUser.name;
|
||||||
this.userInfo.avatar = displayUser.avatar;
|
|
||||||
this.userInfo.deptName = displayUser.deptName;
|
this.userInfo.deptName = displayUser.deptName;
|
||||||
|
this.userInfo.avatar = '';
|
||||||
|
this.userInfo.avatarOssId = '';
|
||||||
|
|
||||||
|
if (!displayUser.avatar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedAvatar = await this.resolveAvatarValue(displayUser.avatar);
|
||||||
|
if (resolvedAvatar) {
|
||||||
|
const displayableUrl = await this.getDisplayableAvatarUrl(
|
||||||
|
resolvedAvatar.url,
|
||||||
|
resolvedAvatar.ossId,
|
||||||
|
);
|
||||||
|
this.userInfo.avatar = displayableUrl || resolvedAvatar.url;
|
||||||
|
this.userInfo.avatarOssId = resolvedAvatar.ossId || '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户信息失败:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 统一处理头像字段,兼容URL与OSS ID
|
||||||
|
async resolveAvatarValue(rawAvatar) {
|
||||||
|
const avatarValue = (rawAvatar || '').toString().trim();
|
||||||
|
if (!avatarValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接可用的URL
|
||||||
|
if (this.isDirectUrl(avatarValue)) {
|
||||||
|
return { url: avatarValue, ossId: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端仅返回了相对路径(例如/resource/oss/preview/xxx 或 /api/resource/...)
|
||||||
|
if (avatarValue.startsWith('/resource/') || avatarValue.startsWith('/api/resource/')) {
|
||||||
|
return {
|
||||||
|
url: this.normalizeResourceUrl(avatarValue),
|
||||||
|
ossId: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其余场景按OSS ID处理
|
||||||
|
const ossId = avatarValue.split(',')[0].trim();
|
||||||
|
if (!ossId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlFromOss = await this.fetchAvatarFromOss(ossId);
|
||||||
|
return urlFromOss
|
||||||
|
? {
|
||||||
|
url: urlFromOss,
|
||||||
|
ossId,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 判断是否为绝对可用的URL
|
||||||
|
isDirectUrl(value) {
|
||||||
|
const lower = value.toLowerCase();
|
||||||
|
return (
|
||||||
|
this.isAbsoluteExternalUrl(lower) ||
|
||||||
|
lower.startsWith('data:') ||
|
||||||
|
lower.startsWith('blob:') ||
|
||||||
|
value.startsWith('/static/')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
isAbsoluteExternalUrl(value) {
|
||||||
|
const lower = value.toLowerCase();
|
||||||
|
return lower.startsWith('http://') || lower.startsWith('https://');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 将后端返回的/resource路径转换成可直接展示的URL
|
||||||
|
normalizeResourceUrl(url) {
|
||||||
|
if (!url) return '';
|
||||||
|
if (url.startsWith('/api/')) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return `${REQUEST_CONFIG.serverUrl}${url}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 确保头像URL可以直接展示(必要时转换为blob URL)
|
||||||
|
async getDisplayableAvatarUrl(url, ossId) {
|
||||||
|
if (!url) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态资源或可匿名访问的URL直接返回
|
||||||
|
if (this.isAbsoluteExternalUrl(url) || url.startsWith('/static/')) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先尝试把现有URL转换为blob,确保请求可以带上Token
|
||||||
|
const blobUrl = await this.fetchImageBlobUrl(url);
|
||||||
|
if (blobUrl) {
|
||||||
|
this.updateAvatarBlobUrl(blobUrl);
|
||||||
|
return blobUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兜底:通过下载接口重新获取
|
||||||
|
if (ossId) {
|
||||||
|
const downloadPath = `/resource/oss/download/${encodeURIComponent(ossId)}`;
|
||||||
|
const downloadBlobUrl = await this.fetchImageBlobUrl(downloadPath);
|
||||||
|
if (downloadBlobUrl) {
|
||||||
|
this.updateAvatarBlobUrl(downloadBlobUrl);
|
||||||
|
return downloadBlobUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateAvatarBlobUrl(blobUrl) {
|
||||||
|
if (!blobUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cleanupAvatarBlob();
|
||||||
|
this.avatarBlobUrl = blobUrl;
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanupAvatarBlob() {
|
||||||
|
if (this.avatarBlobUrl && this.avatarBlobUrl.startsWith('blob:')) {
|
||||||
|
try {
|
||||||
|
URL.revokeObjectURL(this.avatarBlobUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('释放头像blob URL失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.avatarBlobUrl = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchImageBlobUrl(imageUrl) {
|
||||||
|
try {
|
||||||
|
if (!imageUrl) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAbsoluteExternalUrl(imageUrl) || imageUrl.startsWith('/static/')) {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
let requestUrl = imageUrl;
|
||||||
|
|
||||||
|
const serverPrefix = REQUEST_CONFIG.serverUrl || '';
|
||||||
|
if (
|
||||||
|
serverPrefix &&
|
||||||
|
requestUrl.startsWith(serverPrefix) &&
|
||||||
|
serverPrefix.startsWith('/')
|
||||||
|
) {
|
||||||
|
requestUrl = requestUrl.slice(serverPrefix.length);
|
||||||
|
if (!requestUrl.startsWith('/')) {
|
||||||
|
requestUrl = `/${requestUrl}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestUrl.startsWith('/api/')) {
|
||||||
|
requestUrl = requestUrl.replace('/api', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestUrl.startsWith('/')) {
|
||||||
|
requestUrl = `/${requestUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await request({
|
||||||
|
url: requestUrl,
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!arrayBuffer) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([arrayBuffer], { type: 'image/*' });
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取头像blob失败:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 通过OSS ID获取文件访问URL
|
||||||
|
async fetchAvatarFromOss(ossId) {
|
||||||
|
try {
|
||||||
|
const response = await request({
|
||||||
|
url: `/resource/oss/listByIds/${encodeURIComponent(ossId)}`,
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
response.code === 200 &&
|
||||||
|
Array.isArray(response.data) &&
|
||||||
|
response.data.length > 0
|
||||||
|
) {
|
||||||
|
const fileInfo = response.data[0];
|
||||||
|
if (fileInfo?.url) {
|
||||||
|
return this.normalizeResourceUrl(fileInfo.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('根据OSS ID获取头像失败:', error);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
// 点击用户头像
|
// 点击用户头像
|
||||||
handleUserClick() {
|
handleUserClick() {
|
||||||
// const info = userManager.getUserInfoString();
|
// const info = userManager.getUserInfoString();
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface UserVO extends BaseEntity {
|
|||||||
phonenumber: string;
|
phonenumber: string;
|
||||||
sex: string;
|
sex: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
avatarUrl?: string;
|
||||||
status: string;
|
status: string;
|
||||||
delFlag: string;
|
delFlag: string;
|
||||||
loginIp: string;
|
loginIp: string;
|
||||||
|
|||||||
@@ -75,7 +75,8 @@
|
|||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
|
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
|
||||||
<div class="avatar-wrapper">
|
<div class="avatar-wrapper">
|
||||||
<img :src="userStore.avatar" class="user-avatar" />
|
<ImagePreview v-if="userStore.avatar" class="user-avatar" :src="userStore.avatar" :width="24" :height="24" :is-oss-id="avatarIsOssId" />
|
||||||
|
<img v-else :src="userStore.avatar" class="user-avatar" />
|
||||||
<el-icon><caret-bottom /></el-icon>
|
<el-icon><caret-bottom /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
@@ -98,9 +99,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ComponentInternalInstance, computed, getCurrentInstance, ref, watch } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import Hamburger from '@/components/Hamburger/index.vue';
|
import Hamburger from '@/components/Hamburger/index.vue';
|
||||||
import TopNav from '@/components/TopNav/index.vue';
|
import TopNav from '@/components/TopNav/index.vue';
|
||||||
import SearchMenu from './TopBar/search.vue';
|
import SearchMenu from './TopBar/search.vue';
|
||||||
|
import ImagePreview from '@/components/ImagePreview/index.vue';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { useUserStore } from '@/store/modules/user';
|
import { useUserStore } from '@/store/modules/user';
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
@@ -129,6 +133,15 @@ const dynamic = ref(false);
|
|||||||
const tenantEnabled = ref(true);
|
const tenantEnabled = ref(true);
|
||||||
// 搜索菜单
|
// 搜索菜单
|
||||||
const searchMenuRef = ref<InstanceType<typeof SearchMenu>>();
|
const searchMenuRef = ref<InstanceType<typeof SearchMenu>>();
|
||||||
|
const avatarIsOssId = computed(() => {
|
||||||
|
const avatar = userStore.avatar;
|
||||||
|
if (!avatar) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isHttp = /^https?:\/\//i.test(avatar);
|
||||||
|
const isAbsolutePath = avatar.startsWith('/');
|
||||||
|
return !isHttp && !isAbsolutePath;
|
||||||
|
});
|
||||||
|
|
||||||
const openSearchMenu = () => {
|
const openSearchMenu = () => {
|
||||||
searchMenuRef.value?.openSearch();
|
searchMenuRef.value?.openSearch();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const userId = ref<string | number>('');
|
const userId = ref<string | number>('');
|
||||||
const tenantId = ref<string>('');
|
const tenantId = ref<string>('');
|
||||||
const avatar = ref('');
|
const avatar = ref('');
|
||||||
|
const avatarUrl = ref('');
|
||||||
const deptName = ref('');
|
const deptName = ref('');
|
||||||
const phonenumber = ref('');
|
const phonenumber = ref('');
|
||||||
const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
|
const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
|
||||||
@@ -40,7 +41,9 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
if (res) {
|
if (res) {
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
const user = data.user;
|
const user = data.user;
|
||||||
const profile = user.avatar == '' || user.avatar == null ? defAva : user.avatar;
|
const avatarValue = user.avatar === '' || user.avatar === null || user.avatar === undefined ? '' : String(user.avatar);
|
||||||
|
const previewUrl = user.avatarUrl || '';
|
||||||
|
const profile = avatarValue || defAva;
|
||||||
|
|
||||||
if (data.roles && data.roles.length > 0) {
|
if (data.roles && data.roles.length > 0) {
|
||||||
// 验证返回的roles是否是一个非空数组
|
// 验证返回的roles是否是一个非空数组
|
||||||
@@ -52,6 +55,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
name.value = user.userName;
|
name.value = user.userName;
|
||||||
nickname.value = user.nickName;
|
nickname.value = user.nickName;
|
||||||
avatar.value = profile;
|
avatar.value = profile;
|
||||||
|
avatarUrl.value = previewUrl || (avatarValue ? '' : defAva);
|
||||||
userId.value = user.userId;
|
userId.value = user.userId;
|
||||||
tenantId.value = user.tenantId;
|
tenantId.value = user.tenantId;
|
||||||
deptName.value = user.deptName;
|
deptName.value = user.deptName;
|
||||||
@@ -72,6 +76,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
|
|
||||||
const setAvatar = (value: string) => {
|
const setAvatar = (value: string) => {
|
||||||
avatar.value = value;
|
avatar.value = value;
|
||||||
|
avatarUrl.value = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -81,6 +86,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
name,
|
name,
|
||||||
nickname,
|
nickname,
|
||||||
avatar,
|
avatar,
|
||||||
|
avatarUrl,
|
||||||
deptName,
|
deptName,
|
||||||
phonenumber,
|
phonenumber,
|
||||||
roles,
|
roles,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="user-info-head" @click="editCropper()">
|
<div class="user-info-head" @click="editCropper()">
|
||||||
<img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
|
<img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
|
||||||
<el-dialog v-model="open" :title="title" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
|
<el-dialog v-model="open" :title="title" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
|
||||||
<el-row>
|
<el-row justify="center">
|
||||||
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
|
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
|
||||||
<vue-cropper
|
<vue-cropper
|
||||||
v-if="visible"
|
v-if="visible"
|
||||||
@@ -17,40 +17,23 @@
|
|||||||
@real-time="realTime"
|
@real-time="realTime"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
|
|
||||||
<div class="avatar-upload-preview">
|
|
||||||
<img :src="options.previews.url" :style="options.previews.img" />
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
<br />
|
<br />
|
||||||
<el-row>
|
<div class="avatar-actions">
|
||||||
<el-col :lg="2" :md="2">
|
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
|
||||||
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
|
<el-button>
|
||||||
<el-button>
|
选择
|
||||||
选择
|
<el-icon class="el-icon--right">
|
||||||
<el-icon class="el-icon--right">
|
<Upload />
|
||||||
<Upload />
|
</el-icon>
|
||||||
</el-icon>
|
</el-button>
|
||||||
</el-button>
|
</el-upload>
|
||||||
</el-upload>
|
<el-button icon="Plus" @click="changeScale(1)"></el-button>
|
||||||
</el-col>
|
<el-button icon="Minus" @click="changeScale(-1)"></el-button>
|
||||||
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
|
<el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
|
||||||
<el-button icon="Plus" @click="changeScale(1)"></el-button>
|
<el-button icon="RefreshRight" @click="rotateRight()"></el-button>
|
||||||
</el-col>
|
<el-button type="primary" @click="uploadImg()">提 交</el-button>
|
||||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
</div>
|
||||||
<el-button icon="Minus" @click="changeScale(-1)"></el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
|
||||||
<el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
|
||||||
<el-button icon="RefreshRight" @click="rotateRight()"></el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
|
|
||||||
<el-button type="primary" @click="uploadImg()">提 交</el-button>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -84,13 +67,13 @@ const title = ref('修改头像');
|
|||||||
const cropper = ref<any>({});
|
const cropper = ref<any>({});
|
||||||
//图片裁剪数据
|
//图片裁剪数据
|
||||||
const options = reactive<Options>({
|
const options = reactive<Options>({
|
||||||
img: userStore.avatar,
|
img: userStore.avatarUrl || userStore.avatar,
|
||||||
autoCrop: true,
|
autoCrop: true,
|
||||||
autoCropWidth: 200,
|
autoCropWidth: 200,
|
||||||
autoCropHeight: 200,
|
autoCropHeight: 200,
|
||||||
fixedBox: true,
|
fixedBox: true,
|
||||||
outputType: 'png',
|
outputType: 'png',
|
||||||
fileName: '',
|
fileName: 'avatar.png',
|
||||||
previews: {},
|
previews: {},
|
||||||
visible: false
|
visible: false
|
||||||
});
|
});
|
||||||
@@ -134,14 +117,20 @@ const beforeUpload = (file: UploadRawFile): any => {
|
|||||||
/** 上传图片 */
|
/** 上传图片 */
|
||||||
const uploadImg = async () => {
|
const uploadImg = async () => {
|
||||||
cropper.value.getCropBlob(async (data: any) => {
|
cropper.value.getCropBlob(async (data: any) => {
|
||||||
const formData = new FormData();
|
try {
|
||||||
formData.append('avatarfile', data, options.fileName);
|
const formData = new FormData();
|
||||||
const res = await uploadAvatar(formData);
|
const fileName = options.fileName || `avatar.${options.outputType || 'png'}`;
|
||||||
open.value = false;
|
formData.append('avatarfile', data, fileName);
|
||||||
options.img = res.data.imgUrl;
|
const res = await uploadAvatar(formData);
|
||||||
userStore.setAvatar(options.img);
|
await userStore.getInfo(); // 刷新用户信息以获取最新的oss_id
|
||||||
proxy?.$modal.msgSuccess('修改成功');
|
options.img = res?.data?.imgUrl || userStore.avatarUrl || userStore.avatar;
|
||||||
visible.value = false;
|
open.value = false;
|
||||||
|
proxy?.$modal.msgSuccess('修改成功');
|
||||||
|
} catch (error) {
|
||||||
|
proxy?.$modal.msgError('上传失败,请重试');
|
||||||
|
} finally {
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
/** 实时预览 */
|
/** 实时预览 */
|
||||||
@@ -150,7 +139,7 @@ const realTime = (data: any) => {
|
|||||||
};
|
};
|
||||||
/** 关闭窗口 */
|
/** 关闭窗口 */
|
||||||
const closeDialog = () => {
|
const closeDialog = () => {
|
||||||
options.img = userStore.avatar;
|
options.img = userStore.avatarUrl || userStore.avatar;
|
||||||
options.visible = false;
|
options.visible = false;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -179,4 +168,12 @@ const closeDialog = () => {
|
|||||||
line-height: 110px;
|
line-height: 110px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user