vue-element-plus-admin(vue3)二次开发入门指南(6)-个人中心头像上传,基本信息和密码修改,屏幕锁定与解锁的头像显示,登录用户头像显示及自动刷新
需要修改的功能模块
1. 个人中心需要修改的图片显示, 基本资料,修改密码的修改

2. 登录用户需要修改的图片,当修改图像后自动刷新图像,如下图红圈显示

3. 锁定屏幕需要修改图片显示

4. 解锁屏幕的修改图像显示

前端用于和后端进行调度API的设计
首先在src/api的文件夹下新建person文件夹,如下图所示

前端API模型设计,
新建src/api/person/types.ts 用于存放模型对象,内容如下所示
//个人信息模型
export interface PersonInfoType {
id: number
username: string
realName: string
phoneNumber: string
email: string
avatarUrl: string
roleList: string[]
}
//修改密码模型
export interface PersonPasswordType {
username?: string
password: string
newPassword: string
}
export interface PersonImageType {
username?: string
content?: string
}
由于后端有权限校验功能,所以头像不能用静态资源访问的方式访问,所以需要使用前端的API进行访问,并http的返回类型是blob,具体如下getUserImageApi 实现
新建src/api/person/index.ts 用于存放调用后端的API
import request from '@/axios'
import type { PersonInfoType, PersonPasswordType, PersonImageType } from './types'
export const PATH_URL = import.meta.env.VITE_API_BASE_PATH
//请求的模型用于校验请求对象
interface PersonParams {
id?: string
username?: string
}
const PERSON_CENTER_API_URL = '/config/console/api/person/center'
const PERSON_PASSWORD_API_URL = '/config/console/api/person/password'
const PERSON_IMAGE_API_URL = '/config/console/api/person/image'
//得到后端的UserInfo
export const getDetailUserInfoApi = (params: PersonParams): Promise<IResponse<PersonInfoType>> => {
console.log('request person center', params)
return request.get({ url: PERSON_CENTER_API_URL, params })
}
//更新后端的UserInfo 基本信息
export const modifyDetailUserInfoApi = (
data: PersonInfoType
): Promise<IResponse<PersonInfoType>> => {
console.log('update person info center', data)
return request.put({ url: PERSON_CENTER_API_URL, data })
}
//更新后端的UserInfo的密码
export const updateUserInfoPasswordApi = (data: PersonPasswordType): Promise<IResponse<any>> => {
console.log('update person password', data)
return request.put({ url: PERSON_PASSWORD_API_URL, data })
}
//更新后端的UserInfo的图片
export const uploadUserImageApi = (data: PersonImageType): Promise<IResponse<any>> => {
console.log('update person image', data)
return request.put({ url: PERSON_IMAGE_API_URL, data })
}
//得到后端的UserInfo的图片
export const getUserImageApi = (urlStr: string): Promise<IResponse<any>> => {
console.log('get person image', urlStr)
return request.get({ url: urlStr, responseType: 'blob' })
}
总体结构如下图所示

前端用于图像自动刷新事件设计
新建src/store/modules/image.ts ,创建useImageStore对象,用于共享图像刷新共享事件内容如下所示
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useImageStore = defineStore('image', () => {
const refresh = ref(0)
function doRefresh() {
refresh.value++
}
return { refresh, doRefresh }
})
前端代码修改
个人中心修改

个人中心上传头像功能,显示的修改
找到src/views/Person/PersonCenter/PersonCenter.vue 文件修改如下显示
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { ref, unref, onMounted } from 'vue'
import { ElDivider, ElImage, ElTag, ElTabPane, ElTabs, ElButton, ElMessage } from 'element-plus'
import defaultAvatar from '@/assets/imgs/avatar.jpg'
import UploadAvatar from './components/UploadAvatar.vue'
import { Dialog } from '@/components/Dialog'
import EditInfo from './components/EditInfo.vue'
import EditPassword from './components/EditPassword.vue'
import { getDetailUserInfoApi, uploadUserImageApi, getUserImageApi } from '@/api/person'
import { PersonInfoType } from '@/api/person/types'
import { useUserStore } from '@/store/modules/user'
import { useImageStore } from '@/store/modules/image'
//得到客户端本地存储的登录用户信息
const userStore = useUserStore()
//生命模型模型
const userInfo = ref<PersonInfoType>()
//定义根据客户端用户名得到服务器端的用户详情
const fetchDetailUserApi = async () => {
// 这里可以调用接口获取用户信息
const params = { username: userStore.getUserInfo?.username }
console.log('get user store' + params)
const result = await getDetailUserInfoApi(params)
console.log('get user store:', result.data)
userInfo.value = result.data
}
const refreshUserInfo = () => {
fetchDetailUserApi()
}
const activeName = ref('first')
const dialogVisible = ref(false)
const uploadAvatarRef = ref<ComponentRef<typeof UploadAvatar>>()
const avatarLoading = ref(false)
const personImgUrl = ref('')
//发送头像更新事件
const imageStore = useImageStore()
//上传图像后重新刷新显示新头像
const reloadImage = async () => {
if (userInfo.value?.avatarUrl) {
await getUserImageApi(userInfo.value.avatarUrl)
.then((response) => {
personImgUrl.value = URL.createObjectURL(response.data) // 创建可访问的 URL
//发送更新头像事件,用户信息
imageStore.doRefresh()
})
.catch((error) => {
console.error('There get person image an error!', error)
})
} else {
console.log('not avatarUrl')
}
}
//用于初始化加载用户信息和加载页面
onMounted(async () => {
await fetchDetailUserApi()
reloadImage()
})
//用于修改上传头像到后端
const saveAvatar = async () => {
try {
avatarLoading.value = true
const base64 = unref(uploadAvatarRef)?.getBase64()
console.log('base64 content', base64)
// 这里可以调用修改头像接口
const update = { username: userStore.getUserInfo?.username, content: base64! }
const res = await uploadUserImageApi(update)
console.log('update image result', res)
if (res.code == 0) {
ElMessage.success('修改成功')
fetchDetailUserApi()
reloadImage()
} else {
ElMessage.error('上传图片失败')
}
dialogVisible.value = false
} catch (error) {
console.log(error)
} finally {
avatarLoading.value = false
}
}
</script>
<template>
<div class="flex w-100% h-100%">
<ContentWrap title="个人信息" class="w-400px">
<div class="flex justify-center items-center">
<div
class="avatar w-[150px] h-[150px] relative cursor-pointer"
@click="dialogVisible = true"
>
<ElImage
class="w-[150px] h-[150px] rounded-full"
:src="personImgUrl || defaultAvatar"
fit="fill"
/>
</div>
</div>
<ElDivider />
<div class="flex justify-between items-center">
<div>账号:</div>
<div>{{ userInfo?.username }}</div>
</div>
<ElDivider />
<div class="flex justify-between items-center">
<div>昵称:</div>
<div>{{ userInfo?.realName }}</div>
</div>
<ElDivider />
<div class="flex justify-between items-center">
<div>手机号码:</div>
<div>{{ userInfo?.phoneNumber ?? '-' }}</div>
</div>
<ElDivider />
<div class="flex justify-between items-center">
<div>用户邮箱:</div>
<div>{{ userInfo?.email ?? '-' }}</div>
</div>
<ElDivider />
<div class="flex justify-between items-center">
<div>所属角色:</div>
<div>
<template v-if="userInfo?.roleList?.length">
<ElTag v-for="item in userInfo?.roleList || []" :key="item" class="ml-2 mb-w"
>{{ item }}
</ElTag>
</template>
<template v-else>-</template>
</div>
</div>
<ElDivider />
</ContentWrap>
<ContentWrap title="基本资料" class="flex-[3] ml-20px">
<ElTabs v-model="activeName">
<ElTabPane label="基本信息" name="first">
<EditInfo :user-info="userInfo" :refresh-user-info="refreshUserInfo" />
</ElTabPane>
<ElTabPane label="修改密码" name="second">
<EditPassword />
</ElTabPane>
</ElTabs>
</ContentWrap>
</div>
<Dialog v-model="dialogVisible" title="修改头像" width="800px">
<UploadAvatar ref="uploadAvatarRef" :url="userInfo?.avatarUrl || defaultAvatar" />
<template #footer>
<ElButton type="primary" :loading="avatarLoading" @click="saveAvatar"> 保存 </ElButton>
<ElButton @click="dialogVisible = false">关闭</ElButton>
</template>
</Dialog>
</template>
<style lang="less" scoped>
.avatar {
position: relative;
&::after {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
font-size: 50px;
color: #fff;
background-color: rgb(0 0 0 / 40%);
border-radius: 50%;
content: '+';
opacity: 0;
justify-content: center;
align-items: center;
}
&:hover {
&::after {
opacity: 1;
}
}
}
</style>
个人中心基本信息的修改
找到找到src/views/Person/PersonCenter/components/EditInfo.vue 文件修改如下显示
<script lang="ts" setup>
import { FormSchema, Form } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { useValidator } from '@/hooks/web/useValidator'
import { reactive, ref, watch } from 'vue'
import { ElDivider, ElMessage, ElMessageBox } from 'element-plus'
import { modifyDetailUserInfoApi } from '@/api/person'
import { PersonInfoType } from '@/api/person/types'
const props = defineProps({
userInfo: {
type: Object,
default: () => ({})
},
refreshUserInfo: { type: Function }
})
const { required, phone, maxlength, email } = useValidator()
const formSchema = reactive<FormSchema[]>([
{
field: 'realName',
label: '昵称',
component: 'Input',
colProps: {
span: 24
}
},
{
field: 'phoneNumber',
label: '手机号码',
component: 'Input',
colProps: {
span: 24
}
},
{
field: 'email',
label: '邮箱',
component: 'Input',
colProps: {
span: 24
}
}
])
const rules = reactive({
realName: [required(), maxlength(50)],
phoneNumber: [phone()],
email: [email()]
})
const { formRegister, formMethods } = useForm()
const { setValues, getElFormExpose, getFormData } = formMethods
watch(
() => props.userInfo,
(value) => {
setValues(value)
},
{
immediate: true,
deep: true
}
)
const saveLoading = ref(false)
//修改用户基本信息
const save = async () => {
const elForm = await getElFormExpose()
const valid = await elForm?.validate().catch((err) => {
console.log(err)
})
if (valid) {
ElMessageBox.confirm('是否确认修改?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
saveLoading.value = true
// 这里可以调用修改用户信息接口
const data = await getFormData<PersonInfoType>()
const res = await modifyDetailUserInfoApi(data)
if (res.code == 0) {
ElMessage.success('修改成功')
//修改成功后进行刷新
props.refreshUserInfo!()
}
} catch (error) {
console.log(error)
} finally {
saveLoading.value = false
}
})
.catch(() => {})
}
}
</script>
<template>
<Form :rules="rules" @register="formRegister" :schema="formSchema" />
<ElDivider />
<BaseButton type="primary" @click="save">保存</BaseButton>
</template>
个人中心修改密码功能的修改
<script setup lang="ts">
import { Form, FormSchema } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { reactive, ref } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { ElMessage, ElMessageBox, ElDivider } from 'element-plus'
import { updateUserInfoPasswordApi } from '@/api/person'
import { PersonPasswordType } from '@/api/person/types'
import { useUserStore } from '@/store/modules/user'
const { required } = useValidator()
const userStore = useUserStore()
const formSchema = reactive<FormSchema[]>([
{
field: 'password',
label: '旧密码',
component: 'InputPassword',
colProps: {
span: 24
}
},
{
field: 'newPassword',
label: '新密码',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
strength: true
}
},
{
field: 'newPassword2',
label: '确认新密码',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
strength: true
}
}
])
const rules = reactive({
password: [required()],
newPassword: [
required(),
{
asyncValidator: async (_, val, callback) => {
const formData = await getFormData()
const { newPassword2 } = formData
if (val !== newPassword2) {
callback(new Error('新密码与确认新密码不一致'))
} else {
callback()
}
}
}
],
newPassword2: [
required(),
{
asyncValidator: async (_, val, callback) => {
const formData = await getFormData()
const { newPassword } = formData
if (val !== newPassword) {
callback(new Error('确认新密码与新密码不一致'))
} else {
callback()
}
}
}
]
})
const { formRegister, formMethods } = useForm()
const { getFormData, getElFormExpose } = formMethods
const saveLoading = ref(false)
//修改密码和后台交互的逻辑
const save = async () => {
const elForm = await getElFormExpose()
const valid = await elForm?.validate().catch((err) => {
console.log(err)
})
if (valid) {
ElMessageBox.confirm('是否确认修改?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
saveLoading.value = true
// 这里可以调用修改密码的接口
const data = await getFormData<PersonPasswordType>()
data.username = userStore.getUserInfo?.username
const res = await updateUserInfoPasswordApi(data)
if (res.code == 0) {
ElMessage.success('修改密码成功')
//修改成功后重新登录,新密码生效
userStore.logout()
} else {
ElMessage.error('修改密码失败')
}
} catch (error) {
console.log(error)
} finally {
saveLoading.value = false
}
})
.catch(() => {})
}
}
</script>
<template>
<Form :rules="rules" @register="formRegister" :schema="formSchema" />
<ElDivider />
<BaseButton type="primary" @click="save">确认修改</BaseButton>
</template>
登录用户显示

需要修改上图中红圈中的图像为自己上传的图像
在项目中找到文件src/components/UserInfo/src/UserInfo.vue
修改代码如下
<script setup lang="ts">
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { useDesign } from '@/hooks/web/useDesign'
import LockDialog from './components/LockDialog.vue'
import { ref, computed, onMounted, watch } from 'vue'
import LockPage from './components/LockPage.vue'
import { useLockStore } from '@/store/modules/lock'
import { useUserStore } from '@/store/modules/user'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const userStore = useUserStore()
const lockStore = useLockStore()
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('user-info')
const { t } = useI18n()
const loginOut = () => {
userStore.logoutConfirm()
}
const dialogVisible = ref<boolean>(false)
// 锁定屏幕
const lockScreen = () => {
dialogVisible.value = true
}
const toDocument = () => {
window.open('https://element-plus-admin-doc.cn/')
}
const toPage = (path: string) => {
push(path)
}
//修改正确显示上传头像的功能以及修改及时刷新
import { getDetailUserInfoApi, getUserImageApi } from '@/api/person'
import { useImageStore } from '@/store/modules/image'
import { storeToRefs } from 'pinia'
const imageStore = useImageStore()
const { refresh } = storeToRefs(imageStore)
import defaultAvatar from '@/assets/imgs/avatar.jpg'
const imgUrl = ref('')
//得到头像的函数
const getImage = async () => {
const params = { username: userStore.getUserInfo?.username }
const res = await getDetailUserInfoApi(params)
if (res.code == 0) {
console.log('get the image url')
await getUserImageApi(res.data.avatarUrl)
.then((response) => {
imgUrl.value = URL.createObjectURL(response.data) // 创建可访问的 URL
})
.catch((error) => {
console.error('There get person image an error!', error)
})
}
}
//在onMounted阶段重新赋值头像地址
onMounted(() => {
getImage()
})
//监听修改头像事件,监听到自动刷新
watch(refresh, () => {
getImage()
})
</script>
<template>
<ElDropdown class="custom-hover" :class="prefixCls" trigger="click">
<div class="flex items-center">
<!-- 如何有自定义头像就显示头像,没有显示默认-->
<img
:src="imgUrl || defaultAvatar"
alt=""
class="w-[calc(var(--logo-height)-25px)] rounded-[50%]"
/>
<span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">{{
userStore.getUserInfo?.username
}}</span>
</div>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem>
<div @click="toPage('/personal/personal-center')">
{{ t('router.personalCenter') }}
</div>
</ElDropdownItem>
<ElDropdownItem>
<div @click="toDocument">{{ t('common.document') }}</div>
</ElDropdownItem>
<ElDropdownItem divided>
<div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
</ElDropdownItem>
<ElDropdownItem>
<div @click="loginOut">{{ t('common.loginOut') }}</div>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<LockDialog v-if="dialogVisible" v-model="dialogVisible" />
<teleport to="body">
<transition name="fade-bottom" mode="out-in">
<LockPage v-if="getIsLock" />
</transition>
</teleport>
</template>
<style scoped lang="less">
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition:
opacity 0.25s,
transform 0.3s;
}
.fade-bottom-enter-from {
opacity: 0;
transform: translateY(-10%);
}
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10%);
}
</style>
锁定屏幕页面显示头像的修改
在项目中找到文件src/components/UserInfo/src/components/LockDialog.vue
修改代码如下
<script setup lang="ts">
import { useI18n } from '@/hooks/web/useI18n'
import { ref, watch, onMounted } from 'vue'
import { Dialog } from '@/components/Dialog'
import { Form } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { reactive, computed } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { FormSchema } from '@/components/Form'
import { useDesign } from '@/hooks/web/useDesign'
import { useLockStore } from '@/store/modules/lock'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-dialog')
const { required } = useValidator()
const { t } = useI18n()
const lockStore = useLockStore()
const props = defineProps({
modelValue: {
type: Boolean
}
})
const emit = defineEmits(['update:modelValue'])
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
console.log('set: ', val)
emit('update:modelValue', val)
}
})
const dialogTitle = ref(t('lock.lockScreen'))
const rules = reactive({
password: [required()]
})
const schema: FormSchema[] = reactive([
{
label: t('lock.lockPassword'),
field: 'password',
component: 'Input',
componentProps: {
type: 'password',
showPassword: true,
// 按下enter键触发登录
onKeydown: (_e: any) => {
if (_e.key === 'Enter') {
handleLock()
}
}
}
}
])
const { formRegister, formMethods } = useForm()
const { getFormData, getElFormExpose, getComponentExpose } = formMethods
// 自动聚焦输入框
watch(
dialogVisible,
async (val) => {
if (val) {
const formExposeInput = await getComponentExpose('password')
setTimeout(() => {
formExposeInput?.focus()
}, 10)
}
},
{ immediate: true }
)
const handleLock = async () => {
const formExpose = await getElFormExpose()
formExpose?.validate(async (valid) => {
if (valid) {
dialogVisible.value = false
const formData = await getFormData()
lockStore.setLockInfo({
isLock: true,
...formData
})
}
})
}
import { getDetailUserInfoApi } from '@/api/person'
import { useUserStore } from '@/store/modules/user'
import defaultAvatar from '@/assets/imgs/avatar.jpg'
const userStore = useUserStore()
const imgUrl = ref('')
onMounted(async () => {
const params = { username: userStore.getUserInfo?.username }
const res = await getDetailUserInfoApi(params)
if (res.code == 0) {
console.log('get the image url')
imgUrl.value = res.data.avatarUrl
}
})
</script>
<template>
<Dialog
v-model="dialogVisible"
width="500px"
max-height="170px"
:class="prefixCls"
:title="dialogTitle"
>
<div class="flex flex-col items-center">
<img :src="imgUrl || defaultAvatar" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--top-header-text-color)]">{{
userStore.getUserInfo?.username
}}</span>
</div>
<Form :is-col="false" :schema="schema" :rules="rules" @register="formRegister" />
<template #footer>
<BaseButton type="primary" @click="handleLock">{{ t('lock.lock') }}</BaseButton>
</template>
</Dialog>
</template>
<style lang="less" scoped>
:global(.v-lock-dialog) {
@media (width <= 767px) {
max-width: calc(100vw - 16px);
}
}
</style>
解锁屏幕页面显示头像的修改
在项目中找到文件src/components/UserInfo/src/components/LockPage.vue
修改代码如下
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { ElInput } from 'element-plus'
//import { resetRouter } from '@/router'
//import { useRouter } from 'vue-router'
import { useStorage } from '@/hooks/web/useStorage'
import { useLockStore } from '@/store/modules/lock'
import { useI18n } from '@/hooks/web/useI18n'
import { useNow } from '@/hooks/web/useNow'
import { useDesign } from '@/hooks/web/useDesign'
import { Icon } from '@/components/Icon'
import { loginOutApi } from '@/api/login'
import { useTagsViewStore } from '@/store/modules/tagsView'
const tagsViewStore = useTagsViewStore()
const { clear } = useStorage()
//const { replace } = useRouter()
const password = ref('')
const loading = ref(false)
const errMsg = ref(false)
const showDate = ref(true)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-page')
const lockStore = useLockStore()
const { hour, month, minute, meridiem, year, day, week } = useNow(true)
const { t } = useI18n()
// 解锁
async function unLock() {
if (!password.value) {
return
}
const pwd = password.value
try {
loading.value = true
const res = await lockStore.unLock(pwd)
errMsg.value = !res
} finally {
loading.value = false
}
}
// 返回登录
import { useUserStoreWithOut, useUserStore } from '@/store/modules/user'
async function goLogin() {
const res = await loginOutApi().catch(() => {})
if (res) {
clear()
tagsViewStore.delAllViews()
//resetRouter() // 重置静态路由表
lockStore.resetLockInfo()
const userStore = useUserStoreWithOut()
userStore.logout()
}
}
const passwordInputRef = ref<ComponentRef<typeof ElInput>>()
function handleShowForm(show = false) {
showDate.value = show
if (!show) {
requestAnimationFrame(() => {
passwordInputRef.value?.focus()
})
}
}
//以下是头像加载业务逻辑
import { getDetailUserInfoApi, getUserImageApi } from '@/api/person'
import defaultAvatar from '@/assets/imgs/avatar.jpg'
const imgUrl = ref('')
//加载头像功能,后端有权限校验功能
onMounted(async () => {
const userStore = useUserStore()
const params = { username: userStore.getUserInfo?.username }
const res = await getDetailUserInfoApi(params)
if (res.code == 0) {
console.log('get the image url')
await getUserImageApi(res.data.avatarUrl)
.then((response) => {
imgUrl.value = URL.createObjectURL(response.data) // 创建可访问的 URL
})
.catch((error) => {
console.error('There get person image an error!', error)
})
}
})
</script>
<template>
<div
:class="prefixCls"
class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
>
<div
:class="`${prefixCls}__unlock`"
class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
@click="handleShowForm(false)"
v-show="showDate"
>
<Icon icon="vi-ep:lock" />
<span>{{ t('lock.unlock') }}</span>
</div>
<div class="flex w-screen h-screen justify-center items-center">
<div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
<span>{{ hour }}</span>
<span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
{{ meridiem }}
</span>
</div>
<div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
<span> {{ minute }}</span>
</div>
</div>
<transition name="fade-slide">
<div :class="`${prefixCls}-entry`" v-show="!showDate">
<div :class="`${prefixCls}-entry-content`">
<div class="flex flex-col items-center">
<img :src="imgUrl || defaultAvatar" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--logo-title-text-color)]">Archer</span>
</div>
<ElInput
type="password"
:placeholder="t('lock.placeholder')"
class="enter-x"
v-model="password"
@keydown.enter="unLock"
ref="passwordInputRef"
/>
<span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
{{ t('lock.message') }}
</span>
<div :class="`${prefixCls}-entry__footer enter-x`">
<BaseButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="handleShowForm(true)"
>
{{ t('common.back') }}
</BaseButton>
<BaseButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="goLogin"
>
{{ t('lock.backToLogin') }}
</BaseButton>
<BaseButton
type="primary"
class="mt-2"
size="small"
link
@click="unLock()"
:disabled="loading"
>
{{ t('lock.entrySystem') }}
</BaseButton>
</div>
</div>
</div>
</transition>
<div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
<div class="text-5xl mb-4 enter-x" v-show="!showDate">
{{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
</div>
<div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
</div>
</div>
</template>
<style lang="less" scoped>
@prefix-cls: ~'@{adminNamespace}-lock-page';
// Small screen / tablet
@screen-sm: 576px;
// Medium screen / desktop
@screen-md: 768px;
// Large screen / wide desktop
@screen-lg: 992px;
// Extra large screen / full hd
@screen-xl: 1200px;
// Extra extra large screen / large desktop
@screen-2xl: 1600px;
@error-color: #ed6f6f;
.@{prefix-cls} {
z-index: 3000;
&__unlock {
transform: translate(-50%, 0);
}
&__hour,
&__minute {
display: flex;
font-weight: 700;
color: #bababa;
background-color: #141313;
border-radius: 30px;
justify-content: center;
align-items: center;
@media screen and (max-width: @screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (min-width: @screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (max-width: @screen-sm) {
span:not(.meridiem) {
font-size: 90px;
}
}
@media screen and (min-width: @screen-lg) {
span:not(.meridiem) {
font-size: 220px;
}
}
@media screen and (min-width: @screen-xl) {
span:not(.meridiem) {
font-size: 260px;
}
}
@media screen and (min-width: @screen-2xl) {
span:not(.meridiem) {
font-size: 320px;
}
}
}
&-entry {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
background-color: rgb(0 0 0 / 50%);
backdrop-filter: blur(8px);
justify-content: center;
align-items: center;
&-content {
width: 260px;
}
&__header {
text-align: center;
&-img {
width: 70px;
margin: 0 auto;
border-radius: 50%;
}
&-name {
margin-top: 5px;
font-weight: 500;
color: #bababa;
}
}
&__err-msg {
display: inline-block;
margin-top: 10px;
color: @error-color;
}
&__footer {
display: flex;
justify-content: space-between;
}
}
}
</style>
后端代码修改
后端的代码只是为了和前端交互具体的逻辑可以根据自己需求更改
后端的模型设计
上传头像模型对象
package com.finance.vue3.model;
import java.io.Serial;
import java.io.Serializable;
/**
* 用于上传用户头像的模型
*/
public class PersonImage implements Serializable {
@Serial
private static final long serialVersionUID = 3275978170164117541L;
private String username;
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
个人基本信息模型对象
package com.finance.vue3.model;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 用于个人信息的模型
*/
public class PersonInfo implements Serializable {
@Serial
private static final long serialVersionUID = 6910484354874886238L;
private long id;
private String username;
private String realName;
private String phoneNumber;
private String email; private String avatarUrl;
private List<String> roleList;
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public List<String> getRoleList() {
return roleList;
}
public void setRoleList(List<String> roleList) {
this.roleList = roleList;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
个人用户密码模型
package com.finance.vue3.model;
import java.io.Serial;
import java.io.Serializable;
/**
* 用户个人用户密码的修改
*/
public class PersonPassword implements Serializable {
@Serial
private static final long serialVersionUID = -2873499965806193913L;
private String username;
private String password;
private String newPassword;
public String getNewPassword() {
return newPassword;
}
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
后端业务逻辑代码
private Map<String, PersonInfo> persons = new HashMap<>();
@PostConstruct
private void initPersonInfo(){
PersonInfo info = new PersonInfo();
info.setId(1L);
info.setUsername(authConfig.getUser());
info.setRealName("Admin");
info.setPhoneNumber("18888888886");
info.setEmail("502431556@qq.com");
info.setAvatarUrl("/config/console/api/person/image?username=admin");
List<String> roles = new ArrayList<>();
roles.add("超级管理员");
info.setRoleList(roles);
persons.put(info.getUsername(), info);
}
//得到用户中心的用户信息
@Bean
public RouterFunction<ServerResponse> getPersonCenterRouterFunction(ResourceLoader resourceLoader) {
return route(GET(PERSON_CENTER_API_URL).and(accept(MediaType.APPLICATION_JSON)), request -> {
String bastPath = request.uri().toString();
logger.info("request person path:{}", bastPath);
String username = request.queryParam("username").get().trim();
logger.info("username;{}", username);
PersonInfo info = persons.get(username);
String val = JSON.toJSONString(APIResponseDTO.buildSuccess(info), SerializerFeature.PrettyFormat);
logger.info("result:{}", val);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(val));
}
);
}
//修改用户信息
@Bean
public RouterFunction<ServerResponse> updateUserInfoRouterFunction(ResourceLoader resourceLoader) {
return route(PUT(PERSON_CENTER_API_URL).and(accept(MediaType.APPLICATION_JSON)), request -> {
String bastPath = request.uri().toString();
logger.info("request person information path:{}", bastPath);
Mono<PersonInfo> updatePersonMono = request.bodyToMono(PersonInfo.class);
return updatePersonMono.flatMap(user -> {
logger.info("-username----------{}---",user.getUsername());
try {
PersonInfo info = persons.get(user.getUsername());
if(user.getRealName() != null){
info.setRealName(user.getRealName());
}
if (user.getEmail() != null){
info.setEmail(user.getEmail());
}
if (user.getPhoneNumber() != null){
info.setPhoneNumber(user.getPhoneNumber());
}
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.body( Mono.just(JSON.toJSONString( APIResponseDTO.buildSuccess(info))), String.class);
}catch (Exception e){
logger.error(e.getMessage(), e);
APIResponseDTO respErr = APIResponseDTO.buildFailure(-1);
respErr.setMsg("error");
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.body( Mono.just(JSON.toJSONString(respErr)), String.class);
}
});
}
);
}
//修改用户密码
@Bean
public RouterFunction<ServerResponse> updateUserInfoPasswdRouterFunction(ResourceLoader resourceLoader) {
return route(PUT(PERSON_PASSWORD_API_URL).and(accept(MediaType.APPLICATION_JSON)), request -> {
String bastPath = request.uri().toString();
logger.info("request person information path:{}", bastPath);
Mono<PersonPassword> updatePasswordMono = request.bodyToMono(PersonPassword.class);
return updatePasswordMono.flatMap(user -> {
logger.info("-username----------{}---",user.getUsername());
try {
if (user.getUsername().equals(authConfig.getUser()) && user.getPassword().equals(authConfig.getPassword())){
authConfig.setPassword(user.getNewPassword());
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.body( Mono.just(JSON.toJSONString( APIResponseDTO.buildSuccess(null))), String.class);
}else {
throw new RuntimeException("update password failure");
}
}catch (Exception e){
logger.error(e.getMessage(), e);
APIResponseDTO respErr = APIResponseDTO.buildFailure(-1);
respErr.setMsg("error");
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.body( Mono.just(JSON.toJSONString(respErr)), String.class);
}
});
}
);
}
//得到用户的用户缩略图
@Bean
public RouterFunction<ServerResponse> getUserInfoImageRouterFunction(ResourceLoader resourceLoader) {
return route(GET(PERSON_IMAGE_API_URL).and(accept(MediaType.ALL)), request -> {
String bastPath = request.uri().toString();
logger.info("get request user image path:{}", bastPath);
String username = request.queryParam("username").get().trim();
String fileName = imgFileDir.getAbsolutePath()+ File.separator+ username+".png";
File file = new File(fileName);
MediaType mediaType = MediaType.IMAGE_PNG;
FileSystemResource resource = new FileSystemResource(fileName);
byte[] imageBytes = null; // 获取或生成图片字节数组
try {
imageBytes = resource.getContentAsByteArray();
} catch (IOException e) {
logger.error(e.getMessage(), e);
return ServerResponse.notFound().build();
}
return ServerResponse.ok().contentType(mediaType).bodyValue(imageBytes);
}
);
}
//更新用户头像
@Bean
public RouterFunction<ServerResponse> updateUserInfoImageRouterFunction(ResourceLoader resourceLoader) {
return route(PUT(PERSON_IMAGE_API_URL).and(accept(MediaType.APPLICATION_JSON)), request -> {
String bastPath = request.uri().toString();
logger.info("put request person image path:{}", bastPath);
Mono<PersonImage> updatePasswordMono = request.bodyToMono(PersonImage.class);
return updatePasswordMono.flatMap(image -> {
try {
logger.info("-username----------{}---",image.getUsername());
//logger.info(image.getContent());
String fileName = imgFileDir.getAbsolutePath()+File.separator+ image.getUsername()+".png";
int lth = "data:image/png;base64,".length();
File file = new File(fileName);
if (file.exists()){
file.delete();
logger.info("delete file: {}", file.getAbsolutePath());
}
logger.info("begin write file--{}", file.getAbsolutePath());
FileUtils.writeFile(file, Base64Utils.decodeFromString(image.getContent().substring(lth)));
logger.info("write file successfully");
//在实际应用中baseUrl一般需要根据发布应用的地址(有时候是gateway的地址)拼接
String url = "/config/console/api/person/image?username=" + image.getUsername();
persons.get(image.getUsername()).setAvatarUrl(url);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body( Mono.just(JSON.toJSONString( APIResponseDTO.buildSuccess("update success image"))), String.class);
}catch (Exception e){
logger.error(e.getMessage(), e);
APIResponseDTO respErr = APIResponseDTO.buildFailure(-1);
respErr.setMsg("error");
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.body( Mono.just(JSON.toJSONString(respErr)), String.class);
}
});
}
);
}
后端模块的权限校验
person模块的权限校验,使用过滤器统一处理的业务代码如下所示,只是做了简单的url,在实际应用中可以根据自己的业务自己设计
package com.finance.vue3.web.controller;
import com.alibaba.fastjson.JSON;
import com.finance.vue3.model.APIResponseDTO;
import com.finance.vue3.service.JWTService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Configuration
public class AuthRequestFilter implements WebFilter {
@Autowired
private JWTService jwtService;
private Logger logger = LoggerFactory.getLogger(AuthRequestFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();
if (url.startsWith("/config/console/api/route/") || url.startsWith("/config/console/api/person/")){
try {
String token = exchange.getRequest().getHeaders().get("token").get(0);
System.out.println("Request is being --------filtered--"+ url);
logger.info("header token:{}", token);
jwtService.verifyAPPToken(token);
} catch (Exception e) {
logger.error("reqUrl:"+ url +"------------"+ e.getMessage(), e);
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
// 获取DataBufferFactory用于创建DataBuffer
DataBufferFactory bufferFactory = response.bufferFactory();
// 创建DataBuffer并写入内容
DataBuffer buffer = bufferFactory.wrap(JSON.toJSONString(APIResponseDTO.buildFailure(401, "Unauthorized or token expire")).getBytes());
// 将DataBuffer写入响应体
return response.writeWith(Mono.just(buffer));
}
}
return chain.filter(exchange).onErrorResume(Exception.class, e -> {
logger.error(e.getMessage(), e);
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add("Content-Type", "application/json");
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(
JSON.toJSONString(APIResponseDTO.buildFailure(400, e.getMessage())).getBytes()
);
return response.writeWith(Mono.just(buffer));
} );
}
}
后端代码的详细,请查看项目: https://gitee.com/tigershi123/finance-back-parent.git
更多推荐



所有评论(0)