Merge branch 'master' into docker
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -203,3 +203,4 @@ cython_debug/
|
||||
THAI-Platform/*
|
||||
urbanLifelineWeb/packages/wechat_demo/*
|
||||
urbanLifelineWeb/packages/workcase_wechat/unpackage/*
|
||||
docs/AI训练资料
|
||||
17
difyPlugin/.vscode/launch.json
vendored
Normal file
17
difyPlugin/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "DifyPlugin: FastAPI",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "run.py",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"python": "F:\\Environment\\conda\\envs\\difyPlugin\\python.exe",
|
||||
"env": {
|
||||
"PYTHONPATH": "${workspaceFolder}/difyPlugin"
|
||||
},
|
||||
"jinja": true
|
||||
}
|
||||
]
|
||||
}
|
||||
0
difyPlugin/.vscode/settings.json
vendored
Normal file
0
difyPlugin/.vscode/settings.json
vendored
Normal file
@@ -169,3 +169,20 @@ COMMENT ON COLUMN ai.tb_knowledge_file.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN ai.tb_knowledge_file.delete_time IS '删除时间';
|
||||
COMMENT ON COLUMN ai.tb_knowledge_file.deleted IS '是否删除';
|
||||
|
||||
DROP TABLE IF EXISTS ai.tb_knowledge_file_log CASCADE;
|
||||
CREATE TABLE ai.tb_knowledge_file_log(
|
||||
optsn VARCHAR(50) NOT NULL, -- 流水号
|
||||
log_id VARCHAR(50) NOT NULL, -- 日志ID
|
||||
knowledge_id VARCHAR(50) NOT NULL, -- 知识库ID
|
||||
file_root_id VARCHAR(50) NOT NULL, -- 文件根ID
|
||||
file_id VARCHAR(50) NOT NULL, -- 文件ID
|
||||
file_name VARCHAR(100) NOT NULL, -- 文件名
|
||||
service VARCHAR(50) NOT NULL, -- 所属服务 workcase、bidding
|
||||
version INTEGER NOT NULL DEFAULT 1, -- 文件版本
|
||||
action VARCHAR(50) NOT NULL, -- 操作类型 upload、update、delete
|
||||
creator VARCHAR(50) NOT NULL, -- 创建者(用户ID)
|
||||
creator_name VARCHAR(100) NOT NULL, -- 创建者姓名
|
||||
create_time TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
|
||||
PRIMARY KEY (optsn),
|
||||
UNIQUE (knowledge_id, file_id)
|
||||
);
|
||||
|
||||
@@ -38,8 +38,10 @@ CREATE TABLE workcase.tb_chat_room(
|
||||
guest_name VARCHAR(100) NOT NULL, -- 来客姓名
|
||||
ai_session_id VARCHAR(50) DEFAULT NULL, -- AI对话会话ID(从ai.tb_chat同步)
|
||||
message_count INTEGER NOT NULL DEFAULT 0, -- 消息总数
|
||||
device_code VARCHAR(50) NOT NULL, -- 设备代码
|
||||
last_message_time TIMESTAMPTZ DEFAULT NULL, -- 最后消息时间
|
||||
last_message TEXT DEFAULT NULL, -- 最后一条消息内容(用于列表展示)
|
||||
comment_level INTEGER DEFAULT 0, -- 服务评分(1-5)
|
||||
closed_by VARCHAR(50) DEFAULT NULL, -- 关闭人
|
||||
closed_time TIMESTAMPTZ DEFAULT NULL, -- 关闭时间
|
||||
creator VARCHAR(50) NOT NULL, -- 创建人(系统自动创建)
|
||||
|
||||
14
urbanLifelineServ/.vscode/launch.json
vendored
14
urbanLifelineServ/.vscode/launch.json
vendored
@@ -1,20 +1,6 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "java",
|
||||
"name": "URLQRCodeParseTest",
|
||||
"request": "launch",
|
||||
"mainClass": "org.xyzh.workcase.test.URLQRCodeParseTest",
|
||||
"projectName": "workcase"
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
"name": "QRCodeTest",
|
||||
"request": "launch",
|
||||
"mainClass": "org.xyzh.workcase.test.QRCodeTest",
|
||||
"projectName": "workcase"
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
"name": "AesEncryptUtil",
|
||||
|
||||
@@ -284,9 +284,8 @@ public class DifyApiClient {
|
||||
dataMap.put("process_rule", defaultProcessRule);
|
||||
}
|
||||
|
||||
// 默认设置文档形式和语言
|
||||
dataMap.put("doc_form", "text_model");
|
||||
dataMap.put("doc_language", "Chinese");
|
||||
// 只保留官方支持的参数
|
||||
// doc_form 和 doc_language 不是请求参数,移除
|
||||
|
||||
String dataJson = JSON.toJSONString(dataMap);
|
||||
logger.info("上传文档到知识库: datasetId={}, file={}, data={}", datasetId, originalFilename, dataJson);
|
||||
@@ -732,6 +731,56 @@ public class DifyApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话变量
|
||||
* @param conversationId 会话ID
|
||||
* @param userId 用户标识
|
||||
* @param lastId 当前页最后面一条记录的ID,默认null
|
||||
* @param limit 一次请求返回多少条记录,默认20条,最大100条,最小1条
|
||||
* @param apiKey API密钥
|
||||
* @return 对话变量响应
|
||||
*/
|
||||
public ConversationVariablesResponse getConversationVariables(
|
||||
String conversationId,
|
||||
String userId,
|
||||
String lastId,
|
||||
Integer limit,
|
||||
String apiKey) {
|
||||
|
||||
StringBuilder urlBuilder = new StringBuilder(
|
||||
difyConfig.getFullApiUrl("/conversations/" + conversationId + "/variables"));
|
||||
urlBuilder.append("?user=").append(userId);
|
||||
|
||||
if (lastId != null && !lastId.isEmpty()) {
|
||||
urlBuilder.append("&last_id=").append(lastId);
|
||||
}
|
||||
if (limit != null) {
|
||||
urlBuilder.append("&limit=").append(limit);
|
||||
}
|
||||
|
||||
try {
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(urlBuilder.toString())
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.get()
|
||||
.build();
|
||||
|
||||
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
logger.error("获取对话变量失败: {} - {}", response.code(), responseBody);
|
||||
throw new DifyException("获取对话变量失败: " + responseBody);
|
||||
}
|
||||
|
||||
return JSON.parseObject(responseBody, ConversationVariablesResponse.class);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("获取对话变量异常", e);
|
||||
throw new DifyException("获取对话变量异常: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 通用 HTTP 方法(用于代理转发)=====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.xyzh.ai.client.dto;
|
||||
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @description 对话变量响应
|
||||
* @filename ConversationVariablesResponse.java
|
||||
* @author AI Assistant
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-29
|
||||
*/
|
||||
@Data
|
||||
public class ConversationVariablesResponse {
|
||||
|
||||
private Integer limit;
|
||||
|
||||
@JSONField(name = "has_more")
|
||||
private Boolean hasMore;
|
||||
|
||||
private List<ConversationVariableItem> data;
|
||||
|
||||
/**
|
||||
* 对话中的变量项
|
||||
*/
|
||||
@Data
|
||||
public static class ConversationVariableItem {
|
||||
|
||||
/**
|
||||
* 变量ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 变量名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 变量类型 (string, number, boolean 等)
|
||||
*/
|
||||
@JSONField(name = "value_type")
|
||||
private String valueType;
|
||||
|
||||
/**
|
||||
* 变量值
|
||||
*/
|
||||
private String value;
|
||||
|
||||
/**
|
||||
* 变量描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 创建时间戳
|
||||
*/
|
||||
@JSONField(name = "created_at")
|
||||
private Long createdAt;
|
||||
|
||||
/**
|
||||
* 最后更新时间戳
|
||||
*/
|
||||
@JSONField(name = "updated_at")
|
||||
private Long updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.xyzh.ai.client.dto.ConversationVariablesResponse;
|
||||
import org.xyzh.api.ai.dto.ChatPrepareData;
|
||||
import org.xyzh.api.ai.dto.TbChat;
|
||||
import org.xyzh.api.ai.dto.TbChatMessage;
|
||||
@@ -46,6 +47,9 @@ public class ChatController {
|
||||
@Autowired
|
||||
private AIFileUploadService fileUploadService;
|
||||
|
||||
@Autowired
|
||||
private org.xyzh.ai.client.DifyApiClient difyApiClient;
|
||||
|
||||
// ====================== 会话管理 ======================
|
||||
|
||||
/**
|
||||
@@ -151,7 +155,7 @@ public class ChatController {
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
@PostMapping("/conversation/page")
|
||||
public ResultDomain<PageDomain<TbChat>> getChatPage(@RequestBody PageRequest<TbChat> pageRequest, @RequestHeader("Authorization") String token) {
|
||||
public ResultDomain<TbChat> getChatPage(@RequestBody PageRequest<TbChat> pageRequest, @RequestHeader("Authorization") String token) {
|
||||
log.info("分页获取会话列表: agentId={}", pageRequest.getFilter().getAgentId());
|
||||
|
||||
pageRequest.getFilter().setUserType(false);
|
||||
@@ -164,6 +168,53 @@ public class ChatController {
|
||||
return chatService.getChatPage(pageRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取对话变量
|
||||
* @param params 请求参数(包含agentId, conversationId, userId, lastId, limit)
|
||||
* @author yslg
|
||||
* @since 2025-12-29
|
||||
*/
|
||||
@PostMapping("/conversation/variables")
|
||||
public ResultDomain<org.xyzh.ai.client.dto.ConversationVariablesResponse> getConversationVariables(
|
||||
@RequestBody Map<String, Object> params,
|
||||
@RequestHeader("Authorization") String token) {
|
||||
|
||||
// 参数验证
|
||||
ValidationResult result = ValidationUtils.validateMap(params, Arrays.asList(
|
||||
ValidationUtils.requiredString("agentId", "智能体ID", 1, 100),
|
||||
ValidationUtils.requiredString("conversationId", "会话ID", 1, 100),
|
||||
ValidationUtils.requiredString("userId", "用户ID", 1, 100)
|
||||
));
|
||||
if (!result.isValid()) {
|
||||
return ResultDomain.failure(result.getAllErrors());
|
||||
}
|
||||
|
||||
String agentId = (String) params.get("agentId");
|
||||
String conversationId = (String) params.get("conversationId");
|
||||
String userId = (String) params.get("userId");
|
||||
String lastId = params.containsKey("lastId") ? (String) params.get("lastId") : null;
|
||||
Integer limit = params.containsKey("limit") ?
|
||||
Integer.parseInt(params.get("limit").toString()) : 20;
|
||||
|
||||
log.info("获取对话变量: agentId={}, conversationId={}, userId={}", agentId, conversationId, userId);
|
||||
|
||||
try {
|
||||
// 获取智能体信息以获取 API Key
|
||||
// 这里需要根据 agentId 获取对应的 API Key
|
||||
// 暂时先使用一个占位符,实际使用时需要从数据库或配置中获取
|
||||
// 或者通过 chatService 获取智能体配置
|
||||
|
||||
// 调用 Dify API 获取会话变量
|
||||
ConversationVariablesResponse response =
|
||||
difyApiClient.getConversationVariables(conversationId, userId, lastId, limit, agentId);
|
||||
|
||||
return ResultDomain.success("获取对话变量成功",response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取对话变量失败", e);
|
||||
return ResultDomain.failure("获取对话变量失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 消息管理 ======================
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,6 @@ import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -16,9 +15,11 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.xyzh.api.ai.service.DifyProxyService;
|
||||
import org.xyzh.api.ai.service.KnowledgeFileLogService;
|
||||
import org.xyzh.api.ai.service.KnowledgeService;
|
||||
import org.xyzh.api.ai.dto.TbKnowledge;
|
||||
import org.xyzh.api.ai.dto.TbKnowledgeFile;
|
||||
import org.xyzh.api.ai.dto.TbKnowledgeFileLog;
|
||||
import org.xyzh.api.ai.vo.KnowledgeFileVO;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
@@ -46,6 +47,9 @@ public class KnowledgeController {
|
||||
@Autowired
|
||||
private KnowledgeService knowledgeService;
|
||||
|
||||
@Autowired
|
||||
private KnowledgeFileLogService knowledgeFileLogService;
|
||||
|
||||
@Autowired
|
||||
private DifyProxyService difyProxyService;
|
||||
|
||||
@@ -235,10 +239,10 @@ public class KnowledgeController {
|
||||
* @since 2025-12-18
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('ai:knowledge:file:delete')")
|
||||
@DeleteMapping("/file/{fileId}")
|
||||
public ResultDomain<Boolean> deleteFile(@PathVariable("fileId") @NotBlank String fileId) {
|
||||
logger.info("删除知识库文件: fileId={}", fileId);
|
||||
return knowledgeService.deleteKnowledgeFileById(fileId);
|
||||
@DeleteMapping("/file/{fileRootId}")
|
||||
public ResultDomain<Boolean> deleteFile(@PathVariable("fileRootId") @NotBlank String fileRootId) {
|
||||
logger.info("删除知识库文件: fileId={}", fileRootId);
|
||||
return knowledgeService.deleteKnowledgeFileById(fileRootId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,4 +351,29 @@ public class KnowledgeController {
|
||||
logger.info("更新文档状态: datasetId={}, action={}", datasetId, action);
|
||||
return difyProxyService.updateDocumentStatus(datasetId, action, requestBody);
|
||||
}
|
||||
|
||||
// ================================ 知识库文件操作日志 =======================
|
||||
/**
|
||||
* @description 查询知识库操作日志列表
|
||||
* @param fileLog
|
||||
* @author yslg
|
||||
* @since 2025-12-18
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('ai:knowledge:file:view')")
|
||||
@PostMapping("/datasets/log/list")
|
||||
public ResultDomain<TbKnowledgeFileLog> getKnowledgeFileLogList(@RequestBody TbKnowledgeFileLog fileLog){
|
||||
return knowledgeFileLogService.getKnowledgeFileLogList(fileLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 查询知识库操作日志分页
|
||||
* @param pageRequest
|
||||
* @author yslg
|
||||
* @since 2025-12-18
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('ai:knowledge:file:view')")
|
||||
@PostMapping("/datasets/log/page")
|
||||
public ResultDomain<TbKnowledgeFileLog> getKnowledgeFileLogPage(@RequestBody PageRequest<TbKnowledgeFileLog> pageRequest){
|
||||
return knowledgeFileLogService.getKnowledgeFileLogPage(pageRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.xyzh.ai.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.xyzh.api.ai.dto.TbKnowledgeFile;
|
||||
import org.xyzh.api.ai.dto.TbKnowledgeFileLog;
|
||||
import org.xyzh.api.ai.vo.KnowledgeFileVO;
|
||||
import org.xyzh.common.core.page.PageParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @description 知识库文件数据访问层
|
||||
* @filename KnowledgeFileMapper.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
@Mapper
|
||||
public interface TbKnowledgeFileLogMapper {
|
||||
|
||||
int addKnowledgeFileLog(TbKnowledgeFileLog tbKnowledgeFileLog);
|
||||
|
||||
List<TbKnowledgeFileLog> getKnowledgeFileLogList(@Param("filter") TbKnowledgeFileLog filter);
|
||||
|
||||
List<TbKnowledgeFileLog> getKnowledgeFileLogPage(@Param("pageParam") PageParam pageParam,@Param("filter") TbKnowledgeFileLog filter);
|
||||
|
||||
|
||||
int countKnowledgeFileLog(@Param("filter") TbKnowledgeFileLog filter);
|
||||
|
||||
}
|
||||
@@ -32,6 +32,8 @@ public interface TbKnowledgeMapper {
|
||||
*/
|
||||
int deleteKnowledge(TbKnowledge knowledge);
|
||||
|
||||
int updateKnowledgeFileCount(@Param("knowledgeId") String knowledgeId, @Param("num") Integer num);
|
||||
|
||||
/**
|
||||
* 根据ID查询知识库
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.xyzh.ai.service.impl;
|
||||
|
||||
import org.apache.dubbo.config.annotation.DubboReference;
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -13,15 +14,18 @@ import org.xyzh.ai.client.dto.DocumentUploadResponse;
|
||||
import org.xyzh.api.ai.dto.TbAgent;
|
||||
import org.xyzh.api.ai.service.AIFileUploadService;
|
||||
import org.xyzh.api.ai.service.AgentService;
|
||||
import org.xyzh.api.file.dto.TbSysFileDTO;
|
||||
import org.xyzh.api.file.service.FileService;
|
||||
import org.xyzh.common.auth.utils.LoginUtil;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @description AI文件上传服务实现(只负责与Dify交互,不处理minio和数据库)
|
||||
* @description AI文件上传服务实现(同时上传到MinIO和Dify)
|
||||
* @filename AIFileUploadServiceImpl.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
@@ -37,6 +41,9 @@ public class AIFileUploadServiceImpl implements AIFileUploadService {
|
||||
@Autowired
|
||||
private AgentService agentService;
|
||||
|
||||
@DubboReference(version = "1.0.0", group = "file", timeout = 30000, retries = 0)
|
||||
private FileService fileService;
|
||||
|
||||
// ============================ 对话文件管理 ============================
|
||||
|
||||
@Override
|
||||
@@ -56,31 +63,58 @@ public class AIFileUploadServiceImpl implements AIFileUploadService {
|
||||
}
|
||||
TbAgent agent = agentResult.getData();
|
||||
|
||||
File tempFile = null;
|
||||
try {
|
||||
// 3. 将MultipartFile转换为临时File
|
||||
tempFile = File.createTempFile("upload_", "_" + file.getOriginalFilename());
|
||||
file.transferTo(tempFile);
|
||||
|
||||
// 4. 获取当前用户
|
||||
// 3. 获取当前用户
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
userId = "anonymous";
|
||||
}
|
||||
|
||||
// 5. 上传到Dify
|
||||
File tempFile = null;
|
||||
String sysFileId = null;
|
||||
String sysFileUrl = null;
|
||||
|
||||
try {
|
||||
// 4. 上传到MinIO(通过FileService,使用字节数组方式)
|
||||
byte[] fileBytes = file.getBytes();
|
||||
String fileName = file.getOriginalFilename();
|
||||
String contentType = file.getContentType();
|
||||
ResultDomain<TbSysFileDTO> fileResult = fileService.uploadFileBytes(fileBytes, fileName, contentType, "ai-chat", agentId);
|
||||
if (fileResult.getSuccess() && fileResult.getData() != null) {
|
||||
TbSysFileDTO sysFile = fileResult.getData();
|
||||
sysFileId = sysFile.getFileId();
|
||||
sysFileUrl = sysFile.getUrl();
|
||||
logger.info("上传文件到MinIO成功: fileId={}, url={}", sysFileId, sysFileUrl);
|
||||
} else {
|
||||
logger.warn("上传文件到MinIO失败: {}", fileResult.getMessage());
|
||||
// MinIO上传失败不阻断流程,继续上传到Dify
|
||||
}
|
||||
|
||||
// 5. 将MultipartFile转换为临时File用于Dify上传
|
||||
tempFile = File.createTempFile("upload_", "_" + file.getOriginalFilename());
|
||||
file.transferTo(tempFile);
|
||||
|
||||
// 6. 上传到Dify
|
||||
DifyFileInfo difyFile = difyApiClient.uploadFileForChat(tempFile, file.getOriginalFilename(), userId, agent.getApiKey());
|
||||
if (difyFile != null && StringUtils.hasText(difyFile.getId())) {
|
||||
logger.info("上传对话文件成功: agentId={}, fileId={}", agentId, difyFile.getId());
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
logger.info("上传对话文件到Dify成功: agentId={}, difyFileId={}", agentId, difyFile.getId());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
// Dify返回的信息
|
||||
result.put("id", difyFile.getId());
|
||||
result.put("name", difyFile.getName());
|
||||
result.put("size", difyFile.getSize());
|
||||
result.put("type", difyFile.getType());
|
||||
result.put("extension", difyFile.getExtension());
|
||||
result.put("mime_type", difyFile.getMimeType());
|
||||
result.put("upload_file_id", difyFile.getUploadFileId());
|
||||
// 系统文件信息(用于前端展示和数据库存储)
|
||||
result.put("sys_file_id", sysFileId);
|
||||
result.put("preview_url", sysFileUrl);
|
||||
result.put("source_url", sysFileUrl);
|
||||
|
||||
return ResultDomain.success("上传成功", result);
|
||||
}
|
||||
return ResultDomain.failure("上传文件失败");
|
||||
return ResultDomain.failure("上传文件到Dify失败");
|
||||
} catch (Exception e) {
|
||||
logger.error("上传对话文件异常: {}", e.getMessage(), e);
|
||||
return ResultDomain.failure("上传文件异常: " + e.getMessage());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.xyzh.ai.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
@@ -12,6 +13,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.xyzh.ai.client.DifyApiClient;
|
||||
import org.xyzh.ai.client.callback.StreamCallback;
|
||||
import org.xyzh.ai.client.dto.ChatRequest;
|
||||
import org.xyzh.ai.config.DifyConfig;
|
||||
import org.xyzh.ai.mapper.TbChatMapper;
|
||||
import org.xyzh.ai.mapper.TbChatMessageMapper;
|
||||
import org.xyzh.api.ai.dto.ChatPrepareData;
|
||||
@@ -19,17 +21,23 @@ import org.xyzh.api.ai.dto.DifyFileInfo;
|
||||
import org.xyzh.api.ai.dto.TbAgent;
|
||||
import org.xyzh.api.ai.dto.TbChat;
|
||||
import org.xyzh.api.ai.dto.TbChatMessage;
|
||||
import org.xyzh.api.ai.dto.TbKnowledge;
|
||||
import org.xyzh.api.ai.service.AgentChatService;
|
||||
import org.xyzh.api.ai.service.AgentService;
|
||||
import org.xyzh.api.ai.service.KnowledgeService;
|
||||
import org.xyzh.api.system.service.GuestService;
|
||||
import org.xyzh.common.core.domain.LoginDomain;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageDomain;
|
||||
import org.xyzh.common.core.page.PageParam;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
import org.xyzh.common.redis.service.RedisService;
|
||||
import org.xyzh.common.utils.NonUtils;
|
||||
import org.xyzh.common.utils.id.IdUtil;
|
||||
import org.xyzh.common.auth.utils.LoginUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -69,20 +77,11 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
/**
|
||||
* @description 根据 userType 获取用户ID
|
||||
* @param chat 会话信息(包含 userId 和 userType)
|
||||
* @return 真实的系统用户ID
|
||||
*/
|
||||
private String getUserIdByType(TbChat chat) {
|
||||
if (!chat.getUserType()) {
|
||||
// 来客(userType=false):直接返回传入的 userId(已经是真正的系统 userId)
|
||||
return chat.getUserId();
|
||||
} else {
|
||||
// 员工(userType=true):从登录信息获取 userId
|
||||
return LoginUtil.getCurrentUserId();
|
||||
}
|
||||
}
|
||||
@Autowired
|
||||
private KnowledgeService knowledgeService;
|
||||
|
||||
@Autowired
|
||||
private DifyConfig difyConfig;
|
||||
|
||||
/**
|
||||
* @description 判断智能体是否是outer
|
||||
@@ -130,7 +129,8 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
}
|
||||
|
||||
// 2. 获取用户ID并校验权限
|
||||
String userId = getUserIdByType(chat);
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
String userId = loginDomain.getUser().getUserId();
|
||||
if (userId == null) {
|
||||
return ResultDomain.failure("用户信息获取失败");
|
||||
}
|
||||
@@ -164,7 +164,8 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
return ResultDomain.failure("智能体不可用");
|
||||
}
|
||||
// 2. 获取用户ID并校验权限
|
||||
String userId = getUserIdByType(filter);
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
String userId = loginDomain.getUser().getUserId();
|
||||
if (userId == null) {
|
||||
return ResultDomain.failure("用户信息获取失败");
|
||||
}
|
||||
@@ -189,12 +190,9 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbChat> getChatList(TbChat filter) {
|
||||
// 判断agent是否是outer
|
||||
if(!isOuterAgent(filter.getAgentId())){
|
||||
return ResultDomain.failure("智能体不可用");
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
String userId = getUserIdByType(filter);
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
return ResultDomain.failure("用户信息获取失败");
|
||||
}
|
||||
@@ -204,16 +202,16 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<PageDomain<TbChat>> getChatPage(PageRequest<TbChat> pageRequest) {
|
||||
public ResultDomain<TbChat> getChatPage(PageRequest<TbChat> pageRequest) {
|
||||
TbChat filter = pageRequest.getFilter();
|
||||
// 判断agent是否是outer(来客才需要校验)
|
||||
if (!filter.getUserType() && !isOuterAgent(filter.getAgentId())) {
|
||||
return ResultDomain.<PageDomain<TbChat>>failure("智能体不可用");
|
||||
return ResultDomain.<TbChat>failure("智能体不可用");
|
||||
}
|
||||
// 获取用户ID
|
||||
String userId = getUserIdByType(filter);
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
return ResultDomain.<PageDomain<TbChat>>failure("用户信息获取失败");
|
||||
return ResultDomain.<TbChat>failure("用户信息获取失败");
|
||||
}
|
||||
filter.setUserId(userId);
|
||||
|
||||
@@ -224,7 +222,7 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
pageParam.setTotal((int) total);
|
||||
|
||||
PageDomain<TbChat> pageDomain = new PageDomain<>(pageParam, chatList);
|
||||
return ResultDomain.<PageDomain<TbChat>>success("查询成功", pageDomain);
|
||||
return ResultDomain.<TbChat>success("查询成功", pageDomain);
|
||||
}
|
||||
|
||||
// ====================== 智能体聊天管理 ======================
|
||||
@@ -241,7 +239,7 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
return ResultDomain.failure("智能体不可用");
|
||||
}
|
||||
// 2. 获取用户ID并校验权限
|
||||
String userId = getUserIdByType(filter);
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
return ResultDomain.failure("用户信息获取失败");
|
||||
}
|
||||
@@ -272,7 +270,8 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
chatFilter.setUserId(prepareData.getUserId());
|
||||
chatFilter.setUserType(prepareData.getUserType());
|
||||
|
||||
String userId = getUserIdByType(chatFilter);
|
||||
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
|
||||
String userId = loginDomain.getUser().getUserId();
|
||||
if (userId == null) {
|
||||
return ResultDomain.failure("用户信息获取失败");
|
||||
}
|
||||
@@ -299,6 +298,9 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
sessionData.put("userId", userId);
|
||||
sessionData.put("filesData", prepareData.getFiles());
|
||||
sessionData.put("apiKey", agent.getApiKey());
|
||||
sessionData.put("outer", agent.getIsOuter());
|
||||
sessionData.put("service", prepareData.getService());
|
||||
sessionData.put("isGuest", "guest".equals(loginDomain.getUser().getStatus()));
|
||||
|
||||
String cacheKey = CHAT_SESSION_PREFIX + sessionId;
|
||||
redisService.set(cacheKey, sessionData, SESSION_TTL, TimeUnit.SECONDS);
|
||||
@@ -332,6 +334,10 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
String query = (String) sessionData.get("query");
|
||||
String userId = (String) sessionData.get("userId");
|
||||
String apiKey = (String) sessionData.get("apiKey");
|
||||
String service = (String) sessionData.get("service");
|
||||
Boolean outer = (Boolean) sessionData.get("outer");
|
||||
Boolean isGuest = (Boolean) sessionData.get("isGuest");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<DifyFileInfo> filesData = (List<DifyFileInfo>) sessionData.get("filesData");
|
||||
|
||||
@@ -346,6 +352,18 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
userMessage.setChatId(chatId);
|
||||
userMessage.setRole("user");
|
||||
userMessage.setContent(query);
|
||||
|
||||
// 提取系统文件ID列表保存到消息中
|
||||
if (filesData != null && !filesData.isEmpty()) {
|
||||
List<String> sysFileIds = filesData.stream()
|
||||
.map(DifyFileInfo::getSysFileId)
|
||||
.filter(StringUtils::hasText)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
if (!sysFileIds.isEmpty()) {
|
||||
userMessage.setFiles(sysFileIds);
|
||||
}
|
||||
}
|
||||
|
||||
chatMessageMapper.insertChatMessage(userMessage);
|
||||
|
||||
// 5. 构建Dify请求
|
||||
@@ -353,7 +371,23 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
chatRequest.setQuery(query);
|
||||
chatRequest.setUser(userId);
|
||||
chatRequest.setResponseMode("streaming");
|
||||
chatRequest.setInputs(new HashMap<>()); // Dify API 要求 inputs 必传
|
||||
Map<String, Object> inputsMap = new HashMap<>();
|
||||
chatRequest.setInputs(inputsMap); // Dify API 要求 inputs 必传
|
||||
// 处理动态知识库的问题
|
||||
if(outer && NonUtils.isNotEmpty(service)){
|
||||
TbKnowledge filter = new TbKnowledge();
|
||||
filter.setService(service);
|
||||
filter.setCategory(isGuest?"external":"internal");
|
||||
ResultDomain<TbKnowledge> knowledgeRD = knowledgeService.listKnowledges(filter);
|
||||
List<String> datasets = new ArrayList<>();
|
||||
if(knowledgeRD.getSuccess()){
|
||||
datasets = knowledgeRD.getDataList().stream().map(TbKnowledge::getDifyDatasetId).toList();
|
||||
}
|
||||
inputsMap.put("datasets", JSON.toJSONString(datasets));
|
||||
inputsMap.put("dataset_apikey", difyConfig.getKnowledgeApiKey());
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (filesData != null && !filesData.isEmpty()) {
|
||||
chatRequest.setFiles(filesData);
|
||||
@@ -442,7 +476,7 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
TbAgent agent = agentResult.getData();
|
||||
|
||||
// 2. 获取用户ID
|
||||
String userId = getUserIdByType(filter);
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
return ResultDomain.failure("用户信息获取失败");
|
||||
}
|
||||
@@ -468,7 +502,7 @@ public class AgentChatServiceImpl implements AgentChatService {
|
||||
}
|
||||
|
||||
// 2. 获取用户ID
|
||||
String userId = getUserIdByType(filter);
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
return ResultDomain.failure("用户信息获取失败");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.xyzh.ai.service.impl;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.xyzh.ai.mapper.TbKnowledgeFileLogMapper;
|
||||
import org.xyzh.api.ai.dto.TbKnowledgeFileLog;
|
||||
import org.xyzh.api.ai.service.KnowledgeFileLogService;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageDomain;
|
||||
import org.xyzh.common.core.page.PageParam;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
import org.xyzh.common.utils.NonUtils;
|
||||
import org.xyzh.common.utils.id.IdUtil;
|
||||
import org.xyzh.common.utils.validation.ValidationResult;
|
||||
import org.xyzh.common.utils.validation.ValidationUtils;
|
||||
|
||||
@DubboService(version="1.0.0", group="ai", timeout=3000, retries=0)
|
||||
public class KnowledgeFileLogServiceImpl implements KnowledgeFileLogService{
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(KnowledgeServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private TbKnowledgeFileLogMapper knowledgeFileLogMapper;
|
||||
|
||||
/**
|
||||
* @description 新增知识库文件操作日志
|
||||
* @param knowledgeFileLog
|
||||
* @return 日志
|
||||
* @author yslg
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
@Override
|
||||
public ResultDomain<TbKnowledgeFileLog> addKnowledgeFileLog(TbKnowledgeFileLog knowledgeFileLog){
|
||||
knowledgeFileLog.setOptsn(IdUtil.getOptsn());
|
||||
knowledgeFileLog.setLogId(IdUtil.generateID());
|
||||
ValidationResult rt = ValidationUtils.validate(knowledgeFileLog, Arrays.asList(
|
||||
|
||||
));
|
||||
if(!rt.isValid()){
|
||||
return ResultDomain.failure("日志参数校验失败");
|
||||
}
|
||||
int result = knowledgeFileLogMapper.addKnowledgeFileLog(knowledgeFileLog);
|
||||
if(result >0){
|
||||
return ResultDomain.success("添加知识库文件日志成功", knowledgeFileLog);
|
||||
}else {
|
||||
return ResultDomain.failure("添加知识库文件日志失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 查询知识库日志操作列表
|
||||
* @param filter
|
||||
* @return 日志列表
|
||||
* @author yslg
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
@Override
|
||||
public ResultDomain<TbKnowledgeFileLog> getKnowledgeFileLogList(TbKnowledgeFileLog filter){
|
||||
List<TbKnowledgeFileLog> logs = knowledgeFileLogMapper.getKnowledgeFileLogList(filter);
|
||||
return ResultDomain.success("查询知识库日志成功",logs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 查询知识库日志操作分页
|
||||
* @param pageRequest
|
||||
* @return 日志分页数据
|
||||
* @author yslg
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
@Override
|
||||
public ResultDomain<TbKnowledgeFileLog> getKnowledgeFileLogPage(PageRequest<TbKnowledgeFileLog> pageRequest){
|
||||
List<TbKnowledgeFileLog> logs = knowledgeFileLogMapper.getKnowledgeFileLogPage(pageRequest.getPageParam(), pageRequest.getFilter());
|
||||
int total = knowledgeFileLogMapper.countKnowledgeFileLog(pageRequest.getFilter());
|
||||
PageDomain<TbKnowledgeFileLog> pageDomain = new PageDomain<>();
|
||||
pageDomain.setDataList(logs);
|
||||
PageParam pageParam = pageRequest.getPageParam();
|
||||
pageParam.setTotal(total);
|
||||
pageDomain.setPageParam(pageParam);
|
||||
return ResultDomain.success("查询知识库日志成功", pageDomain);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -16,7 +16,10 @@ import org.xyzh.ai.mapper.TbKnowledgeFileMapper;
|
||||
import org.xyzh.ai.mapper.TbKnowledgeMapper;
|
||||
import org.xyzh.api.ai.dto.TbKnowledge;
|
||||
import org.xyzh.api.ai.dto.TbKnowledgeFile;
|
||||
import org.xyzh.api.ai.dto.TbKnowledgeFileLog;
|
||||
import org.xyzh.api.ai.constance.KnowledgeFileLogAction;
|
||||
import org.xyzh.api.ai.service.AIFileUploadService;
|
||||
import org.xyzh.api.ai.service.KnowledgeFileLogService;
|
||||
import org.xyzh.api.ai.service.KnowledgeService;
|
||||
import org.xyzh.api.ai.vo.KnowledgeFileVO;
|
||||
import org.xyzh.api.file.dto.TbSysFileDTO;
|
||||
@@ -58,6 +61,9 @@ public class KnowledgeServiceImpl implements KnowledgeService {
|
||||
@DubboReference(version = "1.0.0", group = "file", timeout = 30000, retries = 0)
|
||||
private FileService fileService;
|
||||
|
||||
@Autowired
|
||||
private KnowledgeFileLogService knowledgeFileLogService;
|
||||
|
||||
@Autowired
|
||||
private AIFileUploadService aiFileUploadService;
|
||||
|
||||
@@ -533,9 +539,22 @@ public class KnowledgeServiceImpl implements KnowledgeService {
|
||||
knowledgeFile.setDifyFileId(difyFileId);
|
||||
knowledgeFile.setVersion(1);
|
||||
|
||||
knowledgeMapper.updateKnowledgeFileCount(knowledgeId, 1);
|
||||
int rows = knowledgeFileMapper.insertKnowledgeFile(knowledgeFile);
|
||||
if (rows > 0) {
|
||||
logger.info("保存知识库文件记录成功: knowledgeId={}, fileId={}, difyFileId={}", knowledgeId, fileId, difyFileId);
|
||||
// 记录日志
|
||||
TbKnowledgeFileLog log = new TbKnowledgeFileLog();
|
||||
log.setKnowledgeId(knowledgeId);
|
||||
log.setFileRootId(fileId);
|
||||
log.setFileId(fileId);
|
||||
log.setFileName(file.getOriginalFilename());
|
||||
log.setVersion(1);
|
||||
log.setAction(KnowledgeFileLogAction.UPLOAD.getAction());
|
||||
log.setService("workcase");
|
||||
log.setCreator(LoginUtil.getCurrentUserId());
|
||||
log.setCreatorName(LoginUtil.getCurrentUserName());
|
||||
knowledgeFileLogService.addKnowledgeFileLog(log);
|
||||
return ResultDomain.success("上传成功", knowledgeFile);
|
||||
}
|
||||
|
||||
@@ -682,6 +701,18 @@ public class KnowledgeServiceImpl implements KnowledgeService {
|
||||
int rows = knowledgeFileMapper.insertKnowledgeFile(newKnowledgeFile);
|
||||
if (rows > 0) {
|
||||
logger.info("保存新版本记录成功: knowledgeId={}, fileRootId={}, newVersion={}", knowledgeId, fileRootId, newVersion);
|
||||
// 记录日志
|
||||
TbKnowledgeFileLog log = new TbKnowledgeFileLog();
|
||||
log.setKnowledgeId(knowledgeId);
|
||||
log.setFileRootId(fileRootId);
|
||||
log.setFileId(newFileId);
|
||||
log.setFileName(file.getOriginalFilename());
|
||||
log.setVersion(newVersion);
|
||||
log.setAction(KnowledgeFileLogAction.UPDATE.getAction());
|
||||
log.setService("workcase");
|
||||
log.setCreator(LoginUtil.getCurrentUserId());
|
||||
log.setCreatorName(LoginUtil.getCurrentUserName());
|
||||
knowledgeFileLogService.addKnowledgeFileLog(log);
|
||||
return ResultDomain.success("更新成功", newKnowledgeFile);
|
||||
}
|
||||
|
||||
@@ -722,15 +753,27 @@ public class KnowledgeServiceImpl implements KnowledgeService {
|
||||
if (!difyDocIds.isEmpty()) {
|
||||
aiFileUploadService.batchDeleteFilesFromDify(knowledge.getDifyDatasetId(), difyDocIds);
|
||||
}
|
||||
}else{
|
||||
return ResultDomain.failure("知识库未关联Dify");
|
||||
}
|
||||
|
||||
// 3. 软删除本地记录和minio文件
|
||||
int rows = knowledgeFileMapper.deleteFilesByRootId(fileRootId);
|
||||
knowledgeMapper.updateKnowledgeFileCount(knowledge.getKnowledgeId(), -1);
|
||||
if (rows > 0) {
|
||||
logger.info("删除知识库文件成功: fileRootId={}", fileRootId);
|
||||
for (TbKnowledgeFile file : versions) {
|
||||
fileService.deleteFile(file.getFileId());
|
||||
}
|
||||
// 记录日志
|
||||
TbKnowledgeFileLog log = new TbKnowledgeFileLog();
|
||||
log.setKnowledgeId(knowledge.getKnowledgeId());
|
||||
log.setFileRootId(fileRootId);
|
||||
log.setAction(KnowledgeFileLogAction.DELETE.getAction());
|
||||
log.setService("workcase");
|
||||
log.setCreator(LoginUtil.getCurrentUserId());
|
||||
log.setCreatorName(LoginUtil.getCurrentUserName());
|
||||
knowledgeFileLogService.addKnowledgeFileLog(log);
|
||||
return ResultDomain.success("删除成功", true);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,12 @@ security:
|
||||
spring:
|
||||
application:
|
||||
name: ai-service
|
||||
|
||||
# 文件上传配置
|
||||
servlet:
|
||||
multipart:
|
||||
enabled: true
|
||||
max-file-size: 500MB
|
||||
max-request-size: 500MB
|
||||
# ================== Spring Cloud Nacos ==================
|
||||
cloud:
|
||||
nacos:
|
||||
@@ -72,6 +77,7 @@ dubbo:
|
||||
name: urban-lifeline-agent
|
||||
qos-enable: false
|
||||
protocol:
|
||||
payload: 110100480
|
||||
name: dubbo
|
||||
port: -1
|
||||
registry:
|
||||
|
||||
@@ -72,6 +72,7 @@ dubbo:
|
||||
name: urban-lifeline-agent
|
||||
qos-enable: false
|
||||
protocol:
|
||||
payload: 110100480
|
||||
name: dubbo
|
||||
port: -1
|
||||
registry:
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<AppenderRef ref="RollingFile"/>
|
||||
</Logger>
|
||||
|
||||
<Logger name="org.xyzh.agent" level="debug" additivity="false">
|
||||
<Logger name="org.xyzh.ai" level="debug" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
<AppenderRef ref="RollingFile"/>
|
||||
</Logger>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.xyzh.ai.mapper.TbKnowledgeFileLogMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="org.xyzh.api.ai.dto.TbKnowledgeFileLog">
|
||||
<result column="optsn" property="optsn" jdbcType="VARCHAR"/>
|
||||
<result column="log_id" property="logId" jdbcType="VARCHAR"/>
|
||||
<result column="knowledge_id" property="knowledgeId" jdbcType="VARCHAR"/>
|
||||
<result column="file_root_id" property="fileRootId" jdbcType="VARCHAR"/>
|
||||
<result column="file_id" property="fileId" jdbcType="VARCHAR"/>
|
||||
<result column="file_name" property="fileName" jdbcType="VARCHAR"/>
|
||||
<result column="action" property="action" jdbcType="VARCHAR"/>
|
||||
<result column="version" property="version" jdbcType="INTEGER"/>
|
||||
<result column="creator" property="creator" jdbcType="VARCHAR"/>
|
||||
<result column="creator_name" property="creatorName" jdbcType="VARCHAR"/>
|
||||
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
optsn, log_id, knowledge_id, file_root_id, file_id, file_name, version,
|
||||
action, creator, creator_name, create_time
|
||||
</sql>
|
||||
|
||||
<insert id="addKnowledgeFileLog" parameterType="org.xyzh.api.ai.dto.TbKnowledgeFileLog">
|
||||
INSERT INTO ai.tb_knowledge_file_log (
|
||||
optsn, log_id, knowledge_id, file_root_id, file_id, file_name, version,
|
||||
action, service, creator, creator_name, create_time
|
||||
) VALUES (
|
||||
#{optsn}, #{logId}, #{knowledgeId}, #{fileRootId}, #{fileId}, #{fileName}, #{version},
|
||||
#{action}, #{service}, #{creator}, #{creatorName}, NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<select id="getKnowledgeFileLogList" resultMap="BaseResultMap">
|
||||
SELECT
|
||||
<include refid="Base_Column_List"/>
|
||||
FROM ai.tb_knowledge_file_log
|
||||
WHERE 1=1
|
||||
<if test="filter.knowledgeId != null and filter.knowledgeId != ''">
|
||||
AND knowledge_id = #{filter.knowledgeId}
|
||||
</if>
|
||||
<if test="filter.service != null and filter.service != ''">
|
||||
AND service = #{filter.service}
|
||||
</if>
|
||||
<if test="filter.fileName != null and filter.fileName != ''">
|
||||
AND file_name LIKE CONCAT('%', #{filter.fileName}, '%')
|
||||
</if>
|
||||
<if test="filter.creatorName != null and filter.creatorName != ''">
|
||||
AND creator_name LIKE CONCAT('%', #{filter.creatorName}, '%')
|
||||
</if>
|
||||
ORDER BY create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="getKnowledgeFileLogPage" resultMap="BaseResultMap">
|
||||
SELECT
|
||||
<include refid="Base_Column_List"/>
|
||||
FROM ai.tb_knowledge_file_log
|
||||
WHERE 1=1
|
||||
<if test="filter.knowledgeId != null and filter.knowledgeId != ''">
|
||||
AND knowledge_id = #{filter.knowledgeId}
|
||||
</if>
|
||||
<if test="filter.service != null and filter.service != ''">
|
||||
AND service = #{filter.service}
|
||||
</if>
|
||||
<if test="filter.fileName != null and filter.fileName != ''">
|
||||
AND file_name LIKE CONCAT('%', #{filter.fileName}, '%')
|
||||
</if>
|
||||
<if test="filter.creatorName != null and filter.creatorName != ''">
|
||||
AND creator_name LIKE CONCAT('%', #{filter.creatorName}, '%')
|
||||
</if>
|
||||
ORDER BY create_time DESC
|
||||
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
|
||||
</select>
|
||||
|
||||
<select id="countKnowledgeFileLog" resultType="int">
|
||||
SELECT
|
||||
count(log_id)
|
||||
FROM ai.tb_knowledge_file_log
|
||||
WHERE 1=1
|
||||
<if test="filter.knowledgeId != null and filter.knowledgeId != ''">
|
||||
AND knowledge_id = #{filter.knowledgeId}
|
||||
</if>
|
||||
<if test="filter.service != null and filter.service != ''">
|
||||
AND service = #{filter.service}
|
||||
</if>
|
||||
<if test="filter.fileName != null and filter.fileName != ''">
|
||||
AND file_name LIKE CONCAT('%', #{filter.fileName}, '%')
|
||||
</if>
|
||||
<if test="filter.creatorName != null and filter.creatorName != ''">
|
||||
AND creator_name LIKE CONCAT('%', #{filter.creatorName}, '%')
|
||||
</if>
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -112,6 +112,12 @@
|
||||
WHERE knowledge_id = #{knowledgeId} AND deleted = false
|
||||
</update>
|
||||
|
||||
<update id="updateKnowledgeFileCount">
|
||||
UPDATE ai.tb_knowledge
|
||||
SET document_count = document_count + #{num}
|
||||
WHERE knowledge_id = #{knowledgeId} AND deleted = false
|
||||
</update>
|
||||
|
||||
<select id="selectKnowledgeById" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM ai.tb_knowledge
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.xyzh.api.ai.constance;
|
||||
|
||||
|
||||
public enum KnowledgeFileLogAction {
|
||||
UPLOAD("upload", "上传文件"),
|
||||
DELETE("delete", "删除文件"),
|
||||
DOWNLOAD("download", "下载文件"),
|
||||
UPDATE("update", "更新文件"),
|
||||
SEGMENT_CREATE("segment_create", "创建分段"),
|
||||
SEGMENT_UPDATE("segment_update", "更新分段"),
|
||||
SEGMENT_DELETE("segment_delete", "更新分段");
|
||||
|
||||
private String action;
|
||||
private String description;
|
||||
private KnowledgeFileLogAction(String action, String description) {
|
||||
this.action = action;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public String getDescription(){
|
||||
return description;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,4 +29,6 @@ public class ChatPrepareData implements Serializable {
|
||||
@Schema(description = "用户类型(false=来客,true=员工)")
|
||||
private Boolean userType;
|
||||
|
||||
@Schema(description = "服务名称")
|
||||
private String service;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.xyzh.api.ai.dto;
|
||||
|
||||
import org.xyzh.common.dto.BaseDTO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "知识库文件日志")
|
||||
public class TbKnowledgeFileLog extends BaseDTO {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "日志Id")
|
||||
private String logId;
|
||||
|
||||
@Schema(description = "知识库Id")
|
||||
private String knowledgeId;
|
||||
|
||||
@Schema(description = "文件根Id")
|
||||
private String fileRootId;
|
||||
|
||||
@Schema(description = "文件Id")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "文件名称")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "服务名称")
|
||||
private String service;
|
||||
|
||||
@Schema(description = "文件版本")
|
||||
private Integer version;
|
||||
|
||||
@Schema(description = "操作")
|
||||
private String action;
|
||||
|
||||
@Schema(description = "操作人用户名")
|
||||
private String creatorName;
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public interface AgentChatService {
|
||||
* @param pageRequest 分页请求参数
|
||||
* @return 分页会话列表
|
||||
*/
|
||||
ResultDomain<PageDomain<TbChat>> getChatPage(PageRequest<TbChat> pageRequest);
|
||||
ResultDomain<TbChat> getChatPage(PageRequest<TbChat> pageRequest);
|
||||
|
||||
|
||||
// ====================== 智能体聊天管理 ======================
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.xyzh.api.ai.service;
|
||||
|
||||
import org.xyzh.api.ai.dto.TbKnowledgeFileLog;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
|
||||
/**
|
||||
* @description 知识库文件操作日志
|
||||
* @filename KnowledgeFileLogService.java
|
||||
* @author yslg
|
||||
* @copyright yslg
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
public interface KnowledgeFileLogService {
|
||||
|
||||
/**
|
||||
* @description 新增知识库文件操作日志
|
||||
* @param knowledgeFileLog
|
||||
* @return 日志
|
||||
* @author yslg
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
ResultDomain<TbKnowledgeFileLog> addKnowledgeFileLog(TbKnowledgeFileLog knowledgeFileLog);
|
||||
|
||||
/**
|
||||
* @description 查询知识库日志操作列表
|
||||
* @param filter
|
||||
* @return 日志列表
|
||||
* @author yslg
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
ResultDomain<TbKnowledgeFileLog> getKnowledgeFileLogList(TbKnowledgeFileLog filter);
|
||||
|
||||
/**
|
||||
* @description 查询知识库日志操作分页
|
||||
* @param pageRequest
|
||||
* @return 日志分页数据
|
||||
* @author yslg
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
ResultDomain<TbKnowledgeFileLog> getKnowledgeFileLogPage(PageRequest<TbKnowledgeFileLog> pageRequest);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.xyzh.api.system.constance;
|
||||
|
||||
/**
|
||||
* 通过redis事件,实现数据库更新配置,更新其他服务的bean数据
|
||||
*/
|
||||
public class SysConfigRedisPrefix {
|
||||
public static final String SYS_CONFIG_DIFY="sys:config:dify";
|
||||
}
|
||||
@@ -60,6 +60,12 @@ public class TbChatRoomDTO extends BaseDTO {
|
||||
@Schema(description = "最后一条消息内容")
|
||||
private String lastMessage;
|
||||
|
||||
@Schema(description = "服务评分(1-5星)")
|
||||
private Integer commentLevel;
|
||||
|
||||
@Schema(description = "设备代码")
|
||||
private String deviceCode;
|
||||
|
||||
@Schema(description = "关闭人")
|
||||
private String closedBy;
|
||||
|
||||
|
||||
@@ -208,4 +208,16 @@ public interface ChatRoomService {
|
||||
*/
|
||||
ResultDomain<CustomerServiceVO> assignCustomerService(String roomId);
|
||||
|
||||
// ========================= 聊天室评分管理 ==========================
|
||||
|
||||
/**
|
||||
* @description 提交聊天室服务评分
|
||||
* @param roomId 聊天室ID
|
||||
* @param commentLevel 评分(1-5星)
|
||||
* @param userId 评分用户ID
|
||||
* @author cascade
|
||||
* @since 2025-12-29
|
||||
*/
|
||||
ResultDomain<Boolean> submitCommentLevel(String roomId, Integer commentLevel, String userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.xyzh.api.workcase.service;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO;
|
||||
import org.xyzh.api.workcase.vo.WorkcaseProcessVO;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
|
||||
@@ -110,7 +111,7 @@ public interface WorkcaseService {
|
||||
* @author yslg
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessList(TbWorkcaseProcessDTO filter);
|
||||
ResultDomain<WorkcaseProcessVO> getWorkcaseProcessList(TbWorkcaseProcessDTO filter);
|
||||
|
||||
/**
|
||||
* @description 获取工单处理过程分页
|
||||
@@ -118,7 +119,7 @@ public interface WorkcaseService {
|
||||
* @author yslg
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessPage(PageRequest<TbWorkcaseProcessDTO> pageRequest);
|
||||
ResultDomain<WorkcaseProcessVO> getWorkcaseProcessPage(PageRequest<TbWorkcaseProcessDTO> pageRequest);
|
||||
|
||||
// ====================== 工单设备管理 ======================
|
||||
/**
|
||||
|
||||
@@ -63,6 +63,12 @@ public class ChatRoomVO extends BaseVO {
|
||||
@Schema(description = "最后一条消息内容")
|
||||
private String lastMessage;
|
||||
|
||||
@Schema(description = "服务评分(1-5星)")
|
||||
private Integer commentLevel;
|
||||
|
||||
@Schema(description = "设备代码")
|
||||
private String deviceCode;
|
||||
|
||||
@Schema(description = "关闭人")
|
||||
private String closedBy;
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.xyzh.api.workcase.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.xyzh.common.vo.BaseVO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 工单列表VO
|
||||
* 用于前端列表展示(简化版)
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "工单列表VO")
|
||||
public class TicketListVO extends BaseVO {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "工单ID")
|
||||
private String ticketId;
|
||||
|
||||
@Schema(description = "工单编号")
|
||||
private String ticketNo;
|
||||
|
||||
@Schema(description = "客户姓名")
|
||||
private String customerName;
|
||||
|
||||
@Schema(description = "工单标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "工单类型名称")
|
||||
private String ticketTypeName;
|
||||
|
||||
@Schema(description = "优先级")
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "优先级名称")
|
||||
private String priorityName;
|
||||
|
||||
@Schema(description = "工单状态")
|
||||
private String ticketStatus;
|
||||
|
||||
@Schema(description = "工单状态名称")
|
||||
private String ticketStatusName;
|
||||
|
||||
@Schema(description = "处理人姓名")
|
||||
private String assignedToName;
|
||||
|
||||
@Schema(description = "SLA截止时间", format = "date-time")
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date slaDeadline;
|
||||
|
||||
@Schema(description = "是否逾期", defaultValue = "false")
|
||||
private Boolean isOverdue;
|
||||
|
||||
@Schema(description = "创建者姓名")
|
||||
private String creatorName;
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package org.xyzh.api.workcase.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.xyzh.common.vo.BaseVO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 工单VO
|
||||
* 用于前端展示工单信息
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "工单VO")
|
||||
public class TicketVO extends BaseVO {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "工单ID")
|
||||
private String ticketId;
|
||||
|
||||
@Schema(description = "工单编号")
|
||||
private String ticketNo;
|
||||
|
||||
@Schema(description = "客户ID")
|
||||
private String customerId;
|
||||
|
||||
@Schema(description = "客户姓名")
|
||||
private String customerName;
|
||||
|
||||
@Schema(description = "客户电话")
|
||||
private String customerPhone;
|
||||
|
||||
@Schema(description = "关联会话ID")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "工单类型")
|
||||
private String ticketType;
|
||||
|
||||
@Schema(description = "工单类型名称")
|
||||
private String ticketTypeName;
|
||||
|
||||
@Schema(description = "工单分类")
|
||||
private String ticketCategory;
|
||||
|
||||
@Schema(description = "优先级")
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "优先级名称")
|
||||
private String priorityName;
|
||||
|
||||
@Schema(description = "优先级颜色")
|
||||
private String priorityColor;
|
||||
|
||||
@Schema(description = "工单标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "问题描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "附件ID数组")
|
||||
private List<String> attachments;
|
||||
|
||||
@Schema(description = "附件数量")
|
||||
private Integer attachmentCount;
|
||||
|
||||
@Schema(description = "工单来源")
|
||||
private String ticketSource;
|
||||
|
||||
@Schema(description = "工单来源名称")
|
||||
private String ticketSourceName;
|
||||
|
||||
@Schema(description = "分配给(处理人ID)")
|
||||
private String assignedTo;
|
||||
|
||||
@Schema(description = "处理人姓名")
|
||||
private String assignedToName;
|
||||
|
||||
@Schema(description = "分配部门")
|
||||
private String assignedDept;
|
||||
|
||||
@Schema(description = "分配部门名称")
|
||||
private String assignedDeptName;
|
||||
|
||||
@Schema(description = "工单状态")
|
||||
private String ticketStatus;
|
||||
|
||||
@Schema(description = "工单状态名称")
|
||||
private String ticketStatusName;
|
||||
|
||||
@Schema(description = "工单状态颜色")
|
||||
private String statusColor;
|
||||
|
||||
@Schema(description = "解决方案")
|
||||
private String resolution;
|
||||
|
||||
@Schema(description = "解决时间", format = "date-time")
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date resolutionTime;
|
||||
|
||||
@Schema(description = "关闭时间", format = "date-time")
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date closeTime;
|
||||
|
||||
@Schema(description = "首次响应时间", format = "date-time")
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date responseTime;
|
||||
|
||||
@Schema(description = "SLA截止时间", format = "date-time")
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date slaDeadline;
|
||||
|
||||
@Schema(description = "是否逾期", defaultValue = "false")
|
||||
private Boolean isOverdue;
|
||||
|
||||
@Schema(description = "距离SLA截止的剩余时间(分钟)")
|
||||
private Integer slaRemainingMinutes;
|
||||
|
||||
@Schema(description = "客户评分(1-5星)")
|
||||
private Integer customerRating;
|
||||
|
||||
@Schema(description = "客户反馈")
|
||||
private String customerFeedback;
|
||||
|
||||
@Schema(description = "CRM系统工单ID")
|
||||
private String crmTicketId;
|
||||
|
||||
@Schema(description = "同步状态")
|
||||
private String syncStatus;
|
||||
|
||||
@Schema(description = "同步状态名称")
|
||||
private String syncStatusName;
|
||||
|
||||
@Schema(description = "工单标签")
|
||||
private List<String> tags;
|
||||
|
||||
@Schema(description = "工单元数据")
|
||||
private JsonNode metadata;
|
||||
|
||||
@Schema(description = "处理记录数量")
|
||||
private Integer logCount;
|
||||
|
||||
@Schema(description = "创建者姓名")
|
||||
private String creatorName;
|
||||
|
||||
@Schema(description = "更新者姓名")
|
||||
private String updaterName;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.xyzh.api.workcase.vo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.xyzh.common.vo.BaseVO;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* @description 工单过程VO
|
||||
* @filename WorkcaseProcessVO.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-31
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "工单过程VO")
|
||||
public class WorkcaseProcessVO extends BaseVO {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "工单ID")
|
||||
private String workcaseId;
|
||||
|
||||
@Schema(description = "过程ID")
|
||||
private String processId;
|
||||
|
||||
@Schema(description = "动作 info:记录,assign:指派,redeploy:转派,repeal:撤销,finish:完成")
|
||||
private String action;
|
||||
|
||||
@Schema(description = "消息")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "携带文件列表")
|
||||
private List<String> files;
|
||||
|
||||
@Schema(description = "处理人ID(指派、转派专属)")
|
||||
private String processor;
|
||||
|
||||
@Schema(description = "处理人名称")
|
||||
private String processorName;
|
||||
}
|
||||
368
urbanLifelineServ/dify/动态知识库检索.yml
Normal file
368
urbanLifelineServ/dify/动态知识库检索.yml
Normal file
@@ -0,0 +1,368 @@
|
||||
app:
|
||||
description: 根据用户指定的知识库进行检索,返回相似片段结构化数据。
|
||||
icon: 🤖
|
||||
icon_background: '#FFEAD5'
|
||||
mode: workflow
|
||||
name: 动态知识库检索
|
||||
use_icon_as_answer_icon: false
|
||||
dependencies: []
|
||||
kind: app
|
||||
version: 0.5.0
|
||||
workflow:
|
||||
conversation_variables: []
|
||||
environment_variables: []
|
||||
features:
|
||||
file_upload:
|
||||
allowed_file_extensions:
|
||||
- .JPG
|
||||
- .JPEG
|
||||
- .PNG
|
||||
- .GIF
|
||||
- .WEBP
|
||||
- .SVG
|
||||
allowed_file_types:
|
||||
- image
|
||||
allowed_file_upload_methods:
|
||||
- local_file
|
||||
- remote_url
|
||||
enabled: false
|
||||
fileUploadConfig:
|
||||
audio_file_size_limit: 50
|
||||
batch_count_limit: 5
|
||||
file_size_limit: 15
|
||||
image_file_batch_limit: 10
|
||||
image_file_size_limit: 10
|
||||
single_chunk_attachment_limit: 10
|
||||
video_file_size_limit: 100
|
||||
workflow_file_upload_limit: 10
|
||||
image:
|
||||
enabled: false
|
||||
number_limits: 3
|
||||
transfer_methods:
|
||||
- local_file
|
||||
- remote_url
|
||||
number_limits: 3
|
||||
opening_statement: ''
|
||||
retriever_resource:
|
||||
enabled: true
|
||||
sensitive_word_avoidance:
|
||||
enabled: false
|
||||
speech_to_text:
|
||||
enabled: false
|
||||
suggested_questions: []
|
||||
suggested_questions_after_answer:
|
||||
enabled: false
|
||||
text_to_speech:
|
||||
enabled: false
|
||||
language: ''
|
||||
voice: ''
|
||||
graph:
|
||||
edges:
|
||||
- data:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: code
|
||||
targetType: iteration
|
||||
id: 1747125551298-source-1747125586388-target
|
||||
selected: false
|
||||
source: '1747125551298'
|
||||
sourceHandle: source
|
||||
target: '1747125586388'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInIteration: true
|
||||
isInLoop: false
|
||||
iteration_id: '1747125586388'
|
||||
sourceType: iteration-start
|
||||
targetType: http-request
|
||||
id: 1747125586388start-source-1747125795256-target
|
||||
source: 1747125586388start
|
||||
sourceHandle: source
|
||||
target: '1747125795256'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 1002
|
||||
- data:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: iteration
|
||||
targetType: code
|
||||
id: 1747125586388-source-1747125859504-target
|
||||
source: '1747125586388'
|
||||
sourceHandle: source
|
||||
target: '1747125859504'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: code
|
||||
targetType: end
|
||||
id: 1747125859504-source-1747125871123-target
|
||||
source: '1747125859504'
|
||||
sourceHandle: source
|
||||
target: '1747125871123'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInLoop: false
|
||||
sourceType: start
|
||||
targetType: code
|
||||
id: 1747125462435-source-1747125551298-target
|
||||
source: '1747125462435'
|
||||
sourceHandle: source
|
||||
target: '1747125551298'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
nodes:
|
||||
- data:
|
||||
desc: ''
|
||||
selected: false
|
||||
title: 开始
|
||||
type: start
|
||||
variables:
|
||||
- label: 知识库集(多个Id使用英文逗号隔开)
|
||||
max_length: 256
|
||||
options: []
|
||||
required: true
|
||||
type: text-input
|
||||
variable: dataset_ids
|
||||
- label: 用户问题
|
||||
max_length: 256
|
||||
options: []
|
||||
required: true
|
||||
type: text-input
|
||||
variable: query
|
||||
- default: ''
|
||||
hint: ''
|
||||
label: dataset_apikey
|
||||
max_length: 100
|
||||
options: []
|
||||
placeholder: ''
|
||||
required: true
|
||||
type: text-input
|
||||
variable: dataset_apikey
|
||||
height: 161
|
||||
id: '1747125462435'
|
||||
position:
|
||||
x: 80
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 80
|
||||
y: 282
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
code: "\nimport json\ndef main(dataset_ids:str):\n return {\n \"\
|
||||
result\": json.loads(dataset_ids)\n }\n"
|
||||
code_language: python3
|
||||
desc: ''
|
||||
outputs:
|
||||
result:
|
||||
children: null
|
||||
type: array[string]
|
||||
selected: false
|
||||
title: 知识库集
|
||||
type: code
|
||||
variables:
|
||||
- value_selector:
|
||||
- '1747125462435'
|
||||
- dataset_ids
|
||||
value_type: string
|
||||
variable: dataset_ids
|
||||
height: 52
|
||||
id: '1747125551298'
|
||||
position:
|
||||
x: 682
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 682
|
||||
y: 282
|
||||
selected: true
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
desc: ''
|
||||
error_handle_mode: terminated
|
||||
height: 231
|
||||
is_parallel: true
|
||||
iterator_selector:
|
||||
- '1747125551298'
|
||||
- result
|
||||
output_selector:
|
||||
- '1747125795256'
|
||||
- body
|
||||
output_type: array[string]
|
||||
parallel_nums: 10
|
||||
selected: false
|
||||
start_node_id: 1747125586388start
|
||||
title: 迭代
|
||||
type: iteration
|
||||
width: 388
|
||||
height: 231
|
||||
id: '1747125586388'
|
||||
position:
|
||||
x: 988
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 988
|
||||
y: 282
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 388
|
||||
zIndex: 1
|
||||
- data:
|
||||
desc: ''
|
||||
isInIteration: true
|
||||
selected: false
|
||||
title: ''
|
||||
type: iteration-start
|
||||
draggable: false
|
||||
height: 48
|
||||
id: 1747125586388start
|
||||
parentId: '1747125586388'
|
||||
position:
|
||||
x: 24
|
||||
y: 68
|
||||
positionAbsolute:
|
||||
x: 1012
|
||||
y: 350
|
||||
selectable: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom-iteration-start
|
||||
width: 44
|
||||
zIndex: 1002
|
||||
- data:
|
||||
authorization:
|
||||
config: null
|
||||
type: no-auth
|
||||
body:
|
||||
data:
|
||||
- id: key-value-274
|
||||
key: ''
|
||||
type: text
|
||||
value: '{
|
||||
|
||||
"query": {{#1747125462435.query#}}
|
||||
|
||||
}'
|
||||
type: json
|
||||
desc: ''
|
||||
headers: 'Authorization:Bearer {{#1747125462435.dataset_apikey#}}
|
||||
|
||||
Content-Type:application/json'
|
||||
isInIteration: true
|
||||
isInLoop: false
|
||||
iteration_id: '1747125586388'
|
||||
method: post
|
||||
params: ''
|
||||
retry_config:
|
||||
max_retries: 3
|
||||
retry_enabled: false
|
||||
retry_interval: 100
|
||||
selected: false
|
||||
ssl_verify: true
|
||||
timeout:
|
||||
max_connect_timeout: 0
|
||||
max_read_timeout: 0
|
||||
max_write_timeout: 0
|
||||
title: HTTP 请求
|
||||
type: http-request
|
||||
url: http://nginx:80/v1/datasets/{{#1747125586388.item#}}/retrieve
|
||||
variables: []
|
||||
height: 111
|
||||
id: '1747125795256'
|
||||
parentId: '1747125586388'
|
||||
position:
|
||||
x: 120.421417167448
|
||||
y: 54
|
||||
positionAbsolute:
|
||||
x: 1108.421417167448
|
||||
y: 336
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
zIndex: 1002
|
||||
- data:
|
||||
code: "\nfunction main({arg1}) {\n const result = [];\n \n // 判断arg1是否为空\n\
|
||||
\ if (!arg1 || arg1.length === 0) {\n return {\n result:\
|
||||
\ JSON.stringify([]) // 返回空数组的JSON字符串\n };\n }\n \n //\
|
||||
\ 遍历arg1中的每个元素\n for (const str of arg1) {\n try {\n \
|
||||
\ // 解析JSON\n const parsed = JSON.parse(str);\n \
|
||||
\ // 检查是否有records属性且不为空数组\n if (parsed.records && Array.isArray(parsed.records)\
|
||||
\ && parsed.records.length > 0) {\n result.push(...parsed.records)\n\
|
||||
\ }\n } catch (e) {\n // 如果JSON解析失败,跳过这个元素\n\
|
||||
\ console.error('Failed to parse JSON:', e);\n }\n \
|
||||
\ }\n \n // 将结果数组转换为JSON字符串返回\n return {\n result: JSON.stringify(result)\n\
|
||||
\ }\n}\n"
|
||||
code_language: javascript
|
||||
desc: ''
|
||||
outputs:
|
||||
result:
|
||||
children: null
|
||||
type: string
|
||||
selected: false
|
||||
title: 知识库检索结果整理
|
||||
type: code
|
||||
variables:
|
||||
- value_selector:
|
||||
- '1747125586388'
|
||||
- output
|
||||
variable: arg1
|
||||
height: 52
|
||||
id: '1747125859504'
|
||||
position:
|
||||
x: 1436
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 1436
|
||||
y: 282
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
desc: ''
|
||||
outputs:
|
||||
- value_selector:
|
||||
- '1747125859504'
|
||||
- result
|
||||
value_type: string
|
||||
variable: output
|
||||
selected: false
|
||||
title: 结束
|
||||
type: end
|
||||
height: 88
|
||||
id: '1747125871123'
|
||||
position:
|
||||
x: 1739
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 1739
|
||||
y: 282
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
viewport:
|
||||
x: -33.22404773079188
|
||||
y: -134.2994836345366
|
||||
zoom: 0.8705505632961247
|
||||
rag_pipeline_variables: []
|
||||
559
urbanLifelineServ/dify/泰豪小电.yml
Normal file
559
urbanLifelineServ/dify/泰豪小电.yml
Normal file
@@ -0,0 +1,559 @@
|
||||
app:
|
||||
description: ''
|
||||
icon: 🤖
|
||||
icon_background: '#FFEAD5'
|
||||
mode: advanced-chat
|
||||
name: 泰豪小电
|
||||
use_icon_as_answer_icon: false
|
||||
dependencies:
|
||||
- current_identifier: null
|
||||
type: marketplace
|
||||
value:
|
||||
marketplace_plugin_unique_identifier: langgenius/siliconflow:0.0.38@4795747d4fca05fee9daf34b1bcc110ffbbfcd9112f5f9e914f90b8b5dd549e5
|
||||
version: null
|
||||
kind: app
|
||||
version: 0.5.0
|
||||
workflow:
|
||||
conversation_variables:
|
||||
- description: 设备代码
|
||||
id: 8a1c2cb7-376c-424a-914a-52e4155d9398
|
||||
name: device_code
|
||||
selector:
|
||||
- conversation
|
||||
- device_code
|
||||
value: ''
|
||||
value_type: string
|
||||
- description: 知识库访问apiKey
|
||||
id: 9e385516-ad32-4ab5-ac90-1fa7c5126d06
|
||||
name: dataset_apikey
|
||||
selector:
|
||||
- conversation
|
||||
- dataset_apikey
|
||||
value: ''
|
||||
value_type: string
|
||||
- description: 知识库id集合,json序列化
|
||||
id: a9aaff15-cf7a-4054-a607-8ce319005703
|
||||
name: datasets
|
||||
selector:
|
||||
- conversation
|
||||
- datasets
|
||||
value: ''
|
||||
value_type: string
|
||||
environment_variables: []
|
||||
features:
|
||||
file_upload:
|
||||
allowed_file_extensions:
|
||||
- .JPG
|
||||
- .JPEG
|
||||
- .PNG
|
||||
- .GIF
|
||||
- .WEBP
|
||||
- .SVG
|
||||
allowed_file_types:
|
||||
- image
|
||||
allowed_file_upload_methods:
|
||||
- local_file
|
||||
- remote_url
|
||||
enabled: false
|
||||
fileUploadConfig:
|
||||
audio_file_size_limit: 50
|
||||
batch_count_limit: 5
|
||||
file_size_limit: 500
|
||||
image_file_batch_limit: 10
|
||||
image_file_size_limit: 10
|
||||
single_chunk_attachment_limit: 10
|
||||
video_file_size_limit: 100
|
||||
workflow_file_upload_limit: 10
|
||||
image:
|
||||
enabled: false
|
||||
number_limits: 3
|
||||
transfer_methods:
|
||||
- local_file
|
||||
- remote_url
|
||||
number_limits: 3
|
||||
opening_statement: ''
|
||||
retriever_resource:
|
||||
enabled: true
|
||||
sensitive_word_avoidance:
|
||||
enabled: false
|
||||
speech_to_text:
|
||||
enabled: false
|
||||
suggested_questions: []
|
||||
suggested_questions_after_answer:
|
||||
enabled: false
|
||||
text_to_speech:
|
||||
enabled: false
|
||||
language: ''
|
||||
voice: ''
|
||||
graph:
|
||||
edges:
|
||||
- data:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: start
|
||||
targetType: if-else
|
||||
id: 1766469212796-source-1766998282613-target
|
||||
selected: false
|
||||
source: '1766469212796'
|
||||
sourceHandle: source
|
||||
target: '1766998282613'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: start
|
||||
targetType: if-else
|
||||
id: 1766469212796-source-1766998308433-target
|
||||
selected: false
|
||||
source: '1766469212796'
|
||||
sourceHandle: source
|
||||
target: '1766998308433'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: if-else
|
||||
targetType: assigner
|
||||
id: 1766998282613-true-1766998338007-target
|
||||
selected: false
|
||||
source: '1766998282613'
|
||||
sourceHandle: 'true'
|
||||
target: '1766998338007'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: if-else
|
||||
targetType: assigner
|
||||
id: 1766998308433-true-1766998352957-target
|
||||
selected: false
|
||||
source: '1766998308433'
|
||||
sourceHandle: 'true'
|
||||
target: '1766998352957'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: assigner
|
||||
targetType: tool
|
||||
id: 1766998338007-source-1766998463133-target
|
||||
selected: false
|
||||
source: '1766998338007'
|
||||
sourceHandle: source
|
||||
target: '1766998463133'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInLoop: false
|
||||
sourceType: assigner
|
||||
targetType: tool
|
||||
id: 1766998352957-source-1766998463133-target
|
||||
selected: false
|
||||
source: '1766998352957'
|
||||
sourceHandle: source
|
||||
target: '1766998463133'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInLoop: false
|
||||
sourceType: if-else
|
||||
targetType: tool
|
||||
id: 1766998282613-false-1766998463133-target
|
||||
selected: false
|
||||
source: '1766998282613'
|
||||
sourceHandle: 'false'
|
||||
target: '1766998463133'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInLoop: false
|
||||
sourceType: if-else
|
||||
targetType: tool
|
||||
id: 1766998308433-false-1766998463133-target
|
||||
selected: false
|
||||
source: '1766998308433'
|
||||
sourceHandle: 'false'
|
||||
target: '1766998463133'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInLoop: false
|
||||
sourceType: tool
|
||||
targetType: llm
|
||||
id: 1766998463133-source-1766998751026-target
|
||||
source: '1766998463133'
|
||||
sourceHandle: source
|
||||
target: '1766998751026'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInLoop: false
|
||||
sourceType: llm
|
||||
targetType: answer
|
||||
id: 1766998751026-source-answer-target
|
||||
source: '1766998751026'
|
||||
sourceHandle: source
|
||||
target: answer
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
nodes:
|
||||
- data:
|
||||
selected: false
|
||||
title: 用户输入
|
||||
type: start
|
||||
variables:
|
||||
- default: ''
|
||||
hint: ''
|
||||
label: 知识库id集合,json序列化
|
||||
max_length: 4096
|
||||
options: []
|
||||
placeholder: ''
|
||||
required: true
|
||||
type: paragraph
|
||||
variable: datasets
|
||||
- default: ''
|
||||
hint: ''
|
||||
label: 知识库访问apiKey
|
||||
max_length: 100
|
||||
options: []
|
||||
placeholder: ''
|
||||
required: true
|
||||
type: text-input
|
||||
variable: dataset_apikey
|
||||
height: 135
|
||||
id: '1766469212796'
|
||||
position:
|
||||
x: 0
|
||||
y: 33
|
||||
positionAbsolute:
|
||||
x: 0
|
||||
y: 33
|
||||
selected: true
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
answer: '{{#1766998751026.text#}}'
|
||||
selected: false
|
||||
title: 直接回复
|
||||
type: answer
|
||||
variables: []
|
||||
height: 103
|
||||
id: answer
|
||||
position:
|
||||
x: 1770
|
||||
y: 87
|
||||
positionAbsolute:
|
||||
x: 1770
|
||||
y: 87
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
cases:
|
||||
- case_id: 'true'
|
||||
conditions:
|
||||
- comparison_operator: empty
|
||||
id: 8edef3b0-823d-481c-a4eb-254a0bd42535
|
||||
value: ''
|
||||
varType: string
|
||||
variable_selector:
|
||||
- conversation
|
||||
- datasets
|
||||
id: 'true'
|
||||
logical_operator: and
|
||||
selected: false
|
||||
title: datasets初始化
|
||||
type: if-else
|
||||
height: 124
|
||||
id: '1766998282613'
|
||||
position:
|
||||
x: 362
|
||||
y: 16
|
||||
positionAbsolute:
|
||||
x: 362
|
||||
y: 16
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
cases:
|
||||
- case_id: 'true'
|
||||
conditions:
|
||||
- comparison_operator: empty
|
||||
id: 2976abfb-0b2d-43ec-bb02-f547892e39de
|
||||
value: ''
|
||||
varType: string
|
||||
variable_selector:
|
||||
- conversation
|
||||
- dataset_apikey
|
||||
id: 'true'
|
||||
logical_operator: and
|
||||
selected: false
|
||||
title: 条件分支 2
|
||||
type: if-else
|
||||
height: 124
|
||||
id: '1766998308433'
|
||||
position:
|
||||
x: 362
|
||||
y: 220
|
||||
positionAbsolute:
|
||||
x: 362
|
||||
y: 220
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
items:
|
||||
- input_type: variable
|
||||
operation: over-write
|
||||
value:
|
||||
- '1766469212796'
|
||||
- datasets
|
||||
variable_selector:
|
||||
- conversation
|
||||
- datasets
|
||||
write_mode: over-write
|
||||
selected: false
|
||||
title: 变量赋值
|
||||
type: assigner
|
||||
version: '2'
|
||||
height: 84
|
||||
id: '1766998338007'
|
||||
position:
|
||||
x: 724
|
||||
y: 0
|
||||
positionAbsolute:
|
||||
x: 724
|
||||
y: 0
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
items:
|
||||
- input_type: variable
|
||||
operation: over-write
|
||||
value:
|
||||
- '1766469212796'
|
||||
- dataset_apikey
|
||||
variable_selector:
|
||||
- conversation
|
||||
- dataset_apikey
|
||||
write_mode: over-write
|
||||
selected: false
|
||||
title: 变量赋值 2
|
||||
type: assigner
|
||||
version: '2'
|
||||
height: 84
|
||||
id: '1766998352957'
|
||||
position:
|
||||
x: 724
|
||||
y: 185
|
||||
positionAbsolute:
|
||||
x: 724
|
||||
y: 185
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
is_team_authorization: true
|
||||
paramSchemas:
|
||||
- auto_generate: null
|
||||
default: null
|
||||
form: llm
|
||||
human_description:
|
||||
en_US: ''
|
||||
ja_JP: ''
|
||||
pt_BR: ''
|
||||
zh_Hans: ''
|
||||
label:
|
||||
en_US: 知识库集(多个Id使用英文逗号隔开)
|
||||
ja_JP: 知识库集(多个Id使用英文逗号隔开)
|
||||
pt_BR: 知识库集(多个Id使用英文逗号隔开)
|
||||
zh_Hans: 知识库集(多个Id使用英文逗号隔开)
|
||||
llm_description: ''
|
||||
max: null
|
||||
min: null
|
||||
name: dataset_ids
|
||||
options: []
|
||||
placeholder:
|
||||
en_US: ''
|
||||
ja_JP: ''
|
||||
pt_BR: ''
|
||||
zh_Hans: ''
|
||||
precision: null
|
||||
required: true
|
||||
scope: null
|
||||
template: null
|
||||
type: string
|
||||
- auto_generate: null
|
||||
default: null
|
||||
form: llm
|
||||
human_description:
|
||||
en_US: ''
|
||||
ja_JP: ''
|
||||
pt_BR: ''
|
||||
zh_Hans: ''
|
||||
label:
|
||||
en_US: 用户问题
|
||||
ja_JP: 用户问题
|
||||
pt_BR: 用户问题
|
||||
zh_Hans: 用户问题
|
||||
llm_description: ''
|
||||
max: null
|
||||
min: null
|
||||
name: query
|
||||
options: []
|
||||
placeholder:
|
||||
en_US: ''
|
||||
ja_JP: ''
|
||||
pt_BR: ''
|
||||
zh_Hans: ''
|
||||
precision: null
|
||||
required: true
|
||||
scope: null
|
||||
template: null
|
||||
type: string
|
||||
- auto_generate: null
|
||||
default: ''
|
||||
form: llm
|
||||
human_description:
|
||||
en_US: ''
|
||||
ja_JP: ''
|
||||
pt_BR: ''
|
||||
zh_Hans: ''
|
||||
label:
|
||||
en_US: dataset_apikey
|
||||
ja_JP: dataset_apikey
|
||||
pt_BR: dataset_apikey
|
||||
zh_Hans: dataset_apikey
|
||||
llm_description: ''
|
||||
max: null
|
||||
min: null
|
||||
name: dataset_apikey
|
||||
options: []
|
||||
placeholder:
|
||||
en_US: ''
|
||||
ja_JP: ''
|
||||
pt_BR: ''
|
||||
zh_Hans: ''
|
||||
precision: null
|
||||
required: true
|
||||
scope: null
|
||||
template: null
|
||||
type: string
|
||||
params:
|
||||
dataset_apikey: ''
|
||||
dataset_ids: ''
|
||||
query: ''
|
||||
plugin_id: null
|
||||
plugin_unique_identifier: null
|
||||
provider_icon:
|
||||
background: '#FFEAD5'
|
||||
content: 🤖
|
||||
provider_id: 5d6141ab-e1ad-401f-a8b6-da6ce4dd682f
|
||||
provider_name: 动态知识库检索
|
||||
provider_type: workflow
|
||||
selected: false
|
||||
title: 动态知识库检索
|
||||
tool_configurations: {}
|
||||
tool_description: 根据用户指定的知识库进行检索,返回相似片段结构化数据。
|
||||
tool_label: 动态知识库检索
|
||||
tool_name: dynamic_dataset
|
||||
tool_node_version: '2'
|
||||
tool_parameters:
|
||||
dataset_apikey:
|
||||
type: mixed
|
||||
value: '{{#conversation.dataset_apikey#}}'
|
||||
dataset_ids:
|
||||
type: mixed
|
||||
value: '{{#conversation.datasets#}}'
|
||||
query:
|
||||
type: mixed
|
||||
value: '{{#sys.query#}}'
|
||||
type: tool
|
||||
height: 52
|
||||
id: '1766998463133'
|
||||
position:
|
||||
x: 1086
|
||||
y: 113
|
||||
positionAbsolute:
|
||||
x: 1086
|
||||
y: 113
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
context:
|
||||
enabled: true
|
||||
variable_selector:
|
||||
- '1766998463133'
|
||||
- text
|
||||
model:
|
||||
completion_params:
|
||||
temperature: 0.7
|
||||
mode: chat
|
||||
name: Pro/moonshotai/Kimi-K2-Instruct
|
||||
provider: langgenius/siliconflow/siliconflow
|
||||
prompt_config:
|
||||
jinja2_variables:
|
||||
- value_selector:
|
||||
- '1766998463133'
|
||||
- text
|
||||
variable: text
|
||||
prompt_template:
|
||||
- edition_type: jinja2
|
||||
id: 1ee839fc-fff4-41b5-bbe3-b778b428cef4
|
||||
jinja2_text: 根据知识库内容{{ text }}回答用户问题
|
||||
role: system
|
||||
text: ''
|
||||
selected: false
|
||||
title: LLM
|
||||
type: llm
|
||||
vision:
|
||||
enabled: false
|
||||
height: 88
|
||||
id: '1766998751026'
|
||||
position:
|
||||
x: 1428
|
||||
y: 95
|
||||
positionAbsolute:
|
||||
x: 1428
|
||||
y: 95
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
viewport:
|
||||
x: -671.7
|
||||
y: 334.55
|
||||
zoom: 0.7
|
||||
rag_pipeline_variables: []
|
||||
@@ -29,7 +29,6 @@ security:
|
||||
spring:
|
||||
application:
|
||||
name: file-service
|
||||
|
||||
# ================== Spring Cloud Nacos ==================
|
||||
cloud:
|
||||
nacos:
|
||||
@@ -57,8 +56,8 @@ spring:
|
||||
servlet:
|
||||
multipart:
|
||||
enabled: true
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
max-file-size: 500MB
|
||||
max-request-size: 500MB
|
||||
|
||||
# ================== SpringDoc ==================
|
||||
springdoc:
|
||||
@@ -79,6 +78,7 @@ dubbo:
|
||||
name: urban-lifeline-file
|
||||
qos-enable: false
|
||||
protocol:
|
||||
payload: 110100480
|
||||
name: dubbo
|
||||
port: -1
|
||||
registry:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.xyzh.workcase.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -40,6 +41,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@@ -58,7 +60,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/workcase/chat")
|
||||
public class WorkcaseChatContorller {
|
||||
public class WorkcaseChatController {
|
||||
|
||||
@Autowired
|
||||
private WorkcaseChatService workcaseChatService;
|
||||
@@ -76,7 +78,8 @@ public class WorkcaseChatContorller {
|
||||
@PostMapping("/room")
|
||||
public ResultDomain<TbChatRoomDTO> createChatRoom(@RequestBody TbChatRoomDTO chatRoom) {
|
||||
ValidationResult vr = ValidationUtils.validate(chatRoom, Arrays.asList(
|
||||
ValidationUtils.requiredString("guestId", "来客ID")
|
||||
ValidationUtils.requiredString("guestId", "来客ID"),
|
||||
ValidationUtils.requiredString("deviceCode", "设备代码")
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
@@ -109,6 +112,31 @@ public class WorkcaseChatContorller {
|
||||
return chatRoomService.closeChatRoom(roomId, closedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交聊天室服务评分
|
||||
* @param roomId 聊天室ID
|
||||
* @param commentLevel 评分(1-5星)
|
||||
* @return 提交结果
|
||||
*/
|
||||
@Operation(summary = "提交聊天室服务评分")
|
||||
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||
@PostMapping("/room/{roomId}/comment")
|
||||
public ResultDomain<Boolean> submitComment(
|
||||
@PathVariable("roomId") String roomId,
|
||||
@RequestBody Map<String, Integer> body) {
|
||||
|
||||
Integer commentLevel = body.get("commentLevel");
|
||||
if (commentLevel == null || commentLevel < 1 || commentLevel > 5) {
|
||||
return ResultDomain.failure("评分必须在1-5之间");
|
||||
}
|
||||
|
||||
// 获取当前登录用户ID
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
|
||||
// 调用服务层提交评分
|
||||
return chatRoomService.submitCommentLevel(roomId, commentLevel, userId);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取聊天室详情")
|
||||
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||
@GetMapping("/room/{roomId}")
|
||||
@@ -14,6 +14,7 @@ import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO;
|
||||
import org.xyzh.api.workcase.service.WorkcaseService;
|
||||
import org.xyzh.api.workcase.vo.WorkcaseProcessVO;
|
||||
import org.xyzh.common.auth.utils.LoginUtil;
|
||||
import org.xyzh.common.core.domain.LoginDomain;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
@@ -49,7 +50,8 @@ public class WorkcaseController {
|
||||
@PostMapping
|
||||
public ResultDomain<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
|
||||
ValidationResult vr = ValidationUtils.validate(workcase, Arrays.asList(
|
||||
ValidationUtils.requiredString("deviceNamePlateImg", "设备名称牌图片"),
|
||||
ValidationUtils.requiredString("deviceNamePlate", "设备名称牌图片"),
|
||||
ValidationUtils.requiredString("deviceCode", "设备代码"),
|
||||
ValidationUtils.requiredString("type", "问题类型"),
|
||||
ValidationUtils.requiredString("userId", "用户ID"),
|
||||
ValidationUtils.requiredString("username", "用户名称")
|
||||
@@ -176,14 +178,14 @@ public class WorkcaseController {
|
||||
@Operation(summary = "查询工单处理过程列表")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:process')")
|
||||
@PostMapping("/process/list")
|
||||
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessList(@RequestBody TbWorkcaseProcessDTO filter) {
|
||||
public ResultDomain<WorkcaseProcessVO> getWorkcaseProcessList(@RequestBody TbWorkcaseProcessDTO filter) {
|
||||
return workcaseService.getWorkcaseProcessList(filter);
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询工单处理过程")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:process')")
|
||||
@PostMapping("/process/page")
|
||||
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessPage(@RequestBody PageRequest<TbWorkcaseProcessDTO> pageRequest) {
|
||||
public ResultDomain<WorkcaseProcessVO> getWorkcaseProcessPage(@RequestBody PageRequest<TbWorkcaseProcessDTO> pageRequest) {
|
||||
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
|
||||
ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null),
|
||||
ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100)
|
||||
|
||||
@@ -67,6 +67,21 @@ public class ChatMessageListener implements MessageListener {
|
||||
// 转发到聊天室列表订阅者,前端刷新列表状态
|
||||
messagingTemplate.convertAndSend("/topic/chat/list-update", chatMessage);
|
||||
logger.debug("列表更新已转发到STOMP: /topic/chat/list-update");
|
||||
|
||||
// 同时转发到对应聊天室频道,确保聊天窗口也能收到消息
|
||||
String roomId = chatMessage.getRoomId();
|
||||
if (roomId != null && !roomId.isEmpty()) {
|
||||
// 查询完整的VO数据
|
||||
ChatRoomMessageVO messageVO = chatMessageMapper.selectChatMessageVOById(chatMessage.getMessageId());
|
||||
if (messageVO != null) {
|
||||
messagingTemplate.convertAndSend("/topic/chat/" + roomId, messageVO);
|
||||
logger.debug("列表更新消息同时转发到聊天室: /topic/chat/{}", roomId);
|
||||
} else {
|
||||
// 如果查不到VO(可能事务未提交),直接用DTO转发
|
||||
logger.warn("未找到消息VO,使用DTO转发: messageId={}", chatMessage.getMessageId());
|
||||
messagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("处理Redis消息失败", e);
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO;
|
||||
import org.xyzh.api.workcase.vo.WorkcaseProcessVO;
|
||||
import org.xyzh.common.core.page.PageParam;
|
||||
|
||||
/**
|
||||
@@ -40,12 +41,12 @@ public interface TbWorkcaseProcessMapper {
|
||||
/**
|
||||
* 查询工单过程列表
|
||||
*/
|
||||
List<TbWorkcaseProcessDTO> selectWorkcaseProcessList(@Param("filter") TbWorkcaseProcessDTO filter);
|
||||
List<WorkcaseProcessVO> selectWorkcaseProcessList(@Param("filter") TbWorkcaseProcessDTO filter);
|
||||
|
||||
/**
|
||||
* 分页查询工单过程
|
||||
*/
|
||||
List<TbWorkcaseProcessDTO> selectWorkcaseProcessPage(@Param("filter") TbWorkcaseProcessDTO filter, @Param("pageParam") PageParam pageParam);
|
||||
List<WorkcaseProcessVO> selectWorkcaseProcessPage(@Param("filter") TbWorkcaseProcessDTO filter, @Param("pageParam") PageParam pageParam);
|
||||
|
||||
/**
|
||||
* 统计工单过程数量
|
||||
|
||||
@@ -122,7 +122,11 @@ public class ChatRoomServiceImpl implements ChatRoomService {
|
||||
}
|
||||
// 从AI同步对话历史
|
||||
if(NonUtils.isNotEmpty(chatRoom.getAiSessionId())){
|
||||
try{
|
||||
syncAiChatMessages(chatRoom);
|
||||
}catch(Exception ex){
|
||||
return ResultDomain.failure("创建失败");
|
||||
}
|
||||
}
|
||||
|
||||
return ResultDomain.success("创建成功", chatRoom);
|
||||
@@ -680,7 +684,8 @@ public class ChatRoomServiceImpl implements ChatRoomService {
|
||||
roomMsg.setMessageType("text");
|
||||
roomMsg.setStatus("sent");
|
||||
roomMsg.setFiles(aiMsg.getFiles());
|
||||
roomMsg.setSendTime(new Date(baseTime + i * 1000L));
|
||||
roomMsg.setSendTime(aiMsg.getCreateTime());
|
||||
roomMsg.setCreateTime(aiMsg.getCreateTime());
|
||||
roomMsg.setIsAiMessage(true);
|
||||
roomMsg.setAiMessageId(aiMsg.getMessageId());
|
||||
roomMsg.setCreator(chatRoom.getGuestId());
|
||||
@@ -764,4 +769,60 @@ public class ChatRoomServiceImpl implements ChatRoomService {
|
||||
logger.error("发布列表更新到Redis失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= 聊天室评分管理 ==========================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultDomain<Boolean> submitCommentLevel(String roomId, Integer commentLevel, String userId) {
|
||||
logger.info("提交聊天室服务评分: roomId={}, commentLevel={}, userId={}", roomId, commentLevel, userId);
|
||||
|
||||
// 参数校验
|
||||
if (NonUtils.isEmpty(roomId)) {
|
||||
return ResultDomain.failure("聊天室ID不能为空");
|
||||
}
|
||||
if (commentLevel == null || commentLevel < 1 || commentLevel > 5) {
|
||||
return ResultDomain.failure("评分必须在1-5星之间");
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 检查聊天室是否存在
|
||||
TbChatRoomDTO chatRoom = chatRoomMapper.selectChatRoomById(roomId);
|
||||
if (chatRoom == null) {
|
||||
return ResultDomain.failure("聊天室不存在");
|
||||
}
|
||||
|
||||
// 2. 检查用户是否是聊天室成员(来客)
|
||||
if (!userId.equals(chatRoom.getGuestId())) {
|
||||
return ResultDomain.failure("只有来客可以对服务进行评分");
|
||||
}
|
||||
|
||||
// 3. 检查是否已评分
|
||||
if (chatRoom.getCommentLevel() != null && chatRoom.getCommentLevel() > 0) {
|
||||
return ResultDomain.failure("已经评分过了,不能重复评分");
|
||||
}
|
||||
|
||||
// 4. 更新评分
|
||||
TbChatRoomDTO updateRoom = new TbChatRoomDTO();
|
||||
updateRoom.setRoomId(roomId);
|
||||
updateRoom.setCommentLevel(commentLevel);
|
||||
|
||||
int rows = chatRoomMapper.updateChatRoom(updateRoom);
|
||||
if (rows > 0) {
|
||||
logger.info("聊天室服务评分成功: roomId={}, commentLevel={}", roomId, commentLevel);
|
||||
|
||||
// TODO: 后续可以在这里更新客服人员的平均满意度评分
|
||||
// updateCustomerServiceSatisfaction(chatRoom, commentLevel);
|
||||
|
||||
return ResultDomain.success("评分成功",true);
|
||||
} else {
|
||||
return ResultDomain.failure("评分提交失败");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("提交聊天室服务评分失败: roomId={}, commentLevel={}", roomId, commentLevel, e);
|
||||
return ResultDomain.failure("评分提交失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO;
|
||||
import org.xyzh.api.workcase.service.WorkcaseService;
|
||||
import org.xyzh.api.workcase.vo.WorkcaseProcessVO;
|
||||
import org.xyzh.common.auth.utils.LoginUtil;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageDomain;
|
||||
@@ -370,10 +371,38 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
workcaseMapper.updateWorkcase(workcase);
|
||||
}
|
||||
} else if (WorkcaseProcessAction.FINISH.getName().equals(action)) {
|
||||
// 1. 更新工单状态为已完成
|
||||
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
|
||||
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
|
||||
workcase.setStatus("done");
|
||||
workcaseMapper.updateWorkcase(workcase);
|
||||
|
||||
// 2. 发送系统评分消息到聊天室
|
||||
try {
|
||||
TbWorkcaseDTO workcaseData = workcaseMapper.selectWorkcaseById(workcaseProcess.getWorkcaseId());
|
||||
if (workcaseData != null && workcaseData.getRoomId() != null) {
|
||||
// 创建系统评分消息
|
||||
org.xyzh.api.workcase.dto.TbChatRoomMessageDTO commentMessage = new org.xyzh.api.workcase.dto.TbChatRoomMessageDTO();
|
||||
commentMessage.setMessageId(IdUtil.generateUUID());
|
||||
commentMessage.setOptsn(IdUtil.getOptsn());
|
||||
commentMessage.setRoomId(workcaseData.getRoomId());
|
||||
commentMessage.setSenderId("system");
|
||||
commentMessage.setSenderType("system"); // 系统消息
|
||||
commentMessage.setSenderName("系统");
|
||||
commentMessage.setMessageType("comment"); // 评分消息
|
||||
commentMessage.setContent("请为本次服务评分");
|
||||
commentMessage.setStatus("sent");
|
||||
commentMessage.setCreator("system");
|
||||
|
||||
// 发送消息到聊天室
|
||||
chatRoomService.sendMessage(commentMessage);
|
||||
logger.info("工单完成,已发送系统评分消息: workcaseId={}, roomId={}",
|
||||
workcaseProcess.getWorkcaseId(), workcaseData.getRoomId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("发送系统评分消息失败: workcaseId={}", workcaseProcess.getWorkcaseId(), e);
|
||||
// 不影响工单完成流程,只记录错误日志
|
||||
}
|
||||
} else if (WorkcaseProcessAction.REPEAL.getName().equals(action)) {
|
||||
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
|
||||
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
|
||||
@@ -417,21 +446,21 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessList(TbWorkcaseProcessDTO filter) {
|
||||
List<TbWorkcaseProcessDTO> list = workcaseProcessMapper.selectWorkcaseProcessList(filter);
|
||||
public ResultDomain<WorkcaseProcessVO> getWorkcaseProcessList(TbWorkcaseProcessDTO filter) {
|
||||
List<WorkcaseProcessVO> list = workcaseProcessMapper.selectWorkcaseProcessList(filter);
|
||||
return ResultDomain.success("查询成功", list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessPage(PageRequest<TbWorkcaseProcessDTO> pageRequest) {
|
||||
public ResultDomain<WorkcaseProcessVO> getWorkcaseProcessPage(PageRequest<TbWorkcaseProcessDTO> pageRequest) {
|
||||
TbWorkcaseProcessDTO filter = pageRequest.getFilter();
|
||||
PageParam pageParam = pageRequest.getPageParam();
|
||||
|
||||
List<TbWorkcaseProcessDTO> list = workcaseProcessMapper.selectWorkcaseProcessPage(filter, pageParam);
|
||||
List<WorkcaseProcessVO> list = workcaseProcessMapper.selectWorkcaseProcessPage(filter, pageParam);
|
||||
long total = workcaseProcessMapper.countWorkcaseProcesses(filter);
|
||||
|
||||
pageParam.setTotal((int) total);
|
||||
PageDomain<TbWorkcaseProcessDTO> pageDomain = new PageDomain<>(pageParam, list);
|
||||
PageDomain<WorkcaseProcessVO> pageDomain = new PageDomain<>(pageParam, list);
|
||||
return ResultDomain.success("查询成功", pageDomain);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,12 @@ security:
|
||||
spring:
|
||||
application:
|
||||
name: workcase-service
|
||||
|
||||
# 文件上传配置
|
||||
servlet:
|
||||
multipart:
|
||||
enabled: true
|
||||
max-file-size: 500MB
|
||||
max-request-size: 500MB
|
||||
# ================== Spring Cloud Nacos ==================
|
||||
cloud:
|
||||
nacos:
|
||||
@@ -75,6 +80,7 @@ dubbo:
|
||||
name: urban-lifeline-workcase
|
||||
qos-enable: false
|
||||
protocol:
|
||||
payload: 110100480
|
||||
name: dubbo
|
||||
port: -1
|
||||
registry:
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
<insert id="insertChatMessage" parameterType="org.xyzh.api.workcase.dto.TbChatRoomMessageDTO">
|
||||
INSERT INTO workcase.tb_chat_room_message (
|
||||
optsn, message_id, room_id, sender_id, sender_type, sender_name, content, creator
|
||||
optsn, message_id, room_id, sender_id, sender_type, sender_name, content, creator,send_time
|
||||
<if test="messageType != null">, message_type</if>
|
||||
<if test="files != null">, files</if>
|
||||
<if test="contentExtra != null">, content_extra</if>
|
||||
@@ -62,8 +62,9 @@
|
||||
<if test="isAiMessage != null">, is_ai_message</if>
|
||||
<if test="aiMessageId != null">, ai_message_id</if>
|
||||
<if test="status != null">, status</if>
|
||||
<if test="createTime != null">, create_time</if>
|
||||
) VALUES (
|
||||
#{optsn}, #{messageId}, #{roomId}, #{senderId}, #{senderType}, #{senderName}, #{content}, #{creator}
|
||||
#{optsn}, #{messageId}, #{roomId}, #{senderId}, #{senderType}, #{senderName}, #{content}, #{creator},#{sendTime}
|
||||
<if test="messageType != null">, #{messageType}</if>
|
||||
<if test="files != null">, #{files, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}</if>
|
||||
<if test="contentExtra != null">, #{contentExtra, typeHandler=org.xyzh.common.jdbc.handler.FastJson2TypeHandler}::jsonb</if>
|
||||
@@ -71,6 +72,7 @@
|
||||
<if test="isAiMessage != null">, #{isAiMessage}</if>
|
||||
<if test="aiMessageId != null">, #{aiMessageId}</if>
|
||||
<if test="status != null">, #{status}</if>
|
||||
<if test="createTime != null">, #{createTime}</if>
|
||||
)
|
||||
</insert>
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
<result column="guest_name" property="guestName" jdbcType="VARCHAR"/>
|
||||
<result column="ai_session_id" property="aiSessionId" jdbcType="VARCHAR"/>
|
||||
<result column="message_count" property="messageCount" jdbcType="INTEGER"/>
|
||||
<result column="device_code" property="deviceCode" jdbcType="VARCHAR"/>
|
||||
<result column="last_message_time" property="lastMessageTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="last_message" property="lastMessage" jdbcType="VARCHAR"/>
|
||||
<result column="comment_level" property="commentLevel" jdbcType="INTEGER"/>
|
||||
<result column="closed_by" property="closedBy" jdbcType="VARCHAR"/>
|
||||
<result column="closed_time" property="closedTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="creator" property="creator" jdbcType="VARCHAR"/>
|
||||
@@ -35,9 +37,11 @@
|
||||
<result column="guest_name" property="guestName" jdbcType="VARCHAR"/>
|
||||
<result column="ai_session_id" property="aiSessionId" jdbcType="VARCHAR"/>
|
||||
<result column="message_count" property="messageCount" jdbcType="INTEGER"/>
|
||||
<result column="device_code" property="deviceCode" jdbcType="VARCHAR"/>
|
||||
<result column="unread_count" property="unreadCount" jdbcType="INTEGER"/>
|
||||
<result column="last_message_time" property="lastMessageTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="last_message" property="lastMessage" jdbcType="VARCHAR"/>
|
||||
<result column="comment_level" property="commentLevel" jdbcType="INTEGER"/>
|
||||
<result column="closed_by" property="closedBy" jdbcType="VARCHAR"/>
|
||||
<result column="closed_time" property="closedTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="creator" property="creator" jdbcType="VARCHAR"/>
|
||||
@@ -49,13 +53,13 @@
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
room_id, optsn, workcase_id, room_name, room_type, status, guest_id, guest_name,
|
||||
ai_session_id, message_count, last_message_time, last_message, closed_by, closed_time,
|
||||
ai_session_id, message_count, device_code, last_message_time, last_message, comment_level, closed_by, closed_time,
|
||||
creator, create_time, update_time, delete_time, deleted
|
||||
</sql>
|
||||
|
||||
<insert id="insertChatRoom" parameterType="org.xyzh.api.workcase.dto.TbChatRoomDTO">
|
||||
INSERT INTO workcase.tb_chat_room (
|
||||
optsn, room_id, workcase_id, room_name, guest_id, guest_name, creator
|
||||
optsn, room_id, workcase_id, room_name, guest_id, guest_name, device_code, creator
|
||||
<if test="roomType != null">, room_type</if>
|
||||
<if test="status != null">, status</if>
|
||||
<if test="aiSessionId != null">, ai_session_id</if>
|
||||
@@ -63,7 +67,7 @@
|
||||
<if test="lastMessageTime != null">, last_message_time</if>
|
||||
<if test="lastMessage != null">, last_message</if>
|
||||
) VALUES (
|
||||
#{optsn}, #{roomId}, #{workcaseId}, #{roomName}, #{guestId}, #{guestName}, #{creator}
|
||||
#{optsn}, #{roomId}, #{workcaseId}, #{roomName}, #{guestId}, #{guestName}, #{deviceCode}, #{creator}
|
||||
<if test="roomType != null">, #{roomType}</if>
|
||||
<if test="status != null">, #{status}</if>
|
||||
<if test="aiSessionId != null">, #{aiSessionId}</if>
|
||||
@@ -82,8 +86,10 @@
|
||||
<if test="status != null and status != ''">status = #{status},</if>
|
||||
<if test="aiSessionId != null">ai_session_id = #{aiSessionId},</if>
|
||||
<if test="messageCount != null">message_count = #{messageCount},</if>
|
||||
<if test="deviceCode != null and deviceCode != ''">device_code = #{deviceCode},</if>
|
||||
<if test="lastMessageTime != null">last_message_time = #{lastMessageTime},</if>
|
||||
<if test="lastMessage != null">last_message = #{lastMessage},</if>
|
||||
<if test="commentLevel != null">comment_level = #{commentLevel},</if>
|
||||
<if test="closedBy != null">closed_by = #{closedBy},</if>
|
||||
<if test="closedTime != null">closed_time = #{closedTime},</if>
|
||||
update_time = now()
|
||||
@@ -121,8 +127,8 @@
|
||||
|
||||
<select id="selectChatRoomPage" resultMap="VOResultMap">
|
||||
SELECT r.room_id, r.optsn, r.workcase_id, r.room_name, r.room_type, r.status,
|
||||
r.guest_id, r.guest_name, r.ai_session_id, r.message_count,
|
||||
r.last_message_time, r.last_message, r.closed_by, r.closed_time,
|
||||
r.guest_id, r.guest_name, r.ai_session_id, r.message_count, r.device_code,
|
||||
r.last_message_time, r.last_message, r.comment_level, r.closed_by, r.closed_time,
|
||||
r.creator, r.create_time, r.update_time, r.delete_time, r.deleted,
|
||||
COALESCE(m.unread_count, 0) as unread_count
|
||||
FROM workcase.tb_chat_room r
|
||||
|
||||
@@ -34,10 +34,9 @@
|
||||
|
||||
<insert id="insertWorkcase" parameterType="org.xyzh.api.workcase.dto.TbWorkcaseDTO">
|
||||
INSERT INTO workcase.tb_workcase (
|
||||
optsn, workcase_id, room_id, user_id, username, phone, type, device_name_plate_img, creator
|
||||
optsn, workcase_id, room_id, user_id, username, phone, type, device_code,device_name_plate, creator
|
||||
<if test="device != null">, device</if>
|
||||
<if test="deviceCode != null">, device_code</if>
|
||||
<if test="deviceNamePlate != null">, device_name_plate</if>
|
||||
<if test="deviceNamePlateImg != null">, device_name_plate_img</if>
|
||||
<if test="address != null">, address</if>
|
||||
<if test="description != null">, description</if>
|
||||
<if test="imgs != null">, imgs</if>
|
||||
@@ -45,10 +44,9 @@
|
||||
<if test="status != null">, status</if>
|
||||
<if test="processor != null">, processor</if>
|
||||
) VALUES (
|
||||
#{optsn}, #{workcaseId}, #{roomId}, #{userId}, #{username}, #{phone}, #{type}, #{deviceNamePlateImg}, #{creator}
|
||||
#{optsn}, #{workcaseId}, #{roomId}, #{userId}, #{username}, #{phone}, #{type}, #{deviceCode}, #{deviceNamePlate}, #{creator}
|
||||
<if test="device != null">, #{device}</if>
|
||||
<if test="deviceCode != null">, #{deviceCode}</if>
|
||||
<if test="deviceNamePlate != null">, #{deviceNamePlate}</if>
|
||||
<if test="deviceNamePlateImg != null">, #{deviceNamePlateImg}</if>
|
||||
<if test="address != null">, #{address}</if>
|
||||
<if test="description != null">, #{description}</if>
|
||||
<if test="imgs != null">, #{imgs, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}</if>
|
||||
|
||||
@@ -15,6 +15,21 @@
|
||||
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="VOResultMap" type="org.xyzh.api.workcase.vo.WorkcaseProcessVO">
|
||||
<id column="process_id" property="processId" jdbcType="VARCHAR"/>
|
||||
<result column="optsn" property="optsn" jdbcType="VARCHAR"/>
|
||||
<result column="workcase_id" property="workcaseId" jdbcType="VARCHAR"/>
|
||||
<result column="action" property="action" jdbcType="VARCHAR"/>
|
||||
<result column="message" property="message" jdbcType="VARCHAR"/>
|
||||
<result column="files" property="files" jdbcType="ARRAY" typeHandler="org.xyzh.common.jdbc.handler.StringArrayTypeHandler"/>
|
||||
<result column="processor" property="processor" jdbcType="VARCHAR"/>
|
||||
<result column="processor_name" property="processorName" jdbcType="VARCHAR"/>
|
||||
<result column="remark" property="remark" jdbcType="VARCHAR"/>
|
||||
<result column="creator" property="creator" jdbcType="VARCHAR"/>
|
||||
<result column="creator_name" property="creatorName" jdbcType="VARCHAR"/>
|
||||
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
process_id, optsn, workcase_id, action, message, files, processor, remark, creator, create_time
|
||||
</sql>
|
||||
@@ -58,50 +73,64 @@
|
||||
WHERE process_id = #{processId}
|
||||
</select>
|
||||
|
||||
<select id="selectWorkcaseProcessList" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM workcase.tb_workcase_process
|
||||
<select id="selectWorkcaseProcessList" resultMap="VOResultMap">
|
||||
SELECT p.process_id, p.optsn, p.workcase_id, p.action, p.message, p.files,
|
||||
p.processor, p.remark, p.creator, p.create_time,
|
||||
COALESCE(u1.username, g1.name) as creator_name,
|
||||
COALESCE(u2.username, g2.name) as processor_name
|
||||
FROM workcase.tb_workcase_process p
|
||||
LEFT JOIN sys.tb_sys_user_info u1 ON p.creator = u1.user_id
|
||||
LEFT JOIN sys.tb_guest g1 ON p.creator = g1.user_id
|
||||
LEFT JOIN sys.tb_sys_user_info u2 ON p.processor = u2.user_id
|
||||
LEFT JOIN sys.tb_guest g2 ON p.processor = g2.user_id
|
||||
<where>
|
||||
<if test="filter.processId != null and filter.processId != ''">
|
||||
AND process_id = #{filter.processId}
|
||||
AND p.process_id = #{filter.processId}
|
||||
</if>
|
||||
<if test="filter.workcaseId != null and filter.workcaseId != ''">
|
||||
AND workcase_id = #{filter.workcaseId}
|
||||
AND p.workcase_id = #{filter.workcaseId}
|
||||
</if>
|
||||
<if test="filter.action != null and filter.action != ''">
|
||||
AND action = #{filter.action}
|
||||
AND p.action = #{filter.action}
|
||||
</if>
|
||||
<if test="filter.processor != null and filter.processor != ''">
|
||||
AND processor = #{filter.processor}
|
||||
AND p.processor = #{filter.processor}
|
||||
</if>
|
||||
<if test="filter.creator != null and filter.creator != ''">
|
||||
AND creator = #{filter.creator}
|
||||
AND p.creator = #{filter.creator}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY create_time ASC
|
||||
ORDER BY p.create_time ASC
|
||||
</select>
|
||||
|
||||
<select id="selectWorkcaseProcessPage" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM workcase.tb_workcase_process
|
||||
<select id="selectWorkcaseProcessPage" resultMap="VOResultMap">
|
||||
SELECT p.process_id, p.optsn, p.workcase_id, p.action, p.message, p.files,
|
||||
p.processor, p.remark, p.creator, p.create_time,
|
||||
COALESCE(u1.username, g1.name) as creator_name,
|
||||
COALESCE(u2.username, g2.name) as processor_name
|
||||
FROM workcase.tb_workcase_process p
|
||||
LEFT JOIN sys.tb_sys_user_info u1 ON p.creator = u1.user_id
|
||||
LEFT JOIN sys.tb_guest g1 ON p.creator = g1.user_id
|
||||
LEFT JOIN sys.tb_sys_user_info u2 ON p.processor = u2.user_id
|
||||
LEFT JOIN sys.tb_guest g2 ON p.processor = g2.user_id
|
||||
<where>
|
||||
<if test="filter.processId != null and filter.processId != ''">
|
||||
AND process_id = #{filter.processId}
|
||||
AND p.process_id = #{filter.processId}
|
||||
</if>
|
||||
<if test="filter.workcaseId != null and filter.workcaseId != ''">
|
||||
AND workcase_id = #{filter.workcaseId}
|
||||
AND p.workcase_id = #{filter.workcaseId}
|
||||
</if>
|
||||
<if test="filter.action != null and filter.action != ''">
|
||||
AND action = #{filter.action}
|
||||
AND p.action = #{filter.action}
|
||||
</if>
|
||||
<if test="filter.processor != null and filter.processor != ''">
|
||||
AND processor = #{filter.processor}
|
||||
AND p.processor = #{filter.processor}
|
||||
</if>
|
||||
<if test="filter.creator != null and filter.creator != ''">
|
||||
AND creator = #{filter.creator}
|
||||
AND p.creator = #{filter.creator}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY create_time ASC
|
||||
ORDER BY p.create_time ASC
|
||||
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
|
||||
</select>
|
||||
|
||||
|
||||
@@ -134,10 +134,9 @@ async function handleLogin() {
|
||||
if (response.success && response.data) {
|
||||
const loginData = response.data
|
||||
|
||||
// 8. 保存 Token
|
||||
// 8. 保存 Token(只用 TokenManager,避免格式不一致)
|
||||
if (loginData.token) {
|
||||
TokenManager.setToken(loginData.token, loginForm.rememberMe)
|
||||
localStorage.setItem('token', loginData.token)
|
||||
}
|
||||
|
||||
// 9. 保存 LoginDomain 到 LocalStorage
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { api } from '@/api/index'
|
||||
import type { ResultDomain, PageRequest } from '@/types'
|
||||
import type { TbKnowledge, TbKnowledgeFile, KnowledgeFileVO, SegmentRequestBody, DocumentStatusRequestBody } from '@/types/ai'
|
||||
import type { TbKnowledge, TbKnowledgeFile, KnowledgeFileVO, SegmentRequestBody, DocumentStatusRequestBody, TbKnowledgeFileLog } from '@/types/ai'
|
||||
|
||||
/**
|
||||
* @description AI知识库相关接口
|
||||
@@ -162,8 +162,8 @@ export const aiKnowledgeAPI = {
|
||||
* 删除知识库文件
|
||||
* @param fileId 文件ID
|
||||
*/
|
||||
async deleteFile(fileId: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`${this.baseUrl}/file/${fileId}`)
|
||||
async deleteFile(fileRootId: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`${this.baseUrl}/file/${fileRootId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -263,5 +263,25 @@ export const aiKnowledgeAPI = {
|
||||
requestBody
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== 日志管理 ======================
|
||||
|
||||
/**
|
||||
* 查询知识库操作日志列表
|
||||
* @param fileLog 查询条件
|
||||
*/
|
||||
async getFileLogList(fileLog: TbKnowledgeFileLog): Promise<ResultDomain<TbKnowledgeFileLog>> {
|
||||
const response = await api.post<TbKnowledgeFileLog>(`${this.baseUrl}/datasets/log/list`, fileLog)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 分页查询知识库操作日志
|
||||
* @param pageRequest 分页请求
|
||||
*/
|
||||
async getFileLogPage(pageRequest: PageRequest<TbKnowledgeFileLog>): Promise<ResultDomain<TbKnowledgeFileLog>> {
|
||||
const response = await api.post<TbKnowledgeFileLog>(`${this.baseUrl}/datasets/log/page`, pageRequest)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ export const aiChatAPI = {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('agentId', agentId)
|
||||
const response = await api.uploadPut<DifyFileInfo>(`${this.baseUrl}/file/upload`, formData)
|
||||
const response = await api.upload<DifyFileInfo>(`${this.baseUrl}/file/upload`, formData)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export const fileAPI = {
|
||||
if (param.uploader) {
|
||||
formData.append('uploader', param.uploader);
|
||||
}
|
||||
const response = await api.upload<TbSysFileDTO>(`${this.baseUrl}/batch-upload`, formData);
|
||||
const response = await api.upload<TbSysFileDTO>(`${this.baseUrl}/upload/batch`, formData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="name">{{ file.name || file.fileName || '未知文件' }}</div>
|
||||
<div class="name">{{ file.name || '未知文件' }}</div>
|
||||
<div class="size">{{ file.size ? formatFileSize(file.size) : '' }}</div>
|
||||
</div>
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { FILE_DOWNLOAD_URL } from '@/config'
|
||||
import { FILE_DOWNLOAD_URL, FILE_MAX_SIZE } from '@/config'
|
||||
import { fileAPI } from '@/api/file/file'
|
||||
import type { TbSysFileDTO } from '@/types/file/file'
|
||||
import { ElButton, ElDialog } from 'element-plus'
|
||||
@@ -179,7 +179,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
coverImg: '',
|
||||
fileList: () => [],
|
||||
accept: '',
|
||||
maxSize: 10 * 1024 * 1024,
|
||||
maxSize: FILE_MAX_SIZE,
|
||||
maxCount: 10,
|
||||
title: '文件上传',
|
||||
buttonText: '上传文件',
|
||||
@@ -226,7 +226,7 @@ const currentFileList = computed(() => {
|
||||
// 判断已上传文件是否为图片
|
||||
const isUploadedImageFile = (file: InternalFile): boolean => {
|
||||
if (file.localPreviewUrl) return true // 有本地预览说明是图片
|
||||
const mimeType = file.mimeType || file.extension || ''
|
||||
const mimeType = file.mimeType || ''
|
||||
return mimeType.includes('image') || /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name || '')
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ const getUploadedFileUrl = (file: InternalFile): string => {
|
||||
|
||||
// 获取已上传文件的类型图标
|
||||
const getUploadedFileTypeIcon = (file: InternalFile): string => {
|
||||
const ext = file.extension || file.name?.split('.').pop() || ''
|
||||
const ext = file.type || file.name?.split('.').pop() || ''
|
||||
const iconMap: Record<string, string> = {
|
||||
pdf: '📄',
|
||||
doc: '📝',
|
||||
|
||||
@@ -205,6 +205,9 @@ export const FILE_DOWNLOAD_URL = config.file.downloadUrl;
|
||||
export const PUBLIC_IMG_PATH = config.publicImgPath;
|
||||
export const PUBLIC_WEB_PATH = config.publicWebPath;
|
||||
|
||||
// 文件上传大小限制(100MB)
|
||||
export const FILE_MAX_SIZE = 100 * 1024 * 1024;
|
||||
|
||||
// 导出完整配置对象
|
||||
export const APP_CONFIG = {
|
||||
// 应用标题
|
||||
|
||||
@@ -13,27 +13,27 @@ export interface DifyFileInfo {
|
||||
/** 文件扩展名 */
|
||||
extension?: string
|
||||
/** 文件MIME类型 */
|
||||
mimeType?: string
|
||||
mime_type?: string
|
||||
/** 上传人ID */
|
||||
createdBy?: string
|
||||
created_by?: string
|
||||
/** 上传时间(时间戳) */
|
||||
createdAt?: number
|
||||
created_at?: number
|
||||
/** 预览URL */
|
||||
previewUrl?: string
|
||||
preview_url?: string
|
||||
/** 源文件URL */
|
||||
sourceUrl?: string
|
||||
source_url?: string
|
||||
/** 文件类型:image、document、audio、video、file */
|
||||
type?: string
|
||||
/** 传输方式:remote_url、local_file */
|
||||
transferMethod?: string
|
||||
transfer_method?: string
|
||||
/** 文件URL或ID */
|
||||
url?: string
|
||||
/** 本地文件上传ID */
|
||||
uploadFileId?: string
|
||||
upload_file_id?: string
|
||||
/** 系统文件ID */
|
||||
sysFileId?: string
|
||||
sys_file_id?: string
|
||||
/** 文件路径(从系统文件表获取) */
|
||||
filePath?: string
|
||||
file_path?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,6 +88,7 @@ export interface ChatPrepareData {
|
||||
userId?: string
|
||||
/** 用户类型(false=来客,true=员工) */
|
||||
userType?: boolean
|
||||
service?: string
|
||||
}
|
||||
|
||||
// ==================== 请求参数类型(必传校验) ====================
|
||||
@@ -106,24 +107,6 @@ export interface CreateChatParam {
|
||||
title?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备流式对话参数
|
||||
*/
|
||||
export interface PrepareChatParam {
|
||||
/** 对话ID(必传) */
|
||||
chatId: string
|
||||
/** 用户问题(必传) */
|
||||
query: string
|
||||
/** 智能体ID(必传) */
|
||||
agentId: string
|
||||
/** 用户类型(必传) */
|
||||
userType: boolean
|
||||
/** 用户ID */
|
||||
userId?: string
|
||||
/** 文件列表 */
|
||||
files?: DifyFileInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话参数
|
||||
*/
|
||||
|
||||
@@ -106,3 +106,27 @@ export interface SegmentRequestBody {
|
||||
export interface DocumentStatusRequestBody {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 知识库文件操作日志
|
||||
*/
|
||||
export interface TbKnowledgeFileLog extends BaseDTO {
|
||||
/** 日志ID */
|
||||
logId?: string
|
||||
/** 知识库ID */
|
||||
knowledgeId?: string
|
||||
/** 文件根ID */
|
||||
fileRootId?: string
|
||||
/** 文件ID */
|
||||
fileId?: string
|
||||
/** 文件名称 */
|
||||
fileName?: string
|
||||
/** 服务名称 */
|
||||
service?: string
|
||||
/** 文件版本 */
|
||||
version?: number
|
||||
/** 操作类型 upload/download/delete/update */
|
||||
action?: string
|
||||
/** 操作人用户名 */
|
||||
creatorName?: string
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { api } from 'shared/api'
|
||||
import type { ResultDomain, PageRequest } from 'shared/types'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO, TbWorkcaseDeviceDTO } from '@/types/workcase'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO, TbWorkcaseDeviceDTO, WorkcaseProcessVO } from '@/types/workcase'
|
||||
|
||||
/**
|
||||
* @description 工单管理接口
|
||||
@@ -121,8 +121,8 @@ export const workcaseAPI = {
|
||||
* 查询工单处理过程列表
|
||||
* @param filter 筛选条件
|
||||
*/
|
||||
async getWorkcaseProcessList(filter?: TbWorkcaseProcessDTO): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
|
||||
const response = await api.post<TbWorkcaseProcessDTO>(`${this.baseUrl}/process/list`, filter || {})
|
||||
async getWorkcaseProcessList(filter?: TbWorkcaseProcessDTO): Promise<ResultDomain<WorkcaseProcessVO>> {
|
||||
const response = await api.post<WorkcaseProcessVO>(`${this.baseUrl}/process/list`, filter || {})
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -130,8 +130,8 @@ export const workcaseAPI = {
|
||||
* 分页查询工单处理过程
|
||||
* @param pageRequest 分页请求
|
||||
*/
|
||||
async getWorkcaseProcessPage(pageRequest: PageRequest<TbWorkcaseProcessDTO>): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
|
||||
const response = await api.post<TbWorkcaseProcessDTO>(`${this.baseUrl}/process/page`, pageRequest)
|
||||
async getWorkcaseProcessPage(pageRequest: PageRequest<TbWorkcaseProcessDTO>): Promise<ResultDomain<WorkcaseProcessVO>> {
|
||||
const response = await api.post<WorkcaseProcessVO>(`${this.baseUrl}/process/page`, pageRequest)
|
||||
return response.data
|
||||
},
|
||||
|
||||
|
||||
@@ -272,5 +272,19 @@ export const workcaseChatAPI = {
|
||||
async endVideoMeeting(meetingId: string): Promise<ResultDomain<VideoMeetingVO>> {
|
||||
const response = await api.post<VideoMeetingVO>(`${this.baseUrl}/meeting/${meetingId}/end`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== 聊天室评分管理 ======================
|
||||
|
||||
/**
|
||||
* 提交聊天室服务评分
|
||||
* @param roomId 聊天室ID
|
||||
* @param commentLevel 评分(1-5星)
|
||||
*/
|
||||
async submitComment(roomId: string, commentLevel: number): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.post<boolean>(`/urban-lifeline/workcase/chat/room/${roomId}/comment`, null, {
|
||||
params: { commentLevel }
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// 全局EL分页组件样式
|
||||
|
||||
// ==================== 品牌色变量 ====================
|
||||
$brand-color: #0055AA;
|
||||
$brand-color-light: #EBF5FF;
|
||||
$brand-color-hover: #004488;
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -190,11 +195,32 @@
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
// 分页样式
|
||||
.table-pagination {
|
||||
margin-top: 12px;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
// 全局分页样式(不需要 :deep,因为这是全局样式)
|
||||
.el-pagination {
|
||||
.el-pager {
|
||||
li {
|
||||
border-radius: 6px;
|
||||
|
||||
&.is-active {
|
||||
background: $brand-color;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-prev,
|
||||
.btn-next {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name-cell {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
v-for="engineer in availableEngineers"
|
||||
:key="engineer.userId"
|
||||
:label="`${engineer.username} (${engineer.statusName || '未知状态'})`"
|
||||
:value="engineer.userId"
|
||||
:value="engineer.userId!"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>{{ engineer.username }}</span>
|
||||
@@ -46,7 +46,7 @@
|
||||
ref="fileUploadRef"
|
||||
mode="content"
|
||||
:max-count="5"
|
||||
:max-size="10 * 1024 * 1024"
|
||||
:max-size="FILE_MAX_SIZE"
|
||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx"
|
||||
:auto-upload="false"
|
||||
:custom-upload="handleFilesUpload"
|
||||
@@ -65,12 +65,13 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ElDialog, ElButton, ElInput, ElSelect, ElOption, ElMessage } from 'element-plus'
|
||||
import { FileUpload } from 'shared/components'
|
||||
import { fileAPI } from 'shared/api'
|
||||
import { fileAPI } from 'shared/api/file'
|
||||
import { workcaseAPI } from '@/api/workcase'
|
||||
import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
|
||||
import type { TbWorkcaseProcessDTO } from '@/types/workcase/workcase'
|
||||
import type { CustomerServiceVO } from '@/types/workcase/customer'
|
||||
import type { TbSysFileDTO } from 'shared/types'
|
||||
import { FILE_MAX_SIZE } from '@/config'
|
||||
|
||||
interface Props {
|
||||
/** 是否显示弹窗 */
|
||||
|
||||
@@ -216,6 +216,9 @@ export const FILE_UPLOAD_URL = config.file.uploadUrl;
|
||||
export const PUBLIC_IMG_PATH = config.publicImgPath;
|
||||
export const PUBLIC_WEB_PATH = config.publicWebPath;
|
||||
|
||||
// 文件上传大小限制(100MB)
|
||||
export const FILE_MAX_SIZE = 100 * 1024 * 1024;
|
||||
|
||||
// 导出完整配置对象
|
||||
export const APP_CONFIG = {
|
||||
// 应用标题
|
||||
|
||||
@@ -64,9 +64,9 @@ router.beforeEach(async (to, from, next) => {
|
||||
const newToken = loginDomain.token
|
||||
|
||||
// 保存到localStorage(覆盖旧的登录状态)
|
||||
localStorage.setItem('token', newToken)
|
||||
localStorage.setItem('loginDomain', JSON.stringify(loginDomain))
|
||||
// 只用 TokenManager 存储 token,避免格式不一致
|
||||
TokenManager.setToken(newToken)
|
||||
localStorage.setItem('loginDomain', JSON.stringify(loginDomain))
|
||||
|
||||
console.log('[Workcase Router] Token验证成功,登录状态已刷新')
|
||||
} else {
|
||||
|
||||
@@ -103,6 +103,8 @@ declare module 'shared/types' {
|
||||
|
||||
export interface BaseVO extends BaseDTO {
|
||||
id?: string
|
||||
creatorName?: string
|
||||
updaterName?: string
|
||||
}
|
||||
|
||||
// 重新导出 response
|
||||
@@ -131,14 +133,14 @@ declare module 'shared/types' {
|
||||
DifyFileInfo,
|
||||
ChatPrepareData,
|
||||
CreateChatParam,
|
||||
PrepareChatParam,
|
||||
StopChatParam,
|
||||
CommentMessageParam,
|
||||
ChatListParam,
|
||||
ChatMessageListParam,
|
||||
SSEMessageData,
|
||||
SSECallbacks,
|
||||
SSETask
|
||||
SSETask,
|
||||
TbKnowledgeFileLog
|
||||
} from '../../../shared/src/types/ai'
|
||||
|
||||
// 重新导出 menu
|
||||
|
||||
@@ -13,11 +13,13 @@ export interface TbChatRoomDTO extends BaseDTO {
|
||||
status?: string
|
||||
guestId?: string
|
||||
guestName?: string
|
||||
deviceCode?: string
|
||||
aiSessionId?: string
|
||||
currentAgentId?: string
|
||||
agentCount?: number
|
||||
messageCount?: number
|
||||
unreadCount?: number
|
||||
commentLevel?: number
|
||||
lastMessageTime?: string
|
||||
lastMessage?: string
|
||||
closedBy?: string
|
||||
@@ -163,12 +165,14 @@ export interface ChatRoomVO extends BaseVO {
|
||||
status?: string
|
||||
guestId?: string
|
||||
guestName?: string
|
||||
deviceCode?: string
|
||||
aiSessionId?: string
|
||||
currentAgentId?: string
|
||||
currentAgentName?: string
|
||||
agentCount?: number
|
||||
messageCount?: number
|
||||
unreadCount?: number
|
||||
commentLevel?: number
|
||||
lastMessageTime?: string
|
||||
lastMessage?: string
|
||||
closedBy?: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BaseDTO } from 'shared/types'
|
||||
import type { BaseDTO, BaseVO } from 'shared/types'
|
||||
|
||||
/**
|
||||
* 工单表对象
|
||||
@@ -72,3 +72,23 @@ export interface TbWorkcaseDeviceDTO extends BaseDTO {
|
||||
/** 文件根ID */
|
||||
fileRootId?: string
|
||||
}
|
||||
|
||||
export interface WorkcaseProcessVO extends BaseVO {
|
||||
/** 工单ID */
|
||||
workcaseId?: string
|
||||
/** 过程ID */
|
||||
processId?: string
|
||||
/** 动作 info:记录,assign:指派,redeploy:转派,repeal:撤销,finish:完成 */
|
||||
action?: 'info' | 'assign' | 'redeploy' | 'repeal' | 'finish'
|
||||
/** 消息 */
|
||||
message?: string
|
||||
/** 携带文件列表 */
|
||||
files?: string[]
|
||||
/** 处理人(指派、转派专属) */
|
||||
processor?: string
|
||||
processorName?: string
|
||||
/** 创建时间 */
|
||||
createTime?: string
|
||||
/** 更新时间 */
|
||||
updateTime?: string
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
:title="'上传文档到:' + currentKnowledgeName"
|
||||
button-text="上传文档"
|
||||
accept=".pdf,.doc,.docx,.txt,.md"
|
||||
:max-size="50 * 1024 * 1024"
|
||||
:max-size="FILE_MAX_SIZE"
|
||||
:max-count="10"
|
||||
:custom-upload="customKnowledgeUpload"
|
||||
@upload-error="handleUploadError"
|
||||
@@ -110,7 +110,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { aiKnowledgeAPI } from 'shared/api/ai'
|
||||
import { FileUpload, FileHistory } from 'shared/components'
|
||||
import DocumentSegment from 'shared/components/ai/knowledge/DocumentSegment.vue'
|
||||
import { FILE_DOWNLOAD_URL } from '@/config/index'
|
||||
import { FILE_DOWNLOAD_URL, FILE_MAX_SIZE } from '@/config/index'
|
||||
import type { TbKnowledge } from 'shared/types'
|
||||
|
||||
// Tab 配置
|
||||
@@ -278,7 +278,7 @@ const deleteFile = async (row: DocumentItem) => {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const result = await aiKnowledgeAPI.deleteFile(row.id)
|
||||
const result = await aiKnowledgeAPI.deleteFile(row.fileRootId)
|
||||
if (result.success) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDocuments(activeKnowledgeId.value)
|
||||
@@ -306,7 +306,7 @@ const customKnowledgeUpload = async (files: File[]) => {
|
||||
const result = await aiKnowledgeAPI.uploadToKnowledge(files[0], targetKnowledgeId)
|
||||
if (result.success) {
|
||||
ElMessage.success('文件上传成功')
|
||||
fetchKnowledges()
|
||||
// fetchKnowledges()
|
||||
fetchDocuments(activeKnowledgeId.value)
|
||||
} else {
|
||||
throw new Error(result.message || '上传失败')
|
||||
@@ -316,7 +316,7 @@ const customKnowledgeUpload = async (files: File[]) => {
|
||||
const result = await aiKnowledgeAPI.batchUploadToKnowledge(files, targetKnowledgeId)
|
||||
if (result.success) {
|
||||
ElMessage.success('文件上传成功')
|
||||
fetchKnowledges()
|
||||
// fetchKnowledges()
|
||||
fetchDocuments(activeKnowledgeId.value)
|
||||
} else {
|
||||
throw new Error(result.message || '上传失败')
|
||||
|
||||
@@ -11,49 +11,72 @@
|
||||
<!-- 筛选区域 -->
|
||||
<el-card class="filter-card">
|
||||
<div class="ticket-filters">
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 280px;" />
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 280px;"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
|
||||
<div class="filter-right">
|
||||
<el-select v-model="operationFilter" placeholder="操作类型" clearable style="width: 140px;">
|
||||
<el-select v-model="filter.action" placeholder="操作类型" clearable style="width: 140px;" @change="handleSearch">
|
||||
<el-option label="上传" value="upload" />
|
||||
<el-option label="下载" value="download" />
|
||||
<el-option label="删除" value="delete" />
|
||||
<el-option label="更新" value="update" />
|
||||
</el-select>
|
||||
<el-select v-model="kbTypeFilter" placeholder="知识库类型" clearable style="width: 140px;">
|
||||
<el-option label="外部知识库" value="external" />
|
||||
<el-option label="内部知识库" value="internal" />
|
||||
<el-select v-model="filter.knowledgeId" placeholder="知识库" clearable style="width: 180px;" @change="handleSearch">
|
||||
<el-option
|
||||
v-for="kb in knowledgeList"
|
||||
:key="kb.knowledgeId"
|
||||
:label="kb.title"
|
||||
:value="kb.knowledgeId"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-model="searchKeyword" placeholder="搜索文件名/操作人" style="width: 200px;" :prefix-icon="Search" clearable />
|
||||
<el-input
|
||||
v-model="filter.fileName"
|
||||
placeholder="搜索文件名"
|
||||
style="width: 200px;"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<el-card>
|
||||
<el-table :data="filteredLogs" style="width: 100%">
|
||||
<el-table-column prop="logId" label="日志ID" width="120">
|
||||
<el-card v-loading="loading">
|
||||
<el-table :data="logs" style="width: 100%">
|
||||
<el-table-column prop="logId" label="日志ID" width="180">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #409eff; font-weight: 500;">{{ row.logId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileName" label="文件名" min-width="200" />
|
||||
<el-table-column prop="operation" label="操作类型" width="100">
|
||||
<el-table-column prop="fileName" label="文件名" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="action" label="操作类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getOperationType(row.operation)" size="small">
|
||||
{{ row.operationName }}
|
||||
<el-tag :type="getOperationType(row.action)" size="small">
|
||||
{{ getOperationName(row.action) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="kbType" label="知识库" width="100">
|
||||
<el-table-column prop="version" label="版本" width="80">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.kbType === 'external' ? '外部' : '内部' }}</span>
|
||||
<span>v{{ row.version || 1 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作人" width="100" />
|
||||
<el-table-column prop="operationTime" label="操作时间" width="160" />
|
||||
<el-table-column prop="fileSize" label="文件大小" width="100" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<el-table-column prop="creatorName" label="操作人" width="120" />
|
||||
<el-table-column prop="createTime" label="操作时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
|
||||
</template>
|
||||
@@ -61,7 +84,15 @@
|
||||
</el-table>
|
||||
|
||||
<div class="table-pagination">
|
||||
<el-pagination v-model:current-page="currentPage" :page-size="10" :total="logs.length" layout="total, prev, pager, next" />
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="loadLogs"
|
||||
@current-change="loadLogs"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -76,19 +107,22 @@
|
||||
<span>{{ selectedLog.fileName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-tag :type="getOperationType(selectedLog.operation)">{{ selectedLog.operationName }}</el-tag>
|
||||
<el-tag :type="getOperationType(selectedLog.action)">{{ getOperationName(selectedLog.action) }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="知识库">
|
||||
<span>{{ selectedLog.kbType === 'external' ? '外部知识库' : '内部知识库' }}</span>
|
||||
<el-form-item label="文件版本">
|
||||
<span>v{{ selectedLog.version || 1 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人">
|
||||
<span>{{ selectedLog.operator }}</span>
|
||||
<span>{{ selectedLog.creatorName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作时间">
|
||||
<span>{{ selectedLog.operationTime }}</span>
|
||||
<span>{{ formatTime(selectedLog.createTime) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件大小">
|
||||
<span>{{ selectedLog.fileSize }}</span>
|
||||
<el-form-item label="知识库ID">
|
||||
<span>{{ selectedLog.knowledgeId }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件ID">
|
||||
<span>{{ selectedLog.fileId }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
@@ -96,63 +130,140 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { Download, Search } from 'lucide-vue-next'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { aiKnowledgeAPI } from 'shared/api/ai'
|
||||
import type { TbKnowledgeFileLog, TbKnowledge } from 'shared/types'
|
||||
|
||||
const loading = ref(false)
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
const operationFilter = ref('')
|
||||
const kbTypeFilter = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const showDetailDialog = ref(false)
|
||||
const selectedLog = ref<any>(null)
|
||||
const selectedLog = ref<TbKnowledgeFileLog | null>(null)
|
||||
|
||||
const logs = ref([
|
||||
{ logId: 'LOG001', fileName: 'TH-500GF操作手册.pdf', operation: 'upload', operationName: '上传', kbType: 'external', operator: '张三', operationTime: '2024-12-10 14:30', fileSize: '2.5MB' },
|
||||
{ logId: 'LOG002', fileName: 'TH-300D故障排查指南.pdf', operation: 'upload', operationName: '上传', kbType: 'external', operator: '李四', operationTime: '2024-12-09 10:15', fileSize: '1.8MB' },
|
||||
{ logId: 'LOG003', fileName: '内部技术规范v2.0.pdf', operation: 'update', operationName: '更新', kbType: 'internal', operator: '赵六', operationTime: '2024-12-11 16:20', fileSize: '3.2MB' },
|
||||
{ logId: 'LOG004', fileName: '售后服务流程.pdf', operation: 'download', operationName: '下载', kbType: 'internal', operator: '孙七', operationTime: '2024-12-10 11:00', fileSize: '1.1MB' },
|
||||
{ logId: 'LOG005', fileName: '员工培训手册.pdf', operation: 'delete', operationName: '删除', kbType: 'internal', operator: '周八', operationTime: '2024-12-09 15:30', fileSize: '2.0MB' }
|
||||
])
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
let result = logs.value
|
||||
if (operationFilter.value) {
|
||||
result = result.filter(l => l.operation === operationFilter.value)
|
||||
}
|
||||
if (kbTypeFilter.value) {
|
||||
result = result.filter(l => l.kbType === kbTypeFilter.value)
|
||||
}
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(l =>
|
||||
l.fileName.toLowerCase().includes(keyword) ||
|
||||
l.operator.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
return result.slice((currentPage.value - 1) * 10, currentPage.value * 10)
|
||||
// 筛选条件
|
||||
const filter = reactive<TbKnowledgeFileLog>({
|
||||
action: '',
|
||||
knowledgeId: '',
|
||||
fileName: '',
|
||||
service: 'workcase'
|
||||
})
|
||||
|
||||
const getOperationType = (operation: string) => {
|
||||
const map: Record<string, string> = {
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 日志列表
|
||||
const logs = ref<TbKnowledgeFileLog[]>([])
|
||||
|
||||
// 知识库列表(用于筛选)
|
||||
const knowledgeList = ref<TbKnowledge[]>([])
|
||||
|
||||
// 操作类型映射
|
||||
const operationTypeMap: Record<string, string> = {
|
||||
upload: 'success',
|
||||
download: 'info',
|
||||
delete: 'danger',
|
||||
update: 'warning'
|
||||
}
|
||||
return map[operation] || 'info'
|
||||
}
|
||||
|
||||
const viewDetail = (row: any) => {
|
||||
const operationNameMap: Record<string, string> = {
|
||||
upload: '上传',
|
||||
download: '下载',
|
||||
delete: '删除',
|
||||
update: '更新'
|
||||
}
|
||||
|
||||
const getOperationType = (action: string) => operationTypeMap[action] || 'info'
|
||||
const getOperationName = (action: string) => operationNameMap[action] || action
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string | number): string => {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 加载知识库列表
|
||||
const loadKnowledgeList = async () => {
|
||||
try {
|
||||
const result = await aiKnowledgeAPI.listKnowledges({})
|
||||
if (result.success && result.dataList) {
|
||||
knowledgeList.value = result.dataList
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载知识库列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载日志列表
|
||||
const loadLogs = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 构建查询条件
|
||||
const queryFilter: TbKnowledgeFileLog = { ...filter }
|
||||
console.log(queryFilter)
|
||||
// 处理日期范围
|
||||
if (dateRange.value && dateRange.value[0] && dateRange.value[1]) {
|
||||
queryFilter.createTimeStart = dateRange.value[0].toISOString()
|
||||
queryFilter.createTimeEnd = dateRange.value[1].toISOString()
|
||||
}
|
||||
|
||||
const result = await aiKnowledgeAPI.getFileLogPage({
|
||||
filter: queryFilter,
|
||||
pageParam: {
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
total: 0
|
||||
}
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
logs.value = result.dataList || []
|
||||
pagination.total = result.pageParam?.total || 0
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载日志失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载日志失败:', error)
|
||||
ElMessage.error('加载日志失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (row: TbKnowledgeFileLog) => {
|
||||
selectedLog.value = row
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
// 导出日志
|
||||
const exportLogs = () => {
|
||||
ElMessage.success('日志导出成功')
|
||||
ElMessage.success('日志导出功能开发中')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadKnowledgeList()
|
||||
loadLogs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<AdminLayout title="工单日志" info="查看工单操作记录">
|
||||
<AdminLayout title="工单日志" info="查看工单流程处理记录">
|
||||
<template #action>
|
||||
<el-button type="primary" @click="exportLogs">
|
||||
<el-icon><Download /></el-icon>
|
||||
@@ -11,46 +11,52 @@
|
||||
<!-- 筛选区域 -->
|
||||
<el-card class="filter-card">
|
||||
<div class="ticket-filters">
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 280px;" />
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 280px;" @change="handleSearch" />
|
||||
|
||||
<div class="filter-right">
|
||||
<el-select v-model="operationFilter" placeholder="操作类型" clearable style="width: 140px;">
|
||||
<el-option label="创建" value="create" />
|
||||
<el-option label="更新" value="update" />
|
||||
<el-option label="指派" value="assign" />
|
||||
<el-option label="完成" value="complete" />
|
||||
<el-option label="关闭" value="close" />
|
||||
<el-select v-model="filter.action" placeholder="操作类型" clearable style="width: 140px;" @change="handleSearch">
|
||||
<el-option v-for="item in actionOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<el-select v-model="operatorFilter" placeholder="操作人" clearable style="width: 120px;">
|
||||
<el-option label="王五" value="wangwu" />
|
||||
<el-option label="赵六" value="zhaoliu" />
|
||||
<el-option label="孙七" value="sunqi" />
|
||||
</el-select>
|
||||
<el-input v-model="searchKeyword" placeholder="搜索工单号/内容" style="width: 200px;" :prefix-icon="Search" clearable />
|
||||
<el-input v-model="filter.workcaseId" placeholder="搜索工单ID" style="width: 200px;" :prefix-icon="Search" clearable @keyup.enter="handleSearch" @clear="handleSearch" />
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<el-card>
|
||||
<el-table :data="filteredLogs" style="width: 100%">
|
||||
<el-table-column prop="logId" label="日志ID" width="120">
|
||||
<el-table :data="processLogs" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="processId" label="流程ID" width="180">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #409eff; font-weight: 500;">{{ row.logId }}</span>
|
||||
<span style="color: #409eff; font-weight: 500;">{{ row.processId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ticketNo" label="工单号" width="120" />
|
||||
<el-table-column prop="operation" label="操作类型" width="100">
|
||||
<el-table-column prop="workcaseId" label="工单ID" width="180" />
|
||||
<el-table-column prop="action" label="操作类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getOperationType(row.operation)" size="small">
|
||||
{{ row.operationName }}
|
||||
<el-tag :type="getActionTagType(row.action)" size="small">
|
||||
{{ getActionLabel(row.action) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作人" width="100" />
|
||||
<el-table-column prop="content" label="操作内容" min-width="200" />
|
||||
<el-table-column prop="operationTime" label="操作时间" width="160" />
|
||||
<el-table-column prop="ipAddress" label="IP地址" width="130" />
|
||||
<el-table-column prop="message" label="操作内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="processorName" label="处理人" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.processorName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="files" label="附件" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.files?.length" size="small" type="info">{{ row.files.length }}个</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="creatorName" label="操作人" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.creatorName || row.creator || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="操作时间" width="170" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
|
||||
@@ -59,99 +65,154 @@
|
||||
</el-table>
|
||||
|
||||
<div class="table-pagination">
|
||||
<el-pagination v-model:current-page="currentPage" :page-size="10" :total="logs.length" layout="total, prev, pager, next" />
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.size"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSearch"
|
||||
@current-change="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情弹窗 -->
|
||||
<el-dialog v-model="showDetailDialog" title="日志详情" width="600px">
|
||||
<el-form v-if="selectedLog" label-width="100px">
|
||||
<el-form-item label="日志ID">
|
||||
<span>{{ selectedLog.logId }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="工单号">
|
||||
<span>{{ selectedLog.ticketNo }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-tag :type="getOperationType(selectedLog.operation)">{{ selectedLog.operationName }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人">
|
||||
<span>{{ selectedLog.operator }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作内容">
|
||||
<span>{{ selectedLog.content }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作时间">
|
||||
<span>{{ selectedLog.operationTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="IP地址">
|
||||
<span>{{ selectedLog.ipAddress }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-dialog v-model="showDetailDialog" title="流程详情" width="600px">
|
||||
<el-descriptions v-if="selectedLog" :column="1" border>
|
||||
<el-descriptions-item label="流程ID">{{ selectedLog.processId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工单ID">{{ selectedLog.workcaseId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作类型">
|
||||
<el-tag :type="getActionTagType(selectedLog.action)">{{ getActionLabel(selectedLog.action) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作内容">{{ selectedLog.message || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理人">{{ selectedLog.processorName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">{{ selectedLog.creatorName || selectedLog.creator || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作时间">{{ selectedLog.createTime }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="selectedLog.files?.length" label="附件">
|
||||
<div class="file-list">
|
||||
<el-tag v-for="(file, index) in selectedLog.files" :key="index" size="small" style="margin-right: 8px; margin-bottom: 4px;">
|
||||
{{ file }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { Download, Search } from 'lucide-vue-next'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { workcaseAPI } from '@/api/workcase'
|
||||
import type { TbWorkcaseProcessDTO, WorkcaseProcessVO } from '@/types/workcase'
|
||||
import type { PageRequest, PageParam } from 'shared/types'
|
||||
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
const operationFilter = ref('')
|
||||
const operatorFilter = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
// 操作类型选项
|
||||
const actionOptions = [
|
||||
{ value: 'info', label: '记录' },
|
||||
{ value: 'assign', label: '指派' },
|
||||
{ value: 'redeploy', label: '转派' },
|
||||
{ value: 'repeal', label: '撤销' },
|
||||
{ value: 'finish', label: '完成' }
|
||||
]
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const dateRange = ref<[string, string] | null>(null)
|
||||
const showDetailDialog = ref(false)
|
||||
const selectedLog = ref<any>(null)
|
||||
const selectedLog = ref<WorkcaseProcessVO | null>(null)
|
||||
const processLogs = ref<WorkcaseProcessVO[]>([])
|
||||
|
||||
const logs = ref([
|
||||
{ logId: 'LOG001', ticketNo: 'TK001', operation: 'create', operationName: '创建', operator: '王五', content: '创建工单,客户反映设备显示屏不亮', operationTime: '2024-12-13 10:30', ipAddress: '192.168.1.100' },
|
||||
{ logId: 'LOG002', ticketNo: 'TK001', operation: 'assign', operationName: '指派', operator: '赵六', content: '将工单指派给技术人员处理', operationTime: '2024-12-13 10:35', ipAddress: '192.168.1.101' },
|
||||
{ logId: 'LOG003', ticketNo: 'TK002', operation: 'create', operationName: '创建', operator: '孙七', content: '创建工单,客户反映机械故障', operationTime: '2024-12-13 09:15', ipAddress: '192.168.1.102' },
|
||||
{ logId: 'LOG004', ticketNo: 'TK002', operation: 'update', operationName: '更新', operator: '王五', content: '更新工单状态为处理中', operationTime: '2024-12-13 09:45', ipAddress: '192.168.1.100' },
|
||||
{ logId: 'LOG005', ticketNo: 'TK003', operation: 'complete', operationName: '完成', operator: '赵六', content: '工单处理完成,客户已确认', operationTime: '2024-12-12 14:20', ipAddress: '192.168.1.101' }
|
||||
])
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
let result = logs.value
|
||||
if (operationFilter.value) {
|
||||
result = result.filter(l => l.operation === operationFilter.value)
|
||||
}
|
||||
if (operatorFilter.value) {
|
||||
result = result.filter(l => l.operator === operatorFilter.value)
|
||||
}
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(l =>
|
||||
l.ticketNo.toLowerCase().includes(keyword) ||
|
||||
l.content.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
return result.slice((currentPage.value - 1) * 10, currentPage.value * 10)
|
||||
// 筛选条件
|
||||
const filter = reactive<TbWorkcaseProcessDTO>({
|
||||
workcaseId: '',
|
||||
action: undefined
|
||||
})
|
||||
|
||||
const getOperationType = (operation: string) => {
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 获取操作类型标签
|
||||
const getActionLabel = (action?: string) => {
|
||||
const map: Record<string, string> = {
|
||||
create: 'success',
|
||||
update: 'info',
|
||||
assign: 'warning',
|
||||
complete: 'success',
|
||||
close: 'danger'
|
||||
info: '记录',
|
||||
assign: '指派',
|
||||
redeploy: '转派',
|
||||
repeal: '撤销',
|
||||
finish: '完成'
|
||||
}
|
||||
return map[operation] || 'info'
|
||||
return map[action || ''] || action || '-'
|
||||
}
|
||||
|
||||
const viewDetail = (row: any) => {
|
||||
// 获取操作类型标签样式
|
||||
const getActionTagType = (action?: string) => {
|
||||
const map: Record<string, string> = {
|
||||
info: 'info',
|
||||
assign: 'warning',
|
||||
redeploy: '',
|
||||
repeal: 'danger',
|
||||
finish: 'success'
|
||||
}
|
||||
return map[action || ''] || 'info'
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
const handleSearch = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const filterData: TbWorkcaseProcessDTO = { ...filter }
|
||||
|
||||
// 日期范围
|
||||
if (dateRange.value) {
|
||||
filterData.startTime = dateRange.value[0]
|
||||
filterData.endTime = dateRange.value[1]
|
||||
}
|
||||
|
||||
const pageParam: PageParam = {
|
||||
pageNumber: pagination.current,
|
||||
pageSize: pagination.size
|
||||
}
|
||||
|
||||
const pageRequest: PageRequest<TbWorkcaseProcessDTO> = {
|
||||
filter: filterData,
|
||||
pageParam
|
||||
}
|
||||
|
||||
const res = await workcaseAPI.getWorkcaseProcessPage(pageRequest)
|
||||
if (res.success) {
|
||||
processLogs.value = res.dataList || res.pageDomain?.dataList || []
|
||||
pagination.total = res.pageDomain?.pageParam?.totalElements || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询工单流程失败:', error)
|
||||
ElMessage.error('查询失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (row: WorkcaseProcessVO) => {
|
||||
selectedLog.value = row
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
// 导出日志
|
||||
const exportLogs = () => {
|
||||
ElMessage.success('日志导出成功')
|
||||
ElMessage.success('日志导出功能开发中')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -162,4 +223,9 @@ const exportLogs = () => {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -247,32 +247,6 @@ $brand-color-hover: #004488;
|
||||
}
|
||||
}
|
||||
|
||||
// 分页样式
|
||||
.table-pagination {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:deep(.el-pagination) {
|
||||
.el-pager {
|
||||
li {
|
||||
border-radius: 6px;
|
||||
|
||||
&.is-active {
|
||||
background: $brand-color;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-prev,
|
||||
.btn-next {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗样式
|
||||
:deep(.el-dialog) {
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
|
||||
<el-button v-if="row.status === 'pending'" type="warning" link size="small" @click="assignTicket(row)">指派</el-button>
|
||||
<el-button v-if="row.status === 'processing'" type="success" link size="small" @click="completeTicket(row)">完成</el-button>
|
||||
<el-button v-if="row.status === 'processing' || row.status === 'process'" type="success" link size="small" @click="completeTicket(row)">完成</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -153,9 +153,10 @@
|
||||
<el-dialog v-model="showDetailDialog" title="工单详情" width="900px" destroy-on-close>
|
||||
<WorkcaseDetail
|
||||
v-if="showDetailDialog"
|
||||
:workcase="currentWorkcase"
|
||||
:workcase-id="currentWorkcase.workcaseId"
|
||||
mode="view"
|
||||
@cancel="showDetailDialog = false"
|
||||
@complete="handleCompleteFromDetail"
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
@@ -303,15 +304,25 @@ const completeWorkcaseAPI = async (workcase: TbWorkcaseDTO) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= Mock 数据 =========================
|
||||
const mockTickets = [
|
||||
{ workcaseId: 'WC001', optsn: 'TK001', username: '张三', phone: '13800138000', type: 'electrical', device: 'TH-500GF', emergency: 'emergency' as const, status: 'pending' as const, processor: '', createTime: '2024-12-13 10:30' },
|
||||
{ workcaseId: 'WC002', optsn: 'TK002', username: '李四', phone: '13800138001', type: 'mechanical', device: 'TH-300D', emergency: 'normal' as const, status: 'processing' as const, processor: '王五', createTime: '2024-12-13 09:15' },
|
||||
{ workcaseId: 'WC003', optsn: 'TK003', username: '王五', phone: '13800138002', type: 'control', device: 'S-200X', emergency: 'normal' as const, status: 'done' as const, processor: '赵六', createTime: '2024-12-12 14:20' },
|
||||
{ workcaseId: 'WC004', optsn: 'TK004', username: '赵六', phone: '13800138003', type: 'parts', device: 'TH-800GF', emergency: 'normal' as const, status: 'pending' as const, processor: '', createTime: '2024-12-13 11:00' },
|
||||
{ workcaseId: 'WC005', optsn: 'TK005', username: '孙七', phone: '13800138004', type: 'install', device: 'G-100S', emergency: 'emergency' as const, status: 'processing' as const, processor: '李四', createTime: '2024-12-13 08:45' }
|
||||
]
|
||||
/**
|
||||
* 从详情页完成工单
|
||||
*/
|
||||
const handleCompleteFromDetail = async (workcaseId: string) => {
|
||||
const process: TbWorkcaseProcessDTO = {
|
||||
workcaseId: workcaseId,
|
||||
action: 'finish',
|
||||
message: '工单完成'
|
||||
}
|
||||
|
||||
const res = await workcaseAPI.createWorkcaseProcess(process)
|
||||
if (res.success) {
|
||||
ElMessage.success('完成成功')
|
||||
showDetailDialog.value = false
|
||||
loadWorkcases()
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========================= 字段映射 =========================
|
||||
@@ -331,6 +342,7 @@ const emergencyMap: Record<string, string> = {
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
process: '处理中',
|
||||
processing: '处理中',
|
||||
done: '已完成'
|
||||
}
|
||||
@@ -363,6 +375,7 @@ const filteredTickets = computed(() => {
|
||||
const getStatusType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
process: 'info',
|
||||
processing: 'info',
|
||||
done: 'success'
|
||||
}
|
||||
|
||||
@@ -386,15 +386,18 @@ $brand-color-hover: #004488;
|
||||
&.user {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.message-content {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
background: $brand-color;
|
||||
color: #fff;
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
text-align: right;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,8 +422,18 @@ $brand-color-hover: #004488;
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 80%;
|
||||
|
||||
&.user {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 12px 16px;
|
||||
border-radius: 16px 16px 16px 4px;
|
||||
background: #fff;
|
||||
@@ -458,11 +471,17 @@ $brand-color-hover: #004488;
|
||||
&:nth-child(3) { animation-delay: 0s; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
// 用户消息气泡中的样式
|
||||
.message-row.user .message-bubble {
|
||||
.message-time {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -538,11 +557,14 @@ $brand-color-hover: #004488;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.chat-textarea {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
@@ -551,25 +573,13 @@ $brand-color-hover: #004488;
|
||||
background: transparent;
|
||||
line-height: 1.5;
|
||||
max-height: 120px;
|
||||
min-height: 24px;
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid #f8fafc;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
padding: 8px;
|
||||
color: #94a3b8;
|
||||
@@ -578,11 +588,21 @@ $brand-color-hover: #004488;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #64748b;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
&.uploading {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
@@ -593,6 +613,7 @@ $brand-color-hover: #004488;
|
||||
border-radius: 12px;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.active {
|
||||
background: $brand-color;
|
||||
@@ -717,3 +738,137 @@ $brand-color-hover: #004488;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 文件上传相关样式 ====================
|
||||
.uploaded-files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.uploaded-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
max-width: 200px;
|
||||
|
||||
.file-preview {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.image {
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&.document {
|
||||
background: $brand-color-light;
|
||||
color: $brand-color;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.remove-file-btn {
|
||||
padding: 4px;
|
||||
color: #94a3b8;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 消息气泡外的文件样式
|
||||
.message-files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.message-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
max-width: 220px;
|
||||
text-decoration: none;
|
||||
color: #374151;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
border-color: $brand-color;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.image {
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&.document {
|
||||
background: $brand-color-light;
|
||||
color: $brand-color;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@@ -122,8 +122,10 @@
|
||||
<Headphones v-else :size="16" />
|
||||
</div>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<div class="message-bubble" :class="msg.role">
|
||||
<!-- 消息内容区域 -->
|
||||
<div class="message-content" :class="msg.role">
|
||||
<!-- 文字气泡 -->
|
||||
<div v-if="msg.text || (isStreaming && msg.role === 'assistant' && messages.indexOf(msg) === messages.length - 1)" class="message-bubble" :class="msg.role">
|
||||
<div
|
||||
v-if="msg.text"
|
||||
class="message-text"
|
||||
@@ -134,6 +136,25 @@
|
||||
<div v-if="!msg.text && isStreaming && msg.role === 'assistant'" class="loading-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 消息携带的文件(在气泡外面) -->
|
||||
<div v-if="msg.files && msg.files.length > 0" class="message-files">
|
||||
<a
|
||||
v-for="fileId in msg.files"
|
||||
:key="fileId"
|
||||
:href="getFileDownloadUrl(fileId)"
|
||||
target="_blank"
|
||||
class="message-file-item"
|
||||
>
|
||||
<div v-if="isImageFileById(fileId)" class="file-preview image">
|
||||
<img :src="getFileDownloadUrl(fileId)" :alt="getFileName(fileId)" />
|
||||
</div>
|
||||
<div v-else class="file-preview document">
|
||||
<FileIcon :size="18" />
|
||||
</div>
|
||||
<span class="file-name">{{ getFileName(fileId) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="message-time">{{ msg.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,8 +181,46 @@
|
||||
<div class="input-wrapper">
|
||||
<!-- 输入卡片 -->
|
||||
<div class="input-card">
|
||||
<!-- 输入框 -->
|
||||
<!-- 已上传文件预览 -->
|
||||
<div v-if="uploadedFiles.length > 0" class="uploaded-files">
|
||||
<div
|
||||
v-for="(file, index) in uploadedFiles"
|
||||
:key="file.id || index"
|
||||
class="uploaded-file-item"
|
||||
>
|
||||
<div v-if="isImageFile(file)" class="file-preview image">
|
||||
<img :src="getFilePreviewUrl(file)" :alt="file.name" />
|
||||
</div>
|
||||
<div v-else class="file-preview document">
|
||||
<FileIcon :size="20" />
|
||||
</div>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<button class="remove-file-btn" @click="removeUploadedFile(index)">
|
||||
<X :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 输入框行(包含输入框和按钮) -->
|
||||
<div class="input-row">
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt"
|
||||
style="display: none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
<button
|
||||
class="tool-btn"
|
||||
:class="{ uploading: isUploading }"
|
||||
:disabled="isUploading"
|
||||
@click="triggerFileUpload"
|
||||
title="添加附件"
|
||||
>
|
||||
<Loader2 v-if="isUploading" :size="18" class="spin" />
|
||||
<Paperclip v-else :size="18" />
|
||||
</button>
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="inputText"
|
||||
@@ -171,24 +230,16 @@
|
||||
:rows="1"
|
||||
class="chat-textarea"
|
||||
/>
|
||||
</div>
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar-row">
|
||||
<div class="toolbar-actions">
|
||||
<button class="tool-btn" title="添加附件">
|
||||
<Paperclip :size="18" />
|
||||
</button>
|
||||
<button
|
||||
class="send-btn"
|
||||
:class="{ active: inputText.trim() }"
|
||||
:disabled="!inputText.trim()"
|
||||
:class="{ active: inputText.trim() || uploadedFiles.length > 0 }"
|
||||
:disabled="(!inputText.trim() && uploadedFiles.length === 0) || isStreaming"
|
||||
@click="sendMessage"
|
||||
>
|
||||
<Send :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="disclaimer">AI 生成内容仅供参考 · 泰豪集团内部绝密信息请勿上传</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,27 +263,24 @@ import {
|
||||
Paperclip,
|
||||
Send,
|
||||
User,
|
||||
Headphones
|
||||
Headphones,
|
||||
X,
|
||||
Image,
|
||||
File as FileIcon,
|
||||
Loader2
|
||||
} from 'lucide-vue-next'
|
||||
import { aiChatAPI, agentAPI } from 'shared/api/ai'
|
||||
import { fileAPI } from 'shared/api/file'
|
||||
import type {
|
||||
TbChat,
|
||||
TbChatMessage,
|
||||
TbAgent,
|
||||
PrepareChatParam,
|
||||
ChatPrepareData,
|
||||
SSEMessageData,
|
||||
DifyFileInfo
|
||||
DifyFileInfo,
|
||||
TbSysFileDTO
|
||||
} from 'shared/types'
|
||||
import { AGENT_ID } from '@/config'
|
||||
|
||||
// 显示用消息接口
|
||||
interface DisplayMessage {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
text: string
|
||||
time: string
|
||||
messageId?: string
|
||||
}
|
||||
import { AGENT_ID, FILE_DOWNLOAD_URL, FILE_MAX_SIZE } from '@/config'
|
||||
|
||||
// 用户信息(TODO: 从实际用户store获取)
|
||||
const userId = computed(()=>{
|
||||
@@ -255,7 +303,7 @@ const chatHistory = ref<TbChat[]>([])
|
||||
const currentChatTitle = ref<string>('')
|
||||
|
||||
// 聊天消息列表
|
||||
const messages = ref<DisplayMessage[]>([])
|
||||
const messages = ref<TbChatMessage[]>([])
|
||||
|
||||
// 流式对话状态
|
||||
const isStreaming = ref(false)
|
||||
@@ -265,6 +313,14 @@ const eventSource = ref<EventSource | null>(null)
|
||||
// 输入框文本
|
||||
const inputText = ref('')
|
||||
|
||||
// 上传的文件列表
|
||||
const uploadedFiles = ref<DifyFileInfo[]>([])
|
||||
const isUploading = ref(false)
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// 文件信息缓存 (fileId -> TbSysFileDTO)
|
||||
const fileInfoCache = ref<Map<string, TbSysFileDTO>>(new Map())
|
||||
|
||||
// 消息容器引用
|
||||
const messagesRef = ref<HTMLElement | null>(null)
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
@@ -288,6 +344,7 @@ const startNewChat = async () => {
|
||||
currentChatTitle.value = ''
|
||||
messages.value = []
|
||||
inputText.value = ''
|
||||
uploadedFiles.value = []
|
||||
|
||||
// 创建新会话
|
||||
if (agentId && userId.value) {
|
||||
@@ -320,12 +377,14 @@ const loadChat = async (chatId: string) => {
|
||||
if (result.success && result.dataList) {
|
||||
const messageList = Array.isArray(result.dataList) ? result.dataList : [result.dataList]
|
||||
messages.value = messageList.map((msg: TbChatMessage) => ({
|
||||
...msg,
|
||||
id: msg.messageId || String(Date.now()),
|
||||
role: msg.role === 'user' ? 'user' : 'assistant',
|
||||
text: msg.content || '',
|
||||
time: formatTime(msg.createTime),
|
||||
messageId: msg.messageId
|
||||
} as DisplayMessage))
|
||||
time: formatTime(msg.createTime)
|
||||
} as TbChatMessage))
|
||||
|
||||
// 加载消息中的文件信息
|
||||
await loadMessagesFilesInfo(messageList)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载对话消息失败:', error)
|
||||
@@ -360,20 +419,36 @@ const scrollToBottom = () => {
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = async () => {
|
||||
if (!inputText.value.trim() || isStreaming.value) return
|
||||
// 允许只有文件或只有文本
|
||||
if ((!inputText.value.trim() && uploadedFiles.value.length === 0) || isStreaming.value) return
|
||||
if (!agentId) {
|
||||
console.error('未选择智能体')
|
||||
return
|
||||
}
|
||||
|
||||
const query = inputText.value.trim()
|
||||
const userMessage: DisplayMessage = {
|
||||
const query = inputText.value.trim() || '[文件]'
|
||||
const currentFiles = [...uploadedFiles.value] // 保存当前文件列表副本
|
||||
const userMessage: TbChatMessage = {
|
||||
id: String(Date.now()),
|
||||
role: 'user',
|
||||
text: query,
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
text: inputText.value.trim(),
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
files: currentFiles.length > 0 ? currentFiles.map(item => item.sys_file_id): undefined
|
||||
}
|
||||
|
||||
// 将上传文件的信息缓存起来,用于立即渲染
|
||||
currentFiles.forEach(f => {
|
||||
if (f.sys_file_id) {
|
||||
fileInfoCache.value.set(f.sys_file_id, {
|
||||
fileId: f.sys_file_id,
|
||||
name: f.name,
|
||||
url: f.preview_url || f.source_url,
|
||||
extension: f.extension,
|
||||
mimeType: f.mime_type
|
||||
} as TbSysFileDTO)
|
||||
}
|
||||
})
|
||||
|
||||
messages.value.push(userMessage)
|
||||
inputText.value = ''
|
||||
|
||||
@@ -404,14 +479,19 @@ const sendMessage = async () => {
|
||||
}
|
||||
|
||||
// 准备流式对话参数
|
||||
const prepareParam: PrepareChatParam = {
|
||||
const prepareParam: ChatPrepareData = {
|
||||
chatId: currentChatId.value!,
|
||||
query: query,
|
||||
agentId: agentId,
|
||||
userType: userType.value,
|
||||
userId: userId.value
|
||||
userId: userId.value,
|
||||
files: uploadedFiles.value.length > 0 ? uploadedFiles.value : undefined,
|
||||
service: "workcase"
|
||||
}
|
||||
|
||||
// 清空已上传的文件
|
||||
uploadedFiles.value = []
|
||||
|
||||
try {
|
||||
// 准备流式对话
|
||||
const prepareResult = await aiChatAPI.prepareStreamChat(prepareParam)
|
||||
@@ -422,7 +502,7 @@ const sendMessage = async () => {
|
||||
const sessionId = prepareResult.data
|
||||
|
||||
// 创建AI回复消息占位
|
||||
const assistantMessage: DisplayMessage = {
|
||||
const assistantMessage: TbChatMessage = {
|
||||
id: String(Date.now() + 1),
|
||||
role: 'assistant',
|
||||
text: '',
|
||||
@@ -631,6 +711,126 @@ const handleKeyDown = (e: KeyboardEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 触发文件选择
|
||||
const triggerFileUpload = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = async (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const files = target.files
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
for (const file of Array.from(files)) {
|
||||
await uploadFile(file)
|
||||
}
|
||||
|
||||
// 清空input,允许重复选择同一文件
|
||||
target.value = ''
|
||||
}
|
||||
|
||||
// 上传单个文件
|
||||
const uploadFile = async (file: File) => {
|
||||
if (!agentId) {
|
||||
console.error('未选择智能体')
|
||||
return
|
||||
}
|
||||
|
||||
// 文件大小限制
|
||||
if (file.size > FILE_MAX_SIZE) {
|
||||
console.error(`文件大小超过${FILE_MAX_SIZE / 1024 / 1024}MB限制`)
|
||||
return
|
||||
}
|
||||
|
||||
isUploading.value = true
|
||||
|
||||
try {
|
||||
const result = await aiChatAPI.uploadFileForChat(file, agentId)
|
||||
if (result.success && result.data) {
|
||||
uploadedFiles.value.push(result.data)
|
||||
} else {
|
||||
console.error('文件上传失败:', result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error)
|
||||
} finally {
|
||||
isUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 移除已上传的文件
|
||||
const removeUploadedFile = (index: number) => {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (file: DifyFileInfo): boolean => {
|
||||
return file.type === 'image' || file.mime_type?.startsWith('image/') || false
|
||||
}
|
||||
|
||||
// 获取文件预览URL
|
||||
const getFilePreviewUrl = (file: DifyFileInfo): string => {
|
||||
return file.preview_url || file.source_url || file.url || ''
|
||||
}
|
||||
|
||||
// 加载消息中的文件信息
|
||||
const loadMessagesFilesInfo = async (messageList: TbChatMessage[]) => {
|
||||
// 收集所有文件ID
|
||||
const fileIds: string[] = []
|
||||
messageList.forEach(msg => {
|
||||
if (msg.files) {
|
||||
const filesArray = Array.isArray(msg.files) ? msg.files : [msg.files]
|
||||
filesArray.forEach((id: string) => {
|
||||
if (id && !fileInfoCache.value.has(id)) {
|
||||
fileIds.push(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (fileIds.length === 0) return
|
||||
|
||||
// 逐个查询文件信息
|
||||
for (const fileId of fileIds) {
|
||||
try {
|
||||
const res = await fileAPI.getFileById(fileId)
|
||||
if (res.success && res.data) {
|
||||
fileInfoCache.value.set(fileId, res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`加载文件信息失败: ${fileId}`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取缓存的文件信息
|
||||
const getFileInfo = (fileId: string): TbSysFileDTO | undefined => {
|
||||
return fileInfoCache.value.get(fileId)
|
||||
}
|
||||
|
||||
// 获取文件名(从缓存)
|
||||
const getFileName = (fileId: string): string => {
|
||||
const fileInfo = fileInfoCache.value.get(fileId)
|
||||
return fileInfo?.name || fileId.substring(0, 8) + '...'
|
||||
}
|
||||
|
||||
// 获取文件下载URL
|
||||
const getFileDownloadUrl = (fileId: string): string => {
|
||||
if (!fileId) return ''
|
||||
const fileInfo = fileInfoCache.value.get(fileId)
|
||||
if (fileInfo?.url) return fileInfo.url
|
||||
return `${FILE_DOWNLOAD_URL}${fileId}`
|
||||
}
|
||||
|
||||
// 判断文件ID对应的文件是否为图片
|
||||
const isImageFileById = (fileId: string): boolean => {
|
||||
const fileInfo = fileInfoCache.value.get(fileId)
|
||||
if (!fileInfo) return false
|
||||
const ext = (fileInfo.extension || fileInfo.name?.split('.').pop() || '').toLowerCase()
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(ext)
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(async () => {
|
||||
// TODO: 根据路由参数或配置获取智能体ID
|
||||
|
||||
@@ -94,11 +94,14 @@
|
||||
:room-name="currentRoom?.roomName"
|
||||
:file-download-url="FILE_DOWNLOAD_URL"
|
||||
:has-more="hasMore"
|
||||
:guest-id="currentRoom?.guestId"
|
||||
:comment-level="currentRoom?.commentLevel"
|
||||
:loading-more="loadingMore"
|
||||
@send-message="handleSendMessage"
|
||||
@start-meeting="startMeeting"
|
||||
@start-meeting="startMeeting()"
|
||||
@download-file="downloadFile"
|
||||
@load-more="loadMoreMessages"
|
||||
@submit-comment="handleSubmitComment"
|
||||
>
|
||||
<template #header>
|
||||
<div class="chat-room-header">
|
||||
@@ -171,6 +174,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElButton, ElInput, ElDialog, ElMessage } from 'element-plus'
|
||||
import { Search, FileText, MessageSquare, MessageCircle, ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||
import ChatRoom from './chatRoom/ChatRoom.vue'
|
||||
@@ -185,12 +189,12 @@ import { Client } from '@stomp/stompjs'
|
||||
// WebSocket配置 (通过Nginx代理访问网关,再到workcase服务)
|
||||
// SockJS URL (http://)
|
||||
const getWsUrl = () => {
|
||||
const token = JSON.parse(localStorage.getItem('token')).value || ''
|
||||
const token = JSON.parse(localStorage.getItem('token')!).value
|
||||
const protocol = window.location.protocol
|
||||
const host = window.location.host
|
||||
return `${protocol}//${host}/api/urban-lifeline/workcase/ws/chat-sockjs?token=${encodeURIComponent(token)}`
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
// STOMP客户端
|
||||
let stompClient: any = null
|
||||
let roomSubscription: any = null
|
||||
@@ -488,6 +492,23 @@ const handleSendMessage = async (content: string, files: File[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理评分提交(从ChatRoom组件触发)
|
||||
const handleSubmitComment = async (rating: number) => {
|
||||
if (!currentRoomId.value) return
|
||||
|
||||
try {
|
||||
const result = await workcaseChatAPI.submitComment(currentRoomId.value, rating)
|
||||
if (result.success) {
|
||||
ElMessage.success('感谢您的评分!')
|
||||
} else {
|
||||
ElMessage.error(result.message || '评分提交失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('评分提交失败:', error)
|
||||
ElMessage.error('评分提交失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (fileId: string) => {
|
||||
window.open(`${FILE_DOWNLOAD_URL}/${fileId}`, '_blank')
|
||||
@@ -525,43 +546,17 @@ const startMeeting = async () => {
|
||||
const activeResult = await workcaseChatAPI.getActiveMeeting(currentRoomId.value)
|
||||
if (activeResult.success && activeResult.data) {
|
||||
// 已有活跃会议,直接加入
|
||||
currentMeetingId.value = activeResult.data.meetingId!
|
||||
const joinResult = await workcaseChatAPI.joinVideoMeeting(currentMeetingId.value!)
|
||||
const currentMeetingId = activeResult.data.meetingId!
|
||||
const joinResult = await workcaseChatAPI.joinVideoMeeting(currentMeetingId)
|
||||
if (joinResult.success && joinResult.data?.iframeUrl) {
|
||||
// 使用router跳转到JitsiMeetingView页面,附加roomId参数用于返回
|
||||
const meetingUrl = joinResult.data.iframeUrl + `&roomId=${currentRoomId.value}`
|
||||
const meetingUrl = joinResult.data.iframeUrl + `&roomId=${currentRoomId}`
|
||||
router.push(meetingUrl)
|
||||
} else {
|
||||
ElMessage.error(joinResult.message || '加入会议失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 没有活跃会议,创建新会议
|
||||
const createResult = await workcaseChatAPI.createVideoMeeting({
|
||||
roomId: currentRoomId.value,
|
||||
meetingName: currentRoom.value?.roomName || '视频会议'
|
||||
})
|
||||
|
||||
if (createResult.success && createResult.data) {
|
||||
currentMeetingId.value = createResult.data.meetingId!
|
||||
|
||||
// 开始会议
|
||||
await workcaseChatAPI.startVideoMeeting(currentMeetingId.value!)
|
||||
|
||||
// 加入会议获取会议页面URL
|
||||
const joinResult = await workcaseChatAPI.joinVideoMeeting(currentMeetingId.value!)
|
||||
if (joinResult.success && joinResult.data?.iframeUrl) {
|
||||
// 使用router跳转到JitsiMeetingView页面,附加roomId参数用于返回
|
||||
const meetingUrl = joinResult.data.iframeUrl + `&roomId=${currentRoomId.value}`
|
||||
router.push(meetingUrl)
|
||||
ElMessage.success('会议已创建')
|
||||
} else {
|
||||
ElMessage.error(joinResult.message || '获取会议链接失败')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(createResult.message || '创建会议失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发起会议失败:', error)
|
||||
ElMessage.error('发起会议失败')
|
||||
@@ -686,9 +681,17 @@ const subscribeToRoom = (roomId: string) => {
|
||||
// 避免重复添加自己发送的普通消息
|
||||
// 但会议消息(meet类型)始终添加,因为它是系统生成的通知
|
||||
if (chatMessage.messageType === 'meet' || chatMessage.senderId !== loginDomain.user.userId) {
|
||||
// 会议消息延时处理,等待数据库事务提交
|
||||
if (chatMessage.messageType === 'meet') {
|
||||
console.log('[ChatRoom] 收到会议消息,延时1秒后刷新')
|
||||
setTimeout(() => {
|
||||
loadMessages(roomId)
|
||||
}, 1000)
|
||||
} else {
|
||||
messages.value.push(chatMessage)
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,35 @@ $brand-color-hover: #004488;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
// 系统消息样式(居中显示)
|
||||
&.is-system {
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
|
||||
.system-message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 80%;
|
||||
|
||||
.system-message-text {
|
||||
padding: 8px 16px;
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
|
||||
@@ -21,8 +21,30 @@
|
||||
v-for="message in messages"
|
||||
:key="message.messageId"
|
||||
class="message-row"
|
||||
:class="message.senderId === currentUserId ? 'is-me' : 'other'"
|
||||
:class="getMessageClass(message)"
|
||||
>
|
||||
<!-- 系统消息(居中显示) -->
|
||||
<template v-if="message.senderType === 'system'">
|
||||
<div class="system-message-container">
|
||||
<!-- 评分消息卡片 -->
|
||||
<template v-if="message.messageType === 'comment'">
|
||||
<CommentMessageCard
|
||||
:room-id="roomId"
|
||||
:can-comment="canComment"
|
||||
:initial-rating="commentLevel"
|
||||
@submit="handleCommentSubmit"
|
||||
/>
|
||||
</template>
|
||||
<!-- 其他系统消息 -->
|
||||
<template v-else>
|
||||
<div class="system-message-text">{{ message.content }}</div>
|
||||
</template>
|
||||
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 普通用户/客服消息 -->
|
||||
<template v-else>
|
||||
<div>
|
||||
<!-- 头像 -->
|
||||
<div class="message-avatar">
|
||||
@@ -68,6 +90,7 @@
|
||||
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,15 +164,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ref, nextTick, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { FileText, Video, Paperclip, Send } from 'lucide-vue-next'
|
||||
import MeetingCreate from '../MeetingCreate/MeetingCreate.vue'
|
||||
import MeetingCard from '../MeetingCard/MeetingCard.vue'
|
||||
import CommentMessageCard from './CommentMessageCard/CommentMessageCard.vue'
|
||||
import type { ChatRoomMessageVO, VideoMeetingVO } from '@/types/workcase'
|
||||
import { workcaseChatAPI } from '@/api/workcase'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { FILE_DOWNLOAD_URL } from '@/config'
|
||||
const router = useRouter()
|
||||
|
||||
interface Props {
|
||||
@@ -158,25 +182,31 @@ interface Props {
|
||||
roomId: string
|
||||
roomName?: string
|
||||
workcaseId?: string
|
||||
fileDownloadUrl?: string
|
||||
commentLevel?: number
|
||||
hasMore?: boolean
|
||||
loadingMore?: boolean
|
||||
guestId?: string // 聊天室访客ID,用于判断评价权限
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
roomName: '聊天室',
|
||||
fileDownloadUrl: '',
|
||||
hasMore: true,
|
||||
loadingMore: false
|
||||
loadingMore: false,
|
||||
guestId: ''
|
||||
})
|
||||
|
||||
const FILE_DOWNLOAD_URL = props.fileDownloadUrl
|
||||
|
||||
// 计算当前用户是否可以评价(只有访客可以评价)
|
||||
const canComment = computed(() => {
|
||||
return props.currentUserId === props.guestId
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'send-message': [content: string, files: File[]]
|
||||
'download-file': [fileId: string]
|
||||
'load-more': []
|
||||
'start-meeting': []
|
||||
'submit-comment': [rating: number]
|
||||
}>()
|
||||
|
||||
// 会议相关状态
|
||||
@@ -251,7 +281,31 @@ const handleFileSelect = (e: Event) => {
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (messagesRef.value) {
|
||||
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
||||
// 使用 smooth 滚动到底部
|
||||
messagesRef.value.scrollTo({
|
||||
top: messagesRef.value.scrollHeight,
|
||||
behavior: 'instant'
|
||||
})
|
||||
|
||||
// 延迟再次确保滚动到底部(处理内容动态渲染的情况)
|
||||
setTimeout(() => {
|
||||
if (messagesRef.value) {
|
||||
messagesRef.value.scrollTo({
|
||||
top: messagesRef.value.scrollHeight,
|
||||
behavior: 'instant'
|
||||
})
|
||||
}
|
||||
}, 100)
|
||||
|
||||
// 最后一次确保(处理图片等异步内容加载)
|
||||
setTimeout(() => {
|
||||
if (messagesRef.value) {
|
||||
messagesRef.value.scrollTo({
|
||||
top: messagesRef.value.scrollHeight,
|
||||
behavior: 'instant'
|
||||
})
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -333,6 +387,25 @@ function getMeetingId(contentExtra: Record<string, any> | undefined): string {
|
||||
return contentExtra.meetingId as string
|
||||
}
|
||||
|
||||
// 获取消息的CSS类
|
||||
const getMessageClass = (message: ChatRoomMessageVO) => {
|
||||
if (message.senderType === 'system') {
|
||||
return 'is-system'
|
||||
}
|
||||
return message.senderId === props.currentUserId ? 'is-me' : 'other'
|
||||
}
|
||||
|
||||
// 处理评分提交
|
||||
const handleCommentSubmit = async (rating: number) => {
|
||||
try {
|
||||
emit('submit-comment', rating)
|
||||
ElMessage.success('感谢您的评分!')
|
||||
} catch (error) {
|
||||
console.error('提交评分失败:', error)
|
||||
ElMessage.error('提交评分失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// Markdown渲染函数
|
||||
const renderMarkdown = (text: string): string => {
|
||||
if (!text) return ''
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
|
||||
.comment-message-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
max-width: 360px;
|
||||
margin: 0 auto;
|
||||
|
||||
.comment-header {
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
|
||||
.comment-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.star-rating {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.star-item {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rating-desc {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.submitted-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-permission {
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding: 10px 28px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 24px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #667eea;
|
||||
border-color: transparent;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="comment-message-card">
|
||||
<div class="comment-header">
|
||||
<span class="comment-title">{{ title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="comment-body">
|
||||
<!-- 星级评分 -->
|
||||
<div class="star-rating">
|
||||
<div
|
||||
v-for="star in 5"
|
||||
:key="star"
|
||||
class="star-item"
|
||||
:class="{
|
||||
'is-active': star <= currentRating,
|
||||
'is-disabled': !canComment || isSubmitted
|
||||
}"
|
||||
@click="handleStarClick(star)"
|
||||
@mouseenter="handleStarHover(star)"
|
||||
@mouseleave="handleStarLeave"
|
||||
>
|
||||
<Star
|
||||
:size="28"
|
||||
:fill="star <= (hoverRating || currentRating) ? '#FFD700' : 'none'"
|
||||
:stroke="star <= (hoverRating || currentRating) ? '#FFD700' : '#d0d0d0'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评分描述 -->
|
||||
<div v-if="currentRating > 0" class="rating-desc">
|
||||
{{ getRatingDesc(currentRating) }}
|
||||
</div>
|
||||
|
||||
<!-- 不可评价提示 -->
|
||||
<div v-if="!canComment && !isSubmitted" class="no-permission">
|
||||
仅访客可评价
|
||||
</div>
|
||||
|
||||
<!-- 已评分状态 -->
|
||||
<div v-else-if="isSubmitted" class="submitted-status">
|
||||
<Check :size="16" />
|
||||
已评分
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button
|
||||
v-else-if="canComment"
|
||||
class="submit-btn"
|
||||
:class="{ 'is-active': currentRating > 0 }"
|
||||
:disabled="currentRating === 0 || submitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ submitting ? '提交中...' : '提交评分' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Star, Check } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
roomId: string
|
||||
initialRating?: number
|
||||
canComment?: boolean // 是否可以评价(当前用户是否为guestId)
|
||||
title?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
initialRating: 0,
|
||||
canComment: false,
|
||||
title: '请为本次服务评分'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'submit': [rating: number]
|
||||
}>()
|
||||
|
||||
const currentRating = ref(props.initialRating)
|
||||
const hoverRating = ref(0)
|
||||
const submitting = ref(false)
|
||||
const isSubmitted = ref(props.initialRating > 0)
|
||||
|
||||
// 监听 initialRating 变化
|
||||
watch(() => props.initialRating, (newVal) => {
|
||||
currentRating.value = newVal
|
||||
isSubmitted.value = newVal > 0
|
||||
})
|
||||
|
||||
// 星级描述映射
|
||||
const ratingDescriptions: Record<number, string> = {
|
||||
1: '非常不满意',
|
||||
2: '不满意',
|
||||
3: '一般',
|
||||
4: '满意',
|
||||
5: '非常满意'
|
||||
}
|
||||
|
||||
const getRatingDesc = (rating: number): string => {
|
||||
return ratingDescriptions[rating] || ''
|
||||
}
|
||||
|
||||
const handleStarClick = (star: number) => {
|
||||
if (!props.canComment || isSubmitted.value) return
|
||||
currentRating.value = star
|
||||
}
|
||||
|
||||
const handleStarHover = (star: number) => {
|
||||
if (!props.canComment || isSubmitted.value) return
|
||||
hoverRating.value = star
|
||||
}
|
||||
|
||||
const handleStarLeave = () => {
|
||||
hoverRating.value = 0
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (currentRating.value === 0 || !props.canComment || isSubmitted.value) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
emit('submit', currentRating.value)
|
||||
isSubmitted.value = true
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import url('./CommentMessageCard.scss');
|
||||
</style>
|
||||
@@ -149,8 +149,13 @@
|
||||
<div class="title-bar"></div>
|
||||
处理记录
|
||||
</div>
|
||||
<!-- 工单处理人记录工单过程-->
|
||||
<ElButton v-if="isProcessor" type="primary" size="small" @click="showAddProcessDialog = true">
|
||||
<!-- 工单处理人记录工单过程 - 仅在处理中状态显示-->
|
||||
<ElButton
|
||||
v-if="isProcessor && (formData.status === 'processing')"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="showAddProcessDialog = true"
|
||||
>
|
||||
添加处理记录
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -211,14 +216,14 @@
|
||||
指派工程师
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="mode === 'view' && !isCreator && formData.status === 'processing'"
|
||||
v-if="mode === 'view' && !isCreator && (formData.status === 'processing')"
|
||||
type="warning"
|
||||
@click="handleRedeploy"
|
||||
>
|
||||
转派工程师
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="mode === 'view' && !isCreator && formData.status === 'processing'"
|
||||
v-if="mode === 'view' && !isCreator && (formData.status === 'processing')"
|
||||
type="success"
|
||||
@click="handleComplete"
|
||||
>
|
||||
@@ -251,7 +256,7 @@
|
||||
ref="processFileUploadRef"
|
||||
mode="content"
|
||||
:max-count="5"
|
||||
:max-size="10 * 1024 * 1024"
|
||||
:max-size="FILE_MAX_SIZE"
|
||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx"
|
||||
v-model:file-list="processUploadedFiles"
|
||||
@upload-success="handleProcessUploadSuccess"
|
||||
@@ -281,27 +286,24 @@ import { ref, watch, onMounted, computed } from 'vue'
|
||||
import { ChatMessage } from '@/views/public/ChatRoom/'
|
||||
import { ElButton, ElInput, ElSelect, ElOption, ElDialog, ElMessage, ElLoading } from 'element-plus'
|
||||
import { MessageSquare, ImageIcon as ImageIcon, Plus } from 'lucide-vue-next'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO } from '@/types/workcase/workcase'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO, WorkcaseProcessVO } from '@/types/workcase/workcase'
|
||||
import type { TbSysFileDTO } from 'shared/types'
|
||||
import { workcaseAPI } from '@/api/workcase'
|
||||
import { fileAPI } from 'shared/api/file'
|
||||
import { FileUpload } from 'shared/components'
|
||||
import { WorkcaseAssign } from '@/components'
|
||||
import { FILE_DOWNLOAD_URL } from '@/config'
|
||||
import { FILE_DOWNLOAD_URL, FILE_MAX_SIZE } from '@/config'
|
||||
|
||||
interface Props {
|
||||
mode?: 'view' | 'edit' | 'create'
|
||||
workcaseId?: string // 查看/编辑模式传入 workcaseId,组件内部加载数据
|
||||
roomId?: string // 创建模式传入 roomId
|
||||
workcase?: TbWorkcaseDTO // 兼容旧用法,直接传入数据
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'view',
|
||||
workcaseId: '',
|
||||
roomId: '',
|
||||
workcase: () => ({} as TbWorkcaseDTO)
|
||||
})
|
||||
roomId: ''})
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: []
|
||||
@@ -313,7 +315,6 @@ const emit = defineEmits<{
|
||||
|
||||
const loading = ref(false)
|
||||
const formData = ref<TbWorkcaseDTO>({
|
||||
...props.workcase
|
||||
})
|
||||
|
||||
// 故障类型选项(与微信端保持一致)
|
||||
@@ -321,7 +322,7 @@ const faultTypes = ['电气系统故障', '机械故障', '控制系统故障',
|
||||
|
||||
const showChatMessage = ref(false)
|
||||
const currentRoomId = ref<string>('')
|
||||
const processList = ref<TbWorkcaseProcessDTO[]>([])
|
||||
const processList = ref<WorkcaseProcessVO[]>([])
|
||||
|
||||
// 文件信息缓存 (fileId -> TbSysFileDTO)
|
||||
const fileInfoCache = ref<Map<string, TbSysFileDTO>>(new Map())
|
||||
@@ -390,7 +391,7 @@ const loadProcessList = async (workcaseId: string) => {
|
||||
}
|
||||
|
||||
// 加载处理记录中的文件信息
|
||||
const loadFilesInfo = async (processes: TbWorkcaseProcessDTO[]) => {
|
||||
const loadFilesInfo = async (processes: WorkcaseProcessVO[]) => {
|
||||
// 收集所有文件ID
|
||||
const fileIds: string[] = []
|
||||
processes.forEach(p => {
|
||||
@@ -526,16 +527,7 @@ onMounted(() => {
|
||||
|
||||
if (props.mode === 'view' || props.mode === 'edit') {
|
||||
// 查看/编辑模式:通过 workcaseId 加载数据
|
||||
if (props.workcaseId) {
|
||||
loadWorkcaseDetail(props.workcaseId)
|
||||
} else if (props.workcase?.workcaseId) {
|
||||
// 兼容旧用法
|
||||
formData.value = { ...props.workcase }
|
||||
currentRoomId.value = props.workcase.roomId || ''
|
||||
if (props.workcase.workcaseId) {
|
||||
loadProcessList(props.workcase.workcaseId)
|
||||
}
|
||||
}
|
||||
} else if (props.mode === 'create') {
|
||||
// 创建模式:初始化空表单,设置 roomId
|
||||
formData.value = {
|
||||
@@ -556,16 +548,10 @@ watch(() => props.workcaseId, (newVal) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 兼容旧用法:监听 workcase prop 变化
|
||||
watch(() => props.workcase, (newVal) => {
|
||||
if (newVal && !props.workcaseId) {
|
||||
formData.value = { ...newVal }
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
const statusLabel = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
process: '处理中',
|
||||
processing: '处理中',
|
||||
done: '已完成'
|
||||
}
|
||||
@@ -575,6 +561,7 @@ const statusLabel = (status: string) => {
|
||||
const statusClass = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'status-pending',
|
||||
process: 'status-processing',
|
||||
processing: 'status-processing',
|
||||
done: 'status-done'
|
||||
}
|
||||
@@ -729,8 +716,7 @@ const submitProcessRecord = async () => {
|
||||
try {
|
||||
// 从已上传文件列表获取文件ID
|
||||
const fileIds = processUploadedFiles.value
|
||||
.map(f => f.fileId)
|
||||
.filter((id): id is string => !!id)
|
||||
.map((f: TbSysFileDTO) => f.fileId)
|
||||
|
||||
// 提交处理记录
|
||||
const params: TbWorkcaseProcessDTO = {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { request } from '../base'
|
||||
import { request, uploadFile } from '../base'
|
||||
import type { ResultDomain } from '../../types'
|
||||
import type {
|
||||
TbChat,
|
||||
TbChatMessage,
|
||||
CreateChatParam,
|
||||
PrepareChatParam,
|
||||
ChatPrepareData,
|
||||
StopChatParam,
|
||||
CommentMessageParam,
|
||||
ChatListParam,
|
||||
ChatMessageListParam,
|
||||
SSECallbacks,
|
||||
SSETask,
|
||||
SSEMessageData
|
||||
SSEMessageData,
|
||||
DifyFileInfo
|
||||
} from '../../types/ai/aiChat'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
@@ -71,7 +72,7 @@ export const aiChatAPI = {
|
||||
* 准备流式对话会话
|
||||
* @param param agentId、chatId、query、userId 必传
|
||||
*/
|
||||
prepareChatMessageSession(param: PrepareChatParam): Promise<ResultDomain<string>> {
|
||||
prepareChatMessageSession(param: ChatPrepareData): Promise<ResultDomain<string>> {
|
||||
return request<string>({ url: `${this.baseUrl}/stream/prepare`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
@@ -190,5 +191,21 @@ export const aiChatAPI = {
|
||||
*/
|
||||
commentChatMessage(param: CommentMessageParam): Promise<ResultDomain<boolean>> {
|
||||
return request<boolean>({ url: `${this.baseUrl}/comment`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
// ====================== 文件上传 ======================
|
||||
|
||||
/**
|
||||
* 上传文件用于对话(图文多模态)
|
||||
* @param filePath 文件临时路径
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
uploadFileForChat(filePath: string, agentId: string): Promise<ResultDomain<DifyFileInfo>> {
|
||||
return uploadFile<DifyFileInfo>({
|
||||
url: `${this.baseUrl}/file/upload`,
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { agentId: agentId }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { request } from '../base'
|
||||
import type { ResultDomain, PageRequest } from '../../types'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO, TbWorkcaseDeviceDTO } from '../../types/workcase'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO, TbWorkcaseDeviceDTO, WorkcaseProcessVO } from '../../types/workcase'
|
||||
|
||||
/**
|
||||
* @description 工单管理接口
|
||||
@@ -110,16 +110,16 @@ export const workcaseAPI = {
|
||||
* 查询工单处理过程列表
|
||||
* @param filter 筛选条件
|
||||
*/
|
||||
getWorkcaseProcessList(filter?: TbWorkcaseProcessDTO): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
|
||||
return request<TbWorkcaseProcessDTO>({ url: `${this.baseUrl}/process/list`, method: 'POST', data: filter || {} })
|
||||
getWorkcaseProcessList(filter?: TbWorkcaseProcessDTO): Promise<ResultDomain<WorkcaseProcessVO>> {
|
||||
return request<WorkcaseProcessVO>({ url: `${this.baseUrl}/process/list`, method: 'POST', data: filter || {} })
|
||||
},
|
||||
|
||||
/**
|
||||
* 分页查询工单处理过程
|
||||
* @param pageRequest 分页请求
|
||||
*/
|
||||
getWorkcaseProcessPage(pageRequest: PageRequest<TbWorkcaseProcessDTO>): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
|
||||
return request<TbWorkcaseProcessDTO>({ url: `${this.baseUrl}/process/page`, method: 'POST', data: pageRequest })
|
||||
getWorkcaseProcessPage(pageRequest: PageRequest<TbWorkcaseProcessDTO>): Promise<ResultDomain<WorkcaseProcessVO>> {
|
||||
return request<WorkcaseProcessVO>({ url: `${this.baseUrl}/process/page`, method: 'POST', data: pageRequest })
|
||||
},
|
||||
|
||||
// ========================= 工单设备管理 =========================
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
TbChat,
|
||||
TbChatMessage,
|
||||
CreateChatParam,
|
||||
PrepareChatParam,
|
||||
ChatPrepareData,
|
||||
StopChatParam,
|
||||
CommentMessageParam,
|
||||
ChatListParam,
|
||||
@@ -263,5 +263,20 @@ export const workcaseChatAPI = {
|
||||
*/
|
||||
endVideoMeeting(meetingId: string): Promise<ResultDomain<any>> {
|
||||
return request({ url: `${this.baseUrl}/meeting/${meetingId}/end`, method: 'POST' })
|
||||
},
|
||||
|
||||
// ====================== 聊天室评分管理 ======================
|
||||
|
||||
/**
|
||||
* 提交聊天室服务评分
|
||||
* @param roomId 聊天室ID
|
||||
* @param commentLevel 评分(1-5星)
|
||||
*/
|
||||
submitComment(roomId: string, commentLevel: number): Promise<ResultDomain<boolean>> {
|
||||
return request({
|
||||
url: `/urban-lifeline/workcase/chat/room/${roomId}/comment`,
|
||||
method: 'POST',
|
||||
data: { commentLevel }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export const AGENT_ID = '17664699513920001'
|
||||
export const BASE_URL = 'http://localhost:8180'
|
||||
export const WS_HOST = 'localhost:8180' // WebSocket host(不包含协议)
|
||||
export const FILE_DOWNLOAD_URL = 'http://localhost:8180/urban-lifeline/sys-file/download?fileId='
|
||||
@@ -0,0 +1,118 @@
|
||||
.comment-message-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx;
|
||||
color: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
|
||||
max-width: 600rpx;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.comment-header {
|
||||
margin-bottom: 32rpx;
|
||||
text-align: center;
|
||||
|
||||
.comment-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
|
||||
.star-rating {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16rpx;
|
||||
|
||||
.star-item {
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:active:not(.is-disabled) {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.star-icon {
|
||||
font-size: 56rpx;
|
||||
line-height: 1;
|
||||
|
||||
&.star-filled {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
&.star-empty {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rating-desc {
|
||||
.rating-desc-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.submitted-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 32rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 40rpx;
|
||||
|
||||
.submitted-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.no-permission {
|
||||
padding: 16rpx 32rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 40rpx;
|
||||
|
||||
.no-permission-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding: 20rpx 56rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 48rpx;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.is-active {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-color: transparent;
|
||||
|
||||
.submit-btn-text {
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.submit-btn-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<view class="comment-message-card">
|
||||
<view class="comment-header">
|
||||
<text class="comment-title">{{ title }}</text>
|
||||
</view>
|
||||
|
||||
<view class="comment-body">
|
||||
<!-- 星级评分 -->
|
||||
<view class="star-rating">
|
||||
<view
|
||||
v-for="star in 5"
|
||||
:key="star"
|
||||
class="star-item"
|
||||
:class="{
|
||||
'is-active': star <= currentRating,
|
||||
'is-disabled': !canComment || isSubmitted
|
||||
}"
|
||||
@tap="handleStarClick(star)"
|
||||
>
|
||||
<text class="star-icon" :class="star <= currentRating ? 'star-filled' : 'star-empty'">★</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评分描述 -->
|
||||
<view v-if="currentRating > 0" class="rating-desc">
|
||||
<text class="rating-desc-text">{{ getRatingDesc(currentRating) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 不可评价提示 -->
|
||||
<view v-if="!canComment && !isSubmitted" class="no-permission">
|
||||
<text class="no-permission-text">仅访客可评价</text>
|
||||
</view>
|
||||
|
||||
<!-- 已评分状态 -->
|
||||
<view v-else-if="isSubmitted" class="submitted-status">
|
||||
<text class="submitted-text">✓ 已评分</text>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button
|
||||
v-else-if="canComment"
|
||||
class="submit-btn"
|
||||
:class="{ 'is-active': currentRating > 0 }"
|
||||
:disabled="currentRating === 0 || submitting"
|
||||
@tap="handleSubmit"
|
||||
>
|
||||
<text class="submit-btn-text">{{ submitting ? '提交中...' : '提交评分' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
roomId: string
|
||||
initialRating?: number
|
||||
canComment?: boolean // 是否可以评价(当前用户是否为guestId)
|
||||
title?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
initialRating: 0,
|
||||
canComment: false,
|
||||
title: '请为本次服务评分'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [rating: number]
|
||||
}>()
|
||||
|
||||
const currentRating = ref(props.initialRating)
|
||||
const submitting = ref(false)
|
||||
const isSubmitted = ref(props.initialRating > 0)
|
||||
|
||||
// 监听 initialRating 变化
|
||||
watch(() => props.initialRating, (newVal) => {
|
||||
currentRating.value = newVal
|
||||
isSubmitted.value = newVal > 0
|
||||
})
|
||||
|
||||
// 星级描述映射
|
||||
const ratingDescriptions = {
|
||||
1: '非常不满意',
|
||||
2: '不满意',
|
||||
3: '一般',
|
||||
4: '满意',
|
||||
5: '非常满意'
|
||||
}
|
||||
|
||||
const getRatingDesc = (rating: number): string => {
|
||||
return ratingDescriptions[rating as keyof typeof ratingDescriptions] || ''
|
||||
}
|
||||
|
||||
const handleStarClick = (star: number) => {
|
||||
if (!props.canComment || isSubmitted.value) return
|
||||
currentRating.value = star
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (currentRating.value === 0 || !props.canComment || isSubmitted.value) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
emit('submit', currentRating.value)
|
||||
isSubmitted.value = true
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './CommentMessageCard.scss';
|
||||
</style>
|
||||
@@ -329,6 +329,7 @@
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.other-row {
|
||||
@@ -363,12 +364,13 @@
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 480rpx;
|
||||
max-width: 70%;
|
||||
min-width: 0; // 允许flex子元素收缩
|
||||
}
|
||||
|
||||
// 会议卡片需要更宽的空间
|
||||
.message-content.meeting-card-wrapper {
|
||||
max-width: 600rpx;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.self-row .message-content {
|
||||
@@ -382,11 +384,13 @@
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 480rpx;
|
||||
max-width: 100%;
|
||||
padding: 18rpx 20rpx;
|
||||
border-radius: 18rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
word-break: break-all; // 长单词/URL换行
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.other-bubble {
|
||||
@@ -403,6 +407,15 @@
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.message-rich-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
@@ -462,3 +475,25 @@
|
||||
font-size: 36rpx;
|
||||
color: #4b87ff;
|
||||
}
|
||||
// ==================== 系统消息样式 ====================
|
||||
.system-row {
|
||||
justify-content: center;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.system-message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.system-message-text {
|
||||
padding: 16rpx 32rpx;
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
border-radius: 32rpx;
|
||||
font-size: 26rpx;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
</view>
|
||||
<!-- 聊天消息区域 -->
|
||||
<scroll-view class="chat-area" scroll-y="true" :scroll-top="scrollTop"
|
||||
:scroll-with-animation="false"
|
||||
:style="{ top: (headerPaddingTop + 88) + 'px' }"
|
||||
@scrolltoupper="loadMoreMessages"
|
||||
upper-threshold="50">
|
||||
@@ -54,8 +55,31 @@
|
||||
<text class="loading-more-text">没有更多消息了</text>
|
||||
</view>
|
||||
<view class="message-list">
|
||||
<view class="message-item" v-for="msg in messages" :key="msg.messageId"
|
||||
:class="msg.senderType === 'guest' ? 'self' : 'other'">
|
||||
<view class="message-item" v-for="msg in messages" :key="msg.messageId">
|
||||
<!-- 系统消息(居中显示) -->
|
||||
<view class="message-row system-row" v-if="msg.senderType === 'system'">
|
||||
<view class="system-message-container">
|
||||
<!-- 评分消息卡片 -->
|
||||
<template v-if="msg.messageType === 'comment'">
|
||||
<CommentMessageCard
|
||||
:room-id="roomId"
|
||||
:can-comment="getCanComment()"
|
||||
:initial-rating="commentLevel"
|
||||
@submit="handleCommentSubmit"
|
||||
/>
|
||||
</template>
|
||||
<!-- 其他系统消息 -->
|
||||
<template v-else>
|
||||
<view class="system-message-text">
|
||||
<text>{{ msg.content }}</text>
|
||||
</view>
|
||||
</template>
|
||||
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 普通用户/客服消息 -->
|
||||
<view v-else :class="msg.senderType === 'guest' ? 'self' : 'other'">
|
||||
<!-- 对方消息(左侧) -->
|
||||
<view class="message-row other-row" v-if="msg.senderType !== 'guest'">
|
||||
<view>
|
||||
@@ -97,6 +121,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部输入区 -->
|
||||
@@ -120,6 +145,7 @@
|
||||
import { ref, reactive, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import MeetingCard from '../../meeting/meetingCard/MeetingCard.uvue'
|
||||
import CommentMessageCard from './CommentMessageCard/CommentMessageCard.uvue'
|
||||
import type { ChatRoomMessageVO, CustomerVO, ChatMemberVO, TbChatRoomMessageDTO, VideoMeetingVO } from '@/types/workcase'
|
||||
import { workcaseChatAPI } from '@/api/workcase'
|
||||
import { wsClient } from '@/utils/websocket'
|
||||
@@ -130,6 +156,9 @@ const headerTotalHeight = ref<number>(88)
|
||||
const roomId = ref<string>('')
|
||||
const workcaseId = ref<string>('')
|
||||
const roomName = ref<string>('聊天室')
|
||||
const guestId = ref<string>('') // 聊天室访客ID
|
||||
const deviceCode = ref<string>('') // 聊天室设备代码
|
||||
const commentLevel = ref<number>(0) // 已有评分
|
||||
const inputText = ref<string>('')
|
||||
const scrollTop = ref<number>(0)
|
||||
const loading = ref<boolean>(false)
|
||||
@@ -211,6 +240,9 @@ const totalMembers = computed<MemberDisplay[]>(() => {
|
||||
return Array.from(memberMap.values())
|
||||
})
|
||||
|
||||
function getCanComment(): boolean {
|
||||
return currentUserId.value === guestId.value
|
||||
}
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
@@ -280,6 +312,9 @@ async function refreshChatRoomInfo() {
|
||||
if (roomRes.success && roomRes.data) {
|
||||
roomName.value = roomRes.data.roomName || '聊天室'
|
||||
workcaseId.value = roomRes.data.workcaseId || ''
|
||||
guestId.value = roomRes.data.guestId || ''
|
||||
deviceCode.value = roomRes.data.deviceCode || ''
|
||||
commentLevel.value = roomRes.data.commentLevel || 0
|
||||
messageTotal.value = roomRes.data.messageCount || 0
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -311,7 +346,10 @@ async function loadChatRoom() {
|
||||
if (roomRes.success && roomRes.data) {
|
||||
roomName.value = roomRes.data.roomName || '聊天室'
|
||||
workcaseId.value = roomRes.data.workcaseId || ''
|
||||
guestId.value = roomRes.data.guestId || ''
|
||||
deviceCode.value = roomRes.data.deviceCode || ''
|
||||
messageTotal.value = roomRes.data.messageCount || 0
|
||||
commentLevel.value = roomRes.data.commentLevel!
|
||||
}
|
||||
// 后端是降序查询,page1是最新消息
|
||||
currentPage.value = 1
|
||||
@@ -346,9 +384,14 @@ async function loadMessages() {
|
||||
messages.splice(0, messages.length, ...reversedList)
|
||||
console.log('[loadMessages] 加载完成, 消息数:', messages.length)
|
||||
|
||||
// 加载完第一页后检查是否需要自动填充
|
||||
// 加载完第一页后滚动到底部,需要等待 DOM 完全渲染
|
||||
if (currentPage.value === 1) {
|
||||
nextTick(() => scrollToBottom())
|
||||
// 使用 setTimeout 确保 DOM 完全渲染后再滚动
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 300)
|
||||
})
|
||||
} else {
|
||||
nextTick(() => scrollToBottom())
|
||||
}
|
||||
@@ -436,10 +479,10 @@ function renderMarkdown(text: string): string {
|
||||
html = html.replace(/(?<!\*)\*([^\*]+)\*(?!\*)/g, '<em>$1</em>')
|
||||
|
||||
// 处理行内代码(`语法)
|
||||
html = html.replace(/`([^`]+)`/g, '<code style="background-color:#f5f5f5;padding:2px 6px;border-radius:3px;font-family:monospace;color:#e53e3e;">$1</code>')
|
||||
html = html.replace(/`([^`]+)`/g, '<code style="background-color:#f5f5f5;padding:2px 6px;border-radius:3px;font-family:monospace;color:#e53e3e;word-break:break-all;">$1</code>')
|
||||
|
||||
// 处理链接([text](url)语法)
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#0055AA;text-decoration:underline;">$1</a>')
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#0055AA;text-decoration:underline;word-break:break-all;">$1</a>')
|
||||
|
||||
// 处理标题(# ## ###等)
|
||||
html = html.replace(/^### (.+)$/gm, '<div style="font-size:16px;font-weight:600;margin:8px 0 4px;">$1</div>')
|
||||
@@ -452,7 +495,8 @@ function renderMarkdown(text: string): string {
|
||||
// 处理换行
|
||||
html = html.replace(/\n/g, '<br/>')
|
||||
|
||||
return html
|
||||
// 包裹在带有换行样式的容器中
|
||||
return `<div style="word-break:break-all;overflow-wrap:break-word;white-space:pre-wrap;">${html}</div>`
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
@@ -517,7 +561,14 @@ async function sendMessage() {
|
||||
|
||||
// 滚动到底部
|
||||
function scrollToBottom() {
|
||||
// uni-app 的 scroll-view 需要 scroll-top 值发生变化才会触发滚动
|
||||
// 先重置为 0
|
||||
scrollTop.value = 0
|
||||
// 使用 setTimeout 确保重置生效
|
||||
setTimeout(() => {
|
||||
// 使用一个足够大的值确保滚动到底部
|
||||
scrollTop.value = 999999
|
||||
}, 50)
|
||||
}
|
||||
|
||||
// 处理工单操作
|
||||
@@ -539,8 +590,8 @@ function handleWorkcaseAction() {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 跳转到创建工单页面
|
||||
const url = `/pages/workcase/workcaseDetail/workcaseDetail?mode=create&roomId=${roomId.value}`
|
||||
// 跳转到创建工单页面,携带 deviceCode
|
||||
const url = `/pages/workcase/workcaseDetail/workcaseDetail?mode=create&roomId=${roomId.value}&deviceCode=${encodeURIComponent(deviceCode.value || '')}`
|
||||
console.log('[handleWorkcaseAction] 创建工单,跳转URL:', url)
|
||||
uni.navigateTo({
|
||||
url: url,
|
||||
@@ -644,6 +695,31 @@ async function handleJoinMeeting(meetingId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理评分提交
|
||||
async function handleCommentSubmit(rating: number) {
|
||||
console.log('[handleCommentSubmit] 提交评分:', rating)
|
||||
try {
|
||||
const result = await workcaseChatAPI.submitComment(roomId.value, rating)
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '感谢您的评分!',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || '评分提交失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[handleCommentSubmit] 评分提交失败:', error)
|
||||
uni.showToast({
|
||||
title: '评分提交失败,请稍后重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
@@ -711,6 +787,16 @@ function handleNewMessage(message: ChatRoomMessageVO) {
|
||||
return
|
||||
}
|
||||
|
||||
// 会议消息延时处理,等待数据库事务提交
|
||||
if (message.messageType === 'meet') {
|
||||
console.log('[chatRoom] 收到会议消息,延时1秒后刷新')
|
||||
setTimeout(async () => {
|
||||
// 重新加载最新消息,确保获取到完整的会议消息数据
|
||||
await loadMessages()
|
||||
}, 1000)
|
||||
return
|
||||
}
|
||||
|
||||
// 添加新消息到列表
|
||||
messages.push(message)
|
||||
nextTick(() => scrollToBottom())
|
||||
|
||||
@@ -261,6 +261,15 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
// 消息内容包装器(包含气泡和文件列表)
|
||||
.message-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.bot-message-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -328,6 +337,17 @@
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
// rich-text 组件样式(用于 Markdown 渲染)
|
||||
.message-rich-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: #999999;
|
||||
@@ -426,18 +446,121 @@
|
||||
.chat-input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
// 已上传文件预览区
|
||||
.uploaded-files {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px 8px 4px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.uploaded-file-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 80px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.file-preview-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-preview-doc {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.doc-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.remove-file-btn {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background: #ff4d4f;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// 输入行(上传按钮+输入框+发送按钮)
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.upload-btn.uploading {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
@@ -458,6 +581,16 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.send-btn.active {
|
||||
background: linear-gradient(135deg, #5b9eff 0%, #4b87ff 100%);
|
||||
border-color: #4b87ff;
|
||||
}
|
||||
|
||||
.send-btn.active .send-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.send-icon {
|
||||
@@ -465,6 +598,65 @@
|
||||
color: #4b87ff;
|
||||
}
|
||||
|
||||
// 消息中的文件列表
|
||||
.message-files {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-file-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.file-thumb {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.file-thumb.image {
|
||||
border: 1px solid #e5e5e5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.file-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-thumb.doc {
|
||||
background: #f5f5f5;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.file-name-small {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
// 打字指示器动画
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
@@ -504,3 +696,110 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 设备代码输入弹窗样式
|
||||
.device-code-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
width: 80%;
|
||||
max-width: 320px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.device-code-input {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
background-color: #f9f9f9;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.device-code-input:focus {
|
||||
border-color: #007AFF;
|
||||
background-color: white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modal-btn.cancel {
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.modal-btn.cancel:active {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.modal-btn.confirm {
|
||||
background-color: #007AFF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-btn.confirm:active {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.modal-btn .btn-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -52,9 +52,24 @@
|
||||
:class="item.type === 'user' ? 'user-message' : 'bot-message'">
|
||||
<!-- 用户消息(右侧) -->
|
||||
<view class="user-message-content" v-if="item.type === 'user'">
|
||||
<view class="message-bubble user-bubble">
|
||||
<view class="message-content-wrapper">
|
||||
<!-- 文字气泡 -->
|
||||
<view class="message-bubble user-bubble" v-if="item.content">
|
||||
<text class="message-text">{{item.content}}</text>
|
||||
</view>
|
||||
<!-- 用户消息的文件列表(在气泡外面) -->
|
||||
<view v-if="item.files && item.files.length > 0" class="message-files">
|
||||
<view v-for="fileId in item.files" :key="fileId" class="message-file-item" @tap="previewFile(fileId)">
|
||||
<view v-if="isImageFileById(fileId)" class="file-thumb image">
|
||||
<image :src="getFileDownloadUrl(fileId)" mode="aspectFill" class="file-img" />
|
||||
</view>
|
||||
<view v-else class="file-thumb doc">
|
||||
<text class="file-icon">📄</text>
|
||||
<text class="file-name-small">{{getFileName(fileId)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="avatar user-avatar">
|
||||
<text class="avatar-text">我</text>
|
||||
</view>
|
||||
@@ -103,27 +118,78 @@
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="chat-input-wrap">
|
||||
<!-- 已上传文件预览 -->
|
||||
<view v-if="uploadedFiles.length > 0" class="uploaded-files">
|
||||
<view v-for="(file, index) in uploadedFiles" :key="file.id || index" class="uploaded-file-item">
|
||||
<view v-if="isImageFile(file)" class="file-preview-image">
|
||||
<image :src="getFilePreviewUrl(file)" mode="aspectFill" class="preview-img" />
|
||||
</view>
|
||||
<view v-else class="file-preview-doc">
|
||||
<text class="doc-icon">📄</text>
|
||||
</view>
|
||||
<text class="file-name">{{file.name || '文件'}}</text>
|
||||
<view class="remove-file-btn" @tap="removeUploadedFile(index)">
|
||||
<text class="remove-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 输入框 -->
|
||||
<view class="input-row">
|
||||
<view class="upload-btn" :class="{ uploading: isUploading }" @tap="showUploadOptions">
|
||||
<text v-if="isUploading" class="upload-icon">⏳</text>
|
||||
<text v-else class="upload-icon">📎</text>
|
||||
</view>
|
||||
<input class="chat-input" v-model="inputText" placeholder="输入问题 来问问我~" @confirm="sendMessage" />
|
||||
<view class="send-btn" @tap="sendMessage">
|
||||
<view class="send-btn" :class="{ active: inputText.trim() || uploadedFiles.length > 0 }" @tap="sendMessage">
|
||||
<text class="send-icon">➤</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 设备代码输入弹窗 -->
|
||||
<view class="device-code-modal" v-if="showDeviceCodeDialog">
|
||||
<view class="modal-mask" @tap="cancelDeviceCodeInput"></view>
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">请输入设备代码</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<input
|
||||
class="device-code-input"
|
||||
v-model="deviceCodeInput"
|
||||
placeholder="请输入设备代码"
|
||||
focus
|
||||
@confirm="confirmDeviceCodeInput"
|
||||
/>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="modal-btn cancel" @tap="cancelDeviceCodeInput">
|
||||
<text class="btn-text">取消</text>
|
||||
</button>
|
||||
<button class="modal-btn confirm" @tap="confirmDeviceCodeInput">
|
||||
<text class="btn-text">确定</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted } from 'vue'
|
||||
import { guestAPI, aiChatAPI, workcaseChatAPI } from '@/api'
|
||||
import type { TbWorkcaseDTO } from '@/types'
|
||||
import { AGENT_ID } from '@/config'
|
||||
import type { DifyFileInfo } from '@/types/ai/aiChat'
|
||||
import { AGENT_ID, FILE_DOWNLOAD_URL } from '@/config'
|
||||
// 前端消息展示类型
|
||||
interface ChatMessageItem {
|
||||
type: 'user' | 'bot'
|
||||
content: string
|
||||
time: string
|
||||
actions?: string[] | null
|
||||
files?: string[] // 文件ID数组
|
||||
}
|
||||
const agentId = AGENT_ID
|
||||
// 响应式数据
|
||||
@@ -136,6 +202,12 @@
|
||||
const headerPaddingTop = ref<number>(44) // header顶部padding,默认44px
|
||||
const headerTotalHeight = ref<number>(76) // header总高度,默认76px
|
||||
|
||||
// 文件上传相关
|
||||
const uploadedFiles = ref<DifyFileInfo[]>([])
|
||||
const isUploading = ref<boolean>(false)
|
||||
// 文件信息缓存 (fileId -> DifyFileInfo)
|
||||
const fileInfoCache = ref<Map<string, DifyFileInfo>>(new Map())
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref({
|
||||
wechatId: '',
|
||||
@@ -149,6 +221,12 @@
|
||||
const chatId = ref<string>('') // 当前会话ID
|
||||
const currentTaskId = ref<string>('') // 当前任务ID(用于停止)
|
||||
|
||||
// 设备代码相关
|
||||
const deviceCode = ref<string>('') // 设备代码
|
||||
const showDeviceCodeDialog = ref<boolean>(false) // 是否显示设备代码输入弹窗
|
||||
const deviceCodeInput = ref<string>('') // 弹窗中的设备代码输入
|
||||
const pendingAction = ref<'workcase' | 'human' | ''>('') // 待执行的操作类型
|
||||
|
||||
// 初始化用户信息
|
||||
async function initUserInfo() {
|
||||
// #ifdef MP-WEIXIN
|
||||
@@ -257,18 +335,38 @@
|
||||
// 发送消息
|
||||
async function sendMessage() {
|
||||
const text = inputText.value.trim()
|
||||
if (!text || isTyping.value) return
|
||||
// 允许只有文件或只有文本
|
||||
if ((!text && uploadedFiles.value.length === 0) || isTyping.value) return
|
||||
|
||||
// 添加用户消息
|
||||
addMessage('user', text)
|
||||
const query = text || '[文件]'
|
||||
const currentFiles = [...uploadedFiles.value] // 保存当前文件列表副本
|
||||
|
||||
// 将文件信息缓存起来,用于立即渲染
|
||||
currentFiles.forEach(f => {
|
||||
if (f.sys_file_id) {
|
||||
fileInfoCache.value.set(f.sys_file_id, f)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加用户消息(包含文件)
|
||||
const userMessage: ChatMessageItem = {
|
||||
type: 'user',
|
||||
content: text,
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
files: currentFiles.length > 0 ? currentFiles.map(item => item.sys_file_id || '') : undefined
|
||||
}
|
||||
messages.value.push(userMessage)
|
||||
inputText.value = ''
|
||||
|
||||
// 调用AI聊天接口
|
||||
await callAIChat(text)
|
||||
// 清空已上传的文件
|
||||
uploadedFiles.value = []
|
||||
|
||||
// 调用AI聊天接口(携带文件)
|
||||
await callAIChat(query, currentFiles)
|
||||
}
|
||||
|
||||
// 调用AI聊天接口
|
||||
async function callAIChat(query : string) {
|
||||
async function callAIChat(query : string, files : DifyFileInfo[] = []) {
|
||||
isTyping.value = true
|
||||
|
||||
try {
|
||||
@@ -288,14 +386,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 准备流式对话
|
||||
const prepareRes = await aiChatAPI.prepareChatMessageSession({
|
||||
// 准备流式对话(包含文件)
|
||||
const prepareData: ChatPrepareData = {
|
||||
chatId: chatId.value,
|
||||
query: query,
|
||||
agentId: agentId,
|
||||
userType: userType.value,
|
||||
userId: userInfo.value.userId
|
||||
})
|
||||
userId: userInfo.value.userId,
|
||||
files: files.length > 0 ? files : undefined,
|
||||
service: "workcase"
|
||||
}
|
||||
console.log('准备流式对话参数:', JSON.stringify(prepareData))
|
||||
|
||||
const prepareRes = await aiChatAPI.prepareChatMessageSession(prepareData)
|
||||
if (!prepareRes.success || !prepareRes.data) {
|
||||
throw new Error(prepareRes.message || '准备对话失败')
|
||||
}
|
||||
@@ -419,10 +522,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 直接跳转到工单详情页的 create 模式(复用 workcaseDetail 页面)
|
||||
async function showCreator() {
|
||||
// 首页直接创建工单:为了让工单和聊天室绑定,这里先创建一个聊天室(workcase类型),再带 roomId 跳转
|
||||
// 如果你希望“无聊天室也能创建工单”,后端 WorkcaseServiceImpl 也支持 roomId 为空时自动创建聊天室
|
||||
// 检查并获取设备代码
|
||||
function checkDeviceCode(action: 'workcase' | 'human') {
|
||||
if (!deviceCode.value) {
|
||||
// 如果没有设备代码,显示输入弹窗
|
||||
pendingAction.value = action
|
||||
deviceCodeInput.value = ''
|
||||
showDeviceCodeDialog.value = true
|
||||
} else {
|
||||
// 如果已有设备代码,直接执行对应操作
|
||||
if (action === 'workcase') {
|
||||
doCreateWorkcase()
|
||||
} else {
|
||||
doContactHuman()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 确认输入设备代码
|
||||
function confirmDeviceCodeInput() {
|
||||
if (!deviceCodeInput.value.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入设备代码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
deviceCode.value = deviceCodeInput.value.trim()
|
||||
showDeviceCodeDialog.value = false
|
||||
|
||||
// 执行待处理的操作
|
||||
if (pendingAction.value === 'workcase') {
|
||||
doCreateWorkcase()
|
||||
} else if (pendingAction.value === 'human') {
|
||||
doContactHuman()
|
||||
}
|
||||
pendingAction.value = ''
|
||||
}
|
||||
|
||||
// 取消输入设备代码
|
||||
function cancelDeviceCodeInput() {
|
||||
showDeviceCodeDialog.value = false
|
||||
deviceCodeInput.value = ''
|
||||
pendingAction.value = ''
|
||||
}
|
||||
|
||||
// 实际创建工单
|
||||
async function doCreateWorkcase() {
|
||||
uni.showLoading({ title: '正在创建工单...' })
|
||||
try {
|
||||
const res = await workcaseChatAPI.createChatRoom({
|
||||
@@ -431,6 +578,7 @@
|
||||
roomName: `${userInfo.value.username || '访客'}的工单`,
|
||||
roomType: 'workcase',
|
||||
status: 'active',
|
||||
deviceCode: deviceCode.value,
|
||||
aiSessionId: chatId.value || ''
|
||||
})
|
||||
uni.hideLoading()
|
||||
@@ -451,6 +599,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 实际联系人工
|
||||
async function doContactHuman() {
|
||||
uni.showLoading({ title: '正在连接客服...' })
|
||||
try {
|
||||
// 创建聊天室
|
||||
const res = await workcaseChatAPI.createChatRoom({
|
||||
guestId: userInfo.value.userId || userInfo.value.wechatId,
|
||||
guestName: userInfo.value.username || '访客',
|
||||
roomName: `${userInfo.value.username || '访客'}的咨询`,
|
||||
roomType: 'guest',
|
||||
status: 'active',
|
||||
deviceCode: deviceCode.value,
|
||||
aiSessionId: chatId.value || ''
|
||||
})
|
||||
uni.hideLoading()
|
||||
|
||||
if (res.success && res.data) {
|
||||
const roomId = res.data.roomId
|
||||
console.log('创建聊天室成功:', roomId)
|
||||
// 跳转到聊天室页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${roomId}&roomName=${encodeURIComponent(res.data.roomName || '人工客服')}`
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || '连接客服失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
uni.hideLoading()
|
||||
console.error('创建聊天室失败:', error)
|
||||
uni.showToast({
|
||||
title: '连接客服失败,请稍后重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 直接跳转到工单详情页的 create 模式(复用 workcaseDetail 页面)
|
||||
async function showCreator() {
|
||||
// 检查设备代码
|
||||
checkDeviceCode('workcase')
|
||||
}
|
||||
|
||||
// 兼容旧逻辑:不再使用页面内工单创建器
|
||||
function hideCreator() {
|
||||
showWorkcaseCreator.value = false
|
||||
@@ -487,40 +680,8 @@
|
||||
|
||||
// 联系人工客服 - 创建聊天室并进入
|
||||
async function contactHuman() {
|
||||
uni.showLoading({ title: '正在连接客服...' })
|
||||
try {
|
||||
// 创建聊天室
|
||||
const res = await workcaseChatAPI.createChatRoom({
|
||||
guestId: userInfo.value.userId || userInfo.value.wechatId,
|
||||
guestName: userInfo.value.username || '访客',
|
||||
roomName: `${userInfo.value.username || '访客'}的咨询`,
|
||||
roomType: 'guest',
|
||||
status: 'active',
|
||||
aiSessionId: chatId.value || ''
|
||||
})
|
||||
uni.hideLoading()
|
||||
|
||||
if (res.success && res.data) {
|
||||
const roomId = res.data.roomId
|
||||
console.log('创建聊天室成功:', roomId)
|
||||
// 跳转到聊天室页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${roomId}&roomName=${encodeURIComponent(res.data.roomName || '人工客服')}`
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || '连接客服失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
uni.hideLoading()
|
||||
console.error('创建聊天室失败:', error)
|
||||
uni.showToast({
|
||||
title: '连接客服失败,请稍后重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
// 检查设备代码
|
||||
checkDeviceCode('human')
|
||||
}
|
||||
|
||||
// 处理快速问题
|
||||
@@ -529,7 +690,7 @@
|
||||
await callAIChat(question)
|
||||
}
|
||||
|
||||
// Markdown渲染函数(返回富文本节点)
|
||||
// Markdown渲染函数(返回富文本节点)
|
||||
function renderMarkdown(text : string) : string {
|
||||
if (!text) return ''
|
||||
|
||||
@@ -539,30 +700,31 @@
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
// 处理粗体(**语法)
|
||||
// 处理粗体(**语法)
|
||||
html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>')
|
||||
|
||||
// 处理斜体(*语法,但要避免和粗体冲突)
|
||||
// 处理斜体(*语法,但要避免和粗体冲突)
|
||||
html = html.replace(/(?<!\*)\*([^\*]+)\*(?!\*)/g, '<em>$1</em>')
|
||||
|
||||
// 处理行内代码(`语法)
|
||||
html = html.replace(/`([^`]+)`/g, '<code style="background-color:#f5f5f5;padding:2px 6px;border-radius:3px;font-family:monospace;color:#e53e3e;">$1</code>')
|
||||
// 处理行内代码(`语法)
|
||||
html = html.replace(/`([^`]+)`/g, '<code style="background-color:#f5f5f5;padding:2px 6px;border-radius:3px;font-family:monospace;color:#e53e3e;word-break:break-all;">$1</code>')
|
||||
|
||||
// 处理链接([text](url)语法)
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#0055AA;text-decoration:underline;">$1</a>')
|
||||
// 处理链接([text](url)语法)
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#0055AA;text-decoration:underline;word-break:break-all;overflow-wrap:break-word;">$1</a>')
|
||||
|
||||
// 处理标题(# ## ###等)
|
||||
html = html.replace(/^### (.+)$/gm, '<div style="font-size:16px;font-weight:600;margin:8px 0 4px;">$1</div>')
|
||||
html = html.replace(/^## (.+)$/gm, '<div style="font-size:18px;font-weight:600;margin:10px 0 6px;">$1</div>')
|
||||
html = html.replace(/^# (.+)$/gm, '<div style="font-size:20px;font-weight:700;margin:12px 0 8px;">$1</div>')
|
||||
// 处理标题(# ## ###等)
|
||||
html = html.replace(/^### (.+)$/gm, '<div style="font-size:16px;font-weight:600;margin:8px 0 4px;word-break:break-word;">$1</div>')
|
||||
html = html.replace(/^## (.+)$/gm, '<div style="font-size:18px;font-weight:600;margin:10px 0 6px;word-break:break-word;">$1</div>')
|
||||
html = html.replace(/^# (.+)$/gm, '<div style="font-size:20px;font-weight:700;margin:12px 0 8px;word-break:break-word;">$1</div>')
|
||||
|
||||
// 处理无序列表(- 或 * 开头)
|
||||
html = html.replace(/^[*-] (.+)$/gm, '<div style="margin-left:16px;">• $1</div>')
|
||||
// 处理无序列表(- 或 * 开头)
|
||||
html = html.replace(/^[*-] (.+)$/gm, '<div style="margin-left:16px;word-break:break-word;">• $1</div>')
|
||||
|
||||
// 处理换行
|
||||
html = html.replace(/\n/g, '<br/>')
|
||||
|
||||
return html
|
||||
// 包裹在一个具有换行样式的容器中
|
||||
return `<div style="word-break:break-word;overflow-wrap:break-word;white-space:normal;max-width:100%;">${html}</div>`
|
||||
}
|
||||
|
||||
// 显示上传选项
|
||||
@@ -594,10 +756,9 @@
|
||||
count: 1,
|
||||
sourceType: ['camera'],
|
||||
success: (res) => {
|
||||
// 处理图片上传逻辑
|
||||
console.log('选择的图片:', res.tempFilePaths)
|
||||
addMessage('user', '[图片]')
|
||||
simulateAIResponse('收到您发送的图片')
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
uploadSingleFile(res.tempFilePaths[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -605,25 +766,178 @@
|
||||
// 从相册选择
|
||||
function chooseImageFromAlbum() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
count: 9,
|
||||
sourceType: ['album'],
|
||||
success: (res) => {
|
||||
// 处理图片上传逻辑
|
||||
console.log('选择的图片:', res.tempFilePaths)
|
||||
addMessage('user', '[图片]')
|
||||
simulateAIResponse('收到您发送的图片')
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
res.tempFilePaths.forEach((filePath: string) => {
|
||||
uploadSingleFile(filePath)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 选择文件
|
||||
function chooseFile() {
|
||||
// 这里可以扩展文件选择功能
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序使用 chooseMessageFile
|
||||
uni.chooseMessageFile({
|
||||
count: 5,
|
||||
type: 'file',
|
||||
extension: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt'],
|
||||
success: (res: any) => {
|
||||
console.log('选择文件成功:', res)
|
||||
if (res.tempFiles && res.tempFiles.length > 0) {
|
||||
res.tempFiles.forEach((file: any) => {
|
||||
uploadSingleFile(file.path)
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('选择文件失败:', err)
|
||||
uni.showToast({
|
||||
title: '文件选择功能开发中',
|
||||
title: '选择文件失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// 非微信小程序环境
|
||||
// @ts-ignore
|
||||
if (typeof uni.chooseFile === 'function') {
|
||||
// @ts-ignore
|
||||
uni.chooseFile({
|
||||
count: 5,
|
||||
extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt'],
|
||||
success: (res: any) => {
|
||||
console.log('选择文件成功:', res)
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
res.tempFilePaths.forEach((filePath: string) => {
|
||||
uploadSingleFile(filePath)
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('选择文件失败:', err)
|
||||
uni.showToast({
|
||||
title: '选择文件失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '当前环境不支持文件选择',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 上传单个文件
|
||||
async function uploadSingleFile(filePath: string) {
|
||||
console.log('开始上传文件:', filePath)
|
||||
|
||||
if (!agentId) {
|
||||
uni.showToast({ title: '智能体未配置', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
isUploading.value = true
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
|
||||
try {
|
||||
const result = await aiChatAPI.uploadFileForChat(filePath, agentId)
|
||||
console.log('上传结果:', result)
|
||||
|
||||
if (result.success && result.data) {
|
||||
uploadedFiles.value.push(result.data)
|
||||
uni.showToast({ title: '上传成功', icon: 'success', duration: 1000 })
|
||||
} else {
|
||||
uni.showToast({ title: result.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('文件上传失败:', error)
|
||||
uni.showToast({ title: '上传失败: ' + (error.message || '未知错误'), icon: 'none' })
|
||||
} finally {
|
||||
isUploading.value = false
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 移除已上传的文件
|
||||
function removeUploadedFile(index: number) {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 判断是否为图片文件
|
||||
function isImageFile(file: DifyFileInfo): boolean {
|
||||
return file.type === 'image' || file.mime_type?.startsWith('image/') || false
|
||||
}
|
||||
|
||||
// 获取文件预览URL
|
||||
function getFilePreviewUrl(file: DifyFileInfo): string {
|
||||
return file.preview_url || file.source_url || file.url || ''
|
||||
}
|
||||
|
||||
// 获取文件下载URL(通过文件ID)
|
||||
function getFileDownloadUrl(fileId: string): string {
|
||||
return `${FILE_DOWNLOAD_URL}${fileId}`
|
||||
}
|
||||
|
||||
// 判断文件ID对应的文件是否为图片
|
||||
function isImageFileById(fileId: string): boolean {
|
||||
// 从缓存中查找文件信息
|
||||
const file = fileInfoCache.value.get(fileId)
|
||||
if (file) {
|
||||
return isImageFile(file)
|
||||
}
|
||||
// 如果缓存中没有,尝试从uploadedFiles中查找
|
||||
const uploadedFile = uploadedFiles.value.find(f => f.sys_file_id === fileId)
|
||||
if (uploadedFile) {
|
||||
return isImageFile(uploadedFile)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取文件名(从缓存)
|
||||
function getFileName(fileId: string): string {
|
||||
const file = fileInfoCache.value.get(fileId)
|
||||
return file?.name || fileId.substring(0, 8) + '...'
|
||||
}
|
||||
|
||||
// 文件预览
|
||||
function previewFile(fileId: string) {
|
||||
const url = getFileDownloadUrl(fileId)
|
||||
// 如果是图片,使用图片预览
|
||||
if (isImageFileById(fileId)) {
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
current: url
|
||||
})
|
||||
} else {
|
||||
// 其他文件,提示下载或打开
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '是否下载该文件?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.downloadFile({
|
||||
url: url,
|
||||
success: (downloadRes) => {
|
||||
if (downloadRes.statusCode === 200) {
|
||||
uni.showToast({ title: '下载成功', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,116 +1,185 @@
|
||||
.meeting-create-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
padding-bottom: 120rpx;
|
||||
.page {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background-color: #fff;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
padding-bottom: 16rpx;
|
||||
z-index: 100;
|
||||
border-bottom: 1rpx solid #e5e7eb;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
height: 64rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-back-icon {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left-width: 4rpx;
|
||||
border-left-style: solid;
|
||||
border-left-color: #333;
|
||||
border-bottom-width: 4rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #333;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
font-size: 34rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
line-height: 64rpx;
|
||||
}
|
||||
|
||||
.nav-capsule {
|
||||
width: 174rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx;
|
||||
padding-bottom: 140rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
// 表单容器
|
||||
.form-container {
|
||||
background-color: #fff;
|
||||
margin-top: 16rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.form-item.required .label-text::after {
|
||||
content: ' *';
|
||||
color: #f56c6c;
|
||||
padding: 24rpx;
|
||||
border-bottom-width: 1rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
color: #6b7280;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
font-size: 28rpx;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.required-star {
|
||||
color: #f56c6c;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 16rpx 24rpx;
|
||||
border: 1px solid #dcdfe6;
|
||||
padding: 0 24rpx;
|
||||
height: 68rpx;
|
||||
background-color: #f9fafb;
|
||||
border-width: 1rpx;
|
||||
border-style: solid;
|
||||
border-color: #e5e7eb;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.picker-display {
|
||||
padding: 16rpx 24rpx;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.picker-display .placeholder {
|
||||
color: #c0c4cc;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.form-tip text {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
color: #9ca3af;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
.required {
|
||||
color: #ef4444;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.picker-content {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 68rpx;
|
||||
padding: 0 24rpx;
|
||||
background-color: #f9fafb;
|
||||
border-width: 1rpx;
|
||||
border-style: solid;
|
||||
border-color: #e5e7eb;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.picker-text.placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #9ca3af;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
// 底部占位
|
||||
.footer-placeholder {
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
// 底部操作栏
|
||||
.footer-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx;
|
||||
bottom: 0;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #ebeef5;
|
||||
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
border-top-width: 1rpx;
|
||||
border-top-style: solid;
|
||||
border-top-color: #e5e7eb;
|
||||
padding: 24rpx;
|
||||
flex-direction: row;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.btn {
|
||||
.action-button {
|
||||
flex: 1;
|
||||
padding: 24rpx 0;
|
||||
height: 80rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 32rpx;
|
||||
text-align: center;
|
||||
border: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-width: 1rpx;
|
||||
border-style: solid;
|
||||
border-color: #e5e7eb;
|
||||
background-color: #fff;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: #f5f7fa;
|
||||
color: #606266;
|
||||
margin-right: 16rpx;
|
||||
.action-button.primary {
|
||||
background-color: #4b87ff;
|
||||
border-color: #4b87ff;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background-color: #409eff;
|
||||
.action-button.primary .button-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-submit[loading] {
|
||||
opacity: 0.7;
|
||||
.button-text {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
<template>
|
||||
<view class="meeting-create-page">
|
||||
<view class="page-header">
|
||||
<text class="page-title">创建视频会议</text>
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
<view class="page">
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
|
||||
<view class="nav-back" @tap="handleCancel">
|
||||
<view class="nav-back-icon"></view>
|
||||
</view>
|
||||
<text class="nav-title">创建视频会议</text>
|
||||
<view class="nav-capsule"></view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<scroll-view class="content" scroll-y="true" :style="{ marginTop: headerTotalHeight + 'px' }">
|
||||
<!-- 表单区域 -->
|
||||
<view class="section">
|
||||
<view class="form-container">
|
||||
<!-- 会议名称 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-text">会议名称</text>
|
||||
</view>
|
||||
<text class="form-label">会议名称</text>
|
||||
<input
|
||||
v-model="formData.meetingName"
|
||||
class="form-input"
|
||||
@@ -19,66 +29,56 @@
|
||||
</view>
|
||||
|
||||
<!-- 开始时间 -->
|
||||
<view class="form-item required">
|
||||
<view class="form-label">
|
||||
<text class="label-text">开始时间</text>
|
||||
<text class="required-star">*</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">开始时间<text class="required">*</text></text>
|
||||
<picker
|
||||
mode="multiSelector"
|
||||
:value="startTimePickerValue"
|
||||
:range="timePickerRange"
|
||||
@change="handleStartTimeChange"
|
||||
>
|
||||
<view class="picker-display">
|
||||
<text :class="formData.startTime ? '' : 'placeholder'">
|
||||
<view class="picker-content">
|
||||
<text class="picker-text" :class="{ placeholder: !formData.startTime }">
|
||||
{{ formData.startTime || '请选择开始时间' }}
|
||||
</text>
|
||||
<text class="picker-arrow">></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 结束时间 -->
|
||||
<view class="form-item required">
|
||||
<view class="form-label">
|
||||
<text class="label-text">结束时间</text>
|
||||
<text class="required-star">*</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">结束时间<text class="required">*</text></text>
|
||||
<picker
|
||||
mode="multiSelector"
|
||||
:value="endTimePickerValue"
|
||||
:range="timePickerRange"
|
||||
@change="handleEndTimeChange"
|
||||
>
|
||||
<view class="picker-display">
|
||||
<text :class="formData.endTime ? '' : 'placeholder'">
|
||||
<view class="picker-content">
|
||||
<text class="picker-text" :class="{ placeholder: !formData.endTime }">
|
||||
{{ formData.endTime || '请选择结束时间' }}
|
||||
</text>
|
||||
<text class="picker-arrow">></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 提前入会 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-text">提前入会(分钟)</text>
|
||||
</view>
|
||||
<text class="form-label">提前入会(分钟)</text>
|
||||
<input
|
||||
v-model.number="formData.advance"
|
||||
class="form-input"
|
||||
type="number"
|
||||
placeholder="提前入会时间"
|
||||
/>
|
||||
<view class="form-tip">
|
||||
<text>用户可在会议开始前N分钟加入</text>
|
||||
</view>
|
||||
<text class="form-tip">用户可在会议开始前N分钟加入</text>
|
||||
</view>
|
||||
|
||||
<!-- 会议密码 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-text">会议密码</text>
|
||||
</view>
|
||||
<text class="form-label">会议密码</text>
|
||||
<input
|
||||
v-model="formData.meetingPassword"
|
||||
class="form-input"
|
||||
@@ -91,9 +91,7 @@
|
||||
|
||||
<!-- 最大人数 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-text">最大人数</text>
|
||||
</view>
|
||||
<text class="form-label">最大人数</text>
|
||||
<input
|
||||
v-model.number="formData.maxParticipants"
|
||||
class="form-input"
|
||||
@@ -102,21 +100,37 @@
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="form-footer">
|
||||
<button class="btn btn-cancel" @click="handleCancel">取消</button>
|
||||
<button class="btn btn-submit" :loading="submitting" @click="handleSubmit">创建会议</button>
|
||||
<!-- 底部占位 -->
|
||||
<view class="footer-placeholder"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="footer-actions">
|
||||
<view class="action-button" @tap="handleCancel">
|
||||
<text class="button-text">取消</text>
|
||||
</view>
|
||||
<view class="action-button primary" @tap="handleSubmit">
|
||||
<text class="button-text">创建会议</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { workcaseChatAPI } from '../../../api/workcase/workcaseChat'
|
||||
import type { CreateMeetingParam } from '../../../types/workcase/chatRoom'
|
||||
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
const headerTotalHeight = ref<number>(88)
|
||||
|
||||
// 路由参数
|
||||
const roomId = ref('')
|
||||
const workcaseId = ref('')
|
||||
@@ -136,8 +150,8 @@ const formData = reactive<CreateMeetingParam>({
|
||||
const submitting = ref(false)
|
||||
|
||||
// 时间选择器数据
|
||||
const startTimePickerValue = ref([0, 0, 0, 0])
|
||||
const endTimePickerValue = ref([0, 0, 0, 0])
|
||||
const startTimePickerValue = ref([0, 0, 0])
|
||||
const endTimePickerValue = ref([0, 0, 0])
|
||||
|
||||
// 生成时间选择器范围
|
||||
const timePickerRange = computed(() => {
|
||||
@@ -179,6 +193,26 @@ onLoad((options: any) => {
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
const statusBarHeight = windowInfo.statusBarHeight || 44
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
headerPaddingTop.value = menuButtonInfo.top
|
||||
headerTotalHeight.value = menuButtonInfo.bottom + 8
|
||||
} catch (e) {
|
||||
headerPaddingTop.value = statusBarHeight
|
||||
headerTotalHeight.value = statusBarHeight + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
headerPaddingTop.value = statusBarHeight
|
||||
headerTotalHeight.value = statusBarHeight + 44
|
||||
// #endif
|
||||
})
|
||||
|
||||
// 处理开始时间选择
|
||||
function handleStartTimeChange(e: any) {
|
||||
const val = e.detail.value
|
||||
@@ -214,69 +248,45 @@ function handleEndTimeChange(e: any) {
|
||||
// 验证表单
|
||||
function validateForm(): boolean {
|
||||
if (!formData.startTime) {
|
||||
uni.showToast({
|
||||
title: '请选择开始时间',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '请选择开始时间', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!formData.endTime) {
|
||||
uni.showToast({
|
||||
title: '请选择结束时间',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '请选择结束时间', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
const start = new Date(formData.startTime).getTime()
|
||||
const end = new Date(formData.endTime).getTime()
|
||||
const start = new Date(formData.startTime.replace(' ', 'T')).getTime()
|
||||
const end = new Date(formData.endTime.replace(' ', 'T')).getTime()
|
||||
|
||||
if (start < Date.now()) {
|
||||
uni.showToast({
|
||||
title: '开始时间不能早于当前时间',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '开始时间不能早于当前时间', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (end <= start) {
|
||||
uni.showToast({
|
||||
title: '结束时间必须晚于开始时间',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '结束时间必须晚于开始时间', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (end - start < 5 * 60 * 1000) {
|
||||
uni.showToast({
|
||||
title: '会议时长不能少于5分钟',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '会议时长不能少于5分钟', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (end - start > 24 * 60 * 60 * 1000) {
|
||||
uni.showToast({
|
||||
title: '会议时长不能超过24小时',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '会议时长不能超过24小时', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (formData.advance !== undefined && (formData.advance < 0 || formData.advance > 60)) {
|
||||
uni.showToast({
|
||||
title: '提前入会时间范围为0-60分钟',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '提前入会时间范围为0-60分钟', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (formData.maxParticipants !== undefined && (formData.maxParticipants < 2 || formData.maxParticipants > 100)) {
|
||||
uni.showToast({
|
||||
title: '参与人数范围为2-100人',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '参与人数范围为2-100人', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -289,33 +299,24 @@ async function handleSubmit() {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
const result = await workcaseChatAPI.createVideoMeeting(formData)
|
||||
|
||||
if (result.success && result.data) {
|
||||
uni.showToast({
|
||||
title: '会议创建成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
uni.showToast({ title: '会议创建成功', icon: 'success' })
|
||||
// 延迟返回,让用户看到成功提示
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || '创建会议失败',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: result.message || '创建会议失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建会议失败:', error)
|
||||
uni.showToast({
|
||||
title: '创建会议失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '创建会议失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -327,6 +328,6 @@ function handleCancel() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import url('./MeetingCreate.scss')
|
||||
<style lang="scss" scoped>
|
||||
@import "./MeetingCreate.scss";
|
||||
</style>
|
||||
|
||||
@@ -95,14 +95,14 @@
|
||||
|
||||
<!-- 设备铭牌 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">设备铭牌</text>
|
||||
<text class="form-label">设备铭牌<text class="required" v-if="mode === 'create'">*</text></text>
|
||||
<input v-if="mode === 'create'" class="form-input" v-model="workcase.deviceNamePlate" placeholder="请输入设备铭牌"/>
|
||||
<text v-else class="form-value">{{ workcase.deviceNamePlate || '-' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 铭牌照片 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">铭牌照片<text class="required" v-if="mode === 'create'">*</text></text>
|
||||
<text class="form-label">铭牌照片</text>
|
||||
<!-- 创建模式:上传铭牌 -->
|
||||
<view v-if="mode === 'create'" class="nameplate-upload">
|
||||
<view v-if="workcase.deviceNamePlateImg" class="nameplate-preview" @tap="previewNameplateImage">
|
||||
@@ -164,8 +164,8 @@
|
||||
<view class="title-bar"></view>
|
||||
<text class="title-text">处理记录</text>
|
||||
</view>
|
||||
<!-- 处理人记录处理过程 -->
|
||||
<view v-if="isProcessor" class="add-process-section">
|
||||
<!-- 处理人记录处理过程 - 仅在处理中状态显示 -->
|
||||
<view v-if="isProcessor && workcase.status === 'processing'" class="add-process-section">
|
||||
<button class="add-process-btn" type="primary" size="mini" @tap="navigateToAddProcess">
|
||||
添加处理记录
|
||||
</button>
|
||||
@@ -380,7 +380,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO } from '@/types/workcase'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO, WorkcaseProcessVO } from '@/types/workcase'
|
||||
import type { CustomerServiceVO } from '@/types/workcase/chatRoom'
|
||||
import { workcaseAPI, fileAPI, workcaseChatAPI } from '@/api'
|
||||
|
||||
@@ -401,7 +401,7 @@ const userId = JSON.parse(uni.getStorageSync('loginDomain')).userInfo.userId
|
||||
const workcase = reactive<TbWorkcaseDTO>({})
|
||||
|
||||
// 处理记录
|
||||
const processList = reactive<TbWorkcaseProcessDTO[]>([])
|
||||
const processList = reactive<WorkcaseProcessVO[]>([])
|
||||
|
||||
// 文件信息缓存
|
||||
const fileInfoCache = reactive<Map<string, any>>(new Map())
|
||||
@@ -457,6 +457,7 @@ onLoad((options: any) => {
|
||||
mode.value = 'create'
|
||||
// create 模式必须从上一页带入 roomId(前端先建 room 的策略)
|
||||
const roomId = options.roomId || ''
|
||||
const deviceCodeParam = decodeURIComponent(options.deviceCode || '')
|
||||
if (!roomId) {
|
||||
uni.showToast({ title: '缺少roomId,无法创建工单', icon: 'none' })
|
||||
// 直接退出,避免后续提交失败
|
||||
@@ -485,6 +486,8 @@ onLoad((options: any) => {
|
||||
phone,
|
||||
userId,
|
||||
roomId,
|
||||
deviceCode: deviceCodeParam,
|
||||
deviceNamePlate: deviceCodeParam,
|
||||
device: '',
|
||||
type: '',
|
||||
address: '',
|
||||
@@ -1102,8 +1105,8 @@ async function submitWorkcase() {
|
||||
uni.showToast({ title: '请输入故障描述', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!workcase.deviceNamePlateImg) {
|
||||
uni.showToast({ title: '请上传设备铭牌照片', icon: 'none' })
|
||||
if (!workcase.deviceNamePlate) {
|
||||
uni.showToast({ title: '请输入设备铭牌', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!workcase.imgs || workcase.imgs.length === 0) {
|
||||
@@ -1236,15 +1239,15 @@ async function submitProcessRecord() {
|
||||
|
||||
submittingProcess.value = true
|
||||
try {
|
||||
const fileIds = processForm.files.map(f => f.fileId).join(',')
|
||||
const fileIds = processForm.files.map(f => f.fileId)
|
||||
const params: TbWorkcaseProcessDTO = {
|
||||
workcaseId: workcase.workcaseId,
|
||||
action: 'info',
|
||||
message: processForm.message,
|
||||
files: fileIds || undefined
|
||||
files: fileIds.length > 0 ? fileIds : undefined
|
||||
}
|
||||
|
||||
const res = await workcaseAPI.addWorkcaseProcess(params)
|
||||
const res = await workcaseAPI.createWorkcaseProcess(params)
|
||||
if (res.success) {
|
||||
uni.showToast({ title: '处理记录添加成功', icon: 'success' })
|
||||
closeAddProcessDialog()
|
||||
|
||||
@@ -12,27 +12,27 @@ export interface DifyFileInfo {
|
||||
/** 文件扩展名 */
|
||||
extension?: string
|
||||
/** 文件MIME类型 */
|
||||
mimeType?: string
|
||||
mime_type?: string
|
||||
/** 上传人ID */
|
||||
createdBy?: string
|
||||
created_by?: string
|
||||
/** 上传时间(时间戳) */
|
||||
createdAt?: number
|
||||
created_at?: number
|
||||
/** 预览URL */
|
||||
previewUrl?: string
|
||||
preview_url?: string
|
||||
/** 源文件URL */
|
||||
sourceUrl?: string
|
||||
source_url?: string
|
||||
/** 文件类型:image、document、audio、video、file */
|
||||
type?: string
|
||||
/** 传输方式:remote_url、local_file */
|
||||
transferMethod?: string
|
||||
transfer_method?: string
|
||||
/** 文件URL或ID */
|
||||
url?: string
|
||||
/** 本地文件上传ID */
|
||||
uploadFileId?: string
|
||||
upload_file_id?: string
|
||||
/** 系统文件ID */
|
||||
sysFileId?: string
|
||||
sys_file_id?: string
|
||||
/** 文件路径(从系统文件表获取) */
|
||||
filePath?: string
|
||||
file_path?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +87,7 @@ export interface ChatPrepareData {
|
||||
userId?: string
|
||||
/** 用户类型(false=来客,true=员工) */
|
||||
userType?: boolean
|
||||
service?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,23 +126,6 @@ export interface CreateChatParam {
|
||||
title?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备流式对话参数
|
||||
*/
|
||||
export interface PrepareChatParam {
|
||||
/** 对话ID(必传) */
|
||||
chatId: string
|
||||
/** 用户问题(必传) */
|
||||
query: string
|
||||
/** 智能体ID */
|
||||
agentId: string
|
||||
userType: boolean
|
||||
/** 用户ID */
|
||||
userId?: string
|
||||
/** 用户类型 */
|
||||
/** 文件列表 */
|
||||
files?: DifyFileInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话参数
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface TbChatRoomDTO extends BaseDTO {
|
||||
status?: string
|
||||
guestId?: string
|
||||
guestName?: string
|
||||
deviceCode?: string
|
||||
commentLevel?: number
|
||||
aiSessionId?: string
|
||||
currentAgentId?: string
|
||||
agentCount?: number
|
||||
@@ -164,6 +166,8 @@ export interface ChatRoomVO extends BaseVO {
|
||||
status?: string
|
||||
guestId?: string
|
||||
guestName?: string
|
||||
commentLevel?: string
|
||||
deviceCode?: string
|
||||
aiSessionId?: string
|
||||
currentAgentId?: string
|
||||
currentAgentName?: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BaseDTO } from '../base'
|
||||
import type { BaseDTO, BaseVO } from '../base'
|
||||
|
||||
/**
|
||||
* 工单表对象
|
||||
@@ -74,3 +74,23 @@ export interface TbWorkcaseDeviceDTO extends BaseDTO {
|
||||
/** 文件根ID */
|
||||
fileRootId?: string
|
||||
}
|
||||
|
||||
export interface WorkcaseProcessVO extends BaseVO {
|
||||
/** 工单ID */
|
||||
workcaseId?: string
|
||||
/** 过程ID */
|
||||
processId?: string
|
||||
/** 动作 info:记录,assign:指派,redeploy:转派,repeal:撤销,finish:完成 */
|
||||
action?: 'info' | 'assign' | 'redeploy' | 'repeal' | 'finish'
|
||||
/** 消息 */
|
||||
message?: string
|
||||
/** 携带文件列表 */
|
||||
files?: string[]
|
||||
/** 处理人(指派、转派专属) */
|
||||
processor?: string
|
||||
processorName?: string
|
||||
/** 创建时间 */
|
||||
createTime?: string
|
||||
/** 更新时间 */
|
||||
updateTime?: string
|
||||
}
|
||||
4
修改点.md
Normal file
4
修改点.md
Normal file
@@ -0,0 +1,4 @@
|
||||
1. createTableWorkcase.sql 修改了tb_chat_room 增加了device_code字段。修改相关dto\vo\xml。
|
||||
2. WorkcaseChatController.java 修改创建聊天室的接口,增加了deviceCode字段必传。
|
||||
3. 修改workcase/types/workcase/chatRoom.ts里的dto和vo。修改workcase_wechat/types/workcase/chatRoom.ts的dto和vo
|
||||
4. 修改workcase_wechat/pages/index/index.uvue。新增const deviceCode = ref('');只有这个有值时,才让用户创建聊天室和工单(工单自动填入表单),否则弹窗让用户填写
|
||||
Reference in New Issue
Block a user