ソースを参照

技术标书智能体

lamphua 8時間前
コミット
a481b5f745

+ 6
- 0
oa-back/ruoyi-llm/pom.xml ファイルの表示

@@ -76,6 +76,12 @@
76 76
             <version>3.9.5</version>
77 77
         </dependency>
78 78
 
79
+        <!-- Spring Boot Web -->
80
+        <dependency>
81
+            <groupId>org.springframework.boot</groupId>
82
+            <artifactId>spring-boot-starter-web</artifactId>
83
+        </dependency>
84
+
79 85
     </dependencies>
80 86
 
81 87
 </project>

+ 16
- 2
oa-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/controller/CmcAgentController.java ファイルの表示

@@ -3,9 +3,12 @@ package com.ruoyi.web.llm.controller;
3 3
 import java.io.IOException;
4 4
 import java.util.Date;
5 5
 import java.util.List;
6
+import java.util.concurrent.ExecutionException;
7
+
6 8
 import javax.servlet.http.HttpServletResponse;
7 9
 
8 10
 import org.noear.solon.ai.chat.message.ChatMessage;
11
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
9 12
 import org.springframework.beans.factory.annotation.Autowired;
10 13
 import org.springframework.web.bind.annotation.GetMapping;
11 14
 import org.springframework.web.bind.annotation.PostMapping;
@@ -17,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
17 20
 import org.springframework.web.bind.annotation.RestController;
18 21
 
19 22
 import com.alibaba.fastjson2.JSONObject;
23
+import com.ruoyi.common.annotation.Anonymous;
20 24
 import com.ruoyi.common.annotation.Log;
21 25
 import com.ruoyi.common.core.controller.BaseController;
22 26
 import com.ruoyi.common.core.domain.AjaxResult;
@@ -110,9 +114,19 @@ public class CmcAgentController extends BaseController
110 114
      * @return
111 115
      */
112 116
     @PostMapping("/modifyFile")
113
-    public AjaxResult uploadModifyFile(String topicId, MultipartFile file, String agentName) throws IOException
117
+    public AjaxResult uploadModifyFile(String topicId, MultipartFile file, String agentName, String selectedNode) throws IOException, InterruptedException, ExecutionException
118
+    {
119
+        return success(cmcAgentService.uploadModifyFile(topicId, file, agentName, selectedNode));
120
+    }
121
+
122
+    /**
123
+     * 流式生成章节内容(SSE方式)
124
+     */
125
+    @Anonymous
126
+    @PostMapping(value = "/streamGenerate", produces = "text/event-stream;charset=UTF-8")
127
+    public SseEmitter streamGenerateChapters(String topicId, MultipartFile file, String agentName, String selectedNode) throws IOException
114 128
     {
115
-        return success(cmcAgentService.uploadModifyFile(topicId, file, agentName));
129
+        return cmcAgentService.streamGenerateChapters(topicId, file, agentName, selectedNode);
116 130
     }
117 131
 
118 132
     /**

+ 6
- 0
oa-back/ruoyi-system/pom.xml ファイルの表示

@@ -63,6 +63,12 @@
63 63
             <version>3.9.5</version>
64 64
         </dependency>
65 65
 
66
+        <!-- Spring Boot Web -->
67
+        <dependency>
68
+            <groupId>org.springframework.boot</groupId>
69
+            <artifactId>spring-boot-starter-web</artifactId>
70
+        </dependency>
71
+
66 72
         <dependency>
67 73
             <groupId>io.milvus</groupId>
68 74
             <artifactId>milvus-sdk-java</artifactId>

+ 158
- 0
oa-back/ruoyi-system/src/main/java/com/ruoyi/llm/domain/ChapterStreamResponse.java ファイルの表示

@@ -0,0 +1,158 @@
1
+package com.ruoyi.llm.domain;
2
+
3
+/**
4
+ * 章节流式响应DTO
5
+ */
6
+public class ChapterStreamResponse {
7
+
8
+    /**
9
+     * 章节标题
10
+     */
11
+    private String title;
12
+
13
+    /**
14
+     * 当前章节内容片段
15
+     */
16
+    private String content;
17
+
18
+    /**
19
+     * 是否是当前章节的最后一块内容
20
+     */
21
+    private boolean isLast;
22
+
23
+    /**
24
+     * 当前章节索引(从1开始)
25
+     */
26
+    private int chapterIndex;
27
+
28
+    /**
29
+     * 总章节数
30
+     */
31
+    private int totalChapters;
32
+
33
+    /**
34
+     * 是否发生错误
35
+     */
36
+    private boolean error;
37
+
38
+    /**
39
+     * 错误信息
40
+     */
41
+    private String errorMessage;
42
+
43
+    /**
44
+     * 是否全部完成
45
+     */
46
+    private boolean completed;
47
+
48
+    public ChapterStreamResponse() {
49
+    }
50
+
51
+    public static ChapterStreamResponse content(String title, String content, boolean isLast, int chapterIndex, int totalChapters) {
52
+        ChapterStreamResponse response = new ChapterStreamResponse();
53
+        response.title = title;
54
+        response.content = content;
55
+        response.isLast = isLast;
56
+        response.chapterIndex = chapterIndex;
57
+        response.totalChapters = totalChapters;
58
+        response.error = false;
59
+        response.completed = false;
60
+        return response;
61
+    }
62
+
63
+    public static ChapterStreamResponse error(String title, String errorMessage) {
64
+        ChapterStreamResponse response = new ChapterStreamResponse();
65
+        response.title = title;
66
+        response.content = "该章节内容生成失败,请手动填写。";
67
+        response.isLast = true;
68
+        response.error = true;
69
+        response.errorMessage = errorMessage;
70
+        response.completed = false;
71
+        return response;
72
+    }
73
+
74
+    public static ChapterStreamResponse completed() {
75
+        ChapterStreamResponse response = new ChapterStreamResponse();
76
+        response.completed = true;
77
+        response.isLast = true;
78
+        return response;
79
+    }
80
+
81
+    // Getters and Setters
82
+    public String getTitle() {
83
+        return title;
84
+    }
85
+
86
+    public void setTitle(String title) {
87
+        this.title = title;
88
+    }
89
+
90
+    public String getContent() {
91
+        return content;
92
+    }
93
+
94
+    public void setContent(String content) {
95
+        this.content = content;
96
+    }
97
+
98
+    public boolean isLast() {
99
+        return isLast;
100
+    }
101
+
102
+    public void setLast(boolean last) {
103
+        isLast = last;
104
+    }
105
+
106
+    public int getChapterIndex() {
107
+        return chapterIndex;
108
+    }
109
+
110
+    public void setChapterIndex(int chapterIndex) {
111
+        this.chapterIndex = chapterIndex;
112
+    }
113
+
114
+    public int getTotalChapters() {
115
+        return totalChapters;
116
+    }
117
+
118
+    public void setTotalChapters(int totalChapters) {
119
+        this.totalChapters = totalChapters;
120
+    }
121
+
122
+    public boolean isError() {
123
+        return error;
124
+    }
125
+
126
+    public void setError(boolean error) {
127
+        this.error = error;
128
+    }
129
+
130
+    public String getErrorMessage() {
131
+        return errorMessage;
132
+    }
133
+
134
+    public void setErrorMessage(String errorMessage) {
135
+        this.errorMessage = errorMessage;
136
+    }
137
+
138
+    public boolean isCompleted() {
139
+        return completed;
140
+    }
141
+
142
+    public void setCompleted(boolean completed) {
143
+        this.completed = completed;
144
+    }
145
+
146
+    @Override
147
+    public String toString() {
148
+        return "ChapterStreamResponse{" +
149
+                "title='" + title + '\'' +
150
+                ", content='" + content + '\'' +
151
+                ", isLast=" + isLast +
152
+                ", chapterIndex=" + chapterIndex +
153
+                ", totalChapters=" + totalChapters +
154
+                ", error=" + error +
155
+                ", completed=" + completed +
156
+                '}';
157
+    }
158
+}

+ 18
- 1
oa-back/ruoyi-system/src/main/java/com/ruoyi/llm/service/ICmcAgentService.java ファイルの表示

@@ -2,6 +2,8 @@ package com.ruoyi.llm.service;
2 2
 
3 3
 import com.alibaba.fastjson2.JSONObject;
4 4
 import com.ruoyi.llm.domain.CmcAgent;
5
+
6
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
5 7
 import org.springframework.web.multipart.MultipartFile;
6 8
 
7 9
 import java.io.IOException;
@@ -50,10 +52,13 @@ public interface ICmcAgentService
50 52
     /**
51 53
      * 上传修改文件
52 54
      *
55
+     * @param topicId 话题ID
53 56
      * @param file 文件
57
+     * @param agentName 智能体名称
58
+     * @param selectedNode 选中的节点标题(可选,为空时生成全部章节)
54 59
      * @return 结果
55 60
      */
56
-    public JSONObject uploadModifyFile(String topicId, MultipartFile file, String agentName) throws IOException;
61
+    public JSONObject uploadModifyFile(String topicId, MultipartFile file, String agentName, String selectedNode) throws IOException, InterruptedException, java.util.concurrent.ExecutionException;
57 62
 
58 63
     /**
59 64
      * 上传多文件
@@ -109,4 +114,16 @@ public interface ICmcAgentService
109 114
      * @return 结果
110 115
      */
111 116
     public JSONObject writeTitles(JSONObject data) throws IOException;
117
+
118
+    /**
119
+     * 流式生成章节内容(SSE方式)
120
+     *
121
+     * @param topicId 主题ID
122
+     * @param file 招标文件
123
+     * @param agentName 智能体名称
124
+     * @param selectedNode 选中的节点标题(可选,为空时生成全部章节)
125
+     * @return SseEmitter
126
+     */
127
+    public SseEmitter streamGenerateChapters(String topicId, MultipartFile file, String agentName, String selectedNode) throws IOException;
128
+
112 129
 }

+ 112
- 0
oa-back/ruoyi-system/src/main/java/com/ruoyi/llm/service/MilvusConnectionPool.java ファイルの表示

@@ -0,0 +1,112 @@
1
+package com.ruoyi.llm.service;
2
+
3
+import io.milvus.v2.client.ConnectConfig;
4
+import io.milvus.v2.client.MilvusClientV2;
5
+import io.milvus.v2.service.collection.request.LoadCollectionReq;
6
+import org.springframework.beans.factory.annotation.Value;
7
+import org.springframework.stereotype.Component;
8
+
9
+import javax.annotation.PostConstruct;
10
+import javax.annotation.PreDestroy;
11
+import java.util.*;
12
+import java.util.concurrent.BlockingQueue;
13
+import java.util.concurrent.ConcurrentHashMap;
14
+import java.util.concurrent.LinkedBlockingQueue;
15
+import java.util.concurrent.TimeUnit;
16
+import java.util.concurrent.locks.ReadWriteLock;
17
+import java.util.concurrent.locks.ReentrantReadWriteLock;
18
+
19
+@Component
20
+public class MilvusConnectionPool {
21
+
22
+    @Value("${cmc.milvusService.url}")
23
+    private String milvusServiceUrl;
24
+
25
+    private static final int POOL_SIZE = 5;
26
+    private final List<MilvusClientV2> clientPool = new ArrayList<>();
27
+    private final BlockingQueue<MilvusClientV2> availableClients = new LinkedBlockingQueue<>();
28
+    private final Set<String> loadedCollections = ConcurrentHashMap.newKeySet();
29
+    private final ReadWriteLock collectionLock = new ReentrantReadWriteLock();
30
+    private volatile boolean initialized = false;
31
+
32
+    @PostConstruct
33
+    public void init() {
34
+        if (milvusServiceUrl == null || milvusServiceUrl.isEmpty()) {
35
+            throw new IllegalArgumentException("milvusServiceUrl 配置不能为空");
36
+        }
37
+
38
+        for (int i = 0; i < POOL_SIZE; i++) {
39
+            MilvusClientV2 client = new MilvusClientV2(
40
+                    ConnectConfig.builder()
41
+                            .uri(milvusServiceUrl)
42
+                            .build());
43
+            clientPool.add(client);
44
+            availableClients.offer(client);
45
+        }
46
+        initialized = true;
47
+        System.out.println("Milvus连接池初始化完成,连接数: " + POOL_SIZE);
48
+    }
49
+
50
+    @PreDestroy
51
+    public void destroy() {
52
+        for (MilvusClientV2 client : clientPool) {
53
+            try {
54
+                if (client != null) {
55
+                    client.close();
56
+                }
57
+            } catch (Exception e) {
58
+                System.err.println("关闭Milvus连接时出错: " + e.getMessage());
59
+            }
60
+        }
61
+        clientPool.clear();
62
+        availableClients.clear();
63
+        System.out.println("Milvus连接池已关闭");
64
+    }
65
+
66
+    public MilvusClientV2 getClient() throws InterruptedException {
67
+        MilvusClientV2 client = availableClients.poll(5, TimeUnit.SECONDS);
68
+        if (client == null) {
69
+            throw new RuntimeException("获取Milvus连接超时");
70
+        }
71
+        return client;
72
+    }
73
+
74
+    public void returnClient(MilvusClientV2 client) {
75
+        if (client != null) {
76
+            availableClients.offer(client);
77
+        }
78
+    }
79
+
80
+    public void ensureCollectionLoaded(MilvusClientV2 client, String collectionName) {
81
+        collectionLock.readLock().lock();
82
+        try {
83
+            if (loadedCollections.contains(collectionName)) {
84
+                return;
85
+            }
86
+        } finally {
87
+            collectionLock.readLock().unlock();
88
+        }
89
+
90
+        collectionLock.writeLock().lock();
91
+        try {
92
+            if (!loadedCollections.contains(collectionName)) {
93
+                LoadCollectionReq loadCollectionReq = LoadCollectionReq.builder()
94
+                        .collectionName(collectionName)
95
+                        .build();
96
+                client.loadCollection(loadCollectionReq);
97
+                loadedCollections.add(collectionName);
98
+                System.out.println("集合已加载: " + collectionName);
99
+            }
100
+        } finally {
101
+            collectionLock.writeLock().unlock();
102
+        }
103
+    }
104
+
105
+    public boolean isInitialized() {
106
+        return initialized;
107
+    }
108
+
109
+    public Set<String> getLoadedCollections() {
110
+        return new HashSet<>(loadedCollections);
111
+    }
112
+}

+ 899
- 283
oa-back/ruoyi-system/src/main/java/com/ruoyi/llm/service/impl/CmcAgentServiceImpl.java
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 2
- 2
oa-back/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml ファイルの表示

@@ -145,7 +145,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
145 145
 		</if>
146 146
 		<!-- 数据范围过滤 -->
147 147
 		${params.dataScope}
148
-		order by u.dept_id, u.user_id
148
+		order by u.dept_id, ps.post_level desc, ps.salary_level desc, u.user_id
149 149
 	</select>
150 150
 
151 151
     <select id="selectUserServingList" parameterType="SysUser" resultMap="SysUserResult">
@@ -192,7 +192,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
192 192
 		</if>
193 193
 		<!-- 数据范围过滤 -->
194 194
 		${params.dataScope}
195
-		order by u.dept_id, u.user_id
195
+		order by u.dept_id, ps.post_level desc, ps.salary_level desc, u.user_id
196 196
 	</select>
197 197
 
198 198
 	<select id="selectAllocatedList" parameterType="SysUser" resultMap="SysUserResult">

+ 154
- 5
oa-ui/src/api/llm/agent.js ファイルの表示

@@ -2,9 +2,10 @@
2 2
  * @Author: wrh
3 3
  * @Date: 2025-07-17 18:06:24
4 4
  * @LastEditors: wrh
5
- * @LastEditTime: 2026-04-24 13:52:03
5
+ * @LastEditTime: 2026-05-14 19:52:03
6 6
  */
7 7
 import request from '@/utils/request'
8
+import { getToken } from '@/utils/auth'
8 9
 
9 10
 // 查询智能体列表
10 11
 export function listAgent(query) {
@@ -64,14 +65,14 @@ export function uploadFile(file, agentName) {
64 65
   })
65 66
 }
66 67
 
67
-// 上传文件
68
-export function uploadModifyFile(topicId, file, agentName, chapterNumber) {
68
+// 上传修改文件
69
+export function uploadModifyFile(topicId, file, agentName, selectedNode) {
69 70
   const formData = new FormData()
70 71
   formData.append('topicId', topicId)
71 72
   formData.append('file', file)
72 73
   formData.append('agentName', agentName)
73
-  if (chapterNumber) {
74
-    formData.append('chapterNumber', chapterNumber)
74
+  if (selectedNode) {
75
+    formData.append('selectedNode', selectedNode)
75 76
   }
76 77
   return request({
77 78
     url: '/llm/agent/modifyFile',
@@ -125,3 +126,151 @@ export function writeTitles(data) {
125 126
     data: data
126 127
   })
127 128
 }
129
+
130
+/**
131
+ * 解析SSE事件 - 优化版
132
+ * 正确处理多行data和事件类型
133
+ */
134
+function parseSseEvents(buffer) {
135
+  const events = []
136
+  const lines = buffer.split(/\r?\n/)
137
+  let currentEvent = null
138
+  let currentData = []
139
+  let remaining = []
140
+  let i = 0
141
+  
142
+  for (; i < lines.length; i++) {
143
+    const line = lines[i]
144
+    
145
+    // 空行表示事件结束
146
+    if (line === '') {
147
+      if (currentData.length > 0) {
148
+        const data = currentData.join('\n')
149
+        events.push({
150
+          event: currentEvent || 'message',
151
+          data: data
152
+        })
153
+        currentEvent = null
154
+        currentData = []
155
+      }
156
+      continue
157
+    }
158
+    
159
+    // 解析事件类型
160
+    if (line.startsWith('event:')) {
161
+      currentEvent = line.slice(6).trim()
162
+      continue
163
+    }
164
+    
165
+    // 解析数据行
166
+    if (line.startsWith('data:')) {
167
+      const dataValue = line.slice(5).trimStart()
168
+      currentData.push(dataValue)
169
+      continue
170
+    }
171
+  }
172
+  
173
+  // 保存剩余未完成的数据
174
+  if (i < lines.length) {
175
+    remaining = lines.slice(i).join('\n')
176
+  } else if (currentData.length > 0) {
177
+    // 如果最后没有空行,但数据已完整,也要保留
178
+    remaining = lines.slice(i - currentData.length - (currentEvent ? 1 : 0)).join('\n')
179
+  } else {
180
+    remaining = ''
181
+  }
182
+  
183
+  return { events, rest: remaining }
184
+}
185
+
186
+/**
187
+ * 规范化SSE数据
188
+ */
189
+function normalizeSseData(data) {
190
+  if (data == null) return ''
191
+  let s = String(data)
192
+  // 移除可能的 "data:" 前缀
193
+  while (s.startsWith('data:')) {
194
+    s = s.slice(5)
195
+    if (s.startsWith(' ')) s = s.slice(1)
196
+  }
197
+  return s
198
+}
199
+
200
+/**
201
+ * 流式生成标书章节(SSE)- 支持逐字输出
202
+ * @param {FormData} formData - 包含 topicId, agentName, file, selectedNode
203
+ * @param {Function} onMessage - 收到消息时的回调,接收解析后的JSON对象
204
+ * @param {Function} onError - 错误回调
205
+ * @param {Function} onComplete - 完成回调
206
+ * @returns {AbortController} 用于取消请求的控制器
207
+ */
208
+export function streamGenerateChapters(formData, onMessage, onError, onComplete) {
209
+  const controller = new AbortController()
210
+
211
+  fetch(process.env.VUE_APP_BASE_API + '/llm/agent/streamGenerate', {
212
+    method: 'POST',
213
+    body: formData,
214
+    headers: {
215
+      'Accept': 'text/event-stream',
216
+      'Authorization': 'Bearer ' + getToken(),
217
+      'Cache-Control': 'no-cache'
218
+    },
219
+    signal: controller.signal
220
+  })
221
+    .then(async (response) => {
222
+      if (!response.ok) {
223
+        throw new Error(`HTTP error! status: ${response.status}`)
224
+      }
225
+      if (!response.body) {
226
+        throw new Error('ReadableStream not supported')
227
+      }
228
+
229
+      const reader = response.body.getReader()
230
+      const decoder = new TextDecoder('utf-8')
231
+      let buffer = ''
232
+
233
+      while (true) {
234
+        const { done, value } = await reader.read()
235
+        if (done) break
236
+
237
+        buffer += decoder.decode(value, { stream: true })
238
+        
239
+        // 解析SSE事件
240
+        const { events, rest } = parseSseEvents(buffer)
241
+        buffer = rest
242
+
243
+        for (const event of events) {
244
+          const normalizedData = normalizeSseData(event.data)
245
+          
246
+          // 检查是否结束
247
+          if (normalizedData === '[DONE]') {
248
+            onComplete && onComplete()
249
+            controller.abort()
250
+            return
251
+          }
252
+          
253
+          try {
254
+            const jsonData = JSON.parse(normalizedData)
255
+            // 根据事件名称分发
256
+            onMessage && onMessage(jsonData, event.event)
257
+          } catch (e) {
258
+            console.warn('解析JSON失败,作为普通文本处理:', normalizedData)
259
+            onMessage && onMessage({ content: normalizedData }, event.event)
260
+          }
261
+        }
262
+      }
263
+      
264
+      onComplete && onComplete()
265
+    })
266
+    .catch((error) => {
267
+      if (error.name === 'AbortError') {
268
+        console.log('请求已取消')
269
+        return
270
+      }
271
+      console.error('流式请求错误:', error)
272
+      onError && onError(error)
273
+    })
274
+
275
+  return controller
276
+}

+ 1
- 1
oa-ui/src/api/llm/session.js ファイルの表示

@@ -163,4 +163,4 @@ export function getAnswerWithDocumentStream(params, onMessage, onError, onComple
163 163
   const baseURL = process.env.VUE_APP_BASE_API
164 164
   const url = `${baseURL}/llm/session/answerWithDocument?topicId=${params.topicId}&chatId=${params.chatId}&question=${encodeURIComponent(params.question)}`
165 165
   return streamFetchSse(url, onMessage, onError, onComplete)
166
-}
166
+}

+ 705
- 336
oa-ui/src/views/llm/agent/AgentDetail.vue
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 19
- 8
oa-ui/src/views/llm/agent/index.vue ファイルの表示

@@ -8,10 +8,13 @@
8 8
   <div class="app-container">
9 9
     <div class="main-content">
10 10
       <!-- 左侧智能体列表 -->
11
-      <div class="left-panel">
11
+      <div v-if="showLeftPanel" class="left-panel">
12 12
         <div class="panel-header">
13
-          <h3>智能体列表</h3>
14
-          <span class="agent-count">{{ agentList.length }} 个智能体</span>
13
+          <div class="header-left">
14
+  
15
+            <h3>智能体列表</h3>
16
+            <span class="agent-count">{{ agentList.length }} 个智能体</span>
17
+          </div>
15 18
           <el-button type="primary" size="small" @click="openAddDialog" icon="el-icon-plus">
16 19
             新增智能体
17 20
           </el-button>
@@ -69,8 +72,10 @@
69 72
       </div>
70 73
 
71 74
       <!-- 右侧详细内容 -->
72
-      <div class="agent-detail">
73
-        <AgentDetail :agent-id="selectedAgentId" />
75
+      <div class="right-panel">
76
+        <div class="agent-detail">
77
+          <AgentDetail :agent-id="selectedAgentId" :show-left-panel="showLeftPanel" @toggle-left-panel="showLeftPanel = !showLeftPanel" />
78
+        </div>
74 79
       </div>
75 80
 
76 81
       <el-dialog :visible.sync="agentDialogVisible" :title="dialogTitle" width="500px" @close="resetForm">
@@ -123,6 +128,7 @@ export default {
123 128
       agentList: [],
124 129
       total: 0,
125 130
       selectedAgentId: null,
131
+      showLeftPanel: true,
126 132
       dialogVisible: false,
127 133
       form: {
128 134
         agentName: '',
@@ -310,9 +316,7 @@ export default {
310 316
 }
311 317
 
312 318
 .left-panel {
313
-  width: 25%;
314
-  min-width: 260px;
315
-  max-width: 320px;
319
+  width: 20%;
316 320
   background: white;
317 321
   border-radius: 8px;
318 322
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
@@ -329,6 +333,7 @@ export default {
329 333
   display: flex;
330 334
   flex-direction: column;
331 335
   overflow: hidden;
336
+
332 337
 }
333 338
 
334 339
 .panel-header {
@@ -339,6 +344,12 @@ export default {
339 344
   align-items: center;
340 345
   background: #fafbfc;
341 346
 
347
+  .header-left {
348
+    display: flex;
349
+    align-items: center;
350
+    gap: 8px;
351
+  }
352
+
342 353
   h3 {
343 354
     margin: 0;
344 355
     font-size: 16px;

+ 32
- 5
oa-ui/src/views/llm/knowledge/index.vue ファイルの表示

@@ -3,10 +3,12 @@
3 3
     <!-- 主要内容区域 -->
4 4
     <div class="main-content">
5 5
       <!-- 左侧知识库列表 -->
6
-      <div class="left-panel">
6
+      <div v-if="showLeftPanel" class="left-panel">
7 7
         <div class="panel-header">
8
-          <h3>知识库列表</h3>
9
-          <span class="knowledge-count">{{ knowledgeList.length }} 个知识库</span>
8
+          <div class="header-left">
9
+            <h3>知识库列表</h3>
10
+            <span class="knowledge-count">{{ knowledgeList.length }} 个知识库</span>
11
+          </div>
10 12
           <el-button type="primary" size="small" @click="handleAdd" v-hasPermi="['llm:knowledge:add']"
11 13
             icon="el-icon-plus">
12 14
             新增知识库
@@ -71,7 +73,10 @@
71 73
       <!-- 右侧面板 -->
72 74
       <div class="right-panel">
73 75
         <div class="panel-header">
74
-          <h3>{{ isChatMode ? '知识库对话' : (isScanMode ? '内容列表' : '文件列表') }}</h3>
76
+          <div class="header-left">
77
+            <el-button class="collapse-icon-btn" :icon="showLeftPanel ? 'el-icon-s-fold' : 'el-icon-s-unfold'" @click="showLeftPanel = !showLeftPanel" />
78
+            <h3>{{ isChatMode ? '知识库对话' : (isScanMode ? '内容列表' : '文件列表') }}</h3>
79
+          </div>
75 80
           <div class="header-actions">
76 81
             <el-button v-if="!isChatMode && !isScanMode" type="primary" icon="el-icon-chat-round" @click="handleChat(selectedKnowledge)"
77 82
               :disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:chat']">
@@ -441,6 +446,7 @@ export default {
441 446
   data() {
442 447
     return {
443 448
       knowledgeList: [],
449
+      showLeftPanel: true,
444 450
       open: false,
445 451
       uploadOpen: false,
446 452
       loading: true,
@@ -1277,7 +1283,7 @@ export default {
1277 1283
 }
1278 1284
 
1279 1285
 .left-panel {
1280
-  width: 250px;
1286
+  width: 20%;
1281 1287
   background: white;
1282 1288
   border-radius: 8px;
1283 1289
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
@@ -1304,6 +1310,27 @@ export default {
1304 1310
   align-items: center;
1305 1311
   background: #fafbfc;
1306 1312
 
1313
+  .header-left {
1314
+    display: flex;
1315
+    align-items: center;
1316
+    gap: 8px;
1317
+  }
1318
+
1319
+  .collapse-icon-btn {
1320
+    padding: 0;
1321
+    width: 24px;
1322
+    height: 24px;
1323
+    border: none;
1324
+    background: transparent;
1325
+    color: #6c757d;
1326
+    transition: all 0.3s;
1327
+
1328
+    &:hover {
1329
+      background: #e9ecef;
1330
+      color: #495057;
1331
+    }
1332
+  }
1333
+
1307 1334
   h3 {
1308 1335
     margin: 0;
1309 1336
     font-size: 16px;

読み込み中…
キャンセル
保存