需要修改的功能模块

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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐