This commit is contained in:
zlwu committed 2026-07-12 18:23:02 +08:00
1 parent 2142b33cfa
commit 3520f9ce90
27 files changed
+248553 -252041

No files matched your search

@@ -1,62 +1,72 @@
<!-- 用户菜单 -->
<template>
<ElPopover
ref="userMenuPopover"
placement="bottom-end"
:width="240"
:hide-after="0"
:offset="10"
trigger="hover"
:show-arrow="false"
popper-class="user-menu-popover"
popper-style="padding: 5px 16px;"
>
<template #reference>
<div class="user-avatar-wrapper">
<el-avatar :size="34" :src="avatarDisplayUrl">
{{ userInfo.realName?.charAt(0) || userInfo.username?.charAt(0) || '管' }}
</el-avatar>
</div>
</template>
<template #default>
<div class="pt-3">
<div class="flex-c pb-1 px-0">
<el-avatar :size="40" :src="avatarDisplayUrl" style="margin-right: 12px">
<div class="user-menu-root">
<ElPopover
ref="userMenuPopover"
placement="bottom-end"
:width="240"
:hide-after="0"
:offset="10"
trigger="hover"
:show-arrow="false"
popper-class="user-menu-popover"
popper-style="padding: 5px 16px;"
>
<template #reference>
<div class="user-avatar-wrapper">
<el-avatar :size="34" :src="avatarDisplayUrl">
{{ userInfo.realName?.charAt(0) || userInfo.username?.charAt(0) || '管' }}
</el-avatar>
<div class="w-[calc(100%-60px)] h-full">
<span class="block text-sm font-medium text-g-800 truncate">
{{ userInfo.realName || userInfo.username || '管理员' }}
</span>
<span class="block mt-0.5 text-xs text-g-500 truncate">
{{ getRoleName(userInfo.role) }}
</span>
</div>
</div>
<ul class="py-4 mt-3 border-t border-g-300/80">
<li class="btn-item">
<ArtSvgIcon icon="user" />
<span>{{ userInfo.phone || '暂无电话' }}</span>
</li>
<li v-if="userInfo.regionName" class="btn-item">
<ArtSvgIcon icon="location" />
<span>{{ userInfo.regionName }}</span>
</li>
<div class="w-full h-px my-2 bg-g-300/80"></div>
<div class="log-out c-p" @click="loginOut">
{{ $t('topBar.user.logout') }}
</template>
<template #default>
<div class="pt-3">
<div class="flex-c pb-1 px-0">
<el-avatar :size="40" :src="avatarDisplayUrl" style="margin-right: 12px">
{{ userInfo.realName?.charAt(0) || userInfo.username?.charAt(0) || '管' }}
</el-avatar>
<div class="w-[calc(100%-60px)] h-full">
<span class="block text-sm font-medium text-g-800 truncate">
{{ userInfo.realName || userInfo.username || '管理员' }}
</span>
<span class="block mt-0.5 text-xs text-g-500 truncate">
{{ getRoleName(userInfo.role) }}
</span>
</div>
</div>
</ul>
</div>
</template>
</ElPopover>
<ul class="py-4 mt-3 border-t border-g-300/80">
<li class="btn-item">
<ArtSvgIcon icon="user" />
<span>{{ userInfo.phone || '暂无电话' }}</span>
</li>
<li v-if="userInfo.regionName" class="btn-item">
<ArtSvgIcon icon="location" />
<span>{{ userInfo.regionName }}</span>
</li>
<div class="w-full h-px my-2 bg-g-300/80"></div>
<li class="btn-item" @click="openPasswordDialog">
<ArtSvgIcon icon="ri:lock-password-line" />
<span>{{ $t('topBar.user.changePassword') }}</span>
</li>
<div class="w-full h-px my-2 bg-g-300/80"></div>
<div class="log-out c-p" @click="loginOut">
{{ $t('topBar.user.logout') }}
</div>
</ul>
</div>
</template>
</ElPopover>
<UserPasswordDialog v-model:visible="passwordDialogVisible" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { ElMessageBox } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
import { resolveUserAvatarUrl } from '@/utils/user-avatar'
import UserPasswordDialog from './UserPasswordDialog.vue'
defineOptions({ name: 'ArtUserMenu' })
@@ -69,6 +79,7 @@
)
const userMenuPopover = ref()
const passwordDialogVisible = ref(false)
/**
* 获取角色名称
@@ -83,6 +94,13 @@
return roleMap[role] || role || '未知角色'
}
function openPasswordDialog() {
closeUserMenu()
setTimeout(() => {
passwordDialogVisible.value = true
}, 150)
}
/**
* 用户登出确认
*/
@@ -0,0 +1,151 @@
<!-- 当前用户修改密码 -->
<template>
<el-dialog
v-model="dialogVisible"
title="修改密码"
width="480px"
class="dialog-form-layout"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
@close="handleClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="原密码" prop="oldPassword" class="form-col-span-2">
<el-input
v-model="formData.oldPassword"
type="password"
show-password
placeholder="请输入原密码"
autocomplete="current-password"
/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword" class="form-col-span-2">
<el-input
v-model="formData.newPassword"
type="password"
show-password
placeholder="至少 6 位"
autocomplete="new-password"
/>
</el-form-item>
<el-form-item label="确认新密码" prop="confirmPassword" class="form-col-span-2">
<el-input
v-model="formData.confirmPassword"
type="password"
show-password
placeholder="请再次输入新密码"
autocomplete="new-password"
/>
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="handleClose">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { Close, Check } from '@element-plus/icons-vue'
import { fetchChangePassword } from '@/api/auth'
const props = defineProps<{
visible: boolean
}>()
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void
}>()
const formRef = ref<FormInstance>()
const submitting = ref(false)
const formData = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const formRules: FormRules = {
oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '新密码长度不能少于 6 位', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{
validator: (_rule, value, callback) => {
if (value !== formData.newPassword) {
callback(new Error('两次输入的新密码不一致'))
return
}
callback()
},
trigger: 'blur'
}
]
}
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
function resetForm() {
formData.oldPassword = ''
formData.newPassword = ''
formData.confirmPassword = ''
formRef.value?.clearValidate?.()
}
function handleClose() {
dialogVisible.value = false
}
async function handleSubmit() {
if (!formRef.value) return
try {
await formRef.value.validate()
submitting.value = true
await fetchChangePassword({
oldPassword: formData.oldPassword,
newPassword: formData.newPassword
})
dialogVisible.value = false
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
submitting.value = false
}
}
watch(
() => props.visible,
(val) => {
if (val) {
resetForm()
}
}
)
</script>