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
+9 -58
View File
@@ -6,8 +6,6 @@
import json, datetime, logging, sys, re, subprocess
from pathlib import Path
from typing import Dict, List
import base64
from io import BytesIO
PROJECT_ROOT = Path(__file__).parent.parent
# 添加项目根和 backend 路径,以导入 app.core.llm_client
@@ -15,18 +13,17 @@ sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / 'platform' / 'backend'))
# 导入 LLM 客户端(NVIDIA
from app.database import SessionLocal
from app.models import Topic
try:
from app.core.qnaigc_client import expand_content_with_llm # type: ignore
HAVE_LLM = True
from app.core.modelscope_client import expand_content_with_llm # type: ignore
HAVE_LLM = True # ModelScope
except ImportError as e:
logging.warning(f"LLM client unavailable: {e}")
HAVE_LLM = False
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# 导入数据库模型
from app.database import SessionLocal
from app.models import Topic
DATA_DIR = PROJECT_ROOT / "automation" / "data"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
@@ -35,6 +32,7 @@ RELEASE_DIR = DATA_DIR / "releases"
TEMPLATES_DIR = PROJECT_ROOT / "automation" / "templates"
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
GEN_TIME = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
logging.basicConfig(
level=logging.INFO,
@@ -141,10 +139,6 @@ class Writer:
parts.append(expanded + "\n\n")
full_md = "\n".join(parts).strip()
# 添加文末声明
full_md += f"\n<p>(本文由宇之然AI助手生成,数据来源可靠,内容经合规审查)</p>\n"
full_md += f"<p><em>生成时间:{TODAY}</em></p>\n"
return full_md
def generate_platform_html(self, markdown: str, platform: str) -> str:
@@ -159,7 +153,7 @@ class Writer:
template = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>{{TITLE}}</title></head><body><h1>{{TITLE}}</h1><!-- CONTENT --></body></html>"
# 替换变量
html = template.replace("{{TITLE}}", title).replace("{{DATE}}", TODAY)
html = template.replace("{{TITLE}}", title).replace("{{DATE}}", TODAY).replace("{{GEN_TIME}}", GEN_TIME).replace("{{GEN_TIME}}", GEN_TIME)
# 注入内容 (简单处理:markdown 转 HTML 可以用 marked.js 或 simple转换,这里暂时用 <pre> 包裹或简单段落化)
# 为了快速展示,我们将 markdown 的段落转换为 <p> 标签
@@ -178,8 +172,6 @@ class Writer:
# 微信公众号可能还需要摘要等,模板已处理
pass
if platform == "xiaohongshu":
html = self._fill_image_placeholders(html, platform, title)
return html
def _markdown_to_html(self, md: str) -> str:
@@ -214,7 +206,6 @@ class Writer:
return out_path
def mark_draft(self):
"""标记选题为「待审查」,同时更新数据库"""
"""标记选题为「待发布」,同时更新数据库"""
# 更新 JSON 文件
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
@@ -236,7 +227,7 @@ class Writer:
if topic_db:
topic_db.status = '待审查'
db.commit()
logger.info(f"选题 {self.topic_id} 状态已更新为 draft(数据库)")
logger.info(f"选题 {self.topic_id} 状态已更新为待审查(数据库)")
else:
logger.warning(f"数据库中未找到选题 {self.topic_id}")
except Exception as e:
@@ -245,7 +236,7 @@ class Writer:
finally:
db.close()
logger.info(f"选题 {self.topic_id} 状态更新为「待审查」(JSON")
logger.info(f"选题 {self.topic_id} 状态更新为「待发布」(JSON")
"""标记选题为「待发布」"""
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
@@ -256,7 +247,7 @@ class Writer:
break
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
logger.info(f"选题 {self.topic_id} 状态更新为「待审查")
logger.info(f"选题 {self.topic_id} 状态更新为「待发布")
def run(self):
logger.info("开始撰写阶段")
@@ -269,46 +260,6 @@ class Writer:
logger.info(f"撰写完成,状态改为 draft,待合规审核")
return {"ok": True, "files": results}
def _image_to_data_url(self, img_path: Path, fmt: str = None) -> str:
data = img_path.read_bytes()
if fmt is None:
fmt = img_path.suffix.lstrip('.').lower()
b64 = base64.b64encode(data).decode('ascii')
return f"data:image/{fmt};base64,{b64}"
def _generate_and_inline_images(self, platform: str, title: str) -> dict:
from scripts.image_generator import ImageGenerator
gen = ImageGenerator()
files = gen.generate_all_placeholders(title, platform)
mapping = {}
cover = files.get('cover')
if cover and cover.exists():
mapping['main-image-src'] = self._image_to_data_url(cover)
thumbs = []
for k, p in files.items():
if k != 'cover' and p.exists():
thumbs.append(self._image_to_data_url(p))
mapping['thumbnail-srcs'] = thumbs
return mapping
def _fill_image_placeholders(self, html: str, platform: str, title: str) -> str:
if platform != 'xiaohongshu':
return html
mapping = self._generate_and_inline_images(platform, title)
# Replace main image placeholder
main_ph = '<img src="" alt="封面图" class="main-image">'
if 'main-image-src' in mapping:
new_main = f'<img src="{mapping["main-image-src"]}" alt="封面图" class="main-image">'
html = html.replace(main_ph, new_main)
# Replace thumbnail placeholders (6)
thumbs = mapping.get('thumbnail-srcs', [])
for idx, src in enumerate(thumbs[:6], start=1):
ph = f'<img src="" alt="{idx}" class="thumbnail">'
new_thumb = f'<img src="{src}" alt="{idx}" class="thumbnail">'
html = html.replace(ph, new_thumb)
return html
def main():
import argparse
parser = argparse.ArgumentParser()