lamphua 1 settimana fa
parent
commit
c9f1b1e2eb

+ 10
- 0
oa-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/service/IMilvusService.java Vedi File

@@ -45,4 +45,14 @@ public interface IMilvusService {
45 45
      * 删除知识库所有文件
46 46
      */
47 47
     public void removeAllDocument(String collectionName);
48
+
49
+    /**
50
+     * 列出所有的title
51
+     */
52
+    public List<String> listTiles(String collectionName);
53
+
54
+    /**
55
+     * 根据title名查询相关content
56
+     */
57
+    public List<String> listByTitle(String collectionName, String title);
48 58
 }

+ 23
- 23
oa-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/service/impl/LangChainMilvusServiceImpl.java Vedi File

@@ -75,7 +75,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
75 75
     private String milvusServiceUrl;
76 76
 
77 77
     private MilvusClientV2 milvusClient;
78
-    
78
+
79 79
     // 复用 ChatModel 实例
80 80
     private ChatModel chatModel;
81 81
 
@@ -84,12 +84,12 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
84 84
         if (milvusServiceUrl == null || milvusServiceUrl.isEmpty()) {
85 85
             throw new IllegalArgumentException("milvusServiceUrl 配置不能为空");
86 86
         }
87
-//        milvusClient = new MilvusClientV2(
88
-//                ConnectConfig.builder()
89
-//                        .uri(milvusServiceUrl)
90
-//                        .build());
87
+        milvusClient = new MilvusClientV2(
88
+                ConnectConfig.builder()
89
+                        .uri(milvusServiceUrl)
90
+                        .build());
91 91
     }
92
-    
92
+
93 93
     @PostConstruct
94 94
     public void initChatModel() {
95 95
         // 初始化 ChatModel
@@ -97,7 +97,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
97 97
                 .model("Qwen")
98 98
                 .build();
99 99
     }
100
-    
100
+
101 101
     @PreDestroy
102 102
     public void destroyMilvusClient() {
103 103
         if (milvusClient != null) {
@@ -137,7 +137,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
137 137
                 // 构建上传目录路径
138 138
                 String uploadDir = RuoYiConfig.getProfile() + "/upload/rag/knowledge/" + collectionName;
139 139
                 File profilePath = new File(uploadDir);
140
-                
140
+
141 141
                 // 确保目录存在,创建目录并设置权限
142 142
                 if (!profilePath.exists())
143 143
                     profilePath.mkdirs();
@@ -491,7 +491,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
491 491
     private String extractAllTitles(File transferFile) throws Exception {
492 492
         StringBuilder titlesBuilder = new StringBuilder();
493 493
         String filename = transferFile.getName().toLowerCase();
494
-        
494
+
495 495
         if (filename.endsWith(".docx")) {
496 496
             try (XWPFDocument document = new XWPFDocument(new FileInputStream(transferFile))) {
497 497
                 for (XWPFParagraph paragraph : document.getParagraphs()) {
@@ -558,7 +558,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
558 558
                                     String[] paragraphs = currentLevel2Content.toString().split("\n");
559 559
                                     for (String para : paragraphs) {
560 560
                                         if (para.trim().isEmpty()) continue;
561
-                                        
561
+
562 562
                                         // 检查是否是三级标题(简单判断:如果是三级标题,样式应该是3,但这里需要重新解析)
563 563
                                         // 由于已经将内容转为字符串,这里采用另一种方式:假设三级标题以数字+点+空格开头(如"1.1.")
564 564
                                         // 或者可以重新遍历段落,但为了效率,这里采用简单的判断方式
@@ -569,7 +569,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
569 569
                                                 break;
570 570
                                             }
571 571
                                         }
572
-                                        
572
+
573 573
                                         if (isLevel3Title) {
574 574
                                             // 保存当前三级标题内容
575 575
                                             if (currentLevel3Content.length() != 0) {
@@ -627,7 +627,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
627 627
                         String[] paragraphs = currentLevel2Content.toString().split("\n");
628 628
                         for (String para : paragraphs) {
629 629
                             if (para.trim().isEmpty()) continue;
630
-                            
630
+
631 631
                             boolean isLevel3Title = false;
632 632
                             for (XWPFParagraph p : xwpfDocument.getParagraphs()) {
633 633
                                 if (p.getText().trim().equals(para.trim()) && p.getStyle() != null && p.getStyle().equals("3")) {
@@ -635,7 +635,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
635 635
                                     break;
636 636
                                 }
637 637
                             }
638
-                            
638
+
639 639
                             if (isLevel3Title) {
640 640
                                 // 保存当前三级标题内容
641 641
                                 if (currentLevel3Content.length() != 0) {
@@ -694,20 +694,20 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
694 694
                                 StringBuilder currentLevel3Content = new StringBuilder();
695 695
                                 Range level2Range = hwpfDocument.getRange();
696 696
                                 boolean foundCurrentLevel2 = false;
697
-                                
697
+
698 698
                                 for (int j = 0; j < level2Range.numParagraphs(); j++) {
699 699
                                     Paragraph p = level2Range.getParagraph(j);
700 700
                                     String paraText = p.text().trim();
701 701
                                     if (paraText.isEmpty()) continue;
702
-                                    
702
+
703 703
                                     // 找到当前二级标题的开始
704 704
                                     if (p.getStyleIndex() == 2 && paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
705 705
                                         foundCurrentLevel2 = true;
706 706
                                     }
707
-                                    
707
+
708 708
                                     if (foundCurrentLevel2) {
709 709
                                         short paraStyleIndex = p.getStyleIndex();
710
-                                        
710
+
711 711
                                         if (paraStyleIndex == 3) {
712 712
                                             // 三级标题
713 713
                                             // 保存当前三级标题内容
@@ -729,7 +729,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
729 729
                                         }
730 730
                                     }
731 731
                                 }
732
-                                
732
+
733 733
                                 // 保存最后一个三级标题内容
734 734
                                 if (currentLevel3Content.length() != 0) {
735 735
                                     TextSegment segment = TextSegment.from(currentLevel3Content.toString());
@@ -762,20 +762,20 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
762 762
                         StringBuilder currentLevel3Content = new StringBuilder();
763 763
                         Range level2Range = hwpfDocument.getRange();
764 764
                         boolean foundCurrentLevel2 = false;
765
-                        
765
+
766 766
                         for (int j = 0; j < level2Range.numParagraphs(); j++) {
767 767
                             Paragraph p = level2Range.getParagraph(j);
768 768
                             String paraText = p.text().trim();
769 769
                             if (paraText.isEmpty()) continue;
770
-                            
770
+
771 771
                             // 找到当前二级标题的开始
772 772
                             if (p.getStyleIndex() == 2 && paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
773 773
                                 foundCurrentLevel2 = true;
774 774
                             }
775
-                            
775
+
776 776
                             if (foundCurrentLevel2) {
777 777
                                 short paraStyleIndex = p.getStyleIndex();
778
-                                
778
+
779 779
                                 if (paraStyleIndex == 3) {
780 780
                                     // 三级标题
781 781
                                     // 保存当前三级标题内容
@@ -797,7 +797,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
797 797
                                 }
798 798
                             }
799 799
                         }
800
-                        
800
+
801 801
                         // 保存最后一个三级标题内容
802 802
                         if (currentLevel3Content.length() != 0) {
803 803
                             TextSegment segment = TextSegment.from(currentLevel3Content.toString());

+ 79
- 13
oa-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/service/impl/MilvusServiceImpl.java Vedi File

@@ -35,12 +35,12 @@ public class MilvusServiceImpl implements IMilvusService {
35 35
         if (milvusServiceUrl == null || milvusServiceUrl.isEmpty()) {
36 36
             throw new IllegalArgumentException("milvusServiceUrl 配置不能为空");
37 37
         }
38
-//        milvusClient = new MilvusClientV2(
39
-//                ConnectConfig.builder()
40
-//                        .uri(milvusServiceUrl)
41
-//                        .build());
38
+        milvusClient = new MilvusClientV2(
39
+                ConnectConfig.builder()
40
+                        .uri(milvusServiceUrl)
41
+                        .build());
42 42
     }
43
-    
43
+
44 44
     @PreDestroy
45 45
     public void destroyMilvusClient() {
46 46
         if (milvusClient != null) {
@@ -76,15 +76,15 @@ public class MilvusServiceImpl implements IMilvusService {
76 76
                 .build());
77 77
 
78 78
         schema.addField(AddFieldReq.builder()
79
-                .fieldName("file_type")
79
+                .fieldName("title")
80 80
                 .dataType(DataType.VarChar)
81
-                .maxLength(10)
81
+                .maxLength(256)
82 82
                 .build());
83 83
 
84 84
         schema.addField(AddFieldReq.builder()
85
-                .fieldName("title")
85
+                .fieldName("file_type")
86 86
                 .dataType(DataType.VarChar)
87
-                .maxLength(256)
87
+                .maxLength(10)
88 88
                 .build());
89 89
 
90 90
         schema.addField(AddFieldReq.builder()
@@ -220,10 +220,10 @@ public class MilvusServiceImpl implements IMilvusService {
220 220
                 .build();
221 221
         if (fileType != null && !fileType.equals(""))
222 222
             queryParam = QueryReq.builder()
223
-                .collectionName(collectionName)
224
-                .filter(String.format("file_type == \"%s\"", fileType))
225
-                .outputFields(Arrays.asList("file_type", "file_name"))
226
-                .build();
223
+                    .collectionName(collectionName)
224
+                    .filter(String.format("file_type == \"%s\"", fileType))
225
+                    .outputFields(Arrays.asList("file_type", "file_name"))
226
+                    .build();
227 227
         QueryResp queryResp = milvusClient.query(queryParam);
228 228
         List<QueryResp.QueryResult> rowRecordList;
229 229
         if (queryResp != null) {
@@ -271,4 +271,70 @@ public class MilvusServiceImpl implements IMilvusService {
271 271
         milvusClient.delete(deleteReq);
272 272
     }
273 273
 
274
+    /**
275
+     * 列出所有的title
276
+     */
277
+    @Override
278
+    public List<String> listTiles(String collectionName) {
279
+        List<String> titleList = new ArrayList<>();
280
+        LoadCollectionReq loadCollectionReq = LoadCollectionReq.builder()
281
+                .collectionName(collectionName)
282
+                .build();
283
+        milvusClient.loadCollection(loadCollectionReq);
284
+        QueryReq queryParam = QueryReq.builder()
285
+                .collectionName(collectionName)
286
+                .filter("id > 0")
287
+                .outputFields(Arrays.asList("title"))
288
+                .build();
289
+        QueryResp queryResp = milvusClient.query(queryParam);
290
+        List<QueryResp.QueryResult> rowRecordList;
291
+        if (queryResp != null) {
292
+            rowRecordList = queryResp.getQueryResults();
293
+            for (QueryResp.QueryResult rowRecord : rowRecordList) {
294
+                Object title = rowRecord.getEntity().get("title");
295
+                if (title != null) {
296
+                    titleList.add(title.toString());
297
+                }
298
+            }
299
+        }
300
+        ReleaseCollectionReq releaseCollectionReq = ReleaseCollectionReq.builder()
301
+                .collectionName(collectionName)
302
+                .build();
303
+        milvusClient.releaseCollection(releaseCollectionReq);
304
+        return titleList.stream().distinct().collect(Collectors.toList());
305
+    }
306
+
307
+    /**
308
+     * 根据title名查询相关content
309
+     */
310
+    @Override
311
+    public List<String> listByTitle(String collectionName, String title) {
312
+        List<String> contentList = new ArrayList<>();
313
+        LoadCollectionReq loadCollectionReq = LoadCollectionReq.builder()
314
+                .collectionName(collectionName)
315
+                .build();
316
+        milvusClient.loadCollection(loadCollectionReq);
317
+        QueryReq queryParam = QueryReq.builder()
318
+                .collectionName(collectionName)
319
+                .filter(String.format("title == \"%s\"", title))
320
+                .outputFields(Arrays.asList("content"))
321
+                .build();
322
+        QueryResp queryResp = milvusClient.query(queryParam);
323
+        List<QueryResp.QueryResult> rowRecordList;
324
+        if (queryResp != null) {
325
+            rowRecordList = queryResp.getQueryResults();
326
+            for (QueryResp.QueryResult rowRecord : rowRecordList) {
327
+                Object content = rowRecord.getEntity().get("content");
328
+                if (content != null) {
329
+                    contentList.add(content.toString());
330
+                }
331
+            }
332
+        }
333
+        ReleaseCollectionReq releaseCollectionReq = ReleaseCollectionReq.builder()
334
+                .collectionName(collectionName)
335
+                .build();
336
+        milvusClient.releaseCollection(releaseCollectionReq);
337
+        return contentList;
338
+    }
339
+
274 340
 }

+ 19
- 1
oa-ui/src/api/llm/knowLedge.js Vedi File

@@ -2,7 +2,7 @@
2 2
  * @Author: ysh
3 3
  * @Date: 2025-06-30 09:56:10
4 4
  * @LastEditors: wrh
5
- * @LastEditTime: 2025-07-18 15:40:00
5
+ * @LastEditTime: 2026-04-17 09:54:52
6 6
  */
7 7
 import request from '@/utils/request'
8 8
 
@@ -92,3 +92,21 @@ export function deleteKnowledgeFile(fileName,collectionName) {
92 92
     params: { fileName, collectionName }
93 93
   })
94 94
 }
95
+
96
+// 列出所有的title
97
+export function listTiles(collectionName) {
98
+  return request({
99
+    url: '/llm/knowledge/listTiles',
100
+    method: 'get',
101
+    params: { collectionName }
102
+  })
103
+}
104
+
105
+// 根据title名查询相关content
106
+export function listByTitle(collectionName, title) {
107
+  return request({
108
+    url: '/llm/knowledge/listByTitle',
109
+    method: 'get',
110
+    params: { collectionName, title }
111
+  })
112
+}

+ 256
- 6
oa-ui/src/views/llm/knowledge/index.vue Vedi File

@@ -71,24 +71,31 @@
71 71
       <!-- 右侧面板 -->
72 72
       <div class="right-panel">
73 73
         <div class="panel-header">
74
-          <h3>{{ isChatMode ? '知识库对话' : '文件列表' }}</h3>
74
+          <h3>{{ isChatMode ? '知识库对话' : (isTitleMode ? '标题列表' : '文件列表') }}</h3>
75 75
           <div class="header-actions">
76
-            <el-button v-if="!isChatMode" type="primary" icon="el-icon-chat-round" @click="handleChat(selectedKnowledge)"
76
+            <el-button v-if="!isChatMode && !isTitleMode" type="primary" icon="el-icon-chat-round" @click="handleChat(selectedKnowledge)"
77 77
               :disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:chat']">
78 78
               开始对话
79 79
             </el-button>
80
-            <el-button v-if="!isChatMode" type="primary" icon="el-icon-upload" @click="handleUpload(selectedKnowledge)"
80
+            <el-button v-if="!isChatMode && !isTitleMode" type="primary" icon="el-icon-upload" @click="handleUpload(selectedKnowledge)"
81 81
               :disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:upload']">
82 82
               上传文件
83 83
             </el-button>
84
+            <el-button v-if="!isChatMode && !isTitleMode" type="default" icon="el-icon-notebook-2" @click="switchToTitleMode"
85
+              :disabled="!selectedKnowledge">
86
+              查看内容
87
+            </el-button>
84 88
             <el-button v-if="isChatMode" type="default" icon="el-icon-document" @click="switchToFileMode">
85 89
               返回文件列表
86 90
             </el-button>
91
+            <el-button v-if="isTitleMode" type="default" icon="el-icon-document" @click="switchToFileMode">
92
+              返回文件列表
93
+            </el-button>
87 94
           </div>
88 95
         </div>
89 96
 
90 97
         <!-- 文件列表模式 -->
91
-        <div v-if="!isChatMode" class="file-content" v-loading="fileLoading">
98
+        <div v-if="!isChatMode && !isTitleMode" class="file-content" v-loading="fileLoading">
92 99
           <div v-if="selectedKnowledge" class="selected-knowledge">
93 100
             <i class="el-icon-folder folder-icon">
94 101
             </i>
@@ -133,6 +140,62 @@
133 140
             @pagination="handleFilePagination" :autoScroll="false" />
134 141
         </div>
135 142
 
143
+        <!-- 标题列表模式 -->
144
+        <div v-if="!isChatMode && isTitleMode" class="title-content" v-loading="titleLoading">
145
+          <div v-if="selectedKnowledge" class="selected-knowledge">
146
+            <i class="el-icon-folder folder-icon">
147
+            </i>
148
+            <span class="knowledge-name">{{ selectedKnowledge.collectionName }}</span>
149
+            <span class="title-mode-badge">标题模式</span>
150
+          </div>
151
+
152
+          <div v-if="!selectedKnowledge" class="empty-state">
153
+            <i class="el-icon-notebook-2 empty-icon">
154
+            </i>
155
+            <p>请选择一个知识库查看内容</p>
156
+          </div>
157
+
158
+          <div v-else-if="titleList.length === 0" class="empty-state">
159
+            <i class="el-icon-notebook-2 empty-icon">
160
+            </i>
161
+            <p>该知识库暂无标题</p>
162
+          </div>
163
+
164
+          <div v-else class="title-content-container">
165
+            <!-- 左侧标题列表 -->
166
+            <div class="title-list">
167
+              <div v-for="(title, index) in titleList" :key="index" class="title-item"
168
+                :class="{ 'active': selectedTitle === title }"
169
+                @click="selectTitle(title)">
170
+                <i class="el-icon-notebook-2 title-icon"></i>
171
+                <span class="title-text">{{ title }}</span>
172
+              </div>
173
+            </div>
174
+
175
+            <!-- 右侧内容列表 -->
176
+            <div class="content-list" v-loading="contentLoading">
177
+              <div v-if="!selectedTitle" class="empty-state">
178
+                <i class="el-icon-document empty-icon"></i>
179
+                <p>请选择一个标题查看内容</p>
180
+              </div>
181
+              <div v-else-if="contentList.length === 0" class="empty-state">
182
+                <i class="el-icon-document empty-icon"></i>
183
+                <p>该标题暂无内容</p>
184
+              </div>
185
+              <div v-else class="content-items">
186
+                <div v-for="(content, index) in contentList" :key="index" class="content-item">
187
+                  <div class="content-header">
188
+                    <span class="content-index">{{ index + 1 }}</span>
189
+                  </div>
190
+                  <div class="content-body">
191
+                    {{ content }}
192
+                  </div>
193
+                </div>
194
+              </div>
195
+            </div>
196
+          </div>
197
+        </div>
198
+
136 199
         <!-- 聊天模式 -->
137 200
         <div v-else class="chat-content">
138 201
           <div v-if="selectedKnowledge" class="selected-knowledge">
@@ -302,7 +365,7 @@
302 365
 import { parseTime } from "@/utils/ruoyi";
303 366
 import { Message } from 'element-ui'
304 367
 import { getToken } from "@/utils/auth";
305
-import { listKnowledge, listKnowLedgeByCollectionName, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile, getProcessValue } from "@/api/llm/knowLedge";
368
+import { listKnowledge, listKnowLedgeByCollectionName, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile, getProcessValue, listTiles, listByTitle } from "@/api/llm/knowLedge";
306 369
 import { getAnswer, getAnswerStream, getContextFile } from '@/api/llm/rag';
307 370
 import { marked } from 'marked';
308 371
 
@@ -451,10 +514,28 @@ export default {
451 514
       },
452 515
 
453 516
       // 定时器
454
-      timer: null
517
+      timer: null,
518
+
519
+      // 标题和内容相关状态
520
+      isTitleMode: false, // 是否显示标题模式
521
+      titleList: [], // 标题列表
522
+      contentList: [], // 内容列表
523
+      selectedTitle: null, // 选中的标题
524
+      titleLoading: false, // 标题加载状态
525
+      contentLoading: false // 内容加载状态
455 526
     }
456 527
   },
457 528
   mounted() {
529
+    // 重置状态,确保页面刷新后不会停留在对话模式或标题模式
530
+    this.isChatMode = false;
531
+    this.isTitleMode = false;
532
+    this.chatMessages = [];
533
+    this.chatInput = '';
534
+    this.streamingStarted = false;
535
+    this.titleList = [];
536
+    this.contentList = [];
537
+    this.selectedTitle = null;
538
+    
458 539
     this.getList();
459 540
   },
460 541
 
@@ -581,6 +662,50 @@ export default {
581 662
       }
582 663
 
583 664
       this.isChatMode = false;
665
+      this.isTitleMode = false;
666
+    },
667
+
668
+    /** 切换到标题模式 */
669
+    switchToTitleMode() {
670
+      this.isTitleMode = true;
671
+      this.isChatMode = false;
672
+      this.titleList = [];
673
+      this.contentList = [];
674
+      this.selectedTitle = null;
675
+      
676
+      // 获取标题列表
677
+      this.titleLoading = true;
678
+      listTiles(this.selectedKnowledge.collectionName).then(response => {
679
+        if (Array.isArray(response.data)) {
680
+          this.titleList = response.data;
681
+        } else {
682
+          this.titleList = [];
683
+        }
684
+        this.titleLoading = false;
685
+      }).catch(error => {
686
+        this.titleLoading = false;
687
+        this.$modal.msgError("获取标题列表失败:" + error.message);
688
+      });
689
+    },
690
+
691
+    /** 选择标题 */
692
+    selectTitle(title) {
693
+      this.selectedTitle = title;
694
+      this.contentList = [];
695
+      
696
+      // 获取内容列表
697
+      this.contentLoading = true;
698
+      listByTitle(this.selectedKnowledge.collectionName, title).then(response => {
699
+        if (Array.isArray(response.data)) {
700
+          this.contentList = response.data;
701
+        } else {
702
+          this.contentList = [];
703
+        }
704
+        this.contentLoading = false;
705
+      }).catch(error => {
706
+        this.contentLoading = false;
707
+        this.$modal.msgError("获取内容列表失败:" + error.message);
708
+      });
584 709
     },
585 710
 
586 711
     /** 发送消息 */
@@ -1282,6 +1407,131 @@ export default {
1282 1407
   overflow-y: auto;
1283 1408
 }
1284 1409
 
1410
+.title-content {
1411
+  flex: 1;
1412
+  padding: 20px;
1413
+  overflow: hidden;
1414
+}
1415
+
1416
+.title-content-container {
1417
+  display: flex;
1418
+  height: calc(100vh - 240px);
1419
+  gap: 20px;
1420
+  overflow: hidden;
1421
+}
1422
+
1423
+.title-list {
1424
+  width: 300px;
1425
+  background: #fafbfc;
1426
+  border-radius: 8px;
1427
+  border: 1px solid #e4e7ed;
1428
+  padding: 12px;
1429
+  overflow-y: auto;
1430
+}
1431
+
1432
+.title-item {
1433
+  display: flex;
1434
+  align-items: center;
1435
+  gap: 8px;
1436
+  padding: 10px 12px;
1437
+  margin-bottom: 8px;
1438
+  border-radius: 6px;
1439
+  cursor: pointer;
1440
+  transition: all 0.3s ease;
1441
+  background: white;
1442
+  border: 1px solid #e4e7ed;
1443
+
1444
+  &:hover {
1445
+    border-color: #409eff;
1446
+    background: #f0f9ff;
1447
+  }
1448
+
1449
+  &.active {
1450
+    border-color: #409eff;
1451
+    background: #e6f7ff;
1452
+    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
1453
+  }
1454
+
1455
+  .title-icon {
1456
+    color: #409eff;
1457
+    font-size: 16px;
1458
+  }
1459
+
1460
+  .title-text {
1461
+    flex: 1;
1462
+    font-size: 14px;
1463
+    color: #303133;
1464
+    line-height: 1.4;
1465
+    overflow: hidden;
1466
+    text-overflow: ellipsis;
1467
+    white-space: nowrap;
1468
+  }
1469
+}
1470
+
1471
+.content-list {
1472
+  flex: 1;
1473
+  background: #fafbfc;
1474
+  border-radius: 8px;
1475
+  border: 1px solid #e4e7ed;
1476
+  padding: 20px;
1477
+  overflow-y: auto;
1478
+}
1479
+
1480
+.content-items {
1481
+  display: flex;
1482
+  flex-direction: column;
1483
+  gap: 16px;
1484
+}
1485
+
1486
+.content-item {
1487
+  background: white;
1488
+  border: 1px solid #e4e7ed;
1489
+  border-radius: 8px;
1490
+  padding: 16px;
1491
+  transition: all 0.3s ease;
1492
+
1493
+  &:hover {
1494
+    border-color: #409eff;
1495
+    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
1496
+  }
1497
+
1498
+  .content-header {
1499
+    display: flex;
1500
+    align-items: center;
1501
+    margin-bottom: 12px;
1502
+
1503
+    .content-index {
1504
+      background: #409eff;
1505
+      color: white;
1506
+      width: 24px;
1507
+      height: 24px;
1508
+      border-radius: 50%;
1509
+      display: flex;
1510
+      align-items: center;
1511
+      justify-content: center;
1512
+      font-size: 12px;
1513
+      font-weight: 600;
1514
+    }
1515
+  }
1516
+
1517
+  .content-body {
1518
+    font-size: 14px;
1519
+    color: #303133;
1520
+    line-height: 1.6;
1521
+    white-space: pre-wrap;
1522
+    word-break: break-word;
1523
+  }
1524
+}
1525
+
1526
+.title-mode-badge {
1527
+  background: #67c23a;
1528
+  color: white;
1529
+  padding: 2px 8px;
1530
+  border-radius: 12px;
1531
+  font-size: 12px;
1532
+  margin-left: auto;
1533
+}
1534
+
1285 1535
 // 聊天模式样式
1286 1536
 .chat-content {
1287 1537
   flex: 1;

Loading…
Annulla
Salva