chore: 添加 vue-i18n 依赖并更新 package.json
This commit is contained in:
241
src/App.vue
241
src/App.vue
@@ -2,20 +2,22 @@
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 py-6 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="text-center mb-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">密码生成器</h1>
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div></div>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ $t('title') }}</h1>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
安全、现代、易用的密码生成工具
|
||||
{{ $t('subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-lg rounded-lg p-5 space-y-5">
|
||||
<!-- 说明部分 -->
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-700 rounded-lg p-3">
|
||||
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">使用说明</h3>
|
||||
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">{{ $t('instructions.title') }}</h3>
|
||||
<ul class="mt-1 text-sm text-yellow-700 dark:text-yellow-300 list-disc pl-5 space-y-0.5">
|
||||
<li>密码计算全部在本地进行,确保安全</li>
|
||||
<li>建议使用复杂的记忆密码</li>
|
||||
<li>区分代码建议使用网站特征(如:qq, github)</li>
|
||||
<li v-for="(item, index) in instructionItems" :key="index">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -23,14 +25,14 @@
|
||||
<!-- 记忆密码 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
记忆密码
|
||||
{{ $t('form.memoryPassword') }}
|
||||
</label>
|
||||
<div class="relative rounded-md shadow-sm">
|
||||
<input
|
||||
v-model="memoryPassword"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
class="input pr-20"
|
||||
placeholder="输入你的记忆密码"
|
||||
:placeholder="$t('form.memoryPasswordPlaceholder')"
|
||||
/>
|
||||
<button
|
||||
@click="togglePasswordVisibility"
|
||||
@@ -38,7 +40,7 @@
|
||||
text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200
|
||||
flex items-center justify-center transition-colors duration-200"
|
||||
>
|
||||
{{ showPassword ? '隐藏' : '显示' }}
|
||||
{{ showPassword ? $t('buttons.hide') : $t('buttons.show') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,27 +48,60 @@
|
||||
<!-- 区分代码 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
区分代码
|
||||
{{ $t('form.distinguishCode') }}
|
||||
</label>
|
||||
<div class="relative rounded-md shadow-sm">
|
||||
<input
|
||||
v-model="distinguishCode"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="输入区分代码(如:qq)"
|
||||
:placeholder="$t('form.distinguishCodePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码选项 -->
|
||||
<div class="space-y-3">
|
||||
<!-- 算法选择 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
密码长度
|
||||
{{ $t('form.algorithm') }}
|
||||
</label>
|
||||
<select v-model="selectedAlgorithm" class="input py-1.5">
|
||||
<option
|
||||
v-for="(algorithm, key) in algorithms"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
{{ $t('locale') === 'zh' ? algorithm.name : algorithm.nameEn }}
|
||||
({{ $t('security.' + algorithm.security) }})
|
||||
</option>
|
||||
</select>
|
||||
<div class="flex items-center justify-between mt-1">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t('locale') === 'zh' ? algorithms[selectedAlgorithm].description : algorithms[selectedAlgorithm].descriptionEn }}
|
||||
</p>
|
||||
<button
|
||||
@click="showAlgorithmInfo = !showAlgorithmInfo"
|
||||
class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 ml-2"
|
||||
>
|
||||
{{ showAlgorithmInfo ? $t('algorithmInfo.hide') : $t('algorithmInfo.show') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 算法说明面板 -->
|
||||
<div v-if="showAlgorithmInfo" class="mt-4">
|
||||
<AlgorithmInfo :selected-algorithm="selectedAlgorithm" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{{ $t('form.passwordLength') }}
|
||||
</label>
|
||||
<select v-model="passwordLength" class="input py-1.5">
|
||||
<option v-for="length in [10,11,12,13,14,15,16,17,18,19,20]" :key="length" :value="length">
|
||||
{{ length }} 位
|
||||
{{ length }} {{ $t('form.lengthUnit') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -75,25 +110,25 @@
|
||||
<div>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="usePunctuation" class="w-4 h-4 rounded text-primary-600 focus:ring-primary-500" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">使用标点</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ $t('options.usePunctuation') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="useUpperCase" class="w-4 h-4 rounded text-primary-600 focus:ring-primary-500" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">大写字母</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ $t('options.useUpperCase') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="useNumbers" class="w-4 h-4 rounded text-primary-600 focus:ring-primary-500" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">使用数字</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ $t('options.useNumbers') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="useSpecialChars" class="w-4 h-4 rounded text-primary-600 focus:ring-primary-500" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">特殊字符</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ $t('options.useSpecialChars') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,7 +137,7 @@
|
||||
<!-- 生成的密码 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
生成的密码
|
||||
{{ $t('form.generatedPassword') }}
|
||||
</label>
|
||||
<div class="flex space-x-2">
|
||||
<div class="relative flex-1">
|
||||
@@ -117,24 +152,14 @@
|
||||
@click="copyPassword"
|
||||
class="btn btn-primary py-1.5"
|
||||
>
|
||||
复制
|
||||
{{ $t('buttons.copy') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码强度指示器 -->
|
||||
<div v-if="generatedPassword" class="space-y-1">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600 dark:text-gray-400">密码强度</span>
|
||||
<span :class="passwordStrengthClass">{{ passwordStrengthText }}</span>
|
||||
</div>
|
||||
<div class="h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
:class="passwordStrengthBarClass"
|
||||
:style="{ width: passwordStrengthPercentage + '%' }"
|
||||
class="h-full transition-all duration-300"
|
||||
></div>
|
||||
</div>
|
||||
<!-- 密码强度详细分析 -->
|
||||
<div v-if="generatedPassword" class="mt-4">
|
||||
<PasswordStrength :strength-data="passwordStrengthData" />
|
||||
</div>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
@@ -143,7 +168,7 @@
|
||||
class="w-full btn btn-primary py-2 text-base font-semibold"
|
||||
:disabled="isGenerating"
|
||||
>
|
||||
{{ isGenerating ? '生成中...' : '生成密码' }}
|
||||
{{ isGenerating ? $t('buttons.generating') : $t('buttons.generate') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,10 +178,24 @@
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import LanguageSwitcher from './components/LanguageSwitcher.vue'
|
||||
import AlgorithmInfo from './components/AlgorithmInfo.vue'
|
||||
import PasswordStrength from './components/PasswordStrength.vue'
|
||||
import zh from './locales/zh.js'
|
||||
import en from './locales/en.js'
|
||||
import { generatePasswordWithAlgorithm, algorithms } from './utils/passwordAlgorithms.js'
|
||||
import { analyzePasswordStrength } from './utils/passwordStrengthAnalyzer.js'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
LanguageSwitcher,
|
||||
AlgorithmInfo,
|
||||
PasswordStrength
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const memoryPassword = ref('')
|
||||
const distinguishCode = ref('')
|
||||
const passwordLength = ref(16)
|
||||
@@ -167,6 +206,21 @@ export default {
|
||||
const showPassword = ref(false)
|
||||
const generatedPassword = ref('')
|
||||
const isGenerating = ref(false)
|
||||
const selectedAlgorithm = ref('sha512-salt') // 默认算法
|
||||
const showAlgorithmInfo = ref(false) // 控制算法说明面板显示
|
||||
|
||||
// 获取使用说明列表
|
||||
const instructionItems = computed(() => {
|
||||
const { locale } = useI18n()
|
||||
const currentLocale = locale.value
|
||||
|
||||
// 直接从语言对象中获取数组
|
||||
if (currentLocale === 'zh') {
|
||||
return zh.instructions.items
|
||||
} else {
|
||||
return en.instructions.items
|
||||
}
|
||||
})
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
showPassword.value = !showPassword.value
|
||||
@@ -181,52 +235,31 @@ export default {
|
||||
|
||||
const generatePassword = async () => {
|
||||
if (!memoryPassword.value || !distinguishCode.value) {
|
||||
alert('请填写记忆密码和区分代码')
|
||||
alert(t('messages.fillRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
isGenerating.value = true
|
||||
|
||||
try {
|
||||
// 使用 Web Crypto API 进行更安全的密码生成
|
||||
const encoder = new TextEncoder()
|
||||
const baseData = encoder.encode(memoryPassword.value + distinguishCode.value)
|
||||
|
||||
// 添加随机因子,确保每次生成的密码都不同
|
||||
const randomSalt = getRandomBytes(16)
|
||||
const combinedData = new Uint8Array(baseData.length + randomSalt.length)
|
||||
combinedData.set(baseData)
|
||||
combinedData.set(randomSalt, baseData.length)
|
||||
|
||||
const hashBuffer = await window.crypto.subtle.digest('SHA-512', combinedData)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
|
||||
// 根据选项生成密码
|
||||
let password = ''
|
||||
const chars = {
|
||||
lowercase: 'abcdefghijklmnopqrstuvwxyz',
|
||||
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
numbers: '0123456789',
|
||||
special: '!@#$%^&*()_+-=[]{}|;:,.<>?'
|
||||
const options = {
|
||||
useUpperCase: useUpperCase.value,
|
||||
useNumbers: useNumbers.value,
|
||||
useSpecialChars: useSpecialChars.value
|
||||
}
|
||||
|
||||
let availableChars = chars.lowercase
|
||||
if (useUpperCase.value) availableChars += chars.uppercase
|
||||
if (useNumbers.value) availableChars += chars.numbers
|
||||
if (useSpecialChars.value) availableChars += chars.special
|
||||
|
||||
// 使用更多的熵来生成密码
|
||||
const randomValues = getRandomBytes(passwordLength.value * 2)
|
||||
for (let i = 0; i < passwordLength.value; i++) {
|
||||
const combinedEntropy = (hashArray[i] + randomValues[i]) % availableChars.length
|
||||
password += availableChars[combinedEntropy]
|
||||
}
|
||||
|
||||
|
||||
const password = await generatePasswordWithAlgorithm(
|
||||
selectedAlgorithm.value,
|
||||
memoryPassword.value,
|
||||
distinguishCode.value,
|
||||
passwordLength.value,
|
||||
options
|
||||
)
|
||||
|
||||
generatedPassword.value = password
|
||||
} catch (error) {
|
||||
console.error('密码生成失败:', error)
|
||||
alert('密码生成失败,请重试')
|
||||
alert(t('messages.generateFailed'))
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
@@ -237,7 +270,7 @@ export default {
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(generatedPassword.value)
|
||||
alert('密码已复制到剪贴板')
|
||||
alert(t('messages.passwordCopied'))
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
// 如果 clipboard API 失败,使用传统方法
|
||||
@@ -247,53 +280,22 @@ export default {
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
alert('密码已复制到剪贴板')
|
||||
alert(t('messages.passwordCopied'))
|
||||
}
|
||||
}
|
||||
|
||||
const passwordStrength = computed(() => {
|
||||
if (!generatedPassword.value) return 0
|
||||
|
||||
let strength = 0
|
||||
const password = generatedPassword.value
|
||||
|
||||
// 长度得分
|
||||
strength += Math.min(password.length * 4, 40)
|
||||
|
||||
// 字符类型得分
|
||||
if (password.match(/[A-Z]/)) strength += 10
|
||||
if (password.match(/[a-z]/)) strength += 10
|
||||
if (password.match(/[0-9]/)) strength += 10
|
||||
if (password.match(/[^A-Za-z0-9]/)) strength += 10
|
||||
|
||||
// 重复字符扣分
|
||||
const uniqueChars = new Set(password).size
|
||||
strength -= (password.length - uniqueChars) * 2
|
||||
|
||||
return Math.max(0, Math.min(100, strength))
|
||||
})
|
||||
|
||||
const passwordStrengthPercentage = computed(() => passwordStrength.value)
|
||||
|
||||
const passwordStrengthText = computed(() => {
|
||||
if (passwordStrength.value < 40) return '弱'
|
||||
if (passwordStrength.value < 60) return '中等'
|
||||
if (passwordStrength.value < 80) return '强'
|
||||
return '极强'
|
||||
})
|
||||
|
||||
const passwordStrengthClass = computed(() => {
|
||||
if (passwordStrength.value < 40) return 'text-red-600 dark:text-red-400'
|
||||
if (passwordStrength.value < 60) return 'text-yellow-600 dark:text-yellow-400'
|
||||
if (passwordStrength.value < 80) return 'text-blue-600 dark:text-blue-400'
|
||||
return 'text-green-600 dark:text-green-400'
|
||||
})
|
||||
|
||||
const passwordStrengthBarClass = computed(() => {
|
||||
if (passwordStrength.value < 40) return 'bg-red-500'
|
||||
if (passwordStrength.value < 60) return 'bg-yellow-500'
|
||||
if (passwordStrength.value < 80) return 'bg-blue-500'
|
||||
return 'bg-green-500'
|
||||
// 密码强度分析
|
||||
const passwordStrengthData = computed(() => {
|
||||
if (!generatedPassword.value) {
|
||||
return {
|
||||
score: 0,
|
||||
level: 'very-weak',
|
||||
percentage: 0,
|
||||
details: {},
|
||||
suggestions: []
|
||||
}
|
||||
}
|
||||
return analyzePasswordStrength(generatedPassword.value)
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -307,14 +309,15 @@ export default {
|
||||
showPassword,
|
||||
generatedPassword,
|
||||
isGenerating,
|
||||
selectedAlgorithm,
|
||||
algorithms,
|
||||
showAlgorithmInfo,
|
||||
instructionItems,
|
||||
togglePasswordVisibility,
|
||||
generatePassword,
|
||||
copyPassword,
|
||||
passwordStrengthPercentage,
|
||||
passwordStrengthText,
|
||||
passwordStrengthClass,
|
||||
passwordStrengthBarClass
|
||||
passwordStrengthData
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
133
src/components/AlgorithmInfo.vue
Normal file
133
src/components/AlgorithmInfo.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="algorithm-info">
|
||||
<!-- 算法说明标题 -->
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">
|
||||
{{ $t('algorithmInfo.title') }}
|
||||
</h3>
|
||||
|
||||
<!-- 算法列表 -->
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="(algorithm, key) in algorithms"
|
||||
:key="key"
|
||||
class="algorithm-card p-4 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
:class="{ 'border-blue-500 bg-blue-50 dark:bg-blue-900/20': selectedAlgorithm === key }"
|
||||
>
|
||||
<!-- 算法名称和安全级别 -->
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-medium text-gray-800 dark:text-gray-200">
|
||||
{{ $t('locale') === 'zh' ? algorithm.name : algorithm.nameEn }}
|
||||
</h4>
|
||||
<span
|
||||
class="px-2 py-1 text-xs rounded-full"
|
||||
:class="getSecurityBadgeClass(algorithm.security)"
|
||||
>
|
||||
{{ $t('security.' + algorithm.security) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 算法描述 -->
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
{{ $t('locale') === 'zh' ? algorithm.description : algorithm.descriptionEn }}
|
||||
</p>
|
||||
|
||||
<!-- 算法特性 -->
|
||||
<div class="flex items-center space-x-4 text-xs">
|
||||
<span
|
||||
class="flex items-center"
|
||||
:class="algorithm.deterministic ? 'text-orange-600 dark:text-orange-400' : 'text-green-600 dark:text-green-400'"
|
||||
>
|
||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path v-if="algorithm.deterministic" fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
<path v-else fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{{ algorithm.deterministic ? $t('algorithmInfo.deterministic') : $t('algorithmInfo.random') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 安全建议 -->
|
||||
<div class="mt-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<h4 class="font-medium text-yellow-800 dark:text-yellow-200 mb-2">
|
||||
{{ $t('algorithmInfo.securityTips.title') }}
|
||||
</h4>
|
||||
<ul class="text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
|
||||
<li v-for="tip in securityTips" :key="tip">
|
||||
• {{ tip }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { algorithms } from '../utils/passwordAlgorithms.js'
|
||||
import zh from '../locales/zh.js'
|
||||
import en from '../locales/en.js'
|
||||
|
||||
export default {
|
||||
name: 'AlgorithmInfo',
|
||||
props: {
|
||||
selectedAlgorithm: {
|
||||
type: String,
|
||||
default: 'sha512-salt'
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { locale } = useI18n()
|
||||
|
||||
/**
|
||||
* 获取安全建议列表
|
||||
* Get security tips list
|
||||
* @returns {Array} 安全建议数组
|
||||
*/
|
||||
const securityTips = computed(() => {
|
||||
const currentLocale = locale.value
|
||||
if (currentLocale === 'zh') {
|
||||
return zh.algorithmInfo.securityTips.items
|
||||
} else {
|
||||
return en.algorithmInfo.securityTips.items
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取安全级别徽章样式
|
||||
* Get security level badge class
|
||||
* @param {string} security - 安全级别
|
||||
* @returns {string} CSS类名
|
||||
*/
|
||||
const getSecurityBadgeClass = (security) => {
|
||||
const classes = {
|
||||
'medium': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
|
||||
'high': 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
|
||||
'very-high': 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
}
|
||||
return classes[security] || classes.medium
|
||||
}
|
||||
|
||||
return {
|
||||
algorithms,
|
||||
securityTips,
|
||||
getSecurityBadgeClass
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.algorithm-card {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.algorithm-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark .algorithm-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
</style>
|
||||
115
src/components/LanguageSwitcher.vue
Normal file
115
src/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="toggleDropdown"
|
||||
class="flex items-center space-x-2 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300
|
||||
bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md
|
||||
hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500
|
||||
transition-colors duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" />
|
||||
</svg>
|
||||
<span>{{ currentLanguageLabel }}</span>
|
||||
<svg class="w-4 h-4 transition-transform duration-200" :class="{ 'rotate-180': isOpen }"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 下拉菜单 -->
|
||||
<div v-if="isOpen"
|
||||
class="absolute right-0 mt-2 w-32 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
|
||||
rounded-md shadow-lg z-50 overflow-hidden">
|
||||
<button
|
||||
v-for="lang in languages"
|
||||
:key="lang.code"
|
||||
@click="switchLanguage(lang.code)"
|
||||
class="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200"
|
||||
:class="{ 'bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400': currentLanguage === lang.code }"
|
||||
>
|
||||
{{ lang.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { switchLanguage as switchLang, getCurrentLanguage } from '../locales/index.js'
|
||||
|
||||
export default {
|
||||
name: 'LanguageSwitcher',
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const isOpen = ref(false)
|
||||
const currentLanguage = ref(getCurrentLanguage())
|
||||
|
||||
/**
|
||||
* 可用语言列表
|
||||
* Available languages list
|
||||
*/
|
||||
const languages = [
|
||||
{ code: 'zh', label: '中文' },
|
||||
{ code: 'en', label: 'English' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 当前语言标签
|
||||
* Current language label
|
||||
*/
|
||||
const currentLanguageLabel = computed(() => {
|
||||
const lang = languages.find(l => l.code === currentLanguage.value)
|
||||
return lang ? lang.label : '中文'
|
||||
})
|
||||
|
||||
/**
|
||||
* 切换下拉菜单显示状态
|
||||
* Toggle dropdown visibility
|
||||
*/
|
||||
const toggleDropdown = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换语言
|
||||
* Switch language
|
||||
*/
|
||||
const switchLanguage = (locale) => {
|
||||
switchLang(locale)
|
||||
currentLanguage.value = locale
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击外部关闭下拉菜单
|
||||
* Close dropdown when clicking outside
|
||||
*/
|
||||
const handleClickOutside = (event) => {
|
||||
if (!event.target.closest('.relative')) {
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
currentLanguage,
|
||||
currentLanguageLabel,
|
||||
languages,
|
||||
toggleDropdown,
|
||||
switchLanguage
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
311
src/components/PasswordStrength.vue
Normal file
311
src/components/PasswordStrength.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<div class="password-strength-container">
|
||||
<!-- 总体强度显示 -->
|
||||
<div class="strength-overview mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">
|
||||
{{ $t('passwordStrength.title') }}
|
||||
</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span :class="getStrengthColor(strengthData.level)" class="font-bold">
|
||||
{{ $t(`passwordStrength.levels.${strengthData.level}`) }}
|
||||
</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
({{ strengthData.score }}/100)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 强度进度条 -->
|
||||
<div class="strength-bar-container">
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 mb-2">
|
||||
<div
|
||||
:class="getStrengthBgColor(strengthData.level)"
|
||||
class="h-3 rounded-full transition-all duration-500 ease-out"
|
||||
:style="{ width: strengthData.percentage + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>{{ $t('passwordStrength.levels.very-weak') }}</span>
|
||||
<span>{{ $t('passwordStrength.levels.weak') }}</span>
|
||||
<span>{{ $t('passwordStrength.levels.fair') }}</span>
|
||||
<span>{{ $t('passwordStrength.levels.good') }}</span>
|
||||
<span>{{ $t('passwordStrength.levels.strong') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细分析 -->
|
||||
<div class="strength-details" v-if="showDetails">
|
||||
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||||
{{ $t('passwordStrength.detailsTitle') }}
|
||||
</h4>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- 长度分析 -->
|
||||
<div class="detail-item">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i :class="strengthData.details.length.passed ? 'text-green-500' : 'text-red-500'"
|
||||
class="fas fa-ruler-horizontal"></i>
|
||||
<span class="font-medium">{{ $t('passwordStrength.criteria.length') }}</span>
|
||||
<span class="text-sm text-gray-500">({{ strengthData.details.length.value }})</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span :class="strengthData.details.length.passed ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-sm font-medium">
|
||||
{{ strengthData.details.length.score }}/100
|
||||
</span>
|
||||
<i :class="strengthData.details.length.passed ? 'fas fa-check-circle text-green-500' : 'fas fa-times-circle text-red-500'"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1 ml-6">
|
||||
{{ $t(`passwordStrength.messages.${strengthData.details.length.message}`) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 字符多样性分析 -->
|
||||
<div class="detail-item">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i :class="strengthData.details.variety.passed ? 'text-green-500' : 'text-red-500'"
|
||||
class="fas fa-palette"></i>
|
||||
<span class="font-medium">{{ $t('passwordStrength.criteria.variety') }}</span>
|
||||
<span class="text-sm text-gray-500">({{ strengthData.details.variety.details.count }}/4)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span :class="strengthData.details.variety.passed ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-sm font-medium">
|
||||
{{ strengthData.details.variety.score }}/100
|
||||
</span>
|
||||
<i :class="strengthData.details.variety.passed ? 'fas fa-check-circle text-green-500' : 'fas fa-times-circle text-red-500'"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1 ml-6">
|
||||
{{ $t(`passwordStrength.messages.${strengthData.details.variety.message}`) }}
|
||||
</p>
|
||||
<!-- 字符类型指示器 -->
|
||||
<div class="flex space-x-2 mt-2 ml-6">
|
||||
<span :class="strengthData.details.variety.details.hasLower ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-500'"
|
||||
class="px-2 py-1 rounded text-xs">
|
||||
{{ $t('passwordStrength.charTypes.lowercase') }}
|
||||
</span>
|
||||
<span :class="strengthData.details.variety.details.hasUpper ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-500'"
|
||||
class="px-2 py-1 rounded text-xs">
|
||||
{{ $t('passwordStrength.charTypes.uppercase') }}
|
||||
</span>
|
||||
<span :class="strengthData.details.variety.details.hasDigits ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-500'"
|
||||
class="px-2 py-1 rounded text-xs">
|
||||
{{ $t('passwordStrength.charTypes.numbers') }}
|
||||
</span>
|
||||
<span :class="strengthData.details.variety.details.hasSpecial ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-500'"
|
||||
class="px-2 py-1 rounded text-xs">
|
||||
{{ $t('passwordStrength.charTypes.special') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 常见密码检查 -->
|
||||
<div class="detail-item">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i :class="strengthData.details.common.passed ? 'text-green-500' : 'text-red-500'"
|
||||
class="fas fa-shield-alt"></i>
|
||||
<span class="font-medium">{{ $t('passwordStrength.criteria.common') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span :class="strengthData.details.common.passed ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-sm font-medium">
|
||||
{{ strengthData.details.common.score }}/100
|
||||
</span>
|
||||
<i :class="strengthData.details.common.passed ? 'fas fa-check-circle text-green-500' : 'fas fa-times-circle text-red-500'"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1 ml-6">
|
||||
{{ $t(`passwordStrength.messages.${strengthData.details.common.message}`) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 键盘模式检查 -->
|
||||
<div class="detail-item">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i :class="strengthData.details.patterns.passed ? 'text-green-500' : 'text-red-500'"
|
||||
class="fas fa-keyboard"></i>
|
||||
<span class="font-medium">{{ $t('passwordStrength.criteria.patterns') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span :class="strengthData.details.patterns.passed ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-sm font-medium">
|
||||
{{ strengthData.details.patterns.score }}/100
|
||||
</span>
|
||||
<i :class="strengthData.details.patterns.passed ? 'fas fa-check-circle text-green-500' : 'fas fa-times-circle text-red-500'"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1 ml-6">
|
||||
{{ $t(`passwordStrength.messages.${strengthData.details.patterns.message}`) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 重复字符检查 -->
|
||||
<div class="detail-item">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i :class="strengthData.details.repetition.passed ? 'text-green-500' : 'text-red-500'"
|
||||
class="fas fa-repeat"></i>
|
||||
<span class="font-medium">{{ $t('passwordStrength.criteria.repetition') }}</span>
|
||||
<span class="text-sm text-gray-500" v-if="strengthData.details.repetition.repeatRatio">
|
||||
({{ strengthData.details.repetition.repeatRatio }}%)
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span :class="strengthData.details.repetition.passed ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-sm font-medium">
|
||||
{{ strengthData.details.repetition.score }}/100
|
||||
</span>
|
||||
<i :class="strengthData.details.repetition.passed ? 'fas fa-check-circle text-green-500' : 'fas fa-times-circle text-red-500'"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1 ml-6">
|
||||
{{ $t(`passwordStrength.messages.${strengthData.details.repetition.message}`) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 改进建议 -->
|
||||
<div class="suggestions mt-4" v-if="strengthData.suggestions && strengthData.suggestions.length > 0">
|
||||
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||||
{{ $t('passwordStrength.suggestionsTitle') }}
|
||||
</h4>
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3">
|
||||
<ul class="space-y-1">
|
||||
<li v-for="suggestion in strengthData.suggestions" :key="suggestion"
|
||||
class="flex items-start space-x-2 text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<i class="fas fa-lightbulb text-yellow-600 mt-0.5"></i>
|
||||
<span>{{ $t(`passwordStrength.suggestions.${suggestion}`) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开/收起按钮 -->
|
||||
<div class="mt-4 text-center">
|
||||
<button
|
||||
@click="showDetails = !showDetails"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 text-sm font-medium transition-colors"
|
||||
>
|
||||
<i :class="showDetails ? 'fas fa-chevron-up' : 'fas fa-chevron-down'" class="mr-1"></i>
|
||||
{{ showDetails ? $t('passwordStrength.hideDetails') : $t('passwordStrength.showDetails') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { getStrengthColor, getStrengthBgColor } from '../utils/passwordStrengthAnalyzer.js';
|
||||
|
||||
/**
|
||||
* 组件属性定义
|
||||
*/
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 密码强度分析数据
|
||||
* @type {Object}
|
||||
*/
|
||||
strengthData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({
|
||||
score: 0,
|
||||
level: 'very-weak',
|
||||
percentage: 0,
|
||||
details: {},
|
||||
suggestions: []
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 响应式数据
|
||||
*/
|
||||
const showDetails = ref(false);
|
||||
|
||||
/**
|
||||
* 获取强度等级对应的文本颜色类
|
||||
* @param {string} level - 强度等级
|
||||
* @returns {string} CSS类名
|
||||
*/
|
||||
const getStrengthColorClass = (level) => {
|
||||
return getStrengthColor(level);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取强度等级对应的背景颜色类
|
||||
* @param {string} level - 强度等级
|
||||
* @returns {string} CSS类名
|
||||
*/
|
||||
const getStrengthBgColorClass = (level) => {
|
||||
return getStrengthBgColor(level);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PasswordStrength',
|
||||
methods: {
|
||||
/**
|
||||
* 获取强度颜色类
|
||||
* @param {string} level - 强度等级
|
||||
* @returns {string} 颜色类名
|
||||
*/
|
||||
getStrengthColor(level) {
|
||||
return getStrengthColor(level);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取强度背景颜色类
|
||||
* @param {string} level - 强度等级
|
||||
* @returns {string} 背景颜色类名
|
||||
*/
|
||||
getStrengthBgColor(level) {
|
||||
return getStrengthBgColor(level);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.password-strength-container {
|
||||
@apply bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
@apply bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3 border border-gray-100 dark:border-gray-600;
|
||||
}
|
||||
|
||||
.strength-bar-container {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
/* 进度条动画 */
|
||||
.strength-bar-container .h-3 {
|
||||
transition: width 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 640px) {
|
||||
.password-strength-container {
|
||||
@apply p-3;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
@apply p-2;
|
||||
}
|
||||
|
||||
.flex.items-center.justify-between {
|
||||
@apply flex-col items-start space-y-2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
179
src/locales/en.js
Normal file
179
src/locales/en.js
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* English language configuration
|
||||
* 英文语言配置文件
|
||||
*/
|
||||
export default {
|
||||
// Page title and description
|
||||
title: 'Password Generator',
|
||||
subtitle: 'Secure, modern, and easy-to-use password generation tool',
|
||||
|
||||
// Instructions
|
||||
instructions: {
|
||||
title: 'Instructions',
|
||||
items: [
|
||||
'All password calculations are performed locally to ensure security',
|
||||
'It is recommended to use a complex memory password',
|
||||
'Distinguish codes should use website features (e.g., qq, github)'
|
||||
]
|
||||
},
|
||||
|
||||
// Form labels
|
||||
form: {
|
||||
memoryPassword: 'Memory Password',
|
||||
memoryPasswordPlaceholder: 'Enter your memory password',
|
||||
distinguishCode: 'Distinguish Code',
|
||||
distinguishCodePlaceholder: 'Enter distinguish code (e.g., qq)',
|
||||
passwordLength: 'Password Length',
|
||||
lengthUnit: 'characters',
|
||||
algorithm: 'Generation Algorithm',
|
||||
generatedPassword: 'Generated Password'
|
||||
},
|
||||
|
||||
// Password options
|
||||
options: {
|
||||
usePunctuation: 'Use Punctuation',
|
||||
useUpperCase: 'Uppercase Letters',
|
||||
useNumbers: 'Use Numbers',
|
||||
useSpecialChars: 'Special Characters'
|
||||
},
|
||||
|
||||
// Button text
|
||||
buttons: {
|
||||
show: 'Show',
|
||||
hide: 'Hide',
|
||||
copy: 'Copy',
|
||||
generate: 'Generate Password',
|
||||
generating: 'Generating...'
|
||||
},
|
||||
|
||||
// Password strength
|
||||
strength: {
|
||||
label: 'Password Strength',
|
||||
weak: 'Weak',
|
||||
medium: 'Medium',
|
||||
strong: 'Strong',
|
||||
veryStrong: 'Very Strong'
|
||||
},
|
||||
|
||||
// Messages
|
||||
messages: {
|
||||
fillRequired: 'Please fill in memory password and distinguish code',
|
||||
passwordCopied: 'Password copied to clipboard',
|
||||
generateFailed: 'Password generation failed, please try again',
|
||||
copyFailed: 'Copy failed'
|
||||
},
|
||||
|
||||
// Security levels
|
||||
security: {
|
||||
'medium': 'Medium Security',
|
||||
'high': 'High Security',
|
||||
'very-high': 'Very High Security'
|
||||
},
|
||||
|
||||
// Algorithm information
|
||||
algorithmInfo: {
|
||||
title: 'Algorithm Information',
|
||||
show: 'Show Details',
|
||||
hide: 'Hide Details',
|
||||
deterministic: 'Deterministic',
|
||||
random: 'Random',
|
||||
securityTips: {
|
||||
title: 'Security Tips',
|
||||
items: [
|
||||
'Choose high security algorithms for better protection',
|
||||
'Deterministic algorithms generate same password within time window',
|
||||
'Random algorithms generate different passwords each time',
|
||||
'PBKDF2 algorithm is suitable for scenarios requiring highest security',
|
||||
'Regularly change memory password and distinguish code'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Password Strength Analysis
|
||||
passwordStrength: {
|
||||
title: 'Password Strength Analysis',
|
||||
detailsTitle: 'Detailed Analysis',
|
||||
suggestionsTitle: 'Improvement Suggestions',
|
||||
showDetails: 'Show Details',
|
||||
hideDetails: 'Hide Details',
|
||||
|
||||
// Strength Levels
|
||||
levels: {
|
||||
'very-weak': 'Very Weak',
|
||||
'weak': 'Weak',
|
||||
'fair': 'Fair',
|
||||
'good': 'Good',
|
||||
'strong': 'Strong'
|
||||
},
|
||||
|
||||
// Evaluation Criteria
|
||||
criteria: {
|
||||
length: 'Password Length',
|
||||
variety: 'Character Variety',
|
||||
common: 'Common Password Check',
|
||||
patterns: 'Keyboard Pattern Check',
|
||||
repetition: 'Character Repetition Check'
|
||||
},
|
||||
|
||||
// Character Types
|
||||
charTypes: {
|
||||
lowercase: 'Lowercase',
|
||||
uppercase: 'Uppercase',
|
||||
numbers: 'Numbers',
|
||||
special: 'Special Characters'
|
||||
},
|
||||
|
||||
// Analysis Messages
|
||||
messages: {
|
||||
// Length related
|
||||
password_empty: 'Password is empty',
|
||||
too_short: 'Password too short, recommend at least 6 characters',
|
||||
short: 'Password is short, recommend at least 8 characters',
|
||||
good_length: 'Good password length',
|
||||
very_good_length: 'Very good password length',
|
||||
excellent_length: 'Excellent password length',
|
||||
|
||||
// Character variety related
|
||||
no_characters: 'No valid characters',
|
||||
single_type: 'Contains only one character type',
|
||||
two_types: 'Contains two character types',
|
||||
three_types: 'Contains three character types, good',
|
||||
all_types: 'Contains all character types, excellent',
|
||||
|
||||
// Common password related
|
||||
not_common: 'Not a common password',
|
||||
very_common: 'This is a very common password',
|
||||
contains_common: 'Contains common password fragments',
|
||||
|
||||
// Keyboard pattern related
|
||||
no_patterns: 'No obvious keyboard patterns',
|
||||
keyboard_pattern: 'Contains keyboard patterns',
|
||||
sequential_pattern: 'Contains sequential character patterns',
|
||||
|
||||
// Character repetition related
|
||||
no_repetition: 'No obvious repetition',
|
||||
consecutive_repeats: 'Contains consecutive repeated characters',
|
||||
high_repetition: 'Character repetition rate too high',
|
||||
moderate_repetition: 'Moderate character repetition rate'
|
||||
},
|
||||
|
||||
// Improvement Suggestions
|
||||
suggestions: {
|
||||
increase_length: 'Increase password length to at least 8 characters',
|
||||
add_uppercase: 'Add uppercase letters',
|
||||
add_lowercase: 'Add lowercase letters',
|
||||
add_numbers: 'Add numbers',
|
||||
add_special: 'Add special characters (!@#$%^&* etc.)',
|
||||
avoid_common: 'Avoid using common passwords',
|
||||
avoid_patterns: 'Avoid keyboard patterns or sequential characters',
|
||||
reduce_repetition: 'Reduce character repetition'
|
||||
}
|
||||
},
|
||||
|
||||
// Language switcher
|
||||
language: {
|
||||
label: 'Language',
|
||||
chinese: '中文',
|
||||
english: 'English'
|
||||
}
|
||||
}
|
||||
58
src/locales/index.js
Normal file
58
src/locales/index.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 国际化配置文件
|
||||
* Internationalization configuration
|
||||
*/
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import zh from './zh.js'
|
||||
import en from './en.js'
|
||||
|
||||
/**
|
||||
* 获取浏览器默认语言
|
||||
* Get browser default language
|
||||
*/
|
||||
function getDefaultLocale() {
|
||||
const savedLocale = localStorage.getItem('locale')
|
||||
if (savedLocale) {
|
||||
return savedLocale
|
||||
}
|
||||
|
||||
const browserLocale = navigator.language || navigator.userLanguage
|
||||
if (browserLocale.startsWith('zh')) {
|
||||
return 'zh'
|
||||
}
|
||||
return 'en'
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建i18n实例
|
||||
* Create i18n instance
|
||||
*/
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 使用 Composition API 模式
|
||||
locale: getDefaultLocale(), // 默认语言
|
||||
fallbackLocale: 'zh', // 回退语言
|
||||
messages: {
|
||||
zh,
|
||||
en
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 切换语言
|
||||
* Switch language
|
||||
*/
|
||||
export function switchLanguage(locale) {
|
||||
i18n.global.locale.value = locale
|
||||
localStorage.setItem('locale', locale)
|
||||
document.documentElement.lang = locale
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
* Get current language
|
||||
*/
|
||||
export function getCurrentLanguage() {
|
||||
return i18n.global.locale.value
|
||||
}
|
||||
|
||||
export default i18n
|
||||
179
src/locales/zh.js
Normal file
179
src/locales/zh.js
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 中文语言配置文件
|
||||
* Chinese language configuration
|
||||
*/
|
||||
export default {
|
||||
// 页面标题和描述
|
||||
title: '密码生成器',
|
||||
subtitle: '安全、现代、易用的密码生成工具',
|
||||
|
||||
// 使用说明
|
||||
instructions: {
|
||||
title: '使用说明',
|
||||
items: [
|
||||
'密码计算全部在本地进行,确保安全',
|
||||
'建议使用复杂的记忆密码',
|
||||
'区分代码建议使用网站特征(如:qq, github)'
|
||||
]
|
||||
},
|
||||
|
||||
// 表单标签
|
||||
form: {
|
||||
memoryPassword: '记忆密码',
|
||||
memoryPasswordPlaceholder: '输入你的记忆密码',
|
||||
distinguishCode: '区分代码',
|
||||
distinguishCodePlaceholder: '输入区分代码(如:qq)',
|
||||
passwordLength: '密码长度',
|
||||
lengthUnit: '位',
|
||||
algorithm: '生成算法',
|
||||
generatedPassword: '生成的密码'
|
||||
},
|
||||
|
||||
// 密码选项
|
||||
options: {
|
||||
usePunctuation: '使用标点',
|
||||
useUpperCase: '大写字母',
|
||||
useNumbers: '使用数字',
|
||||
useSpecialChars: '特殊字符'
|
||||
},
|
||||
|
||||
// 按钮文本
|
||||
buttons: {
|
||||
show: '显示',
|
||||
hide: '隐藏',
|
||||
copy: '复制',
|
||||
generate: '生成密码',
|
||||
generating: '生成中...'
|
||||
},
|
||||
|
||||
// 密码强度
|
||||
strength: {
|
||||
label: '密码强度',
|
||||
weak: '弱',
|
||||
medium: '中等',
|
||||
strong: '强',
|
||||
veryStrong: '极强'
|
||||
},
|
||||
|
||||
// 提示信息
|
||||
messages: {
|
||||
fillRequired: '请填写记忆密码和区分代码',
|
||||
passwordCopied: '密码已复制到剪贴板',
|
||||
generateFailed: '密码生成失败,请重试',
|
||||
copyFailed: '复制失败'
|
||||
},
|
||||
|
||||
// 安全级别
|
||||
security: {
|
||||
'medium': '中等安全',
|
||||
'high': '高安全',
|
||||
'very-high': '极高安全'
|
||||
},
|
||||
|
||||
// 算法说明
|
||||
algorithmInfo: {
|
||||
title: '算法说明',
|
||||
show: '查看详情',
|
||||
hide: '收起详情',
|
||||
deterministic: '确定性',
|
||||
random: '随机性',
|
||||
securityTips: {
|
||||
title: '安全建议',
|
||||
items: [
|
||||
'选择高安全级别的算法以获得更好的保护',
|
||||
'确定性算法在相同时间窗口内生成相同密码',
|
||||
'随机性算法每次生成不同的密码',
|
||||
'PBKDF2算法适合需要最高安全性的场景',
|
||||
'定期更换记忆密码和区分代码'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// 密码强度分析
|
||||
passwordStrength: {
|
||||
title: '密码强度分析',
|
||||
detailsTitle: '详细分析',
|
||||
suggestionsTitle: '改进建议',
|
||||
showDetails: '显示详情',
|
||||
hideDetails: '隐藏详情',
|
||||
|
||||
// 强度等级
|
||||
levels: {
|
||||
'very-weak': '极弱',
|
||||
'weak': '弱',
|
||||
'fair': '一般',
|
||||
'good': '良好',
|
||||
'strong': '强'
|
||||
},
|
||||
|
||||
// 评估标准
|
||||
criteria: {
|
||||
length: '密码长度',
|
||||
variety: '字符多样性',
|
||||
common: '常见密码检查',
|
||||
patterns: '键盘模式检查',
|
||||
repetition: '重复字符检查'
|
||||
},
|
||||
|
||||
// 字符类型
|
||||
charTypes: {
|
||||
lowercase: '小写字母',
|
||||
uppercase: '大写字母',
|
||||
numbers: '数字',
|
||||
special: '特殊字符'
|
||||
},
|
||||
|
||||
// 分析消息
|
||||
messages: {
|
||||
// 长度相关
|
||||
password_empty: '密码为空',
|
||||
too_short: '密码过短,建议至少6位',
|
||||
short: '密码较短,建议至少8位',
|
||||
good_length: '密码长度良好',
|
||||
very_good_length: '密码长度很好',
|
||||
excellent_length: '密码长度优秀',
|
||||
|
||||
// 字符多样性相关
|
||||
no_characters: '无有效字符',
|
||||
single_type: '仅包含一种字符类型',
|
||||
two_types: '包含两种字符类型',
|
||||
three_types: '包含三种字符类型,很好',
|
||||
all_types: '包含所有字符类型,优秀',
|
||||
|
||||
// 常见密码相关
|
||||
not_common: '不是常见密码',
|
||||
very_common: '这是非常常见的密码',
|
||||
contains_common: '包含常见密码片段',
|
||||
|
||||
// 键盘模式相关
|
||||
no_patterns: '无明显键盘模式',
|
||||
keyboard_pattern: '包含键盘模式',
|
||||
sequential_pattern: '包含连续字符模式',
|
||||
|
||||
// 重复字符相关
|
||||
no_repetition: '无明显重复',
|
||||
consecutive_repeats: '包含连续重复字符',
|
||||
high_repetition: '字符重复率过高',
|
||||
moderate_repetition: '字符重复率适中'
|
||||
},
|
||||
|
||||
// 改进建议
|
||||
suggestions: {
|
||||
increase_length: '增加密码长度至少到8位',
|
||||
add_uppercase: '添加大写字母',
|
||||
add_lowercase: '添加小写字母',
|
||||
add_numbers: '添加数字',
|
||||
add_special: '添加特殊字符(!@#$%^&*等)',
|
||||
avoid_common: '避免使用常见密码',
|
||||
avoid_patterns: '避免使用键盘模式或连续字符',
|
||||
reduce_repetition: '减少字符重复'
|
||||
}
|
||||
},
|
||||
|
||||
// 语言切换
|
||||
language: {
|
||||
label: '语言',
|
||||
chinese: '中文',
|
||||
english: 'English'
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './index.css'
|
||||
import i18n from './locales/index.js'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
/**
|
||||
* 创建Vue应用实例并配置国际化
|
||||
* Create Vue app instance and configure internationalization
|
||||
*/
|
||||
const app = createApp(App)
|
||||
app.use(i18n)
|
||||
app.mount('#app')
|
||||
262
src/utils/passwordAlgorithms.js
Normal file
262
src/utils/passwordAlgorithms.js
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 密码生成算法工具类
|
||||
* Password generation algorithms utility
|
||||
*/
|
||||
|
||||
/**
|
||||
* 生成随机字节数组
|
||||
* Generate random bytes array
|
||||
* @param {number} length - 字节长度
|
||||
* @returns {Uint8Array} 随机字节数组
|
||||
*/
|
||||
export const getRandomBytes = (length) => {
|
||||
const array = new Uint8Array(length)
|
||||
window.crypto.getRandomValues(array)
|
||||
return array
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-512 + 随机盐算法(当前默认算法)
|
||||
* SHA-512 + Random Salt Algorithm (Current default)
|
||||
* @param {string} memoryPassword - 记忆密码
|
||||
* @param {string} distinguishCode - 区分代码
|
||||
* @param {number} length - 密码长度
|
||||
* @returns {Promise<Uint8Array>} 生成的哈希数组
|
||||
*/
|
||||
export const sha512WithSalt = async (memoryPassword, distinguishCode, length) => {
|
||||
const encoder = new TextEncoder()
|
||||
const baseData = encoder.encode(memoryPassword + distinguishCode)
|
||||
|
||||
// 添加随机因子,确保每次生成的密码都不同
|
||||
const randomSalt = getRandomBytes(16)
|
||||
const combinedData = new Uint8Array(baseData.length + randomSalt.length)
|
||||
combinedData.set(baseData)
|
||||
combinedData.set(randomSalt, baseData.length)
|
||||
|
||||
const hashBuffer = await window.crypto.subtle.digest('SHA-512', combinedData)
|
||||
return new Uint8Array(hashBuffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* PBKDF2 算法
|
||||
* PBKDF2 Algorithm
|
||||
* @param {string} memoryPassword - 记忆密码
|
||||
* @param {string} distinguishCode - 区分代码
|
||||
* @param {number} length - 密码长度
|
||||
* @param {number} iterations - 迭代次数
|
||||
* @returns {Promise<Uint8Array>} 生成的密钥数组
|
||||
*/
|
||||
export const pbkdf2Algorithm = async (memoryPassword, distinguishCode, length, iterations = 100000) => {
|
||||
const encoder = new TextEncoder()
|
||||
const password = encoder.encode(memoryPassword)
|
||||
const salt = encoder.encode(distinguishCode + Date.now().toString())
|
||||
|
||||
// 导入密码作为密钥材料
|
||||
const keyMaterial = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
password,
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits']
|
||||
)
|
||||
|
||||
// 使用PBKDF2派生密钥
|
||||
const derivedBits = await window.crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: iterations,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
length * 8 // 转换为位数
|
||||
)
|
||||
|
||||
return new Uint8Array(derivedBits)
|
||||
}
|
||||
|
||||
/**
|
||||
* 双重哈希算法(SHA-256 + SHA-512)
|
||||
* Double Hash Algorithm (SHA-256 + SHA-512)
|
||||
* @param {string} memoryPassword - 记忆密码
|
||||
* @param {string} distinguishCode - 区分代码
|
||||
* @param {number} length - 密码长度
|
||||
* @returns {Promise<Uint8Array>} 生成的哈希数组
|
||||
*/
|
||||
export const doubleHashAlgorithm = async (memoryPassword, distinguishCode, length) => {
|
||||
const encoder = new TextEncoder()
|
||||
const baseData = encoder.encode(memoryPassword + distinguishCode)
|
||||
|
||||
// 第一次SHA-256哈希
|
||||
const firstHash = await window.crypto.subtle.digest('SHA-256', baseData)
|
||||
|
||||
// 添加时间戳和随机盐
|
||||
const timestamp = new TextEncoder().encode(Date.now().toString())
|
||||
const randomSalt = getRandomBytes(8)
|
||||
const combinedData = new Uint8Array(firstHash.byteLength + timestamp.length + randomSalt.length)
|
||||
combinedData.set(new Uint8Array(firstHash))
|
||||
combinedData.set(timestamp, firstHash.byteLength)
|
||||
combinedData.set(randomSalt, firstHash.byteLength + timestamp.length)
|
||||
|
||||
// 第二次SHA-512哈希
|
||||
const secondHash = await window.crypto.subtle.digest('SHA-512', combinedData)
|
||||
return new Uint8Array(secondHash)
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间基础算法(基于当前时间的确定性算法)
|
||||
* Time-based Algorithm (Deterministic algorithm based on current time)
|
||||
* @param {string} memoryPassword - 记忆密码
|
||||
* @param {string} distinguishCode - 区分代码
|
||||
* @param {number} length - 密码长度
|
||||
* @param {number} timeWindow - 时间窗口(分钟),默认30分钟
|
||||
* @returns {Promise<Uint8Array>} 生成的哈希数组
|
||||
*/
|
||||
export const timeBasedAlgorithm = async (memoryPassword, distinguishCode, length, timeWindow = 30) => {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
// 计算时间窗口(每30分钟一个窗口)
|
||||
const currentTime = Math.floor(Date.now() / (timeWindow * 60 * 1000))
|
||||
const timeData = encoder.encode(currentTime.toString())
|
||||
|
||||
const baseData = encoder.encode(memoryPassword + distinguishCode)
|
||||
const combinedData = new Uint8Array(baseData.length + timeData.length)
|
||||
combinedData.set(baseData)
|
||||
combinedData.set(timeData, baseData.length)
|
||||
|
||||
const hashBuffer = await window.crypto.subtle.digest('SHA-512', combinedData)
|
||||
return new Uint8Array(hashBuffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义混合算法(结合多种哈希方法)
|
||||
* Custom Hybrid Algorithm (Combining multiple hash methods)
|
||||
* @param {string} memoryPassword - 记忆密码
|
||||
* @param {string} distinguishCode - 区分代码
|
||||
* @param {number} length - 密码长度
|
||||
* @returns {Promise<Uint8Array>} 生成的哈希数组
|
||||
*/
|
||||
export const hybridAlgorithm = async (memoryPassword, distinguishCode, length) => {
|
||||
const encoder = new TextEncoder()
|
||||
const baseData = encoder.encode(memoryPassword + distinguishCode)
|
||||
|
||||
// 使用多种哈希算法
|
||||
const sha1Hash = await window.crypto.subtle.digest('SHA-1', baseData)
|
||||
const sha256Hash = await window.crypto.subtle.digest('SHA-256', baseData)
|
||||
const sha512Hash = await window.crypto.subtle.digest('SHA-512', baseData)
|
||||
|
||||
// 组合所有哈希结果
|
||||
const combinedLength = sha1Hash.byteLength + sha256Hash.byteLength + sha512Hash.byteLength
|
||||
const combinedHash = new Uint8Array(combinedLength)
|
||||
combinedHash.set(new Uint8Array(sha1Hash))
|
||||
combinedHash.set(new Uint8Array(sha256Hash), sha1Hash.byteLength)
|
||||
combinedHash.set(new Uint8Array(sha512Hash), sha1Hash.byteLength + sha256Hash.byteLength)
|
||||
|
||||
// 添加随机盐并进行最终哈希
|
||||
const randomSalt = getRandomBytes(16)
|
||||
const finalData = new Uint8Array(combinedHash.length + randomSalt.length)
|
||||
finalData.set(combinedHash)
|
||||
finalData.set(randomSalt, combinedHash.length)
|
||||
|
||||
const finalHash = await window.crypto.subtle.digest('SHA-512', finalData)
|
||||
return new Uint8Array(finalHash)
|
||||
}
|
||||
|
||||
/**
|
||||
* 算法配置对象
|
||||
* Algorithm configuration object
|
||||
*/
|
||||
export const algorithms = {
|
||||
'sha512-salt': {
|
||||
name: 'SHA-512 + 随机盐',
|
||||
nameEn: 'SHA-512 + Random Salt',
|
||||
description: '使用SHA-512哈希算法配合随机盐,提供良好的安全性和随机性',
|
||||
descriptionEn: 'Uses SHA-512 hash algorithm with random salt, providing good security and randomness',
|
||||
security: 'high',
|
||||
deterministic: false,
|
||||
function: sha512WithSalt
|
||||
},
|
||||
'pbkdf2': {
|
||||
name: 'PBKDF2',
|
||||
nameEn: 'PBKDF2',
|
||||
description: '基于密码的密钥派生函数,使用10万次迭代,抗暴力破解能力强',
|
||||
descriptionEn: 'Password-Based Key Derivation Function with 100,000 iterations, strong against brute force attacks',
|
||||
security: 'very-high',
|
||||
deterministic: false,
|
||||
function: pbkdf2Algorithm
|
||||
},
|
||||
'double-hash': {
|
||||
name: '双重哈希',
|
||||
nameEn: 'Double Hash',
|
||||
description: '先使用SHA-256再使用SHA-512,双重加密提高安全性',
|
||||
descriptionEn: 'First SHA-256 then SHA-512, double encryption for enhanced security',
|
||||
security: 'high',
|
||||
deterministic: false,
|
||||
function: doubleHashAlgorithm
|
||||
},
|
||||
'time-based': {
|
||||
name: '时间基础',
|
||||
nameEn: 'Time-based',
|
||||
description: '基于30分钟时间窗口的确定性算法,同一时间段生成相同密码',
|
||||
descriptionEn: 'Deterministic algorithm based on 30-minute time windows, generates same password within time period',
|
||||
security: 'medium',
|
||||
deterministic: true,
|
||||
function: timeBasedAlgorithm
|
||||
},
|
||||
'hybrid': {
|
||||
name: '混合算法',
|
||||
nameEn: 'Hybrid Algorithm',
|
||||
description: '结合SHA-1、SHA-256、SHA-512多种哈希算法,提供最高安全性',
|
||||
descriptionEn: 'Combines SHA-1, SHA-256, SHA-512 hash algorithms for maximum security',
|
||||
security: 'very-high',
|
||||
deterministic: false,
|
||||
function: hybridAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据算法类型生成密码
|
||||
* Generate password based on algorithm type
|
||||
* @param {string} algorithmType - 算法类型
|
||||
* @param {string} memoryPassword - 记忆密码
|
||||
* @param {string} distinguishCode - 区分代码
|
||||
* @param {number} passwordLength - 密码长度
|
||||
* @param {object} options - 密码选项
|
||||
* @returns {Promise<string>} 生成的密码
|
||||
*/
|
||||
export const generatePasswordWithAlgorithm = async (algorithmType, memoryPassword, distinguishCode, passwordLength, options) => {
|
||||
const algorithm = algorithms[algorithmType]
|
||||
if (!algorithm) {
|
||||
throw new Error(`未知的算法类型: ${algorithmType}`)
|
||||
}
|
||||
|
||||
// 获取哈希数组
|
||||
const hashArray = await algorithm.function(memoryPassword, distinguishCode, passwordLength)
|
||||
|
||||
// 字符集定义
|
||||
const chars = {
|
||||
lowercase: 'abcdefghijklmnopqrstuvwxyz',
|
||||
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
numbers: '0123456789',
|
||||
special: '!@#$%^&*()_+-=[]{}|;:,.<>?'
|
||||
}
|
||||
|
||||
// 构建可用字符集
|
||||
let availableChars = chars.lowercase
|
||||
if (options.useUpperCase) availableChars += chars.uppercase
|
||||
if (options.useNumbers) availableChars += chars.numbers
|
||||
if (options.useSpecialChars) availableChars += chars.special
|
||||
|
||||
// 生成密码
|
||||
let password = ''
|
||||
const randomValues = algorithm.deterministic ? new Uint8Array(passwordLength) : getRandomBytes(passwordLength * 2)
|
||||
|
||||
for (let i = 0; i < passwordLength; i++) {
|
||||
const combinedEntropy = algorithm.deterministic
|
||||
? hashArray[i % hashArray.length]
|
||||
: (hashArray[i % hashArray.length] + randomValues[i]) % availableChars.length
|
||||
password += availableChars[combinedEntropy % availableChars.length]
|
||||
}
|
||||
|
||||
return password
|
||||
}
|
||||
344
src/utils/passwordStrengthAnalyzer.js
Normal file
344
src/utils/passwordStrengthAnalyzer.js
Normal file
@@ -0,0 +1,344 @@
|
||||
/**
|
||||
* 密码强度分析工具类
|
||||
* 提供全面的密码强度评估和详细分析
|
||||
*/
|
||||
|
||||
// 常见弱密码列表
|
||||
const COMMON_PASSWORDS = [
|
||||
'123456', 'password', '123456789', '12345678', '12345', '1234567',
|
||||
'qwerty', 'abc123', 'password123', 'admin', 'letmein', 'welcome',
|
||||
'123123', 'password1', '1234', 'qwerty123', 'iloveyou', 'princess',
|
||||
'monkey', 'dragon', '000000', '111111', '666666', '888888'
|
||||
];
|
||||
|
||||
// 常见键盘模式
|
||||
const KEYBOARD_PATTERNS = [
|
||||
'qwerty', 'asdf', 'zxcv', '1234', 'abcd', 'qwertyuiop',
|
||||
'asdfghjkl', 'zxcvbnm', '123456789', 'abcdefg'
|
||||
];
|
||||
|
||||
// 常见重复模式
|
||||
const REPEAT_PATTERNS = [
|
||||
'aa', 'bb', 'cc', '11', '22', '33', 'aaa', 'bbb', '111', '222'
|
||||
];
|
||||
|
||||
/**
|
||||
* 分析密码强度
|
||||
* @param {string} password - 要分析的密码
|
||||
* @returns {Object} 密码强度分析结果
|
||||
*/
|
||||
export function analyzePasswordStrength(password) {
|
||||
if (!password) {
|
||||
return {
|
||||
score: 0,
|
||||
level: 'very-weak',
|
||||
percentage: 0,
|
||||
details: {
|
||||
length: { score: 0, passed: false, message: 'password_empty' },
|
||||
variety: { score: 0, passed: false, message: 'no_characters' },
|
||||
common: { score: 0, passed: false, message: 'password_empty' },
|
||||
patterns: { score: 0, passed: false, message: 'password_empty' },
|
||||
repetition: { score: 0, passed: false, message: 'password_empty' }
|
||||
},
|
||||
suggestions: ['add_length', 'add_variety', 'avoid_common']
|
||||
};
|
||||
}
|
||||
|
||||
const analysis = {
|
||||
length: analyzeLengthStrength(password),
|
||||
variety: analyzeCharacterVariety(password),
|
||||
common: analyzeCommonPasswords(password),
|
||||
patterns: analyzeKeyboardPatterns(password),
|
||||
repetition: analyzeRepetition(password)
|
||||
};
|
||||
|
||||
// 计算总分 (满分100)
|
||||
const totalScore = Math.round(
|
||||
analysis.length.score * 0.25 +
|
||||
analysis.variety.score * 0.25 +
|
||||
analysis.common.score * 0.2 +
|
||||
analysis.patterns.score * 0.15 +
|
||||
analysis.repetition.score * 0.15
|
||||
);
|
||||
|
||||
// 确定强度等级
|
||||
const level = getStrengthLevel(totalScore);
|
||||
|
||||
// 生成改进建议
|
||||
const suggestions = generateSuggestions(analysis);
|
||||
|
||||
return {
|
||||
score: totalScore,
|
||||
level,
|
||||
percentage: totalScore,
|
||||
details: analysis,
|
||||
suggestions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析密码长度强度
|
||||
* @param {string} password - 密码
|
||||
* @returns {Object} 长度分析结果
|
||||
*/
|
||||
function analyzeLengthStrength(password) {
|
||||
const length = password.length;
|
||||
let score = 0;
|
||||
let passed = false;
|
||||
let message = '';
|
||||
|
||||
if (length < 6) {
|
||||
score = Math.min(length * 10, 30);
|
||||
message = 'too_short';
|
||||
} else if (length < 8) {
|
||||
score = 40;
|
||||
message = 'short';
|
||||
} else if (length < 12) {
|
||||
score = 60;
|
||||
passed = true;
|
||||
message = 'good_length';
|
||||
} else if (length < 16) {
|
||||
score = 80;
|
||||
passed = true;
|
||||
message = 'very_good_length';
|
||||
} else {
|
||||
score = 100;
|
||||
passed = true;
|
||||
message = 'excellent_length';
|
||||
}
|
||||
|
||||
return { score, passed, message, value: length };
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析字符类型多样性
|
||||
* @param {string} password - 密码
|
||||
* @returns {Object} 字符多样性分析结果
|
||||
*/
|
||||
function analyzeCharacterVariety(password) {
|
||||
const hasLower = /[a-z]/.test(password);
|
||||
const hasUpper = /[A-Z]/.test(password);
|
||||
const hasDigits = /[0-9]/.test(password);
|
||||
const hasSpecial = /[^a-zA-Z0-9]/.test(password);
|
||||
|
||||
const varietyCount = [hasLower, hasUpper, hasDigits, hasSpecial].filter(Boolean).length;
|
||||
|
||||
let score = 0;
|
||||
let passed = false;
|
||||
let message = '';
|
||||
|
||||
switch (varietyCount) {
|
||||
case 0:
|
||||
score = 0;
|
||||
message = 'no_characters';
|
||||
break;
|
||||
case 1:
|
||||
score = 25;
|
||||
message = 'single_type';
|
||||
break;
|
||||
case 2:
|
||||
score = 50;
|
||||
message = 'two_types';
|
||||
break;
|
||||
case 3:
|
||||
score = 75;
|
||||
passed = true;
|
||||
message = 'three_types';
|
||||
break;
|
||||
case 4:
|
||||
score = 100;
|
||||
passed = true;
|
||||
message = 'all_types';
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
score,
|
||||
passed,
|
||||
message,
|
||||
details: { hasLower, hasUpper, hasDigits, hasSpecial, count: varietyCount }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为常见密码
|
||||
* @param {string} password - 密码
|
||||
* @returns {Object} 常见密码检查结果
|
||||
*/
|
||||
function analyzeCommonPasswords(password) {
|
||||
const lowerPassword = password.toLowerCase();
|
||||
const isCommon = COMMON_PASSWORDS.includes(lowerPassword);
|
||||
|
||||
// 检查是否包含常见密码作为子串
|
||||
const containsCommon = COMMON_PASSWORDS.some(common =>
|
||||
lowerPassword.includes(common) && common.length >= 4
|
||||
);
|
||||
|
||||
let score = 100;
|
||||
let passed = true;
|
||||
let message = 'not_common';
|
||||
|
||||
if (isCommon) {
|
||||
score = 0;
|
||||
passed = false;
|
||||
message = 'very_common';
|
||||
} else if (containsCommon) {
|
||||
score = 30;
|
||||
passed = false;
|
||||
message = 'contains_common';
|
||||
}
|
||||
|
||||
return { score, passed, message };
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析键盘模式
|
||||
* @param {string} password - 密码
|
||||
* @returns {Object} 键盘模式分析结果
|
||||
*/
|
||||
function analyzeKeyboardPatterns(password) {
|
||||
const lowerPassword = password.toLowerCase();
|
||||
|
||||
// 检查键盘模式
|
||||
const hasKeyboardPattern = KEYBOARD_PATTERNS.some(pattern =>
|
||||
lowerPassword.includes(pattern)
|
||||
);
|
||||
|
||||
// 检查连续字符 (如 abc, 123)
|
||||
const hasSequential = /(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|123|234|345|456|567|678|789)/.test(lowerPassword);
|
||||
|
||||
let score = 100;
|
||||
let passed = true;
|
||||
let message = 'no_patterns';
|
||||
|
||||
if (hasKeyboardPattern || hasSequential) {
|
||||
score = 20;
|
||||
passed = false;
|
||||
message = hasKeyboardPattern ? 'keyboard_pattern' : 'sequential_pattern';
|
||||
}
|
||||
|
||||
return { score, passed, message };
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析重复字符
|
||||
* @param {string} password - 密码
|
||||
* @returns {Object} 重复字符分析结果
|
||||
*/
|
||||
function analyzeRepetition(password) {
|
||||
// 检查连续重复字符 (如 aaa, 111)
|
||||
const hasConsecutiveRepeats = /(.)\1{2,}/.test(password);
|
||||
|
||||
// 计算字符重复率
|
||||
const charCount = {};
|
||||
for (const char of password) {
|
||||
charCount[char] = (charCount[char] || 0) + 1;
|
||||
}
|
||||
|
||||
const maxRepeat = Math.max(...Object.values(charCount));
|
||||
const repeatRatio = maxRepeat / password.length;
|
||||
|
||||
let score = 100;
|
||||
let passed = true;
|
||||
let message = 'no_repetition';
|
||||
|
||||
if (hasConsecutiveRepeats) {
|
||||
score = 10;
|
||||
passed = false;
|
||||
message = 'consecutive_repeats';
|
||||
} else if (repeatRatio > 0.5) {
|
||||
score = 30;
|
||||
passed = false;
|
||||
message = 'high_repetition';
|
||||
} else if (repeatRatio > 0.3) {
|
||||
score = 60;
|
||||
message = 'moderate_repetition';
|
||||
}
|
||||
|
||||
return { score, passed, message, repeatRatio: Math.round(repeatRatio * 100) };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分数确定强度等级
|
||||
* @param {number} score - 总分
|
||||
* @returns {string} 强度等级
|
||||
*/
|
||||
function getStrengthLevel(score) {
|
||||
if (score < 20) return 'very-weak';
|
||||
if (score < 40) return 'weak';
|
||||
if (score < 60) return 'fair';
|
||||
if (score < 80) return 'good';
|
||||
return 'strong';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成改进建议
|
||||
* @param {Object} analysis - 分析结果
|
||||
* @returns {Array} 建议列表
|
||||
*/
|
||||
function generateSuggestions(analysis) {
|
||||
const suggestions = [];
|
||||
|
||||
// 长度建议
|
||||
if (analysis.length.score < 60) {
|
||||
suggestions.push('increase_length');
|
||||
}
|
||||
|
||||
// 字符多样性建议
|
||||
if (analysis.variety.score < 75) {
|
||||
const { details } = analysis.variety;
|
||||
if (!details.hasUpper) suggestions.push('add_uppercase');
|
||||
if (!details.hasLower) suggestions.push('add_lowercase');
|
||||
if (!details.hasDigits) suggestions.push('add_numbers');
|
||||
if (!details.hasSpecial) suggestions.push('add_special');
|
||||
}
|
||||
|
||||
// 常见密码建议
|
||||
if (analysis.common.score < 50) {
|
||||
suggestions.push('avoid_common');
|
||||
}
|
||||
|
||||
// 模式建议
|
||||
if (analysis.patterns.score < 50) {
|
||||
suggestions.push('avoid_patterns');
|
||||
}
|
||||
|
||||
// 重复建议
|
||||
if (analysis.repetition.score < 50) {
|
||||
suggestions.push('reduce_repetition');
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取强度等级的颜色
|
||||
* @param {string} level - 强度等级
|
||||
* @returns {string} 颜色类名
|
||||
*/
|
||||
export function getStrengthColor(level) {
|
||||
const colors = {
|
||||
'very-weak': 'text-red-600',
|
||||
'weak': 'text-orange-500',
|
||||
'fair': 'text-yellow-500',
|
||||
'good': 'text-blue-500',
|
||||
'strong': 'text-green-600'
|
||||
};
|
||||
return colors[level] || 'text-gray-500';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取强度等级的背景颜色
|
||||
* @param {string} level - 强度等级
|
||||
* @returns {string} 背景颜色类名
|
||||
*/
|
||||
export function getStrengthBgColor(level) {
|
||||
const colors = {
|
||||
'very-weak': 'bg-red-500',
|
||||
'weak': 'bg-orange-500',
|
||||
'fair': 'bg-yellow-500',
|
||||
'good': 'bg-blue-500',
|
||||
'strong': 'bg-green-500'
|
||||
};
|
||||
return colors[level] || 'bg-gray-500';
|
||||
}
|
||||
Reference in New Issue
Block a user