楼宇能效监测控制系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

959 lines
27 KiB

<template>
<div class="ai-report-container">
<!-- 顶部操作栏 -->
<div class="top-bar">
<div class="top-title"></div>
<div class="top-actions">
<el-button
:icon="isFullscreen ? 'el-icon-close' : 'el-icon-rank'"
circle
@click="toggleFullscreen"
title="全屏切换"
/>
<el-button
icon="el-icon-delete"
circle
@click="clearMessages"
title="清空聊天"
/>
</div>
</div>
<!-- 消息列表 -->
<div class="message-list" ref="messageList">
<!-- 欢迎消息 -->
<div
v-for="(message, index) in messages"
:key="index"
:class="['message-wrapper', message.role === 'user' ? 'user-message' : 'assistant-message']"
>
<!-- 头像 -->
<div class="avatar">
<svg-icon
v-if="message.role === 'user'"
icon-class="user"
class-name="avatar-icon"
/>
<svg-icon
v-else
icon-class="ai"
class-name="avatar-icon"
/>
</div>
<!-- 消息内容 -->
<div class="bubble">
<div class="message-content">
<div v-html="message.content"></div>
<!-- 快捷报表按钮 -->
<div
v-if="message.quickReports && message.quickReports.length > 0"
class="quick-reports"
>
<el-button
v-for="(report, rIndex) in message.quickReports"
:key="rIndex"
type="primary"
size="small"
@click="generateReport(report.type)"
>
{{ report.name }}
</el-button>
</div>
<!-- 加载状态 -->
<div v-if="message.loading" class="loading">
<i class="el-icon-loading"></i>
<span>正在生成报告...</span>
</div>
</div>
<!-- 报表预览区域(如果是 AI 消息且包含报表数据) -->
<div
v-if="message.reportData"
class="report-preview"
v-loading="message.reportLoading"
>
<!-- 动态组件渲染报表 -->
<component
:is="getReportComponent(message.reportType)"
:reportData="message.reportData"
:userName="userName"
/>
<!-- 操作按钮 -->
<div class="report-actions">
<el-button
type="success"
size="small"
@click="exportReport(message)"
:loading="message.exportLoading"
>
📥 导出报表
</el-button>
<el-button
type="primary"
size="small"
@click="printReport(message)"
>
🖨 打印报表
</el-button>
</div>
</div>
</div>
</div>
</div>
<!-- 底部输入区 -->
<div class="input-area">
<div class="quick-queries">
<el-tag
v-for="(query, index) in quickQueries"
:key="index"
size="small"
effect="plain"
@click="sendQuickQuery(query)"
>
{{ query }}
</el-tag>
</div>
<div class="input-row">
<textarea
v-model="inputMessage"
@keydown.enter.prevent="sendMessage"
placeholder="输入您的需求,例如:生成上个月的空调系统分析报告..."
rows="1"
ref="messageInput"
:disabled="isGenerating"
></textarea>
<button
@click="sendMessage"
:disabled="!inputMessage.trim() || isGenerating"
>
发送
</button>
</div>
</div>
<!-- 打印对话框 -->
<el-dialog
:visible.sync="dialogPrintVisible"
title="打印预览"
width="900px"
:before-close="handlePrintClose"
>
<div id="report-print-content" ref="reportPrint" class="report-print">
<!-- 打印内容将动态插入 -->
</div>
<el-row type="flex" justify="end" style="margin-top: 0.2rem">
<el-col :span="2">
<el-button type="info" @click="dialogPrintVisible = false">取消</el-button>
</el-col>
<el-col :span="2" style="margin-left: 60px">
<el-button type="success" @click="surePrint">确认打印</el-button>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script>
import { chat, downloadReport } from "@/api/ai";
import { format, getDay } from "@/utils/datetime";
// 导入报表组件
import {
ComprehensiveReport,
AirConditioningReport,
LightingReport,
PumpReport,
LightingAISaving,
AirConditioningAISaving
} from "./components";
export default {
name: "AiReport",
components: {
ComprehensiveReport,
AirConditioningReport,
LightingReport,
PumpReport,
LightingAISaving,
AirConditioningAISaving
},
data() {
return {
// 聊天相关
messages: [],
inputMessage: "",
isGenerating: false,
conversationId: "",
quickQueries: [
"生成项目综合分析报告",
"生成空调系统分析报告",
"生成照明系统分析报告",
"生成水泵系统分析报告",
"AI照明节能策略",
"AI空调节能策略"
],
// 打印相关
dialogPrintVisible: false,
userName: "",
operationDate: getDay(0),
// 报表类型映射
reportTypeMap: {
comprehensive: "ComprehensiveReport",
airConditioning: "AirConditioningReport",
lighting: "LightingReport",
pump: "PumpReport",
lightingAISaving: "LightingAISaving",
airConditioningAISaving: "AirConditioningAISaving"
},
reportTitleMap: {
comprehensive: "项目综合分析报告",
airConditioning: "空调制冷系统分析报告",
lighting: "照明系统分析报告",
pump: "水泵系统分析报告",
lightingAISaving: "AI照明节能策略",
airConditioningAISaving: "AI空调节能策略"
},
// 全屏相关
isFullscreen: false
};
},
mounted() {
this.userName = sessionStorage.getItem("userName");
// 初始化欢迎消息
this.addWelcomeMessage();
// 监听输入框高度变化
this.$nextTick(() => {
this.setupTextareaAutoResize();
});
// 监听全屏状态变化
this.addFullscreenListeners();
},
beforeDestroy() {
this.removeFullscreenListeners();
},
methods: {
// 添加全屏状态监听器
addFullscreenListeners() {
document.addEventListener('fullscreenchange', this.handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange);
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange);
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange);
},
// 移除全屏状态监听器
removeFullscreenListeners() {
document.removeEventListener('fullscreenchange', this.handleFullscreenChange);
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange);
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange);
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange);
},
// 处理全屏状态变化
handleFullscreenChange() {
this.isFullscreen = !!(document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement);
},
// 切换全屏
toggleFullscreen() {
const element = document.querySelector('.ai-report-container');
if (!this.isFullscreen) {
// 进入全屏
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
}
} else {
// 退出全屏
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
}
}
},
// 清空聊天记录
clearMessages() {
this.$confirm('确定要清空所有聊天记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.messages = [];
this.addWelcomeMessage();
this.$message.success('已清空聊天记录');
}).catch(() => {
// 取消操作
});
},
// 添加欢迎消息
addWelcomeMessage() {
this.messages.push({
role: "assistant",
content: "您好!我是您的智能报表助手。请告诉我您需要生成的报表类型。",
quickReports: [
{ name: "项目综合分析报告", type: "comprehensive" },
{ name: "空调系统分析报告", type: "airConditioning" },
{ name: "照明系统分析报告", type: "lighting" },
{ name: "水泵系统分析报告", type: "pump" },
{ name: "AI照明节能策略", type: "lightingAISaving" },
{ name: "AI空调节能策略", type: "airConditioningAISaving" }
]
});
},
// 发送消息
async sendMessage() {
if (!this.inputMessage.trim()) return;
const userMessage = {
role: "user",
content: this.inputMessage
};
this.messages.push(userMessage);
this.inputMessage = "";
this.isGenerating = true;
try {
const response = await chat(this.messages[this.messages.length - 1].content, this.conversationId);
this.conversationId = response.conversationId;
const aiMessage = {
role: "assistant",
content: response.content,
reportData: response.reportData,
reportType: response.reportType,
loading: false,
reportLoading: false,
exportLoading: false,
downloadUrl: response.downloadUrl // Store download URL for export
};
this.messages.push(aiMessage);
} catch (error) {
this.$message.error('生成报告失败,请稍后再试');
console.error('Chat API error:', error);
} finally {
this.isGenerating = false;
}
},
// 生成报表 - 直接渲染对应组件界面,不调用API
generateReport(type) {
// 添加用户消息
const userMessageContent = `生成${this.reportTitleMap[type]}`;
this.messages.push({
role: "user",
content: userMessageContent
});
// 立即添加AI响应 with proper mock data that matches component structure
const mockReportData = this.getMockReportData(type);
const aiMessage = {
role: "assistant",
content: `已为您生成${this.reportTitleMap[type]}`,
reportData: mockReportData,
reportType: type,
loading: false,
reportLoading: false,
exportLoading: false,
downloadUrl: null // No download URL for mock data
};
this.messages.push(aiMessage);
// Scroll to bottom
this.$nextTick(() => {
const messageList = this.$refs.messageList;
if (messageList) {
messageList.scrollTop = messageList.scrollHeight;
}
});
},
// 获取模拟报表数据 - minimal structure that works with all components
getMockReportData(type) {
const now = new Date();
const generateTime = now.toLocaleString('zh-CN');
// Common base structure that all components can handle safely
const baseData = {
title: this.reportTitleMap[type],
generateTime: generateTime,
summary: `这是${this.reportTitleMap[type]}的预览内容。实际数据将根据您的系统配置和历史数据生成。`,
projectInfo: {
description: "系统正在加载项目信息..."
},
equipmentOverview: {
description: "设备概述信息加载中..."
},
operationData: {
dataTable: [],
tableColumns: []
},
analysisSummary: {
points: [
"预览模式:此为示例内容",
"实际报告将包含详细的数据分析",
"点击导出可获取完整报告"
],
suggestions: []
}
};
// Add type-specific enhancements if needed
switch (type) {
case 'comprehensive':
return {
...baseData,
projectInfo: {
description: "项目包含空调制冷系统、照明系统、水泵系统等多个子系统。",
configTable: [
{ system: "空调制冷系统", capacity: "300kW", count: "15台" },
{ system: "照明系统", capacity: "80kW", count: "200盏" },
{ system: "水泵系统", capacity: "120kW", count: "8台" }
],
configTableColumns: [
{ prop: "system", label: "系统名称" },
{ prop: "capacity", label: "装机容量" },
{ prop: "count", label: "设备数量" }
]
},
equipmentOverview: {
description: "各系统设备运行状况良好,整体能效表现优秀。"
}
};
case 'airConditioning':
return {
...baseData,
title: "空调制冷系统分析报告",
summary: "空调制冷系统运行稳定,能效比达到行业优秀水平。",
projectInfo: {
description: "空调系统配置15台制冷主机,总制冷量3000kW。"
}
};
case 'lighting':
return {
...baseData,
title: "照明系统分析报告",
summary: "照明系统已完成LED改造,节电效果显著,节能率达到65%以上。",
projectInfo: {
description: "照明系统共200盏灯具,全部采用LED光源。"
}
};
case 'pump':
return {
...baseData,
title: "水泵系统分析报告",
summary: "水泵系统运行效率良好,吨水能耗指标优于设计值。",
projectInfo: {
description: "水泵系统配置8台水泵,总功率120kW。"
}
};
case 'lightingAISaving':
return {
...baseData,
title: "AI照明节能策略",
summary: "基于光环境、人员行为与使用场景数据分析,生成的场景化调光策略,支持办公区、走廊、会议室、车库等不同功能区域的照明节能优化。",
projectInfo: {
description: "AI照明节能策略已配置,包含亮度占比优化、感应触发时长设置和时段策略。"
}
};
case 'airConditioningAISaving':
return {
...baseData,
title: "AI空调节能策略",
summary: "结合冷水机组、水泵、空调末端等设备运行参数,室内外温湿度及CO₂数据,通过AI优化算法输出全局协同调节策略。",
projectInfo: {
description: "AI空调节能策略已配置,包含供水温度、水泵频率、风机转速等参数优化建议。"
}
};
default:
return baseData;
}
},
// 导出报表
async exportReport(message) {
if (!message.downloadUrl) {
this.$message.warning('该报表暂不支持导出');
return;
}
// Set loading state for this specific message
this.$set(message, 'exportLoading', true);
try {
const { blob, fileName } = await downloadReport(message.downloadUrl);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName || 'report.docx';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
this.$message.success('报表导出成功');
} catch (error) {
this.$message.error('报表导出失败,请稍后再试');
console.error('Export report error:', error);
} finally {
this.$set(message, 'exportLoading', false);
}
},
// 打印报表
printReport(message) {
if (!message.reportData) {
this.$message.warning('该报表暂不支持打印');
return;
}
this.currentPrintMessage = message;
this.dialogPrintVisible = true;
this.$nextTick(() => {
const reportElement = this.$refs.reportPrint;
if (reportElement) {
// Create a temporary div to render the component for printing
const tempDiv = document.createElement('div');
tempDiv.style.cssText = 'width: 100%; min-height: 500px;';
// Render the component content
const component = this.getReportComponent(message.reportType);
if (component) {
// For printing, we'll create a simplified version
const printContent = `
<div class="report-wrapper" style="padding: 20px; font-family: Arial, sans-serif;">
<h2 style="text-align: center; color: #333;">${this.reportTitleMap[message.reportType]}</h2>
<p style="text-align: center; color: #666;">生成时间: ${new Date().toLocaleString()}</p>
<p style="text-align: center; color: #666;">用户: ${this.userName}</p>
<div style="margin-top: 20px;">
<p>报表数据已加载,可在打印预览中查看完整内容。</p>
</div>
</div>
`;
tempDiv.innerHTML = printContent;
reportElement.innerHTML = '';
reportElement.appendChild(tempDiv);
}
}
});
},
// 处理打印对话框关闭
handlePrintClose(done) {
done();
},
// 确认打印
surePrint() {
if (this.$refs.reportPrint) {
// Create a new window for printing
const printWindow = window.open('', '_blank');
if (printWindow) {
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>打印预览</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
.report-wrapper { max-width: 800px; margin: 0 auto; }
</style>
</head>
<body>
${this.$refs.reportPrint.innerHTML}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
printWindow.print();
printWindow.close();
}
}
this.dialogPrintVisible = false;
},
// 发送快捷查询
sendQuickQuery(query) {
this.inputMessage = query;
this.sendMessage();
},
// 获取报表组件
getReportComponent(type) {
const componentMap = {
comprehensive: ComprehensiveReport,
airConditioning: AirConditioningReport,
lighting: LightingReport,
pump: PumpReport,
lightingAISaving: "LightingAISaving",
airConditioningAISaving: "AirConditioningAISaving"
};
return componentMap[type];
},
// 设置输入框自动调整高度
setupTextareaAutoResize() {
const textarea = this.$refs.messageInput;
if (!textarea) return;
// 初始高度设置
textarea.style.height = 'auto';
textarea.style.overflowY = 'auto';
// 监听输入事件
const resizeTextarea = () => {
const maxHeight = 120; // 4 行大约 120px (每行约 30px)
// 重置高度以获取正确的 scrollHeight
textarea.style.height = 'auto';
// 根据内容调整高度
const newHeight = textarea.scrollHeight;
if (newHeight <= maxHeight) {
textarea.style.height = newHeight + 'px';
textarea.style.overflowY = 'hidden';
} else {
textarea.style.height = maxHeight + 'px';
textarea.style.overflowY = 'auto';
}
};
textarea.addEventListener('input', resizeTextarea);
resizeTextarea(); // Initial resize
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/assets/styles/variables.scss";
.ai-report-container {
display: flex;
flex-direction: column;
height: calc(100vh - 130px);
margin: 0.2rem;
background: linear-gradient(135deg, #1a2a3a 0%, #2d3f52 100%);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
position: relative;
transition: all 0.3s ease;
// 全屏状态
&:fullscreen, &:-webkit-full-screen, &:-moz-full-screen, &:-ms-fullscreen {
width: 100%;
height: 100%;
margin: 0;
border-radius: 0;
}
// 顶部操作栏
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.15rem 0.2rem;
background: rgba(26, 42, 58, 0.95);
border-bottom: 1px solid rgba(10, 193, 199, 0.2);
backdrop-filter: blur(10px);
.top-title {
font-size: 0.16rem;
font-weight: 500;
color: #e0e6ed;
letter-spacing: 1px;
}
.top-actions {
display: flex;
gap: 0.08rem;
.el-button {
width: 0.36rem;
height: 0.36rem;
padding: 0;
border-radius: 50%;
background: rgba(10, 193, 199, 0.15);
border-color: rgba(10, 193, 199, 0.3);
color: #0ac1c7;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:hover {
background: rgba(10, 193, 199, 0.3);
color: #fff;
transform: scale(1.1);
}
i {
font-size: 0.18rem;
}
}
}
}
// 消息列表
.message-list {
flex: 1;
overflow-y: auto;
padding: 0.2rem;
min-height: 0;
scrollbar-width: thin;
scrollbar-color: #4a6fa5 #1a2a3a;
// 全屏状态下调整内边距
.ai-report-container:fullscreen & {
padding: 0.3rem;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #1a2a3a;
}
&::-webkit-scrollbar-thumb {
background: #4a6fa5;
border-radius: 3px;
}
.message-wrapper {
display: flex;
margin-bottom: 0.2rem;
.avatar {
width: 0.4rem;
height: 0.4rem;
margin-right: 0.1rem;
.avatar-icon {
width: 100%;
height: 100%;
}
}
.bubble {
background: #2d3f52;
border-radius: 8px;
padding: 0.1rem 0.2rem;
max-width: 80%;
word-wrap: break-word;
.message-content {
position: relative;
.quick-reports {
margin-top: 0.1rem;
.el-button {
margin-right: 0.05rem;
}
}
.loading {
display: flex;
align-items: center;
gap: 0.05rem;
i {
font-size: 0.18rem;
}
}
}
.report-preview {
margin-top: 0.1rem;
padding: 0.2rem;
background: #1a2a3a;
border-radius: 8px;
.report-actions {
display: flex;
justify-content: flex-end;
gap: 0.05rem;
.el-button {
margin-top: 0.1rem;
}
}
}
}
&.user-message {
justify-content: flex-end;
.bubble {
background: #0ac1c7;
color: #fff;
}
}
}
}
// 底部输入区
.input-area {
display: flex;
flex-direction: column;
padding: 0.15rem;
background: rgba(26, 42, 58, 0.95);
border-top: 1px solid rgba(10, 193, 199, 0.2);
flex-shrink: 0;
backdrop-filter: blur(10px);
// 全屏状态下调整内边距
.ai-report-container:fullscreen & {
padding: 0.2rem;
}
.quick-queries {
display: flex;
flex-wrap: wrap;
gap: 0.06rem;
margin-bottom: 0.1rem;
.el-tag {
cursor: pointer;
transition: all 0.2s ease;
border-radius: 0.15rem;
padding: 0.04rem 0.08rem;
font-size: 0.11rem;
background: rgba(10, 193, 199, 0.15);
border-color: rgba(10, 193, 199, 0.3);
color: #0ac1c7;
&:hover {
background: rgba(10, 193, 199, 0.3);
color: #fff;
}
}
}
.input-row {
display: flex;
align-items: flex-end;
gap: 0.1rem;
textarea {
flex: 1;
padding: 0.12rem 0.16rem;
border: 1px solid rgba(10, 193, 199, 0.3);
border-radius: 0.24rem;
resize: none;
outline: none;
font-size: 0.14rem;
line-height: 1.4;
min-height: 0.44rem;
max-height: 1.2rem; // 约 4 行高度
background: rgba(45, 63, 82, 0.6);
color: #e0e6ed;
transition: all 0.3s ease;
font-family: inherit;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #4a6fa5 rgba(45, 63, 82, 0.6);
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: rgba(45, 63, 82, 0.6);
}
&::-webkit-scrollbar-thumb {
background: #4a6fa5;
border-radius: 2px;
}
&::placeholder {
color: #6b8a9e;
}
&:focus {
border-color: #0ac1c7;
box-shadow: 0 0 10px rgba(10, 193, 199, 0.3);
background: rgba(45, 63, 82, 0.8);
}
}
button {
flex-shrink: 0;
padding: 0.12rem 0.24rem;
background: linear-gradient(135deg, #0ac1c7 0%, #09a8ae 100%);
color: white;
border: none;
border-radius: 0.24rem;
font-size: 0.14rem;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
height: 0.44rem;
align-self: center;
font-weight: 500;
box-shadow: 0 2px 8px rgba(10, 193, 199, 0.3);
&: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);
}
&:disabled {
background: #4a6fa5;
cursor: not-allowed;
opacity: 0.6;
box-shadow: none;
}
}
}
}
// 打印预览样式
.report-print {
min-height: 500px;
padding: 20px;
background: white;
border-radius: 8px;
}
}
</style>