mirror of
https://github.com/pure-admin/vue-pure-admin.git
synced 2025-06-08 01:17:23 +08:00
feat: add user settings page
This commit is contained in:
parent
eeb09a8da2
commit
8d45ba8885
@ -1,4 +1,5 @@
|
||||
buttons:
|
||||
hsUserSettings: 账户设置
|
||||
hsLoginOut: 退出系统
|
||||
hsfullscreen: 全屏
|
||||
hsexitfullscreen: 退出全屏
|
||||
|
52
mock/region.ts
Normal file
52
mock/region.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { defineFakeRoute } from "vite-plugin-fake-server/client";
|
||||
|
||||
export default defineFakeRoute([
|
||||
{
|
||||
url: "/get-regions",
|
||||
method: "get",
|
||||
response: () => {
|
||||
return {
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
code: "001",
|
||||
name: "中国",
|
||||
children: [
|
||||
{
|
||||
code: "001001",
|
||||
name: "北京市",
|
||||
children: [
|
||||
{
|
||||
code: "001001001",
|
||||
name: "东城区"
|
||||
},
|
||||
{
|
||||
code: "001001002",
|
||||
name: "西城区"
|
||||
}
|
||||
// 其他区
|
||||
]
|
||||
},
|
||||
{
|
||||
code: "001002",
|
||||
name: "上海市",
|
||||
children: [
|
||||
{
|
||||
code: "001002001",
|
||||
name: "黄浦区"
|
||||
},
|
||||
{
|
||||
code: "001002002",
|
||||
name: "徐汇区"
|
||||
}
|
||||
// 其他区
|
||||
]
|
||||
}
|
||||
// 其他城市
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
]);
|
21
mock/userInfo.ts
Normal file
21
mock/userInfo.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { defineFakeRoute } from "vite-plugin-fake-server/client";
|
||||
|
||||
export default defineFakeRoute([
|
||||
{
|
||||
url: "/get-user-info",
|
||||
method: "get",
|
||||
response: () => {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
avatarUrl: "https://avatars.githubusercontent.com/u/44761321",
|
||||
nickName: "企丸丸",
|
||||
introduce: "我是幻兽帕鲁里的NO.1",
|
||||
regionCode: "001002001",
|
||||
address: "体育路冰鸟密域祭坛地下城",
|
||||
userName: "admin"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
]);
|
@ -23,3 +23,8 @@ export const formUpload = data => {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**所在区域数据*/
|
||||
export const getRegions = (params?: object) => {
|
||||
return http.request<Result>("get", "/get-regions", { params });
|
||||
};
|
||||
|
@ -28,6 +28,20 @@ export type RefreshTokenResult = {
|
||||
};
|
||||
};
|
||||
|
||||
export type UserInfo = {
|
||||
avatarUrl: string;
|
||||
nickName: string;
|
||||
introduce: string;
|
||||
regionCode: string;
|
||||
address: string;
|
||||
userName: string;
|
||||
};
|
||||
|
||||
export type UserInfoResult = {
|
||||
success: boolean;
|
||||
data: UserInfo;
|
||||
};
|
||||
|
||||
/** 登录 */
|
||||
export const getLogin = (data?: object) => {
|
||||
return http.request<UserResult>("post", "/login", { data });
|
||||
@ -37,3 +51,8 @@ export const getLogin = (data?: object) => {
|
||||
export const refreshTokenApi = (data?: object) => {
|
||||
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
|
||||
};
|
||||
|
||||
/**获取个人信息 */
|
||||
export const getUserInfo = (data?: object) => {
|
||||
return http.request<UserInfoResult>("get", "/get-user-info", { data });
|
||||
};
|
||||
|
@ -11,10 +11,13 @@ import globalization from "@/assets/svg/globalization.svg?component";
|
||||
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
|
||||
import Setting from "@iconify-icons/ri/settings-3-line";
|
||||
import Check from "@iconify-icons/ep/check";
|
||||
import UserSettingsLine from "@iconify-icons/ri/user-settings-line";
|
||||
|
||||
const {
|
||||
layout,
|
||||
device,
|
||||
logout,
|
||||
handleOpenUserSettings,
|
||||
onPanel,
|
||||
pureApp,
|
||||
username,
|
||||
@ -91,6 +94,13 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="logout">
|
||||
<el-dropdown-item @click="handleOpenUserSettings">
|
||||
<IconifyIconOffline
|
||||
:icon="UserSettingsLine"
|
||||
style="margin: 5px"
|
||||
/>
|
||||
{{ t("buttons.hsUserSettings") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">
|
||||
<IconifyIconOffline
|
||||
:icon="LogoutCircleRLine"
|
||||
|
@ -12,6 +12,7 @@ import globalization from "@/assets/svg/globalization.svg?component";
|
||||
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
|
||||
import Setting from "@iconify-icons/ri/settings-3-line";
|
||||
import Check from "@iconify-icons/ep/check";
|
||||
import UserSettingsLine from "@iconify-icons/ri/user-settings-line";
|
||||
|
||||
const menuRef = ref();
|
||||
|
||||
@ -27,7 +28,8 @@ const {
|
||||
userAvatar,
|
||||
avatarsStyle,
|
||||
getDropdownItemStyle,
|
||||
getDropdownItemClass
|
||||
getDropdownItemClass,
|
||||
handleOpenUserSettings
|
||||
} = useNav();
|
||||
|
||||
const defaultActive = computed(() =>
|
||||
@ -107,6 +109,10 @@ nextTick(() => {
|
||||
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-item @click="handleOpenUserSettings">
|
||||
<IconifyIconOffline :icon="UserSettingsLine" style="margin: 5px" />
|
||||
{{ t("buttons.hsUserSettings") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-menu class="logout">
|
||||
<el-dropdown-item @click="logout">
|
||||
<IconifyIconOffline
|
||||
|
@ -15,6 +15,7 @@ import globalization from "@/assets/svg/globalization.svg?component";
|
||||
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
|
||||
import Setting from "@iconify-icons/ri/settings-3-line";
|
||||
import Check from "@iconify-icons/ep/check";
|
||||
import UserSettingsLine from "@iconify-icons/ri/user-settings-line";
|
||||
|
||||
const menuRef = ref();
|
||||
const defaultActive = ref(null);
|
||||
@ -31,7 +32,8 @@ const {
|
||||
getDivStyle,
|
||||
avatarsStyle,
|
||||
getDropdownItemStyle,
|
||||
getDropdownItemClass
|
||||
getDropdownItemClass,
|
||||
handleOpenUserSettings
|
||||
} = useNav();
|
||||
|
||||
function getDefaultActive(routePath) {
|
||||
@ -140,6 +142,10 @@ watch(
|
||||
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-item @click="handleOpenUserSettings">
|
||||
<IconifyIconOffline :icon="UserSettingsLine" style="margin: 5px" />
|
||||
{{ t("buttons.hsUserSettings") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-menu class="logout">
|
||||
<el-dropdown-item @click="logout">
|
||||
<IconifyIconOffline
|
||||
|
64
src/layout/components/userSettings/index.vue
Normal file
64
src/layout/components/userSettings/index.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useNav } from "@/layout/hooks/useNav";
|
||||
import leftLine from "@iconify-icons/ri/arrow-left-s-line";
|
||||
import userLine from "@iconify-icons/ri/user-3-line";
|
||||
import profileLine from "@iconify-icons/ri/profile-line";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getUserInfo } from "@/api/user";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { userAvatar, getLogo, username } = useNav();
|
||||
const defaultActive = ref<string>(route.path as string);
|
||||
const userInfo = ref({
|
||||
nickName: "",
|
||||
avatarUrl: "",
|
||||
userName: ""
|
||||
});
|
||||
getUserInfo().then(res => {
|
||||
userInfo.value = res.data;
|
||||
});
|
||||
const handleBack = () => {
|
||||
router.go(-1);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="h-full py-8">
|
||||
<el-aside class="w-70 pure-border-color px-4">
|
||||
<el-menu :default-active="defaultActive" router>
|
||||
<el-menu-item class="flex items-center" @click="handleBack">
|
||||
<IconifyIconOffline :icon="leftLine" />
|
||||
<img :src="getLogo()" class="w-6 h-6" alt="logo" />
|
||||
<span class="ml-2">返回</span>
|
||||
</el-menu-item>
|
||||
<li class="flex items-center ml-8 mt-4">
|
||||
<div>
|
||||
<el-avatar :size="80" :src="userInfo.avatarUrl" />
|
||||
</div>
|
||||
<div class="pl-4">
|
||||
<p>{{ userInfo.nickName }}</p>
|
||||
<el-text class="mt-2" type="info">{{ userInfo.userName }}</el-text>
|
||||
</div>
|
||||
</li>
|
||||
<el-menu-item index="/user-settings/user-info-manage">
|
||||
<el-icon><IconifyIconOffline :icon="userLine" /></el-icon>
|
||||
<span>基本设置</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/user-settings/safe-manage">
|
||||
<el-icon><IconifyIconOffline :icon="profileLine" /></el-icon>
|
||||
<span>安全设置</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pure-border-color {
|
||||
border-color: var(--pure-border-color); /* 使用边框颜色变量 */
|
||||
}
|
||||
</style>
|
7
src/layout/components/userSettings/safeManage.vue
Normal file
7
src/layout/components/userSettings/safeManage.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>安全设置</template>
|
||||
</el-card>
|
||||
</template>
|
184
src/layout/components/userSettings/userInfoManage.vue
Normal file
184
src/layout/components/userSettings/userInfoManage.vue
Normal file
@ -0,0 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
// [TO DO] 只有销毁dialog,再修改cropper src,编辑的图片才变。
|
||||
import { reactive, ref } from "vue";
|
||||
import { useNav } from "@/layout/hooks/useNav";
|
||||
import uploadLine from "@iconify-icons/ri/upload-line";
|
||||
import CroppingUpload from "@/views/system/user/upload.vue";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import { formUpload } from "@/api/mock";
|
||||
import { message } from "@/utils/message";
|
||||
import { createFormData } from "@pureadmin/utils";
|
||||
import { getRegions } from "@/api/mock";
|
||||
import { getUserInfo, UserInfo } from "@/api/user";
|
||||
const { userAvatar, getLogo, username } = useNav();
|
||||
const cropRef = ref();
|
||||
const upload = ref();
|
||||
const isShow = ref(false);
|
||||
const userInfoFormRef = ref<FormInstance>();
|
||||
// 从服务器拉取用户信息,然后填充表单
|
||||
|
||||
const userInfoFormData = reactive({
|
||||
avatarUrl: "",
|
||||
nickName: "",
|
||||
introduce: "",
|
||||
regionCode: "",
|
||||
address: ""
|
||||
});
|
||||
|
||||
getUserInfo().then(res => {
|
||||
Object.assign(userInfoFormData, res.data);
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules<UserInfo>>({
|
||||
nickName: [
|
||||
{ required: true, message: "昵称必填", trigger: "blur" },
|
||||
{ min: 3, max: 5, message: "长度最小3最大16", trigger: "blur" }
|
||||
]
|
||||
});
|
||||
const imgSrc = ref("");
|
||||
const onChange = uploadFile => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
imgSrc.value = e.target.result as string;
|
||||
isShow.value = true;
|
||||
};
|
||||
|
||||
// reader.onloadend = () => {
|
||||
// cropRef.value.init();
|
||||
// };
|
||||
reader.readAsDataURL(uploadFile.raw);
|
||||
/* imgSrc.value = uploadFile.row;
|
||||
isShow.value = true; */
|
||||
};
|
||||
|
||||
let cropperInfo: any;
|
||||
const onCropper = info => {
|
||||
cropperInfo = info;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
cropRef.value.hidePopover();
|
||||
upload.value.clearFiles();
|
||||
isShow.value = false;
|
||||
};
|
||||
|
||||
// 在编辑弹框中上传图片,获取在服务器上的url,
|
||||
const handleSubmitImage = () => {
|
||||
const formData = createFormData({
|
||||
files: new File([cropperInfo], "avatar") // file 文件
|
||||
});
|
||||
formUpload(formData)
|
||||
.then(({ success, data }) => {
|
||||
if (success) {
|
||||
message("提交成功", { type: "success" });
|
||||
handleClose();
|
||||
} else {
|
||||
message("提交失败");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
message(`提交异常 ${error}`, { type: "error" });
|
||||
});
|
||||
};
|
||||
|
||||
const options = ref(); // 级联选择器的选项数据
|
||||
|
||||
// 设置区域数据
|
||||
getRegions().then(res => {
|
||||
options.value = res.data;
|
||||
});
|
||||
|
||||
// 监听级联选择器的值变化
|
||||
const handleCascaderChange = value => {
|
||||
console.log(value);
|
||||
};
|
||||
|
||||
// 更新基本信息
|
||||
const onSubmit = async (formEl: FormInstance) => {
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
console.log(userInfoFormData);
|
||||
console.log("验证成功!");
|
||||
} else {
|
||||
console.log("error submit!", fields);
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>基本信息</template>
|
||||
<el-form
|
||||
ref="userInfoFormRef"
|
||||
label-position="top"
|
||||
:rules="rules"
|
||||
:model="userInfoFormData"
|
||||
>
|
||||
<el-form-item label="头像">
|
||||
<el-avatar :size="80" :src="userAvatar" />
|
||||
<el-upload
|
||||
ref="upload"
|
||||
action="#"
|
||||
:limit="1"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="onChange"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-button plain class="ml-4">
|
||||
<IconifyIconOffline :icon="uploadLine" />
|
||||
<span class="ml-2">更新头像</span>
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称" prop="nickName">
|
||||
<el-input v-model="userInfoFormData.nickName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="简介">
|
||||
<el-input
|
||||
v-model="userInfoFormData.introduce"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="所在地区">
|
||||
<el-cascader
|
||||
v-model="userInfoFormData.regionCode"
|
||||
:options="options"
|
||||
:props="{ value: 'code', label: 'name', emitPath: false }"
|
||||
placeholder="请选择"
|
||||
@change="handleCascaderChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="街道地址">
|
||||
<el-input
|
||||
v-model="userInfoFormData.address"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="onSubmit(userInfoFormRef)"
|
||||
>更新基本信息</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-dialog
|
||||
v-model="isShow"
|
||||
title="编辑头像"
|
||||
:before-close="handleClose"
|
||||
destroy-on-close
|
||||
>
|
||||
<CroppingUpload ref="cropRef" :imgSrc="imgSrc" @cropper="onCropper" />
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitImage">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
@ -99,6 +99,10 @@ export function useNav() {
|
||||
emitter.emit("openPanel");
|
||||
}
|
||||
|
||||
function handleOpenUserSettings() {
|
||||
router.push({ name: "UserInfoManage" });
|
||||
}
|
||||
|
||||
function toggleSideBar() {
|
||||
pureApp.toggleSideBar();
|
||||
}
|
||||
@ -160,6 +164,7 @@ export function useNav() {
|
||||
avatarsStyle,
|
||||
tooltipEffect,
|
||||
getDropdownItemStyle,
|
||||
getDropdownItemClass
|
||||
getDropdownItemClass,
|
||||
handleOpenUserSettings
|
||||
};
|
||||
}
|
||||
|
@ -38,5 +38,29 @@ export default [
|
||||
showLink: false,
|
||||
rank: 103
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/user-settings",
|
||||
name: "UserSettings",
|
||||
component: () => import("@/layout/components/userSettings/index.vue"),
|
||||
meta: {
|
||||
title: $t("buttons.hsUserSettings"),
|
||||
showLink: false,
|
||||
rank: 103
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "user-info-manage",
|
||||
name: "UserInfoManage",
|
||||
component: () =>
|
||||
import("@/layout/components/userSettings/userInfoManage.vue")
|
||||
},
|
||||
{
|
||||
path: "safe-manage",
|
||||
name: "SafeManage",
|
||||
component: () =>
|
||||
import("@/layout/components/userSettings/safeManage.vue")
|
||||
}
|
||||
]
|
||||
}
|
||||
] satisfies Array<RouteConfigsTable>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user