feat: 初始化提交
笑笑 authored at 2025-05-13 21:25:06
11.44 KiB
cube-front
/**
 * HTTP请求工具
 * 封装axios,处理请求拦截、响应拦截、错误处理和401授权问题
 */
import axios, {
  type AxiosError,
  type AxiosInterceptorManager,
  type AxiosRequestConfig,
  type AxiosRequestHeaders,
  type AxiosResponse,
  type InternalAxiosRequestConfig,
} from 'axios';
import queryString from 'query-string';
import { getSession, removeAllCookie, setSession } from './storage';
import { getAccessToken, removeAccessToken } from './token';
import { getConfig } from '../configure';
import { gotoPage } from './router';
import { getResponse } from './common';
import notification from '../components/Notification';

import { ElMessageBox } from 'element-plus';
import { intl } from '../i18n';

/**
 * Modal对话框工具
 * 封装Element Plus的ElMessageBox组件
 */
const Modal = {
  /**
   * 打开模态对话框
   * @param {Object} options - 对话框配置选项
   * @returns {Promise} - 返回对话框实例
   */
  open: (options: {
    title?: string;
    children?: string;
    cancelText?: string;
    okText?: string;
    onOk?: () => void;
    onCancel?: () => void;
    afterClose?: () => void;
    key?: string;
  }) => {
    return ElMessageBox({
      title: options.title || '提示',
      message: options.children || '',
      showCancelButton: !!options.cancelText,
      confirmButtonText: options.okText || '确定',
      cancelButtonText: options.cancelText || '取消',
      type: 'warning',
      callback: (action: 'confirm' | 'cancel' | 'close') => {
        if (action === 'confirm' && typeof options.onOk === 'function') {
          options.onOk();
        } else if (action === 'cancel' && typeof options.onCancel === 'function') {
          options.onCancel();
        }

        if (typeof options.afterClose === 'function') {
          options.afterClose();
        }
      },
    });
  },
  close: () => {
    ElMessageBox.close();
  },
  /**
   * 更新当前打开的对话框
   * @param {Object} props - 要更新的属性
   */
  update: (props: Record<string, unknown>) => {
    // Element Plus的对话框不直接支持更新,这里提供一个兼容接口
    console.log('Modal.update called with props:', props);
  },
};

// 常量定义
const BASE_PATH = '';
const INDEX_ROUTE_PATH = '/';

/**
 * 重定向到登录页
 * @param {Object} options - 配置选项
 * @param {string} options.loginPageUrl - 可选的登录页URL
 */
export function redirectToLogin({ loginPageUrl }: { loginPageUrl?: string } = {}) {
  removeAccessToken();
  removeAllCookie();

  const {
    request: { baseURL: API_HOST },
    auth: { oauthUrl },
  } = getConfig();
  const LOGIN_URL = loginPageUrl || `${API_HOST}${oauthUrl}`;

  const sessionData = getSession('redirectUrl');
  let cacheLocation = sessionData;
  if (!cacheLocation) {
    cacheLocation = encodeURIComponent(`${window.location.origin}${BASE_PATH || '/'}`);
  }

  // const modifyReloginLocation = getConfig().auth?.modifyReloginLocation;
  // if (modifyReloginLocation) {
  //   cacheLocation = modifyReloginLocation(cacheLocation, {
  //     BASE_PATH: BASE_PATH || '/',
  //   });
  // }

  // 构建重定向URL
  const redirectParams = getSession('templateParams') || '';
  if (LOGIN_URL.includes('?')) {
    window.location.href = `${LOGIN_URL}&redirect_uri=${cacheLocation}${redirectParams}`;
  } else {
    window.location.href = `${LOGIN_URL}?redirect_uri=${cacheLocation}${redirectParams}`;
  }
}

// 创建带token的axios实例
const cubeAxios = axios.create();

// 创建不带token的axios实例
const notWithTokenAxios = axios.create();
notWithTokenAxios.interceptors.request.use((config) => {
  const {
    request: { baseURL: API_HOST },
  } = getConfig();
  let { url = '' } = config;
  if (url.indexOf('://') === -1 && !url.startsWith('/_api')) {
    url = `${API_HOST}${url}`;
  }
  return {
    ...config,
    url,
  };
});

// 401错误标志,防止重复处理401
let isErrorFlag = false;

/**
 * 鉴权拦截处理
 * @param {number} status - HTTP状态码
 * @param {InternalAxiosRequestConfig} config - 请求配置
 * @returns {boolean} - 是否继续处理响应
 */
function authIntercept(status: number, config: InternalAxiosRequestConfig) {
  if (status === 401) {
    // 避免重复处理401
    if (isErrorFlag) {
      return false;
    }

    /**
     * 设置重定向URL到会话存储
     */
    const setRedirectUrl = () => {
      let _cacheLocation = window.location.toString().replace('/unauthorized', '');
      // @ts-expect-error 全局窗口对象可能包含routerBase属性
      const basePath = (window.routerBase || BASE_PATH)?.replace(/\/$/, '');
      const url1 = new URL(_cacheLocation);
      let p = url1.pathname;
      if (basePath && p.startsWith(basePath)) {
        p = p.replace(basePath, '');
      }
      if (p === '/') {
        url1.pathname = `${basePath}${INDEX_ROUTE_PATH}`;
        _cacheLocation = url1.toString();
      }
      const cacheLocation = encodeURIComponent(_cacheLocation);
      const searchParams = queryString.parse(window.location.search)?.template;
      const templateParams = searchParams ? `&template=${searchParams}` : '';

      setSession('templateParams', templateParams);
      setSession('redirectUrl', cacheLocation);
    };

    isErrorFlag = true;
    const { url = '' } = config;
    const {
      user: { getUserInfoAxiosConfig },
    } = getConfig();

    let AUTH_SELF_URL = '/Admin/User/Info';
    if (typeof getUserInfoAxiosConfig === 'function') {
      const config = getUserInfoAxiosConfig();
      if (!(config instanceof Promise)) {
        AUTH_SELF_URL = config.url || AUTH_SELF_URL;
      }
    } else {
      AUTH_SELF_URL = getUserInfoAxiosConfig.url || AUTH_SELF_URL;
    }

    const isSelf401 = url.includes(AUTH_SELF_URL);

    if (isSelf401) {
      setRedirectUrl();
      redirectToLogin();
      return false;
    }

    /**
     * 跳转到未授权页面
     * @param {string} pageUrl - 未授权页面路径
     */
    const redirectToUnauthorized = (pageUrl = '/unauthorized') => {
      const language = intl.getLocale()?.replace('-', '_');

      // 登录后需要跳回的界面,放到session中
      if (!window.location.pathname.startsWith(`${BASE_PATH}${pageUrl.replace(/^\//, '')}`)) {
        setRedirectUrl();
      }

      // token失效,跳转到token失效页面
      gotoPage(`${pageUrl}?language=${language}${getSession('templateParams') || ''}`);
    };

    // 当位于/unauthorized页面时,不处理401
    const isInUnauthorizedPage = window.location.toString().indexOf('/unauthorized') !== -1;
    if (isInUnauthorizedPage) {
      return false;
    }

    setTimeout(() => {
      redirectToUnauthorized();
    }, 100);
  }
  return true;
}

/**
 * 响应错误处理
 * @param {Object} error - 错误对象
 * @returns {Promise} - 处理后的Promise
 */
function handleResponseError(error: AxiosError) {
  // 移除debugger语句,优化错误日志记录
  console.error('Request error:', error.message, error.config?.url);

  const { response, config, code } = error;

  // 如果response为空,直接返回
  if (!response) {
    if (code === 'ERR_NETWORK') {
      notification.error({ message: intl.get('notification.network.typeError').d('网络请求异常') });
      return;
    }
    return Promise.reject(error);
  }

  const envConfig = getConfig();

  // 判断是否鉴权问题
  if (response && response.status && !authIntercept(response.status, response.config)) {
    return Promise.reject(false);
  }

  // 状态204当做成功处理
  if (response?.status === 204) {
    return undefined;
  }

  // 响应拦截,请求时设置
  const responseIntercept = envConfig.request.responseIntercept;
  if (responseIntercept && typeof responseIntercept === 'function') {
    responseIntercept(error);
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const data = response?.data as any;
  const errorObj = {
    type: undefined,
    message: error.message,
    // @ts-expect-error 包含描述的可能的字段
    description: `${data?.description || data?.content || response?.requestMessage || ''}`,
  };

  if (data && data.code > 200) {
    errorObj.message =
      data.message || data.code || data.type || intl.get('cube.notification.failed').d('操作失败');
    // errorObj.type === data.type;
  }

  if (data && data.detailsMessage) {
    errorObj.description += `\n ${data.detailsMessage}`;
  }

  if (errorObj.message === 'Network Error') {
    errorObj.message = intl.get('notification.network.typeError').d('网络请求异常');
    errorObj.description = intl.get('notification.typeError.description').d('请稍后重试');
  }

  if (response.status === 200) {
    getResponse(data);
  } else if (response.status) {
    notification.autoNotification(
      errorObj?.type || 'error',
      errorObj?.message,
      errorObj?.description,
    );
  }

  throw error;
}

/**
 * 处理成功响应
 * @param {Object} response - 响应对象
 * @returns {any} - 处理后的响应数据,默认返回data
 */
function handleResponseSuccess(response: AxiosResponse) {
  const { status, data, config } = response;

  if (config.responseType === 'text' && typeof data !== 'string') {
    return JSON.stringify(data);
  }

  // 响应拦截
  const responseIntercept = getConfig().request.responseIntercept;
  if (responseIntercept && typeof responseIntercept === 'function') {
    responseIntercept(response);
  }

  // 异常响应拦截
  if (data && typeof data === 'object' && 'code' in data && data.code !== 0 && data.code !== 200) {
    // 处理业务逻辑错误
    const errorCode = data.code;
    const errorMessage = data.message || intl.get('error.unknown').d('未知错误');

    // 输出业务错误日志,但仍然返回响应以供业务代码处理
    console.warn(`业务错误 [${errorCode}]: ${errorMessage}`, data);
  }

  return data;
}

cubeAxios.interceptors.response.use(handleResponseSuccess, handleResponseError);

/**
 * 请求拦截器
 * @param {Object} config - 请求配置
 * @returns {Object} - 处理后的请求配置
 */
function handleRequestConfig(config: InternalAxiosRequestConfig) {
  const {
    request: { baseURL: API_HOST },
  } = getConfig();
  let { url = '' } = config || {};

  if (url.indexOf('://') === -1 && !url.startsWith('/_api')) {
    url = `${API_HOST}${url}`;
  }

  // 添加额外的请求头
  const additionalRequestHeaderConfig = getConfig().request.additionalRequestHeader;
  let additionalRequestHeader: Record<string, string> = {};
  if (additionalRequestHeaderConfig) {
    additionalRequestHeader =
      typeof additionalRequestHeaderConfig === 'function'
        ? additionalRequestHeaderConfig()
        : additionalRequestHeaderConfig;
  }

  const newOptions: InternalAxiosRequestConfig = {
    ...config,
    url,
    withCredentials: true,
    headers: {
      Authorization: `bearer ${getAccessToken()}`,
      ...additionalRequestHeader,
      ...(config?.headers || {}),
    } as AxiosRequestHeaders,
  };

  return newOptions;
}

/**
 * 请求错误处理
 * @param {Object} error - 错误对象
 * @returns {Promise} - 处理后的Promise
 */
async function handleRequestError(error: AxiosError) {
  const { config } = error;

  console.error('Request processing error:', error.message);

  return Promise.reject(error);
}

const requestHandlers = [handleRequestConfig, handleRequestError];
cubeAxios.interceptors.request.use(handleRequestConfig, handleRequestError);

// 导出配置好的axios实例
export const request = cubeAxios;
export default request;
export { cubeAxios };

// 替换原来导出的toReLogin
export { redirectToLogin as toReLogin };