Просмотр исходного кода

Merge branch 'ai' into allTestUrgent

Zachary 2 дней назад
Родитель
Сommit
977aa6fe3a
29 измененных файлов с 3494 добавлено и 50 удалено
  1. 45 0
      .claude/settings.local.json
  2. 925 0
      src/AIManagement/KBM/API.md
  3. 614 0
      src/AIManagement/KBM/DocumentDetail.vue
  4. 246 0
      src/AIManagement/KBM/DocumentTable.vue
  5. 187 0
      src/AIManagement/KBM/KnowledgeBaseList.vue
  6. 207 0
      src/AIManagement/KBM/MetadataConfig.vue
  7. 210 0
      src/AIManagement/KBM/SearchTest.vue
  8. 190 0
      src/AIManagement/KBM/UploadDocument.vue
  9. 792 0
      src/AIManagement/KBM/index.vue
  10. 1 1
      src/HDrpManagement/reportcenter/index.vue
  11. 1 1
      src/HManagement/serviceDataScreen/index.vue
  12. 1 1
      src/HManagement/serviceDataScreen/indexCopy.vue
  13. 1 1
      src/SManagement/user/team-manage/TeamManage.vue
  14. 3 1
      src/SManagement/user/team-manage/components/RemoveTeam.vue
  15. 22 17
      src/SManagement/user/team-manage/components/add.vue
  16. 21 16
      src/SManagement/user/team-manage/components/edit.vue
  17. 1 1
      src/components/normal-basic-layout-new/details/index.vue
  18. 1 1
      src/components/normal-basic-layout-new/details/modules/tabs/tab.vue
  19. 1 1
      src/components/normal-basic-layout-new/details/modules/task/addTask.vue
  20. 1 1
      src/components/normal-basic-layout-new/index.vue
  21. 1 1
      src/components/normal-basic-layout-new/normalBook.vue
  22. 1 1
      src/components/normal-basic-layout/details/index.vue
  23. 1 1
      src/components/normal-basic-layout/details/modules/tabs/tab.vue
  24. 1 1
      src/components/normal-basic-layout/details/modules/task/addTask.vue
  25. 1 1
      src/components/normal-basic-layout/index.vue
  26. 1 1
      src/components/normal-basic-layout/normalBook.vue
  27. 13 0
      src/router/AIManagement.js
  28. 2 1
      src/router/index.js
  29. 3 1
      src/utils/tool.js

+ 45 - 0
.claude/settings.local.json

@@ -0,0 +1,45 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(npm install *)",
+      "Bash(timeout 30 npm run serve)",
+      "mcp__ide__getDiagnostics",
+      "Bash(pandoc \"D:\\\\Desktop\\\\AI管理-知识库管理.docx\" -o /tmp/doc-output.md)",
+      "Read(//tmp/**)",
+      "Bash(python3)",
+      "Bash(npx vue-cli-service *)",
+      "Read(//home/zxh/.vscode-server/data/Machine/**)",
+      "Read(//home/zxh/.config/Code/User/**)",
+      "Bash(ls ~/.vscode*/extensions/ 2>/dev/null | grep -i chinese || echo \"未找到中文语言包\")",
+      "Read(//home/zxh/**)",
+      "Bash(code --install-extension MS-CEINTL.vscode-language-pack-zh-hans)",
+      "Bash(dpkg -l)",
+      "Read(//opt/**)",
+      "Read(//usr/share/**)",
+      "Bash(find / -maxdepth 4 -name \"gitkraken\" -type f)",
+      "Bash(/home/zxh/下载/GitKrakenPatcher_Linux_x64 --help)",
+      "Read(//snap/gitkraken/307/**)",
+      "Bash(mount)",
+      "Bash(/snap/bin/gitkraken --version)",
+      "Bash(snap info *)",
+      "Bash(cat /snap/gitkraken/current/meta/snap.yaml 2>/dev/null; ls /snap/gitkraken/current/usr/share/gitkraken/ 2>/dev/null || ls /snap/gitkraken/current/usr/lib/gitkraken/ 2>/dev/null || find /snap/gitkraken/current -maxdepth 4 -name \"app.asar\" 2>/dev/null)",
+      "Read(//snap/gitkraken/current/usr/lib/**)",
+      "Bash(chmod +x /home/zxh/下载/GitKrakenPatcher_Linux_x64)",
+      "Bash(npx asar *)",
+      "Bash(mkdir -p ~/.local/share/gitkraken-patched)",
+      "Bash(cp -a /snap/gitkraken/current/usr/share/gitkraken/* ~/.local/share/gitkraken-patched/)",
+      "Bash(cp /home/zxh/下载/strings_12.1.1.json ~/.local/share/gitkraken-patched/app_extracted/src/strings.json && echo \"汉化文件已替换\")",
+      "Bash(rm resources/app.asar)",
+      "Bash(rm -rf app_extracted)",
+      "Bash(~/.local/share/gitkraken-patched/gitkraken --no-sandbox)",
+      "Bash(sudo mount --bind ~/.local/share/gitkraken-patched/resources/app.asar /snap/gitkraken/current/usr/share/gitkraken/resources/app.asar)",
+      "Bash(gtk-launch gitkraken_gitkraken)",
+      "Read(//etc/**)",
+      "Bash(sudo tee -a /etc/fstab)",
+      "Bash(sudo mount -a --target /snap/gitkraken/current/usr/share/gitkraken/resources/app.asar)"
+    ],
+    "additionalDirectories": [
+      "/home/zxh/下载"
+    ]
+  }
+}

+ 925 - 0
src/AIManagement/KBM/API.md

@@ -0,0 +1,925 @@
+# 知识库管理 - 后端接口对接文档
+
+## 概述
+
+本文档梳理知识库管理模块前端所需的所有后端接口。接口统一遵循现有项目的调用规范:
+
+```
+请求方式: this.$api.requested({ classname, method, content })
+请求路径: POST /yos/rest/index
+Content-Type: application/json;charset=UTF-8
+```
+
+---
+
+## 接口清单
+
+### 一、知识库管理(5 个接口)
+
+#### 1.1 获取知识库列表
+
+> 页面加载时调用,返回全部知识库
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase` |
+| method | `getList` |
+
+**请求参数 (content):**
+```json
+{
+  "pageNumber": 1,
+  "pageSize": 999,
+  "where": {
+    "condition": "",
+    "tablefilter": {}
+  }
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "data": {
+    "list": [
+      {
+        "id": 1,
+        "name": "产品知识库",
+        "description": "产品相关文档、功能说明",
+        "tags": ["产品手册", "版本发布"],
+        "documentCount": 12,
+        "createdAt": "2025-01-15 10:30:00",
+        "updatedAt": "2025-06-20 14:22:00"
+      }
+    ],
+    "total": 3
+  }
+}
+```
+
+---
+
+#### 1.2 创建空知识库
+
+> 点击"新建知识库"后调用
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase` |
+| method | `create` |
+
+**请求参数 (content):**
+```json
+{
+  "name": "产品知识库",
+  "description": "产品相关文档和资料"
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "data": {
+    "id": 4,
+    "name": "产品知识库",
+    "description": "产品相关文档和资料",
+    "tags": [],
+    "createdAt": "2025-06-20 14:22:00",
+    "updatedAt": "2025-06-20 14:22:00"
+  },
+  "message": "创建成功"
+}
+```
+
+---
+
+#### 1.3 更新知识库
+
+> 右键"重命名"时调用
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase` |
+| method | `update` |
+
+**请求参数 (content):**
+```json
+{
+  "id": 1,
+  "name": "产品知识库(更新)",
+  "description": "更新后的描述"
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "message": "更新成功"
+}
+```
+
+---
+
+#### 1.4 删除指定知识库
+
+> 右键"删除",二次确认后调用。级联删除其下全部文档。
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase` |
+| method | `delete` |
+
+**请求参数 (content):**
+```json
+{
+  "id": 1
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "message": "删除成功"
+}
+```
+
+---
+
+#### 1.5 设置知识库标签
+
+> 右键"设置标签",在弹窗中增删标签后调用
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase` |
+| method | `setTags` |
+
+**请求参数 (content):**
+```json
+{
+  "id": 1,
+  "tags": ["产品手册", "FAQ", "新标签"]
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "message": "标签设置成功"
+}
+```
+
+---
+
+### 二、知识库详情(1 个接口)
+
+#### 2.1 获取指定知识库的详细信息
+
+> 选中知识库后,在右侧面板顶部展示
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase` |
+| method | `getDetail` |
+
+**请求参数 (content):**
+```json
+{
+  "id": 1
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "data": {
+    "id": 1,
+    "name": "产品知识库",
+    "description": "产品相关文档和资料",
+    "tags": ["产品手册"],
+    "documentCount": 12,
+    "createdAt": "2025-01-15 10:30:00",
+    "updatedAt": "2025-06-20 14:22:00"
+  }
+}
+```
+
+**说明:** 若页面进入时已通过列表接口获取了基本信息,此接口为可选(用于刷新最新状态)。
+
+---
+
+### 三、文档管理(9 个接口)
+
+#### 3.1 获取知识库文档列表
+
+> 选中知识库后,在"文档管理"Tab 分页展示。支持按名称搜索和嵌入状态筛选。
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase.document` |
+| method | `getList` |
+
+**请求参数 (content):**
+```json
+{
+  "pageNumber": 1,
+  "pageSize": 20,
+  "where": {
+    "condition": "",
+    "tablefilter": {
+      "knowledgeBaseId": 1,
+      "embedStatus": "",
+      "status": ""
+    }
+  }
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "data": {
+    "list": [
+      {
+        "id": 1,
+        "knowledgeBaseId": 1,
+        "name": "产品功能清单_v3.2.xlsx",
+        "size": "1.2 MB",
+        "sizeBytes": 1258291,
+        "type": "xlsx",
+        "version": 5,
+        "status": "enabled",
+        "embedStatus": "completed",
+        "embedProgress": 100,
+        "uploadTime": "2025-06-20 14:22:00",
+        "metadata": {
+          "productLine": "CRM系统",
+          "applicableRole": ["市场", "销售"],
+          "validUntil": "2026-06-20"
+        }
+      }
+    ],
+    "total": 12
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| status | string | `enabled` 启用 / `disabled` 禁用 |
+| embedStatus | string | `pending` 待处理 / `processing` 处理中 / `completed` 已完成 / `failed` 失败 |
+| embedProgress | number | 0-100,向量化进度百分比 |
+| metadata | object | 动态键值对,按元数据配置字段存入 |
+
+---
+
+#### 3.2 上传文档
+
+> 上传弹窗提交后调用。需同时支持文件流上传和元数据提交。
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase.document` |
+| method | `upload` |
+
+**请求参数 (content):**
+```json
+{
+  "knowledgeBaseId": 1,
+  "metadata": {
+    "productLine": "CRM系统",
+    "applicableRole": ["市场", "销售"],
+    "validUntil": "2026-06-20"
+  }
+}
+```
+
+**说明:** 文件和 content 通过 FormData 一起提交。前端使用 `el-upload` 组件,后端需处理 `multipart/form-data` 格式。若走 JSON 接口,文件需先转为 Base64 或走独立上传通道(如 Huawei OBS)获取 fileKey 后再调用此接口。
+
+**建议方案(与现有 OBS 模式一致):**
+1. 前端先调 OBS 获取上传 URL → 直传文件 → 获取 fileKey
+2. 再调此接口提交 fileKey + 元数据
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "data": {
+    "id": 9,
+    "name": "产品功能清单_v3.2.xlsx",
+    "embedStatus": "pending",
+    "embedProgress": 0
+  },
+  "message": "上传成功,正在进行向量化处理"
+}
+```
+
+---
+
+#### 3.3 获取文档嵌入状态(进度)
+
+> 上传文档后,前端轮询此接口显示向量化进度条。轮询间隔约 800ms,完成后停止。
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase.document` |
+| method | `getEmbedStatus` |
+
+**请求参数 (content):**
+```json
+{
+  "id": 9
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "data": {
+    "id": 9,
+    "embedStatus": "processing",
+    "embedProgress": 65
+  }
+}
+```
+
+**说明:** 前端在 `embedStatus === 'completed'` 或 `'failed'` 时停止轮询。
+
+---
+
+#### 3.4 更新文档(替换文件)
+
+> 在详情面板点击"更新文档",替换已有文件内容,版本号递增。
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase.document` |
+| method | `updateFile` |
+
+**请求参数 (content):**
+```json
+{
+  "id": 1,
+  "fileKey": "obs://bucket/path/new_file_v2.pdf"
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "data": {
+    "version": 6,
+    "embedStatus": "pending",
+    "embedProgress": 0
+  },
+  "message": "文件已更新,正在重新向量化"
+}
+```
+
+---
+
+#### 3.5 更新文档状态(启用/禁用)
+
+> 表格行中的开关切换,或详情面板点击"启用/禁用"。
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase.document` |
+| method | `updateStatus` |
+
+**请求参数 (content):**
+```json
+{
+  "id": 1,
+  "status": "disabled"
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "message": "文档已禁用"
+}
+```
+
+---
+
+#### 3.6 设置文档元数据
+
+> 在文档详情面板编辑元数据后保存。
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase.document` |
+| method | `setMetadata` |
+
+**请求参数 (content):**
+```json
+{
+  "id": 1,
+  "metadata": {
+    "productLine": "ERP系统",
+    "applicableRole": ["售后"],
+    "validUntil": "2026-12-31",
+    "docType": "技术文档"
+  }
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "message": "元数据保存成功"
+}
+```
+
+---
+
+#### 3.7 获取指定文档的详细信息
+
+> 点击文档名或"详情"按钮时调用。
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase.document` |
+| method | `getDetail` |
+
+**请求参数 (content):**
+```json
+{
+  "id": 1
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "data": {
+    "id": 1,
+    "knowledgeBaseId": 1,
+    "name": "产品功能清单_v3.2.xlsx",
+    "size": "1.2 MB",
+    "sizeBytes": 1258291,
+    "type": "xlsx",
+    "version": 5,
+    "status": "enabled",
+    "embedStatus": "completed",
+    "embedProgress": 100,
+    "uploadTime": "2025-06-20 14:22:00",
+    "metadata": {
+      "productLine": "CRM系统",
+      "applicableRole": ["市场", "销售"],
+      "validUntil": "2026-06-20"
+    },
+    "previewUrl": "https://obs.example.com/preview/doc_1.pdf",
+    "downloadUrl": "https://obs.example.com/download/doc_1.pdf",
+    "versionHistory": [
+      { "version": 5, "time": "2025-06-20 14:22:00", "changeDesc": "替换文件" },
+      { "version": 4, "time": "2025-05-10 09:00:00", "changeDesc": "更新元数据" }
+    ]
+  }
+}
+```
+
+**说明:** `previewUrl` 和 `downloadUrl` 若由 OBS 直签 URL 生成,可在获取详情时动态生成并返回。`versionHistory` 用于记录变更轨迹。
+
+---
+
+#### 3.8 删除指定文档
+
+> 表格或详情面板中点击删除,`el-popconfirm` 二次确认后调用。
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase.document` |
+| method | `delete` |
+
+**请求参数 (content):**
+```json
+{
+  "id": 1
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "message": "文档已删除"
+}
+```
+
+---
+
+#### 3.9 下载 / 批量下载指定文档
+
+> 表格行"下载"按钮,或勾选多条后"批量下载"。
+
+| 下载单个 | 批量下载 |
+|----------|----------|
+| classname: `knowledgeBase.document` | classname: `knowledgeBase.document` |
+| method: `download` | method: `batchDownload` |
+
+**下载单个请求:**
+```json
+{ "id": 1 }
+```
+
+**批量下载请求:**
+```json
+{ "ids": [1, 2, 3] }
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "data": {
+    "downloadUrl": "https://obs.example.com/pack/download_abc123.zip"
+  }
+}
+```
+
+**说明:** 单个下载返回文件直链 URL(前端 `window.open` 或创建 `<a>` 下载),批量下载建议后端打包为 ZIP 后返回下载链接。
+
+---
+
+### 四、元数据管理(6 个接口)
+
+> 元数据相关接口使用数字ID调用方式:`this.$api.requested({ id: '接口ID', content: {...} })`
+
+#### 4.1 获取元数据列表
+
+> "元数据配置"Tab 进入时调用,支持获取内置或自定义元数据
+
+**接口ID:** `2026052715313903`
+
+**请求参数 (content):**
+```json
+{
+  "dataset_id": "知识库ID",
+  "iscustom": true
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| dataset_id | String | 是 | 知识库唯一标识符 |
+| iscustom | Boolean | 否 | 是否获取自定义元数据,默认false(获取内置元数据) |
+
+**响应数据:**
+```json
+{
+  "code": 1,
+  "data": {
+    "doc_metadata": [
+      {
+        "id": "metadata_id_1",
+        "name": "文档分类",
+        "type": "string"
+      }
+    ]
+  }
+}
+```
+
+**说明:** 
+- 系统元数据响应:`data.fields[]`(无id)
+- 自定义元数据响应:`data.doc_metadata[]`(有id)
+
+---
+
+#### 4.2 创建元数据
+
+> "新建字段"弹窗提交后调用
+
+**接口ID:** `2026052715313904`
+
+**请求参数 (content):**
+```json
+{
+  "dataset_id": "知识库ID",
+  "name": "元数据名称",
+  "type": "string"
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| dataset_id | String | 是 | 知识库唯一标识符 |
+| name | String | 是 | 元数据名称 |
+| type | String | 是 | 元数据类型:`string`/`number`/`time` |
+
+**响应数据:**
+```json
+{
+  "code": 1,
+  "data": {},
+  "errmsg": null
+}
+```
+
+**类型映射:** 前端类型需映射到API类型
+| 前端类型 | API类型 |
+|----------|---------|
+| text | string |
+| select | string |
+| multiSelect | string |
+| date | time |
+
+---
+
+#### 4.3 删除元数据
+
+> 删除指定知识库中的特定元数据项(仅自定义字段可删除)
+
+**接口ID:** `2026052715313905`
+
+**请求参数 (content):**
+```json
+{
+  "dataset_id": "知识库ID",
+  "metadata_id": "元数据ID"
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| dataset_id | String | 是 | 知识库唯一标识符 |
+| metadata_id | String | 是 | 要删除的元数据唯一标识符 |
+
+**响应数据:**
+```json
+{
+  "code": 1,
+  "data": {},
+  "errmsg": null
+}
+```
+
+---
+
+#### 4.4 更新元数据
+
+> 编辑弹窗提交后调用,仅允许修改名称
+
+**接口ID:** `2026052715313906`
+
+**请求参数 (content):**
+```json
+{
+  "dataset_id": "知识库ID",
+  "metadata_id": "元数据ID",
+  "name": "新元数据名称"
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| dataset_id | String | 是 | 知识库唯一标识符 |
+| metadata_id | String | 是 | 要更新的元数据唯一标识符 |
+| name | String | 是 | 新的元数据名称 |
+
+**响应数据:**
+```json
+{
+  "code": 1,
+  "data": {},
+  "errmsg": null
+}
+```
+
+---
+
+#### 4.5 启用/禁用系统元数据
+
+> 控制指定知识库中系统元数据的启用状态(全局开关)
+
+**接口ID:** `2026052715313907`
+
+**请求参数 (content):**
+```json
+{
+  "dataset_id": "知识库ID",
+  "enable": true
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| dataset_id | String | 是 | 知识库唯一标识符 |
+| enable | Boolean | 否 | 是否启用系统元数据,默认true |
+
+**响应数据:**
+```json
+{
+  "code": 1,
+  "data": {},
+  "errmsg": null
+}
+```
+
+---
+
+#### 4.6 为文档设置元数据
+
+> 在文档详情面板编辑元数据后保存
+
+**接口ID:** `2026053015572501`
+
+**请求参数 (content):**
+```json
+{
+  "dataset_id": "知识库ID",
+  "document_id": "文档ID",
+  "partial_update": true,
+  "metadata": [
+    {
+      "id": "元数据ID",
+      "name": "元数据名称",
+      "value": "元数据值"
+    }
+  ]
+}
+```
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| dataset_id | String | 是 | 知识库唯一标识符 |
+| document_id | String | 是 | 文档唯一标识符 |
+| partial_update | Boolean | 是 | 是否部分更新,保留未指定字段的现有值 |
+| metadata | Array | 是 | 元数据数组 |
+| metadata[].id | String | 是 | 元数据唯一标识符 |
+| metadata[].name | String | 是 | 元数据名称 |
+| metadata[].value | String | 是 | 元数据值 |
+
+**响应数据:**
+```json
+{
+  "code": 1,
+  "data": {},
+  "errmsg": null
+}
+```
+
+---
+
+### 五、检索测试(1 个接口)
+
+#### 5.1 检索测试
+
+> 输入问题后点击"检索"调用。不经过 LLM,直接返回向量检索命中的文档片段。
+
+| 字段 | 值 |
+|------|-----|
+| classname | `knowledgeBase` |
+| method | `search` |
+
+**请求参数 (content):**
+```json
+{
+  "knowledgeBaseId": 1,
+  "query": "如何配置用户权限",
+  "topK": 10
+}
+```
+
+**响应数据:**
+```json
+{
+  "code": 0,
+  "data": {
+    "results": [
+      {
+        "documentId": 1,
+        "documentName": "产品功能清单_v3.2.xlsx",
+        "content": "用户权限配置:管理员可在系统设置 > 权限管理中进行角色分配,支持按模块、按数据范围设置不同级别的访问权限。",
+        "score": 0.9421
+      },
+      {
+        "documentId": 3,
+        "documentName": "API接口说明_v1.5.docx",
+        "content": "权限相关接口:POST /api/auth/assignRole 用于为用户分配角色,请求参数包括 userId 和 roleId。",
+        "score": 0.8734
+      }
+    ]
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| documentId | number | 来源文档 ID |
+| documentName | string | 来源文档名称 |
+| content | string | 命中的文本片段 |
+| score | number | 相似度得分(0-1),前端转为百分比展示 |
+
+---
+
+## 调用汇总表
+
+| # | 接口 | classname | method | 调用时机 |
+|---|------|-----------|--------|----------|
+| 1 | 获取知识库列表 | `knowledgeBase` | `getList` | 页面加载 |
+| 2 | 创建空知识库 | `knowledgeBase` | `create` | 新建弹窗确认 |
+| 3 | 更新知识库 | `knowledgeBase` | `update` | 重命名弹窗确认 |
+| 4 | 删除知识库 | `knowledgeBase` | `delete` | 右键删除+确认 |
+| 5 | 设置知识库标签 | `knowledgeBase` | `setTags` | 标签弹窗确认 |
+| 6 | 获取知识库详情 | `knowledgeBase` | `getDetail` | 选中知识库(可选刷新) |
+| 7 | 获取文档列表 | `knowledgeBase.document` | `getList` | 选中知识库 / 筛选 / 翻页 |
+| 8 | 上传文档 | `knowledgeBase.document` | `upload` | 上传弹窗确认 |
+| 9 | 获取嵌入状态 | `knowledgeBase.document` | `getEmbedStatus` | 上传后轮询 |
+| 10 | 更新文档文件 | `knowledgeBase.document` | `updateFile` | 替换文件确认 |
+| 11 | 更新文档状态 | `knowledgeBase.document` | `updateStatus` | 开关切换 |
+| 12 | 设置文档元数据 | `knowledgeBase.document` | `setMetadata` | 详情面板保存 |
+| 13 | 获取文档详情 | `knowledgeBase.document` | `getDetail` | 点击详情 |
+| 14 | 删除文档 | `knowledgeBase.document` | `delete` | 删除+确认 |
+| 15 | 下载文档 | `knowledgeBase.document` | `download` | 点击下载 |
+| 16 | 批量下载 | `knowledgeBase.document` | `batchDownload` | 勾选+批量下载 |
+| 17 | 获取元数据列表 | `knowledgeBase.metadata` | `getList` | 元数据配置Tab加载 |
+| 18 | 创建元数据 | `knowledgeBase.metadata` | `create` | 新建字段确认 |
+| 19 | 更新元数据 | `knowledgeBase.metadata` | `update` | 编辑字段确认 |
+| 20 | 删除元数据 | `knowledgeBase.metadata` | `delete` | 删除+确认 |
+| 21 | 启禁元数据 | `knowledgeBase.metadata` | `toggleStatus` | 开关切换 |
+| 22 | 检索测试 | `knowledgeBase` | `search` | 点击检索 |
+
+---
+
+## 通用约定
+
+1. **分页参数**:统一使用 `{ pageNumber, pageSize, where: { condition, tablefilter } }`
+2. **响应格式**:`{ code: 0, data: {...}, message: "xxx" }` — code=0 表示成功
+3. **错误处理**:前端通过 `this.tool.showMessage(res, callback)` 统一处理,code≠0 时自动弹出 message 中的错误提示
+4. **文件上传**:建议沿用华为云 OBS 模式(先取签名URL → 直传 → 回传 fileKey),保证与项目内其他上传模块一致
+5. **向量化**:文档上传/更新后后端异步触发向量化,前端轮询 `getEmbedStatus` 直到 completed/failed
+
+---
+
+## 后端已提供接口(对接状态)
+
+| # | 后端接口ID | 方法名 | 对应功能 | 对接状态 |
+|---|-----------|--------|----------|----------|
+| 1 | `2026052714301001` | queryDatasetList | 获取知识库列表 | 已对接 `fetchKBList()` |
+| 2 | `2026052714301002` | queryDatasetInfo | 获取知识库详情 | 已对接(onSelectKB 内) |
+| 3 | `2026052714301007` | queryDatasetTags | 获取知识库标签 | 已对接 `fetchKBTags()` |
+| 4 | `2026052714301017` | queryDocumentlist | 获取文档列表 | 已对接 `fetchDocumentList()` |
+| 5 | `2026052714301008` | queryDocumentInfo | 获取文档详情 | 已对接 `fetchDocumentDetail()` |
+| 6 | `2026052714301009` | deleteDocument | 删除文档 | 已对接 `deleteDocumentApi()` |
+| 7 | `2026052714301010` | downloadDocument | 下载文档 | 已对接 `downloadDocumentApi()` |
+| 8 | `2026052714301011` | downloadDocumentsBatch | 批量下载 | 已对接 `batchDownloadApi()` |
+| 9 | `2026052714301012` | queryDocumentIndexingStatus | 索引状态查询 | 已对接 `fetchIndexingStatus()` |
+| 10 | `2026052714301013` | createOrUpdateDocumentByText | 新建/更新文本文档 | 待接入 |
+| 11 | `2026052714301016` | updateDocumentsStatusBatch | 批量更新文档状态 `{ dataset_id, document_ids[], action: 'enable'|'disable' }` | 已对接 `onToggleDocStatus()` |
+|| 12 | `2026052715313901` | createDocumentByFile | 文件上传创建文档 | 部分对接(需 OBS attachmentid) |
+|| 13 | `2026052715313903` | queryMetadataList | 获取元数据列表 | 已对接 `fetchMetadata()` |
+|| 14 | `2026052715313904` | createMetadata | 创建元数据 | 已对接 `onCreateField()` |
+|| 15 | `2026052715313905` | deleteMetadata | 删除元数据 | 已对接 `onDeleteField()` |
+|| 16 | `2026052715313906` | updateMetadata | 更新元数据 | 已对接 `onUpdateField()` |
+|| 17 | `2026052715313907` | toggleSystemMetadata | 启用/禁用系统元数据 | 已对接 `onToggleSystemMeta()` |
+|| 18 | `2026053015572501` | setDocumentMetadata | 设置文档元数据 | 已对接 `onSaveDocMetadata()` |
+
+---
+
+## 缺口记录
+
+### 一、缺少的接口(后端未提供)
+
+| 缺口编号 | 功能 | 说明 |
+|----------|------|------|
+| GAP-01 | 创建空知识库 | 前端"新建知识库"功能无对应接口,当前使用 Mock |
+| GAP-02 | 更新知识库 | 右键"重命名"、修改描述无对应接口,当前使用 Mock |
+| GAP-03 | 删除知识库 | 右键"删除知识库"无对应接口,当前使用 Mock |
+| GAP-04 | 设置知识库标签 | 后端仅提供 `queryDatasetTags` 查询标签,无绑定/解绑标签接口。前端"设置标签"功能当前使用 Mock |
+| GAP-05 | 检索测试 | 知识库检索测试(输入问题返回匹配片段)无对应接口,当前使用 Mock 数据 |
+
+### 二、接口差异 / 字段缺失
+
+| 差异编号 | 接口 | 问题 |
+|----------|------|------|
+| DIFF-01 | 文档列表 `2026052714301017` | 响应字段名待确认:文档名 (`name`/`document_name`/`file_name`?)、文件大小 (`size`/`file_size`?)、文件类型 (`type`/`file_type`?)。前端已做多字段兼容映射,具体以 console.log 实际返回为准 |
+| DIFF-02 | 文档详情 `2026052714301008` | 同上,响应字段名待 console.log 确认 |
+| DIFF-03 | 知识库列表 `2026052714301001` | 响应字段名待确认:`id`/`dataset_id`?、名称 `name`/`dataset_name`?、描述 `description`/`desc`?。前端已做多字段兼容映射 |
+| DIFF-04 | 文档索引状态 `2026052714301012` | 参数用 `batch` 而非 `document_id`,前端轮询单个文档时需要传什么 batch 值?待确认 |
+| DIFF-05 | 上传文档 `2026052715313901` | 需要 `attachmentid` 参数(OBS 文件上传后的 ID),无法直接通过文件流上传。前端需先调 OBS 获取 attachmentid 再调此接口 |
+| DIFF-06 | 文档状态更新 | 后端仅提供 `updateDocumentsStatusBatch` 批量接口,无单个文档状态切换接口。前端单个开关已对接批量接口(传单元素数组) |
+| DIFF-07 | 知识库列表 `2026052714301001` | 请求参数仅 `keyword`,无分页参数。大量知识库时的分页如何支持? |
+
+### 三、调用时日志说明
+
+所有 API 调用的请求参数和响应结果均已通过 `console.log('[API] id=xxxxx', ...)` 打印到浏览器控制台。打开 F12 → Console 即可查看实际数据结构。Mock 操作和缺口接口标注了 `console.warn('[GAP] ...')`。

+ 614 - 0
src/AIManagement/KBM/DocumentDetail.vue

@@ -0,0 +1,614 @@
+<template>
+  <el-drawer
+    :visible.sync="drawerVisible"
+    :title="$t('文档详情')"
+    size="560px"
+    direction="rtl"
+    append-to-body
+  >
+    <div class="dd-body">
+      <template v-if="document">
+        <!-- 文件图标 + 名称 -->
+      <div class="dd-file-head">
+        <div class="dd-file-icon" :class="fileTypeClass">
+          <i :class="fileIcon"></i>
+        </div>
+        <div class="dd-file-info">
+          <p class="dd-file-name" :title="document.name">{{ document.name }}</p>
+          <p class="dd-file-meta">
+            <span>{{ document.size }}</span>
+            <span class="dd-meta-divider">|</span>
+            <span>{{ document.type ? document.type.toUpperCase() : '-' }}</span>
+          </p>
+        </div>
+      </div>
+
+      <!-- 基本信息 -->
+      <div class="dd-section">
+        <h4 class="dd-section-title">{{ $t('基本信息') }}</h4>
+        <div class="dd-info-list">
+          <div class="dd-info-row">
+            <span class="dd-info-label">{{ $t('上传时间') }}</span>
+            <span class="dd-info-value">{{ document.uploadTime }}</span>
+          </div>
+          <div class="dd-info-row">
+            <span class="dd-info-label">{{ $t('当前状态') }}</span>
+            <span class="dd-info-value">
+              <span class="dd-status-dot" :class="document.status === 'enabled' ? 'is-on' : 'is-off'"></span>
+              {{ document.status === 'enabled' ? $t('已启用') : $t('已禁用') }}
+            </span>
+          </div>
+        </div>
+      </div>
+
+      <!-- 嵌入状态 -->
+      <div class="dd-section">
+        <h4 class="dd-section-title">{{ $t('向量化嵌入') }}</h4>
+        <div class="dd-embed-card" :class="'dd-embed--' + document.embedStatus">
+        <div class="dd-embed-icon">
+          <i v-if="document.embedStatus === 'completed'" class="el-icon-circle-check"></i>
+          <i v-else-if="['parsing','cleaning','splitting','indexing'].includes(document.embedStatus)" class="el-icon-loading"></i>
+          <i v-else-if="document.embedStatus === 'error'" class="el-icon-circle-close"></i>
+          <i v-else class="el-icon-time"></i>
+        </div>
+        <div class="dd-embed-body">
+          <p class="dd-embed-text">
+            {{ embedStatusText }}
+          </p>
+            <el-progress
+              v-if="['parsing','cleaning','splitting','indexing'].includes(document.embedStatus)"
+              :percentage="document.embedProgress"
+              :stroke-width="6"
+              :show-text="false"
+              style="margin-top: 6px;"
+            />
+            <el-button
+              v-if="document.embedStatus === 'error'"
+              type="text"
+              size="mini"
+              style="padding: 0; margin-top: 4px;"
+              @click="$emit('retry-embed', document.id)"
+            >{{ $t('点击重试') }}</el-button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 元数据 -->
+      <div class="dd-section">
+        <h4 class="dd-section-title">
+          元数据
+          <el-button type="text" size="mini" @click="toggleEdit">{{ editMode ? $t('取消') : $t('编辑') }}</el-button>
+        </h4>
+
+        <div v-if="editMode" class="dd-meta-edit">
+          <!-- 基础字段(只读) -->
+          <div v-for="(val, key) in systemMeta" :key="'sys-' + key" class="dd-meta-edit-item">
+            <label class="dd-meta-edit-label">{{ systemMetaLabel(key) }}</label>
+            <el-input :value="val" size="small" disabled />
+          </div>
+          <!-- 自定义字段(可编辑) -->
+          <div class="dd-meta-edit-item" v-for="field in customFields" :key="field.id">
+            <label class="dd-meta-edit-label">{{ fieldDisplayName(field) }}</label>
+            <template v-if="field.key === 'applicableRole'">
+              <el-select v-model="localMeta[field.key]" multiple size="small" style="width: 100%;">
+                <el-option v-for="opt in (field.options || [])" :key="opt" :label="opt" :value="opt" />
+              </el-select>
+            </template>
+            <template v-else-if="field.type === 'select'">
+              <el-select v-model="localMeta[field.key]" size="small" clearable style="width: 100%;">
+                <el-option v-for="opt in (field.options || [])" :key="opt" :label="opt" :value="opt" />
+              </el-select>
+            </template>
+            <template v-else-if="field.type === 'date'">
+              <el-date-picker v-model="localMeta[field.key]" type="date" size="small" value-format="yyyy-MM-dd" style="width: 100%;" />
+            </template>
+            <template v-else>
+              <el-input v-model="localMeta[field.key]" size="small" />
+            </template>
+          </div>
+          <el-button type="primary" size="small" @click="saveMeta" style="margin-top: 8px;">{{ $t('保存') }}</el-button>
+        </div>
+
+        <div v-else class="dd-meta-view">
+          <!-- 系统字段 -->
+          <div
+            v-for="(val, key) in (document.metadata || {})"
+            :key="'sys-' + key"
+            class="dd-meta-item"
+          >
+            <span class="dd-meta-key">{{ findFieldName(key) || key }}</span>
+            <span class="dd-meta-val">
+              <template v-if="Array.isArray(val)">
+                <el-tag v-for="v in val" :key="v" size="mini" style="margin-right: 4px;">{{ v }}</el-tag>
+                <span v-if="val.length === 0" class="dd-meta-none">{{ $t('未设置') }}</span>
+              </template>
+              <template v-else>{{ val || '-' }}</template>
+            </span>
+          </div>
+          <!-- 自定义字段 -->
+          <div
+            v-for="field in metadataFields"
+            :key="'custom-' + field.id"
+            class="dd-meta-item"
+          >
+            <span class="dd-meta-key">{{ field.name }}</span>
+            <span class="dd-meta-val">
+              <template v-if="document.metadata && document.metadata[field.name]">
+                {{ document.metadata[field.name] }}
+              </template>
+              <template v-else>
+                <span class="dd-meta-none">{{ $t('未设置') }}</span>
+              </template>
+            </span>
+          </div>
+          <div v-if="!document.metadata || Object.keys(document.metadata).length === 0" class="dd-meta-empty" v-show="metadataFields.length === 0">
+            暂无元数据
+          </div>
+        </div>
+      </div>
+
+      <!-- 操作 -->
+      <div class="dd-section">
+        <h4 class="dd-section-title">{{ $t('操作') }}</h4>
+        <div class="dd-actions">
+          <el-button type="primary" size="small" @click="$emit('download', document)">{{ $t('下载文档') }}</el-button>
+          <el-button type="primary" size="small" @click="onToggleStatus">
+            {{ document.status === 'enabled' ? $t('禁用文档') : $t('启用文档') }}
+          </el-button>
+          <el-button type="danger" size="small" @click="deleteDialogVisible = true">{{ $t('删除文档') }}</el-button>
+        </div>
+      </div>
+      </template>
+
+      <!-- 删除确认弹窗 -->
+      <el-dialog
+        :title="$t('确认删除')"
+        :visible.sync="deleteDialogVisible"
+        width="400px"
+        :close-on-click-modal="false"
+        append-to-body
+        @keyup.enter.native="confirmDelete"
+      >
+        <span>{{ $t('确定删除文档', { name: document ? document.name : '' }) }}</span>
+        <span slot="footer">
+          <el-button size="small" @click="deleteDialogVisible = false">{{ $t('取消') }}</el-button>
+          <el-button size="small" type="danger" :loading="deleteLoading" @click="confirmDelete">{{ $t('删除') }}</el-button>
+        </span>
+      </el-dialog>
+
+      <!-- 启用/禁用确认弹窗 -->
+      <el-dialog
+        :title="$t('确认操作')"
+        :visible.sync="toggleDialogVisible"
+        width="400px"
+        :close-on-click-modal="false"
+        append-to-body
+        @keyup.enter.native="confirmToggle"
+      >
+        <span>{{ $t('确定操作文档', { action: document && document.status === 'enabled' ? $t('禁用') : $t('启用'), name: document ? document.name : '' }) }}</span>
+        <span slot="footer">
+          <el-button size="small" @click="toggleDialogVisible = false">{{ $t('取消') }}</el-button>
+          <el-button size="small" type="primary" :loading="toggleLoading" @click="confirmToggle">{{ $t('确定') }}</el-button>
+        </span>
+      </el-dialog>
+    </div>
+  </el-drawer>
+</template>
+
+<script>
+export default {
+  name: 'DocumentDetail',
+  props: {
+    visible: { type: Boolean, default: false },
+    document: { type: Object, default: null },
+    metadataFields: { type: Array, default: () => [] }
+  },
+  data() {
+    return {
+      editMode: false,
+      localMeta: {},
+      deleteDialogVisible: false,
+      deleteLoading: false,
+      toggleDialogVisible: false,
+      toggleLoading: false
+    }
+  },
+  computed: {
+    drawerVisible: {
+      get() { return this.visible },
+      set(v) { if (!v) this.$emit('close') }
+    },
+    fileIcon() {
+      const map = {
+        pdf: 'el-icon-document',
+        docx: 'el-icon-document',
+        doc: 'el-icon-document',
+        xlsx: 'el-icon-s-data',
+        xls: 'el-icon-s-data',
+        pptx: 'el-icon-present',
+        ppt: 'el-icon-present',
+        txt: 'el-icon-tickets',
+        jpg: 'el-icon-picture-outline',
+        jpeg: 'el-icon-picture-outline',
+        png: 'el-icon-picture-outline',
+        gif: 'el-icon-picture-outline'
+      }
+      return map[this.document && this.document.type] || 'el-icon-document'
+    },
+    fileTypeClass() {
+      if (!this.document) return ''
+      const t = this.document.type
+      if (['pdf'].includes(t)) return 'is-pdf'
+      if (['docx', 'doc'].includes(t)) return 'is-word'
+      if (['xlsx', 'xls'].includes(t)) return 'is-excel'
+      if (['pptx', 'ppt'].includes(t)) return 'is-ppt'
+      return ''
+    },
+    embedStatusText() {
+      if (!this.document) return ''
+      const map = {
+        waiting: '等待处理',
+        parsing: '正在解析文档...',
+        cleaning: '正在清洗内容...',
+        splitting: '正在分段...',
+        indexing: '正在向量化...',
+        completed: '嵌入完成',
+        error: '嵌入失败'
+      }
+      return map[this.document.embedStatus] || '等待处理'
+    },
+    // 提取系统基础元数据(只读)
+    systemMeta() {
+      if (!this.document || !this.document.metadata) return {}
+      const sysKeys = ['文档名称', '上传者', '上传时间', '更新时间', '来源']
+      const sysKeyMap = { 'document_name': '文档名称', 'uploader': '上传者', 'upload_date': '上传时间', 'last_update_date': '更新时间', 'source': '来源' }
+      const result = {}
+      // 遍历metadata,找出系统字段
+      Object.entries(this.document.metadata).forEach(([key, val]) => {
+        // 通过原始key或已映射的中文key判断
+        if (sysKeyMap[key] || sysKeys.includes(key)) {
+          result[key] = val
+        }
+      })
+      return result
+    },
+    // 过滤掉系统字段,只保留自定义字段
+    customFields() {
+      const sysKeyMap = { 'document_name': true, 'uploader': true, 'upload_date': true, 'last_update_date': true, 'source': true }
+      const sysNames = ['文档名称', '上传者', '上传时间', '更新时间', '来源']
+      return this.metadataFields.filter(f => !sysKeyMap[f.key] && !sysNames.includes(f.name))
+    }
+  },
+  watch: {
+    document: {
+      immediate: true,
+      handler() {
+        this.editMode = false
+      }
+    }
+  },
+  methods: {
+    onToggleStatus() {
+      this.toggleDialogVisible = true
+    },
+
+    confirmToggle() {
+      this.toggleLoading = true
+      this.$emit('toggle-status', this.document.id, () => {
+        setTimeout(() => {
+          this.toggleLoading = false
+          this.toggleDialogVisible = false
+        }, 500)
+      })
+    },
+
+    confirmDelete() {
+      this.deleteLoading = true
+      this.$emit('delete', this.document.id, () => {
+        setTimeout(() => {
+          this.deleteLoading = false
+          this.deleteDialogVisible = false
+          this.$emit('close')
+        }, 500)
+      })
+    },
+
+    toggleEdit() {
+      if (this.editMode) {
+        this.editMode = false
+        return
+      }
+      // 进入编辑模式时,从文档数据初始化 localMeta
+      const docMeta = (this.document && this.document.metadata) || {}
+      const meta = {}
+      // 先用 metadataFields 的 key 初始化
+      this.metadataFields.forEach(field => {
+        meta[field.key] = docMeta[field.key] !== undefined ? docMeta[field.key] : ''
+      })
+      // 保留文档已有的其他元数据
+      Object.keys(docMeta).forEach(key => {
+        if (meta[key] === undefined) meta[key] = docMeta[key]
+      })
+      this.localMeta = meta
+      this.editMode = true
+    },
+    findFieldName(key) {
+      const sysMap = {
+        document_name: '文档名称',
+        uploader: '上传者',
+        upload_date: '上传日期',
+        last_update_date: '最后更新日期',
+        source: '来源'
+      }
+      if (sysMap[key]) return sysMap[key]
+      const field = this.metadataFields.find(f => f.key === key)
+      return field ? field.name : key
+    },
+    fieldDisplayName(field) {
+      const sysMap = {
+        document_name: '文档名称',
+        uploader: '上传者',
+        upload_date: '上传日期',
+        last_update_date: '最后更新日期',
+        source: '来源'
+      }
+      return sysMap[field.key] || sysMap[field.name] || field.name
+    },
+    systemMetaLabel(key) {
+      // 如果key已经是中文(被映射过),直接返回
+      const cnKeys = ['文档名称', '上传者', '上传时间', '更新时间', '来源']
+      if (cnKeys.includes(key)) return key
+      // 否则映射原始key
+      const map = {
+        document_name: '文档名称',
+        uploader: '上传者',
+        upload_date: '上传时间',
+        last_update_date: '更新时间',
+        source: '来源'
+      }
+      return map[key] || key
+    },
+    saveMeta() {
+      if (this.document) {
+        this.$emit('save-meta', { docId: this.document.id, metadata: { ...this.localMeta } })
+        this.editMode = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dd-body {
+  padding: 0 8px;
+}
+
+/* ---- 文件头部 ---- */
+.dd-file-head {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 8px 0 20px;
+  border-bottom: 1px solid #ebeef5;
+  margin-bottom: 8px;
+}
+
+.dd-file-icon {
+  width: 52px;
+  height: 52px;
+  border-radius: 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 26px;
+  flex-shrink: 0;
+  background: #ecf5ff;
+  color: #3874F6;
+}
+.dd-file-icon.is-word { background: #e9f2ff; color: #2b5fcd; }
+.dd-file-icon.is-excel { background: #e8f8ef; color: #1d9e4c; }
+.dd-file-icon.is-ppt { background: #fef2e8; color: #e4721c; }
+.dd-file-icon.is-pdf { background: #fdecec; color: #e94141; }
+
+.dd-file-info {
+  flex: 1;
+  min-width: 0;
+}
+
+.dd-file-name {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+  margin: 0 0 4px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.dd-file-meta {
+  margin: 0;
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.dd-meta-divider {
+  color: #dcdfe6;
+}
+
+/* ---- 分区 ---- */
+.dd-section {
+  padding: 14px 0;
+  border-bottom: 1px solid #f0f2f5;
+}
+
+.dd-section:last-child {
+  border-bottom: none;
+}
+
+.dd-section-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin: 0 0 10px;
+  font-size: 13px;
+  font-weight: 600;
+  color: #303133;
+}
+
+/* ---- 基本信息 ---- */
+.dd-info-list {
+  background: #fafbfc;
+  border-radius: 6px;
+  padding: 2px 0;
+}
+
+.dd-info-row {
+  display: flex;
+  align-items: center;
+  padding: 8px 14px;
+}
+
+.dd-info-row + .dd-info-row {
+  border-top: 1px solid #f0f2f5;
+}
+
+.dd-info-label {
+  font-size: 12px;
+  color: #909399;
+  width: 70px;
+  flex-shrink: 0;
+}
+
+.dd-info-value {
+  font-size: 13px;
+  color: #303133;
+  display: flex;
+  align-items: center;
+}
+
+.dd-status-dot {
+  width: 7px;
+  height: 7px;
+  border-radius: 50%;
+  margin-right: 6px;
+  display: inline-block;
+}
+.dd-status-dot.is-on  { background: #67C23A; box-shadow: 0 0 0 3px rgba(103, 194, 58, .15); }
+.dd-status-dot.is-off { background: #F56C6C; box-shadow: 0 0 0 3px rgba(245, 108, 108, .15); }
+
+/* ---- 嵌入状态 ---- */
+.dd-embed-card {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  padding: 14px;
+  border-radius: 6px;
+}
+
+.dd-embed--completed { background: #f0f9eb; }
+.dd-embed--parsing { background: #ecf5ff; }
+.dd-embed--cleaning { background: #ecf5ff; }
+.dd-embed--splitting { background: #ecf5ff; }
+.dd-embed--indexing { background: #ecf5ff; }
+.dd-embed--processing { background: #ecf5ff; }
+.dd-embed--error     { background: #fef0f0; }
+.dd-embed--waiting   { background: #fafbfc; }
+
+.dd-embed-icon {
+  font-size: 22px;
+  flex-shrink: 0;
+  margin-top: 2px;
+}
+.dd-embed--completed .dd-embed-icon { color: #67C23A; }
+.dd-embed--parsing .dd-embed-icon { color: #3874F6; }
+.dd-embed--cleaning .dd-embed-icon { color: #3874F6; }
+.dd-embed--splitting .dd-embed-icon { color: #3874F6; }
+.dd-embed--indexing .dd-embed-icon { color: #3874F6; }
+.dd-embed--processing .dd-embed-icon { color: #3874F6; }
+.dd-embed--error     .dd-embed-icon { color: #F56C6C; }
+.dd-embed--waiting   .dd-embed-icon { color: #c0c4cc; }
+
+.dd-embed-body { flex: 1; }
+.dd-embed-text { margin: 0; font-size: 13px; color: #303133; }
+
+/* ---- 元数据查看 ---- */
+.dd-meta-view {
+  background: #fafbfc;
+  border-radius: 6px;
+  padding: 2px 0;
+}
+
+.dd-meta-item {
+  display: flex;
+  align-items: center;
+  padding: 8px 14px;
+}
+
+.dd-meta-item + .dd-meta-item {
+  border-top: 1px solid #f0f2f5;
+}
+
+.dd-meta-key {
+  font-size: 12px;
+  color: #909399;
+  width: 70px;
+  flex-shrink: 0;
+}
+
+.dd-meta-val {
+  font-size: 13px;
+  color: #303133;
+}
+
+.dd-meta-none {
+  color: #c0c4cc;
+}
+
+.dd-meta-empty {
+  font-size: 13px;
+  color: #c0c4cc;
+  padding: 16px;
+  text-align: center;
+}
+
+/* ---- 元数据编辑 ---- */
+.dd-meta-edit-item {
+  margin-bottom: 10px;
+}
+
+.dd-meta-edit-label {
+  display: block;
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 4px;
+}
+
+/* ---- 预览 ---- */
+.dd-preview-box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 32px;
+  background: #fafbfc;
+  border-radius: 6px;
+  border: 1px dashed #e4e7ed;
+  text-align: center;
+  gap: 8px;
+  font-size: 13px;
+  color: #909399;
+}
+
+.dd-preview-box p {
+  margin: 0;
+}
+
+/* ---- 操作 ---- */
+.dd-actions {
+  display: flex;
+  gap: 12px;
+}
+</style>

+ 246 - 0
src/AIManagement/KBM/DocumentTable.vue

@@ -0,0 +1,246 @@
+<template>
+  <div class="dt-wrapper">
+    <!-- 工具栏 -->
+    <div class="dt-toolbar">
+      <div class="dt-toolbar-left">
+        <el-button type="primary" size="small" icon="el-icon-upload2" @click="$emit('upload')">{{ $t('上传文档') }}</el-button>
+        <el-button
+          size="small"
+          icon="el-icon-download"
+          :disabled="selectedRows.length === 0"
+          @click="$emit('batch-download', selectedRows.map(r => r.id))"
+        >{{ $t('批量下载') }}</el-button>
+      </div>
+      <div class="dt-toolbar-right">
+        <el-input
+          v-model="searchText"
+          :placeholder="$t('搜索文档名')"
+          size="small"
+          prefix-icon="el-icon-search"
+          clearable
+          style="width: 220px;"
+          @input="onSearchInput"
+        />
+        <el-select
+          v-model="filterEmbedStatus"
+          :placeholder="$t('嵌入状态')"
+          size="small"
+          clearable
+          style="width: 130px; margin-left: 8px; margin-right: 8px;"
+        >
+          <el-option :label="$t('等待处理')" value="waiting" />
+          <el-option :label="$t('解析中')" value="parsing" />
+          <el-option :label="$t('清洗中')" value="cleaning" />
+          <el-option :label="$t('分段中')" value="splitting" />
+          <el-option :label="$t('索引中')" value="indexing" />
+          <el-option :label="$t('已完成')" value="completed" />
+          <el-option :label="$t('失败')" value="error" />
+        </el-select>
+      </div>
+    </div>
+
+    <!-- 表格 -->
+    <el-table
+      :data="displayDocuments"
+      size="small"
+      stripe
+      style="width: 100%;"
+      @selection-change="handleSelectionChange"
+      row-key="id"
+      height="calc(100vh - 380px)"
+    >
+      <el-table-column type="selection" width="40" />
+      <el-table-column :label="$t('文件名')" min-width="140" show-overflow-tooltip>
+        <template v-slot="{ row }">
+          <span class="dt-file-link" @click="$emit('detail', row)">
+            <i :class="fileIcon(row.type)" style="margin-right: 6px;"></i>
+            {{ row.name }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('大小')" width="90" prop="size" />
+      <el-table-column :label="$t('上传时间')" width="150" prop="uploadTime" />
+      <el-table-column :label="$t('嵌入状态')" width="130" align="center">
+        <template v-slot="{ row }">
+          <el-progress
+            v-if="['parsing','cleaning','splitting','indexing'].includes(row.embedStatus)"
+            :percentage="row.embedProgress"
+            :show-text="false"
+            :stroke-width="6"
+            style="width: 80px; display: inline-block;"
+          />
+          <el-tag v-else-if="row.embedStatus === 'completed'" type="success" size="mini">{{ $t('已完成') }}</el-tag>
+          <el-tag v-else-if="row.embedStatus === 'error'" type="danger" size="mini">{{ $t('失败') }}</el-tag>
+          <el-tag v-else-if="row.embedStatus === 'parsing'" size="mini">{{ $t('解析中') }}</el-tag>
+          <el-tag v-else-if="row.embedStatus === 'cleaning'" size="mini">{{ $t('清洗中') }}</el-tag>
+          <el-tag v-else-if="row.embedStatus === 'splitting'" size="mini">{{ $t('分段中') }}</el-tag>
+          <el-tag v-else-if="row.embedStatus === 'indexing'" type="warning" size="mini">{{ $t('索引中') }}</el-tag>
+          <el-tag v-else type="info" size="mini">{{ $t('等待处理') }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('状态')" width="80" align="center">
+        <template v-slot="{ row }">
+          <el-switch
+            :value="row.status === 'enabled'"
+            active-color="#13ce66"
+            inactive-color="#ff4949"
+            @change="$emit('toggle-status', row.id)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('操作')" width="180" fixed="right">
+        <template v-slot="{ row }">
+          <el-button type="text" size="mini" @click="$emit('detail', row)">{{ $t('详情') }}</el-button>
+          <el-button type="text" size="mini" @click="$emit('download', row)" style="margin-left: 10px;">{{ $t('下载') }}</el-button>
+          <el-popconfirm
+            :title="$t('确定删除该文档')"
+            :confirm-button-text="$t('删除')"
+            :cancel-button-text="$t('取消')"
+            @confirm="$emit('delete', row.id)"
+          >
+            <el-button type="text" size="mini" slot="reference" style="color: #F56C6C; margin-left: 10px;">{{ $t('删除') }}</el-button>
+          </el-popconfirm>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <div class="dt-pagination">
+      <el-pagination
+        small
+        layout="total, sizes, prev, pager, next"
+        :total="total"
+        :page-size="pageSize"
+        :current-page="currentPage"
+        :page-sizes="[10, 20, 50]"
+        @current-change="$emit('page-change', $event)"
+        @size-change="$emit('size-change', $event)"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DocumentTable',
+  props: {
+    kb: { type: Object, default: null },
+    documents: { type: Array, default: () => [] },
+    total: { type: Number, default: 0 },
+    pageSize: { type: Number, default: 20 },
+    currentPage: { type: Number, default: 1 }
+  },
+  data() {
+    return {
+      searchText: '',
+      filterEmbedStatus: '',
+      selectedRows: [],
+      searchTimer: null
+    }
+  },
+  computed: {
+    displayDocuments() {
+      if (!this.filterEmbedStatus) return this.documents
+      return this.documents.filter(d => d.embedStatus === this.filterEmbedStatus)
+    }
+  },
+  methods: {
+    onSearchInput(val) {
+      clearTimeout(this.searchTimer)
+      this.searchTimer = setTimeout(() => {
+        this.$emit('search', val || '')
+      }, 400)
+    },
+    handleSelectionChange(rows) {
+      this.selectedRows = rows
+    },
+    fileIcon(type) {
+      const map = {
+        pdf: 'el-icon-document',
+        docx: 'el-icon-document',
+        doc: 'el-icon-document',
+        xlsx: 'el-icon-s-data',
+        xls: 'el-icon-s-data',
+        pptx: 'el-icon-present',
+        ppt: 'el-icon-present',
+        txt: 'el-icon-tickets',
+        jpg: 'el-icon-picture-outline',
+        jpeg: 'el-icon-picture-outline',
+        png: 'el-icon-picture-outline',
+        gif: 'el-icon-picture-outline'
+      }
+      return map[type] || 'el-icon-document'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dt-wrapper {
+  background: #fff;
+  border-radius: 4px;
+}
+
+.dt-toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 0 12px 16px;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.dt-toolbar-left,
+.dt-toolbar-right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.dt-file-link {
+  color: #3874F6;
+  cursor: pointer;
+}
+
+.dt-file-link:hover {
+  text-decoration: underline;
+}
+
+.dt-pagination {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  padding: 12px 16px;
+  box-sizing: border-box;
+}
+
+.dt-pagination ::v-deep .el-pagination {
+  line-height: 1;
+}
+
+.dt-pagination ::v-deep .el-pagination > * {
+  vertical-align: middle !important;
+}
+
+.dt-pagination ::v-deep .el-pagination__sizes {
+  height: 28px;
+}
+
+.dt-pagination ::v-deep .el-pagination__sizes .el-input {
+  height: 28px;
+}
+
+.dt-pagination ::v-deep .el-pagination__sizes .el-input__inner {
+  height: 28px !important;
+  line-height: 28px !important;
+}
+
+.dt-pagination ::v-deep .el-pagination__sizes .el-input .el-input__suffix {
+  height: 28px;
+  line-height: 28px;
+}
+
+.dt-pagination ::v-deep .el-pager li {
+  vertical-align: middle !important;
+}
+</style>

+ 187 - 0
src/AIManagement/KBM/KnowledgeBaseList.vue

@@ -0,0 +1,187 @@
+<template>
+  <div class="kbl-wrapper">
+    <!-- 搜索区 -->
+    <div class="kbl-header">
+      <el-input
+        v-model="searchText"
+        :placeholder="$t('搜索知识库')"
+        size="small"
+        prefix-icon="el-icon-search"
+        clearable
+        @input="$emit('search', searchText)"
+      />
+    </div>
+
+    <!-- 列表 -->
+    <div class="kbl-list">
+      <div
+        v-for="kb in list"
+        :key="kb.id"
+        class="kbl-item"
+        :class="{ 'kbl-item--active': activeId === kb.id }"
+        @click="$emit('select', kb.id)"
+      >
+        <div class="kbl-item-top">
+          <span class="kbl-item-icon"><i class="el-icon-folder-opened"></i></span>
+          <span class="kbl-item-name" :title="kb.name">{{ kb.name }}</span>
+        </div>
+        <div class="kbl-item-bottom">
+          <span class="kbl-item-time">{{ kb.updatedAt || '-' }}</span>
+          <span class="kbl-item-right">
+            <span class="kbl-item-count">{{ kb.docCount || 0 }} {{ $t('份文档') }}</span>
+            <span class="kbl-item-tags">
+              <span
+                v-for="tag in (kb.tags || [])"
+                :key="tag"
+                class="kbl-tag-dot"
+                :style="{ background: getTagColor(tag) }"
+                :title="tag"
+              ></span>
+            </span>
+          </span>
+        </div>
+      </div>
+
+      <div v-if="list.length === 0" class="kbl-empty">
+        <p>{{ $t('暂无知识库') }}</p>
+      </div>
+    </div>
+
+
+  </div>
+</template>
+
+<script>
+const TAG_COLORS = [
+  '#409EFF', '#67C23A', '#E6A23C', '#F56C6C',
+  '#909399', '#B37FEB', '#13C2C2', '#FA8C16'
+]
+
+export default {
+  name: 'KnowledgeBaseList',
+  props: {
+    list: { type: Array, default: () => [] },
+    activeId: { type: [Number, String], default: null },
+    searchKey: { type: String, default: '' },
+  },
+  data() {
+    return {
+      searchText: '',
+      tagColorMap: {}
+    }
+  },
+
+  methods: {
+    getTagColor(tag) {
+      if (!this.tagColorMap[tag]) {
+        const i = Object.keys(this.tagColorMap).length % TAG_COLORS.length
+        this.$set(this.tagColorMap, tag, TAG_COLORS[i])
+      }
+      return this.tagColorMap[tag]
+    }
+  }
+}
+</script>
+
+<style scoped>
+.kbl-wrapper {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.kbl-header {
+  padding: 12px;
+  border-bottom: 1px solid #e4e7ed;
+}
+
+
+.kbl-list {
+  flex: 1;
+  overflow-y: auto;
+  padding: 4px 0;
+}
+
+.kbl-item {
+  padding: 12px 16px;
+  cursor: pointer;
+  border-left: 3px solid transparent;
+  transition: background 0.15s;
+}
+
+.kbl-item:hover {
+  background: #f5f7fa;
+}
+
+.kbl-item--active {
+  background: #ecf5ff;
+  border-left-color: #3874F6;
+}
+
+.kbl-item-top {
+  display: flex;
+  align-items: center;
+  margin-bottom: 6px;
+}
+
+.kbl-item-icon {
+  color: #3874F6;
+  font-size: 16px;
+  margin-right: 8px;
+  flex-shrink: 0;
+}
+
+.kbl-item-name {
+  font-size: 14px;
+  color: #303133;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  font-weight: 500;
+}
+
+.kbl-item-bottom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-left: 24px;
+}
+
+.kbl-item-time {
+  font-size: 12px;
+  color: #c0c4cc;
+}
+
+.kbl-item-right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.kbl-item-count {
+  font-size: 12px;
+  color: #909399;
+}
+
+.kbl-tag-dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-left: 4px;
+}
+
+.kbl-empty {
+  text-align: center;
+  color: #c0c4cc;
+  padding: 40px 0;
+  font-size: 13px;
+}
+
+.kbl-empty-tip {
+  font-size: 12px;
+  margin-top: 4px;
+}
+
+
+</style>

+ 207 - 0
src/AIManagement/KBM/MetadataConfig.vue

@@ -0,0 +1,207 @@
+<template>
+  <div class="mc-wrapper">
+    <div class="mc-toolbar">
+      <el-button type="primary" size="small" icon="el-icon-plus" @click="openCreateDialog">{{ $t('新建字段') }}</el-button>
+      <span class="mc-toolbar-switch">
+        <el-popconfirm
+          :title="$t('确定停用系统元数据吗', { action: $t(sysMetaEnabled ? '停用' : '启用') })"
+          :confirm-button-text="$t('确定')"
+          @confirm="confirmToggle"
+        >
+          <el-switch slot="reference" :value="sysMetaEnabled" @change="onToggleSystem" />
+        </el-popconfirm>
+        <span class="mc-toolbar-label">{{ $t('启用系统元数据') }}</span>
+      </span>
+    </div>
+
+    <el-table :data="fields" size="small" stripe style="width: 100%;" height="calc(100vh - 330px)">
+      <el-table-column :label="$t('字段名称')" min-width="120" prop="name" />
+      <el-table-column :label="$t('字段类型')" width="100" align="center">
+        <template v-slot="{ row }">
+          <el-tag size="mini" :type="typeTagType(row.type)">{{ typeLabel(row.type) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('类型')" width="90" align="center">
+        <template v-slot="{ row }">
+          <el-tag size="mini" :type="row.isSystem ? 'warning' : 'success'">
+            {{ row.isSystem ? $t('系统') : $t('自定义') }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('操作')" width="120" fixed="right">
+        <template v-slot="{ row }">
+          <el-button type="text" size="mini" @click="openEditDialog(row)" style="margin-right: 12px;">{{ $t('编辑') }}</el-button>
+          <el-button v-if="!row.isSystem" type="text" size="mini" @click="onDeleteClick(row)" style="color: #F56C6C;">{{ $t('删除') }}</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 删除确认弹窗 -->
+    <el-dialog
+      :title="$t('确认删除')"
+      :visible.sync="deleteDialogVisible"
+      width="400px"
+      :close-on-click-modal="false"
+      append-to-body
+      @keyup.enter.native="confirmDelete"
+    >
+      <span>{{$t('确定删除字段', { name: deleteRow ? deleteRow.name : '' })}}</span>
+      <span slot="footer">
+        <el-button size="small" @click="deleteDialogVisible = false">{{ $t('取消') }}</el-button>
+        <el-button size="small" type="danger" :loading="deleteLoading" @click="confirmDelete">{{ $t('删除') }}</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 新建/编辑弹窗 -->
+    <el-dialog
+      :title="$t(dialogMode === 'create' ? '新建元数据字段' : '编辑元数据字段')"
+      :visible.sync="dialogVisible"
+      width="500px"
+      :close-on-click-modal="false"
+      append-to-body
+    >
+      <el-form ref="fieldForm" :model="form" :rules="rules" label-width="80px" size="small">
+        <el-form-item :label="$t('字段名称')" prop="name">
+          <el-input v-model="form.name" :placeholder="$t('如:文档分类')" maxlength="20" />
+        </el-form-item>
+        <el-form-item :label="$t('字段类型')" prop="type">
+          <el-radio-group v-model="form.type" :disabled="dialogMode === 'edit'">
+            <el-radio-button label="text">{{ $t('文本') }}</el-radio-button>
+            <el-radio-button label="date">{{ $t('日期') }}</el-radio-button>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button size="small" @click="dialogVisible = false">{{ $t('取消') }}</el-button>
+        <el-button size="small" type="primary" :loading="submitLoading" @click="submitForm">{{ $t('确定') }}</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MetadataConfig',
+  props: {
+    fields: { type: Array, default: () => [] },
+    sysMetaEnabled: { type: Boolean, default: true }
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      dialogMode: 'create',
+      editFieldId: null,
+      form: {
+        name: '',
+        type: 'text'
+      },
+      rules: {
+        name: [{ required: true, message: this.$t('请输入字段名称'), trigger: 'blur' }],
+        type: [{ required: true, message: this.$t('请选择字段类型'), trigger: 'change' }]
+      },
+      submitLoading: false,
+      deleteDialogVisible: false,
+      deleteLoading: false,
+      deleteRow: null
+    }
+  },
+  methods: {
+    onToggleSystem() {
+      // 不做任何操作,只触发popconfirm弹窗
+    },
+
+    confirmToggle() {
+      this.$emit('toggle-system')
+    },
+
+    typeLabel(type) {
+      const map = { text: this.$t('文本'), date: this.$t('日期') }
+      return map[type] || type
+    },
+
+    typeTagType(type) {
+      const map = { text: 'primary', date: 'danger' }
+      return map[type] || 'info'
+    },
+
+    onDeleteClick(row) {
+      this.deleteRow = row
+      this.deleteDialogVisible = true
+    },
+
+    confirmDelete() {
+      if (!this.deleteRow) return
+      this.deleteLoading = true
+      this.$emit('delete', this.deleteRow.id, () => {
+        setTimeout(() => {
+          this.deleteLoading = false
+          this.deleteDialogVisible = false
+          this.deleteRow = null
+        }, 500)
+      })
+    },
+
+    openCreateDialog() {
+      this.dialogMode = 'create'
+      this.form = { name: '', type: 'text' }
+      this.dialogVisible = true
+      this.$nextTick(() => { this.$refs.fieldForm && this.$refs.fieldForm.clearValidate() })
+    },
+
+    openEditDialog(row) {
+      this.dialogMode = 'edit'
+      this.editFieldId = row.id
+      this.form = {
+        name: row.name,
+        type: row.type
+      }
+      this.dialogVisible = true
+      this.$nextTick(() => { this.$refs.fieldForm && this.$refs.fieldForm.clearValidate() })
+    },
+
+    submitForm() {
+      this.$refs.fieldForm.validate(valid => {
+        if (!valid) return
+        this.submitLoading = true
+        const callback = () => {
+          setTimeout(() => {
+            this.submitLoading = false
+            this.dialogVisible = false
+          }, 500)
+        }
+        if (this.dialogMode === 'create') {
+          this.$emit('create', { ...this.form }, callback)
+        } else {
+          this.$emit('update', { id: this.editFieldId, ...this.form }, callback)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mc-wrapper {
+  background: #fff;
+  border-radius: 4px;
+  padding: 12px 0;
+}
+
+.mc-toolbar {
+  padding: 0 0 12px 0;
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.mc-toolbar-switch {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.mc-toolbar-label {
+  font-size: 13px;
+  color: #606266;
+}
+</style>

+ 210 - 0
src/AIManagement/KBM/SearchTest.vue

@@ -0,0 +1,210 @@
+<template>
+  <div class="st-wrapper">
+    <!-- 检索输入 -->
+    <div class="st-search-bar">
+      <el-input
+        v-model="query"
+        :placeholder="$t('输入测试问题')"
+        size="small"
+        @keyup.native.enter="doSearch"
+        clearable
+        class="st-search-input"
+      >
+        <el-button slot="append" type="primary" icon="el-icon-search" @click="doSearch" :loading="searching">
+          检索
+        </el-button>
+      </el-input>
+    </div>
+
+    <!-- 检索结果 -->
+    <div class="st-results" v-if="searchDone">
+      <div class="st-result-header">
+        <span>{{ $t('共找到') }} <b>{{ results.length }}</b> {{ $t('个相关片段') }}</span>
+      </div>
+
+      <div v-if="results.length === 0" class="st-empty">
+        <i class="el-icon-warning-outline" style="font-size: 40px;"></i>
+        <p>{{ $t(this.$t('未找到相关结果')) }}</p>
+        <p class="st-empty-tip">{{ $t('请尝试调整检索关键词') }}</p>
+      </div>
+
+      <div v-for="(item, idx) in results" :key="idx" class="st-result-card">
+        <div class="st-result-score">
+          <span class="st-score-badge" :style="{ background: scoreColor(item.score) }">
+            {{ $t('相似度') }} {{ (item.score * 100).toFixed(1) }}%
+          </span>
+        </div>
+        <div class="st-result-content">
+          <div class="st-fragment-text">{{ item.content }}</div>
+          <div class="st-fragment-source">
+            <i class="el-icon-document"></i>
+            <span>{{ item.docName }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 初始状态 -->
+    <div v-else class="st-placeholder">
+      <i class="el-icon-search" style="font-size: 48px;"></i>
+      <p>{{ $t('输入问题测试知识库检索效果') }}</p>
+      <p class="st-placeholder-tip">{{ $t('检索使用向量相似度匹配') }}</p>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SearchTest',
+  props: {
+    kbId: { type: [Number, String], default: null },
+    documents: { type: Array, default: () => [] }
+  },
+  data() {
+    return {
+      query: '',
+      searching: false,
+      searchDone: false,
+      results: []
+    }
+  },
+  methods: {
+    async doSearch() {
+      if (!this.query.trim()) {
+        this.$message.warning(this.$t('请输入检索内容'))
+        return
+      }
+
+      this.searching = true
+      this.searchDone = true
+
+      try {
+        const res = await this.$api.requested({
+          id: 2026052714301001, // 暂用已有接口ID占位,后端需提供检索接口
+          content: { dataset_id: String(this.kbId), query: this.query, topK: 10 }
+        })
+        console.log('[API] search', { request: { dataset_id: this.kbId, query: this.query }, response: res })
+        if (res && res.code === 1 && res.data) {
+          const list = res.data.results || res.data.list || res.data || []
+          this.results = list.map(item => ({
+            docName: item.documentName || item.document_name || item.docName || '',
+            content: item.content || item.text || '',
+            score: item.score || 0
+          }))
+          if (this.results.length === 0) {
+            this.$message.info(this.$t('未找到相关结果'))
+          }
+        }
+      } catch (e) {
+        console.error('[GAP] 检索测试接口尚未提供', e)
+        this.results = []
+        this.$message.warning(this.$t('检索功能暂未开放'))
+      }
+
+      this.searching = false
+    },
+
+    scoreColor(score) {
+      if (score >= 0.85) return '#67C23A'
+      if (score >= 0.7) return '#E6A23C'
+      return '#F56C6C'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.st-wrapper {
+  background: #fff;
+  border-radius: 4px;
+  min-height: 400px;
+}
+
+.st-search-bar {
+  padding: 12px 0;
+}
+
+.st-search-input {
+  max-width: 600px;
+}
+
+.st-results {
+  padding: 8px 0;
+}
+
+.st-result-header {
+  font-size: 13px;
+  color: #606266;
+  margin-bottom: 12px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.st-empty {
+  text-align: center;
+  padding: 60px 0;
+  color: #c0c4cc;
+}
+
+.st-empty-tip {
+  font-size: 12px;
+  margin-top: 4px;
+}
+
+.st-result-card {
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  padding: 14px 16px;
+  margin-bottom: 12px;
+  transition: box-shadow 0.2s;
+}
+
+.st-result-card:hover {
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.st-result-score {
+  margin-bottom: 10px;
+}
+
+.st-score-badge {
+  display: inline-block;
+  padding: 2px 10px;
+  border-radius: 10px;
+  font-size: 12px;
+  color: #fff;
+  font-weight: 500;
+}
+
+.st-fragment-text {
+  font-size: 13px;
+  color: #303133;
+  line-height: 1.7;
+  margin-bottom: 8px;
+}
+
+.st-fragment-source {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.st-placeholder {
+  text-align: center;
+  padding: 60px 0;
+  color: #c0c4cc;
+}
+
+.st-placeholder p:first-of-type {
+  margin-top: 16px;
+  font-size: 14px;
+  color: #909399;
+}
+
+.st-placeholder-tip {
+  font-size: 12px;
+  margin-top: 4px;
+}
+</style>

+ 190 - 0
src/AIManagement/KBM/UploadDocument.vue

@@ -0,0 +1,190 @@
+<template>
+  <el-dialog
+    :title="$t('上传文档')"
+    :visible.sync="visible"
+    width="620px"
+    :close-on-click-modal="false"
+    @close="resetForm"
+    append-to-body
+  >
+    <el-form ref="uploadForm" :model="form" :rules="rules" label-width="90px" size="small">
+      <!-- 文件选择 -->
+      <el-form-item :label="$t('选择文件')" prop="files">
+        <el-upload
+          ref="upload"
+          action="#"
+          :auto-upload="false"
+          :multiple="true"
+          :file-list="fileList"
+          :before-upload="beforeUpload"
+          :on-change="handleFileChange"
+          :on-remove="handleFileRemove"
+          drag
+        >
+          <i class="el-icon-upload"></i>
+          <div class="dt-upload-text">{{ $t('将文件拖到此处,或') }}<em>{{ $t('点击上传') }}</em></div>
+          <div class="dt-upload-tip" slot="tip">{{ $t('支持文件格式') }}TXT,单个文件不超过 20MB</div>
+        </el-upload>
+      </el-form-item>
+
+      <!-- 动态元数据字段 -->
+      <template v-for="field in metadataFields">
+        <el-form-item
+          v-if="field.key === 'productLine'"
+          :key="field.id"
+          :label="field.name"
+        >
+          <el-select v-model="form.metadata.productLine" placeholder="请选择产品线" clearable style="width: 100%;">
+            <el-option
+              v-for="opt in (field.options || [])"
+              :key="opt"
+              :label="opt"
+              :value="opt"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item
+          v-else-if="field.key === 'applicableRole'"
+          :key="field.id"
+          :label="field.name"
+        >
+          <el-select v-model="form.metadata.applicableRole" placeholder="请选择适用角色" multiple clearable style="width: 100%;">
+            <el-option
+              v-for="opt in (field.options || [])"
+              :key="opt"
+              :label="opt"
+              :value="opt"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item
+          v-else-if="field.key === 'validUntil'"
+          :key="field.id"
+          :label="field.name"
+        >
+          <el-date-picker v-model="form.metadata.validUntil" type="date" placeholder="选择有效期" value-format="yyyy-MM-dd" style="width: 100%;" />
+        </el-form-item>
+
+        <el-form-item
+          v-else
+          :key="field.id"
+          :label="field.name"
+        >
+          <template v-if="field.type === 'select'">
+            <el-select v-model="form.metadata[field.key]" :placeholder="'请选择' + field.name" clearable style="width: 100%;">
+              <el-option v-for="opt in (field.options || [])" :key="opt" :label="opt" :value="opt" />
+            </el-select>
+          </template>
+          <template v-else>
+            <el-input v-model="form.metadata[field.key]" :placeholder="'请输入' + field.name" />
+          </template>
+        </el-form-item>
+      </template>
+    </el-form>
+
+    <span slot="footer">
+      <el-button size="small" @click="closeDialog">{{ $t('取消') }}</el-button>
+      <el-button size="small" type="primary" :disabled="fileList.length === 0" @click="submitUpload">开始上传</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: 'UploadDocument',
+  props: {
+    visible: { type: Boolean, default: false },
+    metadataFields: { type: Array, default: () => [] }
+  },
+  data() {
+    return {
+      fileList: [],
+      form: {
+        files: [],
+        metadata: {
+          productLine: '',
+          applicableRole: [],
+          validUntil: ''
+        }
+      },
+      rules: {
+        files: [{ required: true, message: this.$t(this.$t('请选择文件')), trigger: 'change' }]
+      }
+    }
+  },
+  methods: {
+    beforeUpload(file) {
+      const maxSize = 20 * 1024 * 1024
+      if (file.size > maxSize) {
+        this.$message.error(this.$t('文件超过限制', { name: file.name }))
+        return false
+      }
+      return false
+    },
+
+    handleFileChange(file) {
+      this.fileList = this.$refs.upload.uploadFiles
+      this.form.files = this.$refs.upload.uploadFiles.map(f => f.raw).filter(Boolean)
+    },
+
+    handleFileRemove() {
+      this.$nextTick(() => {
+        this.fileList = this.$refs.upload.uploadFiles
+        this.form.files = this.$refs.upload.uploadFiles.map(f => f.raw).filter(Boolean)
+      })
+    },
+
+    submitUpload() {
+      if (this.fileList.length === 0 && this.form.files.length === 0) {
+        this.$message.warning(this.$t(this.$t('请选择文件')))
+        return
+      }
+      this.$emit('submit', {
+        files: this.form.files,
+        metadata: { ...this.form.metadata }
+      })
+      this.resetForm()
+    },
+
+    closeDialog() {
+      this.$emit('close')
+      this.resetForm()
+    },
+
+    resetForm() {
+      this.fileList = []
+      this.form = {
+        files: [],
+        metadata: {
+          productLine: '',
+          applicableRole: [],
+          validUntil: ''
+        }
+      }
+      if (this.$refs.uploadForm) {
+        this.$refs.uploadForm.clearValidate()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dt-upload-text {
+  color: #606266;
+  font-size: 13px;
+}
+
+.dt-upload-text em {
+  color: #3874F6;
+  font-style: normal;
+}
+
+.dt-upload-tip {
+  color: #c0c4cc;
+  font-size: 12px;
+  margin-top: 8px;
+}
+</style>

+ 792 - 0
src/AIManagement/KBM/index.vue

@@ -0,0 +1,792 @@
+<template>
+  <div class="kb-manage">
+    <!-- 左侧:知识库列表 -->
+    <div class="kb-left-panel">
+      <KnowledgeBaseList
+        :list="filteredKBList"
+        :activeId="activeKBId"
+        :searchKey="kbSearchKey"
+        @search="kbSearchKey = $event"
+        @select="onSelectKB"
+      />
+    </div>
+
+    <!-- 右侧:详情与操作区 -->
+    <div class="kb-right-panel">
+      <template v-if="activeKB">
+        <div class="kb-detail-header normal-panel">
+          <div class="kb-detail-info">
+            <h3 class="kb-detail-name">{{ activeKB.name }}</h3>
+            <p class="kb-detail-desc">{{ activeKB.description || $t('暂无描述') }}</p>
+            <div class="kb-detail-meta">
+              <span>{{ $t('创建时间') }}:{{ activeKB.createdAt }}</span>
+              <span>{{ $t('更新时间') }}:{{ activeKB.updatedAt }}</span>
+              <span>
+                {{ $t('标签') }}:
+                <el-tag
+                  v-for="tag in activeKB.tags"
+                  :key="tag"
+                  :color="getTagColor(tag)"
+                  size="small"
+                  effect="dark"
+                  class="kb-tag-inline"
+                >{{ tag }}</el-tag>
+                <span v-if="!activeKB.tags || activeKB.tags.length === 0" class="kb-no-tag">{{ $t('无') }}</span>
+              </span>
+            </div>
+          </div>
+        </div>
+
+        <div class="kb-tabs-wrapper">
+          <el-tabs v-model="activeTab" type="card" class="kb-tabs">
+            <el-tab-pane label="文档管理" name="docs">
+              <DocumentTable
+                :kb="activeKB"
+                :documents="currentDocuments"
+                :total="docTotal"
+                :pageSize="docPageSize"
+                :currentPage="docPage"
+                @upload="openUploadDialog"
+                @detail="openDocumentDetail"
+                @preview="onPreviewFile"
+                @toggle-status="onToggleDocStatus"
+                @delete="onDeleteDocument"
+                @download="onDownloadDocument"
+                @batch-download="onBatchDownload"
+                @refresh="loadDocuments"
+                @search="onDocSearch"
+                @page-change="onDocPageChange"
+                @size-change="onDocSizeChange"
+              />
+            </el-tab-pane>
+            <el-tab-pane label="元数据配置" name="metadata">
+              <MetadataConfig
+                :fields="metadataFields"
+                :sysMetaEnabled="sysMetaEnabled"
+                @create="onCreateField"
+                @update="onUpdateField"
+                @delete="onDeleteField"
+                @toggle-system="onToggleSystemMeta"
+              />
+            </el-tab-pane>
+          </el-tabs>
+        </div>
+      </template>
+      <div v-else class="kb-empty-state">
+        <i class="el-icon-document"></i>
+        <p>{{ knowledgeBases.length === 0 ? $t('加载中') : $t('请选择左侧知识库查看详情') }}</p>
+      </div>
+    </div>
+
+    <!-- 上传文档弹窗 -->
+    <UploadDocument
+      :visible="uploadDialogVisible"
+      @close="uploadDialogVisible = false"
+      @submit="onUploadDocument"
+    />
+
+    <!-- 文档详情抽屉 -->
+    <DocumentDetail
+      :visible="detailDrawerVisible"
+      :document="selectedDocument"
+      :metadataFields="metadataFields"
+      @close="detailDrawerVisible = false"
+      @download="onDownloadDocument"
+      @toggle-status="onToggleDocStatus"
+      @delete="onDeleteDocument"
+      @save-meta="onSaveDocMetadata"
+    />
+  </div>
+</template>
+
+<script>
+import KnowledgeBaseList from './KnowledgeBaseList.vue'
+import DocumentTable from './DocumentTable.vue'
+import UploadDocument from './UploadDocument.vue'
+import DocumentDetail from './DocumentDetail.vue'
+import MetadataConfig from './MetadataConfig.vue'
+
+const TAG_COLORS = [
+  '#409EFF', '#67C23A', '#E6A23C', '#F56C6C',
+  '#909399', '#B37FEB', '#13C2C2', '#FA8C16'
+]
+
+export default {
+  name: 'KBManagement',
+  components: {
+    KnowledgeBaseList,
+    DocumentTable,
+    UploadDocument,
+    DocumentDetail,
+    MetadataConfig
+  },
+  data() {
+    return {
+      activeKBId: null,
+      kbSearchKey: '',
+      activeTab: 'docs',
+      uploadDialogVisible: false,
+      detailDrawerVisible: false,
+      selectedDocument: null,
+      tagColorMap: {},
+      previewUrl: '',
+
+      // 全部从接口获取
+      knowledgeBases: [],
+      allDocuments: [],
+      metadataFields: [],
+      sysMetaEnabled: true,
+      docPage: 1,
+      docPageSize: 20,
+      docTotal: 0,
+      docKeyword: ''
+    }
+  },
+  computed: {
+    activeKB() {
+      return this.knowledgeBases.find(kb => kb.id === this.activeKBId) || null
+    },
+    filteredKBList() {
+      if (!this.kbSearchKey) return this.knowledgeBases
+      const keyword = this.kbSearchKey.toLowerCase()
+      return this.knowledgeBases.filter(kb => kb.name.toLowerCase().includes(keyword))
+    },
+    currentDocuments() {
+      return this.allDocuments
+    },
+  },
+  mounted() {
+    this.fetchKBList()
+  },
+  methods: {
+    // ====== 通用请求 ======
+    async apiCall(id, content = {}) {
+      try {
+        const res = await this.$api.requested({ id, content })
+        console.log(`[API] id=${id}`, { request: content, response: res })
+        return res
+      } catch (e) {
+        console.error(`[API] id=${id} error`, e)
+        return null
+      }
+    },
+
+
+    // ====== 知识库 API ======
+    async fetchKBList() {
+      const res = await this.apiCall(2026052714301001, { keyword: this.kbSearchKey || '' })
+      if (res && res.code === 1 && res.data) {
+        const list = res.data.data || res.data.list || res.data || []
+        this.knowledgeBases = list.map(item => ({
+          id: item.id || item.dataset_id,
+          name: item.name || item.dataset_name || '',
+          description: item.description || item.desc || '',
+          docCount: item.total_documents || 0,
+          tags: item.tags || [],
+          createdAt: this.formatTimestamp(item.created_at || item.createdAt),
+          updatedAt: this.formatTimestamp(item.updated_at || item.updatedAt)
+        }))
+        if (!this.activeKBId && this.knowledgeBases.length > 0) {
+          this.activeKBId = this.knowledgeBases[0].id
+          this.fetchDocumentList()
+          this.fetchKBTags()
+          this.fetchMetadata()
+        }
+      }
+    },
+
+    async fetchKBTags() {
+      if (!this.activeKBId) return
+      const res = await this.apiCall(2026052714301007, { dataset_id: String(this.activeKBId) })
+      if (res && res.code === 1 && res.data) {
+        const tags = res.data.tags || res.data.list || res.data || []
+        const kb = this.knowledgeBases.find(k => k.id === this.activeKBId)
+        if (kb) kb.tags = Array.isArray(tags) ? tags : []
+      }
+    },
+
+    // ====== 文档 API ======
+    async fetchDocumentList() {
+      if (!this.activeKBId) return
+      const res = await this.apiCall(2026052714301017, {
+        dataset_id: String(this.activeKBId),
+        keyword: this.docKeyword,
+        status: '',
+        pageNumber: this.docPage,
+        pageSize: this.docPageSize
+      })
+      if (res && res.code === 1 && res.data) {
+        const list = res.data.data || res.data.list || res.data || []
+        const total = res.data.total || list.length || 0
+        this.docTotal = total
+        this.allDocuments = Array.isArray(list) ? list.map(item => ({
+          id: item.id || item.document_id,
+          kbId: this.activeKBId,
+          name: item.name || item.document_name || item.file_name || '',
+          size: this.formatSize(this.getFileSize(item)),
+          type: (item.type || item.file_type || item.name || '').split('.').pop().toLowerCase(),
+          version: item.version || 1,
+          status: (item.display_status === 'available' ? 'enabled' : item.display_status) || (item.enabled === false ? 'disabled' : 'enabled'),
+          embedStatus: item.indexing_status || item.embedStatus || 'waiting',
+          embedProgress: item.completed_segments !== undefined && item.total_segments
+            ? Math.round(item.completed_segments / item.total_segments * 100)
+            : (item.embedProgress || 0),
+          uploadTime: this.formatTimestamp(item.created_at || item.createdBy),
+          metadata: this.formatMetadata(item.metadata || item.doc_metadata || [])
+        })) : []
+      }
+    },
+
+    // 获取文件大小(兼容不同接口返回结构)
+    getFileSize(item) {
+      return item.data_source_info?.upload_file?.size
+        || item.data_source_detail_dict?.upload_file?.size
+        || item.size
+        || 0
+    },
+
+    // 格式化元数据为可读格式
+    formatMetadata(metadata) {
+      if (!Array.isArray(metadata)) return metadata
+      const result = {}
+      const labelMap = {
+        'document_name': '文档名称',
+        'uploader': '上传者',
+        'upload_date': '上传时间',
+        'last_update_date': '更新时间',
+        'source': '来源'
+      }
+      const sourceMap = {
+        'file_upload': '文件上传',
+        'api': 'API导入',
+        'notion': 'Notion导入',
+        'web': '网页抓取'
+      }
+      metadata.forEach(item => {
+        const key = item.name
+        let value = item.value
+        // 时间戳转日期
+        if (item.type === 'time' && value) {
+          value = this.formatTimestamp(parseFloat(value))
+        }
+        // 来源转中文
+        if (key === 'source' && sourceMap[value]) {
+          value = sourceMap[value]
+        }
+        const label = labelMap[key] || key
+        result[label] = value
+      })
+      return result
+    },
+
+    async fetchDocumentDetail(docId) {
+      const res = await this.apiCall(2026052714301008, {
+        dataset_id: String(this.activeKBId),
+        document_id: String(docId)
+      })
+      if (res && res.code === 1 && res.data) {
+        const d = res.data
+        const idx = this.allDocuments.findIndex(doc => doc.id === docId)
+        if (idx > -1) {
+          this.allDocuments[idx] = {
+            ...this.allDocuments[idx],
+            name: d.name || d.document_name || this.allDocuments[idx].name,
+            size: this.formatSize(this.getFileSize(d)) || this.allDocuments[idx].size,
+            type: (d.type || d.file_type || d.name || '').split('.').pop().toLowerCase(),
+            version: d.version || this.allDocuments[idx].version,
+            status: (d.display_status === 'available' ? 'enabled' : d.display_status) || (d.enabled === false ? 'disabled' : (d.enabled === true ? 'enabled' : this.allDocuments[idx].status)),
+            embedStatus: d.indexing_status || d.embedStatus || this.allDocuments[idx].embedStatus,
+            embedProgress: d.embedProgress || this.allDocuments[idx].embedProgress,
+            metadata: this.formatMetadata(d.doc_metadata || d.metadata || this.allDocuments[idx].metadata)
+          }
+        }
+        this.selectedDocument = { ...this.allDocuments[idx] || this.selectedDocument }
+      }
+    },
+
+    async fetchIndexingStatus(docId, batch) {
+      const res = await this.apiCall(2026052714301012, {
+        dataset_id: String(this.activeKBId),
+        batch: batch || String(docId)
+      })
+      if (res && res.code === 1 && res.data) {
+        const doc = this.allDocuments.find(d => d.id === docId)
+        if (doc) {
+          doc.embedStatus = res.data.indexing_status || res.data.status || doc.embedStatus
+          if (res.data.completed_segments !== undefined && res.data.total_segments) {
+            doc.embedProgress = Math.round(res.data.completed_segments / res.data.total_segments * 100)
+          }
+        }
+      }
+    },
+
+    // ====== 知识库操作 ======
+    onSelectKB(id) {
+      this.activeKBId = id
+      this.activeTab = 'docs'
+      this.fetchDocumentList()
+      this.fetchKBTags()
+      this.fetchMetadata()
+    },
+
+
+    // ====== 文档操作 ======
+    openUploadDialog() {
+      this.uploadDialogVisible = true
+    },
+
+    async onUploadDocument({ files, metadata }) {
+      if (!files || files.length === 0) return
+      this.uploadDialogVisible = false
+      const loading = this.$loading({ lock: true, text: this.$t('正在上传文档') })
+
+      try {
+        for (const file of files) {
+          // 1. 获取 OBS 上传地址
+          const ext = file.name.split('.').pop().toLowerCase()
+          const obsRes = await this.$api.requested({
+            classname: 'system.attachment.huawei.OBS',
+            method: 'getFileName',
+            content: { filename: file.name, filetype: ext, parentid: '' }
+          })
+          if (!obsRes || !obsRes.data || !obsRes.data.uploadurl) {
+            this.$message.error(this.$t('获取上传地址失败', { name: file.name }))
+            continue
+          }
+          const uploadUrl = obsRes.data.uploadurl
+          const serialfilename = obsRes.data.serialfilename
+
+          // 2. 上传文件到华为云 OBS
+          const config = {
+            headers: ext === 'pdf'
+              ? { 'Content-Type': 'application/pdf' }
+              : { 'Content-Type': 'application/octet-stream' },
+            onUploadProgress: (e) => {
+              if (e.total) loading.text = this.$t('正在上传') + ` ${file.name} (${Math.round(e.loaded / e.total * 100)}%)`
+            }
+          }
+          await this.$upload.hw_upload(uploadUrl, file, config)
+
+          // 3. 生成附件记录,拿到 attachmentid
+          const attachRes = await this.$api.requested({
+            classname: 'system.attachment.huawei.OBS',
+            method: 'uploadSuccess',
+            content: { serialfilename }
+          })
+          if (!attachRes || !attachRes.data) {
+            this.$message.error(this.$t('附件记录创建失败', { name: file.name }))
+            continue
+          }
+          const attachmentId = attachRes.data.attachmentids
+            ? attachRes.data.attachmentids[0]
+            : (attachRes.data.attachmentid || attachRes.data.id)
+
+          // 4. 调用 Dify 接口创建文档
+          const difyRes = await this.apiCall(2026052715313901, {
+            dataset_id: String(this.activeKBId),
+            attachmentid: attachmentId
+          })
+          if (difyRes && difyRes.code === 1) {
+            this.$message.success(this.$t('上传成功', { name: file.name }))
+          } else {
+            this.$message.error(this.$t('创建文档失败', { name: file.name }))
+          }
+        }
+
+        // 刷新文档列表
+        this.fetchDocumentList()
+      } catch (e) {
+        console.error('[Upload] error', e)
+        this.$message.error(this.$t('上传失败') + ': ' + (e.message || ''))
+      } finally {
+        loading.close()
+      }
+    },
+
+    loadDocuments() {
+      this.fetchDocumentList()
+    },
+
+    onDocSearch(keyword) {
+      this.docKeyword = keyword
+      this.docPage = 1
+      this.fetchDocumentList()
+    },
+
+    onDocPageChange(page) {
+      this.docPage = page
+      this.fetchDocumentList()
+    },
+
+    onDocSizeChange(size) {
+      this.docPageSize = size
+      this.docPage = 1
+      this.fetchDocumentList()
+    },
+
+    // 浏览器可直接预览的文件类型
+    isPreviewable(type) {
+      const previewable = ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'txt', 'html', 'htm']
+      return previewable.includes((type || '').toLowerCase())
+    },
+
+    async onPreviewFile(doc) {
+      const res = await this.apiCall(2026052714301010, {
+        dataset_id: String(this.activeKBId),
+        document_id: String(doc.id)
+      })
+      if (!res || res.code !== 1 || !res.data) {
+        this.$message.error(this.$t('获取文件地址失败'))
+        return
+      }
+      const url = res.data.url || res.data.download_url || res.data.downloadUrl
+      if (!url) return
+      const fullUrl = res.difyBaseUrl ? res.difyBaseUrl + url : url
+
+      if (this.isPreviewable(doc.type)) {
+        // 把 as_attachment=true 改为 false 实现浏览器预览
+        const previewUrl = fullUrl.replace('as_attachment=true', 'as_attachment=false')
+        window.open(previewUrl, '_blank')
+      } else {
+        try {
+          await this.$confirm('该文件类型不支持在线预览,是否直接下载?', '提示', {
+            confirmButtonText: '下载',
+            cancelButtonText: '取消',
+            type: 'warning'
+          })
+          this.triggerDownload(fullUrl, doc.name || '')
+        } catch {
+          // 用户取消
+        }
+      }
+    },
+
+    async openDocumentDetail(doc) {
+      this.selectedDocument = { ...doc }
+      this.detailDrawerVisible = true
+      this.fetchDocumentDetail(doc.id)
+    },
+
+
+
+    async onToggleDocStatus(docId, callback) {
+      const idx = this.allDocuments.findIndex(d => d.id === docId)
+      if (idx === -1) return
+      const doc = this.allDocuments[idx]
+      const action = doc.status === 'enabled' ? 'disable' : 'enable'
+
+      const ok = await this.apiCall(2026052714301016, {
+        dataset_id: String(this.activeKBId),
+        document_ids: [String(docId)],
+        action
+      })
+      if (ok && ok.code === 1) {
+        const newStatus = action === 'enable' ? 'enabled' : 'disabled'
+        // 用 $set 触发 Vue 2 响应式更新
+        this.$set(this.allDocuments, idx, { ...doc, status: newStatus })
+        // 同步更新详情抽屉的数据
+        if (this.selectedDocument && this.selectedDocument.id === docId) {
+          this.selectedDocument = { ...this.selectedDocument, status: newStatus }
+        }
+        this.$message.success(action === 'enable' ? '文档已启用' : '文档已禁用')
+      }
+      callback && callback()
+    },
+
+    async onDeleteDocument(docId, callback) {
+      const ok = await this.apiCall(2026052714301009, {
+        dataset_id: String(this.activeKBId),
+        document_id: String(docId)
+      })
+      if (ok && ok.code === 1) {
+        const idx = this.allDocuments.findIndex(d => d.id === docId)
+        if (idx > -1) this.allDocuments.splice(idx, 1)
+        // 更新分页总数
+        if (this.docTotal > 0) this.docTotal--
+        // 更新左侧知识库列表的文档数
+        const kb = this.knowledgeBases.find(k => k.id === this.activeKBId)
+        if (kb && kb.docCount > 0) kb.docCount--
+        this.$message.success(this.$t('文档已删除'))
+      }
+      callback && callback()
+    },
+
+    triggerDownload(url, filename) {
+      const a = document.createElement('a')
+      a.href = url
+      a.download = filename || ''
+      a.style.display = 'none'
+      document.body.appendChild(a)
+      a.click()
+      document.body.removeChild(a)
+    },
+
+    async onDownloadDocument(doc) {
+      const res = await this.apiCall(2026052714301010, {
+        dataset_id: String(this.activeKBId),
+        document_id: String(doc.id)
+      })
+      if (res && res.code === 1 && res.data) {
+        const url = res.data.url || res.data.download_url || res.data.downloadUrl
+        if (url) {
+          const fullUrl = res.difyBaseUrl ? res.difyBaseUrl + url : url
+          this.triggerDownload(fullUrl, doc.name || '')
+        }
+      }
+    },
+
+    async onBatchDownload(ids) {
+      for (const docId of ids) {
+        const doc = this.allDocuments.find(d => d.id === docId)
+        if (doc) {
+          await this.onDownloadDocument(doc)
+          await new Promise(r => setTimeout(r, 300))
+        }
+      }
+    },
+
+    // ====== 元数据 API ======
+    async fetchMetadata() {
+      if (!this.activeKBId) return
+      const [sysRes, customRes] = await Promise.all([
+        this.apiCall(2026052715313903, { dataset_id: String(this.activeKBId), iscustom: false }),
+        this.apiCall(2026052715313903, { dataset_id: String(this.activeKBId), iscustom: true })
+      ])
+      // Dify 类型 → 前端显示类型
+      const difyToFrontend = (t) => {
+        const map = { string: 'text', number: 'number', time: 'date' }
+        return map[t] || 'text'
+      }
+      const fields = []
+      // 系统元数据:data.fields[]
+      if (sysRes && sysRes.code === 1 && sysRes.data) {
+        const list = sysRes.data.fields || []
+        list.forEach(item => {
+          fields.push({
+            id: item.name,
+            name: item.name,
+            key: item.name,
+            type: difyToFrontend(item.type),
+            options: [],
+            isSystem: true,
+            enabled: true
+          })
+        })
+      }
+      // 自定义元数据:data.doc_metadata[]
+      if (customRes && customRes.code === 1 && customRes.data) {
+        this.sysMetaEnabled = customRes.data.built_in_field_enabled !== false
+        const list = customRes.data.doc_metadata || []
+        list.forEach(item => {
+          fields.push({
+            id: item.id,
+            name: item.name,
+            key: item.name,
+            type: difyToFrontend(item.type),
+            options: [],
+            isSystem: false,
+            enabled: true
+          })
+        })
+      }
+      this.metadataFields = fields
+    },
+
+    async onCreateField(form, callback) {
+      // 前端类型 → Dify 类型映射
+      const typeMap = { text: 'string', date: 'time' }
+      const res = await this.apiCall(2026052715313904, {
+        dataset_id: String(this.activeKBId),
+        name: form.name,
+        type: typeMap[form.type] || 'string'
+      })
+      if (res && res.code === 1) {
+        this.$message.success(this.$t('元数据创建成功'))
+        this.fetchMetadata()
+      }
+      callback && callback()
+    },
+
+    async onUpdateField({ id, name }, callback) {
+      const res = await this.apiCall(2026052715313906, {
+        dataset_id: String(this.activeKBId),
+        metadata_id: String(id),
+        name
+      })
+      if (res && res.code === 1) {
+        this.$message.success(this.$t('元数据更新成功'))
+        this.fetchMetadata()
+      }
+      callback && callback()
+    },
+
+    async onDeleteField(id, callback) {
+      const res = await this.apiCall(2026052715313905, {
+        dataset_id: String(this.activeKBId),
+        metadata_id: String(id)
+      })
+      if (res && res.code === 1) {
+        this.$message.success(this.$t('元数据删除成功'))
+        this.fetchMetadata()
+      }
+      callback && callback()
+    },
+
+    async onToggleSystemMeta() {
+      const res = await this.apiCall(2026052715313907, {
+        dataset_id: String(this.activeKBId),
+        enable: !this.sysMetaEnabled
+      })
+      if (res && res.code === 1) {
+        this.$message.success(this.sysMetaEnabled ? this.$t('系统元数据已禁用') : this.$t('系统元数据已启用'))
+        this.fetchMetadata()
+      }
+    },
+
+    async onSaveDocMetadata({ docId, metadata }) {
+      const metadataArr = Object.entries(metadata).map(([key, value]) => {
+        const field = this.metadataFields.find(f => f.key === key)
+        return {
+          id: field ? field.id : key,
+          name: field ? field.name : key,
+          value: Array.isArray(value) ? value.join(',') : (value || '')
+        }
+      }).filter(item => item.value)
+
+      const res = await this.apiCall(2026053015572501, {
+        dataset_id: String(this.activeKBId),
+        document_id: String(docId),
+        partial_update: true,
+        metadata: metadataArr
+      })
+      if (res && res.code === 1) {
+        this.$message.success(this.$t('元数据保存成功'))
+        // 重新获取文档详情,确保数据一致
+        this.fetchDocumentDetail(docId)
+      }
+    },
+
+
+
+    // ====== 工具方法 ======
+    formatSize(bytes) {
+      if (!bytes || bytes <= 0) return '-'
+      if (bytes < 1024) return bytes + ' B'
+      if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
+      return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
+    },
+    formatTimestamp(ts) {
+      if (!ts) return ''
+      const d = new Date(ts * 1000)
+      const pad = n => String(n).padStart(2, '0')
+      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+    },
+    getTagColor(tag) {
+      if (!this.tagColorMap[tag]) {
+        const idx = Object.keys(this.tagColorMap).length % TAG_COLORS.length
+        this.$set(this.tagColorMap, tag, TAG_COLORS[idx])
+      }
+      return this.tagColorMap[tag]
+    }
+  }
+}
+</script>
+
+<style scoped>
+.kb-manage {
+  display: flex;
+  height: 100%;
+  overflow: hidden;
+}
+
+.kb-left-panel {
+  width: 280px;
+  min-width: 280px;
+  border-right: 1px solid #e4e7ed;
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+}
+
+.kb-right-panel {
+  flex: 1;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background: #f0f2f5;
+}
+
+.kb-detail-header {
+  flex-shrink: 0;
+  padding: 16px 20px;
+  background: #fff;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.kb-detail-name {
+  margin: 0 0 8px 0;
+  font-size: 18px;
+  color: #303133;
+}
+
+.kb-detail-desc {
+  margin: 0 0 10px 0;
+  font-size: 13px;
+  color: #909399;
+  line-height: 1.5;
+}
+
+.kb-detail-meta {
+  display: flex;
+  gap: 24px;
+  font-size: 12px;
+  color: #909399;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.kb-tag-inline { margin: 0 2px; }
+.kb-no-tag { color: #c0c4cc; }
+
+.kb-tabs-wrapper {
+  flex: 1;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.kb-tabs {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  padding: 0 20px 16px;
+}
+
+.kb-tabs >>> .el-tabs__content {
+  flex: 1;
+  overflow: auto;
+}
+
+.kb-tabs >>> .el-tabs__header {
+  margin-bottom: 16px;
+}
+
+.kb-empty-state {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #c0c4cc;
+}
+
+.kb-empty-state i {
+  font-size: 64px;
+  margin-bottom: 16px;
+}
+
+.kb-empty-state p {
+  font-size: 14px;
+}
+</style>

+ 1 - 1
src/HDrpManagement/reportcenter/index.vue

@@ -36,7 +36,7 @@ export default {
   },
   methods:{
     listCreate (params) {
-      params.content.systemappid = JSON.parse(sessionStorage.getItem('activeApp')).systemappid
+      try { params.content.systemappid = JSON.parse(sessionStorage.getItem('activeApp')).systemappid } catch(e) { params.content.systemappid = '' }
       console.log(params);
 
     },

+ 1 - 1
src/HManagement/serviceDataScreen/index.vue

@@ -126,7 +126,7 @@ export default {
       windowHeight: document.documentElement.clientHeight,
       dialogVisible:false,
       dialogPaw:false,
-      activePath:JSON.parse(sessionStorage.getItem('activeApp')).path
+      activePath: (() => { try { return JSON.parse(sessionStorage.getItem('activeApp')).path } catch(e) { return '/' } })()
     }
   },
   mounted() {

+ 1 - 1
src/HManagement/serviceDataScreen/indexCopy.vue

@@ -126,7 +126,7 @@ export default {
       windowHeight: document.documentElement.clientHeight,
       dialogVisible:false,
       dialogPaw:false,
-      activePath:JSON.parse(sessionStorage.getItem('activeApp')).path
+      activePath: (() => { try { return JSON.parse(sessionStorage.getItem('activeApp')).path } catch(e) { return '/' } })()
     }
   },
   mounted() {

+ 1 - 1
src/SManagement/user/team-manage/TeamManage.vue

@@ -13,7 +13,7 @@
       @listCreate="list">
       <div slot="custom"></div>
       <template v-slot:tbList="scope">
-        <div v-if="scope.data.column.columnname == 'rolenames'">
+        <div v-if="scope.data.column.columnname == 'rolenames' && tool.checkAuth($route.name,'selectrole')">
           <el-tag v-for="(item,index) in scope.data.column.data[[scope.data.column.columnname]]" :key="index" style="margin-right:10px">{{$t(item)}}</el-tag>
         </div>
         <div v-else>

+ 3 - 1
src/SManagement/user/team-manage/components/RemoveTeam.vue

@@ -1,6 +1,8 @@
 <template>
   <div class="remove">
-    <img src="@/assets/remove_btn.png" alt="" @click.stop="dialogVisible=true" slot="icon">
+  <div v-if="tool.checkAuth($route.name,'deleterole')" slot="icon">
+    <img src="@/assets/remove_btn.png" alt="" @click.stop="dialogVisible=true">
+  </div>
     <el-dialog :title="$t('提示')" :visible.sync="dialogVisible" width="30%" :before-close="handleClose">
       <span>{{$t(`该角色已授权,是否确认删除`)}}?</span>
       <span slot="footer" class="dialog-footer">

+ 22 - 17
src/SManagement/user/team-manage/components/add.vue

@@ -1,7 +1,7 @@
 <template>
 <div>
-  <el-button type="primary" size="small" @click="dialogVisible = true">{{$t('新 建')}}</el-button>
-  <el-drawer append-to-body :title="$t('新建')" :visible.sync="dialogVisible" :before-close="handleClose" v-if="tool.checkAuth($route.name,'read')" size="800px">
+  <el-button type="primary" size="small" v-if="tool.checkAuth($route.name,'insertuser')" @click="dialogVisible = true">{{$t('新 建')}}</el-button>
+  <el-drawer append-to-body :title="$t('新建')" :visible.sync="dialogVisible" :before-close="handleClose" v-if="tool.checkAuth($route.name,'insertuser')" size="800px">
     <div class="drawer__panel">
       <el-row :gutter="20">
         <el-form ref="formInfo" :model="param.content" :rules="rules" :label-width="tool.onlyZh('90px')" label-position="right">
@@ -25,9 +25,9 @@
               <el-input v-model="param.content.email" :placeholder="$t('请输入')" size="small"></el-input>
             </el-form-item>
           </el-col>
-          <el-col :span="12">
+          <el-col :span="12" v-if="tool.checkAuth($route.name,'selectrole')">
             <el-form-item :label="`${$t(`角色配置`)}:`" prop="roleids">
-              <el-select style="width:100%" v-model="param.content.roleids" multiple :placeholder="$t('请选择')" size="small">
+              <el-select style="width:100%" v-model="param.content.roleids" multiple :disabled="!tool.checkAuth($route.name,'insertrole')" :placeholder="$t('请选择')" size="small">
                 <el-option
                   v-for="(item,index) in checkList"
                   :key="index"
@@ -68,29 +68,34 @@ export default {
           }
       },
       dialogVisible: false,
-      rules: {
+      checkList: []
+    };
+  },
+  props:['editTarget'],
+  components: {
+  },
+  computed: {
+    rules () {
+      let rules = {
         name: [
           { required: true, message: this.$t('请输入名称'), trigger: 'blur' },
         ],
         phonenumber: [
           { required: true, message: this.$t('请输入手机号'), trigger: 'blur' },
-          { pattern:/^1[3-9]\d{9}$/, message: this.$t('请输入正确手机号码'),trigger: 'blur' }
+          { pattern:/^1[3-9]\\d{9}$/, message: this.$t('请输入正确手机号码'),trigger: 'blur' }
         ],
         email: [
           { required: true, message: this.$t('请输入电子邮箱'), trigger: 'blur' },
-          { pattern: /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/, message: this.$t('请输入正确电子邮箱'),trigger: 'blur' }
-        ],
-        roleids: [
+          { pattern: /^([A-Za-z0-9_\\-\\.])+\\@([A-Za-z0-9_\\-\\.])+\\.([A-Za-z]{2,4})$/, message: this.$t('请输入正确电子邮箱'),trigger: 'blur' }
+        ]
+      }
+      if (this.tool.checkAuth(this.$route.name, 'insertrole')) {
+        rules.roleids = [
           { required: true, message: this.$t('请选择你的权限'), trigger: 'blur' }
         ]
-      },
-      checkList: []
-    };
-  },
-  props:['editTarget'],
-  components: {
-  },
-  computed: {
+      }
+      return rules
+    }
   },
   watch: {
     editTarget: {

+ 21 - 16
src/SManagement/user/team-manage/components/edit.vue

@@ -1,7 +1,7 @@
 <template>
 <div>
-  <el-button type="text" size="small" @click="onShow(dialogVisible = true)">{{$t('编 辑')}}</el-button>
-  <el-drawer append-to-body :title="$t('编辑')" :visible.sync="dialogVisible" :before-close="handleClose" v-if="tool.checkAuth($route.name,'read')" size="800px">
+  <el-button type="text" size="small" v-if="tool.checkAuth($route.name,'updateuser')" @click="onShow(dialogVisible = true)">{{$t('编 辑')}}</el-button>
+  <el-drawer append-to-body :title="$t('编辑')" :visible.sync="dialogVisible" :before-close="handleClose" v-if="tool.checkAuth($route.name,'updateuser')" size="800px">
     <div class="drawer__panel">
       <el-row :gutter="20">
         <el-form ref="formInfo" :model="form" :rules="rules" :label-width="tool.onlyZh('90px')" label-position="right">
@@ -25,9 +25,9 @@
               <el-input v-model="form.email" :placeholder="$t('请输入')" size="small"></el-input>
             </el-form-item>
           </el-col>
-          <el-col :span="12">
+          <el-col :span="12" v-if="tool.checkAuth($route.name,'selectrole')">
             <el-form-item :label="`${$t(`角色配置`)}:`" prop="roleids">
-              <el-select style="width:100%" v-model="form.roleids" multiple :placeholder="$t('请选择')" size="small">
+              <el-select style="width:100%" v-model="form.roleids" multiple :disabled="!tool.checkAuth($route.name,'updaterole')" :placeholder="$t('请选择')" size="small">
                 <el-option
                   v-for="(item,index) in checkList"
                   :key="index"
@@ -70,28 +70,33 @@ export default {
         "roleids":[]
       },
       dialogVisible: false,
-      rules: {
+      checkList: []
+    };
+  },
+  components: {
+  },
+  computed: {
+    rules () {
+      let rules = {
         name: [
           { required: true, message: this.$t('请输入名称'), trigger: 'blur' },
         ],
         phonenumber: [
           { required: true, message: this.$t('请输入手机号'), trigger: 'blur' },
-          { pattern:/^1[3-9]\d{9}$/, message: this.$t('请输入正确手机号码'),trigger: 'blur' }
+          { pattern:/^1[3-9]\\d{9}$/, message: this.$t('请输入正确手机号码'),trigger: 'blur' }
         ],
         email: [
           { required: true, message: this.$t('请输入电子邮箱'), trigger: 'blur' },
-          { pattern: /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/, message: this.$t('请输入正确电子邮箱'),trigger: 'blur' }
-        ],
-        roleids: [
+          { pattern: /^([A-Za-z0-9_\\-\\.])+\\@([A-Za-z0-9_\\-\\.])+\\.([A-Za-z]{2,4})$/, message: this.$t('请输入正确电子邮箱'),trigger: 'blur' }
+        ]
+      }
+      if (this.tool.checkAuth(this.$route.name, 'updaterole')) {
+        rules.roleids = [
           { required: true, message: this.$t('请选择你的权限'), trigger: 'blur' }
         ]
-      },
-      checkList: []
-    };
-  },
-  components: {
-  },
-  computed: {
+      }
+      return rules
+    }
   },
   watch: {
     editTarget: {

+ 1 - 1
src/components/normal-basic-layout-new/details/index.vue

@@ -120,7 +120,7 @@ export default {
       collapse:true,
       refreshPage:true,
       reportCenterLsit:[],
-      systemappid:JSON.parse(sessionStorage.getItem('activeApp')).systemappid,
+      systemappid: (() => { try { return JSON.parse(sessionStorage.getItem('activeApp')).systemappid } catch(e) { return '' } })(),
       turnTotal:sessionStorage.getItem('total'),
     }
   },

+ 1 - 1
src/components/normal-basic-layout-new/details/modules/tabs/tab.vue

@@ -47,7 +47,7 @@ export default {
       activeName:'tab0',
       attinfo_attachment:[],
       folderid:JSON.parse(sessionStorage.getItem('folderid')).appfolderid,
-      hidemediastock:!JSON.parse(sessionStorage.getItem('activeApp')).selectfileinattachment,
+      hidemediastock: (() => { try { return !JSON.parse(sessionStorage.getItem('activeApp')).selectfileinattachment } catch(e) { return false } })(),
       hrid:JSON.parse(sessionStorage.getItem('active_account')).hrid,
       issalehr:false,
       siteid:JSON.parse(sessionStorage.getItem('active_account')).siteid

+ 1 - 1
src/components/normal-basic-layout-new/details/modules/task/addTask.vue

@@ -160,7 +160,7 @@ export default {
         "ownerid":this.$route.query.id,
         "endtime":"",
 				"leaderuserid":"",
-        "systemappid":JSON.parse(sessionStorage.getItem('activeApp')).systemappid,
+        "systemappid": (() => { try { return JSON.parse(sessionStorage.getItem('activeApp')).systemappid } catch(e) { return '' } })(),
         "team":{
           "justuserids":true,
           "userids":[],

+ 1 - 1
src/components/normal-basic-layout-new/index.vue

@@ -146,7 +146,7 @@ export default {
       refreshTable:true,
       title:'',
       name:'',
-      systemappid:JSON.parse(sessionStorage.getItem('activeApp')).systemappid,
+      systemappid: (() => { try { return JSON.parse(sessionStorage.getItem('activeApp')).systemappid } catch(e) { return '' } })(),
       reportCenterLsit:[]
     }
   },

+ 1 - 1
src/components/normal-basic-layout-new/normalBook.vue

@@ -149,7 +149,7 @@ export default {
       refreshTable:true,
       title:'',
       name:'',
-      systemappid:JSON.parse(sessionStorage.getItem('activeApp')).systemappid,
+      systemappid: (() => { try { return JSON.parse(sessionStorage.getItem('activeApp')).systemappid } catch(e) { return '' } })(),
       reportCenterLsit:[]
     }
   },

+ 1 - 1
src/components/normal-basic-layout/details/index.vue

@@ -165,7 +165,7 @@ export default {
       collapse:true,
       refreshPage:true,
       reportCenterLsit:[],
-      systemappid:JSON.parse(sessionStorage.getItem('activeApp')).systemappid,
+      systemappid: (() => { try { return JSON.parse(sessionStorage.getItem('activeApp')).systemappid } catch(e) { return '' } })(),
       time:null
     }
   },

+ 1 - 1
src/components/normal-basic-layout/details/modules/tabs/tab.vue

@@ -47,7 +47,7 @@ export default {
       activeName:'tab0',
       attinfo_attachment:[],
       folderid:JSON.parse(sessionStorage.getItem('folderid')).appfolderid,
-      hidemediastock:!JSON.parse(sessionStorage.getItem('activeApp')).selectfileinattachment,
+      hidemediastock: (() => { try { return !JSON.parse(sessionStorage.getItem('activeApp')).selectfileinattachment } catch(e) { return false } })(),
       hrid:JSON.parse(sessionStorage.getItem('active_account')).hrid,
       issalehr:false,
       siteid:JSON.parse(sessionStorage.getItem('active_account')).siteid

+ 1 - 1
src/components/normal-basic-layout/details/modules/task/addTask.vue

@@ -160,7 +160,7 @@ export default {
         "ownerid":this.$route.query.id,
         "endtime":"",
 				"leaderuserid":"",
-        "systemappid":JSON.parse(sessionStorage.getItem('activeApp')).systemappid,
+        "systemappid": (() => { try { return JSON.parse(sessionStorage.getItem('activeApp')).systemappid } catch(e) { return '' } })(),
         "team":{
           "justuserids":true,
           "userids":[],

+ 1 - 1
src/components/normal-basic-layout/index.vue

@@ -147,7 +147,7 @@ export default {
       refreshTable:true,
       title:'',
       name:'',
-      systemappid:JSON.parse(sessionStorage.getItem('activeApp')).systemappid,
+      systemappid: (() => { try { return JSON.parse(sessionStorage.getItem('activeApp')).systemappid } catch(e) { return '' } })(),
       reportCenterLsit:[]
     }
   },

+ 1 - 1
src/components/normal-basic-layout/normalBook.vue

@@ -149,7 +149,7 @@ export default {
       refreshTable:true,
       title:'',
       name:'',
-      systemappid:JSON.parse(sessionStorage.getItem('activeApp')).systemappid,
+      systemappid: (() => { try { return JSON.parse(sessionStorage.getItem('activeApp')).systemappid } catch(e) { return '' } })(),
       reportCenterLsit:[]
     }
   },

+ 13 - 0
src/router/AIManagement.js

@@ -0,0 +1,13 @@
+const AIManagement = [
+  {
+    path: '/KBM',
+    name: 'KBM',
+    meta: {
+      title: '知识库管理',
+      ast_nav: true,
+      keeproute: true,
+    },
+    component: () => import(/* webpackChunkName: "about" */ '@/AIManagement/KBM/index')
+  },
+];
+export default AIManagement;

+ 2 - 1
src/router/index.js

@@ -5,6 +5,7 @@ import SManagement from './SManagement.js';
 import HDrpManagement from './HDrpManagement.js';
 import SDrpManagement from './SDrpManagement.js';
 import bgj from './bgj.js';
+import AIManagement from './AIManagement.js';
 import WebsiteManagement from './WebsiteManagement';
 import OptionSystem from './optionSystem';
 
@@ -173,7 +174,7 @@ let routes = [
 ];
 
 
-routes[2].children = [...routes[2].children,...HManagement,...SManagement,...HDrpManagement,...SDrpManagement,...WebsiteManagement,...OptionSystem,...bgj, {
+routes[2].children = [...routes[2].children,...HManagement,...SManagement,...HDrpManagement,...SDrpManagement,...WebsiteManagement,...OptionSystem,...bgj,...AIManagement, {
   path: '/404',
   name: 'notFound',
   meta: { title: '404', ast_nav: false },

+ 3 - 1
src/utils/tool.js

@@ -11,7 +11,9 @@ export default {
     // await this.sleep(3000)
     let _haveAuth = false
 
-    let app = JSON.parse(sessionStorage.getItem('activeApp'))
+    let app
+    try { app = JSON.parse(sessionStorage.getItem('activeApp')) } catch(e) { return false }
+    if (!app || !app.meta) return false
     // 获取当前应用权限
     let auth_list = app.meta.auth
     // 判断是否拥有权限