新增文章库
This commit is contained in:
parent
90bd40ffde
commit
4e6d6e5307
@ -20,6 +20,7 @@ function admin_topbar(string $active = ''): void
|
|||||||
$links = [
|
$links = [
|
||||||
'nav' => ['nav.php', '导航内容管理'],
|
'nav' => ['nav.php', '导航内容管理'],
|
||||||
'tools' => ['tools.php', '功能区管理'],
|
'tools' => ['tools.php', '功能区管理'],
|
||||||
|
'articles' => ['articles.php', '文章管理'],
|
||||||
'content' => ['content.php', '内容与顺序'],
|
'content' => ['content.php', '内容与顺序'],
|
||||||
];
|
];
|
||||||
foreach ($links as $k => $lk) {
|
foreach ($links as $k => $lk) {
|
||||||
|
|||||||
370
admin/articles.php
Normal file
370
admin/articles.php
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* admin/articles.php —— 文章管理
|
||||||
|
* 分区 + 标题 + 摘要 + Markdown 正文的管理(SQLite 存储);
|
||||||
|
* 文章正文图片上传到 data/articles/img/,正文用站点相对路径引用。
|
||||||
|
*/
|
||||||
|
$P = '../';
|
||||||
|
require_once __DIR__ . '/_guard.php';
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
function settings_set(string $key, string $value): void
|
||||||
|
{
|
||||||
|
$st = db()->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 '<div class="' . ($kind === 'ok' ? 'ok-msg' : 'err-msg') . '">' . he($text) . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
if ($part === '') {
|
||||||
|
$part = '未分类';
|
||||||
|
}
|
||||||
|
if ($title === '') {
|
||||||
|
$msgKind = 'err';
|
||||||
|
$msgText = '文章标题不能为空。';
|
||||||
|
} elseif ($id > 0) {
|
||||||
|
$st = $pdo->prepare('UPDATE articles SET part=?, title=?, summary=?, markdown=?, pinned=?, enabled=?, sort=?, updated_at=? WHERE id=?');
|
||||||
|
$st->execute([$part, $title, $summary, $md, $pinned, $enabled, $sort, 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, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)');
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$st->execute([$part, $title, $summary, $md, $pinned, $enabled, $sort, $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');
|
||||||
|
?>
|
||||||
|
<main class="page-main">
|
||||||
|
<div class="wrap page-body">
|
||||||
|
<h2 style="margin-bottom:6px">文章管理</h2>
|
||||||
|
<p class="tip">文章库(第五大板块)的文章维护:正文为标准 Markdown,图片统一上传到 data/articles/img 目录。</p>
|
||||||
|
|
||||||
|
<?php if ($savedFlag): ?>
|
||||||
|
<div class="ok-msg">保存成功,文章库前台已同步更新。</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($msgText !== ''): echo msg_block($msgKind, $msgText); endif; ?>
|
||||||
|
|
||||||
|
<?php if ($editing || isset($_GET['new'])): ?>
|
||||||
|
<?php
|
||||||
|
$cur = $row ? [
|
||||||
|
'id' => (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'],
|
||||||
|
] : ['id' => 0, 'part' => '未分类', 'title' => '', 'summary' => '', 'md' => '', 'sort' => 0, 'pinned' => 0, 'enabled' => 1];
|
||||||
|
// 分区下拉建议(仅列已有分区)
|
||||||
|
$partSuggest = [];
|
||||||
|
foreach ($list as $a2) {
|
||||||
|
$p2 = trim((string)$a2['part']);
|
||||||
|
if ($p2 !== '' && !in_array($p2, $partSuggest, true)) {
|
||||||
|
$partSuggest[] = $p2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="fieldset-card">
|
||||||
|
<div class="fs-title"><?= $cur['id'] ? '编辑文章 #' . $cur['id'] : '新增文章' ?></div>
|
||||||
|
<form method="post">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="act" value="save">
|
||||||
|
<input type="hidden" name="id" value="<?= (int)$cur['id'] ?>">
|
||||||
|
<div class="field-row" style="flex-wrap:wrap">
|
||||||
|
<div style="flex:1 1 200px;min-width:160px">
|
||||||
|
<label class="fl">分区 / 栏目</label>
|
||||||
|
<input type="text" name="part" value="<?= he($cur['part']) ?>" placeholder="如:安全研究 / 随笔" list="artPartList">
|
||||||
|
<datalist id="artPartList">
|
||||||
|
<?php foreach ($partSuggest as $ps): ?>
|
||||||
|
<option value="<?= he($ps) ?>">
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div style="flex:2 1 260px;min-width:200px">
|
||||||
|
<label class="fl">文章标题</label>
|
||||||
|
<input type="text" name="title" value="<?= he($cur['title']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div style="flex:0 1 120px;min-width:90px">
|
||||||
|
<label class="fl">排序(组内)</label>
|
||||||
|
<input type="number" name="sort" value="<?= (int)$cur['sort'] ?>" min="0" max="999">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="fl">摘要 / 一句话简介(文章区列表展示)</label>
|
||||||
|
<input type="text" name="summary" value="<?= he($cur['summary']) ?>" placeholder="可选">
|
||||||
|
<div class="field-row" style="gap:18px;margin:6px 0">
|
||||||
|
<label class="seg-check"><input type="checkbox" name="pinned" value="1"<?= $cur['pinned'] ? ' checked' : '' ?>> 置顶(首页文章区默认最前)</label>
|
||||||
|
<label class="seg-check"><input type="checkbox" name="enabled" value="1"<?= $cur['enabled'] ? ' checked' : '' ?>> 前台可见</label>
|
||||||
|
</div>
|
||||||
|
<label class="fl">正文(Markdown)</label>
|
||||||
|
<textarea name="markdown" rows="16" style="font-family:Consolas,Menlo,monospace;font-size:13px"><?= he($cur['md']) ?></textarea>
|
||||||
|
<div class="field-row" style="margin-top:8px;align-items:center">
|
||||||
|
<input type="file" id="artImg" accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml" style="flex:0 1 260px">
|
||||||
|
<button type="button" class="btn btn-sm" id="artImgUpload">上传图片并插入引用</button>
|
||||||
|
<span class="tip">上传后自动在正文末尾插入 <code></code> 形式引用;引用路径相对站点根目录,渲染时自动补全前缀。</span>
|
||||||
|
</div>
|
||||||
|
<div id="artUpMsg" style="margin-top:6px"></div>
|
||||||
|
<div class="field-row" style="margin-top:14px">
|
||||||
|
<button type="submit" class="btn btn-primary">保存文章</button>
|
||||||
|
<a class="btn" href="articles.php">取消 / 返回列表</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 文章区首页注要 -->
|
||||||
|
<div class="fieldset-card">
|
||||||
|
<div class="fs-title">文章区首页注要(顶部展示给读者的说明文字)</div>
|
||||||
|
<form method="post">
|
||||||
|
<?= csrf_field() ?><input type="hidden" name="act" value="note">
|
||||||
|
<textarea name="note" rows="3" placeholder="例如:这里收录本人在安全与开发方向的原创文章,欢迎阅读。"><?= he($noteText) ?></textarea>
|
||||||
|
<div style="margin-top:10px"><button type="submit" class="btn btn-primary">保存注要</button></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文章列表 -->
|
||||||
|
<div class="fieldset-card">
|
||||||
|
<div class="field-row" style="align-items:center;justify-content:space-between">
|
||||||
|
<span class="fs-title" style="margin:0;border:none;padding:0">文章列表(共 <?= count($list) ?> 篇)</span>
|
||||||
|
<a class="btn btn-primary btn-sm" href="articles.php?new=1">+ 新增文章</a>
|
||||||
|
</div>
|
||||||
|
<?php if (!$list): ?>
|
||||||
|
<p class="tip" style="margin-top:10px">还没有文章,点击右上角「新增文章」开始写作。</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div style="overflow-x:auto">
|
||||||
|
<table class="tbl">
|
||||||
|
<thead><tr><th>分区</th><th>标题</th><th>状态</th><th>排序</th><th>操作</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($list as $a): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= he((string)$a['part']) ?></td>
|
||||||
|
<td>
|
||||||
|
<a href="articles.php?edit=<?= (int)$a['id'] ?>"><?= he((string)$a['title']) ?></a>
|
||||||
|
<div class="tip"><?= he((string)$a['summary']) ?></div>
|
||||||
|
</td>
|
||||||
|
<td style="white-space:nowrap">
|
||||||
|
<?php if ((int)$a['pinned'] === 1): ?><span class="badge badge-ok">置顶</span><?php endif; ?>
|
||||||
|
<?php if ((int)$a['enabled'] === 1): ?><span class="badge badge-ok">可见</span><?php else: ?><span class="badge badge-no">隐藏</span><?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?= (int)$a['sort'] ?></td>
|
||||||
|
<td style="white-space:nowrap">
|
||||||
|
<form method="post" style="display:inline" onsubmit="return confirm('确认删除该文章?')">
|
||||||
|
<?= csrf_field() ?><input type="hidden" name="act" value="del"><input type="hidden" name="id" value="<?= (int)$a['id'] ?>">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger" title="删除">✕</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="display:inline">
|
||||||
|
<?= csrf_field() ?><input type="hidden" name="act" value="toggle"><input type="hidden" name="field" value="pinned"><input type="hidden" name="id" value="<?= (int)$a['id'] ?>">
|
||||||
|
<button type="submit" class="btn btn-sm" title="<?= (int)$a['pinned'] === 1 ? '取消置顶' : '置顶' ?>"><?= (int)$a['pinned'] === 1 ? '取消置顶' : '置顶' ?></button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="display:inline">
|
||||||
|
<?= csrf_field() ?><input type="hidden" name="act" value="toggle"><input type="hidden" name="field" value="enabled"><input type="hidden" name="id" value="<?= (int)$a['id'] ?>">
|
||||||
|
<button type="submit" class="btn btn-sm"><?= (int)$a['enabled'] === 1 ? '隐藏' : '显示' ?></button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="display:inline">
|
||||||
|
<?= csrf_field() ?><input type="hidden" name="act" value="move"><input type="hidden" name="dir" value="up"><input type="hidden" name="id" value="<?= (int)$a['id'] ?>">
|
||||||
|
<button type="submit" class="btn btn-sm" title="上移">↑</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="display:inline">
|
||||||
|
<?= csrf_field() ?><input type="hidden" name="act" value="move"><input type="hidden" name="dir" value="down"><input type="hidden" name="id" value="<?= (int)$a['id'] ?>">
|
||||||
|
<button type="submit" class="btn btn-sm" title="下移">↓</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="tip">同分区内使用 ↑/↓ 调整顺序,置顶文章在列表中始终靠前。</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php layout_theme_fab(); ?>
|
||||||
|
<script src="../assets/js/common.js"></script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
var btn = document.getElementById('artImgUpload');
|
||||||
|
var file = document.getElementById('artImg');
|
||||||
|
var msg = document.getElementById('artUpMsg');
|
||||||
|
if (!btn || !file) return;
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
if (!file.files || !file.files[0]) { alert('请先选择一张图片。'); return; }
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append('act', 'upload');
|
||||||
|
var csrf = document.querySelector('input[name="csrf"]');
|
||||||
|
if (csrf) fd.append(csrf.name, csrf.value);
|
||||||
|
fd.append('file', file.files[0]);
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', window.location.href);
|
||||||
|
xhr.onload = function () {
|
||||||
|
try {
|
||||||
|
var r = JSON.parse(xhr.responseText || '{}');
|
||||||
|
if (!r.ok) { msg.innerHTML = '<div class="err-msg">上传失败:' + (r.msg || '未知错误') + '</div>'; return; }
|
||||||
|
var ta = document.querySelector('textarea[name="markdown"]');
|
||||||
|
var name = r.file.replace('data/articles/img/', '');
|
||||||
|
var ins = '';
|
||||||
|
if (ta) {
|
||||||
|
if (ta.value && ta.value.charAt(ta.value.length - 1) !== '\n') ta.value += '\n';
|
||||||
|
ta.value += '\n' + ins;
|
||||||
|
}
|
||||||
|
msg.innerHTML = '<div class="ok-msg">图片已上传:data/articles/img/' + name + ',并已插入引用。</div>';
|
||||||
|
file.value = '';
|
||||||
|
} catch (e) {
|
||||||
|
msg.innerHTML = '<div class="err-msg">上传返回异常:' + e + '</div>';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function () { msg.innerHTML = '<div class="err-msg">网络错误,上传失败。</div>'; };
|
||||||
|
xhr.send(fd);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -8,7 +8,7 @@ $P = '../';
|
|||||||
require_once __DIR__ . '/_guard.php';
|
require_once __DIR__ . '/_guard.php';
|
||||||
|
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$catKeys = ['popular', 'red', 'blue', 'tool'];
|
$catKeys = ['popular', 'red', 'blue', 'tool', 'article'];
|
||||||
|
|
||||||
function settings_set(string $key, string $value): void
|
function settings_set(string $key, string $value): void
|
||||||
{
|
{
|
||||||
@ -292,6 +292,7 @@ $boardDefaults = [
|
|||||||
'red' => '面向渗透测试与攻防演练的红队资源集。',
|
'red' => '面向渗透测试与攻防演练的红队资源集。',
|
||||||
'blue' => '面向防御侧:应急响应、威胁狩猎、取证溯源。',
|
'blue' => '面向防御侧:应急响应、威胁狩猎、取证溯源。',
|
||||||
'tool' => '日常效率与安全实用小工具,直达功能区。',
|
'tool' => '日常效率与安全实用小工具,直达功能区。',
|
||||||
|
'article' => '站长原创与整理收录的图文与 Markdown 文章合集。',
|
||||||
];
|
];
|
||||||
|
|
||||||
$logo = setting_get('site_logo', '');
|
$logo = setting_get('site_logo', '');
|
||||||
|
|||||||
70
article/data.php
Normal file
70
article/data.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* article/data.php —— 文章库 JSON 数据接口
|
||||||
|
* data.php?mode=list 文章元数据列表(按分区分组,含分区名)
|
||||||
|
* data.php?mode=content&id=N 单篇文章完整数据(含 markdown 正文)
|
||||||
|
* 说明:仅返回前台可见(enabled=1)文章;隐文不对外。
|
||||||
|
*/
|
||||||
|
require_once dirname(__DIR__) . '/includes/db.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-store');
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
function json_out(array $data): void
|
||||||
|
{
|
||||||
|
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = (string)($_GET['mode'] ?? 'list');
|
||||||
|
|
||||||
|
if ($mode === 'content') {
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
json_out(['ok' => false, 'msg' => '缺少文章 id']);
|
||||||
|
}
|
||||||
|
$st = $pdo->prepare('SELECT id, part, title, summary, markdown, pinned, created_at, updated_at
|
||||||
|
FROM articles WHERE id = ? AND enabled = 1 LIMIT 1');
|
||||||
|
$st->execute([$id]);
|
||||||
|
$row = $st->fetch();
|
||||||
|
if (!$row) {
|
||||||
|
json_out(['ok' => false, 'msg' => '文章不存在或已隐藏']);
|
||||||
|
}
|
||||||
|
json_out(['ok' => true, 'article' => $row]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 默认:list 分组元数据(分区顺序取各分区首篇 id 序;分区内置顶 → sort → id) ----
|
||||||
|
$rows = $pdo->query('SELECT id, part, title, summary, pinned, sort, created_at, updated_at
|
||||||
|
FROM articles WHERE enabled = 1 ORDER BY id ASC')->fetchAll();
|
||||||
|
$parts = [];
|
||||||
|
if ($rows) {
|
||||||
|
$partOrder = [];
|
||||||
|
$byPart = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$part = trim((string)$r['part']);
|
||||||
|
if ($part === '') {
|
||||||
|
$part = '未分类';
|
||||||
|
}
|
||||||
|
if (!isset($byPart[$part])) {
|
||||||
|
$byPart[$part] = [];
|
||||||
|
$partOrder[] = $part;
|
||||||
|
}
|
||||||
|
$byPart[$part][] = $r;
|
||||||
|
}
|
||||||
|
foreach ($partOrder as $part) {
|
||||||
|
$items = $byPart[$part];
|
||||||
|
usort($items, static function (array $a, array $b): int {
|
||||||
|
if ((int)$a['pinned'] !== (int)$b['pinned']) {
|
||||||
|
return (int)$b['pinned'] - (int)$a['pinned'];
|
||||||
|
}
|
||||||
|
if ((int)$a['sort'] !== (int)$b['sort']) {
|
||||||
|
return (int)$a['sort'] <=> (int)$b['sort'];
|
||||||
|
}
|
||||||
|
return (int)$a['id'] <=> (int)$b['id'];
|
||||||
|
});
|
||||||
|
$parts[] = ['part' => $part, 'articles' => $items];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
json_out(['ok' => true, 'parts' => $parts]);
|
||||||
186
article/index.php
Normal file
186
article/index.php
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* article/index.php —— 文章区(文章库)
|
||||||
|
* 结构:顶部栏(返回首页 + 当页名 + 分区/段落开关)
|
||||||
|
* 内容三栏:左 15% 文章标题栏(按分区) / 中 15% 当前文章段落目录 / 右 70% 正文(Markdown 渲染)
|
||||||
|
* 文章正文存 SQLite;图片上传到 data/articles/img/,正文以 data/articles/img/ 站点相对路径引用,
|
||||||
|
* 渲染时自动补全前缀。三栏宽度可拖拽调节,标题栏 / 段落栏可分别隐藏。
|
||||||
|
* 打开页面为「先列表后阅读」:默认右栏展示全部分区文章列表,点击文章进入阅读。
|
||||||
|
*/
|
||||||
|
$P = '../';
|
||||||
|
require_once dirname(__DIR__) . '/includes/layout.php';
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
$noteText = setting_get('article_note', '');
|
||||||
|
|
||||||
|
// 全部可见文章(按 id 稳定取回,分区顺序取各分区首篇 id 序;分区内置顶 → sort → id)
|
||||||
|
$all = $pdo->query('SELECT id, part, title, summary, sort, created_at, updated_at
|
||||||
|
FROM articles WHERE enabled = 1 ORDER BY id ASC')->fetchAll();
|
||||||
|
$rows = [];
|
||||||
|
$partOrder = [];
|
||||||
|
$byPart = [];
|
||||||
|
foreach ($all as $r) {
|
||||||
|
$part = trim((string)$r['part']);
|
||||||
|
if ($part === '') {
|
||||||
|
$part = '未分类';
|
||||||
|
}
|
||||||
|
if (!isset($byPart[$part])) {
|
||||||
|
$byPart[$part] = [];
|
||||||
|
$partOrder[] = $part;
|
||||||
|
}
|
||||||
|
$byPart[$part][] = $r;
|
||||||
|
}
|
||||||
|
foreach ($partOrder as $part) {
|
||||||
|
$items = $byPart[$part];
|
||||||
|
usort($items, static function (array $a, array $b): int {
|
||||||
|
if ((int)$a['pinned'] !== (int)$b['pinned']) {
|
||||||
|
return (int)$b['pinned'] - (int)$a['pinned'];
|
||||||
|
}
|
||||||
|
if ((int)$a['sort'] !== (int)$b['sort']) {
|
||||||
|
return (int)$a['sort'] <=> (int)$b['sort'];
|
||||||
|
}
|
||||||
|
return (int)$a['id'] <=> (int)$b['id'];
|
||||||
|
});
|
||||||
|
$rows[] = ['part' => $part, 'articles' => $items];
|
||||||
|
}
|
||||||
|
$total = count($all);
|
||||||
|
|
||||||
|
// 当前阅读文章
|
||||||
|
$cur = null;
|
||||||
|
$cid = (int)($_GET['id'] ?? 0);
|
||||||
|
if ($cid > 0) {
|
||||||
|
$st = $pdo->prepare('SELECT * FROM articles WHERE id = ? AND enabled = 1 LIMIT 1');
|
||||||
|
$st->execute([$cid]);
|
||||||
|
$cur = $st->fetch();
|
||||||
|
if (!$cur) {
|
||||||
|
$cur = null; // 无效或已隐藏:回退到列表
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout_head($cur ? (string)$cur['title'] : '文章库');
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
/* 文章区专用:将外层正文区铺满视口 */
|
||||||
|
html, body { height: 100%; }
|
||||||
|
</style>
|
||||||
|
<div class="art-app js-art-app">
|
||||||
|
<!-- 顶部栏:返回首页 / 当页名(列表为文章库、阅读时显示文章标题)/ 分区·段落开关 -->
|
||||||
|
<header class="art-topbar">
|
||||||
|
<div class="art-topbar-nav">
|
||||||
|
<a class="btn btn-sm" href="../index.php" title="返回站点首页">← 首页</a>
|
||||||
|
<?php if ($cur): ?>
|
||||||
|
<a class="btn btn-sm" href="index.php" title="返回文章区首页(文章列表)">← 文章区首页</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="art-topbar-title"><?= $cur ? he((string)$cur['title']) : '📚 文章库' ?></div>
|
||||||
|
<div class="art-topbar-acts">
|
||||||
|
<button type="button" class="btn btn-sm js-art-toggle" data-col="title" title="显示 / 隐藏左侧文章标题栏">☰<span class="art-tg-txt">标题栏</span></button>
|
||||||
|
<button type="button" class="btn btn-sm js-art-toggle" data-col="toc" title="显示 / 隐藏中间段落目录">≡<span class="art-tg-txt">段落</span></button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<?php if ($noteText !== ''): ?>
|
||||||
|
<div class="art-note"><?= he($noteText) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="art-frame js-art-frame">
|
||||||
|
<!-- 左:文章标题栏(按分区) -->
|
||||||
|
<aside class="art-col art-col-title js-col-title">
|
||||||
|
<div class="art-col-head">
|
||||||
|
<span>文章目录</span>
|
||||||
|
<button type="button" class="art-mini js-art-toggle" data-col="title" title="隐藏标题栏">✕</button>
|
||||||
|
</div>
|
||||||
|
<nav class="art-nav js-art-nav">
|
||||||
|
<?php if (!$rows): ?>
|
||||||
|
<p class="tip" style="padding:10px">暂无文章</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php foreach ($rows as $g): ?>
|
||||||
|
<div class="art-nav-part">
|
||||||
|
<div class="art-nav-part-name"><?= he($g['part']) ?></div>
|
||||||
|
<?php foreach ($g['articles'] as $a): ?>
|
||||||
|
<a class="art-nav-item<?= ($cur && (int)$cur['id'] === (int)$a['id']) ? ' active' : '' ?>"
|
||||||
|
href="?id=<?= (int)$a['id'] ?>" title="<?= he((string)$a['summary']) ?>"><?= he((string)$a['title']) ?></a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="art-gutter js-gutter" data-var="--w-title"></div>
|
||||||
|
|
||||||
|
<!-- 中:段落目录 -->
|
||||||
|
<aside class="art-col art-col-toc js-col-toc">
|
||||||
|
<div class="art-col-head">
|
||||||
|
<span>段落目录</span>
|
||||||
|
<button type="button" class="art-mini js-art-toggle" data-col="toc" title="隐藏段落栏">✕</button>
|
||||||
|
</div>
|
||||||
|
<nav class="art-toc js-toc-nav"></nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="art-gutter js-gutter" data-var="--w-toc"></div>
|
||||||
|
|
||||||
|
<!-- 右:正文 / 列表 -->
|
||||||
|
<section class="art-body js-art-body" id="artBody">
|
||||||
|
<?php if ($cur): ?>
|
||||||
|
<?php
|
||||||
|
$md = (string)$cur['markdown'];
|
||||||
|
$updated = (string)($cur['updated_at'] ?? '');
|
||||||
|
$part = (string)$cur['part'];
|
||||||
|
?>
|
||||||
|
<div class="art-article js-article" data-id="<?= (int)$cur['id'] ?>">
|
||||||
|
<div class="art-article-head">
|
||||||
|
<div class="art-article-part"><?= he($part) ?></div>
|
||||||
|
<h1 class="art-article-title js-art-title"><?= he((string)$cur['title']) ?></h1>
|
||||||
|
<?php if (trim((string)($cur['summary'] ?? '')) !== ''): ?>
|
||||||
|
<p class="art-article-sum"><?= he(trim((string)$cur['summary'])) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($updated !== ''): ?>
|
||||||
|
<div class="art-article-meta">更新于 <?= he($updated) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="art-md js-art-md" id="artMd"></div>
|
||||||
|
<textarea id="artRaw" hidden><?= he($md) ?></textarea>
|
||||||
|
<script type="text/plain" id="artErrTpl"><div class="err-msg">正文渲染失败:无法加载 Markdown 解析库,请检查网络后刷新。</div></script>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="art-list">
|
||||||
|
<div class="art-list-head">
|
||||||
|
<h1>全部文章 <span class="tip">(共 <?= $total ?> 篇)</span></h1>
|
||||||
|
<p class="tip">点击任意文章开始阅读;左侧「文章目录」可随时切换文章,中间「段落目录」可快速跳转。</p>
|
||||||
|
</div>
|
||||||
|
<?php if (!$rows): ?>
|
||||||
|
<div class="panel-card"><p class="tip">文章库还没有内容,站长可在后台「文章管理」中发布。</p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php foreach ($rows as $g): ?>
|
||||||
|
<div class="art-part-block">
|
||||||
|
<h2 class="art-part-title"><?= he($g['part']) ?></h2>
|
||||||
|
<div class="art-cards">
|
||||||
|
<?php foreach ($g['articles'] as $a): ?>
|
||||||
|
<a class="art-card" href="?id=<?= (int)$a['id'] ?>">
|
||||||
|
<span class="art-card-title"><?= he((string)$a['title']) ?></span>
|
||||||
|
<?php if (trim((string)($a['summary'] ?? '')) !== ''): ?>
|
||||||
|
<span class="art-card-sum"><?= he(trim((string)$a['summary'])) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="art-card-date"><?= he((string)($a['updated_at'] ?? '')) ?></span>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php layout_theme_fab(); ?>
|
||||||
|
<script>
|
||||||
|
window.HP_ART_DATA = {
|
||||||
|
curId: <?= $cur ? (int)$cur['id'] : 0 ?>,
|
||||||
|
listMode: <?= $cur ? 0 : 1 ?>
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script src="../assets/js/common.js"></script>
|
||||||
|
<script src="../assets/js/article.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -200,7 +200,7 @@ a.quick-chip[data-note]:hover::after { opacity: 1; transform: translateX(-50%) t
|
|||||||
.board-icon img.icon-img { width: 30px; height: 30px; }
|
.board-icon img.icon-img { width: 30px; height: 30px; }
|
||||||
.board-section .section-title { text-align: center; font-size: 22px; margin-bottom: 6px; }
|
.board-section .section-title { text-align: center; font-size: 22px; margin-bottom: 6px; }
|
||||||
.board-section .section-sub { text-align: center; color: var(--text-3); font-size: 14px; margin-bottom: 18px; }
|
.board-section .section-sub { text-align: center; color: var(--text-3); font-size: 14px; margin-bottom: 18px; }
|
||||||
.boards { position: relative; display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; }
|
.boards { position: relative; display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 18px; }
|
||||||
.board-card {
|
.board-card {
|
||||||
position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
gap: 12px; background-color: var(--card); background-image: var(--board-tint);
|
gap: 12px; background-color: var(--card); background-image: var(--board-tint);
|
||||||
@ -499,3 +499,165 @@ a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) tran
|
|||||||
.nav-block-grid { grid-template-columns: 1fr; grid-auto-rows: auto; }
|
.nav-block-grid { grid-template-columns: 1fr; grid-auto-rows: auto; }
|
||||||
.nav-btn { justify-content: flex-start; padding: 8px 12px; }
|
.nav-btn { justify-content: flex-start; padding: 8px 12px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============ 文章库(第五板块:三栏阅读区) ============ */
|
||||||
|
.art-app { display: flex; flex-direction: column; height: 100vh; min-height: 0; }
|
||||||
|
.art-topbar {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 10px 14px; background: var(--card);
|
||||||
|
border-bottom: 1px solid var(--line); flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.art-topbar-nav { display: flex; gap: 6px; flex: 0 0 auto; min-width: 0; }
|
||||||
|
.art-topbar-nav .btn { white-space: nowrap; }
|
||||||
|
.art-topbar-title {
|
||||||
|
flex: 1 1 auto; text-align: center; font-size: 17px; font-weight: 600;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; max-width: 100%;
|
||||||
|
}
|
||||||
|
.art-topbar-acts { display: flex; gap: 6px; flex: 0 0 auto; min-width: 0; }
|
||||||
|
.art-topbar-acts .btn.active {
|
||||||
|
background: var(--accent-soft); border-color: var(--brand); color: var(--brand);
|
||||||
|
}
|
||||||
|
.art-note {
|
||||||
|
flex: 0 0 auto; padding: 8px 16px; text-align: center;
|
||||||
|
background: var(--accent-soft); color: var(--text-2); font-size: 13.5px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.art-frame {
|
||||||
|
flex: 1 1 auto; min-height: 0; display: flex;
|
||||||
|
--w-title: 15%; --w-toc: 15%; position: relative;
|
||||||
|
}
|
||||||
|
.art-col {
|
||||||
|
display: flex; flex-direction: column; min-height: 0;
|
||||||
|
background: var(--card); min-width: 0;
|
||||||
|
}
|
||||||
|
.art-col-title { flex: 0 0 var(--w-title, 15%); border-right: 1px solid var(--line); }
|
||||||
|
.art-col-toc { flex: 0 0 var(--w-toc, 15%); border-right: 1px solid var(--line); }
|
||||||
|
.art-col.art-col-hidden { display: none; }
|
||||||
|
/* 窄屏浮层显示态 */
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.art-col-title, .art-col-toc {
|
||||||
|
position: absolute; top: 0; bottom: 0; z-index: 40;
|
||||||
|
width: min(80%, 300px); box-shadow: var(--shadow); display: none;
|
||||||
|
}
|
||||||
|
.art-col-title { left: 0; border-right: 1px solid var(--line); }
|
||||||
|
.art-col-toc { right: 0; border-left: 1px solid var(--line); }
|
||||||
|
.art-col-title.art-col-show-mobile, .art-col-toc.art-col-show-mobile { display: flex; }
|
||||||
|
.art-gutter { display: none !important; }
|
||||||
|
}
|
||||||
|
.art-col-head {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 6px;
|
||||||
|
padding: 8px 12px; font-size: 13px; font-weight: 600; color: var(--text-2);
|
||||||
|
border-bottom: 1px solid var(--line); background: var(--bg-2); flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.art-mini {
|
||||||
|
border: none; background: transparent; cursor: pointer; color: var(--text-3);
|
||||||
|
font-size: 13px; line-height: 1; padding: 3px 6px; border-radius: 5px;
|
||||||
|
}
|
||||||
|
.art-mini:hover { background: var(--hover-bg); color: var(--danger); }
|
||||||
|
.art-nav, .art-toc { overflow-y: auto; flex: 1 1 auto; min-height: 0; padding: 6px 0; }
|
||||||
|
.art-nav-part { margin: 4px 0; }
|
||||||
|
.art-nav-part-name {
|
||||||
|
padding: 5px 12px 3px; font-size: 12px; color: var(--text-3);
|
||||||
|
position: sticky; top: 0; background: var(--card); font-weight: 600;
|
||||||
|
}
|
||||||
|
.art-nav-item {
|
||||||
|
display: block; padding: 6px 12px 6px 16px; font-size: 13.5px;
|
||||||
|
color: var(--text-2); border-left: 2px solid transparent;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.art-nav-item:hover { background: var(--hover-bg); color: var(--brand); }
|
||||||
|
.art-nav-item.active {
|
||||||
|
background: var(--accent-soft); color: var(--brand); border-left-color: var(--brand);
|
||||||
|
}
|
||||||
|
.art-toc-link {
|
||||||
|
display: block; padding: 5px 14px 5px 20px; font-size: 13px; color: var(--text-2);
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; position: relative;
|
||||||
|
}
|
||||||
|
.art-toc-link:hover { color: var(--brand); background: var(--hover-bg); }
|
||||||
|
.art-toc-link.lv2 { padding-left: 30px; font-size: 12.5px; }
|
||||||
|
.art-toc-link.lv3 { padding-left: 42px; font-size: 12px; }
|
||||||
|
.art-toc-link.lv4 { padding-left: 54px; font-size: 11.5px; }
|
||||||
|
.art-gutter {
|
||||||
|
flex: 0 0 7px; cursor: col-resize; background: transparent;
|
||||||
|
position: relative; z-index: 5;
|
||||||
|
}
|
||||||
|
.art-gutter:hover, .art-gutter.dragging { background: var(--brand); opacity: .35; }
|
||||||
|
.art-body {
|
||||||
|
flex: 1 1 auto; min-width: 0; min-height: 0; overflow-y: auto;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 正文排版(Markdown 渲染区) */
|
||||||
|
.art-article { max-width: 1000px; margin: 0 auto; padding: 26px 34px 60px; }
|
||||||
|
.art-article-head { border-bottom: 1px solid var(--line); padding-bottom: 16px; margin-bottom: 18px; }
|
||||||
|
.art-article-part {
|
||||||
|
display: inline-block; font-size: 12px; color: var(--brand);
|
||||||
|
background: var(--accent-soft); border-radius: 6px; padding: 2px 9px; margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.art-article-title { font-size: 26px; margin: 0 0 6px; line-height: 1.35; }
|
||||||
|
.art-article-sum { color: var(--text-3); font-size: 14px; margin: 0 0 8px; }
|
||||||
|
.art-article-meta { color: var(--text-3); font-size: 12px; }
|
||||||
|
.art-md { line-height: 1.75; font-size: 15px; color: var(--text); overflow-wrap: break-word; }
|
||||||
|
.art-md h1, .art-md h2, .art-md h3, .art-md h4, .art-md h5, .art-md h6 {
|
||||||
|
margin: 1.3em 0 .5em; line-height: 1.4;
|
||||||
|
}
|
||||||
|
.art-md h1 { font-size: 1.6em; border-bottom: 1px solid var(--line); padding-bottom: .25em; }
|
||||||
|
.art-md h2 { font-size: 1.4em; border-bottom: 1px solid var(--line); padding-bottom: .2em; }
|
||||||
|
.art-md h3 { font-size: 1.22em; }
|
||||||
|
.art-md h4 { font-size: 1.1em; }
|
||||||
|
.art-md p { margin: .6em 0; }
|
||||||
|
.art-md ul, .art-md ol { padding-left: 1.6em; margin: .5em 0; }
|
||||||
|
.art-md li { margin: .2em 0; }
|
||||||
|
.art-md blockquote {
|
||||||
|
margin: .8em 0; padding: 8px 14px; color: var(--text-2);
|
||||||
|
border-left: 4px solid var(--brand); background: var(--accent-soft); border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
.art-md code {
|
||||||
|
font-family: Consolas, Menlo, Monaco, monospace; font-size: .92em;
|
||||||
|
background: var(--code-bg); color: var(--code-text); border-radius: 4px; padding: 2px 6px;
|
||||||
|
}
|
||||||
|
.art-md pre {
|
||||||
|
background: var(--code-bg); border: 1px solid var(--line); border-radius: 10px;
|
||||||
|
padding: 14px 16px; overflow-x: auto; line-height: 1.6;
|
||||||
|
}
|
||||||
|
.art-md pre code { background: none; padding: 0; color: var(--code-text); }
|
||||||
|
.art-md img { max-width: 100%; border-radius: 8px; margin: .4em 0; }
|
||||||
|
.art-md a { text-decoration: underline; text-underline-offset: 2px; }
|
||||||
|
.art-md table {
|
||||||
|
border-collapse: collapse; margin: .8em 0; max-width: 100%; display: block; overflow-x: auto;
|
||||||
|
}
|
||||||
|
.art-md th, .art-md td { border: 1px solid var(--line-2); padding: 6px 12px; font-size: 14px; }
|
||||||
|
.art-md th { background: var(--bg-2); }
|
||||||
|
.art-md hr { border: none; border-top: 1px solid var(--line); margin: 1.6em 0; }
|
||||||
|
|
||||||
|
/* 列表视图(先列表后阅读) */
|
||||||
|
.art-list { max-width: 1000px; margin: 0 auto; padding: 24px 26px 60px; }
|
||||||
|
.art-list-head h1 { font-size: 24px; margin: 0 0 4px; }
|
||||||
|
.art-list-head .tip { margin: 0 0 10px; }
|
||||||
|
.art-part-block { margin-top: 18px; }
|
||||||
|
.art-part-title {
|
||||||
|
font-size: 17px; color: var(--text-2); margin: 0 0 10px;
|
||||||
|
padding-left: 10px; border-left: 4px solid var(--brand);
|
||||||
|
}
|
||||||
|
.art-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
|
||||||
|
.art-card {
|
||||||
|
display: flex; flex-direction: column; gap: 6px; padding: 14px 16px;
|
||||||
|
background: var(--card); border: 1px solid var(--line); border-radius: 12px;
|
||||||
|
color: var(--text); transition: all .15s ease; min-width: 0;
|
||||||
|
}
|
||||||
|
.art-card:hover { border-color: var(--brand); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
|
||||||
|
.art-card-title { font-weight: 600; font-size: 15px; }
|
||||||
|
.art-card:hover .art-card-title { color: var(--brand); }
|
||||||
|
.art-card-sum { color: var(--text-3); font-size: 13px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
|
.art-card-date { color: var(--text-3); font-size: 12px; }
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.art-cards { grid-template-columns: 1fr; }
|
||||||
|
.art-article { padding: 18px 18px 48px; }
|
||||||
|
.art-list { padding: 16px 14px 48px; }
|
||||||
|
.art-article-title { font-size: 21px; }
|
||||||
|
.art-topbar { gap: 5px; padding: 8px 8px; }
|
||||||
|
.art-topbar .btn { padding: 4px 8px; font-size: 12px; }
|
||||||
|
.art-topbar-title { font-size: 14px; }
|
||||||
|
.art-tg-txt { display: none; }
|
||||||
|
.art-topbar-nav { gap: 4px; }
|
||||||
|
}
|
||||||
|
|||||||
272
assets/js/article.js
Normal file
272
assets/js/article.js
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
/* article.js —— 文章库三栏阅读交互
|
||||||
|
* 功能:三栏(标题栏 / 段落栏 / 正文)宽度拖拽调节、分区栏与段落栏显示/隐藏;
|
||||||
|
* Markdown 正文 CDN 懒加载渲染、正文图片路径前缀补全、段落目录生成与平滑跳转。
|
||||||
|
*/
|
||||||
|
(function (global) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function $(sel, root) { return (root || document).querySelector(sel); }
|
||||||
|
function $$(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
|
||||||
|
|
||||||
|
var app = $('.js-art-app');
|
||||||
|
var frame = $('.js-art-frame');
|
||||||
|
if (!app || !frame) return;
|
||||||
|
|
||||||
|
var BODY_MIN = 22; // 正文列最小占比(%)
|
||||||
|
var KEY = 'hp-art-layout';
|
||||||
|
var MEDIA_SMALL = '(max-width: 860px)';
|
||||||
|
|
||||||
|
/* ---------- 布局状态(宽度 / 显隐) ---------- */
|
||||||
|
function loadState() {
|
||||||
|
var isList = global.HP_ART_DATA && global.HP_ART_DATA.listMode ? true : false;
|
||||||
|
var st = { w1: 15, w2: 15, t1: true, t2: !isList }; // 列表页默认收起段落栏
|
||||||
|
try {
|
||||||
|
var saved = JSON.parse(localStorage.getItem(KEY) || 'null');
|
||||||
|
if (saved) {
|
||||||
|
st.w1 = clampPct(saved.w1, 6, 60);
|
||||||
|
st.w2 = clampPct(saved.w2, 6, 60);
|
||||||
|
if (typeof saved.t1 === 'boolean') st.t1 = saved.t1;
|
||||||
|
if (typeof saved.t2 === 'boolean') st.t2 = saved.t2;
|
||||||
|
} else if (window.matchMedia && window.matchMedia(MEDIA_SMALL).matches) {
|
||||||
|
// 窄屏首次进入:默认只显示正文,两栏以浮层形式随时唤出
|
||||||
|
st.t1 = false;
|
||||||
|
st.t2 = false;
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
function saveState(st) {
|
||||||
|
try { localStorage.setItem(KEY, JSON.stringify(st)); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
function clampPct(v, min, max) {
|
||||||
|
v = parseFloat(v);
|
||||||
|
if (isNaN(v)) return min;
|
||||||
|
return Math.min(max, Math.max(min, v));
|
||||||
|
}
|
||||||
|
var st = loadState();
|
||||||
|
|
||||||
|
function isSmall() {
|
||||||
|
return (window.matchMedia && window.matchMedia(MEDIA_SMALL).matches) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLayout() {
|
||||||
|
frame.style.setProperty('--w-title', st.w1 + '%');
|
||||||
|
frame.style.setProperty('--w-toc', st.w2 + '%');
|
||||||
|
var small = isSmall();
|
||||||
|
var tTitle = $('.js-col-title');
|
||||||
|
var tToc = $('.js-col-toc');
|
||||||
|
var gTitle = $('.js-gutter[data-var="--w-title"]');
|
||||||
|
var gToc = $('.js-gutter[data-var="--w-toc"]');
|
||||||
|
if (!small) {
|
||||||
|
if (tTitle) { tTitle.classList.toggle('art-col-hidden', !st.t1); }
|
||||||
|
if (tToc) { tToc.classList.toggle('art-col-hidden', !st.t2); }
|
||||||
|
if (gTitle) gTitle.style.display = st.t1 ? '' : 'none';
|
||||||
|
if (gToc) gToc.style.display = st.t2 ? '' : 'none';
|
||||||
|
} else {
|
||||||
|
// 窄屏:默认隐藏,靠开关以浮层方式显示
|
||||||
|
if (tTitle) tTitle.classList.toggle('art-col-show-mobile', !!st.t1);
|
||||||
|
if (tToc) tToc.classList.toggle('art-col-show-mobile', !!st.t2);
|
||||||
|
if (gTitle) gTitle.style.display = 'none';
|
||||||
|
if (gToc) gToc.style.display = 'none';
|
||||||
|
}
|
||||||
|
updateToggleBtns();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColVisible(col, visible) {
|
||||||
|
if (col === 'title') st.t1 = visible;
|
||||||
|
if (col === 'toc') st.t2 = visible;
|
||||||
|
applyLayout();
|
||||||
|
saveState(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateToggleBtns() {
|
||||||
|
$$('.js-art-toggle').forEach(function (b) {
|
||||||
|
var c = b.getAttribute('data-col');
|
||||||
|
var visible = (c === 'title') ? st.t1 : st.t2;
|
||||||
|
b.classList.toggle('active', visible);
|
||||||
|
var inSmall = isSmall();
|
||||||
|
b.title = (inSmall
|
||||||
|
? (visible ? '收起' : '展开') + ((c === 'title') ? '文章标题栏' : '段落目录')
|
||||||
|
: '显示 / 隐藏' + ((c === 'title') ? '文章标题栏' : '段落目录'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindToggles() {
|
||||||
|
document.addEventListener('click', function (ev) {
|
||||||
|
var b = ev.target && ev.target.closest ? ev.target.closest('.js-art-toggle') : null;
|
||||||
|
if (!b) return;
|
||||||
|
var c = b.getAttribute('data-col');
|
||||||
|
if (c !== 'title' && c !== 'toc') return;
|
||||||
|
var cur = (c === 'title') ? st.t1 : st.t2;
|
||||||
|
setColVisible(c, !cur);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 分隔条拖拽 ---------- */
|
||||||
|
function bindGutters() {
|
||||||
|
$$('.js-gutter', frame).forEach(function (g) {
|
||||||
|
g.addEventListener('mousedown', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
var colVar = g.getAttribute('data-var'); // --w-title / --w-toc
|
||||||
|
var isTitle = colVar.indexOf('title') !== -1;
|
||||||
|
var startX = ev.clientX;
|
||||||
|
var frameW = frame.getBoundingClientRect().width || 1;
|
||||||
|
var startW = isTitle ? st.w1 : st.w2;
|
||||||
|
var other = isTitle ? st.w2 : st.w1;
|
||||||
|
var moved = false;
|
||||||
|
|
||||||
|
function onMove(e) {
|
||||||
|
var dx = e.clientX - startX;
|
||||||
|
var pct = startW + (dx / frameW) * 100;
|
||||||
|
var max = 100 - BODY_MIN - other; // 保证正文列至少 BODY_MIN%
|
||||||
|
pct = clampPct(pct, 6, Math.max(6, max));
|
||||||
|
if (Math.abs(pct - startW) > 0.2) moved = true;
|
||||||
|
if (isTitle) { st.w1 = pct; } else { st.w2 = pct; }
|
||||||
|
frame.style.setProperty(colVar, pct + '%');
|
||||||
|
g.classList.add('dragging');
|
||||||
|
}
|
||||||
|
function onUp() {
|
||||||
|
document.removeEventListener('mousemove', onMove);
|
||||||
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
g.classList.remove('dragging');
|
||||||
|
if (moved) {
|
||||||
|
if (isTitle) { st.w1 = clampPct(st.w1, 6, Math.max(6, 100 - BODY_MIN - st.w2)); }
|
||||||
|
else { st.w2 = clampPct(st.w2, 6, Math.max(6, 100 - BODY_MIN - st.w1)); }
|
||||||
|
saveState(st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousemove', onMove);
|
||||||
|
document.addEventListener('mouseup', onUp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Markdown 库 CDN 懒加载(marked v4) ---------- */
|
||||||
|
var MARKED_URLS = [
|
||||||
|
'https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js',
|
||||||
|
'https://unpkg.com/marked@4.3.0/marked.min.js'
|
||||||
|
];
|
||||||
|
var markedPromise = null;
|
||||||
|
|
||||||
|
function loadMarked() {
|
||||||
|
if (global.marked) return Promise.resolve(global.marked);
|
||||||
|
if (markedPromise) return markedPromise;
|
||||||
|
markedPromise = new Promise(function (resolve) {
|
||||||
|
var tryLoad = function (i) {
|
||||||
|
if (global.marked) { resolve(global.marked); return; }
|
||||||
|
if (i >= MARKED_URLS.length) { resolve(null); return; }
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.src = MARKED_URLS[i];
|
||||||
|
s.onload = function () { resolve(global.marked || null); };
|
||||||
|
s.onerror = function () { tryLoad(i + 1); };
|
||||||
|
document.head.appendChild(s);
|
||||||
|
};
|
||||||
|
tryLoad(0);
|
||||||
|
});
|
||||||
|
return markedPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 正文渲染与目录 ---------- */
|
||||||
|
var rawEl = document.getElementById('artRaw');
|
||||||
|
var mdBox = document.getElementById('artMd');
|
||||||
|
var tocNav = $('.js-toc-nav');
|
||||||
|
var tocCol = $('.js-col-toc');
|
||||||
|
|
||||||
|
function fixRelativeUrls() {
|
||||||
|
// 正文图片/链接可能使用 data/articles/img/… 站点相对根路径(相对站点根),
|
||||||
|
// 页面位于 article/ 子目录,渲染后需补 ../ 前缀才能正确指向 data/articles/img/。
|
||||||
|
if (!mdBox) return;
|
||||||
|
var absPrefixes = ['#', 'http:', 'https:', '//', 'mailto:', 'tel:', 'data:', '/', '../'];
|
||||||
|
function apply(attr) {
|
||||||
|
mdBox.querySelectorAll('img, a').forEach(function (el) {
|
||||||
|
var v = el.getAttribute(attr);
|
||||||
|
if (!v) return;
|
||||||
|
var t = v.trim();
|
||||||
|
for (var i = 0; i < absPrefixes.length; i++) {
|
||||||
|
if (t.indexOf(absPrefixes[i]) === 0) return;
|
||||||
|
}
|
||||||
|
// 纯相对路径:视为相对站点根 → 相对当前 article/ 目录需 ../
|
||||||
|
el.setAttribute(attr, '../' + v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
apply('src');
|
||||||
|
apply('href');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderError() {
|
||||||
|
var tpl = document.getElementById('artErrTpl');
|
||||||
|
if (mdBox && tpl) mdBox.innerHTML = tpl.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildToc() {
|
||||||
|
if (!mdBox || !tocNav) return;
|
||||||
|
var heads = $$('#artMd h1, #artMd h2, #artMd h3, #artMd h4, #artMd h5');
|
||||||
|
tocNav.innerHTML = '';
|
||||||
|
if (!heads.length) {
|
||||||
|
tocNav.innerHTML = '<p class="tip" style="padding:10px">本文暂无段落标题</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var frag = document.createDocumentFragment();
|
||||||
|
heads.forEach(function (h, i) {
|
||||||
|
var id = 'art-sec-' + i;
|
||||||
|
h.id = id;
|
||||||
|
var lv = parseInt(h.tagName.charAt(1), 10) || 1;
|
||||||
|
var level = Math.min(4, Math.max(1, lv));
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = '#' + id;
|
||||||
|
a.className = 'art-toc-link lv' + level;
|
||||||
|
a.textContent = h.textContent;
|
||||||
|
a.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var target = document.getElementById(id);
|
||||||
|
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
});
|
||||||
|
frag.appendChild(a);
|
||||||
|
});
|
||||||
|
tocNav.appendChild(frag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderArticle() {
|
||||||
|
if (!rawEl || !mdBox) {
|
||||||
|
if (tocNav) tocNav.innerHTML = '<p class="tip" style="padding:10px">选择左侧任意文章后,此处生成段落目录。</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadMarked().then(function (marked) {
|
||||||
|
if (!marked) { renderError(); return; }
|
||||||
|
var md = rawEl.value || '';
|
||||||
|
var html = marked.parse(md, { gfm: true, breaks: false });
|
||||||
|
mdBox.innerHTML = html;
|
||||||
|
fixRelativeUrls();
|
||||||
|
buildToc();
|
||||||
|
// 高亮标题栏当前项
|
||||||
|
var curId = (global.HP_ART_DATA && global.HP_ART_DATA.curId) ? global.HP_ART_DATA.curId : 0;
|
||||||
|
if (curId) {
|
||||||
|
var link = $('.js-art-nav a[href="?id=' + curId + '"]');
|
||||||
|
if (link) {
|
||||||
|
$$('.js-art-nav a').forEach(function (a) { a.classList.remove('active'); });
|
||||||
|
link.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 初始化 ---------- */
|
||||||
|
function init() {
|
||||||
|
applyLayout();
|
||||||
|
bindToggles();
|
||||||
|
bindGutters();
|
||||||
|
renderArticle();
|
||||||
|
// 窗口尺寸变化(跨断点时重新应用显隐策略)
|
||||||
|
var resizeTimer = null;
|
||||||
|
window.addEventListener('resize', function () {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(function () { applyLayout(); }, 120);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})(window);
|
||||||
BIN
data/homepage.db
BIN
data/homepage.db
Binary file not shown.
@ -95,6 +95,18 @@ function db_init(PDO $pdo): void
|
|||||||
enabled INTEGER NOT NULL DEFAULT 1,
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
sort INTEGER NOT NULL DEFAULT 0
|
sort INTEGER NOT NULL DEFAULT 0
|
||||||
)');
|
)');
|
||||||
|
$pdo->exec('CREATE TABLE IF NOT EXISTS articles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
part TEXT NOT NULL DEFAULT \'未分类\',
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
summary TEXT DEFAULT \'\',
|
||||||
|
markdown TEXT DEFAULT \'\',
|
||||||
|
pinned INTEGER NOT NULL DEFAULT 0,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
sort INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT DEFAULT \'\',
|
||||||
|
updated_at TEXT DEFAULT \'\'
|
||||||
|
)');
|
||||||
|
|
||||||
// 兼容旧库:为已存在的 categories 表补充 icon 列
|
// 兼容旧库:为已存在的 categories 表补充 icon 列
|
||||||
db_ensure_column($pdo, 'categories', 'icon', "TEXT DEFAULT ''");
|
db_ensure_column($pdo, 'categories', 'icon', "TEXT DEFAULT ''");
|
||||||
@ -117,6 +129,13 @@ function db_init(PDO $pdo): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 文章库板块分类(第五大板块,旧库缺失时追加) ----------
|
||||||
|
$hasArticleCat = (int)$pdo->query("SELECT COUNT(*) FROM categories WHERE key = 'article'")->fetchColumn();
|
||||||
|
if ($hasArticleCat === 0) {
|
||||||
|
$stC = $pdo->prepare('INSERT INTO categories (key, name, icon, description, sort) VALUES (?,?,?,?,?)');
|
||||||
|
$stC->execute(['article', '文章库', '📚', '站长原创与整理收录的图文与 Markdown 文章合集。', 5]);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 默认设置种子 ----------
|
// ---------- 默认设置种子 ----------
|
||||||
$hasSettings = (int)$pdo->query('SELECT COUNT(*) FROM settings')->fetchColumn();
|
$hasSettings = (int)$pdo->query('SELECT COUNT(*) FROM settings')->fetchColumn();
|
||||||
if ($hasSettings === 0) {
|
if ($hasSettings === 0) {
|
||||||
@ -133,6 +152,7 @@ function db_init(PDO $pdo): void
|
|||||||
'last_updated' => '',
|
'last_updated' => '',
|
||||||
'hero_bg' => '', // 首页顶部标识栏背景(纯色 / CSS 渐变),空 = 主题默认
|
'hero_bg' => '', // 首页顶部标识栏背景(纯色 / CSS 渐变),空 = 主题默认
|
||||||
'cdn_ranges' => '', // CDN 段库文本;空 = 使用默认内置列表
|
'cdn_ranges' => '', // CDN 段库文本;空 = 使用默认内置列表
|
||||||
|
'article_note' => '', // 文章区首页顶部注要(Markdown 风格,纯文本)
|
||||||
];
|
];
|
||||||
$st = $pdo->prepare('INSERT INTO settings (key, value) VALUES (?,?)');
|
$st = $pdo->prepare('INSERT INTO settings (key, value) VALUES (?,?)');
|
||||||
foreach ($defaults as $k => $v) {
|
foreach ($defaults as $k => $v) {
|
||||||
@ -149,8 +169,7 @@ function db_init(PDO $pdo): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 功能区工具默认种子 ----------
|
// ---------- 功能区工具默认种子 ----------
|
||||||
$toolCount = (int)$pdo->query('SELECT COUNT(*) FROM func_tools')->fetchColumn();
|
$toolCount = (int)$pdo->query('SELECT COUNT(*) FROM func_tools')->fetchColumn(); if ($toolCount === 0) {
|
||||||
if ($toolCount === 0) {
|
|
||||||
$seedTools = [
|
$seedTools = [
|
||||||
['编码 / 加解密', '🔐', 'Base64 / Base32 / URL / Unicode / MD5 / SHA 系列', 'codec.php', 0],
|
['编码 / 加解密', '🔐', 'Base64 / Base32 / URL / Unicode / MD5 / SHA 系列', 'codec.php', 0],
|
||||||
['随机密码生成', '🔑', '自定义字符集与长度,一次生成多组', 'password.php', 0],
|
['随机密码生成', '🔑', '自定义字符集与长度,一次生成多组', 'password.php', 0],
|
||||||
@ -178,6 +197,37 @@ function db_init(PDO $pdo): void
|
|||||||
$insTool->execute([$t[0], $t[1], $t[2], $t[3], $mx + 1]);
|
$insTool->execute([$t[0], $t[1], $t[2], $t[3], $mx + 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 文章库示例文章(首次建库时提供一篇使用说明) ----------
|
||||||
|
$artCount = (int)$pdo->query('SELECT COUNT(*) FROM articles')->fetchColumn();
|
||||||
|
if ($artCount === 0) {
|
||||||
|
$sampleMd = <<<MD
|
||||||
|
## 分区与文章
|
||||||
|
|
||||||
|
后台「文章管理」页可新增、编辑、删除文章,正文使用标准 Markdown 书写,支持分区与置顶。
|
||||||
|
|
||||||
|
## 三栏阅读
|
||||||
|
|
||||||
|
- 左列:全站文章标题栏,按分区展示,可折叠;
|
||||||
|
- 中列:当前文章的段落目录,点击可快速定位;
|
||||||
|
- 右列:文章正文(Markdown 渲染)。
|
||||||
|
|
||||||
|
### 图片引用
|
||||||
|
|
||||||
|
图片文件上传到 data/articles/img 目录,正文中写作:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
渲染时会自动补全站内前缀,无需手工填写完整网址。
|
||||||
|
|
||||||
|
## Markdown 支持
|
||||||
|
|
||||||
|
支持**加粗**、*斜体*、`行内代码`、列表、表格与引用等常规语法。
|
||||||
|
MD;
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$stA = $pdo->prepare('INSERT INTO articles (part, title, summary, markdown, pinned, enabled, sort, created_at, updated_at) VALUES (?,?,?,?,1,1,1,?,?)');
|
||||||
|
$stA->execute(['使用说明', '文章库使用说明', '了解分区、三栏阅读与 Markdown 图片引用的基本用法。', $sampleMd, $now, $now]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 标记内容已更新(首页展示"上次更新时间") */
|
/** 标记内容已更新(首页展示"上次更新时间") */
|
||||||
|
|||||||
@ -40,18 +40,21 @@ $catLink = [
|
|||||||
'red' => 'nav.php?cat=red',
|
'red' => 'nav.php?cat=red',
|
||||||
'blue' => 'nav.php?cat=blue',
|
'blue' => 'nav.php?cat=blue',
|
||||||
'tool' => 'func/index.php',
|
'tool' => 'func/index.php',
|
||||||
|
'article' => 'article/index.php',
|
||||||
];
|
];
|
||||||
$catIcon = [
|
$catIcon = [
|
||||||
'popular' => '📖',
|
'popular' => '📖',
|
||||||
'red' => '⚔️',
|
'red' => '⚔️',
|
||||||
'blue' => '🛡️',
|
'blue' => '🛡️',
|
||||||
'tool' => '🧰',
|
'tool' => '🧰',
|
||||||
|
'article' => '📚',
|
||||||
];
|
];
|
||||||
$catHint = [
|
$catHint = [
|
||||||
'popular' => '入门·科普·前沿',
|
'popular' => '入门·科普·前沿',
|
||||||
'red' => '攻防演练资源',
|
'red' => '攻防演练资源',
|
||||||
'blue' => '防御·应急·溯源',
|
'blue' => '防御·应急·溯源',
|
||||||
'tool' => '进入功能区',
|
'tool' => '进入功能区',
|
||||||
|
'article' => '进入文章库',
|
||||||
];
|
];
|
||||||
|
|
||||||
layout_head('');
|
layout_head('');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user