From c3bfce7afb3c2caed4838b2e6e4fc0c5b568fc23 Mon Sep 17 00:00:00 2001
From: MasonLiu <2857911564@qq.com>
Date: Wed, 9 Sep 2026 01:30:24 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=B9=B6=E6=9B=B4=E6=94=B9?=
=?UTF-8?q?=E4=BA=86=E4=B8=80=E4=BA=9B=E6=96=B0=E7=89=B9=E6=80=A7=EF=BC=8C?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BD=91=E9=A1=B5=E6=93=8D=E4=BD=9C=E9=80=BB?=
=?UTF-8?q?=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
admin/articles.php | 255 ++++++++++++++++++++++++++++++++++--------
admin/content.php | 7 +-
admin/exttools.php | 145 +++++++++++++++++++++---
admin/nav.php | 89 ++++++++++++++-
admin/tools.php | 46 +++++++-
article/index.php | 45 ++++----
assets/css/common.css | 83 ++++++++++----
assets/js/common.js | 94 +++++++++++++++-
includes/layout.php | 3 +-
index.php | 9 +-
nav.php | 60 +++++++---
11 files changed, 704 insertions(+), 132 deletions(-)
diff --git a/admin/articles.php b/admin/articles.php
index 81691c6..f2095f1 100644
--- a/admin/articles.php
+++ b/admin/articles.php
@@ -139,6 +139,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
break;
+ case 'part_order':
+ // 分区(分类)拖动排序后持久化到设置:CSV 顺序
+ header('Content-Type: application/json; charset=utf-8');
+ $parts = [];
+ foreach (explode(',', (string)($_POST['order'] ?? '')) as $po) {
+ $po = trim((string)$po);
+ if ($po !== '' && !in_array($po, $parts, true)) { $parts[] = $po; }
+ }
+ settings_set('article_part_order', implode(',', $parts));
+ touch_last_updated();
+ echo json_encode(['ok' => true], JSON_UNESCAPED_UNICODE);
+ exit;
case 'upload':
// 图片上传(供编辑器 XHR 使用,返回 JSON)
header('Content-Type: application/json; charset=utf-8');
@@ -177,6 +189,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$savedFlag = isset($_GET['ok']);
$list = $pdo->query('SELECT * FROM articles ORDER BY part ASC, sort ASC, id ASC')->fetchAll();
+// —— 文章按分区(分类)分组;分区顺序:后台拖动保存的 article_part_order 优先,其余按各组最新更新时间降序 ——
+$artGroups = [];
+foreach ($list as $a) {
+ $p = trim((string)$a['part']);
+ if ($p === '') { $p = '未分类'; }
+ if (!isset($artGroups[$p])) { $artGroups[$p] = ['ts' => (string)$a['updated_at'], 'rows' => []]; }
+ $artGroups[$p]['rows'][] = $a;
+ if (strcmp((string)$a['updated_at'], $artGroups[$p]['ts']) > 0) { $artGroups[$p]['ts'] = (string)$a['updated_at']; }
+}
+$poIdx = [];
+$poCfg = trim((string)setting_get('article_part_order', ''));
+if ($poCfg !== '') {
+ foreach (explode(',', $poCfg) as $po) {
+ $po = trim($po);
+ if ($po !== '' && isset($artGroups[$po])) { $poIdx[$po] = count($poIdx); }
+ }
+}
+uksort($artGroups, static function ($pa, $pb) use ($poIdx, $artGroups) {
+ $ia = $poIdx[$pa] ?? PHP_INT_MAX;
+ $ib = $poIdx[$pb] ?? PHP_INT_MAX;
+ if ($ia !== $ib) return $ia <=> $ib;
+ $d = strcmp($artGroups[$pb]['ts'], $artGroups[$pa]['ts']);
+ return $d !== 0 ? $d : strcmp($pa, $pb);
+});
$noteText = setting_get('article_note', '');
$editing = false;
@@ -289,59 +325,79 @@ admin_topbar('articles');
-
+
-
文章列表(共 = count($list) ?> 篇)
-
+ 新增文章
+
文章列表(共 = count($list) ?> 篇,按分类分组)
+
+
+ + 新增文章
+
+
+ 分类即分组:点组头 ▾ 展开 / 收起该分类;按住组头 ⠿ 拖动可调整分类先后顺序(松手即自动保存,前台按此顺序展示)。
+ 分类内文章使用 ↑/↓ 微调顺序;前台各分类内按文章最后更新时间倒序展示。
+
还没有文章,点击右上角「新增文章」开始写作。
-
-
- | 分区 | 标题 | 状态 | 排序 | 操作 |
-
-
-
- | = he((string)$a['part']) ?> |
-
- = he((string)$a['title']) ?>
- = he((string)$a['summary']) ?>
- |
-
- 置顶
- 可见隐藏
- |
- = (int)$a['sort'] ?> |
-
-
-
-
-
-
- |
-
-
-
-
+
+ $g): ?>
+
+
+
+
+ = he($gPart) ?>
+ = count($g['rows']) ?> 篇
+
+
+
+
+ | 标题 | 更新时间 | 状态 | 操作 |
+
+
+
+ |
+ = he((string)$a['title']) ?>
+ = he((string)$a['summary']) ?>
+ |
+ = he((string)$a['updated_at']) ?> |
+
+ 置顶
+ 可见隐藏
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
-
同分区内使用 ↑/↓ 调整顺序,置顶文章在列表中始终靠前。
+
分组顺序与前台文章区分区一致;组头默认收起,便于大量文章时分批展开管理。
@@ -439,6 +495,113 @@ admin_topbar('articles');
}
});
})();
+(function () {
+ 'use strict';
+ // 文章列表:分类即分组 —— 组头点击折叠/展开、整组拖动排序并即时持久化 part_order
+ var gWrap = document.getElementById('artGroups');
+ if (!gWrap) return;
+
+ function groups() {
+ return Array.prototype.filter.call(gWrap.children, function (c) { return c.classList.contains('xg'); });
+ }
+ function tip(text, isErr) {
+ var old = document.getElementById('artOrderTip');
+ if (old) old.remove();
+ var d = document.createElement('div');
+ d.id = 'artOrderTip';
+ d.className = isErr ? 'err-msg' : 'ok-msg';
+ d.style.margin = '8px 0';
+ d.textContent = text;
+ gWrap.parentNode.insertBefore(d, gWrap);
+ setTimeout(function () { if (d.parentNode) d.parentNode.removeChild(d); }, 3200);
+ }
+ function syncFoldBtn() {
+ var b = document.getElementById('artFoldAll');
+ if (!b) return;
+ var gs = groups();
+ var allFolded = gs.length > 0 && gs.every(function (g) { return g.classList.contains('is-fold'); });
+ var allOpen = gs.length > 0 && gs.every(function (g) { return !g.classList.contains('is-fold'); });
+ b.textContent = allOpen ? '收起全部' : '展开全部';
+ b.title = allFolded ? '展开全部分类' : (allOpen ? '收起全部分类' : '折叠 / 展开所有分类');
+ }
+ gWrap.addEventListener('click', function (ev) {
+ if (ev.target.classList.contains('drag-h')) return;
+ var xgh = ev.target.closest ? ev.target.closest('.xg-h') : null;
+ if (!xgh || !gWrap.contains(xgh)) return;
+ xgh.parentNode.classList.toggle('is-fold');
+ syncFoldBtn();
+ });
+ var foldAllBtn = document.getElementById('artFoldAll');
+ if (foldAllBtn) {
+ foldAllBtn.addEventListener('click', function () {
+ var gs = groups();
+ var anyOpen = gs.some(function (g) { return !g.classList.contains('is-fold'); });
+ for (var i = 0; i < gs.length; i++) { gs[i].classList.toggle('is-fold', anyOpen); }
+ syncFoldBtn();
+ });
+ }
+
+ // 拖动整个分区组排序(鼠标按住手柄拖动,其他组自适应)
+ var dragXg = null;
+ function clearDrop() {
+ groups().forEach(function (g) { g.classList.remove('drop-target', 'drag-src'); });
+ }
+ gWrap.addEventListener('dragstart', function (ev) {
+ var h = ev.target.closest ? ev.target.closest('.drag-h') : null;
+ if (!h || !gWrap.contains(h)) return;
+ var xg = h.closest('.xg');
+ if (!xg) return;
+ dragXg = xg;
+ ev.dataTransfer.effectAllowed = 'move';
+ try { ev.dataTransfer.setData('text/plain', xg.getAttribute('data-part') || ''); } catch (e) {}
+ setTimeout(function () { xg.classList.add('drag-src'); }, 0);
+ });
+ gWrap.addEventListener('dragover', function (ev) {
+ if (!dragXg) return;
+ var xg = ev.target.closest ? ev.target.closest('.xg') : null;
+ if (!xg || xg === dragXg) {
+ clearDrop();
+ return;
+ }
+ ev.preventDefault();
+ ev.dataTransfer.dropEffect = 'move';
+ clearDrop();
+ xg.classList.add('drop-target');
+ });
+ gWrap.addEventListener('drop', function (ev) {
+ if (!dragXg) return;
+ ev.preventDefault();
+ var xg = ev.target.closest ? ev.target.closest('.xg') : null;
+ if (xg && xg !== dragXg) {
+ var rect = xg.getBoundingClientRect();
+ var after = ev.clientY > rect.top + rect.height / 2;
+ gWrap.insertBefore(dragXg, after ? xg.nextSibling : xg);
+ savePartOrder();
+ }
+ });
+ gWrap.addEventListener('dragend', function () {
+ clearDrop();
+ dragXg = null;
+ });
+
+ function savePartOrder() {
+ var order = groups().map(function (g) { return g.getAttribute('data-part') || ''; })
+ .filter(function (v) { return v !== ''; }).join(',');
+ var csrfEl = document.querySelector('input[name="csrf"]');
+ if (!csrfEl) { alert('找不到安全令牌,请刷新页面重试。'); return; }
+ var fd = new FormData();
+ fd.append('act', 'part_order');
+ fd.append('csrf', csrfEl.value);
+ fd.append('order', order);
+ fetch(window.location.href, { method: 'POST', body: fd, credentials: 'same-origin' })
+ .then(function (res) { return res.json().catch(function () { return { ok: false, msg: '服务器返回异常。' }; }); })
+ .then(function (j) {
+ if (j && j.ok === true) tip('分类顺序已保存,前台文章区已同步更新。');
+ else alert('分类顺序保存失败:' + ((j && j.msg) || '未知错误'));
+ })
+ .catch(function () { alert('网络错误,分类顺序未保存。'); });
+ }
+})();