🎨登陆页面以及用户列表操作

This commit is contained in:
coward 2024-05-18 01:11:46 +08:00
parent 0d8f6b314b
commit 316ff4a2b2
42 changed files with 1105 additions and 369 deletions

View File

@ -1,35 +1,15 @@
// @ts-check // @ts-check
/** @type {import("@commitlint/types").UserConfig} */ // /** @type {import("@commitlint/types").UserConfig} */
export default { // export default {
ignores: [commit => commit.includes("init")], // ignores: [commit => commit.includes("init")],
extends: ["@commitlint/config-conventional"], // extends: ["@commitlint/config-conventional"],
rules: { // rules: {
"body-leading-blank": [2, "always"], // "body-leading-blank": [2, "always"],
"footer-leading-blank": [1, "always"], // "footer-leading-blank": [1, "always"],
"header-max-length": [2, "always", 108], // "header-max-length": [2, "always", 108],
"subject-empty": [2, "never"], // "subject-empty": [2, "never"],
"type-empty": [2, "never"], // "type-empty": [2, "never"],
"type-enum": [ // "type-enum": [2, "always", []]
2, // }
"always", // };
[
"feat",
"fix",
"perf",
"style",
"docs",
"test",
"refactor",
"build",
"ci",
"chore",
"revert",
"wip",
"workflow",
"types",
"release"
]
]
}
};

View File

@ -2,8 +2,8 @@
import { defineFakeRoute } from "vite-plugin-fake-server/client"; import { defineFakeRoute } from "vite-plugin-fake-server/client";
/** /**
* roles "admin""common" * roles "user""common"
* admin * user
* common * common
*/ */
const permissionRouter = { const permissionRouter = {
@ -19,7 +19,7 @@ const permissionRouter = {
name: "PermissionPage", name: "PermissionPage",
meta: { meta: {
title: "页面权限", title: "页面权限",
roles: ["admin", "common"] roles: ["user", "common"]
} }
}, },
{ {
@ -27,7 +27,7 @@ const permissionRouter = {
name: "PermissionButton", name: "PermissionButton",
meta: { meta: {
title: "按钮权限", title: "按钮权限",
roles: ["admin", "common"], roles: ["user", "common"],
auths: [ auths: [
"permission:btn:add", "permission:btn:add",
"permission:btn:edit", "permission:btn:edit",

View File

@ -6,16 +6,16 @@ export default defineFakeRoute([
url: "/login", url: "/login",
method: "post", method: "post",
response: ({ body }) => { response: ({ body }) => {
if (body.username === "admin") { if (body.username === "user") {
return { return {
success: true, success: true,
data: { data: {
avatar: "https://avatars.githubusercontent.com/u/44761321", avatar: "https://avatars.githubusercontent.com/u/44761321",
username: "admin", username: "user",
nickname: "小铭", nickname: "小铭",
// 一个用户可能有多个角色 // 一个用户可能有多个角色
roles: ["admin"], roles: ["user"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", accessToken: "eyJhbGciOiJIUzUxMiJ9.user",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
expires: "2030/10/30 00:00:00" expires: "2030/10/30 00:00:00"
} }

View File

@ -1,6 +1,6 @@
{ {
"Version": "5.5.0", "Version": "5.5.0",
"Title": "PureAdmin", "Title": "Wireguard-Dashboard",
"FixedHeader": true, "FixedHeader": true,
"HiddenSideBar": false, "HiddenSideBar": false,
"MultiTagsCache": false, "MultiTagsCache": false,

17
src/api/login.ts Normal file
View File

@ -0,0 +1,17 @@
import { http } from "@/utils/http";
import { baseUri } from "@/api/utils";
// 获取验证码
export const getCaptcha = () => {
return http.request<any>("get", baseUri("/captcha"));
};
// 登陆
export const login = (data?: object) => {
return http.request("post", baseUri("/login"), { data });
};
// 退出登陆
export const logout = () => {
return http.request("delete", baseUri("/user/logout"));
};

View File

@ -1,43 +1,27 @@
import { http } from "@/utils/http"; import { http } from "@/utils/http";
import { baseUri } from "@/api/utils";
export type UserResult = { // 获取当前登陆用户信息
success: boolean; export const getUser = () => {
data: { return http.request<any>("get", baseUri("/user"));
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 当前登录用户的角色 */
roles: Array<string>;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
}; };
export type RefreshTokenResult = { // 获取用户列表
success: boolean; export const userList = (params?: object) => {
data: { return http.request("get", baseUri("/user/list"), { params });
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
}; };
/** 登录 */ // 切换用户状态
export const getLogin = (data?: object) => { export const changeUserStatus = (data?: object) => {
return http.request<UserResult>("post", "/login", { data }); return http.request("put", baseUri("/user/change-status"), { data });
}; };
/** 刷新`token` */ // 新增/编辑用户信息
export const refreshTokenApi = (data?: object) => { export const editUser = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data }); return http.request("post", baseUri("/user/save"), { data });
};
// 删除管理员
export const deleteUser = (userId: string) => {
return http.request("delete", baseUri("/user/delete/" + userId));
}; };

1
src/api/utils.ts Normal file
View File

@ -0,0 +1 @@
export const baseUri = (uri: string) => `/api${uri}`;

View File

@ -13,7 +13,12 @@ import type {
const dialogStore = ref<Array<DialogOptions>>([]); const dialogStore = ref<Array<DialogOptions>>([]);
/** 打开弹框 */ /** 打开弹框 */
const addDialog = (options: DialogOptions) => { const addDialog = (options: {
contentRenderer: () => any;
title: any;
with: string;
props: { formInline: { name: any } };
}) => {
const open = () => const open = () =>
dialogStore.value.push(Object.assign(options, { visible: true })); dialogStore.value.push(Object.assign(options, { visible: true }));
if (options?.openDelay) { if (options?.openDelay) {

View File

@ -95,7 +95,9 @@ function handleClose(
v-bind="options" v-bind="options"
v-model="options.visible" v-model="options.visible"
class="pure-dialog" class="pure-dialog"
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false" :align-center="true"
:fullscreen="fullscreen ? true : !!options?.fullscreen"
center
@closed="handleClose(options, index)" @closed="handleClose(options, index)"
@opened="eventsCallBack('open', options, index)" @opened="eventsCallBack('open', options, index)"
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)" @openAutoFocus="eventsCallBack('openAutoFocus', options, index)"

View File

@ -158,6 +158,8 @@ type ButtonProps = {
interface DialogOptions extends DialogProps { interface DialogOptions extends DialogProps {
/** 内容区组件的 `props`,可通过 `defineProps` 接收 */ /** 内容区组件的 `props`,可通过 `defineProps` 接收 */
props?: any; props?: any;
/** dialog宽度 */
with: string;
/** 是否隐藏 `Dialog` 按钮操作区的内容 */ /** 是否隐藏 `Dialog` 按钮操作区的内容 */
hideFooter?: boolean; hideFooter?: boolean;
/** 确认按钮的 `Popconfirm` 气泡确认框相关配置 */ /** 确认按钮的 `Popconfirm` 气泡确认框相关配置 */

View File

@ -1,4 +1,4 @@
// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载 // 这里存放本地图标,在 src/layout/list.vue 文件中加载,避免在首启动加载
import { addIcon } from "@iconify/vue/dist/offline"; import { addIcon } from "@iconify/vue/dist/offline";
// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标 // 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标

View File

@ -0,0 +1,7 @@
import reImageVerify from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 图形验证码组件 */
export const ReImageVerify = withInstall(reImageVerify);
export default ReImageVerify;

View File

@ -0,0 +1,37 @@
import { ref, onMounted } from "vue";
import { getCaptcha } from "@/api/login";
// 绘制图形验证码
export const useImageVerify = () => {
const imgCode = ref("");
const imgCodeId = ref("");
function setImgCode(code: string) {
imgCode.value = code;
}
function setImgCodeId(codeId: string) {
imgCodeId.value = codeId;
}
function getImgCode() {
getCaptcha().then(res => {
if (res.code === 200) {
imgCode.value = res.data.captcha;
imgCodeId.value = res.data.id;
}
});
}
onMounted(() => {
getImgCode();
});
return {
imgCode,
imgCodeId,
setImgCode,
setImgCodeId,
getImgCode
};
};

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { watch } from "vue";
import { useImageVerify } from "./hooks";
import useGetGlobalProperties from "@/hooks/useGetGlobalProperties";
defineOptions({
name: "ReImageVerify"
});
const { $bus } = useGetGlobalProperties();
interface Props {
codeId?: string; // id
code?: string; //
}
interface Emits {
(e: "update:codeId", codeId: string): void;
(e: "update:code", code: string): void;
}
const props = withDefaults(defineProps<Props>(), {
codeId: "",
code: ""
});
const emit = defineEmits<Emits>();
const { imgCode, imgCodeId, setImgCode, setImgCodeId, getImgCode } =
useImageVerify();
watch(
() => props.codeId,
newValue => {
setImgCodeId(newValue);
}
);
watch(
() => props.code,
newValue => {
setImgCode(newValue);
}
);
watch(imgCodeId, newValue => {
emit("update:codeId", newValue);
});
watch(imgCode, newValue => {
emit("update:code", newValue);
});
$bus.on("refreshCode", value => {
if (!value) return;
getImgCode();
});
defineExpose({ getImgCode });
</script>
<template>
<img
id="verify-code"
width="120"
class="cursor-pointer"
:src="imgCode"
@click="getImgCode"
/>
</template>

View File

@ -0,0 +1,7 @@
import { getCurrentInstance } from "vue";
export default function useGetGlobalProperties() {
const instance = getCurrentInstance();
const globalProperties = instance?.appContext.config.globalProperties;
return { ...globalProperties };
}

View File

@ -0,0 +1,102 @@
<script setup lang="ts">
import { ref } from "vue";
import { FormInstance } from "element-plus";
// props
export interface FormProps {
formInline: {
id: string;
avatar: string;
name: string;
account: string;
email: string;
isAdmin: number;
status: number;
};
}
// props
// https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-props
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
id: "",
avatar: "",
name: "",
account: "",
email: "",
isAdmin: 0,
status: 1
})
});
// vue prop prop Vue
// reactive ref Ref value reactive
// newFormInline === props.formInline true props.formInline
// props.formInline
// https://cn.vuejs.org/guide/components/props.html#one-way-data-flow
const userEditForm = ref(props.formInline);
const userEditFormRef = ref<FormInstance>();
function getUserEditFormRef() {
return userEditFormRef.value;
}
defineExpose({ getUserEditFormRef });
</script>
<template>
<el-form ref="userEditFormRef" :model="userEditForm" label-width="20%">
<el-form-item
prop="name"
label="名称"
:rules="[{ required: true, message: '名称不能为空', trigger: 'blur' }]"
>
<el-input
v-model="userEditForm.name"
class="!w-[220px]"
placeholder="名称"
/>
</el-form-item>
<el-form-item
prop="account"
label="账号"
:rules="[{ required: true, message: '账号不能为空', trigger: 'blur' }]"
>
<el-input
v-model="userEditForm.account"
disabled
class="!w-[220px]"
placeholder="账号"
/>
</el-form-item>
<el-form-item prop="email" label="邮箱">
<el-input
v-model="userEditForm.email"
class="!w-[220px]"
placeholder="邮箱"
/>
</el-form-item>
<el-form-item
prop="isAdmin"
label="超管"
:rules="[
{ required: true, message: '是否为超管不能为空', trigger: 'blur' }
]"
>
<el-select v-model="userEditForm.isAdmin" disabled class="!w-[220px]">
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
</el-form-item>
<el-form-item
prop="status"
label="状态"
:rules="[{ required: true, message: '状态不能为空', trigger: 'blur' }]"
>
<el-select v-model="userEditForm.status" disabled class="!w-[220px]">
<el-option label="禁用" :value="0" />
<el-option label="启用" :value="1" />
</el-select>
</el-form-item>
</el-form>
</template>

View File

@ -9,6 +9,14 @@ import LaySidebarTopCollapse from "../lay-sidebar/components/SidebarTopCollapse.
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 { h, ref } from "vue";
import { addDialog } from "@/components/ReDialog/index";
import { editUser as editUserApi, getUser, userList } from "@/api/user";
import { storageLocal } from "@pureadmin/utils";
import { setUser, userKey } from "@/utils/auth";
import forms, { type FormProps } from "./component/user.vue";
import { useRouter } from "vue-router";
import useGetGlobalProperties from "@/hooks/useGetGlobalProperties";
const { const {
layout, layout,
@ -21,6 +29,62 @@ const {
avatarsStyle, avatarsStyle,
toggleSideBar toggleSideBar
} = useNav(); } = useNav();
const { $bus } = useGetGlobalProperties();
const router = useRouter();
const userEditFormRef = ref();
// eslint-disable-next-line vue/valid-define-emits
const emit = defineEmits();
//
const openUserInfoDialog = () => {
const loginUser = storageLocal().getItem(userKey);
addDialog({
width: "20%",
title: loginUser.name,
contentRenderer: () => h(forms, { ref: userEditFormRef }),
props: {
formInline: {
id: loginUser.id,
avatar: loginUser.avatar,
name: loginUser.name,
account: loginUser.account,
email: loginUser.email,
isAdmin: loginUser.isAdmin,
status: loginUser.status
}
},
beforeSure: (done, { options }) => {
const FormRef = userEditFormRef.value.getUserEditFormRef();
FormRef.validate(valid => {
if (!valid) return;
editUserApi(options.props.formInline).then(res => {
if (res.code === 200) {
//
getUser().then(res => {
if (res.code === 200) {
setUser(res.data);
//
if (router.options.history.location === "/user/index") {
userList({
current: 1,
size: 10
}).then(res => {
if (res.code === 200) {
$bus.emit("userListData", res);
}
});
}
}
});
done();
}
});
});
}
});
};
</script> </script>
<template> <template>
@ -41,11 +105,11 @@ const {
<div v-if="layout === 'vertical'" class="vertical-header-right"> <div v-if="layout === 'vertical'" class="vertical-header-right">
<!-- 菜单搜索 --> <!-- 菜单搜索 -->
<LaySearch id="header-search" /> <!-- <LaySearch id="header-search" />-->
<!-- 全屏 --> <!-- 全屏 -->
<LaySidebarFullScreen id="full-screen" /> <!-- <LaySidebarFullScreen id="full-screen" />-->
<!-- 消息通知 --> <!-- 消息通知 -->
<LayNotice id="header-notice" /> <!-- <LayNotice id="header-notice" />-->
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none"> <span class="el-dropdown-link navbar-bg-hover select-none">
@ -53,6 +117,15 @@ const {
<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-menu class="logout">
<el-dropdown-item @click="openUserInfoDialog">
<IconifyIconOnline
icon="eva:person-outline"
style="margin: 5px"
/>
个人信息
</el-dropdown-item>
</el-dropdown-menu>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
<el-dropdown-item @click="logout"> <el-dropdown-item @click="logout">
<IconifyIconOffline <IconifyIconOffline
@ -110,8 +183,8 @@ const {
} }
img { img {
width: 22px; width: 52px;
height: 22px; height: 52px;
border-radius: 50%; border-radius: 50%;
} }
} }

View File

@ -14,6 +14,7 @@ import { useGlobal, isAllEmpty } from "@pureadmin/utils";
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";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill"; import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import { message } from "@/utils/message";
const errorInfo = const errorInfo =
"The current routing configuration is incorrect, please check the configuration"; "The current routing configuration is incorrect, please check the configuration";
@ -46,9 +47,9 @@ export function useNav() {
/** 昵称(如果昵称为空则显示用户名) */ /** 昵称(如果昵称为空则显示用户名) */
const username = computed(() => { const username = computed(() => {
return isAllEmpty(useUserStoreHook()?.nickname) return !isAllEmpty(useUserStoreHook()?.name)
? useUserStoreHook()?.username ? useUserStoreHook()?.name
: useUserStoreHook()?.nickname; : useUserStoreHook()?.account;
}); });
const avatarsStyle = computed(() => { const avatarsStyle = computed(() => {
@ -81,7 +82,13 @@ export function useNav() {
/** 退出登录 */ /** 退出登录 */
function logout() { function logout() {
useUserStoreHook().logOut(); useUserStoreHook()
.logout()
.then(res => {
if (res.code === 200) {
message("退出登陆成功", { type: "success" });
}
});
} }
function backTopMenu() { function backTopMenu() {

View File

@ -7,6 +7,7 @@ import { MotionPlugin } from "@vueuse/motion";
import { createApp, type Directive } from "vue"; import { createApp, type Directive } from "vue";
import { useElementPlus } from "@/plugins/elementPlus"; import { useElementPlus } from "@/plugins/elementPlus";
import { injectResponsiveStorage } from "@/utils/responsive"; import { injectResponsiveStorage } from "@/utils/responsive";
import mitt from "mitt";
import Table from "@pureadmin/table"; import Table from "@pureadmin/table";
// import PureDescriptions from "@pureadmin/descriptions"; // import PureDescriptions from "@pureadmin/descriptions";
@ -22,6 +23,7 @@ import "element-plus/dist/index.css";
import "./assets/iconfont/iconfont.js"; import "./assets/iconfont/iconfont.js";
import "./assets/iconfont/iconfont.css"; import "./assets/iconfont/iconfont.css";
const EventMitt = mitt();
const app = createApp(App); const app = createApp(App);
// 自定义指令 // 自定义指令
@ -50,6 +52,14 @@ import "tippy.js/themes/light.css";
import VueTippy from "vue-tippy"; import VueTippy from "vue-tippy";
app.use(VueTippy); app.use(VueTippy);
declare module "vue" {
export interface ComponentCustomProperties {
$bus: typeof EventMitt;
}
}
app.config.globalProperties.$bus = EventMitt;
getPlatformConfig(app).then(async config => { getPlatformConfig(app).then(async config => {
setupStore(app); setupStore(app);
app.use(router); app.use(router);

View File

@ -1,17 +1,13 @@
// import "@/utils/sso";
import Cookies from "js-cookie";
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import NProgress from "@/utils/progress"; import NProgress from "@/utils/progress";
import { buildHierarchyTree } from "@/utils/tree"; import { buildHierarchyTree } from "@/utils/tree";
import remainingRouter from "./modules/remaining"; import remainingRouter from "./modules/remaining";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import { isUrl, openLink, storageLocal, isAllEmpty } from "@pureadmin/utils"; import { isUrl, openLink, storageLocal } from "@pureadmin/utils";
import { import {
ascending, ascending,
getTopMenu, addPathMatch,
initRouter,
isOneOfArray,
getHistoryMode, getHistoryMode,
findRouteByPath, findRouteByPath,
handleAliveRoute, handleAliveRoute,
@ -24,12 +20,7 @@ import {
type RouteRecordRaw, type RouteRecordRaw,
type RouteComponent type RouteComponent
} from "vue-router"; } from "vue-router";
import { import { removeToken, TokenKey } from "@/utils/auth";
type DataInfo,
userKey,
removeToken,
multipleTabsKey
} from "@/utils/auth";
/** src/router/modules .ts remaining.ts /** src/router/modules .ts remaining.ts
* https://github.com/mrmlnc/fast-glob#basic-syntax * https://github.com/mrmlnc/fast-glob#basic-syntax
@ -113,7 +104,7 @@ router.beforeEach((to: ToRouteType, _from, next) => {
handleAliveRoute(to); handleAliveRoute(to);
} }
} }
const userInfo = storageLocal().getItem<DataInfo<number>>(userKey); const tokenObj = storageLocal().getItem<any>(TokenKey);
NProgress.start(); NProgress.start();
const externalLink = isUrl(to?.name as string); const externalLink = isUrl(to?.name as string);
if (!externalLink) { if (!externalLink) {
@ -128,11 +119,7 @@ router.beforeEach((to: ToRouteType, _from, next) => {
function toCorrectRoute() { function toCorrectRoute() {
whiteList.includes(to.fullPath) ? next(_from.fullPath) : next(); whiteList.includes(to.fullPath) ? next(_from.fullPath) : next();
} }
if (Cookies.get(multipleTabsKey) && userInfo) { if (tokenObj) {
// 无权限跳转403页面
if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
next({ path: "/error/403" });
}
// 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面 // 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面
if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") { if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") {
next({ path: "/error/404" }); next({ path: "/error/404" });
@ -151,37 +138,23 @@ router.beforeEach((to: ToRouteType, _from, next) => {
usePermissionStoreHook().wholeMenus.length === 0 && usePermissionStoreHook().wholeMenus.length === 0 &&
to.path !== "/login" to.path !== "/login"
) { ) {
initRouter().then((router: Router) => { usePermissionStoreHook().handleWholeMenus([]);
if (!useMultiTagsStoreHook().getMultiTagsCache) { addPathMatch();
const { path } = to; if (!useMultiTagsStoreHook().getMultiTagsCache) {
const route = findRouteByPath( const { path } = to;
path, const route = findRouteByPath(
router.options.routes[0].children path,
); router.options.routes[0].children
getTopMenu(true); );
// query、params模式路由传参数的标签页不在此处处理 // query、params模式路由传参数的标签页不在此处处理
if (route && route.meta?.title) { if (route && route.meta?.title) {
if (isAllEmpty(route.parentId) && route.meta?.backstage) { useMultiTagsStoreHook().handleTags("push", {
// 此处为动态顶级路由(目录) path: route.path,
const { path, name, meta } = route.children[0]; name: route.name,
useMultiTagsStoreHook().handleTags("push", { meta: route.meta
path, });
name,
meta
});
} else {
const { path, name, meta } = route;
useMultiTagsStoreHook().handleTags("push", {
path,
name,
meta
});
}
}
} }
// 确保动态路由完全加入路由列表并且不影响静态路由注意动态路由刷新时router.beforeEach可能会触发两次第一次触发动态路由还未完全添加第二次动态路由才完全添加到路由列表如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断这样就只会触发一次 }
if (isAllEmpty(to.name)) router.push(to.fullPath);
});
} }
toCorrectRoute(); toCorrectRoute();
} }

View File

@ -0,0 +1,17 @@
export default {
path: "/user",
meta: {
title: "用户"
},
children: [
{
path: "/user/index",
name: "UserList",
component: () => import("@/views/user/list.vue"),
meta: {
title: "用户列表",
showParent: true
}
}
]
};

View File

@ -5,7 +5,8 @@ export default {
icon: "ri:information-line", icon: "ri:information-line",
// showLink: false, // showLink: false,
title: "异常页面", title: "异常页面",
rank: 9 rank: 9,
showLink: false
}, },
children: [ children: [
{ {

View File

@ -0,0 +1,26 @@
export default {
path: "/server",
meta: {
title: "服务端"
},
children: [
{
path: "/server/index",
name: "Server",
component: () => import("@/views/server/list.vue"),
meta: {
title: "服务端列表",
showParent: true
}
},
{
path: "/server/config",
name: "ServerConfig",
component: () => import("@/views/server/config.vue"),
meta: {
title: "服务端配置",
showParent: true
}
}
]
};

View File

@ -18,7 +18,7 @@ import {
} from "@pureadmin/utils"; } from "@pureadmin/utils";
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import { buildHierarchyTree } from "@/utils/tree"; import { buildHierarchyTree } from "@/utils/tree";
import { userKey, type DataInfo } from "@/utils/auth"; import { userKey } from "@/utils/auth";
import { type menuType, routerArrays } from "@/layout/types"; import { type menuType, routerArrays } from "@/layout/types";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
@ -83,8 +83,7 @@ function isOneOfArray(a: Array<string>, b: Array<string>) {
/** 从localStorage里取出当前登录用户的角色roles过滤无权限的菜单 */ /** 从localStorage里取出当前登录用户的角色roles过滤无权限的菜单 */
function filterNoPermissionTree(data: RouteComponent[]) { function filterNoPermissionTree(data: RouteComponent[]) {
const currentRoles = const currentRoles = storageLocal().getItem<any>(userKey)?.roles ?? [];
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
const newTree = cloneDeep(data).filter((v: any) => const newTree = cloneDeep(data).filter((v: any) =>
isOneOfArray(v.meta?.roles, currentRoles) isOneOfArray(v.meta?.roles, currentRoles)
); );

View File

@ -1,68 +1,56 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { import { store, router, resetRouter, type userType } from "../utils";
type userType, import { login, logout } from "@/api/login";
store, import { setToken, removeToken, setUser, userKey } from "@/utils/auth";
router, import { getUser } from "@/api/user";
resetRouter, import { storageLocal } from "@pureadmin/utils";
routerArrays,
storageLocal
} from "../utils";
import {
type UserResult,
type RefreshTokenResult,
getLogin,
refreshTokenApi
} from "@/api/user";
import { useMultiTagsStoreHook } from "./multiTags";
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 => ({
// 头像 id: storageLocal().getItem<any>(userKey)?.id ?? "",
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "", name: storageLocal().getItem<any>(userKey)?.name ?? "",
// 用户名 avatar: storageLocal().getItem<any>(userKey)?.avatar ?? "",
username: storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "", account: storageLocal().getItem<any>(userKey)?.account ?? "",
// 昵称 email: storageLocal().getItem<any>(userKey)?.email ?? "",
nickname: storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "", isAdmin: storageLocal().getItem<any>(userKey)?.isAdmin ?? 0,
// 页面级别权限 status: storageLocal().getItem<any>(userKey)?.status ?? 1
roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [],
// 是否勾选了登录页的免登录
isRemembered: false,
// 登录页的免登录存储几天默认7天
loginDay: 7
}), }),
actions: { actions: {
/** 存储头像 */ SET_ID(id: string) {
this.id = id;
},
SET_NAME(name: string) {
this.name = name;
},
SET_AVATAR(avatar: string) { SET_AVATAR(avatar: string) {
this.avatar = avatar; this.avatar = avatar;
}, },
/** 存储用户名 */ SET_ACCOUNT(account: string) {
SET_USERNAME(username: string) { this.account = account;
this.username = username;
}, },
/** 存储昵称 */ SET_EMAIL(email: string) {
SET_NICKNAME(nickname: string) { this.email = email;
this.nickname = nickname;
}, },
/** 存储角色 */ SET_IS_ADMIN(isAdmin: number) {
SET_ROLES(roles: Array<string>) { this.isAdmin = isAdmin;
this.roles = roles;
}, },
/** 存储是否勾选了登录页的免登录 */ SET_STATUS(status: number) {
SET_ISREMEMBERED(bool: boolean) { this.status = status;
this.isRemembered = bool;
},
/** 设置登录页的免登录存储几天 */
SET_LOGINDAY(value: number) {
this.loginDay = Number(value);
}, },
/** 登入 */ /** 登入 */
async loginByUsername(data) { async loginByUsername(data: any) {
return new Promise<UserResult>((resolve, reject) => { return new Promise<any>((resolve, reject) => {
getLogin(data) login(data)
.then(data => { .then(data => {
if (data?.success) setToken(data.data); if (data.code === 200) {
setToken(data.data); // 设置token
getUser().then(res => {
if (res.code === 200) {
setUser(res.data);
}
});
}
resolve(data); resolve(data);
}) })
.catch(error => { .catch(error => {
@ -70,27 +58,19 @@ export const useUserStore = defineStore({
}); });
}); });
}, },
/** 前端登出(不调用接口) */ /** 前端登出 */
logOut() { logout() {
this.username = ""; return new Promise<any>((resolve, reject) => {
this.roles = []; logout()
removeToken();
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
resetRouter();
router.push("/login");
},
/** 刷新`token` */
async handRefreshToken(data) {
return new Promise<RefreshTokenResult>((resolve, reject) => {
refreshTokenApi(data)
.then(data => { .then(data => {
if (data) { if (data.code === 200) {
setToken(data.data); removeToken();
resolve(data); resetRouter();
router.push("/login");
} }
}) })
.catch(error => { .catch(err => {
reject(error); reject(err);
}); });
}); });
} }

View File

@ -37,10 +37,11 @@ export type setType = {
}; };
export type userType = { export type userType = {
id?: string;
name?: string;
avatar?: string; avatar?: string;
username?: string; account?: string;
nickname?: string; email?: string;
roles?: Array<string>; isAdmin?: number;
isRemembered?: boolean; status?: number;
loginDay?: number;
}; };

View File

@ -94,3 +94,8 @@
justify-content: center; justify-content: center;
} }
} }
/*验证码高度控制*/
#verify-code {
height: 100%;
}

View File

@ -2,22 +2,22 @@ import Cookies from "js-cookie";
import { storageLocal } from "@pureadmin/utils"; import { storageLocal } from "@pureadmin/utils";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
export interface DataInfo<T> { // export interface DataInfo<T> {
/** token */ // /** token */
accessToken: string; // accessToken: string;
/** `accessToken`的过期时间(时间戳) */ // /** `accessToken`的过期时间(时间戳) */
expires: T; // expires: T;
/** 用于调用刷新accessToken的接口时所需的token */ // /** 用于调用刷新accessToken的接口时所需的token */
refreshToken: string; // refreshToken: string;
/** 头像 */ // /** 头像 */
avatar?: string; // avatar?: string;
/** 用户名 */ // /** 用户名 */
username?: string; // username?: string;
/** 昵称 */ // /** 昵称 */
nickname?: string; // nickname?: string;
/** 当前登录用户的角色 */ // /** 当前登录用户的角色 */
roles?: Array<string>; // roles?: Array<string>;
} // }
export const userKey = "user-info"; export const userKey = "user-info";
export const TokenKey = "authorized-token"; export const TokenKey = "authorized-token";
@ -30,11 +30,14 @@ export const TokenKey = "authorized-token";
export const multipleTabsKey = "multiple-tabs"; export const multipleTabsKey = "multiple-tabs";
/** 获取`token` */ /** 获取`token` */
export function getToken(): DataInfo<number> { export function getToken(): any {
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错 // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
return Cookies.get(TokenKey) return storageLocal().getItem(TokenKey);
? JSON.parse(Cookies.get(TokenKey)) }
: storageLocal().getItem(userKey);
// 获取当前登陆用户信息
export function getUser(): any {
return storageLocal().getItem(userKey);
} }
/** /**
@ -43,75 +46,32 @@ export function getToken(): DataInfo<number> {
* `accessToken``expires``refreshToken`key值为authorized-token的cookie里 * `accessToken``expires``refreshToken`key值为authorized-token的cookie里
* `avatar``username``nickname``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: any) {
let expires = 0; const token = formatToken(data.token);
const { accessToken, refreshToken } = data; const expires = data.expireAt;
const { isRemembered, loginDay } = useUserStoreHook(); storageLocal().setItem(TokenKey, {
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳将此处代码改为expires = data.expires然后把上面的DataInfo<Date>改成DataInfo<number>即可 expireAt: expires,
const cookieString = JSON.stringify({ accessToken, expires, refreshToken }); token: token
});
}
expires > 0 // 设置用户信息
? Cookies.set(TokenKey, cookieString, { export function setUser(data: any) {
expires: (expires - Date.now()) / 86400000 storageLocal().setItem(userKey, data);
}) useUserStoreHook().SET_ID(data.id);
: Cookies.set(TokenKey, cookieString); useUserStoreHook().SET_NAME(data.name);
useUserStoreHook().SET_AVATAR(data.avatar);
Cookies.set( useUserStoreHook().SET_ACCOUNT(data.account);
multipleTabsKey, useUserStoreHook().SET_EMAIL(data.email);
"true", useUserStoreHook().SET_IS_ADMIN(data.isAdmin);
isRemembered useUserStoreHook().SET_STATUS(data.status);
? {
expires: loginDay
}
: {}
);
function setUserKey({ avatar, username, nickname, roles }) {
useUserStoreHook().SET_AVATAR(avatar);
useUserStoreHook().SET_USERNAME(username);
useUserStoreHook().SET_NICKNAME(nickname);
useUserStoreHook().SET_ROLES(roles);
storageLocal().setItem(userKey, {
refreshToken,
expires,
avatar,
username,
nickname,
roles
});
}
if (data.username && data.roles) {
const { username, roles } = data;
setUserKey({
avatar: data?.avatar ?? "",
username,
nickname: data?.nickname ?? "",
roles
});
} else {
const avatar =
storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "";
const username =
storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "";
const nickname =
storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "";
const roles =
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
setUserKey({
avatar,
username,
nickname,
roles
});
}
} }
/** 删除`token`以及key值为`user-info`的localStorage信息 */ /** 删除`token`以及key值为`user-info`的localStorage信息 */
export function removeToken() { export function removeToken() {
Cookies.remove(TokenKey); Cookies.remove(TokenKey);
Cookies.remove(multipleTabsKey); Cookies.remove(multipleTabsKey);
storageLocal().removeItem(userKey); storageLocal().clear(); // 清空全部
} }
/** 格式化tokenjwt格式 */ /** 格式化tokenjwt格式 */

View File

@ -12,7 +12,7 @@ import type {
import { stringify } from "qs"; import { stringify } from "qs";
import NProgress from "../progress"; import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth"; import { getToken, formatToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user"; import { message } from "@/utils/message";
// 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1 // 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = { const defaultConfig: AxiosRequestConfig = {
@ -73,35 +73,18 @@ class PureHttp {
return config; return config;
} }
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */ /** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
const whiteList = ["/refresh-token", "/login"]; const whiteList = ["/captcha", "/login"];
return whiteList.some(url => config.url.endsWith(url)) return whiteList.some(url => config.url.endsWith(url))
? config ? config
: new Promise(resolve => { : new Promise(resolve => {
const data = getToken(); const data = getToken();
if (data) { if (data) {
const now = new Date().getTime(); const now = new Date().getTime() / 1000;
const expired = parseInt(data.expires) - now <= 0; const expired = parseInt(data.expireAt) - now <= 0;
if (expired) { if (expired) {
if (!PureHttp.isRefreshing) {
PureHttp.isRefreshing = true;
// token过期刷新
useUserStoreHook()
.handRefreshToken({ refreshToken: data.refreshToken })
.then(res => {
const token = res.data.accessToken;
config.headers["Authorization"] = formatToken(token);
PureHttp.requests.forEach(cb => cb(token));
PureHttp.requests = [];
})
.finally(() => {
PureHttp.isRefreshing = false;
});
}
resolve(PureHttp.retryOriginalRequest(config)); resolve(PureHttp.retryOriginalRequest(config));
} else { } else {
config.headers["Authorization"] = formatToken( config.headers["Authorization"] = data.token;
data.accessToken
);
resolve(config); resolve(config);
} }
} else { } else {
@ -167,6 +150,19 @@ class PureHttp {
resolve(response); resolve(response);
}) })
.catch(error => { .catch(error => {
if (error.response === null || error.response === undefined) {
message(error.message, { type: "error" });
} else {
if (
error.response.data === null ||
error.response.data === undefined ||
error.response.data === ""
) {
message(error.response.statusText, { type: "error" });
} else {
message(error.response.data.message, { type: "error" });
}
}
reject(error); reject(error);
}); });
}); });

View File

@ -8,7 +8,7 @@ class StorageProxy implements ProxyStorage {
this.storage.config({ this.storage.config({
// 首选IndexedDB作为第一驱动不支持IndexedDB会自动降级到localStorageWebSQL被弃用详情看https://developer.chrome.com/blog/deprecating-web-sql // 首选IndexedDB作为第一驱动不支持IndexedDB会自动降级到localStorageWebSQL被弃用详情看https://developer.chrome.com/blog/deprecating-web-sql
driver: [this.storage.INDEXEDDB, this.storage.LOCALSTORAGE], driver: [this.storage.INDEXEDDB, this.storage.LOCALSTORAGE],
name: "pure-admin" name: "pure-user"
}); });
} }

View File

@ -30,7 +30,7 @@ interface MessageParams {
onClose?: Function | null; onClose?: Function | null;
} }
/** 用法非常简单,参考 src/views/components/message/index.vue 文件 */ /** 用法非常简单,参考 src/views/components/message/list.vue 文件 */
/** /**
* `Message` * `Message`

View File

@ -7,7 +7,7 @@ import { useNav } from "@/layout/hooks/useNav";
import type { FormInstance } from "element-plus"; import type { FormInstance } from "element-plus";
import { useLayout } from "@/layout/hooks/useLayout"; import { useLayout } from "@/layout/hooks/useLayout";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
import { initRouter, getTopMenu } from "@/router/utils"; import { addPathMatch } from "@/router/utils";
import { bg, avatar, illustration } from "./utils/static"; import { bg, avatar, illustration } from "./utils/static";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, reactive, toRaw, onMounted, onBeforeUnmount } from "vue"; import { ref, reactive, toRaw, onMounted, onBeforeUnmount } from "vue";
@ -17,13 +17,17 @@ import dayIcon from "@/assets/svg/day.svg?component";
import darkIcon from "@/assets/svg/dark.svg?component"; import darkIcon from "@/assets/svg/dark.svg?component";
import Lock from "@iconify-icons/ri/lock-fill"; import Lock from "@iconify-icons/ri/lock-fill";
import User from "@iconify-icons/ri/user-3-fill"; import User from "@iconify-icons/ri/user-3-fill";
import ReImageVerify from "@/components/ReImageVerify/src/index.vue";
import { usePermissionStoreHook } from "@/store/modules/permission";
import useGetGlobalProperties from "@/hooks/useGetGlobalProperties";
defineOptions({ defineOptions({
name: "Login" name: "Login"
}); });
const { $bus } = useGetGlobalProperties();
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
const ruleFormRef = ref<FormInstance>(); const loginRuleFormRef = ref<FormInstance>();
const { initStorage } = useLayout(); const { initStorage } = useLayout();
initStorage(); initStorage();
@ -32,30 +36,38 @@ const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
dataThemeChange(overallStyle.value); dataThemeChange(overallStyle.value);
const { title } = useNav(); const { title } = useNav();
const ruleForm = reactive({ const imgCode = ref("");
username: "admin", const imgCodeId = ref("");
password: "admin123" const loginRuleForm = reactive({
account: "", //
password: "", //
captchaId: "", // id
captchaAnswer: "" //
}); });
//
const onLogin = async (formEl: FormInstance | undefined) => { const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
await formEl.validate((valid, fields) => { await formEl.validate((valid, fields) => {
if (valid) { if (valid) {
loading.value = true; loading.value = true;
loginRuleForm.captchaId = imgCodeId.value;
useUserStoreHook() useUserStoreHook()
.loginByUsername({ username: ruleForm.username, password: "admin123" }) .loginByUsername(loginRuleForm)
.then(res => { .then(res => {
if (res.success) { if (res.code === 200) {
// //
return initRouter().then(() => { usePermissionStoreHook().handleWholeMenus([]);
router.push(getTopMenu(true).path).then(() => { addPathMatch();
message("登录成功", { type: "success" }); router.push("/welcome");
}); message("登录成功", { type: "success" });
});
} else { } else {
message("登录失败", { type: "error" }); message("登录失败", { type: "error" });
} }
}) })
.catch(e => {
$bus.emit("refreshCode", true);
})
.finally(() => (loading.value = false)); .finally(() => (loading.value = false));
} else { } else {
return fields; return fields;
@ -66,7 +78,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
/** 使用公共函数,避免`removeEventListener`失效 */ /** 使用公共函数,避免`removeEventListener`失效 */
function onkeypress({ code }: KeyboardEvent) { function onkeypress({ code }: KeyboardEvent) {
if (code === "Enter") { if (code === "Enter") {
onLogin(ruleFormRef.value); onLogin(loginRuleFormRef.value);
} }
} }
@ -100,28 +112,19 @@ onBeforeUnmount(() => {
<div class="login-form"> <div class="login-form">
<avatar class="avatar" /> <avatar class="avatar" />
<Motion> <Motion>
<h2 class="outline-none">{{ title }}</h2> <h2 class="outline-none">{{ title }} LOGIN</h2>
</Motion> </Motion>
<el-form <el-form
ref="ruleFormRef" ref="loginRuleFormRef"
:model="ruleForm" :model="loginRuleForm"
:rules="loginRules" :rules="loginRules"
size="large" size="large"
> >
<Motion :delay="100"> <Motion :delay="100">
<el-form-item <el-form-item prop="account">
:rules="[
{
required: true,
message: '请输入账号',
trigger: 'blur'
}
]"
prop="username"
>
<el-input <el-input
v-model="ruleForm.username" v-model="loginRuleForm.account"
clearable clearable
placeholder="账号" placeholder="账号"
:prefix-icon="useRenderIcon(User)" :prefix-icon="useRenderIcon(User)"
@ -132,7 +135,7 @@ onBeforeUnmount(() => {
<Motion :delay="150"> <Motion :delay="150">
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input
v-model="ruleForm.password" v-model="loginRuleForm.password"
clearable clearable
show-password show-password
placeholder="密码" placeholder="密码"
@ -140,14 +143,29 @@ onBeforeUnmount(() => {
/> />
</el-form-item> </el-form-item>
</Motion> </Motion>
<Motion :delay="200">
<el-form-item prop="captchaAnswer">
<el-input
v-model="loginRuleForm.captchaAnswer"
clearable
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
>
<template v-slot:append>
<ReImageVerify
v-model:code="imgCode"
v-model:codeId="imgCodeId"
/>
</template>
</el-input>
</el-form-item>
</Motion>
<Motion :delay="250"> <Motion :delay="250">
<el-button <el-button
class="w-full mt-4" class="w-full mt-4"
size="default" size="default"
type="primary" type="primary"
:loading="loading" :loading="loading"
@click="onLogin(ruleFormRef)" @click="onLogin(loginRuleFormRef)"
> >
登录 登录
</el-button> </el-button>

View File

@ -1,25 +1,36 @@
import { reactive } from "vue"; import { reactive } from "vue";
import type { FormRules } from "element-plus"; import type { FormRules } from "element-plus";
/** 密码正则密码格式应为8-18位数字、字母、符号的任意两种组合 */
export const REGEXP_PWD =
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
/** 登录校验 */ /** 登录校验 */
const loginRules = reactive(<FormRules>{ const loginRules = reactive(<FormRules>{
account: [
{
required: true,
message: "账号不能为空",
trigger: "blur"
}
],
password: [ password: [
{ {
validator: (rule, value, callback) => { required: true,
if (value === "") { message: "密码不能为空",
callback(new Error("请输入密码")); trigger: "blur"
} else if (!REGEXP_PWD.test(value)) { },
callback( {
new Error("密码格式应为8-18位数字、字母、符号的任意两种组合") min: 6,
); message: "密码长度最少6位",
} else { trigger: "blur"
callback(); },
} {
}, max: 32,
message: "密码长度最长32位",
trigger: "blur"
}
],
captchaAnswer: [
{
required: true,
message: "验证码不能为空",
trigger: "blur" trigger: "blur"
} }
] ]

View File

@ -20,7 +20,7 @@ const username = ref(useUserStoreHook()?.username);
const options = [ const options = [
{ {
value: "admin", value: "user",
label: "管理员角色" label: "管理员角色"
}, },
{ {

View File

@ -0,0 +1,10 @@
<script setup lang="ts">
defineOptions({
// name name
name: "ServerConfig"
});
</script>
<template>
<div>服务端配置页面</div>
</template>

10
src/views/server/list.vue Normal file
View File

@ -0,0 +1,10 @@
<script setup lang="ts">
defineOptions({
// name name
name: "Server"
});
</script>
<template>
<div>服务端列表</div>
</template>

View File

@ -0,0 +1,119 @@
<script setup lang="ts">
import { ref } from "vue";
import { FormInstance } from "element-plus";
import { storageLocal } from "@pureadmin/utils";
import { userKey } from "@/utils/auth";
// props
export interface FormProps {
formInline: {
id: string;
avatar: string;
name: string;
account: string;
email: string;
isAdmin: number;
status: number;
};
}
// props
// https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-props
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
id: "",
avatar: "",
name: "",
account: "",
email: "",
isAdmin: 0,
status: 1
})
});
// vue prop prop Vue
// reactive ref Ref value reactive
// newFormInline === props.formInline true props.formInline
// props.formInline
// https://cn.vuejs.org/guide/components/props.html#one-way-data-flow
const userEditForm = ref(props.formInline);
const userEditFormRef = ref<FormInstance>();
function getUserEditFormRef() {
return userEditFormRef.value;
}
//
function isAccountDisabled() {
return userEditForm.value.id !== "" && userEditForm.value.id !== undefined;
}
//
function isAdminDisabled() {
// admin
return storageLocal().getItem(userKey).account !== "admin";
}
defineExpose({ getUserEditFormRef });
</script>
<template>
<el-form ref="userEditFormRef" :model="userEditForm" label-width="20%">
<el-form-item
prop="name"
label="名称"
:rules="[{ required: true, message: '名称不能为空', trigger: 'blur' }]"
>
<el-input
v-model="userEditForm.name"
class="!w-[220px]"
placeholder="名称"
/>
</el-form-item>
<el-form-item
prop="account"
label="账号"
:rules="[{ required: true, message: '账号不能为空', trigger: 'blur' }]"
>
<el-input
v-model="userEditForm.account"
:disabled="isAccountDisabled()"
class="!w-[220px]"
placeholder="账号"
/>
</el-form-item>
<el-form-item prop="email" label="邮箱">
<el-input
v-model="userEditForm.email"
class="!w-[220px]"
placeholder="邮箱"
/>
</el-form-item>
<el-form-item
prop="isAdmin"
label="超管"
:rules="[
{ required: true, message: '是否为超管不能为空', trigger: 'blur' }
]"
>
<el-select
v-model="userEditForm.isAdmin"
:disabled="isAdminDisabled()"
class="!w-[220px]"
>
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
</el-form-item>
<el-form-item
prop="status"
label="状态"
:rules="[{ required: true, message: '状态不能为空', trigger: 'blur' }]"
>
<el-select v-model="userEditForm.status" class="!w-[220px]">
<el-option label="禁用" :value="0" />
<el-option label="启用" :value="1" />
</el-select>
</el-form-item>
</el-form>
</template>

300
src/views/user/list.vue Normal file
View File

@ -0,0 +1,300 @@
<script setup lang="tsx">
import {
userList as userListApi,
changeUserStatus as changeUserStatusApi,
editUser as editUserApi,
deleteUser as deleteUserApi
} from "@/api/user";
import { h, reactive, ref } from "vue";
import { Delete, Edit } from "@element-plus/icons-vue";
import { addDialog } from "@/components/ReDialog/index";
import forms, { type FormProps } from "./component/form.vue";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import AddFill from "@iconify-icons/ri/add-circle-line";
import { storageLocal } from "@pureadmin/utils";
import { userKey } from "@/utils/auth";
import { message } from "@/utils/message";
import useGetGlobalProperties from "@/hooks/useGetGlobalProperties";
defineOptions({
// name name
name: "UserList"
});
const { $bus } = useGetGlobalProperties();
// -
const userEditFormRef = ref();
//
const userListForm = {
current: 1,
size: 10
};
let userListData = reactive({
data: [],
total: 0
}); //
//
const userList = () => {
userListApi(userListForm).then(res => {
if (res.code === 200) {
userListData.data = res.data.records;
userListData.total = res.data.total;
userListForm.current = res.data.current;
userListForm.size = res.data.size;
}
});
};
//
const pageChange = (page: number, size: number) => {
userListForm.size = size;
userListForm.current = page;
userList();
};
//
const changeUserStatus = (status: number, userId: string) => {
changeUserStatusApi({
id: userId,
status: status.toString()
}).then(res => {
if (res.code === 200) {
userList();
}
});
};
//
const openEditDialog = (userInfo?: any) => {
addDialog({
width: "20%",
title: userInfo.name,
contentRenderer: () => h(forms, { ref: userEditFormRef }),
props: {
formInline: {
id: userInfo.id,
name: userInfo.name,
avatar: userInfo.avatar,
account: userInfo.account,
email: userInfo.email,
isAdmin: userInfo.isAdmin,
status: userInfo.status
}
},
beforeSure: (done, { options }) => {
const FormRef = userEditFormRef.value.getUserEditFormRef();
FormRef.validate(valid => {
if (!valid) return;
editUserApi(options.props.formInline).then(res => {
if (res.code === 200) {
done();
userList();
}
});
});
}
});
};
//
const openAddDialog = () => {
addDialog({
width: "20%",
title: "添加管理员",
contentRenderer: () => h(forms, { ref: userEditFormRef }),
props: {
formInline: {
isAdmin: 0,
status: 1
}
},
beforeSure: (done, { options }) => {
const FormRef = userEditFormRef.value.getUserEditFormRef();
FormRef.validate(valid => {
if (!valid) return;
editUserApi(options.props.formInline).then(res => {
if (res.code === 200) {
done();
userList();
}
});
});
}
});
};
//
const userDelete = (userId: string) => {
if (userId !== "" || userId !== undefined) {
deleteUserApi(userId).then(res => {
if (res.code === 200) {
userList();
} else {
message(res.message, { type: "error" });
}
});
}
};
//
const deleteOrEditBtnDisable = (userInfo?: object) => {
const loginUser = storageLocal().getItem(userKey);
//
if (loginUser.isAdmin !== 1) {
return false;
}
//
if (loginUser.id === userInfo.id) {
return false;
}
//
return !(userInfo.isAdmin === 1 && loginUser.account !== "admin");
};
$bus.on("userListData", value => {
userListData.data = value.data.records;
userListData.total = value.data.total;
userListForm.current = value.data.current;
userListForm.size = value.data.size;
});
//
userList(); //
</script>
<template>
<div class="user-list-table">
<div class="user-list-table-header">
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openAddDialog"
>
添加
</el-button>
</div>
<div class="list">
<el-table :data="userListData.data" :border="true" style="width: 100%">
<el-table-column prop="id" label="id" min-width="125" align="center" />
<el-table-column
prop="name"
label="名称"
min-width="80"
align="center"
/>
<el-table-column
prop="avatar"
label="头像"
min-width="35"
align="center"
>
<template #default="scope">
<img class="table-avatar" :src="scope.row.avatar" alt="头像" />
</template>
</el-table-column>
<el-table-column prop="account" label="账号" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column
prop="isAdmin"
label="是否为超级管理员"
min-width="60"
align="center"
>
<template #default="scope">
<el-tag v-if="scope.row.isAdmin === 1" effect="dark"></el-tag>
<el-tag v-else effect="dark" type="warning"></el-tag>
</template>
</el-table-column>
<el-table-column
prop="status"
label="状态"
min-width="70"
align="center"
>
<template #default="scope">
<el-switch
v-model="scope.row.status"
:disabled="!deleteOrEditBtnDisable(scope.row)"
active-text="启用"
inactive-text="禁用"
:active-value="1"
:inactive-value="0"
@change="changeUserStatus(scope.row.status, scope.row.id)"
/>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" align="center" />
<el-table-column prop="updatedAt" label="更新时间" align="center" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
v-if="deleteOrEditBtnDisable(scope.row)"
size="small"
type="primary"
:icon="Edit"
@click="openEditDialog(scope.row)"
>
编辑
</el-button>
<el-popconfirm
v-if="deleteOrEditBtnDisable(scope.row)"
width="220"
confirm-button-text="确认"
cancel-button-text="取消"
icon-color="#626AEF"
title="是否删除?"
@confirm="userDelete(scope.row.id)"
>
<template #reference>
<el-button size="small" type="danger" :icon="Delete">
删除
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<div class="div-page">
<el-pagination
small
background
layout="total,prev,pager,next"
:page-size="userListForm.size"
:total="userListData.total"
@change="pageChange"
/>
</div>
</div>
</template>
<style scoped lang="scss">
.table-avatar {
width: 52px;
height: 52px;
border-radius: 50%;
}
.user-list-form .el-input {
--el-input-width: 220px;
}
.list {
margin-top: 10px;
}
.user-list-table-header {
background-color: #fff;
}
.div-page {
padding: 20px;
text-align: right;
background-color: #fff;
}
</style>

View File

@ -5,5 +5,5 @@ defineOptions({
</script> </script>
<template> <template>
<h1>Pure-Admin-Thin非国际化版本</h1> <h1>Wireguard-Dashboard</h1>
</template> </template>

4
types/index.d.ts vendored
View File

@ -75,6 +75,6 @@ interface ComponentElRef<T extends HTMLElement = HTMLDivElement> {
$el: T; $el: T;
} }
function parseInt(s: string | number, radix?: number): number; // function parseInt(s: string | number, radix?: number): number;
function parseFloat(string: string | number): number; // function parseFloat(string: string | number): number;

View File

@ -1,4 +1,5 @@
import Vue, { VNode } from "vue"; import type { VNode } from "vue";
import type Vue from "vue";
declare module "*.tsx" { declare module "*.tsx" {
import Vue from "compatible-vue"; import Vue from "compatible-vue";

View File

@ -24,7 +24,12 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
port: VITE_PORT, port: VITE_PORT,
host: "0.0.0.0", host: "0.0.0.0",
// 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy // 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy
proxy: {}, proxy: {
"/api": {
target: "http://127.0.0.1:9703",
changeOrigin: true
}
},
// 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布 // 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布
warmup: { warmup: {
clientFiles: ["./index.html", "./src/{views,components}/*"] clientFiles: ["./index.html", "./src/{views,components}/*"]