阶段 0 起步:图标与环境自检脚手架

- assets/icons 全套图标:SVG 源文件 + maskable + 12 档 PNG
- favicon.ico / apple-touch-icon.png / site.webmanifest
- index.php 环境自检页(PHP 版本 / 必需扩展 / 配置 / 数据库连接),
  详情页默认不打印连接错误原文,避免泄露数据库账号名
- config.example.php:数据库与 app_key 的配置模板(config.php 不入库)
- README 补充项目定位与六阶段路线图;.gitignore 沿用既有约定
This commit is contained in:
2026-09-29 23:48:45 +08:00
parent d8914a6dfc
commit ecafa110f2
23 changed files with 350 additions and 1 deletions
+141
View File
@@ -0,0 +1,141 @@
<?php
/**
* FamilyBook · 家书 —— 阶段 0 引导页
*
* 这一页不是首页。它的职责是:部署之后先看这里,确认
* 「PHP 版本 / 扩展 / 配置文件 / 数据库连接」四件事都通了,再往上盖功能。
* 正式首页做好之后会替换掉它。
*
* ⚠️ 线上请把 config.php 里的 debug 设为 false:
* 连接报错时,详情里可能含数据库账号名,不该公开展示。
*/
declare(strict_types=1);
define('FB_ROOT', __DIR__);
$configFile = FB_ROOT . '/config.php';
$hasConfig = is_file($configFile);
$config = $hasConfig ? require $configFile : null;
$debug = is_array($config) && !empty($config['debug']);
/**
* 静态资源带版本号(mtime + size)。
* 不带版本号的话,文件换了浏览器还在用旧缓存 —— 详见踩坑记录。
*/
function fb_asset(string $rel): string
{
$p = FB_ROOT . $rel;
if (!is_file($p)) {
return $rel;
}
return $rel . '?v=' . substr(md5(filemtime($p) . filesize($p)), 0, 8);
}
$checks = [];
// 1. PHP 版本:以 PHP 7.4 为底线(线上同时存在 7.4 与 8.1 两套)
$checks['PHP 版本'] = [
'ok' => PHP_VERSION_ID >= 70400,
'val' => PHP_VERSION . '(需 7.4 以上)',
];
// 2. 必需扩展
$needExt = ['pdo_mysql' => '数据库', 'mbstring' => '中文处理', 'openssl' => '敏感字段加密', 'json' => '配置与接口', 'gd' => '图片裁剪'];
foreach ($needExt as $ext => $why) {
$ok = extension_loaded($ext);
$checks['扩展 ' . $ext] = ['ok' => $ok, 'val' => $ok ? '已加载 · ' . $why : '缺失 · ' . $why];
}
// 3. 配置文件
$checks['配置文件 config.php'] = [
'ok' => $hasConfig,
'val' => $hasConfig ? '已就位' : '未找到(请复制 config.example.php 后填写)',
];
// 4. 数据库连接(只有配置齐全才去连)
if (is_array($config) && !empty($config['db'])) {
$d = $config['db'];
try {
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$d['host'], $d['port'], $d['name'], $d['charset']
);
$pdo = new PDO($dsn, $d['user'], $d['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$ver = $pdo->query('SELECT VERSION()')->fetchColumn();
$checks['数据库连接'] = ['ok' => true, 'val' => 'MySQL ' . $ver . ' · 库 ' . $d['name']];
} catch (Throwable $e) {
$checks['数据库连接'] = [
'ok' => false,
'val' => $debug ? $e->getMessage() : '连接失败(把 config.php 的 debug 设为 true 可看详情)',
];
}
}
$allOk = true;
foreach ($checks as $c) {
$allOk = $allOk && $c['ok'];
}
$siteName = is_array($config) && !empty($config['site_name']) ? $config['site_name'] : '家书';
?><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($siteName, ENT_QUOTES, 'UTF-8') ?> · 环境自检</title>
<link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<style>
:root { --paper:#F2E9D8; --cover:#A84A28; --cover2:#C96F45; --ink:#6E3820; --white:#FDFBF5; }
* { box-sizing:border-box; }
body { margin:0; padding:40px 20px; background:var(--paper); color:var(--ink);
font:15px/1.7 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif; }
.wrap { max-width:560px; margin:0 auto; }
.head { display:flex; align-items:center; gap:14px; margin-bottom:6px; }
.head img { width:64px; height:64px; border-radius:14px; }
h1 { font-size:22px; margin:0; font-weight:600; }
.sub { color:#8a6a52; font-size:13px; margin:0 0 24px 78px; }
ul { list-style:none; margin:0; padding:0; background:var(--white);
border-radius:12px; overflow:hidden; }
li { display:flex; align-items:baseline; gap:10px; padding:12px 16px;
border-bottom:1px solid rgba(110,56,32,.08); }
li:last-child { border-bottom:0; }
.dot { width:9px; height:9px; border-radius:50%; flex:0 0 auto; margin-top:8px; }
.ok { background:#6f9e5c; } .bad { background:#c0533f; }
.k { font-weight:600; flex:0 0 auto; }
.v { color:#7a5a42; font-size:13px; word-break:break-all; }
.banner { padding:10px 16px; border-radius:10px; margin-bottom:18px; font-size:14px; }
.pass { background:#e6f0dc; color:#3d6b1f; }
.fail { background:#f8ddda; color:#a1341f; }
</style>
</head>
<body>
<div class="wrap">
<div class="head">
<img src="<?= htmlspecialchars(fb_asset('/assets/icons/icon-192.png'), ENT_QUOTES, 'UTF-8') ?>" alt="">
<div>
<h1><?= htmlspecialchars($siteName, ENT_QUOTES, 'UTF-8') ?> · 环境自检</h1>
</div>
</div>
<p class="sub">FamilyBook —— 一本用岁月写成的家谱</p>
<div class="banner <?= $allOk ? 'pass' : 'fail' ?>">
<?= $allOk ? '全部通过,可以开始盖功能了。' : '还有未通过项,处理后请刷新这一页。' ?>
</div>
<ul>
<?php foreach ($checks as $name => $c): ?>
<li>
<span class="dot <?= $c['ok'] ? 'ok' : 'bad' ?>"></span>
<span class="k"><?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></span>
<span class="v"><?= htmlspecialchars((string)$c['val'], ENT_QUOTES, 'UTF-8') ?></span>
</li>
<?php endforeach; ?>
</ul>
</div>
</body>
</html>