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:
2026-10-28 13:57:29 +08:00
parent a48f414bbb
commit 6b4ee89d19
5 changed files with 90 additions and 142 deletions
+78 -137
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);
}
@@ -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');
}