diff --git a/.env.example b/.env.example index 7a23bfc8..be441124 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ FRONTEND_PORT="18966" ALLOWED_DOMAINS="" # APIs BING_MAP_API_KEY="" +GOOGLE_MAP_API_KEY="" IPINFO_API_TOKEN="" KEYCDN_USER_AGENT="" CLOUDFLARE_API="" diff --git a/README.md b/README.md index 3700d840..929d6253 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ You can use the program without adding any environment variables, but if you wan | `SECURITY_RATE_LIMIT` | No | `"0"` | Controls the number of requests an IP can make to the backend server every 60 minutes (set to 0 for no limit) | | `SECURITY_DELAY_AFTER` | No | `"0"` | Controls the first X requests from an IP every 20 minutes that are not subject to speed limits, and after X requests, the delay will increase | | `SECURITY_BLACKLIST_LOG_FILE_PATH` | No | `"logs/blacklist-ip.log"` | Path setting. Records the list of IPs that triggered the limit after SECURITY_RATE_LIMIT is enabled | -| `BING_MAP_API_KEY` | No | `""` | API Key for Bing Maps, used to display the location of the IP on a map | +| `GOOGLE_MAP_API_KEY=` | No | `""` | API Key for Google Maps, used to display the location of the IP on a map | | `ALLOWED_DOMAINS` | No | `""` | Allowed domains for access, separated by commas, used to prevent misuse of the backend API | | `IPCHECKING_API_KEY` | No | `""` | API Key for IPCheck.ing, used to obtain accurate IP geolocation information | | `IPINFO_API_TOKEN` | No | `""` | API Token for IPInfo.io, used to obtain IP geolocation information through IPInfo.io | @@ -124,7 +124,7 @@ Modify `.env`, and for example, add the following: ```bash BACKEND_PORT=11966 FRONTEND_PORT=18966 -BING_MAP_API_KEY="YOUR_KEY_HERE" +GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" ALLOWED_DOMAINS="example.com" IPCHECKING_API="YOUR_KEY_HERE" ``` @@ -137,7 +137,7 @@ You can add environment variables when running Docker, for example: ```bash docker run -d -p 18966:18966 \ - -e BING_MAP_API_KEY="YOUR_KEY_HERE" \ + -e GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" \ -e ALLOWED_DOMAINS="example.com" \ -e IPCHECKING_API="YOUR_TOKEN_HERE" \ --name myip \ diff --git a/README_FR.md b/README_FR.md index 819f416c..aa9db20d 100644 --- a/README_FR.md +++ b/README_FR.md @@ -96,7 +96,7 @@ Vous pouvez utiliser le programme sans ajouter de variables d'environnement, mai | `SECURITY_RATE_LIMIT` | Non | `"0"` | Contrôle le nombre de requêtes qu'une adresse IP peut faire au serveur backend toutes les 60 minutes (réglé sur 0 pour aucune limite) | | `SECURITY_DELAY_AFTER` | Non | `"0"` | Contrôle les premières X requêtes d'une adresse IP toutes les 20 minutes qui ne sont pas soumises à des limites de vitesse, et après X requêtes, le délai augmentera | | `SECURITY_BLACKLIST_LOG_FILE_PATH` | Non | `"logs/blacklist-ip.log"` | Paramètre de chemin. Enregistre la liste des adresses IP qui ont déclenché la limite après que `SECURITY_RATE_LIMIT` soit activé | -| `BING_MAP_API_KEY` | Non | `""` | Clé API pour Bing Maps, utilisée pour afficher l'emplacement de l'adresse IP sur une carte | +| `GOOGLE_MAP_API_KEY` | Non | `""` | Clé API pour Google Maps, utilisée pour afficher l'emplacement de l'adresse IP sur une carte | | `ALLOWED_DOMAINS` | Non | `""` | Domaines autorisés pour l'accès, séparés par des virgules, utilisés pour empêcher une utilisation abusive de l'API backend | | `IPCHECKING_API_KEY` | Non | `""` | Clé API pour IPCheck.ing, utilisée pour obtenir des informations de géolocalisation précises sur l'adresse IP | | `IPINFO_API_TOKEN` | Non | `""` | Jeton API pour IPInfo.io, utilisé pour obtenir des informations de géolocalisation sur l'adresse IP via IPInfo.io | @@ -124,7 +124,7 @@ Modifiez le fichier `.env`, et par exemple, ajoutez ce qui suit : ```bash BACKEND_PORT=11966 FRONTEND_PORT=18966 -BING_MAP_API_KEY="YOUR_KEY_HERE" +GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" ALLOWED_DOMAINS="example.com" IPCHECKING_API="YOUR_KEY_HERE" ``` @@ -137,7 +137,7 @@ Vous pouvez ajouter des variables d'environnement lors de l'exécution de Docker ```bash docker run -d -p 18966:18966 \ - -e BING_MAP_API_KEY="YOUR_KEY_HERE" \ + -e GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" \ -e ALLOWED_DOMAINS="example.com" \ -e IPCHECKING_API="YOUR_TOKEN_HERE" \ --name myip \ diff --git a/README_ZH.md b/README_ZH.md index d412db8f..9b5b4fdf 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -96,7 +96,7 @@ docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest | `SECURITY_RATE_LIMIT` | 否 | `"0"` | 控制每 60 分钟一个 IP 可以对后端服务器请求的次数(设置为 0 则为不限制) | | `SECURITY_DELAY_AFTER` | 否 | `"0"` | 控制每 20 分钟一个 IP 的前 X 次请求不受速度限制,超过 X 次后会逐次增加延迟 | | `SECURITY_BLACKLIST_LOG_FILE_PATH` | 否 | `"logs/blacklist-ip.log"` | 路径设置。记录由 SECURITY_RATE_LIMIT 开启后,触发限制的 IP 列表 | -| `BING_MAP_API_KEY` | 否 | `""` | Bing 地图的 API Key,用于展示 IP 所在地的地图 | +| `GOOGLE_MAP_API_KEY=` | 否 | `""` | Google 地图的 API Key,用于展示 IP 所在地的地图 | | `ALLOWED_DOMAINS` | 否 | `""` | 允许访问的域名,用逗号分隔,用于防止后端 API 被滥用 | | `IPCHECKING_API_KEY` | 否 | `""` | IPCheck.ing 的 API Key,用于获取精准的 IP 归属地信息 | | `IPINFO_API_TOKEN` | 否 | `""` | IPInfo.io 的 API Token,用于通过 IPInfo.io 获取 IP 归属地信息 | @@ -124,7 +124,7 @@ cp .env.example .env ```bash BACKEND_PORT=11966 FRONTEND_PORT=18966 -BING_MAP_API_KEY="YOUR_KEY_HERE" +GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" ALLOWED_DOMAINS="example.com" IPCHECKING_API="YOUR_KEY_HERE" ``` @@ -137,7 +137,7 @@ IPCHECKING_API="YOUR_KEY_HERE" ```bash docker run -d -p 18966:18966 \ - -e BING_MAP_API_KEY="YOUR_KEY_HERE" \ + -e GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" \ -e ALLOWED_DOMAINS="example.com" \ -e IPCHECKING_API="YOUR_TOKEN_HERE" \ --name myip \ diff --git a/api/configs.js b/api/configs.js index dfa88cd6..acc44196 100644 --- a/api/configs.js +++ b/api/configs.js @@ -17,7 +17,7 @@ export default (req, res) => { const originalSite = hostname === 'ipcheck.ing' || hostname === 'www.ipcheck.ing' || hostname === 'localtest.ipcheck.ing'; const envConfigs = { - bingMap: process.env.BING_MAP_API_KEY, + map: process.env.GOOGLE_MAP_API_KEY, ipInfo: process.env.IPINFO_API_TOKEN, ipChecking: process.env.IPCHECKING_API_KEY, keyCDN: process.env.KEYCDN_USER_AGENT, diff --git a/api/map.js b/api/map.js index 3e48a2ad..1d32e68d 100644 --- a/api/map.js +++ b/api/map.js @@ -2,21 +2,42 @@ import { get } from 'https'; import { refererCheck } from '../common/referer-check.js'; // 验证请求合法性 - function isValidRequest(req) { - const isLatitudeValid = /^-?\d+(\.\d+)?$/.test(req.query.latitude); const isLongitudeValid = /^-?\d+(\.\d+)?$/.test(req.query.longitude); const isLanguageValid = /^[a-z]{2}$/.test(req.query.language); - const isCanvasModeValid = /^(CanvasLight|RoadDark)$/.test(req.query.CanvasMode); - if (!isLatitudeValid || !isLongitudeValid || !isLanguageValid || !isCanvasModeValid) { + if (!isLatitudeValid || !isLongitudeValid || !isLanguageValid) { return false; } else { return true; } } +// 定义白天模式和黑暗模式样式字符串 +const styles = { + Dark: [ + "feature:all|element:geometry.fill|color:0x242f3e", + "feature:all|element:labels.text.stroke|color:0x242f3e", + "feature:all|element:labels.text.fill|color:0x746855", + "feature:administrative.locality|element:labels.text.fill|color:0xd59563", + "feature:poi|element:labels.text.fill|color:0xd59563", + "feature:poi.park|element:geometry|color:0x263c3f", + "feature:poi.park|element:labels.text.fill|color:0x6b9a76", + "feature:road|element:geometry|color:0x38414e", + "feature:road|element:geometry.stroke|color:0x212a37", + "feature:road|element:labels.text.fill|color:0x9ca5b3", + "feature:road.highway|element:geometry|color:0x746855", + "feature:road.highway|element:geometry.stroke|color:0x1f2835", + "feature:road.highway|element:labels.text.fill|color:0xf3d19c", + "feature:transit|element:geometry|color:0x2f3948", + "feature:transit.station|element:labels.text.fill|color:0xd59563", + "feature:water|element:geometry|color:0x17263c", + "feature:water|element:labels.text.fill|color:0x515c6d", + "feature:all|element:labels.text.stroke|color:0x17263c" + ] +}; + export default (req, res) => { // 限制只能从指定域名访问 const referer = req.headers.referer; @@ -36,16 +57,20 @@ export default (req, res) => { return res.status(400).json({ error: 'Missing latitude, longitude, or language' }); } - const mapSize = '800,640'; - const pp = `${latitude},${longitude};46`; - const fmt = 'jpeg'; - const dpi = 'Large'; + const mapSize = '500x400'; + const fmt = 'jpg'; + const scale = 2; + const zoom = 3; - const apiKeys = (process.env.BING_MAP_API_KEY || '').split(','); + const apiKeys = (process.env.GOOGLE_MAP_API_KEY || '').split(','); const apiKey = apiKeys[Math.floor(Math.random() * apiKeys.length)]; - const url = `https://dev.virtualearth.net/REST/v1/Imagery/Map/${CanvasMode}/${latitude},${longitude}/5?mapSize=${mapSize}&pp=${pp}&key=${apiKey}&fmt=${fmt}&dpi=${dpi}&c=${language}`; + let styleParam = ''; + if (CanvasMode === 'Dark') { + styleParam = styles.Dark.join('&style='); + } + const url = `https://maps.googleapis.com/maps/api/staticmap?center=${latitude},${longitude}&markers=color:blue%7C${latitude},${longitude}&scale=${scale}&zoom=${zoom}&maptype=roadmap&language=${language}&format=${fmt}&size=${mapSize}&style=${styleParam}&key=${apiKey}`; get(url, apiRes => { apiRes.pipe(res); diff --git a/frontend/App.vue b/frontend/App.vue index e5a6512a..dc0ed0af 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -451,7 +451,7 @@ const ShortcutKeys = (isOriginalSite) => { { keys: "m", action: () => { - if (configs.value.bingMap) { + if (configs.value.map) { window.scrollTo({ top: 0, behavior: "smooth" }); preferencesRef.value.toggleMaps(); }; diff --git a/frontend/components/Nav.vue b/frontend/components/Nav.vue index ef6fb869..e7fa2648 100644 --- a/frontend/components/Nav.vue +++ b/frontend/components/Nav.vue @@ -16,21 +16,6 @@ }">ing -
- - -
-
diff --git a/frontend/components/widgets/Preferences.vue b/frontend/components/widgets/Preferences.vue index d1707f53..021450aa 100644 --- a/frontend/components/widgets/Preferences.vue +++ b/frontend/components/widgets/Preferences.vue @@ -12,6 +12,32 @@
{{ t('nav.preferences.preferenceTips') }}
+ + +
+
{{ + t('nav.preferences.language') }}
+
+ +
+
{{ t('nav.preferences.languageTips') }}
+
+
@@ -31,7 +57,7 @@ t('nav.preferences.colorLight') }} {{ t('nav.preferences.colorDark') }} - {{ t('nav.preferences.colorAuto') }} + {{ t('nav.preferences.systemAuto') }}
@@ -94,7 +120,7 @@ :class="[isDarkMode ? 'border-light' : 'border-dark']">
{{ t('nav.preferences.autoRunTips') }}
@@ -110,7 +136,7 @@ :class="[isDarkMode ? 'border-light' : 'border-dark']" v-if="userPreferences.autoStart">
+ t('nav.preferences.connectivityAutoRefresh') }}
{{ t('nav.preferences.connectivityAutoRefreshTips') }}
@@ -122,10 +148,10 @@
  • + :class="[isDarkMode ? 'border-light' : 'border-dark']" v-if="configs.map">
    {{ t('nav.preferences.showMapTips') }}
    @@ -141,7 +167,7 @@ :class="[isDarkMode ? 'border-light' : 'border-dark']" v-if="isMobile">
    {{ t('nav.preferences.simpleModeTips') }}
    @@ -156,7 +182,7 @@ :class="[isDarkMode ? 'border-light' : 'border-dark']">
    + t('nav.preferences.popupConnectivityNotifications') }}
    {{ t('nav.preferences.popupConnectivityNotificationsTips') }}
    @@ -186,7 +212,7 @@ import { useMainStore } from '@/store'; import { useI18n } from 'vue-i18n'; import { trackEvent } from '@/utils/use-analytics'; -const {t} = useI18n(); +const { t } = useI18n(); const store = useMainStore(); const isDarkMode = computed(() => store.isDarkMode); @@ -252,7 +278,10 @@ const prefTheme = (value) => { trackEvent('Nav', 'PreferenceClick', 'Theme'); }; - +const prefLanguage = (value) => { + store.updatePreference('lang', value); + trackEvent('Nav', 'PrefereceClick', 'LanguageChange'); +}; const prefConnectivityRefresh = (value) => { store.updatePreference('connectivityAutoRefresh', value); diff --git a/frontend/locales/en.json b/frontend/locales/en.json index d8b67fc1..47e49d18 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -15,7 +15,7 @@ "title": "Preferences", "preferenceTips": "These settings are saved in your browser for easy reuse. Some options require refreshing the page to take effect.", "colorScheme": "Color Scheme", - "colorAuto": "Auto", + "systemAuto": "Auto", "colorLight": "Day", "colorDark": "Night", "ipSourcesToCheck": "Servers To Check IPs", @@ -32,7 +32,9 @@ "popupConnectivityNotifications": "Pop-up Connectivity Alerts", "popupConnectivityNotificationsTips": "When enabled, the initial check's results are shown as a pop-up alert.", "ipDB": "IP Geolocation Database", - "ipDBTips": "You can select the default IP geolocation source to use. If the selected source is unavailable, the system will use the subsequent sources in order." + "ipDBTips": "You can select the default IP geolocation source to use. If the selected source is unavailable, the system will use the subsequent sources in order.", + "language": "Language Setting", + "languageTips": "Refreshing the browser will take effect." } }, "shell": { @@ -887,6 +889,24 @@ "change": "DNS leak test can now display regional information" } ] + }, + { + "version": "v4.4", + "date": "Dec 7, 2024", + "content": [ + { + "type": "add", + "change": "Language preferences" + }, + { + "type": "improve", + "change": "Map provider changed to Google" + }, + { + "type": "improve", + "change": "Other general optimizations" + } + ] } ] } diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index db0513a4..3af0e478 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -15,7 +15,7 @@ "title": "Préférences", "preferenceTips": "Ces paramètres sont enregistrés dans votre navigateur pour une utilisation ultérieure. Certains paramètres nécessitent un rafraîchissement de la page pour prendre effet.", "colorScheme": "Schéma de Couleurs", - "colorAuto": "Par Défaut Système", + "systemAuto": "Système", "colorLight": "Jour", "colorDark": "Nuit", "ipSourcesToCheck": "Serveurs pour vérifier les adresses IP", @@ -32,7 +32,9 @@ "popupConnectivityNotifications": "Alertes de Connectivité Pop-up", "popupConnectivityNotificationsTips": "Activé, les résultats de la première vérification sont affichés sous forme d'alerte pop-up.", "ipDB": "Base de données de géolocalisation IP", - "ipDBTips": "Vous pouvez sélectionner la source de données de géolocalisation IP par défaut. Si la source sélectionnée n'est pas disponible, le système utilisera les sources suivantes." + "ipDBTips": "Vous pouvez sélectionner la source de données de géolocalisation IP par défaut. Si la source sélectionnée n'est pas disponible, le système utilisera les sources suivantes.", + "language": "Paramètres de langue", + "languageTips": "Rafraîchissement du navigateur pour prendre effet." } }, "shell": { @@ -887,6 +889,24 @@ "change": "Le test de fuite DNS peut maintenant afficher des informations régionales" } ] + }, + { + "version": "v4.4", + "date": "Dec 7, 2024", + "content": [ + { + "type": "add", + "change": "Préférences de langue" + }, + { + "type": "improve", + "change": "Fournisseur de carte changé en Google" + }, + { + "type": "improve", + "change": "Autres optimisations générales" + } + ] } ] } diff --git a/frontend/locales/i18n.js b/frontend/locales/i18n.js index c67fe33c..9ab81148 100644 --- a/frontend/locales/i18n.js +++ b/frontend/locales/i18n.js @@ -7,17 +7,25 @@ import fr from './fr.json'; const messages = { en, zh, fr }; +const supportedLanguages = Object.keys(messages); +// 设置语言 function setLanguage() { let locale = 'en'; + let storedPreferences = localStorage.getItem('userPreferences'); + storedPreferences = storedPreferences ? JSON.parse(storedPreferences) : {}; + if (supportedLanguages.includes(storedPreferences.lang)) { + locale = storedPreferences.lang; + return locale; + } const searchParams = new URLSearchParams(window.location.search); const browserLanguage = navigator.language || navigator.userLanguage; const hl = searchParams.get('hl'); - if (hl && ['en', 'zh', 'fr'].includes(hl)) { + if (hl && supportedLanguages.includes(hl)) { locale = hl; } else if (!hl) { const bl = browserLanguage.substring(0, 2); - if (['en', 'zh', 'fr'].includes(bl)) { + if (supportedLanguages.includes(bl)) { locale = bl; } else { locale = 'en'; diff --git a/frontend/locales/zh.json b/frontend/locales/zh.json index 12e76600..3c36d283 100644 --- a/frontend/locales/zh.json +++ b/frontend/locales/zh.json @@ -15,7 +15,7 @@ "title": "偏好设置", "preferenceTips": "这些设置会保存在浏览器,方便下次使用。部分选项需要刷新页面方可生效。", "colorScheme": "颜色方案", - "colorAuto": "跟随系统", + "systemAuto": "跟随系统", "colorLight": "白天", "colorDark": "黑夜", "ipSourcesToCheck": "IP 检测服务器数量", @@ -32,7 +32,9 @@ "popupConnectivityNotifications": "显示可用性检测气泡", "popupConnectivityNotificationsTips": "开启后,将会在首次检测时以气泡形式提示可用性结果。", "ipDB": "IP 解析数据源", - "ipDBTips": "你可以选择你默认使用的 IP 地理位置数据源,如果你选定的不可用,系统会依次使用后续的数据源。" + "ipDBTips": "你可以选择你默认使用的 IP 地理位置数据源,如果你选定的不可用,系统会依次使用后续的数据源。", + "language": "语言设置", + "languageTips": "刷新浏览器后生效。" } }, "shell": { @@ -887,6 +889,24 @@ "change": "DNS 泄露测试可以展示地区信息" } ] + }, + { + "version": "v4.4", + "date": "Dec 7, 2024", + "content": [ + { + "type": "add", + "change": "语言偏好设置" + }, + { + "type": "improve", + "change": "地图展示供应商变更为 Google" + }, + { + "type": "improve", + "change": "其它常规优化" + } + ] } ] } diff --git a/frontend/store.js b/frontend/store.js index 24160a60..80f7db64 100644 --- a/frontend/store.js +++ b/frontend/store.js @@ -128,6 +128,7 @@ export const useMainStore = defineStore('main', { popupConnectivityNotifications: true, ipCardsToShow: 6, ipGeoSource: 0, + lang: 'auto', }; const storedPreferences = localStorage.getItem('userPreferences'); let preferencesToStore; diff --git a/frontend/style/style.css b/frontend/style/style.css index 817c5b8c..f5116618 100644 --- a/frontend/style/style.css +++ b/frontend/style/style.css @@ -107,10 +107,11 @@ .jn-map-image { width: 100%; - height: 200px; + height: 180px; object-fit: cover; object-position: center; border-radius: 0; + overflow: hidden; } .jn-navbar-top { diff --git a/frontend/utils/transform-ip-data.js b/frontend/utils/transform-ip-data.js index 9b507201..2b17a84e 100644 --- a/frontend/utils/transform-ip-data.js +++ b/frontend/utils/transform-ip-data.js @@ -1,5 +1,5 @@ // 解析IP数据 -function transformDataFromIPapi(data, ipGeoSource, t, bingMapLanguage) { +function transformDataFromIPapi(data, ipGeoSource, t, mapLanguage) { if (data.error) { throw new Error(data.reason); } @@ -14,8 +14,8 @@ function transformDataFromIPapi(data, ipGeoSource, t, bingMapLanguage) { isp: data.org || "", asn: data.asn || "", asnlink: data.asn ? `https://radar.cloudflare.com/${data.asn}` : false, - mapUrl: data.latitude && data.longitude ? `/api/map?latitude=${data.latitude}&longitude=${data.longitude}&language=${bingMapLanguage}&CanvasMode=CanvasLight` : "", - mapUrl_dark: data.latitude && data.longitude ? `/api/map?latitude=${data.latitude}&longitude=${data.longitude}&language=${bingMapLanguage}&CanvasMode=RoadDark` : "" + mapUrl: data.latitude && data.longitude ? `/api/map?latitude=${data.latitude}&longitude=${data.longitude}&language=${mapLanguage}` : "", + mapUrl_dark: data.latitude && data.longitude ? `/api/map?latitude=${data.latitude}&longitude=${data.longitude}&language=${mapLanguage}&CanvasMode=Dark` : "" }; if (ipGeoSource === 0) { diff --git a/package.json b/package.json index fb913d2b..87abb981 100644 --- a/package.json +++ b/package.json @@ -15,33 +15,33 @@ "@analytics/google-analytics": "^1.0.7", "@cloudflare/speedtest": "^1.3.0", "@khmyznikov/pwa-install": "^0.4.8", - "@thumbmarkjs/thumbmarkjs": "^0.16.0", + "@thumbmarkjs/thumbmarkjs": "^0.16.1", "analytics": "^0.8.14", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", - "code-inspector-plugin": "^0.17.6", - "concurrently": "^9.0.1", + "code-inspector-plugin": "^0.18.2", + "concurrently": "^9.1.0", "country-code-lookup": "^0.1.3", - "detect-gpu": "^5.0.54", - "dotenv": "^16.4.5", - "express": "^4.21.1", + "detect-gpu": "^5.0.59", + "dotenv": "^16.4.7", + "express": "^4.21.2", "express-rate-limit": "^7.4.1", "express-slow-down": "^2.0.3", "flag-icons": "^7.2.3", "http-proxy-middleware": "^3.0.3", - "maxmind": "^4.3.22", + "maxmind": "^4.3.23", "nodemon": "^3.1.7", - "pinia": "^2.2.5", - "svgmap": "^2.11.1", - "ua-parser-js": "^1.0.39", - "vue": "^3.5.12", - "vue-i18n": "^10.0.4", - "vue-router": "^4.4.5", + "pinia": "^2.3.0", + "svgmap": "^2.12.1", + "ua-parser-js": "^2.0.0", + "vue": "^3.5.13", + "vue-i18n": "^10.0.5", + "vue-router": "^4.5.0", "whoiser": "^1.18.0" }, "devDependencies": { - "@vitejs/plugin-vue": "^5.1.4", - "vite": "^5.4.10", - "vite-plugin-pwa": "^0.20.5" + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.0.3", + "vite-plugin-pwa": "^0.21.1" } }