|
@@ -1,714 +0,0 @@
|
|
|
-<template>
|
|
|
|
|
- <div class="stream-test-container">
|
|
|
|
|
- <div class="test-form">
|
|
|
|
|
- <div class="input-group">
|
|
|
|
|
- <label for="message">消息内容:</label>
|
|
|
|
|
- <textarea
|
|
|
|
|
- id="message"
|
|
|
|
|
- v-model="message"
|
|
|
|
|
- placeholder="请输入要发送的消息...(按空格键发送)"
|
|
|
|
|
- rows="3"
|
|
|
|
|
- @keydown="handleKeyDown"
|
|
|
|
|
- ></textarea>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="input-group">
|
|
|
|
|
- <label for="model">模型(可选):</label>
|
|
|
|
|
- <input
|
|
|
|
|
- id="model"
|
|
|
|
|
- v-model="model"
|
|
|
|
|
- placeholder="模型名称,留空使用默认"
|
|
|
|
|
- type="text"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="button-group">
|
|
|
|
|
- <button
|
|
|
|
|
- @click="startStream"
|
|
|
|
|
- :disabled="isStreaming || !message.trim()"
|
|
|
|
|
- class="start-btn"
|
|
|
|
|
- >
|
|
|
|
|
- {{ isStreaming ? '发送中...' : '开始流式+数据库测试' }}
|
|
|
|
|
- </button>
|
|
|
|
|
- <button
|
|
|
|
|
- @click="stopStream"
|
|
|
|
|
- :disabled="!isStreaming"
|
|
|
|
|
- class="stop-btn"
|
|
|
|
|
- >
|
|
|
|
|
- 停止
|
|
|
|
|
- </button>
|
|
|
|
|
- <button
|
|
|
|
|
- @click="clearResponse"
|
|
|
|
|
- class="clear-btn"
|
|
|
|
|
- >
|
|
|
|
|
- 清空响应
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="response-section">
|
|
|
|
|
- <div class="response-header">
|
|
|
|
|
- <h3>流式+数据库响应:</h3>
|
|
|
|
|
- <div class="status-indicator" :class="{ active: isStreaming }">
|
|
|
|
|
- {{ isStreaming ? '连接中...' : '已断开' }}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="response-content" ref="responseContainer">
|
|
|
|
|
- <div v-if="!responseContent && !isStreaming" class="empty-state">
|
|
|
|
|
- 暂无响应内容
|
|
|
|
|
- </div>
|
|
|
|
|
- <div v-else class="stream-content">
|
|
|
|
|
- <!-- 预览模式 -->
|
|
|
|
|
- <div class="formatted-content vditor-reset" v-html="formattedHtml"></div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="isStreaming" class="typing-indicator">
|
|
|
|
|
- <span class="dot"></span>
|
|
|
|
|
- <span class="dot"></span>
|
|
|
|
|
- <span class="dot"></span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="dbInfo" class="db-info">
|
|
|
|
|
- <strong>数据库信息:</strong>
|
|
|
|
|
- <div>对话ID: {{ dbInfo.ai_conversation_id }}</div>
|
|
|
|
|
- <div>消息ID: {{ dbInfo.ai_message_id }}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="errorMessage" class="error-message">
|
|
|
|
|
- <strong>错误:</strong>{{ errorMessage }}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script>
|
|
|
|
|
-import request from '../request/axios.js'
|
|
|
|
|
-import { apis } from '../request/apis.js'
|
|
|
|
|
-import Vditor from 'vditor'
|
|
|
|
|
-import 'vditor/dist/index.css'
|
|
|
|
|
-
|
|
|
|
|
-export default {
|
|
|
|
|
- name: 'LiuShiTest',
|
|
|
|
|
- data() {
|
|
|
|
|
- return {
|
|
|
|
|
- message: '',
|
|
|
|
|
- model: '',
|
|
|
|
|
- isStreaming: false,
|
|
|
|
|
- responseContent: '',
|
|
|
|
|
- responseChunks: [],
|
|
|
|
|
- errorMessage: '',
|
|
|
|
|
- eventSource: null,
|
|
|
|
|
- buffer: '',
|
|
|
|
|
- formattedHtml: '',
|
|
|
|
|
- dbInfo: null
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- watch: {
|
|
|
|
|
- responseContent: {
|
|
|
|
|
- handler(newContent) {
|
|
|
|
|
- if (newContent) {
|
|
|
|
|
- this.renderWithVditor(newContent)
|
|
|
|
|
- } else {
|
|
|
|
|
- this.formattedHtml = ''
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- immediate: true
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- methods: {
|
|
|
|
|
- // 开始流式请求
|
|
|
|
|
- async startStream() {
|
|
|
|
|
- if (!this.message.trim()) {
|
|
|
|
|
- this.errorMessage = '请输入消息内容'
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- this.isStreaming = true
|
|
|
|
|
- this.responseContent = ''
|
|
|
|
|
- this.responseChunks = []
|
|
|
|
|
- this.errorMessage = ''
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- // 使用流式+数据库集成接口
|
|
|
|
|
- const response = await fetch('http://127.0.0.1:22000/apiv1/stream/chat-with-db', {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- },
|
|
|
|
|
- body: JSON.stringify({
|
|
|
|
|
- message: this.message,
|
|
|
|
|
- user_id: 598,
|
|
|
|
|
- ai_conversation_id: 0, // 0表示新建对话
|
|
|
|
|
- business_type: 0,
|
|
|
|
|
- exam_name: '流式测试',
|
|
|
|
|
- ai_message_id: 0
|
|
|
|
|
- })
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- if (!response.ok) {
|
|
|
|
|
- throw new Error(`HTTP错误: ${response.status}`)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 使用ReadableStream处理流式响应
|
|
|
|
|
- const reader = response.body.getReader()
|
|
|
|
|
- const decoder = new TextDecoder('utf-8')
|
|
|
|
|
-
|
|
|
|
|
- while (true) {
|
|
|
|
|
- const { done, value } = await reader.read()
|
|
|
|
|
-
|
|
|
|
|
- if (done) {
|
|
|
|
|
- break
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const chunk = decoder.decode(value, { stream: true })
|
|
|
|
|
- this.processStreamChunk(chunk)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('流式请求错误:', error)
|
|
|
|
|
- this.errorMessage = `请求失败: ${error.message}`
|
|
|
|
|
- } finally {
|
|
|
|
|
- this.isStreaming = false
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- processStreamChunk(chunk) {
|
|
|
|
|
- // 调试:打印原始数据块
|
|
|
|
|
- console.log('原始数据块:', chunk)
|
|
|
|
|
-
|
|
|
|
|
- // 使用缓冲区处理跨chunk的数据
|
|
|
|
|
- if (!this.buffer) {
|
|
|
|
|
- this.buffer = ''
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- this.buffer += chunk
|
|
|
|
|
-
|
|
|
|
|
- // 处理完整的数据行(按换行符分割)
|
|
|
|
|
- const lines = this.buffer.split('\n')
|
|
|
|
|
- this.buffer = lines.pop() || '' // 保留最后一个不完整的行
|
|
|
|
|
-
|
|
|
|
|
- for (const line of lines) {
|
|
|
|
|
- if (line.trim() === '') continue
|
|
|
|
|
-
|
|
|
|
|
- console.log('收到数据行:', line)
|
|
|
|
|
-
|
|
|
|
|
- if (line.startsWith('data: ')) {
|
|
|
|
|
- const data = line.substring(6)
|
|
|
|
|
-
|
|
|
|
|
- if (data === '[DONE]') {
|
|
|
|
|
- console.log('收到结束信号 [DONE]')
|
|
|
|
|
- this.handleStreamEnd()
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- // 尝试解析JSON数据
|
|
|
|
|
- const parsed = JSON.parse(data)
|
|
|
|
|
-
|
|
|
|
|
- // 处理初始响应(包含数据库ID)
|
|
|
|
|
- if (parsed.type === 'initial') {
|
|
|
|
|
- console.log('收到初始响应:', parsed)
|
|
|
|
|
- console.log('对话ID:', parsed.ai_conversation_id)
|
|
|
|
|
- console.log('消息ID:', parsed.ai_message_id)
|
|
|
|
|
- // 保存数据库信息到界面
|
|
|
|
|
- this.dbInfo = {
|
|
|
|
|
- ai_conversation_id: parsed.ai_conversation_id,
|
|
|
|
|
- ai_message_id: parsed.ai_message_id
|
|
|
|
|
- }
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (parsed.error) {
|
|
|
|
|
- this.errorMessage = parsed.error
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 处理流式响应数据
|
|
|
|
|
- if (parsed.choices && parsed.choices.length > 0) {
|
|
|
|
|
- const choice = parsed.choices[0]
|
|
|
|
|
- console.log('解析的choice:', choice)
|
|
|
|
|
- if (choice.delta && choice.delta.content) {
|
|
|
|
|
- console.log('添加内容:', choice.delta.content)
|
|
|
|
|
- this.responseChunks.push(choice.delta.content)
|
|
|
|
|
- this.responseContent += choice.delta.content
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 检查是否完成
|
|
|
|
|
- if (choice.finish_reason) {
|
|
|
|
|
- console.log('收到完成信号:', choice.finish_reason)
|
|
|
|
|
- this.handleStreamEnd()
|
|
|
|
|
- break
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // JSON解析成功但没有choices,可能是纯数字或其他简单JSON值
|
|
|
|
|
- console.log('JSON解析成功但无choices,数据:', parsed)
|
|
|
|
|
- console.log('将作为文本内容处理:', String(parsed))
|
|
|
|
|
-
|
|
|
|
|
- // 将解析结果转换为字符串并添加
|
|
|
|
|
- const textContent = String(parsed)
|
|
|
|
|
- this.responseChunks.push(textContent)
|
|
|
|
|
- this.responseContent += textContent
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- // 如果不是JSON格式,直接作为文本内容处理
|
|
|
|
|
- console.log('收到文本内容:', data)
|
|
|
|
|
- console.log('JSON解析失败,原因:', e.message)
|
|
|
|
|
-
|
|
|
|
|
- // 处理转义的换行符,将\n转换回真正的换行符
|
|
|
|
|
- const processedData = data.replace(/\\n/g, '\n')
|
|
|
|
|
-
|
|
|
|
|
- this.responseChunks.push(processedData)
|
|
|
|
|
- this.responseContent += processedData
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- // 处理流式结束
|
|
|
|
|
- handleStreamEnd() {
|
|
|
|
|
- // 强制最终渲染,确保所有内容都被正确解析
|
|
|
|
|
- this.renderWithVditor(this.responseContent)
|
|
|
|
|
- console.log('流式响应结束,执行最终渲染')
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- renderWithVditor(content) {
|
|
|
|
|
- try {
|
|
|
|
|
- console.log('开始使用Vditor渲染,内容长度:', content.length)
|
|
|
|
|
- console.log('原始内容:', content)
|
|
|
|
|
-
|
|
|
|
|
- // 创建一个临时的DOM元素
|
|
|
|
|
- const tempDiv = document.createElement('div')
|
|
|
|
|
- tempDiv.style.display = 'none'
|
|
|
|
|
- document.body.appendChild(tempDiv)
|
|
|
|
|
-
|
|
|
|
|
- // 使用Vditor.preview方法渲染 - 简化配置
|
|
|
|
|
- Vditor.preview(tempDiv, content, {
|
|
|
|
|
- mode: 'light',
|
|
|
|
|
- markdown: {
|
|
|
|
|
- toc: false,
|
|
|
|
|
- mark: false,
|
|
|
|
|
- footnotes: false,
|
|
|
|
|
- autoSpace: false,
|
|
|
|
|
- fixTermTypo: false,
|
|
|
|
|
- chinesePunct: false,
|
|
|
|
|
- linkBase: '',
|
|
|
|
|
- linkPrefix: '',
|
|
|
|
|
- listStyle: false,
|
|
|
|
|
- paragraphBeginningSpace: false
|
|
|
|
|
- },
|
|
|
|
|
- theme: {
|
|
|
|
|
- current: 'light',
|
|
|
|
|
- path: 'https://cdn.jsdelivr.net/npm/vditor@3.10.9/dist/css/content-theme'
|
|
|
|
|
- },
|
|
|
|
|
- after: () => {
|
|
|
|
|
- // 获取Vditor渲染的结果并进行规范引用处理
|
|
|
|
|
- let html = tempDiv.innerHTML
|
|
|
|
|
-
|
|
|
|
|
- // 处理规范引用 - 将中括号内容转换为可点击的规范引用
|
|
|
|
|
- html = this.processStandardReferences(html)
|
|
|
|
|
-
|
|
|
|
|
- this.formattedHtml = html
|
|
|
|
|
- console.log('Vditor渲染完成,HTML长度:', this.formattedHtml.length)
|
|
|
|
|
- console.log('HTML预览:', this.formattedHtml.substring(0, 200) + '...')
|
|
|
|
|
-
|
|
|
|
|
- // 清理临时元素
|
|
|
|
|
- document.body.removeChild(tempDiv)
|
|
|
|
|
-
|
|
|
|
|
- // 等待DOM更新后绑定点击事件
|
|
|
|
|
- this.$nextTick(() => {
|
|
|
|
|
- this.bindStandardReferenceEvents()
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('Vditor渲染错误:', error)
|
|
|
|
|
- // 渲染失败时使用简单HTML转换
|
|
|
|
|
- this.formattedHtml = content.replace(/\n/g, '<br>')
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- // 处理规范引用 - 将中括号内容转换为可点击的规范引用
|
|
|
|
|
- processStandardReferences(html) {
|
|
|
|
|
- if (!html) return html
|
|
|
|
|
-
|
|
|
|
|
- console.log('开始处理规范引用,HTML长度:', html.length)
|
|
|
|
|
-
|
|
|
|
|
- // 处理中括号为可点击的标准引用/普通引用
|
|
|
|
|
- const processedHtml = html.replace(/\[([^\[\]]+)\]/g, (match, content) => {
|
|
|
|
|
- console.log('发现规范引用:', content)
|
|
|
|
|
-
|
|
|
|
|
- // 检查是否已经是处理过的规范引用
|
|
|
|
|
- if (/^<span\s+class="standard-reference"/i.test(content)) {
|
|
|
|
|
- return match
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 检查是否是标准格式:书名号+内容+括号+编号
|
|
|
|
|
- const standardMatch = content.match(/^([《「『【]?[\s\S]*?[》」』】]?)[\s]*\(([^)]+)\)$/)
|
|
|
|
|
- if (standardMatch) {
|
|
|
|
|
- const standardName = standardMatch[1]
|
|
|
|
|
- const standardNumber = standardMatch[2]
|
|
|
|
|
- console.log('标准格式规范:', { standardName, standardNumber })
|
|
|
|
|
- return `<span class="standard-reference" data-standard="${content}" data-name="${standardName}" data-number="${standardNumber}" title="点击查看标准详情" style="background-color: #EAEAEE; color: #616161; font-size: 0.75rem; padding: 3px 8px; border-radius: 6px; cursor: pointer; display: inline-block; margin: 4px 2px; border: 1px solid #EAEAEE; font-weight: 500; transition: all 0.2s ease; line-height: 1.4;">${content}</span>`
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 普通引用格式
|
|
|
|
|
- console.log('普通格式规范:', content)
|
|
|
|
|
- return `<span class="standard-reference" data-reference="${content}" title="点击查看详情" style="background-color: #EAEAEE; color: #616161; font-size: 0.75rem; padding: 3px 8px; border-radius: 6px; cursor: pointer; display: inline-block; margin: 4px 2px; border: 1px solid #EAEAEE; font-weight: 500; transition: all 0.2s ease; line-height: 1.4;">${content}</span>`
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- console.log('规范引用处理完成')
|
|
|
|
|
- return processedHtml
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- // 绑定规范引用点击事件
|
|
|
|
|
- bindStandardReferenceEvents() {
|
|
|
|
|
- const references = document.querySelectorAll('.standard-reference')
|
|
|
|
|
- console.log('找到规范引用元素数量:', references.length)
|
|
|
|
|
-
|
|
|
|
|
- references.forEach((ref, index) => {
|
|
|
|
|
- // 移除之前的事件监听器
|
|
|
|
|
- ref.removeEventListener('click', this.handleStandardReferenceClick)
|
|
|
|
|
-
|
|
|
|
|
- // 添加新的点击事件监听器
|
|
|
|
|
- ref.addEventListener('click', this.handleStandardReferenceClick)
|
|
|
|
|
-
|
|
|
|
|
- console.log(`绑定规范引用 ${index + 1}:`, ref.textContent)
|
|
|
|
|
- })
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- // 处理规范引用点击事件
|
|
|
|
|
- async handleStandardReferenceClick(event) {
|
|
|
|
|
- event.preventDefault()
|
|
|
|
|
- event.stopPropagation()
|
|
|
|
|
-
|
|
|
|
|
- const element = event.currentTarget
|
|
|
|
|
- const content = element.textContent
|
|
|
|
|
- const standardName = element.getAttribute('data-name')
|
|
|
|
|
- const standardNumber = element.getAttribute('data-number')
|
|
|
|
|
- const standardData = element.getAttribute('data-standard')
|
|
|
|
|
- const referenceData = element.getAttribute('data-reference')
|
|
|
|
|
-
|
|
|
|
|
- console.log('点击规范引用:', {
|
|
|
|
|
- content,
|
|
|
|
|
- standardName,
|
|
|
|
|
- standardNumber,
|
|
|
|
|
- standardData,
|
|
|
|
|
- referenceData
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- // 确定要查询的文件名
|
|
|
|
|
- let fileName = ''
|
|
|
|
|
- if (standardData) {
|
|
|
|
|
- fileName = standardData
|
|
|
|
|
- } else if (referenceData) {
|
|
|
|
|
- fileName = referenceData
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (fileName) {
|
|
|
|
|
- try {
|
|
|
|
|
- console.log('正在获取文件链接,文件名:', fileName)
|
|
|
|
|
-
|
|
|
|
|
- // 调用后端接口获取文件链接
|
|
|
|
|
- const response = await apis.getFileLink({ fileName })
|
|
|
|
|
- console.log('获取文件链接响应:', response)
|
|
|
|
|
-
|
|
|
|
|
- if (response.statusCode === 200 && response.data) {
|
|
|
|
|
- const fileLink = response.data
|
|
|
|
|
- console.log('获取到文件链接:', fileLink)
|
|
|
|
|
-
|
|
|
|
|
- // 如果有文件链接,打开预览
|
|
|
|
|
- if (fileLink) {
|
|
|
|
|
- // 在新窗口中打开文件链接
|
|
|
|
|
- window.open(fileLink, '_blank')
|
|
|
|
|
- console.log('文件已在新窗口中打开')
|
|
|
|
|
- } else {
|
|
|
|
|
- console.log('暂无文件')
|
|
|
|
|
- alert('暂无文件')
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- console.log('暂无文件')
|
|
|
|
|
- alert('暂无文件')
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('获取文件链接失败:', error)
|
|
|
|
|
- alert('获取文件失败,请稍后重试')
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- // 停止流式请求
|
|
|
|
|
- stopStream() {
|
|
|
|
|
- this.isStreaming = false
|
|
|
|
|
- if (this.eventSource) {
|
|
|
|
|
- this.eventSource.close()
|
|
|
|
|
- this.eventSource = null
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- // 清空响应
|
|
|
|
|
- clearResponse() {
|
|
|
|
|
- this.responseContent = ''
|
|
|
|
|
- this.responseChunks = []
|
|
|
|
|
- this.formattedHtml = ''
|
|
|
|
|
- this.errorMessage = ''
|
|
|
|
|
- this.dbInfo = null
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- // 处理键盘事件
|
|
|
|
|
- handleKeyDown(event) {
|
|
|
|
|
- // 空格键发送
|
|
|
|
|
- if (event.code === 'Space' && !event.shiftKey && !event.ctrlKey && !event.altKey) {
|
|
|
|
|
- // 阻止默认的空格输入行为
|
|
|
|
|
- event.preventDefault()
|
|
|
|
|
-
|
|
|
|
|
- // 检查是否可以发送
|
|
|
|
|
- if (!this.isStreaming && this.message.trim()) {
|
|
|
|
|
- this.startStream()
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- async mounted() {
|
|
|
|
|
- // 组件挂载后初始化
|
|
|
|
|
- await this.$nextTick()
|
|
|
|
|
- },
|
|
|
|
|
-
|
|
|
|
|
- beforeUnmount() {
|
|
|
|
|
- // 清理资源
|
|
|
|
|
- if (this.eventSource) {
|
|
|
|
|
- this.eventSource.close()
|
|
|
|
|
- this.eventSource = null
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style scoped>
|
|
|
|
|
-.stream-test-container {
|
|
|
|
|
- max-width: 1200px;
|
|
|
|
|
- margin: 0 auto;
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
- font-family: Arial, sans-serif;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.test-form {
|
|
|
|
|
- background: #f8f9fa;
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.input-group {
|
|
|
|
|
- margin-bottom: 15px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.input-group label {
|
|
|
|
|
- display: block;
|
|
|
|
|
- margin-bottom: 5px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #333;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.input-group textarea,
|
|
|
|
|
-.input-group input {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- padding: 10px;
|
|
|
|
|
- border: 1px solid #ddd;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.input-group textarea {
|
|
|
|
|
- resize: vertical;
|
|
|
|
|
- min-height: 80px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.button-group {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 10px;
|
|
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.button-group button {
|
|
|
|
|
- padding: 10px 20px;
|
|
|
|
|
- border: none;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- transition: background-color 0.3s;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.start-btn {
|
|
|
|
|
- background: #007bff;
|
|
|
|
|
- color: white;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.start-btn:hover:not(:disabled) {
|
|
|
|
|
- background: #0056b3;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.start-btn:disabled {
|
|
|
|
|
- background: #6c757d;
|
|
|
|
|
- cursor: not-allowed;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stop-btn {
|
|
|
|
|
- background: #dc3545;
|
|
|
|
|
- color: white;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stop-btn:hover:not(:disabled) {
|
|
|
|
|
- background: #c82333;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stop-btn:disabled {
|
|
|
|
|
- background: #6c757d;
|
|
|
|
|
- cursor: not-allowed;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.clear-btn {
|
|
|
|
|
- background: #6c757d;
|
|
|
|
|
- color: white;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.clear-btn:hover {
|
|
|
|
|
- background: #545b62;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.response-section {
|
|
|
|
|
- background: white;
|
|
|
|
|
- border: 1px solid #ddd;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.response-header {
|
|
|
|
|
- background: #f8f9fa;
|
|
|
|
|
- padding: 15px 20px;
|
|
|
|
|
- border-bottom: 1px solid #ddd;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.response-header h3 {
|
|
|
|
|
- margin: 0;
|
|
|
|
|
- color: #333;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.status-indicator {
|
|
|
|
|
- padding: 5px 10px;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- background: #6c757d;
|
|
|
|
|
- color: white;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.status-indicator.active {
|
|
|
|
|
- background: #28a745;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.response-content {
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
- min-height: 200px;
|
|
|
|
|
- max-height: 600px;
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.empty-state {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- color: #6c757d;
|
|
|
|
|
- font-style: italic;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stream-content {
|
|
|
|
|
- line-height: 1.6;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.formatted-content {
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.typing-indicator {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
- color: #6c757d;
|
|
|
|
|
- font-style: italic;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.dot {
|
|
|
|
|
- width: 6px;
|
|
|
|
|
- height: 6px;
|
|
|
|
|
- background: #6c757d;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- animation: typing 1.4s infinite ease-in-out;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.dot:nth-child(2) {
|
|
|
|
|
- animation-delay: 0.2s;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.dot:nth-child(3) {
|
|
|
|
|
- animation-delay: 0.4s;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-@keyframes typing {
|
|
|
|
|
- 0%, 60%, 100% {
|
|
|
|
|
- transform: translateY(0);
|
|
|
|
|
- opacity: 0.5;
|
|
|
|
|
- }
|
|
|
|
|
- 30% {
|
|
|
|
|
- transform: translateY(-8px);
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.db-info {
|
|
|
|
|
- background: #d1ecf1;
|
|
|
|
|
- color: #0c5460;
|
|
|
|
|
- padding: 10px;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- margin-top: 10px;
|
|
|
|
|
- border: 1px solid #bee5eb;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.db-info div {
|
|
|
|
|
- margin: 2px 0;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.error-message {
|
|
|
|
|
- background: #f8d7da;
|
|
|
|
|
- color: #721c24;
|
|
|
|
|
- padding: 10px;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- margin-top: 10px;
|
|
|
|
|
- border: 1px solid #f5c6cb;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 规范引用样式 */
|
|
|
|
|
-:deep(.standard-reference) {
|
|
|
|
|
- background-color: #EAEAEE !important;
|
|
|
|
|
- color: #616161 !important;
|
|
|
|
|
- font-size: 0.75rem !important;
|
|
|
|
|
- padding: 3px 8px !important;
|
|
|
|
|
- border-radius: 6px !important;
|
|
|
|
|
- cursor: pointer !important;
|
|
|
|
|
- display: inline-block !important;
|
|
|
|
|
- margin: 4px 2px !important;
|
|
|
|
|
- border: 1px solid #EAEAEE !important;
|
|
|
|
|
- font-weight: 500 !important;
|
|
|
|
|
- transition: all 0.2s ease !important;
|
|
|
|
|
- line-height: 1.4 !important;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.standard-reference:hover) {
|
|
|
|
|
- background-color: #d1d5db !important;
|
|
|
|
|
- border-color: #d1d5db !important;
|
|
|
|
|
-}
|
|
|
|
|
-</style>
|
|
|