From ac565df702d10c6cb5caf5cdec131c07b3e9d7f7 Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期五, 12 六月 2026 10:46:52 +0800
Subject: [PATCH] Merge branch 'dev_NEW_pro' into dev_宁夏_万通新型
---
src/views/collaborativeApproval/notificationManagement/summary/index.vue | 8
src/views/financialManagement/receivable/salesOut.vue | 24
.gitignore | 2
doc/知识库模块前端实现文档.md | 1212 ++++++++++
src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue | 236 ++
src/views/financialManagement/payable/payment.vue | 22
src/views/productionManagement/workOrder/index.vue | 2
multiple/assets/favicon/KHYYfavicon.ico | 0
src/api/collaborativeApproval/knowledgeBase.js | 161 +
doc/知识库模块传参方式和参数命名规范文档.md | 941 ++++++++
src/views/financialManagement/voucher/index.vue | 13
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue | 741 +++---
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue | 6
src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue | 2
src/views/financialManagement/payable/input-invoice.vue | 22
src/views/productionManagement/workOrderManagement/index.vue | 2
src/views/qualityManagement/metricBinding/index.vue | 1
src/api/procurementManagement/paymentLedger.js | 9
src/views/financialManagement/assets/intangibleAssets.vue | 5
src/views/financialManagement/receivable/reconciliation.vue | 25
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue | 4
vite.config.js | 2
src/views/productionManagement/productStructure/DetailNew/index.vue | 626 +++++
src/views/collaborativeApproval/knowledgeBase/index.vue | 741 ++++++
src/views/inventoryManagement/stockManagement/New.vue | 8
src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue | 6
src/views/personnelManagement/socialSecuritySet/components/formDia.vue | 784 +++---
multiple/assets/favicon/NYfavicon.ico | 0
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue | 4
src/components/PIMTable/PIMTable.vue | 2
multiple/assets/logo/KHYYLogo.png | 0
multiple/assets/logo/NYLogo.png | 0
src/views/financialManagement/receivable/invoiceApply.vue | 23
src/views/productionManagement/workOrderEdit/index.vue | 2
src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue | 9
src/views/financialManagement/receivable/salesReturn.vue | 24
src/views/financialManagement/payable/paymentApply.vue | 20
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js | 4
src/views/financialManagement/payable/reconciliation.vue | 22
src/views/financialManagement/payable/purchaseIn.vue | 22
doc/知识库RAG功能实现文档.md | 1034 +++++++++
src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue | 6
src/views/productionManagement/productionOrder/index.vue | 2
src/views/financialManagement/receivable/outputInvoice.vue | 23
src/views/financialManagement/payable/purchaseReturn.vue | 20
45 files changed, 5,960 insertions(+), 862 deletions(-)
diff --git a/.gitignore b/.gitignore
index 78a752d..f76fc8f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@
yarn-error.log*
**/*.log
+.claude/
+
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
diff --git "a/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md" "b/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..4a94cc5
--- /dev/null
+++ "b/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md"
@@ -0,0 +1,1034 @@
+# 鐭ヨ瘑搴揜AG鍚戦噺妫�绱㈠姛鑳藉疄鐜版枃妗�
+
+## 涓�銆佸姛鑳芥杩�
+
+鍩轰簬 RAG锛圧etrieval-Augmented Generation锛夋妧鏈疄鐜扮煡璇嗗簱闂瓟鍔熻兘锛屾敮鎸侊細
+- 鐭ヨ瘑搴撶鐞嗭紙CRUD锛�
+- 鏂囦欢涓婁紶涓庡悜閲忓寲澶勭悊
+- 鍩轰簬鍚戦噺妫�绱㈢殑鏅鸿兘闂瓟
+- 澶氱鏂囦欢鏍煎紡鏀寔锛坱xt銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df锛�
+
+## 浜屻�佹妧鏈灦鏋�
+
+### 2.1 鎶�鏈爤
+| 缁勪欢 | 鎶�鏈� |
+|------|------|
+| 鍚戦噺鏁版嵁搴� | Pinecone |
+| Embedding妯″瀷 | 闃块噷浜� DashScope text-embedding-v3 |
+| LLM | 闃块噷浜戦�氫箟鍗冮棶 qwen-max |
+| 妗嗘灦 | langchain4j |
+| ORM | MyBatis-Plus |
+
+### 2.2 鏋舵瀯鍥�
+
+```
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鍓嶇搴旂敤 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� Controller Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹� KnowledgeBaseCtrl 鈹� 鈹� KnowledgeChatController 鈹� 鈹�
+鈹� 鈹� (鐭ヨ瘑搴撶鐞�) 鈹� 鈹� (鐭ヨ瘑搴撻棶绛�) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� Service Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹侹nowledgeBaseService 鈹� 鈹� KnowledgeRagService 鈹� 鈹�
+鈹� 鈹� (鐭ヨ瘑搴揅RUD) 鈹� 鈹� (鍚戦噺鍖�/妫�绱�) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� AI Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹� KnowledgeChatAgent 鈹� 鈹� EmbeddingStore (Pinecone) 鈹� 鈹�
+鈹� 鈹� (闂瓟Agent) 鈹� 鈹� (鍚戦噺瀛樺偍) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+```
+
+---
+
+## 涓夈�佸悗绔疄鐜�
+
+### 3.1 鏁版嵁搴撹璁�
+
+#### 3.1.1 鐭ヨ瘑搴撹〃锛坘nowledge_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 '瑙e喅鏁堢巼',
+ problem TEXT COMMENT '闂鎻忚堪',
+ solution TEXT COMMENT '瑙e喅鏂规',
+ 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 鐭ヨ瘑搴撳悜閲忚褰曡〃锛坘nowledge_base_vector锛�
+
+```sql
+CREATE TABLE knowledge_base_vector (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '涓婚敭ID',
+ knowledge_base_id BIGINT NOT NULL COMMENT '鍏宠仈鐭ヨ瘑搴揑D',
+ 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 閰嶇疆鏂囦欢锛坅pplication.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 鏍稿績浠g爜瀹炵幇
+
+#### 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 鐭ヨ瘑搴撻棶绛擜gent
+
+**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());
+ }
+}
+```
+
+---
+
+## 鍥涖�丄PI鎺ュ彛鏂囨。
+
+### 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": "濡備綍澶勭悊搴撳瓨鐩樼偣宸紓锛�"
+}
+```
+
+**鍝嶅簲**锛圫SE娴佸紡锛�
+```
+鏍规嵁鐭ヨ瘑搴撳唴瀹癸紝搴撳瓨鐩樼偣宸紓鐨勫鐞嗘祦绋嬪涓嬶細
+
+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锛堟祦寮忔帴鍙o級
+3. 鍚庣澶勭悊锛�
+ 鈹溾攢鈹� 鏋勫缓鍛藉悕绌洪棿锛歬b-{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灏佽
+```
\ No newline at end of file
diff --git "a/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\344\274\240\345\217\202\346\226\271\345\274\217\345\222\214\345\217\202\346\225\260\345\221\275\345\220\215\350\247\204\350\214\203\346\226\207\346\241\243.md" "b/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\344\274\240\345\217\202\346\226\271\345\274\217\345\222\214\345\217\202\346\225\260\345\221\275\345\220\215\350\247\204\350\214\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..e962ac2
--- /dev/null
+++ "b/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\344\274\240\345\217\202\346\226\271\345\274\217\345\222\214\345\217\202\346\225\260\345\221\275\345\220\215\350\247\204\350\214\203\346\226\207\346\241\243.md"
@@ -0,0 +1,941 @@
+# 鐭ヨ瘑搴撴ā鍧椾紶鍙傛柟寮忓拰鍙傛暟鍛藉悕瑙勮寖鏂囨。
+
+## 涓�銆佹杩�
+
+鏈枃妗h缁嗚鏄庣煡璇嗗簱妯″潡涓墍鏈夋帴鍙c�佺粍浠躲�佹柟娉曠殑浼犲弬鏂瑰紡鍜屽弬鏁板懡鍚嶈鑼�,鏃ㄥ湪:
+- 缁熶竴鍓嶅悗绔弬鏁板懡鍚嶈鑼�
+- 鏄庣‘鍙傛暟绫诲瀷鍜屽繀濉��
+- 瑙勮寖浼犲弬鏂瑰紡(GET params銆丳OST body銆丏ELETE data)
+- 鎻愪緵娓呮櫚鐨勫弬鏁版槧灏勫叧绯�
+
+---
+
+## 浜屻�佸弬鏁板懡鍚嶈鑼�
+
+### 2.1 鍩烘湰瑙勮寖
+
+#### 鍛藉悕椋庢牸
+- **鍚庣鍙傛暟**: 閬靛惊 Java 椹煎嘲鍛藉悕娉� (camelCase)
+- **鍓嶇鍙傛暟**: 閬靛惊 JavaScript 椹煎嘲鍛藉悕娉� (camelCase)
+- **鏁版嵁搴撳瓧娈�**: 閬靛惊 MySQL 涓嬪垝绾垮懡鍚嶆硶 (snake_case)
+- **鎺ュ彛URL**: 閬靛惊 RESTful 椋庢牸,浣跨敤灏忓啓鍜岃繛瀛楃
+
+#### 鍛藉悕绾﹀畾
+1. **ID鐩稿叧**: 缁熶竴浣跨敤 `id`銆乣Id` 鍚庣紑
+ - `knowledgeBaseId` - 鐭ヨ瘑搴揑D
+ - `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` - 鏇存柊鏃堕棿
+
+---
+
+## 涓夈�佹帴鍙d紶鍙傛柟寮忚鑼�
+
+### 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` 涓�
+- 鉁� 鍙傛暟鍚嶄笌鍚庣瀹炰綋瀛楁淇濇寔涓�鑷�
+- 鉁� 蹇呭~鍙傛暟闇�瑕佸湪琛ㄥ崟楠岃瘉瑙勫垯涓0鏄�
+- 鉁� 鏁板�肩被鍨嬪弬鏁伴渶鎸囧畾榛樿鍊�
+
+---
+
+### 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]); // 鉁� 鐩存帴浼犻�扞D鏁扮粍
+```
+
+**璇锋眰浣撴牸寮�**: JSON 鏁扮粍 `[1, 2, 3]`
+
+**瑙勮寖瑕佺偣**:
+- 鉁� DELETE璇锋眰鐨勫弬鏁版斁鍦� `data` 涓�
+- 鉁� 鎵归噺鍒犻櫎浼犻�扞D鏁扮粍
+- 鉁� 鍗曚釜鍒犻櫎涔熷彲浠ヤ紶閫掓暟缁� `[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 鐭ヨ瘑搴撶鐞嗘帴鍙e弬鏁�
+
+#### 鏌ヨ鍒楄〃 (`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 | 瑙e喅鏁堢巼 | "high" |
+| problem | String | 鏄� | data | 闂鎻忚堪 | "鐢ㄦ埛涓嶄細鎿嶄綔" |
+| solution | String | 鏄� | data | 瑙e喅鏂规 | "鍙傝�冩墜鍐�" |
+| 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 | 鐭ヨ瘑搴揑D | 10 |
+| *(鍏朵粬鍙傛暟鍚屾柊澧�)* | - | - | data | - | - |
+
+**鍓嶇璋冪敤**:
+```javascript
+updateKnowledgeBase({
+ id: 10,
+ title: "鎿嶄綔鎵嬪唽(鏇存柊)",
+ type: "guide",
+ // ...鍏朵粬鍙傛暟
+});
+```
+
+---
+
+#### 鍒犻櫎鐭ヨ瘑搴� (`DELETE /knowledgeBase/delete`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| ids | Long[] | 鏄� | data | 鐭ヨ瘑搴揑D鏁扮粍 | [1, 2, 3] |
+
+**鍓嶇璋冪敤**:
+```javascript
+delKnowledgeBase([1, 2, 3]);
+```
+
+---
+
+### 4.2 鏂囦欢绠$悊鎺ュ彛鍙傛暟
+
+#### 鏌ヨ鍚戦噺鍖栫姸鎬� (`GET /knowledgeBase/vector/status/{knowledgeBaseId}`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| knowledgeBaseId | Long | 鏄� | URL璺緞 | 鐭ヨ瘑搴揑D | 10 |
+
+**鍓嶇璋冪敤**:
+```javascript
+getVectorStatus(10);
+```
+
+**URL**: `/knowledgeBase/vector/status/10`
+
+---
+
+#### 淇濆瓨鏂囦欢鍏宠仈 (`POST /knowledgeBase/file/save`)
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 浼犲弬浣嶇疆 | 璇存槑 | 绀轰緥鍊� |
+|--------|------|------|----------|------|--------|
+| knowledgeBaseId | Long | 鏄� | data | 鐭ヨ瘑搴揑D | 10 |
+| storageBlobIds | Long[] | 鏄� | data | 鏂囦欢blob ID鏁扮粍 | [123, 124] |
+
+**鍓嶇璋冪敤**:
+```javascript
+saveKnowledgeBaseFiles({
+ knowledgeBaseId: 10,
+ storageBlobIds: [123, 124]
+});
+```
+
+**閲嶈璇存槑**:
+- 鈿狅笍 **蹇呴』鍏堣皟鐢� `/common/upload` 涓婁紶鏂囦欢**
+- 鈿狅笍 **鑾峰彇杩斿洖鐨� `data.id` 浣滀负 `storageBlobId`**
+- 鈿狅笍 **姝ゆ帴鍙hЕ鍙戝紓姝ュ悜閲忓寲澶勭悊**
+
+---
+
+#### 鍒犻櫎鐭ヨ瘑搴撴枃浠� (`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 | 鐭ヨ瘑搴揑D | 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, // 鐭ヨ瘑搴揑D
+ title: "鎿嶄綔鎵嬪唽", // 鐭ヨ瘑鏍囬
+ type: "guide", // 鐭ヨ瘑绫诲瀷
+ scenario: "绯荤粺鎿嶄綔", // 閫傜敤鍦烘櫙
+ efficiency: "high", // 瑙e喅鏁堢巼
+ problem: "...", // 闂鎻忚堪
+ solution: "...", // 瑙e喅鏂规
+ 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銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df 鏍煎紡鐨勬枃浠�');
+ 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; // 闃叉閲嶅鍙戦��
+ }
+
+ // ...鍙戦�佽姹�
+};
+```
+
+---
+
+## 涔濄�佸父瑙侀敊璇拰瑙e喅鏂规
+
+### 9.1 鍙傛暟鍚嶄笉鍖归厤
+
+**閿欒绀轰緥**:
+```javascript
+// 鉂� 閿欒: 浣跨敤浜嗕笅鍒掔嚎鍛藉悕
+saveKnowledgeBaseFiles({
+ knowledge_base_id: 10,
+ storage_blob_ids: [123]
+});
+
+// 鉁� 姝g‘: 浣跨敤椹煎嘲鍛藉悕
+saveKnowledgeBaseFiles({
+ knowledgeBaseId: 10,
+ storageBlobIds: [123]
+});
+```
+
+---
+
+### 9.2 浼犲弬浣嶇疆閿欒
+
+**閿欒绀轰緥**:
+```javascript
+// 鉂� 閿欒: POST璇锋眰浣跨敤 params
+export function addKnowledgeBase(data) {
+ return request({
+ url: "/knowledgeBase/add",
+ method: "post",
+ params: data // 鉂� 閿欒
+ });
+}
+
+// 鉁� 姝g‘: POST璇锋眰浣跨敤 data
+export function addKnowledgeBase(data) {
+ return request({
+ url: "/knowledgeBase/add",
+ method: "post",
+ data: data // 鉁� 姝g‘
+ });
+}
+```
+
+---
+
+### 9.3 DELETE璇锋眰鍙傛暟閿欒
+
+**閿欒绀轰緥**:
+```javascript
+// 鉂� 閿欒: DELETE浣跨敤 params 浼犻�掓暟缁�
+export function delKnowledgeBase(ids) {
+ return request({
+ url: "/knowledgeBase/delete",
+ method: "delete",
+ params: ids // 鉂� 閿欒
+ });
+}
+
+// 鉁� 姝g‘: DELETE浣跨敤 data 浼犻�掓暟缁�
+export function delKnowledgeBase(ids) {
+ return request({
+ url: "/knowledgeBase/delete",
+ method: "delete",
+ data: ids // 鉁� 姝g‘
+ });
+}
+```
+
+---
+
+### 9.4 娴佸紡鎺ュ彛閿欒
+
+**閿欒绀轰緥**:
+```javascript
+// 鉂� 閿欒: 娴佸紡鎺ュ彛浣跨敤 axios
+export function knowledgeChat(data) {
+ return request({
+ url: "/ai/knowledge/chat",
+ method: "post",
+ data: data // 鉂� axios涓嶆敮鎸佹祦寮�
+ });
+}
+
+// 鉁� 姝g‘: 娴佸紡鎺ュ彛浣跨敤 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) // 鉁� 姝g‘
+ });
+}
+```
+
+---
+
+### 9.5 鏂囦欢涓婁紶ID鑾峰彇閿欒
+
+**閿欒绀轰緥**:
+```javascript
+// 鉂� 閿欒: 涓婁紶鎴愬姛鍚庢病鏈変繚瀛� storageBlobId
+const handleUploadSuccess = (response) => {
+ if (response.code === 200) {
+ ElMessage.success("涓婁紶鎴愬姛");
+ // 鉂� 娌℃湁淇濆瓨 response.data.id
+ }
+};
+
+// 鉁� 姝g‘: 淇濆瓨 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 蹇呭~鍙傛暟鏍¢獙
+
+鉁� **鍦ㄨ皟鐢ㄦ帴鍙e墠鏍¢獙蹇呭~鍙傛暟**
+```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}`;
+```
+
+---
+
+## 鍗佷竴銆佸弬鏁版槧灏勫叧绯绘�荤粨
+
+### 鐭ヨ瘑搴揑D鐩稿叧
+| 鍦烘櫙 | 鍙傛暟鍚� | 绫诲瀷 | 鏉ユ簮 |
+|------|--------|------|------|
+| 鐭ヨ瘑搴撳垪琛ㄦ煡璇� | - | - | 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 (绾㈣壊) |
+
+---
+
+## 鍗佷笁銆佹�荤粨
+
+鏈枃妗h缁嗚鑼冧簡鐭ヨ瘑搴撴ā鍧楃殑鍙傛暟鍛藉悕鍜屼紶鍙傛柟寮�,閬靛惊浠ヤ笅鍘熷垯:
+
+1. 鉁� **鍛藉悕涓�鑷存��**: 鍓嶅悗绔弬鏁板悕淇濇寔涓�鑷�(椹煎嘲鍛藉悕)
+2. 鉁� **浼犲弬瑙勮寖鎬�**: GET鐢╬arams銆丳OST鐢╠ata銆丏ELETE鐢╠ata
+3. 鉁� **绫诲瀷鏄庣‘鎬�**: 鏄庣‘鍙傛暟绫诲瀷,鍚堢悊璁剧疆榛樿鍊�
+4. 鉁� **鏍¢獙瀹屾暣鎬�**: 蹇呭~鍙傛暟闇�鏍¢獙,鍙�夊弬鏁版湁榛樿鍊�
+5. 鉁� **閿欒閬垮厤**: 閬靛惊瑙勮寖閬垮厤甯歌閿欒
+
+寤鸿鍥㈤槦鎴愬憳涓ユ牸閬靛惊鏈鑼�,纭繚鍓嶅悗绔弬鏁颁紶閫掔殑涓�鑷存�у拰鍙潬鎬с��
\ No newline at end of file
diff --git "a/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\345\211\215\347\253\257\345\256\236\347\216\260\346\226\207\346\241\243.md" "b/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\345\211\215\347\253\257\345\256\236\347\216\260\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..c2ea6e2
--- /dev/null
+++ "b/doc/\347\237\245\350\257\206\345\272\223\346\250\241\345\235\227\345\211\215\347\253\257\345\256\236\347\216\260\346\226\207\346\241\243.md"
@@ -0,0 +1,1212 @@
+# 鐭ヨ瘑搴撴ā鍧楀畬鏁村墠绔疄鐜版枃妗�
+
+## 涓�銆佹ā鍧楁杩�
+
+鐭ヨ瘑搴撴ā鍧楁槸涓�涓泦鎴愪簡RAG(妫�绱㈠寮虹敓鎴�)鎶�鏈殑鏅鸿兘鐭ヨ瘑绠$悊绯荤粺,鏀寔:
+- **鐭ヨ瘑搴揅RUD绠$悊** - 鍒涘缓銆佺紪杈戙�佸垹闄ゃ�佹煡璇㈢煡璇嗗簱
+- **鏂囦欢涓婁紶涓庡悜閲忓寲** - 鏀寔澶氱鏂囦欢鏍煎紡,鑷姩杩涜鍚戦噺鍒囩墖澶勭悊
+- **鏅鸿兘闂瓟** - 鍩轰簬涓婁紶鏂囦欢鍐呭杩涜AI闂瓟
+- **鏂囦欢绠$悊** - 鏌ョ湅鏂囦欢鍚戦噺鍖栫姸鎬�,鏀寔閲嶆柊澶勭悊鍜屽垹闄�
+
+### 鎶�鏈灦鏋�
+- **鍓嶇妗嗘灦**: Vue 3 + Composition API
+- **UI缁勪欢搴�**: Element Plus
+- **鍚戦噺鏁版嵁搴�**: Pinecone
+- **AI妯″瀷**: 闃块噷浜戦�氫箟鍗冮棶
+- **鏂囦欢澶勭悊**: 鏀寔docx銆亁lsx銆乸df銆乼xt銆乵d绛夋牸寮�
+
+---
+
+## 浜屻�佹枃浠剁粨鏋�
+
+```
+src/
+鈹溾攢鈹� api/
+鈹� 鈹斺攢鈹� collaborativeApproval/
+鈹� 鈹斺攢鈹� knowledgeBase.js # API鎺ュ彛灏佽
+鈹溾攢鈹� views/
+鈹� 鈹斺攢鈹� collaborativeApproval/
+鈹� 鈹斺攢鈹� knowledgeBase/
+鈹� 鈹斺攢鈹� index.vue # 涓婚〉闈㈢粍浠�
+鈹斺攢鈹� components/
+ 鈹溾攢鈹� PIMTable/
+ 鈹� 鈹斺攢鈹� PIMTable.vue # 琛ㄦ牸缁勪欢
+ 鈹斺攢鈹� Dialog/
+ 鈹斺攢鈹� FormDialog.vue # 寮圭獥缁勪欢
+```
+
+---
+
+## 涓夈�丄PI鎺ュ彛瀹氫箟
+
+### 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, // 鐭ヨ瘑搴揑D
+ storageBlobIds: [123, 124] // 涓婁紶鏂囦欢杩斿洖鐨刡lob 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椤电甯冨眬,鍖呭惈涓や釜涓昏鍔熻兘妯″潡:
+- **鐭ヨ瘑搴撶鐞�** - 鐭ヨ瘑搴揅RUD鎿嶄綔
+- **鐭ヨ瘑搴撻棶绛�** - 鍩轰簬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: "瑙e喅鏁堢巼",
+ 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绉掑埛鏂版枃浠跺垪琛�
+ 鈫�
+鏄剧ず鍚戦噺鍖栫姸鎬�(寰呭鐞嗏啋澶勭悊涓啋宸插畬鎴�)
+```
+
+#### 浠g爜瀹炵幇
+
+```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銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df 鏍煎紡鐨勬枃浠�');
+ 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("鏂囦欢鍏宠仈淇濆瓨鎴愬姛,姝e湪鍚庡彴澶勭悊鍚戦噺鍖�");
+ 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 (娴佸紡鎺ュ彛)
+ 鈫�
+鍚庣澶勭悊:
+ - 瀵归棶棰樿繘琛屽悜閲忓寲
+ - 鍦≒inecone涓绱㈢浉鍏冲垏鐗�
+ - 鏋勫缓涓婁笅鏂嘝rompt
+ - 璋冪敤LLM鐢熸垚鍥炵瓟
+ 鈫�
+娴佸紡杩斿洖AI鍥炵瓟
+ 鈫�
+鍓嶇瀹炴椂鏄剧ず鍥炵瓟鍐呭
+ 鈫�
+鑷姩婊氬姩鍒板簳閮�
+```
+
+#### 浠g爜瀹炵幇
+
+```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">姝e湪鎬濊�冧腑...</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 | 澶勭悊涓� | 姝e湪杩涜鍚戦噺鍒囩墖澶勭悊 | 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: '瑙e喅鏂规', elTagType: 'primary' },
+ { value: 'experience', label: '缁忛獙鍒嗕韩', elTagType: 'info' },
+ { value: 'guide', label: '鎿嶄綔鎸囧崡', elTagType: 'danger' }
+];
+```
+
+### 7.3 瑙e喅鏁堢巼鏄犲皠
+
+```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銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df 鏍煎紡
+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 鐭ヨ瘑搴撶鐞�
+
+- [ ] 鏂板鐭ヨ瘑搴撴垚鍔�
+- [ ] 缂栬緫鐭ヨ瘑搴撴垚鍔�
+- [ ] 鍒犻櫎鐭ヨ瘑搴撴垚鍔�(鍗曚釜/鎵归噺)
+- [ ] 鎼滅储鍔熻兘姝e父(鎸夋爣棰樸�佺被鍨�)
+- [ ] 鍒嗛〉鍔熻兘姝e父
+- [ ] 瀵煎嚭鍔熻兘姝e父
+
+### 10.2 鏂囦欢涓婁紶
+
+- [ ] 鍗曟枃浠朵笂浼犳垚鍔�
+- [ ] 澶氭枃浠朵笂浼犳垚鍔�
+- [ ] 鏂囦欢绫诲瀷鏍¢獙姝e父
+- [ ] 鏂囦欢澶у皬鏍¢獙姝e父
+- [ ] 淇濆瓨鏂囦欢鍏宠仈鎴愬姛
+- [ ] 鍚戦噺鍖栫姸鎬佹纭樉绀�
+
+### 10.3 鏂囦欢绠$悊
+
+- [ ] 鏌ョ湅鏂囦欢鍒楄〃姝e父
+- [ ] 鍚戦噺鍖栫姸鎬佽疆璇㈡甯�
+- [ ] 閲嶆柊澶勭悊澶辫触鏂囦欢鎴愬姛
+- [ ] 鍒犻櫎鏂囦欢鎴愬姛
+- [ ] 鏂囦欢棰勮/涓嬭浇姝e父
+
+### 10.4 鐭ヨ瘑搴撻棶绛�
+
+- [ ] 闂瓟寮圭獥鎵撳紑姝e父
+- [ ] 鍙戦�侀棶棰樻垚鍔�
+- [ ] 娴佸紡鍝嶅簲鏄剧ず姝e父
+- [ ] 澶氳疆瀵硅瘽姝e父
+- [ ] 鑷姩婊氬姩鍒板簳閮�
+- [ ] 閿欒澶勭悊姝e父
+
+---
+
+## 鍗佷竴銆佸父瑙侀棶棰�
+
+### Q1: 鏂囦欢涓婁紶鍚庢病鏈夎Е鍙戝悜閲忓寲?
+
+**A**: 妫�鏌ユ槸鍚﹁皟鐢ㄤ簡 `/knowledgeBase/file/save` 鎺ュ彛銆備笂浼犳垚鍔熷悗蹇呴』璋冪敤姝ゆ帴鍙f墠鑳借Е鍙戝悜閲忓寲銆�
+
+### 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)
+- 鉁� 瀹屾垚鐭ヨ瘑搴揅RUD鍔熻兘
+- 鉁� 瀹屾垚鏂囦欢涓婁紶涓庡悜閲忓寲鍔熻兘
+- 鉁� 瀹屾垚鐭ヨ瘑搴撻棶绛斿姛鑳�
+- 鉁� 瀹屾垚鏂囦欢绠$悊鍔熻兘
+- 鉁� 瀹屾垚鍚戦噺鍖栫姸鎬佹樉绀�
+
+### v1.1.0 (璁″垝涓�)
+- 馃敳 娣诲姞鍘嗗彶璁板綍鏌ヨ
+- 馃敳 娣诲姞鎵归噺鎿嶄綔鍔熻兘
+- 馃敳 娣诲姞鏂囦欢棰勮鍔熻兘
+- 馃敳 浼樺寲鍚戦噺鍖栬繘搴︽樉绀�
\ No newline at end of file
diff --git a/multiple/assets/favicon/KHYYfavicon.ico b/multiple/assets/favicon/KHYYfavicon.ico
new file mode 100644
index 0000000..9982880
--- /dev/null
+++ b/multiple/assets/favicon/KHYYfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/NYfavicon.ico b/multiple/assets/favicon/NYfavicon.ico
new file mode 100644
index 0000000..a0c9f8b
--- /dev/null
+++ b/multiple/assets/favicon/NYfavicon.ico
Binary files differ
diff --git a/multiple/assets/logo/KHYYLogo.png b/multiple/assets/logo/KHYYLogo.png
new file mode 100644
index 0000000..53b479d
--- /dev/null
+++ b/multiple/assets/logo/KHYYLogo.png
Binary files differ
diff --git a/multiple/assets/logo/NYLogo.png b/multiple/assets/logo/NYLogo.png
new file mode 100644
index 0000000..d6eea98
--- /dev/null
+++ b/multiple/assets/logo/NYLogo.png
Binary files differ
diff --git a/src/api/collaborativeApproval/knowledgeBase.js b/src/api/collaborativeApproval/knowledgeBase.js
index b195525..eb44ac2 100644
--- a/src/api/collaborativeApproval/knowledgeBase.js
+++ b/src/api/collaborativeApproval/knowledgeBase.js
@@ -1,55 +1,172 @@
import request from "@/utils/request";
+import { getToken } from '@/utils/auth';
-// 鏌ヨ鐭ヨ瘑搴撳垪琛�
+/**
+ * 鐭ヨ瘑搴撶鐞嗘帴鍙�
+ * 浼犲弬瑙勮寖:
+ * - GET璇锋眰: 浣跨敤 params
+ * - POST璇锋眰: 浣跨敤 data
+ * - DELETE璇锋眰: 浣跨敤 data
+ * - 娴佸紡璇锋眰: 浣跨敤 Fetch API
+ */
+
+// ==================== 鐭ヨ瘑搴揅RUD鎺ュ彛 ====================
+
+/**
+ * 鏌ヨ鐭ヨ瘑搴撳垪琛�
+ * @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] - 瑙e喅鏁堢巼(鍙��)
+ * @param {string} data.problem - 闂鎻忚堪(蹇呭~)
+ * @param {string} data.solution - 瑙e喅鏂规(蹇呭~)
+ * @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 - 鐭ヨ瘑搴揑D(蹇呭~)
+ * @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 - 鐭ヨ瘑搴揑D鏁扮粍
+ * @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 - 鐭ヨ瘑搴揑D
+ * @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 - 鐭ヨ瘑搴揑D(蹇呭~)
+ * @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 - 鐭ヨ瘑搴揑D(蹇呭~)
+ * @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",
+ });
+}
diff --git a/src/api/procurementManagement/paymentLedger.js b/src/api/procurementManagement/paymentLedger.js
index 6c5d9de..537ba9f 100644
--- a/src/api/procurementManagement/paymentLedger.js
+++ b/src/api/procurementManagement/paymentLedger.js
@@ -18,3 +18,12 @@
params,
});
}
+
+/** 浠樻鍙拌处 - 浠樻鐧昏鍒楄〃 */
+export function registrationList(params) {
+ return request({
+ url: "/purchase/report/registration",
+ method: "get",
+ params,
+ });
+}
diff --git a/src/components/PIMTable/PIMTable.vue b/src/components/PIMTable/PIMTable.vue
index d63c197..74615af 100644
--- a/src/components/PIMTable/PIMTable.vue
+++ b/src/components/PIMTable/PIMTable.vue
@@ -11,7 +11,7 @@
: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"
diff --git a/src/views/collaborativeApproval/knowledgeBase/index.vue b/src/views/collaborativeApproval/knowledgeBase/index.vue
index 43fee33..f50d06b 100644
--- a/src/views/collaborativeApproval/knowledgeBase/index.vue
+++ b/src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -225,18 +225,167 @@
</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">姝e湪鎬濊�冧腑...</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 = {
@@ -283,7 +432,18 @@
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 {
@@ -297,7 +457,18 @@
dialogTitle,
dialogType,
viewDialogVisible,
- currentKnowledge
+ currentKnowledge,
+ filesDialogVisible,
+ currentKnowledgeBase,
+ fileList,
+ uploadedBlobIds,
+ savingFiles,
+ vectorStatusTimer,
+ chatDialogVisible,
+ messages,
+ inputQuestion,
+ chatLoading,
+ memoryId
} = toRefs(data);
// 琛ㄥ崟寮曠敤
@@ -305,6 +476,12 @@
// 鐢ㄦ埛鐩稿叧
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([
@@ -352,6 +529,18 @@
}
},
{
+ label: "鏂囦欢鏁伴噺",
+ prop: "fileCount",
+ width: 100,
+ align: "center"
+ },
+ {
+ label: "鍒囩墖鏁伴噺",
+ prop: "totalChunkCount",
+ width: 100,
+ align: "center"
+ },
+ {
label: "浣跨敤娆℃暟",
prop: "usageCount",
width: 100,
@@ -379,6 +568,20 @@
type: "text",
clickFun: (row) => {
openForm("edit", row);
+ }
+ },
+ {
+ name: "鏂囦欢",
+ type: "text",
+ clickFun: (row) => {
+ openFilesDialog(row);
+ }
+ },
+ {
+ name: "闂瓟",
+ type: "text",
+ clickFun: (row) => {
+ openChatDialog(row);
}
},
{
@@ -422,21 +625,31 @@
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);
+ });
};
// 鍒嗛〉澶勭悊
@@ -611,27 +824,47 @@
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);
@@ -645,19 +878,22 @@
return;
}
- ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄�,鏄惁纭鍒犻櫎?", "鍒犻櫎", {
confirmButtonText: "纭",
cancelButtonText: "鍙栨秷",
type: "warning",
}).then(() => {
- // console.log(selectedIds.value);
+ // 鉁� DELETE璇锋眰浣跨敤data浼犻�扞D鏁扮粍
delKnowledgeBase(selectedIds.value).then(res => {
if(res.code == 200){
ElMessage.success("鍒犻櫎鎴愬姛");
selectedIds.value = [];
getList();
}
- })
+ }).catch(err => {
+ console.error("鍒犻櫎鐭ヨ瘑搴撳け璐�:", err);
+ ElMessage.error(err.msg || "鍒犻櫎澶辫触");
+ });
}).catch(() => {
// 鐢ㄦ埛鍙栨秷
});
@@ -680,6 +916,364 @@
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銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df 鏍煎紡鐨勬枃浠�');
+ 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>,鎵�浠ata鏄暟缁�
+ 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, // 鐭ヨ瘑搴揑D
+ storageBlobIds: uploadedBlobIds.value // 鏂囦欢blob ID鏁扮粍
+ });
+
+ ElMessage.success("鏂囦欢鍏宠仈淇濆瓨鎴愬姛,姝e湪鍚庡彴澶勭悊鍚戦噺鍖�");
+ 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浼犻�扞D鏁扮粍
+ await deleteKnowledgeBaseFile([row.id]); // 娉ㄦ剰: row.id鏄悜閲忚褰旾D,涓嶆槸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鐨刦allback鏂规
+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, // 鐭ヨ瘑搴揑D
+ 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>
@@ -755,4 +1349,113 @@
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>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue b/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
index b2fb88d..ac7a123 100644
--- a/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
@@ -1,154 +1,148 @@
<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>
@@ -163,37 +157,37 @@
// 鐢宠绫诲瀷閫夐」
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: ''
})
// 琛ㄥ崟寮曠敤
@@ -205,307 +199,270 @@
// 鍛樺伐鍒楄〃
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>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue b/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
index 26e2c24..dea9495 100644
--- a/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
@@ -36,7 +36,7 @@
<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">
@@ -233,9 +233,9 @@
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 {
diff --git a/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue b/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
index 9b5325f..235faa3 100644
--- a/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
@@ -145,10 +145,11 @@
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)
}
@@ -156,7 +157,7 @@
cells.push({
type: 'meeting',
meeting: meeting,
- span: endIdx - startIdx,
+ span: displayEndIdx - startIdx,
startTime: meeting.startTime,
endTime: meeting.endTime
})
diff --git a/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue b/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
index dace531..a3ae223 100644
--- a/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
@@ -34,7 +34,7 @@
<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">
@@ -231,9 +231,9 @@
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 => {
diff --git a/src/views/collaborativeApproval/notificationManagement/summary/index.vue b/src/views/collaborativeApproval/notificationManagement/summary/index.vue
index bf6e230..9f3fc7f 100644
--- a/src/views/collaborativeApproval/notificationManagement/summary/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/summary/index.vue
@@ -28,7 +28,7 @@
<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">
@@ -207,9 +207,9 @@
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 {
@@ -260,7 +260,7 @@
<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>
diff --git a/src/views/financialManagement/assets/intangibleAssets.vue b/src/views/financialManagement/assets/intangibleAssets.vue
index 9aef2bf..b2c13b1 100644
--- a/src/views/financialManagement/assets/intangibleAssets.vue
+++ b/src/views/financialManagement/assets/intangibleAssets.vue
@@ -72,7 +72,7 @@
</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>
@@ -160,7 +160,7 @@
<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>
@@ -244,6 +244,7 @@
);
const createDefaultForm = () => ({
+ id: null,
assetCode: "",
assetName: "",
category: "",
diff --git a/src/views/financialManagement/payable/input-invoice.vue b/src/views/financialManagement/payable/input-invoice.vue
index 86ebd09..ae40709 100644
--- a/src/views/financialManagement/payable/input-invoice.vue
+++ b/src/views/financialManagement/payable/input-invoice.vue
@@ -51,6 +51,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -375,6 +377,26 @@
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, ",");
diff --git a/src/views/financialManagement/payable/payment.vue b/src/views/financialManagement/payable/payment.vue
index 18e7941..dd386d8 100644
--- a/src/views/financialManagement/payable/payment.vue
+++ b/src/views/financialManagement/payable/payment.vue
@@ -65,6 +65,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -149,7 +151,25 @@
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)
diff --git a/src/views/financialManagement/payable/paymentApply.vue b/src/views/financialManagement/payable/paymentApply.vue
index e34793f..309d6d3 100644
--- a/src/views/financialManagement/payable/paymentApply.vue
+++ b/src/views/financialManagement/payable/paymentApply.vue
@@ -52,6 +52,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -477,6 +479,24 @@
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, ",");
diff --git a/src/views/financialManagement/payable/purchaseIn.vue b/src/views/financialManagement/payable/purchaseIn.vue
index 532bcb4..b105859 100644
--- a/src/views/financialManagement/payable/purchaseIn.vue
+++ b/src/views/financialManagement/payable/purchaseIn.vue
@@ -49,6 +49,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -189,7 +191,25 @@
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(),
diff --git a/src/views/financialManagement/payable/purchaseReturn.vue b/src/views/financialManagement/payable/purchaseReturn.vue
index eeec383..4171df2 100644
--- a/src/views/financialManagement/payable/purchaseReturn.vue
+++ b/src/views/financialManagement/payable/purchaseReturn.vue
@@ -44,6 +44,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -175,6 +177,24 @@
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",
diff --git a/src/views/financialManagement/payable/reconciliation.vue b/src/views/financialManagement/payable/reconciliation.vue
index e749e56..a3b8495 100644
--- a/src/views/financialManagement/payable/reconciliation.vue
+++ b/src/views/financialManagement/payable/reconciliation.vue
@@ -35,6 +35,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -405,6 +407,26 @@
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, ",");
diff --git a/src/views/financialManagement/receivable/invoiceApply.vue b/src/views/financialManagement/receivable/invoiceApply.vue
index 85f30b2..21b5f0e 100644
--- a/src/views/financialManagement/receivable/invoiceApply.vue
+++ b/src/views/financialManagement/receivable/invoiceApply.vue
@@ -48,6 +48,8 @@
v-loading="tableLoading"
:column="columns"
:tableData="dataList"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -648,6 +650,27 @@
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, ",");
diff --git a/src/views/financialManagement/receivable/outputInvoice.vue b/src/views/financialManagement/receivable/outputInvoice.vue
index d746aea..22cc202 100644
--- a/src/views/financialManagement/receivable/outputInvoice.vue
+++ b/src/views/financialManagement/receivable/outputInvoice.vue
@@ -46,6 +46,8 @@
v-loading="tableLoading"
:column="columns"
:tableData="dataList"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -324,6 +326,27 @@
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, ",");
diff --git a/src/views/financialManagement/receivable/reconciliation.vue b/src/views/financialManagement/receivable/reconciliation.vue
index b1bff0e..03400ef 100644
--- a/src/views/financialManagement/receivable/reconciliation.vue
+++ b/src/views/financialManagement/receivable/reconciliation.vue
@@ -30,6 +30,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -388,6 +390,29 @@
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, ",");
diff --git a/src/views/financialManagement/receivable/salesOut.vue b/src/views/financialManagement/receivable/salesOut.vue
index 0e24b37..f6205d8 100644
--- a/src/views/financialManagement/receivable/salesOut.vue
+++ b/src/views/financialManagement/receivable/salesOut.vue
@@ -43,6 +43,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -86,7 +88,6 @@
label: "閲戦",
prop: "outboundAmount",
minWidth: "120",
- align: "right",
formatData: val =>
val === null || val === undefined || val === ""
? ""
@@ -158,6 +159,27 @@
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",
diff --git a/src/views/financialManagement/receivable/salesReturn.vue b/src/views/financialManagement/receivable/salesReturn.vue
index c58d330..afe363c 100644
--- a/src/views/financialManagement/receivable/salesReturn.vue
+++ b/src/views/financialManagement/receivable/salesReturn.vue
@@ -37,6 +37,8 @@
:column="columns"
:tableData="dataList"
:tableLoading="tableLoading"
+ isShowSummary
+ :summaryMethod="getSummaries"
:page="{
current: pagination.currentPage,
size: pagination.pageSize,
@@ -80,7 +82,6 @@
label: "閫�娆炬�婚",
prop: "refundAmount",
minWidth: "120",
- align: "right",
formatData: (val) =>
val === null || val === undefined || val === ""
? ""
@@ -149,6 +150,27 @@
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",
diff --git a/src/views/financialManagement/voucher/index.vue b/src/views/financialManagement/voucher/index.vue
index 1aa6f69..0c02d51 100644
--- a/src/views/financialManagement/voucher/index.vue
+++ b/src/views/financialManagement/voucher/index.vue
@@ -25,7 +25,7 @@
</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>
@@ -453,6 +453,11 @@
return map[key] || "";
};
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁颁娇鐢� current/size锛屾棩鏈熻寖鍥存媶鍒嗕负 startDate/endDate
const getTableData = async () => {
try {
@@ -518,9 +523,9 @@
getTableData();
};
-const changePage = ({ current, size }) => {
- pagination.currentPage = current;
- pagination.pageSize = size;
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
getTableData();
};
diff --git a/src/views/inventoryManagement/stockManagement/New.vue b/src/views/inventoryManagement/stockManagement/New.vue
index 7ee84ea..9a49c46 100644
--- a/src/views/inventoryManagement/stockManagement/New.vue
+++ b/src/views/inventoryManagement/stockManagement/New.vue
@@ -86,8 +86,9 @@
<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="鎵瑰彿"
@@ -99,9 +100,10 @@
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="澶囨敞"
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
index ecff3a4..2d70a44 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
@@ -19,9 +19,9 @@
{{ 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">
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
index 3f0e99c..9442a5a 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -140,7 +140,7 @@
const tableColumn = ref([
// { label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 110 },
- { label: "鐢宠浜哄悕绉�", prop: "applicantName", minWidth: 100 },
+ { label: "鍙戣捣浜�", prop: "applicantName", minWidth: 100 },
{ label: "妯℃澘绫诲瀷", prop: "businessName", minWidth: 120 },
{
label: "瀹℃壒绫诲瀷",
@@ -154,7 +154,9 @@
prop: "unread",
width: 90,
align: "center",
+ dataType: "tag",
formatData: (v) => (v ? "鏄�" : "鍚�"),
+ formatType: (v) => (v ? "success" : "danger"),
},
{
label: "瀹℃壒鐘舵��",
diff --git a/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
index 9b3d91e..3c381dc 100644
--- a/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
+++ b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
@@ -24,7 +24,7 @@
<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>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
index c9da4fc..661c990 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
@@ -16,8 +16,8 @@
<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>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
index 17737e3..e2b5977 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
@@ -16,8 +16,8 @@
<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>
diff --git a/src/views/personnelManagement/socialSecuritySet/components/formDia.vue b/src/views/personnelManagement/socialSecuritySet/components/formDia.vue
index 71f5e61..985a6b1 100644
--- a/src/views/personnelManagement/socialSecuritySet/components/formDia.vue
+++ b/src/views/personnelManagement/socialSecuritySet/components/formDia.vue
@@ -1,34 +1,31 @@
<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>
@@ -38,124 +35,111 @@
<!-- 鍙充晶锛氬熀纭�淇℃伅 + 淇濋櫓绫诲瀷 -->
<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>
@@ -170,301 +154,327 @@
</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>
diff --git a/src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue b/src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue
new file mode 100644
index 0000000..f0ac5a6
--- /dev/null
+++ b/src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue
@@ -0,0 +1,236 @@
+<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>
diff --git a/src/views/productionManagement/productStructure/DetailNew/index.vue b/src/views/productionManagement/productStructure/DetailNew/index.vue
new file mode 100644
index 0000000..1262c80
--- /dev/null
+++ b/src/views/productionManagement/productStructure/DetailNew/index.vue
@@ -0,0 +1,626 @@
+<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;
+
+ // 濡傛灉鏈夊伐搴廔D锛屾鏌ユ墍鏈夐」鏄惁閮戒娇鐢ㄧ浉鍚岀殑宸ュ簭
+ 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) => {
+ // 涓烘瘡涓猺ow鐢熸垚鍞竴鐨勮矾寰�
+ // 浣跨敤row.id鎴栫储寮曚綔涓哄敮涓�鏍囪瘑
+ let path = "dataList";
+
+ // 绠�鍗曞疄鐜帮細浣跨敤row鐨刬d鎴栦竴涓敮涓�鏍囪瘑
+ 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;
+ }
+
+ // 鍏堝姞杞藉伐搴忛�夐」锛屽啀鍔犺浇鏁版嵁锛岀‘淇漞l-select鑳藉姝g‘鍥炴樉
+ 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>
\ No newline at end of file
diff --git a/src/views/productionManagement/productionOrder/index.vue b/src/views/productionManagement/productionOrder/index.vue
index 9c7682b..4d71881 100644
--- a/src/views/productionManagement/productionOrder/index.vue
+++ b/src/views/productionManagement/productionOrder/index.vue
@@ -487,7 +487,7 @@
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 鍒嗘棰滆壊锛氱孩/姗�/钃�/缁�
diff --git a/src/views/productionManagement/workOrder/index.vue b/src/views/productionManagement/workOrder/index.vue
index 8c76b2e..d3b0f93 100644
--- a/src/views/productionManagement/workOrder/index.vue
+++ b/src/views/productionManagement/workOrder/index.vue
@@ -475,7 +475,7 @@
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);
diff --git a/src/views/productionManagement/workOrderEdit/index.vue b/src/views/productionManagement/workOrderEdit/index.vue
index 37cbb4e..98d29f5 100644
--- a/src/views/productionManagement/workOrderEdit/index.vue
+++ b/src/views/productionManagement/workOrderEdit/index.vue
@@ -297,7 +297,7 @@
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 => {
diff --git a/src/views/productionManagement/workOrderManagement/index.vue b/src/views/productionManagement/workOrderManagement/index.vue
index 8bc6dc2..b721de6 100644
--- a/src/views/productionManagement/workOrderManagement/index.vue
+++ b/src/views/productionManagement/workOrderManagement/index.vue
@@ -549,7 +549,7 @@
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);
diff --git a/src/views/qualityManagement/metricBinding/index.vue b/src/views/qualityManagement/metricBinding/index.vue
index 1ac268a..b5d2a8f 100644
--- a/src/views/qualityManagement/metricBinding/index.vue
+++ b/src/views/qualityManagement/metricBinding/index.vue
@@ -128,6 +128,7 @@
<el-tree-select
v-model="selectedProductIds"
multiple
+ filterable
collapse-tags
collapse-tags-tooltip
placeholder="璇烽�夋嫨浜у搧锛堝彲澶氶�夛級"
diff --git a/vite.config.js b/vite.config.js
index fcc019d..03311be 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -12,7 +12,7 @@
: 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:{
--
Gitblit v1.9.3