209 lines
7.4 KiB
PHP
209 lines
7.4 KiB
PHP
<?php
|
||
/**
|
||
* layout.php —— 页面公共输出组件
|
||
* 约定:每个页面在引入本文件前需定义全局 $P(资源前缀,根目录为 '',func/、admin/ 子目录为 '../')。
|
||
*/
|
||
require_once __DIR__ . '/db.php';
|
||
|
||
if (!isset($P)) {
|
||
$P = '';
|
||
}
|
||
|
||
/** HTML 转义 */
|
||
function he($s): string
|
||
{
|
||
return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
/** 当前 logo 的站点相对路径(后台配置优先,空则默认) */
|
||
function site_logo_rel(): string
|
||
{
|
||
$logo = setting_get('site_logo', '');
|
||
return $logo !== '' ? $logo : 'assets/img/logo.svg';
|
||
}
|
||
|
||
/** 输出页面 <head>:标题/favicon/主题防闪烁/公共样式 */
|
||
function layout_head(string $title = ''): void
|
||
{
|
||
global $P;
|
||
$site = setting_get('site_name', '知识导航站');
|
||
$logo = site_logo_rel();
|
||
$fullTitle = ($title !== '' ? $title . ' - ' : '') . $site;
|
||
$prefix = $P;
|
||
echo '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">';
|
||
echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
|
||
echo '<meta name="renderer" content="webkit">';
|
||
// 主题防闪烁:在任何样式加载前应用
|
||
echo '<script>(function(){var t;try{t=localStorage.getItem("hp-theme")}catch(e){}';
|
||
echo 'if(!t){t=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light"}';
|
||
echo 'document.documentElement.setAttribute("data-theme",t);})();</script>';
|
||
echo '<title>' . he($fullTitle) . '</title>';
|
||
echo '<link rel="icon" href="' . he($prefix . $logo) . '">';
|
||
echo '<link rel="stylesheet" href="' . he($prefix) . 'assets/css/common.css">';
|
||
echo '</head><body>' . "\n";
|
||
}
|
||
|
||
/** 昼夜切换浮动按钮 */
|
||
function layout_theme_fab(): void
|
||
{
|
||
echo '<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式">';
|
||
echo '<span class="js-theme-icon"></span></button>' . "\n";
|
||
}
|
||
|
||
/** 站内 logo 图片(内容区使用,同 favicon) */
|
||
function layout_logo_img(string $cls = 'logo', string $alt = 'logo'): void
|
||
{
|
||
global $P;
|
||
echo '<img class="' . he($cls) . '" src="' . he($P . site_logo_rel()) . '" alt="' . he($alt) . '">';
|
||
}
|
||
|
||
/** 全站页脚 */
|
||
function layout_footer(): void
|
||
{
|
||
global $P;
|
||
$site = setting_get('site_name', '知识导航站');
|
||
$icp = setting_get('icp_no', '');
|
||
$gongan = setting_get('gongan_no', '');
|
||
$gonganLink = setting_get('gongan_link', '');
|
||
$copyright = setting_get('copyright', '');
|
||
$extra = setting_get('footer_text', '');
|
||
echo '<footer class="footer"><div class="wrap footer-inner">';
|
||
if ($icp !== '') {
|
||
echo '<div><a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" style="color:inherit">' . he($icp) . '</a></div>';
|
||
}
|
||
if ($gongan !== '') {
|
||
if ($gonganLink !== '') {
|
||
echo '<div><a href="' . he($gonganLink) . '" target="_blank" rel="noopener noreferrer" style="color:inherit">' . he($gongan) . '</a></div>';
|
||
} else {
|
||
echo '<div>' . he($gongan) . '</div>';
|
||
}
|
||
}
|
||
if ($copyright !== '') {
|
||
echo '<div>' . he(str_replace('{site_name}', $site, $copyright)) . '</div>';
|
||
}
|
||
if ($extra !== '') {
|
||
echo '<div>' . he($extra) . '</div>';
|
||
}
|
||
echo '<div><a class="admin-link" href="' . he($P) . 'admin/login.php">后台管理</a></div>';
|
||
echo '</div></footer>' . "\n";
|
||
}
|
||
|
||
/** 读取首页展示的名言(quota.txt 随机一行) */
|
||
function home_quote(): string
|
||
{
|
||
$file = DATA_DIR . '/quota.txt';
|
||
$lines = [];
|
||
if (is_file($file)) {
|
||
$raw = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||
if (is_array($raw)) {
|
||
foreach ($raw as $ln) {
|
||
$ln = trim($ln);
|
||
if ($ln !== '' && strpos($ln, '#') !== 0) {
|
||
$lines[] = $ln;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (!$lines) {
|
||
return '知识就是力量。';
|
||
}
|
||
return $lines[array_rand($lines)];
|
||
}
|
||
|
||
/** 图标值是否属于图片(dataURL / http / 常见图片扩展 / 相对路径) */
|
||
function icon_is_image(string $raw): bool
|
||
{
|
||
$v = trim($raw);
|
||
if ($v === '') {
|
||
return false;
|
||
}
|
||
if (preg_match('/^data:image\//i', $v) || preg_match('#^https?://#i', $v)) {
|
||
return true;
|
||
}
|
||
if (preg_match('/\.(png|jpe?g|gif|svg|webp)(\?|#|$)/i', $v)) {
|
||
return true;
|
||
}
|
||
return $v[0] === '/' || strpos($v, './') === 0 || strpos($v, '../') === 0;
|
||
}
|
||
|
||
/** 渲染图标:图片返回 <img>,否则返回转义文本(供前台/后台预览复用) */
|
||
function site_icon(?string $raw, string $alt = 'icon'): string
|
||
{
|
||
$v = trim((string)$raw);
|
||
if ($v === '') {
|
||
return '';
|
||
}
|
||
if (icon_is_image($v)) {
|
||
return '<img class="icon-img" src="' . he($v) . '" alt="' . he($alt) . '" loading="lazy">';
|
||
}
|
||
return he($v);
|
||
}
|
||
|
||
/** 默认 CDN IPv4 段库(每行:CIDR + 空格 + 厂商名;# 开头为注释,由后台可覆盖) */
|
||
function default_cdn_ranges(): string
|
||
{
|
||
return implode("\n", [
|
||
'# 内置默认:Cloudflare 官方 IPv4 段(可在后台“CDN IP 段库”补充其它厂商)',
|
||
'103.21.244.0/22 Cloudflare',
|
||
'103.22.200.0/22 Cloudflare',
|
||
'103.31.4.0/22 Cloudflare',
|
||
'104.16.0.0/13 Cloudflare',
|
||
'104.24.0.0/14 Cloudflare',
|
||
'108.162.192.0/18 Cloudflare',
|
||
'131.0.72.0/22 Cloudflare',
|
||
'141.101.64.0/18 Cloudflare',
|
||
'162.158.0.0/15 Cloudflare',
|
||
'172.64.0.0/13 Cloudflare',
|
||
'173.245.48.0/20 Cloudflare',
|
||
'188.114.96.0/20 Cloudflare',
|
||
'190.93.240.0/20 Cloudflare',
|
||
'197.234.240.0/22 Cloudflare',
|
||
'198.41.128.0/17 Cloudflare',
|
||
]);
|
||
}
|
||
|
||
/** 当前生效的 CDN 段库文本(后台配置优先,为空回退默认内置列表) */
|
||
function cdn_ranges_text(): string
|
||
{
|
||
$v = setting_get('cdn_ranges', '');
|
||
return $v !== '' ? $v : default_cdn_ranges();
|
||
}
|
||
|
||
/** 默认搜索引擎列表(后台未配置时的兜底) */
|
||
function default_search_engines(): array
|
||
{
|
||
return [
|
||
['key' => 'bing', 'name' => 'Bing', 'icon' => 'B', 'url' => 'https://www.bing.com/search?q={kw}'],
|
||
['key' => 'baidu', 'name' => '百度', 'icon' => '度', 'url' => 'https://www.baidu.com/s?wd={kw}'],
|
||
['key' => 'github', 'name' => 'GitHub', 'icon' => 'G', 'url' => 'https://github.com/search?q={kw}'],
|
||
['key' => 'google', 'name' => 'Google', 'icon' => 'G', 'url' => 'https://www.google.com/search?q={kw}'],
|
||
];
|
||
}
|
||
|
||
/** 读取当前生效的搜索引擎列表(优先后台配置,无效或为空回退默认) */
|
||
function search_engines(): array
|
||
{
|
||
$raw = setting_get('search_engines', '');
|
||
if ($raw === '') {
|
||
return default_search_engines();
|
||
}
|
||
$arr = json_decode($raw, true);
|
||
if (!is_array($arr) || count($arr) === 0) {
|
||
return default_search_engines();
|
||
}
|
||
$out = [];
|
||
foreach ($arr as $i => $e) {
|
||
if (!is_array($e)) continue;
|
||
$name = trim((string)($e['name'] ?? ''));
|
||
$url = trim((string)($e['url'] ?? ''));
|
||
$icon = trim((string)($e['icon'] ?? ''));
|
||
if ($name === '' || $url === '' || strpos($url, '{kw}') === false) continue;
|
||
$key = trim((string)($e['key'] ?? ''));
|
||
if ($key === '') {
|
||
$key = 'e' . ((int)$i + 1);
|
||
}
|
||
$out[] = ['key' => $key, 'name' => $name, 'icon' => ($icon !== '' ? $icon : 'S'), 'url' => $url];
|
||
}
|
||
return $out !== [] ? $out : default_search_engines();
|
||
}
|