Prechádzať zdrojové kódy

更新ESLint配置以关闭ts-comment警告,添加SVG图标加载器并导入所有SVG图标,重构Icon组件以支持SVG图标类型,优化首页组件以使用SVG图标,新增测试页面及相关路由配置,更新axios配置以支持请求重试和取消功能,提升代码结构和可读性。

xbx 1 týždeň pred
rodič
commit
1b05ca30a2

+ 41 - 0
.cursor/rules/project.mdc

@@ -0,0 +1,41 @@
+---
+description: 
+globs: 
+alwaysApply: true
+---
+
+# Cursor项目规则
+
+## 项目技术栈
+
+本项目基于以下技术栈搭建:
+- Vue 3
+- antd design vue
+- Pinia
+- typescript
+- rspack
+
+## 公共组件规范
+ 尽量实现公共的组件和方法 组件应当在components文件夹下面
+
+## 代码风格规范
+
+### 编码原则
+
+- **可读性优先**:代码应当易于理解,结构清晰,避免过度复杂的实现
+- **高效简洁**:追求高效的代码执行和简洁的代码表达,避免冗余
+- **实用性为主**:优先采用常用、成熟的编程模式,避免使用晦涩难懂的"高大上"技巧
+
+### 注释规范
+
+- 所有代码必须包含中文注释
+- 业务逻辑必须有相应的注释说明
+- 复杂的逻辑处理必须有详细的步骤注释
+- 函数和组件应当有清晰的功能说明注释
+
+## 开发流程
+
+- 在开始新功能开发前,请先熟悉本项目的公共组件和代码规范
+- 代码提交前进行自检,确保符合上述规范要求
+
+- 有疑问时请参考已有代码实现或向团队成员咨询

+ 7 - 6
config/rspack.base.config.ts

@@ -82,9 +82,15 @@ export const baseConfig = {
         test: /\.d\.ts$/,
         loader: 'ignore-loader',
       },
+      // SVG处理规则
       {
         test: /\.svg$/,
-        include: [path.resolve(__dirname, 'src/assets/svg')],
+        exclude: [path.resolve(__dirname, '../src/assets/icons')],
+        type: 'asset/resource',
+      },
+      {
+        test: /\.svg$/,
+        include: [path.resolve(__dirname, '../src/assets/icons')],
         use: [
           {
             loader: 'svg-sprite-loader',
@@ -95,11 +101,6 @@ export const baseConfig = {
         ],
       },
       {
-        test: /\.svg$/,
-        exclude: [path.resolve(__dirname, 'src/assets/svg')],
-        type: 'asset/resource',
-      },
-      {
         test: /\.(png|jpe?g|gif)$/i,
         type: 'asset/resource',
       },

+ 19 - 7
eslint.config.mjs

@@ -17,6 +17,9 @@ export default defineConfigWithVueTs(
   {
     name: 'app/files-to-lint',
     files: ['**/*.{ts,mts,tsx,vue}'],
+    rules: {
+      '@typescript-eslint/ban-ts-comment': 'off', // 彻底关闭 ts-comment 警告
+    }
   },
   globalIgnores([
     // 构建输出
@@ -84,8 +87,16 @@ export default defineConfigWithVueTs(
         ...globals.node,
         // 添加全局变量
         defineOptions: 'readonly', // 允许使用 defineOptions
-      } 
-    } 
+      },
+      parserOptions: {
+        ecmaFeatures: {
+          jsx: true
+        }
+      }
+    },
+    linterOptions: {
+      reportUnusedDisableDirectives: false
+    }
   },
   // Vue规则配置
   pluginVue.configs['flat/essential'],
@@ -93,6 +104,9 @@ export default defineConfigWithVueTs(
   eslintConfigPrettier,
   {
     rules: {
+      // 禁用烦人的 ts-comment 警告
+      '@typescript-eslint/ban-ts-comment': 'off',
+      
       // Vue特定规则
       'vue/multi-word-component-names': 'off', // 关闭组件名必须是多词的规则
       'vue/first-attribute-linebreak': 'off', // 关闭属性换行规则
@@ -104,22 +118,20 @@ export default defineConfigWithVueTs(
           component: 'always'
         }
       }],
-      'vue/max-attributes-per-line': ['warn', {
-        singleline: 3,
-        multiline: 1
-      }],
+      'vue/max-attributes-per-line': 'off', // 关闭每行最大属性数量限制
       'vue/first-attribute-linebreak': 'off', // 关闭属性换行规则
       // 通用规则
       'no-console': 'off', // 关闭控制台语句的警告
       'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
       '@typescript-eslint/no-require-imports': 'off', // 允许 require 导入
-      '@typescript-eslint/ban-ts-comment': 'off', // 关闭 ts-comment 警告
+      '@typescript-eslint/ban-ts-comment': 'off', // 完全关闭 ts-comment 警告
     }
   },
   // TypeScript规则配置
   vueTsConfigs.recommended,
   {
     rules: {
+      '@typescript-eslint/ban-ts-comment': 'off', // 彻底关闭 ts-comment 警告
       '@typescript-eslint/explicit-function-return-type': 'off',
       '@typescript-eslint/explicit-module-boundary-types': 'off',
       '@typescript-eslint/no-explicit-any': 'off', // 关闭 any 类型的警告

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/assets/icons/ai.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/assets/icons/fast.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
src/assets/icons/mark.svg


src/assets/svg/reader.svg → src/assets/icons/reader.svg


+ 23 - 79
src/components/Icon/Index.vue

@@ -1,53 +1,35 @@
 <script setup lang="ts">
-import { computed, defineProps, withDefaults, h, onMounted } from 'vue';
+import { computed } from 'vue';
 import * as AntdIcons from '@ant-design/icons-vue';
-import { hasIcon } from './register';
 
 defineOptions({
   name: 'SvgIcon'
 });
 
-// 定义组件属性类型
 interface Props {
   /** 图标名称 */
   name: string;
-  /** 图标类型: 'iconfont' | 'antd' */
-  type?: 'iconfont' | 'antd';
+  /** 图标类型: 'svg' | 'antd' */
+  type?: 'svg' | 'antd';
   /** 图标尺寸 (支持CSS单位) */
   size?: string | number;
   /** 图标颜色 */
   color?: string;
   /** 旋转动画 */
   spin?: boolean;
-  /** 旋转角度 */
-  rotationDegree?: number;
   /** 自定义类名 */
   className?: string;
 }
 
 // 设置默认属性值
 const props = withDefaults(defineProps<Props>(), {
-  type: 'iconfont',
+  type: 'svg',
   size: '1em',
   color: 'currentColor',
   spin: false,
-  rotationDegree: 360,
   className: ''
 });
 
-// 检查图标是否存在
-const iconExists = computed(() => {
-  if (props.type === 'iconfont') {
-    console.log(hasIcon(props.name), 'hasIcon(props.name)')
-    return hasIcon(props.name);
-  } else if (props.type === 'antd') {
-    return !!AntdIcons[props.name as keyof typeof AntdIcons];
-  }
-
-  console.log(props.name, 'props.name',props.type)
-  return false;
-});
-
 // 动态解析Ant Design图标
 const antdIcon = computed(() => {
   if (props.type !== 'antd') return null;
@@ -55,7 +37,7 @@ const antdIcon = computed(() => {
   const iconComponent = AntdIcons[props.name as keyof typeof AntdIcons] as any;
   
   if (!iconComponent) {
-    console.warn(`Ant Design icon not found: ${props.name}`);
+    console.warn(`Ant Design图标未找到: ${props.name}`);
   }
   
   return iconComponent || null;
@@ -67,43 +49,21 @@ const formattedSize = computed(() => {
   return props.size;
 });
 
-// 组合类名
-const iconClasses = computed(() => [
-  props.className,
-  props.type === 'iconfont' ? 'iconfont-svg' : 'antd-icon',
-  { 'icon-spin': props.spin }
-]);
-
-// 自定义样式,包括旋转角度
-const customStyle = computed(() => {
-  const style: Record<string, any> = {
-    '--rotation-degree': `${props.rotationDegree}deg`,
-  };
-  
-  if (props.type === 'iconfont') {
-    style.width = formattedSize.value;
-    style.height = formattedSize.value;
-    style.color = props.color;
-  }
-  
-  return style;
-});
-
-// 在开发环境下检查图标是否存在
-onMounted(() => {
-  if (process.env.NODE_ENV !== 'production' && !iconExists.value) {
-    console.warn(`Icon not found: ${props.name} (type: ${props.type})`);
-  }
-});
+// 自定义样式
+const style = computed(() => ({
+  width: formattedSize.value,
+  height: formattedSize.value,
+  color: props.color,
+  animation: props.spin ? 'icon-spin 1s infinite linear' : 'none'
+}));
 </script>
 
 <template>
-  <!-- Iconfont SVG图标 -->
-   
+  <!-- SVG图标 -->
   <svg 
-    v-if="type === 'iconfont' && iconExists"
-    :class="iconClasses"
-    :style="customStyle"
+    v-if="type === 'svg'"
+    :class="['svg-icon', className]"
+    :style="style"
     aria-hidden="true"
   >
     <use :xlink:href="`#icon-${name}`" />
@@ -117,8 +77,7 @@ onMounted(() => {
       spin, 
       style: { 
         color, 
-        fontSize: formattedSize,
-        '--rotation-degree': `${rotationDegree}deg`
+        fontSize: formattedSize
       } 
     }"
   />
@@ -134,7 +93,7 @@ onMounted(() => {
 </template>
 
 <style scoped>
-.iconfont-svg {
+.svg-icon {
   display: inline-block;
   vertical-align: -0.15em;
   fill: currentColor;
@@ -142,26 +101,6 @@ onMounted(() => {
   transition: transform 0.3s ease;
 }
 
-.iconfont-svg:hover, .antd-icon:hover {
-  animation: icon-spin 1s infinite linear;
-}
-
-.antd-icon {
-  display: inline-block;
-  line-height: 1;
-  vertical-align: -0.125em;
-  transition: transform 0.3s ease;
-}
-
-.icon-spin {
-  animation: icon-spin 1s infinite linear !important;
-}
-
-@keyframes icon-spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(var(--rotation-degree, 360deg)); }
-}
-
 .icon-fallback {
   display: inline-block;
   width: 1em;
@@ -173,4 +112,9 @@ onMounted(() => {
   font-weight: bold;
   color: #999;
 }
+
+@keyframes icon-spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
 </style>

+ 0 - 45
src/components/Icon/register.ts

@@ -1,45 +0,0 @@
-// 存储已加载的图标名称
-const loadedIcons: string[] = [];
-
-// 导入所有SVG图标并返回图标名称列表
-const importAllSvg = () => {
-  try {
-   
-    const requireContext = (require as any).context('@/assets/svg', false, /\.svg$/);
-    
-    // 获取所有图标的路径
-    const iconPaths = requireContext.keys() as string[];
-    
-    // 导入所有图标
-    iconPaths.forEach(path => {
-      requireContext(path);
-      
-      // 从路径中提取图标名称 (例如: ./icon-name.svg -> icon-name)
-      const iconName = path.match(/\.\/(.*)\.svg$/)?.[1] || '';
-      if (iconName) {
-        loadedIcons.push(iconName);
-      }
-    });
-    console.log(`成功加载 ${loadedIcons.length} 个SVG图标`);
-    return loadedIcons;
-  } catch (error) {
-    console.error('SVG图标导入错误:', error);
-    return [];
-  }
-};
-
-// 执行导入
-const icons = importAllSvg();
-
-// 导出图标列表,可用于图标预览或自动补全
-export default icons;
-
-// 检查图标是否存在
-export const hasIcon = (name: string): boolean => {
-  return loadedIcons.includes(name);
-};
-
-// 获取所有图标名称
-export const getIconNames = (): string[] => {
-  return [...loadedIcons];
-};

+ 1 - 2
src/components/index.ts

@@ -1,8 +1,7 @@
 import { App } from 'vue';
 import SvgIcon from './Icon/Index.vue';
 
-// 导入SVG图标
-import './Icon/register';
+
 
 // 需要全局注册的组件列表
 const components = {

+ 48 - 0
src/config/axios/axiosCancel.ts

@@ -0,0 +1,48 @@
+import type { AxiosRequestConfig,Canceler } from 'axios'
+
+// 用于存储每个请求的标识和取消函数
+let pendingMap = new Map<string, Canceler>()
+
+import { isFunction } from 'lodash-es';
+
+const getPendingUrl = (config: AxiosRequestConfig): string => {
+  return [config.method, config.url].join('&')
+}
+
+export class AxiosCanceler {
+  // 添加请求
+  public addPending(config: AxiosRequestConfig) {
+    this.removePending(config)
+    const url = getPendingUrl(config)
+    // @ts-ignore
+    config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
+      if (!pendingMap.has(url)) {
+        // 如果 pending 中不存在当前请求,则添加进去
+        pendingMap.set(url, cancel)
+      }
+    })
+  }
+  //取消全部请求
+  public removeAllPending() {
+    pendingMap.forEach(cancel => {
+      cancel && isFunction(cancel) && cancel()
+    })
+  }
+
+  public removePending(config: AxiosRequestConfig) {
+    const url = getPendingUrl(config)
+    if (pendingMap.has(url)) {
+      // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
+      const cancel = pendingMap.get(url)
+      cancel && cancel()
+      pendingMap.delete(url)
+    }
+  }
+
+  reset(): void {
+    pendingMap = new Map<string, Canceler>()
+  }
+
+
+
+}

+ 29 - 0
src/config/axios/axiosRetry.ts

@@ -0,0 +1,29 @@
+// @ts-nocheck
+
+import type { AxiosInstance, AxiosError } from 'axios';
+
+export class AxiosRetry {
+  public retry(AxiosInstance: AxiosInstance, error: AxiosError) {
+    // @ts-ignore
+    const { config } = error.response!;
+    // eslint-disable-next-line no-unsafe-optional-chaining
+    // @ts-ignore
+    const { waitTime, count = 0 } = config?.retryRequest
+    config.__retryCount = config.__retryCount || 0
+    if (config.__retryCount >= count) {
+      return Promise.reject(error)
+    }
+
+    config.__retryCount += 1
+    return this.delay(waitTime).then(() => AxiosInstance(config))
+
+  }
+
+  /**
+   * 延迟
+   */
+  private delay(waitTime: number) {
+    return new Promise((resolve) => setTimeout(resolve, waitTime))
+  }
+
+}

+ 38 - 0
src/config/axios/config.ts

@@ -0,0 +1,38 @@
+interface RetryRequest {
+  isOpenRetry: boolean
+  count: number
+  waitTime: number
+}
+
+
+
+const config: {
+  base_url: string
+  result_code: number | string
+  default_headers: AxiosHeaders
+  request_timeout: number,
+
+} = {
+  /**
+   * api请求基础路径
+   */
+  base_url: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL,
+  /**
+   * 接口成功返回状态码
+   */
+  result_code: 200,
+
+  /**
+   * 接口请求超时时间
+   */
+  request_timeout: 30000,
+
+  /**
+   * 默认接口请求类型
+   * 可选值:application/x-www-form-urlencoded multipart/form-data
+   */
+  default_headers: 'application/json',
+
+}
+
+export { config }

+ 51 - 0
src/config/axios/index.ts

@@ -0,0 +1,51 @@
+import { service } from './service'
+
+import { config } from './config'
+
+const { default_headers } = config
+
+const request = (option: any) => {
+  const { url, method, params, data, headersType, responseType, ...config } = option
+  return service({
+    url: url,
+    method,
+    params,
+    data,
+    ...config,
+    responseType: responseType,
+    headers: {
+      'Content-Type': headersType || default_headers
+    }
+  })
+}
+export default {
+  get: async <T = any>(option: any) => {
+    const res = await request({ method: 'GET', ...option })
+    return res.data as unknown as T
+  },
+  post: async <T = any>(option: any) => {
+    const res = await request({ method: 'POST', ...option })
+    return res.data as unknown as T
+  },
+  postOriginal: async (option: any) => {
+    const res = await request({ method: 'POST', ...option })
+    return res
+  },
+  delete: async <T = any>(option: any) => {
+    const res = await request({ method: 'DELETE', ...option })
+    return res.data as unknown as T
+  },
+  put: async <T = any>(option: any) => {
+    const res = await request({ method: 'PUT', ...option })
+    return res.data as unknown as T
+  },
+  download: async <T = any>(option: any) => {
+    const res = await request({ method: 'GET', responseType: 'blob', ...option })
+    return res as unknown as Promise<T>
+  },
+  upload: async <T = any>(option: any) => {
+    option.headersType = 'multipart/form-data'
+    const res = await request({ method: 'POST', ...option })
+    return res as unknown as Promise<T>
+  }
+}

+ 268 - 0
src/config/axios/service.ts

@@ -0,0 +1,268 @@
+import axios, {
+  AxiosError,
+  AxiosInstance,
+  AxiosRequestHeaders,
+  AxiosResponse,
+  InternalAxiosRequestConfig
+} from 'axios'
+
+import { AxiosCanceler } from './axiosCancel'
+import { AxiosRetry } from './axiosRetry'
+
+import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
+import qs from 'qs'
+import { config } from '@/config/axios/config'
+import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth'
+import errorCode from './errorCode'
+
+import { resetRouter } from '@/router'
+import { deleteUserCache } from '@/hooks/web/useCache'
+
+const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
+const { result_code, base_url, request_timeout } = config
+
+
+/**
+   * 响应重试
+   * */
+const retryRequest  = {
+  isOpenRetry: false,
+  count: 5,
+  waitTime: 400
+}
+
+// 需要忽略的提示。忽略后,自动 Promise.reject('error')
+const ignoreMsgs = [
+  '无效的刷新令牌', // 刷新令牌被删除时,不用提示
+  '刷新令牌已过期' // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面
+]
+// 是否显示重新登录
+export const isRelogin = { show: false }
+// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
+// 请求队列
+let requestList: any[] = []
+// 是否正在刷新中
+let isRefreshToken = false
+// 请求白名单,无须token的接口
+const whiteList: string[] = ['/login', '/refresh-token']
+
+// 创建axios实例
+const service: AxiosInstance = axios.create({
+  baseURL: base_url, // api 的 base_url
+  timeout: request_timeout, // 请求超时时间
+  withCredentials: false,  // 禁用 Cookie 等信息
+  retryRequest, //重试
+})
+
+const axiosCanceler = new AxiosCanceler()
+
+// request拦截器
+service.interceptors.request.use(
+  (config: InternalAxiosRequestConfig) => {
+    // 是否需要设置 token
+    let isToken = (config!.headers || {}).isToken === false
+    whiteList.some((v) => {
+      if (config.url) {
+        config.url.indexOf(v) > -1
+        return (isToken = false)
+      }
+    })
+
+    if (getAccessToken() && !isToken) {
+      ; (config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token
+    }
+    // 设置租户
+    if (tenantEnable && tenantEnable === 'true') {
+      const tenantId = getTenantId()
+      if (tenantId){
+        (config as Recordable).headers['tenant-id'] = tenantId
+      }else{
+        (config as Recordable).headers['tenant-id'] = '1'
+      }
+
+    }
+    const params = config.params || {}
+    const data = config.data || false
+    if (
+      config.method?.toUpperCase() === 'POST' &&
+      (config.headers as AxiosRequestHeaders)['Content-Type'] ===
+      'application/x-www-form-urlencoded'
+    ) {
+      config.data = qs.stringify(data)
+    }
+    // get参数编码
+    if (config.method?.toUpperCase() === 'GET' && params) {
+      config.params = {}
+      const paramsStr = qs.stringify(params, { allowDots: true })
+      if (paramsStr) {
+        config.url = config.url + '?' + paramsStr
+      }
+    }
+    const ignoreCancelToken = config?.ignoreCancelToken ?? true
+    //将请求添加到队列中
+    !ignoreCancelToken && axiosCanceler.addPending(config)
+
+    return config
+  },
+  (error: AxiosError) => {
+    // Do something with request error
+    console.log(error) // for debug
+    return Promise.reject(error)
+  }
+)
+
+// response 拦截器
+service.interceptors.response.use(
+  async (response: AxiosResponse<any>) => {
+    let { data } = response
+    const config = response.config
+    response && axiosCanceler.removePending(response.config)
+    if (!data) {
+      // 返回“[HTTP]请求没有返回值”;
+      throw new Error()
+    }
+    const { t } = useI18n()
+    // 未设置状态码则默认成功状态
+    // 二进制数据则直接返回,例如说 Excel 导出
+    if (
+      response.request.responseType === 'blob' ||
+      response.request.responseType === 'arraybuffer'
+    ) {
+      // 注意:如果导出的响应为 json,说明可能失败了,不直接返回进行下载
+      if (response.data.type !== 'application/json') {
+        return response.data
+      }
+      data = await new Response(response.data).json()
+    }
+    const code = data.code || result_code
+    // 获取错误信息
+    const msg = data.msg || errorCode[code] || errorCode['default']
+    if (ignoreMsgs.indexOf(msg) !== -1) {
+      // 如果是忽略的错误码,直接返回 msg 异常
+      return Promise.reject(msg)
+    } else if (code === 401) {
+      // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
+      if (!isRefreshToken) {
+        isRefreshToken = true
+        // 1. 如果获取不到刷新令牌,则只能执行登出操作
+        if (!getRefreshToken()) {
+          return handleAuthorized()
+        }
+        // 2. 进行刷新访问令牌
+        try {
+          const refreshTokenRes = await refreshToken()
+          // 2.1 刷新成功,则回放队列的请求 + 当前请求
+          setToken((await refreshTokenRes).data.data)
+          config.headers!.Authorization = 'Bearer ' + getAccessToken()
+          requestList.forEach((cb: any) => {
+            cb()
+          })
+          requestList = []
+          return service(config)
+        } catch (e) {
+          // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
+          // 2.2 刷新失败,只回放队列的请求
+          requestList.forEach((cb: any) => {
+            cb()
+          })
+          // 提示是否要登出。即不回放当前请求!不然会形成递归
+          return handleAuthorized()
+        } finally {
+          requestList = []
+          isRefreshToken = false
+        }
+      } else {
+        // 添加到队列,等待刷新获取到新的令牌
+        return new Promise((resolve) => {
+          requestList.push(() => {
+            config.headers!.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
+            resolve(service(config))
+          })
+        })
+      }
+    } else if (code === 500) {
+      ElMessage.error(t('sys.api.errMsg500'))
+      return Promise.reject(new Error(msg))
+    } /*  else if (code === 901) {
+      ElMessage.error({
+        offset: 300,
+        dangerouslyUseHTMLString: true,
+        message:
+          '<div>' +
+          t('sys.api.errMsg901') +
+          '</div>' +
+          '<div> &nbsp; </div>' +
+          '<div>参考 https://doc.iocoder.cn/ 教程</div>' +
+          '<div> &nbsp; </div>' +
+          '<div>5 分钟搭建本地环境</div>'
+      })
+      return Promise.reject(new Error(msg))
+    }  */ else if (code !== 200) {
+      if (msg === '无效的刷新令牌') {
+        // hard coding:忽略这个提示,直接登出
+        console.log(msg)
+        return handleAuthorized()
+      } else {
+        ElNotification.error({ title: msg })
+      }
+      return Promise.reject('error')
+    } else {
+      return data
+    }
+  },
+  (error: AxiosError) => {
+    console.log('err' + error) // for debug
+    let { message, config } = error || {}
+    const { t } = useI18n()
+    if (message === 'Network Error') {
+      message = t('sys.api.errorMessage')
+    } else if (message.includes('timeout')) {
+      message = t('sys.api.apiTimeoutMessage')
+    } else if (message.includes('Request failed with status code')) {
+      message = t('sys.api.apiRequestFailed') + message.substr(message.length - 3)
+    }
+    ElMessage.error(message)
+    if (axios.isCancel(error)) {
+      return Promise.reject(error)
+    }
+
+    // 添加自动重试机制 保险起见 只针对GET请求
+    const retryRequest = new AxiosRetry()
+    // @ts-ignore
+    const { isOpenRetry = false } = config.retryRequest!
+    config!.method?.toUpperCase() === 'GET' &&
+      isOpenRetry &&
+      // @ts-ignore
+      retryRequest.retry(service, error)
+    return Promise.reject(error)
+
+  }
+)
+
+const refreshToken = async () => {
+  axios.defaults.headers.common['tenant-id'] = getTenantId()
+  return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken())
+}
+const handleAuthorized = () => {
+  const { t } = useI18n()
+  if (!isRelogin.show) {
+    isRelogin.show = true
+    ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), {
+      showCancelButton: false,
+      closeOnClickModal: false,
+      showClose: false,
+      closeOnPressEscape: false,
+      confirmButtonText: t('login.relogin'),
+      type: 'warning'
+    }).then(() => {
+      resetRouter() // 重置静态路由表
+      deleteUserCache() // 删除用户缓存
+      removeToken()
+      isRelogin.show = false
+      // 干掉token后再走一次路由让它过router.beforeEach的校验
+      window.location.href = window.location.href
+    })
+  }
+  return Promise.reject(t('sys.api.timeoutMessage'))
+}
+export { service }

+ 3 - 0
src/main.ts

@@ -5,6 +5,9 @@ import App from './App.vue';
 import router from './router';
 import store from './store';
 
+// 导入SVG图标
+import './plugins/svg-icons';
+
 // 导入全局组件注册函数
 import registerGlobalComponents from './components';
 

+ 9 - 0
src/plugins/svg-icons.ts

@@ -0,0 +1,9 @@
+/**
+ * SVG图标加载器
+ * 该文件用于导入所有SVG图标,使其可以通过svg-sprite-loader生成的sprite使用
+ */
+
+// @ts-nocheck
+// 导入所有SVG图标
+const svgRequire = require.context('../assets/icons', false, /\.svg$/);
+svgRequire.keys().forEach(svgRequire);

+ 14 - 0
src/router/routes/modules/test.ts

@@ -0,0 +1,14 @@
+import { AppRouteRecordRaw } from '../types';
+
+const TEST: AppRouteRecordRaw = {
+  path: '/test',
+  name: 'TEST',
+  component: () => import('@/views/test/index.vue'),
+  meta: {
+    requiresAuth: false,
+    order: 1,
+    title: '测试页面'
+  },
+};
+
+export default TEST; 

+ 1 - 1
src/views/home/components/topAnime.vue

@@ -43,7 +43,7 @@
           }"
         />
       </div>
-      <h1 class="text-5xl md:text-6xl font-bold text-center text-white mb-8 drop-shadow-lg" mt-6>为您提供自然流畅的语音合成服务</h1>
+      <h1 class="text-5xl md:text-6xl font-bold text-center text-white mb-8 drop-shadow-lg" mt-6>拟真级语音 · 多场景适配</h1>
     </Motion>
   </section>
 </template>

+ 51 - 10
src/views/home/index.vue

@@ -9,17 +9,22 @@
     <TopAnime />
 
     <!--功能介绍区-->
-    <section class="h-[100vh] flex items-center w-full relative" id="product">
+    <section class="h-[100vh] flex items-center w-full relative" id="product" ref="productSection">
       <div class="py-20 container mx-auto px-6">
         <h2 class="text-4xl font-bold text-white mb-10">我们的特色</h2>
         <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
           <div
-            v-for="item in products"
+            v-for="(item, index) in products"
             :key="item.title"
-            class="group bg-white/90 rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 hover:border-blue-200 product-card"
+            class="group bg-white/95 rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 hover:border-blue-200 text-left product-card opacity-0 transform translate-y-10"
+            :class="{'animate-fade-in-up': isProductVisible}"
+            :style="{animationDelay: `${index * 0.15}s`}"
           >
-          <div class="text-blue-500 text-3xl mb-4 transition-all duration-300 group-hover:text-blue-600 group-hover:scale-110"></div>
-            <SvgIcon name="reader" type="iconfont" />
+            <div class="text-3xl mb-4 transition-all duration-300 group-hover:text-blue-600 group-hover:scale-110">
+              <SvgIcon :name="item.icon" type="svg" size="2em" color="currentColor" />
+            </div>
+            <h3 class="text-xl font-semibold mb-2 text-gray-800 group-hover:text-blue-600">{{ item.title }}</h3>
+            <p class="text-gray-600 group-hover:text-gray-700">{{ item.description }}</p>
           </div>
         </div>
       </div>
@@ -122,30 +127,47 @@
 import HomeNav from './components/nav.vue';
 import ParallaxBackground from './components/ParallaxBackground.vue';
 import TopAnime from './components/topAnime.vue';
-import { onMounted, onUnmounted } from 'vue';
+import { onMounted, onUnmounted, ref } from 'vue';
+import { useIntersectionObserver } from '@vueuse/core';
+
+// 产品区域的引用
+const productSection = ref(null);
+// 控制产品区域动画的变量
+const isProductVisible = ref(false);
+
+// 使用VueUse的useIntersectionObserver监听元素可见性
+useIntersectionObserver(
+  productSection,
+  ([{ isIntersecting }]) => {
+    if (isIntersecting) {
+      isProductVisible.value = true;
+    }
+  },
+  { threshold: 0.3 } // 当元素30%进入视口时触发
+);
 
 const products = [
   {
     title: '智能语音合成',
     description: '提供自然流畅的语音合成服务,支持多种语言和音色选择。',
-    icon: 'fa-solid fa-robot',
+    icon: 'ai',
   },
   {
     title: '语音克隆',
     description:
       '只需少量语音样本,即可克隆出与原始声音高度相似的个性化语音模型。',
-    icon: 'fa-solid fa-microphone',
+    icon: 'mark',
   },
   {
     title: '语音编辑',
     description:
       '强大的语音编辑工具,可调整语速、音调、情感等参数,满足专业需求。',
-    icon: 'fa-solid fa-sliders',
+    icon: 'reader',
   },
   {
     title: '批量处理',
     description: '支持大规模文本批量转换为语音,提高工作效率,节省时间成本。',
-    icon: 'fa-solid fa-bolt',
+    icon: 'fast',
   },
 ];
 
@@ -169,17 +191,36 @@ onMounted(() => {
 </script>
 <style lang="scss" scoped>
 .home-page {
+  background-color: 000!important;
   /* 组件级别的滚动条隐藏 */
   &::-webkit-scrollbar {
     display: none;
   }
 
   #product {
+    background-color: 000!important;
     .product-card {
+      background-color: 000!important;
       &:hover {
         transform: translateY(-10px) rotate(1deg) scale(1.02);
       }
     }
   }
 }
+
+/* 渐入动画 */
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.animate-fade-in-up {
+  animation: fadeInUp 0.8s ease forwards;
+}
 </style>

+ 68 - 0
src/views/test/index.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="test-container">
+    <h1>测试页面</h1>
+    <p>这是一个用于测试的页面</p>
+    <div class="test-content">
+      <!-- 在这里添加您的测试内容 -->
+      <div class="test-card">
+        <h3>测试组件1</h3>
+        <div class="test-box"></div>
+      </div>
+      <div class="test-card">
+        <h3>测试组件2</h3>
+        <div class="test-box"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+// 在这里添加您的测试代码
+import { ref, onMounted } from 'vue';
+
+const testValue = ref('测试值');
+
+onMounted(() => {
+  console.log('测试页面已加载');
+});
+</script>
+
+<style scoped lang="scss">
+// 引入变量
+@import '@/styles/variables.scss';
+
+.test-container {
+  padding: 20px;
+  
+  h1 {
+    margin-bottom: 20px;
+    color: $primary-color;
+  }
+  
+  .test-content {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+    margin-top: 30px;
+    
+    .test-card {
+      width: 300px;
+      padding: 20px;
+      border-radius: $border-radius;
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+      background-color: #fff;
+      
+      h3 {
+        margin-bottom: 15px;
+        color: #444;
+      }
+      
+      .test-box {
+        height: 150px;
+        background-color: #f5f5f5;
+        border-radius: 6px;
+      }
+    }
+  }
+}
+</style> 

+ 2 - 1
tsconfig.json

@@ -17,7 +17,8 @@
     "baseUrl": ".",
     "paths": {
       "@/*": ["src/*"]
-    }
+    },
+    "typeRoots": ["./node_modules/@types", "./src/types"]
   },
   "include": ["src"],
   "exclude": ["./rspack.*.config.ts", "rspack.config.ts"],