统一全部表单控件尺寸与外观;删除下载地址字段

用户:「输入框、按钮、下拉框特别丑,一点设计感都没有,高度太随意,
      全面检查一下。还有那个下载链接要它干什么?」

一、表单控件(这是主要工作量)
  原来样式散在 6 处(.field / .filter-row / .filters / .mail-test / .admin 各一套),
  padding 有 8/9/12px 三种,select 与 input 高度不等,并排放参差不齐。

  改成一套体系:
  - CSS 变量统一尺度:--ctl-h 38px / --ctl-h-sm 32px / --ctl-h-lg 42px,
    统一圆角 9px、统一左右内边距 12px、统一边框与阴影
  - 高度用「固定 height + line-height 居中」而不是 padding 撑
  - 🔴 选择器必须**宽松**(按 type 通吃,不用 .field 之类限定)。
     第一版限制了容器,导致后台零散布局里的裸 select/input 只有 19/21px,
     和旁边 38px 按钮并排,正是用户说的高度随意。
     放开后实测:19px → 38px,全部一致
  - 下拉框自绘箭头(内联 SVG),不再用各平台不一的原生箭头,右侧留 34px
  - 搜索框加左侧放大镜图标,并去掉 Chrome 自带清除按钮
  - 聚焦统一 brand 光晕,悬停边框加深
  - 按钮也改固定 height(原来 padding 撑出 38.3px,与输入框差 0.3px)
  - .btn.sm 必须显式改 height,否则 padding 被 height 吃掉、小按钮还是 38px

  文件选择按钮:原生控件重做成真按钮
    <label class=file-btn> 包住 input[type=file]
    input 用 clip 视觉隐藏(不能用 display:none,那样点不到也聚焦不了)
    按钮内显示「选择图片」+ 图标 + 选中后的文件名

二、删除「下载地址」字段
  它唯一用途:状态改为「已解决」时把邮件按钮换成「下载最新版本」。
  收益小、维护成本高(每个软件都要填一条 URL)。
  - 后台表单项删除
  - mailer 里对应的判断分支也删掉,按钮统一「查看详情」
  - ⚠️ 数据库 download_url 列**保留不删**:删列不可逆,万一要恢复还得迁一次

验证(用控件总览测试页 + 真实页面实测):
  10 类控件高度全部 38px(文本/数字/下拉/搜索/文件按钮/主按钮/小按钮…),
  文本域 110px,同一行垂直中心完全一致(935.3)
  裸 select/input 从 19/21px 修正到 38px
  下拉箭头 SVG 生效、右内边距 34px;搜索框放大镜生效、左内边距 36px
  真实三个页面(提交页/列表页/详情页)逐一检查:
    没有 checkbox/radio/file 被误改,所有可见控件高度收敛为 38(文本域 110)
  下载地址字段:admin.php 输出与源码中均无 download_url 表单项
  5 个路由 200
This commit is contained in:
2026-11-04 19:17:20 +08:00
parent a2a3306169
commit 27614d8ca8
5 changed files with 250 additions and 51 deletions
+19 -5
View File
@@ -993,21 +993,29 @@
var wrap = input.closest('.icon-up');
if (!wrap) { return; }
var prev = wrap.querySelector('.icon-prev');
// 按钮上的文件名(可选,新样式才有)
var nameEl = wrap.querySelector('[data-file-name]');
// 状态条:放在文件输入框下面,比页面顶部的提示显眼得多
/* 状态条插在**整个文件按钮之后**(不是 input 之后)。
* input 现在被包在 <label class="file-btn"> 里,如果插到 input.nextSibling
* 会跑到 label 内部,跟按钮文字挤在一起。 */
var btn = input.closest('.file-btn') || input;
var tip = document.createElement('span');
tip.className = 'icon-up-tip';
tip.setAttribute('role', 'status');
input.parentNode.insertBefore(tip, input.nextSibling);
if (btn.parentNode) { btn.parentNode.insertBefore(tip, btn.nextSibling); }
function setTip(msg, kind) {
tip.textContent = msg || '';
tip.className = 'icon-up-tip' + (kind ? ' is-' + kind : '');
}
function setName(txt) {
if (nameEl) { nameEl.textContent = txt || ''; }
}
input.addEventListener('change', function () {
var f = this.files && this.files[0];
if (!f) { setTip(''); return; }
if (!f) { setTip(''); setName(''); return; }
var KB = Math.round(f.size / 1024);
var sizeTxt = KB >= 1024 ? (KB / 1024).toFixed(1) + ' MB' : KB + ' KB';
@@ -1016,17 +1024,20 @@
var okType = /^image\/(png|jpeg|webp)$/.test(f.type);
if (!okType) {
setTip('✕ 只能选 PNG / JPG / WEBP 图片,当前是「' + (f.type || '未知类型') + '」', 'err');
setName('');
this.value = '';
return;
}
// ② 大小(后端限 2MB)
if (f.size > 2 * 1024 * 1024) {
setTip('✕ 图片 ' + sizeTxt + ',超过 2 MB 上限,请压缩后再传', 'err');
setName('');
this.value = '';
return;
}
if (f.size <= 0) {
setTip('✕ 这是个空文件', 'err');
setName('');
this.value = '';
return;
}
@@ -1038,6 +1049,7 @@
img.onload = function () {
if (img.width < 32 || img.height < 32) {
setTip('✕ 图片只有 ' + img.width + '×' + img.height + ',至少 32×32', 'err');
setName('');
input.value = '';
return;
}
@@ -1050,16 +1062,18 @@
prev.appendChild(el);
}
var warn = (img.width !== img.height) ? ' ⚠ 不是正方形,显示时会被裁切' : '';
setTip('✓ 已选择:' + f.name + '(' + img.width + '×' + img.height + ',' + sizeTxt + ')'
setName(f.name);
setTip('✓ ' + img.width + '×' + img.height + ',' + sizeTxt
+ warn + ' 保存后生效', 'ok');
};
img.onerror = function () {
setTip('✕ 这张图读不出来,可能已损坏', 'err');
setName('');
input.value = '';
};
img.src = ev.target.result;
};
fr.onerror = function () { setTip('✕ 读取文件失败', 'err'); };
fr.onerror = function () { setTip('✕ 读取文件失败', 'err'); setName(''); };
fr.readAsDataURL(f);
});
})(inputs[i]);