refactor: 优化项目代码并修复多处问题
- 统一替换多处页面硬编码的拖拽图标路径为导入的静态资源变量
- 优化路由存储初始化逻辑,添加并发缓存避免重复调用接口
- 修复标签页管理逻辑,解决首页因query/hash不同导致的重复标签问题
- 添加全局错误捕获与日志功能,记录各类运行时错误
- 调整开发环境配置文件的注释URL
- 优化布局菜单的类型转换逻辑,提取为计算属性减少重复代码
| | |
| | | // url: 'http://localhost:3000', |
| | | url: 'http://221.182.45.100:8000', |
| | | // url: 'http://rt-admin.test.xiaopeng.local', |
| | | // url: 'https://rt-api.deploy-test.xiaopeng.com', |
| | | // url: 'http://rt-api.deploy-test.xiaopeng.com', |
| | | }, |
| | | production: { |
| | | url: 'https://rt-admin.deploy-test.xiaopeng.com', |
| | |
| | | const routeStore = useRouteStore() |
| | | |
| | | const { layoutMode } = storeToRefs(useAppStore()) |
| | | const layoutMenus = computed(() => routeStore.menus as MenuOption[]) |
| | | |
| | | const { |
| | | layout, |
| | |
| | | } = useLayoutMenu({ |
| | | mode: layoutMode, |
| | | accordion: true, |
| | | menus: routeStore.menus as MenuOption[], |
| | | menus: layoutMenus, |
| | | }) |
| | | |
| | | watch(() => route.path, () => { |
| | |
| | | import { router } from './router' |
| | | import App from './App.vue' |
| | | |
| | | function getCurrentRouteInfo() { |
| | | const currentRoute = router.currentRoute.value |
| | | return { |
| | | name: currentRoute?.name ?? '', |
| | | path: currentRoute?.path ?? '', |
| | | fullPath: currentRoute?.fullPath ?? '', |
| | | query: currentRoute?.query ?? {}, |
| | | params: currentRoute?.params ?? {}, |
| | | } |
| | | } |
| | | |
| | | function logGlobalError(type: string, payload: Record<string, unknown>) { |
| | | console.error(`[global-error] ${type}`, { |
| | | ...payload, |
| | | route: getCurrentRouteInfo(), |
| | | timestamp: new Date().toISOString(), |
| | | }) |
| | | } |
| | | |
| | | window.addEventListener('error', (event) => { |
| | | logGlobalError('window.error', { |
| | | message: event.message, |
| | | filename: event.filename, |
| | | lineno: event.lineno, |
| | | colno: event.colno, |
| | | error: event.error, |
| | | }) |
| | | }) |
| | | |
| | | window.addEventListener('unhandledrejection', (event) => { |
| | | logGlobalError('window.unhandledrejection', { |
| | | reason: event.reason, |
| | | }) |
| | | }) |
| | | |
| | | // 创建应用实例并挂载 |
| | | const app = createApp(App) |
| | | |
| | | app.config.errorHandler = (error, instance, info) => { |
| | | logGlobalError('vue.errorHandler', { |
| | | info, |
| | | component: instance?.type, |
| | | error, |
| | | }) |
| | | } |
| | | |
| | | app.mount('#app') |
| | |
| | | import { $t, local } from '@/utils' |
| | | import { createMenus, createRoutes, generateCacheRoutes } from './helper' |
| | | |
| | | let initAuthRoutePromise: Promise<void> | null = null |
| | | |
| | | interface RoutesStatus { |
| | | isInitAuthRoute: boolean |
| | | menus: MenuOption[] |
| | |
| | | } |
| | | }, |
| | | async initAuthRoute() { |
| | | if (this.isInitAuthRoute) |
| | | return |
| | | |
| | | if (initAuthRoutePromise) |
| | | return initAuthRoutePromise |
| | | |
| | | this.isInitAuthRoute = false |
| | | |
| | | try { |
| | | // Initialize route information |
| | | const rowRoutes = await this.initRouteInfo() |
| | | console.log('rowRoutes:', rowRoutes) |
| | | if (!rowRoutes) { |
| | | const error = new Error('Failed to get route information') |
| | | window.$message.error($t(`app.getRouteError`)) |
| | | initAuthRoutePromise = (async () => { |
| | | try { |
| | | // Initialize route information |
| | | const rowRoutes = await this.initRouteInfo() |
| | | console.log('rowRoutes:', rowRoutes) |
| | | if (!rowRoutes) { |
| | | const error = new Error('Failed to get route information') |
| | | window.$message.error($t(`app.getRouteError`)) |
| | | throw error |
| | | } |
| | | this.rowRoutes = rowRoutes |
| | | |
| | | // Generate actual route and insert |
| | | const routes = createRoutes(rowRoutes) |
| | | console.log('routes:', routes) |
| | | if (router.hasRoute('appRoot')) |
| | | router.removeRoute('appRoot') |
| | | router.addRoute(routes) |
| | | |
| | | // Generate side menu |
| | | this.menus = createMenus(rowRoutes) |
| | | |
| | | // Generate the route cache |
| | | this.cacheRoutes = generateCacheRoutes(rowRoutes) |
| | | |
| | | this.isInitAuthRoute = true |
| | | } |
| | | catch (error) { |
| | | // 重置状态并重新抛出错误 |
| | | this.isInitAuthRoute = false |
| | | throw error |
| | | } |
| | | this.rowRoutes = rowRoutes |
| | | finally { |
| | | initAuthRoutePromise = null |
| | | } |
| | | })() |
| | | |
| | | // Generate actual route and insert |
| | | const routes = createRoutes(rowRoutes) |
| | | console.log('routes:', routes) |
| | | router.addRoute(routes) |
| | | |
| | | // Generate side menu |
| | | this.menus = createMenus(rowRoutes) |
| | | |
| | | // Generate the route cache |
| | | this.cacheRoutes = generateCacheRoutes(rowRoutes) |
| | | |
| | | this.isInitAuthRoute = true |
| | | } |
| | | catch (error) { |
| | | // 重置状态并重新抛出错误 |
| | | this.isInitAuthRoute = false |
| | | throw error |
| | | } |
| | | return initAuthRoutePromise |
| | | }, |
| | | }, |
| | | }) |
| | |
| | | import type { RouteLocationNormalized } from 'vue-router' |
| | | import { router } from '@/router' |
| | | |
| | | const HOME_TAB_PATH = import.meta.env.VITE_HOME_PATH || '/dashboard' |
| | | |
| | | interface TabState { |
| | | pinTabs: RouteLocationNormalized[] |
| | | tabs: RouteLocationNormalized[] |
| | |
| | | allTabs: state => [...state.pinTabs, ...state.tabs], |
| | | }, |
| | | actions: { |
| | | normalizeTabPath(fullPath: string) { |
| | | if (!fullPath) { |
| | | return fullPath |
| | | } |
| | | if (fullPath === HOME_TAB_PATH || fullPath.startsWith(`${HOME_TAB_PATH}?`) || fullPath.startsWith(`${HOME_TAB_PATH}#`)) { |
| | | return HOME_TAB_PATH |
| | | } |
| | | return fullPath |
| | | }, |
| | | isHomeTabRoute(route: Pick<RouteLocationNormalized, 'path' | 'name' | 'fullPath'>) { |
| | | return route.path === HOME_TAB_PATH || route.name === 'dashboard' || this.normalizeTabPath(route.fullPath) === HOME_TAB_PATH |
| | | }, |
| | | normalizeTabRoute(route: RouteLocationNormalized) { |
| | | if (!this.isHomeTabRoute(route)) { |
| | | return route |
| | | } |
| | | return { |
| | | ...route, |
| | | path: HOME_TAB_PATH, |
| | | fullPath: HOME_TAB_PATH, |
| | | query: {}, |
| | | hash: '', |
| | | } as RouteLocationNormalized |
| | | }, |
| | | addTab(route: RouteLocationNormalized) { |
| | | // 根据meta确定是否不添加,可用于错误页,登录页等 |
| | | if (route.meta.withoutTab) |
| | | return |
| | | |
| | | const normalizedRoute = this.normalizeTabRoute(route) |
| | | |
| | | // 首页无论 query/hash 如何变化,都只保留一个固定页签 |
| | | if (this.isHomeTabRoute(normalizedRoute)) { |
| | | this.tabs = this.tabs.filter(item => !this.isHomeTabRoute(item)) |
| | | const index = this.pinTabs.findIndex(item => this.isHomeTabRoute(item)) |
| | | if (index > -1) { |
| | | this.pinTabs[index] = normalizedRoute |
| | | } |
| | | else { |
| | | this.pinTabs.push(normalizedRoute) |
| | | } |
| | | return |
| | | } |
| | | |
| | | // 如果标签名称已存在则不添加 |
| | | if (this.hasExistTab(route.fullPath as string)) |
| | | if (this.hasExistTab(normalizedRoute.fullPath as string)) |
| | | return |
| | | |
| | | // 根据meta.pinTab传递到不同的分组中 |
| | | if (route.meta.pinTab) |
| | | this.pinTabs.push(route) |
| | | if (normalizedRoute.meta.pinTab) |
| | | this.pinTabs.push(normalizedRoute) |
| | | else |
| | | this.tabs.push(route) |
| | | this.tabs.push(normalizedRoute) |
| | | }, |
| | | async closeTab(fullPath: string) { |
| | | fullPath = this.normalizeTabPath(fullPath) |
| | | const tabsLength = this.tabs.length |
| | | // 如果动态标签大于一个,才会标签跳转 |
| | | if (this.tabs.length > 1) { |
| | |
| | | }, |
| | | |
| | | closeOtherTabs(fullPath: string) { |
| | | fullPath = this.normalizeTabPath(fullPath) |
| | | const index = this.getTabIndex(fullPath) |
| | | this.tabs = this.tabs.filter((item, i) => i === index) |
| | | }, |
| | | closeLeftTabs(fullPath: string) { |
| | | fullPath = this.normalizeTabPath(fullPath) |
| | | const index = this.getTabIndex(fullPath) |
| | | this.tabs = this.tabs.filter((item, i) => i >= index) |
| | | }, |
| | | closeRightTabs(fullPath: string) { |
| | | fullPath = this.normalizeTabPath(fullPath) |
| | | const index = this.getTabIndex(fullPath) |
| | | this.tabs = this.tabs.filter((item, i) => i <= index) |
| | | }, |
| | |
| | | }, |
| | | |
| | | hasExistTab(fullPath: string) { |
| | | fullPath = this.normalizeTabPath(fullPath) |
| | | const _tabs = [...this.tabs, ...this.pinTabs] |
| | | return _tabs.some((item) => { |
| | | return item.fullPath === fullPath |
| | | return this.normalizeTabPath(item.fullPath) === fullPath |
| | | }) |
| | | }, |
| | | /* 设置当前激活的标签 */ |
| | | setCurrentTab(fullPath: string) { |
| | | this.currentTabPath = fullPath |
| | | this.currentTabPath = this.normalizeTabPath(fullPath) |
| | | }, |
| | | getTabIndex(fullPath: string) { |
| | | fullPath = this.normalizeTabPath(fullPath) |
| | | return this.tabs.findIndex((item) => { |
| | | return item.fullPath === fullPath |
| | | return this.normalizeTabPath(item.fullPath) === fullPath |
| | | }) |
| | | }, |
| | | modifyTab(fullPath: string, modifyFn: (route: RouteLocationNormalized) => void) { |
| | |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | import SelectPointModal from "../patrol-points/components/SelectPointModal.vue"; |
| | | import VehicleSelectModal from "@/views/assetManagement/fleetManagement/components/VehicleSelectModal.vue"; |
| | | import dragIcon from "@/assets/point/union.png"; |
| | | import { |
| | | addCruisePlan, |
| | | editCruisePlan, |
| | |
| | | > |
| | | <div class="w-16 flex"> |
| | | <img |
| | | src="/src/assets/point/union.png" |
| | | :src="dragIcon" |
| | | alt="拖动" |
| | | style="width: 15px; height: 15px" |
| | | @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }" |
| | |
| | | import SelectPointModal from '../patrol-points/components/SelectPointModal.vue' |
| | | import VehicleSelectModal from '@/views/assetManagement/fleetManagement/components/VehicleSelectModal.vue' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import dragIcon from '@/assets/point/union.png' |
| | | import { deleteCruiseOrder, editCruisePlan, exportCruiseOrderExcel, fetchCruiseOrderPageList, fetchCruisePlanDetail } from '@/service/api/cruise' |
| | | import { downloadFile } from '@/utils/common' |
| | | import { deriveDefaultValue } from 'naive-ui/es/color-picker/src/utils' |
| | |
| | | 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png'; |
| | | } |
| | | " |
| | | src="/src/assets/point/union.png" |
| | | :src="dragIcon" |
| | | alt="拖动" |
| | | style="width: 15px; height: 15px" |
| | | /> |
| | |
| | | import dayjs from 'dayjs' |
| | | import { addCruiseCheckInRule } from '@/service/api/maintenance' |
| | | import { getOperationsUserListNoPage } from '@/service/api/attendance' |
| | | import dragIcon from '@/assets/point/union.png' |
| | | import { useTabStore } from '@/store' |
| | | |
| | | const tabStore = useTabStore() |
| | |
| | | title: '排序', |
| | | key: 'sort', |
| | | width: 60, |
| | | render: () => <img src="/src/assets/point/union.png" alt="拖动" style="width: 15px; height: 15px" onError={(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }} />, |
| | | render: () => <img src={dragIcon} alt="拖动" style="width: 15px; height: 15px" />, |
| | | }, |
| | | { |
| | | title: '打卡地', |
| | |
| | | import SelectPointModal from '@/views/testDemo/specialPointManagement/components/SelectPointModal.vue' |
| | | import VehicleSelectModal from '@/views/assetManagement/fleetManagement/components/VehicleSelectModal.vue' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import dragIcon from '@/assets/point/union.png' |
| | | import { deleteCruiseOrder, editCruisePlan, exportCruiseOrderExcel, fetchCruiseOrderPageList, fetchCruisePlanDetail } from '@/service/api/copyText' |
| | | import { downloadFile } from '@/utils/common' |
| | | |
| | |
| | | @drop="handleDrop(point.id)" |
| | | > |
| | | <div class="w-16 flex"> |
| | | <img src="/src/assets/point/union.png" alt="拖动" style="width: 15px; height: 15px"> |
| | | <img :src="dragIcon" alt="拖动" style="width: 15px; height: 15px"> |
| | | </div> |
| | | <div class="flex-1 text-sm pr-4"> |
| | | {{ point.name }} |
| | |
| | | import { NButton, NDataTable, NForm, NFormItem, NInput, NModal, NPopconfirm, NSpace, useMessage } from 'naive-ui' |
| | | import type { DataTableColumns, FormInst, FormRules } from 'naive-ui' |
| | | import AiRichTextEditor from '@/components/custom/Editor/AiRichTextEditor/index.vue' |
| | | import dragIcon from '@/assets/point/union.png' |
| | | import { deleteLoginAgreement, getLoginAgreementPageList, saveLoginAgreement } from '@/service/api/operations' |
| | | |
| | | const props = defineProps<{ |
| | |
| | | render: () => { |
| | | return h('div', { class: 'sort-cell' }, [ |
| | | h('img', { |
| | | src: '/src/assets/point/union.png', |
| | | src: dragIcon, |
| | | alt: '拖动', |
| | | style: 'width: 15px; height: 15px;', |
| | | class: 'sort-icon', |
| | | }), |
| | | ]) |
| | | }, |
| | |
| | | .sort-cell { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .sort-icon { |
| | | width: 15px; |
| | | height: 15px; |
| | | display: block; |
| | | } |
| | | |
| | | .rich-text-wrapper { |