549 lines
25 KiB
PHP
549 lines
25 KiB
PHP
<?php
|
||
/**
|
||
* admin/nav.php —— 导航内容管理(块/链接增删改查 + 顺序)
|
||
* 完整重同步保存:提交的 JSON 中不存在的老块/老链接将被删除。
|
||
*/
|
||
$P = '../';
|
||
require_once __DIR__ . '/_guard.php';
|
||
|
||
$pdo = db();
|
||
$catKeys = ['popular', 'red', 'blue'];
|
||
$cat = isset($_GET['c']) ? (string)$_GET['c'] : 'popular';
|
||
if (!in_array($cat, $catKeys, true)) {
|
||
$cat = 'popular';
|
||
}
|
||
$stCat = $pdo->prepare('SELECT * FROM categories WHERE key = ? LIMIT 1');
|
||
$stCat->execute([$cat]);
|
||
$catRow = $stCat->fetch();
|
||
if (!$catRow) {
|
||
http_response_code(404);
|
||
die('分类不存在');
|
||
}
|
||
|
||
$saved = isset($_GET['ok']);
|
||
$errorMsg = '';
|
||
|
||
// ---------- 保存处理 ----------
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save') {
|
||
if (!csrf_verify()) {
|
||
$errorMsg = '安全校验失败,请刷新页面重试。';
|
||
} else {
|
||
$raw = (string)($_POST['data'] ?? '');
|
||
$data = json_decode($raw, true);
|
||
if (!is_array($data)) {
|
||
$errorMsg = '提交数据格式错误。';
|
||
} else {
|
||
try {
|
||
$pdo->beginTransaction();
|
||
// 现有块
|
||
$st = $pdo->prepare('SELECT id FROM nav_blocks WHERE cat_id = ?');
|
||
$st->execute([(int)$catRow['id']]);
|
||
$oldBlocks = array_map('intval', array_column($st->fetchAll(), 'id'));
|
||
// 现有链接(按块分组)
|
||
$oldItemsByBlock = [];
|
||
foreach ($oldBlocks as $bid) {
|
||
$st = $pdo->prepare('SELECT id FROM nav_items WHERE block_id = ?');
|
||
$st->execute([$bid]);
|
||
$oldItemsByBlock[$bid] = array_map('intval', array_column($st->fetchAll(), 'id'));
|
||
}
|
||
// 本轮保留的块 id
|
||
$keepBlocks = [];
|
||
$keepItems = [];
|
||
foreach ($data as $b) {
|
||
if (is_array($b) && isset($b['title'])) {
|
||
$id = (int)($b['id'] ?? 0);
|
||
if ($id > 0) $keepBlocks[] = $id;
|
||
foreach (($b['items'] ?? []) as $it) {
|
||
if (is_array($it) && (int)($it['id'] ?? 0) > 0) {
|
||
$keepItems[] = (int)$it['id'];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 删除被移除的块及其链接
|
||
$delBlocks = array_values(array_diff($oldBlocks, $keepBlocks));
|
||
foreach ($delBlocks as $dbid) {
|
||
$pdo->prepare('DELETE FROM nav_items WHERE block_id = ?')->execute([$dbid]);
|
||
$pdo->prepare('DELETE FROM nav_blocks WHERE id = ?')->execute([$dbid]);
|
||
unset($oldItemsByBlock[$dbid]);
|
||
}
|
||
// 删除被移除的链接
|
||
foreach ($oldItemsByBlock as $bid => $ids) {
|
||
$del = array_values(array_diff($ids, $keepItems));
|
||
foreach ($del as $did) {
|
||
$pdo->prepare('DELETE FROM nav_items WHERE id = ?')->execute([$did]);
|
||
}
|
||
}
|
||
// 写回块与链接
|
||
$stInsBlock = $pdo->prepare('INSERT INTO nav_blocks (cat_id, title, sort) VALUES (?,?,?)');
|
||
$stUpdBlock = $pdo->prepare('UPDATE nav_blocks SET title = ?, sort = ? WHERE id = ?');
|
||
$stInsItem = $pdo->prepare('INSERT INTO nav_items (block_id, label, url, note, sort) VALUES (?,?,?,?,?)');
|
||
$stUpdItem = $pdo->prepare('UPDATE nav_items SET label = ?, url = ?, note = ?, sort = ? WHERE id = ? AND block_id = ?');
|
||
$blockSort = 0;
|
||
foreach ($data as $b) {
|
||
if (!is_array($b) || !isset($b['title'])) continue;
|
||
$title = trim((string)$b['title']);
|
||
if ($title === '') continue;
|
||
$bid = (int)($b['id'] ?? 0);
|
||
$blockBelongs = in_array($bid, $oldBlocks, true);
|
||
if ($bid > 0 && $blockBelongs) {
|
||
$stUpdBlock->execute([$title, $blockSort, $bid]);
|
||
} else {
|
||
$stInsBlock->execute([(int)$catRow['id'], $title, $blockSort]);
|
||
$bid = (int)$pdo->lastInsertId();
|
||
}
|
||
$itemSort = 0;
|
||
foreach (($b['items'] ?? []) as $it) {
|
||
if (!is_array($it)) continue;
|
||
$label = trim((string)($it['label'] ?? ''));
|
||
$url = trim((string)($it['url'] ?? ''));
|
||
$note = trim((string)($it['note'] ?? ''));
|
||
if ($label === '' && $url === '') continue;
|
||
$iid = (int)($it['id'] ?? 0);
|
||
if ($iid > 0 && in_array($iid, $oldItemsByBlock[$bid] ?? [], true)) {
|
||
$stUpdItem->execute([$label, $url, $note, $itemSort, $iid, $bid]);
|
||
} else {
|
||
$stInsItem->execute([$bid, $label, $url, $note, $itemSort]);
|
||
}
|
||
$itemSort++;
|
||
}
|
||
$blockSort++;
|
||
}
|
||
$pdo->commit();
|
||
touch_last_updated();
|
||
header('Location: nav.php?c=' . urlencode($cat) . '&ok=1');
|
||
exit;
|
||
} catch (Throwable $e) {
|
||
if ($pdo->inTransaction()) {
|
||
$pdo->rollBack();
|
||
}
|
||
$errorMsg = '保存失败:' . $e->getMessage();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------- 读取现有数据用于展示 ----------
|
||
$st = $pdo->prepare('SELECT * FROM nav_blocks WHERE cat_id = ? ORDER BY sort ASC, id ASC');
|
||
$st->execute([(int)$catRow['id']]);
|
||
$blocks = $st->fetchAll();
|
||
$itemsMap = [];
|
||
if ($blocks) {
|
||
$ids = array_map('intval', array_column($blocks, 'id'));
|
||
$in = implode(',', array_fill(0, count($ids), '?'));
|
||
$st = $pdo->prepare("SELECT * FROM nav_items WHERE block_id IN ($in) ORDER BY sort ASC, id ASC");
|
||
$st->execute($ids);
|
||
foreach ($st->fetchAll() as $it) {
|
||
$itemsMap[(int)$it['block_id']][] = $it;
|
||
}
|
||
}
|
||
|
||
$catNames = [];
|
||
foreach ($catKeys as $ck) {
|
||
$row = $pdo->query("SELECT key, name FROM categories WHERE key = '" . $ck . "'")->fetch();
|
||
$catNames[$ck] = $row['name'] ?? $ck;
|
||
}
|
||
|
||
$csrfVal = csrf_token();
|
||
|
||
layout_head('导航内容管理');
|
||
admin_topbar('nav');
|
||
?>
|
||
<main class="page-main">
|
||
<div class="wrap page-body">
|
||
<h2 style="margin-bottom:6px">导航内容管理</h2>
|
||
<p class="tip">管理 <?= he($catNames[$cat]) ?> 的导航块与链接;块内最多 15 个链接(3 小列 × 5 行),超出部分请拆分到新块。</p>
|
||
|
||
<!-- 分类切换 -->
|
||
<div class="field-row" style="margin:12px 0">
|
||
<?php foreach ($catKeys as $ck): ?>
|
||
<a class="btn <?= $cat === $ck ? 'btn-primary' : '' ?>" href="nav.php?c=<?= he($ck) ?>"><?= he($catNames[$ck]) ?></a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<?php if ($saved): ?>
|
||
<script>window.hpToastMsg = <?= json_encode(['kind' => 'ok', 'text' => '保存成功,前台页面已同步更新。'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||
<?php endif; ?>
|
||
<?php if ($errorMsg !== ''): ?>
|
||
<script>window.hpToastMsg = <?= json_encode(['kind' => 'err', 'text' => $errorMsg], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||
<?php endif; ?>
|
||
|
||
<div class="fieldset-card" style="margin-bottom:16px">
|
||
<div class="fs-title">⚡ 批量导入导航链接(<?= he($catNames[$cat]) ?>)</div>
|
||
<p class="tip">在下面一次粘贴多行内容,点击「解析导入」后会按格式自动生成导航块,并<b>追加到下方编辑器末尾</b>,可再编辑确认后统一点「保存全部修改」。<br>
|
||
每行一条,支持几种写法:
|
||
<code>名称 | 网址 [ | 备注]</code>(也兼容 <code>网址 | 名称</code>)或 <code>[名称](网址)</code> 或 <code>名称 https://网址</code>;以 <code>## 块名</code> 开头的行开启一个新导航块;未写块名则归入“批量导入”。
|
||
单个块最多 15 个链接,超出会自动拆分为 <code>块名 2</code>、<code>块名 3</code> …</p>
|
||
<textarea id="importText" rows="7" spellcheck="false" placeholder="例如:
|
||
## 安全研究
|
||
看雪 | https://www.kanxue.com | 逆向/安全社区
|
||
先知社区 https://xz.aliyun.com
|
||
[FreeBuf](https://www.freebuf.com)
|
||
## 资讯
|
||
https://www.ithome.com | IT之家"></textarea>
|
||
<div class="field-row" style="margin-top:8px">
|
||
<button type="button" class="btn btn-primary" id="btnParseImport">解析并导入</button>
|
||
<button type="button" class="btn" id="btnClearImport">清空输入框</button>
|
||
<span class="tip" style="align-self:center">导入仅是“填入编辑区”,不会立即生效,请务必点击下方的「保存全部修改」。</span>
|
||
</div>
|
||
<div id="importMsg"></div>
|
||
</div>
|
||
|
||
<form method="post" id="navForm" autocomplete="off">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save">
|
||
<input type="hidden" name="cat" value="<?= he($cat) ?>">
|
||
<textarea name="data" id="jsonData" hidden></textarea>
|
||
|
||
<div id="blocksWrap">
|
||
<?php foreach ($blocks as $b):
|
||
$items = $itemsMap[(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>
|
||
<button type="button" class="btn btn-sm b-down" title="下移">↓</button>
|
||
<button type="button" class="btn btn-sm btn-danger b-del" title="删除该块及其链接">删除块</button>
|
||
</span>
|
||
</div>
|
||
<div class="m-items">
|
||
<?php foreach (array_slice($items, 0, 15) as $it): ?>
|
||
<div class="m-item">
|
||
<input type="text" class="i-label" value="<?= he((string)$it['label']) ?>" placeholder="文字(必填)">
|
||
<input type="text" class="i-url" value="<?= he((string)$it['url']) ?>" placeholder="链接,如 https://…(必填)">
|
||
<input type="text" class="i-note" value="<?= he((string)$it['note']) ?>" placeholder="备注(可选,前台悬浮提示)">
|
||
<button type="button" class="btn btn-sm btn-danger i-del" title="删除该链接">✕</button>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<button type="button" class="btn btn-sm b-add-item" style="margin-top:8px">+ 添加链接</button>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<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>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</main>
|
||
|
||
<?php layout_theme_fab(); ?>
|
||
<script src="../assets/js/common.js"></script>
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
var wrap = document.getElementById('blocksWrap');
|
||
var jsonData = document.getElementById('jsonData');
|
||
|
||
/* ---------- 模板 ---------- */
|
||
function itemHTML(label, url, note) {
|
||
return '<div class="m-item">' +
|
||
'<input type="text" class="i-label" value="' + escAttr(label) + '" placeholder="文字(必填)">' +
|
||
'<input type="text" class="i-url" value="' + escAttr(url) + '" placeholder="链接,如 https://…(必填)">' +
|
||
'<input type="text" class="i-note" value="' + escAttr(note) + '" placeholder="备注(可选,前台悬浮提示)">' +
|
||
'<button type="button" class="btn btn-sm btn-danger i-del" title="删除该链接">✕</button>' +
|
||
'</div>';
|
||
}
|
||
function blockHTML(id, title, items) {
|
||
var inner = items.map(function (it) {
|
||
return itemHTML(it.label || '', it.url || '', it.note || '');
|
||
}).join('');
|
||
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>' +
|
||
'<button type="button" class="btn btn-sm b-down" title="下移">↓</button>' +
|
||
'<button type="button" class="btn btn-sm btn-danger b-del" title="删除该块及其链接">删除块</button>' +
|
||
'</span></div>' +
|
||
'<div class="m-items">' + inner + '<button type="button" class="btn btn-sm b-add-item" style="margin-top:8px">+ 添加链接</button></div>' +
|
||
'</div>';
|
||
}
|
||
function escAttr(s) {
|
||
return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
/* ---------- 行内事件代理 ---------- */
|
||
wrap.addEventListener('click', function (ev) {
|
||
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')) {
|
||
(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')) {
|
||
var sib = target.classList.contains('b-up') ? block.previousElementSibling : block.nextElementSibling;
|
||
if (sib && sib.classList.contains('m-block')) {
|
||
if (target.classList.contains('b-up')) wrap.insertBefore(block, sib);
|
||
else wrap.insertBefore(sib, block);
|
||
}
|
||
return;
|
||
}
|
||
if (target.classList.contains('b-add-item')) {
|
||
var list = block.querySelector('.m-items');
|
||
var count = list.querySelectorAll('.m-item').length;
|
||
if (count >= 15) { alert('单个导航块最多 15 个链接,请新建导航块。'); return; }
|
||
list.insertBefore(createElFromHTML(itemHTML('', '', '')), target);
|
||
return;
|
||
}
|
||
if (target.classList.contains('i-del')) {
|
||
target.closest('.m-item').remove();
|
||
}
|
||
});
|
||
|
||
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();
|
||
});
|
||
|
||
/* ---------- 批量导入解析 ---------- */
|
||
var MAX_ITEMS = 15;
|
||
var importTextEl = document.getElementById('importText');
|
||
var importMsg = document.getElementById('importMsg');
|
||
|
||
// 单行 -> item;无法识别返回 null
|
||
function parseImportLine(raw) {
|
||
var s = String(raw).trim();
|
||
if (s === '') return null;
|
||
// 1) | 分隔:名称 | 网址 [ | 备注](也兼容 网址 | 名称)
|
||
if (s.indexOf('|') !== -1) {
|
||
var p = s.split('|').map(function (x) { return x.trim(); });
|
||
var isUrl = function (x) { return /^https?:\/\//i.test(x); };
|
||
var label1 = p[0], url1 = p[1], note1 = p[2] || '';
|
||
if (isUrl(p[0]) && p[1] !== '' && !isUrl(p[1])) {
|
||
// 网址在前、名称在后
|
||
label1 = p[1]; url1 = p[0]; note1 = p[2] || '';
|
||
}
|
||
if (url1 !== '') return { label: label1, url: url1, note: note1 };
|
||
}
|
||
// 2) Markdown:[名称](网址 ["备注"])
|
||
var m = s.match(/^\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+["“]([^"”]*)["”])?\s*\)$/);
|
||
if (m) return { label: m[1].trim(), url: m[2].trim(), note: (m[3] || '').trim() };
|
||
// 3) 名称 https://… 或 名称 https://… 备注
|
||
var m2 = s.match(/^(.*?)(https?:\/\/\S+)(?:\s+(.*))?$/i);
|
||
if (m2 && m2[2]) {
|
||
return { label: (m2[1] || '').trim(), url: m2[2].replace(/[),,。]+$/, ''), note: (m2[3] || '').trim() };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// host 作为缺省名称
|
||
function hostOf(url) {
|
||
try {
|
||
var u = new URL(url);
|
||
return u.hostname.replace(/^www\./, '');
|
||
} catch (e) {
|
||
return url;
|
||
}
|
||
}
|
||
|
||
// 全文 -> [{title, items}]
|
||
function importTextToBlocks(text) {
|
||
var lines = String(text).split(/\r?\n/);
|
||
var blocks = [];
|
||
var cur = null; // 当前块
|
||
var used = {}; // 已使用的标题(同名自动加序号)
|
||
var parsed = 0, skipped = 0;
|
||
|
||
function makeTitle(base) {
|
||
var t = base;
|
||
var n = 1;
|
||
while (used[t]) {
|
||
n++;
|
||
t = base + ' ' + n;
|
||
}
|
||
used[t] = true;
|
||
return t;
|
||
}
|
||
|
||
function ensureBlock() {
|
||
if (cur && cur.items.length < MAX_ITEMS) return;
|
||
var title = makeTitle(cur ? cur.title : '批量导入');
|
||
cur = { title: title, items: [] };
|
||
blocks.push(cur);
|
||
}
|
||
|
||
lines.forEach(function (ln) {
|
||
var s = String(ln).trim();
|
||
if (s === '') return;
|
||
// 块标题行
|
||
var hm = s.match(/^#{1,6}\s+(.+)$/);
|
||
if (hm) {
|
||
var t = hm[1].trim();
|
||
if (t !== '') {
|
||
var t2 = makeTitle(t);
|
||
cur = { title: t2, items: [] };
|
||
blocks.push(cur);
|
||
}
|
||
return;
|
||
}
|
||
var it = parseImportLine(s);
|
||
if (!it) { skipped++; return; }
|
||
if (it.url === '') { skipped++; return; }
|
||
if (it.label === '') it.label = hostOf(it.url);
|
||
ensureBlock();
|
||
cur.items.push(it);
|
||
parsed++;
|
||
});
|
||
return { blocks: blocks, parsed: parsed, skipped: skipped };
|
||
}
|
||
|
||
function bindImport() {
|
||
var btn = document.getElementById('btnParseImport');
|
||
var clearBtn = document.getElementById('btnClearImport');
|
||
if (!btn || !importTextEl || !importMsg) return;
|
||
btn.addEventListener('click', function () {
|
||
var r = importTextToBlocks(importTextEl.value);
|
||
if (!r.parsed) {
|
||
importMsg.innerHTML = '<div class="err-msg">未解析到任何链接。请检查格式,每行至少包含“名称 网址”或“名称 | 网址”。</div>';
|
||
return;
|
||
}
|
||
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>');
|
||
parts.push('<p class="tip">请在下方向下核对;如需调整可修改标题/链接后,统一点击「保存全部修改」生效。</p>');
|
||
importMsg.innerHTML = parts.join('');
|
||
importTextEl.value = '';
|
||
wrap.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
});
|
||
clearBtn.addEventListener('click', function () {
|
||
importTextEl.value = '';
|
||
importMsg.innerHTML = '';
|
||
importTextEl.focus();
|
||
});
|
||
}
|
||
bindImport();
|
||
|
||
function createElFromHTML(html) {
|
||
var t = document.createElement('template');
|
||
t.innerHTML = html.trim();
|
||
return t.content.firstChild;
|
||
}
|
||
|
||
/* ---------- 收集并保存 ---------- */
|
||
document.getElementById('btnSave').addEventListener('click', function () {
|
||
var blocks = Array.prototype.slice.call(wrap.querySelectorAll('.m-block'));
|
||
var data = [];
|
||
var hasInvalid = false;
|
||
|
||
blocks.forEach(function (block) {
|
||
if (hasInvalid) return;
|
||
var title = block.querySelector('.b-title').value.trim();
|
||
if (!title) { alert('存在未填写标题的导航块,请填写或删除。'); hasInvalid = true; return; }
|
||
var items = [];
|
||
Array.prototype.slice.call(block.querySelectorAll('.m-item')).forEach(function (row) {
|
||
var label = row.querySelector('.i-label').value.trim();
|
||
var url = row.querySelector('.i-url').value.trim();
|
||
var note = row.querySelector('.i-note').value.trim();
|
||
if (label === '' && url === '') return; // 空行忽略
|
||
if (label === '' || url === '') {
|
||
alert('链接的“文字”与“链接”不能为空。');
|
||
hasInvalid = true;
|
||
return;
|
||
}
|
||
items.push({ id: 0, label: label, url: url, note: note });
|
||
});
|
||
if (hasInvalid) return;
|
||
data.push({ id: parseInt(block.getAttribute('data-id'), 10) || 0, title: title, items: items });
|
||
});
|
||
if (hasInvalid) return;
|
||
jsonData.value = JSON.stringify(data);
|
||
document.getElementById('navForm').submit();
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|