diff --git a/docs/tasks/task_detail_2026_07_02.md b/docs/tasks/task_detail_2026_07_02.md
index b0be953..da9f033 100644
--- a/docs/tasks/task_detail_2026_07_02.md
+++ b/docs/tasks/task_detail_2026_07_02.md
@@ -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. 执行前端类型检查,确认本轮布局调整稳定。
+- **执行结果**: 当前任务详情页已改为“上 + 左右”结构,用户可在左侧切换段落,在右侧查看对应生成结果预览。
diff --git a/docs/tasks/task_detail_2026_07_03.md b/docs/tasks/task_detail_2026_07_03.md
new file mode 100644
index 0000000..93f6985
--- /dev/null
+++ b/docs/tasks/task_detail_2026_07_03.md
@@ -0,0 +1,10 @@
+# 任务执行摘要
+
+## 会话 ID: local-20260703092828
+- [2026-07-03 09:28:28]
+- **执行原因**: 用户要求提交当前已完成的预览页布局调整代码。
+- **执行过程**:
+ 1. 检查当前工作区改动,确认仅包含任务详情页左右布局调整和对应任务记录。
+ 2. 整理并暂存相关文件,排除未跟踪的原型目录。
+ 3. 准备使用中文提交信息完成本次代码提交。
+- **执行结果**: 当前改动已整理完成并准备提交,包含任务详情页“上 + 左右”结构调整。
diff --git a/web/src/views/PreviewEdit.vue b/web/src/views/PreviewEdit.vue
index e7fab52..8b68697 100644
--- a/web/src/views/PreviewEdit.vue
+++ b/web/src/views/PreviewEdit.vue
@@ -33,37 +33,55 @@
{{ progressMessage || documentInfo.error || '任务已创建,等待执行。' }}
-
-
-
-
-
{{ item.sort_index }}
-
-
{{ item.title }}
-
{{ item.file_note }}
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
{{ selectedParagraph.title }}
+
+
+
文件要求:{{ selectedMapping.file_note }}
+
+ {{ file.file_name }}
+
+
+
+
+
+
+
+
@@ -96,11 +114,21 @@ const documentInfo = ref
({})
const logs = ref([])
const progressPercent = ref(0)
const progressMessage = ref('')
+const selectedParagraphId = ref(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 = {
@@ -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;