Selaa lähdekoodia

bugfix:修复一系列BUG

Logistics System Developer 1 kuukausi sitten
vanhempi
sitoutus
d4a9425613

+ 143 - 0
Native2H5.md

@@ -0,0 +1,143 @@
+JSBridge实现Native和H5方法互调的原理是什么?
+浏览259发布于2025-05-28 13:51广东
+JSBridge 是实现 Native(iOS/Android)与 H5 之间方法互调的核心技术,其原理基于双向通信机制。下面从通信原理、实现方式和关键细节三个方面进行详细解析:
+
+一、核心通信原理
+JSBridge 的核心是构建 Native 与 JavaScript 的双向数据通道,主要通过以下两种方式实现:
+
+1. JavaScript 调用 Native
+原理:利用 WebView 提供的接口,让 JavaScript 代码能够触发 Native 方法。
+实现方式:
+URL Scheme 拦截:JavaScript 通过修改 URL(如 jsbridge://method?param=123),Native 监听 WebView 的 URL 变化并解析参数。
+注入 API:Native 向 WebView 的 JavaScript 环境注入对象或方法(如 window.bridge.callNative),JavaScript 直接调用。
+2. Native 调用 JavaScript
+原理:Native 代码通过 WebView 提供的接口执行 JavaScript 代码。
+实现方式:
+WebView 内置方法:
+iOS:WKWebView.evaluateJavaScript(_:completionHandler:)
+Android:webView.loadUrl("javascript:yourFunction()")
+消息传递:Native 发送消息到 H5,H5 通过监听事件接收。
+二、具体实现方式
+1. URL Scheme 拦截(通用方案)
+JavaScript 端:
+javascript
+
+// 通过修改 URL 触发 Native 方法
+function callNative(method, params) {
+  const url = `jsbridge://${method}?${JSON.stringify(params)}`;
+  window.location.href = url;
+}
+Native 端:
+iOS(Swift):
+swift
+
+func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
+    if let url = navigationAction.request.url, url.scheme == "jsbridge" {
+        // 解析 method 和 params
+        handleJSBridge(url)
+        decisionHandler(.cancel) // 拦截 URL
+        return
+    }
+    decisionHandler(.allow)
+}
+Android(Java):
+java
+
+webView.setWebViewClient(new WebViewClient() {
+    @Override
+    public boolean shouldOverrideUrlLoading(WebView view, String url) {
+        if (url.startsWith("jsbridge://")) {
+            // 解析 method 和 params
+            handleJSBridge(url);
+            return true; // 拦截 URL
+        }
+        return super.shouldOverrideUrlLoading(view, url);
+    }
+});
+2. 注入 API(高性能方案)
+iOS(WKWebView):
+swift
+
+// 注入 bridge 对象到 JavaScript 环境
+let config = WKWebViewConfiguration()
+let controller = WKUserContentController()
+controller.add(self, name: "bridge") // 实现 WKScriptMessageHandler 协议
+config.userContentController = controller
+Android:
+java
+
+// 注入 Java 对象到 JavaScript
+class JsInterface {
+    @JavascriptInterface
+    public void callNative(String method, String params) {
+        // 处理来自 JavaScript 的调用
+    }
+}
+webView.addJavascriptInterface(new JsInterface(), "bridge");
+JavaScript 调用:
+javascript
+
+// 直接调用注入的 API
+window.bridge.callNative("login", { username: "test" });
+3. 消息队列机制(优化方案)
+为解决同步调用和复杂参数传递问题,通常引入消息队列:
+
+JavaScript 端:
+javascript
+
+const messageQueue = [];
+function callNative(method, params) {
+  messageQueue.push({ method, params });
+  // 通知 Native 有新消息
+  window.webkit.messageHandlers.bridge.postMessage("newMessage");
+}
+Native 端:
+swift
+
+func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+    if message.name == "bridge" {
+        // 从消息队列获取消息并处理
+        processMessageQueue()
+    }
+}
+三、关键技术细节
+协议设计:
+
+定义统一的消息格式(如 JSON-RPC):
+json
+
+{
+  "id": 1,
+  "method": "login",
+  "params": { "username": "test" },
+  "callbackId": "cb_123"
+}
+异步回调处理:
+
+JavaScript 注册回调函数并生成唯一 ID,传递给 Native。
+Native 执行完成后,通过 ID 调用对应的 JavaScript 回调:
+javascript
+
+const callbacks = {};
+function callNativeWithCallback(method, params, callback) {
+  const callbackId = `cb_${Date.now()}`;
+  callbacks[callbackId] = callback;
+  window.bridge.callNative(method, { ...params, callbackId });
+}
+安全机制:
+
+白名单校验:限制允许调用的方法和域名。
+参数过滤:防止 XSS 攻击和恶意代码注入。
+签名验证:对敏感操作进行签名校验。
+四、常见应用场景
+混合开发:H5 页面调用 Native 摄像头、定位等功能。
+数据同步:Native 实时推送数据到 H5 页面。
+支付流程:H5 调用 Native 支付 SDK 完成支付。
+性能优化:复杂计算交给 Native 处理,避免阻塞 H5。
+五、主流 JSBridge 库
+iOS:WKWebViewJavascriptBridge、WebViewJavascriptBridge
+Android:DSBridge-Android、JavascriptInterface
+跨平台:React Native、Flutter(通过 Platform Channel)
+六、总结
+JSBridge 的核心在于利用 WebView 提供的接口,构建双向通信通道,并通过消息协议和队列机制实现复杂交互。其实现方式各有优劣,实际开发中需根据性能要求、兼容性和安全需求选择合适的方案。

+ 7 - 7
shudao-chat-go/conf/app.conf

@@ -2,7 +2,7 @@ appname = shudao-chat-go
 httpport = 22000
 runmode = dev
 
-# 我们musql配置
+# mysql配置
 #mysqluser = "shudao"
 #mysqlpass = "root"
 #mysqlpass = "YDdYntHtC7h5bniB"
@@ -13,7 +13,7 @@ runmode = dev
 # shudao-chat-go配置
 mysqluser = "root"
 mysqlpass = "88888888"
-mysqlurls = "172.16.29.101"
+mysqlurls = "172.16.35.57"
 mysqlhttpport = "21000"
 mysqldb = "shudao"
 
@@ -33,10 +33,10 @@ intent_model = "Qwen2.5-1.5B-Instruct"
 # YOLO API配置
 yolo_base_url = "http://172.16.35.50:18080"
 
-# Chroma数据库配置
-chroma_host = "172.16.29.101"
-chroma_port = "23000"
-chroma_collection_name = "my_rag_collection"
+# # Chroma数据库配置
+# chroma_host = "172.16.35.57"
+# chroma_port = "23000"
+# chroma_collection_name = "my_rag_collection"
 
 # 搜索API配置
 search_api_url = "http://localhost:24000/api/search"
@@ -46,7 +46,7 @@ heartbeat_api_url = "http://localhost:24000/api/health"
 # 基础URL配置 - 手动切换
 # 本地环境: https://172.16.29.101:22000
 # 生产环境: https://aqai.shudaodsj.com:22000
-base_url = "https://aqai.shudaodsj.com:22000"
+base_url = "https://aqai.shudaodsj.com:22001"
 
 # Token验证API配置
 # 生产环境:使用外部认证服务

+ 1 - 1
shudao-chat-go/utils/config.go

@@ -6,7 +6,7 @@ import (
 
 // GetBaseURL 获取基础URL
 func GetBaseURL() string {
-	return web.AppConfig.DefaultString("base_url", "https://172.16.29.101:22001")
+	return web.AppConfig.DefaultString("base_url")
 }
 
 // GetProxyURL 生成OSS代理URL(加密版本)

+ 1 - 4
src/components/FilePreviewDrawer.vue

@@ -71,7 +71,6 @@
 <script setup>
 import { ref, watch, onBeforeUnmount } from 'vue'
 import { Document, Loading, CircleClose } from '@element-plus/icons-vue'
-import { buildDocumentPreviewUrl } from '@/utils/apiConfig'
 
 const props = defineProps({
   modelValue: {
@@ -150,10 +149,8 @@ const loadFile = async () => {
   clearLoadingTimer()
   
   try {
-    // 开发环境:直接使用原始URL
-    // 生产环境:通过 OSS 解析服务代理
     const originalUrl = props.filePath
-    const convertedUrl = buildDocumentPreviewUrl(originalUrl)
+    const convertedUrl = originalUrl
     
     console.log('📄 [文档预览] 原始URL:', originalUrl)
     console.log('📄 [文档预览] 转换后URL:', convertedUrl)

+ 1 - 2
src/components/FileReportCard.vue

@@ -157,7 +157,6 @@ import { computed, ref } from 'vue'
 import { ElMessage, ElDialog } from 'element-plus'
 import { Document, View, CopyDocument, Reading, List, Link, WarningFilled, ArrowRight } from '@element-plus/icons-vue'
 import StreamMarkdown from './StreamMarkdown.vue'
-import { buildDocumentPreviewUrl } from '@/utils/apiConfig'
 
 const props = defineProps({
   report: {
@@ -233,7 +232,7 @@ const openSourceUrl = () => {
   if (!sourceUrl.value) return
   
   // 根据环境转换文档预览URL
-  sourcePreviewUrl.value = buildDocumentPreviewUrl(sourceUrl.value)
+  sourcePreviewUrl.value = sourceUrl.value
   sourcePreviewTitle.value = props.report.report?.display_name || props.report.source_file || '文件预览'
   showSourcePreview.value = true
   

+ 1 - 112
src/utils/apiConfig.js

@@ -3,8 +3,6 @@
  * 根据环境自动处理 API 路径前缀
  */
 
-import CryptoJS from 'crypto-js'
-
 /**
  * 获取 API 路径前缀
  * 开发环境:/api/v1
@@ -40,113 +38,4 @@ export function getSSEApiPrefix() {
  */
 export function getReportApiPrefix() {
   return getApiPrefix()
-}
-
-/**
- * 解密文件URL
- * @param {string} encryptedUrl - 加密的Base64 URL字符串
- * @returns {string} 解密后的原始URL
- */
-export function decryptFileUrl(encryptedUrl) {
-  if (!encryptedUrl) return ''
-
-  try {
-    // 配置密钥和IV (必须与后端一致)
-    const KEY = 'shudao-aes-key-32bytes-changeeee'  // 32字节
-    const IV = 'shudao-aes-iv16!'                    // 16字节
-
-    // 1. Base64 URL-Safe解码 (替换URL安全字符)
-    const base64 = encryptedUrl
-      .replace(/-/g, '+')   // 将 - 替换为 +
-      .replace(/_/g, '/')   // 将 _ 替换为 /
-
-    // 2. 转换为CryptoJS格式
-    const key = CryptoJS.enc.Utf8.parse(KEY)
-    const iv = CryptoJS.enc.Utf8.parse(IV)
-    const encrypted = CryptoJS.enc.Base64.parse(base64)
-
-    // 3. AES-CBC解密
-    const decrypted = CryptoJS.AES.decrypt(
-      { ciphertext: encrypted },
-      key,
-      {
-        iv: iv,
-        mode: CryptoJS.mode.CBC,
-        padding: CryptoJS.pad.Pkcs7
-      }
-    )
-
-    // 4. 转换为UTF-8字符串
-    const originalUrl = decrypted.toString(CryptoJS.enc.Utf8)
-
-    if (!originalUrl) {
-      throw new Error('解密失败:结果为空')
-    }
-
-    return originalUrl
-
-  } catch (error) {
-    console.error('[URL解密] 失败:', error)
-    throw new Error(`URL解密失败: ${error.message}`)
-  }
-}
-
-/**
- * 转换文档预览URL
- * 根据运行环境自动转换文档预览地址
- *
- * 使用场景:
- * - PDF/文档预览
- * - 图片预览
- * - 其他需要通过OSS代理访问的资源
- *
- * 环境说明:
- * 1. 开发环境(import.meta.env.DEV = true):
- *    - 直接返回原始URL
- *    - 示例:http://172.16.17.52:8060/gdsc-ai-aqzs/documents/xxx.pdf
- *
- * 2. 生产环境(import.meta.env.PROD = true):
- *    - 通过OSS解析服务代理访问
- *    - 转换格式:https://aqai.shudaodsj.com:22000/apiv1/oss/parse/?url={原始URL编码}
- *    - 示例输入:http://172.16.17.52:8060/gdsc-ai-aqzs/documents/xxx.pdf
- *    - 示例输出:https://aqai.shudaodsj.com:22000/apiv1/oss/parse/?url=http%3A%2F%2F172.16.17.52%3A8060%2Fgdsc-ai-aqzs%2Fdocuments%2Fxxx.pdf
- *
- * @param {string} originalUrl - 原始文档URL(可能是加密的)
- * @returns {string} 转换后的预览URL
- *
- * @example
- * // 开发环境
- * buildDocumentPreviewUrl('http://172.16.17.52:8060/gdsc-ai-aqzs/documents/xxx.pdf')
- * // => 'http://172.16.17.52:8060/gdsc-ai-aqzs/documents/xxx.pdf'
- *
- * @example
- * // 生产环境
- * buildDocumentPreviewUrl('http://172.16.17.52:8060/gdsc-ai-aqzs/documents/xxx.pdf')
- * // => 'https://aqai.shudaodsj.com:22000/apiv1/oss/parse/?url=http%3A%2F%2F172.16.17.52%3A8060%2Fgdsc-ai-aqzs%2Fdocuments%2Fxxx.pdf'
- */
-export function buildDocumentPreviewUrl(originalUrl) {
-  if (!originalUrl) return ''
-
-  // 尝试解密URL(如果是加密的)
-  let decryptedUrl = originalUrl
-  try {
-    // 判断是否为加密URL(Base64 URL-Safe格式通常包含 - 或 _ 字符)
-    if (originalUrl.includes('-') || originalUrl.includes('_')) {
-      decryptedUrl = decryptFileUrl(originalUrl)
-    }
-  } catch (error) {
-    console.warn('[URL解密] 跳过解密,使用原始URL:', error.message)
-    decryptedUrl = originalUrl
-  }
-
-  // 开发环境:直接返回解密后的URL,可以直接访问内网地址
-  if (import.meta.env.DEV) {
-    return decryptedUrl
-  }
-
-  // 生产环境:通过 OSS 解析服务代理访问
-  // 这样可以解决生产环境无法直接访问内网地址的问题
-  const ossParseUrl = 'https://aqai.shudaodsj.com:22000/apiv1/oss/parse/'
-  return `${ossParseUrl}?url=${encodeURIComponent(decryptedUrl)}`
-}
-
+}

+ 93 - 41
src/utils/sse.js

@@ -1,11 +1,11 @@
 /**
- * SSE工具函数(支持自动重连)
+ * SSE工具函数(支持自动重连,仅支持 POST 方法
  */
 
 import { getToken, getTokenType } from './auth.js'
 
 /**
- * SSE连接管理器类(支持自动重连)
+ * SSE连接管理器类(支持自动重连,仅支持 POST 方法
  */
 class SSEConnectionManager {
   constructor(url, handlers = {}, options = {}) {
@@ -16,10 +16,12 @@ class SSEConnectionManager {
       retryDelay: options.retryDelay || 1000, // 初始重试延迟(毫秒)
       maxRetryDelay: options.maxRetryDelay || 30000, // 最大重试延迟(毫秒)
       enableAutoReconnect: options.enableAutoReconnect !== false, // 是否启用自动重连,默认true
+      body: options.body || {}, // POST 请求体
+      headers: options.headers || {}, // 自定义请求头
       ...options
     }
 
-    this.eventSource = null
+    this.abortController = null // 用于取消 fetch 请求
     this.retryCount = 0
     this.retryTimer = null
     this.isManualClose = false // 是否手动关闭
@@ -31,41 +33,96 @@ class SSEConnectionManager {
   }
 
   /**
-   * 建立SSE连接
+   * 建立SSE连接(使用 POST 方法)
    */
-  connect() {
+  async connect() {
     // 如果手动关闭或已完成,不再重连
     if (this.isManualClose || this.isCompleted) {
       console.log('🚫 SSE连接已手动关闭或已完成,不再重连')
       return
     }
 
-    // 添加Token到URL
-    let url = this.originalUrl
     const token = getToken()
     const tokenType = getTokenType()
 
-    if (token && tokenType) {
-      const urlObj = new URL(url, window.location.origin)
-      urlObj.searchParams.set('token', token)
-      url = urlObj.toString()
-      console.log('🔐 SSE 连接已添加认证 Token(通过 URL 参数)')
-    } else {
+    if (!token || !tokenType) {
       console.warn('⚠️ SSE 连接未找到 Token,可能会导致认证失败')
     }
 
     try {
-      this.eventSource = new EventSource(url)
-      console.log(`🔌 SSE连接已建立 (重试次数: ${this.retryCount}/${this.options.maxRetries})`)
+      const token = getToken()
+      const tokenType = getTokenType()
 
-      // 绑定事件处理器
-      this.eventSource.onmessage = this.handleMessage.bind(this)
-      this.eventSource.onerror = this.handleError.bind(this)
-      this.eventSource.onopen = this.handleOpen.bind(this)
+      // 创建 AbortController 用于取消请求
+      this.abortController = new AbortController()
 
-      // 启动心跳检测(可选)
-      this.startHeartbeat()
+      // 构建请求头
+      const headers = {
+        'Content-Type': 'application/json',
+        ...this.options.headers
+      }
+
+      // 添加认证 Token
+      if (token && tokenType) {
+        headers['Authorization'] = `${tokenType} ${token}`
+        console.log('🔐 SSE 连接已添加认证 Token(通过 Authorization 头)')
+      }
+
+      console.log(`🔌 SSE连接已建立 [POST] (重试次数: ${this.retryCount}/${this.options.maxRetries})`)
+
+      // 发起 fetch 请求
+      const response = await fetch(this.originalUrl, {
+        method: 'POST',
+        headers,
+        body: JSON.stringify(this.options.body),
+        signal: this.abortController.signal
+      })
+
+      if (!response.ok) {
+        throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
+      }
+
+      // 调用连接打开回调
+      this.handleOpen()
+
+      // 读取流式响应
+      const reader = response.body.getReader()
+      const decoder = new TextDecoder()
+      let buffer = ''
+
+      try {
+        while (true) {
+          const { done, value } = await reader.read()
+
+          if (done) {
+            console.log('✅ SSE流读取完成')
+            break
+          }
+
+          // 解码数据块
+          buffer += decoder.decode(value, { stream: true })
+
+          // 按行分割数据
+          const lines = buffer.split('\n')
+          buffer = lines.pop() || '' // 保留最后一个不完整的行
 
+          for (const line of lines) {
+            if (line.startsWith('data: ')) {
+              const data = line.slice(6).trim()
+              if (data) {
+                this.handleMessage({ data })
+              }
+            }
+          }
+        }
+      } catch (error) {
+        if (error.name === 'AbortError') {
+          console.log('🚫 SSE连接已被取消')
+        } else {
+          console.error('❌ 读取SSE流失败:', error)
+          this.handleError(error)
+        }
+      }
     } catch (error) {
       console.error('❌ 创建SSE连接失败:', error)
       this.scheduleReconnect()
@@ -126,27 +183,21 @@ class SSEConnectionManager {
   handleError(error) {
     console.error('❌ SSE连接错误:', error)
 
-    if (this.eventSource) {
-      console.error('EventSource readyState:', this.eventSource.readyState)
-      console.error('EventSource url:', this.eventSource.url)
-    }
-
     // 如果是手动关闭或已完成,不处理错误
     if (this.isManualClose || this.isCompleted) {
       return
     }
 
     // 创建详细的错误信息
-    const detailedError = new Error(
-      `SSE连接失败 (状态: ${this.eventSource?.readyState === 0 ? '连接中' : this.eventSource?.readyState === 1 ? '已连接' : '已关闭'})`
-    )
+    const detailedError = error instanceof Error ? error : new Error('SSE连接失败')
 
     // 调用错误回调
     this.handlers.onError && this.handlers.onError(detailedError)
 
-    // 关闭当前连接
-    if (this.eventSource) {
-      this.eventSource.close()
+    // 取消当前请求
+    if (this.abortController) {
+      this.abortController.abort()
+      this.abortController = null
     }
 
     // 尝试重连
@@ -247,9 +298,10 @@ class SSEConnectionManager {
       this.heartbeatTimer = null
     }
 
-    // 关闭EventSource
-    if (this.eventSource && this.eventSource.readyState !== EventSource.CLOSED) {
-      this.eventSource.close()
+    // 取消 fetch 请求
+    if (this.abortController) {
+      this.abortController.abort()
+      this.abortController = null
     }
   }
 
@@ -258,16 +310,16 @@ class SSEConnectionManager {
    */
   getState() {
     return {
-      readyState: this.eventSource?.readyState,
       retryCount: this.retryCount,
       isManualClose: this.isManualClose,
-      isCompleted: this.isCompleted
+      isCompleted: this.isCompleted,
+      isConnected: this.abortController !== null
     }
   }
 }
 
 /**
- * 创建SSE连接(带自动重连功能)
+ * 创建SSE连接(带自动重连功能,仅支持 POST 方法
  * @param {string} url - SSE URL
  * @param {Object} handlers - 事件处理器
  * @param {Function} handlers.onMessage - 接收消息时的回调
@@ -278,6 +330,8 @@ class SSEConnectionManager {
  * @param {Function} handlers.onReconnecting - 重连中回调
  * @param {Function} handlers.onMaxRetriesReached - 达到最大重试次数回调
  * @param {Object} options - 配置选项
+ * @param {Object} options.body - POST 请求体(必需)
+ * @param {Object} options.headers - 自定义请求头,可选
  * @param {number} options.maxRetries - 最大重试次数,默认5
  * @param {number} options.retryDelay - 初始重试延迟(毫秒),默认1000
  * @param {number} options.maxRetryDelay - 最大重试延迟(毫秒),默认30000
@@ -291,13 +345,11 @@ export const createSSEConnection = (url, handlers = {}, options = {}) => {
 
 /**
  * 关闭SSE连接
- * @param {SSEConnectionManager|EventSource} connection - SSE连接管理器或EventSource实例
+ * @param {SSEConnectionManager} connection - SSE连接管理器实例
  */
 export const closeSSEConnection = (connection) => {
   if (connection instanceof SSEConnectionManager) {
     connection.close()
-  } else if (connection && connection.readyState !== EventSource.CLOSED) {
-    connection.close()
   }
 }
 

+ 15 - 11
src/views/AIWriting.vue

@@ -70,12 +70,16 @@
             <p class="subtitle">智能生成办公文档,提升办公效能,高效创作</p>
 
             <div class="input-area">
-              <div class="template-input-container" 
-                   contenteditable="true" 
-                   @input="handleTemplateInput" 
+              <div class="template-input-container"
+                   contenteditable="true"
+                   @input="handleTemplateInput"
                    @copy="handleCopy"
                    placeholder="请在这里输入您的写作要求...">
-                请帮我生成一份正式的总结报告,要求格式规范、语言严谨。具体内容包括<span class="editable-highlight" contenteditable="true">总结主题:</span>、<span class="editable-highlight" contenteditable="true">总结时间:</span>、<span class="editable-highlight" contenteditable="true">主要业绩和成果:</span>、<span class="editable-highlight" contenteditable="true">存在的问题和不足:</span>、<span class="editable-highlight" contenteditable="true">下一阶段工作计划:</span>的内容。请按照标准工作总结格式生成全文,包含"工作总结、问题不足、未来计划"三部分的完整报告。
+                总结主题:<span class="editable-highlight" contenteditable="true"></span>
+总结时间:<span class="editable-highlight" contenteditable="true"></span>
+主要内容:<span class="editable-highlight" contenteditable="true"></span>
+
+请帮我生成一份正式的总结报告,要求格式规范、语言严谨,具体参考以上内容,按照标准工作总结格式生成全文,包含"工作总结、问题不足、未来计划"三部分的完整报告。
               </div>
 
               <div class="input-actions">
@@ -2997,22 +3001,22 @@ const useTemplate = (templateName) => {
   
   switch (templateName) {
     case "通知模板":
-      content = "请帮我生成一份正式的通知,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">发文单位:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">通知主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">发文背景:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">通知目的:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">具体事项:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">发文日期:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">收文单位:</span>等内容。请按照标准公文格式生成完整通知,包括文号、标题、正文、落款等所有要素。";
+      content = "通知主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n通知对象:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n具体事项:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的通知,要求格式规范、语言严谨,具体参考以上内容,按照标准公文格式生成完整通知,包括文号、标题、正文、落款等所有要素。";
       break;
     case "公告模板":
-    content = "请帮我生成一份正式的公告,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">发文单位:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告编号:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">发布背景:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告核心条款:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">发文日期:</span>等内容。请按照标准公告格式生成全文,包括标题、正文、落款等所有要素。";
+    content = "公告主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n发文单位:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n核心内容:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的公告,要求格式规范、语言严谨,具体参考以上内容,按照标准公告格式生成全文,包括标题、正文、落款等所有要素。";
       break;
     case "会议纪要模版":
-      content = "请帮我生成一份正式的会议纪要,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">会议名称:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">会议时间:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">主持人:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">参会人员:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">主要议题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">会议议定事项:</span>的内容。请按照标准会议纪要格式生成全文,包含标题、导语、议定事项和落款。";
+      content = "会议主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n会议时间:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n主要议题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的会议纪要,要求格式规范、语言严谨,具体参考以上内容,按照标准会议纪要格式生成全文,包含标题、导语、议定事项和落款。";
       break;
     case "决定模版":
-      content = "请帮我生成一份正式的决定,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">发文单位:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">标题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">文号:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决定事项:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决定背景:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决定依据:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">具体内容:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">生效时间:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">主送机关:</span>的内容。请按照标准决定公文格式生成完成文件。";
+      content = "决定主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n决定依据:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n决定内容:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的决定,要求格式规范、语言严谨,具体参考以上内容,按照标准决定公文格式生成完整文件。";
       break;
     case "工作汇报模板":
-      content = "请帮我生成一份正式的总结报告,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">总结主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">总结时间:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">主要业绩和成果:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">存在的问题和不足:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">下一阶段工作计划:</span>的内容。请按照标准工作总结格式生成全文,包含\"工作总结、问题不足、未来计划\"三部分的完整报告。";
+      content = "总结主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n总结时间:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n主要内容:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的总结报告,要求格式规范、语言严谨,具体参考以上内容,按照标准工作总结格式生成全文,包含\"工作总结、问题不足、未来计划\"三部分的完整报告。";
       break;
     default:
-    content = "请帮我生成一份正式的公告,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">发文单位:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告编号:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">发布背景:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告核心条款:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">发文日期:</span>等内容。请按照标准公告格式生成全文,包括标题、正文、落款等所有要素。";
+    content = "公告主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n发文单位:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n核心内容:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的公告,要求格式规范、语言严谨,具体参考以上内容,按照标准公告格式生成全文,包括标题、正文、落款等所有要素。";
   }
   
   // 将模板内容填充到输入框中
@@ -4485,7 +4489,7 @@ onMounted(async () => {
   await nextTick();
   
   // 初始化模板内容,包含默认的模板文字
-  const defaultTemplate = "请帮我生成一份正式的总结报告,要求格式规范、语言严谨。具体内容包括总结主题:、总结时间:、主要业绩和成果:、存在的问题和不足:、下一阶段工作计划:的内容。请按照标准工作总结格式生成全文,包含\"工作总结、问题不足、未来计划\"三部分的完整报告。";
+  const defaultTemplate = "总结主题:\n总结时间:\n主要内容:\n\n请帮我生成一份正式的总结报告,要求格式规范、语言严谨,具体参考以上内容,按照标准工作总结格式生成全文,包含\"工作总结、问题不足、未来计划\"三部分的完整报告。";
   templateContent.value = defaultTemplate;
   
   // 获取历史记录列表

+ 39 - 27
src/views/Chat.vue

@@ -1748,37 +1748,34 @@ const handleSSEMessage = (data, aiMessageIndex) => {
   
   switch (data.type) {
     case 'intent':
-      // 意图识别完成,更新为查询知识库状态
-      updateMessageStatus(aiMessage, 'querying_kb')
-      
-      // 如果启用联网搜索,稍后会更新为web_searching状态
-      // (当收到web_search_raw事件时)
-      
       // 检查是否为专业问题
       if (data.is_professional_question === false) {
+        // 非专业问题:立即隐藏状态显示组件
+        aiMessage.showStats = false
+
         // 非专业问题,只输出summary字段内容并终止流程
         const summaryContent = data.summary || '抱歉,我暂时无法回答您的问题。'
-        
+
         // 只设置summary,不设置content和displayContent,避免重复显示
         aiMessage.summary = summaryContent
         aiMessage.isTyping = false // 停止加载动画
-        
+
         // 保存到数据库
         if (aiMessage.ai_message_id) {
           updateAIMessageContent(aiMessage.ai_message_id, summaryContent, summaryContent)
             .catch(err => console.error('回写AI消息失败:', err))
         }
-        
+
         // 关闭SSE连接
         if (sseConnection) {
           closeSSEConnection(sseConnection)
           sseConnection = null
         }
-        
+
         // 重置发送状态
         isSending.value = false
         streamingReports.value.clear()
-        
+
         // 重置AI回复流程状态
         isAIReplyProcessComplete.value = true
         
@@ -1834,18 +1831,24 @@ const handleSSEMessage = (data, aiMessageIndex) => {
         
         return // 终止处理
       }
-      
+
+      // 专业问题:意图识别完成,更新为查询知识库状态
+      updateMessageStatus(aiMessage, 'querying_kb')
+
+      // 如果启用联网搜索,稍后会更新为web_searching状态
+      // (当收到web_search_raw事件时)
+
       // 保存问题总结并使用打字机效果
       if (data.summary) {
         const fullSummary = data.summary
         aiMessage._fullSummary = fullSummary
         aiMessage.summary = ''
-        
+
         // 使用打字机效果显示问题总结
         startReportFieldTypewriter(
-          { file_index: 'summary', report: aiMessage, _typewriterStates: {} }, 
-          'summary', 
-          fullSummary, 
+          { file_index: 'summary', report: aiMessage, _typewriterStates: {} },
+          'summary',
+          fullSummary,
           50
         ).catch(err => {
           console.error('问题总结打字机效果失败:', err)
@@ -2546,26 +2549,32 @@ const handleReportGeneratorSubmit = async (data) => {
   })
   
   try {
-    const params = new URLSearchParams({
+    const apiPrefix = getApiPrefix()
+    const url = `${apiPrefix}/report/complete-flow`
+
+    // 构建 POST 请求体
+    const requestBody = {
       user_question: data.question,
       window_size: data.windowSize,
-      // n_results: data.nResults,
       n_results: 10,
-      // ===== 已删除:user_id - 后端从token解析 =====
       ai_conversation_id: ai_conversation_id.value,
       is_network_search_enabled: isNetworkSearchEnabled.value
+    }
+
+    console.log('📤 发起 SSE POST 请求:', {
+      url,
+      body: requestBody
     })
-    
-    const apiPrefix = getApiPrefix()
-    const url = `${apiPrefix}/report/complete-flow?${params.toString()}`
-    
+
     sseConnection = createSSEConnection(url, {
       onMessage: (eventData) => handleSSEMessage(eventData, aiMessageIndex),
       onError: handleSSEError,
       onComplete: handleSSEComplete,
       onInterrupted: handleSSEInterrupted
+    }, {
+      body: requestBody
     })
-    
+
   } catch (error) {
     console.error('启动失败:', error)
     ElMessage.error(`启动失败: ${error.message}`)
@@ -4783,8 +4792,11 @@ onActivated(async () => {
     padding: 6px 16px;
     box-shadow: 0 2px 8px rgba(91, 141, 239, 0.1);
     transition: all 0.3s ease;
-    max-width: 85%;
-    
+    max-width: 750px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+
     h2 {
       margin: 0;
       font-size: 13px;
@@ -4795,7 +4807,7 @@ onActivated(async () => {
       text-overflow: ellipsis;
       letter-spacing: 0.3px;
     }
-    
+
     &:hover {
       background: rgba(91, 141, 239, 0.12);
       border-color: rgba(91, 141, 239, 0.3);

+ 36 - 34
src/views/HazardDetection.vue

@@ -269,11 +269,10 @@
                                 <div class="process-flow">
                                     <!-- 流程步骤1 -->
                                     <div class="process-step">
-                                        <div class="step-image">
-                                            <img
-                                                src="../assets/Hazard/17.png"
-                                                alt="步骤1"
-                                            />
+                                        <div class="step-icon-wrapper">
+                                            <el-icon class="step-icon" :size="40" color="#3E7BFA">
+                                                <Upload />
+                                            </el-icon>
                                             <div class="step-number">1</div>
                                         </div>
                                         <div class="step-content">
@@ -281,7 +280,7 @@
                                                 上传图片
                                             </div>
                                             <div class="step-desc">
-                                                选择包含安全隐患的图片
+                                                选择相关场景的图片
                                             </div>
                                         </div>
                                     </div>
@@ -291,15 +290,14 @@
 
                                     <!-- 流程步骤2 -->
                                     <div class="process-step">
-                                        <div class="step-image">
-                                            <img
-                                                src="../assets/Hazard/18.png"
-                                                alt="步骤2"
-                                            />
+                                        <div class="step-icon-wrapper">
+                                            <el-icon class="step-icon" :size="40" color="#3E7BFA">
+                                                <View />
+                                            </el-icon>
                                             <div class="step-number">2</div>
                                         </div>
                                         <div class="step-content">
-                                            <div class="step-title">AI识别</div>
+                                            <div class="step-title">场景识别</div>
                                             <div class="step-desc">
                                                 智能识别场景要素
                                             </div>
@@ -311,19 +309,18 @@
 
                                     <!-- 流程步骤3 -->
                                     <div class="process-step">
-                                        <div class="step-image">
-                                            <img
-                                                src="../assets/Hazard/19.png"
-                                                alt="步骤3"
-                                            />
+                                        <div class="step-icon-wrapper">
+                                            <el-icon class="step-icon" :size="40" color="#3E7BFA">
+                                                <DataAnalysis />
+                                            </el-icon>
                                             <div class="step-number">3</div>
                                         </div>
                                         <div class="step-content">
                                             <div class="step-title">
-                                                隐患分析
+                                                场景分析
                                             </div>
                                             <div class="step-desc">
-                                                智能分析安全隐患
+                                                智能分析常见隐患
                                             </div>
                                         </div>
                                     </div>
@@ -333,19 +330,18 @@
 
                                     <!-- 流程步骤4 -->
                                     <div class="process-step">
-                                        <div class="step-image">
-                                            <img
-                                                src="../assets/Hazard/20.png"
-                                                alt="步骤4"
-                                            />
+                                        <div class="step-icon-wrapper">
+                                            <el-icon class="step-icon" :size="40" color="#3E7BFA">
+                                                <Bell />
+                                            </el-icon>
                                             <div class="step-number">4</div>
                                         </div>
                                         <div class="step-content">
                                             <div class="step-title">
-                                                查看隐患
+                                                隐患提示
                                             </div>
                                             <div class="step-desc">
-                                                查看详细隐患结果
+                                                提示场景常见隐患
                                             </div>
                                         </div>
                                     </div>
@@ -1086,6 +1082,7 @@
 <script setup>
 import { ref, onMounted, computed } from "vue";
 import { ElMessage } from "element-plus";
+import { Upload, View, DataAnalysis, Bell } from "@element-plus/icons-vue";
 import Sidebar from "@/components/Sidebar.vue";
 import DeleteConfirmModal from "@/components/DeleteConfirmModal.vue";
 import { apis } from "@/request/apis.js";
@@ -3080,19 +3077,24 @@ onMounted(() => {
                     width: 100%;
                     // margin-bottom: 20px;
 
-                    .step-image {
+                    .step-icon-wrapper {
                         position: relative;
                         width: 80px;
                         height: 80px;
                         margin-bottom: 12px;
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        background: linear-gradient(135deg, #EBF3FF 0%, #F5F9FF 100%);
+                        border-radius: 50%;
+                        box-shadow: 0 4px 12px rgba(62, 123, 250, 0.15);
 
-                        img {
-                            width: 100%;
-                            height: 100%;
-                            object-fit: cover;
-                            border-radius: 50%;
-                            // 黑色阴影
-                            box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
+                        .step-icon {
+                            transition: transform 0.3s ease;
+                        }
+
+                        &:hover .step-icon {
+                            transform: scale(1.1);
                         }
 
                         .step-number {

+ 70 - 464
src/views/NotFound.vue

@@ -1,62 +1,13 @@
 <template>
   <div class="not-found-container">
-    <!-- 错误图标 -->
-    <div class="error-icon">
-      <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
-        <circle cx="100" cy="100" r="80" fill="none" stroke="#ff6b6b" stroke-width="4"/>
-        <line x1="70" y1="70" x2="130" y2="130" stroke="#ff6b6b" stroke-width="4" stroke-linecap="round"/>
-        <line x1="130" y1="70" x2="70" y2="130" stroke="#ff6b6b" stroke-width="4" stroke-linecap="round"/>
-      </svg>
-    </div>
-
-    <!-- 404 错误码 -->
-    <h1 class="error-code">404</h1>
-
-    <!-- 错误标题 -->
-    <h2 class="error-title">{{ errorTitle }}</h2>
-
-    <!-- 错误消息 -->
-    <p class="error-message">{{ errorMessage }}</p>
-
-    <!-- 详细信息 -->
-    <div class="error-details" v-if="showDetails">
-      <p class="detail-text">{{ detailMessage }}</p>
-    </div>
-
-    <!-- 调试信息区域 -->
-    <div class="debug-info" v-if="debugInfo.length > 0">
-      <div class="debug-header" @click="toggleDebug">
-        <span>🐛 调试信息 (点击{{ showDebug ? '收起' : '展开' }})</span>
-      </div>
-      <div class="debug-content" v-if="showDebug">
-        <div class="debug-item" v-for="(log, index) in debugInfo" :key="index" :class="log.type">
-          <span class="debug-time">{{ log.time }}</span>
-          <span class="debug-message">{{ log.message }}</span>
-        </div>
+    <!-- Loading 动效 -->
+    <div class="loading-container">
+      <div class="loading-spinner">
+        <div class="spinner-ring"></div>
+        <div class="spinner-ring"></div>
+        <div class="spinner-ring"></div>
       </div>
-    </div>
-
-    <!-- 操作按钮 -->
-    <div class="action-buttons">
-      <button class="btn-primary" @click="retry">
-        <span class="icon">🔄</span>
-        重新尝试
-      </button>
-      <button class="btn-secondary" @click="contactSupport">
-        <span class="icon">📞</span>
-        联系支持
-      </button>
-    </div>
-
-    <!-- 帮助信息 -->
-    <div class="help-info">
-      <p>如果问题持续存在,请尝试以下操作:</p>
-      <ul>
-        <li>确认您有访问权限</li>
-        <li>检查网络连接</li>
-        <li>清除浏览器缓存后重试</li>
-        <li>联系系统管理员获取帮助</li>
-      </ul>
+      <p class="loading-text">正在为您跳转到统一登录门户,请稍后...</p>
     </div>
   </div>
 </template>
@@ -64,172 +15,23 @@
 <script>
 export default {
   name: 'NotFound',
-  data() {
-    return {
-      errorTitle: '认证失败',
-      errorMessage: '无法验证您的访问权限',
-      detailMessage: '票据验证失败或已过期,请重新从门户系统登录。',
-      showDetails: true,
-      debugInfo: [],
-      showDebug: false
-    }
-  },
   mounted() {
-    // 收集调试信息
-    this.collectDebugInfo()
-    
-    // 检查路由参数,自定义错误信息
+    // 记录日志
     const reason = this.$route.query.reason
-    
-    if (reason === 'ticket_failed') {
-      this.errorTitle = '票据验证失败'
-      this.errorMessage = '无法验证您的访问票据'
-      this.detailMessage = '票据可能已过期或无效,请重新从统一认证门户登录。'
-    } else if (reason === 'ticket_not_found') {
-      this.errorTitle = '缺少访问凭证'
-      this.errorMessage = '未检测到有效的访问票据'
-      this.detailMessage = '请从4A统一认证门户进入系统,不要直接访问此地址。'
-    } else if (reason === 'token_expired') {
-      this.errorTitle = '登录已过期'
-      this.errorMessage = '您的登录状态已失效'
-      this.detailMessage = '令牌已过期,请重新从统一认证门户登录。'
-    } else if (reason === 'logout') {
-      this.errorTitle = '已退出登录'
-      this.errorMessage = '您已成功退出系统'
-      this.detailMessage = '如需继续使用,请重新从统一认证门户登录。'
-    } else if (reason === 'no_permission') {
-      this.errorTitle = '无访问权限'
-      this.errorMessage = '您没有访问此系统的权限'
-      this.detailMessage = '请联系系统管理员申请访问权限。'
-    } else if (reason === 'network_error') {
-      this.errorTitle = '网络错误'
-      this.errorMessage = '无法连接到认证服务器'
-      this.detailMessage = '请检查网络连接后重试。'
-    } else if (reason === 'app_token_lost') {
-      this.errorTitle = 'APP 会话异常'
-      this.errorMessage = '检测到 APP 环境中登录状态丢失'
-      this.detailMessage = '这可能是 APP 返回操作导致的异常。请尝试重新打开此页面,或联系技术支持。'
-    } else if (!reason) {
-      this.errorTitle = '页面未找到'
-      this.errorMessage = '您访问的页面不存在'
-      this.detailMessage = '请检查URL是否正确,或返回首页。'
-    }
-    
     console.log('🚫 进入404页面,原因:', reason || '未知')
-  },
-  methods: {
-    collectDebugInfo() {
-      const logs = []
-      
-      // 基本信息
-      logs.push({
-        type: 'info',
-        time: new Date().toLocaleTimeString(),
-        message: `当前 URL: ${window.location.href}`
-      })
-      
-      logs.push({
-        type: 'info',
-        time: new Date().toLocaleTimeString(),
-        message: `用户代理: ${navigator.userAgent}`
-      })
-      
-      logs.push({
-        type: 'info',
-        time: new Date().toLocaleTimeString(),
-        message: `是否移动设备: ${/Mobile|Android|iPhone|iPad/i.test(navigator.userAgent)}`
-      })
-      
-      // localStorage 信息
-      const hasToken = !!localStorage.getItem('shudao_refresh_token')
-      logs.push({
-        type: hasToken ? 'success' : 'error',
-        time: new Date().toLocaleTimeString(),
-        message: `本地 Token: ${hasToken ? '存在' : '不存在'}`
-      })
-      
-      const hasUsername = !!localStorage.getItem('shudao_username')
-      logs.push({
-        type: 'info',
-        time: new Date().toLocaleTimeString(),
-        message: `用户名: ${hasUsername ? localStorage.getItem('shudao_username') : '未保存'}`
-      })
-      
-      // 路由信息
-      const reason = this.$route.query.reason
-      logs.push({
-        type: 'warning',
-        time: new Date().toLocaleTimeString(),
-        message: `错误原因: ${reason || '未指定'}`
-      })
-      
-      // 检查 URL 中是否还有票据参数
-      const url = new URL(window.location.href)
-      const hasTicketInUrl = url.hash.includes('iamcaspticket') || url.search.includes('iamcaspticket')
-      logs.push({
-        type: hasTicketInUrl ? 'warning' : 'info',
-        time: new Date().toLocaleTimeString(),
-        message: `URL 中票据参数: ${hasTicketInUrl ? '存在 (异常!)' : '已清理'}`
-      })
-      
-      // 尝试从 sessionStorage 获取认证日志
-      try {
-        const authLogs = sessionStorage.getItem('auth_debug_logs')
-        if (authLogs) {
-          const parsedLogs = JSON.parse(authLogs)
-          parsedLogs.forEach(log => {
-            logs.push({
-              type: log.level || 'info',
-              time: log.time || new Date().toLocaleTimeString(),
-              message: log.message
-            })
-          })
-        }
-      } catch (e) {
-        logs.push({
-          type: 'error',
-          time: new Date().toLocaleTimeString(),
-          message: `读取认证日志失败: ${e.message}`
-        })
-      }
-      
-      this.debugInfo = logs
-    },
-    
-    toggleDebug() {
-      this.showDebug = !this.showDebug
-    },
-    
-    retry() {
-      console.log('🔄 用户点击重新尝试')
-      // 清除本地存储
-      localStorage.removeItem('shudao_refresh_token')
-      localStorage.removeItem('shudao_token_type')
-      localStorage.removeItem('shudao_username')
-      sessionStorage.removeItem('auth_debug_logs')
-      
-      // 跳转回首页,触发重新认证
-      window.location.href = window.location.origin + window.location.pathname
-    },
-    
-    contactSupport() {
-      console.log('📞 用户请求联系支持')
-      
-      // 生成调试报告
-      const report = this.debugInfo.map(log => `[${log.time}] ${log.message}`).join('\n')
-      const reportText = `调试报告:\n\n${report}\n\n请将此信息发送给技术支持。`
-      
-      // 复制到剪贴板
-      if (navigator.clipboard) {
-        navigator.clipboard.writeText(reportText).then(() => {
-          alert('调试信息已复制到剪贴板!\n\n请联系系统管理员并提供此信息。')
-        }).catch(() => {
-          alert(reportText)
-        })
-      } else {
-        alert(reportText)
-      }
-    }
+    console.log('🔄 即将重定向到登录门户...')
+
+    // 清除本地存储的认证信息
+    localStorage.removeItem('shudao_refresh_token')
+    localStorage.removeItem('shudao_token_type')
+    localStorage.removeItem('shudao_username')
+    sessionStorage.removeItem('auth_debug_logs')
+
+    // 延迟 2 秒后重定向到登录门户网站
+    setTimeout(() => {
+      console.log('🚀 开始跳转到登录门户')
+      window.location.href = 'https://tyrz.scgsdsj.com/iga/login_sd.html'
+    }, 2000)
   }
 }
 </script>
@@ -247,250 +49,78 @@ export default {
   text-align: center;
 }
 
-/* 错误图标 */
-.error-icon {
-  width: 100px;
-  height: 100px;
-  margin: 0 auto 30px;
-  animation: fadeIn 0.6s ease-out;
+/* Loading 容器 */
+.loading-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 24px;
+  animation: fadeIn 0.8s ease-out both;
 }
 
 @keyframes fadeIn {
   from {
     opacity: 0;
-    transform: scale(0.8);
+    transform: translateY(-20px);
   }
   to {
     opacity: 1;
-    transform: scale(1);
+    transform: translateY(0);
   }
 }
 
-.error-icon svg {
-  width: 100%;
-  height: 100%;
-  filter: drop-shadow(0 2px 8px rgba(255, 107, 107, 0.2));
-}
-
-/* 404 错误码 */
-.error-code {
-  font-size: 96px;
-  font-weight: 700;
-  color: #ff6b6b;
-  margin: 0 0 20px;
-  line-height: 1;
-  letter-spacing: -2px;
-}
-
-/* 错误标题 */
-.error-title {
-  font-size: 24px;
-  font-weight: 600;
-  color: #2c3e50;
-  margin: 0 0 12px;
-}
-
-/* 错误消息 */
-.error-message {
+/* Loading 文字 */
+.loading-text {
   font-size: 16px;
-  color: #95a5a6;
-  margin: 0 0 30px;
-  line-height: 1.6;
-  max-width: 500px;
-}
-
-/* 详细信息 - 浅黄色胶囊样式 */
-.error-details {
-  background: #fff9e6;
-  border-radius: 25px;
-  padding: 16px 24px;
-  margin: 0 auto 30px;
-  max-width: 600px;
-  display: inline-block;
-}
-
-.detail-text {
-  margin: 0;
-  color: #d4a017;
-  font-size: 14px;
-  line-height: 1.6;
-}
-
-/* 操作按钮 - 胶囊样式 */
-.action-buttons {
-  display: flex;
-  gap: 12px;
-  justify-content: center;
-  margin-bottom: 40px;
-  flex-wrap: wrap;
-}
-
-.action-buttons button {
-  padding: 12px 28px;
-  border: none;
-  border-radius: 25px;
-  font-size: 15px;
-  font-weight: 500;
-  cursor: pointer;
-  transition: all 0.3s ease;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-}
-
-/* 主按钮 - 浅蓝色 */
-.btn-primary {
-  background: #5dade2;
-  color: white;
-}
-
-.btn-primary:hover {
-  background: #3498db;
-  transform: translateY(-2px);
-  box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
-}
-
-/* 次要按钮 - 白色边框 */
-.btn-secondary {
-  background: white;
   color: #5dade2;
-  border: 2px solid #5dade2;
-}
-
-.btn-secondary:hover {
-  background: #5dade2;
-  color: white;
-  transform: translateY(-2px);
-  box-shadow: 0 4px 12px rgba(52, 152, 219, 0.2);
-}
-
-.icon {
-  font-size: 16px;
-}
-
-/* 帮助信息 */
-.help-info {
-  background: #f8f9fa;
-  border-radius: 16px;
-  padding: 24px 28px;
-  text-align: left;
-  max-width: 500px;
-  margin: 0 auto;
-}
-
-.help-info p {
-  margin: 0 0 16px;
-  font-weight: 600;
-  color: #2c3e50;
-  font-size: 15px;
-}
-
-.help-info ul {
+  font-weight: 500;
   margin: 0;
-  padding-left: 0;
-  list-style: none;
+  letter-spacing: 0.5px;
 }
 
-.help-info li {
-  margin-bottom: 12px;
-  color: #7f8c8d;
-  font-size: 14px;
-  line-height: 1.6;
+/* Spinner 容器 */
+.loading-spinner {
   position: relative;
-  padding-left: 24px;
+  width: 80px;
+  height: 80px;
 }
 
-.help-info li:before {
-  content: "✓";
+/* Spinner 圆环 */
+.spinner-ring {
   position: absolute;
-  left: 0;
-  color: #5dade2;
-  font-weight: bold;
-  font-size: 16px;
-}
-
-.help-info li:last-child {
-  margin-bottom: 0;
-}
-
-/* 调试信息样式 */
-.debug-info {
-  background: #f8f9fa;
-  border: 1px solid #e9ecef;
-  border-radius: 16px;
-  margin: 20px auto;
-  overflow: hidden;
-  max-width: 600px;
-}
-
-.debug-header {
-  background: #2c3e50;
-  color: white;
-  padding: 14px 20px;
-  cursor: pointer;
-  user-select: none;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  transition: background 0.3s;
-  font-size: 14px;
-  font-weight: 500;
-}
-
-.debug-header:hover {
-  background: #34495e;
-}
-
-.debug-content {
-  max-height: 400px;
-  overflow-y: auto;
-  padding: 16px;
-}
-
-.debug-item {
-  padding: 10px 12px;
-  margin-bottom: 8px;
-  border-radius: 8px;
-  font-family: 'Courier New', monospace;
-  font-size: 12px;
-  display: flex;
-  gap: 10px;
-  align-items: flex-start;
-}
-
-.debug-item.info {
-  background: #e3f2fd;
-  border-left: 3px solid #5dade2;
-}
-
-.debug-item.success {
-  background: #e8f5e9;
-  border-left: 3px solid #4caf50;
+  width: 100%;
+  height: 100%;
+  border: 4px solid transparent;
+  border-radius: 50%;
+  animation: spin 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
 }
 
-.debug-item.warning {
-  background: #fff3e0;
-  border-left: 3px solid #ff9800;
+.spinner-ring:nth-child(1) {
+  border-top-color: #5dade2;
+  animation-delay: 0s;
 }
 
-.debug-item.error {
-  background: #ffebee;
-  border-left: 3px solid #f44336;
+.spinner-ring:nth-child(2) {
+  border-right-color: #3498db;
+  animation-delay: 0.15s;
 }
 
-.debug-time {
-  flex-shrink: 0;
-  font-weight: bold;
-  color: #666;
-  min-width: 80px;
+.spinner-ring:nth-child(3) {
+  border-bottom-color: #5dade2;
+  animation-delay: 0.3s;
+  opacity: 0.6;
 }
 
-.debug-message {
-  flex: 1;
-  word-break: break-all;
-  line-height: 1.5;
-  color: #333;
+@keyframes spin {
+  0% {
+    transform: rotate(0deg) scale(1);
+  }
+  50% {
+    transform: rotate(180deg) scale(1.1);
+  }
+  100% {
+    transform: rotate(360deg) scale(1);
+  }
 }
 
 /* 移动端适配 */
@@ -499,37 +129,13 @@ export default {
     padding: 30px 16px;
   }
 
-  .error-icon {
-    width: 80px;
-    height: 80px;
-    margin-bottom: 24px;
-  }
-
-  .error-code {
-    font-size: 72px;
-  }
-
-  .error-title {
-    font-size: 20px;
-  }
-
-  .error-message {
-    font-size: 15px;
-  }
-
-  .action-buttons {
-    flex-direction: column;
-    width: 100%;
-    max-width: 300px;
-  }
-
-  .action-buttons button {
-    width: 100%;
-    justify-content: center;
+  .loading-spinner {
+    width: 60px;
+    height: 60px;
   }
 
-  .help-info {
-    padding: 20px;
+  .loading-text {
+    font-size: 14px;
   }
 }
 </style>

+ 6 - 6
src/views/mobile/m-AIWriting.vue

@@ -339,22 +339,22 @@ const useTemplate = (templateName) => {
   
   switch (templateName) {
     case '公告模板':
-      content = "请帮我生成一份正式的公告,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">发文单位:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告编号:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">发布背景:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告核心条款:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">发文日期:</span>等内容。请按照标准公告格式生成全文,包括标题、正文、落款等所有要素。"
+      content = "公告主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n发文单位:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n核心内容:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的公告,要求格式规范、语言严谨,具体参考以上内容,按照标准公告格式生成全文,包括标题、正文、落款等所有要素。"
       break
     case '通知模板':
-      content = "请帮我生成一份正式的通知,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">通知对象:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">通知主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">通知背景:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">通知内容:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">时间安排:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">联系方式:</span>等内容。请按照标准通知格式生成全文,确保表达清楚、信息准确。"
+      content = "通知主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n通知对象:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n具体事项:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的通知,要求格式规范、语言严谨,具体参考以上内容,按照标准公文格式生成完整通知,包括文号、标题、正文、落款等所有要素。"
       break
     case '工作汇报模板':
-      content = "请帮我生成一份正式的工作汇报,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">汇报主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">汇报时间:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">主要工作内容:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">完成情况:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">存在问题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">下步计划:</span>等内容。请按照标准工作汇报格式生成全文,确保内容全面、数据准确。"
+      content = "总结主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n总结时间:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n主要内容:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的总结报告,要求格式规范、语言严谨,具体参考以上内容,按照标准工作总结格式生成全文,包含\"工作总结、问题不足、未来计划\"三部分的完整报告。"
       break
     case '会议纪要模版':
-      content = "请帮我生成一份正式的会议纪要,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">会议主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">会议时间:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">参会人员:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">会议议程:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">议题讨论:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决议事项:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">后续安排:</span>等内容。请按照标准会议纪要格式生成全文,确保记录准确、要点清晰。"
+      content = "会议主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n会议时间:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n主要议题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的会议纪要,要求格式规范、语言严谨,具体参考以上内容,按照标准会议纪要格式生成全文,包含标题、导语、议定事项和落款。"
       break
     case '决定模版':
-      content = "请帮我生成一份正式的决定,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">决定主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决定背景:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决定依据:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决定内容:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">执行要求:</span>等内容。请按照标准决定文件格式生成全文,确保表述准确、要求明确。"
+      content = "决定主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n决定依据:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n决定内容:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的决定,要求格式规范、语言严谨,具体参考以上内容,按照标准决定公文格式生成完整文件。"
       break
     default:
-      content = "请帮我生成一份正式的文档,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">文档主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">主要内容:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">具体要求:</span>等内容。"
+      content = "文档主题:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n主要内容:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n具体要求:<span class=\"editable-highlight\" contenteditable=\"true\"></span>\n\n请帮我生成一份正式的文档,要求格式规范、语言严谨,具体参考以上内容。"
   }
   
   // 设置模板内容到输入框

+ 110 - 231
src/views/mobile/m-Chat.vue

@@ -142,19 +142,10 @@
               <div class="message-content" :data-message-index="index" :ref="el => messageContentRefs[index] = el">
                 <!-- AI响应内容 -->
                 <div class="ai-response-content">
-                  <!-- 进度统计卡片 - 与PC端一致,只要showStats为true就显示 -->
-                  <div 
-                    v-if="message.showStats" 
+                  <!-- 进度统计卡片 -->
+                  <div
+                    v-if="message.showStats"
                     class="stats-card"
-                    :class="{ 'is-sticky': messageScrollStates[index]?.isSticky && messageScrollStates[index]?.initialized && !messageScrollStates[index]?.isInitializing }"
-                    :style="(messageScrollStates[index]?.isSticky && messageScrollStates[index]?.initialized && !messageScrollStates[index]?.isInitializing && messageScrollStates[index]?.initialLeft > 0 && messageScrollStates[index]?.initialWidth > 0) ? {
-                      position: 'fixed',
-                      top: '60px',
-                      left: messageScrollStates[index].initialLeft + 'px',
-                      width: messageScrollStates[index].initialWidth + 'px',
-                      zIndex: 999
-                    } : {}"
-                    :data-message-index="index"
                   >
                 <div class="stats-left">
                   <StatusAvatar 
@@ -173,15 +164,6 @@
                   </div>
                   <span class="progress-percentage">{{ message.progress }}%</span>
                 </div>
-                
-                <!-- 导出按钮 -->
-                <div v-if="message.progress === 100 && message.reports && message.reports.length > 0" class="stats-right">
-                  <ExportButton
-                    :reports="message.reports.filter(r => r.status === 'completed' && r.type !== 'category_title')"
-                    :disabled="false"
-                    :title="exportTitle"
-                  />
-              </div>
             </div>
             
                   <!-- 问题总结 -->
@@ -452,7 +434,14 @@
             </svg>
             <p>{{ fileError }}</p>
           </div>
-          <iframe v-else-if="previewFilePath" :src="previewFilePath" frameborder="0" class="file-iframe"></iframe>
+          <iframe
+            v-else-if="previewFilePath"
+            :src="previewFilePath"
+            frameborder="0"
+            class="file-iframe"
+            @load="fileLoading = false"
+            @error="fileError = '文件加载失败'"
+          ></iframe>
           <div v-else class="file-empty">
             <svg class="empty-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
               <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
@@ -475,7 +464,6 @@ import MobileToast from '@/components/MobileToast.vue'
 import MobileHistoryDrawer from '@/components/MobileHistoryDrawer.vue'
 import CategoryTitle from '@/components/CategoryTitle.vue'
 import FileReportCard from '@/components/FileReportCard.vue'
-import ExportButton from '@/components/ExportButton.vue'
 import StreamMarkdown from '@/components/StreamMarkdown.vue'
 import WebSearchCapsule from '@/components/WebSearchCapsule.vue'
 import WebSearchSummary from '@/components/WebSearchSummary.vue'
@@ -571,9 +559,8 @@ const previewFileName = ref('')
 const fileLoading = ref(false)
 const fileError = ref('')
 
-// 进度卡片悬浮状态 - 使用与PC端一致的状态管理
+// 消息内容引用
 const messageContentRefs = ref({})
-const messageScrollStates = ref({})
 
 // 语音识别功能
 const {
@@ -622,7 +609,6 @@ let sseConnection = null
 // 报告生成相关状态
 const categoryExpandStates = ref({}) // 分类展开状态
 const currentQuestion = ref('') // 当前问题
-const exportTitle = ref('知识库文件') // 导出标题
 const streamingReports = ref(new Map()) // 流式报告缓存
 const reportTypewriters = new Map() // 存储每个报告字段的打字机定时器
 const typewriterIntervals = new Map() // 存储打字机定时器
@@ -2110,31 +2096,38 @@ const openInNewTab = () => {
 }
 
 // 处理文件预览
-const handleFilePreview = async (data) => {
+const handleFilePreview = (data) => {
   console.log('移动端打开文件预览:', data)
-  
+
   // 重置状态
   fileError.value = ''
   fileLoading.value = false
-  
-  if (!data || (!data.filePath && !data.fileName)) {
-    fileError.value = '文件路径为空'
-    previewFilePath.value = ''
+
+  // 处理不同类型的输入参数
+  if (typeof data === 'string') {
+    previewFilePath.value = data
     previewFileName.value = ''
-  } else {
+  } else if (data && data.filePath) {
     previewFilePath.value = data.filePath
     previewFileName.value = data.fileName || ''
-    
-    // 显示加载状态
-    if (data.filePath) {
-      fileLoading.value = true
-      // 模拟加载延迟,实际iframe会自己处理加载
-      setTimeout(() => {
+  } else {
+    fileError.value = '文件路径为空'
+    previewFilePath.value = ''
+    previewFileName.value = ''
+  }
+
+  // 显示加载状态
+  if (previewFilePath.value) {
+    fileLoading.value = true
+    // 设置超时,如果5秒后还在加载,显示错误
+    setTimeout(() => {
+      if (fileLoading.value) {
         fileLoading.value = false
-      }, 500)
-    }
+        fileError.value = '😔 抱歉,未找到文件链接,正在快马加鞭修复中!'
+      }
+    }, 5000)
   }
-  
+
   showFilePreview.value = true
 }
 
@@ -2470,37 +2463,34 @@ const handleSSEMessage = (data, aiMessageIndex) => {
   
   switch (data.type) {
     case 'intent':
-      // 意图识别完成,更新为查询知识库状态
-      updateMessageStatus(aiMessage, 'querying_kb')
-      
-      // 如果启用联网搜索,稍后会更新为web_searching状态
-      // (当收到web_search_raw事件时)
-      
       // 检查是否为专业问题
       if (data.is_professional_question === false) {
+        // 非专业问题:立即隐藏状态显示组件
+        aiMessage.showStats = false
+
         // 非专业问题,只输出summary字段内容并终止流程
         const summaryContent = data.summary || '抱歉,我暂时无法回答您的问题。'
-        
+
         // 只设置summary,不设置content和displayContent,避免重复显示
         aiMessage.summary = summaryContent
         aiMessage.isTyping = false // 停止加载动画
-        
+
         // 保存到数据库
         if (aiMessage.ai_message_id) {
           updateAIMessageContent(aiMessage.ai_message_id, summaryContent, summaryContent)
             .catch(err => console.error('回写AI消息失败:', err))
         }
-        
+
         // 关闭SSE连接
         if (sseConnection) {
           closeSSEConnection(sseConnection)
           sseConnection = null
         }
-        
+
         // 重置发送状态
         isSending.value = false
         streamingReports.value.clear()
-        
+
         // 重置AI回复流程状态
         isAIReplyProcessComplete.value = true
         
@@ -2556,18 +2546,24 @@ const handleSSEMessage = (data, aiMessageIndex) => {
         
         return // 终止处理
       }
-      
+
+      // 专业问题:意图识别完成,更新为查询知识库状态
+      updateMessageStatus(aiMessage, 'querying_kb')
+
+      // 如果启用联网搜索,稍后会更新为web_searching状态
+      // (当收到web_search_raw事件时)
+
       // 保存问题总结并使用打字机效果
       if (data.summary) {
         const fullSummary = data.summary
         aiMessage._fullSummary = fullSummary
         aiMessage.summary = ''
-        
+
         // 使用打字机效果显示问题总结
         startReportFieldTypewriter(
-          { file_index: 'summary', report: aiMessage, _typewriterStates: {} }, 
-          'summary', 
-          fullSummary, 
+          { file_index: 'summary', report: aiMessage, _typewriterStates: {} },
+          'summary',
+          fullSummary,
           50
         ).catch(err => {
           console.error('问题总结打字机效果失败:', err)
@@ -2589,11 +2585,6 @@ const handleSSEMessage = (data, aiMessageIndex) => {
         // 如果没有联网搜索,直接显示检索结果
         updateMessageStatus(aiMessage, 'data_retrieved')
       }
-      
-      // 文档数量更新后,触发滚动检测以初始化进度卡片
-      nextTick(() => {
-        handleProgressCardScroll()
-      })
       break
       
     case 'category_title':
@@ -2969,12 +2960,6 @@ const handleSSEComplete = () => {
       if (message.showStats && message.reports && message.reports.length > 0) {
         message.progress = 100
         console.log('✅ SSE完成,设置进度为100%')
-        console.log('📊 导出按钮条件检查:', {
-          progress: message.progress,
-          hasReports: !!message.reports,
-          reportsLength: message.reports?.length,
-          showStats: message.showStats
-        })
       }
       
       // 只有在进度达到100%或没有报告时才停止typing状态
@@ -3268,35 +3253,34 @@ const handleReportGeneratorSubmit = async (data) => {
     rawData: null,
     userFeedback: null
   })
-  
-  // 等待DOM更新后触发滚动检测,初始化进度卡片状态
-  nextTick(() => {
-    setTimeout(() => {
-      handleProgressCardScroll()
-    }, 100)
-  })
-  
+
   try {
-    const params = new URLSearchParams({
+    const apiPrefix = getApiPrefix()
+    const url = `${apiPrefix}/report/complete-flow`
+
+    // 构建 POST 请求体
+    const requestBody = {
       user_question: data.question,
       window_size: data.windowSize,
-      // n_results: data.nResults,
       n_results: 2,
-      // ===== 已删除:user_id - 后端从token解析 =====
       ai_conversation_id: ai_conversation_id.value,
       is_network_search_enabled: isNetworkSearchEnabled.value
+    }
+
+    console.log('📤 发起 SSE POST 请求:', {
+      url,
+      body: requestBody
     })
-    
-    const apiPrefix = getApiPrefix()
-    const url = `${apiPrefix}/report/complete-flow?${params.toString()}`
-    
+
     sseConnection = createSSEConnection(url, {
       onMessage: (eventData) => handleSSEMessage(eventData, aiMessageIndex),
       onError: handleSSEError,
       onComplete: handleSSEComplete,
       onInterrupted: handleSSEInterrupted
+    }, {
+      body: requestBody
     })
-    
+
   } catch (error) {
     console.error('启动失败:', error)
     showToastMessage(`启动失败: ${error.message}`, 2000)
@@ -3502,12 +3486,7 @@ onMounted(async () => {
     // 移动端使用 window 滚动而不是容器滚动
     console.log('✅ 移动端:添加 window 滚动监听')
     
-    // 添加测试监听器来确认滚动事件能触发
-    window.addEventListener('scroll', () => {
-      console.log('🔄 移动端滚动事件触发!scrollY:', window.scrollY)
-    }, { once: true })
-    
-    window.addEventListener('scroll', handleProgressCardScroll)
+
     
     // 测试TTS服务连接
     try {
@@ -4171,129 +4150,7 @@ const editUserMessage = (message) => {
   })
 }
 
-// 处理滚动事件,实现进度卡片悬浮效果(与PC端一致的逻辑)
-const handleProgressCardScroll = () => {
-  console.log('📱 移动端 handleProgressCardScroll 被触发')
-  const mobileHeader = document.querySelector('.mobile-header')
-  const headerBottom = mobileHeader ? mobileHeader.getBoundingClientRect().bottom : 60
-  console.log('📐 移动端 headerBottom:', headerBottom)
-  console.log('📊 移动端 messageContentRefs 数量:', Object.keys(messageContentRefs.value).length)
-  
-  Object.keys(messageContentRefs.value).forEach(index => {
-    const message = chatMessages.value[index]
-    if (!message || message.type !== 'ai' || !message.showStats) {
-      console.log(`⏭️ 移动端跳过消息 ${index}:`, { 
-        exists: !!message, 
-        type: message?.type, 
-        showStats: message?.showStats 
-      })
-      return
-    }
-    
-    console.log(`✅ 移动端处理消息 ${index}`)
-    
-    const contentEl = messageContentRefs.value[index]
-    if (!contentEl) {
-      console.log(`❌ 移动端消息 ${index} 没有找到 contentEl`)
-      return
-    }
-    
-    // 查找进度统计卡片
-    const statsCard = contentEl.querySelector('.stats-card')
-    if (!statsCard) {
-      console.log(`❌ 移动端消息 ${index} 没有找到 .stats-card`)
-      return
-    }
-    
-    console.log(`✅ 移动端消息 ${index} 找到进度卡片`)
-    
-    const cardRect = statsCard.getBoundingClientRect()
-    const contentRect = contentEl.getBoundingClientRect()
-    
-    // 如果还没有保存状态,先初始化
-    if (!messageScrollStates.value[index]) {
-      messageScrollStates.value[index] = {
-        initialLeft: cardRect.left,
-        initialWidth: cardRect.width,
-        isSticky: false,
-        offsetTop: cardRect.top - contentRect.top,
-        initialized: false,
-        isInitializing: true,
-        initStartTime: Date.now()
-      }
-      
-      // 使用nextTick确保DOM渲染完成后再标记初始化完成
-      nextTick(() => {
-        requestAnimationFrame(() => {
-          if (messageScrollStates.value[index]) {
-            const updatedStatsCard = contentEl.querySelector('.stats-card')
-            if (updatedStatsCard) {
-              const updatedCardRect = updatedStatsCard.getBoundingClientRect()
-              messageScrollStates.value[index].initialLeft = updatedCardRect.left
-              messageScrollStates.value[index].initialWidth = updatedCardRect.width
-            }
-            
-            const elapsed = Date.now() - messageScrollStates.value[index].initStartTime
-            const remainingTime = Math.max(0, 100 - elapsed)
-            
-            setTimeout(() => {
-              if (messageScrollStates.value[index]) {
-                messageScrollStates.value[index].initialized = true
-                messageScrollStates.value[index].isInitializing = false
-              }
-            }, remainingTime)
-          }
-        })
-      })
-      
-      return
-    }
-    
-    // 如果还没初始化完成或正在初始化,跳过吸附判断
-    if (!messageScrollStates.value[index].initialized || messageScrollStates.value[index].isInitializing) {
-      return
-    }
-    
-    // 额外检查:确保初始化后至少过了100ms才允许吸附
-    const timeSinceInit = Date.now() - messageScrollStates.value[index].initStartTime
-    if (timeSinceInit < 100) {
-      return
-    }
-    
-    // 计算卡片的"自然"位置(如果不吸附的话应该在哪里)
-    const naturalTop = contentRect.top + messageScrollStates.value[index].offsetTop
-    
-    // 判断是否需要吸附
-    // 条件:卡片的自然位置已经到达或超过顶部域,且消息内容还在可视范围内
-    const shouldStick = naturalTop <= headerBottom && contentRect.bottom > headerBottom + 60
-    
-    console.log(`📍 移动端消息 ${index} 吸附判断:`, {
-      naturalTop,
-      headerBottom,
-      'naturalTop <= headerBottom': naturalTop <= headerBottom,
-      'contentRect.bottom': contentRect.bottom,
-      'headerBottom + 60': headerBottom + 60,
-      shouldStick,
-      currentlySticky: messageScrollStates.value[index].isSticky
-    })
-    
-    // 如果从吸附变为非吸附,或从非吸附变为吸附,更新位置信息
-    if (messageScrollStates.value[index].isSticky !== shouldStick) {
-      if (!shouldStick) {
-        // 解除吸附时,重新计算位置
-        messageScrollStates.value[index].initialLeft = cardRect.left
-        messageScrollStates.value[index].initialWidth = cardRect.width
-      } else {
-        // 开始吸附时,使用当前位置
-        messageScrollStates.value[index].initialLeft = cardRect.left
-        messageScrollStates.value[index].initialWidth = cardRect.width
-      }
-    }
-    
-    messageScrollStates.value[index].isSticky = shouldStick
-    messageScrollStates.value[index].stickyTop = headerBottom
-  })
-}
+
 
 // 语音输入相关方法
 const handleVoiceClick = () => {
@@ -4573,10 +4430,7 @@ onBeforeUnmount(() => {
   window.removeEventListener('beforeunload', handlePageUnload)
   window.removeEventListener('unload', handlePageUnload)
   document.removeEventListener('visibilitychange', handleVisibilityChange)
-  
-  // 清理滚动监听器
-  window.removeEventListener('scroll', handleProgressCardScroll)
-  
+
   // 清理规范引用点击事件监听器
   document.removeEventListener('click', handleStandardReferenceClick)
 })
@@ -4827,47 +4681,55 @@ onActivated(async () => {
   display: flex;
   flex-direction: column;
   gap: 0;
-  
+  overflow-x: hidden;
+  max-width: 100%;
+
   .ai-message-main {
     display: flex;
     gap: 12px;
+    overflow-x: hidden;
+    max-width: 100%;
   }
-  
+
   .ai-avatar-small {
     width: 52px;
     height: 52px;
     flex-shrink: 0;
-    
+
     .ai-icon {
       width: 100%;
       height: 100%;
       object-fit: contain;
     }
   }
-  
+
   // 网络搜索胶囊外层容器
   .web-search-capsule-outer {
     margin-bottom: 8px;
     display: flex;
     justify-content: flex-start;
     padding-left: 62px; // 为头像留空间
+    overflow-x: hidden;
+    max-width: 100%;
   }
-  
+
   // AI消息主体容器
   .ai-message-main {
     display: flex;
     gap: 10px;
     align-items: flex-start;
     margin-bottom: 16px;
+    overflow-x: hidden;
+    max-width: 100%;
   }
-  
+
   // AI头像样式 - 移动端(用户已设置为52px)
   .ai-avatar-small {
     width: 52px;
     height: 52px;
     flex-shrink: 0;
     animation: avatar-pulse 2s ease-in-out infinite;
-    
+
     .ai-icon {
       width: 100%;
       height: 100%;
@@ -4875,7 +4737,7 @@ onActivated(async () => {
       filter: drop-shadow(0 2px 8px rgba(91, 141, 239, 0.15));
     }
   }
-  
+
   @keyframes avatar-pulse {
     0%, 100% {
       transform: scale(1);
@@ -4884,16 +4746,20 @@ onActivated(async () => {
       transform: scale(1.05);
     }
   }
-  
+
   // 白色气泡容器 - 包裹所有AI内容
   .message-content {
     flex: 1;
     min-width: 0;
+    max-width: 100%;
     background: white;
     border-radius: 12px;
     padding: 16px;
     box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
     text-align: left;
+    overflow-x: hidden;
+    word-wrap: break-word;
+    overflow-wrap: break-word;
   }
   
   // AI响应内容容器
@@ -4972,22 +4838,35 @@ onActivated(async () => {
   // AI文本内容
   .ai-text {
     text-align: left;
-    
+    max-width: 100%;
+    overflow-x: hidden;
+    word-wrap: break-word;
+
     .ai-markdown-content {
       text-align: left;
-      
+      max-width: 100%;
+      overflow-x: hidden;
+      word-wrap: break-word;
+
       * {
         text-align: left !important;
+        max-width: 100%;
+        word-wrap: break-word;
       }
     }
   }
-  
+
   .question-summary {
     text-align: left;
     margin-bottom: 12px;
-    
+    max-width: 100%;
+    overflow-x: hidden;
+    word-wrap: break-word;
+
     * {
       text-align: left !important;
+      max-width: 100%;
+      word-wrap: break-word;
     }
   }
   

+ 51 - 0
移动客户端与H5对接规范.md

@@ -0,0 +1,51 @@
+移动客户端与H5对接规范
+一、概述
+在移动互联网技术发展成熟的今天,为了更好的满足app快速研发、及时更新、模块分离等需要;在原生应用中集成H5页面也越来越重要和常见。因此需要制定良好的规范以提高软件整体质量及研发效率。
+二、集成形式及系统环境
+(一)集成形式
+简单来说,就是把H5放到客户端中加载。为了更好的提升体验,客户端要保证H5容器的稳定和性能,
+客户端需要为H5提供加载容器及基本的能力支持,如:上传文件、定位、错误处理。
+(二)宿主系统环境
+Android:最低支持Android 8.0 系统,浏览器内核为Chrome;
+iOS:最低支持iOS 13系统,浏览器内核为WebKit。
+针对上述宿主环境,H5业务系统需要做兼容性适配。
+三、集成要求
+(一)单点登录
+1.app访问H5业务系统时,首先调用该系统接口获取授权token,然后按照该业务系统要求携带相关参数访问业务系统h5链接,实现单点登录。
+2.针对业务系统无权限用户,需要返回给app单独的错误码和错误提示信息。
+3.如使用4a作为认证,在访问业务系统时,无权限账户要进行友好提示。
+(二)导航栏设计
+1.建议使用app原生导航栏,app会监听h5页面标题的变化并进行展示。
+2.如受到业务系统限制不能使用app原生导航栏,需要调用交互关闭导航栏。
+3.H5页面自定义导航栏的情况下,需要适配安全区并且需要增加关闭原生页面的交互。
+(三) H5与app交互
+为了保证业务系统功能完善、性能优良,对接时往往需要支持多种交互。
+3.1 返回上一级与关闭页面
+在使用app原生导航栏的情况下,建议同时添加“返回webGoBack()”和“关闭页面(nativeClosePage())”这两个交互来实现返回上一级、关闭页面的功能;如业务系统评估不需要增加,则app根据webview返回栈调用返回和关闭。
+3.2 当前支持的交互
+当前支持的交互内容如下表所列,H5业务系统可以按照需要使用;如需要增加新的交互,可协商添加。
+移动端集成H5原生交互协议
+序号	功能	交互名称	动作(需求)表述	交互集成建议	备注
+1
+
+
+	返回与关闭
+	webGoBack()	H5提供JS返回方法,供原生调用	建议集成
+	 点击原生返回按钮时调用,由h5执行返回逻辑
+		finishPage()	JS调用原生交互关闭当前页面	建议集成
+	
+2
+	原生导航控制
+	showNativeNav(show)
+	JS调用原生方法关闭/显示导航栏	按需	参数show:0隐藏,1显示
+3
+	下载文件	downloadFile(url)
+	JS调用原生方法下载文件;下载后自动预览	按需
+	对于H5不支持查看(或需要下载)的文件,需要通知原生进行下载查看。参数url:下载地址的全路径。
+4	扫描二维码	startScan() 	JS调用原生方法扫描二维码	按需	 
+5		setScanResult(String result)	原生将扫码结果传递给h5	按需	
+6	请求定位权限
+	requestLocPerm()
+	JS调用原生方法请求获取定位权限	按需	
+		getLocationCallback()	原生通知定位权限获取成功	按需	
+7	打电话	callPhone(tel)	JS调用原生方法拨打电话	按需