feat: 简航 SimpleNav 个人网站导航系统(PHP + SQLite)
- 前台:搜索引擎切换+联想下拉、网格板块(常用/工具)、合集标签页 - 后台:初始化/登录、网站/板块/引擎增删改、上下移排序 - 添加网站自动抓取名称与图标(存储于服务器),支持自定义简称 - 一键全部更新(批量刷新图标+名称,保留自定义简称) - 搜索联想服务端代理(必应/百度/谷歌/搜狗/360/DuckDuckGo) - 零依赖:原生 CSS/JS,PDO SQLite
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* 简航 SimpleNav —— 后台认证与通用工具
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require_once dirname(__DIR__) . '/functions.php';
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
function admin_count(): int
|
||||
{
|
||||
return (int)db()->query('SELECT COUNT(*) FROM users')->fetchColumn();
|
||||
}
|
||||
|
||||
function current_user(): ?array
|
||||
{
|
||||
if (empty($_SESSION['uid'])) {
|
||||
return null;
|
||||
}
|
||||
$st = db()->prepare('SELECT id, username FROM users WHERE id=?');
|
||||
$st->execute([$_SESSION['uid']]);
|
||||
$u = $st->fetch();
|
||||
return $u ?: null;
|
||||
}
|
||||
|
||||
function require_login(): void
|
||||
{
|
||||
if (!current_user()) {
|
||||
header('Location: ' . url('login.php'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function csrf_token(): string
|
||||
{
|
||||
if (empty($_SESSION['csrf'])) {
|
||||
$_SESSION['csrf'] = bin2hex(random_bytes(16));
|
||||
}
|
||||
return $_SESSION['csrf'];
|
||||
}
|
||||
|
||||
function csrf_field(): string
|
||||
{
|
||||
return '<input type="hidden" name="csrf" value="' . e(csrf_token()) . '">';
|
||||
}
|
||||
|
||||
function csrf_check(): void
|
||||
{
|
||||
$t = isset($_POST['csrf']) ? (string)$_POST['csrf'] : '';
|
||||
if (!hash_equals(csrf_token(), $t)) {
|
||||
http_response_code(419);
|
||||
exit('CSRF 校验失败,请刷新页面重试。');
|
||||
}
|
||||
}
|
||||
|
||||
/** 统一的后台页面外壳 */
|
||||
function admin_layout(string $active, string $title, string $body): void
|
||||
{
|
||||
$nav = [
|
||||
'sites' => ['网站管理', 'admin/'],
|
||||
'groups' => ['板块管理', 'admin/?p=groups'],
|
||||
'engines' => ['搜索引擎', 'admin/?p=engines'],
|
||||
'settings' => ['系统设置', 'admin/?p=settings'],
|
||||
];
|
||||
?><!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= e($title) ?> · <?= e(site_name()) ?> 后台</title>
|
||||
<link rel="stylesheet" href="<?= e(url('assets/css/style.css')) ?>">
|
||||
<link rel="stylesheet" href="<?= e(url('assets/css/admin.css')) ?>">
|
||||
</head>
|
||||
<body class="admin">
|
||||
<header class="topbar">
|
||||
<div class="brand"><a href="<?= e(url()) ?>"><?= e(site_name()) ?></a> <span>后台</span></div>
|
||||
<nav class="topnav">
|
||||
<?php foreach ($nav as $k => $v): ?>
|
||||
<a href="<?= e(url($v[1])) ?>" class="<?= $k === $active ? 'active' : '' ?>"><?= e($v[0]) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<div class="topright">
|
||||
<span class="who"><?= e(current_user()['username'] ?? '') ?></span>
|
||||
<a class="btn ghost" href="<?= e(url('logout.php')) ?>">退出</a>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap"><?= $body ?></main>
|
||||
<script src="<?= e(url('assets/js/admin.js')) ?>"></script>
|
||||
</body>
|
||||
</html><?php
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
/**
|
||||
* 简航 SimpleNav —— 后台操作处理(全部 POST + CSRF)
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/_auth.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ' . url('admin/'));
|
||||
exit;
|
||||
}
|
||||
csrf_check();
|
||||
|
||||
$pdo = db();
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
$back = 'admin/';
|
||||
if (!empty($_POST['p'])) {
|
||||
$back = 'admin/?p=' . preg_replace('/[^a-z]/', '', (string)$_POST['p']);
|
||||
}
|
||||
function flash(string $m, string $t = 'ok'): void { $_SESSION['flash'] = ['m' => $m, 't' => $t]; }
|
||||
function post_id(): int { return (int)($_POST['id'] ?? 0); }
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
|
||||
/* ---------- 网站 ---------- */
|
||||
case 'site_save': {
|
||||
$id = post_id();
|
||||
$gid = (int)($_POST['group_id'] ?? 0);
|
||||
$url = norm_url((string)($_POST['url'] ?? ''));
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$short = trim((string)($_POST['short_name'] ?? ''));
|
||||
$icon = trim((string)($_POST['icon'] ?? ''));
|
||||
$visible = isset($_POST['is_visible']) ? 1 : 0;
|
||||
if ($url === '' || $gid <= 0) {
|
||||
flash('网址和板块不能为空', 'err');
|
||||
break;
|
||||
}
|
||||
$now = date('Y-m-d H:i:s');
|
||||
if ($id > 0) {
|
||||
$st = $pdo->prepare('UPDATE sites SET group_id=?, url=?, name=?, short_name=?, icon=?, is_visible=?, updated_at=? WHERE id=?');
|
||||
$st->execute([$gid, $url, $name, $short, $icon, $visible, $now, $id]);
|
||||
flash('已保存网站');
|
||||
} else {
|
||||
$max = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM sites WHERE group_id=' . $gid)->fetchColumn();
|
||||
$st = $pdo->prepare('INSERT INTO sites (group_id,url,name,short_name,icon,sort_order,is_visible,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)');
|
||||
$st->execute([$gid, $url, $name, $short, $icon, $max, $visible, $now, $now]);
|
||||
flash('已添加网站');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'site_delete': {
|
||||
$pdo->prepare('DELETE FROM sites WHERE id=?')->execute([post_id()]);
|
||||
flash('已删除网站');
|
||||
break;
|
||||
}
|
||||
case 'site_toggle': {
|
||||
$pdo->prepare('UPDATE sites SET is_visible = 1 - is_visible WHERE id=?')->execute([post_id()]);
|
||||
flash('已切换显示状态');
|
||||
break;
|
||||
}
|
||||
case 'site_move': {
|
||||
move_sort('sites', post_id(), (string)($_POST['dir'] ?? 'up'), 'group_id');
|
||||
break;
|
||||
}
|
||||
|
||||
/* ---------- 板块 ---------- */
|
||||
case 'group_save': {
|
||||
$id = post_id();
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$display = ($_POST['display'] ?? 'grid') === 'tabs' ? 'tabs' : 'grid';
|
||||
$visible = isset($_POST['is_visible']) ? 1 : 0;
|
||||
if ($name === '') { flash('板块名称不能为空', 'err'); break; }
|
||||
if ($id > 0) {
|
||||
$pdo->prepare('UPDATE groups SET name=?, display=?, is_visible=? WHERE id=?')
|
||||
->execute([$name, $display, $visible, $id]);
|
||||
flash('已保存板块');
|
||||
} else {
|
||||
$max = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM groups')->fetchColumn();
|
||||
$pdo->prepare('INSERT INTO groups (name,slug,display,sort_order,is_visible) VALUES (?,?,?,?,?)')
|
||||
->execute([$name, slugify($name), $display, $max, $visible]);
|
||||
flash('已添加板块');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'group_delete': {
|
||||
$pdo->prepare('DELETE FROM groups WHERE id=?')->execute([post_id()]);
|
||||
flash('已删除板块(含其下网站)');
|
||||
break;
|
||||
}
|
||||
case 'group_toggle': {
|
||||
$pdo->prepare('UPDATE groups SET is_visible = 1 - is_visible WHERE id=?')->execute([post_id()]);
|
||||
flash('已切换显示状态');
|
||||
break;
|
||||
}
|
||||
case 'group_move': {
|
||||
move_sort('groups', post_id(), (string)($_POST['dir'] ?? 'up'), null);
|
||||
break;
|
||||
}
|
||||
|
||||
/* ---------- 搜索引擎 ---------- */
|
||||
case 'engine_save': {
|
||||
$id = post_id();
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$slug = preg_replace('/[^a-z0-9\-_]/i', '', (string)($_POST['slug'] ?? ''));
|
||||
$search = trim((string)($_POST['search_url'] ?? ''));
|
||||
$param = trim((string)($_POST['param'] ?? 'q'));
|
||||
$sugUrl = trim((string)($_POST['suggest_url'] ?? ''));
|
||||
$sugType = preg_replace('/[^a-z0-9_]/i', '', (string)($_POST['suggest_type'] ?? ''));
|
||||
$color = preg_match('/^#[0-9a-fA-F]{3,8}$/', (string)($_POST['color'] ?? '')) ? $_POST['color'] : '#2F6BF7';
|
||||
$active = isset($_POST['is_active']) ? 1 : 0;
|
||||
if ($name === '' || $slug === '' || $search === '') { flash('名称/标识/搜索地址不能为空', 'err'); break; }
|
||||
if ($id > 0) {
|
||||
$pdo->prepare('UPDATE engines SET name=?,slug=?,search_url=?,param=?,suggest_url=?,suggest_type=?,color=?,is_active=? WHERE id=?')
|
||||
->execute([$name, $slug, $search, $param, $sugUrl, $sugType, $color, $active, $id]);
|
||||
flash('已保存搜索引擎');
|
||||
} else {
|
||||
$max = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM engines')->fetchColumn();
|
||||
$pdo->prepare('INSERT INTO engines (name,slug,search_url,param,suggest_url,suggest_type,color,is_active,sort_order) VALUES (?,?,?,?,?,?,?,?,?)')
|
||||
->execute([$name, $slug, $search, $param, $sugUrl, $sugType, $color, $active, $max]);
|
||||
flash('已添加搜索引擎');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'engine_delete': {
|
||||
$pdo->prepare('DELETE FROM engines WHERE id=?')->execute([post_id()]);
|
||||
flash('已删除搜索引擎');
|
||||
break;
|
||||
}
|
||||
case 'engine_toggle': {
|
||||
$pdo->prepare('UPDATE engines SET is_active = 1 - is_active WHERE id=?')->execute([post_id()]);
|
||||
flash('已切换启用状态');
|
||||
break;
|
||||
}
|
||||
case 'engine_move': {
|
||||
move_sort('engines', post_id(), (string)($_POST['dir'] ?? 'up'), null);
|
||||
break;
|
||||
}
|
||||
|
||||
/* ---------- 设置 ---------- */
|
||||
case 'settings_save': {
|
||||
$map = [
|
||||
'site_name' => trim((string)($_POST['site_name'] ?? '')),
|
||||
'subtitle' => trim((string)($_POST['subtitle'] ?? '')),
|
||||
'default_engine' => preg_replace('/[^a-z0-9\-_]/i', '', (string)($_POST['default_engine'] ?? '')),
|
||||
'primary_color' => preg_match('/^#[0-9a-fA-F]{3,8}$/', (string)($_POST['primary_color'] ?? '')) ? $_POST['primary_color'] : '#2F6BF7',
|
||||
'open_blank' => isset($_POST['open_blank']) ? '1' : '0',
|
||||
];
|
||||
$st = $pdo->prepare('INSERT INTO settings (k,v) VALUES (?,?) ON CONFLICT(k) DO UPDATE SET v=excluded.v');
|
||||
foreach ($map as $k => $v) { $st->execute([$k, $v]); }
|
||||
flash('设置已保存');
|
||||
$back = 'admin/?p=settings';
|
||||
break;
|
||||
}
|
||||
case 'password_save': {
|
||||
$old = (string)($_POST['old_password'] ?? '');
|
||||
$new = (string)($_POST['new_password'] ?? '');
|
||||
$u = current_user();
|
||||
$st = $pdo->prepare('SELECT password_hash FROM users WHERE id=?');
|
||||
$st->execute([$u['id']]);
|
||||
$hash = (string)$st->fetchColumn();
|
||||
if (!password_verify($old, $hash)) { flash('原密码不正确', 'err'); $back = 'admin/?p=settings'; break; }
|
||||
if (mb_strlen($new) < 4) { flash('新密码至少 4 位', 'err'); $back = 'admin/?p=settings'; break; }
|
||||
$pdo->prepare('UPDATE users SET password_hash=? WHERE id=?')->execute([password_hash($new, PASSWORD_DEFAULT), $u['id']]);
|
||||
flash('密码已更新');
|
||||
$back = 'admin/?p=settings';
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
flash('未知操作', 'err');
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
flash('操作失败:' . $e->getMessage(), 'err');
|
||||
}
|
||||
|
||||
header('Location: ' . url($back));
|
||||
exit;
|
||||
|
||||
/** 上/下移动排序(同组内交换 sort_order) */
|
||||
function move_sort(string $table, int $id, string $dir, ?string $groupCol): void
|
||||
{
|
||||
if ($id <= 0) return;
|
||||
$pdo = db();
|
||||
$st = $pdo->prepare("SELECT * FROM {$table} WHERE id=?");
|
||||
$st->execute([$id]);
|
||||
$cur = $st->fetch();
|
||||
if (!$cur) return;
|
||||
|
||||
$where = $groupCol ? " AND {$groupCol}=" . (int)$cur[$groupCol] : '';
|
||||
if ($dir === 'up') {
|
||||
$sql = "SELECT * FROM {$table} WHERE sort_order < ?{$where} ORDER BY sort_order DESC LIMIT 1";
|
||||
} else {
|
||||
$sql = "SELECT * FROM {$table} WHERE sort_order > ?{$where} ORDER BY sort_order ASC LIMIT 1";
|
||||
}
|
||||
$st2 = $pdo->prepare($sql);
|
||||
$st2->execute([$cur['sort_order']]);
|
||||
$nb = $st2->fetch();
|
||||
if (!$nb) return;
|
||||
|
||||
$upd = $pdo->prepare("UPDATE {$table} SET sort_order=? WHERE id=?");
|
||||
$upd->execute([$nb['sort_order'], $cur['id']]);
|
||||
$upd->execute([$cur['sort_order'], $nb['id']]);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* 简航 SimpleNav —— 添加/编辑网站时自动抓取元信息(供后台 AJAX 调用)
|
||||
* 返回:{ ok, name, short, icon, url }
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/_auth.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$url = isset($_GET['url']) ? norm_url((string)$_GET['url']) : '';
|
||||
if ($url === '') {
|
||||
echo json_encode(['ok' => false, 'msg' => '缺少网址'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$meta = fetch_site_meta($url);
|
||||
$domain = domain_of($meta['final_url'] ?: $url);
|
||||
$iconFile = '';
|
||||
if (!empty($meta['icon'])) {
|
||||
$iconFile = download_icon($meta['icon'], $domain);
|
||||
}
|
||||
if ($iconFile === '' && $domain !== '') {
|
||||
// 兜底再试一次根目录 favicon.ico
|
||||
$iconFile = download_icon('http://' . $domain . '/favicon.ico', $domain);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'url' => $url,
|
||||
'name' => $meta['name'] !== '' ? $meta['name'] : ($meta['title'] !== '' ? $meta['title'] : $domain),
|
||||
'short' => $meta['name'] !== '' ? $meta['name'] : $domain,
|
||||
'icon' => $iconFile,
|
||||
'icon_url' => $iconFile !== '' ? icon_src($iconFile) : '',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
/**
|
||||
* 简航 SimpleNav —— 后台首页(网站 / 板块 / 搜索引擎 / 设置)
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/_auth.php';
|
||||
require_login();
|
||||
|
||||
$pdo = db();
|
||||
$p = isset($_GET['p']) ? (string)$_GET['p'] : 'sites';
|
||||
if (!in_array($p, ['sites', 'groups', 'engines', 'settings'], true)) {
|
||||
$p = 'sites';
|
||||
}
|
||||
$flash = $_SESSION['flash'] ?? null;
|
||||
unset($_SESSION['flash']);
|
||||
|
||||
$groups = $pdo->query('SELECT * FROM groups ORDER BY sort_order, id')->fetchAll();
|
||||
$engines = $pdo->query('SELECT * FROM engines ORDER BY sort_order, id')->fetchAll();
|
||||
|
||||
$SUGGEST_TYPES = ['bing_osjson', 'baidu_sugrec', 'google_firefox', 'sogou_ajaj', 'so360', 'ddg'];
|
||||
|
||||
function flash_html(?array $f): string
|
||||
{
|
||||
if (!$f) return '';
|
||||
$cls = ($f['t'] ?? 'ok') === 'err' ? 'alert err' : 'alert ok';
|
||||
return '<div class="' . $cls . '">' . e($f['m']) . '</div>';
|
||||
}
|
||||
|
||||
function toggle_form(string $action, int $id): string
|
||||
{
|
||||
return '<form method="post" action="' . e(url('admin/actions.php')) . '" class="inline">'
|
||||
. csrf_field() . '<input type="hidden" name="action" value="' . e($action) . '">'
|
||||
. '<input type="hidden" name="id" value="' . $id . '">'
|
||||
. '<button class="ico-btn" title="切换显示">' . "\u{1F441}" . '</button></form>';
|
||||
}
|
||||
|
||||
function move_form(string $action, int $id, string $dir, string $label, string $p): string
|
||||
{
|
||||
return '<form method="post" action="' . e(url('admin/actions.php')) . '" class="inline">'
|
||||
. csrf_field() . '<input type="hidden" name="action" value="' . e($action) . '">'
|
||||
. '<input type="hidden" name="id" value="' . $id . '">'
|
||||
. '<input type="hidden" name="dir" value="' . $dir . '">'
|
||||
. '<input type="hidden" name="p" value="' . e($p) . '">'
|
||||
. '<button class="ico-btn" title="' . e($label) . '">' . ($dir === 'up' ? '↑' : '↓') . '</button></form>';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
if ($p === 'sites'):
|
||||
$rows = $pdo->query('SELECT s.*, g.name AS gname FROM sites s JOIN groups g ON g.id=s.group_id ORDER BY g.sort_order, g.id, s.sort_order, s.id')->fetchAll();
|
||||
?>
|
||||
<?= flash_html($flash) ?>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head">
|
||||
<h2>新增 / 编辑网站</h2>
|
||||
<button type="button" class="btn ghost" id="btn-update-all" data-url="<?= e(url('admin/update_all.php')) ?>">⟳ 全部更新(图标+名称)</button>
|
||||
</div>
|
||||
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="grid-form" id="site-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="site_save">
|
||||
<input type="hidden" name="id" id="f-id" value="0">
|
||||
<input type="hidden" name="icon" id="f-icon" value="">
|
||||
<label>板块
|
||||
<select name="group_id" id="f-group" required>
|
||||
<?php foreach ($groups as $g): ?>
|
||||
<option value="<?= (int)$g['id'] ?>"><?= e($g['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label class="span2">网址
|
||||
<span class="row">
|
||||
<input type="text" name="url" id="f-url" placeholder="https://example.com" required>
|
||||
<button type="button" class="btn ghost" id="btn-fetch" data-url="<?= e(url('admin/fetch_meta.php')) ?>">自动获取</button>
|
||||
</span>
|
||||
</label>
|
||||
<label>网站名称(自动)
|
||||
<input type="text" name="name" id="f-name" placeholder="自动抓取">
|
||||
</label>
|
||||
<label>简称(页面显示,手动)
|
||||
<input type="text" name="short_name" id="f-short" placeholder="如:知乎">
|
||||
</label>
|
||||
<label class="chk"><input type="checkbox" name="is_visible" id="f-visible" checked> 显示</label>
|
||||
<div class="span2 form-actions">
|
||||
<button type="submit" class="btn primary">保存</button>
|
||||
<button type="button" class="btn ghost" id="btn-reset">清空</button>
|
||||
<span class="hint" id="fetch-hint"></span>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head"><h2>网站列表(<?= count($rows) ?>)</h2></div>
|
||||
<table class="tbl">
|
||||
<thead><tr><th>图标</th><th>简称(展示)</th><th>名称</th><th>网址</th><th>板块</th><th>显示</th><th>排序</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $r): $label = $r['short_name'] !== '' ? $r['short_name'] : ($r['name'] !== '' ? $r['name'] : domain_of($r['url'])); $src = icon_src($r['icon']); ?>
|
||||
<tr>
|
||||
<td><?php if ($src): ?><img class="mini" src="<?= e($src) ?>" alt=""><?php else: ?><span class="mini letter" style="background:<?= e(letter_color($label)) ?>"><?= e(mb_substr($label, 0, 1)) ?></span><?php endif; ?></td>
|
||||
<td><?= e($label) ?></td>
|
||||
<td class="muted"><?= e($r['name']) ?></td>
|
||||
<td class="muted ell"><a href="<?= e($r['url']) ?>" target="_blank" rel="noopener"><?= e($r['url']) ?></a></td>
|
||||
<td><?= e($r['gname']) ?></td>
|
||||
<td><?= toggle_form('site_toggle', (int)$r['id']) ?></td>
|
||||
<td class="nowrap"><?= move_form('site_move', (int)$r['id'], 'up', '上移', '') . move_form('site_move', (int)$r['id'], 'down', '下移', '') ?></td>
|
||||
<td class="nowrap">
|
||||
<button type="button" class="ico-btn edit-site"
|
||||
data-id="<?= (int)$r['id'] ?>" data-group="<?= (int)$r['group_id'] ?>"
|
||||
data-url="<?= e($r['url']) ?>" data-name="<?= e($r['name']) ?>"
|
||||
data-short="<?= e($r['short_name']) ?>" data-icon="<?= e($r['icon']) ?>"
|
||||
data-visible="<?= (int)$r['is_visible'] ?>" title="编辑">✎</button>
|
||||
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="inline" onsubmit="return confirm('确定删除该网站?');">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="site_delete">
|
||||
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
|
||||
<button class="ico-btn danger" title="删除">✕</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<?php
|
||||
|
||||
elseif ($p === 'groups'): ?>
|
||||
<?= flash_html($flash) ?>
|
||||
<section class="card">
|
||||
<div class="card-head"><h2>新增 / 编辑板块</h2></div>
|
||||
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="grid-form" id="group-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="group_save">
|
||||
<input type="hidden" name="id" id="g-id" value="0">
|
||||
<input type="hidden" name="p" value="groups">
|
||||
<label>板块名称<input type="text" name="name" id="g-name" required></label>
|
||||
<label>展示方式
|
||||
<select name="display" id="g-display">
|
||||
<option value="grid">普通网格板块(如:常用 / 工具)</option>
|
||||
<option value="tabs">合集标签页</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="chk"><input type="checkbox" name="is_visible" id="g-visible" checked> 显示</label>
|
||||
<div class="span2 form-actions"><button class="btn primary">保存</button><button type="button" class="btn ghost" id="g-reset">清空</button></div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="card">
|
||||
<table class="tbl">
|
||||
<thead><tr><th>名称</th><th>展示方式</th><th>网站数</th><th>显示</th><th>排序</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($groups as $g): $cnt = (int)$pdo->query('SELECT COUNT(*) FROM sites WHERE group_id=' . (int)$g['id'])->fetchColumn(); ?>
|
||||
<tr>
|
||||
<td><?= e($g['name']) ?></td>
|
||||
<td><?= $g['display'] === 'tabs' ? '合集标签页' : '普通网格' ?></td>
|
||||
<td><?= $cnt ?></td>
|
||||
<td><?= toggle_form('group_toggle', (int)$g['id']) ?></td>
|
||||
<td class="nowrap"><?= move_form('group_move', (int)$g['id'], 'up', '上移', 'groups') . move_form('group_move', (int)$g['id'], 'down', '下移', 'groups') ?></td>
|
||||
<td class="nowrap">
|
||||
<button type="button" class="ico-btn edit-group" data-id="<?= (int)$g['id'] ?>" data-name="<?= e($g['name']) ?>" data-display="<?= e($g['display']) ?>" data-visible="<?= (int)$g['is_visible'] ?>">✎</button>
|
||||
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="inline" onsubmit="return confirm('删除板块会同时删除其下所有网站,确定?');">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="group_delete"><input type="hidden" name="p" value="groups"><input type="hidden" name="id" value="<?= (int)$g['id'] ?>">
|
||||
<button class="ico-btn danger">✕</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<?php
|
||||
|
||||
elseif ($p === 'engines'): ?>
|
||||
<?= flash_html($flash) ?>
|
||||
<section class="card">
|
||||
<div class="card-head"><h2>新增 / 编辑搜索引擎</h2></div>
|
||||
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="grid-form" id="engine-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="engine_save">
|
||||
<input type="hidden" name="id" id="e-id" value="0">
|
||||
<input type="hidden" name="p" value="engines">
|
||||
<label>名称<input type="text" name="name" id="e-name" required></label>
|
||||
<label>标识 slug<input type="text" name="slug" id="e-slug" placeholder="bing" required></label>
|
||||
<label class="span2">搜索地址<input type="text" name="search_url" id="e-search" placeholder="https://cn.bing.com/search" required></label>
|
||||
<label>参数名<input type="text" name="param" id="e-param" placeholder="q / wd / query"></label>
|
||||
<label>主题色<input type="text" name="color" id="e-color" placeholder="#60A504"></label>
|
||||
<label class="span2">联想接口({q} 为关键词占位)<input type="text" name="suggest_url" id="e-sug" placeholder="https://api.bing.com/osjson.aspx?query={q}"></label>
|
||||
<label>联想解析类型
|
||||
<select name="suggest_type" id="e-sugtype">
|
||||
<?php foreach ($SUGGEST_TYPES as $t): ?><option value="<?= e($t) ?>"><?= e($t) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label class="chk"><input type="checkbox" name="is_active" id="e-active"> 启用(显示在首页)</label>
|
||||
<div class="span2 form-actions"><button class="btn primary">保存</button><button type="button" class="btn ghost" id="e-reset">清空</button></div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="card">
|
||||
<table class="tbl">
|
||||
<thead><tr><th>名称</th><th>搜索地址</th><th>联想类型</th><th>启用</th><th>排序</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($engines as $en): ?>
|
||||
<tr>
|
||||
<td><span style="color:<?= e($en['color']) ?>">■</span> <?= e($en['name']) ?> <span class="muted">(<?= e($en['slug']) ?>)</span></td>
|
||||
<td class="muted ell"><?= e($en['search_url']) ?></td>
|
||||
<td class="muted"><?= e($en['suggest_type']) ?></td>
|
||||
<td><?= $en['is_active'] ? '<span class="ok-dot">已启用</span>' : '<span class="muted">未启用</span>' ?> <?= toggle_form('engine_toggle', (int)$en['id']) ?></td>
|
||||
<td class="nowrap"><?= move_form('engine_move', (int)$en['id'], 'up', '上移', 'engines') . move_form('engine_move', (int)$en['id'], 'down', '下移', 'engines') ?></td>
|
||||
<td class="nowrap">
|
||||
<button type="button" class="ico-btn edit-engine"
|
||||
data-id="<?= (int)$en['id'] ?>" data-name="<?= e($en['name']) ?>" data-slug="<?= e($en['slug']) ?>"
|
||||
data-search="<?= e($en['search_url']) ?>" data-param="<?= e($en['param']) ?>" data-sug="<?= e($en['suggest_url']) ?>"
|
||||
data-sugtype="<?= e($en['suggest_type']) ?>" data-color="<?= e($en['color']) ?>" data-active="<?= (int)$en['is_active'] ?>">✎</button>
|
||||
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="inline" onsubmit="return confirm('确定删除该引擎?');">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="engine_delete"><input type="hidden" name="p" value="engines"><input type="hidden" name="id" value="<?= (int)$en['id'] ?>">
|
||||
<button class="ico-btn danger">✕</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="hint">提示:联想接口需填写对应平台的地址,解析类型可选 bing_osjson / baidu_sugrec / google_firefox / sogou_ajaj / so360 / ddg。</p>
|
||||
</section>
|
||||
<?php
|
||||
|
||||
else: // settings ?>
|
||||
<?= flash_html($flash) ?>
|
||||
<section class="card">
|
||||
<div class="card-head"><h2>站点设置</h2></div>
|
||||
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="grid-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="settings_save">
|
||||
<label>站点名称<input type="text" name="site_name" value="<?= e(setting('site_name', '简航')) ?>"></label>
|
||||
<label>副标题<input type="text" name="subtitle" value="<?= e(setting('subtitle', '个人网站导航')) ?>"></label>
|
||||
<label>默认搜索引擎
|
||||
<select name="default_engine">
|
||||
<?php foreach ($engines as $en): ?>
|
||||
<option value="<?= e($en['slug']) ?>" <?= setting('default_engine') === $en['slug'] ? 'selected' : '' ?>><?= e($en['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>主题色<input type="text" name="primary_color" value="<?= e(setting('primary_color', '#2F6BF7')) ?>"></label>
|
||||
<label class="chk"><input type="checkbox" name="open_blank" <?= setting('open_blank', '1') === '1' ? 'checked' : '' ?>> 搜索结果新窗口打开</label>
|
||||
<div class="span2 form-actions"><button class="btn primary">保存设置</button></div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="card">
|
||||
<div class="card-head"><h2>修改管理员密码</h2></div>
|
||||
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="grid-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="password_save">
|
||||
<label>原密码<input type="password" name="old_password" autocomplete="current-password" required></label>
|
||||
<label>新密码<input type="password" name="new_password" autocomplete="new-password" required></label>
|
||||
<div class="span2 form-actions"><button class="btn primary">修改密码</button></div>
|
||||
</form>
|
||||
</section>
|
||||
<?php
|
||||
endif;
|
||||
|
||||
$body = ob_get_clean();
|
||||
admin_layout($p, '后台', $body);
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* 简航 SimpleNav —— 一键“全部更新”(分批)。
|
||||
* 批量刷新每个网站的图标与名称;**保留用户自定义的简称 short_name 不变**。
|
||||
* 前端按 offset 分批调用,避免一次请求超时。
|
||||
* 返回:{ done, total, offset, updated, failed }
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/_auth.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
set_time_limit(30);
|
||||
|
||||
$offset = max(0, (int)($_GET['offset'] ?? 0));
|
||||
$limit = min(12, max(1, (int)($_GET['limit'] ?? 6)));
|
||||
$pdo = db();
|
||||
|
||||
$total = (int)$pdo->query('SELECT COUNT(*) FROM sites')->fetchColumn();
|
||||
$rows = $pdo->query('SELECT id, url FROM sites ORDER BY id LIMIT ' . $limit . ' OFFSET ' . $offset)->fetchAll();
|
||||
|
||||
$updated = 0;
|
||||
$failed = 0;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$url = norm_url((string)$row['url']);
|
||||
$meta = fetch_site_meta($url);
|
||||
$domain = domain_of($meta['final_url'] ?: $url);
|
||||
$name = $meta['name'] !== '' ? $meta['name'] : $meta['title'];
|
||||
$iconFile = '';
|
||||
if (!empty($meta['icon'])) {
|
||||
$iconFile = download_icon($meta['icon'], $domain);
|
||||
}
|
||||
if ($iconFile === '' && $domain !== '') {
|
||||
$iconFile = download_icon('http://' . $domain . '/favicon.ico', $domain);
|
||||
}
|
||||
|
||||
$sets = ['updated_at=?'];
|
||||
$args = [$now];
|
||||
if ($name !== '') { $sets[] = 'name=?'; $args[] = $name; }
|
||||
if ($iconFile !== '') { $sets[] = 'icon=?'; $args[] = $iconFile; }
|
||||
// 注意:这里绝不修改 short_name(用户自定义简称)
|
||||
|
||||
$args[] = (int)$row['id'];
|
||||
try {
|
||||
$pdo->prepare('UPDATE sites SET ' . implode(',', $sets) . ' WHERE id=?')->execute($args);
|
||||
if ($iconFile !== '' || $name !== '') { $updated++; } else { $failed++; }
|
||||
} catch (Throwable $e) {
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
$next = $offset + count($rows);
|
||||
echo json_encode([
|
||||
'done' => $next >= $total || count($rows) === 0,
|
||||
'total' => $total,
|
||||
'offset' => $next,
|
||||
'updated' => $updated,
|
||||
'failed' => $failed,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
Reference in New Issue
Block a user