diff --git a/components.d.ts b/components.d.ts index 2ac19fe..ce1a001 100644 --- a/components.d.ts +++ b/components.d.ts @@ -12,12 +12,19 @@ export {} declare module 'vue' { export interface GlobalComponents { CardItem: typeof import('./src/components/cardItem/index.vue')['default'] + CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default'] ElAside: typeof import('element-plus/es')['ElAside'] + ElAvatar: typeof import('element-plus/es')['ElAvatar'] + ElBadge: typeof import('element-plus/es')['ElBadge'] ElButton: typeof import('element-plus/es')['ElButton'] + ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElCol: typeof import('element-plus/es')['ElCol'] ElContainer: typeof import('element-plus/es')['ElContainer'] ElDatePick: typeof import('element-plus/es')['ElDatePick'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] + ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] + ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] @@ -32,11 +39,20 @@ declare module 'vue' { ElMain: typeof import('element-plus/es')['ElMain'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElPopover: typeof import('element-plus/es')['ElPopover'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElRow: typeof import('element-plus/es')['ElRow'] 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'] + ElTag: typeof import('element-plus/es')['ElTag'] + ElTimeline: typeof import('element-plus/es')['ElTimeline'] + ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTree: typeof import('element-plus/es')['ElTree'] + OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default'] PageForm: typeof import('./src/components/pageForm/index.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/config.js b/config.js index 214efe0..a006adf 100644 --- a/config.js +++ b/config.js @@ -1,3 +1,9 @@ +/** + * 项目配置文件 + * VITE_PROJECT_PREFIX 项目前缀,默认是'/' + * VITE_PUBLIC_PATH 当前项目的basePath默认是'/' + * VITE_APP_BASE_API 请求接口地址域名 + * */ export const VITE_PROJECT_PREFIX = '/'; -export const VITE_PUBLIC_PATH = './'; -export const VITE_APP_BASE_API = 'http://api.test.com'; \ No newline at end of file +export const VITE_PUBLIC_PATH = '/'; +export const VITE_APP_BASE_API = 'https://mversion-dev.zzmjart.com/api' //'http://192.168.42.106'; \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts index 8487b86..e95d9b5 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,18 +1,17 @@ import request from '@/request'; -// 设置请求的参数信息 -export const getUserList = (params?: any) => { - return request.get('/api/user/list', params); -}; - // 获取路由菜单数据 export const getRouteMenus = () => { - return request.get('/api/menus'); + return request.get('/auth/v1/backend/menu'); }; // 登录接口 export const login = (data: { username: string; password: string }) => { - return request.post('/api/auth/login', data); + return request.post('/auth/oauth2/token', data,{ + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); }; // 获取用户信息 diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png new file mode 100644 index 0000000..7ca3ee0 Binary files /dev/null and b/src/assets/images/logo.png differ diff --git a/src/components/commonFilter/index.vue b/src/components/commonFilter/index.vue new file mode 100644 index 0000000..d70c4bf --- /dev/null +++ b/src/components/commonFilter/index.vue @@ -0,0 +1,70 @@ + + + + + \ No newline at end of file diff --git a/src/components/overflowTabs/index.vue b/src/components/overflowTabs/index.vue new file mode 100644 index 0000000..a28ef09 --- /dev/null +++ b/src/components/overflowTabs/index.vue @@ -0,0 +1,220 @@ + + + + + \ No newline at end of file diff --git a/src/components/stageBreadcrumbs/index.vue b/src/components/stageBreadcrumbs/index.vue index d89d084..bcda1ae 100644 --- a/src/components/stageBreadcrumbs/index.vue +++ b/src/components/stageBreadcrumbs/index.vue @@ -26,10 +26,13 @@ const { title } = defineProps<{ justify-content: space-between; :deep(.mj-panel-title){ margin-bottom: 0; + flex-shrink: 0; } .stage-breadcrumbs-content{ flex: 1; + display: flex; + align-items: center; &::before{ content:''; display: inline-block; diff --git a/src/mock/menu.ts b/src/mock/menu.ts index b963ba9..6abacd1 100644 --- a/src/mock/menu.ts +++ b/src/mock/menu.ts @@ -46,7 +46,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "clue", path: "clue", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackBackstageManage/organization/index.vue", meta: { title: "线索管理", icon: "", @@ -58,7 +58,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "customer", path: "customer", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "客户管理", icon: "", @@ -69,7 +69,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "studio", path: "studio", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "游戏与工作室", icon: "", @@ -80,7 +80,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "businessmanage", path: "businessmanage", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "商机管理", icon: "", @@ -103,7 +103,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "income", path: "income", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "收入合同", icon: "", @@ -126,7 +126,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "requirement", path: "requirement", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "需求管理", icon: "", @@ -137,7 +137,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "projectmanage", path: "projectmanage", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "项目管理", icon: "", @@ -148,7 +148,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "task", path: "task", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "任务管理", icon: "", @@ -159,7 +159,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "work", path: "work", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "工时管理", icon: "", @@ -193,7 +193,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "resume", path: "resume", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "流程管理", icon: "", @@ -204,7 +204,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "push", path: "push", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "推送管理", icon: "", @@ -216,7 +216,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "job", path: "job", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "主场岗位", icon: "", @@ -240,7 +240,7 @@ export const mockMenuData: MockMenuRoute[] = [ { path: "flow", name: "flow", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "流程管理", icon: "", @@ -250,7 +250,7 @@ export const mockMenuData: MockMenuRoute[] = [ }, { name: "organization", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", path: "organization", meta: { title: "组织管理", @@ -262,7 +262,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "personnel", path: "personnel", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "人员管理", icon: "", @@ -273,7 +273,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "permission", path: "permission", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/organization/index.vue", meta: { title: "权限管理", icon: "", @@ -284,7 +284,7 @@ export const mockMenuData: MockMenuRoute[] = [ { name: "dict", path: "dict", - component: "@/pages/StageManage/organization/index.vue", + component: "@/pages/BackstageManage/dict/index.vue", meta: { title: "字典管理", icon: "", diff --git a/src/pages/Layout/index.vue b/src/pages/Layout/index.vue index bd7ef05..3d08a19 100644 --- a/src/pages/Layout/index.vue +++ b/src/pages/Layout/index.vue @@ -4,7 +4,9 @@
-
+
{{ topTitle }} @@ -25,12 +27,15 @@ + + + @@ -44,6 +49,8 @@ import mjMenus from "@/components/standardMenu/index.vue"; import { useUserStore } from "@/store"; import { DArrowLeft, DArrowRight } from "@element-plus/icons-vue"; +import rightMenuGroup from './rightMenuGroup.vue'; +import companyLogo from '@/assets/images/logo.png'; defineOptions({ name: "Layout", }); @@ -212,10 +219,18 @@ onUnmounted(() => { flex-direction: column; border-right: 1px solid var(--mj-border-color); - .mj-aside-company { + .mj-aside-logo { height: var(--mj-menu-header-height); + line-height: var(--mj-menu-header-height); border-bottom: 1px solid var(--mj-border-color); flex-shrink: 0; + .mj-company-logo{ + width: 39px; + height: 32px; + display: inline-block; + vertical-align: middle; + margin-left: 20px; + } } .mj-aside-menu { @@ -245,6 +260,14 @@ onUnmounted(() => { } } + .mj-header-content{ + display: flex; + justify-content: space-between; + .mj-standard-menu{ + flex: 1; + } + } + .mj-header-content { --el-header-padding: 0; border-bottom: 1px solid var(--mj-border-color); diff --git a/src/pages/Layout/rightMenuGroup.vue b/src/pages/Layout/rightMenuGroup.vue new file mode 100644 index 0000000..0052ebf --- /dev/null +++ b/src/pages/Layout/rightMenuGroup.vue @@ -0,0 +1,197 @@ + + + diff --git a/src/pages/Login/index.vue b/src/pages/Login/index.vue index ad4ddce..4e02361 100644 --- a/src/pages/Login/index.vue +++ b/src/pages/Login/index.vue @@ -52,9 +52,6 @@
-
-
- - - - -
+ + + + + + + +
@@ -52,13 +56,26 @@ + \ No newline at end of file diff --git a/src/pages/stage/personnel/index.vue b/src/pages/stage/personnel/index.vue new file mode 100644 index 0000000..aaa49fc --- /dev/null +++ b/src/pages/stage/personnel/index.vue @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/src/request/index.ts b/src/request/index.ts index 061dc2f..43fad20 100644 --- a/src/request/index.ts +++ b/src/request/index.ts @@ -1,15 +1,20 @@ import axios from "axios"; -import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; -import { ElNotification } from 'element-plus'; -import { getMockData, shouldUseMock } from '@/mock' //mock数据信息 - - -const baseUrl = import.meta.env.VITE_APP_BASE_API; +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; +import { ElNotification } from "element-plus"; +import { VITE_APP_BASE_API } from "../../config.js"; +import TokenManager from "@/utils/storage"; +import { getMockData, shouldUseMock } from "@/mock"; //mock数据信息 +const tokenManager = TokenManager.getInstance(); +const baseUrl = "/api"; //TODO: 本地调试需要修改为/api // 1. 锁和队列定义在类外部,确保全局唯一 let isRefreshing = false; let requestsQueue: Array<(token: string) => void> = []; +// 登录接口 传递参数不一样 +const AUTH_OAUTH2_TOKEN_URL = "/auth/oauth2/token"; +const CLIENT_CREDENTIALS = "Basic " + window.btoa("mversion:secret"); + class HttpRequest { private instance: AxiosInstance; @@ -24,16 +29,24 @@ class HttpRequest { } // --- Token 管理方法 --- - private getAccessToken() { return localStorage.getItem("accessToken"); } - private getRefreshToken() { return localStorage.getItem("refreshToken"); } + private getAccessToken() { + return tokenManager.getToken("accessToken"); + } + private getRefreshToken() { + return tokenManager.getToken("refreshToken"); + } private clearTokens() { - localStorage.removeItem("accessToken"); - localStorage.removeItem("refreshToken"); + tokenManager.clearStorage(); // 这里可以触发跳转登录逻辑,例如:router.push('/login') } private setTokens(data: any) { - localStorage.setItem("accessToken", data.accessToken); - localStorage.setItem("refreshToken", data.refreshToken); + tokenManager.setToken("accessToken", data.accessToken); + tokenManager.setToken("refreshToken", data.refreshToken); + } + + // 判断是否为认证接口 + private isAuthEndpoint(url: string): boolean { + return url === AUTH_OAUTH2_TOKEN_URL; } private setupInterceptors() { @@ -41,7 +54,11 @@ class HttpRequest { this.instance.interceptors.request.use( (config) => { const token = this.getAccessToken(); - if (token && config.headers) { + + // 如果login接口传递clientId参数 + if (this.isAuthEndpoint(config.url || "")) { + config.headers.Authorization = CLIENT_CREDENTIALS; + } else if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } return config; @@ -54,12 +71,16 @@ class HttpRequest { async (response: AxiosResponse) => { const { data: res, config: originalRequest } = response; + console.log("响应拦截器",res,originalRequest) + // TODO:如果是登录接口就不要走全局的拦截 而是直接返回当前的数据信息 + if (this.isAuthEndpoint(originalRequest.url || "")) { + return res; + } // 业务成功直接返回数据 - if (res.code === 0) return res; + if (res.code === 200) return res.data; - // 重点:401 未授权处理 + // 401 未授权处理 if (res.code === 401) { - // 如果已经在刷新中了,将请求挂起并加入队列 if (isRefreshing) { return new Promise((resolve) => { @@ -73,30 +94,32 @@ class HttpRequest { // 开始刷新流程 isRefreshing = true; const rToken = this.getRefreshToken(); - + if (!rToken) { this.clearTokens(); return Promise.reject(res); } try { - // 使用一个干净的 axios 实例去发刷新请求,避免拦截器死循环 - const refreshRes = await axios.post(`${this.instance.defaults.baseURL}/auth/refresh`, { - refreshToken: rToken - }); - + // TODO:使用一个干净的 axios 实例去发刷新请求,避免拦截器死循环 + const refreshRes = await axios.post( + `${this.instance.defaults.baseURL}/auth/refresh`, + { + refreshToken: rToken, + } + ); + const newToken = refreshRes.data.data.accessToken; this.setTokens(refreshRes.data.data); // 刷新成功:释放队列 - requestsQueue.forEach(callback => callback(newToken)); + requestsQueue.forEach((callback) => callback(newToken)); requestsQueue = []; isRefreshing = false; // 重试本次请求 originalRequest.headers!.Authorization = `Bearer ${newToken}`; return this.instance(originalRequest); - } catch (error) { // 刷新失败:清理并彻底报错 isRefreshing = false; @@ -107,7 +130,7 @@ class HttpRequest { } // 其它业务错误 - ElNotification.error({ title: '提示', message: res.msg || '服务异常' }); + ElNotification.error({ title: "提示", message: res.msg || "服务异常" }); return Promise.reject(res); }, (error) => { @@ -121,63 +144,75 @@ 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); - }); - } - } - + // TODO:检查是否应该使用 Mock 数据 + // const requestUrl = config.url || ''; + // 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); + // }); + // } + // } + return this.instance(config) as unknown as Promise>; } - // GET 请求 - public get(url: string, params?: any, config?: AxiosRequestConfig): Promise> { - return this.request({ - url, - method: 'get', - params, - ...config + // GET 请求 + public get( + url: string, + params?: any, + config?: AxiosRequestConfig + ): Promise> { + return this.request({ + url, + method: "get", + params, + ...config, }); } // POST 请求 - public post(url: string, data?: any, config?: AxiosRequestConfig): Promise> { - return this.request({ - url, - method: 'post', - data, - ...config + public post( + url: string, + data?: any, + config?: AxiosRequestConfig + ): Promise> { + return this.request({ + url, + method: "post", + data, + ...config, }); } // PUT 请求 - public put(url: string, data?: any, config?: AxiosRequestConfig): Promise> { - return this.request({ - url, - method: 'put', - data, - ...config + public put( + url: string, + data?: any, + config?: AxiosRequestConfig + ): Promise> { + return this.request({ + url, + method: "put", + data, + ...config, }); } // DELETE 请求 - public delete(url: string, config?: AxiosRequestConfig): Promise> { - return this.request({ - url, - method: 'delete', - ...config + public delete( + url: string, + config?: AxiosRequestConfig + ): Promise> { + return this.request({ + url, + method: "delete", + ...config, }); } } @@ -186,13 +221,13 @@ const httpRequest = new HttpRequest(baseUrl); // 导出方法 export const request = { - get: (url: string, params?: any, config?: AxiosRequestConfig) => + get: (url: string, params?: any, config?: AxiosRequestConfig) => httpRequest.get(url, params, config), - post: (url: string, data?: any, config?: AxiosRequestConfig) => + post: (url: string, data?: any, config?: AxiosRequestConfig) => httpRequest.post(url, data, config), - put: (url: string, data?: any, config?: AxiosRequestConfig) => + put: (url: string, data?: any, config?: AxiosRequestConfig) => httpRequest.put(url, data, config), - delete: (url: string, config?: AxiosRequestConfig) => + delete: (url: string, config?: AxiosRequestConfig) => httpRequest.delete(url, config), }; -export default httpRequest; \ No newline at end of file +export default httpRequest; diff --git a/src/router/index.ts b/src/router/index.ts index d1629b1..032af76 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,13 +1,12 @@ import { createWebHistory, createRouter, type RouteRecordRaw } from 'vue-router' import { useUserStore } from '@/store' import { getRouteMenus } from '@/api' -import useTokenRefresh from '@/hooks/useTokenRefresh' +import TokenManager from '@/utils/storage'; import Login from '@/pages/Login/index.vue'; import HomeView from '@/pages/Layout/index.vue'; -const baseUrl = import.meta.env.VITE_APP_BASE_API || ''; - +const tokenManager = TokenManager.getInstance(); // 基础路由(不需要权限验证) const constantRoutes: RouteRecordRaw[] = [ { @@ -27,7 +26,7 @@ const asyncRoutes: RouteRecordRaw[] = [ path: '/', name: 'Layout', component: HomeView, - redirect: '/home', + // redirect: '/home', meta: { requiresAuth: true, }, @@ -57,7 +56,7 @@ const loadComponent = (componentPath: string) => { fullPath = componentPath } else if (componentPath.includes('/')) { // 补全路径,确保以 @/pages 开头 - fullPath = componentPath.startsWith('pages/') ? `@/${componentPath}` : `@/pages/${componentPath}` + fullPath = componentPath.startsWith('pages/') ? `@/${componentPath}` : `@/pages/${componentPath}/index.vue` } else { // 补全 index.vue fullPath = `@/pages/${componentPath}/index.vue` @@ -76,34 +75,35 @@ const loadComponent = (componentPath: string) => { } // 将后端返回的路由数据转换为 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 - } - - // 处理子路由 +const transformRoutes = (routes: any[], parentCode: string = ''): RouteRecordRaw[] => { + return routes.flatMap((route) => { + const fullCode = parentCode ? `${parentCode}/${route.code}` : route.code; + + // 如果当前路由有子路由,说明它是一个路由前缀,不需要组件 if (route.children && route.children.length > 0) { - routeRecord.children = transformRoutes(route.children) - } + // 将子路由的路径加上当前路由的前缀,然后递归处理 + return transformRoutes(route.children, fullCode); + } else { + // 叶子节点才需要组件和路由配置 + const component = fullCode ? loadComponent(fullCode) : undefined; + + const routeRecord: any = { + path: route.code, + name: route.code, + meta: { + title: route.name, + icon: route.icon, + ...route.meta, + }, + } - return routeRecord as RouteRecordRaw - }) + if (component) { + routeRecord.component = component; + } + + return routeRecord as RouteRecordRaw; + } + }); } // 添加动态路由 @@ -116,15 +116,50 @@ const addDynamicRoutes = async () => { } try { - // 从后端获取路由菜单数据 - const response = await getRouteMenus() - if (response.code === 0 && response.data) { + // TODO:从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单) + let response:any; + if (userStore.isBackendUser) { + const backendResponse = await getRouteMenus(); + response = [{ + code: 'stage', + name: '后台管理', + icon: '', + children: backendResponse, + }]; + }else{ + // response = await getUserMenus(); + response = []; + } + if (response) { + const processRoutes = (routes: any[], prefix: string = ''): RouteRecordRaw[] => { + return routes.flatMap(route => { + const currentPath = prefix ? `${prefix}/${route.code}` : route.code; + + if (route.children && route.children.length > 0) { + // 如果有子路由,递归处理并添加当前路径作为前缀 + return processRoutes(route.children, currentPath); + } else { + // 叶子节点,创建路由记录 + const component = loadComponent(currentPath); + return { + path: route.code, + name: route.code, + component: component || HomeView, // 使用Layout的组件 + meta: { + title: route.name, + icon: route.icon, + ...route.meta, + } + } as RouteRecordRaw; + } + }); + }; // 转换路由数据 - const dynamicRoutes = transformRoutes(Array.isArray(response.data) ? response.data : [response.data]) + // const dynamicRoutes = transformRoutes(Array.isArray(response) ? response : [response]) + const dynamicRoutes = processRoutes(Array.isArray(response) ? response : [response]) // 将动态路由添加到 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) @@ -135,15 +170,14 @@ const addDynamicRoutes = async () => { router.addRoute(route) }) } - + console.log('Layout route:', router.getRoutes()) // 保存路由数据到 store - userStore.setRoutes(response.data) + userStore.setRoutes(response) // 标记路由已加载 userStore.isRoutesLoaded = true } } catch (error) { - console.error('Failed to load routes:', error) // 如果获取路由失败,清除用户数据并跳转到登录页 userStore.clearUserData() router.push('/login') @@ -153,10 +187,10 @@ const addDynamicRoutes = async () => { // 路由导航守卫 router.beforeEach(async (to, _from, next) => { const userStore = useUserStore() - const { getAccessToken } = useTokenRefresh(baseUrl) + const accessToken = tokenManager.getToken('accessToken'); // 获取 token - const token = getAccessToken() || userStore.token + const token = accessToken || userStore.token // 如果已登录,更新 store 中的 token if (token) { diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 03a7d1c..2425b07 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -1,7 +1,7 @@ import { defineStore } from "pinia"; -import useTokenRefresh from "@/hooks/useTokenRefresh"; +import TokenManager from '@/utils/storage'; +const tokenManager = TokenManager.getInstance(); -const baseUrl = import.meta.env.VITE_APP_BASE_API || ""; interface UserInfo { name?: string; @@ -25,12 +25,14 @@ interface RouteMenu { const useUserStore = defineStore("user", { state: () => { - const { getAccessToken } = useTokenRefresh(baseUrl); + const accessToken = tokenManager.getToken('accessToken'); + const userInfo = tokenManager.getToken('userInfo'); return { - token: getAccessToken() || "", - userInfo: {} as UserInfo, + token: accessToken || "", + userInfo: userInfo ? JSON.parse(userInfo) : {}, routes: [] as RouteMenu[], isRoutesLoaded: false, // 标记路由是否已加载 + isBackendUser:true, //标记是否是后台用户 }; }, getters: { @@ -54,8 +56,7 @@ const useUserStore = defineStore("user", { this.userInfo = {}; this.routes = []; this.isRoutesLoaded = false; - const { clearTokens } = useTokenRefresh(baseUrl); - clearTokens(); + tokenManager.clearStorage(); }, }, }); diff --git a/src/styles/common.scss b/src/styles/common.scss index 6fb9da3..0b106ba 100644 --- a/src/styles/common.scss +++ b/src/styles/common.scss @@ -19,4 +19,19 @@ html,body{ } +.filter-popper.el-popover.el-popper{ + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); +} + +// 全局重新element相关样式 +.mj-input-form{ + .el-input{ + --el-border-radius-base:10px; + --el-border-color:#E2E8F0; + } +} + + diff --git a/src/styles/element.scss b/src/styles/element.scss index 298e17a..4858def 100644 --- a/src/styles/element.scss +++ b/src/styles/element.scss @@ -23,11 +23,19 @@ // 标注弹窗样式 .standard-ui-dialog{ - --el-dialog-padding-primary:0; - --el-dialog-inset-padding-primary:16px; + &.el-dialog{ + --el-dialog-inset-padding-primary:16px; + --el-dialog-padding-primary:0; + --el-dialog-border-radius:16px; + --el-dialog-bg-header-footer:#FBFCFD; + --el-dialog-border-header-footer-color:#E5E7EB; + padding: var(--el-dialog-padding-primary); + } .el-dialog__header{ - border-bottom: 1px solid #E5E7EB; + border-bottom: 1px solid var(--el-dialog-border-header-footer-color); + background-color:var(--el-dialog-bg-header-footer); padding: var(--el-dialog-inset-padding-primary); + border-radius: var(--el-dialog-border-radius) var(--el-dialog-border-radius) 0 0; } .el-dialog__headerbtn{ height: 60px; @@ -37,12 +45,8 @@ } .el-dialog__footer{ padding: var(--el-dialog-inset-padding-primary); + background-color: var(--el-dialog-bg-header-footer); + border-top: 1px solid var(--el-dialog-border-header-footer-color); + border-radius: 0 0 var(--el-dialog-border-radius) var(--el-dialog-border-radius); } -} - - -// 全局重新element相关样式 -.el-input{ - --el-border-radius-base:10px; - --el-border-color:#E2E8F0; } \ No newline at end of file diff --git a/src/styles/stage.scss b/src/styles/stage.scss index 5703cee..bdf31c8 100644 --- a/src/styles/stage.scss +++ b/src/styles/stage.scss @@ -1,30 +1,35 @@ - - -.mj-panel-title{ +.mj-panel-title { font-size: 15px; font-weight: bold; - color:#1D293D; + color: #1D293D; + display: flex; + align-items: center; margin-bottom: 16px; - &::before{ - content:""; + + &::before { + content: ""; width: 4px; height: 16px; background-color: #155DFC; - display: inline-block; - vertical-align: middle; - border-radius: 3px; + border-radius: 2px; 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; - } +.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; + } +} + +// 自定义组件中overflow-tabs高亮样式 +.is-active-item-overflow-tabs { + color: #409eff; + font-weight: bold; } \ No newline at end of file diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..b459f19 --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,33 @@ +class TokenManager { + private static instance: TokenManager | null = null; + private storage: Storage; + + private constructor(storageType: 'localStorage' | 'sessionStorage' = 'localStorage') { + this.storage = storageType === 'localStorage' ? localStorage : sessionStorage; + } + + public static getInstance(storageType: 'localStorage' | 'sessionStorage' = 'localStorage'): TokenManager { + if (!TokenManager.instance) { + TokenManager.instance = new TokenManager(storageType); + } + return TokenManager.instance; + } + + setToken(key: string, token: string): void { + this.storage.setItem(key, token); + } + + getToken(key: string): string | null { + return this.storage.getItem(key); + } + + removeToken(key: string): void { + this.storage.removeItem(key); + } + clearStorage(): void { + this.storage.clear(); + } +} + + +export default TokenManager; \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index cfe9845..0702ad4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -44,7 +44,7 @@ export default defineConfig(({ mode }) => { "/api": { target: VITE_APP_BASE_API, changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ""), + rewrite: (path) => path.replace(/^\/api/, "") }, }, },