main
mula.liu 2025-09-03 18:10:52 +08:00
parent ff2eeee5a3
commit df96e75e7f
14 changed files with 367 additions and 4763 deletions

BIN
.DS_Store vendored 100644

Binary file not shown.

8
.dockerignore 100644
View File

@ -0,0 +1,8 @@
node_modules
npm-debug.log
.git
.gitignore
README.md
Dockerfile
.dockerignore
docker-compose.yml

32
Dockerfile 100644
View File

@ -0,0 +1,32 @@
# 生产环境服务器 - PM2版本
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/node:18-alpine
# 设置工作目录
WORKDIR /app
# 复制构建产物
COPY dist ./dist
# 复制服务器文件和PM2配置
COPY server.js .
COPY ecosystem.config.json .
# 复制生产环境package.json
COPY package.prod.json package.json
# 安装生产依赖和PM2
RUN npm install --production && \
npm install -g pm2
# 创建日志目录
RUN mkdir -p logs
# 暴露端口
EXPOSE 3001
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3001/health || exit 1
# 启动PM2
CMD ["pm2-runtime", "start", "ecosystem.config.json"]

69
deploy-prod.sh 100755
View File

@ -0,0 +1,69 @@
#!/bin/bash
echo "🚀 开始部署iMeeting前端服务PM2模式..."
# 检查node_modules
if [ ! -d "node_modules" ]; then
echo "📦 安装依赖..."
yarn install
fi
# 构建前端
echo "🔨 构建前端应用..."
yarn build
# 检查构建是否成功
if [ $? -ne 0 ]; then
echo "❌ 构建失败!请检查代码错误"
exit 1
fi
if [ ! -d "dist" ]; then
echo "❌ 构建失败dist目录未生成"
exit 1
fi
echo "✅ 前端构建完成开始Docker部署..."
# 创建日志目录
mkdir -p logs
# 停止并删除现有容器
echo "📦 停止现有容器..."
docker-compose -f docker-compose.prod.yml down
# 构建新镜像
echo "🔨 构建Docker镜像..."
docker-compose -f docker-compose.prod.yml build --no-cache
# 启动服务
echo "▶️ 启动PM2服务..."
docker-compose -f docker-compose.prod.yml up -d
# 检查服务状态
echo "🔍 检查服务状态..."
sleep 15
docker-compose -f docker-compose.prod.yml ps
# 检查PM2进程状态
echo "🔄 检查PM2进程状态..."
docker exec imeeting-frontend pm2 list
# 检查健康状态
echo "🏥 检查健康状态..."
curl -f http://localhost:3001/health && echo "✅ 前端服务健康检查通过" || echo "❌ 前端服务健康检查失败"
echo ""
echo "🎉 部署完成!"
echo "📱 前端服务访问地址: http://localhost:3001"
echo "📊 查看日志: docker-compose -f docker-compose.prod.yml logs -f"
echo "📈 查看PM2状态: docker exec imeeting-frontend pm2 monit"
echo "📋 查看PM2进程: docker exec imeeting-frontend pm2 list"
echo "🛑 停止服务: docker-compose -f docker-compose.prod.yml down"
echo ""
echo "💡 提示PM2模式特性"
echo " ✅ 集群模式2个实例"
echo " ✅ 自动重启和故障恢复"
echo " ✅ 内存限制保护1GB"
echo " ✅ 详细日志管理"
echo " ✅ 进程监控和健康检查"

View File

@ -0,0 +1,21 @@
services:
imeeting-frontend:
build:
context: .
dockerfile: Dockerfile
container_name: imeeting-frontend
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- PORT=3001
- BACKEND_URL=http://host.docker.internal:8001
volumes:
# 挂载日志目录到宿主机
- ./logs:/app/logs
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health", "||", "exit", "1"]
interval: 30s
timeout: 10s
retries: 3

View File

@ -0,0 +1,21 @@
{
"name": "imeeting-frontend",
"script": "server.js",
"instances": 2,
"exec_mode": "cluster",
"env": {
"NODE_ENV": "production",
"PORT": 3001
},
"error_file": "./logs/err.log",
"out_file": "./logs/out.log",
"log_file": "./logs/combined.log",
"time": true,
"log_date_format": "YYYY-MM-DD HH:mm:ss Z",
"max_memory_restart": "1G",
"restart_delay": 4000,
"autorestart": true,
"watch": false,
"max_restarts": 10,
"min_uptime": "10s"
}

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>会议助手</title>
<title>会议</title>
</head>
<body>
<div id="root"></div>

4665
package-lock.json generated

File diff suppressed because it is too large Load Diff

13
package.prod.json 100644
View File

@ -0,0 +1,13 @@
{
"name": "imeeting-frontend-server",
"version": "1.0.0",
"type": "commonjs",
"main": "server.js",
"dependencies": {
"express": "4.18.2",
"http-proxy-middleware": "2.0.6"
},
"engines": {
"node": ">=18.0.0"
}
}

55
server.js 100644
View File

@ -0,0 +1,55 @@
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const path = require("path");
const app = express();
const PORT = process.env.PORT;
const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000";
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// API代理 - 需要在body parser之前
app.use("/api", createProxyMiddleware({
target: BACKEND_URL,
changeOrigin: true,
timeout: 30000,
proxyTimeout: 30000
}));
// 上传文件代理
app.use("/uploads", createProxyMiddleware({
target: BACKEND_URL,
changeOrigin: true,
timeout: 30000,
proxyTimeout: 30000
}));
// 设置请求体大小限制 - 放在代理之后
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
// 静态文件服务
app.use(express.static(path.join(__dirname, "dist"), {
maxAge: '1d',
etag: true
}));
// SPA路由处理 - 必须放在最后
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "dist", "index.html"));
});
// 错误处理中间件
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`iMeeting Frontend Server running on port ${PORT}`);
console.log(`Backend proxy target: ${BACKEND_URL}`);
console.log(`Environment: ${process.env.NODE_ENV}`);
});

View File

@ -160,9 +160,8 @@ const HomePage = ({ onLogin }) => {
</form>
<div className="demo-info">
<p>测试账号</p>
<p>用户名: user1, 密码: hashed_password_1</p>
<p>用户名: user2, 密码: hashed_password_2</p>
<p>开通账号</p>
<p>请联系18980500203</p>
</div>
</div>
</div>

View File

@ -1612,7 +1612,6 @@
min-height: 80px;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 12px;
font-family: inherit;
font-size: 14px;
line-height: 1.5;

View File

@ -260,9 +260,17 @@ const MeetingDetails = ({ user }) => {
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
if (hours > 0) {
// 60::
return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
// 60:
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
};
const handlePlayPause = () => {
@ -600,19 +608,14 @@ const MeetingDetails = ({ user }) => {
return;
}
// ReactMarkdown
// Markdown
const tempDiv = document.createElement('div');
tempDiv.style.position = 'fixed';
tempDiv.style.top = '-9999px';
tempDiv.style.width = '800px';
tempDiv.style.padding = '20px';
tempDiv.style.backgroundColor = 'white';
tempDiv.style.display = 'none';
document.body.appendChild(tempDiv);
// markdown-to-html
const ReactMarkdown = (await import('react-markdown')).default;
const { createRoot } = await import('react-dom/client');
document.body.appendChild(tempDiv);
const root = createRoot(tempDiv);
// MarkdownHTML
@ -624,101 +627,151 @@ const MeetingDetails = ({ user }) => {
children: summaryContent
})
);
setTimeout(resolve, 100); //
setTimeout(resolve, 100);
});
const renderedHTML = tempDiv.innerHTML;
// HTMLPDF
const printContainer = document.createElement('div');
printContainer.style.position = 'fixed';
printContainer.style.top = '-9999px';
printContainer.style.width = '210mm';
printContainer.style.padding = '20mm';
printContainer.style.backgroundColor = 'white';
printContainer.style.fontFamily = 'Arial, sans-serif';
printContainer.style.fontSize = '14px';
printContainer.style.lineHeight = '1.6';
printContainer.style.color = '#333';
// PDFHTML使Markdown
//
const meetingTime = formatDateTime(meeting.meeting_time);
const attendeesList = meeting.attendees.map(attendee =>
typeof attendee === 'string' ? attendee : attendee.caption
).join('、');
printContainer.innerHTML = `
<div>
<h1 style="color: #2563eb; margin-bottom: 30px; font-size: 24px; border-bottom: 2px solid #e5e7eb; padding-bottom: 10px;">
${meeting.title || '会议总结'}
</h1>
// iframe
const printFrame = document.createElement('iframe');
printFrame.style.position = 'fixed';
printFrame.style.width = '0';
printFrame.style.height = '0';
printFrame.style.border = 'none';
printFrame.style.left = '-9999px';
document.body.appendChild(printFrame);
const printDocument = printFrame.contentWindow.document;
// 使BlobURL
const htmlContent = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>${meeting.title || '会议总结'}</title>
<style>
@charset "UTF-8";
@page { size: A4; margin: 20mm; }
body {
font-family: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
margin: 0;
padding: 20px;
}
h1 {
color: #2563eb;
font-size: 24px;
margin-bottom: 30px;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 10px;
}
h2 {
color: #374151;
font-size: 18px;
margin: 25px 0 15px;
}
h3 {
color: #1e293b;
font-size: 16px;
margin: 20px 0 10px;
}
p {
margin: 10px 0;
color: #475569;
}
ul, ol {
margin: 10px 0;
padding-left: 25px;
}
li {
margin: 5px 0;
color: #475569;
}
strong {
color: #1e293b;
font-weight: 600;
}
.info-section {
background: #f9fafb;
padding: 20px;
margin-bottom: 30px;
border-radius: 8px;
}
.info-section h2 {
margin-top: 0;
}
.content-section {
margin-bottom: 30px;
}
.footer-section {
margin-top: 50px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 12px;
color: #6b7280;
}
@media print {
body { padding: 0; }
h1 { page-break-before: avoid; }
h2 { page-break-before: avoid; }
}
</style>
</head>
<body>
<h1>${meeting.title || '会议总结'}</h1>
<div class="info-section">
<h2>会议信息</h2>
<p><strong>会议时间</strong>${meetingTime}</p>
<p><strong>创建人</strong>${meeting.creator_username}</p>
<p><strong>参会人数</strong>${meeting.attendees.length}</p>
<p><strong>参会人员</strong>${attendeesList}</p>
</div>
<div class="content-section">
<h2>会议摘要</h2>
${renderedHTML}
</div>
<div class="footer-section">
<p>导出时间${new Date().toLocaleString('zh-CN')}</p>
</div>
</body>
</html>`;
// 使BlobURL
const blob = new Blob([htmlContent], { type: 'text/html; charset=UTF-8' });
const url = URL.createObjectURL(blob);
// iframesrcblob URL
printFrame.src = url;
// iframe
printFrame.onload = () => {
setTimeout(() => {
//
printFrame.contentWindow.focus();
printFrame.contentWindow.print();
<div style="margin-bottom: 30px; background: #f9fafb; padding: 20px; border-radius: 8px;">
<h2 style="color: #374151; font-size: 16px; margin-bottom: 15px;">会议信息</h2>
<p style="margin: 8px 0;"><strong>会议时间</strong>${meetingTime}</p>
<p style="margin: 8px 0;"><strong>创建人</strong>${meeting.creator_username}</p>
<p style="margin: 8px 0;"><strong>参会人数</strong>${meeting.attendees.length}</p>
<p style="margin: 8px 0;"><strong>参会人员</strong>${attendeesList}</p>
</div>
<div style="margin-bottom: 30px;">
<h2 style="color: #374151; font-size: 16px; margin-bottom: 15px;">会议摘要</h2>
<div style="line-height: 1.8;">${renderedHTML}</div>
</div>
<div style="margin-top: 50px; padding-top: 20px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #6b7280;">
<p>导出时间${new Date().toLocaleString('zh-CN')}</p>
</div>
</div>
`;
document.body.appendChild(printContainer);
// 使PDF
const originalContent = document.body.innerHTML;
const originalTitle = document.title;
//
document.body.innerHTML = printContainer.innerHTML;
document.title = `${meeting.title || '会议总结'}_${new Date().toISOString().split('T')[0]}`;
//
const printStyles = document.createElement('style');
printStyles.innerHTML = `
@media print {
body { margin: 0; padding: 20px; font-family: 'Microsoft YaHei', Arial, sans-serif; }
h1 { page-break-before: avoid; }
h2 { page-break-before: avoid; }
h3 { margin-top: 1.5rem; margin-bottom: 0.75rem; color: #1e293b; }
h4 { margin-top: 1rem; margin-bottom: 0.5rem; color: #1e293b; }
p { margin-bottom: 0.75rem; color: #475569; line-height: 1.6; }
ul, ol { margin: 0.75rem 0; padding-left: 1.5rem; }
li { margin-bottom: 0.25rem; color: #475569; }
strong { color: #1e293b; font-weight: 600; }
code { background: #f1f5f9; padding: 2px 4px; border-radius: 3px; color: #dc2626; }
.page-break { page-break-before: always; }
}
`;
document.head.appendChild(printStyles);
//
window.print();
//
setTimeout(() => {
document.body.innerHTML = originalContent;
document.title = originalTitle;
document.head.removeChild(printStyles);
document.body.removeChild(printContainer);
document.body.removeChild(tempDiv);
// React
window.location.reload();
}, 1000);
//
setTimeout(() => {
URL.revokeObjectURL(url);
root.unmount();
document.body.removeChild(tempDiv);
document.body.removeChild(printFrame);
}, 2000);
}, 500);
};
} catch (error) {
console.error('PDF导出失败:', error);
alert('PDF导出失败请重试。建议使用浏览器的打印功能并选择"保存为PDF"。');
alert('PDF导出失败请重试。');
}
};

View File

@ -6,7 +6,6 @@ export default defineConfig({
plugins: [react()],
server: {
host: true, // Optional: Allows the server to be accessible externally
port: 5173, // Optional: Specify a port if needed
allowedHosts: ['imeeting.unisspace.com'], // Add the problematic hostname here
proxy: {
'/api': {