使用 Vue3 + Element Plus + Tailwind CSS + CDN
密码保存在 localStorage
演示:[链接登录后可见]
本文转载自:[链接登录后可见]
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>密码生成器</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
<script src="https://unpkg.com/element-plus/dist/index.full.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 min-h-screen p-4">
<div id="app">
<div class="max-w-[80vw] mx-auto my-8">
<el-card class="!border-gray-200 !shadow-md">
<template #header>
<h2 class="text-xl font-bold text-gray-800">随机密码生成器</h2>
</template>
<div class="space-y-3 mb-5">
<el-checkbox-group v-model="selectedChars" class="grid grid-cols-2 gap-3">
<el-checkbox label="lower">小写字母 (a-z)</el-checkbox>
<el-checkbox label="upper">大写字母 (A-Z)</el-checkbox>
<el-checkbox label="number">数字 (0-9)</el-checkbox>
<el-checkbox label="symbol">特殊符号 (!@#$%^&*)</el-checkbox>
</el-checkbox-group>
<div class="pt-2 space-y-2 border-t border-gray-200">
<el-checkbox v-model="firstIsLetter">首个字符是字母</el-checkbox>
<el-checkbox
v-model="firstIsUpperCase"
:disabled="!firstIsLetter">
首个字符是大写字母
</el-checkbox>
</div>
</div>
<div class="mb-5">
<div class="flex items-center gap-4">
<span class="text-gray-600 w-12">长度:</span>
<el-slider
v-model="length"
:min="4"
:max="128"
class="flex-1"
show-input/>
</div>
</div>
<div class="bg-gray-100 p-3 rounded-md mb-5 text-sm text-gray-600 break-all">
当前字符集: {{ characterSet }}
</div>
<div class="flex gap-2 mb-4">
<el-input
v-model="generatedPassword"
readonly
placeholder="生成的密码"
@click="copyPassword"
class="flex-1">
<template #append>
<el-button
type="primary"
@click="generatePassword"
:disabled="!canGenerate"
class="!px-4">
生成
</el-button>
</template>
</el-input>
</div>
<el-button
type="success"
@click="savePassword"
:disabled="!generatedPassword"
class="w-full mb-4">
保存密码
</el-button>
<div v-if="savedPasswords.length" class="mt-6">
<div class="text-gray-600 font-medium mb-3">已保存密码:</div>
<div class="space-y-2">
<div
v-for="(pwd, index) in savedPasswords"
:key="index"
class="flex items-center justify-between bg-gray-50 p-3 rounded border border-gray-200">
<span class="font-mono text-sm">{{ pwd }}</span>
<div class="flex gap-2">
<el-button
size="small"
@click="copySavedPassword(pwd)"
class="!text-gray-600 hover:!text-blue-500">
复制
</el-button>
<el-button
size="small"
@click="removePassword(index)"
class="!text-red-500 hover:!text-red-600">
删除
</el-button>
</div>
</div>
</div>
</div>
</el-card>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted } = Vue;
const ElementPlus = window.ElementPlus;
createApp({
setup() {
const charSets = {
lower: 'abcdefghijklmnopqrstuvwxyz',
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
number: '0123456789',
symbol: '!@#$%^&*'
};
const firstIsLetter = ref(false);
const firstIsUpperCase = ref(false);
const selectedChars = ref(['lower', 'upper', 'number']);
const length = ref(12);
const generatedPassword = ref('');
const savedPasswords = ref([]);
onMounted(() => {
const saved = localStorage.getItem('savedPasswords');
if (saved) {
savedPasswords.value = JSON.parse(saved);
}
});
const characterSet = computed(() => {
return selectedChars.value
.map(type => charSets[type])
.join('');
});
const canGenerate = computed(() => {
const hasChars = selectedChars.value.length > 0;
const validLength = length.value >= 4 && length.value <= 128;
let validFirstChar = true;
if (firstIsUpperCase.value) {
validFirstChar = selectedChars.value.includes('upper');
} else if (firstIsLetter.value) {
validFirstChar = selectedChars.value.includes('lower') ||
selectedChars.value.includes('upper');
}
return hasChars && validLength && validFirstChar;
});
const generatePassword = () => {
if (!canGenerate.value) return;
let password = '';
const chars = characterSet.value;
let firstCharSet = chars;
if (firstIsUpperCase.value) {
firstCharSet = charSets.upper;
} else if (firstIsLetter.value) {
firstCharSet = [
...(selectedChars.value.includes('lower') ? charSets.lower : ''),
...(selectedChars.value.includes('upper') ? charSets.upper : '')
].join('');
}
const firstRandom = new Uint32Array(1);
crypto.getRandomValues(firstRandom);
password += firstCharSet[firstRandom[0] % firstCharSet.length];
if (length.value > 1) {
const remainingValues = new Uint32Array(length.value - 1);
crypto.getRandomValues(remainingValues);
for (let i = 0; i < length.value - 1; i++) {
password += chars[remainingValues[i] % chars.length];
}
}
generatedPassword.value = password;
};
const copyPassword = () => {
if (generatedPassword.value) {
navigator.clipboard.writeText(generatedPassword.value);
ElementPlus.ElMessage.success('已复制到剪贴板');
}
};
const savePassword = () => {
if (generatedPassword.value) {
savedPasswords.value.unshift(generatedPassword.value);
localStorage.setItem('savedPasswords', JSON.stringify(savedPasswords.value));
ElementPlus.ElMessage.success('密码已保存');
}
};
const copySavedPassword = (password) => {
navigator.clipboard.writeText(password);
ElementPlus.ElMessage.success('已复制到剪贴板');
};
const removePassword = (index) => {
savedPasswords.value.splice(index, 1);
localStorage.setItem('savedPasswords', JSON.stringify(savedPasswords.value));
ElementPlus.ElMessage.success('已删除');
};
return {
selectedChars,
length,
generatedPassword,
savedPasswords,
characterSet,
canGenerate,
generatePassword,
copyPassword,
savePassword,
copySavedPassword,
removePassword,
firstIsLetter,
firstIsUpperCase
};
}
})
.use(ElementPlus)
.mount('#app');
</script>
</body>
</html>