NewHome/admin/nav.php

323 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
/* ---------- 行内事件代理 ---------- */
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>