refactor: 整合知识导图接口

将思维导图生成、查询和文件列表接口收敛到知识库 API,并移除独立 mindmap router 与前端模块。
This commit is contained in:
Wenjie Zhang 2026-05-26 19:38:44 +08:00
parent b08ddc118f
commit 7b1b326523
10 changed files with 379 additions and 284 deletions

View File

@ -4,6 +4,11 @@ import json
import textwrap
from typing import Any
from yuxi import config, knowledge_base
from yuxi.models import select_model
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
from yuxi.utils import logger
MINDMAP_SYSTEM_PROMPT = """你是一个专业的知识整理助手。
你的任务是分析用户提供的文件列表生成一个层次分明的思维导图结构
@ -58,6 +63,18 @@ MINDMAP_SYSTEM_PROMPT = """你是一个专业的知识整理助手。
"""
class MindmapNotFoundError(ValueError):
pass
class MindmapValidationError(ValueError):
pass
class MindmapGenerationError(ValueError):
pass
def build_database_file_list(files: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
return [
{
@ -114,3 +131,115 @@ def parse_mindmap_content(content: str) -> dict[str, Any]:
if not isinstance(mindmap_data, dict) or "content" not in mindmap_data:
raise ValueError("思维导图结构不正确")
return mindmap_data
async def get_mindmap_database_files(kb_id: str) -> dict[str, Any]:
db_info = await knowledge_base.get_database_info(kb_id)
if not db_info:
raise MindmapNotFoundError(f"知识库 {kb_id} 不存在")
file_list = build_database_file_list(db_info.get("files", {}))
return {
"message": "success",
"kb_id": kb_id,
"slug": kb_id,
"db_name": db_info.get("name", ""),
"files": file_list,
"total": len(file_list),
}
async def generate_database_mindmap(
kb_id: str, file_ids: list[str] | None = None, user_prompt: str = ""
) -> dict[str, Any]:
db_info = await knowledge_base.get_database_info(kb_id)
if not db_info:
raise MindmapNotFoundError(f"知识库 {kb_id} 不存在")
db_name = db_info.get("name", "知识库")
all_files = db_info.get("files", {})
selected_file_ids = list(file_ids or all_files.keys())
if not selected_file_ids:
raise MindmapValidationError("知识库中没有文件")
original_count = len(selected_file_ids)
if len(selected_file_ids) > 20:
selected_file_ids = selected_file_ids[:20]
logger.info(f"文件数量超过限制,已从{original_count}个文件中选择前20个文件生成思维导图")
files_info = collect_mindmap_files(all_files, selected_file_ids)
if not files_info:
raise MindmapValidationError("选择的文件不存在")
logger.info(f"开始生成思维导图,知识库: {db_name}, 文件数量: {len(files_info)}")
model = select_model(model_spec=config.default_model)
messages = [
{"role": "system", "content": MINDMAP_SYSTEM_PROMPT},
{"role": "user", "content": build_mindmap_user_message(db_name, files_info, user_prompt)},
]
response = await model.call(messages, stream=False)
content = response.content if hasattr(response, "content") else str(response)
try:
mindmap_data = parse_mindmap_content(content)
except ValueError as e:
logger.error(f"AI返回的JSON解析失败: {e}, 原始内容: {content}")
raise MindmapGenerationError(f"AI返回格式错误: {str(e)}") from e
logger.info("思维导图生成成功")
try:
await KnowledgeBaseRepository().update(kb_id, {"mindmap": mindmap_data})
logger.info(f"思维导图已保存到知识库: {kb_id}")
except Exception as save_error:
logger.error(f"保存思维导图失败: {save_error}")
return {
"message": "success",
"mindmap": mindmap_data,
"kb_id": kb_id,
"slug": kb_id,
"db_name": db_name,
"file_count": len(files_info),
"original_file_count": original_count,
"truncated": len(files_info) < original_count,
}
async def get_mindmap_databases_overview(uid: str) -> dict[str, Any]:
databases = await knowledge_base.get_databases_by_uid(uid)
db_list = []
for db_info in databases.get("databases", []):
kb_id = db_info.get("kb_id") or db_info.get("slug")
if not kb_id:
continue
detail_info = await knowledge_base.get_database_info(kb_id)
file_count = len(detail_info.get("files", {})) if detail_info else 0
db_list.append(
{
"kb_id": kb_id,
"slug": kb_id,
"name": db_info.get("name", ""),
"description": db_info.get("description", ""),
"kb_type": db_info.get("kb_type", ""),
"file_count": file_count,
}
)
return {"message": "success", "databases": db_list, "total": len(db_list)}
async def get_database_mindmap_data(kb_id: str) -> dict[str, Any]:
kb = await KnowledgeBaseRepository().get_by_kb_id(kb_id)
if kb is None:
raise MindmapNotFoundError(f"知识库 {kb_id} 不存在")
return {
"message": "success",
"mindmap": kb.mindmap,
"kb_id": kb_id,
"slug": kb_id,
"db_name": kb.name,
}

View File

@ -47,10 +47,8 @@ if not _LITE_MODE:
from server.routers.graph_router import graph
from server.routers.knowledge_router import knowledge
from server.routers.knowledge_eval_router import evaluation
from server.routers.knowledge_mindmap_router import mindmap
# 知识库与图谱能力依赖较重LITE 模式下跳过这组接口。
router.include_router(knowledge) # /api/knowledge/* 知识库管理与检索
router.include_router(evaluation) # /api/evaluation/* 知识库评估
router.include_router(mindmap) # /api/mindmap/* 思维导图生成与查询
router.include_router(graph) # /api/graph/* 图谱查询与管理

View File

@ -1,178 +0,0 @@
"""
知识库思维导图路由模块
提供思维导图相关的 API 接口包括
- 获取知识库文件列表
- AI 生成思维导图
- 保存和加载思维导图配置
"""
import traceback
from fastapi import APIRouter, Body, Depends, HTTPException
from server.utils.auth_middleware import get_admin_user
from yuxi import config, knowledge_base
from yuxi.knowledge.utils.mindmap_utils import (
MINDMAP_SYSTEM_PROMPT,
build_database_file_list,
build_mindmap_user_message,
collect_mindmap_files,
parse_mindmap_content,
)
from yuxi.models import select_model
from yuxi.storage.postgres.models_business import User
from yuxi.utils import logger
mindmap = APIRouter(prefix="/mindmap", tags=["mindmap"])
@mindmap.get("/databases/{kb_id}/files")
async def get_database_files(kb_id: str, current_user: User = Depends(get_admin_user)):
"""获取指定知识库的所有文件列表。"""
try:
db_info = await knowledge_base.get_database_info(kb_id)
if not db_info:
raise HTTPException(status_code=404, detail=f"知识库 {kb_id} 不存在")
file_list = build_database_file_list(db_info.get("files", {}))
return {
"message": "success",
"kb_id": kb_id,
"db_name": db_info.get("name", ""),
"files": file_list,
"total": len(file_list),
}
except HTTPException:
raise
except Exception as e:
logger.error(f"获取知识库文件列表失败: {e}, {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"获取文件列表失败: {str(e)}")
@mindmap.post("/generate")
async def generate_mindmap(
kb_id: str = Body(..., description="知识库ID"),
file_ids: list[str] = Body(default=[], description="选择的文件ID列表"),
user_prompt: str = Body(default="", description="用户自定义提示词"),
current_user: User = Depends(get_admin_user),
):
"""使用 AI 分析知识库文件,生成思维导图结构。"""
try:
db_info = await knowledge_base.get_database_info(kb_id)
if not db_info:
raise HTTPException(status_code=404, detail=f"知识库 {kb_id} 不存在")
db_name = db_info.get("name", "知识库")
all_files = db_info.get("files", {})
if not file_ids:
file_ids = list(all_files.keys())
if not file_ids:
raise HTTPException(status_code=400, detail="知识库中没有文件")
original_count = len(file_ids)
if len(file_ids) > 20:
file_ids = file_ids[:20]
logger.info(f"文件数量超过限制,已从{original_count}个文件中选择前20个文件生成思维导图")
files_info = collect_mindmap_files(all_files, file_ids)
if not files_info:
raise HTTPException(status_code=400, detail="选择的文件不存在")
logger.info(f"开始生成思维导图,知识库: {db_name}, 文件数量: {len(files_info)}")
model = select_model(model_spec=config.default_model)
messages = [
{"role": "system", "content": MINDMAP_SYSTEM_PROMPT},
{"role": "user", "content": build_mindmap_user_message(db_name, files_info, user_prompt)},
]
response = await model.call(messages, stream=False)
content = response.content if hasattr(response, "content") else str(response)
try:
mindmap_data = parse_mindmap_content(content)
except ValueError as e:
logger.error(f"AI返回的JSON解析失败: {e}, 原始内容: {content}")
raise HTTPException(status_code=500, detail=f"AI返回格式错误: {str(e)}")
logger.info("思维导图生成成功")
try:
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
await KnowledgeBaseRepository().update(kb_id, {"mindmap": mindmap_data})
logger.info(f"思维导图已保存到知识库: {kb_id}")
except Exception as save_error:
logger.error(f"保存思维导图失败: {save_error}")
return {
"message": "success",
"mindmap": mindmap_data,
"kb_id": kb_id,
"db_name": db_name,
"file_count": len(files_info),
"original_file_count": original_count,
"truncated": len(files_info) < original_count,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"生成思维导图失败: {e}, {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"生成思维导图失败: {str(e)}")
@mindmap.get("/databases")
async def get_databases_overview(current_user: User = Depends(get_admin_user)):
"""获取所有知识库的概览信息,用于思维导图界面选择。"""
try:
databases = await knowledge_base.get_databases_by_uid(current_user.uid)
db_list = []
for db_info in databases.get("databases", []):
kb_id = db_info.get("kb_id")
if not kb_id:
continue
detail_info = await knowledge_base.get_database_info(kb_id)
file_count = len(detail_info.get("files", {})) if detail_info else 0
db_list.append(
{
"kb_id": kb_id,
"name": db_info.get("name", ""),
"description": db_info.get("description", ""),
"kb_type": db_info.get("kb_type", ""),
"file_count": file_count,
}
)
return {"message": "success", "databases": db_list, "total": len(db_list)}
except Exception as e:
logger.error(f"获取知识库列表失败: {e}, {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"获取知识库列表失败: {str(e)}")
@mindmap.get("/database/{kb_id}")
async def get_database_mindmap(kb_id: str, current_user: User = Depends(get_admin_user)):
"""获取知识库关联的思维导图。"""
try:
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
kb = await KnowledgeBaseRepository().get_by_kb_id(kb_id)
if kb is None:
raise HTTPException(status_code=404, detail=f"知识库 {kb_id} 不存在")
return {
"message": "success",
"mindmap": kb.mindmap,
"kb_id": kb_id,
"db_name": kb.name,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"获取知识库思维导图失败: {e}, {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"获取思维导图失败: {str(e)}")

View File

@ -20,6 +20,15 @@ from yuxi.knowledge.graphs.milvus_graph_service import GRAPH_TASK_TYPE, MilvusGr
from yuxi.plugins.parser import Parser, SUPPORTED_FILE_EXTENSIONS, is_supported_file_extension
from yuxi.knowledge.utils import calculate_content_hash
from yuxi.knowledge.utils.kb_utils import is_minio_url, parse_minio_url
from yuxi.knowledge.utils.mindmap_utils import (
MindmapGenerationError,
MindmapNotFoundError,
MindmapValidationError,
generate_database_mindmap,
get_database_mindmap_data,
get_mindmap_database_files,
get_mindmap_databases_overview,
)
from yuxi.services.model_cache import model_cache
from yuxi.services.workspace_service import MAX_WORKSPACE_UPLOAD_SIZE_BYTES, resolve_workspace_file_path
from yuxi.storage.postgres.models_business import User
@ -229,6 +238,58 @@ async def get_accessible_databases(current_user: User = Depends(get_required_use
return {"message": f"获取可访问知识库列表失败: {str(e)}", "databases": []}
def _raise_mindmap_http_exception(error: Exception, operation: str) -> None:
if isinstance(error, MindmapNotFoundError):
raise HTTPException(status_code=404, detail=str(error))
if isinstance(error, MindmapValidationError):
raise HTTPException(status_code=400, detail=str(error))
if isinstance(error, MindmapGenerationError):
raise HTTPException(status_code=500, detail=str(error))
logger.error(f"{operation}失败: {error}, {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"{operation}失败: {str(error)}")
@knowledge.get("/mindmap/databases")
async def get_mindmap_databases(current_user: User = Depends(get_admin_user)):
"""获取所有知识库的概览信息,用于思维导图界面选择。"""
try:
return await get_mindmap_databases_overview(current_user.uid)
except Exception as e:
_raise_mindmap_http_exception(e, "获取知识库列表")
@knowledge.get("/databases/{kb_id}/mindmap/files")
async def get_database_mindmap_files(kb_id: str, current_user: User = Depends(get_admin_user)):
"""获取指定知识库的所有文件列表。"""
try:
return await get_mindmap_database_files(kb_id)
except Exception as e:
_raise_mindmap_http_exception(e, "获取文件列表")
@knowledge.post("/databases/{kb_id}/mindmap/generate")
async def generate_mindmap(
kb_id: str,
file_ids: list[str] | None = Body(default=None, description="选择的文件ID列表"),
user_prompt: str = Body(default="", description="用户自定义提示词"),
current_user: User = Depends(get_admin_user),
):
"""使用 AI 分析知识库文件,生成思维导图结构。"""
try:
return await generate_database_mindmap(kb_id, file_ids, user_prompt)
except Exception as e:
_raise_mindmap_http_exception(e, "生成思维导图")
@knowledge.get("/databases/{kb_id}/mindmap")
async def get_database_mindmap(kb_id: str, current_user: User = Depends(get_admin_user)):
"""获取知识库关联的思维导图。"""
try:
return await get_database_mindmap_data(kb_id)
except Exception as e:
_raise_mindmap_http_exception(e, "获取知识库思维导图")
@knowledge.get("/databases/{kb_id}")
async def get_database_info(kb_id: str, current_user: User = Depends(get_admin_user)):
"""获取知识库详细信息"""

View File

@ -1,5 +1,5 @@
"""
Integration tests for knowledge router and mindmap router endpoints.
Integration tests for knowledge router endpoints.
"""
from __future__ import annotations
@ -384,13 +384,13 @@ async def test_dify_query_params_and_documents_readonly(test_client, admin_heade
# =============================================================================
# === Mindmap Router Tests ===
# === Mindmap Tests ===
# =============================================================================
async def test_get_databases_overview(test_client, admin_headers, knowledge_database):
"""测试获取所有知识库概览"""
response = await test_client.get("/api/mindmap/databases", headers=admin_headers)
response = await test_client.get("/api/knowledge/mindmap/databases", headers=admin_headers)
assert response.status_code == 200, response.text
payload = response.json()
assert payload["message"] == "success"
@ -405,7 +405,7 @@ async def test_get_databases_overview(test_client, admin_headers, knowledge_data
async def test_get_database_files(test_client, admin_headers, knowledge_database):
"""测试获取知识库文件列表"""
slug = knowledge_database["slug"]
response = await test_client.get(f"/api/mindmap/databases/{slug}/files", headers=admin_headers)
response = await test_client.get(f"/api/knowledge/databases/{slug}/mindmap/files", headers=admin_headers)
assert response.status_code == 200, response.text
payload = response.json()
assert payload["message"] == "success"
@ -417,7 +417,7 @@ async def test_get_database_files(test_client, admin_headers, knowledge_database
async def test_get_database_files_not_found(test_client, admin_headers):
"""测试获取不存在的知识库文件列表"""
response = await test_client.get("/api/mindmap/databases/nonexistent_slug/files", headers=admin_headers)
response = await test_client.get("/api/knowledge/databases/nonexistent_slug/mindmap/files", headers=admin_headers)
assert response.status_code == 404
@ -425,8 +425,8 @@ async def test_generate_mindmap_empty_files(test_client, admin_headers, knowledg
"""测试空文件列表生成思维导图"""
slug = knowledge_database["slug"]
response = await test_client.post(
"/api/mindmap/generate",
json={"slug": slug, "file_ids": [], "user_prompt": ""},
f"/api/knowledge/databases/{slug}/mindmap/generate",
json={"file_ids": [], "user_prompt": ""},
headers=admin_headers,
)
# 空文件应该返回400错误
@ -437,7 +437,7 @@ async def test_generate_mindmap_empty_files(test_client, admin_headers, knowledg
async def test_get_database_mindmap_not_exists(test_client, admin_headers, knowledge_database):
"""测试获取不存在的思维导图"""
slug = knowledge_database["slug"]
response = await test_client.get(f"/api/mindmap/database/{slug}", headers=admin_headers)
response = await test_client.get(f"/api/knowledge/databases/{slug}/mindmap", headers=admin_headers)
assert response.status_code == 200, response.text
payload = response.json()
assert payload["slug"] == slug
@ -455,8 +455,8 @@ async def test_generate_and_get_mindmap(test_client, admin_headers, knowledge_da
# 空文件场景 - 预期返回400错误
generate_response = await test_client.post(
"/api/mindmap/generate",
json={"slug": slug, "file_ids": [], "user_prompt": ""},
f"/api/knowledge/databases/{slug}/mindmap/generate",
json={"file_ids": [], "user_prompt": ""},
headers=admin_headers,
)
assert generate_response.status_code == 400
@ -728,15 +728,17 @@ async def test_mindmap_permissions(test_client, standard_user, knowledge_databas
slug = knowledge_database["slug"]
# 普通用户应该无法访问
forbidden_list = await test_client.get("/api/mindmap/databases", headers=standard_user["headers"])
forbidden_list = await test_client.get("/api/knowledge/mindmap/databases", headers=standard_user["headers"])
_assert_forbidden_response(forbidden_list)
forbidden_files = await test_client.get(f"/api/mindmap/databases/{slug}/files", headers=standard_user["headers"])
forbidden_files = await test_client.get(
f"/api/knowledge/databases/{slug}/mindmap/files", headers=standard_user["headers"]
)
_assert_forbidden_response(forbidden_files)
forbidden_generate = await test_client.post(
"/api/mindmap/generate",
json={"slug": slug, "file_ids": []},
f"/api/knowledge/databases/{slug}/mindmap/generate",
json={"file_ids": []},
headers=standard_user["headers"],
)
_assert_forbidden_response(forbidden_generate)

View File

@ -57,6 +57,7 @@
- 重梳理知识库评估存储评估数据集、题目、评估运行和逐题结果统一入库JSONL 仅作为导入/导出格式;后端和前端 API 统一使用 dataset/run 语义。
- 扩展知识库上传来源:添加“从工作区上传”模式,后端将当前用户工作区文件预处理上传到 MinIO前端沿用现有 `addDocuments` 入库链路提交 MinIO URL、内容哈希和文件大小。
- 重构知识库详情页布局:`DatabaseInfo` 改为顶部详情 header + 左侧功能 tab 侧边栏 + 右侧内容区Milvus 默认进入文件管理并将检索测试、知识图谱、知识导图、检索配置、RAG 评估和评估基准统一纳入侧边栏导航;只读连接器保留检索测试与检索配置。
- 整合知识导图接口:移除独立 mindmap router 与前端 API 模块,思维导图生成、查询和文件列表接口统一收敛到知识库 API 下。
- 收敛独立模型配置模块运行时:运行时 chat / embedding / rerank 均统一从 provider 模块与模型缓存读取 `provider_id:model_id`旧版静态模型配置、v1 slash spec、旧模型列表接口和 Ollama 适配已移除。
- 调整智能体配置归属与字段权限:`AgentConfig` 从部门共享改为按 `uid` 隔离,所有登录用户可管理自己的配置;`BaseContext` 支持字段级 `auth` 元数据,后端按用户角色过滤可见与可保存的配置项。
- 新增用户级沙盒环境变量:增加 `agent_envs` 表与 `/api/user/agent-env` 接口,设置面板支持当前用户维护 Agent 沙盒环境变量;创建新沙盒时与全局 `sandbox.env` 合并注入,用户变量优先。

View File

@ -9,7 +9,6 @@ export * from './knowledge_api' // 知识库管理API
export * from './graph_api' // 图谱API
export * from './agent_api' // 智能体API
export * from './tasker' // 任务管理API
export * from './mindmap_api' // 思维导图API
export * from './department_api' // 部门管理API
export * from './mcp_api' // MCP API
export * from './skill_api' // Skills API

View File

@ -229,6 +229,31 @@ export const graphBuildApi = {
}
}
// =============================================================================
// === 思维导图分组 ===
// =============================================================================
export const mindmapApi = {
getDatabases: async () => {
return apiAdminGet('/api/knowledge/mindmap/databases')
},
getDatabaseFiles: async (kbId) => {
return apiAdminGet(`/api/knowledge/databases/${kbId}/mindmap/files`)
},
generateMindmap: async (kbId, fileIds = [], userPrompt = '') => {
return apiAdminPost(`/api/knowledge/databases/${kbId}/mindmap/generate`, {
file_ids: fileIds,
user_prompt: userPrompt
})
},
getByDatabase: async (kbId) => {
return apiAdminGet(`/api/knowledge/databases/${kbId}/mindmap`)
}
}
// =============================================================================
// === 查询分组 ===
// =============================================================================

View File

@ -1,53 +0,0 @@
import { apiAdminGet, apiAdminPost } from './base'
/**
* 思维导图API模块
* 提供思维导图相关的接口功能
*/
// =============================================================================
// === 知识库管理 ===
// =============================================================================
export const mindmapApi = {
/**
* 获取所有知识库概览用于选择
* @returns {Promise} - 知识库列表
*/
getDatabases: async () => {
return apiAdminGet('/api/mindmap/databases')
},
/**
* 获取指定知识库的文件列表
* @param {string} kbId - 知识库ID
* @returns {Promise} - 文件列表
*/
getDatabaseFiles: async (kbId) => {
return apiAdminGet(`/api/mindmap/databases/${kbId}/files`)
},
/**
* AI生成思维导图
* @param {string} kbId - 知识库ID
* @param {Array<string>} fileIds - 选择的文件ID列表为空则使用所有文件
* @param {string} userPrompt - 用户自定义提示词
* @returns {Promise} - 思维导图数据
*/
generateMindmap: async (kbId, fileIds = [], userPrompt = '') => {
return apiAdminPost('/api/mindmap/generate', {
kb_id: kbId,
file_ids: fileIds,
user_prompt: userPrompt
})
},
/**
* 获取知识库的思维导图
* @param {string} kbId - 知识库ID
* @returns {Promise} - 思维导图数据
*/
getByDatabase: async (kbId) => {
return apiAdminGet(`/api/mindmap/database/${kbId}`)
}
}

View File

@ -15,32 +15,44 @@
<!-- 空状态 -->
<div v-else-if="!mindmapData" class="empty-state">
<Network :size="32" />
<p>暂无思维导图</p>
<a-button type="primary" size="small" @click="generateMindmap">
<template #icon><Sparkles :size="14" /></template>
生成思维导图
</a-button>
<div class="empty-icon">
<MapIcon :size="24" />
</div>
<p class="empty-title">暂无思维导图</p>
<p class="empty-description">从当前知识库内容生成结构化导图</p>
<button
type="button"
class="lucide-icon-btn mindmap-primary-action"
@click="generateMindmap"
>
<Sparkles :size="14" />
<span>生成思维导图</span>
</button>
</div>
<!-- 思维导图显示 -->
<div v-else class="mindmap-container">
<div class="mindmap-toolbar">
<a-space :size="8">
<a-button
type="text"
size="small"
<button
type="button"
class="lucide-icon-btn mindmap-toolbar-btn"
:disabled="generating"
@click="refreshMindmap"
:loading="generating"
title="重新生成"
>
<template #icon><RefreshCw :size="14" /></template>
<RefreshCw :size="14" :class="{ spin: generating }" />
<span class="toolbar-text">重新生成</span>
</a-button>
<a-button type="text" size="small" @click="fitView" title="适应视图">
<template #icon><Maximize2 :size="14" /></template>
</button>
<button
type="button"
class="lucide-icon-btn mindmap-toolbar-btn"
@click="fitView"
title="适应视图"
>
<Maximize2 :size="14" />
<span class="toolbar-text">适应视图</span>
</a-button>
</button>
</a-space>
</div>
<div class="mindmap-svg-container">
@ -54,8 +66,8 @@
<script setup>
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { message } from 'ant-design-vue'
import { RefreshCw, Network, Sparkles, Maximize2 } from 'lucide-vue-next'
import { mindmapApi } from '@/apis/mindmap_api'
import { RefreshCw, Map as MapIcon, Sparkles, Maximize2 } from 'lucide-vue-next'
import { mindmapApi } from '@/apis/knowledge_api'
import { Markmap } from 'markmap-view'
import { Transformer } from 'markmap-lib'
@ -90,13 +102,20 @@ const loadMindmap = async () => {
loading.value = true
const response = await mindmapApi.getByDatabase(props.kbId)
if (response.mindmap) {
mindmapData.value = response.mindmap
const mindmap = response.mindmap || null
mindmapData.value = mindmap
if (markmapInstance) {
markmapInstance.destroy()
markmapInstance = null
}
if (mindmap) {
await nextTick()
// DOM
setTimeout(() => {
renderMindmap(response.mindmap)
renderMindmap(mindmap)
}, 100)
}
} catch (error) {
@ -302,9 +321,11 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
background: var(--gray-0);
border-top: 1px solid var(--border-color);
border: 1px solid var(--gray-150);
border-radius: 8px;
height: 100%;
min-height: 0;
overflow: hidden;
}
.section-content {
@ -323,20 +344,71 @@ onUnmounted(() => {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 24px;
color: var(--text-secondary);
gap: 10px;
padding: 28px;
color: var(--gray-500);
font-size: 13px;
svg {
color: var(--text-tertiary);
}
text-align: center;
p {
margin: 0;
}
}
.empty-icon {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--gray-150);
border-radius: 12px;
background: var(--main-30);
color: var(--main-color);
}
.empty-title {
margin-top: 2px;
color: var(--gray-900);
font-size: 15px;
font-weight: 600;
}
.empty-description {
max-width: 280px;
color: var(--gray-500);
line-height: 1.5;
}
.mindmap-primary-action {
min-height: 32px;
margin-top: 4px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 6px;
background: var(--main-600);
color: var(--main-0);
font: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
appearance: none;
transition:
background-color 0.18s ease,
color 0.18s ease;
&:hover,
&:focus-visible {
background: var(--main-700);
color: var(--main-0);
outline: none;
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--main-200);
}
}
.mindmap-container {
flex: 1;
display: flex;
@ -346,9 +418,9 @@ onUnmounted(() => {
}
.mindmap-toolbar {
padding: 8px 16px;
padding: 8px 12px;
background: var(--gray-0);
border-bottom: 1px solid var(--border-color);
border-bottom: 1px solid var(--gray-150);
display: flex;
align-items: center;
justify-content: flex-end;
@ -357,14 +429,53 @@ onUnmounted(() => {
margin-left: 4px;
font-size: 13px;
}
}
:deep(.ant-btn-text) {
display: flex;
align-items: center;
.mindmap-toolbar-btn {
min-height: 30px;
padding: 0 10px;
border: 1px solid transparent;
border-radius: 6px;
background: transparent;
color: var(--gray-600);
font: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
appearance: none;
transition:
background-color 0.18s ease,
color 0.18s ease;
&:hover {
background: rgba(0, 0, 0, 0.04);
}
&:hover,
&:focus-visible {
background: var(--gray-50);
color: var(--main-color);
outline: none;
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--main-100);
}
&:disabled {
opacity: 0.55;
cursor: not-allowed;
}
&:disabled:hover {
background: transparent;
color: var(--gray-600);
}
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}