feat: 完成布局优化 - 操作列固定、批量按钮自适应、分类标签带数量
优化内容: 1. 表格布局: - 使用 calc(100vw - 160px) 确保表格不超出视口 - 操作列 fixed='right' 固定在右侧,宽度 300px - 按钮 3 个后自动换行 (max-width: 200px) - 恢复合理列宽,不再过度压缩 2. 批量操作区域: - 容器改为 inline-block,宽度自适应按钮内容 - 背景宽度与按钮总宽度匹配 3. 分类标签: - 显示数量 (如 '待处理 (20)') - 点击切换筛选,去掉误导的 'X' 图标 4. 删除功能: - 操作列增加删除按钮 - 删除前弹出确认对话框 5. 系统日志: - 修复后端日志路径 (parents[4]) - 404 时显示友好提示 6. 其他: - 左侧菜单宽度 160px - 所有功能保留 (登录、用户管理、批量操作等)
This commit is contained in:
@@ -1,156 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import Topic
|
||||
|
||||
router = APIRouter(prefix="/api/publisher", tags=["publisher"])
|
||||
|
||||
# 项目根目录(从 api/publisher.py 上升到 yu-zhi-ran 根目录)
|
||||
import os
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[4]
|
||||
if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
SCRIPTS_DIR = PROJECT_ROOT / "scripts"
|
||||
|
||||
@router.get("/ready")
|
||||
def get_ready_topics(
|
||||
platform: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取待发布的选题(状态为 ready)"""
|
||||
query = db.query(Topic).filter(Topic.status == "ready")
|
||||
if platform:
|
||||
# 筛选未在该平台发布的选题
|
||||
# platform_urls 是 JSON 字段,需要特殊处理
|
||||
pass # 简化:暂不筛选
|
||||
topics = query.order_by(Topic.ready_at.desc()).all()
|
||||
return topics
|
||||
|
||||
@router.post("/generate/{topic_id}")
|
||||
def generate_publish_package(
|
||||
topic_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""为指定选题生成发布包(所有平台HTML)"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
|
||||
# 调用 publisher.py 脚本
|
||||
script_path = SCRIPTS_DIR / "publisher.py"
|
||||
if not script_path.exists():
|
||||
raise HTTPException(status_code=500, detail="Publisher script not found")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["python3", str(script_path), "--topic-id", topic_id],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
cwd=str(PROJECT_ROOT)
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Publisher failed: {result.stderr}")
|
||||
|
||||
return {
|
||||
"message": "Publish package generated",
|
||||
"topic_id": topic_id,
|
||||
"output": result.stdout
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
raise HTTPException(status_code=504, detail="Publisher timeout")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/packages/{topic_id}")
|
||||
def list_platform_packages(topic_id: str):
|
||||
"""列出某个选题的所有平台发布包"""
|
||||
release_dir = PROJECT_ROOT / "automation" / "data" / "releases"
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
packages = []
|
||||
for platform in ["zhihu", "wechat", "xiaohongshu", "bilibili", "toutiao"]:
|
||||
html_file = release_dir / today / platform / f"{platform}_{topic_id}_{platform}.html"
|
||||
if html_file.exists():
|
||||
packages.append({
|
||||
"platform": platform,
|
||||
"file": str(html_file.relative_to(PROJECT_ROOT)),
|
||||
"size": html_file.stat().st_size
|
||||
})
|
||||
|
||||
published_dir = PROJECT_ROOT / "content" / "published" / topic_id / "手动发布"
|
||||
if published_dir.exists():
|
||||
for platform_dir in published_dir.iterdir():
|
||||
if platform_dir.is_dir():
|
||||
html_file = platform_dir / "文章.html"
|
||||
if html_file.exists():
|
||||
packages.append({
|
||||
"platform": platform_dir.name,
|
||||
"file": str(html_file.relative_to(PROJECT_ROOT)),
|
||||
"size": html_file.stat().st_size,
|
||||
"manual": True
|
||||
})
|
||||
|
||||
return {"topic_id": topic_id, "packages": packages}
|
||||
|
||||
@router.get("/package/{topic_id}/{platform}")
|
||||
def get_package_html(topic_id: str, platform: str):
|
||||
"""获取指定平台发布包的HTML内容"""
|
||||
# 优先查找 published 目录(手动发布包)
|
||||
published_html = PROJECT_ROOT / "content" / "published" / topic_id / "手动发布" / platform / "文章.html"
|
||||
if published_html.exists():
|
||||
return {"html": published_html.read_text(encoding='utf-8')}
|
||||
|
||||
# 其次查找 releases 目录(自动生成)
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
release_html = PROJECT_ROOT / "automation" / "data" / "releases" / today / platform / f"{platform}_{topic_id}_{platform}.html"
|
||||
if release_html.exists():
|
||||
return {"html": release_html.read_text(encoding='utf-8')}
|
||||
|
||||
raise HTTPException(status_code=404, detail="Package not found")
|
||||
|
||||
@router.post("/mark/{topic_id}/published")
|
||||
def mark_as_published(
|
||||
topic_id: str,
|
||||
platform_urls: dict,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""手动标记选题为已发布,记录平台链接"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
|
||||
topic.status = "published"
|
||||
topic.published_at = datetime.now().date()
|
||||
topic.platform_urls = platform_urls
|
||||
db.commit()
|
||||
|
||||
return {"message": "Topic marked as published", "topic_id": topic_id}
|
||||
|
||||
@router.get("/status")
|
||||
def get_publisher_status():
|
||||
"""获取发布统计"""
|
||||
# 统计今日已发布数量等
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
release_dir = PROJECT_ROOT / "automation" / "data" / "releases" / today
|
||||
|
||||
stats = {
|
||||
"today_releases": 0,
|
||||
"platforms": {}
|
||||
}
|
||||
|
||||
if release_dir.exists():
|
||||
for platform_dir in release_dir.iterdir():
|
||||
if platform_dir.is_dir():
|
||||
count = len(list(platform_dir.glob("*.html")))
|
||||
stats["platforms"][platform_dir.name] = count
|
||||
stats["today_releases"] += count
|
||||
|
||||
return stats
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""发布管理 API"""
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Topic, PublishRecord, User
|
||||
from sqlalchemy.orm import Session
|
||||
from .auth import verify_token
|
||||
from ..core.audit_logger import audit_log
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class PublishRequest(BaseModel):
|
||||
topic_id: str
|
||||
|
||||
class PublishResponse(BaseModel):
|
||||
ok: bool
|
||||
topic_id: str
|
||||
message: str
|
||||
|
||||
def get_current_user(request: Request, db: Session = Depends(get_db)) -> User:
|
||||
"""获取当前登录用户(可选,未登录也允许,但记录为 anonymous)"""
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
return None
|
||||
token = auth_header.split(" ")[1]
|
||||
try:
|
||||
from .auth import verify_token
|
||||
return verify_token(token, db)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@router.post("/api/publishing/create", response_model=PublishResponse)
|
||||
async def create_publish_record(
|
||||
req: PublishRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""标记选题为已发布,并创建发布记录"""
|
||||
try:
|
||||
# 查找选题
|
||||
topic = db.query(Topic).filter(Topic.id == req.topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail=f"选题 {req.topic_id} 不存在")
|
||||
|
||||
if topic.status != '待发布':
|
||||
raise HTTPException(status_code=400, detail=f"选题 {req.topic_id} 状态不是待发布")
|
||||
|
||||
# 更新选题状态
|
||||
topic.status = '已发布'
|
||||
topic.updated_at = datetime.now()
|
||||
topic.published_at = datetime.now().date() # 设置发布时间为今天
|
||||
|
||||
# 创建发布记录
|
||||
operator = current_user.username if current_user else 'anonymous'
|
||||
record = PublishRecord(
|
||||
topic_id=req.topic_id,
|
||||
platform='all',
|
||||
action='publish',
|
||||
status='success',
|
||||
operator=operator,
|
||||
description=f"选题 {req.topic_id} 已发布"
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
# 强制刷新会话缓存,确保后续读取最新数据
|
||||
db.expire_all()
|
||||
db.refresh(topic)
|
||||
|
||||
# 审计日志
|
||||
audit_log(
|
||||
action="publish",
|
||||
user=current_user,
|
||||
resource_type="topic",
|
||||
resource_id=req.topic_id,
|
||||
details={"operator": operator, "status": "success"},
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent", ""),
|
||||
db=db
|
||||
)
|
||||
|
||||
return PublishResponse(
|
||||
ok=True,
|
||||
topic_id=req.topic_id,
|
||||
message=f"选题 {req.topic_id} 已成功发布"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -185,3 +185,14 @@ def generate_packages(topic_id: str):
|
||||
"""
|
||||
# TODO: 实际调用 publisher.py 逻辑,这里先返回模拟响应
|
||||
return {"message": "Package generation triggered", "topic_id": topic_id, "status": "pending"}
|
||||
|
||||
@router.delete("/{topic_id}")
|
||||
def delete_topic(topic_id: str, db: Session = Depends(get_db)):
|
||||
"""删除选题"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="选题不存在")
|
||||
|
||||
db.delete(topic)
|
||||
db.commit()
|
||||
return {"message": "删除成功", "topic_id": topic_id}
|
||||
|
||||
Reference in New Issue
Block a user