v1.0.2
parent
ff2eeee5a3
commit
df96e75e7f
|
|
@ -0,0 +1,8 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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 " ✅ 进程监控和健康检查"
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
// 创建一个临时的React容器用于渲染Markdown
|
||||
// 先渲染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);
|
||||
|
||||
// 渲染Markdown内容并获取HTML
|
||||
|
|
@ -624,101 +627,151 @@ const MeetingDetails = ({ user }) => {
|
|||
children: summaryContent
|
||||
})
|
||||
);
|
||||
setTimeout(resolve, 100); // 等待渲染完成
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
const renderedHTML = tempDiv.innerHTML;
|
||||
|
||||
// 创建一个隐藏的HTML容器用于生成PDF
|
||||
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';
|
||||
|
||||
// 创建PDF内容的HTML,使用渲染后的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;
|
||||
|
||||
// 使用Blob和URL来确保编码正确
|
||||
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>`;
|
||||
|
||||
// 使用Blob创建URL以确保正确的编码
|
||||
const blob = new Blob([htmlContent], { type: 'text/html; charset=UTF-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// 设置iframe的src为blob 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导出失败,请重试。');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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': {
|
||||
|
|
|
|||
Loading…
Reference in New Issue