545 lines
26 KiB
PHP
545 lines
26 KiB
PHP
<?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);
|
|||
|
|
?>
|
|||
|
|
<div class="page-head">
|
|||
|
|
<h1>导航</h1>
|
|||
|
|
<span class="sub">你个人用的网址导航 · 每人一份</span>
|
|||
|
|
<div class="actions">
|
|||
|
|
<a class="btn btn-ghost btn-sm" href="<?= e(url('nav')) ?>">刷新</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<?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() ?>
|
|||
|
|
<input type="hidden" name="act" value="phrase_random">
|
|||
|
|
<button class="btn btn-ghost btn-sm" type="submit">换一串随机的</button>
|
|||
|
|
</form>
|
|||
|
|
<?php endif; ?>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<?php /* ==================== ② 设置(照搬 SimpleNav 的 settings)==================== */ ?>
|
|||
|
|
<div class="card card-pad">
|
|||
|
|
<h2 class="cinema-h2">导航设置</h2>
|
|||
|
|
<form method="post" action="<?= e(url('nav/save')) ?>">
|
|||
|
|
<?= csrf_field() ?>
|
|||
|
|
<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>
|
|||
|
|
|
|||
|
|
<?php /* ==================== ③ 站点:新增/编辑 ==================== */ ?>
|
|||
|
|
<?php if ($canWrite): ?>
|
|||
|
|
<div class="card card-pad">
|
|||
|
|
<h2 class="cinema-h2"><?= $editSite ? '编辑站点' : '新增站点' ?></h2>
|
|||
|
|
<form method="post" action="<?= e(url('nav/save')) ?>">
|
|||
|
|
<?= csrf_field() ?>
|
|||
|
|
<input type="hidden" name="act" value="site_save">
|
|||
|
|
<?php if ($editSite): ?>
|
|||
|
|
<input type="hidden" name="id" value="<?= (int)$editSite['id'] ?>">
|
|||
|
|
<?php endif; ?>
|
|||
|
|
<div class="form-grid">
|
|||
|
|
<div class="field">
|
|||
|
|
<label>网址 *</label>
|
|||
|
|
<input class="input mono" name="url" required
|
|||
|
|
value="<?= e((string)($editSite['url'] ?? '')) ?>" placeholder="https://example.com">
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>所属板块 *</label>
|
|||
|
|
<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>
|
|||
|
|
<input class="input" name="name" value="<?= e((string)($editSite['name'] ?? '')) ?>"
|
|||
|
|
placeholder="留空会自动抓">
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>简称</label>
|
|||
|
|
<input class="input" name="short_name" value="<?= e((string)($editSite['short_name'] ?? '')) ?>"
|
|||
|
|
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 /* ==================== ④ 站点列表(按板块分组,可拖拽排序)==================== */ ?>
|
|||
|
|
<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>
|
|||
|
|
|
|||
|
|
<?php /* ==================== ⑤ 板块 ==================== */ ?>
|
|||
|
|
<div class="card card-pad">
|
|||
|
|
<h2 class="cinema-h2"><?= $editGroup ? '编辑板块' : '新增板块' ?></h2>
|
|||
|
|
<form method="post" action="<?= e(url('nav/save')) ?>">
|
|||
|
|
<?= csrf_field() ?>
|
|||
|
|
<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">
|
|||
|
|
<label>板块名 *</label>
|
|||
|
|
<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>
|
|||
|
|
|
|||
|
|
<?php /* ==================== ⑥ 搜索引擎 ==================== */ ?>
|
|||
|
|
<div class="card card-pad">
|
|||
|
|
<h2 class="cinema-h2"><?= $editEngine ? '编辑搜索引擎' : '新增搜索引擎' ?></h2>
|
|||
|
|
<form method="post" action="<?= e(url('nav/save')) ?>">
|
|||
|
|
<?= csrf_field() ?>
|
|||
|
|
<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">
|
|||
|
|
<label>名称 *</label>
|
|||
|
|
<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">
|
|||
|
|
<label>搜索地址 *</label>
|
|||
|
|
<input class="input mono" name="search_url" required
|
|||
|
|
value="<?= e((string)($editEngine['search_url'] ?? '')) ?>"
|
|||
|
|
placeholder="https://cn.bing.com/search">
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>关键词参数 *</label>
|
|||
|
|
<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>
|
|||
|
|
<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>
|
|||
|
|
<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>
|
|||
|
|
<?php endif; ?>
|
|||
|
|
|
|||
|
|
<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(); });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
})();
|
|||
|
|
</script>
|