1406 lines
46 KiB
PHP
1406 lines
46 KiB
PHP
<?php
|
||
session_start();
|
||
|
||
// 配置
|
||
define('DATA_FILE', 'nav_data.json');
|
||
define('USERS_FILE', 'users.json');
|
||
define('APP_NAME', '个人导航');
|
||
define('VERSION', '1.0.0');
|
||
|
||
// 默认用户配置
|
||
$default_users = [
|
||
'admin' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi' // password: password
|
||
];
|
||
|
||
// 初始化用户文件
|
||
if (!file_exists(USERS_FILE)) {
|
||
file_put_contents(USERS_FILE, json_encode($default_users));
|
||
}
|
||
|
||
// 初始化导航数据文件
|
||
if (!file_exists(DATA_FILE)) {
|
||
$default_data = [
|
||
'quick_links' => [
|
||
['name' => 'Google', 'url' => 'https://www.google.com'],
|
||
['name' => 'GitHub', 'url' => 'https://github.com'],
|
||
['name' => 'YouTube', 'url' => 'https://www.youtube.com'],
|
||
['name' => '哔哩哔哩', 'url' => 'https://www.bilibili.com'],
|
||
['name' => '知乎', 'url' => 'https://www.zhihu.com'],
|
||
['name' => '微博', 'url' => 'https://www.weibo.com']
|
||
],
|
||
'categories' => [
|
||
[
|
||
'title' => '工作相关',
|
||
'links' => [
|
||
['name' => 'Gmail', 'url' => 'https://mail.google.com'],
|
||
['name' => 'Google Calendar', 'url' => 'https://calendar.google.com'],
|
||
['name' => 'Google Docs', 'url' => 'https://docs.google.com'],
|
||
['name' => 'Trello', 'url' => 'https://trello.com'],
|
||
['name' => 'Slack', 'url' => 'https://slack.com']
|
||
]
|
||
],
|
||
[
|
||
'title' => '开发工具',
|
||
'links' => [
|
||
['name' => 'Stack Overflow', 'url' => 'https://stackoverflow.com'],
|
||
['name' => 'MDN Web Docs', 'url' => 'https://developer.mozilla.org'],
|
||
['name' => 'CodePen', 'url' => 'https://codepen.io'],
|
||
['name' => 'CodeSandbox', 'url' => 'https://codesandbox.io'],
|
||
['name' => 'NPM', 'url' => 'https://npmjs.com']
|
||
]
|
||
],
|
||
[
|
||
'title' => '设计资源',
|
||
'links' => [
|
||
['name' => 'Dribbble', 'url' => 'https://dribbble.com'],
|
||
['name' => 'Behance', 'url' => 'https://behance.net'],
|
||
['name' => 'Unsplash', 'url' => 'https://unsplash.com'],
|
||
['name' => 'Figma', 'url' => 'https://figma.com'],
|
||
['name' => 'Coolors', 'url' => 'https://coolors.co']
|
||
]
|
||
],
|
||
[
|
||
'title' => '学习平台',
|
||
'links' => [
|
||
['name' => 'Coursera', 'url' => 'https://coursera.org'],
|
||
['name' => 'edX', 'url' => 'https://edx.org'],
|
||
['name' => 'Udemy', 'url' => 'https://udemy.com'],
|
||
['name' => 'Khan Academy', 'url' => 'https://khanacademy.org'],
|
||
['name' => 'freeCodeCamp', 'url' => 'https://freecodecamp.org']
|
||
]
|
||
],
|
||
[
|
||
'title' => '新闻资讯',
|
||
'links' => [
|
||
['name' => 'Hacker News', 'url' => 'https://news.ycombinator.com'],
|
||
['name' => 'Reddit', 'url' => 'https://www.reddit.com'],
|
||
['name' => 'Product Hunt', 'url' => 'https://producthunt.com'],
|
||
['name' => 'TechCrunch', 'url' => 'https://techcrunch.com'],
|
||
['name' => '36氪', 'url' => 'https://36kr.com']
|
||
]
|
||
],
|
||
[
|
||
'title' => '娱乐休闲',
|
||
'links' => [
|
||
['name' => 'Netflix', 'url' => 'https://www.netflix.com'],
|
||
['name' => 'Spotify', 'url' => 'https://www.spotify.com'],
|
||
['name' => 'SoundCloud', 'url' => 'https://soundcloud.com'],
|
||
['name' => 'Twitch', 'url' => 'https://www.twitch.tv'],
|
||
['name' => '豆瓣', 'url' => 'https://www.douban.com']
|
||
]
|
||
]
|
||
]
|
||
];
|
||
file_put_contents(DATA_FILE, json_encode($default_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||
}
|
||
|
||
// 读取导航数据
|
||
function getNavData() {
|
||
if (!file_exists(DATA_FILE)) {
|
||
return ['quick_links' => [], 'categories' => []];
|
||
}
|
||
$json = file_get_contents(DATA_FILE);
|
||
$data = json_decode($json, true);
|
||
if (!$data) {
|
||
return ['quick_links' => [], 'categories' => []];
|
||
}
|
||
if (!isset($data['quick_links'])) $data['quick_links'] = [];
|
||
if (!isset($data['categories'])) $data['categories'] = [];
|
||
return $data;
|
||
}
|
||
|
||
// 保存导航数据
|
||
function saveNavData($data) {
|
||
return file_put_contents(DATA_FILE, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||
}
|
||
|
||
// 验证用户
|
||
function verifyUser($username, $password) {
|
||
if (!file_exists(USERS_FILE)) {
|
||
return false;
|
||
}
|
||
$users = json_decode(file_get_contents(USERS_FILE), true);
|
||
if (!isset($users[$username])) {
|
||
return false;
|
||
}
|
||
return password_verify($password, $users[$username]);
|
||
}
|
||
|
||
// 检查是否已登录
|
||
function isLoggedIn() {
|
||
return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
|
||
}
|
||
|
||
// 处理登录
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'login') {
|
||
$username = $_POST['username'] ?? '';
|
||
$password = $_POST['password'] ?? '';
|
||
|
||
if (verifyUser($username, $password)) {
|
||
$_SESSION['logged_in'] = true;
|
||
$_SESSION['username'] = $username;
|
||
header('Location: ?');
|
||
exit;
|
||
} else {
|
||
$error = '用户名或密码错误';
|
||
}
|
||
}
|
||
|
||
// 处理登出
|
||
if (isset($_GET['logout'])) {
|
||
session_destroy();
|
||
header('Location: ?');
|
||
exit;
|
||
}
|
||
|
||
// 处理API请求
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['api'])) {
|
||
if (!isLoggedIn()) {
|
||
http_response_code(401);
|
||
echo json_encode(['error' => '未授权']);
|
||
exit;
|
||
}
|
||
|
||
header('Content-Type: application/json');
|
||
$data = getNavData();
|
||
|
||
switch ($_POST['api']) {
|
||
case 'add_category':
|
||
$title = trim($_POST['title'] ?? '');
|
||
if ($title) {
|
||
if (!isset($data['categories'])) {
|
||
$data['categories'] = [];
|
||
}
|
||
$data['categories'][] = ['title' => $title, 'links' => []];
|
||
if (saveNavData($data)) {
|
||
echo json_encode(['success' => true, 'message' => '分类添加成功']);
|
||
} else {
|
||
echo json_encode(['error' => '保存失败']);
|
||
}
|
||
} else {
|
||
echo json_encode(['error' => '分类名称不能为空']);
|
||
}
|
||
break;
|
||
|
||
case 'edit_category':
|
||
$index = intval($_POST['index'] ?? -1);
|
||
$title = trim($_POST['title'] ?? '');
|
||
if ($index >= 0 && $title) {
|
||
if (isset($data['categories'][$index])) {
|
||
$data['categories'][$index]['title'] = $title;
|
||
if (saveNavData($data)) {
|
||
echo json_encode(['success' => true, 'message' => '分类修改成功']);
|
||
} else {
|
||
echo json_encode(['error' => '保存失败']);
|
||
}
|
||
} else {
|
||
echo json_encode(['error' => '分类不存在']);
|
||
}
|
||
} else {
|
||
echo json_encode(['error' => '参数错误']);
|
||
}
|
||
break;
|
||
|
||
case 'delete_category':
|
||
$index = intval($_POST['index'] ?? -1);
|
||
if ($index >= 0 && isset($data['categories'][$index])) {
|
||
array_splice($data['categories'], $index, 1);
|
||
if (saveNavData($data)) {
|
||
echo json_encode(['success' => true, 'message' => '分类删除成功']);
|
||
} else {
|
||
echo json_encode(['error' => '保存失败']);
|
||
}
|
||
} else {
|
||
echo json_encode(['error' => '分类不存在']);
|
||
}
|
||
break;
|
||
|
||
case 'add_link':
|
||
$type = $_POST['type'] ?? 'category';
|
||
$index = intval($_POST['index'] ?? -1);
|
||
$name = trim($_POST['name'] ?? '');
|
||
$url = trim($_POST['url'] ?? '');
|
||
|
||
if ($name && $url) {
|
||
if ($type === 'quick') {
|
||
if (!isset($data['quick_links'])) {
|
||
$data['quick_links'] = [];
|
||
}
|
||
$data['quick_links'][] = ['name' => $name, 'url' => $url];
|
||
if (saveNavData($data)) {
|
||
echo json_encode(['success' => true, 'message' => '快速链接添加成功']);
|
||
} else {
|
||
echo json_encode(['error' => '保存失败']);
|
||
}
|
||
} elseif ($type === 'category' && $index >= 0 && isset($data['categories'][$index])) {
|
||
if (!isset($data['categories'][$index]['links'])) {
|
||
$data['categories'][$index]['links'] = [];
|
||
}
|
||
$data['categories'][$index]['links'][] = ['name' => $name, 'url' => $url];
|
||
if (saveNavData($data)) {
|
||
echo json_encode(['success' => true, 'message' => '链接添加成功']);
|
||
} else {
|
||
echo json_encode(['error' => '保存失败']);
|
||
}
|
||
} else {
|
||
echo json_encode(['error' => '分类不存在']);
|
||
}
|
||
} else {
|
||
echo json_encode(['error' => '链接名称和地址不能为空']);
|
||
}
|
||
break;
|
||
|
||
case 'edit_link':
|
||
$type = $_POST['type'] ?? 'category';
|
||
$categoryIndex = intval($_POST['category_index'] ?? -1);
|
||
$linkIndex = intval($_POST['link_index'] ?? -1);
|
||
$name = trim($_POST['name'] ?? '');
|
||
$url = trim($_POST['url'] ?? '');
|
||
|
||
if ($name && $url) {
|
||
if ($type === 'quick' && $linkIndex >= 0 && isset($data['quick_links'][$linkIndex])) {
|
||
$data['quick_links'][$linkIndex] = ['name' => $name, 'url' => $url];
|
||
if (saveNavData($data)) {
|
||
echo json_encode(['success' => true, 'message' => '快速链接修改成功']);
|
||
} else {
|
||
echo json_encode(['error' => '保存失败']);
|
||
}
|
||
} elseif ($type === 'category' && $categoryIndex >= 0 && $linkIndex >= 0 && isset($data['categories'][$categoryIndex])) {
|
||
$data['categories'][$categoryIndex]['links'][$linkIndex] = ['name' => $name, 'url' => $url];
|
||
if (saveNavData($data)) {
|
||
echo json_encode(['success' => true, 'message' => '链接修改成功']);
|
||
} else {
|
||
echo json_encode(['error' => '保存失败']);
|
||
}
|
||
} else {
|
||
echo json_encode(['error' => '链接不存在']);
|
||
}
|
||
} else {
|
||
echo json_encode(['error' => '链接名称和地址不能为空']);
|
||
}
|
||
break;
|
||
|
||
case 'delete_link':
|
||
$type = $_POST['type'] ?? 'category';
|
||
$categoryIndex = intval($_POST['category_index'] ?? -1);
|
||
$linkIndex = intval($_POST['link_index'] ?? -1);
|
||
|
||
if ($type === 'quick' && $linkIndex >= 0 && isset($data['quick_links'][$linkIndex])) {
|
||
array_splice($data['quick_links'], $linkIndex, 1);
|
||
if (saveNavData($data)) {
|
||
echo json_encode(['success' => true, 'message' => '快速链接删除成功']);
|
||
} else {
|
||
echo json_encode(['error' => '保存失败']);
|
||
}
|
||
} elseif ($type === 'category' && $categoryIndex >= 0 && $linkIndex >= 0 && isset($data['categories'][$categoryIndex])) {
|
||
array_splice($data['categories'][$categoryIndex]['links'], $linkIndex, 1);
|
||
if (saveNavData($data)) {
|
||
echo json_encode(['success' => true, 'message' => '链接删除成功']);
|
||
} else {
|
||
echo json_encode(['error' => '保存失败']);
|
||
}
|
||
} else {
|
||
echo json_encode(['error' => '链接不存在']);
|
||
}
|
||
break;
|
||
|
||
case 'get_data':
|
||
echo json_encode($data);
|
||
break;
|
||
|
||
default:
|
||
echo json_encode(['error' => '未知操作']);
|
||
}
|
||
exit;
|
||
}
|
||
|
||
// 如果未登录,显示登录页面
|
||
if (!isLoggedIn()) {
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>登录 - <?php echo APP_NAME; ?></title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.login-container {
|
||
background: white;
|
||
padding: 40px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
width: 100%;
|
||
max-width: 400px;
|
||
}
|
||
|
||
.login-header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.login-header h1 {
|
||
font-size: 28px;
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.login-header p {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
color: #555;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
transition: border-color 0.3s;
|
||
}
|
||
|
||
.form-input:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.login-btn {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.login-btn:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.error {
|
||
background: #fee;
|
||
color: #c33;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
margin-bottom: 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.default-info {
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background: #f8f9fa;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
color: #666;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="login-container">
|
||
<div class="login-header">
|
||
<h1><?php echo APP_NAME; ?></h1>
|
||
<p>请登录以管理您的导航</p>
|
||
</div>
|
||
|
||
<?php if (isset($error)): ?>
|
||
<div class="error"><?php echo $error; ?></div>
|
||
<?php endif; ?>
|
||
|
||
<form method="post">
|
||
<input type="hidden" name="action" value="login">
|
||
<div class="form-group">
|
||
<label class="form-label">用户名</label>
|
||
<input type="text" name="username" class="form-input" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">密码</label>
|
||
<input type="password" name="password" class="form-input" required>
|
||
</div>
|
||
<button type="submit" class="login-btn">登录</button>
|
||
</form>
|
||
|
||
<div class="default-info">
|
||
<strong>默认账号:</strong><br>
|
||
用户名:admin<br>
|
||
密码:password
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
<?php
|
||
exit;
|
||
}
|
||
|
||
// 已登录,显示导航页面
|
||
$navData = getNavData();
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title><?php echo APP_NAME; ?></title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
--bg-primary: #ffffff;
|
||
--bg-secondary: #f8f9fa;
|
||
--text-primary: #2c3e50;
|
||
--text-secondary: #6c757d;
|
||
--text-muted: #adb5bd;
|
||
--border-color: #e9ecef;
|
||
--accent-color: #3498db;
|
||
--hover-bg: #f1f3f5;
|
||
--shadow-sm: 0 2px 4px rgba(0,0,0,0.08);
|
||
--shadow-md: 0 4px 12px rgba(0,0,0,0.1);
|
||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
[data-theme="dark"] {
|
||
--bg-primary: #1a1a1a;
|
||
--bg-secondary: #2d2d2d;
|
||
--text-primary: #e9ecef;
|
||
--text-secondary: #adb5bd;
|
||
--text-muted: #6c757d;
|
||
--border-color: #404040;
|
||
--accent-color: #5dade2;
|
||
--hover-bg: #343434;
|
||
--shadow-sm: 0 2px 4px rgba(0,0,0,0.3);
|
||
--shadow-md: 0 4px 12px rgba(0,0,0,0.4);
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
background: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
line-height: 1.6;
|
||
transition: var(--transition);
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
/* Header */
|
||
header {
|
||
padding: 40px 0;
|
||
text-align: center;
|
||
border-bottom: 1px solid var(--border-color);
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
.header-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 2rem;
|
||
font-weight: 300;
|
||
letter-spacing: 2px;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.theme-toggle, .logout-btn, .manage-btn {
|
||
background: none;
|
||
border: 1px solid var(--border-color);
|
||
color: var(--text-primary);
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
font-size: 14px;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.theme-toggle:hover, .logout-btn:hover, .manage-btn:hover {
|
||
background: var(--hover-bg);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.manage-btn {
|
||
background: var(--accent-color);
|
||
color: white;
|
||
border-color: var(--accent-color);
|
||
}
|
||
|
||
.manage-btn:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
/* Search */
|
||
.search-section {
|
||
margin-bottom: 50px;
|
||
}
|
||
|
||
.search-container {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
position: relative;
|
||
}
|
||
|
||
.search-input {
|
||
width: 100%;
|
||
padding: 16px 20px;
|
||
font-size: 16px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 8px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.search-input:focus {
|
||
outline: none;
|
||
border-color: var(--accent-color);
|
||
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||
}
|
||
|
||
.search-hint {
|
||
position: absolute;
|
||
right: 20px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: var(--text-muted);
|
||
font-size: 14px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Quick Links */
|
||
.quick-links {
|
||
background: var(--bg-secondary);
|
||
border-radius: 12px;
|
||
padding: 30px;
|
||
margin-bottom: 40px;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.quick-links-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.quick-links-title {
|
||
font-size: 20px;
|
||
font-weight: 500;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.add-quick-link {
|
||
background: var(--accent-color);
|
||
color: white;
|
||
border: none;
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.add-quick-link:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.quick-links-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 20px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.quick-link {
|
||
color: var(--text-secondary);
|
||
text-decoration: none;
|
||
padding: 8px 16px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 20px;
|
||
transition: var(--transition);
|
||
font-size: 14px;
|
||
position: relative;
|
||
}
|
||
|
||
.quick-link:hover {
|
||
background: var(--accent-color);
|
||
color: white;
|
||
border-color: var(--accent-color);
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.quick-link .delete-btn {
|
||
display: none;
|
||
position: absolute;
|
||
top: -8px;
|
||
right: -8px;
|
||
background: #e74c3c;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
width: 20px;
|
||
height: 20px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.quick-link:hover .delete-btn {
|
||
display: block;
|
||
}
|
||
|
||
/* Navigation Grid */
|
||
.nav-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 30px;
|
||
margin-bottom: 50px;
|
||
}
|
||
|
||
.nav-category {
|
||
background: var(--bg-secondary);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
transition: var(--transition);
|
||
border: 1px solid var(--border-color);
|
||
position: relative;
|
||
}
|
||
|
||
.nav-category:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.category-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.category-title {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: var(--text-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.category-title::before {
|
||
content: '';
|
||
width: 3px;
|
||
height: 18px;
|
||
background: var(--accent-color);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.category-actions {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
.category-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
padding: 4px;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.category-btn:hover {
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
.link-list {
|
||
list-style: none;
|
||
}
|
||
|
||
.link-item {
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.link-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.link {
|
||
color: var(--text-secondary);
|
||
text-decoration: none;
|
||
font-size: 15px;
|
||
display: inline-block;
|
||
transition: var(--transition);
|
||
position: relative;
|
||
padding: 4px 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.link::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 0;
|
||
height: 1px;
|
||
background: var(--accent-color);
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.link:hover {
|
||
color: var(--accent-color);
|
||
transform: translateX(4px);
|
||
}
|
||
|
||
.link:hover::after {
|
||
width: 100%;
|
||
}
|
||
|
||
.link-actions {
|
||
display: none;
|
||
gap: 5px;
|
||
}
|
||
|
||
.link-item:hover .link-actions {
|
||
display: flex;
|
||
}
|
||
|
||
.link-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
padding: 2px;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.link-btn:hover {
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
.link-btn.delete {
|
||
color: #e74c3c;
|
||
}
|
||
|
||
.link-btn.delete:hover {
|
||
color: #c0392b;
|
||
}
|
||
|
||
/* Footer */
|
||
footer {
|
||
text-align: center;
|
||
padding: 30px 0;
|
||
color: var(--text-muted);
|
||
font-size: 14px;
|
||
border-top: 1px solid var(--border-color);
|
||
}
|
||
|
||
/* Modal */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.modal.active {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-content {
|
||
background: var(--bg-primary);
|
||
border-radius: 12px;
|
||
padding: 30px;
|
||
max-width: 500px;
|
||
width: 90%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
animation: slideUp 0.3s ease;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 20px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: var(--text-secondary);
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.close-btn:hover {
|
||
color: var(--text-primary);
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 6px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-input:focus {
|
||
outline: none;
|
||
border-color: var(--accent-color);
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--accent-color);
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
opacity: 0.9;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
/* Notification */
|
||
.notification {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 12px 20px;
|
||
border-radius: 6px;
|
||
color: white;
|
||
z-index: 2000;
|
||
animation: slideInRight 0.3s ease;
|
||
display: none;
|
||
}
|
||
|
||
.notification.success {
|
||
background: #27ae60;
|
||
}
|
||
|
||
.notification.error {
|
||
background: #e74c3c;
|
||
}
|
||
|
||
/* Animations */
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
transform: translateY(20px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes slideInRight {
|
||
from {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
h1 {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.header-content {
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.nav-grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 20px;
|
||
}
|
||
|
||
.quick-links-grid {
|
||
gap: 12px;
|
||
}
|
||
|
||
.quick-link {
|
||
font-size: 13px;
|
||
padding: 6px 12px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="container">
|
||
<div class="header-content">
|
||
<h1><?php echo APP_NAME; ?></h1>
|
||
<div class="header-actions">
|
||
<button class="theme-toggle" onclick="toggleTheme()">🌙 深色</button>
|
||
<a href="?logout" class="logout-btn">退出</a>
|
||
<button class="manage-btn" onclick="toggleManageMode()">管理</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="container">
|
||
<!-- Search Section -->
|
||
<section class="search-section">
|
||
<div class="search-container">
|
||
<input
|
||
type="text"
|
||
class="search-input"
|
||
id="searchInput"
|
||
placeholder="搜索网站..."
|
||
onkeyup="searchLinks(event)"
|
||
>
|
||
<span class="search-hint">Ctrl+K</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Quick Links -->
|
||
<section class="quick-links">
|
||
<div class="quick-links-header">
|
||
<h2 class="quick-links-title">快速访问</h2>
|
||
<button class="add-quick-link" onclick="addQuickLink()" style="display:none;">+ 添加</button>
|
||
</div>
|
||
<div class="quick-links-grid" id="quickLinks">
|
||
<?php foreach ($navData['quick_links'] as $index => $link): ?>
|
||
<a href="<?php echo htmlspecialchars($link['url']); ?>" class="quick-link" target="_blank">
|
||
<?php echo htmlspecialchars($link['name']); ?>
|
||
<button class="delete-btn" onclick="deleteQuickLink(event, <?php echo $index; ?>)" style="display:none;">×</button>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Navigation Grid -->
|
||
<div class="nav-grid" id="navGrid">
|
||
<?php foreach ($navData['categories'] as $categoryIndex => $category): ?>
|
||
<div class="nav-category">
|
||
<div class="category-header">
|
||
<h3 class="category-title"><?php echo htmlspecialchars($category['title']); ?></h3>
|
||
<div class="category-actions" style="display:none;">
|
||
<button class="category-btn" onclick="editCategory(<?php echo $categoryIndex; ?>)">✏️</button>
|
||
<button class="category-btn delete" onclick="deleteCategory(<?php echo $categoryIndex; ?>)">🗑️</button>
|
||
<button class="category-btn" onclick="addLink(<?php echo $categoryIndex; ?>)">➕</button>
|
||
</div>
|
||
</div>
|
||
<ul class="link-list">
|
||
<?php foreach ($category['links'] as $linkIndex => $link): ?>
|
||
<li class="link-item">
|
||
<a href="<?php echo htmlspecialchars($link['url']); ?>" class="link" target="_blank">
|
||
<?php echo htmlspecialchars($link['name']); ?>
|
||
</a>
|
||
<div class="link-actions" style="display:none;">
|
||
<button class="link-btn" onclick="editLink(<?php echo $categoryIndex; ?>, <?php echo $linkIndex; ?>)">✏️</button>
|
||
<button class="link-btn delete" onclick="deleteLink(<?php echo $categoryIndex; ?>, <?php echo $linkIndex; ?>)">🗑️</button>
|
||
</div>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
<!-- Add Category Button -->
|
||
<div class="nav-category" id="addCategoryBtn" style="display:none; border: 2px dashed var(--border-color); cursor: pointer;" onclick="addCategory()">
|
||
<div style="display: flex; align-items: center; justify-content: center; height: 100%; min-height: 200px; color: var(--text-muted);">
|
||
<div style="text-align: center;">
|
||
<div style="font-size: 48px; margin-bottom: 10px;">➕</div>
|
||
<div>添加新分类</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<footer>
|
||
<div class="container">
|
||
<p>© 2024 <?php echo APP_NAME; ?> v<?php echo VERSION; ?> | 用心收集,高效访问</p>
|
||
</div>
|
||
</footer>
|
||
|
||
<!-- Modal -->
|
||
<div class="modal" id="editModal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3 class="modal-title" id="modalTitle">编辑</h3>
|
||
<button class="close-btn" onclick="closeModal()">×</button>
|
||
</div>
|
||
<form id="editForm" onsubmit="handleSubmit(event)">
|
||
<div class="form-group">
|
||
<label class="form-label" id="label1">名称</label>
|
||
<input type="text" class="form-input" id="input1" name="input1" required>
|
||
</div>
|
||
<div class="form-group" id="urlGroup">
|
||
<label class="form-label" id="label2">链接</label>
|
||
<input type="url" class="form-input" id="input2" name="input2">
|
||
</div>
|
||
<button type="submit" class="btn-primary">保存</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Notification -->
|
||
<div class="notification" id="notification"></div>
|
||
|
||
<script>
|
||
let manageMode = false;
|
||
let currentEdit = {
|
||
type: '',
|
||
index: -1,
|
||
categoryIndex: -1,
|
||
linkIndex: -1
|
||
};
|
||
|
||
// Theme Toggle
|
||
function toggleTheme() {
|
||
const body = document.body;
|
||
const themeToggle = document.querySelector('.theme-toggle');
|
||
|
||
if (body.getAttribute('data-theme') === 'dark') {
|
||
body.removeAttribute('data-theme');
|
||
themeToggle.textContent = '🌙 深色';
|
||
localStorage.setItem('theme', 'light');
|
||
} else {
|
||
body.setAttribute('data-theme', 'dark');
|
||
themeToggle.textContent = '☀️ 浅色';
|
||
localStorage.setItem('theme', 'dark');
|
||
}
|
||
}
|
||
|
||
// Load saved theme
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
const savedTheme = localStorage.getItem('theme');
|
||
if (savedTheme === 'dark') {
|
||
document.body.setAttribute('data-theme', 'dark');
|
||
document.querySelector('.theme-toggle').textContent = '☀️ 浅色';
|
||
}
|
||
});
|
||
|
||
// Search functionality
|
||
function searchLinks(event) {
|
||
const searchTerm = event.target.value.toLowerCase();
|
||
const allLinks = document.querySelectorAll('.link');
|
||
const allCategories = document.querySelectorAll('.nav-category');
|
||
const quickLinks = document.querySelectorAll('.quick-link');
|
||
|
||
if (searchTerm === '') {
|
||
allLinks.forEach(link => {
|
||
link.parentElement.style.display = 'flex';
|
||
});
|
||
allCategories.forEach(category => {
|
||
category.style.display = 'block';
|
||
});
|
||
quickLinks.forEach(link => {
|
||
link.parentElement.style.display = 'inline-flex';
|
||
});
|
||
return;
|
||
}
|
||
|
||
allCategories.forEach(category => {
|
||
let hasVisibleLinks = false;
|
||
const links = category.querySelectorAll('.link');
|
||
|
||
links.forEach(link => {
|
||
const text = link.textContent.toLowerCase();
|
||
if (text.includes(searchTerm)) {
|
||
link.parentElement.style.display = 'flex';
|
||
hasVisibleLinks = true;
|
||
} else {
|
||
link.parentElement.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
category.style.display = hasVisibleLinks ? 'block' : 'none';
|
||
});
|
||
|
||
quickLinks.forEach(link => {
|
||
const text = link.textContent.toLowerCase();
|
||
link.parentElement.style.display = text.includes(searchTerm) ? 'inline-flex' : 'none';
|
||
});
|
||
}
|
||
|
||
// Keyboard shortcut for search
|
||
document.addEventListener('keydown', (e) => {
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||
e.preventDefault();
|
||
document.getElementById('searchInput').focus();
|
||
}
|
||
});
|
||
|
||
// Toggle manage mode
|
||
function toggleManageMode() {
|
||
manageMode = !manageMode;
|
||
const manageBtn = document.querySelector('.manage-btn');
|
||
const hiddenElements = document.querySelectorAll('.category-actions, .link-actions, .delete-btn, .add-quick-link, #addCategoryBtn');
|
||
|
||
if (manageMode) {
|
||
manageBtn.textContent = '完成';
|
||
manageBtn.style.background = '#e74c3c';
|
||
hiddenElements.forEach(el => el.style.display = '');
|
||
} else {
|
||
manageBtn.textContent = '管理';
|
||
manageBtn.style.background = '';
|
||
hiddenElements.forEach(el => el.style.display = 'none');
|
||
}
|
||
}
|
||
|
||
// Show notification
|
||
function showNotification(message, type = 'success') {
|
||
const notification = document.getElementById('notification');
|
||
notification.textContent = message;
|
||
notification.className = `notification ${type}`;
|
||
notification.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
notification.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
// Modal functions
|
||
function openModal(title, showUrl = true) {
|
||
document.getElementById('modalTitle').textContent = title;
|
||
const urlGroup = document.getElementById('urlGroup');
|
||
const urlInput = document.getElementById('input2');
|
||
|
||
if (showUrl) {
|
||
urlGroup.style.display = 'block';
|
||
urlInput.required = true;
|
||
} else {
|
||
urlGroup.style.display = 'none';
|
||
urlInput.required = false;
|
||
urlInput.value = ''; // 清空URL输入框
|
||
}
|
||
|
||
document.getElementById('editModal').classList.add('active');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('editModal').classList.remove('active');
|
||
document.getElementById('editForm').reset();
|
||
}
|
||
|
||
// Category functions
|
||
function addCategory() {
|
||
currentEdit = { type: 'add_category' };
|
||
openModal('添加分类', false);
|
||
document.getElementById('label1').textContent = '分类名称';
|
||
}
|
||
|
||
function editCategory(index) {
|
||
currentEdit = { type: 'edit_category', index };
|
||
const title = document.querySelectorAll('.category-title')[index].textContent.trim();
|
||
openModal('编辑分类', false);
|
||
document.getElementById('label1').textContent = '分类名称';
|
||
document.getElementById('input1').value = title;
|
||
}
|
||
|
||
function deleteCategory(index) {
|
||
if (confirm('确定要删除这个分类吗?')) {
|
||
apiRequest('delete_category', { index: index });
|
||
}
|
||
}
|
||
|
||
// Link functions
|
||
function addLink(categoryIndex) {
|
||
currentEdit = { type: 'add_link', categoryIndex };
|
||
openModal('添加链接', true);
|
||
document.getElementById('label1').textContent = '链接名称';
|
||
document.getElementById('label2').textContent = '链接地址';
|
||
}
|
||
|
||
function editLink(categoryIndex, linkIndex) {
|
||
currentEdit = { type: 'edit_link', categoryIndex, linkIndex };
|
||
const link = document.querySelectorAll('.nav-category')[categoryIndex].querySelectorAll('.link')[linkIndex];
|
||
openModal('编辑链接', true);
|
||
document.getElementById('label1').textContent = '链接名称';
|
||
document.getElementById('label2').textContent = '链接地址';
|
||
document.getElementById('input1').value = link.textContent.trim();
|
||
document.getElementById('input2').value = link.href;
|
||
}
|
||
|
||
function deleteLink(categoryIndex, linkIndex) {
|
||
if (confirm('确定要删除这个链接吗?')) {
|
||
apiRequest('delete_link', { type: 'category', category_index: categoryIndex, link_index: linkIndex });
|
||
}
|
||
}
|
||
|
||
// Quick link functions
|
||
function addQuickLink() {
|
||
currentEdit = { type: 'add_quick_link' };
|
||
openModal('添加快速链接', true);
|
||
document.getElementById('label1').textContent = '链接名称';
|
||
document.getElementById('label2').textContent = '链接地址';
|
||
}
|
||
|
||
function deleteQuickLink(event, index) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
if (confirm('确定要删除这个快速链接吗?')) {
|
||
apiRequest('delete_link', { type: 'quick', link_index: index });
|
||
}
|
||
}
|
||
|
||
// Handle form submit
|
||
function handleSubmit(event) {
|
||
event.preventDefault();
|
||
|
||
const input1 = document.getElementById('input1').value;
|
||
const input2 = document.getElementById('input2').value;
|
||
|
||
switch (currentEdit.type) {
|
||
case 'add_category':
|
||
apiRequest('add_category', { title: input1 });
|
||
break;
|
||
case 'edit_category':
|
||
apiRequest('edit_category', { index: currentEdit.index, title: input1 });
|
||
break;
|
||
case 'add_link':
|
||
apiRequest('add_link', { type: 'category', index: currentEdit.categoryIndex, name: input1, url: input2 });
|
||
break;
|
||
case 'edit_link':
|
||
apiRequest('edit_link', { type: 'category', category_index: currentEdit.categoryIndex, link_index: currentEdit.linkIndex, name: input1, url: input2 });
|
||
break;
|
||
case 'add_quick_link':
|
||
apiRequest('add_link', { type: 'quick', name: input1, url: input2 });
|
||
break;
|
||
}
|
||
|
||
closeModal();
|
||
}
|
||
|
||
// API request
|
||
function apiRequest(action, data) {
|
||
data.api = action;
|
||
|
||
const params = new URLSearchParams();
|
||
for (const key in data) {
|
||
params.append(key, data[key]);
|
||
}
|
||
|
||
fetch('', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: params
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success) {
|
||
showNotification(result.message || '操作成功');
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 1000);
|
||
} else {
|
||
showNotification(result.error || '操作失败', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('请求失败', 'error');
|
||
});
|
||
}
|
||
|
||
// Close modal when clicking outside
|
||
document.getElementById('editModal').addEventListener('click', (e) => {
|
||
if (e.target === e.currentTarget) {
|
||
closeModal();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
<?php
|
||
// End of file
|
||
?>
|