12 changed files with 4181 additions and 369 deletions
@ -0,0 +1,381 @@ |
|||||||
|
# AI 智能报表组件化开发文档 |
||||||
|
|
||||||
|
## 📦 组件架构 |
||||||
|
|
||||||
|
采用组件化设计思路,将每个报表类型独立封装为可复用的 Vue 组件,提高代码的可维护性和可扩展性。 |
||||||
|
|
||||||
|
### 目录结构 |
||||||
|
|
||||||
|
``` |
||||||
|
src/views/ai/ |
||||||
|
├── components/ # 报表组件目录 |
||||||
|
│ ├── ComprehensiveReport.vue # 项目综合分析报告组件 |
||||||
|
│ ├── AirConditioningReport.vue # 空调制冷系统分析报告组件 |
||||||
|
│ ├── LightingReport.vue # 照明系统分析报告组件 |
||||||
|
│ ├── PumpReport.vue # 水泵系统分析报告组件 |
||||||
|
│ └── index.js # 组件统一导出文件 |
||||||
|
├── index.vue # AI报表主页面 |
||||||
|
└── README.md # 功能说明文档 |
||||||
|
``` |
||||||
|
|
||||||
|
## 🎯 组件说明 |
||||||
|
|
||||||
|
### 1. ComprehensiveReport (项目综合分析报告) |
||||||
|
|
||||||
|
**功能描述**: 综合展示项目所有子系统的设备配置、运行数据和能效表现 |
||||||
|
|
||||||
|
**包含图表**: |
||||||
|
- 各系统能耗占比饼图 |
||||||
|
- 系统能效趋势折线图 |
||||||
|
- 主要设备用电对比柱状图 |
||||||
|
|
||||||
|
**使用示例**: |
||||||
|
```vue |
||||||
|
<ComprehensiveReport |
||||||
|
:reportData="reportData" |
||||||
|
:userName="userName" |
||||||
|
/> |
||||||
|
``` |
||||||
|
|
||||||
|
### 2. AirConditioningReport (空调制冷系统分析报告) |
||||||
|
|
||||||
|
**功能描述**: 针对空调制冷系统的专业分析报告 |
||||||
|
|
||||||
|
**包含图表**: |
||||||
|
- 制冷设备组用电占比饼图 |
||||||
|
- 制冷系统能效趋势图 |
||||||
|
- 每日用电量与制冷量对比图 |
||||||
|
|
||||||
|
**使用示例**: |
||||||
|
```vue |
||||||
|
<AirConditioningReport |
||||||
|
:reportData="reportData" |
||||||
|
:userName="userName" |
||||||
|
/> |
||||||
|
``` |
||||||
|
|
||||||
|
### 3. LightingReport (照明系统分析报告) |
||||||
|
|
||||||
|
**功能描述**: 照明系统节能效果分析报告 |
||||||
|
|
||||||
|
**包含图表**: |
||||||
|
- 节电量趋势图 (双维度:节能数 + 节电量) |
||||||
|
- 节能率趋势图 (含环比/同比分析) |
||||||
|
|
||||||
|
**使用示例**: |
||||||
|
```vue |
||||||
|
<LightingReport |
||||||
|
:reportData="reportData" |
||||||
|
:userName="userName" |
||||||
|
/> |
||||||
|
``` |
||||||
|
|
||||||
|
### 4. PumpReport (水泵系统分析报告) |
||||||
|
|
||||||
|
**功能描述**: 水泵系统运行效率和能耗分析报告 |
||||||
|
|
||||||
|
**包含图表**: |
||||||
|
- 水泵组用电占比饼图 |
||||||
|
- 分区吨水能耗趋势图 |
||||||
|
- 每日供水量与用电量对比图 |
||||||
|
|
||||||
|
**使用示例**: |
||||||
|
```vue |
||||||
|
<PumpReport |
||||||
|
:reportData="reportData" |
||||||
|
:userName="userName" |
||||||
|
/> |
||||||
|
``` |
||||||
|
|
||||||
|
## 🔧 组件通用接口 |
||||||
|
|
||||||
|
### Props |
||||||
|
|
||||||
|
所有报表组件都接收以下 props: |
||||||
|
|
||||||
|
| Prop 名称 | 类型 | 必填 | 说明 | |
||||||
|
|--------------|--------|------|------------------------| |
||||||
|
| `reportData` | Object | 是 | 报表数据对象 | |
||||||
|
| `userName` | String | 否 | 操作员姓名,用于报表底部 | |
||||||
|
|
||||||
|
### reportData 数据结构 |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ |
||||||
|
title: String, // 报表标题 |
||||||
|
generateTime: String, // 生成时间 |
||||||
|
summary: String, // 报告摘要 |
||||||
|
projectInfo: { // 项目情况 |
||||||
|
description: String, // 项目描述 |
||||||
|
configTable: Array, // 配置表格数据 |
||||||
|
configTableColumns: Array // 表格列定义 |
||||||
|
}, |
||||||
|
equipmentOverview: { // 设备概述 |
||||||
|
description: String // 设备描述 |
||||||
|
}, |
||||||
|
operationData: { // 运行数据 |
||||||
|
dataTable: Array, // 数据表格 |
||||||
|
tableColumns: Array // 表格列定义 |
||||||
|
}, |
||||||
|
analysisSummary: { // 分析总结 |
||||||
|
points: Array, // 分析要点 |
||||||
|
suggestions: Array // 优化建议 |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## 🎨 样式特点 |
||||||
|
|
||||||
|
### 统一风格 |
||||||
|
- 所有组件使用相同的样式基类 `.report-wrapper` |
||||||
|
- 保持与 Element UI 一致的设计语言 |
||||||
|
- 响应式布局,自适应不同屏幕尺寸 |
||||||
|
|
||||||
|
### 图表样式 |
||||||
|
- 图表容器固定高度 300px |
||||||
|
- 使用 grid 布局自动排列 |
||||||
|
- 最小宽度 400px,保证图表清晰度 |
||||||
|
|
||||||
|
### 配色方案 |
||||||
|
- 主色调:#409EFF (Element UI 主题色) |
||||||
|
- 成功色:#67C23A |
||||||
|
- 警告色:#E6A23C |
||||||
|
- 危险色:#F56C6C |
||||||
|
|
||||||
|
## 📊 ECharts 图表配置 |
||||||
|
|
||||||
|
### 图表自适应 |
||||||
|
每个组件都在 `mounted` 钩子中初始化图表,在 `beforeDestroy` 钩子中销毁实例: |
||||||
|
|
||||||
|
```javascript |
||||||
|
mounted() { |
||||||
|
this.$nextTick(() => { |
||||||
|
this.renderCharts(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
beforeDestroy() { |
||||||
|
this.chartInstances.forEach((chart) => { |
||||||
|
if (chart) chart.dispose(); |
||||||
|
}); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 响应式配置 |
||||||
|
图表标题字体大小根据容器宽度动态计算: |
||||||
|
|
||||||
|
```javascript |
||||||
|
const width = this.$refs.chartBox?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
``` |
||||||
|
|
||||||
|
## 🔄 动态组件加载 |
||||||
|
|
||||||
|
主页面使用 Vue 的动态组件特性: |
||||||
|
|
||||||
|
```vue |
||||||
|
<component |
||||||
|
:is="reportComponent" |
||||||
|
:reportData="reportData" |
||||||
|
:userName="userName" |
||||||
|
/> |
||||||
|
``` |
||||||
|
|
||||||
|
通过 `reportComponent` 计算属性动态返回组件名称: |
||||||
|
|
||||||
|
```javascript |
||||||
|
computed: { |
||||||
|
reportComponent() { |
||||||
|
if (!this.currentReportType) return null; |
||||||
|
return this.reportTypeMap[this.currentReportType]; |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## 🚀 使用方式 |
||||||
|
|
||||||
|
### 1. 导入组件 |
||||||
|
|
||||||
|
```javascript |
||||||
|
import { |
||||||
|
ComprehensiveReport, |
||||||
|
AirConditioningReport, |
||||||
|
LightingReport, |
||||||
|
PumpReport |
||||||
|
} from "./components"; |
||||||
|
|
||||||
|
export default { |
||||||
|
components: { |
||||||
|
ComprehensiveReport, |
||||||
|
AirConditioningReport, |
||||||
|
LightingReport, |
||||||
|
PumpReport |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 2. 动态渲染 |
||||||
|
|
||||||
|
```javascript |
||||||
|
data() { |
||||||
|
return { |
||||||
|
currentReportType: 'comprehensive', // comprehensive | airConditioning | lighting | pump |
||||||
|
reportData: {} |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 3. 切换报表类型 |
||||||
|
|
||||||
|
```javascript |
||||||
|
methods: { |
||||||
|
generateReport(reportType) { |
||||||
|
this.currentReportType = reportType; |
||||||
|
// 获取报表数据... |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## 📝 扩展指南 |
||||||
|
|
||||||
|
### 添加新报表类型 |
||||||
|
|
||||||
|
#### 步骤 1: 创建新组件 |
||||||
|
|
||||||
|
在 `components` 目录下创建新的 `.vue` 文件: |
||||||
|
|
||||||
|
```vue |
||||||
|
<template> |
||||||
|
<div class="custom-report report-wrapper"> |
||||||
|
<!-- 报表内容 --> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "CustomReport", |
||||||
|
props: { |
||||||
|
reportData: Object, |
||||||
|
userName: String |
||||||
|
} |
||||||
|
// ... 其他逻辑 |
||||||
|
} |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
#### 步骤 2: 导出组件 |
||||||
|
|
||||||
|
在 `index.js` 中添加导出: |
||||||
|
|
||||||
|
```javascript |
||||||
|
export { default as CustomReport } from './CustomReport.vue' |
||||||
|
``` |
||||||
|
|
||||||
|
#### 步骤 3: 注册组件 |
||||||
|
|
||||||
|
在主页面中导入并使用: |
||||||
|
|
||||||
|
```javascript |
||||||
|
import { CustomReport } from "./components"; |
||||||
|
|
||||||
|
export default { |
||||||
|
components: { |
||||||
|
CustomReport |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
reportTypeMap: { |
||||||
|
custom: "CustomReport" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 自定义图表配置 |
||||||
|
|
||||||
|
修改组件中的 `getChartOption` 方法: |
||||||
|
|
||||||
|
```javascript |
||||||
|
methods: { |
||||||
|
getCustomChartOption() { |
||||||
|
return { |
||||||
|
title: { text: '自定义图表' }, |
||||||
|
xAxis: { /* ... */ }, |
||||||
|
yAxis: { /* ... */ }, |
||||||
|
series: [/* ... */] |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## ✅ 优势总结 |
||||||
|
|
||||||
|
### 1. **模块化** |
||||||
|
- 每个报表独立封装,互不干扰 |
||||||
|
- 便于团队协作开发 |
||||||
|
- 易于单元测试 |
||||||
|
|
||||||
|
### 2. **可复用性** |
||||||
|
- 组件可在不同页面复用 |
||||||
|
- 支持动态加载和切换 |
||||||
|
- 统一的接口规范 |
||||||
|
|
||||||
|
### 3. **可维护性** |
||||||
|
- 代码结构清晰,职责单一 |
||||||
|
- 修改某个报表不影响其他报表 |
||||||
|
- 便于版本管理和回滚 |
||||||
|
|
||||||
|
### 4. **性能优化** |
||||||
|
- 按需加载组件 |
||||||
|
- 图表实例自动销毁,避免内存泄漏 |
||||||
|
- 支持懒加载和虚拟滚动 |
||||||
|
|
||||||
|
### 5. **易扩展** |
||||||
|
- 新增报表类型只需添加新组件 |
||||||
|
- 支持插件化开发 |
||||||
|
- 向后兼容性好 |
||||||
|
|
||||||
|
## 🔍 调试技巧 |
||||||
|
|
||||||
|
### 查看组件实例 |
||||||
|
|
||||||
|
```javascript |
||||||
|
// 在控制台查看当前激活的报表组件 |
||||||
|
console.log(this.$children.find( |
||||||
|
child => child.$options.name === this.reportComponent |
||||||
|
)); |
||||||
|
``` |
||||||
|
|
||||||
|
### 检查图表渲染 |
||||||
|
|
||||||
|
```javascript |
||||||
|
// 验证图表是否正确初始化 |
||||||
|
this.chartInstances.forEach((chart, index) => { |
||||||
|
console.log(`Chart ${index}:`, chart.isDisposed()); |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
### 性能监控 |
||||||
|
|
||||||
|
```javascript |
||||||
|
// 记录组件渲染时间 |
||||||
|
const start = performance.now(); |
||||||
|
this.$nextTick(() => { |
||||||
|
const end = performance.now(); |
||||||
|
console.log(`Render time: ${end - start}ms`); |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
## 📖 最佳实践 |
||||||
|
|
||||||
|
1. **Props 验证**: 始终为 props 定义类型和默认值 |
||||||
|
2. **事件命名**: 使用 kebab-case 命名自定义事件 |
||||||
|
3. **样式隔离**: 使用 `scoped` 避免样式污染 |
||||||
|
4. **资源清理**: 及时销毁定时器、图表实例等资源 |
||||||
|
5. **错误处理**: 添加完善的错误捕获和用户提示 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
**版本**: v2.0.0 |
||||||
|
**更新日期**: 2026-03-06 |
||||||
|
**文档状态**: 已完成 ✅ |
||||||
@ -0,0 +1,536 @@ |
|||||||
|
<template> |
||||||
|
<div class="air-conditioning-report report-wrapper"> |
||||||
|
<!-- 报表标题 --> |
||||||
|
<div class="report-title-section"> |
||||||
|
<h1>{{ reportData.title }}</h1> |
||||||
|
<p class="report-time">生成时间:{{ reportData.generateTime }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表摘要 --> |
||||||
|
<div class="report-summary"> |
||||||
|
<h3>报告摘要</h3> |
||||||
|
<p>{{ reportData.summary }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表主体内容 --> |
||||||
|
<div class="report-main"> |
||||||
|
<!-- 项目情况 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>一、项目情况</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<p>{{ reportData.projectInfo.description }}</p> |
||||||
|
<el-table |
||||||
|
v-if="reportData.projectInfo.configTable" |
||||||
|
:data="reportData.projectInfo.configTable" |
||||||
|
border |
||||||
|
size="small" |
||||||
|
> |
||||||
|
<el-table-column |
||||||
|
v-for="col in reportData.projectInfo.configTableColumns" |
||||||
|
:key="col.prop" |
||||||
|
:prop="col.prop" |
||||||
|
:label="col.label" |
||||||
|
/> |
||||||
|
</el-table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 设备概述 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>二、设备概述</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<p>{{ reportData.equipmentOverview.description }}</p> |
||||||
|
<div class="charts-container"> |
||||||
|
<!-- 设备用电占比饼图 --> |
||||||
|
<div ref="devicePieChart" class="chart-box"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 运行数据 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>三、运行数据</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<div class="charts-container"> |
||||||
|
<!-- 能效趋势图 --> |
||||||
|
<div ref="efficiencyTrendChart" class="chart-box"></div> |
||||||
|
<!-- 每日用电趋势图 --> |
||||||
|
<div ref="dailyEnergyChart" class="chart-box"></div> |
||||||
|
</div> |
||||||
|
<el-table |
||||||
|
v-if="reportData.operationData.dataTable" |
||||||
|
:data="reportData.operationData.dataTable" |
||||||
|
border |
||||||
|
stripe |
||||||
|
size="small" |
||||||
|
> |
||||||
|
<el-table-column |
||||||
|
v-for="col in reportData.operationData.tableColumns" |
||||||
|
:key="col.prop" |
||||||
|
:prop="col.prop" |
||||||
|
:label="col.label" |
||||||
|
/> |
||||||
|
</el-table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 分析总结 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>四、分析总结</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<div class="analysis-points"> |
||||||
|
<div |
||||||
|
v-for="(point, index) in reportData.analysisSummary.points" |
||||||
|
:key="index" |
||||||
|
class="analysis-point" |
||||||
|
> |
||||||
|
<i class="el-icon-caret-right"></i> |
||||||
|
<span>{{ point }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-if="reportData.analysisSummary.suggestions" class="suggestions"> |
||||||
|
<h4>优化建议:</h4> |
||||||
|
<ul> |
||||||
|
<li |
||||||
|
v-for="(suggestion, index) in reportData.analysisSummary.suggestions" |
||||||
|
:key="index" |
||||||
|
> |
||||||
|
{{ suggestion }} |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表底部信息 --> |
||||||
|
<div class="report-footer"> |
||||||
|
<div class="footer-info"> |
||||||
|
<span>操作员:{{ userName }}</span> |
||||||
|
<span>生成日期:{{ operationDate }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import * as echarts from "echarts"; |
||||||
|
import { format, getDay } from "@/utils/datetime"; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "AirConditioningReport", |
||||||
|
props: { |
||||||
|
reportData: { |
||||||
|
type: Object, |
||||||
|
default: () => ({}) |
||||||
|
}, |
||||||
|
userName: { |
||||||
|
type: String, |
||||||
|
default: "" |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
operationDate: getDay(0), |
||||||
|
chartInstances: [] |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.$nextTick(() => { |
||||||
|
this.renderCharts(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
beforeDestroy() { |
||||||
|
this.chartInstances.forEach((chart) => { |
||||||
|
if (chart) chart.dispose(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
renderCharts() { |
||||||
|
// 销毁旧图表 |
||||||
|
this.chartInstances.forEach((chart) => { |
||||||
|
if (chart) chart.dispose(); |
||||||
|
}); |
||||||
|
this.chartInstances = []; |
||||||
|
|
||||||
|
// 渲染设备用电占比饼图 |
||||||
|
if (this.$refs.devicePieChart) { |
||||||
|
const chart = echarts.init(this.$refs.devicePieChart); |
||||||
|
chart.setOption(this.getDevicePieOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
|
||||||
|
// 渲染能效趋势图 |
||||||
|
if (this.$refs.efficiencyTrendChart) { |
||||||
|
const chart = echarts.init(this.$refs.efficiencyTrendChart); |
||||||
|
chart.setOption(this.getEfficiencyTrendOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
|
||||||
|
// 渲染每日用电趋势图 |
||||||
|
if (this.$refs.dailyEnergyChart) { |
||||||
|
const chart = echarts.init(this.$refs.dailyEnergyChart); |
||||||
|
chart.setOption(this.getDailyEnergyOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
// 设备用电占比饼图配置 |
||||||
|
getDevicePieOption() { |
||||||
|
const width = this.$refs.devicePieChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "制冷设备组用电占比", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "item", |
||||||
|
formatter: "{a} <br/>{b}: {c} kWh ({d}%)" |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
orient: "vertical", |
||||||
|
left: "left", |
||||||
|
top: "middle" |
||||||
|
}, |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "用电占比", |
||||||
|
type: "pie", |
||||||
|
radius: ["40%", "70%"], |
||||||
|
center: ["50%", "50%"], |
||||||
|
data: [ |
||||||
|
{ value: 1850, name: "冷水机组" }, |
||||||
|
{ value: 920, name: "冷冻泵" }, |
||||||
|
{ value: 780, name: "冷却泵" }, |
||||||
|
{ value: 450, name: "冷却塔风机" } |
||||||
|
], |
||||||
|
emphasis: { |
||||||
|
itemStyle: { |
||||||
|
shadowBlur: 10, |
||||||
|
shadowOffsetX: 0, |
||||||
|
shadowColor: "rgba(0, 0, 0, 0.5)" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
// 能效趋势图配置 |
||||||
|
getEfficiencyTrendOption() { |
||||||
|
const width = this.$refs.efficiencyTrendChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "制冷系统能效趋势", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "axis" |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
data: ["机组 COP", "系统 COP", "目标 COP"], |
||||||
|
top: "bottom" |
||||||
|
}, |
||||||
|
grid: { |
||||||
|
left: "3%", |
||||||
|
right: "4%", |
||||||
|
bottom: "15%", |
||||||
|
containLabel: true |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
type: "category", |
||||||
|
boundaryGap: false, |
||||||
|
data: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00", "23:59"] |
||||||
|
}, |
||||||
|
yAxis: { |
||||||
|
type: "value", |
||||||
|
name: "COP 值" |
||||||
|
}, |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "机组 COP", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [5.8, 6.2, 6.5, 6.8, 6.6, 6.3, 6.0], |
||||||
|
lineStyle: { |
||||||
|
width: 3, |
||||||
|
color: "#409EFF" |
||||||
|
}, |
||||||
|
areaStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "rgba(64, 158, 255, 0.5)" }, |
||||||
|
{ offset: 1, color: "rgba(64, 158, 255, 0.05)" } |
||||||
|
]) |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "系统 COP", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [5.2, 5.6, 5.9, 6.1, 5.9, 5.7, 5.4], |
||||||
|
lineStyle: { |
||||||
|
width: 3, |
||||||
|
color: "#67C23A" |
||||||
|
}, |
||||||
|
areaStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "rgba(103, 194, 58, 0.5)" }, |
||||||
|
{ offset: 1, color: "rgba(103, 194, 58, 0.05)" } |
||||||
|
]) |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "目标 COP", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0], |
||||||
|
lineStyle: { |
||||||
|
width: 2, |
||||||
|
color: "#E6A23C", |
||||||
|
type: "dashed" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
// 每日用电趋势图配置 |
||||||
|
getDailyEnergyOption() { |
||||||
|
const width = this.$refs.dailyEnergyChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "每日用电量趋势", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "axis", |
||||||
|
axisPointer: { |
||||||
|
type: "shadow" |
||||||
|
} |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
data: ["总用电量", "制冷量"], |
||||||
|
top: "bottom" |
||||||
|
}, |
||||||
|
grid: { |
||||||
|
left: "3%", |
||||||
|
right: "4%", |
||||||
|
bottom: "15%", |
||||||
|
containLabel: true |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
type: "category", |
||||||
|
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] |
||||||
|
}, |
||||||
|
yAxis: [ |
||||||
|
{ |
||||||
|
type: "value", |
||||||
|
name: "用电量 (kWh)", |
||||||
|
position: "left" |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: "value", |
||||||
|
name: "制冷量 (GJ)", |
||||||
|
position: "right" |
||||||
|
} |
||||||
|
], |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "总用电量", |
||||||
|
type: "bar", |
||||||
|
barWidth: "40%", |
||||||
|
data: [3200, 3450, 3100, 3600, 3350, 2800, 2650], |
||||||
|
itemStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "#83bff6" }, |
||||||
|
{ offset: 1, color: "#188df0" } |
||||||
|
]) |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "制冷量", |
||||||
|
type: "line", |
||||||
|
yAxisIndex: 1, |
||||||
|
smooth: true, |
||||||
|
data: [18.5, 19.8, 17.6, 20.5, 19.2, 15.8, 14.9], |
||||||
|
lineStyle: { |
||||||
|
width: 3, |
||||||
|
color: "#F56C6C" |
||||||
|
}, |
||||||
|
itemStyle: { |
||||||
|
color: "#F56C6C" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@import "~@/assets/styles/variables.scss"; |
||||||
|
|
||||||
|
.report-wrapper { |
||||||
|
background: #fff; |
||||||
|
padding: 0.2rem; |
||||||
|
border-radius: 4px; |
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); |
||||||
|
|
||||||
|
.report-title-section { |
||||||
|
text-align: center; |
||||||
|
border-bottom: 2px solid $blue; |
||||||
|
padding-bottom: 0.12rem; |
||||||
|
margin-bottom: 0.2rem; |
||||||
|
|
||||||
|
h1 { |
||||||
|
margin: 0 0 0.1rem 0; |
||||||
|
font-size: 0.2rem; |
||||||
|
color: $blue; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.report-time { |
||||||
|
font-size: 0.12rem; |
||||||
|
color: #909399; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-summary { |
||||||
|
background: #ecf5ff; |
||||||
|
padding: 0.12rem; |
||||||
|
border-radius: 3px; |
||||||
|
margin-bottom: 0.2rem; |
||||||
|
border-left: 4px solid $light-blue; |
||||||
|
|
||||||
|
h3 { |
||||||
|
margin: 0 0 0.08rem 0; |
||||||
|
font-size: 0.15rem; |
||||||
|
color: $light-blue; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin: 0; |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.6; |
||||||
|
color: #606266; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-main { |
||||||
|
.report-section { |
||||||
|
margin-bottom: 0.25rem; |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: 0.16rem; |
||||||
|
color: $light-blue; |
||||||
|
border-left: 4px solid $light-blue; |
||||||
|
padding-left: 0.1rem; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.section-content { |
||||||
|
p { |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.7; |
||||||
|
color: #606266; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
} |
||||||
|
|
||||||
|
.charts-container { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); |
||||||
|
gap: 0.15rem; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
|
||||||
|
.chart-box { |
||||||
|
width: 100%; |
||||||
|
height: 280px; |
||||||
|
background: #fafafa; |
||||||
|
border-radius: 3px; |
||||||
|
padding: 0.1rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.analysis-points { |
||||||
|
.analysis-point { |
||||||
|
display: flex; |
||||||
|
align-items: flex-start; |
||||||
|
gap: 0.06rem; |
||||||
|
margin-bottom: 0.08rem; |
||||||
|
font-size: 0.13rem; |
||||||
|
color: #606266; |
||||||
|
|
||||||
|
i { |
||||||
|
color: $green; |
||||||
|
font-size: 0.14rem; |
||||||
|
margin-top: 0.02rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.suggestions { |
||||||
|
margin-top: 0.12rem; |
||||||
|
padding: 0.12rem; |
||||||
|
background: #fef0f0; |
||||||
|
border-radius: 3px; |
||||||
|
border-left: 4px solid $red; |
||||||
|
|
||||||
|
h4 { |
||||||
|
margin: 0 0 0.08rem 0; |
||||||
|
font-size: 0.14rem; |
||||||
|
color: $red; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
ul { |
||||||
|
margin: 0; |
||||||
|
padding-left: 0.18rem; |
||||||
|
|
||||||
|
li { |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.7; |
||||||
|
color: #606266; |
||||||
|
margin-bottom: 0.05rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-footer { |
||||||
|
border-top: 1px solid #e6e6e6; |
||||||
|
padding-top: 0.12rem; |
||||||
|
margin-top: 0.15rem; |
||||||
|
|
||||||
|
.footer-info { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
font-size: 0.11rem; |
||||||
|
color: #909399; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,527 @@ |
|||||||
|
<template> |
||||||
|
<div class="comprehensive-report report-wrapper"> |
||||||
|
<!-- 报表标题 --> |
||||||
|
<div class="report-title-section"> |
||||||
|
<h1>{{ reportData.title }}</h1> |
||||||
|
<p class="report-time">生成时间:{{ reportData.generateTime }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表摘要 --> |
||||||
|
<div class="report-summary"> |
||||||
|
<h3>报告摘要</h3> |
||||||
|
<p>{{ reportData.summary }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表主体内容 --> |
||||||
|
<div class="report-main"> |
||||||
|
<!-- 项目情况 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>一、项目情况</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<p>{{ reportData.projectInfo.description }}</p> |
||||||
|
<el-table |
||||||
|
v-if="reportData.projectInfo.configTable" |
||||||
|
:data="reportData.projectInfo.configTable" |
||||||
|
border |
||||||
|
size="small" |
||||||
|
> |
||||||
|
<el-table-column |
||||||
|
v-for="col in reportData.projectInfo.configTableColumns" |
||||||
|
:key="col.prop" |
||||||
|
:prop="col.prop" |
||||||
|
:label="col.label" |
||||||
|
/> |
||||||
|
</el-table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 设备概述 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>二、设备概述</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<p>{{ reportData.equipmentOverview.description }}</p> |
||||||
|
<div class="charts-container"> |
||||||
|
<!-- 能耗占比饼图 --> |
||||||
|
<div ref="energyPieChart" class="chart-box"></div> |
||||||
|
<!-- 设备用电对比柱状图 --> |
||||||
|
<div ref="deviceBarChart" class="chart-box"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 运行数据 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>三、运行数据</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<div class="charts-container"> |
||||||
|
<!-- 能效趋势折线图 --> |
||||||
|
<div ref="efficiencyLineChart" class="chart-box"></div> |
||||||
|
</div> |
||||||
|
<el-table |
||||||
|
v-if="reportData.operationData.dataTable" |
||||||
|
:data="reportData.operationData.dataTable" |
||||||
|
border |
||||||
|
stripe |
||||||
|
size="small" |
||||||
|
> |
||||||
|
<el-table-column |
||||||
|
v-for="col in reportData.operationData.tableColumns" |
||||||
|
:key="col.prop" |
||||||
|
:prop="col.prop" |
||||||
|
:label="col.label" |
||||||
|
/> |
||||||
|
</el-table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 分析总结 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>四、分析总结</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<div class="analysis-points"> |
||||||
|
<div |
||||||
|
v-for="(point, index) in reportData.analysisSummary.points" |
||||||
|
:key="index" |
||||||
|
class="analysis-point" |
||||||
|
> |
||||||
|
<i class="el-icon-caret-right"></i> |
||||||
|
<span>{{ point }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-if="reportData.analysisSummary.suggestions" class="suggestions"> |
||||||
|
<h4>优化建议:</h4> |
||||||
|
<ul> |
||||||
|
<li |
||||||
|
v-for="(suggestion, index) in reportData.analysisSummary.suggestions" |
||||||
|
:key="index" |
||||||
|
> |
||||||
|
{{ suggestion }} |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表底部信息 --> |
||||||
|
<div class="report-footer"> |
||||||
|
<div class="footer-info"> |
||||||
|
<span>操作员:{{ userName }}</span> |
||||||
|
<span>生成日期:{{ operationDate }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import * as echarts from "echarts"; |
||||||
|
import { format, getDay } from "@/utils/datetime"; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "ComprehensiveReport", |
||||||
|
props: { |
||||||
|
reportData: { |
||||||
|
type: Object, |
||||||
|
default: () => ({}) |
||||||
|
}, |
||||||
|
userName: { |
||||||
|
type: String, |
||||||
|
default: "" |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
operationDate: getDay(0), |
||||||
|
chartInstances: [] |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.$nextTick(() => { |
||||||
|
this.renderCharts(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
beforeDestroy() { |
||||||
|
this.chartInstances.forEach((chart) => { |
||||||
|
if (chart) chart.dispose(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
renderCharts() { |
||||||
|
// 销毁旧图表 |
||||||
|
this.chartInstances.forEach((chart) => { |
||||||
|
if (chart) chart.dispose(); |
||||||
|
}); |
||||||
|
this.chartInstances = []; |
||||||
|
|
||||||
|
// 渲染能耗占比饼图 |
||||||
|
if (this.$refs.energyPieChart) { |
||||||
|
const chart = echarts.init(this.$refs.energyPieChart); |
||||||
|
chart.setOption(this.getEnergyPieOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
|
||||||
|
// 渲染能效趋势折线图 |
||||||
|
if (this.$refs.efficiencyLineChart) { |
||||||
|
const chart = echarts.init(this.$refs.efficiencyLineChart); |
||||||
|
chart.setOption(this.getEfficiencyLineOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
|
||||||
|
// 渲染设备用电对比柱状图 |
||||||
|
if (this.$refs.deviceBarChart) { |
||||||
|
const chart = echarts.init(this.$refs.deviceBarChart); |
||||||
|
chart.setOption(this.getDeviceBarOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
// 能耗占比饼图配置 |
||||||
|
getEnergyPieOption() { |
||||||
|
const width = this.$refs.energyPieChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "各系统能耗占比", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "item", |
||||||
|
formatter: "{a} <br/>{b}: {c} ({d}%)" |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
orient: "vertical", |
||||||
|
left: "left", |
||||||
|
top: "middle" |
||||||
|
}, |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "能耗占比", |
||||||
|
type: "pie", |
||||||
|
radius: "60%", |
||||||
|
center: ["50%", "50%"], |
||||||
|
data: [ |
||||||
|
{ value: 1200, name: "空调制冷系统" }, |
||||||
|
{ value: 800, name: "照明系统" }, |
||||||
|
{ value: 950, name: "水泵系统" }, |
||||||
|
{ value: 600, name: "热回收系统" }, |
||||||
|
{ value: 450, name: "其他" } |
||||||
|
], |
||||||
|
emphasis: { |
||||||
|
itemStyle: { |
||||||
|
shadowBlur: 10, |
||||||
|
shadowOffsetX: 0, |
||||||
|
shadowColor: "rgba(0, 0, 0, 0.5)" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
// 能效趋势折线图配置 |
||||||
|
getEfficiencyLineOption() { |
||||||
|
const width = this.$refs.efficiencyLineChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "系统能效趋势", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "axis" |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
data: ["空调系统", "水泵系统", "整体能效"], |
||||||
|
top: "bottom" |
||||||
|
}, |
||||||
|
grid: { |
||||||
|
left: "3%", |
||||||
|
right: "4%", |
||||||
|
bottom: "15%", |
||||||
|
containLabel: true |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
type: "category", |
||||||
|
boundaryGap: false, |
||||||
|
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] |
||||||
|
}, |
||||||
|
yAxis: { |
||||||
|
type: "value", |
||||||
|
name: "能效比 (COP)" |
||||||
|
}, |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "空调系统", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [5.2, 5.5, 5.8, 5.6, 5.9, 6.1, 6.0], |
||||||
|
areaStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "rgba(64, 158, 255, 0.5)" }, |
||||||
|
{ offset: 1, color: "rgba(64, 158, 255, 0.05)" } |
||||||
|
]) |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "水泵系统", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [4.8, 5.0, 5.2, 5.1, 5.3, 5.5, 5.4], |
||||||
|
areaStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "rgba(103, 194, 58, 0.5)" }, |
||||||
|
{ offset: 1, color: "rgba(103, 194, 58, 0.05)" } |
||||||
|
]) |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "整体能效", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [5.0, 5.3, 5.5, 5.4, 5.6, 5.8, 5.7], |
||||||
|
lineStyle: { |
||||||
|
width: 3, |
||||||
|
color: "#E6A23C" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
// 设备用电对比柱状图配置 |
||||||
|
getDeviceBarOption() { |
||||||
|
const width = this.$refs.deviceBarChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "主要设备用电对比", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "axis", |
||||||
|
axisPointer: { |
||||||
|
type: "shadow" |
||||||
|
} |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
data: ["用电量 (kWh)", "占比 (%)"], |
||||||
|
top: "bottom" |
||||||
|
}, |
||||||
|
grid: { |
||||||
|
left: "3%", |
||||||
|
right: "4%", |
||||||
|
bottom: "15%", |
||||||
|
containLabel: true |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
type: "category", |
||||||
|
data: ["冷水机组", "冷冻泵", "冷却泵", "照明灯具", "水泵组", "其他设备"] |
||||||
|
}, |
||||||
|
yAxis: [ |
||||||
|
{ |
||||||
|
type: "value", |
||||||
|
name: "用电量 (kWh)", |
||||||
|
position: "left" |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: "value", |
||||||
|
name: "占比 (%)", |
||||||
|
position: "right" |
||||||
|
} |
||||||
|
], |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "用电量 (kWh)", |
||||||
|
type: "bar", |
||||||
|
barWidth: "40%", |
||||||
|
data: [850, 420, 380, 520, 680, 350], |
||||||
|
itemStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "#83bff6" }, |
||||||
|
{ offset: 1, color: "#188df0" } |
||||||
|
]) |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "占比 (%)", |
||||||
|
type: "bar", |
||||||
|
yAxisIndex: 1, |
||||||
|
barWidth: "40%", |
||||||
|
data: [26.5, 13.1, 11.9, 16.2, 21.2, 10.9], |
||||||
|
itemStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "#f093fb" }, |
||||||
|
{ offset: 1, color: "#f5576c" } |
||||||
|
]) |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@import "~@/assets/styles/variables.scss"; |
||||||
|
|
||||||
|
.report-wrapper { |
||||||
|
background: #fff; |
||||||
|
padding: 0.2rem; |
||||||
|
border-radius: 4px; |
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); |
||||||
|
|
||||||
|
.report-title-section { |
||||||
|
text-align: center; |
||||||
|
border-bottom: 2px solid $blue; |
||||||
|
padding-bottom: 0.12rem; |
||||||
|
margin-bottom: 0.2rem; |
||||||
|
|
||||||
|
h1 { |
||||||
|
margin: 0 0 0.1rem 0; |
||||||
|
font-size: 0.2rem; |
||||||
|
color: $blue; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.report-time { |
||||||
|
font-size: 0.12rem; |
||||||
|
color: #909399; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-summary { |
||||||
|
background: #ecf5ff; |
||||||
|
padding: 0.12rem; |
||||||
|
border-radius: 3px; |
||||||
|
margin-bottom: 0.2rem; |
||||||
|
border-left: 4px solid $light-blue; |
||||||
|
|
||||||
|
h3 { |
||||||
|
margin: 0 0 0.08rem 0; |
||||||
|
font-size: 0.15rem; |
||||||
|
color: $light-blue; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin: 0; |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.6; |
||||||
|
color: #606266; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-main { |
||||||
|
.report-section { |
||||||
|
margin-bottom: 0.25rem; |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: 0.16rem; |
||||||
|
color: $light-blue; |
||||||
|
border-left: 4px solid $light-blue; |
||||||
|
padding-left: 0.1rem; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.section-content { |
||||||
|
p { |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.7; |
||||||
|
color: #606266; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
} |
||||||
|
|
||||||
|
.charts-container { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); |
||||||
|
gap: 0.15rem; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
|
||||||
|
.chart-box { |
||||||
|
width: 100%; |
||||||
|
height: 280px; |
||||||
|
background: #fafafa; |
||||||
|
border-radius: 3px; |
||||||
|
padding: 0.1rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.analysis-points { |
||||||
|
.analysis-point { |
||||||
|
display: flex; |
||||||
|
align-items: flex-start; |
||||||
|
gap: 0.06rem; |
||||||
|
margin-bottom: 0.08rem; |
||||||
|
font-size: 0.13rem; |
||||||
|
color: #606266; |
||||||
|
|
||||||
|
i { |
||||||
|
color: $green; |
||||||
|
font-size: 0.14rem; |
||||||
|
margin-top: 0.02rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.suggestions { |
||||||
|
margin-top: 0.12rem; |
||||||
|
padding: 0.12rem; |
||||||
|
background: #fef0f0; |
||||||
|
border-radius: 3px; |
||||||
|
border-left: 4px solid $red; |
||||||
|
|
||||||
|
h4 { |
||||||
|
margin: 0 0 0.08rem 0; |
||||||
|
font-size: 0.14rem; |
||||||
|
color: $red; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
ul { |
||||||
|
margin: 0; |
||||||
|
padding-left: 0.18rem; |
||||||
|
|
||||||
|
li { |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.7; |
||||||
|
color: #606266; |
||||||
|
margin-bottom: 0.05rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-footer { |
||||||
|
border-top: 1px solid #e6e6e6; |
||||||
|
padding-top: 0.12rem; |
||||||
|
margin-top: 0.15rem; |
||||||
|
|
||||||
|
.footer-info { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
font-size: 0.11rem; |
||||||
|
color: #909399; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,467 @@ |
|||||||
|
<template> |
||||||
|
<div class="lighting-report report-wrapper"> |
||||||
|
<!-- 报表标题 --> |
||||||
|
<div class="report-title-section"> |
||||||
|
<h1>{{ reportData.title }}</h1> |
||||||
|
<p class="report-time">生成时间:{{ reportData.generateTime }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表摘要 --> |
||||||
|
<div class="report-summary"> |
||||||
|
<h3>报告摘要</h3> |
||||||
|
<p>{{ reportData.summary }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表主体内容 --> |
||||||
|
<div class="report-main"> |
||||||
|
<!-- 项目情况 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>一、项目情况</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<p>{{ reportData.projectInfo.description }}</p> |
||||||
|
<el-table |
||||||
|
v-if="reportData.projectInfo.configTable" |
||||||
|
:data="reportData.projectInfo.configTable" |
||||||
|
border |
||||||
|
size="small" |
||||||
|
> |
||||||
|
<el-table-column |
||||||
|
v-for="col in reportData.projectInfo.configTableColumns" |
||||||
|
:key="col.prop" |
||||||
|
:prop="col.prop" |
||||||
|
:label="col.label" |
||||||
|
/> |
||||||
|
</el-table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 运行数据 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>二、运行数据</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<div class="charts-container"> |
||||||
|
<!-- 节电量趋势图 (双维度) --> |
||||||
|
<div ref="energySavingTrendChart" class="chart-box"></div> |
||||||
|
<!-- 节能率趋势图 --> |
||||||
|
<div ref="savingRateChart" class="chart-box"></div> |
||||||
|
</div> |
||||||
|
<el-table |
||||||
|
v-if="reportData.operationData.dataTable" |
||||||
|
:data="reportData.operationData.dataTable" |
||||||
|
border |
||||||
|
stripe |
||||||
|
size="small" |
||||||
|
> |
||||||
|
<el-table-column |
||||||
|
v-for="col in reportData.operationData.tableColumns" |
||||||
|
:key="col.prop" |
||||||
|
:prop="col.prop" |
||||||
|
:label="col.label" |
||||||
|
/> |
||||||
|
</el-table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 分析总结 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>三、分析总结</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<div class="analysis-points"> |
||||||
|
<div |
||||||
|
v-for="(point, index) in reportData.analysisSummary.points" |
||||||
|
:key="index" |
||||||
|
class="analysis-point" |
||||||
|
> |
||||||
|
<i class="el-icon-caret-right"></i> |
||||||
|
<span>{{ point }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-if="reportData.analysisSummary.suggestions" class="suggestions"> |
||||||
|
<h4>优化建议:</h4> |
||||||
|
<ul> |
||||||
|
<li |
||||||
|
v-for="(suggestion, index) in reportData.analysisSummary.suggestions" |
||||||
|
:key="index" |
||||||
|
> |
||||||
|
{{ suggestion }} |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表底部信息 --> |
||||||
|
<div class="report-footer"> |
||||||
|
<div class="footer-info"> |
||||||
|
<span>操作员:{{ userName }}</span> |
||||||
|
<span>生成日期:{{ operationDate }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import * as echarts from "echarts"; |
||||||
|
import { format, getDay } from "@/utils/datetime"; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "LightingReport", |
||||||
|
props: { |
||||||
|
reportData: { |
||||||
|
type: Object, |
||||||
|
default: () => ({}) |
||||||
|
}, |
||||||
|
userName: { |
||||||
|
type: String, |
||||||
|
default: "" |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
operationDate: getDay(0), |
||||||
|
chartInstances: [] |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.$nextTick(() => { |
||||||
|
this.renderCharts(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
beforeDestroy() { |
||||||
|
this.chartInstances.forEach((chart) => { |
||||||
|
if (chart) chart.dispose(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
renderCharts() { |
||||||
|
// 销毁旧图表 |
||||||
|
this.chartInstances.forEach((chart) => { |
||||||
|
if (chart) chart.dispose(); |
||||||
|
}); |
||||||
|
this.chartInstances = []; |
||||||
|
|
||||||
|
// 渲染节电量趋势图 (双维度) |
||||||
|
if (this.$refs.energySavingTrendChart) { |
||||||
|
const chart = echarts.init(this.$refs.energySavingTrendChart); |
||||||
|
chart.setOption(this.getEnergySavingTrendOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
|
||||||
|
// 渲染节能率趋势图 |
||||||
|
if (this.$refs.savingRateChart) { |
||||||
|
const chart = echarts.init(this.$refs.savingRateChart); |
||||||
|
chart.setOption(this.getSavingRateOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
// 节电量趋势图配置 (双维度) |
||||||
|
getEnergySavingTrendOption() { |
||||||
|
const width = this.$refs.energySavingTrendChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "节电量趋势分析", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "axis", |
||||||
|
axisPointer: { |
||||||
|
type: "shadow" |
||||||
|
} |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
data: ["节能数 (盏)", "节电量 (kWh)"], |
||||||
|
top: "bottom" |
||||||
|
}, |
||||||
|
grid: { |
||||||
|
left: "3%", |
||||||
|
right: "4%", |
||||||
|
bottom: "15%", |
||||||
|
containLabel: true |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
type: "category", |
||||||
|
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] |
||||||
|
}, |
||||||
|
yAxis: [ |
||||||
|
{ |
||||||
|
type: "value", |
||||||
|
name: "节能数 (盏)", |
||||||
|
position: "left" |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: "value", |
||||||
|
name: "节电量 (kWh)", |
||||||
|
position: "right" |
||||||
|
} |
||||||
|
], |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "节能数 (盏)", |
||||||
|
type: "bar", |
||||||
|
barWidth: "35%", |
||||||
|
data: [125, 132, 118, 145, 138, 95, 88], |
||||||
|
itemStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "#f093fb" }, |
||||||
|
{ offset: 1, color: "#f5576c" } |
||||||
|
]) |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "节电量 (kWh)", |
||||||
|
type: "bar", |
||||||
|
barWidth: "35%", |
||||||
|
yAxisIndex: 1, |
||||||
|
data: [450, 478, 425, 520, 495, 340, 315], |
||||||
|
itemStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "#83bff6" }, |
||||||
|
{ offset: 1, color: "#188df0" } |
||||||
|
]) |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
// 节能率趋势图配置 |
||||||
|
getSavingRateOption() { |
||||||
|
const width = this.$refs.savingRateChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "节能率趋势分析", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "axis" |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
data: ["当日节能率", "环比变化", "同比变化"], |
||||||
|
top: "bottom" |
||||||
|
}, |
||||||
|
grid: { |
||||||
|
left: "3%", |
||||||
|
right: "4%", |
||||||
|
bottom: "15%", |
||||||
|
containLabel: true |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
type: "category", |
||||||
|
boundaryGap: false, |
||||||
|
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] |
||||||
|
}, |
||||||
|
yAxis: { |
||||||
|
type: "value", |
||||||
|
name: "节能率 (%)", |
||||||
|
axisLabel: { |
||||||
|
formatter: "{value}%" |
||||||
|
} |
||||||
|
}, |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "当日节能率", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [32.5, 34.2, 31.8, 35.6, 33.9, 28.5, 27.2], |
||||||
|
lineStyle: { |
||||||
|
width: 3, |
||||||
|
color: "#67C23A" |
||||||
|
}, |
||||||
|
areaStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "rgba(103, 194, 58, 0.5)" }, |
||||||
|
{ offset: 1, color: "rgba(103, 194, 58, 0.05)" } |
||||||
|
]) |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "环比变化", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [1.2, 1.7, -2.4, 3.8, -1.7, -5.4, -1.3], |
||||||
|
lineStyle: { |
||||||
|
width: 2, |
||||||
|
color: "#E6A23C", |
||||||
|
type: "dashed" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "同比变化", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [3.5, 4.2, 2.8, 5.1, 3.9, 1.5, 0.8], |
||||||
|
lineStyle: { |
||||||
|
width: 2, |
||||||
|
color: "#409EFF", |
||||||
|
type: "dotted" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@import "~@/assets/styles/variables.scss"; |
||||||
|
|
||||||
|
.report-wrapper { |
||||||
|
background: #fff; |
||||||
|
padding: 0.2rem; |
||||||
|
border-radius: 4px; |
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); |
||||||
|
|
||||||
|
.report-title-section { |
||||||
|
text-align: center; |
||||||
|
border-bottom: 2px solid $blue; |
||||||
|
padding-bottom: 0.12rem; |
||||||
|
margin-bottom: 0.2rem; |
||||||
|
|
||||||
|
h1 { |
||||||
|
margin: 0 0 0.1rem 0; |
||||||
|
font-size: 0.2rem; |
||||||
|
color: $blue; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.report-time { |
||||||
|
font-size: 0.12rem; |
||||||
|
color: #909399; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-summary { |
||||||
|
background: #ecf5ff; |
||||||
|
padding: 0.12rem; |
||||||
|
border-radius: 3px; |
||||||
|
margin-bottom: 0.2rem; |
||||||
|
border-left: 4px solid $light-blue; |
||||||
|
|
||||||
|
h3 { |
||||||
|
margin: 0 0 0.08rem 0; |
||||||
|
font-size: 0.15rem; |
||||||
|
color: $light-blue; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin: 0; |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.6; |
||||||
|
color: #606266; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-main { |
||||||
|
.report-section { |
||||||
|
margin-bottom: 0.25rem; |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: 0.16rem; |
||||||
|
color: $light-blue; |
||||||
|
border-left: 4px solid $light-blue; |
||||||
|
padding-left: 0.1rem; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.section-content { |
||||||
|
p { |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.7; |
||||||
|
color: #606266; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
} |
||||||
|
|
||||||
|
.charts-container { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); |
||||||
|
gap: 0.15rem; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
|
||||||
|
.chart-box { |
||||||
|
width: 100%; |
||||||
|
height: 280px; |
||||||
|
background: #fafafa; |
||||||
|
border-radius: 3px; |
||||||
|
padding: 0.1rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.analysis-points { |
||||||
|
.analysis-point { |
||||||
|
display: flex; |
||||||
|
align-items: flex-start; |
||||||
|
gap: 0.06rem; |
||||||
|
margin-bottom: 0.08rem; |
||||||
|
font-size: 0.13rem; |
||||||
|
color: #606266; |
||||||
|
|
||||||
|
i { |
||||||
|
color: $green; |
||||||
|
font-size: 0.14rem; |
||||||
|
margin-top: 0.02rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.suggestions { |
||||||
|
margin-top: 0.12rem; |
||||||
|
padding: 0.12rem; |
||||||
|
background: #fef0f0; |
||||||
|
border-radius: 3px; |
||||||
|
border-left: 4px solid $red; |
||||||
|
|
||||||
|
h4 { |
||||||
|
margin: 0 0 0.08rem 0; |
||||||
|
font-size: 0.14rem; |
||||||
|
color: $red; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
ul { |
||||||
|
margin: 0; |
||||||
|
padding-left: 0.18rem; |
||||||
|
|
||||||
|
li { |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.7; |
||||||
|
color: #606266; |
||||||
|
margin-bottom: 0.05rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-footer { |
||||||
|
border-top: 1px solid #e6e6e6; |
||||||
|
padding-top: 0.12rem; |
||||||
|
margin-top: 0.15rem; |
||||||
|
|
||||||
|
.footer-info { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
font-size: 0.11rem; |
||||||
|
color: #909399; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,539 @@ |
|||||||
|
<template> |
||||||
|
<div class="pump-report report-wrapper"> |
||||||
|
<!-- 报表标题 --> |
||||||
|
<div class="report-title-section"> |
||||||
|
<h1>{{ reportData.title }}</h1> |
||||||
|
<p class="report-time">生成时间:{{ reportData.generateTime }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表摘要 --> |
||||||
|
<div class="report-summary"> |
||||||
|
<h3>报告摘要</h3> |
||||||
|
<p>{{ reportData.summary }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表主体内容 --> |
||||||
|
<div class="report-main"> |
||||||
|
<!-- 项目情况 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>一、项目情况</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<p>{{ reportData.projectInfo.description }}</p> |
||||||
|
<el-table |
||||||
|
v-if="reportData.projectInfo.configTable" |
||||||
|
:data="reportData.projectInfo.configTable" |
||||||
|
border |
||||||
|
size="small" |
||||||
|
> |
||||||
|
<el-table-column |
||||||
|
v-for="col in reportData.projectInfo.configTableColumns" |
||||||
|
:key="col.prop" |
||||||
|
:prop="col.prop" |
||||||
|
:label="col.label" |
||||||
|
/> |
||||||
|
</el-table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 设备概述 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>二、设备概述</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<p>{{ reportData.equipmentOverview.description }}</p> |
||||||
|
<div class="charts-container"> |
||||||
|
<!-- 水泵组用电占比饼图 --> |
||||||
|
<div ref="pumpPieChart" class="chart-box"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 运行数据 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>三、运行数据</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<div class="charts-container"> |
||||||
|
<!-- 分区吨水能耗趋势图 --> |
||||||
|
<div ref="zoneEnergyChart" class="chart-box"></div> |
||||||
|
<!-- 每日供水量与用电量对比折线图 --> |
||||||
|
<div ref="waterPowerChart" class="chart-box"></div> |
||||||
|
</div> |
||||||
|
<el-table |
||||||
|
v-if="reportData.operationData.dataTable" |
||||||
|
:data="reportData.operationData.dataTable" |
||||||
|
border |
||||||
|
stripe |
||||||
|
size="small" |
||||||
|
> |
||||||
|
<el-table-column |
||||||
|
v-for="col in reportData.operationData.tableColumns" |
||||||
|
:key="col.prop" |
||||||
|
:prop="col.prop" |
||||||
|
:label="col.label" |
||||||
|
/> |
||||||
|
</el-table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<!-- 分析总结 --> |
||||||
|
<section class="report-section"> |
||||||
|
<h2>四、分析总结</h2> |
||||||
|
<div class="section-content"> |
||||||
|
<div class="analysis-points"> |
||||||
|
<div |
||||||
|
v-for="(point, index) in reportData.analysisSummary.points" |
||||||
|
:key="index" |
||||||
|
class="analysis-point" |
||||||
|
> |
||||||
|
<i class="el-icon-caret-right"></i> |
||||||
|
<span>{{ point }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-if="reportData.analysisSummary.suggestions" class="suggestions"> |
||||||
|
<h4>优化建议:</h4> |
||||||
|
<ul> |
||||||
|
<li |
||||||
|
v-for="(suggestion, index) in reportData.analysisSummary.suggestions" |
||||||
|
:key="index" |
||||||
|
> |
||||||
|
{{ suggestion }} |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 报表底部信息 --> |
||||||
|
<div class="report-footer"> |
||||||
|
<div class="footer-info"> |
||||||
|
<span>操作员:{{ userName }}</span> |
||||||
|
<span>生成日期:{{ operationDate }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import * as echarts from "echarts"; |
||||||
|
import { format, getDay } from "@/utils/datetime"; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "PumpReport", |
||||||
|
props: { |
||||||
|
reportData: { |
||||||
|
type: Object, |
||||||
|
default: () => ({}) |
||||||
|
}, |
||||||
|
userName: { |
||||||
|
type: String, |
||||||
|
default: "" |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
operationDate: getDay(0), |
||||||
|
chartInstances: [] |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.$nextTick(() => { |
||||||
|
this.renderCharts(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
beforeDestroy() { |
||||||
|
this.chartInstances.forEach((chart) => { |
||||||
|
if (chart) chart.dispose(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
renderCharts() { |
||||||
|
// 销毁旧图表 |
||||||
|
this.chartInstances.forEach((chart) => { |
||||||
|
if (chart) chart.dispose(); |
||||||
|
}); |
||||||
|
this.chartInstances = []; |
||||||
|
|
||||||
|
// 渲染水泵组用电占比饼图 |
||||||
|
if (this.$refs.pumpPieChart) { |
||||||
|
const chart = echarts.init(this.$refs.pumpPieChart); |
||||||
|
chart.setOption(this.getPumpPieOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
|
||||||
|
// 渲染分区吨水能耗趋势图 |
||||||
|
if (this.$refs.zoneEnergyChart) { |
||||||
|
const chart = echarts.init(this.$refs.zoneEnergyChart); |
||||||
|
chart.setOption(this.getZoneEnergyOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
|
||||||
|
// 渲染每日供水量与用电量对比折线图 |
||||||
|
if (this.$refs.waterPowerChart) { |
||||||
|
const chart = echarts.init(this.$refs.waterPowerChart); |
||||||
|
chart.setOption(this.getWaterPowerOption()); |
||||||
|
this.chartInstances.push(chart); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
// 水泵组用电占比饼图配置 |
||||||
|
getPumpPieOption() { |
||||||
|
const width = this.$refs.pumpPieChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "水泵组用电占比", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "item", |
||||||
|
formatter: "{a} <br/>{b}: {c} kWh ({d}%)" |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
orient: "vertical", |
||||||
|
left: "left", |
||||||
|
top: "middle" |
||||||
|
}, |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "用电占比", |
||||||
|
type: "pie", |
||||||
|
radius: ["40%", "70%"], |
||||||
|
center: ["50%", "50%"], |
||||||
|
data: [ |
||||||
|
{ value: 680, name: "高区水泵组" }, |
||||||
|
{ value: 520, name: "中区水泵组" }, |
||||||
|
{ value: 450, name: "低区水泵组" }, |
||||||
|
{ value: 280, name: "稳压泵组" } |
||||||
|
], |
||||||
|
emphasis: { |
||||||
|
itemStyle: { |
||||||
|
shadowBlur: 10, |
||||||
|
shadowOffsetX: 0, |
||||||
|
shadowColor: "rgba(0, 0, 0, 0.5)" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
// 分区吨水能耗趋势图配置 |
||||||
|
getZoneEnergyOption() { |
||||||
|
const width = this.$refs.zoneEnergyChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "分区吨水能耗趋势", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "axis" |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
data: ["高区", "中区", "低区", "平均"], |
||||||
|
top: "bottom" |
||||||
|
}, |
||||||
|
grid: { |
||||||
|
left: "3%", |
||||||
|
right: "4%", |
||||||
|
bottom: "15%", |
||||||
|
containLabel: true |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
type: "category", |
||||||
|
boundaryGap: false, |
||||||
|
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] |
||||||
|
}, |
||||||
|
yAxis: { |
||||||
|
type: "value", |
||||||
|
name: "吨水电耗 (kWh/m³)" |
||||||
|
}, |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "高区", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [0.42, 0.45, 0.43, 0.46, 0.44, 0.38, 0.36], |
||||||
|
lineStyle: { |
||||||
|
width: 2, |
||||||
|
color: "#F56C6C" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "中区", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [0.35, 0.37, 0.36, 0.38, 0.36, 0.32, 0.30], |
||||||
|
lineStyle: { |
||||||
|
width: 2, |
||||||
|
color: "#E6A23C" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "低区", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [0.28, 0.30, 0.29, 0.31, 0.29, 0.26, 0.24], |
||||||
|
lineStyle: { |
||||||
|
width: 2, |
||||||
|
color: "#67C23A" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "平均", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [0.35, 0.37, 0.36, 0.38, 0.36, 0.32, 0.30], |
||||||
|
lineStyle: { |
||||||
|
width: 3, |
||||||
|
color: "#409EFF", |
||||||
|
type: "dashed" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
// 每日供水量与用电量对比折线图配置 |
||||||
|
getWaterPowerOption() { |
||||||
|
const width = this.$refs.waterPowerChart?.clientWidth || 400; |
||||||
|
const titleFontSize = width / 50; |
||||||
|
|
||||||
|
return { |
||||||
|
title: { |
||||||
|
text: "每日供水量与用电量对比", |
||||||
|
left: "center", |
||||||
|
textStyle: { |
||||||
|
fontSize: titleFontSize, |
||||||
|
color: "#333" |
||||||
|
} |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
trigger: "axis" |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
data: ["供水量 (m³)", "用电量 (kWh)"], |
||||||
|
top: "bottom" |
||||||
|
}, |
||||||
|
grid: { |
||||||
|
left: "3%", |
||||||
|
right: "4%", |
||||||
|
bottom: "15%", |
||||||
|
containLabel: true |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
type: "category", |
||||||
|
boundaryGap: false, |
||||||
|
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] |
||||||
|
}, |
||||||
|
yAxis: [ |
||||||
|
{ |
||||||
|
type: "value", |
||||||
|
name: "供水量 (m³)", |
||||||
|
position: "left" |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: "value", |
||||||
|
name: "用电量 (kWh)", |
||||||
|
position: "right" |
||||||
|
} |
||||||
|
], |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
name: "供水量 (m³)", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
data: [1850, 1920, 1880, 1950, 1900, 1650, 1580], |
||||||
|
lineStyle: { |
||||||
|
width: 3, |
||||||
|
color: "#409EFF" |
||||||
|
}, |
||||||
|
areaStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "rgba(64, 158, 255, 0.5)" }, |
||||||
|
{ offset: 1, color: "rgba(64, 158, 255, 0.05)" } |
||||||
|
]) |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "用电量 (kWh)", |
||||||
|
type: "line", |
||||||
|
smooth: true, |
||||||
|
yAxisIndex: 1, |
||||||
|
data: [680, 720, 695, 740, 710, 580, 545], |
||||||
|
lineStyle: { |
||||||
|
width: 3, |
||||||
|
color: "#67C23A" |
||||||
|
}, |
||||||
|
areaStyle: { |
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
||||||
|
{ offset: 0, color: "rgba(103, 194, 58, 0.5)" }, |
||||||
|
{ offset: 1, color: "rgba(103, 194, 58, 0.05)" } |
||||||
|
]) |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@import "~@/assets/styles/variables.scss"; |
||||||
|
|
||||||
|
.report-wrapper { |
||||||
|
background: #fff; |
||||||
|
padding: 0.2rem; |
||||||
|
border-radius: 4px; |
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); |
||||||
|
|
||||||
|
.report-title-section { |
||||||
|
text-align: center; |
||||||
|
border-bottom: 2px solid $blue; |
||||||
|
padding-bottom: 0.12rem; |
||||||
|
margin-bottom: 0.2rem; |
||||||
|
|
||||||
|
h1 { |
||||||
|
margin: 0 0 0.1rem 0; |
||||||
|
font-size: 0.2rem; |
||||||
|
color: $blue; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.report-time { |
||||||
|
font-size: 0.12rem; |
||||||
|
color: #909399; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-summary { |
||||||
|
background: #ecf5ff; |
||||||
|
padding: 0.12rem; |
||||||
|
border-radius: 3px; |
||||||
|
margin-bottom: 0.2rem; |
||||||
|
border-left: 4px solid $light-blue; |
||||||
|
|
||||||
|
h3 { |
||||||
|
margin: 0 0 0.08rem 0; |
||||||
|
font-size: 0.15rem; |
||||||
|
color: $light-blue; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin: 0; |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.6; |
||||||
|
color: #606266; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-main { |
||||||
|
.report-section { |
||||||
|
margin-bottom: 0.25rem; |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: 0.16rem; |
||||||
|
color: $light-blue; |
||||||
|
border-left: 4px solid $light-blue; |
||||||
|
padding-left: 0.1rem; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.section-content { |
||||||
|
p { |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.7; |
||||||
|
color: #606266; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
} |
||||||
|
|
||||||
|
.charts-container { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); |
||||||
|
gap: 0.15rem; |
||||||
|
margin-bottom: 0.12rem; |
||||||
|
|
||||||
|
.chart-box { |
||||||
|
width: 100%; |
||||||
|
height: 280px; |
||||||
|
background: #fafafa; |
||||||
|
border-radius: 3px; |
||||||
|
padding: 0.1rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.analysis-points { |
||||||
|
.analysis-point { |
||||||
|
display: flex; |
||||||
|
align-items: flex-start; |
||||||
|
gap: 0.06rem; |
||||||
|
margin-bottom: 0.08rem; |
||||||
|
font-size: 0.13rem; |
||||||
|
color: #606266; |
||||||
|
|
||||||
|
i { |
||||||
|
color: $green; |
||||||
|
font-size: 0.14rem; |
||||||
|
margin-top: 0.02rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.suggestions { |
||||||
|
margin-top: 0.12rem; |
||||||
|
padding: 0.12rem; |
||||||
|
background: #fef0f0; |
||||||
|
border-radius: 3px; |
||||||
|
border-left: 4px solid $red; |
||||||
|
|
||||||
|
h4 { |
||||||
|
margin: 0 0 0.08rem 0; |
||||||
|
font-size: 0.14rem; |
||||||
|
color: $red; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
ul { |
||||||
|
margin: 0; |
||||||
|
padding-left: 0.18rem; |
||||||
|
|
||||||
|
li { |
||||||
|
font-size: 0.13rem; |
||||||
|
line-height: 1.7; |
||||||
|
color: #606266; |
||||||
|
margin-bottom: 0.05rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.report-footer { |
||||||
|
border-top: 1px solid #e6e6e6; |
||||||
|
padding-top: 0.12rem; |
||||||
|
margin-top: 0.15rem; |
||||||
|
|
||||||
|
.footer-info { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
font-size: 0.11rem; |
||||||
|
color: #909399; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
/** |
||||||
|
* AI报表组件统一导出 |
||||||
|
*/ |
||||||
|
|
||||||
|
// 项目综合分析报告
|
||||||
|
export { default as ComprehensiveReport } from './ComprehensiveReport.vue' |
||||||
|
|
||||||
|
// 空调制冷系统分析报告
|
||||||
|
export { default as AirConditioningReport } from './AirConditioningReport.vue' |
||||||
|
|
||||||
|
// 照明系统分析报告
|
||||||
|
export { default as LightingReport } from './LightingReport.vue' |
||||||
|
|
||||||
|
// 水泵系统分析报告
|
||||||
|
export { default as PumpReport } from './PumpReport.vue' |
||||||
@ -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> |
||||||
Loading…
Reference in new issue