2367 lines
76 KiB
PHP
2367 lines
76 KiB
PHP
<?php
|
||
session_start();
|
||
|
||
// 配置
|
||
define('DB_FILE', 'mynotes2025.db');
|
||
define('UPLOAD_DIR', 'uploads');
|
||
define('APP_NAME', '简约记事本');
|
||
define('VERSION', '1.0.3');
|
||
define('NOTES_PER_PAGE', 25);
|
||
define('ENCRYPTION_KEY', 'MySecretKey2025!@#'); // 修改为你自己的密钥
|
||
|
||
// 确保上传目录存在
|
||
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="代码"></></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()">×</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('`);
|
||
}
|
||
}
|
||
|
||
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
|
||
?>
|