chore: 添加初始项目文件和依赖项
初始化项目,添加 favicon.ico、screenshot.png 等静态资源文件,以及 Vue、TailwindCSS 等依赖项。配置了 Vite 和 PostCSS,并生成了基本的项目结构。
This commit is contained in:
320
src/App.vue
Normal file
320
src/App.vue
Normal file
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<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>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
安全、现代、易用的密码生成工具
|
||||
</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>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 记忆密码 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
记忆密码
|
||||
</label>
|
||||
<div class="relative rounded-md shadow-sm">
|
||||
<input
|
||||
v-model="memoryPassword"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
class="input pr-20"
|
||||
placeholder="输入你的记忆密码"
|
||||
/>
|
||||
<button
|
||||
@click="togglePasswordVisibility"
|
||||
class="absolute right-0 top-0 h-full px-3 border-l border-gray-300 dark:border-gray-600
|
||||
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 ? '隐藏' : '显示' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 区分代码 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
区分代码
|
||||
</label>
|
||||
<div class="relative rounded-md shadow-sm">
|
||||
<input
|
||||
v-model="distinguishCode"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="输入区分代码(如:qq)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码选项 -->
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
密码长度
|
||||
</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 }} 位
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生成的密码 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
生成的密码
|
||||
</label>
|
||||
<div class="flex space-x-2">
|
||||
<div class="relative flex-1">
|
||||
<input
|
||||
:value="generatedPassword"
|
||||
type="text"
|
||||
class="input py-1.5 font-mono"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@click="copyPassword"
|
||||
class="btn btn-primary py-1.5"
|
||||
>
|
||||
复制
|
||||
</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>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<button
|
||||
@click="generatePassword"
|
||||
class="w-full btn btn-primary py-2 text-base font-semibold"
|
||||
:disabled="isGenerating"
|
||||
>
|
||||
{{ isGenerating ? '生成中...' : '生成密码' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
setup() {
|
||||
const memoryPassword = ref('')
|
||||
const distinguishCode = ref('')
|
||||
const passwordLength = ref(16)
|
||||
const usePunctuation = ref(true)
|
||||
const useUpperCase = ref(true)
|
||||
const useNumbers = ref(true)
|
||||
const useSpecialChars = ref(true)
|
||||
const showPassword = ref(false)
|
||||
const generatedPassword = ref('')
|
||||
const isGenerating = ref(false)
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
showPassword.value = !showPassword.value
|
||||
}
|
||||
|
||||
// 生成随机字节数组
|
||||
const getRandomBytes = (length) => {
|
||||
const array = new Uint8Array(length)
|
||||
window.crypto.getRandomValues(array)
|
||||
return array
|
||||
}
|
||||
|
||||
const generatePassword = async () => {
|
||||
if (!memoryPassword.value || !distinguishCode.value) {
|
||||
alert('请填写记忆密码和区分代码')
|
||||
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: '!@#$%^&*()_+-=[]{}|;:,.<>?'
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
generatedPassword.value = password
|
||||
} catch (error) {
|
||||
console.error('密码生成失败:', error)
|
||||
alert('密码生成失败,请重试')
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const copyPassword = async () => {
|
||||
if (!generatedPassword.value) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(generatedPassword.value)
|
||||
alert('密码已复制到剪贴板')
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
// 如果 clipboard API 失败,使用传统方法
|
||||
const input = document.createElement('textarea')
|
||||
input.value = generatedPassword.value
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
alert('密码已复制到剪贴板')
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
})
|
||||
|
||||
return {
|
||||
memoryPassword,
|
||||
distinguishCode,
|
||||
passwordLength,
|
||||
usePunctuation,
|
||||
useUpperCase,
|
||||
useNumbers,
|
||||
useSpecialChars,
|
||||
showPassword,
|
||||
generatedPassword,
|
||||
isGenerating,
|
||||
togglePasswordVisibility,
|
||||
generatePassword,
|
||||
copyPassword,
|
||||
passwordStrengthPercentage,
|
||||
passwordStrengthText,
|
||||
passwordStrengthClass,
|
||||
passwordStrengthBarClass
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
23
src/index.css
Normal file
23
src/index.css
Normal file
@@ -0,0 +1,23 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply px-4 py-2 rounded-md font-medium transition-colors duration-200;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-primary-600 text-white hover:bg-primary-700;
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800;
|
||||
}
|
||||
}
|
||||
5
src/main.js
Normal file
5
src/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './index.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
118
src/seek_password.js
Normal file
118
src/seek_password.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* sha512加密密码
|
||||
* @param {记忆密码} pwd
|
||||
* @param {区分代码} key
|
||||
*/
|
||||
function hex_password(pwd, key) {
|
||||
var hexone = sha512.hmac(key, pwd);
|
||||
var hextwo = sha512.hmac("hello", hexone);
|
||||
var hexthree = sha512.hmac("world", hexone);
|
||||
|
||||
var source = hextwo.split("");
|
||||
var rule = hexthree.split("");
|
||||
console.assert(rule.length === source.length, "sha512长度错误!");
|
||||
|
||||
// 字母大小写转换
|
||||
for (var i = 0; i < source.length; ++i) {
|
||||
if (isNaN(source[i])) {
|
||||
var str = "whenthecatisawaythemicewillplay666";
|
||||
if (str.search(rule[i]) > -1) {
|
||||
source[i] = source[i].toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
return source.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成密码
|
||||
* @param {sha512加密后字符串} hash
|
||||
* @param {输出密码长度} length
|
||||
* @param {是否使用标点} rule_of_punctuation
|
||||
* @param {是否区分大小写} rule_of_letter
|
||||
*/
|
||||
function seek_password(hash, length, rule_of_punctuation, rule_of_letter) {
|
||||
// 生成字符表
|
||||
var lower = "abcdefghijklmnopqrstuvwxyz".split("");
|
||||
var upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||
var number = "0123456789".split("");
|
||||
var punctuation = "~*-+()!@#$^&".split("");
|
||||
var alphabet = lower.concat(number);
|
||||
if (parseInt(rule_of_punctuation) == 1) {
|
||||
alphabet = alphabet.concat(punctuation);
|
||||
}
|
||||
if (parseInt(rule_of_letter) == 1) {
|
||||
alphabet = alphabet.concat(upper);
|
||||
}
|
||||
|
||||
// 生成密码
|
||||
// 从0开始截取长度为length的字符串,直到满足密码复杂度为止
|
||||
for (var i = 0; i <= hash.length - length; ++i) {
|
||||
var sub_hash = hash.slice(i, i + parseInt(length)).split("");
|
||||
var count = 0;
|
||||
var map_index = sub_hash.map(function(c) {
|
||||
count = (count + c.charCodeAt()) % alphabet.length;
|
||||
return count;
|
||||
});
|
||||
var sk_pwd = map_index.map(function(k) {
|
||||
return alphabet[k];
|
||||
});
|
||||
|
||||
// 验证密码
|
||||
var matched = [false, false, false, false];
|
||||
sk_pwd.forEach(function(e) {
|
||||
matched[0] = matched[0] || lower.includes(e);
|
||||
matched[1] = matched[1] || upper.includes(e);
|
||||
matched[2] = matched[2] || number.includes(e);
|
||||
matched[3] = matched[3] || punctuation.includes(e);
|
||||
});
|
||||
if (parseInt(rule_of_letter) == -1) {
|
||||
matched[1] = true;
|
||||
}
|
||||
if (parseInt(rule_of_punctuation) == -1) {
|
||||
matched[3] = true;
|
||||
}
|
||||
if (!matched.includes(false)) {
|
||||
return sk_pwd.join("");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下拉选择框内容
|
||||
* @param {id} select_id
|
||||
*/
|
||||
function get_select_option(select_id) {
|
||||
var select = document.getElementById(select_id);
|
||||
var select_index = select.selectedIndex;
|
||||
return [
|
||||
select.options[select_index].value,
|
||||
select.options[select_index].text
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成密码
|
||||
*/
|
||||
function generate_password() {
|
||||
//获取页面传过来的值
|
||||
var pwd = document.getElementById("pwd").value;
|
||||
var key = document.getElementById("key").value;
|
||||
var rule_of_punctuation = get_select_option("rule_of_punctuation");
|
||||
var rule_of_letter = get_select_option("rule_of_letter");
|
||||
var pwd_length = get_select_option("pwd_length");
|
||||
|
||||
//加密
|
||||
if (pwd && key) {
|
||||
var hash = hex_password(pwd, key);
|
||||
console.assert(hash.length === 128, "hash长度不是128位!");
|
||||
var sk_pwd = seek_password(
|
||||
hash,
|
||||
pwd_length[0],
|
||||
rule_of_punctuation[0],
|
||||
rule_of_letter[0]
|
||||
);
|
||||
return sk_pwd;
|
||||
}
|
||||
}
|
||||
1
src/seek_password.min.js
vendored
Normal file
1
src/seek_password.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
function hex_password(pwd,key){var hexone=sha512.hmac(key,pwd),hextwo=sha512.hmac("hello",hexone),hexthree=sha512.hmac("world",hexone),source=hextwo.split(""),rule=hexthree.split("");console.assert(rule.length===source.length,"sha512长度错误!");for(var i=0;i<source.length;++i){var str;if(isNaN(source[i]))"whenthecatisawaythemicewillplay".search(rule[i])>-1&&(source[i]=source[i].toUpperCase())}return source.join("")}function seek_password(hash,length,rule_of_punctuation,rule_of_letter){var lower="abcdefghijklmnopqrstuvwxyz".split(""),upper="ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""),number="0123456789".split(""),punctuation=",.:;!?".split(""),alphabet=lower.concat(number);1==parseInt(rule_of_punctuation)&&(alphabet=alphabet.concat(punctuation)),1==parseInt(rule_of_letter)&&(alphabet=alphabet.concat(upper));for(var i=0;i<=hash.length-length;++i){var sub_hash=hash.slice(i,i+parseInt(length)).split(""),count=0,map_index,sk_pwd=sub_hash.map(function(c){return count=(count+c.charCodeAt())%alphabet.length}).map(function(k){return alphabet[k]}),matched=[!1,!1,!1,!1];if(sk_pwd.forEach(function(e){matched[0]=matched[0]||lower.includes(e),matched[1]=matched[1]||upper.includes(e),matched[2]=matched[2]||number.includes(e),matched[3]=matched[3]||punctuation.includes(e)}),-1==parseInt(rule_of_letter)&&(matched[1]=!0),-1==parseInt(rule_of_punctuation)&&(matched[3]=!0),!matched.includes(!1))return sk_pwd.join("")}return""}function get_select_option(select_id){var select=document.getElementById(select_id),select_index=select.selectedIndex;return[select.options[select_index].value,select.options[select_index].text]}function generate_password(){var pwd=document.getElementById("pwd").value,key=document.getElementById("key").value,rule_of_punctuation=get_select_option("rule_of_punctuation"),rule_of_letter=get_select_option("rule_of_letter"),pwd_length=get_select_option("pwd_length");if(pwd&&key){var hash=hex_password(pwd,key),sk_pwd;return console.assert(128===hash.length,"hash长度不是128位!"),seek_password(hash,pwd_length[0],rule_of_punctuation[0],rule_of_letter[0])}}
|
||||
Reference in New Issue
Block a user