Browse Source

b端国际化 首页

master
hx 2 months ago
parent
commit
7cea2c9576
  1. 3
      package.json
  2. 13
      public/index.html
  3. 3
      src/components/FileUpload/index.vue
  4. 7
      src/components/ImageUpload/index.vue
  5. 82
      src/lang/dashboard-messages.js
  6. 3
      src/lang/index.js
  7. 10
      src/main.js
  8. 110
      src/permission.js
  9. 5
      src/router/index.js
  10. 39
      src/store/modules/permission.js
  11. 121
      src/utils/math.js
  12. 112
      src/views/dashboard/index.vue
  13. 93
      src/views/login.vue

3
package.json

@ -69,6 +69,7 @@
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
"last 2 versions" "last 2 versions",
"Edge >= 18"
] ]
} }

13
public/index.html

@ -194,6 +194,19 @@
opacity: 0.5; opacity: 0.5;
} }
</style> </style>
<script>
(function () {
var ua = window.navigator && window.navigator.userAgent ? window.navigator.userAgent : '';
var isLegacyEdge = /Edge\/\d+/i.test(ua) && !/Edg\//i.test(ua);
var search = window.location && window.location.search ? window.location.search : '';
if (!isLegacyEdge || /(?:\?|&)hotreload=false(?:&|$)/i.test(search)) {
return;
}
var nextSearch = search ? search + '&hotreload=false' : '?hotreload=false';
var nextUrl = window.location.pathname + nextSearch + (window.location.hash || '');
window.location.replace(nextUrl);
})();
</script>
</head> </head>
<body> <body>
<div id="app"> <div id="app">

3
src/components/FileUpload/index.vue

@ -105,7 +105,8 @@ export default {
mounted() { mounted() {
if (this.drag && !this.disabled) { if (this.drag && !this.disabled) {
this.$nextTick(() => { 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, { Sortable.create(element, {
ghostClass: 'file-upload-darg', ghostClass: 'file-upload-darg',
onEnd: (evt) => { onEnd: (evt) => {

7
src/components/ImageUpload/index.vue

@ -110,7 +110,12 @@ export default {
mounted() { mounted() {
if (this.drag && !this.disabled) { if (this.drag && !this.disabled) {
this.$nextTick(() => { 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, { Sortable.create(element, {
onEnd: (evt) => { onEnd: (evt) => {
const movedItem = this.fileList.splice(evt.oldIndex, 1)[0] const movedItem = this.fileList.splice(evt.oldIndex, 1)[0]

82
src/lang/dashboard-messages.js

@ -7,23 +7,23 @@ const dashboardMessages = {
enabled: "启用数", enabled: "启用数",
disabled: "禁用数", disabled: "禁用数",
claimed: "已认领数", claimed: "已认领数",
unclaimed: "未认领数", unclaimed: "未认领数"
}, },
map: { map: {
title: "设备轨迹点", title: "设备轨迹点",
serviceLabel: "地图服务", serviceLabel: "地图服务",
empty: "暂无设备坐标数据", empty: "暂无设备坐标数据"
}, },
provider: { provider: {
maptiler: "MapTiler", maptiler: "MapTiler",
amap: "高德地图", amap: "高德地图",
google: "谷歌地图", google: "谷歌地图"
}, },
popup: { popup: {
device: "设备", device: "设备",
time: "时间", time: "时间",
remark: "备注", remark: "备注",
coordinates: "坐标", coordinates: "坐标"
}, },
message: { message: {
mapConfigLoadFailed: "地图配置加载失败", mapConfigLoadFailed: "地图配置加载失败",
@ -32,10 +32,10 @@ const dashboardMessages = {
amapLoadFailed: "高德地图加载失败", amapLoadFailed: "高德地图加载失败",
missingMaptilerKey: "当前企业未配置 MapTiler Key", missingMaptilerKey: "当前企业未配置 MapTiler Key",
missingGoogleKey: "当前企业未配置谷歌地图 Key", missingGoogleKey: "当前企业未配置谷歌地图 Key",
missingAmapKey: "当前企业未配置高德地图 Key", missingAmapKey: "当前企业未配置高德地图 Key"
}, }
}, }
}, }
}, },
"en-US": { "en-US": {
dashboard: { dashboard: {
@ -45,23 +45,23 @@ const dashboardMessages = {
enabled: "Enabled", enabled: "Enabled",
disabled: "Disabled", disabled: "Disabled",
claimed: "Claimed", claimed: "Claimed",
unclaimed: "Unclaimed", unclaimed: "Unclaimed"
}, },
map: { map: {
title: "Device Track Points", title: "Device Track Points",
serviceLabel: "Map Service", serviceLabel: "Map Service",
empty: "No device coordinate data", empty: "No device coordinate data"
}, },
provider: { provider: {
maptiler: "MapTiler", maptiler: "MapTiler",
amap: "Amap", amap: "Amap",
google: "Google Maps", google: "Google Maps"
}, },
popup: { popup: {
device: "Device", device: "Device",
time: "Time", time: "Time",
remark: "Remark", remark: "Remark",
coordinates: "Coordinates", coordinates: "Coordinates"
}, },
message: { message: {
mapConfigLoadFailed: "Failed to load map configuration", mapConfigLoadFailed: "Failed to load map configuration",
@ -70,10 +70,10 @@ const dashboardMessages = {
amapLoadFailed: "Failed to load Amap", amapLoadFailed: "Failed to load Amap",
missingMaptilerKey: "MapTiler key is not configured for this business", missingMaptilerKey: "MapTiler key is not configured for this business",
missingGoogleKey: "Google Maps 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": { "fr-FR": {
dashboard: { dashboard: {
@ -83,23 +83,23 @@ const dashboardMessages = {
enabled: "Actifs", enabled: "Actifs",
disabled: "Desactives", disabled: "Desactives",
claimed: "Reclames", claimed: "Reclames",
unclaimed: "Non reclames", unclaimed: "Non reclames"
}, },
map: { map: {
title: "Points de trajectoire", title: "Points de trajectoire",
serviceLabel: "Service de carte", serviceLabel: "Service de carte",
empty: "Aucune coordonnee appareil", empty: "Aucune coordonnee appareil"
}, },
provider: { provider: {
maptiler: "MapTiler", maptiler: "MapTiler",
amap: "Amap", amap: "Amap",
google: "Google Maps", google: "Google Maps"
}, },
popup: { popup: {
device: "Appareil", device: "Appareil",
time: "Heure", time: "Heure",
remark: "Remarque", remark: "Remarque",
coordinates: "Coordonnees", coordinates: "Coordonnees"
}, },
message: { message: {
mapConfigLoadFailed: "Echec du chargement de la configuration de carte", mapConfigLoadFailed: "Echec du chargement de la configuration de carte",
@ -108,10 +108,10 @@ const dashboardMessages = {
amapLoadFailed: "Echec du chargement d'Amap", amapLoadFailed: "Echec du chargement d'Amap",
missingMaptilerKey: "La cle MapTiler n'est pas configuree", missingMaptilerKey: "La cle MapTiler n'est pas configuree",
missingGoogleKey: "La cle Google Maps 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": { "es-ES": {
dashboard: { dashboard: {
@ -121,23 +121,23 @@ const dashboardMessages = {
enabled: "Habilitados", enabled: "Habilitados",
disabled: "Deshabilitados", disabled: "Deshabilitados",
claimed: "Reclamados", claimed: "Reclamados",
unclaimed: "No reclamados", unclaimed: "No reclamados"
}, },
map: { map: {
title: "Puntos de trayectoria", title: "Puntos de trayectoria",
serviceLabel: "Servicio de mapa", serviceLabel: "Servicio de mapa",
empty: "No hay coordenadas de dispositivos", empty: "No hay coordenadas de dispositivos"
}, },
provider: { provider: {
maptiler: "MapTiler", maptiler: "MapTiler",
amap: "Amap", amap: "Amap",
google: "Google Maps", google: "Google Maps"
}, },
popup: { popup: {
device: "Dispositivo", device: "Dispositivo",
time: "Hora", time: "Hora",
remark: "Observacion", remark: "Observacion",
coordinates: "Coordenadas", coordinates: "Coordenadas"
}, },
message: { message: {
mapConfigLoadFailed: "No se pudo cargar la configuracion del mapa", mapConfigLoadFailed: "No se pudo cargar la configuracion del mapa",
@ -146,10 +146,10 @@ const dashboardMessages = {
amapLoadFailed: "No se pudo cargar Amap", amapLoadFailed: "No se pudo cargar Amap",
missingMaptilerKey: "MapTiler key no esta configurada", missingMaptilerKey: "MapTiler key no esta configurada",
missingGoogleKey: "Google Maps 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": { "pt-BR": {
dashboard: { dashboard: {
@ -159,23 +159,23 @@ const dashboardMessages = {
enabled: "Ativos", enabled: "Ativos",
disabled: "Desativados", disabled: "Desativados",
claimed: "Reivindicados", claimed: "Reivindicados",
unclaimed: "Nao reivindicados", unclaimed: "Nao reivindicados"
}, },
map: { map: {
title: "Pontos de trajeto", title: "Pontos de trajeto",
serviceLabel: "Servico de mapa", serviceLabel: "Servico de mapa",
empty: "Sem dados de coordenadas de dispositivo", empty: "Sem dados de coordenadas de dispositivo"
}, },
provider: { provider: {
maptiler: "MapTiler", maptiler: "MapTiler",
amap: "Amap", amap: "Amap",
google: "Google Maps", google: "Google Maps"
}, },
popup: { popup: {
device: "Dispositivo", device: "Dispositivo",
time: "Hora", time: "Hora",
remark: "Observacao", remark: "Observacao",
coordinates: "Coordenadas", coordinates: "Coordenadas"
}, },
message: { message: {
mapConfigLoadFailed: "Falha ao carregar configuracao do mapa", mapConfigLoadFailed: "Falha ao carregar configuracao do mapa",
@ -184,11 +184,11 @@ const dashboardMessages = {
amapLoadFailed: "Falha ao carregar Amap", amapLoadFailed: "Falha ao carregar Amap",
missingMaptilerKey: "MapTiler key nao configurada", missingMaptilerKey: "MapTiler key nao configurada",
missingGoogleKey: "Google Maps key nao configurada", missingGoogleKey: "Google Maps key nao configurada",
missingAmapKey: "Amap key nao configurada", missingAmapKey: "Amap key nao configurada"
}, }
}, }
}, }
}, }
}; };
export default dashboardMessages; export default dashboardMessages;

3
src/lang/index.js

@ -6,6 +6,7 @@ import dashboardMessages from "./dashboard-messages";
import systemMessages from "./system-messages"; import systemMessages from "./system-messages";
import systemUserDeviceMessages from "./system-user-device-messages"; import systemUserDeviceMessages from "./system-user-device-messages";
import profileMessages from "./profile-messages"; import profileMessages from "./profile-messages";
import noPermissionMessages from "./no-permission-messages";
import { getLanguage } from "@/utils/language"; import { getLanguage } from "@/utils/language";
const DEFAULT_LANGUAGE = "zh-CN"; const DEFAULT_LANGUAGE = "zh-CN";
@ -54,7 +55,7 @@ const mergedMessages = mergeLocaleMessages(
); );
const mergedMessagesWithDashboard = mergeLocaleMessages( const mergedMessagesWithDashboard = mergeLocaleMessages(
mergedMessages, mergeLocaleMessages(mergedMessages, noPermissionMessages),
dashboardMessages dashboardMessages
); );

10
src/main.js

@ -19,7 +19,7 @@ import router from "./router";
import directive from "./directive"; // directive import directive from "./directive"; // directive
import plugins from "./plugins"; // plugins import plugins from "./plugins"; // plugins
import { download } from "@/utils/ruoyi"; 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 I18nPlugin, { t as i18nT } from "@/lang";
import { getLanguage } from "@/utils/language"; import { getLanguage } from "@/utils/language";
@ -131,9 +131,15 @@ if (typeof document !== "undefined") {
: process.env.VUE_APP_TITLE; : process.env.VUE_APP_TITLE;
} }
new Vue({ const app = new Vue({
el: "#app", el: "#app",
router, router,
store, store,
render: (h) => h(App), render: (h) => h(App),
}); });
if (app && typeof app.$nextTick === "function") {
app.$nextTick(() => {
warmupMathjs();
});
}

110
src/permission.js

@ -1,63 +1,75 @@
import router from './router' import router from "./router";
import store from './store' import store from "./store";
import { Message } from 'element-ui' import { Message } from "element-ui";
import NProgress from 'nprogress' import NProgress from "nprogress";
import 'nprogress/nprogress.css' import "nprogress/nprogress.css";
import { getToken } from '@/utils/auth' import { getToken } from "@/utils/auth";
import { isPathMatch } from '@/utils/validate' import { isPathMatch } from "@/utils/validate";
import { isRelogin } from '@/utils/request' import { isRelogin } from "@/utils/request";
NProgress.configure({ showSpinner: false }) NProgress.configure({ showSpinner: false });
const whiteList = ['/login', '/register'] const whiteList = ["/login", "/register"];
const isWhiteList = (path) => { 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) => { router.beforeEach((to, from, next) => {
NProgress.start() NProgress.start();
if (getToken()) { if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title) if (to.meta.title) {
/* has token*/ store.dispatch("settings/setTitle", to.meta.title);
if (to.path === '/login') { }
next({ path: '/' }) if (to.path === "/login") {
NProgress.done() next({ path: "/" });
} else if (isWhiteList(to.path)) { NProgress.done();
next() return;
} 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()
}
} }
} else {
// 没有token
if (isWhiteList(to.path)) { 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 { } else {
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页 next(`/login?redirect=${encodeURIComponent(to.fullPath)}`);
NProgress.done()
} }
NProgress.done();
} }
}) });
router.afterEach(() => { router.afterEach(() => {
NProgress.done() NProgress.done();
}) });

5
src/router/index.js

@ -61,6 +61,11 @@ export const constantRoutes = [
component: () => import('@/views/error/401'), component: () => import('@/views/error/401'),
hidden: true hidden: true
}, },
{
path: '/no-permission',
component: () => import('@/views/error/noPermission'),
hidden: true
},
// { // {
// path: '', // path: '',
// component: Layout, // component: Layout,

39
src/store/modules/permission.js

@ -35,27 +35,36 @@ const permission = {
// 向后端请求路由数据 // 向后端请求路由数据
getRouters().then(res => { getRouters().then(res => {
const sdata = JSON.parse(JSON.stringify(res.data).replaceAll("/index/index","Layout").replaceAll("\"component\":\"/","\"component\":\"")); const routeJson = JSON.stringify(res.data)
const rdata = JSON.parse(JSON.stringify(res.data).replaceAll("/index/index","Layout").replaceAll("\"component\":\"/","\"component\":\"")) .split("/index/index").join("Layout")
.split("\"component\":\"/").join("\"component\":\"")
const sdata = JSON.parse(routeJson);
const rdata = JSON.parse(routeJson)
const sidebarRoutes = filterAsyncRouter(sdata) const sidebarRoutes = filterAsyncRouter(sdata)
const rewriteRoutes = filterAsyncRouter(rdata, false, true) const rewriteRoutes = filterAsyncRouter(rdata, false, true)
const first = sidebarRoutes?.[0]?.children?.[0] const first = findFirstLeafRoute(sidebarRoutes)
if (first) { if (first) {
console.log(first) console.log(first)
rewriteRoutes.push( { rewriteRoutes.push( {
path: "", path: "/",
component: Layout, component: Layout,
redirect: "index", redirect: "/index",
children: [ children: [
{ {
path: "index", path: "index",
component: first.component, component: first.component,
name: first.name, name: "Index",
meta: first.meta meta: first.meta
}, },
] ]
}) })
} else {
rewriteRoutes.push({
path: "/",
redirect: "/no-permission",
hidden: true
})
} }
const asyncRoutes = filterDynamicRoutes(dynamicRoutes) const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true }) rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
@ -113,6 +122,24 @@ function filterChildren(childrenMap, lastRouter = false) {
return children 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) { export function filterDynamicRoutes(routes) {
const res = [] const res = []

121
src/utils/math.js

@ -1,33 +1,94 @@
const $math = require('mathjs') const Big = require('big.js')
export const math = {
add (a,b) { let mathInstance = null
return $math.format( let mathPromise = null
$math.add(
$math.bignumber(a),$math.bignumber(b) const BIG_METHOD_MAP = {
) add: 'plus',
) subtract: 'minus',
}, multiply: 'times',
subtract(a,b) { divide: 'div'
return $math.format( }
$math.subtract(
$math.bignumber(a),$math.bignumber(b) function normalizeMathModule(mod) {
) return mod && mod.default ? mod.default : mod
) }
},
multiply (a,b) { function createBig(value) {
return $math.format( return new Big(value)
$math.multiply( }
$math.bignumber(a),$math.bignumber(b)
) function formatBigResult(result) {
) return result.toFixed()
}, }
divide(a,b) {
return $math.format( function calculateWithBig(type, a, b) {
$math.divide( const left = createBig(a)
$math.bignumber(a),$math.bignumber(b) 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()
}
}

112
src/views/dashboard/index.vue

@ -3,23 +3,58 @@
<div class="dashboard-stats"> <div class="dashboard-stats">
<div class="stat-card"> <div class="stat-card">
<div class="stat-card__label">{{ $t("dashboard.overview.stats.total") }}</div> <div class="stat-card__label">{{ $t("dashboard.overview.stats.total") }}</div>
<div class="stat-card__value">{{ stats.total }}</div> <div class="stat-card__value">
<count-to
:key="`total-${statVersions.total}`"
:start-val="0"
:end-val="displayStats.total"
:duration="1200"
/>
</div>
</div> </div>
<div class="stat-card stat-card--enabled"> <div class="stat-card stat-card--enabled">
<div class="stat-card__label">{{ $t("dashboard.overview.stats.enabled") }}</div> <div class="stat-card__label">{{ $t("dashboard.overview.stats.enabled") }}</div>
<div class="stat-card__value">{{ stats.enabled }}</div> <div class="stat-card__value">
<count-to
:key="`enabled-${statVersions.enabled}`"
:start-val="0"
:end-val="displayStats.enabled"
:duration="1200"
/>
</div>
</div> </div>
<div class="stat-card stat-card--disabled"> <div class="stat-card stat-card--disabled">
<div class="stat-card__label">{{ $t("dashboard.overview.stats.disabled") }}</div> <div class="stat-card__label">{{ $t("dashboard.overview.stats.disabled") }}</div>
<div class="stat-card__value">{{ stats.disabled }}</div> <div class="stat-card__value">
<count-to
:key="`disabled-${statVersions.disabled}`"
:start-val="0"
:end-val="displayStats.disabled"
:duration="1200"
/>
</div>
</div> </div>
<div class="stat-card stat-card--claimed"> <div class="stat-card stat-card--claimed">
<div class="stat-card__label">{{ $t("dashboard.overview.stats.claimed") }}</div> <div class="stat-card__label">{{ $t("dashboard.overview.stats.claimed") }}</div>
<div class="stat-card__value">{{ stats.claimed }}</div> <div class="stat-card__value">
<count-to
:key="`claimed-${statVersions.claimed}`"
:start-val="0"
:end-val="displayStats.claimed"
:duration="1200"
/>
</div>
</div> </div>
<div class="stat-card stat-card--unclaimed"> <div class="stat-card stat-card--unclaimed">
<div class="stat-card__label">{{ $t("dashboard.overview.stats.unclaimed") }}</div> <div class="stat-card__label">{{ $t("dashboard.overview.stats.unclaimed") }}</div>
<div class="stat-card__value">{{ stats.unclaimed }}</div> <div class="stat-card__value">
<count-to
:key="`unclaimed-${statVersions.unclaimed}`"
:start-val="0"
:end-val="displayStats.unclaimed"
:duration="1200"
/>
</div>
</div> </div>
</div> </div>
@ -67,6 +102,7 @@
import { getDashboardOverview, getDeviceTrajectoryMapConfig } from "@/api/device/device"; import { getDashboardOverview, getDeviceTrajectoryMapConfig } from "@/api/device/device";
import { loadAMap } from "@/utils/loadAMap"; import { loadAMap } from "@/utils/loadAMap";
import { loadLeaflet } from "@/utils/loadLeaflet"; import { loadLeaflet } from "@/utils/loadLeaflet";
import CountTo from "vue-count-to";
const DEFAULT_CENTER = [31.2304, 121.4737]; const DEFAULT_CENTER = [31.2304, 121.4737];
const AMAP_DEFAULT_CENTER = [121.4737, 31.2304]; 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 { export default {
name: "DashboardOverview", name: "DashboardOverview",
components: {
CountTo,
},
data() { data() {
return { return {
loading: false, loading: false,
mapLoading: false, mapLoading: false,
mapLoadError: "", mapLoadError: "",
stats: getDefaultStats(), stats: getDefaultStats(),
displayStats: getDefaultStats(),
statVersions: getDefaultStatVersions(),
statTimers: [],
statDelayStep: 140,
devicePoints: [], devicePoints: [],
mapProvider: "amap", mapProvider: "amap",
mapConfig: null, mapConfig: null,
@ -140,6 +193,7 @@ export default {
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener("resize", this.handleWindowResize); window.removeEventListener("resize", this.handleWindowResize);
this.clearStatTimers();
if (this.resizeTimer) { if (this.resizeTimer) {
clearTimeout(this.resizeTimer); clearTimeout(this.resizeTimer);
this.resizeTimer = null; this.resizeTimer = null;
@ -204,22 +258,45 @@ export default {
await this.initCurrentMap(); await this.initCurrentMap();
this.renderMapPoints(); 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() { async loadDashboardData() {
this.loading = true; this.loading = true;
try { try {
const response = await getDashboardOverview(); const response = await getDashboardOverview();
const data = response && response.data ? response.data : {}; const data = response && response.data ? response.data : {};
this.stats = { const nextStats = {
total: Number(data.totalCount) || 0, total: Number(data.totalCount) || 0,
enabled: Number(data.enabledCount) || 0, enabled: Number(data.enabledCount) || 0,
disabled: Number(data.disabledCount) || 0, disabled: Number(data.disabledCount) || 0,
claimed: Number(data.claimedCount) || 0, claimed: Number(data.claimedCount) || 0,
unclaimed: Number(data.unclaimedCount) || 0, unclaimed: Number(data.unclaimedCount) || 0,
}; };
this.animateStats(nextStats);
this.devicePoints = this.buildDevicePoints(data.points); this.devicePoints = this.buildDevicePoints(data.points);
this.renderMapPoints(); this.renderMapPoints();
} catch (error) { } catch (error) {
this.stats = getDefaultStats(); this.animateStats(getDefaultStats());
this.devicePoints = []; this.devicePoints = [];
this.clearMapMarkers(); this.clearMapMarkers();
this.$message.error((error && error.message) || this.$t("dashboard.overview.message.dataLoadFailed")); 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], { const marker = this.mapsApi.marker([point.latNum, point.lngNum], {
title: point.alias || point.sn || String(point.id || ""), title: point.alias || point.sn || String(point.id || ""),
}); });
marker.bindPopup(this.buildPopupContent(point)); marker.bindPopup(this.buildPopupContentSafe(point));
marker.addTo(this.map); marker.addTo(this.map);
this.mapMarkers.push(marker); this.mapMarkers.push(marker);
bounds.push([point.latNum, point.lngNum]); bounds.push([point.latNum, point.lngNum]);
@ -461,7 +538,7 @@ export default {
offset: new this.mapsApi.Pixel(0, -24), 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]); this.mapInfoWindow.open(this.map, [point.lngNum, point.latNum]);
}); });
return marker; return marker;
@ -476,6 +553,23 @@ export default {
}); });
} }
}, },
buildPopupContentSafe(point) {
const separator = ": ";
return (
'<div class="dashboard-map-popup">' +
`<div><strong>${this.escapeHtml(this.$t("dashboard.overview.popup.device"))}</strong>${separator}${this.escapeHtml(
point.alias || point.sn || point.id || "-"
)}</div>` +
`<div><strong>${this.escapeHtml(this.$t("dashboard.overview.popup.time"))}</strong>${separator}${this.escapeHtml(
this.formatDateTime(point.lastLocationTime)
)}</div>` +
`<div><strong>${this.escapeHtml(this.$t("dashboard.overview.popup.remark"))}</strong>${separator}${this.escapeHtml(
point.remark || "-"
)}</div>` +
"</div>"
);
},
buildPopupContent(point) { buildPopupContent(point) {
return ( return (
'<div class="dashboard-map-popup">' + '<div class="dashboard-map-popup">' +

93
src/views/login.vue

@ -1,7 +1,7 @@
<template> <template>
<div class="login"> <div class="login">
<!-- 左侧动态背景区域 --> <!-- 左侧动态背景区域 -->
<div class="login-left"> <div class="login-left" v-once>
<!-- 网格背景 --> <!-- 网格背景 -->
<div class="grid-overlay"></div> <div class="grid-overlay"></div>
@ -113,7 +113,7 @@
<div class="orbit-ring"></div> <div class="orbit-ring"></div>
<!-- 粒子效果 --> <!-- 粒子效果 -->
<div class="particles" id="particles"></div> <div ref="particles" class="particles"></div>
<!-- 中央超大定位标记 --> <!-- 中央超大定位标记 -->
<div class="center-marker"> <div class="center-marker">
@ -234,7 +234,9 @@ export default {
] ]
}, },
loading: false, loading: false,
redirect: undefined redirect: undefined,
particleTask: null,
particleTaskType: ""
}; };
}, },
watch: { watch: {
@ -249,22 +251,57 @@ export default {
// this.getCookie(); // this.getCookie();
}, },
mounted() { mounted() {
this.initParticles(); this.scheduleParticles();
},
beforeDestroy() {
this.cancelParticleTask();
this.clearParticles();
}, },
methods: { methods: {
scheduleParticles() {
const run = () => this.$nextTick(() => this.initParticles());
if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") {
this.particleTaskType = "idle";
this.particleTask = window.requestIdleCallback(run, { timeout: 180 });
return;
}
this.particleTaskType = "timeout";
this.particleTask = window.setTimeout(run, 32);
},
cancelParticleTask() {
if (typeof window === "undefined" || this.particleTask == null) {
return;
}
if (this.particleTaskType === "idle" && typeof window.cancelIdleCallback === "function") {
window.cancelIdleCallback(this.particleTask);
} else {
window.clearTimeout(this.particleTask);
}
this.particleTask = null;
this.particleTaskType = "";
},
initParticles() { initParticles() {
const particlesContainer = document.getElementById('particles'); const particlesContainer = this.$refs.particles;
if (!particlesContainer || particlesContainer.childElementCount > 0) {
return;
}
const fragment = document.createDocumentFragment();
for (let i = 0; i < 40; i++) {
const particle = document.createElement('span');
particle.className = 'particle';
particle.style.left = Math.random() * 100 + '%';
particle.style.animationDelay = Math.random() * 25 + 's';
particle.style.animationDuration = (20 + Math.random() * 10) + 's';
particle.style.width = (2 + Math.random() * 3) + 'px';
particle.style.height = particle.style.width;
fragment.appendChild(particle);
}
particlesContainer.appendChild(fragment);
},
clearParticles() {
const particlesContainer = this.$refs.particles;
if (particlesContainer) { if (particlesContainer) {
for (let i = 0; i < 40; i++) { particlesContainer.textContent = "";
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = Math.random() * 100 + '%';
particle.style.animationDelay = Math.random() * 25 + 's';
particle.style.animationDuration = (20 + Math.random() * 10) + 's';
particle.style.width = (2 + Math.random() * 3) + 'px';
particle.style.height = particle.style.width;
particlesContainer.appendChild(particle);
}
} }
}, },
icoCreate(icoUrl) { icoCreate(icoUrl) {
@ -302,7 +339,8 @@ export default {
Cookies.remove('rememberMe'); Cookies.remove('rememberMe');
} }
this.$store.dispatch("Login", this.loginForm).then(() => { this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(() => { }); const targetPath = this.redirect && this.redirect !== "/no-permission" ? this.redirect : "/index";
this.$router.push({ path: targetPath }).catch(() => { });
}).catch(() => { }).catch(() => {
this.loading = false; this.loading = false;
}); });
@ -326,6 +364,7 @@ export default {
position: relative; position: relative;
background: linear-gradient(135deg, #0a1929 0%, #0d3c61 30%, #01579b 60%, #006064 100%); background: linear-gradient(135deg, #0a1929 0%, #0d3c61 30%, #01579b 60%, #006064 100%);
overflow: hidden; overflow: hidden;
contain: layout paint;
} }
/* 网格背景 */ /* 网格背景 */
@ -364,12 +403,15 @@ export default {
.pin { .pin {
position: absolute; position: absolute;
animation: pulsePin 3s ease-in-out infinite; animation: pulsePin 3s ease-in-out infinite;
will-change: transform, opacity;
backface-visibility: hidden;
} }
.pin svg { .pin svg {
width: 50px; width: 50px;
height: 50px; height: 50px;
filter: drop-shadow(0 0 15px currentColor); filter: drop-shadow(0 0 15px currentColor);
transform: translateZ(0);
} }
.pin:nth-child(1) { top: 15%; left: 12%; animation-delay: 0s; color: rgba(76,175,80,0.9); } .pin:nth-child(1) { top: 15%; left: 12%; animation-delay: 0s; color: rgba(76,175,80,0.9); }
@ -409,6 +451,8 @@ export default {
width: 350px; width: 350px;
height: 350px; height: 350px;
animation: float 4s ease-in-out infinite; animation: float 4s ease-in-out infinite;
will-change: transform;
transform: translateZ(0);
} }
@keyframes float { @keyframes float {
@ -425,6 +469,8 @@ export default {
border: 2px solid rgba(76, 175, 80, 0.15); border: 2px solid rgba(76, 175, 80, 0.15);
border-radius: 50%; border-radius: 50%;
animation: rotate 60s linear infinite; animation: rotate 60s linear infinite;
will-change: transform;
backface-visibility: hidden;
} }
.orbit-ring:nth-child(1) { width: 500px; height: 500px; } .orbit-ring:nth-child(1) { width: 500px; height: 500px; }
@ -445,6 +491,7 @@ export default {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
contain: strict;
} }
.particle { .particle {
@ -454,6 +501,8 @@ export default {
background: rgba(76, 175, 80, 0.5); background: rgba(76, 175, 80, 0.5);
border-radius: 50%; border-radius: 50%;
animation: particleFloat 25s linear infinite; animation: particleFloat 25s linear infinite;
will-change: transform, opacity;
transform: translateZ(0);
} }
@keyframes particleFloat { @keyframes particleFloat {
@ -487,10 +536,14 @@ export default {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); width: 100px;
height: 100px;
transform: translate3d(-50%, -50%, 0) scale(1);
border: 2px solid rgba(76, 175, 80, 0.3); border: 2px solid rgba(76, 175, 80, 0.3);
border-radius: 50%; border-radius: 50%;
animation: rippleEffect 4s ease-out infinite; animation: rippleEffect 4s ease-out infinite;
will-change: transform, opacity;
backface-visibility: hidden;
} }
.ripple:nth-child(1) { animation-delay: 0s; } .ripple:nth-child(1) { animation-delay: 0s; }
@ -500,13 +553,11 @@ export default {
@keyframes rippleEffect { @keyframes rippleEffect {
0% { 0% {
width: 100px; transform: translate3d(-50%, -50%, 0) scale(1);
height: 100px;
opacity: 0.8; opacity: 0.8;
} }
100% { 100% {
width: 800px; transform: translate3d(-50%, -50%, 0) scale(8);
height: 800px;
opacity: 0; opacity: 0;
} }
} }

Loading…
Cancel
Save