添加并更改了一些新特性,优化网页操作逻辑
This commit is contained in:
parent
e09fc8ae6a
commit
c3bfce7afb
@ -139,6 +139,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
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':
|
case 'upload':
|
||||||
// 图片上传(供编辑器 XHR 使用,返回 JSON)
|
// 图片上传(供编辑器 XHR 使用,返回 JSON)
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
@ -177,6 +189,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
$savedFlag = isset($_GET['ok']);
|
$savedFlag = isset($_GET['ok']);
|
||||||
$list = $pdo->query('SELECT * FROM articles ORDER BY part ASC, sort ASC, id ASC')->fetchAll();
|
$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', '');
|
$noteText = setting_get('article_note', '');
|
||||||
|
|
||||||
$editing = false;
|
$editing = false;
|
||||||
@ -289,33 +325,49 @@ admin_topbar('articles');
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文章列表 -->
|
<!-- 文章列表:分类即分组(默认收起,组可拖动排序) -->
|
||||||
<div class="fieldset-card">
|
<div class="fieldset-card">
|
||||||
<div class="field-row" style="align-items:center;justify-content:space-between">
|
<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>
|
<a class="btn btn-primary btn-sm" href="articles.php?new=1">+ 新增文章</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="tip" style="margin-top:6px">
|
||||||
|
分类即分组:点组头 ▾ 展开 / 收起该分类;按住组头 <strong>⠿</strong> 拖动可调整分类先后顺序(松手即自动保存,前台按此顺序展示)。
|
||||||
|
分类内文章使用 ↑/↓ 微调顺序;前台各分类内按文章<strong>最后更新时间</strong>倒序展示。
|
||||||
|
</p>
|
||||||
<?php if (!$list): ?>
|
<?php if (!$list): ?>
|
||||||
<p class="tip" style="margin-top:10px">还没有文章,点击右上角「新增文章」开始写作。</p>
|
<p class="tip" style="margin-top:10px">还没有文章,点击右上角「新增文章」开始写作。</p>
|
||||||
<?php else: ?>
|
<?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">
|
<div style="overflow-x:auto">
|
||||||
<table class="tbl">
|
<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>
|
<tbody>
|
||||||
<?php foreach ($list as $a): ?>
|
<?php foreach ($g['rows'] as $a): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= he((string)$a['part']) ?></td>
|
|
||||||
<td>
|
<td>
|
||||||
<a href="articles.php?edit=<?= (int)$a['id'] ?>"><?= he((string)$a['title']) ?></a>
|
<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>
|
||||||
|
<td style="white-space:nowrap"><?= he((string)$a['updated_at']) ?></td>
|
||||||
<td style="white-space:nowrap">
|
<td style="white-space:nowrap">
|
||||||
<?php if ((int)$a['pinned'] === 1): ?><span class="badge badge-ok">置顶</span><?php endif; ?>
|
<?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; ?>
|
<?php if ((int)$a['enabled'] === 1): ?><span class="badge badge-ok">可见</span><?php else: ?><span class="badge badge-no">隐藏</span><?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td><?= (int)$a['sort'] ?></td>
|
|
||||||
<td style="white-space:nowrap">
|
<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'] ?>">
|
<?= 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>
|
<button type="submit" class="btn btn-sm btn-danger" title="删除">✕</button>
|
||||||
</form>
|
</form>
|
||||||
@ -329,11 +381,11 @@ admin_topbar('articles');
|
|||||||
</form>
|
</form>
|
||||||
<form method="post" style="display:inline">
|
<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'] ?>">
|
<?= 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>
|
||||||
<form method="post" style="display:inline">
|
<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'] ?>">
|
<?= 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>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -341,7 +393,11 @@ admin_topbar('articles');
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p class="tip">同分区内使用 ↑/↓ 调整顺序,置顶文章在列表中始终靠前。</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<p class="tip" style="margin-top:10px">分组顺序与前台文章区分区一致;组头默认收起,便于大量文章时分批展开管理。</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -373,7 +373,7 @@ admin_topbar('content');
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<?php if ($logo !== ''): ?>
|
<?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">
|
<?= 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>
|
<div style="margin-top:8px"><button type="submit" class="btn btn-sm btn-danger">恢复默认 LOGO</button></div>
|
||||||
</form>
|
</form>
|
||||||
@ -442,7 +442,7 @@ admin_topbar('content');
|
|||||||
<button type="submit" class="btn btn-primary">保存搜索引擎</button>
|
<button type="submit" class="btn btn-primary">保存搜索引擎</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" onsubmit="return confirm('确认恢复为默认的 4 个搜索引擎?')">
|
<form method="post" data-confirm="确认恢复为默认的 4 个搜索引擎?当前自定义引擎将全部被覆盖。">
|
||||||
<?= csrf_field() ?><input type="hidden" name="act" value="engines_reset">
|
<?= csrf_field() ?><input type="hidden" name="act" value="engines_reset">
|
||||||
<button type="submit" class="btn btn-sm btn-danger">恢复默认引擎</button>
|
<button type="submit" class="btn btn-sm btn-danger">恢复默认引擎</button>
|
||||||
</form>
|
</form>
|
||||||
@ -642,7 +642,8 @@ admin_topbar('content');
|
|||||||
var row = ev.target.closest('.l-row');
|
var row = ev.target.closest('.l-row');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
if (ev.target.classList.contains('l-del')) {
|
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')) {
|
} 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;
|
var sib = ev.target.classList.contains('l-up') ? row.previousElementSibling : row.nextElementSibling;
|
||||||
if (sib && sib.classList.contains('l-row')) {
|
if (sib && sib.classList.contains('l-row')) {
|
||||||
|
|||||||
@ -165,7 +165,7 @@ admin_topbar('exttools');
|
|||||||
</select>
|
</select>
|
||||||
<input type="text" id="fKw" placeholder="关键词:匹配名称 / 简介 / 网址 / 分区…">
|
<input type="text" id="fKw" placeholder="关键词:匹配名称 / 简介 / 网址 / 分区…">
|
||||||
<button type="button" class="btn btn-sm" id="fClear">清空筛选</button>
|
<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>
|
<span class="ext-stat" id="extStat"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -200,15 +200,31 @@ admin_topbar('exttools');
|
|||||||
$isNone = ($gk === "\x00");
|
$isNone = ($gk === "\x00");
|
||||||
$gLabel = $isNone ? '未分组' : $gk;
|
$gLabel = $isNone ? '未分组' : $gk;
|
||||||
$gData = $isNone ? '__none__' : $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="点击折叠 / 展开">
|
<div class="xg-h" title="点击折叠 / 展开">
|
||||||
<button type="button" class="xg-arr" aria-hidden="true">▾</button>
|
<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-name"><?= he($gLabel) ?></span>
|
||||||
<span class="xg-cnt"><?= count($gItems) ?> 条</span>
|
<span class="xg-cnt"><?= $gN ?> 条</span>
|
||||||
<?php if ($isNone): ?>
|
<?php if ($isNone): ?>
|
||||||
<span class="xg-hint tip">分区留空的工具在此平铺</span>
|
<span class="xg-hint tip">分区留空的工具在此平铺</span>
|
||||||
<?php endif; ?>
|
<?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>
|
||||||
<div class="xg-b">
|
<div class="xg-b">
|
||||||
<?php foreach ($gItems as $t):
|
<?php foreach ($gItems as $t):
|
||||||
@ -297,13 +313,19 @@ admin_topbar('exttools');
|
|||||||
if (cur) return cur;
|
if (cur) return cur;
|
||||||
var label = key === '__none__' ? '未分组' : key;
|
var label = key === '__none__' ? '未分组' : key;
|
||||||
var xg = document.createElement('div');
|
var xg = document.createElement('div');
|
||||||
xg.className = 'xg';
|
xg.className = 'xg is-fold';
|
||||||
xg.setAttribute('data-g', key);
|
xg.setAttribute('data-g', key);
|
||||||
xg.innerHTML = '<div class="xg-h" title="点击折叠 / 展开">' +
|
xg.innerHTML = '<div class="xg-h" title="点击折叠 / 展开">' +
|
||||||
'<button type="button" class="xg-arr" aria-hidden="true">▾</button>' +
|
'<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-name">' + esc(label) + '</span>' +
|
||||||
'<span class="xg-cnt"></span>' +
|
'<span class="xg-cnt"></span>' +
|
||||||
(key === '__none__' ? '<span class="xg-hint tip">分区留空的工具在此平铺</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>';
|
'</div><div class="xg-b"></div>';
|
||||||
rows.appendChild(xg);
|
rows.appendChild(xg);
|
||||||
return xg;
|
return xg;
|
||||||
@ -314,6 +336,7 @@ admin_topbar('exttools');
|
|||||||
var tmp = document.createElement('div');
|
var tmp = document.createElement('div');
|
||||||
tmp.innerHTML = html;
|
tmp.innerHTML = html;
|
||||||
xg.querySelector('.xg-b').appendChild(tmp.firstChild);
|
xg.querySelector('.xg-b').appendChild(tmp.firstChild);
|
||||||
|
xg.classList.remove('is-fold'); // 展开目标组,便于查看新增行
|
||||||
}
|
}
|
||||||
function pruneEmptyGroups() {
|
function pruneEmptyGroups() {
|
||||||
var xgs = allGroups();
|
var xgs = allGroups();
|
||||||
@ -321,13 +344,41 @@ admin_topbar('exttools');
|
|||||||
if (!xgs[i].querySelector('.t-row')) xgs[i].remove();
|
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() {
|
function refreshStats() {
|
||||||
var total = rows.querySelectorAll('.t-row').length;
|
var total = rows.querySelectorAll('.t-row').length;
|
||||||
Array.prototype.forEach.call(allGroups(), function (xg) {
|
Array.prototype.forEach.call(allGroups(), function (xg) {
|
||||||
var cnt = xg.querySelectorAll('.t-row').length;
|
var cnt = xg.querySelectorAll('.t-row').length;
|
||||||
var c = xg.querySelector('.xg-cnt');
|
var c = xg.querySelector('.xg-cnt');
|
||||||
if (c) c.textContent = cnt + ' 条';
|
if (c) c.textContent = cnt + ' 条';
|
||||||
|
syncGroupCat(xg);
|
||||||
});
|
});
|
||||||
|
updateFoldLabel();
|
||||||
applyFilter();
|
applyFilter();
|
||||||
var stat = document.getElementById('extStat');
|
var stat = document.getElementById('extStat');
|
||||||
if (stat) stat.textContent = '共 ' + total + ' 条';
|
if (stat) stat.textContent = '共 ' + total + ' 条';
|
||||||
@ -409,30 +460,33 @@ admin_topbar('exttools');
|
|||||||
for (var j = 0; j < xgs.length; j++) {
|
for (var j = 0; j < xgs.length; j++) {
|
||||||
xgs[j].classList.toggle('is-fold', anyOpen);
|
xgs[j].classList.toggle('is-fold', anyOpen);
|
||||||
}
|
}
|
||||||
var btn = document.getElementById('foldAll');
|
updateFoldLabel();
|
||||||
btn.textContent = anyOpen ? '展开全部' : '收起全部';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 组头折叠 / 行操作 ----------
|
// ---------- 组头折叠 / 行操作 ----------
|
||||||
rows.addEventListener('click', function (ev) {
|
rows.addEventListener('click', function (ev) {
|
||||||
var xgh = ev.target.closest('.xg-h');
|
var t = ev.target;
|
||||||
if (xgh) {
|
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');
|
xgh.parentNode.classList.toggle('is-fold');
|
||||||
|
updateFoldLabel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var row = ev.target.closest('.t-row');
|
var row = t.closest('.t-row');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
if (ev.target.classList.contains('t-del')) {
|
if (t.classList.contains('t-del')) {
|
||||||
if (confirm('确认删除该外部工具?')) {
|
(window.HomePage && HomePage.ask ? HomePage.ask('确认删除该外部工具?', { okText: '删除' }) : Promise.resolve(window.confirm('确认删除该外部工具?')))
|
||||||
|
.then(function (ok) {
|
||||||
|
if (!ok) return;
|
||||||
row.remove();
|
row.remove();
|
||||||
pruneEmptyGroups();
|
pruneEmptyGroups();
|
||||||
refreshStats();
|
refreshStats();
|
||||||
}
|
});
|
||||||
} else if (ev.target.classList.contains('t-up') || ev.target.classList.contains('t-down')) {
|
} else if (t.classList.contains('t-up') || t.classList.contains('t-down')) {
|
||||||
var sib = ev.target.classList.contains('t-up') ? row.previousElementSibling : row.nextElementSibling;
|
var sib = t.classList.contains('t-up') ? row.previousElementSibling : row.nextElementSibling;
|
||||||
if (sib && sib.classList.contains('t-row')) {
|
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);
|
else row.parentNode.insertBefore(sib, row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -441,6 +495,20 @@ admin_topbar('exttools');
|
|||||||
// 分区/启停/展示范围 等变更
|
// 分区/启停/展示范围 等变更
|
||||||
rows.addEventListener('change', function (ev) {
|
rows.addEventListener('change', function (ev) {
|
||||||
var t = ev.target;
|
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')) {
|
if (t.classList.contains('t-enabled')) {
|
||||||
var input = t;
|
var input = t;
|
||||||
var row = input.closest('.t-row');
|
var row = input.closest('.t-row');
|
||||||
@ -475,16 +543,57 @@ admin_topbar('exttools');
|
|||||||
var row2 = t.closest('.t-row');
|
var row2 = t.closest('.t-row');
|
||||||
var key = t.value.trim() === '' ? '__none__' : t.value.trim();
|
var key = t.value.trim() === '' ? '__none__' : t.value.trim();
|
||||||
var xg = ensureGroup(key);
|
var xg = ensureGroup(key);
|
||||||
|
xg.classList.remove('is-fold'); // 展开目标分区
|
||||||
xg.querySelector('.xg-b').appendChild(row2);
|
xg.querySelector('.xg-b').appendChild(row2);
|
||||||
pruneEmptyGroups();
|
pruneEmptyGroups();
|
||||||
refreshStats();
|
refreshStats();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (t.classList.contains('t-catc')) {
|
if (t.classList.contains('t-catc')) {
|
||||||
|
syncGroupCat(t.closest('.xg'));
|
||||||
applyFilter();
|
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 () {
|
document.getElementById('extAdd').addEventListener('click', function () {
|
||||||
appendRowTo('', rowHTML('', '', '', ''));
|
appendRowTo('', rowHTML('', '', '', ''));
|
||||||
|
|||||||
@ -198,8 +198,10 @@ https://www.ithome.com | IT之家"></textarea>
|
|||||||
<div id="blocksWrap">
|
<div id="blocksWrap">
|
||||||
<?php foreach ($blocks as $b):
|
<?php foreach ($blocks as $b):
|
||||||
$items = $itemsMap[(int)$b['id']] ?? []; ?>
|
$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">
|
<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="导航块标题(必填)">
|
<input type="text" class="b-title" value="<?= he((string)$b['title']) ?>" placeholder="导航块标题(必填)">
|
||||||
<span class="ops">
|
<span class="ops">
|
||||||
<button type="button" class="btn btn-sm b-up" title="上移">↑</button>
|
<button type="button" class="btn btn-sm b-up" title="上移">↑</button>
|
||||||
@ -222,7 +224,10 @@ https://www.ithome.com | IT之家"></textarea>
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<button type="button" class="btn btn-primary" id="btnSave">保存全部修改</button>
|
<button type="button" class="btn btn-primary" id="btnSave">保存全部修改</button>
|
||||||
<span class="tip" id="saveTip"></span>
|
<span class="tip" id="saveTip"></span>
|
||||||
@ -252,8 +257,10 @@ https://www.ithome.com | IT之家"></textarea>
|
|||||||
var inner = items.map(function (it) {
|
var inner = items.map(function (it) {
|
||||||
return itemHTML(it.label || '', it.url || '', it.note || '');
|
return itemHTML(it.label || '', it.url || '', it.note || '');
|
||||||
}).join('');
|
}).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">' +
|
'<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="导航块标题(必填)">' +
|
'<input type="text" class="b-title" value="' + escAttr(title) + '" placeholder="导航块标题(必填)">' +
|
||||||
'<span class="ops">' +
|
'<span class="ops">' +
|
||||||
'<button type="button" class="btn btn-sm b-up" title="上移">↑</button>' +
|
'<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 target = ev.target;
|
||||||
var block = target.closest('.m-block');
|
var block = target.closest('.m-block');
|
||||||
if (!block) return;
|
if (!block) return;
|
||||||
|
if (target.classList.contains('c-arr')) {
|
||||||
|
block.classList.toggle('is-fold');
|
||||||
|
updateNavFold();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (target.classList.contains('b-del')) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (target.classList.contains('b-up') || target.classList.contains('b-down')) {
|
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 () {
|
document.getElementById('btnAddBlock').addEventListener('click', function () {
|
||||||
wrap.insertAdjacentHTML('beforeend', blockHTML(0, '', []));
|
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();
|
var frag = document.createDocumentFragment();
|
||||||
r.blocks.forEach(function (b) {
|
r.blocks.forEach(function (b) {
|
||||||
var div = createElFromHTML(blockHTML(0, b.title, b.items));
|
var div = createElFromHTML(blockHTML(0, b.title, b.items));
|
||||||
|
if (div.classList) { div.classList.remove('is-fold'); }
|
||||||
frag.appendChild(div);
|
frag.appendChild(div);
|
||||||
});
|
});
|
||||||
wrap.appendChild(frag);
|
wrap.appendChild(frag);
|
||||||
|
updateNavFold();
|
||||||
var parts = [];
|
var parts = [];
|
||||||
parts.push('<div class="ok-msg">已解析 ' + r.parsed + ' 条链接,生成 ' + r.blocks.length + ' 个导航块并追加到下方。</div>');
|
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>');
|
if (r.skipped) parts.push('<p class="tip" style="margin-top:6px">另有 ' + r.skipped + ' 行未能识别,已跳过。</p>');
|
||||||
|
|||||||
@ -163,6 +163,7 @@ admin_topbar('tools');
|
|||||||
<div id="toolsRows">
|
<div id="toolsRows">
|
||||||
<?php foreach ($tools as $t): ?>
|
<?php foreach ($tools as $t): ?>
|
||||||
<div class="t-row" data-id="<?= (int)$t['id'] ?>">
|
<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-ctl">
|
||||||
<span class="icon-preview"></span>
|
<span class="icon-preview"></span>
|
||||||
<input type="text" class="icon-input t-icon" value="<?= he((string)$t['icon']) ?>" placeholder="图标">
|
<input type="text" class="icon-input t-icon" value="<?= he((string)$t['icon']) ?>" placeholder="图标">
|
||||||
@ -215,6 +216,7 @@ admin_topbar('tools');
|
|||||||
}
|
}
|
||||||
function rowHTML() {
|
function rowHTML() {
|
||||||
return '<div class="t-row" data-id="0">' +
|
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-ctl">' +
|
||||||
'<span class="icon-preview"></span>' +
|
'<span class="icon-preview"></span>' +
|
||||||
'<input type="text" class="icon-input t-icon" value="" placeholder="图标">' +
|
'<input type="text" class="icon-input t-icon" value="" placeholder="图标">' +
|
||||||
@ -277,7 +279,8 @@ admin_topbar('tools');
|
|||||||
var row = ev.target.closest('.t-row');
|
var row = ev.target.closest('.t-row');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
if (ev.target.classList.contains('t-del')) {
|
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')) {
|
} 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;
|
var sib = ev.target.classList.contains('t-up') ? row.previousElementSibling : row.nextElementSibling;
|
||||||
if (sib && sib.classList.contains('t-row')) {
|
if (sib && sib.classList.contains('t-row')) {
|
||||||
@ -296,6 +299,47 @@ admin_topbar('tools');
|
|||||||
rows.querySelectorAll('.t-ext').forEach(function (sel) {
|
rows.querySelectorAll('.t-ext').forEach(function (sel) {
|
||||||
placeholderHint(sel, sel.parentNode.querySelector('.t-url'));
|
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 () {
|
document.getElementById('toolsAdd').addEventListener('click', function () {
|
||||||
rows.insertAdjacentHTML('beforeend', rowHTML());
|
rows.insertAdjacentHTML('beforeend', rowHTML());
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,35 +13,38 @@ require_once dirname(__DIR__) . '/includes/layout.php';
|
|||||||
$pdo = db();
|
$pdo = db();
|
||||||
$noteText = setting_get('article_note', '');
|
$noteText = setting_get('article_note', '');
|
||||||
|
|
||||||
// 全部可见文章(按 id 稳定取回,分区顺序取各分区首篇 id 序;分区内置顶 → sort → id)
|
// 全部可见文章(先按最后更新时间倒序拉取,保证各分区内均为“更新在前”排序)
|
||||||
$all = $pdo->query('SELECT id, part, title, summary, pinned, sort, created_at, updated_at
|
$all = $pdo->query('SELECT id, part, title, summary, updated_at
|
||||||
FROM articles WHERE enabled = 1 ORDER BY id ASC')->fetchAll();
|
FROM articles WHERE enabled = 1 ORDER BY updated_at DESC, id DESC')->fetchAll();
|
||||||
$rows = [];
|
|
||||||
$partOrder = [];
|
|
||||||
$byPart = [];
|
$byPart = [];
|
||||||
foreach ($all as $r) {
|
foreach ($all as $r) {
|
||||||
$part = trim((string)$r['part']);
|
$part = trim((string)$r['part']);
|
||||||
if ($part === '') {
|
if ($part === '') {
|
||||||
$part = '未分类';
|
$part = '未分类';
|
||||||
}
|
}
|
||||||
if (!isset($byPart[$part])) {
|
|
||||||
$byPart[$part] = [];
|
|
||||||
$partOrder[] = $part;
|
|
||||||
}
|
|
||||||
$byPart[$part][] = $r;
|
$byPart[$part][] = $r;
|
||||||
}
|
}
|
||||||
foreach ($partOrder as $part) {
|
// 分区顺序:后台「文章管理」拖出的分区顺序(article_part_order)优先;未配置则按各分区最新更新时间降序
|
||||||
$items = $byPart[$part];
|
$poIdx = [];
|
||||||
usort($items, static function (array $a, array $b): int {
|
$poCfg = trim((string)setting_get('article_part_order', ''));
|
||||||
if ((int)$a['pinned'] !== (int)$b['pinned']) {
|
if ($poCfg !== '') {
|
||||||
return (int)$b['pinned'] - (int)$a['pinned'];
|
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);
|
$total = count($all);
|
||||||
// 从库首页“文章区”卡片进入阅读时(?from=nav),顶部返回按钮语义切换
|
// 从库首页“文章区”卡片进入阅读时(?from=nav),顶部返回按钮语义切换
|
||||||
|
|||||||
@ -131,6 +131,15 @@ select { width: auto; min-width: 150px; }
|
|||||||
transition: transform .2s ease, background .2s ease;
|
transition: transform .2s ease, background .2s ease;
|
||||||
}
|
}
|
||||||
.theme-fab:hover { transform: scale(1.08); }
|
.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; }
|
.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 { 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; }
|
.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-strip { display: grid; grid-template-columns: repeat(auto-fill, minmax(205px, 1fr)); gap: 12px; margin-bottom: 24px; }
|
||||||
.quick-chip {
|
.quick-tile {
|
||||||
position: relative; display: inline-flex; align-items: center; gap: 6px; padding: 8px 18px; border-radius: 999px;
|
display: flex; align-items: center; gap: 12px; padding: 13px 14px; min-width: 0;
|
||||||
background: var(--card); border: 1px solid var(--line-2); color: var(--text-2); font-size: 14px;
|
background: var(--card); border: 1px solid var(--line); border-radius: 14px;
|
||||||
box-shadow: var(--shadow-sm); text-decoration: none;
|
text-decoration: none; color: var(--text); box-shadow: var(--shadow-sm);
|
||||||
transition: border-color .15s ease, color .15s ease, transform .12s ease, box-shadow .15s ease;
|
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-tile:hover { transform: translateY(-2px); border-color: var(--brand); box-shadow: var(--shadow); }
|
||||||
.quick-chip .q-icon { font-size: 15px; line-height: 1; display: inline-flex; align-items: center; }
|
.q-tile-ic {
|
||||||
.quick-chip .icon-img { width: 16px; height: 16px; object-fit: contain; }
|
flex: 0 0 42px; width: 42px; height: 42px; border-radius: 12px;
|
||||||
/* 快捷站点按钮 hover 备注(data-note) */
|
background: var(--accent-soft); display: flex; align-items: center; justify-content: center;
|
||||||
a.quick-chip[data-note]::after {
|
font-size: 20px; line-height: 1;
|
||||||
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;
|
.q-tile-ic .icon-img { width: 22px; height: 22px; object-fit: contain; }
|
||||||
background: var(--tooltip-bg); color: var(--tooltip-text); font-size: 12px; line-height: 1.5;
|
.q-tile-b { min-width: 0; flex: 1; }
|
||||||
padding: 6px 10px; border-radius: 7px; white-space: normal; word-break: break-word;
|
.q-tile-b b { display: block; font-size: 14.5px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
text-align: center; opacity: 0; pointer-events: none; box-shadow: var(--shadow);
|
.q-tile-b small {
|
||||||
transition: opacity .14s ease, transform .14s ease;
|
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 输出)在各容器中的尺寸 */
|
/* 通用图标 <img>(site_icon 输出)在各容器中的尺寸 */
|
||||||
.icon-img { object-fit: contain; vertical-align: middle; }
|
.icon-img { object-fit: contain; vertical-align: middle; }
|
||||||
.board-icon img.icon-img { width: 30px; height: 30px; }
|
.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 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-block-head .ops { margin-left: auto; display: flex; gap: 6px; align-items: center; }
|
||||||
.m-items { padding: 4px 12px 12px; }
|
.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 { 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; }
|
.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; }
|
.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 { 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-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.is-fold .xg-arr { transform: rotate(-90deg); }
|
||||||
.xg-h .xg-name { font-weight: 600; color: var(--text-2); }
|
.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-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 { padding: 4px 14px 10px; }
|
||||||
.xg-b .t-row:first-child { margin-top: 8px; }
|
.xg-b .t-row:first-child { margin-top: 8px; }
|
||||||
.xg.is-fold .xg-b { display: none; }
|
.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 select { width: auto; min-width: 130px; flex: 0 1 auto; }
|
||||||
.ext-filter #fKw { flex: 1 1 220px; min-width: 180px; }
|
.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; }
|
.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-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; }
|
.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 文本,用于后台行内即时启停 */
|
/* 滑动圆条开关:内部 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); }
|
.t-cats label input { margin: 0; accent-color: var(--brand); }
|
||||||
.add-btn { margin: 6px 0 18px; }
|
.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-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); }
|
.login-card { width: 100%; max-width: 380px; background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 30px; box-shadow: var(--shadow); }
|
||||||
|
|||||||
@ -291,6 +291,95 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 站点风格确认弹窗(回车确认 / Esc 取消 / 点击遮罩取消) ---------- */
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s).replace(/&/g, '&').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 =
|
||||||
|
'<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 () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
initTheme();
|
initTheme();
|
||||||
@ -300,9 +389,12 @@
|
|||||||
initSearch();
|
initSearch();
|
||||||
initIconControls();
|
initIconControls();
|
||||||
initCopyButtons();
|
initCopyButtons();
|
||||||
|
bindConfirmForms();
|
||||||
|
initToTop();
|
||||||
});
|
});
|
||||||
|
|
||||||
global.HomePage = {
|
global.HomePage = {
|
||||||
$: $, $$: $$, copyText: copyText, applyTheme: applyTheme, currentTheme: currentTheme
|
$: $, $$: $$, copyText: copyText, applyTheme: applyTheme, currentTheme: currentTheme,
|
||||||
|
confirm: siteConfirm, ask: askConfirm
|
||||||
};
|
};
|
||||||
})(window);
|
})(window);
|
||||||
|
|||||||
@ -47,11 +47,12 @@ function layout_head(string $title = ''): void
|
|||||||
echo '</head><body>' . "\n";
|
echo '</head><body>' . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 昼夜切换浮动按钮 */
|
/** 昼夜切换浮动按钮 + 返回顶部按钮(全站页面右下角) */
|
||||||
function layout_theme_fab(): void
|
function layout_theme_fab(): void
|
||||||
{
|
{
|
||||||
echo '<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式">';
|
echo '<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式">';
|
||||||
echo '<span class="js-theme-icon"></span></button>' . "\n";
|
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) */
|
/** 站内 logo 图片(内容区使用,同 favicon) */
|
||||||
|
|||||||
@ -107,9 +107,12 @@ layout_head('');
|
|||||||
$qIcon = $qIcon !== '' ? $qIcon : '🔗';
|
$qIcon = $qIcon !== '' ? $qIcon : '🔗';
|
||||||
$qNote = trim((string)($ql['note'] ?? ''));
|
$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; ?>>
|
<a class="quick-tile" href="<?= he((string)$ql['url']) ?>" target="_blank" rel="noopener noreferrer" title="<?= he($qNote !== '' ? $qNote : (string)$ql['name']) ?>">
|
||||||
<span class="q-icon"><?= site_icon($qIcon, '') ?></span>
|
<span class="q-tile-ic"><?= site_icon($qIcon, '') ?></span>
|
||||||
<span><?= he((string)$ql['name']) ?></span>
|
<span class="q-tile-b">
|
||||||
|
<b><?= he((string)$ql['name']) ?></b>
|
||||||
|
<?php if ($qNote !== ''): ?><small><?= he($qNote) ?></small><?php endif; ?>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
46
nav.php
46
nav.php
@ -70,13 +70,45 @@ foreach ($pdo->query('SELECT * FROM func_tools WHERE enabled = 1 ORDER BY sort A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— 文章区数据(仅已被分发到当前库的文章) ——
|
// —— 文章区数据(仅分发到当前库;按分类分组,分类内按最后更新时间倒序) ——
|
||||||
$libArts = [];
|
$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)) {
|
if (in_array($catKey, array_map('trim', explode(',', trim((string)($a['board_cats'] ?? '')))), true)) {
|
||||||
$libArts[] = $a;
|
$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 = [];
|
$extLinks = [];
|
||||||
@ -282,19 +314,21 @@ layout_head($catName);
|
|||||||
<a class="lnx-go" href="article/index.php">前往完整文章库 ›</a>
|
<a class="lnx-go" href="article/index.php">前往完整文章库 ›</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="lnx-note st-tip">由后台「文章管理」为文章勾选分发到的库;点击进入阅读页(返回按钮已适配为返回上一页)。</div>
|
<div class="lnx-note st-tip">由后台「文章管理」为文章勾选分发到的库;点击进入阅读页(返回按钮已适配为返回上一页)。</div>
|
||||||
<?php if (!$libArts): ?>
|
<?php if (!$artGroupOrder): ?>
|
||||||
<div class="lnx-empty">还没有文章分发到本库<span class="st-tip">,可到后台「文章管理」编辑文章并勾选“展示到库首页”</span>。</div>
|
<div class="lnx-empty">还没有文章分发到本库<span class="st-tip">,可到后台「文章管理」编辑文章并勾选“展示到库首页”</span>。</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
<?php foreach ($artGroupOrder as $ag): ?>
|
||||||
|
<h4 class="lnx-eg"><?= he($ag['p']) ?></h4>
|
||||||
<div class="lnx-arts">
|
<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) ?>">
|
<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>
|
<h4><?= he((string)$ak['title']) ?></h4>
|
||||||
<p><?= he(trim((string)$ak['summary']) !== '' ? (string)$ak['summary'] : '(无摘要)') ?></p>
|
<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>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user