Files
qiji/frontend/src/router/index.ts
T
v6ole 1ec20ee53e chore: 保存进行中的未提交改动(企微定向推送+搜索筛选UI等)
保存合并前工作树中遗留的进行中改动,避免分支合并时丢失:
- scheduler: 填报汇总改为定向推送给支局长/领导(而非广播@all)
- router: JWT token 校验修复,bind token(UUID)不被误剥离
- 工作计划/计划列表: 搜索+筛选+分页 UI
- 各列表 API 增加 search 参数支持
- docker-compose.backend.yml + backend/.dockerignore 纳入版本管理

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 09:17:03 +08:00

135 lines
6.1 KiB
TypeScript

import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { isMobile } from '@/utils'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { public: true },
},
{
path: '/bind-wecom',
name: 'BindWecom',
component: () => import('@/views/BindWecom.vue'),
meta: { public: true },
},
{
path: '/bind-wechat',
name: 'BindWechat',
component: () => import('@/views/BindWechat.vue'),
meta: { public: true },
},
{
path: '/wecom-bind',
name: 'WecomBind',
component: () => import('@/views/WecomBind.vue'),
meta: { public: true },
},
// Mobile routes (manager-facing)
{
path: '/m',
component: () => import('@/components/MobileLayout.vue'),
children: [
{ path: '', name: 'MobileHome', component: () => import('@/views/mobile/Home.vue') },
{ path: 'work', name: 'WorkCenter', component: () => import('@/views/mobile/WorkCenter.vue') },
{ path: 'visit/new', name: 'VisitForm', component: () => import('@/views/mobile/VisitForm.vue') },
{ path: 'visit/:id/edit', name: 'VisitEdit', component: () => import('@/views/mobile/VisitForm.vue') },
{ path: 'work-plan/new', name: 'WorkPlanForm', component: () => import('@/views/mobile/WorkPlanForm.vue') },
{ path: 'work-plan/:id/edit', name: 'WorkPlanEdit', component: () => import('@/views/mobile/WorkPlanForm.vue') },
{ path: 'mini-biz/new', name: 'MiniBusinessForm', component: () => import('@/views/mobile/MiniBusinessForm.vue') },
{ path: 'mini-biz/:id/edit', name: 'MiniBusinessEdit', component: () => import('@/views/mobile/MiniBusinessForm.vue') },
{ path: 'key-visit/new', name: 'KeyVisitForm', component: () => import('@/views/mobile/KeyVisitForm.vue') },
{ path: 'key-visit/:id/edit', name: 'KeyVisitEdit', component: () => import('@/views/mobile/KeyVisitForm.vue') },
{ path: 'note/new', name: 'DailyNoteForm', component: () => import('@/views/mobile/DailyNoteForm.vue') },
{ path: 'note/:id/edit', name: 'DailyNoteEdit', component: () => import('@/views/mobile/DailyNoteForm.vue') },
{ path: 'visits', name: 'VisitsList', component: () => import('@/views/mobile/VisitsList.vue') },
{ path: 'notes', name: 'NotesList', component: () => import('@/views/mobile/NotesList.vue') },
{ path: 'plans', name: 'PlansList', component: () => import('@/views/mobile/PlansList.vue') },
{ path: 'mini-biz', name: 'MiniBizList', component: () => import('@/views/mobile/MiniBizList.vue') },
{ path: 'key-visits', name: 'KeyVisitsList', component: () => import('@/views/mobile/KeyVisitsList.vue') },
{ path: 'leaves', name: 'LeavesList', component: () => import('@/views/mobile/LeavesList.vue') },
{ path: 'leave/new', name: 'LeaveForm', component: () => import('@/views/mobile/LeaveForm.vue') },
{ path: 'leave/:id/edit', name: 'LeaveEdit', component: () => import('@/views/mobile/LeaveForm.vue') },
],
},
// Desktop routes (director/leader-facing)
{
path: '/',
component: () => import('@/components/DesktopLayout.vue'),
children: [
{ path: '', name: 'Dashboard', component: () => import('@/views/desktop/Dashboard.vue') },
{ path: 'weekly-report', name: 'WeeklyReport', component: () => import('@/views/desktop/WeeklyReport.vue') },
{ path: 'light-board', name: 'LightBoard', component: () => import('@/views/desktop/LightBoard.vue') },
{ path: 'work-plans', name: 'WorkPlans', component: () => import('@/views/desktop/WorkPlans.vue') },
{ path: 'mini-business', name: 'MiniBusiness', component: () => import('@/views/desktop/MiniBusiness.vue') },
{ path: 'key-visits', name: 'KeyVisits', component: () => import('@/views/desktop/KeyVisits.vue') },
{ path: 'leaves', name: 'Leaves', component: () => import('@/views/desktop/Leaves.vue') },
{ path: 'workspace', name: 'ManagerWorkspace', component: () => import('@/views/desktop/ManagerWorkspace.vue') },
{ path: 'customers', name: 'CustomerManage', component: () => import('@/views/desktop/CustomerManage.vue') },
{ path: 'users', name: 'UserManage', component: () => import('@/views/desktop/UserManage.vue') },
{ path: 'audit-log', name: 'AuditLog', component: () => import('@/views/desktop/AuditLog.vue') },
{ path: 'settings', name: 'Settings', component: () => import('@/views/desktop/Settings.vue') },
],
},
],
})
router.beforeEach((to, _from, next) => {
const auth = useAuthStore()
// Handle WeChat Work OAuth silent login callback: ?token=JWT
// Only strip the token if it's a valid JWT — bind tokens (UUID hex) must
// survive so pages like WecomBind can read them.
const tokenParam = to.query.token as string
if (tokenParam && tokenParam.split('.').length === 3) {
// Save token to auth store (the JWT contains user_id, name, role, theme)
try {
const payload = JSON.parse(atob(tokenParam.split('.')[1]))
auth.saveLogin({
access_token: tokenParam,
user_id: payload.user_id,
name: payload.name,
role: payload.role,
theme: payload.theme,
})
// Remove token from URL only on successful JWT decode
const cleanQuery = { ...to.query }
delete cleanQuery.token
next({ path: to.path, query: cleanQuery, replace: true })
return
} catch (_) { /* invalid JWT — leave token for page to handle */ }
}
if (to.meta.public) {
next()
return
}
if (!auth.isLoggedIn) {
next('/login')
return
}
// Auto-redirect on FIRST entry only (no previous route = fresh page load)
// Don't interfere with manual navigation (user clicked switch button)
const viewOverride = to.query.view as string
if (!viewOverride && !_from.name) {
const onMobile = isMobile()
if (onMobile && to.path === '/') {
next('/m')
return
}
if (!onMobile && to.path.startsWith('/m')) {
next('/')
return
}
}
next()
})
export default router