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 (
+ '"
+ );
+ },
buildPopupContent(point) {
return (
'