<template>
|
<div class="navbar visitor-navbar">
|
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav" />
|
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav" />
|
|
<div class="right-menu visitor-top-actions">
|
<button class="visitor-icon-button" type="button" title="全屏" @click="toggleFullScreen">
|
<span class="expand-icon"></span>
|
</button>
|
<span class="visitor-user-avatar">{{ userInitial }}</span>
|
<el-dropdown class="visitor-user-menu" trigger="click" @command="handleUserCommand">
|
<button class="visitor-user-name" type="button">
|
<span>{{ displayName }}</span>
|
<i class="el-icon-caret-bottom"></i>
|
</button>
|
<el-dropdown-menu slot="dropdown" class="visitor-user-dropdown">
|
<el-dropdown-item v-if="!isMiddlePlatformMode" command="profile">账号设置</el-dropdown-item>
|
<el-dropdown-item v-if="!isMiddlePlatformMode" command="password">重置密码</el-dropdown-item>
|
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
</el-dropdown-menu>
|
</el-dropdown>
|
</div>
|
|
<el-dialog title="个人信息设置" :visible.sync="profileOpen" width="486px" append-to-body>
|
<el-form ref="profileForm" :model="profileForm" :rules="profileRules" label-width="86px" class="visitor-profile-form">
|
<el-form-item label="用户名称" prop="nickName">
|
<el-input v-model="profileForm.nickName" placeholder="请输入" />
|
</el-form-item>
|
<el-form-item label="头像">
|
<div class="visitor-avatar-upload" @click="chooseAvatar">
|
<img v-if="avatarPreview" :src="avatarPreview" alt="" />
|
<span v-else>+</span>
|
</div>
|
<input ref="avatarInput" type="file" accept="image/*" hidden @change="handleAvatarChange" />
|
</el-form-item>
|
</el-form>
|
<div slot="footer" class="dialog-footer">
|
<el-button @click="profileOpen = false">取消</el-button>
|
<el-button type="primary" :loading="profileSaving" @click="saveProfile">保存</el-button>
|
</div>
|
</el-dialog>
|
|
<el-dialog title="重置密码" :visible.sync="passwordOpen" width="646px" append-to-body>
|
<el-form ref="passwordForm" :model="passwordForm" :rules="passwordRules" label-width="96px" class="visitor-password-form">
|
<el-form-item label="原密码" prop="oldPassword">
|
<el-input v-model="passwordForm.oldPassword" type="password" show-password placeholder="请输入至少12位含大小写字母、特殊字符的密码" />
|
</el-form-item>
|
<el-form-item label="新密码" prop="newPassword">
|
<el-input v-model="passwordForm.newPassword" type="password" show-password placeholder="请输入至少12位含大小写字母、特殊字符的密码" />
|
</el-form-item>
|
<el-form-item label="确认密码" prop="confirmPassword">
|
<el-input v-model="passwordForm.confirmPassword" type="password" show-password placeholder="请输入至少12位含大小写字母、特殊字符的密码" />
|
</el-form-item>
|
</el-form>
|
<div slot="footer" class="dialog-footer">
|
<el-button @click="passwordOpen = false">取消</el-button>
|
<el-button type="primary" :loading="passwordSaving" @click="savePassword">确认</el-button>
|
</div>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script>
|
import { mapGetters } from 'vuex'
|
import Breadcrumb from '@/components/Breadcrumb'
|
import TopNav from '@/components/TopNav'
|
import Hamburger from '@/components/Hamburger'
|
import { getUserProfile, updateUserProfile, updateUserPwd, uploadAvatar } from '@/api/system/user'
|
import { getVisitorApiUrl } from '@/utils/request'
|
|
export default {
|
components: {
|
Breadcrumb,
|
TopNav,
|
Hamburger
|
},
|
// 目的:初始化个人信息、重置密码弹窗状态和表单校验规则。
|
data() {
|
// 目的:校验新密码复杂度,满足至少 12 位且包含大小写字母和特殊字符。
|
const strongPassword = (rule, value, callback) => {
|
if (!value) {
|
callback(new Error('请输入密码'))
|
return
|
}
|
if (!this.isStrongPassword(value)) {
|
callback(new Error('至少12位,需包含大小写字母和特殊字符'))
|
return
|
}
|
callback()
|
}
|
// 目的:校验确认密码与新密码一致,避免提交不一致的重置密码。
|
const confirmPassword = (rule, value, callback) => {
|
if (value !== this.passwordForm.newPassword) {
|
callback(new Error('两次输入的新密码不一致'))
|
return
|
}
|
callback()
|
}
|
return {
|
profileOpen: false,
|
passwordOpen: false,
|
profileSaving: false,
|
passwordSaving: false,
|
avatarFile: null,
|
avatarPreview: '',
|
profileForm: {
|
nickName: '',
|
phonenumber: '',
|
email: '',
|
sex: '2'
|
},
|
passwordForm: {
|
oldPassword: '',
|
newPassword: '',
|
confirmPassword: ''
|
},
|
profileRules: {
|
nickName: [{ required: true, message: '请输入用户名称', trigger: 'blur' }]
|
},
|
passwordRules: {
|
oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
|
newPassword: [{ required: true, validator: strongPassword, trigger: 'blur' }],
|
confirmPassword: [{ required: true, message: '请确认新密码', trigger: 'blur' }, { validator: confirmPassword, trigger: 'blur' }]
|
}
|
}
|
},
|
computed: {
|
...mapGetters([
|
'sidebar',
|
'avatar',
|
'name',
|
'device'
|
]),
|
// 目的:生成导航栏展示名称,未取到用户名称时使用默认文本。
|
displayName() {
|
return this.name || '用户'
|
},
|
// 目的:提取用户名称首字作为头像占位内容。
|
userInitial() {
|
return this.displayName.substring(0, 1)
|
},
|
// 目的:读取顶部导航配置,控制面包屑和顶部菜单展示。
|
topNav() {
|
return this.$store.state.settings.topNav
|
}
|
},
|
methods: {
|
// 目的:切换左侧菜单展开状态。
|
toggleSideBar() {
|
this.$store.dispatch('app/toggleSideBar')
|
},
|
// 目的:切换浏览器全屏状态,供访客后台大屏式查看使用。
|
toggleFullScreen() {
|
if (!document.fullscreenElement && document.documentElement.requestFullscreen) {
|
document.documentElement.requestFullscreen()
|
return
|
}
|
if (document.exitFullscreen) {
|
document.exitFullscreen()
|
}
|
},
|
// 目的:处理用户下拉菜单命令,并在中台模式下禁止账号设置和重置密码入口。
|
handleUserCommand(command) {
|
if (command === 'profile') {
|
// 集群版对接中台时,账号设置入口按业务要求隐藏;这里兜底防止手动触发命令。
|
if (this.isMiddlePlatformMode) {
|
return
|
}
|
this.openProfile()
|
} else if (command === 'password') {
|
// 重置密码同样不允许在中台模式下打开,后端也会拒绝对应接口。
|
if (this.isMiddlePlatformMode) {
|
return
|
}
|
this.openPassword()
|
} else if (command === 'logout') {
|
this.logout()
|
}
|
},
|
// 目的:打开个人信息设置弹窗并加载当前用户资料。
|
openProfile() {
|
this.profileOpen = true
|
this.avatarFile = null
|
this.avatarPreview = this.avatar || ''
|
getUserProfile().then(res => {
|
const user = res.data || res.user || {}
|
this.profileForm = {
|
nickName: user.nickName || user.userName || this.displayName,
|
phonenumber: user.phonenumber || '',
|
email: user.email || '',
|
sex: user.sex != null ? String(user.sex) : '2'
|
}
|
this.avatarPreview = user.avatar ? getVisitorApiUrl(user.avatar) : this.avatar
|
})
|
},
|
// 目的:触发隐藏的头像文件选择控件。
|
chooseAvatar() {
|
this.$refs.avatarInput.click()
|
},
|
// 目的:校验头像文件类型并生成本地预览地址。
|
handleAvatarChange(event) {
|
const file = event.target.files && event.target.files[0]
|
if (!file) {
|
return
|
}
|
if (!/^image\//.test(file.type)) {
|
this.$modal.msgError('请选择图片文件')
|
event.target.value = ''
|
return
|
}
|
this.avatarFile = file
|
this.avatarPreview = URL.createObjectURL(file)
|
},
|
// 目的:保存个人信息和头像,并同步 Vuex 中的展示名称和头像。
|
saveProfile() {
|
this.$refs.profileForm.validate(valid => {
|
if (!valid) {
|
return
|
}
|
this.profileSaving = true
|
updateUserProfile(this.profileForm).then(() => {
|
if (!this.avatarFile) {
|
return null
|
}
|
const formData = new FormData()
|
formData.append('avatarfile', this.avatarFile)
|
return uploadAvatar(formData)
|
}).then(res => {
|
this.$store.commit('SET_NAME', this.profileForm.nickName)
|
if (res && res.imgUrl) {
|
this.$store.commit('SET_AVATAR', getVisitorApiUrl(res.imgUrl))
|
}
|
this.$modal.msgSuccess('保存成功')
|
this.profileOpen = false
|
}).finally(() => {
|
this.profileSaving = false
|
})
|
})
|
},
|
// 目的:打开重置密码弹窗并清空旧校验状态。
|
openPassword() {
|
this.passwordForm = {
|
oldPassword: '',
|
newPassword: '',
|
confirmPassword: ''
|
}
|
this.passwordOpen = true
|
this.$nextTick(() => this.$refs.passwordForm && this.$refs.passwordForm.clearValidate())
|
},
|
// 目的:提交当前用户重置密码请求,成功后退出登录重新认证。
|
savePassword() {
|
this.$refs.passwordForm.validate(valid => {
|
if (!valid) {
|
return
|
}
|
this.passwordSaving = true
|
updateUserPwd(this.passwordForm.oldPassword, this.passwordForm.newPassword).then(() => {
|
this.$modal.msgSuccess('密码修改成功,请重新登录')
|
this.passwordOpen = false
|
setTimeout(() => {
|
this.$store.dispatch('LogOut').then(() => {
|
this.$router.push('/login')
|
})
|
}, 600)
|
}).finally(() => {
|
this.passwordSaving = false
|
})
|
})
|
},
|
// 目的:判断密码是否满足本地复杂度规则。
|
isStrongPassword(password) {
|
return password.length >= 12 && /[a-z]/.test(password) && /[A-Z]/.test(password) && /[^A-Za-z0-9]/.test(password)
|
},
|
// 目的:确认退出登录并清理前端登录态。
|
async logout() {
|
this.$confirm('确定注销并退出系统吗?', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
}).then(() => {
|
this.$store.dispatch('LogOut').then(() => {
|
this.$router.push('/login')
|
})
|
}).catch(() => {})
|
}
|
}
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
.visitor-navbar {
|
.hamburger-container {
|
line-height: 42px;
|
height: 42px;
|
float: left;
|
cursor: pointer;
|
}
|
|
.breadcrumb-container {
|
display: none;
|
}
|
|
.topmenu-container {
|
position: absolute;
|
left: 50px;
|
}
|
}
|
|
.visitor-top-actions {
|
height: 42px;
|
line-height: 42px;
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
color: #20324d;
|
}
|
|
.visitor-icon-button {
|
width: 32px;
|
height: 32px;
|
display: grid;
|
place-items: center;
|
border: 0;
|
border-radius: 10px;
|
background: #fff;
|
box-shadow: 0 6px 18px rgba(36, 86, 160, 0.08);
|
cursor: pointer;
|
}
|
|
.expand-icon {
|
width: 14px;
|
height: 14px;
|
display: block;
|
border: 2px solid #7d91ad;
|
border-radius: 3px;
|
}
|
|
.visitor-user-avatar {
|
width: 22px;
|
height: 22px;
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
border-radius: 50%;
|
color: #fff;
|
background: linear-gradient(135deg, #3c78ff, #786bff);
|
font-size: 12px;
|
font-weight: 700;
|
}
|
|
.visitor-user-name {
|
border: 0;
|
color: #20324d;
|
background: transparent;
|
font-size: 13px;
|
font-weight: 700;
|
cursor: pointer;
|
}
|
|
.visitor-avatar-upload {
|
width: 72px;
|
height: 72px;
|
display: grid;
|
place-items: center;
|
border: 1px solid #d2d7df;
|
color: #666;
|
background: #fafafa;
|
font-size: 28px;
|
cursor: pointer;
|
overflow: hidden;
|
|
img {
|
width: 100%;
|
height: 100%;
|
object-fit: cover;
|
}
|
}
|
|
.visitor-profile-form,
|
.visitor-password-form {
|
padding: 20px 56px 0;
|
}
|
</style>
|