323 lines
14 KiB
PHP
323 lines
14 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): ?>
|
||
<div class="ok-msg">保存成功,前台页面已同步更新。</div>
|
||
<?php endif; ?>
|
||
<?php if ($errorMsg !== ''): ?>
|
||
<div class="err-msg"><?= he($errorMsg) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<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" data-id="<?= (int)$b['id'] ?>">
|
||
<div class="m-block-head">
|
||
<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>
|
||
|
||
<button type="button" class="btn btn-sm add-btn" id="btnAddBlock">+ 新建导航块</button>
|
||
<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" data-id="' + (id || 0) + '">' +
|
||
'<div class="m-block-head">' +
|
||
'<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('b-del')) {
|
||
if (confirm('确认删除该导航块及其全部链接?')) 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();
|
||
}
|
||
});
|
||
|
||
document.getElementById('btnAddBlock').addEventListener('click', function () {
|
||
wrap.insertAdjacentHTML('beforeend', blockHTML(0, '', []));
|
||
});
|
||
|
||
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>
|