Files
FeedLite/lib/layout.php
T
laoyang c09a9875a3 标题收到最上顶头,标签下移一行;顶栏标题回当前软件;去掉跨软件引导
用户三个问题:

一、软件名/分类/状态三个标签挤在标题前面,把标题推到右边很难看
  列表页:item-top 里 badges 在前、标题在后 → 标题被挤到右边
  详情页:同一个毛病
  改法(两处一致):
    行1 标题(顶头、独占一行)
    行2 标签(分类/状态;详情页还有软件名)
    行3 摘要 / 作者信息
  列表页新增 .item-head + .item-tags;详情页新增 .subject-head + .subject-tags。
  ⚠️ .item-top 保留 —— admin.php 有三处内部列表还在用,删了后台会塌。

二、顶栏「XX 反馈系统」点完跳首页
  用户:「点完之后应该还是进入这个反馈系统,而不是进到首页。首页没有太大作用,
        只是演示说明用的。正常使用都是每个软件独立,要让用户觉得
        这套反馈系统就是专门为这个软件做的。」
  改法:brand-mini 的 href 从 "./" 改为 fb_software_list_url()。

三、软件内部不该引导去其他软件
  用户:「在一个反馈系统里面,我要反馈、问题列表、左上角那个链接,
        全部都是这个软件相关的内容,跟其他软件没关系。」
  - 提交成功页删掉整个「也可以去其他软件反馈」区块及其 CSS
  - 删掉成功页的「返回首页」按钮(已有「看该软件其他反馈」,重复且无意义)
  - 跨软件只剩顶栏三横线菜单一个入口

验证:列表页/详情页 DOM 顺序均为 标题→标签→作者(实测 item-title 58 < item-tags 171;
subject-title 49 < subject-tags 151 < item-meta 592);
浏览器实测标题 top=282 标签 top=315(列表)、155→194(详情),标签确实在下方;
标题左边缘距卡片边 18/23px,就是卡片自身 padding,没有额外缩进;
顶栏标题 href=index.php?sw=synccalendar,不再指向根路径;全站 8 个路由 200。
2026-11-03 13:43:15 +08:00

237 lines
11 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);
?>
<?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:「点完之后应该还是进入这个反馈系统,而不是进到首页,
首页其实没有太大作用 —— 它只是演示/说明用的。
正常人用的时候都是每个软件独立使用,要让用户觉得这套反馈系统
就是专门为这个软件做的。」
⇒ 在某个软件的反馈系统里,标题 / 「问题列表」/「我要反馈」
全都只跟这个软件有关,跟其他软件无关。
想切别的软件,只有右上角三横线那一个入口。 */ ?>
<a class="brand-mini" href="<?php echo e(fb_software_list_url($curSw)); ?>"
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: ?>
<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
/* 「问题列表」只在进入某个软件之后才有意义 ——
* 首页现在是介绍页,没有"全部问题"可回。 */
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; ?>
<a class="btn-mini hide-sm" href="new.php<?php echo $curSw ? '?sw=' . e(urlencode((string)$curSw['slug'])) : ''; ?>">我要反馈</a>
<?php if (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
}