Files
FeedLite/assets/style.css
T
laoyang 4033170ad7 修复:跨 tab 保存互相清空配置;补齐全流程邮件通知与统一模板 (#2)
## 一、用户报「我明明配过 SMTP,怎么全丢了、信也发不出」

**没有重置数据库**(installed_at 仍是 11-01 15:14,oss.*/site_name/admin.username 全在)。

真因是我上一轮「邮件拆独立 tab」挖的坑:站点设置与邮件通知**共用 act=settings**,
而保存函数把 smtp 字段无条件写库:

    foreach ($smtpText as $k) {
        fb_set_setting($k, trim((string)$post($k, \x27\x27)));   // 没提交 → 空串 → 覆盖
    }

在站点设置页保存附件设置时,该表单没有 smtp_* 字段,$post() 取到空串,
于是 host/user/from 被清成空。smtp.pass 因为单独做了「留空=不修改」而幸存,
所以现场是 pass 有值、其余全空 —— 这个组合本身就是 bug 的指纹。

修法:新增 $has() 判断「本次请求是否真的提交了该字段」,没提交就跳过。
按 tab 分两组保护:
- $isSiteForm:站点信息/页脚/附件/OSS,逐字段 has() 双保险
- $isMailForm:SMTP/通知开关,整段 + 逐字段

checkbox 尤其要保护:未提交会被当成「取消勾选」,
在站点设置页存一次就会把「有人回复时通知」全关掉。

## 二、notify.admin_new_thread 是假的(用户说「提交了反馈收不到邮件」)

这个设置项:白名单里有、后台有勾选框,**但没有任何代码实现它**。
grep 只命中三处(白名单/保存/界面),没有一处真正发信。

已在 new.php 提交成功后实现:查新帖 → 取所属软件名 → fb_notify_event(\x27new\x27)。

## 三、补齐全部事件,统一入口

此前只有 reply 和 close 两个事件,且两套模板风格不一致。
新增 fb_notify_event() 统一入口,处理「开关→收件人→节流→发信→日志」。

覆盖 8 类事件:reply / status / approve / hide / close / new。
- status 按状态给不同文案(待处理/处理中/已解决/关闭/不予考虑),
  「不予考虑」语气克制、必须带理由并留申诉入口
- approve / hide 原先完全不发信,现在补上
- 状态没变化时不发信(不打扰用户)

## 四、邮件模板重做

抽出 fb_mail_layout() 统一外壳,所有事件共用:
- table + 内联样式(Outlook/QQ邮箱/Gmail 兼容)
- 600px 卡片、顶部主色条、圆角徽标、字段表、引用块、callout、CTA 按钮
- 响应式(已验 375px 无溢出、无裁切)
- preheader 摘要、纯文本兜底链接、收件人提示

## 五、后台状态按钮(用户要求)

问题详情页顶部一排按钮,点一下立刻生效并自动发信:
「通过并公开」「处理中」「已解决」「不予考虑」「撤下」
- 当前状态对应的按钮置灰
- 「不予考虑」展开内联面板填理由(不用 window.prompt)
- 新增 setstatus 动作;「通过并公开」会同时置可见

## 六、notify_log 加 reason 列

原表只有 reply_id,无法区分事件类型。加 reason 并幂等迁移;
老库 reply_id 是 NOT NULL,非回复事件用 0 表示。

## 验证

邮件专项 73/73:
- 8 类事件模板全部完整 HTML、table 布局、徽标、页脚、无 script
- 8 个主题互不相同;状态徽标文案正确
- 总开关关闭时 admin_new_thread 仍可独立生效
- 节流只作用于 reply/status,close 不受影响
- SMTP 未启用时留失败日志(含 reason 与原因)
- C1-C5:站点页保存后 smtp.host/user/notify.enabled 均未被清空;
  邮件页保存后 oss.bucket 未被清空

反向验证:完整回退两层保护后 C1/C2/C3 全部 FAIL
(host 从 smtp.qiye.aliyun.com 变成空、notify.enabled 从 1 变 0),
证明测试能真正抓到该 bug,不是假通过。

回归:全新安装 33/33、老基线 25/25、附件 22/22。
2026-11-02 22:57:00 +08:00

518 lines
24 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* FeedLite · 轻反馈 —— 简洁、克制的基础视觉基调 */
:root{
--fg:#1f2329; --muted:#6b7280; --line:#e5e7eb; --line-strong:#d7dbe0;
--bg:#f5f6f8; --card:#ffffff; --brand:#0057B8; --brand-600:#004a9e; --brand-soft:#e8f0f9;
/* 顶栏品牌图标:与官网首页同一枚应用图标(内联,离线预览也能显示) */
--icon-mark:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAADsOAAA7DgHMtqGDAAADj0lEQVR4nO1b205TQRTdGB/UB78B0BdfRNA3NUT/QPHMHIgaP0BeMPqoog8mJqDyCfoBImJnDuCFyEWhIGiUegONimkFodYEaqpj9qEEwZbS0zOXXlaykqZNO2uvM9M9Z8/ZABkhysBy9gLhl4Dwh0B5CAj7CZQLo4iaCJtwNRLWDITVZI4tY+CMAOGvtQfnnSGwnWNuLFmBBnYAZU8NCMAvDoLlVGwseCtwAAgLGyDaZ7IZsJ3D6wdP+CEg7Jd+sdIYBytQm+bKOxVAecQAkfJngsV3rolelBXYms/EgdV/jJRTA0SpJeF1K1c/v1OdRwPYxFL8dtc+7WJ00Q5U4/S/rF2INrKLkNzeiqIkYfcBCHujXYg+hgAoixkgRBNZDHL5gU22IxraxsXdkYiYnlsUid9/hGzgGDhWRzAi6tvGXQ05mQAev1jZ2CtGp6JCN0Ymo64WpQZUNvaKSDQuTEE4GhcVXk0AD9PehCu/FsH3UW/LAbL8Aq55U2HfGJNvQOdoRJiKO8GwfAO+zi0KU/Hl+6J8AxIKUp1XoDbpBqTCp9kFsftsf/aDe2TVuX7xeXYhpRYtBrR0TikLfpmt9z6YY8DQu3mx5XiXsuC3nuh2054xBiBwe9rzYkZ0P5dLHGO9P2JtBpiCkgG0NANEQaTBbSe7xdWOyfxYAi2S0uD2Uz35YcCQpDRoXRvLDwNkpMEnb+c9bbu1GWAKSgbQ0gwQUpdAIsW69JoGNzc4oq71mW+32Epuh6dT7MNzTYNY2fUDeIss3YCOYMTXNIjV3NhCwhcDbg8pKInVpymKekmDj17O+hY8gl5XUBTdZDu+TVk/MayqLA7JgxE8jDAFqKX8tKKDEfhn7aaryqi+8p5PhWgOBiwvBzyMwHo8lqRVHY7iWO3DYXfNazscBQ88cyuUMcCmmyFlekC1Acgr7env8fEz1XpA+YDJ88WP31bq+vga39OhBbQMmtwG72p67BJf69IB2gY2haBbgG4C5T+0i9BFwqNQlI/JrnpclrIHxf6gZLN2Ifp4Hg2oKd4ZwKuSzRI8VHzBs1crDRPYWqZbkGragSNrWmb4oHZRysj6/u8lpKy8aJqmsC8yTc9grdtapl2kNMaBsoMZeged/QXbOIl9kRuC5fYQDhRQ8H3uEs8OArvJ6tztovYAckh1lB2FnGEHqt0GI9w6uoaY2GXCYkvt86iRXwDL2bOR0P4Cy8Hrhv+SooQAAAAASUVORK5CYII=");
--ok:#0f9d58; --ok-soft:#e9f7ef; --warn:#b7791f; --warn-soft:#fff8e6;
--danger:#d93025; --danger-soft:#fdecea; --shadow:0 1px 3px rgba(16,24,40,.06);
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0; background:var(--bg); color:var(--fg);
font-family:-apple-system,BlinkMacSystemFont,"HarmonyOS Sans SC","HarmonyOS Sans","PingFang SC","Helvetica Neue","Microsoft YaHei",sans-serif;
font-size:15.5px; line-height:1.75; letter-spacing:.01em;
}
.wrap{max-width:880px; margin:0 auto; padding:0 16px}
a{color:var(--brand); text-decoration:none}
a:hover{text-decoration:underline}
h1{font-size:23px; margin:0 0 8px; font-weight:700}
h2{font-size:17px; margin:0 0 12px; font-weight:700}
p{margin:8px 0}
.muted{color:var(--muted)}
.small{font-size:13px}
.dot{color:var(--line-strong); padding:0 2px}
/* ---------- 顶栏(与官网首页同一套导航样式) / 页脚 ---------- */
.topbar{
position:sticky; top:0; z-index:20;
background:rgba(255,255,255,.82);
backdrop-filter:saturate(180%) blur(14px);
-webkit-backdrop-filter:saturate(180%) blur(14px);
border-bottom:1px solid var(--line);
}
.topbar .wrap{display:flex; align-items:center; gap:14px; height:64px}
.topbar .brand-mini{display:flex; align-items:center; gap:10px; font-weight:700; font-size:17px; color:var(--fg); min-width:0}
.topbar .brand-mini:hover{text-decoration:none; color:var(--fg)}
.topbar .brand-text{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.topbar .logo-mark{
width:30px; height:30px; flex:none; display:block; border-radius:8px;
background-image:var(--icon-mark); background-size:cover; background-position:center;
box-shadow:var(--shadow);
}
.topbar .spacer{flex:1}
.topbar .navlink{color:var(--muted); font-size:14.5px; padding:8px 10px; border-radius:8px; white-space:nowrap}
.topbar .navlink:hover{color:var(--brand); background:var(--brand-soft); text-decoration:none}
.topbar .btn-mini{
flex:none; font-size:14.5px; font-weight:600; color:#fff; background:var(--brand);
padding:9px 18px; border-radius:999px; white-space:nowrap;
box-shadow:0 4px 14px -4px rgba(0,87,184,.55);
}
.topbar .btn-mini:hover{background:var(--brand-600); color:#fff; text-decoration:none}
.hide-sm{display:inline-block}
@media (max-width:640px){
.hide-sm{display:none}
.topbar .wrap{gap:10px}
.topbar .brand-mini{font-size:15.5px; gap:9px}
.topbar .logo-mark{width:28px; height:28px}
}
main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
.foot{border-top:1px solid var(--line); background:var(--card); padding:22px 0 34px; margin-top:40px}
.foot p{margin:4px 0; text-align:center; font-size:13px; color:var(--muted)}
.foot a{color:var(--muted); text-decoration:underline}
/* 版权句 + 管理后台入口:同一行居中,窄屏自动换行 */
.foot-line{display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:0 6px}
.foot-line .dot{padding:0}
/* ---------- 按钮 ---------- */
.btn{
display:inline-block; border:1px solid var(--line-strong); background:var(--card);
color:var(--fg); border-radius:9px; padding:8px 16px; font-size:14.5px;
cursor:pointer; line-height:1.4; font-family:inherit;
}
.btn:hover{border-color:var(--brand); color:var(--brand); text-decoration:none}
.btn.primary{background:var(--brand); border-color:var(--brand); color:#fff; font-weight:600}
.btn.primary:hover{background:var(--brand-600); color:#fff}
/* ---------- 页头 ---------- */
.page-head{display:flex; align-items:flex-start; gap:16px; margin:6px 0 18px}
.page-head .btn{margin-left:auto; flex:none; margin-top:4px}
.page-head h1{margin-bottom:4px}
.page-head .muted{font-size:14px; margin:0}
/* ---------- 后台处理按钮栏 ---------- */
.act-bar{
margin:0 0 18px; padding:14px 16px;
background:var(--card); border:1px solid var(--line); border-radius:12px;
}
.act-bar-title{
font-size:12.5px; font-weight:700; letter-spacing:.5px;
color:var(--muted); text-transform:uppercase; margin-bottom:10px;
}
.act-bar-body{display:flex; flex-wrap:wrap; gap:8px; align-items:center}
.act-inline{display:inline}
.act-btn{white-space:nowrap}
.act-btn.is-cur{
opacity:.55; cursor:default;
box-shadow:none;
}
.act-btn.ok{border-color:#b7dfc2; color:#1e7b3c}
.act-btn.ok:hover:not(:disabled){background:#f0f9f2}
.act-btn.warn{border-color:#f0dfae; color:#8a6a15}
.act-btn.warn:hover:not(:disabled){background:#fff9ec}
.act-state.on{
display:inline-block; padding:6px 12px; border-radius:99px;
font-size:12.5px; font-weight:600;
background:#f0f9f2; color:#1e7b3c; border:1px solid #b7dfc2;
}
.act-hint{
margin-left:auto; font-size:12.5px; color:var(--muted);
}
@media (max-width:560px){
.act-hint{margin-left:0; flex-basis:100%}
}
.act-note{
margin-top:14px; padding-top:14px; border-top:1px dashed var(--line);
}
/* ---------- 主题色选择器 ---------- */
.sw-dot{
display:inline-block; width:10px; height:10px; border-radius:50%;
flex:none; margin-right:2px; vertical-align:middle;
box-shadow:0 0 0 1px rgba(0,0,0,.12);
}
.color-row{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
.color-input{
width:46px; height:34px; padding:2px; border:1px solid var(--line-strong);
border-radius:9px; background:var(--card); cursor:pointer; flex:none;
}
.color-presets{display:flex; gap:6px; flex-wrap:wrap}
.color-preset{
width:26px; height:26px; border-radius:50%; border:2px solid transparent;
cursor:pointer; padding:0; flex:none; transition:transform .12s;
}
.color-preset:hover{transform:scale(1.12)}
.color-preset.on{border-color:var(--fg); box-shadow:0 0 0 2px var(--card) inset}
/* ---------- 邮件测试 ---------- */
.mail-test{
display:flex; flex-wrap:wrap; align-items:center; gap:10px;
padding:12px 14px; margin:6px 0 16px;
background:var(--card); border:1px dashed var(--line); border-radius:11px;
}
.mail-test input[type=email]{
flex:1 1 220px; min-width:180px;
border:1px solid var(--line-strong); border-radius:9px; padding:8px 10px;
font-size:14px; background:var(--card); color:var(--fg); font-family:inherit;
}
.mail-test-out{flex:1 1 100%; font-size:13px; line-height:1.6}
.mail-test-out.ok{color:var(--ok)}
.mail-test-out.err{color:#c9302c}
/* RAM 角色探测结果 */
.role-ok{
padding:8px 11px; border-radius:9px; font-size:13.5px;
background:var(--ok-soft, #eef8f0); border:1px solid #cfe8d5;
}
.role-warn{
padding:8px 11px; border-radius:9px; font-size:13.5px;
background:#fff8e6; border:1px solid #f0e0b0; color:#8a6d1f;
}
/* ---------- 表单就地校验的错误提示 ---------- */
.field.has-error > input,
.field.has-error > textarea,
.field.has-error > select,
.field.has-error .chips{
border-color:#d9534f !important;
box-shadow:0 0 0 3px rgba(217,83,79,.12) !important;
}
.field-error{
display:block; margin-top:6px; font-style:normal;
font-size:12.5px; color:#c9302c; line-height:1.5;
}
.field-error::before{content:"⚠ "; font-style:normal}
/* ---------- 后台:软件 / 分类 / 状态 ---------- */
.card-list{display:flex; flex-direction:column; gap:8px; margin-bottom:16px}
.tax-row{
display:flex; align-items:flex-start; gap:12px; flex-wrap:wrap;
padding:12px 14px; background:var(--card); border:1px solid var(--line);
border-radius:11px;
}
.tax-row.on{border-color:var(--brand); box-shadow:0 0 0 1px var(--brand) inset}
.tax-row.off{opacity:.62}
.tax-main{flex:1 1 320px; min-width:0}
.tax-name{font-weight:600; font-size:15.5px; text-decoration:none; color:var(--fg)}
.tax-name:hover{color:var(--brand)}
.tax-name.plain{font-weight:600}
.tax-slug{
display:inline-block; margin-left:8px; font-size:12px; color:var(--muted);
font-family:ui-monospace,Consolas,monospace;
}
.tax-meta{margin-left:10px; font-size:12.5px; color:var(--muted)}
.tax-note{margin-top:5px; font-size:13px; color:var(--muted); line-height:1.5}
.tax-acts{display:flex; flex-wrap:wrap; gap:6px; align-items:center}
.inline-form{display:inline}
.btn.sm{padding:5px 11px; font-size:13px; border-radius:8px}
.btn.danger{border-color:#e0b4b4; color:#b4242c}
.btn.danger:hover{background:#fdf2f2; border-color:#b4242c}
.tax-form-box{
margin:18px 0 8px; border:1px solid var(--line); border-radius:11px;
background:var(--card); overflow:hidden;
}
.tax-form-box>summary{
cursor:pointer; padding:12px 14px; font-weight:600; font-size:14.5px;
background:var(--bg-soft, #f7f8fa); user-select:none;
}
.tax-form-box>summary:hover{color:var(--brand)}
.tax-form-box[open]>summary{border-bottom:1px solid var(--line)}
.sm-form{padding:14px; margin:0}
.sm-form .field{margin-bottom:10px}
/* ---------- 软件切换(一级) ---------- */
.software-bar{
display:flex; flex-wrap:wrap; align-items:center; gap:8px;
margin-bottom:14px; padding-bottom:14px; border-bottom:1px solid var(--line);
}
.software-bar .sw-label{font-size:13.5px; color:var(--muted); font-weight:600}
.software-bar .chips{margin-bottom:0}
/* ---------- 筛选(二级:分类 / 状态) ---------- */
.filters{margin-bottom:16px}
.chips{display:flex; flex-wrap:wrap; gap:8px; margin-bottom:12px}
.chip{
padding:5px 13px; border-radius:999px; background:var(--card);
border:1px solid var(--line); font-size:13.5px; color:var(--fg);
}
.chip:hover{border-color:var(--brand); color:var(--brand); text-decoration:none}
.chip.on{background:var(--brand); border-color:var(--brand); color:#fff; font-weight:600}
.filter-row{display:flex; flex-wrap:wrap; gap:8px}
.filter-row select,.filter-row input[type=search]{
border:1px solid var(--line-strong); border-radius:9px; padding:8px 10px;
font-size:14px; background:var(--card); color:var(--fg); font-family:inherit;
}
.filter-row input[type=search]{flex:1; min-width:160px}
/* ---------- 列表 ---------- */
.list{display:flex; flex-direction:column; gap:12px}
.item{
display:block; background:var(--card); border:1px solid var(--line);
border-radius:13px; padding:15px 17px; box-shadow:var(--shadow); color:inherit;
}
.item:hover{border-color:var(--brand); text-decoration:none; box-shadow:0 3px 12px rgba(0,87,184,.10)}
.item-top{display:flex; align-items:center; flex-wrap:wrap; gap:8px}
.item-title{font-weight:600; font-size:16px; flex:1; min-width:160px}
.item-ex{color:var(--muted); font-size:14px; margin:8px 0 8px; word-break:break-word}
.item-meta{display:flex; flex-wrap:wrap; align-items:center; gap:6px; font-size:13px; color:var(--muted)}
.item-meta .who{color:#3a424d; font-weight:600}
/* 问题编号:便于按 id 逐条跟进与引用 */
.item-id{
flex:none; font-size:12.5px; font-weight:600; color:var(--muted);
font-variant-numeric:tabular-nums; letter-spacing:.02em;
border:1px solid var(--line); border-radius:6px; padding:0 7px; line-height:1.9;
}
/* ---------- 徽标 ---------- */
.badge{
display:inline-block; font-size:12px; line-height:1.6; padding:1px 9px;
border-radius:999px; background:var(--brand-soft); color:var(--brand);
font-weight:600; white-space:nowrap;
}
.badge.st{background:#f1f3f5; color:#5b636e}
.badge.st-open{background:var(--warn-soft); color:var(--warn)}
.badge.st-doing{background:var(--brand-soft); color:var(--brand)}
.badge.st-resolved{background:var(--ok-soft); color:var(--ok)}
.badge.st-closed{background:#f1f3f5; color:#8a929c}
.badge.official{background:var(--brand); color:#fff}
/* 软件徽章:比分类更"重",一眼看出属于哪个软件 */
.badge.sw{background:#eef2f8; color:#2b4a74; border:1px solid #cfdcee; font-weight:600}
.badge.st-wontfix{background:#f6eef0; color:#a13c53}
/* ---------- 分页 ---------- */
.pager{display:flex; align-items:center; gap:12px; margin:22px 0 0; justify-content:center}
.pager-info{font-size:13.5px; color:var(--muted)}
/* ---------- 空态 ---------- */
.empty{
background:var(--card); border:1px dashed var(--line-strong); border-radius:13px;
padding:34px 20px; text-align:center; color:var(--muted);
}
.empty.small{padding:20px}
/* ---------- 面包屑 ---------- */
.crumbs{margin:4px 0 14px; font-size:14px}
/* ---------- 主题 / 回复 ---------- */
.subject{background:var(--card); border:1px solid var(--line); border-radius:14px; padding:20px 22px; box-shadow:var(--shadow)}
.subject-title{font-size:21px; margin:0; flex:1; min-width:200px}
.subject-body{margin-top:14px; white-space:normal; word-break:break-word; line-height:1.85}
.subject-body br{content:""}
.replies{margin-top:22px}
.reply{
background:var(--card); border:1px solid var(--line); border-left:3px solid var(--line-strong);
border-radius:11px; padding:13px 16px; margin-bottom:11px;
}
.reply.admin{border-left-color:var(--brand); background:linear-gradient(180deg,var(--brand-soft),var(--card) 42%)}
.reply-head{display:flex; align-items:center; gap:8px; font-size:13px; color:var(--muted); margin-bottom:6px}
.reply-head .who{font-weight:600; color:#3a424d; font-size:14px}
.reply-body{word-break:break-word; line-height:1.8}
/* ---------- 表单 ---------- */
.narrow{max-width:640px}
.card-form{background:var(--card); border:1px solid var(--line); border-radius:14px; padding:20px 22px; box-shadow:var(--shadow); margin-top:14px}
.field{display:block; margin-bottom:16px}
.field > span{display:block; font-size:14px; font-weight:600; margin-bottom:6px}
.field .req{color:var(--danger)}
.field .hint{font-style:normal; font-weight:400; font-size:12.5px; color:var(--muted); margin-left:6px}
.field input[type=text],.field input[type=email],.field input[type=tel],
.field input[type=password],.field textarea,.field select{
width:100%; border:1px solid var(--line-strong); border-radius:9px;
padding:9px 12px; font-size:15px; font-family:inherit; background:var(--card);
color:var(--fg); line-height:1.6;
}
.field textarea{resize:vertical; min-height:110px}
.field input:focus,.field textarea:focus,.field select:focus{
outline:none; border-color:var(--brand); box-shadow:0 0 0 3px var(--brand-soft);
}
.row2{display:grid; grid-template-columns:1fr 1fr; gap:14px}
@media (max-width:560px){ .row2{grid-template-columns:1fr} }
.captcha{margin:18px 0 0; padding:14px 16px; background:#fafbfc; border:1px solid var(--line); border-radius:11px}
.captcha .field{margin-bottom:0}
.captcha-line{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
.captcha-line .c-q{
flex:1; min-width:200px; font-weight:600; color:var(--brand);
background:var(--brand-soft); border-radius:8px; padding:8px 12px;
}
.captcha-line input{flex:none; width:110px}
.captcha-line .reload{font-size:13.5px; white-space:nowrap}
.form-actions{margin-top:20px}
.form-actions .btn{padding:10px 26px; font-size:15px}
.admin-pw{margin-top:6px}
.admin-status{margin-bottom:16px}
/* 蜜罐字段:对用户不可见,但保留在 DOM 中诱捕机器人 */
.hp-field{position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden}
/* ---------- 提示条 ---------- */
.flash{border-radius:10px; padding:11px 15px; font-size:14px; margin:0 0 16px}
.flash.error{background:var(--danger-soft); border:1px solid #f6c6c1; color:#9c2b21}
.flash.ok{background:var(--ok-soft); border:1px solid #bfe5cd; color:#0b6b3d}
/* ---------- 附件上传(浏览器直传 OSS) ---------- */
.uploader-box{
margin:18px 0 0; padding:14px 16px; background:#fafbfc;
border:1px solid var(--line); border-radius:11px;
}
.uploader{border-radius:10px; transition:background .15s, box-shadow .15s}
.uploader + .uploader{margin-top:14px; padding-top:14px; border-top:1px dashed var(--line)}
.uploader.dragging{background:var(--brand-soft); box-shadow:0 0 0 2px var(--brand) inset}
.up-head{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
.up-title{font-size:14px; font-weight:600}
.up-head .btn{margin-left:auto; padding:6px 14px; font-size:13.5px; flex:none}
.up-file{display:none}
.up-list{display:flex; flex-wrap:wrap; gap:9px; margin-top:10px}
.up-list:empty{display:none}
.up-item{
position:relative; display:flex; gap:10px; align-items:center; width:100%;
padding:8px 10px; border:1px solid var(--line); border-radius:10px; background:var(--card);
}
.up-thumb{
width:52px; height:52px; flex:none; border-radius:8px; overflow:hidden; background:#eef1f4;
display:flex; align-items:center; justify-content:center;
}
.up-thumb img{width:100%; height:100%; object-fit:cover; display:block}
.up-thumb-video{font-size:10px; font-weight:700; color:var(--muted); letter-spacing:.08em}
.up-info{flex:1; min-width:0}
.up-name{font-size:13.5px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.up-state{font-size:12.5px; color:var(--muted); word-break:break-word}
.up-bar{height:4px; border-radius:2px; background:#e6eaee; overflow:hidden; margin-top:5px}
.up-bar i{display:block; height:100%; width:0; background:var(--brand); transition:width .2s}
.up-item.done .up-bar{display:none}
.up-item.fail{border-color:#f6c6c1; background:var(--danger-soft)}
.up-item.fail .up-state{color:var(--danger)}
.up-del{
flex:none; border:1px solid var(--line-strong); background:var(--card); color:var(--muted);
border-radius:7px; padding:5px 10px; font-size:12.5px; cursor:pointer; font-family:inherit;
}
.up-del:hover{border-color:var(--danger); color:var(--danger)}
.up-del:disabled{opacity:.45; cursor:default}
/* ---------- 附件展示 ---------- */
/* align-items:flex-start —— 否则同行里有视频(多了标题栏)时,图片盒子会被拉伸,底部留白 */
.atts{display:flex; flex-wrap:wrap; align-items:flex-start; gap:10px; margin-top:14px}
.atts .att{
margin:0; max-width:100%; border:1px solid var(--line); border-radius:11px;
overflow:hidden; background:var(--card);
}
.atts .att-image img{display:block; width:220px; height:auto; max-height:200px; object-fit:cover}
.atts.compact .att-image img{width:150px; max-height:140px}
.atts .att-video{flex:1 1 320px; max-width:440px}
.atts .att-video video{display:block; width:100%; max-height:330px; background:#000}
.att-video-ph{
width:360px; max-width:100%; height:200px; background:#11161c; color:#8b97a5;
display:flex; align-items:center; justify-content:center; font-size:13px;
}
.atts figcaption{
display:flex; gap:8px; align-items:center; padding:6px 9px; font-size:12px; color:var(--muted);
}
.atts .att-name{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.atts .att-size{margin-left:auto; flex:none}
.thumbs{display:flex; gap:8px; margin:0 0 8px}
.thumbs img{
width:78px; height:78px; object-fit:cover; display:block;
border-radius:9px; border:1px solid var(--line);
}
.att-tag{color:var(--brand); font-weight:600}
/* ---------- 附件灯箱(点击放大 + 左右切换) ---------- */
.atts .att-image .att-lb{display:block; line-height:0; cursor:zoom-in}
.atts .att-image .att-lb:focus-visible{outline:2px solid var(--brand); outline-offset:2px}
.att-zoom{
flex:none; border:1px solid var(--line-strong); background:var(--card); color:var(--muted);
border-radius:6px; padding:2px 9px; font-size:12px; line-height:1.6; cursor:pointer; font-family:inherit;
}
.att-zoom:hover{border-color:var(--brand); color:var(--brand)}
html.lb-open,body.lb-open{overflow:hidden}
.lb{
position:fixed; inset:0; z-index:1000; background:rgba(12,14,17,.94);
display:flex; flex-direction:column; animation:lb-in .16s ease-out;
}
.lb[hidden]{display:none}
@keyframes lb-in{from{opacity:0} to{opacity:1}}
.lb-stage{flex:1; min-height:0; position:relative}
.lb-media{
position:absolute; inset:0; z-index:1; padding:48px 62px;
display:flex; align-items:center; justify-content:center;
}
.lb-media img,.lb-media video{
display:block; max-width:100%; max-height:100%; border-radius:6px;
box-shadow:0 10px 44px rgba(0,0,0,.55);
}
.lb-media video{background:#000; width:auto}
.lb-media.loading::after{
content:''; position:absolute; width:26px; height:26px; border-radius:50%;
border:3px solid rgba(255,255,255,.26); border-top-color:#fff;
animation:lb-spin .8s linear infinite;
}
@keyframes lb-spin{to{transform:rotate(360deg)}}
.lb-nav{
position:absolute; top:50%; transform:translateY(-50%); z-index:2;
width:46px; height:46px; padding:0 0 3px; border:0; border-radius:50%; cursor:pointer;
background:rgba(255,255,255,.14); color:#fff; font-size:26px; line-height:1;
display:flex; align-items:center; justify-content:center; font-family:inherit;
}
.lb-nav:hover{background:rgba(255,255,255,.32)}
.lb-nav[hidden]{display:none}
.lb-prev{left:14px}
.lb-next{right:14px}
.lb-x{
position:absolute; top:14px; right:14px; z-index:3;
width:40px; height:40px; border:0; border-radius:50%; cursor:pointer;
background:rgba(255,255,255,.14); color:#fff; font-size:17px; font-family:inherit;
}
.lb-x:hover{background:rgba(255,255,255,.32)}
.lb-bar{
flex:none; display:flex; align-items:center; gap:10px; flex-wrap:wrap;
padding:10px 18px 16px; color:#e9edf2; font-size:13.5px;
}
.lb-name{font-weight:600; max-width:58vw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.lb-meta{color:#9aa4b0; white-space:nowrap}
.lb-orig{margin-left:auto; color:#9ec5ff; white-space:nowrap}
.lb-orig[hidden]{display:none}
@media (max-width:560px){
.lb-media{padding:40px 10px}
.lb-nav{width:38px; height:38px; font-size:22px}
.lb-prev{left:6px} .lb-next{right:6px}
.lb-name{max-width:44vw}
}
/* ---------- 深色模式 ---------- */
@media (prefers-color-scheme: dark){
:root{
--fg:#e6e8eb; --muted:#9aa2ad; --line:#2c3138; --line-strong:#3a4149;
--bg:#15181c; --card:#1c2024; --brand:#5aa2ff; --brand-soft:#1e2a3d;
--ok:#4ec98a; --ok-soft:#16301f; --warn:#e2b559; --warn-soft:#2a2517;
--danger:#ff8a80; --danger-soft:#33191a; --shadow:0 1px 3px rgba(0,0,0,.35);
}
.item-meta .who,.reply-head .who{color:#cfd4da}
.field > span{color:#e6e8eb}
.captcha{background:#21262b}
.uploader-box{background:#21262b}
.up-thumb{background:#2c3138}
.up-bar{background:#2c3138}
.up-item.fail{border-color:#5a2a2a}
.badge.st{background:#242a30; color:#a8b0ba}
.badge.st-closed{background:#242a30; color:#8a929c}
.topbar{background:rgba(21,24,28,.82)}
.topbar .navlink:hover{background:var(--brand-soft)}
.btn:hover{border-color:var(--brand)}
.reply.admin{background:linear-gradient(180deg,var(--brand-soft),var(--card) 42%)}
.flash.error{background:var(--danger-soft); border-color:#5a2a2a; color:#ffb4ab}
.flash.ok{background:var(--ok-soft); border-color:#26503a; color:#9fe3bd}
}
/* ---- 管理后台 ---- */
.tabs{display:flex; flex-wrap:wrap; gap:8px; margin:16px 0 4px; border-bottom:1px solid var(--line); padding-bottom:10px}
.tab{padding:7px 14px; border-radius:999px; font-size:14px; color:var(--muted); background:var(--card); border:1px solid var(--line)}
.tab:hover{color:var(--brand); border-color:var(--brand)}
.tab.on{background:var(--brand); border-color:var(--brand); color:#fff}
.grid2{display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:12px 16px; margin:12px 0}
.grid2 .field{margin:0}
.check{display:flex; align-items:center; gap:8px; font-size:14px; color:var(--muted); margin:8px 0}
.check input{width:16px; height:16px}
.btn.danger{border-color:#e08b84; color:#c0392b}
.btn.danger:hover{background:#fdecea}
.filters{display:flex; flex-wrap:wrap; gap:8px; margin:18px 0 4px}
.filters input[type=text]{flex:1 1 220px; min-width:180px}
.filters select{max-width:160px}
@media (max-width:640px){
.grid2{grid-template-columns:1fr}
.filters input[type=text],.filters select{width:100%; max-width:none}
}