CatPtain commited on
Commit
b7560a4
·
verified ·
1 Parent(s): d0bcced

Upload 10 files

Browse files
backend/package.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "pptist-backend",
3
+ "version": "1.0.0",
4
+ "description": "PPTist Backend Server for Huggingface Space",
5
+ "main": "src/app.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "node src/app.js",
9
+ "start": "node src/app.js",
10
+ "build": "echo 'No build step required'"
11
+ },
12
+ "dependencies": {
13
+ "express": "^4.18.2",
14
+ "cors": "^2.8.5",
15
+ "helmet": "^7.1.0",
16
+ "bcryptjs": "^2.4.3",
17
+ "jsonwebtoken": "^9.0.2",
18
+ "multer": "^1.4.5-lts.1",
19
+ "axios": "^1.6.0",
20
+ "dotenv": "^16.3.1",
21
+ "express-rate-limit": "^7.1.5",
22
+ "uuid": "^9.0.1"
23
+ },
24
+ "devDependencies": {
25
+ "nodemon": "^3.0.2"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ }
30
+ }
backend/src/app.js ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import helmet from 'helmet';
4
+ import rateLimit from 'express-rate-limit';
5
+ import dotenv from 'dotenv';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ import authRoutes from './routes/auth.js';
10
+ import pptRoutes from './routes/ppt.js';
11
+ import publicRoutes from './routes/public.js';
12
+ import { authenticateToken } from './middleware/auth.js';
13
+ import { errorHandler } from './middleware/errorHandler.js';
14
+
15
+ dotenv.config();
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+
20
+ const app = express();
21
+ const PORT = process.env.PORT || 7860; // 修改为7860端口
22
+
23
+ // 安全中间件
24
+ app.use(helmet({
25
+ contentSecurityPolicy: false, // 为了兼容前端静态文件
26
+ }));
27
+
28
+ // 限流中间件
29
+ const limiter = rateLimit({
30
+ windowMs: 15 * 60 * 1000, // 15分钟
31
+ max: 100, // 每个IP每15分钟最多100个请求
32
+ message: 'Too many requests from this IP, please try again later.'
33
+ });
34
+ app.use('/api', limiter);
35
+
36
+ // CORS配置
37
+ app.use(cors({
38
+ origin: process.env.FRONTEND_URL || '*',
39
+ credentials: true
40
+ }));
41
+
42
+ app.use(express.json({ limit: '50mb' }));
43
+ app.use(express.urlencoded({ extended: true, limit: '50mb' }));
44
+
45
+ // 提供前端静态文件
46
+ app.use(express.static(path.join(__dirname, '../../frontend/dist')));
47
+
48
+ // API路由
49
+ app.use('/api/auth', authRoutes);
50
+ app.use('/api/ppt', authenticateToken, pptRoutes);
51
+ app.use('/api/public', publicRoutes);
52
+
53
+ // 健康检查
54
+ app.get('/api/health', (req, res) => {
55
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
56
+ });
57
+
58
+ // 前端路由处理 - 必须在API路由之后
59
+ app.get('*', (req, res) => {
60
+ res.sendFile(path.join(__dirname, '../../frontend/dist/index.html'));
61
+ });
62
+
63
+ // 错误处理中间件
64
+ app.use(errorHandler);
65
+
66
+ app.listen(PORT, '0.0.0.0', () => {
67
+ console.log(`Server is running on port ${PORT}`);
68
+ console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
69
+ });
backend/src/config/users.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 内置用户配置
2
+ export const USERS = [
3
+ {
4
+ id: 'PS01',
5
+ username: 'PS01',
6
+ password: 'admin_cybercity2025',
7
+ role: 'admin'
8
+ },
9
+ {
10
+ id: 'PS02',
11
+ username: 'PS02',
12
+ password: 'cybercity2025',
13
+ role: 'user'
14
+ },
15
+ {
16
+ id: 'PS03',
17
+ username: 'PS03',
18
+ password: 'cybercity2025',
19
+ role: 'user'
20
+ },
21
+ {
22
+ id: 'PS04',
23
+ username: 'PS04',
24
+ password: 'cybercity2025',
25
+ role: 'user'
26
+ }
27
+ ];
28
+
29
+ // JWT配置
30
+ export const JWT_SECRET = process.env.JWT_SECRET || 'pptist-secret-key-2025';
31
+ export const JWT_EXPIRES_IN = '24h';
32
+
33
+ // GitHub配置
34
+ export const GITHUB_CONFIG = {
35
+ token: process.env.GITHUB_TOKEN,
36
+ repositories: (process.env.GITHUB_REPOS || 'https://github.com/CaPaCaptain/PPTist_huggingface_db').split(','),
37
+ apiUrl: 'https://api.github.com'
38
+ };
backend/src/middleware/auth.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import jwt from 'jsonwebtoken';
2
+ import { JWT_SECRET } from '../config/users.js';
3
+
4
+ export const authenticateToken = (req, res, next) => {
5
+ const authHeader = req.headers['authorization'];
6
+ const token = authHeader && authHeader.split(' ')[1];
7
+
8
+ if (!token) {
9
+ return res.status(401).json({ error: 'Access token required' });
10
+ }
11
+
12
+ jwt.verify(token, JWT_SECRET, (err, user) => {
13
+ if (err) {
14
+ return res.status(403).json({ error: 'Invalid or expired token' });
15
+ }
16
+ req.user = user;
17
+ next();
18
+ });
19
+ };
backend/src/middleware/errorHandler.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const errorHandler = (err, req, res, next) => {
2
+ console.error('Error:', err);
3
+
4
+ // JWT错误
5
+ if (err.name === 'JsonWebTokenError') {
6
+ return res.status(401).json({ error: 'Invalid token' });
7
+ }
8
+
9
+ if (err.name === 'TokenExpiredError') {
10
+ return res.status(401).json({ error: 'Token expired' });
11
+ }
12
+
13
+ // GitHub API错误
14
+ if (err.response && err.response.status) {
15
+ const status = err.response.status;
16
+ const message = err.response.data?.message || 'GitHub API error';
17
+
18
+ if (status === 401) {
19
+ return res.status(500).json({ error: 'GitHub authentication failed' });
20
+ }
21
+
22
+ if (status === 404) {
23
+ return res.status(404).json({ error: 'Resource not found in GitHub' });
24
+ }
25
+
26
+ return res.status(500).json({ error: `GitHub API error: ${message}` });
27
+ }
28
+
29
+ // 默认错误
30
+ res.status(500).json({
31
+ error: process.env.NODE_ENV === 'production'
32
+ ? 'Internal server error'
33
+ : err.message
34
+ });
35
+ };
backend/src/routes/auth.js ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import jwt from 'jsonwebtoken';
3
+ import bcrypt from 'bcryptjs';
4
+ import { USERS, JWT_SECRET, JWT_EXPIRES_IN } from '../config/users.js';
5
+
6
+ const router = express.Router();
7
+
8
+ // 登录
9
+ router.post('/login', async (req, res, next) => {
10
+ try {
11
+ const { username, password } = req.body;
12
+
13
+ if (!username || !password) {
14
+ return res.status(400).json({ error: 'Username and password are required' });
15
+ }
16
+
17
+ // 查找用户
18
+ const user = USERS.find(u => u.username === username);
19
+ if (!user) {
20
+ return res.status(401).json({ error: 'Invalid credentials' });
21
+ }
22
+
23
+ // 验证密码
24
+ if (user.password !== password) {
25
+ return res.status(401).json({ error: 'Invalid credentials' });
26
+ }
27
+
28
+ // 生成JWT token
29
+ const token = jwt.sign(
30
+ {
31
+ userId: user.id,
32
+ username: user.username,
33
+ role: user.role
34
+ },
35
+ JWT_SECRET,
36
+ { expiresIn: JWT_EXPIRES_IN }
37
+ );
38
+
39
+ res.json({
40
+ token,
41
+ user: {
42
+ id: user.id,
43
+ username: user.username,
44
+ role: user.role
45
+ }
46
+ });
47
+ } catch (error) {
48
+ next(error);
49
+ }
50
+ });
51
+
52
+ // 验证token
53
+ router.get('/verify', (req, res, next) => {
54
+ try {
55
+ const authHeader = req.headers['authorization'];
56
+ const token = authHeader && authHeader.split(' ')[1];
57
+
58
+ if (!token) {
59
+ return res.status(401).json({ error: 'No token provided' });
60
+ }
61
+
62
+ jwt.verify(token, JWT_SECRET, (err, decoded) => {
63
+ if (err) {
64
+ return res.status(401).json({ error: 'Invalid token' });
65
+ }
66
+
67
+ res.json({
68
+ user: {
69
+ id: decoded.userId,
70
+ username: decoded.username,
71
+ role: decoded.role
72
+ }
73
+ });
74
+ });
75
+ } catch (error) {
76
+ next(error);
77
+ }
78
+ });
79
+
80
+ // 获取用户信息
81
+ router.get('/user', (req, res, next) => {
82
+ try {
83
+ const authHeader = req.headers['authorization'];
84
+ const token = authHeader && authHeader.split(' ')[1];
85
+
86
+ if (!token) {
87
+ return res.status(401).json({ error: 'No token provided' });
88
+ }
89
+
90
+ jwt.verify(token, JWT_SECRET, (err, decoded) => {
91
+ if (err) {
92
+ return res.status(401).json({ error: 'Invalid token' });
93
+ }
94
+
95
+ const user = USERS.find(u => u.id === decoded.userId);
96
+ if (!user) {
97
+ return res.status(404).json({ error: 'User not found' });
98
+ }
99
+
100
+ res.json({
101
+ id: user.id,
102
+ username: user.username,
103
+ role: user.role
104
+ });
105
+ });
106
+ } catch (error) {
107
+ next(error);
108
+ }
109
+ });
110
+
111
+ export default router;
backend/src/routes/ppt.js ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import githubService from '../services/githubService.js';
4
+ import memoryStorageService from '../services/memoryStorageService.js';
5
+
6
+ const router = express.Router();
7
+
8
+ // 选择存储服务
9
+ const getStorageService = () => {
10
+ // 如果GitHub Token未配置,使用内存存储
11
+ if (!process.env.GITHUB_TOKEN) {
12
+ return memoryStorageService;
13
+ }
14
+ return githubService;
15
+ };
16
+
17
+ // 获取用户的PPT列表
18
+ router.get('/list', async (req, res, next) => {
19
+ try {
20
+ const userId = req.user.userId;
21
+ const storageService = getStorageService();
22
+ const pptList = await storageService.getUserPPTList(userId);
23
+ res.json(pptList);
24
+ } catch (error) {
25
+ next(error);
26
+ }
27
+ });
28
+
29
+ // 获取指定PPT数据
30
+ router.get('/:pptId', async (req, res, next) => {
31
+ try {
32
+ const userId = req.user.userId;
33
+ const { pptId } = req.params;
34
+ const fileName = `${pptId}.json`;
35
+ const storageService = getStorageService();
36
+
37
+ let pptData = null;
38
+
39
+ // 如果是GitHub服务,尝试所有仓库
40
+ if (storageService === githubService && storageService.repositories) {
41
+ for (let i = 0; i < storageService.repositories.length; i++) {
42
+ try {
43
+ const result = await storageService.getFile(userId, fileName, i);
44
+ if (result) {
45
+ pptData = result.content;
46
+ break;
47
+ }
48
+ } catch (error) {
49
+ continue;
50
+ }
51
+ }
52
+ } else {
53
+ // 内存存储服务
54
+ const result = await storageService.getFile(userId, fileName);
55
+ if (result) {
56
+ pptData = result.content;
57
+ }
58
+ }
59
+
60
+ if (!pptData) {
61
+ return res.status(404).json({ error: 'PPT not found' });
62
+ }
63
+
64
+ res.json(pptData);
65
+ } catch (error) {
66
+ next(error);
67
+ }
68
+ });
69
+
70
+ // 保存PPT数据
71
+ router.post('/save', async (req, res, next) => {
72
+ try {
73
+ const userId = req.user.userId;
74
+ const { pptId, title, slides, theme } = req.body;
75
+
76
+ if (!pptId || !slides) {
77
+ return res.status(400).json({ error: 'PPT ID and slides are required' });
78
+ }
79
+
80
+ const fileName = `${pptId}.json`;
81
+ const pptData = {
82
+ id: pptId,
83
+ title: title || '未命名演示文稿',
84
+ slides: slides,
85
+ theme: theme || {},
86
+ createdAt: new Date().toISOString(),
87
+ updatedAt: new Date().toISOString()
88
+ };
89
+
90
+ const storageService = getStorageService();
91
+
92
+ // 如果是GitHub服务,保存到第一个仓库
93
+ if (storageService === githubService) {
94
+ await storageService.saveFile(userId, fileName, pptData, 0);
95
+ } else {
96
+ // 内存存储服务
97
+ await storageService.saveFile(userId, fileName, pptData);
98
+ }
99
+
100
+ res.json({ message: 'PPT saved successfully', pptId });
101
+ } catch (error) {
102
+ next(error);
103
+ }
104
+ });
105
+
106
+ // 创建新PPT
107
+ router.post('/create', async (req, res, next) => {
108
+ try {
109
+ const userId = req.user.userId;
110
+ const { title } = req.body;
111
+
112
+ const pptId = uuidv4();
113
+ const fileName = `${pptId}.json`;
114
+
115
+ // 创建默认PPT数据
116
+ const defaultSlide = {
117
+ id: uuidv4(),
118
+ elements: [],
119
+ background: {
120
+ type: 'solid',
121
+ color: '#ffffff'
122
+ }
123
+ };
124
+
125
+ const pptData = {
126
+ id: pptId,
127
+ title: title || '新建演示文稿',
128
+ slides: [defaultSlide],
129
+ theme: {},
130
+ createdAt: new Date().toISOString(),
131
+ updatedAt: new Date().toISOString()
132
+ };
133
+
134
+ const storageService = getStorageService();
135
+
136
+ // 保存新创建的PPT
137
+ if (storageService === githubService) {
138
+ await storageService.saveFile(userId, fileName, pptData, 0);
139
+ } else {
140
+ await storageService.saveFile(userId, fileName, pptData);
141
+ }
142
+
143
+ res.json({
144
+ message: 'PPT created successfully',
145
+ pptId,
146
+ ppt: pptData
147
+ });
148
+ } catch (error) {
149
+ next(error);
150
+ }
151
+ });
152
+
153
+ // 删除PPT
154
+ router.delete('/:pptId', async (req, res, next) => {
155
+ try {
156
+ const userId = req.user.userId;
157
+ const { pptId } = req.params;
158
+ const fileName = `${pptId}.json`;
159
+ const storageService = getStorageService();
160
+
161
+ // 如果是GitHub服务,从所有仓库中删除
162
+ if (storageService === githubService && storageService.repositories) {
163
+ for (let i = 0; i < storageService.repositories.length; i++) {
164
+ try {
165
+ await storageService.deleteFile(userId, fileName, i);
166
+ } catch (error) {
167
+ // 继续尝试其他仓库
168
+ continue;
169
+ }
170
+ }
171
+ } else {
172
+ // 内存存储服务
173
+ await storageService.deleteFile(userId, fileName);
174
+ }
175
+
176
+ res.json({ message: 'PPT deleted successfully' });
177
+ } catch (error) {
178
+ next(error);
179
+ }
180
+ });
181
+
182
+ // 复制PPT
183
+ router.post('/:pptId/copy', async (req, res, next) => {
184
+ try {
185
+ const userId = req.user.userId;
186
+ const { pptId } = req.params;
187
+ const { title } = req.body;
188
+ const sourceFileName = `${pptId}.json`;
189
+ const storageService = getStorageService();
190
+
191
+ // 获取源PPT数据
192
+ let sourcePPT = null;
193
+ if (storageService === githubService && storageService.repositories) {
194
+ for (let i = 0; i < storageService.repositories.length; i++) {
195
+ try {
196
+ const result = await storageService.getFile(userId, sourceFileName, i);
197
+ if (result) {
198
+ sourcePPT = result.content;
199
+ break;
200
+ }
201
+ } catch (error) {
202
+ continue;
203
+ }
204
+ }
205
+ } else {
206
+ const result = await storageService.getFile(userId, sourceFileName);
207
+ if (result) {
208
+ sourcePPT = result.content;
209
+ }
210
+ }
211
+
212
+ if (!sourcePPT) {
213
+ return res.status(404).json({ error: 'Source PPT not found' });
214
+ }
215
+
216
+ // 创建新的PPT ID和数据
217
+ const newPptId = uuidv4();
218
+ const newFileName = `${newPptId}.json`;
219
+ const newPPTData = {
220
+ ...sourcePPT,
221
+ id: newPptId,
222
+ title: title || `${sourcePPT.title} - 副本`,
223
+ createdAt: new Date().toISOString(),
224
+ updatedAt: new Date().toISOString()
225
+ };
226
+
227
+ // 保存复制的PPT
228
+ if (storageService === githubService) {
229
+ await storageService.saveFile(userId, newFileName, newPPTData, 0);
230
+ } else {
231
+ await storageService.saveFile(userId, newFileName, newPPTData);
232
+ }
233
+
234
+ res.json({
235
+ message: 'PPT copied successfully',
236
+ pptId: newPptId,
237
+ ppt: newPPTData
238
+ });
239
+ } catch (error) {
240
+ next(error);
241
+ }
242
+ });
243
+
244
+ export default router;
backend/src/routes/public.js ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import githubService from '../services/githubService.js';
3
+ import memoryStorageService from '../services/memoryStorageService.js';
4
+
5
+ const router = express.Router();
6
+
7
+ // 选择存储服务
8
+ const getStorageService = () => {
9
+ // 如果GitHub Token未配置,使用内存存储
10
+ if (!process.env.GITHUB_TOKEN) {
11
+ return memoryStorageService;
12
+ }
13
+ return githubService;
14
+ };
15
+
16
+ // 生成HTML页面来显示PPT幻灯片
17
+ const generateSlideHTML = (pptData, slideIndex, title) => {
18
+ const slide = pptData.slides[slideIndex];
19
+ const theme = pptData.theme || {};
20
+
21
+ // 渲染幻灯片元素
22
+ const renderElements = (elements) => {
23
+ if (!elements || elements.length === 0) return '';
24
+
25
+ return elements.map(element => {
26
+ const style = `
27
+ position: absolute;
28
+ left: ${element.left}px;
29
+ top: ${element.top}px;
30
+ width: ${element.width}px;
31
+ height: ${element.height}px;
32
+ transform: rotate(${element.rotate || 0}deg);
33
+ z-index: ${element.zIndex || 1};
34
+ `;
35
+
36
+ switch (element.type) {
37
+ case 'text':
38
+ return `
39
+ <div style="${style}
40
+ font-size: ${element.fontSize || 14}px;
41
+ font-family: ${element.fontName || 'Arial'};
42
+ color: ${element.defaultColor || '#000'};
43
+ font-weight: ${element.bold ? 'bold' : 'normal'};
44
+ font-style: ${element.italic ? 'italic' : 'normal'};
45
+ text-decoration: ${element.underline ? 'underline' : 'none'};
46
+ text-align: ${element.align || 'left'};
47
+ line-height: ${element.lineHeight || 1.2};
48
+ padding: 10px;
49
+ word-wrap: break-word;
50
+ overflow: hidden;
51
+ ">
52
+ ${element.content || ''}
53
+ </div>
54
+ `;
55
+ case 'image':
56
+ return `
57
+ <div style="${style}">
58
+ <img src="${element.src}" alt="" style="width: 100%; height: 100%; object-fit: ${element.objectFit || 'contain'};" />
59
+ </div>
60
+ `;
61
+ case 'shape':
62
+ const shapeStyle = element.fill ? `background-color: ${element.fill};` : '';
63
+ const borderStyle = element.outline ? `border: ${element.outline.width || 1}px ${element.outline.style || 'solid'} ${element.outline.color || '#000'};` : '';
64
+ return `
65
+ <div style="${style} ${shapeStyle} ${borderStyle}"></div>
66
+ `;
67
+ default:
68
+ return `<div style="${style}"></div>`;
69
+ }
70
+ }).join('');
71
+ };
72
+
73
+ return `
74
+ <!DOCTYPE html>
75
+ <html lang="zh-CN">
76
+ <head>
77
+ <meta charset="UTF-8">
78
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
79
+ <title>${title} - 第${slideIndex + 1}页</title>
80
+ <style>
81
+ * {
82
+ margin: 0;
83
+ padding: 0;
84
+ box-sizing: border-box;
85
+ }
86
+ body {
87
+ font-family: 'Microsoft YaHei', Arial, sans-serif;
88
+ background-color: #f5f5f5;
89
+ display: flex;
90
+ justify-content: center;
91
+ align-items: center;
92
+ min-height: 100vh;
93
+ padding: 0;
94
+ }
95
+ .slide-container {
96
+ width: 900px;
97
+ height: 506px;
98
+ background-color: ${slide.background?.color || theme.backgroundColor || '#ffffff'};
99
+ position: relative;
100
+ border-radius: 8px;
101
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
102
+ overflow: hidden;
103
+ }
104
+ ${slide.background?.type === 'image' ? `
105
+ .slide-container::before {
106
+ content: '';
107
+ position: absolute;
108
+ top: 0;
109
+ left: 0;
110
+ right: 0;
111
+ bottom: 0;
112
+ background-image: url('${slide.background.image}');
113
+ background-size: ${slide.background.imageSize || 'cover'};
114
+ background-position: center;
115
+ background-repeat: no-repeat;
116
+ z-index: 0;
117
+ }
118
+ ` : ''}
119
+ </style>
120
+ </head>
121
+ <body>
122
+ <div class="slide-container">
123
+ ${renderElements(slide.elements)}
124
+ </div>
125
+ </body>
126
+ </html>
127
+ `;
128
+ };
129
+
130
+ // 公开访问PPT页面 - 返回HTML页面
131
+ router.get('/view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
132
+ try {
133
+ const { userId, pptId, slideIndex = 0 } = req.params;
134
+ const querySlideIndex = req.query.slide ? parseInt(req.query.slide) : parseInt(slideIndex);
135
+ const fileName = `${pptId}.json`;
136
+ const storageService = getStorageService();
137
+
138
+ let pptData = null;
139
+
140
+ // 如果是GitHub服务,尝试所有仓库
141
+ if (storageService === githubService && storageService.repositories) {
142
+ for (let i = 0; i < storageService.repositories.length; i++) {
143
+ try {
144
+ const result = await storageService.getFile(userId, fileName, i);
145
+ if (result) {
146
+ pptData = result.content;
147
+ break;
148
+ }
149
+ } catch (error) {
150
+ continue;
151
+ }
152
+ }
153
+ } else {
154
+ // 内存存储服务
155
+ const result = await storageService.getFile(userId, fileName);
156
+ if (result) {
157
+ pptData = result.content;
158
+ }
159
+ }
160
+
161
+ if (!pptData) {
162
+ return res.status(404).send(`
163
+ <!DOCTYPE html>
164
+ <html lang="zh-CN">
165
+ <head>
166
+ <meta charset="UTF-8">
167
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
168
+ <title>PPT未找到</title>
169
+ <style>
170
+ body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
171
+ .error { color: #e74c3c; font-size: 18px; }
172
+ </style>
173
+ </head>
174
+ <body>
175
+ <div class="error">
176
+ <h2>PPT未找到</h2>
177
+ <p>请检查链接是否正确</p>
178
+ </div>
179
+ </body>
180
+ </html>
181
+ `);
182
+ }
183
+
184
+ const slideIdx = querySlideIndex;
185
+ if (slideIdx >= pptData.slides.length || slideIdx < 0) {
186
+ return res.status(404).send(`
187
+ <!DOCTYPE html>
188
+ <html lang="zh-CN">
189
+ <head>
190
+ <meta charset="UTF-8">
191
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
192
+ <title>幻灯片未找到</title>
193
+ <style>
194
+ body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
195
+ .error { color: #e74c3c; font-size: 18px; }
196
+ </style>
197
+ </head>
198
+ <body>
199
+ <div class="error">
200
+ <h2>幻灯片未找到</h2>
201
+ <p>请检查幻灯片索引是否正确</p>
202
+ </div>
203
+ </body>
204
+ </html>
205
+ `);
206
+ }
207
+
208
+ // 返回HTML页面
209
+ const htmlContent = generateSlideHTML(pptData, slideIdx, pptData.title);
210
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
211
+ res.send(htmlContent);
212
+ } catch (error) {
213
+ next(error);
214
+ }
215
+ });
216
+
217
+ // API接口:获取PPT数据和指定幻灯片(JSON格式)
218
+ router.get('/api/view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
219
+ try {
220
+ const { userId, pptId, slideIndex = 0 } = req.params;
221
+ const fileName = `${pptId}.json`;
222
+ const storageService = getStorageService();
223
+
224
+ let pptData = null;
225
+
226
+ // 如果是GitHub服务,尝试所有仓库
227
+ if (storageService === githubService && storageService.repositories) {
228
+ for (let i = 0; i < storageService.repositories.length; i++) {
229
+ try {
230
+ const result = await storageService.getFile(userId, fileName, i);
231
+ if (result) {
232
+ pptData = result.content;
233
+ break;
234
+ }
235
+ } catch (error) {
236
+ continue;
237
+ }
238
+ }
239
+ } else {
240
+ // 内存存储服务
241
+ const result = await storageService.getFile(userId, fileName);
242
+ if (result) {
243
+ pptData = result.content;
244
+ }
245
+ }
246
+
247
+ if (!pptData) {
248
+ return res.status(404).json({ error: 'PPT not found' });
249
+ }
250
+
251
+ const slideIdx = parseInt(slideIndex);
252
+ if (slideIdx >= pptData.slides.length || slideIdx < 0) {
253
+ return res.status(404).json({ error: 'Slide not found' });
254
+ }
255
+
256
+ // 返回PPT数据和指定幻灯片
257
+ res.json({
258
+ id: pptData.id,
259
+ title: pptData.title,
260
+ theme: pptData.theme,
261
+ currentSlide: pptData.slides[slideIdx],
262
+ slideIndex: slideIdx,
263
+ totalSlides: pptData.slides.length,
264
+ isPublicView: true
265
+ });
266
+ } catch (error) {
267
+ next(error);
268
+ }
269
+ });
270
+
271
+ // 获取完整PPT数据(只读模式)
272
+ router.get('/ppt/:userId/:pptId', async (req, res, next) => {
273
+ try {
274
+ const { userId, pptId } = req.params;
275
+ const fileName = `${pptId}.json`;
276
+ const storageService = getStorageService();
277
+
278
+ let pptData = null;
279
+
280
+ // 如果是GitHub服务,尝试所有仓库
281
+ if (storageService === githubService && storageService.repositories) {
282
+ for (let i = 0; i < storageService.repositories.length; i++) {
283
+ try {
284
+ const result = await storageService.getFile(userId, fileName, i);
285
+ if (result) {
286
+ pptData = result.content;
287
+ break;
288
+ }
289
+ } catch (error) {
290
+ continue;
291
+ }
292
+ }
293
+ } else {
294
+ // 内存存储服务
295
+ const result = await storageService.getFile(userId, fileName);
296
+ if (result) {
297
+ pptData = result.content;
298
+ }
299
+ }
300
+
301
+ if (!pptData) {
302
+ return res.status(404).json({ error: 'PPT not found' });
303
+ }
304
+
305
+ // 返回���读版本的PPT数据
306
+ res.json({
307
+ ...pptData,
308
+ isPublicView: true,
309
+ readOnly: true
310
+ });
311
+ } catch (error) {
312
+ next(error);
313
+ }
314
+ });
315
+
316
+ // 生成PPT分享链接
317
+ router.post('/generate-share-link', async (req, res, next) => {
318
+ try {
319
+ const { userId, pptId, slideIndex = 0 } = req.body;
320
+
321
+ if (!userId || !pptId) {
322
+ return res.status(400).json({ error: 'User ID and PPT ID are required' });
323
+ }
324
+
325
+ // 验证PPT是否存在
326
+ const fileName = `${pptId}.json`;
327
+ const storageService = getStorageService();
328
+ let pptExists = false;
329
+
330
+ // 如果是GitHub服务,尝试所有仓库
331
+ if (storageService === githubService && storageService.repositories) {
332
+ for (let i = 0; i < storageService.repositories.length; i++) {
333
+ try {
334
+ const result = await storageService.getFile(userId, fileName, i);
335
+ if (result) {
336
+ pptExists = true;
337
+ break;
338
+ }
339
+ } catch (error) {
340
+ continue;
341
+ }
342
+ }
343
+ } else {
344
+ // 内存存储服务
345
+ const result = await storageService.getFile(userId, fileName);
346
+ if (result) {
347
+ pptExists = true;
348
+ }
349
+ }
350
+
351
+ if (!pptExists) {
352
+ return res.status(404).json({ error: 'PPT not found' });
353
+ }
354
+
355
+ const baseUrl = process.env.PUBLIC_URL || req.get('host');
356
+ const protocol = process.env.NODE_ENV === 'production' ? 'https' : req.protocol;
357
+
358
+ const shareLinks = {
359
+ // 单页分享链接
360
+ slideUrl: `${protocol}://${baseUrl}/api/public/view/${userId}/${pptId}/${slideIndex}`,
361
+ // 完整PPT分享链接
362
+ pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`,
363
+ // 前端查看链接
364
+ viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`
365
+ };
366
+
367
+ res.json(shareLinks);
368
+ } catch (error) {
369
+ next(error);
370
+ }
371
+ });
372
+
373
+ export default router;
backend/src/services/githubService.js ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+ import { GITHUB_CONFIG } from '../config/users.js';
3
+ import memoryStorageService from './memoryStorageService.js';
4
+
5
+ class GitHubService {
6
+ constructor() {
7
+ this.apiUrl = GITHUB_CONFIG.apiUrl;
8
+ this.token = GITHUB_CONFIG.token;
9
+ this.repositories = GITHUB_CONFIG.repositories;
10
+ this.useMemoryStorage = !this.token; // 如果没有token,使用内存存储
11
+
12
+ if (!this.token) {
13
+ console.warn('GitHub token not configured, using memory storage for development');
14
+ }
15
+ }
16
+
17
+ // 获取仓库信息
18
+ parseRepoUrl(repoUrl) {
19
+ const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
20
+ if (!match) throw new Error('Invalid GitHub repository URL');
21
+ return { owner: match[1], repo: match[2] };
22
+ }
23
+
24
+ // 获取文件内容
25
+ async getFile(userId, fileName, repoIndex = 0) {
26
+ // 如果使用内存存储
27
+ if (this.useMemoryStorage) {
28
+ return await memoryStorageService.getFile(userId, fileName);
29
+ }
30
+
31
+ // 原有的GitHub逻辑
32
+ try {
33
+ const repoUrl = this.repositories[repoIndex];
34
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
35
+ const path = `users/${userId}/${fileName}`;
36
+
37
+ const response = await axios.get(
38
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
39
+ {
40
+ headers: {
41
+ 'Authorization': `token ${this.token}`,
42
+ 'Accept': 'application/vnd.github.v3+json'
43
+ }
44
+ }
45
+ );
46
+
47
+ const content = Buffer.from(response.data.content, 'base64').toString('utf8');
48
+ return {
49
+ content: JSON.parse(content),
50
+ sha: response.data.sha
51
+ };
52
+ } catch (error) {
53
+ if (error.response?.status === 404) {
54
+ return null;
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ // 保存文件
61
+ async saveFile(userId, fileName, data, repoIndex = 0) {
62
+ // 如果使用内存存储
63
+ if (this.useMemoryStorage) {
64
+ return await memoryStorageService.saveFile(userId, fileName, data);
65
+ }
66
+
67
+ // 原有的GitHub逻辑
68
+ const repoUrl = this.repositories[repoIndex];
69
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
70
+ const path = `users/${userId}/${fileName}`;
71
+
72
+ // 先尝试获取现有文件的SHA
73
+ let sha = null;
74
+ try {
75
+ const existing = await this.getFile(userId, fileName, repoIndex);
76
+ if (existing) {
77
+ sha = existing.sha;
78
+ }
79
+ } catch (error) {
80
+ // 忽略获取SHA的错误
81
+ }
82
+
83
+ const content = Buffer.from(JSON.stringify(data, null, 2)).toString('base64');
84
+
85
+ const payload = {
86
+ message: `Update ${fileName} for user ${userId}`,
87
+ content: content,
88
+ branch: 'main'
89
+ };
90
+
91
+ if (sha) {
92
+ payload.sha = sha;
93
+ }
94
+
95
+ const response = await axios.put(
96
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
97
+ payload,
98
+ {
99
+ headers: {
100
+ 'Authorization': `token ${this.token}`,
101
+ 'Accept': 'application/vnd.github.v3+json'
102
+ }
103
+ }
104
+ );
105
+
106
+ return response.data;
107
+ }
108
+
109
+ // 获取用户的所有PPT列表
110
+ async getUserPPTList(userId) {
111
+ // 如果使用内存存储
112
+ if (this.useMemoryStorage) {
113
+ return await memoryStorageService.getUserPPTList(userId);
114
+ }
115
+
116
+ // 原有的GitHub逻辑
117
+ const results = [];
118
+
119
+ for (let i = 0; i < this.repositories.length; i++) {
120
+ try {
121
+ const repoUrl = this.repositories[i];
122
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
123
+ const path = `users/${userId}`;
124
+
125
+ const response = await axios.get(
126
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
127
+ {
128
+ headers: {
129
+ 'Authorization': `token ${this.token}`,
130
+ 'Accept': 'application/vnd.github.v3+json'
131
+ }
132
+ }
133
+ );
134
+
135
+ const files = response.data
136
+ .filter(item => item.type === 'file' && item.name.endsWith('.json'));
137
+
138
+ // 获取每个PPT文件的实际内容以读取标题
139
+ for (const file of files) {
140
+ try {
141
+ const pptId = file.name.replace('.json', '');
142
+ const fileContent = await this.getFile(userId, file.name, i);
143
+
144
+ if (fileContent && fileContent.content) {
145
+ results.push({
146
+ name: pptId,
147
+ title: fileContent.content.title || '未命名演示文稿',
148
+ lastModified: fileContent.content.updatedAt || fileContent.content.createdAt,
149
+ repoIndex: i,
150
+ repoUrl: repoUrl
151
+ });
152
+ }
153
+ } catch (error) {
154
+ // 如果读取单个文件失败,使用文件名作为标题
155
+ console.warn(`Failed to read PPT content for ${file.name}:`, error.message);
156
+ results.push({
157
+ name: file.name.replace('.json', ''),
158
+ title: file.name.replace('.json', ''),
159
+ lastModified: new Date().toISOString(),
160
+ repoIndex: i,
161
+ repoUrl: repoUrl
162
+ });
163
+ }
164
+ }
165
+ } catch (error) {
166
+ if (error.response?.status !== 404) {
167
+ console.error(`Error fetching files from repo ${i}:`, error.message);
168
+ }
169
+ }
170
+ }
171
+
172
+ return results;
173
+ }
174
+
175
+ // 删除文件
176
+ async deleteFile(userId, fileName, repoIndex = 0) {
177
+ // 如果使用内存存储
178
+ if (this.useMemoryStorage) {
179
+ return await memoryStorageService.deleteFile(userId, fileName);
180
+ }
181
+
182
+ // 原有的GitHub逻辑
183
+ const existing = await this.getFile(userId, fileName, repoIndex);
184
+ if (!existing) {
185
+ throw new Error('File not found');
186
+ }
187
+
188
+ const repoUrl = this.repositories[repoIndex];
189
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
190
+ const path = `users/${userId}/${fileName}`;
191
+
192
+ const response = await axios.delete(
193
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
194
+ {
195
+ data: {
196
+ message: `Delete ${fileName} for user ${userId}`,
197
+ sha: existing.sha,
198
+ branch: 'main'
199
+ },
200
+ headers: {
201
+ 'Authorization': `token ${this.token}`,
202
+ 'Accept': 'application/vnd.github.v3+json'
203
+ }
204
+ }
205
+ );
206
+
207
+ return response.data;
208
+ }
209
+ }
210
+
211
+ export default new GitHubService();
backend/src/services/memoryStorageService.js ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { v4 as uuidv4 } from 'uuid';
2
+
3
+ // 内存存储,用于开发和测试
4
+ class MemoryStorageService {
5
+ constructor() {
6
+ this.storage = new Map(); // userId -> Map<pptId, pptData>
7
+ }
8
+
9
+ // 获取用户的所有PPT列表
10
+ async getUserPPTList(userId) {
11
+ const userStorage = this.storage.get(userId) || new Map();
12
+ const pptList = Array.from(userStorage.entries()).map(([pptId, pptData]) => ({
13
+ name: pptId,
14
+ title: pptData.title || '未命名演示文稿',
15
+ lastModified: pptData.updatedAt,
16
+ repoIndex: 0,
17
+ repoUrl: 'memory://local'
18
+ }));
19
+ return pptList;
20
+ }
21
+
22
+ // 获取PPT数据
23
+ async getFile(userId, fileName) {
24
+ const pptId = fileName.replace('.json', '');
25
+ const userStorage = this.storage.get(userId) || new Map();
26
+ const pptData = userStorage.get(pptId);
27
+
28
+ if (!pptData) {
29
+ return null;
30
+ }
31
+
32
+ return {
33
+ content: pptData,
34
+ sha: 'memory-sha'
35
+ };
36
+ }
37
+
38
+ // 保存PPT数据
39
+ async saveFile(userId, fileName, data) {
40
+ const pptId = fileName.replace('.json', '');
41
+
42
+ if (!this.storage.has(userId)) {
43
+ this.storage.set(userId, new Map());
44
+ }
45
+
46
+ const userStorage = this.storage.get(userId);
47
+ userStorage.set(pptId, {
48
+ ...data,
49
+ updatedAt: new Date().toISOString()
50
+ });
51
+
52
+ return { success: true };
53
+ }
54
+
55
+ // 删除PPT
56
+ async deleteFile(userId, fileName) {
57
+ const pptId = fileName.replace('.json', '');
58
+ const userStorage = this.storage.get(userId);
59
+
60
+ if (!userStorage || !userStorage.has(pptId)) {
61
+ throw new Error('File not found');
62
+ }
63
+
64
+ userStorage.delete(pptId);
65
+ return { success: true };
66
+ }
67
+ }
68
+
69
+ export default new MemoryStorageService();