阶段 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:
@@ -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>
|
||||
Reference in New Issue
Block a user