chore: 添加初始项目文件和依赖项

初始化项目,添加 favicon.ico、screenshot.png 等静态资源文件,以及 Vue、TailwindCSS 等依赖项。配置了 Vite 和 PostCSS,并生成了基本的项目结构。
This commit is contained in:
2025-04-14 18:19:10 +08:00
commit 9218f57271
3497 changed files with 869949 additions and 0 deletions

320
src/App.vue Normal file
View 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
View 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
View 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
View 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
View 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])}}