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'); - +
- 文章列表(共 篇) - + 新增文章 + 文章列表(共 篇,按分类分组) + + + + 新增文章 +
+

+ 分类即分组:点组头 ▾ 展开 / 收起该分类;按住组头 拖动可调整分类先后顺序(松手即自动保存,前台按此顺序展示)。 + 分类内文章使用 ↑/↓ 微调顺序;前台各分类内按文章最后更新时间倒序展示。 +

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

-
- - - - - - - - - - - - - -
分区标题状态排序操作
- -
-
- 置顶 - 可见隐藏 - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
+
+ $g): ?> +
+
+ + + + +
+
+
+ + + + + + + + + + + + +
标题更新时间状态操作
+ +
+
+ 置顶 + 可见隐藏 + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
-

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

+

分组顺序与前台文章区分区一致;组头默认收起,便于大量文章时分批展开管理。

@@ -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('网络错误,分类顺序未保存。'); }); + } +})(); diff --git a/admin/content.php b/admin/content.php index 0183b6b..70aaac7 100644 --- a/admin/content.php +++ b/admin/content.php @@ -373,7 +373,7 @@ admin_topbar('content'); -
+
@@ -442,7 +442,7 @@ admin_topbar('content'); -
+
@@ -642,7 +642,8 @@ admin_topbar('content'); var row = ev.target.closest('.l-row'); if (!row) return; if (ev.target.classList.contains('l-del')) { - if (confirm('确认删除该快捷站点?')) row.remove(); + (window.HomePage && HomePage.ask ? HomePage.ask('确认删除该快捷站点?', { okText: '删除' }) : Promise.resolve(window.confirm('确认删除该快捷站点?'))) + .then(function (ok) { if (ok) row.remove(); }); } else if (ev.target.classList.contains('l-up') || ev.target.classList.contains('l-down')) { var sib = ev.target.classList.contains('l-up') ? row.previousElementSibling : row.nextElementSibling; if (sib && sib.classList.contains('l-row')) { diff --git a/admin/exttools.php b/admin/exttools.php index 225db74..9c0b7df 100644 --- a/admin/exttools.php +++ b/admin/exttools.php @@ -165,7 +165,7 @@ admin_topbar('exttools'); - + @@ -200,15 +200,31 @@ admin_topbar('exttools'); $isNone = ($gk === "\x00"); $gLabel = $isNone ? '未分组' : $gk; $gData = $isNone ? '__none__' : $gk; + $gN = count($gItems); + $gCnt = ['popular' => 0, 'red' => 0, 'blue' => 0]; + foreach ($gItems as $gt) { + foreach (array_map('trim', explode(',', trim((string)$gt['cat_keys']))) as $ck) { + if (isset($gCnt[$ck])) { $gCnt[$ck]++; } + } + } ?> -
+
+ - + 分区留空的工具在此平铺 + + + 整组库 + + + + +
' + '' + + '' + '' + esc(label) + '' + '' + (key === '__none__' ? '分区留空的工具在此平铺' : '') + + '' + + '整组库' + + '' + + '' + + '' + '
'; rows.appendChild(xg); return xg; @@ -314,6 +336,7 @@ admin_topbar('exttools'); var tmp = document.createElement('div'); tmp.innerHTML = html; xg.querySelector('.xg-b').appendChild(tmp.firstChild); + xg.classList.remove('is-fold'); // 展开目标组,便于查看新增行 } function pruneEmptyGroups() { var xgs = allGroups(); @@ -321,13 +344,41 @@ admin_topbar('exttools'); if (!xgs[i].querySelector('.t-row')) xgs[i].remove(); } } + function syncGroupCat(xg) { + if (!xg) return; + var ks = ['popular', 'red', 'blue']; + var rws = xg.querySelectorAll('.t-row'); + for (var i = 0; i < ks.length; i++) { + var box = xg.querySelector('.g-catc[data-k="' + ks[i] + '"]'); + if (!box) continue; + var on = 0; + for (var j = 0; j < rws.length; j++) { + var rc = rws[j].querySelector('.t-catc[data-k="' + ks[i] + '"]'); + if (rc && rc.checked) on++; + } + box.checked = rws.length > 0 && on === rws.length; + box.indeterminate = on > 0 && on < rws.length; + } + } + function updateFoldLabel() { + var btn = document.getElementById('foldAll'); + if (!btn) return; + var gs = allGroups(); + var anyOpen = false; + for (var i = 0; i < gs.length; i++) { + if (!gs[i].classList.contains('is-fold')) { anyOpen = true; break; } + } + btn.textContent = anyOpen ? '收起全部' : '展开全部'; + } function refreshStats() { var total = rows.querySelectorAll('.t-row').length; Array.prototype.forEach.call(allGroups(), function (xg) { var cnt = xg.querySelectorAll('.t-row').length; var c = xg.querySelector('.xg-cnt'); if (c) c.textContent = cnt + ' 条'; + syncGroupCat(xg); }); + updateFoldLabel(); applyFilter(); var stat = document.getElementById('extStat'); if (stat) stat.textContent = '共 ' + total + ' 条'; @@ -409,30 +460,33 @@ admin_topbar('exttools'); for (var j = 0; j < xgs.length; j++) { xgs[j].classList.toggle('is-fold', anyOpen); } - var btn = document.getElementById('foldAll'); - btn.textContent = anyOpen ? '展开全部' : '收起全部'; + updateFoldLabel(); }); } // ---------- 组头折叠 / 行操作 ---------- rows.addEventListener('click', function (ev) { - var xgh = ev.target.closest('.xg-h'); - if (xgh) { + var t = ev.target; + var xgh = t.closest ? t.closest('.xg-h') : null; + if (xgh && !t.closest('.xg-cat') && !t.closest('.drag-h')) { xgh.parentNode.classList.toggle('is-fold'); + updateFoldLabel(); return; } - var row = ev.target.closest('.t-row'); + var row = t.closest('.t-row'); if (!row) return; - if (ev.target.classList.contains('t-del')) { - if (confirm('确认删除该外部工具?')) { - row.remove(); - pruneEmptyGroups(); - refreshStats(); - } - } else if (ev.target.classList.contains('t-up') || ev.target.classList.contains('t-down')) { - var sib = ev.target.classList.contains('t-up') ? row.previousElementSibling : row.nextElementSibling; + if (t.classList.contains('t-del')) { + (window.HomePage && HomePage.ask ? HomePage.ask('确认删除该外部工具?', { okText: '删除' }) : Promise.resolve(window.confirm('确认删除该外部工具?'))) + .then(function (ok) { + if (!ok) return; + row.remove(); + pruneEmptyGroups(); + refreshStats(); + }); + } else if (t.classList.contains('t-up') || t.classList.contains('t-down')) { + var sib = t.classList.contains('t-up') ? row.previousElementSibling : row.nextElementSibling; if (sib && sib.classList.contains('t-row')) { - if (ev.target.classList.contains('t-up')) row.parentNode.insertBefore(row, sib); + if (t.classList.contains('t-up')) row.parentNode.insertBefore(row, sib); else row.parentNode.insertBefore(sib, row); } } @@ -441,6 +495,20 @@ admin_topbar('exttools'); // 分区/启停/展示范围 等变更 rows.addEventListener('change', function (ev) { var t = ev.target; + if (t.classList.contains('g-catc')) { + // 组级库选择:应用到本组全部行的同库勾选 + var k = t.getAttribute('data-k'); + var xg = t.closest('.xg'); + if (k && xg) { + var target = t.checked; + var rcs = xg.querySelectorAll('.t-catc[data-k="' + k + '"]'); + for (var i = 0; i < rcs.length; i++) { rcs[i].checked = target; } + t.indeterminate = false; + syncGroupCat(xg); + applyFilter(); + } + return; + } if (t.classList.contains('t-enabled')) { var input = t; var row = input.closest('.t-row'); @@ -475,16 +543,57 @@ admin_topbar('exttools'); var row2 = t.closest('.t-row'); var key = t.value.trim() === '' ? '__none__' : t.value.trim(); var xg = ensureGroup(key); + xg.classList.remove('is-fold'); // 展开目标分区 xg.querySelector('.xg-b').appendChild(row2); pruneEmptyGroups(); refreshStats(); return; } if (t.classList.contains('t-catc')) { + syncGroupCat(t.closest('.xg')); applyFilter(); } }); + // ---------- 整组拖动排序(拖动结果随「保存外部工具」一并持久化) ---------- + var dragXg = null; + function clearDrop() { + allGroups().forEach(function (g) { g.classList.remove('drag-src', 'drop-target'); }); + } + rows.addEventListener('dragstart', function (ev) { + var h = ev.target.closest ? ev.target.closest('.drag-h') : null; + if (!h || !rows.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-g') || ''); } catch (e) {} + setTimeout(function () { xg.classList.add('drag-src'); }, 0); + }); + rows.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'); + }); + rows.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; + rows.insertBefore(dragXg, after ? xg.nextSibling : xg); + } + }); + rows.addEventListener('dragend', function () { + clearDrop(); + dragXg = null; + }); + // ---------- 新增 / 批量导入 ---------- document.getElementById('extAdd').addEventListener('click', function () { appendRowTo('', rowHTML('', '', '', '')); diff --git a/admin/nav.php b/admin/nav.php index 74b2c96..6583070 100644 --- a/admin/nav.php +++ b/admin/nav.php @@ -198,8 +198,10 @@ https://www.ithome.com | IT之家">
-
+
+ + @@ -222,7 +224,10 @@ https://www.ithome.com | IT之家">
- +
+ + +
@@ -252,8 +257,10 @@ https://www.ithome.com | IT之家"> var inner = items.map(function (it) { return itemHTML(it.label || '', it.url || '', it.note || ''); }).join(''); - return '
' + + return '
' + '
' + + '' + + '' + '' + '' + '' + @@ -272,9 +279,15 @@ https://www.ithome.com | IT之家"> var target = ev.target; var block = target.closest('.m-block'); if (!block) return; + if (target.classList.contains('c-arr')) { + block.classList.toggle('is-fold'); + updateNavFold(); + return; + } if (target.classList.contains('b-del')) { - if (confirm('确认删除该导航块及其全部链接?')) block.remove(); + (window.HomePage && HomePage.ask ? HomePage.ask('确认删除该导航块及其全部链接?', { okText: '删除块' }) : Promise.resolve(window.confirm('确认删除该导航块及其全部链接?'))) + .then(function (ok) { if (ok) block.remove(); }); return; } if (target.classList.contains('b-up') || target.classList.contains('b-down')) { @@ -297,8 +310,74 @@ https://www.ithome.com | IT之家"> } }); + function updateNavFold() { + var btn = document.getElementById('btnNavFoldAll'); + if (!btn) return; + var bls = wrap.querySelectorAll('.m-block'); + var anyOpen = false; + for (var i = 0; i < bls.length; i++) { + if (!bls[i].classList.contains('is-fold')) { anyOpen = true; break; } + } + btn.textContent = anyOpen ? '收起全部' : '展开全部'; + } + var foldAllBtn = document.getElementById('btnNavFoldAll'); + if (foldAllBtn) { + foldAllBtn.addEventListener('click', function () { + var bls = wrap.querySelectorAll('.m-block'); + var anyOpen = false; + for (var i = 0; i < bls.length; i++) { + if (!bls[i].classList.contains('is-fold')) { anyOpen = true; break; } + } + for (var j = 0; j < bls.length; j++) { bls[j].classList.toggle('is-fold', anyOpen); } + updateNavFold(); + }); + } + + // 整块拖动排序(拖动结果随「保存全部修改」一并持久化) + var dragBlock = null; + function clearBlockDrop() { + var bls = wrap.querySelectorAll('.m-block'); + for (var i = 0; i < bls.length; i++) { bls[i].classList.remove('drag-src', 'drop-target'); } + } + wrap.addEventListener('dragstart', function (ev) { + var h = ev.target.closest ? ev.target.closest('.drag-h') : null; + if (!h || !wrap.contains(h)) return; + var bl = h.closest('.m-block'); + if (!bl) return; + dragBlock = bl; + ev.dataTransfer.effectAllowed = 'move'; + try { ev.dataTransfer.setData('text/plain', bl.getAttribute('data-id') || ''); } catch (e) {} + setTimeout(function () { bl.classList.add('drag-src'); }, 0); + }); + wrap.addEventListener('dragover', function (ev) { + if (!dragBlock) return; + var bl = ev.target.closest ? ev.target.closest('.m-block') : null; + if (!bl || bl === dragBlock) { clearBlockDrop(); return; } + ev.preventDefault(); + ev.dataTransfer.dropEffect = 'move'; + clearBlockDrop(); + bl.classList.add('drop-target'); + }); + wrap.addEventListener('drop', function (ev) { + if (!dragBlock) return; + ev.preventDefault(); + var bl = ev.target.closest ? ev.target.closest('.m-block') : null; + if (bl && bl !== dragBlock) { + var rect = bl.getBoundingClientRect(); + var after = ev.clientY > rect.top + rect.height / 2; + wrap.insertBefore(dragBlock, after ? bl.nextSibling : bl); + } + }); + wrap.addEventListener('dragend', function () { + clearBlockDrop(); + dragBlock = null; + }); + document.getElementById('btnAddBlock').addEventListener('click', function () { wrap.insertAdjacentHTML('beforeend', blockHTML(0, '', [])); + var last = wrap.lastElementChild; + if (last && last.classList.contains('m-block')) { last.classList.remove('is-fold'); } + updateNavFold(); }); /* ---------- 批量导入解析 ---------- */ @@ -406,9 +485,11 @@ https://www.ithome.com | IT之家"> var frag = document.createDocumentFragment(); r.blocks.forEach(function (b) { var div = createElFromHTML(blockHTML(0, b.title, b.items)); + if (div.classList) { div.classList.remove('is-fold'); } frag.appendChild(div); }); wrap.appendChild(frag); + updateNavFold(); var parts = []; parts.push('
已解析 ' + r.parsed + ' 条链接,生成 ' + r.blocks.length + ' 个导航块并追加到下方。
'); if (r.skipped) parts.push('

另有 ' + r.skipped + ' 行未能识别,已跳过。

'); diff --git a/admin/tools.php b/admin/tools.php index 8d79a40..ba17919 100644 --- a/admin/tools.php +++ b/admin/tools.php @@ -163,6 +163,7 @@ admin_topbar('tools');
+ @@ -215,6 +216,7 @@ admin_topbar('tools'); } function rowHTML() { return '
' + + '' + '' + '' + '' + @@ -277,7 +279,8 @@ admin_topbar('tools'); var row = ev.target.closest('.t-row'); if (!row) return; if (ev.target.classList.contains('t-del')) { - if (confirm('确认删除该工具入口?')) row.remove(); + (window.HomePage && HomePage.ask ? HomePage.ask('确认删除该工具入口?', { okText: '删除' }) : Promise.resolve(window.confirm('确认删除该工具入口?'))) + .then(function (ok) { if (ok) row.remove(); }); } else if (ev.target.classList.contains('t-up') || ev.target.classList.contains('t-down')) { var sib = ev.target.classList.contains('t-up') ? row.previousElementSibling : row.nextElementSibling; if (sib && sib.classList.contains('t-row')) { @@ -296,6 +299,47 @@ admin_topbar('tools'); rows.querySelectorAll('.t-ext').forEach(function (sel) { placeholderHint(sel, sel.parentNode.querySelector('.t-url')); }); + + // 行内拖动排序(拖动结果随「保存功能区」一并持久化) + var dragRow = null; + function clearRowDrop() { + var rs = rows.querySelectorAll('.t-row'); + for (var i = 0; i < rs.length; i++) rs[i].classList.remove('drag-src', 'drop-target'); + } + rows.addEventListener('dragstart', function (ev) { + var h = ev.target.closest ? ev.target.closest('.drag-h') : null; + if (!h || !rows.contains(h)) return; + var row = h.closest('.t-row'); + if (!row) return; + dragRow = row; + ev.dataTransfer.effectAllowed = 'move'; + try { ev.dataTransfer.setData('text/plain', row.getAttribute('data-id') || ''); } catch (e) {} + setTimeout(function () { row.classList.add('drag-src'); }, 0); + }); + rows.addEventListener('dragover', function (ev) { + if (!dragRow) return; + var row = ev.target.closest ? ev.target.closest('.t-row') : null; + if (!row || row === dragRow) { clearRowDrop(); return; } + ev.preventDefault(); + ev.dataTransfer.dropEffect = 'move'; + clearRowDrop(); + row.classList.add('drop-target'); + }); + rows.addEventListener('drop', function (ev) { + if (!dragRow) return; + ev.preventDefault(); + var row = ev.target.closest ? ev.target.closest('.t-row') : null; + if (row && row !== dragRow) { + var rect = row.getBoundingClientRect(); + var after = ev.clientY > rect.top + rect.height / 2; + rows.insertBefore(dragRow, after ? row.nextSibling : row); + } + }); + rows.addEventListener('dragend', function () { + clearRowDrop(); + dragRow = null; + }); + document.getElementById('toolsAdd').addEventListener('click', function () { rows.insertAdjacentHTML('beforeend', rowHTML()); }); diff --git a/article/index.php b/article/index.php index bed70aa..e4a39fd 100644 --- a/article/index.php +++ b/article/index.php @@ -13,35 +13,38 @@ 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, pinned, sort, created_at, updated_at - FROM articles WHERE enabled = 1 ORDER BY id ASC')->fetchAll(); -$rows = []; -$partOrder = []; +// 全部可见文章(先按最后更新时间倒序拉取,保证各分区内均为“更新在前”排序) +$all = $pdo->query('SELECT id, part, title, summary, updated_at + FROM articles WHERE enabled = 1 ORDER BY updated_at DESC, id DESC')->fetchAll(); $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]; +// 分区顺序:后台「文章管理」拖出的分区顺序(article_part_order)优先;未配置则按各分区最新更新时间降序 +$poIdx = []; +$poCfg = trim((string)setting_get('article_part_order', '')); +if ($poCfg !== '') { + foreach (explode(',', $poCfg) as $po) { + $po = trim($po); + if ($po !== '' && isset($byPart[$po])) $poIdx[$po] = count($poIdx); + } +} +$partKeys = array_keys($byPart); +usort($partKeys, static function (string $a, string $b) use ($poIdx, $byPart): int { + $ia = $poIdx[$a] ?? PHP_INT_MAX; + $ib = $poIdx[$b] ?? PHP_INT_MAX; + if ($ia !== $ib) return $ia <=> $ib; + // 各分区首篇即最新(SQL 已按 updated_at 倒序) + $d = strcmp((string)$byPart[$b][0]['updated_at'], (string)$byPart[$a][0]['updated_at']); + return $d !== 0 ? $d : strcmp($a, $b); +}); +$rows = []; +foreach ($partKeys as $p) { + $rows[] = ['part' => $p, 'articles' => $byPart[$p]]; } $total = count($all); // 从库首页“文章区”卡片进入阅读时(?from=nav),顶部返回按钮语义切换 diff --git a/assets/css/common.css b/assets/css/common.css index 34857bb..719a5f2 100644 --- a/assets/css/common.css +++ b/assets/css/common.css @@ -131,6 +131,15 @@ select { width: auto; min-width: 150px; } transition: transform .2s ease, background .2s ease; } .theme-fab:hover { transform: scale(1.08); } +/* 返回顶部按钮:与昼夜按钮并排(靠右两枚圆钮,仅滚动后出现) */ +.theme-fab.to-top { + right: 76px; opacity: 0; visibility: hidden; + transition: opacity .2s ease, visibility .2s ease, transform .2s ease; +} +.theme-fab.to-top.show { opacity: 1; visibility: visible; } +@media (max-width: 520px) { + .theme-fab.to-top { right: 74px; } +} /* ============ 通用头部(品牌条 / 顶部条) ============ */ .brand { display: inline-flex; align-items: center; gap: 12px; } @@ -174,27 +183,27 @@ select { width: auto; min-width: 150px; } /* 首页板块区:内容放得下时填满视口剩余高度(不出现竖向滚动条),内容超高时自然滚动 */ .board-section { flex: 1 0 auto; display: flex; flex-direction: column; padding: 26px 0 24px; } .board-section > .wrap { flex: 1; display: flex; flex-direction: column; justify-content: center; } -/* 首页快捷站点条(后台“内容与顺序”配置,点击新标签页打开) */ -.quick-strip { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-bottom: 24px; } -.quick-chip { - position: relative; display: inline-flex; align-items: center; gap: 6px; padding: 8px 18px; border-radius: 999px; - background: var(--card); border: 1px solid var(--line-2); color: var(--text-2); font-size: 14px; - box-shadow: var(--shadow-sm); text-decoration: none; - transition: border-color .15s ease, color .15s ease, transform .12s ease, box-shadow .15s ease; +/* 首页快捷站点:磁贴卡片(圆形图标底 + 名称 + 备注常显,网格对齐) */ +.quick-strip { display: grid; grid-template-columns: repeat(auto-fill, minmax(205px, 1fr)); gap: 12px; margin-bottom: 24px; } +.quick-tile { + display: flex; align-items: center; gap: 12px; padding: 13px 14px; min-width: 0; + background: var(--card); border: 1px solid var(--line); border-radius: 14px; + text-decoration: none; color: var(--text); box-shadow: var(--shadow-sm); + transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease; } -.quick-chip:hover { border-color: var(--brand); color: var(--brand); transform: translateY(-1px); box-shadow: var(--shadow); } -.quick-chip .q-icon { font-size: 15px; line-height: 1; display: inline-flex; align-items: center; } -.quick-chip .icon-img { width: 16px; height: 16px; object-fit: contain; } -/* 快捷站点按钮 hover 备注(data-note) */ -a.quick-chip[data-note]::after { - content: attr(data-note); position: absolute; z-index: 60; bottom: calc(100% + 8px); left: 50%; - transform: translateX(-50%) translateY(4px); width: max-content; max-width: 320px; - background: var(--tooltip-bg); color: var(--tooltip-text); font-size: 12px; line-height: 1.5; - padding: 6px 10px; border-radius: 7px; white-space: normal; word-break: break-word; - text-align: center; opacity: 0; pointer-events: none; box-shadow: var(--shadow); - transition: opacity .14s ease, transform .14s ease; +.quick-tile:hover { transform: translateY(-2px); border-color: var(--brand); box-shadow: var(--shadow); } +.q-tile-ic { + flex: 0 0 42px; width: 42px; height: 42px; border-radius: 12px; + background: var(--accent-soft); display: flex; align-items: center; justify-content: center; + font-size: 20px; line-height: 1; +} +.q-tile-ic .icon-img { width: 22px; height: 22px; object-fit: contain; } +.q-tile-b { min-width: 0; flex: 1; } +.q-tile-b b { display: block; font-size: 14.5px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.q-tile-b small { + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-clamp: 2; + overflow: hidden; color: var(--text-3); font-size: 12px; line-height: 1.5; margin-top: 2px; word-break: break-word; } -a.quick-chip[data-note]:hover::after { opacity: 1; transform: translateX(-50%) translateY(0); } /* 通用图标 (site_icon 输出)在各容器中的尺寸 */ .icon-img { object-fit: contain; vertical-align: middle; } .board-icon img.icon-img { width: 30px; height: 30px; } @@ -423,6 +432,7 @@ a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) tran .m-block-head input.b-title { flex: 1 1 240px; width: auto; max-width: 520px; } .m-block-head .ops { margin-left: auto; display: flex; gap: 6px; align-items: center; } .m-items { padding: 4px 12px 12px; } +.m-block.is-fold .m-items { display: none; } .m-item { display: grid; grid-template-columns: 1fr 1.4fr 1.2fr 36px; gap: 8px; align-items: center; margin-top: 8px; } .m-item .m-del { text-align: center; } /* 后台搜索引擎管理行 */ @@ -453,12 +463,15 @@ a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) tran .l-row .l-note { flex: 1 1 170px; min-width: 120px; } /* 外部工具管理:分区分组折叠 */ .xg { border: 1px dashed var(--line); border-radius: 12px; margin-bottom: 12px; overflow: hidden; } -.xg-h { display: flex; align-items: center; gap: 8px; padding: 8px 14px; background: var(--bg-2); cursor: pointer; user-select: none; } +.xg-h { display: flex; align-items: center; gap: 8px; padding: 8px 14px; background: var(--bg-2); cursor: pointer; user-select: none; flex-wrap: wrap; row-gap: 4px; } .xg-h .xg-arr { flex: 0 0 auto; border: none; background: none; padding: 2px 4px; font-size: 12px; color: var(--text-3); cursor: pointer; transition: transform .18s ease; } .xg.is-fold .xg-arr { transform: rotate(-90deg); } .xg-h .xg-name { font-weight: 600; color: var(--text-2); } .xg-h .xg-cnt { font-size: 12px; color: var(--text-3); } -.xg-h .xg-acts { margin-left: auto; display: flex; gap: 6px; align-items: center; } +.xg-h .xg-acts { margin-left: auto; display: flex; gap: 8px; align-items: center; } +.xg-h .xg-cat .t-cats { padding: 2px 8px; } +.xg-h .xg-cat label { cursor: pointer; } +.xg-h .xg-cat { display: flex; align-items: center; min-width: 0; flex: 0 0 auto; margin-left: auto; } .xg-b { padding: 4px 14px 10px; } .xg-b .t-row:first-child { margin-top: 8px; } .xg.is-fold .xg-b { display: none; } @@ -469,6 +482,18 @@ a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) tran .ext-filter select { width: auto; min-width: 130px; flex: 0 1 auto; } .ext-filter #fKw { flex: 1 1 220px; min-width: 180px; } .ext-stat { font-size: 13px; color: var(--text-3); white-space: nowrap; margin-left: auto; } +/* 通用拖动手柄 / 拖拽反馈 */ +.drag-h { + flex: 0 0 auto; cursor: grab; user-select: none; touch-action: none; + border: none; background: none; padding: 4px 6px; font-size: 13px; line-height: 1; color: var(--text-3); +} +.drag-h:hover { color: var(--brand); } +.drag-h:active { cursor: grabbing; } +.drag-src { opacity: .4; } +.drop-target, .drag-over { border-color: var(--brand) !important; box-shadow: 0 0 0 2px var(--accent-soft) !important; } +/* 折叠头箭头(块头/分区头通用) */ +.c-arr { flex: 0 0 auto; border: none; background: none; padding: 2px 6px; font-size: 12px; color: var(--text-3); cursor: pointer; transition: transform .18s ease; } +.is-fold .c-arr { transform: rotate(-90deg); } .t-row .ops, .l-row .ops { display: flex; gap: 6px; flex: 0 0 auto; margin-left: auto; } .t-on { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: var(--text-2); white-space: nowrap; flex: 0 0 auto; } /* 滑动圆条开关:内部 OFF/ON 文本,用于后台行内即时启停 */ @@ -507,6 +532,22 @@ a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) tran .t-cats label input { margin: 0; accent-color: var(--brand); } .add-btn { margin: 6px 0 18px; } +/* 站点风格确认弹窗(回车确认 / Esc 取消) */ +.sc-overlay { + position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; + padding: 20px; background: rgba(0, 0, 0, .45); backdrop-filter: blur(2px); animation: scFade .14s ease; +} +.sc-card { + width: min(420px, 100%); background: var(--card); border: 1px solid var(--line); border-radius: 14px; + box-shadow: 0 18px 50px rgba(0, 0, 0, .25); overflow: hidden; animation: scPop .16s ease; +} +.sc-head { padding: 16px 18px 0; font-size: 15px; font-weight: 700; color: var(--text); } +.sc-body { padding: 8px 18px 4px; font-size: 13.5px; color: var(--text-2); line-height: 1.7; white-space: pre-wrap; word-break: break-word; } +.sc-foot { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 18px 18px; } +.sc-foot .btn { min-width: 76px; } +@keyframes scFade { from { opacity: 0; } to { opacity: 1; } } +@keyframes scPop { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } } + /* 登录页 */ .login-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg-2); padding: 20px; } .login-card { width: 100%; max-width: 380px; background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 30px; box-shadow: var(--shadow); } diff --git a/assets/js/common.js b/assets/js/common.js index 1342641..bb786ce 100644 --- a/assets/js/common.js +++ b/assets/js/common.js @@ -291,6 +291,95 @@ }); } + /* ---------- 站点风格确认弹窗(回车确认 / Esc 取消 / 点击遮罩取消) ---------- */ + function escHtml(s) { + return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + function siteConfirm(message, opts) { + opts = opts || {}; + return new Promise(function (resolve) { + var danger = opts.danger !== false; + var okText = opts.okText || (danger ? '删除' : '确认'); + var overlay = document.createElement('div'); + overlay.className = 'sc-overlay'; + overlay.innerHTML = + '
' + + '
' + escHtml(opts.title || '操作确认') + '
' + + '
' + escHtml(message || '') + '
' + + '
' + + '' + + '' + + '
'; + var okBtn = overlay.querySelector('.sc-ok'); + var cancelBtn = overlay.querySelector('.sc-cancel'); + var finished = false; + function close(val) { + if (finished) return; + finished = true; + document.removeEventListener('keydown', onKey, true); + if (overlay.parentNode) overlay.parentNode.removeChild(overlay); + resolve(val); + } + function onKey(ev) { + if (ev.key === 'Escape') { ev.preventDefault(); close(false); } + else if (ev.key === 'Enter') { ev.preventDefault(); close(true); } + } + okBtn.addEventListener('click', function () { close(true); }); + cancelBtn.addEventListener('click', function () { close(false); }); + overlay.addEventListener('mousedown', function (ev) { if (ev.target === overlay) close(false); }); + document.addEventListener('keydown', onKey, true); + document.body.appendChild(overlay); + okBtn.focus(); + }); + } + // 统一入口:旧浏览器/异常时回落原生 confirm(仍然以 Promise 风格使用) + function askConfirm(message, opts) { + return siteConfirm(message, opts); + } + // 表单:带 data-confirm="提示文案" 的表单提交前弹窗确认 + function bindConfirmForms() { + document.addEventListener('submit', function (ev) { + var f = ev.target; + if (!f || !f.getAttribute || !f.hasAttribute('data-confirm')) return; + ev.preventDefault(); + siteConfirm(f.getAttribute('data-confirm')).then(function (ok) { if (ok) f.submit(); }); + }); + } + + /* ---------- 返回顶部(右下角按钮,滚动后出现;兼容内滚容器页面) ---------- */ + function initToTop() { + var btn = document.querySelector('.js-to-top'); + if (!btn) return; + var scrollables = []; + var doc = document.scrollingElement || document.documentElement; + if (doc) scrollables.push(doc); + Array.prototype.forEach.call(document.querySelectorAll('*'), function (el) { + if (el === doc || el === btn) return; + if (el.scrollHeight <= el.clientHeight + 40) return; + var cs; + try { cs = window.getComputedStyle(el); } catch (e) { cs = null; } + if (!cs) return; + if (cs.overflowY === 'auto' || cs.overflowY === 'scroll') scrollables.push(el); + }); + function measure() { + var v = 0; + for (var i = 0; i < scrollables.length; i++) { + if (scrollables[i].scrollTop > v) v = scrollables[i].scrollTop; + } + btn.classList.toggle('show', v > 260); + } + for (var s = 0; s < scrollables.length; s++) { + scrollables[s].addEventListener('scroll', measure, { passive: true }); + } + window.addEventListener('resize', measure); + btn.addEventListener('click', function () { + var root = doc || document.documentElement; + if (root && root.scrollTop > 0) root.scrollTo({ top: 0, behavior: 'smooth' }); + else window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + measure(); + } + /* ---------- 自动初始化 ---------- */ document.addEventListener('DOMContentLoaded', function () { initTheme(); @@ -300,9 +389,12 @@ initSearch(); initIconControls(); initCopyButtons(); + bindConfirmForms(); + initToTop(); }); global.HomePage = { - $: $, $$: $$, copyText: copyText, applyTheme: applyTheme, currentTheme: currentTheme + $: $, $$: $$, copyText: copyText, applyTheme: applyTheme, currentTheme: currentTheme, + confirm: siteConfirm, ask: askConfirm }; })(window); diff --git a/includes/layout.php b/includes/layout.php index bef699c..05725c4 100644 --- a/includes/layout.php +++ b/includes/layout.php @@ -47,11 +47,12 @@ function layout_head(string $title = ''): void echo '' . "\n"; } -/** 昼夜切换浮动按钮 */ +/** 昼夜切换浮动按钮 + 返回顶部按钮(全站页面右下角) */ function layout_theme_fab(): void { echo '' . "\n"; + echo '' . "\n"; } /** 站内 logo 图片(内容区使用,同 favicon) */ diff --git a/index.php b/index.php index 1ddc174..7d14e44 100644 --- a/index.php +++ b/index.php @@ -107,9 +107,12 @@ layout_head(''); $qIcon = $qIcon !== '' ? $qIcon : '🔗'; $qNote = trim((string)($ql['note'] ?? '')); ?> - data-note=""> - - + + + + + +
diff --git a/nav.php b/nav.php index d6b19b3..f04c802 100644 --- a/nav.php +++ b/nav.php @@ -70,13 +70,45 @@ foreach ($pdo->query('SELECT * FROM func_tools WHERE enabled = 1 ORDER BY sort A } } -// —— 文章区数据(仅已被分发到当前库的文章) —— +// —— 文章区数据(仅分发到当前库;按分类分组,分类内按最后更新时间倒序) —— $libArts = []; -foreach ($pdo->query('SELECT id, part, title, summary, pinned, sort, created_at, updated_at, board_cats FROM articles WHERE enabled = 1 ORDER BY pinned DESC, sort ASC, id ASC')->fetchAll() as $a) { +foreach ($pdo->query('SELECT id, part, title, summary, updated_at, board_cats FROM articles WHERE enabled = 1 ORDER BY updated_at DESC, id DESC')->fetchAll() as $a) { if (in_array($catKey, array_map('trim', explode(',', trim((string)($a['board_cats'] ?? '')))), true)) { $libArts[] = $a; } } +// 按分类(part)分组;分类顺序:后台拖出的分区顺序优先,否则按各组最新更新时间降序 +$artGroups = []; +foreach ($libArts as $a) { + $p = trim((string)$a['part']); + if ($p === '') $p = '未分类'; + if (!isset($artGroups[$p])) { + $artGroups[$p] = ['ts' => (string)$a['updated_at'], 'items' => []]; + } + $artGroups[$p]['items'][] = $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 ($a, $b) use ($poIdx, $artGroups) { + $ia = $poIdx[$a] ?? PHP_INT_MAX; + $ib = $poIdx[$b] ?? PHP_INT_MAX; + if ($ia !== $ib) return $ia <=> $ib; + $d = strcmp($artGroups[$b]['ts'], $artGroups[$a]['ts']); + return $d !== 0 ? $d : strcmp($a, $b); +}); +$artGroupOrder = []; +foreach ($artGroups as $p => $g) { + $artGroupOrder[] = ['p' => $p, 'items' => $g['items']]; +} // —— 外部工具区数据(通用或包含当前库) —— $extLinks = []; @@ -282,19 +314,21 @@ layout_head($catName); 前往完整文章库 ›
由后台「文章管理」为文章勾选分发到的库;点击进入阅读页(返回按钮已适配为返回上一页)。
- +
还没有文章分发到本库,可到后台「文章管理」编辑文章并勾选“展示到库首页”
-
- - - -

-

- -
- -
+ +

+ +