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 还在偷写"。
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
## 特点
|
||||
|
||||
- **纯前端,零依赖** — 一个 `index.html`,没有构建步骤、没有 CDN、没有任何外部请求。断网可用。
|
||||
- **左设置、右操作** — 设置栏在左,不调任何设置时视线直接落到右侧的密码与「复制密码」。窄屏自动变上下排列,设置在上。
|
||||
- **密码学安全随机** — 走 `crypto.getRandomValues` + 拒绝采样消除取模偏差,不用 `Math.random()`。
|
||||
- **生成即复制** — 默认开启自动复制;剪贴板 API 不可用(如 `file://` 下的旧浏览器)时自动降级到 `execCommand`。
|
||||
- **深浅色** — 跟随系统偏好,可手动切换并记住选择。首帧前定主题,不会白屏闪一下。
|
||||
@@ -16,7 +17,6 @@
|
||||
- **自定义排除** — 想避开 `@`、空格或某个符号,直接填进去。
|
||||
- **强度与熵** — 按 `长度 × log₂(字符池)` 算真实熵,分五档给出评价,同时显示字符池规模。
|
||||
- **批量候选** — 一次出 5 个,点任意一条即复制。
|
||||
- **最近生成** — 只存在本机 `localStorage`,最多 6 条,可一键清空。
|
||||
|
||||
## 使用
|
||||
|
||||
@@ -44,7 +44,7 @@ python -m http.server 8000
|
||||
- 所有运算在浏览器里完成,**不联网、不上传、不写日志**。
|
||||
- 用 `crypto.getRandomValues` 取随机数,并按「拒绝采样」丢弃落在偏置区间的值,避免取模带来的分布倾斜。
|
||||
- 勾「每类至少一位」时,先从每类各取一位,剩余位从全池随机,最后整体洗牌 —— 保证覆盖的同时不引入位置可预测性。
|
||||
- 历史记录存在 `localStorage` 里,任何能访问这台机器的人都能看到。**在共用电脑上建议用完点「清空」**,或干脆关掉自动复制、不复制到历史。
|
||||
- **不保留任何生成记录**。密码只存在于剪贴板与页面显示,刷新即失去痕迹;`localStorage` 里只存偏好设置与主题,不存密码。
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
@@ -63,11 +63,18 @@ index.html 全部内容:结构、样式、脚本
|
||||
改完 `index.html` 后建议跑一遍(脚本在仓库外的 `_tooling/`,配合 `jsdom` 与 `playwright`):
|
||||
|
||||
```bash
|
||||
node _sp_test.js # 功能判据(jsdom,80 条,含反向注入 24 条)
|
||||
node _sp_visual.js # 真实浏览器几何 / 对比度 / 多视口(约 101 条)
|
||||
node _sp_clip.js # 真实剪贴板端到端(18 条)
|
||||
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 注进去根本影响不到被测判据,
|
||||
会表现成"注入后仍全绿",看着像判据没牙齿,其实是靶子选错。
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 439 KiB After Width: | Height: | Size: 383 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 410 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 318 KiB |
+78
-137
@@ -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);
|
||||
}
|
||||
@@ -888,8 +904,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 +957,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 +1025,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,14 +1081,6 @@ 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>
|
||||
|
||||
@@ -1097,11 +1106,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 +1399,6 @@ input[type="range"]::-moz-range-thumb {
|
||||
if (!current) return;
|
||||
copyText(current, function (ok) {
|
||||
if (ok) {
|
||||
pushHistory(current);
|
||||
toast('已复制到剪贴板');
|
||||
} else {
|
||||
toast('复制被拦截,请手动选中', true);
|
||||
@@ -1414,63 +1420,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 +1466,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 +1736,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 +1778,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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user