Преглед на файлове

新增知识库上传文件的功能

余思翰 преди 1 седмица
родител
ревизия
dbdab2ba79

+ 1
- 1
llm-back/ruoyi-admin/src/main/resources/application.yml Целия файл

@@ -7,7 +7,7 @@ cmc:
7 7
   # 版权年份
8 8
   copyrightYear: 2024
9 9
   # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
10
-  profile: /home/cmc/projects/cmc-llm
10
+  profile: D:/ruoyi/uploadPath
11 11
   # 获取ip地址开关
12 12
   addressEnabled: false
13 13
   # 验证码类型 math 数字计算 char 字符验证

+ 15
- 4
llm-back/ruoyi-agent/src/main/java/com/ruoyi/agent/service/MilvusService.java Целия файл

@@ -153,16 +153,26 @@ public class MilvusService {
153 153
     public List<String> listDocument(String collectionName, String fileType) {
154 154
         List<String> documentList = new ArrayList<>();
155 155
         loadCollectionName(collectionName);
156
-        QuerySimpleParam queryParam = QuerySimpleParam.newBuilder()
156
+        QuerySimpleParam queryParam = QuerySimpleParam.newBuilder()        
157
+                .withCollectionName(collectionName)
158
+                .withFilter("id > 0")
159
+                .withOutputFields(Arrays.asList("file_type", "file_name"))
160
+                .withLimit(16384L)
161
+                .build();
162
+        if (fileType != null && !fileType.equals(""))
163
+            queryParam = QuerySimpleParam.newBuilder()
157 164
                 .withCollectionName(collectionName)
158 165
                 .withFilter(String.format("file_type == \"%s\"", fileType))
159 166
                 .withOutputFields(Arrays.asList("file_type", "file_name"))
160 167
                 .withLimit(16384L)
161 168
                 .build();
162 169
         QueryResponse queryResults = milvusClient.query(queryParam).getData();
163
-        List<QueryResultsWrapper.RowRecord> rowRecordList = queryResults.getRowRecords();
164
-        for (QueryResultsWrapper.RowRecord rowRecord : rowRecordList) {
165
-            documentList.add(rowRecord.get("file_name").toString());
170
+        List<QueryResultsWrapper.RowRecord> rowRecordList = new ArrayList<>();
171
+        if (queryResults != null) {
172
+            rowRecordList = queryResults.getRowRecords();
173
+            for (QueryResultsWrapper.RowRecord rowRecord : rowRecordList) {
174
+                documentList.add(rowRecord.get("file_name").toString());
175
+            }
166 176
         }
167 177
         releaseCollectionName(collectionName);
168 178
         return documentList.stream().distinct().collect(Collectors.toList());
@@ -172,6 +182,7 @@ public class MilvusService {
172 182
      * 删除知识库文件
173 183
      */
174 184
     public void removeDocument(String collectionName, String fileName) {
185
+        loadCollectionName(collectionName);
175 186
         DeleteParam deleteParam = DeleteParam.newBuilder()
176 187
                 .withCollectionName(collectionName)
177 188
                 .withExpr(String.format("file_name == \"%s\"", fileName))

+ 77
- 0
llm-ui/src/api/llm/knowLedge.js Целия файл

@@ -0,0 +1,77 @@
1
+/*
2
+ * @Author: ysh
3
+ * @Date: 2025-06-30 09:56:10
4
+ * @LastEditors: Please set LastEditors
5
+ * @LastEditTime: 2025-07-03 10:47:54
6
+ */
7
+import request from '@/utils/request'
8
+
9
+// 查询知识库列表
10
+export function listKnowledge(query) {
11
+  return request({
12
+    url: '/llm/knowledge/list',
13
+    method: 'get',
14
+    params: query
15
+  })
16
+}
17
+
18
+// 新增知识库
19
+export function addKnowledge(collectionName, description) {
20
+  return request({
21
+    url: '/llm/knowledge/create',
22
+    method: 'post',
23
+    params: { collectionName, description }
24
+  })
25
+}
26
+
27
+// 修改知识库
28
+export function updateKnowledge(collectionName, newCollectionName) {
29
+  return request({
30
+    url: '/llm/knowledge/modify',
31
+    method: 'post',
32
+    params: { collectionName, newCollectionName }
33
+  })
34
+}
35
+
36
+// 删除知识库
37
+export function delKnowledge(collectionName) {
38
+  return request({
39
+    url: '/llm/knowledge/remove',
40
+    method: 'delete',
41
+    params: { collectionName }
42
+  })
43
+}
44
+
45
+// 插入知识库文件
46
+export function insertKnowledgeFile(file, collectionName) {
47
+  const formData = new FormData()
48
+  formData.append('file', file)
49
+  formData.append('collectionName', collectionName)
50
+  return request({
51
+    url: '/llm/knowledge/insertDocument',
52
+    method: 'post',
53
+    data: formData,
54
+    headers: {
55
+      'Content-Type': 'multipart/form-data'
56
+    }
57
+  })
58
+}
59
+
60
+// 查看知识库文件
61
+export function listKnowledgeDocument(collectionName, fileType) {
62
+  return request({
63
+    url: '/llm/knowledge/listDocument',
64
+    method: 'get',
65
+    params: { collectionName, fileType }
66
+  })
67
+}
68
+
69
+
70
+// 删除知识库文件
71
+export function deleteKnowledgeFile(fileName,collectionName) {
72
+  return request({
73
+    url: '/llm/knowledge/removeDocument',
74
+    method: 'delete',
75
+    params: { fileName, collectionName }
76
+  })
77
+}

+ 867
- 3
llm-ui/src/views/llm/knowledge/index.vue Целия файл

@@ -1,3 +1,867 @@
1
-<template></template>
2
-<script></script>
3
-<style></style>
1
+<template>
2
+  <div class="app-container">
3
+    <!-- 顶部搜索和操作栏 -->
4
+    <div class="top-toolbar">
5
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
6
+        <el-form-item label="知识库名称" prop="collectionName">
7
+          <el-input v-model="queryParams.collectionName" placeholder="请输入知识库名称" clearable @keyup.enter="handleQuery" />
8
+        </el-form-item>
9
+        <el-form-item>
10
+          <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
11
+          <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
12
+        </el-form-item>
13
+      </el-form>
14
+
15
+      <div class="toolbar-actions">
16
+        <el-button type="primary" plain :icon="Plus" @click="handleAdd" v-hasPermi="['llm:knowledge:add']">
17
+          新增知识库
18
+        </el-button>
19
+        <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
20
+      </div>
21
+    </div>
22
+
23
+    <!-- 主要内容区域 -->
24
+    <div class="main-content">
25
+      <!-- 左侧知识库列表 -->
26
+      <div class="left-panel">
27
+        <div class="panel-header">
28
+          <h3>知识库列表</h3>
29
+          <span class="knowledge-count">{{ knowledgeList.length }} 个知识库</span>
30
+        </div>
31
+
32
+        <div class="knowledge-cards" v-loading="loading">
33
+          <div v-for="(item, index) in knowledgeList" :key="index" class="knowledge-card"
34
+            :class="{ 'active': selectedKnowledge?.collectionName === item.collectionName }"
35
+            @click="selectKnowledge(item)">
36
+            <div class="card-header">
37
+              <div class="card-title">
38
+                <el-icon class="folder-icon">
39
+                  <Folder />
40
+                </el-icon>
41
+                <span class="title-text">{{ item.collectionName }}</span>
42
+              </div>
43
+              <div class="card-actions">
44
+                <el-dropdown trigger="click" @command="handleCardAction">
45
+                  <el-button type="text" size="small">
46
+                    <el-icon>
47
+                      <MoreFilled />
48
+                    </el-icon>
49
+                  </el-button>
50
+                  <template #dropdown>
51
+                    <el-dropdown-menu>
52
+                      <el-dropdown-item :command="{ action: 'edit', data: item }" :icon="Edit">编辑</el-dropdown-item>
53
+                      <el-dropdown-item :command="{ action: 'upload', data: item }"
54
+                        :icon="Upload">上传文件</el-dropdown-item>
55
+                      <el-dropdown-item :command="{ action: 'delete', data: item }" :icon="Delete"
56
+                        divided>删除</el-dropdown-item>
57
+                    </el-dropdown-menu>
58
+                  </template>
59
+                </el-dropdown>
60
+              </div>
61
+            </div>
62
+
63
+            <div class="card-content">
64
+              <p class="description">{{ item.description || '暂无描述' }}</p>
65
+              <div class="meta-info">
66
+                <span class="create-time">{{ parseTime(item.createdTime, '{y}-{m}-{d}') }}</span>
67
+                <span class="file-count">{{ item.fileCount || 0 }} 个文件</span>
68
+              </div>
69
+            </div>
70
+          </div>
71
+
72
+          <!-- 空状态 -->
73
+          <div v-if="knowledgeList.length === 0 && !loading" class="empty-state">
74
+            <el-icon class="empty-icon">
75
+              <FolderOpened />
76
+            </el-icon>
77
+            <p>暂无知识库</p>
78
+            <el-button type="primary" @click="handleAdd">创建第一个知识库</el-button>
79
+          </div>
80
+        </div>
81
+      </div>
82
+
83
+      <!-- 右侧文件列表 -->
84
+      <div class="right-panel">
85
+        <div class="panel-header">
86
+          <h3>文件列表</h3>
87
+          <div class="header-actions">
88
+            <el-button type="primary" :icon="Upload" @click="handleUpload(selectedKnowledge)"
89
+              :disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:upload']">
90
+              上传文件
91
+            </el-button>
92
+          </div>
93
+        </div>
94
+
95
+        <div class="file-content" v-loading="fileLoading">
96
+          <div v-if="selectedKnowledge" class="selected-knowledge">
97
+            <el-icon class="folder-icon">
98
+              <Folder />
99
+            </el-icon>
100
+            <span class="knowledge-name">{{ selectedKnowledge.collectionName }}</span>
101
+          </div>
102
+
103
+          <div v-if="!selectedKnowledge" class="empty-state">
104
+            <el-icon class="empty-icon">
105
+              <Document />
106
+            </el-icon>
107
+            <p>请选择一个知识库查看文件</p>
108
+          </div>
109
+
110
+          <div v-else-if="fileList.length === 0" class="empty-state">
111
+            <el-icon class="empty-icon">
112
+              <Document />
113
+            </el-icon>
114
+            <p>该知识库暂无文件</p>
115
+            <el-button type="primary" @click="handleUpload(selectedKnowledge)">上传文件</el-button>
116
+          </div>
117
+
118
+          <div v-else class="file-list">
119
+            <div v-for="(file, index) in fileList" :key="index" class="file-item">
120
+              <div class="file-info">
121
+                <el-icon class="file-icon">
122
+                  <Document />
123
+                </el-icon>
124
+                <div class="file-details">
125
+                  <div class="file-name">{{ file }}</div>
126
+                  <div class="file-meta">
127
+                    <span class="file-type">{{ getFileType(file) }}</span>
128
+                  </div>
129
+                </div>
130
+              </div>
131
+              <div class="file-actions">
132
+                <el-button type="danger" size="small" :icon="Delete" @click="handleDeleteFile(file)">
133
+                  删除
134
+                </el-button>
135
+              </div>
136
+            </div>
137
+          </div>
138
+        </div>
139
+      </div>
140
+    </div>
141
+
142
+    <!-- 添加或修改知识库对话框 -->
143
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
144
+      <el-form ref="knowledgeFormRef" :model="form" :rules="rules" label-width="120px">
145
+        <el-form-item label="知识库名称" prop="collectionName">
146
+          <el-input v-model="form.collectionName" placeholder="请输入知识库名称" :disabled="isModify" />
147
+        </el-form-item>
148
+        <el-form-item label="新知识库名称" prop="newCollectionName" v-if="isModify">
149
+          <el-input v-model="form.newCollectionName" placeholder="请输入新知识库名称" />
150
+        </el-form-item>
151
+        <el-form-item label="描述" prop="description" v-if="!isModify">
152
+          <el-input type="textarea" v-model="form.description" placeholder="请输入描述" />
153
+        </el-form-item>
154
+      </el-form>
155
+      <template #footer>
156
+        <div class="dialog-footer">
157
+          <el-button type="primary" @click="submitForm">确 定</el-button>
158
+          <el-button @click="cancel">取 消</el-button>
159
+        </div>
160
+      </template>
161
+    </el-dialog>
162
+
163
+    <!-- 上传文件对话框 -->
164
+    <el-dialog title="上传文件到知识库" v-model="uploadOpen" width="500px" append-to-body>
165
+      <el-form ref="uploadFormRef" :model="uploadForm" :rules="uploadRules" label-width="80px">
166
+        <el-form-item label="知识库" prop="collectionName">
167
+          <el-input v-model="uploadForm.collectionName" disabled />
168
+        </el-form-item>
169
+        <el-form-item label="选择文件" prop="file">
170
+          <el-upload ref="knowledgeUpload" :limit="1" accept=".doc,.docx,.pdf" :headers="upload.headers" :action="''"
171
+            :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess"
172
+            :auto-upload="false" drag :on-change="handleFileChange">
173
+            <el-icon class="el-icon--upload"><upload-filled /></el-icon>
174
+            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
175
+            <template #tip>
176
+              <div class="el-upload__tip text-center">
177
+                <div class="el-upload__tip">
178
+                  <el-checkbox v-model="upload.updateSupport" />
179
+                  是否更新已经存在的文件数据
180
+                </div>
181
+                <span>支持 .doc、.docx、.pdf 格式文件</span>
182
+              </div>
183
+            </template>
184
+          </el-upload>
185
+        </el-form-item>
186
+      </el-form>
187
+      <template #footer>
188
+        <div class="dialog-footer">
189
+          <el-button type="primary" @click="submitUpload">确 定</el-button>
190
+          <el-button @click="cancelUpload">取 消</el-button>
191
+        </div>
192
+      </template>
193
+    </el-dialog>
194
+  </div>
195
+</template>
196
+
197
+<script setup>
198
+import { ref, reactive, getCurrentInstance, onMounted, nextTick, watch, computed } from 'vue'
199
+import { Search, Refresh, Plus, Edit, Delete, Upload, UploadFilled, Folder, MoreFilled, FolderOpened, Document } from '@element-plus/icons-vue'
200
+import { listKnowledge, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile } from "@/api/llm/knowLedge";
201
+import { getToken } from "@/utils/auth";
202
+
203
+const { proxy } = getCurrentInstance();
204
+
205
+const knowledgeList = ref([]);
206
+const open = ref(false);
207
+const uploadOpen = ref(false);
208
+const loading = ref(true);
209
+const fileLoading = ref(false);
210
+const showSearch = ref(true);
211
+const ids = ref([]);
212
+const single = ref(true);
213
+const multiple = ref(true);
214
+const total = ref(0);
215
+const title = ref("");
216
+const isModify = ref(false);
217
+const selectedKnowledge = ref(null);
218
+const fileList = ref([]);
219
+
220
+// 表单数据
221
+const form = reactive({
222
+  collectionName: '',
223
+  description: '',
224
+  newCollectionName: ''
225
+});
226
+
227
+// 查询参数
228
+const queryParams = reactive({
229
+  collectionName: ''
230
+});
231
+
232
+// 表单验证规则
233
+const rules = reactive({
234
+  collectionName: [
235
+    { required: true, message: "知识库名称不能为空", trigger: "blur" }
236
+  ]
237
+});
238
+
239
+// 上传配置
240
+const upload = reactive({
241
+  file: null,
242
+  fileList: [],
243
+  // 是否禁用上传
244
+  isUploading: false,
245
+  // 是否更新已经存在的文件数据
246
+  updateSupport: false,
247
+  // 设置上传的请求头部
248
+  headers: { Authorization: "Bearer " + getToken() }
249
+});
250
+
251
+// 上传表单
252
+const uploadForm = reactive({
253
+  collectionName: '',
254
+  file: null
255
+});
256
+
257
+// 上传验证规则
258
+const uploadRules = reactive({
259
+  file: [
260
+    { required: true, message: "请选择要上传的文件", trigger: "blur" }
261
+  ]
262
+});
263
+
264
+/** 查询知识库列表 */
265
+function getList() {
266
+  loading.value = true;
267
+  listKnowledge().then(response => {
268
+    // 确保返回的数据是数组格式
269
+    if (Array.isArray(response.data)) {
270
+      knowledgeList.value = response.data;
271
+    } else {
272
+      knowledgeList.value = [];
273
+    }
274
+    loading.value = false;
275
+  }).catch(error => {
276
+    loading.value = false;
277
+    proxy.$modal.msgError("获取知识库列表失败:" + error.message);
278
+  });
279
+}
280
+
281
+/** 选择知识库 */
282
+function selectKnowledge(knowledge) {
283
+  selectedKnowledge.value = knowledge;
284
+  fileLoading.value = true;
285
+  listKnowledgeDocument(knowledge.collectionName).then(response => {
286
+    fileList.value = response.data;
287
+    fileLoading.value = false;
288
+  }).catch(error => {
289
+    fileLoading.value = false;
290
+    proxy.$modal.msgError("获取文件列表失败:" + error.message);
291
+  });
292
+}
293
+
294
+
295
+
296
+/** 处理卡片操作 */
297
+function handleCardAction(command) {
298
+  const { action, data } = command;
299
+  switch (action) {
300
+    case 'edit':
301
+      handleUpdate(data);
302
+      break;
303
+    case 'upload':
304
+      handleUpload(data);
305
+      break;
306
+    case 'delete':
307
+      handleDelete(data);
308
+      break;
309
+  }
310
+}
311
+
312
+/** 删除文件 */
313
+function handleDeleteFile(file) {
314
+  proxy.$modal.confirm(`是否确认删除文件"${file}"?`).then(() => {
315
+    deleteKnowledgeFile(file, selectedKnowledge.value.collectionName).then(response => {
316
+      proxy.$modal.msgSuccess("删除成功");
317
+      // 刷新文件列表
318
+      if (selectedKnowledge.value) {
319
+        fileLoading.value = true;
320
+        listKnowledgeDocument(selectedKnowledge.value.collectionName).then(response => {
321
+          fileList.value = response.data;
322
+          fileLoading.value = false;
323
+        }).catch(error => {
324
+          fileLoading.value = false;
325
+          proxy.$modal.msgError("刷新文件列表失败:" + error.message);
326
+        });
327
+      }
328
+    }).catch(error => {
329
+      proxy.$modal.msgError("删除失败:" + error.message);
330
+    });
331
+  }).catch(() => { });
332
+}
333
+
334
+/** 获取文件类型 */
335
+function getFileType(fileName) {
336
+  if (!fileName) return '未知';
337
+  const extension = fileName.split('.').pop()?.toLowerCase();
338
+  switch (extension) {
339
+    case 'pdf':
340
+      return 'PDF文档';
341
+    case 'doc':
342
+      return 'Word文档';
343
+    case 'docx':
344
+      return 'Word文档';
345
+    default:
346
+      return '文档';
347
+  }
348
+}
349
+
350
+// 取消按钮
351
+function cancel() {
352
+  open.value = false;
353
+  reset();
354
+}
355
+
356
+// 表单重置
357
+function reset() {
358
+  form.collectionName = '';
359
+  form.description = '';
360
+  form.newCollectionName = '';
361
+  // 强制更新DOM
362
+  nextTick(() => {
363
+    proxy.resetForm("form");
364
+  });
365
+}
366
+
367
+/** 搜索按钮操作 */
368
+function handleQuery() {
369
+  getList();
370
+}
371
+
372
+/** 重置按钮操作 */
373
+function resetQuery() {
374
+  proxy.resetForm("queryForm");
375
+  handleQuery();
376
+}
377
+
378
+
379
+/** 新增按钮操作 */
380
+function handleAdd() {
381
+  reset();
382
+  open.value = true;
383
+  title.value = "添加知识库";
384
+  isModify.value = false;
385
+}
386
+
387
+/** 修改按钮操作 */
388
+function handleUpdate(row) {
389
+  reset();
390
+  const collectionName = row?.collectionName || ids.value[0];
391
+  if (!collectionName) {
392
+    proxy.$modal.msgWarning("请先选择要修改的知识库");
393
+    return;
394
+  }
395
+  form.collectionName = collectionName;
396
+  form.newCollectionName = '';
397
+  isModify.value = true;
398
+  open.value = true;
399
+  title.value = "修改知识库";
400
+}
401
+
402
+/** 提交按钮 */
403
+function submitForm() {
404
+  proxy.$refs["knowledgeFormRef"].validate(valid => {
405
+    if (valid) {
406
+      if (form.newCollectionName) {
407
+        // 修改操作
408
+        updateKnowledge(form.collectionName, form.newCollectionName).then(response => {
409
+          proxy.$modal.msgSuccess("修改成功");
410
+          open.value = false;
411
+          getList();
412
+        }).catch(error => {
413
+          proxy.$modal.msgError("修改失败:" + error.message);
414
+        });
415
+      } else {
416
+        // 新增操作
417
+        addKnowledge(form.collectionName, form.description).then(response => {
418
+          proxy.$modal.msgSuccess("新增成功");
419
+          open.value = false;
420
+          getList();
421
+        }).catch(error => {
422
+          proxy.$modal.msgError("新增失败:" + error.message);
423
+        });
424
+      }
425
+    }
426
+  });
427
+}
428
+
429
+/** 删除按钮操作 */
430
+function handleDelete(row) {
431
+  const collectionNames = row?.collectionName || ids.value;
432
+  if (!collectionNames) {
433
+    proxy.$modal.msgWarning("请先选择要删除的知识库");
434
+    return;
435
+  }
436
+  proxy.$modal.confirm('是否确认删除知识库名称为"' + collectionNames + '"的数据项?').then(function () {
437
+    return delKnowledge(collectionNames);
438
+  }).then(() => {
439
+    getList();
440
+    // 如果删除的是当前选中的知识库,清空选择
441
+    if (selectedKnowledge.value?.collectionName === collectionNames) {
442
+      selectedKnowledge.value = null;
443
+      fileList.value = [];
444
+    }
445
+    proxy.$modal.msgSuccess("删除成功");
446
+  }).catch((error) => {
447
+    if (error !== 'cancel') {
448
+      proxy.$modal.msgError("删除失败:" + error.message);
449
+    }
450
+  });
451
+}
452
+
453
+/** 上传文件按钮操作 */
454
+function handleUpload(row) {
455
+  const collectionName = row?.collectionName;
456
+  if (!collectionName) {
457
+    proxy.$modal.msgWarning("请先选择要上传文件的知识库");
458
+    return;
459
+  }
460
+  uploadForm.collectionName = collectionName;
461
+  uploadOpen.value = true;
462
+}
463
+
464
+// 文件上传中处理
465
+function handleFileUploadProgress(event, file, fileList) {
466
+  upload.isUploading = true;
467
+}
468
+
469
+function handleFileChange(file, fileList) {
470
+  upload.file = file.raw;
471
+}
472
+
473
+// 文件上传成功处理
474
+function handleFileSuccess(response, file, fileList) {
475
+  upload.isUploading = false;
476
+  proxy.$refs.upload.clearFiles();
477
+  proxy.$alert(response.msg, "导入结果", { dangerouslyUseHTMLString: true });
478
+  uploadOpen.value = false;
479
+  getList();
480
+  // 如果当前有选中的知识库,刷新文件列表
481
+  if (selectedKnowledge.value) {
482
+    fileLoading.value = true;
483
+    listKnowledgeDocument(selectedKnowledge.value.collectionName).then(response => {
484
+      fileList.value = response.data;
485
+      fileLoading.value = false;
486
+    }).catch(error => {
487
+      fileLoading.value = false;
488
+      proxy.$modal.msgError("刷新文件列表失败:" + error.message);
489
+    });
490
+  }
491
+}
492
+
493
+// 提交上传
494
+function submitUpload() {
495
+  insertKnowledgeFile(upload.file, uploadForm.collectionName).then(response => {
496
+    proxy.$modal.msgSuccess("上传成功");
497
+    uploadOpen.value = false;
498
+    proxy.$refs.knowledgeUpload.clearFiles();
499
+    getList();
500
+  }).catch(error => {
501
+    proxy.$modal.msgError("上传失败:" + error.message);
502
+  });
503
+}
504
+
505
+// 取消上传
506
+function cancelUpload() {
507
+  uploadOpen.value = false;
508
+  proxy.$refs.knowledgeUpload.clearFiles();
509
+}
510
+
511
+onMounted(() => {
512
+  getList();
513
+});
514
+</script>
515
+
516
+<style lang="scss" scoped>
517
+.app-container {
518
+  height: 100vh;
519
+  display: flex;
520
+  flex-direction: column;
521
+  background: #f5f7fa;
522
+}
523
+
524
+.top-toolbar {
525
+  background: white;
526
+  padding: 16px 24px;
527
+  border-bottom: 1px solid #e4e7ed;
528
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
529
+
530
+  .toolbar-actions {
531
+    margin-top: 12px;
532
+    display: flex;
533
+    gap: 8px;
534
+    align-items: center;
535
+  }
536
+}
537
+
538
+.main-content {
539
+  flex: 1;
540
+  display: flex;
541
+  gap: 16px;
542
+  padding: 16px;
543
+  overflow: hidden;
544
+}
545
+
546
+.left-panel {
547
+  width: 400px;
548
+  background: white;
549
+  border-radius: 8px;
550
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
551
+  display: flex;
552
+  flex-direction: column;
553
+  overflow: hidden;
554
+}
555
+
556
+.right-panel {
557
+  flex: 1;
558
+  background: white;
559
+  border-radius: 8px;
560
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
561
+  display: flex;
562
+  flex-direction: column;
563
+  overflow: hidden;
564
+}
565
+
566
+.panel-header {
567
+  padding: 20px 24px;
568
+  border-bottom: 1px solid #e4e7ed;
569
+  display: flex;
570
+  justify-content: space-between;
571
+  align-items: center;
572
+  background: #fafbfc;
573
+
574
+  h3 {
575
+    margin: 0;
576
+    font-size: 16px;
577
+    font-weight: 600;
578
+    color: #303133;
579
+  }
580
+
581
+  .knowledge-count {
582
+    font-size: 12px;
583
+    color: #909399;
584
+    background: #f0f2f5;
585
+    padding: 4px 8px;
586
+    border-radius: 4px;
587
+  }
588
+
589
+  .header-actions {
590
+    display: flex;
591
+    gap: 8px;
592
+  }
593
+}
594
+
595
+.knowledge-cards {
596
+  flex: 1;
597
+  padding: 16px;
598
+  overflow-y: auto;
599
+  display: flex;
600
+  flex-direction: column;
601
+  gap: 12px;
602
+}
603
+
604
+.knowledge-card {
605
+  background: white;
606
+  border: 1px solid #e4e7ed;
607
+  border-radius: 8px;
608
+  padding: 16px;
609
+  cursor: pointer;
610
+  transition: all 0.3s ease;
611
+  position: relative;
612
+
613
+  &:hover {
614
+    border-color: #409eff;
615
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
616
+    transform: translateY(-2px);
617
+  }
618
+
619
+  &.active {
620
+    border-color: #409eff;
621
+    background: #f0f9ff;
622
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
623
+  }
624
+
625
+  .card-header {
626
+    display: flex;
627
+    justify-content: space-between;
628
+    align-items: flex-start;
629
+    margin-bottom: 12px;
630
+
631
+    .card-title {
632
+      display: flex;
633
+      align-items: center;
634
+      gap: 8px;
635
+      flex: 1;
636
+
637
+      .folder-icon {
638
+        color: #409eff;
639
+        font-size: 18px;
640
+      }
641
+
642
+      .title-text {
643
+        font-weight: 600;
644
+        color: #303133;
645
+        font-size: 14px;
646
+        line-height: 1.4;
647
+      }
648
+    }
649
+
650
+    .card-actions {
651
+      opacity: 0;
652
+      transition: opacity 0.3s ease;
653
+    }
654
+  }
655
+
656
+  &:hover .card-actions {
657
+    opacity: 1;
658
+  }
659
+
660
+  .card-content {
661
+    .description {
662
+      color: #606266;
663
+      font-size: 13px;
664
+      line-height: 1.5;
665
+      margin: 0 0 12px 0;
666
+      display: -webkit-box;
667
+      -webkit-line-clamp: 2;
668
+      -webkit-box-orient: vertical;
669
+      overflow: hidden;
670
+    }
671
+
672
+    .meta-info {
673
+      display: flex;
674
+      justify-content: space-between;
675
+      align-items: center;
676
+      font-size: 12px;
677
+      color: #909399;
678
+
679
+      .create-time {
680
+        background: #f0f2f5;
681
+        padding: 2px 6px;
682
+        border-radius: 3px;
683
+      }
684
+
685
+      .file-count {
686
+        background: #e1f3d8;
687
+        color: #67c23a;
688
+        padding: 2px 6px;
689
+        border-radius: 3px;
690
+      }
691
+    }
692
+  }
693
+}
694
+
695
+.file-content {
696
+  flex: 1;
697
+  padding: 20px;
698
+  overflow-y: auto;
699
+}
700
+
701
+.selected-knowledge {
702
+  display: flex;
703
+  align-items: center;
704
+  gap: 8px;
705
+  padding: 12px 16px;
706
+  background: #f0f9ff;
707
+  border-radius: 6px;
708
+  margin-bottom: 20px;
709
+
710
+  .folder-icon {
711
+    color: #409eff;
712
+    font-size: 16px;
713
+  }
714
+
715
+  .knowledge-name {
716
+    font-weight: 600;
717
+    color: #303133;
718
+  }
719
+}
720
+
721
+.file-list {
722
+  display: flex;
723
+  flex-direction: column;
724
+  gap: 12px;
725
+}
726
+
727
+.file-item {
728
+  display: flex;
729
+  justify-content: space-between;
730
+  align-items: center;
731
+  padding: 16px;
732
+  background: #fafbfc;
733
+  border: 1px solid #e4e7ed;
734
+  border-radius: 6px;
735
+  transition: all 0.3s ease;
736
+
737
+  &:hover {
738
+    border-color: #409eff;
739
+    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
740
+  }
741
+
742
+  .file-info {
743
+    display: flex;
744
+    align-items: center;
745
+    gap: 12px;
746
+    flex: 1;
747
+
748
+    .file-icon {
749
+      color: #409eff;
750
+      font-size: 20px;
751
+    }
752
+
753
+    .file-details {
754
+      .file-name {
755
+        font-weight: 500;
756
+        color: #303133;
757
+        margin-bottom: 4px;
758
+      }
759
+
760
+      .file-meta {
761
+        display: flex;
762
+        gap: 12px;
763
+        font-size: 12px;
764
+        color: #909399;
765
+
766
+        .file-type {
767
+          background: #e1f3d8;
768
+          color: #67c23a;
769
+          padding: 2px 6px;
770
+          border-radius: 3px;
771
+        }
772
+      }
773
+    }
774
+  }
775
+
776
+  .file-actions {
777
+    opacity: 0;
778
+    transition: opacity 0.3s ease;
779
+  }
780
+
781
+  &:hover .file-actions {
782
+    opacity: 1;
783
+  }
784
+}
785
+
786
+.empty-state {
787
+  display: flex;
788
+  flex-direction: column;
789
+  align-items: center;
790
+  justify-content: center;
791
+  padding: 60px 20px;
792
+  text-align: center;
793
+  color: #909399;
794
+
795
+  .empty-icon {
796
+    font-size: 48px;
797
+    margin-bottom: 16px;
798
+    opacity: 0.5;
799
+  }
800
+
801
+  p {
802
+    margin: 0 0 16px 0;
803
+    font-size: 14px;
804
+  }
805
+}
806
+
807
+.dialog-footer {
808
+  text-align: right;
809
+}
810
+
811
+.el-upload__tip {
812
+  font-size: 12px;
813
+  color: #606266;
814
+  margin-top: 7px;
815
+}
816
+
817
+:deep(.el-upload-dragger) {
818
+  width: 100%;
819
+}
820
+
821
+:deep(.el-upload__text) {
822
+  margin: 10px 0 16px;
823
+  color: #606266;
824
+  font-size: 14px;
825
+}
826
+
827
+:deep(.el-upload__tip) {
828
+  font-size: 12px;
829
+  color: #606266;
830
+  margin-top: 7px;
831
+}
832
+
833
+// 响应式设计
834
+@media (max-width: 1200px) {
835
+  .main-content {
836
+    flex-direction: column;
837
+  }
838
+
839
+  .left-panel {
840
+    width: 100%;
841
+    height: 300px;
842
+  }
843
+}
844
+
845
+@media (max-width: 768px) {
846
+  .top-toolbar {
847
+    padding: 12px 16px;
848
+  }
849
+
850
+  .main-content {
851
+    padding: 12px;
852
+    gap: 12px;
853
+  }
854
+
855
+  .panel-header {
856
+    padding: 16px 20px;
857
+  }
858
+
859
+  .knowledge-cards {
860
+    padding: 12px;
861
+  }
862
+
863
+  .file-content {
864
+    padding: 16px;
865
+  }
866
+}
867
+</style>

Loading…
Отказ
Запис