Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b92c1ece1 | ||
|
|
9ca33a0328 | ||
|
|
e09943fefb | ||
|
|
33166407ea | ||
|
|
03eca9dc9c | ||
|
|
6b4ee89d19 |
@@ -0,0 +1,12 @@
|
||||
# 本地校验用的依赖(脚本在仓库外的 _tooling/,这里只是 npm install 的落点)
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# 编辑器 / 系统垃圾
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 临时产物
|
||||
*.log
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
一个干净、纯本地的密码生成器。单文件 `index.html`,双击即用,也可丢到任意静态托管上。
|
||||
|
||||

|
||||
**在线使用 → https://password.yangyq.net/**
|
||||
|
||||

|
||||
|
||||
## 特点
|
||||
|
||||
- **纯前端,零依赖** — 一个 `index.html`,没有构建步骤、没有 CDN、没有任何外部请求。断网可用。
|
||||
- **左设置、右操作** — 设置栏在左,不调任何设置时视线直接落到右侧的密码与「复制密码」。窄屏自动变上下排列,设置在上。
|
||||
- **密码学安全随机** — 走 `crypto.getRandomValues` + 拒绝采样消除取模偏差,不用 `Math.random()`。
|
||||
- **生成即复制** — 默认开启自动复制;剪贴板 API 不可用(如 `file://` 下的旧浏览器)时自动降级到 `execCommand`。
|
||||
- **深浅色** — 跟随系统偏好,可手动切换并记住选择。首帧前定主题,不会白屏闪一下。
|
||||
@@ -16,13 +19,28 @@
|
||||
- **自定义排除** — 想避开 `@`、空格或某个符号,直接填进去。
|
||||
- **强度与熵** — 按 `长度 × log₂(字符池)` 算真实熵,分五档给出评价,同时显示字符池规模。
|
||||
- **批量候选** — 一次出 5 个,点任意一条即复制。
|
||||
- **最近生成** — 只存在本机 `localStorage`,最多 6 条,可一键清空。
|
||||
- **不保留记录** — 不记历史,`localStorage` 只存偏好设置与主题,不存密码。
|
||||
|
||||
## 界面
|
||||
|
||||
设置与主操作分居左右两栏 —— 不调设置的人,视线直接落到右侧的密码和「复制密码」上。
|
||||
「复制密码」与「换一个」严格等大(实测 241×42,宽高差 0px),用渐变与实心填充区分主次。
|
||||
|
||||
浅色主题:
|
||||
|
||||

|
||||
|
||||
移动端自动转为上下单列,设置在输出之上:
|
||||
|
||||
<img src="./docs/mobile.png" alt="移动端" width="320">
|
||||
|
||||
## 使用
|
||||
|
||||
直接打开 `index.html` 即可。
|
||||
直接用浏览器打开 `index.html` 即可,无需服务器。
|
||||
|
||||
想用 http 协议(例如需要真实剪贴板权限时):
|
||||
已经部署的在线版本:<https://password.yangyq.net/>
|
||||
|
||||
想在本地用 http 协议(例如需要真实剪贴板权限时):
|
||||
|
||||
```bash
|
||||
python -m http.server 8000
|
||||
@@ -44,7 +62,7 @@ python -m http.server 8000
|
||||
- 所有运算在浏览器里完成,**不联网、不上传、不写日志**。
|
||||
- 用 `crypto.getRandomValues` 取随机数,并按「拒绝采样」丢弃落在偏置区间的值,避免取模带来的分布倾斜。
|
||||
- 勾「每类至少一位」时,先从每类各取一位,剩余位从全池随机,最后整体洗牌 —— 保证覆盖的同时不引入位置可预测性。
|
||||
- 历史记录存在 `localStorage` 里,任何能访问这台机器的人都能看到。**在共用电脑上建议用完点「清空」**,或干脆关掉自动复制、不复制到历史。
|
||||
- **不保留任何生成记录**。密码只存在于剪贴板与页面显示,刷新即失去痕迹;`localStorage` 里只存偏好设置与主题,不存密码。
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
@@ -56,18 +74,65 @@ Chrome / Edge / Firefox / Safari 的近两年版本,以及移动端浏览器
|
||||
|
||||
```
|
||||
index.html 全部内容:结构、样式、脚本
|
||||
docs/ README 用的界面截图
|
||||
logo/ 品牌 logo 多尺寸 PNG(5 档 × 带阴影/无阴影)
|
||||
```
|
||||
|
||||
`docs/logo/` 里的图**只进仓库、不部署到站点**(`_sp_deploy.sh` 只覆盖 `index.html`),
|
||||
所以线上访问 `https://password.yangyq.net/docs/logo/logo-1024.png` 会 404 —— 这是预期行为。
|
||||
|
||||
重新导出 logo(脚本在仓库外的 `_tooling/`,需要 `playwright` + `Pillow`)。
|
||||
**先 `cd _tooling/` 再跑**,脚本按 `__dirname/../simple_password` 定位仓库:
|
||||
|
||||
```bash
|
||||
cd ../_tooling
|
||||
node _sp_logo_export.js # 输出到 docs/logo/:logo-{64,128,256,512,1024}[-flat].png
|
||||
python3 _sp_logo_verify.py # 像素判据 95 项:白边比例 / 主体居中 / 阴影朝向 / 圆角
|
||||
python3 _sp_logo_verify_falsify.py # 反证 6 条:证明上面那套判据抓得住 bug
|
||||
```
|
||||
|
||||
> ⚠️ **不要在不同机器上重跑覆盖仓库里的 PNG**。仓库里这 10 张是在 Windows 侧生成的,
|
||||
> 换到 WSL(或另一台机器)重跑,字体度量与阴影模糊实现不同,产物**逐字节全不一样**
|
||||
> —— 实测 10/10 文件 md5 全变,阴影版最大通道差到 255。判据仍是 95/95 通过、
|
||||
> 几何关系一致,但 diff 会是一大堆无法 review 的二进制变更。真要重跑就接受这一点,
|
||||
> 别把它当成"脚本坏了"。
|
||||
|
||||
尺寸是 64/128/256/512/1024 五档,每档两版:`logo-N.png` 带品牌阴影(用于展示场合)、
|
||||
`logo-N-flat.png` 无阴影(用于 favicon、深色底、贴边摆放)。画布是正方形,
|
||||
白边控制在 shadow 8% / flat 3%(旧版 41%)。
|
||||
|
||||
## 开发校验
|
||||
|
||||
改完 `index.html` 后建议跑一遍(脚本在仓库外的 `_tooling/`,配合 `jsdom` 与 `playwright`):
|
||||
|
||||
```bash
|
||||
node _sp_test.js # 功能判据(jsdom,80 条,含反向注入 24 条)
|
||||
node _sp_visual.js # 真实浏览器几何 / 对比度 / 多视口(约 101 条)
|
||||
node _sp_clip.js # 真实剪贴板端到端(18 条)
|
||||
cd ../_tooling
|
||||
node _sp_test.js # 功能判据(jsdom,78 条)
|
||||
node _sp_visual.js # 真实浏览器几何 / 对比度 / 多视口(114 条)
|
||||
node _sp_clip.js # 真实剪贴板端到端(19 条)
|
||||
node _sp_file_check.js # file:// 双击直开(15 条)
|
||||
node _sp_falsify.js # 反向注入,证明功能判据有牙齿(22 条)
|
||||
node _sp_visual_falsify.js # 反向注入,证明几何判据有牙齿(6 条)
|
||||
```
|
||||
|
||||
反向注入必须单独跑视觉那套:`_sp_falsify.js` 只喂 `_sp_test.js`(jsdom 不做布局),
|
||||
把「按钮等大」「两栏左右」这类几何 bug 注进去根本影响不到被测判据,
|
||||
会表现成"注入后仍全绿",看着像判据没牙齿,其实是靶子选错。
|
||||
|
||||
重拍 README 截图:
|
||||
|
||||
```bash
|
||||
cd ../_tooling
|
||||
node _sp_shots.js # 输出到仓库 docs/:dark.png / light.png / mobile.png
|
||||
node _sp_shot_verify.js # 断言被截图的那个页面状态是对的(26 条)
|
||||
```
|
||||
|
||||
> ⚠️ 截图同样是**跨机器不可复现**的:手机视口实测 Windows 780×2490、WSL 780×2510,
|
||||
> 字体度量差异导致高度不同。判据只断言几何关系,别断言绝对页高,也别拿另一侧的图覆盖仓库。
|
||||
|
||||
> 截图脚本与判据脚本分开,是因为判据脚本的截图(`_sp_shots` 目录)尺寸与时机跟着判据走,
|
||||
> 会带 toast、焦点框、320px 极端视口;README 需要的是干净的产品图。混在一起改判据就会把图搞脏。
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
Before Width: | Height: | Size: 439 KiB After Width: | Height: | Size: 380 KiB |
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 411 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 314 KiB |
@@ -37,6 +37,8 @@
|
||||
|
||||
--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);
|
||||
@@ -73,6 +75,7 @@
|
||||
|
||||
--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);
|
||||
@@ -215,11 +218,15 @@ body::before {
|
||||
|
||||
/* ============================================================
|
||||
主卡片
|
||||
左 = 设置,右 = 输出。
|
||||
语义是「左侧可选地调设置,右侧直接完成主操作」——
|
||||
不动设置的人,视线直接落到右侧的密码与「复制密码」上。
|
||||
============================================================ */
|
||||
.card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr);
|
||||
/* 设置栏偏窄、输出栏偏宽:密码要用大字排开,设置只需容纳控件 */
|
||||
grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr);
|
||||
border-radius: var(--r-lg);
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface);
|
||||
@@ -231,7 +238,7 @@ body::before {
|
||||
|
||||
.pane { padding: clamp(22px, 3vw, 30px); min-width: 0; }
|
||||
|
||||
/* 左栏内容比右栏短,直接顶部对齐会在底部留出一大片空档(实测 180px)。
|
||||
/* 右栏(输出)内容比左栏短,直接顶部对齐会在底部留出一大片空档(实测 180px)。
|
||||
主内容用 auto 上下外边距居中,快捷键提示压在底部,版面才立得住。 */
|
||||
.pane--out {
|
||||
display: flex;
|
||||
@@ -285,7 +292,7 @@ kbd {
|
||||
}
|
||||
|
||||
.pane--ctl {
|
||||
border-left: 1px solid var(--line);
|
||||
border-right: 1px solid var(--line);
|
||||
background: var(--surface-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -374,7 +381,9 @@ kbd {
|
||||
}
|
||||
|
||||
.out-actions {
|
||||
display: flex;
|
||||
display: grid;
|
||||
/* 两个按钮严格等宽:均分而非「主按钮吃掉剩余空间」 */
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
@@ -398,8 +407,8 @@ kbd {
|
||||
.btn svg { width: 16px; height: 16px; flex: none; }
|
||||
.btn:active { transform: scale(.985); }
|
||||
|
||||
/* 「复制密码」:主色渐变,实心 */
|
||||
.btn--primary {
|
||||
flex: 1 1 auto;
|
||||
color: #fff;
|
||||
background: linear-gradient(180deg, #5b5ee8, #4f46e5);
|
||||
box-shadow: 0 10px 24px -12px rgba(99, 102, 241, .95),
|
||||
@@ -411,13 +420,19 @@ kbd {
|
||||
}
|
||||
.btn--primary:active { box-shadow: 0 6px 14px -10px rgba(99, 102, 241, .9); }
|
||||
|
||||
/* 「换一个」:同等尺寸,中性实心填充 + 描边区分层级。
|
||||
早先只是透明描边,和主按钮并排时显得矮一截、也像"不可点"。 */
|
||||
.btn--ghost {
|
||||
flex: 0 1 auto;
|
||||
color: var(--text-2);
|
||||
border-color: var(--line);
|
||||
background: transparent;
|
||||
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--ghost:hover { color: var(--text); border-color: var(--line-2); background: var(--surface-2); }
|
||||
|
||||
.btn[disabled] { opacity: .45; cursor: not-allowed; transform: none; }
|
||||
|
||||
@@ -767,11 +782,12 @@ input[type="range"]::-moz-range-thumb {
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
候选 / 历史
|
||||
候选
|
||||
============================================================ */
|
||||
/* 候选单独一张卡,整宽单栏(原先与本地记录面板并排,那个面板已移除) */
|
||||
.grid2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: clamp(14px, 2vw, 20px);
|
||||
margin-top: clamp(14px, 2vw, 20px);
|
||||
}
|
||||
@@ -857,6 +873,39 @@ input[type="range"]::-moz-range-thumb {
|
||||
.foot b { color: var(--text-2); font-weight: 550; }
|
||||
.foot .sep { opacity: .45; margin: 0 7px; }
|
||||
|
||||
/* 页脚链接区:三个外链,新窗口打开。
|
||||
刻意压得比正文更淡(继承 .foot 的 text-3),hover 才提亮 ——
|
||||
页脚不该跟主按钮抢视觉焦点。
|
||||
::after 的小箭头是"会跳出本页"的提示,也能让截图里一眼看出是新窗口链接。 */
|
||||
.foot-links {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2px 0;
|
||||
}
|
||||
.foot-links a {
|
||||
color: var(--text-3);
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
padding: 2px 3px;
|
||||
transition: color .15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.foot-links a:hover,
|
||||
.foot-links a:focus-visible {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.foot-links a::after {
|
||||
content: "↗";
|
||||
font-size: 10px;
|
||||
margin-left: 2px;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
@@ -888,8 +937,8 @@ input[type="range"]::-moz-range-thumb {
|
||||
============================================================ */
|
||||
@media (max-width: 880px) {
|
||||
.card { grid-template-columns: minmax(0, 1fr); }
|
||||
.pane--ctl { border-left: 0; border-top: 1px solid var(--line); }
|
||||
.grid2 { grid-template-columns: minmax(0, 1fr); }
|
||||
/* DOM 顺序就是「设置 → 输出」,单列时天然是设置在上、输出在下 */
|
||||
.pane--ctl { border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.pw { min-height: 96px; }
|
||||
}
|
||||
|
||||
@@ -941,52 +990,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
<main>
|
||||
<section class="card">
|
||||
|
||||
<!-- 输出 -->
|
||||
<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>
|
||||
|
||||
<!-- 控制 -->
|
||||
<!-- 控制(左):可选地调设置 -->
|
||||
<div class="pane pane--ctl">
|
||||
|
||||
<div class="presets" id="presets" role="group" aria-label="快捷预设">
|
||||
@@ -1054,6 +1058,52 @@ input[type="range"]::-moz-range-thumb {
|
||||
</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">
|
||||
@@ -1064,19 +1114,18 @@ input[type="range"]::-moz-range-thumb {
|
||||
</div>
|
||||
<ul class="list" id="candList"><li class="empty">点右上角一次生成多个备选,点任意一条即复制。</li></ul>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>最近生成</h2>
|
||||
<button class="link-btn" id="clearBtn" type="button">清空</button>
|
||||
</div>
|
||||
<ul class="list" id="histList"><li class="empty">还没有记录。生成并复制后会自动保存在本机。</li></ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="foot">
|
||||
<b>全程本地运算</b><span class="sep">·</span>使用 Web Crypto 密码学安全随机数<span class="sep">·</span>不联网、不上传、不留痕
|
||||
<span><b>全程本地运算</b><span class="sep">·</span>使用 Web Crypto 密码学安全随机数<span class="sep">·</span>不联网、不上传、不留痕</span>
|
||||
<nav class="foot-links" aria-label="项目相关链接">
|
||||
<a href="https://code.yangyq.net/laoyang/simple_password" target="_blank" rel="noopener noreferrer">开源地址</a>
|
||||
<span class="sep" aria-hidden="true">·</span>
|
||||
<a href="https://password.yangyq.net/" target="_blank" rel="noopener noreferrer">演示地址</a>
|
||||
<span class="sep" aria-hidden="true">·</span>
|
||||
<a href="https://feedlite.yangyq.net/?sw=simple_password" target="_blank" rel="noopener noreferrer">反馈</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -1097,11 +1146,9 @@ input[type="range"]::-moz-range-thumb {
|
||||
常量
|
||||
========================================================== */
|
||||
var KEY_PREFS = 'sp.prefs.v1';
|
||||
var KEY_HIST = 'sp.history.v1';
|
||||
var KEY_THEME = 'sp.theme.v1';
|
||||
|
||||
var MAX_LEN = 64;
|
||||
var HIST_MAX = 6;
|
||||
|
||||
var CLASSES = [
|
||||
{ id: 'lower', label: '小写', sample: 'abc', chars: 'abcdefghijklmnopqrstuvwxyz' },
|
||||
@@ -1392,7 +1439,6 @@ input[type="range"]::-moz-range-thumb {
|
||||
if (!current) return;
|
||||
copyText(current, function (ok) {
|
||||
if (ok) {
|
||||
pushHistory(current);
|
||||
toast('已复制到剪贴板');
|
||||
} else {
|
||||
toast('复制被拦截,请手动选中', true);
|
||||
@@ -1414,63 +1460,6 @@ input[type="range"]::-moz-range-thumb {
|
||||
}, 1900);
|
||||
}
|
||||
|
||||
/* ==========================================================
|
||||
历史(本机 localStorage)
|
||||
========================================================== */
|
||||
function readHistory() {
|
||||
try {
|
||||
var raw = localStorage.getItem(KEY_HIST);
|
||||
var arr = raw ? JSON.parse(raw) : [];
|
||||
if (!Array.isArray(arr)) return [];
|
||||
return arr.filter(function (x) { return x && typeof x.p === 'string' && x.p; });
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function pushHistory(pw) {
|
||||
var arr = readHistory();
|
||||
if (arr.length && arr[0].p === pw) { renderHistory(); return; }
|
||||
arr.unshift({ p: pw, t: Date.now() });
|
||||
arr = arr.slice(0, HIST_MAX);
|
||||
try { localStorage.setItem(KEY_HIST, JSON.stringify(arr)); } catch (e) {}
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
var box = $('histList');
|
||||
var arr = readHistory();
|
||||
box.textContent = '';
|
||||
|
||||
if (!arr.length) {
|
||||
var li = document.createElement('li');
|
||||
li.className = 'empty';
|
||||
li.textContent = '还没有记录。生成并复制后会自动保存在本机。';
|
||||
box.appendChild(li);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
(function (item) {
|
||||
box.appendChild(listItem(item.p, ago(item.t), function () {
|
||||
copyText(item.p, function (ok) {
|
||||
toast(ok ? '已从历史复制' : '复制被拦截', !ok);
|
||||
});
|
||||
}));
|
||||
})(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function ago(ts) {
|
||||
var diff = Math.max(0, Date.now() - ts);
|
||||
var m = Math.floor(diff / 60000);
|
||||
if (m < 1) return '刚刚';
|
||||
if (m < 60) return m + ' 分钟前';
|
||||
var h = Math.floor(m / 60);
|
||||
if (h < 24) return h + ' 小时前';
|
||||
return Math.floor(h / 24) + ' 天前';
|
||||
}
|
||||
|
||||
function listItem(text, meta, onClick) {
|
||||
var li = document.createElement('li');
|
||||
li.tabIndex = 0;
|
||||
@@ -1517,7 +1506,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
(function (pw, bits) {
|
||||
box.appendChild(listItem(pw, Math.round(bits) + ' bit', function () {
|
||||
copyText(pw, function (ok) {
|
||||
if (ok) { pushHistory(pw); toast('已复制候选密码'); }
|
||||
if (ok) { toast('已复制候选密码'); }
|
||||
else { toast('复制被拦截', true); }
|
||||
});
|
||||
}));
|
||||
@@ -1787,12 +1776,6 @@ input[type="range"]::-moz-range-thumb {
|
||||
|
||||
$('batchBtn').addEventListener('click', renderCandidates);
|
||||
|
||||
$('clearBtn').addEventListener('click', function () {
|
||||
try { localStorage.removeItem(KEY_HIST); } catch (e) {}
|
||||
renderHistory();
|
||||
toast('历史记录已清空');
|
||||
});
|
||||
|
||||
/* 快捷键:空格=换一个,C=复制(输入框内不拦截) */
|
||||
document.addEventListener('keydown', function (e) {
|
||||
var tag = (e.target && e.target.tagName || '').toLowerCase();
|
||||
@@ -1835,8 +1818,6 @@ input[type="range"]::-moz-range-thumb {
|
||||
/* 首屏只生成,不自动复制:避免在没有用户手势时触发剪贴板策略告警 */
|
||||
regenerate();
|
||||
|
||||
renderHistory();
|
||||
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) meta.setAttribute('content', currentTheme() === 'light' ? '#f4f5f8' : '#0a0b0f');
|
||||
}
|
||||
|
||||