commit 6cf78d4c8633824c1d6df0d0f03639f952dadb96 Author: zhangyonghao <2413683360.qq.com> Date: Sat Mar 21 17:16:13 2026 +0800 init diff --git a/html-generator/backend/.env b/html-generator/backend/.env new file mode 100644 index 0000000..66f8dc2 --- /dev/null +++ b/html-generator/backend/.env @@ -0,0 +1,12 @@ +# 应用配置 +APP_NAME="HTML Generator API" +API_PREFIX="/api" + +# 前端配置 +FRONTEND_BASE_URL="http://localhost:3000" + +# 允许的跨域来源 +ALLOWED_ORIGINS=["*"] + +# 静态文件目录 +STATIC_DIR="../frontend/public/static" \ No newline at end of file diff --git a/html-generator/backend/.env.example b/html-generator/backend/.env.example new file mode 100644 index 0000000..66f8dc2 --- /dev/null +++ b/html-generator/backend/.env.example @@ -0,0 +1,12 @@ +# 应用配置 +APP_NAME="HTML Generator API" +API_PREFIX="/api" + +# 前端配置 +FRONTEND_BASE_URL="http://localhost:3000" + +# 允许的跨域来源 +ALLOWED_ORIGINS=["*"] + +# 静态文件目录 +STATIC_DIR="../frontend/public/static" \ No newline at end of file diff --git a/html-generator/backend/app/__init__.py b/html-generator/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/html-generator/backend/app/__pycache__/__init__.cpython-313.pyc b/html-generator/backend/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..f2cb66b Binary files /dev/null and b/html-generator/backend/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/html-generator/backend/app/__pycache__/config.cpython-313.pyc b/html-generator/backend/app/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..d022e06 Binary files /dev/null and b/html-generator/backend/app/__pycache__/config.cpython-313.pyc differ diff --git a/html-generator/backend/app/__pycache__/database.cpython-313.pyc b/html-generator/backend/app/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..250d215 Binary files /dev/null and b/html-generator/backend/app/__pycache__/database.cpython-313.pyc differ diff --git a/html-generator/backend/app/__pycache__/main.cpython-313.pyc b/html-generator/backend/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..ed06b2a Binary files /dev/null and b/html-generator/backend/app/__pycache__/main.cpython-313.pyc differ diff --git a/html-generator/backend/app/config.py b/html-generator/backend/app/config.py new file mode 100644 index 0000000..84753d9 --- /dev/null +++ b/html-generator/backend/app/config.py @@ -0,0 +1,22 @@ +import os +from pathlib import Path + +from dotenv import load_dotenv + +backend_dir = Path(__file__).resolve().parent.parent +load_dotenv(backend_dir / ".env") + + +class Settings: + app_name = "HTML Generator API" + api_prefix = "/api" + backend_dir = backend_dir + data_dir = backend_dir / "data" + database_path = data_dir / "html_generator.db" + database_url = f"sqlite:///{database_path.as_posix()}" + allowed_origins = ["*"] + frontend_base_url = os.environ.get("FRONTEND_BASE_URL", "http://localhost:3000") + static_dir = backend_dir / "../frontend/public/static" + + +settings = Settings() \ No newline at end of file diff --git a/html-generator/backend/app/database.py b/html-generator/backend/app/database.py new file mode 100644 index 0000000..1d8a1f5 --- /dev/null +++ b/html-generator/backend/app/database.py @@ -0,0 +1,23 @@ +from collections.abc import Generator + +from sqlalchemy import create_engine +from sqlalchemy.orm import declarative_base, sessionmaker + +from app.config import settings + +settings.data_dir.mkdir(parents=True, exist_ok=True) + +engine = create_engine( + settings.database_url, + connect_args={"check_same_thread": False}, +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +def get_db() -> Generator: + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/html-generator/backend/app/main.py b/html-generator/backend/app/main.py new file mode 100644 index 0000000..1500da8 --- /dev/null +++ b/html-generator/backend/app/main.py @@ -0,0 +1,43 @@ +import logging + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.config import settings +from app.database import Base, engine, SessionLocal +from app.models import HTMLFile +from app.routers import html + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", +) +logger = logging.getLogger(__name__) + +Base.metadata.create_all(bind=engine) + +# 删除过期记录 +db = SessionLocal() +try: + deleted_count = HTMLFile.delete_expired_records(db) + if deleted_count > 0: + logger.info(f"Deleted {deleted_count} expired HTML file records") +finally: + db.close() + +app = FastAPI(title=settings.app_name) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.allowed_origins, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(html.router, prefix=settings.api_prefix) + + +@app.get("/") +def health_check() -> dict[str, str]: + return {"message": "HTML Generator API is running"} \ No newline at end of file diff --git a/html-generator/backend/app/models.py b/html-generator/backend/app/models.py new file mode 100644 index 0000000..d8c4d81 --- /dev/null +++ b/html-generator/backend/app/models.py @@ -0,0 +1,30 @@ +from datetime import datetime, timedelta + +from sqlalchemy import DateTime, Integer, String +from sqlalchemy.orm import Mapped, mapped_column, Session + +from app.database import Base + + +class HTMLFile(Base): + __tablename__ = "html_files" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + unique_id: Mapped[str] = mapped_column( + String(32), unique=True, index=True, nullable=False + ) + filename: Mapped[str] = mapped_column(String(255), nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime, default=datetime.utcnow, nullable=False + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False + ) + + @classmethod + def delete_expired_records(cls, db: Session, days: int = 5) -> int: + """删除超过指定天数的记录""" + cutoff_date = datetime.utcnow() - timedelta(days=days) + deleted = db.query(cls).filter(cls.created_at < cutoff_date).delete() + db.commit() + return deleted \ No newline at end of file diff --git a/html-generator/backend/app/routers/__init__.py b/html-generator/backend/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/html-generator/backend/app/routers/html.py b/html-generator/backend/app/routers/html.py new file mode 100644 index 0000000..db1171e --- /dev/null +++ b/html-generator/backend/app/routers/html.py @@ -0,0 +1,86 @@ +import os +import secrets +import logging + +from fastapi import APIRouter, HTTPException, status, Depends +from sqlalchemy.orm import Session + +from app.config import settings +from app.database import get_db +from app.models import HTMLFile +from app.schemas import HTMLGenerateRequest, HTMLGenerateResponse + +router = APIRouter(prefix="/html", tags=["html"]) +logger = logging.getLogger(__name__) + + +def generate_unique_id() -> str: + return secrets.token_urlsafe(16) + + +@router.post("/generate", response_model=HTMLGenerateResponse, status_code=status.HTTP_201_CREATED) +def generate_html(request: HTMLGenerateRequest, db: Session = Depends(get_db)): + try: + # 先删除过期记录 + deleted_count = HTMLFile.delete_expired_records(db) + if deleted_count > 0: + logger.info(f"Deleted {deleted_count} expired HTML file records") + + # 生成唯一 ID + unique_id = generate_unique_id() + + # 确保静态文件目录存在 + static_dir = settings.static_dir.resolve() + static_dir.mkdir(parents=True, exist_ok=True) + + # 生成 HTML 文件路径 + html_filename = f"{unique_id}.html" + html_path = static_dir / html_filename + + # 写入 HTML 内容 + with open(html_path, "w", encoding="utf-8") as f: + f.write(request.html_content) + + # 保存到数据库 + html_file = HTMLFile( + unique_id=unique_id, + filename=html_filename, + ) + db.add(html_file) + db.commit() + db.refresh(html_file) + + # 生成完整链接 + html_url = f"{settings.frontend_base_url}/static/{html_filename}" + + return HTMLGenerateResponse( + message="HTML 文件生成成功", + unique_id=unique_id, + url=html_url + ) + except Exception as e: + logger.error(f"生成 HTML 文件失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"生成 HTML 文件失败: {str(e)}" + ) + + +@router.get("/{unique_id}") +def get_html_file(unique_id: str, db: Session = Depends(get_db)): + html_file = db.query(HTMLFile).filter(HTMLFile.unique_id == unique_id).first() + + if not html_file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="HTML 文件不存在" + ) + + # 生成完整链接 + html_url = f"{settings.frontend_base_url}/static/{html_file.filename}" + + return { + "message": "HTML 文件查询成功", + "unique_id": html_file.unique_id, + "url": html_url + } \ No newline at end of file diff --git a/html-generator/backend/app/schemas.py b/html-generator/backend/app/schemas.py new file mode 100644 index 0000000..e058831 --- /dev/null +++ b/html-generator/backend/app/schemas.py @@ -0,0 +1,23 @@ +from datetime import datetime +from pydantic import BaseModel + + +class HTMLGenerateRequest(BaseModel): + html_content: str + + +class HTMLGenerateResponse(BaseModel): + message: str + unique_id: str + url: str + + +class HTMLFileResponse(BaseModel): + id: int + unique_id: str + filename: str + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True \ No newline at end of file diff --git a/html-generator/backend/package.json b/html-generator/backend/package.json new file mode 100644 index 0000000..0956cdf --- /dev/null +++ b/html-generator/backend/package.json @@ -0,0 +1,18 @@ +{ + "name": "html-generator-backend", + "version": "1.0.0", + "description": "HTML Generator Backend", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} \ No newline at end of file diff --git a/html-generator/backend/requirements.txt b/html-generator/backend/requirements.txt new file mode 100644 index 0000000..d9e64c4 --- /dev/null +++ b/html-generator/backend/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.104.1 +uvicorn==0.24.0.post1 +python-dotenv==1.0.0 +sqlalchemy==2.0.23 \ No newline at end of file diff --git a/html-generator/backend/server.js b/html-generator/backend/server.js new file mode 100644 index 0000000..45a8cc4 --- /dev/null +++ b/html-generator/backend/server.js @@ -0,0 +1,70 @@ +const express = require('express'); +const cors = require('cors'); +const dotenv = require('dotenv'); +const fs = require('fs'); +const path = require('path'); + +// 加载环境变量 +dotenv.config(); + +const app = express(); +const port = 8000; + +// 中间件 +app.use(cors()); +app.use(express.json()); + +// 生成唯一 ID +function generateUniqueId() { + return Math.random().toString(36).substring(2, 18) + Math.random().toString(36).substring(2, 18); +} + +// 确保静态文件目录存在 +const staticDir = path.resolve(__dirname, '../frontend/public/static'); +if (!fs.existsSync(staticDir)) { + fs.mkdirSync(staticDir, { recursive: true }); +} + +// API 路由 +app.post('/api/html/generate', (req, res) => { + try { + const { html_content } = req.body; + + if (!html_content) { + return res.status(400).json({ error: 'HTML 内容不能为空' }); + } + + // 生成唯一 ID + const uniqueId = generateUniqueId(); + + // 生成 HTML 文件路径 + const htmlFilename = `${uniqueId}.html`; + const htmlPath = path.join(staticDir, htmlFilename); + + // 写入 HTML 内容 + fs.writeFileSync(htmlPath, html_content, 'utf-8'); + + // 生成完整链接 + const frontendBaseUrl = process.env.FRONTEND_BASE_URL || 'http://localhost:3000'; + const htmlUrl = `${frontendBaseUrl}/static/${htmlFilename}`; + + res.status(201).json({ + message: 'HTML 文件生成成功', + unique_id: uniqueId, + url: htmlUrl + }); + } catch (error) { + console.error('生成 HTML 文件失败:', error); + res.status(500).json({ error: `生成 HTML 文件失败: ${error.message}` }); + } +}); + +// 健康检查 +app.get('/', (req, res) => { + res.json({ message: 'HTML Generator API is running' }); +}); + +// 启动服务器 +app.listen(port, () => { + console.log(`服务器运行在 http://localhost:${port}`); +}); \ No newline at end of file diff --git a/html-generator/backend/server.py b/html-generator/backend/server.py new file mode 100644 index 0000000..a7b5a4e --- /dev/null +++ b/html-generator/backend/server.py @@ -0,0 +1,87 @@ +import http.server +import socketserver +import json +import os +import random +import string +from urllib.parse import urlparse, parse_qs + +PORT = 8000 + +# 生成唯一 ID +def generate_unique_id(): + return ''.join(random.choices(string.ascii_letters + string.digits, k=16)) + +# 确保静态文件目录存在 +static_dir = os.path.join(os.path.dirname(__file__), '../frontend/public/static') +if not os.path.exists(static_dir): + os.makedirs(static_dir, exist_ok=True) + +class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def do_POST(self): + if self.path == '/api/html/generate': + # 读取请求体 + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + + try: + # 解析 JSON 数据 + data = json.loads(post_data) + html_content = data.get('html_content', '') + + if not html_content: + self.send_response(400) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({'error': 'HTML 内容不能为空'}).encode('utf-8')) + return + + # 生成唯一 ID + unique_id = generate_unique_id() + + # 生成 HTML 文件路径 + html_filename = f"{unique_id}.html" + html_path = os.path.join(static_dir, html_filename) + + # 写入 HTML 内容 + with open(html_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + # 生成完整链接 + frontend_base_url = 'http://localhost:3000' + html_url = f"{frontend_base_url}/static/{html_filename}" + + # 返回响应 + self.send_response(201) + self.send_header('Content-type', 'application/json') + self.end_headers() + response = { + 'message': 'HTML 文件生成成功', + 'unique_id': unique_id, + 'url': html_url + } + self.wfile.write(json.dumps(response).encode('utf-8')) + except Exception as e: + self.send_response(500) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({'error': f'生成 HTML 文件失败: {str(e)}'}).encode('utf-8')) + else: + self.send_response(404) + self.end_headers() + + def do_GET(self): + if self.path == '/': + # 健康检查 + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({'message': 'HTML Generator API is running'}).encode('utf-8')) + else: + # 静态文件服务 + super().do_GET() + +if __name__ == "__main__": + with socketserver.TCPServer(("", PORT), MyHTTPRequestHandler) as httpd: + print(f"服务器运行在 http://localhost:{PORT}") + httpd.serve_forever() \ No newline at end of file diff --git a/html-generator/frontend/app/globals.css b/html-generator/frontend/app/globals.css new file mode 100644 index 0000000..acc2429 --- /dev/null +++ b/html-generator/frontend/app/globals.css @@ -0,0 +1,54 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: oklch(0.9816 0.0017 247.8577); + --foreground: oklch(0.1221 0 0); + --card: oklch(0.9911 0 0); + --card-foreground: oklch(0.1221 0 0); + --popover: oklch(0.9911 0 0); + --popover-foreground: oklch(0.1221 0 0); + --primary: oklch(0.6264 0.1942 259.2027); + --primary-foreground: oklch(0.9911 0 0); + --secondary: oklch(0.2795 0.0368 259.8165); + --secondary-foreground: oklch(0.9911 0 0); + --muted: oklch(0.9700 0 0); + --muted-foreground: oklch(0.5560 0 0); + --accent: oklch(0.6755 0.1303 40.7093); + --accent-foreground: oklch(0.9911 0 0); + --destructive: oklch(0.6498 0.1805 9.8598); + --destructive-foreground: oklch(0.9911 0 0); + --border: oklch(0.9189 0 0); + --input: oklch(0.9491 0 0); + --ring: oklch(0.6264 0.1942 259.2027); +} + +.dark { + --background: oklch(0.1221 0 0); + --foreground: oklch(0.9816 0.0017 247.8577); + --card: oklch(0.1565 0 0); + --card-foreground: oklch(0.9816 0.0017 247.8577); + --popover: oklch(0.1565 0 0); + --popover-foreground: oklch(0.9816 0.0017 247.8577); + --primary: oklch(0.6264 0.1942 259.2027); + --primary-foreground: oklch(0.9911 0 0); + --secondary: oklch(0.2795 0.0368 259.8165); + --secondary-foreground: oklch(0.9911 0 0); + --muted: oklch(0.2686 0 0); + --muted-foreground: oklch(0.7155 0 0); + --accent: oklch(0.6755 0.1303 40.7093); + --accent-foreground: oklch(0.9911 0 0); + --destructive: oklch(0.6498 0.1805 9.8598); + --destructive-foreground: oklch(0.9911 0 0); + --border: oklch(0.2750 0 0); + --input: oklch(0.3250 0 0); + --ring: oklch(0.6264 0.1942 259.2027); +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background-color: var(--background); + color: var(--foreground); + line-height: 1.6; +} \ No newline at end of file diff --git a/html-generator/frontend/app/layout.tsx b/html-generator/frontend/app/layout.tsx new file mode 100644 index 0000000..e02403b --- /dev/null +++ b/html-generator/frontend/app/layout.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from 'next'; +import './globals.css'; + + +export const metadata: Metadata = { + title: 'HTML Generator', + description: '生成HTML文件并返回可访问链接', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + +
+