Compare commits

...
6 Commits
Author SHA1 Message Date
laoyang 9b92c1ece1 feat(footer): 页脚加开源/演示/反馈三个外链,新窗口打开 (#4)
- 三个链接:Gitea 仓库、本站演示地址、FeedLite 反馈页(带 ?sw=simple_password)
- 全部 target="_blank" + rel="noopener noreferrer"(防新窗口经 window.opener 反向控制)
- 视觉层级压在 --text-3,hover 才切 --accent,不跟主按钮抢焦点
- ::after 加 ↗ 提示"会跳出本页"
- .foot-links 用 flex + flex-wrap,320px 视口实测不溢出、不换行

回归(全部在 WSL 实跑):
  功能 78/78 · 几何 114/114 · 剪贴板 19/19 · file:// 15/15
  页脚外链 23/23 · 页脚渲染 7/7
  反证 22/22 抓到 · 6/6 抓到

判据不是空靶子(都验过能红):
  - 页脚外链判据对**原始文件**跑 → 9 项失败、退出码 1
  - 页脚渲染判据对**伪造现场**(浅色图冒充 dark)跑 → 2 项失败
2026-11-03 13:26:39 +08:00
laoyang 9ca33a0328 docs(readme): 修正校验脚本调用方式,标注跨机器产物不可复现 (#2)
- 三个代码块补 `cd ../_tooling`:脚本不在仓库里,按 README 原来的写法
  (在仓库根目录直接 `node _sp_logo_export.js`)会 MODULE_NOT_FOUND
- 标注 docs/logo 的 PNG 与 docs 截图跨机器不可复现:Windows 重跑与已提交
  版本逐字节相同(10/10 一致),WSL 重跑则 10/10 全变、阴影版最大通道差 255,
  判据两边都是 95/95。先写清楚,免得下次当成脚本坏了去"修"
- 顺带记录 _sp_logo_export.js 改为按 __dirname 定位仓库(支持 SP_REPO 覆盖),
  不再写死 D:/ 或 /home/ 绝对路径
2026-10-29 14:59:52 +08:00
laoyang e09943fefb docs(readme): 补 docs/logo 说明与 logo 重新导出步骤 (#2)
- 目录树补 docs/logo/ 一行,注明「只进仓库、不部署到站点」,
  避免后续有人把线上 /docs/logo/logo-1024.png 的 404 误判成故障
  (_sp_deploy.sh 只覆盖 index.html)
- 新增 logo 重新导出命令:_sp_logo_export.js / _sp_logo_verify.py /
  _sp_logo_verify_falsify.py,解释器统一写 python3(WSL 无 python 别名)
- 尺寸档位按实际产物写 64/128/256/512/1024,五档 × 带阴影/无阴影
2026-10-29 13:24:47 +08:00
laoyang 33166407ea feat(assets): 新增品牌 logo 多尺寸 PNG(5 档 × 带阴影/无阴影)(#2)
从站点 .logo 导出可直接用于 favicon / 应用图标 / 分享卡的素材。

- 尺寸 1024/512/256/128/64,全部 RGBA 透明底
- 两版本:带阴影(贴浅色背景)、-flat(贴彩色/深色背景)
- 以 alpha 通道 bbox 自动识别主体边界再裁切,主体占画布
  85%(带阴影)/ 94%(flat),旧版留白 41% 已消除

导出脚本在仓库外 _tooling/_sp_logo_export.js,配套 95 项像素判据
与 6 条反证,不入库。
2026-10-28 23:39:46 +08:00
laoyang 03eca9dc9c docs: README 加入线上地址与界面章节,截图按新布局重拍,补 .gitignore (#1)
- README 顶部加线上地址 https://password.yangyq.net/
- 新增「界面」章节:左设置/右操作的双栏逻辑、两按钮实测 241×42 宽高差 0px
- 截图全部重拍(旧图还是改动前的布局):dark/light 2400×1640、mobile 780×2490
- 新增 .gitignore,忽略 node_modules(本地跑判据的落点,43MB,之前没被忽略)

验证(不只看文字):
- 截图像素级检查:深浅背景色正确、颜色多样性足够,排除白屏
- _sp_shot_verify.js 26 条:断言被截图的页面状态正确(主题/左右栏/按钮尺寸/无历史模块)
- _sp_readme_check.js 10 条:README 事实性断言与产品逐条对账
- _sp_readme_render.js 9 条:最小 GFM 渲染后三张图 naturalWidth 均 > 0
2026-10-28 15:25:47 +08:00
laoyang 6b4ee89d19 refactor: 设置左移、主操作按钮等大异色、移除最近生成 (#1)
三处按需求调整:

1. 设置区移到左边
   左 = 设置,右 = 输出。不调设置的人视线直接落到右侧密码与「复制密码」。
   DOM 顺序也改成设置在前,窄屏单列时天然是设置在上、输出在下。
   栅格比例同步反过来(设置 .92fr / 输出 1.08fr)。

2. 「复制密码」与「换一个」等大、不同色
   .out-actions 从 flex 改成 grid repeat(2, 1fr),两个按钮严格等宽;
   主按钮保留渐变,次按钮改为实心填充(新增 --surface-3,比 --surface-2 更实,
   避免深色下与卡片同色而看着像没填充)。

3. 移除「最近生成」模块
   删面板、清空按钮,以及 readHistory / pushHistory / renderHistory / ago
   与 sp.history.v1 的全部读写。候选面板改为整宽单栏。
   顺带清掉了共用工具 listItem 上的历史调用点。

验证(六套,全绿):
  功能 jsdom        78 / 0
  视觉几何对比度   114 / 0
  真实剪贴板端到端  19 / 0
  file:// 直开      15 / 0
  功能反向注入      22 / 22 命中
  视觉反向注入       6 / 6 命中

反向注入拆成两套:_sp_falsify.js 只喂 jsdom(不做布局),
把「按钮等大」「两栏左右」这类几何 bug 注进去影响不到被测判据,
会表现成"注入后仍全绿"、被误判成判据没牙齿(靶子选错)。
几何/视觉类归 _sp_visual_falsify.js。

删历史后专补两条"确实没了"的判据:DOM 节点不存在 +
localStorage 不再写入 sp.history.v1 —— 后者能抓到"面板删了但 JS 还在偷写"。
2026-10-28 13:57:29 +08:00
16 changed files with 204 additions and 146 deletions
+12
View File
@@ -0,0 +1,12 @@
# 本地校验用的依赖(脚本在仓库外的 _tooling/,这里只是 npm install 的落点)
node_modules/
package-lock.json
# 编辑器 / 系统垃圾
.vscode/
.idea/
.DS_Store
Thumbs.db
# 临时产物
*.log
+73 -8
View File
@@ -2,11 +2,14 @@
一个干净、纯本地的密码生成器。单文件 `index.html`,双击即用,也可丢到任意静态托管上。
![深色](./docs/dark.png)
**在线使用 → https://password.yangyq.net/**
![深色主题](./docs/dark.png)
## 特点
- **纯前端,零依赖** — 一个 `index.html`,没有构建步骤、没有 CDN、没有任何外部请求。断网可用。
- **左设置、右操作** — 设置栏在左,不调任何设置时视线直接落到右侧的密码与「复制密码」。窄屏自动变上下排列,设置在上。
- **密码学安全随机** — 走 `crypto.getRandomValues` + 拒绝采样消除取模偏差,不用 `Math.random()`。
- **生成即复制** — 默认开启自动复制;剪贴板 API 不可用(如 `file://` 下的旧浏览器)时自动降级到 `execCommand`。
- **深浅色** — 跟随系统偏好,可手动切换并记住选择。首帧前定主题,不会白屏闪一下。
@@ -16,13 +19,28 @@
- **自定义排除** — 想避开 `@`、空格或某个符号,直接填进去。
- **强度与熵** — 按 `长度 × log₂(字符池)` 算真实熵,分五档给出评价,同时显示字符池规模。
- **批量候选** — 一次出 5 个,点任意一条即复制。
- **最近生成** — 只存在本机 `localStorage`,最多 6 条,可一键清空。
- **不保留记录** — 不记历史,`localStorage` 只存偏好设置与主题,不存密码。
## 界面
设置与主操作分居左右两栏 —— 不调设置的人,视线直接落到右侧的密码和「复制密码」上。
「复制密码」与「换一个」严格等大(实测 241×42,宽高差 0px),用渐变与实心填充区分主次。
浅色主题:
![浅色主题](./docs/light.png)
移动端自动转为上下单列,设置在输出之上:
<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
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

After

Width:  |  Height:  |  Size: 380 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 314 KiB

+119 -138
View File
@@ -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');
}