Merge branch 'dev_NEW_pro' into dev_宁夏_万通新型
# Conflicts:
# multiple/config.json
| | |
| | | yarn-error.log* |
| | | **/*.log |
| | | |
| | | .claude/ |
| | | |
| | | tests/**/coverage/ |
| | | tests/e2e/reports |
| | | selenium-debug.log |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # ç¥è¯åºRAGåéæ£ç´¢åè½å®ç°ææ¡£ |
| | | |
| | | ## ä¸ãåè½æ¦è¿° |
| | | |
| | | åºäº RAGï¼Retrieval-Augmented Generationï¼ææ¯å®ç°ç¥è¯åºé®çåè½ï¼æ¯æï¼ |
| | | - ç¥è¯åºç®¡çï¼CRUDï¼ |
| | | - æä»¶ä¸ä¼ ä¸åéåå¤ç |
| | | - åºäºåéæ£ç´¢çæºè½é®ç |
| | | - å¤ç§æä»¶æ ¼å¼æ¯æï¼txtãmdãdocxãxlsxãxlsãpdfï¼ |
| | | |
| | | ## äºãææ¯æ¶æ |
| | | |
| | | ### 2.1 ææ¯æ |
| | | | ç»ä»¶ | ææ¯ | |
| | | |------|------| |
| | | | åéæ°æ®åº | Pinecone | |
| | | | Embedding模å | é¿éäº DashScope text-embedding-v3 | |
| | | | LLM | é¿éäºéä¹åé® qwen-max | |
| | | | æ¡æ¶ | langchain4j | |
| | | | ORM | MyBatis-Plus | |
| | | |
| | | ### 2.2 æ¶æå¾ |
| | | |
| | | ``` |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â å端åºç¨ â |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â |
| | | â¼ |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â Controller Layer â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | â â KnowledgeBaseCtrl â â KnowledgeChatController â â |
| | | â â (ç¥è¯åºç®¡ç) â â (ç¥è¯åºé®ç) â â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â |
| | | â¼ |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â Service Layer â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | â âKnowledgeBaseService â â KnowledgeRagService â â |
| | | â â (ç¥è¯åºCRUD) â â (åéå/æ£ç´¢) â â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â |
| | | â¼ |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | â AI Layer â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | â â KnowledgeChatAgent â â EmbeddingStore (Pinecone) â â |
| | | â â (é®çAgent) â â (åéåå¨) â â |
| | | â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â |
| | | âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãå端å®ç° |
| | | |
| | | ### 3.1 æ°æ®åºè®¾è®¡ |
| | | |
| | | #### 3.1.1 ç¥è¯åºè¡¨ï¼knowledge_baseï¼ |
| | | |
| | | ```sql |
| | | CREATE TABLE knowledge_base ( |
| | | id BIGINT AUTO_INCREMENT PRIMARY KEY, |
| | | title VARCHAR(255) COMMENT 'ç¥è¯æ é¢', |
| | | type VARCHAR(50) COMMENT 'ç¥è¯ç±»å', |
| | | scenario VARCHAR(255) COMMENT 'éç¨åºæ¯', |
| | | efficiency VARCHAR(20) COMMENT 'è§£å³æç', |
| | | problem TEXT COMMENT 'é®é¢æè¿°', |
| | | solution TEXT COMMENT 'è§£å³æ¹æ¡', |
| | | key_points TEXT COMMENT 'å
³é®è¦ç¹', |
| | | creator VARCHAR(100) COMMENT 'å建人', |
| | | usage_count INT DEFAULT 0 COMMENT 'ä½¿ç¨æ¬¡æ°', |
| | | file_count INT DEFAULT 0 COMMENT 'æä»¶æ°é', |
| | | total_chunk_count INT DEFAULT 0 COMMENT 'æ»åçæ°é', |
| | | description VARCHAR(500) COMMENT 'ç¥è¯åºæè¿°', |
| | | create_time DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | create_user INT, |
| | | update_user INT, |
| | | tenant_id BIGINT, |
| | | dept_id BIGINT |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ç¥è¯åºè¡¨'; |
| | | ``` |
| | | |
| | | #### 3.1.2 ç¥è¯åºåéè®°å½è¡¨ï¼knowledge_base_vectorï¼ |
| | | |
| | | ```sql |
| | | CREATE TABLE knowledge_base_vector ( |
| | | id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主é®ID', |
| | | knowledge_base_id BIGINT NOT NULL COMMENT 'å
³èç¥è¯åºID', |
| | | storage_blob_id BIGINT NOT NULL COMMENT 'å
³èæä»¶blob ID', |
| | | file_name VARCHAR(255) NOT NULL COMMENT 'æä»¶åç§°', |
| | | file_type VARCHAR(50) NOT NULL COMMENT 'æä»¶ç±»å', |
| | | vector_status TINYINT DEFAULT 0 COMMENT 'åéåç¶æ: 0-å¾
å¤ç, 1-å¤çä¸, 2-已宿, 3-失败', |
| | | vector_error VARCHAR(500) COMMENT 'åéå失败åå ', |
| | | chunk_count INT DEFAULT 0 COMMENT 'åçæ°é', |
| | | namespace VARCHAR(100) COMMENT 'åéå½å空é´', |
| | | create_time DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | create_user INT, |
| | | update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | update_user INT, |
| | | tenant_id BIGINT, |
| | | dept_id BIGINT, |
| | | INDEX idx_knowledge_base_id (knowledge_base_id), |
| | | INDEX idx_storage_blob_id (storage_blob_id), |
| | | INDEX idx_vector_status (vector_status) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ç¥è¯åºæä»¶åéè®°å½è¡¨'; |
| | | ``` |
| | | |
| | | ### 3.2 Mavenä¾èµ |
| | | |
| | | ```xml |
| | | <!-- langchain4j BOM --> |
| | | <dependencyManagement> |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>dev.langchain4j</groupId> |
| | | <artifactId>langchain4j-bom</artifactId> |
| | | <version>1.0.0-beta3</version> |
| | | <type>pom</type> |
| | | <scope>import</scope> |
| | | </dependency> |
| | | </dependencies> |
| | | </dependencyManagement> |
| | | |
| | | <dependencies> |
| | | <!-- langchain4j æ ¸å¿ --> |
| | | <dependency> |
| | | <groupId>dev.langchain4j</groupId> |
| | | <artifactId>langchain4j-spring-boot-starter</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Pinecone åéæ°æ®åº --> |
| | | <dependency> |
| | | <groupId>dev.langchain4j</groupId> |
| | | <artifactId>langchain4j-pinecone</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- é¿éäº DashScope --> |
| | | <dependency> |
| | | <groupId>dev.langchain4j</groupId> |
| | | <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId> |
| | | </dependency> |
| | | </dependencies> |
| | | ``` |
| | | |
| | | ### 3.3 é
ç½®æä»¶ï¼application.ymlï¼ |
| | | |
| | | ```yaml |
| | | # Pinecone åéæ°æ®åºé
ç½® |
| | | pinecone: |
| | | api-key: your-pinecone-api-key |
| | | index: your-index-name |
| | | namespace: knowledge-base |
| | | |
| | | # langchain4j é
ç½® |
| | | langchain4j: |
| | | community: |
| | | dashscope: |
| | | streaming-chat-model: |
| | | api-key: your-dashscope-api-key |
| | | model-name: "qwen-max" |
| | | embedding-model: |
| | | api-key: your-dashscope-api-key |
| | | model-name: "text-embedding-v3" |
| | | ``` |
| | | |
| | | ### 3.4 æ ¸å¿ä»£ç å®ç° |
| | | |
| | | #### 3.4.1 å®ä½ç±» |
| | | |
| | | **KnowledgeBase.java** |
| | | ```java |
| | | @Data |
| | | @TableName("knowledge_base") |
| | | public class KnowledgeBase implements Serializable { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private String title; |
| | | private String type; |
| | | private String scenario; |
| | | private String efficiency; |
| | | private String problem; |
| | | private String solution; |
| | | private String keyPoints; |
| | | private String creator; |
| | | private Integer usageCount; |
| | | private Integer fileCount; |
| | | private Integer totalChunkCount; |
| | | private String description; |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private LocalDateTime createTime; |
| | | @TableField(fill = FieldFill.INSERT_UPDATE) |
| | | private LocalDateTime updateTime; |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private Integer createUser; |
| | | @TableField(fill = FieldFill.INSERT_UPDATE) |
| | | private Integer updateUser; |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private Long tenantId; |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private Long deptId; |
| | | } |
| | | ``` |
| | | |
| | | **KnowledgeBaseVector.java** |
| | | ```java |
| | | @Data |
| | | @TableName("knowledge_base_vector") |
| | | public class KnowledgeBaseVector implements Serializable { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private Long knowledgeBaseId; |
| | | private Long storageBlobId; |
| | | private String fileName; |
| | | private String fileType; |
| | | private Integer vectorStatus; |
| | | private String vectorError; |
| | | private Integer chunkCount; |
| | | private String namespace; |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private LocalDateTime createTime; |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private Integer createUser; |
| | | @TableField(fill = FieldFill.INSERT_UPDATE) |
| | | private LocalDateTime updateTime; |
| | | @TableField(fill = FieldFill.INSERT_UPDATE) |
| | | private Integer updateUser; |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private Long tenantId; |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private Long deptId; |
| | | |
| | | // åéåç¶æå¸¸é |
| | | public static final int STATUS_PENDING = 0; |
| | | public static final int STATUS_PROCESSING = 1; |
| | | public static final int STATUS_COMPLETED = 2; |
| | | public static final int STATUS_FAILED = 3; |
| | | } |
| | | ``` |
| | | |
| | | #### 3.4.2 EmbeddingStoreé
ç½® |
| | | |
| | | **EmbeddingStoreConfig.java** |
| | | ```java |
| | | @Configuration |
| | | public class EmbeddingStoreConfig { |
| | | |
| | | @Value("${pinecone.api-key}") |
| | | private String pineconeApiKey; |
| | | |
| | | @Value("${pinecone.index}") |
| | | private String indexName; |
| | | |
| | | @Value("${pinecone.namespace}") |
| | | private String namespace; |
| | | |
| | | @Bean |
| | | public Pinecone pinecone() { |
| | | return new Pinecone.Builder(pineconeApiKey).build(); |
| | | } |
| | | |
| | | @Bean |
| | | public Index pineconeIndex(Pinecone pinecone) { |
| | | return pinecone.getIndexConnection(indexName); |
| | | } |
| | | |
| | | @Bean |
| | | public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) { |
| | | return PineconeEmbeddingStore.builder() |
| | | .apiKey(pineconeApiKey) |
| | | .index(indexName) |
| | | .nameSpace(namespace) |
| | | .createIndex(PineconeServerlessIndexConfig.builder() |
| | | .cloud("AWS") |
| | | .region("us-east-1") |
| | | .dimension(embeddingModel.dimension()) |
| | | .build()) |
| | | .build(); |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | #### 3.4.3 RAGæå¡å®ç° |
| | | |
| | | **KnowledgeRagService.java** |
| | | ```java |
| | | public interface KnowledgeRagService { |
| | | void processVectorAsync(Long vectorId); |
| | | void processVector(Long vectorId); |
| | | List<String> searchRelevantContent(String namespace, String query, int maxResults); |
| | | void deleteEmbeddings(String namespace, Long storageBlobId); |
| | | } |
| | | ``` |
| | | |
| | | **KnowledgeRagServiceImpl.java**ï¼æ ¸å¿å®ç°ï¼ |
| | | ```java |
| | | @Slf4j |
| | | @Service |
| | | public class KnowledgeRagServiceImpl implements KnowledgeRagService { |
| | | |
| | | private final KnowledgeBaseVectorService knowledgeBaseVectorService; |
| | | private final StorageBlobService storageBlobService; |
| | | private final EmbeddingModel embeddingModel; |
| | | private final EmbeddingStore<TextSegment> embeddingStore; |
| | | private final FileProperties fileProperties; |
| | | private final Index pineconeIndex; |
| | | |
| | | @Value("${pinecone.namespace}") |
| | | private String namespace; |
| | | |
| | | private static final int CHUNK_SIZE = 500; |
| | | private static final int CHUNK_OVERLAP = 100; |
| | | private static final long CHUNK_THRESHOLD_BYTES = 80L * 1024 * 1024; |
| | | private static final int EMBEDDING_MAX_LENGTH = 8000; |
| | | |
| | | @Override |
| | | @Async("threadPoolTaskExecutor") |
| | | public void processVectorAsync(Long vectorId) { |
| | | processVector(vectorId); |
| | | } |
| | | |
| | | @Override |
| | | public void processVector(Long vectorId) { |
| | | KnowledgeBaseVector vector = knowledgeBaseVectorService.getById(vectorId); |
| | | if (vector == null) return; |
| | | |
| | | try { |
| | | // æ´æ°ç¶æä¸ºå¤çä¸ |
| | | knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_PROCESSING, null, null); |
| | | |
| | | // è·åæä»¶å
容 |
| | | StorageBlob blob = storageBlobService.getById(vector.getStorageBlobId()); |
| | | File file = getFile(blob); |
| | | String content = extractFileContent(file, vector.getFileName()); |
| | | |
| | | if (content == null || content.trim().isEmpty()) { |
| | | throw new RuntimeException("æä»¶å
容为空"); |
| | | } |
| | | |
| | | // ææ¬åç |
| | | List<TextSegment> chunks; |
| | | boolean needChunk = file.length() > CHUNK_THRESHOLD_BYTES || content.length() > EMBEDDING_MAX_LENGTH; |
| | | if (needChunk) { |
| | | chunks = splitText(content, vector); |
| | | } else { |
| | | Map<String, Object> metadata = buildMetadata(vector); |
| | | chunks = List.of(TextSegment.from(content, new Metadata(metadata))); |
| | | } |
| | | |
| | | // çæåµå
¥åéå¹¶åå¨ |
| | | int chunkCount = 0; |
| | | for (TextSegment chunk : chunks) { |
| | | Embedding embedding = embeddingModel.embed(chunk).content(); |
| | | embeddingStore.add(embedding, chunk); |
| | | chunkCount++; |
| | | } |
| | | |
| | | // æ´æ°ç¶æä¸ºå®æ |
| | | knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_COMPLETED, chunkCount, null); |
| | | |
| | | } catch (Exception e) { |
| | | log.error("åéåå¤ç失败", e); |
| | | knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_FAILED, null, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public List<String> searchRelevantContent(String namespace, String query, int maxResults) { |
| | | Embedding queryEmbedding = embeddingModel.embed(query).content(); |
| | | EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() |
| | | .queryEmbedding(queryEmbedding) |
| | | .maxResults(maxResults) |
| | | .minScore(0.7) |
| | | .build(); |
| | | EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest); |
| | | return searchResult.matches().stream() |
| | | .map(match -> match.embedded().text()) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | @Override |
| | | public void deleteEmbeddings(String namespace, Long storageBlobId) { |
| | | Struct filter = Struct.newBuilder() |
| | | .putFields("storageBlobId", Value.newBuilder() |
| | | .setStructValue(Struct.newBuilder() |
| | | .putFields("$eq", Value.newBuilder() |
| | | .setNumberValue(storageBlobId.doubleValue()) |
| | | .build())) |
| | | .build()) |
| | | .build(); |
| | | pineconeIndex.delete(new ArrayList<>(), false, this.namespace, filter); |
| | | } |
| | | |
| | | private String extractFileContent(File file, String fileName) throws Exception { |
| | | String ext = getFileExtension(fileName); |
| | | if (isPlainText(ext)) { |
| | | return readFileWithEncoding(file); |
| | | } |
| | | if ("docx".equals(ext)) { |
| | | return extractDocx(file); |
| | | } |
| | | if ("xlsx".equals(ext) || "xls".equals(ext)) { |
| | | return extractExcel(file); |
| | | } |
| | | return readFileWithEncoding(file); |
| | | } |
| | | |
| | | // ... å
¶ä»è¾
婿¹æ³ |
| | | } |
| | | ``` |
| | | |
| | | #### 3.4.4 ç¥è¯åºé®çAgent |
| | | |
| | | **KnowledgeChatAgent.java** |
| | | ```java |
| | | @AiService( |
| | | wiringMode = EXPLICIT, |
| | | streamingChatModel = "qwenStreamingChatModel", |
| | | chatMemoryProvider = "chatMemoryProvider" |
| | | ) |
| | | public interface KnowledgeChatAgent { |
| | | |
| | | @SystemMessage(""" |
| | | ä½ æ¯ä¼ä¸ç¥è¯åºé®ç婿ã |
| | | ä½ éè¦åºäºæä¾çç¥è¯åºå
容åçç¨æ·é®é¢ã |
| | | éµå¾ªä»¥ä¸è§åï¼ |
| | | 1. ä¸¥æ ¼åºäºç¥è¯åºå
容åçï¼ä¸è¦ç¼é ä¿¡æ¯ |
| | | 2. 妿ç¥è¯åºä¸æ²¡æç¸å
³ä¿¡æ¯ï¼æç¡®åç¥ç¨æ· |
| | | 3. åçè¦åç¡®ãç®æ´ãææ¡ç |
| | | 4. å¼ç¨æ¥æºæ¶æ³¨æ"æ ¹æ®ç¥è¯åºå
容" |
| | | """) |
| | | Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage); |
| | | } |
| | | ``` |
| | | |
| | | #### 3.4.5 Controllerå± |
| | | |
| | | **KnowledgeBaseController.java** |
| | | ```java |
| | | @RestController |
| | | @RequestMapping("/knowledgeBase") |
| | | @Tag(name = "ç¥è¯åºç®¡ç") |
| | | public class KnowledgeBaseController { |
| | | |
| | | @GetMapping("/getList") |
| | | public AjaxResult getList(@RequestParam(defaultValue = "1") long current, |
| | | @RequestParam(defaultValue = "10") long size, |
| | | KnowledgeBase knowledgeBase) { |
| | | Page page = new Page(current, size); |
| | | return AjaxResult.success(knowledgeBaseService.listpage(page, knowledgeBase)); |
| | | } |
| | | |
| | | @PostMapping("/add") |
| | | public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase) { |
| | | return AjaxResult.success(knowledgeBaseService.save(knowledgeBase)); |
| | | } |
| | | |
| | | @PostMapping("/update") |
| | | public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase) { |
| | | return AjaxResult.success(knowledgeBaseService.updateById(knowledgeBase)); |
| | | } |
| | | |
| | | @DeleteMapping("/delete") |
| | | public AjaxResult delete(@RequestBody List<Long> ids) { |
| | | return AjaxResult.success(knowledgeBaseService.removeByIds(ids)); |
| | | } |
| | | |
| | | @GetMapping("/vector/status/{knowledgeBaseId}") |
| | | @Operation(summary = "æ¥è¯¢ç¥è¯åºæä»¶åéåç¶æ") |
| | | public AjaxResult getVectorStatus(@PathVariable Long knowledgeBaseId) { |
| | | return AjaxResult.success(knowledgeBaseVectorService.getVectorStatusByKnowledgeBaseId(knowledgeBaseId)); |
| | | } |
| | | |
| | | @PostMapping("/vector/reprocess/{vectorId}") |
| | | @Operation(summary = "éæ°åéåæä»¶") |
| | | public AjaxResult reprocessVector(@PathVariable Long vectorId) { |
| | | knowledgeBaseVectorService.reprocessVector(vectorId); |
| | | return AjaxResult.success("已鿰æäº¤åéåä»»å¡"); |
| | | } |
| | | |
| | | @PostMapping("/file/save") |
| | | @Operation(summary = "ä¿åç¥è¯åºæä»¶å
³è") |
| | | public AjaxResult saveKnowledgeBaseFiles(@RequestBody KnowledgeBaseFileDTO dto) { |
| | | // ä¿åéä»¶å
³è并触ååéå |
| | | // ... |
| | | } |
| | | |
| | | @DeleteMapping("/file/delete") |
| | | @Operation(summary = "å é¤ç¥è¯åºæä»¶") |
| | | public AjaxResult deleteKnowledgeBaseFiles(@RequestBody List<Long> vectorIds) { |
| | | knowledgeBaseVectorService.deleteVectors(vectorIds); |
| | | return AjaxResult.success(); |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **KnowledgeChatController.java** |
| | | ```java |
| | | @RestController |
| | | @RequestMapping("/ai/knowledge") |
| | | @Tag(name = "ç¥è¯åºé®ç") |
| | | public class KnowledgeChatController { |
| | | |
| | | private final KnowledgeChatAgent knowledgeChatAgent; |
| | | private final KnowledgeRagService knowledgeRagService; |
| | | private final KnowledgeBaseService knowledgeBaseService; |
| | | |
| | | @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8") |
| | | @Operation(summary = "ç¥è¯åºé®ç") |
| | | public Flux<String> chat(@RequestBody KnowledgeChatRequest request) { |
| | | // æ£ç´¢ç¸å
³å
容 |
| | | String namespace = "kb-" + request.getKnowledgeBaseId(); |
| | | List<String> relevantContents = knowledgeRagService.searchRelevantContent( |
| | | namespace, request.getQuestion(), 5); |
| | | |
| | | if (relevantContents.isEmpty()) { |
| | | return Flux.just("ç¥è¯åºä¸æªæ¾å°ç¸å
³å
容"); |
| | | } |
| | | |
| | | // æå»ºä¸ä¸æ |
| | | StringBuilder context = new StringBuilder(); |
| | | context.append("以䏿¯ä»ç¥è¯åºä¸æ£ç´¢å°çç¸å
³å
容ï¼\n\n"); |
| | | for (int i = 0; i < relevantContents.size(); i++) { |
| | | context.append("ãå
容").append(i + 1).append("ã\n"); |
| | | context.append(relevantContents.get(i)).append("\n\n"); |
| | | } |
| | | context.append("---\n请åºäºä»¥ä¸ç¥è¯åºå
容åçï¼\n").append(request.getQuestion()); |
| | | |
| | | return knowledgeChatAgent.chat(request.getMemoryId(), context.toString()); |
| | | } |
| | | |
| | | @GetMapping("/list") |
| | | @Operation(summary = "ç¥è¯åºå表") |
| | | public AjaxResult listKnowledgeBases() { |
| | | return AjaxResult.success(knowledgeBaseService.list()); |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## åãAPIæ¥å£ææ¡£ |
| | | |
| | | ### 4.1 ç¥è¯åºç®¡çæ¥å£ |
| | | |
| | | | æ¥å£ | æ¹æ³ | è·¯å¾ | 说æ | |
| | | |------|------|------|------| |
| | | | è·åå表 | GET | /knowledgeBase/getList | å页æ¥è¯¢ç¥è¯åºå表 | |
| | | | æ°å¢ç¥è¯åº | POST | /knowledgeBase/add | å建ç¥è¯åº | |
| | | | æ´æ°ç¥è¯åº | POST | /knowledgeBase/update | æ´æ°ç¥è¯åºä¿¡æ¯ | |
| | | | å é¤ç¥è¯åº | DELETE | /knowledgeBase/delete | æ¹éå é¤ç¥è¯åº | |
| | | | æ¥è¯¢åéåç¶æ | GET | /knowledgeBase/vector/status/{id} | æ¥è¯¢æä»¶åéåç¶æ | |
| | | | éæ°åéå | POST | /knowledgeBase/vector/reprocess/{id} | éæ°å¤ç失败çæä»¶ | |
| | | | ä¿åæä»¶å
³è | POST | /knowledgeBase/file/save | ä¸ä¼ æä»¶åå
³èå°ç¥è¯åº | |
| | | | å 餿件 | DELETE | /knowledgeBase/file/delete | å é¤ç¥è¯åºæä»¶ | |
| | | |
| | | ### 4.2 ç¥è¯åºé®çæ¥å£ |
| | | |
| | | | æ¥å£ | æ¹æ³ | è·¯å¾ | 说æ | |
| | | |------|------|------|------| |
| | | | ç¥è¯åºé®ç | POST | /ai/knowledge/chat | æµå¼è¿åé®çç»æ | |
| | | | ç¥è¯åºå表 | GET | /ai/knowledge/list | è·åå¯éç¥è¯åºå表 | |
| | | |
| | | ### 4.3 æ¥å£è¯¦ç»è¯´æ |
| | | |
| | | #### 4.3.1 ä¿åç¥è¯åºæä»¶å
³è |
| | | |
| | | **请æ±** |
| | | ```json |
| | | POST /knowledgeBase/file/save |
| | | { |
| | | "knowledgeBaseId": 1, |
| | | "storageBlobIds": [100, 101, 102] |
| | | } |
| | | ``` |
| | | |
| | | **ååº** |
| | | ```json |
| | | { |
| | | "code": 200, |
| | | "msg": "æä½æå" |
| | | } |
| | | ``` |
| | | |
| | | #### 4.3.2 ç¥è¯åºé®ç |
| | | |
| | | **请æ±** |
| | | ```json |
| | | POST /ai/knowledge/chat |
| | | Content-Type: application/json |
| | | |
| | | { |
| | | "knowledgeBaseId": 1, |
| | | "memoryId": "session-uuid", |
| | | "question": "å¦ä½å¤çåºåçç¹å·®å¼ï¼" |
| | | } |
| | | ``` |
| | | |
| | | **ååº**ï¼SSEæµå¼ï¼ |
| | | ``` |
| | | æ ¹æ®ç¥è¯åºå
容ï¼åºåçç¹å·®å¼çå¤çæµç¨å¦ä¸ï¼ |
| | | |
| | | 1. åç°å·®å¼åï¼é¦å
æ ¸å¯¹çç¹è®°å½... |
| | | 2. æ£æ¥æ¯å¦ææ¼çæéç... |
| | | 3. ... |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## äºãå端å®ç° |
| | | |
| | | ### 5.1 ç¥è¯åºç®¡çé¡µé¢ |
| | | |
| | | ```vue |
| | | <template> |
| | | <div class="knowledge-base"> |
| | | <!-- å表 --> |
| | | <el-table :data="tableData" border> |
| | | <el-table-column prop="title" label="ç¥è¯æ é¢" /> |
| | | <el-table-column prop="type" label="ç¥è¯ç±»å" /> |
| | | <el-table-column prop="fileCount" label="æä»¶æ°é" /> |
| | | <el-table-column prop="totalChunkCount" label="åçæ°é" /> |
| | | <el-table-column label="æä½"> |
| | | <template #default="{ row }"> |
| | | <el-button @click="handleEdit(row)">ç¼è¾</el-button> |
| | | <el-button @click="handleFiles(row)">æä»¶ç®¡ç</el-button> |
| | | <el-button @click="handleChat(row)">é®ç</el-button> |
| | | <el-button type="danger" @click="handleDelete(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from 'vue' |
| | | import { getKnowledgeBaseList, deleteKnowledgeBase } from '@/api/knowledge' |
| | | |
| | | const tableData = ref([]) |
| | | |
| | | const loadData = async () => { |
| | | const res = await getKnowledgeBaseList({ current: 1, size: 10 }) |
| | | tableData.value = res.data.records |
| | | } |
| | | |
| | | onMounted(loadData) |
| | | </script> |
| | | ``` |
| | | |
| | | ### 5.2 æä»¶ä¸ä¼ ä¸åéåç¶æ |
| | | |
| | | ```vue |
| | | <template> |
| | | <div class="file-manager"> |
| | | <!-- æä»¶ä¸ä¼ --> |
| | | <el-upload |
| | | :action="uploadUrl" |
| | | :on-success="handleUploadSuccess" |
| | | multiple |
| | | > |
| | | <el-button type="primary">ä¸ä¼ æä»¶</el-button> |
| | | </el-upload> |
| | | |
| | | <!-- æä»¶å表ä¸åéåç¶æ --> |
| | | <el-table :data="fileList"> |
| | | <el-table-column prop="fileName" label="æä»¶å" /> |
| | | <el-table-column label="åéåç¶æ"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getStatusType(row.vectorStatus)"> |
| | | {{ getStatusText(row.vectorStatus) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="chunkCount" label="åçæ°" /> |
| | | <el-table-column label="æä½"> |
| | | <template #default="{ row }"> |
| | | <el-button v-if="row.vectorStatus === 3" @click="reprocess(row)"> |
| | | éæ°å¤ç |
| | | </el-button> |
| | | <el-button type="danger" @click="deleteFile(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/upload' |
| | | |
| | | // ä¸ä¼ æååä¿åå
³è |
| | | const uploadedBlobIds = ref([]) |
| | | |
| | | const handleUploadSuccess = (response, file) => { |
| | | if (response.code === 200) { |
| | | uploadedBlobIds.value.push(response.data.id) |
| | | } |
| | | } |
| | | |
| | | // ä¿åæä»¶å
³è |
| | | const saveFiles = async () => { |
| | | await saveKnowledgeBaseFiles({ |
| | | knowledgeBaseId: props.knowledgeBaseId, |
| | | storageBlobIds: uploadedBlobIds.value |
| | | }) |
| | | // å·æ°æä»¶å表 |
| | | loadFileList() |
| | | } |
| | | |
| | | // ç¶æææ¬æ å° |
| | | const getStatusText = (status) => { |
| | | const map = { |
| | | 0: 'å¾
å¤ç', |
| | | 1: 'å¤çä¸', |
| | | 2: '已宿', |
| | | 3: '失败' |
| | | } |
| | | return map[status] || 'æªç¥' |
| | | } |
| | | |
| | | const getStatusType = (status) => { |
| | | const map = { |
| | | 0: 'info', |
| | | 1: 'warning', |
| | | 2: 'success', |
| | | 3: 'danger' |
| | | } |
| | | return map[status] || 'info' |
| | | } |
| | | </script> |
| | | ``` |
| | | |
| | | ### 5.3 ç¥è¯åºé®ççé¢ |
| | | |
| | | ```vue |
| | | <template> |
| | | <div class="knowledge-chat"> |
| | | <!-- ç¥è¯åºéæ© --> |
| | | <el-select v-model="selectedKbId" placeholder="éæ©ç¥è¯åº"> |
| | | <el-option |
| | | v-for="kb in knowledgeBases" |
| | | :key="kb.id" |
| | | :label="kb.title" |
| | | :value="kb.id" |
| | | /> |
| | | </el-select> |
| | | |
| | | <!-- 对è¯åºå --> |
| | | <div class="chat-messages"> |
| | | <div |
| | | v-for="(msg, index) in messages" |
| | | :key="index" |
| | | :class="['message', msg.role]" |
| | | > |
| | | <div class="content">{{ msg.content }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è¾å
¥æ¡ --> |
| | | <el-input |
| | | v-model="inputQuestion" |
| | | placeholder="请è¾å
¥é®é¢" |
| | | @keyup.enter="sendMessage" |
| | | > |
| | | <template #append> |
| | | <el-button @click="sendMessage" :loading="loading">åé</el-button> |
| | | </template> |
| | | </el-input> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from 'vue' |
| | | import { getKnowledgeBaseList, knowledgeChat } from '@/api/knowledge' |
| | | |
| | | const knowledgeBases = ref([]) |
| | | const selectedKbId = ref(null) |
| | | const messages = ref([]) |
| | | const inputQuestion = ref('') |
| | | const loading = ref(false) |
| | | const memoryId = ref(crypto.randomUUID()) |
| | | |
| | | const sendMessage = async () => { |
| | | if (!inputQuestion.value.trim()) return |
| | | if (!selectedKbId.value) { |
| | | ElMessage.warning('è¯·éæ©ç¥è¯åº') |
| | | return |
| | | } |
| | | |
| | | // æ·»å ç¨æ·æ¶æ¯ |
| | | messages.value.push({ |
| | | role: 'user', |
| | | content: inputQuestion.value |
| | | }) |
| | | |
| | | loading.value = true |
| | | |
| | | try { |
| | | // æµå¼è¯·æ± |
| | | const response = await fetch('/api/ai/knowledge/chat', { |
| | | method: 'POST', |
| | | headers: { 'Content-Type': 'application/json' }, |
| | | body: JSON.stringify({ |
| | | knowledgeBaseId: selectedKbId.value, |
| | | memoryId: memoryId.value, |
| | | question: inputQuestion.value |
| | | }) |
| | | }) |
| | | |
| | | // å¤çSSEæµå¼ååº |
| | | const reader = response.body.getReader() |
| | | const decoder = new TextDecoder() |
| | | let aiContent = '' |
| | | |
| | | messages.value.push({ role: 'assistant', content: '' }) |
| | | |
| | | while (true) { |
| | | const { done, value } = await reader.read() |
| | | if (done) break |
| | | |
| | | const text = decoder.decode(value) |
| | | aiContent += text |
| | | messages.value[messages.value.length - 1].content = aiContent |
| | | } |
| | | } finally { |
| | | loading.value = false |
| | | inputQuestion.value = '' |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | const res = await getKnowledgeBaseList() |
| | | knowledgeBases.value = res.data |
| | | }) |
| | | </script> |
| | | ``` |
| | | |
| | | ### 5.4 APIå°è£
|
| | | |
| | | ```javascript |
| | | // api/knowledge.js |
| | | import request from '@/utils/request' |
| | | |
| | | // è·åç¥è¯åºå表 |
| | | export function getKnowledgeBaseList(params) { |
| | | return request({ |
| | | url: '/knowledgeBase/getList', |
| | | method: 'get', |
| | | params |
| | | }) |
| | | } |
| | | |
| | | // æ°å¢ç¥è¯åº |
| | | export function addKnowledgeBase(data) { |
| | | return request({ |
| | | url: '/knowledgeBase/add', |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | | // æ´æ°ç¥è¯åº |
| | | export function updateKnowledgeBase(data) { |
| | | return request({ |
| | | url: '/knowledgeBase/update', |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | | // å é¤ç¥è¯åº |
| | | export function deleteKnowledgeBase(ids) { |
| | | return request({ |
| | | url: '/knowledgeBase/delete', |
| | | method: 'delete', |
| | | data: ids |
| | | }) |
| | | } |
| | | |
| | | // è·åæä»¶åéåç¶æ |
| | | export function getVectorStatus(knowledgeBaseId) { |
| | | return request({ |
| | | url: `/knowledgeBase/vector/status/${knowledgeBaseId}`, |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // éæ°åéå |
| | | export function reprocessVector(vectorId) { |
| | | return request({ |
| | | url: `/knowledgeBase/vector/reprocess/${vectorId}`, |
| | | method: 'post' |
| | | }) |
| | | } |
| | | |
| | | // ä¿åæä»¶å
³è |
| | | export function saveKnowledgeBaseFiles(data) { |
| | | return request({ |
| | | url: '/knowledgeBase/file/save', |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | | // å 餿件 |
| | | export function deleteKnowledgeBaseFiles(vectorIds) { |
| | | return request({ |
| | | url: '/knowledgeBase/file/delete', |
| | | method: 'delete', |
| | | data: vectorIds |
| | | }) |
| | | } |
| | | |
| | | // ç¥è¯åºé®çï¼æµå¼ï¼ |
| | | export async function knowledgeChat(data) { |
| | | const response = await fetch('/api/ai/knowledge/chat', { |
| | | method: 'POST', |
| | | headers: { 'Content-Type': 'application/json' }, |
| | | body: JSON.stringify(data) |
| | | }) |
| | | return response.body |
| | | } |
| | | |
| | | // è·åç¥è¯åºå表ï¼é®çç¨ï¼ |
| | | export function getKnowledgeBaseListForChat() { |
| | | return request({ |
| | | url: '/ai/knowledge/list', |
| | | method: 'get' |
| | | }) |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## å
ãæ ¸å¿æµç¨ |
| | | |
| | | ### 6.1 æä»¶ä¸ä¼ ä¸åéåæµç¨ |
| | | |
| | | ``` |
| | | 1. å端è°ç¨ /common/upload ä¸ä¼ æä»¶ â è¿å storageBlobId |
| | | 2. å端è°ç¨ /knowledgeBase/file/save å
³èæä»¶å°ç¥è¯åº |
| | | 3. å端å建 KnowledgeBaseVector è®°å½ï¼ç¶æï¼å¾
å¤çï¼ |
| | | 4. åç«¯å¼æ¥è°ç¨ KnowledgeRagService.processVectorAsync() |
| | | âââ æ´æ°ç¶æä¸º"å¤çä¸" |
| | | âââ æåæä»¶å
å®¹ï¼æ¯æå¤ç§æ ¼å¼ï¼ |
| | | âââ èªå¨æ£æµæä»¶ç¼ç ï¼UTF-8/GBKï¼ |
| | | âââ ææ¬åçï¼å¤§æä»¶æé¿å
容æåçï¼ |
| | | âââ çæ Embedding åé |
| | | âââ åå¨å° Pinecone |
| | | âââ æ´æ°ç¶æä¸º"宿"æ"失败" |
| | | ``` |
| | | |
| | | ### 6.2 ç¥è¯åºé®çæµç¨ |
| | | |
| | | ``` |
| | | 1. ç¨æ·éæ©ç¥è¯åºï¼è¾å
¥é®é¢ |
| | | 2. å端è°ç¨ /ai/knowledge/chatï¼æµå¼æ¥å£ï¼ |
| | | 3. å端å¤çï¼ |
| | | âââ æå»ºå½å空é´ï¼kb-{knowledgeBaseId} |
| | | âââ è°ç¨ Embedding 模åçæé®é¢åé |
| | | âââ ä» Pinecone æ£ç´¢ç¸å
³å
容ï¼minScore=0.7, maxResults=5ï¼ |
| | | âââ æå»ºä¸ä¸æ Prompt |
| | | âââ è°ç¨ LLM çæåç |
| | | âââ æµå¼è¿åç»æ |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¸ã注æäºé¡¹ |
| | | |
| | | 1. **Pinecone å½å空é´**ï¼ä¸è½ä½¿ç¨ `__default__`ï¼å¿
须使ç¨èªå®ä¹å½åç©ºé´ |
| | | 2. **æä»¶ç¼ç **ï¼èªå¨æ£æµ UTF-8/GBKï¼é¿å
ä¹±ç |
| | | 3. **åççç¥**ï¼ |
| | | - æä»¶ > 80MB æå
容 > 8000 åç¬¦æ¶æåç |
| | | - åçå¤§å° 500 å符ï¼éå 100 å符 |
| | | - ä¼å
å¨å¥åè¾¹çåå |
| | | 4. **Embedding éå¶**ï¼é¿éäº DashScope éå¶å次è¾å
¥æå¤§ 8192 å符 |
| | | 5. **åéå é¤**ï¼ä½¿ç¨ Pinecone åç客æ·ç«¯ï¼éè¿ metadata filter å é¤ |
| | | 6. **弿¥å¤ç**ï¼åéåä½¿ç¨ `@Async` 弿¥æ§è¡ï¼é¿å
é»å¡æ¥å£ |
| | | |
| | | --- |
| | | |
| | | ## å
«ãæä»¶æ¸
å |
| | | |
| | | ### å端æä»¶ |
| | | ``` |
| | | src/main/java/com/ruoyi/ |
| | | âââ approve/ |
| | | â âââ controller/ |
| | | â â âââ KnowledgeBaseController.java |
| | | â âââ pojo/ |
| | | â â âââ KnowledgeBase.java |
| | | â â âââ KnowledgeBaseVector.java |
| | | â âââ service/ |
| | | â â âââ KnowledgeBaseService.java |
| | | â â âââ KnowledgeBaseVectorService.java |
| | | â â âââ impl/ |
| | | â â âââ KnowledgeBaseServiceImpl.java |
| | | â â âââ KnowledgeBaseVectorServiceImpl.java |
| | | â âââ mapper/ |
| | | â â âââ KnowledgeBaseMapper.java |
| | | â â âââ KnowledgeBaseVectorMapper.java |
| | | â âââ dto/ |
| | | â âââ KnowledgeBaseVectorVO.java |
| | | âââ ai/ |
| | | âââ config/ |
| | | â âââ EmbeddingStoreConfig.java |
| | | â âââ XiaozhiAgentConfig.java |
| | | âââ controller/ |
| | | â âââ KnowledgeChatController.java |
| | | âââ assistant/ |
| | | â âââ KnowledgeChatAgent.java |
| | | âââ service/ |
| | | â âââ KnowledgeRagService.java |
| | | â âââ impl/ |
| | | â âââ KnowledgeRagServiceImpl.java |
| | | âââ dto/ |
| | | âââ KnowledgeChatRequest.java |
| | | ``` |
| | | |
| | | ### å端æä»¶ |
| | | ``` |
| | | src/views/knowledge/ |
| | | âââ index.vue # ç¥è¯åºå表 |
| | | âââ form.vue # æ°å¢/ç¼è¾è¡¨å |
| | | âââ files.vue # æä»¶ç®¡ç |
| | | âââ chat.vue # ç¥è¯åºé®ç |
| | | |
| | | src/api/knowledge.js # APIå°è£
|
| | | ``` |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # ç¥è¯åºæ¨¡åä¼ åæ¹å¼ååæ°å½åè§èææ¡£ |
| | | |
| | | ## ä¸ãæ¦è¿° |
| | | |
| | | æ¬ææ¡£è¯¦ç»è¯´æç¥è¯åºæ¨¡åä¸æææ¥å£ãç»ä»¶ãæ¹æ³çä¼ åæ¹å¼ååæ°å½åè§è,æ¨å¨: |
| | | - ç»ä¸ååç«¯åæ°å½åè§è |
| | | - æç¡®åæ°ç±»ååå¿
å¡«æ§ |
| | | - è§èä¼ åæ¹å¼(GET paramsãPOST bodyãDELETE data) |
| | | - æä¾æ¸
æ°çåæ°æ å°å
³ç³» |
| | | |
| | | --- |
| | | |
| | | ## äºãåæ°å½åè§è |
| | | |
| | | ### 2.1 åºæ¬è§è |
| | | |
| | | #### å½å飿 ¼ |
| | | - **åç«¯åæ°**: éµå¾ª Java 驼峰å½åæ³ (camelCase) |
| | | - **åç«¯åæ°**: éµå¾ª JavaScript 驼峰å½åæ³ (camelCase) |
| | | - **æ°æ®åºå段**: éµå¾ª MySQL ä¸å线å½åæ³ (snake_case) |
| | | - **æ¥å£URL**: éµå¾ª RESTful 飿 ¼,使ç¨å°ååè¿å符 |
| | | |
| | | #### å½åçº¦å® |
| | | 1. **IDç¸å
³**: ç»ä¸ä½¿ç¨ `id`ã`Id` åç¼ |
| | | - `knowledgeBaseId` - ç¥è¯åºID |
| | | - `storageBlobId` - æä»¶blob ID |
| | | - `vectorId` - åéè®°å½ID |
| | | - `memoryId` - ä¼è¯ID |
| | | |
| | | 2. **å表ç¸å
³**: ç»ä¸ä½¿ç¨ `Ids` åç¼ææ°ç»ç±»å |
| | | - `storageBlobIds` - æä»¶blob IDå表 |
| | | - `ids` - éç¨IDå表 |
| | | |
| | | 3. **ç¶æç¸å
³**: ç»ä¸ä½¿ç¨ `Status` åç¼ |
| | | - `vectorStatus` - åéåç¶æ |
| | | |
| | | 4. **æ°éç¸å
³**: ç»ä¸ä½¿ç¨ `Count` åç¼ |
| | | - `fileCount` - æä»¶æ°é |
| | | - `chunkCount` - åçæ°é |
| | | - `totalChunkCount` - æ»åçæ°é |
| | | - `usageCount` - ä½¿ç¨æ¬¡æ° |
| | | |
| | | 5. **æ¶é´ç¸å
³**: ç»ä¸ä½¿ç¨ `Time` åç¼ |
| | | - `createTime` - å建æ¶é´ |
| | | - `updateTime` - æ´æ°æ¶é´ |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãæ¥å£ä¼ åæ¹å¼è§è |
| | | |
| | | ### 3.1 GET è¯·æ± - ä½¿ç¨ params |
| | | |
| | | **éç¨åºæ¯**: æ¥è¯¢ãå表ãå页çè·åæ°æ®çæ¥å£ |
| | | |
| | | **ä¼ åæ¹å¼**: éè¿ URL åæ°ä¼ é,ä½¿ç¨ `params` |
| | | |
| | | **示ä¾**: |
| | | ```javascript |
| | | // æ¥è¯¢ç¥è¯åºå表 |
| | | export function listKnowledgeBase(query) { |
| | | return request({ |
| | | url: "/knowledgeBase/getList", |
| | | method: "get", |
| | | params: query, // â
GET请æ±ä½¿ç¨ params |
| | | }); |
| | | } |
| | | |
| | | // å®é
è°ç¨ |
| | | listKnowledgeBase({ |
| | | current: 1, // å½å页ç |
| | | size: 20, // æ¯é¡µæ¡æ° |
| | | title: "", // ç¥è¯æ é¢(å¯é) |
| | | type: "" // ç¥è¯ç±»å(å¯é) |
| | | }); |
| | | ``` |
| | | |
| | | **URLæ ¼å¼**: `/knowledgeBase/getList?current=1&size=20&title=&type=` |
| | | |
| | | **è§èè¦ç¹**: |
| | | - â
æ¥è¯¢åæ°ç»ä¸æ¾å¨ `params` ä¸ |
| | | - â
å页忰å½å: `current` (å½å页)ã`size` (æ¯é¡µæ¡æ°) |
| | | - â
æç´¢åæ°å½å: ä¸å®ä½åæ®µä¿æä¸è´ |
| | | - â
è·¯å¾åæ°ä½¿ç¨ URL å ä½ç¬¦: `/path/{id}` |
| | | |
| | | --- |
| | | |
| | | ### 3.2 POST è¯·æ± - ä½¿ç¨ data |
| | | |
| | | **éç¨åºæ¯**: æ°å¢ãæ´æ°ãä¿åçæäº¤æ°æ®çæ¥å£ |
| | | |
| | | **ä¼ åæ¹å¼**: éè¿è¯·æ±ä½ä¼ é,ä½¿ç¨ `data` |
| | | |
| | | **示ä¾**: |
| | | ```javascript |
| | | // æ°å¢ç¥è¯åº |
| | | export function addKnowledgeBase(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/add", |
| | | method: "post", |
| | | data: data, // â
POST请æ±ä½¿ç¨ data |
| | | }); |
| | | } |
| | | |
| | | // å®é
è°ç¨ |
| | | addKnowledgeBase({ |
| | | title: "æä½æå", |
| | | type: "guide", |
| | | scenario: "ç³»ç»æä½æå¯¼", |
| | | efficiency: "high", |
| | | problem: "ç¨æ·ä¸ä¼æä½ç³»ç»", |
| | | solution: "æç
§æä½æåæ§è¡...", |
| | | keyPoints: "æ¥éª¤1,æ¥éª¤2,æ¥éª¤3", |
| | | creator: "å¼ ä¸", |
| | | usageCount: 0 |
| | | }); |
| | | ``` |
| | | |
| | | **请æ±ä½æ ¼å¼**: JSON æ ¼å¼,`Content-Type: application/json` |
| | | |
| | | **è§èè¦ç¹**: |
| | | - â
æäº¤æ°æ®ç»ä¸æ¾å¨ `data` ä¸ |
| | | - â
åæ°åä¸å端å®ä½åæ®µä¿æä¸è´ |
| | | - â
å¿
å¡«åæ°éè¦å¨è¡¨åéªè¯è§åä¸å£°æ |
| | | - â
æ°å¼ç±»ååæ°éæå®é»è®¤å¼ |
| | | |
| | | --- |
| | | |
| | | ### 3.3 DELETE è¯·æ± - ä½¿ç¨ data |
| | | |
| | | **éç¨åºæ¯**: å é¤ãæ¹éå é¤çæä½ |
| | | |
| | | **ä¼ åæ¹å¼**: éè¿è¯·æ±ä½ä¼ éæ°ç»æå¯¹è±¡,ä½¿ç¨ `data` |
| | | |
| | | **示ä¾**: |
| | | ```javascript |
| | | // å é¤ç¥è¯åº |
| | | export function delKnowledgeBase(query) { |
| | | return request({ |
| | | url: "/knowledgeBase/delete", |
| | | method: "delete", |
| | | data: query, // â
DELETE请æ±ä½¿ç¨ data ä¼ éæ°ç» |
| | | }); |
| | | } |
| | | |
| | | // å®é
è°ç¨(æ¹éå é¤) |
| | | delKnowledgeBase([1, 2, 3]); // â
ç´æ¥ä¼ éIDæ°ç» |
| | | ``` |
| | | |
| | | **请æ±ä½æ ¼å¼**: JSON æ°ç» `[1, 2, 3]` |
| | | |
| | | **è§èè¦ç¹**: |
| | | - â
DELETE请æ±çåæ°æ¾å¨ `data` ä¸ |
| | | - â
æ¹éå é¤ä¼ éIDæ°ç» |
| | | - â
å个å é¤ä¹å¯ä»¥ä¼ éæ°ç» `[id]` |
| | | - â ï¸ ä¸è¦ä½¿ç¨ `params` ä¼ éå é¤åæ° |
| | | |
| | | --- |
| | | |
| | | ### 3.4 æµå¼è¯·æ± - ä½¿ç¨ Fetch API |
| | | |
| | | **éç¨åºæ¯**: AIé®çãæµå¼è¾åºçéè¦å®æ¶ååºçæ¥å£ |
| | | |
| | | **ä¼ åæ¹å¼**: 使ç¨åç Fetch API,䏿¯æ axios |
| | | |
| | | **示ä¾**: |
| | | ```javascript |
| | | // ç¥è¯åºé®ç(æµå¼) |
| | | export function knowledgeChat(data) { |
| | | const token = getToken(); |
| | | return fetch(import.meta.env.VITE_APP_BASE_API + '/ai/knowledge/chat', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'Authorization': 'Bearer ' + token |
| | | }, |
| | | body: JSON.stringify(data) // â
ä½¿ç¨ body ä¼ éåæ° |
| | | }); |
| | | } |
| | | |
| | | // å®é
è°ç¨ |
| | | knowledgeChat({ |
| | | knowledgeBaseId: 10, |
| | | memoryId: "session-xxx", |
| | | question: "å¦ä½æä½å®¡æ¹æµç¨?" |
| | | }); |
| | | ``` |
| | | |
| | | **è§èè¦ç¹**: |
| | | - â
æµå¼æ¥å£å¿
é¡»ä½¿ç¨ Fetch API |
| | | - â
åæ°ä½¿ç¨ `JSON.stringify()` åºåå |
| | | - â
å¿
é¡»æºå¸¦ Authorization header |
| | | - â ï¸ axios 䏿¯ææµå¼ååº,ä¸è¦ä½¿ç¨ |
| | | |
| | | --- |
| | | |
| | | ## åã宿´åæ°å¯¹ç
§è¡¨ |
| | | |
| | | ### 4.1 ç¥è¯åºç®¡çæ¥å£åæ° |
| | | |
| | | #### æ¥è¯¢å表 (`GET /knowledgeBase/getList`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | current | Integer | æ¯ | params | å½å页ç | 1 | |
| | | | size | Integer | æ¯ | params | æ¯é¡µæ¡æ° | 20 | |
| | | | title | String | å¦ | params | ç¥è¯æ é¢(æ¨¡ç³æç´¢) | "æä½" | |
| | | | type | String | å¦ | params | ç¥è¯ç±»å(精确å¹é
) | "guide" | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | listKnowledgeBase({ |
| | | current: 1, |
| | | size: 20, |
| | | title: "", |
| | | type: "" |
| | | }); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### æ°å¢ç¥è¯åº (`POST /knowledgeBase/add`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | title | String | æ¯ | data | ç¥è¯æ é¢ | "æä½æå" | |
| | | | type | String | æ¯ | data | ç¥è¯ç±»å | "guide" | |
| | | | scenario | String | å¦ | data | éç¨åºæ¯ | "ç³»ç»æä½" | |
| | | | efficiency | String | å¦ | data | è§£å³æç | "high" | |
| | | | problem | String | æ¯ | data | é®é¢æè¿° | "ç¨æ·ä¸ä¼æä½" | |
| | | | solution | String | æ¯ | data | è§£å³æ¹æ¡ | "åèæå" | |
| | | | keyPoints | String | å¦ | data | å
³é®è¦ç¹ | "æ¥éª¤1,æ¥éª¤2" | |
| | | | creator | String | å¦ | data | å建人 | "å¼ ä¸" | |
| | | | usageCount | Integer | å¦ | data | ä½¿ç¨æ¬¡æ° | 0 | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | addKnowledgeBase({ |
| | | title: "æä½æå", |
| | | type: "guide", |
| | | scenario: "ç³»ç»æä½æå¯¼", |
| | | efficiency: "high", |
| | | problem: "ç¨æ·ä¸ä¼æä½ç³»ç»", |
| | | solution: "æç
§æä½æåæ§è¡...", |
| | | keyPoints: "æ¥éª¤1,æ¥éª¤2,æ¥éª¤3", |
| | | creator: "å¼ ä¸", |
| | | usageCount: 0 |
| | | }); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### æ´æ°ç¥è¯åº (`POST /knowledgeBase/update`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | id | Long | æ¯ | data | ç¥è¯åºID | 10 | |
| | | | *(å
¶ä»åæ°åæ°å¢)* | - | - | data | - | - | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | updateKnowledgeBase({ |
| | | id: 10, |
| | | title: "æä½æå(æ´æ°)", |
| | | type: "guide", |
| | | // ...å
¶ä»åæ° |
| | | }); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### å é¤ç¥è¯åº (`DELETE /knowledgeBase/delete`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | ids | Long[] | æ¯ | data | ç¥è¯åºIDæ°ç» | [1, 2, 3] | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | delKnowledgeBase([1, 2, 3]); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 4.2 æä»¶ç®¡çæ¥å£åæ° |
| | | |
| | | #### æ¥è¯¢åéåç¶æ (`GET /knowledgeBase/vector/status/{knowledgeBaseId}`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | knowledgeBaseId | Long | æ¯ | URLè·¯å¾ | ç¥è¯åºID | 10 | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | getVectorStatus(10); |
| | | ``` |
| | | |
| | | **URL**: `/knowledgeBase/vector/status/10` |
| | | |
| | | --- |
| | | |
| | | #### ä¿åæä»¶å
³è (`POST /knowledgeBase/file/save`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | knowledgeBaseId | Long | æ¯ | data | ç¥è¯åºID | 10 | |
| | | | storageBlobIds | Long[] | æ¯ | data | æä»¶blob IDæ°ç» | [123, 124] | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | saveKnowledgeBaseFiles({ |
| | | knowledgeBaseId: 10, |
| | | storageBlobIds: [123, 124] |
| | | }); |
| | | ``` |
| | | |
| | | **éè¦è¯´æ**: |
| | | - â ï¸ **å¿
é¡»å
è°ç¨ `/common/upload` ä¸ä¼ æä»¶** |
| | | - â ï¸ **è·åè¿åç `data.id` ä½ä¸º `storageBlobId`** |
| | | - â ï¸ **æ¤æ¥å£è§¦å弿¥åéåå¤ç** |
| | | |
| | | --- |
| | | |
| | | #### å é¤ç¥è¯åºæä»¶ (`DELETE /knowledgeBase/file/delete`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | ids | Long[] | æ¯ | data | åéè®°å½IDæ°ç» | [1, 2, 3] | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | deleteKnowledgeBaseFile([row.id]); |
| | | // 注æ: row.id æ¯åéè®°å½çID,䏿¯ storageBlobId |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### éæ°åéå (`POST /knowledgeBase/vector/reprocess/{vectorId}`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | vectorId | Long | æ¯ | URLè·¯å¾ | åéè®°å½ID | 1 | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | reprocessVector(1); |
| | | ``` |
| | | |
| | | **URL**: `/knowledgeBase/vector/reprocess/1` |
| | | |
| | | --- |
| | | |
| | | ### 4.3 ç¥è¯é®çæ¥å£åæ° |
| | | |
| | | #### ç¥è¯åºé®ç (`POST /ai/knowledge/chat` - æµå¼) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | knowledgeBaseId | Long | æ¯ | body | ç¥è¯åºID | 10 | |
| | | | memoryId | String | æ¯ | body | ä¼è¯ID | "kb-chat-xxx" | |
| | | | question | String | æ¯ | body | ç¨æ·é®é¢ | "å¦ä½æä½?" | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | knowledgeChat({ |
| | | knowledgeBaseId: chatKnowledgeBaseId.value, |
| | | memoryId: memoryId.value, |
| | | question: currentQuestion |
| | | }); |
| | | ``` |
| | | |
| | | **ä¼è¯IDçæè§è**: |
| | | ```javascript |
| | | // æ¹å¼1: ä½¿ç¨ crypto.randomUUID() (æ¨è) |
| | | memoryId.value = crypto.randomUUID(); |
| | | |
| | | // æ¹å¼2: ä½¿ç¨æ¶é´æ³ |
| | | memoryId.value = 'kb-chat-' + Date.now(); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### æ¥è¯¢é®çåå² (`GET /ai/knowledge/history/{memoryId}`) |
| | | |
| | | | åæ°å | ç±»å | å¿
å¡« | ä¼ åä½ç½® | 说æ | 示ä¾å¼ | |
| | | |--------|------|------|----------|------|--------| |
| | | | memoryId | String | æ¯ | URLè·¯å¾ | ä¼è¯ID | "kb-chat-xxx" | |
| | | |
| | | **å端è°ç¨**: |
| | | ```javascript |
| | | getKnowledgeHistory('kb-chat-xxx'); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## äºãååºæ°æ®å段对ç
§è¡¨ |
| | | |
| | | ### 5.1 ç¥è¯åºå表ååº |
| | | |
| | | ```javascript |
| | | { |
| | | code: 200, |
| | | data: { |
| | | total: 100, |
| | | records: [ |
| | | { |
| | | id: 1, // ç¥è¯åºID |
| | | title: "æä½æå", // ç¥è¯æ é¢ |
| | | type: "guide", // ç¥è¯ç±»å |
| | | scenario: "ç³»ç»æä½", // éç¨åºæ¯ |
| | | efficiency: "high", // è§£å³æç |
| | | problem: "...", // é®é¢æè¿° |
| | | solution: "...", // è§£å³æ¹æ¡ |
| | | keyPoints: "...", // å
³é®è¦ç¹ |
| | | creator: "å¼ ä¸", // å建人 |
| | | usageCount: 10, // ä½¿ç¨æ¬¡æ° |
| | | fileCount: 3, // æä»¶æ°é |
| | | totalChunkCount: 45, // æ»åçæ°é |
| | | createTime: "2026-06-08", // å建æ¶é´ |
| | | updateTime: "2026-06-08" // æ´æ°æ¶é´ |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 5.2 æä»¶åéåç¶æååº |
| | | |
| | | ```javascript |
| | | { |
| | | code: 200, |
| | | data: [ |
| | | { |
| | | id: 1, // åéè®°å½ID |
| | | storageBlobId: 123, // æä»¶blob ID |
| | | fileName: "æä½æå.docx", // æä»¶å |
| | | fileType: "docx", // æä»¶ç±»å |
| | | vectorStatus: 2, // åéåç¶æ: 0-å¾
å¤ç, 1-å¤çä¸, 2-已宿, 3-失败 |
| | | chunkCount: 15, // åçæ°é |
| | | namespace: "kb-10", // åéå½åç©ºé´ |
| | | vectorError: null, // åéåéè¯¯ä¿¡æ¯ |
| | | createTime: "2026-06-08" // å建æ¶é´ |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 5.3 æä»¶ä¸ä¼ ååº |
| | | |
| | | ```javascript |
| | | { |
| | | code: 200, |
| | | data: { |
| | | id: 123, // â ï¸ è¿æ¯ storageBlobId,ç¨äºä¿åæä»¶å
³è |
| | | name: "æä½æå.docx", // æä»¶å |
| | | url: "/profile/upload/...", // æä»¶URL |
| | | previewURL: "...", // é¢è§URL |
| | | downloadURL: "..." // ä¸è½½URL |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **éè¦**: ä¸ä¼ æåå,éè¦æå `response.data.id` ä½ä¸º `storageBlobId` |
| | | |
| | | --- |
| | | |
| | | ## å
ãå端ç»ä»¶ä¼ åè§è |
| | | |
| | | ### 6.1 è¡¨æ ¼ç»ä»¶ä¼ å |
| | | |
| | | ```vue |
| | | <PIMTable |
| | | rowKey="id" <!-- è¡å¯ä¸æ è¯å段 --> |
| | | :column="tableColumn" <!-- åé
ç½® --> |
| | | :tableData="tableData" <!-- è¡¨æ ¼æ°æ® --> |
| | | :page="page" <!-- å页é
ç½® --> |
| | | :isSelection="true" <!-- æ¯å¦æ¯æéæ© --> |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | /> |
| | | ``` |
| | | |
| | | **å页é
ç½®**: |
| | | ```javascript |
| | | page: { |
| | | current: 1, // å½å页ç |
| | | size: 20, // æ¯é¡µæ¡æ° |
| | | total: 0 // æ»è®°å½æ° |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 6.2 å¼¹çªç»ä»¶ä¼ å |
| | | |
| | | ```vue |
| | | <FormDialog |
| | | v-model="dialogVisible" <!-- æ§å¶æ¾ç¤º --> |
| | | :title="dialogTitle" <!-- å¼¹çªæ é¢ --> |
| | | :width="'800px'" <!-- å¼¹çªå®½åº¦ --> |
| | | @close="closeDialog" <!-- å
³éåè° --> |
| | | @confirm="submitForm" <!-- 确认åè° --> |
| | | @cancel="closeDialog" <!-- åæ¶åè° --> |
| | | > |
| | | <!-- å¼¹çªå
容 --> |
| | | </FormDialog> |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 6.3 ä¸ä¼ ç»ä»¶ä¼ å |
| | | |
| | | ```vue |
| | | <el-upload |
| | | :action="uploadUrl" <!-- ä¸ä¼ å°å --> |
| | | :headers="uploadHeaders" <!-- 请æ±å¤´ --> |
| | | :on-success="handleUploadSuccess" <!-- æååè° --> |
| | | :on-error="handleUploadError" <!-- 失败åè° --> |
| | | :before-upload="beforeUpload" <!-- ä¸ä¼ åæ ¡éª --> |
| | | multiple <!-- æ¯æå¤é --> |
| | | :show-file-list="false" <!-- 䏿¾ç¤ºæä»¶å表 --> |
| | | accept=".txt,.md,.docx,.xlsx,.xls,.pdf" <!-- æä»¶ç±»åéå¶ --> |
| | | /> |
| | | ``` |
| | | |
| | | **ä¸ä¼ é
ç½®**: |
| | | ```javascript |
| | | const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload"; |
| | | const uploadHeaders = { |
| | | Authorization: "Bearer " + getToken() |
| | | }; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãåæ°ç±»å转æ¢è§è |
| | | |
| | | ### 7.1 å符串转æ°å¼ |
| | | |
| | | ```javascript |
| | | // å端è¿åçæ°å¼å¯è½æ¯å符串,éè¦è½¬æ¢ |
| | | const id = Number(row.id); |
| | | const count = parseInt(row.chunkCount, 10); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 7.2 æ°å¼è½¬å符串 |
| | | |
| | | ```javascript |
| | | // URLè·¯å¾åæ°éè¦å符串 |
| | | const url = `/knowledgeBase/vector/status/${String(knowledgeBaseId)}`; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 7.3 æ°ç»å¤ç |
| | | |
| | | ```javascript |
| | | // IDæ°ç»å¤ç |
| | | const ids = selection.map(item => item.id); // â
æåID |
| | | await delKnowledgeBase(ids); // â
ä¼ éæ°ç» |
| | | |
| | | // æä»¶blob IDæ°ç» |
| | | const blobIds = uploadedFiles.map(file => file.id); |
| | | await saveKnowledgeBaseFiles({ |
| | | knowledgeBaseId: currentKnowledgeBase.id, |
| | | storageBlobIds: blobIds |
| | | }); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## å
«ãåæ°éªè¯è§è |
| | | |
| | | ### 8.1 表åéªè¯è§å |
| | | |
| | | ```javascript |
| | | const rules = { |
| | | title: [ |
| | | { required: true, message: "请è¾å
¥ç¥è¯æ é¢", trigger: "blur" } |
| | | ], |
| | | type: [ |
| | | { required: true, message: "è¯·éæ©ç¥è¯ç±»å", trigger: "change" } |
| | | ], |
| | | problem: [ |
| | | { required: true, message: "请æè¿°éå°çé®é¢", trigger: "blur" } |
| | | ], |
| | | solution: [ |
| | | { required: true, message: "è¯·è¯¦ç»æè¿°è§£å³æ¹æ¡", trigger: "blur" } |
| | | ] |
| | | }; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 8.2 ä¸ä¼ æä»¶æ ¡éª |
| | | |
| | | ```javascript |
| | | const beforeUpload = (file) => { |
| | | // æä»¶ç±»åæ ¡éª |
| | | const allowedTypes = ['.txt', '.md', '.docx', '.xlsx', '.xls', '.pdf']; |
| | | const fileName = file.name.toLowerCase(); |
| | | const isAllowed = allowedTypes.some(type => fileName.endsWith(type)); |
| | | |
| | | if (!isAllowed) { |
| | | ElMessage.error('åªæ¯æ txtãmdãdocxãxlsxãxlsãpdf æ ¼å¼çæä»¶'); |
| | | return false; |
| | | } |
| | | |
| | | // æä»¶å¤§å°æ ¡éª |
| | | const isLt50M = file.size / 1024 / 1024 < 50; |
| | | if (!isLt50M) { |
| | | ElMessage.error('æä»¶å¤§å°ä¸è½è¶
è¿ 50MB'); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | }; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 8.3 é®çåæ°æ ¡éª |
| | | |
| | | ```javascript |
| | | const sendQuestion = async () => { |
| | | // 空å
å®¹æ ¡éª |
| | | if (!questionInput.value.trim()) { |
| | | ElMessage.warning("请è¾å
¥é®é¢"); |
| | | return; |
| | | } |
| | | |
| | | // ç¥è¯åºéæ©æ ¡éª |
| | | if (!chatKnowledgeBaseId.value) { |
| | | ElMessage.warning("请å
éæ©ç¥è¯åº"); |
| | | return; |
| | | } |
| | | |
| | | // åéç¶ææ ¡éª |
| | | if (sending.value) { |
| | | return; // 鲿¢éå¤åé |
| | | } |
| | | |
| | | // ...åéè¯·æ± |
| | | }; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¹ã常è§é误åè§£å³æ¹æ¡ |
| | | |
| | | ### 9.1 åæ°åä¸å¹é
|
| | | |
| | | **é误示ä¾**: |
| | | ```javascript |
| | | // â é误: 使ç¨äºä¸å线å½å |
| | | saveKnowledgeBaseFiles({ |
| | | knowledge_base_id: 10, |
| | | storage_blob_ids: [123] |
| | | }); |
| | | |
| | | // â
æ£ç¡®: 使ç¨é©¼å³°å½å |
| | | saveKnowledgeBaseFiles({ |
| | | knowledgeBaseId: 10, |
| | | storageBlobIds: [123] |
| | | }); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 9.2 ä¼ åä½ç½®é误 |
| | | |
| | | **é误示ä¾**: |
| | | ```javascript |
| | | // â é误: POST请æ±ä½¿ç¨ params |
| | | export function addKnowledgeBase(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/add", |
| | | method: "post", |
| | | params: data // â é误 |
| | | }); |
| | | } |
| | | |
| | | // â
æ£ç¡®: POST请æ±ä½¿ç¨ data |
| | | export function addKnowledgeBase(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/add", |
| | | method: "post", |
| | | data: data // â
æ£ç¡® |
| | | }); |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 9.3 DELETE请æ±åæ°é误 |
| | | |
| | | **é误示ä¾**: |
| | | ```javascript |
| | | // â é误: DELETEä½¿ç¨ params ä¼ éæ°ç» |
| | | export function delKnowledgeBase(ids) { |
| | | return request({ |
| | | url: "/knowledgeBase/delete", |
| | | method: "delete", |
| | | params: ids // â é误 |
| | | }); |
| | | } |
| | | |
| | | // â
æ£ç¡®: DELETEä½¿ç¨ data ä¼ éæ°ç» |
| | | export function delKnowledgeBase(ids) { |
| | | return request({ |
| | | url: "/knowledgeBase/delete", |
| | | method: "delete", |
| | | data: ids // â
æ£ç¡® |
| | | }); |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 9.4 æµå¼æ¥å£é误 |
| | | |
| | | **é误示ä¾**: |
| | | ```javascript |
| | | // â é误: æµå¼æ¥å£ä½¿ç¨ axios |
| | | export function knowledgeChat(data) { |
| | | return request({ |
| | | url: "/ai/knowledge/chat", |
| | | method: "post", |
| | | data: data // â axios䏿¯ææµå¼ |
| | | }); |
| | | } |
| | | |
| | | // â
æ£ç¡®: æµå¼æ¥å£ä½¿ç¨ Fetch API |
| | | export function knowledgeChat(data) { |
| | | const token = getToken(); |
| | | return fetch(import.meta.env.VITE_APP_BASE_API + '/ai/knowledge/chat', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'Authorization': 'Bearer ' + token |
| | | }, |
| | | body: JSON.stringify(data) // â
æ£ç¡® |
| | | }); |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 9.5 æä»¶ä¸ä¼ IDè·åé误 |
| | | |
| | | **é误示ä¾**: |
| | | ```javascript |
| | | // â é误: ä¸ä¼ æååæ²¡æä¿å storageBlobId |
| | | const handleUploadSuccess = (response) => { |
| | | if (response.code === 200) { |
| | | ElMessage.success("ä¸ä¼ æå"); |
| | | // â æ²¡æä¿å response.data.id |
| | | } |
| | | }; |
| | | |
| | | // â
æ£ç¡®: ä¿å storageBlobId ç¨äºåç»å
³è |
| | | const handleUploadSuccess = (response, file) => { |
| | | if (response.code === 200) { |
| | | uploadedBlobIds.value.push(response.data.id); // â
ä¿åID |
| | | ElMessage.success(`æä»¶ ${file.name} ä¸ä¼ æå`); |
| | | } |
| | | }; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## åãæä½³å®è·µå»ºè®® |
| | | |
| | | ### 10.1 åæ°å½åä¸è´æ§ |
| | | |
| | | â
**åç«¯åæ°åä¸å端å®ä½åæ®µä¿æä¸è´** |
| | | ```javascript |
| | | // å端 |
| | | { |
| | | knowledgeBaseId: 10, |
| | | storageBlobIds: [123] |
| | | } |
| | | |
| | | // å端å®ä½å段 |
| | | private Long knowledgeBaseId; |
| | | private List<Long> storageBlobIds; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 10.2 åæ°ç±»åä¸è´æ§ |
| | | |
| | | â
**æç¡®åæ°ç±»å,é¿å
èªå¨ç±»å转æ¢** |
| | | ```javascript |
| | | // æ°å¼ç±»å |
| | | const id = 10; // number |
| | | const count = 0; // number |
| | | |
| | | // å符串类å |
| | | const title = ""; // string |
| | | const type = ""; // string |
| | | |
| | | // æ°ç»ç±»å |
| | | const ids = []; // array |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 10.3 å¿
å¡«åæ°æ ¡éª |
| | | |
| | | â
**å¨è°ç¨æ¥å£åæ ¡éªå¿
å¡«åæ°** |
| | | ```javascript |
| | | const saveFiles = async () => { |
| | | if (!currentKnowledgeBase.value?.id) { |
| | | ElMessage.error("ç¥è¯åºä¿¡æ¯å¼å¸¸"); |
| | | return; |
| | | } |
| | | |
| | | if (uploadedBlobIds.value.length === 0) { |
| | | ElMessage.warning("请å
ä¸ä¼ æä»¶"); |
| | | return; |
| | | } |
| | | |
| | | // ...è°ç¨æ¥å£ |
| | | }; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 10.4 åæ°é»è®¤å¼ |
| | | |
| | | â
**为å¯éåæ°è®¾ç½®åççé»è®¤å¼** |
| | | ```javascript |
| | | const form = { |
| | | title: "", |
| | | type: "", |
| | | usageCount: 0, // â
æ°å¼ç±»åé»è®¤å¼ä¸º0 |
| | | creator: userStore.nickName || "" // â
使ç¨å½åç¨æ·åä½ä¸ºé»è®¤å¼ |
| | | }; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 10.5 URLè·¯å¾åæ° |
| | | |
| | | â
**è·¯å¾åæ°ä½¿ç¨æ¨¡æ¿å符串** |
| | | ```javascript |
| | | const url = `/knowledgeBase/vector/status/${knowledgeBaseId}`; |
| | | const url = `/knowledgeBase/vector/reprocess/${vectorId}`; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## åä¸ãåæ°æ å°å
³ç³»æ»ç» |
| | | |
| | | ### ç¥è¯åºIDç¸å
³ |
| | | | åºæ¯ | åæ°å | ç±»å | æ¥æº | |
| | | |------|--------|------|------| |
| | | | ç¥è¯åºå表æ¥è¯¢ | - | - | URLè·¯å¾æ åæ° | |
| | | | ç¥è¯åºè¯¦æ
| id | Long | URLè·¯å¾åæ° | |
| | | | æä»¶å
³èä¿å | knowledgeBaseId | Long | POST bodyåæ° | |
| | | | åéåç¶ææ¥è¯¢ | knowledgeBaseId | Long | URLè·¯å¾åæ° | |
| | | | ç¥è¯é®ç | knowledgeBaseId | Long | POST bodyåæ° | |
| | | |
| | | --- |
| | | |
| | | ### æä»¶IDç¸å
³ |
| | | | åºæ¯ | åæ°å | ç±»å | æ¥æº | |
| | | |------|--------|------|------| |
| | | | æä»¶ä¸ä¼ ååº | data.id | Long | ååºæ°æ®(ä½ä¸ºstorageBlobId) | |
| | | | æä»¶å
³èä¿å | storageBlobIds | Long[] | POST bodyåæ° | |
| | | | æä»¶å é¤ | ids | Long[] | DELETE bodyåæ°(åéè®°å½ID) | |
| | | | éæ°åéå | vectorId | Long | URLè·¯å¾åæ° | |
| | | |
| | | --- |
| | | |
| | | ### ä¼è¯IDç¸å
³ |
| | | | åºæ¯ | åæ°å | ç±»å | æ¥æº | |
| | | |------|--------|------|------| |
| | | | ç¥è¯é®ç | memoryId | String | å端çæUUID | |
| | | | é®çå岿¥è¯¢ | memoryId | String | URLè·¯å¾åæ° | |
| | | |
| | | --- |
| | | |
| | | ## åäºãéå½ |
| | | |
| | | ### éå½A: åæ°ç±»å对ç
§è¡¨ |
| | | |
| | | | åæ°ç±»å | JavaScript | Java | MySQL | |
| | | |----------|------------|------|-------| |
| | | | ID | number/Long | Long | BIGINT | |
| | | | æ é¢ | String | String | VARCHAR | |
| | | | ç±»å | String | String | VARCHAR | |
| | | | ç¶æ | Integer | Integer | TINYINT | |
| | | | æ°é | Integer | Integer | INT | |
| | | | æ¶é´ | String/Date | LocalDateTime | DATETIME | |
| | | | æ°ç» | Array | List | - | |
| | | |
| | | --- |
| | | |
| | | ### éå½B: HTTPæ¹æ³ä¸ä¼ åä½ç½®å¯¹ç
§è¡¨ |
| | | |
| | | | HTTPæ¹æ³ | ä¼ åä½ç½® | requesté
ç½® | éç¨åºæ¯ | |
| | | |----------|----------|--------------|----------| |
| | | | GET | URLåæ° | `params: query` | æ¥è¯¢ãå表 | |
| | | | POST | 请æ±ä½ | `data: data` | æ°å¢ãæ´æ°ãä¿å | |
| | | | DELETE | 请æ±ä½ | `data: ids` | å é¤ãæ¹éå é¤ | |
| | | | PUT | 请æ±ä½ | `data: data` | æ´æ°(é¨å使ç¨POST) | |
| | | | æµå¼POST | 请æ±ä½ | `body: JSON.stringify()` | AIé®ç | |
| | | |
| | | --- |
| | | |
| | | ### éå½C: åéåç¶æå¼å¯¹ç
§è¡¨ |
| | | |
| | | | ç¶æå¼ | ç¶æåç§° | å端æ¾ç¤º | æ ç¾é¢è² | |
| | | |--------|----------|----------|----------| |
| | | | 0 | å¾
å¤ç | "å¾
å¤ç" | info (ç°è²) | |
| | | | 1 | å¤çä¸ | "å¤çä¸" | warning (æ©è²) | |
| | | | 2 | 已宿 | "已宿" | success (绿è²) | |
| | | | 3 | 失败 | "失败" | danger (红è²) | |
| | | |
| | | --- |
| | | |
| | | ## åä¸ãæ»ç» |
| | | |
| | | æ¬ææ¡£è¯¦ç»è§èäºç¥è¯åºæ¨¡åçåæ°å½ååä¼ åæ¹å¼,éµå¾ªä»¥ä¸åå: |
| | | |
| | | 1. â
**å½åä¸è´æ§**: ååç«¯åæ°åä¿æä¸è´(驼峰å½å) |
| | | 2. â
**ä¼ åè§èæ§**: GETç¨paramsãPOSTç¨dataãDELETEç¨data |
| | | 3. â
**ç±»åæç¡®æ§**: æç¡®åæ°ç±»å,åç设置é»è®¤å¼ |
| | | 4. â
**æ ¡éªå®æ´æ§**: å¿
å¡«åæ°éæ ¡éª,å¯éåæ°æé»è®¤å¼ |
| | | 5. â
**é误é¿å
**: éµå¾ªè§èé¿å
常è§é误 |
| | | |
| | | 建议å¢éæåä¸¥æ ¼éµå¾ªæ¬è§è,ç¡®ä¿ååç«¯åæ°ä¼ éçä¸è´æ§åå¯é æ§ã |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # ç¥è¯åºæ¨¡å宿´å端å®ç°ææ¡£ |
| | | |
| | | ## ä¸ãæ¨¡åæ¦è¿° |
| | | |
| | | ç¥è¯åºæ¨¡åæ¯ä¸ä¸ªéæäºRAG(æ£ç´¢å¢å¼ºçæ)ææ¯çæºè½ç¥è¯ç®¡çç³»ç»,æ¯æ: |
| | | - **ç¥è¯åºCRUD管ç** - å建ãç¼è¾ãå é¤ãæ¥è¯¢ç¥è¯åº |
| | | - **æä»¶ä¸ä¼ ä¸åéå** - æ¯æå¤ç§æä»¶æ ¼å¼,èªå¨è¿è¡åéåçå¤ç |
| | | - **æºè½é®ç** - åºäºä¸ä¼ æä»¶å
容è¿è¡AIé®ç |
| | | - **æä»¶ç®¡ç** - æ¥çæä»¶åéåç¶æ,æ¯æéæ°å¤çåå é¤ |
| | | |
| | | ### ææ¯æ¶æ |
| | | - **åç«¯æ¡æ¶**: Vue 3 + Composition API |
| | | - **UIç»ä»¶åº**: Element Plus |
| | | - **åéæ°æ®åº**: Pinecone |
| | | - **AI模å**: é¿éäºéä¹åé® |
| | | - **æä»¶å¤ç**: æ¯ædocxãxlsxãpdfãtxtãmdçæ ¼å¼ |
| | | |
| | | --- |
| | | |
| | | ## äºãæä»¶ç»æ |
| | | |
| | | ``` |
| | | src/ |
| | | âââ api/ |
| | | â âââ collaborativeApproval/ |
| | | â âââ knowledgeBase.js # APIæ¥å£å°è£
|
| | | âââ views/ |
| | | â âââ collaborativeApproval/ |
| | | â âââ knowledgeBase/ |
| | | â âââ index.vue # 主页é¢ç»ä»¶ |
| | | âââ components/ |
| | | âââ PIMTable/ |
| | | â âââ PIMTable.vue # è¡¨æ ¼ç»ä»¶ |
| | | âââ Dialog/ |
| | | âââ FormDialog.vue # å¼¹çªç»ä»¶ |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãAPIæ¥å£å®ä¹ |
| | | |
| | | ### 3.1 æä»¶ä½ç½® |
| | | `src/api/collaborativeApproval/knowledgeBase.js` |
| | | |
| | | ### 3.2 宿´æ¥å£å表 |
| | | |
| | | ```javascript |
| | | import request from "@/utils/request"; |
| | | import { getToken } from '@/utils/auth'; |
| | | |
| | | // 1. æ¥è¯¢ç¥è¯åºå表(å页) |
| | | export function listKnowledgeBase(query) { |
| | | return request({ |
| | | url: "/knowledgeBase/getList", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 2. æ°å¢ç¥è¯åº |
| | | export function addKnowledgeBase(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // 3. ä¿®æ¹ç¥è¯åº |
| | | export function updateKnowledgeBase(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/update", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // 4. å é¤ç¥è¯åº |
| | | export function delKnowledgeBase(query) { |
| | | return request({ |
| | | url: "/knowledgeBase/delete", |
| | | method: "delete", |
| | | data: query, |
| | | }); |
| | | } |
| | | |
| | | // 5. æ¥è¯¢ç¥è¯åºæä»¶åéåç¶æ(å
嫿件å表) |
| | | export function getVectorStatus(knowledgeBaseId) { |
| | | return request({ |
| | | url: `/knowledgeBase/vector/status/${knowledgeBaseId}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // 6. ä¿åç¥è¯åºæä»¶å
³è(触ååéå) |
| | | export function saveKnowledgeBaseFiles(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/file/save", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // 7. å é¤ç¥è¯åºæä»¶ |
| | | export function deleteKnowledgeBaseFile(ids) { |
| | | return request({ |
| | | url: "/knowledgeBase/file/delete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | // 8. éæ°åéåæä»¶ |
| | | export function reprocessVector(vectorId) { |
| | | return request({ |
| | | url: `/knowledgeBase/vector/reprocess/${vectorId}`, |
| | | method: "post", |
| | | }); |
| | | } |
| | | |
| | | // 9. ç¥è¯åºé®ç(æµå¼) |
| | | export function knowledgeChat(data, onMessage) { |
| | | const token = getToken(); |
| | | return fetch(import.meta.env.VITE_APP_BASE_API + '/ai/knowledge/chat', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'Authorization': 'Bearer ' + token |
| | | }, |
| | | body: JSON.stringify(data) |
| | | }); |
| | | } |
| | | |
| | | // 10. æ¥è¯¢ç¥è¯åºé®çåå² |
| | | export function getKnowledgeHistory(memoryId) { |
| | | return request({ |
| | | url: `/ai/knowledge/history/${memoryId}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | ``` |
| | | |
| | | ### 3.3 æ¥å£åæ°è¯´æ |
| | | |
| | | #### ç¥è¯åºå表æ¥è¯¢ |
| | | ```javascript |
| | | // 请æ±åæ° |
| | | { |
| | | current: 1, // å½å页ç |
| | | size: 20, // æ¯é¡µæ¡æ° |
| | | title: "", // ç¥è¯æ é¢(å¯é) |
| | | type: "" // ç¥è¯ç±»å(å¯é) |
| | | } |
| | | |
| | | // ååºæ°æ® |
| | | { |
| | | code: 200, |
| | | data: { |
| | | total: 100, |
| | | records: [ |
| | | { |
| | | id: 1, |
| | | title: "æä½æå", |
| | | type: "guide", |
| | | scenario: "ç³»ç»æä½æå¯¼", |
| | | efficiency: "high", |
| | | problem: "ç¨æ·ä¸ä¼æä½ç³»ç»", |
| | | solution: "æç
§æä½æåæ§è¡...", |
| | | keyPoints: "æ¥éª¤1,æ¥éª¤2,æ¥éª¤3", |
| | | creator: "å¼ ä¸", |
| | | usageCount: 10, |
| | | fileCount: 3, // æä»¶æ°é |
| | | totalChunkCount: 45, // æ»åçæ°é |
| | | createTime: "2026-06-08 10:00:00" |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | #### ä¿åæä»¶å
³è |
| | | ```javascript |
| | | // 请æ±åæ° |
| | | { |
| | | knowledgeBaseId: 10, // ç¥è¯åºID |
| | | storageBlobIds: [123, 124] // ä¸ä¼ æä»¶è¿åçblob IDå表 |
| | | } |
| | | |
| | | // ååºæ°æ® |
| | | { |
| | | code: 200, |
| | | msg: "æä½æå" |
| | | } |
| | | ``` |
| | | |
| | | #### åéåç¶ææ¥è¯¢ |
| | | ```javascript |
| | | // ååºæ°æ® |
| | | { |
| | | code: 200, |
| | | data: [ |
| | | { |
| | | id: 1, |
| | | storageBlobId: 123, |
| | | fileName: "æä½æå.docx", |
| | | fileType: "docx", |
| | | vectorStatus: 2, // 0-å¾
å¤ç,1-å¤çä¸,2-已宿,3-失败 |
| | | chunkCount: 15, // åçæ°é |
| | | namespace: "kb-10", |
| | | vectorError: null, |
| | | createTime: "2026-06-08 10:00:00" |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | #### ç¥è¯åºé®ç |
| | | ```javascript |
| | | // 请æ±åæ° |
| | | { |
| | | knowledgeBaseId: 10, |
| | | memoryId: "session-xxx", // ä¼è¯ID,ç¨äºä¿æä¸ä¸æ |
| | | question: "å¦ä½æä½å®¡æ¹æµç¨?" |
| | | } |
| | | |
| | | // ååº(æµå¼è¿å text/stream;charset=utf-8) |
| | | // æ ¹æ®ç¥è¯åºå
容,å®¡æ¹æµç¨çæä½æ¥éª¤å¦ä¸: |
| | | // 1. ç»å½ç³»ç»åè¿å
¥å®¡æ¹ç®¡ç模å... |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## åãæ ¸å¿ç»ä»¶å®ç° |
| | | |
| | | ### 4.1 主页é¢ç»æ |
| | | |
| | | 页é¢éç¨Tab页ç¾å¸å±,å
å«ä¸¤ä¸ªä¸»è¦åè½æ¨¡å: |
| | | - **ç¥è¯åºç®¡ç** - ç¥è¯åºCRUDæä½ |
| | | - **ç¥è¯åºé®ç** - åºäºRAGçæºè½é®ç |
| | | |
| | | ### 4.2 æ°æ®æ¨¡åå®ä¹ |
| | | |
| | | ```javascript |
| | | // ååºå¼æ°æ® |
| | | const data = reactive({ |
| | | // æç´¢è¡¨å |
| | | searchForm: { |
| | | title: "", |
| | | type: "", |
| | | }, |
| | | |
| | | // å页é
ç½® |
| | | page: { |
| | | current: 1, |
| | | size: 20, |
| | | total: 0, |
| | | }, |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | tableData: [], |
| | | tableLoading: false, |
| | | selectedIds: [], |
| | | |
| | | // ç¥è¯åºè¡¨å |
| | | form: { |
| | | title: "", |
| | | type: "", |
| | | scenario: "", |
| | | efficiency: "", |
| | | problem: "", |
| | | solution: "", |
| | | keyPoints: "", |
| | | creator: "", |
| | | usageCount: 0 |
| | | }, |
| | | |
| | | // å¼¹çªæ§å¶ |
| | | dialogVisible: false, |
| | | dialogTitle: "", |
| | | dialogType: "add", // add or edit |
| | | viewDialogVisible: false, |
| | | currentKnowledge: {}, |
| | | |
| | | // æä»¶ç®¡ç |
| | | filesDialogVisible: false, |
| | | currentKnowledgeBase: null, |
| | | fileList: [], |
| | | uploadedBlobIds: [], |
| | | savingFiles: false, |
| | | |
| | | // ç¥è¯åºé®ç |
| | | chatDialogVisible: false, |
| | | messages: [], |
| | | inputQuestion: "", |
| | | chatLoading: false, |
| | | memoryId: "" |
| | | }); |
| | | ``` |
| | | |
| | | ### 4.3 è¡¨æ ¼åé
ç½® |
| | | |
| | | ```javascript |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "ç¥è¯æ é¢", |
| | | prop: "title", |
| | | showOverflowTooltip: true, |
| | | }, |
| | | { |
| | | label: "ç¥è¯ç±»å", |
| | | prop: "type", |
| | | dataType: "tag", |
| | | formatData: (params) => getKnowledgeTypeLabel(params), |
| | | formatType: (params) => getKnowledgeTypeTagType(params) |
| | | }, |
| | | { |
| | | label: "éç¨åºæ¯", |
| | | prop: "scenario", |
| | | width: 150, |
| | | showOverflowTooltip: true, |
| | | }, |
| | | { |
| | | label: "è§£å³æç", |
| | | prop: "efficiency", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | const efficiencyMap = { |
| | | high: "æ¾èæå", |
| | | medium: "ä¸è¬æå", |
| | | low: "轻微æå" |
| | | }; |
| | | return efficiencyMap[params] || params; |
| | | }, |
| | | formatType: (params) => { |
| | | const typeMap = { |
| | | high: "success", |
| | | medium: "warning", |
| | | low: "info" |
| | | }; |
| | | return typeMap[params] || "info"; |
| | | } |
| | | }, |
| | | { |
| | | label: "æä»¶æ°é", |
| | | prop: "fileCount", |
| | | width: 100, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "åçæ°é", |
| | | prop: "totalChunkCount", |
| | | width: 100, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "ä½¿ç¨æ¬¡æ°", |
| | | prop: "usageCount", |
| | | width: 100, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "å建人", |
| | | prop: "creator", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "å建æ¶é´", |
| | | prop: "createTime", |
| | | width: 180, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 280, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => openForm("edit", row) |
| | | }, |
| | | { |
| | | name: "æä»¶", |
| | | type: "text", |
| | | clickFun: (row) => openFilesDialog(row) |
| | | }, |
| | | { |
| | | name: "é®ç", |
| | | type: "text", |
| | | clickFun: (row) => openChatDialog(row) |
| | | }, |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => viewKnowledge(row) |
| | | } |
| | | ] |
| | | } |
| | | ]); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## äºãæ ¸å¿ä¸å¡é»è¾ |
| | | |
| | | ### 5.1 æä»¶ä¸ä¼ ä¸åéåæµç¨ |
| | | |
| | | #### æµç¨å¾ |
| | | ``` |
| | | ç¨æ·ç¹å»"ä¸ä¼ æä»¶" |
| | | â |
| | | éæ©æä»¶(æ¯æå¤é) |
| | | â |
| | | åç«¯æ ¡éªæä»¶ç±»ååå¤§å° |
| | | â |
| | | è°ç¨ /common/upload ä¸ä¼ æä»¶ |
| | | â |
| | | è·å storageBlobId å表 |
| | | â |
| | | ç¨æ·ç¹å»"ä¿åæä»¶å
³è" |
| | | â |
| | | è°ç¨ /knowledgeBase/file/save |
| | | â |
| | | å端å建åéè®°å½ + 弿¥è§¦ååéå |
| | | â |
| | | å端延è¿1ç§å·æ°æä»¶å表 |
| | | â |
| | | æ¾ç¤ºåéåç¶æ(å¾
å¤çâå¤çä¸â已宿) |
| | | ``` |
| | | |
| | | #### 代ç å®ç° |
| | | |
| | | ```vue |
| | | <template> |
| | | <div class="file-manager"> |
| | | <!-- æä»¶ä¸ä¼ --> |
| | | <div class="upload-section"> |
| | | <el-upload |
| | | :action="uploadUrl" |
| | | :headers="uploadHeaders" |
| | | :on-success="handleUploadSuccess" |
| | | :on-error="handleUploadError" |
| | | :before-upload="beforeUpload" |
| | | multiple |
| | | :show-file-list="false" |
| | | accept=".txt,.md,.docx,.xlsx,.xls,.pdf" |
| | | > |
| | | <el-button type="primary">ä¸ä¼ æä»¶</el-button> |
| | | </el-upload> |
| | | <el-button |
| | | type="success" |
| | | @click="saveFiles" |
| | | :disabled="uploadedBlobIds.length === 0" |
| | | :loading="savingFiles" |
| | | > |
| | | ä¿åæä»¶å
³è |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- æä»¶å表 --> |
| | | <el-table :data="fileList" style="margin-top: 20px" border> |
| | | <el-table-column prop="fileName" label="æä»¶å" /> |
| | | <el-table-column prop="fileType" label="æä»¶ç±»å" width="100" /> |
| | | <el-table-column label="åéåç¶æ" width="120"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getStatusType(row.vectorStatus)"> |
| | | {{ getStatusText(row.vectorStatus) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="chunkCount" label="åçæ°" width="100" /> |
| | | <el-table-column label="æä½" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | v-if="row.vectorStatus === 3" |
| | | type="text" |
| | | @click="reprocessFile(row)" |
| | | > |
| | | éæ°å¤ç |
| | | </el-button> |
| | | <el-button type="text" @click="deleteFile(row)" style="color: #f56c6c"> |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { getToken } from "@/utils/auth"; |
| | | |
| | | // æä»¶ä¸ä¼ é
ç½® |
| | | const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload"; |
| | | const uploadHeaders = { Authorization: "Bearer " + getToken() }; |
| | | const uploadedBlobIds = ref([]); |
| | | const fileList = ref([]); |
| | | |
| | | // ä¸ä¼ åæ ¡éª |
| | | const beforeUpload = (file) => { |
| | | const allowedTypes = ['.txt', '.md', '.docx', '.xlsx', '.xls', '.pdf']; |
| | | const fileName = file.name.toLowerCase(); |
| | | const isAllowed = allowedTypes.some(type => fileName.endsWith(type)); |
| | | |
| | | if (!isAllowed) { |
| | | ElMessage.error('åªæ¯æ txtãmdãdocxãxlsxãxlsãpdf æ ¼å¼çæä»¶'); |
| | | return false; |
| | | } |
| | | |
| | | const isLt50M = file.size / 1024 / 1024 < 50; |
| | | if (!isLt50M) { |
| | | ElMessage.error('æä»¶å¤§å°ä¸è½è¶
è¿ 50MB'); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | }; |
| | | |
| | | // ä¸ä¼ æå |
| | | const handleUploadSuccess = (response, file) => { |
| | | if (response.code === 200) { |
| | | uploadedBlobIds.value.push(response.data.id); |
| | | ElMessage.success(`æä»¶ ${file.name} ä¸ä¼ æå`); |
| | | } else { |
| | | ElMessage.error(response.msg || "ä¸ä¼ 失败"); |
| | | } |
| | | }; |
| | | |
| | | // ä¿åæä»¶å
³è |
| | | const saveFiles = async () => { |
| | | if (uploadedBlobIds.value.length === 0) { |
| | | ElMessage.warning("请å
ä¸ä¼ æä»¶"); |
| | | return; |
| | | } |
| | | |
| | | savingFiles.value = true; |
| | | try { |
| | | await saveKnowledgeBaseFiles({ |
| | | knowledgeBaseId: currentKnowledgeBase.value.id, |
| | | storageBlobIds: uploadedBlobIds.value |
| | | }); |
| | | |
| | | ElMessage.success("æä»¶å
³èä¿åæå,æ£å¨åå°å¤çåéå"); |
| | | uploadedBlobIds.value = []; |
| | | |
| | | // å»¶è¿å·æ°æä»¶å表 |
| | | setTimeout(() => { |
| | | loadFileList(); |
| | | }, 1000); |
| | | } catch (error) { |
| | | console.error("ä¿åæä»¶å
³è失败:", error); |
| | | ElMessage.error("ä¿åæä»¶å
³è失败"); |
| | | } finally { |
| | | savingFiles.value = false; |
| | | } |
| | | }; |
| | | |
| | | // å è½½æä»¶å表 |
| | | const loadFileList = async () => { |
| | | if (!currentKnowledgeBase.value?.id) return; |
| | | |
| | | try { |
| | | const res = await getVectorStatus(currentKnowledgeBase.value.id); |
| | | fileList.value = res.data || []; |
| | | } catch (error) { |
| | | console.error("å è½½æä»¶å表失败:", error); |
| | | ElMessage.error("å è½½æä»¶å表失败"); |
| | | } |
| | | }; |
| | | |
| | | // ç¶ææ å° |
| | | const getStatusText = (status) => { |
| | | const map = { |
| | | 0: 'å¾
å¤ç', |
| | | 1: 'å¤çä¸', |
| | | 2: '已宿', |
| | | 3: '失败' |
| | | }; |
| | | return map[status] || 'æªç¥'; |
| | | }; |
| | | |
| | | const getStatusType = (status) => { |
| | | const map = { |
| | | 0: 'info', |
| | | 1: 'warning', |
| | | 2: 'success', |
| | | 3: 'danger' |
| | | }; |
| | | return map[status] || 'info'; |
| | | }; |
| | | |
| | | // éæ°å¤çåéåçæä»¶ |
| | | const reprocessFile = async (row) => { |
| | | try { |
| | | await reprocessVector(row.id); |
| | | ElMessage.success("已鿰æäº¤åéåä»»å¡"); |
| | | setTimeout(() => { |
| | | loadFileList(); |
| | | }, 1000); |
| | | } catch (error) { |
| | | console.error("éæ°å¤ç失败:", error); |
| | | ElMessage.error("éæ°å¤ç失败"); |
| | | } |
| | | }; |
| | | |
| | | // å 餿件 |
| | | const deleteFile = async (row) => { |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | "ç¡®å®è¦å é¤è¯¥æä»¶å?å é¤åå°æ æ³æ¢å¤åéæ°æ®", |
| | | "å é¤ç¡®è®¤", |
| | | { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning" |
| | | } |
| | | ); |
| | | |
| | | await deleteKnowledgeBaseFiles([row.id]); |
| | | ElMessage.success("å 餿å"); |
| | | loadFileList(); |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | console.error("å é¤æä»¶å¤±è´¥:", error); |
| | | ElMessage.error("å é¤æä»¶å¤±è´¥"); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | ``` |
| | | |
| | | ### 5.2 ç¥è¯åºé®çæµç¨ |
| | | |
| | | #### æµç¨å¾ |
| | | ``` |
| | | ç¨æ·éæ©ç¥è¯åº |
| | | â |
| | | è¾å
¥é®é¢å¹¶æäº¤ |
| | | â |
| | | å端çæmemoryId(ç¨äºä¼è¯ä¸ä¸æ) |
| | | â |
| | | è°ç¨ /ai/knowledge/chat (æµå¼æ¥å£) |
| | | â |
| | | å端å¤ç: |
| | | - 对é®é¢è¿è¡åéå |
| | | - å¨Pinecone䏿£ç´¢ç¸å
³åç |
| | | - æå»ºä¸ä¸æPrompt |
| | | - è°ç¨LLMçæåç |
| | | â |
| | | æµå¼è¿åAIåç |
| | | â |
| | | åç«¯å®æ¶æ¾ç¤ºåçå
容 |
| | | â |
| | | èªå¨æ»å¨å°åºé¨ |
| | | ``` |
| | | |
| | | #### 代ç å®ç° |
| | | |
| | | ```vue |
| | | <template> |
| | | <div class="knowledge-chat"> |
| | | <div class="chat-header"> |
| | | <el-tag type="success">å½åç¥è¯åº: {{ currentKnowledgeBase?.title }}</el-tag> |
| | | </div> |
| | | |
| | | <!-- 对è¯åºå --> |
| | | <div class="chat-messages" ref="chatMessagesRef"> |
| | | <div |
| | | v-for="(msg, index) in messages" |
| | | :key="index" |
| | | :class="['message', msg.role]" |
| | | > |
| | | <div class="message-role">{{ msg.role === 'user' ? 'æ' : 'AI婿' }}</div> |
| | | <div class="message-content">{{ msg.content }}</div> |
| | | </div> |
| | | <div v-if="chatLoading" class="message assistant"> |
| | | <div class="message-role">AI婿</div> |
| | | <div class="message-content typing">æ£å¨æèä¸...</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è¾å
¥æ¡ --> |
| | | <div class="chat-input"> |
| | | <el-input |
| | | v-model="inputQuestion" |
| | | placeholder="请è¾å
¥é®é¢,æå车åé" |
| | | @keyup.enter="sendMessage" |
| | | :disabled="chatLoading" |
| | | > |
| | | <template #append> |
| | | <el-button @click="sendMessage" :loading="chatLoading">åé</el-button> |
| | | </template> |
| | | </el-input> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { nextTick } from 'vue'; |
| | | import { getToken } from "@/utils/auth"; |
| | | |
| | | const messages = ref([]); |
| | | const inputQuestion = ref(""); |
| | | const chatLoading = ref(false); |
| | | const memoryId = ref(""); |
| | | const chatMessagesRef = ref(); |
| | | |
| | | // æå¼é®çå¼¹çª |
| | | const openChatDialog = (row) => { |
| | | currentKnowledgeBase.value = row; |
| | | chatDialogVisible.value = true; |
| | | memoryId.value = crypto.randomUUID(); // çæå¯ä¸ä¼è¯ID |
| | | messages.value = []; |
| | | inputQuestion.value = ""; |
| | | }; |
| | | |
| | | // åéæ¶æ¯ |
| | | const sendMessage = async () => { |
| | | if (!inputQuestion.value.trim()) { |
| | | ElMessage.warning("请è¾å
¥é®é¢"); |
| | | return; |
| | | } |
| | | |
| | | const question = inputQuestion.value.trim(); |
| | | |
| | | // æ·»å ç¨æ·æ¶æ¯ |
| | | messages.value.push({ |
| | | role: 'user', |
| | | content: question |
| | | }); |
| | | |
| | | inputQuestion.value = ""; |
| | | chatLoading.value = true; |
| | | |
| | | // æ»å¨å°åºé¨ |
| | | await nextTick(); |
| | | scrollToBottom(); |
| | | |
| | | try { |
| | | // æµå¼è¯·æ± |
| | | const response = await fetch('/api/ai/knowledge/chat', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'Authorization': 'Bearer ' + getToken() |
| | | }, |
| | | body: JSON.stringify({ |
| | | knowledgeBaseId: currentKnowledgeBase.value.id, |
| | | memoryId: memoryId.value, |
| | | question: question |
| | | }) |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | throw new Error('请æ±å¤±è´¥'); |
| | | } |
| | | |
| | | // å¤çSSEæµå¼ååº |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let aiContent = ''; |
| | | |
| | | messages.value.push({ role: 'assistant', content: '' }); |
| | | |
| | | while (true) { |
| | | const { done, value } = await reader.read(); |
| | | if (done) break; |
| | | |
| | | const text = decoder.decode(value); |
| | | aiContent += text; |
| | | messages.value[messages.value.length - 1].content = aiContent; |
| | | |
| | | // æ»å¨å°åºé¨ |
| | | await nextTick(); |
| | | scrollToBottom(); |
| | | } |
| | | } catch (error) { |
| | | console.error("é®ç请æ±å¤±è´¥:", error); |
| | | ElMessage.error("é®ç请æ±å¤±è´¥,请ç¨åéè¯"); |
| | | messages.value.push({ |
| | | role: 'assistant', |
| | | content: 'æ±æ,åçäºé误,请ç¨åéè¯' |
| | | }); |
| | | } finally { |
| | | chatLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // æ»å¨å°åºé¨ |
| | | const scrollToBottom = () => { |
| | | if (chatMessagesRef.value) { |
| | | chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight; |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .knowledge-chat { |
| | | display: flex; |
| | | flex-direction: column; |
| | | height: 500px; |
| | | } |
| | | |
| | | .chat-messages { |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | padding: 16px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .message { |
| | | margin-bottom: 16px; |
| | | max-width: 80%; |
| | | } |
| | | |
| | | .message.user { |
| | | margin-left: auto; |
| | | text-align: right; |
| | | } |
| | | |
| | | .message.assistant { |
| | | margin-right: auto; |
| | | } |
| | | |
| | | .message-role { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .message-content { |
| | | display: inline-block; |
| | | padding: 10px 14px; |
| | | border-radius: 8px; |
| | | line-height: 1.6; |
| | | word-wrap: break-word; |
| | | white-space: pre-wrap; |
| | | } |
| | | |
| | | .message.user .message-content { |
| | | background: #409eff; |
| | | color: white; |
| | | } |
| | | |
| | | .message.assistant .message-content { |
| | | background: white; |
| | | color: #303133; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .typing { |
| | | animation: typing 1.5s infinite; |
| | | } |
| | | |
| | | @keyframes typing { |
| | | 0%, 50%, 100% { opacity: 1; } |
| | | 25%, 75% { opacity: 0.5; } |
| | | } |
| | | </style> |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## å
ãå
³é®å®ç°ç»è |
| | | |
| | | ### 6.1 æä»¶ä¸ä¼ é
ç½® |
| | | |
| | | ```javascript |
| | | // ä¸ä¼ å°å |
| | | const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload"; |
| | | |
| | | // 请æ±å¤´(å¿
é¡»æºå¸¦Token) |
| | | const uploadHeaders = { |
| | | Authorization: "Bearer " + getToken() |
| | | }; |
| | | |
| | | // æ¯æçæä»¶ç±»å |
| | | const acceptTypes = '.txt,.md,.docx,.xlsx,.xls,.pdf'; |
| | | |
| | | // æä»¶å¤§å°éå¶ |
| | | const maxSize = 50 * 1024 * 1024; // 50MB |
| | | ``` |
| | | |
| | | ### 6.2 åéåç¶æè½®è¯¢ |
| | | |
| | | ```javascript |
| | | // å¼å§è½®è¯¢åéåç¶æ |
| | | const startVectorStatusPolling = () => { |
| | | const timer = setInterval(async () => { |
| | | const res = await getVectorStatus(currentKnowledgeBase.value.id); |
| | | const hasProcessing = res.data.some(item => item.vectorStatus === 1); |
| | | |
| | | if (!hasProcessing) { |
| | | clearInterval(timer); |
| | | } |
| | | |
| | | fileList.value = res.data; |
| | | }, 3000); // æ¯3ç§è½®è¯¢ä¸æ¬¡ |
| | | }; |
| | | ``` |
| | | |
| | | ### 6.3 æµå¼ååºå¤ç |
| | | |
| | | ```javascript |
| | | // ä½¿ç¨ Fetch API å¤çæµå¼ååº |
| | | const response = await fetch('/api/ai/knowledge/chat', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'Authorization': 'Bearer ' + getToken() |
| | | }, |
| | | body: JSON.stringify({ |
| | | knowledgeBaseId: 10, |
| | | memoryId: "session-xxx", |
| | | question: "é®é¢å
容" |
| | | }) |
| | | }); |
| | | |
| | | // è·åå¯è¯»æµ |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | |
| | | // éåè¯»åæ°æ® |
| | | while (true) { |
| | | const { done, value } = await reader.read(); |
| | | if (done) break; |
| | | |
| | | const text = decoder.decode(value); |
| | | // å¤çææ¬å |
| | | processText(text); |
| | | } |
| | | ``` |
| | | |
| | | ### 6.4 ä¼è¯ç®¡ç |
| | | |
| | | ```javascript |
| | | // çæå¯ä¸ä¼è¯ID |
| | | const memoryId = crypto.randomUUID(); |
| | | |
| | | // æä½¿ç¨æ¶é´æ³ |
| | | const memoryId = 'kb-chat-' + Date.now(); |
| | | |
| | | // ä¼è¯IDç¨äº: |
| | | // 1. ä¿æå¯¹è¯ä¸ä¸æ |
| | | // 2. æ¯æå¤è½®å¯¹è¯ |
| | | // 3. æ¥è¯¢åå²è®°å½ |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãç¶æç®¡ç |
| | | |
| | | ### 7.1 åéåç¶æå®ä¹ |
| | | |
| | | | ç¶æå¼ | ç¶æåç§° | 说æ | æ ç¾é¢è² | |
| | | |--------|----------|------|----------| |
| | | | 0 | å¾
å¤ç | æä»¶å·²ä¸ä¼ ,çå¾
åéåå¤ç | info(ç°è²) | |
| | | | 1 | å¤çä¸ | æ£å¨è¿è¡åéåçå¤ç | warning(æ©è²) | |
| | | | 2 | 已宿 | åéå宿,å¯è¿è¡æ£ç´¢é®ç | success(绿è²) | |
| | | | 3 | 失败 | åéå失败,ééæ°å¤ç | danger(红è²) | |
| | | |
| | | ### 7.2 ç¥è¯ç±»åé
ç½® |
| | | |
| | | ```javascript |
| | | // 使ç¨åå
¸é
ç½®ç¥è¯ç±»å |
| | | const { knowledge_type } = proxy.useDict("knowledge_type"); |
| | | |
| | | // ç¤ºä¾æ°æ® |
| | | const knowledgeTypeOptions = [ |
| | | { value: 'contract', label: 'ååç¥è¯', elTagType: 'success' }, |
| | | { value: 'approval', label: 'å®¡æ¹æµç¨', elTagType: 'warning' }, |
| | | { value: 'solution', label: 'è§£å³æ¹æ¡', elTagType: 'primary' }, |
| | | { value: 'experience', label: 'ç»éªå享', elTagType: 'info' }, |
| | | { value: 'guide', label: 'æä½æå', elTagType: 'danger' } |
| | | ]; |
| | | ``` |
| | | |
| | | ### 7.3 è§£å³æçæ å° |
| | | |
| | | ```javascript |
| | | const efficiencyMap = { |
| | | high: { label: 'æ¾èæå', color: 'success', score: 40, time: '2-3天' }, |
| | | medium: { label: 'ä¸è¬æå', color: 'warning', score: 25, time: '1-2天' }, |
| | | low: { label: '轻微æå', color: 'info', score: 15, time: '0.5-1天' } |
| | | }; |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## å
«ãæ ·å¼è®¾è®¡ |
| | | |
| | | ### 8.1 æä»¶ç®¡çæ ·å¼ |
| | | |
| | | ```css |
| | | .file-manager { |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .upload-section { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | ``` |
| | | |
| | | ### 8.2 é®çç颿 ·å¼ |
| | | |
| | | ```css |
| | | .knowledge-chat { |
| | | display: flex; |
| | | flex-direction: column; |
| | | height: 500px; |
| | | } |
| | | |
| | | .chat-messages { |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | padding: 16px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .message { |
| | | margin-bottom: 16px; |
| | | max-width: 80%; |
| | | } |
| | | |
| | | .message.user { |
| | | margin-left: auto; |
| | | text-align: right; |
| | | } |
| | | |
| | | .message.assistant { |
| | | margin-right: auto; |
| | | } |
| | | |
| | | .message-content { |
| | | display: inline-block; |
| | | padding: 10px 14px; |
| | | border-radius: 8px; |
| | | line-height: 1.6; |
| | | word-wrap: break-word; |
| | | white-space: pre-wrap; |
| | | } |
| | | |
| | | .message.user .message-content { |
| | | background: #409eff; |
| | | color: white; |
| | | } |
| | | |
| | | .message.assistant .message-content { |
| | | background: white; |
| | | color: #303133; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¹ã注æäºé¡¹ |
| | | |
| | | ### 9.1 æä»¶ä¸ä¼ |
| | | |
| | | 1. **å¿
é¡»è°ç¨ä¿åæ¥å£**: ä¸ä¼ æååå¿
é¡»è°ç¨ `/knowledgeBase/file/save` æè½è§¦ååéå |
| | | 2. **æä»¶ç±»åéå¶**: åªæ¯æ txtãmdãdocxãxlsxãxlsãpdf æ ¼å¼ |
| | | 3. **æä»¶å¤§å°éå¶**: åæä»¶æå¤§ 50MB |
| | | 4. **弿¥å¤ç**: åé忝弿¥å¤ç,ä¸ä¼é»å¡ç¨æ·æä½ |
| | | |
| | | ### 9.2 åéåç¶æ |
| | | |
| | | 1. **ç¶æè½®è¯¢**: 建议æ¯3-5ç§è½®è¯¢ä¸æ¬¡ç¶æ |
| | | 2. **åæ¢è½®è¯¢**: 彿ææä»¶ç¶æé½ä¸æ¯"å¤çä¸"æ¶åæ¢è½®è¯¢ |
| | | 3. **失败å¤ç**: ç¶æä¸º"失败"æ¶å¯ç¹å»"éæ°å¤ç"æé® |
| | | |
| | | ### 9.3 ç¥è¯åºé®ç |
| | | |
| | | 1. **ä¼è¯ID**: æ¯æ¬¡æå¼é®çå¼¹çªéè¦çææ°ç memoryId |
| | | 2. **æµå¼å¤ç**: ä½¿ç¨ Fetch API å¤çæµå¼ååº,䏿¯æ axios |
| | | 3. **é误å¤ç**: éè¦å¤çç½ç»é误åAIååºé误 |
| | | 4. **èªå¨æ»å¨**: æ¯æ¬¡æ¶å°æ°æ¶æ¯èªå¨æ»å¨å°åºé¨ |
| | | |
| | | ### 9.4 æ°æ®ä¸è´æ§ |
| | | |
| | | 1. **å é¤ç¥è¯åº**: éè¦åæ¶å é¤å
³èçæä»¶ååéæ°æ® |
| | | 2. **å 餿件**: å 餿件æ¶åæ¥å é¤åéåºä¸çç¸å
³åç |
| | | 3. **å·æ°å表**: æä»¶æä½åéè¦å·æ°ç¥è¯åºå表,æ´æ°æä»¶æ°éååçæ°é |
| | | |
| | | --- |
| | | |
| | | ## åãæµè¯æ£æ¥æ¸
å |
| | | |
| | | ### 10.1 ç¥è¯åºç®¡ç |
| | | |
| | | - [ ] æ°å¢ç¥è¯åºæå |
| | | - [ ] ç¼è¾ç¥è¯åºæå |
| | | - [ ] å é¤ç¥è¯åºæå(å个/æ¹é) |
| | | - [ ] æç´¢åè½æ£å¸¸(ææ é¢ãç±»å) |
| | | - [ ] å页åè½æ£å¸¸ |
| | | - [ ] 导åºåè½æ£å¸¸ |
| | | |
| | | ### 10.2 æä»¶ä¸ä¼ |
| | | |
| | | - [ ] åæä»¶ä¸ä¼ æå |
| | | - [ ] 夿件ä¸ä¼ æå |
| | | - [ ] æä»¶ç±»åæ ¡éªæ£å¸¸ |
| | | - [ ] æä»¶å¤§å°æ ¡éªæ£å¸¸ |
| | | - [ ] ä¿åæä»¶å
³èæå |
| | | - [ ] åéåç¶ææ£ç¡®æ¾ç¤º |
| | | |
| | | ### 10.3 æä»¶ç®¡ç |
| | | |
| | | - [ ] æ¥çæä»¶å表æ£å¸¸ |
| | | - [ ] åéåç¶æè½®è¯¢æ£å¸¸ |
| | | - [ ] éæ°å¤ç失败æä»¶æå |
| | | - [ ] å 餿件æå |
| | | - [ ] æä»¶é¢è§/ä¸è½½æ£å¸¸ |
| | | |
| | | ### 10.4 ç¥è¯åºé®ç |
| | | |
| | | - [ ] é®çå¼¹çªæå¼æ£å¸¸ |
| | | - [ ] åéé®é¢æå |
| | | - [ ] æµå¼ååºæ¾ç¤ºæ£å¸¸ |
| | | - [ ] å¤è½®å¯¹è¯æ£å¸¸ |
| | | - [ ] èªå¨æ»å¨å°åºé¨ |
| | | - [ ] é误å¤çæ£å¸¸ |
| | | |
| | | --- |
| | | |
| | | ## åä¸ã常è§é®é¢ |
| | | |
| | | ### Q1: æä»¶ä¸ä¼ åæ²¡æè§¦ååéå? |
| | | |
| | | **A**: æ£æ¥æ¯å¦è°ç¨äº `/knowledgeBase/file/save` æ¥å£ãä¸ä¼ æååå¿
é¡»è°ç¨æ¤æ¥å£æè½è§¦ååéåã |
| | | |
| | | ### Q2: åéåç¶æä¸ç´æ¯"å¤çä¸"? |
| | | |
| | | **A**: å¯è½åå : |
| | | 1. å尿塿ªå¯å¨ |
| | | 2. Embedding模åè°ç¨å¤±è´¥ |
| | | 3. Pineconeè¿æ¥å¤±è´¥ |
| | | |
| | | 建议æ¥çåå°æ¥å¿ææ¥é®é¢ã |
| | | |
| | | ### Q3: é®çè¿å空å
容? |
| | | |
| | | **A**: å¯è½åå : |
| | | 1. ç¥è¯åºä¸æ²¡ææä»¶ |
| | | 2. æä»¶æªå®æåéå |
| | | 3. æ£ç´¢ç¸ä¼¼åº¦ä½äºéå¼ |
| | | |
| | | å»ºè®®æ£æ¥æä»¶æ°éååéåç¶æã |
| | | |
| | | ### Q4: æµå¼ååºæ¾ç¤ºä¹±ç ? |
| | | |
| | | **A**: ç¡®ä¿è¯·æ±å¤´å
嫿£ç¡®çç¼ç 设置: |
| | | ```javascript |
| | | headers: { |
| | | 'Content-Type': 'application/json' |
| | | } |
| | | ``` |
| | | |
| | | ### Q5: å¦ä½è°è¯æµå¼æ¥å£? |
| | | |
| | | **A**: ä½¿ç¨æµè§å¨å¼åè
å·¥å
·: |
| | | 1. æå¼ Network æ ç¾ |
| | | 2. æ¾å° `/ai/knowledge/chat` è¯·æ± |
| | | 3. æ¥ç Response æ ç¾,å¯ä»¥çå°æµå¼è¿åçå
容 |
| | | |
| | | --- |
| | | |
| | | ## åäºãä¼å建议 |
| | | |
| | | ### 12.1 æ§è½ä¼å |
| | | |
| | | 1. **èææ»å¨**: æ¶æ¯å表è¶
è¿100æ¡æ¶ä½¿ç¨èææ»å¨ |
| | | 2. **é²æèæµ**: æç´¢è¾å
¥ä½¿ç¨é²æ,ç¶æè½®è¯¢ä½¿ç¨èæµ |
| | | 3. **æå è½½**: æä»¶åè¡¨ä½¿ç¨æå è½½ |
| | | |
| | | ### 12.2 ç¨æ·ä½éªä¼å |
| | | |
| | | 1. **è¿åº¦æç¤º**: åéåæ¶æ¾ç¤ºè¿åº¦æ¡ |
| | | 2. **å¿«æ·é®**: æ¯æå¿«æ·é®æä½(å¦ Ctrl+Enter åé) |
| | | 3. **åå²è®°å½**: æ¯ææ¥çåå²é®çè®°å½ |
| | | 4. **导åºå¯¹è¯**: æ¯æå¯¼åºå¯¹è¯å
容 |
| | | |
| | | ### 12.3 åè½æ©å± |
| | | |
| | | 1. **æä»¶é¢è§**: æ¯æå¨çº¿é¢è§æä»¶å
容 |
| | | 2. **æ¹éæä½**: æ¯ææ¹éå é¤ãæ¹ééæ°å¤ç |
| | | 3. **åéåé
ç½®**: å
è®¸ç¨æ·é
ç½®åç大å°ãéå å¤§å° |
| | | 4. **ç¸ä¼¼åº¦éå¼**: å
è®¸ç¨æ·è°æ´æ£ç´¢ç¸ä¼¼åº¦éå¼ |
| | | |
| | | --- |
| | | |
| | | ## åä¸ãæ´æ°æ¥å¿ |
| | | |
| | | ### v1.0.0 (2026-06-08) |
| | | - â
宿ç¥è¯åºCRUDåè½ |
| | | - â
宿æä»¶ä¸ä¼ ä¸åéååè½ |
| | | - â
宿ç¥è¯åºé®çåè½ |
| | | - â
宿æä»¶ç®¡çåè½ |
| | | - â
宿åéåç¶ææ¾ç¤º |
| | | |
| | | ### v1.1.0 (计åä¸) |
| | | - ð² æ·»å åå²è®°å½æ¥è¯¢ |
| | | - ð² æ·»å æ¹éæä½åè½ |
| | | - ð² æ·»å æä»¶é¢è§åè½ |
| | | - ð² ä¼ååéåè¿åº¦æ¾ç¤º |
| | |
| | | import request from "@/utils/request"; |
| | | import { getToken } from '@/utils/auth'; |
| | | |
| | | // æ¥è¯¢ç¥è¯åºå表 |
| | | /** |
| | | * ç¥è¯åºç®¡çæ¥å£ |
| | | * ä¼ åè§è: |
| | | * - GET请æ±: ä½¿ç¨ params |
| | | * - POST请æ±: ä½¿ç¨ data |
| | | * - DELETE请æ±: ä½¿ç¨ data |
| | | * - æµå¼è¯·æ±: ä½¿ç¨ Fetch API |
| | | */ |
| | | |
| | | // ==================== ç¥è¯åºCRUDæ¥å£ ==================== |
| | | |
| | | /** |
| | | * æ¥è¯¢ç¥è¯åºå表 |
| | | * @param {Object} query - æ¥è¯¢åæ° |
| | | * @param {number} query.current - å½å页ç (å¿
å¡«) |
| | | * @param {number} query.size - æ¯é¡µæ¡æ°(å¿
å¡«) |
| | | * @param {string} [query.title] - ç¥è¯æ é¢(å¯é,æ¨¡ç³æç´¢) |
| | | * @param {string} [query.type] - ç¥è¯ç±»å(å¯é,精确å¹é
) |
| | | * @returns {Promise} |
| | | */ |
| | | export function listKnowledgeBase(query) { |
| | | return request({ |
| | | url: "/knowledgeBase/getList", |
| | | method: "get", |
| | | params: query, |
| | | params: query, // GET请æ±ä½¿ç¨params |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢ç¥è¯åºè¯¦ç» |
| | | // export function getKnowledgeBase(knowledgeBaseId) { |
| | | // return request({ |
| | | // url: "/collaborativeApproval/knowledgeBase/" + knowledgeBaseId, |
| | | // method: "get", |
| | | // }); |
| | | // } |
| | | |
| | | // æ°å¢ç¥è¯åº |
| | | /** |
| | | * æ°å¢ç¥è¯åº |
| | | * @param {Object} data - ç¥è¯åºæ°æ® |
| | | * @param {string} data.title - ç¥è¯æ é¢(å¿
å¡«) |
| | | * @param {string} data.type - ç¥è¯ç±»å(å¿
å¡«) |
| | | * @param {string} [data.scenario] - éç¨åºæ¯(å¯é) |
| | | * @param {string} [data.efficiency] - è§£å³æç(å¯é) |
| | | * @param {string} data.problem - é®é¢æè¿°(å¿
å¡«) |
| | | * @param {string} data.solution - è§£å³æ¹æ¡(å¿
å¡«) |
| | | * @param {string} [data.keyPoints] - å
³é®è¦ç¹(å¯é) |
| | | * @param {string} [data.creator] - å建人(å¯é) |
| | | * @param {number} [data.usageCount=0] - ä½¿ç¨æ¬¡æ°(å¯é) |
| | | * @returns {Promise} |
| | | */ |
| | | export function addKnowledgeBase(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/add", |
| | | method: "post", |
| | | data: data, |
| | | data: data, // POST请æ±ä½¿ç¨data |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹ç¥è¯åº |
| | | /** |
| | | * ä¿®æ¹ç¥è¯åº |
| | | * @param {Object} data - ç¥è¯åºæ°æ® |
| | | * @param {number} data.id - ç¥è¯åºID(å¿
å¡«) |
| | | * @param {string} data.title - ç¥è¯æ é¢(å¿
å¡«) |
| | | * @param {string} data.type - ç¥è¯ç±»å(å¿
å¡«) |
| | | * @returns {Promise} |
| | | */ |
| | | export function updateKnowledgeBase(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/update", |
| | | method: "post", |
| | | data: data, |
| | | data: data, // POST请æ±ä½¿ç¨data |
| | | }); |
| | | } |
| | | |
| | | // å é¤ç¥è¯åº |
| | | export function delKnowledgeBase(query) { |
| | | /** |
| | | * å é¤ç¥è¯åº(æ¯ææ¹éå é¤) |
| | | * @param {number[]} ids - ç¥è¯åºIDæ°ç» |
| | | * @returns {Promise} |
| | | */ |
| | | export function delKnowledgeBase(ids) { |
| | | return request({ |
| | | url: "/knowledgeBase/delete", |
| | | method: "delete", |
| | | data: query, |
| | | data: ids, // DELETE请æ±ä½¿ç¨dataä¼ éæ°ç» |
| | | }); |
| | | } |
| | | |
| | | // æ¹éå é¤ç¥è¯åº |
| | | export function delKnowledgeBaseBatch(knowledgeBaseIds) { |
| | | // ==================== æä»¶ç®¡çæ¥å£ ==================== |
| | | |
| | | /** |
| | | * æ¥è¯¢ç¥è¯åºæä»¶åéåç¶æ |
| | | * @param {number} knowledgeBaseId - ç¥è¯åºID |
| | | * @returns {Promise} è¿åæä»¶å表ååéåç¶æ |
| | | */ |
| | | export function getVectorStatus(knowledgeBaseId) { |
| | | return request({ |
| | | url: "/knowledgeBase/batch", |
| | | method: "delete", |
| | | data: knowledgeBaseIds, |
| | | url: `/knowledgeBase/vector/status/${knowledgeBaseId}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * ä¿åç¥è¯åºæä»¶å
³è(触ååéå) |
| | | * @param {Object} data - æä»¶å
³èæ°æ® |
| | | * @param {number} data.knowledgeBaseId - ç¥è¯åºID(å¿
å¡«) |
| | | * @param {number[]} data.storageBlobIds - æä»¶blob IDæ°ç»(å¿
å¡«) |
| | | * @returns {Promise} |
| | | */ |
| | | export function saveKnowledgeBaseFiles(data) { |
| | | return request({ |
| | | url: "/knowledgeBase/file/save", |
| | | method: "post", |
| | | data: data, // POST请æ±ä½¿ç¨data |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * å é¤ç¥è¯åºæä»¶ |
| | | * @param {number[]} ids - åéè®°å½IDæ°ç» |
| | | * @returns {Promise} |
| | | */ |
| | | export function deleteKnowledgeBaseFile(ids) { |
| | | return request({ |
| | | url: "/knowledgeBase/file/delete", |
| | | method: "delete", |
| | | data: ids, // DELETE请æ±ä½¿ç¨dataä¼ éæ°ç» |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * éæ°åéåæä»¶ |
| | | * @param {number} vectorId - åéè®°å½ID |
| | | * @returns {Promise} |
| | | */ |
| | | export function reprocessVector(vectorId) { |
| | | return request({ |
| | | url: `/knowledgeBase/vector/reprocess/${vectorId}`, |
| | | method: "post", |
| | | }); |
| | | } |
| | | |
| | | // ==================== ç¥è¯é®çæ¥å£ ==================== |
| | | |
| | | /** |
| | | * ç¥è¯åºé®ç(æµå¼) |
| | | * å端æ¥å£: POST /ai/knowledge/chat |
| | | * ååºç±»å: text/stream;charset=utf-8 (Spring Flux<String>) |
| | | * |
| | | * @param {Object} data - é®çåæ° |
| | | * @param {number} data.knowledgeBaseId - ç¥è¯åºID(å¿
å¡«) |
| | | * @param {string} data.memoryId - ä¼è¯ID(å¿
å¡«,ç¨äºä¿æä¸ä¸æ) |
| | | * @param {string} data.question - ç¨æ·é®é¢(å¿
å¡«) |
| | | * @returns {Promise<Response>} è¿åFetch Response对象 |
| | | */ |
| | | export function knowledgeChat(data) { |
| | | const token = getToken(); |
| | | return fetch(import.meta.env.VITE_APP_BASE_API + '/ai/knowledge/chat', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'Authorization': 'Bearer ' + token |
| | | }, |
| | | body: JSON.stringify(data) |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * æ¥è¯¢ç¥è¯åºé®çåå² |
| | | * @param {string} memoryId - ä¼è¯ID |
| | | * @returns {Promise} |
| | | */ |
| | | export function getKnowledgeHistory(memoryId) { |
| | | return request({ |
| | | url: `/ai/knowledge/history/${memoryId}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** 仿¬¾å°è´¦ - 仿¬¾ç»è®°å表 */ |
| | | export function registrationList(params) { |
| | | return request({ |
| | | url: "/purchase/report/registration", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | |
| | | :row-key="rowKey" |
| | | :style="tableStyle" |
| | | tooltip-effect="dark" |
| | | :tooltip-options="{ appendTo: 'body' }" |
| | | :tooltip-options="{ popperOptions: { strategy: 'absolute' } }" |
| | | :expand-row-keys="expandRowKeys" |
| | | :show-summary="isShowSummary" |
| | | :summary-method="summaryMethod" |
| | |
| | | </div> |
| | | </div> |
| | | </FormDialog> |
| | | |
| | | <!-- æä»¶ç®¡çå¼¹çª --> |
| | | <FormDialog |
| | | v-model="filesDialogVisible" |
| | | title="æä»¶ç®¡ç" |
| | | :width="'900px'" |
| | | @close="closeFilesDialog" |
| | | @confirm="closeFilesDialog" |
| | | @cancel="closeFilesDialog" |
| | | > |
| | | <div class="file-manager"> |
| | | <!-- æä»¶ä¸ä¼ --> |
| | | <div class="upload-section"> |
| | | <el-upload |
| | | :action="uploadUrl" |
| | | :headers="uploadHeaders" |
| | | :on-success="handleUploadSuccess" |
| | | :on-error="handleUploadError" |
| | | :before-upload="beforeUpload" |
| | | name="files" |
| | | multiple |
| | | :show-file-list="false" |
| | | accept=".txt,.md,.docx,.xlsx,.xls,.pdf" |
| | | > |
| | | <el-button type="primary">ä¸ä¼ æä»¶</el-button> |
| | | </el-upload> |
| | | <el-button |
| | | type="success" |
| | | @click="saveFiles" |
| | | :disabled="uploadedBlobIds.length === 0" |
| | | :loading="savingFiles" |
| | | style="margin-left: 10px" |
| | | > |
| | | ä¿åæä»¶å
³è |
| | | </el-button> |
| | | <el-button |
| | | v-if="uploadedBlobIds.length > 0" |
| | | type="text" |
| | | @click="clearUploadedFiles" |
| | | style="margin-left: 10px" |
| | | > |
| | | æ¸
空å¾
ä¿åå表 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- å¾
ä¿åçæä»¶å表 --> |
| | | <div v-if="uploadedBlobIds.length > 0" class="uploaded-list"> |
| | | <div class="uploaded-tip"> |
| | | <el-icon style="color: #409eff"><InfoFilled /></el-icon> |
| | | <span>å·²ä¸ä¼ {{ uploadedBlobIds.length }} 个æä»¶,请ç¹å»"ä¿åæä»¶å
³è"æé®è§¦ååéåå¤ç</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æä»¶å表ä¸åéåç¶æ --> |
| | | <el-table :data="fileList" style="margin-top: 20px" border> |
| | | <el-table-column prop="fileName" label="æä»¶å" show-overflow-tooltip /> |
| | | <el-table-column prop="fileType" label="æä»¶ç±»å" width="100" /> |
| | | <el-table-column label="åéåç¶æ" width="120"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getStatusType(row.vectorStatus)"> |
| | | {{ getStatusText(row.vectorStatus) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="chunkCount" label="åçæ°" width="100" align="center" /> |
| | | <el-table-column label="é误信æ¯" width="200" show-overflow-tooltip> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.vectorError" style="color: #f56c6c">{{ row.vectorError }}</span> |
| | | <span v-else style="color: #909399">-</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="createTime" label="ä¸ä¼ æ¶é´" width="180" /> |
| | | <el-table-column label="æä½" width="150" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | v-if="row.vectorStatus === 3" |
| | | type="text" |
| | | @click="reprocessFile(row)" |
| | | > |
| | | éæ°å¤ç |
| | | </el-button> |
| | | <el-button type="text" @click="deleteFile(row)" style="color: #f56c6c"> |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </FormDialog> |
| | | |
| | | <!-- ç¥è¯åºé®çå¼¹çª --> |
| | | <FormDialog |
| | | v-model="chatDialogVisible" |
| | | title="ç¥è¯åºé®ç" |
| | | :width="'800px'" |
| | | @close="closeChatDialog" |
| | | @confirm="closeChatDialog" |
| | | @cancel="closeChatDialog" |
| | | > |
| | | <div class="knowledge-chat"> |
| | | <div class="chat-header"> |
| | | <el-tag type="success">å½åç¥è¯åº: {{ currentKnowledgeBase?.title }}</el-tag> |
| | | </div> |
| | | |
| | | <!-- 对è¯åºå --> |
| | | <div class="chat-messages" ref="chatMessagesRef"> |
| | | <div |
| | | v-for="(msg, index) in messages" |
| | | :key="index" |
| | | :class="['message', msg.role]" |
| | | > |
| | | <div class="message-role">{{ msg.role === 'user' ? 'æ' : 'AI婿' }}</div> |
| | | <div class="message-content">{{ msg.content }}</div> |
| | | </div> |
| | | <div v-if="chatLoading" class="message assistant"> |
| | | <div class="message-role">AI婿</div> |
| | | <div class="message-content typing">æ£å¨æèä¸...</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è¾å
¥æ¡ --> |
| | | <div class="chat-input"> |
| | | <el-input |
| | | v-model="inputQuestion" |
| | | placeholder="请è¾å
¥é®é¢,æå车åé(Ctrl+Enterå¿«æ·åé)" |
| | | @keyup.enter="sendMessage" |
| | | :disabled="chatLoading" |
| | | > |
| | | <template #append> |
| | | <el-button @click="sendMessage" :loading="chatLoading">åé</el-button> |
| | | </template> |
| | | </el-input> |
| | | <div class="chat-actions"> |
| | | <el-button type="text" size="small" @click="clearMessages">æ¸
空对è¯</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </FormDialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch } from "vue"; |
| | | import { Search, InfoFilled } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch, nextTick } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue'; |
| | | import { listKnowledgeBase, delKnowledgeBase,addKnowledgeBase,updateKnowledgeBase } from "@/api/collaborativeApproval/knowledgeBase.js"; |
| | | import { |
| | | listKnowledgeBase, |
| | | delKnowledgeBase, |
| | | addKnowledgeBase, |
| | | updateKnowledgeBase, |
| | | getVectorStatus, |
| | | reprocessVector, |
| | | saveKnowledgeBaseFiles, |
| | | deleteKnowledgeBaseFile, |
| | | knowledgeChat |
| | | } from "@/api/collaborativeApproval/knowledgeBase.js"; |
| | | import useUserStore from '@/store/modules/user'; |
| | | import { userListNoPageByTenantId } from '@/api/system/user.js'; |
| | | import { getToken } from "@/utils/auth"; |
| | | |
| | | // 表åéªè¯è§å |
| | | const rules = { |
| | |
| | | dialogTitle: "", |
| | | dialogType: "add", |
| | | viewDialogVisible: false, |
| | | currentKnowledge: {} |
| | | currentKnowledge: {}, |
| | | filesDialogVisible: false, |
| | | currentKnowledgeBase: null, |
| | | fileList: [], |
| | | uploadedBlobIds: [], |
| | | savingFiles: false, |
| | | vectorStatusTimer: null, // åéåç¶æè½®è¯¢å®æ¶å¨ |
| | | chatDialogVisible: false, |
| | | messages: [], |
| | | inputQuestion: "", |
| | | chatLoading: false, |
| | | memoryId: "" |
| | | }); |
| | | |
| | | const { |
| | |
| | | dialogTitle, |
| | | dialogType, |
| | | viewDialogVisible, |
| | | currentKnowledge |
| | | currentKnowledge, |
| | | filesDialogVisible, |
| | | currentKnowledgeBase, |
| | | fileList, |
| | | uploadedBlobIds, |
| | | savingFiles, |
| | | vectorStatusTimer, |
| | | chatDialogVisible, |
| | | messages, |
| | | inputQuestion, |
| | | chatLoading, |
| | | memoryId |
| | | } = toRefs(data); |
| | | |
| | | // 表åå¼ç¨ |
| | |
| | | // ç¨æ·ç¸å
³ |
| | | const userStore = useUserStore(); |
| | | const userList = ref([]); |
| | | // èå¤©æ¶æ¯å®¹å¨å¼ç¨ |
| | | const chatMessagesRef = ref(); |
| | | |
| | | // æä»¶ä¸ä¼ ç¸å
³ |
| | | const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload"; |
| | | const uploadHeaders = { Authorization: "Bearer " + getToken() }; |
| | | |
| | | // è¡¨æ ¼åé
ç½® |
| | | const tableColumn = ref([ |
| | |
| | | } |
| | | }, |
| | | { |
| | | label: "æä»¶æ°é", |
| | | prop: "fileCount", |
| | | width: 100, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "åçæ°é", |
| | | prop: "totalChunkCount", |
| | | width: 100, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "ä½¿ç¨æ¬¡æ°", |
| | | prop: "usageCount", |
| | | width: 100, |
| | |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | } |
| | | }, |
| | | { |
| | | name: "æä»¶", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openFilesDialog(row); |
| | | } |
| | | }, |
| | | { |
| | | name: "é®ç", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openChatDialog(row); |
| | | } |
| | | }, |
| | | { |
| | |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | listKnowledgeBase({...page.value, ...searchForm.value}) |
| | | |
| | | // â
GET请æ±ä½¿ç¨paramsä¼ å |
| | | listKnowledgeBase({ |
| | | current: page.value.current, |
| | | size: page.value.size, |
| | | title: searchForm.value.title, |
| | | type: searchForm.value.type |
| | | }) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | page.value.total = res.data.total; |
| | | // 妿å½å页æ°è¶
è¿æ»é¡µæ°ï¼éç½®å°ç¬¬1页并鿰æ¥è¯¢ |
| | | |
| | | // 妿å½å页æ°è¶
è¿æ»é¡µæ°,éç½®å°ç¬¬1页并鿰æ¥è¯¢ |
| | | const maxPage = Math.ceil(res.data.total / page.value.size) || 1; |
| | | if (page.value.current > maxPage && maxPage > 0) { |
| | | page.value.current = 1; |
| | | // éæ°æ¥è¯¢ç¬¬1é¡µæ°æ® |
| | | return getList(); |
| | | } |
| | | |
| | | tableData.value = res.data.records; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | | .catch(err => { |
| | | tableLoading.value = false; |
| | | console.error("æ¥è¯¢ç¥è¯åºå表失败:", err); |
| | | }); |
| | | }; |
| | | |
| | | // å页å¤ç |
| | |
| | | const submitForm = async () => { |
| | | try { |
| | | await formRef.value.validate(); |
| | | |
| | | // â
POST请æ±ä½¿ç¨dataä¼ å,æç¡®åæ°ç»æ |
| | | const formData = { |
| | | title: form.value.title, |
| | | type: form.value.type, |
| | | scenario: form.value.scenario || "", |
| | | efficiency: form.value.efficiency || "", |
| | | problem: form.value.problem, |
| | | solution: form.value.solution, |
| | | keyPoints: form.value.keyPoints || "", |
| | | creator: form.value.creator || "", |
| | | usageCount: form.value.usageCount || 0 |
| | | }; |
| | | |
| | | if (dialogType.value === "add") { |
| | | // æ°å¢ç¥è¯ |
| | | addKnowledgeBase({...form.value}).then(res => { |
| | | addKnowledgeBase(formData).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("æ·»å æå"); |
| | | closeKnowledgeDialog(); |
| | | getList(); |
| | | } |
| | | }).catch(err => { |
| | | ElMessage.error(err.msg); |
| | | }) |
| | | console.error("æ·»å ç¥è¯åºå¤±è´¥:", err); |
| | | ElMessage.error(err.msg || "æ·»å 失败"); |
| | | }); |
| | | } else { |
| | | updateKnowledgeBase({...form.value}).then(res => { |
| | | // æ´æ°ç¥è¯ - æ·»å idåæ° |
| | | updateKnowledgeBase({ |
| | | id: form.value.id, |
| | | ...formData |
| | | }).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("æ´æ°æå"); |
| | | closeKnowledgeDialog(); |
| | | getList(); |
| | | } |
| | | }).catch(err => { |
| | | ElMessage.error(err.msg); |
| | | }) |
| | | console.error("æ´æ°ç¥è¯åºå¤±è´¥:", err); |
| | | ElMessage.error(err.msg || "æ´æ°å¤±è´¥"); |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | console.error("表åéªè¯å¤±è´¥:", error); |
| | |
| | | return; |
| | | } |
| | | |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤", { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤,æ¯å¦ç¡®è®¤å é¤?", "å é¤", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | // console.log(selectedIds.value); |
| | | // â
DELETE请æ±ä½¿ç¨dataä¼ éIDæ°ç» |
| | | delKnowledgeBase(selectedIds.value).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("å 餿å"); |
| | | selectedIds.value = []; |
| | | getList(); |
| | | } |
| | | }) |
| | | }).catch(err => { |
| | | console.error("å é¤ç¥è¯åºå¤±è´¥:", err); |
| | | ElMessage.error(err.msg || "å é¤å¤±è´¥"); |
| | | }); |
| | | }).catch(() => { |
| | | // ç¨æ·åæ¶ |
| | | }); |
| | |
| | | const handleExport = () => { |
| | | proxy.download('/knowledgeBase/export', { ...searchForm.value }, 'ç¥è¯åº.xlsx') |
| | | } |
| | | |
| | | // ============ æä»¶ç®¡çç¸å
³ ============ |
| | | |
| | | // æå¼æä»¶ç®¡çå¼¹çª |
| | | const openFilesDialog = (row) => { |
| | | currentKnowledgeBase.value = row; |
| | | filesDialogVisible.value = true; |
| | | loadFileList(); |
| | | }; |
| | | |
| | | // å è½½æä»¶å表 |
| | | const loadFileList = async () => { |
| | | if (!currentKnowledgeBase.value?.id) return; |
| | | |
| | | try { |
| | | const res = await getVectorStatus(currentKnowledgeBase.value.id); |
| | | fileList.value = res.data || []; |
| | | |
| | | // æ£æ¥æ¯å¦æå¤çä¸çæä»¶,妿æåå¯å¨è½®è¯¢ |
| | | const hasProcessing = res.data.some(item => item.vectorStatus === 1); |
| | | if (hasProcessing && !vectorStatusTimer.value) { |
| | | startVectorStatusPolling(); |
| | | } else if (!hasProcessing && vectorStatusTimer.value) { |
| | | stopVectorStatusPolling(); |
| | | } |
| | | } catch (error) { |
| | | console.error("å è½½æä»¶å表失败:", error); |
| | | ElMessage.error("å è½½æä»¶å表失败"); |
| | | } |
| | | }; |
| | | |
| | | // å¼å§è½®è¯¢åéåç¶æ |
| | | const startVectorStatusPolling = () => { |
| | | vectorStatusTimer.value = setInterval(async () => { |
| | | try { |
| | | const res = await getVectorStatus(currentKnowledgeBase.value.id); |
| | | fileList.value = res.data || []; |
| | | |
| | | // æ£æ¥æ¯å¦è¿æå¤çä¸çæä»¶ |
| | | const hasProcessing = res.data.some(item => item.vectorStatus === 1); |
| | | if (!hasProcessing) { |
| | | stopVectorStatusPolling(); |
| | | ElMessage.success("æææä»¶åéåå¤ç宿"); |
| | | } |
| | | } catch (error) { |
| | | console.error("轮询åéåç¶æå¤±è´¥:", error); |
| | | stopVectorStatusPolling(); |
| | | } |
| | | }, 3000); // æ¯3ç§è½®è¯¢ä¸æ¬¡ |
| | | }; |
| | | |
| | | // åæ¢è½®è¯¢åéåç¶æ |
| | | const stopVectorStatusPolling = () => { |
| | | if (vectorStatusTimer.value) { |
| | | clearInterval(vectorStatusTimer.value); |
| | | vectorStatusTimer.value = null; |
| | | } |
| | | }; |
| | | |
| | | // ä¸ä¼ åæ ¡éª |
| | | const beforeUpload = (file) => { |
| | | const allowedTypes = ['.txt', '.md', '.docx', '.xlsx', '.xls', '.pdf']; |
| | | const fileName = file.name.toLowerCase(); |
| | | const isAllowed = allowedTypes.some(type => fileName.endsWith(type)); |
| | | |
| | | if (!isAllowed) { |
| | | ElMessage.error('åªæ¯æ txtãmdãdocxãxlsxãxlsãpdf æ ¼å¼çæä»¶'); |
| | | return false; |
| | | } |
| | | |
| | | const isLt50M = file.size / 1024 / 1024 < 50; |
| | | if (!isLt50M) { |
| | | ElMessage.error('æä»¶å¤§å°ä¸è½è¶
è¿ 50MB'); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | }; |
| | | |
| | | // ä¸ä¼ æå |
| | | const handleUploadSuccess = (response, file) => { |
| | | console.log("ä¸ä¼ ååº:", response); // è°è¯æ¥å¿ |
| | | |
| | | if (response.code === 200) { |
| | | // â
å端è¿åçæ¯ List<StorageBlobVO>,æä»¥dataæ¯æ°ç» |
| | | if (Array.isArray(response.data) && response.data.length > 0) { |
| | | // åæ°ç»ç¬¬ä¸ä¸ªå
ç´ çid |
| | | const blobId = response.data[0].id; |
| | | if (blobId) { |
| | | uploadedBlobIds.value.push(blobId); |
| | | ElMessage.success(`æä»¶ ${file.name} ä¸ä¼ æå`); |
| | | } else { |
| | | console.error("ä¸ä¼ ååºä¸æªæ¾å°id:", response.data[0]); |
| | | ElMessage.error("ä¸ä¼ 失败: æªè·åå°æä»¶ID"); |
| | | } |
| | | } else { |
| | | console.error("ä¸ä¼ ååºæ ¼å¼é误:", response); |
| | | ElMessage.error("ä¸ä¼ 失败: ååºæ ¼å¼é误"); |
| | | } |
| | | } else { |
| | | ElMessage.error(response.msg || "ä¸ä¼ 失败"); |
| | | } |
| | | }; |
| | | |
| | | // ä¸ä¼ 失败 |
| | | const handleUploadError = (error, file) => { |
| | | ElMessage.error(`æä»¶ ${file.name} ä¸ä¼ 失败`); |
| | | }; |
| | | |
| | | // ä¿åæä»¶å
³è |
| | | const saveFiles = async () => { |
| | | // åæ°æ ¡éª |
| | | if (!currentKnowledgeBase.value?.id) { |
| | | ElMessage.error("ç¥è¯åºä¿¡æ¯å¼å¸¸"); |
| | | return; |
| | | } |
| | | |
| | | if (uploadedBlobIds.value.length === 0) { |
| | | ElMessage.warning("请å
ä¸ä¼ æä»¶"); |
| | | return; |
| | | } |
| | | |
| | | savingFiles.value = true; |
| | | |
| | | try { |
| | | // â
POST请æ±ä½¿ç¨dataä¼ å,æç¡®åæ°ç»æ |
| | | await saveKnowledgeBaseFiles({ |
| | | knowledgeBaseId: currentKnowledgeBase.value.id, // ç¥è¯åºID |
| | | storageBlobIds: uploadedBlobIds.value // æä»¶blob IDæ°ç» |
| | | }); |
| | | |
| | | ElMessage.success("æä»¶å
³èä¿åæå,æ£å¨åå°å¤çåéå"); |
| | | uploadedBlobIds.value = []; |
| | | |
| | | // å»¶è¿å·æ°æä»¶å表,ç»åå°å¤çæ¶é´ |
| | | setTimeout(() => { |
| | | loadFileList(); |
| | | }, 1000); |
| | | } catch (error) { |
| | | console.error("ä¿åæä»¶å
³è失败:", error); |
| | | ElMessage.error("ä¿åæä»¶å
³è失败"); |
| | | } finally { |
| | | savingFiles.value = false; |
| | | } |
| | | }; |
| | | |
| | | // éæ°å¤çåéåçæä»¶ |
| | | const reprocessFile = async (row) => { |
| | | try { |
| | | await reprocessVector(row.id); |
| | | ElMessage.success("已鿰æäº¤åéåä»»å¡"); |
| | | // å»¶è¿å·æ° |
| | | setTimeout(() => { |
| | | loadFileList(); |
| | | }, 1000); |
| | | } catch (error) { |
| | | console.error("éæ°å¤ç失败:", error); |
| | | ElMessage.error("éæ°å¤ç失败"); |
| | | } |
| | | }; |
| | | |
| | | // æ¸
空å¾
ä¿åçæä»¶å表 |
| | | const clearUploadedFiles = () => { |
| | | uploadedBlobIds.value = []; |
| | | ElMessage.success("å·²æ¸
空å¾
ä¿åæä»¶å表"); |
| | | }; |
| | | |
| | | // å 餿件 |
| | | const deleteFile = async (row) => { |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | "ç¡®å®è¦å é¤è¯¥æä»¶å?å é¤åå°æ æ³æ¢å¤åéæ°æ®", |
| | | "å é¤ç¡®è®¤", |
| | | { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning" |
| | | } |
| | | ); |
| | | |
| | | // â
DELETE请æ±ä½¿ç¨dataä¼ éIDæ°ç» |
| | | await deleteKnowledgeBaseFile([row.id]); // 注æ: row.idæ¯åéè®°å½ID,䏿¯storageBlobId |
| | | ElMessage.success("å 餿å"); |
| | | loadFileList(); |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | console.error("å é¤æä»¶å¤±è´¥:", error); |
| | | ElMessage.error("å é¤æä»¶å¤±è´¥"); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // ç¶æææ¬æ å° |
| | | const getStatusText = (status) => { |
| | | const map = { |
| | | 0: 'å¾
å¤ç', |
| | | 1: 'å¤çä¸', |
| | | 2: '已宿', |
| | | 3: '失败' |
| | | }; |
| | | return map[status] || 'æªç¥'; |
| | | }; |
| | | |
| | | // ç¶ææ ç¾ç±»åæ å° |
| | | const getStatusType = (status) => { |
| | | const map = { |
| | | 0: 'info', |
| | | 1: 'warning', |
| | | 2: 'success', |
| | | 3: 'danger' |
| | | }; |
| | | return map[status] || 'info'; |
| | | }; |
| | | |
| | | // å
³éæä»¶ç®¡çå¼¹çª |
| | | const closeFilesDialog = () => { |
| | | filesDialogVisible.value = false; |
| | | currentKnowledgeBase.value = null; |
| | | fileList.value = []; |
| | | uploadedBlobIds.value = []; |
| | | stopVectorStatusPolling(); // åæ¢è½®è¯¢ |
| | | getList(); // å·æ°ä¸»å表,æ´æ°æä»¶æ°é |
| | | }; |
| | | |
| | | // ============ ç¥è¯åºé®çç¸å
³ ============ |
| | | |
| | | // çæUUIDçfallbackæ¹æ¡ |
| | | const generateUUID = () => { |
| | | if (typeof crypto !== 'undefined' && crypto.randomUUID) { |
| | | return crypto.randomUUID(); |
| | | } |
| | | // fallback: å
¼å®¹ä¸æ¯æ crypto.randomUUID çç¯å¢ |
| | | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { |
| | | const r = Math.random() * 16 | 0; |
| | | const v = c === 'x' ? r : (r & 0x3 | 0x8); |
| | | return v.toString(16); |
| | | }); |
| | | }; |
| | | |
| | | // æå¼é®çå¼¹çª |
| | | const openChatDialog = (row) => { |
| | | currentKnowledgeBase.value = row; |
| | | chatDialogVisible.value = true; |
| | | memoryId.value = generateUUID(); |
| | | messages.value = []; |
| | | inputQuestion.value = ""; |
| | | }; |
| | | |
| | | // åéæ¶æ¯ |
| | | const sendMessage = async () => { |
| | | // åæ°æ ¡éª |
| | | if (!inputQuestion.value.trim()) { |
| | | ElMessage.warning("请è¾å
¥é®é¢"); |
| | | return; |
| | | } |
| | | |
| | | if (!currentKnowledgeBase.value?.id) { |
| | | ElMessage.error("ç¥è¯åºä¿¡æ¯å¼å¸¸"); |
| | | return; |
| | | } |
| | | |
| | | const question = inputQuestion.value.trim(); |
| | | |
| | | // æ·»å ç¨æ·æ¶æ¯ |
| | | messages.value.push({ |
| | | role: 'user', |
| | | content: question |
| | | }); |
| | | |
| | | inputQuestion.value = ""; |
| | | chatLoading.value = true; |
| | | |
| | | // æ»å¨å°åºé¨ |
| | | await nextTick(); |
| | | scrollToBottom(); |
| | | |
| | | try { |
| | | // â
æµå¼è¯·æ±ä½¿ç¨Fetch API |
| | | const response = await knowledgeChat({ |
| | | knowledgeBaseId: currentKnowledgeBase.value.id, // ç¥è¯åºID |
| | | memoryId: memoryId.value, // ä¼è¯ID |
| | | question: question // ç¨æ·é®é¢ |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | const errorText = await response.text(); |
| | | throw new Error(errorText || '请æ±å¤±è´¥'); |
| | | } |
| | | |
| | | // â
å端è¿å text/stream;charset=utf-8 |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let aiContent = ''; |
| | | |
| | | messages.value.push({ role: 'assistant', content: '' }); |
| | | |
| | | while (true) { |
| | | const { done, value } = await reader.read(); |
| | | if (done) break; |
| | | |
| | | const text = decoder.decode(value, { stream: true }); // â
æ·»å streamé项 |
| | | aiContent += text; |
| | | messages.value[messages.value.length - 1].content = aiContent; |
| | | |
| | | // æ»å¨å°åºé¨ |
| | | await nextTick(); |
| | | scrollToBottom(); |
| | | } |
| | | |
| | | // 妿AIè¿å空å
容,æ¾ç¤ºæç¤º |
| | | if (!aiContent.trim()) { |
| | | messages.value[messages.value.length - 1].content = 'æ±æ,ç¥è¯åºä¸æªæ¾å°ç¸å
³å
容,请å°è¯å
¶ä»é®é¢ã'; |
| | | } |
| | | } catch (error) { |
| | | console.error("é®ç请æ±å¤±è´¥:", error); |
| | | ElMessage.error("é®ç请æ±å¤±è´¥,请ç¨åéè¯"); |
| | | messages.value.push({ |
| | | role: 'assistant', |
| | | content: 'æ±æ,åçäºé误,请ç¨åéè¯' |
| | | }); |
| | | } finally { |
| | | chatLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // æ¸
ç©ºå¯¹è¯ |
| | | const clearMessages = () => { |
| | | ElMessageBox.confirm( |
| | | "ç¡®å®è¦æ¸
空ææå¯¹è¯è®°å½å?", |
| | | "æ¸
空确认", |
| | | { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning" |
| | | } |
| | | ).then(() => { |
| | | messages.value = []; |
| | | memoryId.value = generateUUID(); // éæ°çæä¼è¯ID |
| | | ElMessage.success("对è¯å·²æ¸
空"); |
| | | }).catch(() => { |
| | | // ç¨æ·åæ¶ |
| | | }); |
| | | }; |
| | | |
| | | // æ»å¨å°åºé¨ |
| | | const scrollToBottom = () => { |
| | | if (chatMessagesRef.value) { |
| | | chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight; |
| | | } |
| | | }; |
| | | |
| | | // å
³éé®çå¼¹çª |
| | | const closeChatDialog = () => { |
| | | chatDialogVisible.value = false; |
| | | currentKnowledgeBase.value = null; |
| | | messages.value = []; |
| | | inputQuestion.value = ""; |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | /* æä»¶ç®¡çæ ·å¼ */ |
| | | .file-manager { |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .upload-section { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .uploaded-list { |
| | | margin-top: 16px; |
| | | padding: 12px; |
| | | background: #f0f9ff; |
| | | border-radius: 6px; |
| | | border: 1px solid #b3d8ff; |
| | | } |
| | | |
| | | .uploaded-tip { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | color: #409eff; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | /* ç¥è¯åºé®çæ ·å¼ */ |
| | | .knowledge-chat { |
| | | display: flex; |
| | | flex-direction: column; |
| | | height: 500px; |
| | | } |
| | | |
| | | .chat-header { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .chat-messages { |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | padding: 16px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .message { |
| | | margin-bottom: 16px; |
| | | max-width: 80%; |
| | | } |
| | | |
| | | .message.user { |
| | | margin-left: auto; |
| | | text-align: right; |
| | | } |
| | | |
| | | .message.assistant { |
| | | margin-right: auto; |
| | | } |
| | | |
| | | .message-role { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .message-content { |
| | | display: inline-block; |
| | | padding: 10px 14px; |
| | | border-radius: 8px; |
| | | line-height: 1.6; |
| | | word-wrap: break-word; |
| | | white-space: pre-wrap; |
| | | } |
| | | |
| | | .message.user .message-content { |
| | | background: #409eff; |
| | | color: white; |
| | | } |
| | | |
| | | .message.assistant .message-content { |
| | | background: white; |
| | | color: #303133; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .typing { |
| | | animation: typing 1.5s infinite; |
| | | } |
| | | |
| | | @keyframes typing { |
| | | 0%, 50%, 100% { |
| | | opacity: 1; |
| | | } |
| | | 25%, 75% { |
| | | opacity: 0.5; |
| | | } |
| | | } |
| | | |
| | | .chat-input { |
| | | margin-top: auto; |
| | | } |
| | | |
| | | .chat-actions { |
| | | margin-top: 8px; |
| | | text-align: right; |
| | | } |
| | | |
| | | </style> |
| | |
| | | <template> |
| | | <div> |
| | | |
| | | <!-- ç³è¯·ç±»åéæ© --> |
| | | <el-card class="type-card"> |
| | | <div class="type-selector"> |
| | | <div |
| | | v-for="type in applicationTypes" |
| | | :key="type.value" |
| | | class="type-item" |
| | | :class="{ active: currentType === type.value }" |
| | | @click="changeType(type.value)" |
| | | > |
| | | <div class="type-icon"> |
| | | <el-icon :size="24"><component :is="type.icon"/></el-icon> |
| | | </div> |
| | | <div class="type-info"> |
| | | <div class="type-name">{{ type.name }}</div> |
| | | <div class="type-desc">{{ type.desc }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- ä¼è®®ç³è¯·è¡¨å --> |
| | | <el-card> |
| | | <div class="form-header"> |
| | | <h3>{{ getCurrentTypeName() }}ç³è¯·</h3> |
| | | </div> |
| | | |
| | | <el-form |
| | | ref="meetingFormRef" |
| | | :model="meetingForm" |
| | | :rules="rules" |
| | | label-width="100px" |
| | | > |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®ä¸»é¢" prop="title"> |
| | | <el-input v-model="meetingForm.title" placeholder="请è¾å
¥ä¼è®®ä¸»é¢"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®å®¤" prop="roomId"> |
| | | <el-select v-model="meetingForm.roomId" placeholder="è¯·éæ©ä¼è®®å®¤" style="width: 100%"> |
| | | <el-option |
| | | v-for="room in meetingRooms" |
| | | :key="room.id" |
| | | :label="`${room.name} (${room.location})`" |
| | | :value="room.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="主æäºº" prop="host"> |
| | | <el-input v-model="meetingForm.host" placeholder="请è¾å
¥ä¸»æäººå§å"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®æ¥æ" prop="meetingDate"> |
| | | <el-date-picker |
| | | v-model="meetingForm.meetingDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ä¼è®®æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | :disabled-date="disabledDate" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <!-- 空åï¼ä¿æå¸å± --> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¼å§æ¶é´" prop="startTime"> |
| | | <el-select |
| | | v-model="meetingForm.startTime" |
| | | placeholder="è¯·éæ©å¼å§æ¶é´" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="time in startTimeOptions" |
| | | :key="time.value" |
| | | :label="time.label" |
| | | :value="time.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç»ææ¶é´" prop="endTime"> |
| | | <el-select |
| | | v-model="meetingForm.endTime" |
| | | placeholder="è¯·éæ©ç»ææ¶é´" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="time in endTimeOptions" |
| | | :key="time.value" |
| | | :label="time.label" |
| | | :value="time.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-form-item label="åä¼äººå" prop="participants"> |
| | | <el-select |
| | | v-model="meetingForm.participants" |
| | | multiple |
| | | filterable |
| | | placeholder="è¯·éæ©åä¼äººå" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="person in employees" |
| | | :key="person.id" |
| | | :label="`${person.staffName}${person.postName ? ` (${person.postName})` : ''}`" |
| | | :value="person.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="ä¼è®®è¯´æ" prop="description"> |
| | | <el-input |
| | | v-model="meetingForm.description" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="请è¾å
¥ä¼è®®è¯´æ" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <div class="form-footer"> |
| | | <el-button @click="resetForm">éç½®</el-button> |
| | | <el-button type="primary" @click="submitForm">æäº¤</el-button> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | <div> |
| | | |
| | | <!-- ç³è¯·ç±»åéæ© --> |
| | | <el-card class="type-card"> |
| | | <div class="type-selector"> |
| | | <div |
| | | v-for="type in applicationTypes" |
| | | :key="type.value" |
| | | class="type-item" |
| | | :class="{ active: currentType === type.value }" |
| | | @click="changeType(type.value)" |
| | | > |
| | | <div class="type-icon"> |
| | | <el-icon :size="24"><component :is="type.icon"/></el-icon> |
| | | </div> |
| | | <div class="type-info"> |
| | | <div class="type-name">{{ type.name }}</div> |
| | | <div class="type-desc">{{ type.desc }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- ä¼è®®ç³è¯·è¡¨å --> |
| | | <el-card> |
| | | <div class="form-header"> |
| | | <h3>{{ getCurrentTypeName() }}ç³è¯·</h3> |
| | | </div> |
| | | |
| | | <el-form |
| | | ref="meetingFormRef" |
| | | :model="meetingForm" |
| | | :rules="rules" |
| | | label-width="100px" |
| | | > |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®ä¸»é¢" prop="title"> |
| | | <el-input v-model="meetingForm.title" placeholder="请è¾å
¥ä¼è®®ä¸»é¢"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®å®¤" prop="roomId"> |
| | | <el-select v-model="meetingForm.roomId" placeholder="è¯·éæ©ä¼è®®å®¤" style="width: 100%"> |
| | | <el-option |
| | | v-for="room in meetingRooms" |
| | | :key="room.id" |
| | | :label="`${room.name} (${room.location})`" |
| | | :value="room.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="主æäºº" prop="host"> |
| | | <el-input v-model="meetingForm.host" placeholder="请è¾å
¥ä¸»æäººå§å"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¼è®®æ¥æ" prop="meetingDate"> |
| | | <el-date-picker |
| | | v-model="meetingForm.meetingDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ä¼è®®æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | :disabled-date="disabledDate" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <!-- 空åï¼ä¿æå¸å± --> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¼å§æ¶é´" prop="startTime"> |
| | | <el-time-picker |
| | | v-model="meetingForm.startTime" |
| | | placeholder="è¯·éæ©å¼å§æ¶é´" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | :disabled-hours="disabledStartHours" |
| | | :disabled-minutes="disabledStartMinutes" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç»ææ¶é´" prop="endTime"> |
| | | <el-time-picker |
| | | v-model="meetingForm.endTime" |
| | | placeholder="è¯·éæ©ç»ææ¶é´" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | :disabled-hours="disabledEndHours" |
| | | :disabled-minutes="disabledEndMinutes" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-form-item label="åä¼äººå" prop="participants"> |
| | | <el-select |
| | | v-model="meetingForm.participants" |
| | | multiple |
| | | filterable |
| | | placeholder="è¯·éæ©åä¼äººå" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="person in employees" |
| | | :key="person.id" |
| | | :label="`${person.staffName}${person.postName ? ` (${person.postName})` : ''}`" |
| | | :value="person.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="ä¼è®®è¯´æ" prop="description"> |
| | | <el-input |
| | | v-model="meetingForm.description" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="请è¾å
¥ä¼è®®è¯´æ" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <div class="form-footer"> |
| | | <el-button @click="resetForm">éç½®</el-button> |
| | | <el-button type="primary" @click="submitForm">æäº¤</el-button> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | |
| | | |
| | | // ç³è¯·ç±»åé项 |
| | | const applicationTypes = ref([ |
| | | { |
| | | value: 'approval', |
| | | name: 'å®¡æ¹æµç¨ä¼è®®', |
| | | desc: 'éè¦ç»è¿å¤çº§å®¡æ¹çä¼è®®ç³è¯·', |
| | | icon: Document |
| | | }, |
| | | { |
| | | value: 'department', |
| | | name: 'é¨é¨çº§ä¼è®®', |
| | | desc: 'é¨é¨å
é¨ä¼è®®ç³è¯·æµç¨', |
| | | icon: Promotion |
| | | }, |
| | | { |
| | | value: 'notification', |
| | | name: 'ä¼è®®éç¥', |
| | | desc: 'æ é审æ¹ç´æ¥åå¸çä¼è®®éç¥', |
| | | icon: Bell |
| | | } |
| | | { |
| | | value: 'approval', |
| | | name: 'å®¡æ¹æµç¨ä¼è®®', |
| | | desc: 'éè¦ç»è¿å¤çº§å®¡æ¹çä¼è®®ç³è¯·', |
| | | icon: Document |
| | | }, |
| | | { |
| | | value: 'department', |
| | | name: 'é¨é¨çº§ä¼è®®', |
| | | desc: 'é¨é¨å
é¨ä¼è®®ç³è¯·æµç¨', |
| | | icon: Promotion |
| | | }, |
| | | { |
| | | value: 'notification', |
| | | name: 'ä¼è®®éç¥', |
| | | desc: 'æ é审æ¹ç´æ¥åå¸çä¼è®®éç¥', |
| | | icon: Bell |
| | | } |
| | | ]) |
| | | |
| | | // è¡¨åæ°æ® |
| | | const meetingForm = reactive({ |
| | | title: '', |
| | | type: '', |
| | | roomId: '', |
| | | host: '', |
| | | meetingDate: '', |
| | | startTime: '', |
| | | endTime: '', |
| | | participants: [], |
| | | description: '' |
| | | title: '', |
| | | type: '', |
| | | roomId: '', |
| | | host: '', |
| | | meetingDate: '', |
| | | startTime: '', |
| | | endTime: '', |
| | | participants: [], |
| | | description: '' |
| | | }) |
| | | |
| | | // 表åå¼ç¨ |
| | |
| | | // åå·¥å表 |
| | | const employees = ref([]) |
| | | |
| | | // æ¶é´é项ï¼ä»¥åå°æ¶ä¸ºé´éï¼ |
| | | const timeOptions = ref([]) |
| | | |
| | | const getTimeInMinutes = (time) => { |
| | | if (!time) return -1 |
| | | const [hour, minute] = time.split(':').map(Number) |
| | | return hour * 60 + minute |
| | | if (!time) return -1 |
| | | const [hour, minute] = time.split(':').map(Number) |
| | | return hour * 60 + minute |
| | | } |
| | | |
| | | const isToday = (dateText) => { |
| | | if (!dateText) return false |
| | | const [year, month, day] = dateText.split('-').map(Number) |
| | | const now = new Date() |
| | | return year === now.getFullYear() && month === now.getMonth() + 1 && day === now.getDate() |
| | | if (!dateText) return false |
| | | const [year, month, day] = dateText.split('-').map(Number) |
| | | const now = new Date() |
| | | return year === now.getFullYear() && month === now.getMonth() + 1 && day === now.getDate() |
| | | } |
| | | |
| | | const validateStartTime = (_rule, value, callback) => { |
| | | if (!value) { |
| | | callback() |
| | | return |
| | | } |
| | | |
| | | if (isToday(meetingForm.meetingDate)) { |
| | | const now = new Date() |
| | | const currentMinutes = now.getHours() * 60 + now.getMinutes() |
| | | if (getTimeInMinutes(value) > currentMinutes) { |
| | | callback(new Error('å½å¤©å¼å§æ¶é´ä¸è½æäºå½åæ¶é´')) |
| | | return |
| | | } |
| | | } |
| | | |
| | | callback() |
| | | if (!value) { |
| | | callback() |
| | | return |
| | | } |
| | | callback() |
| | | } |
| | | |
| | | const validateEndTime = (_rule, value, callback) => { |
| | | if (!value || !meetingForm.startTime) { |
| | | callback() |
| | | return |
| | | } |
| | | |
| | | if (getTimeInMinutes(value) <= getTimeInMinutes(meetingForm.startTime)) { |
| | | callback(new Error('ç»ææ¶é´å¿
须大äºå¼å§æ¶é´')) |
| | | return |
| | | } |
| | | |
| | | callback() |
| | | if (!value || !meetingForm.startTime) { |
| | | callback() |
| | | return |
| | | } |
| | | if (getTimeInMinutes(value) <= getTimeInMinutes(meetingForm.startTime)) { |
| | | callback(new Error('ç»ææ¶é´å¿
须大äºå¼å§æ¶é´')) |
| | | return |
| | | } |
| | | callback() |
| | | } |
| | | |
| | | // è¡¨åæ ¡éªè§å |
| | | const rules = { |
| | | title: [{required: true, message: '请è¾å
¥ä¼è®®ä¸»é¢', trigger: 'blur'}], |
| | | roomId: [{required: true, message: 'è¯·éæ©ä¼è®®å®¤', trigger: 'change'}], |
| | | host: [{required: true, message: '请è¾å
¥ä¸»æäºº', trigger: 'blur'}], |
| | | meetingDate: [{required: true, message: 'è¯·éæ©ä¼è®®æ¥æ', trigger: 'change'}], |
| | | startTime: [ |
| | | {required: true, message: 'è¯·éæ©å¼å§æ¶é´', trigger: 'change'}, |
| | | {validator: validateStartTime, trigger: 'change'} |
| | | ], |
| | | endTime: [ |
| | | {required: true, message: 'è¯·éæ©ç»ææ¶é´', trigger: 'change'}, |
| | | {validator: validateEndTime, trigger: 'change'} |
| | | ], |
| | | participants: [{required: true, message: 'è¯·éæ©åä¼äººå', trigger: 'change'}] |
| | | title: [{required: true, message: '请è¾å
¥ä¼è®®ä¸»é¢', trigger: 'blur'}], |
| | | roomId: [{required: true, message: 'è¯·éæ©ä¼è®®å®¤', trigger: 'change'}], |
| | | host: [{required: true, message: '请è¾å
¥ä¸»æäºº', trigger: 'blur'}], |
| | | meetingDate: [{required: true, message: 'è¯·éæ©ä¼è®®æ¥æ', trigger: 'change'}], |
| | | startTime: [ |
| | | {required: true, message: 'è¯·éæ©å¼å§æ¶é´', trigger: 'change'}, |
| | | {validator: validateStartTime, trigger: 'change'} |
| | | ], |
| | | endTime: [ |
| | | {required: true, message: 'è¯·éæ©ç»ææ¶é´', trigger: 'change'}, |
| | | {validator: validateEndTime, trigger: 'change'} |
| | | ], |
| | | participants: [{required: true, message: 'è¯·éæ©åä¼äººå', trigger: 'change'}] |
| | | } |
| | | |
| | | const startTimeOptions = computed(() => { |
| | | if (!isToday(meetingForm.meetingDate)) { |
| | | return timeOptions.value |
| | | } |
| | | const now = new Date() |
| | | const currentMinutes = now.getHours() * 60 + now.getMinutes() |
| | | return timeOptions.value.filter(item => getTimeInMinutes(item.value) <= currentMinutes) |
| | | }) |
| | | |
| | | const endTimeOptions = computed(() => { |
| | | if (!meetingForm.startTime) { |
| | | return timeOptions.value |
| | | } |
| | | const startMinutes = getTimeInMinutes(meetingForm.startTime) |
| | | return timeOptions.value.filter(item => getTimeInMinutes(item.value) > startMinutes) |
| | | }) |
| | | |
| | | // åå§åæ¶é´é项 |
| | | const initTimeOptions = () => { |
| | | const options = [] |
| | | const now = new Date() |
| | | const currentHour = now.getHours() |
| | | const currentMinute = now.getMinutes() |
| | | // meetingDate æ¯ "yyyy-MM-dd" |
| | | const meetingDate = new Date(meetingForm.meetingDate) |
| | | |
| | | const isSameDay = |
| | | now.getFullYear() === meetingDate.getFullYear() && |
| | | now.getMonth() === meetingDate.getMonth() && |
| | | now.getDate() === meetingDate.getDate() |
| | | |
| | | console.log('æ¯å¦åä¸å¤©:', isSameDay) |
| | | for (let hour = 8; hour <= 18; hour++) { |
| | | // å¼å§æ¶é´å¿
é¡»æäºå½åæ¶é´ |
| | | if (hour < currentHour && isSameDay) { |
| | | continue |
| | | } |
| | | if (hour === currentHour && currentMinute > 30 && isSameDay) { |
| | | continue |
| | | } |
| | | // æ¯ä¸ªå°æ¶æ·»å 两个éé¡¹ï¼æ´ç¹ååç¹ |
| | | options.push({ |
| | | value: `${hour.toString().padStart(2, '0')}:00`, |
| | | label: `${hour.toString().padStart(2, '0')}:00` |
| | | }) |
| | | |
| | | if (hour < 18) { // 18:00ä¹å没æåç¹é项 |
| | | options.push({ |
| | | value: `${hour.toString().padStart(2, '0')}:30`, |
| | | label: `${hour.toString().padStart(2, '0')}:30` |
| | | }) |
| | | } |
| | | } |
| | | timeOptions.value = options |
| | | // æ¶é´éæ©å¨ç¦ç¨é»è¾ |
| | | const disabledStartHours = () => { |
| | | const hours = [] |
| | | for (let h = 0; h < 24; h++) { |
| | | if (h < 8 || h > 18) hours.push(h) |
| | | } |
| | | if (isToday(meetingForm.meetingDate)) { |
| | | const now = new Date() |
| | | for (let h = 8; h < now.getHours(); h++) { |
| | | if (!hours.includes(h)) hours.push(h) |
| | | } |
| | | } |
| | | return hours |
| | | } |
| | | |
| | | watch(() => meetingForm.meetingDate, () => { |
| | | if (meetingForm.startTime && !startTimeOptions.value.some(item => item.value === meetingForm.startTime)) { |
| | | meetingForm.startTime = '' |
| | | } |
| | | if (meetingForm.endTime && !endTimeOptions.value.some(item => item.value === meetingForm.endTime)) { |
| | | meetingForm.endTime = '' |
| | | } |
| | | if (meetingForm.startTime) { |
| | | meetingFormRef.value?.validateField('startTime') |
| | | } |
| | | if (meetingForm.endTime) { |
| | | meetingFormRef.value?.validateField('endTime') |
| | | } |
| | | initTimeOptions() |
| | | }) |
| | | const disabledStartMinutes = (hour) => { |
| | | const minutes = [] |
| | | for (let m = 0; m < 60; m++) { |
| | | if (m !== 0 && m !== 30) minutes.push(m) |
| | | } |
| | | if (isToday(meetingForm.meetingDate)) { |
| | | const now = new Date() |
| | | if (hour === now.getHours()) { |
| | | if (now.getMinutes() >= 30) { |
| | | minutes.push(0) |
| | | } |
| | | } |
| | | } |
| | | return minutes |
| | | } |
| | | |
| | | const disabledEndHours = () => { |
| | | const hours = [] |
| | | for (let h = 0; h < 24; h++) { |
| | | if (h < 8 || h > 18) hours.push(h) |
| | | } |
| | | if (meetingForm.startTime) { |
| | | const startHour = parseInt(meetingForm.startTime.split(':')[0]) |
| | | for (let h = 8; h < startHour; h++) { |
| | | if (!hours.includes(h)) hours.push(h) |
| | | } |
| | | } |
| | | return hours |
| | | } |
| | | |
| | | const disabledEndMinutes = (hour) => { |
| | | const minutes = [] |
| | | for (let m = 0; m < 60; m++) { |
| | | if (m !== 0 && m !== 30) minutes.push(m) |
| | | } |
| | | if (meetingForm.startTime) { |
| | | const startHour = parseInt(meetingForm.startTime.split(':')[0]) |
| | | const startMinute = parseInt(meetingForm.startTime.split(':')[1]) |
| | | if (hour === startHour) { |
| | | if (startMinute >= 0) minutes.push(0) |
| | | if (startMinute >= 30) minutes.push(30) |
| | | // only keep minutes > startMinute |
| | | for (let m = 0; m <= startMinute; m++) { |
| | | if (!minutes.includes(m)) minutes.push(m) |
| | | } |
| | | } |
| | | } |
| | | return minutes |
| | | } |
| | | |
| | | watch(() => meetingForm.startTime, () => { |
| | | if (meetingForm.endTime && getTimeInMinutes(meetingForm.endTime) <= getTimeInMinutes(meetingForm.startTime)) { |
| | | meetingForm.endTime = '' |
| | | } |
| | | if (meetingForm.endTime) { |
| | | meetingFormRef.value?.validateField('endTime') |
| | | } |
| | | |
| | | if (meetingForm.endTime && getTimeInMinutes(meetingForm.endTime) <= getTimeInMinutes(meetingForm.startTime)) { |
| | | meetingForm.endTime = '' |
| | | } |
| | | if (meetingForm.endTime) { |
| | | meetingFormRef.value?.validateField('endTime') |
| | | } |
| | | |
| | | }) |
| | | |
| | | // ç¦ç¨æ¥æï¼ç¦ç¨ä»å¤©ä¹åçæ¥æï¼ |
| | | const disabledDate = (time) => { |
| | | // ç¦ç¨ä»å¤©ä¹åçæ¥æ |
| | | return time.getTime() < Date.now() - 86400000 |
| | | // ç¦ç¨ä»å¤©ä¹åçæ¥æ |
| | | return time.getTime() < Date.now() - 86400000 |
| | | } |
| | | |
| | | // 忢ç³è¯·ç±»å |
| | | const changeType = (type) => { |
| | | currentType.value = type |
| | | currentType.value = type |
| | | } |
| | | |
| | | // è·åå½åç±»ååç§° |
| | | const getCurrentTypeName = () => { |
| | | const type = applicationTypes.value.find(t => t.value === currentType.value) |
| | | return type ? type.name : '' |
| | | const type = applicationTypes.value.find(t => t.value === currentType.value) |
| | | return type ? type.name : '' |
| | | } |
| | | |
| | | // é置表å |
| | | const resetForm = () => { |
| | | meetingFormRef.value?.resetFields() |
| | | meetingFormRef.value?.resetFields() |
| | | } |
| | | |
| | | // æäº¤è¡¨å |
| | | const submitForm = () => { |
| | | meetingFormRef.value?.validate((valid) => { |
| | | if (valid) { |
| | | |
| | | let formData = {...meetingForm} |
| | | formData.applicationType = currentType.value |
| | | formData.startTime = `${meetingForm.meetingDate} ${meetingForm.startTime}:00` |
| | | formData.endTime = `${meetingForm.meetingDate} ${meetingForm.endTime}:00` |
| | | formData.participants = JSON.stringify(formData.participants) |
| | | console.log(formData) |
| | | saveMeetingApplication(formData).then(() => { |
| | | |
| | | // 模ææäº¤æä½ |
| | | ElMessage.success(`${getCurrentTypeName()}æäº¤æå`) |
| | | |
| | | // æ ¹æ®ä¸åç±»åæ§è¡ä¸åæä½ |
| | | switch (currentType.value) { |
| | | case 'approval': |
| | | ElMessage.info('ä¼è®®å·²æäº¤å®¡æ¹æµç¨') |
| | | break |
| | | case 'department': |
| | | ElMessage.info('é¨é¨çº§ä¼è®®ç³è¯·å·²æäº¤') |
| | | break |
| | | case 'notification': |
| | | ElMessage.info('ä¼è®®éç¥å·²åå¸') |
| | | break |
| | | } |
| | | resetForm() |
| | | }) |
| | | |
| | | } |
| | | }) |
| | | meetingFormRef.value?.validate((valid) => { |
| | | if (valid) { |
| | | |
| | | let formData = {...meetingForm} |
| | | formData.applicationType = currentType.value |
| | | formData.startTime = `${meetingForm.meetingDate} ${meetingForm.startTime}:00` |
| | | formData.endTime = `${meetingForm.meetingDate} ${meetingForm.endTime}:00` |
| | | formData.participants = JSON.stringify(formData.participants) |
| | | console.log(formData) |
| | | saveMeetingApplication(formData).then(() => { |
| | | |
| | | // 模ææäº¤æä½ |
| | | ElMessage.success(`${getCurrentTypeName()}æäº¤æå`) |
| | | resetForm() |
| | | }) |
| | | |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 页é¢å è½½æ¶åå§å |
| | | onMounted(() => { |
| | | initTimeOptions() |
| | | getRoomEnum().then(res => { |
| | | meetingRooms.value = res.data |
| | | }) |
| | | staffOnJobListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | staffState: 1 |
| | | }).then(res => { |
| | | employees.value = res.data.records.sort((a, b) => (a.postName || '').localeCompare(b.postName || '')) |
| | | }) |
| | | getRoomEnum().then(res => { |
| | | meetingRooms.value = res.data |
| | | }) |
| | | staffOnJobListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | staffState: 1 |
| | | }).then(res => { |
| | | employees.value = res.data.records.sort((a, b) => (a.postName || '').localeCompare(b.postName || '')) |
| | | }) |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .app-container { |
| | | padding: 20px; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .page-header h2 { |
| | | margin: 0; |
| | | color: #303133; |
| | | margin: 0; |
| | | color: #303133; |
| | | } |
| | | |
| | | .type-card { |
| | | margin-bottom: 20px; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .type-selector { |
| | | display: flex; |
| | | gap: 20px; |
| | | display: flex; |
| | | gap: 20px; |
| | | } |
| | | |
| | | .type-item { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 20px; |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 20px; |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | } |
| | | |
| | | .type-item:hover { |
| | | border-color: #409eff; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | border-color: #409eff; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .type-item.active { |
| | | border-color: #409eff; |
| | | background-color: #ecf5ff; |
| | | border-color: #409eff; |
| | | background-color: #ecf5ff; |
| | | } |
| | | |
| | | .type-icon { |
| | | margin-right: 15px; |
| | | color: #409eff; |
| | | margin-right: 15px; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .type-name { |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | margin-bottom: 5px; |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .type-desc { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .form-header { |
| | | margin-bottom: 20px; |
| | | padding-bottom: 15px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | margin-bottom: 20px; |
| | | padding-bottom: 15px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .form-header h3 { |
| | | margin: 0; |
| | | color: #303133; |
| | | margin: 0; |
| | | color: #303133; |
| | | } |
| | | |
| | | .form-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | margin-top: 30px; |
| | | padding-top: 20px; |
| | | border-top: 1px solid #ebeef5; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | margin-top: 30px; |
| | | padding-top: 20px; |
| | | border-top: 1px solid #ebeef5; |
| | | } |
| | | </style> |
| | |
| | | <el-table-column prop="location" label="ä¼è®®å°ç¹" align="center" width="150"/> |
| | | <el-table-column prop="participants" label="åä¼äººæ°" align="center" width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.participants.length }}人 |
| | | {{ scope.row.staffCount }}人 |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="status" label="审æ¹ç¶æ" align="center" width="120"> |
| | |
| | | let resp = await getExamineList({...searchForm, ...queryParams}) |
| | | approvalList.value = resp.data.records.map(it => { |
| | | let room = roomEnum.value.find(room => it.roomId === room.id) |
| | | it.location = `${room.name}(${room.location})` |
| | | it.location = room ? `${room.name}(${room.location})` : '' |
| | | let staffs = JSON.parse(it.participants) |
| | | it.staffCount = staffs.size |
| | | it.staffCount = staffs.length |
| | | it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}` |
| | | it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => { |
| | | return { |
| | |
| | | if (endIdx === -1) { |
| | | endIdx = timeSlots.value.length |
| | | } |
| | | console.log('endIdx111', endIdx) |
| | | if (startIdx !== -1 && endIdx !== -1) { |
| | | // å¾å延伏䏿 ¼ï¼è®©ä¼è®®æ ¼åè¦çå°ç»ææ¶é´åä¸ |
| | | const displayEndIdx = Math.min(endIdx + 1, timeSlots.value.length) |
| | | if (startIdx !== -1) { |
| | | // æ 记被å ç¨çæ¶é´æ®µ |
| | | for (let i = startIdx; i < endIdx; i++) { |
| | | for (let i = startIdx; i < displayEndIdx; i++) { |
| | | occupiedSlots.add(timeSlots.value[i].value) |
| | | } |
| | | |
| | |
| | | cells.push({ |
| | | type: 'meeting', |
| | | meeting: meeting, |
| | | span: endIdx - startIdx, |
| | | span: displayEndIdx - startIdx, |
| | | startTime: meeting.startTime, |
| | | endTime: meeting.endTime |
| | | }) |
| | |
| | | <el-table-column prop="location" label="ä¼è®®å°ç¹" align="center" width="150"/> |
| | | <el-table-column prop="participants" label="åä¼äººæ°" align="center" width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.participants.length }}人 |
| | | {{ scope.row.staffCount }}人 |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="status" label="åå¸ç¶æ" align="center" width="120"> |
| | |
| | | let resp = await getMeetingPublish({...searchForm, ...queryParams}) |
| | | approvalList.value = resp.data.records.map(it => { |
| | | let room = roomEnum.value.find(room => it.roomId === room.id) |
| | | it.location = `${room.name}(${room.location})` |
| | | it.location = room ? `${room.name}(${room.location})` : '' |
| | | let staffs = JSON.parse(it.participants) |
| | | it.staffCount = staffs.size |
| | | it.staffCount = staffs.length |
| | | it.status = it.publishStatus |
| | | it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}` |
| | | it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => { |
| | |
| | | <el-table-column prop="location" label="ä¼è®®å°ç¹" align="center" width="150" /> |
| | | <el-table-column prop="participants" label="åä¼äººæ°" align="center" width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.participants.length }}人 |
| | | {{ scope.row.staffCount }}人 |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" align="center" width="200" fixed="right"> |
| | |
| | | let resp = await getMeetingPublish({ ...searchForm, ...queryParams }) |
| | | meetingList.value = resp.data.records.map(it => { |
| | | let room = roomEnum.value.find(room => it.roomId === room.id) |
| | | it.location = `${room.name}(${room.location})` |
| | | it.location = room ? `${room.name}(${room.location})` : '' |
| | | let staffs = JSON.parse(it.participants) |
| | | it.staffCount = staffs.size |
| | | it.staffCount = staffs.length |
| | | it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}` |
| | | it.participants = staffList.value.filter(staff => staffs.some(id => id === staff.id)).map(staff => { |
| | | return { |
| | |
| | | <p><strong>主æäººï¼</strong>${row.host}</p> |
| | | <p><strong>åä¼äººåï¼</strong></p> |
| | | <ol> |
| | | ${row.participants.map(p => `<li>${p.name}</li>`).join('')} |
| | | ${(row.participants || []).map(p => `<li>${p?.name || ''}</li>`).join('')} |
| | | </ol> |
| | | <p><strong>ä¼è®®å
容ï¼</strong></p> |
| | | <ol> |
| | |
| | | </template> |
| | | <template #operation="{ row }"> |
| | | <el-button type="primary" link @click="view(row)">æ¥ç</el-button> |
| | | <el-button type="primary" link @click="edit(row)">ç¼è¾</el-button> |
| | | <el-button v-if="row.status !== 'amortized'" type="primary" link @click="edit(row)">ç¼è¾</el-button> |
| | | <el-button type="danger" link @click="handleDelete(row)">å é¤</el-button> |
| | | </template> |
| | | </PIMTable> |
| | |
| | | <el-select v-model="form.status" placeholder="è¯·éæ©ç¶æ" style="width: 100%;"> |
| | | <el-option label="å¨ç¨" value="in_use" /> |
| | | <el-option label="é²ç½®" value="idle" /> |
| | | <el-option label="å·²æé宿¯" value="amortized" /> |
| | | <el-option v-if="isView" label="å·²æé宿¯" value="amortized" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | ); |
| | | |
| | | const createDefaultForm = () => ({ |
| | | id: null, |
| | | assetCode: "", |
| | | assetName: "", |
| | | category: "", |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | amount: [{ required: true, message: "请è¾å
¥éé¢", trigger: "blur" }], |
| | | }; |
| | | |
| | | const summaryProps = ["amount", "taxAmount", "totalAmount"]; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (summaryProps.includes(col.property)) { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur[col.property]); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = Number(total.toFixed(2)).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | dataList.value.reduce((sum, item) => sum + Number(item.amount ?? 0), 0) |
| | | ); |
| | | |
| | | const formatMoney = value => { |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "amount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.amount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const formatMoney = value => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value) |
| | | .toFixed(2) |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | applyDate: [{ required: true, message: "è¯·éæ©ç³è¯·æ¥æ", trigger: "change" }], |
| | | }; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "amount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.amount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | getTableData(); |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "inboundAmount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.inboundAmount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | proxy.download( |
| | | "/accountPurchase/exportAccountPurchaseInbound", |
| | | buildFilterParams(), |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | getTableData(); |
| | | }; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "totalAmount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.totalAmount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | proxy.download( |
| | | "/accountPurchase/exportAccountPurchaseReturn", |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | accountStatementDetails: selectedPurchases.value.map(buildDetailSubmitItem), |
| | | }); |
| | | |
| | | const summaryProps = ["openingBalance", "currentPlan", "currentActually", "closingBalance"]; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (summaryProps.includes(col.property)) { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur[col.property]); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = Number(total.toFixed(2)).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | |
| | | v-loading="tableLoading" |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | proxy.download("/accountInvoiceApplication/exportAccountInvoiceApplication", params, filename); |
| | | }; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "amount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.amount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { |
| | | minimumFractionDigits: 2, |
| | | maximumFractionDigits: 2, |
| | | }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | |
| | | v-loading="tableLoading" |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | amount: [{ required: true, message: "请è¾å
¥éé¢", trigger: "blur" }], |
| | | }; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "amount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.amount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { |
| | | minimumFractionDigits: 2, |
| | | maximumFractionDigits: 2, |
| | | }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | accountStatementDetails: selectedSales.value.map(buildDetailSubmitItem), |
| | | }); |
| | | |
| | | const summaryProps = ["openingBalance", "currentPlan", "currentActually", "closingBalance"]; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (summaryProps.includes(col.property)) { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur[col.property]); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = Number(total.toFixed(2)).toLocaleString("zh-CN", { |
| | | minimumFractionDigits: 2, |
| | | maximumFractionDigits: 2, |
| | | }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | label: "éé¢", |
| | | prop: "outboundAmount", |
| | | minWidth: "120", |
| | | align: "right", |
| | | formatData: val => |
| | | val === null || val === undefined || val === "" |
| | | ? "" |
| | |
| | | getTableData(); |
| | | }; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "outboundAmount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.outboundAmount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { |
| | | minimumFractionDigits: 2, |
| | | maximumFractionDigits: 2, |
| | | }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | proxy.download( |
| | | "/accountSales/exportAccountSalesOutbound", |
| | |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | isShowSummary |
| | | :summaryMethod="getSummaries" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | |
| | | label: "鿬¾æ»é¢", |
| | | prop: "refundAmount", |
| | | minWidth: "120", |
| | | align: "right", |
| | | formatData: (val) => |
| | | val === null || val === undefined || val === "" |
| | | ? "" |
| | |
| | | getTableData(); |
| | | }; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | | const sums = []; |
| | | columns.forEach((col, index) => { |
| | | if (index === 0) { |
| | | sums[index] = "å计"; |
| | | } else if (col.property === "refundAmount") { |
| | | const total = data.reduce((prev, cur) => { |
| | | const v = Number(cur.refundAmount); |
| | | return prev + (isNaN(v) ? 0 : v); |
| | | }, 0); |
| | | sums[index] = total.toLocaleString("zh-CN", { |
| | | minimumFractionDigits: 2, |
| | | maximumFractionDigits: 2, |
| | | }); |
| | | } else { |
| | | sums[index] = ""; |
| | | } |
| | | }); |
| | | return sums; |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | proxy.download( |
| | | "/accountSales/exportAccountSalesReturn", |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="getTableData">æç´¢</el-button> |
| | | <el-button type="primary" @click="onSearch">æç´¢</el-button> |
| | | <el-button @click="resetFilters">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | |
| | | return map[key] || ""; |
| | | }; |
| | | |
| | | const onSearch = () => { |
| | | pagination.currentPage = 1; |
| | | getTableData(); |
| | | }; |
| | | |
| | | // èè°çº¦å®ï¼åé¡µåæ°ä½¿ç¨ current/sizeï¼æ¥æèå´æå为 startDate/endDate |
| | | const getTableData = async () => { |
| | | try { |
| | |
| | | getTableData(); |
| | | }; |
| | | |
| | | const changePage = ({ current, size }) => { |
| | | pagination.currentPage = current; |
| | | pagination.pageSize = size; |
| | | const changePage = ({ page, limit }) => { |
| | | pagination.currentPage = page; |
| | | pagination.pageSize = limit; |
| | | getTableData(); |
| | | }; |
| | | |
| | |
| | | <el-form-item label="åºåæ°é" |
| | | prop="qualitity"> |
| | | <el-input-number v-model="formState.qualitity" |
| | | :step="1" |
| | | :min="1" |
| | | :step="0.01" |
| | | :min="0.01" |
| | | :precision="2" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¹å·" |
| | |
| | | label="åºåé¢è¦æ°é" |
| | | prop="warnNum"> |
| | | <el-input-number v-model="formState.warnNum" |
| | | :step="1" |
| | | :step="0.01" |
| | | :min="0" |
| | | :max="formState.qualitity" |
| | | :precision="2" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨" |
| | |
| | | {{ approvalTypeLabel(row.approvalType) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="ç³è¯·äººç¼å·">{{ row.applicantNo || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç³è¯·äººåç§°">{{ row.applicantName || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç³è¯·æè¦">{{ row.summary || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å起人ç¼å·">{{ row.applicantNo || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å起人åç§°">{{ row.applicantName || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æè¦">{{ row.summary || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item v-if="row.rejectReason" |
| | | label="驳ååå " |
| | | :span="2"> |
| | |
| | | |
| | | const tableColumn = ref([ |
| | | // { label: "ç³è¯·äººç¼å·", prop: "applicantNo", width: 110 }, |
| | | { label: "ç³è¯·äººåç§°", prop: "applicantName", minWidth: 100 }, |
| | | { label: "å起人", prop: "applicantName", minWidth: 100 }, |
| | | { label: "模æ¿ç±»å", prop: "businessName", minWidth: 120 }, |
| | | { |
| | | label: "审æ¹ç±»å", |
| | |
| | | prop: "unread", |
| | | width: 90, |
| | | align: "center", |
| | | dataType: "tag", |
| | | formatData: (v) => (v ? "æ¯" : "å¦"), |
| | | formatType: (v) => (v ? "success" : "danger"), |
| | | }, |
| | | { |
| | | label: "审æ¹ç¶æ", |
| | |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div class="search_actions"> |
| | | <el-button type="warning" plain @click="handleExport">导åº</el-button> |
| | | <!-- <el-button type="warning" plain @click="handleExport">导åº</el-button>--> |
| | | <el-button type="primary" @click="openAddWithTemplate">æ°å¢å çç³è¯·</el-button> |
| | | </div> |
| | | </div> |
| | |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div class="search_actions"> |
| | | <el-button type="success" plain @click="handleImportClick">导å
¥</el-button> |
| | | <el-button type="warning" plain @click="handleExport">导åº</el-button> |
| | | <!-- <el-button type="success" plain @click="handleImportClick">导å
¥</el-button>--> |
| | | <!-- <el-button type="warning" plain @click="handleExport">导åº</el-button>--> |
| | | <el-button type="primary" @click="openFormDialog('add')">æ°å¢è´¹ç¨æ¥é</el-button> |
| | | </div> |
| | | </div> |
| | |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div class="search_actions"> |
| | | <el-button type="success" plain @click="handleImportClick">导å
¥</el-button> |
| | | <el-button type="warning" plain @click="handleExport">导åº</el-button> |
| | | <!-- <el-button type="success" plain @click="handleImportClick">导å
¥</el-button>--> |
| | | <!-- <el-button type="warning" plain @click="handleExport">导åº</el-button>--> |
| | | <el-button type="primary" @click="openFormDialog('add')">æ°å¢å·®æ
æ¥é</el-button> |
| | | </div> |
| | | </div> |
| | |
| | | <template> |
| | | <div> |
| | | <FormDialog |
| | | v-model="dialogFormVisible" |
| | | :operation-type="operationType" |
| | | :title="dialogTitle" |
| | | width="80%" |
| | | @close="closeDia" |
| | | @confirm="submitForm" |
| | | @cancel="closeDia" |
| | | > |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-position="top"> |
| | | <FormDialog v-model="dialogFormVisible" |
| | | :operation-type="operationType" |
| | | :title="dialogTitle" |
| | | width="80%" |
| | | @close="closeDia" |
| | | @confirm="submitForm" |
| | | @cancel="closeDia"> |
| | | <el-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-position="top"> |
| | | <el-row :gutter="24"> |
| | | <!-- 左侧ï¼éç¨äººå --> |
| | | <el-col :span="8"> |
| | | <el-form-item label="éç¨äººåï¼" prop="deptIds"> |
| | | <el-form-item label="éç¨äººåï¼" |
| | | prop="deptIds"> |
| | | <div class="dept-checkbox-wrap"> |
| | | <el-checkbox-group |
| | | v-model="form.deptIds" |
| | | :disabled="isDetail" |
| | | > |
| | | <div |
| | | v-for="dept in deptList" |
| | | :key="dept.deptId" |
| | | class="dept-checkbox-item" |
| | | > |
| | | <el-checkbox-group v-model="form.deptIds" |
| | | :disabled="isDetail"> |
| | | <div v-for="dept in deptList" |
| | | :key="dept.deptId" |
| | | class="dept-checkbox-item"> |
| | | <el-checkbox :value="dept.deptId"> |
| | | {{ dept.deptName }} |
| | | <span v-if="dept.personCount != null" class="dept-count" |
| | | >{{ dept.personCount }}人</span |
| | | > |
| | | <span v-if="dept.personCount != null" |
| | | class="dept-count">{{ dept.personCount }}人</span> |
| | | </el-checkbox> |
| | | </div> |
| | | </el-checkbox-group> |
| | |
| | | <!-- å³ä¾§ï¼åºç¡ä¿¡æ¯ + ä¿é©ç±»å --> |
| | | <el-col :span="16"> |
| | | <!-- åºç¡ä¿¡æ¯ --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <el-card class="form-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"><span class="card-title-line">|</span> åºç¡ä¿¡æ¯</span> |
| | | <el-icon class="card-collapse"><ArrowUp /></el-icon> |
| | | <el-icon class="card-collapse"> |
| | | <ArrowUp /> |
| | | </el-icon> |
| | | </template> |
| | | <el-form-item label="æ¹æ¡æ é¢ï¼" prop="title"> |
| | | <el-input |
| | | v-model="form.title" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" |
| | | /> |
| | | <el-form-item label="æ¹æ¡æ é¢ï¼" |
| | | prop="title"> |
| | | <el-input v-model="form.title" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨ï¼" prop="remark"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | type="textarea" |
| | | :rows="2" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" |
| | | /> |
| | | <el-form-item label="夿³¨ï¼" |
| | | prop="remark"> |
| | | <el-input v-model="form.remark" |
| | | type="textarea" |
| | | :rows="2" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" /> |
| | | </el-form-item> |
| | | </el-card> |
| | | |
| | | <!-- ä¿é©ç±»å --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <el-card class="form-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"><span class="card-title-line">|</span> ä¿é©ç±»å</span> |
| | | <el-button |
| | | v-if="!isDetail" |
| | | type="primary" |
| | | size="small" |
| | | @click="addInsuranceBenefit" |
| | | > |
| | | <el-button v-if="!isDetail" |
| | | type="primary" |
| | | size="small" |
| | | @click="addInsuranceBenefit"> |
| | | æ·»å ä¿é©ç¦å© |
| | | </el-button> |
| | | </template> |
| | | <el-row :gutter="16"> |
| | | <el-col |
| | | v-for="(item, index) in form.insuranceBenefits" |
| | | :key="item._key" |
| | | :span="12" |
| | | > |
| | | <el-col v-for="(item, index) in form.insuranceBenefits" |
| | | :key="item._key" |
| | | :span="12"> |
| | | <div class="insurance-benefit-card"> |
| | | <div class="insurance-benefit-title"> |
| | | ä¿é©ç¦å©{{ index + 1 }} |
| | | <el-button |
| | | v-if="!isDetail && form.insuranceBenefits.length > 1" |
| | | type="danger" |
| | | link |
| | | size="small" |
| | | class="card-delete-btn" |
| | | @click="removeInsuranceBenefit(index)" |
| | | > |
| | | <el-button v-if="!isDetail && form.insuranceBenefits.length > 1" |
| | | type="danger" |
| | | link |
| | | size="small" |
| | | class="card-delete-btn" |
| | | @click="removeInsuranceBenefit(index)"> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | <el-form-item |
| | | :prop="'insuranceBenefits.' + index + '.insuranceType'" |
| | | label="ä¿é©ç±»åï¼" |
| | | label-width="100px" |
| | | > |
| | | <el-select |
| | | v-model="item.insuranceType" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="isDetail" |
| | | > |
| | | <el-option |
| | | v-for="opt in insuranceTypeOptions" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | <el-form-item :prop="'insuranceBenefits.' + index + '.insuranceType'" |
| | | label="ä¿é©ç±»åï¼" |
| | | label-width="100px"> |
| | | <el-select v-model="item.insuranceType" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="isDetail"> |
| | | <el-option v-for="opt in insuranceTypeOptions" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="ç¼´è´¹åºæ°ï¼" label-width="100px"> |
| | | <el-form-item label="ç¼´è´¹åºæ°ï¼" |
| | | label-width="100px"> |
| | | <div class="base-salary-wrap"> |
| | | <el-input |
| | | v-model="item.paymentBase" |
| | | placeholder="æ ¹æ®åºæ¬å·¥èµç¼´çº³" |
| | | clearable |
| | | style="width: 120px" |
| | | type="number" |
| | | :disabled="isDetail || item.useBasicSalary" |
| | | @input="handlePaymentBaseInput(item)" |
| | | /> |
| | | <el-checkbox |
| | | v-model="item.useBasicSalary" |
| | | @change="handleUseBasicSalaryChange(item)" |
| | | :disabled="isDetail" |
| | | > |
| | | <el-input v-model="item.paymentBase" |
| | | placeholder="æ ¹æ®åºæ¬å·¥èµç¼´çº³" |
| | | clearable |
| | | style="width: 120px" |
| | | type="number" |
| | | :disabled="isDetail || item.useBasicSalary" |
| | | @input="handlePaymentBaseInput(item)" /> |
| | | <el-checkbox v-model="item.useBasicSalary" |
| | | @change="handleUseBasicSalaryChange(item)" |
| | | :disabled="isDetail"> |
| | | è°ç¨åºæ¬å·¥èµ |
| | | </el-checkbox> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="个人缴费æ¯ä¾ï¼" label-width="100px"> |
| | | <el-form-item label="个人缴费æ¯ä¾ï¼" |
| | | label-width="100px"> |
| | | <div class="personal-ratio-wrap"> |
| | | <el-input |
| | | v-model="item.personalRatio" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 100px" |
| | | type="number" |
| | | :disabled="isDetail" |
| | | /> |
| | | <el-input v-model="item.personalRatio" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 100px" |
| | | type="number" |
| | | :disabled="isDetail" |
| | | :min="0" |
| | | @input="handlePersonalRatioInput(item)" /> |
| | | <span class="ratio-unit">(%)</span> |
| | | <span class="ratio-plus">+</span> |
| | | <el-input |
| | | v-model="item.personalFixed" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 100px" |
| | | type="number" |
| | | :disabled="isDetail" |
| | | /> |
| | | <el-input v-model="item.personalFixed" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 100px" |
| | | type="number" |
| | | :disabled="isDetail" |
| | | :min="0" |
| | | @input="handlePersonalFixedInput(item)" /> |
| | | </div> |
| | | </el-form-item> |
| | | </div> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, getCurrentInstance, nextTick, computed } from "vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { ArrowUp } from "@element-plus/icons-vue"; |
| | | import { listDept } from "@/api/system/dept.js"; |
| | | import { socialSecurityAdd, socialSecurityUpdate } from "@/api/personnelManagement/socialSecuritySet.js"; |
| | | import { |
| | | ref, |
| | | reactive, |
| | | toRefs, |
| | | getCurrentInstance, |
| | | nextTick, |
| | | computed, |
| | | } from "vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { ArrowUp } from "@element-plus/icons-vue"; |
| | | import { listDept } from "@/api/system/dept.js"; |
| | | import { |
| | | socialSecurityAdd, |
| | | socialSecurityUpdate, |
| | | } from "@/api/personnelManagement/socialSecuritySet.js"; |
| | | |
| | | const emit = defineEmits(["close"]); |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(["close"]); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref("add"); |
| | | const formRef = ref(null); |
| | | const deptList = ref([]); |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref("add"); |
| | | const formRef = ref(null); |
| | | const deptList = ref([]); |
| | | |
| | | const isDetail = computed(() => operationType.value === "detail"); |
| | | const isDetail = computed(() => operationType.value === "detail"); |
| | | |
| | | const dialogTitle = () => |
| | | operationType.value === "add" |
| | | ? "æ°å¢æ¹æ¡" |
| | | : operationType.value === "edit" |
| | | ? "ç¼è¾æ¹æ¡" |
| | | : "æ¹æ¡è¯¦æ
"; |
| | | const dialogTitle = () => |
| | | operationType.value === "add" |
| | | ? "æ°å¢æ¹æ¡" |
| | | : operationType.value === "edit" |
| | | ? "ç¼è¾æ¹æ¡" |
| | | : "æ¹æ¡è¯¦æ
"; |
| | | |
| | | // ä¿é©ç±»åé项ï¼å¯æåå
¸æ¿æ¢ï¼ |
| | | const insuranceTypeOptions = [ |
| | | { label: "å
»èä¿é©", value: "å
»èä¿é©" }, |
| | | { label: "å»çä¿é©", value: "å»çä¿é©" }, |
| | | { label: "失ä¸ä¿é©", value: "失ä¸ä¿é©" }, |
| | | { label: "工伤ä¿é©", value: "工伤ä¿é©" }, |
| | | { label: "çè²ä¿é©", value: "çè²ä¿é©" }, |
| | | { label: "å
¬ç§¯é", value: "å
¬ç§¯é" }, |
| | | ]; |
| | | // ä¿é©ç±»åé项ï¼å¯æåå
¸æ¿æ¢ï¼ |
| | | const insuranceTypeOptions = [ |
| | | { label: "å
»èä¿é©", value: "å
»èä¿é©" }, |
| | | { label: "å»çä¿é©", value: "å»çä¿é©" }, |
| | | { label: "失ä¸ä¿é©", value: "失ä¸ä¿é©" }, |
| | | { label: "工伤ä¿é©", value: "工伤ä¿é©" }, |
| | | { label: "çè²ä¿é©", value: "çè²ä¿é©" }, |
| | | { label: "å
¬ç§¯é", value: "å
¬ç§¯é" }, |
| | | ]; |
| | | |
| | | const defaultBenefit = () => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: "", |
| | | paymentBase: "", |
| | | useBasicSalary: false, |
| | | personalRatio: "", |
| | | personalFixed: "", |
| | | }); |
| | | const defaultBenefit = () => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: "", |
| | | paymentBase: "", |
| | | useBasicSalary: false, |
| | | personalRatio: "", |
| | | personalFixed: "", |
| | | }); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | id: undefined, |
| | | title: "", |
| | | remark: "", |
| | | deptIds: [], |
| | | insuranceBenefits: [defaultBenefit()], |
| | | }, |
| | | rules: { |
| | | title: [{ required: true, message: "请è¾å
¥æ¹æ¡æ é¢", trigger: "blur" }], |
| | | deptIds: [ |
| | | { |
| | | required: true, |
| | | type: "array", |
| | | min: 1, |
| | | message: "请è³å°éæ©ä¸ä¸ªéç¨é¨é¨", |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | const data = reactive({ |
| | | form: { |
| | | id: undefined, |
| | | title: "", |
| | | remark: "", |
| | | deptIds: [], |
| | | insuranceBenefits: [defaultBenefit()], |
| | | }, |
| | | rules: { |
| | | title: [{ required: true, message: "请è¾å
¥æ¹æ¡æ é¢", trigger: "blur" }], |
| | | deptIds: [ |
| | | { |
| | | required: true, |
| | | type: "array", |
| | | min: 1, |
| | | message: "请è³å°éæ©ä¸ä¸ªéç¨é¨é¨", |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | |
| | | function flattenDept(tree, list = []) { |
| | | if (!tree || !tree.length) return list; |
| | | tree.forEach((node) => { |
| | | list.push({ |
| | | deptId: node.deptId, |
| | | deptName: node.deptName, |
| | | personCount: node.personCount ?? null, |
| | | function flattenDept(tree, list = []) { |
| | | if (!tree || !tree.length) return list; |
| | | tree.forEach(node => { |
| | | list.push({ |
| | | deptId: node.deptId, |
| | | deptName: node.deptName, |
| | | personCount: node.personCount ?? null, |
| | | }); |
| | | if (node.children && node.children.length) { |
| | | flattenDept(node.children, list); |
| | | } |
| | | }); |
| | | if (node.children && node.children.length) { |
| | | flattenDept(node.children, list); |
| | | } |
| | | }); |
| | | return list; |
| | | } |
| | | |
| | | const loadDeptList = () => { |
| | | listDept().then((res) => { |
| | | const tree = res.data ?? []; |
| | | deptList.value = flattenDept(tree); |
| | | }); |
| | | }; |
| | | |
| | | const addInsuranceBenefit = () => { |
| | | form.value.insuranceBenefits.push(defaultBenefit()); |
| | | }; |
| | | |
| | | const removeInsuranceBenefit = (index) => { |
| | | form.value.insuranceBenefits.splice(index, 1); |
| | | }; |
| | | |
| | | const handleUseBasicSalaryChange = (item) => { |
| | | if (item.useBasicSalary) { |
| | | item.paymentBase = ""; |
| | | return list; |
| | | } |
| | | }; |
| | | |
| | | const handlePaymentBaseInput = (item) => { |
| | | if (item.paymentBase !== "" && item.paymentBase != null) { |
| | | item.useBasicSalary = false; |
| | | } |
| | | }; |
| | | |
| | | const resetForm = () => { |
| | | form.value = { |
| | | id: undefined, |
| | | title: "", |
| | | remark: "", |
| | | deptIds: [], |
| | | insuranceBenefits: [defaultBenefit()], |
| | | const loadDeptList = () => { |
| | | listDept().then(res => { |
| | | const tree = res.data ?? []; |
| | | deptList.value = flattenDept(tree); |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | loadDeptList(); |
| | | resetForm(); |
| | | if ((type === "edit" || type === "detail") && row) { |
| | | const d = row || {}; |
| | | form.value.id = d.id; |
| | | form.value.title = d.title; |
| | | form.value.remark = d.remark ?? ""; |
| | | // deptIds å端å¯è½æ¯éå·åéåç¬¦ä¸²ææ°ç»ï¼è¿éç»ä¸è½¬ä¸ºæ°ç»å¹¶å°½éè¿åæ°å¼ç±»å |
| | | if (d.deptIds) { |
| | | form.value.deptIds = String(d.deptIds) |
| | | .split(",") |
| | | .filter((v) => v !== "") |
| | | .map((v) => { |
| | | const num = Number(v); |
| | | return Number.isNaN(num) ? v : num; |
| | | }); |
| | | } else { |
| | | form.value.deptIds = []; |
| | | const addInsuranceBenefit = () => { |
| | | form.value.insuranceBenefits.push(defaultBenefit()); |
| | | }; |
| | | |
| | | const removeInsuranceBenefit = index => { |
| | | form.value.insuranceBenefits.splice(index, 1); |
| | | }; |
| | | |
| | | const handleUseBasicSalaryChange = item => { |
| | | if (item.useBasicSalary) { |
| | | item.paymentBase = ""; |
| | | } |
| | | const detailList = d.schemeInsuranceDetailList || []; |
| | | form.value.insuranceBenefits = |
| | | detailList.length > 0 |
| | | ? detailList.map((b) => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: b.insuranceType || "", |
| | | paymentBase: b.paymentBase ?? "", |
| | | useBasicSalary: b.useBasicSalary === 2, |
| | | personalRatio: b.personalRatio ?? "", |
| | | personalFixed: b.personalFixed ?? "", |
| | | })) |
| | | : [defaultBenefit()]; |
| | | } |
| | | }; |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | // 详æ
模å¼ä¸ä¸æäº¤ï¼åªå
³éå¼¹çª |
| | | if (operationType.value === "detail") { |
| | | closeDia(); |
| | | return; |
| | | } |
| | | formRef.value?.validate((valid) => { |
| | | if (!valid) return; |
| | | const deptIds = |
| | | Array.isArray(form.value.deptIds) && form.value.deptIds.length |
| | | ? form.value.deptIds.join(",") |
| | | : ""; |
| | | const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map( |
| | | ({ _key, ...rest }) => ({ |
| | | ...rest, |
| | | useBasicSalary: rest.useBasicSalary ? 2 : 1, |
| | | }) |
| | | ); |
| | | const insuranceTypes = schemeInsuranceDetailList |
| | | .map((item) => item.insuranceType) |
| | | .filter((v) => v) |
| | | .join(","); |
| | | // é¨é¨åç§°ï¼å¤ä¸ªä½¿ç¨éå·éå¼ï¼æ ¹æ®éä¸ç deptIds ä¸ deptList 计ç®ï¼ |
| | | const deptNames = (deptList.value || []) |
| | | .filter((d) => |
| | | (form.value.deptIds || []).some( |
| | | (id) => String(id) === String(d.deptId) |
| | | ) |
| | | ) |
| | | .map((d) => d.deptName) |
| | | .join(","); |
| | | const submitData = { |
| | | id: form.value.id, |
| | | title: form.value.title, |
| | | remark: form.value.remark ?? "", |
| | | deptIds, |
| | | insuranceTypes, |
| | | deptNames, |
| | | schemeInsuranceDetailList, |
| | | const handlePaymentBaseInput = item => { |
| | | if (item.paymentBase !== "" && item.paymentBase != null) { |
| | | item.useBasicSalary = false; |
| | | } |
| | | }; |
| | | |
| | | const handlePersonalRatioInput = item => { |
| | | if (item.personalRatio !== "" && item.personalRatio != null) { |
| | | const value = Number(item.personalRatio); |
| | | if (value < 0) { |
| | | item.personalRatio = ""; |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handlePersonalFixedInput = item => { |
| | | if (item.personalFixed !== "" && item.personalFixed != null) { |
| | | const value = Number(item.personalFixed); |
| | | if (value < 0) { |
| | | item.personalFixed = ""; |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const resetForm = () => { |
| | | form.value = { |
| | | id: undefined, |
| | | title: "", |
| | | remark: "", |
| | | deptIds: [], |
| | | insuranceBenefits: [defaultBenefit()], |
| | | }; |
| | | if (operationType.value === "add") { |
| | | socialSecurityAdd(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | socialSecurityUpdate(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå"); |
| | | closeDia(); |
| | | }); |
| | | }; |
| | | |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | loadDeptList(); |
| | | resetForm(); |
| | | if ((type === "edit" || type === "detail") && row) { |
| | | const d = row || {}; |
| | | form.value.id = d.id; |
| | | form.value.title = d.title; |
| | | form.value.remark = d.remark ?? ""; |
| | | // deptIds å端å¯è½æ¯éå·åéåç¬¦ä¸²ææ°ç»ï¼è¿éç»ä¸è½¬ä¸ºæ°ç»å¹¶å°½éè¿åæ°å¼ç±»å |
| | | if (d.deptIds) { |
| | | form.value.deptIds = String(d.deptIds) |
| | | .split(",") |
| | | .filter(v => v !== "") |
| | | .map(v => { |
| | | const num = Number(v); |
| | | return Number.isNaN(num) ? v : num; |
| | | }); |
| | | } else { |
| | | form.value.deptIds = []; |
| | | } |
| | | const detailList = d.schemeInsuranceDetailList || []; |
| | | form.value.insuranceBenefits = |
| | | detailList.length > 0 |
| | | ? detailList.map(b => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: b.insuranceType || "", |
| | | paymentBase: b.paymentBase ?? "", |
| | | useBasicSalary: b.useBasicSalary === 2, |
| | | personalRatio: b.personalRatio ?? "", |
| | | personalFixed: b.personalFixed ?? "", |
| | | })) |
| | | : [defaultBenefit()]; |
| | | } |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | const closeDia = () => { |
| | | proxy.resetForm?.("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | const submitForm = () => { |
| | | // 详æ
模å¼ä¸ä¸æäº¤ï¼åªå
³éå¼¹çª |
| | | if (operationType.value === "detail") { |
| | | closeDia(); |
| | | return; |
| | | } |
| | | formRef.value?.validate(valid => { |
| | | if (!valid) return; |
| | | const deptIds = |
| | | Array.isArray(form.value.deptIds) && form.value.deptIds.length |
| | | ? form.value.deptIds.join(",") |
| | | : ""; |
| | | const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map( |
| | | ({ _key, ...rest }) => ({ |
| | | ...rest, |
| | | useBasicSalary: rest.useBasicSalary ? 2 : 1, |
| | | }) |
| | | ); |
| | | const insuranceTypes = schemeInsuranceDetailList |
| | | .map(item => item.insuranceType) |
| | | .filter(v => v) |
| | | .join(","); |
| | | // é¨é¨åç§°ï¼å¤ä¸ªä½¿ç¨éå·éå¼ï¼æ ¹æ®éä¸ç deptIds ä¸ deptList 计ç®ï¼ |
| | | const deptNames = (deptList.value || []) |
| | | .filter(d => |
| | | (form.value.deptIds || []).some(id => String(id) === String(d.deptId)) |
| | | ) |
| | | .map(d => d.deptName) |
| | | .join(","); |
| | | const submitData = { |
| | | id: form.value.id, |
| | | title: form.value.title, |
| | | remark: form.value.remark ?? "", |
| | | deptIds, |
| | | insuranceTypes, |
| | | deptNames, |
| | | schemeInsuranceDetailList, |
| | | }; |
| | | if (operationType.value === "add") { |
| | | socialSecurityAdd(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | socialSecurityUpdate(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå"); |
| | | closeDia(); |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | const closeDia = () => { |
| | | proxy.resetForm?.("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | .form-card :deep(.el-card__header) { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 16px; |
| | | } |
| | | .card-title { |
| | | font-weight: 500; |
| | | } |
| | | .card-collapse { |
| | | color: #999; |
| | | cursor: pointer; |
| | | } |
| | | .dept-checkbox-wrap { |
| | | max-height: 320px; |
| | | overflow-y: auto; |
| | | padding: 8px 0; |
| | | border: 1px solid var(--el-border-color); |
| | | border-radius: 4px; |
| | | background: #fff; |
| | | } |
| | | .dept-checkbox-item { |
| | | padding: 6px 12px; |
| | | } |
| | | .dept-count { |
| | | color: #909399; |
| | | font-size: 12px; |
| | | margin-left: 4px; |
| | | } |
| | | .insurance-benefit-card { |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | border-radius: 4px; |
| | | padding: 12px 16px; |
| | | margin-bottom: 12px; |
| | | background: #fafafa; |
| | | } |
| | | .insurance-benefit-title { |
| | | font-size: 14px; |
| | | margin-bottom: 12px; |
| | | font-weight: 500; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | .card-delete-btn { |
| | | margin-left: auto; |
| | | } |
| | | .checkbox-group-inline { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | .base-salary-wrap { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .base-salary-text { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | .personal-ratio-wrap { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .ratio-unit, |
| | | .ratio-plus { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | .form-card :deep(.el-card__header) { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 16px; |
| | | } |
| | | .card-title { |
| | | font-weight: 500; |
| | | } |
| | | .card-collapse { |
| | | color: #999; |
| | | cursor: pointer; |
| | | } |
| | | .dept-checkbox-wrap { |
| | | max-height: 320px; |
| | | overflow-y: auto; |
| | | padding: 8px 0; |
| | | border: 1px solid var(--el-border-color); |
| | | border-radius: 4px; |
| | | background: #fff; |
| | | } |
| | | .dept-checkbox-item { |
| | | padding: 6px 12px; |
| | | } |
| | | .dept-count { |
| | | color: #909399; |
| | | font-size: 12px; |
| | | margin-left: 4px; |
| | | } |
| | | .insurance-benefit-card { |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | border-radius: 4px; |
| | | padding: 12px 16px; |
| | | margin-bottom: 12px; |
| | | background: #fafafa; |
| | | } |
| | | .insurance-benefit-title { |
| | | font-size: 14px; |
| | | margin-bottom: 12px; |
| | | font-weight: 500; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | .card-delete-btn { |
| | | margin-left: auto; |
| | | } |
| | | .checkbox-group-inline { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | .base-salary-wrap { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .base-salary-text { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | .personal-ratio-wrap { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .ratio-unit, |
| | | .ratio-plus { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="material-node"> |
| | | <!-- å½åèç¹å¡ç --> |
| | | <div :class="['node-card', isRoot ? 'root-card' : (row.nodeType === 'semiFinished' ? 'semi-finished-card' : 'child-card')]"> |
| | | <div class="node-header"> |
| | | <div class="node-label"> |
| | | <el-tag :type="isRoot ? '' : (row.nodeType === 'semiFinished' ? 'warning' : 'success')" size="small" effect="dark"> |
| | | {{ isRoot ? 'æå' : (row.nodeType === 'semiFinished' ? 'åæå' : 'åæ') }} |
| | | </el-tag> |
| | | <span class="node-title">{{ row.productName || 'æªéæ©äº§å' }}</span> |
| | | <span v-if="row.model" class="node-sub">è§æ ¼: {{ row.model }}</span> |
| | | <span v-if="row.unit" class="node-sub">åä½: {{ row.unit }}</span> |
| | | </div> |
| | | <div class="node-actions"> |
| | | <template v-if="editable && (isRoot || row.nodeType === 'semiFinished')"> |
| | | <el-button type="primary" |
| | | text |
| | | size="small" |
| | | @click="handleAdd('semiFinished')"> |
| | | + æ·»å åæå |
| | | </el-button> |
| | | <el-button type="primary" |
| | | text |
| | | size="small" |
| | | @click="handleAdd('rawMaterial')"> |
| | | + æ·»å åæ |
| | | </el-button> |
| | | </template> |
| | | <el-button v-if="editable" |
| | | type="danger" |
| | | text |
| | | size="small" |
| | | @click="$emit('remove', row.tempId)"> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- ç¼è¾æ¨¡å¼ä¸ç表å --> |
| | | <div v-if="editable" class="node-body"> |
| | | <el-row :gutter="12"> |
| | | <el-col :span="7"> |
| | | <el-form-item label="产å" :rules="[{ required: true, message: 'è¯·éæ©äº§å' }]" style="margin:0"> |
| | | <el-input :model-value="row.productName || ''" |
| | | readonly |
| | | placeholder="ç¹å»éæ©äº§å" |
| | | @click="openSelect" |
| | | style="width:100%"> |
| | | <template #suffix> |
| | | <el-icon><component :is="SearchIcon" /></el-icon> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="è§æ ¼" style="margin:0"> |
| | | <el-select v-model="row.model" |
| | | placeholder="è¯·éæ©è§æ ¼" |
| | | clearable |
| | | style="width:100%" |
| | | @visible-change="(v:boolean) => { if (v) openSelect() }"> |
| | | <el-option v-if="row.model" :label="row.model" :value="row.model" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col v-if="!isRoot" :span="5"> |
| | | <el-form-item label="å·¥åº" :rules="[{ required: true, message: 'è¯·éæ©å·¥åº' }]" style="margin:0"> |
| | | <el-select v-model="row.processId" |
| | | placeholder="è¯·éæ©" |
| | | filterable |
| | | clearable |
| | | style="width:100%" |
| | | @change="(v:any) => $emit('processChange', row, v)"> |
| | | <el-option v-for="item in processOptions" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="æ°é" :rules="[{ required: true, message: 'è¯·å¡«åæ°é' }]" style="margin:0"> |
| | | <el-input-number v-model="row.unitQuantity" |
| | | :min="0" |
| | | :precision="2" |
| | | :step="1" |
| | | controls-position="right" |
| | | style="width:100%" |
| | | @change="$emit('quantityChange')" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="3"> |
| | | <el-form-item label="åä½" style="margin:0"> |
| | | <el-input v-model="row.unit" placeholder="åä½" clearable style="width:100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <!-- éç¼è¾æ¨¡å¼ï¼ç®æ´æ¾ç¤º --> |
| | | <div v-else class="node-view"> |
| | | <span v-if="!isRoot && row.processName">å·¥åº: {{ row.processName }} | </span> |
| | | <span>æ°é: {{ row.unitQuantity || '-' }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- é彿¸²æåèç¹ --> |
| | | <div v-if="row.children && row.children.length > 0" class="node-children"> |
| | | <MaterialCard |
| | | v-for="child in row.children" |
| | | :key="child.tempId" |
| | | :row="child" |
| | | :depth="depth + 1" |
| | | :editable="editable" |
| | | :process-options="processOptions" |
| | | @remove="(id:string) => $emit('remove', id)" |
| | | @add="(id:string, nodeType:string) => $emit('add', id, nodeType)" |
| | | @select-product="(tempId: string, data: any) => $emit('selectProduct', tempId, data)" |
| | | @process-change="(row: any, v: any) => $emit('processChange', row, v)" |
| | | @quantity-change="$emit('quantityChange')" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { computed } from 'vue' |
| | | import { Search } from '@element-plus/icons-vue' |
| | | |
| | | const SearchIcon = Search |
| | | |
| | | const props = defineProps<{ |
| | | row: any |
| | | depth: number |
| | | editable: boolean |
| | | processOptions: any[] |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | | remove: [tempId: string] |
| | | add: [tempId: string, nodeType: string] |
| | | selectProduct: [tempId: string, data: any] |
| | | processChange: [row: any, value: any] |
| | | quantityChange: [] |
| | | }>() |
| | | |
| | | const isRoot = computed(() => props.depth === 0) |
| | | |
| | | const openSelect = () => { |
| | | emit('selectProduct', props.row.tempId, null) |
| | | } |
| | | |
| | | const handleAdd = (nodeType: string) => { |
| | | emit('add', props.row.tempId, nodeType) |
| | | } |
| | | </script> |
| | | |
| | | <script lang="ts"> |
| | | export default { name: 'MaterialCard' } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .material-node { |
| | | margin: 4px 0; |
| | | } |
| | | |
| | | .node-card { |
| | | border: 1px solid #e4e7ed; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .root-card { |
| | | border-color: #409eff; |
| | | border-left: 4px solid #409eff; |
| | | background-color: #f0f5ff; |
| | | } |
| | | |
| | | .child-card { |
| | | border-left: 4px solid #67c23a; |
| | | background-color: #f0f9eb; |
| | | } |
| | | |
| | | .semi-finished-card { |
| | | border-left: 4px solid #e6a23c; |
| | | background-color: #fdf6ec; |
| | | } |
| | | |
| | | .node-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 8px 12px; |
| | | background-color: rgba(0,0,0,0.03); |
| | | flex-wrap: wrap; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .node-label { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .node-title { |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .node-sub { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .node-actions { |
| | | display: flex; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .node-body { |
| | | padding: 10px 12px; |
| | | } |
| | | |
| | | .node-view { |
| | | padding: 6px 12px; |
| | | font-size: 13px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .node-children { |
| | | margin-left: 36px; |
| | | padding-left: 16px; |
| | | border-left: 2px dashed #dcdfe6; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <PageHeader content="产åç»æè¯¦æ
"> |
| | | <template #right-button> |
| | | <el-button v-if="!dataValue.isEdit && !isOrderPage" |
| | | type="primary" |
| | | @click="dataValue.isEdit = true">ç¼è¾ |
| | | </el-button> |
| | | <el-button v-if="dataValue.isEdit && !isOrderPage" |
| | | type="primary" |
| | | @click="cancelEdit">åæ¶ |
| | | </el-button> |
| | | <el-button v-if="!isOrderPage" |
| | | type="primary" |
| | | :loading="dataValue.loading" |
| | | @click="submit" |
| | | :disabled="!dataValue.isEdit">确认 |
| | | </el-button> |
| | | </template> |
| | | </PageHeader> |
| | | <el-table :data="tableData" |
| | | border |
| | | :preserve-expanded-content="false" |
| | | :default-expand-all="true" |
| | | style="width: 100%"> |
| | | <el-table-column type="expand"> |
| | | <template #default="props"> |
| | | <el-form ref="form" :model="dataValue"> |
| | | <div class="tree-container"> |
| | | <div class="tree-legend"> |
| | | <el-tag type="" size="small" effect="dark">æå</el-tag> |
| | | <span style="margin:0 4px">â æä¸å±ï¼äº§åºç©ï¼</span> |
| | | <el-divider direction="vertical" /> |
| | | <el-tag type="warning" size="small" effect="dark">åæå</el-tag> |
| | | <span style="margin:0 4px">ï¼å¯ç»§ç»å±å¼ï¼</span> |
| | | <el-divider direction="vertical" /> |
| | | <span style="margin:0 4px">æä¸å±ï¼æå
¥ç©ï¼â</span> |
| | | <el-tag type="success" size="small" effect="dark">åæ</el-tag> |
| | | </div> |
| | | |
| | | <div v-if="dataValue.dataList.length === 0 && dataValue.isEdit" class="empty-hint"> |
| | | 请ç¹å»ä¸æ¹æé®æ·»å æå |
| | | </div> |
| | | |
| | | <MaterialCard |
| | | v-for="(item, index) in dataValue.dataList" |
| | | :key="item.tempId" |
| | | :row="item" |
| | | :depth="0" |
| | | :editable="dataValue.isEdit" |
| | | :process-options="dataValue.processOptions" |
| | | @remove="(id: string) => removeItem(id)" |
| | | @add="(id: string, nodeType: string) => addChildItem(id, nodeType)" |
| | | @select-product="(tempId: string, _data: any) => { dataValue.currentRowName = tempId; dataValue.showProductDialog = true }" |
| | | @process-change="(row: any, v: any) => handleProcessChange(row, v)" |
| | | @quantity-change="handleUnitQuantityChange" |
| | | /> |
| | | |
| | | <el-button v-if="dataValue.isEdit" |
| | | type="primary" |
| | | plain |
| | | style="margin-top:12px" |
| | | @click="addRootItem"> |
| | | + æ·»å æå |
| | | </el-button> |
| | | </div> |
| | | </el-form> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="BOMç¼å·" |
| | | prop="bomNo" /> |
| | | <el-table-column label="产ååç§°" |
| | | prop="productName" /> |
| | | <el-table-column label="è§æ ¼åå·" |
| | | prop="model" /> |
| | | </el-table> |
| | | <product-select-dialog v-if="dataValue.showProductDialog" |
| | | v-model:model-value="dataValue.showProductDialog" |
| | | :single="true" |
| | | @confirm="handleProduct" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { |
| | | computed, |
| | | defineAsyncComponent, |
| | | defineComponent, |
| | | onMounted, |
| | | reactive, |
| | | ref, |
| | | } from "vue"; |
| | | import { |
| | | queryList, |
| | | addBomDetail, |
| | | } from "@/api/productionManagement/productStructure.js"; |
| | | import { listProcessBom } from "@/api/productionManagement/productionOrder.js"; |
| | | import { list } from "@/api/productionManagement/productionProcess"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | |
| | | defineComponent({ |
| | | name: "StructureEdit", |
| | | }); |
| | | |
| | | const ProductSelectDialog = defineAsyncComponent( |
| | | () => import("@/views/basicData/product/ProductSelectDialog.vue") |
| | | ); |
| | | import MaterialCard from "./MaterialCard.vue"; |
| | | const emit = defineEmits(["update:router"]); |
| | | const form = ref(); |
| | | |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | | const routeId = computed({ |
| | | get() { |
| | | return route.query.id; |
| | | }, |
| | | |
| | | set(val) { |
| | | emit("update:router", val); |
| | | }, |
| | | }); |
| | | |
| | | // ä»è·¯ç±åæ°è·å产åä¿¡æ¯ |
| | | const routeBomNo = computed(() => route.query.bomNo || ""); |
| | | const routeProductName = computed(() => route.query.productName || ""); |
| | | const routeProductModelName = computed( |
| | | () => route.query.productModelName || "" |
| | | ); |
| | | const routeOrderId = computed(() => route.query.orderId); |
| | | const pageType = computed(() => route.query.type); |
| | | const isOrderPage = computed( |
| | | () => pageType.value === "order" && routeOrderId.value |
| | | ); |
| | | |
| | | const dataValue = reactive({ |
| | | dataList: [], |
| | | productOptions: [], |
| | | processOptions: [], |
| | | showProductDialog: false, |
| | | currentRowIndex: null, |
| | | currentRowName: null, |
| | | loading: false, |
| | | isEdit: false, |
| | | }); |
| | | |
| | | const normalizeListData = (source: any) => { |
| | | if (Array.isArray(source)) { |
| | | return source; |
| | | } |
| | | if (Array.isArray(source?.records)) { |
| | | return source.records; |
| | | } |
| | | return []; |
| | | }; |
| | | |
| | | const getProcessOptionById = (id: any) => { |
| | | if (id === undefined || id === null || id === "") { |
| | | return null; |
| | | } |
| | | return ( |
| | | normalizeListData(dataValue.processOptions).find( |
| | | option => String(option.id) === String(id) |
| | | ) || null |
| | | ); |
| | | }; |
| | | |
| | | const syncProcessOperationFields = (item: any) => { |
| | | const processId = item.processId ?? item.operationId ?? ""; |
| | | if (!processId) { |
| | | item.processId = ""; |
| | | item.operationId = ""; |
| | | item.processName = ""; |
| | | item.operationName = ""; |
| | | return; |
| | | } |
| | | |
| | | const option = getProcessOptionById(processId); |
| | | const processName = |
| | | option?.name || item.processName || item.operationName || ""; |
| | | |
| | | item.processId = processId; |
| | | item.operationId = processId; |
| | | item.processName = processName; |
| | | item.operationName = processName; |
| | | }; |
| | | |
| | | const normalizeTreeData = (items: any[], depth: number = 0) => { |
| | | items.forEach((item: any) => { |
| | | item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}`; |
| | | syncProcessOperationFields(item); |
| | | if (depth > 0 && !item.nodeType) { |
| | | item.nodeType = Array.isArray(item.children) && item.children.length > 0 |
| | | ? 'semiFinished' |
| | | : 'rawMaterial'; |
| | | } |
| | | if (Array.isArray(item.children) && item.children.length > 0) { |
| | | normalizeTreeData(item.children, depth + 1); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const toQuantityNumber = (value: any) => { |
| | | const numberValue = Number(value); |
| | | if (!Number.isFinite(numberValue)) { |
| | | return 0; |
| | | } |
| | | return Number(numberValue.toFixed(2)); |
| | | }; |
| | | |
| | | const syncDemandedQuantityTree = ( |
| | | items: any[], |
| | | parentDemandedQuantity: number | null = null |
| | | ) => { |
| | | items.forEach((item: any) => { |
| | | if (parentDemandedQuantity !== null) { |
| | | item.demandedQuantity = toQuantityNumber( |
| | | parentDemandedQuantity * toQuantityNumber(item.unitQuantity) |
| | | ); |
| | | } |
| | | |
| | | if (Array.isArray(item.children) && item.children.length > 0) { |
| | | syncDemandedQuantityTree( |
| | | item.children, |
| | | toQuantityNumber(item.demandedQuantity) |
| | | ); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const recalculateDemandedQuantities = () => { |
| | | if (!isOrderPage.value) { |
| | | return; |
| | | } |
| | | |
| | | syncDemandedQuantityTree(dataValue.dataList); |
| | | }; |
| | | |
| | | const buildSubmitTree = (items: any[]) => { |
| | | return items.map((item: any) => { |
| | | const current = { ...item }; |
| | | syncProcessOperationFields(current); |
| | | current.children = Array.isArray(current.children) |
| | | ? buildSubmitTree(current.children) |
| | | : []; |
| | | return current; |
| | | }); |
| | | }; |
| | | |
| | | const findSiblings = (items: any[], tempId: string): any[] | null => { |
| | | if (!items || items.length === 0) return null; |
| | | // æ£æ¥å½åå±çº§ |
| | | if (items.some(item => item.tempId === tempId)) { |
| | | return items; |
| | | } |
| | | // é彿¥æ¾å级 |
| | | for (const item of items) { |
| | | if (item.children && item.children.length > 0) { |
| | | const result = findSiblings(item.children, tempId); |
| | | if (result) return result; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const handleProcessChange = (row: any, value: any) => { |
| | | row.processId = value || ""; |
| | | syncProcessOperationFields(row); |
| | | |
| | | // æ£æ¥åä¸å±çº§æ¯å¦å·²ç»æå
¶ä»ä¸åçå·¥åºè¢«éä¸ |
| | | const siblings = findSiblings(dataValue.dataList, row.tempId); |
| | | if (siblings && value) { |
| | | const hasDifferentProcess = siblings.some(sibling => { |
| | | return sibling.tempId !== row.tempId && sibling.processId && sibling.processId !== value; |
| | | }); |
| | | if (hasDifferentProcess) { |
| | | ElMessage.warning("åä¸å±çº§å·²åå¨ä¸åçå·¥åºï¼è¯·å
ç»ä¸å·¥åºååè¿è¡ä¿®æ¹"); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handleUnitQuantityChange = () => { |
| | | recalculateDemandedQuantities(); |
| | | }; |
| | | |
| | | const tableData = reactive([ |
| | | { |
| | | productName: "", |
| | | model: "", |
| | | bomNo: "", |
| | | }, |
| | | ]); |
| | | |
| | | const openDialog = (tempId: any) => { |
| | | console.log(tempId, "tempId"); |
| | | dataValue.currentRowName = tempId; |
| | | dataValue.showProductDialog = true; |
| | | }; |
| | | |
| | | const fetchData = async () => { |
| | | if (isOrderPage.value) { |
| | | // 订åæ
åµï¼ä½¿ç¨è®¢åç产åç»ææ¥å£ |
| | | const { data } = await listProcessBom({ orderId: routeOrderId.value }); |
| | | dataValue.dataList = (data as any) || []; |
| | | normalizeTreeData(dataValue.dataList); |
| | | recalculateDemandedQuantities(); |
| | | } else { |
| | | // é订åæ
åµï¼ä½¿ç¨åæ¥çæ¥å£ |
| | | const { data } = await queryList(routeId.value); |
| | | dataValue.dataList = (data as any) || []; |
| | | console.log(dataValue); |
| | | normalizeTreeData(dataValue.dataList); |
| | | console.log(dataValue.dataList, "dataValue.dataList"); |
| | | } |
| | | }; |
| | | |
| | | const fetchProcessOptions = async () => { |
| | | const { data } = await list({}); |
| | | console.log(data, "dataValue.dataList"); |
| | | dataValue.processOptions = normalizeListData(data); |
| | | }; |
| | | |
| | | const handleProduct = (row: any) => { |
| | | if (!Array.isArray(row) || row.length === 0) { |
| | | ElMessage.warning("è¯·éæ©ä¸ä¸ªäº§å"); |
| | | return; |
| | | } |
| | | // åªå
许ä¸ä¸ªï¼å¦æä¸æ¸¸è¿åäºå¤ä¸ªï¼é»è®¤ä½¿ç¨æå䏿¬¡éæ©å¹¶è¦çå½åå¼ |
| | | const productData = row[row.length - 1]; |
| | | |
| | | // æå¤å±ç»ä»¶ä¸ï¼ä¸å½å产åç¸åç产ååªè½æä¸ä¸ª |
| | | const isTopLevel = dataValue.dataList.some( |
| | | item => (item as any).tempId === dataValue.currentRowName |
| | | ); |
| | | if (isTopLevel) { |
| | | if ( |
| | | productData.productName === tableData[0].productName && |
| | | productData.model === tableData[0].model |
| | | ) { |
| | | // æ¥æ¾æ¯å¦å·²ç»æå
¶ä»é¡¶å±è¡å·²ç»æ¯è¿ä¸ªäº§å |
| | | const hasOther = dataValue.dataList.some( |
| | | item => |
| | | (item as any).tempId !== dataValue.currentRowName && |
| | | (item as any).productName === tableData[0].productName && |
| | | (item as any).model === tableData[0].model |
| | | ); |
| | | if (hasOther) { |
| | | ElMessage.warning("æå¤å±åå½å产å䏿 ·çä¸çº§åªè½æä¸ä¸ª"); |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | // dataValue.dataList[dataValue.currentRowIndex].productName = |
| | | // row[0].productName; |
| | | // dataValue.dataList[dataValue.currentRowIndex].model = row[0].model; |
| | | // dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id; |
| | | // dataValue.dataList[dataValue.currentRowIndex].unit = row[0].unit || ""; |
| | | dataValue.dataList.map(item => { |
| | | if (item.tempId === dataValue.currentRowName) { |
| | | item.productName = productData.productName; |
| | | item.model = productData.model; |
| | | item.productModelId = productData.id; |
| | | item.unit = productData.unit || ""; |
| | | return; |
| | | } |
| | | childItem(item, dataValue.currentRowName, productData); |
| | | }); |
| | | dataValue.showProductDialog = false; |
| | | }; |
| | | const childItem = (item: any, tempId: any, productData: any) => { |
| | | if (item.tempId === tempId) { |
| | | item.productName = productData.productName; |
| | | item.model = productData.model; |
| | | item.productModelId = productData.id; |
| | | item.unit = productData.unit || ""; |
| | | return true; |
| | | } |
| | | if (item.children && item.children.length > 0) { |
| | | for (let child of item.children) { |
| | | if (childItem(child, tempId, productData)) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | }; |
| | | |
| | | // é彿 ¡éªææå±çº§çè¡¨åæ°æ® |
| | | const validateAll = () => { |
| | | let isValid = true; |
| | | |
| | | // æ ¡éªä¸ç»å
å¼èç¹çå·¥åºæ¯å¦é½ç¸å |
| | | const checkProcessUniqueness = (items: any[]) => { |
| | | if (!items || items.length === 0 || !isValid) return; |
| | | |
| | | // è·å第ä¸ä¸ªé空çå·¥åºIDä½ä¸ºåè |
| | | const firstProcessId = items.find(item => item.processId)?.processId; |
| | | |
| | | // 妿æå·¥åºIDï¼æ£æ¥ææé¡¹æ¯å¦é½ä½¿ç¨ç¸åçå·¥åº |
| | | if (firstProcessId) { |
| | | for (const item of items) { |
| | | if (item.processId && item.processId !== firstProcessId) { |
| | | const option1 = getProcessOptionById(firstProcessId); |
| | | const option2 = getProcessOptionById(item.processId); |
| | | const processName1 = option1?.name || "æªç¥å·¥åº"; |
| | | const processName2 = option2?.name || "æªç¥å·¥åº"; |
| | | ElMessage.error( |
| | | `å½åå±çº§ä¸å·¥åºä¸ä¸è´ï¼è¯·ä½¿ç¨ç¸åçå·¥åºãåå¨ã${processName1}ãåã${processName2}ã` |
| | | ); |
| | | isValid = false; |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // é彿 ¡éªå级çå
å¼èç¹ |
| | | for (const item of items) { |
| | | if (item.children && item.children.length > 0) { |
| | | checkProcessUniqueness(item.children); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // æ ¡éªå½æ° |
| | | const validateItem = (item: any, isTopLevel = false) => { |
| | | if (!isValid) return; |
| | | // æ ¡éªå½å项çå¿
å¡«åæ®µ |
| | | if (!item.model) { |
| | | ElMessage.error("è¯·éæ©è§æ ¼"); |
| | | isValid = false; |
| | | return; |
| | | } |
| | | if (!isTopLevel && !item.processId) { |
| | | ElMessage.error("è¯·éæ©æ¶èå·¥åº"); |
| | | isValid = false; |
| | | return; |
| | | } |
| | | if (!item.unitQuantity) { |
| | | ElMessage.error("请è¾å
¥åä½äº§åºæéæ°é"); |
| | | isValid = false; |
| | | return; |
| | | } |
| | | if (isOrderPage.value && !item.demandedQuantity) { |
| | | ElMessage.error("请è¾å
¥éæ±æ»é"); |
| | | isValid = false; |
| | | return; |
| | | } |
| | | // if (!item.unit) { |
| | | // ElMessage.error("请è¾å
¥åä½"); |
| | | // isValid = false; |
| | | // return; |
| | | // } |
| | | |
| | | // é彿 ¡éªå项忮µ |
| | | if (item.children && item.children.length > 0) { |
| | | item.children.forEach(child => { |
| | | validateItem(child, false); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // 1. é¦å
æ ¡éªåä¸ç¶çº§ä¸çå屿¶èå·¥åºæ¯å¦å¯ä¸ |
| | | checkProcessUniqueness(dataValue.dataList); |
| | | if (!isValid) return false; |
| | | |
| | | // 2. ç¶åéåæ ¡éªææé¡¶å±é¡¹çåæ®µå¿
å¡«æ
åµ |
| | | dataValue.dataList.forEach(item => { |
| | | validateItem(item, true); |
| | | }); |
| | | |
| | | return isValid; |
| | | }; |
| | | |
| | | const submit = () => { |
| | | dataValue.loading = true; |
| | | normalizeTreeData(dataValue.dataList); |
| | | recalculateDemandedQuantities(); |
| | | |
| | | // å
è¿è¡è¡¨åæ ¡éª |
| | | const valid = validateAll(); |
| | | console.log(dataValue.dataList, "dataValue.dataList"); |
| | | if (valid) { |
| | | addBomDetail({ |
| | | bomId: routeId.value, |
| | | children: buildSubmitTree(dataValue.dataList || []), |
| | | }) |
| | | .then(res => { |
| | | router.go(-1); |
| | | ElMessage.success("ä¿åæå"); |
| | | dataValue.loading = false; |
| | | }) |
| | | .catch(() => { |
| | | dataValue.loading = false; |
| | | }); |
| | | } else { |
| | | dataValue.loading = false; |
| | | } |
| | | }; |
| | | |
| | | const removeItem = (tempId: string) => { |
| | | const topIndex = dataValue.dataList.findIndex(item => item.tempId === tempId); |
| | | if (topIndex !== -1) { |
| | | dataValue.dataList.splice(topIndex, 1); |
| | | return; |
| | | } |
| | | |
| | | const delchildItem = (items: any[], tempId: any) => { |
| | | for (let i = 0; i < items.length; i++) { |
| | | const item = items[i]; |
| | | if (item.tempId === tempId) { |
| | | items.splice(i, 1); |
| | | return true; |
| | | } |
| | | if (item.children && item.children.length > 0) { |
| | | if (delchildItem(item.children, tempId)) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | }; |
| | | |
| | | dataValue.dataList.forEach(item => { |
| | | if (item.children && item.children.length > 0) { |
| | | delchildItem(item.children, tempId); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const newChildNode = (parentItem: any, nodeType: string = 'rawMaterial') => ({ |
| | | parentId: parentItem.id || "", |
| | | parentTempId: parentItem.tempId || "", |
| | | productName: "", |
| | | productId: "", |
| | | model: undefined, |
| | | productModelId: undefined, |
| | | processId: "", |
| | | processName: "", |
| | | operationId: "", |
| | | operationName: "", |
| | | unitQuantity: 1, |
| | | demandedQuantity: 0, |
| | | unit: "", |
| | | nodeType, |
| | | children: [], |
| | | tempId: new Date().getTime(), |
| | | }); |
| | | |
| | | const addRootItem = () => { |
| | | dataValue.dataList.push(newChildNode({ id: "", tempId: "" })); |
| | | }; |
| | | |
| | | const addChildItem = (parentTempId: string, nodeType: string = 'rawMaterial') => { |
| | | const addToItem = (items: any[]): boolean => { |
| | | for (const item of items) { |
| | | if (item.tempId === parentTempId) { |
| | | if (!item.children) item.children = []; |
| | | item.children.push(newChildNode(item, nodeType)); |
| | | recalculateDemandedQuantities(); |
| | | return true; |
| | | } |
| | | if (item.children?.length > 0) { |
| | | if (addToItem(item.children)) return true; |
| | | } |
| | | } |
| | | return false; |
| | | }; |
| | | addToItem(dataValue.dataList); |
| | | }; |
| | | |
| | | const getPropPath = (row, field) => { |
| | | // 为æ¯ä¸ªrowçæå¯ä¸çè·¯å¾ |
| | | // 使ç¨row.idæç´¢å¼ä½ä¸ºå¯ä¸æ è¯ |
| | | let path = "dataList"; |
| | | |
| | | // ç®åå®ç°ï¼ä½¿ç¨rowçidæä¸ä¸ªå¯ä¸æ è¯ |
| | | const uniqueId = row.id || Math.floor(Math.random() * 10000); |
| | | path += `.${uniqueId}`; |
| | | |
| | | return path + `.${field}`; |
| | | }; |
| | | |
| | | const cancelEdit = () => { |
| | | dataValue.isEdit = false; |
| | | // dataValue.dataList = dataValue.dataList.filter(item => item.id !== undefined); |
| | | fetchData(); |
| | | }; |
| | | |
| | | onMounted(async () => { |
| | | // ä»è·¯ç±åæ°åæ¾æ°æ® |
| | | tableData[0].productName = routeProductName.value as string; |
| | | tableData[0].model = routeProductModelName.value as string; |
| | | tableData[0].bomNo = routeBomNo.value as string; |
| | | |
| | | // 订åæ
åµä¸ç¦ç¨ç¼è¾ |
| | | if (isOrderPage.value) { |
| | | dataValue.isEdit = false; |
| | | } |
| | | |
| | | // å
å 载工åºé项ï¼åå è½½æ°æ®ï¼ç¡®ä¿el-selectè½å¤æ£ç¡®åæ¾ |
| | | await fetchProcessOptions(); |
| | | await fetchData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .tree-container { |
| | | padding: 8px 0; |
| | | } |
| | | .tree-legend { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | padding: 8px 12px; |
| | | background: #f5f7fa; |
| | | border-radius: 6px; |
| | | font-size: 13px; |
| | | color: #606266; |
| | | } |
| | | .empty-hint { |
| | | text-align: center; |
| | | color: #909399; |
| | | padding: 24px 0; |
| | | } |
| | | </style> |
| | |
| | | if (!Number.isFinite(n)) return 0; |
| | | if (n <= 0) return 0; |
| | | if (n >= 100) return 100; |
| | | return Math.round(n); |
| | | return parseFloat(n.toFixed(2)); |
| | | }; |
| | | |
| | | // 30/50/80/100 åæ®µé¢è²ï¼çº¢/æ©/è/绿 |
| | |
| | | if (!Number.isFinite(n)) return 0; |
| | | if (n <= 0) return 0; |
| | | if (n >= 100) return 100; |
| | | return Math.round(n); |
| | | return parseFloat(n.toFixed(2)); |
| | | }; |
| | | const progressColor = percentage => { |
| | | const p = toProgressPercentage(percentage); |
| | |
| | | if (!Number.isFinite(n)) return 0; |
| | | if (n <= 0) return 0; |
| | | if (n >= 100) return 100; |
| | | return Math.round(n); |
| | | return parseFloat(n.toFixed(2)); |
| | | }; |
| | | |
| | | const progressColor = percentage => { |
| | |
| | | if (!Number.isFinite(n)) return 0; |
| | | if (n <= 0) return 0; |
| | | if (n >= 100) return 100; |
| | | return Math.round(n); |
| | | return parseFloat(n.toFixed(2)); |
| | | }; |
| | | const progressColor = percentage => { |
| | | const p = toProgressPercentage(percentage); |
| | |
| | | <el-tree-select |
| | | v-model="selectedProductIds" |
| | | multiple |
| | | filterable |
| | | collapse-tags |
| | | collapse-tags-tooltip |
| | | placeholder="è¯·éæ©äº§åï¼å¯å¤éï¼" |
| | |
| | | : env.VITE_BASE_API; |
| | | const javaUrl = |
| | | env.VITE_APP_ENV === "development" |
| | | ? "http://1.15.17.182:9048" |
| | | ? "http://1.15.17.182:9049" |
| | | : env.VITE_JAVA_API; |
| | | return { |
| | | define:{ |