Files
qiji/frontend/src/router/index.ts
T
v6ole 804812dd7e feat: 企业微信OAuth静默登录
- 新增 GET /api/wecom/oauth-url 和 /api/wecom/oauth-callback
- OAuth回调返回HTML页面直接写localStorage(避坑企微webview 307重定向不可靠)
- 菜单URL改为OAuth静默授权链接(snsapi_base + agentid)
- redirect目标编码到state参数中(避坑redirect_uri不允许自定义query)
- wecom_userid大小写不敏感匹配
- 前端路由守卫处理 ?token= 自动登录(fallback)
- Login.vue企微环境检测自动跳OAuth
- setup-menu同步更新为OAuth链接

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-30 12:33:43 +08:00

133 lines
5.9 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
const tokenParam = to.query.token as string
if (tokenParam) {
// 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,
})
} catch (_) { /* invalid token, ignore */ }
// Remove token from URL
const cleanQuery = { ...to.query }
delete cleanQuery.token
next({ path: to.path, query: cleanQuery, replace: true })
return
}
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