From dc7469ec3f1e990b08ea12c0321acb36e8f541cb Mon Sep 17 00:00:00 2001 From: cat_shark <1716967236@qq.com> Date: Sat, 20 Jun 2026 10:33:14 +0800 Subject: [PATCH] feat: upload review mind map files --- src/__tests__/api/review.spec.ts | 10 ++ src/api/review.ts | 17 +++ src/components/ReviewDetail.vue | 194 ++++++++++++++++++++++++------- 3 files changed, 176 insertions(+), 45 deletions(-) diff --git a/src/__tests__/api/review.spec.ts b/src/__tests__/api/review.spec.ts index 7e7c661..572be5c 100644 --- a/src/__tests__/api/review.spec.ts +++ b/src/__tests__/api/review.spec.ts @@ -23,6 +23,7 @@ import { getTaskReview, updateReviewApplication, upsertReviewMindMap, + uploadReviewMindMap, } from '@/api/review' const mockedRequest = vi.mocked(request) @@ -111,6 +112,7 @@ describe('review API', () => { it('review mind map API calls expected endpoints', async () => { mockedRequest.get.mockResolvedValue({ code: 200, data: null }) mockedRequest.put.mockResolvedValue({ code: 200, data: { id: 1 } }) + mockedRequest.post.mockResolvedValue({ code: 200, data: { id: 2 } }) await getReviewMindMap('T001') expect(mockedRequest.get).toHaveBeenCalledWith('/review/mind-map/T001') @@ -125,5 +127,13 @@ describe('review API', () => { content: 'content', contentFormat: 'TEXT', }) + + const file = new File(['# Java'], 'java.md', { type: 'text/markdown' }) + await uploadReviewMindMap('T001', file) + expect(mockedRequest.post).toHaveBeenCalledWith( + '/review/mind-map/T001/upload', + expect.any(FormData), + { headers: { 'Content-Type': 'multipart/form-data' } }, + ) }) }) diff --git a/src/api/review.ts b/src/api/review.ts index 923f162..b462957 100644 --- a/src/api/review.ts +++ b/src/api/review.ts @@ -40,6 +40,15 @@ export interface ReviewMindMap { title: string; content: string; contentFormat: "TEXT" | "MERMAID" | "JSON"; + sourceType?: "MANUAL" | "FILE"; + fileName?: string; + filePath?: string; + fileFormat?: "XMIND" | "MARKDOWN" | "OPML" | "FREEMIND" | "TEXT"; + parsedContent?: string; + parseStatus?: "SUCCESS" | "FAILED"; + parseError?: string; + summary?: string; + lastParsedTime?: string; createdTime?: string; lastModifiedTime?: string; } @@ -112,3 +121,11 @@ export const upsertReviewMindMap = ( ) => { return request.put(`/review/mind-map/${taskNum}`, payload); }; + +export const uploadReviewMindMap = (taskNum: string, file: File) => { + const formData = new FormData(); + formData.append("file", file); + return request.post(`/review/mind-map/${taskNum}/upload`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); +}; diff --git a/src/components/ReviewDetail.vue b/src/components/ReviewDetail.vue index 2e473e1..0c18a0e 100644 --- a/src/components/ReviewDetail.vue +++ b/src/components/ReviewDetail.vue @@ -10,7 +10,7 @@ import { getReviewMindMap, getTaskReview, updateReviewApplication, - upsertReviewMindMap, + uploadReviewMindMap, type ReviewApplication, type ReviewFeedItem, type ReviewMindMap, @@ -50,16 +50,7 @@ const applicationForm = ref<{ }); const mindMap = ref(null); -const mindMapSaving = ref(false); -const mindMapForm = ref<{ - title: string; - content: string; - contentFormat: ReviewMindMap["contentFormat"]; -}>({ - title: "任务思维导图", - content: "", - contentFormat: "TEXT", -}); +const mindMapUploading = ref(false); const sessionInfo = ref<{ taskName: string; @@ -83,6 +74,40 @@ const statusText: Record = { DONE: "已完成", }; +type MindMapNode = { + title?: string; + notes?: string; + children?: MindMapNode[]; +}; + +const flattenedMindMapNodes = computed(() => { + const parsedContent = mindMap.value?.parsedContent; + if (!parsedContent) { + return []; + } + try { + const parsed = JSON.parse(parsedContent) as MindMapNode & { nodes?: MindMapNode[] }; + const rows: { title: string; level: number }[] = []; + const walk = (node: MindMapNode, level: number) => { + if (node.title) { + rows.push({ title: node.title, level }); + } + (node.children || []).forEach(child => walk(child, level + 1)); + }; + rows.push({ title: parsed.title || mindMap.value?.title || "思维导图", level: 0 }); + (parsed.nodes || parsed.children || []).forEach(node => walk(node, 1)); + return rows; + } catch { + return []; + } +}); + +const mindMapStatusType = computed(() => { + if (mindMap.value?.parseStatus === "FAILED") return "danger"; + if (mindMap.value?.parseStatus === "SUCCESS") return "success"; + return "info"; +}); + const formatDuration = (seconds?: number): string => { if (seconds == null) return "-"; const h = Math.floor(seconds / 3600); @@ -146,17 +171,6 @@ const loadReviewExtensions = async (tn: string) => { const mindMapRes = await getReviewMindMap(tn).catch(() => null); const loadedMindMap: ReviewMindMap | null = mindMapRes?.data || null; mindMap.value = loadedMindMap; - mindMapForm.value = loadedMindMap - ? { - title: loadedMindMap.title || "任务思维导图", - content: loadedMindMap.content || "", - contentFormat: loadedMindMap.contentFormat || "TEXT", - } - : { - title: `${sessionInfo.value?.taskName || "任务"}思维导图`, - content: "", - contentFormat: "TEXT", - }; }; const loadDetail = async () => { @@ -298,22 +312,26 @@ const removeApplication = async (item: ReviewApplication) => { } }; -const saveMindMap = async () => { +const handleMindMapFileChange = async (uploadFile: any) => { if (!taskNum.value) { ElMessage.warning("未找到关联任务"); return; } - if (!mindMapForm.value.title.trim() || !mindMapForm.value.content.trim()) { - ElMessage.warning("思维导图标题和内容不能为空"); + const file = uploadFile.raw as File | undefined; + if (!file) { return; } - mindMapSaving.value = true; + mindMapUploading.value = true; try { - const res = await upsertReviewMindMap(taskNum.value, mindMapForm.value); + const res = await uploadReviewMindMap(taskNum.value, file); mindMap.value = res?.data || null; - ElMessage.success("思维导图已保存"); + if (mindMap.value?.parseStatus === "FAILED") { + ElMessage.warning("文件已上传,但解析失败"); + } else { + ElMessage.success("思维导图已解析"); + } } finally { - mindMapSaving.value = false; + mindMapUploading.value = false; } }; @@ -434,27 +452,48 @@ watch(

思维导图

- {{ mindMap?.contentFormat || mindMapForm.contentFormat }} + + {{ mindMap?.parseStatus || "未上传" }} +
-
- - - - - - +
+ + 上传导图文件 + + + {{ mindMap.fileFormat }} +
- -
- 保存 + +
+
+ {{ mindMap.title }} + {{ mindMap.fileName }} +
+

{{ mindMap.summary }}

+

+ {{ mindMap.parseError || "解析失败" }} +

+
+
+ {{ node.title }} +
+
+
{{ mindMap.content }}
+
@@ -636,6 +675,71 @@ watch( gap: 10px; } +.mind-map-upload-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.mind-map-preview { + border: 1px solid var(--border-soft); + border-radius: 8px; + background: #fbfefc; + padding: 12px; +} + +.mind-map-meta { + display: flex; + flex-direction: column; + gap: 4px; +} + +.mind-map-meta strong { + color: var(--text-primary); + word-break: break-word; +} + +.mind-map-meta span, +.mind-map-summary { + color: var(--text-secondary); + font-size: 13px; + word-break: break-word; +} + +.mind-map-summary, +.mind-map-error { + margin: 8px 0 0; +} + +.mind-map-error { + color: #c62828; + font-size: 13px; + word-break: break-word; +} + +.mind-map-outline { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.mind-map-node { + font-size: 13px; + line-height: 1.5; + color: var(--text-primary); + word-break: break-word; +} + +.mind-map-content { + margin: 10px 0 0; + white-space: pre-wrap; + word-break: break-word; + font-size: 13px; + color: var(--text-primary); +} + .form-row { display: flex; align-items: center;