zouzs hace 1 mes
padre
commit
c9de882d07

+ 2 - 1
src/api/routes.ts

@@ -1,4 +1,5 @@
 import { http } from "@/utils/http";
+import {baseUrlApi} from "./utils";
 
 type Result = {
   success: boolean;
@@ -6,5 +7,5 @@ type Result = {
 };
 
 export const getAsyncRoutes = () => {
-  return http.request<Result>("get", "/get-async-routes");
+  return http.request<Result>("get", baseUrlApi("system/menu/getRouters/authPerm"));
 };

+ 32 - 0
src/api/system/menu.ts

@@ -0,0 +1,32 @@
+import {http} from "@/utils/http";
+import {baseUrlApi} from "../utils";
+
+export interface BasicResponseModel<T = any> {
+    code: number;
+    msg: string;
+    data: T;
+}
+
+export const getSystemMenuList = (query?: object) => {
+    return http.request<BasicResponseModel>("get", baseUrlApi("system/menu/list"), {params: query});
+};
+
+export const getSystemMenuDetailById = (id: number) => {
+    return http.request<BasicResponseModel>("get", baseUrlApi(`system/menu/${id}`));
+};
+
+export const addMenu = (data?: object) => {
+    return http.post<BasicResponseModel, any>(baseUrlApi(`system/menu`), {data});
+};
+
+export const updateMenu = (data?: object) => {
+    return http.put<BasicResponseModel, any>(baseUrlApi(`system/menu`), {data});
+}
+
+export const deleteMenu = (id: number) => {
+    return http.request<BasicResponseModel>("delete", baseUrlApi(`system/menu/${id}`));
+}
+
+export const getSystemList = () => {
+    return http.request<BasicResponseModel>("get", baseUrlApi(`system/businessSystem/list`));
+}

+ 55 - 67
src/api/user.ts

@@ -1,106 +1,94 @@
-import { http } from "@/utils/http";
-import { baseUrlApi } from "./utils";
+import {http} from "@/utils/http";
+import {baseUrlApi} from "./utils";
 
 export interface BasicResponseModel<T = any> {
-  code: number;
-  msg: string;
-  data: T;
+    code: number;
+    msg: string;
+    data: T;
 }
 
 export type RefreshTokenResult = {
-  success: boolean;
-  data: {
-    /** `token` */
-    accessToken: string;
-    /** 用于调用刷新`accessToken`的接口时所需的`token` */
-    refreshToken: string;
-    /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
-    expires: Date;
-  };
+    success: boolean;
+    data: {
+        /** `token` */
+        accessToken: string;
+        /** 用于调用刷新`accessToken`的接口时所需的`token` */
+        refreshToken: string;
+        /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
+        expires: Date;
+    };
 };
 
 export type LoginNameResult = {
-  code: number;
-  msg: string;
-  data: boolean;
+    code: number;
+    msg: string;
+    data: boolean;
 };
 
 /** 登录姓名验证 */
 export const checkLoginNameApi = (data?: string) => {
-  return http.request<LoginNameResult>(
-    "post",
-    baseUrlApi(`adm/account/checkLoginName/${data}`)
-  );
+    return http.request<LoginNameResult>(
+        "post",
+        baseUrlApi(`adm/account/checkLoginName/${data}`)
+    );
 };
 
 export type VerificationCodeResult = {
-  code: number;
-  msg: string;
-  data: {
-    /** 验证码 */
-    verification: string;
-    /** 验证码对应的`token`,用于登录时传入后台做验证 */
-    imgToken: string;
-  };
+    code: number;
+    msg: string;
+    captchaEnabled: boolean;
+    uuid: string;
+    img: string;
 };
 
 /** 获取验证码 */
 export const getVerificationCode = (data?: object) => {
-  return http.request<VerificationCodeResult>(
-    "post",
-    baseUrlApi("adm/account/verificationCode"),
-    { data }
-  );
+    return http.request<VerificationCodeResult>(
+        "get",
+        baseUrlApi("code"),
+        {data}
+    );
 };
 
 /** 获取短信验证码 */
 export const getVerifySmsApi = (data?: object) => {
-  return http.request<BasicResponseModel>(
-    "post",
-    baseUrlApi("adm/account/sendVerifySms"),
-    { data }
-  );
+    return http.request<BasicResponseModel>(
+        "post",
+        baseUrlApi("adm/account/sendVerifySms"),
+        {data}
+    );
 };
 
 export type UserResult = {
-  code: number;
-  msg: string;
-  data: {
-    admUserId: number;
-    admUserName: string;
-    id: number;
-    loginName: string;
-    operationRole: number;
-    roleId: number;
-    roleName: string;
-    token: string;
-    userMenuTree: object;
-    userModule: string;
-    _businessDockingId: number;
-  };
+    code: number;
+    msg: string;
+    data: {
+        access_token: string,
+        expires_in: number
+    };
 };
 
 /** 登录 */
 export const getLogin = (data?: object) => {
-  return http.request<UserResult>(
-    "post",
-    baseUrlApi("adm/account/adminLogin/V2"),
-    { data }
-  );
+    return http.request<UserResult>(
+        "post",
+        baseUrlApi("auth/login"),
+        {data}
+    );
 };
 
 /** 登录 */
 export const verifySmsLogin = (data?: object) => {
-  return http.request<UserResult>(
-    "post",
-    baseUrlApi("adm/account/verifySmsLogin"),
-    { data }
-  );
+    return http.request<UserResult>(
+        "post",
+        baseUrlApi("adm/account/verifySmsLogin"),
+        {data}
+    );
 };
 
 /** 刷新`token` */
 export const refreshTokenApi = (data?: object) => {
-  return http.request<RefreshTokenResult>("post", baseUrlApi("refresh-token"), {
-    data
-  });
+    return http.request<RefreshTokenResult>("post", baseUrlApi("refresh-token"), {
+        data
+    });
 };

+ 4 - 4
src/router/index.ts

@@ -157,10 +157,10 @@ router.beforeEach((to: ToRouteType, _from, next) => {
         usePermissionStoreHook().wholeMenus.length === 0 &&
         to.path !== "/login"
       ) {
-        // initRouter().then((router: Router) => {
-        // 使用下面方法替换initRouter
+        initRouter().then((router: Router) => {
+        /*// 使用下面方法替换initRouter
         usePermissionStoreHook().handleWholeMenus([]);
-        addPathMatch();
+        addPathMatch();*/
         if (!useMultiTagsStoreHook().getMultiTagsCache) {
           const { path } = to;
           const route = findRouteByPath(
@@ -190,7 +190,7 @@ router.beforeEach((to: ToRouteType, _from, next) => {
         }
         // 确保动态路由完全加入路由列表并且不影响静态路由(注意:动态路由刷新时router.beforeEach可能会触发两次,第一次触发动态路由还未完全添加,第二次动态路由才完全添加到路由列表,如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断,这样就只会触发一次)
         if (isAllEmpty(to.name)) router.push(to.fullPath);
-        // });
+        });
       }
       toCorrectRoute();
     }

+ 3 - 3
src/store/modules/user.ts

@@ -69,9 +69,9 @@ export const useUserStore = defineStore("pure-user", {
       return new Promise<UserResult>((resolve, reject) => {
         const loginfn = submitType === 1 ? getLogin : verifySmsLogin;
         loginfn(data)
-          .then(data => {
-            if (data?.code === 0) setToken(data.data);
-            resolve(data);
+          .then(res => {
+            if (res?.code === 200) setToken(res.data);
+            resolve(res);
           })
           .catch(error => {
             reject(error);

+ 115 - 112
src/utils/auth.ts

@@ -1,40 +1,43 @@
 import Cookies from "js-cookie";
-import { useUserStoreHook } from "@/store/modules/user";
-import { storageLocal, isString, isIncludeAllChildren } from "@pureadmin/utils";
+import {useUserStoreHook} from "@/store/modules/user";
+import {storageLocal, isString, isIncludeAllChildren} from "@pureadmin/utils";
 
 export interface DataInfo<T> {
-  /** token */
-  accessToken: string;
-  /** `accessToken`的过期时间(时间戳) */
-  expires: T;
-  /** 用于调用刷新accessToken的接口时所需的token */
-  refreshToken: string;
-  /** 头像 */
-  avatar?: string;
-  /** 用户名 */
-  username?: string;
-  /** 昵称 */
-  nickname?: string;
-  /** 当前登录用户的角色 */
-  roles?: Array<string>;
-  /** 当前登录用户的按钮级别权限 */
-  permissions?: Array<string>;
+    /** token */
+    accessToken: string;
+    /** `accessToken`的过期时间(时间戳) */
+    expires: T;
+    /** 用于调用刷新accessToken的接口时所需的token */
+    refreshToken: string;
+    /** 头像 */
+    avatar?: string;
+    /** 用户名 */
+    username?: string;
+    /** 昵称 */
+    nickname?: string;
+    /** 当前登录用户的角色 */
+    roles?: Array<string>;
+    /** 当前登录用户的按钮级别权限 */
+    permissions?: Array<string>;
 
-  admUserId: number;
-  admUserName: string;
-  id: number;
-  loginName: string;
-  operationRole: number;
-  roleId: number;
-  roleName: string;
-  token: string;
-  userMenuTree: object;
-  userModule: string;
-  _businessDockingId: number;
+    admUserId: number;
+    admUserName: string;
+    id: number;
+    loginName: string;
+    operationRole: number;
+    roleId: number;
+    roleName: string;
+    token: string;
+    userMenuTree: object;
+    userModule: string;
+    _businessDockingId: number;
+
+    access_token: string;
+    expires_in: number;
 }
 
 export const userKey = "user-info";
-export const TokenKey = "authorized-token";
+export const TokenKey = "Admin-Token";
 /**
  * 通过`multiple-tabs`是否在`cookie`中,判断用户是否已经登录系统,
  * 从而支持多标签页打开已经登录的系统后无需再登录。
@@ -45,10 +48,10 @@ export const multipleTabsKey = "multiple-tabs";
 
 /** 获取`token` */
 export function getToken(): DataInfo<number> {
-  // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
-  return Cookies.get(TokenKey)
-    ? JSON.parse(Cookies.get(TokenKey))
-    : storageLocal().getItem(userKey);
+    // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
+    return Cookies.get(TokenKey)
+        ? JSON.parse(Cookies.get(TokenKey))
+        : storageLocal().getItem(userKey);
 }
 
 /**
@@ -58,98 +61,98 @@ export function getToken(): DataInfo<number> {
  * 将`avatar`、`username`、`nickname`、`roles`、`permissions`、`refreshToken`、`expires`这七条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁)
  */
 export function setToken(data: DataInfo<number>) {
-  const expires = 0;
-  // const { accessToken, refreshToken } = data;
-  const { token } = data;
-  const { isRemembered, loginDay } = useUserStoreHook();
-  // expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可
-  // const cookieString = JSON.stringify({ token, expires, refreshToken });
-  const cookieString = JSON.stringify({ token, expires });
+    const expires = 0;
+    // const { accessToken, refreshToken } = data;
+    const {access_token, expires_in} = data;
+    const {isRemembered, loginDay} = useUserStoreHook();
+    // expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可
+    // const cookieString = JSON.stringify({ token, expires, refreshToken });
+    const cookieString = JSON.stringify({access_token, expires_in});
 
-  expires > 0
-    ? Cookies.set(TokenKey, cookieString, {
-        expires: (expires - Date.now()) / 86400000
-      })
-    : Cookies.set(TokenKey, cookieString);
+    expires_in > 0
+        ? Cookies.set(TokenKey, cookieString, {
+            expires: expires_in
+        })
+        : Cookies.set(TokenKey, cookieString);
 
-  Cookies.set(
-    multipleTabsKey,
-    "true",
-    isRemembered
-      ? {
-          expires: loginDay
-        }
-      : {}
-  );
+    Cookies.set(
+        multipleTabsKey,
+        "true",
+        isRemembered
+            ? {
+                expires: loginDay
+            }
+            : {}
+    );
 
-  function setUserKey({ avatar, username, nickname, roles, permissions }) {
-    useUserStoreHook().SET_AVATAR(avatar);
-    useUserStoreHook().SET_USERNAME(username);
-    useUserStoreHook().SET_NICKNAME(nickname);
-    useUserStoreHook().SET_ROLES(roles);
-    useUserStoreHook().SET_PERMS(permissions);
-    storageLocal().setItem(userKey, {
-      // refreshToken,
-      expires,
-      avatar,
-      username,
-      nickname,
-      roles,
-      permissions
-    });
-  }
+    function setUserKey({avatar, username, nickname, roles, permissions}) {
+        useUserStoreHook().SET_AVATAR(avatar);
+        useUserStoreHook().SET_USERNAME(username);
+        useUserStoreHook().SET_NICKNAME(nickname);
+        useUserStoreHook().SET_ROLES(roles);
+        useUserStoreHook().SET_PERMS(permissions);
+        storageLocal().setItem(userKey, {
+            // refreshToken,
+            expires,
+            avatar,
+            username,
+            nickname,
+            roles,
+            permissions
+        });
+    }
 
-  if (data.username && data.roles) {
-    const { username, roles } = data;
-    setUserKey({
-      avatar: data?.avatar ?? "",
-      username,
-      nickname: data?.nickname ?? "",
-      roles,
-      permissions: data?.permissions ?? []
-    });
-  } 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 ?? [];
-    const permissions =
-      storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [];
-    setUserKey({
-      avatar,
-      username,
-      nickname,
-      roles,
-      permissions
-    });
-  }
+    if (data.username && data.roles) {
+        const {username, roles} = data;
+        setUserKey({
+            avatar: data?.avatar ?? "",
+            username,
+            nickname: data?.nickname ?? "",
+            roles,
+            permissions: data?.permissions ?? []
+        });
+    } 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 ?? [];
+        const permissions =
+            storageLocal().getItem<DataInfo<number>>(userKey)?.permissions ?? [];
+        setUserKey({
+            avatar,
+            username,
+            nickname,
+            roles,
+            permissions
+        });
+    }
 }
 
 /** 删除`token`以及key值为`user-info`的localStorage信息 */
 export function removeToken() {
-  Cookies.remove(TokenKey);
-  Cookies.remove(multipleTabsKey);
-  storageLocal().removeItem(userKey);
+    Cookies.remove(TokenKey);
+    Cookies.remove(multipleTabsKey);
+    storageLocal().removeItem(userKey);
 }
 
 /** 格式化token(jwt格式) */
 export const formatToken = (token: string): string => {
-  return token;
+    return "Bearer " + token;
 };
 
 /** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/
 export const hasPerms = (value: string | Array<string>): boolean => {
-  if (!value) return false;
-  const allPerms = "*:*:*";
-  const { permissions } = useUserStoreHook();
-  if (!permissions) return false;
-  if (permissions.length === 1 && permissions[0] === allPerms) return true;
-  const isAuths = isString(value)
-    ? permissions.includes(value)
-    : isIncludeAllChildren(value, permissions);
-  return isAuths ? true : false;
+    if (!value) return false;
+    const allPerms = "*:*:*";
+    const {permissions} = useUserStoreHook();
+    if (!permissions) return false;
+    if (permissions.length === 1 && permissions[0] === allPerms) return true;
+    const isAuths = isString(value)
+        ? permissions.includes(value)
+        : isIncludeAllChildren(value, permissions);
+    return isAuths ? true : false;
 };

+ 216 - 206
src/utils/http/index.ts

@@ -1,223 +1,233 @@
 import Axios, {
-  type AxiosInstance,
-  type AxiosRequestConfig,
-  type CustomParamsSerializer
+    type AxiosInstance,
+    type AxiosRequestConfig,
+    type CustomParamsSerializer
 } from "axios";
 import type {
-  PureHttpError,
-  RequestMethods,
-  PureHttpResponse,
-  PureHttpRequestConfig,
-  FileHttpRequestConfig
+    PureHttpError,
+    RequestMethods,
+    PureHttpResponse,
+    PureHttpRequestConfig,
+    FileHttpRequestConfig
 } from "./types.d";
-import { stringify } from "qs";
+import {stringify} from "qs";
 import NProgress from "../progress";
-import { getToken, formatToken } from "@/utils/auth";
-import { useUserStoreHook } from "@/store/modules/user";
+import {getToken, formatToken} from "@/utils/auth";
+import {useUserStoreHook} from "@/store/modules/user";
 
 // 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
 const defaultConfig: AxiosRequestConfig = {
-  // 请求超时时间
-  timeout: 10000,
-  headers: {
-    Accept: "application/json, text/plain, */*",
-    "Content-Type": "application/json",
-    "X-Requested-With": "XMLHttpRequest"
-  },
-  // 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
-  paramsSerializer: {
-    serialize: stringify as unknown as CustomParamsSerializer
-  }
+    // 请求超时时间
+    timeout: 10000,
+    headers: {
+        Accept: "application/json, text/plain, */*",
+        "Content-Type": "application/json",
+        "X-Requested-With": "XMLHttpRequest"
+    },
+    // 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
+    paramsSerializer: {
+        serialize: stringify as unknown as CustomParamsSerializer
+    }
 };
 
 class PureHttp {
-  constructor() {
-    this.httpInterceptorsRequest();
-    this.httpInterceptorsResponse();
-  }
-
-  /** `token`过期后,暂存待执行的请求 */
-  private static requests = [];
-
-  /** 防止重复刷新`token` */
-  private static isRefreshing = false;
-
-  /** 初始化配置对象 */
-  private static initConfig: PureHttpRequestConfig = {};
-
-  /** 保存当前`Axios`实例对象 */
-  private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
-
-  /** 重连原始请求 */
-  private static retryOriginalRequest(config: PureHttpRequestConfig) {
-    return new Promise(resolve => {
-      PureHttp.requests.push((token: string) => {
-        config.headers["Authorization"] = formatToken(token);
-        resolve(config);
-      });
-    });
-  }
-
-  /** 请求拦截 */
-  private httpInterceptorsRequest(): void {
-    PureHttp.axiosInstance.interceptors.request.use(
-      async (config: PureHttpRequestConfig): Promise<any> => {
-        // 开启进度条动画
-        NProgress.start();
-        // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
-        if (typeof config.beforeRequestCallback === "function") {
-          config.beforeRequestCallback(config);
-          return config;
-        }
-        if (PureHttp.initConfig.beforeRequestCallback) {
-          PureHttp.initConfig.beforeRequestCallback(config);
-          return config;
-        }
-        /** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
-        const whiteList = ["/refresh-token", "/login"];
-        return whiteList.some(url => config.url.endsWith(url))
-          ? config
-          : new Promise(resolve => {
-              const data = getToken();
-              if (data) {
-                  console.log("请求拦截器拿到的token",data)
-                const now = new Date().getTime();
-                const expired = parseInt(data.expires) - 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;
-                      });
-                  }*/
-                    config.headers["Token"] = formatToken(
-                        data.token
-                    );
-                  resolve(config);
-                } else {
-                  config.headers["Token"] = formatToken(
-                    data.token
-                  );
-                  resolve(config);
-                }
-              } else {
+    constructor() {
+        this.httpInterceptorsRequest();
+        this.httpInterceptorsResponse();
+    }
+
+    /** `token`过期后,暂存待执行的请求 */
+    private static requests = [];
+
+    /** 防止重复刷新`token` */
+    private static isRefreshing = false;
+
+    /** 初始化配置对象 */
+    private static initConfig: PureHttpRequestConfig = {};
+
+    /** 保存当前`Axios`实例对象 */
+    private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
+
+    /** 重连原始请求 */
+    private static retryOriginalRequest(config: PureHttpRequestConfig) {
+        return new Promise(resolve => {
+            PureHttp.requests.push((token: string) => {
+                config.headers["Authorization"] = formatToken(token);
                 resolve(config);
-              }
             });
-      },
-      error => {
-        return Promise.reject(error);
-      }
-    );
-  }
-
-  /** 响应拦截 */
-  private httpInterceptorsResponse(): void {
-    const instance = PureHttp.axiosInstance;
-    instance.interceptors.response.use(
-      (response: PureHttpResponse) => {
-        const $config = response.config;
-        // 关闭进度条动画
-        NProgress.done();
-        // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
-        if (typeof $config.beforeResponseCallback === "function") {
-          $config.beforeResponseCallback(response);
-          return response.data;
-        }
-        if (PureHttp.initConfig.beforeResponseCallback) {
-          PureHttp.initConfig.beforeResponseCallback(response);
-          return response.data;
-        }
-        console.log('111111', response.data);
-        if (response.data.code === -10) {
-            console.log('用户未登录')
-            // 用户未登录
-            useUserStoreHook().logOut();
-            return Promise.reject(new Error("用户未登录"));
-        }
-        return response.data;
-      },
-      (error: PureHttpError) => {
-        const $error = error;
-        $error.isCancelRequest = Axios.isCancel($error);
-        // 关闭进度条动画
-        NProgress.done();
-        // 所有的响应异常 区分来源为取消请求/非取消请求
-        return Promise.reject($error);
-      }
-    );
-  }
-
-  /** 通用请求工具函数 */
-  public request<T>(
-    method: RequestMethods,
-    url: string,
-    param?: AxiosRequestConfig,
-    axiosConfig?: PureHttpRequestConfig
-  ): Promise<T> {
-    const config = {
-      method,
-      url,
-      ...param,
-      ...axiosConfig
-    } as PureHttpRequestConfig;
-
-    // 单独处理自定义请求/响应回调
-    return new Promise((resolve, reject) => {
-      PureHttp.axiosInstance
-        .request(config)
-        .then((response: undefined) => {
-          resolve(response);
-        })
-        .catch(error => {
-          reject(error);
         });
-    });
-  }
-
-  /** 单独抽离的`post`工具函数 */
-  public post<T, P>(
-    url: string,
-    params?: AxiosRequestConfig<P>,
-    config?: PureHttpRequestConfig
-  ): Promise<T> {
-    return this.request<T>("post", url, params, config);
-  }
-
-  /** 单独抽离的`get`工具函数 */
-  public get<T, P>(
-    url: string,
-    params?: AxiosRequestConfig<P>,
-    config?: PureHttpRequestConfig
-  ): Promise<T> {
-    return this.request<T>("get", url, params, config);
-  }
-
-  /* ----------------------------------- 导出封装 ----------------------------------- */
-
-  public postExport<T, P>(
-    url: string,
-    params?: AxiosRequestConfig<P>,
-    config?: FileHttpRequestConfig
-  ): Promise<T> {
-    const exportConfig = {
-      responseType: "blob",
-      ...params,
-      ...config
-    } as PureHttpRequestConfig;
-
-    return this.request<T>("post", url, exportConfig);
-  }
-  /* ------------------------------------------------------------------------------- */
+    }
+
+    /** 请求拦截 */
+    private httpInterceptorsRequest(): void {
+        PureHttp.axiosInstance.interceptors.request.use(
+            async (config: PureHttpRequestConfig): Promise<any> => {
+                // 开启进度条动画
+                NProgress.start();
+                // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
+                if (typeof config.beforeRequestCallback === "function") {
+                    config.beforeRequestCallback(config);
+                    return config;
+                }
+                if (PureHttp.initConfig.beforeRequestCallback) {
+                    PureHttp.initConfig.beforeRequestCallback(config);
+                    return config;
+                }
+                /** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
+                const whiteList = ["/refresh-token", "/login"];
+                return whiteList.some(url => config.url.endsWith(url))
+                    ? config
+                    : new Promise(resolve => {
+                        const data = getToken();
+                        if (data) {
+                            console.log("请求拦截器拿到的token", data)
+                            const now = new Date().getTime();
+                            const expired = parseInt(data.expires_in) - 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;
+                                    });
+                                }*/
+                                config.headers["authorization"] = formatToken(
+                                    data.access_token
+                                );
+                                resolve(config);
+                            } else {
+                                config.headers["authorization"] = formatToken(
+                                    data.access_token
+                                );
+                                resolve(config);
+                            }
+                        } else {
+                            resolve(config);
+                        }
+                    });
+            },
+            error => {
+                return Promise.reject(error);
+            }
+        );
+    }
+
+    /** 响应拦截 */
+    private httpInterceptorsResponse(): void {
+        const instance = PureHttp.axiosInstance;
+        instance.interceptors.response.use(
+            (response: PureHttpResponse) => {
+                const $config = response.config;
+                // 关闭进度条动画
+                NProgress.done();
+                // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
+                if (typeof $config.beforeResponseCallback === "function") {
+                    $config.beforeResponseCallback(response);
+                    return response.data;
+                }
+                if (PureHttp.initConfig.beforeResponseCallback) {
+                    PureHttp.initConfig.beforeResponseCallback(response);
+                    return response.data;
+                }
+                console.log('111111', response.data);
+                if (response.data.code === -10) {
+                    console.log('用户未登录')
+                    // 用户未登录
+                    useUserStoreHook().logOut();
+                    return Promise.reject(new Error("用户未登录"));
+                }
+                return response.data;
+            },
+            (error: PureHttpError) => {
+                const $error = error;
+                $error.isCancelRequest = Axios.isCancel($error);
+                // 关闭进度条动画
+                NProgress.done();
+                // 所有的响应异常 区分来源为取消请求/非取消请求
+                return Promise.reject($error);
+            }
+        );
+    }
+
+    /** 通用请求工具函数 */
+    public request<T>(
+        method: RequestMethods,
+        url: string,
+        param?: AxiosRequestConfig,
+        axiosConfig?: PureHttpRequestConfig
+    ): Promise<T> {
+        const config = {
+            method,
+            url,
+            ...param,
+            ...axiosConfig
+        } as PureHttpRequestConfig;
+
+        // 单独处理自定义请求/响应回调
+        return new Promise((resolve, reject) => {
+            PureHttp.axiosInstance
+                .request(config)
+                .then((response: undefined) => {
+                    resolve(response);
+                })
+                .catch(error => {
+                    reject(error);
+                });
+        });
+    }
+
+    /** 单独抽离的`post`工具函数 */
+    public post<T, P>(
+        url: string,
+        params?: AxiosRequestConfig<P>,
+        config?: PureHttpRequestConfig
+    ): Promise<T> {
+        return this.request<T>("post", url, params, config);
+    }
+
+    /** 单独抽离的`get`工具函数 */
+    public get<T, P>(
+        url: string,
+        params?: AxiosRequestConfig<P>,
+        config?: PureHttpRequestConfig
+    ): Promise<T> {
+        return this.request<T>("get", url, params, config);
+    }
+
+    /** 单独抽离的`put`工具函数 */
+    public put<T, P>(
+        url: string,
+        params?: AxiosRequestConfig<P>,
+        config?: PureHttpRequestConfig
+    ): Promise<T> {
+        return this.request<T>("put", url, params, config);
+    }
+
+    /* ----------------------------------- 导出封装 ----------------------------------- */
+
+    public postExport<T, P>(
+        url: string,
+        params?: AxiosRequestConfig<P>,
+        config?: FileHttpRequestConfig
+    ): Promise<T> {
+        const exportConfig = {
+            responseType: "blob",
+            ...params,
+            ...config
+        } as PureHttpRequestConfig;
+
+        return this.request<T>("post", url, exportConfig);
+    }
+
+    /* ------------------------------------------------------------------------------- */
 }
 
 export const http = new PureHttp();

+ 199 - 198
src/views/login/index.vue

@@ -1,21 +1,21 @@
 <script setup lang="ts">
 // @ts-check
 import Motion from "./utils/motion";
-import { useRouter, useRoute } from "vue-router";
-import { message } from "@/utils/message";
-import { loginRules, smsRules } from "./utils/rule";
-import { ref, reactive, toRaw, onMounted } from "vue";
-import { debounce } from "@pureadmin/utils";
-import { useNav } from "@/layout/hooks/useNav";
-import { useEventListener } from "@vueuse/core";
-import type { FormInstance } from "element-plus";
-import { useLayout } from "@/layout/hooks/useLayout";
-import { useUserStoreHook } from "@/store/modules/user";
-import { getTopMenu, addPathMatch } from "@/router/utils";
-import { bg, avatar, illustration } from "./utils/static";
-import { useRenderIcon } from "@/components/ReIcon/src/hooks";
-import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
-import { ElMessage } from "element-plus";
+import {useRoute, useRouter} from "vue-router";
+import {message} from "@/utils/message";
+import {loginRules, smsRules} from "./utils/rule";
+import {onMounted, reactive, ref, toRaw} from "vue";
+import {debounce} from "@pureadmin/utils";
+import {useNav} from "@/layout/hooks/useNav";
+import {useEventListener} from "@vueuse/core";
+import type {FormInstance} from "element-plus";
+import {ElMessage} from "element-plus";
+import {useLayout} from "@/layout/hooks/useLayout";
+import {useUserStoreHook} from "@/store/modules/user";
+import {addPathMatch, getTopMenu, initRouter} from "@/router/utils";
+import {avatar, bg, illustration} from "./utils/static";
+import {useRenderIcon} from "@/components/ReIcon/src/hooks";
+import {useDataThemeChange} from "@/layout/hooks/useDataThemeChange";
 
 import dayIcon from "@/assets/svg/day.svg?component";
 import darkIcon from "@/assets/svg/dark.svg?component";
@@ -23,10 +23,11 @@ import Lock from "~icons/ri/lock-fill";
 import User from "~icons/ri/user-3-fill";
 import MaterialSymbolsLightDomainVerificationRounded from "~icons/material-symbols-light/domain-verification-rounded";
 import EpIphone from "~icons/ep/iphone";
-import { getVerificationCode, getVerifySmsApi } from "@/api/user";
-import { usePermissionStoreHook } from "@/store/modules/permission";
-import { AesEncode } from "@/utils/crypto";
-import { md5 } from "js-md5";
+import {getVerificationCode, getVerifySmsApi} from "@/api/user";
+import {usePermissionStoreHook} from "@/store/modules/permission";
+import {AesEncode} from "@/utils/crypto";
+import {md5} from "js-md5";
+import {rule} from "postcss";
 
 defineOptions({
   name: "Login"
@@ -43,34 +44,35 @@ const smsFormRef = ref<FormInstance>();
 const sendVerifySmsCodeText = ref<string>("点击发送验证码");
 const sendInterval = ref();
 const countdown = ref<number>(60);
+const imgSrc = ref<string>(null);
 
-const { initStorage } = useLayout();
+const {initStorage} = useLayout();
 initStorage();
 
-const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
+const {dataTheme, overallStyle, dataThemeChange} = useDataThemeChange();
 dataThemeChange(overallStyle.value);
-const { title } = useNav();
+const {title} = useNav();
 
 const ruleForm = reactive({
-  loginName: "zouzishun",
-  loginPwd: "zZs19970821/",
-  verification: "",
-  imgToken: "",
-  agree: true
+  username: "admin",
+  password: "admin123",
+  code: "",
+  uuid: "",
 });
 
 const smsRuleForm = reactive({
   verifySmsCode: ""
 });
 
-const imgToken = ref("");
+const uuid = ref("");
 
 const getVerifyCode = async () => {
   try {
     let res = await getVerificationCode();
-    if (res.code === 0) {
-      ruleForm.verification = res.data.verification;
-      imgToken.value = res.data.imgToken;
+    if (res.code === 200) {
+      ruleForm.code = "";
+      imgSrc.value = `data:image/png;base64,${res.img}`;
+      ruleForm.uuid = res.uuid;
     }
   } catch (e) {
     console.log(e);
@@ -82,30 +84,30 @@ const getVerifySmsCode = () => {
   if (!canSendSecond.value) return;
   canSendSecond.value = false;
   getVerifySmsApi({
-    loginName: AesEncode(ruleForm.loginName),
-    loginPwd: AesEncode(md5(ruleForm.loginPwd).toUpperCase())
+    username: ruleForm.username,
+    password: ruleForm.password
   })
-    .then(() => {
-      sendInterval.value = setInterval(function () {
-        if (countdown.value === 0) {
-          clearInterval(sendInterval.value);
-          sendVerifySmsCodeText.value = "点击发送验证码";
-          canSendSecond.value = true;
-          countdown.value = 60;
-          sendInterval.value = null;
-        } else {
-          countdown.value = countdown.value - 1;
-          sendVerifySmsCodeText.value = `${countdown.value}秒后重试`;
-        }
-      }, 1000);
-    })
-    .catch(() => {
-      canSendSecond.value = true;
-    });
+      .then(() => {
+        sendInterval.value = setInterval(function () {
+          if (countdown.value === 0) {
+            clearInterval(sendInterval.value);
+            sendVerifySmsCodeText.value = "点击发送验证码";
+            canSendSecond.value = true;
+            countdown.value = 60;
+            sendInterval.value = null;
+          } else {
+            countdown.value = countdown.value - 1;
+            sendVerifySmsCodeText.value = `${countdown.value}秒后重试`;
+          }
+        }, 1000);
+      })
+      .catch(() => {
+        canSendSecond.value = true;
+      });
 };
 
 const onLogin = async (
-  formEl: FormInstance | undefined,
+    formEl: FormInstance | undefined,
 ) => {
   if (!formEl) return;
   const submitType = showVerifySms.value ? 2 : 1;
@@ -115,111 +117,110 @@ const onLogin = async (
     if (valid) {
       loading.value = true;
       let params = {
-        loginName: AesEncode(ruleForm.loginName),
-        loginPwd: AesEncode(md5(ruleForm.loginPwd).toUpperCase()),
-        verification:
-          submitType === 1
-            ? AesEncode(ruleForm.verification)
-            : AesEncode(smsRuleForm.verifySmsCode),
-        imgToken: submitType === 1 ? AesEncode(imgToken.value) : undefined
+        ...ruleForm
       };
       useUserStoreHook()
-        .loginByUsername(params, submitType)
-        .then(res => {
-          let { code, msg } = res;
-          if (code === 0 || code === 3) {
-            // 存储输入信息
-            /*const { storageExpires } = commonSetting;
-            if (formData.agree) {
-              storage.setCookie(
-                "loginRemember",
-                "1",
-                storageExpires * 24 * 60 * 60
-              );
-              storage.setCookie(
-                "loginRememberName",
-                username,
-                storageExpires * 24 * 60 * 60
-              );
-              storage.setCookie(
-                "loginRememberPwd",
-                loginPwd,
-                storageExpires * 24 * 60 * 60
-              );
-            } else {
-              storage.setCookie(
-                "loginRemember",
-                "0",
-                storageExpires * 24 * 60 * 60
-              );
-            }*/
-            message("登录成功,即将进入系统", {
-              type: "success",
-              duration: 1000,
-              onClose() {
-                if (code === 3) {
-                  ElMessage.warning(
-                    "为了保障您的账号信息安全,请修改登录密码,需包含8位数字、字母、特殊字符"
-                  );
-                  // 去修改密码页
-                  router.replace("/system/pwd_reset");
-                } else {
-                  const toPath = decodeURIComponent(
-                    (route.query?.redirect || "/") as string
-                  );
-                  console.log(route.name);
-                  if (route.name === "Login") {
-                    /*initRouter().then(() => {
-                      router.push(getTopMenu(true).path);
-                    });*/
-                    // 全部采取静态路由模式
-                    usePermissionStoreHook().handleWholeMenus([]);
-                    addPathMatch();
-                    router.push(getTopMenu(true).path);
-                  } else router.replace(toPath);
+          .loginByUsername(params, submitType)
+          .then(res => {
+            console.log(res);
+            let {code, msg} = res;
+            if (code === 200 || code === 3) {
+              // 存储输入信息
+              /*const { storageExpires } = commonSetting;
+              if (formData.agree) {
+                storage.setCookie(
+                  "loginRemember",
+                  "1",
+                  storageExpires * 24 * 60 * 60
+                );
+                storage.setCookie(
+                  "loginRememberName",
+                  username,
+                  storageExpires * 24 * 60 * 60
+                );
+                storage.setCookie(
+                  "loginRememberPwd",
+                  password,
+                  storageExpires * 24 * 60 * 60
+                );
+              } else {
+                storage.setCookie(
+                  "loginRemember",
+                  "0",
+                  storageExpires * 24 * 60 * 60
+                );
+              }*/
+              message("登录成功,即将进入系统", {
+                type: "success",
+                duration: 1000,
+                onClose() {
+                  if (code === 3) {
+                    ElMessage.warning(
+                        "为了保障您的账号信息安全,请修改登录密码,需包含8位数字、字母、特殊字符"
+                    );
+                    // 去修改密码页
+                    router.replace("/system/pwd_reset");
+                  } else {
+                    const toPath = decodeURIComponent(
+                        (route.query?.redirect || "/") as string
+                    );
+                    console.log(route.name);
+                    if (route.name === "Login") {
+                      initRouter().then(() => {
+                        router.push(getTopMenu(true).path);
+                      });
+                      // 全部采取静态路由模式
+                      /*usePermissionStoreHook().handleWholeMenus([]);
+                      addPathMatch();
+                      router.push(getTopMenu(true).path);*/
+                    } else router.replace(toPath);
+                  }
                 }
-              }
-            });
-            return;
-          }
+              });
+              return;
+            }
 
-          if (submitType === 1) {
-            // 普通登录
-            if (
-              msg === "短信锁已过期" ||
-              msg === "不是常用IP登录" ||
-              msg === "上次登录时间超过7天" ||
-              msg === "错误次数超过3次"
-            ) {
-              showVerifySms.value = true;
-              canSendSecond.value = true;
+            if (submitType === 1) {
+              // 普通登录
+              if (
+                  msg === "短信锁已过期" ||
+                  msg === "不是常用IP登录" ||
+                  msg === "上次登录时间超过7天" ||
+                  msg === "错误次数超过3次"
+              ) {
+                showVerifySms.value = true;
+                canSendSecond.value = true;
+              } else {
+                ElMessage.warning(msg);
+                getVerifyCode();
+              }
             } else {
+              // sms登录
               ElMessage.warning(msg);
               getVerifyCode();
+              showVerifySms.value = false;
             }
-          } else {
-            // sms登录
-            ElMessage.warning(msg);
+          })
+          .catch((e) => {
+            ElMessage.error('登录失败');
             getVerifyCode();
-            showVerifySms.value = false;
-          }
-        })
-        .finally(() => (loading.value = false));
+          })
+          .finally(() => (loading.value = false));
     }
   });
 };
 
 const immediateDebounce: any = debounce(
-  formRef => onLogin(formRef),
-  1000,
-  true
+    formRef => onLogin(formRef),
+    1000,
+    true
 );
 
-useEventListener(document, "keydown", ({ code }) => {
+useEventListener(document, "keydown", ({code}) => {
   if (
-    ["Enter", "NumpadEnter"].includes(code) &&
-    !disabled.value &&
-    !loading.value
+      ["Enter", "NumpadEnter"].includes(code) &&
+      !disabled.value &&
+      !loading.value
   )
     immediateDebounce(ruleFormRef.value);
 });
@@ -231,66 +232,66 @@ onMounted(() => {
 
 <template>
   <div class="select-none">
-    <img :src="bg" class="wave" />
+    <img :src="bg" class="wave"/>
     <div class="flex-c absolute right-5 top-3">
       <!-- 主题 -->
       <el-switch
-        v-model="dataTheme"
-        inline-prompt
-        :active-icon="dayIcon"
-        :inactive-icon="darkIcon"
-        @change="dataThemeChange"
+          v-model="dataTheme"
+          inline-prompt
+          :active-icon="dayIcon"
+          :inactive-icon="darkIcon"
+          @change="dataThemeChange"
       />
     </div>
     <div class="login-container">
       <div class="img">
-        <component :is="toRaw(illustration)" />
+        <component :is="toRaw(illustration)"/>
       </div>
       <div class="login-box">
         <div class="login-form">
-          <avatar class="avatar" />
+          <avatar class="avatar"/>
           <Motion>
             <h2 class="outline-hidden">{{ title }}</h2>
           </Motion>
 
           <el-form
-            v-show="!showVerifySms"
-            ref="ruleFormRef"
-            :model="ruleForm"
-            :rules="loginRules"
-            size="large"
+              v-show="!showVerifySms"
+              ref="ruleFormRef"
+              :model="ruleForm"
+              :rules="loginRules"
+              size="large"
           >
             <Motion :delay="100">
-              <el-form-item prop="loginName">
+              <el-form-item prop="username">
                 <el-input
-                  v-model="ruleForm.loginName"
-                  clearable
-                  placeholder="账号"
-                  :prefix-icon="useRenderIcon(User)"
+                    v-model="ruleForm.username"
+                    clearable
+                    placeholder="账号"
+                    :prefix-icon="useRenderIcon(User)"
                 />
               </el-form-item>
             </Motion>
 
             <Motion :delay="150">
-              <el-form-item prop="loginPwd">
+              <el-form-item prop="password">
                 <el-input
-                  v-model="ruleForm.loginPwd"
-                  clearable
-                  show-password
-                  placeholder="密码"
-                  :prefix-icon="useRenderIcon(Lock)"
+                    v-model="ruleForm.password"
+                    clearable
+                    show-password
+                    placeholder="密码"
+                    :prefix-icon="useRenderIcon(Lock)"
                 />
               </el-form-item>
             </Motion>
 
             <Motion :delay="200">
-              <el-form-item prop="verification">
+              <el-form-item prop="code">
                 <el-row class="w-full">
                   <el-col :span="17">
                     <el-input
-                      v-model="ruleForm.verification"
-                      clearable
-                      :prefix-icon="
+                        v-model="ruleForm.code"
+                        clearable
+                        :prefix-icon="
                         useRenderIcon(
                           MaterialSymbolsLightDomainVerificationRounded
                         )
@@ -299,15 +300,15 @@ onMounted(() => {
                   </el-col>
                   <el-col :span="6" :offset="1">
                     <div
-                      v-optimize="{
+                        v-optimize="{
                         event: 'click',
                         fn: getVerifyCode,
                         immediate: true,
                         timeout: 1000
                       }"
-                      class="verify-code"
+                        class="verify-code"
                     >
-                      {{ ruleForm.verification }}
+                      <img class="w-full h-full" :src="imgSrc" alt="">
                     </div>
                   </el-col>
                 </el-row>
@@ -324,12 +325,12 @@ onMounted(() => {
 
             <Motion :delay="300">
               <el-button
-                class="w-full"
-                size="default"
-                type="primary"
-                :loading="loading"
-                :disabled="disabled"
-                @click="onLogin(ruleFormRef)"
+                  class="w-full"
+                  size="default"
+                  type="primary"
+                  :loading="loading"
+                  :disabled="disabled"
+                  @click="onLogin(ruleFormRef)"
               >
                 登录
               </el-button>
@@ -338,36 +339,36 @@ onMounted(() => {
 
           <!-- 短信验证码表单 -->
           <el-form
-            v-show="showVerifySms"
-            ref="smsFormRef"
-            :model="smsRuleForm"
-            :rules="smsRules"
-            size="large"
-            @submit.prevent
+              v-show="showVerifySms"
+              ref="smsFormRef"
+              :model="smsRuleForm"
+              :rules="smsRules"
+              size="large"
+              @submit.prevent
           >
             <Motion :delay="100">
-              <el-form-item prop="loginName">
+              <el-form-item prop="username">
                 <el-row class="w-full">
                   <el-col :span="15">
                     <el-input
-                      v-model="smsRuleForm.verifySmsCode"
-                      clearable
-                      placeholder="请输入验证码"
-                      :prefix-icon="useRenderIcon(EpIphone)"
+                        v-model="smsRuleForm.verifySmsCode"
+                        clearable
+                        placeholder="请输入验证码"
+                        :prefix-icon="useRenderIcon(EpIphone)"
                     />
                   </el-col>
                   <el-col :span="8" :offset="1">
                     <el-button
-                      v-optimize="{
+                        v-optimize="{
                         event: 'click',
                         fn: getVerifySmsCode,
                         immediate: true,
                         timeout: 1000
                       }"
-                      class="w-full"
-                      type="primary"
-                      :disabled="!canSendSecond"
-                      >{{ sendVerifySmsCodeText }}
+                        class="w-full"
+                        type="primary"
+                        :disabled="!canSendSecond"
+                    >{{ sendVerifySmsCodeText }}
                     </el-button>
                   </el-col>
                 </el-row>
@@ -384,12 +385,12 @@ onMounted(() => {
 
             <Motion :delay="200">
               <el-button
-                class="w-full"
-                size="default"
-                type="primary"
-                :loading="loading"
-                :disabled="disabled"
-                @click="onLogin(smsFormRef)"
+                  class="w-full"
+                  size="default"
+                  type="primary"
+                  :loading="loading"
+                  :disabled="disabled"
+                  @click="onLogin(smsFormRef)"
               >
                 登录
               </el-button>

+ 670 - 0
src/views/system/menu/index.vue

@@ -0,0 +1,670 @@
+<template>
+  <div>
+    <PlusPage
+        ref="plusPageInstance"
+        :columns="tableConfig"
+        :request="getList"
+        :before-search-submit="handleBeforeSearch"
+        :is-card="true"
+        :search="{
+          labelWidth: 100
+        }"
+        :table="{
+          actionBar: { buttons, type: 'link', width: 140 },
+          adaptive: { offsetBottom: 50 },
+          onSelectionChange: handleSelect,
+          treeProps: { children: 'children' },
+          rowKey: 'menuId'
+        }"
+        :pagination="false"
+    >
+      <template #table-title>
+        <el-row class="button-row">
+          <el-button size="default" type="success" @click="handleCreate"> 添加</el-button>
+        </el-row>
+      </template>
+    </PlusPage>
+    <!-- 弹窗编辑 -->
+    <PlusDialogForm
+        ref="dialogForm"
+        v-model="form"
+        v-model:visible="dialogVisible"
+        @change="handleChange"
+        @confirm="handleSubmit"
+        @submit-error="handleSubmitError"
+        @close="handleClose"
+        :form="{ columns, labelPosition: 'right',labelWidth: 100, rules, rowProps: {gutter: 20}, colProps: {span: 12} }"
+        :dialog="{ title: dialogTitle + '菜单', width: 800, confirmLoading }"
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {computed, defineOptions, nextTick, onMounted, reactive, ref, toRefs} from "vue";
+import type {FormRules} from 'element-plus'
+import {
+  type PlusColumn,
+  type FieldValues,
+  PlusDialogForm,
+  PlusPage,
+  useTable,
+  PlusPageInstance
+} from "plus-pro-components";
+import {cloneDeep} from "lodash-es";
+import {ElMessage} from "element-plus";
+import ExportView from "@/components/ExportView/index.vue";
+import {
+  addMenu,
+  deleteMenu,
+  getSystemList,
+  getSystemMenuDetailById,
+  getSystemMenuList,
+  updateMenu
+} from "@/api/system/menu";
+import {h} from "vue";
+
+defineOptions({
+  name: "PageTable"
+});
+
+interface TableRow {
+  id: number;
+  name: string;
+  status: string;
+  tag: string;
+  time: Date;
+}
+
+onMounted(async () => {
+  // 获取系统列表
+  let res = await getSystemList();
+  systemList.value = res.data.map((item: any) => ({
+    label: item.sysName,
+    value: item.sysCode
+  }))
+})
+
+// 菜单列表
+const menuList = ref([]);
+const plusPageInstance = ref<PlusPageInstance | null>(null)
+
+const getList = async (query: Record<string, any>) => {
+  let res = await getSystemMenuList(query);
+  let list = listToTree(res.data);
+  menuList.value = list;
+  return {
+    data: list,
+    total: list.length,
+  }
+};
+
+const listToTree = (data: object[]) => {
+  // * 先生成parent建立父子关系
+  const obj = {};
+  data.forEach((item: any) => {
+    obj[item.menuId] = item;
+  });
+  // * obj -> {1001: {id: 1001, parentId: 0, name: 'AA'}, 1002: {...}}
+  // console.log(obj, "obj")
+  const parentList = [];
+  data.forEach((item: any) => {
+    const parent = obj[item.parentId];
+    if (parent) {
+      // * 当前项有父节点
+      parent.children = parent.children || [];
+      parent.children.push(item);
+    } else {
+      // * 当前项没有父节点 -> 顶层
+      parentList.push(item);
+    }
+  });
+  return parentList;
+}
+
+// 重新请求列表接口
+const refresh = () => {
+  plusPageInstance.value?.getList()
+}
+
+// 搜索之前函数
+const handleBeforeSearch = (values: any) => {
+  const params = cloneDeep(values);
+  Reflect.set(params, "name1", Reflect.get(values, "name"));
+  Reflect.deleteProperty(params, "name");
+  console.log(params);
+
+  // 返回新的参数
+  return params;
+};
+
+const dialogTitle = computed(() => (state.isCreate ? '新增' : '编辑'))
+const {buttons} = useTable<TableRow[]>();
+
+const systemList = ref([]);
+
+// 表格数据
+const tableConfig: PlusColumn[] = [
+  {
+    label: "菜单名称",
+    tooltip: "名称最多显示6个字符",
+    prop: "menuName",
+    width: 180,
+    tableColumnProps: {
+      showOverflowTooltip: true
+    },
+  },
+  {
+    label: "图标",
+    prop: "icon",
+    hideInSearch: true,
+    width: 80,
+    // 返回一个组件
+    render: (value) =>
+        h('i',
+            {
+              class: `${value} iconfont`,
+              style: {fontSize: '16px'}
+            },
+        )
+  },
+  {
+    label: "排序",
+    prop: "orderNum",
+    width: 80,
+    hideInSearch: true
+  },
+  {
+    label: "权限标识",
+    prop: "perms",
+    hideInSearch: true
+  },
+  {
+    label: "网关URL",
+    prop: "url",
+    hideInSearch: true
+  },
+  {
+    label: "组件路径",
+    prop: "component",
+    hideInSearch: true
+  },
+  {
+    label: "状态",
+    prop: "status",
+    valueType: "tag",
+    hideInSearch: true,
+    fieldProps: value => ({
+      type: value === '0' ? 'primary' : 'danger'
+    }),
+    fieldSlots: {
+      default: ({value}) => (value === '0' ? '正常' : '停用')
+    }
+  },
+  {
+    label: "系统",
+    prop: "systemCode",
+    valueType: "select",
+    options: computed(() => systemList.value),
+    fieldProps: {
+      clearable: true,
+      placeholder: '请选择系统',
+      style: {width: '100%'}
+    }
+  },
+  {
+    label: "菜单状态",
+    prop: "status",
+    valueType: "select",
+    options: [{label: '正常', value: '0'}, {label: '停用', value: '1'}],
+    fieldProps: {
+      clearable: true,
+      placeholder: '请选择菜单状态',
+      style: {width: '100%'}
+    }
+  },
+  {
+    label: "创建时间",
+    prop: "createTime",
+    valueType: "date-picker",
+    hideInSearch: true,
+    minWidth: 180
+  }
+];
+
+/*--------------------表单--------------------*/
+
+const dialogForm = ref(null);
+
+interface State {
+  dialogVisible: boolean;
+  detailsVisible: boolean;
+  confirmLoading: boolean;
+  selectedIds: number[];
+  isCreate: boolean;
+  form: {
+    component: string, // 组件路径
+    icon: string, // 菜单图标
+    isCache: string, // 是否缓存 (0缓存 1不缓存)
+    isFrame: string, // 是否外链 (0是 1否)
+    menuName: string,// 菜单名称
+    menuType: string, // 菜单类型(M目录 C菜单 F按钮)
+    orderNum: string,// 显示排序
+    parentId: number,// 上级菜单
+    path: string, // 路由地址
+    perms: string, // 权限标识
+    query: string, // 路由参数
+    routeName: string, // 路由名称
+    status: string, // 菜单状态(0正常 1停用)
+    systemCode: string, // 系统编码
+    url: string, // 网关URL
+    urlmatch: string, // 网关权限
+    visible: string, // 显示状态(0显示 1隐藏)
+  };
+  rules: FormRules;
+}
+
+const state = reactive<State>({
+  dialogVisible: false,
+  detailsVisible: false,
+  confirmLoading: false,
+  selectedIds: [],
+  isCreate: false,
+  form: {
+    component: null, // 组件路径
+    icon: null, // 菜单图标
+    isCache: '0', // 是否缓存 (0缓存 1不缓存)
+    isFrame: '1', // 是否外链 (0是 1否)
+    menuName: null,// 菜单名称
+    menuType: 'M', // 菜单类型(M目录 C菜单 F按钮)
+    orderNum: null,// 显示排序
+    parentId: 0,// 上级菜单
+    path: null, // 路由地址
+    perms: null, // 权限标识
+    query: null, // 路由参数
+    routeName: null, // 路由名称
+    status: '0', // 菜单状态(0正常 1停用)
+    systemCode: null, // 系统编码
+    url: null, // 网关URL
+    urlmatch: null, // 网关权限
+    visible: '0', // 显示状态(0显示 1隐藏)
+  },
+  rules: {
+    parentId: [{required: true, message: '请选择上级菜单', trigger: 'blur'}],
+    orderNum: [{required: true, message: '请输入排序', trigger: 'blur'}],
+    menuName: [{required: true, message: '请输入菜单名称', trigger: 'blur'}],
+    path: [{required: true, message: '请输入路由地址', trigger: 'blur'}],
+  }
+})
+
+const columns: PlusColumn[] = [
+  {
+    label: '上级菜单',
+    prop: 'parentId',
+    colProps: {span: 24},
+    valueType: 'tree-select',
+    fieldProps: {
+      placeholder: '请选择上级菜单',
+      clearable: true,
+      showSearch: true,
+      checkStrictly: true,
+      defaultExpandAll: false,
+      style: {width: '100%'},
+      data: computed(() => {
+        return [{menuId: 0, menuName: '主类目', children: [...menuList.value]},]
+      }),
+      // el-tree-select 组件属性
+      props: {
+        label: 'menuName',
+        value: 'menuId',
+        children: 'children'
+      }
+    }
+  },
+  {
+    label: "系统编码",
+    prop: "systemCode",
+    valueType: 'input',
+    colProps: {span: 24},
+    fieldProps: {
+      placeholder: '请输入系统编码',
+      clearable: true
+    }
+  },
+  {
+    label: "菜单类型",
+    prop: "menuType",
+    valueType: 'radio',
+    colProps: {span: 24},
+    fieldProps: {
+      placeholder: '请选择菜单类型',
+      clearable: true,
+      options: [
+        {label: '目录', value: 'M'},
+        {label: '菜单', value: 'C'},
+        {label: '按钮', value: 'F'}
+      ],
+    },
+  },
+  {
+    label: "菜单图标",
+    prop: "icon",
+    valueType: 'input',
+    colProps: {span: 12},
+    fieldProps: {
+      placeholder: '请输入菜单图标',
+      clearable: true,
+    },
+    hideInForm: computed(() => form.value.menuType === 'F')
+  },
+  {
+    label: "显示排序",
+    prop: "orderNum",
+    valueType: 'input-number',
+    colProps: {span: 12},
+    fieldProps: {
+      placeholder: '请输入显示排序',
+      style: {width: '100%'},
+      min: 0
+    }
+  },
+  {
+    label: "菜单名称",
+    prop: "menuName",
+    colProps: {span: 12},
+    fieldProps: {
+      placeholder: '请输入菜单名称',
+      clearable: true
+    }
+  },
+  {
+    label: "路由名称",
+    prop: "routeName",
+    colProps: {span: 12},
+    fieldProps: {
+      placeholder: '请输入路由名称',
+      clearable: true
+    },
+    hideInForm: computed(() => form.value.menuType !== 'C')
+  },
+  {
+    label: "是否外链",
+    prop: "isFrame",
+    valueType: 'radio',
+    colProps: {span: 12},
+    fieldProps: {
+      options: [
+        {label: '是', value: '0'},
+        {label: '否', value: '1'}
+      ]
+    },
+    tooltip: '选择是外链则路由地址需要以`http(s)://`开头',
+    hideInForm: computed(() => form.value.menuType === 'F')
+  },
+  {
+    label: "路由地址",
+    prop: "path",
+    colProps: {span: 12},
+    fieldProps: {
+      placeholder: '请输入路由地址',
+      clearable: true
+    },
+    tooltip: "访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头",
+    hideInForm: computed(() => form.value.menuType === 'F')
+  },
+  {
+    label: "组件路径",
+    prop: "component",
+    colProps: {span: 12},
+    fieldProps: {
+      placeholder: '请输入组件路径',
+      clearable: true
+    },
+    tooltip: "访问的组件路径,如:`system/user/index`,默认在`views`目录下",
+    hideInForm: computed(() => form.value.menuType !== 'C')
+  },
+  {
+    label: "权限字符",
+    prop: "perms",
+    colProps: {span: 12},
+    fieldProps: {
+      placeholder: '请输入权限标识',
+      clearable: true
+    },
+    tooltip: "控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)",
+    hideInForm: computed(() => form.value.menuType === 'M')
+  },
+  {
+    label: "网关URL",
+    prop: "url",
+    colProps: {span: 12},
+    fieldProps: {
+      placeholder: '请输入URL',
+      clearable: true
+    },
+  },
+  {
+    label: "网关权限",
+    prop: "urlmatch",
+    colProps: {span: 12},
+    fieldProps: {
+      placeholder: '请输入网关权限标识',
+      clearable: true
+    },
+    tooltip: "控制器中定义的权限字符",
+  },
+  {
+    label: "路由参数",
+    prop: "query",
+    colProps: {span: 12},
+    fieldProps: {
+      placeholder: '请输入路由参数',
+      clearable: true
+    },
+    tooltip: "访问路由的默认传递参数,如:`{\"id\": 1, \"name\": \"ry\"}`",
+    hideInForm: computed(() => form.value.menuType !== 'C')
+  },
+  {
+    label: "是否缓存",
+    prop: "isCache",
+    valueType: 'radio',
+    colProps: {span: 12},
+    fieldProps: {
+      options: [
+        {label: '是', value: '0'},
+        {label: '否', value: '1'}
+      ]
+    },
+    tooltip: "选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致",
+    hideInForm: computed(() => form.value.menuType !== 'C')
+  },
+  {
+    label: "显示状态",
+    prop: "visible",
+    valueType: 'radio',
+    colProps: {span: 12},
+    fieldProps: {
+      options: [
+        {label: '显示', value: '0'},
+        {label: '隐藏', value: '1'}
+      ]
+    },
+    tooltip: "选择隐藏则路由将不会出现在侧边栏,但仍然可以访问",
+    hideInForm: computed(() => form.value.menuType === 'F')
+  },
+  {
+    label: "菜单状态",
+    prop: "status",
+    valueType: 'radio',
+    colProps: {span: 12},
+    fieldProps: {
+      options: [
+        {label: '正常', value: '0'},
+        {label: '停用', value: '1'}
+      ]
+    },
+    tooltip: "选择停用则路由将不会出现在侧边栏,也不能被访问"
+  },
+]
+
+// 创建
+const handleCreate = (): void => {
+  form.value = {
+    component: null, // 组件路径
+    icon: null, // 菜单图标
+    isCache: '0', // 是否缓存 (0缓存 1不缓存)
+    isFrame: '1', // 是否外链 (0是 1否)
+    menuName: null,// 菜单名称
+    menuType: 'M', // 菜单类型(M目录 C菜单 F按钮)
+    orderNum: null,// 显示排序
+    parentId: 0,// 上级菜单
+    path: null, // 路由地址
+    perms: null, // 权限标识
+    query: null, // 路由参数
+    routeName: null, // 路由名称
+    status: '0', // 菜单状态(0正常 1停用)
+    systemCode: null, // 系统编码
+    url: null, // 网关URL
+    urlmatch: null, // 网关权限
+    visible: '0', // 显示状态(0显示 1隐藏)
+  }
+  state.isCreate = true
+  state.dialogVisible = true
+}
+
+// 选择
+const handleSelect = (data: any) => {
+  state.selectedIds = [...data].map(item => item.id)
+}
+
+const handleChange = (values: FieldValues) => {
+  console.log(values, 'change')
+}
+
+const handleSubmit = async (values: FieldValues) => {
+  console.log(values, 'Submit')
+  confirmLoading.value = true
+  if (state.isCreate) {
+    try {
+      let params = form.value
+      let res = await addMenu(params)
+      if (res.code === 200) {
+        ElMessage.success('新增成功')
+        confirmLoading.value = false
+        dialogVisible.value = false
+        refresh()
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } finally {
+      confirmLoading.value = false
+    }
+  } else {
+    // 编辑
+    try {
+      let params = form.value
+      let res = await updateMenu(params)
+      if (res.code === 200) {
+        ElMessage.success('修改成功')
+        confirmLoading.value = false
+        dialogVisible.value = false
+        refresh()
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } finally {
+      confirmLoading.value = false
+    }
+  }
+}
+const handleSubmitError = (err: any) => {
+  console.log(err, 'err')
+}
+const handleClose = () => {
+  // 重置表单
+  nextTick(() => {
+    if (dialogForm.value) {
+      dialogForm.value.formInstance.resetFields()
+    }
+  })
+  console.log(dialogForm.value.formInstance)
+}
+
+buttons.value = [
+  {
+    // 修改
+    text: "修改",
+    code: "edit",
+    // props v0.1.16 版本新增函数类型
+    props: {
+      type: "primary"
+    },
+    onClick(params) {
+      getSystemMenuDetailById(params.row.menuId).then(res => {
+        form.value = res.data
+        state.isCreate = false
+        state.dialogVisible = true
+      })
+    },
+  },
+  {
+    // 新增
+    text: "新增",
+    code: "view",
+    props: {
+      type: "success"
+    },
+    onClick(params) {
+      console.log("新增", params)
+      form.value = {
+        component: null, // 组件路径
+        icon: null, // 菜单图标
+        isCache: '0', // 是否缓存 (0缓存 1不缓存)
+        isFrame: '1', // 是否外链 (0是 1否)
+        menuName: null,// 菜单名称
+        menuType: 'M', // 菜单类型(M目录 C菜单 F按钮)
+        orderNum: null,// 显示排序
+        parentId: params.row.menuId,// 上级菜单
+        path: null, // 路由地址
+        perms: null, // 权限标识
+        query: null, // 路由参数
+        routeName: null, // 路由名称
+        status: '0', // 菜单状态(0正常 1停用)
+        systemCode: null, // 系统编码
+        url: null, // 网关URL
+        urlmatch: null, // 网关权限
+        visible: '0', // 显示状态(0显示 1隐藏)
+      }
+      state.isCreate = true
+      state.dialogVisible = true
+    },
+  },
+  {
+    // 删除
+    text: "删除",
+    code: "delete",
+    // props v0.1.16 版本新增计算属性支持
+    props: computed(() => ({type: "danger"})),
+    confirm: {
+      options: {
+        draggable: true,
+        message: "确定删除此数据吗?"
+      }
+    },
+    onConfirm: async (params) => {
+      try {
+        let res = await deleteMenu(params.row.menuId)
+        if (res.code === 200) {
+          ElMessage.success('删除成功')
+          refresh()
+        } else {
+          ElMessage.error(res.msg)
+        }
+      } catch (e) {
+        ElMessage.error('删除失败')
+      }
+    }
+  }
+];
+
+const {form, confirmLoading, rules, dialogVisible} = toRefs(state)
+</script>

+ 1 - 1
vite.config.ts

@@ -27,7 +27,7 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
       proxy: {
         "/api": {
           // 这里填写后端地址
-          target: "https://pre-admin.pre.jiebide.xin",
+          target: "http://192.168.1.168/api",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/api/, "")
         }