diff --git a/components.d.ts b/components.d.ts index f9dea66..11af8c2 100644 --- a/components.d.ts +++ b/components.d.ts @@ -11,6 +11,7 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + CardItem: typeof import('./src/components/cardItem/index.vue')['default'] ElAside: typeof import('element-plus/es')['ElAside'] ElButton: typeof import('element-plus/es')['ElButton'] ElContainer: typeof import('element-plus/es')['ElContainer'] @@ -18,14 +19,27 @@ declare module 'vue' { ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElHeader: typeof import('element-plus/es')['ElHeader'] + ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] ElMain: typeof import('element-plus/es')['ElMain'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] + ElTree: typeof import('element-plus/es')['ElTree'] PageForm: typeof import('./src/components/pageForm/index.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + StageBreadcrumbs: typeof import('./src/components/stageBreadcrumbs/index.vue')['default'] + StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default'] + StandMenu: typeof import('./src/components/standMenu/index.vue')['default'] } } diff --git a/src/api/index.ts b/src/api/index.ts index 76196c4..8487b86 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -3,4 +3,19 @@ import request from '@/request'; // 设置请求的参数信息 export const getUserList = (params?: any) => { return request.get('/api/user/list', params); +}; + +// 获取路由菜单数据 +export const getRouteMenus = () => { + return request.get('/api/menus'); +}; + +// 登录接口 +export const login = (data: { username: string; password: string }) => { + return request.post('/api/auth/login', data); +}; + +// 获取用户信息 +export const getUserInfo = () => { + return request.get('/api/user/info'); }; \ No newline at end of file diff --git a/src/components/cardItem/index.vue b/src/components/cardItem/index.vue new file mode 100644 index 0000000..9dc1aed --- /dev/null +++ b/src/components/cardItem/index.vue @@ -0,0 +1,128 @@ + + + \ No newline at end of file diff --git a/src/components/stageBreadcrumbs/index.vue b/src/components/stageBreadcrumbs/index.vue new file mode 100644 index 0000000..5380644 --- /dev/null +++ b/src/components/stageBreadcrumbs/index.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/components/standardMenu/index.vue b/src/components/standardMenu/index.vue new file mode 100644 index 0000000..8bfc138 --- /dev/null +++ b/src/components/standardMenu/index.vue @@ -0,0 +1,127 @@ + + + \ No newline at end of file diff --git a/src/hooks/useCopy.ts b/src/hooks/useCopy.ts new file mode 100644 index 0000000..eb8dfd9 --- /dev/null +++ b/src/hooks/useCopy.ts @@ -0,0 +1,239 @@ +import { ref } from 'vue' +import { ElMessage } from 'element-plus' + +interface UseCopyOptions { + /** + * 是否显示成功提示 + * @default true + */ + showSuccessMessage?: boolean + /** + * 是否显示失败提示 + * @default true + */ + showErrorMessage?: boolean + /** + * 成功提示文案 + * @default '复制成功' + */ + successMessage?: string + /** + * 失败提示文案 + * @default '复制失败,请手动复制' + */ + errorMessage?: string + /** + * 成功提示持续时间(毫秒) + * @default 2000 + */ + successDuration?: number + /** + * 失败提示持续时间(毫秒) + * @default 3000 + */ + errorDuration?: number +} + +/** + * 复制文本到剪贴板的 hooks + * @param options 配置选项 + * @returns 复制函数和状态 + */ +export default function useCopy(options: UseCopyOptions = {}) { + const { + showSuccessMessage = true, + showErrorMessage = true, + successMessage = '复制成功', + errorMessage = '复制失败,请手动复制', + successDuration = 2000, + errorDuration = 3000, + } = options + + // 复制状态 + const isCopying = ref(false) + const lastCopiedText = ref('') + + /** + * 使用 Clipboard API 复制(现代浏览器) + */ + const copyWithClipboardAPI = async (text: string): Promise => { + try { + // 检查是否支持 Clipboard API + if (!navigator.clipboard || !navigator.clipboard.writeText) { + return false + } + + await navigator.clipboard.writeText(text) + return true + } catch (error) { + console.error('Clipboard API copy failed:', error) + return false + } + } + + /** + * 使用 execCommand 复制(兼容旧浏览器) + */ + const copyWithExecCommand = (text: string): boolean => { + try { + // 创建一个临时的 textarea 元素 + const textarea = document.createElement('textarea') + textarea.value = text + textarea.style.position = 'fixed' + textarea.style.left = '-9999px' + textarea.style.top = '-9999px' + textarea.style.opacity = '0' + textarea.setAttribute('readonly', 'readonly') + + document.body.appendChild(textarea) + + // 选中文本 + textarea.select() + textarea.setSelectionRange(0, text.length) // 兼容移动端 + + // 执行复制命令 + const successful = document.execCommand('copy') + + // 清理临时元素 + document.body.removeChild(textarea) + + return successful + } catch (error) { + console.error('execCommand copy failed:', error) + return false + } + } + + /** + * 复制文本到剪贴板 + * @param text 要复制的文本 + * @returns Promise 是否复制成功 + */ + const copy = async (text: string): Promise => { + if (!text) { + if (showErrorMessage) { + ElMessage({ + message: '复制内容不能为空', + type: 'warning', + duration: errorDuration, + }) + } + return false + } + + isCopying.value = true + lastCopiedText.value = text + + try { + // 优先使用 Clipboard API + let success = await copyWithClipboardAPI(text) + + // 如果 Clipboard API 失败,降级到 execCommand + if (!success) { + success = copyWithExecCommand(text) + } + + if (success) { + if (showSuccessMessage) { + ElMessage({ + message: successMessage, + type: 'success', + duration: successDuration, + }) + } + return true + } else { + // 两种方法都失败,尝试最后的方法:创建一个选择区域 + const fallbackSuccess = fallbackCopy(text) + if (!fallbackSuccess && showErrorMessage) { + ElMessage({ + message: errorMessage, + type: 'error', + duration: errorDuration, + }) + } + return fallbackSuccess + } + } catch (error) { + console.error('Copy failed:', error) + if (showErrorMessage) { + ElMessage({ + message: errorMessage, + type: 'error', + duration: errorDuration, + }) + } + return false + } finally { + isCopying.value = false + } + } + + /** + * 备用复制方法:创建选择区域 + */ + const fallbackCopy = (text: string): boolean => { + try { + const range = document.createRange() + const selection = window.getSelection() + + if (!selection) { + return false + } + + // 创建一个临时元素 + const tempDiv = document.createElement('div') + tempDiv.textContent = text + tempDiv.style.position = 'fixed' + tempDiv.style.left = '-9999px' + tempDiv.style.top = '-9999px' + + document.body.appendChild(tempDiv) + + range.selectNodeContents(tempDiv) + selection.removeAllRanges() + selection.addRange(range) + + const successful = document.execCommand('copy') + + selection.removeAllRanges() + document.body.removeChild(tempDiv) + + return successful + } catch (error) { + console.error('Fallback copy failed:', error) + return false + } + } + + /** + * 复制当前选中的文本 + */ + const copySelection = async (): Promise => { + try { + const selectedText = window.getSelection()?.toString() || '' + if (!selectedText) { + if (showErrorMessage) { + ElMessage({ + message: '请先选择要复制的内容', + type: 'warning', + duration: errorDuration, + }) + } + return false + } + return await copy(selectedText) + } catch (error) { + console.error('Copy selection failed:', error) + return false + } + } + + return { + copy, + copySelection, + isCopying, + lastCopiedText, + } +} + diff --git a/src/mock/auth.ts b/src/mock/auth.ts new file mode 100644 index 0000000..712af6f --- /dev/null +++ b/src/mock/auth.ts @@ -0,0 +1,52 @@ +/** + * 登录相关 Mock 数据 + */ + +/** + * Mock 登录响应 + */ +export const getMockLoginResponse = (username: string, password: string) => { + // 模拟登录验证 + if (username && password) { + return { + code: 0, + data: { + accessToken: 'mock_access_token_' + Date.now(), + refreshToken: 'mock_refresh_token_' + Date.now(), + expiresIn: 7200, // 2小时 + userInfo: { + id: 1, + username: username, + name: username === 'admin' ? '管理员' : '普通用户', + role: username === 'admin' ? 'admin' : 'user', + avatar: '', + }, + }, + msg: '登录成功', + } + } else { + return { + code: 400, + data: null, + msg: '用户名或密码不能为空', + } + } +} + +/** + * Mock 用户信息响应 + */ +export const getMockUserInfoResponse = () => { + return { + code: 0, + data: { + id: 1, + username: 'admin', + name: '管理员', + role: 'admin', + avatar: '', + }, + msg: '获取用户信息成功', + } +} + diff --git a/src/mock/index.ts b/src/mock/index.ts new file mode 100644 index 0000000..f01e7ff --- /dev/null +++ b/src/mock/index.ts @@ -0,0 +1,97 @@ +/** + * Mock 数据管理 + * 用于开发阶段模拟后端接口返回的数据 + */ + +import { getMockMenuResponse } from './menu' +import { getMockLoginResponse, getMockUserInfoResponse } from './auth' + +// 是否启用 Mock(可以通过环境变量控制) +export const ENABLE_MOCK = import.meta.env.VITE_USE_MOCK === 'true' || import.meta.env.DEV + +/** + * Mock API 响应映射 + * key: API 路径 + * value: 返回的 Mock 数据函数 + */ +const mockApiMap: Record any> = { + '/api/menus': () => getMockMenuResponse(), + '/api/auth/login': (_params?: any, data?: any) => { + return getMockLoginResponse(data?.username || '', data?.password || '') + }, + '/api/user/info': () => getMockUserInfoResponse(), + // 可以在这里添加更多的 mock 接口 + // '/api/user/list': getMockUserList, +} + +/** + * 获取 Mock 数据 + * @param url API 路径 + * @param params 请求参数(GET) + * @param data 请求体(POST/PUT) + * @returns Mock 数据或 null + */ +export const getMockData = (url: string, params?: any, data?: any): any => { + if (!ENABLE_MOCK || !url) { + return null + } + + // 精确匹配 + if (mockApiMap[url]) { + return mockApiMap[url](params, data) + } + + // 模糊匹配(支持带查询参数的 URL) + const urlWithoutQuery = url.split('?')[0] + if (urlWithoutQuery && mockApiMap[urlWithoutQuery]) { + return mockApiMap[urlWithoutQuery](params, data) + } + + return null +} + +/** + * 检查是否应该使用 Mock 数据 + * @param url API 路径 + * @returns 是否应该使用 Mock + */ +export const shouldUseMock = (url: string): boolean => { + if (!ENABLE_MOCK || !url) { + return false + } + + // 精确匹配 + if (mockApiMap[url]) { + return true + } + + // 模糊匹配(支持带查询参数的 URL) + const urlWithoutQuery = url.split('?')[0] + return !!(urlWithoutQuery && mockApiMap[urlWithoutQuery]) +} + +/** + * 添加 Mock 接口 + * @param url API 路径 + * @param mockFn Mock 数据函数 + */ +export const addMockApi = (url: string, mockFn: (params?: any, data?: any) => any) => { + mockApiMap[url] = mockFn +} + +/** + * 移除 Mock 接口 + * @param url API 路径 + */ +export const removeMockApi = (url: string) => { + delete mockApiMap[url] +} + +export default { + getMockData, + shouldUseMock, + addMockApi, + removeMockApi, + ENABLE_MOCK, +} + diff --git a/src/mock/menu.ts b/src/mock/menu.ts new file mode 100644 index 0000000..b963ba9 --- /dev/null +++ b/src/mock/menu.ts @@ -0,0 +1,309 @@ +/** + * 菜单路由 Mock 数据 + * 用于开发阶段模拟后端返回的菜单数据 + */ + +export interface MockMenuRoute { + path: string; + name?: string; + component?: string; + meta?: { + title?: string; + icon?: string; + requiresAuth?: boolean; + roles?: string[]; + [key: string]: any; + }; + children?: MockMenuRoute[]; +} + +/** + * Mock 菜单数据 + * 根据实际后端接口返回的数据格式进行配置 + */ +export const mockMenuData: MockMenuRoute[] = [ + { + path: "/home", + name: "Home", + component: "Home", // 对应 @/pages/Home/index.vue + meta: { + title: "首页", + icon: "HomeFilled", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + { + name: "business", + path: "/business", + meta: { + title: "商机管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + children: [ + { + name: "clue", + path: "clue", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "线索管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + + }, + { + name: "customer", + path: "customer", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "客户管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + { + name: "studio", + path: "studio", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "游戏与工作室", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + { + name: "businessmanage", + path: "businessmanage", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "商机管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + ], + }, + { + name: "contract", + path: "/contract", + meta: { + title: "合同管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + children: [ + { + name: "income", + path: "income", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "收入合同", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + ], + }, + { + name: "project", + path: "/project", + meta:{ + title: "项目管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + children: [ + { + name: "requirement", + path: "requirement", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "需求管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + { + name: "projectmanage", + path: "projectmanage", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "项目管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + { + name: "task", + path: "task", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "任务管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + { + name: "work", + path: "work", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "工时管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + ], + }, + { + name: "team", + path: "/team", + meta:{ + title: "团队管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + children: [], + }, + { + name: "recruit", + path: "/recruit", + meta:{ + title: "招聘管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + children: [ + { + name: "resume", + path: "resume", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "流程管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + { + name: "push", + path: "push", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "推送管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + + }, + { + name: "job", + path: "job", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "主场岗位", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + ], + }, + { + name: "dashboard", + title: "dashboard", + path: "/stage", + meta: { + title: "后台管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + children: [ + { + path: "flow", + name: "flow", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "流程管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + { + name: "organization", + component: "@/pages/StageManage/organization/index.vue", + path: "organization", + meta: { + title: "组织管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + { + name: "personnel", + path: "personnel", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "人员管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + }, + }, + { + name: "permission", + path: "permission", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "权限管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + } + }, + { + name: "dict", + path: "dict", + component: "@/pages/StageManage/organization/index.vue", + meta: { + title: "字典管理", + icon: "", + requiresAuth: true, + roles: ["admin", "user"], + } + }, + ], + }, +]; + +/** + * 获取 Mock 菜单数据的响应格式 + * 模拟后端接口返回的数据结构 + */ +export const getMockMenuResponse = () => { + return { + code: 0, + data: mockMenuData, + msg: "获取菜单成功", + }; +}; diff --git a/src/pages/Home/index.vue b/src/pages/Home/index.vue index 255f81c..1b0dac5 100644 --- a/src/pages/Home/index.vue +++ b/src/pages/Home/index.vue @@ -6,7 +6,7 @@ diff --git a/src/pages/Login/index.vue b/src/pages/Login/index.vue index dcc3f3b..2195681 100644 --- a/src/pages/Login/index.vue +++ b/src/pages/Login/index.vue @@ -1,14 +1,167 @@ + - + + diff --git a/src/pages/StageManage/organization/index.vue b/src/pages/StageManage/organization/index.vue new file mode 100644 index 0000000..9faa05a --- /dev/null +++ b/src/pages/StageManage/organization/index.vue @@ -0,0 +1,182 @@ + + + diff --git a/src/request/index.ts b/src/request/index.ts index e057c9b..2306a5f 100644 --- a/src/request/index.ts +++ b/src/request/index.ts @@ -3,6 +3,7 @@ import type {AxiosInstance, AxiosRequestConfig, AxiosResponse} from 'axios'; import useTokenRefresh from "@/hooks/useTokenRefresh"; import 'element-plus/es/components/notification/style/css' import { ElNotification } from 'element-plus' +import { getMockData, shouldUseMock } from '@/mock' //mock数据信息 const baseUrl = import.meta.env.VITE_APP_BASE_API; @@ -72,7 +73,6 @@ class HttpRequest { instance.interceptors.response.use( async (response: AxiosResponse) => { const { data: responseData, status } = response; - const originalRequest = response.config as AxiosRequestConfig & { _retry?: boolean }; switch (responseData.code) { case 0: @@ -193,6 +193,24 @@ class HttpRequest { // 通用请求方法 public request(config: AxiosRequestConfig): Promise> { + // 检查是否应该使用 Mock 数据 + const requestUrl = config.url || ''; + + // 优先使用请求 URL(通常是相对路径,如 /api/menus) + // 这样可以直接匹配 mockApiMap 中的 key + if (shouldUseMock(requestUrl)) { + const mockData = getMockData(requestUrl, config.params, config.data); + if (mockData) { + console.log(`[Mock] 使用 Mock 数据: ${requestUrl}`, mockData); + // 模拟网络延迟 + return new Promise((resolve) => { + setTimeout(() => { + resolve(mockData as ApiResponse); + }, 100); + }); + } + } + const instance = this.getInstance(config); return instance(config).catch((error) => { // 统一错误处理 diff --git a/src/router/index.ts b/src/router/index.ts index 7d2c0e3..d1629b1 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,16 +1,204 @@ -import { createWebHistory, createRouter } from 'vue-router' +import { createWebHistory, createRouter, type RouteRecordRaw } from 'vue-router' +import { useUserStore } from '@/store' +import { getRouteMenus } from '@/api' +import useTokenRefresh from '@/hooks/useTokenRefresh' import Login from '@/pages/Login/index.vue'; import HomeView from '@/pages/Layout/index.vue'; -const routes = [ - { path: '/', component: HomeView }, - { path: '/login', component: Login }, + +const baseUrl = import.meta.env.VITE_APP_BASE_API || ''; + +// 基础路由(不需要权限验证) +const constantRoutes: RouteRecordRaw[] = [ + { + path: '/login', + name: 'Login', + component: Login, + meta: { + title: '登录', + requiresAuth: false + } + }, +] + +// 动态路由(需要权限验证) +const asyncRoutes: RouteRecordRaw[] = [ + { + path: '/', + name: 'Layout', + component: HomeView, + redirect: '/home', + meta: { + requiresAuth: true, + }, + children: [] as RouteRecordRaw[], + }, ] const router = createRouter({ history: createWebHistory(), - routes, + routes: [...constantRoutes, ...asyncRoutes], }) +// 白名单路由(不需要登录即可访问) +const whiteList = ['/login'] + +// 1. 预先加载所有可能的页面组件 +// 这里的路径模式要覆盖到你所有的业务组件 +const modules = import.meta.glob('@/pages/**/*.vue') + +const loadComponent = (componentPath: string) => { + if (!componentPath) return null + + // 统一路径格式处理 + let fullPath = '' + + if (componentPath.startsWith('@/')) { + fullPath = componentPath + } else if (componentPath.includes('/')) { + // 补全路径,确保以 @/pages 开头 + fullPath = componentPath.startsWith('pages/') ? `@/${componentPath}` : `@/pages/${componentPath}` + } else { + // 补全 index.vue + fullPath = `@/pages/${componentPath}/index.vue` + } + + // 重点:将 @/ 转换为 /src/,因为 glob 默认生成的 key 是相对于项目根目录的 + const key = fullPath.replace('@/', '/src/') + + // 从 modules 中查找对应的导入函数 + if (modules[key]) { + return modules[key] + } else { + console.error(`未找到组件文件: ${key}. 请检查路径或大小写。`) + return null + } +} + +// 将后端返回的路由数据转换为 Vue Router 路由 +const transformRoutes = (routes: any[]): RouteRecordRaw[] => { + return routes.map((route) => { + const component = route.component ? loadComponent(route.component) : undefined + // 构建基础路由对象 + const routeRecord: any = { + path: route.path, + name: route.name || route.path, + meta: { + title: route.meta?.title || route.title || route.name, + icon: route.meta?.icon || route.icon, + requiresAuth: route.meta?.requiresAuth !== false, // 默认需要权限 + roles: route.meta?.roles || route.roles, + ...route.meta, + }, + } + + // 如果有组件,添加组件属性 + if (component) { + routeRecord.component = component + } + + // 处理子路由 + if (route.children && route.children.length > 0) { + routeRecord.children = transformRoutes(route.children) + } + + return routeRecord as RouteRecordRaw + }) +} + +// 添加动态路由 +const addDynamicRoutes = async () => { + const userStore = useUserStore() + + // 如果路由已加载,直接返回 + if (userStore.isRoutesLoaded) { + return + } + + try { + // 从后端获取路由菜单数据 + const response = await getRouteMenus() + if (response.code === 0 && response.data) { + // 转换路由数据 + const dynamicRoutes = transformRoutes(Array.isArray(response.data) ? response.data : [response.data]) + // 将动态路由添加到 Layout 的 children 中 + const layoutRoute = router.getRoutes().find(route => route.name === 'Layout') + + console.log('Layout route:', layoutRoute,dynamicRoutes) + if (layoutRoute) { + dynamicRoutes.forEach(route => { + router.addRoute('Layout', route) + }) + } else { + // 如果找不到 Layout 路由,直接添加到根路由 + dynamicRoutes.forEach(route => { + router.addRoute(route) + }) + } + + // 保存路由数据到 store + userStore.setRoutes(response.data) + + // 标记路由已加载 + userStore.isRoutesLoaded = true + } + } catch (error) { + console.error('Failed to load routes:', error) + // 如果获取路由失败,清除用户数据并跳转到登录页 + userStore.clearUserData() + router.push('/login') + } +} + +// 路由导航守卫 +router.beforeEach(async (to, _from, next) => { + const userStore = useUserStore() + const { getAccessToken } = useTokenRefresh(baseUrl) + + // 获取 token + const token = getAccessToken() || userStore.token + + // 如果已登录,更新 store 中的 token + if (token) { + userStore.setToken(token) + } + + // 判断是否在白名单中 + if (whiteList.includes(to.path)) { + // 如果在白名单中且已登录,重定向到首页 + if (token) { + next('/') + } else { + next() + } + return + } + + // 需要登录验证 + if (!token) { + // 未登录,重定向到登录页 + next({ + path: '/login', + query: { redirect: to.fullPath }, // 保存当前路径,登录后可以跳转回来 + }) + return + } + + // 已登录,检查路由是否已加载 + if (!userStore.isRoutesLoaded) { + try { + // 加载动态路由 + await addDynamicRoutes() + // 路由加载完成后,重新导航到目标路由 + next({ ...to, replace: true }) + } catch (error) { + console.error('Route loading error:', error) + next('/login') + } + } else { + // 路由已加载,直接放行 + next() + } +}) export default router \ No newline at end of file diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 260d936..03a7d1c 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -1,12 +1,63 @@ import { defineStore } from "pinia"; +import useTokenRefresh from "@/hooks/useTokenRefresh"; + +const baseUrl = import.meta.env.VITE_APP_BASE_API || ""; + +interface UserInfo { + name?: string; + role?: string; + [key: string]: any; +} + +interface RouteMenu { + path: string; + name?: string; + component?: string; + meta?: { + title?: string; + icon?: string; + requiresAuth?: boolean; + roles?: string[]; + [key: string]: any; + }; + children?: RouteMenu[]; +} const useUserStore = defineStore("user", { state: () => { + const { getAccessToken } = useTokenRefresh(baseUrl); return { - name: "user", + token: getAccessToken() || "", + userInfo: {} as UserInfo, + routes: [] as RouteMenu[], + isRoutesLoaded: false, // 标记路由是否已加载 }; }, + getters: { + isLoggedIn: (state) => { + return !!state.token; + }, + }, + actions: { + setToken(token: string) { + this.token = token; + }, + setUserInfo(userInfo: UserInfo) { + this.userInfo = userInfo; + }, + setRoutes(routes: RouteMenu[]) { + this.routes = routes; + this.isRoutesLoaded = true; + }, + clearUserData() { + this.token = ""; + this.userInfo = {}; + this.routes = []; + this.isRoutesLoaded = false; + const { clearTokens } = useTokenRefresh(baseUrl); + clearTokens(); + }, + }, }); - export default useUserStore; \ No newline at end of file diff --git a/src/styles/common.scss b/src/styles/common.scss index caa723c..6fb9da3 100644 --- a/src/styles/common.scss +++ b/src/styles/common.scss @@ -1,4 +1,5 @@ @use './element.scss' as *; +@use './stage.scss' as *; html,body{ height: 100%; @@ -8,4 +9,14 @@ html,body{ #app{ height: inherit; -} \ No newline at end of file +} + + +:root{ + --mj-menu-header-height:#{$mj-menu-header-height}; + --mj-border-color:#{$mj-border-color}; + --mj-padding-standard:#{$mj-padding-standard}; +} + + + diff --git a/src/styles/element.scss b/src/styles/element.scss index 68adf99..298e17a 100644 --- a/src/styles/element.scss +++ b/src/styles/element.scss @@ -38,4 +38,11 @@ .el-dialog__footer{ padding: var(--el-dialog-inset-padding-primary); } +} + + +// 全局重新element相关样式 +.el-input{ + --el-border-radius-base:10px; + --el-border-color:#E2E8F0; } \ No newline at end of file diff --git a/src/styles/element/index.scss b/src/styles/element/index.scss index 8b13789..4af72b9 100644 --- a/src/styles/element/index.scss +++ b/src/styles/element/index.scss @@ -1 +1,5 @@ +$mj-menu-header-height:60px; +$mj-border-color:#e5e7eb; +$mj-padding-standard:16px; + diff --git a/src/styles/stage.scss b/src/styles/stage.scss new file mode 100644 index 0000000..5703cee --- /dev/null +++ b/src/styles/stage.scss @@ -0,0 +1,30 @@ + + +.mj-panel-title{ + font-size: 15px; + font-weight: bold; + color:#1D293D; + margin-bottom: 16px; + &::before{ + content:""; + width: 4px; + height: 16px; + background-color: #155DFC; + display: inline-block; + vertical-align: middle; + border-radius: 3px; + margin-right: 8px; + margin-bottom: 2px; + } +} + +.mj-panel_header{ + height: 54px; + padding: 0 24px; + box-sizing: border-box; + border-bottom: 1px solid #F1F5F9; + .el-tabs{ + --el-tabs-header-height:54px; + --el-border-color-light:transparent; + } +} \ No newline at end of file diff --git a/src/utils/download.ts b/src/utils/download.ts new file mode 100644 index 0000000..efedbd0 --- /dev/null +++ b/src/utils/download.ts @@ -0,0 +1 @@ +// 下载数据模块 \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json index 8d16e42..e05b1af 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -10,7 +10,10 @@ "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["src/*"] + } }, "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] }