index.js 16 KB

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