diff --git a/lib/admin_taxonomy.php b/lib/admin_taxonomy.php
index af18d61..593b4e2 100644
--- a/lib/admin_taxonomy.php
+++ b/lib/admin_taxonomy.php
@@ -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 '
';
echo '
';
+ list($swC, $swCd, $swCs) = fb_software_colors($sw);
+ echo '
';
echo '
' . e((string)$sw['name']) . '';
echo '
' . e((string)$sw['slug']) . '';
if (!$on) { echo '
已停用'; }
@@ -305,6 +309,24 @@ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) {
echo '
';
+
+ // 主题色:前台按软件换色,用短链接进不同软件一眼就能区分
+ $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 '
主题色前台进这个软件时整体配色跟着变,用来区分不同软件';
+ echo '
';
+ echo '
';
+ echo '
';
+ foreach ($presets as $pc) {
+ $on = strcasecmp($pc, $sugColor) === 0 ? ' on' : '';
+ echo '';
+ }
+ echo '
';
+ echo '
';
+ echo '
';
+ echo '
';
if ($esw) {
echo '
';
diff --git a/lib/bootstrap.php b/lib/bootstrap.php
index eabe69d..2a713f5 100644
--- a/lib/bootstrap.php
+++ b/lib/bootstrap.php
@@ -8,7 +8,7 @@ if (!defined('FB_ROOT')) {
define('FB_ROOT', dirname(__DIR__));
// 静态资源版本号:改 assets/ 下的 css/js 后把这个数字 +1,
// 让浏览器与 CDN 缓存失效。全站只此一处,别再往页面里写死数字。
- if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 11); }
+ if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 13); }
}
/* ------------------------------------------------------------------
diff --git a/lib/db.php b/lib/db.php
index 5d371d3..34dac71 100644
--- a/lib/db.php
+++ b/lib/db.php
@@ -305,6 +305,14 @@ SQL
);
$pdo->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_st_key ON statuses(key_name)');
+ /* ---- 软件增加主题色(用于前台按软件区分视觉)---- */
+ $swCols = $pdo->query('PRAGMA table_info(software)')->fetchAll();
+ $swNames = [];
+ foreach ($swCols as $c2) { if (isset($c2['name'])) { $swNames[(string)$c2['name']] = true; } }
+ if (!isset($swNames['color'])) {
+ $pdo->exec("ALTER TABLE software ADD COLUMN color TEXT NOT NULL DEFAULT ''");
+ }
+
/* ---- threads 增加两列(老 category 文本列保留,不删)---- */
$cols = $pdo->query('PRAGMA table_info(threads)')->fetchAll();
$names = [];
diff --git a/lib/layout.php b/lib/layout.php
index a84df27..2c1bcab 100644
--- a/lib/layout.php
+++ b/lib/layout.php
@@ -13,12 +13,23 @@ function fb_layout_head($title, array $opt = []) {
$site = (string)fb_cfg('site_name');
$robots = isset($opt['robots']) ? (string)$opt['robots'] : 'index,follow';
$full = $title === '' ? $site : ($title . ' · ' . $site);
+
+ /* ---- 按软件换主题色 ----
+ * 通过 $opt['software'] 传入软件行;全站主题只靠 --brand / --brand-600 /
+ * --brand-soft 三个变量,覆写它们即可,不用为每个软件写一套 CSS。
+ * 没传软件(如后台、安装页)就用主题默认色,保持中性。 */
+ $swColors = null;
+ if (!empty($opt['software'])) {
+ list($cMain, $cDark, $cSoft) = fb_software_colors($opt['software']);
+ if ($cMain !== '') { $swColors = [$cMain, $cDark, $cSoft]; }
+ }
+ $themeColor = $swColors ? $swColors[0] : '#0057B8';
?>
-
+
@@ -28,6 +39,14 @@ function fb_layout_head($title, array $opt = []) {
+
+
+
+
diff --git a/lib/oss.php b/lib/oss.php
index 967293c..bcfe8ed 100644
--- a/lib/oss.php
+++ b/lib/oss.php
@@ -136,9 +136,54 @@ function fb_oss_metadata_token() {
return trim((string)$body);
}
+/**
+ * 自动探测本 ECS 实例绑定的 RAM 角色名。
+ *
+ * 元数据接口 GET /latest/meta-data/ram/security-credentials/(末尾带斜杠)
+ * 返回的是「角色名列表」,每行一个。绝大多数实例只绑一个角色。
+ * 所以用户完全不需要知道角色叫什么 —— 我们去问机器就行。
+ *
+ * @return array [bool $ok, string $role|string $err]
+ */
+function fb_oss_detect_instance_role() {
+ $url = fb_oss_metadata_base() . '/latest/meta-data/ram/security-credentials/';
+ $headers = [];
+ $tok = fb_oss_metadata_token();
+ if ($tok !== '') { $headers[] = 'X-aliyun-ECS-Metadata-Token: ' . $tok; }
+
+ list($st, , $body) = fb_http('GET', $url, [
+ 'timeout' => 3, 'noproxy' => true, 'headers' => $headers,
+ ]);
+ if ($st < 200 || $st >= 300) {
+ return [false, '读不到 ECS 元数据(HTTP ' . $st . ')。不在 ECS 上,或元数据服务被禁用。'];
+ }
+ $txt = trim((string)$body);
+ if ($txt === '') {
+ return [false, '这台 ECS 实例没有绑定任何 RAM 角色。请在阿里云控制台给实例绑定一个角色,再回来点「自动探测」。'];
+ }
+ // 可能是多行(多角色)或 JSON 数组,两种都兼容
+ $names = [];
+ $j = json_decode($txt, true);
+ if (is_array($j)) {
+ foreach ($j as $v) { if (is_string($v) && trim($v) !== '') { $names[] = trim($v); } }
+ } else {
+ foreach (preg_split('/\r?\n/', $txt) as $line) {
+ $line = trim($line);
+ if ($line !== '') { $names[] = $line; }
+ }
+ }
+ if (!$names) { return [false, '元数据返回内容无法识别为角色名。']; }
+ return [true, $names[0]]; // 多角色时取第一个,后台会提示可手改
+}
+
/** 从 ECS 元数据读取实例 RAM 角色凭证 @return array [bool, array|string] */
function fb_oss_instance_creds() {
$role = trim((string)fb_oss_cfg('instance_role', ''));
+ // 没填就自动探测一次,免得用户还要去控制台查角色名
+ if ($role === '') {
+ list($ok, $r) = fb_oss_detect_instance_role();
+ if ($ok) { $role = (string)$r; }
+ }
if ($role === '') { return [false, '未配置 oss.instance_role']; }
$url = fb_oss_metadata_base() . '/latest/meta-data/ram/security-credentials/' . rawurlencode($role);
diff --git a/lib/util.php b/lib/util.php
index 874149e..0b2b34e 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -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',
];
}
diff --git a/new.php b/new.php
index 14568f9..9bf9189 100644
--- a/new.php
+++ b/new.php
@@ -109,7 +109,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
-fb_layout_head('我要反馈', ['robots' => 'noindex,follow']);
+fb_layout_head('我要反馈', ['robots' => 'noindex,follow', 'software' => $sw]);
?>
diff --git a/thread.php b/thread.php
index 51697ae..afee292 100644
--- a/thread.php
+++ b/thread.php
@@ -149,7 +149,8 @@ $thread = $st->fetch();
// 附件(主题级 + 各回复)
$attMap = fb_attach_for_thread($pdo, $id);
-fb_layout_head(fb_excerpt($thread['title'], 50));
+$thSwForColor = !empty($thread['software_id']) ? fb_software((int)$thread['software_id']) : null;
+fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]);
?>