软件徽标带图标+主题色;标题用软件色;删冗余欢迎语;抽屉改竖排覆盖

用户四点要求:
1. 待审核标题前的软件名要带图标
2. 每个软件配了颜色,徽标与标题都应显示该颜色
3. 「欢迎在这里反馈使用问题、提出功能建议」这类话没意义,全部删掉
4. 右上角三横线抽屉:不要宽屏横排/窄屏竖排两套,
   统一竖排并把下面内容盖住,简单一点

改动:
- 新增 fb_software_chip():软件徽标带图标 + 该软件主题色
  (原来一律固定蓝色 #eef2f8/#2b4a74,等于白配了颜色)
  配 fb_color_soft()/fb_color_line() 由主色推导同色系浅底与描边
  替换 admin.php 两处、thread.php 一处(原来是 badge 套 badge,双重边框)
- 顶栏标题与内容区标识都用软件主题色(PHP 内联 --swc 变量)
- index.php 不再渲染 site_desc(配置项保留,后台仍可编辑)
- 抽屉改为统一竖排 absolute 浮层 + 遮罩盖住正文,删掉宽屏横排分支
  打开时锁背景滚动,点遮罩/Esc/再点按钮都能关
  .topbar 作为定位参照(sticky 即可)

顺手修:fb_hex_to_rgb 的三位简写展开前要先校验字符,
否则 zzz 这类非法值可能被误展开(本次测试用例选错才发现校验不够严)

验证:3 个软件 chip 的图标/名字/主色/浅底/描边全部正确,
无图标退回字符、XSS 转义通过、颜色工具函数 6 项通过;
CDP 真实浏览器在 390px 与 1280px 两种宽度下各 13 项断言全过
(绝对定位、在顶栏下方、铺满宽度、条目纵排同x、y递增、
层级高于正文、遮罩同步显隐、可关);
三个软件标题色分别 #b4553c / #6a4c93 / #0057b8;全部路由 200。
This commit is contained in:
2026-11-03 09:20:14 +08:00
parent 314a1cf2a5
commit 28d28ad51c
7 changed files with 168 additions and 38 deletions
+2 -2
View File
@@ -802,7 +802,7 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
$dSw = !empty($t['software_id']) ? fb_software((int)$t['software_id']) : null;
$dCat = !empty($t['category_id']) ? fb_category((int)$t['category_id']) : null;
?>
<?php if ($dSw): ?><span class="badge sw"><?php echo e((string)$dSw['name']); ?></span><?php endif; ?>
<?php if ($dSw): ?><?php echo fb_software_chip($dSw); ?><?php endif; ?>
<?php if ($dCat): ?><span class="badge cat"><?php echo e((string)$dCat['name']); ?></span><?php endif; ?>
<span class="badge st st-<?php echo e($t['status']); ?>"><?php echo e(fb_status_label($t['status'])); ?></span>
<span class="item-title"><?php echo e($t['title']); ?></span>
@@ -1451,7 +1451,7 @@ function fb_admin_pending_row(array $t) {
<div class="prow-main">
<?php $cSw = !empty($t['software_id']) ? fb_software((int)$t['software_id']) : null; ?>
<div class="prow-top">
<?php if ($cSw): ?><span class="badge sw"><?php echo e((string)$cSw['name']); ?></span><?php endif; ?>
<?php if ($cSw): ?><?php echo fb_software_chip($cSw); ?><?php endif; ?>
<?php if (!empty($t['category_id'])): ?>
<span class="badge cat"><?php echo e(fb_category_label((int)$t['category_id'])); ?></span>
<?php endif; ?>
+19 -1
View File
@@ -808,15 +808,20 @@
function bindSoftwarePanel() {
var btn = document.getElementById('swToggle');
var panel = document.getElementById('swPanel');
var mask = document.getElementById('swMask');
if (!btn || !panel) { return; }
function open() {
panel.hidden = false;
if (mask) { mask.hidden = false; }
btn.setAttribute('aria-expanded', 'true');
document.body.style.overflow = 'hidden'; // 抽屉开着时锁住背景滚动
}
function close() {
panel.hidden = true;
if (mask) { mask.hidden = true; }
btn.setAttribute('aria-expanded', 'false');
document.body.style.overflow = '';
}
function toggle() {
if (panel.hidden) { open(); } else { close(); }
@@ -827,7 +832,15 @@
toggle();
});
// 点面板内部不算「点外部」
// 点遮罩关闭(遮罩就是「下面的内容」,点了就是点外部)
if (mask) {
mask.addEventListener('click', function (e) {
e.stopPropagation();
close();
});
}
// 点面板内部不算点外部
panel.addEventListener('click', function (e) { e.stopPropagation(); });
document.addEventListener('click', function () {
@@ -837,6 +850,11 @@
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !panel.hidden) { close(); btn.focus(); }
});
// 转到别的页面时把滚动锁解掉,避免返回后页面不能滚
window.addEventListener('pagehide', function () {
document.body.style.overflow = '';
});
}
/* ---------- 邮件测试发送(后台设置页) ---------- */
+46 -18
View File
@@ -31,6 +31,7 @@ p{margin:8px 0}
backdrop-filter:saturate(180%) blur(14px);
-webkit-backdrop-filter:saturate(180%) blur(14px);
border-bottom:1px solid var(--line);
/* 软件抽屉是 absolute + top:100%,需要它来做定位参照,否则会跑到页面最上面 */
}
.topbar .wrap{display:flex; align-items:center; gap:14px; height:66px}
.topbar .brand-mini{display:flex; align-items:center; gap:10px; font-weight:700; font-size:17px; color:var(--fg); min-width:0}
@@ -64,34 +65,46 @@ p{margin:8px 0}
.sw-toggle[aria-expanded="true"] span:nth-child(2){opacity:0}
.sw-toggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}
/* ---------- 软件抽屉:一律竖排浮层,覆盖在正文之上 ----------
* 用户 2026-11-03:「都保持统一,点了之后下拉出来一个条一个条竖着排列,
* 把下面的内容全部盖住」。
* 早期实现是宽屏横排、窄屏竖排两套,结果手机上会跟正文撞在一起;
* 现在只有一种形态:绝对定位的浮卡,overlay 盖住页面内容。 */
.sw-panel{
border-top:1px solid var(--line);
position:absolute; left:0; right:0; top:100%;
z-index:60;
background:var(--card);
box-shadow:0 8px 20px -12px rgba(20,30,50,.18);
border-top:1px solid var(--line);
border-bottom:1px solid var(--line);
box-shadow:0 18px 40px -12px rgba(20,30,50,.28);
max-height:calc(100vh - 66px);
overflow:auto;
}
.sw-panel .wrap{padding-top:14px; padding-bottom:16px}
.sw-panel .wrap{padding-top:14px; padding-bottom:14px}
.sw-panel-hd{
font-size:13.5px; font-weight:600; color:var(--muted);
margin-bottom:10px; letter-spacing:.2px;
font-size:13px; font-weight:600; color:var(--muted);
margin-bottom:8px; letter-spacing:.2px;
}
/* 永远竖排:一条一行,不再有宽屏横排分支 */
.sw-panel-list{display:flex; flex-direction:column; gap:2px}
.sw-item{
display:flex; align-items:center; gap:10px;
padding:9px 10px; border-radius:10px; color:var(--fg);
padding:10px 12px; border-radius:10px; color:var(--fg);
border:1px solid transparent;
}
.sw-item:hover{background:var(--bg, #f6f8fb); text-decoration:none; color:var(--fg)}
.sw-item.on{background:var(--brand-soft)}
.sw-item.on .sw-badge-nm{color:var(--brand); font-weight:700}
.sw-item.on{background:var(--brand-soft); border-color:var(--brand-soft)}
.sw-item.on .sw-badge-nm{font-weight:700}
.sw-item-cur{
margin-left:auto; font-size:11.5px; font-weight:600; color:var(--brand);
background:var(--card); border:1px solid var(--brand);
padding:1px 8px; border-radius:99px;
}
@media (min-width:561px){
.sw-panel-list{flex-direction:row; flex-wrap:wrap; gap:8px}
.sw-item{padding:7px 12px; border:1px solid var(--line)}
.sw-item.on{border-color:var(--brand)}
.sw-item-cur{margin-left:4px}
/* 抽屉打开时给正文加一层遮罩,点它也能关 —— 视觉上更明确「盖住了」 */
.sw-mask{
position:fixed; inset:0; z-index:55;
background:rgba(15,23,35,.18);
}
.topbar .brand-text{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.topbar .logo-mark{
@@ -191,10 +204,12 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
.sw-badge-sm .sw-badge-nm{font-size:14px}
.sw-badge-md .sw-badge-nm{font-size:16px; font-weight:600}
/* 内容区:只是软件标识,字号刻意小于顶栏标题(17px),
* 避免「上面的字比下面的小」这种主次颠倒。 */
.sw-badge-lg .sw-badge-nm{font-size:15px; font-weight:600}
/* 顶栏主标题:比「选择软件」标签(13.5px)大两号 = 17px,且大于内容区的 15px */
.sw-badge-brand .sw-badge-nm{font-size:17px; font-weight:700; letter-spacing:.2px}
* 避免「上面的字比下面的小」这种主次颠倒。颜色同样跟随软件主题色。 */
.sw-badge-lg .sw-badge-nm{font-size:15px; font-weight:600; color:var(--swc, var(--fg))}
/* 顶栏主标题:比「选择软件」标签(13.5px)大两号 = 17px,且大于内容区的 15px。
* 颜色跟随软件主题色(--swc 由 PHP 内联写到 .brand-mini 上),
* 让「同步日历」「密码生成器」这些标题一眼可辨。 */
.sw-badge-brand .sw-badge-nm{font-size:17px; font-weight:700; letter-spacing:.2px; color:var(--swc, var(--fg))}
/* 页头副行:图标 + 软件名 */
.page-sw-id{margin:0; display:flex; align-items:center}
/* 提交页顶部的软件上下文条:同样无背景色,只留一行轻分隔 */
@@ -422,7 +437,20 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
.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}
/* 软件徽标:每个软件用自己的主题色 + 图标,方便一眼区分是哪个软件。
* 颜色由 PHP 内联写入(--swc / --swc-soft),这里只管形态。 */
.badge.sw{
display:inline-flex; align-items:center; gap:5px;
background:var(--swc-soft, #eef2f8); color:var(--swc, #2b4a74);
border:1px solid var(--swc-line, #cfdcee); font-weight:600;
padding:1px 9px 1px 6px;
}
.badge.sw .badge-sw-ic{
display:inline-flex; align-items:center; justify-content:center;
width:15px; height:15px; flex:none; line-height:1;
}
.badge.sw .badge-sw-ic img{width:15px; height:15px; display:block; object-fit:contain}
.badge.sw .badge-sw-nm{white-space:nowrap}
.badge.st-wontfix{background:#f6eef0; color:#a13c53}
/* ---------- 分页 ---------- */
+9 -3
View File
@@ -87,17 +87,23 @@ fb_layout_head($sw ? fb_software_site_title($sw) : $headTitle, ['software' => $s
<div class="page-head">
<div>
<?php
$siteDesc = (string)fb_cfg('site_desc');
/* 站点描述(site_desc)不再显示在页面上 —— 用户 2026-11-03 明确说
* 「欢迎在这里反馈使用问题、提出功能建议」这类话没有意义,全部删掉。
* 配置项保留,后台仍可编辑,只是前台不渲染。 */
$swNote = ($sw && !empty($sw['note'])) ? (string)$sw['note'] : '';
?>
<?php if ($sw): ?>
<?php /* 内容区只放「图标 + 软件名」,不再重复「XX 反馈系统」——
那个标题已经放到最上面的顶栏里了,两处重复反而啰嗦。 */ ?>
<div class="page-sw-id"><?php echo fb_software_badge($sw, 'lg'); ?></div>
<?php
/* 内容区标识也用软件主题色 */
list($pageSwColor) = fb_software_colors($sw);
if ($pageSwColor === '') { $pageSwColor = '#0057B8'; }
?>
<div class="page-sw-id" style="--swc:<?php echo e($pageSwColor); ?>"><?php echo fb_software_badge($sw, 'lg'); ?></div>
<?php else: ?>
<h1>全部问题</h1>
<?php endif; ?>
<?php if ($siteDesc !== ''): ?><p class="muted"><?php echo e($siteDesc); ?></p><?php endif; ?>
<?php if ($swNote !== ''): ?><p class="muted small" style="margin:2px 0 0"><?php echo e($swNote); ?></p><?php endif; ?>
</div>
<a class="btn primary" href="new.php<?php echo $sw ? '?sw=' . e(urlencode((string)$sw['slug'])) : ''; ?>">我要反馈</a>
+13 -1
View File
@@ -64,7 +64,16 @@ function fb_layout_head($title, array $opt = []) {
$swList = fb_softwares(true);
?>
<?php if ($curSw): ?>
<a class="brand-mini" href="./" title="<?php echo e(fb_software_site_title($curSw)); ?>">
<?php
/* 顶栏标题用该软件的主题色(用户 2026-11-03 要求):
* 每个软件都配了颜色,标题仍用统一黑色等于白配。
* --swc 传给 CSS,让 .sw-badge-brand 的文字取这个颜色。 */
list($curSwColor) = fb_software_colors($curSw);
if ($curSwColor === '') { $curSwColor = '#0057B8'; }
?>
<a class="brand-mini" href="./"
style="--swc:<?php echo e($curSwColor); ?>"
title="<?php echo e(fb_software_site_title($curSw)); ?>">
<?php echo fb_software_badge($curSw, 'brand', fb_software_site_title($curSw)); ?>
</a>
<?php else: ?>
@@ -91,6 +100,9 @@ function fb_layout_head($title, array $opt = []) {
</div>
<?php if (count($swList) > 1): ?>
<?php /* 遮罩:抽屉打开时盖住正文,点击即关。
用户要求「把下面的内容全部盖住」,加一层浅遮罩最直观。 */ ?>
<div class="sw-mask" id="swMask" hidden></div>
<div class="sw-panel" id="swPanel" hidden>
<div class="wrap">
<div class="sw-panel-hd">选择软件</div>
+78 -12
View File
@@ -335,19 +335,21 @@ function fb_software_badge($software, $size = 'md', $label = '') {
$icon = fb_software_icon($software);
$name = $label !== '' ? (string)$label : (string)$software['name'];
/* 尺寸档位(以「选择软件」标签的 13.5px 为基准,逐档放大):
* sm 18px 图标 / 14px 字 软件选择器里的小标签
* md 26px 图标 / 19px 字 「选择软件」抽屉里的条目
* lg 40px 图标 / 22px 字 内容区「图标 + 软件名」
* brand 32px 图标 / 17px 字 顶栏主标题
/* 尺寸档位(以「选择软件」标签的 13.5px 为基准):
* sm 18px 图标 / 14px 字 小标签
* md 24px 图标 / 16px 字 抽屉里的条目
* lg 24px 图标 / 15px 字 内容区「图标 + 软件名」
* brand 30px 图标 / 17px 字 顶栏主标题
*
* 🔴 用户明确指出:顶栏标题的字号「比下面还小」是错的,但给到 26px 又太大。
* 折中标准 = 比「选择软件」那个 13.5px 标签大两号 ≈ 17px。
* 图标同比缩到 32px,别让图标压过文字。 */
$px = ['sm' => 18, 'md' => 26, 'lg' => 40, 'brand' => 32];
$fs = ['sm' => 14, 'md' => 19, 'lg' => 22, 'brand' => 17];
$box = isset($px[$size]) ? $px[$size] : 26;
$fnt = isset($fs[$size]) ? $fs[$size] : 19;
* 🔴 两条硬约束(都踩过):
* 1. 顶栏标题 = 比「选择软件」的 13.5px 大两号 ≈ 17px,不要再放大到 26px;
* 2. **顶栏必须比内容区大**。之前顶栏 26px、内容区 30px,
* 主次直接颠倒,看着就是「上面的字比下面的小」。
* 现在 17 > 15,层级正确;内容区只是标识,不该抢标题的戏。 */
$px = ['sm' => 18, 'md' => 24, 'lg' => 24, 'brand' => 30];
$fs = ['sm' => 14, 'md' => 16, 'lg' => 15, 'brand' => 17];
$box = isset($px[$size]) ? $px[$size] : 24;
$fnt = isset($fs[$size]) ? $fs[$size] : 16;
/* 有上传的图标图片就用图片(管理员自己传的 logo),没有才用字符。
* 两者外框尺寸一致,换用图片不会让布局跳动。 */
@@ -386,6 +388,70 @@ function fb_software_site_title($software) {
return $name . '反馈系统';
}
/**
* 软件徽标(后台/前台的小标签)—— 带图标,且用该软件自己的主题色。
*
* 为什么不复用 .badge 的固定蓝色:每个软件都配了主题色,
* 徽标却一律蓝色,等于把「颜色区分软件」这条设计浪费掉了。
*
* @param array $sw 软件行
* @param bool $icon 是否显示图标(默认显示)
*/
function fb_software_chip($sw, $withIcon = true) {
if (!is_array($sw) || empty($sw['name'])) { return ''; }
$name = (string)$sw['name'];
list($c) = fb_software_colors($sw);
if ($c === '') { $c = '#0057B8'; }
$soft = fb_color_soft($c); // 同色系浅底
$line = fb_color_line($c); // 同色系描边
$icHtml = '';
if ($withIcon) {
$imgKey = isset($sw['icon_key']) ? trim((string)$sw['icon_key']) : '';
$imgUrl = $imgKey !== '' ? fb_icon_url($imgKey, 30) : '';
if ($imgUrl !== '') {
$icHtml = '<span class="badge-sw-ic"><img src="' . e($imgUrl) . '" alt=""></span>';
} else {
$icHtml = '<span class="badge-sw-ic" aria-hidden="true">' . e(fb_software_icon($sw)) . '</span>';
}
}
return '<span class="badge sw"'
. ' style="--swc:' . e($c) . ';--swc-soft:' . e($soft) . ';--swc-line:' . e($line) . '">'
. $icHtml
. '<span class="badge-sw-nm">' . e($name) . '</span>'
. '</span>';
}
/** 把主色调淡成浅底(与主色同色系,避免灰化) */
function fb_color_soft($hex) {
$rgb = fb_hex_to_rgb($hex);
if (!$rgb) { return '#eef2f8'; }
return sprintf('#%02x%02x%02x',
(int)round($rgb[0] + (255 - $rgb[0]) * 0.90),
(int)round($rgb[1] + (255 - $rgb[1]) * 0.90),
(int)round($rgb[2] + (255 - $rgb[2]) * 0.90));
}
/** 把主色调成描边色(略深于浅底) */
function fb_color_line($hex) {
$rgb = fb_hex_to_rgb($hex);
if (!$rgb) { return '#cfdcee'; }
return sprintf('#%02x%02x%02x',
(int)round($rgb[0] + (255 - $rgb[0]) * 0.72),
(int)round($rgb[1] + (255 - $rgb[1]) * 0.72),
(int)round($rgb[2] + (255 - $rgb[2]) * 0.72));
}
/** #rrggbb -> [r,g,b],非法返回 null */
function fb_hex_to_rgb($hex) {
$h = ltrim((string)$hex, '#');
if (strlen($h) === 3) { $h = $h[0].$h[0].$h[1].$h[1].$h[2].$h[2]; }
if (!preg_match('/^[0-9a-fA-F]{6}$/', $h)) { return null; }
return [hexdec(substr($h, 0, 2)), hexdec(substr($h, 2, 2)), hexdec(substr($h, 4, 2))];
}
/** 某软件下的分类;不传软件则返回空数组 */
function fb_categories($softwareId = null, $onlyEnabled = true) {
if ($softwareId === null) { return []; }
+1 -1
View File
@@ -161,7 +161,7 @@ fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]);
$thSw = !empty($thread['software_id']) ? fb_software((int)$thread['software_id']) : null;
$thCat = !empty($thread['category_id']) ? fb_category((int)$thread['category_id']) : null;
?>
<?php if ($thSw): ?><span class="badge sw"><?php echo fb_software_badge($thSw, 'sm'); ?></span><?php endif; ?>
<?php if ($thSw): ?><?php echo fb_software_chip($thSw); ?><?php endif; ?>
<?php if ($thCat): ?><span class="badge cat"><?php echo e((string)$thCat['name']); ?></span><?php endif; ?>
<span class="badge st st-<?php echo e($thread['status']); ?>"><?php echo e(fb_status_label($thread['status'])); ?></span>
<h1 class="subject-title"><?php echo e($thread['title']); ?></h1>