804812dd7e
- 新增 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>
133 lines
5.9 KiB
TypeScript
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
|