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
+73
View File
@@ -94,6 +94,71 @@ function fb_default_software() {
return $list ? $list[0] : null;
}
/**
* 校验并规范化主题色(#RRGGBB)。
* @return string 合法则返回小写 6 位十六进制;非法返回空串
*/
function fb_color_norm($c) {
$c = strtolower(trim((string)$c));
if ($c === '') { return ''; }
if ($c[0] !== '#') { $c = '#' . $c; }
if (!preg_match('/^#[0-9a-f]{6}$/', $c)) { return ''; }
return $c;
}
/**
* 由主色推导出「深一档」和「浅一档」两个色,供 --brand-600 / --brand-soft 用。
* 不做真实色彩空间换算,只按通道线性调:深的一档乘 0.85,浅的往白色混 88%。
* 对区分软件这个目的来说足够,也不会因为极端颜色算出难看的对比。
*
* @return array [$hex, $hexDark, $hexSoft]
*/
function fb_color_variants($hex) {
$hex = fb_color_norm($hex);
if ($hex === '') { return ['', '', '']; }
$r = hexdec(substr($hex, 1, 2));
$g = hexdec(substr($hex, 3, 2));
$b = hexdec(substr($hex, 5, 2));
$dark = sprintf('#%02x%02x%02x',
max(0, (int)round($r * 0.85)),
max(0, (int)round($g * 0.85)),
max(0, (int)round($b * 0.85)));
// 往白色混 88%,得到很淡的底色(用于 badge / 选中态背景)
$mix = 0.88;
$soft = sprintf('#%02x%02x%02x',
(int)round($r + (255 - $r) * $mix),
(int)round($g + (255 - $g) * $mix),
(int)round($b + (255 - $b) * $mix));
return [$hex, $dark, $soft];
}
/**
* 给某个软件挑一个默认主题色(未配置时用)。
* 按软件的排序位次从调色板里取,保证同一站点里不同软件颜色不撞。
*/
function fb_color_default_for($softwareId) {
$palette = ['#0057B8', '#2E7D5B', '#B4553C', '#6A4C93', '#0E7C86', '#A02C5A', '#8A6D1F', '#3A5A8C'];
$id = max(0, (int)$softwareId);
return $palette[$id % count($palette)];
}
/**
* 取软件的主题色三元组(主色 / 深 / 浅)。
* 没配就用默认调色板,永远返回可用值。
* @return array [hex, hexDark, hexSoft]
*/
function fb_software_colors($software) {
$hex = '';
if (is_array($software)) {
$hex = fb_color_norm(isset($software['color']) ? $software['color'] : '');
if ($hex === '') { $hex = fb_color_default_for(isset($software['id']) ? $software['id'] : 0); }
}
return fb_color_variants($hex);
}
/** 某软件下的分类;不传软件则返回空数组 */
function fb_categories($softwareId = null, $onlyEnabled = true) {
if ($softwareId === null) { return []; }
@@ -301,6 +366,14 @@ function fb_setting_keys() {
'moderation.enabled', 'moderation.close_mail_subject',
'admin.password_hash',
'admin.username',
// 邮件通知(SMTP)—— 原先完全没有,后台配不了,只能改 config.php
'smtp.enabled',
'smtp.host', 'smtp.port', 'smtp.secure',
'smtp.user', 'smtp.pass',
'smtp.from', 'smtp.from_name',
'smtp.timeout', 'smtp.verify_ssl',
'notify.enabled', 'notify.on_every_reply', 'notify.cooldown_seconds',
'notify.admin_new_thread',
];
}