Add files via upload

This commit is contained in:
hotbob011
2026-03-09 00:58:23 +08:00
committed by GitHub
parent 9bd5d804e2
commit 82cc9f3022
7 changed files with 4680 additions and 0 deletions

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

File diff suppressed because it is too large Load Diff

49
src/js/config.js Normal file
View 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
View 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
View 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
View 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
View 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);
}