Files
FeedLite/lib/layout.php
T
laoyang 26d00ea981 后台顶栏去掉「我要反馈」「三横线选软件」「问题列表」
用户问:「管理后台还需要我要反馈这个按钮吗?还需要右上角那三个横线的设置按钮吗?
还需要拉下来后选择不同的软件吗?这些是不是都不需要了呀?」

确认都不需要,而且原来确实都在:
后台调 fb_layout_head() 时没传 software,走「无软件」分支,
结果顶栏照样渲染了问题列表 / 我要反馈 / 三横线,三横线里还列着
「轻反馈、同步日历、密码生成器、反馈测试」四个软件 —— 在后台点它毫无意义。

理由:
① 「我要反馈」是给访客的入口,管理员不该从后台跳去前台表单;
② 后台是跨软件视图(待审核/全部问题本身就带软件筛选器),
   再放切软件菜单是重复的,切了也不会改变后台视图;
③ 「问题列表」依赖 ?sw= 当前软件上下文,后台没有这个上下文,
   点了只会跳到前台某个软件,属于误导。

改法:fb_layout_head() 新增 [admin] 开关,admin.php 两处
(管理登录、管理后台主界面)都传 admin=true。
后台顶栏只留:站点标识(回首页)+「去前台看看」。
唯一的后台出口是「去前台看看」,管理员偶尔需要核对前台实际效果。

验证:
  后台顶栏链接只剩 brand-mini(href=./) 和 navlink(href=index.php, 去前台看看);
  sw-toggle / sw-panel 关键字均已不存在。
  前台顶栏三样仍在(问题列表 / 我要反馈 / 三横线),未被波及。
  6 个路由全 200。
2026-11-03 21:11:51 +08:00

299 lines
15 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 /* 按软件覆写主题色:只改这三个变量,其它样式自动跟着变 */ ?>
<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">
<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
}