prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'); $st->execute([$key, $value]); } function renumber_sorts(PDO $pdo): void { $rows = $pdo->query('SELECT id, part, sort FROM articles ORDER BY part ASC, sort ASC, id ASC')->fetchAll(); $idxByPart = []; $st = $pdo->prepare('UPDATE articles SET sort = ? WHERE id = ?'); foreach ($rows as $r) { $key = (string)$r['part']; if (!isset($idxByPart[$key])) { $idxByPart[$key] = 1; } $st->execute([$idxByPart[$key], (int)$r['id']]); $idxByPart[$key]++; } } function msg_block(string $kind, string $text): string { return '
' . he($text) . '
'; } $msgKind = ''; $msgText = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!csrf_verify()) { $msgKind = 'err'; $msgText = '安全校验失败,请刷新页面重试。'; } else { $act = (string)($_POST['act'] ?? ''); switch ($act) { case 'note': settings_set('article_note', trim((string)($_POST['note'] ?? ''))); touch_last_updated(); $msgKind = 'ok'; $msgText = '文章区首页注要已保存。'; break; case 'save': $id = max(0, (int)($_POST['id'] ?? 0)); $part = trim((string)($_POST['part'] ?? '')); $title = trim((string)($_POST['title'] ?? '')); $summary = trim((string)($_POST['summary'] ?? '')); $md = (string)($_POST['markdown'] ?? ''); $sort = max(0, (int)($_POST['sort'] ?? 0)); $pinned = (isset($_POST['pinned']) && $_POST['pinned'] === '1') ? 1 : 0; $enabled = (isset($_POST['enabled']) && $_POST['enabled'] === '1') ? 1 : 0; // 分发到库首页(文章区):board_cats 逗号分隔;未勾选=不展示到库页 $bcKept = []; foreach ((array)($_POST['board_cats'] ?? []) as $bk) { $bk = trim((string)$bk); if (in_array($bk, ['popular', 'red', 'blue'], true) && !in_array($bk, $bcKept, true)) { $bcKept[] = $bk; } } $boardCats = implode(',', $bcKept); if ($part === '' || $part === '__new__') { $part = '未分类'; } if ($title === '') { $msgKind = 'err'; $msgText = '文章标题不能为空。'; } elseif ($id > 0) { $st = $pdo->prepare('UPDATE articles SET part=?, title=?, summary=?, markdown=?, pinned=?, enabled=?, sort=?, board_cats=?, updated_at=? WHERE id=?'); $st->execute([$part, $title, $summary, $md, $pinned, $enabled, $sort, $boardCats, date('Y-m-d H:i:s'), $id]); $msgKind = 'ok'; $msgText = '文章已保存。'; } else { $st = $pdo->prepare('INSERT INTO articles (part, title, summary, markdown, pinned, enabled, sort, board_cats, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)'); $now = date('Y-m-d H:i:s'); $st->execute([$part, $title, $summary, $md, $pinned, $enabled, $sort, $boardCats, $now, $now]); $id = (int)$pdo->lastInsertId(); $msgKind = 'ok'; $msgText = '文章已发布(可继续编辑)。'; } if ($msgKind === 'ok') { renumber_sorts($pdo); touch_last_updated(); header('Location: articles.php?edit=' . $id . '&ok=1'); exit; } break; case 'del': $id = (int)($_POST['id'] ?? 0); if ($id > 0) { $pdo->prepare('DELETE FROM articles WHERE id = ?')->execute([$id]); renumber_sorts($pdo); touch_last_updated(); $msgKind = 'ok'; $msgText = '文章已删除。'; } break; case 'toggle': $id = (int)($_POST['id'] ?? 0); $field = (string)($_POST['field'] ?? ''); if ($id > 0 && ($field === 'pinned' || $field === 'enabled')) { $cur = (int)$pdo->query('SELECT ' . $field . ' FROM articles WHERE id = ' . $id)->fetchColumn(); $pdo->prepare('UPDATE articles SET ' . $field . ' = ? WHERE id = ?')->execute([$cur ? 0 : 1, $id]); $msgKind = 'ok'; $msgText = $field === 'pinned' ? '置顶状态已更新。' : '启停状态已更新。'; } break; case 'move': $id = (int)($_POST['id'] ?? 0); $dir = ((string)($_POST['dir'] ?? '') === 'down') ? 'down' : 'up'; $rows = $pdo->query('SELECT id, part, sort FROM articles ORDER BY part ASC, sort ASC, id ASC')->fetchAll(); $idx = -1; foreach ($rows as $i => $r) { if ((int)$r['id'] === $id) { $idx = $i; break; } } if ($idx >= 0) { $target = ($dir === 'down') ? $idx + 1 : $idx - 1; if ($target >= 0 && $target < count($rows) && $rows[$target]['part'] === $rows[$idx]['part']) { $tmp = $rows[$idx]; $rows[$idx] = $rows[$target]; $rows[$target] = $tmp; renumber_sorts($pdo); $msgKind = 'ok'; $msgText = '顺序已调整。'; } } break; case 'upload': // 图片上传(供编辑器 XHR 使用,返回 JSON) header('Content-Type: application/json; charset=utf-8'); $up = $_FILES['file'] ?? null; if (!$up || ($up['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { echo json_encode(['ok' => false, 'msg' => '未收到文件'], JSON_UNESCAPED_UNICODE); exit; } if (($up['size'] ?? 0) > 2 * 1024 * 1024) { echo json_encode(['ok' => false, 'msg' => '图片不能超过 2MB'], JSON_UNESCAPED_UNICODE); exit; } $extMap = ['png' => 'png', 'jpg' => 'jpg', 'jpeg' => 'jpg', 'gif' => 'gif', 'webp' => 'webp', 'svg' => 'svg']; $ext = strtolower(pathinfo((string)$up['name'], PATHINFO_EXTENSION)); if (!isset($extMap[$ext])) { echo json_encode(['ok' => false, 'msg' => '仅支持 png / jpg / gif / webp / svg'], JSON_UNESCAPED_UNICODE); exit; } $dir = DATA_DIR . '/articles/img'; if (!is_dir($dir)) { @mkdir($dir, 0777, true); } $filename = 'art_' . date('YmdHis') . '_' . bin2hex(random_bytes(4)) . '.' . $extMap[$ext]; if (!@move_uploaded_file($up['tmp_name'], $dir . '/' . $filename)) { echo json_encode(['ok' => false, 'msg' => '保存失败,请检查 data/articles/img 目录权限'], JSON_UNESCAPED_UNICODE); exit; } echo json_encode(['ok' => true, 'file' => 'data/articles/img/' . $filename], JSON_UNESCAPED_UNICODE); exit; default: $msgKind = 'err'; $msgText = '未知操作。'; } } } $savedFlag = isset($_GET['ok']); $list = $pdo->query('SELECT * FROM articles ORDER BY part ASC, sort ASC, id ASC')->fetchAll(); $noteText = setting_get('article_note', ''); $editing = false; $row = null; $editId = (int)($_GET['edit'] ?? 0); if ($editId > 0) { $st = $pdo->prepare('SELECT * FROM articles WHERE id = ?'); $st->execute([$editId]); $row = $st->fetch(); if ($row) { $editing = true; } } layout_head('文章管理'); admin_topbar('articles'); ?>

文章管理

文章库(第五大板块)的文章维护:正文为标准 Markdown,图片统一上传到 data/articles/img 目录。

保存成功,文章库前台已同步更新。
(int)$row['id'], 'part' => (string)$row['part'], 'title' => (string)$row['title'], 'summary' => (string)$row['summary'], 'md' => (string)$row['markdown'], 'sort' => (int)$row['sort'], 'pinned' => (int)$row['pinned'], 'enabled' => (int)$row['enabled'], 'board_cats' => trim((string)($row['board_cats'] ?? '')), ] : ['id' => 0, 'part' => '未分类', 'title' => '', 'summary' => '', 'md' => '', 'sort' => 0, 'pinned' => 0, 'enabled' => 1, 'board_cats' => '']; // 分区下拉选项:未分类 + 全部已有分区;编辑中的自定义分区自动补充,保证能回显选中 $curPart = trim((string)$cur['part']); if ($curPart === '') { $curPart = '未分类'; } $partOptions = ['未分类']; foreach ($list as $a2) { $p2 = trim((string)$a2['part']); if ($p2 === '' || $p2 === '未分类') continue; if (!in_array($p2, $partOptions, true)) { $partOptions[] = $p2; } } if (!in_array($curPart, $partOptions, true)) { array_unshift($partOptions, $curPart); } ?>
可直接下拉选择已有分区;需要新栏目时选「+ 新建分区…」并输入名称后回车。
上传后自动在正文末尾插入 ![说明](data/articles/img/文件.png) 形式引用;引用路径相对站点根目录,渲染时自动补全前缀。
取消 / 返回列表
文章区首页注要(顶部展示给读者的说明文字)
文章列表(共 篇) + 新增文章

还没有文章,点击右上角「新增文章」开始写作。

分区标题状态排序操作
置顶 可见隐藏

同分区内使用 ↑/↓ 调整顺序,置顶文章在列表中始终靠前。