import { nextTick } from 'vue' import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' import { useProgressStore } from '@/stores/progress' import { useTabsStore } from '@/stores/tabs' /** 静态路由(无需权限) */ export const staticRoutes: RouteRecordRaw[] = [ { path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), meta: { title: '登录', requiresAuth: false, layout: 'blank' }, }, { path: '/', component: () => import('@/layouts/DefaultLayout.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', name: 'Dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '仪表盘', icon: 'Odometer', requiresAuth: true }, }, { path: 'profile', name: 'Profile', component: () => import('@/views/Profile.vue'), meta: { title: '个人中心', icon: 'User', requiresAuth: true }, }, // === 阶段 1:CrudTable 落地验证 === { path: 'workorder/admin', name: 'WorkorderAdmin', component: () => import('@/views/workorder/Admin.vue'), meta: { title: '工单管理', icon: 'Tickets', requiresAuth: true, menuGuarded: true }, }, { path: 'workorder/my', name: 'WorkorderMy', component: () => import('@/views/workorder/My.vue'), meta: { title: '我的工单', icon: 'Document', requiresAuth: true, menuGuarded: true }, }, { path: 'system/roles', name: 'SystemRoles', component: () => import('@/views/system/Roles.vue'), meta: { title: '角色管理', icon: 'UserFilled', requiresAuth: true }, }, { path: 'system/users', name: 'SystemUsers', component: () => import('@/views/system/Users.vue'), meta: { title: '用户管理', icon: 'User', requiresAuth: true }, }, { path: 'inventory/products', name: 'InventoryProducts', component: () => import('@/views/inventory/Products.vue'), meta: { title: '产品管理', icon: 'Goods', requiresAuth: true, menuGuarded: true }, }, { path: 'sales/orders', name: 'SalesOrders', component: () => import('@/views/sales/Orders.vue'), meta: { title: '销售订单', icon: 'ShoppingCart', requiresAuth: true, menuGuarded: true }, }, // === 阶段 2:Company 列表页 === { path: 'company/tenant', name: 'CompanyTenant', component: () => import('@/views/company/Tenant.vue'), meta: { title: '租户管理', icon: 'OfficeBuilding', requiresAuth: true, menuGuarded: true }, }, { path: 'company/company', name: 'CompanyCompany', component: () => import('@/views/company/Company.vue'), meta: { title: '公司管理', icon: 'Management', requiresAuth: true, menuGuarded: true }, }, { path: 'company/department', name: 'CompanyDepartment', component: () => import('@/views/company/Department.vue'), meta: { title: '部门管理', icon: 'Share', requiresAuth: true, menuGuarded: true }, }, { path: 'company/workgroup', name: 'CompanyWorkgroup', component: () => import('@/views/company/Workgroup.vue'), meta: { title: '工作组管理', icon: 'UserFilled', requiresAuth: true, menuGuarded: true }, }, { path: 'company/role', name: 'CompanyRole', component: () => import('@/views/company/Role.vue'), meta: { title: '公司角色', icon: 'Avatar', requiresAuth: true, menuGuarded: true }, }, // === 阶段 3:详情页 === { path: 'system/user-detail', name: 'SystemUserDetail', component: () => import('@/views/system/UserDetail.vue'), meta: { title: '用户详情', requiresAuth: true }, }, { path: 'system/role-detail', name: 'SystemRoleDetail', component: () => import('@/views/system/RoleDetail.vue'), meta: { title: '角色详情', requiresAuth: true }, }, { path: 'system/role-permissions', name: 'SystemRolePermissions', component: () => import('@/views/system/RolePermissions.vue'), meta: { title: '权限分配', requiresAuth: true }, }, { path: 'company/tenant-detail', name: 'CompanyTenantDetail', component: () => import('@/views/company/TenantDetail.vue'), meta: { title: '租户详情', requiresAuth: true }, }, { path: 'company/workgroup-detail', name: 'CompanyWorkgroupDetail', component: () => import('@/views/company/WorkgroupDetail.vue'), meta: { title: '工作组详情', requiresAuth: true }, }, { path: 'company/department-detail', name: 'CompanyDepartmentDetail', component: () => import('@/views/company/DepartmentDetail.vue'), meta: { title: '部门详情', requiresAuth: true }, }, // === 阶段 4:复杂详情页 === { path: 'company/company-detail', name: 'CompanyCompanyDetail', component: () => import('@/views/company/CompanyDetail.vue'), meta: { title: '公司详情', requiresAuth: true }, }, { path: 'company/role-detail', name: 'CompanyRoleDetail', component: () => import('@/views/company/RoleDetail.vue'), meta: { title: '公司角色详情', requiresAuth: true }, }, { path: 'company/employee-detail', name: 'CompanyEmployeeDetail', component: () => import('@/views/company/EmployeeDetail.vue'), meta: { title: '员工详情', requiresAuth: true }, }, // === 阶段 5:业务功能页 === { path: 'system/message', name: 'SystemMessage', component: () => import('@/views/system/Message.vue'), meta: { title: '站内信', icon: 'ChatDotRound', requiresAuth: true, menuGuarded: true }, }, { path: 'system/tenant-create', name: 'SystemTenantCreate', component: () => import('@/views/system/TenantCreate.vue'), meta: { title: '租户入驻', requiresAuth: true, menuGuarded: true }, }, { path: 'system/tenant-review', name: 'SystemTenantReview', component: () => import('@/views/system/TenantReview.vue'), meta: { title: '租户审核', requiresAuth: true, menuGuarded: true }, }, { path: 'system/payment-config', name: 'SystemPaymentConfig', component: () => import('@/views/system/PaymentConfig.vue'), meta: { title: '支付配置', requiresAuth: true }, }, { path: 'company/epcreate', name: 'CompanyEpCreate', component: () => import('@/views/company/EpCreate.vue'), meta: { title: '企业创建向导', requiresAuth: true }, }, // === 阶段 6:独立工具 === { path: 'doc/editor', name: 'DocEditor', component: () => import('@/views/doc/Editor.vue'), meta: { title: '文档编辑器', requiresAuth: true, menuGuarded: true }, }, { path: 'assets/manager', name: 'AssetsManager', component: () => import('@/views/assets/Manager.vue'), meta: { title: '资产管理', requiresAuth: true, menuGuarded: true }, }, // === 阶段 7:管理员工具 === { path: 'system/audit-log', name: 'SystemAuditLog', component: () => import('@/views/system/AuditLog.vue'), meta: { title: '审计日志', requiresAuth: true }, }, { path: 'system/settings', name: 'SystemSettings', component: () => import('@/views/system/Settings.vue'), meta: { title: '系统设置', requiresAuth: true }, }, { path: 'system/menu', name: 'SystemMenu', component: () => import('@/views/system/Menu.vue'), meta: { title: '菜单管理', requiresAuth: true, menuGuarded: true }, }, { path: 'system/database', name: 'SystemDatabase', component: () => import('@/views/system/Database.vue'), meta: { title: '数据库管理', requiresAuth: true, menuGuarded: true }, }, { path: 'system/dbtable', name: 'SystemDbTable', component: () => import('@/views/system/DbTable.vue'), meta: { title: '表结构管理', requiresAuth: true }, }, // === 枢机协律(CreateE):旧页面 iframe 直载(LegacyPage) === { path: 'private/CreateE/shop', name: 'PrivateCreateEShop', component: () => import('@/views/LegacyPage.vue'), meta: { title: '商店', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/shop.html' }, }, { path: 'private/CreateE/Recharge', name: 'PrivateCreateERecharge', component: () => import('@/views/LegacyPage.vue'), meta: { title: '储值充值', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/Recharge.html' }, }, { path: 'private/CreateE/wallet', name: 'PrivateCreateEWallet', component: () => import('@/views/LegacyPage.vue'), meta: { title: '钱包', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/wallet.html' }, }, { path: 'private/CreateE/enterprise', name: 'PrivateCreateEEnterprise', component: () => import('@/views/LegacyPage.vue'), meta: { title: '企业管理', requiresAuth: true, legacySrc: '/pages/private/CreateE/enterprise.html' }, }, { path: 'private/CreateE/StoreValue', name: 'PrivateCreateEStoreValue', component: () => import('@/views/LegacyPage.vue'), meta: { title: '储值查看', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/StoreValue.html' }, }, // === 阶段 8:SKU(移动端 C 端,待决策是否拆分独立工程) === { path: 'sku', name: 'Sku', component: () => import('@/views/SKU/Sku.vue'), meta: { title: '商品详情', requiresAuth: false }, }, // === 兜底:未匹配路由在 DefaultLayout 内显示 404(保留 menu/nav/tab) === // 作为 DefaultLayout 子路由,享有完整框架;requiresAuth: true 使其自动开 tab { path: ':pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/NotFound.vue'), meta: { title: '页面不存在', requiresAuth: true }, }, ], }, ] /** 动态路由(从后端菜单加载后注入) */ export const dynamicRoutes: RouteRecordRaw[] = [] const router = createRouter({ // hash 模式:URL 形如 gerp.gvsds.com/#/dashboard // 适配 frp 内网穿透部署,无需后端 rewrite 配置,避免刷新 404 history: createWebHashHistory(), routes: staticRoutes, scrollBehavior(_to, _from, savedPosition) { return savedPosition || { top: 0 } }, }) /** 标记本次导航是否为首次打开的新 tab(用于 afterEach 区分 loading 策略) */ let justOpenedNewTab = false /** 全局前置守卫:进度条启动、立即开 tab、登录态校验、动态路由注入 */ router.beforeEach(async (to, _from, next) => { const tabsStore = useTabsStore() justOpenedNewTab = false // === 菜单守卫(先于 addTab,避免受限页 tab 误入标签栏)=== // menuGuarded 路由(后端菜单驱动的业务页)必须出现在当前用户可见菜单树中。 // 用途:URL 直达私有页(如 #/private/CreateE/wallet)时,若菜单树不含该路径 // (当前角色公司无权),直接跳回 dashboard,阻止访问公司私有业务。 // 注意:不能 next({name:'NotFound'})——catch-all 保留原路径会再次触发守卫, // 造成导航无限循环 + tab 反复增删(栈溢出)。直接 next('/dashboard') 使 // URL/tabs/路由三方一致,Dashboard 只挂载一次,无循环。 // menus 未加载(首次进入/未登录)时放行,交由下方 restoreSession 处理登录与菜单。 const userStore = useUserStore() if (to.meta.requiresAuth === true && to.meta.menuGuarded && userStore.menus.length > 0 && !userStore.hasMenuPath(to.path)) { useProgressStore().done() ElMessage.warning('无权访问该页面') next('/dashboard') return } // 立即添加 tab:与 loading 同步出现,不等登录检查或组件加载 // addTab 返回 true=新 tab(首次打开,需 loading),false=已存在(已缓存,瞬时切换) if (to.meta.requiresAuth === true && to.meta.title) { justOpenedNewTab = tabsStore.addTab(to) } // 设置页面标题 document.title = (to.meta.title as string) || 'GERP 管理系统' // progress 策略: // - 新 tab 或非 tab 页面(login 等):启动 loading(首次挂载组件 + 加载数据) // - 已缓存 tab:不显示 loading(瞬时 v-show 切换,组件已挂载,零请求) if (justOpenedNewTab || to.meta.requiresAuth !== true) { useProgressStore().start() } else { useProgressStore().done() } // 登录页:若已处于登录状态(内存态或 Cookie 会话可恢复),跳转面板 if (to.name === 'Login') { if (userStore.isLoggedIn) { next({ path: (to.query.redirect as string) || '/dashboard', replace: true }) return } // 尝试从 Cookie 会话静默恢复登录态:成功则跳面板,失败则停留登录页 if (await userStore.restoreSession()) { next({ path: (to.query.redirect as string) || '/dashboard', replace: true }) } else { next() } return } // 不需要登录的页面直接放行 if (to.meta.requiresAuth === false) { next() return } // 已登录但未加载用户信息:尝试静默恢复会话 if (!userStore.isLoggedIn) { // 使用 restoreSession(silent 模式):session 过期时不弹"登录状态已失效"提示 // 直接跳转登录页,避免 login 页面出现错误弹窗 if (await userStore.restoreSession()) { // 恢复成功,继续导航 } else { // 恢复失败:移除乐观添加的 tab,跳转登录 tabsStore.removeTab(to.path) next({ name: 'Login', query: { redirect: to.fullPath } }) return } } next() }) /** 全局后置钩子:根据新 tab/已缓存 tab 采取不同 loading 收尾策略 */ router.afterEach(async () => { // nextTick 确保新组件已挂载到 DOM await nextTick() if (justOpenedNewTab) { // 新 tab:等页面组件 onMounted 主动 done(),500ms 兜底防止遗漏 useProgressStore().setFallbackTimeout(500) } else { // 已缓存 tab(瞬时 v-show 切换)或非 tab 页面:立即隐藏 loading useProgressStore().done() } }) export default router