新增用户页面,以及用户头像支持,用户头像修改,修改默认用户头像

This commit is contained in:
洛洛希雅Lolosia 2023-06-06 08:55:48 +08:00
parent cd59783e90
commit 476f8394b1
8 changed files with 441 additions and 9 deletions

View File

@ -2,7 +2,7 @@
"name": "vue3-admin-plus",
"version": "2.0.2",
"license": "MIT",
"author": "kuanghua",
"author": "一七年夏",
"packageManager": "pnpm@7.9.0",
"scripts": {
"dev": "vite --mode serve-dev",
@ -44,6 +44,7 @@
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue": "^3.2.37",
"vue-clipboard3": "^2.0.0",
"vue-cropper": "next",
"vue-i18n": "9.1.10",
"vue-router": "^4.1.5",
"vxe-table": "^4.3.5",

View File

@ -1,5 +1,5 @@
//获取用户信息
import { post } from '@/utils/request'
import request, { post } from '@/utils/request'
export interface IUser {
id: string
@ -38,6 +38,10 @@ export async function logout(): Promise<void> {
return post('/logout')
}
export async function getUser(idList): Promise<IUser[]> {
return post('/user/get', { idList })
}
/**
*
* @param keys
@ -46,3 +50,31 @@ export async function logout(): Promise<void> {
export function userSearching(keys) {
return post('user/searching', { keys })
}
//修改密码
export function updatePassword(data: { id?; origin; target; session? }) {
return post('/user/updatePassword', data)
}
//修改密码
export function setAvatar(id: string, avatar: Blob) {
const form = new FormData()
form.append('file', avatar, avatar instanceof File ? avatar.name : `${id}.jpg`)
return request({
url: `/user/avatar`,
params: { id },
method: 'put',
data: form
})
}
//修改密码
export function getAvatar(id: string): Promise<Blob> {
return request({
url: `/user/avatar?id=${id}`,
responseType: 'blob',
headers: {
'Cache-Control': 'no-cache'
}
}) as any
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -42,12 +42,9 @@
<router-link to="/">
<el-dropdown-item divided>{{ langTitle("Home") }}</el-dropdown-item>
</router-link>
<a target="_blank" href="https://github.com/jzfai/vue3-admin-plus">
<el-dropdown-item>{{ langTitle("Github") }}</el-dropdown-item>
</a>
<a target="_blank" href="https://github.jzfai.top/low-code-platform">
<el-dropdown-item>{{ langTitle("LowCodePlatFrom") }}</el-dropdown-item>
</a>
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item divided @click="loginOut">{{ langTitle("login out") }}</el-dropdown-item>
</el-dropdown-menu>
</template>

View File

@ -16,6 +16,7 @@ import { setupI18n } from '@/lang'
import '@/styles/index.scss' // global css
import 'bootstrap-icons/font/bootstrap-icons.scss'
import 'vue-cropper/dist/index.css'
//svg-icon
import 'virtual:svg-icons-register'

View File

@ -1,7 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouterTypes } from '~/basic'
import Layout from '@/layout/index.vue'
import settings from '@/settings'
const Layout = () => import('@/layout/index.vue')
export const constantRoutes: RouterTypes = [
{
@ -37,6 +37,19 @@ export const constantRoutes: RouterTypes = [
meta: { title: 'Dashboard', icon: 'house-door', affix: true }
}
]
},
{
path: '/user',
name: '用户',
component: Layout,
hidden: true,
children: [
{
path: 'profile',
name: '首选项',
component: () => import('@/views/user/profile/index.vue')
}
]
}
]

View File

@ -0,0 +1,295 @@
<template>
<el-dialog
:model-value="modelValue"
title="编辑头像"
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
width="600px"
@update:model-value="v => $emit('update:modelValue', v)"
>
<div style="display: flex" class="avatar">
<div class="avatar-left">
<div v-show="!options.img">
<el-upload
ref="upload"
action=""
style="text-align: center;margin-bottom: 24px"
:on-change="uploads"
accept="image/png, image/jpeg, image/jpg"
:show-file-list="false"
:auto-upload="false">
<template #trigger>
<el-button ref="uploadBtn" size="small" type="primary">选择图片</el-button>
</template>
</el-upload>
<div>支持jpgpng格式的图片大小不超过3M</div>
</div>
<div v-show="options.img" class="avatar-left-crop">
<VueCropper
ref="cropper"
class="crop-box"
:img="options.img"
:auto-crop="options.autoCrop"
:fixed-box="options.fixedBox"
:can-move-box="options.canMoveBox"
:auto-crop-width="options.autoCropWidth"
:auto-crop-height="options.autoCropHeight"
:center-box="options.centerBox"
mode="contain"
@real-time="realTime" />
<p class="avatar-left-p">鼠标滚轮缩放控制图片显示大小鼠标拖拽调整显示位置</p>
</div>
</div>
<div class="avatar-right">
<div v-for="(item, i) in previewsDiv" :key="i" class="avatar-right-div" :style="item.style">
<div v-show="options.img" :class="previews.div" class="avatar-right-previews" :style="item.zoomStyle">
<img :src="previews.url" :style="previews.img" alt="img" />
</div>
</div>
<div class="avatar-right-text">
<el-button v-if="options.img" link type="primary" @click="uploadPreviews">重新上传</el-button>
<span v-else>预览</span>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeDialog"> </el-button>
<el-button type="primary" @click="getCrop"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import { VueCropper } from "vue-cropper";
import { getAvatar, setAvatar } from "@/api/user";
import { ElMessage } from "element-plus";
export default {
name: "AvatarEdit",
components: {
VueCropper
},
props: {
modelValue: {
type: Boolean,
default: false
}
},
data() {
return {
//vueCropper
options: {
img: "", //
autoCrop: true, //
fixedBox: true, //
canMoveBox: false, //
autoCropWidth: 200, //
autoCropHeight: 200, //
centerBox: false //
},
//
previews: {},
//
previewsDiv: [
//128px
{
style: {
width: "108px",
height: "108px",
margin: "0 auto"
},
zoomStyle: {
zoom: 0.54
}
},
//68px
{
style: {
width: "68px",
height: "68px",
margin: "27px auto"
},
zoomStyle: {
zoom: 0.34
}
},
//48px
{
style: {
width: "48px",
height: "48px",
margin: "0 auto"
},
zoomStyle: {
zoom: 0.24
}
}
]
};
},
methods: {
//
uploads(file) {
const isIMAGE = file.raw.type === "image/jpeg" || file.raw.type === "image/png";
const isLt3M = file.raw.size / 1024 / 1024 < 3;
if (!isIMAGE) {
this.$message({
showClose: true,
message: "请选择 jpg、png 格式的图片!",
type: "error" //
});
return false;
}
if (!isLt3M) {
this.$message({
showClose: true,
message: "上传图片大小不能超过 3MB",
type: "error" //
});
return false;
}
const reader = new FileReader();
reader.readAsDataURL(file.raw);
reader.onload = e => {
this.options.img = e.target.result; //base64
};
},
//
realTime(data) {
this.previews = data;
},
//
uploadPreviews() {
this.$refs.uploadBtn.$el.click();
},
//
async getCrop() {
/** @type {Blob} */
const blob = await new Promise(r => {
this.$refs.cropper.getCropBlob((data) => {
r(data);
});
});
const userInfo = useBasicStore().userInfo;
await setAvatar(userInfo.id, blob);
// base64
const data = await getAvatar(userInfo.id);
const fr = new FileReader();
fr.onload = () => userInfo.avatar = fr.result;
fr.readAsDataURL(data);
ElMessage.success("操作成功!");
this.closeDialog();
},
//
closeDialog() {
// closeAvatarEdits()
this.$emit("update:modelValue", false);
// data (Object.assign this.$data this.$options.data())
Object.assign(this.$data, this.$options.data());
}
}
};
</script>
<style lang="scss" scoped>
:deep(.el-dialog__header) {
padding: 24px 0 11px 28px;
}
:deep(.el-dialog__title) {
color: #333333;
}
:deep(.el-dialog__body) {
padding: 0 28px;
}
:deep(.el-dialog__footer) {
padding: 20px 28px;
.el-button {
width: 145px;
}
}
.avatar {
display: flex;
.avatar-left {
display: flex;
justify-content: center;
align-items: center;
width: 400px;
height: 400px;
background-color: #F0F2F5;
margin-right: 10px;
border-radius: 4px;
.avatar-left-crop {
width: 400px;
height: 400px;
position: relative;
.crop-box {
width: 100%;
height: 100%;
border-radius: 4px;
overflow: hidden
}
}
.avatar-left-p {
text-align: center;
width: 100%;
position: absolute;
bottom: 20px;
color: #ffffff;
font-size: 14px;
pointer-events: none;
}
}
.avatar-right {
width: 150px;
height: 400px;
background-color: #F0F2F5;
border-radius: 4px;
padding: 16px 0;
box-sizing: border-box;
.avatar-right-div {
border: 3px solid #ffffff;
border-radius: 24%;
}
.avatar-right-previews {
width: 200px;
height: 200px;
overflow: hidden;
border-radius: 24%;
}
.avatar-right-text {
text-align: center;
margin-top: 50px;
font-size: 14px;
:deep(.el-button) {
padding: 0;
}
span {
color: #666666;
}
}
}
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<div class="user-profile">
<el-card header="个人信息">
<el-descriptions label-width="120px" size="default" border :column="1">
<el-descriptions-item label="头像">
<div class="avatar-frame">
<img :src="userInfo.avatar || UserImage" class="avatar" alt="用户头像" @click="showAvatarEditor = true" />
<div class="avatar-tip">
<i class="bi bi-pen-fill" />
<span>修改头像</span>
</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="用户名">
{{ userInfo.realName }}
</el-descriptions-item>
<el-descriptions-item label="账号">
{{ userInfo.userName }}
</el-descriptions-item>
<el-descriptions-item label="手机号">
{{ userInfo.phone }}
</el-descriptions-item>
<el-descriptions-item label="UID">
{{ userInfo.id }}
</el-descriptions-item>
<el-descriptions-item label="注册日期">
{{ userInfo.createdAt }}
</el-descriptions-item>
<el-descriptions-item label="上次更新">
{{ userInfo.updatedAt }}
</el-descriptions-item>
</el-descriptions>
</el-card>
<avatar-edit v-model="showAvatarEditor" />
</div>
</template>
<script setup lang="ts">
import AvatarEdit from "@/views/user/profile/components/AvatarEditor.vue";
import UserImage from "@/assets/layout/user.png";
import MenuIcon from "@/components/MenuIcon.vue";
const showAvatarEditor = ref(false);
const userInfo = toRef(useBasicStore(), "userInfo");
</script>
<style scoped lang="scss">
.user-profile {
max-width: 1024px;
margin: 0 auto;
.avatar-frame {
position: relative;
height: 96px;
width: 96px;
overflow: hidden;
border-radius: 24%;
&:not(:hover) .avatar-tip {
display: none;
}
.avatar {
height: 96px;
width: 96px;
cursor: pointer;
-webkit-user-drag: none;
-moz-user-drag: none;
user-drag: none;
}
.avatar-tip {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #88888888;
color: white;
pointer-events: none;
.bi {
font-size: 2em;
}
}
}
}
</style>