添加并更改了一些新特性,优化网页操作逻辑

This commit is contained in:
MasonLiu 2026-09-09 01:30:24 +08:00
parent e09fc8ae6a
commit c3bfce7afb
11 changed files with 704 additions and 132 deletions

View File

@ -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,33 +325,49 @@ admin_topbar('articles');
</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>
<span class="fs-title" style="margin:0;border:none;padding:0">文章列表(共 <?= count($list) ?> 篇,按分类分组)</span>
<span style="display:inline-flex;gap:8px;align-items:center">
<button type="button" class="btn btn-sm" id="artFoldAll" title="折叠 / 展开所有分类">展开全部</button>
<a class="btn btn-primary btn-sm" href="articles.php?new=1">+ 新增文章</a>
</span>
</div>
<p class="tip" style="margin-top:6px">
分类即分组:点组头 展开 / 收起该分类;按住组头 <strong></strong> 拖动可调整分类先后顺序(松手即自动保存,前台按此顺序展示)。
分类内文章使用 / 微调顺序;前台各分类内按文章<strong>最后更新时间</strong>倒序展示。
</p>
<?php if (!$list): ?>
<p class="tip" style="margin-top:10px">还没有文章,点击右上角「新增文章」开始写作。</p>
<?php else: ?>
<div id="artGroups" style="margin-top:12px">
<?php foreach ($artGroups as $gPart => $g): ?>
<div class="xg is-fold" data-part="<?= he($gPart) ?>">
<div class="xg-h" title="点击展开 / 收起该分类">
<button type="button" class="c-arr" aria-hidden="true"></button>
<button type="button" class="drag-h" title="按住拖动,调整该分类先后顺序" draggable="true"></button>
<span class="xg-name"><?= he($gPart) ?></span>
<span class="xg-cnt"><?= count($g['rows']) ?> 篇</span>
</div>
<div class="xg-b">
<div style="overflow-x:auto">
<table class="tbl">
<thead><tr><th>分区</th><th>标题</th><th>状态</th><th>排序</th><th>操作</th></tr></thead>
<thead><tr><th>标题</th><th>更新时间</th><th>状态</th><th>操作</th></tr></thead>
<tbody>
<?php foreach ($list as $a): ?>
<?php foreach ($g['rows'] 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>
<?php if (trim((string)$a['summary']) !== ''): ?><div class="tip"><?= he((string)$a['summary']) ?></div><?php endif; ?>
</td>
<td style="white-space:nowrap"><?= he((string)$a['updated_at']) ?></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('确认删除该文章?')">
<form method="post" style="display:inline" data-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>
@ -329,11 +381,11 @@ admin_topbar('articles');
</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>
<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>
<button type="submit" class="btn btn-sm" title="本分类内下移"></button>
</form>
</td>
</tr>
@ -341,7 +393,11 @@ admin_topbar('articles');
</tbody>
</table>
</div>
<p class="tip">同分区内使用 / 调整顺序,置顶文章在列表中始终靠前。</p>
</div>
</div>
<?php endforeach; ?>
</div>
<p class="tip" style="margin-top:10px">分组顺序与前台文章区分区一致;组头默认收起,便于大量文章时分批展开管理。</p>
<?php endif; ?>
</div>
</div>
@ -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('网络错误,分类顺序未保存。'); });
}
})();
</script>
</body>
</html>

View File

@ -373,7 +373,7 @@ admin_topbar('content');
</div>
</form>
<?php if ($logo !== ''): ?>
<form method="post" onsubmit="return confirm('确认恢复默认 LOGO')">
<form method="post" data-confirm="确认恢复默认 LOGO当前自定义图标将被移除。">
<?= csrf_field() ?><input type="hidden" name="act" value="logo_clear">
<div style="margin-top:8px"><button type="submit" class="btn btn-sm btn-danger">恢复默认 LOGO</button></div>
</form>
@ -442,7 +442,7 @@ admin_topbar('content');
<button type="submit" class="btn btn-primary">保存搜索引擎</button>
</div>
</form>
<form method="post" onsubmit="return confirm('确认恢复为默认的 4 个搜索引擎?')">
<form method="post" data-confirm="确认恢复为默认的 4 个搜索引擎?当前自定义引擎将全部被覆盖。">
<?= csrf_field() ?><input type="hidden" name="act" value="engines_reset">
<button type="submit" class="btn btn-sm btn-danger">恢复默认引擎</button>
</form>
@ -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')) {

View File

@ -165,7 +165,7 @@ admin_topbar('exttools');
</select>
<input type="text" id="fKw" placeholder="关键词:匹配名称 / 简介 / 网址 / 分区…">
<button type="button" class="btn btn-sm" id="fClear">清空筛选</button>
<button type="button" class="btn btn-sm" id="foldAll" title="折叠/展开所有分区">收起全部</button>
<button type="button" class="btn btn-sm" id="foldAll" title="折叠/展开所有分区">展开全部</button>
<span class="ext-stat" id="extStat"></span>
</div>
</div>
@ -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]++; }
}
}
?>
<div class="xg" data-g="<?= he($gData) ?>">
<div class="xg is-fold" data-g="<?= he($gData) ?>">
<div class="xg-h" title="点击折叠 / 展开">
<button type="button" class="xg-arr" aria-hidden="true"></button>
<button type="button" class="drag-h" title="按住拖动,调整该分区先后顺序(点保存后生效)" draggable="true"></button>
<span class="xg-name"><?= he($gLabel) ?></span>
<span class="xg-cnt"><?= count($gItems) ?> 条</span>
<span class="xg-cnt"><?= $gN ?> 条</span>
<?php if ($isNone): ?>
<span class="xg-hint tip">分区留空的工具在此平铺</span>
<?php endif; ?>
<span class="xg-cat" title="整组库选择:勾选 / 取消会应用到本分区全部行(部分勾选时显示半选)">
<span class="t-cats">
<span class="t-cats-t">整组库</span>
<label><input type="checkbox" class="g-catc" data-k="popular"<?= ($gN > 0 && $gCnt['popular'] === $gN) ? ' checked' : '' ?>>科普</label>
<label><input type="checkbox" class="g-catc" data-k="red"<?= ($gN > 0 && $gCnt['red'] === $gN) ? ' checked' : '' ?>>红队</label>
<label><input type="checkbox" class="g-catc" data-k="blue"<?= ($gN > 0 && $gCnt['blue'] === $gN) ? ' checked' : '' ?>>蓝队</label>
</span>
</span>
</div>
<div class="xg-b">
<?php foreach ($gItems as $t):
@ -297,13 +313,19 @@ admin_topbar('exttools');
if (cur) return cur;
var label = key === '__none__' ? '未分组' : key;
var xg = document.createElement('div');
xg.className = 'xg';
xg.className = 'xg is-fold';
xg.setAttribute('data-g', key);
xg.innerHTML = '<div class="xg-h" title="点击折叠 / 展开">' +
'<button type="button" class="xg-arr" aria-hidden="true">▾</button>' +
'<button type="button" class="drag-h" title="按住拖动,调整该分区先后顺序(点保存后生效)" draggable="true">⠿</button>' +
'<span class="xg-name">' + esc(label) + '</span>' +
'<span class="xg-cnt"></span>' +
(key === '__none__' ? '<span class="xg-hint tip">分区留空的工具在此平铺</span>' : '') +
'<span class="xg-cat" title="整组库选择:勾选 / 取消会应用到本分区全部行">' +
'<span class="t-cats"><span class="t-cats-t">整组库</span>' +
'<label><input type="checkbox" class="g-catc" data-k="popular">科普</label>' +
'<label><input type="checkbox" class="g-catc" data-k="red">红队</label>' +
'<label><input type="checkbox" class="g-catc" data-k="blue">蓝队</label></span></span>' +
'</div><div class="xg-b"></div>';
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('确认删除该外部工具?')) {
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 (ev.target.classList.contains('t-up') || ev.target.classList.contains('t-down')) {
var sib = ev.target.classList.contains('t-up') ? row.previousElementSibling : row.nextElementSibling;
});
} 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('', '', '', ''));

View File

@ -198,8 +198,10 @@ https://www.ithome.com | IT之家"></textarea>
<div id="blocksWrap">
<?php foreach ($blocks as $b):
$items = $itemsMap[(int)$b['id']] ?? []; ?>
<div class="m-block" data-id="<?= (int)$b['id'] ?>">
<div class="m-block is-fold" data-id="<?= (int)$b['id'] ?>">
<div class="m-block-head">
<button type="button" class="c-arr" title="展开 / 收起该块链接" aria-hidden="true"></button>
<button type="button" class="drag-h" title="按住拖动,调整该块先后顺序" draggable="true"></button>
<input type="text" class="b-title" value="<?= he((string)$b['title']) ?>" placeholder="导航块标题(必填)">
<span class="ops">
<button type="button" class="btn btn-sm b-up" title="上移"></button>
@ -222,7 +224,10 @@ https://www.ithome.com | IT之家"></textarea>
<?php endforeach; ?>
</div>
<button type="button" class="btn btn-sm add-btn" id="btnAddBlock">+ 新建导航块</button>
<div class="field-row" style="margin:8px 0 16px">
<button type="button" class="btn btn-sm" id="btnAddBlock">+ 新建导航块</button>
<button type="button" class="btn btn-sm" id="btnNavFoldAll" title="折叠 / 展开所有块">展开全部</button>
</div>
<div>
<button type="button" class="btn btn-primary" id="btnSave">保存全部修改</button>
<span class="tip" id="saveTip"></span>
@ -252,8 +257,10 @@ https://www.ithome.com | IT之家"></textarea>
var inner = items.map(function (it) {
return itemHTML(it.label || '', it.url || '', it.note || '');
}).join('');
return '<div class="m-block" data-id="' + (id || 0) + '">' +
return '<div class="m-block is-fold" data-id="' + (id || 0) + '">' +
'<div class="m-block-head">' +
'<button type="button" class="c-arr" title="展开 / 收起该块链接" aria-hidden="true">▾</button>' +
'<button type="button" class="drag-h" title="按住拖动,调整该块先后顺序" draggable="true">⠿</button>' +
'<input type="text" class="b-title" value="' + escAttr(title) + '" placeholder="导航块标题(必填)">' +
'<span class="ops">' +
'<button type="button" class="btn btn-sm b-up" title="上移">↑</button>' +
@ -272,9 +279,15 @@ https://www.ithome.com | IT之家"></textarea>
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之家"></textarea>
}
});
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之家"></textarea>
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('<div class="ok-msg">已解析 ' + r.parsed + ' 条链接,生成 ' + r.blocks.length + ' 个导航块并追加到下方。</div>');
if (r.skipped) parts.push('<p class="tip" style="margin-top:6px">另有 ' + r.skipped + ' 行未能识别,已跳过。</p>');

View File

@ -163,6 +163,7 @@ admin_topbar('tools');
<div id="toolsRows">
<?php foreach ($tools as $t): ?>
<div class="t-row" data-id="<?= (int)$t['id'] ?>">
<button type="button" class="drag-h" draggable="true" title="按住拖动调整顺序"></button>
<span class="icon-ctl">
<span class="icon-preview"></span>
<input type="text" class="icon-input t-icon" value="<?= he((string)$t['icon']) ?>" placeholder="图标">
@ -215,6 +216,7 @@ admin_topbar('tools');
}
function rowHTML() {
return '<div class="t-row" data-id="0">' +
'<button type="button" class="drag-h" draggable="true" title="按住拖动调整顺序">⠿</button>' +
'<span class="icon-ctl">' +
'<span class="icon-preview"></span>' +
'<input type="text" class="icon-input t-icon" value="" placeholder="图标">' +
@ -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());
});

View File

@ -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'];
// 分区顺序后台「文章管理」拖出的分区顺序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);
}
if ((int)$a['sort'] !== (int)$b['sort']) {
return (int)$a['sort'] <=> (int)$b['sort'];
}
return (int)$a['id'] <=> (int)$b['id'];
$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[] = ['part' => $part, 'articles' => $items];
$rows = [];
foreach ($partKeys as $p) {
$rows[] = ['part' => $p, 'articles' => $byPart[$p]];
}
$total = count($all);
// 从库首页“文章区”卡片进入阅读时(?from=nav顶部返回按钮语义切换

View File

@ -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); }
/* 通用图标 <img>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); }

View File

@ -291,6 +291,95 @@
});
}
/* ---------- 站点风格确认弹窗(回车确认 / Esc 取消 / 点击遮罩取消) ---------- */
function escHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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 =
'<div class="sc-card" role="alertdialog" aria-modal="true">' +
'<div class="sc-head">' + escHtml(opts.title || '操作确认') + '</div>' +
'<div class="sc-body">' + escHtml(message || '') + '</div>' +
'<div class="sc-foot">' +
'<button type="button" class="btn btn-sm sc-cancel">' + escHtml(opts.cancelText || '取消') + '</button>' +
'<button type="button" class="btn btn-sm ' + (danger ? 'btn-danger' : 'btn-primary') + ' sc-ok">' + escHtml(okText) + '</button>' +
'</div></div>';
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);

View File

@ -47,11 +47,12 @@ function layout_head(string $title = ''): void
echo '</head><body>' . "\n";
}
/** 昼夜切换浮动按钮 */
/** 昼夜切换浮动按钮 + 返回顶部按钮(全站页面右下角) */
function layout_theme_fab(): void
{
echo '<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式">';
echo '<span class="js-theme-icon"></span></button>' . "\n";
echo '<button type="button" class="theme-fab to-top js-to-top" title="返回顶部" aria-label="返回顶部">↑</button>' . "\n";
}
/** 站内 logo 图片(内容区使用,同 favicon */

View File

@ -107,9 +107,12 @@ layout_head('');
$qIcon = $qIcon !== '' ? $qIcon : '🔗';
$qNote = trim((string)($ql['note'] ?? ''));
?>
<a class="quick-chip" href="<?= he((string)$ql['url']) ?>" target="_blank" rel="noopener noreferrer"<?php if ($qNote !== ''): ?> data-note="<?= he($qNote) ?>"<?php endif; ?>>
<span class="q-icon"><?= site_icon($qIcon, '') ?></span>
<span><?= he((string)$ql['name']) ?></span>
<a class="quick-tile" href="<?= he((string)$ql['url']) ?>" target="_blank" rel="noopener noreferrer" title="<?= he($qNote !== '' ? $qNote : (string)$ql['name']) ?>">
<span class="q-tile-ic"><?= site_icon($qIcon, '') ?></span>
<span class="q-tile-b">
<b><?= he((string)$ql['name']) ?></b>
<?php if ($qNote !== ''): ?><small><?= he($qNote) ?></small><?php endif; ?>
</span>
</a>
<?php endforeach; ?>
</div>

46
nav.php
View File

@ -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);
<a class="lnx-go" href="article/index.php">前往完整文章库 </a>
</div>
<div class="lnx-note st-tip">由后台「文章管理」为文章勾选分发到的库;点击进入阅读页(返回按钮已适配为返回上一页)。</div>
<?php if (!$libArts): ?>
<?php if (!$artGroupOrder): ?>
<div class="lnx-empty">还没有文章分发到本库<span class="st-tip">,可到后台「文章管理」编辑文章并勾选“展示到库首页”</span></div>
<?php else: ?>
<?php foreach ($artGroupOrder as $ag): ?>
<h4 class="lnx-eg"><?= he($ag['p']) ?></h4>
<div class="lnx-arts">
<?php foreach ($libArts as $ak): ?>
<?php foreach ($ag['items'] as $ak): ?>
<a class="lnx-art" href="article/index.php?id=<?= (int)$ak['id'] ?>&from=nav&cat=<?= he($catKey) ?>">
<span class="lnx-ab"><?= he((string)$ak['part']) ?></span>
<h4><?= he((string)$ak['title']) ?></h4>
<p><?= he(trim((string)$ak['summary']) !== '' ? (string)$ak['summary'] : '(无摘要)') ?></p>
<time><?= he((string)$ak['created_at']) ?></time>
<time>更新于 <?= he((string)$ak['updated_at']) ?></time>
</a>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</section>