Files
Ai-Single-Page/z.ai/notes/index.php
T
2025-10-12 19:27:28 +00:00

2367 lines
76 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
session_start();
// 配置
define('DB_FILE', 'notes.db');
define('UPLOAD_DIR', 'uploads');
define('APP_NAME', '简约记事本');
define('VERSION', '1.0.0');
define('NOTES_PER_PAGE', 25);
define('ENCRYPTION_KEY', 'YourSecretKey123!@#'); // 修改为你自己的密钥
// 确保上传目录存在
if (!file_exists(UPLOAD_DIR)) {
mkdir(UPLOAD_DIR, 0755, true);
}
// 获取当前页面基础URL
function getBaseUrl() {
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$path = dirname($_SERVER['PHP_SELF']);
return $protocol . '://' . $host . $path;
}
// 获取当前文件名
function getCurrentFile() {
return basename($_SERVER['PHP_SELF']);
}
// 获取返回首页的URL
function getHomeUrl() {
$currentFile = getCurrentFile();
// 如果当前文件是 index.php 或者是目录下的默认文件
if ($currentFile === 'index.php' || $currentFile === basename(dirname($_SERVER['PHP_SELF'])) . '.php') {
return getBaseUrl() . '/';
} else {
return getBaseUrl() . '/' . $currentFile;
}
}
// 加密类
class Encryption {
private static $method = 'AES-256-CBC';
private static $key = ENCRYPTION_KEY;
public static function encrypt($data) {
if (empty($data)) return $data;
$iv = openssl_random_pseudo_bytes(16);
$encrypted = openssl_encrypt($data, self::$method, self::$key, 0, $iv);
return base64_encode($iv . $encrypted);
}
public static function decrypt($data) {
if (empty($data)) return $data;
$data = base64_decode($data);
$iv = substr($data, 0, 16);
$encrypted = substr($data, 16);
return openssl_decrypt($encrypted, self::$method, self::$key, 0, $iv);
}
}
// 初始化数据库
function initDatabase() {
try {
$db = new SQLite3(DB_FILE);
// 创建用户表
$db->exec('
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
');
// 创建笔记表(加密存储)
$db->exec('
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title_enc TEXT NOT NULL,
content_enc TEXT NOT NULL,
tags TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
');
// 创建附件表
$db->exec('
CREATE TABLE IF NOT EXISTS attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER NOT NULL,
filename TEXT NOT NULL,
original_name TEXT NOT NULL,
file_size INTEGER NOT NULL,
file_type TEXT NOT NULL,
upload_time DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES notes (id) ON DELETE CASCADE
)
');
// 创建索引
$db->exec('CREATE INDEX IF NOT EXISTS idx_notes_tags ON notes(tags)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(created_at)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_attachments_note_id ON attachments(note_id)');
return $db;
} catch (Exception $e) {
die('数据库初始化失败: ' . $e->getMessage());
}
}
// 获取数据库连接
function getDB() {
static $db = null;
if ($db === null) {
$db = initDatabase();
}
return $db;
}
// 检查是否已初始化
function isInitialized() {
$db = getDB();
$result = $db->querySingle('SELECT COUNT(*) FROM users');
return $result > 0;
}
// 验证用户登录
function verifyUser($username, $password) {
$db = getDB();
$stmt = $db->prepare('SELECT password FROM users WHERE username = ?');
$stmt->bindValue(1, $username, SQLITE3_TEXT);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user && password_verify($password, $user['password'])) {
return true;
}
return false;
}
// 创建用户
function createUser($username, $password) {
$db = getDB();
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
$stmt->bindValue(1, $username, SQLITE3_TEXT);
$stmt->bindValue(2, $hashedPassword, SQLITE3_TEXT);
return $stmt->execute();
}
// 更新用户密码
function updatePassword($newPassword) {
$db = getDB();
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
$stmt = $db->prepare('UPDATE users SET password = ? WHERE id = 1');
$stmt->bindValue(1, $hashedPassword, SQLITE3_TEXT);
return $stmt->execute();
}
// 获取笔记总数
function getNotesCount($search = '', $tag = '') {
$db = getDB();
$sql = 'SELECT COUNT(*) FROM notes WHERE 1=1';
$params = [];
if ($search) {
// 搜索需要解密,这里简化处理,只搜索标签
$sql .= ' AND tags LIKE ?';
$searchParam = '%' . $search . '%';
$params[] = $searchParam;
}
if ($tag) {
$sql .= ' AND tags LIKE ?';
$params[] = '%' . $tag . '%';
}
$stmt = $db->prepare($sql);
foreach ($params as $i => $param) {
$stmt->bindValue($i + 1, $param, SQLITE3_TEXT);
}
return $stmt->execute()->fetchArray()[0];
}
// 获取笔记列表(分页)
function getNotes($page = 1, $search = '', $tag = '') {
$db = getDB();
$offset = ($page - 1) * NOTES_PER_PAGE;
$sql = 'SELECT * FROM notes WHERE 1=1';
$params = [];
if ($search) {
$sql .= ' AND tags LIKE ?';
$params[] = '%' . $search . '%';
}
if ($tag) {
$sql .= ' AND tags LIKE ?';
$params[] = '%' . $tag . '%';
}
$sql .= ' ORDER BY updated_at DESC LIMIT ? OFFSET ?';
$params[] = NOTES_PER_PAGE;
$params[] = $offset;
$stmt = $db->prepare($sql);
foreach ($params as $i => $param) {
$stmt->bindValue($i + 1, $param, SQLITE3_TEXT);
}
$result = $stmt->execute();
$notes = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
// 解密数据
$row['title'] = Encryption::decrypt($row['title_enc']);
$row['content'] = Encryption::decrypt($row['content_enc']);
unset($row['title_enc'], $row['content_enc']);
$notes[] = $row;
}
return $notes;
}
// 获取单条笔记
function getNote($id) {
$db = getDB();
$stmt = $db->prepare('SELECT * FROM notes WHERE id = ?');
$stmt->bindValue(1, $id, SQLITE3_INTEGER);
$result = $stmt->execute();
$note = $result->fetchArray(SQLITE3_ASSOC);
if ($note) {
// 解密数据
$note['title'] = Encryption::decrypt($note['title_enc']);
$note['content'] = Encryption::decrypt($note['content_enc']);
unset($note['title_enc'], $note['content_enc']);
}
return $note;
}
// 创建笔记
function createNote($title, $content, $tags) {
$db = getDB();
$stmt = $db->prepare('INSERT INTO notes (title_enc, content_enc, tags) VALUES (?, ?, ?)');
$stmt->bindValue(1, Encryption::encrypt($title), SQLITE3_TEXT);
$stmt->bindValue(2, Encryption::encrypt($content), SQLITE3_TEXT);
$stmt->bindValue(3, $tags, SQLITE3_TEXT);
$stmt->execute();
return $db->lastInsertRowID();
}
// 更新笔记
function updateNote($id, $title, $content, $tags) {
$db = getDB();
$stmt = $db->prepare('UPDATE notes SET title_enc = ?, content_enc = ?, tags = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?');
$stmt->bindValue(1, Encryption::encrypt($title), SQLITE3_TEXT);
$stmt->bindValue(2, Encryption::encrypt($content), SQLITE3_TEXT);
$stmt->bindValue(3, $tags, SQLITE3_TEXT);
$stmt->bindValue(4, $id, SQLITE3_INTEGER);
return $stmt->execute();
}
// 删除笔记
function deleteNote($id) {
$db = getDB();
$stmt = $db->prepare('DELETE FROM notes WHERE id = ?');
$stmt->bindValue(1, $id, SQLITE3_INTEGER);
return $stmt->execute();
}
// 获取笔记附件
function getNoteAttachments($noteId) {
$db = getDB();
$stmt = $db->prepare('SELECT * FROM attachments WHERE note_id = ? ORDER BY upload_time DESC');
$stmt->bindValue(1, $noteId, SQLITE3_INTEGER);
$result = $stmt->execute();
$attachments = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$attachments[] = $row;
}
return $attachments;
}
// 添加附件
function addAttachment($noteId, $filename, $originalName, $fileSize, $fileType) {
$db = getDB();
$stmt = $db->prepare('INSERT INTO attachments (note_id, filename, original_name, file_size, file_type) VALUES (?, ?, ?, ?, ?)');
$stmt->bindValue(1, $noteId, SQLITE3_INTEGER);
$stmt->bindValue(2, $filename, SQLITE3_TEXT);
$stmt->bindValue(3, $originalName, SQLITE3_TEXT);
$stmt->bindValue(4, $fileSize, SQLITE3_INTEGER);
$stmt->bindValue(5, $fileType, SQLITE3_TEXT);
return $stmt->execute();
}
// 删除附件
function deleteAttachment($id) {
$db = getDB();
$stmt = $db->prepare('SELECT filename FROM attachments WHERE id = ?');
$stmt->bindValue(1, $id, SQLITE3_INTEGER);
$result = $stmt->execute();
$attachment = $result->fetchArray(SQLITE3_ASSOC);
if ($attachment && file_exists(UPLOAD_DIR . '/' . $attachment['filename'])) {
unlink(UPLOAD_DIR . '/' . $attachment['filename']);
}
$stmt = $db->prepare('DELETE FROM attachments WHERE id = ?');
$stmt->bindValue(1, $id, SQLITE3_INTEGER);
return $stmt->execute();
}
// 获取所有标签
function getAllTags() {
$db = getDB();
$result = $db->query('SELECT tags FROM notes WHERE tags IS NOT NULL AND tags != ""');
$tags = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$noteTags = explode(',', $row['tags']);
foreach ($noteTags as $tag) {
$tag = trim($tag);
if ($tag && !in_array($tag, $tags)) {
$tags[] = $tag;
}
}
}
sort($tags);
return $tags;
}
// 处理文件上传
function handleFileUpload($file, $noteId) {
if ($file['error'] !== UPLOAD_ERR_OK) {
return false;
}
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
$maxSize = 10 * 1024 * 1024; // 10MB
if (!in_array($file['type'], $allowedTypes) || $file['size'] > $maxSize) {
return false;
}
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = uniqid() . '.' . $extension;
$uploadPath = UPLOAD_DIR . '/' . $filename;
if (move_uploaded_file($file['tmp_name'], $uploadPath)) {
addAttachment($noteId, $filename, $file['name'], $file['size'], $file['type']);
return $filename;
}
return false;
}
// Markdown转HTML(增强版)
function markdownToHtml($markdown) {
// 简单的Markdown解析
$html = $markdown;
// 图片(支持本地上传的图片)
$html = preg_replace('/!\[(.*?)\]\((.*?)\)/', '<img src="$2" alt="$1" style="max-width: 100%; height: auto; border-radius: 6px; margin: 10px 0;">', $html);
// 标题
$html = preg_replace('/^### (.*$)/m', '<h3>$1</h3>', $html);
$html = preg_replace('/^## (.*$)/m', '<h2>$1</h2>', $html);
$html = preg_replace('/^# (.*$)/m', '<h1>$1</h1>', $html);
// 粗体和斜体
$html = preg_replace('/\*\*(.*?)\*\*/', '<strong>$1</strong>', $html);
$html = preg_replace('/\*(.*?)\*/', '<em>$1</em>', $html);
// 代码块
$html = preg_replace('/```(.*?)```/s', '<pre><code>$1</code></pre>', $html);
$html = preg_replace('/`(.*?)`/', '<code>$1</code>', $html);
// 链接
$html = preg_replace('/\[(.*?)\]\((.*?)\)/', '<a href="$2" target="_blank">$1</a>', $html);
// 无序列表
$html = preg_replace('/^\* (.+)$/m', '<li>$1</li>', $html);
$html = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $html);
// 有序列表
$html = preg_replace('/^\d+\. (.+)$/m', '<li>$1</li>', $html);
// 引用
$html = preg_replace('/^> (.+)$/m', '<blockquote>$1</blockquote>', $html);
// 换行
$html = preg_replace('/\n\n/', '</p><p>', $html);
$html = preg_replace('/\n/', '<br>', $html);
// 段落
$html = '<p>' . $html . '</p>';
// 清理多余的段落标签
$html = preg_replace('/<p><\/p>/', '', $html);
$html = preg_replace('/<p>(<h[1-6]>)/', '$1', $html);
$html = preg_replace('/(<\/h[1-6]>)<\/p>/', '$1', $html);
$html = preg_replace('/<p>(<pre>)/', '$1', $html);
$html = preg_replace('/(<\/pre>)<\/p>/', '$1', $html);
$html = preg_replace('/<p>(<ul>)/', '$1', $html);
$html = preg_replace('/(<\/ul>)<\/p>/', '$1', $html);
$html = preg_replace('/<p>(<blockquote>)/', '$1', $html);
$html = preg_replace('/(<\/blockquote>)<\/p>/', '$1', $html);
$html = preg_replace('/<p>(<li>)/', '$1', $html);
$html = preg_replace('/(<\/li>)<\/p>/', '$1', $html);
return $html;
}
// 检查登录状态
function isLoggedIn() {
return isset($_SESSION['user_id']);
}
// 处理登录
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'init') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if ($username && $password) {
if (createUser($username, $password)) {
$_SESSION['user_id'] = 1;
$_SESSION['username'] = $username;
header('Location: ?');
exit;
} else {
$error = '初始化失败';
}
} else {
$error = '请填写完整信息';
}
} elseif ($_POST['action'] === 'login') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (verifyUser($username, $password)) {
$_SESSION['user_id'] = 1;
$_SESSION['username'] = $username;
header('Location: ?');
exit;
} else {
$error = '用户名或密码错误';
}
} elseif ($_POST['action'] === 'change_password') {
$oldPassword = $_POST['old_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
if (verifyUser($_SESSION['username'], $oldPassword)) {
if (updatePassword($newPassword)) {
$success = '密码修改成功';
} else {
$error = '密码修改失败';
}
} else {
$error = '原密码错误';
}
} elseif ($_POST['action'] === 'save_note') {
if (!isLoggedIn()) {
header('Location: ?');
exit;
}
$id = intval($_POST['id'] ?? 0);
$title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? '');
$tags = trim($_POST['tags'] ?? '');
if ($title && $content) {
if ($id > 0) {
updateNote($id, $title, $content, $tags);
$noteId = $id;
} else {
$noteId = createNote($title, $content, $tags);
}
// 处理文件上传
if (!empty($_FILES['attachments']['name'][0])) {
foreach ($_FILES['attachments']['name'] as $key => $name) {
if (!empty($name)) {
$file = [
'name' => $name,
'type' => $_FILES['attachments']['type'][$key],
'tmp_name' => $_FILES['attachments']['tmp_name'][$key],
'error' => $_FILES['attachments']['error'][$key],
'size' => $_FILES['attachments']['size'][$key]
];
handleFileUpload($file, $noteId);
}
}
}
header('Location: ?');
exit;
} else {
$error = '标题和内容不能为空';
}
}
}
// 处理删除
if (isset($_GET['delete']) && isLoggedIn()) {
$id = intval($_GET['delete']);
deleteNote($id);
header('Location: ?');
exit;
}
// 处理删除附件
if (isset($_GET['delete_attachment']) && isLoggedIn()) {
$id = intval($_GET['delete_attachment']);
deleteAttachment($id);
header('Location: ' . $_SERVER['HTTP_REFERER']);
exit;
}
// 处理登出
if (isset($_GET['logout'])) {
session_destroy();
header('Location: ?');
exit;
}
// 获取分页参数
$page = max(1, intval($_GET['page'] ?? 1));
$search = $_GET['search'] ?? '';
$tag = $_GET['tag'] ?? '';
// 获取导航URL
$homeUrl = getHomeUrl();
// 如果未初始化,显示初始化页面
if (!isInitialized()) {
?>
<!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;
}
.init-container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
width: 100%;
max-width: 400px;
}
.init-header {
text-align: center;
margin-bottom: 30px;
}
.init-header h1 {
font-size: 28px;
color: #333;
margin-bottom: 10px;
}
.init-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;
}
.init-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;
}
.init-btn:hover {
transform: translateY(-2px);
}
.error {
background: #fee;
color: #c33;
padding: 10px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="init-container">
<div class="init-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="init">
<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="init-btn">创建账号</button>
</form>
</div>
</body>
</html>
<?php
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;
}
</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>
</body>
</html>
<?php
exit;
}
// 已登录,显示主界面
$totalNotes = getNotesCount($search, $tag);
$totalPages = ceil($totalNotes / NOTES_PER_PAGE);
$notes = getNotes($page, $search, $tag);
$allTags = getAllTags();
$editNote = null;
$viewNote = null;
if (isset($_GET['edit'])) {
$editNote = getNote(intval($_GET['edit']));
} elseif (isset($_GET['view'])) {
$viewNote = getNote(intval($_GET['view']));
}
?>
<!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;
--bg-tertiary: #e9ecef;
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--text-muted: #adb5bd;
--border-color: #dee2e6;
--accent-color: #3498db;
--success-color: #27ae60;
--danger-color: #e74c3c;
--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;
--bg-tertiary: #404040;
--text-primary: #e9ecef;
--text-secondary: #adb5bd;
--text-muted: #6c757d;
--border-color: #495057;
--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 {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 20px 0;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.logo-link {
text-decoration: none;
color: inherit;
transition: var(--transition);
}
.logo-link:hover {
opacity: 0.8;
transform: scale(1.02);
}
.header-actions {
display: flex;
gap: 15px;
align-items: center;
}
.search-box {
position: relative;
}
.search-input {
padding: 8px 35px 8px 12px;
border: 1px solid var(--border-color);
border-radius: 20px;
background: var(--bg-primary);
color: var(--text-primary);
width: 250px;
font-size: 14px;
transition: var(--transition);
}
.search-input:focus {
outline: none;
border-color: var(--accent-color);
width: 300px;
}
.search-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
}
.btn {
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
text-decoration: none;
font-size: 14px;
cursor: pointer;
transition: var(--transition);
}
.btn:hover {
background: var(--hover-bg);
transform: translateY(-1px);
}
.btn-primary {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.btn-primary:hover {
opacity: 0.9;
}
.theme-toggle {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: var(--text-primary);
transition: var(--transition);
}
.theme-toggle:hover {
transform: rotate(180deg);
}
/* 面包屑导航 */
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
padding: 10px 0;
font-size: 14px;
color: var(--text-secondary);
}
.breadcrumb-item {
color: var(--text-secondary);
text-decoration: none;
transition: var(--transition);
}
.breadcrumb-item:hover {
color: var(--accent-color);
}
.breadcrumb-separator {
color: var(--text-muted);
}
.breadcrumb-current {
color: var(--text-primary);
font-weight: 500;
}
/* 返回按钮 */
.back-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--bg-tertiary);
color: var(--text-secondary);
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: var(--transition);
margin-bottom: 20px;
}
.back-button:hover {
background: var(--hover-bg);
color: var(--accent-color);
transform: translateX(-2px);
}
/* Main Layout */
.main-layout {
display: flex;
gap: 30px;
margin-top: 30px;
}
.sidebar {
width: 250px;
flex-shrink: 0;
}
.content {
flex: 1;
}
/* Sidebar */
.sidebar-section {
background: var(--bg-secondary);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid var(--border-color);
}
.sidebar-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
color: var(--text-primary);
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
padding: 4px 10px;
background: var(--bg-tertiary);
color: var(--text-secondary);
border-radius: 12px;
font-size: 12px;
text-decoration: none;
transition: var(--transition);
}
.tag:hover {
background: var(--accent-color);
color: white;
transform: scale(1.05);
}
.tag.active {
background: var(--accent-color);
color: white;
}
/* Notes List */
.notes-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.notes-title {
font-size: 24px;
font-weight: 600;
}
.notes-grid {
display: grid;
gap: 20px;
}
.note-card {
background: var(--bg-secondary);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--border-color);
transition: var(--transition);
cursor: pointer;
}
.note-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.note-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 10px;
}
.note-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 5px;
}
.note-date {
font-size: 12px;
color: var(--text-muted);
}
.note-content {
color: var(--text-secondary);
font-size: 14px;
line-height: 1.5;
margin-bottom: 10px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.note-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.note-tag {
padding: 2px 8px;
background: var(--bg-tertiary);
color: var(--text-secondary);
border-radius: 10px;
font-size: 11px;
}
.note-actions {
display: flex;
gap: 10px;
}
.note-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
transition: var(--transition);
}
.note-btn:hover {
color: var(--accent-color);
}
.note-btn.delete:hover {
color: var(--danger-color);
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-top: 30px;
}
.pagination-info {
color: var(--text-secondary);
font-size: 14px;
}
.pagination-links {
display: flex;
gap: 5px;
}
.page-link {
padding: 6px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
text-decoration: none;
font-size: 14px;
transition: var(--transition);
}
.page-link:hover {
background: var(--hover-bg);
}
.page-link.active {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.page-link.disabled {
color: var(--text-muted);
cursor: not-allowed;
}
/* Note View */
.note-view {
background: var(--bg-secondary);
border-radius: 8px;
padding: 30px;
border: 1px solid var(--border-color);
}
.note-view-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.note-view-title {
font-size: 28px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 10px;
}
.note-view-meta {
color: var(--text-muted);
font-size: 14px;
}
.note-view-actions {
display: flex;
gap: 10px;
}
.note-view-content {
line-height: 1.8;
font-size: 16px;
}
.note-view-tags {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
}
/* Editor */
.editor-container {
background: var(--bg-secondary);
border-radius: 8px;
padding: 30px;
border: 1px solid var(--border-color);
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.editor-title {
font-size: 20px;
font-weight: 600;
}
.editor-actions {
display: flex;
gap: 10px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: var(--text-secondary);
}
.form-input, .form-textarea {
width: 100%;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
font-family: inherit;
transition: var(--transition);
}
.form-input:focus, .form-textarea:focus {
outline: none;
border-color: var(--accent-color);
}
.form-textarea {
min-height: 400px;
resize: vertical;
}
/* Editor Toolbar */
.editor-toolbar {
display: flex;
flex-wrap: wrap;
gap: 5px;
padding: 10px;
background: var(--bg-tertiary);
border-radius: 6px 6px 0 0;
border: 1px solid var(--border-color);
border-bottom: none;
}
.toolbar-btn {
padding: 6px 10px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: var(--transition);
}
.toolbar-btn:hover {
background: var(--hover-bg);
}
.toolbar-btn.active {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.toolbar-separator {
width: 1px;
background: var(--border-color);
margin: 0 5px;
}
.editor-tabs {
display: flex;
gap: 10px;
margin-bottom: 0;
}
.tab {
padding: 8px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-bottom: none;
border-radius: 6px 6px 0 0;
cursor: pointer;
transition: var(--transition);
}
.tab.active {
background: var(--bg-primary);
color: var(--accent-color);
border-bottom: 1px solid var(--bg-primary);
margin-bottom: -1px;
}
.editor-content {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 0 6px 6px 6px;
min-height: 400px;
padding: 20px;
overflow-y: auto;
}
.preview-content {
line-height: 1.6;
}
.preview-content h1, .preview-content h2, .preview-content h3 {
margin: 20px 0 10px 0;
color: var(--text-primary);
}
.preview-content p {
margin-bottom: 15px;
}
.preview-content code {
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
.preview-content pre {
background: var(--bg-tertiary);
padding: 15px;
border-radius: 6px;
overflow-x: auto;
margin: 15px 0;
}
.preview-content pre code {
background: none;
padding: 0;
}
.preview-content a {
color: var(--accent-color);
text-decoration: none;
}
.preview-content a:hover {
text-decoration: underline;
}
.preview-content ul, .preview-content ol {
margin: 15px 0;
padding-left: 30px;
}
.preview-content li {
margin-bottom: 5px;
}
.preview-content blockquote {
border-left: 4px solid var(--accent-color);
padding-left: 15px;
margin: 15px 0;
color: var(--text-secondary);
font-style: italic;
}
/* Attachments */
.attachments-section {
margin-top: 20px;
padding: 15px;
background: var(--bg-tertiary);
border-radius: 6px;
}
.attachments-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
color: var(--text-primary);
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.attachment-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--bg-primary);
border-radius: 4px;
font-size: 13px;
transition: var(--transition);
}
.attachment-item:hover {
background: var(--hover-bg);
}
.attachment-link {
color: var(--accent-color);
text-decoration: none;
}
.attachment-link:hover {
text-decoration: underline;
}
.attachment-delete {
color: var(--danger-color);
cursor: pointer;
font-size: 12px;
}
.attachment-delete:hover {
color: #c0392b;
}
.upload-area {
border: 2px dashed var(--border-color);
border-radius: 6px;
padding: 20px;
text-align: center;
transition: var(--transition);
cursor: pointer;
}
.upload-area:hover {
border-color: var(--accent-color);
background: var(--hover-bg);
}
.upload-area.dragover {
border-color: var(--accent-color);
background: var(--hover-bg);
}
.upload-icon {
font-size: 48px;
color: var(--text-muted);
margin-bottom: 10px;
}
.upload-text {
color: var(--text-secondary);
font-size: 14px;
}
.file-input {
display: none;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--bg-primary);
border-radius: 12px;
padding: 30px;
max-width: 400px;
width: 90%;
animation: slideUp 0.3s ease;
}
.modal-header {
margin-bottom: 20px;
}
.modal-title {
font-size: 20px;
font-weight: 600;
}
.close-modal {
float: right;
font-size: 24px;
cursor: pointer;
color: var(--text-muted);
}
.close-modal:hover {
color: var(--text-primary);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
}
.empty-icon {
font-size: 64px;
margin-bottom: 20px;
}
.empty-text {
font-size: 18px;
margin-bottom: 20px;
}
/* Animations */
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Responsive */
@media (max-width: 768px) {
.main-layout {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.search-input {
width: 150px;
}
.search-input:focus {
width: 200px;
}
.header-content {
flex-wrap: wrap;
gap: 10px;
}
.note-view-title {
font-size: 24px;
}
.pagination {
flex-wrap: wrap;
}
.editor-toolbar {
gap: 2px;
}
.toolbar-btn {
padding: 4px 6px;
font-size: 12px;
}
}
</style>
</head>
<body>
<header>
<div class="container">
<div class="header-content">
<div class="logo">
<a href="<?php echo $homeUrl; ?>" class="logo-link">
<?php echo APP_NAME; ?> 🔒
</a>
</div>
<div class="header-actions">
<div class="search-box">
<form method="get">
<input type="text" name="search" class="search-input" placeholder="搜索笔记..." value="<?php echo htmlspecialchars($search); ?>">
<button type="submit" class="search-icon">🔍</button>
</form>
</div>
<a href="?new" class="btn btn-primary">新建笔记</a>
<button class="btn" onclick="openPasswordModal()">修改密码</button>
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<a href="?logout" class="btn">退出</a>
</div>
</div>
</div>
</header>
<div class="container">
<?php if (isset($_GET['view']) || isset($_GET['edit']) || isset($_GET['new'])): ?>
<!-- 面包屑导航 -->
<nav class="breadcrumb">
<a href="<?php echo $homeUrl; ?>" class="breadcrumb-item">
<?php echo APP_NAME; ?>
</a>
<span class="breadcrumb-separator"></span>
<?php if (isset($_GET['new'])): ?>
<span class="breadcrumb-current">新建笔记</span>
<?php elseif (isset($_GET['edit'])): ?>
<span class="breadcrumb-current">编辑笔记</span>
<?php elseif (isset($_GET['view'])): ?>
<span class="breadcrumb-current">查看笔记</span>
<?php endif; ?>
</nav>
<?php endif; ?>
<?php if (isset($_GET['view'])): ?>
<!-- Note View -->
<div class="note-view">
<div class="note-view-header">
<div>
<div class="note-view-title"><?php echo htmlspecialchars($viewNote['title']); ?></div>
<div class="note-view-meta">
创建于 <?php echo date('Y-m-d H:i', strtotime($viewNote['created_at'])); ?>
<?php if ($viewNote['updated_at'] !== $viewNote['created_at']): ?>
· 更新于 <?php echo date('Y-m-d H:i', strtotime($viewNote['updated_at'])); ?>
<?php endif; ?>
</div>
</div>
<div class="note-view-actions">
<button class="btn" onclick="editNote(<?php echo $viewNote['id']; ?>)">编辑</button>
<button class="btn" onclick="deleteNote(<?php echo $viewNote['id']; ?>)" style="color: var(--danger-color);">删除</button>
</div>
</div>
<div class="note-view-content">
<?php echo markdownToHtml($viewNote['content']); ?>
</div>
<!-- Attachments -->
<?php $attachments = getNoteAttachments($viewNote['id']); ?>
<?php if (!empty($attachments)): ?>
<div class="attachments-section">
<div class="attachments-title">附件</div>
<div class="attachment-list">
<?php foreach ($attachments as $attachment): ?>
<div class="attachment-item">
<?php if (strpos($attachment['file_type'], 'image/') === 0): ?>
🖼️
<?php elseif (strpos($attachment['file_type'], 'pdf') !== false): ?>
📄
<?php else: ?>
📎
<?php endif; ?>
<a href="<?php echo UPLOAD_DIR . '/' . $attachment['filename']; ?>" target="_blank" class="attachment-link">
<?php echo htmlspecialchars($attachment['original_name']); ?>
</a>
<span class="attachment-delete" onclick="deleteAttachment(<?php echo $attachment['id']; ?>)">✕</span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if ($viewNote['tags']): ?>
<div class="note-view-tags">
<div class="sidebar-title">标签</div>
<div class="tag-list">
<?php foreach (explode(',', $viewNote['tags']) as $tag): ?>
<span class="tag"><?php echo htmlspecialchars(trim($tag)); ?></span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php elseif (isset($_GET['new']) || isset($_GET['edit'])): ?>
<!-- Editor -->
<div class="editor-container">
<div class="editor-header">
<div class="editor-title"><?php echo $editNote ? '编辑笔记' : '新建笔记'; ?></div>
<div class="editor-actions">
<a href="<?php echo $homeUrl; ?>" class="btn">取消</a>
<button class="btn btn-primary" onclick="saveNote()">保存</button>
</div>
</div>
<form method="post" id="noteForm" enctype="multipart/form-data">
<input type="hidden" name="action" value="save_note">
<input type="hidden" name="id" value="<?php echo $editNote['id'] ?? 0; ?>">
<div class="form-group">
<label class="form-label">标题</label>
<input type="text" name="title" class="form-input" value="<?php echo htmlspecialchars($editNote['title'] ?? ''); ?>" required>
</div>
<div class="form-group">
<label class="form-label">标签(用逗号分隔)</label>
<input type="text" name="tags" class="form-input" value="<?php echo htmlspecialchars($editNote['tags'] ?? ''); ?>" placeholder="例如:工作,重要,待办">
</div>
<div class="form-group">
<div class="editor-toolbar">
<button type="button" class="toolbar-btn" onclick="insertMarkdown('**', '**')" title="粗体">B</button>
<button type="button" class="toolbar-btn" onclick="insertMarkdown('*', '*')" title="斜体" style="font-style: italic;">I</button>
<button type="button" class="toolbar-btn" onclick="insertMarkdown('`', '`')" title="代码">&lt;/&gt;</button>
<div class="toolbar-separator"></div>
<button type="button" class="toolbar-btn" onclick="insertMarkdown('# ', '')" title="标题1">H1</button>
<button type="button" class="toolbar-btn" onclick="insertMarkdown('## ', '')" title="标题2">H2</button>
<button type="button" class="toolbar-btn" onclick="insertMarkdown('### ', '')" title="标题3">H3</button>
<div class="toolbar-separator"></div>
<button type="button" class="toolbar-btn" onclick="insertMarkdown('- ', '')" title="无序列表">•</button>
<button type="button" class="toolbar-btn" onclick="insertMarkdown('1. ', '')" title="有序列表">1.</button>
<button type="button" class="toolbar-btn" onclick="insertMarkdown('> ', '')" title="引用">"</button>
<div class="toolbar-separator"></div>
<button type="button" class="toolbar-btn" onclick="insertLink()" title="链接">🔗</button>
<button type="button" class="toolbar-btn" onclick="insertImage()" title="图片">🖼️</button>
<button type="button" class="toolbar-btn" onclick="insertCode()" title="代码块">{}</button>
<div class="toolbar-separator"></div>
<button type="button" class="toolbar-btn" onclick="insertTable()" title="表格">⊞</button>
<button type="button" class="toolbar-btn" onclick="insertLine()" title="分割线">—</button>
</div>
<div class="editor-tabs">
<button type="button" class="tab active" onclick="switchTab('edit')">编辑</button>
<button type="button" class="tab" onclick="switchTab('preview')">预览</button>
</div>
<textarea name="content" class="form-textarea" id="contentEditor" oninput="updatePreview()"><?php echo htmlspecialchars($editNote['content'] ?? ''); ?></textarea>
<div class="editor-content preview-content" id="previewContent" style="display: none;"></div>
</div>
<!-- File Upload -->
<div class="form-group">
<label class="form-label">附件上传</label>
<div class="upload-area" onclick="document.getElementById('fileInput').click()" ondrop="handleDrop(event)" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)">
<div class="upload-icon">📁</div>
<div class="upload-text">点击或拖拽文件到此处上传</div>
<div class="upload-text" style="font-size: 12px; margin-top: 5px;">支持图片、PDF、Word文档,最大10MB</div>
</div>
<input type="file" id="fileInput" class="file-input" multiple accept="image/*,.pdf,.doc,.docx,.txt" onchange="handleFileSelect(event)">
<input type="hidden" name="attachments[]" multiple>
</div>
<!-- Existing Attachments -->
<?php if ($editNote): ?>
<?php $attachments = getNoteAttachments($editNote['id']); ?>
<?php if (!empty($attachments)): ?>
<div class="attachments-section">
<div class="attachments-title">已有附件</div>
<div class="attachment-list">
<?php foreach ($attachments as $attachment): ?>
<div class="attachment-item">
<?php if (strpos($attachment['file_type'], 'image/') === 0): ?>
🖼️
<?php elseif (strpos($attachment['file_type'], 'pdf') !== false): ?>
📄
<?php else: ?>
📎
<?php endif; ?>
<a href="<?php echo UPLOAD_DIR . '/' . $attachment['filename']; ?>" target="_blank" class="attachment-link">
<?php echo htmlspecialchars($attachment['original_name']); ?>
</a>
<span class="attachment-delete" onclick="deleteAttachment(<?php echo $attachment['id']; ?>)">✕</span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</form>
</div>
<?php else: ?>
<!-- Notes List -->
<div class="main-layout">
<aside class="sidebar">
<div class="sidebar-section">
<div class="sidebar-title">标签</div>
<div class="tag-list">
<a href="?" class="tag <?php echo !$tag ? 'active' : ''; ?>">全部</a>
<?php foreach ($allTags as $tagItem): ?>
<a href="?tag=<?php echo urlencode($tagItem); ?>" class="tag <?php echo $tag === $tagItem ? 'active' : ''; ?>">
<?php echo htmlspecialchars($tagItem); ?>
</a>
<?php endforeach; ?>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">统计</div>
<div style="color: var(--text-secondary); font-size: 14px;">
<div>总笔记数:<?php echo $totalNotes; ?></div>
<div>当前页:<?php echo count($notes); ?>/<?php echo NOTES_PER_PAGE; ?></div>
<div>总页数:<?php echo $totalPages; ?></div>
</div>
</div>
</aside>
<main class="content">
<div class="notes-header">
<div class="notes-title">
<?php
if ($search) {
echo '搜索结果:' . htmlspecialchars($search);
} elseif ($tag) {
echo '标签:' . htmlspecialchars($tag);
} else {
echo '所有笔记';
}
?>
</div>
<div class="notes-count">共 <?php echo $totalNotes; ?> 篇</div>
</div>
<?php if (empty($notes)): ?>
<div class="empty-state">
<div class="empty-icon">📝</div>
<div class="empty-text">暂无笔记</div>
<a href="?new" class="btn btn-primary">创建第一篇笔记</a>
</div>
<?php else: ?>
<div class="notes-grid">
<?php foreach ($notes as $note): ?>
<div class="note-card" onclick="viewNote(<?php echo $note['id']; ?>)">
<div class="note-header">
<div>
<div class="note-title"><?php echo htmlspecialchars($note['title']); ?></div>
<div class="note-date">
创建于 <?php echo date('Y-m-d H:i', strtotime($note['created_at'])); ?>
<?php if ($note['updated_at'] !== $note['created_at']): ?>
· 更新于 <?php echo date('Y-m-d H:i', strtotime($note['updated_at'])); ?>
<?php endif; ?>
</div>
</div>
<div class="note-actions" onclick="event.stopPropagation()">
<button class="note-btn" onclick="viewNote(<?php echo $note['id']; ?>)">👁️</button>
<button class="note-btn" onclick="editNote(<?php echo $note['id']; ?>)">✏️</button>
<button class="note-btn delete" onclick="deleteNote(<?php echo $note['id']; ?>)">🗑️</button>
</div>
</div>
<div class="note-content"><?php echo htmlspecialchars(mb_substr(strip_tags($note['content']), 0, 200)); ?>...</div>
<?php if ($note['tags']): ?>
<div class="note-tags">
<?php foreach (explode(',', $note['tags']) as $noteTag): ?>
<span class="note-tag"><?php echo htmlspecialchars(trim($noteTag)); ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div class="pagination">
<div class="pagination-info">
第 <?php echo $page; ?> 页,共 <?php echo $totalPages; ?> 页
</div>
<div class="pagination-links">
<?php if ($page > 1): ?>
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $page - 1])); ?>" class="page-link">上一页</a>
<?php else: ?>
<span class="page-link disabled">上一页</span>
<?php endif; ?>
<?php
$startPage = max(1, $page - 2);
$endPage = min($totalPages, $page + 2);
if ($startPage > 1) {
echo '<a href="?' . http_build_query(array_merge($_GET, ['page' => 1])) . '" class="page-link">1</a>';
if ($startPage > 2) {
echo '<span class="page-link disabled">...</span>';
}
}
for ($i = $startPage; $i <= $endPage; $i++) {
$activeClass = $i == $page ? 'active' : '';
echo '<a href="?' . http_build_query(array_merge($_GET, ['page' => $i])) . '" class="page-link ' . $activeClass . '">' . $i . '</a>';
}
if ($endPage < $totalPages) {
if ($endPage < $totalPages - 1) {
echo '<span class="page-link disabled">...</span>';
}
echo '<a href="?' . http_build_query(array_merge($_GET, ['page' => $totalPages])) . '" class="page-link">' . $totalPages . '</a>';
}
?>
<?php if ($page < $totalPages): ?>
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $page + 1])); ?>" class="page-link">下一页</a>
<?php else: ?>
<span class="page-link disabled">下一页</span>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</main>
</div>
<?php endif; ?>
</div>
<!-- Password Modal -->
<div class="modal" id="passwordModal">
<div class="modal-content">
<div class="modal-header">
<span class="close-modal" onclick="closePasswordModal()">&times;</span>
<div class="modal-title">修改密码</div>
</div>
<?php if (isset($error)): ?>
<div style="color: var(--danger-color); margin-bottom: 15px;"><?php echo $error; ?></div>
<?php endif; ?>
<?php if (isset($success)): ?>
<div style="color: var(--success-color); margin-bottom: 15px;"><?php echo $success; ?></div>
<?php endif; ?>
<form method="post">
<input type="hidden" name="action" value="change_password">
<div class="form-group">
<label class="form-label">原密码</label>
<input type="password" name="old_password" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">新密码</label>
<input type="password" name="new_password" class="form-input" required>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">修改密码</button>
</form>
</div>
</div>
<script>
// 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 = '☀️';
}
});
// Editor Functions
function insertMarkdown(before, after) {
const textarea = document.getElementById('contentEditor');
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
const selectedText = text.substring(start, end);
const replacement = before + selectedText + after;
textarea.value = text.substring(0, start) + replacement + text.substring(end);
// Set cursor position
const newCursorPos = start + before.length + selectedText.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
textarea.focus();
updatePreview();
}
function insertLink() {
const url = prompt('请输入链接地址:');
if (url) {
insertMarkdown('[', `](${url})`);
}
}
function insertImage() {
const url = prompt('请输入图片地址:');
if (url) {
insertMarkdown('![', `](${url})`);
}
}
function insertCode() {
const code = prompt('请输入代码:');
if (code) {
insertMarkdown('```\n', '\n```');
}
}
function insertTable() {
const rows = prompt('请输入行数:', '3');
const cols = prompt('请输入列数:', '3');
if (rows && cols) {
let table = '\n';
for (let i = 0; i < parseInt(rows); i++) {
for (let j = 0; j < parseInt(cols); j++) {
table += '| 单元格 ';
}
table += '|\n';
if (i === 0) {
for (let j = 0; j < parseInt(cols); j++) {
table += '| --- ';
}
table += '|\n';
}
}
insertAtCursor(table);
}
}
function insertLine() {
insertAtCursor('\n---\n');
}
function insertAtCursor(text) {
const textarea = document.getElementById('contentEditor');
const start = textarea.selectionStart;
const value = textarea.value;
textarea.value = value.substring(0, start) + text + value.substring(start);
textarea.setSelectionRange(start + text.length, start + text.length);
textarea.focus();
updatePreview();
}
// Tab Switch
function switchTab(tab) {
const tabs = document.querySelectorAll('.tab');
const editor = document.getElementById('contentEditor');
const preview = document.getElementById('previewContent');
tabs.forEach(t => t.classList.remove('active'));
if (tab === 'edit') {
tabs[0].classList.add('active');
editor.style.display = 'block';
preview.style.display = 'none';
} else {
tabs[1].classList.add('active');
editor.style.display = 'none';
preview.style.display = 'block';
updatePreview();
}
}
// Update Preview
function updatePreview() {
const content = document.getElementById('contentEditor').value;
const preview = document.getElementById('previewContent');
// 增强的Markdown解析
let html = content;
// 图片(GitHub风格)
html = html.replace(/!\[(.*?)\]\((.*?)\)/g, '<img src="$2" alt="$1" style="max-width: 100%; height: auto; border-radius: 6px; margin: 10px 0;">');
// 标题
html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
// 粗体和斜体
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
// 代码块
html = html.replace(/```(.*?)```/gs, '<pre><code>$1</code></pre>');
html = html.replace(/`(.*?)`/g, '<code>$1</code>');
// 链接
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>');
// 表格
html = html.replace(/\|(.+)\|/g, function(match) {
const cells = match.split('|').filter(cell => cell.trim());
return '<tr>' + cells.map(cell => `<td>${cell.trim()}</td>`).join('') + '</tr>';
});
html = html.replace(/(<tr>.*<\/tr>)/gs, '<table>$1</table>');
// 无序列表
html = html.replace(/^\* (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>');
// 有序列表
html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
// 引用
html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');
// 分割线
html = html.replace(/^---$/gm, '<hr>');
// 换行
html = html.replace(/\n\n/g, '</p><p>');
html = html.replace(/\n/g, '<br>');
// 段落
html = '<p>' + html + '</p>';
// 清理
html = html.replace(/<p><\/p>/g, '');
html = html.replace(/<p>(<h[1-6]>)/g, '$1');
html = html.replace(/(<\/h[1-6]>)<\/p>/g, '$1');
html = html.replace(/<p>(<pre>)/g, '$1');
html = html.replace(/(<\/pre>)<\/p>/g, '$1');
html = html.replace(/<p>(<ul>)/g, '$1');
html = html.replace(/(<\/ul>)<\/p>/g, '$1');
html = html.replace(/<p>(<table>)/g, '$1');
html = html.replace(/(<\/table>)<\/p>/g, '$1');
html = html.replace(/<p>(<blockquote>)/g, '$1');
html = html.replace(/(<\/blockquote>)<\/p>/g, '$1');
html = html.replace(/<p>(<li>)/g, '$1');
html = html.replace(/(<\/li>)<\/p>/g, '$1');
html = html.replace(/<p>(<tr>)/g, '$1');
html = html.replace(/(<\/tr>)<\/p>/g, '$1');
html = html.replace(/<p>(<hr>)/g, '$1');
html = html.replace(/(<hr>)<\/p>/g, '$1');
preview.innerHTML = html;
}
// File Upload
let uploadedFiles = [];
function handleFileSelect(event) {
const files = event.target.files;
handleFiles(files);
}
function handleDrop(event) {
event.preventDefault();
event.currentTarget.classList.remove('dragover');
const files = event.dataTransfer.files;
handleFiles(files);
}
function handleDragOver(event) {
event.preventDefault();
event.currentTarget.classList.add('dragover');
}
function handleDragLeave(event) {
event.currentTarget.classList.remove('dragover');
}
function handleFiles(files) {
const uploadText = document.querySelector('.upload-text');
const fileInput = document.getElementById('fileInput');
// Clear existing files
uploadedFiles = [];
const dt = new DataTransfer();
// Add new files
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.size > 10 * 1024 * 1024) {
alert('文件 ' + file.name + ' 超过10MB限制');
continue;
}
dt.items.add(file);
uploadedFiles.push(file);
}
fileInput.files = dt.files;
// Show selected files
if (uploadedFiles.length > 0) {
const fileNames = uploadedFiles.map(f => f.name).join(', ');
uploadText.textContent = '已选择: ' + fileNames;
} else {
uploadText.textContent = '点击或拖拽文件到此处上传';
}
}
// Save Note
function saveNote() {
const form = document.getElementById('noteForm');
const formData = new FormData(form);
// Add uploaded files to form data
if (uploadedFiles.length > 0) {
for (let i = 0; i < uploadedFiles.length; i++) {
formData.append('attachments[]', uploadedFiles[i]);
}
}
// Submit form
fetch('', {
method: 'POST',
body: formData
}).then(response => {
if (response.redirected) {
window.location.href = response.url;
} else {
window.location.reload();
}
});
}
// View Note
function viewNote(id) {
window.location.href = '?view=' + id;
}
// Edit Note
function editNote(id) {
window.location.href = '?edit=' + id;
}
// Delete Note
function deleteNote(id) {
if (confirm('确定要删除这篇笔记吗?')) {
window.location.href = '?delete=' + id;
}
}
// Delete Attachment
function deleteAttachment(id) {
if (confirm('确定要删除这个附件吗?')) {
window.location.href = '?delete_attachment=' + id;
}
}
// Password Modal
function openPasswordModal() {
document.getElementById('passwordModal').classList.add('active');
}
function closePasswordModal() {
document.getElementById('passwordModal').classList.remove('active');
}
// Close modal when clicking outside
document.getElementById('passwordModal').addEventListener('click', (e) => {
if (e.target === e.currentTarget) {
closePasswordModal();
}
});
// Auto-save draft
let autoSaveTimer;
document.getElementById('contentEditor')?.addEventListener('input', () => {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(() => {
localStorage.setItem('note_draft', document.getElementById('contentEditor').value);
}, 1000);
});
// Restore draft
window.addEventListener('DOMContentLoaded', () => {
const draft = localStorage.getItem('note_draft');
if (draft && !document.getElementById('contentEditor').value) {
document.getElementById('contentEditor').value = draft;
}
});
</script>
</body>
</html>
<?php
// End of file
?>