diff --git a/package.json b/package.json index 77aad6d..dd809d4 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ }, "browserslist": [ "> 1%", - "last 2 versions" + "last 2 versions", + "Edge >= 18" ] } diff --git a/public/index.html b/public/index.html index 2b34200..c99830e 100644 --- a/public/index.html +++ b/public/index.html @@ -194,6 +194,19 @@ opacity: 0.5; } +
diff --git a/src/components/FileUpload/index.vue b/src/components/FileUpload/index.vue index ca37c55..217f387 100644 --- a/src/components/FileUpload/index.vue +++ b/src/components/FileUpload/index.vue @@ -105,7 +105,8 @@ export default { mounted() { if (this.drag && !this.disabled) { this.$nextTick(() => { - const element = this.$refs.uploadFileList?.$el || this.$refs.uploadFileList + const uploadFileListRef = this.$refs.uploadFileList + const element = uploadFileListRef && uploadFileListRef.$el ? uploadFileListRef.$el : uploadFileListRef Sortable.create(element, { ghostClass: 'file-upload-darg', onEnd: (evt) => { diff --git a/src/components/ImageUpload/index.vue b/src/components/ImageUpload/index.vue index 6b24f6e..d46ae3c 100644 --- a/src/components/ImageUpload/index.vue +++ b/src/components/ImageUpload/index.vue @@ -110,7 +110,12 @@ export default { mounted() { if (this.drag && !this.disabled) { this.$nextTick(() => { - const element = this.$refs.imageUpload?.$el?.querySelector('.el-upload-list') + const imageUploadRef = this.$refs.imageUpload + const uploadRoot = imageUploadRef && imageUploadRef.$el ? imageUploadRef.$el : null + const element = uploadRoot ? uploadRoot.querySelector('.el-upload-list') : null + if (!element) { + return + } Sortable.create(element, { onEnd: (evt) => { const movedItem = this.fileList.splice(evt.oldIndex, 1)[0] diff --git a/src/lang/dashboard-messages.js b/src/lang/dashboard-messages.js index 063afc4..f46237e 100644 --- a/src/lang/dashboard-messages.js +++ b/src/lang/dashboard-messages.js @@ -7,23 +7,23 @@ const dashboardMessages = { enabled: "启用数", disabled: "禁用数", claimed: "已认领数", - unclaimed: "未认领数", + unclaimed: "未认领数" }, map: { title: "设备轨迹点", serviceLabel: "地图服务", - empty: "暂无设备坐标数据", + empty: "暂无设备坐标数据" }, provider: { maptiler: "MapTiler", amap: "高德地图", - google: "谷歌地图", + google: "谷歌地图" }, popup: { device: "设备", time: "时间", remark: "备注", - coordinates: "坐标", + coordinates: "坐标" }, message: { mapConfigLoadFailed: "地图配置加载失败", @@ -32,10 +32,10 @@ const dashboardMessages = { amapLoadFailed: "高德地图加载失败", missingMaptilerKey: "当前企业未配置 MapTiler Key", missingGoogleKey: "当前企业未配置谷歌地图 Key", - missingAmapKey: "当前企业未配置高德地图 Key", - }, - }, - }, + missingAmapKey: "当前企业未配置高德地图 Key" + } + } + } }, "en-US": { dashboard: { @@ -45,23 +45,23 @@ const dashboardMessages = { enabled: "Enabled", disabled: "Disabled", claimed: "Claimed", - unclaimed: "Unclaimed", + unclaimed: "Unclaimed" }, map: { title: "Device Track Points", serviceLabel: "Map Service", - empty: "No device coordinate data", + empty: "No device coordinate data" }, provider: { maptiler: "MapTiler", amap: "Amap", - google: "Google Maps", + google: "Google Maps" }, popup: { device: "Device", time: "Time", remark: "Remark", - coordinates: "Coordinates", + coordinates: "Coordinates" }, message: { mapConfigLoadFailed: "Failed to load map configuration", @@ -70,10 +70,10 @@ const dashboardMessages = { amapLoadFailed: "Failed to load Amap", missingMaptilerKey: "MapTiler key is not configured for this business", missingGoogleKey: "Google Maps key is not configured for this business", - missingAmapKey: "Amap key is not configured for this business", - }, - }, - }, + missingAmapKey: "Amap key is not configured for this business" + } + } + } }, "fr-FR": { dashboard: { @@ -83,23 +83,23 @@ const dashboardMessages = { enabled: "Actifs", disabled: "Desactives", claimed: "Reclames", - unclaimed: "Non reclames", + unclaimed: "Non reclames" }, map: { title: "Points de trajectoire", serviceLabel: "Service de carte", - empty: "Aucune coordonnee appareil", + empty: "Aucune coordonnee appareil" }, provider: { maptiler: "MapTiler", amap: "Amap", - google: "Google Maps", + google: "Google Maps" }, popup: { device: "Appareil", time: "Heure", remark: "Remarque", - coordinates: "Coordonnees", + coordinates: "Coordonnees" }, message: { mapConfigLoadFailed: "Echec du chargement de la configuration de carte", @@ -108,10 +108,10 @@ const dashboardMessages = { amapLoadFailed: "Echec du chargement d'Amap", missingMaptilerKey: "La cle MapTiler n'est pas configuree", missingGoogleKey: "La cle Google Maps n'est pas configuree", - missingAmapKey: "La cle Amap n'est pas configuree", - }, - }, - }, + missingAmapKey: "La cle Amap n'est pas configuree" + } + } + } }, "es-ES": { dashboard: { @@ -121,23 +121,23 @@ const dashboardMessages = { enabled: "Habilitados", disabled: "Deshabilitados", claimed: "Reclamados", - unclaimed: "No reclamados", + unclaimed: "No reclamados" }, map: { title: "Puntos de trayectoria", serviceLabel: "Servicio de mapa", - empty: "No hay coordenadas de dispositivos", + empty: "No hay coordenadas de dispositivos" }, provider: { maptiler: "MapTiler", amap: "Amap", - google: "Google Maps", + google: "Google Maps" }, popup: { device: "Dispositivo", time: "Hora", remark: "Observacion", - coordinates: "Coordenadas", + coordinates: "Coordenadas" }, message: { mapConfigLoadFailed: "No se pudo cargar la configuracion del mapa", @@ -146,10 +146,10 @@ const dashboardMessages = { amapLoadFailed: "No se pudo cargar Amap", missingMaptilerKey: "MapTiler key no esta configurada", missingGoogleKey: "Google Maps key no esta configurada", - missingAmapKey: "Amap key no esta configurada", - }, - }, - }, + missingAmapKey: "Amap key no esta configurada" + } + } + } }, "pt-BR": { dashboard: { @@ -159,23 +159,23 @@ const dashboardMessages = { enabled: "Ativos", disabled: "Desativados", claimed: "Reivindicados", - unclaimed: "Nao reivindicados", + unclaimed: "Nao reivindicados" }, map: { title: "Pontos de trajeto", serviceLabel: "Servico de mapa", - empty: "Sem dados de coordenadas de dispositivo", + empty: "Sem dados de coordenadas de dispositivo" }, provider: { maptiler: "MapTiler", amap: "Amap", - google: "Google Maps", + google: "Google Maps" }, popup: { device: "Dispositivo", time: "Hora", remark: "Observacao", - coordinates: "Coordenadas", + coordinates: "Coordenadas" }, message: { mapConfigLoadFailed: "Falha ao carregar configuracao do mapa", @@ -184,11 +184,11 @@ const dashboardMessages = { amapLoadFailed: "Falha ao carregar Amap", missingMaptilerKey: "MapTiler key nao configurada", missingGoogleKey: "Google Maps key nao configurada", - missingAmapKey: "Amap key nao configurada", - }, - }, - }, - }, + missingAmapKey: "Amap key nao configurada" + } + } + } + } }; export default dashboardMessages; diff --git a/src/lang/index.js b/src/lang/index.js index c92dc3f..49856f2 100644 --- a/src/lang/index.js +++ b/src/lang/index.js @@ -6,6 +6,7 @@ import dashboardMessages from "./dashboard-messages"; import systemMessages from "./system-messages"; import systemUserDeviceMessages from "./system-user-device-messages"; import profileMessages from "./profile-messages"; +import noPermissionMessages from "./no-permission-messages"; import { getLanguage } from "@/utils/language"; const DEFAULT_LANGUAGE = "zh-CN"; @@ -54,7 +55,7 @@ const mergedMessages = mergeLocaleMessages( ); const mergedMessagesWithDashboard = mergeLocaleMessages( - mergedMessages, + mergeLocaleMessages(mergedMessages, noPermissionMessages), dashboardMessages ); diff --git a/src/main.js b/src/main.js index 939380a..58bafa8 100644 --- a/src/main.js +++ b/src/main.js @@ -19,7 +19,7 @@ import router from "./router"; import directive from "./directive"; // directive import plugins from "./plugins"; // plugins import { download } from "@/utils/ruoyi"; -import { math } from "@/utils/math.js"; +import { math, warmupMathjs } from "@/utils/math.js"; import I18nPlugin, { t as i18nT } from "@/lang"; import { getLanguage } from "@/utils/language"; @@ -131,9 +131,15 @@ if (typeof document !== "undefined") { : process.env.VUE_APP_TITLE; } -new Vue({ +const app = new Vue({ el: "#app", router, store, render: (h) => h(App), }); + +if (app && typeof app.$nextTick === "function") { + app.$nextTick(() => { + warmupMathjs(); + }); +} diff --git a/src/permission.js b/src/permission.js index b66190b..38ec31b 100644 --- a/src/permission.js +++ b/src/permission.js @@ -1,63 +1,75 @@ -import router from './router' -import store from './store' -import { Message } from 'element-ui' -import NProgress from 'nprogress' -import 'nprogress/nprogress.css' -import { getToken } from '@/utils/auth' -import { isPathMatch } from '@/utils/validate' -import { isRelogin } from '@/utils/request' +import router from "./router"; +import store from "./store"; +import { Message } from "element-ui"; +import NProgress from "nprogress"; +import "nprogress/nprogress.css"; +import { getToken } from "@/utils/auth"; +import { isPathMatch } from "@/utils/validate"; +import { isRelogin } from "@/utils/request"; -NProgress.configure({ showSpinner: false }) +NProgress.configure({ showSpinner: false }); -const whiteList = ['/login', '/register'] +const whiteList = ["/login", "/register"]; const isWhiteList = (path) => { - return whiteList.some(pattern => isPathMatch(pattern, path)) -} + return whiteList.some((pattern) => isPathMatch(pattern, path)); +}; + +const hasHomeRoute = (routes) => { + return Array.isArray(routes) && routes.some((route) => route && route.path === "/" && route.redirect === "/index"); +}; router.beforeEach((to, from, next) => { - NProgress.start() + NProgress.start(); if (getToken()) { - to.meta.title && store.dispatch('settings/setTitle', to.meta.title) - /* has token*/ - if (to.path === '/login') { - next({ path: '/' }) - NProgress.done() - } else if (isWhiteList(to.path)) { - next() - } else { - if (store.getters.roles.length === 0) { - isRelogin.show = true - // 判断当前用户是否已拉取完user_info信息 - store.dispatch('GetInfo').then(() => { - isRelogin.show = false - store.dispatch('GenerateRoutes').then(accessRoutes => { - // 根据roles权限生成可访问的路由表 - router.addRoutes(accessRoutes) // 动态添加可访问路由表 - next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 - }) - }).catch(err => { - store.dispatch('LogOut').then(() => { - Message.error(err) - next({ path: '/' }) - }) - }) - } else { - next() - } + if (to.meta.title) { + store.dispatch("settings/setTitle", to.meta.title); + } + if (to.path === "/login") { + next({ path: "/" }); + NProgress.done(); + return; } - } else { - // 没有token if (isWhiteList(to.path)) { - // 在免登录白名单,直接进入 - next() + next(); + return; + } + if (store.getters.roles.length === 0) { + isRelogin.show = true; + store.dispatch("GetInfo").then(() => { + isRelogin.show = false; + store.dispatch("GenerateRoutes").then((accessRoutes) => { + router.addRoutes(accessRoutes); + if (!hasHomeRoute(accessRoutes) && to.path !== "/no-permission") { + next({ path: "/no-permission", replace: true }); + return; + } + next({ ...to, replace: true }); + }); + }).catch((err) => { + store.dispatch("LogOut").then(() => { + Message.error(err); + next({ path: "/" }); + }); + }); + return; + } + next(); + return; + } + + if (isWhiteList(to.path)) { + next(); + } else { + if (to.path === "/no-permission") { + next("/login"); } else { - next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页 - NProgress.done() + next(`/login?redirect=${encodeURIComponent(to.fullPath)}`); } + NProgress.done(); } -}) +}); router.afterEach(() => { - NProgress.done() -}) + NProgress.done(); +}); diff --git a/src/router/index.js b/src/router/index.js index 4722ee3..1f3aa92 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -61,6 +61,11 @@ export const constantRoutes = [ component: () => import('@/views/error/401'), hidden: true }, + { + path: '/no-permission', + component: () => import('@/views/error/noPermission'), + hidden: true + }, // { // path: '', // component: Layout, diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js index 7b5b36c..bc543c5 100644 --- a/src/store/modules/permission.js +++ b/src/store/modules/permission.js @@ -35,27 +35,36 @@ const permission = { // 向后端请求路由数据 getRouters().then(res => { - const sdata = JSON.parse(JSON.stringify(res.data).replaceAll("/index/index","Layout").replaceAll("\"component\":\"/","\"component\":\"")); - const rdata = JSON.parse(JSON.stringify(res.data).replaceAll("/index/index","Layout").replaceAll("\"component\":\"/","\"component\":\"")) + const routeJson = JSON.stringify(res.data) + .split("/index/index").join("Layout") + .split("\"component\":\"/").join("\"component\":\"") + const sdata = JSON.parse(routeJson); + const rdata = JSON.parse(routeJson) const sidebarRoutes = filterAsyncRouter(sdata) const rewriteRoutes = filterAsyncRouter(rdata, false, true) - const first = sidebarRoutes?.[0]?.children?.[0] + const first = findFirstLeafRoute(sidebarRoutes) if (first) { console.log(first) rewriteRoutes.push( { - path: "", + path: "/", component: Layout, - redirect: "index", + redirect: "/index", children: [ { path: "index", component: first.component, - name: first.name, + name: "Index", meta: first.meta }, ] }) + } else { + rewriteRoutes.push({ + path: "/", + redirect: "/no-permission", + hidden: true + }) } const asyncRoutes = filterDynamicRoutes(dynamicRoutes) rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true }) @@ -113,6 +122,24 @@ function filterChildren(childrenMap, lastRouter = false) { return children } +function findFirstLeafRoute(routes) { + if (!Array.isArray(routes)) { + return null + } + for (const route of routes) { + if (route.children && route.children.length) { + const child = findFirstLeafRoute(route.children) + if (child) { + return child + } + } + if (route.component && route.component !== Layout) { + return route + } + } + return null +} + // 动态路由遍历,验证是否具备权限 export function filterDynamicRoutes(routes) { const res = [] diff --git a/src/utils/math.js b/src/utils/math.js index 9323b25..881919e 100644 --- a/src/utils/math.js +++ b/src/utils/math.js @@ -1,33 +1,94 @@ -const $math = require('mathjs') -export const math = { - add (a,b) { - return $math.format( - $math.add( - $math.bignumber(a),$math.bignumber(b) - ) - ) - }, - subtract(a,b) { - return $math.format( - $math.subtract( - $math.bignumber(a),$math.bignumber(b) - ) - ) - }, - multiply (a,b) { - return $math.format( - $math.multiply( - $math.bignumber(a),$math.bignumber(b) - ) - ) - }, - divide(a,b) { - return $math.format( - $math.divide( - $math.bignumber(a),$math.bignumber(b) - ) - ) - } +const Big = require('big.js') + +let mathInstance = null +let mathPromise = null + +const BIG_METHOD_MAP = { + add: 'plus', + subtract: 'minus', + multiply: 'times', + divide: 'div' +} + +function normalizeMathModule(mod) { + return mod && mod.default ? mod.default : mod +} + +function createBig(value) { + return new Big(value) +} + +function formatBigResult(result) { + return result.toFixed() +} + +function calculateWithBig(type, a, b) { + const left = createBig(a) + const right = createBig(b) + return formatBigResult(left[BIG_METHOD_MAP[type]](right)) +} + +function calculateWithMathjs(type, a, b) { + return mathInstance.format( + mathInstance[type]( + mathInstance.bignumber(a), + mathInstance.bignumber(b) + ) + ) +} + +function calculate(type, a, b) { + if (mathInstance) { + return calculateWithMathjs(type, a, b) + } + return calculateWithBig(type, a, b) } +export function loadMathjs() { + if (mathInstance) { + return Promise.resolve(mathInstance) + } + if (!mathPromise) { + mathPromise = import(/* webpackChunkName: "mathjs" */ 'mathjs') + .then(mod => { + mathInstance = normalizeMathModule(mod) + return mathInstance + }) + .catch(err => { + mathPromise = null + throw err + }) + } + return mathPromise +} + +export function warmupMathjs() { + const run = () => { + loadMathjs().catch(() => {}) + } + if (typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function') { + window.requestIdleCallback(run, { timeout: 1500 }) + return + } + if (typeof window !== 'undefined') { + window.setTimeout(run, 0) + } +} +export const math = { + add(a, b) { + return calculate('add', a, b) + }, + subtract(a, b) { + return calculate('subtract', a, b) + }, + multiply(a, b) { + return calculate('multiply', a, b) + }, + divide(a, b) { + return calculate('divide', a, b) + }, + ready() { + return loadMathjs() + } +} diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue index 8e7854b..d089304 100644 --- a/src/views/dashboard/index.vue +++ b/src/views/dashboard/index.vue @@ -3,23 +3,58 @@
{{ $t("dashboard.overview.stats.total") }}
-
{{ stats.total }}
+
+ +
{{ $t("dashboard.overview.stats.enabled") }}
-
{{ stats.enabled }}
+
+ +
{{ $t("dashboard.overview.stats.disabled") }}
-
{{ stats.disabled }}
+
+ +
{{ $t("dashboard.overview.stats.claimed") }}
-
{{ stats.claimed }}
+
+ +
{{ $t("dashboard.overview.stats.unclaimed") }}
-
{{ stats.unclaimed }}
+
+ +
@@ -67,6 +102,7 @@ import { getDashboardOverview, getDeviceTrajectoryMapConfig } from "@/api/device/device"; import { loadAMap } from "@/utils/loadAMap"; import { loadLeaflet } from "@/utils/loadLeaflet"; +import CountTo from "vue-count-to"; const DEFAULT_CENTER = [31.2304, 121.4737]; const AMAP_DEFAULT_CENTER = [121.4737, 31.2304]; @@ -86,14 +122,31 @@ function getDefaultStats() { }; } +function getDefaultStatVersions() { + return { + total: 0, + enabled: 0, + disabled: 0, + claimed: 0, + unclaimed: 0, + }; +} + export default { name: "DashboardOverview", + components: { + CountTo, + }, data() { return { loading: false, mapLoading: false, mapLoadError: "", stats: getDefaultStats(), + displayStats: getDefaultStats(), + statVersions: getDefaultStatVersions(), + statTimers: [], + statDelayStep: 140, devicePoints: [], mapProvider: "amap", mapConfig: null, @@ -140,6 +193,7 @@ export default { }, beforeDestroy() { window.removeEventListener("resize", this.handleWindowResize); + this.clearStatTimers(); if (this.resizeTimer) { clearTimeout(this.resizeTimer); this.resizeTimer = null; @@ -204,22 +258,45 @@ export default { await this.initCurrentMap(); this.renderMapPoints(); }, + clearStatTimers() { + if (!Array.isArray(this.statTimers) || !this.statTimers.length) { + return; + } + this.statTimers.forEach((timer) => clearTimeout(timer)); + this.statTimers = []; + }, + animateStats(targetStats) { + const keys = ["total", "enabled", "disabled", "claimed", "unclaimed"]; + this.clearStatTimers(); + this.stats = { ...targetStats }; + this.displayStats = getDefaultStats(); + + keys.forEach((key, index) => { + this.statVersions[key] = (this.statVersions[key] || 0) + 1; + const timer = setTimeout(() => { + this.$set(this.displayStats, key, Number(targetStats[key]) || 0); + this.$set(this.statVersions, key, (this.statVersions[key] || 0) + 1); + }, 120 + index * this.statDelayStep); + this.statTimers.push(timer); + }); + }, async loadDashboardData() { this.loading = true; try { const response = await getDashboardOverview(); const data = response && response.data ? response.data : {}; - this.stats = { + const nextStats = { total: Number(data.totalCount) || 0, enabled: Number(data.enabledCount) || 0, disabled: Number(data.disabledCount) || 0, claimed: Number(data.claimedCount) || 0, unclaimed: Number(data.unclaimedCount) || 0, }; + this.animateStats(nextStats); this.devicePoints = this.buildDevicePoints(data.points); this.renderMapPoints(); } catch (error) { - this.stats = getDefaultStats(); + this.animateStats(getDefaultStats()); this.devicePoints = []; this.clearMapMarkers(); this.$message.error((error && error.message) || this.$t("dashboard.overview.message.dataLoadFailed")); @@ -422,7 +499,7 @@ export default { const marker = this.mapsApi.marker([point.latNum, point.lngNum], { title: point.alias || point.sn || String(point.id || ""), }); - marker.bindPopup(this.buildPopupContent(point)); + marker.bindPopup(this.buildPopupContentSafe(point)); marker.addTo(this.map); this.mapMarkers.push(marker); bounds.push([point.latNum, point.lngNum]); @@ -461,7 +538,7 @@ export default { offset: new this.mapsApi.Pixel(0, -24), }); } - this.mapInfoWindow.setContent(this.buildPopupContent(point)); + this.mapInfoWindow.setContent(this.buildPopupContentSafe(point)); this.mapInfoWindow.open(this.map, [point.lngNum, point.latNum]); }); return marker; @@ -476,6 +553,23 @@ export default { }); } }, + buildPopupContentSafe(point) { + const separator = ": "; + return ( + '
' + + `
${this.escapeHtml(this.$t("dashboard.overview.popup.device"))}${separator}${this.escapeHtml( + point.alias || point.sn || point.id || "-" + )}
` + + `
${this.escapeHtml(this.$t("dashboard.overview.popup.time"))}${separator}${this.escapeHtml( + this.formatDateTime(point.lastLocationTime) + )}
` + + `
${this.escapeHtml(this.$t("dashboard.overview.popup.remark"))}${separator}${this.escapeHtml( + point.remark || "-" + )}
` + + + "
" + ); + }, buildPopupContent(point) { return ( '
' + diff --git a/src/views/login.vue b/src/views/login.vue index 8ff74e9..5ed26e8 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -1,7 +1,7 @@