Files
FeedLite/lib/layout.php
T
laoyang 8db38a834d 下拉框改回软件主题色;修 color-mix 与变量配合失效的问题
用户:「下拉框的颜色就用那个软件的主题颜色,不行吗?现在怎么还是都是灰的呀?
我感觉用主题颜色就行,挺好看的。」

上一版我判断错了:看到同步日历主题色 #ff3f44 算出的 --brand-soft 是 #ffe8e9(粉),
就自己决定换成中性灰,理由写的是"工具控件不该跟主题色"。
这是替用户做审美决定,而且用户要的恰恰是「跟着软件变颜色」——
这正是这套系统区分不同软件的核心手段。已改回主题色系。

改了什么:
- 下拉项 hover  → --brand-tint-1(主题色与白混 91%)
- 下拉项选中    → --brand-tint-2(混 80%,比 hover 深一档)
- 选中项 hover  → --brand-tint-3(混 72%,再深一点)
- 按钮打开态:浅主题底 + 主题边框 + 主题文字
- 箭头打开时转 180 并染成主题色

关键技术问题:
最初想用 CSS 的 color-mix(in srgb, var(--brand) 9%, #fff) 动态算浅色。
实测在 Chrome 里虽然 CSS.supports 返回 true、规则也匹配上了,
但计算值却是 oklab(0 0 0 / 0)(完全透明)—— 变量在 color-mix 里解析失败。
改成在 PHP 里用 fb_color_tint() 把值算好,
以 --brand-tint-1/2/3 三个 CSS 变量输出。
好处:不依赖浏览器版本,也没有运行时开销。
默认(无当前软件的页面,如后台)在 :root 里给了蓝色的一套兜底值。

验证(真实页面 + 真实鼠标事件):
  feedlite      #0057b8 → 选中 rgb(184,208,235) 浅蓝
  synccalendar  #ff3f44 → 选中 rgb(255,201,203) 浅红
  simple_password #a556f7 → 选中 rgb(230,208,253) 浅紫
  simplenav     #3571fb → 选中 rgb(198,215,254) 亮蓝
  真实 mousemove 悬停:背景 rgb(255,238,238),:hover 匹配为 true
  选中项 rgb(255,201,203) 明显深于 hover rgb(255,238,238),两者可区分

踩坑记录:中途用 JS 加 is-active 类来测 hover 只能触发 .is-active,
必须发真实 mouseMoved 事件才能验证 :hover,否则会误判成"样式失效"。
2026-11-04 19:31:55 +08:00

312 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 页面外壳(头部 / 底部)
*/
if (!defined('FB_ROOT')) { http_response_code(403); exit('forbidden'); }
/**
* 输出页面头部
* @param string $title 页面标题
* @param array $opt ['robots' => 'noindex', 'desc' => '...']
*/
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="<?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">
<link rel="icon" type="image/x-icon" href="assets/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
<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
/* 按软件覆写主题色。
* --brand-tint-1 / --brand-tint-2 是给下拉项用的两档浅色:
* tint-1(与白混 91%)→ hover
* tint-2(与白混 80%)→ 选中项,比 hover 更深一档,一眼能分开
* 在 PHP 里算好再输出,而不是 CSS 的 color-mix():
* 实测 color-mix 配 var(--brand) 在 Chrome 里解析成透明,规则匹配上却不生效。 */
$tint1 = fb_color_tint($swColors[0], 0.91);
$tint2 = fb_color_tint($swColors[0], 0.80);
$tint3 = fb_color_tint($swColors[0], 0.72); // 选中项 hover 时再深一点
?>
<style>:root{
--brand:<?php echo e($swColors[0]); ?>;
--brand-600:<?php echo e($swColors[1]); ?>;
--brand-soft:<?php echo e($swColors[2]); ?>;
--brand-tint-1:<?php echo e($tint1); ?>;
--brand-tint-2:<?php echo e($tint2); ?>;
--brand-tint-3:<?php echo e($tint3); ?>;
}</style>
<?php endif; ?>
</head>
<body>
<header class="topbar">
<div class="wrap">
<?php
/* 顶栏品牌位:当前在哪个软件下,就显示「图标 + XX 反馈系统」。
* 这是页面唯一的主标题 —— 多软件共用一套反馈系统,
* 用户要一眼看清「我现在是在给哪个软件的反馈系统提问题」。
* 内容区不再重复一遍同样的文字,只保留「图标 + 软件名」。
*
* 字号刻意比「选择软件」那个标签大一号左右(17px),
* 不再压过内容区标题 —— 之前给到 26px 反而比下面的字还小,
* 视觉上主次颠倒。 */
$curSw = isset($opt['software']) && is_array($opt['software']) ? $opt['software'] : null;
$swList = fb_softwares(true);
/* ---- 后台模式 ----
* 用户 2026-11-03 问:「管理后台还需要『我要反馈』吗?还需要右上角三横线吗?
* 还需要下拉选软件吗?这些是不是都不需要了?」
* ⇒ 都不需要,全去掉。理由:
* ① 「我要反馈」是给访客用的入口,管理员不该从后台跳去前台表单;
* ② 后台是**跨软件**视图(待审核/全部问题本身就带软件筛选器),
* 再放一个切软件的菜单是重复的,而且切了也不会改变后台视图;
* ③ 「问题列表」依赖 ?sw= 当前软件上下文,后台没有这个上下文,
* 点了只会跳到前台某个软件,属于误导。
* 后台保留的只有:站点标识(回首页)+ 管理后台 + 退出登录。 */
$isAdminBar = !empty($opt['admin']);
?>
<?php if ($curSw): ?>
<?php
/* 顶栏标题用该软件的主题色(用户 2026-11-03 要求):
* 每个软件都配了颜色,标题仍用统一黑色等于白配。
* --swc 传给 CSS,让 .sw-badge-brand 的文字取这个颜色。 */
list($curSwColor) = fb_software_colors($curSw);
if ($curSwColor === '') { $curSwColor = '#0057B8'; }
?>
<?php /* 🔴 点标题回到**当前软件**的列表,不是回首页。
用户 2026-11-03:「点完之后应该还是进入这个反馈系统,而不是进到首页,
首页其实没有太大作用 —— 它只是演示/说明用的。
正常人用的时候都是每个软件独立使用,要让用户觉得这套反馈系统
就是专门为这个软件做的。」
⇒ 在某个软件的反馈系统里,标题 / 「问题列表」/「我要反馈」
全都只跟这个软件有关,跟其他软件无关。
想切别的软件,只有右上角三横线那一个入口。 */ ?>
<?php
/* 软件的一句话简介(software.note)放在标题正下方,灰色小字。
* 用户 2026-11-03:「把「优秀的鸿蒙 CalDAV 同步客户端」放在最上面
* 那个「同步日历反馈系统」下面那个位置,用个灰色的。」
* 这样内容区就不用再重复一遍标题和简介了。 */
$swNoteTop = !empty($curSw['note']) ? trim((string)$curSw['note']) : '';
/* 图标要单独拿出来放在左边,才能与「标题 + 简介」两行**整体**垂直居中。
* 🔴 用户 2026-11-03:「图标应该在标题和简介的正中间…现在图标和上面的标题
* 是对齐的、在图标的正中间…把图标往下放一放,让标题和它下面的简介合起来
* 和图标高度一样,两个对齐上下。」
* ⇒ 结构必须是「图标 | 文字列」两栏,所以这里不用 fb_software_badge()
* (那个函数把图标和名字包在同一个 .sw-badge 里,天然是单行)。
* 改用 fb_software_icon()/icon_key 自己拼图标,名字单独放右侧。 */
$bIcon = fb_software_icon($curSw);
$bKey = trim((string)(isset($curSw['icon_key']) ? $curSw['icon_key'] : ''));
$bImg = $bKey !== '' ? fb_icon_url($bKey, 64) : '';
$bTitle = fb_software_site_title($curSw);
?>
<a class="brand-mini" href="<?php echo e(fb_software_list_url($curSw)); ?>"
style="--swc:<?php echo e($curSwColor); ?>"
title="<?php echo e($bTitle); ?>">
<span class="brand-ic" aria-hidden="true">
<?php if ($bImg !== ''): ?>
<img src="<?php echo e($bImg); ?>" alt="" width="30" height="30" loading="lazy">
<?php else: ?>
<span class="brand-ic-ch"><?php echo e($bIcon); ?></span>
<?php endif; ?>
</span>
<span class="brand-txt">
<span class="brand-title"><?php echo e($bTitle); ?></span>
<?php if ($swNoteTop !== ''): ?>
<span class="brand-mini-note"><?php echo e($swNoteTop); ?></span>
<?php endif; ?>
</span>
</a>
<?php else: ?>
<a class="brand-mini" href="./" title="<?php echo e($site); ?>">
<span class="logo-mark" role="img" aria-label="轻反馈图标"></span>
<span class="brand-text"><?php echo e($site); ?></span>
</a>
<?php endif; ?>
<div class="spacer"></div>
<?php
/* 🔴 后台模式:这三样全都不渲染。
* 用户 2026-11-03 问「后台还需要我要反馈吗?还需要三横线吗?还需要选软件吗?」
* ⇒ 都不需要,理由写在上面的 $isAdminBar 注释里。
*
* 后台顶栏只留「回前台看看」一个小按钮 —— 管理员有时需要确认
* 前台实际显示效果,这是唯一有意义的出口。
* 「管理后台」本身不标(已经在后台了,且下面的页头有大标题+退出登录),
* 避免一排没用的标记占位置。 */
if ($isAdminBar):
?>
<?php if ($curSw): ?>
<a class="navlink" href="<?php echo e(fb_software_list_url($curSw)); ?>">去前台看看</a>
<?php else: ?>
<a class="navlink" href="index.php">去前台看看</a>
<?php endif; ?>
<?php else: ?>
<?php
/* 「问题列表」只在进入某个软件之后才有意义 ——
* 首页现在是介绍页,没有"全部问题"可回。 */
if ($curSw):
?>
<a class="navlink hide-sm" href="?sw=<?php echo e(urlencode((string)$curSw['slug'])); ?>">问题列表</a>
<?php endif; ?>
<?php if (fb_admin_logged_in()): ?><a class="navlink hide-sm" href="admin.php">管理后台</a><?php endif; ?>
<?php /* ⚠️ 这里**不能**加 hide-sm:列表页那个「我要反馈」大按钮已删除,
「我要反馈」现在全站只剩顶栏这一个入口。
如果手机上把它隐藏掉,手机端就没有提交入口了。 */ ?>
<a class="btn-mini" href="new.php<?php echo $curSw ? '?sw=' . e(urlencode((string)$curSw['slug'])) : ''; ?>">我要反馈</a>
<?php endif; /* isAdminBar */ ?>
<?php if (!$isAdminBar && count($swList) > 1): ?>
<?php /* 右上角三横线:点开就是一个小下拉菜单。
不用整页平铺「选择软件:」,也不必做成侧边栏。 */ ?>
<button type="button" class="sw-toggle" id="swToggle"
aria-label="切换软件" aria-expanded="false" aria-controls="swPanel">
<span></span><span></span><span></span>
</button>
<?php /* 极简下拉:贴在按钮下方、右对齐,内容多高就多高。
参考家书 FamilyBook 顶栏那个小浮窗的做法。 */ ?>
<div class="sw-panel" id="swPanel" hidden>
<div class="sw-panel-list">
<?php foreach ($swList as $s): ?>
<?php
$on = $curSw && (int)$s['id'] === (int)$curSw['id'];
$href = fb_qs(['sw' => (string)$s['slug'], 'cat' => null, 'page' => null]);
if ($href === '') { $href = '?sw=' . urlencode((string)$s['slug']); }
?>
<a class="sw-item<?php echo $on ? ' on' : ''; ?>" href="<?php echo e($href); ?>">
<?php echo fb_software_badge($s, 'sm'); ?>
<?php if ($on): ?><span class="sw-item-cur">当前</span><?php endif; ?>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</header>
<main class="wrap">
<?php
}
/** 输出页面底部 */
function fb_layout_foot() {
// 页脚两件事:① 常驻「管理后台」入口(省得每次手敲 admin.php);② 可关闭的 FeedLite 版权句。
// 版权句里的 FeedLite 指向开源仓库,地址可后台改;关掉开关则整句不渲染,只留管理后台链接。
$showCredit = (bool)fb_cfg('footer.show_credit', true);
$creditUrl = trim((string)fb_cfg('footer.credit_url', ''));
if ($creditUrl === '') { $creditUrl = fb_credit_url_default(); }
// 只放行 http/https:防止把 javascript:/data: 之类写进页脚链接(后台设置项,同样要防)
if (!preg_match('~^https?://~i', $creditUrl)) { $creditUrl = fb_credit_url_default(); }
?>
<footer class="foot">
<div class="wrap">
<p class="muted">本站仅保存您提交的昵称、邮箱与内容,用于问题跟进;页面不会公开展示邮箱与手机号。</p>
<p class="muted foot-line">
<?php if ($showCredit): ?><span><?php echo e((string)fb_cfg('site_name', '轻反馈')); ?> · 由 <a href="<?php echo e($creditUrl); ?>" target="_blank" rel="noopener">FeedLite</a> 驱动</span><?php endif; ?>
<?php if ($showCredit): ?><span class="dot">·</span><?php endif; ?>
<a href="admin.php">管理后台</a>
</p>
</div>
</footer>
<script src="assets/app.js?v=<?php echo (int)FB_ASSET_V; ?>"></script>
</body>
</html><?php
}
/** 渲染提示条 */
function fb_flash_box($msg, $type = 'error') {
if ($msg === '' || $msg === null) { return; }
echo '<div class="flash ' . e($type) . '">' . e($msg) . '</div>';
}
/**
* 渲染附件上传控件(图片 / 视频;上传经服务器中转落地,local/OSS 前端同一路径)
* 需置于 <form> 内部:会输出一个 name="attachments" 的隐藏字段,
* 由 assets/app.js 在文件上传成功后回填 JSON 描述。
*
* @param array $existing 校验失败时已上传的附件描述(避免用户重复上传)
*/
function fb_uploaders(array $existing = []) {
$kinds = [];
if (fb_upload_kind_on('image')) { $kinds[] = 'image'; }
if (fb_upload_kind_on('video')) { $kinds[] = 'video'; }
if (!$kinds) { return; }
$init = [];
foreach ($existing as $it) {
$kind = isset($it['kind']) ? (string)$it['kind'] : 'image';
$key = isset($it['key']) ? (string)$it['key'] : '';
if ($key === '') { continue; }
$init[] = [
'kind' => $kind,
'key' => $key,
'size' => isset($it['size']) ? (int)$it['size'] : 0,
'name' => isset($it['name']) ? (string)$it['name'] : '',
'mime' => isset($it['mime']) ? (string)$it['mime'] : '',
'w' => isset($it['width']) ? (int)$it['width'] : 0,
'h' => isset($it['height']) ? (int)$it['height'] : 0,
'url' => fb_attach_url($key),
'thumb' => $kind === 'image' ? fb_attach_thumb_url($key, 360) : '',
];
}
?>
<div class="uploader-box"
data-uploader-root
data-csrf="<?php echo e(fb_csrf_token()); ?>"
data-sign-url="upload.php"
data-init="<?php echo e(json_encode($init, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ?>">
<?php foreach ($kinds as $kind):
$isImg = ($kind === 'image');
$maxN = (int)fb_upload_cfg($kind . '_max_per_post', 1);
$maxB = (int)fb_upload_cfg($kind . '_max_bytes', 0);
$accept = implode(',', (array)fb_upload_cfg($kind . '_mimes', []));
$label = $isImg ? '图片' : '视频';
// 0 表示不限制,文案别写成「最多 0 张」
$cntTxt = $maxN > 0 ? ('最多 ' . $maxN . ($isImg ? ' 张' : ' 个')) : '数量不限';
$sizeTxt = $maxB > 0
? ('单' . ($isImg ? '张' : '个') . '不超过 ' . fb_size_h($maxB))
: '大小不限';
$desc = $cntTxt . ',' . $sizeTxt . ($isImg ? ';大图会自动压缩后上传' : '');
?>
<div class="uploader" data-uploader data-kind="<?php echo e($kind); ?>"
data-max-count="<?php echo $maxN; ?>"
data-max-bytes="<?php echo $maxB; ?>"
data-max-side="<?php echo (int)fb_upload_cfg('image_max_side', 1600); ?>"
data-accept="<?php echo e($accept); ?>"
data-label="<?php echo e($label); ?>">
<div class="up-head">
<span class="up-title"><?php echo e($label); ?> <i class="hint"><?php echo e($desc); ?></i></span>
<button type="button" class="btn up-pick">选择<?php echo e($label); ?></button>
<input type="file" class="up-file" accept="<?php echo e($accept); ?>"
<?php echo $isImg ? 'multiple' : ''; ?> tabindex="-1">
</div>
<div class="up-list"></div>
</div>
<?php endforeach; ?>
<input type="hidden" name="attachments" class="att-field" value="">
</div>
<?php
}