feat: upload review mind map files
This commit is contained in:
@@ -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' } },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
};
|
||||
|
||||
+149
-45
@@ -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<ReviewMindMap | null>(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<ReviewApplication["status"], string> = {
|
||||
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(
|
||||
<article class="surface-card extension-card">
|
||||
<div class="extension-head">
|
||||
<h3>思维导图</h3>
|
||||
<el-tag size="small" type="warning" effect="plain">{{ mindMap?.contentFormat || mindMapForm.contentFormat }}</el-tag>
|
||||
<el-tag size="small" :type="mindMapStatusType" effect="plain">
|
||||
{{ mindMap?.parseStatus || "未上传" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="mind-map-form">
|
||||
<div class="form-row">
|
||||
<el-input v-model="mindMapForm.title" placeholder="标题" />
|
||||
<el-select v-model="mindMapForm.contentFormat" placeholder="格式">
|
||||
<el-option label="文本" value="TEXT" />
|
||||
<el-option label="Mermaid" value="MERMAID" />
|
||||
<el-option label="JSON" value="JSON" />
|
||||
</el-select>
|
||||
<div class="mind-map-upload-row">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept=".xmind,.md,.markdown,.opml,.mm,.txt"
|
||||
:on-change="handleMindMapFileChange"
|
||||
>
|
||||
<el-button type="success" :loading="mindMapUploading">上传导图文件</el-button>
|
||||
</el-upload>
|
||||
<el-tag v-if="mindMap?.fileFormat" size="small" effect="plain">
|
||||
{{ mindMap.fileFormat }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="mindMapForm.content"
|
||||
type="textarea"
|
||||
:rows="12"
|
||||
placeholder="思维导图内容"
|
||||
/>
|
||||
<div class="edit-actions">
|
||||
<el-button type="success" :loading="mindMapSaving" @click="saveMindMap">保存</el-button>
|
||||
|
||||
<div v-if="mindMap" class="mind-map-preview">
|
||||
<div class="mind-map-meta">
|
||||
<strong>{{ mindMap.title }}</strong>
|
||||
<span v-if="mindMap.fileName">{{ mindMap.fileName }}</span>
|
||||
</div>
|
||||
<p v-if="mindMap.summary" class="mind-map-summary">{{ mindMap.summary }}</p>
|
||||
<p v-if="mindMap.parseStatus === 'FAILED'" class="mind-map-error">
|
||||
{{ mindMap.parseError || "解析失败" }}
|
||||
</p>
|
||||
<div v-if="flattenedMindMapNodes.length > 0" class="mind-map-outline">
|
||||
<div
|
||||
v-for="(node, idx) in flattenedMindMapNodes"
|
||||
:key="`${idx}-${node.title}`"
|
||||
class="mind-map-node"
|
||||
:style="{ paddingLeft: `${node.level * 16}px` }"
|
||||
>
|
||||
{{ node.title }}
|
||||
</div>
|
||||
</div>
|
||||
<pre v-else-if="mindMap.content" class="mind-map-content">{{ mindMap.content }}</pre>
|
||||
</div>
|
||||
<el-empty v-else description="暂无思维导图" :image-size="64" />
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user