mirror of
https://github.com/sinodidi/---OS.git
synced 2026-03-20 15:57:47 +08:00
飞牛OS微信聊天记录备份工具
这是一个运行在飞牛OS上的微信聊天记录备份工具
This commit is contained in:
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -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"]
|
||||
56
README.md
Normal file
56
README.md
Normal file
@@ -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
|
||||
148
backend/index.js
Normal file
148
backend/index.js
Normal file
@@ -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}`);
|
||||
});
|
||||
22
backend/package.json
Normal file
22
backend/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@@ -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
|
||||
40
frontend/package.json
Normal file
40
frontend/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
20
frontend/public/index.html
Normal file
20
frontend/public/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="飞牛OS微信备份工具"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>飞牛OS微信备份</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>您需要启用JavaScript才能运行此应用。</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
25
frontend/public/manifest.json
Normal file
25
frontend/public/manifest.json
Normal file
@@ -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"
|
||||
}
|
||||
35
frontend/src/App.css
Normal file
35
frontend/src/App.css
Normal file
@@ -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;
|
||||
}
|
||||
70
frontend/src/App.js
Normal file
70
frontend/src/App.js
Normal file
@@ -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 : <Navigate to="/login" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<Router>
|
||||
<div className="app">
|
||||
<Routes>
|
||||
<Route path="/login" element={
|
||||
isAuthenticated ?
|
||||
<Navigate to="/" /> :
|
||||
<Login setIsAuthenticated={setIsAuthenticated} />
|
||||
} />
|
||||
|
||||
<Route path="/" element={
|
||||
<PrivateRoute>
|
||||
<Dashboard setIsAuthenticated={setIsAuthenticated} />
|
||||
</PrivateRoute>
|
||||
} />
|
||||
|
||||
<Route path="/chats" element={
|
||||
<PrivateRoute>
|
||||
<ChatView />
|
||||
</PrivateRoute>
|
||||
} />
|
||||
|
||||
<Route path="/chat/:chatId" element={
|
||||
<PrivateRoute>
|
||||
<ChatDetail />
|
||||
</PrivateRoute>
|
||||
} />
|
||||
|
||||
{/* 默认重定向到首页 */}
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
10
frontend/src/index.js
Normal file
10
frontend/src/index.js
Normal file
@@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
297
frontend/src/pages/ChatDetail.css
Normal file
297
frontend/src/pages/ChatDetail.css
Normal file
@@ -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;
|
||||
}
|
||||
398
frontend/src/pages/ChatDetail.js
Normal file
398
frontend/src/pages/ChatDetail.js
Normal file
@@ -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 (
|
||||
<div className="message-image">
|
||||
<img src={message.imageUrl} alt="图片消息" />
|
||||
</div>
|
||||
);
|
||||
case 'voice':
|
||||
return (
|
||||
<div className="message-voice">
|
||||
<AudioOutlined />
|
||||
<span className="voice-duration">{message.duration}</span>
|
||||
</div>
|
||||
);
|
||||
case 'video':
|
||||
return (
|
||||
<div className="message-video">
|
||||
<div className="video-thumbnail">
|
||||
<img src={message.thumbnail} alt="视频缩略图" />
|
||||
<div className="video-play-icon">
|
||||
<VideoCameraOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<span className="video-duration">{message.duration}</span>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <span>{message.content}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
// 根据日期对消息分组
|
||||
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 (
|
||||
<Layout className="chat-detail-layout">
|
||||
<Header className="chat-detail-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={handleBack}
|
||||
className="back-button"
|
||||
/>
|
||||
{chatInfo && (
|
||||
<div className="chat-info">
|
||||
<Title level={4}>{chatInfo.name}</Title>
|
||||
{chatInfo.isGroup && (
|
||||
<span className="member-count">{chatInfo.memberCount}人</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EllipsisOutlined />}
|
||||
className="more-button"
|
||||
/>
|
||||
</Header>
|
||||
|
||||
<Content className="chat-detail-content" ref={messagesContainerRef}>
|
||||
<Spin spinning={loading}>
|
||||
<div className="messages-container">
|
||||
{Object.keys(messagesByDate).map(date => (
|
||||
<div key={date} className="message-date-group">
|
||||
<div className="date-divider">
|
||||
<span>{new Date(date).toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' })}</span>
|
||||
</div>
|
||||
|
||||
{messagesByDate[date].map((message, index) => (
|
||||
<div key={message.id} className="message-wrapper">
|
||||
{shouldShowTime(messagesByDate[date], index) && (
|
||||
<div className="message-time-indicator">
|
||||
<span>{message.timestamp}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`message-item ${message.senderId === 'user' ? 'message-mine' : ''}`}>
|
||||
{message.senderId !== 'user' && (
|
||||
<Avatar
|
||||
className="message-avatar"
|
||||
src={message.senderId === '1' && chatInfo?.avatar ? chatInfo.avatar : null}
|
||||
style={{ backgroundColor: !chatInfo?.avatar ? '#1890ff' : null }}
|
||||
>
|
||||
{!chatInfo?.avatar ? message.senderName.charAt(0) : null}
|
||||
</Avatar>
|
||||
)}
|
||||
<div className={`message-content ${message.senderId === 'user' ? 'message-content-mine' : ''}`}>
|
||||
{chatInfo?.isGroup && message.senderId !== 'user' && (
|
||||
<div className="message-sender-name">{message.senderName}</div>
|
||||
)}
|
||||
<div className={`message-bubble ${message.type !== 'text' ? `message-bubble-${message.type}` : ''}`}>
|
||||
{renderMessageContent(message)}
|
||||
</div>
|
||||
</div>
|
||||
{message.senderId === 'user' && (
|
||||
<Avatar className="message-avatar message-avatar-mine">我</Avatar>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</Spin>
|
||||
</Content>
|
||||
|
||||
<Footer className="chat-input-footer">
|
||||
<div className="chat-input-container">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<AudioOutlined />}
|
||||
className="input-action-button"
|
||||
/>
|
||||
<div className="text-input-wrapper">
|
||||
<TextArea
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
placeholder="发送消息"
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
onPressEnter={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-actions">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SmileOutlined />}
|
||||
className="input-action-button"
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
className="input-action-button"
|
||||
/>
|
||||
{inputText.trim() ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSendMessage}
|
||||
className="send-button"
|
||||
shape="circle"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="action-buttons">
|
||||
<Tooltip title="下载聊天记录">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleDownload}
|
||||
className="action-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="恢复到微信">
|
||||
<Button
|
||||
icon={<RestoreOutlined />}
|
||||
onClick={handleRestore}
|
||||
className="action-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Footer>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatDetail;
|
||||
161
frontend/src/pages/ChatView.css
Normal file
161
frontend/src/pages/ChatView.css
Normal file
@@ -0,0 +1,161 @@
|
||||
.chat-view-layout {
|
||||
min-height: 100vh;
|
||||
background-color: #ededed;
|
||||
}
|
||||
|
||||
.chat-view-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-view-header h4 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin-right: 16px;
|
||||
font-size: 16px;
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.search-icon-button {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.wechat-tabs-container {
|
||||
background-color: #ededed;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.wechat-tabs {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.wechat-tabs .ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.wechat-tabs .ant-tabs-nav::before {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.wechat-tabs .ant-tabs-tab {
|
||||
padding: 8px 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.wechat-tabs .ant-tabs-tab-active {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wechat-tabs .ant-tabs-ink-bar {
|
||||
background-color: #07c160;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: 8px 16px;
|
||||
background-color: #ededed;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: #b2b2b2;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.chat-view-content {
|
||||
padding: 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.chat-list-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-item-pinned {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.pinned-indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background-color: #07c160;
|
||||
}
|
||||
|
||||
.chat-list-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.chat-item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.chat-time {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-item-desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.last-message-text {
|
||||
flex: 1;
|
||||
max-width: 240px;
|
||||
color: #b2b2b2 !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.unread-badge .ant-badge-count {
|
||||
background-color: #f43530;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
color: #b2b2b2;
|
||||
}
|
||||
297
frontend/src/pages/ChatView.js
Normal file
297
frontend/src/pages/ChatView.js
Normal file
@@ -0,0 +1,297 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Layout, List, Avatar, Input, Button, Spin, Typography, Badge, Tag, Tabs } from 'antd';
|
||||
import {
|
||||
SearchOutlined,
|
||||
MessageOutlined,
|
||||
UserOutlined,
|
||||
ArrowLeftOutlined,
|
||||
FileImageOutlined,
|
||||
AudioOutlined,
|
||||
VideoCameraOutlined,
|
||||
WechatOutlined,
|
||||
StarOutlined,
|
||||
TeamOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import './ChatView.css';
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
const { Search } = Input;
|
||||
const { Title, Text } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const ChatView = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [chats, setChats] = useState([]);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [filteredChats, setFilteredChats] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState('chats');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 加载聊天数据
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// 实际应用中应该从后端API获取数据
|
||||
// const response = await axios.get('http://localhost:5000/api/chats');
|
||||
// setChats(response.data);
|
||||
|
||||
// 模拟API响应
|
||||
setTimeout(() => {
|
||||
const mockChats = [
|
||||
{
|
||||
id: '1',
|
||||
name: '张三',
|
||||
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
|
||||
lastMessage: '晚上一起吃饭吗?',
|
||||
lastTime: '14:23',
|
||||
unread: 3,
|
||||
isGroup: false,
|
||||
isPinned: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '工作群',
|
||||
avatar: '',
|
||||
lastMessage: '李四: 明天的会议推迟到下午2点',
|
||||
lastTime: '昨天',
|
||||
unread: 0,
|
||||
isGroup: true,
|
||||
isPinned: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '王五',
|
||||
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
|
||||
lastMessage: '[图片]',
|
||||
lastTime: '周一',
|
||||
unread: 0,
|
||||
isGroup: false,
|
||||
isPinned: false,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '家人群',
|
||||
avatar: '',
|
||||
lastMessage: '妈妈: 注意天气变化,记得添衣服',
|
||||
lastTime: '08/12',
|
||||
unread: 5,
|
||||
isGroup: true,
|
||||
isPinned: false,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '赵六',
|
||||
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
|
||||
lastMessage: '[语音]',
|
||||
lastTime: '08/10',
|
||||
unread: 0,
|
||||
isGroup: false,
|
||||
isPinned: false,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: '同学群',
|
||||
avatar: '',
|
||||
lastMessage: '小明: 周末聚会,大家都来啊',
|
||||
lastTime: '08/05',
|
||||
unread: 0,
|
||||
isGroup: true,
|
||||
isPinned: false,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: '李华',
|
||||
avatar: '',
|
||||
lastMessage: '好的,我知道了',
|
||||
lastTime: '08/01',
|
||||
unread: 0,
|
||||
isGroup: false,
|
||||
isPinned: false,
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: '技术交流群',
|
||||
avatar: '',
|
||||
lastMessage: '小张: 有人遇到这个问题吗?',
|
||||
lastTime: '07/28',
|
||||
unread: 0,
|
||||
isGroup: true,
|
||||
isPinned: false,
|
||||
},
|
||||
];
|
||||
|
||||
setChats(mockChats);
|
||||
setFilteredChats(mockChats);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Error fetching chats:', error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// 搜索功能
|
||||
const handleSearch = (value) => {
|
||||
setSearchText(value);
|
||||
|
||||
if (!value) {
|
||||
setFilteredChats(chats);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = chats.filter(chat =>
|
||||
chat.name.toLowerCase().includes(value.toLowerCase()) ||
|
||||
chat.lastMessage.toLowerCase().includes(value.toLowerCase())
|
||||
);
|
||||
|
||||
setFilteredChats(filtered);
|
||||
};
|
||||
|
||||
// 返回仪表盘
|
||||
const handleBack = () => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
// 渲染消息类型图标
|
||||
const renderMessageTypeIcon = (message) => {
|
||||
if (message.includes('[图片]')) {
|
||||
return <FileImageOutlined style={{ marginRight: 8, color: '#52c41a' }} />;
|
||||
} else if (message.includes('[语音]')) {
|
||||
return <AudioOutlined style={{ marginRight: 8, color: '#1890ff' }} />;
|
||||
} else if (message.includes('[视频]')) {
|
||||
return <VideoCameraOutlined style={{ marginRight: 8, color: '#ff4d4f' }} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 根据置顶状态和最后消息时间排序
|
||||
const sortedChats = [...filteredChats].sort((a, b) => {
|
||||
if (a.isPinned && !b.isPinned) return -1;
|
||||
if (!a.isPinned && b.isPinned) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<Layout className="chat-view-layout">
|
||||
<Header className="chat-view-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={handleBack}
|
||||
className="back-button"
|
||||
/>
|
||||
<Title level={4}>微信</Title>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SearchOutlined />}
|
||||
className="search-icon-button"
|
||||
onClick={() => document.querySelector('.search-input').focus()}
|
||||
/>
|
||||
</Header>
|
||||
|
||||
<div className="wechat-tabs-container">
|
||||
<Tabs
|
||||
defaultActiveKey="chats"
|
||||
onChange={setActiveTab}
|
||||
centered
|
||||
className="wechat-tabs"
|
||||
>
|
||||
<TabPane
|
||||
tab={<span><WechatOutlined />聊天</span>}
|
||||
key="chats"
|
||||
/>
|
||||
<TabPane
|
||||
tab={<span><TeamOutlined />通讯录</span>}
|
||||
key="contacts"
|
||||
/>
|
||||
<TabPane
|
||||
tab={<span><StarOutlined />收藏</span>}
|
||||
key="favorites"
|
||||
/>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="search-container">
|
||||
<div className="search-wrapper">
|
||||
<SearchOutlined className="search-icon" />
|
||||
<Input
|
||||
placeholder="搜索"
|
||||
bordered={false}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onPressEnter={() => handleSearch(searchText)}
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Content className="chat-view-content">
|
||||
<Spin spinning={loading}>
|
||||
{activeTab === 'chats' && (
|
||||
<List
|
||||
dataSource={sortedChats}
|
||||
renderItem={item => (
|
||||
<List.Item
|
||||
className={`chat-list-item ${item.isPinned ? 'chat-item-pinned' : ''}`}
|
||||
onClick={() => navigate(`/chat/${item.id}`)}
|
||||
>
|
||||
{item.isPinned && <div className="pinned-indicator"></div>}
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
item.avatar ? (
|
||||
<Avatar src={item.avatar} size={48} />
|
||||
) : (
|
||||
<Avatar size={48} style={{ backgroundColor: item.isGroup ? '#1890ff' : '#f56a00' }}>
|
||||
{item.isGroup ? 'G' : item.name.charAt(0)}
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
title={
|
||||
<div className="chat-item-title">
|
||||
<Text strong>{item.name}</Text>
|
||||
<Text type="secondary" className="chat-time">{item.lastTime}</Text>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div className="chat-item-desc">
|
||||
{renderMessageTypeIcon(item.lastMessage)}
|
||||
<Text type="secondary" ellipsis className="last-message-text">
|
||||
{item.lastMessage}
|
||||
</Text>
|
||||
{item.unread > 0 && (
|
||||
<Badge count={item.unread} className="unread-badge" />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'contacts' && (
|
||||
<div className="placeholder-content">
|
||||
<TeamOutlined style={{ fontSize: 48, color: '#d9d9d9' }} />
|
||||
<p>通讯录功能正在开发中</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'favorites' && (
|
||||
<div className="placeholder-content">
|
||||
<StarOutlined style={{ fontSize: 48, color: '#d9d9d9' }} />
|
||||
<p>收藏功能正在开发中</p>
|
||||
</div>
|
||||
)}
|
||||
</Spin>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatView;
|
||||
86
frontend/src/pages/Dashboard.css
Normal file
86
frontend/src/pages/Dashboard.css
Normal file
@@ -0,0 +1,86 @@
|
||||
.dashboard-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-sider {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
padding: 0 16px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.trigger-button {
|
||||
font-size: 18px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
margin: 24px;
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-description {
|
||||
margin-top: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.sider-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
223
frontend/src/pages/Dashboard.js
Normal file
223
frontend/src/pages/Dashboard.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Layout, Menu, Button, Typography, Statistic, Card, Row, Col, Spin, message } from 'antd';
|
||||
import {
|
||||
MessageOutlined,
|
||||
UserOutlined,
|
||||
FileOutlined,
|
||||
CloudUploadOutlined,
|
||||
CloudDownloadOutlined,
|
||||
LogoutOutlined,
|
||||
HomeOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
ClockCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import './Dashboard.css';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
const { Title } = Typography;
|
||||
|
||||
const Dashboard = ({ setIsAuthenticated }) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [stats, setStats] = useState({
|
||||
totalChats: 0,
|
||||
totalContacts: 0,
|
||||
totalMessages: 0,
|
||||
lastBackup: '未备份'
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 假设从后端API获取数据
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// 实际应用中应该从后端API获取数据
|
||||
// const response = await axios.get('http://localhost:5000/api/dashboard/stats');
|
||||
// setStats(response.data);
|
||||
|
||||
// 模拟API响应
|
||||
setTimeout(() => {
|
||||
setStats({
|
||||
totalChats: 142,
|
||||
totalContacts: 237,
|
||||
totalMessages: 12483,
|
||||
lastBackup: '2023-08-15 14:30:22'
|
||||
});
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
message.error('获取数据失败');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// 登出函数
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
setIsAuthenticated(false);
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
// 创建新备份
|
||||
const handleCreateBackup = () => {
|
||||
message.info('正在启动备份过程...');
|
||||
// 实际应用中应调用后端API启动备份
|
||||
};
|
||||
|
||||
// 恢复备份到微信
|
||||
const handleRestoreBackup = () => {
|
||||
message.info('准备恢复备份到微信...');
|
||||
// 实际应用中应调用后端API启动恢复过程
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout className="dashboard-layout">
|
||||
<Sider trigger={null} collapsible collapsed={collapsed} theme="light" className="dashboard-sider">
|
||||
<div className="logo">
|
||||
<img src="/logo.png" alt="Logo" className="logo-img" />
|
||||
{!collapsed && <span className="logo-text">飞牛OS微信备份</span>}
|
||||
</div>
|
||||
<Menu
|
||||
mode="inline"
|
||||
defaultSelectedKeys={['1']}
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
icon: <HomeOutlined />,
|
||||
label: '仪表盘',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
icon: <MessageOutlined />,
|
||||
label: <Link to="/chats">聊天记录</Link>,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
icon: <UserOutlined />,
|
||||
label: '联系人',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
icon: <FileOutlined />,
|
||||
label: '文件管理',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="sider-footer">
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={handleLogout}
|
||||
block
|
||||
>
|
||||
{!collapsed && '退出登录'}
|
||||
</Button>
|
||||
</div>
|
||||
</Sider>
|
||||
<Layout className="site-layout">
|
||||
<Header className="dashboard-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="trigger-button"
|
||||
/>
|
||||
<div className="header-title">
|
||||
<Title level={4}>微信备份仪表盘</Title>
|
||||
</div>
|
||||
</Header>
|
||||
<Content className="dashboard-content">
|
||||
<Spin spinning={loading}>
|
||||
<div className="content-wrapper">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="聊天会话"
|
||||
value={stats.totalChats}
|
||||
prefix={<MessageOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="联系人"
|
||||
value={stats.totalContacts}
|
||||
prefix={<UserOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="消息总数"
|
||||
value={stats.totalMessages}
|
||||
prefix={<MessageOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="最近备份时间"
|
||||
value={stats.lastBackup}
|
||||
prefix={<ClockCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} className="action-row">
|
||||
<Col xs={24} sm={12}>
|
||||
<Card className="action-card">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CloudUploadOutlined />}
|
||||
size="large"
|
||||
onClick={handleCreateBackup}
|
||||
block
|
||||
>
|
||||
创建新备份
|
||||
</Button>
|
||||
<p className="action-description">
|
||||
创建微信聊天记录、联系人和媒体文件的新备份
|
||||
</p>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Card className="action-card">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<CloudDownloadOutlined />}
|
||||
size="large"
|
||||
onClick={handleRestoreBackup}
|
||||
block
|
||||
>
|
||||
恢复备份到微信
|
||||
</Button>
|
||||
<p className="action-description">
|
||||
将已备份的聊天记录和媒体文件恢复到微信中
|
||||
</p>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Spin>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
50
frontend/src/pages/Login.css
Normal file
50
frontend/src/pages/Login.css
Normal file
@@ -0,0 +1,50 @@
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #4361ee, #3f37c9);
|
||||
}
|
||||
|
||||
.login-content {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
background: #4361ee;
|
||||
border-color: #4361ee;
|
||||
}
|
||||
|
||||
.login-button:hover,
|
||||
.login-button:focus {
|
||||
background: #3f37c9;
|
||||
border-color: #3f37c9;
|
||||
}
|
||||
84
frontend/src/pages/Login.js
Normal file
84
frontend/src/pages/Login.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Input, Button, Card, Typography, message, Spin } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import './Login.css';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const Login = ({ setIsAuthenticated }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onFinish = async (values) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 实际应用中会连接到后端API
|
||||
const response = await axios.post('http://localhost:5000/api/login', {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
});
|
||||
|
||||
if (response.data.token) {
|
||||
localStorage.setItem('token', response.data.token);
|
||||
localStorage.setItem('user', JSON.stringify(response.data.user));
|
||||
setIsAuthenticated(true);
|
||||
message.success('登录成功!');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('登录失败,请检查用户名和密码!');
|
||||
console.error('Login error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-container">
|
||||
<div className="login-content">
|
||||
<Card className="login-card">
|
||||
<div className="login-header">
|
||||
<img src="/logo.png" alt="飞牛OS微信备份" className="login-logo" />
|
||||
<Title level={3}>飞牛OS微信备份</Title>
|
||||
</div>
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
name="login"
|
||||
initialValues={{ remember: true }}
|
||||
onFinish={onFinish}
|
||||
size="large"
|
||||
className="login-form"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入用户名!' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="用户名"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码!' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" className="login-button">
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
49
install.sh
Normal file
49
install.sh
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 打印彩色信息
|
||||
function print_info() {
|
||||
echo -e "\033[36m$1\033[0m"
|
||||
}
|
||||
|
||||
function print_success() {
|
||||
echo -e "\033[32m$1\033[0m"
|
||||
}
|
||||
|
||||
function print_error() {
|
||||
echo -e "\033[31m$1\033[0m"
|
||||
}
|
||||
|
||||
print_info "=== 飞牛OS微信备份工具安装脚本 ==="
|
||||
print_info "正在检查环境..."
|
||||
|
||||
# 检查Docker是否安装
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "未检测到Docker,请先安装Docker。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查Docker Compose是否安装
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
print_error "未检测到Docker Compose,请先安装Docker Compose。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "环境检查通过!"
|
||||
|
||||
# 创建数据目录
|
||||
print_info "创建数据目录..."
|
||||
mkdir -p data
|
||||
|
||||
# 构建并启动容器
|
||||
print_info "构建并启动容器..."
|
||||
docker-compose up -d
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "飞牛OS微信备份工具已成功安装!"
|
||||
print_info "您现在可以通过浏览器访问 http://localhost:8888 使用该工具"
|
||||
print_info "默认用户名: admin"
|
||||
print_info "默认密码: admin"
|
||||
print_info "请尽快修改默认密码以确保安全"
|
||||
else
|
||||
print_error "安装过程中出现错误,请检查上面的错误信息"
|
||||
fi
|
||||
Reference in New Issue
Block a user