Prechádzať zdrojové kódy

feat(架构): 登录登出功能完善

shizhongming 2 rokov pred
rodič
commit
d01cf65828

+ 3 - 0
.env.development

@@ -12,3 +12,6 @@ VITE_GLOB_UPLOAD_URL=/upload
 
 # Interface prefix
 VITE_GLOB_API_URL_PREFIX=
+
+# 单体架构
+VITE_GLOB_API_MODE=standalone

+ 62 - 5
src/api/sys/menu.ts

@@ -1,14 +1,71 @@
-import { defHttp } from '@/utils/http/axios';
-import { getMenuListResultModel } from './model/menuModel';
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+import { getMenuListResultModel, RouteItem } from './model/menuModel';
+import { useLocaleStore } from '@/store/modules/locale';
+import StringUtils from '@/utils/StringUtils';
+import TreeUtils from '@/utils/TreeUtils';
 
 enum Api {
-  GetMenuList = '/getMenuList',
+  GetMenuList = '/sys/user/listUserMenu',
 }
 
 /**
  * @description: Get user menu based on id
  */
 
-export const getMenuList = () => {
-  return defHttp.get<getMenuListResultModel>({ url: Api.GetMenuList });
+export const getMenuList = async () => {
+  const locales = useLocaleStore().localInfo.availableLocales;
+  const menuList: Array<getMenuListResultModel> = await defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.GetMenuList,
+    data: locales,
+  });
+  const routeMenuList: RouteItem[] = menuList.map((item) => {
+    const {
+      url,
+      functionName,
+      locales,
+      icon,
+      functionId,
+      parentId,
+      component,
+      componentName,
+      redirect,
+      cached,
+      isMenu,
+    } = item;
+    // 兼容icon
+    let compatibleIcon = icon;
+    if (compatibleIcon && compatibleIcon.indexOf(':') === -1) {
+      compatibleIcon = StringUtils.humpToLine(compatibleIcon);
+      compatibleIcon = 'ant-design:' + compatibleIcon;
+    }
+    const routeItem: RouteItem = {
+      path: url,
+      name: componentName || functionName,
+      component,
+      meta: {
+        hideMenu: isMenu === false,
+        title: functionName,
+        locales,
+        icon: compatibleIcon,
+        key: functionId,
+        parentKey: parentId,
+        queryToProps: true,
+      },
+    };
+    if (redirect) {
+      routeItem.redirect = redirect;
+    }
+    if (cached === false) {
+      routeItem.meta.ignoreKeepAlive = true;
+    }
+    return routeItem;
+  });
+  // 构建树
+  return TreeUtils.convertList2Tree(
+    routeMenuList,
+    (data) => data.meta.key,
+    (data) => data.meta.parentKey,
+    0,
+  );
 };

+ 17 - 1
src/api/sys/model/menuModel.ts

@@ -11,7 +11,23 @@ export interface RouteItem {
   children?: RouteItem[];
 }
 
+export type FunctionType = 'MENU' | 'CATALOG';
+
 /**
  * @description: Get menu return value
  */
-export type getMenuListResultModel = RouteItem[];
+export interface getMenuListResultModel {
+  component: string;
+  componentName: string;
+  cached?: boolean;
+  functionName: string;
+  functionType: FunctionType;
+  internalOrExternal: boolean;
+  locales: Recordable<string>;
+  url: string;
+  icon?: string;
+  functionId: number;
+  parentId: number;
+  isMenu?: boolean;
+  redirect?: string;
+}

+ 8 - 4
src/api/sys/model/userModel.ts

@@ -4,6 +4,8 @@
 export interface LoginParams {
   username: string;
   password: string;
+  codeKey: string;
+  code: string;
 }
 
 export interface RoleInfo {
@@ -15,24 +17,26 @@ export interface RoleInfo {
  * @description: Login interface return value
  */
 export interface LoginResultModel {
-  userId: string | number;
+  user: GetUserInfoModel;
   token: string;
-  roles: RoleInfo[];
+  roles: Array<string>;
+  permissions: Array<string>;
 }
 
 /**
  * @description: Get user information return value
  */
 export interface GetUserInfoModel {
-  roles: RoleInfo[];
   // 用户id
-  userId: string | number;
+  userId: number;
   // 用户名
   username: string;
+  fullName: string;
   // 真实名字
   realName: string;
   // 头像
   avatar: string;
   // 介绍
   desc?: string;
+  homePath?: string;
 }

+ 7 - 6
src/api/sys/user.ts

@@ -1,11 +1,11 @@
-import { defHttp } from '@/utils/http/axios';
-import { LoginParams, LoginResultModel, GetUserInfoModel } from './model/userModel';
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+import { GetUserInfoModel, LoginParams, LoginResultModel } from './model/userModel';
 
 import { ErrorMessageMode } from '#/axios';
 
 enum Api {
-  Login = '/login',
-  Logout = '/logout',
+  Login = '/auth/login',
+  Logout = '/auth/logout',
   GetUserInfo = '/getUserInfo',
   GetPermCode = '/getPermCode',
   TestRetry = '/testRetry',
@@ -15,8 +15,9 @@ enum Api {
  * @description: user login api
  */
 export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal') {
-  return defHttp.post<LoginResultModel>(
+  return defHttp.postForm<LoginResultModel>(
     {
+      service: ApiServiceEnum.SMART_AUTH,
       url: Api.Login,
       params,
     },
@@ -38,7 +39,7 @@ export function getPermCode() {
 }
 
 export function doLogout() {
-  return defHttp.get({ url: Api.Logout });
+  return defHttp.postForm({ service: ApiServiceEnum.SMART_AUTH, url: Api.Logout });
 }
 
 export function testRetry() {

+ 2 - 0
src/enums/cacheEnum.ts

@@ -29,6 +29,8 @@ export const APP_SESSION_CACHE_KEY = 'COMMON__SESSION__KEY__';
 // table 列设置
 export const TABLE_SETTING_KEY = 'TABLE__SETTING__KEY__';
 
+export const PERMISSION_KEY = 'PERMISSIONS__KEY__';
+
 export enum CacheTypeEnum {
   SESSION,
   LOCAL,

+ 3 - 3
src/enums/httpEnum.ts

@@ -2,9 +2,9 @@
  * @description: Request result set
  */
 export enum ResultEnum {
-  SUCCESS = 0,
-  ERROR = -1,
-  TIMEOUT = 401,
+  SUCCESS = 200,
+  ERROR = 500,
+  TIMEOUT = 503,
   TYPE = 'success',
 }
 

+ 8 - 2
src/hooks/setting/index.ts

@@ -3,8 +3,13 @@ import type { GlobConfig } from '#/config';
 import { getAppEnvConfig } from '@/utils/env';
 
 export const useGlobSetting = (): Readonly<GlobConfig> => {
-  const { VITE_GLOB_APP_TITLE, VITE_GLOB_API_URL, VITE_GLOB_API_URL_PREFIX, VITE_GLOB_UPLOAD_URL } =
-    getAppEnvConfig();
+  const {
+    VITE_GLOB_APP_TITLE,
+    VITE_GLOB_API_URL,
+    VITE_GLOB_API_URL_PREFIX,
+    VITE_GLOB_UPLOAD_URL,
+    VITE_GLOB_API_MODE,
+  } = getAppEnvConfig();
 
   // Take global configuration
   const glob: Readonly<GlobConfig> = {
@@ -13,6 +18,7 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
     shortName: VITE_GLOB_APP_TITLE.replace(/\s/g, '_').replace(/-/g, '_'),
     urlPrefix: VITE_GLOB_API_URL_PREFIX,
     uploadUrl: VITE_GLOB_UPLOAD_URL,
+    isStandalone: VITE_GLOB_API_MODE === 'standalone',
   };
   return glob as Readonly<GlobConfig>;
 };

+ 43 - 0
src/locales/helper.ts

@@ -1,6 +1,7 @@
 import type { LocaleType } from '#/config';
 
 import { set } from 'lodash-es';
+import { deepMerge } from '@/utils';
 
 export const loadLocalePool: LocaleType[] = [];
 
@@ -35,3 +36,45 @@ export function genMessage(langs: Record<string, Record<string, any>>, prefix =
   });
   return obj;
 }
+
+export const generateModuleMessage = (langs: Record<string, Record<string, any>>) => {
+  let result: Recordable = {};
+  Object.keys(langs).forEach((key) => {
+    const langModule = langs[key].default;
+    if (langModule.trans) {
+      result = deepMerge(result, transferI18n(langModule));
+    } else {
+      result = deepMerge(result, langModule);
+    }
+  });
+
+  return result;
+};
+
+export type I18nTransfer = {
+  trans: boolean;
+  key: string;
+  data: { [index: string]: any };
+};
+
+/**
+ * 转换国际化信息
+ */
+export const transferI18n = (data: I18nTransfer | any) => {
+  if (!data.trans) {
+    return data;
+  }
+  const keySplit = data.key.split('.');
+  let object = {};
+  for (let i = keySplit.length - 1; i >= 0; i--) {
+    const key = keySplit[i];
+    const itemData: any = {};
+    if (i === keySplit.length - 1) {
+      itemData[key] = data.data;
+    } else {
+      itemData[key] = object;
+    }
+    object = itemData;
+  }
+  return object;
+};

+ 7 - 2
src/locales/lang/en.ts

@@ -1,10 +1,15 @@
-import { genMessage } from '../helper';
+import { generateModuleMessage, genMessage } from '../helper';
 import antdLocale from 'ant-design-vue/es/locale/en_US';
+import { deepMerge } from '@/utils';
 
 const modules = import.meta.glob('./en/**/*.{json,ts,js}', { eager: true });
+const modulesLocales = import.meta.glob('../../modules/**/lang/en_US.ts', { eager: true });
 export default {
   message: {
-    ...genMessage(modules as Recordable<Recordable>, 'en'),
+    ...deepMerge(
+      genMessage(modules as Recordable<Recordable>, 'en'),
+      generateModuleMessage(modulesLocales as Recordable<Recordable>),
+    ),
     antdLocale,
   },
   dateLocale: null,

+ 6 - 3
src/locales/lang/zh_CN.ts

@@ -1,12 +1,15 @@
-import { genMessage } from '../helper';
+import { generateModuleMessage, genMessage } from '../helper';
 import antdLocale from 'ant-design-vue/es/locale/zh_CN';
 import { deepMerge } from '@/utils';
 
 const modules = import.meta.glob('./zh-CN/**/*.{json,ts,js}', { eager: true });
-
+const modulesLocales = import.meta.glob('../../modules/**/lang/zh_CN.ts', { eager: true });
 export default {
   message: {
-    ...genMessage(modules as Recordable<Recordable>, 'zh-CN'),
+    ...deepMerge(
+      genMessage(modules as Recordable<Recordable>, 'zh-CN'),
+      generateModuleMessage(modulesLocales as Recordable<Recordable>),
+    ),
     antdLocale: {
       ...antdLocale,
       DatePicker: deepMerge(

+ 8 - 0
src/modules/system/views/login/lang/en_US.ts

@@ -0,0 +1,8 @@
+export default {
+  trans: true,
+  key: 'system.login',
+  data: {
+    captchaRefreshTooltip: 'Click refresh verification code',
+    'login-captcha': 'Please enter captcha',
+  },
+};

+ 8 - 0
src/modules/system/views/login/lang/zh_CN.ts

@@ -0,0 +1,8 @@
+export default {
+  trans: true,
+  key: 'system.login',
+  data: {
+    captchaRefreshTooltip: '点击刷新验证码',
+    'login-captcha': '请输入验证码',
+  },
+};

+ 1 - 1
src/settings/projectSetting.ts

@@ -27,7 +27,7 @@ const setting: ProjectConfig = {
   settingButtonPosition: SettingButtonPositionEnum.AUTO,
 
   // Permission mode
-  permissionMode: PermissionModeEnum.ROUTE_MAPPING,
+  permissionMode: PermissionModeEnum.BACK,
 
   // Permission-related cache is stored in sessionStorage or localStorage
   permissionCacheType: CacheTypeEnum.LOCAL,

+ 8 - 2
src/store/modules/permission.ts

@@ -23,6 +23,8 @@ import { getPermCode } from '@/api/sys/user';
 
 import { useMessage } from '@/hooks/web/useMessage';
 import { PageEnum } from '@/enums/pageEnum';
+import { getAuthCache, setAuthCache } from '@/utils/auth';
+import { PERMISSION_KEY } from '@/enums/cacheEnum';
 
 interface PermissionState {
   // Permission code list
@@ -61,7 +63,10 @@ export const usePermissionStore = defineStore({
   }),
   getters: {
     getPermCodeList(state): string[] | number[] {
-      return state.permCodeList;
+      if (state.permCodeList && state.permCodeList.length > 0) {
+        return state.permCodeList;
+      }
+      return getAuthCache(PERMISSION_KEY) || [];
     },
     getBackMenuList(state): Menu[] {
       return state.backMenuList;
@@ -79,6 +84,7 @@ export const usePermissionStore = defineStore({
   actions: {
     setPermCodeList(codeList: string[]) {
       this.permCodeList = codeList;
+      setAuthCache(PERMISSION_KEY, this.permCodeList);
     },
 
     setBackMenuList(list: Menu[]) {
@@ -221,7 +227,7 @@ export const usePermissionStore = defineStore({
           // 这个功能可能只需要执行一次,实际项目可以自己放在合适的时间
           let routeList: AppRouteRecordRaw[] = [];
           try {
-            await this.changePermissionCode();
+            // await this.changePermissionCode();
             routeList = (await getMenuList()) as AppRouteRecordRaw[];
           } catch (error) {
             console.error(error);

+ 30 - 24
src/store/modules/user.ts

@@ -6,21 +6,20 @@ import { RoleEnum } from '@/enums/roleEnum';
 import { PageEnum } from '@/enums/pageEnum';
 import { ROLES_KEY, TOKEN_KEY, USER_INFO_KEY } from '@/enums/cacheEnum';
 import { getAuthCache, setAuthCache } from '@/utils/auth';
-import { GetUserInfoModel, LoginParams } from '@/api/sys/model/userModel';
-import { doLogout, getUserInfo, loginApi } from '@/api/sys/user';
+import { GetUserInfoModel, LoginParams, LoginResultModel } from '@/api/sys/model/userModel';
+import { doLogout, loginApi } from '@/api/sys/user';
 import { useI18n } from '@/hooks/web/useI18n';
 import { useMessage } from '@/hooks/web/useMessage';
 import { router } from '@/router';
 import { usePermissionStore } from '@/store/modules/permission';
 import { RouteRecordRaw } from 'vue-router';
 import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic';
-import { isArray } from '@/utils/is';
 import { h } from 'vue';
 
 interface UserState {
   userInfo: Nullable<UserInfo>;
   token?: string;
-  roleList: RoleEnum[];
+  roleList: string[];
   sessionTimeout?: boolean;
   lastUpdateTime: number;
 }
@@ -46,7 +45,7 @@ export const useUserStore = defineStore({
     getToken(state): string {
       return state.token || getAuthCache<string>(TOKEN_KEY);
     },
-    getRoleList(state): RoleEnum[] {
+    getRoleList(state): string[] {
       return state.roleList.length > 0 ? state.roleList : getAuthCache<RoleEnum[]>(ROLES_KEY);
     },
     getSessionTimeout(state): boolean {
@@ -61,7 +60,7 @@ export const useUserStore = defineStore({
       this.token = info ? info : ''; // for null or undefined value
       setAuthCache(TOKEN_KEY, info);
     },
-    setRoleList(roleList: RoleEnum[]) {
+    setRoleList(roleList: string[]) {
       this.roleList = roleList;
       setAuthCache(ROLES_KEY, roleList);
     },
@@ -91,25 +90,31 @@ export const useUserStore = defineStore({
       try {
         const { goHome = true, mode, ...loginParams } = params;
         const data = await loginApi(loginParams, mode);
-        const { token } = data;
 
-        // save token
-        this.setToken(token);
-        return this.afterLoginAction(goHome);
+        return this.afterLoginAction(data, goHome);
       } catch (error) {
         return Promise.reject(error);
       }
     },
-    async afterLoginAction(goHome?: boolean): Promise<GetUserInfoModel | null> {
+    async afterLoginAction(
+      loginResult: LoginResultModel,
+      goHome?: boolean,
+    ): Promise<GetUserInfoModel | null> {
+      const { token, user: userInfo, permissions, roles } = loginResult;
+      this.setToken(token);
+      this.setRoleList(roles);
       if (!this.getToken) return null;
       // get user info
-      const userInfo = await this.getUserInfoAction();
+      this.setUserInfo({
+        ...userInfo,
+      });
 
       const sessionTimeout = this.sessionTimeout;
       if (sessionTimeout) {
         this.setSessionTimeout(false);
       } else {
         const permissionStore = usePermissionStore();
+        permissionStore.setPermCodeList(permissions);
         if (!permissionStore.isDynamicAddedRoute) {
           const routes = await permissionStore.buildRoutesAction();
           routes.forEach((route) => {
@@ -123,18 +128,19 @@ export const useUserStore = defineStore({
       return userInfo;
     },
     async getUserInfoAction(): Promise<UserInfo | null> {
-      if (!this.getToken) return null;
-      const userInfo = await getUserInfo();
-      const { roles = [] } = userInfo;
-      if (isArray(roles)) {
-        const roleList = roles.map((item) => item.value) as RoleEnum[];
-        this.setRoleList(roleList);
-      } else {
-        userInfo.roles = [];
-        this.setRoleList([]);
-      }
-      this.setUserInfo(userInfo);
-      return userInfo;
+      // if (!this.getToken) return null;
+      // const userInfo = await getUserInfo();
+      // const { roles = [] } = userInfo;
+      // if (isArray(roles)) {
+      //   const roleList = roles.map((item) => item.value) as RoleEnum[];
+      //   this.setRoleList(roleList);
+      // } else {
+      //   userInfo.roles = [];
+      //   this.setRoleList([]);
+      // }
+      // this.setUserInfo(userInfo);
+      // return userInfo;
+      return this.getUserInfo;
     },
     /**
      * @description: logout

+ 19 - 0
src/utils/StringUtils.ts

@@ -0,0 +1,19 @@
+/**
+ * 字符串处理工具类
+ */
+export default class StringUtils {
+  /**
+   * 判断String 是为未空
+   * @param value
+   */
+  public static hasLength(value: string | null | undefined): boolean {
+    return !!value && value.length > 0;
+  }
+
+  public static humpToLine(camelCaseName: string): string {
+    camelCaseName = camelCaseName.replace(camelCaseName[0], camelCaseName[0].toLowerCase());
+    return camelCaseName.replace(/([A-Z])/g, function (match) {
+      return '-' + match.toLowerCase();
+    });
+  }
+}

+ 52 - 0
src/utils/TreeUtils.ts

@@ -0,0 +1,52 @@
+/**
+ * 树形工具类
+ */
+export default class TreeUtils {
+  public static CHILDREN = 'children';
+  public static HAS_CHILD = 'hasChild';
+  public static HAS_PARENT = 'hasParent';
+
+  /**
+   * 将list转为tree
+   * @param list 列表数据
+   * @param keyGetter
+   * @param parentKeyGetter
+   * @param topParentCode 顶级节点的parentId
+   */
+  public static convertList2Tree<T>(
+    list: T[],
+    keyGetter: (T) => string | number,
+    parentKeyGetter: (T) => string | number,
+    topParentCode?: string | number,
+  ): T[] | null {
+    if (list == null) {
+      return null;
+    }
+    if (topParentCode === undefined || topParentCode === null) {
+      topParentCode = '0';
+    }
+    const treeList: any[] = [];
+    for (const value of list) {
+      const parentId = parentKeyGetter(value);
+      // 如果父ID 等于顶级父ID,则是顶级节点
+      if (parentId === null || parentId === topParentCode) {
+        treeList.push(value);
+        continue;
+      }
+      for (const parent of list) {
+        const id = keyGetter(parent);
+        if (id === parentId) {
+          if (!parent[this.CHILDREN]) {
+            parent[this.CHILDREN] = [];
+          }
+          parent[this.CHILDREN].push(value);
+          // 设置节点含有下级
+          parent[this.HAS_CHILD] = true;
+          // 设置节点含有上级
+          value[this.HAS_PARENT] = true;
+        }
+      }
+    }
+    return treeList;
+  }
+}

+ 10 - 0
src/utils/auth/index.ts

@@ -1,6 +1,7 @@
 import { Persistent, BasicKeys } from '@/utils/cache/persistent';
 import { CacheTypeEnum, TOKEN_KEY } from '@/enums/cacheEnum';
 import projectSetting from '@/settings/projectSetting';
+import sha256 from 'crypto-js/sha256';
 
 const { permissionCacheType } = projectSetting;
 const isLocal = permissionCacheType === CacheTypeEnum.LOCAL;
@@ -23,3 +24,12 @@ export function clearAuthCache(immediate = true) {
   const fn = isLocal ? Persistent.clearLocal : Persistent.clearSession;
   return fn(immediate);
 }
+
+/**
+ * 创建密码
+ * @param username
+ * @param password
+ */
+export const createPassword = (username: string, password: string) => {
+  return sha256(sha256(`${username}${password}888888$#@`)).toString();
+};

+ 2 - 0
src/utils/cache/persistent.ts

@@ -14,6 +14,7 @@ import {
   APP_SESSION_CACHE_KEY,
   MULTIPLE_TABS_KEY,
   TABLE_SETTING_KEY,
+  PERMISSION_KEY,
 } from '@/enums/cacheEnum';
 import { DEFAULT_CACHE_TIME } from '@/settings/encryptionSetting';
 import { toRaw } from 'vue';
@@ -27,6 +28,7 @@ interface BasicStore {
   [PROJ_CFG_KEY]: ProjectConfig;
   [MULTIPLE_TABS_KEY]: RouteLocationNormalized[];
   [TABLE_SETTING_KEY]: Partial<TableSetting>;
+  [PERMISSION_KEY]: string[];
 }
 
 type LocalStore = BasicStore;

+ 7 - 1
src/utils/env.ts

@@ -30,7 +30,12 @@ export function getAppEnvConfig() {
     ? // Get the global configuration (the configuration will be extracted independently when packaging)
       (import.meta.env as unknown as GlobEnvConfig)
     : (window[ENV_NAME] as unknown as GlobEnvConfig);
-  const { VITE_GLOB_APP_TITLE, VITE_GLOB_API_URL_PREFIX, VITE_GLOB_UPLOAD_URL } = ENV;
+  const {
+    VITE_GLOB_APP_TITLE,
+    VITE_GLOB_API_URL_PREFIX,
+    VITE_GLOB_UPLOAD_URL,
+    VITE_GLOB_API_MODE,
+  } = ENV;
   let { VITE_GLOB_API_URL } = ENV;
   if (localStorage.getItem(API_ADDRESS)) {
     const address = JSON.parse(localStorage.getItem(API_ADDRESS) || '{}');
@@ -41,6 +46,7 @@ export function getAppEnvConfig() {
     VITE_GLOB_API_URL,
     VITE_GLOB_API_URL_PREFIX,
     VITE_GLOB_UPLOAD_URL,
+    VITE_GLOB_API_MODE,
   };
 }
 

+ 25 - 15
src/utils/http/axios/Axios.ts

@@ -1,11 +1,5 @@
-import type {
-  AxiosRequestConfig,
-  AxiosInstance,
-  AxiosResponse,
-  AxiosError,
-  InternalAxiosRequestConfig,
-} from 'axios';
-import type { RequestOptions, Result, UploadFileParams } from '#/axios';
+import type { AxiosInstance, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios';
+import type { SmartAxiosRequestConfig, RequestOptions, Result, UploadFileParams } from '#/axios';
 import type { CreateAxiosOptions } from './axiosTransform';
 import axios from 'axios';
 import qs from 'qs';
@@ -13,6 +7,7 @@ import { AxiosCanceler } from './axiosCancel';
 import { isFunction } from '@/utils/is';
 import { cloneDeep } from 'lodash-es';
 import { ContentTypeEnum, RequestEnum } from '@/enums/httpEnum';
+import { useGlobSetting } from '@/hooks/setting';
 
 export * from './axiosTransform';
 
@@ -126,7 +121,7 @@ export class VAxios {
   /**
    * @description:  File Upload
    */
-  uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
+  uploadFile<T = any>(config: SmartAxiosRequestConfig, params: UploadFileParams) {
     const formData = new window.FormData();
     const customFilename = params.name || 'file';
 
@@ -163,7 +158,7 @@ export class VAxios {
   }
 
   // support form-data
-  supportFormData(config: AxiosRequestConfig) {
+  supportFormData(config: SmartAxiosRequestConfig) {
     const headers = config.headers || this.options.headers;
     const contentType = headers?.['Content-Type'] || headers?.['content-type'];
 
@@ -181,23 +176,23 @@ export class VAxios {
     };
   }
 
-  get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+  get<T = any>(config: SmartAxiosRequestConfig, options?: RequestOptions): Promise<T> {
     return this.request({ ...config, method: 'GET' }, options);
   }
 
-  post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+  post<T = any>(config: SmartAxiosRequestConfig, options?: RequestOptions): Promise<T> {
     return this.request({ ...config, method: 'POST' }, options);
   }
 
-  put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+  put<T = any>(config: SmartAxiosRequestConfig, options?: RequestOptions): Promise<T> {
     return this.request({ ...config, method: 'PUT' }, options);
   }
 
-  delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+  delete<T = any>(config: SmartAxiosRequestConfig, options?: RequestOptions): Promise<T> {
     return this.request({ ...config, method: 'DELETE' }, options);
   }
 
-  request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+  request<T = any>(config: SmartAxiosRequestConfig, options?: RequestOptions): Promise<T> {
     let conf: CreateAxiosOptions = cloneDeep(config);
     // cancelToken 如果被深拷贝,会导致最外层无法使用cancel方法来取消请求
     if (config.cancelToken) {
@@ -249,4 +244,19 @@ export class VAxios {
         });
     });
   }
+
+  getApiUrlByService(service: string): string {
+    const url = this.options.requestOptions?.apiUrl || '';
+    const { isStandalone } = useGlobSetting();
+    if (!isStandalone) {
+      return `${url}/${service}`;
+    }
+    return url;
+  }
+
+  postForm<T = any>(config: SmartAxiosRequestConfig, options?: RequestOptions): Promise<T> {
+    const headers = config.headers || {};
+    headers['Content-Type'] = ContentTypeEnum.FORM_URLENCODED;
+    return this.request({ ...config, method: 'POST', headers }, options);
+  }
 }

+ 8 - 10
src/utils/http/axios/axiosTransform.ts

@@ -1,15 +1,10 @@
 /**
  * Data processing class, can be configured according to the project
  */
-import type {
-  AxiosInstance,
-  AxiosRequestConfig,
-  AxiosResponse,
-  InternalAxiosRequestConfig,
-} from 'axios';
-import type { RequestOptions, Result } from '#/axios';
-
-export interface CreateAxiosOptions extends AxiosRequestConfig {
+import type { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
+import type { RequestOptions, Result, SmartAxiosRequestConfig } from '#/axios';
+
+export interface CreateAxiosOptions extends SmartAxiosRequestConfig {
   authenticationScheme?: string;
   transform?: AxiosTransform;
   requestOptions?: RequestOptions;
@@ -20,7 +15,10 @@ export abstract class AxiosTransform {
    * A function that is called before a request is sent. It can modify the request configuration as needed.
    * 在发送请求之前调用的函数。它可以根据需要修改请求配置。
    */
-  beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
+  beforeRequestHook?: (
+    config: SmartAxiosRequestConfig,
+    options: RequestOptions,
+  ) => SmartAxiosRequestConfig;
 
   /**
    * @description: 处理响应数据

+ 13 - 1
src/utils/http/axios/index.ts

@@ -51,7 +51,7 @@ const transform: AxiosTransform = {
       throw new Error(t('sys.api.apiRequestFailed'));
     }
     //  这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
-    const { code, result, message } = data;
+    const { code, data: result, message } = data;
 
     // 这里逻辑可以根据项目进行修改
     const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS;
@@ -226,6 +226,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
     // 深度合并
     deepMerge(
       {
+        service: ApiServiceEnum.NONE,
         // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
         // authentication schemes,e.g: Bearer
         // authenticationScheme: 'Bearer',
@@ -283,3 +284,14 @@ export const defHttp = createAxios();
 //     urlPrefix: 'xxx',
 //   },
 // });
+
+export enum ApiServiceEnum {
+  NONE = '',
+  SMART_AUTH = 'smart-auth',
+  SMART_SYSTEM = 'smart-system',
+  SMART_FILE = 'smart-file',
+  // 代码生成器
+  SMART_CODE = 'smart-code',
+  // 消息服务,包括短信等
+  SMART_MESSAGE = 'smart-message',
+}

+ 1 - 1
src/views/sys/login/Login.vue

@@ -185,7 +185,7 @@
       }
     }
 
-    input:not([type='checkbox']) {
+    input:not([type='checkbox'], .login-captcha) {
       min-width: 360px;
       /* stylelint-disable-next-line media-query-no-invalid */
       @media (max-width: @screen-xl) {

+ 38 - 3
src/views/sys/login/LoginForm.vue

@@ -25,6 +25,25 @@
       />
     </FormItem>
 
+    <ARow :gutter="16">
+      <ACol :span="16">
+        <FormItem name="captcha">
+          <Input
+            v-model:value="formData.captcha"
+            class="login-captcha"
+            :placeholder="t('system.login.login-captcha')"
+            size="large"
+          />
+        </FormItem>
+      </ACol>
+      <ACol :span="8">
+        <Tooltip>
+          <template #title>{{ t('system.login.captchaRefreshTooltip') }}</template>
+          <img style="height: 40px" :src="computedCaptchaUrl" @click="handleChangeCaptcha" />
+        </Tooltip>
+      </ACol>
+    </ARow>
+
     <ARow class="enter-x">
       <ACol :span="12">
         <FormItem>
@@ -84,7 +103,7 @@
 <script lang="ts" setup>
   import { reactive, ref, unref, computed } from 'vue';
 
-  import { Checkbox, Form, Input, Row, Col, Button, Divider } from 'ant-design-vue';
+  import { Checkbox, Form, Input, Row, Col, Button, Divider, Tooltip } from 'ant-design-vue';
   import {
     GithubFilled,
     WechatFilled,
@@ -100,6 +119,9 @@
   import { useUserStore } from '@/store/modules/user';
   import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
   import { useDesign } from '@/hooks/web/useDesign';
+  import { buildUUID } from '@/utils/uuid';
+  import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+  import { createPassword } from '@/utils/auth';
   //import { onKeyStroke } from '@vueuse/core';
 
   const ACol = Col;
@@ -119,8 +141,10 @@
   const rememberMe = ref(false);
 
   const formData = reactive({
-    account: 'vben',
+    account: 'admin',
     password: '123456',
+    captcha: '',
+    captchaKey: buildUUID(),
   });
 
   const { validForm } = useFormValid(formRef);
@@ -135,9 +159,11 @@
     try {
       loading.value = true;
       const userInfo = await userStore.login({
-        password: data.password,
+        password: createPassword(data.account, data.password),
         username: data.account,
         mode: 'none', //不要默认的错误提示
+        codeKey: formData.captchaKey,
+        code: formData.captcha,
       });
       if (userInfo) {
         notification.success({
@@ -156,4 +182,13 @@
       loading.value = false;
     }
   }
+
+  const computedCaptchaUrl = computed(() => {
+    return `${defHttp.getApiUrlByService(ApiServiceEnum.SMART_AUTH)}/auth/createCaptcha?codeKey=${
+      formData.captchaKey
+    }`;
+  });
+  const handleChangeCaptcha = () => {
+    formData.captchaKey = buildUUID();
+  };
 </script>

+ 12 - 1
types/axios.d.ts

@@ -1,3 +1,6 @@
+import type { ApiServiceEnum } from '@/utils/http/axios';
+import type { AxiosRequestConfig } from 'axios';
+
 export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
 export type SuccessMessageMode = ErrorMessageMode;
 
@@ -39,7 +42,8 @@ export interface Result<T = any> {
   code: number;
   type: 'success' | 'error' | 'warning';
   message: string;
-  result: T;
+  data: T;
+  exceptionNo?: number;
 }
 
 // multipart/form-data: upload file
@@ -54,3 +58,10 @@ export interface UploadFileParams {
   filename?: string;
   [key: string]: any;
 }
+
+export type ApiService = ApiServiceEnum;
+
+export interface SmartAxiosRequestConfig<D = any> extends AxiosRequestConfig<D> {
+  // 指定请求发送的服务
+  service: ApiService;
+}

+ 11 - 0
types/config.d.ts

@@ -149,6 +149,8 @@ export interface GlobConfig {
   urlPrefix?: string;
   // Project abbreviation
   shortName: string;
+  // 是否是单体架构
+  isStandalone: boolean;
 }
 export interface GlobEnvConfig {
   // Site title
@@ -159,4 +161,13 @@ export interface GlobEnvConfig {
   VITE_GLOB_API_URL_PREFIX?: string;
   // Upload url
   VITE_GLOB_UPLOAD_URL?: string;
+  /**
+   * 后台api模式,standalone:单体架构,cloud:微服务架构
+   */
+  VITE_GLOB_API_MODE?: ApiMode;
 }
+
+/**
+ * 后台api模式,standalone:单体架构,cloud:微服务架构
+ */
+type ApiMode = 'standalone' | 'cloud';

+ 0 - 2
types/store.d.ts

@@ -1,6 +1,5 @@
 import { ErrorTypeEnum } from '@/enums/exceptionEnum';
 import { MenuModeEnum, MenuTypeEnum } from '@/enums/menuEnum';
-import { RoleInfo } from '@/api/sys/model/userModel';
 
 // Lock screen information
 export interface LockInfo {
@@ -42,7 +41,6 @@ export interface UserInfo {
   avatar: string;
   desc?: string;
   homePath?: string;
-  roles: RoleInfo[];
 }
 
 export interface BeforeMiniState {

+ 1 - 3
types/vue-router.d.ts

@@ -1,5 +1,3 @@
-import { RoleEnum } from '@/enums/roleEnum';
-
 export {};
 
 declare module 'vue-router' {
@@ -14,7 +12,7 @@ declare module 'vue-router' {
     // Whether to ignore permissions
     ignoreAuth?: boolean;
     // role info
-    roles?: RoleEnum[];
+    roles?: string[];
     // Whether not to cache
     ignoreKeepAlive?: boolean;
     // Is it fixed on tab

+ 1 - 1
vite.config.ts

@@ -17,7 +17,7 @@ export default defineApplicationConfig({
     server: {
       proxy: {
         '/basic-api': {
-          target: 'http://localhost:3000',
+          target: 'http://localhost:9095',
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp(`^/basic-api`), ''),