$r) { if (!is_array($r)) continue; $name = trim((string)($r['name'] ?? '')); $grp = trim(preg_replace('/\s+/', ' ', (string)($r['grp'] ?? ''))); $desc = trim((string)($r['description'] ?? '')); $url = trim((string)($r['url'] ?? '')); $cats = []; if (isset($r['cats']) && is_array($r['cats'])) { foreach ($r['cats'] as $ck) { $s = trim((string)$ck); if (in_array($s, ['popular', 'red', 'blue'], true) && !in_array($s, $cats, true)) { $cats[] = $s; } } } if ($name === '' && $url === '') continue; // 空行忽略 if ($name === '' || $url === '') { $errorMsg = '第 ' . ((int)$i + 1) . ' 行:名称与网址均不能为空。'; break; } if (!preg_match('#^https?://#i', $url)) { $errorMsg = '第 ' . ((int)$i + 1) . ' 行:外部工具需以 http(s):// 开头的完整网址。'; break; } $clean[] = [ 'id' => (int)($r['id'] ?? 0), 'name' => $name, 'grp' => $grp, 'description' => $desc, 'url' => $url, 'cats' => implode(',', $cats), 'enabled' => (((string)($r['enabled'] ?? '1') === '1')) ? 1 : 0, ]; } if ($errorMsg === '') { try { $pdo->beginTransaction(); $oldIds = array_map('intval', array_column($pdo->query('SELECT id FROM ext_links')->fetchAll(), 'id')); $keep = []; foreach ($clean as $c) { if ($c['id'] > 0) $keep[] = $c['id']; } $del = array_values(array_diff($oldIds, $keep)); foreach ($del as $did) { $pdo->prepare('DELETE FROM ext_links WHERE id = ?')->execute([$did]); } $stUpd = $pdo->prepare('UPDATE ext_links SET name=?, url=?, description=?, grp=?, cat_keys=?, enabled=?, sort=? WHERE id=?'); $stIns = $pdo->prepare('INSERT INTO ext_links (name, url, description, grp, cat_keys, enabled, sort) VALUES (?,?,?,?,?,?,?)'); $sort = 0; foreach ($clean as $c) { $sort++; if ($c['id'] > 0 && in_array($c['id'], $oldIds, true)) { $stUpd->execute([$c['name'], $c['url'], $c['description'], $c['grp'], $c['cats'], $c['enabled'], $sort, $c['id']]); } else { $stIns->execute([$c['name'], $c['url'], $c['description'], $c['grp'], $c['cats'], $c['enabled'], $sort]); } } $pdo->commit(); touch_last_updated(); header('Location: exttools.php?ok=1'); exit; } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); $errorMsg = '保存失败:' . $e->getMessage(); } } } } } // ---------- 行内即时启停(单条生效,无需整表保存) ---------- if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'toggle') { header('Content-Type: application/json; charset=utf-8'); if (!csrf_verify()) { echo json_encode(['ok' => false, 'msg' => '安全校验失败,请刷新页面后重试。'], JSON_UNESCAPED_UNICODE); } else { $tid = (int)($_POST['id'] ?? 0); $ten = ((string)($_POST['enabled'] ?? '') === '1') ? 1 : 0; if ($tid > 0) { $pdo->prepare('UPDATE ext_links SET enabled = ? WHERE id = ?')->execute([$ten, $tid]); touch_last_updated(); echo json_encode(['ok' => true]); } else { echo json_encode(['ok' => false, 'msg' => '无效的记录 ID。'], JSON_UNESCAPED_UNICODE); } } exit; } // ---------- 展示(按 sort 排序) ---------- $tools = $pdo->query('SELECT * FROM ext_links ORDER BY sort ASC, id ASC')->fetchAll(); // 供“分区”筛选下拉使用(按出现顺序去重) $grpNames = []; foreach ($tools as $t) { $g = trim((string)($t['grp'] ?? '')); if ($g !== '' && !in_array($g, $grpNames, true)) { $grpNames[] = $g; } } layout_head('外部工具区管理'); admin_topbar('exttools'); ?>

外部工具区管理

链接较多时可先筛选再编辑:按 分区/栏目、启停状态、展示范围、关键词 快速过滤(仅影响当前浏览,不影响保存)。 行首“分区 / 栏目”即前台分组标题(如“合集 / 导航”),留空 = 不分组平铺; 展示范围勾选库,全部不勾选 = 所有库通用

保存成功,库首页外部工具区已同步更新。
批量导入外部工具(快速导入,自动带分区)

数组内可穿插分区标记 {"section":"栏目名"}(其后的工具归入该分区,直至下一分区标记); 工具行为 {"name","url","description"},名称与网址缺失的空行会自动忽略。 点击「解析并追加」后将追加到对应分区下方,核对后统一点「保存外部工具」生效。

$gItems): $isNone = ($gk === "\x00"); $gLabel = $isNone ? '未分组' : $gk; $gData = $isNone ? '__none__' : $gk; $gN = count($gItems); $gCnt = ['popular' => 0, 'red' => 0, 'blue' => 0]; foreach ($gItems as $gt) { foreach (array_map('trim', explode(',', trim((string)$gt['cat_keys']))) as $ck) { if (isset($gCnt[$ck])) { $gCnt[$ck]++; } } } ?>
分区留空的工具在此平铺 整组库
展示于
筛选只是便于浏览,全部行都会随「保存」提交