import express from 'express'; import githubService from '../services/githubService.js'; import memoryStorageService from '../services/memoryStorageService.js'; import screenshotService from '../services/screenshotService.js'; const router = express.Router(); // 选择存储服务 const getStorageService = () => { // 如果GitHub Token未配置,使用内存存储 if (!process.env.GITHUB_TOKEN) { return memoryStorageService; } return githubService; }; // 生成HTML页面来显示PPT幻灯片 const generateSlideHTML = (pptData, slideIndex, title) => { const slide = pptData.slides[slideIndex]; const theme = pptData.theme || {}; // 精确计算PPT内容的真实尺寸 - 优先使用预设尺寸,然后基于元素计算 const calculatePptDimensions = (slide) => { // 1. 优先使用PPT数据中的预设尺寸 if (pptData.slideSize && pptData.slideSize.width && pptData.slideSize.height) { const result = { width: Math.ceil(pptData.slideSize.width), height: Math.ceil(pptData.slideSize.height) }; console.log(`使用PPT预设尺寸: ${result.width}x${result.height}`); return result; } // 2. 使用slide级别的尺寸设置 if (slide.width && slide.height) { const result = { width: Math.ceil(slide.width), height: Math.ceil(slide.height) }; console.log(`使用slide尺寸: ${result.width}x${result.height}`); return result; } // 3. 根据元素分布计算合适的尺寸 if (!slide.elements || slide.elements.length === 0) { // 如果没有元素,使用标准PPT尺寸 const result = { width: 960, height: 720 }; // 4:3标准比例 console.log(`使用默认尺寸: ${result.width}x${result.height}`); return result; } let maxRight = 0; let maxBottom = 0; let minLeft = Infinity; let minTop = Infinity; // 计算所有元素的实际边界 slide.elements.forEach(element => { const left = element.left || 0; const top = element.top || 0; const width = element.width || 0; const height = element.height || 0; const elementRight = left + width; const elementBottom = top + height; maxRight = Math.max(maxRight, elementRight); maxBottom = Math.max(maxBottom, elementBottom); minLeft = Math.min(minLeft, left); minTop = Math.min(minTop, top); }); // 重置无限值 if (minLeft === Infinity) minLeft = 0; if (minTop === Infinity) minTop = 0; // 计算内容区域的实际尺寸 const contentWidth = maxRight - minLeft; const contentHeight = maxBottom - minTop; // 添加适当的边距,确保内容不被裁切 const paddingX = Math.max(40, Math.abs(minLeft)); const paddingY = Math.max(40, Math.abs(minTop)); // 计算最终的PPT尺寸 let finalWidth = Math.max(contentWidth + paddingX * 2, 800); let finalHeight = Math.max(contentHeight + paddingY * 2, 600); // 确保尺寸为偶数(避免半像素问题) finalWidth = Math.ceil(finalWidth / 2) * 2; finalHeight = Math.ceil(finalHeight / 2) * 2; const result = { width: finalWidth, height: finalHeight }; console.log(`根据元素计算尺寸: ${result.width}x${result.height}`); return result; }; const pptDimensions = calculatePptDimensions(slide); // 渲染幻灯片元素 const renderElements = (elements) => { if (!elements || elements.length === 0) return ''; return elements.map(element => { const style = ` position: absolute; left: ${element.left}px; top: ${element.top}px; width: ${element.width}px; height: ${element.height}px; transform: rotate(${element.rotate || 0}deg); z-index: ${element.zIndex || 1}; `; switch (element.type) { case 'text': return `
${element.content || ''}
`; case 'image': return `
`; case 'shape': const shapeStyle = element.fill ? `background-color: ${element.fill};` : ''; const borderStyle = element.outline ? `border: ${element.outline.width || 1}px ${element.outline.style || 'solid'} ${element.outline.color || '#000'};` : ''; return `
`; default: return `
`; } }).join(''); }; return ` ${title} - 第${slideIndex + 1}页
${renderElements(slide.elements)}
`; }; // 公开访问PPT页面 - 返回HTML页面 router.get('/view/:userId/:pptId/:slideIndex?', async (req, res, next) => { try { const { userId, pptId, slideIndex = 0 } = req.params; const querySlideIndex = req.query.slide ? parseInt(req.query.slide) : parseInt(slideIndex); const fileName = `${pptId}.json`; const storageService = getStorageService(); let pptData = null; // 如果是GitHub服务,尝试所有仓库 if (storageService === githubService && storageService.repositories) { for (let i = 0; i < storageService.repositories.length; i++) { try { const result = await storageService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } } else { // 内存存储服务 const result = await storageService.getFile(userId, fileName); if (result) { pptData = result.content; } } if (!pptData) { return res.status(404).send(` PPT未找到

PPT未找到

请检查链接是否正确

`); } const slideIdx = querySlideIndex; if (slideIdx >= pptData.slides.length || slideIdx < 0) { return res.status(404).send(` 幻灯片未找到

幻灯片未找到

请检查幻灯片索引是否正确

`); } // 返回HTML页面 const htmlContent = generateSlideHTML(pptData, slideIdx, pptData.title); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(htmlContent); } catch (error) { next(error); } }); // API接口:获取PPT数据和指定幻灯片(JSON格式) router.get('/api-view/:userId/:pptId/:slideIndex?', async (req, res, next) => { try { const { userId, pptId, slideIndex = 0 } = req.params; const fileName = `${pptId}.json`; const storageService = getStorageService(); let pptData = null; // 如果是GitHub服务,尝试所有仓库 if (storageService === githubService && storageService.repositories) { for (let i = 0; i < storageService.repositories.length; i++) { try { const result = await storageService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } } else { // 内存存储服务 const result = await storageService.getFile(userId, fileName); if (result) { pptData = result.content; } } if (!pptData) { return res.status(404).json({ error: 'PPT not found' }); } const slideIdx = parseInt(slideIndex); if (slideIdx >= pptData.slides.length || slideIdx < 0) { return res.status(404).json({ error: 'Slide not found' }); } // 返回PPT数据和指定幻灯片 res.json({ id: pptData.id, title: pptData.title, theme: pptData.theme, currentSlide: pptData.slides[slideIdx], slideIndex: slideIdx, totalSlides: pptData.slides.length, isPublicView: true }); } catch (error) { next(error); } }); // 获取完整PPT数据(只读模式) router.get('/ppt/:userId/:pptId', async (req, res, next) => { try { const { userId, pptId } = req.params; const fileName = `${pptId}.json`; const storageService = getStorageService(); let pptData = null; // 如果是GitHub服务,尝试所有仓库 if (storageService === githubService && storageService.repositories) { for (let i = 0; i < storageService.repositories.length; i++) { try { const result = await storageService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } } else { // 内存存储服务 const result = await storageService.getFile(userId, fileName); if (result) { pptData = result.content; } } if (!pptData) { return res.status(404).json({ error: 'PPT not found' }); } // 返回只读版本的PPT数据 res.json({ ...pptData, isPublicView: true, readOnly: true }); } catch (error) { next(error); } }); // 生成PPT分享链接 router.post('/generate-share-link', async (req, res, next) => { try { const { userId, pptId, slideIndex = 0 } = req.body; if (!userId || !pptId) { return res.status(400).json({ error: 'User ID and PPT ID are required' }); } console.log(`Generating share link for PPT: ${pptId}, User: ${userId}`); // 验证PPT是否存在 const fileName = `${pptId}.json`; const storageService = getStorageService(); let pptExists = false; let pptData = null; console.log(`Using storage service: ${storageService === githubService ? 'GitHub' : 'Memory'}`); // 如果是GitHub服务,尝试所有仓库 if (storageService === githubService && storageService.repositories) { console.log(`Checking ${storageService.repositories.length} GitHub repositories...`); for (let i = 0; i < storageService.repositories.length; i++) { try { console.log(`Checking repository ${i}: ${storageService.repositories[i]}`); const result = await storageService.getFile(userId, fileName, i); if (result) { pptExists = true; pptData = result.content; console.log(`PPT found in repository ${i}`); break; } } catch (error) { console.log(`PPT not found in repository ${i}: ${error.message}`); continue; } } } else { // 内存存储服务 console.log('Checking memory storage...'); try { const result = await storageService.getFile(userId, fileName); if (result) { pptExists = true; pptData = result.content; console.log('PPT found in memory storage'); } } catch (error) { console.log(`PPT not found in memory storage: ${error.message}`); } } if (!pptExists) { console.log('PPT not found in any storage location'); // 额外尝试:如果GitHub失败,检查memory storage作为fallback if (storageService === githubService) { console.log('GitHub lookup failed, trying memory storage fallback...'); try { const memoryResult = await memoryStorageService.getFile(userId, fileName); if (memoryResult) { pptExists = true; pptData = memoryResult.content; console.log('PPT found in memory storage fallback'); } } catch (memoryError) { console.log(`Memory storage fallback also failed: ${memoryError.message}`); } } } if (!pptExists) { return res.status(404).json({ error: 'PPT not found', details: `PPT ${pptId} not found for user ${userId}`, searchedLocations: storageService === githubService ? 'GitHub repositories and memory storage' : 'Memory storage only' }); } const baseUrl = process.env.PUBLIC_URL || req.get('host'); const protocol = process.env.NODE_ENV === 'production' ? 'https' : req.protocol; const shareLinks = { // 单页分享链接 slideUrl: `${protocol}://${baseUrl}/api/public/view/${userId}/${pptId}/${slideIndex}`, // 完整PPT分享链接 pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`, // 前端查看链接 viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`, // 新增:截图链接 screenshotUrl: `${protocol}://${baseUrl}/api/public/screenshot/${userId}/${pptId}/${slideIndex}`, // 添加PPT信息 pptInfo: { id: pptId, title: pptData?.title || 'Unknown Title', slideCount: pptData?.slides?.length || 0 } }; console.log('Share links generated successfully:', shareLinks); res.json(shareLinks); } catch (error) { console.error('Share link generation error:', error); next(error); } }); // 截图功能 - 返回JPEG图片 router.get('/screenshot/:userId/:pptId/:slideIndex?', async (req, res, next) => { try { console.log('Screenshot request received:', req.params); const { userId, pptId, slideIndex = 0 } = req.params; const slideIdx = parseInt(slideIndex); const fileName = `${pptId}.json`; const storageService = getStorageService(); console.log(`Generating screenshot for: ${userId}/${pptId}/${slideIdx}`); let pptData = null; // 获取PPT数据(复用现有逻辑) if (storageService === githubService && storageService.repositories) { console.log('Checking GitHub repositories...'); for (let i = 0; i < storageService.repositories.length; i++) { try { const result = await storageService.getFile(userId, fileName, i); if (result) { pptData = result.content; console.log(`PPT data found in repository ${i}`); break; } } catch (error) { continue; } } } else { console.log('Checking memory storage...'); const result = await storageService.getFile(userId, fileName); if (result) { pptData = result.content; console.log('PPT data found in memory storage'); } } // 如果GitHub失败,尝试内存存储fallback if (!pptData && storageService === githubService) { console.log('Trying memory storage fallback...'); try { const memoryResult = await memoryStorageService.getFile(userId, fileName); if (memoryResult) { pptData = memoryResult.content; console.log('PPT data found in memory storage fallback'); } } catch (memoryError) { console.log('Memory storage fallback failed:', memoryError.message); } } if (!pptData) { console.log('PPT not found'); return res.status(404).json({ error: 'PPT not found' }); } if (slideIdx >= pptData.slides.length || slideIdx < 0) { console.log('Slide index out of bounds'); return res.status(404).json({ error: 'Slide not found' }); } console.log('Generating HTML content...'); // 生成HTML内容(复用现有函数) const htmlContent = generateSlideHTML(pptData, slideIdx, pptData.title); console.log('Calling screenshot service...'); // 生成截图 const screenshot = await screenshotService.generateScreenshot(htmlContent); console.log('Screenshot generated, sending response...'); // 返回图片 res.setHeader('Content-Type', 'image/jpeg'); res.setHeader('Cache-Control', 'public, max-age=60'); // 1分钟缓存,减少服务器压力 res.setHeader('Content-Disposition', `inline; filename="${pptData.title}-${slideIdx + 1}.jpg"`); res.send(screenshot); } catch (error) { console.error('Screenshot route error:', error); console.error('Stack trace:', error.stack); // 返回一个简单的错误响应而不是抛出异常 try { res.status(500).setHeader('Content-Type', 'application/json'); res.json({ error: 'Screenshot generation failed', message: error.message, details: process.env.NODE_ENV === 'development' ? error.stack : undefined }); } catch (responseError) { console.error('Error sending error response:', responseError); // 如果连错误响应都发送失败,调用next next(error); } } }); export default router;