feat: 内容数据迁移至数据库,合规审查全链路打通

- 文章 HTML 存储从文件系统迁移至 articles 表,删除 releases 目录
- 合规审查从 DB 读取 HTML,审查结果写回 DB,通过后自动推进至待发布
- 新增 todayCount 筛选按钮,与系统概览统计数据一致
- 全屏预览修复:提升 z-index 超过侧边栏,添加退出全屏/关闭按钮
- 统一 '优化' → '审查' 命名,消除前后端术语不一致
- 调度器创作完成后自动触发审查(生成 → 审查 → 待发布)
- 清理旧备份/调试文件、过期大纲和研究笔记
This commit is contained in:
Yuzhiran Dev
2026-05-13 17:33:56 +08:00
parent bc6a302e59
commit 233e23016c
234 changed files with 5670 additions and 10651 deletions
+6 -1
View File
@@ -16,4 +16,9 @@ ENVIRONMENT=development
LOG_LEVEL=INFO
# CORS配置(生产环境)
ALLOWED_ORIGINS=http://localhost:8080,https://yourdomain.com
ALLOWED_ORIGINS=http://localhost:8080,https://yourdomain.com
# LLM API配置
LLM_API_KEY=nvapi-your-key-here
LLM_BASE_URL=https://integrate.api.nvidia.com/v1
LLM_MODEL=google/gemma-3n-e4b-it
+19 -30
View File
@@ -1,53 +1,42 @@
from fastapi import APIRouter, HTTPException, Query, Depends
from sqlalchemy.orm import Session
from pathlib import Path
import os
from datetime import datetime, date
from ..database import get_db
from ..models import User
from ..models import User, Article
from .auth import get_current_user
router = APIRouter(prefix="/api/articles", tags=["articles"])
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
@router.get("/drafts")
def list_drafts(publish_date: str = None, current_user: User = Depends(get_current_user)):
"""列出指定日期的草稿文件(三平台)"""
if not publish_date:
publish_date = date.today().isoformat()
base_dir = PROJECT_ROOT / "automation" / "data" / "releases" / publish_date
if not base_dir.exists():
raise HTTPException(status_code=404, detail="No releases for this date")
platforms = ["zhihu", "wechat", "xiaohongshu"]
def list_drafts(topic_id: str = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""从 articles 表列出草稿"""
query = db.query(Article)
if topic_id:
query = query.filter(Article.topic_id == topic_id)
articles = query.order_by(Article.created_at.desc()).all()
result = {}
for p in platforms:
path = base_dir / p
if path.exists():
files = sorted([f.name for f in path.glob("*.html") if f.is_file()])
result[p] = files
else:
result[p] = []
return {"date": publish_date, "files": result}
for a in articles:
result.setdefault(a.platform, []).append({"id": a.id, "topic_id": a.topic_id, "status": a.status})
return {"articles": result}
@router.get("/{topic_id}/preview")
def preview_article(topic_id: str, platform: str = "zhihu", publish_date: str = None, current_user: User = Depends(get_current_user)):
"""预览某选题的HTML内容"""
if not publish_date:
publish_date = date.today().isoformat()
filename = f"{platform}_{topic_id}_{platform}.html"
file_path = PROJECT_ROOT / "automation" / "data" / "releases" / publish_date / platform / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail=f"Article not found: {file_path}")
content = file_path.read_text(encoding='utf-8')
return {"topic_id": topic_id, "platform": platform, "html": content}
def preview_article(topic_id: str, platform: str = "zhihu", current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""从 articles 表预览某选题的 HTML 内容"""
article_id = f"{platform}_{topic_id}"
article = db.query(Article).filter(Article.id == article_id).first()
if not article or not article.html_content:
raise HTTPException(status_code=404, detail=f"Article not found for {topic_id} on {platform}")
return {"topic_id": topic_id, "platform": platform, "html": article.html_content}
@router.get("/optimization-report")
def get_optimization_report(publish_date: str = None, current_user: User = Depends(get_current_user)):
"""获取合规优化报告"""
if not publish_date:
publish_date = date.today().isoformat()
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
report_path = PROJECT_ROOT / "automation" / "data" / "drafts" / publish_date / "optimization_report.json"
if not report_path.exists():
raise HTTPException(status_code=404, detail="No optimization report for this date")
+5 -2
View File
@@ -17,11 +17,14 @@ router = APIRouter(prefix="/api/calendar", tags=["calendar"])
@router.get("", response_model=List[ContentCalendarResponse])
def get_calendar(
year: int = Query(...),
month: int = Query(...),
year: Optional[int] = Query(None),
month: Optional[int] = Query(None),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
today = date.today()
year = year or today.year
month = month or today.month
start = date(year, month, 1)
last_day = monthrange(year, month)[1]
end = date(year, month, last_day)
+2 -2
View File
@@ -35,8 +35,8 @@ async def create_publish_record(
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} 状态不是待发布")
if topic.status not in ('ready', '待发布'):
raise HTTPException(status_code=400, detail=f"选题 {req.topic_id} 状态不是待发布(当前: {topic.status}")
# 更新选题状态
topic.status = '已发布'
+70 -7
View File
@@ -69,18 +69,37 @@ def trigger_generation(topic_id: str = Body(None, embed=True), db: Session = Dep
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/optimize/run", dependencies=[Depends(get_current_user)])
def trigger_optimization(topic_ids: List[str] = Body(None, embed=True), db: Session = Depends(get_db)):
@router.post("/review/run", dependencies=[Depends(get_current_user)])
def trigger_review(topic_ids: List[str] = Body(None, embed=True), db: Session = Depends(get_db)):
try:
result = run_optimizer(topic_ids)
if not result["ok"]:
raise HTTPException(status_code=500, detail=result["error"])
report = result.get("report")
if report:
sync_all_topics()
return {"message": "Optimization completed", "summary": report["summary"]}
if report and report["summary"]["total_articles"] > 0:
s = report["summary"]
passed = s["passed_auto"]
manual = s["need_manual"]
total = s["total_articles"]
avg = s["average_score"]
msg = f"审查完成: {total} 篇, {passed} 篇通过 ({avg:.0f}分)"
if manual:
msg += f", {manual} 篇需人工处理"
return {"message": msg, "summary": s}
else:
return {"message": "Optimization completed", "stdout": result.get("stdout", "")}
if topic_ids:
updated = 0
for tid in topic_ids:
topic = db.query(Topic).filter(Topic.id == tid).first()
if topic and topic.status in ('review', '待审查'):
topic.status = 'ready'
if not topic.generated_at:
topic.generated_at = datetime.utcnow()
updated += 1
db.commit()
if updated:
logger.info(f"Review: {updated} topics advanced to 'ready' (no release files)")
return {"message": "审查完成(未找到 release 文件,仅推进状态)", "stdout": result.get("stdout", "")}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@@ -152,4 +171,48 @@ def refresh_all():
@router.get("/scheduler/status", dependencies=[Depends(get_current_user)])
def get_scheduler_status():
return {"jobs": scheduler.get_jobs()}
return {"running": scheduler._started, "jobs": scheduler.get_jobs()}
@router.get("/modules/status", dependencies=[Depends(get_current_user)])
def get_modules_status():
today_str = date.today().isoformat()
module_logs = {
"creator": {
"key": "🤖 内容创作引擎",
"log": LOGS_DIR / f"creator_{today_str}.log",
"config_file": PROJECT_ROOT / "automation" / "data" / "outlines" / today_str,
},
"optimizer": {
"key": "🔍 内容优化器",
"log": LOGS_DIR / f"optimizer_{today_str}.log",
"config_file": PROJECT_ROOT / "automation" / "data" / "drafts" / today_str,
},
"collector": {
"key": "📡 内容收集器",
"log": LOGS_DIR / f"collector_{today_str}.log",
"config_file": PROJECT_ROOT / "automation" / "data",
},
}
modules = []
for mod_id, cfg in module_logs.items():
log_file = cfg["log"]
last_run = None
task_count = 0
success_rate = None
if log_file.exists():
mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
last_run = mtime.strftime("%Y-%m-%d %H:%M")
content = log_file.read_text(encoding="utf-8", errors="ignore")
task_count = content.count("完成") + content.count("success") + content.count("SUCCESS")
total = task_count + content.count("失败") + content.count("failed") + content.count("ERROR")
success_rate = round(task_count / total * 100) if total > 0 else None
modules.append({
"id": mod_id,
"title": cfg["key"],
"status": "running" if log_file.exists() else "stopped",
"last_run": last_run or "从未运行",
"task_count": task_count,
"success_rate": success_rate,
})
return {"modules": modules, "scheduler": {"running": scheduler._started, "jobs": scheduler.get_jobs()}}
+19 -16
View File
@@ -88,9 +88,9 @@ def start_task(
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
from datetime import datetime
from datetime import datetime, timezone
task.status = "running"
task.started_at = datetime.now()
task.started_at = datetime.now(timezone.utc)
task.message = "任务已启动"
task.progress = 0
db.commit()
@@ -130,16 +130,17 @@ def complete_task(
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
from datetime import datetime
from datetime import datetime, timezone
finished = datetime.now(timezone.utc)
task.status = "completed"
task.finished_at = datetime.now()
task.finished_at = finished
task.progress = 100
if message:
task.message = message
if result_data:
task.result_data = result_data
if task.started_at:
task.duration = int((task.finished_at - task.started_at).total_seconds())
task.duration = int((finished - task.started_at).total_seconds())
db.commit()
db.refresh(task)
return task
@@ -156,12 +157,13 @@ def fail_task(
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
from datetime import datetime
from datetime import datetime, timezone
finished = datetime.now(timezone.utc)
task.status = "failed"
task.finished_at = datetime.now()
task.finished_at = finished
task.error_msg = error_msg
if task.started_at:
task.duration = int((task.finished_at - task.started_at).total_seconds())
task.duration = int((finished - task.started_at).total_seconds())
db.commit()
db.refresh(task)
return task
@@ -189,8 +191,9 @@ def run_creator_task(
current_user=Depends(get_current_user)
):
import threading
from datetime import datetime
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
task_id = f"task_{uuid.uuid4().hex[:16]}"
task = ContentTask(
@@ -198,7 +201,7 @@ def run_creator_task(
topic_id=topic_id,
stage="creator",
status="running",
started_at=datetime.now(),
started_at=now,
created_by=current_user.username
)
db.add(task)
@@ -210,22 +213,22 @@ def run_creator_task(
def _run():
try:
result = run_creator(topic_id)
from datetime import datetime
finished = datetime.now(timezone.utc)
task.status = "completed"
task.finished_at = datetime.now()
task.finished_at = finished
task.progress = 100
task.message = "创作完成"
task.result_data = result or {}
if task.started_at:
task.duration = int((task.finished_at - task.started_at).total_seconds())
task.duration = int((finished - task.started_at).total_seconds())
db.commit()
except Exception as e:
from datetime import datetime
finished = datetime.now(timezone.utc)
task.status = "failed"
task.finished_at = datetime.now()
task.finished_at = finished
task.error_msg = str(e)
if task.started_at:
task.duration = int((task.finished_at - task.started_at).total_seconds())
task.duration = int((finished - task.started_at).total_seconds())
db.commit()
thread = threading.Thread(target=_run)
+7 -3
View File
@@ -167,9 +167,13 @@ def delete_topic(
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
raise HTTPException(status_code=404, detail="Topic not found")
db.delete(topic)
db.commit()
return {"ok": True}
try:
db.delete(topic)
db.commit()
return {"ok": True}
except Exception as e:
db.rollback()
raise HTTPException(status_code=400, detail=f"删除失败:该选题有关联数据(文章/发布记录等),请先删除关联数据。{str(e)}")
@router.post("/{topic_id}/score")
+62 -23
View File
@@ -17,6 +17,7 @@ def run_creator(topic_id: str = None):
Args:
topic_id: 可选,指定要创作的选题ID。不指定则创作优先级最高的选题。
"""
from datetime import datetime, timezone
script_path = PROJECT_ROOT / "scripts" / "creator.py"
venv_python = PROJECT_ROOT / "platform" / "backend" / "venv" / "bin" / "python"
if venv_python.exists():
@@ -30,32 +31,70 @@ def run_creator(topic_id: str = None):
cwd=str(PROJECT_ROOT),
capture_output=True,
text=True,
timeout=1800 # 30分钟超时,避免AI撰写超时
timeout=1800
)
if result.returncode != 0:
logger.error(f"Creator failed: {result.stderr}")
if topic_id:
try:
sync_topic_to_db(topic_id)
except Exception as e:
logger.warning(f"Sync after creation failed: {e}")
return {"ok": False, "error": result.stderr}
# 解析日志,找出选择了哪个选题
topic_id = None
for line in result.stdout.splitlines():
if "选择了选题:" in line:
# 格式: 2026-04-16 ... INFO - 选择了选题: 标题 (优先级: X)
# 标题可能在行内,但ID不一定有。我们稍后用文件同步。
logger.info(line.strip())
if "选题" in line and "已标记为「待发布」" in line:
# 如: 2026-04-16 ... INFO - 选题 A01 已标记为「待发布」
import re
from .sync import sync_topic_to_db
m = re.search(r'选题\s+([A-Za-z0-9]+)', line)
if m:
topic_id = m.group(1)
if not topic_id:
for line in result.stdout.splitlines():
if "选题" in line and "已标记为「待发布」" in line:
import re
m = re.search(r'选题\s+([A-Za-z0-9]+)', line)
if m:
topic_id = m.group(1)
if topic_id:
try:
from ..database import SessionLocal
from ..models import Topic, Article
db = SessionLocal()
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if topic:
topic.generated_at = datetime.now(timezone.utc)
if topic.status in ('pending', '待处理'):
topic.status = 'review'
db.commit()
logger.info(f"Topic {topic_id} updated: generated_at set, status→{topic.status}")
# 将 release 文件同步到 articles 表,然后删除文件系统文件
releases_dir = PROJECT_ROOT / "automation" / "data" / "releases"
if releases_dir.exists():
for dd in sorted(releases_dir.iterdir(), reverse=True):
if not dd.is_dir() or not dd.name[:4].isdigit():
continue
for platform_dir in ["zhihu", "wechat", "xiaohongshu"]:
pdir = dd / platform_dir
if not pdir.exists():
continue
for hf in pdir.glob(f"*{topic_id}.html"):
html = hf.read_text(encoding='utf-8')
article_id = f"{platform_dir}_{topic_id}"
existing = db.query(Article).filter(Article.id == article_id).first()
if existing:
existing.html_content = html
else:
db.add(Article(
id=article_id,
topic_id=topic_id,
platform=platform_dir,
file_path=f"db:{article_id}",
html_content=html,
status="draft",
))
hf.unlink()
logger.info(f"Synced {hf.name} → articles table, deleted file")
# 清理空目录
for platform_dir in ["zhihu", "wechat", "xiaohongshu"]:
pdir = dd / platform_dir
if pdir.exists() and not any(pdir.iterdir()):
pdir.rmdir()
if dd.exists() and not any(dd.iterdir()):
dd.rmdir()
db.commit()
except Exception as e:
logger.warning(f"DB sync after creation failed: {e}")
return {
"ok": True,
"topic_id": topic_id,
@@ -1,118 +0,0 @@
""" 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}" 开始
- 字数:200-300 字(精炼为主)
- 语言:直白、有冲击力,避免空洞套话
- 使用 Markdown 格式
- 每个论点配具体案例或数据支撑
- 确保与整体文章调性一致
直接输出完整 Markdown 章节(包括标题和正文)。"""
if context:
prompt = f"# 参考资料\n{context}\n\n{prompt}"
try:
result = call_llm(prompt, temperature=0.8, max_tokens=1000)
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}")
+36 -54
View File
@@ -1,57 +1,50 @@
"""
NVIDIA 专用 LLM 客户端(优化配置)
支持 Google Gemma 和其他 NVIDIA 模型
Unified LLM Client
支持 NVIDIA / 兼容 OpenAI 格式的 API,配置从环境变量读取
"""
import os
import requests
import json
from typing import Optional, Dict, Any
from pathlib import Path
from dotenv import load_dotenv
env_path = Path(__file__).resolve().parents[2] / ".env"
load_dotenv(env_path)
class LLMError(Exception):
pass
# 固定配置(你的可用 key
CONFIG = {
"base_url": "https://integrate.api.nvidia.com/v1",
"api_key": "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",
"base_url": os.getenv("LLM_BASE_URL", "https://integrate.api.nvidia.com/v1"),
"api_key": os.getenv("LLM_API_KEY", ""),
"model": os.getenv("LLM_MODEL", "google/gemma-3n-e4b-it"),
}
def call_llm(
prompt: str,
model: str = "google/gemma-3n-e4b-it",
model: Optional[str] = None,
system_prompt: str = "你是一个专业的内容创作助手。",
temperature: float = 0.20,
max_tokens: int = 512,
max_tokens: int = 2048,
top_p: float = 0.70,
frequency_penalty: float = 0.00,
presence_penalty: float = 0.00,
stream: bool = False,
additional_params: Optional[Dict[str, Any]] = None,
) -> str:
"""
调用 NVIDIA LLM 生成文本
参数:
prompt: 用户提示词
model: 模型名称,默认 google/gemma-3n-e4b-it
system_prompt: 系统提示词
temperature: 温度参数 (0.0-2.0),默认 0.20
max_tokens: 最大生成 token 数,默认 512
top_p: 核采样参数 (0.0-1.0),默认 0.70
frequency_penalty: 频率惩罚 (0.0-2.0),默认 0.00
presence_penalty: 存在惩罚 (0.0-2.0),默认 0.00
stream: 是否流式输出,默认 False
additional_params: 额外参数(如 reasoning_effort
"""
if not CONFIG["api_key"]:
raise LLMError("LLM_API_KEY 未配置,请在 backend/.env 中设置")
endpoint = f"{CONFIG['base_url'].rstrip('/')}/chat/completions"
headers = {
"Authorization": f"Bearer {CONFIG['api_key']}",
"Content-Type": "application/json"
}
# 基础参数
payload = {
"model": model,
"model": model or CONFIG["model"],
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
@@ -63,11 +56,9 @@ def call_llm(
"presence_penalty": presence_penalty,
"stream": stream,
}
# 添加额外参数(如 reasoning_effort
if additional_params:
payload.update(additional_params)
try:
resp = requests.post(endpoint, json=payload, headers=headers, timeout=120, stream=stream)
if resp.status_code != 200:
@@ -75,22 +66,18 @@ def call_llm(
if stream:
full = []
for line in resp.iter_lines():
if not line:
continue
if not line: continue
if line.startswith(b'data: '):
data = line[6:]
if data == b'[DONE]':
break
if data == b'[DONE]': break
try:
chunk = json.loads(data)
delta = chunk['choices'][0]['delta']
# 支持 reasoning_content 或 reasoning 字段
if 'reasoning_content' in delta and delta['reasoning_content']:
if delta.get('reasoning_content'):
full.append(delta['reasoning_content'])
if 'content' in delta and delta['content']:
if delta.get('content'):
full.append(delta['content'])
except Exception:
continue
except Exception: continue
return "".join(full)
else:
data = resp.json()
@@ -100,9 +87,13 @@ def call_llm(
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"""你是一个专业的内容创作者,风格精炼、直接、切中要点。请将以下大纲扩展为完整的文章章节,要求如下:
def expand_content_with_llm(
topic: dict,
section_title: str,
section_content: str,
context: str = ""
) -> str:
prompt = f"""你是一个专业的内容创作者,风格精炼、直接、切中要点。请将以下大纲扩展为完整的文章章节:
### 选题信息
标题:{topic.get('title')}
@@ -117,7 +108,7 @@ def expand_content_with_llm(topic: dict, section_title: str, section_content: st
### 输出要求
- 以 "## {section_title}" 开始
- 字数:200-300 字(精炼为主)
- 字数:200-300 字
- 语言:直白、有冲击力,避免空洞套话
- 使用 Markdown 格式
- 每个论点配具体案例或数据支撑
@@ -128,24 +119,15 @@ def expand_content_with_llm(topic: dict, section_title: str, section_content: st
prompt = f"# 参考资料\n{context}\n\n{prompt}"
try:
result = call_llm(
prompt,
model="google/gemma-3n-e4b-it",
temperature=0.20,
max_tokens=1000,
top_p=0.70,
frequency_penalty=0.00,
presence_penalty=0.00
)
result = call_llm(prompt, temperature=0.3, max_tokens=1500)
return result.strip()
except Exception as e:
return f"## {section_title}\n\nLLM 调用失败:{e},请手动补充)"
# 测试
if __name__ == "__main__":
try:
print(f"[nvidia_client] 使用模型: google/gemma-3n-e4b-it")
print(f"[nvidia_client] 模型:{CONFIG['model']}")
resp = call_llm("你好,请用一句话介绍你自己。", max_tokens=50)
print(f"[nvidia_client] 响应: {resp}")
print(f"[nvidia_client] 响应{resp}")
except Exception as e:
print(f"[nvidia_client] 错误: {e}")
print(f"[nvidia_client] 错误{e}")
+6 -2
View File
@@ -9,7 +9,7 @@ from typing import List
logger = logging.getLogger(__name__)
# 计算项目根目录(从本文件位置上升4层)
PROJECT_ROOT = Path(__file__).resolve().parents[1]
PROJECT_ROOT = Path(__file__).resolve().parents[4]
if os.getenv('PROJECT_ROOT'):
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
@@ -20,7 +20,11 @@ def run_optimizer(topic_ids: List[str] = None):
topic_ids: 可选,指定要优化的选题ID列表。不指定则优化所有 draft 文章。
"""
script_path = PROJECT_ROOT / "scripts" / "compliance_optimizer.py"
cmd = ["python3", str(script_path)]
venv_python = PROJECT_ROOT / "platform" / "backend" / "venv" / "bin" / "python"
if venv_python.exists():
cmd = [str(venv_python), str(script_path)]
else:
cmd = ["python3", str(script_path)]
if topic_ids:
cmd.extend(["--topic-ids", ','.join(topic_ids)])
logger.info(f"[DEBUG] Running optimizer with topic_ids={topic_ids}, cmd={' '.join(cmd)}")
-112
View File
@@ -1,112 +0,0 @@
"""
qnaigc 专用 LLM 客户端
模型:arcee-ai/trinity-large-preview
"""
import requests
import json
from typing import Optional
class LLMError(Exception):
pass
CONFIG = {
"base_url": "https://api.qnaigc.com/v1",
"api_key": "sk-2cb9561a18351015d3120ffac4abae0480fa17e0d28469bdce5fc905d1a42e0d",
"model": "arcee-ai/trinity-large-preview",
}
def call_llm(
prompt: str,
system_prompt: str = "你是一个专业的内容创作助手。",
temperature: float = 0.7,
max_tokens: int = 2000,
stream: bool = False,
) -> str:
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}" 开始
- 字数:200-300 字(精炼为主)
- 语言:直白、有冲击力,避免空洞套话
- 使用 Markdown 格式
- 每个论点配具体案例或数据支撑
- 确保与整体文章调性一致
直接输出完整 Markdown 章节(包括标题和正文)。"""
if context:
prompt = f"# 参考资料\n{context}\n\n{prompt}"
try:
result = call_llm(prompt, temperature=0.8, max_tokens=1000)
return result.strip()
except Exception as e:
return f"## {section_title}\n\nLLM 调用失败:{e},请手动补充)"
# 测试
if __name__ == "__main__":
try:
print(f"[qnaigc_client] 使用模型: {CONFIG['model']}")
resp = call_llm("你好,请用一句话介绍你自己。", max_tokens=50)
print(f"[qnaigc_client] 响应: {resp}")
except Exception as e:
print(f"[qnaigc_client] 错误: {e}")
+9 -1
View File
@@ -60,8 +60,16 @@ class TaskScheduler:
logger.info("[Scheduled] Starting content generation...")
result = run_creator()
logger.info("[Scheduled] Generation completed: %s", result)
created_id = result.get("topic_id") if isinstance(result, dict) else None
if created_id:
logger.info("[Scheduled] Running compliance review on %s...", created_id)
review_result = run_optimizer([created_id])
if review_result.get("ok"):
logger.info("[Scheduled] Review completed for %s", created_id)
else:
logger.warning("[Scheduled] Review failed: %s", review_result.get("error"))
except Exception as e:
logger.exception("[Scheduled] Generation failed: %s", e)
logger.exception("[Scheduled] Generation pipeline failed: %s", e)
def _run_optimize(self):
try:
+10 -84
View File
@@ -1,20 +1,10 @@
import json
from datetime import datetime, date
from pathlib import Path
from sqlalchemy.orm import Session
from ..database import SessionLocal
from ..models import Topic
import os
# 计算项目根目录(从本文件位置上升4层)
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if os.getenv('PROJECT_ROOT'):
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
TOPICS_FILE = PROJECT_ROOT / "automation" / "data" / "sustainability_topics.json"
def sync_topic_to_db(topic_id: str, db: Session = None) -> Topic:
"""注意:此函数原用于将JSON单个选题同步到数据库。现已不需要,保留用于兼容。当前方向相反(DB为主),此处仅从数据库导出到JSON(如果需要)"""
# 为了不破坏旧调用,我们改为从数据库读取并写入 JSON 文件(单条更新)
close_db = False
if db is None:
db = SessionLocal()
@@ -23,88 +13,24 @@ def sync_topic_to_db(topic_id: str, db: Session = None) -> Topic:
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
raise ValueError(f"Topic {topic_id} not found in DB")
# 写入 JSON 文件(作为备份)
try:
if TOPICS_FILE.exists():
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
else:
topics = []
# 转为字典
tdict = {
'id': topic.id,
'title': topic.title,
'field': topic.field,
'format': topic.format,
'core_concept': topic.core_concept,
'audience_pain': topic.audience_pain,
'unique_angle': topic.unique_angle,
'priority': topic.priority,
'priority_score': topic.priority_score,
'total_score': topic.total_score,
'status': topic.status,
'cases': topic.cases or [],
'source_file': topic.source_file,
'created_at': topic.created_at.isoformat() if topic.created_at else None,
'updated_at': topic.updated_at.isoformat() if topic.updated_at else None,
'ready_at': topic.ready_at.isoformat() if topic.ready_at else None,
'published_at': topic.published_at.isoformat() if topic.published_at else None,
'compliance_score': topic.compliance_score,
'platform_urls': topic.platform_urls or {}
}
# 更新或追加
found = False
for i, t in enumerate(topics):
if t['id'] == topic_id:
topics[i] = tdict
found = True
break
if not found:
topics.append(tdict)
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"[Warning] JSON backup failed: {e}")
return topic
finally:
if close_db:
db.close()
def sync_all_topics():
"""导出所有选题到 JSON 文件(用于备份或兼容)"""
db = SessionLocal()
logger = __import__('logging').getLogger(__name__)
try:
topics = db.query(Topic).order_by(Topic.created_at).all()
topic_list = []
for t in topics:
tdict = {
'id': t.id,
'title': t.title,
'field': t.field,
'format': t.format,
'core_concept': t.core_concept,
'audience_pain': t.audience_pain,
'unique_angle': t.unique_angle,
'priority': t.priority,
'priority_score': t.priority_score,
'total_score': t.total_score,
'status': t.status,
'cases': t.cases or [],
'source_file': t.source_file,
'created_at': t.created_at.isoformat() if t.created_at else None,
'updated_at': t.updated_at.isoformat() if t.updated_at else None,
'ready_at': t.ready_at.isoformat() if t.ready_at else None,
'published_at': t.published_at.isoformat() if t.published_at else None,
'compliance_score': t.compliance_score,
'platform_urls': t.platform_urls or {}
}
topic_list.append(tdict)
TOPICS_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topic_list, f, ensure_ascii=False, indent=2)
print(f"✅ 导出 {len(topic_list)} 个选题到 JSON (兼容模式)")
finally:
from sqlalchemy import func
db = SessionLocal()
total = db.query(Topic).count()
rows = db.query(Topic.status, func.count(Topic.id)).group_by(Topic.status).all()
by_status = {s: int(c) for s, c in rows}
logger.info(f"sync_all_topics: DB verified — {total} topics total, statuses: {by_status}")
db.close()
except Exception as e:
logger.error(f"sync_all_topics: DB connection failed — {e}")
raise
if __name__ == "__main__":
sync_all_topics()
View File
+4
View File
@@ -75,6 +75,10 @@ def import_initial_data():
for cfg in default_system_configs:
if db.query(SystemConfig).filter(SystemConfig.key == cfg["key"]).first() is None:
db.add(SystemConfig(**cfg))
if db.query(SystemConfig).filter(SystemConfig.key == "review_llm_id").first() is None:
first_llm = db.query(LLMConfig).filter(LLMConfig.is_active == True).first()
if first_llm:
db.add(SystemConfig(key="review_llm_id", value=str(first_llm.id), description="审查使用的 LLM 配置 ID(留空则用环境变量默认值)"))
db.commit()
print("✅ 插入默认系统配置")
+21 -2
View File
@@ -11,14 +11,33 @@ from .database import engine, get_db, init_db
from .models import Base
from .api import topics, system, articles, publishing, auth, admin, audit, optimizer_logs, cases, task_logs, llm_configs, system_configs, topic_config, calendar, metrics, assets, tasks, platform_config
from .initial_data import import_initial_data
from .core.scheduler import scheduler
app = FastAPI(title="宇之然内容创作平台", version="0.1.0")
logger = logging.getLogger(__name__)
# CORS
@app.on_event("startup")
def start_scheduler():
scheduler.start()
logger.info("Background scheduler started")
@app.on_event("shutdown")
def stop_scheduler():
scheduler.shutdown()
logger.info("Background scheduler shut down")
# CORS - local-only origins
_local_origins = [
"http://localhost:8001",
"http://127.0.0.1:8001",
"http://localhost:8000",
"http://127.0.0.1:8000",
]
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=_local_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
+1 -1
View File
@@ -204,7 +204,7 @@ class Article(Base):
class PublishRecord(Base):
__tablename__ = "publish_records"
id = Column(Integer, primary_key=True, index=True)
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
topic_id = Column(String, ForeignKey("topics.id"), nullable=False)
platform = Column(String, nullable=False)
action = Column(String, nullable=False)
+1 -1
View File
@@ -80,7 +80,7 @@ class TopicBase(BaseModel):
class TopicCreate(BaseModel):
id: Optional[str] = None
field_id: Optional[int] = None
title: str
title: str = Field(..., min_length=1)
format: Optional[str] = None
core_concept: Optional[str] = None
audience_pain: Optional[str] = None
Binary file not shown.
+249 -144
View File
@@ -5,29 +5,31 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 系统管理</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
.navbar-title { font-size: 20px; font-weight: 600; }
.navbar-user { display: flex; align-items: center; gap: 16px; }
.user-info { display: flex; align-items: center; gap: 8px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; min-height: calc(100vh - 60px); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.mobile-nav-btn { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; background: none; border: none; font-size: 10px; color: #909399; cursor: pointer; padding: 4px; }
.mobile-nav-btn.active { color: #409eff; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.card-loading { display: flex; justify-content: center; align-items: center; min-height: 200px; color: #909399; font-size: 14px; }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; color: #909399; }
.empty-state .empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.4; }
.empty-state .empty-text { font-size: 14px; margin-bottom: 16px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
.filter-bar { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
.toolbar { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.el-table { width: 100%; }
.el-table .el-table__cell { word-break: break-word; }
.stat-summary { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
.stat-item { background: #f0f5ff; border-radius: 8px; padding: 12px 20px; display: flex; flex-direction: column; align-items: center; min-width: 100px; }
.stat-item .num { font-size: 24px; font-weight: 600; color: #409eff; }
.stat-item .label { font-size: 12px; color: #909399; margin-top: 4px; }
.mobile-card-list { display: none; }
@media (max-width: 768px) {
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
.card { padding: 16px; }
.data-table { display: none; }
@@ -46,6 +48,9 @@
.mobile-card-label { color: #909399; flex-shrink: 0; margin-right: 8px; }
.mobile-card-value { color: #303133; text-align: right; word-break: break-word; }
.mobile-card-actions { display: flex; gap: 8px; justify-content: flex-end; padding-top: 10px; margin-top: 6px; border-top: 1px solid #ebeef5; }
.stat-summary { gap: 8px; }
.stat-item { min-width: 70px; padding: 8px 12px; }
.stat-item .num { font-size: 18px; }
}
</style>
<script src="navigation-component.js"></script>
@@ -57,18 +62,39 @@
<navigation-component current-page="admin" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<el-tabs v-model="activeTab">
<el-tab-pane label="案例管理" name="cases">
<div class="toolbar">
<el-button type="primary" @click="showCaseDialog()">新增案例</el-button>
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">⚙️ 系统管理</h2>
<div class="filter-bar">
<el-button size="default" :type="activeTab === 'cases' ? 'primary' : ''" @click="switchTab('cases')">案例管理</el-button>
<el-button size="default" :type="activeTab === 'tasklogs' ? 'primary' : ''" @click="switchTab('tasklogs')">任务日志</el-button>
<el-button size="default" :type="activeTab === 'llmconfigs' ? 'primary' : ''" @click="switchTab('llmconfigs')">LLM配置</el-button>
<el-button size="default" :type="activeTab === 'systemconfigs' ? 'primary' : ''" @click="switchTab('systemconfigs')">系统配置</el-button>
</div>
</div>
<div v-if="activeTab === 'cases'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showCaseDialog()">新增案例</el-button>
</div>
<div v-if="casesLoading" class="card-loading">加载中...</div>
<template v-else-if="cases.length === 0">
<div class="empty-state">
<div class="empty-icon">📋</div>
<div class="empty-text">暂无案例数据</div>
<el-button type="primary" size="small" @click="showCaseDialog()">新增第一个案例</el-button>
</div>
<el-table :data="cases" border stripe class="data-table">
<el-table-column prop="id" label="ID" width="70"/>
<el-table-column prop="title" label="标题" min-width="150"/>
<el-table-column prop="field" label="领域" width="100"/>
<el-table-column prop="summary" label="概述" min-width="200" :show-overflow-tooltip="true"/>
<el-table-column prop="source" label="来源" width="100"/>
</template>
<template v-else>
<div class="stat-summary">
<div class="stat-item"><span class="num">{{ cases.length }}</span><span class="label">总案例</span></div>
</div>
<el-table :data="cases" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="title" label="标题" min-width="120" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="field" label="领域" width="100"></el-table-column>
<el-table-column prop="summary" label="概述" min-width="200" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="source" label="来源" width="100"></el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="scope">
<el-button size="small" @click="showCaseDialog(scope.row)">编辑</el-button>
@@ -76,52 +102,73 @@
</template>
</el-table-column>
</el-table>
<div class="mobile-card-list">
<div v-for="item in cases" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">标题</span><span class="mobile-card-value">{{ item.title }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">领域</span><span class="mobile-card-value">{{ item.field }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">来源</span><span class="mobile-card-value">{{ item.source }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showCaseDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteCase(item.id)">删除</el-button>
</div>
</template>
<div class="mobile-card-list">
<div v-for="item in cases" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">标题</span><span class="mobile-card-value">{{ item.title }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">领域</span><span class="mobile-card-value">{{ item.field }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">来源</span><span class="mobile-card-value">{{ item.source }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showCaseDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteCase(item.id)">删除</el-button>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="任务日志" name="tasklogs">
<div class="toolbar">
<el-button @click="loadTaskLogs()">刷新</el-button>
</div>
</div>
<div v-if="activeTab === 'tasklogs'">
<div class="toolbar">
<el-button size="small" @click="loadTaskLogs()">刷新</el-button>
</div>
<div v-if="taskLogsLoading" class="card-loading">加载中...</div>
<template v-else-if="taskLogs.length === 0">
<div class="empty-state">
<div class="empty-icon">📝</div>
<div class="empty-text">暂无任务日志</div>
</div>
<el-table :data="taskLogs" border stripe class="data-table">
<el-table-column prop="id" label="ID" width="70"/>
<el-table-column prop="task_name" label="任务名称" min-width="120"/>
<el-table-column prop="topic_id" label="选题" width="100"/>
<el-table-column prop="status" label="状态" width="80"/>
<el-table-column prop="message" label="消息" min-width="150" :show-overflow-tooltip="true"/>
<el-table-column prop="started_at" label="开始" width="150"/>
<el-table-column prop="finished_at" label="结束" width="150"/>
<el-table-column prop="duration" label="耗时" width="80"/>
</template>
<template v-else>
<el-table :data="taskLogs" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="task_name" label="任务名称" min-width="120"></el-table-column>
<el-table-column prop="topic_id" label="选题" width="100"></el-table-column>
<el-table-column prop="status" label="状态" width="80"></el-table-column>
<el-table-column prop="message" label="消息" min-width="150" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="started_at" label="开始" width="150"></el-table-column>
<el-table-column prop="finished_at" label="结束" width="150"></el-table-column>
<el-table-column prop="duration" label="耗时" width="80"></el-table-column>
</el-table>
<div class="mobile-card-list">
<div v-for="item in taskLogs" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">任务</span><span class="mobile-card-value">{{ item.task_name }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">选题</span><span class="mobile-card-value">{{ item.topic_id }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">状态</span><span class="mobile-card-value">{{ item.status }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">消息</span><span class="mobile-card-value">{{ item.message }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">耗时</span><span class="mobile-card-value">{{ item.duration }}s</span></div>
</div>
</template>
<div class="mobile-card-list">
<div v-for="item in taskLogs" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">任务</span><span class="mobile-card-value">{{ item.task_name }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">选题</span><span class="mobile-card-value">{{ item.topic_id }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">状态</span><span class="mobile-card-value">{{ item.status }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">消息</span><span class="mobile-card-value">{{ item.message }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">耗时</span><span class="mobile-card-value">{{ item.duration }}s</span></div>
</div>
</el-tab-pane>
<el-tab-pane label="LLM配置" name="llmconfigs">
<div class="toolbar">
<el-button type="primary" @click="showLLMConfigDialog()">新增配置</el-button>
</div>
</div>
<div v-if="activeTab === 'llmconfigs'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showLLMConfigDialog()">新增配置</el-button>
</div>
<div v-if="llmConfigsLoading" class="card-loading">加载中...</div>
<template v-else-if="llmConfigs.length === 0">
<div class="empty-state">
<div class="empty-icon">🤖</div>
<div class="empty-text">暂无 LLM 配置</div>
<el-button type="primary" size="small" @click="showLLMConfigDialog()">新增配置</el-button>
</div>
<el-table :data="llmConfigs" border stripe class="data-table">
<el-table-column prop="id" label="ID" width="70"/>
<el-table-column prop="name" label="名称" min-width="120"/>
<el-table-column prop="model" label="模型" min-width="180"/>
<el-table-column prop="temperature" label="温度" width="80"/>
<el-table-column prop="max_tokens" label="最大Token" width="110"/>
</template>
<template v-else>
<el-table :data="llmConfigs" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="name" label="名称" min-width="120"></el-table-column>
<el-table-column prop="model" label="模型" min-width="180"></el-table-column>
<el-table-column prop="temperature" label="温度" width="80"></el-table-column>
<el-table-column prop="max_tokens" label="最大Token" width="110"></el-table-column>
<el-table-column prop="is_active" label="激活" width="70">
<template #default="scope">{{ scope.row.is_active ? '是' : '否' }}</template>
</el-table-column>
@@ -132,31 +179,42 @@
</template>
</el-table-column>
</el-table>
<div class="mobile-card-list">
<div v-for="item in llmConfigs" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">名称</span><span class="mobile-card-value">{{ item.name }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">模型</span><span class="mobile-card-value">{{ item.model }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">温度</span><span class="mobile-card-value">{{ item.temperature }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">激活</span><span class="mobile-card-value">{{ item.is_active ? '是' : '否' }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showLLMConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteLLMConfig(item.id)">删除</el-button>
</div>
</template>
<div class="mobile-card-list">
<div v-for="item in llmConfigs" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">名称</span><span class="mobile-card-value">{{ item.name }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">模型</span><span class="mobile-card-value">{{ item.model }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">温度</span><span class="mobile-card-value">{{ item.temperature }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">激活</span><span class="mobile-card-value">{{ item.is_active ? '是' : '否' }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showLLMConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteLLMConfig(item.id)">删除</el-button>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="系统配置" name="systemconfigs">
<div class="toolbar">
<el-button type="primary" @click="showSystemConfigDialog()">新增配置</el-button>
</div>
</div>
<div v-if="activeTab === 'systemconfigs'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showSystemConfigDialog()">新增配置</el-button>
</div>
<div v-if="systemConfigsLoading" class="card-loading">加载中...</div>
<template v-else-if="systemConfigs.length === 0">
<div class="empty-state">
<div class="empty-icon">⚙️</div>
<div class="empty-text">暂无系统配置</div>
<el-button type="primary" size="small" @click="showSystemConfigDialog()">新增配置</el-button>
</div>
<el-table :data="systemConfigs" border stripe class="data-table">
<el-table-column prop="key" label="键" min-width="180"/>
</template>
<template v-else>
<el-table :data="systemConfigs" border stripe class="data-table" style="width:100%">
<el-table-column prop="key" label="键" min-width="180"></el-table-column>
<el-table-column prop="value" label="值" min-width="250">
<template #default="scope">
{{ typeof scope.row.value === 'object' ? JSON.stringify(scope.row.value) : scope.row.value }}
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="200"/>
<el-table-column prop="description" label="描述" min-width="200"></el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="scope">
<el-button size="small" @click="showSystemConfigDialog(scope.row)">编辑</el-button>
@@ -164,34 +222,44 @@
</template>
</el-table-column>
</el-table>
<div class="mobile-card-list">
<div v-for="item in systemConfigs" :key="item.key" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label"></span><span class="mobile-card-value">{{ item.key }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label"></span><span class="mobile-card-value">{{ typeof item.value === 'object' ? JSON.stringify(item.value) : item.value }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">描述</span><span class="mobile-card-value">{{ item.description }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showSystemConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteSystemConfig(item.key)">删除</el-button>
</div>
</template>
<div class="mobile-card-list">
<div v-for="item in systemConfigs" :key="item.key" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label"></span><span class="mobile-card-value">{{ item.key }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label"></span><span class="mobile-card-value">{{ typeof item.value === 'object' ? JSON.stringify(item.value) : item.value }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">描述</span><span class="mobile-card-value">{{ item.description }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showSystemConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteSystemConfig(item.key)">删除</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</main>
</div>
<el-dialog v-model="caseDialogVisible" :title="caseDialogTitle" width="600px">
<el-dialog v-model="caseDialogVisible" :title="caseDialogTitle" width="700px" :close-on-click-modal="false">
<el-form :model="caseForm" label-width="80px">
<el-form-item label="标题"><el-input v-model="caseForm.title"/></el-form-item>
<el-form-item label="领域"><el-input v-model="caseForm.field"/></el-form-item>
<el-form-item label="概述"><el-input type="textarea" v-model="caseForm.summary"/></el-form-item>
<el-form-item label="关键指标"><el-input v-model="caseForm.key_metrics"/></el-form-item>
<el-form-item label="日期"><el-input v-model="caseForm.date"/></el-form-item>
<el-form-item label="来源"><el-input v-model="caseForm.source"/></el-form-item>
<el-form-item label="来源URL"><el-input v-model="caseForm.source_url"/></el-form-item>
<el-form-item label="可信度"><el-input v-model="caseForm.credibility_rating"/></el-form-item>
<el-form-item label="国内适用性"><el-input v-model="caseForm.china_applicability"/></el-form-item>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="标题"><el-input v-model="caseForm.title"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="领域"><el-input v-model="caseForm.field"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="概述"><el-input type="textarea" v-model="caseForm.summary" :rows="3"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="关键指标"><el-input v-model="caseForm.key_metrics"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="日期"><el-input v-model="caseForm.date"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="来源"><el-input v-model="caseForm.source"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="来源URL"><el-input v-model="caseForm.source_url"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="可信度"><el-input v-model="caseForm.credibility_rating"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="国内适用性"><el-input v-model="caseForm.china_applicability"/></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="caseDialogVisible=false">取消</el-button>
@@ -199,17 +267,25 @@
</template>
</el-dialog>
<el-dialog v-model="llmConfigDialogVisible" :title="llmConfigDialogTitle" width="600px">
<el-form :model="llmConfigForm" label-width="120px">
<el-form-item label="名称"><el-input v-model="llmConfigForm.name"/></el-form-item>
<el-form-item label="系统提示词"><el-input type="textarea" v-model="llmConfigForm.system_prompt"/></el-form-item>
<el-form-item label="用户提示模板"><el-input type="textarea" v-model="llmConfigForm.user_prompt_template"/></el-form-item>
<el-form-item label="温度"><el-input-number v-model="llmConfigForm.temperature" :min="0" :max="2" :step="0.1"/></el-form-item>
<el-form-item label="最大Token"><el-input-number v-model="llmConfigForm.max_tokens" :min="1" :max="10000"/></el-form-item>
<el-form-item label="模型"><el-input v-model="llmConfigForm.model"/></el-form-item>
<el-form-item label="激活">
<el-switch v-model="llmConfigForm.is_active"/>
</el-form-item>
<el-dialog v-model="llmConfigDialogVisible" :title="llmConfigDialogTitle" width="700px" :close-on-click-modal="false">
<el-form :model="llmConfigForm" label-width="100px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="名称"><el-input v-model="llmConfigForm.name"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="模型"><el-input v-model="llmConfigForm.model"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="温度"><el-input-number v-model="llmConfigForm.temperature" :min="0" :max="2" :step="0.1"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="最大Token"><el-input-number v-model="llmConfigForm.max_tokens" :min="1" :max="10000"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="系统提示词"><el-input type="textarea" v-model="llmConfigForm.system_prompt" :rows="4"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="提示模板"><el-input type="textarea" v-model="llmConfigForm.user_prompt_template" :rows="4"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="激活"><el-switch v-model="llmConfigForm.is_active"/></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="llmConfigDialogVisible=false">取消</el-button>
@@ -217,11 +293,15 @@
</template>
</el-dialog>
<el-dialog v-model="systemConfigDialogVisible" :title="systemConfigDialogTitle" width="500px">
<el-form :model="systemConfigForm" label-width="100px">
<el-form-item label="键"><el-input v-model="systemConfigForm.key" :disabled="!!editingSystemConfigKey"/></el-form-item>
<el-form-item label=""><el-input v-model="systemConfigForm.value" type="textarea"/></el-form-item>
<el-form-item label="描述"><el-input v-model="systemConfigForm.description"/></el-form-item>
<el-dialog v-model="systemConfigDialogVisible" :title="systemConfigDialogTitle" width="600px" :close-on-click-modal="false">
<el-form :model="systemConfigForm" label-width="80px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label=""><el-input v-model="systemConfigForm.key" :disabled="!!editingSystemConfigKey"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="描述"><el-input v-model="systemConfigForm.description"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="值"><el-input v-model="systemConfigForm.value" type="textarea" :rows="4"/></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="systemConfigDialogVisible=false">取消</el-button>
@@ -232,7 +312,7 @@
<script src="vue.global.prod.js"></script>
<script src="element-plus.full.js"></script>
<script>
const { createApp, ref, reactive, onMounted, watch } = Vue;
const { createApp, ref, reactive, onMounted } = Vue;
const { ElMessage, ElMessageBox } = ElementPlus;
const app = createApp({
@@ -240,38 +320,42 @@
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = 'login.html'; return {}; }
const apiBase = '';
const api = {
get: (url) => fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
post: (url, body) => fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
put: (url, body) => fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
delete: (url) => fetch(url, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
get: (url) => fetch(apiBase + url, { headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
post: (url, body) => fetch(apiBase + url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
put: (url, body) => fetch(apiBase + url, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
delete: (url) => fetch(apiBase + url, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
};
const activeTab = ref('cases');
const currentUser = ref({ username: '' });
const isAdmin = ref(false);
const isLoggedIn = ref(false);
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(response => response.ok ? response.json() : Promise.reject())
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => {
const user = data.user || { username: '', role: 'user' };
currentUser.value = user;
isAdmin.value = user.role === 'admin';
isLoggedIn.value = true;
if (!isAdmin.value) { ElMessage.warning('需要管理员权限'); window.location.href = '/login.html'; }
})
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/login.html'; });
const redirectToPage = (page) => { window.location.href = '/' + page; };
const cases = ref([]);
const casesLoading = ref(false);
const caseDialogVisible = ref(false);
const caseDialogTitle = ref('新增案例');
const caseForm = reactive({ id: null, title: '', field: '', summary: '', key_metrics: '', date: '', source: '', source_url: '', credibility_rating: '', china_applicability: '' });
const editingCaseId = ref(null);
const loadCases = async () => { try { cases.value = await api.get('/api/admin/cases'); } catch (e) { ElMessage.error('加载案例失败: ' + e.message); } };
const loadCases = async () => {
casesLoading.value = true;
try { cases.value = await api.get('/api/admin/cases'); } catch (e) { ElMessage.error('加载案例失败: ' + e.message); }
finally { casesLoading.value = false; }
};
const showCaseDialog = (row = null) => {
if (row) { caseDialogTitle.value = '编辑案例'; editingCaseId.value = row.id; Object.assign(caseForm, row); }
else { caseDialogTitle.value = '新增案例'; editingCaseId.value = null; Object.keys(caseForm).forEach(k => { if (k === 'id') caseForm.id = null; else caseForm[k] = ''; }); }
@@ -290,15 +374,25 @@
};
const taskLogs = ref([]);
const loadTaskLogs = async () => { try { taskLogs.value = await api.get('/api/admin/tasklogs'); } catch (e) { ElMessage.error('加载任务日志失败: ' + e.message); } };
const taskLogsLoading = ref(false);
const loadTaskLogs = async () => {
taskLogsLoading.value = true;
try { taskLogs.value = await api.get('/api/admin/tasklogs'); } catch (e) { ElMessage.error('加载任务日志失败: ' + e.message); }
finally { taskLogsLoading.value = false; }
};
const llmConfigs = ref([]);
const llmConfigsLoading = ref(false);
const llmConfigDialogVisible = ref(false);
const llmConfigDialogTitle = ref('新增配置');
const llmConfigForm = reactive({ id: null, name: '', system_prompt: '', user_prompt_template: '', temperature: 0.7, max_tokens: 2000, model: '', is_active: true });
const editingLLMConfigId = ref(null);
const loadLLMConfigs = async () => { try { llmConfigs.value = await api.get('/api/admin/llmconfigs'); } catch (e) { ElMessage.error('加载LLM配置失败: ' + e.message); } };
const loadLLMConfigs = async () => {
llmConfigsLoading.value = true;
try { llmConfigs.value = await api.get('/api/admin/llmconfigs'); } catch (e) { ElMessage.error('加载LLM配置失败: ' + e.message); }
finally { llmConfigsLoading.value = false; }
};
const showLLMConfigDialog = (row = null) => {
if (row) { llmConfigDialogTitle.value = '编辑配置'; editingLLMConfigId.value = row.id; Object.assign(llmConfigForm, row); }
else {
@@ -320,12 +414,17 @@
};
const systemConfigs = ref([]);
const systemConfigsLoading = ref(false);
const systemConfigDialogVisible = ref(false);
const systemConfigDialogTitle = ref('新增配置');
const systemConfigForm = reactive({ key: '', value: '', description: '' });
const editingSystemConfigKey = ref(null);
const loadSystemConfigs = async () => { try { systemConfigs.value = await api.get('/api/admin/systemconfigs'); } catch (e) { ElMessage.error('加载系统配置失败: ' + e.message); } };
const loadSystemConfigs = async () => {
systemConfigsLoading.value = true;
try { systemConfigs.value = await api.get('/api/admin/systemconfigs'); } catch (e) { ElMessage.error('加载系统配置失败: ' + e.message); }
finally { systemConfigsLoading.value = false; }
};
const showSystemConfigDialog = (row = null) => {
if (row) { systemConfigDialogTitle.value = '编辑配置'; editingSystemConfigKey.value = row.key; systemConfigForm.key = row.key; systemConfigForm.value = (typeof row.value === 'object') ? JSON.stringify(row.value) : (row.value || ''); systemConfigForm.description = row.description || ''; }
else { systemConfigDialogTitle.value = '新增配置'; editingSystemConfigKey.value = null; systemConfigForm.key = ''; systemConfigForm.value = ''; systemConfigForm.description = ''; }
@@ -345,26 +444,32 @@
const logout = () => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; };
watch(activeTab, (t) => {
if (t === 'cases' && !cases.value.length) loadCases();
if (t === 'tasklogs' && !taskLogs.value.length) loadTaskLogs();
if (t === 'llmconfigs' && !llmConfigs.value.length) loadLLMConfigs();
if (t === 'systemconfigs' && !systemConfigs.value.length) loadSystemConfigs();
});
const tabLoaders = {
cases: loadCases, tasklogs: loadTaskLogs,
llmconfigs: loadLLMConfigs, systemconfigs: loadSystemConfigs,
};
const loadedTabs = new Set();
const switchTab = (name) => {
activeTab.value = name;
if (!loadedTabs.has(name)) {
loadedTabs.add(name);
tabLoaders[name]();
}
};
onMounted(() => {
if (activeTab.value === 'cases') loadCases();
else if (activeTab.value === 'tasklogs') loadTaskLogs();
else if (activeTab.value === 'llmconfigs') loadLLMConfigs();
else if (activeTab.value === 'systemconfigs') loadSystemConfigs();
loadedTabs.add('cases');
loadCases();
});
return {
activeTab, cases, caseDialogVisible, caseForm, caseDialogTitle, showCaseDialog, saveCase, deleteCase,
taskLogs, loadTaskLogs,
llmConfigs, llmConfigDialogVisible, llmConfigForm, llmConfigDialogTitle, showLLMConfigDialog, saveLLMConfig, deleteLLMConfig,
systemConfigs, systemConfigDialogVisible, systemConfigForm, systemConfigDialogTitle, showSystemConfigDialog, saveSystemConfig, deleteSystemConfig,
logout, currentUser, isAdmin, isLoggedIn, redirectToPage
activeTab, switchTab,
cases, casesLoading, caseDialogVisible, caseForm, caseDialogTitle, showCaseDialog, saveCase, deleteCase,
taskLogs, taskLogsLoading, loadTaskLogs,
llmConfigs, llmConfigsLoading, llmConfigDialogVisible, llmConfigForm, llmConfigDialogTitle, showLLMConfigDialog, saveLLMConfig, deleteLLMConfig,
systemConfigs, systemConfigsLoading, systemConfigDialogVisible, systemConfigForm, systemConfigDialogTitle, showSystemConfigDialog, saveSystemConfig, deleteSystemConfig,
logout, currentUser, isAdmin, redirectToPage
};
}
});
+3 -12
View File
@@ -5,23 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 素材库</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
.navbar-title { font-size: 20px; font-weight: 600; }
.navbar-user { display: flex; align-items: center; gap: 16px; }
.user-info { display: flex; align-items: center; gap: 8px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; }
.sidebar { width: 180px; background: white; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
}
.asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }
@@ -46,7 +37,7 @@
<div class="main-content">
<main class="content-area">
<h2 style="font-size: 24px; font-weight: 700; margin-bottom: 24px; color: #303133;">🖼️ 素材库</h2>
<div class="card">
<div class="card page-fade">
<div class="filter-bar">
<el-input v-model="searchKeyword" placeholder="搜索素材..." style="width: 200px;" clearable @clear="loadAssets" @keyup.enter="loadAssets">
<template #prefix><span>🔍</span></template>
+98 -15
View File
@@ -5,29 +5,29 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 内容日历</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
.navbar-title { font-size: 20px; font-weight: 600; }
.navbar-user { display: flex; align-items: center; gap: 16px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; }
.sidebar { width: 180px; background: white; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.content-area { flex: 1; padding: 32px; overflow-y: auto; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 12px; padding-bottom: 80px; }
.calendar-day { min-height: 56px !important; padding: 2px; }
.calendar-weekday { padding: 6px; font-size: 12px; }
.day-entry { font-size: 10px; padding: 2px 4px; white-space: normal; }
.calendar-grid { gap: 2px; }
.day-number { font-size: 12px; margin-bottom: 0; }
.day-lunar { font-size: 9px; margin-top: -1px; }
.day-term { font-size: 8px; padding: 0 3px; }
.day-holiday { font-size: 8px; padding: 0 3px; }
}
.calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.calendar-title { font-size: 28px; font-weight: 700; color: #303133; }
.calendar-nav { display: flex; align-items: center; gap: 16px; }
.calendar-nav { display: flex; align-items: center; gap: 16px; white-space: nowrap; }
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; }
.calendar-weekday { text-align: center; font-weight: 600; color: #606266; padding: 12px; background: #f5f7fa; border-radius: 8px; }
.calendar-day { min-height: 100px; background: #fafafa; border-radius: 8px; padding: 8px; border: 1px solid #ebeef5; cursor: pointer; transition: all 0.2s; }
@@ -35,6 +35,9 @@
.calendar-day.other-month { opacity: 0.4; }
.calendar-day.today { border-color: #409EFF; background: #ecf5ff; }
.day-number { font-weight: 600; font-size: 14px; margin-bottom: 8px; color: #303133; }
.day-lunar { font-size: 10px; color: #909399; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.day-term { display: inline-block; font-size: 9px; color: #E6A23C; background: #fdf6ec; border-radius: 3px; padding: 0 4px; margin-top: 1px; font-weight: 500; white-space: nowrap; }
.day-holiday { display: inline-block; font-size: 9px; color: #F56C6C; background: #fef0f0; border-radius: 3px; padding: 0 4px; margin-top: 1px; font-weight: 500; white-space: nowrap; }
.day-entries { display: flex; flex-direction: column; gap: 4px; }
.day-entry { font-size: 11px; padding: 4px 6px; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
.day-entry.planned { background: #fdf6ec; color: #E6A23C; }
@@ -68,7 +71,7 @@
></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<div class="card page-fade">
<div class="calendar-header">
<h2 class="calendar-title">📅 内容日历</h2>
<div class="calendar-nav">
@@ -93,6 +96,9 @@
:class="{ 'other-month': !day.isCurrentMonth, 'today': day.isToday }"
@click="openDayDialog(day)">
<div class="day-number">{{ day.day }}</div>
<div v-if="day.lunar" class="day-lunar">{{ day.lunar }}</div>
<div v-if="day.solarTerm" class="day-term">{{ day.solarTerm }}</div>
<div v-if="day.holiday" class="day-holiday">{{ day.holiday }}</div>
<div class="day-entries">
<div v-for="entry in day.entries" :key="entry.id"
class="day-entry"
@@ -167,6 +173,80 @@
</el-dialog>
</div>
<script>
// ---- 农历/节气/节日数据 ----
const LUNAR_YEARS = [
[2024, 2,10, 0, 30,29,30,29,30,29,30,29,29,30,30,30],
[2025, 1,29, 6, 30,29,30,29,30,29,30,29,30,29,30,29,30],
[2026, 2,17, 0, 29,30,29,30,29,30,29,30,29,30,29,30],
[2027, 2, 6, 0, 30,29,30,29,30,29,30,29,30,29,30,29],
[2028, 1,26, 5, 30,29,30,29,30,29,30,30,29,30,29,30,29],
];
const LUNAR_MONTH_NAMES = ['', '正月','二月','三月','四月','五月','六月','七月','八月','九月','十月','冬月','腊月'];
const LUNAR_DAY_NAMES = ['', '初一','初二','初三','初四','初五','初六','初七','初八','初九','初十','十一','十二','十三','十四','十五','十六','十七','十八','十九','二十','廿一','廿二','廿三','廿四','廿五','廿六','廿七','廿八','廿九','三十'];
// 公历节日 (月-日)
const SOLAR_HOLIDAYS = {
'1-1': '元旦', '2-14': '情人节', '3-8': '妇女节', '3-12': '植树节',
'4-1': '愚人节', '5-1': '劳动节', '5-4': '青年节',
'6-1': '儿童节', '7-1': '建党节', '8-1': '建军节',
'9-10': '教师节', '10-1': '国庆节', '12-25': '圣诞节',
};
// 节气近似日期 (月-日)
const SOLAR_TERMS = {
'1-5':'小寒','1-20':'大寒','2-4':'立春','2-19':'雨水',
'3-6':'惊蛰','3-21':'春分','4-5':'清明','4-20':'谷雨',
'5-6':'立夏','5-21':'小满','6-6':'芒种','6-21':'夏至',
'7-7':'小暑','7-23':'大暑','8-7':'立秋','8-23':'处暑',
'9-7':'白露','9-23':'秋分','10-8':'寒露','10-23':'霜降',
'11-7':'立冬','11-22':'小雪','12-7':'大雪','12-22':'冬至',
};
function gregorianToLunar(year, month, day) {
const target = new Date(year, month - 1, day).getTime();
for (const ly of LUNAR_YEARS) {
const [lyear, sm, sd, leap, ...monthDays] = ly;
const start = new Date(lyear, sm - 1, sd);
const totalDays = monthDays.reduce((a, b) => a + b, 0);
const end = new Date(start);
end.setDate(end.getDate() + totalDays);
if (target < start.getTime() || target >= end.getTime()) continue;
const diff = Math.floor((target - start.getTime()) / 86400000);
let accum = 0;
for (let i = 0; i < monthDays.length; i++) {
if (diff < accum + monthDays[i]) {
let lunarMonth, isLeap = false;
if (leap > 0 && i === leap) {
isLeap = true;
lunarMonth = leap;
} else if (leap > 0 && i > leap) {
lunarMonth = i;
} else {
lunarMonth = i + 1;
}
const lunarDay = diff - accum + 1;
const mName = (isLeap ? '闰' : '') + (LUNAR_MONTH_NAMES[lunarMonth] || lunarMonth + '月');
const dName = LUNAR_DAY_NAMES[lunarDay] || lunarDay + '日';
let lunarStr = mName + dName;
if (lunarStr.length > 6) lunarStr = (isLeap ? '闰' : '') + lunarMonth + '月' + dName;
return lunarStr;
}
accum += monthDays[i];
}
}
return '';
}
function getDayMeta(year, month, day) {
const key = month + '-' + day;
const solarTerm = SOLAR_TERMS[key] || '';
const holiday = SOLAR_HOLIDAYS[key] || '';
const lunar = gregorianToLunar(year, month, day);
return { solarTerm, holiday, lunar };
}
</script>
<script src="vue.global.prod.js"></script>
<script src="element-plus.full.js"></script>
<script>
@@ -212,7 +292,8 @@
for (let i = startWeek - 1; i >= 0; i--) {
const d = new Date(currentYear.value, currentMonth.value - 1, -i);
days.push({ day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear(), isCurrentMonth: false, isToday: false, entries: [] });
const meta = getDayMeta(d.getFullYear(), d.getMonth() + 1, d.getDate());
days.push({ day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear(), isCurrentMonth: false, isToday: false, entries: [], ...meta });
}
for (let i = 1; i <= totalDays; i++) {
const isToday = today.getFullYear() === currentYear.value && today.getMonth() + 1 === currentMonth.value && today.getDate() === i;
@@ -220,12 +301,14 @@
const pd = new Date(e.planned_date);
return pd.getFullYear() === currentYear.value && pd.getMonth() + 1 === currentMonth.value && pd.getDate() === i;
});
days.push({ day: i, month: currentMonth.value, year: currentYear.value, isCurrentMonth: true, isToday, entries: dayEntries });
const meta = getDayMeta(currentYear.value, currentMonth.value, i);
days.push({ day: i, month: currentMonth.value, year: currentYear.value, isCurrentMonth: true, isToday, entries: dayEntries, ...meta });
}
const remaining = 42 - days.length;
for (let i = 1; i <= remaining; i++) {
const d = new Date(currentYear.value, currentMonth.value, i);
days.push({ day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear(), isCurrentMonth: false, isToday: false, entries: [] });
const meta = getDayMeta(d.getFullYear(), d.getMonth() + 1, d.getDate());
days.push({ day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear(), isCurrentMonth: false, isToday: false, entries: [], ...meta });
}
return days;
});
-7
View File
@@ -1,7 +0,0 @@
(function() {
var d = document.createElement('div');
d.style.cssText = 'position:fixed;top:0;left:0;background:rgba(0,0,0,0.9);color:#fff;padding:8px;font-size:12px;z-index:999999;max-width:90vw;overflow:auto;';
d.innerHTML = 'Vue: ' + typeof Vue + '<br>ElementPlus: ' + typeof ElementPlus + '<br>Time: ' + new Date().toLocaleTimeString();
document.body.appendChild(d);
console.log('Debug panel injected', d.innerHTML);
})();
-194
View File
@@ -1,194 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - Vue调试</title>
<!-- 资源加载 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.3/dist/index.css">
<script src="https://unpkg.com/element-plus@2.4.3/dist/index.full.min.js"></script>
<style>
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; margin-bottom: 24px; }
aside button { width: 100%; text-align: left; border: none; background: transparent; border-radius: 8px; margin-bottom: 4px; }
@media (max-width: 768px) { main { padding-bottom: 70px; } }
.nav-title { text-align: center; }
.status-badge { display: inline-flex; align-items: center; gap: 4px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; }
.status-dot.pending { background: #E6A23C; }
.status-dot.review { background: #F56C6C; }
.status-dot.ready { background: #67C23A; }
.status-dot.published { background: #409EFF; }
.debug-panel { background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto; }
</style>
</head>
<body>
<div id="app">
<!-- 导航栏 -->
<nav class="bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg">
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
<h1 class="text-2xl font-bold nav-title">宇之然内容创作平台 - Vue调试</h1>
</div>
</nav>
<!-- 侧边栏 -->
<div class="page flex gap-6">
<aside class="w-40 flex-shrink-0 hidden md:block">
<button @click="testType='topics'" :class="['px-4 py-2 rounded-lg', testType === 'topics' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📋 选题管理</button>
<button @click="testType='logs'" :class="['px-4 py-2 rounded-lg', testType === 'logs' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📄 系统日志</button>
<button @click="testType='users'" :class="['px-4 py-2 rounded-lg', testType === 'users' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">👥 用户管理</button>
</aside>
<!-- 主内容区 -->
<main class="flex-1">
<div class="card" style="padding: 20px; margin-top: 20px;">
<h2 class="text-2xl font-bold text-gray-800 mb-6">🔍 Vue调试控制台</h2>
<!-- 调试信息显示 -->
<div class="debug-panel mb-4">
<strong>调试输出:</strong><br/>
<span v-for="log in debugLogs" :key="log">{{ log }}</span>
</div>
<!-- 测试按钮 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<button @click="runDebugTest" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">运行调试测试</button>
<button @click="testElementPlus" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">测试Element Plus</button>
<button @click="resetDebug" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">重置调试</button>
</div>
<!-- 测试结果 -->
<div v-if="testResults.length > 0" class="mb-4 p-4 bg-green-50 border-l-4 border-green-400">
<h3 class="font-bold mb-2">测试结果:</h3>
<ul class="list-disc pl-5">
<li v-for="result in testResults">{{ result }}</li>
</ul>
</div>
<!-- 模拟表格 -->
<div v-if="testType === 'topics'" class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-gray-50">
<th class="border p-2"><input type="checkbox"></th>
<th class="border p-2">ID</th>
<th class="border p-2">标题</th>
<th class="border p-2">状态</th>
</tr>
</thead>
<tbody>
<tr v-for="topic in mockTopics" :key="topic.id">
<td class="border p-2"><input type="checkbox"></td>
<td class="border p-2">{{ topic.id }}</td>
<td class="border p-2">{{ topic.title }}</td>
<td class="border p-2">
<span class="status-badge">
<span class="status-dot" :class="getStatusClass(topic.status)"></span>
{{ getStatusText(topic.status) }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<script>
const DebugApp = {
data() {
return {
testType: 'topics',
debugLogs: [
'Vue调试应用已启动',
'请运行调试测试查看详细信息',
''
],
testResults: [],
mockTopics: [
{ id: 'A01', title: '可持续发展趋势分析', status: 'pending' },
{ id: 'B02', title: 'AI在内容创作中的应用', status: 'review' },
{ id: 'C03', title: '数字化转型案例研究', status: 'ready' }
]
}
},
methods: {
addLog(message) {
this.debugLogs.push('[' + new Date().toLocaleTimeString() + '] ' + message);
},
runDebugTest() {
this.addLog('开始运行调试测试...');
// 测试数据绑定
setTimeout(() => {
this.addLog('✅ 数据绑定测试通过');
}, 100);
// 测试方法调用
setTimeout(() => {
this.addLog('✅ 方法调用测试通过');
this.testResults.push('Vue数据绑定正常');
}, 200);
// 测试DOM操作
setTimeout(() => {
this.addLog('✅ DOM操作测试通过');
this.testResults.push('VueDOM渲染正常');
}, 300);
},
testElementPlus() {
this.addLog('正在测试Element Plus集成...');
// 模拟Element Plus功能测试
setTimeout(() => {
this.addLog('✅ Element Plus样式加载成功');
this.addLog('✅ Element Plus组件可用');
this.testResults.push('Element Plus集成正常');
}, 200);
},
resetDebug() {
this.debugLogs = ['Vue调试应用已启动', '请运行调试测试查看详细信息', ''];
this.testResults = [];
this.addLog('调试信息已重置');
},
getStatusClass(status) {
const classes = {
'pending': 'status-dot pending',
'review': 'status-dot review',
'ready': 'status-dot ready',
'published': 'status-dot published'
};
return classes[status] || '';
},
getStatusText(status) {
const texts = {
'pending': '待处理',
'review': '待审查',
'ready': '待发布',
'published': '已发布'
};
return texts[status] || status;
}
},
mounted() {
this.addLog('Vue应用程序挂载完成');
this.addLog('应用状态:', this.$data);
console.log('Vue调试应用已启动');
}
}
Vue.createApp(DebugApp).mount('#app')
</script>
</body>
</html>
-269
View File
@@ -1,269 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 诊断测试</title>
<!-- 测试资源加载 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.3/dist/index.css">
<script src="https://unpkg.com/element-plus@2.4.3/dist/index.full.min.js"></script>
<style>
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; margin-bottom: 24px; }
aside button { width: 100%; text-align: left; border: none; background: transparent; border-radius: 8px; margin-bottom: 4px; }
@media (max-width: 768px) { main { padding-bottom: 70px; } }
.nav-title { text-align: center; }
.status-badge { display: inline-flex; align-items: center; gap: 4px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; }
.status-dot.pending { background: #E6A23C; }
.status-dot.review { background: #F56C6C; }
.status-dot.ready { background: #67C23A; }
.status-dot.published { background: #409EFF; }
</style>
</head>
<body>
<div id="app">
<!-- 导航栏 -->
<nav class="bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg">
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
<h1 class="text-2xl font-bold nav-title">宇之然内容创作平台 - 诊断测试</h1>
</div>
</nav>
<!-- 侧边栏 -->
<div class="page flex gap-6">
<aside class="w-40 flex-shrink-0 hidden md:block">
<button @click="showTest('topics')" :class="['px-4 py-2 rounded-lg', testType === 'topics' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📋 选题管理测试</button>
<button @click="showTest('logs')" :class="['px-4 py-2 rounded-lg', testType === 'logs' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📄 系统日志测试</button>
<button @click="showTest('users')" :class="['px-4 py-2 rounded-lg', testType === 'users' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">👥 用户管理测试</button>
</aside>
<!-- 主内容区 -->
<main class="flex-1">
<div class="card" style="padding: 20px; margin-top: 20px;">
<h2 class="text-2xl font-bold text-gray-800 mb-6">🔍 功能诊断测试</h2>
<!-- 测试结果显示 -->
<div v-if="testResults.length > 0" class="mb-4 p-4 bg-green-50 border-l-4 border-green-400">
<h3 class="font-bold mb-2">✅ 测试结果:</h3>
<ul class="list-disc pl-5">
<li v-for="result in testResults">{{ result }}</li>
</ul>
</div>
<!-- 选题管理测试 -->
<div v-if="testType === 'topics'">
<h3 class="text-xl font-bold mb-4">📋 选题管理功能测试</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div class="p-4 bg-blue-50 rounded">
<h4 class="font-bold mb-2">批量操作测试</h4>
<button @click="testBatchOperations" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">测试批量刷新</button>
<span v-if="batchTested" class="ml-2 text-green-600">✅ 通过</span>
</div>
<div class="p-4 bg-green-50 rounded">
<h4 class="font-bold mb-2">数据加载测试</h4>
<button @click="testDataLoading" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">测试数据加载</button>
<span v-if="dataLoaded" class="ml-2 text-green-600">✅ 通过</span>
</div>
</div>
<!-- 模拟表格 -->
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-gray-50">
<th class="border p-2"><input type="checkbox"></th>
<th class="border p-2">ID</th>
<th class="border p-2">标题</th>
<th class="border p-2">状态</th>
<th class="border p-2">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="topic in mockTopics" :key="topic.id">
<td class="border p-2"><input type="checkbox"></td>
<td class="border p-2">{{ topic.id }}</td>
<td class="border p-2">{{ topic.title }}</td>
<td class="border p-2">
<span class="status-badge">
<span class="status-dot" :class="getStatusClass(topic.status)"></span>
{{ getStatusText(topic.status) }}
</span>
</td>
<td class="border p-2">
<button class="px-2 py-1 bg-blue-500 text-white rounded mr-1 text-xs">预览</button>
<button class="px-2 py-1 bg-green-500 text-white rounded mr-1 text-xs">创作</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 系统日志测试 -->
<div v-if="testType === 'logs'" class="p-6 bg-yellow-50 rounded">
<h3 class="text-xl font-bold mb-4">📄 系统日志功能测试</h3>
<div class="flex flex-wrap gap-4 mb-4">
<select v-model="logType" class="px-3 py-2 border rounded">
<option value="creator">创作日志</option>
<option value="optimizer">优化日志</option>
<option value="collector">收集日志</option>
</select>
<input v-model="logDate" type="date" class="px-3 py-2 border rounded">
<button @click="fetchMockLogs" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">加载日志</button>
</div>
<pre class="bg-white p-4 rounded border min-h-[200px] whitespace-pre-wrap font-mono text-sm">{{ logContent }}</pre>
</div>
<!-- 用户管理测试 -->
<div v-if="testType === 'users'" class="p-6 bg-purple-50 rounded">
<h3 class="text-xl font-bold mb-4">👥 用户管理功能测试</h3>
<div class="flex justify-between items-center mb-4">
<h4 class="font-bold">用户列表</h4>
<button @click="addMockUser" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">+ 新建用户</button>
</div>
<table class="w-full border-collapse">
<thead>
<tr class="bg-gray-50">
<th class="border p-2">ID</th>
<th class="border p-2">用户名</th>
<th class="border p-2">角色</th>
<th class="border p-2">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td class="border p-2">{{ user.id }}</td>
<td class="border p-2">{{ user.username }}</td>
<td class="border p-2">
<span :class="[user.role === 'admin' ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800', 'px-2 py-1 rounded']">
{{ user.role === 'admin' ? '管理员' : '编辑' }}
</span>
</td>
<td class="border p-2">
<button @click="deleteUser(user.id)" class="px-2 py-1 bg-red-500 text-white rounded text-xs" :disabled="user.role === 'admin'">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<script>
const DiagnosticApp = {
data() {
return {
testType: 'topics',
testResults: [],
batchTested: false,
dataLoaded: false,
mockTopics: [
{ id: 'A01', title: '可持续发展趋势分析', status: 'pending' },
{ id: 'B02', title: 'AI在内容创作中的应用', status: 'review' },
{ id: 'C03', title: '数字化转型案例研究', status: 'ready' }
],
logType: 'creator',
logDate: '',
logContent: '请选择日志类型和日期,然后点击加载',
users: [
{ id: 'admin', username: '管理员', role: 'admin' },
{ id: 'editor1', username: '编辑小王', role: 'editor' }
]
}
},
methods: {
showTest(type) {
this.testType = type;
this.testResults = [];
},
// 测试方法
testBatchOperations() {
this.testResults.push('✅ 批量操作按钮点击正常');
this.batchTested = true;
console.log('批量操作测试通过');
},
testDataLoading() {
setTimeout(() => {
this.testResults.push('✅ 数据加载正常 (3个选题)');
this.dataLoaded = true;
console.log('数据加载测试通过');
}, 500);
},
fetchMockLogs() {
const logs = {
creator: `2026-04-27 11:45:23 | 成功生成选题 B02 - AI在内容创作中的应用
2026-04-27 11:30:15 | 开始生成选题 C03 - 数字化转型案例研究`,
optimizer: `2026-04-27 12:05:30 | 优化完成选题 C03 - 合规分提升至78
2026-04-27 11:50:45 | 优化中选题 B02 - 等待人工审核`,
collector: `2026-04-27 10:30:12 | 收集到3个新选题
2026-04-27 09:45:20 | 更新行业热点数据`
}[this.logType] || '暂无日志数据';
this.logContent = `日志类型: ${this.logType}
日期: ${this.logDate || '今天'}
${logs}`;
this.testResults.push(`✅ 日志加载成功 (${this.logType})`);
},
addMockUser() {
const newId = 'user' + Date.now();
this.users.push({ id: newId, username: '新用户', role: 'editor' });
this.testResults.push('✅ 新建用户成功');
},
deleteUser(id) {
if (id !== 'admin') {
this.users = this.users.filter(u => u.id !== id);
this.testResults.push('✅ 删除用户成功');
} else {
this.testResults.push('❌ 不能删除管理员');
}
},
getStatusClass(status) {
const classes = {
'pending': 'status-dot pending',
'review': 'status-dot review',
'ready': 'status-dot ready',
'published': 'status-dot published'
};
return classes[status] || '';
},
getStatusText(status) {
const texts = {
'pending': '待处理',
'review': '待审查',
'ready': '待发布',
'published': '已发布'
};
return texts[status] || status;
}
},
mounted() {
console.log('诊断测试应用已启动');
this.testDataLoading(); // 自动测试数据加载
}
}
Vue.createApp(DiagnosticApp).mount('#app')
</script>
</body>
</html>
-410
View File
@@ -1,410 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 综合诊断</title>
<!-- 资源加载 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.3/dist/index.css">
<script src="https://unpkg.com/element-plus@2.4.3/dist/index.full.min.js"></script>
<style>
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; margin-bottom: 24px; }
aside button { width: 100%; text-align: left; border: none; background: transparent; border-radius: 8px; margin-bottom: 4px; }
@media (max-width: 768px) { main { padding-bottom: 70px; } }
.nav-title { text-align: center; }
.status-badge { display: inline-flex; align-items: center; gap: 4px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; }
.status-dot.pending { background: #E6A23C; }
.status-dot.review { background: #F56C6C; }
.status-dot.ready { background: #67C23A; }
.status-dot.published { background: #409EFF; }
.debug-panel { background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto; }
.btn-primary { padding: 8px 16px; background: #409EFF; color: white; border: none; border-radius: 4px; cursor: pointer; }
.btn-primary:hover { background: #337ecc; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.table th { background-color: #f2f2f2; }
.result-success { color: green; font-weight: bold; }
.result-error { color: red; font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<!-- 导航栏 -->
<nav class="bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg">
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
<h1 class="text-2xl font-bold nav-title">宇之然内容创作平台 - 综合诊断</h1>
</div>
</nav>
<!-- 侧边栏 -->
<div class="page flex gap-6">
<aside class="w-40 flex-shrink-0 hidden md:block">
<button @click="activeTab='diagnostics'" :class="['px-4 py-2 rounded-lg', activeTab === 'diagnostics' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">🔍 诊断测试</button>
<button @click="activeTab='results'" :class="['px-4 py-2 rounded-lg', activeTab === 'results' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📊 测试结果</button>
<button @click="activeTab='solutions'" :class="['px-4 py-2 rounded-lg', activeTab === 'solutions' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">🔧 解决方案</button>
</aside>
<!-- 主内容区 -->
<main class="flex-1">
<div class="card" style="padding: 20px; margin-top: 20px;">
<h2 class="text-2xl font-bold text-gray-800 mb-6">🎯 Vue应用综合诊断与修复</h2>
<!-- 诊断面板 -->
<div v-if="activeTab === 'diagnostics'" class="mb-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<button @click="runComprehensiveTest" class="btn-primary">🔄 运行全面诊断</button>
<button @click="testElementPlusIntegration" class="btn-primary">🧪 测试Element Plus集成</button>
<button @click="testVueCore" class="btn-primary">⚡ 测试Vue核心功能</button>
<button @click="resetAll" class="btn-primary">🔄 重置所有测试</button>
</div>
<!-- 实时调试输出 -->
<div class="debug-panel mb-4">
<strong>诊断日志:</strong><br/>
<span v-for="log in diagnosticLogs" :key="log">{{ log }}</span>
</div>
<!-- 当前状态显示 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="p-4 bg-blue-50 rounded">
<h4 class="font-bold">Vue状态</h4>
<p>初始化: <span :class="vueInitialized ? 'result-success' : 'result-error'">{{ vueInitialized ? '✅' : '❌' }}</span></p>
<p>数据绑定: <span :class="dataBindingWorking ? 'result-success' : 'result-error'">{{ dataBindingWorking ? '✅' : '❌' }}</span></p>
</div>
<div class="p-4 bg-green-50 rounded">
<h4 class="font-bold">Element Plus</h4>
<p>样式加载: <span :class="elementPlusStylesLoaded ? 'result-success' : 'result-error'">{{ elementPlusStylesLoaded ? '✅' : '❌' }}</span></p>
<p>组件可用: <span :class="elementPlusComponentsAvailable ? 'result-success' : 'result-error'">{{ elementPlusComponentsAvailable ? '✅' : '❌' }}</span></p>
</div>
<div class="p-4 bg-yellow-50 rounded">
<h4 class="font-bold">功能状态</h4>
<p>表格渲染: <span :class="tableRenderingWorking ? 'result-success' : 'result-error'">{{ tableRenderingWorking ? '✅' : '❌' }}</span></p>
<p>事件处理: <span :class="eventHandlingWorking ? 'result-success' : 'result-error'">{{ eventHandlingWorking ? '✅' : '❌' }}</span></p>
</div>
</div>
</div>
<!-- 结果面板 -->
<div v-if="activeTab === 'results'" class="space-y-4">
<h3 class="text-xl font-bold">📊 详细测试结果</h3>
<div class="p-4 bg-green-50 rounded" v-for="result in testResults" :key="result.id">
<div class="flex justify-between items-start">
<div>
<h4 class="font-bold">{{ result.title }}</h4>
<p>{{ result.description }}</p>
</div>
<span :class="[result.status === 'passed' ? 'result-success' : 'result-error', 'ml-4']">
{{ result.status === 'passed' ? '✅' : '❌' }}
</span>
</div>
</div>
<div v-if="testResults.length === 0" class="p-4 bg-gray-50 rounded">
<p class="text-gray-500">还没有运行任何测试。请点击上方的"运行全面诊断"开始。</p>
</div>
</div>
<!-- 解决方案面板 -->
<div v-if="activeTab === 'solutions'" class="space-y-4">
<h3 class="text-xl font-bold">🔧 问题解决方案</h3>
<div class="p-4 bg-blue-50 rounded">
<h4 class="font-bold mb-2">方案1: 检查浏览器控制台错误</h4>
<ul class="list-disc pl-5 space-y-1">
<li>打开开发者工具(F12)</li>
<li>切换到Console选项卡</li>
<li>刷新页面并记录所有JavaScript错误</li>
<li>根据错误信息进行针对性修复</li>
</ul>
</div>
<div class="p-4 bg-green-50 rounded">
<h4 class="font-bold mb-2">方案2: 简化Vue应用</h4>
<ul class="list-disc pl-5 space-y-1">
<li>移除所有Element Plus依赖</li>
<li>使用纯HTML/CSS/JS实现基本功能</li>
<li>确保Vue能正常工作</li>
<li>逐步添加复杂功能</li>
</ul>
</div>
<div class="p-4 bg-yellow-50 rounded">
<h4 class="font-bold mb-2">方案3: 本地托管资源</h4>
<ul class="list-disc pl-5 space-y-1">
<li>下载Vue和Element Plus到本地</li>
<li>更新HTML中的CDN链接为本地路径</li>
<li>确保所有资源文件正确放置</li>
<li>重新测试页面功能</li>
</ul>
</div>
<div class="p-4 bg-purple-50 rounded">
<h4 class="font-bold mb-2">方案4: 重构页面结构</h4>
<ul class="list-disc pl-5 space-y-1">
<li>拆分复杂的Vue组件</li>
<li>简化数据结构和状态管理</li>
<li>确保每个功能模块独立工作</li>
<li>分阶段测试和验证</li>
</ul>
</div>
</div>
<!-- 功能演示区域 -->
<div class="mt-8">
<h3 class="text-xl font-bold mb-4">📋 功能演示</h3>
<!-- 模拟选题管理 -->
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th style="width: 55px"><input type="checkbox" @change="toggleSelectAll"></th>
<th style="width: 70px">ID</th>
<th>标题</th>
<th style="width: 100px">领域</th>
<th style="width: 90px">状态</th>
<th style="width: 210px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="topic in topics" :key="topic.id">
<td><input type="checkbox" v-model="selectedTopicIds" :value="topic.id"></td>
<td>{{ topic.id }}</td>
<td>{{ topic.title }}</td>
<td>{{ topic.field }}</td>
<td>
<span class="status-badge">
<span class="status-dot" :class="getStatusClass(topic.status)"></span>
{{ getStatusText(topic.status) }}
</span>
</td>
<td>
<button @click="openPreview(topic)" class="px-2 py-1 bg-blue-500 text-white rounded mr-1 text-xs">预览</button>
<button @click="createTopic(topic)" class="px-2 py-1 bg-green-500 text-white rounded mr-1 text-xs">创作</button>
<button @click="deleteTopic(topic.id)" class="px-2 py-1 bg-red-500 text-white rounded text-xs">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
</div>
<script>
const DiagnosticApp = {
data() {
return {
activeTab: 'diagnostics',
diagnosticLogs: [
'综合诊断应用已启动',
'请运行测试查看详细信息',
''
],
testResults: [],
vueInitialized: false,
dataBindingWorking: false,
elementPlusStylesLoaded: false,
elementPlusComponentsAvailable: false,
tableRenderingWorking: false,
eventHandlingWorking: false,
// 选题数据
topics: [
{ id: 'A01', title: '可持续发展趋势分析', field: '环保', status: 'pending' },
{ id: 'B02', title: 'AI在内容创作中的应用', field: '科技', status: 'review' },
{ id: 'C03', title: '数字化转型案例研究', field: '商业', status: 'ready' }
],
selectedTopicIds: []
}
},
methods: {
addLog(message) {
this.diagnosticLogs.push('[' + new Date().toLocaleTimeString() + '] ' + message);
},
runComprehensiveTest() {
this.addLog('开始运行全面诊断...');
this.testResults = [];
// 测试Vue初始化
setTimeout(() => {
this.vueInitialized = true;
this.addLog('✅ Vue应用程序初始化成功');
this.testResults.push({
id: 'vue-init',
title: 'Vue初始化测试',
description: 'Vue.createApp和mount执行正常',
status: 'passed'
});
}, 100);
// 测试数据绑定
setTimeout(() => {
this.dataBindingWorking = true;
this.addLog('✅ Vue数据绑定测试通过');
this.testResults.push({
id: 'data-binding',
title: '数据绑定测试',
description: '文本插值和变量引用正常',
status: 'passed'
});
}, 200);
// 测试Element Plus样式
setTimeout(() => {
this.elementPlusStylesLoaded = true;
this.addLog('✅ Element Plus样式加载成功');
this.testResults.push({
id: 'element-styles',
title: 'Element Plus样式测试',
description: 'CSS样式文件加载正常',
status: 'passed'
});
}, 300);
// 测试Element Plus组件
setTimeout(() => {
this.elementPlusComponentsAvailable = true;
this.addLog('✅ Element Plus组件模拟可用');
this.testResults.push({
id: 'element-components',
title: 'Element Plus组件测试',
description: '组件API和功能模拟正常',
status: 'passed'
});
}, 400);
// 测试表格渲染
setTimeout(() => {
this.tableRenderingWorking = true;
this.addLog('✅ Vue表格渲染测试通过');
this.testResults.push({
id: 'table-rendering',
title: '表格渲染测试',
description: 'v-for列表渲染和动态数据绑定正常',
status: 'passed'
});
}, 500);
// 测试事件处理
setTimeout(() => {
this.eventHandlingWorking = true;
this.addLog('✅ Vue事件处理测试通过');
this.testResults.push({
id: 'event-handling',
title: '事件处理测试',
description: '@click等事件监听器正常工作',
status: 'passed'
});
}, 600);
},
testElementPlusIntegration() {
this.addLog('正在测试Element Plus集成...');
setTimeout(() => {
this.elementPlusStylesLoaded = true;
this.elementPlusComponentsAvailable = true;
this.addLog('✅ Element Plus集成测试通过');
this.testResults.push({
id: 'element-integration',
title: 'Element Plus集成测试',
description: '样式和组件功能模拟正常',
status: 'passed'
});
}, 300);
},
testVueCore() {
this.addLog('正在测试Vue核心功能...');
setTimeout(() => {
this.vueInitialized = true;
this.dataBindingWorking = true;
this.tableRenderingWorking = true;
this.eventHandlingWorking = true;
this.addLog('✅ Vue核心功能测试通过');
this.testResults.push({
id: 'vue-core',
title: 'Vue核心功能测试',
description: '数据绑定、计算属性、生命周期钩子正常',
status: 'passed'
});
}, 300);
},
resetAll() {
this.diagnosticLogs = ['综合诊断应用已启动', '请运行测试查看详细信息', ''];
this.testResults = [];
this.vueInitialized = false;
this.dataBindingWorking = false;
this.elementPlusStylesLoaded = false;
this.elementPlusComponentsAvailable = false;
this.tableRenderingWorking = false;
this.eventHandlingWorking = false;
this.selectedTopicIds = [];
this.addLog('所有测试已重置');
},
toggleSelectAll(event) {
if (event.target.checked) {
this.selectedTopicIds = this.topics.map(t => t.id);
} else {
this.selectedTopicIds = [];
}
},
openPreview(topic) {
this.addLog('打开选题预览: ' + topic.title);
},
createTopic(topic) {
this.addLog('创作选题: ' + topic.title);
},
deleteTopic(id) {
this.addLog('删除选题: ' + id);
},
getStatusClass(status) {
const classes = {
'pending': 'status-dot pending',
'review': 'status-dot review',
'ready': 'status-dot ready',
'published': 'status-dot published'
};
return classes[status] || '';
},
getStatusText(status) {
const texts = {
'pending': '待处理',
'review': '待审查',
'ready': '待发布',
'published': '已发布'
};
return texts[status] || status;
}
},
mounted() {
this.addLog('综合诊断应用程序挂载完成');
console.log('Vue综合诊断应用已启动');
}
}
Vue.createApp(DiagnosticApp).mount('#app')
</script>
</body>
</html>
-452
View File
@@ -1,452 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>最终解决方案</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.panel { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.btn { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 4px; margin: 5px; cursor: pointer; }
.btn:hover { background: #0056b3; }
.status { font-weight: bold; padding: 5px 10px; border-radius: 4px; }
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
.warning { background: #fff3cd; color: #856404; }
.info { background: #d1ecf1; color: #0c5460; }
</style>
</head>
<body>
<div class="container">
<h1 style="color: #007bff;">宇之然内容创作平台 - Vue问题诊断</h1>
<!-- 问题描述 -->
<div class="panel info">
<h3>📋 问题描述</h3>
<p><strong>症状:</strong> 选题管理、系统日志、用户管理页面点击菜单后只显示标题,没有实际内容</p>
<p><strong>可能原因:</strong> Vue应用初始化失败、Element Plus集成问题、CSS样式冲突等</p>
</div>
<!-- 诊断按钮 -->
<div class="panel">
<h3>🔍 快速诊断</h3>
<button onclick="runQuickTest()" class="btn">运行快速诊断</button>
<button onclick="checkConsole()" class="btn">检查控制台错误</button>
<button onclick="resetPage()" class="btn">重置页面</button>
<div id="testResults" style="margin-top: 15px;"></div>
</div>
<!-- 详细分析 -->
<div class="panel">
<h3>🔬 详细分析</h3>
<div id="detailedAnalysis"></div>
</div>
<!-- 解决方案 -->
<div class="panel">
<h3>💡 解决方案</h3>
<ol id="solutionsList"></ol>
</div>
<!-- 紧急修复 -->
<div class="panel warning">
<h3>🚨 紧急修复方案</h3>
<button onclick="applyEmergencyFix()" class="btn">应用紧急修复</button>
<p id="emergencyResult" style="margin-top: 10px;"></p>
</div>
</div>
<script>
let appData = {
vueReady: false,
elementPlusLoaded: false,
cssLoaded: false,
domReady: false,
errors: [],
warnings: []
};
function runQuickTest() {
document.getElementById('testResults').innerHTML = '<p>正在运行诊断测试...</p>';
// 检查Vue
setTimeout(() => {
if (window.Vue) {
appData.vueReady = true;
addResult('✅ Vue 3库已加载', 'success');
} else {
appData.errors.push('Vue 3库未加载');
addResult('❌ Vue 3库加载失败', 'error');
}
// 检查DOM
const appElement = document.getElementById('app');
if (appElement) {
appData.domReady = true;
addResult('✅ DOM元素存在', 'success');
} else {
appData.errors.push('找不到#app元素');
addResult('❌ DOM元素缺失', 'error');
}
// 检查Tailwind
const tailwindScript = document.querySelector('script[src*="tailwindcss"]');
if (tailwindScript) {
appData.cssLoaded = true;
addResult('✅ Tailwind CSS已加载', 'success');
} else {
appData.warnings.push('Tailwind CSS可能未正确加载');
addResult('⚠️ Tailwind CSS状态未知', 'warning');
}
updateDetailedAnalysis();
generateSolutions();
}, 100);
}
function checkConsole() {
console.log('=== 宇之然Vue应用诊断 ===');
console.log('Vue状态:', appData.vueReady ? 'ready' : 'not ready');
console.log('DOM状态:', appData.domReady ? 'ready' : 'not ready');
console.log('CSS状态:', appData.cssLoaded ? 'loaded' : 'not loaded');
console.log('错误列表:', appData.errors);
console.log('警告列表:', appData.warnings);
addResult('✅ 控制台检查完成,请查看浏览器开发者工具(F12)', 'info');
}
function resetPage() {
location.reload();
}
function applyEmergencyFix() {
document.getElementById('emergencyResult').innerHTML = '<p>正在应用紧急修复...</p>';
setTimeout(() => {
// 创建一个新的极简Vue应用
const emergencyHTML = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 紧急修复版</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.3/dist/index.css">
<script src="https://unpkg.com/element-plus@2.4.3/dist/index.full.min.js"></script>
<style>
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }
.nav { background: linear-gradient(to right, #2563eb, #1d4ed8); color: white; padding: 1rem 2rem; }
.sidebar { width: 160px; background: #f3f4f6; padding: 1rem; }
.content { flex: 1; padding: 1.5rem; }
.card { background: white; border-radius: 0.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 1.5rem; margin-bottom: 1.5rem; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border: 1px solid #e5e7eb; padding: 0.75rem; text-align: left; }
.table th { background: #f9fafb; }
.btn { padding: 0.5rem 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.25rem; cursor: pointer; }
.btn:hover { background: #2563eb; }
.btn:disabled { background: #9ca3af; cursor: not-allowed; }
.flex { display: flex; }
.gap-4 { gap: 1rem; }
.mb-4 { margin-bottom: 1rem; }
.hidden.md\:block { display: none; }
@media (min-width: 768px) { .hidden.md\:block { display: block; } }
</style>
</head>
<body>
<div id="app">
<!-- 导航栏 -->
<nav class="nav">
<h1 style="text-align: center; margin: 0;">宇之然内容创作平台</h1>
</nav>
<!-- 侧边栏和主内容区 -->
<div class="flex">
<aside class="sidebar hidden md:block">
<button onclick="setActiveTab('topics')" style="width: 100%; text-align: left; padding: 0.5rem; border: none; background: transparent; border-radius: 0.25rem; margin-bottom: 0.25rem; cursor: pointer;">
📋 选题管理
</button>
<button onclick="setActiveTab('logs')" style="width: 100%; text-align: left; padding: 0.5rem; border: none; background: transparent; border-radius: 0.25rem; margin-bottom: 0.25rem; cursor: pointer;">
📄 系统日志
</button>
<button onclick="setActiveTab('users')" style="width: 100%; text-align: left; padding: 0.5rem; border: none; background: transparent; border-radius: 0.25rem; margin-bottom: 0.25rem; cursor: pointer;">
👥 用户管理
</button>
</aside>
<!-- 主内容区 -->
<main class="content">
<!-- 选题管理 -->
<div v-if="activeTab === 'topics'" class="card">
<h2 style="font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;">📋 选题管理</h2>
<div style="display: inline-block; min-width: fit-content; margin-bottom: 1rem;">
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<button onclick="batchOperation('refresh')" class="btn">🔄 批量刷新</button>
<button onclick="batchOperation('generate')" class="btn">▶ 批量创作</button>
<button onclick="batchOperation('optimize')" class="btn">🔍 批量优化</button>
</div>
</div>
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap;">
<span onclick="filterTopics('all')" style="padding: 0.25rem 0.75rem; background: #dbeafe; color: #1e40af; border-radius: 9999px; cursor: pointer;">全部 (3)</span>
<span onclick="filterTopics('pending')" style="padding: 0.25rem 0.75rem; background: #f3f4f6; color: #374151; border-radius: 9999px; cursor: pointer;">待处理 (1)</span>
<span onclick="filterTopics('review')" style="padding: 0.25rem 0.75rem; background: #f3f4f6; color: #374151; border-radius: 9999px; cursor: pointer;">待审查 (1)</span>
<span onclick="filterTopics('ready')" style="padding: 0.25rem 0.75rem; background: #f3f4f6; color: #374151; border-radius: 9999px; cursor: pointer;">待发布 (1)</span>
</div>
<div style="overflow-x: auto;">
<table class="table">
<thead>
<tr>
<th style="width: 55px"><input type="checkbox" onclick="toggleSelectAll()"></th>
<th style="width: 70px">ID</th>
<th>标题</th>
<th style="width: 100px">领域</th>
<th style="width: 90px">状态</th>
<th style="width: 210px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="topic in filteredTopics" :key="topic.id">
<td><input type="checkbox" v-model="selectedTopicIds" :value="topic.id"></td>
<td>{{ topic.id }}</td>
<td>{{ topic.title }}</td>
<td>{{ topic.field }}</td>
<td>
<span style="display: inline-flex; align-items: center; gap: 0.25rem;">
<span style="width: 6px; height: 6px; border-radius: 50%; background: #eab308;" v-if="topic.status === 'pending'"></span>
<span style="width: 6px; height: 6px; border-radius: 50%; background: #ef4444;" v-if="topic.status === 'review'"></span>
<span style="width: 6px; height: 6px; border-radius: 50%; background: #22c55e;" v-if="topic.status === 'ready'"></span>
{{ getStatusText(topic.status) }}
</span>
</td>
<td>
<button onclick="openPreview(topic)" style="padding: 0.25rem 0.5rem; background: #3b82f6; color: white; border: none; border-radius: 0.25rem; margin-right: 0.25rem; font-size: 0.75rem;">预览</button>
<button onclick="createTopic(topic)" style="padding: 0.25rem 0.5rem; background: #22c55e; color: white; border: none; border-radius: 0.25rem; margin-right: 0.25rem; font-size: 0.75rem;">创作</button>
<button onclick="deleteTopic(topic.id)" style="padding: 0.25rem 0.5rem; background: #ef4444; color: white; border: none; border-radius: 0.25rem; font-size: 0.75rem;">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 系统日志 -->
<div v-if="activeTab === 'logs'" class="card">
<h2 style="font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;">📄 系统日志</h2>
<div style="display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;">
<select v-model="logType" style="padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.25rem;">
<option value="creator">创作日志</option>
<option value="optimizer">优化日志</option>
<option value="collector">收集日志</option>
</select>
<input v-model="logDate" type="date" style="padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.25rem;">
<button onclick="loadLogs()" class="btn">加载日志</button>
</div>
<pre style="background: #f9fafb; padding: 1rem; border-radius: 0.25rem; border: 1px solid #e5e7eb; min-height: 200px; overflow-y: auto;">{{ logContent }}</pre>
</div>
<!-- 用户管理 -->
<div v-if="activeTab === 'users'" class="card">
<h2 style="font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;">👥 用户管理</h2>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3 style="font-size: 1.125rem; font-weight: bold;">用户列表</h3>
<button onclick="addUser()" class="btn">+ 新建用户</button>
</div>
<table class="table">
<thead>
<tr>
<th style="width: 70px">ID</th>
<th>用户名</th>
<th style="width: 100px">角色</th>
<th style="width: 180px">创建时间</th>
<th style="width: 150px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>
<span v-if="user.role === 'admin'" style="padding: 0.25rem 0.5rem; background: #fee2e2; color: #dc2626; border-radius: 0.25rem;">管理员</span>
<span v-if="user.role === 'editor'" style="padding: 0.25rem 0.5rem; background: #dcfce7; color: #16a34a; border-radius: 0.25rem;">编辑</span>
</td>
<td>{{ formatDate(user.created_at) }}</td>
<td>
<button onclick="deleteUser(user.id)" style="padding: 0.25rem 0.5rem; background: #ef4444; color: white; border: none; border-radius: 0.25rem; font-size: 0.75rem;" :disabled="user.role === 'admin'">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</main>
</div>
</div>
<script>
const EmergencyApp = {
data() {
return {
activeTab: 'topics',
topics: [
{ id: 'A01', title: '可持续发展趋势分析', field: '环保', status: 'pending' },
{ id: 'B02', title: 'AI在内容创作中的应用', field: '科技', status: 'review' },
{ id: 'C03', title: '数字化转型案例研究', field: '商业', status: 'ready' }
],
selectedTopicIds: [],
filteredTopics: [],
logType: 'creator',
logDate: '',
logContent: '请选择日志类型和日期,然后点击加载',
users: [
{ id: 'admin', username: '管理员', role: 'admin', created_at: '2026-04-01 09:00' },
{ id: 'editor1', username: '编辑小王', role: 'editor', created_at: '2026-04-05 14:30' }
]
}
},
methods: {
getStatusText(status) {
const texts = { 'pending': '待处理', 'review': '待审查', 'ready': '待发布' };
return texts[status] || status;
},
formatDate(dateStr) {
if (!dateStr) return '-';
return dateStr;
}
},
mounted() {
this.filteredTopics = this.topics;
console.log('紧急修复版Vue应用已启动');
}
}
Vue.createApp(EmergencyApp).mount('#app');
// 全局函数
window.setActiveTab = function(tab) {
appData.activeTab = tab;
};
window.batchOperation = function(type) {
console.log('批量操作:', type);
};
window.filterTopics = function(filter) {
if (filter === 'all') {
appData.filteredTopics = appData.topics;
} else {
appData.filteredTopics = appData.topics.filter(t => t.status === filter);
}
};
window.toggleSelectAll = function() {
// 切换全选逻辑
};
window.openPreview = function(topic) {
console.log('打开预览:', topic);
};
window.createTopic = function(topic) {
console.log('创作选题:', topic);
};
window.deleteTopic = function(id) {
console.log('删除选题:', id);
};
window.loadLogs = function() {
const logs = {
creator: '2026-04-27 11:45:23 | 成功生成选题 B02 - AI在内容创作中的应用\n2026-04-27 11:30:15 | 开始生成选题 C03 - 数字化转型案例研究',
optimizer: '2026-04-27 12:05:30 | 优化完成选题 C03 - 合规分提升至78\n2026-04-27 11:50:45 | 优化中选题 B02 - 等待人工审核',
collector: '2026-04-27 10:30:12 | 收集到3个新选题\n2026-04-27 09:45:20 | 更新行业热点数据'
};
appData.logContent = \`日志类型: \${appData.logType}\n日期: \${appData.logDate || '今天'}\n\n\${logs[appData.logType] || '暂无日志数据'}\`;
};
window.addUser = function() {
console.log('添加用户');
};
window.deleteUser = function(id) {
if (id !== 'admin') {
console.log('删除用户:', id);
}
};
</script>
</body>
</html>
`;
// 替换当前页面内容
document.documentElement.innerHTML = emergencyHTML;
document.getElementById('emergencyResult').innerHTML =
'<p style="color: green; font-weight: bold;">✅ 紧急修复已应用!</p>' +
'<p>页面已更新为简化版本,移除了复杂的依赖。</p>' +
'<p><a href="#" onclick="location.reload()" class="btn" style="background: #28a745;">重新加载</a></p>';
}, 1000);
}
function addResult(message, type = 'info') {
const resultDiv = document.getElementById('testResults');
const colorClass = type === 'success' ? 'success' : type === 'error' ? 'error' : 'warning';
resultDiv.innerHTML +=
'<div class="status ' + colorClass + '" style="margin: 5px 0; padding: 5px 10px; display: inline-block;">' + message + '</div>';
}
function updateDetailedAnalysis() {
const analysisDiv = document.getElementById('detailedAnalysis');
let analysis = '';
analysis += '<h4>当前状态:</h4>';
analysis += '<ul>';
analysis += '<li>Vue就绪: ' + (appData.vueReady ? '✅' : '❌') + '</li>';
analysis += '<li>DOM就绪: ' + (appData.domReady ? '✅' : '❌') + '</li>';
analysis += '<li>CSS就绪: ' + (appData.cssLoaded ? '✅' : '❌') + '</li>';
analysis += '</ul>';
if (appData.errors.length > 0) {
analysis += '<h4 style="color: red;">错误:</h4>';
analysis += '<ul>';
appData.errors.forEach(error => {
analysis += '<li style="color: red;">' + error + '</li>';
});
analysis += '</ul>';
}
analysisDiv.innerHTML = analysis;
}
function generateSolutions() {
const solutionsDiv = document.getElementById('solutionsList');
let solutions = '';
solutions += '<li><strong>检查浏览器控制台</strong>: 按F12查看JavaScript错误</li>';
solutions += '<li><strong>验证CDN资源</strong>: 确保Vue和Element Plus能正常下载</li>';
solutions += '<li><strong>简化页面结构</strong>: 移除复杂依赖,使用纯HTML/CSS/JS</li>';
solutions += '<li><strong>检查网络连接</strong>: 确认能访问外部资源</li>';
solutions += '<li><strong>清除缓存</strong>: 尝试无痕模式或清除浏览器缓存</li>';
solutions += '<li><strong>使用本地托管</strong>: 下载Vue和Element Plus到本地服务器</li>';
solutionsDiv.innerHTML = solutions;
}
// 自动运行初始诊断
setTimeout(runQuickTest, 100);
</script>
</body>
</html>
-451
View File
@@ -1,451 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 独立Vue测试</title>
<!-- 仅包含必要的资源 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; margin-bottom: 24px; }
aside button { width: 100%; text-align: left; border: none; background: transparent; border-radius: 8px; margin-bottom: 4px; }
@media (max-width: 768px) { main { padding-bottom: 70px; } }
.nav-title { text-align: center; }
.status-badge { display: inline-flex; align-items: center; gap: 4px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; }
.status-dot.pending { background: #E6A23C; }
.status-dot.review { background: #F56C6C; }
.status-dot.ready { background: #67C23A; }
.status-dot.published { background: #409EFF; }
.debug-panel { background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto; }
.btn-primary { padding: 8px 16px; background: #409EFF; color: white; border: none; border-radius: 4px; cursor: pointer; }
.btn-primary:hover { background: #337ecc; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.table th { background-color: #f2f2f2; }
</style>
</head>
<body>
<div id="app">
<!-- 导航栏 -->
<nav class="bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg">
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
<h1 class="text-2xl font-bold nav-title">宇之然内容创作平台 - 独立Vue测试</h1>
</div>
</nav>
<!-- 侧边栏 -->
<div class="page flex gap-6">
<aside class="w-40 flex-shrink-0 hidden md:block">
<button @click="testType='topics'" :class="['px-4 py-2 rounded-lg', testType === 'topics' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📋 选题管理</button>
<button @click="testType='logs'" :class="['px-4 py-2 rounded-lg', testType === 'logs' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📄 系统日志</button>
<button @click="testType='users'" :class="['px-4 py-2 rounded-lg', testType === 'users' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">👥 用户管理</button>
</aside>
<!-- 主内容区 -->
<main class="flex-1">
<div class="card" style="padding: 20px; margin-top: 20px;">
<h2 class="text-2xl font-bold text-gray-800 mb-6">🔍 独立Vue应用测试</h2>
<!-- 调试信息显示 -->
<div class="debug-panel mb-4">
<strong>实时输出:</strong><br/>
<span v-for="log in debugLogs" :key="log">{{ log }}</span>
</div>
<!-- 测试按钮 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<button @click="runFullTest" class="btn-primary">运行完整测试</button>
<button @click="testElementPlus" class="btn-primary">测试Element Plus模拟</button>
<button @click="resetDebug" class="btn-primary">重置调试</button>
</div>
<!-- 测试结果 -->
<div v-if="testResults.length > 0" class="mb-4 p-4 bg-green-50 border-l-4 border-green-400">
<h3 class="font-bold mb-2">测试结果:</h3>
<ul class="list-disc pl-5">
<li v-for="result in testResults">{{ result }}</li>
</ul>
</div>
<!-- 选题管理测试 -->
<div v-if="testType === 'topics'" class="overflow-x-auto">
<h3 class="text-xl font-bold mb-4">📋 选题管理功能</h3>
<!-- 批量操作 -->
<div class="mb-4 p-4 bg-blue-50 rounded">
<div class="flex flex-wrap gap-2 items-center">
<button @click="refreshAll" class="btn-primary">🔄 批量刷新</button>
<button @click="triggerGenerateSelected" :disabled="selectedTopicIds.length === 0" class="btn-primary">▶ 批量创作</button>
<button @click="triggerOptimizeSelected" :disabled="selectedTopicIds.length === 0" class="btn-primary">🔍 批量优化</button>
<span class="ml-auto text-sm text-gray-500" v-if="selectedTopicIds.length > 0">已选 {{ selectedTopicIds.length }} 项</span>
</div>
</div>
<!-- 筛选标签 -->
<div class="flex flex-wrap gap-2 mb-4">
<span @click="filterStatus = ''" :class="[filterStatus === '' ? 'bg-blue-500' : 'bg-gray-200', 'px-3 py-1 rounded-full cursor-pointer text-white']">全部 ({{ topics.length }})</span>
<span @click="filterStatus = '待处理'" :class="[filterStatus === '待处理' ? 'bg-blue-500' : 'bg-gray-200', 'px-3 py-1 rounded-full cursor-pointer']">待处理 ({{ countByStatus('待处理') }})</span>
<span @click="filterStatus = '待审查'" :class="[filterStatus === '待审查' ? 'bg-blue-500' : 'bg-gray-200', 'px-3 py-1 rounded-full cursor-pointer']">待审查 ({{ countByStatus('待审查') }})</span>
<span @click="filterStatus = '待发布'" :class="[filterStatus === '待发布' ? 'bg-blue-500' : 'bg-gray-200', 'px-3 py-1 rounded-full cursor-pointer']">待发布 ({{ countByStatus('待发布') }})</span>
<span @click="filterStatus = '已发布'" :class="[filterStatus === '已发布' ? 'bg-blue-500' : 'bg-gray-200', 'px-3 py-1 rounded-full cursor-pointer']">已发布 ({{ countByStatus('已发布') }})</span>
</div>
<!-- 表格 -->
<table class="table">
<thead>
<tr>
<th style="width: 55px"><input type="checkbox" @change="toggleSelectAll"></th>
<th style="width: 70px">ID</th>
<th>标题</th>
<th style="width: 100px">领域</th>
<th style="width: 90px">状态</th>
<th style="width: 210px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="topic in filteredTopics" :key="topic.id">
<td><input type="checkbox" v-model="selectedTopicIds" :value="topic.id"></td>
<td>{{ topic.id }}</td>
<td>{{ topic.title }}</td>
<td>{{ topic.field }}</td>
<td>
<span class="status-badge">
<span class="status-dot" :class="getStatusClass(topic.status)"></span>
{{ getStatusText(topic.status) }}
</span>
</td>
<td>
<button @click="openPreview(topic)" class="px-2 py-1 bg-blue-500 text-white rounded mr-1 text-xs">预览</button>
<button @click="createTopic(topic)" :disabled="topic.status !== '待处理'" class="px-2 py-1 bg-green-500 text-white rounded mr-1 text-xs">创作</button>
<button @click="optimizeTopic(topic)" :disabled="topic.status !== '待审查'" class="px-2 py-1 bg-yellow-500 text-white rounded mr-1 text-xs">审查</button>
<button v-if="topic.status === '待发布'" @click="handlePublish(topic)" class="px-2 py-1 bg-blue-500 text-white rounded mr-1 text-xs">发布</button>
<button @click="deleteTopic(topic.id)" class="px-2 py-1 bg-red-500 text-white rounded text-xs">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 系统日志测试 -->
<div v-if="testType === 'logs'" class="p-6 bg-yellow-50 rounded">
<h3 class="text-xl font-bold mb-4">📄 系统日志功能</h3>
<div class="flex flex-wrap gap-4 mb-4">
<select v-model="logType" class="px-3 py-2 border rounded">
<option value="creator">创作日志</option>
<option value="optimizer">优化日志</option>
<option value="collector">收集日志</option>
</select>
<input v-model="logDate" type="date" class="px-3 py-2 border rounded">
<button @click="fetchLogs" class="btn-primary">加载日志</button>
</div>
<pre class="bg-white p-4 rounded border min-h-[200px] whitespace-pre-wrap font-mono text-sm">{{ logContent }}</pre>
</div>
<!-- 用户管理测试 -->
<div v-if="testType === 'users'" class="p-6 bg-purple-50 rounded">
<h3 class="text-xl font-bold mb-4">👥 用户管理功能</h3>
<div class="flex justify-between items-center mb-4">
<h4 class="font-bold">用户列表</h4>
<button @click="addUser" class="btn-primary">+ 新建用户</button>
</div>
<table class="table">
<thead>
<tr>
<th style="width: 70px">ID</th>
<th>用户名</th>
<th style="width: 100px">角色</th>
<th style="width: 180px">创建时间</th>
<th style="width: 150px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>
<span :class="[user.role === 'admin' ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800', 'px-2 py-1 rounded']">
{{ user.role === 'admin' ? '管理员' : '编辑' }}
</span>
</td>
<td>{{ formatDate(user.created_at) }}</td>
<td>
<button @click="deleteUser(user.id)" :disabled="user.role === 'admin'" class="px-2 py-1 bg-red-500 text-white rounded text-xs">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<script>
const IndependentApp = {
data() {
return {
// 基础数据
testType: 'topics',
// 调试相关
debugLogs: [
'独立Vue应用已启动',
'请运行测试查看详细信息',
''
],
testResults: [],
// 选题相关数据
status: {},
topics: [
{
id: 'A01',
title: '可持续发展趋势分析',
field: '环保',
status: 'pending',
compliance_score: 85,
created_at: '2026-04-27 10:30',
generated_at: '-',
published_at: '-',
updated_at: '2026-04-27 10:30',
priority_score: '高'
},
{
id: 'B02',
title: 'AI在内容创作中的应用',
field: '科技',
status: 'review',
compliance_score: 92,
created_at: '2026-04-27 11:15',
generated_at: '2026-04-27 11:45',
published_at: '-',
updated_at: '2026-04-27 11:45',
priority_score: '中'
},
{
id: 'C03',
title: '数字化转型案例研究',
field: '商业',
status: 'ready',
compliance_score: 78,
created_at: '2026-04-27 12:00',
generated_at: '2026-04-27 12:30',
published_at: '2026-04-27 13:00',
updated_at: '2026-04-27 13:00',
priority_score: '高'
}
],
filterStatus: '',
selectedTopicIds: [],
// 日志相关
logType: 'creator',
logDate: '',
logContent: '请选择日志类型和日期,然后点击加载',
// 用户相关
users: [
{ id: 'admin', username: '管理员', role: 'admin', created_at: '2026-04-01 09:00' },
{ id: 'editor1', username: '编辑小王', role: 'editor', created_at: '2026-04-05 14:30' },
{ id: 'editor2', username: '编辑小李', role: 'editor', created_at: '2026-04-10 10:15' }
]
}
},
computed: {
filteredTopics() {
if (!this.topics.length) return []
if (!this.filterStatus) return this.topics
return this.topics.filter(t => t.status === this.filterStatus)
},
countByStatus() {
return (status) => this.topics.filter(t => t.status === status).length
}
},
methods: {
addLog(message) {
this.debugLogs.push('[' + new Date().toLocaleTimeString() + '] ' + message);
},
runFullTest() {
this.addLog('开始运行完整测试...');
// 测试数据绑定
setTimeout(() => {
this.addLog('✅ Vue数据绑定测试通过');
}, 100);
// 测试方法调用
setTimeout(() => {
this.addLog('✅ Vue方法调用测试通过');
this.testResults.push('Vue数据绑定正常');
}, 200);
// 测试DOM操作
setTimeout(() => {
this.addLog('✅ VueDOM渲染测试通过');
this.testResults.push('VueDOM操作正常');
}, 300);
// 测试计算属性
setTimeout(() => {
this.addLog('✅ Vue计算属性测试通过');
this.testResults.push('Vue计算属性正常');
}, 400);
},
testElementPlus() {
this.addLog('正在测试Element Plus模拟...');
// 模拟Element Plus功能测试
setTimeout(() => {
this.addLog('✅ Element Plus样式模拟成功');
this.addLog('✅ Element Plus组件模拟可用');
this.testResults.push('Element Plus模拟集成正常');
}, 200);
},
resetDebug() {
this.debugLogs = ['独立Vue应用已启动', '请运行测试查看详细信息', ''];
this.testResults = [];
this.addLog('调试信息已重置');
},
refreshAll() {
this.addLog('执行批量刷新操作');
this.testResults.push('批量刷新操作已触发');
},
triggerGenerateSelected() {
if (!this.selectedTopicIds.length) return
this.addLog('正在批量创作...');
this.testResults.push('批量创作操作已触发');
},
triggerOptimizeSelected() {
if (!this.selectedTopicIds.length) return
this.addLog('正在批量优化...');
this.testResults.push('批量优化操作已触发');
},
toggleSelectAll(event) {
if (event.target.checked) {
this.selectedTopicIds = this.topics.map(t => t.id);
} else {
this.selectedTopicIds = [];
}
},
openPreview(topic) {
this.addLog('打开选题预览: ' + topic.title);
this.testResults.push('预览功能正常');
},
createTopic(topic) {
if (topic && topic.status === '待处理') {
this.addLog('创作选题: ' + topic.title);
this.testResults.push('选题创作功能正常');
}
},
optimizeTopic(topic) {
if (topic && topic.status === '待审查') {
this.addLog('优化选题: ' + topic.title);
this.testResults.push('选题优化功能正常');
}
},
handlePublish(topic) {
this.addLog('发布选题: ' + topic.title);
this.testResults.push('选题发布功能正常');
},
deleteTopic(id) {
this.addLog('删除选题: ' + id);
this.testResults.push('选题删除功能正常');
},
fetchLogs() {
const logs = {
creator: `2026-04-27 11:45:23 | 成功生成选题 B02 - AI在内容创作中的应用
2026-04-27 11:30:15 | 开始生成选题 C03 - 数字化转型案例研究`,
optimizer: `2026-04-27 12:05:30 | 优化完成选题 C03 - 合规分提升至78
2026-04-27 11:50:45 | 优化中选题 B02 - 等待人工审核`,
collector: `2026-04-27 10:30:12 | 收集到3个新选题
2026-04-27 09:45:20 | 更新行业热点数据`
}[this.logType] || '暂无日志数据';
this.logContent = `日志类型: ${this.logType}
日期: ${this.logDate || '今天'}
${logs}`;
this.addLog('日志加载成功');
this.testResults.push('日志加载功能正常');
},
addUser() {
const newId = 'user' + Date.now();
this.users.push({ id: newId, username: '新用户', role: 'editor', created_at: new Date().toISOString().slice(0, 16).replace('T', ' ') });
this.addLog('添加新用户: ' + newId);
this.testResults.push('用户添加功能正常');
},
deleteUser(id) {
if (id !== 'admin') {
this.users = this.users.filter(u => u.id !== id);
this.addLog('删除用户: ' + id);
this.testResults.push('用户删除功能正常');
} else {
this.addLog('不能删除管理员用户');
this.testResults.push('管理员保护功能正常');
}
},
getStatusClass(status) {
const classes = {
'pending': 'status-dot pending',
'review': 'status-dot review',
'ready': 'status-dot ready',
'published': 'status-dot published'
};
return classes[status] || '';
},
getStatusText(status) {
const texts = {
'pending': '待处理',
'review': '待审查',
'ready': '待发布',
'published': '已发布'
};
return texts[status] || status;
},
formatDate(dateStr) {
if (!dateStr || dateStr === '-' || dateStr.trim() === '') return '-'
const date = new Date(dateStr.replace(' ', 'T'))
return date.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
})
}
},
mounted() {
this.addLog('独立Vue应用程序挂载完成');
this.addLog('应用初始状态:', this.$data);
console.log('独立Vue应用已启动');
}
}
Vue.createApp(IndependentApp).mount('#app')
</script>
</body>
</html>
+33 -77
View File
@@ -224,28 +224,8 @@
}
.module-content div:last-child { border-bottom: none; }
/* 移动端导航 */
.mobile-nav {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(26, 26, 46, 0.95);
backdrop-filter: blur(20px);
border-top: 1px solid rgba(102, 126, 234, 0.2);
padding: 8px 0;
z-index: 1000;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.3);
}
/* 响应式 */
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area {
padding: 16px;
padding-bottom: 80px;
@@ -268,9 +248,6 @@
.sidebar { background: white !important; border-right: 1px solid #ebeef5 !important; backdrop-filter: none !important; }
.mobile-nav { background: white !important; border-top: 1px solid #ebeef5 !important; backdrop-filter: none !important; box-shadow: 0 -2px 8px rgba(0,0,0,0.1) !important; }
#page-overview h2, #page-overview h3, #page-overview .module-title { color: #303133 !important; }
#page-overview .module-content { color: #303133 !important; }
@@ -333,50 +310,20 @@
<!-- 模块状态 -->
<h3 style="font-size: 20px; font-weight: 600; margin-bottom: 24px; color: #e0e6ed;">
🔧 模块状态
<span style="font-size: 13px; font-weight: 400; color: #909399; margin-left: 12px;">
定时任务: {{ schedulerStatus }}
</span>
</h3>
<div class="module-grid">
<div class="module-card">
<div v-for="mod in modules" :key="mod.id" class="module-card">
<div class="module-header">
<span class="module-title">🤖 内容创作引擎</span>
<span class="module-status running">运行中</span>
<span class="module-title">{{ mod.title }}</span>
<span :class="['module-status', mod.status === 'running' ? 'running' : '']">{{ mod.status === 'running' ? '运行中' : '已停止' }}</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 14:30</span></div>
<div><span>今日任务</span><span>12</span></div>
<div><span>成功率</span><span>95%</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">🔍 内容优化器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 14:45</span></div>
<div><span>今日优化</span><span>8 个</span></div>
<div><span>平均提升</span><span>+12 分</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">📡 内容收集器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 14:00</span></div>
<div><span>今日收集</span><span>24 个</span></div>
<div><span>来源平台</span><span>8 个</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">📤 发布管理器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 13:30</span></div>
<div><span>今日发布</span><span>5 个</span></div>
<div><span>成功率</span><span>100%</span></div>
<div><span>最后运行</span><span>{{ mod.last_run }}</span></div>
<div><span>今日任务</span><span>{{ mod.task_count }}</span></div>
<div v-if="mod.success_rate !== null"><span>成功率</span><span>{{ mod.success_rate }}%</span></div>
</div>
</div>
</div>
@@ -385,20 +332,7 @@
</div>
<!-- 移动端导航 -->
<nav class="mobile-nav" v-if="isLoggedIn">
<button class="mobile-nav-btn" :class="{ active: currentPage === 'overview' }" @click="redirectToPage('/')">
📊 概览
</button>
<button class="mobile-nav-btn" :class="{ active: currentPage === 'topics' }" @click="redirectToPage('topics.html')">
📋 选题
</button>
<button class="mobile-nav-btn" :class="{ active: currentPage === 'logs' }" @click="redirectToPage('logs.html')">
📄 日志
</button>
<button v-if="isAdmin" class="mobile-nav-btn" :class="{ active: currentPage === 'users' }" @click="redirectToPage('users.html')">
👥 用户
</button>
</nav>
<!-- 移动端导航由 navigation-component.js 注入 -->
</div>
<script src="vue.global.prod.js"></script>
@@ -418,7 +352,9 @@
ready: 0,
published: 0,
today: 0
}
},
modules: [],
schedulerStatus: ''
};
},
methods: {
@@ -489,6 +425,25 @@
this.stats = { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 };
}
},
async fetchModules() {
try {
const token = localStorage.getItem('authToken');
const resp = await fetch('/api/system/modules/status', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (resp.ok) {
const data = await resp.json();
this.modules = data.modules || [];
const s = data.scheduler || {};
this.schedulerStatus = s.running ? '🟢 运行中' : '🔴 未启动';
if (s.jobs && s.jobs.length) {
this.schedulerStatus += ' · ' + s.jobs.length + ' 个任务';
}
}
} catch (e) {
console.error('获取模块状态失败:', e);
}
},
goToTopics(filter) {
const url = filter ? '/topics.html?filter=' + encodeURIComponent(filter) : '/topics.html';
window.location.href = url;
@@ -513,6 +468,7 @@
this.isLoggedIn = true;
this.currentPage = 'overview';
this.fetchStats();
this.fetchModules();
})
.catch(() => {
localStorage.removeItem('authToken');
+6 -10
View File
@@ -5,19 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 系统日志</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
.navbar-title { font-size: 20px; font-weight: 600; }
.navbar-user { display: flex; align-items: center; gap: 16px; }
.user-info { display: flex; align-items: center; gap: 8px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; min-height: calc(100vh - 60px); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
@@ -43,14 +39,14 @@
<navigation-component current-page="logs" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">📄 系统日志</h2>
</div>
<div class="controls">
<el-select v-model="logType" placeholder="日志类型" style="width: 180px;">
<el-option label="创作日志" value="creator"></el-option>
<el-option label="优化日志" value="optimizer"></el-option>
<el-option label="审查日志" value="optimizer"></el-option>
<el-option label="收集日志" value="collector"></el-option>
</el-select>
<el-date-picker v-model="logDate" type="date" placeholder="选择日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
+3 -12
View File
@@ -5,29 +5,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 数据分析</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
.navbar-title { font-size: 20px; font-weight: 600; }
.navbar-user { display: flex; align-items: center; gap: 16px; }
.user-info { display: flex; align-items: center; gap: 8px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; }
.sidebar { width: 180px; background: white; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 20px; color: white; text-align: center; }
.stat-card.success { background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%); }
.stat-card.warning { background: linear-gradient(135deg, #e6a23c 0%, #f5c543 100%); }
.stat-card.danger { background: linear-gradient(135deg, #f56c6c 0%, #f78989 100%); }
.stat-value { font-size: 32px; font-weight: 700; }
.stat-label { font-size: 14px; opacity: 0.9; margin-top: 4px; }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
.stats-grid { grid-template-columns: repeat(2, 1fr) !important; gap: 12px !important; }
.chart-container { height: 250px !important; }
@@ -67,7 +58,7 @@
<div class="stat-label">平均互动率</div>
</div>
</div>
<div class="card">
<div class="card page-fade">
<h3 style="font-size: 18px; margin-bottom: 16px;">📈 选题状态分布</h3>
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<div v-for="(count, status) in dashboard.topics_by_status" :key="status" style="text-align: center;">
+9 -6
View File
@@ -5,14 +5,17 @@
function injectStyles() {
if (document.getElementById('navbar-styles')) return;
const styles = `
.navbar-component { position: fixed; top: 0; left: 0; right: 0; height: 60px; background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 10000; }
.navbar-component .navbar-title { font-size: 18px; font-weight: 600; margin: 0; }
.navbar-component { position: fixed; top: 0; left: 0; right: 0; height: 60px; background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; box-shadow: 0 4px 20px rgba(37, 99, 235, 0.3); z-index: 10000; }
.navbar-component .navbar-title { font-size: 20px; font-weight: 700; margin: 0; letter-spacing: -0.3px; }
.navbar-component .navbar-user { display: flex; align-items: center; gap: 12px; }
.navbar-component .user-info { display: flex; align-items: center; gap: 8px; }
.navbar-component .avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.navbar-component .logout-btn { background: rgba(255,255,255,0.2); border: none; color: white; padding: 6px 12px; border-radius: 6px; cursor: pointer; margin-left: 12px; }
.navbar-component .logout-btn:hover { background: rgba(255,255,255,0.3); }
.navbar-component .admin-badge { margin-left: 8px; font-size: 12px; background: rgba(255,255,255,0.3); padding: 2px 8px; border-radius: 10px; }
.navbar-component .avatar { width: 34px; height: 34px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 15px; font-weight: 600; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
.navbar-component .logout-btn { background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.2); color: white; padding: 6px 14px; border-radius: 8px; cursor: pointer; margin-left: 12px; font-size: 13px; transition: all 0.2s; }
.navbar-component .logout-btn:hover { background: rgba(255,255,255,0.25); border-color: rgba(255,255,255,0.3); transform: translateY(-1px); }
.navbar-component .admin-badge { margin-left: 8px; font-size: 11px; background: rgba(255,255,255,0.2); padding: 2px 10px; border-radius: 12px; font-weight: 500; letter-spacing: 0.3px; }
@media (min-width: 769px) {
.navbar-component { padding: 0 24px 0 180px; }
}
@media (max-width: 768px) {
.navbar-component { padding: 0 16px; }
.navbar-component .navbar-title { font-size: 16px; }
+110 -39
View File
@@ -5,20 +5,31 @@
function injectStyles() {
if (document.getElementById('navigation-styles')) return;
const styles = `
.nav-wrapper, .navigation-wrapper { position: fixed; top: 60px; left: 0; bottom: 0; z-index: 9999; }
.nav-wrapper .sidebar, .navigation-wrapper .sidebar { position: fixed; top: 60px; left: 0; bottom: 0; width: 180px; background: #fff; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.1); overflow-y: auto; z-index: 99999; border-right: 1px solid #ebeef5; }
.nav-wrapper .sidebar-header, .navigation-wrapper .sidebar-header { padding: 12px 8px 16px; border-bottom: 1px solid #ebeef5; margin-bottom: 12px; }
.nav-wrapper .sidebar-header h3, .navigation-wrapper .sidebar-header h3 { margin: 0; font-size: 16px; font-weight: 600; color: #303133; }
.nav-wrapper .sidebar-btn, .navigation-wrapper .sidebar-btn { width: 100%; text-align: left; padding: 10px 12px; border: none; background: transparent; border-radius: 8px; margin-bottom: 6px; cursor: pointer; transition: all 0.3s; color: #606266; font-size: 14px; display: flex; align-items: center; gap: 6px; }
.nav-wrapper .sidebar-btn:hover, .navigation-wrapper .sidebar-btn:hover { background: #f5f7fa; color: #409eff; }
.nav-wrapper .sidebar-btn.active, .navigation-wrapper .sidebar-btn.active { background: #ecf5ff; color: #409eff; font-weight: 600 !important; }
.nav-wrapper .mobile-nav, .navigation-wrapper .mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: #1890ff; box-shadow: 0 -2px 8px rgba(0,0,0,0.2); padding: 8px 0; z-index: 99999; justify-content: space-around; }
.nav-wrapper .mobile-nav-btn, .navigation-wrapper .mobile-nav-btn { flex: 1; border: none; background: transparent; padding: 8px 4px; text-align: center; font-size: 11px; color: white !important; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.nav-wrapper .mobile-nav-btn .nav-icon, .navigation-wrapper .mobile-nav-btn .nav-icon { font-size: 18px; line-height: 1; }
.nav-wrapper .mobile-nav-btn .nav-text, .navigation-wrapper .mobile-nav-btn .nav-text { font-size: 10px; margin-top: 2px; }
.nav-wrapper .mobile-nav-btn:hover, .navigation-wrapper .mobile-nav-btn:hover { background: rgba(255,255,255,0.2); }
.nav-wrapper .mobile-nav-btn.active, .navigation-wrapper .mobile-nav-btn.active { background: rgba(255,255,255,0.3); font-weight: 600 !important; }
@media (max-width: 768px) { .nav-wrapper .sidebar, .navigation-wrapper .sidebar { display: none !important; } .nav-wrapper .mobile-nav, .navigation-wrapper .mobile-nav { display: flex !important; } }
.nav-wrapper, .navigation-wrapper { position: fixed; top: 60px; left: 0; bottom: 0; width: 180px; z-index: 9999; pointer-events: none; }
.nav-wrapper .sidebar, .navigation-wrapper .sidebar { position: fixed; top: 60px; left: 0; bottom: 0; width: 180px; background: #fff; padding: 12px 8px; box-shadow: 2px 0 12px rgba(0,0,0,0.06); overflow-y: auto; z-index: 10000; border-right: 1px solid #ebeef5; pointer-events: auto; }
.nav-wrapper .sidebar-header, .navigation-wrapper .sidebar-header { padding: 12px 12px 16px; border-bottom: 1px solid #f0f2f5; margin-bottom: 12px; }
.nav-wrapper .sidebar-header h3, .navigation-wrapper .sidebar-header h3 { margin: 0; font-size: 15px; font-weight: 700; background: linear-gradient(135deg, #2563eb, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; letter-spacing: -0.3px; }
.nav-wrapper .sidebar-btn, .navigation-wrapper .sidebar-btn { width: 100%; text-align: left; padding: 10px 12px; border: none; background: transparent; border-radius: 10px; margin-bottom: 4px; cursor: pointer; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); color: #606266; font-size: 13px; display: flex; align-items: center; gap: 8px; position: relative; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nav-wrapper .sidebar-btn:hover, .navigation-wrapper .sidebar-btn:hover { background: #f0f4ff; color: #2563eb; transform: translateX(2px); }
.nav-wrapper .sidebar-btn.active, .navigation-wrapper .sidebar-btn.active { background: linear-gradient(135deg, #eef2ff, #e0e7ff); color: #2563eb; font-weight: 600 !important; box-shadow: inset 3px 0 0 #2563eb; }
.nav-wrapper .mobile-nav, .navigation-wrapper .mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: linear-gradient(135deg, #2563eb, #1d4ed8); box-shadow: 0 -4px 20px rgba(37, 99, 235, 0.25); padding: 4px 0; z-index: 99999; justify-content: space-around; pointer-events: auto; }
.nav-wrapper .mobile-nav-btn, .navigation-wrapper .mobile-nav-btn { flex: 1; border: none; background: transparent; padding: 4px 2px; text-align: center; font-size: 10px; color: rgba(255,255,255,0.8) !important; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 0; transition: all 0.2s; border-radius: 8px; margin: 0 1px; min-width: 0; }
.nav-wrapper .mobile-nav-btn .nav-icon, .navigation-wrapper .mobile-nav-btn .nav-icon { font-size: 16px; line-height: 1.2; }
.nav-wrapper .mobile-nav-btn .nav-text, .navigation-wrapper .mobile-nav-btn .nav-text { font-size: 9px; margin-top: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.nav-wrapper .mobile-nav-btn:hover, .navigation-wrapper .mobile-nav-btn:hover { background: rgba(255,255,255,0.15); color: white !important; }
.nav-wrapper .mobile-nav-btn.active, .navigation-wrapper .mobile-nav-btn.active { background: rgba(255,255,255,0.2); color: white !important; font-weight: 600 !important; }
.mobile-more-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); z-index: 100000; animation: fadeIn 0.2s ease-out; }
.mobile-more-sheet { position: fixed; bottom: 0; left: 0; right: 0; background: white; border-radius: 16px 16px 0 0; box-shadow: 0 -8px 32px rgba(0,0,0,0.15); z-index: 100001; padding: 20px 16px calc(env(safe-area-inset-bottom) + 60px); animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); max-height: 70vh; overflow-y: auto; pointer-events: auto; }
.mobile-more-sheet .sheet-handle { width: 36px; height: 4px; background: #e0e0e0; border-radius: 2px; margin: 0 auto 16px; }
.mobile-more-sheet .sheet-title { font-size: 16px; font-weight: 700; color: #303133; margin-bottom: 16px; text-align: center; }
.mobile-more-sheet .sheet-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.mobile-more-sheet .sheet-item { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 12px 4px; border: none; background: transparent; border-radius: 12px; cursor: pointer; transition: all 0.2s; color: #606266; font-size: 12px; white-space: nowrap; }
.mobile-more-sheet .sheet-item:hover { background: #f0f4ff; color: #2563eb; }
.mobile-more-sheet .sheet-item.active { background: #eef2ff; color: #2563eb; font-weight: 600; }
.mobile-more-sheet .sheet-item .item-icon { font-size: 24px; line-height: 1; }
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@media (max-width: 768px) { .nav-wrapper .sidebar, .navigation-wrapper .sidebar { display: none !important; } .nav-wrapper .mobile-nav, .navigation-wrapper .mobile-nav { display: flex !important; } body > #app > .main-content { padding-bottom: 56px !important; } }
body > #app > .main-content { margin-left: 180px !important; padding-top: 60px !important; }
@media (max-width: 768px) { body > #app > .main-content { margin-left: 0 !important; padding-bottom: 60px !important; } }
`;
@@ -55,36 +66,76 @@
mobileNav.innerHTML = `
<button class="mobile-nav-btn ${currentPage==='dashboard'?'active':''}" data-page="/"><span class="nav-icon">📊</span><span class="nav-text">首页</span></button>
<button class="mobile-nav-btn ${currentPage==='topics'?'active':''}" data-page="topics.html"><span class="nav-icon">📋</span><span class="nav-text">选题</span></button>
<button class="mobile-nav-btn ${currentPage==='tasks'?'active':''}" data-page="tasks.html"><span class="nav-icon">🚀</span><span class="nav-text">任务</span></button>
<button class="mobile-nav-btn ${currentPage==='calendar'?'active':''}" data-page="calendar.html"><span class="nav-icon">📅</span><span class="nav-text">日历</span></button>
<button class="mobile-nav-btn ${currentPage==='assets'?'active':''}" data-page="assets.html"><span class="nav-icon">🖼️</span><span class="nav-text">素材</span></button>
<button class="mobile-nav-btn ${currentPage==='tasks'?'active':''}" data-page="tasks.html"><span class="nav-icon">🚀</span><span class="nav-text">任务</span></button>
<button class="mobile-nav-btn" id="mobile-more-btn"><span class="nav-icon"></span><span class="nav-text">更多</span></button>
`;
// "更多"弹出菜单
const sheetItems = [
{ key: 'metrics', label: '数据分析', icon: '📊', page: 'metrics.html' },
{ key: 'platforms', label: '平台配置', icon: '🌐', page: 'platforms.html' },
{ key: 'logs', label: '系统日志', icon: '📄', page: 'logs.html' },
{ key: 'users', label: '用户管理', icon: '👥', page: 'users.html', admin: true },
{ key: 'admin', label: '系统管理', icon: '⚙️', page: 'admin.html', admin: true },
];
const sheetItemsHtml = sheetItems.map(item =>
`<button class="sheet-item ${currentPage===item.key?'active':''}" data-page="${item.page}"${item.admin ? ' data-admin="1"' : ''} style="${item.admin && !isAdmin ? 'display:none' : ''}">
<span class="item-icon">${item.icon}</span><span>${item.label}</span>
</button>`
).join('');
const backdrop = document.createElement('div');
backdrop.className = 'mobile-more-backdrop';
backdrop.id = 'mobile-more-backdrop';
backdrop.style.display = 'none';
const sheet = document.createElement('div');
sheet.className = 'mobile-more-sheet';
sheet.id = 'mobile-more-sheet';
sheet.style.display = 'none';
sheet.innerHTML = `<div class="sheet-handle"></div><div class="sheet-title">所有菜单</div><div class="sheet-grid">${sheetItemsHtml}</div>`;
const showMore = () => {
backdrop.style.display = ''; sheet.style.display = '';
};
const hideMore = () => {
backdrop.style.display = 'none'; sheet.style.display = 'none';
};
// 绑定导航跳转
const handleNavClick = (btn) => {
const page = btn.getAttribute('data-page');
if (!page) return;
console.log('[Nav] 点击导航:', page);
hideMore();
if (onNavigate) { onNavigate(page); return; }
const target = page === '/' ? '/' : (page.startsWith('/') ? page : '/' + page);
window.location.href = target;
};
wrapper.appendChild(sidebar);
wrapper.appendChild(mobileNav);
wrapper.appendChild(backdrop);
wrapper.appendChild(sheet);
// 绑定点击事件
wrapper.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const page = btn.getAttribute('data-page');
console.log('[Nav] 点击导航:', page);
if (onNavigate) {
onNavigate(page);
} else {
// 直接跳转 - 修复路径处理
let target;
if (page === '/' || page === '') {
target = '/';
} else if (page.startsWith('/')) {
target = page;
} else {
target = '/' + page;
}
window.location.href = target;
}
});
// 事件绑定
wrapper.querySelectorAll('.sidebar-btn').forEach(btn => {
btn.addEventListener('click', (e) => { e.preventDefault(); handleNavClick(btn); });
});
wrapper.querySelectorAll('.mobile-nav-btn').forEach(btn => {
if (btn.id === 'mobile-more-btn') {
btn.addEventListener('click', (e) => { e.preventDefault(); showMore(); });
} else {
btn.addEventListener('click', (e) => { e.preventDefault(); handleNavClick(btn); });
}
});
wrapper.querySelectorAll('.sheet-item').forEach(btn => {
btn.addEventListener('click', (e) => { e.preventDefault(); handleNavClick(btn); });
});
backdrop.addEventListener('click', hideMore);
return wrapper;
}
@@ -233,10 +284,18 @@
mobileItems: [
{ key: 'dashboard', label: '首页', icon: '📊', page: '/' },
{ key: 'topics', label: '选题', icon: '📋', page: 'topics.html' },
{ key: 'tasks', label: '任务', icon: '🚀', page: 'tasks.html' },
{ key: 'calendar', label: '日历', icon: '📅', page: 'calendar.html' },
{ key: 'assets', label: '素材', icon: '🖼️', page: 'assets.html' },
{ key: 'tasks', label: '任务', icon: '🚀', page: 'tasks.html' }
]
{ key: 'assets', label: '素材', icon: '🖼️', page: 'assets.html' }
],
sheetItems: [
{ key: 'metrics', label: '数据分析', icon: '📊', page: 'metrics.html' },
{ key: 'platforms', label: '平台配置', icon: '🌐', page: 'platforms.html' },
{ key: 'logs', label: '系统日志', icon: '📄', page: 'logs.html' },
{ key: 'users', label: '用户管理', icon: '👥', page: 'users.html', admin: true },
{ key: 'admin', label: '系统管理', icon: '⚙️', page: 'admin.html', admin: true }
],
showMoreSheet: false
};
},
template: `
@@ -259,7 +318,19 @@
<span class="nav-icon">{{ item.icon }}</span>
<span class="nav-text">{{ item.label }}</span>
</button>
<button class="mobile-nav-btn" @click="showMoreSheet = !showMoreSheet"><span class="nav-icon">⬆</span><span class="nav-text">更多</span></button>
</nav>
<div v-if="showMoreSheet" class="mobile-more-backdrop" @click="showMoreSheet = false"></div>
<div v-if="showMoreSheet" class="mobile-more-sheet">
<div class="sheet-handle"></div>
<div class="sheet-title">所有菜单</div>
<div class="sheet-grid">
<button v-for="item in sheetItems" :key="item.key" v-show="!item.admin || isAdmin" :class="['sheet-item', { active: currentPage === item.key }]" @click="navigate(item.page)">
<span class="item-icon">{{ item.icon }}</span>
<span>{{ item.label }}</span>
</button>
</div>
</div>
</div>
`,
methods: {
+6 -12
View File
@@ -5,20 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 平台配置</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
.navbar-title { font-size: 20px; font-weight: 600; }
.navbar-user { display: flex; align-items: center; gap: 16px; }
.user-info { display: flex; align-items: center; gap: 8px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; min-height: calc(100vh - 60px); }
.sidebar { width: 180px; background: white; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
@@ -50,8 +46,6 @@
.loading-state { text-align: center; padding: 60px 20px; color: #909399; font-size: 16px; }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
.card { padding: 16px; }
.platform-card { padding: 16px; }
@@ -79,7 +73,7 @@
<el-button @click="loadPlatforms">🔄 刷新</el-button>
</div>
</div>
<div class="card">
<div class="card page-fade">
<div v-if="loading" class="loading-state">加载中...</div>
<div v-else-if="platforms.length === 0" class="empty-state">
<div class="empty-state-icon">🌐</div>
-315
View File
@@ -1,315 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面渲染诊断</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.diagnostic-panel { margin: 15px 0; padding: 15px; border-radius: 6px; border-left: 4px solid #007bff; }
.success { background-color: #d4edda; border-color: #28a745; color: #155724; }
.warning { background-color: #fff3cd; border-color: #ffc107; color: #856404; }
.error { background-color: #f8d7da; border-color: #dc3545; color: #721c24; }
.info { background-color: #d1ecf1; border-color: #17a2b8; color: #0c5460; }
.test-btn { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }
.test-btn:hover { background: #0056b3; }
.test-btn:disabled { background: #6c757d; cursor: not-allowed; }
pre { background: #f8f9fa; padding: 10px; border-radius: 4px; overflow-x: auto; }
</style>
</head>
<body>
<div id="app">
<h1>宇之然内容创作平台 - 页面渲染诊断</h1>
<!-- 诊断控制面板 -->
<div class="diagnostic-panel info">
<h3>📋 诊断控制</h3>
<button @click="runBasicTest" :disabled="testing" class="test-btn">🔍 基础功能测试</button>
<button @click="runRenderTest" :disabled="testing" class="test-btn">🎨 渲染能力测试</button>
<button @click="runVueTest" :disabled="testing" class="test-btn">⚡ Vue核心测试</button>
<button @click="resetDiagnostic" class="test-btn">🔄 重置诊断</button>
<p v-if="testing">正在运行测试中...</p>
</div>
<!-- 实时输出 -->
<div class="diagnostic-panel" :class="{'success': output.length > 0 && lastResult === 'success', 'error': output.length > 0 && lastResult === 'error'}">
<h3>📊 实时输出</h3>
<div v-for="line in output" :key="line" style="margin: 5px 0;">{{ line }}</div>
</div>
<!-- 详细结果 -->
<div class="diagnostic-panel success" v-if="results.length > 0">
<h3>✅ 测试结果</h3>
<ul>
<li v-for="result in results" :key="result.id">{{ result.message }}</li>
</ul>
</div>
<!-- 问题分析 -->
<div class="diagnostic-panel warning" v-if="issues.length > 0">
<h3>⚠️ 发现的问题</h3>
<ul>
<li v-for="issue in issues" :key="issue">{{ issue }}</li>
</ul>
</div>
<!-- DOM结构检查 -->
<div class="diagnostic-panel info">
<h3>🏗️ DOM结构检查</h3>
<button @click="checkDOMStructure" class="test-btn">检查DOM结构</button>
<pre v-if="domInfo">{{ domInfo }}</pre>
</div>
<!-- 资源加载检查 -->
<div class="diagnostic-panel info">
<h3>🌐 资源加载检查</h3>
<button @click="checkResourceLoading" class="test-btn">检查资源加载</button>
<pre v-if="resourceInfo">{{ resourceInfo }}</pre>
</div>
<!-- 网络状态检查 -->
<div class="diagnostic-panel info">
<h3>📡 网络状态</h3>
<button @click="checkNetworkStatus" class="test-btn">检查网络状态</button>
<p v-if="networkInfo">{{ networkInfo }}</p>
</div>
<!-- 建议操作 -->
<div class="diagnostic-panel success">
<h3>💡 建议操作</h3>
<ol>
<li v-for="suggestion in suggestions" :key="suggestion">{{ suggestion }}</li>
</ol>
</div>
</div>
<script>
const RenderApp = {
data() {
return {
testing: false,
output: [
'页面渲染诊断工具已启动',
'请运行测试查看具体问题',
''
],
results: [],
issues: [],
lastResult: null,
domInfo: '',
resourceInfo: '',
networkInfo: '',
suggestions: []
}
},
methods: {
addOutput(message, type = 'info') {
this.output.push('[' + new Date().toLocaleTimeString() + '] ' + message);
if (type === 'success') this.lastResult = 'success';
if (type === 'error') this.lastResult = 'error';
},
runBasicTest() {
this.testing = true;
this.addOutput('开始基础功能测试...');
setTimeout(() => {
try {
// 测试基本DOM操作
const appElement = document.getElementById('app');
if (!appElement) {
throw new Error('找不到#app元素');
}
this.addOutput('✅ DOM元素检查通过', 'success');
this.results.push({ id: 'dom-element', message: 'DOM元素存在且可访问' });
// 测试Vue实例
if (window.Vue) {
this.addOutput('✅ Vue 3库已加载', 'success');
this.results.push({ id: 'vue-library', message: 'Vue 3库正确加载' });
} else {
throw new Error('Vue 3库未加载');
}
// 测试响应式数据
this.addOutput('✅ 响应式数据绑定正常', 'success');
this.results.push({ id: 'reactive-data', message: 'Vue响应式系统正常工作' });
this.testing = false;
} catch (error) {
this.addOutput('❌ 基础测试失败: ' + error.message, 'error');
this.issues.push('基础功能异常: ' + error.message);
this.testing = false;
}
}, 500);
},
runRenderTest() {
this.testing = true;
this.addOutput('开始渲染能力测试...');
setTimeout(() => {
try {
// 检查CSS样式
const styleElements = document.querySelectorAll('style, link[rel="stylesheet"]');
this.addOutput('✅ 发现 ' + styleElements.length + ' 个样式元素', 'success');
// 检查Tailwind
if (document.querySelector('script[src*="tailwindcss"]')) {
this.addOutput('✅ Tailwind CSS已加载', 'success');
this.results.push({ id: 'tailwind', message: 'Tailwind CSS样式框架正常' });
}
// 检查Vue渲染
this.addOutput('✅ Vue组件渲染测试通过', 'success');
this.results.push({ id: 'vue-render', message: 'Vue组件渲染功能正常' });
this.testing = false;
} catch (error) {
this.addOutput('❌ 渲染测试失败: ' + error.message, 'error');
this.issues.push('渲染功能异常: ' + error.message);
this.testing = false;
}
}, 500);
},
runVueTest() {
this.testing = true;
this.addOutput('开始Vue核心测试...');
setTimeout(() => {
try {
// 测试Vue应用实例
if (this.$data) {
this.addOutput('✅ Vue实例数据访问正常', 'success');
this.results.push({ id: 'vue-instance', message: 'Vue实例正确创建和挂载' });
}
// 测试事件处理
this.addOutput('✅ 事件处理器设置正常', 'success');
this.results.push({ id: 'event-handling', message: 'Vue事件监听器正常工作' });
// 测试计算属性
if (typeof this.countByStatus === 'function') {
this.addOutput('✅ 计算属性功能正常', 'success');
this.results.push({ id: 'computed-properties', message: 'Vue计算属性正常工作' });
}
this.testing = false;
} catch (error) {
this.addOutput('❌ Vue测试失败: ' + error.message, 'error');
this.issues.push('Vue功能异常: ' + error.message);
this.testing = false;
}
}, 500);
},
checkDOMStructure() {
this.addOutput('正在检查DOM结构...');
setTimeout(() => {
try {
const structure = {
'html标签': document.getElementsByTagName('html').length,
'head标签': document.getElementsByTagName('head').length,
'body标签': document.getElementsByTagName('body').length,
'#app元素': document.getElementById('app') ? '存在' : '不存在',
'Vue元素': document.querySelectorAll('[v-if], [v-for], [@click]').length,
'表格元素': document.querySelectorAll('table, th, td').length
};
this.domInfo = JSON.stringify(structure, null, 2);
this.addOutput('✅ DOM结构检查完成', 'success');
} catch (error) {
this.addOutput('❌ DOM检查失败: ' + error.message, 'error');
}
}, 200);
},
checkResourceLoading() {
this.addOutput('正在检查资源加载...');
setTimeout(() => {
try {
const resources = [];
// 检查脚本
document.querySelectorAll('script[src]').forEach(script => {
resources.push({
type: 'script',
src: script.src,
loaded: script.readyState === 'complete' || script.readyState === 'loaded'
});
});
// 检查样式表
document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
resources.push({
type: 'stylesheet',
href: link.href,
loaded: true // 简化处理
});
});
this.resourceInfo = JSON.stringify(resources.slice(0, 5), null, 2); // 只显示前5个
this.addOutput('✅ 资源加载检查完成', 'success');
} catch (error) {
this.addOutput('❌ 资源检查失败: ' + error.message, 'error');
}
}, 200);
},
checkNetworkStatus() {
this.addOutput('正在检查网络状态...');
setTimeout(() => {
try {
// 简化的网络状态检查
const status = {
online: navigator.onLine,
userAgent: navigator.userAgent,
connection: navigator.connection ? navigator.connection.effectiveType : 'unknown'
};
this.networkInfo = JSON.stringify(status, null, 2);
this.addOutput('✅ 网络状态检查完成', 'success');
} catch (error) {
this.addOutput('❌ 网络检查失败: ' + error.message, 'error');
}
}, 200);
},
resetDiagnostic() {
this.output = ['页面渲染诊断工具已启动', '请运行测试查看具体问题', ''];
this.results = [];
this.issues = [];
this.lastResult = null;
this.domInfo = '';
this.resourceInfo = '';
this.networkInfo = '';
this.suggestions = [];
this.addOutput('诊断已重置');
}
},
mounted() {
this.addOutput('Vue渲染诊断应用程序已启动');
console.log('Vue渲染诊断已初始化');
}
}
Vue.createApp(RenderApp).mount('#app')
</script>
</body>
</html>
-476
View File
@@ -1,476 +0,0 @@
<script>
const { ref, reactive, computed, onMounted, watch } = Vue;
const { ElMessage, ElNotification, ElMessageBox } = ElementPlus;
// 图标组件
const CopyDocument = Vue.h('el-icon', { name: 'CopyDocument' });
const FullScreen = Vue.h('el-icon', { name: 'FullScreen' });
const Document = Vue.h('el-icon', { name: 'Document' });
const Upload = Vue.h('el-icon', { name: 'Upload' });
const Promotion = Vue.h('el-icon', { name: 'Promotion' });
const app = Vue.createApp({
name: 'YuZhiRanPlatform',
setup() {
// ========== 变量声明区 ==========
const API_BASE = window.location.origin;
// 状态
const isLoggedIn = ref(false);
const isAdmin = ref(false);
const loginForm = reactive({ username: '', password: '' });
const loginError = ref('');
const status = ref({});
const topics = ref([]);
const selectedTopicIds = ref([]); // 批量操作选中
const filterStatus = ref('');
const generating = ref(false);
const optimizing = ref(false);
const loadingAll = ref(false);
const loadingTable = ref(false);
const loadingLogs = ref(false);
const loadingOverlay = ref(false);
const loadingText = ref('');
const pipeline = ref({ status_distribution: {} });
const pipelineLoading = ref(false);
const pipelineModules = ref([]);
const previewVisible = ref(false);
const previewTopic = ref({ title: '' });
const previewPlatform = ref('zhihu');
const previewHtml = ref('');
const fullScreenPreview = ref(false);
const showLogs = ref(false);
const logType = ref('creator');
const logDate = ref(new Date().toISOString().split('T')[0]);
const logContent = ref('');
// 计算属性
const filteredTopics = computed(() => {
if (!filterStatus.value) return topics.value || [];
return (topics.value || []).filter(t => t && t.status === filterStatus.value);
});
// ========== 工具函数 ==========
const formatDate = (val) => {
if (!val) return '-';
const d = new Date(val);
if (isNaN(d.getTime())) return val;
return d.toLocaleString('zh-CN', { hour12: false });
};
const formatRelativeTime = (val) => {
if (!val) return '-';
const d = new Date(val);
if (isNaN(d.getTime())) return '-';
const now = new Date();
const diff = now - d;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}天前`;
return formatDate(val);
};
// ========== 业务方法 ==========
const countByStatus = (status) => {
return (topics.value || []).filter(t => t.status === status).length;
};
const getPriorityType = (score) => {
if (!score) return '';
if (score >= 20) return 'danger';
if (score >= 15) return 'warning';
return 'success';
};
const getStatusClass = (status) => {
const map = {
'待处理': 'pending',
'待审查': 'review',
'待发布': 'ready',
'已发布': 'published'
};
return map[status] || '';
};
const refresh = async () => {
try {
const [s, t] = await Promise.all([
fetch(API_BASE + '/api/system/status').then(r => r.json()),
fetch(API_BASE + '/api/topics').then(r => r.json())
]);
status.value = s;
topics.value = t;
} catch (e) {
ElMessage.error('刷新失败:' + e.message);
}
};
const refreshPipeline = async () => {
pipelineLoading.value = true;
try {
const res = await fetch(API_BASE + '/api/system/pipeline/status');
if (res.ok) {
const data = await res.json();
pipeline.value = data;
pipelineModules.value = Object.entries(data.pipeline_modules || {}).map(([name, info]) => ({
module: name,
last_run: info.last_run || '未运行',
status_ok: !info.has_error && info.exists,
status_text: info.exists && !info.has_error ? '正常' : info.exists ? '有错误' : '缺失',
error: info.has_error ? '检测到错误' : ''
}));
}
} catch (e) {
ElMessage.error('获取流水线状态失败');
} finally {
pipelineLoading.value = false;
}
};
const refreshAll = async () => {
loadingAll.value = true;
try {
await Promise.all([refresh(), refreshPipeline()]);
ElMessage.success('刷新成功');
} catch (e) {
ElMessage.error('刷新失败');
} finally {
loadingAll.value = false;
}
};
const triggerGenerate = async () => {
generating.value = true;
try {
const res = await fetch(API_BASE + '/api/system/generate/run', { method: 'POST' });
const data = await res.json();
if (data.result && data.result.ok) {
ElMessage.success('创作任务已启动');
setTimeout(refresh, 3000);
} else {
ElMessage.error('启动失败:' + (data.error || '未知错误'));
}
} catch (e) {
ElMessage.error('请求失败:' + e.message);
} finally {
generating.value = false;
}
};
const triggerOptimize = async () => {
optimizing.value = true;
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', { method: 'POST' });
const data = await res.json();
if (data.summary) {
ElNotification({
title: '优化完成',
message: `自动通过 ${data.summary.passed_auto || 0} 篇,需人工 ${data.summary.need_manual || 0}`,
type: 'success'
});
await refresh();
} else {
ElMessage.success('优化完成');
}
} catch (e) {
ElMessage.error('优化失败:' + e.message);
} finally {
optimizing.value = false;
}
};
const openPreview = async (topic) => {
previewTopic.value = { id: topic.id, title: topic.title };
previewPlatform.value = 'zhihu';
previewVisible.value = true;
await loadPreview();
};
const loadPreview = async () => {
previewHtml.value = '';
console.log('[Preview] Loading topic:', previewTopic.value.id, 'platform:', previewPlatform.value);
try {
const res = await fetch(`${API_BASE}/api/articles/${previewTopic.value.id}/preview?platform=${previewPlatform.value}`);
console.log('[Preview] Response status:', res.status);
if (res.ok) {
const data = await res.json();
console.log('[Preview] Got HTML, length:', data.html?.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data.html, 'text/html');
const contentDiv = doc.querySelector('.content');
console.log('[Preview] Found .content:', !!contentDiv);
if (contentDiv) {
previewHtml.value = contentDiv.innerHTML;
console.log('[Preview] Set previewHtml from .content');
} else {
const header = doc.querySelector('.header');
const footer = doc.querySelector('footer');
const tags = doc.querySelector('.tags');
const interaction = doc.querySelector('.interaction');
if (header) header.remove();
if (footer) footer.remove();
if (tags) tags.remove();
if (interaction) interaction.remove();
previewHtml.value = doc.body.innerHTML;
console.log('[Preview] Set previewHtml from body.innerHTML');
}
} else if (res.status === 404) {
previewHtml.value = '<div class="text-center py-12 text-gray-500"><p>暂未创作文章,请先点击创作按钮生成</p></div>';
} else {
ElMessage.error('加载预览失败:' + res.status);
}
} catch (e) {
previewHtml.value = '<div class="text-center py-12 text-gray-500"><p>请求失败,请检查后端服务是否运行</p></div>';
console.error('Preview error:', e);
}
};
const copyPreviewHtml = async () => {
if (!previewHtml.value) return;
try {
const parser = new DOMParser();
const doc = parser.parseFromString(previewHtml.value, 'text/html');
const header = doc.querySelector('.header');
if (header) header.remove();
const footer = doc.querySelector('footer');
if (footer) footer.remove();
const tagsDiv = doc.querySelector('.tags');
if (tagsDiv) tagsDiv.remove();
const interaction = doc.querySelector('.interaction');
if (interaction) interaction.remove();
const contentDiv = doc.querySelector('.content');
let text = '';
if (contentDiv) {
text = contentDiv.innerText.trim();
} else {
text = doc.body.innerText.trim();
}
if (!text) {
ElMessage.warning('未提取到正文内容');
return;
}
await navigator.clipboard.writeText(text);
ElMessage.success('正文已复制到剪贴板');
} catch (e) {
console.error('Copy error:', e);
ElMessage.error('复制失败');
}
};
const expandPreview = () => {
fullScreenPreview.value = true;
};
const handleShowLogs = () => {
showLogs.value = true;
};
const fetchLogs = async () => {
loadingLogs.value = true;
try {
const res = await fetch(`${API_BASE}/api/system/logs/${logDate.value}?log_type=${logType.value}`);
if (res.ok) {
const data = await res.json();
logContent.value = data.content ? data.content.join('\n') : '无内容';
} else {
ElMessage.error('加载日志失败');
}
} catch (e) {
ElMessage.error('请求失败');
} finally {
loadingLogs.value = false;
}
};
const createTopic = async (topic) => {
if (topic.published_urls && Object.keys(topic.published_urls).length > 0) {
try {
await ElMessageBox.alert(
'本文已发布过,重新创作将覆盖原有内容。是否继续?',
'重新创作确认',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning',
}
);
} catch (e) {
return;
}
}
try {
const res = await fetch(`${API_BASE}/api/system/generate/run?topic_id=${topic.id}`, { method: 'POST' });
const data = await res.json();
if (data.result && data.result.ok) {
ElMessage.success(`选题 ${topic.id} 创作任务已启动`);
setTimeout(refresh, 3000);
} else {
ElMessage.error('创作失败:' + (data.error || '未知错误'));
}
} catch (e) {
ElMessage.error('请求失败:' + e.message);
}
};
const optimizeTopic = async (topic) => {
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_ids: [topic.id] })
});
const data = await res.json();
if (data.summary) {
ElMessage.success(`选题 ${topic.id} 优化完成`);
setTimeout(refresh, 2000);
} else {
ElMessage.success('优化完成');
}
} catch (e) {
ElMessage.error('优化失败:' + e.message);
}
};
// 批量操作
const triggerGenerateSelected = async () => {
if (selectedTopicIds.value.length === 0) {
ElMessage.warning('请先选择要创作的选题');
return;
}
generating.value = true;
try {
const res = await fetch(API_BASE + '/api/system/generate/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_ids: selectedTopicIds.value })
});
const data = await res.json();
if (data.result && data.result.ok) {
ElMessage.success(`已启动 ${selectedTopicIds.value.length} 个选题的创作任务`);
selectedTopicIds.value = [];
setTimeout(refresh, 3000);
} else {
ElMessage.error('批量创作失败:' + (data.error || '未知错误'));
}
} catch (e) {
ElMessage.error('请求失败:' + e.message);
} finally {
generating.value = false;
}
};
const triggerOptimizeSelected = async () => {
if (selectedTopicIds.value.length === 0) {
ElMessage.warning('请先选择要优化的选题');
return;
}
optimizing.value = true;
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_ids: selectedTopicIds.value })
});
const data = await res.json();
if (data.summary) {
ElNotification({
title: '批量优化完成',
message: `自动通过 ${data.summary.passed_auto || 0} 篇,需人工 ${data.summary.need_manual || 0}`,
type: 'success'
});
selectedTopicIds.value = [];
await refresh();
} else {
ElMessage.success('批量优化完成');
}
} catch (e) {
ElMessage.error('批量优化失败:' + e.message);
} finally {
optimizing.value = false;
}
};
const handlePublish = async (topic) => {
try {
ElMessage.info(`正在发布选题 ${topic.id}...`);
const res = await fetch(API_BASE + '/api/publishing/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_id: topic.id })
});
if (!res.ok) throw new Error('发布失败');
const data = await res.json();
ElMessage.success(`选题 ${topic.id} 已发布`);
await refresh();
} catch (e) {
ElMessage.error('发布失败:' + e.message);
}
};
const openCreateTopic = () => {
ElMessage.info('新建选题功能待实现');
};
// 页面路由
const currentPage = ref('overview');
const switchPage = (page) => {
currentPage.value = page;
};
const goToTopicsWithFilter = (status) => {
currentPage.value = 'topics';
filterStatus.value = status;
};
// 生命周期
watch(previewPlatform, loadPreview);
onMounted(() => {
const authToken = localStorage.getItem('auth_token');
const role = localStorage.getItem('user_role');
if (authToken) {
isLoggedIn.value = true;
if (role === 'admin') isAdmin.value = true;
}
refresh();
refreshPipeline();
});
// 返回给模板
return {
// 状态
status, topics, filterStatus, filteredTopics,
generating, optimizing, loadingAll, loadingTable, loadingLogs, loadingOverlay, loadingText,
pipeline, pipelineLoading, pipelineModules,
previewVisible, previewTopic, previewPlatform, previewHtml, fullScreenPreview,
showLogs, logType, logDate, logContent,
// 页面路由
currentPage,
// 方法
countByStatus, getPriorityType, getStatusClass,
refresh, refreshPipeline, refreshAll,
triggerGenerate, triggerOptimize,
openPreview, loadPreview, copyPreviewHtml, expandPreview,
fetchLogs,
createTopic, optimizeTopic, handlePublish,
openCreateTopic,
// 工具函数
formatDate, formatRelativeTime,
switchPage, goToTopicsWithFilter,
// 认证(未完整)
isLoggedIn, isAdmin, loginForm, loginError,
// 图标
Document, Upload, CopyDocument, FullScreen, Promotion
};
}
});
app.use(ElementPlus);
app.mount('#app');
</script>
-398
View File
@@ -1,398 +0,0 @@
// 修复后的 Vue 3 setup 函数体
// 所有变量和方法必须在 return 之前定义
const API_BASE = window.location.origin;
// 1. 状态变量
const isLoggedIn = ref(false);
const isAdmin = ref(false);
const loginForm = reactive({ username: '', password: '' });
const loginError = ref('');
const status = ref({});
const topics = ref([]);
const filterStatus = ref('');
const generating = ref(false);
const optimizing = ref(false);
const loadingAll = ref(false);
const loadingTable = ref(false);
const loadingLogs = ref(false);
const loadingOverlay = ref(false);
const loadingText = ref('');
const pipeline = ref({ status_distribution: {} });
const pipelineLoading = ref(false);
const pipelineModules = ref([]);
const previewVisible = ref(false);
const previewTopic = ref({ title: '' });
const previewPlatform = ref('zhihu');
const previewHtml = ref('');
const fullScreenPreview = ref(false);
const showLogs = ref(false);
const logType = ref('creator');
const logDate = ref(new Date().toISOString().split('T')[0]);
const logContent = ref('');
// 2. 计算属性
const filteredTopics = computed(() => {
if (!filterStatus.value) return topics.value || [];
return (topics.value || []).filter(t => t && t.status === filterStatus.value);
});
// 3. 工具函数
const formatDate = (val) => {
if (!val) return '-';
const d = new Date(val);
if (isNaN(d.getTime())) return val;
return d.toLocaleString('zh-CN', { hour12: false });
};
const formatRelativeTime = (val) => {
if (!val) return '-';
const d = new Date(val);
if (isNaN(d.getTime())) return '-';
const now = new Date();
const diff = now - d;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}天前`;
return formatDate(val);
};
// 4. 业务方法
const countByStatus = (status) => {
return (topics.value || []).filter(t => t.status === status).length;
};
const getPriorityType = (score) => {
if (!score) return '';
if (score >= 20) return 'danger';
if (score >= 15) return 'warning';
return 'success';
};
const getStatusClass = (status) => {
const map = {
'待处理': 'pending',
'待审查': 'review',
'待发布': 'ready',
'已发布': 'published'
};
return map[status] || '';
};
const refresh = async () => {
try {
const [s, t] = await Promise.all([
fetch(API_BASE + '/api/system/status').then(r => r.json()),
fetch(API_BASE + '/api/topics').then(r => r.json())
]);
status.value = s;
topics.value = t;
} catch (e) {
ElMessage.error('刷新失败:' + e.message);
}
};
const refreshPipeline = async () => {
pipelineLoading.value = true;
try {
const res = await fetch(API_BASE + '/api/system/pipeline/status');
if (res.ok) {
const data = await res.json();
pipeline.value = data;
pipelineModules.value = Object.entries(data.pipeline_modules || {}).map(([name, info]) => ({
module: name,
last_run: info.last_run || '未运行',
status_ok: !info.has_error && info.exists,
status_text: info.exists && !info.has_error ? '正常' : info.exists ? '有错误' : '缺失',
error: info.has_error ? '检测到错误' : ''
}));
}
} catch (e) {
ElMessage.error('获取流水线状态失败');
} finally {
pipelineLoading.value = false;
}
};
const refreshAll = async () => {
loadingAll.value = true;
try {
await Promise.all([refresh(), refreshPipeline()]);
ElMessage.success('刷新成功');
} catch (e) {
ElMessage.error('刷新失败');
} finally {
loadingAll.value = false;
}
};
const triggerGenerate = async () => {
generating.value = true;
try {
const res = await fetch(API_BASE + '/api/system/generate/run', { method: 'POST' });
const data = await res.json();
if (data.result && data.result.ok) {
ElMessage.success('创作任务已启动');
setTimeout(refresh, 3000);
} else {
ElMessage.error('启动失败:' + (data.error || '未知错误'));
}
} catch (e) {
ElMessage.error('请求失败:' + e.message);
} finally {
generating.value = false;
}
};
const triggerOptimize = async () => {
optimizing.value = true;
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', { method: 'POST' });
const data = await res.json();
if (data.summary) {
ElNotification({
title: '优化完成',
message: `自动通过 ${data.summary.passed_auto || 0} 篇,需人工 ${data.summary.need_manual || 0}`,
type: 'success'
});
await refresh();
} else {
ElMessage.success('优化完成');
}
} catch (e) {
ElMessage.error('优化失败:' + e.message);
} finally {
optimizing.value = false;
}
};
const openPreview = async (topic) => {
previewTopic.value = { id: topic.id, title: topic.title };
previewPlatform.value = 'zhihu';
previewVisible.value = true;
await loadPreview();
};
const loadPreview = async () => {
previewHtml.value = '';
console.log('[Preview] Loading topic:', previewTopic.value.id, 'platform:', previewPlatform.value);
try {
const res = await fetch(`${API_BASE}/api/articles/${previewTopic.value.id}/preview?platform=${previewPlatform.value}`);
console.log('[Preview] Response status:', res.status);
if (res.ok) {
const data = await res.json();
console.log('[Preview] Got HTML, length:', data.html?.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data.html, 'text/html');
const contentDiv = doc.querySelector('.content');
console.log('[Preview] Found .content:', !!contentDiv);
if (contentDiv) {
previewHtml.value = contentDiv.innerHTML;
console.log('[Preview] Set previewHtml from .content');
} else {
const header = doc.querySelector('.header');
const footer = doc.querySelector('footer');
const tags = doc.querySelector('.tags');
const interaction = doc.querySelector('.interaction');
if (header) header.remove();
if (footer) footer.remove();
if (tags) tags.remove();
if (interaction) interaction.remove();
previewHtml.value = doc.body.innerHTML;
console.log('[Preview] Set previewHtml from body.innerHTML');
}
} else if (res.status === 404) {
previewHtml.value = '<div class="text-center py-12 text-gray-500"><p>暂未创作文章,请先点击创作按钮生成</p></div>';
} else {
ElMessage.error('加载预览失败:' + res.status);
}
} catch (e) {
previewHtml.value = '<div class="text-center py-12 text-gray-500"><p>请求失败,请检查后端服务是否运行</p></div>';
console.error('Preview error:', e);
}
};
const copyPreviewHtml = async () => {
if (!previewHtml.value) return;
try {
const parser = new DOMParser();
const doc = parser.parseFromString(previewHtml.value, 'text/html');
const header = doc.querySelector('.header');
if (header) header.remove();
const footer = doc.querySelector('footer');
if (footer) footer.remove();
const tagsDiv = doc.querySelector('.tags');
if (tagsDiv) tagsDiv.remove();
const interaction = doc.querySelector('.interaction');
if (interaction) interaction.remove();
const contentDiv = doc.querySelector('.content');
let text = '';
if (contentDiv) {
text = contentDiv.innerText.trim();
} else {
text = doc.body.innerText.trim();
}
if (!text) {
ElMessage.warning('未提取到正文内容');
return;
}
await navigator.clipboard.writeText(text);
ElMessage.success('正文已复制到剪贴板');
} catch (e) {
console.error('Copy error:', e);
ElMessage.error('复制失败');
}
};
const expandPreview = () => {
fullScreenPreview.value = true;
};
const handleShowLogs = () => {
showLogs.value = true;
};
const fetchLogs = async () => {
loadingLogs.value = true;
try {
const res = await fetch(`${API_BASE}/api/system/logs/${logDate.value}?log_type=${logType.value}`);
if (res.ok) {
const data = await res.json();
logContent.value = data.content ? data.content.join('\n') : '无内容';
} else {
ElMessage.error('加载日志失败');
}
} catch (e) {
ElMessage.error('请求失败');
} finally {
loadingLogs.value = false;
}
};
const createTopic = async (topic) => {
if (topic.published_urls && Object.keys(topic.published_urls).length > 0) {
try {
await ElMessageBox.alert(
'本文已发布过,重新创作将覆盖原有内容。是否继续?',
'重新创作确认',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning',
}
);
} catch (e) {
return;
}
}
try {
const res = await fetch(`${API_BASE}/api/system/generate/run?topic_id=${topic.id}`, { method: 'POST' });
const data = await res.json();
if (data.result && data.result.ok) {
ElMessage.success(`选题 ${topic.id} 创作任务已启动`);
setTimeout(refresh, 3000);
} else {
ElMessage.error('创作失败:' + (data.error || '未知错误'));
}
} catch (e) {
ElMessage.error('请求失败:' + e.message);
}
};
const optimizeTopic = async (topic) => {
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_ids: [topic.id] })
});
const data = await res.json();
if (data.summary) {
ElMessage.success(`选题 ${topic.id} 优化完成`);
setTimeout(refresh, 2000);
} else {
ElMessage.success('优化完成');
}
} catch (e) {
ElMessage.error('优化失败:' + e.message);
}
};
const handlePublish = async (topic) => {
try {
ElMessage.info(`正在发布选题 ${topic.id}...`);
const res = await fetch(API_BASE + '/api/publishing/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_id: topic.id })
});
if (!res.ok) throw new Error('发布失败');
const data = await res.json();
ElMessage.success(`选题 ${topic.id} 已发布`);
await refresh();
} catch (e) {
ElMessage.error('发布失败:' + e.message);
}
};
const openCreateTopic = () => {
ElMessage.info('新建选题功能待实现');
};
// 5. 页面路由
const currentPage = ref('overview');
const switchPage = (page) => {
currentPage.value = page;
};
const goToTopicsWithFilter = (status) => {
currentPage.value = 'topics';
filterStatus.value = status;
};
// 6. 生命周期(必须在 return 之前)
watch(previewPlatform, loadPreview);
onMounted(() => {
const authToken = localStorage.getItem('auth_token');
const role = localStorage.getItem('user_role');
if (authToken) {
isLoggedIn.value = true;
if (role === 'admin') isAdmin.value = true;
}
refresh();
refreshPipeline();
});
// 7. 返回给模板
return {
// 状态
status, topics, filterStatus, filteredTopics,
generating, optimizing, loadingAll, loadingTable, loadingLogs, loadingOverlay, loadingText,
pipeline, pipelineLoading, pipelineModules,
previewVisible, previewTopic, previewPlatform, previewHtml, fullScreenPreview,
showLogs, logType, logDate, logContent,
// 页面路由
currentPage,
// 方法
countByStatus, getPriorityType, getStatusClass,
refresh, refreshPipeline, refreshAll,
triggerGenerate, triggerOptimize,
openPreview, loadPreview, copyPreviewHtml, expandPreview,
fetchLogs,
createTopic, optimizeTopic, handlePublish,
openCreateTopic,
// 工具函数
formatDate, formatRelativeTime,
switchPage, goToTopicsWithFilter,
// 认证(未完整)
isLoggedIn, isAdmin, loginForm, loginError,
// 图标
Document, Upload, CopyDocument, FullScreen, Promotion
};
-96
View File
@@ -1,96 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue最简单测试</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.test-result { margin: 10px 0; padding: 10px; border-radius: 4px; }
.success { background-color: #d4edda; color: #155724; }
.error { background-color: #f8d7da; color: #721c24; }
.info { background-color: #d1ecf1; color: #0c5460; }
</style>
</head>
<body>
<div id="app">
<h1>{{ title }}</h1>
<!-- 基础功能测试 -->
<div class="test-result info">
<strong>基础测试:</strong>
<p>当前计数: {{ count }}</p>
<button @click="count++">增加计数</button>
</div>
<!-- Vue初始化状态 -->
<div class="test-result" :class="{'success': vueReady, 'error': !vueReady}">
<strong>Vue状态:</strong>
<p v-if="vueReady">✅ Vue已就绪</p>
<p v-if="!vueReady">❌ Vue未就绪</p>
</div>
<!-- 调试信息 -->
<div class="test-result info">
<strong>调试信息:</strong>
<ul>
<li v-for="log in debugLogs" :key="log">{{ log }}</li>
</ul>
</div>
</div>
<script>
const SimpleApp = {
data() {
return {
title: "Vue最简测试",
count: 0,
vueReady: false,
debugLogs: []
}
},
methods: {
addLog(message) {
this.debugLogs.push('[' + new Date().toLocaleTimeString() + '] ' + message);
}
},
mounted() {
this.addLog('Vue应用已启动');
// 检查Vue是否正确初始化
try {
console.log('Vue实例:', this);
console.log('数据对象:', this.$data);
// 测试基本响应式
setTimeout(() => {
this.vueReady = true;
this.addLog('✅ Vue响应式系统正常工作');
// 测试事件处理
this.addLog('✅ 事件监听器已设置');
// 测试数据绑定
this.addLog('✅ 文本插值正常工作');
}, 100);
} catch (error) {
this.vueReady = false;
this.addLog('❌ Vue初始化失败: ' + error.message);
}
}
}
try {
Vue.createApp(SimpleApp).mount('#app');
console.log('Vue应用程序已成功创建和挂载');
} catch (error) {
console.error('Vue应用程序创建失败:', error);
}
</script>
</body>
</html>
+1 -1
View File
@@ -1,5 +1,5 @@
// Service Worker for 宇之然内容创作平台
const CACHE_NAME = 'yuzhiran-v1';
const CACHE_NAME = 'yuzhiran-v2';
const CACHE_URLS = [
'/',
'/index.html',
+98 -37
View File
@@ -5,19 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 创作任务</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
.navbar-title { font-size: 20px; font-weight: 600; }
.navbar-user { display: flex; align-items: center; gap: 16px; }
.user-info { display: flex; align-items: center; gap: 8px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; min-height: calc(100vh - 60px); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
@@ -44,12 +40,28 @@
.error-box { margin-top: 8px; padding: 8px 12px; background: #fef0f0; border-radius: 6px; font-size: 13px; color: #f56c6c; border-left: 3px solid #f56c6c; }
.task-card-list-mobile { display: none; }
.schedule-row { display: flex; align-items: center; gap: 16px; padding: 14px 16px; border-radius: 10px; background: #f8faff; border: 1px solid #e8edf5; margin-bottom: 8px; transition: all 0.2s; }
.schedule-row:hover { background: #f0f4ff; }
.schedule-icon { font-size: 22px; width: 36px; text-align: center; flex-shrink: 0; }
.schedule-info { flex: 1; min-width: 0; }
.schedule-name { font-size: 14px; font-weight: 600; color: #303133; }
.schedule-time { font-size: 12px; color: #909399; margin-top: 2px; }
.schedule-next { font-size: 12px; color: #409eff; white-space: nowrap; }
.schedule-badge { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.schedule-badge.active { background: #67c23a; }
.schedule-badge.inactive { background: #c0c4cc; }
.detail-row { display: flex; padding: 10px 0; border-bottom: 1px solid #f0f2f5; }
.detail-row:last-child { border-bottom: none; }
.detail-label { width: 80px; flex-shrink: 0; font-size: 13px; color: #909399; }
.detail-value { flex: 1; font-size: 13px; color: #303133; word-break: break-all; }
@media (max-width: 768px) {
.content-area { padding: 16px; padding-bottom: 80px; }
.card { padding: 16px; }
.task-table { display: none; }
.task-card-list-mobile { display: block; }
.schedule-row { padding: 10px 12px; gap: 10px; flex-wrap: wrap; }
.schedule-next { width: 100%; margin-left: 46px; }
}
</style>
<script src="navigation-component.js"></script>
@@ -61,7 +73,32 @@
<navigation-component current-page="tasks" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">🚀 定时任务</h2>
<div class="toolbar">
<el-button @click="loadSchedulerStatus">🔄 刷新</el-button>
</div>
</div>
<div v-if="schedulerLoading" style="text-align: center; padding: 20px; color: #909399;">加载中...</div>
<div v-else>
<div v-if="schedulerJobs.length === 0" style="text-align: center; padding: 30px 20px; color: #909399; font-size: 14px;">暂无定时任务</div>
<div v-for="job in schedulerJobs" :key="job.id" class="schedule-row">
<div class="schedule-icon">{{ job.icon }}</div>
<div class="schedule-info">
<div class="schedule-name">{{ job.name }}</div>
<div class="schedule-time">⏰ 每日 {{ job.time }}</div>
</div>
<div class="schedule-next">
<span class="schedule-badge" :class="job.active ? 'active' : 'inactive'"></span>
<span>{{ job.active ? '运行中' : '等待中' }}</span>
<span v-if="job.next_run" style="margin-left: 8px; color: #909399;">下次: {{ job.next_run }}</span>
</div>
</div>
</div>
</div>
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">🚀 创作任务</h2>
<div class="toolbar">
@@ -114,27 +151,23 @@
</div>
</main>
</div>
<el-dialog v-model="showDetailDialog" title="任务详情" width="600px">
<el-dialog v-model="showDetailDialog" title="任务详情" width="600px" class="task-detail-dialog">
<div v-if="detailTask">
<el-descriptions :column="2" border>
<el-descriptions-item label="任务ID">{{ detailTask.task_id }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ getStatusLabel(detailTask.status) }}</el-descriptions-item>
<el-descriptions-item label="阶段">{{ getStageLabel(detailTask.stage) }}</el-descriptions-item>
<el-descriptions-item label="进度">{{ detailTask.progress }}%</el-descriptions-item>
<el-descriptions-item label="选题ID" :span="2">{{ detailTask.topic_id || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(detailTask.created_at) }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ formatDate(detailTask.started_at) }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ formatDate(detailTask.finished_at) }}</el-descriptions-item>
<el-descriptions-item label="耗时">{{ detailTask.duration ? detailTask.duration + '秒' : '-' }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ detailTask.created_by }}</el-descriptions-item>
<el-descriptions-item label="消息" :span="2">{{ detailTask.message || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="detailTask.error_msg" label="错误信息" :span="2">
<span style="color: #f56c6c;">{{ detailTask.error_msg }}</span>
</el-descriptions-item>
</el-descriptions>
<div class="detail-row"><span class="detail-label">任务ID</span><span class="detail-value">{{ detailTask.task_id }}</span></div>
<div class="detail-row"><span class="detail-label">状态</span><span class="detail-value"><span class="status-badge" :class="'status-' + detailTask.status">{{ getStatusLabel(detailTask.status) }}</span></span></div>
<div class="detail-row"><span class="detail-label">阶段</span><span class="detail-value"><span class="task-stage" :class="'stage-' + detailTask.stage">{{ getStageLabel(detailTask.stage) }}</span></span></div>
<div class="detail-row"><span class="detail-label">进度</span><span class="detail-value">{{ detailTask.progress }}%</span></div>
<div class="detail-row"><span class="detail-label">选题ID</span><span class="detail-value">{{ detailTask.topic_id || '-' }}</span></div>
<div class="detail-row"><span class="detail-label">创建时间</span><span class="detail-value">{{ formatDate(detailTask.created_at) }}</span></div>
<div class="detail-row"><span class="detail-label">开始时间</span><span class="detail-value">{{ formatDate(detailTask.started_at) }}</span></div>
<div class="detail-row"><span class="detail-label">完成时间</span><span class="detail-value">{{ formatDate(detailTask.finished_at) }}</span></div>
<div class="detail-row"><span class="detail-label">耗时</span><span class="detail-value">{{ detailTask.duration ? detailTask.duration + '秒' : '-' }}</span></div>
<div class="detail-row"><span class="detail-label">创建人</span><span class="detail-value">{{ detailTask.created_by }}</span></div>
<div class="detail-row"><span class="detail-label">消息</span><span class="detail-value">{{ detailTask.message || '-' }}</span></div>
<div v-if="detailTask.error_msg" class="detail-row"><span class="detail-label">错误信息</span><span class="detail-value" style="color: #f56c6c;">{{ detailTask.error_msg }}</span></div>
<div v-if="detailTask.result_data" style="margin-top: 16px;">
<div style="font-weight: 600; margin-bottom: 8px;">结果数据:</div>
<pre style="background: #f5f7fa; padding: 12px; border-radius: 8px; overflow: auto; max-height: 300px; font-size: 13px;">{{ JSON.stringify(detailTask.result_data, null, 2) }}</pre>
<div style="font-weight: 600; margin-bottom: 8px; font-size: 13px;">结果数据:</div>
<pre style="background: #f5f7fa; padding: 12px; border-radius: 8px; overflow: auto; max-height: 200px; font-size: 12px;">{{ JSON.stringify(detailTask.result_data, null, 2) }}</pre>
</div>
</div>
</el-dialog>
@@ -143,13 +176,20 @@
<script src="element-plus.full.js"></script>
<script>
const TasksApp = {
data() {
return {
currentUser: { username: '' }, isAdmin: false, isLoggedIn: false,
tasks: [], loading: false, filterStatus: '',
showDetailDialog: false, detailTask: null
}
},
data() {
const SCHEDULER_JOBS = {
'scheduled_sync': { icon: '🔄', name: '数据同步', defaultTime: '02:30' },
'scheduled_generate': { icon: '🤖', name: '内容创作', defaultTime: '03:30' },
'scheduled_optimize': { icon: '🔍', name: '合规审查', defaultTime: '04:30' },
};
return {
currentUser: { username: '' }, isAdmin: false, isLoggedIn: false,
tasks: [], loading: false, filterStatus: '',
showDetailDialog: false, detailTask: null,
schedulerJobs: [], schedulerLoading: false,
SCHEDULER_JOBS,
}
},
methods: {
getToken() { return localStorage.getItem('authToken'); },
async api(url, opts = {}) {
@@ -166,11 +206,32 @@ const TasksApp = {
if (!token) { window.location.href = '/login.html'; return; }
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; this.loadTasks(); })
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; this.loadTasks(); this.loadSchedulerStatus(); })
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
},
async loadSchedulerStatus() {
this.schedulerLoading = true;
try {
const data = await this.api('/api/system/scheduler/status');
if (!data) return;
this.schedulerJobs = (data.jobs || []).map(job => {
const info = this.SCHEDULER_JOBS[job.id] || { icon: '⏰', name: job.id, defaultTime: '' };
// Parse cron trigger for time display
let time = info.defaultTime;
const m = job.trigger && job.trigger.match(/hour='?(\d+)'?,\s*minute='?(\d+)'?/);
if (m) time = m[1].padStart(2,'0') + ':' + m[2].padStart(2,'0');
// Format next_run_time
let next_run = '';
if (job.next_run_time) {
try { next_run = new Date(job.next_run_time).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } catch(e) { next_run = job.next_run_time; }
}
return { id: job.id, icon: info.icon, name: info.name, time, active: data.running, next_run };
});
} catch (e) { console.error(e); }
finally { this.schedulerLoading = false; }
},
getStatusLabel(status) { return { 'pending': '等待中', 'running': '进行中', 'completed': '已完成', 'failed': '失败', 'cancelled': '已取消' }[status] || status; },
getStageLabel(stage) { return { 'creator': '创作', 'optimize': '优化', 'review': '审查', 'publish': '发布' }[stage] || stage; },
getStageLabel(stage) { return { 'creator': '创作', 'optimize': '审查', 'review': '审查', 'publish': '发布' }[stage] || stage; },
formatDate(dateStr) {
if (!dateStr) return '-';
try { return new Date(dateStr).toLocaleString('zh-CN'); } catch (e) { return dateStr; }
-90
View File
@@ -1,90 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>前端测试</title>
<script src="vue.global.prod.js"></script>
<link rel="stylesheet" href="element-plus.css" />
<!-- Tailwind CSS -->
<!-- Vue 3 -->
<!-- Element Plus CSS -->
<!-- Element Plus JS -->
<style>
/* 基础重置 */
body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
/* 卡片组件 */
.card { background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 24px; margin-bottom: 24px; transition: all 0.3s; }
.card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
/* 侧边栏 */
.sidebar { width: 160px; position: fixed; height: 100vh; left: 0; top: 0; background: #f5f5f5; border-right: 1px solid #e0e0e0; }
/* 主内容区 */
.main-content { margin-left: 160px; width: calc(100vw - 160px); min-height: 100vh; overflow-x: auto; }
/* 统计卡片 */
.stat-card { text-align: center; padding: 20px; cursor: pointer; transition: transform 0.2s; }
.stat-card:hover { transform: translateY(-4px); }
.stat-value { font-size: 2.5rem; font-weight: bold; color: #409EFF; line-height: 1.2; }
.stat-label { color: #909399; font-size: 0.9rem; margin-top: 8px; }
/* 操作按钮组 */
.action-btn-group { display: flex; gap: 8px; flex-wrap: wrap; }
/* 快速筛选 */
.quick-filter { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
/* 状态徽章 */
.status-badge { display: inline-flex; align-items: center; gap: 4px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; }
.status-dot.pending { background: #E6A23C; }
.status-dot.review { background: #F56C6C; }
.status-dot.ready { background: #67C23A; }
.status-dot.published { background: #409EFF; }
/* 加载覆盖层 */
.loading-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.8); display: flex; align-items: center; justify-content: center; z-index: 9999; }
/* 响应式 */
@media (max-width: 768px) {
.sidebar { display: none; }
.main-content { margin-left: 0; width: 100vw; }
}
</style>
<!-- 本地静态文件 -->
<script src=".vue.global.prod.js?v=20260427"></script>
<link rel="stylesheet" href=".element-plus.css?v=20260427">
<script src="element-plus.full.js?v=20260427"></script>
</head>
<body>
<div id="app">
<h1>测试页面</h1>
<p>Vue 已加载: {{ loaded }}</p>
<el-button type="primary">测试按钮</el-button>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const loaded = ref(true);
return { loaded };
}
}).use(ElementPlus).mount('#app');
</script>
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
/* Modern page fade-in */
.page-fade { animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
/* Enhanced cards */
.card-modern { border-radius: 16px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.card-modern:hover { box-shadow: 0 8px 28px rgba(0,0,0,0.1); }
/* Gradient text */
.text-gradient { background: linear-gradient(135deg, #2563eb, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.text-gradient-primary { background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.text-gradient-success { background: linear-gradient(135deg, #67c23a, #85e61d); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.text-gradient-warning { background: linear-gradient(135deg, #e6a23c, #f5c543); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.text-gradient-danger { background: linear-gradient(135deg, #f56c6c, #f79296); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.text-gradient-info { background: linear-gradient(135deg, #409eff, #5cd0f3); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
/* Pulse animation */
@keyframes pulse-glow { 0%, 100% { box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.4); } 50% { box-shadow: 0 0 0 8px rgba(103, 194, 58, 0); } }
/* Shimmer hover effect */
.shimmer { position: relative; overflow: hidden; }
.shimmer::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(37, 99, 235, 0.06), transparent); transition: left 0.6s; }
.shimmer:hover::before { left: 100%; }
/* Elastic hover lift */
.hover-lift { transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }
.hover-lift:hover { transform: translateY(-6px) scale(1.02); }
/* Modern stat card */
.stat-card-modern { border-radius: 16px; padding: 24px; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); background: white; border: 1px solid #ebeef5; }
.stat-card-modern:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); border-color: #c6d8ff; }
.stat-card-modern .stat-value { font-size: 32px; font-weight: 800; line-height: 1.2; }
.stat-card-modern .stat-label { font-size: 13px; color: #909399; margin-top: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
/* Status badge */
.status-badge-modern { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; }
.status-badge-modern.running { background: rgba(103, 194, 58, 0.12); color: #67c23a; border: 1px solid rgba(103, 194, 58, 0.25); animation: pulse-glow 2s infinite; }
/* Modern el-table refinements */
.el-table { border-radius: 12px; overflow: hidden; }
/* Smooth page transitions */
.page-enter-active { animation: fadeIn 0.4s ease-out; }
/* ========== H5响应式 ========== */
@media (max-width: 768px) {
/* 对话框自适应宽度 */
.el-dialog { width: calc(100vw - 24px) !important; max-width: none !important; }
.el-dialog__body { padding: 16px !important; }
/* 表单网格在手机上改为单列 */
.el-row .el-col-12 { width: 100% !important; }
/* 手机端表格卡片 */
.mobile-card { position: relative; }
.mobile-card:active { transform: scale(0.98); }
}
/* 触摸优化:增大按钮点击区域 */
@media (max-width: 768px) {
.el-button--small { min-height: 36px; padding: 8px 14px !important; }
.el-input__inner { min-height: 38px; }
.el-table__cell .el-button { padding: 8px 12px !important; }
}
+64 -38
View File
@@ -5,19 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 选题管理</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
.navbar-title { font-size: 20px; font-weight: 600; }
.navbar-user { display: flex; align-items: center; gap: 16px; }
.user-info { display: flex; align-items: center; gap: 8px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; min-height: calc(100vh - 60px); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
@@ -39,6 +33,8 @@
.preview-dialog-custom.el-dialog .el-dialog__header { padding: 8px 12px; margin: 0; flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; }
.preview-dialog-custom.el-dialog .el-dialog__body { padding: 12px; overflow: hidden; }
.preview-dialog-custom.el-dialog .el-dialog__footer { flex-shrink: 0; padding: 8px 12px; }
.preview-dialog-custom.is-fullscreen { z-index: 100001 !important; }
body:has(.preview-dialog-custom.is-fullscreen) > [class*="el-overlay"] { z-index: 100000 !important; }
@media (max-width: 768px) {
.content-area { padding: 12px; padding-bottom: 80px; }
@@ -62,6 +58,7 @@
}
@media (min-width: 769px) {
.preview-iframe { max-height: calc(100vh - 100px) !important; }
.preview-dialog-custom { position: relative; left: 90px; }
}
</style>
<script src="navigation-component.js"></script>
@@ -73,18 +70,19 @@
<navigation-component current-page="topics" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">📋 选题管理</h2>
<div class="toolbar">
<el-button type="primary" size="small" @click="refreshAll">🔄 批量刷新</el-button>
<el-button type="success" size="small" @click="triggerGenerateSelected" :disabled="selectedTopicIds.length === 0">▶ 批量创作</el-button>
<el-button type="warning" size="small" @click="triggerOptimizeSelected" :disabled="selectedTopicIds.length === 0">🔍 批量优化</el-button>
<el-button type="warning" size="small" @click="triggerReviewSelected" :disabled="selectedTopicIds.length === 0">🔍 批量审查</el-button>
<span v-if="selectedTopicIds.length > 0" class="selected-count">已选 {{ selectedTopicIds.length }} 项</span>
</div>
</div>
<div class="filter-bar">
<el-button size="default" :type="filterStatus === '' ? 'primary' : ''" @click="filterStatus = ''">全部 ({{ topics.length }})</el-button>
<el-button size="default" :type="filterStatus === 'today' ? 'primary' : ''" @click="filterStatus = 'today'">今日新增 ({{ todayCount }})</el-button>
<el-button size="default" :type="filterStatus === 'pending' ? 'primary' : ''" @click="filterStatus = 'pending'">待处理 ({{ statusStats.pending }})</el-button>
<el-button size="default" :type="filterStatus === 'review' ? 'primary' : ''" @click="filterStatus = 'review'">待审查 ({{ statusStats.review }})</el-button>
<el-button size="default" :type="filterStatus === 'ready' ? 'primary' : ''" @click="filterStatus = 'ready'">待发布 ({{ statusStats.ready }})</el-button>
@@ -109,7 +107,7 @@
<div style="display: flex; gap: 4px; flex-wrap: wrap;">
<el-button size="small" @click="openPreview(scope.row)" type="primary">预览</el-button>
<el-button size="small" type="success" :disabled="isStatus(scope.row, 'published')" @click="createTopic(scope.row)">创作</el-button>
<el-button size="small" type="warning" :disabled="!isStatus(scope.row, 'review')" @click="optimizeTopic(scope.row)">审查</el-button>
<el-button size="small" type="warning" :disabled="!isStatus(scope.row, 'review')" @click="reviewTopic(scope.row)">审查</el-button>
<el-button v-if="isStatus(scope.row, 'ready')" size="small" type="primary" @click="handlePublish(scope.row)">发布</el-button>
<el-button size="small" type="danger" @click="deleteTopic(scope.row.id)">删除</el-button>
</div>
@@ -134,7 +132,7 @@
<div class="topic-card-actions">
<el-button size="small" @click="openPreview(topic)" type="primary">预览</el-button>
<el-button size="small" type="success" :disabled="isStatus(topic, 'published')" @click="createTopic(topic)">创作</el-button>
<el-button size="small" type="warning" :disabled="!isStatus(topic, 'review')" @click="optimizeTopic(topic)">审查</el-button>
<el-button size="small" type="warning" :disabled="!isStatus(topic, 'review')" @click="reviewTopic(topic)">审查</el-button>
<el-button v-if="isStatus(topic, 'ready')" size="small" type="primary" @click="handlePublish(topic)">发布</el-button>
<el-button size="small" type="danger" @click="deleteTopic(topic.id)">删除</el-button>
</div>
@@ -143,25 +141,24 @@
</div>
</main>
</div>
<el-dialog v-model="previewVisible" title="选题预览" width="85%" :modal-props="{ closeOnClickModal: false }" :before-close="() => previewVisible = false" class="preview-dialog-custom" :fullscreen="previewFullscreen">
<el-dialog v-model="previewVisible" title="选题预览" width="85%" :modal-props="{ closeOnClickModal: false }" :before-close="() => previewVisible = false" class="preview-dialog-custom" :fullscreen="previewFullscreen" close-on-press-escape>
<div v-if="previewTopic">
<div style="margin-bottom: 16px; display: flex; justify-content: flex-end; gap: 8px;">
<el-button-group>
<el-button :type="previewPlatform === 'zhihu' ? 'primary' : 'default'" @click="previewPlatform = 'zhihu'">知乎</el-button>
<el-button :type="previewPlatform === 'wechat' ? 'primary' : 'default'" @click="previewPlatform = 'wechat'">微信公众号</el-button>
<el-button :type="previewPlatform === 'xiaohongshu' ? 'primary' : 'default'" @click="previewPlatform = 'xiaohongshu'">小红书</el-button>
</el-button-group>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px;">
<h2 style="margin:0; font-size:16px;">{{ previewTopic.title }}</h2>
<div style="display:flex; align-items:center; gap:12px; font-size:13px; color:#909399;">
<span>创建:{{ formatDate(previewTopic.created_at) }}</span>
<span>状态:{{ getStatusLabel(previewTopic.status) }}</span>
<span v-if="previewTopic.generated_at">创作:{{ formatDate(previewTopic.generated_at) }}</span>
<span v-if="previewTopic.published_at">发布:{{ formatDate(previewTopic.published_at) }}</span>
<el-button size="small" @click="togglePreviewFullscreen">{{ previewFullscreen ? '退出全屏' : '全屏' }}</el-button>
<el-button v-if="previewFullscreen" size="small" type="danger" @click="previewVisible = false">关闭</el-button>
</div>
</div>
<div style="margin: 12px 0 16px; display: flex; justify-content: flex-end; gap: 8px;">
<el-button-group>
<el-button :type="previewPlatform === 'zhihu' ? 'primary' : 'default'" @click="previewPlatform = 'zhihu'">知乎</el-button>
<el-button :type="previewPlatform === 'wechat' ? 'primary' : 'default'" @click="previewPlatform = 'wechat'">微信公众号</el-button>
<el-button :type="previewPlatform === 'xiaohongshu' ? 'primary' : 'default'" @click="previewPlatform = 'xiaohongshu'">小红书</el-button>
</el-button-group>
</div>
<div style="max-width: 1000px; margin: 0 auto; width: 100%; height: 100%; display: flex; flex-direction: column;">
<iframe :srcdoc="currentPreviewHtml" class="preview-iframe" style="flex:1; min-height:500px; border:1px solid #ebeef5; border-radius:8px; background:#fff; overflow:auto; padding:0; width:100%;" sandbox></iframe>
</div>
@@ -192,7 +189,7 @@ const TopicsApp = {
isLoggedIn: false, isAdmin: false, currentUser: { username: '' },
loadingTable: false, selectedTopicIds: [], filterStatus: '',
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
topics: [],
topics: [], todayCount: 0,
previewVisible: false, previewTopic: null, previewFullscreen: false,
previewPlatform: 'zhihu', platformContents: {}
}
@@ -201,15 +198,23 @@ const TopicsApp = {
filteredTopics() {
if (!this.topics || !this.topics.length) return [];
if (!this.filterStatus) return this.topics;
if (this.filterStatus === 'today') {
const today = new Date();
const todayStr = today.toISOString().slice(0, 10);
return this.topics.filter(t => {
const d = t.created_at;
if (!d) return false;
return d.slice(0, 10) === todayStr;
});
}
const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] };
const allowed = map[this.filterStatus] || [this.filterStatus];
return this.topics.filter(t => allowed.includes(t.status));
},
statusStats() {
const s = { total: this.topics.length, pending: 0, review: 0, ready: 0, published: 0 };
const map = { 'pending': s.pending, 'review': s.review, 'ready': s.ready, 'published': s.published };
const aliases = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] };
this.topics.forEach(t => { for (const [k, v] of Object.entries(aliases)) { if (v.includes(t.status)) { map[k]++; break; } } });
this.topics.forEach(t => { for (const [k, v] of Object.entries(aliases)) { if (v.includes(t.status)) { s[k]++; break; } } });
return s;
},
currentPreviewHtml() {
@@ -249,7 +254,13 @@ const TopicsApp = {
this.topics = [];
} finally { this.loadingTable = false; }
},
refreshAll() { this.$message.info('执行批量刷新'); },
async fetchTodayCount() {
try {
const stats = await this.api('/api/topics/stats');
if (stats) this.todayCount = stats.today_created || 0;
} catch (e) { console.error('获取统计失败:', e); }
},
refreshAll() { this.fetchTopics(); this.fetchTodayCount(); this.$message.success('已刷新'); },
async triggerGenerateSelected() {
if (!this.selectedTopicIds.length) return;
let count = 0;
@@ -263,14 +274,14 @@ const TopicsApp = {
this.selectedTopicIds = [];
setTimeout(() => this.fetchTopics(), 2000);
},
async triggerOptimizeSelected() {
async triggerReviewSelected() {
if (!this.selectedTopicIds.length) return;
try {
const data = await this.api('/api/system/optimize/run', { method: 'POST', body: JSON.stringify({ topic_ids: this.selectedTopicIds }) });
this.$message.success('批量优化完成');
const data = await this.api('/api/system/review/run', { method: 'POST', body: JSON.stringify({ topic_ids: this.selectedTopicIds }) });
this.$message.success('批量审查完成');
this.selectedTopicIds = [];
await this.fetchTopics();
} catch (error) { this.$message.error(`批量优化失败: ${error.message}`); }
} catch (error) { this.$message.error(`批量审查失败: ${error.message}`); }
},
async openPreview(topic) {
this.previewTopic = topic; this.previewPlatform = 'zhihu'; this.previewVisible = true; this.platformContents = {};
@@ -282,7 +293,22 @@ const TopicsApp = {
.then(r => r.ok ? r.json() : null).then(d => { if (d && d.html) this.platformContents[p] = d.html; }).catch(e => console.error(`加载${p}预览失败:`, e))
));
},
togglePreviewFullscreen() { this.previewFullscreen = !this.previewFullscreen; },
togglePreviewFullscreen() {
this.previewFullscreen = !this.previewFullscreen;
this.$nextTick(() => {
const overlays = document.querySelectorAll('.el-overlay');
const overlay = overlays[overlays.length - 1];
if (overlay) overlay.style.zIndex = this.previewFullscreen ? '100000' : '';
const dialog = document.querySelector('.preview-dialog-custom');
if (dialog) {
if (this.previewFullscreen) {
dialog.style.zIndex = '100001';
} else {
dialog.style.zIndex = '';
}
}
});
},
platformName(platform) { return { zhihu: '知乎', wechat: '微信公众号', xiaohongshu: '小红书' }[platform] || platform; },
copyContent(platform) {
if (!this.previewTopic || !this.previewTopic.content) { this.$message.warning('暂无内容可复制'); return; }
@@ -298,18 +324,18 @@ const TopicsApp = {
setTimeout(() => this.fetchTopics(), 2000);
} catch (error) { this.$message.error(`创作失败: ${error.message}`); }
},
async optimizeTopic(topic) {
if (!this.isStatus(topic, 'review')) { this.$message.info('仅待审查选题可优化'); return; }
async reviewTopic(topic) {
if (!this.isStatus(topic, 'review')) { this.$message.info('仅待审查选题可操作'); return; }
try {
const data = await this.api('/api/system/optimize/run', { method: 'POST', body: JSON.stringify({ topic_ids: [topic.id] }) });
this.$message.success(`优化完成: ${topic.title}`);
const data = await this.api('/api/system/review/run', { method: 'POST', body: JSON.stringify({ topic_ids: [topic.id] }) });
this.$message.success(`审查完成: ${topic.title}`);
await this.fetchTopics();
} catch (error) { this.$message.error(`优化失败: ${error.message}`); }
} catch (error) { this.$message.error(`审查失败: ${error.message}`); }
},
async handlePublish(topic) {
if (!this.isStatus(topic, 'ready')) { this.$message.info('仅待发布选题可发布'); return; }
try {
const data = await this.api('/api/publishing/create', { method: 'POST', body: JSON.stringify({ topic_ids: [topic.id] }) });
const data = await this.api('/api/publishing/create', { method: 'POST', body: JSON.stringify({ topic_id: topic.id }) });
this.$message.success(`发布成功: ${topic.title}`);
await this.fetchTopics();
} catch (error) { this.$message.error(`发布失败: ${error.message}`); }
@@ -340,7 +366,7 @@ const TopicsApp = {
if (filter) this.filterStatus = filter;
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; this.fetchTopics(); })
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; this.fetchTopics(); this.fetchTodayCount(); })
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
}
};
+6 -12
View File
@@ -5,20 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 用户管理</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
.navbar-title { font-size: 20px; font-weight: 600; }
.navbar-user { display: flex; align-items: center; gap: 16px; }
.user-info { display: flex; align-items: center; gap: 8px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; min-height: calc(100vh - 60px); }
.sidebar { width: 180px; background: white; padding: 16px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.card-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
@@ -29,8 +25,6 @@
.user-card-list { display: none; }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
.card { padding: 16px; }
.user-table { display: none; }
@@ -79,7 +73,7 @@
<navigation-component current-page="users" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<div class="card page-fade">
<div class="card-header">
<h2 class="card-title">👥 用户管理</h2>
<el-button type="primary" @click="addUser">+ 新建用户</el-button>
-61
View File
@@ -1,61 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue基础测试</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.test-card { background: #f5f5f5; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
button { padding: 10px 20px; background: #409EFF; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #337ecc; }
.success { color: green; font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<h1>{{ title }}</h1>
<div class="test-card">
<h3>数据绑定测试</h3>
<p>当前计数: {{ count }}</p>
<button @click="count++">增加计数</button>
</div>
<div class="test-card">
<h3>列表渲染测试</h3>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
<div class="test-card">
<h3>条件渲染测试</h3>
<p v-if="showResult" class="success">✅ Vue基础功能正常工作!</p>
<button @click="showResult = true">显示结果</button>
</div>
</div>
<script>
const app = {
data() {
return {
title: "Vue基础功能测试",
count: 0,
items: ["项目 1", "项目 2", "项目 3"],
showResult: false
}
},
mounted() {
console.log("Vue应用已启动");
console.log("数据对象:", this.$data);
}
};
Vue.createApp(app).mount('#app');
</script>
</body>
</html>