From d95b08eb8c85e43ee5174829a6f14dc9cb732990 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Wed, 8 Jul 2026 00:39:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E7=9F=AD=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=B9=B3=E5=8F=B0=E9=A1=B9=E7=9B=AE=20v1.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 20 + README.md | 138 +++++ app.js | 376 ++++++++++++ package.json | 16 + public/admin-login.html | 79 +++ public/admin-panel.html | 392 ++++++++++++ public/css/style.css | 1249 +++++++++++++++++++++++++++++++++++++++ public/index.html | 104 ++++ public/js/app.js | 93 +++ public/js/player.js | 424 +++++++++++++ public/video.html | 176 ++++++ start.sh | 34 ++ stop.sh | 26 + 13 files changed, 3127 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.js create mode 100644 package.json create mode 100644 public/admin-login.html create mode 100644 public/admin-panel.html create mode 100644 public/css/style.css create mode 100644 public/index.html create mode 100644 public/js/app.js create mode 100644 public/js/player.js create mode 100644 public/video.html create mode 100755 start.sh create mode 100755 stop.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..614d443 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# 依赖 +node_modules/ +package-lock.json + +# 日志 +logs/ +*.log + +# 运行时数据 +data/*.json + +# 系统文件 +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..be62f5d --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# 短视频展示平台 + +一个现代化的短视频展示平台,支持自适应布局、弹幕功能和后台管理。 + +## 功能特性 + +### 前端功能 +- ✅ 响应式设计,完美适配桌面端和移动端 +- ✅ 主流视频平台风格的UI设计 +- ✅ 首页视频推荐展示 +- ✅ 分类筛选(推荐、热门、萌宠、美食、旅行、科技、风景) +- ✅ 视频播放页面 +- ✅ 实时弹幕系统 +- ✅ 弹幕颜色选择 +- ✅ 点赞功能 +- WebSocket实时弹幕推送 + +### 后台管理 +- ✅ 管理员登录认证 +- ✅ 视频管理(增删改查) +- ✅ 数据统计面板 +- ✅ 视频列表管理 + +### 技术栈 +- **前端**: HTML5 + CSS3 + JavaScript (原生) +- **后端**: Node.js + Express +- **实时通信**: WebSocket +- **数据存储**: JSON文件(可轻松迁移至数据库) + +## 快速开始 + +### 安装依赖 +```bash +npm install +``` + +### 启动服务 +```bash +# 方式1: 使用启动脚本 +./start.sh + +# 方式2: 直接运行 +node app.js +``` + +### 访问地址 +- **前台首页**: http://localhost:16035 +- **后台管理**: http://localhost:16035/admin + +### 默认管理员账号 +- 用户名: `admin` +- 密码: `admin123` + +## 停止服务 +```bash +./stop.sh +``` + +## 项目结构 + +``` +video-platform/ +├── app.js # 主程序入口 +├── package.json # 项目配置 +├── start.sh # 启动脚本 +├── stop.sh # 停止脚本 +├── public/ # 静态文件 +│ ├── index.html # 首页 +│ ├── video.html # 视频播放页 +│ ├── admin-login.html # 后台登录页 +│ ├── admin-panel.html # 后台管理面板 +│ ├── css/ +│ │ └── style.css # 样式文件 +│ └── js/ +│ ├── app.js # 主逻辑 +│ └── player.js # 播放器和弹幕逻辑 +├── data/ # 数据存储 +│ ├── videos.json # 视频数据 +│ ├── users.json # 用户数据 +│ └── comments.json # 弹幕数据 +├── logs/ # 日志目录 +└── views/ # 视图模板(预留) +``` + +## API接口 + +### 前台API +- `GET /api/videos` - 获取视频列表 +- `GET /api/videos/:id` - 获取单个视频详情 +- `POST /api/videos/:id/like` - 点赞视频 +- `GET /api/videos/:id/danmaku` - 获取弹幕列表 +- `POST /api/videos/:id/danmaku` - 发送弹幕 + +### 后台API +- `POST /api/admin/login` - 管理员登录 +- `POST /api/admin/logout` - 管理员登出 +- `GET /api/admin/check` - 检查登录状态 +- `POST /api/admin/videos` - 添加视频 +- `PUT /api/admin/videos/:id` - 更新视频 +- `DELETE /api/admin/videos/:id` - 删除视频 + +## 弹幕系统 + +弹幕通过WebSocket实现实时推送: +- 支持自定义颜色 +- 随机垂直位置 +- 平滑滚动动画 +- 时间同步播放 + +## 自定义配置 + +### 修改端口 +在 `app.js` 中修改 `PORT` 常量: +```javascript +const PORT = 16035; +``` + +### 修改管理员密码 +首次运行后,编辑 `data/users.json` 文件。 + +## 浏览器兼容性 + +- Chrome 80+ +- Firefox 75+ +- Safari 13+ +- Edge 80+ + +## 注意事项 + +1. 生产环境建议使用反向代理(如 Nginx) +2. 建议迁移到数据库存储(如 MongoDB、MySQL) +3. 建议添加HTTPS支持 +4. 建议添加视频上传功能 +5. 建议添加用户注册登录系统 + +## License + +MIT \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..67410b4 --- /dev/null +++ b/app.js @@ -0,0 +1,376 @@ +const express = require('express'); +const session = require('express-session'); +const path = require('path'); +const fs = require('fs'); +const WebSocket = require('ws'); + +const app = express(); +const PORT = 16035; + +// 中间件配置 +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(express.static(path.join(__dirname, 'public'))); + +// Session配置 +app.use(session({ + secret: 'video-platform-secret-key-2024', + resave: false, + saveUninitialized: false, + cookie: { maxAge: 24 * 60 * 60 * 1000 } +})); + +// 数据存储路径 +const DATA_DIR = path.join(__dirname, 'data'); +const VIDEOS_FILE = path.join(DATA_DIR, 'videos.json'); +const USERS_FILE = path.join(DATA_DIR, 'users.json'); +const COMMENTS_FILE = path.join(DATA_DIR, 'comments.json'); + +// 确保数据目录和文件存在 +if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); +} + +// 初始化数据文件 +function initDataFile(filePath, defaultData) { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, JSON.stringify(defaultData, null, 2), 'utf8'); + } +} + +// 初始化默认数据 +initDataFile(VIDEOS_FILE, [ + { + id: 1, + title: "精彩短视频演示", + description: "这是一个示例视频,展示了平台的播放功能", + url: "https://www.w3schools.com/html/mov_bbb.mp4", + cover: "https://picsum.photos/400/300?random=1", + duration: 10, + views: 1520, + likes: 128, + category: "推荐", + author: "管理员", + createTime: "2024-01-15" + }, + { + id: 2, + title: "城市夜景延时摄影", + description: "美丽的城市夜景,灯光璀璨", + url: "https://www.w3schools.com/html/movie.mp4", + cover: "https://picsum.photos/400/300?random=2", + duration: 12, + views: 2340, + likes: 256, + category: "风景", + author: "摄影师小王", + createTime: "2024-01-14" + }, + { + id: 3, + title: "可爱猫咪日常", + description: "萌宠猫咪的有趣日常", + url: "https://www.w3schools.com/html/mov_bbb.mp4", + cover: "https://picsum.photos/400/300?random=3", + duration: 8, + views: 5670, + likes: 892, + category: "萌宠", + author: "铲屎官", + createTime: "2024-01-13" + }, + { + id: 4, + title: "美食制作教程", + description: "教你做出美味的家常菜", + url: "https://www.w3schools.com/html/movie.mp4", + cover: "https://picsum.photos/400/300?random=4", + duration: 15, + views: 3210, + likes: 445, + category: "美食", + author: "美食达人", + createTime: "2024-01-12" + }, + { + id: 5, + title: "科技产品开箱", + description: "最新科技产品开箱体验", + url: "https://www.w3schools.com/html/mov_bbb.mp4", + cover: "https://picsum.photos/400/300?random=5", + duration: 20, + views: 4560, + likes: 523, + category: "科技", + author: "数码博主", + createTime: "2024-01-11" + }, + { + id: 6, + title: "旅行vlog记录", + description: "美丽的旅行风景记录", + url: "https://www.w3schools.com/html/movie.mp4", + cover: "https://picsum.photos/400/300?random=6", + duration: 18, + views: 2890, + likes: 367, + category: "旅行", + author: "旅行家", + createTime: "2024-01-10" + } +]); + +initDataFile(USERS_FILE, { + admin: { password: 'admin123', role: 'admin' } +}); + +initDataFile(COMMENTS_FILE, {}); + +// 辅助函数 +function readJSON(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +function writeJSON(filePath, data) { + fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8'); +} + +// ============ 前端路由 ============ + +// 首页 +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +// 视频播放页 +app.get('/video/:id', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'video.html')); +}); + +// 后台管理登录页 +app.get('/admin', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'admin-login.html')); +}); + +// 后台管理面板 +app.get('/admin/panel', (req, res) => { + if (req.session.user && req.session.user.role === 'admin') { + res.sendFile(path.join(__dirname, 'public', 'admin-panel.html')); + } else { + res.redirect('/admin'); + } +}); + +// ============ API路由 ============ + +// 获取视频列表 +app.get('/api/videos', (req, res) => { + const videos = readJSON(VIDEOS_FILE) || []; + res.json({ success: true, data: videos }); +}); + +// 获取单个视频 +app.get('/api/videos/:id', (req, res) => { + const videos = readJSON(VIDEOS_FILE) || []; + const video = videos.find(v => v.id === parseInt(req.params.id)); + if (video) { + // 增加观看次数 + video.views = (video.views || 0) + 1; + writeJSON(VIDEOS_FILE, videos); + res.json({ success: true, data: video }); + } else { + res.status(404).json({ success: false, message: '视频不存在' }); + } +}); + +// 点赞视频 +app.post('/api/videos/:id/like', (req, res) => { + const videos = readJSON(VIDEOS_FILE) || []; + const video = videos.find(v => v.id === parseInt(req.params.id)); + if (video) { + video.likes = (video.likes || 0) + 1; + writeJSON(VIDEOS_FILE, videos); + res.json({ success: true, likes: video.likes }); + } else { + res.status(404).json({ success: false, message: '视频不存在' }); + } +}); + +// 获取弹幕 +app.get('/api/videos/:id/danmaku', (req, res) => { + const comments = readJSON(COMMENTS_FILE) || {}; + const danmaku = comments[req.params.id] || []; + res.json({ success: true, data: danmaku }); +}); + +// 发送弹幕 +app.post('/api/videos/:id/danmaku', (req, res) => { + const { time, text, color } = req.body; + if (!text || text.trim() === '') { + return res.status(400).json({ success: false, message: '弹幕内容不能为空' }); + } + + const comments = readJSON(COMMENTS_FILE) || {}; + if (!comments[req.params.id]) { + comments[req.params.id] = []; + } + + const danmaku = { + id: Date.now(), + time: parseFloat(time) || 0, + text: text.trim().substring(0, 100), + color: color || '#ffffff', + createTime: new Date().toISOString() + }; + + comments[req.params.id].push(danmaku); + writeJSON(COMMENTS_FILE, comments); + + // 广播弹幕 + broadcastDanmaku(req.params.id, danmaku); + + res.json({ success: true, data: danmaku }); +}); + +// 管理员登录 +app.post('/api/admin/login', (req, res) => { + const { username, password } = req.body; + const users = readJSON(USERS_FILE) || {}; + + if (users[username] && users[username].password === password) { + req.session.user = { + username, + role: users[username].role + }; + res.json({ success: true, message: '登录成功' }); + } else { + res.status(401).json({ success: false, message: '用户名或密码错误' }); + } +}); + +// 管理员登出 +app.post('/api/admin/logout', (req, res) => { + req.session.destroy(); + res.json({ success: true }); +}); + +// 检查登录状态 +app.get('/api/admin/check', (req, res) => { + if (req.session.user && req.session.user.role === 'admin') { + res.json({ success: true, user: req.session.user }); + } else { + res.status(401).json({ success: false }); + } +}); + +// 添加视频 +app.post('/api/admin/videos', (req, res) => { + if (!req.session.user || req.session.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '未授权' }); + } + + const videos = readJSON(VIDEOS_FILE) || []; + const newVideo = { + id: Date.now(), + ...req.body, + views: 0, + likes: 0, + createTime: new Date().toISOString().split('T')[0] + }; + + videos.push(newVideo); + writeJSON(VIDEOS_FILE, videos); + + res.json({ success: true, data: newVideo }); +}); + +// 更新视频 +app.put('/api/admin/videos/:id', (req, res) => { + if (!req.session.user || req.session.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '未授权' }); + } + + const videos = readJSON(VIDEOS_FILE) || []; + const index = videos.findIndex(v => v.id === parseInt(req.params.id)); + + if (index !== -1) { + videos[index] = { ...videos[index], ...req.body }; + writeJSON(VIDEOS_FILE, videos); + res.json({ success: true, data: videos[index] }); + } else { + res.status(404).json({ success: false, message: '视频不存在' }); + } +}); + +// 删除视频 +app.delete('/api/admin/videos/:id', (req, res) => { + if (!req.session.user || req.session.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '未授权' }); + } + + const videos = readJSON(VIDEOS_FILE) || []; + const index = videos.findIndex(v => v.id === parseInt(req.params.id)); + + if (index !== -1) { + videos.splice(index, 1); + writeJSON(VIDEOS_FILE, videos); + res.json({ success: true }); + } else { + res.status(404).json({ success: false, message: '视频不存在' }); + } +}); + +// ============ WebSocket 弹幕实时推送 ============ +const server = app.listen(PORT, () => { + console.log(`短视频平台已启动: http://localhost:${PORT}`); + console.log(`后台管理地址: http://localhost:${PORT}/admin`); +}); + +const wss = new WebSocket.Server({ server }); + +const videoClients = {}; + +wss.on('connection', (ws) => { + let currentVideoId = null; + + ws.on('message', (message) => { + try { + const data = JSON.parse(message); + if (data.type === 'join' && data.videoId) { + currentVideoId = data.videoId; + if (!videoClients[currentVideoId]) { + videoClients[currentVideoId] = new Set(); + } + videoClients[currentVideoId].add(ws); + } + } catch (e) { + console.error('WebSocket消息解析错误:', e); + } + }); + + ws.on('close', () => { + if (currentVideoId && videoClients[currentVideoId]) { + videoClients[currentVideoId].delete(ws); + } + }); +}); + +function broadcastDanmaku(videoId, danmaku) { + if (videoClients[videoId]) { + const message = JSON.stringify({ + type: 'danmaku', + data: danmaku + }); + videoClients[videoId].forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(message); + } + }); + } +} + +module.exports = app; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6376d0c --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "video-platform", + "version": "1.0.0", + "description": "短视频展示平台", + "main": "app.js", + "scripts": { + "start": "node app.js", + "dev": "node app.js" + }, + "dependencies": { + "express": "^4.18.2", + "express-session": "^1.17.3", + "multer": "^1.4.5-lts.1", + "ws": "^8.14.2" + } +} \ No newline at end of file diff --git a/public/admin-login.html b/public/admin-login.html new file mode 100644 index 0000000..40ce7b6 --- /dev/null +++ b/public/admin-login.html @@ -0,0 +1,79 @@ + + + + + + 后台管理登录 - 短视频平台 + + + +
+ +
+ + + + \ No newline at end of file diff --git a/public/admin-panel.html b/public/admin-panel.html new file mode 100644 index 0000000..fe4ff30 --- /dev/null +++ b/public/admin-panel.html @@ -0,0 +1,392 @@ + + + + + + 后台管理 - 短视频平台 + + + + +
+
+ +
+ 管理员 + +
+
+
+ +
+ + + + +
+ +
+
+

视频管理

+ +
+ +
+ + + + + + + + + + + + + + + + + +
ID封面标题分类作者播放量点赞上传时间操作
+
+
+ + +
+
+

数据统计

+
+ +
+
+
+ + + +
+
+
0
+
视频总数
+
+
+ +
+
+ + + +
+
+
0
+
总播放量
+
+
+ +
+ +
+
0
+
总点赞数
+
+
+
+ +
+

📊 数据可视化图表(可扩展)

+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..cba05a4 --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,1249 @@ +/* 全局样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #ff2442; + --secondary-color: #fe2c55; + --bg-color: #0f0f0f; + --card-bg: #1a1a1a; + --text-primary: #ffffff; + --text-secondary: #999999; + --border-color: #2a2a2a; + --hover-bg: #252525; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif; + background-color: var(--bg-color); + color: var(--text-primary); + line-height: 1.6; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 0 16px; +} + +/* ========== 头部导航 ========== */ +.header { + background-color: var(--card-bg); + border-bottom: 1px solid var(--border-color); + position: sticky; + top: 0; + z-index: 100; +} + +.header .container { + display: flex; + align-items: center; + height: 60px; + gap: 24px; +} + +.logo a { + display: flex; + align-items: center; + gap: 8px; + text-decoration: none; + color: var(--text-primary); + font-size: 18px; + font-weight: bold; +} + +.nav-menu { + display: flex; + gap: 24px; +} + +.nav-item { + color: var(--text-secondary); + text-decoration: none; + font-size: 15px; + transition: color 0.3s; +} + +.nav-item:hover, +.nav-item.active { + color: var(--text-primary); +} + +.search-box { + flex: 1; + max-width: 400px; + display: flex; + background-color: var(--bg-color); + border-radius: 20px; + overflow: hidden; +} + +.search-box input { + flex: 1; + background: none; + border: none; + color: var(--text-primary); + padding: 10px 16px; + font-size: 14px; + outline: none; +} + +.search-box input::placeholder { + color: var(--text-secondary); +} + +.search-box button { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 0 16px; + transition: color 0.3s; +} + +.search-box button:hover { + color: var(--text-primary); +} + +.header-actions { + display: flex; + gap: 12px; + align-items: center; +} + +.btn-upload { + display: flex; + align-items: center; + gap: 6px; + background-color: var(--primary-color); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.btn-upload:hover { + background-color: var(--secondary-color); +} + +.btn-admin { + color: var(--text-secondary); + text-decoration: none; + transition: color 0.3s; +} + +.btn-admin:hover { + color: var(--text-primary); +} + +/* ========== 主内容区 ========== */ +.main-content { + padding: 24px 0; + min-height: calc(100vh - 60px - 80px); +} + +/* ========== 分类标签 ========== */ +.category-tabs { + display: flex; + gap: 12px; + margin-bottom: 24px; + overflow-x: auto; + padding-bottom: 8px; +} + +.category-tab { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + color: var(--text-secondary); + padding: 8px 20px; + border-radius: 20px; + cursor: pointer; + font-size: 14px; + white-space: nowrap; + transition: all 0.3s; +} + +.category-tab:hover, +.category-tab.active { + background-color: var(--primary-color); + border-color: var(--primary-color); + color: white; +} + +/* ========== 视频网格 ========== */ +.video-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 16px; +} + +.video-card { + background-color: var(--card-bg); + border-radius: 8px; + overflow: hidden; + cursor: pointer; + transition: transform 0.3s, box-shadow 0.3s; +} + +.video-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); +} + +.video-cover { + position: relative; + width: 100%; + aspect-ratio: 16/9; + background-color: var(--bg-color); +} + +.video-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.video-duration { + position: absolute; + bottom: 8px; + right: 8px; + background-color: rgba(0, 0, 0, 0.75); + color: white; + padding: 2px 6px; + border-radius: 4px; + font-size: 12px; +} + +.video-info { + padding: 12px; +} + +.video-title { + font-size: 14px; + font-weight: 500; + color: var(--text-primary); + margin-bottom: 8px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-height: 1.4; +} + +.video-meta { + display: flex; + justify-content: space-between; + color: var(--text-secondary); + font-size: 12px; +} + +/* ========== 加载更多 ========== */ +.load-more { + text-align: center; + margin-top: 32px; +} + +.load-more button { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 12px 48px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.load-more button:hover { + background-color: var(--hover-bg); +} + +/* ========== 视频播放页 ========== */ +.video-page .container { + display: flex; + gap: 24px; + max-width: 1600px; +} + +.video-container { + flex: 1; + max-width: 900px; +} + +.player-wrapper { + background-color: #000; + border-radius: 8px; + overflow: hidden; +} + +.video-player { + position: relative; + width: 100%; + aspect-ratio: 16/9; + background-color: #000; +} + +.video-player video { + width: 100%; + height: 100%; + object-fit: contain; +} + +/* 弹幕层 */ +.danmaku-layer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden; +} + +.danmaku-item { + position: absolute; + white-space: nowrap; + font-size: 20px; + font-weight: bold; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); + animation: danmaku-scroll linear; + pointer-events: none; +} + +@keyframes danmaku-scroll { + from { + transform: translateX(100%); + } + to { + transform: translateX(-100%); + } +} + +/* 控制栏 */ +.player-controls { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); + padding: 16px 12px 8px; + display: flex; + align-items: center; + gap: 12px; + opacity: 0; + transition: opacity 0.3s; +} + +.video-player:hover .player-controls { + opacity: 1; +} + +.play-btn { + background: none; + border: none; + color: white; + cursor: pointer; + padding: 4px; +} + +.progress-bar { + flex: 1; + height: 4px; + background-color: rgba(255, 255, 255, 0.3); + border-radius: 2px; + cursor: pointer; + position: relative; +} + +.progress-played { + height: 100%; + background-color: var(--primary-color); + border-radius: 2px; + width: 0%; +} + +.progress-handle { + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 12px; + height: 12px; + background-color: var(--primary-color); + border-radius: 50%; + left: 0%; + opacity: 0; + transition: opacity 0.3s; +} + +.progress-bar:hover .progress-handle { + opacity: 1; +} + +.time-display { + color: white; + font-size: 12px; + font-family: monospace; +} + +.control-right { + display: flex; + align-items: center; + gap: 8px; +} + +.volume-btn, +.fullscreen-btn { + background: none; + border: none; + color: white; + cursor: pointer; + padding: 4px; +} + +.volume-slider { + width: 60px; + height: 4px; + -webkit-appearance: none; + background: rgba(255, 255, 255, 0.3); + border-radius: 2px; + outline: none; +} + +.volume-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 12px; + height: 12px; + background: white; + border-radius: 50%; + cursor: pointer; +} + +/* 弹幕输入 */ +.danmaku-input-wrapper { + display: flex; + gap: 12px; + padding: 12px; + background-color: var(--card-bg); + border-radius: 0 0 8px 8px; +} + +.danmaku-input { + flex: 1; + background-color: var(--bg-color); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 10px 16px; + border-radius: 4px; + font-size: 14px; + outline: none; +} + +.danmaku-input:focus { + border-color: var(--primary-color); +} + +.color-picker { + display: flex; + align-items: center; + gap: 6px; +} + +.color-label { + color: var(--text-secondary); + font-size: 12px; +} + +.color-btn { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; + transition: border-color 0.3s; +} + +.color-btn.active, +.color-btn:hover { + border-color: white; +} + +.danmaku-send-btn { + background-color: var(--primary-color); + color: white; + border: none; + padding: 10px 24px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.danmaku-send-btn:hover { + background-color: var(--secondary-color); +} + +/* 视频信息 */ +.video-info { + margin-top: 16px; +} + +.video-info .video-title { + font-size: 20px; + font-weight: 600; + margin-bottom: 12px; + -webkit-line-clamp: unset; +} + +.video-info .video-meta { + font-size: 14px; + margin-bottom: 16px; +} + +.video-actions { + display: flex; + gap: 16px; + margin-bottom: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border-color); +} + +.action-btn { + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-size: 14px; + transition: color 0.3s; +} + +.action-btn:hover { + color: var(--text-primary); +} + +.author-info { + display: flex; + gap: 12px; + align-items: center; +} + +.author-avatar { + width: 48px; + height: 48px; + background-color: var(--bg-color); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); +} + +.author-name { + font-weight: 500; + margin-bottom: 2px; +} + +.author-desc { + font-size: 14px; + color: var(--text-secondary); +} + +/* 侧边栏 */ +.sidebar { + width: 360px; + flex-shrink: 0; +} + +.sidebar-title { + font-size: 16px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-color); +} + +.recommend-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.recommend-item { + display: flex; + gap: 12px; + cursor: pointer; + transition: opacity 0.3s; +} + +.recommend-item:hover { + opacity: 0.8; +} + +.recommend-cover { + width: 160px; + aspect-ratio: 16/9; + border-radius: 4px; + overflow: hidden; + flex-shrink: 0; +} + +.recommend-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.recommend-info { + flex: 1; + display: flex; + flex-direction: column; +} + +.recommend-info .video-title { + font-size: 13px; + margin-bottom: 8px; +} + +.recommend-info .video-meta { + font-size: 12px; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} + +/* ========== 登录页 ========== */ +.login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background: linear-gradient(135deg, var(--bg-color) 0%, #1a1a1a 100%); +} + +.login-container { + width: 100%; + max-width: 400px; + padding: 16px; +} + +.login-box { + background-color: var(--card-bg); + border-radius: 12px; + padding: 40px 32px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +.login-header { + text-align: center; + margin-bottom: 32px; +} + +.login-header svg { + margin-bottom: 16px; +} + +.login-header h1 { + font-size: 24px; + margin-bottom: 8px; +} + +.login-header p { + color: var(--text-secondary); + font-size: 14px; +} + +.login-form .form-group { + margin-bottom: 20px; +} + +.login-form label { + display: block; + font-size: 14px; + margin-bottom: 8px; + color: var(--text-secondary); +} + +.login-form input { + width: 100%; + padding: 12px 16px; + background-color: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 14px; + outline: none; + transition: border-color 0.3s; +} + +.login-form input:focus { + border-color: var(--primary-color); +} + +.login-btn { + width: 100%; + padding: 14px; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 6px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.login-btn:hover { + background-color: var(--secondary-color); +} + +.login-error { + margin-top: 12px; + padding: 12px; + background-color: rgba(255, 36, 66, 0.1); + border: 1px solid var(--primary-color); + border-radius: 6px; + color: var(--primary-color); + font-size: 14px; + text-align: center; +} + +.login-hint { + margin-top: 20px; + text-align: center; + color: var(--text-secondary); + font-size: 12px; +} + +.login-footer { + margin-top: 24px; + text-align: center; +} + +.login-footer a { + color: var(--text-secondary); + text-decoration: none; + font-size: 14px; +} + +.login-footer a:hover { + color: var(--text-primary); +} + +/* ========== 后台管理 ========== */ +.admin-page { + background-color: #0a0a0a; +} + +.admin-header { + background-color: var(--card-bg); + border-bottom: 1px solid var(--border-color); + height: 60px; +} + +.admin-header-content { + max-width: 100%; + margin: 0 auto; + padding: 0 24px; + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; +} + +.admin-user { + display: flex; + align-items: center; + gap: 16px; + color: var(--text-secondary); +} + +.logout-btn { + background-color: transparent; + border: 1px solid var(--border-color); + color: var(--text-secondary); + padding: 6px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + transition: all 0.3s; +} + +.logout-btn:hover { + border-color: var(--primary-color); + color: var(--primary-color); +} + +.admin-container { + display: flex; + min-height: calc(100vh - 60px); +} + +.admin-sidebar { + width: 240px; + background-color: var(--card-bg); + border-right: 1px solid var(--border-color); + padding: 24px 0; +} + +.admin-nav { + display: flex; + flex-direction: column; +} + +.admin-nav .nav-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 24px; + color: var(--text-secondary); + text-decoration: none; + transition: all 0.3s; +} + +.admin-nav .nav-item:hover, +.admin-nav .nav-item.active { + background-color: var(--hover-bg); + color: var(--text-primary); +} + +.admin-main { + flex: 1; + padding: 24px; + overflow-x: auto; +} + +.admin-tab { + display: none; +} + +.admin-tab.active { + display: block; +} + +.tab-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.tab-header h2 { + font-size: 20px; +} + +.btn-primary { + background-color: var(--primary-color); + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.btn-primary:hover { + background-color: var(--secondary-color); +} + +.btn-secondary { + background-color: var(--hover-bg); + color: var(--text-primary); + border: 1px solid var(--border-color); + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.btn-secondary:hover { + background-color: var(--card-bg); +} + +.video-table-wrapper { + background-color: var(--card-bg); + border-radius: 8px; + overflow-x: auto; +} + +.video-table { + width: 100%; + border-collapse: collapse; +} + +.video-table th, +.video-table td { + padding: 16px; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.video-table th { + color: var(--text-secondary); + font-weight: 500; + font-size: 13px; + text-transform: uppercase; +} + +.video-table td { + font-size: 14px; +} + +.table-cover { + width: 80px; + height: 45px; + object-fit: cover; + border-radius: 4px; +} + +.video-title-cell { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.category-tag { + display: inline-block; + background-color: rgba(255, 36, 66, 0.2); + color: var(--primary-color); + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; +} + +.action-cell { + display: flex; + gap: 8px; +} + +.btn-edit, +.btn-delete { + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + cursor: pointer; + border: none; + transition: all 0.3s; +} + +.btn-edit { + background-color: rgba(66, 165, 245, 0.2); + color: #42a5f5; +} + +.btn-edit:hover { + background-color: rgba(66, 165, 245, 0.3); +} + +.btn-delete { + background-color: rgba(244, 67, 54, 0.2); + color: #f44336; +} + +.btn-delete:hover { + background-color: rgba(244, 67, 54, 0.3); +} + +/* 统计卡片 */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 24px; + margin-bottom: 32px; +} + +.stat-card { + background-color: var(--card-bg); + border-radius: 8px; + padding: 24px; + display: flex; + align-items: center; + gap: 20px; +} + +.stat-icon { + width: 64px; + height: 64px; + background-color: rgba(255, 36, 66, 0.1); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + color: var(--primary-color); +} + +.stat-icon.views-icon { + background-color: rgba(66, 165, 245, 0.1); + color: #42a5f5; +} + +.stat-icon.likes-icon { + background-color: rgba(76, 175, 80, 0.1); + color: #4caf50; +} + +.stat-value { + font-size: 32px; + font-weight: bold; + margin-bottom: 4px; +} + +.stat-label { + color: var(--text-secondary); + font-size: 14px; +} + +.chart-placeholder { + background-color: var(--card-bg); + border-radius: 8px; + padding: 60px; + text-align: center; + color: var(--text-secondary); +} + +/* 模态框 */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.75); + z-index: 1000; + align-items: center; + justify-content: center; +} + +.modal.active { + display: flex; +} + +.modal-content { + background-color: var(--card-bg); + border-radius: 12px; + width: 90%; + max-width: 600px; + max-height: 90vh; + overflow-y: auto; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid var(--border-color); +} + +.modal-header h3 { + font-size: 18px; +} + +.modal-close { + background: none; + border: none; + color: var(--text-secondary); + font-size: 24px; + cursor: pointer; + line-height: 1; +} + +.modal-close:hover { + color: var(--text-primary); +} + +.video-form { + padding: 24px; +} + +.video-form .form-group { + margin-bottom: 20px; +} + +.video-form label { + display: block; + font-size: 14px; + margin-bottom: 8px; + color: var(--text-secondary); +} + +.video-form input, +.video-form select, +.video-form textarea { + width: 100%; + padding: 12px 16px; + background-color: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 14px; + outline: none; + transition: border-color 0.3s; +} + +.video-form input:focus, +.video-form select:focus, +.video-form textarea:focus { + border-color: var(--primary-color); +} + +.video-form select { + cursor: pointer; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; +} + +/* ========== 底部 ========== */ +.footer { + background-color: var(--card-bg); + border-top: 1px solid var(--border-color); + padding: 24px 0; + text-align: center; + color: var(--text-secondary); + font-size: 14px; +} + +/* ========== 响应式设计 ========== */ +@media (max-width: 1024px) { + .video-page .container { + flex-direction: column; + } + + .sidebar { + width: 100%; + } + + .recommend-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 16px; + } + + .recommend-item { + flex-direction: column; + } + + .recommend-cover { + width: 100%; + } + + .recommend-info .video-title { + -webkit-line-clamp: 2; + } +} + +@media (max-width: 768px) { + .header .container { + gap: 12px; + } + + .nav-menu { + display: none; + } + + .search-box { + order: 10; + max-width: none; + width: 100%; + margin-top: 8px; + } + + .header .container { + flex-wrap: wrap; + height: auto; + padding: 12px 16px; + } + + .btn-upload span { + display: none; + } + + .video-grid { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 12px; + } + + .video-info { + padding: 8px; + } + + .video-title { + font-size: 13px; + } + + .video-meta { + font-size: 11px; + } + + .video-actions { + flex-wrap: wrap; + } + + .danmaku-input-wrapper { + flex-direction: column; + gap: 8px; + } + + .color-picker { + justify-content: center; + } + + .admin-sidebar { + display: none; + } + + .admin-container { + flex-direction: column; + } + + .form-row { + grid-template-columns: 1fr; + } + + .video-table th:nth-child(7), + .video-table td:nth-child(7), + .video-table th:nth-child(8), + .video-table td:nth-child(8) { + display: none; + } +} + +@media (max-width: 480px) { + .container { + padding: 0 12px; + } + + .login-box { + padding: 24px 20px; + } + + .stat-card { + flex-direction: column; + text-align: center; + } + + .stats-grid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..cb07ca1 --- /dev/null +++ b/public/index.html @@ -0,0 +1,104 @@ + + + + + + 短视频平台 - 发现精彩 + + + + +
+
+ + + + + + +
+ + + + + + +
+
+
+ + +
+
+ +
+ + + + + + + +
+ + +
+ +
+ + +
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..8b3b126 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,93 @@ +// 全局配置 +const API_BASE = ''; + +// 格式化数字 +function formatNumber(num) { + if (num >= 10000) { + return (num / 10000).toFixed(1) + '万'; + } + return num || 0; +} + +// 格式化时长 +function formatDuration(seconds) { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + +// 格式化日期 +function formatDate(dateStr) { + return dateStr || ''; +} + +// 加载视频列表 +async function loadVideos(category = 'all') { + try { + const response = await fetch(`${API_BASE}/api/videos`); + const data = await response.json(); + + if (data.success) { + let videos = data.data; + + // 分类过滤 + if (category && category !== 'all') { + videos = videos.filter(v => v.category === category); + } + + renderVideos(videos); + } + } catch (error) { + console.error('加载视频失败:', error); + } +} + +// 渲染视频列表 +function renderVideos(videos) { + const grid = document.getElementById('videoGrid'); + if (!grid) return; + + grid.innerHTML = videos.map(video => ` +
+
+ ${video.title} + ${formatDuration(video.duration || 0)} +
+
+

${video.title}

+
+ ${formatNumber(video.views)} 播放 + ${video.author || '未知'} +
+
+
+ `).join(''); +} + +// 跳转到视频页 +function goToVideo(id) { + window.location.href = `/video/${id}`; +} + +// 搜索功能 +function handleSearch() { + const keyword = document.getElementById('searchInput').value.trim(); + if (keyword) { + alert('搜索功能开发中...'); + // TODO: 实现搜索 + } +} + +// 回车搜索 +document.addEventListener('DOMContentLoaded', () => { + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + handleSearch(); + } + }); + } +}); \ No newline at end of file diff --git a/public/js/player.js b/public/js/player.js new file mode 100644 index 0000000..d6d8115 --- /dev/null +++ b/public/js/player.js @@ -0,0 +1,424 @@ +// 视频播放器和弹幕系统 + +let videoData = null; +let player = null; +let videoElement = null; +let danmakuLayer = null; +let progressBar = null; +let progressPlayed = null; +let progressHandle = null; +let currentTimeDisplay = null; +let durationDisplay = null; +let playBtn = null; +let volumeSlider = null; +let fullscreenBtn = null; +let danmakuInput = null; +let sendDanmakuBtn = null; +let selectedColor = '#ffffff'; +let ws = null; +let videoId = null; +let danmakuList = []; + +// 初始化视频页面 +async function initVideoPage() { + // 获取视频ID + const pathParts = window.location.pathname.split('/'); + videoId = pathParts[pathParts.length - 1]; + + if (!videoId) { + alert('视频不存在'); + window.location.href = '/'; + return; + } + + // 获取DOM元素 + videoElement = document.getElementById('mainVideo'); + danmakuLayer = document.getElementById('danmakuLayer'); + progressBar = document.getElementById('progressBar'); + progressPlayed = document.getElementById('progressPlayed'); + progressHandle = document.getElementById('progressHandle'); + currentTimeDisplay = document.getElementById('currentTime'); + durationDisplay = document.getElementById('duration'); + playBtn = document.getElementById('playBtn'); + volumeSlider = document.getElementById('volumeSlider'); + fullscreenBtn = document.getElementById('fullscreenBtn'); + danmakuInput = document.getElementById('danmakuInput'); + sendDanmakuBtn = document.getElementById('sendDanmaku'); + + // 加载视频数据 + await loadVideoData(); + + // 初始化播放器 + initPlayer(); + + // 加载弹幕 + await loadDanmaku(); + + // 连接WebSocket + connectWebSocket(); + + // 初始化弹幕输入 + initDanmakuInput(); + + // 加载推荐视频 + loadRecommendVideos(); +} + +// 加载视频数据 +async function loadVideoData() { + try { + const response = await fetch(`/api/videos/${videoId}`); + const data = await response.json(); + + if (data.success) { + videoData = data.data; + + // 更新页面信息 + document.getElementById('videoTitle').textContent = videoData.title; + document.getElementById('viewCount').textContent = formatNumber(videoData.views); + document.getElementById('uploadDate').textContent = videoData.createTime; + document.getElementById('authorName').textContent = videoData.author || '未知作者'; + document.getElementById('videoDesc').textContent = videoData.description || '暂无描述'; + document.getElementById('likeCount').textContent = formatNumber(videoData.likes); + + // 设置视频源 + videoElement.querySelector('source').src = videoData.url; + videoElement.load(); + + // 更新页面标题 + document.title = `${videoData.title} - 短视频平台`; + } else { + alert('视频不存在'); + window.location.href = '/'; + } + } catch (error) { + console.error('加载视频失败:', error); + alert('加载失败,请重试'); + } +} + +// 初始化播放器 +function initPlayer() { + // 播放/暂停按钮 + playBtn.addEventListener('click', togglePlay); + + // 视频点击播放/暂停 + videoElement.addEventListener('click', togglePlay); + + // 视频加载完成 + videoElement.addEventListener('loadedmetadata', () => { + durationDisplay.textContent = formatDuration(videoElement.duration); + }); + + // 时间更新 + videoElement.addEventListener('timeupdate', updateProgress); + + // 播放状态变化 + videoElement.addEventListener('play', () => { + playBtn.querySelector('.play-icon').style.display = 'none'; + playBtn.querySelector('.pause-icon').style.display = 'block'; + }); + + videoElement.addEventListener('pause', () => { + playBtn.querySelector('.play-icon').style.display = 'block'; + playBtn.querySelector('.pause-icon').style.display = 'none'; + }); + + // 进度条点击 + progressBar.addEventListener('click', seekVideo); + + // 音量控制 + volumeSlider.addEventListener('input', (e) => { + videoElement.volume = e.target.value / 100; + }); + + // 全屏 + fullscreenBtn.addEventListener('click', toggleFullscreen); + + // 点赞按钮 + document.getElementById('likeBtn').addEventListener('click', likeVideo); +} + +// 切换播放/暂停 +function togglePlay() { + if (videoElement.paused) { + videoElement.play(); + } else { + videoElement.pause(); + } +} + +// 更新进度条 +function updateProgress() { + const current = videoElement.currentTime; + const duration = videoElement.duration; + const percent = (current / duration) * 100; + + progressPlayed.style.width = percent + '%'; + progressHandle.style.left = percent + '%'; + currentTimeDisplay.textContent = formatDuration(current); + + // 播放弹幕 + playDanmakuAtTime(current); +} + +// 格式化时长 +function formatDuration(seconds) { + if (isNaN(seconds)) return '0:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + +// 跳转视频 +function seekVideo(e) { + const rect = progressBar.getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + videoElement.currentTime = percent * videoElement.duration; +} + +// 全屏 +function toggleFullscreen() { + const playerWrapper = document.querySelector('.player-wrapper'); + + if (!document.fullscreenElement) { + if (playerWrapper.requestFullscreen) { + playerWrapper.requestFullscreen(); + } else if (playerWrapper.webkitRequestFullscreen) { + playerWrapper.webkitRequestFullscreen(); + } else if (playerWrapper.msRequestFullscreen) { + playerWrapper.msRequestFullscreen(); + } + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } +} + +// 点赞视频 +async function likeVideo() { + try { + const response = await fetch(`/api/videos/${videoId}/like`, { + method: 'POST' + }); + const data = await response.json(); + + if (data.success) { + document.getElementById('likeCount').textContent = formatNumber(data.likes); + } + } catch (error) { + console.error('点赞失败:', error); + } +} + +// 加载弹幕 +async function loadDanmaku() { + try { + const response = await fetch(`/api/videos/${videoId}/danmaku`); + const data = await response.json(); + + if (data.success) { + danmakuList = data.data; + } + } catch (error) { + console.error('加载弹幕失败:', error); + } +} + +// 在指定时间播放弹幕 +function playDanmakuAtTime(currentTime) { + const timeWindow = 0.3; // 0.3秒的时间窗口 + + danmakuList.forEach(danmaku => { + // 检查是否已经播放过 + if (danmaku.played) return; + + // 检查是否在当前时间窗口内 + if (Math.abs(danmaku.time - currentTime) < timeWindow) { + createDanmakuElement(danmaku); + danmaku.played = true; + } + }); +} + +// 创建弹幕元素 +function createDanmakuElement(danmaku) { + const elem = document.createElement('div'); + elem.className = 'danmaku-item'; + elem.textContent = danmaku.text; + elem.style.color = danmaku.color || '#ffffff'; + + // 随机垂直位置 + const randomTop = Math.random() * 70; // 0-70% + elem.style.top = randomTop + '%'; + + // 设置动画时长(根据视频宽度调整) + const duration = 6 + Math.random() * 4; // 6-10秒 + elem.style.animationDuration = duration + 's'; + + danmakuLayer.appendChild(elem); + + // 动画结束后移除 + setTimeout(() => { + if (elem.parentNode) { + elem.parentNode.removeChild(elem); + } + }, duration * 1000); +} + +// 初始化弹幕输入 +function initDanmakuInput() { + // 颜色选择 + document.querySelectorAll('.color-btn').forEach(btn => { + btn.addEventListener('click', function() { + document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('active')); + this.classList.add('active'); + selectedColor = this.dataset.color; + }); + }); + + // 发送弹幕 + sendDanmakuBtn.addEventListener('click', sendDanmaku); + + // 回车发送 + danmakuInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + sendDanmaku(); + } + }); +} + +// 发送弹幕 +async function sendDanmaku() { + const text = danmakuInput.value.trim(); + if (!text) return; + + const danmaku = { + time: videoElement.currentTime, + text: text, + color: selectedColor + }; + + try { + const response = await fetch(`/api/videos/${videoId}/danmaku`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(danmaku) + }); + + const data = await response.json(); + + if (data.success) { + // 添加到本地列表并显示 + danmakuList.push(data.data); + createDanmakuElement(data.data); + danmakuInput.value = ''; + } + } catch (error) { + console.error('发送弹幕失败:', error); + } +} + +// 连接WebSocket +function connectWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}`; + + ws = new WebSocket(wsUrl); + + ws.onopen = () => { + // 加入视频房间 + ws.send(JSON.stringify({ + type: 'join', + videoId: videoId + })); + }; + + ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + + if (message.type === 'danmaku') { + // 接收到新弹幕 + danmakuList.push(message.data); + + // 如果是当前时间附近的弹幕,立即显示 + if (Math.abs(message.data.time - videoElement.currentTime) < 1) { + createDanmakuElement(message.data); + } + } + } catch (error) { + console.error('WebSocket消息解析错误:', error); + } + }; + + ws.onerror = (error) => { + console.error('WebSocket错误:', error); + }; + + ws.onclose = () => { + // 重连 + setTimeout(connectWebSocket, 3000); + }; +} + +// 加载推荐视频 +async function loadRecommendVideos() { + try { + const response = await fetch('/api/videos'); + const data = await response.json(); + + if (data.success) { + // 过滤掉当前视频,随机取6个 + let videos = data.data.filter(v => v.id != videoId); + videos = videos.sort(() => Math.random() - 0.5).slice(0, 6); + + const list = document.getElementById('recommendList'); + list.innerHTML = videos.map(video => ` +
+
+ ${video.title} +
+
+

${video.title}

+
+ ${formatNumber(video.views)} 播放 + ${video.author || '未知'} +
+
+
+ `).join(''); + } + } catch (error) { + console.error('加载推荐视频失败:', error); + } +} + +// 格式化数字 +function formatNumber(num) { + if (num >= 10000) { + return (num / 10000).toFixed(1) + '万'; + } + return num || 0; +} + +// 跳转到视频 +function goToVideo(id) { + window.location.href = `/video/${id}`; +} + +// 页面卸载时关闭WebSocket +window.addEventListener('beforeunload', () => { + if (ws) { + ws.close(); + } +}); \ No newline at end of file diff --git a/public/video.html b/public/video.html new file mode 100644 index 0000000..d43987f --- /dev/null +++ b/public/video.html @@ -0,0 +1,176 @@ + + + + + + 视频播放 - 短视频平台 + + + + +
+
+ + + + + + + +
+
+ + +
+
+
+ +
+
+ + + +
+ + +
+ + +
+
+
+
+ + + 0:00 / 0:00 + + +
+ + + + +
+
+
+ + +
+ +
+ 颜色: + + + + + + +
+ +
+
+ + +
+

加载中...

+
+ 0 次播放 + +
+ +
+ + + +
+ +
+
+ + + +
+
+
加载中...
+
加载中...
+
+
+
+
+ + + +
+
+ + + + + + \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..c717bde --- /dev/null +++ b/start.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# 短视频平台启动脚本 + +cd "$(dirname "$0")" + +# 检查是否已经在运行 +if lsof -i:16035 > /dev/null 2>&1; then + echo "服务已在运行中 (端口 16035)" + echo "访问地址: http://localhost:16035" + echo "后台管理: http://localhost:16035/admin" + exit 0 +fi + +# 启动服务 +echo "正在启动短视频平台..." +nohup node app.js > logs/app.log 2>&1 & + +# 等待服务启动 +sleep 2 + +# 检查是否成功启动 +if lsof -i:16035 > /dev/null 2>&1; then + echo "✓ 服务启动成功!" + echo "" + echo "访问地址: http://localhost:16035" + echo "后台管理: http://localhost:16035/admin" + echo "" + echo "默认管理员账号:" + echo " 用户名: admin" + echo " 密码: admin123" +else + echo "✗ 服务启动失败,请检查日志: logs/app.log" + exit 1 +fi \ No newline at end of file diff --git a/stop.sh b/stop.sh new file mode 100755 index 0000000..c841842 --- /dev/null +++ b/stop.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# 短视频平台停止脚本 + +cd "$(dirname "$0")" + +# 查找并停止进程 +PID=$(lsof -t -i:16035 2>/dev/null) + +if [ -z "$PID" ]; then + echo "服务未运行" + exit 0 +fi + +echo "正在停止服务 (PID: $PID)..." +kill $PID + +# 等待进程结束 +sleep 2 + +# 再次检查 +if lsof -i:16035 > /dev/null 2>&1; then + echo "强制停止..." + kill -9 $PID +fi + +echo "✓ 服务已停止" \ No newline at end of file