🎨登陆页面以及用户列表操作
This commit is contained in:
parent
0d8f6b314b
commit
316ff4a2b2
@ -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"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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
17
src/api/login.ts
Normal 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"));
|
||||||
|
};
|
@ -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
1
src/api/utils.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const baseUri = (uri: string) => `/api${uri}`;
|
@ -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) {
|
||||||
|
@ -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)"
|
||||||
|
@ -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` 气泡确认框相关配置 */
|
||||||
|
@ -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 添加即可渲染菜单图标
|
||||||
|
7
src/components/ReImageVerify/index.ts
Normal file
7
src/components/ReImageVerify/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import reImageVerify from "./src/index.vue";
|
||||||
|
import { withInstall } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
/** 图形验证码组件 */
|
||||||
|
export const ReImageVerify = withInstall(reImageVerify);
|
||||||
|
|
||||||
|
export default ReImageVerify;
|
37
src/components/ReImageVerify/src/hooks.ts
Normal file
37
src/components/ReImageVerify/src/hooks.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
70
src/components/ReImageVerify/src/index.vue
Normal file
70
src/components/ReImageVerify/src/index.vue
Normal 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>
|
7
src/hooks/useGetGlobalProperties.ts
Normal file
7
src/hooks/useGetGlobalProperties.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { getCurrentInstance } from "vue";
|
||||||
|
|
||||||
|
export default function useGetGlobalProperties() {
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
const globalProperties = instance?.appContext.config.globalProperties;
|
||||||
|
return { ...globalProperties };
|
||||||
|
}
|
102
src/layout/components/lay-navbar/component/user.vue
Normal file
102
src/layout/components/lay-navbar/component/user.vue
Normal 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>
|
@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
10
src/main.ts
10
src/main.ts
@ -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);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
17
src/router/modules/admin.ts
Normal file
17
src/router/modules/admin.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -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: [
|
||||||
{
|
{
|
||||||
|
26
src/router/modules/server.ts
Normal file
26
src/router/modules/server.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -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)
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
};
|
};
|
||||||
|
@ -94,3 +94,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*验证码高度控制*/
|
||||||
|
#verify-code {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
@ -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(); // 清空全部
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 格式化token(jwt格式) */
|
/** 格式化token(jwt格式) */
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@ class StorageProxy implements ProxyStorage {
|
|||||||
this.storage.config({
|
this.storage.config({
|
||||||
// 首选IndexedDB作为第一驱动,不支持IndexedDB会自动降级到localStorage(WebSQL被弃用,详情看https://developer.chrome.com/blog/deprecating-web-sql)
|
// 首选IndexedDB作为第一驱动,不支持IndexedDB会自动降级到localStorage(WebSQL被弃用,详情看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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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` 消息提示函数
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -20,7 +20,7 @@ const username = ref(useUserStoreHook()?.username);
|
|||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
value: "admin",
|
value: "user",
|
||||||
label: "管理员角色"
|
label: "管理员角色"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
10
src/views/server/config.vue
Normal file
10
src/views/server/config.vue
Normal 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
10
src/views/server/list.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
// name 作为一种规范最好必须写上并且和路由的name保持一致
|
||||||
|
name: "Server"
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>服务端列表</div>
|
||||||
|
</template>
|
119
src/views/user/component/form.vue
Normal file
119
src/views/user/component/form.vue
Normal 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
300
src/views/user/list.vue
Normal 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>
|
@ -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
4
types/index.d.ts
vendored
@ -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;
|
||||||
|
3
types/shims-tsx.d.ts
vendored
3
types/shims-tsx.d.ts
vendored
@ -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";
|
||||||
|
@ -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}/*"]
|
||||||
|
Loading…
Reference in New Issue
Block a user