chore: 优化代码
This commit is contained in:
parent
fd8bb91fa6
commit
291aaf6022
@ -74,9 +74,6 @@ importers:
|
|||||||
vue-advanced-cropper:
|
vue-advanced-cropper:
|
||||||
specifier: ^2.8.9
|
specifier: ^2.8.9
|
||||||
version: 2.8.9(vue@3.5.13(typescript@5.8.2))
|
version: 2.8.9(vue@3.5.13(typescript@5.8.2))
|
||||||
vue-cropper:
|
|
||||||
specifier: ^0.6.5
|
|
||||||
version: 0.6.5
|
|
||||||
vue-draggable-plus:
|
vue-draggable-plus:
|
||||||
specifier: 0.6.0
|
specifier: 0.6.0
|
||||||
version: 0.6.0(@types/sortablejs@1.15.8)
|
version: 0.6.0(@types/sortablejs@1.15.8)
|
||||||
@ -4082,9 +4079,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.0.0
|
vue: ^3.0.0
|
||||||
|
|
||||||
vue-cropper@0.6.5:
|
|
||||||
resolution: {integrity: sha512-lSvY6IpeA/Tv/iPZ/FOkMHVRBPSlm7t57nuHEZFBMRNOH8ElvfqVlnHGDOAMlvPhh9gHiddiQoASS+fY0MFX0g==}
|
|
||||||
|
|
||||||
vue-demi@0.13.11:
|
vue-demi@0.13.11:
|
||||||
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
|
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -8378,8 +8372,6 @@ snapshots:
|
|||||||
easy-bem: 1.1.1
|
easy-bem: 1.1.1
|
||||||
vue: 3.5.13(typescript@5.8.2)
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
|
||||||
vue-cropper@0.6.5: {}
|
|
||||||
|
|
||||||
vue-demi@0.13.11(vue@3.5.13(typescript@5.8.2)):
|
vue-demi@0.13.11(vue@3.5.13(typescript@5.8.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.13(typescript@5.8.2)
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { VNode } from 'vue';
|
import type { VNode } from 'vue';
|
||||||
|
import { useBoolean, useLoading } from '@sa/hooks';
|
||||||
import { useAuthStore } from '@/store/modules/auth';
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import { useSvgIcon } from '@/hooks/common/icon';
|
import { useSvgIcon } from '@/hooks/common/icon';
|
||||||
@ -15,21 +16,23 @@ const authStore = useAuthStore();
|
|||||||
const { routerPushByKey, toLogin } = useRouterPush();
|
const { routerPushByKey, toLogin } = useRouterPush();
|
||||||
const { SvgIconVNode } = useSvgIcon();
|
const { SvgIconVNode } = useSvgIcon();
|
||||||
|
|
||||||
const avatarLoading = ref(true);
|
// 使用 useBoolean 管理加载状态
|
||||||
const avatarError = ref(false);
|
const { loading: avatarLoading, endLoading: endAvatarLoading } = useLoading(true);
|
||||||
|
// 使用 useBoolean 管理错误状态
|
||||||
|
const { bool: avatarError, setTrue: setError, setFalse: clearError } = useBoolean(false);
|
||||||
|
|
||||||
function loginOrRegister() {
|
function loginOrRegister() {
|
||||||
toLogin();
|
toLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAvatarLoad() {
|
function handleAvatarLoad() {
|
||||||
avatarLoading.value = false;
|
endAvatarLoading();
|
||||||
avatarError.value = false;
|
clearError();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAvatarError() {
|
function handleAvatarError() {
|
||||||
avatarLoading.value = false;
|
endAvatarLoading();
|
||||||
avatarError.value = true;
|
setError();
|
||||||
}
|
}
|
||||||
|
|
||||||
type DropdownKey = 'user-center' | 'logout';
|
type DropdownKey = 'user-center' | 'logout';
|
||||||
@ -91,9 +94,9 @@ function handleDropdown(key: DropdownKey) {
|
|||||||
{{ $t('page.login.common.loginOrRegister') }}
|
{{ $t('page.login.common.loginOrRegister') }}
|
||||||
</NButton>
|
</NButton>
|
||||||
<NDropdown v-else placement="bottom" trigger="click" :options="options" @select="handleDropdown">
|
<NDropdown v-else placement="bottom" trigger="click" :options="options" @select="handleDropdown">
|
||||||
<div class="avatar-wrapper">
|
<div class="flex cursor-pointer items-center rounded-md px-2 py-1 transition-colors duration-300 hover:bg-black/6">
|
||||||
<NSpin :show="avatarLoading">
|
<NSpin :show="avatarLoading">
|
||||||
<div class="avatar-container" :class="{ 'avatar-error': avatarError }">
|
<div class="flex items-center gap-2" :class="{ 'opacity-50': avatarError }">
|
||||||
<NAvatar
|
<NAvatar
|
||||||
v-if="authStore.userInfo.user?.avatar"
|
v-if="authStore.userInfo.user?.avatar"
|
||||||
:size="24"
|
:size="24"
|
||||||
@ -103,7 +106,9 @@ function handleDropdown(key: DropdownKey) {
|
|||||||
@error="handleAvatarError"
|
@error="handleAvatarError"
|
||||||
/>
|
/>
|
||||||
<NAvatar v-else :size="32" round :src="defaultAvatar" @load="handleAvatarLoad" @error="handleAvatarError" />
|
<NAvatar v-else :size="32" round :src="defaultAvatar" @load="handleAvatarLoad" @error="handleAvatarError" />
|
||||||
<span class="user-name">{{ authStore.userInfo.user?.nickName }}</span>
|
<span class="max-w-120px truncate text-14px font-medium">
|
||||||
|
{{ authStore.userInfo.user?.nickName }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
</div>
|
</div>
|
||||||
@ -136,20 +141,9 @@ function handleDropdown(key: DropdownKey) {
|
|||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-role {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
background-color: var(--tag-color);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -3,6 +3,7 @@ import { reactive, ref } from 'vue';
|
|||||||
import type { UploadFileInfo } from 'naive-ui';
|
import type { UploadFileInfo } from 'naive-ui';
|
||||||
import { NButton, NModal, NUpload } from 'naive-ui';
|
import { NButton, NModal, NUpload } from 'naive-ui';
|
||||||
import { Cropper } from 'vue-advanced-cropper';
|
import { Cropper } from 'vue-advanced-cropper';
|
||||||
|
import { useBoolean, useLoading } from '@sa/hooks';
|
||||||
import { fetchUpdateUserAvatar } from '@/service/api/system';
|
import { fetchUpdateUserAvatar } from '@/service/api/system';
|
||||||
import { useAuthStore } from '@/store/modules/auth';
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
import defaultAvatar from '@/assets/imgs/soybean.jpg';
|
import defaultAvatar from '@/assets/imgs/soybean.jpg';
|
||||||
@ -24,7 +25,11 @@ interface CropperRef {
|
|||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
const showModal = ref(false);
|
// 使用 useBoolean 管理模态框显示状态
|
||||||
|
const { bool: showModal, setTrue: showDrawer, setFalse: hideDrawer } = useBoolean();
|
||||||
|
// 使用 useLoading 管理加载状态
|
||||||
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
|
||||||
const imageUrl = ref(authStore.userInfo.user?.avatar || defaultAvatar);
|
const imageUrl = ref(authStore.userInfo.user?.avatar || defaultAvatar);
|
||||||
const cropperRef = ref<CropperRef | null>(null);
|
const cropperRef = ref<CropperRef | null>(null);
|
||||||
|
|
||||||
@ -39,7 +44,7 @@ const options = reactive<CropperOptions>({
|
|||||||
|
|
||||||
/** 编辑头像 */
|
/** 编辑头像 */
|
||||||
function handleEdit() {
|
function handleEdit() {
|
||||||
showModal.value = true;
|
showDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理文件选择 */
|
/** 处理文件选择 */
|
||||||
@ -66,51 +71,74 @@ async function handleFileSelect(data: { file: UploadFileInfo }) {
|
|||||||
async function handleCrop() {
|
async function handleCrop() {
|
||||||
if (!cropperRef.value) return;
|
if (!cropperRef.value) return;
|
||||||
|
|
||||||
const { canvas } = cropperRef.value.getResult();
|
startLoading();
|
||||||
|
try {
|
||||||
|
const { canvas } = cropperRef.value.getResult();
|
||||||
|
|
||||||
// 将 canvas 转换为 blob
|
// 将 canvas 转换为 blob
|
||||||
canvas.toBlob(async (blob: Blob | null) => {
|
canvas.toBlob(async (blob: Blob | null) => {
|
||||||
if (!blob) return;
|
if (!blob) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('avatarfile', blob, options.fileName || 'avatar.png');
|
formData.append('avatarfile', blob, options.fileName || 'avatar.png');
|
||||||
|
|
||||||
const { error } = await fetchUpdateUserAvatar(formData);
|
const { error } = await fetchUpdateUserAvatar(formData);
|
||||||
if (!error) {
|
if (!error) {
|
||||||
window.$message?.success('头像更新成功!');
|
window.$message?.success('头像更新成功!');
|
||||||
imageUrl.value = URL.createObjectURL(blob);
|
imageUrl.value = URL.createObjectURL(blob);
|
||||||
handleClose();
|
hideDrawer();
|
||||||
}
|
}
|
||||||
}, 'image/png');
|
}, 'image/png');
|
||||||
|
} finally {
|
||||||
|
endLoading();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 关闭对话框 */
|
/** 关闭对话框 */
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
showModal.value = false;
|
hideDrawer();
|
||||||
options.img = imageUrl.value;
|
options.img = imageUrl.value;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="avatar-wrapper" @click="handleEdit">
|
<div class="cursor-pointer" @click="handleEdit">
|
||||||
<div class="avatar-container">
|
<div class="relative h-120px w-120px overflow-hidden rounded-full">
|
||||||
<img :src="imageUrl" alt="user-avatar" class="avatar-image" />
|
<img :src="imageUrl" alt="user-avatar" class="h-full w-full object-cover" />
|
||||||
<div class="avatar-overlay">
|
<div
|
||||||
|
class="absolute inset-0 flex-center bg-black/50 text-white opacity-0 transition-opacity duration-300 hover:opacity-100"
|
||||||
|
>
|
||||||
<SvgIcon icon="ep:plus" class="text-24px" />
|
<SvgIcon icon="ep:plus" class="text-24px" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NModal v-model:show="showModal" preset="card" title="修改头像" class="w-400px" @close="handleClose">
|
<NModal v-model:show="showModal" preset="card" title="修改头像" class="w-400px" @close="handleClose">
|
||||||
<div class="upload-container">
|
<div class="flex-col-center gap-20px py-20px">
|
||||||
<div v-if="options.img !== imageUrl" class="cropper-container">
|
<div v-if="options.img !== imageUrl" class="h-300px w-full">
|
||||||
<Cropper ref="cropperRef" class="cropper" :src="options.img" :stencil-props="options.stencilProps" />
|
<Cropper
|
||||||
|
ref="cropperRef"
|
||||||
|
class="h-full bg-gray-100"
|
||||||
|
:src="options.img"
|
||||||
|
:stencil-props="options.stencilProps"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<img v-else :src="imageUrl" alt="user-avatar" class="preview-image" />
|
<img
|
||||||
<div class="button-group">
|
v-else
|
||||||
|
:src="imageUrl"
|
||||||
|
alt="user-avatar"
|
||||||
|
class="h-200px w-200px border border-gray-200 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
<div class="flex gap-12px">
|
||||||
<NUpload accept=".jpg,.jpeg,.png,.gif" :max="1" :show-file-list="false" @before-upload="handleFileSelect">
|
<NUpload accept=".jpg,.jpeg,.png,.gif" :max="1" :show-file-list="false" @before-upload="handleFileSelect">
|
||||||
<NButton class="upload-button">选择图片</NButton>
|
<NButton class="min-w-100px">选择图片</NButton>
|
||||||
</NUpload>
|
</NUpload>
|
||||||
<NButton v-if="options.img !== imageUrl" type="primary" class="upload-button" @click="handleCrop">
|
<NButton
|
||||||
|
v-if="options.img !== imageUrl"
|
||||||
|
type="primary"
|
||||||
|
class="min-w-100px"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleCrop"
|
||||||
|
>
|
||||||
确认裁剪
|
确认裁剪
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user