feat: 添加账户设置功能页面且兼容移动端 (#1022)

This commit is contained in:
EchoByte 2024-04-12 16:20:33 +08:00 committed by GitHub
parent 296db3789b
commit 332678ba88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 860 additions and 22 deletions

View File

@ -1,4 +1,5 @@
buttons: buttons:
pureAccountSettings: Account Settings
pureLoginOut: LoginOut pureLoginOut: LoginOut
pureLogin: Login pureLogin: Login
pureSystemSet: Open ProjectConfig pureSystemSet: Open ProjectConfig
@ -175,4 +176,4 @@ login:
purePassWordRuleReg: The password format should be any combination of 8-18 digits purePassWordRuleReg: The password format should be any combination of 8-18 digits
purePassWordSureReg: Please enter confirm password purePassWordSureReg: Please enter confirm password
purePassWordDifferentReg: The two passwords do not match! purePassWordDifferentReg: The two passwords do not match!
purePassWordUpdateReg: Password has been updated purePassWordUpdateReg: Password has been updated

View File

@ -1,4 +1,5 @@
buttons: buttons:
pureAccountSettings: 账户设置
pureLoginOut: 退出系统 pureLoginOut: 退出系统
pureLogin: 登录 pureLogin: 登录
pureSystemSet: 打开项目配置 pureSystemSet: 打开项目配置
@ -175,4 +176,4 @@ login:
purePassWordRuleReg: 密码格式应为8-18位数字、字母、符号的任意两种组合 purePassWordRuleReg: 密码格式应为8-18位数字、字母、符号的任意两种组合
purePassWordSureReg: 请输入确认密码 purePassWordSureReg: 请输入确认密码
purePassWordDifferentReg: 两次密码不一致! purePassWordDifferentReg: 两次密码不一致!
purePassWordUpdateReg: 修改密码成功 purePassWordUpdateReg: 修改密码成功

View File

@ -10,7 +10,9 @@ export default defineFakeRoute([
return { return {
success: true, success: true,
data: { data: {
avatar: "https://avatars.githubusercontent.com/u/44761321",
username: "admin", username: "admin",
nickname: "小铭",
// 一个用户可能有多个角色 // 一个用户可能有多个角色
roles: ["admin"], roles: ["admin"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
@ -22,8 +24,9 @@ export default defineFakeRoute([
return { return {
success: true, success: true,
data: { data: {
avatar: "https://avatars.githubusercontent.com/u/52823142",
username: "common", username: "common",
// 一个用户可能有多个角色 nickname: "小林",
roles: ["common"], roles: ["common"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.common", accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh", refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",

59
mock/mine.ts Normal file
View File

@ -0,0 +1,59 @@
import { defineFakeRoute } from "vite-plugin-fake-server/client";
import { faker } from "@faker-js/faker/locale/zh_CN";
export default defineFakeRoute([
// 账户设置-个人信息
{
url: "/mine",
method: "get",
response: () => {
return {
success: true,
data: {
avatar: "https://avatars.githubusercontent.com/u/44761321",
username: "admin",
nickname: "小铭",
email: "pureadmin@163.com",
phone: "15888886789",
description: "一个热爱开源的前端工程师"
}
};
}
},
// 账户设置-个人安全日志
{
url: "/mine-logs",
method: "get",
response: () => {
let list = [
{
id: 1,
ip: faker.internet.ipv4(),
address: "中国河南省信阳市",
system: "macOS",
browser: "Chrome",
summary: "账户登录", // 详情
operatingTime: new Date() // 时间
},
{
id: 2,
ip: faker.internet.ipv4(),
address: "中国广东省深圳市",
system: "Windows",
browser: "Firefox",
summary: "绑定了手机号码",
operatingTime: new Date().setDate(new Date().getDate() - 1)
}
];
return {
success: true,
data: {
list,
total: list.length, // 总条目数
pageSize: 10, // 每页显示条目个数
currentPage: 1 // 当前页数
}
};
}
}
]);

View File

@ -9,9 +9,9 @@ export default defineFakeRoute([
response: ({ body }) => { response: ({ body }) => {
let list = [ let list = [
{ {
username: "admin",
nickname: "admin",
avatar: "https://avatars.githubusercontent.com/u/44761321", avatar: "https://avatars.githubusercontent.com/u/44761321",
username: "admin",
nickname: "小铭",
phone: "15888886789", phone: "15888886789",
email: faker.internet.email(), email: faker.internet.email(),
sex: 0, sex: 0,
@ -27,9 +27,9 @@ export default defineFakeRoute([
createTime: 1605456000000 createTime: 1605456000000
}, },
{ {
username: "common",
nickname: "common",
avatar: "https://avatars.githubusercontent.com/u/52823142", avatar: "https://avatars.githubusercontent.com/u/52823142",
username: "common",
nickname: "小林",
phone: "18288882345", phone: "18288882345",
email: faker.internet.email(), email: faker.internet.email(),
sex: 1, sex: 1,
@ -397,6 +397,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -420,6 +421,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -443,6 +445,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -466,6 +469,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -489,6 +493,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -512,6 +517,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: true, keepAlive: true,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -535,6 +541,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: true, keepAlive: true,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -558,6 +565,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: true, keepAlive: true,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -581,6 +589,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: true, keepAlive: true,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -604,6 +613,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: true, keepAlive: true,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -627,6 +637,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: true, keepAlive: true,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -651,6 +662,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -674,6 +686,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -697,6 +710,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -720,6 +734,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -743,6 +758,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -766,6 +782,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -790,6 +807,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -813,6 +831,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -836,6 +855,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -859,6 +879,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -882,6 +903,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -906,6 +928,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -929,6 +952,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -952,6 +976,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -975,6 +1000,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -998,6 +1024,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -1022,6 +1049,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -1045,6 +1073,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -1068,6 +1097,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: false, showLink: false,
showParent: false showParent: false
}, },
@ -1091,6 +1121,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: false, showLink: false,
showParent: false showParent: false
} }
@ -1472,6 +1503,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -1495,6 +1527,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -1518,6 +1551,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -1541,6 +1575,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
}, },
@ -1564,6 +1599,7 @@ export default defineFakeRoute([
frameLoading: true, frameLoading: true,
keepAlive: false, keepAlive: false,
hiddenTag: false, hiddenTag: false,
fixedTag: false,
showLink: true, showLink: true,
showParent: false showParent: false
} }

View File

@ -28,12 +28,56 @@ export type RefreshTokenResult = {
}; };
}; };
export type UserInfo = {
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 邮箱 */
email: string;
/** 联系电话 */
phone: string;
/** 简介 */
description: string;
};
export type UserInfoResult = {
success: boolean;
data: UserInfo;
};
type ResultTable = {
success: boolean;
data?: {
/** 列表数据 */
list: Array<any>;
/** 总条目数 */
total?: number;
/** 每页显示条目个数 */
pageSize?: number;
/** 当前页数 */
currentPage?: number;
};
};
/** 登录 */ /** 登录 */
export const getLogin = (data?: object) => { export const getLogin = (data?: object) => {
return http.request<UserResult>("post", "/login", { data }); return http.request<UserResult>("post", "/login", { data });
}; };
/** 刷新token */ /** 刷新`token` */
export const refreshTokenApi = (data?: object) => { export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data }); return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
}; };
/** 账户设置-个人信息 */
export const getMine = (data?: object) => {
return http.request<UserInfoResult>("get", "/mine", { data });
};
/** 账户设置-个人安全日志 */
export const getMineLogs = (data?: object) => {
return http.request<ResultTable>("get", "/mine-logs", { data });
};

View File

@ -0,0 +1,7 @@
import reCropperPreview from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 图片裁剪预览组件 */
export const ReCropperPreview = withInstall(reCropperPreview);
export default ReCropperPreview;

View File

@ -3,6 +3,10 @@ import { ref } from "vue";
import ReCropper from "@/components/ReCropper"; import ReCropper from "@/components/ReCropper";
import { formatBytes } from "@pureadmin/utils"; import { formatBytes } from "@pureadmin/utils";
defineOptions({
name: "ReCropperPreview"
});
const props = defineProps({ const props = defineProps({
imgSrc: String imgSrc: String
}); });

View File

@ -11,6 +11,10 @@ import { isFunction } from "@pureadmin/utils";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill"; import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill"; import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
defineOptions({
name: "ReDialog"
});
const fullscreen = ref(false); const fullscreen = ref(false);
const footerButtons = computed(() => { const footerButtons = computed(() => {

View File

@ -2,6 +2,10 @@
import flippers from "./filpper"; import flippers from "./filpper";
import { ref, unref, nextTick, onUnmounted } from "vue"; import { ref, unref, nextTick, onUnmounted } from "vue";
defineOptions({
name: "ReFlop"
});
const timer = ref(null); const timer = ref(null);
const flipObjs = ref([]); const flipObjs = ref([]);

View File

@ -2,6 +2,10 @@
import { h, onMounted, ref, useSlots } from "vue"; import { h, onMounted, ref, useSlots } from "vue";
import { type TippyOptions, useTippy } from "vue-tippy"; import { type TippyOptions, useTippy } from "vue-tippy";
defineOptions({
name: "ReText"
});
const props = defineProps({ const props = defineProps({
// //
lineClamp: { lineClamp: {

View File

@ -8,9 +8,11 @@ import Breadcrumb from "./sidebar/breadCrumb.vue";
import topCollapse from "./sidebar/topCollapse.vue"; import topCollapse from "./sidebar/topCollapse.vue";
import { useTranslationLang } from "../hooks/useTranslationLang"; import { useTranslationLang } from "../hooks/useTranslationLang";
import globalization from "@/assets/svg/globalization.svg?component"; import globalization from "@/assets/svg/globalization.svg?component";
import AccountSettingsIcon from "@iconify-icons/ri/user-settings-line";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line"; import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line"; import Setting from "@iconify-icons/ri/settings-3-line";
import Check from "@iconify-icons/ep/check"; import Check from "@iconify-icons/ep/check";
const { const {
layout, layout,
device, device,
@ -21,6 +23,7 @@ const {
userAvatar, userAvatar,
avatarsStyle, avatarsStyle,
toggleSideBar, toggleSideBar,
toAccountSettings,
getDropdownItemStyle, getDropdownItemStyle,
getDropdownItemClass getDropdownItemClass
} = useNav(); } = useNav();
@ -91,6 +94,13 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
<el-dropdown-item @click="toAccountSettings">
<IconifyIconOffline
:icon="AccountSettingsIcon"
style="margin: 5px"
/>
{{ t("buttons.pureAccountSettings") }}
</el-dropdown-item>
<el-dropdown-item @click="logout"> <el-dropdown-item @click="logout">
<IconifyIconOffline <IconifyIconOffline
:icon="LogoutCircleRLine" :icon="LogoutCircleRLine"
@ -177,7 +187,7 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
} }
.logout { .logout {
max-width: 120px; width: 120px;
::v-deep(.el-dropdown-menu__item) { ::v-deep(.el-dropdown-menu__item) {
display: inline-flex; display: inline-flex;

View File

@ -9,6 +9,7 @@ import { useNav } from "@/layout/hooks/useNav";
import { useTranslationLang } from "../../hooks/useTranslationLang"; import { useTranslationLang } from "../../hooks/useTranslationLang";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import globalization from "@/assets/svg/globalization.svg?component"; import globalization from "@/assets/svg/globalization.svg?component";
import AccountSettingsIcon from "@iconify-icons/ri/user-settings-line";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line"; import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line"; import Setting from "@iconify-icons/ri/settings-3-line";
import Check from "@iconify-icons/ep/check"; import Check from "@iconify-icons/ep/check";
@ -26,6 +27,7 @@ const {
username, username,
userAvatar, userAvatar,
avatarsStyle, avatarsStyle,
toAccountSettings,
getDropdownItemStyle, getDropdownItemStyle,
getDropdownItemClass getDropdownItemClass
} = useNav(); } = useNav();
@ -107,6 +109,13 @@ nextTick(() => {
<p v-if="username" class="dark:text-white">{{ username }}</p> <p v-if="username" class="dark:text-white">{{ username }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-item @click="toAccountSettings">
<IconifyIconOffline
:icon="AccountSettingsIcon"
style="margin: 5px"
/>
{{ t("buttons.pureAccountSettings") }}
</el-dropdown-item>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
<el-dropdown-item @click="logout"> <el-dropdown-item @click="logout">
<IconifyIconOffline <IconifyIconOffline
@ -151,7 +160,7 @@ nextTick(() => {
} }
.logout { .logout {
max-width: 120px; width: 120px;
::v-deep(.el-dropdown-menu__item) { ::v-deep(.el-dropdown-menu__item) {
display: inline-flex; display: inline-flex;

View File

@ -12,6 +12,7 @@ import { getParentPaths, findRouteByPath } from "@/router/utils";
import { useTranslationLang } from "../../hooks/useTranslationLang"; import { useTranslationLang } from "../../hooks/useTranslationLang";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import globalization from "@/assets/svg/globalization.svg?component"; import globalization from "@/assets/svg/globalization.svg?component";
import AccountSettingsIcon from "@iconify-icons/ri/user-settings-line";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line"; import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line"; import Setting from "@iconify-icons/ri/settings-3-line";
import Check from "@iconify-icons/ep/check"; import Check from "@iconify-icons/ep/check";
@ -30,6 +31,7 @@ const {
userAvatar, userAvatar,
getDivStyle, getDivStyle,
avatarsStyle, avatarsStyle,
toAccountSettings,
getDropdownItemStyle, getDropdownItemStyle,
getDropdownItemClass getDropdownItemClass
} = useNav(); } = useNav();
@ -140,6 +142,13 @@ watch(
<p v-if="username" class="dark:text-white">{{ username }}</p> <p v-if="username" class="dark:text-white">{{ username }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-item @click="toAccountSettings">
<IconifyIconOffline
:icon="AccountSettingsIcon"
style="margin: 5px"
/>
{{ t("buttons.pureAccountSettings") }}
</el-dropdown-item>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
<el-dropdown-item @click="logout"> <el-dropdown-item @click="logout">
<IconifyIconOffline <IconifyIconOffline
@ -184,7 +193,7 @@ watch(
} }
.logout { .logout {
max-width: 120px; width: 120px;
::v-deep(.el-dropdown-menu__item) { ::v-deep(.el-dropdown-menu__item) {
display: inline-flex; display: inline-flex;

View File

@ -2,16 +2,16 @@ import { storeToRefs } from "pinia";
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { emitter } from "@/utils/mitt"; import { emitter } from "@/utils/mitt";
import userAvatar from "@/assets/user.jpg"; import Avatar from "@/assets/user.jpg";
import { getTopMenu } from "@/router/utils"; import { getTopMenu } from "@/router/utils";
import { useFullscreen } from "@vueuse/core"; import { useFullscreen } from "@vueuse/core";
import { useGlobal } from "@pureadmin/utils";
import type { routeMetaType } from "../types"; import type { routeMetaType } from "../types";
import { transformI18n } from "@/plugins/i18n"; import { transformI18n } from "@/plugins/i18n";
import { router, remainingPaths } from "@/router"; import { router, remainingPaths } from "@/router";
import { computed, type CSSProperties } from "vue"; import { computed, type CSSProperties } from "vue";
import { useAppStoreHook } from "@/store/modules/app"; import { useAppStoreHook } from "@/store/modules/app";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
import { useGlobal, isAllEmpty } from "@pureadmin/utils";
import { useEpThemeStoreHook } from "@/store/modules/epTheme"; import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill"; import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
@ -37,9 +37,18 @@ export function useNav() {
}; };
}); });
/** 用户名 */ /** 头像(如果头像为空则使用 src/assets/user.jpg */
const userAvatar = computed(() => {
return isAllEmpty(useUserStoreHook()?.avatar)
? Avatar
: useUserStoreHook()?.avatar;
});
/** 昵称(如果昵称为空则显示用户名) */
const username = computed(() => { const username = computed(() => {
return useUserStoreHook()?.username; return isAllEmpty(useUserStoreHook()?.nickname)
? useUserStoreHook()?.username
: useUserStoreHook()?.nickname;
}); });
/** 设置国际化选中后的样式 */ /** 设置国际化选中后的样式 */
@ -99,6 +108,10 @@ export function useNav() {
emitter.emit("openPanel"); emitter.emit("openPanel");
} }
function toAccountSettings() {
router.push({ name: "AccountSettings" });
}
function toggleSideBar() { function toggleSideBar() {
pureApp.toggleSideBar(); pureApp.toggleSideBar();
} }
@ -159,6 +172,7 @@ export function useNav() {
userAvatar, userAvatar,
avatarsStyle, avatarsStyle,
tooltipEffect, tooltipEffect,
toAccountSettings,
getDropdownItemStyle, getDropdownItemStyle,
getDropdownItemClass getDropdownItemClass
}; };

View File

@ -38,5 +38,15 @@ export default [
showLink: false, showLink: false,
rank: 103 rank: 103
} }
},
{
path: "/account-settings",
name: "AccountSettings",
component: () => import("@/views/account-settings/index.vue"),
meta: {
title: $t("buttons.pureAccountSettings"),
showLink: false,
rank: 104
}
} }
] satisfies Array<RouteConfigsTable>; ] satisfies Array<RouteConfigsTable>;

View File

@ -38,7 +38,9 @@ export type setType = {
}; };
export type userType = { export type userType = {
avatar?: string;
username?: string; username?: string;
nickname?: string;
roles?: Array<string>; roles?: Array<string>;
verifyCode?: string; verifyCode?: string;
currentPage?: number; currentPage?: number;

View File

@ -12,8 +12,12 @@ import { type DataInfo, setToken, removeToken, userKey } from "@/utils/auth";
export const useUserStore = defineStore({ export const useUserStore = defineStore({
id: "pure-user", id: "pure-user",
state: (): userType => ({ state: (): userType => ({
// 头像
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "",
// 用户名 // 用户名
username: storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "", username: storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "",
// 昵称
nickname: storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "",
// 页面级别权限 // 页面级别权限
roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [], roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [],
// 前端生成的验证码(按实际需求替换) // 前端生成的验证码(按实际需求替换)
@ -26,10 +30,18 @@ export const useUserStore = defineStore({
loginDay: 7 loginDay: 7
}), }),
actions: { actions: {
/** 存储头像 */
SET_AVATAR(avatar: string) {
this.avatar = avatar;
},
/** 存储用户名 */ /** 存储用户名 */
SET_USERNAME(username: string) { SET_USERNAME(username: string) {
this.username = username; this.username = username;
}, },
/** 存储昵称 */
SET_NICKNAME(nickname: string) {
this.nickname = nickname;
},
/** 存储角色 */ /** 存储角色 */
SET_ROLES(roles: Array<string>) { SET_ROLES(roles: Array<string>) {
this.roles = roles; this.roles = roles;

View File

@ -9,8 +9,12 @@ export interface DataInfo<T> {
expires: T; expires: T;
/** 用于调用刷新accessToken的接口时所需的token */ /** 用于调用刷新accessToken的接口时所需的token */
refreshToken: string; refreshToken: string;
/** 头像 */
avatar?: string;
/** 用户名 */ /** 用户名 */
username?: string; username?: string;
/** 昵称 */
nickname?: string;
/** 当前登陆用户的角色 */ /** 当前登陆用户的角色 */
roles?: Array<string>; roles?: Array<string>;
} }
@ -37,7 +41,7 @@ export function getToken(): DataInfo<number> {
* @description `token``token` * @description `token``token`
* `accessToken`访使`token``refreshToken``accessToken``token``refreshToken`30`accessToken`2`expires``accessToken` * `accessToken`访使`token``refreshToken``accessToken``token``refreshToken`30`accessToken`2`expires``accessToken`
* `accessToken``expires`key值为authorized-token的cookie里 * `accessToken``expires`key值为authorized-token的cookie里
* `username``roles``refreshToken``expires`key值为`user-info`localStorage里`multipleTabsKey` * `avatar``username``nickname``roles``refreshToken``expires`key值为`user-info`localStorage里`multipleTabsKey`
*/ */
export function setToken(data: DataInfo<Date>) { export function setToken(data: DataInfo<Date>) {
let expires = 0; let expires = 0;
@ -62,26 +66,44 @@ export function setToken(data: DataInfo<Date>) {
: {} : {}
); );
function setUserKey(username: string, roles: Array<string>) { function setUserKey({ avatar, username, nickname, roles }) {
useUserStoreHook().SET_AVATAR(avatar);
useUserStoreHook().SET_USERNAME(username); useUserStoreHook().SET_USERNAME(username);
useUserStoreHook().SET_NICKNAME(nickname);
useUserStoreHook().SET_ROLES(roles); useUserStoreHook().SET_ROLES(roles);
storageLocal().setItem(userKey, { storageLocal().setItem(userKey, {
refreshToken, refreshToken,
expires, expires,
avatar,
username, username,
nickname,
roles roles
}); });
} }
if (data.username && data.roles) { if (data.username && data.roles) {
const { username, roles } = data; const { username, roles } = data;
setUserKey(username, roles); setUserKey({
avatar: data?.avatar ?? "",
username,
nickname: data?.nickname ?? "",
roles
});
} else { } else {
const avatar =
storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "";
const username = const username =
storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? ""; storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "";
const nickname =
storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "";
const roles = const roles =
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? []; storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
setUserKey(username, roles); setUserKey({
avatar,
username,
nickname,
roles
});
} }
} }

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { ref } from "vue";
import { message } from "@/utils/message";
import { deviceDetection } from "@pureadmin/utils";
const list = ref([
{
title: "账户密码",
illustrate: "当前密码强度:强",
button: "修改"
},
{
title: "密保手机",
illustrate: "已经绑定手机158****6789",
button: "修改"
},
{
title: "密保问题",
illustrate: "未设置密保问题,密保问题可有效保护账户安全",
button: "修改"
},
{
title: "备用邮箱",
illustrate: "已绑定邮箱pure***@163.com",
button: "修改"
}
]);
function onClick(item) {
console.log("onClick", item.title);
message("请根据具体业务自行实现", { type: "success" });
}
</script>
<template>
<div
:class="[
'min-w-[180px]',
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
]"
>
<h3 class="my-8">账户管理</h3>
<div v-for="(item, index) in list" :key="index">
<div class="flex items-center">
<div class="flex-1">
<p>{{ item.title }}</p>
<el-text class="mx-1" type="info">{{ item.illustrate }}</el-text>
</div>
<el-button type="primary" text @click="onClick(item)">
{{ item.button }}
</el-button>
</div>
<el-divider />
</div>
</div>
</template>
<style lang="scss" scoped>
.el-divider--horizontal {
border-top: 0.1px var(--el-border-color) var(--el-border-style);
}
</style>

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
import { ref } from "vue";
import { message } from "@/utils/message";
import { deviceDetection } from "@pureadmin/utils";
const list = ref([
{
title: "账户密码",
illustrate: "其他用户的消息将以站内信的形式通知",
checked: true
},
{
title: "系统消息",
illustrate: "系统消息将以站内信的形式通知",
checked: true
},
{
title: "待办任务",
illustrate: "待办任务将以站内信的形式通知",
checked: true
}
]);
function onChange(val, item) {
console.log("onChange", val);
message(`${item.title}设置成功`, { type: "success" });
}
</script>
<template>
<div
:class="[
'min-w-[180px]',
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
]"
>
<h3 class="my-8">偏好设置</h3>
<div v-for="(item, index) in list" :key="index">
<div class="flex items-center">
<div class="flex-1">
<p>{{ item.title }}</p>
<p class="wp-4">
<el-text class="mx-1" type="info">
{{ item.illustrate }}
</el-text>
</p>
</div>
<el-switch
v-model="item.checked"
inline-prompt
active-text="是"
inactive-text="否"
@change="val => onChange(val, item)"
/>
</div>
<el-divider />
</div>
</div>
</template>
<style lang="scss" scoped>
.el-divider--horizontal {
border-top: 0.1px var(--el-border-color) var(--el-border-style);
}
</style>

View File

@ -0,0 +1,187 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import { formUpload } from "@/api/mock";
import { message } from "@/utils/message";
import { type UserInfo, getMine } from "@/api/user";
import type { FormInstance, FormRules } from "element-plus";
import ReCropperPreview from "@/components/ReCropperPreview";
import { createFormData, deviceDetection } from "@pureadmin/utils";
import uploadLine from "@iconify-icons/ri/upload-line";
const imgSrc = ref("");
const cropperInfo = ref();
const cropRef = ref();
const uploadRef = ref();
const isShow = ref(false);
const userInfoFormRef = ref<FormInstance>();
const userInfos = reactive({
avatar: "",
nickname: "",
email: "",
phone: "",
description: ""
});
const rules = reactive<FormRules<UserInfo>>({
nickname: [{ required: true, message: "昵称必填", trigger: "blur" }]
});
function queryEmail(queryString, callback) {
const emailList = [
{ value: "@qq.com" },
{ value: "@126.com" },
{ value: "@163.com" }
];
let results = [];
let queryList = [];
emailList.map(item =>
queryList.push({ value: queryString.split("@")[0] + item.value })
);
results = queryString
? queryList.filter(
item =>
item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
)
: queryList;
callback(results);
}
const onChange = uploadFile => {
const reader = new FileReader();
reader.onload = e => {
imgSrc.value = e.target.result as string;
isShow.value = true;
};
reader.readAsDataURL(uploadFile.raw);
};
const handleClose = () => {
cropRef.value.hidePopover();
uploadRef.value.clearFiles();
isShow.value = false;
};
const onCropper = info => (cropperInfo.value = info);
const handleSubmitImage = () => {
const formData = createFormData({
files: new File([cropperInfo.value], "avatar")
});
formUpload(formData)
.then(({ success, data }) => {
if (success) {
message("更新头像成功", { type: "success" });
handleClose();
} else {
message("更新头像失败");
}
})
.catch(error => {
message(`提交异常 ${error}`, { type: "error" });
});
};
//
const onSubmit = async (formEl: FormInstance) => {
await formEl.validate((valid, fields) => {
if (valid) {
console.log(userInfos);
message("更新信息成功", { type: "success" });
} else {
console.log("error submit!", fields);
}
});
};
getMine().then(res => {
Object.assign(userInfos, res.data);
});
</script>
<template>
<div
:class="[
'min-w-[180px]',
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
]"
>
<h3 class="my-8">个人信息</h3>
<el-form
ref="userInfoFormRef"
label-position="top"
:rules="rules"
:model="userInfos"
>
<el-form-item label="头像">
<el-avatar :size="80" :src="userInfos.avatar" />
<el-upload
ref="uploadRef"
accept="image/*"
action="#"
:limit="1"
:auto-upload="false"
:show-file-list="false"
:on-change="onChange"
>
<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="userInfos.nickname" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-autocomplete
v-model="userInfos.email"
:fetch-suggestions="queryEmail"
:trigger-on-focus="false"
placeholder="请输入邮箱"
clearable
class="w-full"
/>
</el-form-item>
<el-form-item label="联系电话">
<el-input
v-model="userInfos.phone"
placeholder="请输入联系电话"
clearable
/>
</el-form-item>
<el-form-item label="简介">
<el-input
v-model="userInfos.description"
placeholder="请输入简介"
type="textarea"
:autosize="{ minRows: 6, maxRows: 8 }"
maxlength="56"
show-word-limit
/>
</el-form-item>
<el-button type="primary" @click="onSubmit(userInfoFormRef)">
更新信息
</el-button>
</el-form>
<el-dialog
v-model="isShow"
width="40%"
title="编辑头像"
destroy-on-close
:closeOnClickModal="false"
:before-close="handleClose"
:fullscreen="deviceDetection()"
>
<ReCropperPreview ref="cropRef" :imgSrc="imgSrc" @cropper="onCropper" />
<template #footer>
<div class="dialog-footer">
<el-button bg text @click="handleClose">取消</el-button>
<el-button bg text type="primary" @click="handleSubmitImage">
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>

View File

@ -0,0 +1,87 @@
<script setup lang="ts">
import dayjs from "dayjs";
import { getMineLogs } from "@/api/user";
import { reactive, ref, onMounted } from "vue";
import { deviceDetection } from "@pureadmin/utils";
import type { PaginationProps } from "@pureadmin/table";
const loading = ref(true);
const dataList = ref([]);
const pagination = reactive<PaginationProps>({
total: 0,
pageSize: 10,
currentPage: 1,
background: true,
layout: "prev, pager, next"
});
const columns: TableColumnList = [
{
label: "详情",
prop: "summary",
minWidth: 140
},
{
label: "IP 地址",
prop: "ip",
minWidth: 100
},
{
label: "地点",
prop: "address",
minWidth: 140
},
{
label: "操作系统",
prop: "system",
minWidth: 100
},
{
label: "浏览器类型",
prop: "browser",
minWidth: 100
},
{
label: "时间",
prop: "operatingTime",
minWidth: 180,
formatter: ({ operatingTime }) =>
dayjs(operatingTime).format("YYYY-MM-DD HH:mm:ss")
}
];
async function onSearch() {
loading.value = true;
const { data } = await getMineLogs();
dataList.value = data.list;
pagination.total = data.total;
pagination.pageSize = data.pageSize;
pagination.currentPage = data.currentPage;
setTimeout(() => {
loading.value = false;
}, 200);
}
onMounted(() => {
onSearch();
});
</script>
<template>
<div
:class="[
'min-w-[180px]',
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
]"
>
<h3 class="my-8">安全日志</h3>
<pure-table
row-key="id"
table-layout="auto"
:loading="loading"
:data="dataList"
:columns="columns"
:pagination="pagination"
/>
</div>
</template>

View File

@ -0,0 +1,183 @@
<script setup lang="ts">
import { getMine } from "@/api/user";
import { useRouter } from "vue-router";
import { ref, onBeforeMount } from "vue";
import { ReText } from "@/components/ReText";
import Profile from "./components/profile.vue";
import Preferences from "./components/preferences.vue";
import SecurityLog from "./components/securityLog.vue";
import { useGlobal, deviceDetection } from "@pureadmin/utils";
import AccountManagement from "./components/accountManagement.vue";
import TopCollapse from "@/layout/components/sidebar/topCollapse.vue";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import leftLine from "@iconify-icons/ri/arrow-left-s-line";
import ProfileIcon from "@iconify-icons/ri/user-3-line";
import PreferencesIcon from "@iconify-icons/ri/settings-3-line";
import SecurityLogIcon from "@iconify-icons/ri/window-line";
import AccountManagementIcon from "@iconify-icons/ri/profile-line";
defineOptions({
name: "AccountSettings"
});
const router = useRouter();
const isOpen = ref(deviceDetection() ? false : true);
const { $storage } = useGlobal<GlobalPropertiesApi>();
onBeforeMount(() => {
useDataThemeChange().dataThemeChange($storage.layout?.overallStyle);
});
const userInfo = ref({
avatar: "",
username: "",
nickname: ""
});
const panes = [
{
key: "profile",
label: "个人信息",
icon: ProfileIcon,
component: Profile
},
{
key: "preferences",
label: "偏好设置",
icon: PreferencesIcon,
component: Preferences
},
{
key: "securityLog",
label: "安全日志",
icon: SecurityLogIcon,
component: SecurityLog
},
{
key: "accountManagement",
label: "账户管理",
icon: AccountManagementIcon,
component: AccountManagement
}
];
const witchPane = ref("profile");
getMine().then(res => {
userInfo.value = res.data;
});
</script>
<template>
<el-container class="h-full">
<el-aside
v-if="isOpen"
class="settings-sidebar px-2 dark:!bg-[var(--el-bg-color)]"
:width="deviceDetection() ? '180px' : '240px'"
>
<el-menu :default-active="witchPane" class="settings-menu">
<el-menu-item
class="hover:!transition-all hover:!duration-200 hover:!text-base !h-[50px]"
@click="router.go(-1)"
>
<div class="flex items-center">
<IconifyIconOffline :icon="leftLine" />
<span class="ml-2">返回</span>
</div>
</el-menu-item>
<div class="flex items-center ml-8 mt-4 mb-4">
<el-avatar :size="48" :src="userInfo.avatar" />
<div class="ml-4 flex flex-col max-w-[130px]">
<ReText class="font-bold !self-baseline">
{{ userInfo.nickname }}
</ReText>
<ReText class="!self-baseline" type="info">
{{ userInfo.username }}
</ReText>
</div>
</div>
<el-menu-item
v-for="item in panes"
:key="item.key"
:index="item.key"
@click="
() => {
witchPane = item.key;
if (deviceDetection()) {
isOpen = !isOpen;
}
}
"
>
<div class="flex items-center z-10">
<el-icon><IconifyIconOffline :icon="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</div>
</el-menu-item>
</el-menu>
</el-aside>
<el-main>
<TopCollapse
v-if="deviceDetection()"
class="px-0"
:is-active="isOpen"
@toggleClick="isOpen = !isOpen"
/>
<component
:is="panes.find(item => item.key === witchPane).component"
:class="[!deviceDetection() && 'ml-[120px]']"
/>
</el-main>
</el-container>
</template>
<style lang="scss" scoped>
.settings-sidebar {
overflow: hidden;
background: $menuBg;
border-right: 1px solid var(--pure-border-color);
}
.settings-menu {
background-color: transparent;
border: none;
::v-deep(.el-menu-item) {
height: 48px !important;
color: $menuText !important;
background-color: transparent !important;
transition: color 0.2s;
&:hover {
color: $menuTitleHover !important;
}
&.is-active {
color: #fff !important;
&:hover {
color: #fff !important;
}
&::before {
position: absolute;
inset: 0 8px;
margin: 4px 0;
clear: both;
content: "";
background: var(--el-color-primary);
border-radius: 3px;
}
}
}
}
body[layout] {
.el-menu--vertical .is-active {
color: #fff !important;
transition: color 0.2s;
&:hover {
color: #fff !important;
}
}
}
</style>

View File

@ -5,11 +5,11 @@ import editForm from "../form/index.vue";
import { zxcvbn } from "@zxcvbn-ts/core"; import { zxcvbn } from "@zxcvbn-ts/core";
import { handleTree } from "@/utils/tree"; import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message"; import { message } from "@/utils/message";
import croppingUpload from "../upload.vue";
import userAvatar from "@/assets/user.jpg"; import userAvatar from "@/assets/user.jpg";
import { usePublicHooks } from "../../hooks"; import { usePublicHooks } from "../../hooks";
import { addDialog } from "@/components/ReDialog"; import { addDialog } from "@/components/ReDialog";
import type { PaginationProps } from "@pureadmin/table"; import type { PaginationProps } from "@pureadmin/table";
import ReCropperPreview from "@/components/ReCropperPreview";
import type { FormItemProps, RoleFormItemProps } from "../utils/types"; import type { FormItemProps, RoleFormItemProps } from "../utils/types";
import { import {
getKeyList, getKeyList,
@ -365,11 +365,10 @@ export function useUser(tableRef: Ref, treeRef: Ref) {
addDialog({ addDialog({
title: "裁剪、上传头像", title: "裁剪、上传头像",
width: "40%", width: "40%",
draggable: true,
closeOnClickModal: false, closeOnClickModal: false,
fullscreen: deviceDetection(), fullscreen: deviceDetection(),
contentRenderer: () => contentRenderer: () =>
h(croppingUpload, { h(ReCropperPreview, {
ref: cropRef, ref: cropRef,
imgSrc: row.avatar || userAvatar, imgSrc: row.avatar || userAvatar,
onCropper: info => (avatarInfo.value = info) onCropper: info => (avatarInfo.value = info)