2026-10-28 11:52:37 +08:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="zh-CN" data-theme="dark">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
|
|
|
<title>Simple Password · 密码生成器</title>
|
|
|
|
|
<meta name="description" content="一个干净、纯本地的密码生成器。可调长度、自选字符组成,生成即复制。所有运算在你的浏览器里完成。">
|
|
|
|
|
<meta name="color-scheme" content="dark light">
|
|
|
|
|
<meta name="theme-color" content="#0a0b0f">
|
|
|
|
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%236366f1'/%3E%3Cpath d='M24 30v-4a8 8 0 0 1 16 0v4' stroke='white' stroke-width='4' fill='none' stroke-linecap='round'/%3E%3Crect x='20' y='30' width='24' height='17' rx='5' fill='white'/%3E%3C/svg%3E">
|
|
|
|
|
<script>
|
|
|
|
|
/* 主题要在首帧之前定下来,避免白屏闪烁 */
|
|
|
|
|
(function () {
|
|
|
|
|
var t;
|
|
|
|
|
try { t = localStorage.getItem('sp.theme.v1'); } catch (e) {}
|
|
|
|
|
if (t !== 'dark' && t !== 'light') {
|
|
|
|
|
t = (window.matchMedia && matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark';
|
|
|
|
|
}
|
|
|
|
|
document.documentElement.setAttribute('data-theme', t);
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
<style>
|
|
|
|
|
/* ============================================================
|
|
|
|
|
设计变量
|
|
|
|
|
============================================================ */
|
|
|
|
|
:root {
|
|
|
|
|
--sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
|
|
|
|
|
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
|
|
|
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
|
|
|
|
"Liberation Mono", "Courier New", monospace;
|
|
|
|
|
|
|
|
|
|
--bg: #0a0b0f;
|
|
|
|
|
--bg-soft: #101218;
|
|
|
|
|
--glow-a: rgba(99, 102, 241, .22);
|
|
|
|
|
--glow-b: rgba(168, 85, 247, .16);
|
|
|
|
|
--glow-c: rgba(34, 211, 238, .10);
|
|
|
|
|
|
|
|
|
|
--surface: rgba(20, 22, 30, .72);
|
|
|
|
|
--surface-2: rgba(255, 255, 255, .035);
|
2026-10-28 13:57:29 +08:00
|
|
|
/* 比 surface-2 更实的填充层:用于需要"看起来是个实体按钮"的次操作 */
|
|
|
|
|
--surface-3: rgba(255, 255, 255, .085);
|
2026-10-28 11:52:37 +08:00
|
|
|
--pw-bg: rgba(255, 255, 255, .045);
|
|
|
|
|
|
|
|
|
|
--line: rgba(255, 255, 255, .085);
|
|
|
|
|
--line-2: rgba(255, 255, 255, .16);
|
|
|
|
|
|
|
|
|
|
--text: #eef1f7;
|
|
|
|
|
--text-2: #a4abbb;
|
|
|
|
|
--text-3: #868da0;
|
|
|
|
|
|
|
|
|
|
--accent: #8b87ff;
|
|
|
|
|
--accent-solid: #6366f1;
|
|
|
|
|
--accent-2: #c084fc;
|
|
|
|
|
--accent-soft: rgba(139, 135, 255, .14);
|
|
|
|
|
|
|
|
|
|
--ok: #34d399;
|
|
|
|
|
--warn: #fbbf24;
|
|
|
|
|
--bad: #f87171;
|
|
|
|
|
|
|
|
|
|
--r-lg: 26px;
|
|
|
|
|
--r-md: 16px;
|
|
|
|
|
--r-sm: 11px;
|
|
|
|
|
|
|
|
|
|
--shadow: 0 1px 0 rgba(255, 255, 255, .045) inset,
|
|
|
|
|
0 30px 70px -34px rgba(0, 0, 0, .95);
|
|
|
|
|
--ring: 0 0 0 3px var(--accent-soft);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-theme="light"] {
|
|
|
|
|
--bg: #f4f5f8;
|
|
|
|
|
--bg-soft: #eceef4;
|
|
|
|
|
--glow-a: rgba(99, 102, 241, .16);
|
|
|
|
|
--glow-b: rgba(168, 85, 247, .13);
|
|
|
|
|
--glow-c: rgba(14, 165, 233, .12);
|
|
|
|
|
|
|
|
|
|
--surface: rgba(255, 255, 255, .78);
|
|
|
|
|
--surface-2: rgba(16, 18, 27, .028);
|
2026-10-28 13:57:29 +08:00
|
|
|
--surface-3: rgba(16, 18, 27, .062);
|
2026-10-28 11:52:37 +08:00
|
|
|
--pw-bg: rgba(16, 18, 27, .035);
|
|
|
|
|
|
|
|
|
|
--line: rgba(16, 18, 27, .085);
|
|
|
|
|
--line-2: rgba(16, 18, 27, .15);
|
|
|
|
|
|
|
|
|
|
--text: #12141b;
|
|
|
|
|
--text-2: #565c6b;
|
|
|
|
|
--text-3: #686d7c;
|
|
|
|
|
|
|
|
|
|
--accent: #4f46e5;
|
|
|
|
|
--accent-solid: #4f46e5;
|
|
|
|
|
--accent-2: #9333ea;
|
|
|
|
|
--accent-soft: rgba(79, 70, 229, .10);
|
|
|
|
|
|
|
|
|
|
--ok: #0f9d76;
|
|
|
|
|
--warn: #b45309;
|
|
|
|
|
--bad: #dc2626;
|
|
|
|
|
|
|
|
|
|
--shadow: 0 1px 2px rgba(16, 18, 27, .04),
|
|
|
|
|
0 26px 60px -30px rgba(16, 18, 27, .35);
|
|
|
|
|
--ring: 0 0 0 3px var(--accent-soft);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
基础
|
|
|
|
|
============================================================ */
|
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
|
|
|
|
|
|
html { -webkit-text-size-adjust: 100%; }
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
margin: 0;
|
|
|
|
|
min-height: 100dvh;
|
|
|
|
|
font-family: var(--sans);
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
line-height: 1.55;
|
|
|
|
|
color: var(--text);
|
|
|
|
|
background: var(--bg);
|
|
|
|
|
-webkit-font-smoothing: antialiased;
|
|
|
|
|
-moz-osx-font-smoothing: grayscale;
|
|
|
|
|
text-rendering: optimizeLegibility;
|
|
|
|
|
transition: background-color .35s ease, color .35s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 背景光晕 */
|
|
|
|
|
body::before {
|
|
|
|
|
content: "";
|
|
|
|
|
position: fixed;
|
|
|
|
|
inset: 0;
|
|
|
|
|
z-index: -1;
|
|
|
|
|
background:
|
|
|
|
|
radial-gradient(46rem 32rem at 8% -12%, var(--glow-a), transparent 62%),
|
|
|
|
|
radial-gradient(40rem 30rem at 96% 4%, var(--glow-b), transparent 58%),
|
|
|
|
|
radial-gradient(52rem 34rem at 50% 112%, var(--glow-c), transparent 60%),
|
|
|
|
|
var(--bg);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::selection { background: var(--accent-soft); }
|
|
|
|
|
|
|
|
|
|
:focus-visible {
|
|
|
|
|
outline: none;
|
|
|
|
|
box-shadow: var(--ring);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
布局
|
|
|
|
|
============================================================ */
|
|
|
|
|
.page {
|
|
|
|
|
width: 100%;
|
|
|
|
|
max-width: 1080px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
padding: clamp(20px, 3.4vw, 38px) clamp(16px, 3vw, 28px) 64px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.topbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-bottom: clamp(18px, 3vw, 28px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 13px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.logo {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
flex: none;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
display: grid;
|
|
|
|
|
place-items: center;
|
|
|
|
|
color: #fff;
|
|
|
|
|
background: linear-gradient(145deg, #6366f1, #a855f7 62%, #7c3aed);
|
|
|
|
|
box-shadow: 0 8px 20px -8px rgba(99, 102, 241, .85),
|
|
|
|
|
0 1px 0 rgba(255, 255, 255, .3) inset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.logo svg { width: 21px; height: 21px; display: block; }
|
|
|
|
|
|
|
|
|
|
.brand-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
|
|
|
|
|
.brand-text strong {
|
|
|
|
|
font-size: 15.5px;
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
letter-spacing: -.01em;
|
|
|
|
|
}
|
|
|
|
|
.brand-text span {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
letter-spacing: .1em;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon-btn {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
flex: none;
|
|
|
|
|
display: grid;
|
|
|
|
|
place-items: center;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
color: var(--text-2);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: color .18s, background .18s, border-color .18s, transform .18s;
|
|
|
|
|
}
|
|
|
|
|
.icon-btn:hover { color: var(--text); border-color: var(--line-2); transform: translateY(-1px); }
|
|
|
|
|
.icon-btn:active { transform: translateY(0) scale(.97); }
|
|
|
|
|
.icon-btn svg { width: 18px; height: 18px; }
|
|
|
|
|
.icon-btn .sun { display: none; }
|
|
|
|
|
[data-theme="light"] .icon-btn .sun { display: block; }
|
|
|
|
|
[data-theme="light"] .icon-btn .moon { display: none; }
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
主卡片
|
2026-10-28 13:57:29 +08:00
|
|
|
左 = 设置,右 = 输出。
|
|
|
|
|
语义是「左侧可选地调设置,右侧直接完成主操作」——
|
|
|
|
|
不动设置的人,视线直接落到右侧的密码与「复制密码」上。
|
2026-10-28 11:52:37 +08:00
|
|
|
============================================================ */
|
|
|
|
|
.card {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: grid;
|
2026-10-28 13:57:29 +08:00
|
|
|
/* 设置栏偏窄、输出栏偏宽:密码要用大字排开,设置只需容纳控件 */
|
|
|
|
|
grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr);
|
2026-10-28 11:52:37 +08:00
|
|
|
border-radius: var(--r-lg);
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
backdrop-filter: blur(22px) saturate(150%);
|
|
|
|
|
-webkit-backdrop-filter: blur(22px) saturate(150%);
|
|
|
|
|
box-shadow: var(--shadow);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pane { padding: clamp(22px, 3vw, 30px); min-width: 0; }
|
|
|
|
|
|
2026-10-28 13:57:29 +08:00
|
|
|
/* 右栏(输出)内容比左栏短,直接顶部对齐会在底部留出一大片空档(实测 180px)。
|
2026-10-28 11:52:37 +08:00
|
|
|
主内容用 auto 上下外边距居中,快捷键提示压在底部,版面才立得住。 */
|
|
|
|
|
.pane--out {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
.out-main { margin: auto 0; }
|
|
|
|
|
|
|
|
|
|
/* 左栏补充一条"字符池"信息:既填住空档,也让用户看见 S 到底有多少种可能 */
|
|
|
|
|
.pool {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin: 18px 0 0;
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
border-top: 1px solid var(--line);
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
}
|
|
|
|
|
.pool b {
|
|
|
|
|
font-family: var(--mono);
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
color: var(--text-2);
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
}
|
|
|
|
|
.pool .sep { opacity: .4; }
|
|
|
|
|
|
|
|
|
|
.shortcuts {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px 14px;
|
|
|
|
|
margin: 22px 0 0;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
}
|
|
|
|
|
.shortcuts span { display: inline-flex; align-items: center; gap: 6px; }
|
|
|
|
|
kbd {
|
|
|
|
|
font-family: var(--mono);
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
padding: 4px 6px;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
color: var(--text-2);
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
border: 1px solid var(--line-2);
|
|
|
|
|
border-bottom-width: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pane--ctl {
|
2026-10-28 13:57:29 +08:00
|
|
|
border-right: 1px solid var(--line);
|
2026-10-28 11:52:37 +08:00
|
|
|
background: var(--surface-2);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 22px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------- 输出区 ---------- */
|
|
|
|
|
.label-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.eyebrow {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
letter-spacing: .13em;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tag {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 5px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
padding: 3px 9px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
transition: color .2s, border-color .2s;
|
|
|
|
|
}
|
|
|
|
|
.tag .dot {
|
|
|
|
|
width: 5px;
|
|
|
|
|
height: 5px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: currentColor;
|
|
|
|
|
opacity: .55;
|
|
|
|
|
}
|
|
|
|
|
.tag.is-live { color: var(--accent); border-color: var(--accent-soft); }
|
|
|
|
|
.tag.is-live .dot { opacity: 1; box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
|
|
|
|
|
|
|
|
.pw {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: block;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: clamp(20px, 2.6vw, 28px) clamp(16px, 2.2vw, 24px);
|
|
|
|
|
min-height: 132px;
|
|
|
|
|
border-radius: var(--r-md);
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
background: var(--pw-bg);
|
|
|
|
|
font-family: var(--mono);
|
|
|
|
|
font-size: clamp(20px, 2.55vw, 30px);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
line-height: 1.46;
|
|
|
|
|
letter-spacing: .012em;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
color: var(--text);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
user-select: all;
|
|
|
|
|
transition: border-color .2s, background .2s, box-shadow .2s;
|
|
|
|
|
}
|
|
|
|
|
.pw:hover { border-color: var(--line-2); }
|
|
|
|
|
.pw:active { box-shadow: var(--ring); }
|
|
|
|
|
|
|
|
|
|
.pw .ch-a { color: var(--text); }
|
|
|
|
|
.pw .ch-d { color: var(--accent); }
|
|
|
|
|
.pw .ch-s { color: var(--accent-2); }
|
|
|
|
|
|
|
|
|
|
.pw.is-empty {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
font-family: var(--sans);
|
|
|
|
|
font-size: 14.5px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
color: var(--bad);
|
|
|
|
|
cursor: default;
|
|
|
|
|
user-select: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.out-actions {
|
2026-10-28 13:57:29 +08:00
|
|
|
display: grid;
|
|
|
|
|
/* 两个按钮严格等宽:均分而非「主按钮吃掉剩余空间」 */
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
2026-10-28 11:52:37 +08:00
|
|
|
gap: 10px;
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn {
|
|
|
|
|
appearance: none;
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
letter-spacing: .01em;
|
|
|
|
|
padding: 11px 18px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
transition: transform .16s, box-shadow .2s, background .2s, border-color .2s, color .2s;
|
|
|
|
|
}
|
|
|
|
|
.btn svg { width: 16px; height: 16px; flex: none; }
|
|
|
|
|
.btn:active { transform: scale(.985); }
|
|
|
|
|
|
2026-10-28 13:57:29 +08:00
|
|
|
/* 「复制密码」:主色渐变,实心 */
|
2026-10-28 11:52:37 +08:00
|
|
|
.btn--primary {
|
|
|
|
|
color: #fff;
|
|
|
|
|
background: linear-gradient(180deg, #5b5ee8, #4f46e5);
|
|
|
|
|
box-shadow: 0 10px 24px -12px rgba(99, 102, 241, .95),
|
|
|
|
|
0 1px 0 rgba(255, 255, 255, .22) inset;
|
|
|
|
|
}
|
|
|
|
|
.btn--primary:hover {
|
|
|
|
|
box-shadow: 0 14px 30px -12px rgba(99, 102, 241, 1),
|
|
|
|
|
0 1px 0 rgba(255, 255, 255, .28) inset;
|
|
|
|
|
}
|
|
|
|
|
.btn--primary:active { box-shadow: 0 6px 14px -10px rgba(99, 102, 241, .9); }
|
|
|
|
|
|
2026-10-28 13:57:29 +08:00
|
|
|
/* 「换一个」:同等尺寸,中性实心填充 + 描边区分层级。
|
|
|
|
|
早先只是透明描边,和主按钮并排时显得矮一截、也像"不可点"。 */
|
2026-10-28 11:52:37 +08:00
|
|
|
.btn--ghost {
|
2026-10-28 13:57:29 +08:00
|
|
|
color: var(--text);
|
|
|
|
|
border-color: var(--line-2);
|
|
|
|
|
background: var(--surface-3);
|
|
|
|
|
}
|
|
|
|
|
.btn--ghost:hover {
|
|
|
|
|
color: var(--text);
|
|
|
|
|
border-color: var(--line-2);
|
|
|
|
|
background: var(--surface-3);
|
|
|
|
|
box-shadow: 0 1px 0 rgba(255, 255, 255, .04) inset;
|
2026-10-28 11:52:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn[disabled] { opacity: .45; cursor: not-allowed; transform: none; }
|
|
|
|
|
|
|
|
|
|
/* ---------- 强度 ---------- */
|
|
|
|
|
.meter { margin-top: 24px; }
|
|
|
|
|
|
|
|
|
|
.meter-track {
|
|
|
|
|
height: 5px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: var(--line);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
.meter-fill {
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 0%;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: var(--text-3);
|
|
|
|
|
transition: width .38s cubic-bezier(.4, 0, .2, 1), background-color .38s;
|
|
|
|
|
}
|
|
|
|
|
.meter-fill.lvl-1 { background: var(--bad); }
|
|
|
|
|
.meter-fill.lvl-2 { background: var(--warn); }
|
|
|
|
|
.meter-fill.lvl-3 { background: #a3e635; }
|
|
|
|
|
.meter-fill.lvl-4 { background: var(--ok); }
|
|
|
|
|
.meter-fill.lvl-5 { background: linear-gradient(90deg, var(--ok), var(--accent)); }
|
|
|
|
|
|
|
|
|
|
.meter-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
}
|
|
|
|
|
.meter-meta .strength { font-weight: 600; color: var(--text-2); }
|
|
|
|
|
.meter-meta .entropy {
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
font-family: var(--mono);
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
letter-spacing: .02em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------- 预设 ---------- */
|
|
|
|
|
.presets {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 7px;
|
|
|
|
|
}
|
|
|
|
|
.preset {
|
|
|
|
|
appearance: none;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
font-weight: 550;
|
|
|
|
|
color: var(--text-2);
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
background: transparent;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: color .16s, border-color .16s, background .16s;
|
|
|
|
|
}
|
|
|
|
|
.preset:hover { color: var(--text); border-color: var(--line-2); background: var(--surface-2); }
|
|
|
|
|
.preset.is-active {
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
border-color: var(--accent-soft);
|
|
|
|
|
background: var(--accent-soft);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------- 字段 ---------- */
|
|
|
|
|
.field { display: flex; flex-direction: column; gap: 11px; }
|
|
|
|
|
|
|
|
|
|
.field-head {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
.field-head > label,
|
|
|
|
|
.field-head > span {
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-2);
|
|
|
|
|
letter-spacing: .02em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.len-out {
|
|
|
|
|
font-family: var(--mono);
|
|
|
|
|
font-size: 19px;
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
color: var(--text);
|
|
|
|
|
line-height: 1;
|
|
|
|
|
letter-spacing: -.02em;
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
}
|
|
|
|
|
.len-out small {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
letter-spacing: .08em;
|
|
|
|
|
margin-left: 3px;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.link-btn {
|
|
|
|
|
appearance: none;
|
|
|
|
|
border: 0;
|
|
|
|
|
background: none;
|
|
|
|
|
padding: 0;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
font-weight: 550;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: color .16s;
|
|
|
|
|
}
|
|
|
|
|
.link-btn:hover { color: var(--accent); }
|
|
|
|
|
|
|
|
|
|
/* ---------- 滑块 ---------- */
|
|
|
|
|
input[type="range"] {
|
|
|
|
|
-webkit-appearance: none;
|
|
|
|
|
appearance: none;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 22px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
background: transparent;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
input[type="range"]::-webkit-slider-runnable-track {
|
|
|
|
|
height: 5px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: linear-gradient(90deg,
|
|
|
|
|
var(--accent) 0%, var(--accent) var(--fill, 20%),
|
|
|
|
|
var(--line) var(--fill, 20%), var(--line) 100%);
|
|
|
|
|
}
|
|
|
|
|
input[type="range"]::-webkit-slider-thumb {
|
|
|
|
|
-webkit-appearance: none;
|
|
|
|
|
width: 18px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
margin-top: -6.5px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border: 2px solid var(--accent-solid);
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
|
|
|
|
|
transition: transform .15s, box-shadow .15s;
|
|
|
|
|
}
|
|
|
|
|
input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.1); }
|
|
|
|
|
input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.16); }
|
|
|
|
|
input[type="range"]:focus-visible::-webkit-slider-thumb {
|
|
|
|
|
box-shadow: 0 0 0 5px var(--accent-soft);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input[type="range"]::-moz-range-track {
|
|
|
|
|
height: 5px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: var(--line);
|
|
|
|
|
}
|
|
|
|
|
input[type="range"]::-moz-range-progress {
|
|
|
|
|
height: 5px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: var(--accent);
|
|
|
|
|
}
|
|
|
|
|
input[type="range"]::-moz-range-thumb {
|
|
|
|
|
width: 14px;
|
|
|
|
|
height: 14px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border: 2px solid var(--accent-solid);
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------- 字符类型 ---------- */
|
|
|
|
|
.chips {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chip {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
border-radius: var(--r-sm);
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
background: transparent;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: border-color .16s, background .16s, color .16s;
|
|
|
|
|
}
|
|
|
|
|
.chip input {
|
|
|
|
|
position: absolute;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
width: 1px;
|
|
|
|
|
height: 1px;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
.chip:hover { border-color: var(--line-2); background: var(--surface-2); }
|
|
|
|
|
|
|
|
|
|
.chip-box {
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
flex: none;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
border: 1.5px solid var(--line-2);
|
|
|
|
|
display: grid;
|
|
|
|
|
place-items: center;
|
|
|
|
|
transition: background .16s, border-color .16s;
|
|
|
|
|
}
|
|
|
|
|
.chip-box svg {
|
|
|
|
|
width: 10px;
|
|
|
|
|
height: 10px;
|
|
|
|
|
color: #fff;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: scale(.6);
|
|
|
|
|
transition: opacity .16s, transform .16s;
|
|
|
|
|
}
|
|
|
|
|
.chip.is-on .chip-box {
|
|
|
|
|
background: var(--accent-solid);
|
|
|
|
|
border-color: var(--accent-solid);
|
|
|
|
|
}
|
|
|
|
|
.chip.is-on .chip-box svg { opacity: 1; transform: scale(1); }
|
|
|
|
|
|
|
|
|
|
.chip-body { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
|
|
|
|
|
.chip-name { font-size: 13px; font-weight: 550; color: var(--text-2); transition: color .16s; }
|
|
|
|
|
.chip-sample {
|
|
|
|
|
font-family: var(--mono);
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
margin-top: 1px;
|
|
|
|
|
letter-spacing: .04em;
|
|
|
|
|
}
|
|
|
|
|
.chip.is-on { border-color: var(--accent-soft); background: var(--accent-soft); }
|
|
|
|
|
.chip.is-on .chip-name { color: var(--text); }
|
|
|
|
|
.chip:focus-within { box-shadow: var(--ring); }
|
|
|
|
|
|
|
|
|
|
/* ---------- 开关 ---------- */
|
|
|
|
|
.switches { display: flex; flex-direction: column; gap: 2px; }
|
|
|
|
|
|
|
|
|
|
.switch {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 14px;
|
|
|
|
|
padding: 9px 0;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
.switch + .switch { border-top: 1px solid var(--line); }
|
|
|
|
|
|
|
|
|
|
.switch-text { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
|
|
|
|
|
.switch-text b {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 550;
|
|
|
|
|
color: var(--text-2);
|
|
|
|
|
letter-spacing: .01em;
|
|
|
|
|
}
|
|
|
|
|
.switch-text i {
|
|
|
|
|
font-style: normal;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.switch input {
|
|
|
|
|
position: absolute;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
width: 1px;
|
|
|
|
|
height: 1px;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
.track {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 38px;
|
|
|
|
|
height: 22px;
|
|
|
|
|
flex: none;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: var(--line-2);
|
|
|
|
|
transition: background .22s;
|
|
|
|
|
}
|
|
|
|
|
.track::after {
|
|
|
|
|
content: "";
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 3px;
|
|
|
|
|
left: 3px;
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: #fff;
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, .35);
|
|
|
|
|
transition: transform .22s cubic-bezier(.4, 0, .2, 1);
|
|
|
|
|
}
|
|
|
|
|
.switch input:checked + .track { background: var(--accent-solid); }
|
|
|
|
|
.switch input:checked + .track::after { transform: translateX(16px); }
|
|
|
|
|
.switch input:focus-visible + .track { box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
|
|
|
|
|
|
|
|
/* ---------- 高级 ---------- */
|
|
|
|
|
.adv {
|
|
|
|
|
border-top: 1px solid var(--line);
|
|
|
|
|
padding-top: 14px;
|
|
|
|
|
margin-top: -4px;
|
|
|
|
|
}
|
|
|
|
|
.adv summary {
|
|
|
|
|
list-style: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
font-weight: 550;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
transition: color .16s;
|
|
|
|
|
user-select: none;
|
|
|
|
|
}
|
|
|
|
|
.adv summary::-webkit-details-marker { display: none; }
|
|
|
|
|
.adv summary:hover { color: var(--text-2); }
|
|
|
|
|
.adv summary svg {
|
|
|
|
|
width: 12px;
|
|
|
|
|
height: 12px;
|
|
|
|
|
transition: transform .22s;
|
|
|
|
|
}
|
|
|
|
|
.adv[open] summary svg { transform: rotate(90deg); }
|
|
|
|
|
.adv[open] summary { color: var(--text-2); margin-bottom: 11px; }
|
|
|
|
|
|
|
|
|
|
.adv-body { animation: fade-down .22s ease both; }
|
|
|
|
|
@keyframes fade-down {
|
|
|
|
|
from { opacity: 0; transform: translateY(-4px); }
|
|
|
|
|
to { opacity: 1; transform: none; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.text-input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
font-family: var(--mono);
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--text);
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
border-radius: var(--r-sm);
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
background: var(--pw-bg);
|
|
|
|
|
transition: border-color .16s;
|
|
|
|
|
}
|
|
|
|
|
.text-input::placeholder { color: var(--text-3); font-family: var(--sans); }
|
|
|
|
|
.text-input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
|
|
|
|
|
|
|
|
|
|
.hint {
|
|
|
|
|
margin: 8px 0 0;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
2026-10-28 13:57:29 +08:00
|
|
|
候选
|
2026-10-28 11:52:37 +08:00
|
|
|
============================================================ */
|
2026-10-28 13:57:29 +08:00
|
|
|
/* 候选单独一张卡,整宽单栏(原先与本地记录面板并排,那个面板已移除) */
|
2026-10-28 11:52:37 +08:00
|
|
|
.grid2 {
|
|
|
|
|
display: grid;
|
2026-10-28 13:57:29 +08:00
|
|
|
grid-template-columns: minmax(0, 1fr);
|
2026-10-28 11:52:37 +08:00
|
|
|
gap: clamp(14px, 2vw, 20px);
|
|
|
|
|
margin-top: clamp(14px, 2vw, 20px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.panel {
|
|
|
|
|
border-radius: var(--r-lg);
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
backdrop-filter: blur(18px) saturate(140%);
|
|
|
|
|
-webkit-backdrop-filter: blur(18px) saturate(140%);
|
|
|
|
|
padding: 20px 22px 18px;
|
|
|
|
|
box-shadow: var(--shadow);
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.panel-head {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
.panel-head h2 {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
letter-spacing: .13em;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list { list-style: none; margin: 0; padding: 0; }
|
|
|
|
|
|
|
|
|
|
.list li {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 9px 10px;
|
|
|
|
|
margin: 0 -10px;
|
|
|
|
|
border-radius: 9px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background .16s;
|
|
|
|
|
}
|
|
|
|
|
.list li:hover { background: var(--surface-2); }
|
|
|
|
|
.list li:focus-visible { box-shadow: var(--ring); }
|
|
|
|
|
|
|
|
|
|
.list .val {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
font-family: var(--mono);
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
letter-spacing: .01em;
|
|
|
|
|
color: var(--text-2);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
.list .meta {
|
|
|
|
|
flex: none;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty {
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
padding: 10px 0 6px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
页脚 / 提示
|
|
|
|
|
============================================================ */
|
|
|
|
|
.foot {
|
|
|
|
|
margin-top: 26px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-3);
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
}
|
|
|
|
|
.foot b { color: var(--text-2); font-weight: 550; }
|
|
|
|
|
.foot .sep { opacity: .45; margin: 0 7px; }
|
|
|
|
|
|
|
|
|
|
.toast {
|
|
|
|
|
position: fixed;
|
|
|
|
|
left: 50%;
|
|
|
|
|
bottom: 26px;
|
|
|
|
|
z-index: 50;
|
|
|
|
|
transform: translate(-50%, 14px);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 10px 17px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 550;
|
|
|
|
|
color: var(--text);
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
border: 1px solid var(--line-2);
|
|
|
|
|
backdrop-filter: blur(18px) saturate(160%);
|
|
|
|
|
-webkit-backdrop-filter: blur(18px) saturate(160%);
|
|
|
|
|
box-shadow: 0 16px 40px -16px rgba(0, 0, 0, .55);
|
|
|
|
|
transition: opacity .24s, transform .24s cubic-bezier(.4, 0, .2, 1);
|
|
|
|
|
}
|
|
|
|
|
.toast.show { opacity: 1; transform: translate(-50%, 0); }
|
|
|
|
|
.toast svg { width: 15px; height: 15px; color: var(--ok); }
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
响应式
|
|
|
|
|
============================================================ */
|
|
|
|
|
@media (max-width: 880px) {
|
|
|
|
|
.card { grid-template-columns: minmax(0, 1fr); }
|
2026-10-28 13:57:29 +08:00
|
|
|
/* DOM 顺序就是「设置 → 输出」,单列时天然是设置在上、输出在下 */
|
|
|
|
|
.pane--ctl { border-right: 0; border-bottom: 1px solid var(--line); }
|
2026-10-28 11:52:37 +08:00
|
|
|
.pw { min-height: 96px; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 420px) {
|
|
|
|
|
.btn { padding: 11px 14px; font-size: 13.5px; }
|
|
|
|
|
.pw { font-size: 19px; min-height: 88px; }
|
|
|
|
|
.panel { padding: 17px 16px 14px; }
|
|
|
|
|
.list li { padding: 9px 8px; margin: 0 -8px; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
*, *::before, *::after {
|
|
|
|
|
animation-duration: .001ms !important;
|
|
|
|
|
transition-duration: .001ms !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
<div class="page">
|
|
|
|
|
|
|
|
|
|
<header class="topbar">
|
|
|
|
|
<div class="brand">
|
|
|
|
|
<div class="logo" aria-hidden="true">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
<rect x="4" y="10.5" width="16" height="10.5" rx="3.2"/>
|
|
|
|
|
<path d="M8 10.5V7.6a4 4 0 0 1 8 0v2.9"/>
|
|
|
|
|
<circle cx="12" cy="15.8" r="1.35" fill="currentColor" stroke="none"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="brand-text">
|
|
|
|
|
<strong>Simple Password</strong>
|
|
|
|
|
<span>Generator</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button class="icon-btn" id="themeBtn" type="button" title="切换深浅色" aria-label="切换深浅色主题">
|
|
|
|
|
<svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
<path d="M20.5 14.4A8.6 8.6 0 0 1 9.6 3.5a8.9 8.9 0 1 0 10.9 10.9z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
<circle cx="12" cy="12" r="4.2"/>
|
|
|
|
|
<path d="M12 2.6v2M12 19.4v2M2.6 12h2M19.4 12h2M5.4 5.4l1.4 1.4M17.2 17.2l1.4 1.4M18.6 5.4l-1.4 1.4M6.8 17.2l-1.4 1.4"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main>
|
|
|
|
|
<section class="card">
|
|
|
|
|
|
2026-10-28 13:57:29 +08:00
|
|
|
<!-- 控制(左):可选地调设置 -->
|
2026-10-28 11:52:37 +08:00
|
|
|
<div class="pane pane--ctl">
|
|
|
|
|
|
|
|
|
|
<div class="presets" id="presets" role="group" aria-label="快捷预设">
|
|
|
|
|
<button class="preset" type="button" data-preset="std">标准</button>
|
|
|
|
|
<button class="preset" type="button" data-preset="strong">高强度</button>
|
|
|
|
|
<button class="preset" type="button" data-preset="pin">数字 PIN</button>
|
|
|
|
|
<button class="preset" type="button" data-preset="pass">易读口令</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
<div class="field-head">
|
|
|
|
|
<label for="len">长度</label>
|
|
|
|
|
<output class="len-out" id="lenOut" for="len">16<small>位</small></output>
|
|
|
|
|
</div>
|
|
|
|
|
<input type="range" id="len" min="4" max="64" step="1" value="16" aria-label="密码长度">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
<div class="field-head">
|
|
|
|
|
<span>字符组成</span>
|
|
|
|
|
<button class="link-btn" id="allBtn" type="button">全选</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="chips" id="chips"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="switches">
|
|
|
|
|
<label class="switch">
|
|
|
|
|
<span class="switch-text">
|
|
|
|
|
<b>排除易混淆字符</b>
|
|
|
|
|
<i>去掉 0 O o 1 l I,抄写不出错</i>
|
|
|
|
|
</span>
|
|
|
|
|
<input type="checkbox" id="optAmbig" checked>
|
|
|
|
|
<span class="track"></span>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<label class="switch">
|
|
|
|
|
<span class="switch-text">
|
|
|
|
|
<b>每类至少一位</b>
|
|
|
|
|
<i>所选的字符类型都会出现在结果里</i>
|
|
|
|
|
</span>
|
|
|
|
|
<input type="checkbox" id="optEach" checked>
|
|
|
|
|
<span class="track"></span>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<label class="switch">
|
|
|
|
|
<span class="switch-text">
|
|
|
|
|
<b>自动复制</b>
|
|
|
|
|
<i>生成后直接写入剪贴板</i>
|
|
|
|
|
</span>
|
|
|
|
|
<input type="checkbox" id="optCopy" checked>
|
|
|
|
|
<span class="track"></span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<details class="adv">
|
|
|
|
|
<summary>
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"/></svg>
|
|
|
|
|
自定义排除字符
|
|
|
|
|
</summary>
|
|
|
|
|
<div class="adv-body">
|
|
|
|
|
<input class="text-input" id="exclude" type="text" maxlength="80"
|
|
|
|
|
placeholder="想避免的字符,例如 ! @ $ 空格" spellcheck="false" autocomplete="off">
|
|
|
|
|
<p class="hint">这些字符不会出现在生成的密码中,可留空。</p>
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
|
|
|
|
|
</div>
|
2026-10-28 13:57:29 +08:00
|
|
|
|
|
|
|
|
<!-- 输出(右):主操作都在这一侧 -->
|
|
|
|
|
<div class="pane pane--out">
|
|
|
|
|
<div class="out-main">
|
|
|
|
|
<div class="label-row">
|
|
|
|
|
<span class="eyebrow">生成的密码</span>
|
|
|
|
|
<span class="tag" id="autoTag"><span class="dot"></span><span id="autoTagText">自动复制</span></span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="pw" id="pw" role="textbox" aria-readonly="true" tabindex="0" title="点击即可复制"></div>
|
|
|
|
|
|
|
|
|
|
<div class="out-actions">
|
|
|
|
|
<button class="btn btn--primary" id="copyBtn" type="button">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
<rect x="9" y="9" width="11.5" height="11.5" rx="2.4"/>
|
|
|
|
|
<path d="M15 6.2A2.2 2.2 0 0 0 12.8 4H5.7A2.2 2.2 0 0 0 3.5 6.2v7.1A2.2 2.2 0 0 0 5.7 15"/>
|
|
|
|
|
</svg>
|
|
|
|
|
复制密码
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn--ghost" id="regenBtn" type="button">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
<path d="M20.4 12a8.4 8.4 0 1 1-2.6-6.1"/>
|
|
|
|
|
<path d="M20.6 4.2v5h-5"/>
|
|
|
|
|
</svg>
|
|
|
|
|
换一个
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="meter">
|
|
|
|
|
<div class="meter-track"><div class="meter-fill" id="meterFill"></div></div>
|
|
|
|
|
<div class="meter-meta">
|
|
|
|
|
<span class="strength" id="strength">—</span>
|
|
|
|
|
<span class="entropy" id="entropy">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p class="pool" id="pool"></p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p class="shortcuts">
|
|
|
|
|
<span><kbd>空格</kbd> 换一个</span>
|
|
|
|
|
<span><kbd>C</kbd> 复制</span>
|
|
|
|
|
<span><kbd>点击密码</kbd> 也可以复制</span>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-10-28 11:52:37 +08:00
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="grid2">
|
|
|
|
|
<div class="panel">
|
|
|
|
|
<div class="panel-head">
|
|
|
|
|
<h2>候选密码</h2>
|
|
|
|
|
<button class="link-btn" id="batchBtn" type="button">生成 5 个</button>
|
|
|
|
|
</div>
|
|
|
|
|
<ul class="list" id="candList"><li class="empty">点右上角一次生成多个备选,点任意一条即复制。</li></ul>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<footer class="foot">
|
|
|
|
|
<b>全程本地运算</b><span class="sep">·</span>使用 Web Crypto 密码学安全随机数<span class="sep">·</span>不联网、不上传、不留痕
|
|
|
|
|
</footer>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="toast" id="toast" role="status" aria-live="polite">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6.5L9.5 17 4 11.5"/></svg>
|
|
|
|
|
<span id="toastText">已复制</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<noscript>
|
|
|
|
|
<div style="padding:20px;text-align:center;font-family:sans-serif">此工具需要启用 JavaScript 才能在本地生成密码。</div>
|
|
|
|
|
</noscript>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function () {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
常量
|
|
|
|
|
========================================================== */
|
|
|
|
|
var KEY_PREFS = 'sp.prefs.v1';
|
|
|
|
|
var KEY_THEME = 'sp.theme.v1';
|
|
|
|
|
|
|
|
|
|
var MAX_LEN = 64;
|
|
|
|
|
|
|
|
|
|
var CLASSES = [
|
|
|
|
|
{ id: 'lower', label: '小写', sample: 'abc', chars: 'abcdefghijklmnopqrstuvwxyz' },
|
|
|
|
|
{ id: 'upper', label: '大写', sample: 'ABC', chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' },
|
|
|
|
|
{ id: 'digit', label: '数字', sample: '123', chars: '0123456789' },
|
|
|
|
|
{ id: 'symbol', label: '符号', sample: '!@#', chars: '!@#$%^&*()-_=+[]{};:,.?/~' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/* 易混淆:数字 0 / 字母 O o / 数字 1 / 字母 l I */
|
|
|
|
|
var AMBIGUOUS = '0Oo1lI';
|
|
|
|
|
|
|
|
|
|
var PRESETS = {
|
|
|
|
|
std: { len: 16, sets: ['lower', 'upper', 'digit', 'symbol'], ambig: true, each: true },
|
|
|
|
|
strong: { len: 28, sets: ['lower', 'upper', 'digit', 'symbol'], ambig: true, each: true },
|
|
|
|
|
pin: { len: 6, sets: ['digit'], ambig: true, each: false },
|
|
|
|
|
pass: { len: 20, sets: ['lower', 'upper', 'digit'], ambig: true, each: true }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
状态
|
|
|
|
|
========================================================== */
|
|
|
|
|
var prefs = {
|
|
|
|
|
len: 16,
|
|
|
|
|
sets: { lower: true, upper: true, digit: true, symbol: true },
|
|
|
|
|
ambig: true,
|
|
|
|
|
each: true,
|
|
|
|
|
autoCopy: true,
|
|
|
|
|
exclude: ''
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var current = '';
|
|
|
|
|
var copyTimer = null;
|
|
|
|
|
var toastTimer = null;
|
|
|
|
|
|
|
|
|
|
function $(id) { return document.getElementById(id); }
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
随机数:拒绝采样,消除取模偏差
|
|
|
|
|
========================================================== */
|
|
|
|
|
var RND = new Uint32Array(1);
|
|
|
|
|
var TWO32 = 4294967296;
|
|
|
|
|
|
|
|
|
|
function randInt(n) {
|
|
|
|
|
if (n <= 1) return 0;
|
|
|
|
|
var limit = Math.floor(TWO32 / n) * n;
|
|
|
|
|
var v;
|
|
|
|
|
do {
|
|
|
|
|
if (window.crypto && crypto.getRandomValues) {
|
|
|
|
|
crypto.getRandomValues(RND);
|
|
|
|
|
v = RND[0];
|
|
|
|
|
} else {
|
|
|
|
|
v = Math.floor(Math.random() * TWO32);
|
|
|
|
|
}
|
|
|
|
|
} while (v >= limit);
|
|
|
|
|
return v % n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shuffle(arr) {
|
|
|
|
|
for (var i = arr.length - 1; i > 0; i--) {
|
|
|
|
|
var j = randInt(i + 1);
|
|
|
|
|
var t = arr[i];
|
|
|
|
|
arr[i] = arr[j];
|
|
|
|
|
arr[j] = t;
|
|
|
|
|
}
|
|
|
|
|
return arr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
字符池
|
|
|
|
|
========================================================== */
|
|
|
|
|
function excludedSet() {
|
|
|
|
|
if (!prefs.exclude) return null;
|
|
|
|
|
var s = {}, chars = Array.from(prefs.exclude);
|
|
|
|
|
for (var i = 0; i < chars.length; i++) s[chars[i]] = true;
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 返回按字符类型分组的可用字符数组(已过滤易混淆 / 自定义排除) */
|
|
|
|
|
function pools() {
|
|
|
|
|
var ex = excludedSet();
|
|
|
|
|
var out = [];
|
|
|
|
|
for (var i = 0; i < CLASSES.length; i++) {
|
|
|
|
|
var def = CLASSES[i];
|
|
|
|
|
if (!prefs.sets[def.id]) continue;
|
|
|
|
|
var kept = '';
|
|
|
|
|
for (var k = 0; k < def.chars.length; k++) {
|
|
|
|
|
var ch = def.chars.charAt(k);
|
|
|
|
|
if (prefs.ambig && AMBIGUOUS.indexOf(ch) !== -1) continue;
|
|
|
|
|
if (ex && ex[ch]) continue;
|
|
|
|
|
kept += ch;
|
|
|
|
|
}
|
|
|
|
|
if (kept) out.push(kept);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
生成
|
|
|
|
|
========================================================== */
|
|
|
|
|
function generate() {
|
|
|
|
|
var groups = pools();
|
|
|
|
|
if (!groups.length) {
|
|
|
|
|
return { error: '请至少选择一种字符类型' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pool = groups.join('');
|
|
|
|
|
var need = groups.length;
|
|
|
|
|
var len = prefs.len;
|
|
|
|
|
|
|
|
|
|
if (prefs.each && len < need) {
|
|
|
|
|
return { error: '长度不足,无法覆盖所选的全部字符类型' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var chars = [];
|
|
|
|
|
|
|
|
|
|
if (prefs.each) {
|
|
|
|
|
/* 每个类型先各取一位,剩余长度从全池随机,最后整体洗牌 */
|
|
|
|
|
for (var g = 0; g < groups.length; g++) {
|
|
|
|
|
chars.push(groups[g].charAt(randInt(groups[g].length)));
|
|
|
|
|
}
|
|
|
|
|
for (var r = need; r < len; r++) {
|
|
|
|
|
chars.push(pool.charAt(randInt(pool.length)));
|
|
|
|
|
}
|
|
|
|
|
shuffle(chars);
|
|
|
|
|
} else {
|
|
|
|
|
for (var i = 0; i < len; i++) {
|
|
|
|
|
chars.push(pool.charAt(randInt(pool.length)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var bits = len * Math.log(pool.length) / Math.LN2;
|
|
|
|
|
var lvl = strengthLevel(bits);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
pw: chars.join(''),
|
|
|
|
|
bits: bits,
|
|
|
|
|
level: lvl.level,
|
|
|
|
|
label: lvl.label,
|
|
|
|
|
poolSize: pool.length,
|
|
|
|
|
groups: groups.length,
|
|
|
|
|
each: prefs.each
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function strengthLevel(bits) {
|
|
|
|
|
if (bits >= 112) return { level: 5, label: '极强' };
|
|
|
|
|
if (bits >= 80) return { level: 4, label: '很强' };
|
|
|
|
|
if (bits >= 60) return { level: 3, label: '强' };
|
|
|
|
|
if (bits >= 40) return { level: 2, label: '一般' };
|
|
|
|
|
return { level: 1, label: '弱' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
渲染:密码
|
|
|
|
|
========================================================== */
|
|
|
|
|
function paint(el, pw) {
|
|
|
|
|
var frag = document.createDocumentFragment();
|
|
|
|
|
for (var i = 0; i < pw.length; i++) {
|
|
|
|
|
var ch = pw.charAt(i);
|
|
|
|
|
var cls;
|
|
|
|
|
if (ch >= '0' && ch <= '9') cls = 'ch-d';
|
|
|
|
|
else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) cls = 'ch-a';
|
|
|
|
|
else cls = 'ch-s';
|
|
|
|
|
var span = document.createElement('span');
|
|
|
|
|
span.className = cls;
|
|
|
|
|
span.textContent = ch;
|
|
|
|
|
frag.appendChild(span);
|
|
|
|
|
}
|
|
|
|
|
el.textContent = '';
|
|
|
|
|
el.appendChild(frag);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPool(info) {
|
|
|
|
|
var el = $('pool');
|
|
|
|
|
if (!info || !info.poolSize) {
|
|
|
|
|
el.textContent = '';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
el.textContent = '';
|
|
|
|
|
var b1 = document.createElement('b');
|
|
|
|
|
b1.textContent = info.poolSize;
|
|
|
|
|
el.appendChild(document.createTextNode('可用字符 '));
|
|
|
|
|
el.appendChild(b1);
|
|
|
|
|
el.appendChild(document.createTextNode(' 个'));
|
|
|
|
|
|
|
|
|
|
var sep = document.createElement('span');
|
|
|
|
|
sep.className = 'sep';
|
|
|
|
|
sep.textContent = '·';
|
|
|
|
|
el.appendChild(sep);
|
|
|
|
|
|
|
|
|
|
var b2 = document.createElement('b');
|
|
|
|
|
b2.textContent = info.groups;
|
|
|
|
|
el.appendChild(b2);
|
|
|
|
|
el.appendChild(document.createTextNode(' 类组合'));
|
|
|
|
|
|
|
|
|
|
if (info.each) {
|
|
|
|
|
var sep2 = document.createElement('span');
|
|
|
|
|
sep2.className = 'sep';
|
|
|
|
|
sep2.textContent = '·';
|
|
|
|
|
el.appendChild(sep2);
|
|
|
|
|
el.appendChild(document.createTextNode('每类至少 1 位'));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setMeter(level, bits, label) {
|
|
|
|
|
var fill = $('meterFill');
|
|
|
|
|
fill.className = 'meter-fill' + (level ? ' lvl-' + level : '');
|
|
|
|
|
fill.style.width = (level * 20) + '%';
|
|
|
|
|
$('strength').textContent = label || '—';
|
|
|
|
|
$('entropy').textContent = bits ? '约 ' + Math.round(bits) + ' bit 熵' : '—';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
渲染:主流程
|
|
|
|
|
========================================================== */
|
|
|
|
|
function regenerate() {
|
|
|
|
|
var el = $('pw');
|
|
|
|
|
var res = generate();
|
|
|
|
|
|
|
|
|
|
if (res.error) {
|
|
|
|
|
current = '';
|
|
|
|
|
el.className = 'pw is-empty';
|
|
|
|
|
el.textContent = res.error;
|
|
|
|
|
setMeter(0, 0, '不可用');
|
|
|
|
|
renderPool(null);
|
|
|
|
|
$('copyBtn').disabled = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current = res.pw;
|
|
|
|
|
el.className = 'pw';
|
|
|
|
|
paint(el, res.pw);
|
|
|
|
|
setMeter(res.level, res.bits, res.label);
|
|
|
|
|
renderPool(res);
|
|
|
|
|
$('copyBtn').disabled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 控件变动:立刻重算,稍等再复制(拖滑块时不至于疯狂写剪贴板) */
|
|
|
|
|
function onControlsChanged(immediate) {
|
|
|
|
|
syncLengthBounds();
|
|
|
|
|
persist();
|
|
|
|
|
regenerate();
|
|
|
|
|
updatePresetHighlight();
|
|
|
|
|
|
|
|
|
|
if (!prefs.autoCopy) return;
|
|
|
|
|
clearTimeout(copyTimer);
|
|
|
|
|
if (immediate) {
|
|
|
|
|
copyNow();
|
|
|
|
|
} else {
|
|
|
|
|
copyTimer = setTimeout(copyNow, 420);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
剪贴板
|
|
|
|
|
========================================================== */
|
|
|
|
|
function copyText(text, cb) {
|
|
|
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
|
|
|
navigator.clipboard.writeText(text).then(function () {
|
|
|
|
|
cb(true);
|
|
|
|
|
}, function () {
|
|
|
|
|
cb(fallbackCopy(text));
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
cb(fallbackCopy(text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fallbackCopy(text) {
|
|
|
|
|
var ta = document.createElement('textarea');
|
|
|
|
|
ta.value = text;
|
|
|
|
|
ta.setAttribute('readonly', '');
|
|
|
|
|
ta.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;padding:0;border:0;opacity:0;';
|
|
|
|
|
document.body.appendChild(ta);
|
|
|
|
|
var ok = false;
|
|
|
|
|
try {
|
|
|
|
|
ta.select();
|
|
|
|
|
ta.setSelectionRange(0, text.length);
|
|
|
|
|
ok = document.execCommand('copy');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
ok = false;
|
|
|
|
|
}
|
|
|
|
|
document.body.removeChild(ta);
|
|
|
|
|
return ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function copyNow() {
|
|
|
|
|
if (!current) return;
|
|
|
|
|
copyText(current, function (ok) {
|
|
|
|
|
if (ok) {
|
|
|
|
|
toast('已复制到剪贴板');
|
|
|
|
|
} else {
|
|
|
|
|
toast('复制被拦截,请手动选中', true);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
提示条
|
|
|
|
|
========================================================== */
|
|
|
|
|
function toast(msg, isError) {
|
|
|
|
|
var el = $('toast');
|
|
|
|
|
$('toastText').textContent = msg;
|
|
|
|
|
el.style.setProperty('--ok', isError ? 'var(--bad)' : 'var(--ok)');
|
|
|
|
|
el.classList.add('show');
|
|
|
|
|
clearTimeout(toastTimer);
|
|
|
|
|
toastTimer = setTimeout(function () {
|
|
|
|
|
el.classList.remove('show');
|
|
|
|
|
}, 1900);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function listItem(text, meta, onClick) {
|
|
|
|
|
var li = document.createElement('li');
|
|
|
|
|
li.tabIndex = 0;
|
|
|
|
|
li.title = '点击复制';
|
|
|
|
|
|
|
|
|
|
var span = document.createElement('span');
|
|
|
|
|
span.className = 'val';
|
|
|
|
|
span.textContent = text;
|
|
|
|
|
|
|
|
|
|
var m = document.createElement('span');
|
|
|
|
|
m.className = 'meta';
|
|
|
|
|
m.textContent = meta;
|
|
|
|
|
|
|
|
|
|
li.appendChild(span);
|
|
|
|
|
li.appendChild(m);
|
|
|
|
|
|
|
|
|
|
li.addEventListener('click', onClick);
|
|
|
|
|
li.addEventListener('keydown', function (e) {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
onClick();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return li;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
候选
|
|
|
|
|
========================================================== */
|
|
|
|
|
function renderCandidates() {
|
|
|
|
|
var box = $('candList');
|
|
|
|
|
box.textContent = '';
|
|
|
|
|
|
|
|
|
|
var made = 0;
|
|
|
|
|
for (var i = 0; i < 5; i++) {
|
|
|
|
|
var res = generate();
|
|
|
|
|
if (res.error) {
|
|
|
|
|
var err = document.createElement('li');
|
|
|
|
|
err.className = 'empty';
|
|
|
|
|
err.textContent = res.error;
|
|
|
|
|
box.appendChild(err);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
(function (pw, bits) {
|
|
|
|
|
box.appendChild(listItem(pw, Math.round(bits) + ' bit', function () {
|
|
|
|
|
copyText(pw, function (ok) {
|
2026-10-28 13:57:29 +08:00
|
|
|
if (ok) { toast('已复制候选密码'); }
|
2026-10-28 11:52:37 +08:00
|
|
|
else { toast('复制被拦截', true); }
|
|
|
|
|
});
|
|
|
|
|
}));
|
|
|
|
|
})(res.pw, res.bits);
|
|
|
|
|
made++;
|
|
|
|
|
}
|
|
|
|
|
if (!made) box.innerHTML = '<li class="empty">生成失败</li>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
长度上下限
|
|
|
|
|
========================================================== */
|
|
|
|
|
function syncLengthBounds() {
|
|
|
|
|
var n = 0;
|
|
|
|
|
for (var i = 0; i < CLASSES.length; i++) if (prefs.sets[CLASSES[i].id]) n++;
|
|
|
|
|
|
|
|
|
|
var min = prefs.each ? Math.max(2, n) : 1;
|
|
|
|
|
if (min > MAX_LEN) min = MAX_LEN;
|
|
|
|
|
|
|
|
|
|
var slider = $('len');
|
|
|
|
|
slider.min = min;
|
|
|
|
|
|
|
|
|
|
if (prefs.len < min) prefs.len = min;
|
|
|
|
|
if (prefs.len > MAX_LEN) prefs.len = MAX_LEN;
|
|
|
|
|
slider.value = prefs.len;
|
|
|
|
|
|
|
|
|
|
var pct = MAX_LEN > min ? ((prefs.len - min) / (MAX_LEN - min)) * 100 : 100;
|
|
|
|
|
slider.style.setProperty('--fill', pct + '%');
|
|
|
|
|
|
|
|
|
|
$('lenOut').innerHTML = prefs.len + '<small>位</small>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
界面同步
|
|
|
|
|
========================================================== */
|
|
|
|
|
function buildChips() {
|
|
|
|
|
var box = $('chips');
|
|
|
|
|
box.textContent = '';
|
|
|
|
|
for (var i = 0; i < CLASSES.length; i++) {
|
|
|
|
|
(function (def) {
|
|
|
|
|
var label = document.createElement('label');
|
|
|
|
|
label.className = 'chip';
|
|
|
|
|
|
|
|
|
|
var input = document.createElement('input');
|
|
|
|
|
input.type = 'checkbox';
|
|
|
|
|
input.setAttribute('data-set', def.id);
|
|
|
|
|
input.checked = !!prefs.sets[def.id];
|
|
|
|
|
|
|
|
|
|
var boxEl = document.createElement('span');
|
|
|
|
|
boxEl.className = 'chip-box';
|
|
|
|
|
boxEl.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6.5L9.5 17 4 11.5"/></svg>';
|
|
|
|
|
|
|
|
|
|
var body = document.createElement('span');
|
|
|
|
|
body.className = 'chip-body';
|
|
|
|
|
var name = document.createElement('span');
|
|
|
|
|
name.className = 'chip-name';
|
|
|
|
|
name.textContent = def.label;
|
|
|
|
|
var sample = document.createElement('span');
|
|
|
|
|
sample.className = 'chip-sample';
|
|
|
|
|
sample.textContent = def.sample;
|
|
|
|
|
body.appendChild(name);
|
|
|
|
|
body.appendChild(sample);
|
|
|
|
|
|
|
|
|
|
label.appendChild(input);
|
|
|
|
|
label.appendChild(boxEl);
|
|
|
|
|
label.appendChild(body);
|
|
|
|
|
|
|
|
|
|
input.addEventListener('change', function () {
|
|
|
|
|
prefs.sets[def.id] = input.checked;
|
|
|
|
|
label.classList.toggle('is-on', input.checked);
|
|
|
|
|
ensureOneSet();
|
|
|
|
|
onControlsChanged(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
label.classList.toggle('is-on', input.checked);
|
|
|
|
|
box.appendChild(label);
|
|
|
|
|
})(CLASSES[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 不允许一个都不选 */
|
|
|
|
|
function ensureOneSet() {
|
|
|
|
|
var any = false;
|
|
|
|
|
for (var i = 0; i < CLASSES.length; i++) if (prefs.sets[CLASSES[i].id]) any = true;
|
|
|
|
|
if (!any) {
|
|
|
|
|
prefs.sets.lower = true;
|
|
|
|
|
var inputs = $('chips').querySelectorAll('input[data-set]');
|
|
|
|
|
for (var k = 0; k < inputs.length; k++) {
|
|
|
|
|
if (inputs[k].getAttribute('data-set') === 'lower') inputs[k].checked = true;
|
|
|
|
|
}
|
|
|
|
|
var first = $('chips').querySelector('.chip');
|
|
|
|
|
if (first) first.classList.add('is-on');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function syncChips() {
|
|
|
|
|
var inputs = $('chips').querySelectorAll('input[data-set]');
|
|
|
|
|
for (var i = 0; i < inputs.length; i++) {
|
|
|
|
|
var id = inputs[i].getAttribute('data-set');
|
|
|
|
|
inputs[i].checked = !!prefs.sets[id];
|
|
|
|
|
inputs[i].parentNode.classList.toggle('is-on', !!prefs.sets[id]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updatePresetHighlight() {
|
|
|
|
|
var btns = $('presets').querySelectorAll('.preset');
|
|
|
|
|
for (var i = 0; i < btns.length; i++) {
|
|
|
|
|
var p = PRESETS[btns[i].getAttribute('data-preset')];
|
|
|
|
|
if (!p) continue;
|
|
|
|
|
var same = prefs.len === p.len && prefs.ambig === p.ambig && prefs.each === p.each;
|
|
|
|
|
if (same) {
|
|
|
|
|
for (var c = 0; c < CLASSES.length; c++) {
|
|
|
|
|
var id = CLASSES[c].id;
|
|
|
|
|
var want = p.sets.indexOf(id) !== -1;
|
|
|
|
|
if (!!prefs.sets[id] !== want) { same = false; break; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
btns[i].classList.toggle('is-active', same);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function syncAutoTag() {
|
|
|
|
|
var tag = $('autoTag');
|
|
|
|
|
tag.classList.toggle('is-live', prefs.autoCopy);
|
|
|
|
|
$('autoTagText').textContent = prefs.autoCopy ? '自动复制已开启' : '自动复制已关闭';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
持久化
|
|
|
|
|
========================================================== */
|
|
|
|
|
function persist() {
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(KEY_PREFS, JSON.stringify({
|
|
|
|
|
len: prefs.len,
|
|
|
|
|
sets: prefs.sets,
|
|
|
|
|
ambig: prefs.ambig,
|
|
|
|
|
each: prefs.each,
|
|
|
|
|
autoCopy: prefs.autoCopy,
|
|
|
|
|
exclude: prefs.exclude
|
|
|
|
|
}));
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function restore() {
|
|
|
|
|
var raw;
|
|
|
|
|
try { raw = localStorage.getItem(KEY_PREFS); } catch (e) { return; }
|
|
|
|
|
if (!raw) return;
|
|
|
|
|
var d;
|
|
|
|
|
try { d = JSON.parse(raw); } catch (e) { return; }
|
|
|
|
|
if (!d || typeof d !== 'object') return;
|
|
|
|
|
|
|
|
|
|
if (typeof d.len === 'number' && isFinite(d.len)) {
|
|
|
|
|
prefs.len = Math.min(MAX_LEN, Math.max(1, Math.round(d.len)));
|
|
|
|
|
}
|
|
|
|
|
if (d.sets && typeof d.sets === 'object') {
|
|
|
|
|
for (var i = 0; i < CLASSES.length; i++) {
|
|
|
|
|
var id = CLASSES[i].id;
|
|
|
|
|
if (typeof d.sets[id] === 'boolean') prefs.sets[id] = d.sets[id];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof d.ambig === 'boolean') prefs.ambig = d.ambig;
|
|
|
|
|
if (typeof d.each === 'boolean') prefs.each = d.each;
|
|
|
|
|
if (typeof d.autoCopy === 'boolean') prefs.autoCopy = d.autoCopy;
|
|
|
|
|
if (typeof d.exclude === 'string') prefs.exclude = d.exclude.slice(0, 80);
|
|
|
|
|
|
|
|
|
|
var any = false;
|
|
|
|
|
for (var k = 0; k < CLASSES.length; k++) if (prefs.sets[CLASSES[k].id]) any = true;
|
|
|
|
|
if (!any) prefs.sets.lower = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
主题
|
|
|
|
|
========================================================== */
|
|
|
|
|
function currentTheme() {
|
|
|
|
|
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setTheme(t) {
|
|
|
|
|
document.documentElement.setAttribute('data-theme', t);
|
|
|
|
|
try { localStorage.setItem(KEY_THEME, t); } catch (e) {}
|
|
|
|
|
var meta = document.querySelector('meta[name="theme-color"]');
|
|
|
|
|
if (meta) meta.setAttribute('content', t === 'light' ? '#f4f5f8' : '#0a0b0f');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
事件绑定
|
|
|
|
|
========================================================== */
|
|
|
|
|
function bind() {
|
|
|
|
|
$('themeBtn').addEventListener('click', function () {
|
|
|
|
|
setTheme(currentTheme() === 'dark' ? 'light' : 'dark');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('len').addEventListener('input', function () {
|
|
|
|
|
prefs.len = parseInt(this.value, 10) || 1;
|
|
|
|
|
onControlsChanged(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('optAmbig').addEventListener('change', function () {
|
|
|
|
|
prefs.ambig = this.checked;
|
|
|
|
|
onControlsChanged(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('optEach').addEventListener('change', function () {
|
|
|
|
|
prefs.each = this.checked;
|
|
|
|
|
onControlsChanged(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('optCopy').addEventListener('change', function () {
|
|
|
|
|
prefs.autoCopy = this.checked;
|
|
|
|
|
syncAutoTag();
|
|
|
|
|
persist();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var ex = $('exclude');
|
|
|
|
|
ex.addEventListener('input', function () {
|
|
|
|
|
prefs.exclude = this.value;
|
|
|
|
|
onControlsChanged(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('allBtn').addEventListener('click', function () {
|
|
|
|
|
var all = true;
|
|
|
|
|
for (var i = 0; i < CLASSES.length; i++) if (!prefs.sets[CLASSES[i].id]) all = false;
|
|
|
|
|
var next = !all;
|
|
|
|
|
for (var k = 0; k < CLASSES.length; k++) prefs.sets[CLASSES[k].id] = next;
|
|
|
|
|
syncChips();
|
|
|
|
|
onControlsChanged(false);
|
|
|
|
|
this.textContent = next ? '清空' : '全选';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('presets').addEventListener('click', function (e) {
|
|
|
|
|
var btn = e.target.closest ? e.target.closest('.preset') : null;
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
var p = PRESETS[btn.getAttribute('data-preset')];
|
|
|
|
|
if (!p) return;
|
|
|
|
|
|
|
|
|
|
prefs.len = p.len;
|
|
|
|
|
prefs.ambig = p.ambig;
|
|
|
|
|
prefs.each = p.each;
|
|
|
|
|
for (var i = 0; i < CLASSES.length; i++) {
|
|
|
|
|
prefs.sets[CLASSES[i].id] = p.sets.indexOf(CLASSES[i].id) !== -1;
|
|
|
|
|
}
|
|
|
|
|
syncChips();
|
|
|
|
|
$('optAmbig').checked = prefs.ambig;
|
|
|
|
|
$('optEach').checked = prefs.each;
|
|
|
|
|
onControlsChanged(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('copyBtn').addEventListener('click', function () {
|
|
|
|
|
copyNow();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('regenBtn').addEventListener('click', function () {
|
|
|
|
|
onControlsChanged(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('pw').addEventListener('click', function () {
|
|
|
|
|
if (!current) return;
|
|
|
|
|
copyNow();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('pw').addEventListener('keydown', function (e) {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (current) copyNow();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('batchBtn').addEventListener('click', renderCandidates);
|
|
|
|
|
|
|
|
|
|
/* 快捷键:空格=换一个,C=复制(输入框内不拦截) */
|
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
|
|
|
var tag = (e.target && e.target.tagName || '').toLowerCase();
|
|
|
|
|
if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
|
|
|
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
|
|
|
|
|
|
|
|
if (e.code === 'Space') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
onControlsChanged(true);
|
|
|
|
|
} else if (e.key === 'c' || e.key === 'C') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
copyNow();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ==========================================================
|
|
|
|
|
启动
|
|
|
|
|
========================================================== */
|
|
|
|
|
function init() {
|
|
|
|
|
restore();
|
|
|
|
|
|
|
|
|
|
buildChips();
|
|
|
|
|
syncChips();
|
|
|
|
|
syncLengthBounds();
|
|
|
|
|
syncAutoTag();
|
|
|
|
|
updatePresetHighlight();
|
|
|
|
|
|
|
|
|
|
$('optAmbig').checked = prefs.ambig;
|
|
|
|
|
$('optEach').checked = prefs.each;
|
|
|
|
|
$('optCopy').checked = prefs.autoCopy;
|
|
|
|
|
$('exclude').value = prefs.exclude;
|
|
|
|
|
|
|
|
|
|
var all = true;
|
|
|
|
|
for (var i = 0; i < CLASSES.length; i++) if (!prefs.sets[CLASSES[i].id]) all = false;
|
|
|
|
|
$('allBtn').textContent = all ? '清空' : '全选';
|
|
|
|
|
|
|
|
|
|
bind();
|
|
|
|
|
|
|
|
|
|
/* 首屏只生成,不自动复制:避免在没有用户手势时触发剪贴板策略告警 */
|
|
|
|
|
regenerate();
|
|
|
|
|
|
|
|
|
|
var meta = document.querySelector('meta[name="theme-color"]');
|
|
|
|
|
if (meta) meta.setAttribute('content', currentTheme() === 'light' ? '#f4f5f8' : '#0a0b0f');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
|
} else {
|
|
|
|
|
init();
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|