389 lines
15 KiB
TypeScript
389 lines
15 KiB
TypeScript
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
|