| | |
| | | VITE_ROUTE_LOAD_MODE = static |
| | | |
| | | # 设置登陆后跳转地址 |
| | | VITE_HOME_PATH = /dashboard |
| | | VITE_HOME_PATH = /transaction/order/orderManagement |
| | | |
| | | # 本地存储前缀 |
| | | VITE_STORAGE_PREFIX = |
| | |
| | | export * from './useBoolean' |
| | | export * from './useAuthEntryBootstrap' |
| | | export * from './useEcharts' |
| | | export * from './usePermission' |
| New file |
| | |
| | | import type { RouteLocationNormalizedLoaded } from 'vue-router' |
| | | import { fetchSysUserInfo } from '@/service/api/account' |
| | | import { fetchSsoCallback } from '@/service/api/login' |
| | | import { useAuthStore } from '@/store' |
| | | |
| | | // 统一处理“默认落地页”的登录入口初始化。 |
| | | // 目前首页已切到订单管理,原先 dashboard 承接的 SSO 回调和用户信息同步 |
| | | // 需要在新的默认页复用,避免后续再次复制同样逻辑。 |
| | | async function requestSsoCallback(code: string, state: string, scene: string, attempt: string) { |
| | | return fetchSsoCallback({ code, state }) |
| | | } |
| | | |
| | | // 返回 true 表示当前页面已经处理了登录入口跳转,不应继续执行页面自己的初始化请求。 |
| | | export async function bootstrapAuthEntry(route: RouteLocationNormalizedLoaded, scene: string) { |
| | | const authStore = useAuthStore() |
| | | const { code, state } = route.query |
| | | |
| | | // 线上 SSO 会通过 code/state 回调到默认落地页,这里优先完成单点登录换 token。 |
| | | if (typeof code === 'string' && typeof state === 'string') { |
| | | try { |
| | | const firstResult = await requestSsoCallback(code, state, scene, 'first') |
| | | |
| | | if (!firstResult.data) { |
| | | const secondResult = await requestSsoCallback(code, state, scene, 'retry') |
| | | |
| | | if (!secondResult.data) |
| | | return true |
| | | |
| | | await authStore.handleLoginInfo(secondResult.data) |
| | | return true |
| | | } |
| | | |
| | | await authStore.handleLoginInfo(firstResult.data) |
| | | return true |
| | | } |
| | | catch (error) { |
| | | return true |
| | | } |
| | | } |
| | | |
| | | // 非 SSO 回调场景下,补拉一次当前登录人的完整信息和权限, |
| | | // 保证默认落地页不依赖旧 dashboard 也能完成用户态同步。 |
| | | try { |
| | | const result = await fetchSysUserInfo() |
| | | |
| | | if (result.isSuccess && result.data) |
| | | await authStore.syncAuthInfo(result.data) |
| | | } |
| | | catch {} |
| | | |
| | | return false |
| | | } |
| | |
| | | menuType: 'page', |
| | | componentPath: '/dashboard/index.vue', |
| | | id: 1, |
| | | hide: true, // 不在菜单中显示 |
| | | keepAlive: true, |
| | | pid: null, |
| | | }, |
| | | { |
| | | name: 'testDemo', |
| | | path: '/testDemo', |
| | | title: '测试跑单demo', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:analysis', |
| | | menuType: 'dir', |
| | | componentPath: null, |
| | | id: 9, |
| | | pid: null, |
| | | }, |
| | | { |
| | | name: 'testPointManagement', |
| | | path: '/testDemo/testPointManagement', |
| | | title: '测试点位管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/testPointManagement/index.vue', |
| | | id: 91, |
| | | keepAlive: true, |
| | | pid: 9, |
| | | }, |
| | | { |
| | | name: 'testPointManagementList', |
| | | path: '/testDemo/testPointManagement/list', |
| | | title: '测试点位列表', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/testPointManagement/list.vue', |
| | | id: 92, |
| | | pid: 9, |
| | | hide: true, |
| | | fullScreen: true, |
| | | activeMenu: '/testDemo/testPointManagement', |
| | | }, |
| | | { |
| | | name: 'testDemoGeofenceManagement', |
| | | path: '/testDemo/geofenceManagement', |
| | | title: '测试围栏管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/geofenceManagement/index.vue', |
| | | id: 202642974, |
| | | keepAlive: true, |
| | | pid: 9, |
| | | }, |
| | | { |
| | | name: 'testDemoGeofenceDetail', |
| | | path: '/testDemo/geofenceManagement/detail', |
| | | title: '围栏详情', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/geofenceManagement/detail.vue', |
| | | id: 94, |
| | | pid: 9, |
| | | hide: true, |
| | | fullScreen: true, |
| | | activeMenu: '/testDemo/geofenceManagement', |
| | | }, |
| | | { |
| | | name: 'testDemoAddFence', |
| | | path: '/testDemo/geofenceManagement/addFence', |
| | | title: '新增围栏', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/geofenceManagement/detail.vue', |
| | | id: 202642975, |
| | | pid: 9, |
| | | hide: true, |
| | | fullScreen: true, |
| | | activeMenu: '/testDemo/geofenceManagement', |
| | | }, |
| | | { |
| | | name: 'testDemoEditFence', |
| | | path: '/testDemo/geofenceManagement/editFence', |
| | | title: '编辑围栏', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/geofenceManagement/detail.vue', |
| | | id: 202642976, |
| | | pid: 9, |
| | | hide: true, |
| | | fullScreen: true, |
| | | activeMenu: '/testDemo/geofenceManagement', |
| | | }, |
| | | { |
| | | name: 'specialPointManagement', |
| | | path: '/testDemo/specialPointManagement', |
| | | title: '特殊点位管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/specialPointManagement/index.vue', |
| | | id: 10, |
| | | keepAlive: true, |
| | | pid: 9, |
| | | }, |
| | | { |
| | | name: 'testPlanList', |
| | | path: '/testDemo/testPlanList', |
| | | title: '测试计划列表', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/testPlanList/index.vue', |
| | | id: 11, |
| | | keepAlive: true, |
| | | pid: 9, |
| | | }, |
| | | { |
| | | name: 'testPlanAdd', |
| | | path: '/testDemo/testPlanList/add', |
| | | title: '新增测试计划', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/testPlanList/add.vue', |
| | | id: 111, |
| | | pid: 9, |
| | | hide: true, |
| | | activeMenu: '/testDemo/testPlanList', |
| | | }, |
| | | { |
| | | name: 'testPlanDetail', |
| | | path: '/testDemo/testPlanList/detail/:id', |
| | | title: '测试计划详情', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/testPlanList/detail.vue', |
| | | id: 112, |
| | | pid: 9, |
| | | hide: true, |
| | | activeMenu: '/testDemo/testPlanList', |
| | | }, |
| | | { |
| | | name: 'testDemoOrderDetail', |
| | | path: '/testDemo/testPlanList/orderDetail', |
| | | title: '订单详情', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:alarm', |
| | | menuType: 'page', |
| | | componentPath: '/transaction/order/orderManagement/detail.vue', |
| | | id: 60111, |
| | | pid: 9, |
| | | hide: true, |
| | | activeMenu: '/testDemo/testPlanList', |
| | | }, |
| | | { |
| | | name: 'systemOperationLog', |
| | | path: '/systemOperationLog', |
| | | title: '系统操作日志', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:log', |
| | | menuType: 'page', |
| | | componentPath: '/system/operationLog/index.vue', |
| | | id: 12, |
| | | keepAlive: true, |
| | | pid: null, |
| | | fullScreen: false, |
| | | }, |
| | | { |
| | | name: 'system', |
| | |
| | | path: '/transaction', |
| | | title: '交易&定价', |
| | | requiresAuth: true, |
| | | // 默认首页已切到订单管理,父级目录也需要跳过菜单权限过滤,保证整条链路可见。 |
| | | ignorePermission: true, |
| | | icon: 'icon-park-outline:analysis', |
| | | menuType: 'dir', |
| | | componentPath: null, |
| | |
| | | path: '/transaction/order', |
| | | title: '订单', |
| | | requiresAuth: true, |
| | | // 同上,父级目录保留可见,具体数据权限仍由页面内部控制。 |
| | | ignorePermission: true, |
| | | icon: 'icon-park-outline:analysis', |
| | | menuType: 'dir', |
| | | componentPath: null, |
| | |
| | | path: '/transaction/order/orderManagement', |
| | | title: '订单管理', |
| | | requiresAuth: true, |
| | | // 订单管理作为默认落地页,不参与菜单/路由权限过滤,但仍需要登录。 |
| | | ignorePermission: true, |
| | | icon: 'icon-park-outline:alarm', |
| | | menuType: 'page', |
| | | componentPath: '/transaction/order/orderManagement/index.vue', |
| | |
| | | hide: true, |
| | | activeMenu: '/service/ticket/workOrder', |
| | | }, |
| | | { |
| | | name: 'testDemo', |
| | | path: '/testDemo', |
| | | title: '测试跑单demo', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:analysis', |
| | | menuType: 'dir', |
| | | componentPath: null, |
| | | id: 9, |
| | | pid: null, |
| | | }, |
| | | { |
| | | name: 'testPointManagement', |
| | | path: '/testDemo/testPointManagement', |
| | | title: '测试点位管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/testPointManagement/index.vue', |
| | | id: 91, |
| | | keepAlive: true, |
| | | pid: 9, |
| | | }, |
| | | { |
| | | name: 'testPointManagementList', |
| | | path: '/testDemo/testPointManagement/list', |
| | | title: '测试点位列表', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/testPointManagement/list.vue', |
| | | id: 92, |
| | | pid: 9, |
| | | hide: true, |
| | | fullScreen: true, |
| | | activeMenu: '/testDemo/testPointManagement', |
| | | }, |
| | | { |
| | | name: 'testDemoGeofenceManagement', |
| | | path: '/testDemo/geofenceManagement', |
| | | title: '测试围栏管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/geofenceManagement/index.vue', |
| | | id: 202642974, |
| | | keepAlive: true, |
| | | pid: 9, |
| | | }, |
| | | { |
| | | name: 'testDemoGeofenceDetail', |
| | | path: '/testDemo/geofenceManagement/detail', |
| | | title: '围栏详情', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/geofenceManagement/detail.vue', |
| | | id: 94, |
| | | pid: 9, |
| | | hide: true, |
| | | fullScreen: true, |
| | | activeMenu: '/testDemo/geofenceManagement', |
| | | }, |
| | | { |
| | | name: 'testDemoAddFence', |
| | | path: '/testDemo/geofenceManagement/addFence', |
| | | title: '新增围栏', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/geofenceManagement/detail.vue', |
| | | id: 202642975, |
| | | pid: 9, |
| | | hide: true, |
| | | fullScreen: true, |
| | | activeMenu: '/testDemo/geofenceManagement', |
| | | }, |
| | | { |
| | | name: 'testDemoEditFence', |
| | | path: '/testDemo/geofenceManagement/editFence', |
| | | title: '编辑围栏', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/geofenceManagement/detail.vue', |
| | | id: 202642976, |
| | | pid: 9, |
| | | hide: true, |
| | | fullScreen: true, |
| | | activeMenu: '/testDemo/geofenceManagement', |
| | | }, |
| | | { |
| | | name: 'specialPointManagement', |
| | | path: '/testDemo/specialPointManagement', |
| | | title: '特殊点位管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/specialPointManagement/index.vue', |
| | | id: 10, |
| | | keepAlive: true, |
| | | pid: 9, |
| | | }, |
| | | { |
| | | name: 'testPlanList', |
| | | path: '/testDemo/testPlanList', |
| | | title: '测试计划列表', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/testPlanList/index.vue', |
| | | id: 11, |
| | | keepAlive: true, |
| | | pid: 9, |
| | | }, |
| | | { |
| | | name: 'testPlanAdd', |
| | | path: '/testDemo/testPlanList/add', |
| | | title: '新增测试计划', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/testPlanList/add.vue', |
| | | id: 111, |
| | | pid: 9, |
| | | hide: true, |
| | | activeMenu: '/testDemo/testPlanList', |
| | | }, |
| | | { |
| | | name: 'testPlanDetail', |
| | | path: '/testDemo/testPlanList/detail/:id', |
| | | title: '测试计划详情', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/testDemo/testPlanList/detail.vue', |
| | | id: 112, |
| | | pid: 9, |
| | | hide: true, |
| | | activeMenu: '/testDemo/testPlanList', |
| | | }, |
| | | { |
| | | name: 'testDemoOrderDetail', |
| | | path: '/testDemo/testPlanList/orderDetail', |
| | | title: '订单详情', |
| | | requiresAuth: false, |
| | | icon: 'icon-park-outline:alarm', |
| | | menuType: 'page', |
| | | componentPath: '/transaction/order/orderManagement/detail.vue', |
| | | id: 60111, |
| | | pid: 9, |
| | | hide: true, |
| | | activeMenu: '/testDemo/testPlanList', |
| | | }, |
| | | { |
| | | name: 'systemOperationLog', |
| | | path: '/systemOperationLog', |
| | | title: '系统操作日志', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:log', |
| | | menuType: 'page', |
| | | componentPath: '/system/operationLog/index.vue', |
| | | id: 12, |
| | | keepAlive: true, |
| | | pid: null, |
| | | fullScreen: false, |
| | | }, |
| | | |
| | | ] |
| | |
| | | const routeStore = useRouteStore() |
| | | await routeStore.initAuthRoute(true) |
| | | |
| | | router.replace({ path: '/dashboard' }) |
| | | router.replace({ path: import.meta.env.VITE_HOME_PATH }) |
| | | }, |
| | | }, |
| | | }) |
| | |
| | | import { RouterLink } from 'vue-router' |
| | | |
| | | const metaFields: AppRoute.MetaKeys[] |
| | | = ['title', 'icon', 'requiresAuth', 'roles', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType', 'fullScreen'] |
| | | = ['title', 'icon', 'requiresAuth', 'roles', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType', 'fullScreen', 'ignorePermission'] |
| | | |
| | | function shouldSkipPermission(route: Pick<AppRoute.Route, 'meta'>) { |
| | | // Hidden pages are entered from already-authorized actions, so they only keep login checks. |
| | | return route.meta?.requiresAuth === false || route.meta?.hide === true |
| | | } |
| | | |
| | | // ignorePermission 仅跳过“菜单/路由权限过滤”,不改变登录校验语义。 |
| | | // 这类页面仍然需要登录,但允许作为默认落地页在无菜单权限时显示出来。 |
| | | function shouldSkipRoutePermission(route: Pick<AppRoute.Route, 'meta'>) { |
| | | return shouldSkipPermission(route) || route.meta?.ignorePermission === true |
| | | } |
| | | |
| | | function standardizedRoutes(route: AppRoute.RowRoute[]) { |
| | |
| | | export function createRoutes(routes: AppRoute.RowRoute[]) { |
| | | const { hasPermission } = usePermission() |
| | | |
| | | // console.log('Creating routes with input:', routes) |
| | | |
| | | // Structure the meta field |
| | | |
| | | // 正式解开 |
| | | |
| | | let resultRouter = standardizedRoutes(routes) |
| | | |
| | | // console.log('After standardization:', resultRouter) |
| | | |
| | | // Route permission filtering |
| | | resultRouter = resultRouter.filter((i) => { |
| | | if (shouldSkipPermission(i)) |
| | | // 默认落地页等白名单路由需要保留完整父子链,避免首次进入命中 404。 |
| | | if (shouldSkipRoutePermission(i)) |
| | | return true |
| | | if (i.id) { |
| | | return hasPermission(i.id) |
| | | } |
| | | return hasPermission(i.meta.roles) |
| | | }) |
| | | // 正式解开 |
| | | console.log('After permission filtering:', resultRouter) |
| | | |
| | | // Generate routes, no need to import files for those with redirect |
| | | const modules = import.meta.glob('@/views/**/*.vue') |
| | | // console.log('Available modules:', Object.keys(modules)) |
| | | |
| | | resultRouter = resultRouter.map((item: AppRoute.Route) => { |
| | | if (item.componentPath && !item.redirect) { |
| | | const componentPath = `/src/views${item.componentPath}` |
| | | // console.log(`Looking for component at: ${componentPath}`) |
| | | if (modules[componentPath]) { |
| | | if (modules[componentPath]) |
| | | item.component = modules[componentPath] |
| | | // console.log(`Component found for: ${item.path}`) |
| | | } |
| | | else { |
| | | // console.warn(`Component not found for: ${componentPath}`) |
| | | } |
| | | } |
| | | return item |
| | | }) |
| | | |
| | | // Generate route tree |
| | | // 正式解开 |
| | | resultRouter = arrayToTree(resultRouter) as AppRoute.Route[] |
| | | |
| | | const appRootRoute: RouteRecordRaw = { |
| | |
| | | |
| | | // Insert the processed route into the root route |
| | | appRootRoute.children = resultRouter as unknown as RouteRecordRaw[] |
| | | // console.log('Final app root route:', appRootRoute) |
| | | return appRootRoute |
| | | } |
| | | |
| | |
| | | |
| | | // Route permission filtering |
| | | resultRouter = resultRouter.filter((i) => { |
| | | if (shouldSkipPermission(i)) |
| | | // 菜单和路由都复用同一套 ignorePermission 规则,避免“页面可进但菜单不显示”。 |
| | | if (shouldSkipRoutePermission(i)) |
| | | return true |
| | | // 优先检查路由 ID 是否在权限列表中 |
| | | if (i.id) { |
| | |
| | | throw new Error('Failed to fetch user routes') |
| | | } |
| | | |
| | | // Hidden pages also skip permission filtering; keep them mounted in dynamic mode. |
| | | const publicRoutes = staticRoutes.filter(route => route.requiresAuth === false || route.hide === true) |
| | | // Hidden pages and permission-exempt landing pages skip route permission filtering in dynamic mode. |
| | | const publicRoutes = staticRoutes.filter( |
| | | route => route.requiresAuth === false || route.hide === true || route.ignorePermission === true, |
| | | ) |
| | | const userRoutes = result.data |
| | | const mergedRoutes = [...publicRoutes.filter(p => !userRoutes.some(u => u.name === p.name)), ...userRoutes] |
| | | |
| | |
| | | menuType?: MenuType |
| | | /** 当前路由是否会全屏显示 */ |
| | | fullScreen?: boolean |
| | | /** 当前路由是否跳过菜单/路由权限过滤,仅保留登录校验,常用于默认落地页 */ |
| | | ignorePermission?: boolean |
| | | } |
| | | |
| | | type MetaKeys = keyof RouteMeta |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted } from 'vue' |
| | | import { useRoute } from 'vue-router' |
| | | import { fetchSsoCallback, fetchSysUserInfo } from '@/service' |
| | | import { useAuthStore } from '@/store' |
| | | import { bootstrapAuthEntry } from '@/hooks' |
| | | |
| | | const route = useRoute() |
| | | const authStore = useAuthStore() |
| | | |
| | | async function requestSsoCallback(code: string, state: string, attempt: string) { |
| | | console.log(`[dashboard] /account/ssoAuthSdk/sysLogin ${attempt} start`, { code, state }) |
| | | const result = await fetchSsoCallback({ code, state }) |
| | | console.log(`[dashboard] /account/ssoAuthSdk/sysLogin ${attempt} result`, result) |
| | | return result |
| | | } |
| | | |
| | | async function handleSsoCallback() { |
| | | const { code, state } = route.query |
| | | |
| | | if (typeof code !== 'string' || typeof state !== 'string') |
| | | return false |
| | | |
| | | try { |
| | | const firstResult = await requestSsoCallback(code, state, 'first') |
| | | |
| | | if (!firstResult.data) { |
| | | console.warn('[dashboard] /account/ssoAuthSdk/sysLogin first attempt missing data, retry once', firstResult) |
| | | |
| | | const secondResult = await requestSsoCallback(code, state, 'retry') |
| | | |
| | | if (!secondResult.data) { |
| | | console.warn('[dashboard] /account/ssoAuthSdk/sysLogin retry still missing data', secondResult) |
| | | return true |
| | | } |
| | | |
| | | await authStore.handleLoginInfo(secondResult.data) |
| | | return true |
| | | } |
| | | |
| | | await authStore.handleLoginInfo(firstResult.data) |
| | | return true |
| | | } |
| | | catch (error) { |
| | | console.error('[dashboard] /account/ssoAuthSdk/sysLogin error', error) |
| | | return true |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | const handledSsoCallback = await handleSsoCallback() |
| | | if (handledSsoCallback) |
| | | return |
| | | |
| | | try { |
| | | const result = await fetchSysUserInfo() |
| | | console.log('[dashboard] /account/api/v1/getSysUserInfo', result) |
| | | |
| | | if (result.isSuccess && result.data) |
| | | await authStore.syncAuthInfo(result.data) |
| | | } |
| | | catch (error) { |
| | | console.error('[dashboard] /account/api/v1/getSysUserInfo error', error) |
| | | } |
| | | // dashboard 先保留同一套入口初始化,兼容 SSO 回调地址切换期间的中间态。 |
| | | await bootstrapAuthEntry(route, 'dashboard') |
| | | }) |
| | | </script> |
| | | |
| | |
| | | <n-gi :span="1" class="h-368px flex flex-col"> |
| | | <div class="box relative"> |
| | | <div class="head"> |
| | | 点位柱状图 |
| | | 每天点位情况<span class="header-tit">统计7天范围内,每天的点位情况</span> |
| | | </div> |
| | | <div class="legend"> |
| | | <div class="legend-item"> |
| | |
| | | <n-gi :span="1" class="h-368px flex flex-col"> |
| | | <div class="box"> |
| | | <div class="head"> |
| | | 点位状态占比 |
| | | 点位整体情况<span class="header-tit">统计全部点位的正常点位和异常点位占比</span> |
| | | </div> |
| | | <div ref="pointPieRef" class="box-content" /> |
| | | </div> |
| | |
| | | <n-gi :span="1" class="h-368px flex flex-col"> |
| | | <div class="box relative"> |
| | | <div class="head"> |
| | | 打卡柱状图 |
| | | 每天打卡情况<span class="header-tit">统计7天范围内,每天的打卡情况</span> |
| | | </div> |
| | | <div class="legend"> |
| | | <div class="legend-item"> |
| | |
| | | <n-gi :span="1" class="h-368px flex flex-col"> |
| | | <div class="box"> |
| | | <div class="head"> |
| | | 打卡状态占比 |
| | | 打卡整体情况<span class="header-tit">统计所有运维工作人员打卡情况</span> |
| | | </div> |
| | | <div ref="checkInPieRef" class="box-content" /> |
| | | </div> |
| | |
| | | border-radius: 3px; |
| | | background: #0068FF; |
| | | } |
| | | |
| | | .header-tit { |
| | | font-size: 12px; |
| | | color: #868686; |
| | | margin-left: 12px; |
| | | } |
| | | } |
| | | |
| | | .box-content { |
| | |
| | | xczHmDuration: string |
| | | headShowCount: string |
| | | nearPointDistance: any |
| | | nearPointDistanceSearch: any |
| | | operateFree: boolean |
| | | temperatureUnit: string |
| | | } |
| | |
| | | xczHmDuration: '', |
| | | headShowCount: '', |
| | | nearPointDistance: '', |
| | | nearPointDistanceSearch: '', |
| | | operateFree: true, |
| | | temperatureUnit: '℃', |
| | | }) |
| | |
| | | // 仅保留数字,并在输入阶段直接截断为最多5位,确保不超过99999 |
| | | const normalizedValue = value.replace(/\D/g, '').slice(0, 5) |
| | | formModel.value.nearPointDistance = normalizedValue |
| | | } |
| | | |
| | | function handleNearPointDistanceSearchChange(value: string) { |
| | | // 仅保留数字,并在输入阶段直接截断为最多5位,确保不超过99999 |
| | | const normalizedValue = value.replace(/\D/g, '').slice(0, 5) |
| | | formModel.value.nearPointDistanceSearch = normalizedValue |
| | | } |
| | | |
| | | const rules: FormRules = { |
| | |
| | | nearPointDistance: [ |
| | | { required: true, message: '请输入正整数', trigger: 'blur' }, |
| | | ], |
| | | nearPointDistanceSearch: [ |
| | | { required: true, message: '请输入正整数', trigger: 'blur' }, |
| | | ], |
| | | temperatureUnit: [ |
| | | { required: true, message: '请选择', trigger: 'change' }, |
| | | ], |
| | |
| | | arriveTime: Number(formModel.value.arriveTime ?? 0), |
| | | headShowCount: Number(formModel.value.headShowCount ?? 0), |
| | | nearPointDistance: Number(formModel.value.nearPointDistance ?? 0), |
| | | nearPointDistanceSearch: Number(formModel.value.nearPointDistanceSearch ?? 0), |
| | | operateFree: formModel.value.operateFree, |
| | | temperatureUnit: formModel.value.temperatureUnit, |
| | | xczSyDistance: Number(formModel.value.xczSyDistance ?? 0), |
| | |
| | | formModel.value.arriveTime = d.arriveTime ? d.arriveTime.toString() : '' |
| | | formModel.value.headShowCount = d.headShowCount ? d.headShowCount.toString() : '' |
| | | formModel.value.nearPointDistance = d.nearPointDistance ? d.nearPointDistance.toString() : '' |
| | | formModel.value.nearPointDistanceSearch = d.nearPointDistanceSearch ? d.nearPointDistanceSearch.toString() : '' |
| | | formModel.value.operateFree = d.operateFree |
| | | formModel.value.temperatureUnit = d.temperatureUnit || '℃' |
| | | formModel.value.xczSyDistance = d.xczSyDistance ? d.xczSyDistance.toString() : '' |
| | |
| | | <span class="inline-note">内</span> |
| | | </div> |
| | | </n-form-item> |
| | | <n-form-item label="小程序首页地图推荐距离" path="nearPointDistanceSearch" required class="row-item"> |
| | | <div class="inline-input"> |
| | | <n-input-group style="width: 222px;"> |
| | | <n-input |
| | | :value="formModel.nearPointDistanceSearch" placeholder="请输入" |
| | | :allow-input="onlyAllowNumberAndDash" |
| | | @update:value="handleNearPointDistanceSearchChange" |
| | | /> |
| | | <n-input-group-label>m</n-input-group-label> |
| | | </n-input-group> |
| | | <span class="inline-note">内</span> |
| | | </div> |
| | | </n-form-item> |
| | | <n-form-item label="小程序天气单位" path="temperatureUnit" required class="row-item"> |
| | | <n-select |
| | | v-model:value="formModel.temperatureUnit" |
| | |
| | | <script setup lang="tsx"> |
| | | import { computed, onMounted, onUnmounted, ref } from 'vue' |
| | | import type { DataTableColumns, DataTableRowKey } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { bootstrapAuthEntry, useBoolean } from '@/hooks' |
| | | import { usePermission } from '@/hooks/usePermission' |
| | | import { cancelAndCloseOrder, deleteOrder, exportOrderExcel, getFleetList, getOrderList, refundOrder } from '@/service/api/transaction' |
| | | import { operationPageList } from '@/service/api/vehicleConvoy' |
| | | import { getRegionCascaderNoArea } from '@/service/api/operations' |
| | |
| | | const { t } = useI18n() |
| | | const message = useMessage() |
| | | const router = useRouter() |
| | | const route = useRoute() |
| | | const dialog = useDialog() |
| | | const { hasPermission } = usePermission() |
| | | // 默认落地页允许进入,但无 6011 权限时不请求列表和相关下拉数据。 |
| | | const canViewOrderList = computed(() => hasPermission(6011)) |
| | | |
| | | const initialModel = { |
| | | orderNumber: '', |
| | |
| | | }) |
| | | |
| | | function handleSearch() { |
| | | if (!canViewOrderList.value) |
| | | return |
| | | pageParams.value.pageNum = 1 |
| | | getList() |
| | | } |
| | |
| | | const count = ref(0) |
| | | |
| | | async function getList() { |
| | | if (!canViewOrderList.value) { |
| | | listData.value = [] |
| | | count.value = 0 |
| | | return |
| | | } |
| | | startLoading() |
| | | const params = { |
| | | ...pageParams.value, |
| | |
| | | } |
| | | |
| | | async function handleExport() { |
| | | if (!canViewOrderList.value) |
| | | return |
| | | let params: any = {} |
| | | if (checkedRowKeysRef.value.length > 0) { |
| | | params.ids = checkedRowKeysRef.value |
| | |
| | | } |
| | | |
| | | async function fetchCascaderData() { |
| | | if (!canViewOrderList.value) |
| | | return |
| | | const res = await getRegionCascaderNoArea() |
| | | if (res.code === 200 && res.data) { |
| | | pcaOptions.value = formatCascaderData(res.data) |
| | |
| | | } |
| | | |
| | | async function fetchFleetList() { |
| | | if (!canViewOrderList.value) |
| | | return |
| | | const res = await getFleetList() |
| | | if (res.code === 200) { |
| | | options.value = res.data.map((item: any) => ({ |
| | |
| | | } |
| | | |
| | | async function fetchPlateNumberOptions() { |
| | | if (!canViewOrderList.value) { |
| | | plateNumberOptions.value = [] |
| | | return |
| | | } |
| | | const res = await operationPageList({ |
| | | pageNum: 1, |
| | | pageSize: 99999, |
| | |
| | | |
| | | let timer: any = null |
| | | |
| | | onMounted(() => { |
| | | onMounted(async () => { |
| | | // 承接原 dashboard 的入口初始化逻辑,确保默认首页切换后 |
| | | // SSO 回调和用户信息同步仍然会在首屏完成。 |
| | | const handledAuthEntry = await bootstrapAuthEntry(route, 'orderManagement') |
| | | if (handledAuthEntry) |
| | | return |
| | | |
| | | // 页面允许访问,但没有查看权限时保持空态,不发列表/轮询请求。 |
| | | if (!canViewOrderList.value) { |
| | | listData.value = [] |
| | | count.value = 0 |
| | | return |
| | | } |
| | | getList() |
| | | fetchCascaderData() |
| | | fetchFleetList() |
| | |
| | | } |
| | | |
| | | function changePage(page: number, size: number) { |
| | | if (!canViewOrderList.value) |
| | | return |
| | | pageParams.value.pageNum = page |
| | | pageParams.value.pageSize = size |
| | | getList() |