feat: 内容数据迁移至数据库,合规审查全链路打通
- 文章 HTML 存储从文件系统迁移至 articles 表,删除 releases 目录 - 合规审查从 DB 读取 HTML,审查结果写回 DB,通过后自动推进至待发布 - 新增 todayCount 筛选按钮,与系统概览统计数据一致 - 全屏预览修复:提升 z-index 超过侧边栏,添加退出全屏/关闭按钮 - 统一 '优化' → '审查' 命名,消除前后端术语不一致 - 调度器创作完成后自动触发审查(生成 → 审查 → 待发布) - 清理旧备份/调试文件、过期大纲和研究笔记
This commit is contained in:
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = '已发布'
|
||||
|
||||
@@ -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()}}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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\n(LLM 调用失败:{e},请手动补充)"
|
||||
|
||||
# 测试
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
print(f"[modelscope_client] 使用模型:{CONFIG['model']}")
|
||||
resp = call_llm("你好,请用一句话介绍你自己。", max_tokens=50)
|
||||
print(f"[modelscope_client] 响应:{resp}")
|
||||
except Exception as e:
|
||||
print(f"[modelscope_client] 错误:{e}")
|
||||
@@ -1,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\n(LLM 调用失败:{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}")
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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\n(LLM 调用失败:{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}")
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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("✅ 插入默认系统配置")
|
||||
|
||||
|
||||
@@ -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=["*"],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
};
|
||||
@@ -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,5 +1,5 @@
|
||||
// Service Worker for 宇之然内容创作平台
|
||||
const CACHE_NAME = 'yuzhiran-v1';
|
||||
const CACHE_NAME = 'yuzhiran-v2';
|
||||
const CACHE_URLS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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'; });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user