新增用户页面,以及用户头像支持,用户头像修改,修改默认用户头像
This commit is contained in:
parent
cd59783e90
commit
476f8394b1
|
@ -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",
|
||||
|
|
|
@ -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 |
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
295
src/views/user/profile/components/AvatarEditor.vue
Normal file
295
src/views/user/profile/components/AvatarEditor.vue
Normal 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>支持jpg、png格式的图片,大小不超过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>
|
||||
|
93
src/views/user/profile/index.vue
Normal file
93
src/views/user/profile/index.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user