Files
GERP/src/router/index.ts
T

409 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: 'workorder/detail/:id',
name: 'WorkorderDetail',
component: () => import('@/views/workorder/WorkOrderDetail.vue'),
meta: { title: '工单详情', requiresAuth: 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/msync-servers',
name: 'SystemMSyncServers',
component: () => import('@/views/system/MSyncServers.vue'),
meta: { title: '服务器管理', requiresAuth: 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
}
}
// 已登录但 CheckAdmin 未加载(强刷后从 localStorage 缓存恢复会话的路径):
// 补发 fetchAdminCheck,确保 adminChecked 置位、isAdmin 正确。
// 页面组件据此区分"加载中"与"确实无权限"(如 Database/DbTable 等管理员页)。
// 幂等:登录流程/restoreSession 已调用过则 adminChecked=true,这里不再重复请求。
if (userStore.isLoggedIn && !userStore.adminChecked) {
void userStore.fetchAdminCheck()
}
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