From 2c8e50d2535417c8feffa1368d6d689f8e47cfa0 Mon Sep 17 00:00:00 2001 From: 25604 Date: Sat, 28 Feb 2026 16:36:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0ai=E5=8A=A9=E6=89=8B=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E5=AF=B9=E8=AF=9D=E7=94=9F=E6=88=90=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 207 +++++++++++- src/api/ai.js | 208 ++++++++++++ src/router/index.js | 8 + src/views/ai/index.vue | 472 +++++++++++++++++++++++++++ src/views/index.vue | 686 +++++++++++++++++++++------------------ src/views/index_ers.vue | 693 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1962 insertions(+), 312 deletions(-) create mode 100644 src/api/ai.js create mode 100644 src/views/ai/index.vue create mode 100644 src/views/index_ers.vue diff --git a/src/App.vue b/src/App.vue index cc9aa15..b267916 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,34 +2,87 @@
+
+ 节能岛AI助手 +
+ +
+ +
+
@@ -44,4 +157,80 @@ export default { #app .theme-picker { display: none; } + +.ai-robot-float { + position: fixed; + width: 60px; + height: 60px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); + cursor: move; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.3s ease, box-shadow 0.3s ease; + z-index: 9999; + user-select: none; +} + +.ai-robot-float:hover { + transform: scale(1.1); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); +} + +.ai-robot-float img { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; +} + + + diff --git a/src/api/ai.js b/src/api/ai.js new file mode 100644 index 0000000..935de63 --- /dev/null +++ b/src/api/ai.js @@ -0,0 +1,208 @@ +import request from "@/utils/request"; +import Cookies from 'js-cookie'; + +/** + * 创建AI报表任务 + * @param {string} prompt - 用户输入的提示词 + * @returns {Promise<{taskId: string}>} + */ +export function createReportTask(prompt) { + return request({ + url: "/ai/reports/async", + method: "post", + data: prompt, + headers: { + "Content-Type": "text/plain", + }, + }); +} + +/** + * 下载AI报表(带token认证) + * @param {string} downloadUrl - 下载URL + * @returns {Promise<{ blob: Blob, fileName: string }>} + */ +export function downloadReport(downloadUrl) { + const token = Cookies.get("Admin-Token"); + const baseURL = process.env.VUE_APP_BASE_API; + const url = downloadUrl.startsWith('http') ? downloadUrl : `${baseURL}${downloadUrl}`; + + return fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }).then(response => { + if (!response.ok) { + throw new Error(`Download failed with status: ${response.status}`); + } + + // 解析Content-Disposition头获取文件名 + let fileName = 'report.docx'; // 默认文件名 + const contentDisposition = response.headers.get('Content-Disposition'); + + if (contentDisposition) { + // 尝试解析 filename*=UTF-8'' 格式 + const utf8FilenameMatch = contentDisposition.match(/filename\*=UTF-8''(.+)/); + if (utf8FilenameMatch) { + try { + fileName = decodeURIComponent(utf8FilenameMatch[1]); + } catch (e) { + console.error('Failed to decode filename:', e); + } + } else { + // 尝试解析 filename="..." 或 filename=... 格式 + const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/); + if (filenameMatch) { + fileName = filenameMatch[1]; + } + } + } + + return response.blob().then(blob => ({ blob, fileName })); + }); +} + +/** + * 创建SSE连接(使用fetch实现,支持Authorization请求头) + * @param {string} taskId - 任务ID + * @param {Object} callbacks - 回调函数集合 + * @param {Function} callbacks.onReasoning - 推理内容回调 + * @param {Function} callbacks.onCompleted - 完成回调 + * @param {Function} callbacks.onFailed - 失败回调 + * @param {Function} callbacks.onError - 错误回调 + * @param {number} timeout - 超时时间(毫秒),默认30000(30秒) + * @returns {Object} SSE连接实例,包含close方法 + */ +export function createSSEConnection(taskId, callbacks, timeout = 30000) { + const { + onReasoning = () => {}, + onCompleted = () => {}, + onFailed = () => {}, + onError = () => {}, + } = callbacks; + + const token = Cookies.get("Admin-Token"); + const baseURL = process.env.VUE_APP_BASE_API; + const url = `${baseURL}/ai/reports/async/${taskId}/subscribe`; + + let controller = new AbortController(); + let timeoutId = null; + let isCompleted = false; + + const connect = async () => { + // 设置超时 + timeoutId = setTimeout(() => { + if (!isCompleted) { + controller.abort(); + const error = new Error(`SSE connection timeout after ${timeout}ms`); + onError(error); + } + }, timeout); + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'text/event-stream', + }, + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + let lastDataTime = Date.now(); + + // 数据超时检测(如果超过超时时间没有收到新数据) + const dataTimeoutId = setInterval(() => { + if (!isCompleted && Date.now() - lastDataTime > timeout) { + clearInterval(dataTimeoutId); + controller.abort(); + const error = new Error(`SSE no data received for ${timeout}ms`); + onError(error); + } + }, 5000); + + let currentEventType = null; + let currentData = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + lastDataTime = Date.now(); + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine.startsWith('event:')) { + currentEventType = trimmedLine.slice(6).trim(); + } else if (trimmedLine.startsWith('data:')) { + currentData = trimmedLine.slice(5).trim(); + if (currentEventType && currentData) { + if (currentEventType.toLowerCase() === 'reasoning') { + // reasoning 数据通常是文本内容,直接传递 + onReasoning(currentData); + } else if (currentEventType.toLowerCase() === 'completed') { + // COMPLETED 数据可能是JSON对象,尝试解析 + try { + const parsedData = JSON.parse(currentData); + onCompleted(parsedData); + } catch (e) { + onCompleted(currentData); + } + isCompleted = true; + clearTimeout(timeoutId); + clearInterval(dataTimeoutId); + } else if (currentEventType.toLowerCase() === 'failed') { + try { + const parsedData = JSON.parse(currentData); + onFailed(parsedData); + } catch (e) { + onFailed(currentData); + } + isCompleted = true; + clearTimeout(timeoutId); + clearInterval(dataTimeoutId); + } + } + } else if (trimmedLine === '') { + // 空行,事件分隔符,重置事件和数据 + currentEventType = null; + currentData = ''; + } + } + } + } catch (error) { + if (error.name === 'AbortError') { + if (!isCompleted) { + console.log('SSE connection closed'); + } + } else { + console.error('SSE error:', error); + onError(error); + } + } finally { + clearTimeout(timeoutId); + isCompleted = true; + } + }; + + connect(); + + return { + close: () => { + clearTimeout(timeoutId); + isCompleted = true; + controller.abort(); + }, + }; +} diff --git a/src/router/index.js b/src/router/index.js index 39af3ca..229dd4d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -96,6 +96,14 @@ export const constantRoutes = [ component: () => import("@/views/bigScreen/bigScreen"), meta: { title: "大屏总览", icon: "screen" }, }, + // AI助手 + { + path: "/ai", + name: "AI", + hidden: true, + component: () => import("@/views/ai/index"), + meta: { title: "AI助手", icon: "robot" }, + }, ]; // 动态路由,基于用户权限动态去加载 diff --git a/src/views/ai/index.vue b/src/views/ai/index.vue new file mode 100644 index 0000000..5e764e7 --- /dev/null +++ b/src/views/ai/index.vue @@ -0,0 +1,472 @@ + + + + + \ No newline at end of file diff --git a/src/views/index.vue b/src/views/index.vue index 550ab45..8832686 100644 --- a/src/views/index.vue +++ b/src/views/index.vue @@ -62,161 +62,90 @@
-
+
-
出水温度
+
项目概况
-
离心机高温出水温度
-
+
总耗电量(kwh)
+
{{ projectView.totalEle }}
-
中温换热出水温度
-
+
总热水补水(吨)
+
+ {{ projectView.totalWater }} +
-
低温1换热出水温度
-
+
总蒸汽流量(吨)
+
+ {{ projectView.totalGas }} +
-
低温2换热出水温度
-
+
总产冷量(kw)
+
+ {{ projectView.totalCold }} +
-
-
-
-
-
热量数据
-
-
-
生产积累热量
+
今年耗电量(kwh)
- {{ heatData.productionHeatSum }}kwh + {{ projectView.yearEle }}
-
散热累计热量
+
今年热水补水(吨)
- {{ heatData.dissipationHeatSum }}kwh + {{ projectView.yearWater }}
-
总热量回收
+
今年蒸汽流量(吨)
- {{ heatData.totalHeatRecoverySum }}kwh + {{ projectView.yearGas }}
-
热利用率
-
{{ heatData.heatUtilization }}%
+
今年产冷量(kw)
+
+ {{ projectView.yearCold }} +
+
+
+
冷源系统
+
+ +
-
+
-
系统数据
-
-
-
-
离心机入口温度
-
-
-
-
离心机出水温度
-
-
-
-
保障进水温度
-
-
+
冷源能耗
-
- - - + +
+
+
+
热水系统
+
-
-
-
-
阀门开度
-
- +
+
+
风柜系统
-
-
-
-
热回收数据
-
-
-
-
瞬时热量:
-
{{heatRecoveryData.instantaneousHeatSum}}kw
-
-
-
日累计热量:
-
{{heatRecoveryData.dailyAccumulatedHeat}}kwh
-
-
-
累计热量:
-
{{heatRecoveryData.accumulatedHeatSum}}kwh
-
-
-
-
-
-
应用测数据
-
-
-
-
瞬时热量:
-
{{applicationData.instantaneousHeatSum}}kw
-
-
-
日累计热量:
-
{{applicationData.dailyAccumulatedHeat}}kwh
-
-
-
累计热量:
-
{{applicationData.accumulatedHeatSum}}kwh
-
-
-
+ +
+
+
+
温度系统
+
+ +