Explorar el Código

更新依赖项,提升@ant-design/icons-vue至7.0.1,添加mitt库,更新@types/qs至6.14.0,移除不再使用的tencentDocApi.ts文件,增强用户登录信息守卫,确保未登录用户重定向至登录页面,导出token和用户信息常量以供其他模块使用。

xbx hace 2 semanas
padre
commit
36d41bca95

+ 4 - 3
package.json

@@ -23,11 +23,12 @@
     ]
   },
   "dependencies": {
-    "@ant-design/icons-vue": "^5.5.0",
-    "ant-design-vue": "^4.2.6",
+    "@ant-design/icons-vue": "^7.0.1",
     "@vueuse/core": "^13.5.0",
+    "ant-design-vue": "^4.2.6",
     "axios": "^1.9.0",
     "dayjs": "^1.11.13",
+    "mitt": "^3.0.1",
     "pinia": "^3.0.3",
     "qs": "^6.14.0",
     "query-string": "^9.2.2",
@@ -38,10 +39,10 @@
     "@rspack/cli": "^1.3.10",
     "@rspack/core": "^1.3.10",
     "@types/node": "^22.15.19",
+    "@types/qs": "^6.14.0",
     "@vue/eslint-config-typescript": "^14.5.0",
     "cross-env": "^7.0.3",
     "css-loader": "^7.1.2",
-    "@types/qs": "^6.14.0",
     "dotenv": "^16.5.0",
     "eslint": "^9.23.0",
     "eslint-config-prettier": "^10.1.5",

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 336 - 935
pnpm-lock.yaml


+ 2 - 0
src/main.ts

@@ -2,8 +2,10 @@ import './style.css';
 import { createApp } from 'vue';
 import App from './App.vue';
 import router from './router';
+import store from './store';
 
 const app = createApp(App);
 
 app.use(router);
+app.use(store);
 app.mount('#app');

+ 9 - 0
src/router/guard/index.ts

@@ -0,0 +1,9 @@
+import setupUserLoginInfoGuard from './userLogin';
+import type { Router,RouteLocationNormalized } from 'vue-router';
+
+import { setRouteEmitter } from '@/utils/router-listen';
+
+export default function setupRouterGuard(router: Router) {
+  setupUserLoginInfoGuard(router);
+  setRouteEmitter(router);
+}

+ 19 - 2
src/router/guard/userLogin.ts

@@ -1,5 +1,22 @@
 import type { Router, LocationQueryRaw } from 'vue-router';
+import { useUserStore } from '@/store/modules/user';
 
 export default function setupUserLoginInfoGuard(router: Router) {
-    
-}
+  router.beforeEach(async (to, from, next) => {
+    const userStore = useUserStore();
+    if (to.meta.requiresAuth && !userStore.isLoggedIn) {
+      if (to.name == 'login') {
+        next();
+        return;
+      } else {
+        next({
+          path: '/login',
+          query: { redirect: to.fullPath } as LocationQueryRaw,
+        });
+        return;
+      }
+    } else {
+      next();
+    }
+  });
+}

+ 31 - 0
src/router/routes/base.ts

@@ -0,0 +1,31 @@
+import type { RouteRecordRaw } from 'vue-router';
+import { REDIRECT_ROUTE_NAME } from '@/router/constants';
+
+export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue');
+
+export const REDIRECT_MAIN: RouteRecordRaw = {
+  path: '/redirect',
+  name: 'redirectWrapper',
+  component: DEFAULT_LAYOUT,
+  meta: {
+    requiresAuth: true,
+    hideInMenu: true,
+  },
+  children: [
+    {
+      path: '/redirect/:path',
+      name: REDIRECT_ROUTE_NAME,
+      component: () => import('@/views/redirect/index.vue'),
+      meta: {
+        requiresAuth: true,
+        hideInMenu: true,
+      },
+    },
+  ],
+};
+
+export const NOT_FOUND_ROUTE: RouteRecordRaw = {
+  path: '/:pathMatch(.*)*',
+  name: 'notFound',
+  component: () => import('@/views/not-found/index.vue'),
+};

+ 21 - 0
src/router/routes/types.ts

@@ -0,0 +1,21 @@
+
+import { defineComponent } from 'vue';
+import type { RouteMeta, NavigationGuard } from 'vue-router';
+
+export type Component<T = any> =
+  | ReturnType<typeof defineComponent>
+  | (() => Promise<typeof import('*.vue')>)
+  | (() => Promise<T>);
+
+export interface AppRouteRecordRaw {
+  path: string;
+  name?: string | symbol;
+  meta?: RouteMeta;
+  redirect?: string;
+  component: Component | string;
+  children?: AppRouteRecordRaw[];
+  alias?: string | string[];
+  props?: Record<string, any>;
+  beforeEnter?: NavigationGuard | NavigationGuard[];
+  fullPath?: string;
+}

+ 22 - 0
src/store/index.ts

@@ -0,0 +1,22 @@
+import { createPinia } from 'pinia';
+import type { App } from 'vue';
+
+// 导入模块
+import userModule from './modules/user';
+
+// 创建 pinia 实例
+const pinia = createPinia();
+
+// 导出 store 实例
+export const store = {
+  // 安装 pinia 插件
+  install(app: App) {
+    app.use(pinia);
+  }
+};
+
+// 导出各个模块的 store
+export const useUserStore = userModule.store;
+
+// 默认导出 store 实例
+export default store; 

+ 156 - 0
src/store/modules/user/index.ts

@@ -0,0 +1,156 @@
+import { defineStore } from 'pinia';
+import type { UserInfo, UserState } from './types';
+import { localStorage } from '@/utils/storage';
+import { TOKEN_KEY, USER_INFO_KEY, getToken, hasToken } from '@/utils/auth';
+
+// 定义用户模块 Store
+export const useUserStore = defineStore('user', {
+  // 状态
+  state: (): UserState => ({
+    userInfo: null,
+    token: null,
+    isLoggedIn: false,
+    permissions: [],
+    roles: []
+  }),
+
+  // Getters
+  getters: {
+    // 获取用户信息
+    getUserInfo(): UserInfo | null {
+      return this.userInfo;
+    },
+    // 判断用户是否登录 - 根据 token 判断
+    getIsLoggedIn(): boolean {
+      // 如果内存中没有 token,则检查本地存储
+      if (!this.token) {
+        // 使用 auth 中的 hasToken 函数检查是否存在 token
+        if (hasToken()) {
+          // 如果存在,则获取 token 并更新状态
+          this.token = getToken();
+          return true;
+        }
+        return false;
+      }
+      return true;
+    },
+    // 获取用户权限
+    getPermissions(): string[] {
+      return this.permissions;
+    },
+    // 获取用户角色
+    getRoles(): string[] {
+      return this.roles;
+    }
+  },
+
+  // Actions
+  actions: {
+    // 设置用户信息
+    setUserInfo(userInfo: UserInfo | null) {
+      this.userInfo = userInfo;
+      this.isLoggedIn = !!userInfo;
+      
+      // 保存到本地存储
+      if (userInfo) {
+        localStorage.set(USER_INFO_KEY, userInfo);
+      } else {
+        localStorage.remove(USER_INFO_KEY);
+      }
+    },
+    // 设置 Token
+    setToken(token: string | null) {
+      this.token = token;
+      
+      // 保存到本地存储
+      if (token) {
+        localStorage.set(TOKEN_KEY, token);
+      } else {
+        localStorage.remove(TOKEN_KEY);
+      }
+    },
+    // 设置权限
+    setPermissions(permissions: string[]) {
+      this.permissions = permissions;
+      
+      // 保存到本地存储 - 更新用户信息对象
+      const userInfo = this.userInfo ? { ...this.userInfo, permissions } : { permissions };
+      this.setUserInfo(userInfo as UserInfo);
+    },
+    // 设置角色
+    setRoles(roles: string[]) {
+      this.roles = roles;
+      
+      // 保存到本地存储 - 更新用户信息对象
+      const userInfo = this.userInfo ? { ...this.userInfo, roles } : { roles };
+      this.setUserInfo(userInfo as UserInfo);
+    },
+    // 登录
+    async login(username: string, password: string) {
+      try {
+        // 这里应该调用实际的登录 API
+        // const { data } = await api.login({ username, password });
+        
+        // 模拟登录成功
+        const mockUserInfo: UserInfo = {
+          id: '1',
+          username,
+          avatar: 'https://example.com/avatar.png'
+        };
+        const mockToken = 'mock-token-' + Date.now();
+        
+        // 保存用户信息和 Token
+        this.setUserInfo(mockUserInfo);
+        this.setToken(mockToken);
+        this.setPermissions(['read', 'write']);
+        this.setRoles(['user']);
+        
+        return { success: true };
+      } catch (error) {
+        return { success: false, error };
+      }
+    },
+    // 登出
+    logout() {
+      this.setUserInfo(null);
+      this.setToken(null);
+      this.setPermissions([]);
+      this.setRoles([]);
+    },
+    // 从本地存储恢复用户状态
+    restoreFromStorage() {
+      try {
+        // 恢复 token
+        const token = getToken();
+        if (token) {
+          this.token = token;
+        }
+        
+        // 恢复用户信息
+        const userInfo = localStorage.get<UserInfo>(USER_INFO_KEY);
+        if (userInfo) {
+          this.userInfo = userInfo;
+          this.isLoggedIn = true;
+        }
+        
+        return {
+          token: this.token,
+          userInfo: this.userInfo,
+          isLoggedIn: this.isLoggedIn
+        };
+      } catch (error) {
+        console.error('Failed to restore user state from storage', error);
+        return { error };
+      }
+    }
+  }
+});
+
+// 在创建 store 时自动从本地存储恢复状态
+const userStore = useUserStore();
+userStore.restoreFromStorage();
+
+// 导出默认模块
+export default {
+  store: useUserStore
+}; 

+ 19 - 0
src/store/modules/user/types.ts

@@ -0,0 +1,19 @@
+// 用户模块类型定义
+
+// 用户信息接口
+export interface UserInfo {
+  id?: string | number;
+  username?: string;
+  avatar?: string;
+  email?: string;
+  // 根据需要添加更多字段
+}
+
+// 用户模块状态接口
+export interface UserState {
+  userInfo: UserInfo | null;
+  token: string | null;
+  isLoggedIn: boolean;
+  permissions: string[];
+  roles: string[];
+} 

+ 15 - 0
src/store/types.ts

@@ -0,0 +1,15 @@
+// store 全局类型定义
+import type { UserState } from './modules/user/types';
+
+// 定义根状态类型
+export interface RootState {
+  user: UserState;
+  // 其他模块状态可以在这里添加
+}
+
+// 定义 Store 模块接口
+export interface StoreModule<T> {
+  state: T;
+  getters?: Record<string, Function>;
+  actions?: Record<string, Function>;
+} 

+ 2 - 2
src/utils/auth.ts

@@ -4,8 +4,8 @@
 import { localStorage } from './storage';
 
 // Token 相关的键名
-const TOKEN_KEY = 'user_token';
-const USER_INFO_KEY = 'user_info';
+export const TOKEN_KEY = 'user_token';
+export const USER_INFO_KEY = 'user_info';
 
 /**
  * 设置用户 token

+ 31 - 0
src/utils/router-listen.ts

@@ -0,0 +1,31 @@
+/**
+ * Listening to routes alone would waste rendering performance. Use the publish-subscribe model for distribution management
+ * 单独监听路由会浪费渲染性能。使用发布订阅模式去进行分发管理。
+ */
+import mitt, { Handler } from 'mitt';
+import type { Router,RouteLocationNormalized } from 'vue-router';
+
+const emitter = mitt();
+
+const key = Symbol('ROUTE_CHANGE');
+
+let latestRoute: Router;
+
+export function setRouteEmitter(to: Router) {
+  emitter.emit(key, to);
+  latestRoute = to;
+}
+
+export function listenerRouteChange(
+  handler: (route: Router) => void,
+  immediate = true
+) {
+  emitter.on(key, handler as Handler);
+  if (immediate && latestRoute) {
+    handler(latestRoute);
+  }
+}
+
+export function removeRouteListener() {
+  emitter.off(key);
+}

+ 0 - 180
src/utils/tencentDocApi.ts

@@ -1,180 +0,0 @@
-import axios from 'axios';
-
-/**
- * 腾讯文档API交互工具类
- * 基于腾讯文档的非官方API实现数据获取
- */
-export interface TencentDocConfig {
-  cookie: string;
-  documentUrl: string;
-}
-
-export interface OperationResult {
-  operationId: string;
-  status: boolean;
-  message?: string;
-}
-
-export interface ProgressResult {
-  progress: number;
-  file_url?: string;
-  message?: string;
-}
-
-/**
- * 从URL中提取文档ID
- * @param documentUrl 腾讯文档URL
- * @returns 文档ID
- */
-export function extractDocumentId(documentUrl: string): string {
-  // 处理类似 https://docs.qq.com/sheet/DQVZpaFVqdnJFU3hn 的URL
-  const urlParts = documentUrl.split('/');
-  const lastPart = urlParts[urlParts.length - 1].split('?')[0];
-  return lastPart;
-}
-
-/**
- * 腾讯文档API客户端
- */
-export class TencentDocClient {
-  private cookie: string;
-  private documentId: string;
-  private documentUrl: string;
-
-  constructor(config: TencentDocConfig) {
-    this.cookie = config.cookie;
-    this.documentUrl = config.documentUrl;
-    this.documentId = extractDocumentId(config.documentUrl);
-  }
-
-  /**
-   * 创建导出任务
-   * @returns 操作结果,包含operationId
-   */
-  async createExportTask(): Promise<OperationResult> {
-    try {
-      const exportUrl = 'https://docs.qq.com/v1/export/export_office';
-      
-      const requestData = {
-        docId: this.documentId,
-        version: '2',
-      };
-      
-      const headers = {
-        'content-type': 'application/x-www-form-urlencoded',
-        'Cookie': this.cookie,
-        'Referer': this.documentUrl
-      };
-      
-      const response = await axios.post(exportUrl, requestData, { headers });
-      
-      if (response.data && response.data.operationId) {
-        return {
-          operationId: response.data.operationId,
-          status: true
-        };
-      } else {
-        throw new Error('获取operationId失败');
-      }
-    } catch (error) {
-      console.error('创建导出任务失败:', error);
-      return {
-        operationId: '',
-        status: false,
-        message: error instanceof Error ? error.message : '未知错误'
-      };
-    }
-  }
-
-  /**
-   * 查询导出进度
-   * @param operationId 操作ID
-   * @returns 进度结果
-   */
-  async queryExportProgress(operationId: string): Promise<ProgressResult> {
-    try {
-      const progressUrl = `https://docs.qq.com/v1/export/query_progress?operationId=${operationId}`;
-      
-      const headers = {
-        'Cookie': this.cookie,
-        'Referer': this.documentUrl
-      };
-      
-      const response = await axios.get(progressUrl, { headers });
-      
-      if (response.data) {
-        return {
-          progress: response.data.progress || 0,
-          file_url: response.data.file_url
-        };
-      } else {
-        throw new Error('查询导出进度失败');
-      }
-    } catch (error) {
-      console.error('查询导出进度失败:', error);
-      return {
-        progress: 0,
-        message: error instanceof Error ? error.message : '未知错误'
-      };
-    }
-  }
-
-  /**
-   * 下载导出的文件
-   * @param fileUrl 文件URL
-   * @returns 文件的二进制数据
-   */
-  async downloadExportedFile(fileUrl: string): Promise<ArrayBuffer> {
-    try {
-      const response = await axios.get(fileUrl, {
-        responseType: 'arraybuffer',
-        headers: {
-          'Cookie': this.cookie
-        }
-      });
-      
-      return response.data;
-    } catch (error) {
-      console.error('下载文件失败:', error);
-      throw error;
-    }
-  }
-
-  /**
-   * 完整的导出流程
-   * @param maxRetries 最大重试次数
-   * @param retryInterval 重试间隔(毫秒)
-   * @returns 文件的二进制数据
-   */
-  async exportDocument(maxRetries = 10, retryInterval = 1000): Promise<ArrayBuffer> {
-    // 创建导出任务
-    const operationResult = await this.createExportTask();
-    if (!operationResult.status) {
-      throw new Error(`创建导出任务失败: ${operationResult.message}`);
-    }
-
-    const operationId = operationResult.operationId;
-    let retries = 0;
-    let fileUrl = '';
-
-    // 轮询查询进度
-    while (retries < maxRetries) {
-      const progressResult = await this.queryExportProgress(operationId);
-      
-      if (progressResult.progress === 100 && progressResult.file_url) {
-        fileUrl = progressResult.file_url;
-        break;
-      }
-      
-      retries++;
-      await new Promise(resolve => setTimeout(resolve, retryInterval));
-    }
-
-    if (!fileUrl) {
-      throw new Error('导出超时或未获取到文件下载地址');
-    }
-
-    // 下载文件
-    return await this.downloadExportedFile(fileUrl);
-  }
-}