index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. import Vue from 'vue'
  2. import VueRouter from 'vue-router'
  3. import Layout from '@/views/Layout.vue'
  4. import axios from 'axios'
  5. import { getEffectivePermissions, hasPermission } from '@/utils/permissions'
  6. Vue.use(VueRouter)
  7. const routes = [
  8. {
  9. path: '/login',
  10. name: 'Login',
  11. component: () => import('@/views/Login.vue')
  12. },
  13. {
  14. path: '/403',
  15. name: 'Forbidden',
  16. component: { template: '<div style="text-align:center;padding:100px"><h1>403</h1><p>权限不足</p><router-link to="/">返回首页</router-link></div>' },
  17. meta: { title: '权限不足' }
  18. },
  19. {
  20. path: '/',
  21. component: Layout,
  22. redirect: '/dashboard',
  23. children: [
  24. {
  25. path: 'dashboard',
  26. name: 'Dashboard',
  27. component: () => import('@/views/Dashboard.vue'),
  28. meta: { title: '首页', perm: 'dashboard' }
  29. },
  30. // 家庭管理
  31. {
  32. path: 'families',
  33. name: 'Families',
  34. component: () => import('@/views/Families.vue'),
  35. meta: { title: '家庭列表', perm: 'family:list' }
  36. },
  37. {
  38. path: 'children',
  39. name: 'Children',
  40. component: () => import('@/views/Children.vue'),
  41. meta: { title: '孩子管理', perm: 'family:children' }
  42. },
  43. {
  44. path: 'points',
  45. name: 'Points',
  46. component: () => import('@/views/Points.vue'),
  47. meta: { title: '积分管理', perm: 'family:points' }
  48. },
  49. // 任务管理
  50. {
  51. path: 'tasks',
  52. name: 'Tasks',
  53. component: () => import('@/views/Tasks.vue'),
  54. meta: { title: '任务列表', perm: 'task:list' }
  55. },
  56. {
  57. path: 'task-templates',
  58. name: 'TaskTemplates',
  59. component: () => import('@/views/TaskTemplates.vue'),
  60. meta: { title: '任务模板', perm: 'task:templates' }
  61. },
  62. {
  63. path: 'growth-task',
  64. name: 'GrowthTaskManagement',
  65. component: () => import('@/views/admin/GrowthTaskManagement'),
  66. meta: { title: '成长任务管理', perm: 'task:list' }
  67. },
  68. // 奖励管理
  69. {
  70. path: 'rewards',
  71. name: 'Rewards',
  72. component: () => import('@/views/Rewards.vue'),
  73. meta: { title: '奖励列表', perm: 'reward:list' }
  74. },
  75. // 成长规划师管理
  76. {
  77. path: 'guide-packages',
  78. name: 'GuidePackages',
  79. component: () => import('@/views/GuidePackages.vue'),
  80. meta: { title: '成长规划师套餐', requiresTeacher: true, perm: 'biz:packages' }
  81. },
  82. {
  83. path: 'guide-family-task',
  84. name: 'GuideFamilyTask',
  85. component: () => import('@/views/GuideFamilyTask.vue'),
  86. meta: { title: '家庭任务管理', requiresTeacher: true, perm: 'service:families' }
  87. },
  88. {
  89. path: 'teacher-consult',
  90. name: 'TeacherConsult',
  91. component: () => import('@/views/TeacherConsult.vue'),
  92. meta: { title: '家长咨询', requiresTeacher: true, perm: 'assessment:consult' }
  93. },
  94. // 新增成长规划师模块
  95. {
  96. path: 'teacher-dashboard',
  97. name: 'TeacherDashboard',
  98. component: () => import('@/views/teacher/TeacherDashboard.vue'),
  99. meta: { title: '成长规划师工作台', requiresTeacher: true, perm: 'dashboard' }
  100. },
  101. {
  102. path: 'teacher-families',
  103. name: 'TeacherFamilies',
  104. component: () => import('@/views/teacher/TeacherFamilies.vue'),
  105. meta: { title: '我的家庭', requiresTeacher: true, perm: 'service:families' }
  106. },
  107. {
  108. path: 'teacher-team',
  109. name: 'TeacherTeam',
  110. component: () => import('@/views/teacher/TeacherTeam.vue'),
  111. meta: { title: '我的团队', requiresTeacher: true, perm: 'service:team' }
  112. },
  113. {
  114. path: 'teacher-packages',
  115. name: 'TeacherPackages',
  116. component: () => import('@/views/teacher/TeacherPackages.vue'),
  117. meta: { title: '套餐管理', requiresTeacher: true, perm: 'biz:packages' }
  118. },
  119. {
  120. path: 'teacher-orders',
  121. name: 'TeacherOrders',
  122. component: () => import('@/views/teacher/TeacherOrders.vue'),
  123. meta: { title: '订单与佣金', requiresTeacher: true, perm: 'biz:orders' }
  124. },
  125. // 系统管理
  126. {
  127. path: 'users',
  128. name: 'Users',
  129. component: () => import('@/views/Users.vue'),
  130. meta: { title: '用户管理', perm: 'system:users' }
  131. },
  132. {
  133. path: 'package-audit',
  134. name: 'PackageAudit',
  135. component: () => import('@/views/PackageAudit.vue'),
  136. meta: { title: '套餐审核', perm: 'system:audit' }
  137. },
  138. {
  139. path: 'guide-audit',
  140. name: 'GuideAudit',
  141. component: () => import('@/views/GuideAudit.vue'),
  142. meta: { title: '成长规划师审核', perm: 'system:audit' }
  143. },
  144. {
  145. path: 'operation-logs',
  146. name: 'OperationLogs',
  147. component: () => import('@/views/OperationLogs.vue'),
  148. meta: { title: '操作日志', perm: 'system:logs' }
  149. },
  150. // 服务管理(管理员)
  151. {
  152. path: 'service-types',
  153. name: 'ServiceTypes',
  154. component: () => import('@/views/admin/ServiceTypes.vue'),
  155. meta: { title: '服务类型管理', perm: 'system:config' }
  156. },
  157. {
  158. path: 'service-contents',
  159. name: 'ServiceContents',
  160. component: () => import('@/views/admin/ServiceContents.vue'),
  161. meta: { title: '服务内容管理', perm: 'system:config' }
  162. },
  163. {
  164. path: 'package-templates',
  165. name: 'PackageTemplates',
  166. component: () => import('@/views/admin/PackageTemplates.vue'),
  167. meta: { title: '套餐模板管理', perm: 'system:config' }
  168. },
  169. {
  170. path: 'teacher-messages',
  171. name: 'TeacherMessages',
  172. component: () => import('@/views/teacher/TeacherMessages.vue'),
  173. meta: { title: '消息中心', requiresTeacher: true, perm: 'messages' }
  174. },
  175. {
  176. path: 'teacher-assessment',
  177. name: 'TeacherAssessment',
  178. component: () => import('@/views/teacher/TeacherAssessment.vue'),
  179. meta: { title: 'DAN测评管理', requiresTeacher: true, perm: 'assessment:dan' }
  180. },
  181. {
  182. path: 'assessment-admin',
  183. name: 'AssessmentAdmin',
  184. component: () => import('@/views/admin/Assessment.vue'),
  185. meta: { title: '测评管理', perm: 'assessment:dan' }
  186. },
  187. {
  188. path: 'vendor-review',
  189. name: 'VendorReview',
  190. component: () => import('@/views/admin/VendorReview.vue'),
  191. meta: { title: '服务商审核', perm: 'system:audit' }
  192. },
  193. {
  194. path: 'product-manage',
  195. name: 'ProductManage',
  196. component: () => import('@/views/admin/ProductManage.vue'),
  197. meta: { title: '商品管理', perm: 'system:products' }
  198. },
  199. {
  200. path: 'order-manage',
  201. name: 'OrderManage',
  202. component: () => import('@/views/admin/OrderManage.vue'),
  203. meta: { title: '订单管理', perm: 'system:orders' }
  204. },
  205. {
  206. path: 'sys-config',
  207. name: 'SysConfig',
  208. component: () => import('@/views/admin/SysConfig.vue'),
  209. meta: { title: '系统配置', perm: 'system:config' }
  210. },
  211. {
  212. path: 'energy-sandbox',
  213. name: 'EnergySandbox',
  214. component: () => import('@/views/admin/EnergySandbox.vue'),
  215. meta: { title: '五维能量', perm: 'system:energy' }
  216. },
  217. // ========== 文章管理 ==========
  218. {
  219. path: 'article-categories',
  220. name: 'ArticleCategory',
  221. component: () => import('@/views/admin/ArticleCategory.vue'),
  222. meta: { title: '文章分类', perm: 'articles:categories' }
  223. },
  224. {
  225. path: 'article-manage',
  226. name: 'ArticleManage',
  227. component: () => import('@/views/admin/ArticleManage.vue'),
  228. meta: { title: '文章管理', perm: 'articles:manage' }
  229. },
  230. {
  231. path: 'article-edit',
  232. name: 'ArticleEdit',
  233. component: () => import('@/views/admin/ArticleEdit.vue'),
  234. meta: { title: '文章编辑', perm: 'articles:manage' }
  235. },
  236. // ========== 分佣裂变系统 ==========
  237. {
  238. path: 'product-profit-rate',
  239. name: 'ProductProfitRate',
  240. component: () => import('@/views/admin/ProductProfitRate.vue'),
  241. meta: { title: '产品利润率', perm: 'system:products' }
  242. },
  243. {
  244. path: 'withdraw-audit',
  245. name: 'WithdrawAudit',
  246. component: () => import('@/views/admin/WithdrawAudit.vue'),
  247. meta: { title: '提现审核', perm: 'system:audit' }
  248. },
  249. {
  250. path: 'education-systems',
  251. name: 'EducationSystems',
  252. component: () => import('@/views/admin/EducationSystems.vue'),
  253. meta: { title: '教育体系管理', perm: 'system:education' }
  254. },
  255. {
  256. path: 'zodiac-configs',
  257. name: 'ZodiacConfigs',
  258. component: () => import('@/views/admin/ZodiacConfigs.vue'),
  259. meta: { title: '星座配置', perm: 'system:config' }
  260. },
  261. {
  262. path: 'bazi-configs',
  263. name: 'BaziConfigs',
  264. component: () => import('@/views/admin/BaziConfigs.vue'),
  265. meta: { title: '八字配置', perm: 'system:config' }
  266. },
  267. {
  268. path: 'blood-type-configs',
  269. name: 'BloodTypeConfigs',
  270. component: () => import('@/views/admin/BloodTypeConfigs.vue'),
  271. meta: { title: '血型配置', perm: 'system:config' }
  272. },
  273. {
  274. path: 'relationship-types',
  275. name: 'RelationshipTypes',
  276. component: () => import('@/views/admin/RelationshipTypes.vue'),
  277. meta: { title: '关系类型配置', perm: 'system:config' }
  278. },
  279. // ========== DanShop 电商管理 ==========
  280. {
  281. path: 'danshop-category',
  282. name: 'DanShopCategory',
  283. component: () => import('@/views/admin/DanShopCategoryManage'),
  284. meta: { title: '商品分类管理', perm: 'system:danshop' }
  285. },
  286. {
  287. path: 'danshop-profit-rule',
  288. name: 'DanShopProfitRule',
  289. component: () => import('@/views/admin/DanShopProfitRuleConfig'),
  290. meta: { title: '分润规则配置', perm: 'system:danshop' }
  291. },
  292. {
  293. path: 'danshop-logistics',
  294. name: 'DanShopLogistics',
  295. component: () => import('@/views/admin/DanShopLogisticsManage'),
  296. meta: { title: '物流管理', perm: 'system:danshop' }
  297. },
  298. {
  299. path: 'danshop-product',
  300. name: 'DanShopProduct',
  301. component: () => import('@/views/admin/ProductManage.vue'),
  302. meta: { title: 'DanShop商品', perm: 'system:products' }
  303. },
  304. {
  305. path: 'danshop-order',
  306. name: 'DanShopOrder',
  307. component: () => import('@/views/admin/OrderManage.vue'),
  308. meta: { title: 'DanShop订单', perm: 'system:orders' }
  309. },
  310. // ========== 饮食管理 (admin) ==========
  311. {
  312. path: 'foods',
  313. name: 'Foods',
  314. component: () => import('@/views/admin/Foods.vue'),
  315. meta: { title: '食材管理', perm: 'diet:foods' }
  316. },
  317. {
  318. path: 'recipes',
  319. name: 'Recipes',
  320. component: () => import('@/views/admin/Recipes.vue'),
  321. meta: { title: '食谱管理', perm: 'diet:recipes' }
  322. },
  323. {
  324. path: 'seasonal-foods',
  325. name: 'SeasonalFoods',
  326. component: () => import('@/views/admin/SeasonalFoods.vue'),
  327. meta: { title: '应季食材', perm: 'diet:seasonal' }
  328. },
  329. {
  330. path: 'my-families',
  331. name: 'MyFamilies',
  332. component: () => import('@/views/teacher/FamilyList.vue'),
  333. meta: { title: '我的家庭', perm: 'service:family' }
  334. },
  335. {
  336. path: 'growth-records',
  337. name: 'GrowthRecords',
  338. component: () => import('@/views/teacher/GrowthRecords.vue'),
  339. meta: { title: '成长记录', perm: 'growth:records' }
  340. },
  341. {
  342. path: 'growth-plans',
  343. name: 'GrowthPlans',
  344. component: () => import('@/views/teacher/GrowthPlans.vue'),
  345. meta: { title: '成长计划', perm: 'growth:plans' }
  346. },
  347. {
  348. path: 'health-reports',
  349. name: 'HealthReports',
  350. component: () => import('@/views/nutritionist/HealthReports.vue'),
  351. meta: { title: '健康报告', perm: 'health:reports' }
  352. },
  353. {
  354. path: 'health-indicators',
  355. name: 'HealthIndicators',
  356. component: () => import('@/views/nutritionist/HealthIndicators.vue'),
  357. meta: { title: '健康指标', perm: 'health:indicators' }
  358. },
  359. {
  360. path: 'health-checkins',
  361. name: 'HealthCheckins',
  362. component: () => import('@/views/nutritionist/HealthCheckins.vue'),
  363. meta: { title: '健康打卡', perm: 'health:checkins' }
  364. },
  365. {
  366. path: 'nutrition-mappings',
  367. name: 'NutritionMappings',
  368. component: () => import('@/views/nutritionist/NutritionMappings.vue'),
  369. meta: { title: '营养素映射管理', perm: 'health:mappings' }
  370. },
  371. {
  372. path: 'activities',
  373. name: 'Activities',
  374. component: () => import('@/views/admin/Activities.vue'),
  375. meta: { title: '活动列表', perm: 'activity:list' }
  376. },
  377. {
  378. path: 'activity-review',
  379. name: 'ActivityReview',
  380. component: () => import('@/views/admin/ActivityReview.vue'),
  381. meta: { title: '活动审核', perm: 'activity:review' }
  382. },
  383. // ========== 维度配置 + 知识库管理 ==========
  384. {
  385. path: 'coupon',
  386. name: 'CouponManagement',
  387. component: () => import('@/views/admin/CouponManagement'),
  388. meta: { title: '优惠券管理', perm: 'system:coupon' }
  389. },
  390. {
  391. path: 'promotion',
  392. name: 'PromotionManagement',
  393. component: () => import('@/views/admin/PromotionManagement'),
  394. meta: { title: '推广管理', perm: 'system:promotion' }
  395. },
  396. {
  397. path: 'dimension-config',
  398. name: 'DimensionConfig',
  399. component: () => import('@/views/admin/dimension'),
  400. meta: { title: '维度配置', perm: 'system:config' }
  401. },
  402. {
  403. path: 'knowledge-base',
  404. name: 'KnowledgeBase',
  405. component: () => import('@/views/admin/knowledge'),
  406. meta: { title: '知识库管理', perm: 'system:config' }
  407. },
  408. // ========== 健康维度配置 ==========
  409. {
  410. path: 'health-norm-config',
  411. name: 'HealthNormConfig',
  412. component: () => import('@/views/admin/dimension-config'),
  413. meta: { title: '健康常模配置', perm: 'system:config' }
  414. },
  415. {
  416. path: 'health-data-source',
  417. name: 'HealthDataSource',
  418. component: () => import('@/views/admin/data-source-config'),
  419. meta: { title: '健康数据源配置', perm: 'system:config' }
  420. }
  421. ]
  422. }
  423. ]
  424. const router = new VueRouter({
  425. mode: 'history',
  426. base: process.env.BASE_URL,
  427. routes
  428. })
  429. // Token 验证缓存:避免 redirect 链中重复调用 /api/admin-auth/info
  430. let tokenValidationCache = { token: '', timestamp: 0 }
  431. const TOKEN_CACHE_TTL = 30000 // 30秒内不重复验证
  432. async function validateToken(token) {
  433. try {
  434. const baseURL = process.env.VUE_APP_BASE_API || ''
  435. const res = await axios.post(`${baseURL}/api/admin-auth/info`, {}, {
  436. headers: { Authorization: `Bearer ${token}` },
  437. validateStatus: () => true
  438. })
  439. return res.status === 200 && res.data?.code === 200
  440. } catch (e) {
  441. return false
  442. }
  443. }
  444. router.beforeEach(async (to, from, next) => {
  445. const token = localStorage.getItem('token')
  446. const role = localStorage.getItem('role')
  447. // 1. 登录页允许访问
  448. if (to.path === '/login') {
  449. // 如果已有token和role,直接跳转到对应首页
  450. if (token && role === 'teacher') {
  451. return next('/teacher-dashboard')
  452. }
  453. return next()
  454. }
  455. // 2. 无Token强制跳转
  456. if (!token) {
  457. return next('/login')
  458. }
  459. // 3. Token有效性验证 (使用缓存避免 redirect 链重复调用)
  460. const now = Date.now()
  461. let isValid = false
  462. if (tokenValidationCache.token === token && (now - tokenValidationCache.timestamp) < TOKEN_CACHE_TTL) {
  463. isValid = true
  464. } else {
  465. isValid = await validateToken(token)
  466. if (isValid) {
  467. tokenValidationCache = { token, timestamp: now }
  468. }
  469. }
  470. if (!isValid) {
  471. tokenValidationCache = { token: '', timestamp: 0 }
  472. localStorage.clear()
  473. return next('/login')
  474. }
  475. // 4. 登录后首页重定向(多角色)
  476. const roleHomeMap = {
  477. 'teacher': '/teacher-dashboard',
  478. 'nutritionist': '/dashboard',
  479. 'activity_admin': '/dashboard',
  480. 'article_admin': '/dashboard'
  481. }
  482. if (to.path === '/dashboard' && roleHomeMap[role]) {
  483. return next({ path: roleHomeMap[role], replace: true })
  484. }
  485. // 5. 权限检查:如果路由有 perm 要求,检查用户权限
  486. const requiredPerm = to.meta && to.meta.perm
  487. if (requiredPerm) {
  488. const rolesStr = localStorage.getItem('roles')
  489. const roles = rolesStr ? JSON.parse(rolesStr) : [localStorage.getItem('role')]
  490. const perms = getEffectivePermissions(roles)
  491. if (!hasPermission(perms, requiredPerm)) {
  492. console.warn('Access denied: insufficient permissions for', to.name)
  493. return next({ path: roleHomeMap[role] || '/dashboard', query: { accessDenied: 'no-permission' }, replace: true })
  494. }
  495. }
  496. return next()
  497. })
  498. // Suppress navigation guard redirect errors (teacher/admin route cross-access)
  499. const originalPush = VueRouter.prototype.push
  500. VueRouter.prototype.push = function push(location) {
  501. return originalPush.call(this, location).catch(err => err)
  502. }
  503. export default router