|  | @@ -10,34 +10,34 @@
 | 
	
		
			
				|  |  |      <div class="chat-messages"  v-show="isChating" ref="messagesRef" :class="{ 'chat-active': isChating }">
 | 
	
		
			
				|  |  |        <div v-for="(message, index) in messages" :key="index" 
 | 
	
		
			
				|  |  |             :class="['message', message.role === 'user' ? 'user' : 'assistant']">
 | 
	
		
			
				|  |  | -        <div class="avatar">
 | 
	
		
			
				|  |  | +        <div class="avatar" v-if="message.content">
 | 
	
		
			
				|  |  |            <img :src="message.role === 'user' ? userAvatar : assistantAvatar" :alt="message.role">
 | 
	
		
			
				|  |  |          </div>
 | 
	
		
			
				|  |  | -        <div class="content">
 | 
	
		
			
				|  |  | -          <div class="text">{{ message.content }}</div>
 | 
	
		
			
				|  |  | +        <div class="content" v-if="message.content">
 | 
	
		
			
				|  |  | +          <div class="text markdown-body" v-html="message.content"></div>
 | 
	
		
			
				|  |  |            <div class="message-actions" v-if="message.role === 'assistant'">
 | 
	
		
			
				|  |  |              <div class="left-actions">
 | 
	
		
			
				|  |  |                <button class="action-btn" @click="copyContent(message.content)">
 | 
	
		
			
				|  |  |                  <img src="@/assets/icon-copy.png" alt="复制">
 | 
	
		
			
				|  |  |                </button>
 | 
	
		
			
				|  |  | -              <button class="action-btn" @click="toggleFavorite(message.id)">
 | 
	
		
			
				|  |  | +              <!-- <button class="action-btn" @click="toggleFavorite(message.id)">
 | 
	
		
			
				|  |  |                  <img src="@/assets/icon-star.png" alt="收藏">
 | 
	
		
			
				|  |  | -              </button>
 | 
	
		
			
				|  |  | +              </button> -->
 | 
	
		
			
				|  |  |                <button class="action-btn reanswer" @click="reAnswer(message.id)">
 | 
	
		
			
				|  |  |                  <img src="@/assets/icon-reanswer.png" alt="重新回答">
 | 
	
		
			
				|  |  |                  <span>重新回答</span>
 | 
	
		
			
				|  |  |                </button>
 | 
	
		
			
				|  |  |              </div>
 | 
	
		
			
				|  |  | -            <div class="right-actions">
 | 
	
		
			
				|  |  | +            <!-- <div class="right-actions">
 | 
	
		
			
				|  |  |                <button class="action-btn" @click="handleLike(message.id, true)">
 | 
	
		
			
				|  |  |                  <img src="@/assets/icon-like.png" alt="点赞">
 | 
	
		
			
				|  |  |                </button>
 | 
	
		
			
				|  |  |                <button class="action-btn" @click="handleLike(message.id, false)">
 | 
	
		
			
				|  |  |                  <img src="@/assets/icon-dislike.png" alt="反对">
 | 
	
		
			
				|  |  |                </button>
 | 
	
		
			
				|  |  | -            </div>
 | 
	
		
			
				|  |  | +            </div> -->
 | 
	
		
			
				|  |  |            </div>
 | 
	
		
			
				|  |  | -          <div class="time">{{ formatTime(message.time) }}</div>
 | 
	
		
			
				|  |  | +          <!-- <div class="time">{{ formatTime(message.time) }}</div> -->
 | 
	
		
			
				|  |  |          </div>
 | 
	
		
			
				|  |  |        </div>
 | 
	
		
			
				|  |  |        <div v-if="loading" class="message assistant">
 | 
	
	
		
			
				|  | @@ -61,7 +61,7 @@
 | 
	
		
			
				|  |  |          ></textarea>
 | 
	
		
			
				|  |  |          <div class="input-actions">
 | 
	
		
			
				|  |  |            <div class="select-wrap">
 | 
	
		
			
				|  |  | -            <el-select v-model="selectedModel" class="model-select">
 | 
	
		
			
				|  |  | +            <el-select v-model="selectedModel" class="model-select" :disabled="isChating">
 | 
	
		
			
				|  |  |                <el-option v-for="model in models" :key="model.id" :value="model.id" :label="model.name">
 | 
	
		
			
				|  |  |                  {{ model.name }}
 | 
	
		
			
				|  |  |                </el-option>
 | 
	
	
		
			
				|  | @@ -73,24 +73,23 @@
 | 
	
		
			
				|  |  |            </button>
 | 
	
		
			
				|  |  |          </div>
 | 
	
		
			
				|  |  |        </div>
 | 
	
		
			
				|  |  | -      <!-- 知识库列表 -->
 | 
	
		
			
				|  |  | -      <!-- <div class="knowledge-base" v-if="!isChating">
 | 
	
		
			
				|  |  | +    </div>
 | 
	
		
			
				|  |  | +    <!-- 知识库列表 -->
 | 
	
		
			
				|  |  | +    <!-- <div class="knowledge-base" v-if="!isChating">
 | 
	
		
			
				|  |  |          <div class="knowledge-tags">
 | 
	
		
			
				|  |  |            <button 
 | 
	
		
			
				|  |  |              v-for="(item, index) in knowledgeList" 
 | 
	
		
			
				|  |  |              :key="index"
 | 
	
		
			
				|  |  | -            :class="['knowledge-tag', { active: selectedKnowledge.includes(item.id) }]"
 | 
	
		
			
				|  |  | -            @click="toggleKnowledge(item.id)"
 | 
	
		
			
				|  |  | +            :class="['knowledge-tag', { active: selectedKnowledge.includes(item.datasetId) }]"
 | 
	
		
			
				|  |  | +            @click="toggleKnowledge(item.datasetId)"
 | 
	
		
			
				|  |  |            >
 | 
	
		
			
				|  |  |              {{ item.name }}
 | 
	
		
			
				|  |  |            </button>
 | 
	
		
			
				|  |  |          </div>
 | 
	
		
			
				|  |  |        </div> -->
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    </div>
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  |      <!-- 模版区域 -->
 | 
	
		
			
				|  |  | -    <div class="template-section">
 | 
	
		
			
				|  |  | +    <div class="template-section" v-if="!isChating">
 | 
	
		
			
				|  |  |        <div class="template-type">
 | 
	
		
			
				|  |  |          <div class="type-title">全文</div>
 | 
	
		
			
				|  |  |          <div class="type-list">
 | 
	
	
		
			
				|  | @@ -119,23 +118,33 @@
 | 
	
		
			
				|  |  |        </div>
 | 
	
		
			
				|  |  |      </div>
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    </div>
 | 
	
		
			
				|  |  |  </template>
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  <script setup>
 | 
	
		
			
				|  |  |  import { ref, onMounted, nextTick } from 'vue'
 | 
	
		
			
				|  |  | -import { useRouter } from 'vue-router'
 | 
	
		
			
				|  |  | +import { useRoute,useRouter } from 'vue-router'
 | 
	
		
			
				|  |  |  import userAvatar from '@/assets/user-avatar.png'
 | 
	
		
			
				|  |  |  import assistantAvatar from '@/assets/assistant-avatar.png'
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +import { post,get } from '@/utils/request'
 | 
	
		
			
				|  |  | +import { ElMessage } from 'element-plus'
 | 
	
		
			
				|  |  | +import MarkdownIt from 'markdown-it'
 | 
	
		
			
				|  |  | +import hljs from 'highlight.js'
 | 
	
		
			
				|  |  | +import 'highlight.js/styles/github.css'
 | 
	
		
			
				|  |  | +import { useUserStore } from '../stores/user'
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const userStore = useUserStore()
 | 
	
		
			
				|  |  |  const router = useRouter()
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +const route = useRoute()
 | 
	
		
			
				|  |  |  const messagesRef = ref(null)
 | 
	
		
			
				|  |  |  const inputMessage = ref('')
 | 
	
		
			
				|  |  |  const loading = ref(false)
 | 
	
		
			
				|  |  |  const isChating = ref(false)
 | 
	
		
			
				|  |  |  const selectedKnowledge = ref([])
 | 
	
		
			
				|  |  |  const selectedModel = ref('DeepSeek')
 | 
	
		
			
				|  |  | +const sessionId = ref('')
 | 
	
		
			
				|  |  | +const chatId = ref('')
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import iconTemplate1 from '@/assets/icon-template-1.png'
 | 
	
		
			
				|  |  |  import iconTemplate2 from '@/assets/icon-template-2.png'
 | 
	
	
		
			
				|  | @@ -145,10 +154,35 @@ const templateList1 = ref([
 | 
	
		
			
				|  |  |    { id: 2, name: '工作月报', content: '辅助生成内容全面、条理清晰的工作月 报.', icon: iconTemplate2 },
 | 
	
		
			
				|  |  |    { id: 3, name: '会议纪要', content: '全面分析市场需求、竞争环境、商业投 入.', icon: iconTemplate3 }
 | 
	
		
			
				|  |  |  ])
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  const templateList2 = ref([
 | 
	
		
			
				|  |  |    { id: 1, name: '商业计划书', content: '你是一个企业管理顾问,现在有一家公 司请', icon: iconTemplate1 },
 | 
	
		
			
				|  |  |  ])
 | 
	
		
			
				|  |  | +const useTemplate = async (id) => {
 | 
	
		
			
				|  |  | +  // 根据ID找到对应的模板
 | 
	
		
			
				|  |  | +  const template = [...templateList1.value, ...templateList2.value].find(t => t.id === id)
 | 
	
		
			
				|  |  | +  if (!template) return
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  try {
 | 
	
		
			
				|  |  | +    // 调用create接口
 | 
	
		
			
				|  |  | +    const response = await post('/admin/user/chat/create', {
 | 
	
		
			
				|  |  | +      userId: userStore.userInfo.id,
 | 
	
		
			
				|  |  | +      datasetIds: selectedKnowledge.value.join(','),
 | 
	
		
			
				|  |  | +      templateId: template.id
 | 
	
		
			
				|  |  | +    })
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (response.data) {
 | 
	
		
			
				|  |  | +      sessionId.value = response.data.sessionId
 | 
	
		
			
				|  |  | +      chatId.value = response.data.chatId
 | 
	
		
			
				|  |  | +      ElMessage.success('初始化对话成功')
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      ElMessage.error('初始化对话失败')
 | 
	
		
			
				|  |  | +      return
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  } catch (error) {
 | 
	
		
			
				|  |  | +    console.error('初始化对话失败:', error)
 | 
	
		
			
				|  |  | +    ElMessage.error('初始化对话失败,请重试')
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  // 对话模型列表
 | 
	
		
			
				|  |  |  const models = ref([
 | 
	
	
		
			
				|  | @@ -158,33 +192,45 @@ const models = ref([
 | 
	
		
			
				|  |  |  ])
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  // 知识库列表
 | 
	
		
			
				|  |  | -const knowledgeList = ref([
 | 
	
		
			
				|  |  | -  { id: 1, name: '知识库1' },
 | 
	
		
			
				|  |  | -  { id: 2, name: '知识库2' },
 | 
	
		
			
				|  |  | -  { id: 3, name: '知识库3' },
 | 
	
		
			
				|  |  | -  { id: 4, name: '知识库4' }
 | 
	
		
			
				|  |  | -])
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -const useTemplate = (id) => {
 | 
	
		
			
				|  |  | -  // 根据ID找到对应的模板
 | 
	
		
			
				|  |  | -  const template = [...templateList1.value, ...templateList2.value].find(t => t.id === id)
 | 
	
		
			
				|  |  | -  if (!template) return
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | -  // 转到SolutionChat页面,传递完整的模板信息
 | 
	
		
			
				|  |  | -  router.push({
 | 
	
		
			
				|  |  | -    path: '/solution-chat',
 | 
	
		
			
				|  |  | -    query: {
 | 
	
		
			
				|  |  | -      template: JSON.stringify({
 | 
	
		
			
				|  |  | -        id: template.id,
 | 
	
		
			
				|  |  | -        name: template.name,
 | 
	
		
			
				|  |  | -        content: template.content
 | 
	
		
			
				|  |  | -      })
 | 
	
		
			
				|  |  | +const knowledgeList = ref([])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 获取知识库列表
 | 
	
		
			
				|  |  | +const getKnowledgeList = async () => {
 | 
	
		
			
				|  |  | +  try {
 | 
	
		
			
				|  |  | +    const userId = userStore.userInfo.id
 | 
	
		
			
				|  |  | +    const response = await get('/admin/user/chat/knowInfoList?userId='+userId)
 | 
	
		
			
				|  |  | +    // console.log('response', response);
 | 
	
		
			
				|  |  | +    if (response.data) {
 | 
	
		
			
				|  |  | +      knowledgeList.value = response.data
 | 
	
		
			
				|  |  | +      // 默认选中第一个知识库
 | 
	
		
			
				|  |  | +      selectedKnowledge.value = [knowledgeList.value[0].datasetId]
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      ElMessage.error('获取知识库列表失败')
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | -  })
 | 
	
		
			
				|  |  | +  } catch (error) {
 | 
	
		
			
				|  |  | +    console.error('获取知识库列表失败:', error)
 | 
	
		
			
				|  |  | +    ElMessage.error('获取知识库列表失败')
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  const messages = ref([])
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +// 创建 markdown-it 实例,配置代码高亮
 | 
	
		
			
				|  |  | +const md = new MarkdownIt({
 | 
	
		
			
				|  |  | +  html: false,
 | 
	
		
			
				|  |  | +  breaks: true,
 | 
	
		
			
				|  |  | +  linkify: true,
 | 
	
		
			
				|  |  | +  highlight: function (str, lang) {
 | 
	
		
			
				|  |  | +    if (lang && hljs.getLanguage(lang)) {
 | 
	
		
			
				|  |  | +      try {
 | 
	
		
			
				|  |  | +        return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`
 | 
	
		
			
				|  |  | +      } catch (__) {}
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    // 使用默认的转义
 | 
	
		
			
				|  |  | +    return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>`
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  const formatTime = (time) => {
 | 
	
		
			
				|  |  |    return new Date(time).toLocaleTimeString()
 | 
	
		
			
				|  |  |  }
 | 
	
	
		
			
				|  | @@ -206,6 +252,10 @@ const scrollToBottom = async () => {
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  const handleSend = async () => {
 | 
	
		
			
				|  |  | +  if (selectedKnowledge.value.length === 0) {
 | 
	
		
			
				|  |  | +      ElMessage.warning('请先选择知识库')
 | 
	
		
			
				|  |  | +      return
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |    const message = inputMessage.value.trim()
 | 
	
		
			
				|  |  |    if (!message || loading.value) return
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -213,6 +263,28 @@ const handleSend = async () => {
 | 
	
		
			
				|  |  |      isChating.value = true
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +  // 如果没有session_id和chat_id,先获取它们
 | 
	
		
			
				|  |  | +  if (!sessionId.value || !chatId.value) {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const response = await post('/admin/user/chat/create', {
 | 
	
		
			
				|  |  | +        userId: userStore.userInfo.id,
 | 
	
		
			
				|  |  | +        datasetIds: selectedKnowledge.value.join(',')
 | 
	
		
			
				|  |  | +      })
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      if (response.data) {
 | 
	
		
			
				|  |  | +        sessionId.value = response.data.sessionId
 | 
	
		
			
				|  |  | +        chatId.value = response.data.chatId
 | 
	
		
			
				|  |  | +      } else {
 | 
	
		
			
				|  |  | +        ElMessage.error('初始化对话失败')
 | 
	
		
			
				|  |  | +        return
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('初始化对话失败:', error)
 | 
	
		
			
				|  |  | +      ElMessage.error('初始化对话失败,请重试')
 | 
	
		
			
				|  |  | +      return
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    // 添加用户消息
 | 
	
		
			
				|  |  |    messages.value.push({
 | 
	
		
			
				|  |  |      role: 'user',
 | 
	
	
		
			
				|  | @@ -220,41 +292,97 @@ const handleSend = async () => {
 | 
	
		
			
				|  |  |      time: new Date()
 | 
	
		
			
				|  |  |    })
 | 
	
		
			
				|  |  |    
 | 
	
		
			
				|  |  | -  inputMessage.value = ''
 | 
	
		
			
				|  |  |    await scrollToBottom()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    // 开始流式响应
 | 
	
		
			
				|  |  |    loading.value = true
 | 
	
		
			
				|  |  | -  // TODO: 实现流式响应的API调用
 | 
	
		
			
				|  |  |    simulateStreamResponse()
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -const simulateStreamResponse = () => {
 | 
	
		
			
				|  |  | -  const response = '这是一个模拟的AI响应消息。在实际应用中,这里应该调用后端API获取真实的流式响应。'
 | 
	
		
			
				|  |  | -  let index = 0
 | 
	
		
			
				|  |  | -  const messageObj = {
 | 
	
		
			
				|  |  | +const simulateStreamResponse = async () => {
 | 
	
		
			
				|  |  | +  loading.value = true;
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  const messageIndex = messages.value.length;
 | 
	
		
			
				|  |  | +  messages.value.push({
 | 
	
		
			
				|  |  |      id: Date.now(),
 | 
	
		
			
				|  |  |      role: 'assistant',
 | 
	
		
			
				|  |  |      content: '',
 | 
	
		
			
				|  |  |      time: new Date()
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  | -  messages.value.push(messageObj)
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  const timer = setInterval(() => {
 | 
	
		
			
				|  |  | -    if (index < response.length) {
 | 
	
		
			
				|  |  | -      messageObj.content += response[index]
 | 
	
		
			
				|  |  | -      index++
 | 
	
		
			
				|  |  | -      scrollToBottom()
 | 
	
		
			
				|  |  | -    } else {
 | 
	
		
			
				|  |  | -      clearInterval(timer)
 | 
	
		
			
				|  |  | -      loading.value = false
 | 
	
		
			
				|  |  | +  const token = localStorage.getItem('token')
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  let accumulatedText = ''; // 用于累积接收到的文本
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // /admin/user/chat/converse
 | 
	
		
			
				|  |  | +  // /admin/ragflow/chat/converse
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  //请求ai聊天接口
 | 
	
		
			
				|  |  | +  const eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}/admin/user/chat/converse?chat_id=${chatId.value}&question=${encodeURIComponent(inputMessage.value)}&stream=true&session_id=${sessionId.value}&user_id=${userStore.userInfo.id}&token=${token}`);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  eventSource.onmessage = async (event) => {  
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const data = JSON.parse(event.data);
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      if (data.data === true) {
 | 
	
		
			
				|  |  | +       
 | 
	
		
			
				|  |  | +        // accumulatedText = '<think>' + accumulatedText;
 | 
	
		
			
				|  |  | +          console.log('数据流结束',accumulatedText);
 | 
	
		
			
				|  |  | +        // 检查是否包含<think>标签
 | 
	
		
			
				|  |  | +        if (accumulatedText.startsWith('<think>') && accumulatedText.endsWith('</think>')) {
 | 
	
		
			
				|  |  | +          // 如果是think标签内容,直接显示原文
 | 
	
		
			
				|  |  | +          messages.value[messageIndex].content = accumulatedText;
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +          // 否则进行markdown渲染
 | 
	
		
			
				|  |  | +          const formattedText = md.render(accumulatedText);
 | 
	
		
			
				|  |  | +          messages.value[messageIndex].content = formattedText;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        eventSource.close();
 | 
	
		
			
				|  |  | +        loading.value = false;
 | 
	
		
			
				|  |  | +        await scrollToBottom();
 | 
	
		
			
				|  |  | +        // AI回答结束后跳转到solution-chat页面
 | 
	
		
			
				|  |  | +        router.push(`/solution-chat?historyId=${chatId.value}`);
 | 
	
		
			
				|  |  | +        return;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      if (data.data && data.data.answer) {
 | 
	
		
			
				|  |  | +        console.log('data.data.answer',data.data.answer);
 | 
	
		
			
				|  |  | +        // 累积接收到的文本
 | 
	
		
			
				|  |  | +        accumulatedText += data.data.answer;
 | 
	
		
			
				|  |  | +        // 临时显示文本
 | 
	
		
			
				|  |  | +        messages.value[messageIndex].content = accumulatedText;
 | 
	
		
			
				|  |  | +        await nextTick();
 | 
	
		
			
				|  |  | +        await scrollToBottom();
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('Parse error:', error);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | -  }, 50)
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  eventSource.onerror = (error) => {
 | 
	
		
			
				|  |  | +    console.error('EventSource error:', error);
 | 
	
		
			
				|  |  | +    eventSource.close();
 | 
	
		
			
				|  |  | +    loading.value = false;
 | 
	
		
			
				|  |  | +    if (!messages.value[messageIndex].content) {
 | 
	
		
			
				|  |  | +      ElMessage.error('对话请求失败,请重试');
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  eventSource.onopen = () => {
 | 
	
		
			
				|  |  | +    console.log('连接已建立');
 | 
	
		
			
				|  |  | +    // 清空输入框
 | 
	
		
			
				|  |  | +    inputMessage.value = '';
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +// 修改复制功能,去除HTML标签
 | 
	
		
			
				|  |  |  const copyContent = (content) => {
 | 
	
		
			
				|  |  | -  navigator.clipboard.writeText(content)
 | 
	
		
			
				|  |  | -  // TODO: 添加复制成功提示
 | 
	
		
			
				|  |  | +  // 创建临时元素来去除HTML标签
 | 
	
		
			
				|  |  | +  const temp = document.createElement('div')
 | 
	
		
			
				|  |  | +  temp.innerHTML = content
 | 
	
		
			
				|  |  | +  const plainText = temp.textContent || temp.innerText
 | 
	
		
			
				|  |  | +  navigator.clipboard.writeText(plainText)
 | 
	
		
			
				|  |  | +  ElMessage.success('复制成功')
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  const toggleFavorite = (messageId) => {
 | 
	
	
		
			
				|  | @@ -262,14 +390,121 @@ const toggleFavorite = (messageId) => {
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  const reAnswer = (messageId) => {
 | 
	
		
			
				|  |  | -  // TODO: 实现重新回答功能
 | 
	
		
			
				|  |  | +  // 找到当前消息的索引
 | 
	
		
			
				|  |  | +  const currentIndex = messages.value.findIndex(msg => msg.id === messageId);
 | 
	
		
			
				|  |  | +  if (currentIndex === -1) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 找到这条消息之前最近的用户消息
 | 
	
		
			
				|  |  | +  let userMessageIndex = currentIndex - 1;
 | 
	
		
			
				|  |  | +  while (userMessageIndex >= 0) {
 | 
	
		
			
				|  |  | +    if (messages.value[userMessageIndex].role === 'user') {
 | 
	
		
			
				|  |  | +      // 获取用户的问题
 | 
	
		
			
				|  |  | +      const question = messages.value[userMessageIndex].content;
 | 
	
		
			
				|  |  | +      // 设置输入框的内容
 | 
	
		
			
				|  |  | +      inputMessage.value = question;
 | 
	
		
			
				|  |  | +      // 触发发送
 | 
	
		
			
				|  |  | +      handleSend();
 | 
	
		
			
				|  |  | +      break;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    userMessageIndex--;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  scrollToBottom();
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  const handleLike = (messageId, isLike) => {
 | 
	
		
			
				|  |  |    // TODO: 实现点赞/反对功能
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +// 获取历史记录
 | 
	
		
			
				|  |  | +const getHistoryChat = async (historyId) => {
 | 
	
		
			
				|  |  | +  try {
 | 
	
		
			
				|  |  | +    const response = await get(`/admin/userHistoryLog/selectById?id=${historyId}`)
 | 
	
		
			
				|  |  | +    if (response.data) {
 | 
	
		
			
				|  |  | +      const { chatId: historyChatId, sessionId: historySessionId, askList } = response.data
 | 
	
		
			
				|  |  | +      chatId.value = historyChatId
 | 
	
		
			
				|  |  | +      sessionId.value = historySessionId
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 清空现有消息
 | 
	
		
			
				|  |  | +      messages.value = []
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 按照 askSeq 排序对话记录
 | 
	
		
			
				|  |  | +      const sortedMessages = askList.sort((a, b) => Number(a.askSeq) - Number(b.askSeq))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 添加所有对话记录
 | 
	
		
			
				|  |  | +      for (const message of sortedMessages) {
 | 
	
		
			
				|  |  | +        // 添加用户问题
 | 
	
		
			
				|  |  | +        messages.value.push({
 | 
	
		
			
				|  |  | +          role: 'user',
 | 
	
		
			
				|  |  | +          content: message.askContent,
 | 
	
		
			
				|  |  | +          time: new Date(message.createTime)
 | 
	
		
			
				|  |  | +        })
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 处理AI回答
 | 
	
		
			
				|  |  | +        try {
 | 
	
		
			
				|  |  | +          let answerContent = message.answerContent
 | 
	
		
			
				|  |  | +          let formattedAnswer = ''
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          // 移除开头的 "data:"
 | 
	
		
			
				|  |  | +          if (answerContent.startsWith('data:')) {
 | 
	
		
			
				|  |  | +            answerContent = answerContent.substring(5)
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          // 尝试解析第一个 JSON 对象(包含实际回答)
 | 
	
		
			
				|  |  | +          try {
 | 
	
		
			
				|  |  | +            const firstBrace = answerContent.indexOf('{')
 | 
	
		
			
				|  |  | +            const lastBrace = answerContent.indexOf('}{')
 | 
	
		
			
				|  |  | +            if (firstBrace !== -1 && lastBrace !== -1) {
 | 
	
		
			
				|  |  | +              const jsonStr = answerContent.substring(firstBrace, lastBrace + 1)
 | 
	
		
			
				|  |  | +              const jsonData = JSON.parse(jsonStr)
 | 
	
		
			
				|  |  | +              if (jsonData.data && jsonData.data.answer) {
 | 
	
		
			
				|  |  | +                formattedAnswer = jsonData.data.answer
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +          } catch (e) {
 | 
	
		
			
				|  |  | +            console.warn('解析回答内容失败:', e)
 | 
	
		
			
				|  |  | +            formattedAnswer = answerContent
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          // 如果没有成功解析出任何内容,使用原始内容
 | 
	
		
			
				|  |  | +          if (!formattedAnswer.trim()) {
 | 
	
		
			
				|  |  | +            formattedAnswer = answerContent
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          // 添加AI回答
 | 
	
		
			
				|  |  | +          messages.value.push({
 | 
	
		
			
				|  |  | +            id: message.id,
 | 
	
		
			
				|  |  | +            role: 'assistant',
 | 
	
		
			
				|  |  | +            content: md.render(formattedAnswer),
 | 
	
		
			
				|  |  | +            time: new Date(message.createTime)
 | 
	
		
			
				|  |  | +          })
 | 
	
		
			
				|  |  | +        } catch (error) {
 | 
	
		
			
				|  |  | +          console.error('处理回答内容失败:', error)
 | 
	
		
			
				|  |  | +          // 如果处理失败,直接显示原始内容
 | 
	
		
			
				|  |  | +          messages.value.push({
 | 
	
		
			
				|  |  | +            id: message.id,
 | 
	
		
			
				|  |  | +            role: 'assistant',
 | 
	
		
			
				|  |  | +            content: answerContent,
 | 
	
		
			
				|  |  | +            time: new Date(message.createTime)
 | 
	
		
			
				|  |  | +          })
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      isChating.value = true
 | 
	
		
			
				|  |  | +      await nextTick()
 | 
	
		
			
				|  |  | +      scrollToBottom()
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  } catch (error) {
 | 
	
		
			
				|  |  | +    console.error('获取历史记录失败:', error)
 | 
	
		
			
				|  |  | +    ElMessage.error('获取历史记录失败')
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  onMounted(() => {
 | 
	
		
			
				|  |  | +  const historyId = route.query.historyId
 | 
	
		
			
				|  |  | +  if (historyId) {
 | 
	
		
			
				|  |  | +    getHistoryChat(historyId)
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  getKnowledgeList()
 | 
	
		
			
				|  |  |    scrollToBottom()
 | 
	
		
			
				|  |  |  })
 | 
	
		
			
				|  |  |  </script>
 | 
	
	
		
			
				|  | @@ -281,6 +516,9 @@ onMounted(() => {
 | 
	
		
			
				|  |  |    flex-direction: column;
 | 
	
		
			
				|  |  |    background: url('@/assets/ai-chat-bg.png') no-repeat center center;
 | 
	
		
			
				|  |  |    background-size: cover;
 | 
	
		
			
				|  |  | +  &:has(.chat-active){
 | 
	
		
			
				|  |  | +    background: #fff;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    .welcome-section {
 | 
	
		
			
				|  |  |      margin-top: 100px;
 | 
	
	
		
			
				|  | @@ -298,7 +536,7 @@ onMounted(() => {
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    .knowledge-base {
 | 
	
		
			
				|  |  | -    max-width: 800px;
 | 
	
		
			
				|  |  | +    width: 800px;
 | 
	
		
			
				|  |  |      margin: 24px auto 0;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      .knowledge-tags {
 | 
	
	
		
			
				|  | @@ -309,20 +547,20 @@ onMounted(() => {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        .knowledge-tag {
 | 
	
		
			
				|  |  |          padding: 8px 16px;
 | 
	
		
			
				|  |  | -        border: 1px solid #dcdfe6;
 | 
	
		
			
				|  |  | -        border-radius: 20px;
 | 
	
		
			
				|  |  | -        background: rgba(255, 255, 255, 0.9);
 | 
	
		
			
				|  |  | +        border: 1px solid #DADDE8;
 | 
	
		
			
				|  |  | +        border-radius: 15px;
 | 
	
		
			
				|  |  | +        background: transparent;
 | 
	
		
			
				|  |  |          cursor: pointer;
 | 
	
		
			
				|  |  |          transition: all 0.3s;
 | 
	
		
			
				|  |  | +        color: #414967;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          &:hover {
 | 
	
		
			
				|  |  |            background: #f5f7fa;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          &.active {
 | 
	
		
			
				|  |  | -          background: $primary-color;
 | 
	
		
			
				|  |  | -          color: white;
 | 
	
		
			
				|  |  | -          border-color: $primary-color;
 | 
	
		
			
				|  |  | +          color: #FF7575;
 | 
	
		
			
				|  |  | +          border-color: #FF7575;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |      }
 | 
	
	
		
			
				|  | @@ -331,7 +569,7 @@ onMounted(() => {
 | 
	
		
			
				|  |  |    .chat-messages {
 | 
	
		
			
				|  |  |      flex: 1;
 | 
	
		
			
				|  |  |      overflow-y: auto;
 | 
	
		
			
				|  |  | -    padding: 20px;
 | 
	
		
			
				|  |  | +    padding: 20px 20%;
 | 
	
		
			
				|  |  |      transition: all 0.3s;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      &.chat-active {
 | 
	
	
		
			
				|  | @@ -340,14 +578,17 @@ onMounted(() => {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      .message {
 | 
	
		
			
				|  |  |        display: flex;
 | 
	
		
			
				|  |  | -      margin-bottom: 20px;
 | 
	
		
			
				|  |  | +      margin-bottom: 100px;
 | 
	
		
			
				|  |  |        .avatar{
 | 
	
		
			
				|  |  |            img{
 | 
	
		
			
				|  |  |              width: 40px;
 | 
	
		
			
				|  |  |              height: 40px;
 | 
	
		
			
				|  |  |            }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -      
 | 
	
		
			
				|  |  | +      .text {
 | 
	
		
			
				|  |  | +        padding: 10px 20px;
 | 
	
		
			
				|  |  | +        font-size: 16px;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  |        &.user {
 | 
	
		
			
				|  |  |          flex-direction: row-reverse;
 | 
	
		
			
				|  |  |          .content {
 | 
	
	
		
			
				|  | @@ -355,8 +596,8 @@ onMounted(() => {
 | 
	
		
			
				|  |  |            margin-left: 0;
 | 
	
		
			
				|  |  |            
 | 
	
		
			
				|  |  |            .text {
 | 
	
		
			
				|  |  | -            // background: $primary-color;
 | 
	
		
			
				|  |  | -            // color: white;
 | 
	
		
			
				|  |  | +            background: #FFF5F5;
 | 
	
		
			
				|  |  | +            color: #1B1C21;
 | 
	
		
			
				|  |  |              border-radius: 10px 2px 10px 10px;
 | 
	
		
			
				|  |  |            }
 | 
	
		
			
				|  |  |            
 | 
	
	
		
			
				|  | @@ -414,15 +655,17 @@ onMounted(() => {
 | 
	
		
			
				|  |  |      margin: 0 auto;
 | 
	
		
			
				|  |  |      padding: 20px;
 | 
	
		
			
				|  |  |      background: rgba(255, 255, 255, 0.9);
 | 
	
		
			
				|  |  | +    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
 | 
	
		
			
				|  |  |      backdrop-filter: blur(10px);
 | 
	
		
			
				|  |  |      border-radius: 20px;
 | 
	
		
			
				|  |  |      min-width: 800px;
 | 
	
		
			
				|  |  |      &.chat-active{
 | 
	
		
			
				|  |  |        position: fixed;
 | 
	
		
			
				|  |  |        bottom: 0;
 | 
	
		
			
				|  |  | -      left: 0;
 | 
	
		
			
				|  |  | +      left: $sidebar-width;
 | 
	
		
			
				|  |  |        right: 0;
 | 
	
		
			
				|  |  |        padding: 20px;
 | 
	
		
			
				|  |  | +      box-shadow: none;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      .input-container {
 | 
	
	
		
			
				|  | @@ -501,6 +744,99 @@ onMounted(() => {
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 添加 markdown 样式
 | 
	
		
			
				|  |  | +  .markdown-body {
 | 
	
		
			
				|  |  | +    line-height: 1.6;
 | 
	
		
			
				|  |  | +    font-size: 14px;
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    p {
 | 
	
		
			
				|  |  | +      margin: 8px 0;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    h1, h2, h3, h4, h5, h6 {
 | 
	
		
			
				|  |  | +      margin: 16px 0 8px;
 | 
	
		
			
				|  |  | +      font-weight: 600;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    code {
 | 
	
		
			
				|  |  | +      background-color: rgba(175, 184, 193, 0.2);
 | 
	
		
			
				|  |  | +      padding: 0.2em 0.4em;
 | 
	
		
			
				|  |  | +      border-radius: 6px;
 | 
	
		
			
				|  |  | +      font-family: monospace;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    pre {
 | 
	
		
			
				|  |  | +      background-color: #f6f8fa;
 | 
	
		
			
				|  |  | +      border-radius: 6px;
 | 
	
		
			
				|  |  | +      padding: 16px;
 | 
	
		
			
				|  |  | +      overflow: auto;
 | 
	
		
			
				|  |  | +      margin: 16px 0;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      code {
 | 
	
		
			
				|  |  | +        background-color: transparent;
 | 
	
		
			
				|  |  | +        padding: 0;
 | 
	
		
			
				|  |  | +        font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
 | 
	
		
			
				|  |  | +        font-size: 14px;
 | 
	
		
			
				|  |  | +        line-height: 1.45;
 | 
	
		
			
				|  |  | +        tab-size: 4;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      &.hljs {
 | 
	
		
			
				|  |  | +        background: #f6f8fa;
 | 
	
		
			
				|  |  | +        border: 1px solid #e1e4e8;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    ul, ol {
 | 
	
		
			
				|  |  | +      padding-left: 20px;
 | 
	
		
			
				|  |  | +      margin: 8px 0;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    table {
 | 
	
		
			
				|  |  | +      border-collapse: collapse;
 | 
	
		
			
				|  |  | +      margin: 8px 0;
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      th, td {
 | 
	
		
			
				|  |  | +        border: 1px solid #d0d7de;
 | 
	
		
			
				|  |  | +        padding: 6px 13px;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      th {
 | 
	
		
			
				|  |  | +        background-color: #f6f8fa;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    blockquote {
 | 
	
		
			
				|  |  | +      border-left: 4px solid #d0d7de;
 | 
	
		
			
				|  |  | +      padding-left: 16px;
 | 
	
		
			
				|  |  | +      margin: 8px 0;
 | 
	
		
			
				|  |  | +      color: #656d76;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    img {
 | 
	
		
			
				|  |  | +      max-width: 100%;
 | 
	
		
			
				|  |  | +      height: auto;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    a {
 | 
	
		
			
				|  |  | +      color: #0969da;
 | 
	
		
			
				|  |  | +      text-decoration: none;
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      &:hover {
 | 
	
		
			
				|  |  | +        text-decoration: underline;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    code {
 | 
	
		
			
				|  |  | +      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
 | 
	
		
			
				|  |  | +      font-size: 0.9em;
 | 
	
		
			
				|  |  | +      padding: 0.2em 0.4em;
 | 
	
		
			
				|  |  | +      margin: 0;
 | 
	
		
			
				|  |  | +      background-color: rgba(175, 184, 193, 0.2);
 | 
	
		
			
				|  |  | +      border-radius: 6px;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  .template-section{
 | 
	
		
			
				|  |  |    margin: 24px auto;
 |