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:
lt
2026-04-27 11:32:17 +08:00
parent 59d2a76df4
commit 277b13eaae
137 changed files with 8615 additions and 1213 deletions
+65
View File
@@ -0,0 +1,65 @@
"""
审计日志记录模块
用法:
from .audit_logger import audit_log
audit_log(action="create_user", user=current_user, details={...}, request=request, db=db)
"""
from sqlalchemy.orm import Session
from ..models import AuditLog
from typing import Optional, Dict, Any
from datetime import datetime
def audit_log(
action: str,
*,
user=None, # User 对象或 None
username: Optional[str] = None,
resource_type: Optional[str] = None,
resource_id: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None,
db: Session = None
) -> None:
"""
记录审计日志
参数:
action: 操作类型(必填),如 "login", "create_user", "delete_user", "publish"
user: 操作用户的 User 对象(可选,如果提供则自动填充 user_id 和 username
username: 直接指定用户名(如果 user 为 None 则必须提供)
resource_type: 资源类型,如 "user", "topic", "publish_record"
resource_id: 资源ID
details: 操作详情字典(如变更前后的值)
ip_address: IP 地址
user_agent: User-Agent
db: 数据库会话(必填)
"""
if db is None:
raise ValueError("db session is required")
# 确定 user_id 和 username
user_id = None
if user is not None:
user_id = getattr(user, 'id', None)
username = getattr(user, 'username', username)
if not username:
username = "anonymous"
log = AuditLog(
user_id=user_id,
username=username,
action=action,
resource_type=resource_type,
resource_id=resource_id,
details=details or {},
ip_address=ip_address,
user_agent=user_agent,
created_at=datetime.utcnow()
)
db.add(log)
db.commit()
# 不抛出异常,避免影响主流程
+2 -2
View File
@@ -6,7 +6,7 @@ import os
logger = logging.getLogger(__name__)
# 计算项目根目录(从本文件位置上升4层)
PROJECT_ROOT = Path(__file__).resolve().parents[4]
PROJECT_ROOT = Path(__file__).resolve().parents[1]
# 允许环境变量覆盖(适合容器部署)
if os.getenv('PROJECT_ROOT'):
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
@@ -26,7 +26,7 @@ def run_creator(topic_id: str = None):
cwd=str(PROJECT_ROOT),
capture_output=True,
text=True,
timeout=300 # 5分钟超时
timeout=1800 # 30分钟超时,避免AI撰写超时
)
if result.returncode != 0:
logger.error(f"Creator failed: {result.stderr}")
@@ -0,0 +1,118 @@
""" ModelScope 专用 LLM 客户端 """
import requests
import json
from typing import Optional
class LLMError(Exception):
pass
# 临时使用 NVIDIA 端点(ModelScope Key 已失效)
CONFIG = {
"base_url": "https://integrate.api.nvidia.com/v1",
"api_key": "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",
"model": "stepfun-ai/step-3.5-flash",
}
def call_llm(
prompt: str,
system_prompt: str = "你是一个专业的内容创作助手。",
temperature: float = 0.7,
max_tokens: int = 2000,
stream: bool = False,
) -> str:
"""调用 ModelScope LLM 生成文本"""
endpoint = f"{CONFIG['base_url'].rstrip('/')}/chat/completions"
headers = {
"Authorization": f"Bearer {CONFIG['api_key']}",
"Content-Type": "application/json"
}
payload = {
"model": CONFIG["model"],
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
"temperature": temperature,
"max_tokens": max_tokens,
"stream": stream,
}
try:
resp = requests.post(endpoint, json=payload, headers=headers, timeout=120, stream=stream)
if resp.status_code != 200:
raise LLMError(f"HTTP {resp.status_code}: {resp.text[:200]}")
if stream:
full = []
for line in resp.iter_lines():
if not line:
continue
if line.startswith(b'data: '):
data = line[6:]
if data == b'[DONE]':
break
try:
chunk = json.loads(data)
delta = chunk['choices'][0]['delta']
if 'reasoning_content' in delta and delta['reasoning_content']:
full.append(delta['reasoning_content'])
if 'content' in delta and delta['content']:
full.append(delta['content'])
except Exception:
continue
return "".join(full)
else:
data = resp.json()
msg = data["choices"][0]["message"]
content = msg.get('content') or msg.get('reasoning') or msg.get('reasoning_content')
return content.strip() if content else ''
except requests.RequestException as e:
raise LLMError(f"Request failed: {e}")
def expand_content_with_llm(
topic: dict,
section_title: str,
section_content: str,
context: str = ""
) -> str:
"""扩写大纲章节,返回包含 ## 标题的完整 Markdown"""
prompt = f"""你是一个专业的内容创作者。请将以下大纲扩展为完整的文章章节。
# 选题信息
- 标题:{topic.get('title')}
- 领域:{topic.get('field')}
- 核心观点:{topic.get('core_concept', '')}
- 受众痛点:{topic.get('audience_pain', '')}
- 独特视角:{topic.get('unique_angle', '')}
# 当前章节
## {section_title}
{section_content}
# 要求
- 以 `## {section_title}` 作为章节标题开头
- 字数:300-500 字
- 风格:客观、专业、易懂
- 使用 Markdown 格式
- 包含具体数据或案例(如果有)
- 保持与整体文章调性一致
直接输出完整的 Markdown 章节(包括 ## 标题和正文段落)。"""
if context:
prompt = f"# 参考资料\n{context}\n\n{prompt}"
try:
result = call_llm(prompt, temperature=0.8, max_tokens=2000)
return result.strip()
except Exception as e:
return f"## {section_title}\n\nLLM 调用失败:{e},请手动补充)"
# 测试
if __name__ == "__main__":
try:
print(f"[modelscope_client] 使用模型:{CONFIG['model']}")
resp = call_llm("你好,请用一句话介绍你自己。", max_tokens=50)
print(f"[modelscope_client] 响应:{resp}")
except Exception as e:
print(f"[modelscope_client] 错误:{e}")
+1 -1
View File
@@ -9,7 +9,7 @@ from typing import List
logger = logging.getLogger(__name__)
# 计算项目根目录(从本文件位置上升4层)
PROJECT_ROOT = Path(__file__).resolve().parents[4]
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if os.getenv('PROJECT_ROOT'):
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))