Add a password generator. 增加密码生成器

This commit is contained in:
star7th
2025-11-03 23:04:25 +08:00
parent 48bdb5905d
commit 84a70ee1ef
8 changed files with 390 additions and 0 deletions
+302
View File
@@ -0,0 +1,302 @@
'use client';
import React, { useEffect, useMemo, useState } from 'react';
import ToolHeader from '@/components/ToolHeader';
import { useLanguage } from '@/context/LanguageContext';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCopy, faDownload, faKey, faRefresh, faTrash, faCheck } from '@fortawesome/free-solid-svg-icons';
const styles = {
card: "card p-6",
input: "search-input w-full",
label: "block mb-1 text-sm text-tertiary",
section: "mb-6",
row: "grid grid-cols-1 md:grid-cols-2 gap-4",
checkbox: "flex items-center gap-2 text-sm text-tertiary",
numberInput: "search-input w-full",
btn: "btn-secondary text-xs px-3 py-1",
primaryBtn: "btn-primary text-xs px-3 py-1",
listItem: "flex items-center justify-between p-2 border border-purple/20 rounded-md bg-purple/5",
code: "font-mono break-all",
hint: "text-xs text-tertiary",
chips: "flex flex-wrap gap-2",
chip: "px-2 py-1 rounded border border-purple/30 text-xs text-tertiary bg-purple/10",
iconButton: "text-tertiary hover:text-purple transition-colors",
};
type CharsetOptionKey = 'uppercase' | 'lowercase' | 'digits' | 'symbols';
const DEFAULT_SYMBOLS = "!@#$%^&*()-_=+[]{};:,.<>?/\\|";
const SIMILAR_CHARS = "Il1O0o";
const AMBIGUOUS_SYMBOLS = "{}[]()/\\'\"`~,;:.<>";
export default function PasswordGenerator() {
const { t } = useLanguage();
const [length, setLength] = useState(12);
const [count, setCount] = useState(10);
const [forceAllSets, setForceAllSets] = useState(true);
const [includeSets, setIncludeSets] = useState<Record<CharsetOptionKey, boolean>>({
uppercase: true,
lowercase: true,
digits: true,
symbols: true,
});
const [customInclude, setCustomInclude] = useState('');
const [excludeChars, setExcludeChars] = useState(SIMILAR_CHARS);
const [avoidSimilar, setAvoidSimilar] = useState(true);
const [avoidAmbiguous, setAvoidAmbiguous] = useState(false);
const [results, setResults] = useState<string[]>([]);
const [copiedIndex, setCopiedIndex] = useState<number | 'all' | null>(null);
useEffect(() => {
generate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const charset = useMemo(() => {
let chars = '';
if (includeSets.uppercase) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (includeSets.lowercase) chars += 'abcdefghijklmnopqrstuvwxyz';
if (includeSets.digits) chars += '0123456789';
if (includeSets.symbols) chars += DEFAULT_SYMBOLS;
if (customInclude) chars += customInclude;
let excludes = excludeChars || '';
if (avoidSimilar) excludes += SIMILAR_CHARS;
if (avoidAmbiguous) excludes += AMBIGUOUS_SYMBOLS;
if (excludes) {
const excludeSet = new Set(excludes.split(''));
chars = Array.from(new Set(chars.split('')))
.filter((c) => !excludeSet.has(c))
.join('');
}
return chars;
}, [includeSets, customInclude, excludeChars, avoidSimilar, avoidAmbiguous]);
const ensureAllSets = (candidate: string): boolean => {
if (!forceAllSets) return true;
const checks = [
!includeSets.uppercase || /[A-Z]/.test(candidate),
!includeSets.lowercase || /[a-z]/.test(candidate),
!includeSets.digits || /[0-9]/.test(candidate),
!includeSets.symbols || new RegExp(`[${escapeForRegex(DEFAULT_SYMBOLS)}]`).test(candidate),
];
return checks.every(Boolean);
};
const generateOne = (pool: string, len: number): string => {
if (!pool) return '';
const array = new Uint32Array(len);
if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
window.crypto.getRandomValues(array);
} else {
for (let i = 0; i < len; i++) array[i] = Math.floor(Math.random() * 0xffffffff);
}
const chars = [] as string[];
for (let i = 0; i < len; i++) {
const idx = array[i] % pool.length;
chars.push(pool[idx]);
}
return chars.join('');
};
const generate = () => {
const pool = charset;
const list: string[] = [];
const target = Math.min(Math.max(count, 1), 100);
const len = Math.min(Math.max(length, 4), 128);
let attempts = 0;
while (list.length < target && attempts < target * 100) {
attempts++;
const candidate = generateOne(pool, len);
if (candidate && ensureAllSets(candidate)) {
list.push(candidate);
}
}
setResults(list);
};
const copyOne = async (idx: number) => {
try {
await navigator.clipboard.writeText(results[idx] || '');
setCopiedIndex(idx);
setTimeout(() => setCopiedIndex(null), 1500);
} catch {}
};
const copyAll = async () => {
try {
await navigator.clipboard.writeText(results.join('\n'));
setCopiedIndex('all');
setTimeout(() => setCopiedIndex(null), 1500);
} catch {}
};
const downloadTxt = () => {
const blob = new Blob([results.join('\n')], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'passwords.txt';
a.click();
URL.revokeObjectURL(url);
};
const toggleSet = (key: CharsetOptionKey) => {
setIncludeSets((prev) => ({ ...prev, [key]: !prev[key] }));
};
const reset = () => {
setLength(12);
setCount(10);
setForceAllSets(true);
setIncludeSets({ uppercase: true, lowercase: true, digits: true, symbols: true });
setCustomInclude('');
setExcludeChars(SIMILAR_CHARS);
setAvoidSimilar(true);
setAvoidAmbiguous(false);
setResults([]);
};
return (
<div className="container mx-auto px-4 py-8 max-w-5xl">
<ToolHeader
toolCode="password_generator"
icon={faKey}
title={t('tools.password_generator.title')}
description={t('tools.password_generator.description')}
/>
<div className={styles.section}>
<div className={styles.card}>
<div className={styles.row}>
<div>
<label className={styles.label}>{t('tools.password_generator.length')}</label>
<input
className={styles.numberInput}
type="number"
min={4}
max={128}
value={length}
onChange={(e) => setLength(parseInt(e.target.value || '0') || 0)}
/>
<div className={styles.hint}>{t('tools.password_generator.length_hint')}</div>
</div>
<div>
<label className={styles.label}>{t('tools.password_generator.count')}</label>
<input
className={styles.numberInput}
type="number"
min={1}
max={100}
value={count}
onChange={(e) => setCount(parseInt(e.target.value || '0') || 0)}
/>
<div className={styles.hint}>{t('tools.password_generator.count_hint')}</div>
</div>
</div>
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className={styles.checkbox}>
<input id="uppercase" type="checkbox" checked={includeSets.uppercase} onChange={() => toggleSet('uppercase')} />
<label htmlFor="uppercase">{t('tools.password_generator.set_uppercase')}</label>
</div>
<div className={styles.checkbox}>
<input id="lowercase" type="checkbox" checked={includeSets.lowercase} onChange={() => toggleSet('lowercase')} />
<label htmlFor="lowercase">{t('tools.password_generator.set_lowercase')}</label>
</div>
<div className={styles.checkbox}>
<input id="digits" type="checkbox" checked={includeSets.digits} onChange={() => toggleSet('digits')} />
<label htmlFor="digits">{t('tools.password_generator.set_digits')}</label>
</div>
<div className={styles.checkbox}>
<input id="symbols" type="checkbox" checked={includeSets.symbols} onChange={() => toggleSet('symbols')} />
<label htmlFor="symbols">{t('tools.password_generator.set_symbols')}</label>
</div>
<div className={styles.checkbox}>
<input id="forceAll" type="checkbox" checked={forceAllSets} onChange={() => setForceAllSets(!forceAllSets)} />
<label htmlFor="forceAll">{t('tools.password_generator.force_all_sets')}</label>
</div>
</div>
<div className="space-y-3">
<div>
<label className={styles.label}>{t('tools.password_generator.custom_include')}</label>
<input
className={styles.input}
type="text"
value={customInclude}
onChange={(e) => setCustomInclude(e.target.value)}
placeholder={t('tools.password_generator.custom_include_placeholder')}
/>
<div className={styles.hint}>{t('tools.password_generator.custom_include_hint')}</div>
</div>
<div>
<label className={styles.label}>{t('tools.password_generator.exclude_chars')}</label>
<input
className={styles.input}
type="text"
value={excludeChars}
onChange={(e) => setExcludeChars(e.target.value)}
placeholder={t('tools.password_generator.exclude_chars_placeholder')}
/>
<div className={styles.chips}>
<button className={styles.chip} onClick={() => setAvoidSimilar(!avoidSimilar)}>
{avoidSimilar ? t('tools.password_generator.avoid_similar_on') : t('tools.password_generator.avoid_similar_off')}
</button>
<button className={styles.chip} onClick={() => setAvoidAmbiguous(!avoidAmbiguous)}>
{avoidAmbiguous ? t('tools.password_generator.avoid_ambiguous_on') : t('tools.password_generator.avoid_ambiguous_off')}
</button>
</div>
</div>
</div>
</div>
<div className="mt-6 flex flex-wrap gap-2">
<button className={styles.primaryBtn} onClick={generate}>
<FontAwesomeIcon icon={faRefresh} className="mr-2" /> {t('tools.password_generator.generate')}
</button>
<button className={styles.btn} onClick={copyAll}>
<FontAwesomeIcon icon={copiedIndex === 'all' ? faCheck : faCopy} className="mr-2" /> {t('tools.password_generator.copy_all')}
</button>
<button className={styles.btn} onClick={downloadTxt}>
<FontAwesomeIcon icon={faDownload} className="mr-2" /> {t('tools.password_generator.download')}
</button>
<button className={styles.btn} onClick={reset}>
<FontAwesomeIcon icon={faTrash} className="mr-2" /> {t('tools.password_generator.reset')}
</button>
</div>
</div>
</div>
<div className={styles.section}>
<div className={styles.card}>
<h2 className="text-xl font-medium mb-4">{t('tools.password_generator.results')}</h2>
<div className="space-y-2">
{results.length === 0 && (
<div className={styles.hint}>{t('tools.password_generator.no_result')}</div>
)}
{results.map((pwd, idx) => (
<div className={styles.listItem} key={idx}>
<div className={styles.code}>{pwd}</div>
<button className={styles.iconButton} onClick={() => copyOne(idx)} title={t('tools.password_generator.copy_one')}>
<FontAwesomeIcon icon={copiedIndex === idx ? faCheck : faCopy} />
</button>
</div>
))}
</div>
</div>
</div>
</div>
);
}
function escapeForRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
+1
View File
@@ -33,6 +33,7 @@ export const en = {
image_to_ico: tools.image_to_ico.en,
cron_generator: tools.cron_generator.en,
icon_designer: tools.icon_designer.en,
password_generator: tools.password_generator.en,
pdf_converter: tools.pdf_converter.en,
pdf_manager: tools.pdf_manager.en,
+2
View File
@@ -28,6 +28,7 @@ import imageToIco from './image_to_ico';
import cronGenerator from './cron_generator';
import { iconDesigner } from './icon_designer';
import { pdfConverterI18n } from './pdf_converter';
import { passwordGenerator } from './password_generator';
import { pdfManager } from './pdf_manager';
import { pdfCompressor } from './pdf_compressor';
@@ -61,6 +62,7 @@ export const tools = {
image_to_ico: imageToIco,
cron_generator: cronGenerator,
icon_designer: iconDesigner,
password_generator: passwordGenerator,
pdf_converter: pdfConverterI18n,
@@ -0,0 +1,33 @@
export const passwordGeneratorEn = {
title: 'Random Password Generator',
description: 'Generate strong random passwords with custom charset, length and count',
length: 'Password Length',
length_hint: 'Range 4 - 128, 12+ recommended for strength',
count: 'Quantity',
count_hint: 'Range 1 - 100',
set_uppercase: 'Include uppercase A-Z',
set_lowercase: 'Include lowercase a-z',
set_digits: 'Include digits 0-9',
set_symbols: 'Include symbols',
force_all_sets: 'Enforce at least one of each selected type',
custom_include: 'Custom characters to include',
custom_include_placeholder: 'e.g. €¥✓',
custom_include_hint: 'These characters will be added into the pool',
exclude_chars: 'Exclude characters',
exclude_chars_placeholder: 'Characters to avoid',
avoid_similar_on: 'Avoid similar (I l 1 O 0 o): On',
avoid_similar_off: 'Avoid similar (I l 1 O 0 o): Off',
avoid_ambiguous_on: 'Avoid ambiguous ({}[]()/\\\'"`~,;:.<>): On',
avoid_ambiguous_off: 'Avoid ambiguous ({}[]()/\\\'"`~,;:.<>): Off',
generate: 'Generate',
copy_all: 'Copy All',
download: 'Download TXT',
reset: 'Reset',
results: 'Results',
no_result: 'No passwords yet',
copy_one: 'Copy this',
};
export default passwordGeneratorEn;
@@ -0,0 +1,11 @@
import zh from './zh';
import en from './en';
export const passwordGenerator = {
zh,
en,
};
export default passwordGenerator;
@@ -0,0 +1,33 @@
export const passwordGeneratorZh = {
title: '随机密码生成器',
description: '支持自定义字符集、长度与数量,生成强随机密码',
length: '密码长度',
length_hint: '范围 4 - 128,推荐 12+ 保证强度',
count: '生成数量',
count_hint: '范围 1 - 100',
set_uppercase: '包含大写字母 A-Z',
set_lowercase: '包含小写字母 a-z',
set_digits: '包含数字 0-9',
set_symbols: '包含符号',
force_all_sets: '强制每个密码至少包含所选类型',
custom_include: '自定义追加字符',
custom_include_placeholder: '例如:€¥✓',
custom_include_hint: '这些字符会被加入候选集合',
exclude_chars: '排除字符',
exclude_chars_placeholder: '不希望出现的字符',
avoid_similar_on: '忽略相似字符(I l 1 O 0 o):开',
avoid_similar_off: '忽略相似字符(I l 1 O 0 o):关',
avoid_ambiguous_on: '忽略歧义符号({}[]()/\\\'"`~,;:.<>):开',
avoid_ambiguous_off: '忽略歧义符号({}[]()/\\\'"`~,;:.<>):关',
generate: '生成',
copy_all: '复制全部',
download: '下载TXT',
reset: '重置',
results: '生成结果',
no_result: '尚未生成密码',
copy_one: '复制此行',
};
export default passwordGeneratorZh;
+1
View File
@@ -33,6 +33,7 @@ export const zh = {
image_to_ico: tools.image_to_ico.zh,
cron_generator: tools.cron_generator.zh,
icon_designer: tools.icon_designer.zh,
password_generator: tools.password_generator.zh,
pdf_converter: tools.pdf_converter.zh,
pdf_manager: tools.pdf_manager.zh,
+7
View File
@@ -179,6 +179,13 @@ const tools: Tool[] = [
keywords: ['图标设计', '图标生成', 'icon设计', '图标制作', 'app图标', 'logo设计', 'favicon制作', '图标工具', 'icon designer', 'icon generator', 'tubiao', 'tb', 'sheji', 'sj', 'zhizuo', 'zz']
},
{
code: 'password_generator',
icon: faKey,
category: ['code'],
keywords: ['密码', '随机密码', '密码生成', '口令', '强密码', 'password', 'random', 'generator', 'security', 'mima', 'mm', 'suiji']
},
// PDF工具