Add files via upload
This commit is contained in:
2055
src/css/main.css
Normal file
2055
src/css/main.css
Normal file
File diff suppressed because it is too large
Load Diff
1539
src/js/address-generator.js
Normal file
1539
src/js/address-generator.js
Normal file
File diff suppressed because it is too large
Load Diff
49
src/js/config.js
Normal file
49
src/js/config.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* MockAddress Core 配置模块
|
||||
* 允许用户自定义数据路径和其他设置,不影响正式站点
|
||||
*/
|
||||
|
||||
// 默认配置(用于正式站点 mockaddress.com)
|
||||
const defaultConfig = {
|
||||
// 数据文件基础路径
|
||||
// 如果用户想用自己的数据,可以设置为 'my-data/' 或 '/custom/path/data/'
|
||||
dataBasePath: null, // null 表示使用自动路径检测(正式站点行为)
|
||||
|
||||
// 是否启用自动路径检测(针对多语言目录结构)
|
||||
// 如果设为 false,则只使用 dataBasePath
|
||||
autoDetectPaths: true,
|
||||
|
||||
// 自定义数据加载器(可选)
|
||||
// 如果提供,将优先使用此函数加载数据,而不是默认的 fetch
|
||||
customDataLoader: null
|
||||
};
|
||||
|
||||
// 用户配置(会被 merge 到默认配置)
|
||||
let userConfig = {};
|
||||
|
||||
/**
|
||||
* 初始化配置
|
||||
* @param {Object} config - 用户配置对象
|
||||
* @example
|
||||
* MockAddressCore.config({
|
||||
* dataBasePath: 'my-data/',
|
||||
* autoDetectPaths: false
|
||||
* });
|
||||
*/
|
||||
export function configure(config = {}) {
|
||||
userConfig = { ...defaultConfig, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
*/
|
||||
export function getConfig() {
|
||||
return { ...defaultConfig, ...userConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置配置为默认值
|
||||
*/
|
||||
export function resetConfig() {
|
||||
userConfig = {};
|
||||
}
|
||||
319
src/js/language-switcher.js
Normal file
319
src/js/language-switcher.js
Normal file
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* 语言切换模块
|
||||
* 处理多语言切换和跳转
|
||||
*/
|
||||
|
||||
// 支持的语言配置
|
||||
const languages = {
|
||||
'zh': {
|
||||
code: 'zh',
|
||||
name: '简体中文',
|
||||
nativeName: '简体中文',
|
||||
flag: '🇨🇳',
|
||||
path: '' // 根目录
|
||||
},
|
||||
'en': {
|
||||
code: 'en',
|
||||
name: 'English',
|
||||
nativeName: 'English',
|
||||
flag: '🇬🇧',
|
||||
path: '/en'
|
||||
},
|
||||
'ru': {
|
||||
code: 'ru',
|
||||
name: 'Русский',
|
||||
nativeName: 'Русский',
|
||||
flag: '🇷🇺',
|
||||
path: '/ru'
|
||||
},
|
||||
'es': {
|
||||
code: 'es',
|
||||
name: 'Español',
|
||||
nativeName: 'Español',
|
||||
flag: '🇪🇸',
|
||||
path: '/es'
|
||||
},
|
||||
'pt': {
|
||||
code: 'pt',
|
||||
name: 'Português',
|
||||
nativeName: 'Português (BR)',
|
||||
flag: '🇧🇷',
|
||||
path: '/pt'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前语言代码
|
||||
*/
|
||||
function getCurrentLanguage() {
|
||||
const path = window.location.pathname;
|
||||
|
||||
// 从 URL 路径判断
|
||||
if (path.startsWith('/en/') || path.startsWith('/en')) {
|
||||
return 'en';
|
||||
} else if (path.startsWith('/ru/') || path.startsWith('/ru')) {
|
||||
return 'ru';
|
||||
} else if (path.startsWith('/es/') || path.startsWith('/es')) {
|
||||
return 'es';
|
||||
} else if (path.startsWith('/pt/') || path.startsWith('/pt')) {
|
||||
return 'pt';
|
||||
}
|
||||
return 'zh'; // 默认中文
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面路径(去除语言前缀,SEO友好格式)
|
||||
* 将 /index.html 转换为 /,生成更友好的URL
|
||||
*/
|
||||
function getCurrentPagePath() {
|
||||
const path = window.location.pathname;
|
||||
const currentLang = getCurrentLanguage();
|
||||
|
||||
let pagePath = path;
|
||||
|
||||
// 移除语言前缀(如果存在)
|
||||
if (currentLang !== 'zh') {
|
||||
const langPrefix = `/${currentLang}`;
|
||||
if (path.startsWith(langPrefix)) {
|
||||
pagePath = path.substring(langPrefix.length) || '/';
|
||||
}
|
||||
}
|
||||
|
||||
// 将 /index.html 转换为 / (SEO友好)
|
||||
// 将 /xxx/index.html 转换为 /xxx/
|
||||
if (pagePath.endsWith('/index.html')) {
|
||||
pagePath = pagePath.replace(/\/index\.html$/, '/');
|
||||
} else if (pagePath === '/index.html') {
|
||||
pagePath = '/';
|
||||
}
|
||||
|
||||
// 确保路径以 / 开头
|
||||
if (!pagePath.startsWith('/')) {
|
||||
pagePath = '/' + pagePath;
|
||||
}
|
||||
|
||||
// 目录页:如果不是根路径,确保以 / 结尾(SEO友好)
|
||||
// 但文章页/文件页(.html)不能追加 /,否则会变成 xxx.html/ 导致资源相对路径解析错误
|
||||
const isHtmlFile = /\.html$/i.test(pagePath);
|
||||
if (!isHtmlFile && pagePath !== '/' && !pagePath.endsWith('/')) {
|
||||
pagePath = pagePath + '/';
|
||||
}
|
||||
|
||||
return pagePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到指定语言
|
||||
*/
|
||||
function switchLanguage(langCode) {
|
||||
try {
|
||||
const targetLang = languages[langCode];
|
||||
if (!targetLang) {
|
||||
console.error(`Unsupported language: ${langCode}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPagePath = getCurrentPagePath();
|
||||
|
||||
// 跳转到对应语言路径(包含博客 /post/ 在内)
|
||||
const targetPath = targetLang.path + currentPagePath;
|
||||
|
||||
// 防止重复跳转
|
||||
if (window.location.href === window.location.origin + targetPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = targetPath;
|
||||
} catch (error) {
|
||||
console.error('Error switching language:', error);
|
||||
// 如果出错,至少尝试跳转到目标语言的首页
|
||||
const targetLang = languages[langCode];
|
||||
if (targetLang) {
|
||||
window.location.href = targetLang.path + '/';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将页面内的“站内绝对链接”修正为当前语言目录
|
||||
* 例如:在 /en/ 下,把 href="/post/" 自动改为 href="/en/post/"
|
||||
*
|
||||
* 注意:
|
||||
* - 只处理以 "/" 开头的链接(站内绝对路径)
|
||||
* - 不处理静态资源(.css/.js/.json/.png/...)
|
||||
* - 不重复添加语言前缀
|
||||
*/
|
||||
export function localizeInternalAbsoluteLinks() {
|
||||
const currentLang = getCurrentLanguage();
|
||||
if (currentLang === 'zh') return;
|
||||
|
||||
const langPrefix = languages[currentLang]?.path || '';
|
||||
if (!langPrefix) return;
|
||||
|
||||
const isStaticAsset = (href) =>
|
||||
/\.(css|js|json|png|jpg|jpeg|webp|gif|svg|ico|xml|txt|map)(\?|#|$)/i.test(href);
|
||||
|
||||
document.querySelectorAll('a[href^="/"]').forEach((a) => {
|
||||
const href = a.getAttribute('href');
|
||||
if (!href) return;
|
||||
|
||||
// 跳过协议相对 URL(//example.com)
|
||||
if (href.startsWith('//')) return;
|
||||
|
||||
// 跳过静态资源
|
||||
if (isStaticAsset(href)) return;
|
||||
|
||||
// 已经包含语言前缀则跳过
|
||||
if (
|
||||
href === langPrefix ||
|
||||
href.startsWith(langPrefix + '/') ||
|
||||
href.startsWith('/en/') ||
|
||||
href.startsWith('/ru/') ||
|
||||
href.startsWith('/es/') ||
|
||||
href.startsWith('/pt/')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理 "/" 根路径
|
||||
const normalized = href === '/' ? '/' : href;
|
||||
a.setAttribute('href', `${langPrefix}${normalized}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 全局事件监听器标志,确保只添加一次
|
||||
let globalClickHandlerAdded = false;
|
||||
|
||||
/**
|
||||
* 初始化语言切换器
|
||||
*/
|
||||
export function initLanguageSwitcher() {
|
||||
const switchers = document.querySelectorAll('.language-switcher');
|
||||
if (!switchers.length) return;
|
||||
|
||||
// 将切换函数暴露到全局(向后兼容)
|
||||
window.switchLanguage = switchLanguage;
|
||||
|
||||
// 获取当前语言
|
||||
const currentLang = getCurrentLanguage();
|
||||
const currentLangData = languages[currentLang];
|
||||
|
||||
// 添加全局点击事件监听器(只添加一次)
|
||||
if (!globalClickHandlerAdded) {
|
||||
globalClickHandlerAdded = true;
|
||||
document.addEventListener('click', (e) => {
|
||||
// 关闭所有语言下拉菜单(如果点击的不是语言切换器内部)
|
||||
const clickedSwitcher = e.target.closest('.language-switcher');
|
||||
if (!clickedSwitcher) {
|
||||
document.querySelectorAll('.language-dropdown.active').forEach((dropdown) => {
|
||||
dropdown.classList.remove('active');
|
||||
});
|
||||
} else {
|
||||
// 如果点击的是语言切换器内部,检查是否点击在下拉菜单外部
|
||||
const clickedDropdown = e.target.closest('.language-dropdown');
|
||||
const clickedButton = e.target.closest('.language-switcher-btn, #language-switcher-btn');
|
||||
if (!clickedDropdown && !clickedButton) {
|
||||
clickedSwitcher.querySelectorAll('.language-dropdown.active').forEach((dropdown) => {
|
||||
dropdown.classList.remove('active');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
switchers.forEach((wrapper) => {
|
||||
if (wrapper.dataset.langInited === '1') return;
|
||||
wrapper.dataset.langInited = '1';
|
||||
|
||||
const langButton =
|
||||
wrapper.querySelector('#language-switcher-btn') ||
|
||||
wrapper.querySelector('.language-switcher-btn');
|
||||
const langDropdown =
|
||||
wrapper.querySelector('#language-dropdown') ||
|
||||
wrapper.querySelector('.language-dropdown');
|
||||
const langButtonText =
|
||||
wrapper.querySelector('#language-switcher-text') ||
|
||||
wrapper.querySelector('.language-switcher-text');
|
||||
|
||||
if (!langButton || !langDropdown) return;
|
||||
|
||||
// 更新按钮显示
|
||||
if (langButtonText) {
|
||||
langButtonText.textContent = `${currentLangData.flag} ${currentLangData.nativeName}`;
|
||||
}
|
||||
|
||||
// 生成语言选项(不使用 inline onclick,避免被其他脚本/策略影响)
|
||||
langDropdown.innerHTML = Object.values(languages)
|
||||
.map((lang) => {
|
||||
const isActive = lang.code === currentLang;
|
||||
return `
|
||||
<a href="#"
|
||||
class="language-option ${isActive ? 'active' : ''}"
|
||||
data-lang="${lang.code}">
|
||||
<span class="language-flag">${lang.flag}</span>
|
||||
<span class="language-name">${lang.nativeName}</span>
|
||||
</a>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
// 切换下拉菜单显示
|
||||
langButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const isOpen = langDropdown.classList.contains('active');
|
||||
|
||||
// 关闭其他下拉菜单
|
||||
document
|
||||
.querySelectorAll('.dropdown-menu.active, .language-dropdown.active')
|
||||
.forEach((menu) => {
|
||||
if (menu !== langDropdown) menu.classList.remove('active');
|
||||
});
|
||||
|
||||
langDropdown.classList.toggle('active', !isOpen);
|
||||
});
|
||||
|
||||
// 点击语言选项
|
||||
langDropdown.addEventListener('click', (e) => {
|
||||
const a = e.target.closest('a[data-lang]');
|
||||
if (!a) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 防止重复点击
|
||||
if (a.classList.contains('switching')) {
|
||||
return;
|
||||
}
|
||||
a.classList.add('switching');
|
||||
|
||||
const langCode = a.getAttribute('data-lang');
|
||||
try {
|
||||
switchLanguage(langCode);
|
||||
} catch (error) {
|
||||
console.error('Error in language switch handler:', error);
|
||||
a.classList.remove('switching');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 自动初始化 - 只在 DOM 加载完成后执行
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
localizeInternalAbsoluteLinks();
|
||||
initLanguageSwitcher();
|
||||
});
|
||||
} else {
|
||||
// DOM 已经加载完成,立即执行
|
||||
localizeInternalAbsoluteLinks();
|
||||
initLanguageSwitcher();
|
||||
}
|
||||
|
||||
|
||||
// 自动初始化
|
||||
// 注意:有些页面会在脚本里提前调用 initLanguageSwitcher(),但当时 DOM 可能还没完全准备好。
|
||||
// 所以这里始终在 DOMContentLoaded 再跑一遍,确保绑定成功(内部有去重逻辑)。
|
||||
initLanguageSwitcher();
|
||||
document.addEventListener('DOMContentLoaded', initLanguageSwitcher);
|
||||
|
||||
303
src/js/mac-generator.js
Normal file
303
src/js/mac-generator.js
Normal file
@@ -0,0 +1,303 @@
|
||||
// MAC Address Generator
|
||||
|
||||
import { randomElement } from './utils.js';
|
||||
import { getConfig } from './config.js';
|
||||
|
||||
// Data cache to reduce server requests
|
||||
const dataCache = new Map();
|
||||
const CACHE_PREFIX = 'mac_data_cache_';
|
||||
const CACHE_VERSION = 'v1';
|
||||
const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
// Load OUI data from JSON file with caching (memory + localStorage)
|
||||
async function loadOuiData() {
|
||||
const filePath = 'data/macOuiData.json';
|
||||
|
||||
try {
|
||||
// Check memory cache first
|
||||
if (dataCache.has(filePath)) {
|
||||
return dataCache.get(filePath);
|
||||
}
|
||||
|
||||
// Check localStorage cache
|
||||
const cacheKey = CACHE_PREFIX + filePath;
|
||||
try {
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
if (cachedData) {
|
||||
const parsed = JSON.parse(cachedData);
|
||||
if (parsed.timestamp && (Date.now() - parsed.timestamp) < CACHE_EXPIRY) {
|
||||
dataCache.set(filePath, parsed.data);
|
||||
return parsed.data;
|
||||
} else {
|
||||
localStorage.removeItem(cacheKey);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('localStorage cache read failed:', e);
|
||||
}
|
||||
|
||||
// Get user configuration
|
||||
const config = getConfig();
|
||||
const fileName = filePath.split('/').pop();
|
||||
|
||||
// Build paths array based on configuration
|
||||
const paths = [];
|
||||
|
||||
// If user has configured a custom dataBasePath, use it first
|
||||
if (config.dataBasePath) {
|
||||
// Ensure trailing slash
|
||||
const basePath = config.dataBasePath.endsWith('/') ? config.dataBasePath : config.dataBasePath + '/';
|
||||
paths.push(basePath + fileName);
|
||||
}
|
||||
|
||||
// If autoDetectPaths is enabled (default), add automatic path detection
|
||||
// This preserves the original behavior for mockaddress.com
|
||||
if (config.autoDetectPaths !== false) {
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
// Try multiple possible paths
|
||||
// Priority: relative path (../data/) first, then absolute paths
|
||||
paths.push(
|
||||
`../data/${fileName}`, // Relative: go up one level, then into data (works for all language versions)
|
||||
`/data/${fileName}`, // Absolute path from root (for Chinese version)
|
||||
`data/${fileName}`, // Relative to current directory (fallback)
|
||||
filePath // Original path (fallback)
|
||||
);
|
||||
|
||||
// Add language-specific absolute paths if we're in a language subdirectory
|
||||
const pathParts = currentPath.split('/').filter(p => p && p !== 'index.html' && p !== '');
|
||||
if (pathParts.length >= 1 && ['en', 'ru', 'es', 'pt'].includes(pathParts[0])) {
|
||||
// We're in a language subdirectory, add language-specific absolute path
|
||||
const lang = pathParts[0];
|
||||
paths.splice(paths.length - 2, 0, `/${lang}/data/${fileName}`); // Insert before fallback paths
|
||||
}
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const response = await fetch(path, {
|
||||
// Add cache control to help browser cache
|
||||
cache: 'default'
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
dataCache.set(filePath, data);
|
||||
|
||||
// Store in localStorage
|
||||
try {
|
||||
const cacheData = {
|
||||
data: data,
|
||||
timestamp: Date.now(),
|
||||
version: CACHE_VERSION
|
||||
};
|
||||
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
|
||||
} catch (e) {
|
||||
console.warn('localStorage cache write failed:', e);
|
||||
}
|
||||
|
||||
return data;
|
||||
} else {
|
||||
lastError = `HTTP ${response.status} for ${path}`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Record error but continue trying other paths
|
||||
lastError = e.message || e.toString();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Failed to load ${filePath}. Tried paths:`, paths, 'Last error:', lastError);
|
||||
throw new Error(`Failed to load ${filePath}: ${lastError || 'All paths failed'}`);
|
||||
} catch (error) {
|
||||
console.error(`Error loading OUI data:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate random byte using crypto.getRandomValues
|
||||
function randomByte() {
|
||||
return crypto.getRandomValues(new Uint8Array(1))[0];
|
||||
}
|
||||
|
||||
// Convert OUI string (e.g., "00:03:93") to bytes
|
||||
function ouiStringToBytes(ouiString) {
|
||||
const parts = ouiString.split(':');
|
||||
return new Uint8Array([
|
||||
parseInt(parts[0], 16),
|
||||
parseInt(parts[1], 16),
|
||||
parseInt(parts[2], 16)
|
||||
]);
|
||||
}
|
||||
|
||||
// Generate MAC address
|
||||
function generateMACAddress(options = {}) {
|
||||
const {
|
||||
vendor = 'random',
|
||||
format = 'colon',
|
||||
unicast = true,
|
||||
laa = false
|
||||
} = options;
|
||||
|
||||
let bytes = new Uint8Array(6);
|
||||
|
||||
// Generate first 3 bytes (OUI)
|
||||
if (vendor !== 'random' && options.ouiDb) {
|
||||
// Search in full OUI database for matching vendor
|
||||
const matchingOuis = Object.keys(options.ouiDb).filter(oui => {
|
||||
const vendorName = options.ouiDb[oui];
|
||||
// Check if vendor name contains the search term or vice versa
|
||||
return vendorName.toLowerCase().includes(vendor.toLowerCase()) ||
|
||||
vendor.toLowerCase().includes(vendorName.toLowerCase().split(',')[0]);
|
||||
});
|
||||
|
||||
if (matchingOuis.length > 0) {
|
||||
// Use random OUI from matching vendors
|
||||
const selectedOui = randomElement(matchingOuis);
|
||||
const ouiBytes = ouiStringToBytes(selectedOui);
|
||||
bytes[0] = ouiBytes[0];
|
||||
bytes[1] = ouiBytes[1];
|
||||
bytes[2] = ouiBytes[2];
|
||||
} else {
|
||||
// Vendor not found, generate random
|
||||
crypto.getRandomValues(bytes.subarray(0, 3));
|
||||
}
|
||||
} else {
|
||||
// Completely random
|
||||
crypto.getRandomValues(bytes.subarray(0, 3));
|
||||
}
|
||||
|
||||
// Generate last 3 bytes (device identifier)
|
||||
crypto.getRandomValues(bytes.subarray(3));
|
||||
|
||||
// Apply unicast bit (LSB of first byte = 0 for unicast, 1 for multicast)
|
||||
if (unicast) {
|
||||
bytes[0] &= 0xFE; // Clear bit 0
|
||||
} else {
|
||||
bytes[0] |= 0x01; // Set bit 0
|
||||
}
|
||||
|
||||
// Apply LAA bit (bit 1 of first byte = 1 for locally administered, 0 for globally unique)
|
||||
if (laa) {
|
||||
bytes[0] |= 0x02; // Set bit 1
|
||||
} else {
|
||||
bytes[0] &= 0xFD; // Clear bit 1
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Format MAC address
|
||||
function formatMACAddress(bytes, format = 'colon') {
|
||||
const hex = Array.from(bytes).map(b =>
|
||||
b.toString(16).padStart(2, '0').toUpperCase()
|
||||
);
|
||||
|
||||
switch (format) {
|
||||
case 'colon':
|
||||
return hex.join(':');
|
||||
case 'hyphen':
|
||||
return hex.join('-');
|
||||
case 'dot':
|
||||
return `${hex[0]}${hex[1]}.${hex[2]}${hex[3]}.${hex[4]}${hex[5]}`;
|
||||
case 'none':
|
||||
return hex.join('');
|
||||
case 'space':
|
||||
return hex.join(' ');
|
||||
default:
|
||||
return hex.join(':');
|
||||
}
|
||||
}
|
||||
|
||||
// Convert MAC to IPv6 Link-Local (EUI-64)
|
||||
function macToIPv6(bytes) {
|
||||
const b = [...bytes];
|
||||
// Flip U/L bit (bit 7 of first byte)
|
||||
b[0] ^= 0x02;
|
||||
|
||||
// Insert FFFE in the middle
|
||||
const eui64 = [b[0], b[1], b[2], 0xFF, 0xFE, b[3], b[4], b[5]];
|
||||
|
||||
// Convert to IPv6 format
|
||||
const groups = [];
|
||||
for (let i = 0; i < 8; i += 2) {
|
||||
const group = ((eui64[i] << 8) | eui64[i + 1]).toString(16);
|
||||
groups.push(group);
|
||||
}
|
||||
|
||||
return 'fe80::' + groups.join(':');
|
||||
}
|
||||
|
||||
// Identify vendor from MAC address
|
||||
function identifyVendor(bytes, ouiDb) {
|
||||
if (!ouiDb) return null;
|
||||
|
||||
const ouiString = Array.from(bytes.slice(0, 3))
|
||||
.map(b => b.toString(16).padStart(2, '0').toUpperCase())
|
||||
.join(':');
|
||||
|
||||
return ouiDb[ouiString] || null;
|
||||
}
|
||||
|
||||
// Generate MAC address with all options
|
||||
export async function generateMAC(options = {}) {
|
||||
try {
|
||||
const {
|
||||
count = 1,
|
||||
vendor = 'random',
|
||||
format = 'colon',
|
||||
unicast = true,
|
||||
laa = false,
|
||||
showIPv6 = false
|
||||
} = options;
|
||||
|
||||
// 限制最大生成数量为888
|
||||
const actualCount = Math.min(888, Math.max(1, count));
|
||||
|
||||
// Load OUI data
|
||||
const ouiDb = await loadOuiData();
|
||||
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < actualCount; i++) {
|
||||
const bytes = generateMACAddress({
|
||||
vendor,
|
||||
format,
|
||||
unicast,
|
||||
laa,
|
||||
ouiDb
|
||||
});
|
||||
|
||||
const mac = formatMACAddress(bytes, format);
|
||||
const vendorName = identifyVendor(bytes, ouiDb);
|
||||
const ipv6 = showIPv6 ? macToIPv6(bytes) : null;
|
||||
|
||||
results.push({
|
||||
mac,
|
||||
vendor: vendorName,
|
||||
ipv6,
|
||||
bytes: Array.from(bytes),
|
||||
format,
|
||||
unicast,
|
||||
laa
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error generating MAC address:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get available vendors from OUI database
|
||||
export async function getAvailableVendors() {
|
||||
try {
|
||||
const ouiDb = await loadOuiData();
|
||||
const vendors = [...new Set(Object.values(ouiDb))].sort();
|
||||
return vendors;
|
||||
} catch (error) {
|
||||
console.error('Error getting vendors:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
282
src/js/storage.js
Normal file
282
src/js/storage.js
Normal file
@@ -0,0 +1,282 @@
|
||||
// Local Storage Management
|
||||
|
||||
const STORAGE_KEY = 'saved_addresses';
|
||||
const RATE_LIMIT_KEY = 'address_generation_rate_limit';
|
||||
const MIN_INTERVAL_MS = 2000; // 2秒
|
||||
const MAX_PER_HOUR = 88; // 每小时最多88次
|
||||
|
||||
// Get all saved addresses
|
||||
export function getSavedAddresses() {
|
||||
try {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch (error) {
|
||||
console.error('Error loading saved addresses:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract core address fields for comparison (exclude id, savedAt)
|
||||
function getAddressCore(address) {
|
||||
const { id, savedAt, ...core } = address;
|
||||
return core;
|
||||
}
|
||||
|
||||
// Save address
|
||||
export function saveAddress(address) {
|
||||
try {
|
||||
const addresses = getSavedAddresses();
|
||||
|
||||
// Check for duplicates based on core address fields (excluding id and savedAt)
|
||||
const addressCore = getAddressCore(address);
|
||||
const addressCoreString = JSON.stringify(addressCore);
|
||||
const isDuplicate = addresses.some(addr => {
|
||||
const savedCore = getAddressCore(addr);
|
||||
return JSON.stringify(savedCore) === addressCoreString;
|
||||
});
|
||||
|
||||
if (isDuplicate) {
|
||||
return { success: false, message: '该地址已保存,请勿重复添加' };
|
||||
}
|
||||
|
||||
addresses.push({
|
||||
...address,
|
||||
id: Date.now().toString(),
|
||||
savedAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(addresses));
|
||||
return { success: true, message: '地址已成功保存' };
|
||||
} catch (error) {
|
||||
console.error('Error saving address:', error);
|
||||
return { success: false, message: '保存地址时出错' };
|
||||
}
|
||||
}
|
||||
|
||||
// Delete address by id
|
||||
export function deleteAddress(id) {
|
||||
try {
|
||||
const addresses = getSavedAddresses();
|
||||
const filtered = addresses.filter(addr => addr.id !== id);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
|
||||
return { success: true, message: '删除成功' };
|
||||
} catch (error) {
|
||||
console.error('Error deleting address:', error);
|
||||
return { success: false, message: '删除地址时出错' };
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all addresses
|
||||
export function clearAllAddresses() {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
return { success: true, message: '已清空所有地址' };
|
||||
} catch (error) {
|
||||
console.error('Error clearing addresses:', error);
|
||||
return { success: false, message: '清空地址时出错' };
|
||||
}
|
||||
}
|
||||
|
||||
// Get saved addresses count
|
||||
export function getSavedCount() {
|
||||
return getSavedAddresses().length;
|
||||
}
|
||||
|
||||
// Export to CSV
|
||||
export function exportToCSV() {
|
||||
try {
|
||||
const addresses = getSavedAddresses();
|
||||
|
||||
if (addresses.length === 0) {
|
||||
return { success: false, message: '没有保存的地址' };
|
||||
}
|
||||
|
||||
// CSV header
|
||||
const headers = ['姓名', '性别', '电话', '电子邮件', '完整地址'];
|
||||
const rows = addresses.map(addr => {
|
||||
const name = `${addr.firstName || ''} ${addr.lastName || ''}`.trim();
|
||||
const gender = addr.gender || '';
|
||||
const phone = addr.phone || '';
|
||||
const email = addr.email || '';
|
||||
const fullAddress = addr.fullAddress || formatAddress(addr);
|
||||
|
||||
return [
|
||||
name,
|
||||
gender,
|
||||
phone,
|
||||
email,
|
||||
fullAddress
|
||||
].map(field => `"${String(field).replace(/"/g, '""')}"`).join(',');
|
||||
});
|
||||
|
||||
const csv = [headers.map(h => `"${h}"`).join(','), ...rows].join('\n');
|
||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `地址列表_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
return { success: true, message: 'CSV文件已下载' };
|
||||
} catch (error) {
|
||||
console.error('Error exporting CSV:', error);
|
||||
return { success: false, message: '导出CSV失败' };
|
||||
}
|
||||
}
|
||||
|
||||
// Export to JSON
|
||||
export function exportToJSON() {
|
||||
try {
|
||||
const addresses = getSavedAddresses();
|
||||
|
||||
if (addresses.length === 0) {
|
||||
return { success: false, message: '没有保存的地址' };
|
||||
}
|
||||
|
||||
const json = JSON.stringify(addresses, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `地址列表_${new Date().toISOString().split('T')[0]}.json`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
return { success: true, message: 'JSON文件已下载' };
|
||||
} catch (error) {
|
||||
console.error('Error exporting JSON:', error);
|
||||
return { success: false, message: '导出JSON失败' };
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limiting functions
|
||||
function getRateLimitData() {
|
||||
try {
|
||||
const data = localStorage.getItem(RATE_LIMIT_KEY);
|
||||
return data ? JSON.parse(data) : { lastGeneration: 0, hourlyGenerations: [] };
|
||||
} catch (error) {
|
||||
console.error('Error loading rate limit data:', error);
|
||||
return { lastGeneration: 0, hourlyGenerations: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function saveRateLimitData(data) {
|
||||
try {
|
||||
localStorage.setItem(RATE_LIMIT_KEY, JSON.stringify(data));
|
||||
} catch (error) {
|
||||
console.error('Error saving rate limit data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean old generation records (older than 1 hour)
|
||||
function cleanOldGenerations(generations) {
|
||||
const oneHourAgo = Date.now() - (60 * 60 * 1000);
|
||||
return generations.filter(timestamp => timestamp > oneHourAgo);
|
||||
}
|
||||
|
||||
// Check if generation is allowed
|
||||
export function checkGenerationRateLimit() {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const rateLimitData = getRateLimitData();
|
||||
|
||||
// Clean old records
|
||||
rateLimitData.hourlyGenerations = cleanOldGenerations(rateLimitData.hourlyGenerations);
|
||||
|
||||
// Check 2 second interval (skip if first time, lastGeneration is 0)
|
||||
if (rateLimitData.lastGeneration > 0) {
|
||||
const timeSinceLastGeneration = now - rateLimitData.lastGeneration;
|
||||
if (timeSinceLastGeneration < MIN_INTERVAL_MS) {
|
||||
const remainingSeconds = Math.ceil((MIN_INTERVAL_MS - timeSinceLastGeneration) / 1000);
|
||||
return {
|
||||
allowed: false,
|
||||
message: `请等待 ${remainingSeconds} 秒后再生成`,
|
||||
remainingSeconds: remainingSeconds
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check hourly limit
|
||||
if (rateLimitData.hourlyGenerations.length >= MAX_PER_HOUR) {
|
||||
const oldestGeneration = rateLimitData.hourlyGenerations[0];
|
||||
const timeUntilOldestExpires = (oldestGeneration + (60 * 60 * 1000)) - now;
|
||||
const remainingMinutes = Math.ceil(timeUntilOldestExpires / (60 * 1000));
|
||||
return {
|
||||
allowed: false,
|
||||
message: `每小时最多生成 ${MAX_PER_HOUR} 次,请等待 ${remainingMinutes} 分钟`,
|
||||
remainingMinutes: remainingMinutes
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
} catch (error) {
|
||||
console.error('Error checking rate limit:', error);
|
||||
// If there's an error, allow generation to prevent blocking users
|
||||
return { allowed: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Record generation (used for both generation and save operations)
|
||||
export function recordGeneration() {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const rateLimitData = getRateLimitData();
|
||||
|
||||
// Clean old records
|
||||
rateLimitData.hourlyGenerations = cleanOldGenerations(rateLimitData.hourlyGenerations);
|
||||
|
||||
// Update last generation time
|
||||
rateLimitData.lastGeneration = now;
|
||||
|
||||
// Add current generation to hourly list
|
||||
rateLimitData.hourlyGenerations.push(now);
|
||||
|
||||
// Save updated data
|
||||
saveRateLimitData(rateLimitData);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
remainingInHour: MAX_PER_HOUR - rateLimitData.hourlyGenerations.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error recording generation:', error);
|
||||
// Return success even if recording fails to prevent blocking
|
||||
return {
|
||||
success: true,
|
||||
remainingInHour: MAX_PER_HOUR
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if save can be done without recording (if it's within 2 seconds of last generation)
|
||||
export function canSaveWithoutRecording() {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const rateLimitData = getRateLimitData();
|
||||
// If lastGeneration is 0, it's the first time, so save needs to be recorded
|
||||
if (rateLimitData.lastGeneration === 0) {
|
||||
return false;
|
||||
}
|
||||
const timeSinceLastGeneration = now - rateLimitData.lastGeneration;
|
||||
|
||||
// If save happens within 2 seconds of generation, it's part of the same operation
|
||||
return timeSinceLastGeneration < MIN_INTERVAL_MS;
|
||||
} catch (error) {
|
||||
console.error('Error checking save without recording:', error);
|
||||
// On error, require recording to be safe
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to format address
|
||||
function formatAddress(address) {
|
||||
if (typeof address === 'string') {
|
||||
return address;
|
||||
}
|
||||
if (address.street && address.city && address.state && address.zip) {
|
||||
return `${address.street}, ${address.city}, ${address.state} ${address.zip}`;
|
||||
}
|
||||
return JSON.stringify(address);
|
||||
}
|
||||
|
||||
133
src/js/utils.js
Normal file
133
src/js/utils.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// Utility Functions
|
||||
|
||||
// Copy text to clipboard
|
||||
export function copyToClipboard(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
return navigator.clipboard.writeText(text).then(() => {
|
||||
return true;
|
||||
}).catch(() => {
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
return Promise.resolve(true);
|
||||
} catch (err) {
|
||||
document.body.removeChild(textarea);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate random number between min and max (inclusive)
|
||||
export function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
// Get random element from array
|
||||
export function randomElement(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
// Generate random phone number
|
||||
export function generatePhoneNumber(areaCode) {
|
||||
const exchange = randomInt(200, 999);
|
||||
const number = randomInt(1000, 9999);
|
||||
return `${areaCode}-${exchange}-${number}`;
|
||||
}
|
||||
|
||||
// Generate random email
|
||||
export function generateEmail(firstName, lastName) {
|
||||
const domains = ['gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'icloud.com'];
|
||||
const domain = randomElement(domains);
|
||||
const randomNum = randomInt(100, 999);
|
||||
|
||||
// Clean and validate names - remove spaces, dots, and ensure non-empty
|
||||
const cleanFirstName = (firstName || '').toString().trim().toLowerCase().replace(/[.\s]/g, '') || 'user';
|
||||
const cleanLastName = (lastName || '').toString().trim().toLowerCase().replace(/[.\s]/g, '') || 'name';
|
||||
|
||||
// Ensure names are not empty
|
||||
const firstPart = cleanFirstName || 'user';
|
||||
const lastPart = cleanLastName || 'name';
|
||||
|
||||
// Build email: firstnamelastname123@domain.com (no dot between names)
|
||||
return `${firstPart}${lastPart}${randomNum}@${domain}`;
|
||||
}
|
||||
|
||||
// Format address for display
|
||||
export function formatAddress(address) {
|
||||
if (typeof address === 'string') {
|
||||
return address;
|
||||
}
|
||||
if (address.street && address.city && address.state && address.zip) {
|
||||
return `${address.street}, ${address.city}, ${address.state} ${address.zip}`;
|
||||
}
|
||||
return JSON.stringify(address);
|
||||
}
|
||||
|
||||
// Show toast notification
|
||||
export function showToast(message, type = 'success') {
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
right: 20px;
|
||||
background-color: ${type === 'success' ? '#10b981' : '#ef4444'};
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(toast);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Add CSS animation if not exists
|
||||
if (!document.getElementById('toast-animations')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'toast-animations';
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user