2025-07-18 16:38:18 +08:00
|
|
|
|
<template>
|
|
|
|
|
<div class="chat-container">
|
|
|
|
|
<!-- 会话记录侧边栏 -->
|
|
|
|
|
<div class="chat-sidebar" :class="{ 'sidebar-collapsed': isSidebarCollapsed }">
|
|
|
|
|
<div class="sidebar-header">
|
2025-07-29 15:14:47 +08:00
|
|
|
|
<h3 v-show="!isSidebarCollapsed">{{ t('vabI18n.chat.history') }}</h3>
|
2025-07-18 16:38:18 +08:00
|
|
|
|
<el-button
|
|
|
|
|
type="primary"
|
|
|
|
|
class="new-chat-btn"
|
|
|
|
|
@click="() => createNewChat()"
|
|
|
|
|
v-show="!isSidebarCollapsed"
|
|
|
|
|
>
|
|
|
|
|
<svg viewBox="0 0 24 24" class="new-chat-icon">
|
|
|
|
|
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
|
|
|
|
</svg>
|
2025-07-29 15:14:47 +08:00
|
|
|
|
{{ t('vabI18n.chat.newChat') }}
|
2025-07-18 16:38:18 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="chat-history" v-show="!isSidebarCollapsed">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(chat, index) in chatHistory"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="chat-history-item"
|
|
|
|
|
:class="{ 'active': currentChatIndex === index }"
|
|
|
|
|
@click="switchChat(index)"
|
|
|
|
|
>
|
2025-07-29 15:14:47 +08:00
|
|
|
|
<span class="chat-title">{{ chat.title || t('vabI18n.chat.chat') + ` ${index + 1}` }}</span>
|
2025-07-18 16:38:18 +08:00
|
|
|
|
<span class="chat-time">{{ formatTime(chat.timestamp) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="sidebar-toggle" @click="toggleSidebar">
|
|
|
|
|
<svg viewBox="0 0 24 24" class="toggle-icon" :class="{ 'collapsed': isSidebarCollapsed }">
|
|
|
|
|
<path fill="currentColor" d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 主聊天区域 -->
|
|
|
|
|
<div class="chat-main" :class="{ 'main-expanded': isSidebarCollapsed }">
|
|
|
|
|
<!-- 聊天消息区域 -->
|
|
|
|
|
<div class="chat-messages" ref="messagesContainer">
|
|
|
|
|
<div v-for="(msg, index) in messages"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="message-item"
|
|
|
|
|
:class="{ 'user-message': msg.isUser, 'assistant-message': !msg.isUser }"
|
|
|
|
|
@mouseenter="handleMessageHover(index, true)"
|
|
|
|
|
@mouseleave="handleMessageHover(index, false)">
|
|
|
|
|
<!-- 头像 -->
|
|
|
|
|
<div class="avatar" :class="{ 'user-avatar': msg.isUser, 'assistant-avatar': !msg.isUser }">
|
|
|
|
|
<svg v-if="msg.isUser" viewBox="0 0 24 24" class="avatar-icon">
|
|
|
|
|
<path fill="currentColor" d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
|
|
|
|
</svg>
|
2025-07-29 15:14:47 +08:00
|
|
|
|
<img v-else :src="getAssistantAvatar()" :alt="$t('vabI18n.chat.assistant')" class="avatar-icon" />
|
2025-07-18 16:38:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 消息内容 -->
|
|
|
|
|
<div class="message-content-wrapper" :class="{ 'has-charts': !msg.isUser && msg.echarts && msg.echarts.length > 0 }">
|
|
|
|
|
<div class="message-content">
|
|
|
|
|
<div class="message-header">
|
2025-07-29 15:14:47 +08:00
|
|
|
|
<span class="message-sender">{{ msg.isUser ? t('vabI18n.chat.you') : t('vabI18n.chat.assistant') }}</span>
|
2025-07-18 16:38:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
<div class="message-body">
|
|
|
|
|
<!-- 编辑状态 -->
|
|
|
|
|
<template v-if="editingMessageIndex === index">
|
|
|
|
|
<div class="edit-container">
|
|
|
|
|
<div class="edit-input-wrapper">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="editingMessageText"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:autosize="{ minRows: 3, maxRows: 8 }"
|
|
|
|
|
class="edit-textarea"
|
2025-07-29 15:14:47 +08:00
|
|
|
|
:placeholder="$t('vabI18n.chat.placeholder')"
|
2025-07-18 16:38:18 +08:00
|
|
|
|
@keydown.enter.ctrl="handleSaveEdit"
|
|
|
|
|
@keydown.escape="handleCancelEdit"
|
|
|
|
|
/>
|
|
|
|
|
<div class="edit-actions">
|
|
|
|
|
<button
|
|
|
|
|
@click="handleCancelEdit"
|
|
|
|
|
class="edit-btn cancel-btn"
|
|
|
|
|
>
|
2025-07-29 15:14:47 +08:00
|
|
|
|
{{ t('vabI18n.chat.cancel') }}
|
2025-07-18 16:38:18 +08:00
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
@click="handleSaveEdit"
|
|
|
|
|
:disabled="!editingMessageText.trim()"
|
|
|
|
|
class="edit-btn save-btn"
|
|
|
|
|
>
|
2025-07-29 15:14:47 +08:00
|
|
|
|
{{ t('vabI18n.chat.send') }}
|
2025-07-18 16:38:18 +08:00
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 正常显示状态 -->
|
|
|
|
|
<template v-else>
|
|
|
|
|
<template v-if="!msg.isUser">
|
|
|
|
|
<div v-if="parseThink(msg.text).think" class="think-section">
|
|
|
|
|
<div class="think-header">
|
|
|
|
|
<svg viewBox="0 0 24 24" class="think-icon">
|
|
|
|
|
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
|
|
|
|
</svg>
|
2025-07-29 15:14:47 +08:00
|
|
|
|
{{ t('vabI18n.chat.think') }}
|
2025-07-18 16:38:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
{{ parseThink(msg.text).think }}
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="msg.formattedText" class="answer-content">
|
|
|
|
|
<div v-html="msg.formattedText"></div>
|
|
|
|
|
<!-- 渲染 ECharts 图表 -->
|
|
|
|
|
<div v-if="msg.echarts && msg.echarts.length > 0" class="echarts-container">
|
|
|
|
|
<div
|
|
|
|
|
v-for="chart in msg.echarts"
|
|
|
|
|
:key="chart.id"
|
|
|
|
|
class="chart-wrapper"
|
|
|
|
|
>
|
|
|
|
|
<VabChart
|
|
|
|
|
:option="chart.option"
|
|
|
|
|
:init-options="{ renderer: 'svg' }"
|
|
|
|
|
theme="vab-echarts-theme"
|
|
|
|
|
class="message-chart"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else class="loading-content">
|
|
|
|
|
<span class="loading-dots">
|
|
|
|
|
<span class="dot"></span>
|
|
|
|
|
<span class="dot"></span>
|
|
|
|
|
<span class="dot"></span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 文件来源信息 -->
|
|
|
|
|
<div v-if="msg.sources && ((msg.sources.tracePdf && msg.sources.tracePdf.length > 0) || (msg.sources.traceExcel && msg.sources.traceExcel.trim())) && props.chatType === '1'" class="sources-section">
|
|
|
|
|
<div class="sources-header">
|
|
|
|
|
<svg viewBox="0 0 24 24" class="sources-icon">
|
|
|
|
|
<path fill="currentColor" d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
|
|
|
|
|
</svg>
|
2025-07-29 15:14:47 +08:00
|
|
|
|
{{ t('vabI18n.chat.sources') }}
|
2025-07-18 16:38:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
<div class="sources-list">
|
|
|
|
|
<!-- PDF文件来源 -->
|
|
|
|
|
<div
|
|
|
|
|
v-for="source in msg.sources.tracePdf"
|
|
|
|
|
:key="source.fileName"
|
|
|
|
|
class="source-item"
|
|
|
|
|
@click="handleSourceClick(source, msg.conversationId, msg.messageId)"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
:src="getFileTypeIcon(getFileExtension(source.fileName))"
|
|
|
|
|
:alt="getFileExtension(source.fileName)"
|
|
|
|
|
class="source-icon"
|
|
|
|
|
/>
|
|
|
|
|
<el-tooltip
|
|
|
|
|
:content="source.fileName"
|
|
|
|
|
placement="top"
|
|
|
|
|
:disabled="source.fileName.length <= 25"
|
|
|
|
|
>
|
|
|
|
|
<span class="source-name">{{ source.fileName }}</span>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Excel文件来源 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="msg.sources.traceExcel && msg.sources.traceExcel.trim()"
|
|
|
|
|
class="source-item excel-item"
|
|
|
|
|
@click="handleExcelClick(msg.sources.traceExcel)"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
:src="getFileTypeIcon('excel')"
|
|
|
|
|
alt="excel"
|
|
|
|
|
class="source-icon"
|
|
|
|
|
/>
|
|
|
|
|
<el-tooltip
|
|
|
|
|
:content="generateExcelFileName()"
|
|
|
|
|
placement="top"
|
|
|
|
|
>
|
|
|
|
|
<span class="source-name">{{ generateExcelFileName() }}</span>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 推荐问题按钮 -->
|
|
|
|
|
<div v-if="msg.isOpenRemark && props.recommendQuestions && props.recommendQuestions.length > 0" class="recommend-questions-section">
|
|
|
|
|
<div class="recommend-questions-header">
|
|
|
|
|
<!-- <svg viewBox="0 0 24 24" class="recommend-icon">
|
|
|
|
|
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
|
|
|
|
</svg> -->
|
|
|
|
|
<!-- 推荐问题 -->
|
|
|
|
|
</div>
|
|
|
|
|
<div class="recommend-questions-list">
|
|
|
|
|
<el-tooltip
|
|
|
|
|
v-for="(question, qIndex) in props.recommendQuestions"
|
|
|
|
|
:key="qIndex"
|
|
|
|
|
:content="question"
|
|
|
|
|
placement="top"
|
|
|
|
|
:disabled="question.length <= 30"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
class="recommend-question-btn"
|
|
|
|
|
@click="handleRecommendQuestionClick(question)"
|
|
|
|
|
:disabled="isLoading"
|
|
|
|
|
>
|
|
|
|
|
<svg viewBox="0 0 24 24" class="question-icon">
|
|
|
|
|
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="question-text">{{ question }}</span>
|
|
|
|
|
</button>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template v-else>
|
|
|
|
|
{{ msg.text }}
|
|
|
|
|
</template>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-if="msg.isLoading" class="typing-indicator">
|
|
|
|
|
<span class="dot"></span>
|
|
|
|
|
<span class="dot"></span>
|
|
|
|
|
<span class="dot"></span>
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
<!-- 消息操作按钮 -->
|
|
|
|
|
<div
|
|
|
|
|
v-show="hoveredMessageIndex === index && !msg.isLoading && editingMessageIndex !== index"
|
|
|
|
|
class="message-actions"
|
|
|
|
|
:class="{ 'user-actions': msg.isUser, 'assistant-actions': !msg.isUser }"
|
|
|
|
|
>
|
|
|
|
|
<!-- 用户消息操作 -->
|
|
|
|
|
<template v-if="msg.isUser">
|
|
|
|
|
<el-tooltip :content="$t('vabI18n.chat.copy')" placement="top">
|
|
|
|
|
<button class="action-btn copy-btn" @click="handleCopyMessage(msg)">
|
|
|
|
|
<svg viewBox="0 0 24 24" class="action-icon">
|
|
|
|
|
<path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
<el-tooltip :content="$t('vabI18n.chat.edit')" placement="top">
|
|
|
|
|
<button class="action-btn edit-btn" @click="handleEditMessage(msg, index)">
|
|
|
|
|
<svg viewBox="0 0 24 24" class="action-icon">
|
|
|
|
|
<path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 助手消息操作 -->
|
|
|
|
|
<template v-else>
|
|
|
|
|
<el-tooltip :content="$t('vabI18n.chat.copy')" placement="top">
|
|
|
|
|
<button class="action-btn copy-btn" @click="handleCopyMessage(msg)">
|
|
|
|
|
<svg viewBox="0 0 24 24" class="action-icon">
|
|
|
|
|
<path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
<el-tooltip :content="$t('vabI18n.chat.regenerate')" placement="top">
|
|
|
|
|
<button class="action-btn regenerate-btn" @click="handleRegenerateMessage(index)">
|
|
|
|
|
<svg viewBox="0 0 24 24" class="action-icon">
|
|
|
|
|
<path fill="currentColor" d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
<el-tooltip :content="$t('vabI18n.chat.like')" placement="top">
|
|
|
|
|
<button class="action-btn like-btn" @click="handleLikeMessage(index)">
|
|
|
|
|
<svg viewBox="0 0 24 24" class="action-icon">
|
|
|
|
|
<path fill="currentColor" d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
<el-tooltip :content="$t('vabI18n.chat.dislike')" placement="top">
|
|
|
|
|
<button class="action-btn dislike-btn" @click="handleDislikeMessage(index)">
|
|
|
|
|
<svg viewBox="0 0 24 24" class="action-icon">
|
|
|
|
|
<path fill="currentColor" d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-07-30 15:30:23 +08:00
|
|
|
|
<div ref="bottomAnchor"></div>
|
|
|
|
|
|
2025-07-18 16:38:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 输入区域 -->
|
|
|
|
|
<div class="input-area">
|
|
|
|
|
<!-- 中止按钮 -->
|
|
|
|
|
<div v-if="showStopButton" class="stop-button-container">
|
|
|
|
|
<el-tooltip :content="$t('vabI18n.chat.stopGenerating')" placement="top">
|
|
|
|
|
<button
|
|
|
|
|
class="stop-button"
|
|
|
|
|
@click="handleStopStream"
|
|
|
|
|
>
|
|
|
|
|
<svg viewBox="0 0 24 24" class="stop-icon">
|
|
|
|
|
<path fill="currentColor" d="M6 6h12v12H6z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="input-wrapper">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="inputMessage"
|
|
|
|
|
:placeholder="props.placeholder"
|
|
|
|
|
:class="{ 'is-loading': isLoading }"
|
|
|
|
|
@keyup.enter="sendMessage"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:autosize="{ minRows: 1, maxRows: 4 }"
|
|
|
|
|
>
|
|
|
|
|
</el-input>
|
|
|
|
|
<el-button
|
|
|
|
|
type="primary"
|
|
|
|
|
class="send-button"
|
|
|
|
|
@click="sendMessage"
|
|
|
|
|
:disabled="!inputMessage.trim() || isLoading"
|
|
|
|
|
>
|
|
|
|
|
<template v-if="isLoading">
|
|
|
|
|
<span class="loading-icon"></span>
|
|
|
|
|
</template>
|
|
|
|
|
<svg v-else viewBox="0 0 24 24" class="send-icon" width="16" height="16">
|
|
|
|
|
<path fill="currentColor" d="M3.4 20.4l17.45-7.48a1 1 0 000-1.84L3.4 3.6a.993.993 0 00-1.39.91L2 9.12c0 .5.37.93.87.99L17 12L2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="input-footer">
|
2025-07-29 15:14:47 +08:00
|
|
|
|
<span class="footer-hint">{{ t('vabI18n.chat.sendHint') }}</span>
|
2025-07-18 16:38:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 文件预览模态框 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="filePreviewVisible"
|
2025-07-29 15:14:47 +08:00
|
|
|
|
:title="t('vabI18n.chat.previewTitle')"
|
2025-07-18 16:38:18 +08:00
|
|
|
|
width="80%"
|
|
|
|
|
class="file-preview-dialog"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
center
|
|
|
|
|
>
|
|
|
|
|
<div v-loading="previewLoading" class="preview-content">
|
|
|
|
|
<vue-office-pdf
|
|
|
|
|
v-if="previewFileUrl"
|
|
|
|
|
:src="previewFileUrl"
|
|
|
|
|
@rendered="() => { previewLoading = false }"
|
2025-07-29 15:14:47 +08:00
|
|
|
|
@error="() => { previewLoading = false; ElMessage.error(t('vabI18n.chat.previewTitleFail')) }"
|
2025-07-18 16:38:18 +08:00
|
|
|
|
class="pdf-preview"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<!-- Excel表格预览模态框 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="excelPreviewVisible"
|
2025-07-29 15:14:47 +08:00
|
|
|
|
:title="$t('vabI18n.chat.excelPreviewTitle')"
|
2025-07-18 16:38:18 +08:00
|
|
|
|
width="90%"
|
|
|
|
|
class="excel-preview-dialog"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
center
|
|
|
|
|
>
|
|
|
|
|
<div v-loading="excelPreviewLoading" class="excel-preview-content">
|
|
|
|
|
<div
|
|
|
|
|
v-if="excelPreviewContent"
|
|
|
|
|
v-html="excelPreviewContent"
|
|
|
|
|
class="excel-table-content"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
// 组件注册
|
|
|
|
|
const components = {
|
|
|
|
|
VueOfficePdf,
|
|
|
|
|
VabChart
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
import { marked } from 'marked'
|
|
|
|
|
import DOMPurify from 'dompurify'
|
|
|
|
|
import { ref, reactive, nextTick, watch, onMounted } from 'vue'
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
import VueOfficePdf from '@vue-office/pdf'
|
|
|
|
|
import VabChart from '@/plugins/VabChart/index.vue'
|
2025-07-30 15:30:23 +08:00
|
|
|
|
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/24/solid'
|
2025-07-18 16:38:18 +08:00
|
|
|
|
import { sendChatMessage, type ChatMessageResponse, type ChatMessageSendRequest, stopMessagesStream, getFilePathList, type TraceFile, type TraceData } from '@/api/chat'
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
2025-07-29 17:57:22 +08:00
|
|
|
|
import { throttle } from 'lodash'
|
2025-07-18 16:38:18 +08:00
|
|
|
|
|
2025-07-30 15:30:23 +08:00
|
|
|
|
const expanded = ref(false)
|
2025-07-18 16:38:18 +08:00
|
|
|
|
const { t, locale } = useI18n()
|
|
|
|
|
// Props 定义
|
|
|
|
|
interface Props {
|
|
|
|
|
chatType: string // 聊天类型,必填
|
|
|
|
|
userId: string // 用户ID,必填
|
|
|
|
|
assistantAvatar?: string // 助手头像,可选,默认使用内置头像
|
|
|
|
|
initialMessages?: Message[] // 初始消息,可选
|
|
|
|
|
placeholder?: string // 输入框占位符,可选
|
|
|
|
|
maxHistoryCount?: number // 最大历史会话数量,可选
|
|
|
|
|
openRemark?: string // 开场白,可选
|
|
|
|
|
recommendQuestions?: string[] // 推荐问题列表,可选
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
assistantAvatar: '',
|
|
|
|
|
initialMessages: () => [],
|
|
|
|
|
placeholder: "",
|
|
|
|
|
maxHistoryCount: 50,
|
|
|
|
|
openRemark: '',
|
|
|
|
|
recommendQuestions: () => []
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Events 定义
|
|
|
|
|
interface Emits {
|
|
|
|
|
streamComplete: [data: {
|
|
|
|
|
conversationId: string
|
|
|
|
|
messageId: string
|
|
|
|
|
content: string
|
|
|
|
|
}] // 对话流完成事件
|
|
|
|
|
sourceClick: [source: TraceFile] // 文件来源点击事件
|
|
|
|
|
messageReceived: [message: Message] // 收到新消息事件
|
|
|
|
|
chatCreated: [chatIndex: number] // 创建新会话事件
|
|
|
|
|
chatSwitched: [chatIndex: number] // 切换会话事件
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<Emits>()
|
|
|
|
|
|
|
|
|
|
interface EChartData {
|
|
|
|
|
id: string
|
|
|
|
|
option: any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Message {
|
|
|
|
|
text: string
|
|
|
|
|
isUser: boolean
|
|
|
|
|
isLoading?: boolean
|
|
|
|
|
formattedText?: string
|
|
|
|
|
sources?: TraceData // 更新为TraceData类型
|
|
|
|
|
conversationId?: string // 会话ID
|
|
|
|
|
messageId?: string // 消息ID
|
|
|
|
|
echarts?: EChartData[] // ECharts 图表数据
|
|
|
|
|
isOpenRemark?: boolean // 是否为开场白消息
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 15:30:23 +08:00
|
|
|
|
// 消息体底部锚点
|
|
|
|
|
const bottomAnchor = ref<HTMLElement | null>(null)
|
2025-07-18 16:38:18 +08:00
|
|
|
|
|
|
|
|
|
const messages = ref<Message[]>([])
|
|
|
|
|
const inputMessage = ref('')
|
2025-07-30 15:30:23 +08:00
|
|
|
|
const messagesContainer = ref<HTMLElement | null>(null)
|
2025-07-18 16:38:18 +08:00
|
|
|
|
const isLoading = ref(false)
|
|
|
|
|
const currentTaskId = ref('')
|
|
|
|
|
const showStopButton = ref(false)
|
|
|
|
|
const hoveredMessageIndex = ref(-1)
|
|
|
|
|
const editingMessageIndex = ref(-1)
|
|
|
|
|
const editingMessageText = ref('')
|
|
|
|
|
|
|
|
|
|
// 文件预览相关状态
|
|
|
|
|
const filePreviewVisible = ref(false)
|
|
|
|
|
const previewFileUrl = ref('')
|
|
|
|
|
const previewLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
// Excel表格预览相关状态
|
|
|
|
|
const excelPreviewVisible = ref(false)
|
|
|
|
|
const excelPreviewContent = ref('')
|
|
|
|
|
const excelPreviewLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
// 添加新的状态管理
|
|
|
|
|
interface ChatSession {
|
|
|
|
|
messages: Message[]
|
|
|
|
|
title: string
|
|
|
|
|
timestamp: number
|
|
|
|
|
conversationId?: string
|
|
|
|
|
lastMessageId?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isSidebarCollapsed = ref(false)
|
|
|
|
|
const chatHistory = ref<ChatSession[]>([])
|
|
|
|
|
const currentChatIndex = ref(0)
|
|
|
|
|
|
|
|
|
|
// 计算sessionStorage的key
|
|
|
|
|
const getChatHistoryKey = () => {
|
|
|
|
|
return `chat_history_${props.chatType}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 从sessionStorage加载会话历史
|
|
|
|
|
const loadChatHistory = () => {
|
|
|
|
|
const savedHistory = sessionStorage.getItem(getChatHistoryKey())
|
|
|
|
|
if (savedHistory) {
|
|
|
|
|
try {
|
|
|
|
|
chatHistory.value = JSON.parse(savedHistory)
|
|
|
|
|
// 限制历史会话数量
|
|
|
|
|
if (chatHistory.value.length > props.maxHistoryCount) {
|
|
|
|
|
chatHistory.value = chatHistory.value.slice(-props.maxHistoryCount)
|
|
|
|
|
}
|
|
|
|
|
// 如果有历史会话,默认选中最后一个
|
|
|
|
|
if (chatHistory.value.length > 0) {
|
|
|
|
|
currentChatIndex.value = chatHistory.value.length - 1
|
|
|
|
|
messages.value = [...chatHistory.value[currentChatIndex.value].messages]
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('加载会话历史失败:', e)
|
|
|
|
|
chatHistory.value = []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保存会话历史到sessionStorage
|
|
|
|
|
const saveChatHistory = () => {
|
|
|
|
|
// 限制历史会话数量
|
|
|
|
|
if (chatHistory.value.length > props.maxHistoryCount) {
|
|
|
|
|
chatHistory.value = chatHistory.value.slice(-props.maxHistoryCount)
|
|
|
|
|
}
|
|
|
|
|
sessionStorage.setItem(getChatHistoryKey(), JSON.stringify(chatHistory.value))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 切换侧边栏状态
|
|
|
|
|
const toggleSidebar = () => {
|
|
|
|
|
isSidebarCollapsed.value = !isSidebarCollapsed.value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建新会话
|
|
|
|
|
const createNewChat = async () => {
|
|
|
|
|
// 保存当前会话(如果有内容的话)
|
|
|
|
|
if (messages.value.length > 0 && chatHistory.value[currentChatIndex.value]) {
|
|
|
|
|
chatHistory.value[currentChatIndex.value] = {
|
|
|
|
|
messages: [...messages.value],
|
|
|
|
|
title: messages.value[0]?.text.slice(0, 20) + '...' || '新会话',
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
conversationId: chatHistory.value[currentChatIndex.value].conversationId,
|
|
|
|
|
lastMessageId: chatHistory.value[currentChatIndex.value].lastMessageId
|
|
|
|
|
}
|
|
|
|
|
saveChatHistory()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建新会话初始消息
|
|
|
|
|
let initialMessages = [...props.initialMessages]
|
|
|
|
|
|
|
|
|
|
// 如果有开场白,添加助手消息
|
|
|
|
|
if (props.openRemark && props.openRemark.trim()) {
|
|
|
|
|
const formatted = await formatAnswer(props.openRemark)
|
|
|
|
|
const openRemarkMessage: Message = {
|
|
|
|
|
text: props.openRemark,
|
|
|
|
|
isUser: false,
|
|
|
|
|
formattedText: formatted.html,
|
|
|
|
|
echarts: formatted.echarts,
|
|
|
|
|
isOpenRemark: true
|
|
|
|
|
}
|
|
|
|
|
initialMessages.push(openRemarkMessage)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建新会话
|
|
|
|
|
const newSession: ChatSession = {
|
|
|
|
|
messages: initialMessages,
|
|
|
|
|
title: '新会话',
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
}
|
|
|
|
|
chatHistory.value.push(newSession)
|
|
|
|
|
currentChatIndex.value = chatHistory.value.length - 1
|
|
|
|
|
messages.value = initialMessages
|
|
|
|
|
saveChatHistory()
|
|
|
|
|
|
|
|
|
|
// 发射会话创建事件
|
|
|
|
|
emit('chatCreated', currentChatIndex.value)
|
|
|
|
|
|
|
|
|
|
// 滚动到底部显示新消息
|
|
|
|
|
await nextTick()
|
2025-07-30 15:30:23 +08:00
|
|
|
|
throttledScrollToBottom()
|
2025-07-18 16:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 切换会话
|
|
|
|
|
const switchChat = (index: number) => {
|
|
|
|
|
if (index === currentChatIndex.value) return
|
|
|
|
|
|
|
|
|
|
// 保存当前会话
|
|
|
|
|
if (messages.value.length > 0 && chatHistory.value[currentChatIndex.value]) {
|
|
|
|
|
chatHistory.value[currentChatIndex.value] = {
|
|
|
|
|
messages: [...messages.value],
|
|
|
|
|
title: messages.value[0]?.text.slice(0, 20) + '...' || '新会话',
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
conversationId: chatHistory.value[currentChatIndex.value].conversationId,
|
|
|
|
|
lastMessageId: chatHistory.value[currentChatIndex.value].lastMessageId
|
|
|
|
|
}
|
|
|
|
|
saveChatHistory()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 切换到选中的会话
|
|
|
|
|
currentChatIndex.value = index
|
|
|
|
|
messages.value = [...chatHistory.value[index].messages]
|
|
|
|
|
|
|
|
|
|
// 发射会话切换事件
|
|
|
|
|
emit('chatSwitched', index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新当前会话的会话ID和消息ID
|
|
|
|
|
const updateSessionIds = (conversationId: string, messageId: string) => {
|
|
|
|
|
if (chatHistory.value[currentChatIndex.value]) {
|
|
|
|
|
chatHistory.value[currentChatIndex.value].conversationId = conversationId
|
|
|
|
|
chatHistory.value[currentChatIndex.value].lastMessageId = messageId
|
|
|
|
|
saveChatHistory()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 格式化时间
|
|
|
|
|
const formatTime = (timestamp: number) => {
|
|
|
|
|
const date = new Date(timestamp)
|
|
|
|
|
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成Excel文件名 (格式: YYYYMMDDHHMM.excel)
|
|
|
|
|
const generateExcelFileName = (timestamp: number = Date.now()) => {
|
|
|
|
|
const date = new Date(timestamp)
|
|
|
|
|
const year = date.getFullYear()
|
|
|
|
|
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
|
|
|
const day = date.getDate().toString().padStart(2, '0')
|
|
|
|
|
const hour = date.getHours().toString().padStart(2, '0')
|
|
|
|
|
const minute = date.getMinutes().toString().padStart(2, '0')
|
|
|
|
|
|
|
|
|
|
return `${year}${month}${day}${hour}${minute}.excel`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 监听消息变化,自动保存到当前会话
|
|
|
|
|
watch(messages, (newMessages) => {
|
|
|
|
|
if (newMessages.length > 0 && currentChatIndex.value < chatHistory.value.length) {
|
|
|
|
|
const currentSession = chatHistory.value[currentChatIndex.value]
|
|
|
|
|
chatHistory.value[currentChatIndex.value] = {
|
|
|
|
|
messages: [...newMessages],
|
|
|
|
|
title: newMessages[0]?.text.slice(0, 20) + '...' || '新会话',
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
conversationId: currentSession?.conversationId,
|
|
|
|
|
lastMessageId: currentSession?.lastMessageId
|
|
|
|
|
}
|
|
|
|
|
saveChatHistory()
|
|
|
|
|
}
|
|
|
|
|
}, { deep: true })
|
|
|
|
|
|
2025-07-30 15:30:23 +08:00
|
|
|
|
|
|
|
|
|
// 用户是否正在拖动滚动条
|
|
|
|
|
let isUserScrolling = false
|
|
|
|
|
|
2025-07-18 16:38:18 +08:00
|
|
|
|
// 初始化时加载会话历史
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
loadChatHistory()
|
|
|
|
|
// 如果没有历史会话,创建一个新会话
|
|
|
|
|
if (chatHistory.value.length === 0) {
|
|
|
|
|
await createNewChat()
|
|
|
|
|
}
|
2025-07-30 15:30:23 +08:00
|
|
|
|
|
|
|
|
|
//监听用户是否拖动滚动条
|
|
|
|
|
messagesContainer.value?.addEventListener('scroll', () => {
|
|
|
|
|
const el = messagesContainer.value
|
|
|
|
|
if (!el) return
|
|
|
|
|
const threshold = 100
|
|
|
|
|
isUserScrolling = el.scrollHeight - el.scrollTop - el.clientHeight > threshold
|
|
|
|
|
})
|
|
|
|
|
|
2025-07-18 16:38:18 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 解析ECharts代码块
|
|
|
|
|
const parseECharts = (text: string): { processedText: string; echarts: EChartData[] } => {
|
|
|
|
|
const echartsRegex = /```echarts\n([\s\S]*?)\n```/g
|
|
|
|
|
const echarts: EChartData[] = []
|
|
|
|
|
let match
|
|
|
|
|
let processedText = text
|
|
|
|
|
|
|
|
|
|
while ((match = echartsRegex.exec(text)) !== null) {
|
|
|
|
|
try {
|
|
|
|
|
const echartsJson = match[1].trim()
|
|
|
|
|
const option = JSON.parse(echartsJson)
|
|
|
|
|
const id = `echart_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
|
|
|
|
|
|
|
|
echarts.push({ id, option })
|
|
|
|
|
|
|
|
|
|
// 将echarts代码块替换为占位符
|
|
|
|
|
processedText = processedText.replace(match[0], `<div class="echart-placeholder" data-chart-id="${id}"></div>`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('解析ECharts配置失败:', error)
|
|
|
|
|
// 如果解析失败,保留原始代码块
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { processedText, echarts }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 格式化答案
|
|
|
|
|
const formatAnswer = async (text: string) => {
|
|
|
|
|
try {
|
|
|
|
|
// 先解析ECharts代码块
|
|
|
|
|
const { processedText, echarts } = parseECharts(text)
|
|
|
|
|
|
|
|
|
|
// 配置marked以更好地支持表格
|
|
|
|
|
const html = await marked.parse(processedText, {
|
|
|
|
|
gfm: true,
|
|
|
|
|
breaks: true,
|
|
|
|
|
async: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 为表格添加容器包装和强制样式属性
|
|
|
|
|
const wrappedHtml = html.replace(
|
|
|
|
|
/<table>/g,
|
2025-07-30 15:30:23 +08:00
|
|
|
|
`<details class="table-wrapper" open>
|
|
|
|
|
<summary>
|
|
|
|
|
Expand / Collapse table
|
|
|
|
|
</summary>
|
|
|
|
|
<div class="table-container"><table class="excel-table" border="1" width="auto" cellspacing="0" cellpadding="0">`
|
2025-07-18 16:38:18 +08:00
|
|
|
|
).replace(
|
2025-07-30 15:30:23 +08:00
|
|
|
|
/<\/table>/g,
|
|
|
|
|
'</table></div></details>'
|
|
|
|
|
|
2025-07-18 16:38:18 +08:00
|
|
|
|
).replace(
|
|
|
|
|
/<th>/g,
|
|
|
|
|
'<th style="border: 1px solid #cbd5e1; padding: 12px; background: #f1f5f9;">'
|
|
|
|
|
).replace(
|
|
|
|
|
/<td>/g,
|
|
|
|
|
'<td style="border: 1px solid #cbd5e1; padding: 12px;">'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const sanitizedHtml = DOMPurify.sanitize(wrappedHtml, {
|
|
|
|
|
ALLOWED_TAGS: [
|
|
|
|
|
'strong', 'em', 'code', 'pre', 'a', 'br', 'p',
|
|
|
|
|
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'span',
|
|
|
|
|
'blockquote', 'hr', 'table', 'thead', 'tbody',
|
2025-07-30 15:30:23 +08:00
|
|
|
|
'tr', 'th', 'td', 'div', 'details', 'summary'
|
2025-07-18 16:38:18 +08:00
|
|
|
|
],
|
2025-07-30 15:30:23 +08:00
|
|
|
|
ALLOWED_ATTR: ['href', 'target', 'class', 'style', 'data-chart-id', 'border', 'cellspacing', 'cellpadding', 'open']
|
2025-07-18 16:38:18 +08:00
|
|
|
|
}) as string
|
|
|
|
|
|
|
|
|
|
console.log('Markdown输入:', processedText)
|
|
|
|
|
console.log('生成的HTML:', sanitizedHtml)
|
|
|
|
|
|
|
|
|
|
return { html: sanitizedHtml, echarts }
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error formatting answer:', error)
|
|
|
|
|
return { html: text, echarts: [] }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sendMessage = async () => {
|
|
|
|
|
if (!inputMessage.value.trim() || isLoading.value) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
isLoading.value = true
|
|
|
|
|
const userQuery = inputMessage.value
|
|
|
|
|
inputMessage.value = ''
|
|
|
|
|
|
|
|
|
|
// 添加用户消息
|
|
|
|
|
const userMessage: Message = {
|
|
|
|
|
text: userQuery,
|
|
|
|
|
isUser: true,
|
|
|
|
|
}
|
|
|
|
|
messages.value.push(userMessage)
|
|
|
|
|
|
|
|
|
|
// 发射消息接收事件
|
|
|
|
|
emit('messageReceived', userMessage)
|
|
|
|
|
|
|
|
|
|
// 创建响应式机器人消息
|
|
|
|
|
const botMessage = reactive<Message>({
|
|
|
|
|
text: '',
|
|
|
|
|
isUser: false,
|
|
|
|
|
isLoading: true,
|
|
|
|
|
formattedText: ''
|
|
|
|
|
})
|
|
|
|
|
messages.value.push(botMessage)
|
|
|
|
|
await scrollToBottom()
|
|
|
|
|
|
|
|
|
|
// 获取当前会话的会话ID
|
|
|
|
|
const currentSession = chatHistory.value[currentChatIndex.value]
|
|
|
|
|
const conversationId = currentSession?.conversationId
|
|
|
|
|
|
|
|
|
|
// 发起请求
|
|
|
|
|
const response = await sendChatMessage({
|
|
|
|
|
content: userQuery,
|
|
|
|
|
userId: props.userId,
|
|
|
|
|
chatType: props.chatType,
|
|
|
|
|
conversationId: conversationId // 如果有会话ID则带上
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 显示中止按钮
|
|
|
|
|
showStopButton.value = true
|
|
|
|
|
|
|
|
|
|
if (!response.body) throw new Error('No response body')
|
|
|
|
|
|
|
|
|
|
const reader = response.body.getReader()
|
|
|
|
|
const decoder = new TextDecoder()
|
|
|
|
|
let buffer = ''
|
|
|
|
|
let newConversationId = ''
|
|
|
|
|
let newMessageId = ''
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
const { done, value } = await reader.read()
|
|
|
|
|
if (done) break
|
|
|
|
|
|
|
|
|
|
buffer += decoder.decode(value, { stream: true })
|
|
|
|
|
const chunks = buffer.split(/\n\n/)
|
|
|
|
|
buffer = chunks.pop() || ''
|
|
|
|
|
|
|
|
|
|
for (const chunk of chunks) {
|
|
|
|
|
const eventData = chunk.trim()
|
|
|
|
|
if (!eventData) continue
|
|
|
|
|
|
|
|
|
|
const jsonData = eventData.replace(/^data:\s*/, '')
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(jsonData) as ChatMessageResponse
|
|
|
|
|
if (data.answer) {
|
|
|
|
|
botMessage.text += data.answer
|
|
|
|
|
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
|
|
|
|
|
botMessage.formattedText = formatted.html
|
|
|
|
|
botMessage.echarts = formatted.echarts
|
|
|
|
|
|
|
|
|
|
// 更新会话ID和消息ID
|
|
|
|
|
if (data.conversationId) {
|
|
|
|
|
newConversationId = data.conversationId
|
|
|
|
|
}
|
|
|
|
|
if (data.messageId) {
|
|
|
|
|
newMessageId = data.messageId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取并存储taskId
|
|
|
|
|
if (data.taskId) {
|
|
|
|
|
currentTaskId.value = data.taskId
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 17:57:22 +08:00
|
|
|
|
throttledScrollToBottom()
|
2025-07-18 16:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('解析错误:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理剩余数据
|
|
|
|
|
if (buffer) {
|
|
|
|
|
const eventData = buffer.trim()
|
|
|
|
|
if (eventData) {
|
|
|
|
|
try {
|
|
|
|
|
const jsonData = eventData.replace(/^data:\s*/, '')
|
|
|
|
|
const data = JSON.parse(jsonData)
|
|
|
|
|
if (data.answer) {
|
|
|
|
|
botMessage.text += data.answer
|
|
|
|
|
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
|
|
|
|
|
botMessage.formattedText = formatted.html
|
|
|
|
|
botMessage.echarts = formatted.echarts
|
|
|
|
|
|
|
|
|
|
if (data.conversationId) {
|
|
|
|
|
newConversationId = data.conversationId
|
|
|
|
|
}
|
|
|
|
|
if (data.messageId) {
|
|
|
|
|
newMessageId = data.messageId
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('最终解析错误:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新会话ID和消息ID
|
|
|
|
|
if (newConversationId || newMessageId) {
|
|
|
|
|
updateSessionIds(newConversationId, newMessageId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取文件来源数据(仅当chatType=1时)
|
|
|
|
|
if (newConversationId && newMessageId && props.chatType === '1') {
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await getFilePathList(newConversationId, newMessageId)
|
|
|
|
|
if (data && ((data.tracePdf && data.tracePdf.length > 0) || (data.traceExcel && data.traceExcel.trim()))) {
|
|
|
|
|
botMessage.sources = data
|
|
|
|
|
botMessage.conversationId = newConversationId
|
|
|
|
|
botMessage.messageId = newMessageId
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log('获取文件来源失败:', error)
|
|
|
|
|
// 不显示错误信息,静默失败
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发射消息接收事件
|
|
|
|
|
emit('messageReceived', botMessage)
|
|
|
|
|
|
|
|
|
|
// 流式请求完成,发送事件
|
|
|
|
|
emit('streamComplete', {
|
|
|
|
|
conversationId: newConversationId,
|
|
|
|
|
messageId: newMessageId,
|
|
|
|
|
content: userQuery
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('请求失败:', error)
|
|
|
|
|
const errorMessage: Message = {
|
|
|
|
|
text: '请求失败,请稍后重试',
|
|
|
|
|
isUser: false,
|
|
|
|
|
}
|
|
|
|
|
messages.value.push(errorMessage)
|
|
|
|
|
emit('messageReceived', errorMessage)
|
|
|
|
|
} finally {
|
|
|
|
|
isLoading.value = false
|
|
|
|
|
showStopButton.value = false // 隐藏中止按钮
|
|
|
|
|
currentTaskId.value = '' // 清除taskId
|
|
|
|
|
if (messages.value[messages.value.length - 1]?.isLoading) {
|
|
|
|
|
messages.value[messages.value.length - 1].isLoading = false
|
|
|
|
|
}
|
2025-07-30 15:30:23 +08:00
|
|
|
|
scrollToBottomForce()
|
2025-07-18 16:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 15:30:23 +08:00
|
|
|
|
const scrollToBottomForce = async () => {
|
2025-07-18 16:38:18 +08:00
|
|
|
|
await nextTick()
|
|
|
|
|
if (messagesContainer.value) {
|
|
|
|
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 15:30:23 +08:00
|
|
|
|
//将锚点滚动到最底部
|
|
|
|
|
const scrollToBottom = async () => {
|
|
|
|
|
await nextTick()
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
if (messagesContainer.value && !isUserScrolling) {
|
|
|
|
|
bottomAnchor.value.scrollIntoView({
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
block: 'start'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},60)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 17:57:22 +08:00
|
|
|
|
const throttledScrollToBottom = throttle(() => {
|
2025-07-30 15:30:23 +08:00
|
|
|
|
scrollToBottom()
|
|
|
|
|
}, 300,{ leading: true, trailing: true })
|
2025-07-29 17:57:22 +08:00
|
|
|
|
|
2025-07-18 16:38:18 +08:00
|
|
|
|
// 解析方法
|
|
|
|
|
const parseThink = (text: string) => {
|
|
|
|
|
const thinkRegex = /<think>(.*?)<\/think>/s
|
|
|
|
|
const match = text.match(thinkRegex)
|
|
|
|
|
return {
|
|
|
|
|
think: match?.[1] || '',
|
|
|
|
|
answer: match ? text.replace(thinkRegex, '').trim() : text,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取文件类型图标 - 使用动态导入确保路径正确
|
|
|
|
|
const getFileTypeIcon = (fileType: string) => {
|
2025-07-29 15:14:47 +08:00
|
|
|
|
const icons = require.context('@/assets/img/filetype-icon', false, /\.png$/)
|
|
|
|
|
// console.log(icons)
|
|
|
|
|
// console.log('所有图标 keys:', icons.keys())
|
|
|
|
|
|
2025-07-18 16:38:18 +08:00
|
|
|
|
const getIconUrl = (iconName: string) => {
|
2025-07-29 15:14:47 +08:00
|
|
|
|
const fullName = `./${iconName}.png`
|
|
|
|
|
if (icons.keys().includes(fullName)) {
|
|
|
|
|
return icons(fullName)
|
|
|
|
|
} else {
|
|
|
|
|
// console.warn('图标不存在:', fullName)
|
|
|
|
|
return ''
|
|
|
|
|
}
|
2025-07-18 16:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const iconMap: Record<string, string> = {
|
|
|
|
|
'pdf': getIconUrl('pdf'),
|
|
|
|
|
'doc': getIconUrl('word'),
|
|
|
|
|
'docx': getIconUrl('word'),
|
|
|
|
|
'xls': getIconUrl('excel'),
|
|
|
|
|
'xlsx': getIconUrl('excel'),
|
|
|
|
|
'ppt': getIconUrl('ppt'),
|
|
|
|
|
'pptx': getIconUrl('ppt'),
|
|
|
|
|
'txt': getIconUrl('txt'),
|
|
|
|
|
'xml': getIconUrl('xml'),
|
|
|
|
|
'zip': getIconUrl('zip'),
|
|
|
|
|
'mp4': getIconUrl('video'),
|
|
|
|
|
'avi': getIconUrl('video'),
|
|
|
|
|
'mp3': getIconUrl('audio'),
|
|
|
|
|
'wav': getIconUrl('audio'),
|
|
|
|
|
'jpg': getIconUrl('img'),
|
|
|
|
|
'jpeg': getIconUrl('img'),
|
|
|
|
|
'png': getIconUrl('img'),
|
|
|
|
|
'gif': getIconUrl('img'),
|
|
|
|
|
}
|
|
|
|
|
return iconMap[fileType.toLowerCase()] || getIconUrl('unknown')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取文件扩展名
|
|
|
|
|
const getFileExtension = (fileName: string) => {
|
|
|
|
|
return fileName.split('.').pop()?.toLowerCase() || ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理文件来源点击事件
|
|
|
|
|
const handleSourceClick = async (source: TraceFile, conversationId?: string, messageId?: string) => {
|
|
|
|
|
// 只有chatType=1时才允许点击预览
|
|
|
|
|
if (props.chatType !== '1') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!conversationId || !messageId) {
|
|
|
|
|
ElMessage.warning('缺少会话信息,无法预览文件')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
previewLoading.value = true
|
|
|
|
|
filePreviewVisible.value = true
|
|
|
|
|
|
|
|
|
|
// 构造完整的PDF预览URL
|
|
|
|
|
const baseUrl = window.location.protocol + '//' + window.location.host
|
|
|
|
|
previewFileUrl.value = baseUrl + source.filePath
|
|
|
|
|
|
|
|
|
|
console.log('预览文件:', source)
|
|
|
|
|
console.log('预览URL:', previewFileUrl.value)
|
|
|
|
|
// 发射文件来源点击事件,让父组件处理
|
|
|
|
|
emit('sourceClick', source)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取文件预览失败:', error)
|
|
|
|
|
ElMessage.error('获取文件预览失败')
|
|
|
|
|
filePreviewVisible.value = false
|
|
|
|
|
} finally {
|
|
|
|
|
previewLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理Excel表格点击事件
|
|
|
|
|
const handleExcelClick = async (excelContent: string) => {
|
|
|
|
|
try {
|
|
|
|
|
excelPreviewLoading.value = true
|
|
|
|
|
excelPreviewVisible.value = true
|
|
|
|
|
|
|
|
|
|
// 将 \n 字符串转换为真正的换行符
|
|
|
|
|
const processedContent = excelContent.replace(/\\n/g, '\n')
|
|
|
|
|
|
|
|
|
|
// 如果marked转换失败,尝试手动解析markdown表格
|
|
|
|
|
let htmlContent = ''
|
|
|
|
|
try {
|
|
|
|
|
const formattedContent = await formatAnswer(processedContent)
|
|
|
|
|
htmlContent = formattedContent.html
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('marked解析失败,尝试手动解析:', error)
|
|
|
|
|
htmlContent = parseMarkdownTable(processedContent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果HTML中没有table标签,尝试手动解析
|
|
|
|
|
if (!htmlContent.includes('<table')) {
|
|
|
|
|
console.warn('未检测到表格,手动解析markdown')
|
|
|
|
|
htmlContent = parseMarkdownTable(processedContent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
excelPreviewContent.value = htmlContent
|
|
|
|
|
|
|
|
|
|
console.log('原始Excel数据:', excelContent)
|
|
|
|
|
console.log('处理后的Excel数据:', processedContent)
|
|
|
|
|
console.log('最终HTML:', htmlContent)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('解析Excel表格失败:', error)
|
|
|
|
|
ElMessage.error('解析Excel表格失败')
|
|
|
|
|
excelPreviewVisible.value = false
|
|
|
|
|
} finally {
|
|
|
|
|
excelPreviewLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 手动解析markdown表格为HTML
|
|
|
|
|
const parseMarkdownTable = (markdown: string): string => {
|
|
|
|
|
const lines = markdown.trim().split('\n')
|
|
|
|
|
if (lines.length < 3) return markdown // 至少需要表头、分隔线、数据行
|
|
|
|
|
|
|
|
|
|
const headerLine = lines[0]
|
|
|
|
|
const separatorLine = lines[1]
|
|
|
|
|
const dataLines = lines.slice(2)
|
|
|
|
|
|
|
|
|
|
// 检查是否是有效的markdown表格
|
|
|
|
|
if (!headerLine.includes('|') || !separatorLine.includes('---')) {
|
|
|
|
|
return markdown
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析表头
|
|
|
|
|
const headers = headerLine.split('|').map(h => h.trim()).filter(h => h)
|
|
|
|
|
|
|
|
|
|
// 解析数据行
|
|
|
|
|
const rows = dataLines.map(line =>
|
|
|
|
|
line.split('|').map(cell => cell.trim()).filter(cell => cell)
|
|
|
|
|
).filter(row => row.length > 0)
|
|
|
|
|
|
|
|
|
|
// 生成HTML表格
|
|
|
|
|
let html = `
|
|
|
|
|
<div class="table-container">
|
|
|
|
|
<table class="excel-table" border="1" cellspacing="0" cellpadding="0" style="border-collapse: collapse; width: auto; border: 2px solid #cbd5e1;">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
headers.forEach(header => {
|
|
|
|
|
html += `<th style="border: 1px solid #cbd5e1; padding: 12px; background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); font-weight: 600; text-align: left; position: sticky; top: 0; z-index: 2; white-space: nowrap;">${header}</th>`
|
|
|
|
|
//html += `<th style="border: 1px solid #cbd5e1; padding: 12px; background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); font-weight: 600; text-align: left;">${header}</th>`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
html += `
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
rows.forEach((row, rowIndex) => {
|
|
|
|
|
html += '<tr>'
|
|
|
|
|
row.forEach((cell, cellIndex) => {
|
|
|
|
|
const cellStyle = rowIndex % 2 === 0
|
|
|
|
|
? 'border: 1px solid #cbd5e1; padding: 12px; background: white;'
|
|
|
|
|
: 'border: 1px solid #cbd5e1; padding: 12px; background: #f8fafc;'
|
|
|
|
|
html += `<td style="${cellStyle}">${cell}</td>`
|
|
|
|
|
})
|
|
|
|
|
html += '</tr>'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
html += `
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
return html
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取助手头像
|
|
|
|
|
const getAssistantAvatar = () => {
|
|
|
|
|
return props.assistantAvatar || new URL('@/assets/assistant.png', import.meta.url).href
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 中止流式回答
|
|
|
|
|
const handleStopStream = async () => {
|
|
|
|
|
if (currentTaskId.value) {
|
|
|
|
|
try {
|
|
|
|
|
await stopMessagesStream(Number(props.chatType), currentTaskId.value, props.userId)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏中止按钮
|
|
|
|
|
showStopButton.value = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 停止加载状态
|
|
|
|
|
isLoading.value = false
|
|
|
|
|
|
|
|
|
|
// 移除当前消息的加载状态
|
|
|
|
|
if (messages.value.length > 0) {
|
|
|
|
|
const lastMessage = messages.value[messages.value.length - 1]
|
|
|
|
|
if (!lastMessage.isUser && lastMessage.isLoading) {
|
|
|
|
|
lastMessage.isLoading = false
|
|
|
|
|
if (!lastMessage.text.trim()) {
|
|
|
|
|
lastMessage.text = '已停止生成'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清除taskId
|
|
|
|
|
currentTaskId.value = ''
|
|
|
|
|
|
|
|
|
|
ElMessage.success('已停止生成')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('停止流式回答失败:', error)
|
|
|
|
|
ElMessage.error('停止失败,请重试')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理消息悬浮事件
|
|
|
|
|
const handleMessageHover = (index: number, isHovered: boolean) => {
|
|
|
|
|
hoveredMessageIndex.value = isHovered ? index : -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 复制消息内容
|
|
|
|
|
const handleCopyMessage = async (message: Message) => {
|
|
|
|
|
const textToCopy = message.text
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 优先使用现代的 Clipboard API
|
|
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
|
|
|
await navigator.clipboard.writeText(textToCopy)
|
|
|
|
|
ElMessage.success('已复制到剪贴板')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 回退到传统方法
|
|
|
|
|
const textArea = document.createElement('textarea')
|
|
|
|
|
textArea.value = textToCopy
|
|
|
|
|
textArea.style.position = 'fixed'
|
|
|
|
|
textArea.style.left = '-999999px'
|
|
|
|
|
textArea.style.top = '-999999px'
|
|
|
|
|
document.body.appendChild(textArea)
|
|
|
|
|
textArea.focus()
|
|
|
|
|
textArea.select()
|
|
|
|
|
|
|
|
|
|
const successful = document.execCommand('copy')
|
|
|
|
|
document.body.removeChild(textArea)
|
|
|
|
|
|
|
|
|
|
if (successful) {
|
|
|
|
|
ElMessage.success('已复制到剪贴板')
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('execCommand failed')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('复制失败:', error)
|
|
|
|
|
ElMessage.error('复制失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 编辑消息
|
|
|
|
|
const handleEditMessage = (message: Message, index: number) => {
|
|
|
|
|
editingMessageIndex.value = index
|
|
|
|
|
editingMessageText.value = message.text
|
|
|
|
|
hoveredMessageIndex.value = -1 // 隐藏操作按钮
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 取消编辑
|
|
|
|
|
const handleCancelEdit = () => {
|
|
|
|
|
editingMessageIndex.value = -1
|
|
|
|
|
editingMessageText.value = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保存编辑
|
|
|
|
|
const handleSaveEdit = async () => {
|
|
|
|
|
if (!editingMessageText.value.trim()) return
|
|
|
|
|
|
|
|
|
|
const newText = editingMessageText.value.trim()
|
|
|
|
|
|
|
|
|
|
// 重置编辑状态
|
|
|
|
|
editingMessageIndex.value = -1
|
|
|
|
|
editingMessageText.value = ''
|
|
|
|
|
|
|
|
|
|
// 将编辑后的文本作为新的用户问题发送
|
|
|
|
|
inputMessage.value = newText
|
|
|
|
|
await sendMessage()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重新生成回答
|
|
|
|
|
const handleRegenerateMessage = async (index: number) => {
|
|
|
|
|
if (index === 0) return
|
|
|
|
|
|
|
|
|
|
// 获取前一条用户消息
|
|
|
|
|
const userMessage = messages.value[index - 1]
|
|
|
|
|
if (!userMessage || !userMessage.isUser) return
|
|
|
|
|
|
|
|
|
|
// 删除当前助手消息及之后的消息
|
|
|
|
|
messages.value = messages.value.slice(0, index)
|
|
|
|
|
|
|
|
|
|
// 直接重新生成,不创建新的用户消息
|
|
|
|
|
await regenerateResponse(userMessage.text)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重新生成回答的核心逻辑
|
|
|
|
|
const regenerateResponse = async (userQuery: string) => {
|
|
|
|
|
try {
|
|
|
|
|
isLoading.value = true
|
|
|
|
|
|
|
|
|
|
// 创建响应式机器人消息
|
|
|
|
|
const botMessage = reactive<Message>({
|
|
|
|
|
text: '',
|
|
|
|
|
isUser: false,
|
|
|
|
|
isLoading: true,
|
|
|
|
|
formattedText: ''
|
|
|
|
|
})
|
|
|
|
|
messages.value.push(botMessage)
|
2025-07-30 15:30:23 +08:00
|
|
|
|
throttledScrollToBottom()
|
2025-07-18 16:38:18 +08:00
|
|
|
|
|
|
|
|
|
// 获取当前会话的会话ID
|
|
|
|
|
const currentSession = chatHistory.value[currentChatIndex.value]
|
|
|
|
|
const conversationId = currentSession?.conversationId
|
|
|
|
|
|
|
|
|
|
// 发起请求
|
|
|
|
|
const response = await sendChatMessage({
|
|
|
|
|
content: userQuery,
|
|
|
|
|
userId: props.userId,
|
|
|
|
|
chatType: props.chatType,
|
|
|
|
|
conversationId: conversationId
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 显示中止按钮
|
|
|
|
|
showStopButton.value = true
|
|
|
|
|
|
|
|
|
|
if (!response.body) throw new Error('No response body')
|
|
|
|
|
|
|
|
|
|
const reader = response.body.getReader()
|
|
|
|
|
const decoder = new TextDecoder()
|
|
|
|
|
let buffer = ''
|
|
|
|
|
let newConversationId = ''
|
|
|
|
|
let newMessageId = ''
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
const { done, value } = await reader.read()
|
|
|
|
|
if (done) break
|
|
|
|
|
|
|
|
|
|
buffer += decoder.decode(value, { stream: true })
|
|
|
|
|
const chunks = buffer.split(/\n\n/)
|
|
|
|
|
buffer = chunks.pop() || ''
|
|
|
|
|
|
|
|
|
|
for (const chunk of chunks) {
|
|
|
|
|
const eventData = chunk.trim()
|
|
|
|
|
if (!eventData) continue
|
|
|
|
|
|
|
|
|
|
const jsonData = eventData.replace(/^data:\s*/, '')
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(jsonData) as ChatMessageResponse
|
|
|
|
|
if (data.answer) {
|
|
|
|
|
botMessage.text += data.answer
|
|
|
|
|
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
|
|
|
|
|
botMessage.formattedText = formatted.html
|
|
|
|
|
botMessage.echarts = formatted.echarts
|
|
|
|
|
|
|
|
|
|
// 更新会话ID和消息ID
|
|
|
|
|
if (data.conversationId) {
|
|
|
|
|
newConversationId = data.conversationId
|
|
|
|
|
}
|
|
|
|
|
if (data.messageId) {
|
|
|
|
|
newMessageId = data.messageId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取并存储taskId
|
|
|
|
|
if (data.taskId) {
|
|
|
|
|
currentTaskId.value = data.taskId
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 15:30:23 +08:00
|
|
|
|
throttledScrollToBottom()
|
2025-07-18 16:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('解析错误:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理剩余数据
|
|
|
|
|
if (buffer) {
|
|
|
|
|
const eventData = buffer.trim()
|
|
|
|
|
if (eventData) {
|
|
|
|
|
try {
|
|
|
|
|
const jsonData = eventData.replace(/^data:\s*/, '')
|
|
|
|
|
const data = JSON.parse(jsonData)
|
|
|
|
|
if (data.answer) {
|
|
|
|
|
botMessage.text += data.answer
|
|
|
|
|
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
|
|
|
|
|
botMessage.formattedText = formatted.html
|
|
|
|
|
botMessage.echarts = formatted.echarts
|
|
|
|
|
|
|
|
|
|
if (data.conversationId) {
|
|
|
|
|
newConversationId = data.conversationId
|
|
|
|
|
}
|
|
|
|
|
if (data.messageId) {
|
|
|
|
|
newMessageId = data.messageId
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('最终解析错误:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新会话ID和消息ID
|
|
|
|
|
if (newConversationId || newMessageId) {
|
|
|
|
|
updateSessionIds(newConversationId, newMessageId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取文件来源数据(仅当chatType=1时)
|
|
|
|
|
if (newConversationId && newMessageId && props.chatType === '1') {
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await getFilePathList(newConversationId, newMessageId)
|
|
|
|
|
if (data && ((data.tracePdf && data.tracePdf.length > 0) || (data.traceExcel && data.traceExcel.trim()))) {
|
|
|
|
|
botMessage.sources = data
|
|
|
|
|
botMessage.conversationId = newConversationId
|
|
|
|
|
botMessage.messageId = newMessageId
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log('获取文件来源失败:', error)
|
|
|
|
|
// 不显示错误信息,静默失败
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发射消息接收事件
|
|
|
|
|
emit('messageReceived', botMessage)
|
|
|
|
|
|
|
|
|
|
// 流式请求完成,发送事件
|
|
|
|
|
emit('streamComplete', {
|
|
|
|
|
conversationId: newConversationId,
|
|
|
|
|
messageId: newMessageId,
|
|
|
|
|
content: userQuery
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('重新生成失败:', error)
|
|
|
|
|
const errorMessage: Message = {
|
|
|
|
|
text: '重新生成失败,请稍后重试',
|
|
|
|
|
isUser: false,
|
|
|
|
|
}
|
|
|
|
|
messages.value.push(errorMessage)
|
|
|
|
|
emit('messageReceived', errorMessage)
|
|
|
|
|
} finally {
|
|
|
|
|
isLoading.value = false
|
|
|
|
|
showStopButton.value = false
|
|
|
|
|
currentTaskId.value = ''
|
|
|
|
|
if (messages.value[messages.value.length - 1]?.isLoading) {
|
|
|
|
|
messages.value[messages.value.length - 1].isLoading = false
|
|
|
|
|
}
|
2025-07-30 15:30:23 +08:00
|
|
|
|
scrollToBottomForce()
|
2025-07-18 16:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 点赞消息
|
|
|
|
|
const handleLikeMessage = (index: number) => {
|
|
|
|
|
// TODO: 实现点赞逻辑,调用相关API
|
|
|
|
|
ElMessage.success('感谢您的反馈!')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 点踩消息
|
|
|
|
|
const handleDislikeMessage = (index: number) => {
|
|
|
|
|
// TODO: 实现点踩逻辑,调用相关API
|
|
|
|
|
ElMessage.info('感谢您的反馈,我们会继续改进')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理推荐问题点击事件
|
|
|
|
|
const handleRecommendQuestionClick = (question: string) => {
|
|
|
|
|
if (!question.trim() || isLoading.value) return
|
|
|
|
|
|
|
|
|
|
// 将问题内容填入输入框
|
|
|
|
|
inputMessage.value = question
|
|
|
|
|
|
|
|
|
|
// 发送问题
|
|
|
|
|
sendMessage()
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.chat-container {
|
2025-07-30 15:30:23 +08:00
|
|
|
|
height: 100%;
|
2025-07-18 16:38:18 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 侧边栏样式 */
|
|
|
|
|
.chat-sidebar {
|
|
|
|
|
width: 280px;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border-right: 1px solid #e2e8f0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
position: relative;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
transform-origin: left;
|
|
|
|
|
will-change: width, transform;
|
|
|
|
|
border-top-left-radius: 12px;
|
|
|
|
|
border-bottom-left-radius: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-collapsed {
|
|
|
|
|
width: 48px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-header {
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
transition: opacity 0.2s ease;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-collapsed .sidebar-header {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-history {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 0.5rem;
|
|
|
|
|
transition: opacity 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-collapsed .chat-history {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-header h3 {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.new-chat-btn {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
width: 100%;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background: #4f46e5;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 0.5rem;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
color: white;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.new-chat-btn:hover {
|
|
|
|
|
background: #4338ca;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.new-chat-icon {
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-history-item {
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-history-item:hover {
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-history-item.active {
|
|
|
|
|
background: #e0e7ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-title {
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-time {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-toggle {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: -12px;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
|
|
|
z-index: 10;
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-toggle:hover {
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
transform: translateY(-50%) scale(1.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-toggle:active {
|
|
|
|
|
transform: translateY(-50%) scale(0.95);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toggle-icon {
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
transform-origin: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toggle-icon.collapsed {
|
|
|
|
|
transform: rotate(180deg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 主聊天区域样式调整 */
|
|
|
|
|
.chat-main {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
min-width: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
border-top-right-radius: 12px;
|
|
|
|
|
border-bottom-right-radius: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.main-expanded {
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-messages {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 2rem 0;
|
|
|
|
|
padding-bottom: 100px;
|
|
|
|
|
scroll-behavior: smooth;
|
|
|
|
|
background: #f9fafb;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
padding: 1rem 2rem;
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 100%;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-item.user-message {
|
|
|
|
|
flex-direction: row-reverse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
margin: 0 1rem;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-content-wrapper {
|
|
|
|
|
max-width: 85%;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 当消息包含图表时,扩大消息气泡宽度 */
|
|
|
|
|
.assistant-message .message-content-wrapper:has(.echarts-container) {
|
|
|
|
|
max-width: 95%;
|
|
|
|
|
width: 95%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 兼容性:通过类名控制图表消息宽度 */
|
|
|
|
|
.assistant-message .message-content-wrapper.has-charts {
|
|
|
|
|
max-width: 95%;
|
|
|
|
|
width: 95%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.message-content {
|
|
|
|
|
padding: 1rem 1.5rem;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
position: relative;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.user-message .message-content {
|
|
|
|
|
background: #EFF6FF;
|
|
|
|
|
border-radius: 16px 2px 16px 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.assistant-message .message-content {
|
|
|
|
|
background: #FDFDFD;
|
|
|
|
|
border-radius: 2px 16px 16px 16px;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 消息操作按钮样式 */
|
|
|
|
|
.message-actions {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: -2.5rem;
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
backdrop-filter: blur(8px);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 0.5rem;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
|
|
|
animation: fadeInUp 0.2s ease-out;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.user-actions {
|
|
|
|
|
right: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.assistant-actions {
|
|
|
|
|
left: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 操作按钮样式 */
|
|
|
|
|
.action-btn {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: none;
|
|
|
|
|
background: rgba(0, 0, 0, 0.04);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn:hover {
|
|
|
|
|
background: rgba(0, 0, 0, 0.08);
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn:active {
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-icon {
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
transition: color 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn:hover .action-icon {
|
|
|
|
|
color: #374151;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 特定按钮颜色 */
|
|
|
|
|
.copy-btn:hover {
|
|
|
|
|
background: rgba(59, 130, 246, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.copy-btn:hover .action-icon {
|
|
|
|
|
color: #3b82f6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edit-btn:hover {
|
|
|
|
|
background: rgba(245, 158, 11, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edit-btn:hover .action-icon {
|
|
|
|
|
color: #f59e0b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.regenerate-btn:hover {
|
|
|
|
|
background: rgba(16, 185, 129, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.regenerate-btn:hover .action-icon {
|
|
|
|
|
color: #10b981;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.like-btn:hover {
|
|
|
|
|
background: rgba(34, 197, 94, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.like-btn:hover .action-icon {
|
|
|
|
|
color: #22c55e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dislike-btn:hover {
|
|
|
|
|
background: rgba(239, 68, 68, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dislike-btn:hover .action-icon {
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 按钮出现动画 */
|
|
|
|
|
@keyframes fadeInUp {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateY(10px);
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 编辑容器样式 */
|
|
|
|
|
.edit-container {
|
|
|
|
|
width: 100%;
|
|
|
|
|
max-width: 600px;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edit-input-wrapper {
|
|
|
|
|
position: relative;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border: 1.5px solid #e5e7eb;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edit-input-wrapper:focus-within {
|
|
|
|
|
border-color: #3b82f6;
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edit-textarea {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edit-textarea :deep(.el-textarea__inner) {
|
|
|
|
|
border: none;
|
|
|
|
|
background: transparent;
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
padding: 1rem 1rem 3rem 1rem;
|
|
|
|
|
resize: none;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edit-textarea :deep(.el-textarea__inner:focus) {
|
|
|
|
|
border: none;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edit-actions {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 0.75rem;
|
|
|
|
|
right: 0.75rem;
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edit-btn {
|
|
|
|
|
padding: 0.375rem 0.75rem;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
outline: none;
|
|
|
|
|
min-width: 48px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cancel-btn {
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
border-color: #e2e8f0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cancel-btn:hover {
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
color: #475569;
|
|
|
|
|
border-color: #cbd5e1;
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cancel-btn:active {
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.save-btn {
|
|
|
|
|
background: #3b82f6;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
border-color: #3b82f6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.save-btn:hover:not(:disabled) {
|
|
|
|
|
background: #2563eb;
|
|
|
|
|
border-color: #2563eb;
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.save-btn:active:not(:disabled) {
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.save-btn:disabled {
|
|
|
|
|
background: #e5e7eb;
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
border-color: #e5e7eb;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
transform: none;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-header {
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-sender {
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-body {
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.think-section {
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.think-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.05em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.think-icon {
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
margin-right: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content {
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 标题样式 */
|
|
|
|
|
.answer-content :deep(h1),
|
|
|
|
|
.answer-content :deep(h2),
|
|
|
|
|
.answer-content :deep(h3) {
|
|
|
|
|
margin: 1.5rem 0 1rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(h1) {
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
padding-bottom: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(h2) {
|
|
|
|
|
font-size: 1.3rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(h3) {
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 段落和列表样式 */
|
|
|
|
|
.answer-content :deep(p) {
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(ul),
|
|
|
|
|
.answer-content :deep(ol) {
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
padding-left: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(li) {
|
|
|
|
|
margin: 0.5rem 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(ul li) {
|
|
|
|
|
list-style-type: disc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(ol li) {
|
|
|
|
|
list-style-type: decimal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 链接样式 */
|
|
|
|
|
.answer-content :deep(a) {
|
|
|
|
|
color: #4f46e5;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
border-bottom: 1px solid transparent;
|
|
|
|
|
transition: border-color 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(a:hover) {
|
|
|
|
|
border-bottom-color: #4f46e5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 强调和加粗 */
|
|
|
|
|
.answer-content :deep(em) {
|
|
|
|
|
font-style: italic;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(strong) {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #111827;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 代码样式 */
|
|
|
|
|
.answer-content :deep(code) {
|
|
|
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
padding: 0.2em 0.4em;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(pre) {
|
|
|
|
|
background: #1e293b;
|
|
|
|
|
color: #e2e8f0;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(pre code) {
|
|
|
|
|
background: none;
|
|
|
|
|
padding: 0;
|
|
|
|
|
color: inherit;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 引用样式 */
|
|
|
|
|
.answer-content :deep(blockquote) {
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
border-left: 4px solid #e5e7eb;
|
|
|
|
|
background: #f9fafb;
|
|
|
|
|
color: #4b5563;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(blockquote p) {
|
|
|
|
|
margin: 0.5rem 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 水平线 */
|
|
|
|
|
.answer-content :deep(hr) {
|
|
|
|
|
margin: 2rem 0;
|
|
|
|
|
border: 0;
|
|
|
|
|
border-top: 1px solid #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 文件来源样式 */
|
|
|
|
|
.sources-section {
|
|
|
|
|
margin-top: 1rem;
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sources-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.05em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sources-icon {
|
|
|
|
|
width: 14px;
|
|
|
|
|
height: 14px;
|
|
|
|
|
margin-right: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sources-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.source-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
max-width: 200px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.source-item:hover {
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
border-color: #cbd5e1;
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.source-icon {
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.source-name {
|
|
|
|
|
color: #374151;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
flex: 1;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 表格样式优化 */
|
|
|
|
|
.answer-content :deep(table) {
|
|
|
|
|
width: auto;
|
|
|
|
|
min-width: 1000px;
|
|
|
|
|
max-width: 2500px;
|
|
|
|
|
margin: 1.5rem 0;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
|
|
background: white;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 15:30:23 +08:00
|
|
|
|
::v-deep .table-wrapper summary {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: list-item;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
/* border: 1px solid #e2e8f0; */
|
|
|
|
|
/* border-radius: 6px; */
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-07-18 16:38:18 +08:00
|
|
|
|
/* .excel-table th,
|
|
|
|
|
.excel-table td {
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
} */
|
|
|
|
|
|
|
|
|
|
.excel-table th {
|
|
|
|
|
white-space: nowrap; /* 表头不换行 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.excel-table td {
|
|
|
|
|
min-width: 1000px; /* 内容列设置最小宽度 */
|
|
|
|
|
white-space: normal; /* 允许内容换行(可选) */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(th),
|
|
|
|
|
.answer-content :deep(td) {
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
text-align: left;
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
max-width: 200px;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
white-space: normal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(th) {
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #374151;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.025em;
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: 0;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
border-bottom: 2px solid #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(td) {
|
|
|
|
|
background: white;
|
|
|
|
|
transition: background-color 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(tr:hover td) {
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(tr:nth-child(even) td) {
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(tr:nth-child(even):hover td) {
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-container {
|
|
|
|
|
max-height: 350px; /* 限制高度,超出时滚动 */
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
overflow-x: auto; /* 保留横向滚动条(必要) */
|
|
|
|
|
width: 100%;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-container table {
|
|
|
|
|
max-width: 2000px;
|
|
|
|
|
table-layout: auto; /* 或 fixed,视实际内容适配性而定 */
|
|
|
|
|
width: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格容器,支持横向滚动 */
|
|
|
|
|
.answer-content :deep(.table-container) {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
width:100%;
|
|
|
|
|
max-width: 100%; /* 限制在父容器内不超出 */
|
|
|
|
|
max-height: 350px; /* 限制表格高度,触发纵向滚动(根据实际需要可调) */
|
|
|
|
|
margin: 1.5rem 0;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移动端表格优化 */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.answer-content :deep(table) {
|
|
|
|
|
min-width: 600px;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(th),
|
|
|
|
|
.answer-content :deep(td) {
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
max-width: 150px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content :deep(th) {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
border-bottom: 2px solid #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-area {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
background: #f9fafb;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 中止按钮容器 */
|
|
|
|
|
.stop-button-container {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: -2.5rem;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
z-index: 10;
|
|
|
|
|
animation: fadeInDown 0.3s ease-out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 中止按钮样式 */
|
|
|
|
|
.stop-button {
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: rgba(239, 68, 68, 0.1);
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
border: 1.5px solid rgba(239, 68, 68, 0.2);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
box-shadow: 0 3px 8px rgba(239, 68, 68, 0.15);
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background: radial-gradient(circle, rgba(239, 68, 68, 0.1) 0%, transparent 70%);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button:hover {
|
|
|
|
|
background: rgba(239, 68, 68, 0.15);
|
|
|
|
|
border-color: rgba(239, 68, 68, 0.3);
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.25);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button:hover::before {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button:active {
|
|
|
|
|
transform: scale(0.95);
|
|
|
|
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-icon {
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button:hover .stop-icon {
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 按钮出现动画 */
|
|
|
|
|
@keyframes fadeInDown {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(-50%) translateY(-20px);
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateX(-50%) translateY(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-wrapper {
|
|
|
|
|
max-width: 900px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
position: relative;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-wrapper:focus-within {
|
|
|
|
|
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-textarea__inner) {
|
|
|
|
|
padding: 1rem 4rem 1rem 1.5rem;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
background: transparent;
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
resize: none;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-textarea__inner:focus) {
|
|
|
|
|
border-color: #4f46e5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-button {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 0.75rem;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
height: 2.5rem;
|
|
|
|
|
width: 2.5rem;
|
|
|
|
|
min-height: 2.5rem;
|
|
|
|
|
padding: 0;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: #4f46e5;
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
border: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(79, 70, 229, 0.25);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-button:not(:disabled) {
|
|
|
|
|
background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-button:disabled {
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-button:hover:not(:disabled) {
|
|
|
|
|
transform: translateY(-50%) scale(1.05);
|
|
|
|
|
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
|
|
|
|
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-icon {
|
|
|
|
|
width: 1.25rem;
|
|
|
|
|
height: 1.25rem;
|
|
|
|
|
margin-left: 2px;
|
|
|
|
|
transition: transform 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-icon {
|
|
|
|
|
width: 1.25rem;
|
|
|
|
|
height: 1.25rem;
|
|
|
|
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
border-top-color: #ffffff;
|
|
|
|
|
animation: spin 0.8s linear infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
to {
|
|
|
|
|
transform: rotate(360deg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-button.is-loading) {
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-button.is-loading .el-loading-mask) {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-button.is-loading::before) {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-footer {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.chat-container {
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-sidebar {
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-main {
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-sidebar {
|
|
|
|
|
position: absolute;
|
|
|
|
|
height: 100%;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-collapsed {
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-toggle {
|
|
|
|
|
right: -16px;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-item {
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-content-wrapper {
|
|
|
|
|
max-width: 90%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-content {
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移动端消息操作按钮适配 */
|
|
|
|
|
.message-actions {
|
|
|
|
|
bottom: -2rem;
|
|
|
|
|
padding: 0.375rem;
|
|
|
|
|
gap: 0.375rem;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
width: 28px;
|
|
|
|
|
height: 28px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-icon {
|
|
|
|
|
width: 14px;
|
|
|
|
|
height: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-area {
|
|
|
|
|
padding: 1.5rem 1rem;
|
|
|
|
|
padding-top: 2.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
margin: 0 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-messages {
|
|
|
|
|
padding-bottom: 100px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-button {
|
|
|
|
|
right: 0.5rem;
|
|
|
|
|
height: 2.25rem;
|
|
|
|
|
width: 2.25rem;
|
|
|
|
|
min-height: 2.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-button .send-icon,
|
|
|
|
|
.loading-icon {
|
|
|
|
|
width: 1rem;
|
|
|
|
|
height: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-icon {
|
|
|
|
|
border-width: 1.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移动端文件来源样式 */
|
|
|
|
|
.sources-section {
|
|
|
|
|
padding: 0.5rem;
|
|
|
|
|
margin-top: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sources-list {
|
|
|
|
|
gap: 0.375rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.source-item {
|
|
|
|
|
padding: 0.375rem 0.5rem;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
max-width: 150px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.source-icon {
|
|
|
|
|
width: 14px;
|
|
|
|
|
height: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移动端中止按钮适配 */
|
|
|
|
|
.stop-button-container {
|
|
|
|
|
top: -2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-icon {
|
|
|
|
|
width: 14px;
|
|
|
|
|
height: 14px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-content {
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-dots {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-dots .dot {
|
|
|
|
|
width: 0.5rem;
|
|
|
|
|
height: 0.5rem;
|
|
|
|
|
background-color: #e2e8f0;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: pulse 1.4s infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-dots .dot:nth-child(2) {
|
|
|
|
|
animation-delay: 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-dots .dot:nth-child(3) {
|
|
|
|
|
animation-delay: 0.4s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
0%, 80%, 100% {
|
|
|
|
|
transform: scale(0.8);
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
}
|
|
|
|
|
40% {
|
|
|
|
|
transform: scale(1);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.typing-indicator {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.25rem;
|
|
|
|
|
margin-top: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dot {
|
|
|
|
|
width: 4px;
|
|
|
|
|
height: 4px;
|
|
|
|
|
background: #6b7280;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: bounce 1.4s infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dot:nth-child(2) { animation-delay: 0.2s; }
|
|
|
|
|
.dot:nth-child(3) { animation-delay: 0.4s; }
|
|
|
|
|
|
|
|
|
|
@keyframes bounce {
|
|
|
|
|
0%, 80%, 100% { transform: translateY(0); }
|
|
|
|
|
40% { transform: translateY(-4px); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar-icon {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 文件预览模态框样式 */
|
|
|
|
|
.file-preview-dialog {
|
|
|
|
|
.el-dialog {
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__header {
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 12px 12px 0 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__title {
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__headerbtn {
|
|
|
|
|
.el-dialog__close {
|
|
|
|
|
color: white;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__body {
|
|
|
|
|
padding: 0;
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-content {
|
|
|
|
|
min-height: 600px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
border-radius: 0 0 12px 12px;
|
|
|
|
|
|
|
|
|
|
.pdf-preview {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 600px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
|
|
|
background: white;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ECharts 图表样式 */
|
|
|
|
|
.echarts-container {
|
|
|
|
|
margin: 1rem auto; /* 使用auto实现水平居中 */
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
/* 缩减到60%宽度,保持良好的显示比例 */
|
|
|
|
|
width: 60%;
|
|
|
|
|
max-width: 1000px; /* 设置最大宽度避免在大屏上过宽 */
|
|
|
|
|
min-width: 400px; /* 设置最小宽度确保可读性 */
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 15:30:23 +08:00
|
|
|
|
::v-deep .icon-chevron {
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
transition: transform 0.3s ease;
|
|
|
|
|
color: #333; /* 让箭头颜色和文字颜色一致 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-07-18 16:38:18 +08:00
|
|
|
|
.chart-wrapper {
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-wrapper:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-chart {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 500px;
|
|
|
|
|
min-height: 500px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 响应式图表适配 */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.echarts-container {
|
|
|
|
|
margin: 0.75rem auto; /* 移动端也使用auto居中 */
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
/* 移动端保持70%宽度,确保在小屏幕上有足够显示空间 */
|
|
|
|
|
width: 70%;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
min-width: 280px; /* 移动端最小宽度 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-wrapper {
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-chart {
|
|
|
|
|
height: 400px;
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移动端消息内容区域调整 */
|
|
|
|
|
.message-content-wrapper {
|
|
|
|
|
max-width: 92%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移动端包含图表的消息气泡宽度 */
|
|
|
|
|
.assistant-message .message-content-wrapper.has-charts {
|
|
|
|
|
max-width: 96%;
|
|
|
|
|
width: 96%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移动端文件预览适配 */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.file-preview-dialog {
|
|
|
|
|
.el-dialog {
|
|
|
|
|
width: 95% !important;
|
|
|
|
|
margin: 2.5vh auto;
|
|
|
|
|
max-height: 95vh;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__header {
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__title {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-content {
|
|
|
|
|
min-height: 500px;
|
|
|
|
|
|
|
|
|
|
.pdf-preview {
|
|
|
|
|
height: 500px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Excel表格预览模态框样式 */
|
|
|
|
|
.excel-preview-dialog {
|
|
|
|
|
.el-dialog {
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__header {
|
|
|
|
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 12px 12px 0 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__title {
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__headerbtn {
|
|
|
|
|
.el-dialog__close {
|
|
|
|
|
color: white;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__body {
|
|
|
|
|
padding: 0;
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
max-height: 70vh;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.excel-preview-content {
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
border-radius: 0 0 12px 12px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
|
|
|
|
.excel-table-content {
|
|
|
|
|
width: 100%;
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
|
|
|
|
/* 表格样式优化 - 加强边框显示,使用更高优先级 */
|
|
|
|
|
:deep(table),
|
|
|
|
|
:deep(.answer-content table),
|
|
|
|
|
:deep(.table-container table) {
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
border-collapse: collapse !important;
|
|
|
|
|
margin: 0 !important;
|
|
|
|
|
font-size: 14px !important;
|
|
|
|
|
border: 2px solid #cbd5e1 !important;
|
|
|
|
|
background: white !important;
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
overflow: hidden !important;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08) !important;
|
|
|
|
|
max-width: none !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(th),
|
|
|
|
|
:deep(td),
|
|
|
|
|
:deep(.answer-content th),
|
|
|
|
|
:deep(.answer-content td),
|
|
|
|
|
:deep(.table-container th),
|
|
|
|
|
:deep(.table-container td) {
|
|
|
|
|
padding: 12px 16px !important;
|
|
|
|
|
text-align: left !important;
|
|
|
|
|
border: 1px solid #cbd5e1 !important;
|
|
|
|
|
word-break: break-word !important;
|
|
|
|
|
vertical-align: top !important;
|
|
|
|
|
position: relative !important;
|
|
|
|
|
max-width: none !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(th),
|
|
|
|
|
:deep(.answer-content th),
|
|
|
|
|
:deep(.table-container th) {
|
|
|
|
|
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%) !important;
|
|
|
|
|
font-weight: 600 !important;
|
|
|
|
|
color: #1e293b !important;
|
|
|
|
|
position: sticky !important;
|
|
|
|
|
top: 0 !important;
|
|
|
|
|
z-index: 2 !important;
|
|
|
|
|
border-bottom: 2px solid #94a3b8 !important;
|
|
|
|
|
border-right: 1px solid #94a3b8 !important;
|
|
|
|
|
font-size: 13px !important;
|
|
|
|
|
text-transform: uppercase !important;
|
|
|
|
|
letter-spacing: 0.025em !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(td),
|
|
|
|
|
:deep(.answer-content td),
|
|
|
|
|
:deep(.table-container td) {
|
|
|
|
|
background: white !important;
|
|
|
|
|
border-right: 1px solid #cbd5e1 !important;
|
|
|
|
|
border-bottom: 1px solid #cbd5e1 !important;
|
|
|
|
|
transition: all 0.2s ease !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(tr:nth-child(even) td),
|
|
|
|
|
:deep(.answer-content tr:nth-child(even) td),
|
|
|
|
|
:deep(.table-container tr:nth-child(even) td) {
|
|
|
|
|
background: #f8fafc !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(tr:hover td),
|
|
|
|
|
:deep(.answer-content tr:hover td),
|
|
|
|
|
:deep(.table-container tr:hover td) {
|
|
|
|
|
background: #e0f2fe !important;
|
|
|
|
|
transform: scale(1.002) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 第一列样式加强 */
|
|
|
|
|
:deep(th:first-child),
|
|
|
|
|
:deep(td:first-child),
|
|
|
|
|
:deep(.answer-content th:first-child),
|
|
|
|
|
:deep(.answer-content td:first-child),
|
|
|
|
|
:deep(.table-container th:first-child),
|
|
|
|
|
:deep(.table-container td:first-child) {
|
|
|
|
|
border-left: 2px solid #cbd5e1 !important;
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 最后一列右边框加强 */
|
|
|
|
|
:deep(th:last-child),
|
|
|
|
|
:deep(td:last-child),
|
|
|
|
|
:deep(.answer-content th:last-child),
|
|
|
|
|
:deep(.answer-content td:last-child),
|
|
|
|
|
:deep(.table-container th:last-child),
|
|
|
|
|
:deep(.table-container td:last-child) {
|
|
|
|
|
border-right: 2px solid #cbd5e1 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 最后一行底边框加强 */
|
|
|
|
|
:deep(tr:last-child td),
|
|
|
|
|
:deep(.answer-content tr:last-child td),
|
|
|
|
|
:deep(.table-container tr:last-child td) {
|
|
|
|
|
border-bottom: 2px solid #cbd5e1 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Markdown 表格容器样式 */
|
|
|
|
|
:deep(.table-container) {
|
|
|
|
|
overflow-x: auto !important;
|
|
|
|
|
margin: 1rem 0 !important;
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
|
|
|
|
|
border: 1px solid #cbd5e1 !important;
|
|
|
|
|
background: white !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 表格标题样式 */
|
|
|
|
|
:deep(h1),
|
|
|
|
|
:deep(h2),
|
|
|
|
|
:deep(h3) {
|
|
|
|
|
margin: 1.5rem 0 1rem !important;
|
|
|
|
|
font-weight: 600 !important;
|
|
|
|
|
color: #1e293b !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 段落间距 */
|
|
|
|
|
:deep(p) {
|
|
|
|
|
margin: 1rem 0 !important;
|
|
|
|
|
color: #374151 !important;
|
|
|
|
|
line-height: 1.6 !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移动端适配 */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.excel-preview-dialog {
|
|
|
|
|
.el-dialog {
|
|
|
|
|
width: 95% !important;
|
|
|
|
|
margin: 2.5vh auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.excel-table-content {
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
|
|
|
|
:deep(table) {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(th),
|
|
|
|
|
:deep(td) {
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Excel文件按钮样式 */
|
|
|
|
|
.excel-item {
|
|
|
|
|
border: 1px solid #22c55e !important;
|
|
|
|
|
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: linear-gradient(135deg, #bbf7d0 0%, #86efac 100%);
|
|
|
|
|
border-color: #16a34a !important;
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.source-name {
|
|
|
|
|
color: #15803d;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-questions-section {
|
|
|
|
|
margin-top: 1rem;
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-questions-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.05em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-icon {
|
|
|
|
|
width: 14px;
|
|
|
|
|
height: 14px;
|
|
|
|
|
margin-right: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-questions-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-question-btn {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
max-width: 280px;
|
|
|
|
|
color: #374151;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
outline: none;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-text {
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-question-btn::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background: linear-gradient(135deg, rgba(79, 70, 229, 0.1) 0%, rgba(67, 56, 202, 0.1) 100%);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-question-btn:hover:not(:disabled) {
|
|
|
|
|
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
|
|
|
|
border-color: #4f46e5;
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.2);
|
|
|
|
|
color: #4f46e5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-question-btn:hover:not(:disabled)::before {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-question-btn:active:not(:disabled) {
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
box-shadow: 0 2px 8px rgba(79, 70, 229, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-question-btn:disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
transform: none;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border-color: #e5e7eb;
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-question-btn:disabled::before {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-icon {
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
position: relative;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-question-btn:hover:not(:disabled) .question-icon {
|
|
|
|
|
color: #4f46e5;
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-question-btn:disabled .question-icon {
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 移动端适配 */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.recommend-questions-section {
|
|
|
|
|
padding: 0.5rem;
|
|
|
|
|
margin-top: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-questions-list {
|
|
|
|
|
gap: 0.375rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-question-btn {
|
|
|
|
|
padding: 0.375rem 0.5rem;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
max-width: 200px;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-text {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-icon {
|
|
|
|
|
width: 14px;
|
|
|
|
|
height: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-questions-header {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
margin-bottom: 0.375rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recommend-icon {
|
|
|
|
|
width: 12px;
|
|
|
|
|
height: 12px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|