|
|
|
<template>
|
|
|
|
<div class="chat-container">
|
|
|
|
<div class="chat-layout">
|
|
|
|
<!-- 主内容区域 -->
|
|
|
|
<div class="main-content">
|
|
|
|
<!-- 聊天消息区域 -->
|
|
|
|
<div class="chat-messages" ref="messagesContainer">
|
|
|
|
<div v-for="(message, index) in messages" :key="index" :class="['message', message.type]">
|
|
...
|
...
|
@@ -8,7 +9,10 @@ |
|
|
|
<div class="message-text" v-html="message.text"></div>
|
|
|
|
<div v-if="message.type === 'assistant' && message.similarity" class="message-meta">
|
|
|
|
相似度: {{ (message.similarity * 100).toFixed(2) }}%
|
|
|
|
<a v-if="message.fileBase64" @click="showPreview(message)">预览文件</a>
|
|
|
|
<div v-if="message.fileBase64">
|
|
|
|
相关资料:
|
|
|
|
<a @click="showPreview(message)">{{message.fileName}}</a>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
...
|
...
|
@@ -19,29 +23,65 @@ |
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 文件预览区域 - 右侧 -->
|
|
|
|
<!-- 文件预览区域 -->
|
|
|
|
<div v-if="previewContent" class="file-preview">
|
|
|
|
|
|
|
|
<div class="preview-header">
|
|
|
|
<span>文件预览</span>
|
|
|
|
<a-button type="text" @click="closePreview">
|
|
|
|
<template #icon><CloseOutlined /></template>
|
|
|
|
</a-button>
|
|
|
|
<span class="preview-title">文件预览</span>
|
|
|
|
<div class="preview-actions">
|
|
|
|
<a-button type="link" @click="downloadFile" class="download-btn">
|
|
|
|
<DownloadOutlined />
|
|
|
|
<span class="btn-text">下载</span>
|
|
|
|
</a-button>
|
|
|
|
<a-button
|
|
|
|
type="text"
|
|
|
|
@click="closePreview"
|
|
|
|
class="close-btn"
|
|
|
|
title="关闭预览"
|
|
|
|
>
|
|
|
|
<CloseOutlined />
|
|
|
|
</a-button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="preview-content">
|
|
|
|
<div v-if="previewType === 'txt' || previewType === 'md'" class="text-preview">
|
|
|
|
<pre>{{ previewContent }}</pre>
|
|
|
|
</div>
|
|
|
|
<div v-else-if="previewType === 'doc' || previewType === 'docx'" class="doc-preview">
|
|
|
|
<!-- 使用Blob URL直接预览 -->
|
|
|
|
<iframe
|
|
|
|
:src="previewUrl"
|
|
|
|
frameborder="0"
|
|
|
|
style="width:100%; height:100%;"
|
|
|
|
></iframe>
|
|
|
|
<div v-else-if="previewType === 'pdf'" class="pdf-preview" ref="pdfPreview"></div>
|
|
|
|
<div v-else-if="previewType === 'docx'" class="docx-preview" ref="docxPreview"></div>
|
|
|
|
<div v-else-if="previewType === 'doc'" class="unsupported-preview">
|
|
|
|
不支持预览 DOC 格式的文件,请下载后使用本地软件查看
|
|
|
|
</div>
|
|
|
|
<div v-else class="unsupported-preview">
|
|
|
|
不支持预览 {{ previewType.toUpperCase() }} 格式的文件
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<a-button
|
|
|
|
style="margin-left: 1%;
|
|
|
|
background-color: white;
|
|
|
|
color: #666;
|
|
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
|
|
border-radius: 16px;
|
|
|
|
cursor: pointer;
|
|
|
|
font-size: 14px;
|
|
|
|
padding: 2px 10px;
|
|
|
|
width: max-content;
|
|
|
|
margin-right: 6px;
|
|
|
|
white-space: nowrap;
|
|
|
|
transition: all 300ms ease;
|
|
|
|
"
|
|
|
|
v-for="(btn, index) in buttonList"
|
|
|
|
:key="index"
|
|
|
|
type="primary"
|
|
|
|
class="action-button"
|
|
|
|
@click="sendButtonMessage(btn.buttonValues)"
|
|
|
|
>
|
|
|
|
{{ btn.buttonName }}
|
|
|
|
</a-button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 输入区域 -->
|
|
|
|
<div class="chat-input">
|
|
...
|
...
|
@@ -64,12 +104,31 @@ |
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" name="zdy-rag-chat" setup>
|
|
|
|
import { ref, reactive, nextTick } from 'vue';
|
|
|
|
import { sendMessage as apiSendMessage } from './ZdyRag.api';
|
|
|
|
import { ref, reactive, nextTick, onMounted, onBeforeUnmount } from 'vue';
|
|
|
|
import { sendMessage as apiSendMessage , getButtonList} from './ZdyRag.api';
|
|
|
|
import { useMessage } from '/@/hooks/web/useMessage';
|
|
|
|
import { CloseOutlined } from '@ant-design/icons-vue';
|
|
|
|
import { CloseOutlined, DownloadOutlined } from '@ant-design/icons-vue';
|
|
|
|
import * as pdfjsLib from 'pdfjs-dist';
|
|
|
|
import * as mammoth from 'mammoth';
|
|
|
|
import { renderAsync } from 'docx-preview';
|
|
|
|
|
|
|
|
const { createMessage } = useMessage();
|
|
|
|
// 设置PDF.js worker路径
|
|
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdf.worker.min.js';
|
|
|
|
|
|
|
|
const buttonList = reactive<any[]>([]);
|
|
|
|
onMounted(async () => {
|
|
|
|
try {
|
|
|
|
const res = await getButtonList();
|
|
|
|
if (res && Array.isArray(res)) {
|
|
|
|
buttonList.push(...res);
|
|
|
|
} else {
|
|
|
|
// createMessage.warning('未获取到按钮列表数据');
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('获取按钮列表失败:', error);
|
|
|
|
// createMessage.error('获取按钮列表失败');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// 聊天消息数据
|
|
|
|
const messages = reactive([
|
|
...
|
...
|
@@ -77,7 +136,8 @@ const messages = reactive([ |
|
|
|
type: 'assistant',
|
|
|
|
text: '您好!我是智能助手,请问有什么可以帮您?',
|
|
|
|
similarity: null,
|
|
|
|
fileBase64: null
|
|
|
|
fileBase64: null,
|
|
|
|
fileName: null
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
...
|
...
|
@@ -87,6 +147,9 @@ const messagesContainer = ref<HTMLElement>(); |
|
|
|
const previewContent = ref('');
|
|
|
|
const previewType = ref('');
|
|
|
|
const previewUrl = ref('');
|
|
|
|
const currentFile = ref<any>(null);
|
|
|
|
const pdfPreview = ref<HTMLElement>();
|
|
|
|
const docxPreview = ref<HTMLElement>();
|
|
|
|
|
|
|
|
// 发送消息
|
|
|
|
const sendMessage = async () => {
|
|
...
|
...
|
@@ -98,7 +161,8 @@ const sendMessage = async () => { |
|
|
|
type: 'user',
|
|
|
|
text: text,
|
|
|
|
similarity: null,
|
|
|
|
fileBase64: null
|
|
|
|
fileBase64: null,
|
|
|
|
fileName: null
|
|
|
|
});
|
|
|
|
|
|
|
|
inputMessage.value = '';
|
|
...
|
...
|
@@ -119,61 +183,212 @@ const sendMessage = async () => { |
|
|
|
fileName: res.fileName || ''
|
|
|
|
};
|
|
|
|
messages.push(newMessage);
|
|
|
|
|
|
|
|
// 如果有文件内容,自动显示预览
|
|
|
|
if (res.fileBase64 && res.fileName) {
|
|
|
|
showPreview(newMessage);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
createMessage.error('获取回答失败: 返回数据为空');
|
|
|
|
// createMessage.error('获取回答失败: 返回数据为空');
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error: any) {
|
|
|
|
console.error('发送消息失败:', error);
|
|
|
|
createMessage.error('发送消息失败: ' + (error as Error).message);
|
|
|
|
// createMessage.error('发送消息失败: ' + error.message);
|
|
|
|
} finally {
|
|
|
|
loading.value = false;
|
|
|
|
scrollToBottom();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 显示文件预览
|
|
|
|
const showPreview = (message) => {
|
|
|
|
if (!message.fileBase64 || !message.fileName) {
|
|
|
|
console.warn('无法预览: 缺少fileBase64或fileName');
|
|
|
|
return;
|
|
|
|
const sendButtonMessage = async (buttonValue: string) => {
|
|
|
|
if (loading.value) return;
|
|
|
|
|
|
|
|
// 添加用户消息
|
|
|
|
messages.push({
|
|
|
|
type: 'user',
|
|
|
|
text: buttonValue,
|
|
|
|
similarity: null,
|
|
|
|
fileBase64: null,
|
|
|
|
fileName: null
|
|
|
|
});
|
|
|
|
|
|
|
|
loading.value = true;
|
|
|
|
closePreview();
|
|
|
|
|
|
|
|
try {
|
|
|
|
// 调用API发送按钮值
|
|
|
|
const res = await apiSendMessage({ questionText: buttonValue });
|
|
|
|
|
|
|
|
if (res) {
|
|
|
|
const newMessage = {
|
|
|
|
type: 'assistant',
|
|
|
|
text: formatAnswer(res.answer),
|
|
|
|
similarity: res.similarity || null,
|
|
|
|
fileBase64: res.fileBase64 || null,
|
|
|
|
fileName: res.fileName || ''
|
|
|
|
};
|
|
|
|
messages.push(newMessage);
|
|
|
|
} else {
|
|
|
|
// createMessage.error('获取回答失败: 返回数据为空');
|
|
|
|
}
|
|
|
|
} catch (error: any) {
|
|
|
|
console.error('发送按钮消息失败:', error);
|
|
|
|
// createMessage.error('发送按钮消息失败: ' + error.message);
|
|
|
|
} finally {
|
|
|
|
loading.value = false;
|
|
|
|
scrollToBottom();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 增强版showPreview函数
|
|
|
|
const showPreview = async (message: any) => {
|
|
|
|
try {
|
|
|
|
const fileExt = message.fileName.split('.').pop()?.toLowerCase() || '';
|
|
|
|
// 验证基本数据
|
|
|
|
if (!message?.fileBase64 || !message?.fileName) {
|
|
|
|
throw new Error('文件数据不完整');
|
|
|
|
}
|
|
|
|
|
|
|
|
// 关闭之前的预览
|
|
|
|
closePreview();
|
|
|
|
|
|
|
|
// 设置当前文件
|
|
|
|
currentFile.value = message;
|
|
|
|
const fileExt = getFileExtension(message.fileName);
|
|
|
|
previewType.value = fileExt;
|
|
|
|
|
|
|
|
if (fileExt === 'txt' || fileExt === 'md') {
|
|
|
|
// 对于文本文件,直接解码显示
|
|
|
|
previewContent.value = atob(message.fileBase64);
|
|
|
|
} else if (fileExt === 'doc' || fileExt === 'docx') {
|
|
|
|
// 对于Word文档,创建Blob对象并生成URL
|
|
|
|
const byteCharacters = atob(message.fileBase64);
|
|
|
|
const byteNumbers = new Array(byteCharacters.length);
|
|
|
|
for (let i = 0; i < byteCharacters.length; i++) {
|
|
|
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
|
|
}
|
|
|
|
const byteArray = new Uint8Array(byteNumbers);
|
|
|
|
const blob = new Blob([byteArray], {
|
|
|
|
type: fileExt === 'docx' ?
|
|
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' :
|
|
|
|
'application/msword'
|
|
|
|
});
|
|
|
|
|
|
|
|
// 直接使用Blob URL
|
|
|
|
previewUrl.value = URL.createObjectURL(blob);
|
|
|
|
previewContent.value = 'doc-preview';
|
|
|
|
// 根据文件类型处理
|
|
|
|
if (['txt', 'md'].includes(fileExt)) {
|
|
|
|
// 文本文件处理
|
|
|
|
previewContent.value = decodeBase64Content(message.fileBase64);
|
|
|
|
} else if (fileExt === 'pdf') {
|
|
|
|
// PDF文件处理
|
|
|
|
previewContent.value = 'pdf-preview';
|
|
|
|
await nextTick(); // 等待DOM更新
|
|
|
|
await renderPdf(message.fileBase64);
|
|
|
|
} else if (fileExt === 'docx') {
|
|
|
|
// DOCX文件处理
|
|
|
|
previewContent.value = 'docx-preview';
|
|
|
|
await nextTick(); // 等待DOM更新
|
|
|
|
await renderDocx(message.fileBase64);
|
|
|
|
} else if (fileExt === 'doc') {
|
|
|
|
// 不支持DOC格式预览
|
|
|
|
previewContent.value = 'unsupported';
|
|
|
|
throw new Error('不支持DOC格式预览,请下载后查看');
|
|
|
|
} else {
|
|
|
|
console.warn('不支持的文件类型:', fileExt);
|
|
|
|
createMessage.warning(`不支持预览 ${fileExt} 格式的文件`);
|
|
|
|
throw new Error(`不支持的文件类型: ${fileExt}`);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('文件预览失败:', error);
|
|
|
|
createMessage.error('文件预览失败');
|
|
|
|
console.error('预览失败:', error);
|
|
|
|
previewContent.value = 'error';
|
|
|
|
// createMessage.error(`预览失败: ${error.message}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 渲染PDF
|
|
|
|
const renderPdf = async (base64: string) => {
|
|
|
|
if (!pdfPreview.value) return;
|
|
|
|
|
|
|
|
// 清空容器
|
|
|
|
pdfPreview.value.innerHTML = '';
|
|
|
|
|
|
|
|
// 将Base64转换为Uint8Array
|
|
|
|
const binaryString = atob(base64);
|
|
|
|
const bytes = new Uint8Array(binaryString.length);
|
|
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 加载PDF文档
|
|
|
|
const pdfDoc = await pdfjsLib.getDocument({ data: bytes }).promise;
|
|
|
|
|
|
|
|
// 创建Canvas容器
|
|
|
|
const container = document.createElement('div');
|
|
|
|
container.className = 'pdf-pages';
|
|
|
|
pdfPreview.value.appendChild(container);
|
|
|
|
|
|
|
|
// 渲染每一页
|
|
|
|
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
|
|
|
const page = await pdfDoc.getPage(i);
|
|
|
|
const viewport = page.getViewport({ scale: 1.5 });
|
|
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
const context = canvas.getContext('2d');
|
|
|
|
canvas.height = viewport.height;
|
|
|
|
canvas.width = viewport.width;
|
|
|
|
|
|
|
|
const renderContext = {
|
|
|
|
canvasContext: context,
|
|
|
|
viewport: viewport
|
|
|
|
};
|
|
|
|
|
|
|
|
await page.render(renderContext).promise;
|
|
|
|
|
|
|
|
const pageDiv = document.createElement('div');
|
|
|
|
pageDiv.className = 'pdf-page';
|
|
|
|
pageDiv.appendChild(canvas);
|
|
|
|
container.appendChild(pageDiv);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 渲染DOCX
|
|
|
|
const renderDocx = async (base64: string) => {
|
|
|
|
if (!docxPreview.value) return;
|
|
|
|
|
|
|
|
// 清空容器
|
|
|
|
docxPreview.value.innerHTML = '';
|
|
|
|
|
|
|
|
// 将Base64转换为Blob
|
|
|
|
const byteString = atob(base64);
|
|
|
|
const byteNumbers = new Array(byteString.length);
|
|
|
|
for (let i = 0; i < byteString.length; i++) {
|
|
|
|
byteNumbers[i] = byteString.charCodeAt(i);
|
|
|
|
}
|
|
|
|
const byteArray = new Uint8Array(byteNumbers);
|
|
|
|
const blob = new Blob([byteArray], {
|
|
|
|
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
|
|
});
|
|
|
|
|
|
|
|
// 使用docx-preview渲染
|
|
|
|
await renderAsync(blob, docxPreview.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
// 获取文件扩展名
|
|
|
|
const getFileExtension = (filename: string): string => {
|
|
|
|
const match = filename.match(/\.([0-9a-z]+)(?:[\?#]|$)/i);
|
|
|
|
return match ? match[1].toLowerCase() : 'unknown';
|
|
|
|
};
|
|
|
|
|
|
|
|
// Base64解码
|
|
|
|
const decodeBase64Content = (base64: string): string => {
|
|
|
|
try {
|
|
|
|
return decodeURIComponent(escape(atob(base64)));
|
|
|
|
} catch {
|
|
|
|
return atob(base64);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 下载文件
|
|
|
|
const downloadFile = () => {
|
|
|
|
if (!currentFile.value || !currentFile.value.fileBase64 || !currentFile.value.fileName) {
|
|
|
|
// createMessage.warning('无法下载文件');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const byteCharacters = atob(currentFile.value.fileBase64);
|
|
|
|
const byteNumbers = new Array(byteCharacters.length);
|
|
|
|
for (let i = 0; i < byteCharacters.length; i++) {
|
|
|
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
|
|
}
|
|
|
|
const byteArray = new Uint8Array(byteNumbers);
|
|
|
|
const blob = new Blob([byteArray], { type: 'application/octet-stream' });
|
|
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
link.href = URL.createObjectURL(blob);
|
|
|
|
link.download = currentFile.value.fileName;
|
|
|
|
document.body.appendChild(link);
|
|
|
|
link.click();
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
|
|
|
// createMessage.success('文件下载开始');
|
|
|
|
} catch (error: any) {
|
|
|
|
console.error('文件下载失败:', error);
|
|
|
|
// createMessage.error('文件下载失败: ' + error.message);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
...
|
...
|
@@ -185,6 +400,11 @@ const closePreview = () => { |
|
|
|
URL.revokeObjectURL(previewUrl.value);
|
|
|
|
previewUrl.value = '';
|
|
|
|
}
|
|
|
|
currentFile.value = null;
|
|
|
|
|
|
|
|
// 清空预览容器
|
|
|
|
if (pdfPreview.value) pdfPreview.value.innerHTML = '';
|
|
|
|
if (docxPreview.value) docxPreview.value.innerHTML = '';
|
|
|
|
};
|
|
|
|
|
|
|
|
// 格式化回答内容
|
|
...
|
...
|
@@ -201,46 +421,125 @@ const scrollToBottom = () => { |
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// 组件卸载时清理资源
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
closePreview();
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
/* 样式保持不变 */
|
|
|
|
.chat-container {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
height: calc(100vh - 120px);
|
|
|
|
max-width: 1200px; /* 增加宽度以容纳两个区域 */
|
|
|
|
max-width: 1200px;
|
|
|
|
margin: 0 auto;
|
|
|
|
padding: 20px;
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
border-radius: 8px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.chat-layout {
|
|
|
|
.main-content {
|
|
|
|
display: flex;
|
|
|
|
flex: 1;
|
|
|
|
gap: 20px;
|
|
|
|
overflow: hidden;
|
|
|
|
margin-bottom: 20px;
|
|
|
|
overflow: hidden;
|
|
|
|
}
|
|
|
|
|
|
|
|
.chat-messages {
|
|
|
|
flex: 2;
|
|
|
|
flex: 1;
|
|
|
|
overflow-y: auto;
|
|
|
|
padding: 10px;
|
|
|
|
padding: 15px;
|
|
|
|
background-color: white;
|
|
|
|
border-radius: 8px;
|
|
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
|
|
min-height: 300px;
|
|
|
|
}
|
|
|
|
|
|
|
|
//.file-preview {
|
|
|
|
// width: 55%;
|
|
|
|
// min-width: 300px;
|
|
|
|
// display: flex;
|
|
|
|
// flex-direction: column;
|
|
|
|
// background-color: white;
|
|
|
|
// border-radius: 8px;
|
|
|
|
// box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
|
|
// overflow: hidden;
|
|
|
|
//
|
|
|
|
// .preview-header {
|
|
|
|
// display: flex;
|
|
|
|
// justify-content: space-between;
|
|
|
|
// align-items: center;
|
|
|
|
// padding: 10px 15px;
|
|
|
|
// background-color: #f0f0f0;
|
|
|
|
// border-bottom: 1px solid #e8e8e8;
|
|
|
|
//
|
|
|
|
// .preview-actions {
|
|
|
|
// display: flex;
|
|
|
|
// align-items: center;
|
|
|
|
// gap: 8px;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// .preview-content {
|
|
|
|
// flex: 1;
|
|
|
|
// overflow: auto;
|
|
|
|
// padding: 10px;
|
|
|
|
//
|
|
|
|
// .text-preview {
|
|
|
|
// padding: 15px;
|
|
|
|
// white-space: pre-wrap;
|
|
|
|
// font-family: monospace;
|
|
|
|
// line-height: 1.6;
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// .pdf-preview {
|
|
|
|
// height: 100%;
|
|
|
|
// overflow-y: auto;
|
|
|
|
//
|
|
|
|
// .pdf-pages {
|
|
|
|
// display: flex;
|
|
|
|
// flex-direction: column;
|
|
|
|
// gap: 20px;
|
|
|
|
// padding: 10px;
|
|
|
|
//
|
|
|
|
// .pdf-page {
|
|
|
|
// border: 1px solid #e8e8e8;
|
|
|
|
// box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// .docx-preview {
|
|
|
|
// height: 100%;
|
|
|
|
// overflow-y: auto;
|
|
|
|
// padding: 20px;
|
|
|
|
// background-color: #fff;
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// .unsupported-preview {
|
|
|
|
// height: 100%;
|
|
|
|
// padding: 20px;
|
|
|
|
// display: flex;
|
|
|
|
// align-items: center;
|
|
|
|
// justify-content: center;
|
|
|
|
// color: #ff4d4f;
|
|
|
|
// font-weight: 500;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
//}
|
|
|
|
.file-preview {
|
|
|
|
flex: 1;
|
|
|
|
min-width: 350px; /* 确保预览区域有最小宽度 */
|
|
|
|
width: 55%;
|
|
|
|
min-width: 300px;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
background-color: white;
|
|
|
|
border-radius: 8px;
|
|
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
|
|
overflow: hidden;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
.preview-header {
|
|
|
|
display: flex;
|
|
...
|
...
|
@@ -249,30 +548,123 @@ const scrollToBottom = () => { |
|
|
|
padding: 10px 15px;
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
border-bottom: 1px solid #e8e8e8;
|
|
|
|
font-weight: 500;
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
|
|
.preview-actions {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
gap: 10px;
|
|
|
|
|
|
|
|
.ant-btn {
|
|
|
|
padding: 0;
|
|
|
|
height: auto;
|
|
|
|
color: #666;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
transition: color 0.3s;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
color: #1890ff;
|
|
|
|
background: transparent;
|
|
|
|
}
|
|
|
|
|
|
|
|
.anticon {
|
|
|
|
font-size: 16px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.ant-btn-link {
|
|
|
|
padding: 0 8px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.preview-content {
|
|
|
|
flex: 1;
|
|
|
|
overflow: auto;
|
|
|
|
padding: 10px;
|
|
|
|
|
|
|
|
.text-preview {
|
|
|
|
padding: 15px;
|
|
|
|
white-space: pre-wrap;
|
|
|
|
font-family: monospace;
|
|
|
|
line-height: 1.6;
|
|
|
|
background-color: #fafafa;
|
|
|
|
border-radius: 4px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.doc-preview {
|
|
|
|
.pdf-preview {
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
iframe {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
border: none;
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
|
|
.pdf-pages {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
gap: 20px;
|
|
|
|
padding: 10px;
|
|
|
|
|
|
|
|
.pdf-page {
|
|
|
|
border: 1px solid #e8e8e8;
|
|
|
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
|
|
background-color: white;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.docx-preview {
|
|
|
|
height: 100%;
|
|
|
|
overflow-y: auto;
|
|
|
|
padding: 20px;
|
|
|
|
background-color: #fff;
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
border-radius: 4px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.unsupported-preview {
|
|
|
|
height: 100%;
|
|
|
|
padding: 20px;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
color: #ff4d4f;
|
|
|
|
font-weight: 500;
|
|
|
|
background-color: #fff2f0;
|
|
|
|
border-radius: 4px;
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
.file-preview {
|
|
|
|
width: 100%;
|
|
|
|
min-width: auto;
|
|
|
|
max-height: 300px;
|
|
|
|
|
|
|
|
.preview-header {
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
|
|
.preview-actions {
|
|
|
|
gap: 6px;
|
|
|
|
|
|
|
|
.ant-btn-link {
|
|
|
|
padding: 0 4px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.preview-content {
|
|
|
|
padding: 8px;
|
|
|
|
|
|
|
|
.text-preview,
|
|
|
|
.docx-preview {
|
|
|
|
padding: 10px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.message {
|
|
|
|
margin-bottom: 15px;
|
|
|
|
display: flex;
|
|
...
|
...
|
@@ -299,15 +691,16 @@ const scrollToBottom = () => { |
|
|
|
}
|
|
|
|
|
|
|
|
.message-content {
|
|
|
|
max-width: 70%;
|
|
|
|
padding: 10px 15px;
|
|
|
|
max-width: 80%;
|
|
|
|
padding: 12px 16px;
|
|
|
|
word-wrap: break-word;
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
|
}
|
|
|
|
|
|
|
|
.message-meta {
|
|
|
|
font-size: 12px;
|
|
|
|
color: #666;
|
|
|
|
margin-top: 5px;
|
|
|
|
margin-top: 8px;
|
|
|
|
text-align: right;
|
|
|
|
|
|
|
|
a {
|
|
...
|
...
|
@@ -323,17 +716,47 @@ const scrollToBottom = () => { |
|
|
|
|
|
|
|
.chat-input {
|
|
|
|
display: flex;
|
|
|
|
gap: 10px;
|
|
|
|
gap: 12px;
|
|
|
|
padding: 10px 0;
|
|
|
|
|
|
|
|
:deep(.ant-input) {
|
|
|
|
flex: 1;
|
|
|
|
border-radius: 20px;
|
|
|
|
padding: 10px 15px;
|
|
|
|
padding: 12px 18px;
|
|
|
|
border: 1px solid #d9d9d9;
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
border-color: #40a9ff;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:focus {
|
|
|
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.ant-btn {
|
|
|
|
border-radius: 20px;
|
|
|
|
padding: 0 20px;
|
|
|
|
padding: 0 24px;
|
|
|
|
height: 40px;
|
|
|
|
font-weight: 500;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
.main-content {
|
|
|
|
flex-direction: column;
|
|
|
|
}
|
|
|
|
|
|
|
|
.file-preview {
|
|
|
|
width: 100%;
|
|
|
|
min-width: auto;
|
|
|
|
max-height: 300px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.message-content {
|
|
|
|
max-width: 90%;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style> |
...
|
...
|
|