RAM 角色自动探测 + 邮件配置界面 + 每个软件独立主题色 (#1 #2)

## 1. ECS RAM 角色改为自动探测
原先要用户去阿里云控制台查「实例 RAM 角色名」再手填,纯属多余 ——
机器自己知道答案。

新增 fb_oss_detect_instance_role():读元数据
GET /latest/meta-data/ram/security-credentials/(末尾带斜杠)拿到角色名列表。
- 兼容单行 / 多行 / JSON 数组三种返回
- 未配置角色时 fb_oss_instance_creds() 自动探测,不再报「未配置」
- 后台改成只读展示「已绑定:xxx」,手填入口收进「高级」折叠里
- 保存时留空 = 自动探测后写入,不存空值
- 探测失败给出可操作原因(没绑角色 / 不在 ECS 上 / 元数据被禁用)

## 2. 邮件配置界面(原先完全缺失)
lib/mailer.php 有完整 SMTP 实现,但后台一个输入框都没有,
只能改 config.php —— 用户装完根本找不到地方配。

根因有两层:
- fb_setting_keys() 白名单里没有任何 smtp.* / notify.* 键,
  就算加了表单,fb_set_setting() 也会把值全部丢弃
- 设置页只有「站点信息」「附件存储」两个区块

补上:白名单 15 个键 + 后台「邮件通知」区块
- SMTP 服务器/端口/加密方式(SSL/STARTTLS/不加密)
- 登录账号/密码(留空不修改,避免每次重填)
- 发件地址/显示名、超时、SSL 校验
- 通知规则:回复通知、每次回复都通知、新反馈通知管理员、发信冷却
- 「发送测试邮件」按钮,AJAX 发完就地显示成功或失败原因,
  不用保存再靠猜

## 3. 每个软件一个主题色
需求:从短链接进不同软件,颜色不同,一眼区分。

- software 表加 color 字段(幂等迁移)
- 全站配色靠 --brand / --brand-600 / --brand-soft 三个变量,
  按软件覆写这三个即可,不用为每个软件写一套 CSS
- fb_color_variants() 由主色推出深一档(×0.85)与浅一档(往白混 88%)
- 未配色的软件按 id 从 8 色调色板取默认色,保证同站不撞色
- 后台:颜色选择器(原生取色 + 8 个预设 + 用默认)
- 前台:index / new / thread 都按当前软件注入主题色,
  meta theme-color 也跟着变

验证:三功能专项 30/30;新旧回归全过
(全新安装 33/33、老基线 25/25、附件 22/22、安装向导 60/60、约束检查 20/20)
This commit is contained in:
2026-11-01 16:39:09 +08:00
parent c19e475e85
commit f1a81cbaa1
12 changed files with 498 additions and 12 deletions
+26 -4
View File
@@ -28,8 +28,9 @@ function fb_admin_taxonomy_action($pdo, $act, $post) {
}
if (fb_software($slug)) { return ['英文标识「' . $slug . '」已被占用,换一个。', 'error']; }
$sort = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM software')->fetchColumn();
$pdo->prepare('INSERT INTO software(slug,name,note,sort_order,enabled,created_at) VALUES(?,?,?,?,1,?)')
->execute([$slug, $name, $note !== '' ? $note : null, $sort, $now]);
$color = fb_color_norm(isset($post['color']) ? $post['color'] : '');
$pdo->prepare('INSERT INTO software(slug,name,note,color,sort_order,enabled,created_at) VALUES(?,?,?,?,?,1,?)')
->execute([$slug, $name, $note !== '' ? $note : null, $color, $sort, $now]);
$newId = (int)$pdo->lastInsertId();
// 新软件默认给一组常见分类,省得空着没法发帖
$i = 0;
@@ -52,8 +53,9 @@ function fb_admin_taxonomy_action($pdo, $act, $post) {
if ($slug === '') { $slug = (string)$sw['slug']; }
$other = fb_software($slug);
if ($other && (int)$other['id'] !== $id) { return ['英文标识「' . $slug . '」已被其他软件占用。', 'error']; }
$pdo->prepare('UPDATE software SET name=?, slug=?, note=?, sort_order=? WHERE id=?')
->execute([$name, $slug, $note !== '' ? $note : null,
$color = fb_color_norm(isset($post['color']) ? $post['color'] : '');
$pdo->prepare('UPDATE software SET name=?, slug=?, note=?, color=?, sort_order=? WHERE id=?')
->execute([$name, $slug, $note !== '' ? $note : null, $color,
(int)(isset($post['sort_order']) ? $post['sort_order'] : $sw['sort_order']), $id]);
return ['软件「' . $name . '」已保存。', 'ok'];
}
@@ -260,6 +262,8 @@ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) {
$isCur = $sid === $curSwId;
echo '<div class="tax-row' . ($isCur ? ' on' : '') . ($on ? '' : ' off') . '">';
echo ' <div class="tax-main">';
list($swC, $swCd, $swCs) = fb_software_colors($sw);
echo ' <span class="sw-dot" style="background:' . e($swC) . '" title="' . e($swC) . '"></span>';
echo ' <a class="tax-name" href="admin.php?tab=taxonomy&sw=' . $sid . '">' . e((string)$sw['name']) . '</a>';
echo ' <span class="tax-slug">' . e((string)$sw['slug']) . '</span>';
if (!$on) { echo ' <span class="badge st">已停用</span>'; }
@@ -305,6 +309,24 @@ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) {
echo ' <label class="field"><span>一句话说明<i class="hint">显示在列表页标题下,可留空</i></span>'
. '<input type="text" name="note" maxlength="120" value="'
. e($esw ? (string)($esw['note'] === null ? '' : $esw['note']) : '') . '"></label>';
// 主题色:前台按软件换色,用短链接进不同软件一眼就能区分
$curColor = $esw ? fb_color_norm((string)$esw['color']) : '';
$sugColor = $curColor !== '' ? $curColor : fb_color_default_for($esw ? (int)$esw['id'] : 0);
$presets = ['#0057B8', '#2E7D5B', '#B4553C', '#6A4C93', '#0E7C86', '#A02C5A', '#8A6D1F', '#3A5A8C'];
echo ' <div class="field"><span>主题色<i class="hint">前台进这个软件时整体配色跟着变,用来区分不同软件</i></span>';
echo ' <div class="color-row">';
echo ' <input type="color" name="color" value="' . e($sugColor) . '" class="color-input">';
echo ' <div class="color-presets">';
foreach ($presets as $pc) {
$on = strcasecmp($pc, $sugColor) === 0 ? ' on' : '';
echo '<button type="button" class="color-preset' . $on . '" data-color="' . e($pc)
. '" style="background:' . e($pc) . '" title="' . e($pc) . '"></button>';
}
echo ' </div>';
echo ' <button type="button" class="btn sm color-reset">用默认</button>';
echo ' </div>';
echo ' </div>';
if ($esw) {
echo ' <label class="field"><span>排序<i class="hint">数字小的排前面</i></span>'
. '<input type="number" name="sort_order" value="' . (int)$esw['sort_order'] . '"></label>';