Files
KCGL/inventory-web/src/App.vue
2026-05-12 14:07:26 +08:00

495 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus'
import { InfoFilled, SwitchButton, UserFilled, Lock, User, ArrowDown } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import { getMyProfile, changeMyPassword, updateMyEmail } from '@/api/auth'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 计算属性:判断当前是否是登录页
const isLoginPage = computed(() => {
return route.path === '/login'
})
// 页面加载时刷新权限
onMounted(() => {
if (userStore.token) {
userStore.refreshUserPermissions()
}
})
// ================================================================
// 个人中心 / 修改密码
// ================================================================
const profileDialogVisible = ref(false)
const profileLoading = ref(false)
const passwordLoading = ref(false)
interface ProfileData {
id: number
username: string
display_name: string
department: string
email: string
}
const profileForm = ref<ProfileData>({
id: 0,
username: '',
display_name: '',
department: '',
email: ''
})
const passwordForm = ref({
new_password: '',
confirm_password: ''
})
const passwordFormRef = ref()
// ================================================================
// 绑定/修改邮箱
// ================================================================
const emailDialogVisible = ref(false)
const emailLoading = ref(false)
const emailFormRef = ref()
interface EmailForm {
email: string
}
const emailForm = ref<EmailForm>({
email: ''
})
const emailRules = {
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] }
]
}
// 打开邮箱弹窗
const openEmailDialog = () => {
emailForm.value.email = profileForm.value.email || ''
emailDialogVisible.value = true
}
// 提交邮箱修改
const submitEmailUpdate = async () => {
const formRef = emailFormRef.value
if (!formRef) return
await formRef.validate(async (valid: boolean) => {
if (valid) {
emailLoading.value = true
try {
await updateMyEmail({ email: emailForm.value.email })
ElMessage.success('邮箱绑定成功')
emailDialogVisible.value = false
// 刷新个人资料
openProfileDialog()
} catch (e: any) {
ElMessage.error(e?.response?.data?.msg || e?.message || '绑定失败')
} finally {
emailLoading.value = false
}
}
})
}
// 重置表单
const resetEmailForm = () => {
emailFormRef.value?.resetFields()
}
// 打开个人中心弹窗
const openProfileDialog = async () => {
profileDialogVisible.value = true
profileLoading.value = true
passwordForm.value = { new_password: '', confirm_password: '' }
try {
// 【修复】axios 拦截器已解包 response.data
// res 本身已是 { msg, data: { id, username, display_name, department } }
// 故直接取 res.data 即可,多跳一层 res.data.data 会取到 undefined
const res: any = await getMyProfile()
const payload = res.data || res
if (payload && payload.data) {
profileForm.value = payload.data
} else if (payload && payload.username) {
// 兜底:响应已经是平铺结构
profileForm.value = payload
}
} catch (e: any) {
ElMessage.error(e?.message || '获取个人资料失败')
} finally {
profileLoading.value = false
}
}
// 提交修改密码无需旧密码JWT 已证明身份)
const submitPasswordChange = async () => {
const { new_password, confirm_password } = passwordForm.value
if (!new_password || !confirm_password) {
ElMessage.warning('请填写新密码和确认密码')
return
}
if (new_password.length < 6) {
ElMessage.warning('新密码长度不能少于6位')
return
}
if (new_password !== confirm_password) {
ElMessage.warning('新密码与确认密码不一致')
return
}
passwordLoading.value = true
try {
const res: any = await changeMyPassword({
new_password,
confirm_password
})
const msg = res?.msg || '修改成功'
ElMessage.success(msg)
profileDialogVisible.value = false
setTimeout(() => {
userStore.logout()
router.replace('/login')
}, 1500)
} catch (e: any) {
const errMsg = e?.response?.data?.msg || e?.message || '修改失败'
ElMessage.error(errMsg)
} finally {
passwordLoading.value = false
}
}
// ================================================================
// 退出登录
// ================================================================
const handleLogout = () => {
ElMessageBox.confirm(
'确定要退出系统吗?',
'提示',
{
confirmButtonText: '确定退出',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
userStore.logout()
ElMessage({ type: 'success', message: '已安全退出' })
await router.replace('/login')
})
.catch(() => {})
}
</script>
<template>
<div class="app-wrapper">
<header v-if="!isLoginPage" class="app-header">
<div class="logo-container">
<router-link to="/" class="home-link">
<img src="@/assets/iris.png" class="logo" alt="Logo" />
<span class="system-title">MOM</span>
</router-link>
</div>
<div class="header-right">
<!-- 用户下拉菜单 -->
<el-dropdown trigger="click" @command="(cmd: string) => { if (cmd === 'profile') openProfileDialog(); if (cmd === 'logout') handleLogout(); }">
<div class="user-profile">
<el-avatar :size="32" :icon="UserFilled" class="user-avatar" />
<span class="user-name">{{ userStore.username || '管理员' }}</span>
<el-icon class="dropdown-arrow"><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><User /></el-icon> 个人中心
</el-dropdown-item>
<el-dropdown-item command="logout" divided>
<el-icon><SwitchButton /></el-icon> 退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</header>
<main class="app-content">
<router-view />
</main>
<footer v-if="!isLoginPage" class="app-footer">
<span class="version-tag">
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
当前版本:V3.17(4.29部署
</span>
</footer>
<!-- ================================================================ -->
<!-- 个人中心 / 修改密码 弹窗 -->
<!-- 严格脱敏不展示系统角色字段 -->
<!-- ================================================================ -->
<el-dialog
v-model="profileDialogVisible"
title="个人中心"
width="480px"
:close-on-click-modal="!passwordLoading"
destroy-on-close
class="profile-dialog"
>
<div v-loading="profileLoading">
<!-- 个人信息只读 -->
<div class="profile-info-section">
<div class="section-title">
<el-icon><User /></el-icon> 个人信息
</div>
<el-descriptions :column="1" border class="profile-descriptions">
<el-descriptions-item label="姓名/账号">
<el-tag type="info" effect="plain">{{ profileForm.display_name || '-' }}</el-tag>
<span class="account-hint">账号: {{ profileForm.username || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="所属部门">
<el-tag type="success" effect="light">{{ profileForm.department || '-' }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<!-- 严格脱敏系统角色字段已移除不在此展示 -->
</div>
<div style="margin: 16px 0; text-align: center;">
<el-button type="primary" plain @click="openEmailDialog">
绑定/修改邮箱
</el-button>
</div>
<el-divider>
<el-icon><Lock /></el-icon> 修改密码
</el-divider>
<!-- 修改密码表单无需旧密码JWT 已验证身份 -->
<el-form
ref="passwordFormRef"
:model="passwordForm"
label-width="110px"
class="password-form"
>
<el-form-item label="新密码" prop="new_password">
<el-input
v-model="passwordForm.new_password"
type="password"
placeholder="请输入新密码至少6位"
show-password
clearable
/>
</el-form-item>
<el-form-item label="确认新密码" prop="confirm_password">
<el-input
v-model="passwordForm.confirm_password"
type="password"
placeholder="请再次输入新密码"
show-password
clearable
/>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="profileDialogVisible = false" :disabled="passwordLoading">
取消
</el-button>
<el-button
type="primary"
:loading="passwordLoading"
@click="submitPasswordChange"
class="confirm-btn"
>
<el-icon v-if="!passwordLoading"><Lock /></el-icon>
确认修改
</el-button>
</div>
</template>
</el-dialog>
<!-- 绑定/修改邮箱弹窗 -->
<el-dialog v-model="emailDialogVisible" title="绑定/修改邮箱" width="400px" @close="resetEmailForm">
<el-form :model="emailForm" :rules="emailRules" ref="emailFormRef" label-width="80px">
<el-form-item label="新邮箱" prop="email">
<el-input v-model="emailForm.email" placeholder="请输入有效邮箱地址" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="emailDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="emailLoading" @click="submitEmailUpdate">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<style>
.app-wrapper {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
overflow: hidden;
background-color: #f5f7fa;
}
.app-header {
height: 60px;
background-color: #ffffff;
border-bottom: 1px solid #dcdfe6;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
flex-shrink: 0;
z-index: 1000;
}
.logo-container {
display: flex;
align-items: center;
height: 100%;
}
.home-link {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
cursor: pointer;
height: 100%;
user-select: none;
}
.logo {
height: 32px;
width: auto;
object-fit: contain;
}
.system-title {
font-size: 18px;
font-weight: 600;
color: #303133;
letter-spacing: 0.5px;
white-space: nowrap;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.user-profile {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 4px 8px;
border-radius: 8px;
transition: background-color 0.2s;
}
.user-profile:hover {
background-color: #f0f2f5;
}
.user-avatar {
background-color: #409eff;
}
.user-name {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.dropdown-arrow {
font-size: 12px;
color: #909399;
margin-left: 2px;
}
.app-content {
flex: 1;
min-height: 0;
width: 100%;
position: relative;
overflow: hidden;
}
.app-footer {
height: 30px;
background-color: #f0f2f5;
border-top: 1px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 12px;
color: #909399;
z-index: 1000;
}
.version-tag {
display: flex;
align-items: center;
}
/* 个人中心弹窗样式 */
.profile-info-section {
margin-bottom: 8px;
}
.section-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.profile-descriptions .account-hint {
margin-left: 8px;
font-size: 12px;
color: #909399;
}
.password-form {
margin-top: 8px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.confirm-btn {
display: flex;
align-items: center;
gap: 4px;
}
</style>