2026-10-07 05:52:40 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 个人导航 · 管理页(v24 第 24 批 用户第 4 条,从 SimpleNav 的 admin 搬过来)
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 与 SimpleNav 的 admin 的区别(只有一处,但很要紧):
|
|
|
|
|
|
* 原项目是**单用户**系统,admin 直接改全局数据;
|
|
|
|
|
|
* 这里**每人一份**,所有操作都带 `person_id`,且**只能动自己的**。
|
|
|
|
|
|
* 路由层(index.php 的 nav/save)已经把 person_id 焊死在每个动作里 ——
|
|
|
|
|
|
* 本模板只负责把"我自己的"数据摆出来。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 拖拽排序与开关走 AJAX(fetch + X-Requested-With),与 SimpleNav 的 admin.js 同一套;
|
|
|
|
|
|
* 其余表单是普通 POST(保存后 302 回本页,刷一下就看到结果,比 AJAX 简单可靠)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array $gridGroups 我的网格板块
|
|
|
|
|
|
* @param array $tabGroups 我的合集板块
|
|
|
|
|
|
* @param array $allGroups 我的全部板块(下拉用)
|
|
|
|
|
|
* @param array $sitesBy group_id => 站点列表
|
|
|
|
|
|
* @param array $engines 我的搜索引擎
|
|
|
|
|
|
* @param array $navSet 我的导航设置
|
|
|
|
|
|
* @param string $phrase 我的短语(空 = 还没设,展示页不可用)
|
|
|
|
|
|
* @param string $showUrl 展示页完整地址(短语为空时是空串)
|
|
|
|
|
|
* @param string $meAvatar 我的头像地址
|
|
|
|
|
|
*/
|
|
|
|
|
|
$canWrite = \App\Auth::canWrite();
|
|
|
|
|
|
$editSite = null;
|
|
|
|
|
|
$editGroup = null;
|
|
|
|
|
|
$editEngine = null;
|
|
|
|
|
|
// 点「编辑」时用 ?edit_site=12 / edit_group=3 / edit_engine=5 带出既有值
|
|
|
|
|
|
$eSite = (int)($_GET['edit_site'] ?? 0);
|
|
|
|
|
|
$eGrp = (int)($_GET['edit_group'] ?? 0);
|
|
|
|
|
|
$eEng = (int)($_GET['edit_engine'] ?? 0);
|
|
|
|
|
|
foreach ($sitesBy as $gid => $rows) {
|
|
|
|
|
|
foreach ($rows as $r) {
|
|
|
|
|
|
if ((int)$r['id'] === $eSite) { $editSite = $r; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ($allGroups as $g) {
|
|
|
|
|
|
if ((int)$g['id'] === $eGrp) { $editGroup = $g; }
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ($engines as $e) {
|
|
|
|
|
|
if ((int)$e['id'] === $eEng) { $editEngine = $e; }
|
|
|
|
|
|
}
|
|
|
|
|
|
$sc = static fn($k, $d = '') => (string)($navSet[$k] ?? $d);
|
2026-10-08 02:20:19 +08:00
|
|
|
|
/* 🔴 v28 用户第 11 条:「导航模块,右上角增加设置按钮,和其他模块一样。
|
|
|
|
|
|
把导航展示地址,放进去,把导航设置,放进去,把板块设置,放进去,
|
|
|
|
|
|
把搜索引擎的设置,也放进去,你参考一下之前的导航系统 simplenav,
|
|
|
|
|
|
把之前的那些设置,都放在设置中,进来之后,默认就是添加网址,和所有网址的列表。」
|
|
|
|
|
|
|
|
|
|
|
|
⇒ 两种视图,用 `?set=1` 切(服务端渲染,无 JS 也能切;刷新/回退都不丢状态):
|
|
|
|
|
|
· 默认(`?set` 空):**只显示「新增站点」+「站点列表」** —— 日常真正要用的两件事;
|
|
|
|
|
|
· 设置(`?set=1`):展示地址 / 导航设置 / 板块 / 搜索引擎 四块。
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 为什么用查询参数而不是 JS 折叠:设置项里有表单(保存后会 302 回本页),
|
|
|
|
|
|
用 JS 折叠的话每次保存回来都会**自动摊开全部设置**,用户还要再收一次。
|
|
|
|
|
|
用参数就能让"保存后回到设置页"(下面的表单里保留了 `set=1` 的隐藏域)。
|
|
|
|
|
|
⚠️ 保留 `$editSite` 等编辑态:正在编辑某个站点/板块/引擎时,**强制显示设置视图**,
|
|
|
|
|
|
否则用户点"编辑板块"会看到一个没有板块表单的页面(看起来像功能坏了)。 */
|
2026-10-08 10:54:43 +08:00
|
|
|
|
/* 🔴🔴 v29 用户第 5 条:「导航的设置,做成设置按钮,点击后,拉下来设置选项,
|
|
|
|
|
|
分别是地址设置、导航设置、板块设置、搜索引擎设置,把现在的设置页面,拆开,
|
|
|
|
|
|
放在不同的设置选项下。」
|
|
|
|
|
|
⇒ 从"一个大的设置页"改成**下拉菜单 + 四个独立视图**:
|
|
|
|
|
|
· `?set=addr` 地址设置(展示页地址 + 短语)
|
|
|
|
|
|
· `?set=general` 导航设置(标题/搜索引擎/布局等,原 SimpleNav 的 settings)
|
|
|
|
|
|
· `?set=board` 板块设置
|
|
|
|
|
|
· `?set=engine` 搜索引擎设置
|
|
|
|
|
|
· 不带 `set` 默认:新增网址 + 网址列表
|
|
|
|
|
|
⚠️ 为什么拆开(而不只是把四块堆在一个?"设置页"里):
|
|
|
|
|
|
用户点的原话就是"拆开"。而且四块各自是**独立表单**,堆在一页里
|
|
|
|
|
|
每次保存后 302 回来都要重新找自己在哪一块。分开之后
|
|
|
|
|
|
"你在哪一块"由 URL 决定,保存后回到原地,位置感清楚。
|
|
|
|
|
|
⚠️ 用**链接**(服务端渲染)而不是 JS 下拉切换内容:
|
|
|
|
|
|
与 v28 的 `?set=1` 同一理由 —— 设置里有表单,保存会 302 回来,
|
|
|
|
|
|
纯 JS 的展开状态在刷新后就丢了。`<details>` 只负责"菜单展开"这一个动效,
|
|
|
|
|
|
点条目是**真实导航**(URL 变、视图变、刷新/回退都不丢)。
|
|
|
|
|
|
⚠️ 编辑态(正在编辑某板块/某引擎)要落到对应的那个视图里,
|
|
|
|
|
|
否则用户点"编辑板块"会看到一个没有板块表单的页面(看起来像功能坏了)。 */
|
|
|
|
|
|
$setKey = (string)($_GET['set'] ?? '');
|
|
|
|
|
|
if ($editGroup !== null) { $setKey = 'board'; }
|
|
|
|
|
|
if ($editEngine !== null) { $setKey = 'engine'; }
|
|
|
|
|
|
$inSettings = in_array($setKey, ['addr', 'general', 'board', 'engine'], true);
|
|
|
|
|
|
|
|
|
|
|
|
/* 设置项的登记表(菜单与"当前在哪一项"的标题都读它,只有一份) */
|
|
|
|
|
|
$setItems = [
|
|
|
|
|
|
'addr' => ['地址设置', 'M2.5 4h11M2.5 8h7M2.5 12h9'],
|
|
|
|
|
|
'general' => ['导航设置', 'M8 5.5a2.5 2.5 0 100 5 2.5 2.5 0 000-5z M8 1.5v1.6M8 12.9v1.6M1.5 8h1.6M12.9 8h1.6M3.4 3.4l1.1 1.1M11.5 11.5l1.1 1.1M12.6 3.4l-1.1 1.1M4.5 11.5l-1.1 1.1'],
|
|
|
|
|
|
'board' => ['板块设置', 'M2 3.5h4v4H2zM10 3.5h4v4h-4zM2 9.5h4v4H2zM10 9.5h4v4h-4z'],
|
|
|
|
|
|
'engine' => ['搜索引擎设置', 'M7 2.5a4.5 4.5 0 100 9 4.5 4.5 0 000-9z M10.4 10.4L14 14'],
|
|
|
|
|
|
];
|
2026-10-07 05:52:40 +08:00
|
|
|
|
?>
|
|
|
|
|
|
<div class="page-head">
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<h1><?= $inSettings ? e($setItems[$setKey][0]) : '导航' ?></h1>
|
|
|
|
|
|
<span class="sub">
|
2026-10-08 02:20:19 +08:00
|
|
|
|
<?php if ($inSettings): ?>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<a href="<?= e(url('nav')) ?>">← 回到网址列表</a>
|
2026-10-08 02:20:19 +08:00
|
|
|
|
<?php else: ?>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
你个人用的网址导航 · 每人一份
|
2026-10-08 02:20:19 +08:00
|
|
|
|
<?php endif; ?>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
<div class="actions">
|
|
|
|
|
|
<?php /* 🔴 v29 用户第 5 条:右上角「设置」点开是**下拉菜单**,四项分设。
|
|
|
|
|
|
⚠️ 用项目已有的 `.movie-set.menu`(任务/观影模块同款):
|
|
|
|
|
|
外观、点击外部收起、键盘可达全都是现成的,
|
2026-10-08 14:46:16 +08:00
|
|
|
|
这里再手写一套 `<details>` 样式,迟早跟那边不一致。
|
|
|
|
|
|
|
|
|
|
|
|
🔴🔴 v30 用户第 3 条:「导航模块的设置按钮,右上角,不要用文字,
|
|
|
|
|
|
和其他的模块一样,三个横线那个图标。」
|
|
|
|
|
|
⇒ summary 换成与其他七个模块**逐字节相同**的形态:
|
|
|
|
|
|
`class="btn btn-icon"` + 一个 path 里的三条横线(☰)。
|
|
|
|
|
|
⚠️ 三处必须一起改,少一处就是"看着像改了、其实没改":
|
|
|
|
|
|
`btn-ghost btn-sm` → **`btn-icon`**(纯图标方形,高度取 --act-h);
|
|
|
|
|
|
删掉「设置」二字(文字会把方块撑成一行,`.btn-icon` 的 width 形同虚设);
|
|
|
|
|
|
图标换成 ☰(原来是单个向下的箭头,那是"展开"的意思,不是"设置")。
|
|
|
|
|
|
⚠️ 宽度固定为 --act-h 之后,`.btn-icon` 的 `padding:0 !important` 才会生效 ——
|
|
|
|
|
|
仍带 `.btn-sm` 会让它在页头动作区里矮一截(部署脚本有一条专门守这个)。 */ ?>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<details class="movie-set menu">
|
2026-10-08 14:46:16 +08:00
|
|
|
|
<summary class="btn btn-icon" title="导航设置" aria-label="导航设置"><svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/></svg></summary>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<div class="menu-list">
|
|
|
|
|
|
<?php foreach ($setItems as $k => $it): ?>
|
|
|
|
|
|
<a href="<?= e(url('nav', ['set' => $k])) ?>"<?= $setKey === $k ? ' class="on"' : '' ?>>
|
|
|
|
|
|
<svg class="mi" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"
|
|
|
|
|
|
fill="none" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
|
<path d="<?= e($it[1]) ?>"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span><?= e($it[0]) ?></span>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
<a href="<?= e(url('nav')) ?>">
|
|
|
|
|
|
<svg class="mi" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"
|
|
|
|
|
|
fill="none" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
|
<path d="M3 8l3.5 3.5L13 5"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>我的网址</span>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</details>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-08 02:20:19 +08:00
|
|
|
|
<?php /* 设置视图里的四块(展示地址 / 导航设置 / 板块 / 搜索引擎)。
|
|
|
|
|
|
只在设置态渲染 —— 默认进来看到的是"添加网址 + 网址列表"(用户原话)。 */ ?>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<?php /* ==== 地址设置(?set=addr)==== */ ?>
|
|
|
|
|
|
<?php if ($setKey === 'addr'): ?>
|
2026-10-08 02:20:19 +08:00
|
|
|
|
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<?php /* ==================== ① 展示页地址与短语(用户第 4 条的"特殊地址")====================
|
|
|
|
|
|
用户原话:「给他一个非常特殊的地址…地址后面可以跟着用户添加的一个短语,就跟类似
|
|
|
|
|
|
于一个密钥一样,他可以给自己起一个短语…这个页面是不需要登录的。」
|
|
|
|
|
|
⚠️ 这卡片放在**最上面**:它是这个模块与其它模块最不一样的地方(免登录),
|
|
|
|
|
|
也最容易让人困惑("我的导航页在哪?怎么发给别人?")。 */ ?>
|
|
|
|
|
|
<div class="card card-pad" id="navLink">
|
|
|
|
|
|
<h2 class="cinema-h2">
|
|
|
|
|
|
我的导航页地址
|
|
|
|
|
|
<span class="hint">这个地址<b>不需要登录</b>,可以直接发给别人看</span>
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<?php if ($phrase !== ''): ?>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>展示页地址</label>
|
|
|
|
|
|
<input class="input mono" readonly value="<?= e($showUrl) ?>">
|
|
|
|
|
|
<span class="tip">
|
|
|
|
|
|
短语 <b><?= e($phrase) ?></b> 就是这页的"钥匙"—— 换一个短语,旧地址立刻失效。
|
|
|
|
|
|
<?php /* 两种地址写法都支持:?q=xxx 与 ?xxx(见 nav_show.php 的说明) */ ?>
|
|
|
|
|
|
也支持写成 <code class="mono"><?= e(rtrim((string)fb_config('base_url', ''), '/')) ?>/nav/?<?= e($phrase) ?></code>。
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php else: ?>
|
|
|
|
|
|
<p class="tip" style="background:#FFF6E5;border-color:#E8C98A;padding:8px 12px;border-radius:8px">
|
|
|
|
|
|
<b>还没设短语</b> —— 设一个短语之后,才能通过地址把导航页分享出去(那页不需要登录)。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<?php if ($canWrite): ?>
|
|
|
|
|
|
<form method="post" action="<?= e(url('nav/save')) ?>" class="inline-form">
|
|
|
|
|
|
<?= csrf_field() ?>
|
|
|
|
|
|
<input type="hidden" name="act" value="phrase_save">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>我的短语</label>
|
|
|
|
|
|
<input class="input mono" name="phrase" value="<?= e($phrase) ?>"
|
|
|
|
|
|
placeholder="如 yangyq(4~32 位字母/数字/下划线/连字符)" autocomplete="off">
|
|
|
|
|
|
<span class="tip">要 <b>4~32 位</b>,只能用字母、数字、下划线、连字符;不能与别人重复。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="btn-row" style="margin-top:0">
|
|
|
|
|
|
<button class="btn btn-sm" type="submit">保存短语</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
<form method="post" action="<?= e(url('nav/save')) ?>" class="inline-form"
|
|
|
|
|
|
onsubmit="return confirm('换成一串随机短语?之前的地址会立刻失效。')">
|
|
|
|
|
|
<?= csrf_field() ?>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<input type="hidden" name="back" value="addr">
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<input type="hidden" name="act" value="phrase_random">
|
|
|
|
|
|
<button class="btn btn-ghost btn-sm" type="submit">换一串随机的</button>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<?php endif; ?><?php /* ==== /地址设置 ==== */ ?>
|
|
|
|
|
|
|
|
|
|
|
|
<?php /* ==== 导航设置(?set=general)==== */ ?>
|
|
|
|
|
|
<?php if ($setKey === 'general'): ?>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<?php /* ==================== ② 设置(照搬 SimpleNav 的 settings)==================== */ ?>
|
|
|
|
|
|
<div class="card card-pad">
|
|
|
|
|
|
<h2 class="cinema-h2">导航设置</h2>
|
|
|
|
|
|
<form method="post" action="<?= e(url('nav/save')) ?>">
|
|
|
|
|
|
<?= csrf_field() ?>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<input type="hidden" name="back" value="general">
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<input type="hidden" name="act" value="settings_save">
|
|
|
|
|
|
<div class="form-grid">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>站点名</label>
|
|
|
|
|
|
<input class="input" name="site_name" value="<?= e($sc('site_name', '我的导航')) ?>">
|
|
|
|
|
|
<span class="tip">展示页最上方的大标题。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>副标题</label>
|
|
|
|
|
|
<input class="input" name="subtitle" value="<?= e($sc('subtitle')) ?>" placeholder="留空就不显示">
|
|
|
|
|
|
<?php /* ⚠️ SimpleNav 原文把"副标题清空后仍然显示"单列为 bug 现场,
|
|
|
|
|
|
这里保留它的行为:**空就是空**,不回退成默认文案。 */ ?>
|
|
|
|
|
|
<span class="tip">留空就<b>不显示</b>(不会自动填回默认值)。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>主题色</label>
|
|
|
|
|
|
<input class="input" type="color" name="primary_color"
|
|
|
|
|
|
value="<?= e(\App\Nav::safeColor($sc('primary_color', '#2F6BF7'), '#2F6BF7')) ?>"
|
|
|
|
|
|
style="height:38px;padding:3px">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>默认搜索引擎</label>
|
|
|
|
|
|
<select class="select" name="default_engine">
|
|
|
|
|
|
<?php foreach ($engines as $en): ?>
|
|
|
|
|
|
<option value="<?= e((string)$en['slug']) ?>"
|
|
|
|
|
|
<?= (string)$en['slug'] === $sc('default_engine') ? 'selected' : '' ?>>
|
|
|
|
|
|
<?= e((string)$en['name']) ?>
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>链接打开方式</label>
|
|
|
|
|
|
<select class="select" name="open_blank">
|
|
|
|
|
|
<option value="1" <?= $sc('open_blank', '1') === '1' ? 'selected' : '' ?>>新标签页</option>
|
|
|
|
|
|
<option value="0" <?= $sc('open_blank', '1') === '0' ? 'selected' : '' ?>>当前页面</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>联想词按键</label>
|
|
|
|
|
|
<select class="select" name="suggest_keys">
|
|
|
|
|
|
<option value="both" <?= $sc('suggest_keys', 'both') === 'both' ? 'selected' : '' ?>>↓/↑ 与 , . 都认(推荐)</option>
|
|
|
|
|
|
<option value="arrows" <?= $sc('suggest_keys', 'both') === 'arrows' ? 'selected' : '' ?>>只认 ↓/↑</option>
|
|
|
|
|
|
<option value="punct" <?= $sc('suggest_keys', 'both') === 'punct' ? 'selected' : '' ?>>只认 , 和 .</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<span class="tip">在展示页的搜索框里,用哪组键选中联想词。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>合集显示方式</label>
|
|
|
|
|
|
<select class="select" name="collections_layout">
|
|
|
|
|
|
<option value="tabs" <?= $sc('collections_layout', 'tabs') === 'tabs' ? 'selected' : '' ?>>标签页折叠(一次看一个)</option>
|
|
|
|
|
|
<option value="flat" <?= $sc('collections_layout', 'tabs') === 'flat' ? 'selected' : '' ?>>全部平铺(一眼看全)</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="btn-row">
|
|
|
|
|
|
<button class="btn" type="submit">保存设置</button>
|
|
|
|
|
|
<?php if ($canWrite): ?>
|
|
|
|
|
|
<button class="btn btn-ghost" type="submit" formaction="<?= e(url('nav/save')) ?>"
|
|
|
|
|
|
name="act" value="fetch_all"
|
|
|
|
|
|
onclick="return confirm('会把所有站点的名称和图标重新抓一遍,可能比较慢。自己传过图标的不会被覆盖。继续?')">
|
|
|
|
|
|
全部更新(抓名称与图标)
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<?php endif; ?><?php /* ==== /导航设置 ==== */ ?>
|
2026-10-08 02:20:19 +08:00
|
|
|
|
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<?php /* ==================== ③ 站点:新增/编辑 ==================== */
|
|
|
|
|
|
/* 🔴 v29 用户第 5 条:这两块(新增站点 + 站点列表)是**默认视图**的内容,
|
|
|
|
|
|
设置视图(addr/general/board/engine)里不该出现 —— 用户原话是
|
|
|
|
|
|
「进来之后,默认就是添加网址,和所有网址的列表」,反过来说
|
|
|
|
|
|
进了设置就不该再看到"添加网址"。 */
|
|
|
|
|
|
?>
|
|
|
|
|
|
<?php if (!$inSettings && $canWrite): ?>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<div class="card card-pad">
|
|
|
|
|
|
<h2 class="cinema-h2"><?= $editSite ? '编辑站点' : '新增站点' ?></h2>
|
2026-10-07 15:31:26 +08:00
|
|
|
|
<?php /* 🔴 v24 第 26 批 用户第 4 条:「输入 url 之后,如果其他用户添加了,
|
|
|
|
|
|
他可以直接复用」。做法是**输入框失焦时问一次**服务端,
|
|
|
|
|
|
把别人加过的同款站点列出来让用户挑一个 —— 挑中就带上 reuse_key 提交。
|
|
|
|
|
|
⚠️ 复用只在**新增**时给(编辑已有站点时再抄别人语义很乱):
|
|
|
|
|
|
所以这段只在 `!$editSite` 时输出,并带 `is_new=1` 让服务端也认这个前提。 */ ?>
|
|
|
|
|
|
<?php if (!$editSite): ?>
|
|
|
|
|
|
<div class="nav-reuse" id="navReuse" hidden>
|
|
|
|
|
|
<div class="nav-reuse-h">有人加过这个站点,可以直接复用它的图标与名称:</div>
|
|
|
|
|
|
<div class="nav-reuse-list" id="navReuseList"></div>
|
|
|
|
|
|
<div class="nav-reuse-f" id="navReuseFoot"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<form method="post" action="<?= e(url('nav/save')) ?>" id="navSiteForm">
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<?= csrf_field() ?>
|
|
|
|
|
|
<input type="hidden" name="act" value="site_save">
|
|
|
|
|
|
<?php if ($editSite): ?>
|
|
|
|
|
|
<input type="hidden" name="id" value="<?= (int)$editSite['id'] ?>">
|
|
|
|
|
|
<?php endif; ?>
|
2026-10-07 15:31:26 +08:00
|
|
|
|
<?php if (!$editSite): ?>
|
|
|
|
|
|
<?php /* 服务端凭这个标记判断"这是新建、可以复用"(见 index.php 的 site_save 段) */ ?>
|
|
|
|
|
|
<input type="hidden" name="is_new" value="1">
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<?php /* 用户选定的可复用条目(点一下列表里的卡片就填进来) */ ?>
|
|
|
|
|
|
<input type="hidden" name="reuse_key" id="navReuseKey" value="">
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<div class="form-grid">
|
|
|
|
|
|
<div class="field">
|
2026-10-26 06:45:38 +08:00
|
|
|
|
<label class="is-req">网址</label>
|
2026-10-07 15:31:26 +08:00
|
|
|
|
<input class="input mono" name="url" id="navSiteUrl" required
|
2026-10-07 05:52:40 +08:00
|
|
|
|
value="<?= e((string)($editSite['url'] ?? '')) ?>" placeholder="https://example.com">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
2026-10-26 06:45:38 +08:00
|
|
|
|
<label class="is-req">所属板块</label>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<select class="select" name="group_id" required>
|
|
|
|
|
|
<?php foreach ($allGroups as $g): ?>
|
|
|
|
|
|
<option value="<?= (int)$g['id'] ?>"
|
|
|
|
|
|
<?= (int)($editSite['group_id'] ?? 0) === (int)$g['id'] ? 'selected' : '' ?>>
|
|
|
|
|
|
<?= e((string)$g['name']) ?><?= (string)$g['display'] === 'tabs' ? '(合集)' : '' ?>
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>显示名</label>
|
2026-10-07 15:31:26 +08:00
|
|
|
|
<input class="input" name="name" id="navSiteName" value="<?= e((string)($editSite['name'] ?? '')) ?>"
|
2026-10-07 05:52:40 +08:00
|
|
|
|
placeholder="留空会自动抓">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>简称</label>
|
2026-10-07 15:31:26 +08:00
|
|
|
|
<input class="input" name="short_name" id="navSiteShort"
|
|
|
|
|
|
value="<?= e((string)($editSite['short_name'] ?? '')) ?>"
|
2026-10-07 05:52:40 +08:00
|
|
|
|
placeholder="页面上显示的字(优先于显示名)">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field full">
|
|
|
|
|
|
<label class="chk">
|
|
|
|
|
|
<input type="checkbox" name="is_visible" value="1" checked> 在展示页上显示
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<?php if (!$editSite): ?>
|
|
|
|
|
|
<label class="chk">
|
|
|
|
|
|
<input type="checkbox" name="do_fetch" value="1" checked>
|
|
|
|
|
|
顺便抓取网站名与图标(联网,稍慢)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="btn-row">
|
|
|
|
|
|
<button class="btn" type="submit"><?= $editSite ? '保存' : '添加' ?></button>
|
|
|
|
|
|
<?php if ($editSite): ?>
|
|
|
|
|
|
<a class="btn btn-ghost" href="<?= e(url('nav')) ?>">取消</a>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<?php /* ==================== ④ 站点列表(按板块分组,可拖拽排序)==================== */ ?>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<?php if (!$inSettings): ?>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<div class="card card-pad">
|
|
|
|
|
|
<h2 class="cinema-h2">
|
|
|
|
|
|
站点列表
|
|
|
|
|
|
<span class="hint">按住左边的 ⋮⋮ 可以拖动排序</span>
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<?php if (!$sitesBy): ?>
|
|
|
|
|
|
<p class="empty-hint">还没有站点。上面「新增站点」加一个吧。</p>
|
|
|
|
|
|
<?php else: ?>
|
|
|
|
|
|
<?php foreach ($allGroups as $g): ?>
|
|
|
|
|
|
<?php $rows = $sitesBy[(int)$g['id']] ?? []; if (!$rows) { continue; } ?>
|
|
|
|
|
|
<div class="nav-grp-block">
|
|
|
|
|
|
<h3 class="nav-sub"><?= e((string)$g['name']) ?>
|
|
|
|
|
|
<span class="small muted">(<?= count($rows) ?>)</span>
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<ul class="nav-sortlist" data-sort-list
|
|
|
|
|
|
data-reorder-url="<?= e(url('nav/save')) ?>"
|
|
|
|
|
|
data-group-id="<?= (int)$g['id'] ?>">
|
|
|
|
|
|
<?php foreach ($rows as $r): ?>
|
|
|
|
|
|
<li class="nav-sortrow<?= empty($r['is_visible']) ? ' is-off' : '' ?>"
|
|
|
|
|
|
data-id="<?= (int)$r['id'] ?>">
|
|
|
|
|
|
<span class="nav-drag" title="拖动排序">⋮⋮</span>
|
|
|
|
|
|
<?php $icon = \App\Nav::iconUrl((string)$r['icon']); ?>
|
|
|
|
|
|
<?php if ($icon !== ''): ?>
|
|
|
|
|
|
<img class="nav-ico" src="<?= e($icon) ?>" alt="">
|
|
|
|
|
|
<?php else: ?>
|
|
|
|
|
|
<span class="nav-ico nav-ico-letter"
|
|
|
|
|
|
style="background:<?= e(\App\Nav::letterColor(\App\Nav::siteLabel($r))) ?>">
|
|
|
|
|
|
<?= e(mb_substr(\App\Nav::siteLabel($r), 0, 1, 'UTF-8')) ?>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<span class="nav-sname"><?= e(\App\Nav::siteLabel($r)) ?></span>
|
|
|
|
|
|
<span class="nav-surl small muted"><?= e(\App\Nav::domainOf((string)$r['url'])) ?></span>
|
|
|
|
|
|
<?php if (empty($r['is_visible'])): ?><span class="node-badge">已隐藏</span><?php endif; ?>
|
|
|
|
|
|
<?php if (!empty($r['icon_custom'])): ?><span class="node-badge">自定义图标</span><?php endif; ?>
|
|
|
|
|
|
<span class="nav-acts">
|
|
|
|
|
|
<a class="btn btn-ghost btn-sm" href="<?= e(url('nav', ['edit_site' => (int)$r['id']])) ?>">编辑</a>
|
|
|
|
|
|
<button type="button" class="btn btn-ghost btn-sm"
|
|
|
|
|
|
data-nav-toggle="site" data-nav-id="<?= (int)$r['id'] ?>">
|
|
|
|
|
|
<?= empty($r['is_visible']) ? '显示' : '隐藏' ?>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="btn btn-ghost btn-sm"
|
|
|
|
|
|
data-nav-del="site" data-nav-id="<?= (int)$r['id'] ?>"
|
|
|
|
|
|
data-nav-name="<?= e(\App\Nav::siteLabel($r)) ?>">删除</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</div>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<?php endif; ?><?php /* ④ 站点列表(仅默认视图) */ ?>
|
|
|
|
|
|
<?php /* ⚠️ 这里必须补上 277 行那个 `if (!$inSettings && $canWrite)` 的收尾 ——
|
|
|
|
|
|
它原本一路开到了文件末尾(607 行),把**板块设置 / 搜索引擎设置**两个视图
|
|
|
|
|
|
也一起包进了"非设置态"里 → 打开 `?set=board` 时整段被跳过,
|
|
|
|
|
|
页面标题显示"板块设置"、内容却是空的(本轮实测抓到的真 bug:
|
|
|
|
|
|
`<!--DBG-->` 探针证明执行根本没走到 409 行)。
|
|
|
|
|
|
⇒ 「非设置态才显示」只该管**站点编辑 + 站点列表**这两块(见 v29 第 5 条),
|
|
|
|
|
|
设置类视图(addr/general/board/engine)各自独立、不受它管。 */ ?>
|
|
|
|
|
|
<?php endif; ?>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<?php /* ==== 板块设置(?set=board)==== */ ?>
|
|
|
|
|
|
<?php if ($setKey === 'board'): ?>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<?php /* ==================== ⑤ 板块 ==================== */ ?>
|
|
|
|
|
|
<div class="card card-pad">
|
|
|
|
|
|
<h2 class="cinema-h2"><?= $editGroup ? '编辑板块' : '新增板块' ?></h2>
|
|
|
|
|
|
<form method="post" action="<?= e(url('nav/save')) ?>">
|
|
|
|
|
|
<?= csrf_field() ?>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<input type="hidden" name="back" value="board">
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<input type="hidden" name="act" value="group_save">
|
|
|
|
|
|
<?php if ($editGroup): ?>
|
|
|
|
|
|
<input type="hidden" name="id" value="<?= (int)$editGroup['id'] ?>">
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<div class="form-grid">
|
|
|
|
|
|
<div class="field">
|
2026-10-26 06:45:38 +08:00
|
|
|
|
<label class="is-req">板块名</label>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<input class="input" name="name" required value="<?= e((string)($editGroup['name'] ?? '')) ?>"
|
|
|
|
|
|
placeholder="如 常用 / 工具">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>显示方式</label>
|
|
|
|
|
|
<select class="select" name="display">
|
|
|
|
|
|
<option value="grid" <?= (string)($editGroup['display'] ?? 'grid') === 'grid' ? 'selected' : '' ?>>普通网格(一直平铺)</option>
|
|
|
|
|
|
<option value="tabs" <?= (string)($editGroup['display'] ?? '') === 'tabs' ? 'selected' : '' ?>>合集标签页(折叠,点开才看)</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field full">
|
|
|
|
|
|
<label class="chk"><input type="checkbox" name="is_visible" value="1" checked> 在展示页上显示</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="btn-row">
|
|
|
|
|
|
<button class="btn" type="submit"><?= $editGroup ? '保存' : '添加' ?></button>
|
|
|
|
|
|
<?php if ($editGroup): ?>
|
|
|
|
|
|
<a class="btn btn-ghost" href="<?= e(url('nav')) ?>">取消</a>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card card-pad">
|
|
|
|
|
|
<h2 class="cinema-h2">板块列表 <span class="hint">拖动排序</span></h2>
|
|
|
|
|
|
<ul class="nav-sortlist" data-sort-list
|
|
|
|
|
|
data-reorder-url="<?= e(url('nav/save')) ?>">
|
|
|
|
|
|
<?php foreach ($allGroups as $g): ?>
|
|
|
|
|
|
<li class="nav-sortrow<?= empty($g['is_visible']) ? ' is-off' : '' ?>" data-id="<?= (int)$g['id'] ?>">
|
|
|
|
|
|
<span class="nav-drag" title="拖动排序">⋮⋮</span>
|
|
|
|
|
|
<span class="nav-sname"><?= e((string)$g['name']) ?></span>
|
|
|
|
|
|
<span class="node-badge"><?= (string)$g['display'] === 'tabs' ? '合集' : '网格' ?></span>
|
|
|
|
|
|
<span class="small muted"><?= count($sitesBy[(int)$g['id']] ?? []) ?> 个站点</span>
|
|
|
|
|
|
<?php if (empty($g['is_visible'])): ?><span class="node-badge">已隐藏</span><?php endif; ?>
|
|
|
|
|
|
<span class="nav-acts">
|
|
|
|
|
|
<a class="btn btn-ghost btn-sm" href="<?= e(url('nav', ['edit_group' => (int)$g['id']])) ?>">编辑</a>
|
|
|
|
|
|
<button type="button" class="btn btn-ghost btn-sm"
|
|
|
|
|
|
data-nav-toggle="group" data-nav-id="<?= (int)$g['id'] ?>">
|
|
|
|
|
|
<?= empty($g['is_visible']) ? '显示' : '隐藏' ?>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="btn btn-ghost btn-sm"
|
|
|
|
|
|
data-nav-del="group" data-nav-id="<?= (int)$g['id'] ?>"
|
|
|
|
|
|
data-nav-name="<?= e((string)$g['name']) ?>">删除</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<?php endif; ?><?php /* ==== /板块设置 ==== */ ?>
|
|
|
|
|
|
|
|
|
|
|
|
<?php /* ==== 搜索引擎设置(?set=engine)==== */ ?>
|
|
|
|
|
|
<?php if ($setKey === 'engine'): ?>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<?php /* ==================== ⑥ 搜索引擎 ==================== */ ?>
|
|
|
|
|
|
<div class="card card-pad">
|
|
|
|
|
|
<h2 class="cinema-h2"><?= $editEngine ? '编辑搜索引擎' : '新增搜索引擎' ?></h2>
|
|
|
|
|
|
<form method="post" action="<?= e(url('nav/save')) ?>">
|
|
|
|
|
|
<?= csrf_field() ?>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<input type="hidden" name="back" value="engine">
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<input type="hidden" name="act" value="engine_save">
|
|
|
|
|
|
<?php if ($editEngine): ?>
|
|
|
|
|
|
<input type="hidden" name="id" value="<?= (int)$editEngine['id'] ?>">
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<div class="form-grid">
|
|
|
|
|
|
<div class="field">
|
2026-10-26 06:45:38 +08:00
|
|
|
|
<label class="is-req">名称</label>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<input class="input" name="name" required value="<?= e((string)($editEngine['name'] ?? '')) ?>">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>标识</label>
|
|
|
|
|
|
<input class="input mono" name="slug" value="<?= e((string)($editEngine['slug'] ?? '')) ?>"
|
|
|
|
|
|
placeholder="留空自动生成">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
2026-10-26 06:45:38 +08:00
|
|
|
|
<label class="is-req">搜索地址</label>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<input class="input mono" name="search_url" required
|
|
|
|
|
|
value="<?= e((string)($editEngine['search_url'] ?? '')) ?>"
|
|
|
|
|
|
placeholder="https://cn.bing.com/search">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
2026-10-26 06:45:38 +08:00
|
|
|
|
<label class="is-req">关键词参数</label>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<input class="input mono" name="param"
|
|
|
|
|
|
value="<?= e((string)($editEngine['param'] ?? 'q')) ?>" placeholder="q">
|
|
|
|
|
|
<span class="tip">百度是 wd,搜狗是 query,多数是 q。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>联想接口(可选)</label>
|
|
|
|
|
|
<input class="input mono" name="suggest_url"
|
|
|
|
|
|
value="<?= e((string)($editEngine['suggest_url'] ?? '')) ?>"
|
|
|
|
|
|
placeholder="https://api.bing.com/osjson.aspx?query={q}">
|
|
|
|
|
|
<span class="tip">用 <code class="mono">{q}</code> 占位关键词;留空 = 这个引擎没有联想。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>联想类型</label>
|
|
|
|
|
|
<input class="input mono" name="suggest_type"
|
|
|
|
|
|
value="<?= e((string)($editEngine['suggest_type'] ?? '')) ?>"
|
|
|
|
|
|
placeholder="bing_osjson / baidu_sugrec / so360">
|
|
|
|
|
|
</div>
|
2026-10-08 02:20:19 +08:00
|
|
|
|
<?php /* 🔴 v28 用户第 10 条:「联想接口还是不能用啊,需要你补充几个进去,
|
|
|
|
|
|
让用户选择。搜索引擎列表,联想接口,是**对所有用户可见**的。」
|
|
|
|
|
|
⇒ 这里列出内置的联想接口库(App\Nav::SUGGEST_LIBRARY,全局共享一份),
|
|
|
|
|
|
**点一下就把地址与类型填进上面两个框** —— 用户不必去记
|
|
|
|
|
|
"sugrec 还是 osjson"这种东西,也不必手抄 URL(抄错一位就永远空下拉)。
|
|
|
|
|
|
⚠️ 这些只是"填入表单",仍要用户点保存才生效 ——
|
|
|
|
|
|
静默改数据会在用户没注意时改掉他自己的配置。 */ ?>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>内置联想接口 <span class="hint">点一下即可填入上面两项</span></label>
|
|
|
|
|
|
<div class="sug-lib">
|
|
|
|
|
|
<?php foreach (\App\Nav::suggestLibrary() as $sl): ?>
|
|
|
|
|
|
<button type="button" class="sug-lib-item"
|
|
|
|
|
|
data-sug-url="<?= e((string)$sl['url']) ?>"
|
|
|
|
|
|
data-sug-type="<?= e((string)$sl['type']) ?>">
|
|
|
|
|
|
<b><?= e((string)$sl['name']) ?></b>
|
|
|
|
|
|
<span><?= e((string)$sl['note']) ?></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="tip">
|
|
|
|
|
|
这些接口对**所有用户**都可见、可直接选用。国内网络下不可达的
|
|
|
|
|
|
(谷歌 / DuckDuckGo)没有列进来 —— 选了也只会永远是空下拉。
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>颜色</label>
|
|
|
|
|
|
<input class="input" type="color" name="color"
|
|
|
|
|
|
value="<?= e(\App\Nav::safeColor((string)($editEngine['color'] ?? ''), '#2F6BF7')) ?>"
|
|
|
|
|
|
style="height:38px;padding:3px">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="btn-row">
|
|
|
|
|
|
<button class="btn" type="submit"><?= $editEngine ? '保存' : '添加' ?></button>
|
|
|
|
|
|
<?php if ($editEngine): ?>
|
|
|
|
|
|
<a class="btn btn-ghost" href="<?= e(url('nav')) ?>">取消</a>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card card-pad">
|
|
|
|
|
|
<h2 class="cinema-h2">搜索引擎列表</h2>
|
2026-10-07 15:31:26 +08:00
|
|
|
|
<?php /* 🔴 v24 第 26 批 用户第 4 条:「可以内置几个,让用户选择」。
|
|
|
|
|
|
默认已经内置 6 个(必应/百度/360 启用,谷歌/搜狗/DuckGo 未启用 —— 后三个
|
|
|
|
|
|
在国内网络下不可用,见 nav_suggest.php 头部的线上实测)。
|
|
|
|
|
|
⚠️ 但**老用户**库里只有当年那几条(ensureSeed 只在"一条都没有"时才灌),
|
|
|
|
|
|
所以要给一个"补齐内置"的入口,否则"内置几个"对他们等于 0 个。
|
|
|
|
|
|
⚠️ 用独立表单(不是 data-nav-* 那种 AJAX 按钮):它是一次性动作、
|
|
|
|
|
|
执行完要整页刷新看结果,走普通 POST 最省事也最不容易出错。 */ ?>
|
|
|
|
|
|
<p class="small muted" style="margin-top:0">
|
|
|
|
|
|
内置了必应 / 百度 / 360 三档(国内可用)。谷歌 / 搜狗 / DuckGo 也列在下面,
|
|
|
|
|
|
但它们的联想或搜索在国内网络下不可用,所以默认<strong>未启用</b>。
|
|
|
|
|
|
展示页顶部那排标签,就是这里<strong>已启用</strong>的引擎。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<form method="post" action="<?= e(url('nav/save')) ?>" class="inline-form" style="margin:0 0 10px">
|
|
|
|
|
|
<?= csrf_field() ?>
|
|
|
|
|
|
<input type="hidden" name="act" value="engine_seed_add">
|
|
|
|
|
|
<button class="btn btn-sm" type="submit">补齐内置搜索引擎</button>
|
|
|
|
|
|
<span class="small muted">已有的不会被改动(你改过的地址、颜色都保留)。</span>
|
|
|
|
|
|
</form>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
<ul class="nav-sortlist" data-sort-list data-reorder-url="<?= e(url('nav/save')) ?>">
|
|
|
|
|
|
<?php foreach ($engines as $en): ?>
|
|
|
|
|
|
<li class="nav-sortrow<?= empty($en['is_active']) ? ' is-off' : '' ?>" data-id="<?= (int)$en['id'] ?>">
|
|
|
|
|
|
<span class="nav-drag" title="拖动排序">⋮⋮</span>
|
|
|
|
|
|
<span class="nav-ico nav-ico-letter"
|
|
|
|
|
|
style="background:<?= e(\App\Nav::safeColor((string)$en['color'], '#2F6BF7')) ?>">
|
|
|
|
|
|
<?= e(mb_substr((string)$en['name'], 0, 1, 'UTF-8')) ?>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="nav-sname"><?= e((string)$en['name']) ?></span>
|
|
|
|
|
|
<span class="small muted mono"><?= e((string)$en['slug']) ?></span>
|
|
|
|
|
|
<?php if (empty($en['is_active'])): ?><span class="node-badge">未启用</span><?php endif; ?>
|
|
|
|
|
|
<span class="nav-acts">
|
|
|
|
|
|
<a class="btn btn-ghost btn-sm" href="<?= e(url('nav', ['edit_engine' => (int)$en['id']])) ?>">编辑</a>
|
|
|
|
|
|
<button type="button" class="btn btn-ghost btn-sm"
|
|
|
|
|
|
data-nav-toggle="engine" data-nav-id="<?= (int)$en['id'] ?>">
|
|
|
|
|
|
<?= empty($en['is_active']) ? '启用' : '停用' ?>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="btn btn-ghost btn-sm"
|
|
|
|
|
|
data-nav-del="engine" data-nav-id="<?= (int)$en['id'] ?>"
|
|
|
|
|
|
data-nav-name="<?= e((string)$en['name']) ?>">删除</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
2026-10-08 10:54:43 +08:00
|
|
|
|
<?php endif; ?><?php /* ==== /搜索引擎设置 ==== */ ?>
|
2026-10-07 05:52:40 +08:00
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
/* 导航管理页的交互(v24 第 24 批)。
|
|
|
|
|
|
⚠️ 拖拽排序与开关走 AJAX(fetch + X-Requested-With),服务端据此回 JSON;
|
|
|
|
|
|
其余表单是普通 POST —— 保存后整页刷新,结果一眼可见,比 AJAX 可靠。
|
|
|
|
|
|
⚠️ 排序提交的是**整组 id 顺序**(SimpleNav 的做法:整组归一化,
|
|
|
|
|
|
每次拖完间隙都恢复整齐,之后新增仍天然排最后)。 */
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
var TOKEN = (function () {
|
|
|
|
|
|
var el = document.querySelector('input[name="_csrf"]');
|
|
|
|
|
|
return el ? el.value : '';
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
function post(data) {
|
|
|
|
|
|
var fd = new FormData();
|
|
|
|
|
|
fd.append('_csrf', TOKEN);
|
|
|
|
|
|
Object.keys(data).forEach(function (k) {
|
|
|
|
|
|
if (Array.isArray(data[k])) {
|
|
|
|
|
|
data[k].forEach(function (v) { fd.append(k + '[]', v); });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
fd.append(k, data[k]);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return fetch(NAV_SAVE_URL, {
|
|
|
|
|
|
method: 'POST', body: fd, credentials: 'same-origin',
|
|
|
|
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
|
|
|
|
}).then(function (r) { return r.json().catch(function () { return { ok: false, error: '服务器返回的不是 JSON' }; }); });
|
|
|
|
|
|
}
|
|
|
|
|
|
var NAV_SAVE_URL = <?= json_encode(url('nav/save'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
|
|
|
|
|
|
|
|
|
|
/* ---- 拖拽排序(原生 HTML5 DnD,不引库)---- */
|
|
|
|
|
|
Array.prototype.forEach.call(document.querySelectorAll('[data-sort-list]'), function (list) {
|
|
|
|
|
|
var dragEl = null;
|
|
|
|
|
|
list.addEventListener('dragstart', function (e) {
|
|
|
|
|
|
var row = e.target.closest ? e.target.closest('.nav-sortrow') : null;
|
|
|
|
|
|
if (!row) { return; }
|
|
|
|
|
|
dragEl = row;
|
|
|
|
|
|
row.classList.add('is-dragging');
|
|
|
|
|
|
if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'move'; }
|
|
|
|
|
|
});
|
|
|
|
|
|
list.addEventListener('dragover', function (e) {
|
|
|
|
|
|
if (!dragEl) { return; }
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
var row = e.target.closest ? e.target.closest('.nav-sortrow') : null;
|
|
|
|
|
|
if (!row || row === dragEl) { return; }
|
|
|
|
|
|
var r = row.getBoundingClientRect();
|
|
|
|
|
|
var after = (e.clientY - r.top) > r.height / 2;
|
|
|
|
|
|
list.insertBefore(dragEl, after ? row.nextSibling : row);
|
|
|
|
|
|
});
|
|
|
|
|
|
list.addEventListener('dragend', function () {
|
|
|
|
|
|
if (!dragEl) { return; }
|
|
|
|
|
|
dragEl.classList.remove('is-dragging');
|
|
|
|
|
|
dragEl = null;
|
|
|
|
|
|
var ids = Array.prototype.map.call(list.querySelectorAll('.nav-sortrow'),
|
|
|
|
|
|
function (r) { return r.getAttribute('data-id'); });
|
|
|
|
|
|
post({
|
|
|
|
|
|
act: 'site_reorder', ids: ids,
|
|
|
|
|
|
group_id: list.getAttribute('data-group-id') || 0
|
|
|
|
|
|
}).then(function () { window.location.reload(); });
|
|
|
|
|
|
});
|
|
|
|
|
|
/* 让行可拖:整行 draggable,但只有按住 ⋮⋮ 才起拖(避免误拖) */
|
|
|
|
|
|
Array.prototype.forEach.call(list.querySelectorAll('.nav-sortrow'), function (row) {
|
|
|
|
|
|
var handle = row.querySelector('.nav-drag');
|
|
|
|
|
|
if (!handle) { return; }
|
|
|
|
|
|
handle.addEventListener('mousedown', function () { row.draggable = true; });
|
|
|
|
|
|
row.addEventListener('mouseup', function () { row.draggable = false; });
|
|
|
|
|
|
row.addEventListener('mouseleave', function () { row.draggable = false; });
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/* ⚠️ 上面的排序对"方块列表"和"站点列表"是同一套,但两者提交的 act 不同:
|
|
|
|
|
|
站点那组带 data-group-id(属于某个板块),板块/引擎那组不带。
|
|
|
|
|
|
服务端据此区分 —— 这里补上"没有 group-id 时用 group_reorder / engine_reorder"。 */
|
|
|
|
|
|
Array.prototype.forEach.call(document.querySelectorAll('[data-sort-list]'), function (list) {
|
|
|
|
|
|
if (list.getAttribute('data-group-id') !== null) { return; } // 站点组,已在上面处理
|
|
|
|
|
|
// 用列表里第一行的线索判断是"板块"还是"引擎"
|
|
|
|
|
|
var isEngine = list.closest('.card') && /搜索引擎/.test(list.closest('.card').textContent);
|
|
|
|
|
|
var act = isEngine ? 'engine_reorder' : 'group_reorder';
|
|
|
|
|
|
list.addEventListener('dragend', function () {
|
|
|
|
|
|
var ids = Array.prototype.map.call(list.querySelectorAll('.nav-sortrow'),
|
|
|
|
|
|
function (r) { return r.getAttribute('data-id'); });
|
|
|
|
|
|
post({ act: act, ids: ids }).then(function () { window.location.reload(); });
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/* ---- 显示/隐藏 与 删除(都走 AJAX)---- */
|
|
|
|
|
|
document.addEventListener('click', function (e) {
|
|
|
|
|
|
var t = e.target;
|
|
|
|
|
|
if (!t || !t.closest) { return; }
|
|
|
|
|
|
var tg = t.closest('[data-nav-toggle]');
|
|
|
|
|
|
if (tg) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
post({ act: tg.getAttribute('data-nav-toggle') + '_toggle',
|
|
|
|
|
|
id: tg.getAttribute('data-nav-id') })
|
|
|
|
|
|
.then(function () { window.location.reload(); });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var del = t.closest('[data-nav-del]');
|
|
|
|
|
|
if (del) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
var kind = del.getAttribute('data-nav-del');
|
|
|
|
|
|
var nm = del.getAttribute('data-nav-name') || '';
|
|
|
|
|
|
var warn = kind === 'group'
|
|
|
|
|
|
? '删掉板块「' + nm + '」?里面的站点会一起删掉。'
|
|
|
|
|
|
: '删掉「' + nm + '」?';
|
|
|
|
|
|
if (!window.confirm(warn)) { return; }
|
|
|
|
|
|
post({ act: kind + '_delete', id: del.getAttribute('data-nav-id') })
|
|
|
|
|
|
.then(function () { window.location.reload(); });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-10-07 15:31:26 +08:00
|
|
|
|
|
|
|
|
|
|
/* ==================================================================
|
|
|
|
|
|
跨用户复用站点(v24 第 26 批 用户第 4 条)
|
|
|
|
|
|
用户原话:「输入 url 之后,如果其他用户添加了,他可以直接复用,
|
|
|
|
|
|
复用的时候,自定义图标和简称,他可以自己再设置,
|
|
|
|
|
|
设置好以后就固定了。」
|
|
|
|
|
|
|
|
|
|
|
|
交互设计(三拍,对应他这句话的三段):
|
|
|
|
|
|
① **输入 URL 之后** → 输入框失焦(或粘贴后 600ms)自动问一次服务端;
|
|
|
|
|
|
② 有人加过 → 列出可复用的卡片(图标 + 显示名),**点一下即选中**;
|
|
|
|
|
|
③ 选中后 → 把它的 名字/图标 展示出来,并把 reuse_key 填进隐藏域;
|
|
|
|
|
|
用户可以在上面的「显示名 / 简称」里改成自己喜欢的("自己再设置"),
|
|
|
|
|
|
提交时服务端会把图标**复制一份并标成自定义**("设置好以后就固定了")。
|
|
|
|
|
|
⚠️ 输入框再变 → 立刻清空已选(URL 换了,之前选的复用目标就不成立了)。
|
|
|
|
|
|
这一条最容易漏:不清的话用户改了网址、却还带着上一个站点的图标提交。
|
|
|
|
|
|
⚠️ 全程不阻塞:查询失败(离线/超时)就当"没人加过",不弹错、不挡提交。
|
|
|
|
|
|
================================================================== */
|
|
|
|
|
|
var urlIn = document.getElementById('navSiteUrl');
|
|
|
|
|
|
var reuse = document.getElementById('navReuse');
|
|
|
|
|
|
var reuseL = document.getElementById('navReuseList');
|
|
|
|
|
|
var reuseF = document.getElementById('navReuseFoot');
|
|
|
|
|
|
var reuseK = document.getElementById('navReuseKey');
|
|
|
|
|
|
var nameIn = document.getElementById('navSiteName');
|
|
|
|
|
|
var shrtIn = document.getElementById('navSiteShort');
|
|
|
|
|
|
if (urlIn && reuse && reuseL && reuseK) {
|
|
|
|
|
|
var lookupTimer = null;
|
|
|
|
|
|
var lastLookup = '';
|
|
|
|
|
|
|
|
|
|
|
|
function clearReuse() {
|
|
|
|
|
|
reuseK.value = '';
|
|
|
|
|
|
reuse.hidden = true;
|
|
|
|
|
|
reuseL.innerHTML = '';
|
|
|
|
|
|
if (reuseF) { reuseF.textContent = ''; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function doLookup() {
|
|
|
|
|
|
var u = (urlIn.value || '').trim();
|
|
|
|
|
|
if (u === '' || !/^https?:\/\//i.test(u)) { clearReuse(); return; }
|
|
|
|
|
|
if (u === lastLookup) { return; } // 同一个地址不重复问
|
|
|
|
|
|
lastLookup = u;
|
|
|
|
|
|
post({ act: 'site_lookup', url: u }).then(function (d) {
|
|
|
|
|
|
if (!d || !d.items || !d.items.length) { clearReuse(); return; }
|
|
|
|
|
|
reuseL.innerHTML = '';
|
|
|
|
|
|
d.items.forEach(function (it) {
|
|
|
|
|
|
var b = document.createElement('button');
|
|
|
|
|
|
b.type = 'button';
|
|
|
|
|
|
b.className = 'nav-reuse-item';
|
|
|
|
|
|
b.setAttribute('data-key', it.key);
|
|
|
|
|
|
b.setAttribute('data-name', it.name || '');
|
|
|
|
|
|
b.setAttribute('data-short', it.short_name || '');
|
|
|
|
|
|
var img = document.createElement('img');
|
|
|
|
|
|
img.className = 'nav-ico';
|
|
|
|
|
|
img.alt = '';
|
|
|
|
|
|
if (it.icon) { img.src = it.icon; }
|
|
|
|
|
|
var sp = document.createElement('span');
|
|
|
|
|
|
sp.className = 'nav-reuse-nm';
|
|
|
|
|
|
sp.textContent = it.label || '';
|
|
|
|
|
|
b.appendChild(img);
|
|
|
|
|
|
b.appendChild(sp);
|
|
|
|
|
|
b.addEventListener('click', function () {
|
|
|
|
|
|
Array.prototype.forEach.call(reuseL.querySelectorAll('.nav-reuse-item'),
|
|
|
|
|
|
function (x) { x.classList.remove('is-on'); });
|
|
|
|
|
|
b.classList.add('is-on');
|
|
|
|
|
|
reuseK.value = it.key;
|
|
|
|
|
|
/* "他可以自己再设置":把别人的名字/简称**预填**进输入框,
|
|
|
|
|
|
用户想改就直接改(不填也不会错 —— 服务端会用别人的补上)。
|
|
|
|
|
|
⚠️ 只填**空的**字段:用户已经手打过名字时不要覆盖他(那是抢用户的输入)。 */
|
|
|
|
|
|
if (nameIn && nameIn.value.trim() === '' && it.name) { nameIn.value = it.name; }
|
|
|
|
|
|
if (shrtIn && shrtIn.value.trim() === '' && it.short_name) { shrtIn.value = it.short_name; }
|
|
|
|
|
|
if (reuseF) {
|
|
|
|
|
|
reuseF.textContent = '已选「' + (it.label || '') + '」:'
|
|
|
|
|
|
+ '图标会复制一份到你名下并固定(以后「全部更新」不会改它)。'
|
|
|
|
|
|
+ '上面两项可自行修改。';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
reuseL.appendChild(b);
|
|
|
|
|
|
});
|
|
|
|
|
|
reuse.hidden = false;
|
|
|
|
|
|
}).catch(function () { clearReuse(); });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失焦后问(不用 input 事件:边打边问会把半截域名也拿去查)
|
|
|
|
|
|
urlIn.addEventListener('blur', function () { setTimeout(doLookup, 120); });
|
|
|
|
|
|
// 粘贴之后通常不会失焦,补一拍
|
|
|
|
|
|
urlIn.addEventListener('paste', function () {
|
|
|
|
|
|
clearTimeout(lookupTimer);
|
|
|
|
|
|
lookupTimer = setTimeout(doLookup, 600);
|
|
|
|
|
|
});
|
|
|
|
|
|
// URL 一变 → 之前选的复用目标失效,必须清掉
|
|
|
|
|
|
urlIn.addEventListener('input', function () {
|
|
|
|
|
|
if (reuseK.value !== '') { clearReuse(); }
|
|
|
|
|
|
lastLookup = '';
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-10-08 02:20:19 +08:00
|
|
|
|
|
|
|
|
|
|
/* 🔴 v28 用户第 10 条:内置联想接口库 —— 点一下把地址+类型填进**同一个表单**里。
|
|
|
|
|
|
⚠️ 用 closest('form') 定位,而不是 document.querySelector:
|
|
|
|
|
|
页面上可能同时有"新增引擎"和"编辑引擎"两个表单(后者是 ?edit_engine=… 带出来的),
|
|
|
|
|
|
用全局查找会把内容填到**另一个**表单里 —— 用户点了没反应、或更糟:填错地方。 */
|
|
|
|
|
|
Array.prototype.forEach.call(document.querySelectorAll('.sug-lib-item'), function (b) {
|
|
|
|
|
|
b.addEventListener('click', function () {
|
|
|
|
|
|
var form = b.closest('form');
|
|
|
|
|
|
if (!form) { return; }
|
|
|
|
|
|
var u = form.querySelector('input[name="suggest_url"]');
|
|
|
|
|
|
var t = form.querySelector('input[name="suggest_type"]');
|
|
|
|
|
|
if (u) { u.value = b.getAttribute('data-sug-url') || ''; }
|
|
|
|
|
|
if (t) { t.value = b.getAttribute('data-sug-type') || ''; }
|
|
|
|
|
|
// 反馈:让用户看到"填进去了"(否则一片安静,会以为没点上)
|
|
|
|
|
|
b.classList.add('is-picked');
|
|
|
|
|
|
Array.prototype.forEach.call(b.parentNode.children, function (sib) {
|
|
|
|
|
|
if (sib !== b) { sib.classList.remove('is-picked'); }
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-10-07 05:52:40 +08:00
|
|
|
|
})();
|
|
|
|
|
|
</script>
|