1ec20ee53e
保存合并前工作树中遗留的进行中改动,避免分支合并时丢失: - scheduler: 填报汇总改为定向推送给支局长/领导(而非广播@all) - router: JWT token 校验修复,bind token(UUID)不被误剥离 - 工作计划/计划列表: 搜索+筛选+分页 UI - 各列表 API 增加 search 参数支持 - docker-compose.backend.yml + backend/.dockerignore 纳入版本管理 Co-Authored-By: Claude <noreply@anthropic.com>
135 lines
6.1 KiB
TypeScript
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
|