Procházet zdrojové kódy

feat: 前端API适配新后端 + 文档预览功能

- 重写 api/index.js: 全部替换为新后端API端点
- 重写 vite.config.js: 代理到 localhost:8001
- 适配 Login.vue: 新认证API响应结构
- 重写 stores/template.js: 项目/要素/值/附件/实体/规则模型
- 重写 stores/taskCenter.js: 新任务API (running/completed/failed)
- 重写 Editor.vue: 项目管理 + 文档预览(Word排版还原) + 要素视图 + 实体视图
- 更新 TaskCenterPanel.vue: processing->running, deleteTask->cancelTask
- 更新 App.vue: 退出登录调用后端API
- 简化 Register.vue: 暂不支持自助注册
- 后端新增 GET /attachments/{id}/doc-content 接口
- 新增 Python 文档解析脚本 parse_docx.py (提取段落/表格/图片/格式)
何文松 před 1 týdnem
rodič
revize
0e3bb94757

+ 16 - 0
backend/lingyue-project/src/main/java/com/lingyue/project/attachment/controller/AttachmentController.java

@@ -70,4 +70,20 @@ public class AttachmentController {
         attachmentService.updateSortOrder(id, sortOrder);
         return Result.ok();
     }
+
+    @Operation(summary = "获取附件文档内容(解析后的结构化JSON)")
+    @GetMapping("/api/v1/attachments/{id}/doc-content")
+    public Result<Object> getDocContent(@PathVariable Long id) {
+        String json = attachmentService.getDocContent(id);
+        if (json == null) {
+            return Result.ok(null);
+        }
+        // 直接返回 JSON 字符串,让 Jackson 解析为对象
+        try {
+            Object parsed = new com.fasterxml.jackson.databind.ObjectMapper().readValue(json, Object.class);
+            return Result.ok(parsed);
+        } catch (Exception e) {
+            return Result.ok(null);
+        }
+    }
 }

+ 8 - 0
backend/lingyue-project/src/main/java/com/lingyue/project/attachment/service/AttachmentService.java

@@ -123,6 +123,14 @@ public class AttachmentService {
         }
     }
 
+    public String getDocContent(Long attachmentId) {
+        VAttachment va = attachmentViewMapper.selectById(attachmentId);
+        if (va == null) {
+            throw new BusinessException(404, "附件不存在");
+        }
+        return propertyService.getNodePropertyJson(attachmentId, "doc_content");
+    }
+
     private AttachmentVO toAttachmentVO(VAttachment va) {
         AttachmentVO vo = new AttachmentVO();
         vo.setId(va.getId());

+ 3 - 2
frontend/vue-demo/src/App.vue

@@ -62,6 +62,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
 import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
 import TaskCenterFab from '@/components/TaskCenter/TaskCenterFab.vue'
 import TaskCenterPanel from '@/components/TaskCenter/TaskCenterPanel.vue'
+import { authApi } from '@/api'
 
 const router = useRouter()
 const route = useRoute()
@@ -86,8 +87,8 @@ function handleUserCommand(command) {
         confirmButtonText: '退出',
         cancelButtonText: '取消',
         type: 'warning'
-      }).then(() => {
-        // 清除登录信息
+      }).then(async () => {
+        try { await authApi.logout() } catch (e) { /* ignore */ }
         localStorage.removeItem('accessToken')
         localStorage.removeItem('refreshToken')
         localStorage.removeItem('userId')

+ 157 - 625
frontend/vue-demo/src/api/index.js

@@ -1,5 +1,7 @@
 import axios from 'axios'
 
+// ==================== Axios 实例 ====================
+
 const api = axios.create({
   baseURL: '/api/v1',
   timeout: 30000,
@@ -8,14 +10,13 @@ const api = axios.create({
   }
 })
 
-// 请求拦截器
+// 请求拦截器 - 自动附加 Token
 api.interceptors.request.use(
   config => {
-    // 可以在这里添加 token
-    // const token = localStorage.getItem('token')
-    // if (token) {
-    //   config.headers.Authorization = `Bearer ${token}`
-    // }
+    const token = localStorage.getItem('accessToken')
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`
+    }
     return config
   },
   error => Promise.reject(error)
@@ -28,769 +29,300 @@ api.interceptors.response.use(
     if (data.code === 200) {
       return data.data
     }
-    return Promise.reject(new Error(data.msg || '请求失败'))
+    return Promise.reject(new Error(data.message || '请求失败'))
   },
   error => {
-    console.error('API Error:', error)
-    return Promise.reject(error)
+    if (error.response?.status === 401) {
+      localStorage.removeItem('accessToken')
+      localStorage.removeItem('refreshToken')
+      localStorage.removeItem('username')
+      window.location.href = '/login'
+    }
+    const msg = error.response?.data?.message || error.message || '网络错误'
+    return Promise.reject(new Error(msg))
   }
 )
 
-// ==================== 模板 API ====================
-
-export const templateApi = {
-  // 获取模板列表
-  list(page = 1, size = 20) {
-    return api.get('/templates', { params: { page, size } })
-  },
-
-  // 获取首页统计数据
-  getStats() {
-    return api.get('/templates/stats')
-  },
-
-  // 搜索模板
-  search(keyword) {
-    return api.get('/templates/search', { params: { keyword } })
-  },
-
-  // 获取可访问的模板
-  listAccessible() {
-    return api.get('/templates/accessible')
-  },
-
-  // 获取模板详情
-  getById(id) {
-    return api.get(`/templates/${id}`)
-  },
-
-  // 创建模板
-  create(data) {
-    return api.post('/templates', data)
-  },
-
-  // 更新模板
-  update(id, data) {
-    return api.put(`/templates/${id}`, data)
-  },
-
-  // 删除模板
-  delete(id) {
-    return api.delete(`/templates/${id}`)
-  },
-
-  // 发布模板
-  publish(id) {
-    return api.post(`/templates/${id}/publish`)
-  },
-
-  // 归档模板
-  archive(id) {
-    return api.post(`/templates/${id}/archive`)
-  },
-
-  // 复制模板
-  duplicate(id, newName) {
-    return api.post(`/templates/${id}/duplicate`, null, { params: { newName } })
-  }
-}
-
-// ==================== 来源文件 API ====================
-
-export const sourceFileApi = {
-  // 获取模板的来源文件列表
-  list(templateId) {
-    return api.get(`/templates/${templateId}/source-files`)
-  },
-
-  // 添加来源文件定义
-  add(templateId, data) {
-    return api.post(`/templates/${templateId}/source-files`, data)
-  },
-
-  // 更新来源文件定义
-  update(id, data) {
-    return api.put(`/templates/source-files/${id}`, data)
-  },
-
-  // 删除来源文件定义
-  delete(id, force = false) {
-    return api.delete(`/templates/source-files/${id}`, { params: { force } })
-  }
-}
-
-// ==================== 变量 API ====================
-
-export const variableApi = {
-  // 获取模板的变量列表
-  list(templateId, params = {}) {
-    return api.get(`/templates/${templateId}/variables`, { params })
-  },
-
-  // 获取变量按类别分组
-  listGrouped(templateId) {
-    return api.get(`/templates/${templateId}/variables/grouped`)
-  },
+// ==================== 认证 API ====================
 
-  // 添加变量
-  add(templateId, data) {
-    return api.post(`/templates/${templateId}/variables`, data)
+export const authApi = {
+  login(data) {
+    return api.post('/auth/login', { username: data.usernameOrEmail || data.username, password: data.password })
   },
 
-  // 获取变量详情
-  getById(id) {
-    return api.get(`/templates/variables/${id}`)
+  logout() {
+    return api.post('/auth/logout')
   },
 
-  // 更新变量
-  update(id, data) {
-    return api.put(`/templates/variables/${id}`, data)
+  refreshToken(refreshToken) {
+    return api.post('/auth/refresh', { refreshToken })
   },
 
-  // 删除变量
-  delete(id, force = false) {
-    return api.delete(`/templates/variables/${id}`, { params: { force } })
+  getCurrentUser() {
+    return api.get('/auth/me')
   },
 
-  // 预览提取结果
-  preview(id, documentId) {
-    return api.post(`/templates/variables/${id}/preview`, null, { params: { documentId } })
+  getPermissions() {
+    return api.get('/auth/permissions')
   },
 
-  // 重排序变量
-  reorder(templateId, orderedIds) {
-    return api.post(`/templates/${templateId}/variables/reorder`, { orderedIds })
+  changePassword(data) {
+    return api.put('/auth/password', data)
   }
 }
 
-// ==================== 生成任务 API ====================
+// ==================== 项目 API ====================
 
-export const generationApi = {
-  // 获取生成任务列表
+export const projectApi = {
   list(params = {}) {
-    return api.get('/generations', { params })
+    return api.get('/projects', { params })
   },
 
-  // 获取生成任务详情
   getById(id) {
-    return api.get(`/generations/${id}`)
+    return api.get(`/projects/${id}`)
   },
 
-  // 创建生成任务
   create(data) {
-    return api.post('/generations', data)
+    return api.post('/projects', data)
   },
 
-  // 执行变量提取
-  execute(id) {
-    return api.post(`/generations/${id}/execute`)
+  update(id, data) {
+    return api.put(`/projects/${id}`, data)
   },
 
-  // 获取执行进度
-  getProgress(id) {
-    return api.get(`/generations/${id}/progress`)
+  delete(id) {
+    return api.delete(`/projects/${id}`)
   },
 
-  // 修改变量值
-  updateVariableValue(id, variableName, data) {
-    return api.put(`/generations/${id}/variables/${variableName}`, data)
+  copy(id) {
+    return api.post(`/projects/${id}/copy`)
   },
 
-  // 确认并生成文档
-  confirm(id) {
-    return api.post(`/generations/${id}/confirm`)
+  archive(id) {
+    return api.post(`/projects/${id}/archive`)
   },
 
-  // 下载生成文档
-  getDownloadUrl(id) {
-    return `/api/v1/generations/${id}/download`
+  export(id) {
+    return api.get(`/projects/${id}/export`, { responseType: 'blob' })
   }
 }
 
-// ==================== 认证 API ====================
-// 注意:认证接口不在 /api/v1 下,使用独立的 axios 实例
+// ==================== 要素 API ====================
 
-const authInstance = axios.create({
-  baseURL: '/auth',
-  timeout: 30000,
-  headers: {
-    'Content-Type': 'application/json'
-  }
-})
-
-// 认证 API 响应拦截器
-authInstance.interceptors.response.use(
-  response => {
-    const { data } = response
-    if (data.code === 200) {
-      return data.data
-    }
-    return Promise.reject(new Error(data.msg || '请求失败'))
+export const elementApi = {
+  list(projectId) {
+    return api.get(`/projects/${projectId}/elements`)
   },
-  error => {
-    console.error('Auth API Error:', error)
-    return Promise.reject(error)
-  }
-)
 
-export const authApi = {
-  // 用户登录
-  login(data) {
-    return authInstance.post('/login', data)
+  add(projectId, data) {
+    return api.post(`/projects/${projectId}/elements`, data)
   },
 
-  // 用户注册
-  register(data) {
-    return authInstance.post('/register', data)
+  update(projectId, elementId, data) {
+    return api.put(`/projects/${projectId}/elements/${elementId}`, data)
   },
 
-  // 用户登出
-  logout() {
-    const token = localStorage.getItem('accessToken')
-    return authInstance.post('/logout', null, {
-      headers: { Authorization: `Bearer ${token}` }
-    })
-  },
+  delete(projectId, elementId) {
+    return api.delete(`/projects/${projectId}/elements/${elementId}`)
+  }
+}
 
-  // 刷新 Token
-  refreshToken(refreshToken) {
-    return authInstance.post('/refresh', { refreshToken })
+// ==================== 要素值 API ====================
+
+export const valueApi = {
+  list(projectId) {
+    return api.get(`/projects/${projectId}/values`)
   },
 
-  // 获取当前用户信息
-  getCurrentUser() {
-    const token = localStorage.getItem('accessToken')
-    return authInstance.get('/me', {
-      headers: { Authorization: `Bearer ${token}` }
-    })
+  getByKey(projectId, elementKey) {
+    return api.get(`/projects/${projectId}/values/${elementKey}`)
   },
 
-  // 更新用户资料
-  updateProfile(data) {
-    const token = localStorage.getItem('accessToken')
-    return authInstance.put('/profile', data, {
-      headers: { Authorization: `Bearer ${token}` }
-    })
+  update(projectId, elementKey, data) {
+    return api.put(`/projects/${projectId}/values/${elementKey}`, data)
   },
 
-  // 修改密码
-  changePassword(data) {
-    const token = localStorage.getItem('accessToken')
-    return authInstance.put('/password', data, {
-      headers: { Authorization: `Bearer ${token}` }
-    })
+  batchUpdate(projectId, values) {
+    return api.put(`/projects/${projectId}/values`, values)
   }
 }
 
-// ==================== 任务中心 API ====================
+// ==================== 附件 API ====================
 
-export const taskCenterApi = {
-  // 获取任务列表
-  list(params = {}) {
-    return api.get('/tasks/list', { params })
-  },
-
-  // 获取任务详情
-  getById(taskId) {
-    return api.get(`/tasks/${taskId}/detail`)
-  },
-
-  // 根据文档ID获取任务详情
-  getByDocumentId(documentId) {
-    return api.get(`/tasks/by-document/${documentId}`)
+export const attachmentApi = {
+  list(projectId) {
+    return api.get(`/projects/${projectId}/attachments`)
   },
 
-  // 获取任务统计
-  getStatistics() {
-    return api.get('/tasks/statistics')
+  getById(attachmentId) {
+    return api.get(`/attachments/${attachmentId}`)
   },
 
-  // 删除任务
-  delete(taskId) {
-    return api.delete(`/tasks/${taskId}`)
-  }
-}
-
-// ==================== 解析 API ====================
-
-export const parseApi = {
-  // 上传文件并解析
-  upload(file, templateId = null) {
+  upload(projectId, file, displayName = null) {
     const formData = new FormData()
     formData.append('file', file)
-    if (templateId) {
-      formData.append('templateId', templateId)
+    if (displayName) {
+      formData.append('displayName', displayName)
     }
-    return api.post('/parse/upload', formData, {
-      headers: {
-        'Content-Type': 'multipart/form-data'
-      }
+    return api.post(`/projects/${projectId}/attachments/upload`, formData, {
+      headers: { 'Content-Type': 'multipart/form-data' }
     })
   },
 
-  // 启动文档解析
-  startParse(documentId) {
-    return api.post(`/parse/start/${documentId}`)
+  delete(attachmentId) {
+    return api.delete(`/attachments/${attachmentId}`)
   },
 
-  // 获取解析状态
-  getStatus(documentId) {
-    return api.get(`/parse/status/${documentId}`)
-  }
-}
-
-// ==================== 文档 API ====================
-
-export const documentApi = {
-  // 获取文档基本信息
-  getById(documentId) {
-    return api.get(`/documents/${documentId}`)
-  },
-
-  // 获取文档列表
-  list(params = {}) {
-    return api.get('/documents', { params })
-  },
-
-  // 获取文档纯文本内容
-  getText(documentId) {
-    return api.get(`/documents/${documentId}/text`)
+  parse(attachmentId) {
+    return api.post(`/attachments/${attachmentId}/parse`)
   },
 
-  // 获取文档结构化元素
-  getElements(documentId) {
-    return api.get(`/documents/${documentId}/elements`)
+  sort(projectId, orderedIds) {
+    return api.put(`/projects/${projectId}/attachments/sort`, orderedIds)
   },
 
-  // 获取结构化文档(用于编辑器)
-  getStructured(documentId) {
-    return api.get(`/documents/${documentId}/structured`)
-  },
-
-  // 获取文档目录
-  getToc(documentId) {
-    return api.get(`/documents/${documentId}/toc`)
-  },
-
-  // 获取文档解析状态
-  getParseStatus(documentId) {
-    return api.get(`/documents/${documentId}/parse-status`)
-  },
-
-  // 更新文档
-  update(documentId, data) {
-    return api.put(`/documents/${documentId}`, data)
-  },
-
-  // 删除文档
-  delete(documentId) {
-    return api.delete(`/documents/${documentId}`)
-  },
-
-  // 批量删除文档
-  batchDelete(documentIds) {
-    return api.post('/documents/batch-delete', { documentIds })
-  },
-
-  // 获取文档图片列表
-  getImages(documentId) {
-    return api.get(`/documents/${documentId}/images`)
-  },
-
-  // 获取文档表格列表
-  getTables(documentId) {
-    return api.get(`/documents/${documentId}/tables`)
-  },
-
-  // ==================== 块操作 ====================
-
-  // 更新块元素
-  updateBlockElements(documentId, blockId, elements) {
-    return api.put(`/documents/${documentId}/blocks/${blockId}/elements`, { elements })
-  },
-
-  // 创建新块
-  createBlock(documentId, data) {
-    return api.post(`/documents/${documentId}/blocks`, data)
-  },
+  getDocContent(attachmentId) {
+    return api.get(`/attachments/${attachmentId}/doc-content`)
+  }
+}
 
-  // 删除块
-  deleteBlock(documentId, blockId) {
-    return api.delete(`/documents/${documentId}/blocks/${blockId}`)
-  },
+// ==================== 实体 API ====================
 
-  // 批量保存块
-  saveBlocksBatch(documentId, blocks) {
-    return api.post(`/documents/${documentId}/blocks/batch`, { blocks })
+export const entityApi = {
+  listByAttachment(attachmentId) {
+    return api.get(`/attachments/${attachmentId}/entities`)
   },
 
-  // ==================== 实体标记操作 ====================
-
-  /**
-   * 标记实体(将文本转为实体)
-   * @param {string} documentId - 文档ID
-   * @param {string} blockId - 块ID
-   * @param {object} data - 标记数据
-   * @param {number} data.elementIndex - 要标记的元素在 elements 数组中的索引
-   * @param {number} data.startOffset - 在该元素文本中的起始位置
-   * @param {number} data.endOffset - 在该元素文本中的结束位置
-   * @param {string} data.entityType - 实体类型
-   */
-  markEntity(documentId, blockId, data) {
-    return api.post(`/documents/${documentId}/blocks/${blockId}/mark-entity`, data)
+  listByProject(projectId) {
+    return api.get(`/projects/${projectId}/entities`)
   },
 
-  /**
-   * 取消实体标记(将实体还原为文本)
-   * @param {string} documentId - 文档ID
-   * @param {string} blockId - 块ID
-   * @param {string} entityId - 实体ID
-   */
-  unmarkEntity(documentId, blockId, entityId) {
-    return api.delete(`/documents/${documentId}/blocks/${blockId}/entities/${entityId}`)
+  getById(entityId) {
+    return api.get(`/entities/${entityId}`)
   },
 
-  /**
-   * 更新实体类型
-   * @param {string} documentId - 文档ID
-   * @param {string} blockId - 块ID
-   * @param {string} entityId - 实体ID
-   * @param {string} entityType - 新的实体类型
-   */
-  updateEntity(documentId, blockId, entityId, entityType) {
-    return api.put(`/documents/${documentId}/blocks/${blockId}/entities/${entityId}`, { entityType })
+  update(entityId, data) {
+    return api.put(`/entities/${entityId}`, data)
   },
 
-  /**
-   * 确认实体
-   * @param {string} documentId - 文档ID
-   * @param {string} blockId - 块ID
-   * @param {string} entityId - 实体ID
-   */
-  confirmEntity(documentId, blockId, entityId) {
-    return api.post(`/documents/${documentId}/blocks/${blockId}/entities/${entityId}/confirm`)
+  merge(targetId, sourceIds) {
+    return api.post(`/entities/${targetId}/merge`, sourceIds)
   }
 }
 
-// ==================== 项目 API ====================
-
-export const projectApi = {
-  // 创建项目
-  create(data) {
-    return api.post('/extract/projects', data)
-  },
+// ==================== 规则 API ====================
 
-  // 获取项目详情
-  getById(id, includeDocuments = true) {
-    return api.get(`/extract/projects/${id}`, { params: { includeDocuments } })
+export const ruleApi = {
+  list(projectId) {
+    return api.get(`/projects/${projectId}/rules`)
   },
 
-  // 分页查询项目列表
-  list(params = {}) {
-    return api.get('/extract/projects', { params })
+  getById(ruleId) {
+    return api.get(`/rules/${ruleId}`)
   },
 
-  // 按状态查询项目列表
-  listByStatus(status) {
-    return api.get(`/extract/projects/by-status/${status}`)
+  create(projectId, data) {
+    return api.post(`/projects/${projectId}/rules`, data)
   },
 
-  // 更新项目
-  update(id, data) {
-    return api.put(`/extract/projects/${id}`, data)
+  update(ruleId, data) {
+    return api.put(`/rules/${ruleId}`, data)
   },
 
-  // 删除项目
-  delete(id) {
-    return api.delete(`/extract/projects/${id}`)
+  delete(ruleId) {
+    return api.delete(`/rules/${ruleId}`)
   },
 
-  // 归档项目
-  archive(id) {
-    return api.post(`/extract/projects/${id}/archive`)
+  execute(ruleId) {
+    return api.post(`/rules/${ruleId}/execute`)
   },
 
-  // 获取用户项目统计
-  getStatistics() {
-    return api.get('/extract/projects/statistics')
+  batchExecute(projectId) {
+    return api.post(`/projects/${projectId}/rules/execute`)
   }
 }
 
-// ==================== 来源文档 API(项目子资源) ====================
-
-export const sourceDocumentApi = {
-  /**
-   * 添加来源文档到项目
-   * @param {string} projectId - 项目ID
-   * @param {object} data - 文档数据
-   * @param {string} data.documentId - 关联的 Document ID
-   * @param {string} data.alias - 文档别名,如'可研批复'
-   * @param {string} data.docType - 文档类型: pdf/docx/xlsx
-   * @param {number} data.displayOrder - 显示顺序
-   * @param {object} data.metadata - 元数据
-   */
-  add(projectId, data) {
-    return api.post(`/extract/projects/${projectId}/documents`, data)
+// ==================== 文件 API ====================
+
+export const fileApi = {
+  upload(file) {
+    const formData = new FormData()
+    formData.append('file', file)
+    return api.post('/files/upload', formData, {
+      headers: { 'Content-Type': 'multipart/form-data' }
+    })
   },
 
-  /**
-   * 批量添加来源文档
-   * @param {string} projectId - 项目ID
-   * @param {Array} documents - 文档数组
-   */
-  batchAdd(projectId, documents) {
-    return api.post(`/extract/projects/${projectId}/documents/batch`, { documents })
+  getDownloadUrl(fileKey) {
+    return `/api/v1/files/${fileKey}`
   },
 
-  /**
-   * 获取项目的来源文档列表
-   * @param {string} projectId - 项目ID
-   */
-  list(projectId) {
-    return api.get(`/extract/projects/${projectId}/documents`)
-  },
-
-  /**
-   * 获取来源文档详情
-   * @param {string} projectId - 项目ID
-   * @param {string} id - 来源文档ID
-   */
-  getById(projectId, id) {
-    return api.get(`/extract/projects/${projectId}/documents/${id}`)
-  },
-
-  /**
-   * 更新来源文档
-   * @param {string} projectId - 项目ID
-   * @param {string} id - 来源文档ID
-   * @param {object} data - 更新数据
-   */
-  update(projectId, id, data) {
-    return api.put(`/extract/projects/${projectId}/documents/${id}`, data)
-  },
-
-  /**
-   * 移除来源文档
-   * @param {string} projectId - 项目ID
-   * @param {string} id - 来源文档ID
-   * @param {boolean} force - 是否强制删除
-   */
-  remove(projectId, id, force = false) {
-    return api.delete(`/extract/projects/${projectId}/documents/${id}`, { params: { force } })
-  },
-
-  /**
-   * 调整来源文档顺序
-   * @param {string} projectId - 项目ID
-   * @param {Array<string>} orderedIds - 排序后的文档ID数组
-   */
-  reorder(projectId, orderedIds) {
-    return api.post(`/extract/projects/${projectId}/documents/reorder`, { orderedIds })
-  }
-}
+  getPreviewUrl(fileKey) {
+    return `/api/v1/files/${fileKey}/preview`
+  },
 
-// ==================== 知识图谱 API ====================
-// 注意:graph-service 的路径已更新为 /api/v1/graph/*
-
-export const knowledgeGraphApi = {
-  /**
-   * 获取文档图谱
-   * @param {string} documentId - 文档ID
-   */
-  getDocumentGraph(documentId) {
-    return api.get(`/graph/documents/${documentId}`)
-  },
-
-  /**
-   * 获取文档实体列表(按类型分组)
-   * @param {string} documentId - 文档ID
-   * @param {string} type - 可选,筛选实体类型
-   */
-  getDocumentEntities(documentId, type = null) {
-    const params = type ? { type } : {}
-    return api.get(`/graph/documents/${documentId}/entities`, { params })
-  },
-
-  /**
-   * 获取实体详情
-   * @param {string} entityId - 实体ID
-   */
-  getEntityDetail(entityId) {
-    return api.get(`/graph/entities/${entityId}`)
-  },
-
-  /**
-   * 获取用户的全局知识图谱
-   * @param {string} userId - 用户ID
-   * @param {number} limit - 限制返回数量
-   */
-  getUserGraph(userId, limit = 100) {
-    return api.get(`/graph/users/${userId}`, { params: { limit } })
-  },
-
-  /**
-   * 搜索实体
-   * @param {string} keyword - 关键词
-   * @param {string} documentId - 可选,限定文档
-   * @param {number} limit - 限制返回数量
-   */
-  searchEntities(keyword, documentId = null, limit = 20) {
-    const params = { keyword, limit }
-    if (documentId) params.documentId = documentId
-    return api.get('/graph/search', { params })
+  delete(fileKey) {
+    return api.delete(`/files/${fileKey}`)
   }
 }
 
-// ==================== 图谱模板 API ====================
-// graph-service 的模板、项目、附件等接口路径
+// ==================== 任务 API ====================
 
-export const graphTemplateApi = {
-  /**
-   * 获取图谱模板列表
-   */
+export const taskApi = {
   list(params = {}) {
-    return api.get('/graph/templates', { params })
-  },
-
-  /**
-   * 获取图谱模板详情
-   */
-  getById(id) {
-    return api.get(`/graph/templates/${id}`)
-  },
-
-  /**
-   * 创建图谱模板
-   */
-  create(data) {
-    return api.post('/graph/templates', data)
+    return api.get('/tasks', { params })
   },
 
-  /**
-   * 更新图谱模板
-   */
-  update(id, data) {
-    return api.put(`/graph/templates/${id}`, data)
+  getById(taskId) {
+    return api.get(`/tasks/${taskId}`)
   },
 
-  /**
-   * 删除图谱模板
-   */
-  delete(id) {
-    return api.delete(`/graph/templates/${id}`)
+  cancel(taskId) {
+    return api.post(`/tasks/${taskId}/cancel`)
   }
 }
 
-// ==================== 图谱项目 API ====================
+// ==================== 用户管理 API ====================
 
-export const graphProjectApi = {
-  /**
-   * 获取图谱项目列表
-   */
+export const userApi = {
   list(params = {}) {
-    return api.get('/graph/projects', { params })
+    return api.get('/users', { params })
   },
 
-  /**
-   * 获取图谱项目详情
-   */
   getById(id) {
-    return api.get(`/graph/projects/${id}`)
+    return api.get(`/users/${id}`)
   },
 
-  /**
-   * 创建图谱项目
-   */
   create(data) {
-    return api.post('/graph/projects', data)
+    return api.post('/users', data)
   },
 
-  /**
-   * 更新图谱项目
-   */
   update(id, data) {
-    return api.put(`/graph/projects/${id}`, data)
+    return api.put(`/users/${id}`, data)
   },
 
-  /**
-   * 删除图谱项目
-   */
   delete(id) {
-    return api.delete(`/graph/projects/${id}`)
-  },
-
-  /**
-   * 归档图谱项目
-   */
-  archive(id) {
-    return api.post(`/graph/projects/${id}/archive`)
+    return api.delete(`/users/${id}`)
   }
 }
 
-// ==================== 图谱报告 API ====================
-
-export const graphReportApi = {
-  /**
-   * 获取报告列表
-   */
-  list(params = {}) {
-    return api.get('/graph/reports', { params })
-  },
-
-  /**
-   * 获取报告详情
-   */
-  getById(id) {
-    return api.get(`/graph/reports/${id}`)
-  },
-
-  /**
-   * 创建报告
-   */
-  create(data) {
-    return api.post('/graph/reports', data)
-  },
-
-  /**
-   * 更新报告
-   */
-  update(id, data) {
-    return api.put(`/graph/reports/${id}`, data)
-  },
+// ==================== AI API ====================
 
-  /**
-   * 删除报告
-   */
-  delete(id) {
-    return api.delete(`/graph/reports/${id}`)
+export const aiApi = {
+  extractEntities(data) {
+    return api.post('/ai/ner', data)
   },
 
-  /**
-   * 上传报告附件
-   */
-  uploadAttachment(reportId, file, displayName = null) {
-    const formData = new FormData()
-    formData.append('file', file)
-    if (displayName) {
-      formData.append('displayName', displayName)
-    }
-    return api.post(`/graph/reports/${reportId}/attachments/upload`, formData, {
-      headers: { 'Content-Type': 'multipart/form-data' }
-    })
+  chat(data) {
+    return api.post('/ai/chat', data)
   },
 
-  /**
-   * 获取报告附件列表
-   */
-  getAttachments(reportId) {
-    return api.get(`/graph/reports/${reportId}/attachments`)
+  suggest(data) {
+    return api.post('/ai/suggest', data)
   },
 
-  /**
-   * 删除附件
-   */
-  deleteAttachment(attachmentId) {
-    return api.delete(`/graph/attachments/${attachmentId}`)
+  optimize(data) {
+    return api.post('/ai/optimize', data)
   }
 }
 

+ 12 - 14
frontend/vue-demo/src/components/TaskCenter/TaskCenterPanel.vue

@@ -13,11 +13,11 @@
 
         <el-tabs v-model="activeTab" class="task-tabs" @tab-change="handleTabChange">
           <el-tab-pane label="全部" name="all" />
-          <el-tab-pane name="processing">
+          <el-tab-pane name="running">
             <template #label>
               <span class="tab-label">
                 <span class="tab-text">运行中</span>
-                <span class="tab-count" v-if="statusTotals.processing > 0">{{ statusTotals.processing }}</span>
+                <span class="tab-count" v-if="statusTotals.running > 0">{{ statusTotals.running }}</span>
               </span>
             </template>
           </el-tab-pane>
@@ -62,7 +62,7 @@
               <div class="task-meta">
                 <div class="task-meta-left">
                   <div class="task-time">{{ formatRelativeTime(item.createdAt || item.startedAt) }}</div>
-                  <template v-if="item.status === 'processing' && item.startedAt">
+                  <template v-if="item.status === 'running' && item.startedAt">
                     <span class="task-meta-separator">·</span>
                     <div class="task-duration-inline">
                       <span class="task-duration-value">{{ formatElapsedTime(item.startedAt) }}</span>
@@ -76,7 +76,7 @@
 
               <!-- 进度条区域 -->
               <div class="task-progress-section" v-if="selectedId !== item.id">
-                <template v-if="item.status === 'processing' || item.status === 'pending'">
+                <template v-if="item.status === 'running' || item.status === 'pending'">
                   <div class="task-progress-row" v-if="item.progress != null">
                     <el-progress :percentage="item.progress" :stroke-width="6" />
                     <span class="task-progress-percent">{{ item.progress }}%</span>
@@ -183,7 +183,7 @@ const detailLoading = computed(() => store.detailLoading)
 const selectedId = computed(() => store.selectedId)
 const detail = computed(() => store.detail)
 const showDetail = computed(() => !!store.selectedId)
-const statusTotals = computed(() => store.statusTotals || { processing: 0, failed: 0, pending: 0 })
+const statusTotals = computed(() => store.statusTotals || { running: 0, failed: 0, pending: 0 })
 
 const activeTab = computed({
   get: () => store.activeTab,
@@ -215,8 +215,6 @@ watch(
     if (val) {
       store.selectedId = null
       store.detail = null
-      store.fetchRunningCount()
-      store.fetchStatusTotals()
       store.fetchList()
       store.startListPolling()
     } else {
@@ -226,26 +224,26 @@ watch(
 )
 
 onMounted(() => {
-  store.fetchList({ pageNum: 1, pageSize: 20 })
+  store.fetchList({ page: 1, size: 20 })
 })
 
 // 工具函数
 function tagType(status) {
-  if (status === 'processing') return 'primary'
+  if (status === 'running') return 'primary'
   if (status === 'completed') return 'success'
   if (status === 'failed') return 'danger'
   return 'info'
 }
 
 function statusClass(status) {
-  if (status === 'processing') return 'status-processing'
+  if (status === 'running') return 'status-running'
   if (status === 'completed') return 'status-completed'
   if (status === 'failed') return 'status-failed'
   return 'status-pending'
 }
 
 function statusText(status) {
-  if (status === 'processing') return '运行中'
+  if (status === 'running') return '运行中'
   if (status === 'completed') return '已完成'
   if (status === 'failed') return '失败'
   return '等待中'
@@ -344,8 +342,8 @@ async function handleDelete(item) {
         type: 'warning'
       }
     )
-    await store.deleteTask(item.id)
-    ElMessage.success('删除成功')
+    await store.cancelTask(item.id)
+    ElMessage.success('任务已取消')
   } catch (error) {
     if (error !== 'cancel') {
       ElMessage.error(error?.message || '删除失败')
@@ -459,7 +457,7 @@ async function handleDelete(item) {
   transform: translateY(-1px);
 }
 
-.task-card.status-processing {
+.task-card.status-running {
   background-color: rgba(64, 158, 255, 0.05);
   border-color: rgba(64, 158, 255, 0.2);
 }

+ 14 - 82
frontend/vue-demo/src/stores/taskCenter.js

@@ -1,5 +1,5 @@
 import { defineStore } from 'pinia'
-import { taskCenterApi } from '@/api'
+import { taskApi } from '@/api'
 
 export const useTaskCenterStore = defineStore('taskCenter', {
   state: () => ({
@@ -13,7 +13,7 @@ export const useTaskCenterStore = defineStore('taskCenter', {
     detail: null,
     runningTotal: 0,
     statusTotals: {
-      processing: 0,
+      running: 0,
       failed: 0,
       pending: 0
     },
@@ -28,40 +28,6 @@ export const useTaskCenterStore = defineStore('taskCenter', {
   },
 
   actions: {
-    /**
-     * 任务开始时调用:打开任务中心并显示全部任务
-     */
-    async notifyTaskStarted({ documentId } = {}) {
-      this.open = true
-      this.selectedId = null
-      this.detail = null
-      // 先设置为全部,确保能看到任务
-      this.activeTab = 'all'
-      
-      // 等待一小段时间让后端任务创建完成
-      await new Promise(resolve => setTimeout(resolve, 500))
-      
-      this.fetchRunningCount()
-      this.fetchStatusTotals()
-      await this.fetchList({ pageNum: 1, pageSize: 20 })
-      this.startListPolling()
-
-      // 如果传入了 documentId,尝试选中该任务
-      if (documentId) {
-        // 再等待一小段时间让任务创建完成
-        setTimeout(async () => {
-          try {
-            const detail = await taskCenterApi.getByDocumentId(documentId)
-            if (detail && detail.id) {
-              this.selectTask(detail.id)
-            }
-          } catch (e) {
-            console.warn('获取任务详情失败:', e)
-          }
-        }, 1000)
-      }
-    },
-
     toggleOpen() {
       this.open = !this.open
       if (!this.open) {
@@ -69,7 +35,6 @@ export const useTaskCenterStore = defineStore('taskCenter', {
         this.stopListPolling()
       } else {
         this.fetchList()
-        this.fetchStatusTotals()
         this.startListPolling()
       }
     },
@@ -84,20 +49,21 @@ export const useTaskCenterStore = defineStore('taskCenter', {
       this.activeTab = tab || 'all'
     },
 
-    async fetchList({ pageNum = 1, pageSize = 20, silent = false } = {}) {
+    async fetchList({ page = 1, size = 20, silent = false } = {}) {
       if (!silent) {
         this.listLoading = true
       }
       try {
         const params = {
           status: this.activeTab === 'all' ? undefined : this.activeTab,
-          pageNum,
-          pageSize
+          page,
+          size
         }
-        const resp = await taskCenterApi.list(params)
-        // resp 是分页对象 { records, total, current, size }
+        const resp = await taskApi.list(params)
         this.list = resp?.records || []
         this.total = resp?.total || 0
+        // 统计运行中的任务数
+        this.runningTotal = this.list.filter(t => t.status === 'running').length
       } catch (e) {
         console.error('获取任务列表失败:', e)
       } finally {
@@ -107,32 +73,6 @@ export const useTaskCenterStore = defineStore('taskCenter', {
       }
     },
 
-    async fetchRunningCount() {
-      try {
-        const resp = await taskCenterApi.list({
-          status: 'processing',
-          pageNum: 1,
-          pageSize: 1
-        })
-        this.runningTotal = resp?.total || 0
-      } catch (e) {
-        // 失败不影响主流程
-      }
-    },
-
-    async fetchStatusTotals() {
-      try {
-        const stats = await taskCenterApi.getStatistics()
-        this.statusTotals = {
-          processing: stats?.processing || 0,
-          failed: stats?.failed || 0,
-          pending: stats?.pending || 0
-        }
-      } catch (e) {
-        // 统计失败不影响主流程
-      }
-    },
-
     async selectTask(taskId) {
       if (!taskId) return
       this.selectedId = taskId
@@ -145,7 +85,7 @@ export const useTaskCenterStore = defineStore('taskCenter', {
         this.detailLoading = true
       }
       try {
-        const resp = await taskCenterApi.getById(taskId)
+        const resp = await taskApi.getById(taskId)
         this.detail = resp
       } catch (e) {
         console.error('获取任务详情失败:', e)
@@ -160,7 +100,7 @@ export const useTaskCenterStore = defineStore('taskCenter', {
       this.stopPolling()
       if (!this.open) return
       const status = this.detail?.status
-      if (status !== 'processing') return
+      if (status !== 'running' && status !== 'pending') return
 
       this.pollTimer = setInterval(async () => {
         if (!this.selectedId) return
@@ -168,10 +108,7 @@ export const useTaskCenterStore = defineStore('taskCenter', {
           await this.fetchDetail(this.selectedId, true)
           const s = this.detail?.status
           if (s === 'completed' || s === 'failed') {
-            // 任务结束后刷新列表和统计
-            await this.fetchList({ pageNum: 1, pageSize: 20, silent: true })
-            this.fetchRunningCount()
-            this.fetchStatusTotals()
+            await this.fetchList({ page: 1, size: 20, silent: true })
             this.stopPolling()
           }
         } catch (e) {
@@ -197,8 +134,7 @@ export const useTaskCenterStore = defineStore('taskCenter', {
           return
         }
         try {
-          await this.fetchList({ pageNum: 1, pageSize: 20, silent: true })
-          this.fetchStatusTotals()
+          await this.fetchList({ page: 1, size: 20, silent: true })
         } catch (e) {
           // 轮询异常不停止
         }
@@ -212,18 +148,14 @@ export const useTaskCenterStore = defineStore('taskCenter', {
       }
     },
 
-    async deleteTask(taskId) {
+    async cancelTask(taskId) {
       try {
-        await taskCenterApi.delete(taskId)
-        // 如果删除的是当前选中的任务,清除选中状态
+        await taskApi.cancel(taskId)
         if (this.selectedId === taskId) {
           this.selectedId = null
           this.detail = null
         }
-        // 刷新列表和统计
         await this.fetchList()
-        this.fetchRunningCount()
-        this.fetchStatusTotals()
         return true
       } catch (e) {
         throw e

+ 169 - 112
frontend/vue-demo/src/stores/template.js

@@ -1,170 +1,227 @@
 import { defineStore } from 'pinia'
 import { ref, computed } from 'vue'
-import { templateApi, sourceFileApi, variableApi } from '@/api'
+import { projectApi, elementApi, valueApi, attachmentApi, entityApi, ruleApi } from '@/api'
 
-export const useTemplateStore = defineStore('template', () => {
+export const useProjectStore = defineStore('project', () => {
   // 状态
-  const templates = ref([])
-  const currentTemplate = ref(null)
-  const sourceFiles = ref([])
-  const variables = ref([])
-  const groupedVariables = ref({})
+  const projects = ref([])
+  const projectTotal = ref(0)
+  const currentProject = ref(null)
+  const elements = ref([])
+  const values = ref([])
+  const attachments = ref([])
+  const entities = ref([])
+  const rules = ref([])
   const loading = ref(false)
 
-  // 类别配置
-  const categoryConfig = {
-    entity: { label: '核心实体', color: '#1890ff', icon: '🏢' },
-    concept: { label: '概念/技术', color: '#722ed1', icon: '💡' },
-    data: { label: '数据/指标', color: '#52c41a', icon: '📊' },
-    location: { label: '地点/组织', color: '#faad14', icon: '📍' },
-    asset: { label: '资源模板', color: '#eb2f96', icon: '📑' },
-    other: { label: '其他', color: '#8c8c8c', icon: '📌' }
-  }
-
   // 计算属性
-  const variableCount = computed(() => variables.value.length)
-  const sourceFileCount = computed(() => sourceFiles.value.length)
+  const elementCount = computed(() => elements.value.length)
+  const attachmentCount = computed(() => attachments.value.length)
+  const ruleCount = computed(() => rules.value.length)
+  const filledCount = computed(() => values.value.filter(v => v.isFilled).length)
 
-  // 方法
-  async function fetchTemplates(page = 1, size = 20) {
+  // ==================== 项目 ====================
+
+  async function fetchProjects(params = {}) {
     loading.value = true
     try {
-      const data = await templateApi.list(page, size)
-      templates.value = data.records || data
+      const data = await projectApi.list(params)
+      projects.value = data.records || data
+      projectTotal.value = data.total || projects.value.length
       return data
     } finally {
       loading.value = false
     }
   }
 
-  async function fetchTemplateDetail(id) {
+  async function fetchProjectDetail(id) {
     loading.value = true
     try {
-      currentTemplate.value = await templateApi.getById(id)
-      // 同时获取来源文件和变量
-      await Promise.all([
-        fetchSourceFiles(id),
-        fetchVariables(id)
-      ])
-      return currentTemplate.value
+      const data = await projectApi.getById(id)
+      currentProject.value = data.project
+      elements.value = data.elements || []
+      values.value = data.values || []
+      return data
     } finally {
       loading.value = false
     }
   }
 
-  async function fetchSourceFiles(templateId) {
-    sourceFiles.value = await sourceFileApi.list(templateId)
-    return sourceFiles.value
+  async function createProject(data) {
+    const project = await projectApi.create(data)
+    projects.value.unshift(project)
+    return project
   }
 
-  async function fetchVariables(templateId) {
-    variables.value = await variableApi.list(templateId)
-    return variables.value
+  async function updateProject(id, data) {
+    const project = await projectApi.update(id, data)
+    if (currentProject.value?.id === id) {
+      currentProject.value = { ...currentProject.value, ...project }
+    }
+    return project
   }
 
-  async function fetchGroupedVariables(templateId) {
-    groupedVariables.value = await variableApi.listGrouped(templateId)
-    return groupedVariables.value
+  async function deleteProject(id) {
+    await projectApi.delete(id)
+    projects.value = projects.value.filter(p => p.id !== id)
+    if (currentProject.value?.id === id) {
+      currentProject.value = null
+    }
   }
 
-  async function createTemplate(data) {
-    const template = await templateApi.create(data)
-    templates.value.unshift(template)
-    return template
+  async function copyProject(id) {
+    const project = await projectApi.copy(id)
+    projects.value.unshift(project)
+    return project
   }
 
-  async function updateTemplate(id, data) {
-    const template = await templateApi.update(id, data)
-    if (currentTemplate.value?.id === id) {
-      currentTemplate.value = { ...currentTemplate.value, ...template }
+  async function archiveProject(id) {
+    await projectApi.archive(id)
+    const p = projects.value.find(p => p.id === id)
+    if (p) p.status = 'archived'
+    if (currentProject.value?.id === id) {
+      currentProject.value.status = 'archived'
     }
-    return template
   }
 
-  async function deleteTemplate(id) {
-    await templateApi.delete(id)
-    templates.value = templates.value.filter(t => t.id !== id)
-    if (currentTemplate.value?.id === id) {
-      currentTemplate.value = null
-    }
+  // ==================== 要素 ====================
+
+  async function fetchElements(projectId) {
+    elements.value = await elementApi.list(projectId)
+    return elements.value
   }
 
-  async function addVariable(templateId, data) {
-    const variable = await variableApi.add(templateId, data)
-    variables.value.push(variable)
-    return variable
+  async function addElement(projectId, data) {
+    const element = await elementApi.add(projectId, data)
+    elements.value.push(element)
+    return element
   }
 
-  async function updateVariable(id, data) {
-    const variable = await variableApi.update(id, data)
-    const index = variables.value.findIndex(v => v.id === id)
+  async function deleteElement(projectId, elementId) {
+    await elementApi.delete(projectId, elementId)
+    elements.value = elements.value.filter(e => e.id !== elementId)
+  }
+
+  // ==================== 要素值 ====================
+
+  async function fetchValues(projectId) {
+    values.value = await valueApi.list(projectId)
+    return values.value
+  }
+
+  async function updateValue(projectId, elementKey, data) {
+    const val = await valueApi.update(projectId, elementKey, data)
+    const index = values.value.findIndex(v => v.elementKey === elementKey)
     if (index !== -1) {
-      variables.value[index] = variable
+      values.value[index] = val
     }
-    return variable
+    return val
   }
 
-  async function deleteVariable(id) {
-    await variableApi.delete(id)
-    variables.value = variables.value.filter(v => v.id !== id)
+  // ==================== 附件 ====================
+
+  async function fetchAttachments(projectId) {
+    attachments.value = await attachmentApi.list(projectId)
+    return attachments.value
   }
 
-  async function addSourceFile(templateId, data) {
-    const sourceFile = await sourceFileApi.add(templateId, data)
-    sourceFiles.value.push(sourceFile)
-    return sourceFile
+  async function uploadAttachment(projectId, file, displayName) {
+    const att = await attachmentApi.upload(projectId, file, displayName)
+    attachments.value.push(att)
+    return att
   }
 
-  async function updateSourceFile(id, data) {
-    const sourceFile = await sourceFileApi.update(id, data)
-    const index = sourceFiles.value.findIndex(s => s.id === id)
-    if (index !== -1) {
-      sourceFiles.value[index] = sourceFile
-    }
-    return sourceFile
+  async function deleteAttachment(attachmentId) {
+    await attachmentApi.delete(attachmentId)
+    attachments.value = attachments.value.filter(a => a.id !== attachmentId)
   }
 
-  async function deleteSourceFile(id) {
-    await sourceFileApi.delete(id)
-    sourceFiles.value = sourceFiles.value.filter(s => s.id !== id)
+  // ==================== 实体 ====================
+
+  async function fetchEntitiesByProject(projectId) {
+    entities.value = await entityApi.listByProject(projectId)
+    return entities.value
+  }
+
+  async function fetchEntitiesByAttachment(attachmentId) {
+    return await entityApi.listByAttachment(attachmentId)
+  }
+
+  // ==================== 规则 ====================
+
+  async function fetchRules(projectId) {
+    rules.value = await ruleApi.list(projectId)
+    return rules.value
+  }
+
+  async function createRule(projectId, data) {
+    const rule = await ruleApi.create(projectId, data)
+    rules.value.push(rule)
+    return rule
   }
 
+  async function deleteRule(ruleId) {
+    await ruleApi.delete(ruleId)
+    rules.value = rules.value.filter(r => r.id !== ruleId)
+  }
+
+  async function executeRule(ruleId) {
+    return await ruleApi.execute(ruleId)
+  }
+
+  async function batchExecuteRules(projectId) {
+    return await ruleApi.batchExecute(projectId)
+  }
+
+  // ==================== 重置 ====================
+
   function resetState() {
-    currentTemplate.value = null
-    sourceFiles.value = []
-    variables.value = []
-    groupedVariables.value = {}
+    currentProject.value = null
+    elements.value = []
+    values.value = []
+    attachments.value = []
+    entities.value = []
+    rules.value = []
   }
 
   return {
-    // 状态
-    templates,
-    currentTemplate,
-    sourceFiles,
-    variables,
-    groupedVariables,
+    projects,
+    projectTotal,
+    currentProject,
+    elements,
+    values,
+    attachments,
+    entities,
+    rules,
     loading,
-    categoryConfig,
-
-    // 计算属性
-    variableCount,
-    sourceFileCount,
-
-    // 方法
-    fetchTemplates,
-    fetchTemplateDetail,
-    fetchSourceFiles,
-    fetchVariables,
-    fetchGroupedVariables,
-    createTemplate,
-    updateTemplate,
-    deleteTemplate,
-    addVariable,
-    updateVariable,
-    deleteVariable,
-    addSourceFile,
-    updateSourceFile,
-    deleteSourceFile,
+
+    elementCount,
+    attachmentCount,
+    ruleCount,
+    filledCount,
+
+    fetchProjects,
+    fetchProjectDetail,
+    createProject,
+    updateProject,
+    deleteProject,
+    copyProject,
+    archiveProject,
+    fetchElements,
+    addElement,
+    deleteElement,
+    fetchValues,
+    updateValue,
+    fetchAttachments,
+    uploadAttachment,
+    deleteAttachment,
+    fetchEntitiesByProject,
+    fetchEntitiesByAttachment,
+    fetchRules,
+    createRule,
+    deleteRule,
+    executeRule,
+    batchExecuteRules,
     resetState
   }
 })

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 321 - 686
frontend/vue-demo/src/views/Editor.vue


+ 3 - 3
frontend/vue-demo/src/views/Login.vue

@@ -118,11 +118,11 @@ async function handleLogin() {
     try {
       const response = await authApi.login(form)
       
-      // 保存 Token
+      // 保存 Token(新后端返回结构: { accessToken, refreshToken, expiresIn, user: { id, username, realName, roles } })
       localStorage.setItem('accessToken', response.accessToken)
       localStorage.setItem('refreshToken', response.refreshToken)
-      localStorage.setItem('userId', response.userId)
-      localStorage.setItem('username', response.username)
+      localStorage.setItem('userId', response.user?.id)
+      localStorage.setItem('username', response.user?.realName || response.user?.username)
       
       if (rememberMe.value) {
         localStorage.setItem('rememberMe', 'true')

+ 2 - 29
frontend/vue-demo/src/views/Register.vue

@@ -155,35 +155,8 @@ const rules = {
 }
 
 async function handleRegister() {
-  if (!formRef.value) return
-  
-  await formRef.value.validate(async (valid) => {
-    if (!valid) return
-    
-    loading.value = true
-    try {
-      const response = await authApi.register({
-        username: form.username,
-        email: form.email,
-        password: form.password,
-        confirmPassword: form.confirmPassword
-      })
-      
-      // 注册成功后自动登录
-      localStorage.setItem('accessToken', response.accessToken)
-      localStorage.setItem('refreshToken', response.refreshToken)
-      localStorage.setItem('userId', response.userId)
-      localStorage.setItem('username', response.username)
-      
-      ElMessage.success('注册成功')
-      router.push('/')
-    } catch (error) {
-      console.error('注册失败:', error)
-      ElMessage.error(error.message || '注册失败,请稍后重试')
-    } finally {
-      loading.value = false
-    }
-  })
+  ElMessage.info('当前版本暂不支持自助注册,请联系管理员创建账号')
+  router.push('/login')
 }
 </script>
 

+ 4 - 11
frontend/vue-demo/vite.config.js

@@ -2,28 +2,21 @@ import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
 
 // ==================== 配置说明 ====================
-// 1. 本地开发(Java在本机): target 设为 'http://localhost:18520'
-// 2. 远程服务器: target 设为 'http://服务器IP:18520'
-// 
-// 修改下方 API_SERVER 变量即可切换
+// 后端统一入口: http://localhost:8001
+// 所有 API 路径: /api/v1/*
 
-const API_SERVER = process.env.API_SERVER || 'http://localhost:18520'
+const API_SERVER = process.env.API_SERVER || 'http://localhost:8001'
 
 export default defineConfig({
   plugins: [vue()],
   server: {
     port: 5173,
-    host: true, // 允许局域网访问
+    host: true,
     proxy: {
       '/api': {
         target: API_SERVER,
         changeOrigin: true,
         secure: false
-      },
-      '/auth': {
-        target: API_SERVER,
-        changeOrigin: true,
-        secure: false
       }
     }
   },

+ 519 - 0
mock_docs/parse_docx.py

@@ -0,0 +1,519 @@
+#!/usr/bin/env python3
+"""
+解析 原文.docx 并将结构化内容插入数据库
+- 提取段落(含格式:字体、大小、粗体、斜体、颜色、对齐等)
+- 提取表格(含合并单元格)
+- 提取图片(转 base64)
+- 按文档顺序组装 blocks JSON
+- 插入到 node_properties.prop_json 作为附件的文档内容
+"""
+
+import json
+import base64
+import re
+import sys
+import os
+from io import BytesIO
+from collections import OrderedDict
+
+import docx
+from docx import Document
+from docx.shared import Pt, Emu, Inches, Cm, Twips
+from docx.enum.text import WD_ALIGN_PARAGRAPH
+from docx.oxml.ns import qn
+from lxml import etree
+
+DOCX_PATH = os.path.join(os.path.dirname(__file__), "原文.docx")
+
+# ============================================================
+# 1. 图片提取
+# ============================================================
+
+def extract_images(doc):
+    """提取文档中所有图片,返回 {rId: base64_data_uri}"""
+    images = {}
+    for rel_id, rel in doc.part.rels.items():
+        if "image" in rel.reltype:
+            blob = rel.target_part.blob
+            # 判断图片类型
+            target = rel.target_ref.lower()
+            if target.endswith('.png'):
+                mime = 'image/png'
+            elif target.endswith('.jpg') or target.endswith('.jpeg'):
+                mime = 'image/jpeg'
+            elif target.endswith('.gif'):
+                mime = 'image/gif'
+            elif target.endswith('.bmp'):
+                mime = 'image/bmp'
+            elif target.endswith('.emf'):
+                mime = 'image/x-emf'
+            elif target.endswith('.wmf'):
+                mime = 'image/x-wmf'
+            else:
+                mime = 'image/png'
+            b64 = base64.b64encode(blob).decode('ascii')
+            images[rel_id] = f"data:{mime};base64,{b64}"
+    return images
+
+
+# ============================================================
+# 2. 段落/Run 格式提取
+# ============================================================
+
+def get_alignment(paragraph):
+    """获取段落对齐方式"""
+    align = paragraph.alignment
+    if align is None:
+        # 检查 pPr 中的 jc
+        pPr = paragraph._element.find(qn('w:pPr'))
+        if pPr is not None:
+            jc = pPr.find(qn('w:jc'))
+            if jc is not None:
+                return jc.get(qn('w:val'))
+        return None
+    align_map = {
+        WD_ALIGN_PARAGRAPH.LEFT: 'left',
+        WD_ALIGN_PARAGRAPH.CENTER: 'center',
+        WD_ALIGN_PARAGRAPH.RIGHT: 'right',
+        WD_ALIGN_PARAGRAPH.JUSTIFY: 'justify',
+    }
+    return align_map.get(align, None)
+
+
+def get_paragraph_style_info(paragraph):
+    """提取段落级别样式"""
+    style_info = {}
+    
+    pf = paragraph.paragraph_format
+    
+    # 对齐
+    alignment = get_alignment(paragraph)
+    if alignment:
+        style_info['alignment'] = alignment
+    
+    # 缩进
+    if pf.left_indent:
+        style_info['indentLeft'] = int(pf.left_indent)  # EMU
+    if pf.right_indent:
+        style_info['indentRight'] = int(pf.right_indent)
+    if pf.first_line_indent:
+        val = int(pf.first_line_indent)
+        if val > 0:
+            style_info['indentFirstLine'] = val
+        else:
+            style_info['indentHanging'] = -val
+    
+    # 间距
+    if pf.space_before:
+        style_info['spacingBefore'] = int(pf.space_before)
+    if pf.space_after:
+        style_info['spacingAfter'] = int(pf.space_after)
+    if pf.line_spacing:
+        style_info['lineSpacing'] = float(pf.line_spacing)
+    
+    return style_info
+
+
+def get_run_format(run):
+    """提取 Run 级别格式"""
+    fmt = {}
+    font = run.font
+    
+    if font.name:
+        fmt['fontFamily'] = font.name
+    if font.size:
+        fmt['fontSize'] = font.size.pt
+    if font.bold:
+        fmt['bold'] = True
+    if font.italic:
+        fmt['italic'] = True
+    if font.underline and font.underline is not True:
+        fmt['underline'] = str(font.underline)
+    elif font.underline is True:
+        fmt['underline'] = 'single'
+    if font.strike:
+        fmt['strikeThrough'] = True
+    if font.color and font.color.rgb:
+        fmt['color'] = str(font.color.rgb)
+    try:
+        if font.highlight_color:
+            fmt['highlightColor'] = str(font.highlight_color)
+    except (ValueError, KeyError):
+        pass  # skip unsupported highlight values like 'none'
+    
+    # 上下标
+    if font.superscript:
+        fmt['verticalAlign'] = 'superscript'
+    elif font.subscript:
+        fmt['verticalAlign'] = 'subscript'
+    
+    return fmt
+
+
+def detect_paragraph_type(paragraph):
+    """检测段落类型(标题、目录、正文等)"""
+    style_name = paragraph.style.name if paragraph.style else ''
+    
+    if style_name.startswith('Heading') or style_name.startswith('heading'):
+        level = re.search(r'\d+', style_name)
+        if level:
+            return f'heading{level.group()}'
+        return 'heading1'
+    
+    if style_name.startswith('toc ') or style_name.startswith('TOC'):
+        level = re.search(r'\d+', style_name)
+        lvl = level.group() if level else '1'
+        return f'toc{lvl}'
+    
+    if style_name.startswith('List'):
+        return 'list_item'
+    
+    # 检查是否是标题样式(通过 run 格式推断)
+    text = paragraph.text.strip()
+    if text and paragraph.runs:
+        first_run = paragraph.runs[0]
+        if first_run.font.bold and first_run.font.size:
+            size_pt = first_run.font.size.pt
+            if size_pt >= 18:
+                return 'heading1'
+            elif size_pt >= 16:
+                return 'heading2'
+            elif size_pt >= 14:
+                # 检查是否是章节标题(如 "1  企业概述")
+                if re.match(r'^\d+(\.\d+)*\s', text):
+                    dots = text.split()[0].count('.')
+                    if dots == 0:
+                        return 'heading1'
+                    elif dots == 1:
+                        return 'heading2'
+                    else:
+                        return 'heading3'
+    
+    return 'paragraph'
+
+
+# ============================================================
+# 3. 检查段落中的内联图片
+# ============================================================
+
+def get_paragraph_images(paragraph, images_map):
+    """检查段落中是否包含内联图片,返回图片列表"""
+    inline_images = []
+    for run in paragraph.runs:
+        run_xml = run._element
+        drawings = run_xml.findall(qn('w:drawing'))
+        for drawing in drawings:
+            # 查找 blip (图片引用)
+            blips = drawing.findall('.//' + qn('a:blip'))
+            for blip in blips:
+                embed = blip.get(qn('r:embed'))
+                if embed and embed in images_map:
+                    # 获取图片尺寸
+                    extent = drawing.find('.//' + qn('wp:extent'))
+                    width = height = None
+                    if extent is not None:
+                        cx = extent.get('cx')
+                        cy = extent.get('cy')
+                        if cx:
+                            width = int(cx) / 914400  # EMU to inches, then to px approx
+                        if cy:
+                            height = int(cy) / 914400
+                    
+                    inline_images.append({
+                        'rId': embed,
+                        'src': images_map[embed],
+                        'widthInch': round(width, 2) if width else None,
+                        'heightInch': round(height, 2) if height else None,
+                    })
+    return inline_images
+
+
+# ============================================================
+# 4. 表格提取
+# ============================================================
+
+def parse_table(table):
+    """解析表格为结构化数据"""
+    rows_data = []
+    for row in table.rows:
+        cells_data = []
+        for cell in row.cells:
+            cell_text = cell.text.strip()
+            # 检查合并
+            tc = cell._tc
+            grid_span = tc.find(qn('w:tcPr'))
+            colspan = 1
+            if grid_span is not None:
+                gs = grid_span.find(qn('w:gridSpan'))
+                if gs is not None:
+                    colspan = int(gs.get(qn('w:val'), 1))
+            
+            # 单元格内段落格式
+            paras = []
+            for p in cell.paragraphs:
+                runs = []
+                for r in p.runs:
+                    run_data = {'text': r.text}
+                    fmt = get_run_format(r)
+                    if fmt:
+                        run_data['format'] = fmt
+                    runs.append(run_data)
+                if runs:
+                    para_data = {'runs': runs}
+                    align = get_alignment(p)
+                    if align:
+                        para_data['alignment'] = align
+                    paras.append(para_data)
+            
+            cell_data = {
+                'text': cell_text,
+                'colspan': colspan,
+            }
+            if paras:
+                cell_data['paragraphs'] = paras
+            cells_data.append(cell_data)
+        rows_data.append(cells_data)
+    
+    return {
+        'rows': len(table.rows),
+        'cols': len(table.columns),
+        'data': rows_data,
+    }
+
+
+# ============================================================
+# 5. 按文档 XML 顺序遍历(段落+表格交错)
+# ============================================================
+
+def iter_block_items(doc):
+    """
+    按文档 body 中的顺序迭代段落和表格。
+    返回 (type, item) 元组,type 为 'paragraph' 或 'table'。
+    """
+    body = doc.element.body
+    for child in body:
+        if child.tag == qn('w:p'):
+            yield ('paragraph', docx.text.paragraph.Paragraph(child, doc))
+        elif child.tag == qn('w:tbl'):
+            yield ('table', docx.table.Table(child, doc))
+        elif child.tag == qn('w:sectPr'):
+            pass  # section properties, skip
+
+
+# ============================================================
+# 6. 主解析函数
+# ============================================================
+
+def parse_document(docx_path):
+    """解析 Word 文档,返回结构化 JSON"""
+    doc = Document(docx_path)
+    
+    # 提取所有图片
+    images_map = extract_images(doc)
+    print(f"  提取图片: {len(images_map)} 张")
+    
+    # 页面设置
+    section = doc.sections[0]
+    page_info = {
+        'widthMm': round(section.page_width.mm, 1) if section.page_width else 210,
+        'heightMm': round(section.page_height.mm, 1) if section.page_height else 297,
+        'marginTopMm': round(section.top_margin.mm, 1) if section.top_margin else 25.4,
+        'marginBottomMm': round(section.bottom_margin.mm, 1) if section.bottom_margin else 25.4,
+        'marginLeftMm': round(section.left_margin.mm, 1) if section.left_margin else 31.8,
+        'marginRightMm': round(section.right_margin.mm, 1) if section.right_margin else 31.8,
+    }
+    
+    blocks = []
+    block_id = 0
+    
+    for block_type, item in iter_block_items(doc):
+        if block_type == 'paragraph':
+            paragraph = item
+            text = paragraph.text
+            
+            # 检测段落类型
+            para_type = detect_paragraph_type(paragraph)
+            
+            # 段落样式
+            style_info = get_paragraph_style_info(paragraph)
+            
+            # 检查内联图片
+            inline_imgs = get_paragraph_images(paragraph, images_map)
+            
+            # 提取 runs
+            runs = []
+            for r in paragraph.runs:
+                run_text = r.text
+                if not run_text:
+                    continue
+                run_data = {'text': run_text}
+                fmt = get_run_format(r)
+                if fmt:
+                    run_data.update(fmt)
+                runs.append(run_data)
+            
+            block = {
+                'id': f'b{block_id}',
+                'type': para_type,
+            }
+            
+            if runs:
+                block['runs'] = runs
+            
+            if style_info:
+                block['style'] = style_info
+            
+            if inline_imgs:
+                block['images'] = inline_imgs
+            
+            # 即使空段落也保留(用于间距)
+            if not runs and not inline_imgs:
+                block['runs'] = [{'text': ''}]
+            
+            blocks.append(block)
+            block_id += 1
+            
+        elif block_type == 'table':
+            table_data = parse_table(item)
+            block = {
+                'id': f'b{block_id}',
+                'type': 'table',
+                'table': table_data,
+            }
+            blocks.append(block)
+            block_id += 1
+    
+    print(f"  解析完成: {len(blocks)} 个块")
+    
+    return {
+        'page': page_info,
+        'blocks': blocks,
+        'totalBlocks': len(blocks),
+    }
+
+
+# ============================================================
+# 7. 插入数据库
+# ============================================================
+
+def insert_to_db(doc_json):
+    """将解析后的文档内容插入数据库"""
+    import psycopg2
+    
+    conn = psycopg2.connect(
+        host='127.0.0.1',
+        port=5432,
+        dbname='lingyue_zhibao',
+        user='postgres',
+        password='postgres'
+    )
+    cur = conn.cursor()
+    
+    try:
+        # 1. 创建附件节点 (原文.docx)
+        att_node_key = 'ATT-2024-003'
+        att_name = '原文-复审报告'
+        
+        # 检查是否已存在
+        cur.execute("SELECT id FROM nodes WHERE node_key = %s AND node_type = 'ATTACHMENT'", (att_node_key,))
+        row = cur.fetchone()
+        
+        if row:
+            att_id = row[0]
+            print(f"  附件节点已存在: id={att_id}")
+            # 更新 doc_content 属性
+            cur.execute("""
+                INSERT INTO node_properties (node_id, prop_key, prop_json)
+                VALUES (%s, 'doc_content', %s::jsonb)
+                ON CONFLICT (node_id, prop_key) 
+                DO UPDATE SET prop_json = EXCLUDED.prop_json, updated_at = now()
+            """, (att_id, json.dumps(doc_json, ensure_ascii=False)))
+        else:
+            # 获取下一个 id
+            cur.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM nodes WHERE id >= 400 AND id < 500")
+            att_id = cur.fetchone()[0]
+            if att_id < 402:
+                att_id = 402
+            
+            cur.execute("""
+                INSERT INTO nodes (id, node_type, node_key, name, status, created_by, created_at, updated_at)
+                VALUES (%s, 'ATTACHMENT', %s, %s, 'active', 1, now(), now())
+            """, (att_id, att_node_key, att_name))
+            print(f"  创建附件节点: id={att_id}")
+            
+            # 插入基本属性
+            cur.execute("""
+                INSERT INTO node_properties (node_id, prop_key, prop_value) VALUES
+                (%s, 'display_name', '原文.docx'),
+                (%s, 'file_type', 'docx'),
+                (%s, 'file_size', '3538608')
+            """, (att_id, att_id, att_id))
+            
+            # 插入文档内容
+            cur.execute("""
+                INSERT INTO node_properties (node_id, prop_key, prop_json)
+                VALUES (%s, 'doc_content', %s::jsonb)
+            """, (att_id, json.dumps(doc_json, ensure_ascii=False)))
+            
+            # 创建边:PROJECT -> ATTACHMENT
+            cur.execute("""
+                INSERT INTO edges (from_node_id, to_node_id, edge_type)
+                SELECT 10, %s, 'HAS_ATTACHMENT'
+                WHERE NOT EXISTS (
+                    SELECT 1 FROM edges WHERE from_node_id = 10 AND to_node_id = %s AND edge_type = 'HAS_ATTACHMENT'
+                )
+            """, (att_id, att_id))
+        
+        conn.commit()
+        print(f"  数据库插入成功")
+        
+    except Exception as e:
+        conn.rollback()
+        print(f"  数据库错误: {e}")
+        raise
+    finally:
+        cur.close()
+        conn.close()
+
+
+# ============================================================
+# Main
+# ============================================================
+
+if __name__ == '__main__':
+    print("=" * 60)
+    print("解析 原文.docx ...")
+    print("=" * 60)
+    
+    doc_json = parse_document(DOCX_PATH)
+    
+    # 保存 JSON 到文件(调试用,不含 base64 图片)
+    debug_json = json.loads(json.dumps(doc_json))
+    # 统计图片大小
+    total_img_size = 0
+    img_count = 0
+    for block in debug_json['blocks']:
+        if 'images' in block:
+            for img in block['images']:
+                if 'src' in img:
+                    total_img_size += len(img['src'])
+                    img_count += 1
+    
+    print(f"  图片总大小(base64): {total_img_size / 1024 / 1024:.1f} MB ({img_count} 张)")
+    print(f"  JSON 总大小: {len(json.dumps(doc_json, ensure_ascii=False)) / 1024 / 1024:.1f} MB")
+    
+    print("\n插入数据库...")
+    insert_to_db(doc_json)
+    
+    # 保存精简版 JSON(不含 base64)用于调试
+    for block in debug_json['blocks']:
+        if 'images' in block:
+            for img in block['images']:
+                if 'src' in img:
+                    img['src'] = img['src'][:50] + '...[truncated]'
+    
+    debug_path = os.path.join(os.path.dirname(__file__), "parsed_doc_debug.json")
+    with open(debug_path, 'w', encoding='utf-8') as f:
+        json.dump(debug_json, f, ensure_ascii=False, indent=2)
+    print(f"\n调试 JSON 已保存: {debug_path}")
+    print("完成!")

binární
mock_docs/原文.docx


binární
mock_docs/模板.docx


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů