AI 对话web、wx优化
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package org.xyzh.ai.service.impl;
|
package org.xyzh.ai.service.impl;
|
||||||
|
|
||||||
|
import org.apache.dubbo.config.annotation.DubboReference;
|
||||||
import org.apache.dubbo.config.annotation.DubboService;
|
import org.apache.dubbo.config.annotation.DubboService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.dto.TbAgent;
|
||||||
import org.xyzh.api.ai.service.AIFileUploadService;
|
import org.xyzh.api.ai.service.AIFileUploadService;
|
||||||
import org.xyzh.api.ai.service.AgentService;
|
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.auth.utils.LoginUtil;
|
||||||
import org.xyzh.common.core.domain.ResultDomain;
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description AI文件上传服务实现(只负责与Dify交互,不处理minio和数据库)
|
* @description AI文件上传服务实现(同时上传到MinIO和Dify)
|
||||||
* @filename AIFileUploadServiceImpl.java
|
* @filename AIFileUploadServiceImpl.java
|
||||||
* @author yslg
|
* @author yslg
|
||||||
* @copyright xyzh
|
* @copyright xyzh
|
||||||
@@ -37,6 +41,9 @@ public class AIFileUploadServiceImpl implements AIFileUploadService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private AgentService agentService;
|
private AgentService agentService;
|
||||||
|
|
||||||
|
@DubboReference(version = "1.0.0", group = "file", timeout = 30000, retries = 0)
|
||||||
|
private FileService fileService;
|
||||||
|
|
||||||
// ============================ 对话文件管理 ============================
|
// ============================ 对话文件管理 ============================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -56,31 +63,58 @@ public class AIFileUploadServiceImpl implements AIFileUploadService {
|
|||||||
}
|
}
|
||||||
TbAgent agent = agentResult.getData();
|
TbAgent agent = agentResult.getData();
|
||||||
|
|
||||||
|
// 3. 获取当前用户
|
||||||
|
String userId = LoginUtil.getCurrentUserId();
|
||||||
|
if (!StringUtils.hasText(userId)) {
|
||||||
|
userId = "anonymous";
|
||||||
|
}
|
||||||
|
|
||||||
File tempFile = null;
|
File tempFile = null;
|
||||||
|
String sysFileId = null;
|
||||||
|
String sysFileUrl = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 3. 将MultipartFile转换为临时File
|
// 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());
|
tempFile = File.createTempFile("upload_", "_" + file.getOriginalFilename());
|
||||||
file.transferTo(tempFile);
|
file.transferTo(tempFile);
|
||||||
|
|
||||||
// 4. 获取当前用户
|
// 6. 上传到Dify
|
||||||
String userId = LoginUtil.getCurrentUserId();
|
|
||||||
if (!StringUtils.hasText(userId)) {
|
|
||||||
userId = "anonymous";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 上传到Dify
|
|
||||||
DifyFileInfo difyFile = difyApiClient.uploadFileForChat(tempFile, file.getOriginalFilename(), userId, agent.getApiKey());
|
DifyFileInfo difyFile = difyApiClient.uploadFileForChat(tempFile, file.getOriginalFilename(), userId, agent.getApiKey());
|
||||||
if (difyFile != null && StringUtils.hasText(difyFile.getId())) {
|
if (difyFile != null && StringUtils.hasText(difyFile.getId())) {
|
||||||
logger.info("上传对话文件成功: agentId={}, fileId={}", agentId, difyFile.getId());
|
logger.info("上传对话文件到Dify成功: agentId={}, difyFileId={}", agentId, difyFile.getId());
|
||||||
Map<String, Object> result = new java.util.HashMap<>();
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
// Dify返回的信息
|
||||||
result.put("id", difyFile.getId());
|
result.put("id", difyFile.getId());
|
||||||
result.put("name", difyFile.getName());
|
result.put("name", difyFile.getName());
|
||||||
result.put("size", difyFile.getSize());
|
result.put("size", difyFile.getSize());
|
||||||
result.put("type", difyFile.getType());
|
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("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.success("上传成功", result);
|
||||||
}
|
}
|
||||||
return ResultDomain.failure("上传文件失败");
|
return ResultDomain.failure("上传文件到Dify失败");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("上传对话文件异常: {}", e.getMessage(), e);
|
logger.error("上传对话文件异常: {}", e.getMessage(), e);
|
||||||
return ResultDomain.failure("上传文件异常: " + e.getMessage());
|
return ResultDomain.failure("上传文件异常: " + e.getMessage());
|
||||||
|
|||||||
@@ -346,6 +346,18 @@ public class AgentChatServiceImpl implements AgentChatService {
|
|||||||
userMessage.setChatId(chatId);
|
userMessage.setChatId(chatId);
|
||||||
userMessage.setRole("user");
|
userMessage.setRole("user");
|
||||||
userMessage.setContent(query);
|
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);
|
chatMessageMapper.insertChatMessage(userMessage);
|
||||||
|
|
||||||
// 5. 构建Dify请求
|
// 5. 构建Dify请求
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export const aiChatAPI = {
|
|||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('agentId', agentId)
|
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
|
return response.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,27 +13,27 @@ export interface DifyFileInfo {
|
|||||||
/** 文件扩展名 */
|
/** 文件扩展名 */
|
||||||
extension?: string
|
extension?: string
|
||||||
/** 文件MIME类型 */
|
/** 文件MIME类型 */
|
||||||
mimeType?: string
|
mime_type?: string
|
||||||
/** 上传人ID */
|
/** 上传人ID */
|
||||||
createdBy?: string
|
created_by?: string
|
||||||
/** 上传时间(时间戳) */
|
/** 上传时间(时间戳) */
|
||||||
createdAt?: number
|
created_at?: number
|
||||||
/** 预览URL */
|
/** 预览URL */
|
||||||
previewUrl?: string
|
preview_url?: string
|
||||||
/** 源文件URL */
|
/** 源文件URL */
|
||||||
sourceUrl?: string
|
source_url?: string
|
||||||
/** 文件类型:image、document、audio、video、file */
|
/** 文件类型:image、document、audio、video、file */
|
||||||
type?: string
|
type?: string
|
||||||
/** 传输方式:remote_url、local_file */
|
/** 传输方式:remote_url、local_file */
|
||||||
transferMethod?: string
|
transfer_method?: string
|
||||||
/** 文件URL或ID */
|
/** 文件URL或ID */
|
||||||
url?: string
|
url?: string
|
||||||
/** 本地文件上传ID */
|
/** 本地文件上传ID */
|
||||||
uploadFileId?: string
|
upload_file_id?: string
|
||||||
/** 系统文件ID */
|
/** 系统文件ID */
|
||||||
sysFileId?: string
|
sys_file_id?: string
|
||||||
/** 文件路径(从系统文件表获取) */
|
/** 文件路径(从系统文件表获取) */
|
||||||
filePath?: string
|
file_path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -386,15 +386,18 @@ $brand-color-hover: #004488;
|
|||||||
&.user {
|
&.user {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
background: $brand-color;
|
background: $brand-color;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: 16px 16px 4px 16px;
|
border-radius: 16px 16px 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.message-time {
|
.message-time {
|
||||||
text-align: right;
|
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%;
|
max-width: 80%;
|
||||||
|
|
||||||
|
&.user {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 16px 16px 16px 4px;
|
border-radius: 16px 16px 16px 4px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -458,11 +471,17 @@ $brand-color-hover: #004488;
|
|||||||
&:nth-child(3) { animation-delay: 0s; }
|
&:nth-child(3) { animation-delay: 0s; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户消息气泡中的样式
|
||||||
|
.message-row.user .message-bubble {
|
||||||
.message-time {
|
.message-time {
|
||||||
font-size: 12px;
|
color: rgba(255, 255, 255, 0.7);
|
||||||
color: #94a3b8;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,11 +557,14 @@ $brand-color-hover: #004488;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-row {
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-textarea {
|
.chat-textarea {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
resize: none;
|
resize: none;
|
||||||
@@ -551,25 +573,13 @@ $brand-color-hover: #004488;
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
|
min-height: 24px;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: #94a3b8;
|
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 {
|
.tool-btn {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
@@ -578,11 +588,21 @@ $brand-color-hover: #004488;
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.uploading {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.send-btn {
|
.send-btn {
|
||||||
@@ -593,6 +613,7 @@ $brand-color-hover: #004488;
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: $brand-color;
|
background: $brand-color;
|
||||||
@@ -717,3 +738,137 @@ $brand-color-hover: #004488;
|
|||||||
text-decoration: underline;
|
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,17 +122,38 @@
|
|||||||
<Headphones v-else :size="16" />
|
<Headphones v-else :size="16" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 消息内容 -->
|
<!-- 消息内容区域 -->
|
||||||
<div class="message-bubble" :class="msg.role">
|
<div class="message-content" :class="msg.role">
|
||||||
<div
|
<!-- 文字气泡 -->
|
||||||
v-if="msg.text"
|
<div v-if="msg.text || (isStreaming && msg.role === 'assistant' && messages.indexOf(msg) === messages.length - 1)" class="message-bubble" :class="msg.role">
|
||||||
class="message-text"
|
<div
|
||||||
v-html="msg.role === 'assistant' ? renderMarkdown(msg.text) : msg.text"
|
v-if="msg.text"
|
||||||
>
|
class="message-text"
|
||||||
|
v-html="msg.role === 'assistant' ? renderMarkdown(msg.text) : msg.text"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<span v-if="isStreaming && msg.role === 'assistant' && messages.indexOf(msg) === messages.length - 1" class="typing-cursor">|</span>
|
||||||
|
<div v-if="!msg.text && isStreaming && msg.role === 'assistant'" class="loading-dots">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="isStreaming && msg.role === 'assistant' && messages.indexOf(msg) === messages.length - 1" class="typing-cursor">|</span>
|
<!-- 消息携带的文件(在气泡外面) -->
|
||||||
<div v-if="!msg.text && isStreaming && msg.role === 'assistant'" class="loading-dots">
|
<div v-if="msg.files && msg.files.length > 0" class="message-files">
|
||||||
<span></span><span></span><span></span>
|
<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>
|
||||||
<div class="message-time">{{ msg.time }}</div>
|
<div class="message-time">{{ msg.time }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,8 +181,46 @@
|
|||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<!-- 输入卡片 -->
|
<!-- 输入卡片 -->
|
||||||
<div class="input-card">
|
<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">
|
<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
|
<textarea
|
||||||
ref="textareaRef"
|
ref="textareaRef"
|
||||||
v-model="inputText"
|
v-model="inputText"
|
||||||
@@ -171,22 +230,14 @@
|
|||||||
:rows="1"
|
:rows="1"
|
||||||
class="chat-textarea"
|
class="chat-textarea"
|
||||||
/>
|
/>
|
||||||
</div>
|
<button
|
||||||
<!-- 工具栏 -->
|
class="send-btn"
|
||||||
<div class="toolbar-row">
|
:class="{ active: inputText.trim() || uploadedFiles.length > 0 }"
|
||||||
<div class="toolbar-actions">
|
:disabled="(!inputText.trim() && uploadedFiles.length === 0) || isStreaming"
|
||||||
<button class="tool-btn" title="添加附件">
|
@click="sendMessage"
|
||||||
<Paperclip :size="18" />
|
>
|
||||||
</button>
|
<Send :size="18" />
|
||||||
<button
|
</button>
|
||||||
class="send-btn"
|
|
||||||
:class="{ active: inputText.trim() }"
|
|
||||||
:disabled="!inputText.trim()"
|
|
||||||
@click="sendMessage"
|
|
||||||
>
|
|
||||||
<Send :size="18" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="disclaimer">AI 生成内容仅供参考 · 泰豪集团内部绝密信息请勿上传</p>
|
<p class="disclaimer">AI 生成内容仅供参考 · 泰豪集团内部绝密信息请勿上传</p>
|
||||||
@@ -212,27 +263,24 @@ import {
|
|||||||
Paperclip,
|
Paperclip,
|
||||||
Send,
|
Send,
|
||||||
User,
|
User,
|
||||||
Headphones
|
Headphones,
|
||||||
|
X,
|
||||||
|
Image,
|
||||||
|
File as FileIcon,
|
||||||
|
Loader2
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { aiChatAPI, agentAPI } from 'shared/api/ai'
|
import { aiChatAPI, agentAPI } from 'shared/api/ai'
|
||||||
|
import { fileAPI } from 'shared/api/file'
|
||||||
import type {
|
import type {
|
||||||
TbChat,
|
TbChat,
|
||||||
TbChatMessage,
|
TbChatMessage,
|
||||||
TbAgent,
|
TbAgent,
|
||||||
PrepareChatParam,
|
PrepareChatParam,
|
||||||
SSEMessageData,
|
SSEMessageData,
|
||||||
DifyFileInfo
|
DifyFileInfo,
|
||||||
|
TbSysFileDTO
|
||||||
} from 'shared/types'
|
} from 'shared/types'
|
||||||
import { AGENT_ID } from '@/config'
|
import { AGENT_ID, FILE_DOWNLOAD_URL } from '@/config'
|
||||||
|
|
||||||
// 显示用消息接口
|
|
||||||
interface DisplayMessage {
|
|
||||||
id: string
|
|
||||||
role: 'user' | 'assistant'
|
|
||||||
text: string
|
|
||||||
time: string
|
|
||||||
messageId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户信息(TODO: 从实际用户store获取)
|
// 用户信息(TODO: 从实际用户store获取)
|
||||||
const userId = computed(()=>{
|
const userId = computed(()=>{
|
||||||
@@ -255,7 +303,7 @@ const chatHistory = ref<TbChat[]>([])
|
|||||||
const currentChatTitle = ref<string>('')
|
const currentChatTitle = ref<string>('')
|
||||||
|
|
||||||
// 聊天消息列表
|
// 聊天消息列表
|
||||||
const messages = ref<DisplayMessage[]>([])
|
const messages = ref<TbChatMessage[]>([])
|
||||||
|
|
||||||
// 流式对话状态
|
// 流式对话状态
|
||||||
const isStreaming = ref(false)
|
const isStreaming = ref(false)
|
||||||
@@ -265,6 +313,14 @@ const eventSource = ref<EventSource | null>(null)
|
|||||||
// 输入框文本
|
// 输入框文本
|
||||||
const inputText = ref('')
|
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 messagesRef = ref<HTMLElement | null>(null)
|
||||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||||
@@ -288,6 +344,7 @@ const startNewChat = async () => {
|
|||||||
currentChatTitle.value = ''
|
currentChatTitle.value = ''
|
||||||
messages.value = []
|
messages.value = []
|
||||||
inputText.value = ''
|
inputText.value = ''
|
||||||
|
uploadedFiles.value = []
|
||||||
|
|
||||||
// 创建新会话
|
// 创建新会话
|
||||||
if (agentId && userId.value) {
|
if (agentId && userId.value) {
|
||||||
@@ -320,12 +377,14 @@ const loadChat = async (chatId: string) => {
|
|||||||
if (result.success && result.dataList) {
|
if (result.success && result.dataList) {
|
||||||
const messageList = Array.isArray(result.dataList) ? result.dataList : [result.dataList]
|
const messageList = Array.isArray(result.dataList) ? result.dataList : [result.dataList]
|
||||||
messages.value = messageList.map((msg: TbChatMessage) => ({
|
messages.value = messageList.map((msg: TbChatMessage) => ({
|
||||||
|
...msg,
|
||||||
id: msg.messageId || String(Date.now()),
|
id: msg.messageId || String(Date.now()),
|
||||||
role: msg.role === 'user' ? 'user' : 'assistant',
|
|
||||||
text: msg.content || '',
|
text: msg.content || '',
|
||||||
time: formatTime(msg.createTime),
|
time: formatTime(msg.createTime)
|
||||||
messageId: msg.messageId
|
} as TbChatMessage))
|
||||||
} as DisplayMessage))
|
|
||||||
|
// 加载消息中的文件信息
|
||||||
|
await loadMessagesFilesInfo(messageList)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载对话消息失败:', error)
|
console.error('加载对话消息失败:', error)
|
||||||
@@ -360,20 +419,36 @@ const scrollToBottom = () => {
|
|||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
if (!inputText.value.trim() || isStreaming.value) return
|
// 允许只有文件或只有文本
|
||||||
|
if ((!inputText.value.trim() && uploadedFiles.value.length === 0) || isStreaming.value) return
|
||||||
if (!agentId) {
|
if (!agentId) {
|
||||||
console.error('未选择智能体')
|
console.error('未选择智能体')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = inputText.value.trim()
|
const query = inputText.value.trim() || '[文件]'
|
||||||
const userMessage: DisplayMessage = {
|
const currentFiles = [...uploadedFiles.value] // 保存当前文件列表副本
|
||||||
|
const userMessage: TbChatMessage = {
|
||||||
id: String(Date.now()),
|
id: String(Date.now()),
|
||||||
role: 'user',
|
role: 'user',
|
||||||
text: query,
|
text: inputText.value.trim(),
|
||||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
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)
|
messages.value.push(userMessage)
|
||||||
inputText.value = ''
|
inputText.value = ''
|
||||||
|
|
||||||
@@ -409,9 +484,13 @@ const sendMessage = async () => {
|
|||||||
query: query,
|
query: query,
|
||||||
agentId: agentId,
|
agentId: agentId,
|
||||||
userType: userType.value,
|
userType: userType.value,
|
||||||
userId: userId.value
|
userId: userId.value,
|
||||||
|
files: uploadedFiles.value.length > 0 ? uploadedFiles.value : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空已上传的文件
|
||||||
|
uploadedFiles.value = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 准备流式对话
|
// 准备流式对话
|
||||||
const prepareResult = await aiChatAPI.prepareStreamChat(prepareParam)
|
const prepareResult = await aiChatAPI.prepareStreamChat(prepareParam)
|
||||||
@@ -422,7 +501,7 @@ const sendMessage = async () => {
|
|||||||
const sessionId = prepareResult.data
|
const sessionId = prepareResult.data
|
||||||
|
|
||||||
// 创建AI回复消息占位
|
// 创建AI回复消息占位
|
||||||
const assistantMessage: DisplayMessage = {
|
const assistantMessage: TbChatMessage = {
|
||||||
id: String(Date.now() + 1),
|
id: String(Date.now() + 1),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
text: '',
|
text: '',
|
||||||
@@ -631,6 +710,127 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件大小限制 10MB
|
||||||
|
const maxSize = 10 * 1024 * 1024
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
console.error('文件大小超过10MB限制')
|
||||||
|
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 => {
|
||||||
|
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 () => {
|
onMounted(async () => {
|
||||||
// TODO: 根据路由参数或配置获取智能体ID
|
// TODO: 根据路由参数或配置获取智能体ID
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { request } from '../base'
|
import { request, uploadFile } from '../base'
|
||||||
import type { ResultDomain } from '../../types'
|
import type { ResultDomain } from '../../types'
|
||||||
import type {
|
import type {
|
||||||
TbChat,
|
TbChat,
|
||||||
@@ -11,7 +11,8 @@ import type {
|
|||||||
ChatMessageListParam,
|
ChatMessageListParam,
|
||||||
SSECallbacks,
|
SSECallbacks,
|
||||||
SSETask,
|
SSETask,
|
||||||
SSEMessageData
|
SSEMessageData,
|
||||||
|
DifyFileInfo
|
||||||
} from '../../types/ai/aiChat'
|
} from '../../types/ai/aiChat'
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
@@ -190,5 +191,21 @@ export const aiChatAPI = {
|
|||||||
*/
|
*/
|
||||||
commentChatMessage(param: CommentMessageParam): Promise<ResultDomain<boolean>> {
|
commentChatMessage(param: CommentMessageParam): Promise<ResultDomain<boolean>> {
|
||||||
return request<boolean>({ url: `${this.baseUrl}/comment`, method: 'POST', data: param })
|
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,3 +1,4 @@
|
|||||||
export const AGENT_ID = '17664699513920001'
|
export const AGENT_ID = '17664699513920001'
|
||||||
export const BASE_URL = 'http://localhost:8180'
|
export const BASE_URL = 'http://localhost:8180'
|
||||||
export const WS_HOST = 'localhost:8180' // WebSocket host(不包含协议)
|
export const WS_HOST = 'localhost:8180' // WebSocket host(不包含协议)
|
||||||
|
export const FILE_DOWNLOAD_URL = 'http://localhost:8180/urban-lifeline/sys-file/download?fileId='
|
||||||
@@ -261,6 +261,15 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 消息内容包装器(包含气泡和文件列表)
|
||||||
|
.message-content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
.bot-message-content {
|
.bot-message-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -426,18 +435,121 @@
|
|||||||
.chat-input-wrap {
|
.chat-input-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
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 {
|
.chat-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 0 16px;
|
padding: 0 12px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -458,6 +570,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
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 {
|
.send-icon {
|
||||||
@@ -465,6 +587,65 @@
|
|||||||
color: #4b87ff;
|
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 {
|
.typing-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -52,8 +52,23 @@
|
|||||||
:class="item.type === 'user' ? 'user-message' : 'bot-message'">
|
:class="item.type === 'user' ? 'user-message' : 'bot-message'">
|
||||||
<!-- 用户消息(右侧) -->
|
<!-- 用户消息(右侧) -->
|
||||||
<view class="user-message-content" v-if="item.type === 'user'">
|
<view class="user-message-content" v-if="item.type === 'user'">
|
||||||
<view class="message-bubble user-bubble">
|
<view class="message-content-wrapper">
|
||||||
<text class="message-text">{{item.content}}</text>
|
<!-- 文字气泡 -->
|
||||||
|
<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>
|
||||||
<view class="avatar user-avatar">
|
<view class="avatar user-avatar">
|
||||||
<text class="avatar-text">我</text>
|
<text class="avatar-text">我</text>
|
||||||
@@ -103,9 +118,31 @@
|
|||||||
|
|
||||||
<!-- 输入区域 -->
|
<!-- 输入区域 -->
|
||||||
<view class="chat-input-wrap">
|
<view class="chat-input-wrap">
|
||||||
<input class="chat-input" v-model="inputText" placeholder="输入问题 来问问我~" @confirm="sendMessage" />
|
<!-- 已上传文件预览 -->
|
||||||
<view class="send-btn" @tap="sendMessage">
|
<view v-if="uploadedFiles.length > 0" class="uploaded-files">
|
||||||
<text class="send-icon">➤</text>
|
<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" :class="{ active: inputText.trim() || uploadedFiles.length > 0 }" @tap="sendMessage">
|
||||||
|
<text class="send-icon">➤</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -117,13 +154,15 @@
|
|||||||
import { ref, nextTick, onMounted } from 'vue'
|
import { ref, nextTick, onMounted } from 'vue'
|
||||||
import { guestAPI, aiChatAPI, workcaseChatAPI } from '@/api'
|
import { guestAPI, aiChatAPI, workcaseChatAPI } from '@/api'
|
||||||
import type { TbWorkcaseDTO } from '@/types'
|
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 {
|
interface ChatMessageItem {
|
||||||
type: 'user' | 'bot'
|
type: 'user' | 'bot'
|
||||||
content: string
|
content: string
|
||||||
time: string
|
time: string
|
||||||
actions?: string[] | null
|
actions?: string[] | null
|
||||||
|
files?: string[] // 文件ID数组
|
||||||
}
|
}
|
||||||
const agentId = AGENT_ID
|
const agentId = AGENT_ID
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
@@ -136,6 +175,12 @@
|
|||||||
const headerPaddingTop = ref<number>(44) // header顶部padding,默认44px
|
const headerPaddingTop = ref<number>(44) // header顶部padding,默认44px
|
||||||
const headerTotalHeight = ref<number>(76) // header总高度,默认76px
|
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({
|
const userInfo = ref({
|
||||||
wechatId: '',
|
wechatId: '',
|
||||||
@@ -257,18 +302,38 @@
|
|||||||
// 发送消息
|
// 发送消息
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const text = inputText.value.trim()
|
const text = inputText.value.trim()
|
||||||
if (!text || isTyping.value) return
|
// 允许只有文件或只有文本
|
||||||
|
if ((!text && uploadedFiles.value.length === 0) || isTyping.value) return
|
||||||
|
|
||||||
// 添加用户消息
|
const query = text || '[文件]'
|
||||||
addMessage('user', 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 = ''
|
inputText.value = ''
|
||||||
|
|
||||||
// 调用AI聊天接口
|
// 清空已上传的文件
|
||||||
await callAIChat(text)
|
uploadedFiles.value = []
|
||||||
|
|
||||||
|
// 调用AI聊天接口(携带文件)
|
||||||
|
await callAIChat(query, currentFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用AI聊天接口
|
// 调用AI聊天接口
|
||||||
async function callAIChat(query : string) {
|
async function callAIChat(query : string, files : DifyFileInfo[] = []) {
|
||||||
isTyping.value = true
|
isTyping.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -288,14 +353,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备流式对话
|
// 准备流式对话(包含文件)
|
||||||
const prepareRes = await aiChatAPI.prepareChatMessageSession({
|
const prepareData = {
|
||||||
chatId: chatId.value,
|
chatId: chatId.value,
|
||||||
query: query,
|
query: query,
|
||||||
agentId: agentId,
|
agentId: agentId,
|
||||||
userType: userType.value,
|
userType: userType.value,
|
||||||
userId: userInfo.value.userId
|
userId: userInfo.value.userId,
|
||||||
})
|
files: files.length > 0 ? files : undefined
|
||||||
|
}
|
||||||
|
console.log('准备流式对话参数:', JSON.stringify(prepareData))
|
||||||
|
|
||||||
|
const prepareRes = await aiChatAPI.prepareChatMessageSession(prepareData)
|
||||||
if (!prepareRes.success || !prepareRes.data) {
|
if (!prepareRes.success || !prepareRes.data) {
|
||||||
throw new Error(prepareRes.message || '准备对话失败')
|
throw new Error(prepareRes.message || '准备对话失败')
|
||||||
}
|
}
|
||||||
@@ -594,10 +663,9 @@
|
|||||||
count: 1,
|
count: 1,
|
||||||
sourceType: ['camera'],
|
sourceType: ['camera'],
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
// 处理图片上传逻辑
|
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||||
console.log('选择的图片:', res.tempFilePaths)
|
uploadSingleFile(res.tempFilePaths[0])
|
||||||
addMessage('user', '[图片]')
|
}
|
||||||
simulateAIResponse('收到您发送的图片')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -605,24 +673,177 @@
|
|||||||
// 从相册选择
|
// 从相册选择
|
||||||
function chooseImageFromAlbum() {
|
function chooseImageFromAlbum() {
|
||||||
uni.chooseImage({
|
uni.chooseImage({
|
||||||
count: 1,
|
count: 9,
|
||||||
sourceType: ['album'],
|
sourceType: ['album'],
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
// 处理图片上传逻辑
|
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||||
console.log('选择的图片:', res.tempFilePaths)
|
res.tempFilePaths.forEach((filePath: string) => {
|
||||||
addMessage('user', '[图片]')
|
uploadSingleFile(filePath)
|
||||||
simulateAIResponse('收到您发送的图片')
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择文件
|
// 选择文件
|
||||||
function chooseFile() {
|
function chooseFile() {
|
||||||
// 这里可以扩展文件选择功能
|
// #ifdef MP-WEIXIN
|
||||||
uni.showToast({
|
// 微信小程序使用 chooseMessageFile
|
||||||
title: '文件选择功能开发中',
|
uni.chooseMessageFile({
|
||||||
icon: 'none'
|
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: '选择文件失败',
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -12,27 +12,27 @@ export interface DifyFileInfo {
|
|||||||
/** 文件扩展名 */
|
/** 文件扩展名 */
|
||||||
extension?: string
|
extension?: string
|
||||||
/** 文件MIME类型 */
|
/** 文件MIME类型 */
|
||||||
mimeType?: string
|
mime_type?: string
|
||||||
/** 上传人ID */
|
/** 上传人ID */
|
||||||
createdBy?: string
|
created_by?: string
|
||||||
/** 上传时间(时间戳) */
|
/** 上传时间(时间戳) */
|
||||||
createdAt?: number
|
created_at?: number
|
||||||
/** 预览URL */
|
/** 预览URL */
|
||||||
previewUrl?: string
|
preview_url?: string
|
||||||
/** 源文件URL */
|
/** 源文件URL */
|
||||||
sourceUrl?: string
|
source_url?: string
|
||||||
/** 文件类型:image、document、audio、video、file */
|
/** 文件类型:image、document、audio、video、file */
|
||||||
type?: string
|
type?: string
|
||||||
/** 传输方式:remote_url、local_file */
|
/** 传输方式:remote_url、local_file */
|
||||||
transferMethod?: string
|
transfer_method?: string
|
||||||
/** 文件URL或ID */
|
/** 文件URL或ID */
|
||||||
url?: string
|
url?: string
|
||||||
/** 本地文件上传ID */
|
/** 本地文件上传ID */
|
||||||
uploadFileId?: string
|
upload_file_id?: string
|
||||||
/** 系统文件ID */
|
/** 系统文件ID */
|
||||||
sysFileId?: string
|
sys_file_id?: string
|
||||||
/** 文件路径(从系统文件表获取) */
|
/** 文件路径(从系统文件表获取) */
|
||||||
filePath?: string
|
file_path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user