完善模板测试弹窗与多文件解析链路
This commit is contained in:
@@ -7,6 +7,7 @@ cryptography>=42.0.0
|
|||||||
python-docx>=1.1.0
|
python-docx>=1.1.0
|
||||||
openpyxl>=3.1.0
|
openpyxl>=3.1.0
|
||||||
pandas>=2.1.0
|
pandas>=2.1.0
|
||||||
|
xlrd>=2.0.1
|
||||||
httpx>=0.27.0
|
httpx>=0.27.0
|
||||||
pydantic>=2.5.0
|
pydantic>=2.5.0
|
||||||
pydantic-settings>=2.1.0
|
pydantic-settings>=2.1.0
|
||||||
@@ -15,3 +16,4 @@ aiofiles>=23.2.0
|
|||||||
sse-starlette>=2.0.0
|
sse-starlette>=2.0.0
|
||||||
minio>=7.2.0 # MinIO 对象存储 SDK
|
minio>=7.2.0 # MinIO 对象存储 SDK
|
||||||
alembic>=1.13.0
|
alembic>=1.13.0
|
||||||
|
pypdf>=5.0.0
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
@@ -17,6 +18,7 @@ from models.paragraph import Paragraph
|
|||||||
from models.template import Template
|
from models.template import Template
|
||||||
from schemas.schemas import GenerateFullRequest, GenerateTestRequest, Response
|
from schemas.schemas import GenerateFullRequest, GenerateTestRequest, Response
|
||||||
from services.ai_service import call_ai
|
from services.ai_service import call_ai
|
||||||
|
from services.file_summary import summarize_minio_files
|
||||||
from services.generation_runtime import (
|
from services.generation_runtime import (
|
||||||
build_mock_content,
|
build_mock_content,
|
||||||
generation_progress,
|
generation_progress,
|
||||||
@@ -48,6 +50,8 @@ async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(ge
|
|||||||
paragraph = await db.get(Paragraph, body.paragraph_id)
|
paragraph = await db.get(Paragraph, body.paragraph_id)
|
||||||
if paragraph is None or paragraph.template_id != body.template_id:
|
if paragraph is None or paragraph.template_id != body.template_id:
|
||||||
raise HTTPException(status_code=404, detail="段落不存在")
|
raise HTTPException(status_code=404, detail="段落不存在")
|
||||||
|
if body.prompt_text:
|
||||||
|
paragraph.prompt_text = body.prompt_text
|
||||||
|
|
||||||
model = None
|
model = None
|
||||||
if body.model_id:
|
if body.model_id:
|
||||||
@@ -55,11 +59,12 @@ async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(ge
|
|||||||
elif paragraph.model_id:
|
elif paragraph.model_id:
|
||||||
model = await db.get(AiModel, paragraph.model_id)
|
model = await db.get(AiModel, paragraph.model_id)
|
||||||
|
|
||||||
|
file_summaries = await asyncio.to_thread(summarize_minio_files, body.file_paths or []) if body.file_paths else []
|
||||||
if model is None or model.status != "enabled":
|
if model is None or model.status != "enabled":
|
||||||
content = build_mock_content(paragraph)
|
content = build_mock_content(paragraph)
|
||||||
message = "当前未找到可用模型,返回本地模拟生成结果。"
|
message = "当前未找到可用模型,返回本地模拟生成结果。"
|
||||||
else:
|
else:
|
||||||
result = await call_ai(paragraph, model)
|
result = await call_ai(paragraph, model, file_summaries)
|
||||||
content = result.content
|
content = result.content
|
||||||
message = f"已通过模型 {result.used_model} 生成。"
|
message = f"已通过模型 {result.used_model} 生成。"
|
||||||
return Response(
|
return Response(
|
||||||
@@ -67,6 +72,7 @@ async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(ge
|
|||||||
"paragraph_id": paragraph.id,
|
"paragraph_id": paragraph.id,
|
||||||
"content": content,
|
"content": content,
|
||||||
"message": message,
|
"message": message,
|
||||||
|
"file_summaries": file_summaries,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ def _ensure_json_content(text: str) -> dict:
|
|||||||
return {"content": [{"type": "text", "text": stripped}]}
|
return {"content": [{"type": "text", "text": stripped}]}
|
||||||
|
|
||||||
|
|
||||||
def _build_prompt(paragraph: Paragraph) -> tuple[str, str]:
|
def _build_prompt(paragraph: Paragraph, file_summaries: list[dict] | None = None) -> tuple[str, str]:
|
||||||
system_prompt = (
|
system_prompt = (
|
||||||
"你是一个企业文档撰写助手。"
|
"你是一个企业文档撰写助手。"
|
||||||
"请严格输出 JSON,不要输出 JSON 之外的说明。"
|
"请严格输出 JSON,不要输出 JSON 之外的说明。"
|
||||||
@@ -55,6 +55,11 @@ def _build_prompt(paragraph: Paragraph) -> tuple[str, str]:
|
|||||||
user_parts.append(f"模板上下文:{paragraph.content}")
|
user_parts.append(f"模板上下文:{paragraph.content}")
|
||||||
if paragraph.need_prompt and paragraph.prompt_text:
|
if paragraph.need_prompt and paragraph.prompt_text:
|
||||||
user_parts.append(f"附加要求:{paragraph.prompt_text}")
|
user_parts.append(f"附加要求:{paragraph.prompt_text}")
|
||||||
|
if file_summaries:
|
||||||
|
file_blocks = []
|
||||||
|
for item in file_summaries:
|
||||||
|
file_blocks.append(f"文件名:{item['file_name']}\n文件摘要:{item['summary']}")
|
||||||
|
user_parts.append("参考文件内容:\n" + "\n\n".join(file_blocks))
|
||||||
user_parts.append(f"输出格式:{paragraph.output_format}")
|
user_parts.append(f"输出格式:{paragraph.output_format}")
|
||||||
return system_prompt, "\n\n".join(user_parts)
|
return system_prompt, "\n\n".join(user_parts)
|
||||||
|
|
||||||
@@ -157,8 +162,8 @@ async def _call_anthropic(model: AiModel, system_prompt: str, user_prompt: str)
|
|||||||
return AiCallResult(content=_ensure_json_content(text), raw_text=text, used_model=model.name)
|
return AiCallResult(content=_ensure_json_content(text), raw_text=text, used_model=model.name)
|
||||||
|
|
||||||
|
|
||||||
async def call_ai(paragraph: Paragraph, model: AiModel) -> AiCallResult:
|
async def call_ai(paragraph: Paragraph, model: AiModel, file_summaries: list[dict] | None = None) -> AiCallResult:
|
||||||
system_prompt, user_prompt = _build_prompt(paragraph)
|
system_prompt, user_prompt = _build_prompt(paragraph, file_summaries)
|
||||||
if model.api_format == "anthropic":
|
if model.api_format == "anthropic":
|
||||||
return await _call_anthropic(model, system_prompt, user_prompt)
|
return await _call_anthropic(model, system_prompt, user_prompt)
|
||||||
return await _call_openai_compatible(model, system_prompt, user_prompt)
|
return await _call_openai_compatible(model, system_prompt, user_prompt)
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from docx import Document
|
||||||
|
|
||||||
|
from services.minio_client import download_object_bytes, split_bucket_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pypdf import PdfReader
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
PdfReader = None
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_text(content: bytes) -> str:
|
||||||
|
for encoding in ("utf-8", "utf-8-sig", "gbk", "gb18030"):
|
||||||
|
try:
|
||||||
|
return content.decode(encoding)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return content.decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize_docx(content: bytes) -> str:
|
||||||
|
doc = Document(io.BytesIO(content))
|
||||||
|
texts = [paragraph.text.strip() for paragraph in doc.paragraphs if paragraph.text.strip()]
|
||||||
|
return "\n".join(texts[:40])[:4000]
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize_csv(content: bytes) -> str:
|
||||||
|
text = _decode_text(content)
|
||||||
|
reader = csv.reader(io.StringIO(text))
|
||||||
|
rows = list(reader[:20])
|
||||||
|
return "\n".join([" | ".join(row) for row in rows])[:4000]
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize_excel(content: bytes, suffix: str) -> str:
|
||||||
|
excel_buffer = io.BytesIO(content)
|
||||||
|
sheet_map = pd.read_excel(excel_buffer, sheet_name=None) if suffix == ".xlsx" else pd.read_excel(excel_buffer, sheet_name=None, engine="xlrd")
|
||||||
|
parts: list[str] = []
|
||||||
|
for sheet_name, dataframe in list(sheet_map.items())[:5]:
|
||||||
|
preview = dataframe.head(10).fillna("").astype(str)
|
||||||
|
parts.append(f"[工作表] {sheet_name}")
|
||||||
|
parts.append(preview.to_csv(index=False).strip())
|
||||||
|
return "\n".join(parts)[:5000]
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize_pdf(content: bytes) -> str:
|
||||||
|
if PdfReader is None:
|
||||||
|
return "当前环境未安装 PDF 文本解析依赖,无法提取 PDF 正文。"
|
||||||
|
reader = PdfReader(io.BytesIO(content))
|
||||||
|
texts: list[str] = []
|
||||||
|
for page in reader.pages[:10]:
|
||||||
|
texts.append((page.extract_text() or "").strip())
|
||||||
|
return "\n".join(filter(None, texts))[:4000]
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_file_bytes(file_name: str, content: bytes) -> str:
|
||||||
|
suffix = Path(file_name).suffix.lower()
|
||||||
|
if suffix in {".txt", ".md", ".json"}:
|
||||||
|
return _decode_text(content)[:4000]
|
||||||
|
if suffix == ".csv":
|
||||||
|
return _summarize_csv(content)
|
||||||
|
if suffix in {".xlsx", ".xls"}:
|
||||||
|
return _summarize_excel(content, suffix)
|
||||||
|
if suffix == ".docx":
|
||||||
|
return _summarize_docx(content)
|
||||||
|
if suffix == ".pdf":
|
||||||
|
return _summarize_pdf(content)
|
||||||
|
return f"暂不支持解析该文件内容:{file_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_minio_files(file_paths: list[str]) -> list[dict]:
|
||||||
|
summaries: list[dict] = []
|
||||||
|
for file_path in file_paths:
|
||||||
|
bucket, object_name = split_bucket_path(file_path)
|
||||||
|
content = download_object_bytes(bucket, object_name)
|
||||||
|
file_name = Path(object_name).name
|
||||||
|
summaries.append(
|
||||||
|
{
|
||||||
|
"file_name": file_name,
|
||||||
|
"file_path": file_path,
|
||||||
|
"summary": summarize_file_bytes(file_name, content),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return summaries
|
||||||
@@ -94,3 +94,15 @@
|
|||||||
5. 重写执行生成页,使其更接近原型中的左侧模板概览 + 右侧文件配置 + 左下状态区布局,并保留真实 SSE 进度与取消生成能力。
|
5. 重写执行生成页,使其更接近原型中的左侧模板概览 + 右侧文件配置 + 左下状态区布局,并保留真实 SSE 进度与取消生成能力。
|
||||||
6. 执行前端类型检查,确认本轮页面重构稳定。
|
6. 执行前端类型检查,确认本轮页面重构稳定。
|
||||||
- **执行结果**: 现有三大主页面已开始按原型稿收口,整体信息层级和布局结构明显向原型靠齐,同时保留了当前已完成的真实业务能力。
|
- **执行结果**: 现有三大主页面已开始按原型稿收口,整体信息层级和布局结构明显向原型靠齐,同时保留了当前已完成的真实业务能力。
|
||||||
|
|
||||||
|
## 会话 ID: local-20260702161641
|
||||||
|
- [2026-07-02 16:16:41]
|
||||||
|
- **执行原因**: 用户指出模板编辑页尚未对齐原型编辑态,且“立即测试”未弹出文件选择并完成多文件测试链路。
|
||||||
|
- **执行过程**:
|
||||||
|
1. 重新核对模板管理原型中的三栏编辑态和段落测试三步弹窗交互。
|
||||||
|
2. 新增文件摘要服务,支持从 MinIO 下载并解析多种参考文件内容,包括 txt、md、csv、xlsx、xls、docx、pdf。
|
||||||
|
3. 调整 AI 调用服务,在请求模型时拼接“模板上下文 + 段落提示词 + 文件摘要”作为输入。
|
||||||
|
4. 更新段落测试接口,支持接收多文件路径、解析文件内容并把解析摘要返回前端展示。
|
||||||
|
5. 重写模板编辑页,使其更接近原型的三栏编辑结构,并实现“立即测试”三步弹窗、多文件上传、文件摘要展示和测试结果预览。
|
||||||
|
6. 增补 PDF 与老式 Excel 解析依赖,并执行后端语法检查与前端类型检查。
|
||||||
|
- **执行结果**: 模板编辑页已更接近原型编辑态,“立即测试”现支持多文件上传、文件内容解析、带提示词调用 AI 模型并返回结果。
|
||||||
|
|||||||
@@ -1,32 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="display:flex;height:calc(100vh - 112px)">
|
<div class="editor-page">
|
||||||
<div style="width:220px;border-right:1px solid #f0f0f0;overflow-y:auto;padding:12px">
|
<div class="editor-topbar">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
<a class="back-link" @click.prevent="router.push('/templates')">
|
||||||
<h4 style="margin:0">段落列表</h4>
|
<arrow-left-outlined />
|
||||||
<a-button type="primary" size="small" @click="saveTemplate">保存</a-button>
|
返回
|
||||||
|
</a>
|
||||||
|
<span class="divider">|</span>
|
||||||
|
<span class="editor-title">{{ templateName }}</span>
|
||||||
|
<span class="flex-spacer" />
|
||||||
|
<a-button type="primary" @click="saveTemplate">保存模板</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-layout">
|
||||||
|
<aside class="editor-left">
|
||||||
|
<div class="left-head">
|
||||||
|
段落列表
|
||||||
|
<span class="left-head-tip">(点击定位)</span>
|
||||||
|
</div>
|
||||||
|
<div class="left-scroll">
|
||||||
<div
|
<div
|
||||||
v-for="(p, i) in paragraphs"
|
v-for="paragraph in paragraphs"
|
||||||
:key="p.id"
|
:key="paragraph.id"
|
||||||
:class="['para-item', { active: selectedId === p.id }]"
|
:class="['para-list-item', { active: selectedId === paragraph.id }]"
|
||||||
@click="selectPara(p.id)"
|
@click="selectPara(paragraph.id)"
|
||||||
>
|
>
|
||||||
<span class="idx">{{ i + 1 }}</span>
|
<span class="pli-index">{{ paragraph.sort_index }}</span>
|
||||||
<span class="title">{{ p.title || '未命名' }}</span>
|
<span class="pli-title">{{ paragraph.title || '未命名段落' }}</span>
|
||||||
<a-tag :color="p.edit_mode === 'ai' ? 'blue' : 'orange'" style="margin-left:auto">
|
<span :class="['pli-badge', paragraph.edit_mode === 'ai' ? 'ai' : 'manual']">
|
||||||
{{ p.edit_mode === 'ai' ? 'AI' : '手动' }}
|
{{ paragraph.edit_mode === 'ai' ? 'AI 生成' : '人工编辑' }}
|
||||||
</a-tag>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;overflow-y:auto;padding:24px;background:#f0f1f3;display:flex;justify-content:center;align-items:flex-start">
|
</aside>
|
||||||
<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" />
|
|
||||||
|
<section class="editor-center">
|
||||||
|
<div class="center-toolbar">
|
||||||
|
<span class="tb-btn active"><b>B</b></span>
|
||||||
|
<span class="tb-btn"><i>I</i></span>
|
||||||
|
<span class="tb-btn"><u>U</u></span>
|
||||||
|
<span class="tb-divider" />
|
||||||
|
<span class="tb-btn"><font-size-outlined /></span>
|
||||||
|
<span class="tb-btn"><ordered-list-outlined /></span>
|
||||||
|
<span class="tb-btn"><table-outlined /></span>
|
||||||
|
<span class="tb-divider" />
|
||||||
|
<span class="tb-btn"><align-left-outlined /></span>
|
||||||
|
<span class="tb-btn"><align-center-outlined /></span>
|
||||||
|
<span class="tb-btn"><align-right-outlined /></span>
|
||||||
|
<span class="toolbar-hint">点击左侧段落或文档中的段落块查看配置</span>
|
||||||
</div>
|
</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">
|
<div class="center-scroll">
|
||||||
<h4 style="margin:0">段落配置</h4>
|
<div class="doc-edit-page">
|
||||||
<a-button @click="saveTemplate">保存配置</a-button>
|
<div v-for="paragraph in paragraphs" :key="paragraph.id" :id="`paraBlock${paragraph.id}`" :class="['para-block', { selected: selectedId === paragraph.id }]" @click="selectPara(paragraph.id)">
|
||||||
|
<span :class="['para-tag', paragraph.edit_mode === 'ai' ? 'ai' : 'manual']">
|
||||||
|
{{ paragraph.edit_mode === 'ai' ? `AI 生成${paragraph.output_format === 'table' ? ' · 表格' : ''}` : '人工编辑' }}
|
||||||
|
</span>
|
||||||
|
<div class="sec-title" :style="{ marginTop: paragraph.sort_index === 1 ? '0' : '' }">{{ paragraph.title }}</div>
|
||||||
|
<p v-if="paragraph.content">{{ paragraph.content }}</p>
|
||||||
|
<p v-else class="empty-text">点击左侧配置此段落。</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedPara">
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="editor-right">
|
||||||
|
<div class="right-head">
|
||||||
|
<span>段落配置</span>
|
||||||
|
<span class="right-head-name">{{ selectedPara?.title || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="right-scroll" v-if="selectedPara">
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="config-section-title">生成设置</div>
|
||||||
<a-form layout="vertical">
|
<a-form layout="vertical">
|
||||||
<a-form-item label="编辑方式">
|
<a-form-item label="编辑方式">
|
||||||
<a-select v-model:value="selectedPara.edit_mode">
|
<a-select v-model:value="selectedPara.edit_mode">
|
||||||
@@ -36,7 +80,7 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="selectedPara.edit_mode === 'ai'" label="生成模型">
|
<a-form-item v-if="selectedPara.edit_mode === 'ai'" label="生成模型">
|
||||||
<a-select v-model:value="selectedPara.model_id" allowClear placeholder="使用默认模型">
|
<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-option v-for="model in models" :key="model.id" :value="model.id">{{ model.name }}</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="输出格式">
|
<a-form-item label="输出格式">
|
||||||
@@ -47,51 +91,136 @@
|
|||||||
<a-select-option value="chart">图表形式</a-select-option>
|
<a-select-option value="chart">图表形式</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</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-form>
|
||||||
<a-button v-if="selectedPara.edit_mode === 'ai'" type="primary" block @click="testPara">立即测试</a-button>
|
</div>
|
||||||
|
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="config-section-title">提示词与文件</div>
|
||||||
|
<div class="toggle-row">
|
||||||
|
<span>需要提示词</span>
|
||||||
|
<a-switch v-model:checked="selectedPara.need_prompt" />
|
||||||
|
</div>
|
||||||
|
<a-textarea
|
||||||
|
v-if="selectedPara.need_prompt"
|
||||||
|
v-model:value="selectedPara.prompt_text"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="在此输入提示词..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="toggle-row file-row">
|
||||||
|
<span>需要参考文件</span>
|
||||||
|
<a-switch v-model:checked="selectedPara.need_file" />
|
||||||
|
</div>
|
||||||
|
<a-textarea
|
||||||
|
v-if="selectedPara.need_file"
|
||||||
|
v-model:value="selectedPara.file_note"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="提示用户上传什么文件"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-button v-if="selectedPara.edit_mode === 'ai'" type="primary" block @click="openTestModal">
|
||||||
|
立即测试
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-modal v-model:open="testOpen" title="段落测试" :footer="null" width="720px" @cancel="resetTestModal">
|
||||||
|
<a-steps :current="testStep" style="margin-bottom:24px">
|
||||||
|
<a-step title="上传附件" />
|
||||||
|
<a-step title="处理中" />
|
||||||
|
<a-step title="查看结果" />
|
||||||
|
</a-steps>
|
||||||
|
|
||||||
|
<div v-if="testStep === 0">
|
||||||
|
<div class="test-paragraph-card">
|
||||||
|
<div class="test-para-title">{{ selectedPara?.title }}</div>
|
||||||
|
<div class="test-para-desc">{{ selectedPara?.file_note || '可上传多个参考文件,系统会解析内容后与提示词一起发送给模型。' }}</div>
|
||||||
|
<a-upload-dragger :multiple="true" :beforeUpload="beforeTestUpload" :showUploadList="false">
|
||||||
|
<p class="ant-upload-drag-icon"><upload-outlined /></p>
|
||||||
|
<p class="ant-upload-text">点击或拖拽上传参考文件</p>
|
||||||
|
<p class="ant-upload-hint">支持多文件:docx / xlsx / xls / csv / pdf / txt / md</p>
|
||||||
|
</a-upload-dragger>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="testFiles.length" class="uploaded-list">
|
||||||
|
<div class="uploaded-item" v-for="file in testFiles" :key="file.uid">
|
||||||
|
<span class="uploaded-name">{{ file.name }}</span>
|
||||||
|
<a-button type="link" size="small" danger @click="removeTestFile(file.uid)">移除</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a-button type="primary" block :loading="testing" @click="startTest">开始测试</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="testStep === 1" class="test-processing">
|
||||||
|
<a-spin size="large" />
|
||||||
|
<p class="processing-text">{{ testStatusText }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="result-meta">
|
||||||
|
<span class="success-pill">测试完成</span>
|
||||||
|
<span class="meta-text">{{ testResultMessage }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="testFileSummaries.length" class="summary-card">
|
||||||
|
<div class="summary-title">文件解析摘要</div>
|
||||||
|
<div class="summary-item" v-for="item in testFileSummaries" :key="item.file_path">
|
||||||
|
<div class="summary-file">{{ item.file_name }}</div>
|
||||||
|
<pre class="summary-text">{{ item.summary }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card" v-html="testResultHtml" />
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { Modal, message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
AlignCenterOutlined,
|
||||||
|
AlignLeftOutlined,
|
||||||
|
AlignRightOutlined,
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
FontSizeOutlined,
|
||||||
|
OrderedListOutlined,
|
||||||
|
TableOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
import { useTemplateStore } from '@/stores/template'
|
import { useTemplateStore } from '@/stores/template'
|
||||||
import { useModelStore } from '@/stores/model'
|
import { useModelStore } from '@/stores/model'
|
||||||
import { generateApi } from '@/api/generate'
|
import { generateApi } from '@/api/generate'
|
||||||
|
|
||||||
|
interface LocalUploadFile {
|
||||||
|
uid: string
|
||||||
|
name: string
|
||||||
|
raw: File
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const store = useTemplateStore()
|
const store = useTemplateStore()
|
||||||
const modelStore = useModelStore()
|
const modelStore = useModelStore()
|
||||||
|
|
||||||
const paragraphs = ref<any[]>([])
|
const paragraphs = ref<any[]>([])
|
||||||
const models = ref<any[]>([])
|
const models = ref<any[]>([])
|
||||||
const selectedId = ref(0)
|
const selectedId = ref(0)
|
||||||
|
const templateName = ref('模板编辑')
|
||||||
|
|
||||||
|
const testOpen = ref(false)
|
||||||
|
const testStep = ref(0)
|
||||||
|
const testing = ref(false)
|
||||||
|
const testStatusText = ref('正在解析文件内容并请求 AI 模型...')
|
||||||
|
const testFiles = ref<LocalUploadFile[]>([])
|
||||||
|
const testResultHtml = ref('')
|
||||||
|
const testResultMessage = ref('')
|
||||||
|
const testFileSummaries = ref<any[]>([])
|
||||||
|
|
||||||
const selectedPara = computed(() => paragraphs.value.find((item) => item.id === selectedId.value))
|
const selectedPara = computed(() => paragraphs.value.find((item) => item.id === selectedId.value))
|
||||||
const previewHtml = computed(() =>
|
|
||||||
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) {
|
function selectPara(id: number) {
|
||||||
selectedId.value = id
|
selectedId.value = id
|
||||||
@@ -103,39 +232,109 @@ async function saveTemplate() {
|
|||||||
message.success('模板配置已保存')
|
message.success('模板配置已保存')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testPara() {
|
function openTestModal() {
|
||||||
if (!selectedPara.value) {
|
testOpen.value = true
|
||||||
|
testStep.value = 0
|
||||||
|
testing.value = false
|
||||||
|
testFiles.value = []
|
||||||
|
testResultHtml.value = ''
|
||||||
|
testResultMessage.value = ''
|
||||||
|
testFileSummaries.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTestModal() {
|
||||||
|
testOpen.value = false
|
||||||
|
testStep.value = 0
|
||||||
|
testing.value = false
|
||||||
|
testFiles.value = []
|
||||||
|
testResultHtml.value = ''
|
||||||
|
testResultMessage.value = ''
|
||||||
|
testFileSummaries.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeTestUpload(file: File) {
|
||||||
|
testFiles.value.push({
|
||||||
|
uid: `${Date.now()}-${Math.random()}`,
|
||||||
|
name: file.name,
|
||||||
|
raw: file,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTestFile(uid: string) {
|
||||||
|
testFiles.value = testFiles.value.filter((item) => item.uid !== uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTestResult(content: any) {
|
||||||
|
const blocks = content?.content || []
|
||||||
|
return blocks
|
||||||
|
.map((block: any) => {
|
||||||
|
if (block.type === 'table') {
|
||||||
|
const headers = (block.headers || []).map((item: string) => `<th>${item}</th>`).join('')
|
||||||
|
const rows = (block.rows || [])
|
||||||
|
.map((row: string[]) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join('')}</tr>`)
|
||||||
|
.join('')
|
||||||
|
return `
|
||||||
|
<div class="result-block">
|
||||||
|
<div class="result-block-title">${block.title || '表格结果'}</div>
|
||||||
|
<table class="result-table">
|
||||||
|
<thead><tr>${headers}</tr></thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
return `<p class="result-text">${block.text || ''}</p>`
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startTest() {
|
||||||
|
if (!selectedPara.value) return
|
||||||
|
if (selectedPara.value.need_file && !testFiles.value.length) {
|
||||||
|
message.warning('请先上传至少一个参考文件')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testStep.value = 1
|
||||||
|
testing.value = true
|
||||||
|
testStatusText.value = '正在上传文件...'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filePaths: string[] = []
|
||||||
|
for (const item of testFiles.value) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', item.raw)
|
||||||
|
const uploadRes: any = await generateApi.upload(formData)
|
||||||
|
filePaths.push(uploadRes.data.file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
testStatusText.value = '正在解析文件内容并请求 AI 模型...'
|
||||||
const templateId = Number(route.params.id)
|
const templateId = Number(route.params.id)
|
||||||
const response: any = await generateApi.test({
|
const response: any = await generateApi.test({
|
||||||
paragraph_id: selectedPara.value.id,
|
paragraph_id: selectedPara.value.id,
|
||||||
template_id: templateId,
|
template_id: templateId,
|
||||||
prompt_text: selectedPara.value.prompt_text || '',
|
prompt_text: selectedPara.value.prompt_text || '',
|
||||||
model_id: selectedPara.value.model_id || 0,
|
model_id: selectedPara.value.model_id || 0,
|
||||||
file_paths: [],
|
file_paths: filePaths,
|
||||||
})
|
})
|
||||||
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({
|
testResultMessage.value = response.data?.message || '测试完成'
|
||||||
title: '段落测试结果',
|
testFileSummaries.value = response.data?.file_summaries || []
|
||||||
width: 640,
|
testResultHtml.value = renderTestResult(response.data?.content)
|
||||||
content: text || '当前未返回可展示内容。',
|
testStep.value = 2
|
||||||
})
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '段落测试失败')
|
||||||
|
resetTestModal()
|
||||||
|
} finally {
|
||||||
|
testing.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const id = Number(route.params.id)
|
const id = Number(route.params.id)
|
||||||
await store.fetchOne(id)
|
const template = await store.fetchOne(id)
|
||||||
|
templateName.value = template.name
|
||||||
paragraphs.value = store.paragraphs as any
|
paragraphs.value = store.paragraphs as any
|
||||||
await modelStore.fetchList()
|
await modelStore.fetchList()
|
||||||
models.value = modelStore.models as any
|
models.value = modelStore.models as any
|
||||||
@@ -146,32 +345,255 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.para-item {
|
.editor-page {
|
||||||
|
height: calc(100vh - 52px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f5f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-topbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid #e0e2e6;
|
||||||
|
background: #fff;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #5b5bd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
color: #c4c8cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-layout {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-left {
|
||||||
|
width: 220px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-right: 1px solid #e0e2e6;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-head,
|
||||||
|
.right-head {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-head-tip,
|
||||||
|
.right-head-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #9aa1ad;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-scroll,
|
||||||
|
.right-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-center {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f5f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid #e0e2e6;
|
||||||
|
background: #fff;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-bottom: 2px;
|
color: #5b626e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-btn.active,
|
||||||
|
.tb-btn:hover {
|
||||||
|
background: #eeeefb;
|
||||||
|
color: #5b5bd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 18px;
|
||||||
|
background: #e0e2e6;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-hint {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9aa1ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-edit-page {
|
||||||
|
width: 794px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 80px 72px 120px;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-block {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-block:hover {
|
||||||
|
border-color: #e0e2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-block.selected {
|
||||||
|
border-color: #5b5bd6;
|
||||||
|
background: #eeeefb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-tag {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 4px;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-tag.ai {
|
||||||
|
background: #eeeefb;
|
||||||
|
color: #5b5bd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-tag.manual {
|
||||||
|
background: #fff3e0;
|
||||||
|
color: #e68a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sec-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 2px solid #1a1d24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: #9aa1ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-right {
|
||||||
|
width: 380px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-left: 1px solid #e0e2e6;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1d24;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.para-item:hover {
|
.file-row {
|
||||||
background: #f5f5f5;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.para-item.active {
|
.para-list-item {
|
||||||
background: #f0f0ff;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #5b626e;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-list-item:hover {
|
||||||
|
background: #f0f1f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.para-list-item.active {
|
||||||
|
background: #eeeefb;
|
||||||
color: #5b5bd6;
|
color: #5b5bd6;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.para-item .idx {
|
.pli-index {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #f0f0f0;
|
background: #f0f1f3;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -180,15 +602,163 @@ onMounted(async () => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.para-item.active .idx {
|
.para-list-item.active .pli-index {
|
||||||
background: #5b5bd6;
|
background: #5b5bd6;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.para-item .title {
|
.pli-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pli-badge {
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pli-badge.ai {
|
||||||
|
background: #eeeefb;
|
||||||
|
color: #5b5bd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pli-badge.manual {
|
||||||
|
background: #fff3e0;
|
||||||
|
color: #e68a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-paragraph-card {
|
||||||
|
background: #f0f1f3;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-para-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-para-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5b626e;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-list {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #f5f6f8;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-name {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-processing {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processing-text {
|
||||||
|
margin-top: 16px;
|
||||||
|
color: #5b626e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-pill {
|
||||||
|
background: #1a8c4a;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5b626e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
background: #f5f6f8;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-file {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5b626e;
|
||||||
|
background: #fff;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
background: #f0f0ff;
|
||||||
|
padding: 16px;
|
||||||
|
border-left: 3px solid #5b5bd6;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.result-text) {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.result-block-title) {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.result-table) {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.result-table th),
|
||||||
|
:deep(.result-table td) {
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user