|
|
<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> |