Compare commits
2 Commits
ff2eeee5a3
...
eb035d6d35
| Author | SHA1 | Date |
|---|---|---|
|
|
eb035d6d35 | |
|
|
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;
|
||||
|
|
@ -1677,3 +1676,144 @@
|
|||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
/* AI总结进度条样式 */
|
||||
.summary-progress-section {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 8px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-animate {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
animation: progress-shine 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes progress-shine {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.progress-message .status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.progress-message .status-indicator.processing {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 20px 0;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
border-left: 4px solid #3b82f6;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #1e40af;
|
||||
animation: slideInFromTop 0.3s ease-out;
|
||||
}
|
||||
|
||||
.success-message svg {
|
||||
color: #3b82f6;
|
||||
animation: sparkle 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes slideInFromTop {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0%, 100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 更新生成按钮在加载时的样式 */
|
||||
.generate-summary-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
.generate-summary-btn:disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ const MeetingDetails = ({ user }) => {
|
|||
const [userPrompt, setUserPrompt] = useState('');
|
||||
const [summaryHistory, setSummaryHistory] = useState([]);
|
||||
const [currentHighlightIndex, setCurrentHighlightIndex] = useState(-1);
|
||||
const [summaryTaskId, setSummaryTaskId] = useState(null);
|
||||
const [summaryTaskStatus, setSummaryTaskStatus] = useState(null);
|
||||
const [summaryTaskProgress, setSummaryTaskProgress] = useState(0);
|
||||
const [summaryTaskMessage, setSummaryTaskMessage] = useState('');
|
||||
const [summaryPollInterval, setSummaryPollInterval] = useState(null);
|
||||
const audioRef = useRef(null);
|
||||
const transcriptRefs = useRef([]);
|
||||
|
||||
|
|
@ -55,6 +60,11 @@ const MeetingDetails = ({ user }) => {
|
|||
clearInterval(statusCheckInterval);
|
||||
setStatusCheckInterval(null);
|
||||
}
|
||||
if (summaryPollInterval) {
|
||||
console.log('组件卸载,清理总结任务轮询定时器');
|
||||
clearInterval(summaryPollInterval);
|
||||
setSummaryPollInterval(null);
|
||||
}
|
||||
};
|
||||
}, [meeting_id]);
|
||||
|
||||
|
|
@ -260,9 +270,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 = () => {
|
||||
|
|
@ -536,40 +554,147 @@ const MeetingDetails = ({ user }) => {
|
|||
}
|
||||
};
|
||||
|
||||
// AI总结相关函数
|
||||
// AI总结相关函数 - 使用异步API
|
||||
const generateSummary = async () => {
|
||||
if (summaryLoading) return;
|
||||
|
||||
setSummaryLoading(true);
|
||||
setSummaryTaskProgress(0);
|
||||
setSummaryTaskMessage('正在启动AI分析...');
|
||||
setSummaryTaskStatus('pending');
|
||||
|
||||
try {
|
||||
const baseUrl = "";
|
||||
const response = await apiClient.post(`${baseUrl}/api/meetings/${meeting_id}/generate-summary`, {
|
||||
// 使用异步API
|
||||
const response = await apiClient.post(`${baseUrl}/api/meetings/${meeting_id}/generate-summary-async`, {
|
||||
user_prompt: userPrompt
|
||||
});
|
||||
|
||||
setSummaryResult(response.data);
|
||||
const taskId = response.data.task_id;
|
||||
setSummaryTaskId(taskId);
|
||||
|
||||
// 刷新总结历史
|
||||
await fetchSummaryHistory();
|
||||
// 开始轮询任务状态
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const statusResponse = await apiClient.get(`${baseUrl}/api/llm-tasks/${taskId}/status`);
|
||||
const status = statusResponse.data;
|
||||
|
||||
setSummaryTaskStatus(status.status);
|
||||
setSummaryTaskProgress(status.progress || 0);
|
||||
setSummaryTaskMessage(status.message || '处理中...');
|
||||
|
||||
if (status.status === 'completed') {
|
||||
clearInterval(interval);
|
||||
setSummaryPollInterval(null);
|
||||
|
||||
// 设置结果
|
||||
setSummaryResult({
|
||||
content: status.result,
|
||||
task_id: taskId
|
||||
});
|
||||
|
||||
// 刷新总结历史(包含所有任务)
|
||||
await fetchSummaryHistory();
|
||||
|
||||
// 刷新会议摘要
|
||||
await refreshMeetingSummary();
|
||||
|
||||
setSummaryLoading(false);
|
||||
setSummaryTaskMessage('AI总结生成成功!');
|
||||
|
||||
// 3秒后清除成功消息
|
||||
setTimeout(() => {
|
||||
setSummaryTaskMessage('');
|
||||
setSummaryTaskProgress(0);
|
||||
}, 3000);
|
||||
|
||||
} else if (status.status === 'failed') {
|
||||
clearInterval(interval);
|
||||
setSummaryPollInterval(null);
|
||||
setSummaryLoading(false);
|
||||
setError(status.error_message || '生成AI总结失败');
|
||||
setSummaryTaskMessage('生成失败:' + (status.error_message || '未知错误'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error polling task status:', err);
|
||||
// 继续轮询,不中断
|
||||
}
|
||||
}, 3000); // 每3秒查询一次
|
||||
|
||||
// 只刷新会议摘要部分,避免整页刷新
|
||||
await refreshMeetingSummary();
|
||||
setSummaryPollInterval(interval);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error generating summary:', err);
|
||||
setError('生成AI总结失败,请重试');
|
||||
} finally {
|
||||
console.error('Error starting summary generation:', err);
|
||||
setError('启动AI总结失败,请重试');
|
||||
setSummaryLoading(false);
|
||||
setSummaryTaskMessage('');
|
||||
setSummaryTaskProgress(0);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSummaryHistory = async () => {
|
||||
try {
|
||||
const baseUrl = "";
|
||||
const response = await apiClient.get(`${baseUrl}/api/meetings/${meeting_id}/summaries`);
|
||||
setSummaryHistory(response.data.summaries);
|
||||
// 获取所有LLM任务历史(包含进度和状态)
|
||||
const tasksResponse = await apiClient.get(`${baseUrl}/api/meetings/${meeting_id}/llm-tasks`);
|
||||
const tasks = tasksResponse.data.tasks || [];
|
||||
|
||||
// 转换为历史记录格式,包含任务信息
|
||||
const summaries = tasks
|
||||
.filter(task => task.status === 'completed' && task.result)
|
||||
.map(task => ({
|
||||
id: task.task_id,
|
||||
content: task.result,
|
||||
user_prompt: task.user_prompt,
|
||||
created_at: task.created_at,
|
||||
task_info: {
|
||||
task_id: task.task_id,
|
||||
status: task.status,
|
||||
progress: task.progress
|
||||
}
|
||||
}));
|
||||
|
||||
setSummaryHistory(summaries);
|
||||
|
||||
// 如果有进行中的任务,恢复轮询
|
||||
const runningTask = tasks.find(task => ['pending', 'processing'].includes(task.status));
|
||||
if (runningTask && !summaryPollInterval) {
|
||||
setSummaryTaskId(runningTask.task_id);
|
||||
setSummaryTaskStatus(runningTask.status);
|
||||
setSummaryTaskProgress(runningTask.progress || 0);
|
||||
setSummaryLoading(true);
|
||||
|
||||
// 恢复轮询
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const statusResponse = await apiClient.get(`${baseUrl}/api/llm-tasks/${runningTask.task_id}/status`);
|
||||
const status = statusResponse.data;
|
||||
|
||||
setSummaryTaskStatus(status.status);
|
||||
setSummaryTaskProgress(status.progress || 0);
|
||||
setSummaryTaskMessage(status.message || '处理中...');
|
||||
|
||||
if (['completed', 'failed'].includes(status.status)) {
|
||||
clearInterval(interval);
|
||||
setSummaryPollInterval(null);
|
||||
setSummaryLoading(false);
|
||||
|
||||
if (status.status === 'completed') {
|
||||
await fetchSummaryHistory();
|
||||
await refreshMeetingSummary();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error polling task status:', err);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
setSummaryPollInterval(interval);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error fetching summary history:', err);
|
||||
setSummaryHistory([]);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -600,19 +725,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 +744,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导出失败,请重试。');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1264,6 +1434,38 @@ const MeetingDetails = ({ user }) => {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* 任务进度显示 */}
|
||||
{summaryLoading && (
|
||||
<div className="summary-progress-section">
|
||||
<div className="progress-header">
|
||||
<span className="progress-title">生成进度</span>
|
||||
<span className="progress-percentage">{summaryTaskProgress}%</span>
|
||||
</div>
|
||||
<div className="progress-bar-container">
|
||||
<div
|
||||
className="progress-bar-fill"
|
||||
style={{ width: `${summaryTaskProgress}%` }}
|
||||
>
|
||||
<div className="progress-bar-animate"></div>
|
||||
</div>
|
||||
</div>
|
||||
{summaryTaskMessage && (
|
||||
<div className="progress-message">
|
||||
<div className="status-indicator processing"></div>
|
||||
<span>{summaryTaskMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 成功消息 */}
|
||||
{!summaryLoading && summaryTaskMessage && summaryTaskStatus === 'completed' && (
|
||||
<div className="success-message">
|
||||
<Sparkles size={16} />
|
||||
<span>{summaryTaskMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{summaryResult && (
|
||||
<div className="summary-result-section">
|
||||
<div className="summary-result-header">
|
||||
|
|
|
|||
|
|
@ -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