6 changed files with 1962 additions and 312 deletions
@ -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(); |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,472 @@ |
|||||||
|
<template> |
||||||
|
<div class="chat-container"> |
||||||
|
<!-- 消息列表 --> |
||||||
|
<div class="message-list" ref="messageList"> |
||||||
|
<div |
||||||
|
v-for="(msg, index) in messages" |
||||||
|
:key="index" |
||||||
|
class="message-wrapper" |
||||||
|
:class="{ 'user-message': msg.role === 'user', 'assistant-message': msg.role === 'assistant' }" |
||||||
|
> |
||||||
|
<!-- 头像(可自定义图标) --> |
||||||
|
<div class="avatar"> |
||||||
|
<img :src="logo" alt="AI" @error="handleImageError" /> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 消息内容 --> |
||||||
|
<div class="bubble"> |
||||||
|
<div class="message-content"> |
||||||
|
<!-- 如果是JSON格式,用代码块显示 --> |
||||||
|
<pre v-if="isJsonContent(msg.content)" class="json-content"><code>{{ formatJson(msg.content) }}</code></pre> |
||||||
|
<!-- 否则用普通文本显示 --> |
||||||
|
<div v-else v-html="formattedContent(msg)"></div> |
||||||
|
</div> |
||||||
|
<!-- 如果是AI消息且包含下载链接,显示下载按钮 --> |
||||||
|
<div v-if="msg.role === 'assistant' && msg.downloadUrl" class="download-area"> |
||||||
|
<a @click.prevent="handleDownload(msg.downloadUrl, index)" href="#" class="download-btn"> |
||||||
|
{{ msg.downloading ? '下载中...' : '📥 下载报表' }} |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 底部输入区 --> |
||||||
|
<div class="input-area"> |
||||||
|
<textarea |
||||||
|
v-model="userInput" |
||||||
|
@keydown.enter.prevent="sendMessage" |
||||||
|
placeholder="输入你的需求,例如:生成昨天机房的能效报表,PDF格式..." |
||||||
|
rows="1" |
||||||
|
></textarea> |
||||||
|
<button @click="sendMessage" :disabled="!userInput.trim() || loading">发送</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import logo from "@/assets/logo/logo.png"; |
||||||
|
import { createReportTask, createSSEConnection, downloadReport } from "@/api/ai"; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'ChatReport', |
||||||
|
data() { |
||||||
|
return { |
||||||
|
userInput: '', |
||||||
|
messages: [ |
||||||
|
{ |
||||||
|
role: 'assistant', |
||||||
|
content: '您好!我是AI助手,可以帮助您生成机房能效报表。请告诉我您需要什么帮助?', |
||||||
|
downloadUrl: '' |
||||||
|
} |
||||||
|
], // { role: 'user'/'assistant', content: '', downloadUrl: '' } |
||||||
|
loading: false, |
||||||
|
currentEventSource: null, // 当前SSE连接 |
||||||
|
logo: logo, |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.$nextTick(() => { |
||||||
|
this.scrollToBottom(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 格式化消息内容(简单将文本中的换行转为<br>) |
||||||
|
formattedContent() { |
||||||
|
return (msg) => { |
||||||
|
if (!msg.content) return ''; |
||||||
|
return msg.content.replace(/\n/g, '<br>'); |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
// 判断内容是否为JSON格式 |
||||||
|
isJsonContent() { |
||||||
|
return (content) => { |
||||||
|
if (!content) return false; |
||||||
|
content = content.trim(); |
||||||
|
return content.startsWith('{') && content.endsWith('}'); |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
// 格式化JSON |
||||||
|
formatJson() { |
||||||
|
return (content) => { |
||||||
|
try { |
||||||
|
const parsed = JSON.parse(content); |
||||||
|
return JSON.stringify(parsed, null, 2); |
||||||
|
} catch (e) { |
||||||
|
return content; |
||||||
|
} |
||||||
|
}; |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 发送消息 |
||||||
|
sendMessage() { |
||||||
|
const text = this.userInput.trim(); |
||||||
|
if (!text || this.loading) return; |
||||||
|
|
||||||
|
// 关闭之前的SSE连接 |
||||||
|
if (this.currentEventSource) { |
||||||
|
this.currentEventSource.close(); |
||||||
|
this.currentEventSource = null; |
||||||
|
} |
||||||
|
|
||||||
|
// 添加用户消息 |
||||||
|
this.messages.push({ role: 'user', content: text }); |
||||||
|
// 添加一条空的AI消息占位,等待流式填充 |
||||||
|
const aiMsgIndex = this.messages.length; |
||||||
|
this.messages.push({ role: 'assistant', content: '', downloadUrl: '' }); |
||||||
|
this.scrollToBottom(); |
||||||
|
|
||||||
|
// 清空输入框 |
||||||
|
this.userInput = ''; |
||||||
|
this.loading = true; |
||||||
|
|
||||||
|
// 调用后端创建任务 |
||||||
|
createReportTask(text) |
||||||
|
.then(data => { |
||||||
|
const taskId = data.taskId; |
||||||
|
// 建立SSE连接 |
||||||
|
this.connectSSE(taskId, aiMsgIndex); |
||||||
|
}) |
||||||
|
.catch(err => { |
||||||
|
this.handleError('创建任务失败:' + (err.message || err), aiMsgIndex); |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
// 关闭弹窗 |
||||||
|
closeDialog() { |
||||||
|
this.$emit('close'); |
||||||
|
}, |
||||||
|
|
||||||
|
// 图片加载错误处理 |
||||||
|
handleImageError() { |
||||||
|
console.log('图片加载失败,使用默认样式'); |
||||||
|
}, |
||||||
|
|
||||||
|
// 下载报表 |
||||||
|
async handleDownload(downloadUrl, msgIndex) { |
||||||
|
try { |
||||||
|
this.$set(this.messages[msgIndex], 'downloading', true); |
||||||
|
|
||||||
|
const { blob, fileName } = await downloadReport(downloadUrl); |
||||||
|
|
||||||
|
// 创建下载链接并触发下载 |
||||||
|
const url = window.URL.createObjectURL(blob); |
||||||
|
const a = document.createElement('a'); |
||||||
|
a.href = url; |
||||||
|
a.download = fileName; |
||||||
|
document.body.appendChild(a); |
||||||
|
a.click(); |
||||||
|
document.body.removeChild(a); |
||||||
|
window.URL.revokeObjectURL(url); |
||||||
|
|
||||||
|
this.$set(this.messages[msgIndex], 'downloading', false); |
||||||
|
} catch (error) { |
||||||
|
console.error('Download failed:', error); |
||||||
|
this.$message.error('下载失败:' + (error.message || error)); |
||||||
|
this.$set(this.messages[msgIndex], 'downloading', false); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
// 连接SSE |
||||||
|
connectSSE(taskId, msgIndex) { |
||||||
|
const eventSource = createSSEConnection(taskId, { |
||||||
|
onReasoning: (data) => { |
||||||
|
// 收集所有数据,等待完成后统一实现打字机效果 |
||||||
|
// 先存储完整内容 |
||||||
|
if (!this.messages[msgIndex].fullContent) { |
||||||
|
this.$set(this.messages[msgIndex], 'fullContent', data); |
||||||
|
} else { |
||||||
|
this.$set(this.messages[msgIndex], 'fullContent', this.messages[msgIndex].fullContent + data); |
||||||
|
} |
||||||
|
}, |
||||||
|
onCompleted: (downloadUrl) => { |
||||||
|
// 开始打字机效果 |
||||||
|
const fullContent = this.messages[msgIndex].fullContent || ''; |
||||||
|
this.typewriterEffect(msgIndex, fullContent); |
||||||
|
|
||||||
|
// 设置下载链接 |
||||||
|
this.$set(this.messages[msgIndex], 'downloadUrl', downloadUrl); |
||||||
|
this.loading = false; |
||||||
|
eventSource.close(); |
||||||
|
this.currentEventSource = null; |
||||||
|
}, |
||||||
|
onFailed: (errorMsg) => { |
||||||
|
this.handleError('生成失败:' + errorMsg, msgIndex); |
||||||
|
eventSource.close(); |
||||||
|
this.currentEventSource = null; |
||||||
|
}, |
||||||
|
onError: (error) => { |
||||||
|
console.error('SSE error', error); |
||||||
|
// 如果连接意外关闭且未收到完成事件,显示错误 |
||||||
|
if (this.loading) { |
||||||
|
this.handleError('连接中断,请重试', msgIndex); |
||||||
|
} |
||||||
|
eventSource.close(); |
||||||
|
this.currentEventSource = null; |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
this.currentEventSource = eventSource; |
||||||
|
}, |
||||||
|
|
||||||
|
// 打字机效果 |
||||||
|
typewriterEffect(msgIndex, text, index = 0) { |
||||||
|
if (index >= text.length) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.$set(this.messages[msgIndex], 'content', text.substring(0, index + 1)); |
||||||
|
this.scrollToBottom(); |
||||||
|
|
||||||
|
// 1ms后显示下一个字符 |
||||||
|
setTimeout(() => { |
||||||
|
this.typewriterEffect(msgIndex, text, index + 1); |
||||||
|
}, 1); |
||||||
|
}, |
||||||
|
|
||||||
|
// 处理错误 |
||||||
|
handleError(errorText, msgIndex) { |
||||||
|
// 更新AI消息内容为错误提示 |
||||||
|
this.$set(this.messages[msgIndex], 'content', errorText); |
||||||
|
this.loading = false; |
||||||
|
if (this.currentEventSource) { |
||||||
|
this.currentEventSource.close(); |
||||||
|
this.currentEventSource = null; |
||||||
|
} |
||||||
|
this.scrollToBottom(); |
||||||
|
}, |
||||||
|
|
||||||
|
// 滚动到底部 |
||||||
|
scrollToBottom() { |
||||||
|
this.$nextTick(() => { |
||||||
|
const container = this.$refs.messageList; |
||||||
|
if (container) { |
||||||
|
container.scrollTop = container.scrollHeight; |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
}, |
||||||
|
beforeDestroy() { |
||||||
|
// 组件销毁前关闭SSE连接 |
||||||
|
if (this.currentEventSource) { |
||||||
|
this.currentEventSource.close(); |
||||||
|
this.currentEventSource = null; |
||||||
|
} |
||||||
|
}, |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.chat-container { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
height: 100%; |
||||||
|
min-height: 400px; |
||||||
|
background: linear-gradient(135deg, #1a2a3a 0%, #2d3f52 100%); |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
.message-list { |
||||||
|
flex: 1; |
||||||
|
overflow-y: auto; |
||||||
|
padding: 15px; |
||||||
|
min-height: 0; |
||||||
|
scrollbar-width: thin; |
||||||
|
scrollbar-color: #4a6fa5 #1a2a3a; |
||||||
|
} |
||||||
|
|
||||||
|
.message-list::-webkit-scrollbar { |
||||||
|
width: 6px; |
||||||
|
} |
||||||
|
|
||||||
|
.message-list::-webkit-scrollbar-track { |
||||||
|
background: #1a2a3a; |
||||||
|
} |
||||||
|
|
||||||
|
.message-list::-webkit-scrollbar-thumb { |
||||||
|
background: #4a6fa5; |
||||||
|
border-radius: 3px; |
||||||
|
} |
||||||
|
|
||||||
|
.message-wrapper { |
||||||
|
display: flex; |
||||||
|
margin-bottom: 15px; |
||||||
|
} |
||||||
|
|
||||||
|
.user-message { |
||||||
|
flex-direction: row-reverse; |
||||||
|
} |
||||||
|
|
||||||
|
.avatar { |
||||||
|
width: 44px; |
||||||
|
height: 44px; |
||||||
|
margin: 0 10px; |
||||||
|
flex-shrink: 0; |
||||||
|
min-width: 44px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
|
||||||
|
.avatar img { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
border-radius: 50%; |
||||||
|
object-fit: contain; |
||||||
|
background: linear-gradient(135deg, #0ac1c7 0%, #09a8ae 100%); |
||||||
|
border: 2px solid #0ac1c7; |
||||||
|
box-shadow: 0 0 15px rgba(10, 193, 199, 0.6), 0 0 30px rgba(10, 193, 199, 0.3); |
||||||
|
image-rendering: -webkit-optimize-contrast; |
||||||
|
image-rendering: crisp-edges; |
||||||
|
image-rendering: pixelated; |
||||||
|
padding: 8px; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
/* 用户头像样式 */ |
||||||
|
.user-message .avatar img { |
||||||
|
background: linear-gradient(135deg, #4a6fa5 0%, #3d5a80 100%); |
||||||
|
border-color: #4a6fa5; |
||||||
|
box-shadow: 0 0 15px rgba(74, 111, 165, 0.6), 0 0 30px rgba(74, 111, 165, 0.3); |
||||||
|
} |
||||||
|
|
||||||
|
.bubble { |
||||||
|
max-width: 70%; |
||||||
|
padding: 12px 16px; |
||||||
|
border-radius: 16px; |
||||||
|
background: rgba(45, 63, 82, 0.8); |
||||||
|
border: 1px solid rgba(10, 193, 199, 0.3); |
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); |
||||||
|
word-wrap: break-word; |
||||||
|
font-size: 14px; |
||||||
|
backdrop-filter: blur(10px); |
||||||
|
} |
||||||
|
|
||||||
|
.user-message .bubble { |
||||||
|
background: linear-gradient(135deg, #0ac1c7 0%, #09a8ae 100%); |
||||||
|
border-color: #0ac1c7; |
||||||
|
} |
||||||
|
|
||||||
|
.message-content { |
||||||
|
line-height: 1.6; |
||||||
|
color: #e0e6ed; |
||||||
|
word-wrap: break-word; |
||||||
|
} |
||||||
|
|
||||||
|
.user-message .message-content { |
||||||
|
color: #ffffff; |
||||||
|
} |
||||||
|
|
||||||
|
/* JSON内容样式 */ |
||||||
|
.json-content { |
||||||
|
background: rgba(0, 0, 0, 0.4); |
||||||
|
border-radius: 8px; |
||||||
|
padding: 12px; |
||||||
|
margin: 0; |
||||||
|
overflow-x: auto; |
||||||
|
font-family: 'Courier New', Courier, monospace; |
||||||
|
font-size: 13px; |
||||||
|
line-height: 1.5; |
||||||
|
color: #e0e6ed; |
||||||
|
white-space: pre-wrap; |
||||||
|
word-wrap: break-word; |
||||||
|
border: 1px solid rgba(10, 193, 199, 0.2); |
||||||
|
} |
||||||
|
|
||||||
|
.json-content code { |
||||||
|
color: #e0e6ed; |
||||||
|
} |
||||||
|
|
||||||
|
.download-area { |
||||||
|
margin-top: 10px; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.download-btn { |
||||||
|
display: inline-block; |
||||||
|
padding: 8px 20px; |
||||||
|
background: linear-gradient(135deg, #0ac1c7 0%, #09a8ae 100%); |
||||||
|
color: white; |
||||||
|
text-decoration: none; |
||||||
|
border-radius: 20px; |
||||||
|
font-size: 13px; |
||||||
|
transition: all 0.3s ease; |
||||||
|
border: 1px solid #0ac1c7; |
||||||
|
box-shadow: 0 2px 8px rgba(10, 193, 199, 0.3); |
||||||
|
} |
||||||
|
|
||||||
|
.download-btn:hover { |
||||||
|
background: linear-gradient(135deg, #09a8ae 0%, #088a90 100%); |
||||||
|
transform: translateY(-2px); |
||||||
|
box-shadow: 0 4px 12px rgba(10, 193, 199, 0.5); |
||||||
|
} |
||||||
|
|
||||||
|
.input-area { |
||||||
|
display: flex; |
||||||
|
padding: 15px; |
||||||
|
background: rgba(26, 42, 58, 0.95); |
||||||
|
border-top: 1px solid rgba(10, 193, 199, 0.2); |
||||||
|
flex-shrink: 0; |
||||||
|
height: auto; |
||||||
|
backdrop-filter: blur(10px); |
||||||
|
} |
||||||
|
|
||||||
|
.input-area textarea { |
||||||
|
flex: 1; |
||||||
|
padding: 12px 16px; |
||||||
|
border: 1px solid rgba(10, 193, 199, 0.3); |
||||||
|
border-radius: 24px; |
||||||
|
resize: none; |
||||||
|
outline: none; |
||||||
|
font-size: 14px; |
||||||
|
line-height: 1.4; |
||||||
|
max-height: 80px; |
||||||
|
min-height: 44px; |
||||||
|
background: rgba(45, 63, 82, 0.6); |
||||||
|
color: #e0e6ed; |
||||||
|
transition: all 0.3s ease; |
||||||
|
} |
||||||
|
|
||||||
|
.input-area textarea::placeholder { |
||||||
|
color: #6b8a9e; |
||||||
|
} |
||||||
|
|
||||||
|
.input-area textarea:focus { |
||||||
|
border-color: #0ac1c7; |
||||||
|
box-shadow: 0 0 10px rgba(10, 193, 199, 0.3); |
||||||
|
background: rgba(45, 63, 82, 0.8); |
||||||
|
} |
||||||
|
|
||||||
|
.input-area button { |
||||||
|
margin-left: 12px; |
||||||
|
padding: 0 24px; |
||||||
|
background: linear-gradient(135deg, #0ac1c7 0%, #09a8ae 100%); |
||||||
|
color: white; |
||||||
|
border: none; |
||||||
|
border-radius: 24px; |
||||||
|
font-size: 14px; |
||||||
|
cursor: pointer; |
||||||
|
transition: all 0.3s ease; |
||||||
|
white-space: nowrap; |
||||||
|
height: 44px; |
||||||
|
align-self: center; |
||||||
|
font-weight: 500; |
||||||
|
box-shadow: 0 2px 8px rgba(10, 193, 199, 0.3); |
||||||
|
} |
||||||
|
|
||||||
|
.input-area button:hover:not(:disabled) { |
||||||
|
background: linear-gradient(135deg, #09a8ae 0%, #088a90 100%); |
||||||
|
transform: translateY(-2px); |
||||||
|
box-shadow: 0 4px 12px rgba(10, 193, 199, 0.5); |
||||||
|
} |
||||||
|
|
||||||
|
.input-area button:disabled { |
||||||
|
background: #4a6fa5; |
||||||
|
cursor: not-allowed; |
||||||
|
opacity: 0.6; |
||||||
|
box-shadow: none; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,693 @@ |
|||||||
|
<template> |
||||||
|
<div class="app-container"> |
||||||
|
<div class="special-div"> |
||||||
|
<div class="special-top"> |
||||||
|
<div class="special-title">项目简介</div> |
||||||
|
</div> |
||||||
|
<div class="project-all"> |
||||||
|
<img |
||||||
|
class="project-img" |
||||||
|
:src="imgUrl + projectObj.logo" |
||||||
|
@click="showDialog" |
||||||
|
alt="Base64 Image" |
||||||
|
/> |
||||||
|
<div class="project-li"> |
||||||
|
<div class="list-con"> |
||||||
|
<div class="project-left"> |
||||||
|
<img |
||||||
|
class="left-icon" |
||||||
|
src="../assets/images/project-icon1.png" |
||||||
|
alt="" |
||||||
|
/> |
||||||
|
<div class="project-name">项目名称</div> |
||||||
|
</div> |
||||||
|
<div class="project-right">{{ projectObj.proName }}</div> |
||||||
|
</div> |
||||||
|
<div class="list-con"> |
||||||
|
<div class="project-left"> |
||||||
|
<img |
||||||
|
class="left-icon" |
||||||
|
src="../assets/images/project-icon2.png" |
||||||
|
alt="" |
||||||
|
/> |
||||||
|
<div class="project-name">建筑面积</div> |
||||||
|
</div> |
||||||
|
<div class="project-right">{{ projectObj.buildingArea }}m³</div> |
||||||
|
</div> |
||||||
|
<div class="list-con"> |
||||||
|
<div class="project-left"> |
||||||
|
<img |
||||||
|
class="left-icon" |
||||||
|
src="../assets/images/project-icon3.png" |
||||||
|
alt="" |
||||||
|
/> |
||||||
|
<div class="project-name">运营地址</div> |
||||||
|
</div> |
||||||
|
<div class="project-right">{{ projectObj.proAddr }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="project-li"> |
||||||
|
<div class="list-con"> |
||||||
|
<div class="project-left"> |
||||||
|
<img |
||||||
|
class="left-icon" |
||||||
|
src="../assets/images/project-icon3.png" |
||||||
|
alt="" |
||||||
|
/> |
||||||
|
<div class="project-name">项目运行开始时间</div> |
||||||
|
</div> |
||||||
|
<div class="project-right">{{ projectObj.operateStartTime }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="project-data"> |
||||||
|
<div class="special-div" style="width: 49.4%"> |
||||||
|
<div class="special-top"> |
||||||
|
<div class="special-title">出水温度</div> |
||||||
|
</div> |
||||||
|
<div class="overview"> |
||||||
|
<div class="overview-li" @click="goEnergy"> |
||||||
|
<div>离心机高温出水温度</div> |
||||||
|
<div |
||||||
|
class="overview-details" |
||||||
|
v-html="getTempDisplay('离心机高温出水温度', outWaterTemperature)" |
||||||
|
></div> |
||||||
|
</div> |
||||||
|
<div class="overview-li" @click="goEnergy"> |
||||||
|
<div>中温换热出水温度</div> |
||||||
|
<div |
||||||
|
class="overview-details" |
||||||
|
v-html="getTempDisplay('中温换热出水温度', outWaterTemperature)" |
||||||
|
></div> |
||||||
|
</div> |
||||||
|
<div class="overview-li" @click="goEnergy"> |
||||||
|
<div>低温1换热出水温度</div> |
||||||
|
<div |
||||||
|
class="overview-details" |
||||||
|
v-html="getTempDisplay('低温1换热出水温度', outWaterTemperature)" |
||||||
|
></div> |
||||||
|
</div> |
||||||
|
<div class="overview-li" @click="goEnergy"> |
||||||
|
<div>低温2换热出水温度</div> |
||||||
|
<div |
||||||
|
class="overview-details" |
||||||
|
v-html="getTempDisplay('低温2换热出水温度', outWaterTemperature)" |
||||||
|
></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="special-div" style="width: 49.4%"> |
||||||
|
<div class="special-top"> |
||||||
|
<div class="special-title">热量数据</div> |
||||||
|
</div> |
||||||
|
<div class="overview"> |
||||||
|
<div class="overview-li" @click="goEnergy"> |
||||||
|
<div>生产积累热量</div> |
||||||
|
<div class="overview-details"> |
||||||
|
{{ heatData.productionHeatSum }}kwh |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="overview-li" @click="goEnergy"> |
||||||
|
<div>散热累计热量</div> |
||||||
|
<div class="overview-details"> |
||||||
|
{{ heatData.dissipationHeatSum }}kwh |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="overview-li" @click="goEnergy"> |
||||||
|
<div>总热量回收</div> |
||||||
|
<div class="overview-details"> |
||||||
|
{{ heatData.totalHeatRecoverySum }}kwh |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="overview-li" @click="goEnergy"> |
||||||
|
<div>热利用率</div> |
||||||
|
<div class="overview-details">{{ heatData.heatUtilization }}%</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="project-bie"> |
||||||
|
<div class="special-div" style="width: 49.4%"> |
||||||
|
<div class="special-top"> |
||||||
|
<div class="special-title">系统数据</div> |
||||||
|
</div> |
||||||
|
<div class="hot-tem"> |
||||||
|
<div class="tem-li"> |
||||||
|
<div class="tem-title">离心机入口温度</div> |
||||||
|
<div |
||||||
|
class="tem-detail" |
||||||
|
v-html="getTempDisplay('离心机入口温度', systemData)" |
||||||
|
></div> |
||||||
|
</div> |
||||||
|
<div class="tem-li"> |
||||||
|
<div class="tem-title">离心机出水温度</div> |
||||||
|
<div |
||||||
|
class="tem-detail" |
||||||
|
v-html="getTempDisplay('离心机出水温度', systemData)" |
||||||
|
></div> |
||||||
|
</div> |
||||||
|
<div class="tem-li"> |
||||||
|
<div class="tem-title">保障进水温度</div> |
||||||
|
<div |
||||||
|
class="tem-detail" |
||||||
|
v-html="getTempDisplay('保障进水温度', systemData)" |
||||||
|
></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- <div class="pressure"> |
||||||
|
<view-cold-sys |
||||||
|
:subData="getDeviceData('离心机进水压力')" |
||||||
|
:title="'离心机进水压力'" |
||||||
|
></view-cold-sys> |
||||||
|
<view-cold-sys |
||||||
|
:subData="getDeviceData('离心机出水压力')" |
||||||
|
:title="'离心机出水压力'" |
||||||
|
></view-cold-sys> |
||||||
|
<view-cold-sys |
||||||
|
:subData="getDeviceData('离心机压差')" |
||||||
|
:title="'离心机压差'" |
||||||
|
></view-cold-sys> |
||||||
|
</div> --> |
||||||
|
</div> |
||||||
|
<div class="bir-right"> |
||||||
|
<div class="special-div"> |
||||||
|
<div class="special-top"> |
||||||
|
<div class="special-title">阀门开度</div> |
||||||
|
</div> |
||||||
|
<hot-water :subData="valveData"></hot-water> |
||||||
|
</div> |
||||||
|
<div class="later-data"> |
||||||
|
<div class="special-div"> |
||||||
|
<div class="special-top"> |
||||||
|
<div class="special-title">热回收数据</div> |
||||||
|
</div> |
||||||
|
<div class="hot-data"> |
||||||
|
<div class="hot-li"> |
||||||
|
<div class="hot-title">瞬时热量:</div> |
||||||
|
<div class="hot-detail">{{heatRecoveryData.instantaneousHeatSum}}kw</div> |
||||||
|
</div> |
||||||
|
<div class="hot-li"> |
||||||
|
<div class="hot-title">日累计热量:</div> |
||||||
|
<div class="hot-detail">{{heatRecoveryData.dailyAccumulatedHeat}}kwh</div> |
||||||
|
</div> |
||||||
|
<div class="hot-li"> |
||||||
|
<div class="hot-title">累计热量:</div> |
||||||
|
<div class="hot-detail">{{heatRecoveryData.accumulatedHeatSum}}kwh</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="special-div"> |
||||||
|
<div class="special-top"> |
||||||
|
<div class="special-title">应用测数据</div> |
||||||
|
</div> |
||||||
|
<div class="hot-data"> |
||||||
|
<div class="hot-li"> |
||||||
|
<div class="hot-title">瞬时热量:</div> |
||||||
|
<div class="hot-detail">{{applicationData.instantaneousHeatSum}}kw</div> |
||||||
|
</div> |
||||||
|
<div class="hot-li"> |
||||||
|
<div class="hot-title">日累计热量:</div> |
||||||
|
<div class="hot-detail">{{applicationData.dailyAccumulatedHeat}}kwh</div> |
||||||
|
</div> |
||||||
|
<div class="hot-li"> |
||||||
|
<div class="hot-title">累计热量:</div> |
||||||
|
<div class="hot-detail">{{applicationData.accumulatedHeatSum}}kwh</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<el-dialog |
||||||
|
:visible.sync="dialogVisible" |
||||||
|
title="上传图片" |
||||||
|
width="500px" |
||||||
|
@close="addExpenseClose" |
||||||
|
> |
||||||
|
<!-- 使用 el-upload 组件 --> |
||||||
|
<el-upload |
||||||
|
class="upload-demo" |
||||||
|
ref="uploadComponent" |
||||||
|
action="#" |
||||||
|
list-type="picture-card" |
||||||
|
:on-preview="handlePictureCardPreview" |
||||||
|
:on-remove="handleRemove" |
||||||
|
:before-upload="beforeUpload" |
||||||
|
:limit="1" |
||||||
|
:on-exceed="handleExceed" |
||||||
|
:on-change="handleFileChange" |
||||||
|
accept="image/png, image/jpeg" |
||||||
|
:http-request="customHttpRequest" |
||||||
|
> |
||||||
|
<i class="el-icon-plus"></i> |
||||||
|
</el-upload> |
||||||
|
<div slot="footer" class="dialog-footer"> |
||||||
|
<!-- 取消按钮 --> |
||||||
|
<el-button @click="dialogVisible = false">取消</el-button> |
||||||
|
<!-- 确定按钮 --> |
||||||
|
<el-button type="primary" @click="uploadFile">确定</el-button> |
||||||
|
</div> |
||||||
|
</el-dialog> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import { imgUrl } from "@/utils/global"; |
||||||
|
import { introduction, changeLogo, ersDatas } from "@/api/index"; |
||||||
|
// import ViewColdSys from "./components/viewColdSys.vue"; |
||||||
|
import HotWater from "./components/hotWater.vue"; |
||||||
|
|
||||||
|
export default { |
||||||
|
components: { |
||||||
|
// ViewColdSys, |
||||||
|
HotWater, |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
imgUrl: "", |
||||||
|
projectObj: { |
||||||
|
proName: "", |
||||||
|
logo: "", |
||||||
|
proAddr: "", |
||||||
|
buildingArea: "", |
||||||
|
operateStartTime: "", |
||||||
|
}, |
||||||
|
heatData: {}, //热量数据 |
||||||
|
systemData: [], //系统数据 |
||||||
|
valveData: [], //阀门开度 |
||||||
|
outWaterTemperature: [], //出水温度 |
||||||
|
applicationData: {}, //应用测数据 |
||||||
|
heatRecoveryData: {}, //热回收数据 |
||||||
|
coldSys: [], |
||||||
|
hotWaterSys: [], |
||||||
|
dialogVisible: false, |
||||||
|
selectedFile: null, |
||||||
|
viewImageUrl: "", |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.getProject(); |
||||||
|
this.getErsData(); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 全屏操作 |
||||||
|
requestFullscreen() { |
||||||
|
const element = document.documentElement; |
||||||
|
console.log("全屏了吗"); |
||||||
|
if (element.requestFullscreen) { |
||||||
|
element.requestFullscreen(); |
||||||
|
} else if (element.mozRequestFullScreen) { |
||||||
|
// Firefox |
||||||
|
element.mozRequestFullScreen(); |
||||||
|
} else if (element.webkitRequestFullscreen) { |
||||||
|
// Chrome, Safari and Opera |
||||||
|
element.webkitRequestFullscreen(); |
||||||
|
} else if (element.msRequestFullscreen) { |
||||||
|
// IE/Edge |
||||||
|
element.msRequestFullscreen(); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 获取项目简介数据 |
||||||
|
getProject() { |
||||||
|
this.imgUrl = imgUrl; |
||||||
|
introduction().then((res) => { |
||||||
|
console.log("项目资料", res); |
||||||
|
if (res.code == 200) { |
||||||
|
this.projectObj = res.rows[0]; |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
// 显示上传图片弹框 |
||||||
|
showDialog() { |
||||||
|
this.dialogVisible = true; |
||||||
|
}, |
||||||
|
// 处理文件选择事件 |
||||||
|
handleFileChange(file, fileList) { |
||||||
|
this.selectedFile = file.raw; |
||||||
|
}, |
||||||
|
// 关闭弹框 |
||||||
|
addExpenseClose() { |
||||||
|
// 清除上传文件 |
||||||
|
this.$refs.uploadComponent.clearFiles(); |
||||||
|
this.viewImageUrl = ""; |
||||||
|
}, |
||||||
|
// 重置 |
||||||
|
reset() { |
||||||
|
this.dialogData = {}; |
||||||
|
// 清除上传文件 |
||||||
|
this.$refs.uploadComponent.clearFiles(); |
||||||
|
this.viewImageUrl = ""; |
||||||
|
}, |
||||||
|
// 图片移除 |
||||||
|
handleRemove(file, fileList) { |
||||||
|
console.log(file, fileList); |
||||||
|
this.selectedFile = {}; |
||||||
|
}, |
||||||
|
// 图片预览 |
||||||
|
handlePictureCardPreview(file) { |
||||||
|
this.dialogVisible = true; |
||||||
|
this.viewImageUrl = file.url; |
||||||
|
}, |
||||||
|
// 上传成功 |
||||||
|
handleUploadSuccess(response, file, fileList) {}, |
||||||
|
// 处理文件 |
||||||
|
handleFileChange(file, fileList) { |
||||||
|
console.log("file", file); |
||||||
|
this.selectedFile = file.raw; |
||||||
|
}, |
||||||
|
processFile(file) { |
||||||
|
// 在这里你可以对 file.raw 进行任何本地处理 |
||||||
|
console.log("处理的文件", file); |
||||||
|
// 例如:读取文件内容 |
||||||
|
const reader = new FileReader(); |
||||||
|
reader.onload = (event) => { |
||||||
|
console.log("文件内容", event.target.result); |
||||||
|
}; |
||||||
|
reader.readAsDataURL(file); |
||||||
|
}, |
||||||
|
customHttpRequest(options) { |
||||||
|
// 自定义上传逻辑,不进行实际的网络请求 |
||||||
|
const file = options.file; |
||||||
|
}, |
||||||
|
beforeUpload(file) { |
||||||
|
const isJpgOrPng = |
||||||
|
file.type === "image/jpeg" || file.type === "image/png"; |
||||||
|
if (!isJpgOrPng) { |
||||||
|
this.$message.error("上传图片只能是 JPG 或 PNG 格式!"); |
||||||
|
} |
||||||
|
const isLt2M = file.size / 1024 / 1024 < 2; |
||||||
|
if (!isLt2M) { |
||||||
|
this.$message.error("上传图片大小不能超过 2MB!"); |
||||||
|
} |
||||||
|
return isJpgOrPng && isLt2M; |
||||||
|
}, |
||||||
|
handleExceed(files, fileList) { |
||||||
|
this.$message.warning(`上传文件数量超过限制, 当前限制为 1 张`); |
||||||
|
}, |
||||||
|
// 上传 |
||||||
|
uploadFile() { |
||||||
|
console.log("这里进行请求"); |
||||||
|
let data = { |
||||||
|
proId: 0, |
||||||
|
logo: this.selectedFile, |
||||||
|
}; |
||||||
|
changeLogo(data).then((res) => { |
||||||
|
if (res.code == 200) { |
||||||
|
this.$modal.msgSuccess("上传成功"); |
||||||
|
this.getProject(); |
||||||
|
this.dialogVisible = false; |
||||||
|
} else { |
||||||
|
this.$message.error("上传失败"); |
||||||
|
this.dialogVisible = false; |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
// 查询数据 |
||||||
|
getErsData() { |
||||||
|
let data = { |
||||||
|
systemType: "7", |
||||||
|
}; |
||||||
|
ersDatas(data).then((res) => { |
||||||
|
if (res.code == 200) { |
||||||
|
console.log("首页返回数据----------", res.rows[0]); |
||||||
|
this.heatData = res.rows[0].heatData; |
||||||
|
this.systemData = res.rows[0].systemData; |
||||||
|
this.valveData = res.rows[0].valveData; |
||||||
|
this.outWaterTemperature = res.rows[0].outWaterTemperature; |
||||||
|
this.applicationData = res.rows[0].applicationData; |
||||||
|
this.heatRecoveryData = res.rows[0].heatRecoveryData; |
||||||
|
// 计算并添加离心机压差 |
||||||
|
this.addCentrifugePressureDiff(); |
||||||
|
console.log("deviceName-1", this.systemData); |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
// 计算离心机压差并添加到systemData |
||||||
|
addCentrifugePressureDiff() { |
||||||
|
// 查找进水压力和出水压力对象 |
||||||
|
const inletPressure = this.systemData.find( |
||||||
|
(item) => item.deviceTypeName === "离心机进水压力" |
||||||
|
); |
||||||
|
|
||||||
|
const outletPressure = this.systemData.find( |
||||||
|
(item) => item.deviceTypeName === "离心机出水压力" |
||||||
|
); |
||||||
|
|
||||||
|
// 如果两个对象都存在 |
||||||
|
if (inletPressure && outletPressure) { |
||||||
|
// 计算压差(出水压力 - 进水压力) |
||||||
|
const pressureDiff = |
||||||
|
parseFloat(outletPressure.curValue) - |
||||||
|
parseFloat(inletPressure.curValue); |
||||||
|
|
||||||
|
// 判断状态:如果进水或出水压力任一状态为1,则压差状态为1(异常) |
||||||
|
const status = |
||||||
|
inletPressure.status === 1 || outletPressure.status === 1 ? 1 : 0; |
||||||
|
|
||||||
|
// 创建压差对象 |
||||||
|
const pressureDiffObj = { |
||||||
|
paramType: "14", // 可以设置为压差类型,如果没有特定值可以设一个默认 |
||||||
|
curTime: null, |
||||||
|
orderNum: this.systemData.length + 1, // 顺序号设为数组长度+1 |
||||||
|
deviceTypeName: "离心机压差", |
||||||
|
otherName: "压差计算值", |
||||||
|
deviceName: "离心机压差", |
||||||
|
curValue: parseFloat(pressureDiff.toFixed(2)), // 保留两位小数 |
||||||
|
status: status, |
||||||
|
}; |
||||||
|
|
||||||
|
// 将压差对象添加到systemData中 |
||||||
|
this.systemData.push(pressureDiffObj); |
||||||
|
console.log("添加离心机压差对象:", pressureDiffObj); |
||||||
|
} else { |
||||||
|
console.warn("找不到进水压力或出水压力数据"); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 处理系统数据 |
||||||
|
getTempDisplay(deviceName, data) { |
||||||
|
// console.log("this.systemData",this.systemData) |
||||||
|
// console.log("deviceName",deviceName) |
||||||
|
const item = data.find((item) => item.deviceTypeName === deviceName); |
||||||
|
// console.log("item", item); |
||||||
|
|
||||||
|
if (!item) return "--℃"; |
||||||
|
|
||||||
|
if (item.status === 1) { |
||||||
|
return '<span style="color: red">异常</span>'; |
||||||
|
} |
||||||
|
return `${item.curValue}℃`; |
||||||
|
}, |
||||||
|
// 处理系统数据-仪表盘 |
||||||
|
getDeviceData(deviceTypeName) { |
||||||
|
return ( |
||||||
|
this.systemData.find( |
||||||
|
(item) => item.deviceTypeName === deviceTypeName |
||||||
|
) || {} |
||||||
|
); |
||||||
|
}, |
||||||
|
goEnergy() { |
||||||
|
// this.$router.push("/comprehensiveEnergy/systemEnergy"); |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.project-all { |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: flex-start; |
||||||
|
padding: 0.35rem; |
||||||
|
.project-img { |
||||||
|
width: 1.4rem; |
||||||
|
height: 1.4rem; |
||||||
|
border-radius: 0.1rem; |
||||||
|
border: solid 1px #0163a8; |
||||||
|
margin-right: 0.4rem; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
.project-li { |
||||||
|
width: calc((100% - 0.7rem) / 2); |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
.list-con { |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
.project-left { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
.left-icon { |
||||||
|
width: 0.2rem; |
||||||
|
height: 0.2rem; |
||||||
|
} |
||||||
|
.project-name { |
||||||
|
font-family: SourceHanSansCN-Regular; |
||||||
|
font-size: 0.18rem; |
||||||
|
line-height: 0.4rem; |
||||||
|
color: #ffffff; |
||||||
|
opacity: 0.8; |
||||||
|
margin-left: 0.15rem; |
||||||
|
} |
||||||
|
} |
||||||
|
.project-right { |
||||||
|
font-family: SourceHanSansCN-Regular; |
||||||
|
font-size: 0.18rem; |
||||||
|
line-height: 0.4rem; |
||||||
|
color: #ffffff; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.project-li:nth-last-child(1) { |
||||||
|
margin-left: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
.project-data { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: stretch; |
||||||
|
justify-content: space-between; |
||||||
|
margin: 16px 0; |
||||||
|
.overview { |
||||||
|
width: 100%; |
||||||
|
padding: 30px 20px 5px 20px; |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
flex-wrap: wrap; |
||||||
|
.overview-li { |
||||||
|
cursor: pointer; |
||||||
|
width: 49%; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
background-position: center center; |
||||||
|
margin-bottom: 15px; |
||||||
|
font-family: SourceHanSansCN-Regular; |
||||||
|
font-size: 15px; |
||||||
|
color: #ffffff; |
||||||
|
/* 从 rgba(41, 128, 185, 0.8) 渐变到透明 */ |
||||||
|
background: linear-gradient( |
||||||
|
to bottom, |
||||||
|
rgba(31, 100, 146, 0.6) 0%, |
||||||
|
rgba(33, 65, 87, 0.3) 50%, |
||||||
|
rgba(40, 62, 77, 0) 100% |
||||||
|
); |
||||||
|
border-radius: 8px; |
||||||
|
padding: 5px; |
||||||
|
color: white; |
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
||||||
|
.overview-details { |
||||||
|
font-family: DIN-Bold; |
||||||
|
font-size: 20px; |
||||||
|
color: #15e1fd; |
||||||
|
margin-top: 5px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.chartsDiv { |
||||||
|
width: 100%; |
||||||
|
height: 4rem; |
||||||
|
} |
||||||
|
.project-bie { |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: stretch; |
||||||
|
justify-content: space-between; |
||||||
|
.pressure { |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
margin-top: 25px; |
||||||
|
} |
||||||
|
.hot-tem { |
||||||
|
width: 100%; |
||||||
|
padding: 0 20px; |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: stretch; |
||||||
|
flex-wrap: wrap; |
||||||
|
margin-top: 25px; |
||||||
|
.tem-li { |
||||||
|
background-color: rgba(93, 125, 143, 0.5); |
||||||
|
width: 32%; |
||||||
|
padding: 10px; |
||||||
|
border-radius: 10px; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
justify-content: space-between; |
||||||
|
margin-bottom: 20px; |
||||||
|
.tem-title { |
||||||
|
font-size: 15px; |
||||||
|
font-weight: 600; |
||||||
|
color: #ffffff; |
||||||
|
} |
||||||
|
.tem-detail { |
||||||
|
margin-top: 8px; |
||||||
|
font-size: 20px; |
||||||
|
font-weight: 600; |
||||||
|
color: #f89615; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.bir-right { |
||||||
|
width: 49.4%; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
justify-content: space-between; |
||||||
|
.special-div { |
||||||
|
width: 100%; |
||||||
|
margin-bottom: 20px; |
||||||
|
} |
||||||
|
.later-data { |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: space-between; |
||||||
|
.special-div { |
||||||
|
width: 49%; |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
.hot-data { |
||||||
|
width: 100%; |
||||||
|
padding: 25px 25px 5px 25px; |
||||||
|
.hot-li { |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
margin-bottom: 20px; |
||||||
|
.hot-title { |
||||||
|
font-size: 15px; |
||||||
|
font-weight: 600; |
||||||
|
color: #ffffff; |
||||||
|
} |
||||||
|
.hot-detail { |
||||||
|
font-size: 20px; |
||||||
|
font-weight: 600; |
||||||
|
color: #f5f127; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
@media (max-width: 1485px) { |
||||||
|
} |
||||||
|
// 媒体查询,适配大于2000px分辨率的大屏样式 |
||||||
|
@media (min-width: 2000px) { |
||||||
|
} |
||||||
|
</style> |
||||||
Loading…
Reference in new issue