diff --git a/.gitignore b/.gitignore index 10c3adb8..278a0134 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,29 @@ -.DS_Store -bingMapAPIKEY -.env +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.env +.vscode/extensions.json + +bingMapAPIKEY package-lock.json diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index f0a85131..00000000 --- a/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "tabWidth": 2, - "useTabs": false, - "semi": true, - "singleQuote": false -} diff --git a/Dockerfile b/Dockerfile index 51e569b6..ff07aaff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,23 @@ -FROM node:20-alpine -WORKDIR /usr/src/app +# 第一阶段:构建 +FROM node:20-alpine as build-stage +WORKDIR /app COPY package*.json ./ RUN npm install COPY . . -EXPOSE 8966 -CMD ["node", "server.js"] \ No newline at end of file +RUN npm run build + +# 第二阶段:运行 +# 基于 Node.js 官方镜像 +FROM node:20-alpine as production-stage +WORKDIR /app +COPY --from=build-stage /app/node_modules ./node_modules +COPY --from=build-stage /app/package.json ./ +COPY --from=build-stage /app/dist ./dist +COPY --from=build-stage /app/server.js ./ +COPY --from=build-stage /app/static-server.js ./ +COPY --from=build-stage /app/api ./api + +EXPOSE 18966 + +# 启动应用 +CMD ["npm", "start"] diff --git a/LICENSE b/LICENSE index bccbc24d..c41881bd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Jason Ng +Copyright (c) 2024 Jason Ng Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0a50b658..bb8c7c1b 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Notes: You can use my demo, but please don't use it for commercial purposes. If you want to use it for commercial purposes, please deploy it yourself. -[![Deploy with Vercel](https://raw.githubusercontent.com/jason5ng32/MyIP/main/public/res/img/Vercel.svg)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fjason5ng32%2FMyIP&project-name=MyIP&repository-name=MyIP) -[![Deploy with Docker](https://raw.githubusercontent.com/jason5ng32/MyIP/main/public/res/img/Docker.svg)](https://hub.docker.com/r/jason5ng32/myip) +[![Deploy with Vercel](https://raw.githubusercontent.com/jason5ng32/MyIP/main/public/Vercel.svg)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fjason5ng32%2FMyIP&project-name=MyIP&repository-name=MyIP) +[![Deploy with Docker](https://raw.githubusercontent.com/jason5ng32/MyIP/main/public/Docker.svg)](https://hub.docker.com/r/jason5ng32/myip) ## 👀 Main Features @@ -51,13 +51,19 @@ Install: npm install ``` +Build: + +```bash +npm run build +``` + Run: ```bash npm start ``` -The program will run on port 8966. +The program will run on port 18966. If you want to add Bing Maps, make the following changes before starting: @@ -102,13 +108,13 @@ IPINFO_API_TOKEN Click the 'Deploy to Docker' button at the top to complete the deployment. Or, use the following shell: ```bash -docker run -d -p 8966:8966 --name myip --restart always jason5ng32/myip:latest +docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest ``` If you wish to display maps, set the Bing Map API Key and allowed domains during deployment: ```bash -docker run -d -p 8966:8966 \ +docker run -d -p 18966:18966 \ -e BING_MAP_API_KEY="YOUR_KEY_HERE" \ -e ALLOWED_DOMAINS="example.com" \ --name myip \ @@ -119,7 +125,7 @@ docker run -d -p 8966:8966 \ If you want to use IPInfo.io API, you can also add the following: ```bash -docker run -d -p 8966:8966 \ +docker run -d -p 18966:18966 \ -e BING_MAP_API_KEY="YOUR_KEY_HERE" \ -e ALLOWED_DOMAINS="example.com" \ -e IPINFO_API_TOKEN="YOUR_TOKEN_HERE" \ diff --git a/README_CN.md b/README_CN.md index e395deed..d22ba601 100644 --- a/README_CN.md +++ b/README_CN.md @@ -11,8 +11,8 @@ 备注:你可以直接用我已经搭建好的服务,也可以自行搭建。 -[![Deploy with Vercel](https://raw.githubusercontent.com/jason5ng32/MyIP/main/public/res/img/Vercel.svg)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fjason5ng32%2FMyIP&project-name=MyIP&repository-name=MyIP) -[![Deploy with Docker](https://raw.githubusercontent.com/jason5ng32/MyIP/main/public/res/img/Docker.svg)](https://hub.docker.com/r/jason5ng32/myip) +[![Deploy with Vercel](https://raw.githubusercontent.com/jason5ng32/MyIP/main/public/Vercel.svg)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fjason5ng32%2FMyIP&project-name=MyIP&repository-name=MyIP) +[![Deploy with Docker](https://raw.githubusercontent.com/jason5ng32/MyIP/main/public/Docker.svg)](https://hub.docker.com/r/jason5ng32/myip) 这是一个完全开源的 IP 信息查看器,可以查询本机 IP、查询任意 IP、查询国内外网站可用性等。这是我第一次用 Vue.js 练手的项目。我……只是一个普通的产品经理。 @@ -53,13 +53,19 @@ git clone https://github.com/jason5ng32/MyIP.git npm install ``` +编译: + +```bash +npm run build +``` + 运行: ```bash npm start ``` -程序会运行在 8966 端口。 +程序会运行在 18966 端口。 如果你想添加 Bing 地图,在启动之前,进行如下修改: @@ -104,13 +110,13 @@ IPINFO_API_TOKEN 点击顶部的部署到 Docker 按钮,即可完成部署,又或者,直接输入下面的命令: ```bash -docker run -d -p 8966:8966 --name myip --restart always jason5ng32/myip:latest +docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest ``` 如果你希望展示地图,则在部署的时候,设置 Bing Map API Key 和允许的域名: ```bash -docker run -d -p 8966:8966 \ +docker run -d -p 18966:18966 \ -e BING_MAP_API_KEY="YOUR_KEY_HERE" \ -e ALLOWED_DOMAINS="example.com" \ --name myip \ @@ -121,7 +127,7 @@ docker run -d -p 8966:8966 \ 如果你希望同时使用 IPinfo.io 的 API: ```bash -docker run -d -p 8966:8966 \ +docker run -d -p 18966:18966 \ -e BING_MAP_API_KEY="YOUR_KEY_HERE" \ -e ALLOWED_DOMAINS="example.com" \ -e IPINFO_API_TOKEN="YOUR_TOKEN_HERE" \ diff --git a/api/countrycode.json b/api/countrycode.json deleted file mode 100644 index 72982ec0..00000000 --- a/api/countrycode.json +++ /dev/null @@ -1,245 +0,0 @@ -{ - "AF": "Afghanistan", - "AX": "Åland Islands", - "AL": "Albania", - "DZ": "Algeria", - "AS": "American Samoa", - "AD": "AndorrA", - "AO": "Angola", - "AI": "Anguilla", - "AQ": "Antarctica", - "AG": "Antigua and Barbuda", - "AR": "Argentina", - "AM": "Armenia", - "AW": "Aruba", - "AU": "Australia", - "AT": "Austria", - "AZ": "Azerbaijan", - "BS": "Bahamas", - "BH": "Bahrain", - "BD": "Bangladesh", - "BB": "Barbados", - "BY": "Belarus", - "BE": "Belgium", - "BZ": "Belize", - "BJ": "Benin", - "BM": "Bermuda", - "BT": "Bhutan", - "BO": "Bolivia", - "BA": "Bosnia and Herzegovina", - "BW": "Botswana", - "BV": "Bouvet Island", - "BR": "Brazil", - "IO": "British Indian Ocean Territory", - "BN": "Brunei Darussalam", - "BG": "Bulgaria", - "BF": "Burkina Faso", - "BI": "Burundi", - "KH": "Cambodia", - "CM": "Cameroon", - "CA": "Canada", - "CV": "Cape Verde", - "KY": "Cayman Islands", - "CF": "Central African Republic", - "TD": "Chad", - "CL": "Chile", - "CN": "China", - "CX": "Christmas Island", - "CC": "Cocos (Keeling) Islands", - "CO": "Colombia", - "KM": "Comoros", - "CG": "Congo", - "CD": "Congo, The Democratic Republic of the", - "CK": "Cook Islands", - "CR": "Costa Rica", - "CI": "Cote D'Ivoire", - "HR": "Croatia", - "CU": "Cuba", - "CY": "Cyprus", - "CZ": "Czech Republic", - "DK": "Denmark", - "DJ": "Djibouti", - "DM": "Dominica", - "DO": "Dominican Republic", - "EC": "Ecuador", - "EG": "Egypt", - "SV": "El Salvador", - "GQ": "Equatorial Guinea", - "ER": "Eritrea", - "EE": "Estonia", - "ET": "Ethiopia", - "FK": "Falkland Islands (Malvinas)", - "FO": "Faroe Islands", - "FJ": "Fiji", - "FI": "Finland", - "FR": "France", - "GF": "French Guiana", - "PF": "French Polynesia", - "TF": "French Southern Territories", - "GA": "Gabon", - "GM": "Gambia", - "GE": "Georgia", - "DE": "Germany", - "GH": "Ghana", - "GI": "Gibraltar", - "GR": "Greece", - "GL": "Greenland", - "GD": "Grenada", - "GP": "Guadeloupe", - "GU": "Guam", - "GT": "Guatemala", - "GG": "Guernsey", - "GN": "Guinea", - "GW": "Guinea-Bissau", - "GY": "Guyana", - "HT": "Haiti", - "HM": "Heard Island and Mcdonald Islands", - "VA": "Holy See (Vatican City State)", - "HN": "Honduras", - "HK": "Hong Kong", - "HU": "Hungary", - "IS": "Iceland", - "IN": "India", - "ID": "Indonesia", - "IR": "Iran, Islamic Republic Of", - "IQ": "Iraq", - "IE": "Ireland", - "IM": "Isle of Man", - "IL": "Israel", - "IT": "Italy", - "JM": "Jamaica", - "JP": "Japan", - "JE": "Jersey", - "JO": "Jordan", - "KZ": "Kazakhstan", - "KE": "Kenya", - "KI": "Kiribati", - "KP": "Korea, Democratic People'S Republic of", - "KR": "Korea, Republic of", - "KW": "Kuwait", - "KG": "Kyrgyzstan", - "LA": "Lao People'S Democratic Republic", - "LV": "Latvia", - "LB": "Lebanon", - "LS": "Lesotho", - "LR": "Liberia", - "LY": "Libyan Arab Jamahiriya", - "LI": "Liechtenstein", - "LT": "Lithuania", - "LU": "Luxembourg", - "MO": "Macao", - "MK": "Macedonia, The Former Yugoslav Republic of", - "MG": "Madagascar", - "MW": "Malawi", - "MY": "Malaysia", - "MV": "Maldives", - "ML": "Mali", - "MT": "Malta", - "MH": "Marshall Islands", - "MQ": "Martinique", - "MR": "Mauritania", - "MU": "Mauritius", - "YT": "Mayotte", - "MX": "Mexico", - "FM": "Micronesia, Federated States of", - "MD": "Moldova, Republic of", - "MC": "Monaco", - "MN": "Mongolia", - "MS": "Montserrat", - "MA": "Morocco", - "MZ": "Mozambique", - "MM": "Myanmar", - "NA": "Namibia", - "NR": "Nauru", - "NP": "Nepal", - "NL": "Netherlands", - "AN": "Netherlands Antilles", - "NC": "New Caledonia", - "NZ": "New Zealand", - "NI": "Nicaragua", - "NE": "Niger", - "NG": "Nigeria", - "NU": "Niue", - "NF": "Norfolk Island", - "MP": "Northern Mariana Islands", - "NO": "Norway", - "OM": "Oman", - "PK": "Pakistan", - "PW": "Palau", - "PS": "Palestinian Territory, Occupied", - "PA": "Panama", - "PG": "Papua New Guinea", - "PY": "Paraguay", - "PE": "Peru", - "PH": "Philippines", - "PN": "Pitcairn", - "PL": "Poland", - "PT": "Portugal", - "PR": "Puerto Rico", - "QA": "Qatar", - "RE": "Reunion", - "RO": "Romania", - "RU": "Russian Federation", - "RW": "RWANDA", - "SH": "Saint Helena", - "KN": "Saint Kitts and Nevis", - "LC": "Saint Lucia", - "PM": "Saint Pierre and Miquelon", - "VC": "Saint Vincent and the Grenadines", - "WS": "Samoa", - "SM": "San Marino", - "ST": "Sao Tome and Principe", - "SA": "Saudi Arabia", - "SN": "Senegal", - "CS": "Serbia and Montenegro", - "SC": "Seychelles", - "SL": "Sierra Leone", - "SG": "Singapore", - "SK": "Slovakia", - "SI": "Slovenia", - "SB": "Solomon Islands", - "SO": "Somalia", - "ZA": "South Africa", - "GS": "South Georgia and the South Sandwich Islands", - "ES": "Spain", - "LK": "Sri Lanka", - "SD": "Sudan", - "SR": "Suriname", - "SJ": "Svalbard and Jan Mayen", - "SZ": "Swaziland", - "SE": "Sweden", - "CH": "Switzerland", - "SY": "Syrian Arab Republic", - "TW": "Taiwan", - "TJ": "Tajikistan", - "TZ": "Tanzania, United Republic of", - "TH": "Thailand", - "TL": "Timor-Leste", - "TG": "Togo", - "TK": "Tokelau", - "TO": "Tonga", - "TT": "Trinidad and Tobago", - "TN": "Tunisia", - "TR": "Turkey", - "TM": "Turkmenistan", - "TC": "Turks and Caicos Islands", - "TV": "Tuvalu", - "UG": "Uganda", - "UA": "Ukraine", - "AE": "United Arab Emirates", - "GB": "United Kingdom", - "US": "United States", - "UM": "United States Minor Outlying Islands", - "UY": "Uruguay", - "UZ": "Uzbekistan", - "VU": "Vanuatu", - "VE": "Venezuela", - "VN": "Viet Nam", - "VG": "Virgin Islands, British", - "VI": "Virgin Islands, U.S.", - "WF": "Wallis and Futuna", - "EH": "Western Sahara", - "YE": "Yemen", - "ZM": "Zambia", - "ZW": "Zimbabwe" -} \ No newline at end of file diff --git a/api/ipapicom.js b/api/ipapicom.js index daf7e209..1382c807 100644 --- a/api/ipapicom.js +++ b/api/ipapicom.js @@ -1,6 +1,6 @@ -const http = require('http'); +import { get } from 'http'; -module.exports = (req, res) => { +export default (req, res) => { // 限制只能从指定域名访问 const allowedDomains = ['localhost', ...(process.env.ALLOWED_DOMAINS || '').split(',')]; const referer = req.headers.referer; @@ -23,7 +23,7 @@ module.exports = (req, res) => { // 构建请求 ip-api.com 的 URL const url = `http://ip-api.com/json/${ipAddress}`; - http.get(url, apiRes => { + get(url, apiRes => { let data = ''; apiRes.on('data', chunk => data += chunk); apiRes.on('end', () => { @@ -48,7 +48,7 @@ function modifyJsonForIPAPI(json) { ip: query, city, region: regionName, - country, + country: countryCode, country_name: country, country_code: countryCode, latitude: lat, diff --git a/api/ipinfo.js b/api/ipinfo.js index 8e471645..bc60ee19 100644 --- a/api/ipinfo.js +++ b/api/ipinfo.js @@ -1,7 +1,257 @@ -const https = require('https'); -const countryCodes = require('./countrycode.json'); +import { get } from 'https'; -module.exports = (req, res) => { +// 国家代码 +async function loadCountryCodes() { + const countryCodes = { + "AF": "Afghanistan", + "AX": "Åland Islands", + "AL": "Albania", + "DZ": "Algeria", + "AS": "American Samoa", + "AD": "AndorrA", + "AO": "Angola", + "AI": "Anguilla", + "AQ": "Antarctica", + "AG": "Antigua and Barbuda", + "AR": "Argentina", + "AM": "Armenia", + "AW": "Aruba", + "AU": "Australia", + "AT": "Austria", + "AZ": "Azerbaijan", + "BS": "Bahamas", + "BH": "Bahrain", + "BD": "Bangladesh", + "BB": "Barbados", + "BY": "Belarus", + "BE": "Belgium", + "BZ": "Belize", + "BJ": "Benin", + "BM": "Bermuda", + "BT": "Bhutan", + "BO": "Bolivia", + "BA": "Bosnia and Herzegovina", + "BW": "Botswana", + "BV": "Bouvet Island", + "BR": "Brazil", + "IO": "British Indian Ocean Territory", + "BN": "Brunei Darussalam", + "BG": "Bulgaria", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Cambodia", + "CM": "Cameroon", + "CA": "Canada", + "CV": "Cape Verde", + "KY": "Cayman Islands", + "CF": "Central African Republic", + "TD": "Chad", + "CL": "Chile", + "CN": "China", + "CX": "Christmas Island", + "CC": "Cocos (Keeling) Islands", + "CO": "Colombia", + "KM": "Comoros", + "CG": "Congo", + "CD": "Congo, The Democratic Republic of the", + "CK": "Cook Islands", + "CR": "Costa Rica", + "CI": "Cote D'Ivoire", + "HR": "Croatia", + "CU": "Cuba", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DK": "Denmark", + "DJ": "Djibouti", + "DM": "Dominica", + "DO": "Dominican Republic", + "EC": "Ecuador", + "EG": "Egypt", + "SV": "El Salvador", + "GQ": "Equatorial Guinea", + "ER": "Eritrea", + "EE": "Estonia", + "ET": "Ethiopia", + "FK": "Falkland Islands (Malvinas)", + "FO": "Faroe Islands", + "FJ": "Fiji", + "FI": "Finland", + "FR": "France", + "GF": "French Guiana", + "PF": "French Polynesia", + "TF": "French Southern Territories", + "GA": "Gabon", + "GM": "Gambia", + "GE": "Georgia", + "DE": "Germany", + "GH": "Ghana", + "GI": "Gibraltar", + "GR": "Greece", + "GL": "Greenland", + "GD": "Grenada", + "GP": "Guadeloupe", + "GU": "Guam", + "GT": "Guatemala", + "GG": "Guernsey", + "GN": "Guinea", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HT": "Haiti", + "HM": "Heard Island and Mcdonald Islands", + "VA": "Holy See (Vatican City State)", + "HN": "Honduras", + "HK": "Hong Kong", + "HU": "Hungary", + "IS": "Iceland", + "IN": "India", + "ID": "Indonesia", + "IR": "Iran, Islamic Republic Of", + "IQ": "Iraq", + "IE": "Ireland", + "IM": "Isle of Man", + "IL": "Israel", + "IT": "Italy", + "JM": "Jamaica", + "JP": "Japan", + "JE": "Jersey", + "JO": "Jordan", + "KZ": "Kazakhstan", + "KE": "Kenya", + "KI": "Kiribati", + "KP": "Korea, Democratic People'S Republic of", + "KR": "Korea, Republic of", + "KW": "Kuwait", + "KG": "Kyrgyzstan", + "LA": "Lao People'S Democratic Republic", + "LV": "Latvia", + "LB": "Lebanon", + "LS": "Lesotho", + "LR": "Liberia", + "LY": "Libyan Arab Jamahiriya", + "LI": "Liechtenstein", + "LT": "Lithuania", + "LU": "Luxembourg", + "MO": "Macao", + "MK": "Macedonia, The Former Yugoslav Republic of", + "MG": "Madagascar", + "MW": "Malawi", + "MY": "Malaysia", + "MV": "Maldives", + "ML": "Mali", + "MT": "Malta", + "MH": "Marshall Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MU": "Mauritius", + "YT": "Mayotte", + "MX": "Mexico", + "FM": "Micronesia, Federated States of", + "MD": "Moldova, Republic of", + "MC": "Monaco", + "MN": "Mongolia", + "MS": "Montserrat", + "MA": "Morocco", + "MZ": "Mozambique", + "MM": "Myanmar", + "NA": "Namibia", + "NR": "Nauru", + "NP": "Nepal", + "NL": "Netherlands", + "AN": "Netherlands Antilles", + "NC": "New Caledonia", + "NZ": "New Zealand", + "NI": "Nicaragua", + "NE": "Niger", + "NG": "Nigeria", + "NU": "Niue", + "NF": "Norfolk Island", + "MP": "Northern Mariana Islands", + "NO": "Norway", + "OM": "Oman", + "PK": "Pakistan", + "PW": "Palau", + "PS": "Palestinian Territory, Occupied", + "PA": "Panama", + "PG": "Papua New Guinea", + "PY": "Paraguay", + "PE": "Peru", + "PH": "Philippines", + "PN": "Pitcairn", + "PL": "Poland", + "PT": "Portugal", + "PR": "Puerto Rico", + "QA": "Qatar", + "RE": "Reunion", + "RO": "Romania", + "RU": "Russian Federation", + "RW": "RWANDA", + "SH": "Saint Helena", + "KN": "Saint Kitts and Nevis", + "LC": "Saint Lucia", + "PM": "Saint Pierre and Miquelon", + "VC": "Saint Vincent and the Grenadines", + "WS": "Samoa", + "SM": "San Marino", + "ST": "Sao Tome and Principe", + "SA": "Saudi Arabia", + "SN": "Senegal", + "CS": "Serbia and Montenegro", + "SC": "Seychelles", + "SL": "Sierra Leone", + "SG": "Singapore", + "SK": "Slovakia", + "SI": "Slovenia", + "SB": "Solomon Islands", + "SO": "Somalia", + "ZA": "South Africa", + "GS": "South Georgia and the South Sandwich Islands", + "ES": "Spain", + "LK": "Sri Lanka", + "SD": "Sudan", + "SR": "Suriname", + "SJ": "Svalbard and Jan Mayen", + "SZ": "Swaziland", + "SE": "Sweden", + "CH": "Switzerland", + "SY": "Syrian Arab Republic", + "TW": "Taiwan", + "TJ": "Tajikistan", + "TZ": "Tanzania, United Republic of", + "TH": "Thailand", + "TL": "Timor-Leste", + "TG": "Togo", + "TK": "Tokelau", + "TO": "Tonga", + "TT": "Trinidad and Tobago", + "TN": "Tunisia", + "TR": "Turkey", + "TM": "Turkmenistan", + "TC": "Turks and Caicos Islands", + "TV": "Tuvalu", + "UG": "Uganda", + "UA": "Ukraine", + "AE": "United Arab Emirates", + "GB": "United Kingdom", + "US": "United States", + "UM": "United States Minor Outlying Islands", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VU": "Vanuatu", + "VE": "Venezuela", + "VN": "Viet Nam", + "VG": "Virgin Islands, British", + "VI": "Virgin Islands, U.S.", + "WF": "Wallis and Futuna", + "EH": "Western Sahara", + "YE": "Yemen", + "ZM": "Zambia", + "ZW": "Zimbabwe" + }; + + return countryCodes; +} + +export default async (req, res) => { // 限制只能从指定域名访问 const allowedDomains = ['localhost', ...(process.env.ALLOWED_DOMAINS || '').split(',')]; const referer = req.headers.referer; @@ -26,13 +276,17 @@ module.exports = (req, res) => { const token = tokens[Math.floor(Math.random() * tokens.length)]; const url = `https://ipinfo.io/${ipAddress}?token=${token}`; - https.get(url, apiRes => { + get(url, apiRes => { let data = ''; apiRes.on('data', chunk => data += chunk); - apiRes.on('end', () => { + apiRes.on('end', async () => { try { const originalJson = JSON.parse(data); - const modifiedJson = modifyJson(originalJson); + + // 动态导入国家代码 + const countryCodes = await loadCountryCodes(); + const modifiedJson = modifyJson(originalJson, countryCodes); + res.json(modifiedJson); } catch (e) { res.status(500).json({ error: 'Error parsing JSON' }); @@ -43,7 +297,7 @@ module.exports = (req, res) => { }); }; -function modifyJson(json) { +function modifyJson(json, countryCodes) { const { ip, city, region, country, loc, org, timezone } = json; const countryName = countryCodes[country] || 'Unknown Country'; @@ -65,4 +319,3 @@ function modifyJson(json) { org: modifiedOrg }; } - diff --git a/api/map.js b/api/map.js index febb9ea5..07028b5d 100644 --- a/api/map.js +++ b/api/map.js @@ -1,6 +1,6 @@ -const https = require('https'); +import { get } from 'https'; -module.exports = (req, res) => { +export default (req, res) => { // 限制只能从指定域名访问 const allowedDomains = ['localhost', ...(process.env.ALLOWED_DOMAINS || '').split(',')]; @@ -33,7 +33,7 @@ module.exports = (req, res) => { 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}`; - https.get(url, apiRes => { + get(url, apiRes => { apiRes.pipe(res); }).on('error', (e) => { res.status(500).json({ error: e.message }); diff --git a/api/validate-map-key.js b/api/validate-map-key.js index 2ac21852..9ba762c0 100644 --- a/api/validate-map-key.js +++ b/api/validate-map-key.js @@ -1,4 +1,4 @@ -module.exports = (req, res) => { +export default (req, res) => { if (process.env.BING_MAP_API_KEY) { res.status(200).json({ isValid: true }); } else { diff --git a/api/validate-site.js b/api/validate-site.js index aff61cbf..d7eefafb 100644 --- a/api/validate-site.js +++ b/api/validate-site.js @@ -1,4 +1,4 @@ -module.exports = (req, res) => { +export default (req, res) => { const isIpCheckEnabled = process.env.IS_IPCHECK_ING === 'ipcheck.ing'; res.status(200).json({ isIpCheckEnabled }); }; diff --git a/index.html b/index.html new file mode 100644 index 00000000..9cc4d41c --- /dev/null +++ b/index.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + Check My IP Address and Geolocation | Check WebRTC Connection IP | DNS Leak Test | Speed Test | Jason Ng Open + Source + + + + + + + + +
+
+
+

首次加载的时间可能会较长,请等待...

+

It may take a while for the first time, please wait...

+
+
+
+ + + + + \ No newline at end of file diff --git a/package.json b/package.json index e1a3d974..64330a3a 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,33 @@ { "name": "myip", - "version": "2.1.0", - "description": "A better IP Toolbox", - "main": "server.js", + "private": true, + "version": "3.0.0", + "type": "module", "scripts": { - "start": "node server.js", - "test": "echo \"Error: no test specified\" && exit 1" + "dev": "concurrently \"vite\" \"npm run start-server\"", + "build": "vite build", + "preview": "vite preview", + "start-server": "node server.js", + "start-static": "node static-server.js", + "start": "concurrently \"npm run start-static\" \"npm run start-server\"" }, - "repository": { - "type": "git", - "url": "git+https://github.com/jason5ng32/MyIP.git" - }, - "author": "Jason Ng", - "license": "MIT", - "bugs": { - "url": "https://github.com/jason5ng32/MyIP/issues" - }, - "homepage": "https://github.com/jason5ng32/MyIP#readme", "dependencies": { - "dotenv": "^16.3.1", - "express": "^4.18.2" + "@analytics/google-analytics": "^1.0.7", + "@cloudflare/speedtest": "^1.3.0", + "analytics": "^0.8.9", + "bootstrap": "^5.3.2", + "bootstrap-icons": "^1.11.3", + "concurrently": "^8.2.2", + "dotenv": "^16.4.1", + "express": "^4.18.2", + "flag-icons": "^7.1.0", + "http-proxy-middleware": "^2.0.6", + "vue": "^3.3.11", + "vue-i18n": "^9.9.0", + "vuex": "^4.1.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.2", + "vite": "^5.0.8" } -} \ No newline at end of file +} diff --git a/public/res/img/Docker.svg b/public/Docker.svg similarity index 100% rename from public/res/img/Docker.svg rename to public/Docker.svg diff --git a/public/res/img/Vercel.svg b/public/Vercel.svg similarity index 100% rename from public/res/img/Vercel.svg rename to public/Vercel.svg diff --git a/public/contents/connectivityTests.js b/public/contents/connectivityTests.js deleted file mode 100644 index 8d284f71..00000000 --- a/public/contents/connectivityTests.js +++ /dev/null @@ -1,58 +0,0 @@ -export default [ - { - id: "netease", - name: "Netease", - icon: "globe-americas", - url: "https://www.163.com/favicon.ico?", - status: "", - }, - { - id: "baidu", - name: "Baidu", - icon: "globe-americas", - url: "https://www.baidu.com/favicon.ico?", - status: "", - }, - { - id: "wechat", - name: "WeChat", - icon: "wechat", - url: "https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico?", - status: "", - }, - { - id: "google", - name: "Google", - icon: "google", - url: "https://www.google.com/images/errors/robot.png?", - status: "", - }, - { - id: "cloudflare", - name: "Cloudflare", - icon: "cloud-fill", - url: "https://www.cloudflare.com/favicon.ico?", - status: "", - }, - { - id: "youtube", - name: "Youtube", - icon: "youtube", - url: "https://i.ytimg.com/vi/GYkq9Rgoj8E/hq720.jpg?", - status: "", - }, - { - id: "github", - name: "Github", - icon: "github", - url: "https://raw.githubusercontent.com/jason5ng32/fulian4/master/background.jpg?", - status: "", - }, - { - id: "chatgpt", - name: "ChatGPT", - icon: "chat-quote-fill", - url: "https://chat.openai.com/favicon.ico?", - status: "", - }, -]; diff --git a/public/contents/ipDataCards.js b/public/contents/ipDataCards.js deleted file mode 100644 index 3c064be2..00000000 --- a/public/contents/ipDataCards.js +++ /dev/null @@ -1,98 +0,0 @@ -export default [ - { - id: "taobao", - ip: "", - country_name: "", - region: "", - city: "", - latitude: "", - longitude: "", - isp: "", - asn: "", - asnlink: "", - mapUrl: "res/img/defaultMap.jpg", - mapUrl_dark: "res/img/defaultMap_dark.jpg", - showMap: false, - source: "Taobao", - }, - { - id: "special", - ip: "", - country_name: "", - region: "", - city: "", - latitude: "", - longitude: "", - isp: "", - asn: "", - asnlink: "", - mapUrl: "res/img/defaultMap.jpg", - mapUrl_dark: "res/img/defaultMap_dark.jpg", - showMap: false, - source: "Special", - }, - { - id: "cloudflare_v4", - ip: "", - country_name: "", - region: "", - city: "", - latitude: "", - longitude: "", - isp: "", - asn: "", - asnlink: "", - mapUrl: "res/img/defaultMap.jpg", - mapUrl_dark: "res/img/defaultMap_dark.jpg", - showMap: false, - source: "Cloudflare IPv4", - }, - { - id: "cloudflare_v6", - ip: "", - country_name: "", - region: "", - city: "", - latitude: "", - longitude: "", - isp: "", - asn: "", - asnlink: "", - mapUrl: "res/img/defaultMap.jpg", - mapUrl_dark: "res/img/defaultMap_dark.jpg", - showMap: false, - source: "Cloudflare IPv6", - }, - { - id: "ipify_v4", - ip: "", - country_name: "", - region: "", - city: "", - latitude: "", - longitude: "", - isp: "", - asn: "", - asnlink: "", - mapUrl: "res/img/defaultMap.jpg", - mapUrl_dark: "res/img/defaultMap_dark.jpg", - showMap: false, - source: "IPify IPv4", - }, - { - id: "ipify_v6", - ip: "", - country_name: "", - region: "", - city: "", - latitude: "", - longitude: "", - isp: "", - asn: "", - asnlink: "", - mapUrl: "res/img/defaultMap.jpg", - mapUrl_dark: "res/img/defaultMap_dark.jpg", - showMap: false, - source: "IPify IPv6", - }, -]; diff --git a/public/contents/lang_cn.js b/public/contents/lang_cn.js deleted file mode 100644 index afe64037..00000000 --- a/public/contents/lang_cn.js +++ /dev/null @@ -1,157 +0,0 @@ -export default { - nav: { - id: "nav", - Title: "IP 工具箱", - IPinfo: "IP 信息", - Connectivity: "网络连通性", - WebRTC: "WebRTC 测试", - DNSLeakTest: "DNS 泄漏测试", - SpeedTest: "网速测试", - PingTest: "全球测速", - }, - ipInfos: { - id: "ipinfos", - Title: "IP 信息", - Notes: - "将会从 6 个来源检查 IP 数据,如果当前 IP 栈只有 1 个,则没有数据的来源会显示为空。", - Simple: "简约", - Map: "地图", - MapUnavailable: "地图不可用", - Source: "来源", - IP: "IP", - Country: "国家", - Region: "地区", - City: "城市", - ISP: "ISP", - ASN: "ASN", - IPv4Error: "获取失败或不存在 IPv4 地址", - IPv6Error: "获取失败或不存在 IPv6 地址", - }, - connectivity: { - id: "connectivity", - Title: "网络连通性", - Note: "通过加载对应网站上的小图片进行测试,延迟值仅供参考,实际会更小。", - StatusWait: "待检测", - StatusAvailable: "可用", - StatusUnavailable: "不可用", - StatusTimeout: "超时或不可用", - checking: "检查中...", - }, - webrtc: { - id: "webrtc", - Title: "WebRTC 测试", - Note: "WebRTC 往往通过 UDP 直连进行建立,如果测试返回了真实 IP,则意味着你的代理设置没有覆盖这些连接。", - StatusWait: "待检测或连接错误", - StatusError: "测试出错", - }, - dnsleaktest: { - id: "dnsleaktest", - Name: "测试", - Title: "DNS 泄漏测试", - Note: "DNS 泄露(DNS Leaks)的意思是,当你挂上 VPN/代理后,你解析域名时,依然通过当地的运营商进行解析,这时就有 DNS 泄露的风险。", - Note2: - "泄露测试的方法是通过访问新生成的域名,检测你是通过哪个地区的 DNS 出口进行解析,如果返回的出口区域和你当地的运营商区域相同,则有 DNS 泄露风险,你可能需要修改 VPN/代理设置。", - Endpoint: "DNS 出口", - EndpointCountry: "出口地区", - StatusWait: "待检测", - StatusError: "测试出错", - }, - speedtest: { - id: "speedtest", - Title: "网速测试", - Note: "将从 Cloudflare 使用边沿网络进行速度测试,时间可能较长。如果你的网络位于中国大陆,数据可能会有较大的偏差。测速结果仅供参考,实际速度会更快。", - Download: "下载", - Upload: "上传", - Latency: "延迟", - Jitter: "抖动", - Unit: "Mbps", - StatusWait: "--", - StatusError: "测试出错", - videoStreaming: "在线视频:", - gaming: " 分,在线游戏:", - rtc: " 分,视频会议:", - score: "根据网速测试,得出以下质量分数:", - resultNote: " 分。分数仅供参考。", - }, - pingtest: { - id: "pingtest", - Title: "全球延迟测试", - Note: "将从分布在全球不同大洲和地区的服务器,分别对你的 IP 进行 Ping 测试,并获取延迟值。从而了解你与全球网络的连接速度。", - Note2: "目前仅支持 IPv4 地址,请选取你的 IP 地址进行测试", - SelectIP: "选择一个 IP 地址", - Region: "地区", - MinDelay: "最小延迟(ms)", - MaxDelay: "最大延迟(ms)", - AvgDelay: "平均延迟(ms)", - TotalPackets: "总包数", - PacketLoss: "丢包率", - ReceivedPackets: "收到", - DroppedPackets: "丢失", - Run: "运行测试", - Error: "测试失败,看起来你的 IP 不允许进行 Ping 测试。", - }, - ipcheck: { - id: "ipcheck", - Title: "IP 查询", - Placeholder: "请输入 IP 地址", - Button: "查询", - Error: "请输入有效的 IPv4 或 IPv6 地址。", - }, - alert: { - id: "alert", - refreshEverythingMessage: "正在刷新所有数据,请等待...", - refreshEverythingTitle: "正在刷新", - OhNo: "糟糕!", - Congrats: "恭喜呀!", - OhNo_Message: "连通性检测没有通过,部分网站无法访问。", - Congrats_Message: "所有的连通性检测均通过,你可以访问所有网站。", - maskedInfoTitle_1: "IP 隐藏成功", - maskedInfoMessage_1: "IP 信息已隐藏,截图时请注意隐私。", - maskedInfoTitle: "全部隐藏成功", - maskedInfoMessage: "所有信息已隐藏,现在可以安心截图了。", - unmaskedInfoTitle: "取消隐藏", - unmaskedInfoMessage: "信息已显示,截图时请注意隐私。", - }, - shortcutKeys: { - id: "shortcutKeys", - GoToTop: "回到顶部", - GoToBottom: "回到底部", - ToggleDarkMode: "切换深色模式", - RefreshEverything: "刷新所有数据", - RefreshIPCard: "刷新某张 IP 卡片", - RefreshConnectivityTests: "刷新连通性测试", - RefreshWebRTC: "刷新 WebRTC 测试", - RefreshDNSLeakTest: "刷新 DNS 泄漏测试", - StartSpeedTest: "开始网速测试", - ToggleMaps: "切换地图显示", - IPCheck: "IP 查询", - AddBingMapKey: "添加 Bing Map API Key", - ToggleInfoMask: "切换信息隐藏", - Help: "显示快捷键", - HelpNote: "使用快捷键的人,是喜欢效率的人,你真棒!", - GoNext: "下一张卡片", - GoPrevious: "上一张卡片", - }, - page: { - title: - "查看我的 IP 地址 | 查询本机 IP 地址及归属地 | 查看 WebRTC 连接 IP | DNS 泄露检测 | 网速测试 | Jason Ng 阿禅开源作品", - description:"一个更好的 IP 工具箱。轻松查看你的 IP 地址,IP 信息,检查 DNS 泄露,检查 WebRTC 连接,以及测试网站可用性", - keywords:"我的IP,IP工具,我的IP,IP查询,DNS泄漏测试,WebRTC测试,网速测试", - footerName: "Creators: Jason Ng, Seven Yu & ChatGPT", - footerLink: "https://github.com/jason5ng32/MyIP", - copyRightName: "Originally from ", - copyRightLink: "https://ipcheck.ing", - copyRightLinkName: "IPCheck.ing", - }, - addBingMapKey: { - Title: "添加 Bing Map API Key", - Note: "添加后,可以在 IP 信息中显示地图。API Key 只会在当前浏览器中保存,方便下次使用,不会上传到服务器。", - Placeholder: "请输入 Bing Map API Key", - Button: "保存", - removeButton: "删除", - Error: "请输入有效的 Bing Map API Key。", - }, - helpModal: { - Title: "快捷键", - }, -}; diff --git a/public/contents/lang_en.js b/public/contents/lang_en.js deleted file mode 100644 index aa25fc61..00000000 --- a/public/contents/lang_en.js +++ /dev/null @@ -1,161 +0,0 @@ -export default { - nav: { - id: "nav", - Title: "IP Toolbox", - IPinfo: "IP Infos", - Connectivity: "Connectivity", - WebRTC: "WebRTC Test", - DNSLeakTest: "DNS Leak Test", - SpeedTest: "Speed Test", - PingTest: "Global Latency", - }, - ipInfos: { - id: "ipinfos", - Title: "IP Infos", - Notes: - "IP data will be checked from 6 sources. If there is only 1 IP stack currently, sources without data will show as empty.", - Simple: "Simple", - Map: "Map", - MapUnavailable: "Map Unavailable", - Source: "Source", - IP: "IP", - Country: "Country", - Region: "Region", - City: "City", - ISP: "ISP", - ASN: "ASN", - IPv4Error: "Fetch Failed or No IPv4 Address", - IPv6Error: "Fetch Failed or No IPv6 Address", - }, - connectivity: { - id: "connectivity", - Title: "Network Connectivity", - Note: "Testing is done by loading small images from corresponding websites. Delay values are for reference only and will be smaller in reality.", - StatusWait: "Awaiting Test", - StatusAvailable: "OK", - StatusUnavailable: "Unavailable", - StatusTimeout: "Timeout or Unavailable", - checking: "Checking...", - }, - webrtc: { - id: "webrtc", - Title: "WebRTC Test", - Note: "WebRTC often establishes connections directly via UDP. If the test returns your real IP, it means your proxy settings do not cover these connections.", - StatusWait: "Awaiting Test or Connection Error", - StatusError: "Test Error", - }, - dnsleaktest: { - id: "dnsleaktest", - Name: "Test", - Title: "DNS Leak Test", - Note: "A DNS leak means that when you are connected to a VPN/proxy, your domain name resolutions are still done through your local ISP, thus posing a risk of DNS leaks.", - Note2: - "The method to test for DNS leaks involves accessing a newly generated domain name to detect which regional DNS exit you are resolving through. If the endpoint region returned matches your local service provider's region, there is a risk of DNS leakage. In this case, you may need to modify your VPN/proxy settings.", - Endpoint: "DNS Endpoint", - EndpointCountry: "Endpoint Region", - StatusWait: "Awaiting Test", - StatusError: "Test Error", - }, - speedtest: { - id: "speedtest", - Title: "Speed Test", - Note: "Speed test will be done via Cloudflare's edge network, which may take a long time. If your network is located in mainland China, the data may be biased. The speed test results are for reference only, the actual speed will be faster.", - Download: "Download", - Upload: "Upload", - Latency: "Latency", - Jitter: "Jitter", - Unit: "Mbps", - StatusWait: "--", - StatusError: "Test Error", - videoStreaming: "Video Streaming: ", - gaming: ", Gaming: ", - rtc: ", Video Chatting: ", - score: "Based on the speed test, got the following quality scores:", - resultNote: ". The score is for reference only.", - }, - pingtest: { - id: "pingtest", - Title: "Global Latency Test", - Note: "Latency tests will be performed on servers located in different continents and regions around the world to measure the latency of your IP. This will help you understand your connection speed to the global network.", - Note2: "Currently only supports IPv4 addresses, please select your IP address for testing", - SelectIP: "Select an IP Address", - Region: "Region", - MinDelay: "Min Delay (ms)", - MaxDelay: "Max Delay (ms)", - AvgDelay: "Avg Delay (ms)", - TotalPackets: "Total Packets", - PacketLoss: "Packet Loss", - ReceivedPackets: "Received", - DroppedPackets: "Dropped", - Run: "Run Test", - Error: "Test failed, it looks like your IP is not allowed to perform Ping test.", - }, - ipcheck: { - id: "ipcheck", - Title: "IP Check", - Placeholder: "Please enter an IP address", - Button: "Check", - Error: "Please enter a valid IPv4 or IPv6 address.", - }, - alert: { - id: "alert", - refreshEverythingMessage: "Refreshing all data, please wait...", - refreshEverythingTitle: "Refreshing", - OhNo: "Oh No!", - Congrats: "Congrats!", - OhNo_Message: "Connectivity test failed, some websites are not accessible.", - Congrats_Message: - "All connectivity tests passed, you can access all websites.", - maskedInfoTitle_1: "IP masked", - maskedInfoMessage_1: - "IP information has been masked, please be careful when taking screenshots.", - maskedInfoTitle: "All infos masked", - maskedInfoMessage: - "All information has been masked, you can now safely take screenshots.", - unmaskedInfoTitle: "Infos Unmasked", - unmaskedInfoMessage: - "Information has been unmasked, please be careful when taking screenshots.", - }, - shortcutKeys: { - id: "shortcutKeys", - GoToTop: "Go to Top", - GoToBottom: "Go to Bottom", - ToggleDarkMode: "Toggle Dark Mode", - RefreshEverything: "Refresh Everything", - RefreshIPCard: "Refresh IP Card", - RefreshConnectivityTests: "Refresh Connectivity Tests", - RefreshWebRTC: "Refresh WebRTC Test", - RefreshDNSLeakTest: "Refresh DNS Leak Test", - StartSpeedTest: "Start Speed Test", - ToggleMaps: "Toggle Maps", - IPCheck: "IP Check", - AddBingMapKey: "Add Bing Map API Key", - ToggleInfoMask: "Toggle Info Masking", - Help: "Show Shortcut Keys", - HelpNote: "People who use shortcuts are really awesome!", - GoNext: "Next Card", - GoPrevious: "Previous Card", - }, - page: { - title: - "Check My IP Address and Geolocation | Check WebRTC Connection IP | DNS Leak Test | Speed Test | Jason Ng Open Source", - description:"A better IP Toolbox. Easy to check what's your IPs, IP informations, check for DNS leaks, examine WebRTC connections, and test website availability", - keywords:"MyIP,IP Tool,My IP,IP check,IP lookup,DNS leak test,WebRTC test,Speed test", - footerName: "Creators: Jason Ng, Seven Yu & ChatGPT", - footerLink: "https://github.com/jason5ng32/MyIP", - copyRightName: "Originally from ", - copyRightLink: "https://ipcheck.ing", - copyRightLinkName: "IPCheck.ing", - }, - addBingMapKey: { - Title: "Add Bing Map API Key", - Note: "After adding, the map can be displayed in the IP information. The API Key will only be saved in the current browser for the next use, and will not be uploaded to the server.", - Placeholder: "Please enter Bing Map API Key", - Button: "Save", - removeButton: "Remove", - Error: "Please enter a valid Bing Map API Key.", - }, - helpModal: { - Title: "Shortcuts", - }, -}; diff --git a/public/contents/leakTest.js b/public/contents/leakTest.js deleted file mode 100644 index e594ef64..00000000 --- a/public/contents/leakTest.js +++ /dev/null @@ -1,26 +0,0 @@ -export default [ - { - id: "ipapi1", - name: "", - geo: "", - ip: "", - }, - { - id: "ipapi2", - name: "", - geo: "", - ip: "", - }, - { - id: "sfshark1", - name: "", - geo: "", - ip: "", - }, - { - id: "sfshark2", - name: "", - geo: "", - ip: "", - }, -]; diff --git a/public/contents/speedTest.js b/public/contents/speedTest.js deleted file mode 100644 index acc4c1c0..00000000 --- a/public/contents/speedTest.js +++ /dev/null @@ -1,11 +0,0 @@ -export default - { - id: "speedTest", - downloadSpeed: "-", - uploadSpeed: "-", - latency: "-", - jitter: "-", - streamingScore: "-", - gamingScore: "-", - rtcScore: "-", - } diff --git a/public/contents/stunServers.js b/public/contents/stunServers.js deleted file mode 100644 index 85010bbd..00000000 --- a/public/contents/stunServers.js +++ /dev/null @@ -1,50 +0,0 @@ -export default [ - { - id: "google", - name: "Google 1", - url: "stun:stun.l.google.com:19302", - ip: "", - }, - { - id: "google", - name: "Google 2", - url: "stun:stun2.l.google.com:19302", - ip: "", - }, - { - id: "nextcloud", - name: "NxtCld", - url: "stun:stun.nextcloud.com:443", - ip: "", - }, - { - id: "twilio", - name: "Twilio", - url: "stun:global.stun.twilio.com", - ip: "", - }, - { - id: "cloudflare", - name: "Cloudflare", - url: "stun:stun.cloudflare.com", - ip: "", - }, - { - id: "miwifi", - name: "MiWiFi", - url: "stun:stun.miwifi.com", - ip: "", - }, - { - id: "qq", - name: "QQ", - url: "stun:stun.qq.com", - ip: "", - }, - { - id: "stunprotocol", - name: "StnPtc", - url: "stun:stunserver.stunprotocol.org", - ip: "", - }, -]; diff --git a/public/res/img/defaultMap.jpg b/public/defaultMap.jpg similarity index 100% rename from public/res/img/defaultMap.jpg rename to public/defaultMap.jpg diff --git a/public/res/img/defaultMap_dark.jpg b/public/defaultMap_dark.jpg similarity index 100% rename from public/res/img/defaultMap_dark.jpg rename to public/defaultMap_dark.jpg diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 2487ec81..00000000 --- a/public/index.html +++ /dev/null @@ -1,577 +0,0 @@ - - - - - - - - - - - - - - Check My IP Address and Geolocation | Check WebRTC Connection IP | DNS Leak Test | Speed Test | Jason Ng Open - Source - - - - - - - - - - - - -
-
- Loading... -
-
- -
- - - - - - - -
- -
- - -
-
- -
-
-

🕵️ - {{currentTexts.ipInfos.Title}}

-
- -
- - -
- -
- - - - -
- - - -
-
-
-

{{currentTexts.ipInfos.Notes}}

-
-
-
-
-
-
- {{currentTexts.ipInfos.Source}}: {{ card.source }} - -
- Map -
-
    -
  • 🖥️ {{currentTexts.ipInfos.IP}}: {{ card.ip - }} - -
  • -
  • - 🌍 {{currentTexts.ipInfos.Country}}: {{ - card.country_name }}  -
  • -
  • 🏚️ - {{currentTexts.ipInfos.Region}}: {{ card.region - }}
  • -
  • 🚏 - {{currentTexts.ipInfos.City}}: {{ card.city }} -
  • -
  • 🏢 - {{currentTexts.ipInfos.ISP}}: {{ card.isp }} -
  • -
  • 📶 - {{currentTexts.ipInfos.ASN}}: {{ - card.asn }}{{ - card.asn - }}
  • -
-
- -
-
-
-
-
- - -
-
-

🚦 {{currentTexts.connectivity.Title}}

- -
-
-

{{currentTexts.connectivity.Note}}

-
-
-
-
-
-
{{ test.name }}
- -

-

-
-
-
-
-
- - -
-
-

🚥 {{currentTexts.webrtc.Title}}

- -
-
-

{{currentTexts.webrtc.Note}}

-
-
-
-
-
-
{{ stun.name }}
-

{{ stun.url }}

-

-

-
-
-
-
-
- - -
-
-

🛑 {{currentTexts.dnsleaktest.Title}}

- -
-
-

{{currentTexts.dnsleaktest.Note}}

-

{{currentTexts.dnsleaktest.Note2}}

-
-
-
-
-
-
{{ leak.name }}
-

{{currentTexts.dnsleaktest.Endpoint}}: {{ - leak.ip }}

-

{{currentTexts.dnsleaktest.EndpointCountry}}: {{ leak.geo }}

-

-
-
-
-
-
- - - -
-
-

🚀 {{currentTexts.speedtest.Title}}

- - -
-
-

{{currentTexts.speedtest.Note}}

-
-
-
-
-
-
-
-
-
-
-
-
{{currentTexts.speedtest.Download}}
-

- {{speedTest.downloadSpeed}} - Mb/s -

-
-
-
{{currentTexts.speedtest.Upload}}
-

- {{speedTest.uploadSpeed}} - Mb/s -

-
-
-
{{currentTexts.speedtest.Latency}}
-

- {{speedTest.latency}} - ms -

-
-
-
{{currentTexts.speedtest.Jitter}}
-

- {{speedTest.jitter}} - ms -

-
-
-
-

📋 {{currentTexts.speedtest.score}} - {{currentTexts.speedtest.videoStreaming}} - - {{speedTest.streamingScore}} - - {{currentTexts.speedtest.gaming}} - - {{speedTest.gamingScore}} - - {{currentTexts.speedtest.rtc}} - - {{speedTest.rtcScore}} - - {{currentTexts.speedtest.resultNote}} -

-
-
-
-
-
-
-
- - -
-
-

🌐 {{currentTexts.pingtest.Title}}

- -
-
-

{{currentTexts.pingtest.Note}}

-
-
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
{{currentTexts.pingtest.Region}}{{currentTexts.pingtest.MinDelay}}{{currentTexts.pingtest.MaxDelay}}{{currentTexts.pingtest.AvgDelay}}{{currentTexts.pingtest.TotalPackets}}{{currentTexts.pingtest.PacketLoss}}{{currentTexts.pingtest.ReceivedPackets}}{{currentTexts.pingtest.DroppedPackets}}
- - {{ result.country }} - {{ result.stats.min.toFixed(1) }} - {{ result.stats.max.toFixed(1) }} - {{ result.stats.avg.toFixed(1) }} - {{ result.stats.total }}{{ Math.round(result.stats.loss) }}%{{ result.stats.rcv }}{{ result.stats.drop }}
-
-
- -
-

{{currentTexts.pingtest.Error}}

-
- -
-
-
-
-
- - - - - -
-
- - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/res/img/logo-144.png b/public/logo-144.png similarity index 100% rename from public/res/img/logo-144.png rename to public/logo-144.png diff --git a/public/res/img/logo-192.png b/public/logo-192.png similarity index 100% rename from public/res/img/logo-192.png rename to public/logo-192.png diff --git a/public/logo-256.png b/public/logo-256.png new file mode 100644 index 00000000..d5eb64c6 Binary files /dev/null and b/public/logo-256.png differ diff --git a/public/res/img/logo-512.png b/public/logo-512.png similarity index 100% rename from public/res/img/logo-512.png rename to public/logo-512.png diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 00000000..b1fe4077 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index 9bf302f5..f503f4ef 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -5,21 +5,26 @@ "description": "IP Toolbox", "icons": [ { - "src": "res/img/logo-512.png", + "src": "logo-512.png", "type": "image/png" }, { - "src": "res/img/logo-144.png", + "src": "logo-144.png", "sizes": "144x144", "type": "image/png" }, { - "src": "res/img/logo-192.png", + "src": "logo-192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "res/img/logo-512.png", + "src": "logo-256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "logo-512.png", "sizes": "512x512", "type": "image/png" } diff --git a/public/res/app.js b/public/res/app.js deleted file mode 100644 index 1ba25d05..00000000 --- a/public/res/app.js +++ /dev/null @@ -1,1339 +0,0 @@ -import textCN from "../contents/lang_cn.js"; -import textEN from "../contents/lang_en.js"; -import connectivityTests from "../contents/connectivityTests.js"; -import stunServers from "../contents/stunServers.js"; -import ipDataCards from "../contents/ipDataCards.js"; -import leakTest from "../contents/leakTest.js"; -import speedTest from "../contents/speedTest.js"; -import { triggerSpeedTest, resetSpeedTest, engine } from "../res/cfSpeedTest.js"; -import { mappingKeys, navigateCards, keyMap } from "./shortcut.js"; -import config from "../res/ga.js"; - -Vue.config.productionTip = false; - -(function () { - const scriptTag = document.createElement("script"); - scriptTag.async = true; - scriptTag.src = `https://www.googletagmanager.com/gtag/js?id=${config.GOOGLE_ANALYTICS_ID}`; - document.head.appendChild(scriptTag); - - window.dataLayer = window.dataLayer || []; - window.gtag = function () { - window.dataLayer.push(arguments); - }; - window.gtag("js", new Date()); - window.gtag("config", config.GOOGLE_ANALYTICS_ID); -})(); - -new Vue({ - el: "#app", - data: { - - isEnvBingMapKey: false, - BingMapCanvasMode: "CanvasLight", - currentLanguage: "en", - currentTexts: {}, - - alertMessage: "", - alertStyle: "", - alertTitle: "", - alertToShow: false, - inputIP: "", - inputBingMapAPIKEY: "", - bingMapAPIKEYError: false, - bingMapLanguage: "en", - modalQueryResult: null, - modalQueryError: "", - isMapShown: false, - isDarkMode: false, - isMobile: false, - isCardsCollapsed: JSON.parse(localStorage.getItem('isCardsCollapsed')) || false, - isInfoMasked: false, - isInfosLoaded: false, - infoMaskLevel: 0, - ipDataCache: new Map(), - speedTestStatus: "idle", - copiedStatus: {}, - siteValidate: false, - - // from contents - connectivityTests, - originconnectivityTests: {}, - ipDataCards, - originipDataCards: {}, - stunServers, - originstunServers: {}, - leakTest, - speedTest, - originleakTest: {}, - allIPs: [], - selectedIP: null, - pingResults: {}, - pingCheckStatus: "idle", - - // keyMap - keyMap, - }, - methods: { - - getIPFromTaobao() { - window.ipCallback = (data) => { - var ip = data.ip; - this.ipDataCards[0].source = "TaoBao"; - this.fetchIPDetails(0, ip); - delete window.ipCallback; - }; - var script = document.createElement("script"); - script.src = "https://www.taobao.com/help/getip.php?callback=ipCallback"; - document.head.appendChild(script); - document.head.removeChild(script); - }, - - getIPFromSpecial() { - fetch('/api/validate-site') - .then(response => response.json()) - .then(data => { - this.siteValidate = data.isIpCheckEnabled; - if (data.isIpCheckEnabled) { - this.getIPFromGCR(); - } else { - this.getIPFromUpai(); - } - }); - }, - - getIPFromUpai() { - const unixTime = Date.now(); - const url = `https://pubstatic.b0.upaiyun.com/?_upnode&t=${unixTime}`; - - fetch(url) - .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - } - return response.json(); - }) - .then((data) => { - const ip = data.remote_addr; - this.ipDataCards[1].source = "Upai" - this.fetchIPDetails(1, ip); - }) - .catch((error) => { - console.error("Error fetching IP from Upai:", error); - this.ipDataCards[1].ip = this.currentTexts.ipInfos.IPv4Error; - }); - }, - - getIPFromGCR() { - const url = `https://getipfromgoogle.ipcheck.ing/`; - - fetch(url) - .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - } - return response.json(); - }) - .then((data) => { - const fullIp = data.ip; - const ip = fullIp.includes(',') ? fullIp.split(',')[0] : fullIp; - this.ipDataCards[1].source = "IPCheck.ing"; - this.fetchIPDetails(1, ip); - }) - - .catch((error) => { - console.error("Error fetching IP from IPCheck.ing:", error); - this.getIPFromUpai(); - }); - }, - - getIPFromCloudflare_V4() { - fetch("https://1.0.0.1/cdn-cgi/trace") - .then((response) => response.text()) - .then((data) => { - const lines = data.split("\n"); - const ipLine = lines.find((line) => line.startsWith("ip=")); - if (ipLine) { - const ip = ipLine.split("=")[1]; - this.fetchIPDetails(2, ip); - } - }) - .catch((error) => { - console.error("Error fetching IP from Cloudflare:", error); - this.ipDataCards[2].ip = this.currentTexts.ipInfos.IPv4Error; - }); - }, - - getIPFromCloudflare_V6() { - fetch("https://[2606:4700:4700::1111]/cdn-cgi/trace") - .then((response) => response.text()) - .then((data) => { - const lines = data.split("\n"); - const ipLine = lines.find((line) => line.startsWith("ip=")); - if (ipLine) { - const ip = ipLine.split("=")[1]; - this.fetchIPDetails(3, ip); - } - }) - .catch((error) => { - console.error("Error fetching IP from Cloudflare:", error); - this.ipDataCards[3].ip = this.currentTexts.ipInfos.IPv6Error; - }); - }, - getIPFromIpify_V4() { - fetch("https://api4.ipify.org?format=json") - .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - } - return response.json(); - }) - .then((data) => { - this.fetchIPDetails(4, data.ip); - }) - .catch((error) => { - console.error("Error fetching IPv4 address from ipify:", error); - this.ipDataCards[4].ip = this.currentTexts.ipInfos.IPv4Error; - }); - }, - getIPFromIpify_V6() { - fetch("https://api6.ipify.org?format=json") - .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - } - return response.json(); - }) - .then((data) => { - this.fetchIPDetails(5, data.ip); - }) - .catch((error) => { - console.error("Error fetching IPv6 address from ipify:", error); - this.ipDataCards[5].ip = this.currentTexts.ipInfos.IPv6Error; - }); - }, - - async fetchIPDetails(cardIndex, ip) { - const card = this.ipDataCards[cardIndex]; - card.ip = ip; - - // 检查缓存中是否已有该 IP 的数据 - if (this.ipDataCache.has(ip)) { - // 使用缓存的数据填充卡片 - const cachedData = this.ipDataCache.get(ip); - Object.assign(card, cachedData); - return; - } - - // 尝试从多个不同的源获取数据 - const sources = [ - { url: `/api/ipinfo?ip=${ip}`, transform: this.transformDataFromIPapi }, - { url: `https://ipapi.co/${ip}/json/`, transform: this.transformDataFromIPapi }, - { url: `/api/ipapicom?ip=${ip}`, transform: this.transformDataFromIPapi } - ]; - - for (const source of sources) { - try { - const response = await fetch(source.url); - const data = await response.json(); - - // 根据数据源进行数据转换 - const cardData = source.transform(data); - - if (cardData) { - Object.assign(card, cardData); - this.ipDataCache.set(ip, cardData); - break; - } - } catch (error) { - console.error("Error fetching IP details:", error); - } - } - }, - - transformDataFromIPapi(data) { - if (data.error) { - throw new Error(data.reason); - } - - return { - country_name: data.country_name || "", - country_code: data.country || "", - region: data.region || "", - city: data.city || "", - latitude: data.latitude || "", - longitude: data.longitude || "", - isp: data.org || "", - asn: data.asn || "", - asnlink: data.asn ? `https://radar.cloudflare.com/traffic/${data.asn}` : false, - mapUrl: data.latitude && data.longitude ? `/api/map?latitude=${data.latitude}&longitude=${data.longitude}&language=${this.bingMapLanguage}&CanvasMode=CanvasLight` : "", - mapUrl_dark: data.latitude && data.longitude ? `/api/map?latitude=${data.latitude}&longitude=${data.longitude}&language=${this.bingMapLanguage}&CanvasMode=RoadDark` : "" - }; - }, - - - refreshCard(card) { - // 清空卡片数据 - this.clearCardData(card); - switch (card.source) { - case "Cloudflare IPv4": - this.getIPFromCloudflare_V4(card); - break; - case "Cloudflare IPv6": - this.getIPFromCloudflare_V6(card); - break; - case "IPify IPv4": - this.getIPFromIpify_V4(card); - break; - case "IPify IPv6": - this.getIPFromIpify_V6(card); - break; - case "Upai": - this.getIPFromUpai(card); - break; - case "IPCheck.ing": - this.getIPFromGCR(card); - break; - case "TaoBao": - this.getIPFromTaobao(card); - break; - default: - console.error("Undefind Source:", card.source); - } - }, - - clearCardData(card) { - card.ip = ""; - card.country_name = ""; - card.country_code = ""; - card.region = ""; - card.city = ""; - card.latitude = ""; - card.longitude = ""; - card.asn = ""; - card.isp = ""; - card.mapUrl = "res/img/defaultMap.jpg"; - card.mapUrl_dark = "res/img/defaultMap_dark.jpg"; - }, - - toggleMaps() { - this.isMapShown = !this.isMapShown; - this.ipDataCards.forEach((card) => { - card.showMap = this.isMapShown; - }); - }, - - checkAllIPs() { - // 从所有来源获取 IP 地址 - setTimeout(() => { - this.getIPFromSpecial(); - }, 500); - setTimeout(() => { - this.getIPFromTaobao(); - }, 500); - setTimeout(() => { - this.getIPFromCloudflare_V4(); - }, 500); - setTimeout(() => { - this.getIPFromCloudflare_V6(); - }, 100); - setTimeout(() => { - this.getIPFromIpify_V4(); - }, 1000); - setTimeout(() => { - this.getIPFromIpify_V6(); - }, 1000); - }, - - // 检查网络连通性 - checkConnectivityHandler(test, isAlertToShow, onTestComplete) { - const beginTime = +new Date(); - - var img = new Image(); - var timeout = setTimeout(() => { - test.status = this.currentTexts.connectivity.StatusUnavailable; - onTestComplete(false); - }, 3 * 1200); - - img.onload = () => { - clearTimeout(timeout); - test.status = - this.currentTexts.connectivity.StatusAvailable + - ` ( ${+new Date() - beginTime} ms )`; - onTestComplete(true); - }; - - img.onerror = () => { - clearTimeout(timeout); - test.status = this.currentTexts.connectivity.StatusUnavailable; - onTestComplete(false); - }; - - img.src = `${test.url}${Date.now()}`; - }, - - checkAllConnectivity(isAlertToShow, isRefresh) { - - if (isRefresh) { - connectivityTests.forEach((test) => { - test.status = this.currentTexts.connectivity.StatusWait; - }); - } - let totalTests = connectivityTests.length; - let successCount = 0; - let completedCount = 0; - - const onTestComplete = (isSuccess) => { - if (isSuccess) { - successCount++; - } - completedCount++; - - // 只有当所有测试都完成时才做出最终判断 - if (completedCount === totalTests) { - this.alertToShow = true; - if (successCount === totalTests) { - this.updateConnectivityAlert(true, "success"); - } else { - this.updateConnectivityAlert(true, "error"); - } - } - }; - - connectivityTests.forEach((test, index) => { - setTimeout(() => { - this.checkConnectivityHandler(test, isAlertToShow, onTestComplete); - }, 50 * index); - }); - - if (isAlertToShow) { - setTimeout(() => { - this.showToast(); - }, 4000); - } - }, - - updateConnectivityAlert(show, type) { - this.alertToShow = show; - if (type === "success") { - this.alertStyle = "text-success"; - this.alertMessage = this.currentTexts.alert.Congrats_Message; - this.alertTitle = this.currentTexts.alert.Congrats; - } else { - this.alertStyle = "text-danger"; - this.alertMessage = this.currentTexts.alert.OhNo_Message; - this.alertTitle = this.currentTexts.alert.OhNo; - } - }, - - // 通知气泡 - showToast() { - this.$nextTick(() => { - const toastEl = this.$refs.toast; - if (toastEl) { - const toast = new bootstrap.Toast(toastEl); - toast.show(); - } else { - console.error("Toast element not found"); - } - }); - }, - - // 查询 IP 信息 - async submitQuery() { - if (this.isValidIP(this.inputIP)) { - this.modalQueryError = ""; - this.modalQueryResult = null; - await this.fetchIPForModal(this.inputIP); - } else { - this.modalQueryError = this.currentTexts.ipcheck.Error; - this.modalQueryResult = null; - } - }, - setupModalEventListener() { - const modalElement = document.getElementById("IPCheck"); - modalElement.addEventListener("hidden.bs.modal", this.resetModalData); - }, - isValidIP(ip) { - const ipv4Pattern = - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - const ipv6Pattern = - /^(([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4})|(([0-9a-fA-F]{1,4}:){0,6}([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:){0,6}([0-9a-fA-F]{1,4})?))$/; - return ipv4Pattern.test(ip) || ipv6Pattern.test(ip); - }, - async fetchIPForModal(ip) { - const sources = [ - { url: `/api/ipinfo?ip=${ip}`, transform: this.transformDataFromIPapi }, - { url: `https://ipapi.co/${ip}/json/`, transform: this.transformDataFromIPapi }, - { url: `/api/ipapicom?ip=${ip}`, transform: this.transformDataFromIPapi } - ]; - - for (const source of sources) { - try { - const response = await fetch(source.url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - if (data.error) { - throw new Error(data.reason || "IP lookup failed"); - } - - // 使用对应的转换函数更新 modalQueryResult - this.modalQueryResult = source.transform(data); - break; - } catch (error) { - console.error("Error fetching IP details:", error); - } - } - }, - - // 检查 WebRTC - async checkSTUNServer(stun) { - try { - const servers = { iceServers: [{ urls: stun.url }] }; - const pc = new RTCPeerConnection(servers); - let candidateReceived = false; - - pc.onicecandidate = (event) => { - if (event.candidate) { - candidateReceived = true; - const candidate = event.candidate.candidate; - const ipMatch = /([0-9a-f]{1,4}(:[0-9a-f]{1,4}){7}|[0-9a-f]{0,4}(:[0-9a-f]{1,4}){0,6}::[0-9a-f]{0,4}|::[0-9a-f]{1,4}(:[0-9a-f]{1,4}){0,6}|[0-9]{1,3}(\.[0-9]{1,3}){3})/i.exec(candidate); - if (ipMatch) { - stun.ip = ipMatch[0]; - pc.close(); - } - } - }; - - pc.createDataChannel(""); - await pc.createOffer().then((offer) => pc.setLocalDescription(offer)); - - // 设置一个超时计时器 - await new Promise((resolve, reject) => { - setTimeout(() => { - if (!candidateReceived) { - reject(new Error("Stun Server Test Timeout")); - } else { - resolve(); - } - }, 5000); - }); - } catch (error) { - console.error("STUN Server Test Error:", error); - stun.ip = this.currentTexts.webrtc.StatusError; - } - }, - - checkAllWebRTC(isRefresh) { - this.stunServers.forEach((server) => { - if (isRefresh) { - server.ip = this.currentTexts.webrtc.StatusWait; - } - this.checkSTUNServer(server); - }); - }, - - - // DNS 泄漏测试 - generate32DigitString() { - const unixTime = Date.now().toString(); - const fixedString = "jason5ng32"; - const randomString = Math.random().toString(36).substring(2, 11); - - return unixTime + fixedString + randomString; - }, - - generate14DigitString() { - const fixedString = "jn32"; - const randomString = Math.random().toString(36).substring(2, 11); - - return fixedString + randomString; - }, - - fetchLeakTestIpApiCom(index) { - const urlString = this.generate32DigitString(); - const url = `https://${urlString}.edns.ip-api.com/json`; - - fetch(url) - .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - } - return response.json(); - }) - .then((data) => { - if (data.dns && "geo" in data.dns && "ip" in data.dns) { - const geoSplit = data.dns.geo.split(" - "); - this.leakTest[index].geo = geoSplit[0]; - this.leakTest[index].ip = data.dns.ip; - } else { - console.error("Unexpected data structure:", data); - } - }) - .catch((error) => { - console.error("Error fetching leak test data:", error); - this.leakTest[index].geo = this.currentTexts.dnsleaktest.StatusError; - this.leakTest[index].ip = this.currentTexts.dnsleaktest.StatusError; - }); - }, - - fetchLeakTestSfSharkCom(index, key) { - const urlString = this.generate14DigitString(); - const url = `https://${urlString}.ipv4.surfsharkdns.com`; - - fetch(url) - .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - } - return response.json(); - }) - .then((data) => { - const getKey = Object.keys(data)[key]; - const keyEntry = data[getKey]; - - if (keyEntry && keyEntry.Country && keyEntry.IP) { - this.leakTest[index].geo = keyEntry.Country; - this.leakTest[index].ip = keyEntry.IP; - } else { - console.error("Unexpected data structure:", data); - } - }) - .catch((error) => { - console.error("Error fetching leak test data:", error); - this.leakTest[index].geo = this.currentTexts.dnsleaktest.StatusError; - this.leakTest[index].ip = this.currentTexts.dnsleaktest.StatusError; - }); - }, - - checkAllDNSLeakTest(isRefresh) { - if (isRefresh) { - this.leakTest.forEach((server) => { - server.geo = this.currentTexts.dnsleaktest.StatusWait; - server.ip = this.currentTexts.dnsleaktest.StatusWait; - }); - } - setTimeout(() => { - this.fetchLeakTestIpApiCom(0); - }, 100); - - setTimeout(() => { - this.fetchLeakTestIpApiCom(1); - }, 1000); - - setTimeout(() => { - this.fetchLeakTestSfSharkCom(2, 0); - }, 100); - - setTimeout(() => { - this.fetchLeakTestSfSharkCom(3, 0); - }, 1000); - }, - - // 黑暗模式 - toggleDarkMode() { - this.isDarkMode = !this.isDarkMode; - this.updateBodyClass(); - this.PWAColor(); - }, - updateBodyClass() { - if (this.isDarkMode) { - document.body.classList.add("dark-mode"); - } else { - document.body.classList.remove("dark-mode"); - } - }, - checkSystemDarkMode() { - if ( - window.matchMedia && - window.matchMedia("(prefers-color-scheme: dark)").matches - ) { - this.isDarkMode = true; - this.updateBodyClass(); - } - }, - - // 手机简洁模式 - handleResize() { - this.isMobile = window.innerWidth < 768; - }, - toggleCollapse() { - this.isCardsCollapsed = !this.isCardsCollapsed; - }, - - // 更新语言 - toggleLanguage() { - this.currentLanguage = this.currentLanguage === "en" ? "cn" : "en"; - this.updateTexts(); - }, - checkBrowserLanguage() { - const browserLanguage = navigator.language || navigator.userLanguage; - if (browserLanguage.includes("zh")) { - this.currentLanguage = "cn"; - this.bingMapLanguage = "zh"; - } else { - this.currentLanguage = "en"; - this.bingMapLanguage = "en"; - } - }, - updatePageTitle(lang) { - this.$nextTick(() => { - document.title = this.currentTexts.page.title; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute('content', this.currentTexts.page.description); - } - - const metaKeywords = document.querySelector('meta[name="keywords"]'); - if (metaKeywords) { - metaKeywords.setAttribute('content', this.currentTexts.page.keywords); - } - }); - }, - - // 手动设置语言 - getLanguageFromURL() { - const urlParams = new URLSearchParams(window.location.search); - const language = urlParams.get("hl"); - if (language === "zh" || language === "cn") { - this.currentLanguage = "cn"; - this.updateTexts(); - return true; - } else if (language === "en") { - this.currentLanguage = "en"; - this.updateTexts(); - return true; - } - return false; - }, - updateTexts() { - this.currentTexts = this.currentLanguage === "en" ? textEN : textCN; - }, - // 语言补丁,弥补初始化时的翻译延迟 - langPatch() { - connectivityTests.forEach((test) => { - test.status = this.currentTexts.connectivity.StatusWait; - }); - this.stunServers.forEach((server) => { - server.ip = this.currentTexts.webrtc.StatusWait; - }); - let count = 1; - this.leakTest.forEach((server) => { - server.name = this.currentTexts.dnsleaktest.Name + " " + count; - count++; - server.geo = this.currentTexts.dnsleaktest.StatusWait; - server.ip = this.currentTexts.dnsleaktest.StatusWait; - }); - }, - - // 信息遮罩 - toggleInfoMask() { - if (this.infoMaskLevel === 0) { - this.originipDataCards = JSON.parse(JSON.stringify(this.ipDataCards)); - this.originstunServers = JSON.parse(JSON.stringify(this.stunServers)); - this.originleakTest = JSON.parse(JSON.stringify(this.leakTest)); - this.infoMask(); - this.alertStyle = "text-warning"; - this.alertMessage = this.currentTexts.alert.maskedInfoMessage_1; - this.alertTitle = this.currentTexts.alert.maskedInfoTitle_1; - this.alertToShow = true; - this.showToast(); - } else if (this.infoMaskLevel === 1) { - this.infoMask(); - this.alertStyle = "text-success"; - this.alertMessage = this.currentTexts.alert.maskedInfoMessage; - this.alertTitle = this.currentTexts.alert.maskedInfoTitle; - this.alertToShow = true; - this.showToast(); - } else { - this.infoUnmask(); - this.alertStyle = "text-danger"; - this.alertMessage = this.currentTexts.alert.unmaskedInfoMessage; - this.alertTitle = this.currentTexts.alert.unmaskedInfoTitle; - this.alertToShow = true; - this.showToast(); - } - }, - infoMask() { - if (this.infoMaskLevel === 0) { - this.ipDataCards.forEach((card) => { - card.ip = "8.8.8.8"; - }); - this.stunServers.forEach((server) => { - server.ip = "100.100.200.100"; - }); - this.leakTest.forEach((server) => { - server.ip = "12.34.56.78"; - }); - this.infoMaskLevel = 1; - } else if (this.infoMaskLevel === 1) { - this.ipDataCards.forEach((card) => { - card.country_name = "United States"; - card.country_code = "US"; - card.region = "California"; - card.city = "Mountain View"; - card.latitude = "37.40599"; - card.longitude = "-122.078514"; - card.isp = "Google LLC"; - card.asn = "AS15169"; - card.mapUrl = "res/img/defaultMap.jpg"; - card.mapUrl_dark = "res/img/defaultMap_dark.jpg"; - }); - this.leakTest.forEach((server) => { - server.geo = "United States"; - }); - this.infoMaskLevel = 2; - } - }, - infoUnmask() { - this.ipDataCards = JSON.parse(JSON.stringify(this.originipDataCards)); - this.stunServers = JSON.parse(JSON.stringify(this.originstunServers)); - this.leakTest = JSON.parse(JSON.stringify(this.originleakTest)); - this.infoMaskLevel = 0; - }, - - // Bing Map 相关 - validateBingMapKey() { - fetch('/api/validate-map-key') - .then(response => response.json()) - .then(data => { - this.isEnvBingMapKey = data.isValid; - if (!this.isEnvBingMapKey) { - this.isMapShown = false; - } else if (localStorage.getItem("isMapShown")) { - this.isMapShown = localStorage.getItem("isMapShown") === "true"; - } - }) - .catch(error => { - console.error('Error validating Bing Map Key:', error); - this.isEnvBingMapKey = false; - this.isMapShown = false; - }); - }, - - // PWA 颜色 - PWAColor() { - if (this.isDarkMode) { - document - .querySelector('meta[name="theme-color"]') - .setAttribute("content", "#171a1d"); - document - .querySelector('meta[name="background-color"]') - .setAttribute("content", "#212529"); - } else { - document - .querySelector('meta[name="theme-color"]') - .setAttribute("content", "#f8f9fa"); - document - .querySelector('meta[name="background-color"]') - .setAttribute("content", "#ffffff"); - } - }, - - // open or close modal - openModal(id) { - const modalElement = document.getElementById(id); - const modalInstance = bootstrap.Modal.getOrCreateInstance(modalElement); - if (modalInstance) { - modalInstance.show(); - } - }, - closeModal(id) { - const modalElement = document.getElementById(id); - const modalInstance = bootstrap.Modal.getInstance(modalElement); - if (modalInstance) { - modalInstance.hide(); - } - }, - - setupModalFocus() { - const modals = document.querySelectorAll(".modal"); - modals.forEach((modal) => { - modal.addEventListener("shown.bs.modal", () => { - this.$nextTick(() => { - const inputElement = modal.querySelector(".form-control"); - if (inputElement) { - inputElement.focus(); - } - }); - }); - }); - }, - // scroll to element - scrollToElement(el, offset = 0) { - const element = typeof el === "string" ? document.getElementById(el) : el; - const y = element.getBoundingClientRect().top + window.scrollY - offset; - window.scrollTo({ top: y, behavior: "smooth" }); - }, - // Logo 点击事件 - handleLogoClick() { - if (window.scrollY === 0) { - this.refreshEverything(); - } - }, - hideLoading() { - var loadingElement = document.getElementById("loading"); - if (loadingElement) { - loadingElement.classList.add("hidden"); - } - }, - - // Speed Test - updateSpeedTestResults(results) { - const summary = results.getSummary(); - - this.speedTest.downloadSpeed = parseFloat((summary.download / 1000000).toFixed(2)); - this.speedTest.uploadSpeed = parseFloat((summary.upload / 1000000).toFixed(2)); - this.speedTest.latency = parseFloat(summary.latency.toFixed(2)); - this.speedTest.jitter = parseFloat(summary.jitter.toFixed(2)); - }, - - updateSpeedTestColor(status) { - switch (status) { - case 'idle': - return 'text-secondary'; - case 'running': - return 'text-info'; - case 'finished': - return 'text-success'; - case 'error': - return 'text-danger'; - default: - return ''; - } - }, - - startSpeedTest() { - const newEngine = resetSpeedTest(); - newEngine.onRunningChange = running => { - this.speedTestStatus = "running"; - this.speedTest.downloadSpeed = 0; - this.speedTest.uploadSpeed = 0; - this.speedTest.latency = 0; - this.speedTest.jitter = 0; - }; - - newEngine.onResultsChange = ({ type }) => { - const rawData = newEngine.results.raw; - - // 进度条 - let progress = 0; - const progressPerStage = 100 / 3; // 将总进度平均分配到每个阶段 - - if (rawData.download && rawData.download.started) { - progress += rawData.download.finished ? progressPerStage : progressPerStage / 2; - } - if (rawData.upload && rawData.upload.started) { - progress += rawData.upload.finished ? progressPerStage : progressPerStage / 2; - } - if (rawData.latency && rawData.latency.started) { - progress += rawData.latency.finished ? progressPerStage : progressPerStage / 2; - } - - // 确保进度不超过100% - progress = Math.min(progress, 100); - - - // 更新进度条 - const progressBar = document.getElementById('speedtest-progress'); - progressBar.style.width = `${progress}%`; - progressBar.setAttribute('aria-valuenow', progress); - - // 更新下载速度 - if (rawData.download && rawData.download.results) { - const downloadKeys = Object.keys(rawData.download.results); - if (downloadKeys.length > 0) { - const lastDownloadKey = downloadKeys[downloadKeys.length - 1]; - const downloadTimings = rawData.download.results[lastDownloadKey].timings; - if (downloadTimings.length > 0) { - const latestDownload = downloadTimings[downloadTimings.length - 1]; - const newDownloadSpeed = parseFloat((latestDownload.bps / 1000000).toFixed(2)); - if (newDownloadSpeed > this.speedTest.downloadSpeed) { - this.speedTest.downloadSpeed = newDownloadSpeed; - } - } - } - } - // 更新上传速度 - if (rawData.upload && rawData.upload.results) { - const uploadKeys = Object.keys(rawData.upload.results); - if (uploadKeys.length > 0) { - const lastUploadKey = uploadKeys[uploadKeys.length - 1]; - const uploadTimings = rawData.upload.results[lastUploadKey].timings; - if (uploadTimings.length > 0) { - const latestUpload = uploadTimings[uploadTimings.length - 1]; - const newUploadSpeed = parseFloat((latestUpload.bps / 1000000).toFixed(2)); - if (newUploadSpeed > this.speedTest.uploadSpeed) { - this.speedTest.uploadSpeed = newUploadSpeed; - } - } - } - } - // 更新延迟 - if (rawData.latency && rawData.latency.results && rawData.latency.results.timings && rawData.latency.results.timings.length > 0) { - const latencyTimings = rawData.latency.results.timings; - const latestLatency = latencyTimings[latencyTimings.length - 1].ping; - const newLatency = parseFloat(latestLatency.toFixed(2)); - if (newLatency < this.speedTest.latency || this.speedTest.latency === 0) { - this.speedTest.latency = newLatency; - } - } - }; - - newEngine.onFinish = results => { - this.speedTestStatus = "finished"; - this.updateSpeedTestResults(results); - const scores = results.getScores(); - - // 更新 Vue 实例的数据属性 - this.speedTest.streamingScore = scores.streaming.points; - this.speedTest.gamingScore = scores.gaming.points; - this.speedTest.rtcScore = scores.rtc.points; - }; - - newEngine.onError = (e) => { - if (typeof e === 'string' && !e.includes("ICE")) { - this.speedTestStatus = "error"; - } - console.error('Speed Test Error: ', e); - }; - - triggerSpeedTest(); - }, - refreshstartSpeedTest() { - if (this.speedTestStatus !== "running") { - this.startSpeedTest(); - } - }, - // 复制 IP 地址 - copyToClipboard(ip, id) { - navigator.clipboard.writeText(ip).then(() => { - this.$set(this.copiedStatus, id, true); - // 设置定时器在 5 秒后重置状态 - setTimeout(() => { - this.$set(this.copiedStatus, id, false); - }, 5000); - }).catch(err => { - console.error('Copy error', err); - }); - }, - // 快捷键 - registerShortcutKeys() { - const shortcutConfig = [ - { - keys: "g", - action() { - window.scrollTo({ top: 0, behavior: "smooth" }); - }, - description: this.currentTexts.shortcutKeys.GoToTop, - }, - { - keys: 'j', - action: () => navigateCards('down'), - description: this.currentTexts.shortcutKeys.GoNext - }, - { - keys: 'k', - action: () => navigateCards('up'), - description: this.currentTexts.shortcutKeys.GoPrevious - }, - { - keys: "G", - action() { - window.scrollTo({ - top: document.body.scrollHeight, - behavior: "smooth", - }); - }, - description: this.currentTexts.shortcutKeys.GoToBottom, - }, - { - keys: "D", - action: this.toggleDarkMode, - description: this.currentTexts.shortcutKeys.ToggleDarkMode, - }, - { - keys: "R", - action: this.refreshEverything, - description: this.currentTexts.shortcutKeys.RefreshEverything, - }, - { - keys: "([1-6])", - type: "regex", - action: (num) => { - const card = this.ipDataCards[num - 1]; - const [el] = this.$refs[card.id]; - this.scrollToElement(el, 60); - this.refreshCard(card); - }, - description: this.currentTexts.shortcutKeys.RefreshIPCard, - }, - { - keys: "c", - action: () => { - this.scrollToElement("Connectivity", 80); - this.checkAllConnectivity(false, true); - }, - description: this.currentTexts.shortcutKeys.RefreshConnectivityTests, - }, - { - keys: "w", - action: () => { - this.scrollToElement("WebRTC", 80); - this.checkAllWebRTC(true); - }, - description: this.currentTexts.shortcutKeys.RefreshWebRTC, - }, - { - keys: "d", - action: () => { - this.scrollToElement("DNSLeakTest", 80); - this.checkAllDNSLeakTest(true); - }, - description: this.currentTexts.shortcutKeys.RefreshDNSLeakTest, - }, - { - keys: "s", - action: () => { - this.scrollToElement("SpeedTest", 80); - this.refreshstartSpeedTest(); - }, - description: this.currentTexts.shortcutKeys.StartSpeedTest, - }, - { - keys: "m", - action: () => { - if (this.isEnvBingMapKey) { - window.scrollTo({ top: 0, behavior: "smooth" }); - this.toggleMaps(); - } - }, - description: this.currentTexts.shortcutKeys.ToggleMaps, - }, - { - keys: "q", - action: () => { - this.openModal("IPCheck"); - }, - description: this.currentTexts.shortcutKeys.IPCheck, - }, - { - keys: "h", - action: () => { - this.isInfosLoaded && this.toggleInfoMask(); - }, - description: this.currentTexts.shortcutKeys.ToggleInfoMask, - }, - - // help - { - keys: "?", - action: () => { - this.openModal("helpModal"); - }, - description: this.currentTexts.shortcutKeys.Help, - }, - ]; - - shortcutConfig.forEach(config => mappingKeys(config)); - }, - // 初始化测试 - scheduleTimedTasks(tasks) { - tasks.forEach(task => { - setTimeout(() => { - task.action(); - if (task.message) { - this.displayAlert(task.message); - } - }, task.delay); - }); - }, - - initializeTimedChecks() { - const initTasks = [ - { action: () => this.initAllIPs(), delay: 0 }, - { action: () => this.checkAllIPs(), delay: 0 }, - { action: () => this.checkAllConnectivity(true, false), delay: 2000 }, - { action: () => this.checkAllWebRTC(false), delay: 4000 }, - { action: () => this.checkAllDNSLeakTest(false), delay: 2500 }, - { action: () => { this.isInfosLoaded = true; }, delay: 6000 }, - { action: () => this.getAllIPs(), delay: 6000 } - ]; - this.scheduleTimedTasks(initTasks); - }, - - // 通过 logo 点击重新测试 - refreshEverything() { - const refreshTasks = [ - { action: () => this.initAllIPs(), delay: 0 }, - { action: () => this.checkAllIPs(), delay: 0 }, - { action: () => this.checkAllConnectivity(false, true), delay: 2000 }, - { action: () => this.checkAllWebRTC(true), delay: 4000 }, - { action: () => this.checkAllDNSLeakTest(true), delay: 2500 }, - { action: () => this.refreshingAlert(), delay: 500 }, - { action: () => this.getAllIPs(), delay: 6000 } - ]; - this.scheduleTimedTasks(refreshTasks); - this.infoMaskLevel = 0; - }, - - refreshingAlert() { - this.alertStyle = "text-success"; - this.alertMessage = this.currentTexts.alert.refreshEverythingMessage; - this.alertTitle = this.currentTexts.alert.refreshEverythingTitle; - this.alertToShow = true; - this.showToast(); - }, - - // 获取所有的 card 的 IP 地址 - getAllIPs() { - this.ipDataCards.forEach(card => { - if (card.ip && !card.ip.includes(' ') && !card.ip.includes(':')) { - this.allIPs.push(card.ip); - } - }); - this.allIPs = [...new Set(this.allIPs)]; - }, - // 初始化 AllIPs 数组 - initAllIPs() { - this.allIPs[0] = this.currentTexts.pingtest.SelectIP; - this.selectedIP = this.allIPs[0]; - }, - startPingCheck() { - // 清空上一次结果 - this.pingResults = []; - let tryCount = 0; - // 子函数:发起 ping 请求 - const sendPingRequest = async () => { - this.pingCheckStatus = "running"; - try { - const response = await fetch("https://api.globalping.io/v1/measurements", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - limit: 12, - locations: [ - { country: "HK" }, - { country: "TW" }, - { country: "US" }, - { country: "CA" }, - { country: "JP" }, - { country: "SG" }, - { country: "AU" }, - { country: "GB" }, - { country: "DE" }, - { country: "FR" }, - { country: "BR" }, - { country: "IN" }, - ], - target: this.selectedIP, // 使用用户选中的 IP 地址 - type: "ping", - measurementOptions: { - packets: 8 - } - }) - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return await response.json(); - } catch (error) { - console.error("Error sending ping request:", error); - } - }; - - // 子函数:获取 ping 结果 - const fetchPingResults = async (id) => { - try { - const response = await fetch(`https://api.globalping.io/v1/measurements/${id}`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - this.processPingResults(data); - - if (data.status === "in-progress" && tryCount < 4) { - setTimeout(() => fetchPingResults(id), 1000); - tryCount++; - } else { - // 如果 this.pingResults 是空数组,返回错误信息 - if (this.pingResults.length === 0) { - this.pingCheckStatus = "error"; - } else { - this.pingCheckStatus = "finished"; - } - } - } catch (error) { - console.error("Error fetching ping results:", error); - } - }; - - // 执行流程 - sendPingRequest().then(data => { - if (data && data.id) { - setTimeout(() => { - fetchPingResults(data.id); - }, 1000); - } - }); - }, - processPingResults(data) { - const cleanedData = data.results - .filter(item => item.result.status === "finished") - .filter(item => item.result.stats.min !== null) - .map(item => ({ - country: item.probe.country, - stats: item.result.stats - })); - - this.pingResults = cleanedData; - }, - - }, - - created() { - this.hideLoading(); - this.checkSystemDarkMode(); - this.PWAColor(); - const isLanguageSet = this.getLanguageFromURL(); - if (!isLanguageSet) { - this.checkBrowserLanguage(); - } - this.updateTexts(); - this.langPatch(); - this.validateBingMapKey(); - this.isMobile = window.innerWidth < 768; - window.addEventListener("resize", this.handleResize); - }, - - destroyed() { - window.removeEventListener("resize", this.handleResize); - }, - - watch: { - isMapShown(newVal) { - localStorage.setItem("isMapShown", JSON.stringify(newVal)); - }, - isCardsCollapsed(newVal) { - localStorage.setItem('isCardsCollapsed', JSON.stringify(newVal)); - }, - }, - - mounted() { - this.updatePageTitle(this.currentLanguage); - this.initializeTimedChecks(); - this.setupModalFocus(); - this.registerShortcutKeys(); - this.keyMap = keyMap; - this.setupModalEventListener(); - }, - -}); diff --git a/public/res/cfSpeedTest.js b/public/res/cfSpeedTest.js deleted file mode 100644 index 1dc13028..00000000 --- a/public/res/cfSpeedTest.js +++ /dev/null @@ -1,42 +0,0 @@ - -import SpeedTest from 'https://cdn.skypack.dev/@cloudflare/speedtest'; - -let engine = new SpeedTest({ - autoStart: false, -}); - -// Event listeners -engine.onRunningChange = running => { - console.log('Speed Test is ' + (running ? 'running...' : 'finished!')); -}; - -engine.onResultsChange = ({ type }) => { - console.log('Results changed: ', type); -}; - - -engine.onFinish = results => { - console.log('Speed Test finished: ', results.getSummary()); -}; - -engine.onError = (e) => { - console.log('Speed Test Error: ', e); -}; - -const triggerSpeedTest = () => { - engine.play(); -}; - -const resetSpeedTest = () => { - engine = new SpeedTest({ - autoStart: false, - measurements: [ - { type: 'latency', numPackets: 20 }, - { type: 'download', bytes: 5e7, count: 4 }, - { type: 'upload', bytes: 1.5e7, count: 3 } - ] - }); - return engine; -}; - -export { triggerSpeedTest, resetSpeedTest, engine }; diff --git a/public/res/css/style.css b/public/res/css/style.css deleted file mode 100644 index dd0ae2b2..00000000 --- a/public/res/css/style.css +++ /dev/null @@ -1,265 +0,0 @@ -.jn-title { - display: flex; - justify-content: space-between; - align-items: flex-start; - flex-direction: row; - margin-top: 10pt; - margin-bottom: 10pt; -} - -.jn-title2 { - display: flex; - justify-content: space-between; - align-items: flex-end; - flex-direction: row; - margin-top: 10pt; - margin-bottom: 10pt; -} - -.jn-ip-title { - display: flex; - justify-content: space-between; - align-items: center; - flex-direction: row; - border: 0; - background-color:unset; -} - -.jn-alert { - z-index: 9999; - position: fixed; - width: 100%; - bottom: 0; -} - -.jn-card { - box-shadow: 0 0 10pt #0000001c; - height: 100%; - border: 1px solid #9c9c9c2e; -} - -.jn-card:hover { - border-color: #4dc0e461; -} - -.jn-card.keyboard-hover { - border-color: rgba(20, 184, 108, 0.42); -} - -.jn-card.keyboard-hover::after { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - border: 1px solid rgba(20, 184, 108, 0.42); - height: 100%; - pointer-events: none; - border-radius: 4pt; -} - -.jn-container { - padding-top: 2pt; - max-width: 1400px; -} - -.jn-text { - font-weight: bold; -} - -.jn-list-group-item { - padding: 0.75rem 0.25rem; -} - -.jn-opacity { - opacity: 0.5; - cursor: not-allowed; -} - -.jn-map-image { - width: 100%; - height: 200px; - object-fit: cover; - object-position: center; - border-radius: 0; -} - -.jn-navbar-top { - position: fixed; - top: 0; - width: 100%; - z-index: 9999; - border-bottom: 1px solid #0000001c; - -} - -.jn-toast { - z-index: 9999; - right: 0; - bottom: 0; - margin-bottom: 2pt; - margin-right: 40pt; - max-width: 80vw; -} - -footer { - height: 100pt; - padding-top: 20pt; -} - -@media only screen and (max-width: 768px) { - footer { - height: 140pt; - } - - .speedtest-h5 { - /* font-size: 12px; */ - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} - -.jn-radio { - display: flex; - justify-content: flex-end; - align-items: stretch; - flex-direction: row; -} - -.jn-logo { - display: flex; - justify-content: flex-start; - align-items: center; - flex-direction: row; -} - -.dark-mode { - background-color: #212529 !important; - color: #e3e3e3 !important; - border-color: #757575; -} - -.dark-mode::placeholder { - color: #e3e3e38e !important; -} - -.dark-mode-border { - border-color: #a8a8a863; - box-shadow: 0 0 10pt #00000038; -} - -.dark-mode-title { - background-color: #171a1d; -} - -.dark-mode-nav { - background-color: #171a1d !important; - border-color: #a8a8a863 !important; - border-color: #757575; -} - -.jn-deactive { - opacity: 0.5; -} - -.active { - opacity: 1 !important; -} - -.dark-mode-nav-border { - border-color: #a8a8a863 !important; -} - -.dark-mode-close-button { - background-color: white; -} - -.dark-mode-refresh { - background-color: #141618; -} - -.dark-mode-refresh:hover { - background-color: #00000054; -} - -.mobile-list { - border: 0; -} - -.mobile-h2 { - font-size: 1.25rem; -} - -[v-cloak] { - display: none; -} - -.hidden { - display: none !important; -} - -.jn-loading { - margin-top: 50vh; -} - -.jn-shortcut-row { - margin: 2pt 0 2pt 0; - border-bottom: 1px solid; -} - -.jn-dark-mode-help-border { - border-color: #a8a8a863 !important; -} - -.jn-border-bottom-none { - border-bottom: none !important; -} - -.jn-help-note { - background: linear-gradient(to right, #e04343, orange, #8d8d22, green, #50c0ae, indigo, violet); - -webkit-background-clip: text; - background-clip: text; - color: transparent; -} - -.jn-progress { - padding-bottom: 20pt; -} - -.jn-opacity-0 { - opacity: 0; -} - -.jn-progress-dark { - background-color: #4b4b4b !important; -} - -*:focus { - outline: none; -} - -.jn-speedtest-number { - font-size: 2em; -} - -.jn-speedtest-note { - background-color: #b2d6c33b; - border: 1px solid #1a8754; - margin: 0; - border-radius: 6pt; -} -/* .jn-speedtest-note::before { - content: ""; - border-top: 1px solid #19945a5c; - width: 50%; - margin: 0 auto; -} */ - -.speedtest-p { - margin-top:10pt; -} - -.jn-ping-form-select { - min-width: 140pt; -} \ No newline at end of file diff --git a/public/res/ga.js b/public/res/ga.js deleted file mode 100644 index d5814e5a..00000000 --- a/public/res/ga.js +++ /dev/null @@ -1,5 +0,0 @@ -const config = { - GOOGLE_ANALYTICS_ID: 'G-TEYKKD81TL' -}; - -export default config; \ No newline at end of file diff --git a/public/res/vue.js b/public/res/vue.js deleted file mode 100644 index 05e21102..00000000 --- a/public/res/vue.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Vue.js v2.6.11 - * (c) 2014-2019 Evan You - * Released under the MIT License. - */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Vue=t()}(this,function(){"use strict";var e=Object.freeze({});function t(e){return null==e}function n(e){return null!=e}function r(e){return!0===e}function i(e){return"string"==typeof e||"number"==typeof e||"symbol"==typeof e||"boolean"==typeof e}function o(e){return null!==e&&"object"==typeof e}var a=Object.prototype.toString;function s(e){return"[object Object]"===a.call(e)}function c(e){var t=parseFloat(String(e));return t>=0&&Math.floor(t)===t&&isFinite(e)}function u(e){return n(e)&&"function"==typeof e.then&&"function"==typeof e.catch}function l(e){return null==e?"":Array.isArray(e)||s(e)&&e.toString===a?JSON.stringify(e,null,2):String(e)}function f(e){var t=parseFloat(e);return isNaN(t)?e:t}function p(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i-1)return e.splice(n,1)}}var m=Object.prototype.hasOwnProperty;function y(e,t){return m.call(e,t)}function g(e){var t=Object.create(null);return function(n){return t[n]||(t[n]=e(n))}}var _=/-(\w)/g,b=g(function(e){return e.replace(_,function(e,t){return t?t.toUpperCase():""})}),$=g(function(e){return e.charAt(0).toUpperCase()+e.slice(1)}),w=/\B([A-Z])/g,C=g(function(e){return e.replace(w,"-$1").toLowerCase()});var x=Function.prototype.bind?function(e,t){return e.bind(t)}:function(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n};function k(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function A(e,t){for(var n in t)e[n]=t[n];return e}function O(e){for(var t={},n=0;n0,Z=J&&J.indexOf("edge/")>0,G=(J&&J.indexOf("android"),J&&/iphone|ipad|ipod|ios/.test(J)||"ios"===K),X=(J&&/chrome\/\d+/.test(J),J&&/phantomjs/.test(J),J&&J.match(/firefox\/(\d+)/)),Y={}.watch,Q=!1;if(z)try{var ee={};Object.defineProperty(ee,"passive",{get:function(){Q=!0}}),window.addEventListener("test-passive",null,ee)}catch(e){}var te=function(){return void 0===B&&(B=!z&&!V&&"undefined"!=typeof global&&(global.process&&"server"===global.process.env.VUE_ENV)),B},ne=z&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function re(e){return"function"==typeof e&&/native code/.test(e.toString())}var ie,oe="undefined"!=typeof Symbol&&re(Symbol)&&"undefined"!=typeof Reflect&&re(Reflect.ownKeys);ie="undefined"!=typeof Set&&re(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return!0===this.set[e]},e.prototype.add=function(e){this.set[e]=!0},e.prototype.clear=function(){this.set=Object.create(null)},e}();var ae=S,se=0,ce=function(){this.id=se++,this.subs=[]};ce.prototype.addSub=function(e){this.subs.push(e)},ce.prototype.removeSub=function(e){h(this.subs,e)},ce.prototype.depend=function(){ce.target&&ce.target.addDep(this)},ce.prototype.notify=function(){for(var e=this.subs.slice(),t=0,n=e.length;t-1)if(o&&!y(i,"default"))a=!1;else if(""===a||a===C(e)){var c=Pe(String,i.type);(c<0||s0&&(st((u=e(u,(a||"")+"_"+c))[0])&&st(f)&&(s[l]=he(f.text+u[0].text),u.shift()),s.push.apply(s,u)):i(u)?st(f)?s[l]=he(f.text+u):""!==u&&s.push(he(u)):st(u)&&st(f)?s[l]=he(f.text+u.text):(r(o._isVList)&&n(u.tag)&&t(u.key)&&n(a)&&(u.key="__vlist"+a+"_"+c+"__"),s.push(u)));return s}(e):void 0}function st(e){return n(e)&&n(e.text)&&!1===e.isComment}function ct(e,t){if(e){for(var n=Object.create(null),r=oe?Reflect.ownKeys(e):Object.keys(e),i=0;i0,a=t?!!t.$stable:!o,s=t&&t.$key;if(t){if(t._normalized)return t._normalized;if(a&&r&&r!==e&&s===r.$key&&!o&&!r.$hasNormal)return r;for(var c in i={},t)t[c]&&"$"!==c[0]&&(i[c]=pt(n,c,t[c]))}else i={};for(var u in n)u in i||(i[u]=dt(n,u));return t&&Object.isExtensible(t)&&(t._normalized=i),R(i,"$stable",a),R(i,"$key",s),R(i,"$hasNormal",o),i}function pt(e,t,n){var r=function(){var e=arguments.length?n.apply(null,arguments):n({});return(e=e&&"object"==typeof e&&!Array.isArray(e)?[e]:at(e))&&(0===e.length||1===e.length&&e[0].isComment)?void 0:e};return n.proxy&&Object.defineProperty(e,t,{get:r,enumerable:!0,configurable:!0}),r}function dt(e,t){return function(){return e[t]}}function vt(e,t){var r,i,a,s,c;if(Array.isArray(e)||"string"==typeof e)for(r=new Array(e.length),i=0,a=e.length;idocument.createEvent("Event").timeStamp&&(sn=function(){return cn.now()})}function un(){var e,t;for(an=sn(),rn=!0,Qt.sort(function(e,t){return e.id-t.id}),on=0;onon&&Qt[n].id>e.id;)n--;Qt.splice(n+1,0,e)}else Qt.push(e);nn||(nn=!0,Ye(un))}}(this)},fn.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||o(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(e){Re(e,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,e,t)}}},fn.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},fn.prototype.depend=function(){for(var e=this.deps.length;e--;)this.deps[e].depend()},fn.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||h(this.vm._watchers,this);for(var e=this.deps.length;e--;)this.deps[e].removeSub(this);this.active=!1}};var pn={enumerable:!0,configurable:!0,get:S,set:S};function dn(e,t,n){pn.get=function(){return this[t][n]},pn.set=function(e){this[t][n]=e},Object.defineProperty(e,n,pn)}function vn(e){e._watchers=[];var t=e.$options;t.props&&function(e,t){var n=e.$options.propsData||{},r=e._props={},i=e.$options._propKeys=[];e.$parent&&$e(!1);var o=function(o){i.push(o);var a=Me(o,t,n,e);xe(r,o,a),o in e||dn(e,"_props",o)};for(var a in t)o(a);$e(!0)}(e,t.props),t.methods&&function(e,t){e.$options.props;for(var n in t)e[n]="function"!=typeof t[n]?S:x(t[n],e)}(e,t.methods),t.data?function(e){var t=e.$options.data;s(t=e._data="function"==typeof t?function(e,t){le();try{return e.call(t,t)}catch(e){return Re(e,t,"data()"),{}}finally{fe()}}(t,e):t||{})||(t={});var n=Object.keys(t),r=e.$options.props,i=(e.$options.methods,n.length);for(;i--;){var o=n[i];r&&y(r,o)||(a=void 0,36!==(a=(o+"").charCodeAt(0))&&95!==a&&dn(e,"_data",o))}var a;Ce(t,!0)}(e):Ce(e._data={},!0),t.computed&&function(e,t){var n=e._computedWatchers=Object.create(null),r=te();for(var i in t){var o=t[i],a="function"==typeof o?o:o.get;r||(n[i]=new fn(e,a||S,S,hn)),i in e||mn(e,i,o)}}(e,t.computed),t.watch&&t.watch!==Y&&function(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;i-1:"string"==typeof e?e.split(",").indexOf(t)>-1:(n=e,"[object RegExp]"===a.call(n)&&e.test(t));var n}function An(e,t){var n=e.cache,r=e.keys,i=e._vnode;for(var o in n){var a=n[o];if(a){var s=xn(a.componentOptions);s&&!t(s)&&On(n,o,r,i)}}}function On(e,t,n,r){var i=e[t];!i||r&&i.tag===r.tag||i.componentInstance.$destroy(),e[t]=null,h(n,t)}!function(t){t.prototype._init=function(t){var n=this;n._uid=bn++,n._isVue=!0,t&&t._isComponent?function(e,t){var n=e.$options=Object.create(e.constructor.options),r=t._parentVnode;n.parent=t.parent,n._parentVnode=r;var i=r.componentOptions;n.propsData=i.propsData,n._parentListeners=i.listeners,n._renderChildren=i.children,n._componentTag=i.tag,t.render&&(n.render=t.render,n.staticRenderFns=t.staticRenderFns)}(n,t):n.$options=De($n(n.constructor),t||{},n),n._renderProxy=n,n._self=n,function(e){var t=e.$options,n=t.parent;if(n&&!t.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(e)}e.$parent=n,e.$root=n?n.$root:e,e.$children=[],e.$refs={},e._watcher=null,e._inactive=null,e._directInactive=!1,e._isMounted=!1,e._isDestroyed=!1,e._isBeingDestroyed=!1}(n),function(e){e._events=Object.create(null),e._hasHookEvent=!1;var t=e.$options._parentListeners;t&&qt(e,t)}(n),function(t){t._vnode=null,t._staticTrees=null;var n=t.$options,r=t.$vnode=n._parentVnode,i=r&&r.context;t.$slots=ut(n._renderChildren,i),t.$scopedSlots=e,t._c=function(e,n,r,i){return Pt(t,e,n,r,i,!1)},t.$createElement=function(e,n,r,i){return Pt(t,e,n,r,i,!0)};var o=r&&r.data;xe(t,"$attrs",o&&o.attrs||e,null,!0),xe(t,"$listeners",n._parentListeners||e,null,!0)}(n),Yt(n,"beforeCreate"),function(e){var t=ct(e.$options.inject,e);t&&($e(!1),Object.keys(t).forEach(function(n){xe(e,n,t[n])}),$e(!0))}(n),vn(n),function(e){var t=e.$options.provide;t&&(e._provided="function"==typeof t?t.call(e):t)}(n),Yt(n,"created"),n.$options.el&&n.$mount(n.$options.el)}}(wn),function(e){var t={get:function(){return this._data}},n={get:function(){return this._props}};Object.defineProperty(e.prototype,"$data",t),Object.defineProperty(e.prototype,"$props",n),e.prototype.$set=ke,e.prototype.$delete=Ae,e.prototype.$watch=function(e,t,n){if(s(t))return _n(this,e,t,n);(n=n||{}).user=!0;var r=new fn(this,e,t,n);if(n.immediate)try{t.call(this,r.value)}catch(e){Re(e,this,'callback for immediate watcher "'+r.expression+'"')}return function(){r.teardown()}}}(wn),function(e){var t=/^hook:/;e.prototype.$on=function(e,n){var r=this;if(Array.isArray(e))for(var i=0,o=e.length;i1?k(t):t;for(var n=k(arguments,1),r='event handler for "'+e+'"',i=0,o=t.length;iparseInt(this.max)&&On(a,s[0],s,this._vnode)),t.data.keepAlive=!0}return t||e&&e[0]}}};!function(e){var t={get:function(){return F}};Object.defineProperty(e,"config",t),e.util={warn:ae,extend:A,mergeOptions:De,defineReactive:xe},e.set=ke,e.delete=Ae,e.nextTick=Ye,e.observable=function(e){return Ce(e),e},e.options=Object.create(null),M.forEach(function(t){e.options[t+"s"]=Object.create(null)}),e.options._base=e,A(e.options.components,Tn),function(e){e.use=function(e){var t=this._installedPlugins||(this._installedPlugins=[]);if(t.indexOf(e)>-1)return this;var n=k(arguments,1);return n.unshift(this),"function"==typeof e.install?e.install.apply(e,n):"function"==typeof e&&e.apply(null,n),t.push(e),this}}(e),function(e){e.mixin=function(e){return this.options=De(this.options,e),this}}(e),Cn(e),function(e){M.forEach(function(t){e[t]=function(e,n){return n?("component"===t&&s(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"==typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}})}(e)}(wn),Object.defineProperty(wn.prototype,"$isServer",{get:te}),Object.defineProperty(wn.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(wn,"FunctionalRenderContext",{value:Tt}),wn.version="2.6.11";var En=p("style,class"),Nn=p("input,textarea,option,select,progress"),jn=function(e,t,n){return"value"===n&&Nn(e)&&"button"!==t||"selected"===n&&"option"===e||"checked"===n&&"input"===e||"muted"===n&&"video"===e},Dn=p("contenteditable,draggable,spellcheck"),Ln=p("events,caret,typing,plaintext-only"),Mn=function(e,t){return Hn(t)||"false"===t?"false":"contenteditable"===e&&Ln(t)?t:"true"},In=p("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),Fn="http://www.w3.org/1999/xlink",Pn=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},Rn=function(e){return Pn(e)?e.slice(6,e.length):""},Hn=function(e){return null==e||!1===e};function Bn(e){for(var t=e.data,r=e,i=e;n(i.componentInstance);)(i=i.componentInstance._vnode)&&i.data&&(t=Un(i.data,t));for(;n(r=r.parent);)r&&r.data&&(t=Un(t,r.data));return function(e,t){if(n(e)||n(t))return zn(e,Vn(t));return""}(t.staticClass,t.class)}function Un(e,t){return{staticClass:zn(e.staticClass,t.staticClass),class:n(e.class)?[e.class,t.class]:t.class}}function zn(e,t){return e?t?e+" "+t:e:t||""}function Vn(e){return Array.isArray(e)?function(e){for(var t,r="",i=0,o=e.length;i-1?hr(e,t,n):In(t)?Hn(n)?e.removeAttribute(t):(n="allowfullscreen"===t&&"EMBED"===e.tagName?"true":t,e.setAttribute(t,n)):Dn(t)?e.setAttribute(t,Mn(t,n)):Pn(t)?Hn(n)?e.removeAttributeNS(Fn,Rn(t)):e.setAttributeNS(Fn,t,n):hr(e,t,n)}function hr(e,t,n){if(Hn(n))e.removeAttribute(t);else{if(q&&!W&&"TEXTAREA"===e.tagName&&"placeholder"===t&&""!==n&&!e.__ieph){var r=function(t){t.stopImmediatePropagation(),e.removeEventListener("input",r)};e.addEventListener("input",r),e.__ieph=!0}e.setAttribute(t,n)}}var mr={create:dr,update:dr};function yr(e,r){var i=r.elm,o=r.data,a=e.data;if(!(t(o.staticClass)&&t(o.class)&&(t(a)||t(a.staticClass)&&t(a.class)))){var s=Bn(r),c=i._transitionClasses;n(c)&&(s=zn(s,Vn(c))),s!==i._prevClass&&(i.setAttribute("class",s),i._prevClass=s)}}var gr,_r,br,$r,wr,Cr,xr={create:yr,update:yr},kr=/[\w).+\-_$\]]/;function Ar(e){var t,n,r,i,o,a=!1,s=!1,c=!1,u=!1,l=0,f=0,p=0,d=0;for(r=0;r=0&&" "===(h=e.charAt(v));v--);h&&kr.test(h)||(u=!0)}}else void 0===i?(d=r+1,i=e.slice(0,r).trim()):m();function m(){(o||(o=[])).push(e.slice(d,r).trim()),d=r+1}if(void 0===i?i=e.slice(0,r).trim():0!==d&&m(),o)for(r=0;r-1?{exp:e.slice(0,$r),key:'"'+e.slice($r+1)+'"'}:{exp:e,key:null};_r=e,$r=wr=Cr=0;for(;!zr();)Vr(br=Ur())?Jr(br):91===br&&Kr(br);return{exp:e.slice(0,wr),key:e.slice(wr+1,Cr)}}(e);return null===n.key?e+"="+t:"$set("+n.exp+", "+n.key+", "+t+")"}function Ur(){return _r.charCodeAt(++$r)}function zr(){return $r>=gr}function Vr(e){return 34===e||39===e}function Kr(e){var t=1;for(wr=$r;!zr();)if(Vr(e=Ur()))Jr(e);else if(91===e&&t++,93===e&&t--,0===t){Cr=$r;break}}function Jr(e){for(var t=e;!zr()&&(e=Ur())!==t;);}var qr,Wr="__r",Zr="__c";function Gr(e,t,n){var r=qr;return function i(){null!==t.apply(null,arguments)&&Qr(e,i,n,r)}}var Xr=Ve&&!(X&&Number(X[1])<=53);function Yr(e,t,n,r){if(Xr){var i=an,o=t;t=o._wrapper=function(e){if(e.target===e.currentTarget||e.timeStamp>=i||e.timeStamp<=0||e.target.ownerDocument!==document)return o.apply(this,arguments)}}qr.addEventListener(e,t,Q?{capture:n,passive:r}:n)}function Qr(e,t,n,r){(r||qr).removeEventListener(e,t._wrapper||t,n)}function ei(e,r){if(!t(e.data.on)||!t(r.data.on)){var i=r.data.on||{},o=e.data.on||{};qr=r.elm,function(e){if(n(e[Wr])){var t=q?"change":"input";e[t]=[].concat(e[Wr],e[t]||[]),delete e[Wr]}n(e[Zr])&&(e.change=[].concat(e[Zr],e.change||[]),delete e[Zr])}(i),rt(i,o,Yr,Qr,Gr,r.context),qr=void 0}}var ti,ni={create:ei,update:ei};function ri(e,r){if(!t(e.data.domProps)||!t(r.data.domProps)){var i,o,a=r.elm,s=e.data.domProps||{},c=r.data.domProps||{};for(i in n(c.__ob__)&&(c=r.data.domProps=A({},c)),s)i in c||(a[i]="");for(i in c){if(o=c[i],"textContent"===i||"innerHTML"===i){if(r.children&&(r.children.length=0),o===s[i])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===i&&"PROGRESS"!==a.tagName){a._value=o;var u=t(o)?"":String(o);ii(a,u)&&(a.value=u)}else if("innerHTML"===i&&qn(a.tagName)&&t(a.innerHTML)){(ti=ti||document.createElement("div")).innerHTML=""+o+"";for(var l=ti.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;l.firstChild;)a.appendChild(l.firstChild)}else if(o!==s[i])try{a[i]=o}catch(e){}}}}function ii(e,t){return!e.composing&&("OPTION"===e.tagName||function(e,t){var n=!0;try{n=document.activeElement!==e}catch(e){}return n&&e.value!==t}(e,t)||function(e,t){var r=e.value,i=e._vModifiers;if(n(i)){if(i.number)return f(r)!==f(t);if(i.trim)return r.trim()!==t.trim()}return r!==t}(e,t))}var oi={create:ri,update:ri},ai=g(function(e){var t={},n=/:(.+)/;return e.split(/;(?![^(]*\))/g).forEach(function(e){if(e){var r=e.split(n);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t});function si(e){var t=ci(e.style);return e.staticStyle?A(e.staticStyle,t):t}function ci(e){return Array.isArray(e)?O(e):"string"==typeof e?ai(e):e}var ui,li=/^--/,fi=/\s*!important$/,pi=function(e,t,n){if(li.test(t))e.style.setProperty(t,n);else if(fi.test(n))e.style.setProperty(C(t),n.replace(fi,""),"important");else{var r=vi(t);if(Array.isArray(n))for(var i=0,o=n.length;i-1?t.split(yi).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function _i(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(yi).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t),e.classList.length||e.removeAttribute("class");else{for(var n=" "+(e.getAttribute("class")||"")+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");(n=n.trim())?e.setAttribute("class",n):e.removeAttribute("class")}}function bi(e){if(e){if("object"==typeof e){var t={};return!1!==e.css&&A(t,$i(e.name||"v")),A(t,e),t}return"string"==typeof e?$i(e):void 0}}var $i=g(function(e){return{enterClass:e+"-enter",enterToClass:e+"-enter-to",enterActiveClass:e+"-enter-active",leaveClass:e+"-leave",leaveToClass:e+"-leave-to",leaveActiveClass:e+"-leave-active"}}),wi=z&&!W,Ci="transition",xi="animation",ki="transition",Ai="transitionend",Oi="animation",Si="animationend";wi&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(ki="WebkitTransition",Ai="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Oi="WebkitAnimation",Si="webkitAnimationEnd"));var Ti=z?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(e){return e()};function Ei(e){Ti(function(){Ti(e)})}function Ni(e,t){var n=e._transitionClasses||(e._transitionClasses=[]);n.indexOf(t)<0&&(n.push(t),gi(e,t))}function ji(e,t){e._transitionClasses&&h(e._transitionClasses,t),_i(e,t)}function Di(e,t,n){var r=Mi(e,t),i=r.type,o=r.timeout,a=r.propCount;if(!i)return n();var s=i===Ci?Ai:Si,c=0,u=function(){e.removeEventListener(s,l),n()},l=function(t){t.target===e&&++c>=a&&u()};setTimeout(function(){c0&&(n=Ci,l=a,f=o.length):t===xi?u>0&&(n=xi,l=u,f=c.length):f=(n=(l=Math.max(a,u))>0?a>u?Ci:xi:null)?n===Ci?o.length:c.length:0,{type:n,timeout:l,propCount:f,hasTransform:n===Ci&&Li.test(r[ki+"Property"])}}function Ii(e,t){for(;e.length1}function Ui(e,t){!0!==t.data.show&&Pi(t)}var zi=function(e){var o,a,s={},c=e.modules,u=e.nodeOps;for(o=0;ov?_(e,t(i[y+1])?null:i[y+1].elm,i,d,y,o):d>y&&$(r,p,v)}(p,h,y,o,l):n(y)?(n(e.text)&&u.setTextContent(p,""),_(p,null,y,0,y.length-1,o)):n(h)?$(h,0,h.length-1):n(e.text)&&u.setTextContent(p,""):e.text!==i.text&&u.setTextContent(p,i.text),n(v)&&n(d=v.hook)&&n(d=d.postpatch)&&d(e,i)}}}function k(e,t,i){if(r(i)&&n(e.parent))e.parent.data.pendingInsert=t;else for(var o=0;o-1,a.selected!==o&&(a.selected=o);else if(N(Wi(a),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));i||(e.selectedIndex=-1)}}function qi(e,t){return t.every(function(t){return!N(t,e)})}function Wi(e){return"_value"in e?e._value:e.value}function Zi(e){e.target.composing=!0}function Gi(e){e.target.composing&&(e.target.composing=!1,Xi(e.target,"input"))}function Xi(e,t){var n=document.createEvent("HTMLEvents");n.initEvent(t,!0,!0),e.dispatchEvent(n)}function Yi(e){return!e.componentInstance||e.data&&e.data.transition?e:Yi(e.componentInstance._vnode)}var Qi={model:Vi,show:{bind:function(e,t,n){var r=t.value,i=(n=Yi(n)).data&&n.data.transition,o=e.__vOriginalDisplay="none"===e.style.display?"":e.style.display;r&&i?(n.data.show=!0,Pi(n,function(){e.style.display=o})):e.style.display=r?o:"none"},update:function(e,t,n){var r=t.value;!r!=!t.oldValue&&((n=Yi(n)).data&&n.data.transition?(n.data.show=!0,r?Pi(n,function(){e.style.display=e.__vOriginalDisplay}):Ri(n,function(){e.style.display="none"})):e.style.display=r?e.__vOriginalDisplay:"none")},unbind:function(e,t,n,r,i){i||(e.style.display=e.__vOriginalDisplay)}}},eo={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function to(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?to(zt(t.children)):e}function no(e){var t={},n=e.$options;for(var r in n.propsData)t[r]=e[r];var i=n._parentListeners;for(var o in i)t[b(o)]=i[o];return t}function ro(e,t){if(/\d-keep-alive$/.test(t.tag))return e("keep-alive",{props:t.componentOptions.propsData})}var io=function(e){return e.tag||Ut(e)},oo=function(e){return"show"===e.name},ao={name:"transition",props:eo,abstract:!0,render:function(e){var t=this,n=this.$slots.default;if(n&&(n=n.filter(io)).length){var r=this.mode,o=n[0];if(function(e){for(;e=e.parent;)if(e.data.transition)return!0}(this.$vnode))return o;var a=to(o);if(!a)return o;if(this._leaving)return ro(e,o);var s="__transition-"+this._uid+"-";a.key=null==a.key?a.isComment?s+"comment":s+a.tag:i(a.key)?0===String(a.key).indexOf(s)?a.key:s+a.key:a.key;var c=(a.data||(a.data={})).transition=no(this),u=this._vnode,l=to(u);if(a.data.directives&&a.data.directives.some(oo)&&(a.data.show=!0),l&&l.data&&!function(e,t){return t.key===e.key&&t.tag===e.tag}(a,l)&&!Ut(l)&&(!l.componentInstance||!l.componentInstance._vnode.isComment)){var f=l.data.transition=A({},c);if("out-in"===r)return this._leaving=!0,it(f,"afterLeave",function(){t._leaving=!1,t.$forceUpdate()}),ro(e,o);if("in-out"===r){if(Ut(a))return u;var p,d=function(){p()};it(c,"afterEnter",d),it(c,"enterCancelled",d),it(f,"delayLeave",function(e){p=e})}}return o}}},so=A({tag:String,moveClass:String},eo);function co(e){e.elm._moveCb&&e.elm._moveCb(),e.elm._enterCb&&e.elm._enterCb()}function uo(e){e.data.newPos=e.elm.getBoundingClientRect()}function lo(e){var t=e.data.pos,n=e.data.newPos,r=t.left-n.left,i=t.top-n.top;if(r||i){e.data.moved=!0;var o=e.elm.style;o.transform=o.WebkitTransform="translate("+r+"px,"+i+"px)",o.transitionDuration="0s"}}delete so.mode;var fo={Transition:ao,TransitionGroup:{props:so,beforeMount:function(){var e=this,t=this._update;this._update=function(n,r){var i=Zt(e);e.__patch__(e._vnode,e.kept,!1,!0),e._vnode=e.kept,i(),t.call(e,n,r)}},render:function(e){for(var t=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,i=this.$slots.default||[],o=this.children=[],a=no(this),s=0;s-1?Gn[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:Gn[e]=/HTMLUnknownElement/.test(t.toString())},A(wn.options.directives,Qi),A(wn.options.components,fo),wn.prototype.__patch__=z?zi:S,wn.prototype.$mount=function(e,t){return function(e,t,n){var r;return e.$el=t,e.$options.render||(e.$options.render=ve),Yt(e,"beforeMount"),r=function(){e._update(e._render(),n)},new fn(e,r,S,{before:function(){e._isMounted&&!e._isDestroyed&&Yt(e,"beforeUpdate")}},!0),n=!1,null==e.$vnode&&(e._isMounted=!0,Yt(e,"mounted")),e}(this,e=e&&z?Yn(e):void 0,t)},z&&setTimeout(function(){F.devtools&&ne&&ne.emit("init",wn)},0);var po=/\{\{((?:.|\r?\n)+?)\}\}/g,vo=/[-.*+?^${}()|[\]\/\\]/g,ho=g(function(e){var t=e[0].replace(vo,"\\$&"),n=e[1].replace(vo,"\\$&");return new RegExp(t+"((?:.|\\n)+?)"+n,"g")});var mo={staticKeys:["staticClass"],transformNode:function(e,t){t.warn;var n=Fr(e,"class");n&&(e.staticClass=JSON.stringify(n));var r=Ir(e,"class",!1);r&&(e.classBinding=r)},genData:function(e){var t="";return e.staticClass&&(t+="staticClass:"+e.staticClass+","),e.classBinding&&(t+="class:"+e.classBinding+","),t}};var yo,go={staticKeys:["staticStyle"],transformNode:function(e,t){t.warn;var n=Fr(e,"style");n&&(e.staticStyle=JSON.stringify(ai(n)));var r=Ir(e,"style",!1);r&&(e.styleBinding=r)},genData:function(e){var t="";return e.staticStyle&&(t+="staticStyle:"+e.staticStyle+","),e.styleBinding&&(t+="style:("+e.styleBinding+"),"),t}},_o=function(e){return(yo=yo||document.createElement("div")).innerHTML=e,yo.textContent},bo=p("area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr"),$o=p("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source"),wo=p("address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track"),Co=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,xo=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,ko="[a-zA-Z_][\\-\\.0-9_a-zA-Z"+P.source+"]*",Ao="((?:"+ko+"\\:)?"+ko+")",Oo=new RegExp("^<"+Ao),So=/^\s*(\/?)>/,To=new RegExp("^<\\/"+Ao+"[^>]*>"),Eo=/^]+>/i,No=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},Io=/&(?:lt|gt|quot|amp|#39);/g,Fo=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,Po=p("pre,textarea",!0),Ro=function(e,t){return e&&Po(e)&&"\n"===t[0]};function Ho(e,t){var n=t?Fo:Io;return e.replace(n,function(e){return Mo[e]})}var Bo,Uo,zo,Vo,Ko,Jo,qo,Wo,Zo=/^@|^v-on:/,Go=/^v-|^@|^:|^#/,Xo=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,Yo=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,Qo=/^\(|\)$/g,ea=/^\[.*\]$/,ta=/:(.*)$/,na=/^:|^\.|^v-bind:/,ra=/\.[^.\]]+(?=[^\]]*$)/g,ia=/^v-slot(:|$)|^#/,oa=/[\r\n]/,aa=/\s+/g,sa=g(_o),ca="_empty_";function ua(e,t,n){return{type:1,tag:e,attrsList:t,attrsMap:ma(t),rawAttrsMap:{},parent:n,children:[]}}function la(e,t){Bo=t.warn||Sr,Jo=t.isPreTag||T,qo=t.mustUseProp||T,Wo=t.getTagNamespace||T;t.isReservedTag;zo=Tr(t.modules,"transformNode"),Vo=Tr(t.modules,"preTransformNode"),Ko=Tr(t.modules,"postTransformNode"),Uo=t.delimiters;var n,r,i=[],o=!1!==t.preserveWhitespace,a=t.whitespace,s=!1,c=!1;function u(e){if(l(e),s||e.processed||(e=fa(e,t)),i.length||e===n||n.if&&(e.elseif||e.else)&&da(n,{exp:e.elseif,block:e}),r&&!e.forbidden)if(e.elseif||e.else)a=e,(u=function(e){var t=e.length;for(;t--;){if(1===e[t].type)return e[t];e.pop()}}(r.children))&&u.if&&da(u,{exp:a.elseif,block:a});else{if(e.slotScope){var o=e.slotTarget||'"default"';(r.scopedSlots||(r.scopedSlots={}))[o]=e}r.children.push(e),e.parent=r}var a,u;e.children=e.children.filter(function(e){return!e.slotScope}),l(e),e.pre&&(s=!1),Jo(e.tag)&&(c=!1);for(var f=0;f]*>)","i")),p=e.replace(f,function(e,n,r){return u=r.length,Do(l)||"noscript"===l||(n=n.replace(//g,"$1").replace(//g,"$1")),Ro(l,n)&&(n=n.slice(1)),t.chars&&t.chars(n),""});c+=e.length-p.length,e=p,A(l,c-u,c)}else{var d=e.indexOf("<");if(0===d){if(No.test(e)){var v=e.indexOf("--\x3e");if(v>=0){t.shouldKeepComment&&t.comment(e.substring(4,v),c,c+v+3),C(v+3);continue}}if(jo.test(e)){var h=e.indexOf("]>");if(h>=0){C(h+2);continue}}var m=e.match(Eo);if(m){C(m[0].length);continue}var y=e.match(To);if(y){var g=c;C(y[0].length),A(y[1],g,c);continue}var _=x();if(_){k(_),Ro(_.tagName,e)&&C(1);continue}}var b=void 0,$=void 0,w=void 0;if(d>=0){for($=e.slice(d);!(To.test($)||Oo.test($)||No.test($)||jo.test($)||(w=$.indexOf("<",1))<0);)d+=w,$=e.slice(d);b=e.substring(0,d)}d<0&&(b=e),b&&C(b.length),t.chars&&b&&t.chars(b,c-b.length,c)}if(e===n){t.chars&&t.chars(e);break}}function C(t){c+=t,e=e.substring(t)}function x(){var t=e.match(Oo);if(t){var n,r,i={tagName:t[1],attrs:[],start:c};for(C(t[0].length);!(n=e.match(So))&&(r=e.match(xo)||e.match(Co));)r.start=c,C(r[0].length),r.end=c,i.attrs.push(r);if(n)return i.unarySlash=n[1],C(n[0].length),i.end=c,i}}function k(e){var n=e.tagName,c=e.unarySlash;o&&("p"===r&&wo(n)&&A(r),s(n)&&r===n&&A(n));for(var u=a(n)||!!c,l=e.attrs.length,f=new Array(l),p=0;p=0&&i[a].lowerCasedTag!==s;a--);else a=0;if(a>=0){for(var u=i.length-1;u>=a;u--)t.end&&t.end(i[u].tag,n,o);i.length=a,r=a&&i[a-1].tag}else"br"===s?t.start&&t.start(e,[],!0,n,o):"p"===s&&(t.start&&t.start(e,[],!1,n,o),t.end&&t.end(e,n,o))}A()}(e,{warn:Bo,expectHTML:t.expectHTML,isUnaryTag:t.isUnaryTag,canBeLeftOpenTag:t.canBeLeftOpenTag,shouldDecodeNewlines:t.shouldDecodeNewlines,shouldDecodeNewlinesForHref:t.shouldDecodeNewlinesForHref,shouldKeepComment:t.comments,outputSourceRange:t.outputSourceRange,start:function(e,o,a,l,f){var p=r&&r.ns||Wo(e);q&&"svg"===p&&(o=function(e){for(var t=[],n=0;nc&&(s.push(o=e.slice(c,i)),a.push(JSON.stringify(o)));var u=Ar(r[1].trim());a.push("_s("+u+")"),s.push({"@binding":u}),c=i+r[0].length}return c-1"+("true"===o?":("+t+")":":_q("+t+","+o+")")),Mr(e,"change","var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+o+"):("+a+");if(Array.isArray($$a)){var $$v="+(r?"_n("+i+")":i)+",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&("+Br(t,"$$a.concat([$$v])")+")}else{$$i>-1&&("+Br(t,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+Br(t,"$$c")+"}",null,!0)}(e,r,i);else if("input"===o&&"radio"===a)!function(e,t,n){var r=n&&n.number,i=Ir(e,"value")||"null";Er(e,"checked","_q("+t+","+(i=r?"_n("+i+")":i)+")"),Mr(e,"change",Br(t,i),null,!0)}(e,r,i);else if("input"===o||"textarea"===o)!function(e,t,n){var r=e.attrsMap.type,i=n||{},o=i.lazy,a=i.number,s=i.trim,c=!o&&"range"!==r,u=o?"change":"range"===r?Wr:"input",l="$event.target.value";s&&(l="$event.target.value.trim()"),a&&(l="_n("+l+")");var f=Br(t,l);c&&(f="if($event.target.composing)return;"+f),Er(e,"value","("+t+")"),Mr(e,u,f,null,!0),(s||a)&&Mr(e,"blur","$forceUpdate()")}(e,r,i);else if(!F.isReservedTag(o))return Hr(e,r,i),!1;return!0},text:function(e,t){t.value&&Er(e,"textContent","_s("+t.value+")",t)},html:function(e,t){t.value&&Er(e,"innerHTML","_s("+t.value+")",t)}},isPreTag:function(e){return"pre"===e},isUnaryTag:bo,mustUseProp:jn,canBeLeftOpenTag:$o,isReservedTag:Wn,getTagNamespace:Zn,staticKeys:function(e){return e.reduce(function(e,t){return e.concat(t.staticKeys||[])},[]).join(",")}(ba)},xa=g(function(e){return p("type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap"+(e?","+e:""))});function ka(e,t){e&&($a=xa(t.staticKeys||""),wa=t.isReservedTag||T,function e(t){t.static=function(e){if(2===e.type)return!1;if(3===e.type)return!0;return!(!e.pre&&(e.hasBindings||e.if||e.for||d(e.tag)||!wa(e.tag)||function(e){for(;e.parent;){if("template"!==(e=e.parent).tag)return!1;if(e.for)return!0}return!1}(e)||!Object.keys(e).every($a)))}(t);if(1===t.type){if(!wa(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var n=0,r=t.children.length;n|^function(?:\s+[\w$]+)?\s*\(/,Oa=/\([^)]*?\);*$/,Sa=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,Ta={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},Ea={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},Na=function(e){return"if("+e+")return null;"},ja={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:Na("$event.target !== $event.currentTarget"),ctrl:Na("!$event.ctrlKey"),shift:Na("!$event.shiftKey"),alt:Na("!$event.altKey"),meta:Na("!$event.metaKey"),left:Na("'button' in $event && $event.button !== 0"),middle:Na("'button' in $event && $event.button !== 1"),right:Na("'button' in $event && $event.button !== 2")};function Da(e,t){var n=t?"nativeOn:":"on:",r="",i="";for(var o in e){var a=La(e[o]);e[o]&&e[o].dynamic?i+=o+","+a+",":r+='"'+o+'":'+a+","}return r="{"+r.slice(0,-1)+"}",i?n+"_d("+r+",["+i.slice(0,-1)+"])":n+r}function La(e){if(!e)return"function(){}";if(Array.isArray(e))return"["+e.map(function(e){return La(e)}).join(",")+"]";var t=Sa.test(e.value),n=Aa.test(e.value),r=Sa.test(e.value.replace(Oa,""));if(e.modifiers){var i="",o="",a=[];for(var s in e.modifiers)if(ja[s])o+=ja[s],Ta[s]&&a.push(s);else if("exact"===s){var c=e.modifiers;o+=Na(["ctrl","shift","alt","meta"].filter(function(e){return!c[e]}).map(function(e){return"$event."+e+"Key"}).join("||"))}else a.push(s);return a.length&&(i+=function(e){return"if(!$event.type.indexOf('key')&&"+e.map(Ma).join("&&")+")return null;"}(a)),o&&(i+=o),"function($event){"+i+(t?"return "+e.value+"($event)":n?"return ("+e.value+")($event)":r?"return "+e.value:e.value)+"}"}return t||n?e.value:"function($event){"+(r?"return "+e.value:e.value)+"}"}function Ma(e){var t=parseInt(e,10);if(t)return"$event.keyCode!=="+t;var n=Ta[e],r=Ea[e];return"_k($event.keyCode,"+JSON.stringify(e)+","+JSON.stringify(n)+",$event.key,"+JSON.stringify(r)+")"}var Ia={on:function(e,t){e.wrapListeners=function(e){return"_g("+e+","+t.value+")"}},bind:function(e,t){e.wrapData=function(n){return"_b("+n+",'"+e.tag+"',"+t.value+","+(t.modifiers&&t.modifiers.prop?"true":"false")+(t.modifiers&&t.modifiers.sync?",true":"")+")"}},cloak:S},Fa=function(e){this.options=e,this.warn=e.warn||Sr,this.transforms=Tr(e.modules,"transformCode"),this.dataGenFns=Tr(e.modules,"genData"),this.directives=A(A({},Ia),e.directives);var t=e.isReservedTag||T;this.maybeComponent=function(e){return!!e.component||!t(e.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function Pa(e,t){var n=new Fa(t);return{render:"with(this){return "+(e?Ra(e,n):'_c("div")')+"}",staticRenderFns:n.staticRenderFns}}function Ra(e,t){if(e.parent&&(e.pre=e.pre||e.parent.pre),e.staticRoot&&!e.staticProcessed)return Ha(e,t);if(e.once&&!e.onceProcessed)return Ba(e,t);if(e.for&&!e.forProcessed)return za(e,t);if(e.if&&!e.ifProcessed)return Ua(e,t);if("template"!==e.tag||e.slotTarget||t.pre){if("slot"===e.tag)return function(e,t){var n=e.slotName||'"default"',r=qa(e,t),i="_t("+n+(r?","+r:""),o=e.attrs||e.dynamicAttrs?Ga((e.attrs||[]).concat(e.dynamicAttrs||[]).map(function(e){return{name:b(e.name),value:e.value,dynamic:e.dynamic}})):null,a=e.attrsMap["v-bind"];!o&&!a||r||(i+=",null");o&&(i+=","+o);a&&(i+=(o?"":",null")+","+a);return i+")"}(e,t);var n;if(e.component)n=function(e,t,n){var r=t.inlineTemplate?null:qa(t,n,!0);return"_c("+e+","+Va(t,n)+(r?","+r:"")+")"}(e.component,e,t);else{var r;(!e.plain||e.pre&&t.maybeComponent(e))&&(r=Va(e,t));var i=e.inlineTemplate?null:qa(e,t,!0);n="_c('"+e.tag+"'"+(r?","+r:"")+(i?","+i:"")+")"}for(var o=0;o>>0}(a):"")+")"}(e,e.scopedSlots,t)+","),e.model&&(n+="model:{value:"+e.model.value+",callback:"+e.model.callback+",expression:"+e.model.expression+"},"),e.inlineTemplate){var o=function(e,t){var n=e.children[0];if(n&&1===n.type){var r=Pa(n,t.options);return"inlineTemplate:{render:function(){"+r.render+"},staticRenderFns:["+r.staticRenderFns.map(function(e){return"function(){"+e+"}"}).join(",")+"]}"}}(e,t);o&&(n+=o+",")}return n=n.replace(/,$/,"")+"}",e.dynamicAttrs&&(n="_b("+n+',"'+e.tag+'",'+Ga(e.dynamicAttrs)+")"),e.wrapData&&(n=e.wrapData(n)),e.wrapListeners&&(n=e.wrapListeners(n)),n}function Ka(e){return 1===e.type&&("slot"===e.tag||e.children.some(Ka))}function Ja(e,t){var n=e.attrsMap["slot-scope"];if(e.if&&!e.ifProcessed&&!n)return Ua(e,t,Ja,"null");if(e.for&&!e.forProcessed)return za(e,t,Ja);var r=e.slotScope===ca?"":String(e.slotScope),i="function("+r+"){return "+("template"===e.tag?e.if&&n?"("+e.if+")?"+(qa(e,t)||"undefined")+":undefined":qa(e,t)||"undefined":Ra(e,t))+"}",o=r?"":",proxy:true";return"{key:"+(e.slotTarget||'"default"')+",fn:"+i+o+"}"}function qa(e,t,n,r,i){var o=e.children;if(o.length){var a=o[0];if(1===o.length&&a.for&&"template"!==a.tag&&"slot"!==a.tag){var s=n?t.maybeComponent(a)?",1":",0":"";return""+(r||Ra)(a,t)+s}var c=n?function(e,t){for(var n=0,r=0;r':'
',ts.innerHTML.indexOf(" ")>0}var os=!!z&&is(!1),as=!!z&&is(!0),ss=g(function(e){var t=Yn(e);return t&&t.innerHTML}),cs=wn.prototype.$mount;return wn.prototype.$mount=function(e,t){if((e=e&&Yn(e))===document.body||e===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=ss(r));else{if(!r.nodeType)return this;r=r.innerHTML}else e&&(r=function(e){if(e.outerHTML)return e.outerHTML;var t=document.createElement("div");return t.appendChild(e.cloneNode(!0)),t.innerHTML}(e));if(r){var i=rs(r,{outputSourceRange:!1,shouldDecodeNewlines:os,shouldDecodeNewlinesForHref:as,delimiters:n.delimiters,comments:n.comments},this),o=i.render,a=i.staticRenderFns;n.render=o,n.staticRenderFns=a}}return cs.call(this,e,t)},wn.compile=rs,wn}); \ No newline at end of file diff --git a/public/service-worker.js b/public/service-worker.js index a9be8860..442128b0 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,13 +1,12 @@ // service-worker.js -const CACHE_NAME = 'v3_cache'; +const CACHE_NAME = 'v2_cache'; const urlsToCache = [ - 'res/vue.js', - 'res/img/defaultMap.jpg', - 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js', - 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css', - 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.2/font/bootstrap-icons.min.css', - 'https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/3.5.0/css/flag-icon.min.css', - + 'defaultMap.jpg', + 'defaultMap_dark.jpg', + 'logo-144.png', + 'logo-192.png', + 'logo-256.png', + 'logo-512.png', ]; // 安装 Service Worker 时,缓存文件 @@ -15,7 +14,6 @@ self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { - console.log('Opened cache'); return cache.addAll(urlsToCache); }) ); diff --git a/server.js b/server.js index cda93438..99e62272 100644 --- a/server.js +++ b/server.js @@ -1,14 +1,17 @@ -require('dotenv').config(); -const express = require('express'); -const path = require('path'); -const mapHandler = require('./api/map'); -const validateMapKeyHandler = require('./api/validate-map-key'); -const validateSite = require('./api/validate-site'); -const ipinfoHandler = require('./api/ipinfo'); -const ipapicomHandler = require('./api/ipapicom'); +import dotenv from 'dotenv'; +import express from 'express'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import mapHandler from './api/map.js'; +import validateMapKeyHandler from './api/validate-map-key.js'; +import validateSite from './api/validate-site.js'; +import ipinfoHandler from './api/ipinfo.js'; +import ipapicomHandler from './api/ipapicom.js'; + +dotenv.config(); const app = express(); -const port = process.env.PORT || 8966; +const port = process.env.PORT || 11966; // 使用查询参数处理所有地图请求 app.get('/api/map', mapHandler); @@ -18,7 +21,9 @@ app.get('/api/ipinfo', ipinfoHandler); app.get('/api/ipapicom', ipapicomHandler); // 设置静态文件服务 -app.use(express.static(path.join(__dirname, 'public'))); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +app.use(express.static(path.join(__dirname, './dist'))); // 一些判断 app.all('/api/validate-map-key', validateMapKeyHandler); diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 00000000..fcb4c925 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,429 @@ + + + + + diff --git a/src/components/connectivity.vue b/src/components/connectivity.vue new file mode 100644 index 00000000..5bbb8763 --- /dev/null +++ b/src/components/connectivity.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/src/components/dnsleaks.vue b/src/components/dnsleaks.vue new file mode 100644 index 00000000..68fac046 --- /dev/null +++ b/src/components/dnsleaks.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/src/components/footer.vue b/src/components/footer.vue new file mode 100644 index 00000000..45069397 --- /dev/null +++ b/src/components/footer.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/components/globallatency.vue b/src/components/globallatency.vue new file mode 100644 index 00000000..5876c35b --- /dev/null +++ b/src/components/globallatency.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/src/components/help.vue b/src/components/help.vue new file mode 100644 index 00000000..34b58c4a --- /dev/null +++ b/src/components/help.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/components/ipcheck.vue b/src/components/ipcheck.vue new file mode 100644 index 00000000..c27412ec --- /dev/null +++ b/src/components/ipcheck.vue @@ -0,0 +1,580 @@ + + + + + diff --git a/src/components/nav.vue b/src/components/nav.vue new file mode 100644 index 00000000..cdd5e829 --- /dev/null +++ b/src/components/nav.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/src/components/queryip.vue b/src/components/queryip.vue new file mode 100644 index 00000000..93deac99 --- /dev/null +++ b/src/components/queryip.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/src/components/speedtest.vue b/src/components/speedtest.vue new file mode 100644 index 00000000..d881bee7 --- /dev/null +++ b/src/components/speedtest.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/src/components/webrtc.vue b/src/components/webrtc.vue new file mode 100644 index 00000000..14ba073f --- /dev/null +++ b/src/components/webrtc.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 00000000..52d280ea --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,18 @@ +import { createI18n } from 'vue-i18n'; + + +import en from './locales/en.json'; +import zh from './locales/zh.json'; + + +const messages = { en, zh }; + + +const i18n = createI18n({ + legacy: true, + locale: 'zh', + fallbackLocale: 'en', + messages, +}); + +export default i18n; diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 00000000..931cec79 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,154 @@ +{ + "nav": { + "id": "nav", + "Title": "IP Toolbox", + "IPinfo": "IP Infos", + "Connectivity": "Connectivity", + "WebRTC": "WebRTC Test", + "DNSLeakTest": "DNS Leak Test", + "SpeedTest": "Speed Test", + "PingTest": "Global Latency" + }, + "ipInfos": { + "id": "ipinfos", + "Title": "IP Infos", + "Notes": "IP data will be checked from 6 sources. If there is only 1 IP stack currently, sources without data will show as empty.", + "Simple": "Simple", + "Map": "Map", + "MapUnavailable": "Map Unavailable", + "Source": "Source", + "IP": "IP", + "Country": "Country", + "Region": "Region", + "City": "City", + "ISP": "ISP", + "ASN": "ASN", + "IPv4Error": "Fetch Failed or No IPv4 Address", + "IPv6Error": "Fetch Failed or No IPv6 Address" + }, + "connectivity": { + "id": "connectivity", + "Title": "Network Connectivity", + "Note": "Testing is done by loading small images from corresponding websites. Delay values are for reference only and will be smaller in reality.", + "StatusWait": "Awaiting Test", + "StatusAvailable": "OK", + "StatusUnavailable": "Unavailable", + "StatusTimeout": "Timeout or Unavailable", + "checking": "Checking..." + }, + "webrtc": { + "id": "webrtc", + "Title": "WebRTC Test", + "Note": "WebRTC often establishes connections directly via UDP. If the test returns your real IP, it means your proxy settings do not cover these connections.", + "StatusWait": "Awaiting Test or Connection Error", + "StatusError": "Test Error" + }, + "dnsleaktest": { + "id": "dnsleaktest", + "Name": "Test", + "Title": "DNS Leak Test", + "Note": "A DNS leak means that when you are connected to a VPN/proxy, your domain name resolutions are still done through your local ISP, thus posing a risk of DNS leaks.", + "Note2": "The method to test for DNS leaks involves accessing a newly generated domain name to detect which regional DNS exit you are resolving through. If the endpoint region returned matches your local service provider's region, there is a risk of DNS leakage. In this case, you may need to modify your VPN/proxy settings.", + "Endpoint": "DNS Endpoint", + "EndpointCountry": "Endpoint Region", + "StatusWait": "Awaiting Test", + "StatusError": "Test Error" + }, + "speedtest": { + "id": "speedtest", + "Title": "Speed Test", + "Note": "Speed test will be done via Cloudflare's edge network, which may take a long time. If your network is located in mainland China, the data may be biased. The speed test results are for reference only, the actual speed will be faster.", + "Download": "Download", + "Upload": "Upload", + "Latency": "Latency", + "Jitter": "Jitter", + "Unit": "Mbps", + "StatusWait": "--", + "StatusError": "Test Error", + "videoStreaming": "Video Streaming: ", + "gaming": ", Gaming: ", + "rtc": ", Video Chatting: ", + "score": "Based on the speed test, got the following quality scores:", + "resultNote": ". The score is for reference only." + }, + "pingtest": { + "id": "pingtest", + "Title": "Global Latency Test", + "Note": "Latency tests will be performed on servers located in different continents and regions around the world to measure the latency of your IP. This will help you understand your connection speed to the global network.", + "Note2": "Currently only supports IPv4 addresses, please select your IP address for testing", + "SelectIP": "Select an IP Address", + "Region": "Region", + "MinDelay": "Min Delay (ms)", + "MaxDelay": "Max Delay (ms)", + "AvgDelay": "Avg Delay (ms)", + "TotalPackets": "Total Packets", + "PacketLoss": "Packet Loss", + "ReceivedPackets": "Received", + "DroppedPackets": "Dropped", + "Run": "Run Test", + "Error": "Test failed, it looks like your IP is not allowed to perform Ping test." + }, + "ipcheck": { + "id": "ipcheck", + "Title": "IP Check", + "Placeholder": "Please enter an IP address", + "Button": "Check", + "Error": "Please enter a valid IPv4 or IPv6 address." + }, + "alert": { + "id": "alert", + "refreshEverythingMessage": "Refreshing all data, please wait...", + "refreshEverythingTitle": "Refreshing", + "OhNo": "Oh No!", + "Congrats": "Congrats!", + "OhNo_Message": "Connectivity test failed, some websites are not accessible.", + "Congrats_Message": "All connectivity tests passed, you can access all websites.", + "maskedInfoTitle_1": "IP masked", + "maskedInfoMessage_1": "IP information has been masked, please be careful when taking screenshots.", + "maskedInfoTitle": "All infos masked", + "maskedInfoMessage": "All information has been masked, you can now safely take screenshots.", + "unmaskedInfoTitle": "Infos Unmasked", + "unmaskedInfoMessage": "Information has been unmasked, please be careful when taking screenshots." + }, + "shortcutKeys": { + "id": "shortcutKeys", + "GoToTop": "Go to Top", + "GoToBottom": "Go to Bottom", + "ToggleDarkMode": "Toggle Dark Mode", + "RefreshEverything": "Refresh Everything", + "RefreshIPCard": "Refresh IP Card", + "RefreshConnectivityTests": "Refresh Connectivity Tests", + "RefreshWebRTC": "Refresh WebRTC Test", + "RefreshDNSLeakTest": "Refresh DNS Leak Test", + "StartSpeedTest": "Start Speed Test", + "ToggleMaps": "Toggle Maps", + "IPCheck": "IP Check", + "AddBingMapKey": "Add Bing Map API Key", + "ToggleInfoMask": "Toggle Info Masking", + "Help": "Show Shortcut Keys", + "HelpNote": "People who use shortcuts are really awesome!", + "GoNext": "Next Card", + "GoPrevious": "Previous Card" + }, + "page": { + "title": "Check My IP Address and Geolocation | Check WebRTC Connection IP | DNS Leak Test | Speed Test | Jason Ng Open Source", + "description": "A better IP Toolbox. Easy to check what's your IPs, IP informations, check for DNS leaks, examine WebRTC connections, and test website availability", + "keywords": "MyIP,IP Tool,My IP,IP check,IP lookup,DNS leak test,WebRTC test,Speed test", + "footerName": "Creators: Jason Ng, Seven Yu & ChatGPT", + "footerLink": "https://github.com/jason5ng32/MyIP", + "copyRightName": "Originally from ", + "copyRightLink": "https://ipcheck.ing", + "copyRightLinkName": "IPCheck.ing" + }, + "addBingMapKey": { + "Title": "Add Bing Map API Key", + "Note": "After adding, the map can be displayed in the IP information. The API Key will only be saved in the current browser for the next use, and will not be uploaded to the server.", + "Placeholder": "Please enter Bing Map API Key", + "Button": "Save", + "removeButton": "Remove", + "Error": "Please enter a valid Bing Map API Key." + }, + "helpModal": { + "Title": "Shortcuts" + } +} \ No newline at end of file diff --git a/src/locales/zh.json b/src/locales/zh.json new file mode 100644 index 00000000..ed9cb204 --- /dev/null +++ b/src/locales/zh.json @@ -0,0 +1,154 @@ +{ + "nav": { + "id": "nav", + "Title": "IP 工具箱", + "IPinfo": "IP 信息", + "Connectivity": "网络连通性", + "WebRTC": "WebRTC 测试", + "DNSLeakTest": "DNS 泄漏测试", + "SpeedTest": "网速测试", + "PingTest": "全球测速" + }, + "ipInfos": { + "id": "ipinfos", + "Title": "IP 信息", + "Notes": "将会从 6 个来源检查 IP 数据,如果当前 IP 栈只有 1 个,则没有数据的来源会显示为空。", + "Simple": "简约", + "Map": "地图", + "MapUnavailable": "地图不可用", + "Source": "来源", + "IP": "IP", + "Country": "国家", + "Region": "地区", + "City": "城市", + "ISP": "ISP", + "ASN": "ASN", + "IPv4Error": "获取失败或不存在 IPv4 地址", + "IPv6Error": "获取失败或不存在 IPv6 地址" + }, + "connectivity": { + "id": "connectivity", + "Title": "网络连通性", + "Note": "通过加载对应网站上的小图片进行测试,延迟值仅供参考,实际会更小。", + "StatusWait": "待检测", + "StatusAvailable": "可用", + "StatusUnavailable": "不可用", + "StatusTimeout": "超时或不可用", + "checking": "检查中..." + }, + "webrtc": { + "id": "webrtc", + "Title": "WebRTC 测试", + "Note": "WebRTC 往往通过 UDP 直连进行建立,如果测试返回了真实 IP,则意味着你的代理设置没有覆盖这些连接。", + "StatusWait": "待检测或连接错误", + "StatusError": "测试出错" + }, + "dnsleaktest": { + "id": "dnsleaktest", + "Name": "检测", + "Title": "DNS 泄漏测试", + "Note": "DNS 泄露(DNS Leaks)的意思是,当你挂上 VPN/代理后,你解析域名时,依然通过当地的运营商进行解析,这时就有 DNS 泄露的风险。", + "Note2": "泄露测试的方法是通过访问新生成的域名,检测你是通过哪个地区的 DNS 出口进行解析,如果返回的出口区域和你当地的运营商区域相同,则有 DNS 泄露风险,你可能需要修改 VPN/代理设置。", + "Endpoint": "DNS 出口", + "EndpointCountry": "出口地区", + "StatusWait": "待检测", + "StatusError": "测试出错" + }, + "speedtest": { + "id": "speedtest", + "Title": "网速测试", + "Note": "将从 Cloudflare 使用边沿网络进行速度测试,时间可能较长。如果你的网络位于中国大陆,数据可能会有较大的偏差。测速结果仅供参考,实际速度会更快。", + "Download": "下载", + "Upload": "上传", + "Latency": "延迟", + "Jitter": "抖动", + "Unit": "Mbps", + "StatusWait": "--", + "StatusError": "测试出错", + "videoStreaming": "在线视频:", + "gaming": " 分,在线游戏:", + "rtc": " 分,视频会议:", + "score": "根据网速测试,得出以下质量分数:", + "resultNote": " 分。分数仅供参考。" + }, + "pingtest": { + "id": "pingtest", + "Title": "全球延迟测试", + "Note": "将从分布在全球不同大洲和地区的服务器,分别对你的 IP 进行 Ping 测试,并获取延迟值。从而了解你与全球网络的连接速度。", + "Note2": "目前仅支持 IPv4 地址,请选取你的 IP 地址进行测试", + "SelectIP": "选择一个 IP 地址", + "Region": "地区", + "MinDelay": "最小延迟(ms)", + "MaxDelay": "最大延迟(ms)", + "AvgDelay": "平均延迟(ms)", + "TotalPackets": "总包数", + "PacketLoss": "丢包率", + "ReceivedPackets": "收到", + "DroppedPackets": "丢失", + "Run": "运行测试", + "Error": "测试失败,看起来你的 IP 不允许进行 Ping 测试。" + }, + "ipcheck": { + "id": "ipcheck", + "Title": "IP 查询", + "Placeholder": "请输入 IP 地址", + "Button": "查询", + "Error": "请输入有效的 IPv4 或 IPv6 地址。" + }, + "alert": { + "id": "alert", + "refreshEverythingMessage": "正在刷新所有数据,请等待...", + "refreshEverythingTitle": "正在刷新", + "OhNo": "糟糕!", + "Congrats": "恭喜呀!", + "OhNo_Message": "连通性检测没有通过,部分网站无法访问。", + "Congrats_Message": "所有的连通性检测均通过,你可以访问所有网站。", + "maskedInfoTitle_1": "IP 隐藏成功", + "maskedInfoMessage_1": "IP 信息已隐藏,截图时请注意隐私。", + "maskedInfoTitle": "全部隐藏成功", + "maskedInfoMessage": "所有信息已隐藏,现在可以安心截图了。", + "unmaskedInfoTitle": "取消隐藏", + "unmaskedInfoMessage": "信息已显示,截图时请注意隐私。" + }, + "shortcutKeys": { + "id": "shortcutKeys", + "GoToTop": "回到顶部", + "GoToBottom": "回到底部", + "ToggleDarkMode": "切换深色模式", + "RefreshEverything": "刷新所有数据", + "RefreshIPCard": "刷新某张 IP 卡片", + "RefreshConnectivityTests": "刷新连通性测试", + "RefreshWebRTC": "刷新 WebRTC 测试", + "RefreshDNSLeakTest": "刷新 DNS 泄漏测试", + "StartSpeedTest": "开始网速测试", + "ToggleMaps": "切换地图显示", + "IPCheck": "IP 查询", + "AddBingMapKey": "添加 Bing Map API Key", + "ToggleInfoMask": "切换信息隐藏", + "Help": "显示快捷键", + "HelpNote": "使用快捷键的人,是喜欢效率的人,你真棒!", + "GoNext": "下一张卡片", + "GoPrevious": "上一张卡片" + }, + "page": { + "title": "查看我的 IP 地址 | 查询本机 IP 地址及归属地 | 查看 WebRTC 连接 IP | DNS 泄露检测 | 网速测试 | Jason Ng 阿禅开源作品", + "description": "一个更好的 IP 工具箱。轻松查看你的 IP 地址,IP 信息,检查 DNS 泄露,检查 WebRTC 连接,以及测试网站可用性", + "keywords": "我的IP,IP工具,我的IP,IP查询,DNS泄漏测试,WebRTC测试,网速测试", + "footerName": "Creators: Jason Ng, Seven Yu & ChatGPT", + "footerLink": "https://github.com/jason5ng32/MyIP", + "copyRightName": "Originally from ", + "copyRightLink": "https://ipcheck.ing", + "copyRightLinkName": "IPCheck.ing" + }, + "addBingMapKey": { + "Title": "添加 Bing Map API Key", + "Note": "添加后,可以在 IP 信息中显示地图。API Key 只会在当前浏览器中保存,方便下次使用,不会上传到服务器。", + "Placeholder": "请输入 Bing Map API Key", + "Button": "保存", + "removeButton": "删除", + "Error": "请输入有效的 Bing Map API Key。" + }, + "helpModal": { + "Title": "快捷键" + } +} \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..4a8b04b3 --- /dev/null +++ b/src/main.js @@ -0,0 +1,76 @@ +import { createApp } from 'vue' +import i18n from './i18n'; +import './style.css' +import App from './App.vue' +import store from './store'; +import Analytics from 'analytics'; +import googleAnalytics from '@analytics/google-analytics'; + +const app = createApp(App); + +const analytics = Analytics({ + app: 'MyIP', + plugins: [ + googleAnalytics({ + measurementIds: ['G-TEYKKD81TL'], + }) + ] +}); + + +// 窗口大小变化处理函数 +function handleResize() { + store.commit('setIsMobile', window.innerWidth < 768); + } + +// 更新网页标题和元数据的函数 +function updateMeta() { + // 使用 i18n.global.t 来获取翻译后的值 + document.title = i18n.global.t('page.title'); + + const metaKeywords = document.querySelector('meta[name="keywords"]'); + const metaDescription = document.querySelector('meta[name="description"]'); + if (metaKeywords) { + metaKeywords.setAttribute('content', i18n.global.t('page.keywords')); + } + if (metaDescription) { + metaDescription.setAttribute('content', i18n.global.t('page.description')); + } +} + +// 设置语言的函数 +function setLanguageFromURL() { + const searchParams = new URLSearchParams(window.location.search); + const browserLanguage = navigator.language || navigator.userLanguage; + const langCN = browserLanguage.startsWith('zh'); + const hl = searchParams.get('hl'); + if (hl && ['en', 'zh'].includes(hl)) { + i18n.global.locale = hl; + updateMeta(); // 更新网页标题和元数据 + } else if (langCN) { + i18n.global.locale = 'zh'; + updateMeta(); // 更新网页标题和元数据 + } else { + i18n.global.locale = 'en'; + updateMeta(); // 更新网页标题和元数据 + } +} + +// 在应用启动时设置语言 +setLanguageFromURL(); + +// 监听窗口大小变化 +handleResize(); +window.addEventListener('resize', handleResize); + +// 监听 URL 变化 +window.addEventListener('popstate', setLanguageFromURL); + +// 启动 Google Analytics +analytics.page(); + +app.use(store); +app.use(i18n); +app.config.globalProperties.$isLangCN = i18n.global.locale === 'zh'; +app.config.globalProperties.$analytics = analytics; +app.mount('#app'); diff --git a/public/res/shortcut.js b/src/shortcut.js similarity index 100% rename from public/res/shortcut.js rename to src/shortcut.js diff --git a/src/store.js b/src/store.js new file mode 100644 index 00000000..7b1a280c --- /dev/null +++ b/src/store.js @@ -0,0 +1,55 @@ +// store.js +import { createStore } from 'vuex'; + +const store = createStore({ + state() { + return { + // 定义全局共享的状态 + inputBingMapAPIKEY: "", + bingMapAPIKEYError: false, + bingMapLanguage: "en", + isDarkMode: false, + isMobile: false, + siteValidate: true, + + // 全局变量 + shouldRefreshEveryThing: false, + Global_ipDataCards: [], + }; + }, + mutations: { + toggleDarkMode(state) { + state.isDarkMode = !state.isDarkMode; + }, + updateGlobalIpDataCards(state, payload) { + state.Global_ipDataCards = [...payload]; + }, + updateGlobalSiteValidate(state, payload) { + state.siteValidate = payload; + }, + setIsMobile(state, payload) { + state.isMobile = payload; + }, + setRefreshEveryThing(state, payload) { + state.shouldRefreshEveryThing = payload; + }, + SET_DARK_MODE(state, value) { + state.isDarkMode = value; + }, + }, + + actions: { + checkDarkMode({ commit }) { + const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; + commit('SET_DARK_MODE', prefersDarkMode); + }, + }, + + getters: { + isMobile(state) { + return state.isMobile; + } + }, +}); + +export default store; diff --git a/src/style.css b/src/style.css new file mode 100644 index 00000000..66d1aff2 --- /dev/null +++ b/src/style.css @@ -0,0 +1,269 @@ +@import 'bootstrap/dist/css/bootstrap.min.css'; +@import 'bootstrap-icons/font/bootstrap-icons.css'; +@import 'flag-icons/css/flag-icons.min.css'; + +.jn-title { + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-direction: row; + margin-top: 10pt; + margin-bottom: 10pt; +} + +.jn-title2 { + display: flex; + justify-content: space-between; + align-items: flex-end; + flex-direction: row; + margin-top: 10pt; + margin-bottom: 10pt; +} + +.jn-ip-title { + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row; + border: 0; + background-color:unset; +} + +.jn-alert { + z-index: 9999; + position: fixed; + width: 100%; + bottom: 0; +} + +.jn-card { + box-shadow: 0 0 10pt #0000001c; + height: 100%; + border: 1px solid #9c9c9c2e; +} + +.jn-card:hover { + border-color: #4dc0e461; +} + +.jn-card.keyboard-hover { + border-color: rgba(20, 184, 108, 0.42); +} + +.jn-card.keyboard-hover::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + border: 1px solid rgba(20, 184, 108, 0.42); + height: 100%; + pointer-events: none; + border-radius: 4pt; +} + +.jn-container { + padding-top: 2pt; + max-width: 1400px; +} + +.jn-text { + font-weight: bold; +} + +.jn-list-group-item { + padding: 0.75rem 0.25rem; +} + +.jn-opacity { + opacity: 0.5; + cursor: not-allowed; +} + +.jn-map-image { + width: 100%; + height: 200px; + object-fit: cover; + object-position: center; + border-radius: 0; +} + +.jn-navbar-top { + position: fixed; + top: 0; + width: 100%; + z-index: 9999; + border-bottom: 1px solid #0000001c; + +} + +.jn-toast { + z-index: 9999; + right: 0; + bottom: 0; + margin-bottom: 2pt; + margin-right: 40pt; + max-width: 80vw; +} + +footer { + height: 100pt; + padding-top: 20pt; +} + +@media only screen and (max-width: 768px) { + footer { + height: 140pt; + } + + .speedtest-h5 { + /* font-size: 12px; */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.jn-radio { + display: flex; + justify-content: flex-end; + align-items: stretch; + flex-direction: row; +} + +.jn-logo { + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; +} + +.dark-mode { + background-color: #212529 !important; + color: #e3e3e3 !important; + border-color: #757575; +} + +.dark-mode::placeholder { + color: #e3e3e38e !important; +} + +.dark-mode-border { + border-color: #a8a8a863; + box-shadow: 0 0 10pt #00000038; +} + +.dark-mode-title { + background-color: #171a1d; +} + +.dark-mode-nav { + background-color: #171a1d !important; + border-color: #a8a8a863 !important; + border-color: #757575; +} + +.jn-deactive { + opacity: 0.5; +} + +.active { + opacity: 1 !important; +} + +.dark-mode-nav-border { + border-color: #a8a8a863 !important; +} + +.dark-mode-close-button { + background-color: white; +} + +.dark-mode-refresh { + background-color: #141618; +} + +.dark-mode-refresh:hover { + background-color: #00000054; +} + +.mobile-list { + border: 0; +} + +.mobile-h2 { + font-size: 1.25rem; +} + +[v-cloak] { + display: none; +} + +.hidden { + display: none !important; +} + +.jn-loading { + margin-top: 50vh; +} + +.jn-shortcut-row { + margin: 2pt 0 2pt 0; + border-bottom: 1px solid; +} + +.jn-dark-mode-help-border { + border-color: #a8a8a863 !important; +} + +.jn-border-bottom-none { + border-bottom: none !important; +} + +.jn-help-note { + background: linear-gradient(to right, #e04343, orange, #8d8d22, green, #50c0ae, indigo, violet); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.jn-progress { + padding-bottom: 20pt; +} + +.jn-opacity-0 { + opacity: 0; +} + +.jn-progress-dark { + background-color: #4b4b4b !important; +} + +*:focus { + outline: none; +} + +.jn-speedtest-number { + font-size: 2em; +} + +.jn-speedtest-note { + background-color: #b2d6c33b; + border: 1px solid #1a8754; + margin: 0; + border-radius: 6pt; +} +/* .jn-speedtest-note::before { + content: ""; + border-top: 1px solid #19945a5c; + width: 50%; + margin: 0 auto; +} */ + +.speedtest-p { + margin-top:10pt; +} + +.jn-ping-form-select { + min-width: 140pt; +} \ No newline at end of file diff --git a/static-server.js b/static-server.js new file mode 100644 index 00000000..bc3a0479 --- /dev/null +++ b/static-server.js @@ -0,0 +1,29 @@ +// static-server.js +import dotenv from 'dotenv'; +import express from 'express'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { createProxyMiddleware } from 'http-proxy-middleware'; + +dotenv.config(); + +const staticApp = express(); +const staticPort = 18966; +const apiPort = process.env.PORT || 11966; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// API 请求代理到后端服务 +staticApp.use('/api', createProxyMiddleware({ + target: `http://localhost:${apiPort}`, + changeOrigin: true +})); + +// 设置静态文件目录 +staticApp.use(express.static(path.join(__dirname, './dist'))); + +// 启动静态文件服务 +staticApp.listen(staticPort, () => { + console.log(`Static file server running on port http://localhost:${staticPort}`); +}); diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 00000000..dcb3140d --- /dev/null +++ b/vite.config.js @@ -0,0 +1,18 @@ +import dotenv from 'dotenv'; +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +dotenv.config(); + +const apiPort = process.env.PORT || 11966; + +export default defineConfig({ + plugins: [vue()], + server: { + host: '0.0.0.0', + port: 18966, + proxy: { + '/api': `http://localhost:${apiPort}` + } + } +})