🎨登陆页面以及用户列表操作
This commit is contained in:
parent
0d8f6b314b
commit
316ff4a2b2
@ -1,35 +1,15 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import("@commitlint/types").UserConfig} */
|
||||
export default {
|
||||
ignores: [commit => commit.includes("init")],
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
rules: {
|
||||
"body-leading-blank": [2, "always"],
|
||||
"footer-leading-blank": [1, "always"],
|
||||
"header-max-length": [2, "always", 108],
|
||||
"subject-empty": [2, "never"],
|
||||
"type-empty": [2, "never"],
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"style",
|
||||
"docs",
|
||||
"test",
|
||||
"refactor",
|
||||
"build",
|
||||
"ci",
|
||||
"chore",
|
||||
"revert",
|
||||
"wip",
|
||||
"workflow",
|
||||
"types",
|
||||
"release"
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
// /** @type {import("@commitlint/types").UserConfig} */
|
||||
// export default {
|
||||
// ignores: [commit => commit.includes("init")],
|
||||
// extends: ["@commitlint/config-conventional"],
|
||||
// rules: {
|
||||
// "body-leading-blank": [2, "always"],
|
||||
// "footer-leading-blank": [1, "always"],
|
||||
// "header-max-length": [2, "always", 108],
|
||||
// "subject-empty": [2, "never"],
|
||||
// "type-empty": [2, "never"],
|
||||
// "type-enum": [2, "always", []]
|
||||
// }
|
||||
// };
|
||||
|
@ -2,8 +2,8 @@
|
||||
import { defineFakeRoute } from "vite-plugin-fake-server/client";
|
||||
|
||||
/**
|
||||
* roles:页面级别权限,这里模拟二种 "admin"、"common"
|
||||
* admin:管理员角色
|
||||
* roles:页面级别权限,这里模拟二种 "user"、"common"
|
||||
* user:管理员角色
|
||||
* common:普通角色
|
||||
*/
|
||||
const permissionRouter = {
|
||||
@ -19,7 +19,7 @@ const permissionRouter = {
|
||||
name: "PermissionPage",
|
||||
meta: {
|
||||
title: "页面权限",
|
||||
roles: ["admin", "common"]
|
||||
roles: ["user", "common"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -27,7 +27,7 @@ const permissionRouter = {
|
||||
name: "PermissionButton",
|
||||
meta: {
|
||||
title: "按钮权限",
|
||||
roles: ["admin", "common"],
|
||||
roles: ["user", "common"],
|
||||
auths: [
|
||||
"permission:btn:add",
|
||||
"permission:btn:edit",
|
||||
|
@ -6,16 +6,16 @@ export default defineFakeRoute([
|
||||
url: "/login",
|
||||
method: "post",
|
||||
response: ({ body }) => {
|
||||
if (body.username === "admin") {
|
||||
if (body.username === "user") {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
avatar: "https://avatars.githubusercontent.com/u/44761321",
|
||||
username: "admin",
|
||||
username: "user",
|
||||
nickname: "小铭",
|
||||
// 一个用户可能有多个角色
|
||||
roles: ["admin"],
|
||||
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
|
||||
roles: ["user"],
|
||||
accessToken: "eyJhbGciOiJIUzUxMiJ9.user",
|
||||
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
|
||||
expires: "2030/10/30 00:00:00"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"Version": "5.5.0",
|
||||
"Title": "PureAdmin",
|
||||
"Title": "Wireguard-Dashboard",
|
||||
"FixedHeader": true,
|
||||
"HiddenSideBar": 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 { baseUri } from "@/api/utils";
|
||||
|
||||
export type UserResult = {
|
||||
success: boolean;
|
||||
data: {
|
||||
/** 头像 */
|
||||
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 const getUser = () => {
|
||||
return http.request<any>("get", baseUri("/user"));
|
||||
};
|
||||
|
||||
export type RefreshTokenResult = {
|
||||
success: boolean;
|
||||
data: {
|
||||
/** `token` */
|
||||
accessToken: string;
|
||||
/** 用于调用刷新`accessToken`的接口时所需的`token` */
|
||||
refreshToken: string;
|
||||
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
|
||||
expires: Date;
|
||||
};
|
||||
// 获取用户列表
|
||||
export const userList = (params?: object) => {
|
||||
return http.request("get", baseUri("/user/list"), { params });
|
||||
};
|
||||
|
||||
/** 登录 */
|
||||
export const getLogin = (data?: object) => {
|
||||
return http.request<UserResult>("post", "/login", { data });
|
||||
// 切换用户状态
|
||||
export const changeUserStatus = (data?: object) => {
|
||||
return http.request("put", baseUri("/user/change-status"), { data });
|
||||
};
|
||||
|
||||
/** 刷新`token` */
|
||||
export const refreshTokenApi = (data?: object) => {
|
||||
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
|
||||
// 新增/编辑用户信息
|
||||
export const editUser = (data?: object) => {
|
||||
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 addDialog = (options: DialogOptions) => {
|
||||
const addDialog = (options: {
|
||||
contentRenderer: () => any;
|
||||
title: any;
|
||||
with: string;
|
||||
props: { formInline: { name: any } };
|
||||
}) => {
|
||||
const open = () =>
|
||||
dialogStore.value.push(Object.assign(options, { visible: true }));
|
||||
if (options?.openDelay) {
|
||||
|
@ -95,7 +95,9 @@ function handleClose(
|
||||
v-bind="options"
|
||||
v-model="options.visible"
|
||||
class="pure-dialog"
|
||||
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
|
||||
:align-center="true"
|
||||
:fullscreen="fullscreen ? true : !!options?.fullscreen"
|
||||
center
|
||||
@closed="handleClose(options, index)"
|
||||
@opened="eventsCallBack('open', options, index)"
|
||||
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
|
||||
|
@ -158,6 +158,8 @@ type ButtonProps = {
|
||||
interface DialogOptions extends DialogProps {
|
||||
/** 内容区组件的 `props`,可通过 `defineProps` 接收 */
|
||||
props?: any;
|
||||
/** dialog宽度 */
|
||||
with: string;
|
||||
/** 是否隐藏 `Dialog` 按钮操作区的内容 */
|
||||
hideFooter?: boolean;
|
||||
/** 确认按钮的 `Popconfirm` 气泡确认框相关配置 */
|
||||
|
@ -1,4 +1,4 @@
|
||||
// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
|
||||
// 这里存放本地图标,在 src/layout/list.vue 文件中加载,避免在首启动加载
|
||||
import { addIcon } from "@iconify/vue/dist/offline";
|
||||
|
||||
// 本地菜单图标,后端在路由的 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 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 {
|
||||
layout,
|
||||
@ -21,6 +29,62 @@ const {
|
||||
avatarsStyle,
|
||||
toggleSideBar
|
||||
} = 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>
|
||||
|
||||
<template>
|
||||
@ -41,11 +105,11 @@ const {
|
||||
|
||||
<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">
|
||||
<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>
|
||||
</span>
|
||||
<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-item @click="logout">
|
||||
<IconifyIconOffline
|
||||
@ -110,8 +183,8 @@ const {
|
||||
}
|
||||
|
||||
img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import { useGlobal, isAllEmpty } from "@pureadmin/utils";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
|
||||
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
|
||||
import { message } from "@/utils/message";
|
||||
|
||||
const errorInfo =
|
||||
"The current routing configuration is incorrect, please check the configuration";
|
||||
@ -46,9 +47,9 @@ export function useNav() {
|
||||
|
||||
/** 昵称(如果昵称为空则显示用户名) */
|
||||
const username = computed(() => {
|
||||
return isAllEmpty(useUserStoreHook()?.nickname)
|
||||
? useUserStoreHook()?.username
|
||||
: useUserStoreHook()?.nickname;
|
||||
return !isAllEmpty(useUserStoreHook()?.name)
|
||||
? useUserStoreHook()?.name
|
||||
: useUserStoreHook()?.account;
|
||||
});
|
||||
|
||||
const avatarsStyle = computed(() => {
|
||||
@ -81,7 +82,13 @@ export function useNav() {
|
||||
|
||||
/** 退出登录 */
|
||||
function logout() {
|
||||
useUserStoreHook().logOut();
|
||||
useUserStoreHook()
|
||||
.logout()
|
||||
.then(res => {
|
||||
if (res.code === 200) {
|
||||
message("退出登陆成功", { type: "success" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 { useElementPlus } from "@/plugins/elementPlus";
|
||||
import { injectResponsiveStorage } from "@/utils/responsive";
|
||||
import mitt from "mitt";
|
||||
|
||||
import Table from "@pureadmin/table";
|
||||
// import PureDescriptions from "@pureadmin/descriptions";
|
||||
@ -22,6 +23,7 @@ import "element-plus/dist/index.css";
|
||||
import "./assets/iconfont/iconfont.js";
|
||||
import "./assets/iconfont/iconfont.css";
|
||||
|
||||
const EventMitt = mitt();
|
||||
const app = createApp(App);
|
||||
|
||||
// 自定义指令
|
||||
@ -50,6 +52,14 @@ import "tippy.js/themes/light.css";
|
||||
import VueTippy from "vue-tippy";
|
||||
app.use(VueTippy);
|
||||
|
||||
declare module "vue" {
|
||||
export interface ComponentCustomProperties {
|
||||
$bus: typeof EventMitt;
|
||||
}
|
||||
}
|
||||
|
||||
app.config.globalProperties.$bus = EventMitt;
|
||||
|
||||
getPlatformConfig(app).then(async config => {
|
||||
setupStore(app);
|
||||
app.use(router);
|
||||
|
@ -1,17 +1,13 @@
|
||||
// import "@/utils/sso";
|
||||
import Cookies from "js-cookie";
|
||||
import { getConfig } from "@/config";
|
||||
import NProgress from "@/utils/progress";
|
||||
import { buildHierarchyTree } from "@/utils/tree";
|
||||
import remainingRouter from "./modules/remaining";
|
||||
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
import { isUrl, openLink, storageLocal, isAllEmpty } from "@pureadmin/utils";
|
||||
import { isUrl, openLink, storageLocal } from "@pureadmin/utils";
|
||||
import {
|
||||
ascending,
|
||||
getTopMenu,
|
||||
initRouter,
|
||||
isOneOfArray,
|
||||
addPathMatch,
|
||||
getHistoryMode,
|
||||
findRouteByPath,
|
||||
handleAliveRoute,
|
||||
@ -24,12 +20,7 @@ import {
|
||||
type RouteRecordRaw,
|
||||
type RouteComponent
|
||||
} from "vue-router";
|
||||
import {
|
||||
type DataInfo,
|
||||
userKey,
|
||||
removeToken,
|
||||
multipleTabsKey
|
||||
} from "@/utils/auth";
|
||||
import { removeToken, TokenKey } from "@/utils/auth";
|
||||
|
||||
/** 自动导入全部静态路由,无需再手动引入!匹配 src/router/modules 目录(任何嵌套级别)中具有 .ts 扩展名的所有文件,除了 remaining.ts 文件
|
||||
* 如何匹配所有文件请看:https://github.com/mrmlnc/fast-glob#basic-syntax
|
||||
@ -113,7 +104,7 @@ router.beforeEach((to: ToRouteType, _from, next) => {
|
||||
handleAliveRoute(to);
|
||||
}
|
||||
}
|
||||
const userInfo = storageLocal().getItem<DataInfo<number>>(userKey);
|
||||
const tokenObj = storageLocal().getItem<any>(TokenKey);
|
||||
NProgress.start();
|
||||
const externalLink = isUrl(to?.name as string);
|
||||
if (!externalLink) {
|
||||
@ -128,11 +119,7 @@ router.beforeEach((to: ToRouteType, _from, next) => {
|
||||
function toCorrectRoute() {
|
||||
whiteList.includes(to.fullPath) ? next(_from.fullPath) : next();
|
||||
}
|
||||
if (Cookies.get(multipleTabsKey) && userInfo) {
|
||||
// 无权限跳转403页面
|
||||
if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
|
||||
next({ path: "/error/403" });
|
||||
}
|
||||
if (tokenObj) {
|
||||
// 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面
|
||||
if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") {
|
||||
next({ path: "/error/404" });
|
||||
@ -151,37 +138,23 @@ router.beforeEach((to: ToRouteType, _from, next) => {
|
||||
usePermissionStoreHook().wholeMenus.length === 0 &&
|
||||
to.path !== "/login"
|
||||
) {
|
||||
initRouter().then((router: Router) => {
|
||||
if (!useMultiTagsStoreHook().getMultiTagsCache) {
|
||||
const { path } = to;
|
||||
const route = findRouteByPath(
|
||||
path,
|
||||
router.options.routes[0].children
|
||||
);
|
||||
getTopMenu(true);
|
||||
// query、params模式路由传参数的标签页不在此处处理
|
||||
if (route && route.meta?.title) {
|
||||
if (isAllEmpty(route.parentId) && route.meta?.backstage) {
|
||||
// 此处为动态顶级路由(目录)
|
||||
const { path, name, meta } = route.children[0];
|
||||
useMultiTagsStoreHook().handleTags("push", {
|
||||
path,
|
||||
name,
|
||||
meta
|
||||
});
|
||||
} else {
|
||||
const { path, name, meta } = route;
|
||||
useMultiTagsStoreHook().handleTags("push", {
|
||||
path,
|
||||
name,
|
||||
meta
|
||||
});
|
||||
}
|
||||
}
|
||||
usePermissionStoreHook().handleWholeMenus([]);
|
||||
addPathMatch();
|
||||
if (!useMultiTagsStoreHook().getMultiTagsCache) {
|
||||
const { path } = to;
|
||||
const route = findRouteByPath(
|
||||
path,
|
||||
router.options.routes[0].children
|
||||
);
|
||||
// query、params模式路由传参数的标签页不在此处处理
|
||||
if (route && route.meta?.title) {
|
||||
useMultiTagsStoreHook().handleTags("push", {
|
||||
path: route.path,
|
||||
name: route.name,
|
||||
meta: route.meta
|
||||
});
|
||||
}
|
||||
// 确保动态路由完全加入路由列表并且不影响静态路由(注意:动态路由刷新时router.beforeEach可能会触发两次,第一次触发动态路由还未完全添加,第二次动态路由才完全添加到路由列表,如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断,这样就只会触发一次)
|
||||
if (isAllEmpty(to.name)) router.push(to.fullPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
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",
|
||||
// showLink: false,
|
||||
title: "异常页面",
|
||||
rank: 9
|
||||
rank: 9,
|
||||
showLink: false
|
||||
},
|
||||
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";
|
||||
import { getConfig } from "@/config";
|
||||
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 { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
@ -83,8 +83,7 @@ function isOneOfArray(a: Array<string>, b: Array<string>) {
|
||||
|
||||
/** 从localStorage里取出当前登录用户的角色roles,过滤无权限的菜单 */
|
||||
function filterNoPermissionTree(data: RouteComponent[]) {
|
||||
const currentRoles =
|
||||
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
|
||||
const currentRoles = storageLocal().getItem<any>(userKey)?.roles ?? [];
|
||||
const newTree = cloneDeep(data).filter((v: any) =>
|
||||
isOneOfArray(v.meta?.roles, currentRoles)
|
||||
);
|
||||
|
@ -1,68 +1,56 @@
|
||||
import { defineStore } from "pinia";
|
||||
import {
|
||||
type userType,
|
||||
store,
|
||||
router,
|
||||
resetRouter,
|
||||
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";
|
||||
import { store, router, resetRouter, type userType } from "../utils";
|
||||
import { login, logout } from "@/api/login";
|
||||
import { setToken, removeToken, setUser, userKey } from "@/utils/auth";
|
||||
import { getUser } from "@/api/user";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: "pure-user",
|
||||
state: (): userType => ({
|
||||
// 头像
|
||||
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "",
|
||||
// 用户名
|
||||
username: storageLocal().getItem<DataInfo<number>>(userKey)?.username ?? "",
|
||||
// 昵称
|
||||
nickname: storageLocal().getItem<DataInfo<number>>(userKey)?.nickname ?? "",
|
||||
// 页面级别权限
|
||||
roles: storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [],
|
||||
// 是否勾选了登录页的免登录
|
||||
isRemembered: false,
|
||||
// 登录页的免登录存储几天,默认7天
|
||||
loginDay: 7
|
||||
id: storageLocal().getItem<any>(userKey)?.id ?? "",
|
||||
name: storageLocal().getItem<any>(userKey)?.name ?? "",
|
||||
avatar: storageLocal().getItem<any>(userKey)?.avatar ?? "",
|
||||
account: storageLocal().getItem<any>(userKey)?.account ?? "",
|
||||
email: storageLocal().getItem<any>(userKey)?.email ?? "",
|
||||
isAdmin: storageLocal().getItem<any>(userKey)?.isAdmin ?? 0,
|
||||
status: storageLocal().getItem<any>(userKey)?.status ?? 1
|
||||
}),
|
||||
actions: {
|
||||
/** 存储头像 */
|
||||
SET_ID(id: string) {
|
||||
this.id = id;
|
||||
},
|
||||
SET_NAME(name: string) {
|
||||
this.name = name;
|
||||
},
|
||||
SET_AVATAR(avatar: string) {
|
||||
this.avatar = avatar;
|
||||
},
|
||||
/** 存储用户名 */
|
||||
SET_USERNAME(username: string) {
|
||||
this.username = username;
|
||||
SET_ACCOUNT(account: string) {
|
||||
this.account = account;
|
||||
},
|
||||
/** 存储昵称 */
|
||||
SET_NICKNAME(nickname: string) {
|
||||
this.nickname = nickname;
|
||||
SET_EMAIL(email: string) {
|
||||
this.email = email;
|
||||
},
|
||||
/** 存储角色 */
|
||||
SET_ROLES(roles: Array<string>) {
|
||||
this.roles = roles;
|
||||
SET_IS_ADMIN(isAdmin: number) {
|
||||
this.isAdmin = isAdmin;
|
||||
},
|
||||
/** 存储是否勾选了登录页的免登录 */
|
||||
SET_ISREMEMBERED(bool: boolean) {
|
||||
this.isRemembered = bool;
|
||||
},
|
||||
/** 设置登录页的免登录存储几天 */
|
||||
SET_LOGINDAY(value: number) {
|
||||
this.loginDay = Number(value);
|
||||
SET_STATUS(status: number) {
|
||||
this.status = status;
|
||||
},
|
||||
/** 登入 */
|
||||
async loginByUsername(data) {
|
||||
return new Promise<UserResult>((resolve, reject) => {
|
||||
getLogin(data)
|
||||
async loginByUsername(data: any) {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
login(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);
|
||||
})
|
||||
.catch(error => {
|
||||
@ -70,27 +58,19 @@ export const useUserStore = defineStore({
|
||||
});
|
||||
});
|
||||
},
|
||||
/** 前端登出(不调用接口) */
|
||||
logOut() {
|
||||
this.username = "";
|
||||
this.roles = [];
|
||||
removeToken();
|
||||
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
|
||||
resetRouter();
|
||||
router.push("/login");
|
||||
},
|
||||
/** 刷新`token` */
|
||||
async handRefreshToken(data) {
|
||||
return new Promise<RefreshTokenResult>((resolve, reject) => {
|
||||
refreshTokenApi(data)
|
||||
/** 前端登出 */
|
||||
logout() {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
logout()
|
||||
.then(data => {
|
||||
if (data) {
|
||||
setToken(data.data);
|
||||
resolve(data);
|
||||
if (data.code === 200) {
|
||||
removeToken();
|
||||
resetRouter();
|
||||
router.push("/login");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -37,10 +37,11 @@ export type setType = {
|
||||
};
|
||||
|
||||
export type userType = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
username?: string;
|
||||
nickname?: string;
|
||||
roles?: Array<string>;
|
||||
isRemembered?: boolean;
|
||||
loginDay?: number;
|
||||
account?: string;
|
||||
email?: string;
|
||||
isAdmin?: number;
|
||||
status?: number;
|
||||
};
|
||||
|
@ -94,3 +94,8 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/*验证码高度控制*/
|
||||
#verify-code {
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -2,22 +2,22 @@ import Cookies from "js-cookie";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
|
||||
export interface DataInfo<T> {
|
||||
/** token */
|
||||
accessToken: string;
|
||||
/** `accessToken`的过期时间(时间戳) */
|
||||
expires: T;
|
||||
/** 用于调用刷新accessToken的接口时所需的token */
|
||||
refreshToken: string;
|
||||
/** 头像 */
|
||||
avatar?: string;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
/** 当前登录用户的角色 */
|
||||
roles?: Array<string>;
|
||||
}
|
||||
// export interface DataInfo<T> {
|
||||
// /** token */
|
||||
// accessToken: string;
|
||||
// /** `accessToken`的过期时间(时间戳) */
|
||||
// expires: T;
|
||||
// /** 用于调用刷新accessToken的接口时所需的token */
|
||||
// refreshToken: string;
|
||||
// /** 头像 */
|
||||
// avatar?: string;
|
||||
// /** 用户名 */
|
||||
// username?: string;
|
||||
// /** 昵称 */
|
||||
// nickname?: string;
|
||||
// /** 当前登录用户的角色 */
|
||||
// roles?: Array<string>;
|
||||
// }
|
||||
|
||||
export const userKey = "user-info";
|
||||
export const TokenKey = "authorized-token";
|
||||
@ -30,11 +30,14 @@ export const TokenKey = "authorized-token";
|
||||
export const multipleTabsKey = "multiple-tabs";
|
||||
|
||||
/** 获取`token` */
|
||||
export function getToken(): DataInfo<number> {
|
||||
export function getToken(): any {
|
||||
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
|
||||
return Cookies.get(TokenKey)
|
||||
? JSON.parse(Cookies.get(TokenKey))
|
||||
: storageLocal().getItem(userKey);
|
||||
return storageLocal().getItem(TokenKey);
|
||||
}
|
||||
|
||||
// 获取当前登陆用户信息
|
||||
export function getUser(): any {
|
||||
return storageLocal().getItem(userKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,75 +46,32 @@ export function getToken(): DataInfo<number> {
|
||||
* 将`accessToken`、`expires`、`refreshToken`这三条信息放在key值为authorized-token的cookie里(过期自动销毁)
|
||||
* 将`avatar`、`username`、`nickname`、`roles`、`refreshToken`、`expires`这六条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁)
|
||||
*/
|
||||
export function setToken(data: DataInfo<Date>) {
|
||||
let expires = 0;
|
||||
const { accessToken, refreshToken } = data;
|
||||
const { isRemembered, loginDay } = useUserStoreHook();
|
||||
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可
|
||||
const cookieString = JSON.stringify({ accessToken, expires, refreshToken });
|
||||
export function setToken(data: any) {
|
||||
const token = formatToken(data.token);
|
||||
const expires = data.expireAt;
|
||||
storageLocal().setItem(TokenKey, {
|
||||
expireAt: expires,
|
||||
token: token
|
||||
});
|
||||
}
|
||||
|
||||
expires > 0
|
||||
? Cookies.set(TokenKey, cookieString, {
|
||||
expires: (expires - Date.now()) / 86400000
|
||||
})
|
||||
: Cookies.set(TokenKey, cookieString);
|
||||
|
||||
Cookies.set(
|
||||
multipleTabsKey,
|
||||
"true",
|
||||
isRemembered
|
||||
? {
|
||||
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
|
||||
});
|
||||
}
|
||||
// 设置用户信息
|
||||
export function setUser(data: any) {
|
||||
storageLocal().setItem(userKey, data);
|
||||
useUserStoreHook().SET_ID(data.id);
|
||||
useUserStoreHook().SET_NAME(data.name);
|
||||
useUserStoreHook().SET_AVATAR(data.avatar);
|
||||
useUserStoreHook().SET_ACCOUNT(data.account);
|
||||
useUserStoreHook().SET_EMAIL(data.email);
|
||||
useUserStoreHook().SET_IS_ADMIN(data.isAdmin);
|
||||
useUserStoreHook().SET_STATUS(data.status);
|
||||
}
|
||||
|
||||
/** 删除`token`以及key值为`user-info`的localStorage信息 */
|
||||
export function removeToken() {
|
||||
Cookies.remove(TokenKey);
|
||||
Cookies.remove(multipleTabsKey);
|
||||
storageLocal().removeItem(userKey);
|
||||
storageLocal().clear(); // 清空全部
|
||||
}
|
||||
|
||||
/** 格式化token(jwt格式) */
|
||||
|
@ -12,7 +12,7 @@ import type {
|
||||
import { stringify } from "qs";
|
||||
import NProgress from "../progress";
|
||||
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
|
||||
const defaultConfig: AxiosRequestConfig = {
|
||||
@ -73,35 +73,18 @@ class PureHttp {
|
||||
return config;
|
||||
}
|
||||
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
|
||||
const whiteList = ["/refresh-token", "/login"];
|
||||
const whiteList = ["/captcha", "/login"];
|
||||
return whiteList.some(url => config.url.endsWith(url))
|
||||
? config
|
||||
: new Promise(resolve => {
|
||||
const data = getToken();
|
||||
if (data) {
|
||||
const now = new Date().getTime();
|
||||
const expired = parseInt(data.expires) - now <= 0;
|
||||
const now = new Date().getTime() / 1000;
|
||||
const expired = parseInt(data.expireAt) - now <= 0;
|
||||
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));
|
||||
} else {
|
||||
config.headers["Authorization"] = formatToken(
|
||||
data.accessToken
|
||||
);
|
||||
config.headers["Authorization"] = data.token;
|
||||
resolve(config);
|
||||
}
|
||||
} else {
|
||||
@ -167,6 +150,19 @@ class PureHttp {
|
||||
resolve(response);
|
||||
})
|
||||
.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);
|
||||
});
|
||||
});
|
||||
|
@ -8,7 +8,7 @@ class StorageProxy implements ProxyStorage {
|
||||
this.storage.config({
|
||||
// 首选IndexedDB作为第一驱动,不支持IndexedDB会自动降级到localStorage(WebSQL被弃用,详情看https://developer.chrome.com/blog/deprecating-web-sql)
|
||||
driver: [this.storage.INDEXEDDB, this.storage.LOCALSTORAGE],
|
||||
name: "pure-admin"
|
||||
name: "pure-user"
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ interface MessageParams {
|
||||
onClose?: Function | null;
|
||||
}
|
||||
|
||||
/** 用法非常简单,参考 src/views/components/message/index.vue 文件 */
|
||||
/** 用法非常简单,参考 src/views/components/message/list.vue 文件 */
|
||||
|
||||
/**
|
||||
* `Message` 消息提示函数
|
||||
|
@ -7,7 +7,7 @@ import { useNav } from "@/layout/hooks/useNav";
|
||||
import type { FormInstance } from "element-plus";
|
||||
import { useLayout } from "@/layout/hooks/useLayout";
|
||||
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 { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
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 Lock from "@iconify-icons/ri/lock-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({
|
||||
name: "Login"
|
||||
});
|
||||
const { $bus } = useGetGlobalProperties();
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const ruleFormRef = ref<FormInstance>();
|
||||
const loginRuleFormRef = ref<FormInstance>();
|
||||
|
||||
const { initStorage } = useLayout();
|
||||
initStorage();
|
||||
@ -32,30 +36,38 @@ const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
|
||||
dataThemeChange(overallStyle.value);
|
||||
const { title } = useNav();
|
||||
|
||||
const ruleForm = reactive({
|
||||
username: "admin",
|
||||
password: "admin123"
|
||||
const imgCode = ref("");
|
||||
const imgCodeId = ref("");
|
||||
const loginRuleForm = reactive({
|
||||
account: "", // 账号
|
||||
password: "", // 密码
|
||||
captchaId: "", // 验证码id
|
||||
captchaAnswer: "" // 验证码
|
||||
});
|
||||
|
||||
// 登陆接口
|
||||
const onLogin = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
loginRuleForm.captchaId = imgCodeId.value;
|
||||
useUserStoreHook()
|
||||
.loginByUsername({ username: ruleForm.username, password: "admin123" })
|
||||
.loginByUsername(loginRuleForm)
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
if (res.code === 200) {
|
||||
// 获取后端路由
|
||||
return initRouter().then(() => {
|
||||
router.push(getTopMenu(true).path).then(() => {
|
||||
message("登录成功", { type: "success" });
|
||||
});
|
||||
});
|
||||
usePermissionStoreHook().handleWholeMenus([]);
|
||||
addPathMatch();
|
||||
router.push("/welcome");
|
||||
message("登录成功", { type: "success" });
|
||||
} else {
|
||||
message("登录失败", { type: "error" });
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
$bus.emit("refreshCode", true);
|
||||
})
|
||||
.finally(() => (loading.value = false));
|
||||
} else {
|
||||
return fields;
|
||||
@ -66,7 +78,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
|
||||
/** 使用公共函数,避免`removeEventListener`失效 */
|
||||
function onkeypress({ code }: KeyboardEvent) {
|
||||
if (code === "Enter") {
|
||||
onLogin(ruleFormRef.value);
|
||||
onLogin(loginRuleFormRef.value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,28 +112,19 @@ onBeforeUnmount(() => {
|
||||
<div class="login-form">
|
||||
<avatar class="avatar" />
|
||||
<Motion>
|
||||
<h2 class="outline-none">{{ title }}</h2>
|
||||
<h2 class="outline-none">{{ title }} LOGIN</h2>
|
||||
</Motion>
|
||||
|
||||
<el-form
|
||||
ref="ruleFormRef"
|
||||
:model="ruleForm"
|
||||
ref="loginRuleFormRef"
|
||||
:model="loginRuleForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
>
|
||||
<Motion :delay="100">
|
||||
<el-form-item
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入账号',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]"
|
||||
prop="username"
|
||||
>
|
||||
<el-form-item prop="account">
|
||||
<el-input
|
||||
v-model="ruleForm.username"
|
||||
v-model="loginRuleForm.account"
|
||||
clearable
|
||||
placeholder="账号"
|
||||
:prefix-icon="useRenderIcon(User)"
|
||||
@ -132,7 +135,7 @@ onBeforeUnmount(() => {
|
||||
<Motion :delay="150">
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="ruleForm.password"
|
||||
v-model="loginRuleForm.password"
|
||||
clearable
|
||||
show-password
|
||||
placeholder="密码"
|
||||
@ -140,14 +143,29 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</el-form-item>
|
||||
</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">
|
||||
<el-button
|
||||
class="w-full mt-4"
|
||||
size="default"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@click="onLogin(ruleFormRef)"
|
||||
@click="onLogin(loginRuleFormRef)"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
|
@ -1,25 +1,36 @@
|
||||
import { reactive } from "vue";
|
||||
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>{
|
||||
account: [
|
||||
{
|
||||
required: true,
|
||||
message: "账号不能为空",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === "") {
|
||||
callback(new Error("请输入密码"));
|
||||
} else if (!REGEXP_PWD.test(value)) {
|
||||
callback(
|
||||
new Error("密码格式应为8-18位数字、字母、符号的任意两种组合")
|
||||
);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
required: true,
|
||||
message: "密码不能为空",
|
||||
trigger: "blur"
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: "密码长度最少6位",
|
||||
trigger: "blur"
|
||||
},
|
||||
{
|
||||
max: 32,
|
||||
message: "密码长度最长32位",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
captchaAnswer: [
|
||||
{
|
||||
required: true,
|
||||
message: "验证码不能为空",
|
||||
trigger: "blur"
|
||||
}
|
||||
]
|
||||
|
@ -20,7 +20,7 @@ const username = ref(useUserStoreHook()?.username);
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: "admin",
|
||||
value: "user",
|
||||
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>
|
||||
|
||||
<template>
|
||||
<h1>Pure-Admin-Thin(非国际化版本)</h1>
|
||||
<h1>Wireguard-Dashboard</h1>
|
||||
</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;
|
||||
}
|
||||
|
||||
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" {
|
||||
import Vue from "compatible-vue";
|
||||
|
@ -24,7 +24,12 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
|
||||
port: VITE_PORT,
|
||||
host: "0.0.0.0",
|
||||
// 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy
|
||||
proxy: {},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:9703",
|
||||
changeOrigin: true
|
||||
}
|
||||
},
|
||||
// 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布
|
||||
warmup: {
|
||||
clientFiles: ["./index.html", "./src/{views,components}/*"]
|
||||
|
Loading…
Reference in New Issue
Block a user