补齐模型管理与基础生成预览链路
This commit is contained in:
@@ -1,3 +1,20 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from fastapi.responses import PlainTextResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{document_id}/docx")
|
||||||
|
async def export_docx(document_id: int):
|
||||||
|
return PlainTextResponse(
|
||||||
|
f"文档 {document_id} 的 Word 导出功能正在开发中,当前版本请先使用预览页查看结果。",
|
||||||
|
media_type="text/plain; charset=utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{document_id}/pdf")
|
||||||
|
async def export_pdf(document_id: int):
|
||||||
|
return PlainTextResponse(
|
||||||
|
f"文档 {document_id} 的 PDF 导出功能正在开发中,当前版本请先使用预览页查看结果。",
|
||||||
|
media_type="text/plain; charset=utf-8",
|
||||||
|
)
|
||||||
|
|||||||
+216
-1
@@ -1,3 +1,218 @@
|
|||||||
from fastapi import APIRouter
|
import json
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from database import get_db
|
||||||
|
from models.document import Document
|
||||||
|
from models.generation_log import GenerationLog
|
||||||
|
from models.paragraph import Paragraph
|
||||||
|
from models.template import Template
|
||||||
|
from schemas.schemas import GenerateFullRequest, GenerateTestRequest, Response
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_document(document: Document) -> dict:
|
||||||
|
return {
|
||||||
|
"id": document.id,
|
||||||
|
"template_id": document.template_id,
|
||||||
|
"name": document.name,
|
||||||
|
"para_count_done": document.para_count_done,
|
||||||
|
"para_count_total": document.para_count_total,
|
||||||
|
"status": document.status,
|
||||||
|
"file_path": document.file_path,
|
||||||
|
"error": document.error,
|
||||||
|
"created_at": document.created_at,
|
||||||
|
"updated_at": document.updated_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_mock_content(paragraph: Paragraph) -> dict:
|
||||||
|
if paragraph.output_format == "table":
|
||||||
|
return {
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"title": paragraph.title,
|
||||||
|
"headers": ["字段", "内容"],
|
||||||
|
"rows": [
|
||||||
|
["段落标题", paragraph.title],
|
||||||
|
["生成说明", paragraph.prompt_text or "根据模板内容生成"],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks = [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": f"这是“{paragraph.title}”的示例生成内容,可用于前端联调与流程验证。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if paragraph.content:
|
||||||
|
blocks.append({"type": "text", "text": f"模板上下文:{paragraph.content[:200]}"})
|
||||||
|
if paragraph.need_prompt and paragraph.prompt_text:
|
||||||
|
blocks.append({"type": "text", "text": f"预设提示词:{paragraph.prompt_text[:200]}"})
|
||||||
|
return {"content": blocks}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/test")
|
||||||
|
async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(get_db)):
|
||||||
|
paragraph = await db.get(Paragraph, body.paragraph_id)
|
||||||
|
if paragraph is None or paragraph.template_id != body.template_id:
|
||||||
|
raise HTTPException(status_code=404, detail="段落不存在")
|
||||||
|
|
||||||
|
content = _build_mock_content(paragraph)
|
||||||
|
return Response(
|
||||||
|
data={
|
||||||
|
"paragraph_id": paragraph.id,
|
||||||
|
"content": content,
|
||||||
|
"message": "当前返回本地模拟生成结果,便于前端联调。",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/full")
|
||||||
|
async def generate_full(body: GenerateFullRequest, db: AsyncSession = Depends(get_db)):
|
||||||
|
template = await db.get(Template, body.template_id)
|
||||||
|
if template is None:
|
||||||
|
raise HTTPException(status_code=404, detail="模板不存在")
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(Paragraph)
|
||||||
|
.where(Paragraph.template_id == body.template_id)
|
||||||
|
.order_by(Paragraph.sort_index.asc(), Paragraph.id.asc())
|
||||||
|
)
|
||||||
|
paragraphs = result.scalars().all()
|
||||||
|
if not paragraphs:
|
||||||
|
raise HTTPException(status_code=400, detail="模板下暂无可生成段落")
|
||||||
|
|
||||||
|
document = Document(
|
||||||
|
template_id=template.id,
|
||||||
|
name=f"{template.name}-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||||
|
para_count_done=0,
|
||||||
|
para_count_total=len(paragraphs),
|
||||||
|
status="generating",
|
||||||
|
file_path="",
|
||||||
|
error="",
|
||||||
|
)
|
||||||
|
db.add(document)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
done_count = 0
|
||||||
|
for paragraph in paragraphs:
|
||||||
|
if paragraph.edit_mode == "manual":
|
||||||
|
content = {"content": [{"type": "text", "text": paragraph.content or "该段落为人工编辑模式。"}]}
|
||||||
|
else:
|
||||||
|
start = time.perf_counter()
|
||||||
|
content = _build_mock_content(paragraph)
|
||||||
|
duration = round(time.perf_counter() - start, 4)
|
||||||
|
log = GenerationLog(
|
||||||
|
document_id=document.id,
|
||||||
|
paragraph_id=paragraph.id,
|
||||||
|
model_id=paragraph.model_id,
|
||||||
|
status="success",
|
||||||
|
content=json.dumps(content, ensure_ascii=False),
|
||||||
|
duration=duration,
|
||||||
|
error_msg="",
|
||||||
|
)
|
||||||
|
db.add(log)
|
||||||
|
done_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
log = GenerationLog(
|
||||||
|
document_id=document.id,
|
||||||
|
paragraph_id=paragraph.id,
|
||||||
|
model_id=paragraph.model_id,
|
||||||
|
status="success",
|
||||||
|
content=json.dumps(content, ensure_ascii=False),
|
||||||
|
duration=0,
|
||||||
|
error_msg="",
|
||||||
|
)
|
||||||
|
db.add(log)
|
||||||
|
done_count += 1
|
||||||
|
|
||||||
|
document.para_count_done = done_count
|
||||||
|
document.status = "completed"
|
||||||
|
document.file_path = f"mock://document/{document.id}"
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(document)
|
||||||
|
return Response(data=_serialize_document(document))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/documents")
|
||||||
|
async def list_documents(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
total = (await db.execute(select(func.count(Document.id)))).scalar_one()
|
||||||
|
result = await db.execute(
|
||||||
|
select(Document)
|
||||||
|
.order_by(Document.id.desc())
|
||||||
|
.offset((page - 1) * page_size)
|
||||||
|
.limit(page_size)
|
||||||
|
)
|
||||||
|
items = [_serialize_document(item) for item in result.scalars().all()]
|
||||||
|
return Response(data={"items": items, "total": total, "page": page, "page_size": page_size})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/documents/{document_id}")
|
||||||
|
async def get_document(document_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
document = await db.get(Document, document_id)
|
||||||
|
if document is None:
|
||||||
|
raise HTTPException(status_code=404, detail="生成记录不存在")
|
||||||
|
|
||||||
|
log_result = await db.execute(
|
||||||
|
select(GenerationLog, Paragraph)
|
||||||
|
.join(Paragraph, Paragraph.id == GenerationLog.paragraph_id)
|
||||||
|
.where(GenerationLog.document_id == document_id)
|
||||||
|
.order_by(Paragraph.sort_index.asc(), Paragraph.id.asc())
|
||||||
|
)
|
||||||
|
items = []
|
||||||
|
for log, paragraph in log_result.all():
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"id": log.id,
|
||||||
|
"paragraph_id": paragraph.id,
|
||||||
|
"title": paragraph.title,
|
||||||
|
"sort_index": paragraph.sort_index,
|
||||||
|
"status": log.status,
|
||||||
|
"content": json.loads(log.content) if log.content else {"content": []},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = _serialize_document(document)
|
||||||
|
payload["logs"] = items
|
||||||
|
return Response(data=payload)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/cancel/{document_id}")
|
||||||
|
async def cancel_document(document_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
document = await db.get(Document, document_id)
|
||||||
|
if document is None:
|
||||||
|
raise HTTPException(status_code=404, detail="生成记录不存在")
|
||||||
|
|
||||||
|
document.status = "cancelled"
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(document)
|
||||||
|
return Response(data=_serialize_document(document))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/documents/{document_id}")
|
||||||
|
async def delete_document(document_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
document = await db.get(Document, document_id)
|
||||||
|
if document is None:
|
||||||
|
raise HTTPException(status_code=404, detail="生成记录不存在")
|
||||||
|
|
||||||
|
result = await db.execute(select(GenerationLog).where(GenerationLog.document_id == document_id))
|
||||||
|
for log in result.scalars().all():
|
||||||
|
await db.delete(log)
|
||||||
|
|
||||||
|
await db.delete(document)
|
||||||
|
await db.commit()
|
||||||
|
return Response(data={"id": document_id})
|
||||||
|
|||||||
@@ -1,3 +1,97 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from database import get_db
|
||||||
|
from models.ai_model import AiModel
|
||||||
|
from schemas.schemas import AiModelCreate, AiModelUpdate, Response
|
||||||
|
from services.security import decrypt_text, encrypt_text, mask_secret
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_model(model: AiModel) -> dict:
|
||||||
|
api_key = decrypt_text(model.api_key_encrypted)
|
||||||
|
return {
|
||||||
|
"id": model.id,
|
||||||
|
"name": model.name,
|
||||||
|
"provider": model.provider,
|
||||||
|
"api_format": model.api_format,
|
||||||
|
"api_endpoint": model.api_endpoint,
|
||||||
|
"api_key_preview": mask_secret(api_key),
|
||||||
|
"status": model.status,
|
||||||
|
"created_at": model.created_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def list_models(db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(AiModel).order_by(AiModel.id.desc()))
|
||||||
|
items = [_serialize_model(item) for item in result.scalars().all()]
|
||||||
|
return Response(data=items)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("")
|
||||||
|
async def create_model(body: AiModelCreate, db: AsyncSession = Depends(get_db)):
|
||||||
|
model = AiModel(
|
||||||
|
name=body.name,
|
||||||
|
provider=body.provider,
|
||||||
|
api_format=body.api_format,
|
||||||
|
api_endpoint=body.api_endpoint,
|
||||||
|
api_key_encrypted=encrypt_text(body.api_key),
|
||||||
|
status=body.status,
|
||||||
|
)
|
||||||
|
db.add(model)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(model)
|
||||||
|
return Response(data=_serialize_model(model))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{model_id}")
|
||||||
|
async def update_model(model_id: int, body: AiModelUpdate, db: AsyncSession = Depends(get_db)):
|
||||||
|
model = await db.get(AiModel, model_id)
|
||||||
|
if model is None:
|
||||||
|
raise HTTPException(status_code=404, detail="模型不存在")
|
||||||
|
|
||||||
|
if body.name is not None:
|
||||||
|
model.name = body.name
|
||||||
|
if body.provider is not None:
|
||||||
|
model.provider = body.provider
|
||||||
|
if body.api_format is not None:
|
||||||
|
model.api_format = body.api_format
|
||||||
|
if body.api_endpoint is not None:
|
||||||
|
model.api_endpoint = body.api_endpoint
|
||||||
|
if body.status is not None:
|
||||||
|
model.status = body.status
|
||||||
|
if body.api_key:
|
||||||
|
model.api_key_encrypted = encrypt_text(body.api_key)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(model)
|
||||||
|
return Response(data=_serialize_model(model))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{model_id}")
|
||||||
|
async def delete_model(model_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
model = await db.get(AiModel, model_id)
|
||||||
|
if model is None:
|
||||||
|
raise HTTPException(status_code=404, detail="模型不存在")
|
||||||
|
|
||||||
|
await db.delete(model)
|
||||||
|
await db.commit()
|
||||||
|
return Response(data={"id": model_id})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{model_id}/test")
|
||||||
|
async def test_model(model_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
model = await db.get(AiModel, model_id)
|
||||||
|
if model is None:
|
||||||
|
raise HTTPException(status_code=404, detail="模型不存在")
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
data={
|
||||||
|
"id": model.id,
|
||||||
|
"success": True,
|
||||||
|
"message": f"模型 {model.name} 配置校验通过(当前为本地模拟测试)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ class AiModelCreate(BaseModel):
|
|||||||
api_key: str = ""
|
api_key: str = ""
|
||||||
status: str = "enabled"
|
status: str = "enabled"
|
||||||
|
|
||||||
|
|
||||||
|
class AiModelUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
provider: Optional[str] = None
|
||||||
|
api_format: Optional[str] = None
|
||||||
|
api_endpoint: Optional[str] = None
|
||||||
|
api_key: str = ""
|
||||||
|
status: Optional[str] = None
|
||||||
|
|
||||||
class AiModelOut(BaseModel):
|
class AiModelOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
|
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
|
||||||
|
def _build_fernet() -> Fernet:
|
||||||
|
raw_key = settings.ENCRYPTION_KEY.encode("utf-8")
|
||||||
|
digest = hashlib.sha256(raw_key).digest()
|
||||||
|
return Fernet(base64.urlsafe_b64encode(digest))
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_text(value: str) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
return _build_fernet().encrypt(value.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_text(value: str) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return _build_fernet().decrypt(value.encode("utf-8")).decode("utf-8")
|
||||||
|
except InvalidToken:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def mask_secret(value: str) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
if len(value) <= 7:
|
||||||
|
return "*" * len(value)
|
||||||
|
return f"{value[:3]}****{value[-4:]}"
|
||||||
@@ -12,3 +12,16 @@
|
|||||||
6. 修复前端若干现存类型/图标问题,确保 `vue-tsc --noEmit` 可通过。
|
6. 修复前端若干现存类型/图标问题,确保 `vue-tsc --noEmit` 可通过。
|
||||||
7. 更新任务拆解清单,标记本轮已确认完成的阶段项与子任务。
|
7. 更新任务拆解清单,标记本轮已确认完成的阶段项与子任务。
|
||||||
- **执行结果**: 已完成模板 CRUD 路由和模板解析器基础能力,项目当前可通过后端语法检查与前端类型检查,任务清单已同步标注已完成项。
|
- **执行结果**: 已完成模板 CRUD 路由和模板解析器基础能力,项目当前可通过后端语法检查与前端类型检查,任务清单已同步标注已完成项。
|
||||||
|
|
||||||
|
## 会话 ID: local-20260702145958
|
||||||
|
- [2026-07-02 14:59:58]
|
||||||
|
- **执行原因**: 用户要求先提交当前代码,并继续完善到可以初步使用的程度。
|
||||||
|
- **执行过程**:
|
||||||
|
1. 将首批模板解析与模板管理相关改动整理后提交,提交信息使用中文。
|
||||||
|
2. 新增模型管理后端接口,支持模型列表、创建、更新、删除、状态切换与本地模拟测试。
|
||||||
|
3. 新增加密工具,按项目要求对 API Key 做 Fernet 形式加密存储并提供脱敏展示。
|
||||||
|
4. 新增生成与记录接口,支持单段测试、整份文档模拟生成、历史列表、详情预览、取消与删除。
|
||||||
|
5. 将预览页接入真实生成记录数据,将模板编辑页补齐“保存配置”和“立即测试”能力,并让历史页显示动态统计。
|
||||||
|
6. 更新任务拆解清单,补记“模型 CRUD 路由”已完成。
|
||||||
|
7. 再次执行后端语法检查与前端类型检查,确保本轮改动可用。
|
||||||
|
- **执行结果**: 当前系统已可初步走通“上传模板 → 配置段落 → 管理模型 → 触发生成 → 查看记录/预览”的联调链路,导出仍为占位提示实现。
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
### 路由与 API(3 天)
|
### 路由与 API(3 天)
|
||||||
- [x] 模板 CRUD 路由
|
- [x] 模板 CRUD 路由
|
||||||
- [ ] 模型 CRUD 路由
|
- [x] 模型 CRUD 路由
|
||||||
- [ ] 生成相关路由(测试/全量/进度SSE/取消)
|
- [ ] 生成相关路由(测试/全量/进度SSE/取消)
|
||||||
- [ ] 导出路由(Word/PDF)
|
- [ ] 导出路由(Word/PDF)
|
||||||
- [ ] 文件上传/管理
|
- [ ] 文件上传/管理
|
||||||
|
|||||||
@@ -1,14 +1,79 @@
|
|||||||
<template><div style="padding:24px"><a-row :gutter="16" style="margin-bottom:24px"><a-col :span="6"><a-card><a-statistic title="总生成" :value="12" /></a-card></a-col><a-col :span="6"><a-card><a-statistic title="成功" :value="10" value-style="color:#52c41a" /></a-card></a-col><a-col :span="6"><a-card><a-statistic title="中断" :value="1" value-style="color:#faad14" /></a-card></a-col><a-col :span="6"><a-card><a-statistic title="失败" :value="1" value-style="color:#ff4d4f" /></a-card></a-col></a-row><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px"><h3>生成记录</h3><div><a-select style="width:140px;margin-right:8px" placeholder="全部模板" /><a-select style="width:120px" placeholder="全部状态" /></div></div><a-list :dataSource="documents" :grid="{gutter:16,xs:1,sm:1,md:2,lg:2,xl:3,xxl:3}"><template #renderItem="{item}"><a-list-item><a-card hoverable><a-card-meta :title="item.name"><template #description><p>模板:{{item.template_id}}</p><p>段落:{{item.para_count_done}}/{{item.para_count_total}}</p><p>状态:<a-badge :status="item.status==='completed'?'success':item.status==='failed'?'error':'processing'" :text="statusText(item.status)" /></p></template></a-card-meta><template #actions><a-button type="link" @click="preview(item.id)">预览</a-button><a-button type="link" @click="download(item.id)">下载</a-button></template></a-card></a-list-item></template></a-list></div></template>
|
<template>
|
||||||
|
<div style="padding:24px">
|
||||||
|
<a-row :gutter="16" style="margin-bottom:24px">
|
||||||
|
<a-col :span="6"><a-card><a-statistic title="总生成" :value="totalCount" /></a-card></a-col>
|
||||||
|
<a-col :span="6"><a-card><a-statistic title="成功" :value="completedCount" value-style="color:#52c41a" /></a-card></a-col>
|
||||||
|
<a-col :span="6"><a-card><a-statistic title="中断" :value="cancelledCount" value-style="color:#faad14" /></a-card></a-col>
|
||||||
|
<a-col :span="6"><a-card><a-statistic title="失败" :value="failedCount" value-style="color:#ff4d4f" /></a-card></a-col>
|
||||||
|
</a-row>
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||||
|
<h3>生成记录</h3>
|
||||||
|
</div>
|
||||||
|
<a-list :dataSource="documents" :grid="{gutter:16,xs:1,sm:1,md:2,lg:2,xl:3,xxl:3}">
|
||||||
|
<template #renderItem="{item}">
|
||||||
|
<a-list-item>
|
||||||
|
<a-card hoverable>
|
||||||
|
<a-card-meta :title="item.name">
|
||||||
|
<template #description>
|
||||||
|
<p>模板:{{ item.template_id }}</p>
|
||||||
|
<p>段落:{{ item.para_count_done }}/{{ item.para_count_total }}</p>
|
||||||
|
<p>状态:<a-badge :status="statusBadge(item.status)" :text="statusText(item.status)" /></p>
|
||||||
|
</template>
|
||||||
|
</a-card-meta>
|
||||||
|
<template #actions>
|
||||||
|
<a-button type="link" @click="preview(item.id)">预览</a-button>
|
||||||
|
<a-button type="link" @click="download(item.id)">下载</a-button>
|
||||||
|
</template>
|
||||||
|
</a-card>
|
||||||
|
</a-list-item>
|
||||||
|
</template>
|
||||||
|
</a-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { computed, ref, onMounted } from 'vue'
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from 'vue-router'
|
||||||
import { useDocumentStore } from "@/stores/document";
|
import { useDocumentStore } from '@/stores/document'
|
||||||
import { generateApi } from "@/api/generate";
|
import { generateApi } from '@/api/generate'
|
||||||
const router = useRouter();
|
|
||||||
const store = useDocumentStore();
|
const router = useRouter()
|
||||||
const documents = ref<any[]>([]);
|
const store = useDocumentStore()
|
||||||
function statusText(s:string){const m:Record<string,string>={'completed':'已生成','failed':'失败','cancelled':'中断','generating':'生成中','pending':'等待中'};return m[s]||s}
|
const documents = ref<any[]>([])
|
||||||
onMounted(async()=>{await store.fetchList();documents.value=store.documents as any});
|
|
||||||
function preview(id:number){router.push(`/preview/${id}`)}
|
const totalCount = computed(() => documents.value.length)
|
||||||
function download(id:number){window.open(generateApi.exportDocx(id))}
|
const completedCount = computed(() => documents.value.filter((item) => item.status === 'completed').length)
|
||||||
</script>
|
const cancelledCount = computed(() => documents.value.filter((item) => item.status === 'cancelled').length)
|
||||||
|
const failedCount = computed(() => documents.value.filter((item) => item.status === 'failed').length)
|
||||||
|
|
||||||
|
function statusText(status: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
completed: '已生成',
|
||||||
|
failed: '失败',
|
||||||
|
cancelled: '中断',
|
||||||
|
generating: '生成中',
|
||||||
|
pending: '等待中',
|
||||||
|
}
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusBadge(status: string) {
|
||||||
|
if (status === 'completed') return 'success'
|
||||||
|
if (status === 'failed') return 'error'
|
||||||
|
if (status === 'cancelled') return 'warning'
|
||||||
|
return 'processing'
|
||||||
|
}
|
||||||
|
|
||||||
|
function preview(id: number) {
|
||||||
|
router.push(`/preview/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function download(id: number) {
|
||||||
|
window.open(generateApi.exportDocx(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await store.fetchList()
|
||||||
|
documents.value = store.documents as any
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
+194
-17
@@ -1,19 +1,196 @@
|
|||||||
<template><div style="padding:24px"><a-card><template #title><span style="display:flex;align-items:center;gap:8px"><file-text-outlined /> 文档编辑</span></template><template #extra><a-button-group><a-button @click="undo"><undo-outlined />撤销</a-button><a-button @click="redo"><redo-outlined />重做</a-button></a-button-group><a-button type="primary" style="margin-left:12px" @click="exportDocx">导出 Word</a-button><a-button style="margin-left:8px" @click="exportPdf">导出 PDF</a-button></template><div style="display:flex;gap:16px"><div style="flex:1;min-width:0"><div style="display:flex;gap:4px;padding:8px;border:1px solid #d9d9d9;border-bottom:none;border-radius:6px 6px 0 0;flex-wrap:wrap"><a-button size="small"><b>B</b></a-button><a-button size="small"><i>I</i></a-button><a-button size="small"><u>U</u></a-button><a-divider type="vertical" /><a-button size="small"><font-size-outlined /></a-button><a-button size="small"><ordered-list-outlined /></a-button><a-button size="small"><table-outlined /></a-button></div><div ref="editorRef" contenteditable="true" style="border:1px solid #d9d9d9;border-radius:0 0 6px 6px;padding:24px;min-height:500px;outline:none;font-size:14px;line-height:1.8" v-html="editorHtml" @input="onEdit"></div></div><div style="width:200px;flex-shrink:0"><a-card title="文档结构" size="small"><div v-for="s in structure" :key="s.id" :class="['struct-item',{active:s.active}]" @click="scrollTo(s.id)"><file-text-outlined /> {{s.title}}</div></a-card></div></div></a-card></div></template>
|
<template>
|
||||||
|
<div style="padding:24px">
|
||||||
|
<a-card>
|
||||||
|
<template #title>
|
||||||
|
<span style="display:flex;align-items:center;gap:8px">
|
||||||
|
<file-text-outlined />
|
||||||
|
文档预览
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<a-button-group>
|
||||||
|
<a-button @click="undo"><undo-outlined />撤销</a-button>
|
||||||
|
<a-button @click="redo"><redo-outlined />重做</a-button>
|
||||||
|
</a-button-group>
|
||||||
|
<a-button type="primary" style="margin-left:12px" @click="exportDocx">导出 Word</a-button>
|
||||||
|
<a-button style="margin-left:8px" @click="exportPdf">导出 PDF</a-button>
|
||||||
|
</template>
|
||||||
|
<div style="display:flex;gap:16px">
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<div style="display:flex;gap:4px;padding:8px;border:1px solid #d9d9d9;border-bottom:none;border-radius:6px 6px 0 0;flex-wrap:wrap">
|
||||||
|
<a-button size="small"><b>B</b></a-button>
|
||||||
|
<a-button size="small"><i>I</i></a-button>
|
||||||
|
<a-button size="small"><u>U</u></a-button>
|
||||||
|
<a-divider type="vertical" />
|
||||||
|
<a-button size="small"><font-size-outlined /></a-button>
|
||||||
|
<a-button size="small"><ordered-list-outlined /></a-button>
|
||||||
|
<a-button size="small"><table-outlined /></a-button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="editorRef"
|
||||||
|
contenteditable="true"
|
||||||
|
style="border:1px solid #d9d9d9;border-radius:0 0 6px 6px;padding:24px;min-height:500px;outline:none;font-size:14px;line-height:1.8;background:#fff"
|
||||||
|
v-html="editorHtml"
|
||||||
|
@input="onEdit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style="width:220px;flex-shrink:0">
|
||||||
|
<a-card title="文档结构" size="small">
|
||||||
|
<div
|
||||||
|
v-for="s in structure"
|
||||||
|
:key="s.id"
|
||||||
|
class="struct-item"
|
||||||
|
@click="scrollTo(s.anchor)"
|
||||||
|
>
|
||||||
|
<file-text-outlined />
|
||||||
|
{{ s.title }}
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { computed, ref, onMounted } from 'vue'
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from 'vue-router'
|
||||||
import { generateApi } from "@/api/generate";
|
import { generateApi } from '@/api/generate'
|
||||||
import { message } from "ant-design-vue";
|
import { message } from 'ant-design-vue'
|
||||||
import { FileTextOutlined, UndoOutlined, RedoOutlined, FontSizeOutlined, OrderedListOutlined, TableOutlined } from "@ant-design/icons-vue";
|
import {
|
||||||
const route = useRoute();
|
FileTextOutlined,
|
||||||
const editorRef = ref<HTMLElement|null>(null);
|
UndoOutlined,
|
||||||
const editorHtml = ref(`<h2 style="text-align:center">2025年第一季度经济活动分析报告</h2><p style="text-align:center;color:#666">某某集团有限公司</p><h3>一、主要经营指标完成情况</h3><div style="background:#f0f0ff;padding:12px;border-left:3px solid #5b5bd6;border-radius:4px;margin:8px 0"><p>本季度营业收入完成<strong>12.35亿元</strong>,同比上升<strong>8.7%</strong>。</p></div><h3>主要经营指标完成情况表</h3><table border="1" style="width:100%;border-collapse:collapse"><tr><th>指标</th><th>完成值</th><th>同比</th></tr><tr><td>营业收入</td><td>12.35亿</td><td>+8.7%</td></tr></table><h3>二、成本费用分析</h3><p>本季度总成本9.87亿元,同比上升6.2%。</p>`);
|
RedoOutlined,
|
||||||
const structure = ref([{id:'s1',title:'经营指标',active:true},{id:'s2',title:'指标表',active:false},{id:'s3',title:'成本费用',active:false}]);
|
FontSizeOutlined,
|
||||||
function scrollTo(id:string){}
|
OrderedListOutlined,
|
||||||
function onEdit(){}
|
TableOutlined,
|
||||||
function undo(){document.execCommand('undo')}
|
} from '@ant-design/icons-vue'
|
||||||
function redo(){document.execCommand('redo')}
|
|
||||||
function exportDocx(){const id=route.params.id;window.open(generateApi.exportDocx(Number(id)))}
|
interface ContentBlock {
|
||||||
function exportPdf(){const id=route.params.id;window.open(generateApi.exportPdf(Number(id)))}
|
type: string
|
||||||
|
text?: string
|
||||||
|
title?: string
|
||||||
|
headers?: string[]
|
||||||
|
rows?: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogItem {
|
||||||
|
paragraph_id: number
|
||||||
|
title: string
|
||||||
|
sort_index: number
|
||||||
|
content: {
|
||||||
|
content: ContentBlock[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const editorRef = ref<HTMLElement | null>(null)
|
||||||
|
const logs = ref<LogItem[]>([])
|
||||||
|
const editorHtml = ref('<p>加载中...</p>')
|
||||||
|
|
||||||
|
const structure = computed(() =>
|
||||||
|
logs.value.map((item) => ({
|
||||||
|
id: item.paragraph_id,
|
||||||
|
title: item.title || `段落 ${item.sort_index}`,
|
||||||
|
anchor: `section-${item.paragraph_id}`,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
function renderTable(block: ContentBlock) {
|
||||||
|
const headers = block.headers || []
|
||||||
|
const rows = block.rows || []
|
||||||
|
const thead = headers.length
|
||||||
|
? `<thead><tr>${headers.map((header) => `<th>${header}</th>`).join('')}</tr></thead>`
|
||||||
|
: ''
|
||||||
|
const tbody = `<tbody>${rows
|
||||||
|
.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join('')}</tr>`)
|
||||||
|
.join('')}</tbody>`
|
||||||
|
return `<table border="1" style="width:100%;border-collapse:collapse;margin:12px 0">${thead}${tbody}</table>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBlocks(blocks: ContentBlock[]) {
|
||||||
|
return blocks
|
||||||
|
.map((block) => {
|
||||||
|
if (block.type === 'table') {
|
||||||
|
return renderTable(block)
|
||||||
|
}
|
||||||
|
return `<p style="margin:10px 0">${block.text || ''}</p>`
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDocument() {
|
||||||
|
const id = Number(route.params.id)
|
||||||
|
const response: any = await generateApi.getDocument(id)
|
||||||
|
logs.value = response.data?.logs || []
|
||||||
|
if (!logs.value.length) {
|
||||||
|
editorHtml.value = '<p>暂无生成内容。</p>'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editorHtml.value = logs.value
|
||||||
|
.map(
|
||||||
|
(item) => `
|
||||||
|
<section id="section-${item.paragraph_id}" style="margin-bottom:24px">
|
||||||
|
<h3 style="margin-bottom:12px">${item.title}</h3>
|
||||||
|
<div style="background:#f8faff;padding:16px;border-left:3px solid #5b5bd6;border-radius:4px">
|
||||||
|
${renderBlocks(item.content?.content || [])}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollTo(anchor: string) {
|
||||||
|
const element = document.getElementById(anchor)
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEdit() {}
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
document.execCommand('undo')
|
||||||
|
}
|
||||||
|
|
||||||
|
function redo() {
|
||||||
|
document.execCommand('redo')
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportDocx() {
|
||||||
|
const id = Number(route.params.id)
|
||||||
|
window.open(generateApi.exportDocx(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportPdf() {
|
||||||
|
const id = Number(route.params.id)
|
||||||
|
window.open(generateApi.exportPdf(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await loadDocument()
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '加载文档失败')
|
||||||
|
editorHtml.value = '<p>文档加载失败。</p>'
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<style scoped>.struct-item{padding:8px;cursor:pointer;border-radius:4px;font-size:13px}.struct-item:hover{background:#f5f5f5}.struct-item.active{background:#f0f0ff;color:#5b5bd6}</style>
|
|
||||||
|
<style scoped>
|
||||||
|
.struct-item {
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.struct-item:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,21 +1,194 @@
|
|||||||
<template><div style="display:flex;height:calc(100vh - 112px)"><div style="width:220px;border-right:1px solid #f0f0f0;overflow-y:auto;padding:12px"><h4 style="margin-bottom:12px">段落列表</h4><div v-for="(p,i) in paragraphs" :key="p.id" :class="['para-item',{active:selectedId===p.id}]" @click="selectPara(p.id)"><span class="idx">{{i+1}}</span><span class="title">{{p.title||'未命名'}}</span><a-tag :color="p.edit_mode==='ai'?'blue':'orange'" style="margin-left:auto">{{p.edit_mode==='ai'?'AI':'手动'}}</a-tag></div></div><div style="flex:1;overflow-y:auto;padding:24px;background:#f0f1f3;display:flex;justify-content:center;align-items:flex-start"><div style="width:794px;background:#fff;padding:80px 72px;min-height:500px;box-shadow:0 2px 8px rgba(0,0,0,.08)" v-html="previewHtml"></div></div><div style="width:380px;border-left:1px solid #f0f0f0;overflow-y:auto;padding:16px"><h4>段落配置</h4><div v-if="selectedPara"><a-form layout="vertical"><a-form-item label="编辑方式"><a-select v-model:value="selectedPara.edit_mode"><a-select-option value="ai">AI 生成</a-select-option><a-select-option value="manual">人工编辑</a-select-option></a-select></a-form-item><a-form-item v-if="selectedPara.edit_mode==='ai'" label="生成模型"><a-select v-model:value="selectedPara.model_id" allowClear placeholder="使用默认模型"><a-select-option v-for="m in models" :key="m.id" :value="m.id">{{m.name}}</a-select-option></a-select></a-form-item><a-form-item label="输出格式"><a-select v-model:value="selectedPara.output_format"><a-select-option value="text">正式报告段落</a-select-option><a-select-option value="table">表格形式</a-select-option><a-select-option value="mixed">混合内容</a-select-option><a-select-option value="chart">图表形式</a-select-option></a-select></a-form-item><a-form-item label="需要提示词"><a-switch v-model:checked="selectedPara.need_prompt" /></a-form-item><a-form-item v-if="selectedPara.need_prompt" label="预设提示词"><a-textarea v-model:value="selectedPara.prompt_text" :rows="4" placeholder="在此输入提示词..." /></a-form-item><a-form-item label="需要参考文件"><a-switch v-model:checked="selectedPara.need_file" /></a-form-item><a-form-item v-if="selectedPara.need_file" label="备注说明"><a-textarea v-model:value="selectedPara.file_note" :rows="2" placeholder="提示用户上传什么文件" /></a-form-item></a-form><a-button v-if="selectedPara.edit_mode==='ai'" type="primary" block @click="testPara">立即测试</a-button></div></div></div></template>
|
<template>
|
||||||
|
<div style="display:flex;height:calc(100vh - 112px)">
|
||||||
|
<div style="width:220px;border-right:1px solid #f0f0f0;overflow-y:auto;padding:12px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||||
|
<h4 style="margin:0">段落列表</h4>
|
||||||
|
<a-button type="primary" size="small" @click="saveTemplate">保存</a-button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(p, i) in paragraphs"
|
||||||
|
:key="p.id"
|
||||||
|
:class="['para-item', { active: selectedId === p.id }]"
|
||||||
|
@click="selectPara(p.id)"
|
||||||
|
>
|
||||||
|
<span class="idx">{{ i + 1 }}</span>
|
||||||
|
<span class="title">{{ p.title || '未命名' }}</span>
|
||||||
|
<a-tag :color="p.edit_mode === 'ai' ? 'blue' : 'orange'" style="margin-left:auto">
|
||||||
|
{{ p.edit_mode === 'ai' ? 'AI' : '手动' }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;overflow-y:auto;padding:24px;background:#f0f1f3;display:flex;justify-content:center;align-items:flex-start">
|
||||||
|
<div style="width:794px;background:#fff;padding:80px 72px;min-height:500px;box-shadow:0 2px 8px rgba(0,0,0,.08)" v-html="previewHtml" />
|
||||||
|
</div>
|
||||||
|
<div style="width:380px;border-left:1px solid #f0f0f0;overflow-y:auto;padding:16px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||||
|
<h4 style="margin:0">段落配置</h4>
|
||||||
|
<a-button @click="saveTemplate">保存配置</a-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedPara">
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="编辑方式">
|
||||||
|
<a-select v-model:value="selectedPara.edit_mode">
|
||||||
|
<a-select-option value="ai">AI 生成</a-select-option>
|
||||||
|
<a-select-option value="manual">人工编辑</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="selectedPara.edit_mode === 'ai'" label="生成模型">
|
||||||
|
<a-select v-model:value="selectedPara.model_id" allowClear placeholder="使用默认模型">
|
||||||
|
<a-select-option v-for="m in models" :key="m.id" :value="m.id">{{ m.name }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="输出格式">
|
||||||
|
<a-select v-model:value="selectedPara.output_format">
|
||||||
|
<a-select-option value="text">正式报告段落</a-select-option>
|
||||||
|
<a-select-option value="table">表格形式</a-select-option>
|
||||||
|
<a-select-option value="mixed">混合内容</a-select-option>
|
||||||
|
<a-select-option value="chart">图表形式</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="需要提示词">
|
||||||
|
<a-switch v-model:checked="selectedPara.need_prompt" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="selectedPara.need_prompt" label="预设提示词">
|
||||||
|
<a-textarea v-model:value="selectedPara.prompt_text" :rows="4" placeholder="在此输入提示词..." />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="需要参考文件">
|
||||||
|
<a-switch v-model:checked="selectedPara.need_file" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="selectedPara.need_file" label="备注说明">
|
||||||
|
<a-textarea v-model:value="selectedPara.file_note" :rows="2" placeholder="提示用户上传什么文件" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<a-button v-if="selectedPara.edit_mode === 'ai'" type="primary" block @click="testPara">立即测试</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { computed, ref, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute } from 'vue-router'
|
||||||
import { useTemplateStore } from "@/stores/template";
|
import { Modal, message } from 'ant-design-vue'
|
||||||
import { useModelStore } from "@/stores/model";
|
import { useTemplateStore } from '@/stores/template'
|
||||||
import { message } from "ant-design-vue";
|
import { useModelStore } from '@/stores/model'
|
||||||
const route = useRoute();
|
import { generateApi } from '@/api/generate'
|
||||||
const router = useRouter();
|
|
||||||
const store = useTemplateStore();
|
const route = useRoute()
|
||||||
const modelStore = useModelStore();
|
const store = useTemplateStore()
|
||||||
const paragraphs = ref<any[]>([]);
|
const modelStore = useModelStore()
|
||||||
const models = ref<any[]>([]);
|
const paragraphs = ref<any[]>([])
|
||||||
const selectedId = ref(0);
|
const models = ref<any[]>([])
|
||||||
const selectedPara = computed(()=>paragraphs.value.find(p=>p.id===selectedId.value));
|
const selectedId = ref(0)
|
||||||
const previewHtml = computed(()=>{return paragraphs.value.map(p=>'<div style="margin:8px 0;padding:8px;border:2px solid transparent;border-radius:4px'+(selectedId.value===p.id?';border-color:#5b5bd6;background:#f0f0ff':'')+'"><h3>'+(p.title||"")+'</h3><p>'+(p.content||"点击左侧配置此段落")+'</p></div>').join("")});
|
|
||||||
onMounted(async()=>{const id = Number(route.params.id);await store.fetchOne(id);paragraphs.value=store.paragraphs as any;await modelStore.fetchList();models.value=modelStore.models as any;if(paragraphs.value.length)selectedId.value=paragraphs.value[0].id});
|
const selectedPara = computed(() => paragraphs.value.find((item) => item.id === selectedId.value))
|
||||||
function selectPara(id:number){selectedId.value=id;const el=document.getElementById('para-'+id);if(el)el.scrollIntoView({behavior:'smooth',block:'center'})}
|
const previewHtml = computed(() =>
|
||||||
async function testPara(){message.info('段落测试功能待实现')}
|
paragraphs.value
|
||||||
|
.map(
|
||||||
|
(item) =>
|
||||||
|
`<div style="margin:8px 0;padding:8px;border:2px solid transparent;border-radius:4px${
|
||||||
|
selectedId.value === item.id ? ';border-color:#5b5bd6;background:#f0f0ff' : ''
|
||||||
|
}"><h3>${item.title || ''}</h3><p>${item.content || '点击左侧配置此段落'}</p></div>`,
|
||||||
|
)
|
||||||
|
.join(''),
|
||||||
|
)
|
||||||
|
|
||||||
|
function selectPara(id: number) {
|
||||||
|
selectedId.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTemplate() {
|
||||||
|
const templateId = Number(route.params.id)
|
||||||
|
await store.save(templateId)
|
||||||
|
message.success('模板配置已保存')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPara() {
|
||||||
|
if (!selectedPara.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const templateId = Number(route.params.id)
|
||||||
|
const response: any = await generateApi.test({
|
||||||
|
paragraph_id: selectedPara.value.id,
|
||||||
|
template_id: templateId,
|
||||||
|
prompt_text: selectedPara.value.prompt_text || '',
|
||||||
|
model_id: selectedPara.value.model_id || 0,
|
||||||
|
file_paths: [],
|
||||||
|
})
|
||||||
|
const blocks = response.data?.content?.content || []
|
||||||
|
const text = blocks
|
||||||
|
.map((block: any) => {
|
||||||
|
if (block.type === 'table') {
|
||||||
|
return `${block.title || '表格'}:${(block.rows || []).length} 行`
|
||||||
|
}
|
||||||
|
return block.text || ''
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n\n')
|
||||||
|
|
||||||
|
Modal.info({
|
||||||
|
title: '段落测试结果',
|
||||||
|
width: 640,
|
||||||
|
content: text || '当前未返回可展示内容。',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const id = Number(route.params.id)
|
||||||
|
await store.fetchOne(id)
|
||||||
|
paragraphs.value = store.paragraphs as any
|
||||||
|
await modelStore.fetchList()
|
||||||
|
models.value = modelStore.models as any
|
||||||
|
if (paragraphs.value.length) {
|
||||||
|
selectedId.value = paragraphs.value[0].id
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<style scoped>.para-item{display:flex;align-items:center;gap:8px;padding:8px;border-radius:6px;cursor:pointer;margin-bottom:2px;font-size:13px}.para-item:hover{background:#f5f5f5}.para-item.active{background:#f0f0ff;color:#5b5bd6;font-weight:500}.para-item .idx{width:20px;height:20px;border-radius:50%;background:#f0f0f0;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0}.para-item.active .idx{background:#5b5bd6;color:#fff}.para-item .title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}</style>
|
|
||||||
|
<style scoped>
|
||||||
|
.para-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-item:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-item.active {
|
||||||
|
background: #f0f0ff;
|
||||||
|
color: #5b5bd6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-item .idx {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-item.active .idx {
|
||||||
|
background: #5b5bd6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-item .title {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user