用户:「下拉框的颜色就用那个软件的主题颜色,不行吗?现在怎么还是都是灰的呀? 我感觉用主题颜色就行,挺好看的。」 上一版我判断错了:看到同步日历主题色 #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,否则会误判成"样式失效"。
312 lines
16 KiB
PHP
312 lines
16 KiB
PHP
<?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
|
||
}
|