## 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:
+26
-4
@@ -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>';
|
||||
|
||||
+1
-1
@@ -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); }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
+20
-1
@@ -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';
|
||||
?><!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#0057B8">
|
||||
<meta name="theme-color" content="<?php echo e($themeColor); ?>">
|
||||
<meta name="robots" content="<?php echo e($robots); ?>">
|
||||
<title><?php echo e($full); ?></title>
|
||||
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
|
||||
@@ -28,6 +39,14 @@ function fb_layout_head($title, array $opt = []) {
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon.png">
|
||||
<link rel="manifest" href="assets/site.webmanifest">
|
||||
<link rel="stylesheet" href="assets/style.css?v=<?php echo (int)FB_ASSET_V; ?>">
|
||||
<?php if ($swColors): ?>
|
||||
<?php /* 按软件覆写主题色:只改这三个变量,其它样式自动跟着变 */ ?>
|
||||
<style>:root{
|
||||
--brand:<?php echo e($swColors[0]); ?>;
|
||||
--brand-600:<?php echo e($swColors[1]); ?>;
|
||||
--brand-soft:<?php echo e($swColors[2]); ?>;
|
||||
}</style>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
|
||||
+45
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user