Files

1793 lines
51 KiB
HTML
Raw Permalink Normal View History

<!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);
/* 比 surface-2 更实的填充层:用于需要"看起来是个实体按钮"的次操作 */
--surface-3: rgba(255, 255, 255, .085);
--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);
--surface-3: rgba(16, 18, 27, .062);
--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; }
/* ============================================================
主卡片
左 = 设置,右 = 输出。
语义是「左侧可选地调设置,右侧直接完成主操作」——
不动设置的人,视线直接落到右侧的密码与「复制密码」上。
============================================================ */
.card {
position: relative;
display: grid;
/* 设置栏偏窄、输出栏偏宽:密码要用大字排开,设置只需容纳控件 */
grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr);
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; }
/* 右栏(输出)内容比左栏短,直接顶部对齐会在底部留出一大片空档(实测 180px)。
主内容用 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 {
border-right: 1px solid var(--line);
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 {
display: grid;
/* 两个按钮严格等宽:均分而非「主按钮吃掉剩余空间」 */
grid-template-columns: repeat(2, minmax(0, 1fr));
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); }
/* 「复制密码」:主色渐变,实心 */
.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); }
/* 「换一个」:同等尺寸,中性实心填充 + 描边区分层级。
早先只是透明描边,和主按钮并排时显得矮一截、也像"不可点"。 */
.btn--ghost {
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;
}
.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;
}
/* ============================================================
候选
============================================================ */
/* 候选单独一张卡,整宽单栏(原先与本地记录面板并排,那个面板已移除) */
.grid2 {
display: grid;
grid-template-columns: minmax(0, 1fr);
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); }
/* DOM 顺序就是「设置 → 输出」,单列时天然是设置在上、输出在下 */
.pane--ctl { border-right: 0; border-bottom: 1px solid var(--line); }
.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">
<!-- 控制(左):可选地调设置 -->
<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>
<!-- 输出(右):主操作都在这一侧 -->
<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>
</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) {
if (ok) { toast('已复制候选密码'); }
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>