From 93b01223258f2b03fa1987314c54bab8a83fa1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=84i=20me=CC=81ng=20ko=CC=8Cng=20lo=CC=81ng?= Date: Mon, 2 Jun 2025 04:22:18 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A3=9E=E7=89=9BOS=E5=BE=AE=E4=BF=A1=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E8=AE=B0=E5=BD=95=E5=A4=87=E4=BB=BD=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这是一个运行在飞牛OS上的微信聊天记录备份工具 --- Dockerfile | 30 +++ README.md | 56 +++++ backend/index.js | 148 +++++++++++ backend/package.json | 22 ++ docker-compose.yml | 20 ++ frontend/package.json | 40 +++ frontend/public/index.html | 20 ++ frontend/public/manifest.json | 25 ++ frontend/src/App.css | 35 +++ frontend/src/App.js | 70 ++++++ frontend/src/index.js | 10 + frontend/src/pages/ChatDetail.css | 297 ++++++++++++++++++++++ frontend/src/pages/ChatDetail.js | 398 ++++++++++++++++++++++++++++++ frontend/src/pages/ChatView.css | 161 ++++++++++++ frontend/src/pages/ChatView.js | 297 ++++++++++++++++++++++ frontend/src/pages/Dashboard.css | 86 +++++++ frontend/src/pages/Dashboard.js | 223 +++++++++++++++++ frontend/src/pages/Login.css | 50 ++++ frontend/src/pages/Login.js | 84 +++++++ install.sh | 49 ++++ 20 files changed, 2121 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 backend/index.js create mode 100644 backend/package.json create mode 100644 docker-compose.yml create mode 100644 frontend/package.json create mode 100644 frontend/public/index.html create mode 100644 frontend/public/manifest.json create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.js create mode 100644 frontend/src/index.js create mode 100644 frontend/src/pages/ChatDetail.css create mode 100644 frontend/src/pages/ChatDetail.js create mode 100644 frontend/src/pages/ChatView.css create mode 100644 frontend/src/pages/ChatView.js create mode 100644 frontend/src/pages/Dashboard.css create mode 100644 frontend/src/pages/Dashboard.js create mode 100644 frontend/src/pages/Login.css create mode 100644 frontend/src/pages/Login.js create mode 100644 install.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b788662 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# 多阶段构建 +# 阶段1: 构建前端 +FROM node:18-alpine as frontend-builder + +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm install +COPY frontend/ ./ +RUN npm run build + +# 阶段2: 构建后端 +FROM node:18-alpine + +WORKDIR /app +COPY backend/package*.json ./ +RUN npm install --production + +# 复制后端代码 +COPY backend/ ./ +# 从前端构建阶段复制构建结果 +COPY --from=frontend-builder /app/frontend/build ./public + +# 创建数据目录 +RUN mkdir -p /app/data + +# 暴露端口 +EXPOSE 5000 + +# 启动命令 +CMD ["node", "index.js"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..361dd38 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# 飞牛OS微信备份工具 + +这是一个运行在飞牛OS上的微信聊天记录备份工具,提供以下功能: + +- 备份微信聊天记录 +- 随时查看备份的聊天记录 +- 恢复聊天记录到微信中 +- 精致的UI界面(与微信界面一致) +- 安全的登录系统 + +## 界面预览 + +- 登录界面:精美的登录页面,确保用户安全访问 +- 聊天列表:与微信界面一致的聊天列表,包含置顶功能、未读消息提示等 +- 聊天详情:与微信聊天界面一致的布局,支持文本、图片、语音、视频等多种消息类型 +- 仪表盘:直观展示备份数据统计信息 + +## 项目结构 + +``` +. +├── backend/ # 后端代码 +│ ├── api/ # API接口 +│ ├── services/ # 业务逻辑 +│ └── utils/ # 工具函数 +├── frontend/ # 前端代码 +│ ├── public/ # 静态资源 +│ └── src/ # 源代码 +│ ├── assets/ # 图片等资源 +│ ├── components/# 组件 +│ ├── pages/ # 页面 +│ └── utils/ # 工具函数 +└── README.md # 项目说明 +``` + +## 技术栈 + +- 前端:React, Ant Design +- 后端:Node.js, Express +- 数据库:SQLite + +## 特色功能 + +- **微信风格界面**:完全模仿微信的UI设计,让用户有熟悉的使用体验 +- **多种消息类型支持**:支持文本、图片、语音、视频等多种消息类型的备份和查看 +- **聊天记录恢复**:可将备份的聊天记录恢复到微信中 +- **数据安全**:本地存储,数据不会上传到云端,确保用户隐私 + +## 安装与使用 + +1. 克隆仓库到本地 +2. 运行安装脚本:`./install.sh` +3. 访问 `http://localhost:8888` +4. 使用默认账号登录: + - 用户名:admin + - 密码:admin \ No newline at end of file diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 0000000..504e551 --- /dev/null +++ b/backend/index.js @@ -0,0 +1,148 @@ +const express = require('express'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcrypt'); + +// 初始化Express应用 +const app = express(); +const PORT = process.env.PORT || 5000; +const JWT_SECRET = 'flyinox-wechat-backup-secret'; // 实际应用中应使用环境变量存储密钥 + +// 中间件 +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +// 简单的用户验证(实际应用中应使用数据库) +const users = [ + { + id: 1, + username: 'admin', + password: '$2b$10$8KVH7I3NKGjaa2SYj1CMmuOFjBP5.U6H69H8y8fMHpgxBG.xzR.Ke', // admin + name: '管理员' + } +]; + +// 身份验证中间件 +const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) return res.status(401).json({ message: '未提供认证令牌' }); + + jwt.verify(token, JWT_SECRET, (err, user) => { + if (err) return res.status(403).json({ message: '无效或过期的令牌' }); + req.user = user; + next(); + }); +}; + +// 路由定义 +// 登录 +app.post('/api/login', async (req, res) => { + const { username, password } = req.body; + + const user = users.find(user => user.username === username); + if (!user) return res.status(401).json({ message: '用户名或密码错误' }); + + const validPassword = await bcrypt.compare(password, user.password); + if (!validPassword) return res.status(401).json({ message: '用户名或密码错误' }); + + const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '24h' }); + + res.json({ + token, + user: { + id: user.id, + username: user.username, + name: user.name + } + }); +}); + +// 获取仪表盘统计数据 +app.get('/api/dashboard/stats', authenticateToken, (req, res) => { + // 在实际应用中,这些数据应从数据库获取 + res.json({ + totalChats: 142, + totalContacts: 237, + totalMessages: 12483, + lastBackup: '2023-08-15 14:30:22' + }); +}); + +// 获取聊天列表 +app.get('/api/chats', authenticateToken, (req, res) => { + // 在实际应用中,这些数据应从数据库获取 + const chats = [ + { + id: '1', + name: '张三', + avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png', + lastMessage: '晚上一起吃饭吗?', + lastTime: '14:23', + unread: 3, + isGroup: false, + }, + { + id: '2', + name: '工作群', + avatar: '', + lastMessage: '李四: 明天的会议推迟到下午2点', + lastTime: '昨天', + unread: 0, + isGroup: true, + }, + // 其他聊天记录... + ]; + + res.json(chats); +}); + +// 获取聊天详情 +app.get('/api/chats/:chatId', authenticateToken, (req, res) => { + const { chatId } = req.params; + + // 在实际应用中,这些数据应从数据库获取 + const chatInfo = { + id: chatId, + name: chatId === '1' ? '张三' : (chatId === '2' ? '工作群' : '聊天'), + avatar: chatId === '1' ? 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png' : '', + isGroup: chatId === '2' || chatId === '4', + memberCount: chatId === '2' ? 15 : (chatId === '4' ? 5 : 2), + createdAt: '2022-05-20', + }; + + res.json(chatInfo); +}); + +// 获取聊天消息 +app.get('/api/chats/:chatId/messages', authenticateToken, (req, res) => { + const { chatId } = req.params; + + // 在实际应用中,这些数据应从数据库获取 + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + const messages = [ + { + id: '1', + senderId: chatId === '1' ? '1' : 'user', + senderName: chatId === '1' ? '张三' : '我', + content: '你好,最近怎么样?', + type: 'text', + timestamp: '08:30', + date: today.toISOString().split('T')[0], + }, + // 其他消息... + ]; + + res.json(messages); +}); + +// 启动服务器 +app.listen(PORT, () => { + console.log(`服务器运行在 http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..484cd84 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,22 @@ +{ + "name": "wechat-backup-backend", + "version": "0.1.0", + "private": true, + "main": "index.js", + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "body-parser": "^1.20.2", + "sqlite3": "^5.1.6", + "jsonwebtoken": "^9.0.2", + "bcrypt": "^5.1.1", + "multer": "^1.4.5-lts.1" + }, + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7c57550 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3' + +services: + wechat-backup: + build: . + container_name: wechat-backup + restart: always + ports: + - "8888:5000" + volumes: + - ./data:/app/data + environment: + - NODE_ENV=production + - PORT=5000 + networks: + - wechat-backup-network + +networks: + wechat-backup-network: + driver: bridge \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..4939854 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,40 @@ +{ + "name": "wechat-backup-frontend", + "version": "0.1.0", + "private": true, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.16.0", + "antd": "^5.8.6", + "axios": "^1.5.0", + "dayjs": "^1.11.9" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "react-scripts": "5.0.1" + } +} \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..54570fe --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + 飞牛OS微信备份 + + + +
+ + \ No newline at end of file diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 0000000..994654e --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "微信备份", + "name": "飞牛OS微信备份工具", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..ce1d8e4 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,35 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + color: #333; + background-color: #f5f5f5; +} + +.app { + min-height: 100vh; +} + +/* 自定义滚动条样式 */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js new file mode 100644 index 0000000..39e46f0 --- /dev/null +++ b/frontend/src/App.js @@ -0,0 +1,70 @@ +import React, { useState, useEffect } from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { ConfigProvider } from 'antd'; +import zhCN from 'antd/lib/locale/zh_CN'; + +// 导入页面组件 +import Login from './pages/Login'; +import Dashboard from './pages/Dashboard'; +import ChatView from './pages/ChatView'; +import ChatDetail from './pages/ChatDetail'; + +// 导入全局样式 +import './App.css'; + +function App() { + // 认证状态 + const [isAuthenticated, setIsAuthenticated] = useState(false); + + // 检查本地存储中是否有有效的token + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + setIsAuthenticated(true); + } + }, []); + + // 私有路由组件,只有认证后才能访问 + const PrivateRoute = ({ children }) => { + return isAuthenticated ? children : ; + }; + + return ( + + +
+ + : + + } /> + + + + + } /> + + + + + } /> + + + + + } /> + + {/* 默认重定向到首页 */} + } /> + +
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/frontend/src/index.js b/frontend/src/index.js new file mode 100644 index 0000000..7a40b29 --- /dev/null +++ b/frontend/src/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); \ No newline at end of file diff --git a/frontend/src/pages/ChatDetail.css b/frontend/src/pages/ChatDetail.css new file mode 100644 index 0000000..e3a95f0 --- /dev/null +++ b/frontend/src/pages/ChatDetail.css @@ -0,0 +1,297 @@ +.chat-detail-layout { + min-height: 100vh; + background-color: #ededed; + display: flex; + flex-direction: column; +} + +.chat-detail-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + background-color: #ededed; + box-shadow: none; + position: sticky; + top: 0; + z-index: 10; + height: 44px; +} + +.chat-detail-header h4 { + margin: 0; + font-size: 17px; + font-weight: 500; +} + +.back-button { + margin-right: 16px; + font-size: 16px; + color: #07c160; +} + +.more-button { + color: #07c160; + font-size: 20px; +} + +.chat-info { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; +} + +.member-count { + font-size: 12px; + color: #888; +} + +.chat-detail-content { + flex: 1; + padding: 0; + overflow-y: auto; + background-color: #ededed; +} + +.messages-container { + padding: 16px; + display: flex; + flex-direction: column; +} + +.message-date-group { + margin-bottom: 16px; +} + +.date-divider { + text-align: center; + margin: 10px 0; +} + +.date-divider span { + background-color: rgba(0, 0, 0, 0.1); + color: #888; + font-size: 12px; + padding: 2px 8px; + border-radius: 4px; +} + +.message-wrapper { + margin: 16px 0; +} + +.message-time-indicator { + text-align: center; + margin: 8px 0; +} + +.message-time-indicator span { + color: #888; + font-size: 12px; +} + +.message-item { + display: flex; + margin: 8px 0; + padding: 0; + border: none; +} + +.message-mine { + flex-direction: row-reverse; +} + +.message-avatar { + margin: 0 8px; + flex-shrink: 0; +} + +.message-avatar-mine { + background-color: #07c160; +} + +.message-content { + display: flex; + flex-direction: column; + max-width: 70%; +} + +.message-content-mine { + align-items: flex-end; +} + +.message-sender-name { + font-size: 12px; + color: #999; + margin-bottom: 4px; +} + +.message-bubble { + background-color: #fff; + border-radius: 3px; + padding: 8px 12px; + word-break: break-word; + display: flex; + align-items: center; + position: relative; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.message-bubble::before { + content: ''; + position: absolute; + top: 12px; + left: -6px; + width: 0; + height: 0; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-right: 6px solid #fff; +} + +.message-content-mine .message-bubble { + background-color: #95ec69; +} + +.message-content-mine .message-bubble::before { + left: auto; + right: -6px; + border-right: none; + border-left: 6px solid #95ec69; +} + +.message-bubble-image { + padding: 4px; + background-color: transparent; + box-shadow: none; +} + +.message-bubble-image::before, +.message-bubble-video::before { + display: none; +} + +.message-image img { + max-width: 200px; + max-height: 200px; + border-radius: 4px; + display: block; +} + +.message-bubble-voice { + min-width: 80px; +} + +.message-voice { + display: flex; + align-items: center; +} + +.voice-duration { + margin-left: 8px; + color: #888; +} + +.message-bubble-video { + padding: 4px; + background-color: transparent; + box-shadow: none; +} + +.message-video { + position: relative; +} + +.video-thumbnail { + position: relative; +} + +.video-thumbnail img { + max-width: 200px; + max-height: 150px; + border-radius: 4px; + display: block; +} + +.video-play-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(0, 0, 0, 0.5); + color: #fff; + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.video-duration { + position: absolute; + bottom: 8px; + right: 8px; + background-color: rgba(0, 0, 0, 0.5); + color: #fff; + font-size: 12px; + padding: 2px 4px; + border-radius: 2px; +} + +.chat-input-footer { + padding: 8px; + background-color: #f5f5f5; + border-top: 1px solid #e0e0e0; +} + +.chat-input-container { + display: flex; + align-items: center; + background-color: #fff; + border-radius: 4px; + padding: 4px 8px; +} + +.input-action-button { + color: #888; + font-size: 20px; +} + +.text-input-wrapper { + flex: 1; + margin: 0 8px; +} + +.text-input-wrapper .ant-input { + border: none; + box-shadow: none; + padding: 4px 0; +} + +.input-actions { + display: flex; + align-items: center; +} + +.send-button { + background-color: #07c160; + border-color: #07c160; + margin-left: 8px; +} + +.action-buttons { + display: flex; + justify-content: center; + margin-top: 8px; +} + +.action-button { + margin: 0 8px; +} + +.action-button:first-child { + background-color: #07c160; + border-color: #07c160; +} \ No newline at end of file diff --git a/frontend/src/pages/ChatDetail.js b/frontend/src/pages/ChatDetail.js new file mode 100644 index 0000000..fd5522e --- /dev/null +++ b/frontend/src/pages/ChatDetail.js @@ -0,0 +1,398 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Layout, Button, Typography, Avatar, List, Spin, Divider, message, Tag, Space, Input, Tooltip } from 'antd'; +import { + ArrowLeftOutlined, + DownloadOutlined, + RestoreOutlined, + CalendarOutlined, + FileImageOutlined, + AudioOutlined, + VideoCameraOutlined, + SmileOutlined, + PlusOutlined, + SendOutlined, + EllipsisOutlined +} from '@ant-design/icons'; +import { useNavigate, useParams } from 'react-router-dom'; +import axios from 'axios'; +import './ChatDetail.css'; + +const { Header, Content, Footer } = Layout; +const { Title, Text } = Typography; +const { TextArea } = Input; + +const ChatDetail = () => { + const { chatId } = useParams(); + const [loading, setLoading] = useState(true); + const [chatInfo, setChatInfo] = useState(null); + const [messages, setMessages] = useState([]); + const [inputText, setInputText] = useState(''); + const messagesEndRef = useRef(null); + const messagesContainerRef = useRef(null); + + const navigate = useNavigate(); + + // 获取聊天详情和消息 + useEffect(() => { + const fetchData = async () => { + try { + // 实际应用中应该从后端API获取数据 + // const chatResponse = await axios.get(`http://localhost:5000/api/chats/${chatId}`); + // const messagesResponse = await axios.get(`http://localhost:5000/api/chats/${chatId}/messages`); + // setChatInfo(chatResponse.data); + // setMessages(messagesResponse.data); + + // 模拟API响应 + setTimeout(() => { + const mockChatInfo = { + id: chatId, + name: chatId === '1' ? '张三' : (chatId === '2' ? '工作群' : '聊天'), + avatar: chatId === '1' ? 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png' : '', + isGroup: chatId === '2' || chatId === '4', + memberCount: chatId === '2' ? 15 : (chatId === '4' ? 5 : 2), + createdAt: '2022-05-20', + }; + + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + const mockMessages = [ + { + id: '1', + senderId: chatId === '1' ? '1' : 'user2', + senderName: chatId === '1' ? '张三' : '李四', + content: '你好,最近怎么样?', + type: 'text', + timestamp: '08:30', + date: today.toISOString().split('T')[0], + }, + { + id: '2', + senderId: 'user', + senderName: '我', + content: '还不错,你呢?', + type: 'text', + timestamp: '08:32', + date: today.toISOString().split('T')[0], + }, + { + id: '3', + senderId: chatId === '1' ? '1' : 'user2', + senderName: chatId === '1' ? '张三' : '李四', + content: '我很好,谢谢关心!', + type: 'text', + timestamp: '08:35', + date: today.toISOString().split('T')[0], + }, + { + id: '4', + senderId: chatId === '1' ? '1' : 'user3', + senderName: chatId === '1' ? '张三' : '王五', + content: '[图片]', + type: 'image', + timestamp: '09:15', + date: today.toISOString().split('T')[0], + imageUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png' + }, + { + id: '5', + senderId: 'user', + senderName: '我', + content: '这张照片拍得真不错!', + type: 'text', + timestamp: '09:20', + date: today.toISOString().split('T')[0], + }, + { + id: '6', + senderId: chatId === '1' ? '1' : 'user2', + senderName: chatId === '1' ? '张三' : '李四', + content: '谢谢!', + type: 'text', + timestamp: '09:25', + date: today.toISOString().split('T')[0], + }, + { + id: '7', + senderId: 'user', + senderName: '我', + content: '晚上一起吃饭吗?', + type: 'text', + timestamp: '14:23', + date: today.toISOString().split('T')[0], + }, + { + id: '8', + senderId: chatId === '1' ? '1' : 'user3', + senderName: chatId === '1' ? '张三' : '王五', + content: '[语音]', + type: 'voice', + timestamp: '15:30', + date: yesterday.toISOString().split('T')[0], + duration: '0:12' + }, + { + id: '9', + senderId: 'user', + senderName: '我', + content: '[视频]', + type: 'video', + timestamp: '16:05', + date: yesterday.toISOString().split('T')[0], + duration: '0:48', + thumbnail: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png' + }, + ]; + + setChatInfo(mockChatInfo); + setMessages(mockMessages); + setLoading(false); + }, 1000); + } catch (error) { + console.error('Error fetching chat data:', error); + message.error('获取聊天数据失败'); + setLoading(false); + } + }; + + fetchData(); + }, [chatId]); + + // 滚动到底部 + useEffect(() => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [messages]); + + // 返回到聊天列表 + const handleBack = () => { + navigate('/chats'); + }; + + // 下载聊天记录 + const handleDownload = () => { + message.info('正在下载聊天记录...'); + // 实际应用中应调用后端API下载聊天记录 + }; + + // 恢复到微信 + const handleRestore = () => { + message.info('准备恢复聊天记录到微信...'); + // 实际应用中应调用后端API启动恢复过程 + }; + + // 发送消息 + const handleSendMessage = () => { + if (!inputText.trim()) return; + + const newMessage = { + id: `new-${Date.now()}`, + senderId: 'user', + senderName: '我', + content: inputText, + type: 'text', + timestamp: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), + date: new Date().toISOString().split('T')[0], + }; + + setMessages([...messages, newMessage]); + setInputText(''); + }; + + // 渲染消息内容 + const renderMessageContent = (message) => { + switch (message.type) { + case 'image': + return ( +
+ 图片消息 +
+ ); + case 'voice': + return ( +
+ + {message.duration} +
+ ); + case 'video': + return ( +
+
+ 视频缩略图 +
+ +
+
+ {message.duration} +
+ ); + default: + return {message.content}; + } + }; + + // 根据日期对消息分组 + const messagesByDate = messages.reduce((acc, message) => { + if (!acc[message.date]) { + acc[message.date] = []; + } + acc[message.date].push(message); + return acc; + }, {}); + + // 检查是否需要显示时间 + const shouldShowTime = (messages, index) => { + if (index === 0) return true; + const currentMsg = messages[index]; + const prevMsg = messages[index - 1]; + + // 如果两条消息间隔超过5分钟,显示时间 + const currentTime = new Date(`2023-01-01 ${currentMsg.timestamp}`); + const prevTime = new Date(`2023-01-01 ${prevMsg.timestamp}`); + const diffMinutes = (currentTime - prevTime) / (1000 * 60); + + return diffMinutes > 5; + }; + + return ( + +
+
+ + + +
+ {Object.keys(messagesByDate).map(date => ( +
+
+ {new Date(date).toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' })} +
+ + {messagesByDate[date].map((message, index) => ( +
+ {shouldShowTime(messagesByDate[date], index) && ( +
+ {message.timestamp} +
+ )} + +
+ {message.senderId !== 'user' && ( + + {!chatInfo?.avatar ? message.senderName.charAt(0) : null} + + )} +
+ {chatInfo?.isGroup && message.senderId !== 'user' && ( +
{message.senderName}
+ )} +
+ {renderMessageContent(message)} +
+
+ {message.senderId === 'user' && ( + + )} +
+
+ ))} +
+ ))} +
+
+ + + +