<template>
|
<div class="gas-scheduling-container">
|
<el-button size="small" class="fullscreen-btn" @click="toggleFullscreen">
|
<Icon
|
class="is-hover mr-12px cursor-pointer"
|
:icon="isFullscreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
|
color="#8FD6FE"
|
hover-color="var(--el-color-primary)"
|
/>
|
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
</el-button>
|
<div class="gas-scheduling-left">
|
<div id="mqhsssxx">
|
<div class="title"></div>
|
<div class="data1-item" v-for="(item, index) in mqhsList" :key="`dynamics-${index}`">
|
<div class="content">
|
<div class="value">
|
<span>{{item.value}}</span> <span>{{item.unit}}</span>
|
</div>
|
<div class="name">
|
{{item.name}}
|
</div>
|
</div>
|
</div>
|
</div>
|
<div id="tsxx">
|
<div class="title"></div>
|
<div class="data1-item" v-for="(item, index) in tsxxList" :key="`dynamics-${index}`">
|
<div class="content">
|
<div class="value">
|
<span>{{item.value}}</span> <span>{{item.unit}}</span>
|
</div>
|
<div class="name">
|
{{item.name}}
|
</div>
|
</div>
|
</div>
|
</div>
|
<div id="zlxx">
|
<div class="title"></div>
|
<el-table :data="zlxxList" class="transparent-table">
|
<el-table-column prop="name" label="控制器名称" header-class-name="custom-header" width="150"/>
|
<el-table-column prop="zl1" label="1#转炉" header-class-name="custom-header" />
|
<el-table-column prop="zl2" label="2#转炉" header-class-name="custom-header" />
|
<el-table-column prop="zl3" label="3#转炉" header-class-name="custom-header" />
|
</el-table>
|
</div>
|
<div id="mqxhssxx">
|
<div class="title"></div>
|
<div class="data2-item" v-for="(item, index) in mqxhssxxList" :key="`dynamics-${index}`">
|
<div class="content2">
|
<div class="name">
|
{{item.name}}
|
</div>
|
<div class="value">
|
<span>{{item.value}}</span> <span>{{item.unit}}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="gas-scheduling-center">
|
<div class="mode-switch">
|
<el-radio-group v-model="tabPosition" @change="handleChange" class="custom-radio-group">
|
<el-radio-button label="model">大模型模式</el-radio-button>
|
<el-radio-button label="conversation">对话模式</el-radio-button>
|
</el-radio-group>
|
</div>
|
|
<div v-if="tabPosition === 'model'">
|
<!-- 对话列表 -->
|
<ConversationList
|
v-show="false"
|
:active-id="activeConversationId"
|
ref="conversationListRef"
|
@on-conversation-click="handleConversationClick"
|
@on-conversation-clear="handleConversationClear"
|
@on-conversation-delete="handlerConversationDelete"
|
/>
|
<div class="detail-container">
|
<!-- 输入框 -->
|
<div class="input-container">
|
<form class="prompt-from">
|
<textarea
|
class="prompt-input"
|
v-model="prompt"
|
@keydown="handleSendByKeydown"
|
@input="handlePromptInput"
|
@compositionstart="onCompositionstart"
|
@compositionend="onCompositionend"
|
placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
|
></textarea>
|
<div class="prompt-btns">
|
<div class="content">
|
<el-button
|
:class="{ 'active-button': enableContext }"
|
@click="enableContext = !enableContext"
|
>
|
<el-icon class="content-icon" />
|
上下文
|
</el-button>
|
</div>
|
<div class="message">
|
<el-button
|
type="primary"
|
size="default"
|
@click="handleSendByButton"
|
:loading="conversationInProgress"
|
v-if="conversationInProgress == false"
|
>
|
{{ conversationInProgress ? '进行中' : '发消息' }}
|
</el-button>
|
<el-button
|
type="danger"
|
size="default"
|
@click="stopStream()"
|
v-if="conversationInProgress == true"
|
>
|
停止
|
</el-button>
|
</div>
|
</div>
|
</form>
|
</div>
|
|
<!-- main:消息列表 -->
|
<el-main class="main-container">
|
<div class="title">
|
<span>工业能源大模型思考</span>
|
</div>
|
<div>
|
<div class="message-container">
|
<!-- 情况一:消息加载中 -->
|
<MessageLoading v-if="activeMessageListLoading" />
|
<!-- 情况二:消息列表为空 -->
|
<MessageListEmpty
|
v-if="!activeMessageListLoading && messageList.length === 0 && activeConversation"
|
@on-prompt="doSendMessage"
|
/>
|
<!-- 情况三:消息列表不为空 -->
|
<ModelMessageList
|
v-if="!activeMessageListLoading && messageList.length > 0"
|
ref="messageRef"
|
:conversation="activeConversation"
|
:list="messageList"
|
@on-delete-success="handleMessageDelete"
|
@on-edit="handleMessageEdit"
|
@on-refresh="handleMessageRefresh"
|
/>
|
</div>
|
</div>
|
</el-main>
|
<!-- main:调度推理结论 -->
|
<div class="result-container-title">
|
<span>调度推理结论</span><el-button @click="openHistoryMessage" size="small" class="history-button" :icon="ArrowUpBold">历史建议</el-button>
|
</div>
|
<el-main class="result-container">
|
<div class="result">
|
<textarea class="result-content" v-model="ddtlResult"></textarea>
|
</div>
|
</el-main>
|
</div>
|
<!-- 历史建议 -->
|
<HistoryMessageDialog
|
ref="historyMessageRef"
|
:parentMethod="queryHistoryMessage"
|
@gotoManualMethod="gotoManual"
|
/>
|
</div>
|
|
<div v-else>
|
<NormalConversation
|
:data="defaultMessage"
|
/>
|
</div>
|
</div>
|
|
<div class="gas-scheduling-right">
|
<div id="ldghslyc">
|
<div class="title"></div>
|
<div ref="LDGHSLYCEhartContainer" style="width: 100%; height: 180px"></div>
|
</div>
|
<div id="ldggrqsyc">
|
<div class="title"></div>
|
<div ref="LDGGRYCEhartContainer" style="width: 100%; height: 180px"></div>
|
</div>
|
<div id="mqhsjhxx">
|
<div class="title"></div>
|
<div class="time-content" v-for="(item, index) in mqhsjhTimeList" :key="`dynamics-${index}`">
|
<div class="time-content-item">
|
<div class="name">
|
<span>{{item.name}}</span>
|
</div>
|
<div class="time">
|
<div class="in-pot"></div><div class="in">装入{{item.inTime}}</div>
|
<div class="out-pot"></div><div class="out">结束{{item.outTime}}</div>
|
</div>
|
</div>
|
</div>
|
<div class="data2-item" v-for="(item, index) in mqhsjhxxList" :key="`dynamics-${index}`">
|
<div class="content">
|
<div class="name">
|
{{item.name}}
|
</div>
|
<div class="value">
|
<span>{{item.value}}</span> <span>{{item.unit}}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div id="scmbyyxzb">
|
<div class="title"></div>
|
<div class="little-title">生产目标/班</div>
|
<div class="data3-item" v-for="(item, index) in scmbList" :key="`dynamics-${index}`">
|
<div class="content2">
|
<div class="name">
|
{{item.name}}
|
</div>
|
<el-progress
|
:percentage="percentage(item)"
|
:stroke-width="12"
|
:text-inside="true"
|
:color="customColor"
|
:show-text="false"
|
/>
|
<div class="value">
|
<span>{{item.current}}/{{item.total}}</span>
|
</div>
|
<div class="value-content">
|
<span>已吹炼/总炉数</span>
|
</div>
|
</div>
|
</div>
|
<div class="little-title">运行指标/天</div>
|
<div class="zb-content">
|
<div class="item left-label"></div>
|
<div class="item data4-item" v-for="(item, index) in yxzbList" :key="`dynamics-${index}`">
|
<div class="content">
|
<div class="value">
|
<span>{{item.value}}</span> <span>{{item.unit}}</span>
|
</div>
|
<div class="name">
|
{{item.name}}
|
</div>
|
</div>
|
</div>
|
<div class="item right-label"></div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import { ref, onMounted, reactive } from 'vue'
|
import {ChatConversationApi, ChatConversationVO} from "@/api/ai/chat/conversation";
|
import {ChatMessageApi, ChatMessageVO} from "@/api/ai/chat/message";
|
import ModelMessageList from '../components/message/ModelMessageList.vue'
|
import NormalConversation from '../components/conversation/CommonConversation.vue'
|
import MessageListEmpty from '../components/message/MessageListEmpty.vue'
|
import MessageLoading from '../components/message/MessageLoading.vue'
|
import ConversationList from "../components/conversation/ConversationList.vue";
|
import HistoryMessageDialog from "../components/message/HistoryMessageDialog.vue"
|
import * as echarts from "echarts";
|
import {formatToDateTime} from "@/utils/dateUtil";
|
import {refreshToken} from "@/api/login";
|
import {round} from "lodash-es";
|
import {ArrowUpBold} from "@element-plus/icons-vue";
|
import * as authUtil from "@/utils/auth";
|
import HistoryMessageList from "@/views/ai/dashboard/components/message/HistoryMessageList.vue";
|
|
const mqhsList = ref([
|
{
|
name: '单转炉煤气回收流量',
|
value: 130,
|
unit: 'km³/h'
|
},
|
{
|
name: '转炉煤气 O 含量',
|
value: 618,
|
unit: '%'
|
},
|
{
|
name: '转炉煤气 CO 含量',
|
value: 15,
|
unit: '%'
|
},
|
{
|
name: '转炉铁水碳含量',
|
value: 20,
|
unit: '%'
|
},
|
{
|
name: '三通阀信号',
|
value: 0,
|
unit: ''
|
},
|
{
|
name: '单转炉吹氧流量',
|
value: 400,
|
unit: 'kNm³/h'
|
}
|
])
|
|
const tsxxList = ref([
|
{
|
name: '各高炉出铁水信号',
|
value: '进行',
|
unit: ''
|
},
|
{
|
name: '各高炉出铁量',
|
value: 5000,
|
unit: '吨'
|
},
|
{
|
name: '各高炉铁水装入鱼雷罐车信号',
|
value: '进行',
|
unit: 'm³/h'
|
},
|
{
|
name: '鱼雷罐车等待信号',
|
value: '进行',
|
unit: 'm³/h'
|
},
|
{
|
name: '铁水倒入铁水包信号',
|
value: '不进行',
|
unit: 'm³/h'
|
},
|
{
|
name: '铁产量计划',
|
value: 6000,
|
unit: '吨'
|
},
|
])
|
|
const zlxxList = ref([
|
{
|
name: '吹炼状态',
|
zl1: '正在吹炼',
|
zl2: '正在吹炼',
|
zl3: '正在吹炼'
|
},
|
{
|
name: '当前状态持续时间',
|
zl1: '10min',
|
zl2: '10min',
|
zl3: '10min'
|
},
|
{
|
name: '当前炉吹炼开始时刻',
|
zl1: '18:40',
|
zl2: '18:40',
|
zl3: '18:40'
|
},
|
{
|
name: '当前炉吹炼结束时刻',
|
zl1: '18:40',
|
zl2: '18:40',
|
zl3: '18:40'
|
},
|
{
|
name: '前一炉吹炼开始时刻',
|
zl1: '18:40',
|
zl2: '18:40',
|
zl3: '18:40'
|
},
|
{
|
name: '前一炉吹炼结束时刻',
|
zl1: '18:40',
|
zl2: '18:40',
|
zl3: '18:40'
|
}
|
])
|
|
const mqxhssxxList = ref([
|
{
|
name: '去棒三混合站',
|
value: 57.1,
|
unit: 'km³/h'
|
},
|
{
|
name: '东区掺混 LDG',
|
value: 49.4,
|
unit: 'km³/h'
|
},
|
{
|
name: '去焦化方向',
|
value: 67.4,
|
unit: 'm³/h'
|
},
|
{
|
name: '西区掺混 LDG',
|
value: 70,
|
unit: 'm³/h'
|
},
|
{
|
name: '送 BFG 管网',
|
value: 50.1,
|
unit: 'm³/h'
|
},
|
{
|
name: '去热卷二',
|
value: 72.2,
|
unit: 'km³/h'
|
},
|
{
|
name: '热卷一',
|
value: 13.9,
|
unit: 'km³/h'
|
},
|
{
|
name: '转底炉 1',
|
value: 7.0,
|
unit: 'km³/h'
|
},
|
{
|
name: '超薄带',
|
value: 67.4,
|
unit: 'km³/h'
|
},
|
{
|
name: '转底炉 2',
|
value: 13.5,
|
unit: 'km³/h'
|
},
|
{
|
name: '135MW 1',
|
value: 45.3,
|
unit: 'km³/h'
|
},
|
{
|
name: '135MW 2',
|
value: 36.2,
|
unit: 'km³/h'
|
},
|
])
|
|
const mqhsjhxxList = ref([
|
{
|
name: '转炉总炉数\n' +
|
'日计划',
|
value: 567,
|
unit: '炉'
|
},
|
{
|
name: '转炉入炉铁水量\n' +
|
'日计划',
|
value: 200,
|
unit: '吨'
|
},
|
{
|
name: '转炉检修计划',
|
value: '未进行',
|
unit: ''
|
},
|
{
|
name: '钢产量日计划',
|
value: 300,
|
unit: '吨'
|
},
|
{
|
name: '转炉加入废钢总量',
|
value: 500,
|
unit: '吨'
|
},
|
{
|
name: '转炉实绩钢产量',
|
value: 100,
|
unit: '吨'
|
}
|
])
|
|
const mqhsjhTimeList = ref([
|
{
|
name: '兑铁',
|
inTime: '04-23 03:19',
|
outTime: '04-28 14:54',
|
},
|
{
|
name: '吹炼',
|
inTime: '04-23 03:19',
|
outTime: '04-28 14:54',
|
},
|
{
|
name: '出钢',
|
inTime: '04-23 03:19',
|
outTime: '04-28 14:54',
|
}
|
])
|
|
const scmbList = ref([
|
{
|
id: 1,
|
name: '1#转炉',
|
current: 20,
|
total: 30
|
},
|
{
|
id: 2,
|
name: '2#转炉',
|
current: 25,
|
total: 100
|
},
|
{
|
id: 3,
|
name: '3#转炉',
|
current: 4,
|
total: 29
|
}
|
])
|
|
// 自定义进度条颜色(可选)
|
const customColor = '#409EFF';
|
|
const percentage = (item) => {
|
return Math.round((item.current / item.total) * 100);
|
};
|
|
const yxzbList = ref([
|
{
|
name: '昨日LDG拒收时间',
|
value: 0.00,
|
unit: 'min'
|
},
|
{
|
name: '昨日吨钢回收量',
|
value: 86.08,
|
unit: 'm³/t'
|
},{
|
name: '昨日LDG混入累积量',
|
value: 2687.25,
|
unit: 'km³'
|
}
|
|
])
|
|
const ddtlResult = ref('')
|
|
const route = useRoute() // 路由
|
const message = useMessage() // 消息弹窗
|
|
// 聊天对话
|
const conversationListRef = ref()
|
const activeConversationId = ref<number | null>(null) // 选中的对话编号
|
const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation
|
const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
|
|
// 消息列表
|
const messageRef = ref()
|
const activeMessageList = ref<ChatMessageVO[]>([]) // 选中对话的消息列表
|
const activeHistoryMessageList = ref<ChatMessageVO[]>([]) // 历史建议列表
|
const activeHistoryMessageTotal = ref(0) // 历史建议总数
|
const activeMessageListLoading = ref<boolean>(false) // activeMessageList 是否正在加载中
|
const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer 定时器。如果加载速度很快,就不进入加载中
|
// 消息滚动
|
const textSpeed = ref<number>(50) // Typing speed in milliseconds
|
const textRoleRunning = ref<boolean>(false) // Typing speed in milliseconds
|
|
// 发送消息输入框
|
const isComposing = ref(false) // 判断用户是否在输入
|
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
|
const inputTimeout = ref<any>() // 处理输入中回车的定时器
|
const prompt = ref<string>() // prompt
|
const enableContext = ref<boolean>(false) // 是否开启上下文
|
// 接收 Stream 消息
|
const receiveMessageFullText = ref('')
|
const receiveMessageDisplayedText = ref('')
|
|
const tabPosition = ref('model')
|
|
// 模型数据
|
const modelData = ref()
|
|
/** 历史建议 */
|
const historyMessageRef = ref()
|
const openHistoryMessage = async () => {
|
// 刷新 message 列表
|
let resDate = await historyMessageRef.value.dealDate()
|
await getHistoryMessageList(resDate)
|
historyMessageRef.value.open(activeHistoryMessageList.value, activeConversation.value, activeHistoryMessageTotal.value)
|
}
|
|
const queryHistoryMessage = async (queryParams: ChatMessageVO) => {
|
return await getHistoryMessageList(queryParams)
|
}
|
|
//切换对话模式判断
|
const handleChange = async () => {
|
// 对话进行中,不允许切换
|
if (conversationInProgress.value) {
|
message.alert('对话中,不允许切换!')
|
return false
|
}
|
}
|
|
// 默认选中消息
|
const defaultMessage = ref<ChatMessageVO>()
|
|
const gotoManual = async (item: ChatMessageVO) => {
|
defaultMessage.value = item
|
tabPosition.value = 'conversation'
|
}
|
|
// =========== 【聊天对话】相关 ===========
|
|
/** 获取对话信息 */
|
const getConversation = async (id: number | null) => {
|
if (!id) {
|
return
|
}
|
const conversation: ChatConversationVO = await ChatConversationApi.getChatConversationMy(id)
|
if (!conversation) {
|
return
|
}
|
activeConversation.value = conversation
|
activeConversationId.value = conversation.id
|
}
|
|
/**
|
* 点击某个对话
|
*
|
* @param conversation 选中的对话
|
* @return 是否切换成功
|
*/
|
const handleConversationClick = async (conversation: ChatConversationVO) => {
|
// 对话进行中,不允许切换
|
if (conversationInProgress.value) {
|
message.alert('对话中,不允许切换!')
|
return false
|
}
|
|
// 更新选中的对话 id
|
activeConversationId.value = conversation.id
|
activeConversation.value = conversation
|
// 刷新 message 列表
|
await getMessageList()
|
// 滚动底部
|
scrollToBottom(true)
|
// 清空输入框
|
// prompt.value = ''
|
return true
|
}
|
|
/** 删除某个对话*/
|
const handlerConversationDelete = async (delConversation: ChatConversationVO) => {
|
// 删除的对话如果是当前选中的,那么就重置
|
if (activeConversationId.value === delConversation.id) {
|
await handleConversationClear()
|
}
|
}
|
/** 清空选中的对话 */
|
const handleConversationClear = async () => {
|
// 对话进行中,不允许切换
|
if (conversationInProgress.value) {
|
message.alert('对话中,不允许切换!')
|
return false
|
}
|
activeConversationId.value = null
|
activeConversation.value = null
|
activeMessageList.value = []
|
}
|
|
// =========== 【消息列表】相关 ===========
|
|
/** 获取消息 message 列表 */
|
const getMessageList = async () => {
|
try {
|
if (activeConversationId.value === null) {
|
return
|
}
|
// Timer 定时器,如果加载速度很快,就不进入加载中
|
activeMessageListLoadingTimer.value = setTimeout(() => {
|
activeMessageListLoading.value = true
|
}, 60)
|
|
// 获取消息列表
|
activeMessageList.value = await ChatMessageApi.getEnergyChatMessageListByConversationId(
|
activeConversationId.value
|
)
|
if(activeMessageList.value.length != 0) {
|
prompt.value = activeMessageList.value[0].content
|
}
|
|
// 滚动到最下面
|
await nextTick()
|
await scrollToBottom()
|
} finally {
|
// time 定时器,如果加载速度很快,就不进入加载中
|
if (activeMessageListLoadingTimer.value) {
|
clearTimeout(activeMessageListLoadingTimer.value)
|
}
|
// 加载结束
|
activeMessageListLoading.value = false
|
}
|
}
|
|
/** 获取消息 message 列表 */
|
const getHistoryMessageList = async (params: any) => {
|
if (activeConversationId.value === null) {
|
return
|
}
|
params.conversationId = activeConversationId.value
|
// 获取消息列表
|
let pageResult = await ChatMessageApi.getChatMessagePageListByConversationId(params)
|
activeHistoryMessageList.value = pageResult.list
|
activeHistoryMessageTotal.value = pageResult.total
|
if (activeHistoryMessageList.value != null && activeHistoryMessageList.value.length > 0) {
|
activeHistoryMessageList.value.forEach((message: ChatMessageVO) => {
|
if(message.type != 'user') {
|
dealResult(message)
|
}
|
})
|
}
|
return pageResult
|
}
|
//处理调度推理结论
|
const dealResult = (message: any) => {
|
const spliceText = message.content.includes("总结:") ? "总结:" : "结论:";
|
const regex = new RegExp('(\\n*)([\\s\\S]*?)(\\n*)' + spliceText + '([\\s\\S]*)');
|
const match = message.content.match(regex);
|
if(match) {
|
message.thinking = match[2];
|
message.conclusion = match[4]
|
}
|
return message
|
}
|
|
|
/**
|
* 消息列表
|
*
|
* 和 {@link #getMessageList()} 的差异是,把 systemMessage 考虑进去
|
*/
|
const messageList = computed(() => {
|
if (activeMessageList.value.length > 0) {
|
activeMessageList.value[1].thinking = dealResultAndData(activeMessageList.value[1].content)
|
return activeMessageList.value
|
}
|
// 没有消息时,如果有 systemMessage 则展示它
|
if (activeConversation.value?.systemMessage) {
|
return [
|
{
|
id: 0,
|
type: 'system',
|
content: activeConversation.value.systemMessage
|
}
|
]
|
}
|
return []
|
})
|
|
//处理调度推理结论及数据
|
const dealResultAndData = (content: string) => {
|
const spliceText = content.includes("总结:") ? "总结:" : "结论:";
|
// 创建同时捕获前后内容的正则表达式
|
const regex = new RegExp(`^([\\s\\S]*?)${spliceText}([\\s\\S]*)$`);
|
const match = content.match(regex);
|
|
// 获取前面段落(优先返回匹配结果,若无匹配返回全文)
|
content = match ? match[1].trim() : content;
|
// 已存在的后面段落获取方式
|
const result = match ? match[2].trim() : '';
|
ddtlResult.value = result
|
const dataRegex = /转炉煤气回收情况:\s*((?:.*?)(?=\n\d\.|\n|$))/s;
|
const dataMatch = content.match(dataRegex);
|
const dataContent = dataMatch ? dataMatch[1] : '';
|
modelData.value = extractRecoveryDetails(dataContent, 78, 90);
|
if(modelData.value.schedule.length === 3) {
|
initLDGHSLYCChart()
|
}
|
initLDGGRQSYCChart()
|
return content
|
}
|
|
const extractRecoveryDetails = (text, consume, gui, totalMinutes = 60) => {
|
// 正则表达式匹配转炉数据块
|
const furnaceBlocks = Array.from(text.matchAll(/(\d+#转炉.*?)(?=\d+#转炉|$)/gs))
|
.map(match => match[0]);
|
|
// 初始化数据结构
|
const state = reactive({
|
allSchedule: [],
|
result: {},
|
totalRecovery: Array.from({length: totalMinutes}, () => [0]),
|
tankLevels: Array.from({length: totalMinutes + 1}, () => [0])
|
});
|
|
// 解析每个转炉的数据
|
furnaceBlocks.forEach(block => {
|
// 提取转炉编号
|
const furnaceNum = parseInt(block.match(/(\d+)#/)[1]);
|
|
// 解析时间段和回收量
|
const periods = Array.from(block.matchAll(/第(\d+)-(\d+)[^\d]+?(\d+\.?\d*)km³/gs))
|
.map(match => [
|
parseInt(match[1]),
|
parseInt(match[2]),
|
parseFloat(match[3])
|
]);
|
|
// 存储到结果
|
state.result[furnaceNum] = periods;
|
|
// 计算每分钟回收量
|
periods.forEach(([start, end, amount]) => {
|
const duration = end - start;
|
const perMin = amount / duration;
|
for(let t = start; t < end; t++) {
|
state.totalRecovery[t][0] += perMin;
|
}
|
});
|
});
|
|
// 生成0/1序列
|
furnaceBlocks.forEach(block => {
|
const schedule = new Array(totalMinutes).fill(0);
|
Array.from(block.matchAll(/第(\d+)-(\d+)/gs)).forEach(match => {
|
const start = parseInt(match[1]) - 1;
|
const end = parseInt(match[2]) - 1;
|
for(let i = start; i <= end; i++) {
|
if(i >= 0 && i < totalMinutes) schedule[i] = 1;
|
}
|
});
|
state.allSchedule.push(schedule);
|
});
|
|
// 计算柜位
|
let cumulative = 0;
|
const consumptionRate = consume / 60;
|
state.tankLevels = Array.from({length: totalMinutes + 1}, (_, t) => {
|
if(t > 0) cumulative += state.totalRecovery[t-1][0];
|
const consumed = consumptionRate * t;
|
return [Number((gui + cumulative - consumed).toFixed(2))];
|
});
|
|
// 格式化输出
|
return {
|
schedule: state.allSchedule,
|
result: state.result,
|
totalRecovery: state.totalRecovery.map(v => [Number(v[0].toFixed(2))]),
|
tankLevels: state.tankLevels
|
}
|
}
|
|
/** 处理删除 message 消息 */
|
const handleMessageDelete = () => {
|
if (conversationInProgress.value) {
|
message.alert('回答中,不能删除!')
|
return
|
}
|
// 刷新 message 列表
|
getMessageList()
|
}
|
|
/** 处理 message 清空 */
|
const handlerMessageClear = async () => {
|
if (!activeConversationId.value) {
|
return
|
}
|
try {
|
// 刷新 message 列表
|
activeMessageList.value = []
|
} catch {}
|
}
|
|
// =========== 【发送消息】相关 ===========
|
|
/** 处理来自 keydown 的发送消息 */
|
const handleSendByKeydown = async (event) => {
|
// 判断用户是否在输入
|
if (isComposing.value) {
|
return
|
}
|
// 进行中不允许发送
|
if (conversationInProgress.value) {
|
return
|
}
|
const content = prompt.value?.trim() as string
|
if (event.key === 'Enter') {
|
if (event.shiftKey) {
|
// 插入换行
|
prompt.value += '\r\n'
|
event.preventDefault() // 防止默认的换行行为
|
} else {
|
// 发送消息
|
await doSendMessage(content)
|
event.preventDefault() // 防止默认的提交行为
|
}
|
}
|
}
|
|
/** 处理来自【发送】按钮的发送消息 */
|
const handleSendByButton = () => {
|
doSendMessage(prompt.value?.trim() as string)
|
}
|
|
/** 处理 prompt 输入变化 */
|
const handlePromptInput = (event) => {
|
// 非输入法 输入设置为 true
|
if (!isComposing.value) {
|
// 回车 event data 是 null
|
if (event.data == null) {
|
return
|
}
|
isComposing.value = true
|
}
|
// 清理定时器
|
if (inputTimeout.value) {
|
clearTimeout(inputTimeout.value)
|
}
|
// 重置定时器
|
inputTimeout.value = setTimeout(() => {
|
isComposing.value = false
|
}, 400)
|
}
|
// TODO :是不是可以通过 @keydown.enter、@keydown.shift.enter 来实现,回车发送、shift+回车换行;主要看看,是不是可以简化 isComposing 相关的逻辑
|
const onCompositionstart = () => {
|
isComposing.value = true
|
}
|
const onCompositionend = () => {
|
// console.log('输入结束...')
|
setTimeout(() => {
|
isComposing.value = false
|
}, 200)
|
}
|
|
/** 真正执行【发送】消息操作 */
|
const doSendMessage = async (content: string) => {
|
// 校验
|
if (content.length < 1) {
|
message.error('发送失败,原因:内容为空!')
|
return
|
}
|
if (activeConversationId.value == null) {
|
message.error('还没创建对话,不能发送!')
|
return
|
}
|
// 发送请求时如果accessToken过期,无法中断请求,暂时增加请求前刷新token
|
authUtil.setToken(await refreshToken())
|
// 执行发送
|
await doSendMessageStream({
|
conversationId: activeConversationId.value,
|
content: content
|
} as ChatMessageVO)
|
}
|
|
/** 真正执行【发送】消息操作 */
|
const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
// 创建 AbortController 实例,以便中止请求
|
conversationInAbortController.value = new AbortController()
|
// 标记对话进行中
|
conversationInProgress.value = true
|
// 设置为空
|
receiveMessageFullText.value = ''
|
|
try {
|
// 1.0 每次发送消息前先将消息记录chat message清空
|
await handlerMessageClear()
|
// 1.1 先添加两个假数据,等 stream 返回再替换
|
activeMessageList.value.push({
|
id: -1,
|
conversationId: activeConversationId.value,
|
type: 'user',
|
content: userMessage.content,
|
createTime: new Date()
|
} as ChatMessageVO)
|
activeMessageList.value.push({
|
id: -2,
|
conversationId: activeConversationId.value,
|
type: 'assistant',
|
content: '思考中...',
|
createTime: new Date()
|
} as ChatMessageVO)
|
// 1.2 滚动到最下面
|
await nextTick()
|
await scrollToBottom() // 底部
|
// 1.3 开始滚动
|
textRoll()
|
|
// 2. 发送 event stream
|
let isFirstChunk = true // 是否是第一个 chunk 消息段
|
await ChatMessageApi.sendChatMessageStream(
|
userMessage.conversationId,
|
userMessage.content,
|
conversationInAbortController.value,
|
enableContext.value,
|
async (res) => {
|
const { code, data, msg } = JSON.parse(res.data)
|
if (code !== 0) {
|
message.alert(`对话异常! ${msg}`)
|
}
|
|
// 如果内容为空,就不处理。
|
if (data.receive.content === '') {
|
return
|
}
|
// 首次返回需要添加一个 message 到页面,后面的都是更新
|
if (isFirstChunk) {
|
isFirstChunk = false
|
// 弹出两个假数据
|
activeMessageList.value.pop()
|
activeMessageList.value.pop()
|
// 更新返回的数据
|
activeMessageList.value.push(data.send)
|
activeMessageList.value.push(data.receive)
|
}
|
// debugger
|
receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
|
// 滚动到最下面
|
await scrollToBottom()
|
},
|
(error) => {
|
message.alert(`对话异常! ${error}`)
|
stopStream()
|
},
|
() => {
|
stopStream()
|
}
|
)
|
} catch {}
|
}
|
|
/** 停止 stream 流式调用 */
|
const stopStream = async () => {
|
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
if (conversationInAbortController.value) {
|
conversationInAbortController.value.abort()
|
}
|
// 设置为 false
|
conversationInProgress.value = false
|
}
|
|
/** 编辑 message:设置为 prompt,可以再次编辑 */
|
const handleMessageEdit = (message: ChatMessageVO) => {
|
prompt.value = message.content
|
}
|
|
/** 刷新 message:基于指定消息,再次发起对话 */
|
const handleMessageRefresh = (message: ChatMessageVO) => {
|
doSendMessage(message.content)
|
}
|
|
// ============== 【消息滚动】相关 =============
|
|
/** 滚动到 message 底部 */
|
const scrollToBottom = async (isIgnore?: boolean) => {
|
await nextTick()
|
if (messageRef.value) {
|
messageRef.value.scrollToBottom(isIgnore)
|
}
|
}
|
|
/** 自提滚动效果 */
|
const textRoll = async () => {
|
let index = 0
|
try {
|
// 只能执行一次
|
if (textRoleRunning.value) {
|
return
|
}
|
// 设置状态
|
textRoleRunning.value = true
|
receiveMessageDisplayedText.value = ''
|
const task = async () => {
|
// 调整速度
|
const diff =
|
(receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10
|
if (diff > 5) {
|
textSpeed.value = 10
|
} else if (diff > 2) {
|
textSpeed.value = 30
|
} else if (diff > 1.5) {
|
textSpeed.value = 50
|
} else {
|
textSpeed.value = 100
|
}
|
// 对话结束,就按 30 的速度
|
if (!conversationInProgress.value) {
|
textSpeed.value = 10
|
}
|
|
if (index < receiveMessageFullText.value.length) {
|
receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
|
index++
|
|
// 更新 message
|
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
|
lastMessage.content = receiveMessageDisplayedText.value
|
// 滚动到住下面
|
await scrollToBottom()
|
// 重新设置任务
|
timer = setTimeout(task, textSpeed.value)
|
} else {
|
// 不是对话中可以结束
|
if (!conversationInProgress.value) {
|
textRoleRunning.value = false
|
clearTimeout(timer)
|
} else {
|
// 重新设置任务
|
timer = setTimeout(task, textSpeed.value)
|
}
|
}
|
}
|
let timer = setTimeout(task, textSpeed.value)
|
} catch {}
|
}
|
|
const LDGHSLYCEhartContainer = ref();
|
|
// 生成未来60秒的时间标签(LDG回收量预测)
|
const generateLDGHSLYCTimeLabels = () => {
|
const labels = [];
|
for (let i = 0; i < 60; i++) {
|
labels.push(i);
|
}
|
return labels;
|
};
|
|
// 生成未来60秒和过去60秒的时间标签
|
const generateLDGGRQSYCTimeLabels = () => {
|
const labels = [];
|
const now = new Date();
|
// 补零函数
|
const padZero = num => num.toString().padStart(2, '0')
|
for (let i = 0; i < 121; i++) {
|
const date = new Date(now.getTime() + (i-60) * 1000);
|
const formatted = `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())} ` +
|
`${padZero(date.getHours())}:${padZero(date.getMinutes())}:${padZero(date.getSeconds())}`;
|
labels.push(formatted);
|
}
|
return labels;
|
};
|
|
// 数据格式转换示例(LDG回收量预测)
|
const seriesHSLYCDataConverter = () => {
|
const recovery = modelData.value.totalRecovery
|
const totalRecovery = []
|
recovery.forEach(item => {
|
totalRecovery.push(item[0])
|
})
|
return totalRecovery;
|
};
|
|
const seriesHSLYCDataSchedule = (type) => {
|
const max = LDGMaxTotalValue()
|
const schedule = modelData.value.schedule[type]
|
// 返回对象格式数据,包含原始值和基准值
|
const baseline = round(max, 0) + (6 - 2 * type)
|
return schedule.map(item => ({
|
value: item + baseline, // 显示值 = 原始值 + 基准值
|
original: item // 原始值
|
}));
|
};
|
|
// 计算总回收量的最大值
|
const LDGMaxTotalValue = () => {
|
const total = modelData.value.totalRecovery
|
let returnValue = computed(() => {
|
return Math.max(...total)
|
})
|
return returnValue.value
|
};
|
|
// 计算柜容预测趋势的最大值和最小值,用于上下界限显示
|
const LDGComputedValue = (type) => {
|
const tank = modelData.value.tankLevels
|
let returnValue = 0;
|
if(type == 'max') {
|
returnValue = computed(() => {
|
return Number((Math.max(...tank) + 20).toFixed(0))
|
})
|
} else if(type == 'min') {
|
returnValue = computed(() => {
|
return Number((Math.min(...tank) - 60).toFixed(0))
|
})
|
} else if(type == 'average') {
|
returnValue = computed(() => {
|
let sum = 0
|
tank.forEach((item) => {
|
sum += item[0]
|
})
|
return Number((sum / tank.length).toFixed(0));
|
})
|
}
|
return returnValue.value
|
};
|
|
// 数据格式转换示例(LDG柜容趋势预测)
|
const seriesGRQSYCDataConverter = () => {
|
const tank = modelData.value.tankLevels
|
const tankLevels = []
|
tank.forEach(item => {
|
tankLevels.push(item[0])
|
})
|
return tankLevels;
|
};
|
|
// 图表配置
|
const initLDGHSLYCChart = () => {
|
const LDGHSLYCChart = echarts.init(LDGHSLYCEhartContainer.value);
|
const option = {
|
tooltip: {
|
trigger: 'axis',
|
formatter: function (params) {
|
let tooltipContent = params[0].name + '<br/>'; // 时间标签
|
params.forEach(param => {
|
const seriesName = param.seriesName;
|
let originalValue;
|
|
// 判断是否为转炉系列
|
if (seriesName.includes('转炉')) {
|
// 直接从数据项中获取原始值
|
originalValue = param.data.original;
|
tooltipContent += `
|
${param.marker}
|
${seriesName}: ${originalValue.toFixed(0)}<br/>
|
`;
|
} else {
|
// 总回收量直接显示值
|
originalValue = param.value;
|
tooltipContent += `
|
${param.marker}
|
${seriesName}: ${originalValue.toFixed(2)}<br/>
|
`;
|
}
|
});
|
return tooltipContent;
|
}
|
},
|
grid: {
|
left: 25,
|
right: 25,
|
bottom: 10,
|
top: 30,
|
containLabel: true
|
},
|
legend: {
|
top: 10,
|
right: 10,
|
data: ['1#转炉', '2#转炉', '3#转炉', '总回收量'],
|
textStyle: {
|
color: '#8FD6FE'
|
},
|
itemWidth: 20, // 图例标记的宽度
|
itemHeight: 0, // 图例标记的高度,设为较小值使其更像线条
|
},
|
xAxis: {
|
type: 'category',
|
boundaryGap: false,
|
data: generateLDGHSLYCTimeLabels(),
|
axisTick: {
|
show: false
|
},
|
axisLine: {
|
lineStyle: {
|
color: '#C7E7FF'
|
}
|
},
|
axisLabel: {
|
color: '#fff'
|
}
|
},
|
yAxis: {
|
type: 'value',
|
min: 0,
|
axisLine: {
|
show: true,
|
lineStyle: {
|
color: '#C7E7FF',
|
}
|
},
|
axisTick: {
|
show: false
|
},
|
axisLabel: {
|
show: false
|
},
|
splitLine: {
|
show: false // Y轴网格线
|
}
|
},
|
series: [
|
{
|
name: '1#转炉',
|
type: 'line',
|
step: 'start',
|
data: seriesHSLYCDataSchedule(0),
|
showSymbol: false, // 取消数据点
|
emphasis: {
|
focus: 'series'
|
},
|
lineStyle: {
|
color: '#FF7686' // 粉色
|
}
|
},
|
{
|
name: '2#转炉',
|
type: 'line',
|
step: 'start',
|
data: seriesHSLYCDataSchedule(1),
|
showSymbol: false, // 取消数据点
|
emphasis: {
|
focus: 'series'
|
},
|
lineStyle: {
|
color: '#49FFD3' // 绿色
|
}
|
},
|
{
|
name: '3#转炉',
|
type: 'line',
|
step: 'start',
|
showSymbol: false, // 取消数据点
|
data: seriesHSLYCDataSchedule(2),
|
color: '#FAC858',
|
emphasis: {
|
focus: 'series'
|
},
|
lineStyle: {
|
color: '#FFAE81' // 橙色
|
},
|
},
|
{
|
name: '总回收量',
|
type: 'line',
|
step: 'start',
|
showSymbol: false, // 取消数据点
|
data: seriesHSLYCDataConverter(),
|
emphasis: {
|
focus: 'series'
|
},
|
lineStyle: {
|
color: 'white'
|
},
|
}
|
]
|
};
|
LDGHSLYCChart.setOption(option);
|
};
|
|
const LDGGRYCEhartContainer = ref();
|
|
/** 带预测的转炉数据阶梯图配置 */
|
const initLDGGRQSYCChart = () => {
|
const labels = generateLDGGRQSYCTimeLabels()
|
const tankLevels = seriesGRQSYCDataConverter()
|
const fullData = [];
|
for(let i = 0; i < 121; i ++ ) {
|
let value = 90
|
if(i >= 60) {
|
value = tankLevels[i - 60]
|
}
|
fullData.push([labels[i], value]);
|
}
|
|
const splitTime = formatToDateTime(new Date()); // 分割时间点(当前时间)
|
const upperLimit = LDGComputedValue('max');
|
const lowerLimit = LDGComputedValue('min');
|
const averageValue = LDGComputedValue('average');
|
|
// 分割真实数据和预测数据
|
const splitIndex = fullData.findIndex(item => item[0] === splitTime);
|
const realData = fullData.slice(0, splitIndex + 1);
|
const predictData = fullData.slice(splitIndex);
|
|
// 创建纯净的上下限数据数组
|
const upperLimitData = [
|
[labels[0], upperLimit],
|
[labels[labels.length - 1], upperLimit]
|
];
|
|
const lowerLimitData = [
|
[labels[0], lowerLimit],
|
[labels[labels.length - 1], lowerLimit]
|
];
|
|
const LDGGRQSYCChart = echarts.init(LDGGRYCEhartContainer.value);
|
const option = {
|
grid: {
|
left: 0,
|
right: 0,
|
bottom: 10,
|
top: 20,
|
containLabel: true
|
},
|
tooltip: {
|
trigger: 'axis'
|
},
|
xAxis: {
|
type: 'time',
|
axisLabel: {
|
formatter: (value) => {
|
return echarts.time.format(value, '{mm}:{ss}', false)
|
},
|
color: '#C7E7FF'
|
},
|
axisLine: {
|
show: true,
|
lineStyle: {
|
color: '#C7E7FF'
|
}
|
},
|
axisTick: {
|
show: false
|
},
|
splitLine: {
|
show: false
|
}
|
},
|
yAxis: {
|
type: 'value',
|
min: 0,
|
max: LDGComputedValue('max') + 30,
|
axisLine: {
|
show: true,
|
lineStyle: {
|
color: '#C7E7FF'
|
}
|
},
|
axisTick: {
|
show: false
|
},
|
splitLine: {
|
show: false
|
}
|
},
|
series: [
|
// 真实数据(实线)
|
{
|
type: 'line',
|
data: realData,
|
smooth: true,
|
symbol: 'none',
|
lineStyle: { color: '#95E6FF' },
|
markLine: {
|
symbol: ['none', 'none'],
|
label: {
|
show: false
|
},
|
data: [{
|
xAxis: splitTime,
|
lineStyle: {
|
type: 'solid',
|
color: '#5DFF9E',
|
width: 1
|
}
|
}]
|
}
|
},
|
// 预测数据(虚线)
|
{
|
type: 'line',
|
data: predictData,
|
smooth: true,
|
symbol: 'none',
|
lineStyle: {
|
type: 'dashed',
|
color: '#E76666',
|
dashOffset: 5
|
}
|
},
|
// 上限填充
|
{
|
type: 'line',
|
data: upperLimitData,
|
lineStyle: { width: 0 },
|
areaStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: 'rgba(255,0,0,0.2)' }, // 顶部颜色
|
{ offset: 1, color: 'rgba(255,0,0,0.5)' } // 底部颜色(到upperLimit)
|
]),
|
origin: 'end' // 关键:从线条向上填充
|
},
|
silent: true
|
},
|
// 下限填充
|
{
|
type: 'line',
|
data: lowerLimitData,
|
lineStyle: { width: 0 },
|
areaStyle: {
|
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
|
{ offset: 0, color: 'rgba(0,0,255,0.2)' },
|
{ offset: 1, color: 'rgba(0,0,255,0.5)' }
|
])
|
},
|
silent: true
|
},
|
// 辅助线系列
|
{
|
type: 'line',
|
symbol: 'none', // 关闭辅助线系列自身的数据点
|
markLine: {
|
symbol: ['none', 'none'], // 全局隐藏标记点
|
data: [
|
// 上限辅助线配置部分
|
{
|
yAxis: upperLimit,
|
symbol: 'none',
|
label: { show: false },
|
emphasis: {
|
// 这里添加让高亮时标记点也不显示
|
itemStyle: {
|
borderWidth: 0,
|
borderColor: 'transparent',
|
color: 'transparent'
|
},
|
label: { show: false }
|
},
|
lineStyle: { color: 'rgba(255,0,0)', width: 1, type: 'solid' }
|
},
|
// 下限辅助线配置部分
|
{
|
yAxis: lowerLimit,
|
symbol: 'none',
|
label: { show: false },
|
emphasis: {
|
// 这里添加让高亮时标记点也不显示
|
itemStyle: {
|
borderWidth: 0,
|
borderColor: 'transparent',
|
color: 'transparent'
|
},
|
label: { show: false }
|
},
|
lineStyle: { color: 'rgba(0,0,255)', width: 1, type: 'solid' }
|
},
|
{
|
yAxis: averageValue,
|
label: { show: false },
|
lineStyle: { type: 'dashed', color: 'rgba(0,194,255,0.52)', width: 1 }
|
}
|
]
|
}
|
}
|
]
|
};
|
LDGGRQSYCChart.setOption(option);
|
}
|
|
const isFullscreen = ref(false);
|
|
// 核心:统一检测全屏状态
|
const updateFullscreenStatus = () => {
|
// 同时检测 API 全屏和 F11 全屏(近似)
|
isFullscreen.value = !!document.fullscreenElement || window.outerHeight === screen.height;
|
};
|
|
// 监听全屏 API 变化
|
const handleFullscreenChange = () => {
|
updateFullscreenStatus();
|
};
|
|
// 监听 F11 按键(兜底)
|
const handleKeyPress = (e) => {
|
if (e.key === 'F11') {
|
e.preventDefault(); // 尝试阻止默认行为(部分浏览器允许)
|
setTimeout(updateFullscreenStatus, 100); // 延迟确保状态更新
|
}
|
};
|
|
// 监听窗口大小变化(F11 全屏会触发)
|
const handleResize = () => {
|
updateFullscreenStatus();
|
};
|
|
// 切换全屏(API 方式)
|
const toggleFullscreen = async () => {
|
if (!document.fullscreenElement) {
|
await document.documentElement.requestFullscreen();
|
} else {
|
await document.exitFullscreen();
|
}
|
};
|
|
//初始化全屏信息
|
const initFullscreen = async () => {
|
// Fullscreen API 事件
|
const events = ['fullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'];
|
events.forEach(event => {
|
document.addEventListener(event, handleFullscreenChange);
|
});
|
|
// 窗口变化 + 键盘事件兜底
|
window.addEventListener('resize', handleResize);
|
window.addEventListener('keydown', handleKeyPress);
|
|
// 初始状态检测
|
updateFullscreenStatus();
|
}
|
|
/** 初始化 **/
|
onMounted(async () => {
|
await initFullscreen()
|
// 如果有 conversationId 参数,则默认选中
|
if (route.query.conversationId) {
|
const id = route.query.conversationId as unknown as number
|
activeConversationId.value = id
|
await getConversation(id)
|
}
|
|
// 获取列表数据
|
activeMessageListLoading.value = true
|
await getMessageList()
|
})
|
|
// 清理监听
|
onUnmounted(() => {
|
console.log('stopStream')
|
const events = ['fullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'];
|
events.forEach(event => {
|
document.removeEventListener(event, handleFullscreenChange);
|
});
|
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('keydown', handleKeyPress);
|
});
|
|
</script>
|
|
<style lang="scss" scoped>
|
.gas-scheduling-container {
|
display: flex;
|
font-family: Microsoft YaHei, Microsoft YaHei;
|
.fullscreen-btn {
|
background-color: transparent;
|
border: 1px solid rgba(115, 196, 255, 0.5);
|
position: fixed;
|
margin-top: 3.5%;
|
margin-left: 28%;
|
color: rgba(115, 196, 255, 0.8);
|
font-size: 12px;
|
}
|
/* 背景层容器 */
|
&::before {
|
content: '';
|
position: fixed;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
z-index: -1; /* 置于内容层下方 */
|
background:
|
url("@/assets/ai/zhuanlu/bg.png") center/cover no-repeat,
|
linear-gradient(to bottom, #0a1633dd, #0a1633dd); /* 叠加深色遮罩 */
|
pointer-events: none; /* 防止遮挡交互 */
|
}
|
.gas-scheduling-left {
|
width: 23%;
|
height: 89%;
|
margin: 2rem 2rem 0 1.8rem;
|
z-index: 1;
|
background-color: rgba(0, 0, 0, 0); /* 透明背景 */
|
.data1-item {
|
height: 2.6rem;
|
width: 42%;
|
display: inline-block;
|
margin: 8px 10px ;
|
background: url("@/assets/ai/zhuanlu/data_bg1.png") center/cover no-repeat;
|
}
|
.data2-item {
|
height: 30px;
|
width: 42%;
|
display: inline-block;
|
margin: 6px 8px;
|
background: url("@/assets/ai/zhuanlu/data_bg2.png") center/cover no-repeat;
|
}
|
.content {
|
margin-left: 16px;
|
.value {
|
span:nth-child(1){
|
height: 19px;
|
font-weight: bold;
|
font-size: 16px;
|
color: #FFAE81;
|
line-height: 19px;
|
}
|
span:nth-child(2) {
|
height: 16px;
|
font-weight: 400;
|
font-size: 12px;
|
color: #C7E7FF;
|
}
|
}
|
.name {
|
height: 16px;
|
font-weight: 400;
|
font-size: 12px;
|
color: #C7E7FF;
|
}
|
}
|
.content2 {
|
display: flex;
|
width: 11rem;
|
margin-left: 10px;
|
.name {
|
width: 95px;
|
height: 18px;
|
font-weight: 400;
|
font-size: 14px;
|
color: #C7E7FF;
|
}
|
.value {
|
margin-left: auto;
|
margin-right: 5px;
|
span:nth-child(1){
|
height: 15px;
|
font-weight: bold;
|
font-size: 15px;
|
color: #FFAE81;
|
line-height: 15px;
|
}
|
span:nth-child(2) {
|
height: 15px;
|
font-weight: 400;
|
font-size: 12px;
|
color: #C7E7FF;
|
}
|
}
|
}
|
#mqhsssxx {
|
.title {
|
height: 2rem;
|
background:
|
url("@/assets/ai/zhuanlu/mqhsssxx_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
|
}
|
}
|
#tsxx {
|
.title {
|
margin-top: 5px;
|
height: 2rem;
|
background:
|
url("@/assets/ai/zhuanlu/tsxx_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
|
}
|
}
|
#zlxx {
|
.title {
|
margin-top: 5px;
|
height: 2rem;
|
background:
|
url("@/assets/ai/zhuanlu/zlxx_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
|
}
|
:deep(.el-table) {
|
font-weight: 400;
|
font-size: 14px;
|
color: #DBEEFF;
|
text-align: left;
|
background-color: transparent !important;
|
}
|
|
.transparent-table {
|
margin-top: 14px;
|
}
|
|
/* 行样式 */
|
:deep(.el-table .el-table__body tr) {
|
border: 1px solid rgba(16, 198, 255, 0.3);
|
margin-bottom: 4px;
|
border-radius: 4px;
|
overflow: hidden;
|
background-color: transparent;
|
}
|
|
/* 行内容样式 */
|
:deep(.el-table .el-table__body td) {
|
padding: 4px 0;
|
color: #FFAA5D;
|
}
|
|
/* 列头样式 */
|
:deep(.el-table th) {
|
color: #8FD6FE;
|
background:
|
url("@/assets/ai/zhuanlu/table_header_bg.png") center/cover no-repeat !important; /* 叠加深色遮罩 */
|
border: none;
|
}
|
|
:deep(.el-table tr) {
|
background: transparent;
|
}
|
|
/* 行头样式 */
|
:deep(.el-table .el-table__body td:first-child) {
|
color: #8FD6FE;
|
font-weight: 500;
|
background-color: transparent;
|
}
|
|
:deep(.el-table .el-table__body tr:nth-child(even) td) {
|
background-color: rgba(0, 194, 255, 0.1);
|
}
|
|
:deep(.el-table .el-table__body tr:nth-child(odd) td) {
|
background-color: rgba(0, 194, 255, 0.2);
|
}
|
|
/* 移除表格内部边框线 */
|
:deep(.el-table td, .el-table th.is-leaf) {
|
border-bottom: none;
|
}
|
|
:deep(.el-table .el-table__inner-wrapper:before) {
|
background-color: transparent !important;
|
}
|
}
|
#mqxhssxx {
|
.title {
|
height: 30px;
|
margin-top: 15px;
|
background:
|
url("@/assets/ai/zhuanlu/mqxhssxx_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
|
}
|
}
|
}
|
|
.gas-scheduling-center {
|
margin-top: 2.6rem;
|
width: 55.5rem !important;
|
.mode-switch {
|
margin-top: 20px;
|
margin-left: 43rem;
|
font-weight: 400;
|
font-size: 14px;
|
color: #73C4FF;
|
width: 40%;
|
/* 必须穿透到组件内部层级 */
|
:deep(.custom-radio-group) {
|
--el-color-primary: red !important; /* 强制修改主题色变量 */
|
}
|
|
/* 所有按钮基础样式 */
|
:deep(.custom-radio-group .el-radio-button__inner) {
|
background: black !important; /* 未选中黑色背景 */
|
border: 1px solid rgba(173, 216, 230, 0.3) !important; /* 蓝色边框 */
|
font-weight: 300;
|
font-size: 14px;
|
color: #DBEEFF;
|
transition: all 0.3s;
|
}
|
|
/* 选中状态 */
|
:deep(.custom-radio-group .el-radio-button.is-active .el-radio-button__inner) {
|
background: #b92220 !important;
|
color: gold !important;
|
font-weight: bolder;
|
}
|
|
/* 强制覆盖原生选中状态 */
|
:deep(.custom-radio-group .el-radio-button__orig-radio:checked + .el-radio-button__inner) {
|
background: inherit !important; /* 继承上层样式 */
|
}
|
}
|
|
// 头部
|
.detail-container {
|
margin-left: 5px;
|
background-color: rgba(0, 0, 0, 0); /* 透明背景 */
|
z-index: 1;
|
.header {
|
display: flex;
|
flex-direction: row;
|
align-items: center;
|
justify-content: space-between;
|
box-shadow: 0 0 0 0 #dcdfe6;
|
|
.title {
|
font-size: 18px;
|
font-weight: bold;
|
}
|
|
.btns {
|
display: flex;
|
width: 300px;
|
flex-direction: row;
|
justify-content: flex-end;
|
//justify-content: space-between;
|
|
.btn {
|
padding: 10px;
|
}
|
}
|
}
|
}
|
|
// main 容器
|
.main-container {
|
margin: 0;
|
padding: 0;
|
position: relative;
|
height: 30rem;
|
|
.message-container {
|
position: absolute;
|
top: 0;
|
bottom: 0;
|
left: 0;
|
right: 0;
|
overflow-y: hidden;
|
padding: 0;
|
margin: 0;
|
}
|
.title {
|
background: url("@/assets/ai/zhuanlu/think_bg.png") center/cover no-repeat;
|
width: auto;
|
height: 1.8rem;
|
font-weight: 400;
|
font-size: 14px;
|
color: #8FD6FE;
|
text-align: left;
|
font-style: normal;
|
text-transform: none;
|
span {
|
margin-left: 30px;
|
}
|
}
|
}
|
|
.result-container-title {
|
margin-top: 15px;
|
background: url("@/assets/ai/zhuanlu/ddtljl_result_title.png") center/cover no-repeat;
|
width: auto;
|
height: 1.8rem;
|
font-weight: 400;
|
font-size: 14px;
|
color: #8FD6FE;
|
text-align: left;
|
font-style: normal;
|
text-transform: none;
|
span {
|
margin-left: 30px;
|
}
|
.history-button {
|
color: rgba(143, 214, 254);
|
font-weight: bold;
|
float: right;
|
margin-right: 5px;
|
background-color: rgba(0, 255, 255, 0.1);
|
border-radius: 3px;
|
padding: 0 5px;
|
border: none;
|
cursor: pointer
|
}
|
.history-button:hover {
|
color: rgba(143, 214, 254, 0.5);
|
}
|
}
|
// main 容器
|
.result-container {
|
padding: 0;
|
position: relative;
|
width: 100%;
|
/* WebKit */
|
&::-webkit-scrollbar {
|
width: 6px;
|
background: transparent;
|
}
|
&::-webkit-scrollbar-thumb {
|
border-radius: 4px;
|
background: rgba(0, 0, 0, 0.15);
|
transition: background 0.3s;
|
&:hover { background: rgba(0, 0, 0, 0.25); }
|
}
|
.result {
|
margin-top: 10px;
|
margin-left: 18px;
|
border-left: 1px solid #73C4FF;
|
.result-content {
|
width: 53rem;
|
height: 6rem;
|
margin-left: 10px;
|
font-weight: 400;
|
font-size: 14px;
|
background-color: rgba(219,238,255,0);
|
line-height: 21px;
|
text-align: left;
|
font-style: normal;
|
text-transform: none;
|
border: 0;
|
color: rgba(219,238,255,0.6);
|
}
|
.result-content:focus {
|
outline: none;
|
}
|
}
|
}
|
|
// 输入框
|
.input-container {
|
display: flex;
|
flex-direction: column;
|
height: auto;
|
margin: 0;
|
padding: 0;
|
overflow-y: auto; /* 垂直方向溢出时显示滚动条 */
|
overflow-x: hidden; /* 水平方向隐藏滚动条 */
|
/* Firefox */
|
scrollbar-width: thin;
|
scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
|
|
/* WebKit */
|
&::-webkit-scrollbar {
|
width: 6px;
|
background: transparent;
|
}
|
&::-webkit-scrollbar-thumb {
|
border-radius: 4px;
|
background: rgba(0, 0, 0, 0.15);
|
transition: background 0.3s;
|
&:hover { background: rgba(0, 0, 0, 0.25); }
|
}
|
|
.prompt-from {
|
display: flex;
|
flex-direction: column;
|
margin: 10px 20px 20px 20px;
|
padding: 9px 10px;
|
width: 53.9rem;
|
background: rgba(115,196,255,0.05);
|
border-radius: 4px 4px 4px 4px;
|
border: 1px solid #73C4FF;
|
}
|
|
.prompt-input {
|
width: 53rem;
|
height: 11rem;
|
font-weight: 400;
|
font-size: 14px;
|
background-color: rgba(219,238,255,0);
|
line-height: 21px;
|
text-align: left;
|
font-style: normal;
|
text-transform: none;
|
border: 0;
|
color: rgba(219,238,255,0.6);
|
}
|
|
.prompt-input:focus {
|
outline: none;
|
}
|
|
.prompt-btns {
|
display: flex;
|
justify-content: space-between;
|
padding-bottom: 0;
|
padding-top: 5px;
|
.content {
|
/* 默认状态 */
|
.el-button {
|
background: transparent !important;
|
border-color: rgba(115, 196, 255, 0.5);
|
color: #73C4FF;
|
border-radius: 15px !important;
|
}
|
|
/* 上下文图标处理 */
|
.content-icon {
|
color: blue; /* 图标颜色 */
|
font-size: 18px;
|
margin-right: 10px;
|
background: url("@/assets/ai/zhuanlu/content.png");
|
vertical-align: middle;
|
}
|
|
/* 选中状态 */
|
.active-button {
|
background: #409eff !important;
|
border-color: #409eff !important;
|
color: white !important;
|
.content-icon {
|
background: url("@/assets/ai/zhuanlu/content_select.png");
|
vertical-align: middle;
|
}
|
}
|
|
/* 按钮组间距处理 */
|
.button-group .el-button {
|
margin-left: 0;
|
border-radius: 4px;
|
}
|
|
/* 悬停效果 */
|
.el-button:not(.active-button):hover {
|
border-color: rgba(115,196,255,0.5);
|
color: #409eff;
|
}
|
}
|
.message {
|
/* 所有状态通用透明背景 */
|
:deep(.el-button) {
|
background: rgba(73, 254, 210, 0.8) !important;
|
border-color: currentColor; /* 保持与文字同色 */
|
font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
|
font-weight: bold;
|
font-size: 16px;
|
color: #123C4E;
|
clip-path: polygon(
|
0 0,
|
100% 0,
|
100% 100%,
|
10px 100%, /* 右下方向留出10px */
|
0 calc(100% - 10px) /* 左上方向留出10px */
|
);
|
position: relative;
|
padding-left: 15px; /* 增加右侧留白 */
|
}
|
|
/* 悬停状态 */
|
:deep(.el-button:hover) {
|
background: rgba(73, 254, 210, 0.6) !important; /* 轻微悬停反馈 */
|
}
|
|
/* 点击状态 */
|
:deep(.el-button:active) {
|
background: rgba(73, 254, 210, 1) !important;
|
}
|
|
/* 禁用状态 */
|
:deep(.el-button.is-disabled) {
|
opacity: 0.6;
|
background: transparent !important;
|
}
|
|
/* 核心样式覆盖 */
|
:deep(.el-switch__core) {
|
background: transparent !important;
|
border-radius: 0 0 15px 0 !important;
|
border: none !important;
|
height: 40px !important;
|
}
|
|
/* 按钮内容容器 */
|
.button-content {
|
display: flex;
|
align-items: center;
|
padding: 0 15px;
|
height: 100%;
|
}
|
}
|
}
|
}
|
}
|
|
.gas-scheduling-right {
|
width: 22%;
|
height: 89%;
|
margin-left: 4.3rem;
|
margin-top: 2.8rem;
|
z-index: 1;
|
background-color: rgba(0, 0, 0, 0); /* 透明背景 */
|
|
#ldghslyc {
|
.title {
|
height: 1.8rem;
|
background:
|
url("@/assets/ai/zhuanlu/ldghslyc_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
|
}
|
}
|
#ldggrqsyc {
|
.title {
|
height: 1.8rem;
|
background:
|
url("@/assets/ai/zhuanlu/ldggrqsyc_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
|
}
|
}
|
#mqhsjhxx {
|
.title {
|
height: 1.8rem;
|
background:
|
url("@/assets/ai/zhuanlu/mqhsjhxx_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
|
}
|
.time-content {
|
display: inline-block;
|
width: 32%;
|
height: 2.9rem;
|
margin: 10px 0 5px 5px;
|
font-weight: 400;
|
font-size: 12px;
|
color: #C7E7FF;
|
.time-content-item {
|
display: flex;
|
.name {
|
width: 1.6rem;
|
height: 3rem;
|
padding: 8px 3px;
|
background: linear-gradient( 180deg, rgba(115,196,255,0.1) 0%, rgba(255,136,69,0.1) 100%);
|
border-radius: 2px 2px 2px 2px;
|
border: 1px solid rgba(255,255,255,0.15);
|
opacity: 0.9;
|
}
|
.name span {
|
height: 1.8rem;
|
font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
|
font-weight: bold;
|
font-size: 14px;
|
color: #C7E7FF;
|
line-height: 15px;
|
text-align: left;
|
font-style: normal;
|
text-transform: none;
|
}
|
.time {
|
width: 105px;
|
height: 38px;
|
margin: 4px;
|
display: inline-block;
|
.in-pot{
|
width: 4px;
|
height: 4px;
|
background: #49FFD3;
|
border-radius: 80px 80px 80px 80px;
|
margin-top: 7px;
|
margin-right: 3px;
|
}
|
.out-pot{
|
width: 4px;
|
height: 4px;
|
background: #FFAE81;
|
border-radius: 80px 80px 80px 80px;
|
margin-top: 7px;
|
margin-right: 3px;
|
}
|
}
|
.time > div {
|
float: left;
|
height: 25px;
|
}
|
}
|
}
|
.data2-item {
|
height: 2.8rem;
|
width: 45%;
|
display: inline-block;
|
margin: 6px 8px;
|
background: url("@/assets/ai/zhuanlu/data_bg3.png") no-repeat;
|
}
|
.content {
|
display: flex;
|
width: 192px;
|
margin-left: 10px;
|
|
.name {
|
width: 95px;
|
height: 18px;
|
font-weight: 400;
|
font-size: 14px;
|
color: #C7E7FF;
|
}
|
|
.value {
|
margin-top: 10px;
|
margin-left: auto;
|
margin-right: 5px;
|
span:nth-child(1) {
|
height: 15px;
|
font-weight: bold;
|
font-size: 15px;
|
color: #FFAE81;
|
line-height: 15px;
|
}
|
|
span:nth-child(2) {
|
height: 15px;
|
font-weight: 400;
|
font-size: 12px;
|
color: #C7E7FF;
|
}
|
}
|
}
|
}
|
#scmbyyxzb {
|
margin-top: 10px;
|
.title {
|
height: 1.8rem;
|
background:
|
url("@/assets/ai/zhuanlu/scmbyyxzb_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
|
}
|
.little-title {
|
font-size: 14px;
|
color: #8FD6FE;
|
margin: 10px;
|
}
|
.data3-item {
|
height: 5.2rem;
|
width: 30%;
|
display: inline-block;
|
margin: 0 6px 6px 6px;
|
background: url("@/assets/ai/zhuanlu/data_bg4.png") center/cover no-repeat;
|
.name {
|
font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
|
font-weight: bold;
|
font-size: 16px;
|
color: #FFFFFF;
|
margin-left: 3px;
|
}
|
.value {
|
color: #DBEEFF;
|
font-size: 14px;
|
margin-left: 3px;
|
}
|
.value-content {
|
color: #8FD6FE;
|
font-size: 12px;
|
margin-left: 3px;
|
}
|
}
|
.zb-content {
|
display: inline-block;
|
.item {
|
float: left;
|
}
|
.data4-item {
|
height: 2.2rem;
|
width: 7.8rem;
|
display: inline-block;
|
margin: 5px 0;
|
.content {
|
margin-left: 16px;
|
.value {
|
text-align: center;
|
span:nth-child(1){
|
height: 19px;
|
font-weight: bold;
|
font-size: 16px;
|
color: #FFAE81;
|
line-height: 19px;
|
}
|
span:nth-child(2) {
|
height: 16px;
|
font-weight: 400;
|
font-size: 12px;
|
color: #C7E7FF;
|
}
|
}
|
.name {
|
font-weight: 400;
|
font-size: 12px;
|
color: #C7E7FF;
|
}
|
}
|
.content div {
|
height: 25px;
|
}
|
}
|
.left-label {
|
width: 1.2rem;
|
height: 3.5rem;
|
background: url("@/assets/ai/zhuanlu/left_label.png") center/cover no-repeat;
|
}
|
.right-label {
|
width: 1.2rem;
|
height: 3.5rem;
|
background: url("@/assets/ai/zhuanlu/right_label.png") center/cover no-repeat;
|
}
|
}
|
}
|
}
|
}
|
|
/* 背景颜色修改 */
|
:deep(.el-progress .el-progress-bar .el-progress-bar__outer) {
|
width: 90%;
|
margin: 5px 0 2px 5px;
|
background-color: rgba(64, 158, 255, 0.3) !important;
|
border-radius: 2px;
|
}
|
|
/* 进度条填充颜色 */
|
:deep(.el-progress-bar__inner) {
|
border-radius: 2px;
|
transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1);
|
}
|
|
</style>
|