412 lines
14 KiB
JavaScript
Executable File
412 lines
14 KiB
JavaScript
Executable File
class PasswordGenerator {
|
|
constructor() {
|
|
this.apiUrl = 'https://60s.api.shumengya.top/v2/password';
|
|
this.loadStartTime = 0;
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.bindEvents();
|
|
this.updateLengthDisplay();
|
|
this.preloadResources();
|
|
}
|
|
|
|
preloadResources() {
|
|
// 预连接API服务器
|
|
const link = document.createElement('link');
|
|
link.rel = 'preconnect';
|
|
link.href = 'https://60s.api.shumengya.top';
|
|
document.head.appendChild(link);
|
|
}
|
|
|
|
bindEvents() {
|
|
// 长度滑块事件
|
|
const lengthSlider = document.getElementById('length');
|
|
lengthSlider.addEventListener('input', () => this.updateLengthDisplay());
|
|
|
|
// 生成按钮事件
|
|
const generateBtn = document.getElementById('generateBtn');
|
|
generateBtn.addEventListener('click', () => this.generatePassword());
|
|
|
|
// 复制按钮事件
|
|
const copyBtn = document.getElementById('copyBtn');
|
|
copyBtn.addEventListener('click', () => this.copyPassword());
|
|
|
|
// 重试按钮事件
|
|
const retryBtn = document.getElementById('retryBtn');
|
|
retryBtn.addEventListener('click', () => this.generatePassword());
|
|
|
|
// 复选框变化事件
|
|
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
|
|
checkboxes.forEach(checkbox => {
|
|
checkbox.addEventListener('change', () => this.validateForm());
|
|
});
|
|
|
|
// 键盘快捷键
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.ctrlKey && e.key === 'Enter') {
|
|
e.preventDefault();
|
|
this.generatePassword();
|
|
}
|
|
if (e.ctrlKey && e.key === 'c' && document.activeElement.id === 'passwordResult') {
|
|
this.copyPassword();
|
|
}
|
|
});
|
|
}
|
|
|
|
updateLengthDisplay() {
|
|
const lengthSlider = document.getElementById('length');
|
|
const lengthDisplay = document.getElementById('lengthDisplay');
|
|
lengthDisplay.textContent = lengthSlider.value;
|
|
}
|
|
|
|
validateForm() {
|
|
const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked');
|
|
const generateBtn = document.getElementById('generateBtn');
|
|
|
|
// 至少需要选择一种字符类型
|
|
const hasCharacterType = Array.from(checkboxes).some(cb =>
|
|
['numbers', 'uppercase', 'lowercase', 'symbols'].includes(cb.id)
|
|
);
|
|
|
|
generateBtn.disabled = !hasCharacterType;
|
|
|
|
if (!hasCharacterType) {
|
|
this.showToast('请至少选择一种字符类型', 'warning');
|
|
}
|
|
}
|
|
|
|
async generatePassword() {
|
|
this.loadStartTime = Date.now();
|
|
|
|
try {
|
|
this.showLoading(true);
|
|
this.hideError();
|
|
|
|
const params = this.getFormParams();
|
|
const password = await this.callAPI(params);
|
|
|
|
if (password) {
|
|
this.displayPassword(password, params);
|
|
this.showToast('密码生成成功!', 'success');
|
|
|
|
const loadTime = Date.now() - this.loadStartTime;
|
|
console.log(`密码生成完成,耗时: ${loadTime}ms`);
|
|
}
|
|
} catch (error) {
|
|
console.error('生成密码失败:', error);
|
|
this.showError(error.message || '生成密码时发生错误,请重试');
|
|
} finally {
|
|
this.showLoading(false);
|
|
}
|
|
}
|
|
|
|
getFormParams() {
|
|
const length = document.getElementById('length').value;
|
|
const numbers = document.getElementById('numbers').checked;
|
|
const uppercase = document.getElementById('uppercase').checked;
|
|
const lowercase = document.getElementById('lowercase').checked;
|
|
const symbols = document.getElementById('symbols').checked;
|
|
const excludeSimilar = document.getElementById('excludeSimilar').checked;
|
|
const excludeAmbiguous = document.getElementById('excludeAmbiguous').checked;
|
|
|
|
return {
|
|
length: parseInt(length),
|
|
numbers: numbers ? 'true' : 'false',
|
|
uppercase: uppercase ? 'true' : 'false',
|
|
lowercase: lowercase ? 'true' : 'false',
|
|
symbols: symbols ? 'true' : 'false',
|
|
exclude_similar: excludeSimilar ? 'true' : 'false',
|
|
exclude_ambiguous: excludeAmbiguous ? 'true' : 'false',
|
|
encoding: 'json'
|
|
};
|
|
}
|
|
|
|
async callAPI(params) {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
|
|
try {
|
|
const url = new URL(this.apiUrl);
|
|
Object.keys(params).forEach(key => {
|
|
if (params[key] !== undefined && params[key] !== null) {
|
|
url.searchParams.append(key, params[key]);
|
|
}
|
|
});
|
|
|
|
const response = await fetch(url.toString(), {
|
|
method: 'GET',
|
|
signal: controller.signal,
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'User-Agent': 'PasswordGenerator/1.0'
|
|
}
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.code === 200 && data.data && data.data.password) {
|
|
return data.data.password;
|
|
} else {
|
|
throw new Error(data.message || '服务器返回了无效的密码数据');
|
|
}
|
|
} catch (error) {
|
|
clearTimeout(timeoutId);
|
|
|
|
if (error.name === 'AbortError') {
|
|
throw new Error('请求超时,请检查网络连接后重试');
|
|
}
|
|
|
|
if (error.message.includes('Failed to fetch')) {
|
|
throw new Error('网络连接失败,请检查网络后重试');
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
displayPassword(password, params) {
|
|
// 显示结果容器
|
|
const resultContainer = document.getElementById('resultContainer');
|
|
const errorContainer = document.getElementById('errorContainer');
|
|
|
|
resultContainer.style.display = 'block';
|
|
errorContainer.style.display = 'none';
|
|
|
|
// 设置密码
|
|
const passwordInput = document.getElementById('passwordResult');
|
|
passwordInput.value = password;
|
|
|
|
// 计算并显示密码信息
|
|
this.updatePasswordInfo(password, params);
|
|
|
|
// 滚动到结果区域
|
|
resultContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
updatePasswordInfo(password, params) {
|
|
// 基本信息
|
|
document.getElementById('infoLength').textContent = password.length;
|
|
document.getElementById('infoEntropy').textContent = this.calculateEntropy(password).toFixed(1);
|
|
|
|
// 密码强度
|
|
const strength = this.calculateStrength(password);
|
|
const strengthElement = document.getElementById('infoStrength');
|
|
strengthElement.textContent = strength.text;
|
|
strengthElement.className = `info-value strength ${strength.class}`;
|
|
|
|
// 字符类型统计
|
|
const stats = this.analyzeCharacters(password);
|
|
document.getElementById('infoNumbers').textContent = stats.numbers;
|
|
document.getElementById('infoUppercase').textContent = stats.uppercase;
|
|
document.getElementById('infoLowercase').textContent = stats.lowercase;
|
|
document.getElementById('infoSymbols').textContent = stats.symbols;
|
|
|
|
// 使用的字符集
|
|
this.updateCharacterSets(params);
|
|
|
|
// 破解时间估算
|
|
document.getElementById('infoCrackTime').textContent = this.estimateCrackTime(password);
|
|
}
|
|
|
|
calculateEntropy(password) {
|
|
const charset = this.getCharsetSize(password);
|
|
return Math.log2(Math.pow(charset, password.length));
|
|
}
|
|
|
|
getCharsetSize(password) {
|
|
let size = 0;
|
|
if (/[0-9]/.test(password)) size += 10;
|
|
if (/[a-z]/.test(password)) size += 26;
|
|
if (/[A-Z]/.test(password)) size += 26;
|
|
if (/[^a-zA-Z0-9]/.test(password)) size += 32;
|
|
return size;
|
|
}
|
|
|
|
calculateStrength(password) {
|
|
const entropy = this.calculateEntropy(password);
|
|
|
|
if (entropy < 30) {
|
|
return { text: '弱', class: 'weak' };
|
|
} else if (entropy < 50) {
|
|
return { text: '中等', class: 'medium' };
|
|
} else if (entropy < 70) {
|
|
return { text: '强', class: 'strong' };
|
|
} else {
|
|
return { text: '非常强', class: 'very-strong' };
|
|
}
|
|
}
|
|
|
|
analyzeCharacters(password) {
|
|
return {
|
|
numbers: (password.match(/[0-9]/g) || []).length,
|
|
uppercase: (password.match(/[A-Z]/g) || []).length,
|
|
lowercase: (password.match(/[a-z]/g) || []).length,
|
|
symbols: (password.match(/[^a-zA-Z0-9]/g) || []).length
|
|
};
|
|
}
|
|
|
|
updateCharacterSets(params) {
|
|
const setsList = document.getElementById('setsList');
|
|
const sets = [];
|
|
|
|
if (params.numbers === 'true') sets.push('数字 (0-9)');
|
|
if (params.uppercase === 'true') sets.push('大写字母 (A-Z)');
|
|
if (params.lowercase === 'true') sets.push('小写字母 (a-z)');
|
|
if (params.symbols === 'true') sets.push('特殊字符 (!@#$...)');
|
|
|
|
setsList.innerHTML = sets.map(set => `<span class="set-item">${set}</span>`).join('');
|
|
}
|
|
|
|
estimateCrackTime(password) {
|
|
const charset = this.getCharsetSize(password);
|
|
const combinations = Math.pow(charset, password.length);
|
|
const guessesPerSecond = 1e9; // 假设每秒10亿次尝试
|
|
const secondsToCrack = combinations / (2 * guessesPerSecond);
|
|
|
|
if (secondsToCrack < 60) {
|
|
return '不到1分钟';
|
|
} else if (secondsToCrack < 3600) {
|
|
return `${Math.ceil(secondsToCrack / 60)}分钟`;
|
|
} else if (secondsToCrack < 86400) {
|
|
return `${Math.ceil(secondsToCrack / 3600)}小时`;
|
|
} else if (secondsToCrack < 31536000) {
|
|
return `${Math.ceil(secondsToCrack / 86400)}天`;
|
|
} else if (secondsToCrack < 31536000000) {
|
|
return `${Math.ceil(secondsToCrack / 31536000)}年`;
|
|
} else {
|
|
return '数千年以上';
|
|
}
|
|
}
|
|
|
|
async copyPassword() {
|
|
const passwordInput = document.getElementById('passwordResult');
|
|
|
|
try {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(passwordInput.value);
|
|
} else {
|
|
// 降级方案
|
|
passwordInput.select();
|
|
passwordInput.setSelectionRange(0, 99999);
|
|
document.execCommand('copy');
|
|
}
|
|
|
|
this.showToast('密码已复制到剪贴板!', 'success');
|
|
|
|
// 复制按钮反馈
|
|
const copyBtn = document.getElementById('copyBtn');
|
|
const originalText = copyBtn.innerHTML;
|
|
copyBtn.innerHTML = '✓ 已复制';
|
|
copyBtn.style.background = '#2e7d32';
|
|
|
|
setTimeout(() => {
|
|
copyBtn.innerHTML = originalText;
|
|
copyBtn.style.background = '';
|
|
}, 2000);
|
|
|
|
} catch (error) {
|
|
console.error('复制失败:', error);
|
|
this.showToast('复制失败,请手动选择密码', 'error');
|
|
}
|
|
}
|
|
|
|
showLoading(show) {
|
|
const generateBtn = document.getElementById('generateBtn');
|
|
|
|
if (show) {
|
|
generateBtn.disabled = true;
|
|
generateBtn.innerHTML = '<span style="display: inline-block; animation: spin 1s linear infinite;">⟳</span> 生成中...';
|
|
} else {
|
|
generateBtn.disabled = false;
|
|
generateBtn.innerHTML = '🔐 生成密码';
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
const errorContainer = document.getElementById('errorContainer');
|
|
const resultContainer = document.getElementById('resultContainer');
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
|
|
errorMessage.textContent = message;
|
|
errorContainer.style.display = 'block';
|
|
resultContainer.style.display = 'none';
|
|
|
|
errorContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
hideError() {
|
|
const errorContainer = document.getElementById('errorContainer');
|
|
errorContainer.style.display = 'none';
|
|
}
|
|
|
|
showToast(message, type = 'info') {
|
|
// 移除现有的toast
|
|
const existingToast = document.querySelector('.toast');
|
|
if (existingToast) {
|
|
existingToast.remove();
|
|
}
|
|
|
|
const toast = document.createElement('div');
|
|
toast.className = 'toast';
|
|
toast.textContent = message;
|
|
|
|
// 根据类型设置颜色
|
|
const colors = {
|
|
success: '#4caf50',
|
|
error: '#f44336',
|
|
warning: '#ff9800',
|
|
info: '#2196f3'
|
|
};
|
|
|
|
toast.style.background = colors[type] || colors.info;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
// 3秒后自动移除
|
|
setTimeout(() => {
|
|
if (toast.parentNode) {
|
|
toast.remove();
|
|
}
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
// 添加旋转动画样式
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// 页面加载完成后初始化
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
new PasswordGenerator();
|
|
});
|
|
|
|
// 页面可见性变化时的处理
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible') {
|
|
// 页面重新可见时,可以进行一些刷新操作
|
|
console.log('页面重新可见');
|
|
}
|
|
});
|
|
|
|
// 错误处理
|
|
window.addEventListener('error', (event) => {
|
|
console.error('全局错误:', event.error);
|
|
});
|
|
|
|
window.addEventListener('unhandledrejection', (event) => {
|
|
console.error('未处理的Promise拒绝:', event.reason);
|
|
event.preventDefault();
|
|
}); |