feat: add user settings page

This commit is contained in:
edgex 2024-03-18 12:06:10 +08:00 committed by edgexie
parent eeb09a8da2
commit 8d45ba8885
13 changed files with 407 additions and 3 deletions

View File

@ -1,4 +1,5 @@
buttons:
hsUserSettings: 账户设置
hsLoginOut: 退出系统
hsfullscreen: 全屏
hsexitfullscreen: 退出全屏

52
mock/region.ts Normal file
View 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
View 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"
}
};
}
}
]);

View File

@ -23,3 +23,8 @@ export const formUpload = data => {
}
);
};
/**所在区域数据*/
export const getRegions = (params?: object) => {
return http.request<Result>("get", "/get-regions", { params });
};

View File

@ -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 });
};

View File

@ -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"

View File

@ -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

View File

@ -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

View 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>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<el-card shadow="never">
<template #header>安全设置</template>
</el-card>
</template>

View File

@ -0,0 +1,184 @@
<script setup lang="ts">
// [TO DO] dialogcropper 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>

View File

@ -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
};
}

View File

@ -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>;