新增 install.php:全新部署时自动接管,用户只需上传文件、打开站点即进入
安装向导。安装页自己做三件事:
- 建 db/ 与 uploads/{posters,tickets,photos,avatars}/,权限调到 775
- 逐目录真写临时文件实测可写,不靠 is_writable 猜
- 建库、建管理员、写 db/.installed 锁、自删本文件
配套改动:
- config.php 把安装检查前置于建库之前,未安装一律 302 到 install.php。
原实现无条件调用 initDB(),导致「打开首页」就等于建库并种下 admin/123456。
- lib/db.php 的 initDB(\$seedDefaultAdmin = true);安装向导传 false,
管理员由用户自己设,绝不种弱口令。新增 isInstalled()。
- login.php / nav.php 接上 siteName(),站点名称不再只存不用。
- lib/settings.php 新增 siteName(),缺省回退「电影日志」。
PHP 8.5 兼容:
- 删掉 fetch_movie.php 与 lib/storage/http.php 的 curl_close()。
8.5 起该函数 deprecated,调用会往输出插一行警告,把 JSON 响应污染成
「警告 + JSON」,前端 JSON.parse 失败 → 电影名联想功能完全失灵。
- config.php 的 display_errors 由 1 改为 0(生产环境不应向浏览器吐错误),
错误仍写日志;需要调试时设 MOVIELOG_DEBUG=1 打开。
线上验证(movie.yangyq.net,PHP 8.5.12):
未安装时 / 、/login.php 均 302 → install.php;
安装页 200、体检 10/10 通过、自动建好 5 个目录;
安装后 install.php 404、管理员登录成功、首页 200 无报错;
联想接口带会话请求「流浪地球/星际穿越/霸王别姬」均返回纯 JSON(首字符 {)。
506 lines
21 KiB
PHP
506 lines
21 KiB
PHP
<?php
|
||
// ============================================
|
||
// 文件: install.php
|
||
// 说明: 安装向导。全新部署时访问站点,会被自动引到这里;
|
||
// 填完站点名与管理员账号即完成初始化,然后进入系统。
|
||
//
|
||
// ⚠️ 为什么本文件**不 require config.php**:
|
||
// config.php 末尾无条件调用 initDB(),而 initDB() 会建库、建表、
|
||
// 并在 admins 为空时插入默认管理员 admin/123456。
|
||
// 如果安装页也走 config.php,那么「打开安装页」这个动作本身就把库建出来了,
|
||
// 「是否已安装」永远判不出来,安装向导永远进不去。
|
||
// (同一类坑在 FeedLite 上踩过:e2e 因此全 FAIL、源码目录凭空多出 db 文件。)
|
||
// 所以本文件自己定义常量、自己按需建库,绝不触发 config.php 的副作用。
|
||
//
|
||
// 安装完成后:
|
||
// 1) 写 db/.installed 锁文件(该目录已被 .gitignore 排除,不会进仓库);
|
||
// 2) 尝试自删本文件;删不掉(例如站点目录只读)也不影响安全,
|
||
// 因为锁文件已存在,再次访问一律 404。
|
||
// ============================================
|
||
|
||
// ---------- 最小引导(不加载 config.php)----------
|
||
define('ROOT_DIR', __DIR__);
|
||
define('DB_PATH', ROOT_DIR . '/db/movies.db');
|
||
define('INSTALL_LOCK', ROOT_DIR . '/db/.installed');
|
||
|
||
define('UPLOAD_DIR', ROOT_DIR . '/uploads/');
|
||
define('POSTER_DIR', UPLOAD_DIR . 'posters/');
|
||
define('TICKET_DIR', UPLOAD_DIR . 'tickets/');
|
||
define('PHOTO_DIR', UPLOAD_DIR . 'photos/');
|
||
define('AVATAR_DIR', UPLOAD_DIR . 'avatars/');
|
||
|
||
// 安装期把错误显示出来,方便诊断;但出口统一包 try/catch,不把堆栈抛给用户。
|
||
error_reporting(E_ALL);
|
||
ini_set('display_errors', '0');
|
||
ini_set('log_errors', '1');
|
||
|
||
// ---------- 工具 ----------
|
||
function esc($s) {
|
||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||
}
|
||
|
||
// 字符数(不依赖 mbstring:缺 mbstring 时用正则数 UTF-8 码点,避免直接 Fatal)
|
||
function charLen($s) {
|
||
if (function_exists('mb_strlen')) {
|
||
return mb_strlen($s, 'UTF-8');
|
||
}
|
||
if (preg_match_all('/./us', $s, $m) !== false) {
|
||
return count($m[0]);
|
||
}
|
||
return strlen($s);
|
||
}
|
||
|
||
// 写一条 settings(不依赖 SQLite 版本:老库不一定支持 UPSERT 语法)
|
||
// 显式 INSERT,冲突时改 UPDATE,两条语句等效且兼容 3.20 以下。
|
||
function putSetting(PDO $db, $name, $value) {
|
||
$stmt = $db->prepare("SELECT COUNT(*) FROM settings WHERE name = ?");
|
||
$stmt->execute([$name]);
|
||
if ((int)$stmt->fetchColumn() > 0) {
|
||
$up = $db->prepare("UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?");
|
||
return $up->execute([$value, $name]);
|
||
}
|
||
$ins = $db->prepare("INSERT INTO settings (name, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)");
|
||
return $ins->execute([$name, $value]);
|
||
}
|
||
|
||
// 已安装?——只认锁文件。锁在 = 装过了。
|
||
// 注意:不拿「库文件存在」当判据 —— 库可能是空壳(建库失败、被人为清空),
|
||
// 那种情况应该允许重新安装,而不是把用户挡在门外。
|
||
function alreadyInstalled() {
|
||
return is_file(INSTALL_LOCK);
|
||
}
|
||
|
||
function abort404() {
|
||
http_response_code(404);
|
||
header('Content-Type: text/html; charset=utf-8');
|
||
echo '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">'
|
||
. '<title>404 Not Found</title></head><body>'
|
||
. '<h1>404 Not Found</h1><p>页面不存在。</p></body></html>';
|
||
exit;
|
||
}
|
||
|
||
// ---------- 环境体检 ----------
|
||
// 每项:['name'=>显示名, 'ok'=>bool, 'need'=>是否硬性要求, 'hint'=>不通过时的补救说明]
|
||
function runChecks() {
|
||
$checks = [];
|
||
|
||
// PHP 版本:代码用了 PASSWORD_DEFAULT、箭头函数等,7.4 起可跑
|
||
$checks[] = [
|
||
'name' => 'PHP 版本 ≥ 7.4',
|
||
'ok' => version_compare(PHP_VERSION, '7.4.0', '>='),
|
||
'need' => true,
|
||
'hint' => '当前 ' . PHP_VERSION . ',请升级 PHP。',
|
||
];
|
||
|
||
// 扩展:pdo_sqlite 是硬要求;openssl 用于设置项加密(缺了仍能跑,只是云密钥无法加密);
|
||
// fileinfo 用于本机存储模式的图片内容校验;mbstring 处理中文更稳。
|
||
$exts = [
|
||
'pdo_sqlite' => ['PDO SQLite 驱动', true, '数据库必需。'],
|
||
'openssl' => ['OpenSSL', true, '用于加密保存云存储密钥。'],
|
||
'fileinfo' => ['Fileinfo', false, '本机存储模式下按内容校验图片类型;缺失则退化为扩展名判断。'],
|
||
'mbstring' => ['Mbstring', false, '中文截断/编码处理;缺失一般不影响主流程。'],
|
||
];
|
||
foreach ($exts as $ext => $cfg) {
|
||
$checks[] = [
|
||
'name' => '扩展 ' . $cfg[0] . '(' . $ext . ')',
|
||
'ok' => extension_loaded($ext),
|
||
'need' => $cfg[1],
|
||
'hint' => $cfg[2],
|
||
];
|
||
}
|
||
|
||
// 目录可写:db/(建库+密钥)、uploads/ 四个子目录(存图)
|
||
$dirs = [
|
||
ROOT_DIR . '/db' => '数据库目录 db/',
|
||
POSTER_DIR => '海报目录 uploads/posters/',
|
||
TICKET_DIR => '票根目录 uploads/tickets/',
|
||
PHOTO_DIR => '照片目录 uploads/photos/',
|
||
AVATAR_DIR => '头像目录 uploads/avatars/',
|
||
];
|
||
foreach ($dirs as $dir => $label) {
|
||
$exists = is_dir($dir);
|
||
$writable = $exists && is_writable($dir);
|
||
$checks[] = [
|
||
'name' => $label . ' 可写',
|
||
'ok' => $writable,
|
||
'need' => true,
|
||
'hint' => $exists
|
||
? '目录存在但不可写,请给 Web 用户写权限(属主 www,或 chmod 775)。'
|
||
: '目录不存在且无法自动创建,请手动建好并授权。',
|
||
];
|
||
}
|
||
|
||
return $checks;
|
||
}
|
||
|
||
// 体检是否放行(硬性项全过)
|
||
function checksPass(array $checks) {
|
||
foreach ($checks as $c) {
|
||
if ($c['need'] && !$c['ok']) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 尽力创建运行所需目录
|
||
function ensureDirs() {
|
||
$dirs = [ROOT_DIR . '/db', POSTER_DIR, TICKET_DIR, PHOTO_DIR, AVATAR_DIR];
|
||
$failed = [];
|
||
foreach ($dirs as $d) {
|
||
if (!is_dir($d)) {
|
||
if (!@mkdir($d, 0755, true) && !is_dir($d)) {
|
||
$failed[] = $d;
|
||
}
|
||
}
|
||
}
|
||
return $failed;
|
||
}
|
||
|
||
// ---------- 入口 ----------
|
||
if (alreadyInstalled()) {
|
||
abort404(); // 装过了就当它不存在
|
||
}
|
||
|
||
$createdDirs = ensureDirs();
|
||
$checks = runChecks();
|
||
$canInstall = checksPass($checks);
|
||
|
||
$errors = []; // 全局错误
|
||
$fieldErrors = []; // 字段级错误
|
||
$old = ['site_name' => '', 'username' => ''];
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$old['site_name'] = trim((string)($_POST['site_name'] ?? ''));
|
||
$old['username'] = trim((string)($_POST['username'] ?? ''));
|
||
$password = (string)($_POST['password'] ?? '');
|
||
$password2 = (string)($_POST['password2'] ?? '');
|
||
|
||
if (!$canInstall) {
|
||
$errors[] = '环境检查未通过,请先解决下面标红的问题再安装。';
|
||
}
|
||
|
||
// 站点名:存进 settings,用于页面标题(可留空,留空用默认「电影日志」)
|
||
if ($old['site_name'] !== '' && charLen($old['site_name']) > 40) {
|
||
$fieldErrors['site_name'] = '站点名称最多 40 个字。';
|
||
}
|
||
|
||
// 管理员账号
|
||
if ($old['username'] === '') {
|
||
$fieldErrors['username'] = '请填写管理员账号。';
|
||
} elseif (!preg_match('/^[A-Za-z0-9_\x{4e00}-\x{9fa5}]{2,20}$/u', $old['username'])) {
|
||
$fieldErrors['username'] = '账号 2–20 位,可用中文、字母、数字、下划线。';
|
||
} elseif (in_array(strtolower($old['username']), ['admin', 'administrator', 'root'], true)
|
||
&& $password === '123456') {
|
||
// 明确拦住「admin + 123456」这个最容易被扫的弱口令组合
|
||
$fieldErrors['username'] = '请勿使用 admin/123456 这类默认弱口令,换一个账号或改密码。';
|
||
}
|
||
|
||
// 密码
|
||
if ($password === '') {
|
||
$fieldErrors['password'] = '请设置密码。';
|
||
} elseif (charLen($password) < 6) {
|
||
$fieldErrors['password'] = '密码至少 6 位。';
|
||
} elseif ($password !== $password2) {
|
||
$fieldErrors['password2'] = '两次输入的密码不一致。';
|
||
}
|
||
|
||
if (!$errors && !$fieldErrors) {
|
||
try {
|
||
// 建表:require lib/db.php 只定义函数,不执行任何副作用。
|
||
// initDB(false) 表示「不要种默认管理员」——管理员由本页按用户输入创建。
|
||
require_once ROOT_DIR . '/lib/db.php';
|
||
initDB(false);
|
||
|
||
$db = getDB();
|
||
$db->beginTransaction();
|
||
|
||
// 双保险:库若已有管理员(例如重复安装到旧库),不再新增,直接报错让人处理
|
||
$cnt = (int)$db->query("SELECT COUNT(*) FROM admins")->fetchColumn();
|
||
if ($cnt > 0) {
|
||
$db->rollBack();
|
||
throw new RuntimeException('这个数据库里已经有管理员账号了。为避免覆盖,安装已中止。'
|
||
. '若确实要重装,请先备份并删除 db/movies.db。');
|
||
}
|
||
|
||
$hashed = password_hash($password, PASSWORD_DEFAULT);
|
||
$stmt = $db->prepare("INSERT INTO admins (username, password) VALUES (?, ?)");
|
||
$stmt->execute([$old['username'], $hashed]);
|
||
$adminId = (int)$db->lastInsertId();
|
||
|
||
// 站点名写入 settings(表由 initDB 建好)
|
||
if ($old['site_name'] !== '') {
|
||
putSetting($db, 'site_name', $old['site_name']);
|
||
}
|
||
|
||
// 存储方式默认 local(与 settings.php 的缺省一致)
|
||
putSetting($db, 'storage_type', 'local');
|
||
|
||
// 安装完成标记
|
||
putSetting($db, 'installed_at', date('Y-m-d H:i:s'));
|
||
|
||
$db->commit();
|
||
|
||
// 写锁文件(先写,再尝试自删;顺序反了会留下「已自删但没上锁」的窗口)
|
||
$lockOk = @file_put_contents(
|
||
INSTALL_LOCK,
|
||
"installed_at=" . date('c') . "\nadmin=" . $old['username'] . "\n"
|
||
) !== false;
|
||
if ($lockOk) @chmod(INSTALL_LOCK, 0600);
|
||
|
||
// 自删:成功则以后连文件都不存在;失败也无妨,上面的锁已生效
|
||
$selfDeleted = @unlink(__FILE__);
|
||
|
||
// 直接登入,省得用户再输一次
|
||
if (session_status() === PHP_SESSION_NONE) {
|
||
@session_start();
|
||
}
|
||
$_SESSION['admin_id'] = $adminId;
|
||
$_SESSION['admin_username'] = $old['username'];
|
||
$_SESSION['admin_theme'] = '';
|
||
|
||
// 安装成功页
|
||
renderDone($old['site_name'], $old['username'], $lockOk, $selfDeleted);
|
||
exit;
|
||
|
||
} catch (Throwable $e) {
|
||
$errors[] = '安装失败:' . $e->getMessage();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------- 渲染 ----------
|
||
function pageHead($title) {
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN" data-theme="ticket">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="robots" content="noindex,nofollow">
|
||
<title><?= esc($title) ?></title>
|
||
<link rel="stylesheet" href="css/style.css">
|
||
<style>
|
||
/* 安装页自包含样式:直接复用登录页的深色玻璃语言,不依赖 themes.css
|
||
(全新部署时主题 CSS 可能还没生效,安装页必须自身完整可看)。 */
|
||
:root { --primary: #e67e22; --primary-rgb: 230,126,34; --primary-light: #f39c12; }
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0; min-height: 100vh; padding: 32px 16px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
color: #fff;
|
||
}
|
||
.wrap { width: 100%; max-width: 560px; }
|
||
.card {
|
||
background: rgba(255,255,255,0.05);
|
||
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
border-radius: 24px; padding: 40px 36px;
|
||
box-shadow: 0 30px 80px rgba(0,0,0,0.5);
|
||
}
|
||
.logo { text-align: center; margin-bottom: 28px; }
|
||
.logo .icon { font-size: 48px; display: block; margin-bottom: 6px; }
|
||
.logo h1 { font-size: 26px; margin: 0; letter-spacing: 2px; font-weight: 700; }
|
||
.logo p { color: rgba(255,255,255,0.5); font-size: 14px; margin: 6px 0 0; }
|
||
h2.section {
|
||
font-size: 15px; font-weight: 600; color: rgba(255,255,255,0.85);
|
||
margin: 28px 0 12px; padding-bottom: 8px;
|
||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||
}
|
||
h2.section:first-of-type { margin-top: 0; }
|
||
.field { margin-bottom: 18px; }
|
||
.field label { display: block; font-size: 14px; font-weight: 500; color: rgba(255,255,255,0.7); margin-bottom: 6px; }
|
||
.field input {
|
||
width: 100%; padding: 13px 16px;
|
||
background: rgba(255,255,255,0.08);
|
||
border: 1px solid rgba(255,255,255,0.12);
|
||
border-radius: 12px; color: #fff; font-size: 16px;
|
||
transition: all .25s ease; outline: none;
|
||
}
|
||
.field input:focus {
|
||
border-color: var(--primary);
|
||
background: rgba(255,255,255,0.12);
|
||
box-shadow: 0 0 0 4px rgba(var(--primary-rgb),0.15);
|
||
}
|
||
.field input::placeholder { color: rgba(255,255,255,0.3); }
|
||
.field .hint { font-size: 12.5px; color: rgba(255,255,255,0.4); margin-top: 6px; }
|
||
.field .err { font-size: 13px; color: #ff8a80; margin-top: 6px; }
|
||
.field.bad input { border-color: rgba(231,76,60,0.7); }
|
||
.btns { margin-top: 26px; }
|
||
.btn {
|
||
width: 100%; padding: 14px; border: none; border-radius: 12px;
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||
color: #fff; font-size: 17px; font-weight: 600; cursor: pointer;
|
||
transition: all .25s ease; letter-spacing: 1px;
|
||
}
|
||
.btn:hover { transform: translateY(-2px); box-shadow: 0 10px 30px rgba(var(--primary-rgb),0.3); }
|
||
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; box-shadow: none; }
|
||
.btn-ghost {
|
||
display: inline-block; text-decoration: none; text-align: center;
|
||
padding: 13px 22px; border-radius: 12px; color: #fff;
|
||
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);
|
||
font-weight: 600;
|
||
}
|
||
.btn-ghost:hover { background: rgba(255,255,255,0.16); }
|
||
.alert {
|
||
padding: 12px 16px; border-radius: 10px; font-size: 14px; margin-bottom: 18px;
|
||
background: rgba(231,76,60,0.18); border: 1px solid rgba(231,76,60,0.35); color: #ff8a80;
|
||
line-height: 1.6;
|
||
}
|
||
.checks { display: flex; flex-direction: column; gap: 2px; }
|
||
.check {
|
||
display: flex; align-items: flex-start; gap: 10px;
|
||
padding: 9px 12px; border-radius: 9px; font-size: 14px;
|
||
background: rgba(255,255,255,0.03);
|
||
}
|
||
.check .mark { flex: 0 0 auto; font-weight: 700; line-height: 1.5; }
|
||
.check.ok .mark { color: #4ade80; }
|
||
.check.warn .mark { color: #fbbf24; }
|
||
.check.bad .mark { color: #ff6b6b; }
|
||
.check .txt { flex: 1 1 auto; color: rgba(255,255,255,0.85); }
|
||
.check .sub { display: block; font-size: 12.5px; color: rgba(255,255,255,0.45); margin-top: 3px; }
|
||
.check.bad { background: rgba(231,76,60,0.12); }
|
||
.foot { text-align: center; margin-top: 22px; color: rgba(255,255,255,0.3); font-size: 12.5px; line-height: 1.7; }
|
||
.done-icon { text-align: center; font-size: 56px; margin-bottom: 8px; }
|
||
.done-title { text-align: center; font-size: 22px; font-weight: 700; margin: 0 0 8px; }
|
||
.done-sub { text-align: center; color: rgba(255,255,255,0.55); font-size: 14px; margin: 0 0 26px; }
|
||
.kv { font-size: 14px; background: rgba(255,255,255,0.04); border-radius: 12px; padding: 14px 18px; margin-bottom: 22px; }
|
||
.kv div { display: flex; gap: 10px; padding: 5px 0; }
|
||
.kv b { flex: 0 0 84px; color: rgba(255,255,255,0.55); font-weight: 500; }
|
||
.kv span { color: #fff; word-break: break-all; }
|
||
@media (max-width: 520px) {
|
||
.card { padding: 28px 20px; border-radius: 18px; }
|
||
.logo h1 { font-size: 22px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<?php
|
||
}
|
||
|
||
function pageFoot() {
|
||
?>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
<?php
|
||
}
|
||
|
||
function renderDone($siteName, $username, $lockOk, $selfDeleted) {
|
||
pageHead('安装完成 - 电影日志');
|
||
$title = $siteName !== '' ? $siteName : '电影日志';
|
||
?>
|
||
<div class="card">
|
||
<div class="done-icon">🎉</div>
|
||
<h1 class="done-title">安装完成</h1>
|
||
<p class="done-sub">已经可以开始记录观影了</p>
|
||
|
||
<div class="kv">
|
||
<div><b>站点名称</b><span><?= esc($title) ?></span></div>
|
||
<div><b>管理员</b><span><?= esc($username) ?></span></div>
|
||
<div><b>数据文件</b><span>db/movies.db</span></div>
|
||
<div><b>安全锁</b><span><?= $lockOk ? '已启用(db/.installed)' : '未写入,请检查 db/ 权限' ?></span></div>
|
||
<div><b>安装入口</b><span><?= $selfDeleted ? '已自动删除' : '未能自动删除(锁文件已拦截,可手动删)' ?></span></div>
|
||
</div>
|
||
|
||
<a class="btn-ghost" href="index.php" style="display:block;">进入系统 →</a>
|
||
|
||
<p class="foot">
|
||
接下来建议先去「系统设置」确认图片来源存储方式(默认存本机)。
|
||
</p>
|
||
</div>
|
||
<?php
|
||
pageFoot();
|
||
}
|
||
?>
|
||
<?php
|
||
// 正常渲染安装表单(走完上面的 POST 分支后仍在此处,说明安装未成功或首次进入)
|
||
pageHead('安装 - 电影日志');
|
||
?>
|
||
<div class="card">
|
||
<div class="logo">
|
||
<span class="icon">🎬</span>
|
||
<h1>电影日志</h1>
|
||
<p>首次使用,先做一点简单设置</p>
|
||
</div>
|
||
|
||
<?php if ($errors): ?>
|
||
<div class="alert"><?= implode('<br>', array_map('esc', $errors)) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<form method="POST" action="">
|
||
<h2 class="section">① 运行环境</h2>
|
||
<div class="checks">
|
||
<?php foreach ($checks as $c):
|
||
$cls = $c['ok'] ? 'ok' : ($c['need'] ? 'bad' : 'warn');
|
||
$mark = $c['ok'] ? '✓' : ($c['need'] ? '✕' : '!');
|
||
?>
|
||
<div class="check <?= $cls ?>">
|
||
<span class="mark"><?= $mark ?></span>
|
||
<span class="txt">
|
||
<?= esc($c['name']) ?>
|
||
<?php if (!$c['ok']): ?><span class="sub"><?= esc($c['hint']) ?></span><?php endif; ?>
|
||
</span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<h2 class="section">② 站点</h2>
|
||
<div class="field <?= isset($fieldErrors['site_name']) ? 'bad' : '' ?>">
|
||
<label>站点名称</label>
|
||
<input type="text" name="site_name" maxlength="40"
|
||
value="<?= esc($old['site_name']) ?>"
|
||
placeholder="电影日志(留空则用默认)">
|
||
<?php if (isset($fieldErrors['site_name'])): ?>
|
||
<div class="err"><?= esc($fieldErrors['site_name']) ?></div>
|
||
<?php else: ?>
|
||
<div class="hint">显示在登录页与导航栏,可以后面再改。</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<h2 class="section">③ 管理员账号</h2>
|
||
<div class="field <?= isset($fieldErrors['username']) ? 'bad' : '' ?>">
|
||
<label>账号</label>
|
||
<input type="text" name="username" autocomplete="username"
|
||
value="<?= esc($old['username']) ?>"
|
||
placeholder="例如 老杨">
|
||
<?php if (isset($fieldErrors['username'])): ?>
|
||
<div class="err"><?= esc($fieldErrors['username']) ?></div>
|
||
<?php else: ?>
|
||
<div class="hint">2–20 位,可用中文、字母、数字、下划线。</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="field <?= isset($fieldErrors['password']) ? 'bad' : '' ?>">
|
||
<label>密码</label>
|
||
<input type="password" name="password" autocomplete="new-password"
|
||
placeholder="至少 6 位">
|
||
<?php if (isset($fieldErrors['password'])): ?>
|
||
<div class="err"><?= esc($fieldErrors['password']) ?></div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="field <?= isset($fieldErrors['password2']) ? 'bad' : '' ?>">
|
||
<label>确认密码</label>
|
||
<input type="password" name="password2" autocomplete="new-password"
|
||
placeholder="再输一次">
|
||
<?php if (isset($fieldErrors['password2'])): ?>
|
||
<div class="err"><?= esc($fieldErrors['password2']) ?></div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="btns">
|
||
<button type="submit" class="btn" <?= $canInstall ? '' : 'disabled' ?>>
|
||
<?= $canInstall ? '开始安装' : '请先解决环境问题' ?>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
|
||
<p class="foot">
|
||
装好后本页面会自动删除,再次访问返回 404。<br>
|
||
数据库与图片都在站点目录内,迁移时记得一起带走。
|
||
</p>
|
||
</div>
|
||
<?php
|
||
pageFoot();
|