调整任务详情页左右预览布局

This commit is contained in:
zwt13703
2026-07-03 09:28:39 +08:00
parent ff6dacd136
commit 52eb058070
3 changed files with 148 additions and 34 deletions
+18
View File
@@ -219,3 +219,21 @@
4. 调整导出时的表格回写方式,优先克隆模板原有表格结构并填充新数据,尽量保留表格外观和基础样式。
5. 执行后端语法检查,确认导出服务改动稳定。
- **执行结果**: 当前导出链路已改为“尽量复用模板原始段落/表格样式后写入 AI 内容”,比此前的新建空白内容块方式更接近原模板样式,也更容易把 AI 结果正确写回文档。
## 会话 ID: local-20260702202424
- [2026-07-02 20:24:24]
- **执行原因**: 用户希望用极简一句话概括当前项目功能状态,并说明明天的工作重点。
- **执行过程**:
1. 基于当前已完成能力,压缩总结项目现状。
2. 提炼明日优先事项,聚焦导出准确性与样式保真。
- **执行结果**: 已形成简短状态说明,可作为明日继续开发的工作摘要。
## 会话 ID: local-20260702202807
- [2026-07-02 20:28:07]
- **执行原因**: 用户希望将任务详情页改为“上 + 左右”结构,左侧显示段落,右侧显示生成结果预览。
- **执行过程**:
1. 重构任务详情页布局,将页面调整为顶部任务概览、下方左右分栏的结构。
2. 左侧增加段落列表与段落状态展示,并保留附件映射信息。
3. 右侧改为当前选中段落的结果预览区域,展示段落标题、状态、文件要求和生成内容。
4. 执行前端类型检查,确认本轮布局调整稳定。
- **执行结果**: 当前任务详情页已改为“上 + 左右”结构,用户可在左侧切换段落,在右侧查看对应生成结果预览。
+10
View File
@@ -0,0 +1,10 @@
# 任务执行摘要
## 会话 ID: local-20260703092828
- [2026-07-03 09:28:28]
- **执行原因**: 用户要求提交当前已完成的预览页布局调整代码。
- **执行过程**:
1. 检查当前工作区改动,确认仅包含任务详情页左右布局调整和对应任务记录。
2. 整理并暂存相关文件,排除未跟踪的原型目录。
3. 准备使用中文提交信息完成本次代码提交。
- **执行结果**: 当前改动已整理完成并准备提交,包含任务详情页“上 + 左右”结构调整。
+120 -34
View File
@@ -33,37 +33,55 @@
<div class="status-message">{{ progressMessage || documentInfo.error || '任务已创建,等待执行。' }}</div>
</a-card>
<a-card class="mapping-card" title="段落与附件映射">
<div v-if="paragraphMappings.length" class="mapping-list">
<div v-for="item in paragraphMappings" :key="item.paragraph_id" class="mapping-item">
<div class="mapping-top">
<span class="mapping-index">{{ item.sort_index }}</span>
<div class="mapping-main">
<div class="mapping-title">{{ item.title }}</div>
<div v-if="item.file_note" class="mapping-note">{{ item.file_note }}</div>
<div class="detail-layout">
<aside class="detail-left">
<a-card class="left-card" title="段落列表">
<div v-if="paragraphMappings.length" class="mapping-list">
<div
v-for="item in paragraphMappings"
:key="item.paragraph_id"
:class="['mapping-item', { active: selectedParagraphId === item.paragraph_id }]"
@click="selectParagraph(item.paragraph_id)"
>
<div class="mapping-top">
<span class="mapping-index">{{ item.sort_index }}</span>
<div class="mapping-main">
<div class="mapping-title">{{ item.title }}</div>
<div v-if="item.file_note" class="mapping-note">{{ item.file_note }}</div>
</div>
</div>
<div class="mapping-status">
<a-badge :status="statusBadge(logStatusMap[item.paragraph_id] || 'pending')" :text="statusText(logStatusMap[item.paragraph_id] || 'pending')" />
</div>
<div v-if="item.selected_files?.length" class="mapping-files">
<span v-for="file in item.selected_files" :key="file.file_path" class="mapping-file">{{ file.file_name }}</span>
</div>
<div v-else class="mapping-empty">未选择附件</div>
</div>
</div>
<div v-if="item.selected_files?.length" class="mapping-files">
<span v-for="file in item.selected_files" :key="file.file_path" class="mapping-file">{{ file.file_name }}</span>
</div>
<div v-else class="mapping-empty">未选择附件</div>
</div>
</div>
<a-empty v-else description="当前任务未记录附件映射" />
</a-card>
<a-empty v-else description="当前任务未记录段落信息" />
</a-card>
</aside>
<a-card class="preview-card" title="生成结果预览">
<div v-if="logs.length" class="preview-wrap">
<section v-for="item in logs" :key="item.paragraph_id" :id="`section-${item.paragraph_id}`" class="preview-section">
<div class="preview-section-head">
<h3>{{ item.title }}</h3>
<a-badge :status="statusBadge(item.status)" :text="statusText(item.status)" />
</div>
<div class="preview-block" v-html="renderBlocks(item.content?.content || [])" />
</section>
</div>
<a-empty v-else :description="isRunning ? '任务进行中,已完成段落会逐步出现在这里' : '暂无生成内容'" />
</a-card>
<section class="detail-right">
<a-card class="preview-card" title="生成结果预览">
<template v-if="selectedParagraph">
<div class="preview-section">
<div class="preview-section-head">
<h3>{{ selectedParagraph.title }}</h3>
<a-badge :status="statusBadge(selectedParagraph.status)" :text="statusText(selectedParagraph.status)" />
</div>
<div v-if="selectedMapping?.file_note" class="preview-note">文件要求{{ selectedMapping.file_note }}</div>
<div v-if="selectedMapping?.selected_files?.length" class="preview-files">
<span v-for="file in selectedMapping.selected_files" :key="file.file_path" class="mapping-file">{{ file.file_name }}</span>
</div>
<div class="preview-block" v-html="renderBlocks(selectedParagraph.content?.content || [])" />
</div>
</template>
<a-empty v-else :description="isRunning ? '任务进行中,当前段落结果尚未生成' : '当前没有可预览的段落结果'" />
</a-card>
</section>
</div>
</div>
</template>
@@ -96,11 +114,21 @@ const documentInfo = ref<any>({})
const logs = ref<LogItem[]>([])
const progressPercent = ref(0)
const progressMessage = ref('')
const selectedParagraphId = ref<number | null>(null)
let progressSource: EventSource | null = null
const isRunning = computed(() => ['pending', 'generating'].includes(documentInfo.value.status))
const isCompleted = computed(() => documentInfo.value.status === 'completed')
const paragraphMappings = computed(() => documentInfo.value.request_payload?.paragraphs || [])
const logStatusMap = computed(() =>
Object.fromEntries(logs.value.map((item) => [item.paragraph_id, item.status]))
)
const selectedParagraph = computed(() =>
logs.value.find((item) => item.paragraph_id === selectedParagraphId.value) || null
)
const selectedMapping = computed(() =>
paragraphMappings.value.find((item: any) => item.paragraph_id === selectedParagraphId.value) || null
)
function statusText(status: string) {
const map: Record<string, string> = {
@@ -145,6 +173,20 @@ async function loadDocument() {
const response: any = await generateApi.getDocument(id)
documentInfo.value = response.data || {}
logs.value = response.data?.logs || []
if (!selectedParagraphId.value) {
selectedParagraphId.value = logs.value[0]?.paragraph_id || paragraphMappings.value[0]?.paragraph_id || null
}
if (
selectedParagraphId.value &&
!paragraphMappings.value.some((item: any) => item.paragraph_id === selectedParagraphId.value) &&
!logs.value.some((item) => item.paragraph_id === selectedParagraphId.value)
) {
selectedParagraphId.value = logs.value[0]?.paragraph_id || paragraphMappings.value[0]?.paragraph_id || null
}
}
function selectParagraph(paragraphId: number) {
selectedParagraphId.value = paragraphId
}
function bindProgress() {
@@ -254,12 +296,31 @@ onBeforeUnmount(() => {
}
.status-card,
.mapping-card,
.preview-card {
margin-bottom: 16px;
border-radius: 18px;
}
.detail-layout {
display: flex;
gap: 16px;
align-items: flex-start;
}
.detail-left {
width: 360px;
flex-shrink: 0;
}
.detail-right {
min-width: 0;
flex: 1;
}
.left-card {
border-radius: 18px;
}
.status-message {
margin-top: 12px;
font-size: 13px;
@@ -276,6 +337,18 @@ onBeforeUnmount(() => {
border: 1px solid #e5e7eb;
border-radius: 14px;
background: #fafbfc;
cursor: pointer;
transition: all 0.15s ease;
}
.mapping-item:hover {
border-color: #c7d2fe;
background: #f8faff;
}
.mapping-item.active {
border-color: #4f46e5;
background: #eef2ff;
}
.mapping-top {
@@ -312,6 +385,10 @@ onBeforeUnmount(() => {
color: #6b7280;
}
.mapping-status {
margin-top: 10px;
}
.mapping-files {
display: flex;
flex-wrap: wrap;
@@ -334,16 +411,12 @@ onBeforeUnmount(() => {
color: #9ca3af;
}
.preview-wrap {
display: grid;
gap: 16px;
}
.preview-section {
padding: 16px;
border-radius: 14px;
background: #fff;
border: 1px solid #e5e7eb;
min-height: 480px;
}
.preview-section-head {
@@ -358,6 +431,19 @@ onBeforeUnmount(() => {
margin: 0;
}
.preview-note {
margin-bottom: 12px;
font-size: 13px;
color: #6b7280;
}
.preview-files {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
:deep(.result-text) {
margin: 0 0 12px;
line-height: 1.8;