From 84a70ee1ef62635c75e992e5f665480b5515cb69 Mon Sep 17 00:00:00 2001 From: star7th Date: Mon, 3 Nov 2025 23:04:25 +0800 Subject: [PATCH] =?UTF-8?q?Add=20a=20password=20generator.=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=AF=86=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/tools/password_generator/page.tsx | 302 ++++++++++++++++++ src/config/i18n/en.ts | 1 + src/config/i18n/tools/index.ts | 2 + .../i18n/tools/password_generator/en.ts | 33 ++ .../i18n/tools/password_generator/index.ts | 11 + .../i18n/tools/password_generator/zh.ts | 33 ++ src/config/i18n/zh.ts | 1 + src/config/tools.ts | 7 + 8 files changed, 390 insertions(+) create mode 100644 src/app/tools/password_generator/page.tsx create mode 100644 src/config/i18n/tools/password_generator/en.ts create mode 100644 src/config/i18n/tools/password_generator/index.ts create mode 100644 src/config/i18n/tools/password_generator/zh.ts diff --git a/src/app/tools/password_generator/page.tsx b/src/app/tools/password_generator/page.tsx new file mode 100644 index 0000000..fc1f5f9 --- /dev/null +++ b/src/app/tools/password_generator/page.tsx @@ -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>({ + 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([]); + const [copiedIndex, setCopiedIndex] = useState(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 ( +
+ + +
+
+
+
+ + setLength(parseInt(e.target.value || '0') || 0)} + /> +
{t('tools.password_generator.length_hint')}
+
+
+ + setCount(parseInt(e.target.value || '0') || 0)} + /> +
{t('tools.password_generator.count_hint')}
+
+
+ +
+
+
+ toggleSet('uppercase')} /> + +
+
+ toggleSet('lowercase')} /> + +
+
+ toggleSet('digits')} /> + +
+
+ toggleSet('symbols')} /> + +
+
+ setForceAllSets(!forceAllSets)} /> + +
+
+ +
+
+ + setCustomInclude(e.target.value)} + placeholder={t('tools.password_generator.custom_include_placeholder')} + /> +
{t('tools.password_generator.custom_include_hint')}
+
+
+ + setExcludeChars(e.target.value)} + placeholder={t('tools.password_generator.exclude_chars_placeholder')} + /> +
+ + +
+
+
+
+ +
+ + + + +
+
+
+ +
+
+

{t('tools.password_generator.results')}

+
+ {results.length === 0 && ( +
{t('tools.password_generator.no_result')}
+ )} + {results.map((pwd, idx) => ( +
+
{pwd}
+ +
+ ))} +
+
+
+
+ ); +} + +function escapeForRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + + diff --git a/src/config/i18n/en.ts b/src/config/i18n/en.ts index a818760..bc01127 100644 --- a/src/config/i18n/en.ts +++ b/src/config/i18n/en.ts @@ -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, diff --git a/src/config/i18n/tools/index.ts b/src/config/i18n/tools/index.ts index 6f55e01..f236bf8 100644 --- a/src/config/i18n/tools/index.ts +++ b/src/config/i18n/tools/index.ts @@ -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, diff --git a/src/config/i18n/tools/password_generator/en.ts b/src/config/i18n/tools/password_generator/en.ts new file mode 100644 index 0000000..18efbd0 --- /dev/null +++ b/src/config/i18n/tools/password_generator/en.ts @@ -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; + + diff --git a/src/config/i18n/tools/password_generator/index.ts b/src/config/i18n/tools/password_generator/index.ts new file mode 100644 index 0000000..e43a03c --- /dev/null +++ b/src/config/i18n/tools/password_generator/index.ts @@ -0,0 +1,11 @@ +import zh from './zh'; +import en from './en'; + +export const passwordGenerator = { + zh, + en, +}; + +export default passwordGenerator; + + diff --git a/src/config/i18n/tools/password_generator/zh.ts b/src/config/i18n/tools/password_generator/zh.ts new file mode 100644 index 0000000..2942583 --- /dev/null +++ b/src/config/i18n/tools/password_generator/zh.ts @@ -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; + + diff --git a/src/config/i18n/zh.ts b/src/config/i18n/zh.ts index 9eb0025..ae2990c 100644 --- a/src/config/i18n/zh.ts +++ b/src/config/i18n/zh.ts @@ -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, diff --git a/src/config/tools.ts b/src/config/tools.ts index 21ad3fc..e2569bf 100644 --- a/src/config/tools.ts +++ b/src/config/tools.ts @@ -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工具