From 0e5c28e0b39a887d43cf43b35f66748cf81f8610 Mon Sep 17 00:00:00 2001 From: Falck Date: Mon, 6 Apr 2026 14:48:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=98=E7=BD=91=E6=99=AF?= =?UTF-8?q?=E6=B7=B1=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除突兀的代码 - 为官网加装景深 - 修复PKG的导入 --- .gitignore | 5 - store/@{FutureOSS}/pkg/main.py | 60 +- website/architecture.html | 5 +- website/community/UPDATE_PROFILE.md | 134 ---- website/community/add-title-system.sql | 25 - website/community/api/auth.php | 286 ------- website/community/api/index.php | 112 --- website/community/api/posts.php | 340 --------- website/community/assets/css/auth.css | 363 --------- website/community/assets/css/community.css | 572 -------------- website/community/assets/css/dock-popover.css | 177 ----- website/community/assets/css/editor.css | 325 -------- website/community/assets/css/post-drawer.css | 316 -------- website/community/assets/js/auth.js | 168 ---- website/community/assets/js/community.js | 255 ------- website/community/assets/js/dock-popover.js | 91 --- website/community/assets/js/editor.js | 252 ------ website/community/assets/js/polling-system.js | 91 --- website/community/assets/js/post-drawer.js | 236 ------ website/community/assets/js/title-updater.js | 108 --- website/community/edit-profile.php | 388 ---------- website/community/editor.php | 156 ---- website/community/includes/Database.php | 47 -- website/community/includes/dock.php | 75 -- website/community/includes/post-modal.php | 478 ------------ website/community/index.php | 91 --- website/community/install.sh | 79 -- website/community/login.php | 110 --- website/community/migrate-add-bio.php | 29 - website/community/my-posts.php | 498 ------------ website/community/post.php | 117 --- website/community/profile.php | 717 ------------------ website/community/register.php | 142 ---- website/community/schema.sql | 104 --- website/community/seed-announcements.sql | 15 - website/css/hero.css | 103 ++- website/css/page.css | 74 +- website/css/plugins.css | 2 +- website/features.html | 5 +- website/index.html | 26 +- website/js/dock.js | 1 - website/js/mouse-tracker.js | 83 ++ website/js/parallax.js | 219 ++++++ website/plugins.html | 44 +- website/quickstart.html | 5 +- 45 files changed, 526 insertions(+), 7003 deletions(-) delete mode 100644 website/community/UPDATE_PROFILE.md delete mode 100644 website/community/add-title-system.sql delete mode 100644 website/community/api/auth.php delete mode 100644 website/community/api/index.php delete mode 100644 website/community/api/posts.php delete mode 100644 website/community/assets/css/auth.css delete mode 100644 website/community/assets/css/community.css delete mode 100644 website/community/assets/css/dock-popover.css delete mode 100644 website/community/assets/css/editor.css delete mode 100644 website/community/assets/css/post-drawer.css delete mode 100644 website/community/assets/js/auth.js delete mode 100644 website/community/assets/js/community.js delete mode 100644 website/community/assets/js/dock-popover.js delete mode 100644 website/community/assets/js/editor.js delete mode 100644 website/community/assets/js/polling-system.js delete mode 100644 website/community/assets/js/post-drawer.js delete mode 100644 website/community/assets/js/title-updater.js delete mode 100644 website/community/edit-profile.php delete mode 100644 website/community/editor.php delete mode 100644 website/community/includes/Database.php delete mode 100644 website/community/includes/dock.php delete mode 100644 website/community/includes/post-modal.php delete mode 100644 website/community/index.php delete mode 100644 website/community/install.sh delete mode 100644 website/community/login.php delete mode 100644 website/community/migrate-add-bio.php delete mode 100644 website/community/my-posts.php delete mode 100644 website/community/post.php delete mode 100644 website/community/profile.php delete mode 100644 website/community/register.php delete mode 100644 website/community/schema.sql delete mode 100644 website/community/seed-announcements.sql create mode 100644 website/js/mouse-tracker.js create mode 100644 website/js/parallax.js diff --git a/.gitignore b/.gitignore index 4a2de83..9321567 100644 --- a/.gitignore +++ b/.gitignore @@ -14,11 +14,6 @@ start-web.sh website/router.php admin/ -# 数据库配置(含密码) -website/community/config.php -website/community/.env -admin/includes/config.php - # 日志 logs/ *.log diff --git a/store/@{FutureOSS}/pkg/main.py b/store/@{FutureOSS}/pkg/main.py index a724478..e9170b1 100644 --- a/store/@{FutureOSS}/pkg/main.py +++ b/store/@{FutureOSS}/pkg/main.py @@ -41,12 +41,16 @@ class PackageManager: """加载已安装的包""" if not PKG_DIR.exists(): return - for pkg_dir in PKG_DIR.iterdir(): - if pkg_dir.is_dir(): - manifest = pkg_dir / "manifest.json" - if manifest.exists(): - with open(manifest, "r", encoding="utf-8") as f: - self.installed[pkg_dir.name] = json.load(f) + # 扫描 @{author}/plugin_name 结构 + for author_dir in PKG_DIR.iterdir(): + if author_dir.is_dir() and author_dir.name.startswith("@{"): + for plugin_dir in author_dir.iterdir(): + if plugin_dir.is_dir(): + manifest = plugin_dir / "manifest.json" + if manifest.exists(): + with open(manifest, "r", encoding="utf-8") as f: + full_name = author_dir.name + "/" + plugin_dir.name + self.installed[full_name] = json.load(f) def search(self, query: str = "") -> list[PackageInfo]: """搜索可用的包""" @@ -75,14 +79,19 @@ class PackageManager: return results def install(self, name: str, version: str = "") -> bool: - """安装包,支持 @{作者/插件名} 格式""" - # 解析输入格式 @{author/plugin} 或直接插件名 + """安装包,支持 @{作者名称}/插件名称 格式""" + # 解析输入格式 @{author}/plugin 或直接插件名 author = "FutureOSS" # 默认作者 plugin_name = name - - if name.startswith("@{") and "/" in name: - # 解析 @{author/plugin} 格式 - inner = name[2:-1] if name.endswith("}") else name[2:] + + if name.startswith("@{") and "}/" in name: + # 解析 @{author}/plugin 格式 + end_bracket = name.index("}/") + author = name[2:end_bracket] + plugin_name = name[end_bracket + 2:] + elif name.startswith("@{") and name.endswith("}") and "/" in name: + # 兼容旧格式 @{author/plugin} + inner = name[2:-1] parts = inner.split("/", 1) if len(parts) == 2: author, plugin_name = parts @@ -104,8 +113,8 @@ class PackageManager: pkg_info.version = version or "1.0.0" pkg_info.download_url = self.registry + "/store/@{" + author + "/" + plugin_name + "}" - # 创建安装目录 @{author/plugin_name} - install_dir = PKG_DIR / ("@{" + author + "/" + plugin_name + "}") + # 创建安装目录 @{author}/plugin_name + install_dir = PKG_DIR / ("@{" + author + "}") / plugin_name install_dir.mkdir(parents=True, exist_ok=True) try: @@ -124,7 +133,7 @@ class PackageManager: f.write(main_data) # 更新已安装列表 - full_name = "@{" + author + "/" + plugin_name + full_name = "@{" + author + "}/" + plugin_name self.installed[full_name] = manifest_data print(f"[pkg] 已安装: {full_name} {manifest_data.get('metadata', {}).get('version', '')}") return True @@ -136,15 +145,30 @@ class PackageManager: return False def uninstall(self, name: str) -> bool: - """卸载包""" - install_dir = PKG_DIR / name + """卸载包,支持 @{作者名称}/插件名称 格式""" + # 解析格式获取目录路径 + if name.startswith("@{") and "}/" in name: + end_bracket = name.index("}/") + author = name[2:end_bracket] + plugin_name = name[end_bracket + 2:] + install_dir = PKG_DIR / ("@{" + author + "}") / plugin_name + elif name.startswith("@{") and name.endswith("}") and "/" in name: + # 兼容旧格式 + install_dir = PKG_DIR / name + else: + install_dir = PKG_DIR / name + if not install_dir.exists(): print(f"[pkg] 包未安装: {name}") return False try: shutil.rmtree(install_dir) - del self.installed[name] + # 从已安装列表中移除 + for key in list(self.installed.keys()): + if key == name or key.endswith("/" + install_dir.name): + del self.installed[key] + break print(f"[pkg] 已卸载: {name}") return True except Exception as e: diff --git a/website/architecture.html b/website/architecture.html index 4caa4bd..f726e30 100644 --- a/website/architecture.html +++ b/website/architecture.html @@ -24,13 +24,13 @@
-
+
架构设计

插件驱动的分层架构

一切皆为插件,框架本身只提供核心能力

-
+
Future OSS 核心
@@ -89,6 +89,7 @@ + diff --git a/website/community/UPDATE_PROFILE.md b/website/community/UPDATE_PROFILE.md deleted file mode 100644 index 6172ace..0000000 --- a/website/community/UPDATE_PROFILE.md +++ /dev/null @@ -1,134 +0,0 @@ -# 用户个人主页功能更新 - -## 改动概述 - -为用户社区添加了完整的个人主页系统,包括用户信息展示、文章管理、个人简介编辑等功能。 - -## 新增文件 - -### 1. `profile.php` - 用户个人主页 -**功能:** -- 显示用户头像、用户名、角色徽章 -- 显示用户个人简介(bio) -- 显示注册时间和邮箱(仅本人可见) -- 统计数据卡片:文章数、回复数、总浏览、总点赞 -- 最近发表的文章列表(最新 10 篇) -- 最近的回复列表(最新 5 条) -- 支持查看其他用户的主页(通过 `?id=用户ID` 参数) -- 本人访问时显示"编辑资料"按钮 - -**访问方式:** -- `profile.php` - 查看当前登录用户的个人主页 -- `profile.php?id=123` - 查看指定用户的个人主页 - -### 2. `edit-profile.php` - 编辑个人资料 -**功能:** -- 编辑个人简介(bio) -- 实时预览功能 -- 显示用户名和邮箱(只读) -- 保存成功后显示提示信息 - -**访问方式:** -- 从个人主页点击"编辑资料"按钮进入 - -### 3. `my-posts.php` - 我的文章(已更新) -**更新内容:** -- 支持查看其他用户的文章(通过 `?id=用户ID` 参数) -- 只有文章作者才能看到"编辑"和"删除"按钮 -- 页面标题动态显示用户名 - -### 4. `migrate-add-bio.php` - 数据库迁移脚本 -**用途:** -- 为现有的 `users` 表添加 `bio` 字段 -- 检查字段是否已存在,避免重复添加 - -**运行方式:** -```bash -php website/community/migrate-add-bio.php -``` - -## 修改的文件 - -### 1. `includes/dock.php` -**改动:** -- 用户头像链接从 `#` 改为 `profile.php`,点击跳转到个人主页 -- 用户面板新增"个人主页"菜单项 -- 保留"我的文章"菜单项,并显示文章数量徽章 - -### 2. `api/auth.php` -**改动:** -- 新增 `my-post-count` API 端点 -- 用于获取当前登录用户的文章数量 -- 在用户面板中实时显示文章数徽章 - -### 3. `css/dock.css` -**改动:** -- 新增 `.dock-user-avatar` 样式 -- 用户头像图标显示为青色高亮 -- 鼠标悬停时有缩放和背景效果 - -### 4. `assets/css/dock-popover.css` -**改动:** -- 新增 `.popover-menu` 菜单区域样式 -- 新增 `.menu-item` 菜单项样式(带图标、文字、徽章) -- 新增 `.menu-badge` 徽章样式 - -### 5. `assets/js/dock-popover.js` -**改动:** -- 新增 `fetchMyPostCount()` 函数 -- 页面加载时自动获取并显示用户文章数量 -- 保留原有的退登和注销功能 - -### 6. `schema.sql` -**改动:** -- `users` 表新增 `bio TEXT` 字段 -- 用于存储用户的个人简介 - -## 使用方法 - -### 1. 运行数据库迁移 -首次部署需要运行迁移脚本添加 bio 字段: -```bash -cd website/community -php migrate-add-bio.php -``` - -### 2. 访问个人主页 -- 点击左侧 Dock 栏的用户头像图标 -- 或访问 `community/profile.php` - -### 3. 编辑个人简介 -- 在个人主页点击"编辑资料"按钮 -- 或访问 `community/edit-profile.php` - -### 4. 查看我的文章 -- 在用户面板中点击"我的文章" -- 或访问 `community/my-posts.php` - -### 5. 查看其他用户的主页 -- 访问 `community/profile.php?id=用户ID` - -## 技术细节 - -### 数据库查询优化 -- 使用子查询统计回复数,避免 JOIN 导致的性能问题 -- 使用 SUM 聚合函数统计总浏览和总点赞 -- 所有查询都使用参数化查询防止 SQL 注入 - -### 安全性 -- 所有用户输入都经过 `htmlspecialchars()` 转义 -- 只有本人才能编辑和删除自己的文章 -- Session 验证确保用户只能访问自己的数据 - -### 响应式设计 -- 适配桌面端和移动端 -- 小屏幕时自动调整布局和字体大小 -- Dock 栏在移动端自动移到底部 - -## 后续可扩展功能 - -1. **头像上传** - 支持用户上传自定义头像 -2. **关注系统** - 用户可以互相关注 -3. **成就徽章** - 根据用户活跃度颁发徽章 -4. **社交链接** - 添加 GitHub、Twitter 等社交链接 -5. **活动日志** - 记录用户的登录、发帖等活动 diff --git a/website/community/add-title-system.sql b/website/community/add-title-system.sql deleted file mode 100644 index 6bba69e..0000000 --- a/website/community/add-title-system.sql +++ /dev/null @@ -1,25 +0,0 @@ --- 添加称号系统到 users 表 -ALTER TABLE users ADD COLUMN IF NOT EXISTS title VARCHAR(100) DEFAULT '' COMMENT '用户称号'; - --- 设置 admin 用户的称号 -UPDATE users SET title = '你猜为什么是DeepSeek' WHERE role = 'admin' AND username = 'admin'; - --- 创建称号配置表(可选,用于管理称号) -CREATE TABLE IF NOT EXISTS titles ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL UNIQUE COMMENT '称号名称', - color VARCHAR(7) DEFAULT '#06b6d4' COMMENT '称号颜色', - description VARCHAR(255) DEFAULT '' COMMENT '称号描述', - is_admin_only TINYINT(1) DEFAULT 0 COMMENT '是否仅管理员可用', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 插入预设称号 -INSERT IGNORE INTO titles (name, color, description, is_admin_only) VALUES -('你猜为什么是DeepSeek', '#f59e0b', '神秘的管理称号', 1), -('管理员', '#ef4444', '网站管理员', 1), -('版主', '#22c55e', '社区版主', 1), -('活跃用户', '#3b82f6', '经常发帖的活跃用户', 0), -('新手', '#6b7280', '新加入的用户', 0), -('技术达人', '#8b5cf6', '技术方面的大神', 0), -('社区元老', '#f97316', '在很久的时间前就加入社区的用户', 0); diff --git a/website/community/api/auth.php b/website/community/api/auth.php deleted file mode 100644 index 63282fe..0000000 --- a/website/community/api/auth.php +++ /dev/null @@ -1,286 +0,0 @@ - false, 'message' => '请求方法不允许']); - exit; -} - -if ($action === 'login') { - handleLogin(); -} elseif ($action === 'register') { - handleRegister(); -} elseif ($action === 'logout') { - handleLogout(); -} elseif ($action === 'check') { - handleCheck(); -} elseif ($action === 'my-post-count') { - handleMyPostCount(); -} elseif ($action === 'current-user') { - handleCurrentUser(); -} else { - http_response_code(400); - echo json_encode(['success' => false, 'message' => '无效的操作类型']); -} - -/** - * 处理登录 - */ -function handleLogin() { - $input = json_decode(file_get_contents('php://input'), true); - - if (empty($input['username']) || empty($input['password'])) { - echo json_encode(['success' => false, 'message' => '用户名和密码不能为空']); - return; - } - - $username = trim($input['username']); - $password = $input['password']; - $remember = $input['remember'] ?? false; - - try { - $db = Database::getInstance(); - - // 查询用户(支持用户名或邮箱登录) - $user = $db->fetchOne( - "SELECT id, username, email, password_hash, role, avatar FROM users WHERE username = ? OR email = ?", - [$username, $username] - ); - - if (!$user) { - echo json_encode(['success' => false, 'message' => '用户名或密码错误']); - return; - } - - // 验证密码 - if (!password_verify($password, $user['password_hash'])) { - echo json_encode(['success' => false, 'message' => '用户名或密码错误']); - return; - } - - // 设置 session - $_SESSION['user_id'] = $user['id']; - $_SESSION['username'] = $user['username']; - $_SESSION['role'] = $user['role']; - $_SESSION['avatar'] = $user['avatar']; - - // 如果勾选记住我,设置更长的 session 生命周期 - if ($remember) { - ini_set('session.gc_maxlifetime', 30 * 24 * 60 * 60); // 30天 - session_set_cookie_params(30 * 24 * 60 * 60); - } - - echo json_encode([ - 'success' => true, - 'message' => '登录成功', - 'user' => [ - 'id' => $user['id'], - 'username' => $user['username'], - 'role' => $user['role'], - 'avatar' => $user['avatar'] - ] - ]); - } catch (Exception $e) { - http_response_code(500); - echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); - } -} - -/** - * 处理注册 - */ -function handleRegister() { - $input = json_decode(file_get_contents('php://input'), true); - - if (empty($input['username']) || empty($input['email']) || empty($input['password'])) { - echo json_encode(['success' => false, 'message' => '所有字段都不能为空']); - return; - } - - $username = trim($input['username']); - $email = trim($input['email']); - $password = $input['password']; - - // 验证用户名格式 - if (!preg_match('/^[a-zA-Z0-9_]{3,50}$/', $username)) { - echo json_encode(['success' => false, 'message' => '用户名只能包含字母、数字和下划线,长度 3-50 个字符']); - return; - } - - // 验证邮箱格式 - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - echo json_encode(['success' => false, 'message' => '邮箱格式不正确']); - return; - } - - // 验证密码长度 - if (strlen($password) < 6) { - echo json_encode(['success' => false, 'message' => '密码长度至少 6 个字符']); - return; - } - - try { - $db = Database::getInstance(); - - // 检查用户名是否已存在 - $existingUser = $db->fetchOne("SELECT id FROM users WHERE username = ?", [$username]); - if ($existingUser) { - echo json_encode(['success' => false, 'message' => '用户名已被使用']); - return; - } - - // 检查邮箱是否已存在 - $existingEmail = $db->fetchOne("SELECT id FROM users WHERE email = ?", [$email]); - if ($existingEmail) { - echo json_encode(['success' => false, 'message' => '邮箱已被注册']); - return; - } - - // 密码哈希 - $passwordHash = password_hash($password, PASSWORD_DEFAULT); - - // 插入新用户 - $db->query( - "INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, 'member')", - [$username, $email, $passwordHash] - ); - - $userId = $db->lastInsertId(); - - echo json_encode([ - 'success' => true, - 'message' => '注册成功', - 'user' => [ - 'id' => $userId, - 'username' => $username, - 'role' => 'member' - ] - ]); - } catch (Exception $e) { - http_response_code(500); - echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); - } -} - -/** - * 处理登出 - */ -function handleLogout() { - session_destroy(); - echo json_encode(['success' => true, 'message' => '已成功退出']); -} - -/** - * 检查登录状态 - */ -function handleCheck() { - if (isset($_SESSION['user_id'])) { - echo json_encode([ - 'success' => true, - 'logged_in' => true, - 'user' => [ - 'id' => $_SESSION['user_id'], - 'username' => $_SESSION['username'], - 'role' => $_SESSION['role'] ?? 'member', - 'avatar' => $_SESSION['avatar'] ?? '' - ] - ]); - } else { - echo json_encode([ - 'success' => true, - 'logged_in' => false - ]); - } -} - -/** - * 获取用户文章数量 - */ -function handleMyPostCount() { - if (!isset($_SESSION['user_id'])) { - echo json_encode(['success' => false, 'message' => '未登录']); - return; - } - - try { - $db = Database::getInstance(); - $count = $db->fetchOne( - "SELECT COUNT(*) as count FROM posts WHERE user_id = ?", - [$_SESSION['user_id']] - )['count']; - - echo json_encode([ - 'success' => true, - 'count' => (int)$count - ]); - } catch (Exception $e) { - http_response_code(500); - echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); - } -} - -/** - * 获取当前登录用户信息(用于轮询) - */ -function handleCurrentUser() { - if (!isset($_SESSION['user_id'])) { - echo json_encode(['success' => false, 'message' => '未登录']); - return; - } - - try { - $db = Database::getInstance(); - $user = $db->fetchOne( - "SELECT id, username, email, avatar, role, title, bio, created_at FROM users WHERE id = ?", - [$_SESSION['user_id']] - ); - - if (!$user) { - echo json_encode(['success' => false, 'message' => '用户不存在']); - return; - } - - // 获取统计数据 - $stats = $db->fetchOne( - "SELECT - (SELECT COUNT(*) FROM posts WHERE user_id = ?) as post_count, - (SELECT COUNT(*) FROM replies WHERE user_id = ?) as reply_count", - [$user['id'], $user['id']] - ); - - echo json_encode([ - 'success' => true, - 'user' => $user, - 'stats' => [ - 'post_count' => (int)$stats['post_count'], - 'reply_count' => (int)$stats['reply_count'] - ], - 'permissions' => [ - 'can_manage_users' => in_array($user['role'], ['admin']), - 'can_manage_posts' => in_array($user['role'], ['admin', 'moderator']), - 'can_pin_posts' => in_array($user['role'], ['admin', 'moderator']), - 'can_lock_posts' => in_array($user['role'], ['admin', 'moderator']), - 'can_delete_any_post' => in_array($user['role'], ['admin']), - 'can_manage_titles' => in_array($user['role'], ['admin']) - ] - ]); - } catch (Exception $e) { - http_response_code(500); - echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); - } -} diff --git a/website/community/api/index.php b/website/community/api/index.php deleted file mode 100644 index d939925..0000000 --- a/website/community/api/index.php +++ /dev/null @@ -1,112 +0,0 @@ -fetchAll( - "SELECT p.*, u.username, u.avatar, c.name as category_name - FROM posts p - JOIN users u ON p.user_id = u.id - JOIN categories c ON p.category_id = c.id - WHERE p.category_id = ? - ORDER BY p.is_pinned DESC, p.created_at DESC - LIMIT ? OFFSET ?", - [$categoryId, $limit, $offset] - ); - $total = $db->fetchOne("SELECT COUNT(*) as count FROM posts WHERE category_id = ?", [$categoryId])['count']; - } else { - $posts = $db->fetchAll( - "SELECT p.*, u.username, u.avatar, c.name as category_name - FROM posts p - JOIN users u ON p.user_id = u.id - JOIN categories c ON p.category_id = c.id - ORDER BY p.is_pinned DESC, p.created_at DESC - LIMIT ? OFFSET ?", - [$limit, $offset] - ); - $total = $db->fetchOne("SELECT COUNT(*) as count FROM posts")['count']; - } - - echo json_encode([ - 'posts' => $posts, - 'total' => $total, - 'pages' => ceil($total / $limit) - ]); - break; - - case 'post': - $id = (int)($_GET['id'] ?? 0); - $post = $db->fetchOne( - "SELECT p.*, u.username, u.avatar, u.role, c.name as category_name, c.slug as category_slug - FROM posts p - JOIN users u ON p.user_id = u.id - JOIN categories c ON p.category_id = c.id - WHERE p.id = ?", - [$id] - ); - - if (!$post) { - http_response_code(404); - echo json_encode(['error' => '帖子不存在']); - exit; - } - - // 更新浏览数 - $db->query("UPDATE posts SET views = views + 1 WHERE id = ?", [$id]); - $post['views']++; - - // 获取回复 - $replies = $db->fetchAll( - "SELECT r.*, u.username, u.avatar - FROM replies r - JOIN users u ON r.user_id = u.id - WHERE r.post_id = ? - ORDER BY r.is_solution DESC, r.created_at ASC", - [$id] - ); - - echo json_encode(['post' => $post, 'replies' => $replies]); - break; - - case 'categories': - $categories = $db->fetchAll("SELECT * FROM categories ORDER BY sort_order ASC"); - echo json_encode(['categories' => $categories]); - break; - - case 'stats': - $stats = [ - 'posts' => $db->fetchOne("SELECT COUNT(*) as count FROM posts")['count'], - 'replies' => $db->fetchOne("SELECT COUNT(*) as count FROM replies")['count'], - 'users' => $db->fetchOne("SELECT COUNT(*) as count FROM users")['count'], - 'hot_posts' => $db->fetchAll("SELECT id, title, views, likes FROM posts ORDER BY views DESC LIMIT 5"), - ]; - echo json_encode($stats); - break; - - default: - echo json_encode(['error' => '未知操作']); - } -} catch (Exception $e) { - http_response_code(500); - echo json_encode(['error' => $e->getMessage()]); -} diff --git a/website/community/api/posts.php b/website/community/api/posts.php deleted file mode 100644 index bd4763b..0000000 --- a/website/community/api/posts.php +++ /dev/null @@ -1,340 +0,0 @@ - false, 'message' => '请求方法不允许']); - exit; -} - -$action = $_GET['action'] ?? ''; - -// 检查登录状态(除了查看操作) -$requireAuth = in_array($action, ['create', 'update', 'delete', 'pin', 'lock']); -if ($requireAuth && !isset($_SESSION['user_id'])) { - echo json_encode(['success' => false, 'message' => '请先登录']); - exit; -} - -switch ($action) { - case 'create': - handleCreatePost(); - break; - case 'update': - handleUpdatePost(); - break; - case 'delete': - handleDeletePost(); - break; - case 'pin': - handlePinPost(); - break; - case 'lock': - handleLockPost(); - break; - default: - http_response_code(400); - echo json_encode(['success' => false, 'message' => '无效的操作类型']); -} - -/** - * 创建帖子 - */ -function handleCreatePost() { - $input = json_decode(file_get_contents('php://input'), true); - - if (empty($input['title']) || empty($input['content']) || empty($input['category_id'])) { - echo json_encode(['success' => false, 'message' => '标题、内容和分类不能为空']); - return; - } - - $title = trim($input['title']); - $content = trim($input['content']); - $categoryId = (int)$input['category_id']; - $tags = $input['tags'] ?? []; - - // 验证标题长度 - if (mb_strlen($title) < 5 || mb_strlen($title) > 200) { - echo json_encode(['success' => false, 'message' => '标题长度必须在 5-200 个字符之间']); - return; - } - - // 验证内容长度 - if (mb_strlen($content) < 10) { - echo json_encode(['success' => false, 'message' => '内容至少 10 个字符']); - return; - } - - try { - $db = Database::getInstance(); - $userId = $_SESSION['user_id']; - $userRole = $_SESSION['role'] ?? 'member'; - - // 验证分类是否存在 - $category = $db->fetchOne("SELECT * FROM categories WHERE id = ?", [$categoryId]); - if (!$category) { - echo json_encode(['success' => false, 'message' => '分类不存在']); - return; - } - - // 公告分类:禁止通过 API 发帖(只能通过 SQL 直接插入) - if ($category['slug'] === 'announcements') { - echo json_encode(['success' => false, 'message' => '公告不能通过发帖功能创建,请联系管理员通过数据库添加']); - return; - } - - // 生成 slug - $slug = generateSlug($title); - - // 检查 slug 是否重复 - $existing = $db->fetchOne("SELECT id FROM posts WHERE slug = ?", [$slug]); - if ($existing) { - $slug .= '-' . time(); - } - - // 插入帖子 - $db->query( - "INSERT INTO posts (user_id, category_id, title, slug, content) VALUES (?, ?, ?, ?, ?)", - [$userId, $categoryId, $title, $slug, $content] - ); - - $postId = $db->lastInsertId(); - - // 保存标签 - if (!empty($tags)) { - saveTags($db, $postId, $tags); - } - - echo json_encode([ - 'success' => true, - 'message' => '发帖成功', - 'post_id' => $postId - ]); - } catch (Exception $e) { - http_response_code(500); - echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); - } -} - -/** - * 更新帖子 - */ -function handleUpdatePost() { - $input = json_decode(file_get_contents('php://input'), true); - - if (empty($input['id']) || empty($input['title']) || empty($input['content'])) { - echo json_encode(['success' => false, 'message' => '标题和内容不能为空']); - return; - } - - $postId = (int)$input['id']; - $title = trim($input['title']); - $content = trim($input['content']); - $tags = $input['tags'] ?? []; - - try { - $db = Database::getInstance(); - $userId = $_SESSION['user_id']; - - // 检查帖子是否存在且属于当前用户 - $post = $db->fetchOne("SELECT user_id FROM posts WHERE id = ?", [$postId]); - if (!$post) { - echo json_encode(['success' => false, 'message' => '帖子不存在']); - return; - } - - // 只有作者可以编辑 - if ($post['user_id'] != $userId) { - echo json_encode(['success' => false, 'message' => '无权编辑此帖子']); - return; - } - - // 更新帖子 - $db->query( - "UPDATE posts SET title = ?, content = ?, updated_at = NOW() WHERE id = ?", - [$title, $content, $postId] - ); - - // 更新标签 - if (!empty($tags)) { - saveTags($db, $postId, $tags); - } - - echo json_encode([ - 'success' => true, - 'message' => '更新成功' - ]); - } catch (Exception $e) { - http_response_code(500); - echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); - } -} - -/** - * 删除帖子 - */ -function handleDeletePost() { - $input = json_decode(file_get_contents('php://input'), true); - - if (empty($input['id'])) { - echo json_encode(['success' => false, 'message' => '帖子 ID 不能为空']); - return; - } - - $postId = (int)$input['id']; - - try { - $db = Database::getInstance(); - $userId = $_SESSION['user_id']; - $userRole = $_SESSION['role'] ?? 'member'; - - // 检查帖子是否存在 - $post = $db->fetchOne("SELECT user_id FROM posts WHERE id = ?", [$postId]); - if (!$post) { - echo json_encode(['success' => false, 'message' => '帖子不存在']); - return; - } - - // 只有作者或管理员可以删除 - if ($post['user_id'] != $userId && !in_array($userRole, ['admin', 'moderator'])) { - echo json_encode(['success' => false, 'message' => '无权删除此帖子']); - return; - } - - // 删除帖子(外键级联删除回复和点赞) - $db->query("DELETE FROM posts WHERE id = ?", [$postId]); - - echo json_encode([ - 'success' => true, - 'message' => '删除成功' - ]); - } catch (Exception $e) { - http_response_code(500); - echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); - } -} - -/** - * 置顶/取消置顶 - */ -function handlePinPost() { - $input = json_decode(file_get_contents('php://input'), true); - - if (empty($input['id'])) { - echo json_encode(['success' => false, 'message' => '帖子 ID 不能为空']); - return; - } - - $postId = (int)$input['id']; - $pinned = (bool)($input['pinned'] ?? false); - - try { - $db = Database::getInstance(); - $userRole = $_SESSION['role'] ?? 'member'; - - // 只有管理员或版主可以置顶 - if (!in_array($userRole, ['admin', 'moderator'])) { - echo json_encode(['success' => false, 'message' => '无权置顶帖子']); - return; - } - - $db->query("UPDATE posts SET is_pinned = ? WHERE id = ?", [(int)$pinned, $postId]); - - echo json_encode([ - 'success' => true, - 'message' => $pinned ? '已置顶' : '已取消置顶' - ]); - } catch (Exception $e) { - http_response_code(500); - echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); - } -} - -/** - * 锁定/解锁帖子 - */ -function handleLockPost() { - $input = json_decode(file_get_contents('php://input'), true); - - if (empty($input['id'])) { - echo json_encode(['success' => false, 'message' => '帖子 ID 不能为空']); - return; - } - - $postId = (int)$input['id']; - $locked = (bool)($input['locked'] ?? false); - - try { - $db = Database::getInstance(); - $userRole = $_SESSION['role'] ?? 'member'; - - // 只有管理员或版主可以锁定 - if (!in_array($userRole, ['admin', 'moderator'])) { - echo json_encode(['success' => false, 'message' => '无权锁定帖子']); - return; - } - - $db->query("UPDATE posts SET is_locked = ? WHERE id = ?", [(int)$locked, $postId]); - - echo json_encode([ - 'success' => true, - 'message' => $locked ? '已锁定' : '已解锁' - ]); - } catch (Exception $e) { - http_response_code(500); - echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); - } -} - -/** - * 保存标签 - */ -function saveTags($db, $postId, $tags) { - // 先删除旧标签关联 - $db->query("DELETE FROM post_tags WHERE post_id = ?", [$postId]); - - foreach ($tags as $tagName) { - $tagName = trim($tagName); - if (empty($tagName)) continue; - - // 查找或创建标签 - $slug = strtolower(preg_replace('/[^a-zA-Z0-9\x{4e00}-\x{9fa5}]/u', '-', $tagName)); - $tag = $db->fetchOne("SELECT id FROM tags WHERE name = ?", [$tagName]); - - if (!$tag) { - $db->query( - "INSERT INTO tags (name, slug) VALUES (?, ?)", - [$tagName, $slug] - ); - $tagId = $db->lastInsertId(); - } else { - $tagId = $tag['id']; - } - - // 关联标签 - $db->query( - "INSERT IGNORE INTO post_tags (post_id, tag_id) VALUES (?, ?)", - [$postId, $tagId] - ); - } -} - -/** - * 生成 slug - */ -function generateSlug($title) { - // 简单处理:移除特殊字符,替换空格为连字符 - $slug = preg_replace('/[^\p{L}\p{N}\s]/u', '', $title); - $slug = preg_replace('/\s+/', '-', $slug); - $slug = mb_strtolower($slug, 'UTF-8'); - return mb_substr($slug, 0, 100); -} diff --git a/website/community/assets/css/auth.css b/website/community/assets/css/auth.css deleted file mode 100644 index a559b24..0000000 --- a/website/community/assets/css/auth.css +++ /dev/null @@ -1,363 +0,0 @@ -/* ============================================ - OSS Community 认证页面样式 - 与官网风格保持一致 - ============================================ */ - -/* 认证页面布局 */ -.auth-page { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - padding: 40px 20px; - margin-left: 80px; - position: relative; - z-index: 1; -} - -.auth-container { - width: 100%; - max-width: 480px; - position: relative; -} - -/* 认证卡片 */ -.auth-card { - background: rgba(15, 23, 42, 0.8); - backdrop-filter: blur(20px); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: 16px; - padding: 40px; - box-shadow: - 0 20px 60px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(99, 102, 241, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.05); - animation: cardSlideUp 0.6s ease-out; -} - -@keyframes cardSlideUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* 认证头部 */ -.auth-header { - text-align: center; - margin-bottom: 32px; -} - -.auth-logo { - width: 64px; - height: 64px; - margin: 0 auto 20px; - background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); - border-radius: 16px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4); -} - -.auth-logo svg { - width: 32px; - height: 32px; - color: white; -} - -.auth-title { - font-size: 28px; - font-weight: 700; - margin: 0 0 8px 0; - background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.auth-subtitle { - color: #94a3b8; - font-size: 14px; - margin: 0; -} - -/* 表单样式 */ -.auth-form { - display: flex; - flex-direction: column; - gap: 20px; -} - -.form-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.form-label { - font-size: 14px; - font-weight: 500; - color: #e2e8f0; -} - -.input-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.input-icon { - position: absolute; - left: 14px; - width: 18px; - height: 18px; - color: #64748b; - pointer-events: none; - transition: color 0.2s; -} - -.form-input { - width: 100%; - padding: 12px 14px 12px 44px; - background: rgba(30, 41, 59, 0.6); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: 8px; - color: #e2e8f0; - font-size: 14px; - transition: all 0.2s; -} - -.form-input::placeholder { - color: #64748b; -} - -.form-input:focus { - outline: none; - border-color: #6366f1; - background: rgba(30, 41, 59, 0.8); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); -} - -.form-input:focus + .input-icon, -.form-input:focus ~ .input-icon { - color: #6366f1; -} - -.input-hint { - font-size: 12px; - color: #64748b; - margin-top: 4px; -} - -/* 切换密码可见性按钮 */ -.toggle-password { - position: absolute; - right: 12px; - background: none; - border: none; - padding: 4px; - cursor: pointer; - color: #64748b; - transition: color 0.2s; -} - -.toggle-password:hover { - color: #6366f1; -} - -.toggle-password svg { - width: 18px; - height: 18px; -} - -/* 表单选项 */ -.form-options { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 14px; -} - -.checkbox-label { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - color: #94a3b8; -} - -.checkbox-label input[type="checkbox"] { - width: 16px; - height: 16px; - accent-color: #6366f1; - cursor: pointer; -} - -.forgot-link { - color: #6366f1; - text-decoration: none; - transition: color 0.2s; -} - -.forgot-link:hover { - color: #8b5cf6; -} - -/* 警告框 */ -.alert { - padding: 12px 16px; - border-radius: 8px; - font-size: 14px; - display: none; -} - -.alert-error { - background: rgba(239, 68, 68, 0.1); - border: 1px solid rgba(239, 68, 68, 0.3); - color: #fca5a5; -} - -.alert-success { - background: rgba(34, 197, 94, 0.1); - border: 1px solid rgba(34, 197, 94, 0.3); - color: #86efac; -} - -/* 按钮样式 */ -.btn-auth { - width: 100%; - padding: 14px 24px; - font-size: 16px; - font-weight: 600; - margin-top: 8px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; -} - -.btn-auth:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.btn-spinner { - width: 20px; - height: 20px; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -/* 认证页脚 */ -.auth-footer { - text-align: center; - margin-top: 24px; - padding-top: 24px; - border-top: 1px solid rgba(99, 102, 241, 0.2); - color: #94a3b8; - font-size: 14px; -} - -.auth-link { - color: #6366f1; - text-decoration: none; - font-weight: 500; - transition: color 0.2s; -} - -.auth-link:hover { - color: #8b5cf6; - text-decoration: underline; -} - -/* 装饰元素 */ -.auth-decoration { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - overflow: hidden; - z-index: -1; -} - -.deco-circle { - position: absolute; - border-radius: 50%; - background: radial-gradient(circle, rgba(99, 102, 241, 0.3) 0%, transparent 70%); -} - -.deco-circle-1 { - width: 400px; - height: 400px; - top: -100px; - right: -100px; - animation: float 20s ease-in-out infinite; -} - -.deco-circle-2 { - width: 300px; - height: 300px; - bottom: -50px; - left: 10%; - background: radial-gradient(circle, rgba(139, 92, 246, 0.3) 0%, transparent 70%); - animation: float 15s ease-in-out infinite reverse; -} - -.deco-circle-3 { - width: 200px; - height: 200px; - top: 40%; - right: 15%; - background: radial-gradient(circle, rgba(6, 182, 212, 0.2) 0%, transparent 70%); - animation: float 18s ease-in-out infinite; -} - -@keyframes float { - 0%, 100% { - transform: translate(0, 0); - } - 25% { - transform: translate(20px, -20px); - } - 50% { - transform: translate(-10px, 15px); - } - 75% { - transform: translate(15px, 10px); - } -} - -/* 响应式 */ -@media (max-width: 640px) { - .auth-page { - margin-left: 0; - padding: 20px 16px; - } - - .auth-card { - padding: 32px 24px; - } - - .auth-title { - font-size: 24px; - } - - .form-options { - flex-direction: column; - gap: 12px; - align-items: flex-start; - } -} diff --git a/website/community/assets/css/community.css b/website/community/assets/css/community.css deleted file mode 100644 index d3c49e7..0000000 --- a/website/community/assets/css/community.css +++ /dev/null @@ -1,572 +0,0 @@ -/* OSS Community 样式 - 已对齐官网视觉规范 */ -:root { - --bg: #030712; - --bg-card: rgba(255, 255, 255, 0.02); - --border: rgba(255, 255, 255, 0.05); - --border-hover: rgba(6, 182, 212, 0.3); - --cyan: #06b6d4; - --cyan-light: #22d3ee; - --text: #fff; - --text-secondary: #9ca3af; - --text-muted: #6b7280; -} - -* { margin: 0; padding: 0; box-sizing: border-box; } - -body { - font-family: 'Inter', sans-serif; - background: var(--bg); - color: var(--text); - line-height: 1.6; - overflow-x: hidden; -} - -/* 全局背景网格 */ -body::before { - content: ''; - position: fixed; - inset: 0; - background-image: - linear-gradient(rgba(6, 182, 212, 0.03) 1px, transparent 1px), - linear-gradient(90deg, rgba(6, 182, 212, 0.03) 1px, transparent 1px); - background-size: 60px 60px; - pointer-events: none; - z-index: -1; -} - -/* 头部导航 */ -.comm-header { - position: sticky; - top: 0; - z-index: 100; - background: rgba(3, 7, 18, 0.8); - backdrop-filter: blur(16px); - border-bottom: 1px solid var(--border); - transition: border-color 0.3s; -} - -.header-container { - max-width: 1400px; - margin: 0 auto; - padding: 0 24px; - height: 64px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.header-left { - display: flex; - align-items: center; - gap: 24px; -} - -.back-link { - color: var(--text-muted); - text-decoration: none; - font-size: 14px; - transition: all 0.3s ease; - display: flex; - align-items: center; - gap: 4px; -} - -.back-link:hover { - color: var(--cyan-light); - transform: translateX(-4px); -} - -.site-title { - font-size: 18px; - font-weight: 700; - background: linear-gradient(135deg, var(--cyan), #3b82f6); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - letter-spacing: 0.02em; -} - -.btn { - padding: 10px 20px; - border-radius: 10px; - font-size: 14px; - font-weight: 600; - cursor: pointer; - border: none; - transition: all 0.3s ease; - display: flex; - align-items: center; - gap: 6px; -} - -.btn-primary { - background: linear-gradient(135deg, var(--cyan), #3b82f6); - color: #fff; - box-shadow: 0 4px 12px rgba(6, 182, 212, 0.1); -} - -.btn-primary:hover { - box-shadow: 0 8px 24px rgba(6, 182, 212, 0.4); - transform: translateY(-2px); -} - -/* 主布局 */ -.comm-main { - max-width: 1200px; - margin: 0 auto; - padding: 24px 100px 24px 24px; /* 右侧为 Dock 留空间 */ - min-height: calc(100vh - 64px); -} - -.comm-container { - display: grid; - grid-template-columns: 260px 1fr; - gap: 32px; - animation: fadeInUp 0.6s ease forwards; - opacity: 0; - transform: translateY(20px); -} - -@keyframes fadeInUp { - to { opacity: 1; transform: translateY(0); } -} - -/* 侧边栏 */ -.comm-sidebar { - position: sticky; - top: 88px; - height: fit-content; -} - -.sidebar-section { - background: rgba(10, 15, 30, 0.6); - border: 1px solid var(--border); - border-radius: 16px; - padding: 20px; - margin-bottom: 20px; - backdrop-filter: blur(8px); - transition: all 0.3s ease; -} - -.sidebar-section:hover { - border-color: rgba(255, 255, 255, 0.1); -} - -.sidebar-section h3 { - font-size: 12px; - font-weight: 700; - color: var(--text-muted); - margin-bottom: 16px; - text-transform: uppercase; - letter-spacing: 0.1em; -} - -.category-list { list-style: none; } - -.category-list li { - display: flex; - align-items: center; - gap: 12px; - padding: 12px; - border-radius: 10px; - cursor: pointer; - transition: all 0.25s ease; - margin-bottom: 4px; - color: var(--text-secondary); - font-size: 14px; - font-weight: 500; -} - -.category-list li:hover { - background: rgba(255, 255, 255, 0.04); - color: #fff; - transform: translateX(4px); -} - -.category-list li.active { - background: rgba(6, 182, 212, 0.1); - border-left: 3px solid var(--cyan); - color: var(--cyan-light); -} - -.cat-icon { - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - opacity: 0.7; -} - -.cat-icon svg { - width: 16px; - height: 16px; - fill: none; - stroke: currentColor; - stroke-width: 2; -} - -.category-list li.active .cat-icon { opacity: 1; color: var(--cyan); } - -.cat-count { - font-size: 11px; - color: var(--text-muted); - background: rgba(255, 255, 255, 0.03); - padding: 2px 8px; - border-radius: 6px; - font-weight: 600; -} - -.stats-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 12px; - text-align: center; -} - -.stat-item { - padding: 10px 4px; - background: rgba(255, 255, 255, 0.02); - border-radius: 8px; -} - -.stat-num { - display: block; - font-size: 20px; - font-weight: 800; - color: var(--cyan-light); - margin-bottom: 4px; -} - -.stat-label { - font-size: 10px; - color: var(--text-muted); - font-weight: 500; - text-transform: uppercase; -} - -/* 内容区 */ -.comm-content { min-width: 0; } - -.content-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 20px; - padding-bottom: 16px; - border-bottom: 1px solid var(--border); -} - -.content-header h2 { - font-size: 22px; - font-weight: 700; - color: #fff; - display: flex; - align-items: center; - gap: 10px; -} - -.content-header h2::before { - content: ''; - display: block; - width: 4px; - height: 20px; - background: var(--cyan); - border-radius: 2px; -} - -.sort-options { - display: flex; - gap: 8px; - background: rgba(255, 255, 255, 0.02); - border: 1px solid var(--border); - border-radius: 10px; - padding: 4px; -} - -.sort-btn { - padding: 6px 14px; - border-radius: 8px; - background: transparent; - border: none; - color: var(--text-secondary); - font-size: 13px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -} - -.sort-btn:hover { color: #fff; } - -.sort-btn.active { - background: rgba(6, 182, 212, 0.15); - color: var(--cyan-light); - box-shadow: 0 2px 6px rgba(6, 182, 212, 0.1); -} - -/* 帖子列表 - 卡片动效 */ -.posts-list { display: flex; flex-direction: column; gap: 16px; } - -.post-card { - background: rgba(10, 15, 30, 0.6); - border: 1px solid var(--border); - border-radius: 16px; - padding: 24px; - transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); - cursor: pointer; - position: relative; - overflow: hidden; - animation: cardEnter 0.5s ease forwards; - opacity: 0; - transform: translateY(20px); -} - -@keyframes cardEnter { - to { opacity: 1; transform: translateY(0); } -} - -.post-card::before { - content: ''; - position: absolute; - top: 0; left: 0; right: 0; bottom: 0; - background: linear-gradient(135deg, rgba(6, 182, 212, 0.05), transparent); - opacity: 0; - transition: opacity 0.4s; -} - -.post-card:hover { - border-color: var(--border-hover); - transform: translateY(-4px) scale(1.005); - box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4), 0 0 12px rgba(6, 182, 212, 0.1); -} - -.post-card:hover::before { opacity: 1; } - -.post-header { - display: flex; - align-items: center; - gap: 14px; - margin-bottom: 14px; - position: relative; - z-index: 1; -} - -.post-avatar { - width: 44px; - height: 44px; - border-radius: 12px; - background: linear-gradient(135deg, var(--cyan), #3b82f6); - display: flex; - align-items: center; - justify-content: center; - font-weight: 800; - font-size: 18px; - color: #fff; - box-shadow: 0 4px 10px rgba(6, 182, 212, 0.3); - flex-shrink: 0; -} - -.post-meta { flex: 1; min-width: 0; } - -.post-author { - font-weight: 600; - font-size: 14px; - color: #fff; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.post-time { - font-size: 12px; - color: var(--text-muted); -} - -.post-tags { display: flex; gap: 6px; flex-shrink: 0; } - -.tag { - padding: 3px 8px; - border-radius: 6px; - font-size: 11px; - font-weight: 600; - background: rgba(6, 182, 212, 0.08); - border: 1px solid rgba(6, 182, 212, 0.15); - color: var(--cyan-light); - text-transform: uppercase; -} - -.pinned-badge { - padding: 3px 8px; - border-radius: 6px; - font-size: 11px; - font-weight: 700; - background: rgba(245, 158, 11, 0.15); - border: 1px solid rgba(245, 158, 11, 0.3); - color: #fbbf24; - display: flex; - align-items: center; - gap: 4px; -} - -.post-title { - font-size: 17px; - font-weight: 700; - margin-bottom: 8px; - display: flex; - align-items: flex-start; - gap: 8px; - position: relative; - z-index: 1; - color: #fff; - transition: color 0.2s; -} - -.post-card:hover .post-title { color: var(--cyan-light); } - -.post-excerpt { - font-size: 14px; - color: var(--text-secondary); - line-height: 1.6; - margin-bottom: 14px; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - position: relative; - z-index: 1; -} - -.post-stats { - display: flex; - gap: 20px; - font-size: 12px; - color: var(--text-muted); - font-weight: 500; - position: relative; - z-index: 1; -} - -.post-stats span { - display: flex; - align-items: center; - gap: 6px; - transition: color 0.2s; -} - -.post-stats svg { - width: 14px; - height: 14px; - opacity: 0.6; -} - -.post-card:hover .post-stats span { color: var(--text-secondary); } - -/* 空状态 / 加载动画 */ -.empty-state { - text-align: center; - padding: 80px 20px; - color: var(--text-muted); - animation: fadeIn 0.5s ease; -} - -@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } - -.loading-spinner { - width: 32px; - height: 32px; - border: 3px solid rgba(6, 182, 212, 0.1); - border-top-color: var(--cyan); - border-radius: 50%; - animation: spin 0.8s linear infinite; - margin: 0 auto 16px; -} - -@keyframes spin { to { transform: rotate(360deg); } } - -.empty-state-icon { - font-size: 48px; - margin-bottom: 16px; - opacity: 0.4; -} - -/* 分页 */ -.pagination { - display: flex; - justify-content: center; - gap: 8px; - margin-top: 32px; - padding-bottom: 32px; -} - -.page-btn { - width: 36px; - height: 36px; - border-radius: 10px; - background: rgba(255, 255, 255, 0.02); - border: 1px solid var(--border); - color: var(--text-secondary); - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 14px; -} - -.page-btn:hover { - background: rgba(6, 182, 212, 0.1); - border-color: var(--border-hover); - color: var(--cyan); - transform: translateY(-2px); -} - -.page-btn.active { - background: var(--cyan); - border-color: var(--cyan); - color: #fff; - box-shadow: 0 4px 12px rgba(6, 182, 212, 0.4); -} - -/* 响应式 */ -@media (max-width: 900px) { - .comm-container { - grid-template-columns: 1fr; - } - .comm-sidebar { - position: static; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 16px; - } - .stats-grid { margin-top: 0; } -} - -@media (max-width: 600px) { - .header-container { padding: 0 16px; } - .comm-main { padding: 16px; } - .content-header h2 { font-size: 18px; } - .sort-options { display: none; } - .post-card { padding: 16px; } - .post-title { font-size: 15px; } -} -/* 创建帖子按钮样式 - 区别于普通导航链接 */ -button.dock-item { - background: transparent; - border: none; - cursor: pointer; -} - -.dock-action-btn { - color: #10b981; /* 翡翠绿 - 代表创建/动作 */ - background: rgba(16, 185, 129, 0.08); - border: 1px solid rgba(16, 185, 129, 0.2); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); -} - -.dock-action-btn:hover { - background: rgba(16, 185, 129, 0.2); - border-color: #10b981; - color: #34d399; - box-shadow: 0 0 12px rgba(16, 185, 129, 0.4); - transform: scale(1.1); -} diff --git a/website/community/assets/css/dock-popover.css b/website/community/assets/css/dock-popover.css deleted file mode 100644 index 3ca1507..0000000 --- a/website/community/assets/css/dock-popover.css +++ /dev/null @@ -1,177 +0,0 @@ -/* Dock 用户信息面板样式 */ - -.user-popover { - position: fixed; - z-index: 10001; - width: 260px; - background: rgba(15, 23, 42, 0.95); - backdrop-filter: blur(12px); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: 12px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); - opacity: 0; - visibility: hidden; - transform: translateY(10px) scale(0.98); - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - pointer-events: none; - color: #e2e8f0; - font-family: 'Inter', sans-serif; -} - -.user-popover.active { - opacity: 1; - visibility: visible; - transform: translateY(0) scale(1); - pointer-events: auto; -} - -.popover-header { - display: flex; - align-items: center; - gap: 12px; - padding: 16px; - border-bottom: 1px solid rgba(99, 102, 241, 0.1); -} - -.popover-avatar { - width: 44px; - height: 44px; - border-radius: 10px; - background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; - font-weight: 700; - color: white; - flex-shrink: 0; -} - -.popover-info { - flex: 1; - min-width: 0; -} - -.popover-name { - font-size: 14px; - font-weight: 600; - color: #f1f5f9; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.popover-role { - font-size: 11px; - color: #94a3b8; - margin-top: 2px; - text-transform: uppercase; - letter-spacing: 0.5px; - background: rgba(255, 255, 255, 0.05); - padding: 2px 6px; - border-radius: 4px; - display: inline-block; -} - -.popover-menu { - padding: 8px; - border-bottom: 1px solid rgba(99, 102, 241, 0.1); -} - -.menu-item { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - border-radius: 8px; - color: #e2e8f0; - text-decoration: none; - transition: all 0.2s; - background: transparent; - border: none; - cursor: pointer; - width: 100%; -} - -.menu-item:hover { - background: rgba(99, 102, 241, 0.15); - color: #f1f5f9; -} - -.menu-item svg { - width: 16px; - height: 16px; - flex-shrink: 0; - color: #94a3b8; -} - -.menu-item:hover svg { - color: #c7d2fe; -} - -.menu-item span { - flex: 1; - font-size: 13px; - font-weight: 500; -} - -.menu-badge { - font-size: 11px; - font-weight: 600; - padding: 2px 8px; - border-radius: 10px; - background: rgba(99, 102, 241, 0.3); - color: #c7d2fe; - min-width: 24px; - text-align: center; -} - -.popover-footer { - padding: 12px; - display: flex; - flex-direction: column; - gap: 8px; -} - -.popover-btn { - width: 100%; - padding: 8px 12px; - border-radius: 8px; - font-size: 13px; - font-weight: 500; - cursor: pointer; - border: none; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; -} - -.btn-logout { - background: rgba(99, 102, 241, 0.2); - color: #c7d2fe; - border: 1px solid rgba(99, 102, 241, 0.3); -} - -.btn-logout:hover { - background: rgba(99, 102, 241, 0.3); - color: white; -} - -.btn-danger { - background: transparent; - color: #94a3b8; - border: 1px solid rgba(100, 116, 139, 0.2); -} - -.btn-danger:hover { - background: rgba(239, 68, 68, 0.1); - color: #fca5a5; - border-color: rgba(239, 68, 68, 0.3); -} - -.btn-danger svg { - width: 14px; - height: 14px; -} diff --git a/website/community/assets/css/editor.css b/website/community/assets/css/editor.css deleted file mode 100644 index 6d81291..0000000 --- a/website/community/assets/css/editor.css +++ /dev/null @@ -1,325 +0,0 @@ -/* OSS Community Editor - 优化版两栏布局 */ - -* { margin: 0; padding: 0; box-sizing: border-box; } - -.editor-page { - min-height: 100vh; - background: #0f172a; - display: flex; - flex-direction: column; -} - -.editor-container { - flex: 1; - display: flex; - flex-direction: column; - padding: 24px; - padding-right: 100px; - max-width: 1400px; - margin: 0 auto; - width: 100%; -} - -/* 顶部工具栏 */ -.editor-toolbar { - flex-shrink: 0; - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 18px; - background: rgba(15, 23, 42, 0.9); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: 12px; - margin-bottom: 20px; -} - -.toolbar-left { display: flex; align-items: center; gap: 12px; } - -.back-btn { - display: flex; align-items: center; gap: 6px; - color: #94a3b8; text-decoration: none; font-size: 14px; font-weight: 500; - transition: color 0.2s; -} -.back-btn:hover { color: #e2e8f0; } -.back-btn svg { width: 18px; height: 18px; } - -.toolbar-title { - font-size: 16px; font-weight: 700; - background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; background-clip: text; -} - -.toolbar-right { display: flex; gap: 8px; } - -.btn { - display: flex; align-items: center; gap: 6px; - padding: 8px 16px; border-radius: 8px; font-size: 13px; - font-weight: 600; cursor: pointer; border: none; transition: all 0.2s; -} -.btn svg { width: 16px; height: 16px; } - -.btn-outline { - background: rgba(100, 116, 139, 0.2); - color: #94a3b8; border: 1px solid rgba(100, 116, 139, 0.3); -} -.btn-outline:hover { background: rgba(100, 116, 139, 0.3); color: #e2e8f0; } - -.btn-primary { - background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); - color: white; box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); -} -.btn-primary:hover { box-shadow: 0 4px 16px rgba(99, 102, 241, 0.4); transform: translateY(-1px); } - -/* 标题输入 */ -.editor-header { - flex-shrink: 0; - margin-bottom: 20px; -} - -.title-input { - width: 100%; - padding: 12px 16px; - background: rgba(30, 41, 59, 0.6); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: 10px; - color: #e2e8f0; - font-size: 24px; - font-weight: 700; - transition: all 0.2s; -} -.title-input:focus { - outline: none; border-color: #6366f1; - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); -} -.title-input::placeholder { color: #64748b; font-weight: 400; } - -/* 两栏主工作区 */ -.editor-workspace { - display: grid; - grid-template-columns: 1fr 280px; - gap: 20px; - flex: 1; - min-height: calc(100vh - 280px); -} - -/* 编辑器面板 */ -.editor-panel { - background: rgba(15, 23, 42, 0.6); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: 12px; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.panel-header { - flex-shrink: 0; - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 14px; - background: rgba(30, 41, 59, 0.4); - border-bottom: 1px solid rgba(99, 102, 241, 0.1); -} - -.panel-title { - font-size: 12px; font-weight: 600; color: #94a3b8; - text-transform: uppercase; letter-spacing: 0.05em; -} - -.panel-actions { display: flex; gap: 4px; } - -.md-btn { - padding: 6px 8px; - background: transparent; border: 1px solid transparent; border-radius: 6px; - color: #64748b; cursor: pointer; transition: all 0.2s; -} -.md-btn:hover { - background: rgba(99, 102, 241, 0.1); - border-color: rgba(99, 102, 241, 0.2); - color: #e2e8f0; -} -.md-btn svg { width: 16px; height: 16px; } - -.editor-textarea { - flex: 1; - width: 100%; - padding: 18px; - background: transparent; - border: none; - color: #e2e8f0; - font-family: 'JetBrains Mono', monospace; - font-size: 14px; - line-height: 1.8; - resize: none; - overflow-y: auto; - tab-size: 2; - min-height: 600px; -} -.editor-textarea:focus { outline: none; } -.editor-textarea::placeholder { color: #475569; } - -/* 右侧栏 */ -.sidebar-panel { - display: flex; - flex-direction: column; - gap: 16px; -} - -.sidebar-section { - background: rgba(15, 23, 42, 0.6); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: 12px; - padding: 16px; -} - -.sidebar-title { - font-size: 12px; - font-weight: 600; - color: #94a3b8; - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 12px; -} - -.meta-select-large { - width: 100%; - padding: 8px 12px; - background: rgba(30, 41, 59, 0.6); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: 8px; - color: #e2e8f0; - font-size: 13px; -} -.meta-select-large:focus { outline: none; border-color: #6366f1; } - -/* 标签管理 */ -.tags-container { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 10px; -} - -.tag-item { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - background: rgba(99, 102, 241, 0.15); - border: 1px solid rgba(99, 102, 241, 0.3); - border-radius: 14px; - font-size: 12px; - font-weight: 500; - color: #c7d2fe; -} - -.tag-remove { - background: none; border: none; color: #94a3b8; - cursor: pointer; font-size: 16px; line-height: 1; padding: 0; - width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; -} -.tag-remove:hover { color: #ef4444; } - -.tag-input { - width: 100%; - padding: 6px 10px; - background: rgba(15, 23, 42, 0.8); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: 6px; - color: #e2e8f0; - font-size: 12px; - margin-bottom: 10px; -} -.tag-input:focus { outline: none; border-color: #6366f1; } - -.tags-suggestions { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.tag-suggestion { - padding: 3px 10px; - background: rgba(100, 116, 139, 0.15); - border: 1px solid rgba(100, 116, 139, 0.2); - border-radius: 12px; - font-size: 11px; - color: #94a3b8; - cursor: pointer; - transition: all 0.2s; -} -.tag-suggestion:hover { - background: rgba(99, 102, 241, 0.1); - border-color: rgba(99, 102, 241, 0.2); - color: #c7d2fe; -} - -/* Markdown 帮助 */ -.markdown-help { - display: flex; - flex-direction: column; - gap: 6px; -} - -.help-item { - font-size: 12px; - color: #94a3b8; - line-height: 1.5; -} - -.help-item code { - display: inline-block; - background: rgba(30, 41, 59, 0.6); - padding: 2px 6px; - border-radius: 4px; - font-family: 'JetBrains Mono', monospace; - font-size: 11px; - color: #f472b6; - margin-right: 6px; - min-width: 90px; -} - -/* Toast 提示 */ -.toast { - position: fixed; bottom: 16px; right: 16px; - padding: 10px 14px; border-radius: 8px; - display: flex; align-items: center; gap: 6px; - font-size: 13px; font-weight: 500; - z-index: 10000; animation: toastSlideIn 0.3s ease-out; - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3); -} -@keyframes toastSlideIn { - from { opacity: 0; transform: translateX(100px); } - to { opacity: 1; transform: translateX(0); } -} -.toast svg { width: 16px; height: 16px; } -.toast-success { background: rgba(34, 197, 94, 0.15); border: 1px solid rgba(34, 197, 94, 0.3); color: #86efac; } -.toast-error { background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.3); color: #fca5a5; } - -/* 响应式 */ -@media (max-width: 1024px) { - .editor-container { padding: 20px; } - .editor-workspace { grid-template-columns: 1fr; } - .sidebar-panel { - flex-direction: row; - flex-wrap: wrap; - } - .sidebar-section { flex: 1; min-width: 200px; } -} - -@media (max-width: 768px) { - .editor-page { margin-left: 0; } - .editor-container { padding: 16px; padding-bottom: 80px; } - .editor-toolbar { - flex-direction: column; - gap: 12px; - padding: 12px; - } - .toolbar-left, .toolbar-right { width: 100%; justify-content: center; } - .title-input { font-size: 18px; padding: 10px 14px; } - .editor-workspace { min-height: auto; } - .sidebar-panel { flex-direction: column; } - .editor-textarea { min-height: 400px; } -} diff --git a/website/community/assets/css/post-drawer.css b/website/community/assets/css/post-drawer.css deleted file mode 100644 index 32779dd..0000000 --- a/website/community/assets/css/post-drawer.css +++ /dev/null @@ -1,316 +0,0 @@ -/* OSS Community - 文章抽屉样式 */ - -/* 遮罩层 */ -.post-drawer-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(4px); - z-index: 9998; - opacity: 0; - visibility: hidden; - transition: all 0.3s ease; -} - -.post-drawer-overlay.active { - opacity: 1; - visibility: visible; -} - -/* 抽屉容器 */ -.post-drawer { - position: fixed; - left: 50%; - bottom: 0; - transform: translateX(-50%) translateY(100%); - width: 90%; - max-width: 900px; - max-height: 85vh; - background: #0f172a; - border: 1px solid rgba(99, 102, 241, 0.2); - border-bottom: none; - border-radius: 20px 20px 0 0; - box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.5); - z-index: 9999; - display: flex; - flex-direction: column; - transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); - overflow: hidden; -} - -.post-drawer.active { - transform: translateX(-50%) translateY(0); -} - -/* 抽屉顶部标题栏 */ -.post-drawer-header { - flex-shrink: 0; - padding: 24px 28px 20px; - border-bottom: 1px solid rgba(99, 102, 241, 0.1); - background: rgba(15, 23, 42, 0.95); - position: relative; -} - -.post-drawer-close { - position: absolute; - top: 16px; - right: 16px; - width: 36px; - height: 36px; - border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(30, 41, 59, 0.6); - color: #94a3b8; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s; -} - -.post-drawer-close:hover { - background: rgba(239, 68, 68, 0.2); - border-color: rgba(239, 68, 68, 0.3); - color: #fca5a5; - transform: scale(1.05); -} - -.post-drawer-close svg { - width: 18px; - height: 18px; -} - -.post-drawer-title { - font-size: 22px; - font-weight: 700; - color: #e2e8f0; - margin: 0 0 12px 0; - padding-right: 50px; - line-height: 1.3; -} - -.post-drawer-meta { - display: flex; - align-items: center; - gap: 12px; - color: #64748b; - font-size: 13px; -} - -.post-drawer-avatar { - width: 32px; - height: 32px; - border-radius: 8px; - background: linear-gradient(135deg, #6366f1, #8b5cf6); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 14px; - color: white; - flex-shrink: 0; -} - -.post-drawer-user-info { - display: flex; - flex-direction: column; - gap: 2px; -} - -.post-drawer-username { - color: #e2e8f0; - font-weight: 600; - font-size: 13px; -} - -.post-drawer-date { - color: #64748b; - font-size: 12px; -} - -.post-drawer-category { - font-size: 11px; - font-weight: 600; - padding: 3px 10px; - border-radius: 8px; - background: rgba(99, 102, 241, 0.2); - color: #c7d2fe; - margin-left: auto; -} - -/* 抽屉内容区域 */ -.post-drawer-body { - flex: 1; - overflow-y: auto; - padding: 28px; -} - -.post-drawer-content { - color: #cbd5e1; - font-size: 15px; - line-height: 1.8; -} - -.post-drawer-content h1, -.post-drawer-content h2, -.post-drawer-content h3 { - color: #e2e8f0; - margin: 1.2em 0 0.6em; - font-weight: 700; -} - -.post-drawer-content h1 { font-size: 1.8em; border-bottom: 2px solid rgba(99, 102, 241, 0.3); padding-bottom: 0.3em; } -.post-drawer-content h2 { font-size: 1.4em; border-bottom: 1px solid rgba(99, 102, 241, 0.2); padding-bottom: 0.2em; } -.post-drawer-content h3 { font-size: 1.2em; } - -.post-drawer-content p { - color: #cbd5e1; - line-height: 1.8; - margin: 0.8em 0; -} - -.post-drawer-content code { - background: rgba(30, 41, 59, 0.6); - padding: 2px 6px; - border-radius: 4px; - font-family: 'JetBrains Mono', monospace; - font-size: 0.9em; - color: #f472b6; -} - -.post-drawer-content pre { - background: rgba(15, 23, 42, 0.8); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: 8px; - padding: 16px; - overflow-x: auto; - margin: 0.8em 0; -} - -.post-drawer-content pre code { - background: transparent; - padding: 0; - color: #e2e8f0; -} - -.post-drawer-content blockquote { - border-left: 4px solid #6366f1; - margin: 0.8em 0; - padding: 12px 16px; - background: rgba(99, 102, 241, 0.05); - border-radius: 0 8px 8px 0; - color: #94a3b8; -} - -.post-drawer-content ul, -.post-drawer-content ol { - padding-left: 24px; - color: #cbd5e1; -} - -.post-drawer-content li { - margin: 0.4em 0; -} - -.post-drawer-content a { - color: #6366f1; - text-decoration: none; -} - -.post-drawer-content a:hover { - text-decoration: underline; -} - -.post-drawer-content img { - max-width: 100%; - border-radius: 8px; - margin: 0.8em 0; -} - -.post-drawer-content hr { - border: none; - height: 1px; - background: rgba(99, 102, 241, 0.2); - margin: 1.5em 0; -} - -/* 底部统计栏 */ -.post-drawer-footer { - flex-shrink: 0; - padding: 16px 28px; - border-top: 1px solid rgba(99, 102, 241, 0.1); - background: rgba(15, 23, 42, 0.95); - display: flex; - gap: 24px; - color: #64748b; - font-size: 13px; - font-weight: 500; -} - -.post-drawer-stat { - display: flex; - align-items: center; - gap: 6px; -} - -.post-drawer-stat svg { - width: 16px; - height: 16px; - opacity: 0.7; -} - -/* 滚动条样式 */ -.post-drawer-body::-webkit-scrollbar { - width: 6px; -} - -.post-drawer-body::-webkit-scrollbar-track { - background: transparent; -} - -.post-drawer-body::-webkit-scrollbar-thumb { - background: rgba(99, 102, 241, 0.3); - border-radius: 3px; -} - -.post-drawer-body::-webkit-scrollbar-thumb:hover { - background: rgba(99, 102, 241, 0.5); -} - -/* 响应式 */ -@media (max-width: 768px) { - .post-drawer { - width: 100%; - max-width: none; - max-height: 90vh; - border-radius: 16px 16px 0 0; - } - - .post-drawer-header { - padding: 20px 20px 16px; - } - - .post-drawer-title { - font-size: 18px; - } - - .post-drawer-body { - padding: 20px; - } - - .post-drawer-content { - font-size: 14px; - } - - .post-drawer-footer { - padding: 14px 20px; - gap: 16px; - } - - .post-drawer-close { - top: 12px; - right: 12px; - width: 32px; - height: 32px; - } -} diff --git a/website/community/assets/js/auth.js b/website/community/assets/js/auth.js deleted file mode 100644 index 727b127..0000000 --- a/website/community/assets/js/auth.js +++ /dev/null @@ -1,168 +0,0 @@ -/** - * OSS Community 认证页面 JS - * 处理登录/注册表单提交 - */ - -// 切换密码可见性 -function togglePassword(fieldId = 'password') { - const input = document.getElementById(fieldId); - const icon = document.getElementById(`eyeIcon-${fieldId}`) || document.getElementById('eyeIcon'); - - if (input.type === 'password') { - input.type = 'text'; - icon.innerHTML = ` - - `; - } else { - input.type = 'password'; - icon.innerHTML = ` - - - `; - } -} - -// 显示错误消息 -function showError(message) { - const errorEl = document.getElementById('errorMessage'); - const successEl = document.getElementById('successMessage'); - if (errorEl) { - errorEl.textContent = message; - errorEl.style.display = 'block'; - } - if (successEl) { - successEl.style.display = 'none'; - } -} - -// 显示成功消息 -function showSuccess(message) { - const successEl = document.getElementById('successMessage'); - const errorEl = document.getElementById('errorMessage'); - if (successEl) { - successEl.textContent = message; - successEl.style.display = 'block'; - } - if (errorEl) { - errorEl.style.display = 'none'; - } -} - -// 隐藏消息 -function hideMessages() { - const errorEl = document.getElementById('errorMessage'); - const successEl = document.getElementById('successMessage'); - if (errorEl) errorEl.style.display = 'none'; - if (successEl) successEl.style.display = 'none'; -} - -// 设置按钮加载状态 -function setButtonLoading(loading) { - const btn = document.getElementById('submitBtn'); - const text = btn.querySelector('.btn-text'); - const spinner = btn.querySelector('.btn-spinner'); - - if (loading) { - btn.disabled = true; - text.style.display = 'none'; - spinner.style.display = 'block'; - } else { - btn.disabled = false; - text.style.display = 'inline'; - spinner.style.display = 'none'; - } -} - -// 登录表单 -const loginForm = document.getElementById('loginForm'); -if (loginForm) { - loginForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideMessages(); - setButtonLoading(true); - - const formData = { - username: document.getElementById('username').value.trim(), - password: document.getElementById('password').value, - remember: document.getElementById('remember').checked - }; - - try { - const response = await fetch('api/auth.php?action=login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(formData) - }); - - const result = await response.json(); - - if (result.success) { - showSuccess('登录成功!正在跳转...'); - setTimeout(() => { - window.location.href = 'index.php'; - }, 1000); - } else { - showError(result.message || '登录失败,请检查用户名和密码'); - } - } catch (error) { - showError('网络错误,请稍后重试'); - } finally { - setButtonLoading(false); - } - }); -} - -// 注册表单 -const registerForm = document.getElementById('registerForm'); -if (registerForm) { - registerForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideMessages(); - setButtonLoading(true); - - const password = document.getElementById('password').value; - const confirmPassword = document.getElementById('confirmPassword').value; - - // 前端验证 - if (password !== confirmPassword) { - showError('两次输入的密码不一致'); - setButtonLoading(false); - return; - } - - if (password.length < 6) { - showError('密码长度至少 6 个字符'); - setButtonLoading(false); - return; - } - - const formData = { - username: document.getElementById('username').value.trim(), - email: document.getElementById('email').value.trim(), - password: password - }; - - try { - const response = await fetch('api/auth.php?action=register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(formData) - }); - - const result = await response.json(); - - if (result.success) { - showSuccess('注册成功!正在跳转到登录页...'); - setTimeout(() => { - window.location.href = 'login.php'; - }, 1500); - } else { - showError(result.message || '注册失败,请稍后重试'); - } - } catch (error) { - showError('网络错误,请稍后重试'); - } finally { - setButtonLoading(false); - } - }); -} diff --git a/website/community/assets/js/community.js b/website/community/assets/js/community.js deleted file mode 100644 index 1fa9709..0000000 --- a/website/community/assets/js/community.js +++ /dev/null @@ -1,255 +0,0 @@ -/** - * OSS Community Module - * Handles rendering of categories, stats, and posts list. - * Designed to be SPA-friendly (re-initializable). - */ - -// Global state variables -let currentPage = 1; -let currentCategory = ''; -let currentSort = 'latest'; - -// Expose initialization function globally for SPA Router -window.initCommunity = function() { - // Check if we are deep-linked (e.g. ?page=2) - const params = new URLSearchParams(window.location.search); - if(params.has('page')) currentPage = parseInt(params.get('page')); - - // Fetch and Render Data - loadCategories(); - loadStats(); - loadPosts(); - - // Bind Events (to the new DOM elements created by SPA) - bindCommunityEvents(); -}; - -function bindCommunityEvents() { - const catList = document.getElementById('categoryList'); - if (catList) { - catList.addEventListener('click', e => { - const li = e.target.closest('li'); - if (!li) return; - - document.querySelectorAll('.category-list li').forEach(el => el.classList.remove('active')); - li.classList.add('active'); - - currentCategory = li.dataset.id || ''; - currentPage = 1; - document.getElementById('currentCategory').textContent = li.querySelector('span:nth-child(2)').textContent + '帖子'; - loadPosts(); - }); - } - - const sortOptions = document.querySelector('.sort-options'); - if (sortOptions) { - sortOptions.addEventListener('click', e => { - if (!e.target.classList.contains('sort-btn')) return; - document.querySelectorAll('.sort-btn').forEach(btn => btn.classList.remove('active')); - e.target.classList.add('active'); - currentSort = e.target.dataset.sort; - currentPage = 1; - loadPosts(); - }); - } -} - -// --- Data Loading Functions --- - -const API = './api/index.php'; // Relative to community/ directory - -async function loadCategories() { - try { - const res = await fetch(`${API}?action=categories`); - const data = await res.json(); - - const list = document.getElementById('categoryList'); - if (!list) return; - - // Keep the "All" item (first child) if it exists - const allItem = list.firstElementChild; - list.innerHTML = ''; - if (allItem) list.appendChild(allItem); - - data.categories.forEach(cat => { - const li = document.createElement('li'); - li.dataset.id = cat.id; - li.innerHTML = `${getIconSvg(cat.icon)}${cat.name}`; - list.appendChild(li); - }); - } catch (e) { - console.error('Failed to load categories', e); - } -} - -async function loadStats() { - try { - const res = await fetch(`${API}?action=stats`); - const data = await res.json(); - - animateValue('statPosts', 0, data.posts, 1000); - animateValue('statReplies', 0, data.replies, 1000); - animateValue('statUsers', 0, data.users, 1000); - const countAll = document.getElementById('countAll'); - if (countAll) countAll.textContent = data.posts; - } catch (e) { - console.error('Failed to load stats', e); - } -} - -async function loadPosts() { - const list = document.getElementById('postsList'); - if (!list) return; - - list.innerHTML = '

正在连接节点...

'; - - try { - const params = new URLSearchParams({ action: 'posts', page: currentPage }); - if (currentCategory) params.append('category_id', currentCategory); - - const res = await fetch(`${API}?${params}`); - const data = await res.json(); - - list.innerHTML = ''; - - if (data.posts.length === 0) { - list.innerHTML = ` -
-
📭
-

暂无帖子

-

成为第一个发帖的人吧!

-
- `; - return; - } - - data.posts.forEach((post, index) => { - const card = document.createElement('div'); - card.className = 'post-card'; - card.dataset.postId = post.id; - card.style.animationDelay = `${index * 0.1}s`; - - card.innerHTML = ` -
-
${post.username[0].toUpperCase()}
-
-
${escapeHtml(post.username)}
-
${timeAgo(post.created_at)}
-
-
- ${post.category_name} - ${post.is_pinned ? ' 置顶' : ''} -
-
-
${escapeHtml(post.title)}
-
${escapeHtml(post.content.substring(0, 150))}...
-
- - - ${post.views} - - - - ${post.likes} - - - - ${post.reply_count || 0} - -
- `; - list.appendChild(card); - }); - - renderPagination(data.pages); - } catch (e) { - list.innerHTML = '

加载失败,请稍后重试

'; - } -} - -function renderPagination(pages) { - const container = document.getElementById('pagination'); - if (!container) return; - - container.innerHTML = ''; - if (pages <= 1) return; - - for (let i = 1; i <= pages; i++) { - const btn = document.createElement('button'); - btn.className = `page-btn ${i === currentPage ? 'active' : ''}`; - btn.textContent = i; - btn.onclick = () => { - currentPage = i; - loadPosts(); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }; - container.appendChild(btn); - } -} - -// --- Utilities --- - -function animateValue(id, start, end, duration) { - const obj = document.getElementById(id); - if (!obj) return; - let startTimestamp = null; - const step = (timestamp) => { - if (!startTimestamp) startTimestamp = timestamp; - const progress = Math.min((timestamp - startTimestamp) / duration, 1); - obj.textContent = Math.floor(progress * (end - start) + start); - if (progress < 1) { - window.requestAnimationFrame(step); - } - }; - window.requestAnimationFrame(step); -} - -function timeAgo(dateStr) { - const now = new Date(); - const date = new Date(dateStr); - const seconds = Math.floor((now - date) / 1000); - const intervals = [ - [31536000, '年'], [2592000, '个月'], [604800, '周'], - [86400, '天'], [3600, '小时'], [60, '分钟'] - ]; - for (const [secondsCount, label] of intervals) { - const count = Math.floor(seconds / secondsCount); - if (count >= 1) return `${count}${label}前`; - } - return '刚刚'; -} - -function escapeHtml(str) { - if (!str) return ''; - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; -} - -function getIconSvg(name) { - const icons = { - megaphone: '', - question: '', - chat: '', - puzzle: '', - bug: '' - }; - return icons[name] || icons['chat']; -} - -// showCreateModal 函数已移至 post-modal.php 中定义 -// 此处仅保留兼容性封装 -window.showCreateModal = window.showCreateModal || function() { - if (typeof window.showCreatePostModal === 'function') { - window.showCreatePostModal(); - } else { - console.warn('发帖模态框未加载'); - } -}; - -// Auto-init if not loaded via SPA (Hard refresh case) -document.addEventListener('DOMContentLoaded', () => { - if (typeof AppRouter === 'undefined') { - window.initCommunity(); - } -}); \ No newline at end of file diff --git a/website/community/assets/js/dock-popover.js b/website/community/assets/js/dock-popover.js deleted file mode 100644 index 4d4940e..0000000 --- a/website/community/assets/js/dock-popover.js +++ /dev/null @@ -1,91 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const trigger = document.getElementById('dockUserMenuBtn'); - const popover = document.getElementById('dockUserMenu'); - const logoutBtn = document.getElementById('logoutBtn'); - const deleteBtn = document.getElementById('deleteAccountBtn'); - const myPostCount = document.getElementById('myPostCount'); - - // 获取用户文章数量 - async function fetchMyPostCount() { - if (!myPostCount) return; - try { - const res = await fetch('api/auth.php?action=my-post-count'); - if (res.ok) { - const data = await res.json(); - if (data.success) { - myPostCount.textContent = data.count; - } - } - } catch (err) { - console.error('Failed to fetch post count:', err); - } - } - - // 页面加载时获取文章数量 - fetchMyPostCount(); - - if (trigger && popover) { - // 点击图标切换面板 - trigger.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - - const isActive = popover.classList.contains('active'); - if (isActive) { - popover.classList.remove('active'); - } else { - // 计算位置:在图标右侧 - const rect = trigger.getBoundingClientRect(); - // Dock 通常在左侧,我们定位在图标右边,稍微向上偏移一点居中 - popover.style.left = `${rect.right + 16}px`; - popover.style.top = `${rect.top + 10}px`; - popover.classList.add('active'); - } - }); - - // 点击外部关闭 - document.addEventListener('click', (e) => { - if (!popover.contains(e.target) && !trigger.contains(e.target)) { - popover.classList.remove('active'); - } - }); - - // 退出登录逻辑 - if (logoutBtn) { - logoutBtn.addEventListener('click', async () => { - try { - const originalText = logoutBtn.innerHTML; - logoutBtn.innerHTML = ' 退出中...'; - logoutBtn.disabled = true; - - // 使用相对路径,因为 JS 可能在子目录中运行 - // 注意:这里假设 api/auth.php 相对于当前页面路径可用 - // 在 community/index.php 中,api/ 是同级目录 - const res = await fetch('api/auth.php?action=logout'); - - if (res.ok) { - // 退出成功,刷新页面 - window.location.reload(); - } else { - logoutBtn.innerHTML = originalText; - logoutBtn.disabled = false; - alert('退出失败,请重试'); - } - } catch (err) { - console.error('Logout error:', err); - logoutBtn.innerHTML = '退出失败'; - logoutBtn.disabled = false; - } - }); - } - - // 注销账户逻辑 - if (deleteBtn) { - deleteBtn.addEventListener('click', () => { - if (confirm('确定要注销(永久删除)此账户吗?\n此操作不可撤销,所有数据将被清除。')) { - alert('该功能暂未开放,请联系管理员。'); - } - }); - } - } -}); diff --git a/website/community/assets/js/editor.js b/website/community/assets/js/editor.js deleted file mode 100644 index 3bc90d3..0000000 --- a/website/community/assets/js/editor.js +++ /dev/null @@ -1,252 +0,0 @@ -/** - * OSS Community Editor JS - * Markdown 编辑器、实时预览、表单提交 - */ - -document.addEventListener('DOMContentLoaded', () => { - initEditor(); - initToolbar(); - initTags(); - initForm(); -}); - -// 初始化编辑器 -function initEditor() { - const textarea = document.getElementById('postContent'); - const titleInput = document.getElementById('postTitle'); - - // Tab 键支持 - if (textarea) { - textarea.addEventListener('keydown', (e) => { - if (e.key === 'Tab') { - e.preventDefault(); - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end); - textarea.selectionStart = textarea.selectionEnd = start + 2; - } - }); - } -} - -// 初始化工具栏 -function initToolbar() { - const buttons = document.querySelectorAll('.md-btn'); - const textarea = document.getElementById('postContent'); - - if (!textarea) return; - - buttons.forEach(btn => { - btn.addEventListener('click', () => { - const action = btn.dataset.md; - insertMarkdown(textarea, action); - }); - }); -} - -function insertMarkdown(textarea, action) { - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - const selected = textarea.value.substring(start, end); - let insertion = ''; - - switch (action) { - case 'bold': - insertion = `**${selected || '粗体文本'}**`; - break; - case 'italic': - insertion = `*${selected || '斜体文本'}*`; - break; - case 'heading': - insertion = `\n## ${selected || '标题'}\n`; - break; - case 'quote': - insertion = `\n> ${selected || '引用文本'}\n`; - break; - case 'code': - insertion = selected.includes('\n') ? `\n\`\`\`\n${selected || '代码块'}\n\`\`\`\n` : `\`${selected || '行内代码'}\``; - break; - case 'link': - insertion = `[${selected || '链接文本'}](url)`; - break; - case 'list': - insertion = `\n- ${selected || '列表项'}\n`; - break; - } - - textarea.value = textarea.value.substring(0, start) + insertion + textarea.value.substring(end); - textarea.focus(); - textarea.selectionStart = textarea.selectionEnd = start + insertion.length; -} - -// 初始化标签 -function initTags() { - const tagInput = document.getElementById('tagInput'); - const tagsContainer = document.getElementById('tagsContainer'); - - if (!tagInput || !tagsContainer) return; - - tagInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const tagName = tagInput.value.trim(); - if (tagName) { - addTag(tagName); - tagInput.value = ''; - } - } - }); -} - -function addTag(name) { - const tagsContainer = document.getElementById('tagsContainer'); - if (!tagsContainer) return; - - // 检查是否已存在 - const existing = tagsContainer.querySelectorAll('.tag-item'); - for (const tag of existing) { - if (tag.textContent.trim().replace('×', '').trim() === name) { - return; - } - } - - const tagEl = document.createElement('span'); - tagEl.className = 'tag-item'; - tagEl.innerHTML = ` - ${name} - - `; - tagsContainer.appendChild(tagEl); -} - -// 初始化表单 -function initForm() { - const form = document.getElementById('postEditorForm'); - const saveBtn = document.getElementById('savePostBtn'); - - if (!form || !saveBtn) return; - - saveBtn.addEventListener('click', async (e) => { - e.preventDefault(); - - const postId = document.getElementById('editPostId').value; - const title = document.getElementById('postTitle').value.trim(); - const content = document.getElementById('postContent').value.trim(); - const categoryId = document.getElementById('postCategory').value; - - // 收集标签 - const tags = []; - document.querySelectorAll('#tagsContainer .tag-item').forEach(tag => { - const name = tag.textContent.replace('×', '').trim(); - if (name) tags.push(name); - }); - - // 验证 - if (!title) { - showError('请输入帖子标题'); - return; - } - if (title.length < 5) { - showError('标题至少 5 个字符'); - return; - } - if (!content) { - showError('请输入帖子内容'); - return; - } - if (content.length < 10) { - showError('内容至少 10 个字符'); - return; - } - if (!categoryId) { - showError('请选择分类'); - return; - } - - // 提交 - setSaveButtonLoading(true); - - const formData = { - title: title, - content: content, - category_id: categoryId, - tags: tags - }; - - if (postId) { - formData.id = parseInt(postId); - } - - const action = postId ? 'update' : 'create'; - const url = `api/posts.php?action=${action}`; - - try { - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(formData) - }); - - const result = await response.json(); - - if (result.success) { - showSuccess(postId ? '更新成功!' : '发布成功!正在跳转...'); - setTimeout(() => { - window.location.href = `post.php?id=${result.post_id || postId}`; - }, 1000); - } else { - showError(result.message || '操作失败'); - } - } catch (error) { - showError('网络错误,请稍后重试'); - } finally { - setSaveButtonLoading(false); - } - }); -} - -function showSuccess(message) { - const toast = document.getElementById('successToast'); - const msgEl = document.getElementById('successMessage'); - if (toast && msgEl) { - msgEl.textContent = message; - toast.style.display = 'flex'; - setTimeout(() => { toast.style.display = 'none'; }, 3000); - } -} - -function showError(message) { - const toast = document.getElementById('errorToast'); - const msgEl = document.getElementById('errorMessage'); - if (toast && msgEl) { - msgEl.textContent = message; - toast.style.display = 'flex'; - setTimeout(() => { toast.style.display = 'none'; }, 4000); - } -} - -function setSaveButtonLoading(loading) { - const btn = document.getElementById('savePostBtn'); - if (!btn) return; - - if (loading) { - btn.disabled = true; - btn.style.opacity = '0.6'; - btn.innerHTML = ` - - - - - 处理中... - `; - } else { - btn.disabled = false; - btn.style.opacity = '1'; - btn.innerHTML = ` - - - - ${document.getElementById('editPostId').value ? '保存修改' : '发布帖子'} - `; - } -} diff --git a/website/community/assets/js/polling-system.js b/website/community/assets/js/polling-system.js deleted file mode 100644 index 2f93251..0000000 --- a/website/community/assets/js/polling-system.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * OSS Community - 实时轮询系统 - * 每2秒从数据库刷新所有数据 - */ - -class CommunityPollingSystem { - constructor() { - this.interval = 2000; - this.timer = null; - this.isRunning = false; - this.listeners = { user: [], posts: [], stats: [], categories: [] }; - this.cache = { user: null, posts: null, stats: null, categories: null }; - } - - start() { - if (this.isRunning) return; - this.isRunning = true; - this._fetchAll(); - this.timer = setInterval(() => this._fetchAll(), this.interval); - } - - stop() { - clearInterval(this.timer); - this.isRunning = false; - this.timer = null; - } - - on(event, callback) { - if (this.listeners[event]) this.listeners[event].push(callback); - } - - async _fetch(url) { - try { - const res = await fetch(url); - return res.ok ? await res.json() : null; - } catch { return null; } - } - - async _fetchAll() { - // 并行请求所有接口 - const [userData, postsData, statsData, catsData] = await Promise.all([ - this._fetch('api/auth.php?action=current-user'), - this._fetch('api/index.php?action=posts'), - this._fetch('api/index.php?action=stats'), - this._fetch('api/index.php?action=categories') - ]); - - // 用户数据 - if (userData && userData.success) { - const changed = JSON.stringify(userData) !== JSON.stringify(this.cache.user); - this.cache.user = userData; - if (changed) this._notify('user', userData); - } - - // 帖子数据(含 views, likes, replies) - if (postsData && postsData.posts) { - const changed = JSON.stringify(postsData.posts) !== JSON.stringify(this.cache.posts); - this.cache.posts = postsData.posts; - if (changed) this._notify('posts', postsData); - } - - // 统计数据(含 hot_posts) - if (statsData) { - const changed = JSON.stringify(statsData) !== JSON.stringify(this.cache.stats); - this.cache.stats = statsData; - if (changed) this._notify('stats', statsData); - } - - // 分类 - if (catsData && catsData.categories) { - const changed = JSON.stringify(catsData.categories) !== JSON.stringify(this.cache.categories); - this.cache.categories = catsData.categories; - if (changed) this._notify('categories', catsData); - } - } - - _notify(event, data) { - (this.listeners[event] || []).forEach(fn => { - try { fn(data); } catch(e) {} - }); - } -} - -window.Polling = new CommunityPollingSystem(); - -document.addEventListener('DOMContentLoaded', () => { - const dock = document.getElementById('dock'); - if (dock && dock.dataset.loggedIn === '1') { - window.Polling.start(); - } -}); diff --git a/website/community/assets/js/post-drawer.js b/website/community/assets/js/post-drawer.js deleted file mode 100644 index 3e181f6..0000000 --- a/website/community/assets/js/post-drawer.js +++ /dev/null @@ -1,236 +0,0 @@ -/** - * OSS Community - 文章抽屉交互逻辑 - */ -document.addEventListener('DOMContentLoaded', () => { - initPostDrawer(); -}); - -let drawerInitialized = false; -let currentDrawerPostId = null; -let drawerPollTimer = null; - -function initPostDrawer() { - if (drawerInitialized) return; - drawerInitialized = true; - - createDrawerElements(); - - // 全局点击事件委托 - document.addEventListener('click', (e) => { - const postCard = e.target.closest('.post-card'); - if (postCard && postCard.dataset.postId) { - e.preventDefault(); - e.stopPropagation(); - openPostDrawer(postCard.dataset.postId); - } - }); - - const overlay = document.getElementById('postDrawerOverlay'); - if (overlay) overlay.addEventListener('click', closePostDrawer); - - const closeBtn = document.getElementById('postDrawerClose'); - if (closeBtn) closeBtn.addEventListener('click', closePostDrawer); - - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') closePostDrawer(); - }); - - // 监听轮询数据更新抽屉内的 views/likes - if (window.Polling) { - window.Polling.on('posts', (data) => { - if (!data.posts || !currentDrawerPostId) return; - const post = data.posts.find(p => p.id == currentDrawerPostId); - if (post) updateDrawerStats(post); - }); - } -} - -function createDrawerElements() { - if (document.getElementById('postDrawerOverlay')) return; - - // 创建遮罩层 - const overlay = document.createElement('div'); - overlay.id = 'postDrawerOverlay'; - overlay.className = 'post-drawer-overlay'; - - // 创建抽屉 - const drawer = document.createElement('div'); - drawer.id = 'postDrawer'; - drawer.className = 'post-drawer'; - drawer.innerHTML = ` -
- -

-
-
-
-
-
-
- `; - - document.body.appendChild(overlay); - document.body.appendChild(drawer); - - // 绑定关闭按钮 - document.getElementById('postDrawerClose').addEventListener('click', closePostDrawer); -} - -async function openPostDrawer(postId) { - const drawer = document.getElementById('postDrawer'); - const overlay = document.getElementById('postDrawerOverlay'); - const titleEl = document.getElementById('postDrawerTitle'); - const metaEl = document.getElementById('postDrawerMeta'); - const contentEl = document.getElementById('postDrawerContent'); - const footerEl = document.getElementById('postDrawerFooter'); - - if (!drawer || !overlay) return; - - // 显示加载状态 - titleEl.textContent = '加载中...'; - metaEl.innerHTML = ''; - contentEl.innerHTML = '
正在加载文章内容...
'; - footerEl.innerHTML = ''; - - // 显示抽屉(先滑入,再加载内容) - overlay.classList.add('active'); - drawer.classList.add('active'); - document.body.style.overflow = 'hidden'; - - try { - const response = await fetch(`api/index.php?action=post&id=${postId}`); - if (!response.ok) throw new Error('加载失败'); - - const data = await response.json(); - if (!data.post) throw new Error('帖子不存在'); - - const post = data.post; - const replies = data.replies || []; - - currentDrawerPostId = post.id; - - // 更新标题和元信息 - titleEl.textContent = post.title; - metaEl.innerHTML = ` -
${post.username.charAt(0).toUpperCase()}
-
-
${escapeHtml(post.username)}
-
${formatDate(post.created_at)}
-
- ${escapeHtml(post.category_name)} - `; - - // 更新内容(使用 marked 解析 Markdown) - if (typeof marked !== 'undefined') { - contentEl.innerHTML = marked.parse(post.content); - } else { - contentEl.innerHTML = escapeHtml(post.content).replace(/\n/g, '
'); - } - - // 更新底部统计 - footerEl.innerHTML = ` -
- - ${post.views} 浏览 -
-
- - ${post.likes} 点赞 -
-
- - ${replies.length} 回复 -
- `; - - // 如果有回复,在内容下方显示 - if (replies.length > 0) { - contentEl.innerHTML += ` -
-

回复 (${replies.length})

- ${replies.map(reply => ` -
-
-
- ${reply.username.charAt(0).toUpperCase()} -
- ${escapeHtml(reply.username)} - ${formatDate(reply.created_at)} -
-
${escapeHtml(reply.content).replace(/\n/g, '
')}
-
- `).join('')} - `; - } - - } catch (error) { - console.error('Failed to load post:', error); - contentEl.innerHTML = ` -
- - - - - -

加载失败

-

${error.message || '未知错误,请稍后重试'}

-
- `; - } -} - -function closePostDrawer() { - const drawer = document.getElementById('postDrawer'); - const overlay = document.getElementById('postDrawerOverlay'); - - if (drawer) drawer.classList.remove('active'); - if (overlay) overlay.classList.remove('active'); - document.body.style.overflow = ''; - currentDrawerPostId = null; -} - -function updateDrawerStats(post) { - const footerEl = document.getElementById('postDrawerFooter'); - if (!footerEl || !currentDrawerPostId) return; - - footerEl.innerHTML = ` -
- - ${post.views} 浏览 -
-
- - ${post.likes} 点赞 -
-
- - ${post.reply_count || 0} 回复 -
- `; -} - -function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -function formatDate(dateString) { - const date = new Date(dateString); - const now = new Date(); - const diff = now - date; - const minutes = Math.floor(diff / 60000); - const hours = Math.floor(diff / 3600000); - const days = Math.floor(diff / 86400000); - - if (minutes < 1) return '刚刚'; - if (minutes < 60) return `${minutes} 分钟前`; - if (hours < 24) return `${hours} 小时前`; - if (days < 7) return `${days} 天前`; - return date.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }); -} diff --git a/website/community/assets/js/title-updater.js b/website/community/assets/js/title-updater.js deleted file mode 100644 index ab4e0ae..0000000 --- a/website/community/assets/js/title-updater.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * OSS Community - 轮询数据实时更新 - */ - -document.addEventListener('DOMContentLoaded', () => { - if (!window.Polling) return; - - // ========== 用户数据(称号、权限) ========== - let prevTitle = '', prevPerms = {}; - window.Polling.on('user', (data) => { - if (!data.success || !data.user) return; - const { title, role } = data.user; - - // 更新称号徽章 - if (title !== prevTitle) { - prevTitle = title; - const avatar = document.getElementById('dockUserAvatar'); - if (avatar) { - const name = avatar.dataset.tooltip.split(' - ')[0] || avatar.dataset.tooltip; - avatar.dataset.tooltip = title ? `${name} - ${title}` : name; - } - let badge = document.querySelector('.user-title-badge'); - if (title) { - if (!badge) { badge = document.createElement('span'); badge.className = 'user-title-badge'; if (avatar) avatar.appendChild(badge); } - badge.textContent = title; - } else if (badge) badge.remove(); - } - - // 权限控制 - if (data.permissions && JSON.stringify(data.permissions) !== JSON.stringify(prevPerms)) { - prevPerms = data.permissions; - document.querySelectorAll('.admin-only').forEach(el => el.style.display = prevPerms.can_manage_users ? '' : 'none'); - document.querySelectorAll('.moderator-only').forEach(el => el.style.display = prevPerms.can_manage_posts ? '' : 'none'); - } - }); - - // ========== 帖子数据实时更新 views/likes/replies ========== - window.Polling.on('posts', (data) => { - if (!data.posts || !Array.isArray(data.posts)) return; - - data.posts.forEach(post => { - // 更新帖子卡片上的浏览、点赞、回复数 - const cards = document.querySelectorAll(`.post-card[data-post-id="${post.id}"]`); - cards.forEach(card => { - const statsEls = card.querySelectorAll('.post-stats span'); - if (statsEls.length >= 3) { - // views - const viewsSvg = ''; - // likes - const likesSvg = ''; - // replies - const repliesSvg = ''; - - statsEls[0].innerHTML = viewsSvg + ` ${post.views}`; - statsEls[1].innerHTML = likesSvg + ` ${post.likes}`; - statsEls[2].innerHTML = repliesSvg + ` ${post.reply_count || 0}`; - } - }); - - // 更新个人主页帖子列表的 stats - const myListItems = document.querySelectorAll(`.my-post-item[data-post-id="${post.id}"] .my-post-meta`); - myListItems.forEach(meta => { - const spans = meta.querySelectorAll('span'); - if (spans.length >= 4) { - spans[1].textContent = `👁️ ${post.views} 浏览`; - spans[2].textContent = `❤️ ${post.likes} 点赞`; - spans[3].textContent = `💬 ${post.reply_count || 0} 回复`; - } - }); - }); - }); - - // ========== 统计数字实时更新 ========== - window.Polling.on('stats', (data) => { - if (data.posts !== undefined) animateNumber('statPosts', data.posts); - if (data.replies !== undefined) animateNumber('statReplies', data.replies); - if (data.users !== undefined) animateNumber('statUsers', data.users); - const countAll = document.getElementById('countAll'); - if (countAll) countAll.textContent = data.posts || 0; - - // 更新热门帖子 views(如果有这个区域) - if (data.hot_posts && Array.isArray(data.hot_posts)) { - data.hot_posts.forEach(hp => { - const hotItems = document.querySelectorAll(`.hot-post-item[data-post-id="${hp.id}"]`); - hotItems.forEach(item => { - const viewsEl = item.querySelector('.hot-views'); - if (viewsEl) viewsEl.textContent = hp.views; - }); - }); - } - }); - - // ========== 分类更新 ========== - window.Polling.on('categories', (data) => { - if (!data.categories) return; - const countAll = document.getElementById('countAll'); - if (countAll) countAll.textContent = data.posts || data.categories.length; - }); -}); - -// 数字动画 -function animateNumber(id, target) { - const el = document.getElementById(id); - if (!el) return; - const current = parseInt(el.textContent) || 0; - if (current === target) return; - el.textContent = target; -} diff --git a/website/community/edit-profile.php b/website/community/edit-profile.php deleted file mode 100644 index db2df30..0000000 --- a/website/community/edit-profile.php +++ /dev/null @@ -1,388 +0,0 @@ -fetchOne( - "SELECT id, username, email, bio FROM users WHERE id = ?", - [$userId] -); - -$success = $_GET['success'] ?? ''; -$error = $_GET['error'] ?? ''; - -// 处理表单提交 -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $bio = trim($_POST['bio'] ?? ''); - - try { - $db->query( - "UPDATE users SET bio = ? WHERE id = ?", - [$bio, $userId] - ); - header('Location: edit-profile.php?success=1'); - exit; - } catch (Exception $e) { - $error = '保存失败:' . $e->getMessage(); - } -} -?> - - - - - - 编辑资料 - OSS Community - - - - - - - - - - - - -
-
-

编辑个人资料

-

更新您的个人简介和公开信息

-
- - -
- ✓ 资料已成功更新 -
- - - -
- ✗ -
- - -
-
- - -
- -
- - -
- -
- - -
- -
- - 取消 - - -
-
- -
-

预览

-
-
-
- -
-
-
-
-
-
-
- 暂无简介' ?> -
-
-
-
- - - - - - diff --git a/website/community/editor.php b/website/community/editor.php deleted file mode 100644 index 0518e4f..0000000 --- a/website/community/editor.php +++ /dev/null @@ -1,156 +0,0 @@ - 0; -$postData = null; - -if ($isEdit) { - $db = Database::getInstance(); - $post = $db->fetchOne("SELECT * FROM posts WHERE id = ? AND user_id = ?", [$postId, $_SESSION['user_id']]); - if (!$post) { http_response_code(404); die('帖子不存在或无权编辑'); } - $postData = $post; -} - -$db = Database::getInstance(); -$categories = $db->fetchAll("SELECT * FROM categories ORDER BY sort_order ASC"); -$allTags = $db->fetchAll("SELECT * FROM tags ORDER BY name ASC"); -$postTags = []; -if ($postData) { - $postTags = $db->fetchAll("SELECT t.* FROM tags t JOIN post_tags pt ON t.id = pt.tag_id WHERE pt.post_id = ?", [$postId]); -} -?> - - - - - - <?php echo $isEdit ? '编辑帖子' : '发布新帖'; ?> - OSS Community - - - - - - - - - - - -
-
- -
-
- - - 返回 - -

-
-
- -
-
- -
- - - -
- -
- - -
- -
-
- Markdown 编辑器 -
- - - - - - - -
-
- -
- - -
-
-
分类
- -
- -
-
标签
- -
- - - - - - -
-
- - - -
-
- -
-
Markdown 语法
-
-
# 标题 一级标题
-
## 标题 二级标题
-
**粗体** 粗体文字
-
*斜体* 斜体文字
-
`代码` 行内代码
-
> 引用 引用块
-
- 列表 无序列表
-
[链接](url) 超链接
-
-
-
-
-
-
-
- -
- - -
-
- - -
- - - - - diff --git a/website/community/includes/Database.php b/website/community/includes/Database.php deleted file mode 100644 index 2c79c24..0000000 --- a/website/community/includes/Database.php +++ /dev/null @@ -1,47 +0,0 @@ -pdo = new PDO($dsn, $config['username'], $config['password'], [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, - ]); - } - - public static function getInstance() { - if (self::$instance === null) { - self::$instance = new self(); - } - return self::$instance; - } - - public function query($sql, $params = []) { - $stmt = $this->pdo->prepare($sql); - $stmt->execute($params); - return $stmt; - } - - public function fetchAll($sql, $params = []) { - return $this->query($sql, $params)->fetchAll(); - } - - public function fetchOne($sql, $params = []) { - return $this->query($sql, $params)->fetch(); - } - - public function lastInsertId() { - return $this->pdo->lastInsertId(); - } -} diff --git a/website/community/includes/dock.php b/website/community/includes/dock.php deleted file mode 100644 index 16acbd3..0000000 --- a/website/community/includes/dock.php +++ /dev/null @@ -1,75 +0,0 @@ -fetchOne("SELECT role, title FROM users WHERE id = ?", [$_SESSION['user_id']]); - if ($userData) { - $role = isset($userData['role']) ? ucfirst($userData['role']) : 'Member'; - $userTitle = $userData['title'] ?? ''; - // 更新 session 中的 role - $_SESSION['role'] = $userData['role'] ?? 'member'; - } - } catch (Exception $e) { - // 忽略错误,继续执行 - } -} - -// 构建 tooltip 文本 -$tooltipText = $userTitle ? "{$username} - {$userTitle}" : $username; -?> -
- - - - - - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - -
diff --git a/website/community/includes/post-modal.php b/website/community/includes/post-modal.php deleted file mode 100644 index 6ebfca1..0000000 --- a/website/community/includes/post-modal.php +++ /dev/null @@ -1,478 +0,0 @@ - -
-
-
-
-

发布新帖

- -
- -
- - -
- - - 0/200 -
- -
- - -
- -
- - - 0 个字符 -
- -
- - - 例如:Go, 插件, 安装 -
- -
-
- -
- - -
-
-
-
- - - - diff --git a/website/community/index.php b/website/community/index.php deleted file mode 100644 index 18eed3f..0000000 --- a/website/community/index.php +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - OSS Community - 开发者社区 - - - - - - - - - - - - - - - - - - - - -
-
- -
-
-

社区

-
-
-

分类

-
    -
  • - - 全部 - -
  • -
-
- -
-

统计

-
-
- 0 - 帖子 -
-
- 0 - 回复 -
-
- 0 - 用户 -
-
-
-
- - -
-
-

全部帖子

-
- - - -
-
- -
- -
- -
-
-
-
- - - - - - - - - diff --git a/website/community/install.sh b/website/community/install.sh deleted file mode 100644 index 93af858..0000000 --- a/website/community/install.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -# OSS Community 安装脚本 - -set -e - -echo "========================================" -echo " OSS Community 安装向导" -echo "========================================" -echo "" - -# 1. 检测 PHP -echo -ne "[1/4] 检测 PHP 环境..." -if command -v php &> /dev/null; then - echo -e " \033[0;32m已安装 ($(php -v | head -n1 | awk '{print $2}'))\033[0m" -else - echo -e " \033[1;33m未安装 PHP\033[0m" - echo "请运行: sudo apt install php php-mysql php-pdo php-json" - exit 1 -fi - -# 2. 检测 MySQL -echo -ne "[2/4] 检测 MySQL 环境..." -if command -v mysql &> /dev/null; then - echo -e " \033[0;32m已安装\033[0m" -else - echo -e " \033[1;33m未安装 MySQL\033[0m" - echo "请运行: sudo apt install mysql-server" - exit 1 -fi - -# 3. 数据库配置 -echo "" -echo "请输入 MySQL 配置:" -read -p " 数据库主机 [127.0.0.1]: " DB_HOST -DB_HOST=${DB_HOST:-127.0.0.1} -read -p " 数据库端口 [3306]: " DB_PORT -DB_PORT=${DB_PORT:-3306} -read -p " 数据库用户名 [root]: " DB_USER -DB_USER=${DB_USER:-root} -read -sp " 数据库密码: " DB_PASS -echo "" -read -p " 数据库名 [oss_community]: " DB_NAME -DB_NAME=${DB_NAME:-oss_community} - -# 写入配置 -cat > config.php << EOF - '$DB_HOST', - 'port' => '$DB_PORT', - 'dbname' => '$DB_NAME', - 'username' => '$DB_USER', - 'password' => '$DB_PASS', - 'charset' => 'utf8mb4', -]; -EOF - -# 4. 导入数据库 -echo -ne "[3/4] 导入数据库结构..." -if mysql -u "$DB_USER" -p"$DB_PASS" -h "$DB_HOST" -P "$DB_PORT" < schema.sql 2>/dev/null; then - echo -e " \033[0;32m导入成功\033[0m" -else - echo -e " \033[0;31m导入失败,请检查 MySQL 连接信息\033[0m" - exit 1 -fi - -# 5. 启动 PHP 内置服务器 -echo -ne "[4/4] 启动社区服务器..." -echo -e " \033[0;32m完成\033[0m" - -echo "" -echo "========================================" -echo " 安装完成!" -echo "========================================" -echo "" -echo "访问 http://localhost:8081/community/ 查看社区" -echo "" -echo "启动命令: php -S localhost:8081 -t ../" -echo "" diff --git a/website/community/login.php b/website/community/login.php deleted file mode 100644 index ca10518..0000000 --- a/website/community/login.php +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - 登录 - OSS Community - - - - - - - - - - - -
-
-
-
-
- - - -
-

欢迎回来

-

登录到你的 OSS Community 账户

-
- -
-
- -
- - - - -
-
- -
- -
- - - - - -
-
- -
- - 忘记密码? -
- -
-
- - -
- -
-

还没有账户?立即注册

-
-
- -
-
-
-
-
-
-
- - - - - diff --git a/website/community/migrate-add-bio.php b/website/community/migrate-add-bio.php deleted file mode 100644 index aad0f7d..0000000 --- a/website/community/migrate-add-bio.php +++ /dev/null @@ -1,29 +0,0 @@ -fetchAll("SHOW COLUMNS FROM users LIKE 'bio'"); - - if (empty($columns)) { - // 字段不存在,添加 - $db->query("ALTER TABLE users ADD COLUMN bio TEXT AFTER avatar"); - echo "✓ 成功添加 bio 字段\n"; - } else { - echo "✓ bio 字段已存在,无需迁移\n"; - } - - echo "迁移完成!\n"; -} catch (Exception $e) { - echo "✗ 迁移失败:" . $e->getMessage() . "\n"; - exit(1); -} diff --git a/website/community/my-posts.php b/website/community/my-posts.php deleted file mode 100644 index 4d79188..0000000 --- a/website/community/my-posts.php +++ /dev/null @@ -1,498 +0,0 @@ -fetchOne("SELECT id, username FROM users WHERE id = ?", [$viewUserId]); - -if (!$user) { - header('HTTP/1.0 404 Not Found'); - exit('用户不存在'); -} - -$isCurrentUser = ($currentUserId == $viewUserId); - -// 获取用户文章 -$page = max(1, (int)($_GET['page'] ?? 1)); -$limit = 20; -$offset = ($page - 1) * $limit; - -$posts = $db->fetchAll( - "SELECT p.*, c.name as category_name, c.slug as category_slug, - (SELECT COUNT(*) FROM replies r WHERE r.post_id = p.id) as reply_count - FROM posts p - JOIN categories c ON p.category_id = c.id - WHERE p.user_id = ? - ORDER BY p.is_pinned DESC, p.created_at DESC - LIMIT ? OFFSET ?", - [$viewUserId, $limit, $offset] -); - -$total = $db->fetchOne("SELECT COUNT(*) as count FROM posts WHERE user_id = ?", [$viewUserId])['count']; -$pages = ceil($total / $limit); -?> - - - - - - <?= htmlspecialchars($user['username']) ?> 的文章 - OSS Community - - - - - - - - - - - - - -
-
-

-

-
- -
-
-
-
文章总数
-
-
-
-
总浏览量
-
-
-
-
总点赞数
-
-
- -
- -
- - - -

还没有发表文章

-

开始创作您的第一篇文章吧!

- - - - - 发表文章 - -
- - $post): ?> -
-
- -
- - 📌 置顶 - - - ✓ 已解决 - - -
-
-
...
-
- 📅 - 👁️ 浏览 - ❤️ 点赞 - 💬 回复 -
-
- - - - - - 编辑 - - - -
-
- - - 1): ?> -
- - - - - -
- - -
-
- - - - - - diff --git a/website/community/post.php b/website/community/post.php deleted file mode 100644 index e75090d..0000000 --- a/website/community/post.php +++ /dev/null @@ -1,117 +0,0 @@ -fetchOne( - "SELECT p.*, u.username, u.avatar, u.role, c.name as category_name - FROM posts p JOIN users u ON p.user_id = u.id - JOIN categories c ON p.category_id = c.id WHERE p.id = ?", - [$id] -); - -if (!$post) { header('HTTP/1.0 404 Not Found'); exit('帖子不存在'); } - -$db->query("UPDATE posts SET views = views + 1 WHERE id = ?", [$id]); -$replies = $db->fetchAll( - "SELECT r.*, u.username, u.avatar FROM replies r JOIN users u ON r.user_id = u.id WHERE r.post_id = ? ORDER BY r.created_at ASC", - [$id] -); -?> - - - - - - <?= htmlspecialchars($post['title']) ?> - OSS Community - - - - - - - - - - - - - - - -
- - - 返回列表 - -
- -
-
-
-

-
-
-
-
-
·
-
-
-
-
-
- 👁️ 浏览 - ❤️ 点赞 - 💬 回复 -
-
- -
-

回复 ()

- -
暂无回复,抢沙发吧!
- - -
-
-
-
-
-
-
-
-
-
- - -
-
- - - diff --git a/website/community/profile.php b/website/community/profile.php deleted file mode 100644 index 65be39d..0000000 --- a/website/community/profile.php +++ /dev/null @@ -1,717 +0,0 @@ -fetchOne( - "SELECT id, username, email, avatar, role, bio, created_at FROM users WHERE id = ?", - [$viewUserId] -); - -if (!$user) { - header('HTTP/1.0 404 Not Found'); - exit('用户不存在'); -} - -// 获取用户统计数据 -$stats = $db->fetchOne( - "SELECT - (SELECT COUNT(*) FROM posts WHERE user_id = ?) as post_count, - (SELECT COUNT(*) FROM replies WHERE user_id = ?) as reply_count, - (SELECT SUM(views) FROM posts WHERE user_id = ?) as total_views, - (SELECT SUM(likes) FROM posts WHERE user_id = ?) as total_likes", - [$viewUserId, $viewUserId, $viewUserId, $viewUserId] -); - -// 获取用户的文章(最近 10 篇) -$posts = $db->fetchAll( - "SELECT p.*, c.name as category_name, c.slug as category_slug, - (SELECT COUNT(*) FROM replies r WHERE r.post_id = p.id) as reply_count - FROM posts p - JOIN categories c ON p.category_id = c.id - WHERE p.user_id = ? - ORDER BY p.created_at DESC - LIMIT 10", - [$viewUserId] -); - -// 获取用户的最近回复 -$replies = $db->fetchAll( - "SELECT r.*, p.title as post_title, p.id as post_id - FROM replies r - JOIN posts p ON r.post_id = p.id - WHERE r.user_id = ? - ORDER BY r.created_at DESC - LIMIT 5", - [$viewUserId] -); - -$isCurrentUser = ($currentUserId == $viewUserId); -$isAdminOrMod = in_array($_SESSION['role'] ?? '', ['admin', 'moderator']); -?> - - - - - - <?= htmlspecialchars($user['username']) ?> - OSS Community - - - - - - - - - - - - -
- -
-
-
-
-
-

- - - - - - -
- -

- -
- - - 注册于 - - - - - - - -
- -
- - - 编辑资料 - -
- -
-
-
- - -
-
-
-
文章
-
-
-
-
回复
-
-
-
-
浏览
-
-
-
-
点赞
-
-
- - -
- -
-
-

- - 文章 -

- = 10): ?> - - 查看全部 - - - -
- -
- -

还没有发表文章

-
- -
- -
-
- - -
-
...
-
- 📅 - 👁️ - ❤️ - 💬 -
-
- -
- -
- - -
-
-

- - 回复 -

-
- -
- -

还没有回复

-
- -
- -
- Re: -
...
-
-
- -
- -
-
-
- - - - - - diff --git a/website/community/register.php b/website/community/register.php deleted file mode 100644 index daac221..0000000 --- a/website/community/register.php +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - 注册 - OSS Community - - - - - - - - - - - -
-
-
-
-
- - - -
-

创建账户

-

加入 OSS Community 开发者社区

-
- -
-
- -
- - - - -
- 只能包含字母、数字和下划线 -
- -
- -
- - - - -
-
- -
- -
- - - - - -
- 至少 6 个字符 -
- -
- -
- - - - -
-
- -
-
- - -
- -
-

已有账户?立即登录

-
-
- -
-
-
-
-
-
-
- - - - - diff --git a/website/community/schema.sql b/website/community/schema.sql deleted file mode 100644 index c2e5fab..0000000 --- a/website/community/schema.sql +++ /dev/null @@ -1,104 +0,0 @@ --- OSS Community 数据库结构 -CREATE DATABASE IF NOT EXISTS oss_community CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -USE oss_community; - --- 用户表 -CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(50) NOT NULL UNIQUE, - email VARCHAR(100) NOT NULL UNIQUE, - password_hash VARCHAR(255) NOT NULL, - avatar VARCHAR(255) DEFAULT '', - bio TEXT, - role ENUM('admin', 'moderator', 'member') DEFAULT 'member', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 分类表 -CREATE TABLE IF NOT EXISTS categories ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL, - slug VARCHAR(100) NOT NULL UNIQUE, - description TEXT, - icon VARCHAR(50) DEFAULT 'folder', - sort_order INT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 帖子表 -CREATE TABLE IF NOT EXISTS posts ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - category_id INT NOT NULL, - title VARCHAR(200) NOT NULL, - slug VARCHAR(200) NOT NULL UNIQUE, - content TEXT NOT NULL, - views INT DEFAULT 0, - likes INT DEFAULT 0, - is_pinned TINYINT(1) DEFAULT 0, - is_locked TINYINT(1) DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, - FULLTEXT(title, content) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 回复表 -CREATE TABLE IF NOT EXISTS replies ( - id INT AUTO_INCREMENT PRIMARY KEY, - post_id INT NOT NULL, - user_id INT NOT NULL, - content TEXT NOT NULL, - likes INT DEFAULT 0, - is_solution TINYINT(1) DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 点赞表 -CREATE TABLE IF NOT EXISTS likes ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - post_id INT DEFAULT NULL, - reply_id INT DEFAULT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY unique_like (user_id, post_id, reply_id), - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE SET NULL, - FOREIGN KEY (reply_id) REFERENCES replies(id) ON DELETE SET NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 标签表 -CREATE TABLE IF NOT EXISTS tags ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(50) NOT NULL UNIQUE, - slug VARCHAR(50) NOT NULL UNIQUE, - color VARCHAR(7) DEFAULT '#06b6d4' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 帖子标签关联表 -CREATE TABLE IF NOT EXISTS post_tags ( - post_id INT NOT NULL, - tag_id INT NOT NULL, - PRIMARY KEY (post_id, tag_id), - FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, - FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 初始数据 -INSERT IGNORE INTO categories (name, slug, description, icon, sort_order) VALUES -('公告', 'announcements', '官方公告和重要通知', 'megaphone', 1), -('问答', 'q-a', '提问与解答,互助交流', 'question', 2), -('讨论', 'discussions', '技术讨论与想法分享', 'chat', 3), -('插件市场', 'plugins', '插件展示与推荐', 'puzzle', 4), -('反馈', 'feedback', 'Bug 反馈与功能建议', 'bug', 5); - -INSERT IGNORE INTO tags (name, slug, color) VALUES -('Go', 'go', '#00add8'), -('插件', 'plugin', '#8b5cf6'), -('安装', 'install', '#22c55e'), -('配置', 'config', '#3b82f6'), -('求助', 'help', '#f59e0b'); diff --git a/website/community/seed-announcements.sql b/website/community/seed-announcements.sql deleted file mode 100644 index 605f895..0000000 --- a/website/community/seed-announcements.sql +++ /dev/null @@ -1,15 +0,0 @@ --- 插入公告数据 -INSERT IGNORE INTO posts (user_id, category_id, title, slug, content, is_pinned) VALUES -(1, - (SELECT id FROM categories WHERE slug = 'announcements' LIMIT 1), - '欢迎使用 OSS Community', - 'welcome-to-oss-community', - '# 欢迎使用 OSS 开发者社区!\n\n这是我们社区的第一篇公告。\n\n## 社区规则\n\n- 尊重他人,文明交流\n- 禁止发布违法不良信息\n- 鼓励分享技术经验\n- 提问前请先搜索已有帖子\n\n## 功能介绍\n\n- 📝 **发帖** - 分享你的技术经验\n- 💬 **回复** - 参与讨论,帮助他人\n- ❤️ **点赞** - 为优质内容点赞\n- 🏷️ **标签** - 使用标签分类帖子\n- 🔍 **搜索** - 快速找到感兴趣的内容\n\n## 快速开始\n\n1. 注册并登录你的账户\n2. 完善个人资料和简介\n3. 发表第一篇帖子\n4. 参与社区讨论\n\n> 如果你有任何建议或反馈,欢迎在反馈区发帖!\n\n祝你在社区玩得愉快! 🎉', - 1), - -(1, - (SELECT id FROM categories WHERE slug = 'announcements' LIMIT 1), - '社区功能更新日志', - 'community-changelog-v1', - '# 社区功能更新\n\n## v1.1.0 - 2026-04-04\n\n### 新增\n- ✨ 用户个人主页\n- ✨ 编辑个人资料(支持 Bio)\n- ✨ 我的文章页面\n- ✨ 文章统计(浏览、点赞、回复)\n\n### 优化\n- 🚀 响应式布局优化\n- 📱 移动端适配\n- 🎨 UI 样式改进\n\n### 修复\n- 🐛 数据库连接问题\n- 🐛 用户菜单交互问题\n\n---\n\n感谢大家的支持!', - 1); diff --git a/website/css/hero.css b/website/css/hero.css index dea99a8..e15003e 100644 --- a/website/css/hero.css +++ b/website/css/hero.css @@ -148,49 +148,69 @@ box-shadow: 0 0 24px rgba(59, 130, 246, 0.5); } -.code-window { - background: rgba(10, 15, 30, 0.9); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 16px; - padding: 0; - width: 100%; - max-width: 480px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); - overflow: hidden; -} - -.code-header { +/* 涟漪动画区域 */ +.ripple-container { + position: relative; + width: 320px; + height: 320px; display: flex; align-items: center; - gap: 8px; - padding: 16px 20px; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + justify-content: center; } -.code-dot { - width: 12px; - height: 12px; +.ripple { + position: absolute; border-radius: 50%; + border: 2px solid rgba(6, 182, 212, 0.3); + animation: ripple-expand 3s ease-out infinite; } -.code-dot-red { background: #ef4444; } -.code-dot-yellow { background: #eab308; } -.code-dot-green { background: #22c55e; } +.ripple-1 { animation-delay: 0s; } +.ripple-2 { animation-delay: 0.6s; } +.ripple-3 { animation-delay: 1.2s; } +.ripple-4 { animation-delay: 1.8s; } +.ripple-5 { animation-delay: 2.4s; } -.code-title { - margin-left: 12px; - font-size: 13px; - color: var(--text-muted); - font-family: 'JetBrains Mono', monospace; +@keyframes ripple-expand { + 0% { + width: 40px; + height: 40px; + opacity: 1; + border-width: 3px; + border-color: rgba(6, 182, 212, 0.6); + } + 50% { + opacity: 0.5; + border-width: 2px; + border-color: rgba(6, 182, 212, 0.3); + } + 100% { + width: 320px; + height: 320px; + opacity: 0; + border-width: 1px; + border-color: rgba(6, 182, 212, 0); + } } -.code-body { - padding: 20px 24px; - font-family: 'JetBrains Mono', monospace; - font-size: 14px; - line-height: 1.8; - color: var(--text-secondary); - overflow-x: auto; +.hero-logo { + position: relative; + z-index: 1; + width: 140px; + height: 140px; + filter: drop-shadow(0 0 30px rgba(6, 182, 212, 0.4)); + animation: logo-breathe 4s ease-in-out infinite; +} + +@keyframes logo-breathe { + 0%, 100% { + transform: scale(1); + filter: drop-shadow(0 0 30px rgba(6, 182, 212, 0.4)); + } + 50% { + transform: scale(1.05); + filter: drop-shadow(0 0 50px rgba(6, 182, 212, 0.6)); + } } /* 滚动指示器 */ @@ -252,8 +272,25 @@ } .hero-stats { max-width: 400px; } .stat { text-align: center; } + .ripple-container { + margin: 0 auto; + width: 280px; + height: 280px; + } + .hero-logo { + width: 120px; + height: 120px; + } } @media (max-width: 640px) { .hero-stats { grid-template-columns: 1fr; gap: 16px; } + .ripple-container { + width: 240px; + height: 240px; + } + .hero-logo { + width: 100px; + height: 100px; + } } diff --git a/website/css/page.css b/website/css/page.css index aae5af5..706c6d4 100644 --- a/website/css/page.css +++ b/website/css/page.css @@ -4,11 +4,83 @@ margin: 0 auto; padding: 100px 80px 80px 24px; min-height: 100vh; + perspective: 1000px; } .page-header { text-align: center; margin-bottom: 64px; + will-change: transform; + transition: transform 0.05s linear; +} + +/* 陀螺仪权限按钮 */ +#gyro-permission-btn { + position: fixed; + inset: 0; + z-index: 9999; +} + +.gyro-permission-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; +} + +.gyro-permission-card { + background: rgba(20, 30, 50, 0.95); + border: 1px solid rgba(6, 182, 212, 0.3); + border-radius: 20px; + padding: 40px 32px 32px; + text-align: center; + max-width: 320px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); +} + +.gyro-permission-card svg { + color: var(--cyan); + margin-bottom: 16px; +} + +.gyro-permission-card p { + font-size: 18px; + font-weight: 700; + margin-bottom: 8px; + color: #fff; +} + +.gyro-permission-card span { + font-size: 14px; + color: var(--text-secondary); + display: block; + margin-bottom: 24px; +} + +.gyro-permission-card button { + padding: 12px 32px; + border-radius: 12px; + border: none; + background: linear-gradient(135deg, var(--cyan), var(--blue)); + color: #fff; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.gyro-permission-card button:hover { + transform: scale(1.05); + box-shadow: 0 8px 24px rgba(6, 182, 212, 0.3); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } } .page-title { @@ -104,7 +176,7 @@ .pkg-explain { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(2, 1fr); gap: 24px; text-align: center; } diff --git a/website/css/plugins.css b/website/css/plugins.css index dd7f06f..2d92bdb 100644 --- a/website/css/plugins.css +++ b/website/css/plugins.css @@ -31,7 +31,7 @@ .pkg-explain { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(2, 1fr); gap: 24px; text-align: center; } diff --git a/website/features.html b/website/features.html index d0f6f53..8125f84 100644 --- a/website/features.html +++ b/website/features.html @@ -24,13 +24,13 @@
-
+
核心能力

为何选择 Future OSS

每个模块都为生产环境而设计,为企业提供开箱即用的稳定性保障

-
+
@@ -86,6 +86,7 @@ + diff --git a/website/index.html b/website/index.html index c46d024..2304a97 100644 --- a/website/index.html +++ b/website/index.html @@ -24,9 +24,9 @@
-
+
-
+
2026 · 插件驱动 · 一切皆可扩展 @@ -53,20 +53,13 @@
-
-
- - - - start.sh -
-
# 1. 克隆项目
-$ git clone https://gitee.com/starlight-apk/feature-oss.git
-$ cd feature-oss
-
-# 2. 一键启动
-$ bash start.sh
-# ✓ 服务已启动 → http://localhost:8080/
+
+
+
+
+
+
+ Future OSS Logo
@@ -76,6 +69,7 @@ + diff --git a/website/js/dock.js b/website/js/dock.js index 8aca052..96547b6 100644 --- a/website/js/dock.js +++ b/website/js/dock.js @@ -17,7 +17,6 @@ { separator: true }, { href: 'quickstart.html', tooltip: '快速开始', svg: 'M13 10V3L4 14h7v7l9-11h-7z' }, { separator: true }, - { href: 'community/', tooltip: '社区', svg: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z' }, { href: 'https://gitee.com/starlight-apk/feature-oss', tooltip: '源码', svg: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4', target: '_blank' } ]; diff --git a/website/js/mouse-tracker.js b/website/js/mouse-tracker.js new file mode 100644 index 0000000..8d1dc88 --- /dev/null +++ b/website/js/mouse-tracker.js @@ -0,0 +1,83 @@ +// 通用鼠标位置追踪器 +// 提供全局鼠标位置状态,供其他模块使用 + +class MouseTracker { + constructor() { + this.x = 0; // 原始 X (0 ~ window.innerWidth) + this.y = 0; // 原始 Y (0 ~ window.innerHeight) + this.normalizedX = 0; // 归一化 X (-1 ~ 1) + this.normalizedY = 0; // 归一化 Y (-1 ~ 1) + this.smoothX = 0; // 平滑后的 X + this.smoothY = 0; // 平滑后的 Y + this.listeners = []; + this.smoothing = 0.1; // 平滑系数 (0~1,越小越平滑) + this.isInside = false; + + this._init(); + } + + _init() { + document.addEventListener('mousemove', (e) => { + this.x = e.clientX; + this.y = e.clientY; + this.normalizedX = (e.clientX / window.innerWidth) * 2 - 1; + this.normalizedY = (e.clientY / window.innerHeight) * 2 - 1; + this.isInside = true; + + this._notify(); + }); + + document.addEventListener('mouseleave', () => { + this.isInside = false; + }); + + // 平滑动画循环 + this._animate(); + } + + _animate() { + // 平滑插值 + this.smoothX += (this.normalizedX - this.smoothX) * this.smoothing; + this.smoothY += (this.normalizedY - this.smoothY) * this.smoothing; + + requestAnimationFrame(() => this._animate()); + } + + _notify() { + this.listeners.forEach(cb => { + cb({ + x: this.x, + y: this.y, + normalizedX: this.normalizedX, + normalizedY: this.normalizedY, + smoothX: this.smoothX, + smoothY: this.smoothY, + isInside: this.isInside + }); + }); + } + + // 添加监听器 + onUpdate(callback) { + this.listeners.push(callback); + } + + // 移除监听器 + offUpdate(callback) { + const index = this.listeners.indexOf(callback); + if (index > -1) this.listeners.splice(index, 1); + } + + // 设置平滑系数 + setSmoothing(value) { + this.smoothing = Math.max(0, Math.min(1, value)); + } + + // 获取当前平滑值 + getSmooth() { + return { x: this.smoothX, y: this.smoothY }; + } +} + +// 导出全局实例 +window.mouseTracker = new MouseTracker(); diff --git a/website/js/parallax.js b/website/js/parallax.js new file mode 100644 index 0000000..2638c6f --- /dev/null +++ b/website/js/parallax.js @@ -0,0 +1,219 @@ +// 统一视差追踪器 +// 自动检测并使用最佳输入源:鼠标 > 陀螺仪 > 触摸滑动 + +class ParallaxTracker { + constructor() { + this.smoothX = 0; + this.smoothY = 0; + this.targetX = 0; + this.targetY = 0; + this.elements = []; + this.listeners = []; + this.maxMove = 100; // 最大移动像素 + this.smoothing = 0.08; // 平滑系数 + this.isReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + this.activeSource = null; // 'mouse' | 'gyro' | 'touch' + this.rafId = null; + + if (this.isReducedMotion) { + console.log('[Parallax] 用户偏好减少动画,已跳过'); + return; + } + + this._init(); + } + + _init() { + // 查找视差元素 + document.querySelectorAll('[data-parallax-speed]').forEach(el => { + const speed = parseFloat(el.dataset.parallaxSpeed) || 0.1; + this.elements.push({ el, speed }); + el.style.willChange = 'transform'; + }); + + if (this.elements.length === 0) return; + + const isMobile = this._isMobileDevice(); + + // 按优先级尝试输入源 + if (isMobile && this._initGyroscope()) { + this.activeSource = 'gyro'; + console.log('[Parallax] 使用陀螺仪输入'); + } else if (!isMobile && this._initMouse()) { + this.activeSource = 'mouse'; + console.log('[Parallax] 使用鼠标输入'); + } else if (isMobile) { + // 移动端回退方案:触摸滑动 + this._initTouchFallback(); + this.activeSource = 'touch'; + console.log('[Parallax] 使用触摸滑动输入'); + } else { + // 电脑端最后的回退:仍然尝试鼠标 + this._initMouse(); + this.activeSource = 'mouse'; + console.log('[Parallax] 使用鼠标输入(回退)'); + } + + // 启动动画循环 + this._animate(); + console.log(`[Parallax] 已初始化 ${this.elements.length} 个元素`); + } + + // 检测是否为移动设备 + _isMobileDevice() { + return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) + || (navigator.maxTouchPoints && navigator.maxTouchPoints > 2); + } + + // ── 鼠标输入 ── + _initMouse() { + // 支持鼠标和触摸指针的设备 + const hasFinePointer = window.matchMedia('(pointer: fine)').matches; + + document.addEventListener('mousemove', (e) => { + this.targetX = (e.clientX / window.innerWidth) * 2 - 1; + this.targetY = (e.clientY / window.innerHeight) * 2 - 1; + }); + + // 即使没有 fine pointer 也返回 true,让鼠标事件始终生效 + return true; + } + + // ── 陀螺仪输入 ── + _initGyroscope() { + if (!window.DeviceOrientationEvent) return false; + + let hasFired = false; + const handler = (e) => { + // gamma: 左右倾斜 (-90 ~ 90) + // beta: 前后倾斜 (-180 ~ 180) + if (e.gamma === null && e.beta === null) return; + + if (!hasFired) { + hasFired = true; + console.log('[Parallax] 陀螺仪数据已就绪'); + } + + // 归一化到 -1 ~ 1(限制在 ±45 度范围内) + this.targetX = Math.max(-1, Math.min(1, e.gamma / 45)); + this.targetY = Math.max(-1, Math.min(1, (e.beta - 45) / 45)); // 减去 45° 自然持握角度 + }; + + // iOS 13+ 需要用户授权 + if (typeof DeviceOrientationEvent.requestPermission === 'function') { + // 添加点击按钮请求权限的提示 + this._addGyroPermissionButton(handler); + return false; // 等待用户授权后再启用 + } + + window.addEventListener('deviceorientation', handler); + return true; + } + + _addGyroPermissionButton(handler) { + // iOS 设备需要用户交互才能请求陀螺仪权限 + const btn = document.createElement('div'); + btn.id = 'gyro-permission-btn'; + btn.innerHTML = ` +
+
+ + + +

开启陀螺仪视差

+ 移动设备时页面会跟随滚动 + +
+
+ `; + document.body.appendChild(btn); + + btn.querySelector('button').addEventListener('click', async () => { + try { + const permission = await DeviceOrientationEvent.requestPermission(); + if (permission === 'granted') { + window.addEventListener('deviceorientation', handler); + this.activeSource = 'gyro'; + console.log('[Parallax] 陀螺仪权限已授予'); + } + } catch (err) { + console.warn('[Parallax] 陀螺仪权限被拒绝', err); + } + btn.remove(); + }); + + // 点击遮罩关闭 + btn.querySelector('.gyro-permission-overlay').addEventListener('click', () => btn.remove()); + } + + // ── 触摸滑动输入 ── + _initTouchFallback() { + let startX = 0, startY = 0; + let currentX = 0, currentY = 0; + let isTouching = false; + + document.addEventListener('touchstart', (e) => { + isTouching = true; + startX = e.touches[0].clientX; + startY = e.touches[0].clientY; + currentX = startX; + currentY = startY; + }, { passive: true }); + + document.addEventListener('touchmove', (e) => { + if (!isTouching) return; + currentX = e.touches[0].clientX; + currentY = e.touches[0].clientY; + + // 计算相对于起始点的偏移 + const deltaX = (currentX - startX) / window.innerWidth; + const deltaY = (currentY - startY) / window.innerHeight; + + this.targetX = Math.max(-1, Math.min(1, deltaX * 4)); // 放大灵敏度 + this.targetY = Math.max(-1, Math.min(1, deltaY * 4)); + }, { passive: true }); + + document.addEventListener('touchend', () => { + isTouching = false; + // 缓慢回归中心 + const resetInterval = setInterval(() => { + this.targetX *= 0.9; + this.targetY *= 0.9; + if (Math.abs(this.targetX) < 0.01 && Math.abs(this.targetY) < 0.01) { + this.targetX = 0; + this.targetY = 0; + clearInterval(resetInterval); + } + }, 16); + }, { passive: true }); + } + + // ── 动画循环 ── + _animate() { + // 平滑插值(指数移动平均) + this.smoothX += (this.targetX - this.smoothX) * this.smoothing; + this.smoothY += (this.targetY - this.smoothY) * this.smoothing; + + // 应用变换 + this.elements.forEach(({ el, speed }) => { + const moveX = -this.smoothX * speed * this.maxMove; + const moveY = -this.smoothY * speed * this.maxMove; + el.style.transform = `translate3d(${moveX}px, ${moveY}px, 0)`; + }); + + requestAnimationFrame(() => this._animate()); + } + + // 添加监听器(供其他模块使用) + onUpdate(callback) { + this.listeners.push(callback); + } + + // 获取当前平滑值 + getSmooth() { + return { x: this.smoothX, y: this.smoothY }; + } +} + +// 导出全局实例 +window.parallaxTracker = new ParallaxTracker(); diff --git a/website/plugins.html b/website/plugins.html index a627047..e3167d1 100644 --- a/website/plugins.html +++ b/website/plugins.html @@ -24,35 +24,28 @@
-
+
插件生态

包名格式与安装流程

简洁且富有表现力的包名格式,让插件管理像呼吸一样自然

-
+
@{ - 作者 - / - 插件名 - }< - 版本 - > + 作者名称 + }/ + 插件名称
@{ }
-
大括号包裹作者和插件名
+
大括号包裹作者名称
/
-
分隔作者与插件名
-
-
-
< >
-
尖括号包裹语义化版本号
+
分隔作者与插件名称
@@ -67,20 +60,20 @@
$ - oss pkg install @{Falck/http-server}<1.0.0> + oss pkg install @{Falck}/http-server
-
✅ http-server@1.0.0 安装完成 (1.2s)
+
✅ @{Falck}/http-server 安装完成
$ oss pkg list
-
已安装 1 个包:
-
http-server@1.0.0 - HTTP 协议适配器
+
已安装插件:
+
@{Falck}/http-server - HTTP 协议适配器
$ oss pkg update
-
✅ 所有包已是最新版本
+
✅ 所有插件已更新至最新版本
@@ -91,12 +84,12 @@ 命令别名说明 - oss pkg install @{x/y}<v>i / add安装指定版本的插件包 - oss pkg remove @{x/y}rm / uninstall卸载已安装的插件 - oss pkg listls列出所有已安装的包 - oss pkg update [包名]—更新单个或所有包 - oss pkg sync—从远程仓库同步所有包 - oss pkg init—初始化 package.json + oss pkg install @{作者名称}/插件名称i / add安装指定作者的插件 + oss pkg remove @{作者名称}/插件名称rm / uninstall卸载指定的插件 + oss pkg listls列出所有已安装的插件 + oss pkg update [插件名]—更新单个或所有插件 + oss pkg sync—从远程仓库同步所有插件 + oss pkg search 关键词find搜索可用的插件 oss pkg clean—清理下载缓存 @@ -108,6 +101,7 @@ + diff --git a/website/quickstart.html b/website/quickstart.html index e74fc5c..cc8f907 100644 --- a/website/quickstart.html +++ b/website/quickstart.html @@ -24,13 +24,13 @@
-
+
快速开始

三步即可运行

从克隆代码到启动服务,只需几分钟

-
+
1

克隆代码

@@ -66,6 +66,7 @@ +