Эх сурвалжийг харах

Merge remote-tracking branch 'origin/master'

1811872455@163.com 1 сар өмнө
parent
commit
cfd2d1349d

+ 110 - 29
java/storlead-knowledge/storlead-knowledge-api/src/main/java/com/storlead/knowledge/api/KnowledgeController.java

@@ -2,10 +2,10 @@ package com.storlead.knowledge.api;
 
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.JsonMappingException;
 import com.storlead.framework.common.result.Result;
 import com.storlead.knowledge.config.DifyProperties;
 import com.storlead.knowledge.entity.AiDatasetsFileEntity;
@@ -18,6 +18,7 @@ import com.storlead.knowledge.pojo.dto.AiTrainingDTO;
 import com.storlead.knowledge.pojo.dto.AiDatasetsFileDTO;
 import com.storlead.knowledge.pojo.vo.AiTrainingVO;
 import com.storlead.knowledge.pojo.vo.AiDatasetsFileVO;
+import com.storlead.knowledge.pojo.vo.KnowledgeStatisticsVO;
 import com.storlead.knowledge.service.AiTrainingService;
 import com.storlead.knowledge.service.AiDatasetsFileService;
 import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -31,7 +32,10 @@ import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
+import java.time.*;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -61,6 +65,106 @@ public class KnowledgeController {
         );
     }
 
+    @GetMapping("statistics")
+    @ApiOperation("获取知识库统计信息(知识库总数、文档总数、文本块总数、本周新增文本块数)")
+    public Result<KnowledgeStatisticsVO> getStatistics() {
+        KnowledgeStatisticsVO statistics = new KnowledgeStatisticsVO();
+
+        // 1. 调用Dify API获取所有知识库列表(使用较大分页以获取全部数据)
+        String url = difyProperties.getBaseUrl() + "datasets";
+        Result<Object> datasetsResult = httpService.get(
+                url,
+                Map.of("page", 1, "size", 1000),
+                "Bearer " + difyProperties.getDatasetApiKey(),
+                new TypeReference<>() {}
+        );
+        //获取一周前的时间
+        Long oneWeekAgo = Instant.now()
+                .minus(7, ChronoUnit.DAYS)
+                .getEpochSecond();
+
+        int totalDatasets = 0;
+        int totalDocuments = 0;
+        int totalChunks = 0;
+        int thisWeekCount = 0 ;
+        if (datasetsResult.isSuccess()) {
+            Object responseData = datasetsResult.getResult();
+            if (responseData instanceof Map) {
+                Map<?, ?> responseMap = (Map<?, ?>) responseData;
+                // 获取总数
+                Object totalObj = responseMap.get("total");
+                if (totalObj instanceof Number) {
+                    totalDatasets = ((Number) totalObj).intValue();
+                }
+                // 遍历数据集,累加文档数和文本块数
+                Object dataObj = responseMap.get("data");
+                if (dataObj instanceof List) {
+                    List<?> dataList = (List<?>) dataObj;
+                    for (Object item : dataList) {
+                        if (item instanceof Map) {
+                            Map<?, ?> datasetMap = (Map<?, ?>) item;
+                            Object docCount = datasetMap.get("document_count");
+                            if (docCount instanceof Number) {
+                                totalDocuments += ((Number) docCount).intValue();
+                            }
+                            String datasets_id = datasetMap.get("id").toString();
+                            //获取知识库中文档的列表
+                            String url1 = difyProperties.getBaseUrl() + "datasets/"+datasets_id+"/documents";
+                            Result<Object> documents_list  = httpService.get(
+                                    url1,
+                                    Map.of("page", 1, "size", 1000),
+                                    "Bearer " + difyProperties.getDatasetApiKey(),
+                                    new TypeReference<>() {}
+                            );
+
+
+                            if (documents_list.isSuccess()) {
+                                Object responseData1 = documents_list.getResult();
+                                Map<?, ?> responseMap2 = (Map<?, ?>) responseData1;
+                                Object dataObj1 = responseMap2.get("data");
+                                if (dataObj1 instanceof List) {
+                                    List<?> dataList1 = (List<?>) dataObj1;
+                                    for (Object item1 : dataList1) {
+                                        if (item1 instanceof Map) {
+                                            Map<?, ?> datasetMap1 = (Map<?, ?>) item1;
+                                            String document_id = datasetMap1.get("id").toString();
+                                            Long create_at = Long.parseLong(datasetMap1.get("created_at") + "");
+                                            String url2 = difyProperties.getBaseUrl() + "datasets/" + datasets_id + "/documents/" + document_id;
+                                            Result<Object> document_detail_result = httpService.get(
+                                                    url2,
+                                                    null,
+                                                    "Bearer " + difyProperties.getDatasetApiKey(),
+                                                    new TypeReference<>() {
+                                                    });
+                                            if (document_detail_result.isSuccess()) {
+                                                Object responseData2 = document_detail_result.getResult();
+                                                Map<?, ?> responseMap3 = (Map<?, ?>) responseData2;
+                                                Object segment_count = responseMap3.get("segment_count");
+                                                if  (segment_count instanceof Number) {
+                                                    totalChunks += Integer.parseInt(segment_count.toString());
+                                                    if (create_at > oneWeekAgo) {
+                                                        thisWeekCount += Integer.parseInt(segment_count.toString()  );
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        statistics.setTotalDatasets(totalDatasets);
+        statistics.setTotalDocuments(totalDocuments);
+        statistics.setTotalChunks(totalChunks);
+        statistics.setThisWeekNewChunks( thisWeekCount);
+
+        return Result.ok(statistics);
+    }
+
     @GetMapping("datasets/{dataset_id}")
     @ApiOperation("获取指定ID的知识库")
     public Result<Object> getDataset(@PathVariable String dataset_id ) {
@@ -148,7 +252,11 @@ public class KnowledgeController {
                         aiDatasetsFileService.updateById(file);
                         if (!"completed".equals(indexing_status)) {
                             flag = false;
-                        }else{
+                            if ("error".equals(indexing_status)) {
+                                vo.setStatus(-1);
+                                aiTrainingService.updateById(vo);
+                            }
+                        }else {
                             vo1.setCompletedDcumentCount(vo1.getCompletedDcumentCount()+1);
                         }
                     }
@@ -269,31 +377,4 @@ public class KnowledgeController {
         return Result.ok(result);
     }
 
-//    @GetMapping("training/file/{id}")
-//    @ApiOperation("根据ID获取AI训练文件")
-//    public Result<AiDatasetsFileEntity> getTrainingFileById(@PathVariable Long id) {
-//        AiDatasetsFileEntity vo = aiDatasetsFileService.getById(id);
-//        return Result.ok(vo);
-//    }
-
-//    @PostMapping("training/file")
-//    @ApiOperation("创建AI训练文件")
-//    public Result<Boolean> createTrainingFile(@Valid @RequestBody AiDatasetsFileEntity dto) {
-//        boolean result = aiDatasetsFileService.save(dto);
-//        return Result.ok(result);
-//    }
-
-//    @PutMapping("training/file")
-//    @ApiOperation("更新AI训练文件")
-//    public Result<Boolean> updateTrainingFile(@Valid @RequestBody AiDatasetsFileEntity dto) {
-//        boolean result = aiDatasetsFileService.updateById(dto);
-//        return Result.ok(result);
-//    }
-
-//    @DeleteMapping("training/file/{id}")
-//    @ApiOperation("删除AI训练文件")
-//    public Result<Boolean> deleteTrainingFile(@PathVariable Long id) {
-//        boolean result = aiDatasetsFileService.removeById(id);
-//        return Result.ok(result);
-//    }
 }

+ 3 - 3
java/storlead-knowledge/storlead-knowledge-biz/src/main/resources/mapper/AiMessageMapper.xml

@@ -35,8 +35,8 @@
     <select id="queryList" resultMap="AiMessageWithReviewMap">
         SELECT
             m.*,
-            nullif(sum(r.agree), 0) AS agree,
-            nullif(sum(r.disagree), 0) AS disagree
+            IFNULL(sum(r.agree), 0) AS agree,
+            IFNULL(sum(r.disagree), 0) AS disagree
         FROM ai_messages m
                  LEFT JOIN ai_messages_review r ON r.ai_message_id = m.id
             ${ew.customSqlSegment}
@@ -45,7 +45,7 @@
 
 
     <select id="getTotal"  resultType="java.lang.Integer">
-        SELECT count(m.id) as total
+        SELECT  count(DISTINCT m.id) as total
         FROM ai_messages m
                  LEFT JOIN ai_messages_review r ON r.ai_message_id = m.id
             ${ew.customSqlSegment}

+ 2 - 1
java/storlead-knowledge/storlead-knowledge-core/src/main/java/com/storlead/knowledge/entity/AiTrainingEntity.java

@@ -30,6 +30,7 @@ public class AiTrainingEntity extends SysBaseField {
 
     @ApiModelProperty(value = "主键ID")
     @TableId(value = "id", type = IdType.AUTO)
+      
     private Long id;
 
     @ApiModelProperty(value = "知识库ID")
@@ -44,7 +45,7 @@ public class AiTrainingEntity extends SysBaseField {
     @TableField("description")
     private String description;
 
-    @ApiModelProperty(value = "状态:0-运行中,1-完成")
+    @ApiModelProperty(value = "状态:-1 错误  0-运行中,1-完成")
     @TableField("status")
     private Integer status;
 }

+ 34 - 0
java/storlead-knowledge/storlead-knowledge-core/src/main/java/com/storlead/knowledge/pojo/vo/KnowledgeStatisticsVO.java

@@ -0,0 +1,34 @@
+package com.storlead.knowledge.pojo.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * 知识库统计视图对象
+ * </p>
+ *
+ * @author storlead
+ * @since 2026-06-09
+ */
+@Data
+@ApiModel(value = "KnowledgeStatisticsVO", description = "知识库统计视图对象")
+public class KnowledgeStatisticsVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "知识库总数")
+    private Integer totalDatasets;
+
+    @ApiModelProperty(value = "文档总数(所有知识库的文档数之和)")
+    private Integer totalDocuments;
+
+    @ApiModelProperty(value = "文本块总数(所有知识库的文本块数之和)")
+    private Integer totalChunks;
+
+    @ApiModelProperty(value = "本周新增文本块数")
+    private Integer thisWeekNewChunks;
+}

+ 14 - 1
ui/smarttrade-platform/src/api/knowledge/index.js

@@ -1,7 +1,20 @@
 import { get, post, put, remove, upload } from '@/utils/request'
 
 export const knowledgeDatasets = (params = {}) => get('/router/rest/knowledge/base/datasets', params)
+
 export const knowledgeDocuments = (params = {}) => {
     const { dataset_id, ...query } = params
     return get(`/router/rest/knowledge/document/${dataset_id}/documents`, params)
-}
+}
+
+export const createKnowledge = (data = {}) => {
+    const { dataset_id, ...query } = data
+    return post(`/router/rest/knowledge/document/${dataset_id}/document/create_by_text`, data)
+}
+export const chatMessages = (params = {}) => get('/router/rest/chat/messages', params)
+
+export const baseTraining = (params = {}) => get('/router/rest/knowledge/base/training/page', params)
+
+export const creatTraining = (data = {}) => post('/router/rest/knowledge/base/training', data)
+
+export const baseStatistics = (params = {}) => get('/router/rest/knowledge/base/statistics', params)

+ 243 - 85
ui/smarttrade-platform/src/views/KnowledgeView.vue

@@ -9,7 +9,7 @@
         <button class="btn btn-secondary" @click="showImportModal = true">
           <i class="fas fa-file-import"></i> 批量导入
         </button>
-        <button class="btn btn-primary" @click="showAddModal = true">
+        <button class="btn btn-primary" @click="addKnowledgeDocument()">
           <i class="fas fa-plus"></i> 添加知识
         </button>
       </div>
@@ -18,40 +18,31 @@
     <!-- Stats -->
     <div class="stats-row">
       <div class="stat-card-lg">
-        <div class="stat-icon-lg blue"><i class="fas fa-book"></i></div>
+        <div class="stat-icon-lg green"><i class="fas fa-check-circle"></i></div>
         <div class="stat-content-lg">
-          <h3>{{ formatNumber(knowledgeStats.total) }}</h3>
-          <p>知识总数</p>
+          <h3>{{ formatNumber(knowledgeStats.totalDatasets) }}</h3>
+          <p>知识总数</p>
         </div>
       </div>
       <div class="stat-card-lg">
         <div class="stat-icon-lg purple"><i class="fas fa-folder"></i></div>
         <div class="stat-content-lg">
-          <h3>{{ knowledgeStats.categories }}</h3>
-          <p>知识分类</p>
+          <h3>{{ knowledgeStats.totalDocuments }}</h3>
+          <p>文档总数</p>
         </div>
       </div>
       <div class="stat-card-lg">
-        <div class="stat-icon-lg green"><i class="fas fa-check-circle"></i></div>
+        <div class="stat-icon-lg blue"><i class="fas fa-book"></i></div>
         <div class="stat-content-lg">
-          <h3>{{ knowledgeStats.coverage }}%</h3>
-          <p>AI 覆盖率</p>
+          <h3>{{ knowledgeStats.totalChunks }}</h3>
+          <p>文本块总数</p>
         </div>
       </div>
       <div class="stat-card-lg">
         <div class="stat-icon-lg orange"><i class="fas fa-chart-line"></i></div>
         <div class="stat-content-lg">
-          <h3>+{{ knowledgeStats.weeklyNew }}</h3>
-          <p>本周新增</p>
-        </div>
-      </div>
-      <div class="stat-card-lg">
-        <div class="stat-icon-lg" style="background: rgba(139, 92, 246, 0.2); color: var(--secondary);">
-          <i class="fas fa-brain"></i>
-        </div>
-        <div class="stat-content-lg">
-          <h3>{{ knowledgeStats.modelVersion }}</h3>
-          <p>模型版本</p>
+          <h3>+{{ knowledgeStats.thisWeekNewChunks }}</h3>
+          <p>本周新增文本块数</p>
         </div>
       </div>
     </div>
@@ -111,7 +102,7 @@
         <div v-for="tab in tabs" :key="tab.key" 
              class="tab-item"
              :class="{ active: activeTab === tab.key }"
-             @click="activeTab = tab.key">
+             @click="changeTab(tab.key)">
           <i :class="tab.icon"></i>
           {{ tab.label }}
           <span class="tab-count">{{ tab.count }}</span>
@@ -159,6 +150,14 @@
           </div>
         </div>
       </div>
+      <div>
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="1000">
+        </el-pagination>
+      </div>
+      
     </div>
 
     <!-- 全部知识 Tab -->
@@ -173,8 +172,8 @@
               <div class="knowledge-question">{{ item.name }}</div>
               <!-- <span class="knowledge-category" :style="{ background: item.iconBg }">{{ item.categoryName }}</span> -->
             </div>
-            <!-- <div class="knowledge-answer">{{ item.answer }}</div>
-            <div class="knowledge-meta">
+            <!-- <div class="knowledge-answer">{{ item.text }}</div> -->
+            <!-- <div class="knowledge-meta">
               <span><i class="fas fa-eye"></i> {{ item.views }}次查看</span>
               <span><i class="fas fa-thumbs-up"></i> {{ item.likes }}次好评</span>
               <span><i class="fas fa-clock"></i> {{ item.time }}</span>
@@ -198,11 +197,11 @@
         <div class="qa-header">
           <h4><i class="fas fa-comments"></i> 智能问答记录</h4>
           <div class="qa-filters">
-            <select v-model="qaFilter" class="filter-select">
+            <!-- <select v-model="qaFilter" class="filter-select">
               <option value="all">全部来源</option>
               <option value="customer">客户问答</option>
               <option value="internal">内部查询</option>
-            </select>
+            </select> -->
             <select v-model="qaRating" class="filter-select">
               <option value="all">全部评价</option>
               <option value="positive">好评</option>
@@ -212,13 +211,13 @@
         </div>
         <div class="qa-list">
           <div v-for="qa in filteredQA" :key="qa.id" class="qa-card">
-            <div class="qa-source">
+            <!-- <div class="qa-source">
               <span class="source-badge" :class="qa.sourceType">{{ qa.sourceLabel }}</span>
-            </div>
+            </div> -->
             <div class="qa-content">
               <div class="qa-question">
                 <span class="qa-label">问</span>
-                {{ qa.question }}
+                {{ qa.query }}
               </div>
               <div class="qa-answer-block">
                 <span class="qa-label answer">答</span>
@@ -229,10 +228,10 @@
               <div class="qa-time">{{ qa.time }}</div>
               <div class="qa-rating">
                 <button class="rating-btn" :class="{ active: qa.rating === 'positive' }" @click="rateQA(qa, 'positive')">
-                  <i class="fas fa-thumbs-up"></i>
+                  <i class="fas fa-thumbs-up"></i><span style="margin-left: 2px;">{{qa.agree}}</span>
                 </button>
                 <button class="rating-btn" :class="{ active: qa.rating === 'negative' }" @click="rateQA(qa, 'negative')">
-                  <i class="fas fa-thumbs-down"></i>
+                  <i class="fas fa-thumbs-down"></i><span style="margin-left: 2px;">{{qa.disagree}}</span>
                 </button>
               </div>
             </div>
@@ -252,13 +251,13 @@
         </div>
         <div class="training-list">
           <div v-for="task in trainingTasks" :key="task.id" class="training-card">
-            <div class="training-icon" :class="task.status">
-              <i :class="task.status === 'running' ? 'fas fa-spinner fa-spin' : task.status === 'completed' ? 'fas fa-check' : 'fas fa-pause'"></i>
+            <div class="training-icon" :class="task.status==0?'running':'completed'">
+              <i :class="task.status === 0 ? 'fas fa-spinner fa-spin' : 'fas fa-check'"></i>
             </div>
             <div class="training-content">
               <div class="training-name">{{ task.name }}</div>
               <div class="training-desc">{{ task.description }}</div>
-              <div class="training-progress" v-if="task.status === 'running'">
+              <div class="training-progress" v-if="task.status === 0">
                 <div class="progress-bar">
                   <div class="progress-fill" :style="{ width: task.progress + '%' }"></div>
                 </div>
@@ -267,16 +266,16 @@
             </div>
             <div class="training-stats">
               <div class="training-stat">
-                <span class="value">{{ task.documents }}</span>
+                <span class="value">{{ task.totalDocumentCount }}</span>
                 <span class="label">文档数</span>
               </div>
               <div class="training-stat">
-                <span class="value">{{ task.learned }}</span>
+                <span class="value">{{ task.completedDcumentCount }}</span>
                 <span class="label">已学习</span>
               </div>
             </div>
-            <div class="training-status" :class="task.status">
-              {{ getTaskStatusLabel(task.status) }}
+            <div class="training-status" :class="task.status==0?'running':'completed'">
+              {{ task.status==0?'训练中':'已完成' }}
             </div>
           </div>
         </div>
@@ -365,29 +364,27 @@
         <div class="modal-body">
           <div class="form-group">
             <label class="form-label">问题/关键词 <span class="required">*</span></label>
-            <input type="text" class="form-input" v-model="newKnowledge.question" placeholder="例如:MOQ最小起订量是多少?">
+            <input type="text" class="form-input" v-model="newKnowledge.name" placeholder="例如:MOQ最小起订量是多少?">
           </div>
           <div class="form-group">
             <label class="form-label">答案/内容 <span class="required">*</span></label>
-            <textarea class="form-textarea" v-model="newKnowledge.answer" rows="4" placeholder="输入详细答案..."></textarea>
+            <textarea class="form-textarea" v-model="newKnowledge.text" rows="4" placeholder="输入详细答案..."></textarea>
           </div>
-          <div class="form-row">
-            <div class="form-group">
-              <label class="form-label">知识分类</label>
-              <select class="form-select" v-model="newKnowledge.category">
-                <option value="">选择分类</option>
-                <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
-              </select>
-            </div>
-            <div class="form-group">
+          <div class="form-group">
+            <label class="form-label">知识分类 <span class="required">*</span></label>
+            <select class="form-select" v-model="newKnowledge.dataset_id">
+              <option value="">选择分类</option>
+              <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
+            </select>
+          </div>
+            <!-- <div class="form-group">
               <label class="form-label">关键词标签</label>
               <input type="text" class="form-input" v-model="newKnowledge.tags" placeholder="用逗号分隔多个标签">
-            </div>
-          </div>
-          <div class="form-group">
+            </div> -->
+          <!-- <div class="form-group">
             <label class="form-label">关联问题(可选)</label>
             <input type="text" class="form-input" v-model="newKnowledge.relatedQuestions" placeholder="例如:起订量,MOQ,最小订单">
-          </div>
+          </div> -->
         </div>
         <div class="modal-footer">
           <button class="btn btn-secondary" @click="showAddModal = false">取消</button>
@@ -462,9 +459,15 @@
         </div>
         <div class="modal-body">
           <div class="import-zone">
-            <div class="import-icon"><i class="fas fa-cloud-upload-alt"></i></div>
-            <div class="import-text">拖拽文件到此处,或点击上传</div>
-            <div class="import-formats">支持 Excel、CSV、JSON、PDF 格式</div>
+            <el-upload
+              class="upload-demo"
+              drag
+              action="https://jsonplaceholder.typicode.com/posts/"
+              multiple>
+              <div class="import-icon"><i class="fas fa-cloud-upload-alt"></i></div>
+              <div class="import-text">拖拽文件到此处,或点击上传</div>
+              <div class="import-formats">支持 Excel、CSV、JSON、PDF 格式</div>
+            </el-upload>
           </div>
           <div class="import-options">
             <div class="import-option">
@@ -486,6 +489,59 @@
       </div>
     </div>
 
+    <!-- 新建训练 -->
+    <div v-if="showTrainModal" class="modal-overlay" @click.self="showTrainModal = false">
+      <div class="modal import-modal">
+        <div class="modal-header">
+          <div class="modal-title"><i class="fas fa-file-import"></i> 新建训练</div>
+          <button class="modal-close" @click="showTrainModal = false"><i class="fas fa-times"></i></button>
+        </div>
+        <div class="modal-body">
+          <div class="form-group">
+            <label class="form-label">知识分类 <span class="required">*</span></label>
+            <select class="form-select" v-model="trainKnowledge.datasetId">
+              <option value="">选择分类</option>
+              <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
+            </select>
+          </div>
+          <div class="form-group">
+            <label class="form-label">标题 <span class="required">*</span></label>
+            <input type="text" class="form-input" maxlength="30" v-model="trainKnowledge.name" placeholder="30字内">
+          </div>
+          <div class="form-group">
+            <label class="form-label">备注信息 <span class="required">*</span></label>
+            <textarea class="form-textarea" v-model="trainKnowledge.describe" rows="4" maxlength="60" placeholder="请输入内容60字内"></textarea>
+          </div>
+          
+          <div class="import-zone">
+            <el-upload
+              class="upload-demo"
+              drag
+              action=""
+              :auto-upload="false"
+              :show-file-list="false"
+              @change="handleFileChange"
+              multiple>
+              <div class="import-icon"><i class="fas fa-cloud-upload-alt"></i></div>
+              <div class="import-text">拖拽文件到此处,或点击上传</div>
+              <div class="import-formats">支持 Excel、CSV、JSON、PDF 格式</div>
+            </el-upload>
+          </div>
+          <div class="import-options">
+            <div class="import-option" v-for="item in fileListRaw" :key="item.name">
+              <i class="fas fa-file-excel"></i>
+              <span>{{ item.name }}</span>
+              <button icon="el-icon-edit"></button>
+            </div>
+          </div>
+        </div>
+        <div class="modal-footer">
+          <button class="btn btn-secondary" @click="showTrainModal = false">取消</button>
+          <button class="btn btn-primary" @click="startTrain"><i class="fas fa-upload"></i>添加</button>
+        </div>
+      </div>
+    </div>
+
     <!-- Toast -->
     <transition name="toast">
       <div v-if="showToast" class="toast" :class="toastType">
@@ -499,8 +555,9 @@
 
 <script setup>
 import { ref, computed } from 'vue'
-import { knowledgeDatasets,knowledgeDocuments } from '../api/knowledge'
+import { knowledgeDatasets,knowledgeDocuments,createKnowledge,chatMessages,baseTraining,creatTraining,baseStatistics } from '../api/knowledge'
 import { formatTimeAgo } from '@/utils/time'
+import { ElMessage } from 'element-plus'
 
 const showAddModal = ref(false)
 const showDetailModal = ref(false)
@@ -516,13 +573,6 @@ const showToast = ref(false)
 const toastMessage = ref('')
 const toastType = ref('success')
 
-const knowledgeStats = {
-  total: 1247,
-  categories: 12,
-  coverage: 95,
-  weeklyNew: 127,
-  modelVersion: 'v2.3'
-}
 
 const syncStatus = {
   products: 234,
@@ -536,15 +586,16 @@ const tabs = ref([
   { key: 'all', label: '全部知识', count: 0, icon: 'fas fa-book' },
   { key: 'qa', label: '问答记录', count: 0, icon: 'fas fa-comments' },
   { key: 'training', label: 'AI 训练', count: 0, icon: 'fas fa-dumbbell' },
-  { key: 'settings', label: '设置', count: 0, icon: 'fas fa-cog' }
+  // { key: 'settings', label: '设置', count: 0, icon: 'fas fa-cog' }
 ])
 
 const newKnowledge = ref({
-  question: '',
-  answer: '',
-  category: '',
-  tags: '',
-  relatedQuestions: ''
+  dataset_id: '',
+  text: '',
+  name: '',
+  indexing_technique: "high_quality",
+  doc_form: "text_model",
+  doc_language:"Chinese Simplified"
 })
 
 // const categories = ref([
@@ -567,20 +618,42 @@ const newKnowledge = ref({
 //   { id: 6, icon: '🏭', iconBg: 'rgba(6, 182, 212, 0.2)', categoryName: '行业知识', question: 'NVMe和SATA有什么区别?', answer: 'NVMe采用PCIe通道,速度更快延迟更低,适合高性能应用。', extendedAnswer: 'NVMe SSD通过PCIe总线直接与CPU通信,理论带宽可达32GB/s,延迟极低。SATA SSD受限于SATA接口,最大速度约600MB/s。对于需要高IOPS的应用场景,NVMe是更好的选择。', tags: ['NVMe', 'SATA', '技术'], relatedQuestions: ['如何选择接口?', '兼容性如何?'], views: 432, likes: 34, time: '5天前' }
 // ])
 
-const qaRecords = ref([
-  { id: 1, sourceType: 'customer', sourceLabel: '客户问答', question: '你们的NVMe SSD支持哪些接口?', answer: '我们提供M.2 2280、M.2 2242和U.2三种接口规格,支持PCIe 4.0 x4总线,向下兼容PCIe 3.0。', time: '10分钟前', rating: 'positive' },
-  { id: 2, sourceType: 'internal', sourceLabel: '内部查询', question: '中东地区的主要竞争对手有哪些?', answer: '中东SSD市场主要竞争者包括Samsung、WD、Kingston以及本地品牌。我们在价格和交期方面有竞争优势。', time: '1小时前', rating: null },
-  { id: 3, sourceType: 'customer', sourceLabel: '客户问答', question: '能否提供样品测试?', answer: '可以提供样品,标准样品免费提供(限2片),定制样品需支付样品费(后续订单可抵扣)。', time: '2小时前', rating: 'positive' },
-  { id: 4, sourceType: 'customer', sourceLabel: '客户问答', question: '产品有CE认证吗?', answer: '所有出口产品均通过CE、FCC、RoHS认证,可提供认证证书副本。', time: '3小时前', rating: 'positive' },
-  { id: 5, sourceType: 'internal', sourceLabel: '内部查询', question: '上周最热门的产品是什么?', answer: '上周销量最高的是NVMe SSD 1TB企业级型号,其次是2TB消费级型号。', time: '昨天', rating: 'negative' }
-])
+// const qaRecords = ref([
+//   { id: 1, sourceType: 'customer', sourceLabel: '客户问答', question: '你们的NVMe SSD支持哪些接口?', answer: '我们提供M.2 2280、M.2 2242和U.2三种接口规格,支持PCIe 4.0 x4总线,向下兼容PCIe 3.0。', time: '10分钟前', rating: 'positive' },
+//   { id: 2, sourceType: 'internal', sourceLabel: '内部查询', question: '中东地区的主要竞争对手有哪些?', answer: '中东SSD市场主要竞争者包括Samsung、WD、Kingston以及本地品牌。我们在价格和交期方面有竞争优势。', time: '1小时前', rating: null },
+//   { id: 3, sourceType: 'customer', sourceLabel: '客户问答', question: '能否提供样品测试?', answer: '可以提供样品,标准样品免费提供(限2片),定制样品需支付样品费(后续订单可抵扣)。', time: '2小时前', rating: 'positive' },
+//   { id: 4, sourceType: 'customer', sourceLabel: '客户问答', question: '产品有CE认证吗?', answer: '所有出口产品均通过CE、FCC、RoHS认证,可提供认证证书副本。', time: '3小时前', rating: 'positive' },
+//   { id: 5, sourceType: 'internal', sourceLabel: '内部查询', question: '上周最热门的产品是什么?', answer: '上周销量最高的是NVMe SSD 1TB企业级型号,其次是2TB消费级型号。', time: '昨天', rating: 'negative' }
+// ])
+
+// const trainingTasks = ref([
+//   { id: 1, name: '新产品知识训练', description: '导入2024新品规格文档进行训练', status: 'running', progress: 68, documents: 45, learned: 31 },
+//   { id: 2, name: '行业报告更新', description: '更新Q1存储行业市场报告数据', status: 'completed', progress: 100, documents: 12, learned: 12 },
+//   { id: 3, name: '客户问答优化', description: '基于历史问答记录优化回复准确性', status: 'paused', progress: 35, documents: 234, learned: 82 }
+// ])
+
+const knowledgeStats = ref({
+  totalDatasets:0,
+  totalDocuments:0,
+  totalChunks:0,
+  thisWeekNewChunks:0,
+})
+// 获取汇总数
+const getBaseStatistics = () => {
+    baseStatistics().then(res => {
+    knowledgeStats.value = res.result
+    console.log('res :>> ', res);
+  })
+}
+getBaseStatistics()
 
-const trainingTasks = ref([
-  { id: 1, name: '新产品知识训练', description: '导入2024新品规格文档进行训练', status: 'running', progress: 68, documents: 45, learned: 31 },
-  { id: 2, name: '行业报告更新', description: '更新Q1存储行业市场报告数据', status: 'completed', progress: 100, documents: 12, learned: 12 },
-  { id: 3, name: '客户问答优化', description: '基于历史问答记录优化回复准确性', status: 'paused', progress: 35, documents: 234, learned: 82 }
-])
 
+const changeTab = (tab)=>{
+  activeTab.value = tab 
+  if(tab =='qa'){
+    getChatMessages()
+  }
+}
 const categories = ref([])
 // 获取知识分类
 const getKnowledgeDatasets = () => {
@@ -613,7 +686,78 @@ const openCategory = (category) => {
   })
 }
 
+const qaRecords = ref([])
+// 获取问答记录
+const chatParams=ref({
+  pageIndex: 1, 
+  pageSize: 10,
+  keyword:'',
+})
+const getChatMessages = () => {
+    chatMessages(chatParams.value).then(res => {
+    tabs.value[2].count = res.result.total
+    res.result.records.forEach(el => {
+      el.time = formatTimeAgo(el.createdAt)
+    });
+    qaRecords.value = res.result.records
+    console.log('res :>> ', res);
+  })
+}
+getChatMessages()
+
+const trainingTasks = ref([])
+//获取ai训练列表
+const trainingParams=ref({
+  pageIndex: 1, 
+  pageSize: 10,
+  keyword:'',
+})
+const getTraining = () => {
+    baseTraining(trainingParams.value).then(res => {
+    tabs.value[3].count = res.result.total
+    trainingTasks.value = res.result.records
+    console.log('res :>> ', res);
+  })
+}
+getTraining()
+
+const fileListRaw = ref([])
+const handleFileChange = (file, fileList) => {
+  // 提取所有原生File对象
+  fileListRaw.value = fileList.map(item => item.raw)
+  console.log('当前选中所有文件:', fileListRaw.value)
+}
+const trainKnowledge = ref({
+  datasetId: '',
+  name: '',
+  describe: '',
+})
+const startTrain = async () => {
+  if (fileListRaw.value.length === 0) {
+    return ElMessage.warning('请先选择文件')
+  }
+  const formData = new FormData()
+  const jsonBody = {
+    datasetsId: trainKnowledge.value.datasetId,
+    name: trainKnowledge.value.name,
+    describe: trainKnowledge.value.describe,
+  }
+  formData.append('data', JSON.stringify(jsonBody))
+  
+  fileListRaw.value.forEach(file => {
+    formData.append('file', file)
+  })
 
+  try {
+    await creatTraining(formData)
+    ElMessage.success('全部文件上传成功')
+    // 清空上传列表、缓存文件
+    uploadRef.value.clearFiles()
+    fileListRaw.value = []
+  } catch (err) {
+    ElMessage.error(err?.message || '上传失败,请重试')
+  }
+}
 
 // const filteredCategories = computed(() => {
 //   if (!searchQuery.value) return categories.value
@@ -642,7 +786,6 @@ const filteredQA = computed(() => {
 
 const formatNumber = (num) => num.toLocaleString()
 
-const getTaskStatusLabel = (status) => ({ running: '训练中', completed: '已完成', paused: '已暂停' }[status] || status)
 
 
 
@@ -660,9 +803,23 @@ const deleteKnowledge = (item) => {
   showToastMessage('知识已删除', 'success')
 }
 
+const addKnowledgeDocument = ()=> {
+  newKnowledge.value.name = ''
+  newKnowledge.value.text=''
+  newKnowledge.value.dataset_id=''
+  showAddModal.value = true
+}
 const addKnowledge = () => {
-  showAddModal.value = false
-  showToastMessage('知识添加成功,AI 已学习', 'success')
+  if(!newKnowledge.value.name||!newKnowledge.value.text||!newKnowledge.value.dataset_id){
+    ElMessage.error('请填入必填项')
+    return false
+  }
+  createKnowledge(newKnowledge.value).then(res => {
+    if(res.success){
+      howToastMessage('知识添加成功,AI 已学习', 'success')
+      showAddModal.value = false
+    }
+  })
 }
 
 const optimizeAnswer = (item) => {
@@ -892,7 +1049,8 @@ const showToastMessage = (message, type = 'success') => {
 .detail-stat i { color: var(--primary); }
 
 /* Import Zone */
-.import-zone { border: 2px dashed var(--border); border-radius: 14px; padding: 40px; text-align: center; margin-bottom: 20px; }
+.import-zone { border: 2px dashed var(--border); border-radius: 14px; padding: 0px; text-align: center; margin-bottom: 20px; }
+.import-zone :deep(.el-upload-dragger){border: none;background: none;}
 .import-icon { font-size: 48px; color: var(--primary); margin-bottom: 12px; }
 .import-text { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
 .import-formats { font-size: 12px; color: var(--text-muted); }