Skip to content

Commit 7700b97

Browse files
authored
refactor(backend/frontend): consolidate file upload into template routes (#165)
* refactor(backend/frontend): consolidate file upload into template routes - Remove standalone file_router and merge file upload functionality into template_router - Delete backend/routers/file_routes/file.py and consolidate endpoints - Add file upload capability directly to template creation endpoint (/templates/upload) - Implement output format validation for template file uploads (latex, markdown, word) - Add file extension validation matching output format requirements - Implement file size limit (10MB) and encoding validation for uploaded files - Add base64 encoding for binary DOCX files and UTF-8 handling for text files - Add template preview endpoint with format-specific rendering (DOCX, LaTeX, Markdown) - Update frontend API calls to use consolidated template upload endpoint - Update DocxViewer component for improved DOCX preview functionality - Simplify router initialization by removing file_router imports - Streamline file management by consolidating related functionality under template routes * fix(frontend): add default values for template preview data - Add default empty string values for mime_type, download_url, and message in file-info props to prevent undefined errors - Add file_path field to templateForm reactive object for displaying current filename - Ensure template preview component handles missing data gracefully with fallback values * perf(frontend): optimize build bundle splitting and add null safety - Add manual chunk splitting in Vite build configuration to reduce bundle size - Separate vendor dependencies into logical chunks: Vue/Pinia, TDesign, Markdown, and DocX preview - Increase chunk size warning limit to 600KB to accommodate larger dependencies - Add null coalescing operators to DocxViewer file-info props for improved type safety - Prevent potential undefined value errors in template preview data rendering
1 parent 5ab76b7 commit 7700b97

File tree

13 files changed

+693
-266
lines changed

13 files changed

+693
-266
lines changed

backend/routers/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .auth_routes import auth_router
77
from .chat_routes import chat_router
88
from .work_routes import work_router, workspace_router
9-
from .file_routes import file_router, template_router
9+
from .file_routes import template_router
1010
from .config_routes import model_config_router, context_router
1111
from .mcp_routes import mcp_router
1212

@@ -16,7 +16,6 @@
1616
chat_router,
1717
work_router,
1818
workspace_router,
19-
file_router,
2019
template_router,
2120
model_config_router,
2221
context_router,
@@ -28,7 +27,6 @@
2827
'chat_router',
2928
'work_router',
3029
'workspace_router',
31-
'file_router',
3230
'template_router',
3331
'model_config_router',
3432
'context_router',

backend/routers/file_routes/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
文件管理相关路由模块
33
"""
44

5-
from .file import router as file_router
65
from .template import router as template_router
76

8-
__all__ = ['file_router', 'template_router']
7+
__all__ = ['template_router']

backend/routers/file_routes/file.py

Lines changed: 0 additions & 65 deletions
This file was deleted.

backend/routers/file_routes/template.py

Lines changed: 200 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,93 @@
1-
from fastapi import APIRouter, Depends, HTTPException, status
1+
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
2+
from fastapi.responses import FileResponse
23
from sqlalchemy.orm import Session
34
from models import models
45
from schemas import schemas
56
from services import crud
67
from auth import auth
78
from database.database import get_db
8-
from typing import List
9+
from typing import List, Optional
910
from ..utils import route_guard
11+
import base64
12+
import mimetypes
13+
from pathlib import Path
1014

1115
router = APIRouter(prefix="/templates", tags=["模板管理"])
1216

13-
@router.post("", response_model=schemas.PaperTemplateResponse)
17+
# 输出格式与文件扩展名的映射关系
18+
OUTPUT_FORMAT_EXTENSIONS = {
19+
"latex": ".tex",
20+
"markdown": ".md",
21+
"word": ".docx"
22+
}
23+
24+
@router.post("/upload", response_model=schemas.PaperTemplateResponse)
1425
@route_guard
15-
async def create_template(
16-
template: schemas.PaperTemplateCreateWithContent,
26+
async def create_template_with_file(
27+
file: UploadFile = File(...),
28+
name: str = Form(...),
29+
output_format: str = Form(...),
30+
description: Optional[str] = Form(None),
31+
category: Optional[str] = Form(None),
32+
is_public: bool = Form(False),
1733
current_user: int = Depends(auth.get_current_user),
1834
db: Session = Depends(get_db)
1935
):
20-
"""创建论文模板"""
21-
# 提取文件内容
22-
content = template.content
23-
# 创建模板数据(不包含content字段)
36+
"""创建模板(直接上传文件)- 一步完成"""
37+
# 验证输出格式
38+
if output_format not in OUTPUT_FORMAT_EXTENSIONS:
39+
raise HTTPException(
40+
status_code=status.HTTP_400_BAD_REQUEST,
41+
detail=f"不支持的输出格式。支持的格式: {', '.join(OUTPUT_FORMAT_EXTENSIONS.keys())}"
42+
)
43+
44+
# 获取文件扩展名
45+
file_extension = Path(file.filename).suffix.lower()
46+
allowed_extension = OUTPUT_FORMAT_EXTENSIONS[output_format]
47+
48+
# 验证文件扩展名
49+
if file_extension != allowed_extension:
50+
raise HTTPException(
51+
status_code=status.HTTP_400_BAD_REQUEST,
52+
detail=f"输出格式为 '{output_format}' 时,只能上传 {allowed_extension} 文件"
53+
)
54+
55+
# 读取文件内容
56+
content = await file.read()
57+
58+
# 检查文件大小(限制为10MB)
59+
if len(content) > 10 * 1024 * 1024:
60+
raise HTTPException(
61+
status_code=status.HTTP_400_BAD_REQUEST,
62+
detail="文件大小超过限制(最大10MB)"
63+
)
64+
65+
# 判断是否为二进制文件
66+
is_binary = file_extension == '.docx'
67+
68+
# 处理文件内容
69+
if is_binary:
70+
content_str = base64.b64encode(content).decode('utf-8')
71+
else:
72+
try:
73+
content_str = content.decode('utf-8')
74+
except UnicodeDecodeError:
75+
raise HTTPException(
76+
status_code=status.HTTP_400_BAD_REQUEST,
77+
detail="文件编码错误,请使用UTF-8编码"
78+
)
79+
80+
# 创建模板数据
2481
template_data = schemas.PaperTemplateCreate(
25-
name=template.name,
26-
description=template.description,
27-
category=template.category,
28-
output_format=template.output_format,
29-
file_path=template.file_path,
30-
is_public=template.is_public
82+
name=name,
83+
description=description,
84+
category=category,
85+
output_format=output_format,
86+
file_path=file.filename,
87+
is_public=is_public
3188
)
32-
return crud.create_paper_template(db, template_data, current_user, content)
89+
90+
return crud.create_paper_template(db, template_data, current_user, content_str, is_binary)
3391

3492
@router.get("", response_model=List[schemas.PaperTemplateResponse])
3593
@route_guard
@@ -109,13 +167,134 @@ async def force_delete_template(
109167
"""强制删除模板(同时删除引用该模板的工作)"""
110168
return crud.force_delete_paper_template(db, template_id, current_user)
111169

112-
@router.get("/{template_id}/content")
170+
@router.get("/{template_id}/preview")
171+
@route_guard
172+
async def get_template_preview(
173+
template_id: int,
174+
current_user: int = Depends(auth.get_current_user),
175+
db: Session = Depends(get_db)
176+
):
177+
"""获取模板文件预览内容,支持不同文件类型"""
178+
# 检查模板权限
179+
template = crud.get_paper_template(db, template_id)
180+
if not template:
181+
raise HTTPException(
182+
status_code=status.HTTP_404_NOT_FOUND,
183+
detail="Template not found"
184+
)
185+
186+
# 检查权限:只有创建者或公开模板可以访问
187+
if not template.is_public and template.created_by != current_user:
188+
raise HTTPException(
189+
status_code=status.HTTP_403_FORBIDDEN,
190+
detail="Not authorized to access this template"
191+
)
192+
193+
# 获取模板文件路径
194+
from config.paths import get_templates_path
195+
196+
# file_path 现在只存储文件名
197+
if template.file_path:
198+
file_path = get_templates_path() / template.file_path
199+
else:
200+
file_path = get_templates_path() / f"{template_id}_template.md"
201+
202+
if not file_path.exists():
203+
raise HTTPException(
204+
status_code=status.HTTP_404_NOT_FOUND,
205+
detail="Template file not found"
206+
)
207+
208+
# 检测文件类型
209+
file_type = _detect_template_file_type(str(file_path))
210+
211+
if file_type == 'text':
212+
# 文本文件:直接读取文件内容
213+
try:
214+
with open(file_path, 'r', encoding='utf-8') as f:
215+
content = f.read()
216+
return {
217+
"type": "text",
218+
"content": content,
219+
"filename": file_path.name,
220+
"size": file_path.stat().st_size
221+
}
222+
except Exception as e:
223+
raise HTTPException(
224+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
225+
detail=f"Failed to read template content: {str(e)}"
226+
)
227+
228+
else: # binary (docx)
229+
# 二进制文件:返回元数据和下载信息
230+
mime_type, _ = mimetypes.guess_type(str(file_path))
231+
if not mime_type:
232+
mime_type = "application/octet-stream"
233+
234+
return {
235+
"type": "binary",
236+
"filename": file_path.name,
237+
"size": file_path.stat().st_size,
238+
"mime_type": mime_type,
239+
"download_url": f"/templates/{template_id}/download",
240+
"message": "Binary file - use download button to view"
241+
}
242+
243+
@router.get("/{template_id}/download")
113244
@route_guard
114-
async def get_template_content(
245+
async def download_template_file(
115246
template_id: int,
116247
current_user: int = Depends(auth.get_current_user),
117248
db: Session = Depends(get_db)
118249
):
119-
"""获取模板文件内容"""
120-
content = crud.get_template_file_content(db, template_id, current_user)
121-
return {"content": content}
250+
"""下载模板文件"""
251+
# 检查模板权限
252+
template = crud.get_paper_template(db, template_id)
253+
if not template:
254+
raise HTTPException(
255+
status_code=status.HTTP_404_NOT_FOUND,
256+
detail="Template not found"
257+
)
258+
259+
# 检查权限:只有创建者或公开模板可以访问
260+
if not template.is_public and template.created_by != current_user:
261+
raise HTTPException(
262+
status_code=status.HTTP_403_FORBIDDEN,
263+
detail="Not authorized to access this template"
264+
)
265+
266+
# 获取模板文件路径
267+
from config.paths import get_templates_path
268+
269+
# file_path 现在只存储文件名
270+
if template.file_path:
271+
file_path = get_templates_path() / template.file_path
272+
else:
273+
file_path = get_templates_path() / f"{template_id}_template.md"
274+
275+
if not file_path.exists():
276+
raise HTTPException(
277+
status_code=status.HTTP_404_NOT_FOUND,
278+
detail="Template file not found"
279+
)
280+
281+
# 获取MIME类型
282+
mime_type, _ = mimetypes.guess_type(str(file_path))
283+
if mime_type is None:
284+
mime_type = "application/octet-stream"
285+
286+
# 返回文件下载响应
287+
return FileResponse(
288+
path=str(file_path),
289+
media_type=mime_type,
290+
filename=file_path.name
291+
)
292+
293+
def _detect_template_file_type(file_path: str) -> str:
294+
"""检测模板文件类型:返回 'text' 或 'binary'"""
295+
ext = Path(file_path).suffix.lower()
296+
# 模板系统只支持这三种格式
297+
if ext in {'.md', '.tex'}:
298+
return 'text'
299+
else: # .docx 等
300+
return 'binary'

backend/schemas/schemas.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,6 @@ class PaperTemplateBase(BaseModel):
7272
class PaperTemplateCreate(PaperTemplateBase):
7373
pass
7474

75-
class PaperTemplateCreateWithContent(BaseModel):
76-
"""创建模板时包含文件内容的schema"""
77-
name: str
78-
description: Optional[str] = None
79-
category: Optional[str] = None
80-
output_format: str = "markdown" # 输出格式:md, word, latex
81-
file_path: str # 模板文件路径
82-
is_public: bool = False
83-
content: str = "" # 模板文件内容
84-
8575
class PaperTemplateUpdate(BaseModel):
8676
name: Optional[str] = None
8777
description: Optional[str] = None

0 commit comments

Comments
 (0)