c104bf2b4ecf604245b38590bf1e8119530de10b..5e3f6dc1253883bbdef87974cfa950171f87f9ec
2025-08-13 spring
Merge remote-tracking branch 'origin/dev_7004' into dev_7004
5e3f6d 对比 | 目录
2025-08-13 spring
完成能源驾驶舱
c2f399 对比 | 目录
2025-08-13 gaoluyang
路由修改
40e411 对比 | 目录
2025-08-13 gaoluyang
演示部署修改
291a88 对比 | 目录
2025-08-13 gaoluyang
演示部署修改
37b9e8 对比 | 目录
2025-08-13 gaoluyang
Merge remote-tracking branch 'origin/dev_ai' into dev_7004
276c0a 对比 | 目录
2025-08-13 spring
Merge remote-tracking branch 'origin/dev_7004' into dev_7004
3e4bb6 对比 | 目录
2025-08-13 spring
完成用气管理
64adb4 对比 | 目录
2025-08-13 gaoluyang
设备监控页面修改
ff32f4 对比 | 目录
2025-08-13 gaoluyang
设备监控页面添加
a36ebc 对比 | 目录
2025-08-13 gaoluyang
安全监控页面添加
440f11 对比 | 目录
2025-08-13 gaoluyang
Merge remote-tracking branch 'origin/dev_7004' into dev_7004
872439 对比 | 目录
2025-08-13 gaoluyang
采购管理添加生成二维码功能
a44462 对比 | 目录
2025-08-13 spring
公出管理/请假/出差/报销
2a0782 对比 | 目录
2025-08-13 gaoluyang
代码合并
0523f4 对比 | 目录
2025-08-13 maven
Merge remote-tracking branch 'origin/dev_ai' into dev_ai
77141d 对比 | 目录
2025-08-13 maven
yys 合同管理增加附件上传功能
d71769 对比 | 目录
2025-08-12 gaoluyang
瓜州县弘也水泥有限责任公司部署
df2103 对比 | 目录
2025-08-11 spring
修改原材料检验导出、修改权限
5403df 对比 | 目录
已修改15个文件
已添加9个文件
3690 ■■■■■ 文件已修改
.env.development 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.staging 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/energyManagement/waterManagement.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index1.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index2.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index3.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index4.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/energyCockpit/index.vue 1380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/gasManagement/index.vue 624 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/iotMonitor/index.vue 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Form.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/filesDia.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/safetyMonitoring/index.vue 873 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -1,8 +1,8 @@
# é¡µé¢æ ‡é¢˜
VITE_APP_TITLE = å¼˜ä¹Ÿæ°´æ³¥ç®¡ç†ç³»ç»Ÿ
VITE_APP_TITLE = ä¸­å°ä¼ä¸šæ•°å­—化转型套餐包
# å¼€å‘环境配置
VITE_APP_ENV = 'development'
# å¼˜ä¹Ÿæ°´æ³¥ç®¡ç†ç³»ç»Ÿ/开发环境
# ä¸­å°ä¼ä¸šæ•°å­—化转型套餐包/开发环境
VITE_APP_BASE_API = '/dev-api'
.env.production
@@ -1,10 +1,10 @@
# é¡µé¢æ ‡é¢˜
VITE_APP_TITLE = å¼˜ä¹Ÿæ°´æ³¥ç®¡ç†ç³»ç»Ÿ
VITE_APP_TITLE = ä¸­å°ä¼ä¸šæ•°å­—化转型套餐包
# ç”Ÿäº§çŽ¯å¢ƒé…ç½®
VITE_APP_ENV = 'production'
# å¼˜ä¹Ÿæ°´æ³¥ç®¡ç†ç³»ç»Ÿ/生产环境
# ä¸­å°ä¼ä¸šæ•°å­—化转型套餐包/生产环境
VITE_APP_BASE_API = '/prod-api'
# æ˜¯å¦åœ¨æ‰“包时开启压缩,支持 gzip å’Œ brotli
.env.staging
@@ -1,10 +1,10 @@
# é¡µé¢æ ‡é¢˜
VITE_APP_TITLE = å¼˜ä¹Ÿæ°´æ³¥ç®¡ç†ç³»ç»Ÿ
VITE_APP_TITLE = ä¸­å°ä¼ä¸šæ•°å­—化转型套餐包
# ç”Ÿäº§çŽ¯å¢ƒé…ç½®
VITE_APP_ENV = 'staging'
# å¼˜ä¹Ÿæ°´æ³¥ç®¡ç†ç³»ç»Ÿ/生产环境
# ä¸­å°ä¼ä¸šæ•°å­—化转型套餐包/生产环境
VITE_APP_BASE_API = '/stage-api'
# æ˜¯å¦åœ¨æ‰“包时开启压缩,支持 gzip å’Œ brotli
index.html
@@ -6,8 +6,8 @@
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="renderer" content="webkit">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/HYSNico.ico">
  <title>弘也水泥管理系统</title>
  <link rel="icon" href="/favicon.ico">
  <title>中小企业数字化转型套餐包</title>
  <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
  <style>
    html,
package.json
@@ -1,7 +1,7 @@
{
  "name": "ruoyi",
  "version": "3.8.9",
  "description": "弘也水泥管理系统",
  "description": "中小企业数字化转型套餐包",
  "author": "若依",
  "license": "MIT",
  "type": "module",
src/api/energyManagement/waterManagement.js
@@ -90,3 +90,4 @@
    data: query,
  })
}
src/router/index.js
@@ -71,20 +71,33 @@
      }
    ]
  },
  {
    path: '/main/MobileChat',
    component: Layout,
    redirect: '',
    hidden: true,
    children: [
      {
        path: '',
        component: () => import('@/views/chatHome/chatHomeIndex/MobileChat'),
        name: 'MobileChat',
        meta: { title: 'AI对话', icon: 'dashboard', affix: true}
      }
    ]
  },
  // {
  //   path: '/equipment',
  //   component: Layout,
  //   redirect: '/equipment/iot-monitor',
  //   children: [
  //     {
  //       path: 'iot-monitor',
  //       component: () => import('@/views/equipmentManagement/iotMonitor/index.vue'),
  //       name: 'IoTMonitor',
  //       meta: { title: 'IoT监控', icon: 'monitor', noCache: true }
  //     }
  //   ]
  // },
  // {
  //   path: '/main/MobileChat',
  //   component: Layout,
  //   redirect: '',
  //   hidden: true,
  //   children: [
  //     {
  //       path: '',
  //       component: () => import('@/views/chatHome/chatHomeIndex/MobileChat'),
  //       name: 'MobileChat',
  //       meta: { title: 'AI对话', icon: 'dashboard', affix: true}
  //     }
  //   ]
  // },
  {
    path: '/user',
    component: Layout,
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -189,6 +189,12 @@
const { form, rules } = toRefs(data);
const productOptions = ref([]);
const currentApproveStatus = ref(0)
const props = defineProps({
  approveType: {
    type: [Number, String],
    default: 0
  }
})
// å®¡æ‰¹äººèŠ‚ç‚¹ç›¸å…³
const approverNodes = ref([
@@ -265,6 +271,7 @@
const submitForm = () => {
  // æ”¶é›†æ‰€æœ‰èŠ‚ç‚¹çš„å®¡æ‰¹äººid
  form.value.approveUserIds = approverNodes.value.map(node => node.userId).join(',')
  form.value.approveType = props.approveType
  // å®¡æ‰¹äººå¿…填校验
  const hasEmptyApprover = approverNodes.value.some(node => !node.userId)
  if (hasEmptyApprover) {
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -42,7 +42,7 @@
          :total="page.total"
      ></PIMTable>
    </div>
    <info-form-dia ref="infoFormDia" @close="handleQuery"></info-form-dia>
    <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="approveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleQuery"></approval-dia>
    <FileList ref="fileListRef" />
  </div>
@@ -57,6 +57,15 @@
import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
import {approveProcessDelete, approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess.js";
import useUserStore from "@/store/modules/user";
// å®šä¹‰ç»„件接收的props
const props = defineProps({
  approveType: {
    type: [Number, String],
    default: 0
  }
});
const userStore = useUserStore();
@@ -205,7 +214,7 @@
};
const getList = () => {
  tableLoading.value = true;
  approveProcessListPage({...page, ...searchForm.value,}).then(res => {
  approveProcessListPage({...page, ...searchForm.value,approveType:props.approveType}).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
src/views/collaborativeApproval/approvalProcess/index1.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<template>
  <div class="app-container">
    <!-- å¼•å…¥index.vue组件并传递参数 -->
    <ApprovalProcessIndex :approveType="1" />
  </div>
</template>
<script setup>
import ApprovalProcessIndex from './index.vue'
// å®šä¹‰ç»„件名称
defineOptions({
  name: 'ApprovalProcessIndex1'
})
</script>
<style scoped>
.app-container {
  width: 100%;
  height: 100%;
}
</style>
src/views/collaborativeApproval/approvalProcess/index2.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<template>
  <div class="app-container">
    <!-- å¼•å…¥index.vue组件并传递参数 -->
    <ApprovalProcessIndex :approveType="2" />
  </div>
</template>
<script setup>
import ApprovalProcessIndex from './index.vue'
// å®šä¹‰ç»„件名称
defineOptions({
  name: 'ApprovalProcessIndex1'
})
</script>
<style scoped>
.app-container {
  width: 100%;
  height: 100%;
}
</style>
src/views/collaborativeApproval/approvalProcess/index3.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<template>
  <div class="app-container">
    <!-- å¼•å…¥index.vue组件并传递参数 -->
    <ApprovalProcessIndex :approveType="3" />
  </div>
</template>
<script setup>
import ApprovalProcessIndex from './index.vue'
// å®šä¹‰ç»„件名称
defineOptions({
  name: 'ApprovalProcessIndex1'
})
</script>
<style scoped>
.app-container {
  width: 100%;
  height: 100%;
}
</style>
src/views/collaborativeApproval/approvalProcess/index4.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<template>
  <div class="app-container">
    <!-- å¼•å…¥index.vue组件并传递参数 -->
    <ApprovalProcessIndex :approveType="4" />
  </div>
</template>
<script setup>
import ApprovalProcessIndex from './index.vue'
// å®šä¹‰ç»„件名称
defineOptions({
  name: 'ApprovalProcessIndex1'
})
</script>
<style scoped>
.app-container {
  width: 100%;
  height: 100%;
}
</style>
src/views/energyManagement/energyCockpit/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1380 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>能源驾驶舱</h2>
      <div class="header-info">
        <span class="update-time">最后更新:{{ lastUpdateTime }}</span>
        <el-button type="primary" size="small" @click="refreshData">
          <el-icon><Refresh /></el-icon>
          åˆ·æ–°æ•°æ®
        </el-button>
      </div>
    </div>
    <!-- å®žæ—¶èƒ½è€—监控 -->
    <div class="real-time-monitor">
      <el-row :gutter="20">
        <el-col :span="8">
          <el-card class="monitor-card">
            <template #header>
              <div class="card-header">
                <span>电力消耗</span>
                <el-tag type="success" size="small">实时</el-tag>
              </div>
            </template>
            <div class="monitor-content">
              <div class="monitor-value">
                <span class="value">{{ electricityConsumption }}</span>
                <span class="unit">kW·h</span>
              </div>
              <div class="monitor-trend">
                <span class="trend-label">趋势:</span>
                <el-tag :type="getTrendType(electricityTrend)" size="small">
                  {{ electricityTrend > 0 ? '↑' : '↓' }} {{ Math.abs(electricityTrend) }}%
                </el-tag>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="8">
          <el-card class="monitor-card">
            <template #header>
              <div class="card-header">
                <span>水消耗</span>
                <el-tag type="primary" size="small">实时</el-tag>
              </div>
            </template>
            <div class="monitor-content">
              <div class="monitor-value">
                <span class="value">{{ waterConsumption }}</span>
                <span class="unit">m³</span>
              </div>
              <div class="monitor-trend">
                <span class="trend-label">趋势:</span>
                <el-tag :type="getTrendType(waterTrend)" size="small">
                  {{ waterTrend > 0 ? '↑' : '↓' }} {{ Math.abs(waterTrend) }}%
                </el-tag>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="8">
          <el-card class="monitor-card">
            <template #header>
              <div class="card-header">
                <span>气体消耗</span>
                <el-tag type="warning" size="small">实时</el-tag>
              </div>
            </template>
            <div class="monitor-content">
              <div class="monitor-value">
                <span class="value">{{ gasConsumption }}</span>
                <span class="unit">m³</span>
              </div>
              <div class="monitor-trend">
                <span class="trend-label">趋势:</span>
                <el-tag :type="getTrendType(gasTrend)" size="small">
                  {{ gasTrend > 0 ? '↑' : '↓' }} {{ Math.abs(gasTrend) }}%
                </el-tag>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- èƒ½è€—趋势分析 -->
    <div class="trend-analysis">
      <el-card>
        <template #header>
          <div class="card-header">
            <span>能耗趋势分析</span>
            <div class="time-selector">
              <el-radio-group v-model="trendTimeUnit" @change="handleTrendTimeChange">
                <el-radio value="hour">小时</el-radio>
                <el-radio value="day">日</el-radio>
                <el-radio value="week">周</el-radio>
                <el-radio value="month">月</el-radio>
                <el-radio value="year">å¹´</el-radio>
              </el-radio-group>
            </div>
          </div>
        </template>
        <div class="chart-container">
          <div ref="trendChart" style="width: 100%; height: 400px;"></div>
        </div>
      </el-card>
    </div>
    <!-- èƒ½è€—统计与排名 -->
    <div class="statistics-ranking">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-card class="statistics-card">
            <template #header>
              <div class="card-header">
                <span class="card-title">能耗统计报表</span>
                <div class="header-actions">
                  <el-select v-model="statisticsPeriod" @change="handleStatisticsChange" size="small" style="width: 100px;">
                    <el-option label="日统计" value="day" />
                    <el-option label="周统计" value="week" />
                    <el-option label="月统计" value="month" />
                    <el-option label="年统计" value="year" />
                  </el-select>
                </div>
              </div>
            </template>
            <div class="statistics-content">
              <div class="statistics-item">
                <span class="label">总能耗:</span>
                <span class="value">{{ totalEnergyConsumption }} kW·h</span>
              </div>
              <div class="statistics-item">
                <span class="label">同比:</span>
                <span class="value" :class="getComparisonClass(yearOverYear)">
                  {{ yearOverYear > 0 ? '+' : '' }}{{ yearOverYear }}%
                </span>
              </div>
              <div class="statistics-item">
                <span class="label">环比:</span>
                <span class="value" :class="getComparisonClass(monthOverMonth)">
                  {{ monthOverMonth > 0 ? '+' : '' }}{{ monthOverMonth }}%
                </span>
              </div>
              <div class="statistics-item">
                <span class="label">节能率:</span>
                <span class="value success">{{ energySavingRate }}%</span>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="12">
          <el-card class="ranking-card">
            <template #header>
              <div class="card-header">
                <span class="card-title">能耗排名</span>
                <el-select v-model="rankingType" @change="handleRankingChange" size="small" style="width: 120px;">
                  <el-option label="部门排名" value="department" />
                  <el-option label="车间排名" value="workshop" />
                  <el-option label="设备排名" value="equipment" />
                </el-select>
              </div>
            </template>
            <div class="ranking-list">
              <div v-for="(item, index) in rankingList" :key="index" class="ranking-item">
                <div class="ranking-number" :class="getRankingClass(index + 1)">{{ index + 1 }}</div>
                <div class="ranking-info">
                  <div class="ranking-name">{{ item.name }}</div>
                  <div class="ranking-value">{{ item.value }} kW·h</div>
                </div>
                <div class="ranking-trend">
                  <el-tag :type="getTrendType(item.trend)" size="small">
                    {{ item.trend > 0 ? '↑' : '↓' }} {{ Math.abs(item.trend) }}%
                  </el-tag>
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- å¼‚常分析与智能控制 -->
    <div class="analysis-control">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-card class="abnormal-card">
            <template #header>
              <div class="card-header">
                <span class="card-title">异常分析</span>
                <el-tag type="danger" size="small">{{ abnormalCount }}个异常</el-tag>
              </div>
            </template>
            <div class="abnormal-list">
              <div v-for="(item, index) in abnormalList" :key="index" class="abnormal-item">
                <div class="abnormal-icon">
                  <el-icon :color="getAbnormalColor(item.level)">
                    <Warning v-if="item.level === 'warning'" />
                    <CircleClose v-else />
                  </el-icon>
                </div>
                <div class="abnormal-content">
                  <div class="abnormal-title">{{ item.title }}</div>
                  <div class="abnormal-desc">{{ item.description }}</div>
                  <div class="abnormal-time">{{ item.time }}</div>
                </div>
                <div class="abnormal-action">
                  <el-button link size="small" @click="handleAbnormal(item)">处理</el-button>
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="12">
          <el-card class="control-card">
            <template #header>
              <div class="card-header">
                <span class="card-title">智能控制系统</span>
                <el-switch v-model="autoControlEnabled" @change="handleAutoControlChange" />
              </div>
            </template>
            <div class="control-content">
              <div class="control-item">
                <span class="label">峰谷平电价管理:</span>
                <el-tag :type="getPriceType(currentPriceType)" size="small">
                  {{ getPriceTypeText(currentPriceType) }}
                </el-tag>
              </div>
              <div class="control-item">
                <span class="label">负荷预测:</span>
                <span class="value">{{ loadForecast }} kW</span>
              </div>
              <div class="control-item">
                <span class="label">自动启停:</span>
                <el-tag :type="autoStartStop ? 'success' : 'info'" size="small">
                  {{ autoStartStop ? '已启用' : '已禁用' }}
                </el-tag>
              </div>
              <div class="control-item">
                <span class="label">智能调节:</span>
                <el-progress :percentage="intelligentAdjustment" :color="getProgressColor" />
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- çŽ¯ä¿æŒ‡æ ‡ -->
    <div class="environmental-indicators">
      <el-card>
        <template #header>
          <div class="card-header">
            <span>环保指标监控</span>
          </div>
        </template>
        <el-row :gutter="20">
          <el-col :span="8">
            <div class="indicator-item">
              <div class="indicator-title">碳排放量</div>
              <div class="indicator-value">{{ carbonEmission }} kg</div>
              <div class="indicator-trend">
                <span>同比:</span>
                <span :class="getComparisonClass(carbonEmissionTrend)">
                  {{ carbonEmissionTrend > 0 ? '+' : '' }}{{ carbonEmissionTrend }}%
                </span>
              </div>
            </div>
          </el-col>
          <el-col :span="8">
            <div class="indicator-item">
              <div class="indicator-title">环保达标率</div>
              <div class="indicator-value">{{ environmentalCompliance }}%</div>
              <div class="indicator-trend">
                <span>目标:</span>
                <span class="success">95%</span>
              </div>
            </div>
          </el-col>
          <el-col :span="8">
            <div class="indicator-item">
              <div class="indicator-title">绿色能源占比</div>
              <div class="indicator-value">{{ greenEnergyRatio }}%</div>
              <div class="indicator-trend">
                <span>目标:</span>
                <span class="success">30%</span>
              </div>
            </div>
          </el-col>
        </el-row>
      </el-card>
    </div>
    <!-- å¤šç»´åº¦æŠ¥è¡¨ -->
    <div class="multi-dimensional-reports">
      <el-card>
        <template #header>
          <div class="card-header">
            <span>多维度报表</span>
          </div>
        </template>
        <div class="report-filters">
          <el-row :gutter="20">
            <el-col :span="6">
              <el-form-item label="时间维度">
                <el-select v-model="reportTimeDimension" placeholder="选择时间维度">
                  <el-option label="小时" value="hour" />
                  <el-option label="日" value="day" />
                  <el-option label="周" value="week" />
                  <el-option label="月" value="month" />
                  <el-option label="å¹´" value="year" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="部门维度">
                <el-select v-model="reportDepartmentDimension" placeholder="选择部门">
                  <el-option label="全部部门" value="all" />
                  <el-option label="生产部" value="production" />
                  <el-option label="技术部" value="technology" />
                  <el-option label="行政部" value="administration" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="设备维度">
                <el-select v-model="reportEquipmentDimension" placeholder="选择设备类型">
                  <el-option label="全部设备" value="all" />
                  <el-option label="电力设备" value="electricity" />
                  <el-option label="水处理设备" value="water" />
                  <el-option label="气体设备" value="gas" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item>
                <el-button type="primary" @click="generateReport">生成报表</el-button>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <div class="report-preview">
          <div class="report-data">
            <el-row :gutter="20">
              <el-col :span="8">
                <div class="data-card">
                  <div class="data-title">电力消耗</div>
                  <div class="data-value">{{ reportData.electricity }} kW·h</div>
                  <div class="data-trend">
                    <span :class="getTrendClass(reportData.electricityTrend)">
                      {{ reportData.electricityTrend > 0 ? '↑' : '↓' }} {{ Math.abs(reportData.electricityTrend) }}%
                    </span>
                  </div>
                </div>
              </el-col>
              <el-col :span="8">
                <div class="data-card">
                  <div class="data-title">水消耗</div>
                  <div class="data-value">{{ reportData.water }} m³</div>
                  <div class="data-trend">
                    <span :class="getTrendClass(reportData.waterTrend)">
                      {{ reportData.waterTrend > 0 ? '↑' : '↓' }} {{ Math.abs(reportData.waterTrend) }}%
                    </span>
                  </div>
                </div>
              </el-col>
              <el-col :span="8">
                <div class="data-card">
                  <div class="data-title">气体消耗</div>
                  <div class="data-value">{{ reportData.gas }} m³</div>
                  <div class="data-trend">
                    <span :class="getTrendClass(reportData.gasTrend)">
                      {{ reportData.gasTrend > 0 ? '↑' : '↓' }} {{ Math.abs(reportData.gasTrend) }}%
                    </span>
                  </div>
                </div>
              </el-col>
            </el-row>
            <div class="report-chart">
              <div class="chart-title">能耗趋势图</div>
              <div class="chart-bars">
                <div v-for="(item, index) in reportData.chartData" :key="index" class="chart-bar">
                  <div class="bar-label">{{ item.label }}</div>
                  <div class="bar-container">
                    <div class="bar-fill" :style="{ height: item.percentage + '%', backgroundColor: item.color }"></div>
                  </div>
                  <div class="bar-value">{{ item.value }}</div>
                </div>
              </div>
            </div>
            <div class="report-summary">
              <div class="summary-item">
                <span class="summary-label">总能耗:</span>
                <span class="summary-value">{{ reportData.totalEnergy }} kW·h</span>
              </div>
              <div class="summary-item">
                <span class="summary-label">平均能耗:</span>
                <span class="summary-value">{{ reportData.averageEnergy }} kW·h</span>
              </div>
              <div class="summary-item">
                <span class="summary-label">能耗效率:</span>
                <span class="summary-value">{{ reportData.efficiency }}%</span>
              </div>
            </div>
          </div>
        </div>
      </el-card>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import * as echarts from 'echarts'
import {
  Refresh,
  Download,
  Warning,
  CircleClose,
  Document,
  Edit,
  Bell
} from '@element-plus/icons-vue'
// å“åº”式数据
const lastUpdateTime = ref('')
const electricityConsumption = ref(0)
const waterConsumption = ref(0)
const gasConsumption = ref(0)
const electricityTrend = ref(0)
const waterTrend = ref(0)
const gasTrend = ref(0)
// è¶‹åŠ¿åˆ†æž
const trendTimeUnit = ref('day')
const trendChart = ref(null)
let chartInstance = null
// ç»Ÿè®¡æŠ¥è¡¨
const statisticsPeriod = ref('month')
const totalEnergyConsumption = ref(0)
const yearOverYear = ref(0)
const monthOverMonth = ref(0)
const energySavingRate = ref(0)
// èƒ½è€—排名
const rankingType = ref('department')
const rankingList = ref([])
// å¼‚常分析
const abnormalCount = ref(0)
const abnormalList = ref([])
// æ™ºèƒ½æŽ§åˆ¶
const autoControlEnabled = ref(true)
const currentPriceType = ref('peak')
const loadForecast = ref(0)
const autoStartStop = ref(true)
const intelligentAdjustment = ref(0)
// çŽ¯ä¿æŒ‡æ ‡
const carbonEmission = ref(0)
const carbonEmissionTrend = ref(0)
const environmentalCompliance = ref(0)
const greenEnergyRatio = ref(0)
// å¤šç»´åº¦æŠ¥è¡¨
const reportTimeDimension = ref('month')
const reportDepartmentDimension = ref('all')
const reportEquipmentDimension = ref('all')
const reportData = ref({
  electricity: 0,
  water: 0,
  gas: 0,
  electricityTrend: 0,
  waterTrend: 0,
  gasTrend: 0,
  totalEnergy: 0,
  averageEnergy: 0,
  efficiency: 0,
  chartData: []
})
// å®šæ—¶å™¨
let updateTimer = null
// èŽ·å–è¶‹åŠ¿ç±»åž‹æ ·å¼
const getTrendType = (trend) => {
  if (trend > 0) return 'danger'
  if (trend < 0) return 'success'
  return 'info'
}
// èŽ·å–å¯¹æ¯”ç±»åž‹æ ·å¼
const getComparisonClass = (value) => {
  if (value > 0) return 'danger'
  if (value < 0) return 'success'
  return 'info'
}
// èŽ·å–æŽ’åæ ·å¼
const getRankingClass = (rank) => {
  if (rank === 1) return 'ranking-first'
  if (rank === 2) return 'ranking-second'
  if (rank === 3) return 'ranking-third'
  return 'ranking-normal'
}
// èŽ·å–å¼‚å¸¸é¢œè‰²
const getAbnormalColor = (level) => {
  return level === 'warning' ? '#E6A23C' : '#F56C6C'
}
// èŽ·å–ç”µä»·ç±»åž‹æ ·å¼
const getPriceType = (type) => {
  const typeMap = {
    peak: 'danger',
    normal: 'warning',
    valley: 'success'
  }
  return typeMap[type] || 'info'
}
// èŽ·å–ç”µä»·ç±»åž‹æ–‡æœ¬
const getPriceTypeText = (type) => {
  const typeMap = {
    peak: '峰时',
    normal: '平时',
    valley: '谷时'
  }
  return typeMap[type] || '未知'
}
// èŽ·å–è¿›åº¦æ¡é¢œè‰²
const getProgressColor = (percentage) => {
  if (percentage < 50) return '#67C23A'
  if (percentage < 80) return '#E6A23C'
  return '#F56C6C'
}
// èŽ·å–è¶‹åŠ¿æ ·å¼
const getTrendClass = (trend) => {
  if (trend > 0) return 'trend-up'
  if (trend < 0) return 'trend-down'
  return 'trend-stable'
}
// æ¨¡æ‹Ÿæ•°æ®ç”Ÿæˆ
const generateMockData = () => {
  // å®žæ—¶èƒ½è€—数据
  electricityConsumption.value = Math.floor(Math.random() * 1000) + 2000
  waterConsumption.value = Math.floor(Math.random() * 100) + 150
  gasConsumption.value = Math.floor(Math.random() * 50) + 80
  // è¶‹åŠ¿æ•°æ®
  electricityTrend.value = (Math.random() * 20 - 10).toFixed(1)
  waterTrend.value = (Math.random() * 15 - 7.5).toFixed(1)
  gasTrend.value = (Math.random() * 12 - 6).toFixed(1)
  // ç»Ÿè®¡æ•°æ®
  totalEnergyConsumption.value = Math.floor(Math.random() * 50000) + 100000
  yearOverYear.value = (Math.random() * 20 - 10).toFixed(1)
  monthOverMonth.value = (Math.random() * 15 - 7.5).toFixed(1)
  energySavingRate.value = (Math.random() * 10 + 5).toFixed(1)
  // æŽ’名数据
  rankingList.value = [
    { name: '生产车间A', value: Math.floor(Math.random() * 5000) + 10000, trend: (Math.random() * 20 - 10).toFixed(1) },
    { name: '生产车间B', value: Math.floor(Math.random() * 4000) + 8000, trend: (Math.random() * 20 - 10).toFixed(1) },
    { name: '技术研发部', value: Math.floor(Math.random() * 3000) + 6000, trend: (Math.random() * 20 - 10).toFixed(1) },
    { name: '行政办公区', value: Math.floor(Math.random() * 2000) + 4000, trend: (Math.random() * 20 - 10).toFixed(1) },
    { name: '后勤保障区', value: Math.floor(Math.random() * 1500) + 3000, trend: (Math.random() * 20 - 10).toFixed(1) }
  ].sort((a, b) => b.value - a.value)
  // å¼‚常数据
  abnormalCount.value = Math.floor(Math.random() * 5) + 1
  abnormalList.value = [
    { level: 'warning', title: '电力负荷过高', description: '生产车间A电力负荷达到85%,建议检查设备运行状态', time: '2分钟前' },
    { level: 'error', title: '水压异常', description: '水处理设备压力异常,当前压力0.3MPa,低于正常范围', time: '5分钟前' }
  ]
  // æ™ºèƒ½æŽ§åˆ¶æ•°æ®
  loadForecast.value = Math.floor(Math.random() * 500) + 1500
  intelligentAdjustment.value = Math.floor(Math.random() * 30) + 60
  // çŽ¯ä¿æŒ‡æ ‡
  carbonEmission.value = Math.floor(Math.random() * 1000) + 5000
  carbonEmissionTrend.value = (Math.random() * 15 - 7.5).toFixed(1)
  environmentalCompliance.value = (Math.random() * 5 + 95).toFixed(1)
  greenEnergyRatio.value = (Math.random() * 10 + 25).toFixed(1)
  // æ›´æ–°æœ€åŽæ›´æ–°æ—¶é—´
  lastUpdateTime.value = new Date().toLocaleString()
  // åŒæ—¶æ›´æ–°æŠ¥è¡¨æ•°æ®
  generateReportData()
}
// åˆå§‹åŒ–趋势图表
const initTrendChart = () => {
  if (chartInstance) {
    chartInstance.dispose()
  }
  chartInstance = echarts.init(trendChart.value)
  const option = {
    title: {
      text: '能耗趋势分析',
      left: 'center'
    },
    tooltip: {
      trigger: 'axis'
    },
    legend: {
      data: ['电力', 'æ°´', '气体'],
      bottom: 10
    },
    xAxis: {
      type: 'category',
      data: generateTimeData()
    },
    yAxis: {
      type: 'value',
      name: '消耗量'
    },
    series: [
      {
        name: '电力',
        type: 'line',
        data: generateSeriesData(),
        smooth: true
      },
      {
        name: 'æ°´',
        type: 'line',
        data: generateSeriesData(),
        smooth: true
      },
      {
        name: '气体',
        type: 'line',
        data: generateSeriesData(),
        smooth: true
      }
    ]
  }
  chartInstance.setOption(option)
}
// ç”Ÿæˆæ—¶é—´æ•°æ®
const generateTimeData = () => {
  const data = []
  const now = new Date()
  switch (trendTimeUnit.value) {
    case 'hour':
      for (let i = 23; i >= 0; i--) {
        const time = new Date(now.getTime() - i * 60 * 60 * 1000)
        data.unshift(time.getHours() + ':00')
      }
      break
    case 'day':
      for (let i = 29; i >= 0; i--) {
        const time = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
        data.unshift(time.getDate() + '日')
      }
      break
    case 'week':
      for (let i = 11; i >= 0; i--) {
        data.unshift(`第${12 - i}周`)
      }
      break
    case 'month':
      for (let i = 11; i >= 0; i--) {
        const month = (12 - i) % 12 || 12
        data.unshift(`${month}月`)
      }
      break
    case 'year':
      for (let i = 4; i >= 0; i--) {
        const year = new Date().getFullYear() - i
        data.unshift(`${year}å¹´`)
      }
      break
  }
  return data
}
// ç”Ÿæˆç³»åˆ—数据
const generateSeriesData = () => {
  const data = []
  const count = trendTimeUnit.value === 'hour' ? 24 :
                trendTimeUnit.value === 'day' ? 30 :
                trendTimeUnit.value === 'week' ? 12 :
                trendTimeUnit.value === 'month' ? 12 : 5
  for (let i = 0; i < count; i++) {
    data.push(Math.floor(Math.random() * 1000) + 500)
  }
  return data
}
// å¤„理趋势时间变化
const handleTrendTimeChange = () => {
  nextTick(() => {
    initTrendChart()
  })
}
// å¤„理统计周期变化
const handleStatisticsChange = () => {
  generateMockData()
}
// å¤„理排名类型变化
const handleRankingChange = () => {
  // æ ¹æ®ç±»åž‹é‡æ–°ç”ŸæˆæŽ’名数据
  generateMockData()
}
// å¤„理自动控制变化
const handleAutoControlChange = (value) => {
  ElMessage.success(`智能控制系统已${value ? '启用' : '禁用'}`)
}
// å¤„理异常
const handleAbnormal = (item) => {
  ElMessage.info(`正在处理异常:${item.title}`)
}
// åˆ·æ–°æ•°æ®
const refreshData = () => {
  generateMockData()
  if (chartInstance) {
    initTrendChart()
  }
  ElMessage.success('数据已刷新')
}
// å¯¼å‡ºç»Ÿè®¡
const exportStatistics = () => {
  ElMessage.success('统计数据导出成功')
}
// å¯¼å‡ºçŽ¯ä¿æŠ¥å‘Š
const exportEnvironmentalReport = () => {
  ElMessage.success('环保报告导出成功')
}
// ç”Ÿæˆè‡ªå®šä¹‰æŠ¥è¡¨
const generateCustomReport = () => {
  ElMessage.info('自定义报表功能开发中...')
}
// è®¢é˜…报表
const subscribeReport = () => {
  ElMessage.info('报表订阅功能开发中...')
}
// ç”ŸæˆæŠ¥è¡¨æ•°æ®
const generateReportData = () => {
  // ç”ŸæˆåŸºç¡€æ•°æ®
  reportData.value.electricity = Math.floor(Math.random() * 5000) + 8000
  reportData.value.water = Math.floor(Math.random() * 200) + 300
  reportData.value.gas = Math.floor(Math.random() * 100) + 150
  // ç”Ÿæˆè¶‹åŠ¿æ•°æ®
  reportData.value.electricityTrend = (Math.random() * 20 - 10).toFixed(1)
  reportData.value.waterTrend = (Math.random() * 15 - 7.5).toFixed(1)
  reportData.value.gasTrend = (Math.random() * 12 - 6).toFixed(1)
  // è®¡ç®—总能耗和平均能耗
  reportData.value.totalEnergy = reportData.value.electricity + reportData.value.water * 0.1 + reportData.value.gas * 0.05
  reportData.value.averageEnergy = Math.floor(reportData.value.totalEnergy / 3)
  reportData.value.efficiency = (Math.random() * 20 + 80).toFixed(1)
  // ç”Ÿæˆå›¾è¡¨æ•°æ®
  const labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
  const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#9c27b0', '#ff9800']
  reportData.value.chartData = labels.map((label, index) => ({
    label,
    value: Math.floor(Math.random() * 1000) + 500,
    percentage: Math.floor(Math.random() * 40) + 30,
    color: colors[index]
  }))
}
// ç”ŸæˆæŠ¥è¡¨
const generateReport = () => {
  generateReportData()
  ElMessage.success('报表生成成功')
}
// å¯åŠ¨å®šæ—¶æ›´æ–°
const startAutoUpdate = () => {
  updateTimer = setInterval(() => {
    generateMockData()
    if (chartInstance) {
      initTrendChart()
    }
  }, 60000) // æ¯åˆ†é’Ÿæ›´æ–°ä¸€æ¬¡
}
// åœæ­¢å®šæ—¶æ›´æ–°
const stopAutoUpdate = () => {
  if (updateTimer) {
    clearInterval(updateTimer)
    updateTimer = null
  }
}
// ç»„件挂载
onMounted(() => {
  generateMockData()
  nextTick(() => {
    initTrendChart()
  })
  startAutoUpdate()
})
// ç»„件卸载
onUnmounted(() => {
  stopAutoUpdate()
  if (chartInstance) {
    chartInstance.dispose()
  }
})
</script>
<style lang="scss" scoped>
.app-container {
  padding: 12px;
  background: #f5f5f5;
  min-height: 100vh;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
  padding: 16px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  h2 {
    margin: 0;
    color: #303133;
    font-size: 22px;
  }
  .header-info {
    display: flex;
    align-items: center;
    gap: 12px;
    .update-time {
      color: #909399;
      font-size: 14px;
    }
  }
}
.real-time-monitor {
  margin-bottom: 12px;
  .monitor-card {
    .monitor-content {
      text-align: center;
      padding: 16px 0;
      .monitor-value {
        margin-bottom: 12px;
        .value {
          font-size: 28px;
          font-weight: bold;
          color: #409eff;
        }
        .unit {
          font-size: 14px;
          color: #909399;
          margin-left: 4px;
        }
      }
      .monitor-trend {
        .trend-label {
          font-size: 14px;
          color: #606266;
          margin-right: 6px;
        }
      }
    }
  }
}
.trend-analysis {
  margin-bottom: 12px;
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .time-selector {
      .el-radio-group {
        .el-radio {
          margin-right: 4px;
        }
      }
    }
  }
  .chart-container {
    padding: 16px 0;
  }
}
.statistics-ranking {
  margin-bottom: 12px;
  .statistics-card, .ranking-card {
    height: 100%;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
    border-radius: 8px;
    transition: all 0.3s ease;
    &:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
    }
  }
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #fafafa;
    .card-title {
      font-size: 15px;
      font-weight: 600;
      color: #303133;
    }
    .header-actions {
      display: flex;
      gap: 8px;
      align-items: center;
    }
  }
  .statistics-content {
    padding: 16px;
    .statistics-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 12px;
      padding: 10px 12px;
      background: #f8f9fa;
      border-radius: 6px;
      transition: background-color 0.3s ease;
      &:hover {
        background: #e9ecef;
      }
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        color: #606266;
        font-size: 14px;
        font-weight: 500;
      }
      .value {
        font-weight: bold;
        font-size: 15px;
        &.success {
          color: #67c23a;
        }
      }
    }
  }
  .ranking-list {
    padding: 16px;
    .ranking-item {
      display: flex;
      align-items: center;
      padding: 12px;
      margin-bottom: 6px;
      background: #f8f9fa;
      border-radius: 6px;
      transition: all 0.3s ease;
      &:hover {
        background: #e9ecef;
        transform: translateX(4px);
      }
      &:last-child {
        margin-bottom: 0;
      }
      .ranking-number {
        width: 32px;
        height: 32px;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: bold;
        font-size: 14px;
        margin-right: 12px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        &.ranking-first {
          background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
          color: #fff;
        }
        &.ranking-second {
          background: linear-gradient(135deg, #c0c0c0 0%, #d4d4d4 100%);
          color: #fff;
        }
        &.ranking-third {
          background: linear-gradient(135deg, #cd7f32 0%, #daa520 100%);
          color: #fff;
        }
        &.ranking-normal {
          background: linear-gradient(135deg, #f5f5f5 0%, #e9ecef 100%);
          color: #909399;
        }
      }
      .ranking-info {
        flex: 1;
        .ranking-name {
          font-weight: 600;
          color: #303133;
          margin-bottom: 4px;
          font-size: 14px;
        }
        .ranking-value {
          color: #606266;
          font-size: 13px;
          font-weight: 500;
        }
      }
      .ranking-trend {
        margin-left: 12px;
      }
    }
  }
}
.analysis-control {
  margin-bottom: 20px;
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .abnormal-list {
    .abnormal-item {
      display: flex;
      align-items: flex-start;
      padding: 15px 0;
      border-bottom: 1px solid #f0f0f0;
      &:last-child {
        border-bottom: none;
      }
      .abnormal-icon {
        margin-right: 15px;
        margin-top: 2px;
      }
      .abnormal-content {
        flex: 1;
        .abnormal-title {
          font-weight: bold;
          color: #303133;
          margin-bottom: 5px;
        }
        .abnormal-desc {
          color: #606266;
          font-size: 14px;
          margin-bottom: 5px;
        }
        .abnormal-time {
          color: #909399;
          font-size: 12px;
        }
      }
      .abnormal-action {
        margin-left: 15px;
      }
    }
  }
  .control-content {
    .control-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 20px;
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        color: #606266;
        font-size: 14px;
      }
      .value {
        font-weight: bold;
        color: #303133;
      }
    }
  }
}
.environmental-indicators {
  margin-bottom: 20px;
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .header-actions {
      display: flex;
      gap: 10px;
    }
  }
  .indicator-item {
    text-align: center;
    padding: 20px 0;
    .indicator-title {
      color: #606266;
      font-size: 14px;
      margin-bottom: 10px;
    }
    .indicator-value {
      font-size: 24px;
      font-weight: bold;
      color: #409eff;
      margin-bottom: 10px;
    }
    .indicator-trend {
      font-size: 12px;
      color: #909399;
      .success {
        color: #67c23a;
      }
    }
  }
}
.multi-dimensional-reports {
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .header-actions {
      display: flex;
      gap: 10px;
    }
  }
  .report-filters {
    padding: 20px 0;
    border-bottom: 1px solid #f0f0f0;
    margin-bottom: 20px;
  }
  .report-preview {
    .report-data {
      padding: 20px 0;
      .data-card {
        text-align: center;
        padding: 16px;
        background: #f8f9fa;
        border-radius: 8px;
        margin-bottom: 16px;
        .data-title {
          color: #606266;
          font-size: 14px;
          margin-bottom: 8px;
        }
        .data-value {
          font-size: 20px;
          font-weight: bold;
          color: #303133;
          margin-bottom: 8px;
        }
        .data-trend {
          font-size: 12px;
          .trend-up {
            color: #f56c6c;
          }
          .trend-down {
            color: #67c23a;
          }
          .trend-stable {
            color: #909399;
          }
        }
      }
      .report-chart {
        margin: 20px 0;
        padding: 20px;
        background: #f8f9fa;
        border-radius: 8px;
        .chart-title {
          text-align: center;
          font-size: 16px;
          font-weight: 600;
          color: #303133;
          margin-bottom: 16px;
        }
        .chart-bars {
          display: flex;
          justify-content: space-around;
          align-items: flex-end;
          height: 120px;
          .chart-bar {
            text-align: center;
            flex: 1;
            margin: 0 8px;
            .bar-label {
              font-size: 12px;
              color: #606266;
              margin-bottom: 8px;
            }
            .bar-container {
              height: 80px;
              background: #e9ecef;
              border-radius: 4px;
              position: relative;
              margin-bottom: 8px;
            }
            .bar-fill {
              position: absolute;
              bottom: 0;
              left: 0;
              right: 0;
              border-radius: 4px;
              transition: height 0.3s ease;
            }
            .bar-value {
              font-size: 12px;
              color: #303133;
              font-weight: 500;
            }
          }
        }
      }
      .report-summary {
        display: flex;
        justify-content: space-around;
        padding: 20px;
        background: #f8f9fa;
        border-radius: 8px;
        .summary-item {
          text-align: center;
          .summary-label {
            display: block;
            color: #606266;
            font-size: 14px;
            margin-bottom: 8px;
          }
          .summary-value {
            font-size: 18px;
            font-weight: bold;
            color: #303133;
          }
        }
      }
    }
  }
}
// é€šç”¨æ ·å¼
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.success {
  color: #67c23a;
}
.danger {
  color: #f56c6c;
}
.warning {
  color: #e6a23c;
}
.info {
  color: #909399;
}
</style>
src/views/energyManagement/gasManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,624 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>用气管理系统</h2>
      <div class="header-info">
        <span class="update-time">最后更新:{{ lastUpdateTime }}</span>
        <el-button type="primary" size="small" @click="refreshData">
          <el-icon><Refresh /></el-icon>
          åˆ·æ–°æ•°æ®
        </el-button>
      </div>
    </div>
    <!-- ç»Ÿè®¡å¡ç‰‡åŒºåŸŸ -->
    <div class="stats-cards">
      <el-row :gutter="20">
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-icon gas-device">
                <el-icon size="32"><Box /></el-icon>
              </div>
              <div class="stat-info">
                <div class="stat-value">{{ totalDevices }}</div>
                <div class="stat-label">在用设备</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-icon daily-consumption">
                <el-icon size="32"><TrendCharts /></el-icon>
              </div>
              <div class="stat-info">
                <div class="stat-value">{{ dailyConsumption }} m³</div>
                <div class="stat-label">日耗量</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-icon monthly-consumption">
                <el-icon size="32"><DataLine /></el-icon>
              </div>
              <div class="stat-info">
                <div class="stat-value">{{ monthlyConsumption }} m³</div>
                <div class="stat-label">月耗量</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-icon gas-price">
                <el-icon size="32"><Money /></el-icon>
              </div>
              <div class="stat-info">
                <div class="stat-value">Â¥{{ gasUnitPrice }}</div>
                <div class="stat-label">气体单价</div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- è´¹ç”¨ç»Ÿè®¡åŒºåŸŸ -->
    <div class="cost-stats">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-card class="cost-card">
            <template #header>
              <div class="card-header">
                <span>日费用统计</span>
                <el-tag type="success" size="small">今日</el-tag>
              </div>
            </template>
            <div class="cost-content">
              <div class="cost-main">
                <span class="cost-amount">Â¥{{ dailyTotalCost.toFixed(2) }}</span>
                <span class="cost-unit">元</span>
              </div>
              <div class="cost-details">
                <div class="cost-item">
                  <span>消耗量:</span>
                  <span>{{ dailyConsumption }} m³</span>
                </div>
                <div class="cost-item">
                  <span>单价:</span>
                  <span>Â¥{{ gasUnitPrice }}/m³</span>
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="12">
          <el-card class="cost-card">
            <template #header>
              <div class="card-header">
                <span>月费用统计</span>
                <el-tag type="primary" size="small">本月</el-tag>
              </div>
            </template>
            <div class="cost-content">
              <div class="cost-main">
                <span class="cost-amount">Â¥{{ monthlyTotalCost.toFixed(2) }}</span>
                <span class="cost-unit">元</span>
              </div>
              <div class="cost-details">
                <div class="cost-item">
                  <span>消耗量:</span>
                  <span>{{ monthlyConsumption }} m³</span>
                </div>
                <div class="cost-item">
                  <span>平均单价:</span>
                  <span>Â¥{{ gasUnitPrice }}/m³</span>
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- è®¾å¤‡åˆ—表区域 -->
    <div class="device-section">
      <el-card>
        <template #header>
                     <div class="card-header">
             <span>设备监控</span>
             <div class="header-actions">
               <el-button type="primary" size="small" @click="addDevice">
                 <el-icon><Plus /></el-icon>
                 æ·»åŠ è®¾å¤‡
               </el-button>
             </div>
           </div>
        </template>
        <el-table :data="deviceList" border style="width: 100%" v-loading="tableLoading">
          <el-table-column align="center" label="序号" type="index" width="60" />
          <el-table-column label="设备编号" prop="deviceCode" width="120" show-overflow-tooltip />
          <el-table-column label="设备名称" prop="deviceName" width="150" show-overflow-tooltip />
          <el-table-column label="设备类型" prop="deviceType" width="120" show-overflow-tooltip />
          <el-table-column label="规格型号" prop="specification" width="150" show-overflow-tooltip />
          <el-table-column label="当前压力(MPa)" prop="currentPressure" width="130" show-overflow-tooltip>
            <template #default="scope">
              <span :class="getPressureClass(scope.row.currentPressure)">
                {{ scope.row.currentPressure }}
              </span>
            </template>
          </el-table-column>
          <el-table-column label="当前温度(℃)" prop="currentTemperature" width="130" show-overflow-tooltip>
            <template #default="scope">
              <span :class="getTemperatureClass(scope.row.currentTemperature)">
                {{ scope.row.currentTemperature }}
              </span>
            </template>
          </el-table-column>
          <el-table-column label="气体浓度(ppm)" prop="gasConcentration" width="140" show-overflow-tooltip>
            <template #default="scope">
              <span :class="getConcentrationClass(scope.row.gasConcentration)">
                {{ scope.row.gasConcentration }}
              </span>
            </template>
          </el-table-column>
          <el-table-column label="运行状态" prop="status" width="100" show-overflow-tooltip>
            <template #default="scope">
              <el-tag :type="getStatusType(scope.row.status)" size="small">
                {{ getStatusText(scope.row.status) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="最后更新" prop="lastUpdate" width="160" show-overflow-tooltip />
                     <el-table-column label="操作" align="center" width="100" fixed="right">
             <template #default="scope">
               <el-button link size="small" @click="editDevice(scope.row)">
                 ç¼–辑
               </el-button>
             </template>
           </el-table-column>
        </el-table>
      </el-card>
    </div>
    <!-- æ·»åŠ /编辑设备弹窗 -->
    <el-dialog v-model="deviceDialogVisible" :title="dialogTitle" width="600px">
      <el-form :model="deviceForm" :rules="deviceRules" ref="deviceFormRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="设备编号" prop="deviceCode">
              <el-input v-model="deviceForm.deviceCode" placeholder="请输入设备编号" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="设备名称" prop="deviceName">
              <el-input v-model="deviceForm.deviceName" placeholder="请输入设备名称" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="设备类型" prop="deviceType">
              <el-select v-model="deviceForm.deviceType" placeholder="请选择设备类型" style="width: 100%">
                <el-option label="液化气储罐" value="液化气储罐" />
                <el-option label="压缩气储罐" value="压缩气储罐" />
                <el-option label="天然气储罐" value="天然气储罐" />
                <el-option label="氧气储罐" value="氧气储罐" />
                <el-option label="其他" value="其他" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="规格型号" prop="specification">
              <el-input v-model="deviceForm.specification" placeholder="请输入规格型号" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="设计压力(MPa)" prop="designPressure">
              <el-input-number v-model="deviceForm.designPressure" :min="0" :precision="2" style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="容积(m³)" prop="volume">
              <el-input-number v-model="deviceForm.volume" :min="0" :precision="2" style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <el-button @click="deviceDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="saveDevice">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
  Refresh,
  Box,
  TrendCharts,
  DataLine,
  Money,
  Plus
} from '@element-plus/icons-vue'
// å“åº”式数据
const lastUpdateTime = ref('')
const totalDevices = ref(0)
const dailyConsumption = ref(0)
const monthlyConsumption = ref(0)
const gasUnitPrice = ref(0)
const dailyTotalCost = ref(0)
const monthlyTotalCost = ref(0)
const deviceList = ref([])
const tableLoading = ref(false)
const deviceDialogVisible = ref(false)
const dialogTitle = ref('')
const deviceFormRef = ref()
// è®¾å¤‡è¡¨å•数据
const deviceForm = reactive({
  deviceCode: '',
  deviceName: '',
  deviceType: '',
  specification: '',
  designPressure: 0,
  volume: 0
})
// è¡¨å•验证规则
const deviceRules = {
  deviceCode: [{ required: true, message: '请输入设备编号', trigger: 'blur' }],
  deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
  deviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
  specification: [{ required: true, message: '请输入规格型号', trigger: 'blur' }],
  designPressure: [{ required: true, message: '请输入设计压力', trigger: 'blur' }],
  volume: [{ required: true, message: '请输入容积', trigger: 'blur' }]
}
// å®šæ—¶å™¨
let updateTimer = null
// æ¨¡æ‹Ÿæ•°æ®ç”Ÿæˆ
const generateMockData = () => {
  // æ›´æ–°ç»Ÿè®¡æ•°æ®
  totalDevices.value = Math.floor(Math.random() * 10) + 15 // 15-25台设备
  dailyConsumption.value = Math.floor(Math.random() * 100) + 200 // 200-300 m³
  monthlyConsumption.value = Math.floor(Math.random() * 2000) + 5000 // 5000-7000 m³
  gasUnitPrice.value = (Math.random() * 2 + 3).toFixed(2) // 3-5元/m³
  // è®¡ç®—费用
  dailyTotalCost.value = dailyConsumption.value * gasUnitPrice.value
  monthlyTotalCost.value = monthlyConsumption.value * gasUnitPrice.value
  // æ›´æ–°è®¾å¤‡åˆ—表数据
  deviceList.value = Array.from({ length: totalDevices.value }, (_, index) => ({
    id: index + 1,
    deviceCode: `GT${String(index + 1).padStart(3, '0')}`,
    deviceName: `储气罐${index + 1}`,
    deviceType: ['液化气储罐', '压缩气储罐', '天然气储罐', '氧气储罐'][Math.floor(Math.random() * 4)],
    specification: `${Math.floor(Math.random() * 50) + 50}m³`,
    currentPressure: (Math.random() * 2 + 0.5).toFixed(2),
    currentTemperature: (Math.random() * 20 + 15).toFixed(1),
    gasConcentration: (Math.random() * 10).toFixed(2),
    status: ['running', 'stopped', 'warning', 'error'][Math.floor(Math.random() * 4)],
    lastUpdate: new Date().toLocaleString()
  }))
  // æ›´æ–°æœ€åŽæ›´æ–°æ—¶é—´
  lastUpdateTime.value = new Date().toLocaleString()
}
// èŽ·å–åŽ‹åŠ›çŠ¶æ€æ ·å¼
const getPressureClass = (pressure) => {
  const p = parseFloat(pressure)
  if (p < 0.8) return 'pressure-low'
  if (p > 1.5) return 'pressure-high'
  return 'pressure-normal'
}
// èŽ·å–æ¸©åº¦çŠ¶æ€æ ·å¼
const getTemperatureClass = (temperature) => {
  const t = parseFloat(temperature)
  if (t < 10 || t > 35) return 'temperature-warning'
  return 'temperature-normal'
}
// èŽ·å–æµ“åº¦çŠ¶æ€æ ·å¼
const getConcentrationClass = (concentration) => {
  const c = parseFloat(concentration)
  if (c > 5) return 'concentration-warning'
  return 'concentration-normal'
}
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    running: 'success',
    stopped: 'info',
    warning: 'warning',
    error: 'danger'
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    running: '运行中',
    stopped: '已停止',
    warning: '警告',
    error: '故障'
  }
  return statusMap[status] || '未知'
}
// åˆ·æ–°æ•°æ®
const refreshData = () => {
  generateMockData()
  ElMessage.success('数据已刷新')
}
// æ·»åŠ è®¾å¤‡
const addDevice = () => {
  dialogTitle.value = '添加设备'
  Object.keys(deviceForm).forEach(key => {
    deviceForm[key] = key === 'designPressure' || key === 'volume' ? 0 : ''
  })
  deviceDialogVisible.value = true
}
// ç¼–辑设备
const editDevice = (row) => {
  dialogTitle.value = '编辑设备'
  Object.keys(deviceForm).forEach(key => {
    if (row[key] !== undefined) {
      deviceForm[key] = row[key]
    }
  })
  deviceDialogVisible.value = true
}
// ä¿å­˜è®¾å¤‡
const saveDevice = () => {
  deviceFormRef.value.validate((valid) => {
    if (valid) {
      ElMessage.success('保存成功')
      deviceDialogVisible.value = false
      refreshData()
    }
  })
}
// å¯åŠ¨å®šæ—¶æ›´æ–°
const startAutoUpdate = () => {
  updateTimer = setInterval(() => {
    generateMockData()
  }, 60000) // æ¯åˆ†é’Ÿæ›´æ–°ä¸€æ¬¡
}
// åœæ­¢å®šæ—¶æ›´æ–°
const stopAutoUpdate = () => {
  if (updateTimer) {
    clearInterval(updateTimer)
    updateTimer = null
  }
}
// ç»„件挂载
onMounted(() => {
  generateMockData()
  startAutoUpdate()
})
// ç»„件卸载
onUnmounted(() => {
  stopAutoUpdate()
})
</script>
<style lang="scss" scoped>
.app-container {
  padding: 20px;
  background: #f5f5f5;
  min-height: 100vh;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  h2 {
    margin: 0;
    color: #303133;
    font-size: 24px;
  }
  .header-info {
    display: flex;
    align-items: center;
    gap: 15px;
    .update-time {
      color: #909399;
      font-size: 14px;
    }
  }
}
.stats-cards {
  margin-bottom: 20px;
  .stat-card {
    .stat-content {
      display: flex;
      align-items: center;
      padding: 10px;
      .stat-icon {
        width: 60px;
        height: 60px;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        margin-right: 15px;
        &.gas-device {
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          color: white;
        }
        &.daily-consumption {
          background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
          color: white;
        }
        &.monthly-consumption {
          background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
          color: white;
        }
        &.gas-price {
          background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
          color: white;
        }
      }
      .stat-info {
        .stat-value {
          font-size: 24px;
          font-weight: bold;
          color: #303133;
          line-height: 1;
        }
        .stat-label {
          font-size: 14px;
          color: #909399;
          margin-top: 5px;
        }
      }
    }
  }
}
.cost-stats {
  margin-bottom: 20px;
  .cost-card {
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .cost-content {
      text-align: center;
      padding: 20px 0;
      .cost-main {
        margin-bottom: 15px;
        .cost-amount {
          font-size: 36px;
          font-weight: bold;
          color: #409eff;
        }
        .cost-unit {
          font-size: 16px;
          color: #909399;
          margin-left: 5px;
        }
      }
      .cost-details {
        .cost-item {
          display: flex;
          justify-content: space-between;
          margin-bottom: 8px;
          font-size: 14px;
          color: #606266;
          &:last-child {
            margin-bottom: 0;
          }
        }
      }
    }
  }
}
.device-section {
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .header-actions {
      display: flex;
      gap: 10px;
    }
  }
}
// çŠ¶æ€æ ·å¼
.pressure-low {
  color: #e6a23c;
  font-weight: bold;
}
.pressure-normal {
  color: #67c23a;
  font-weight: bold;
}
.pressure-high {
  color: #f56c6c;
  font-weight: bold;
}
.temperature-normal {
  color: #67c23a;
  font-weight: bold;
}
.temperature-warning {
  color: #e6a23c;
  font-weight: bold;
}
.concentration-normal {
  color: #67c23a;
  font-weight: bold;
}
.concentration-warning {
  color: #f56c6c;
  font-weight: bold;
}
</style>
src/views/equipmentManagement/iotMonitor/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,317 @@
<template>
  <div class="app-container iot-monitor">
    <div class="header">
      <div class="title">实时工况监控(IoT)</div>
      <div class="actions">
        <el-button type="primary" @click="toggleCollecting">{{ collecting ? '暂停采集' : '启动采集' }}</el-button>
        <el-button @click="resetAll">重置</el-button>
        <span class="ts">上次更新时间:{{ lastUpdatedDisplay }}</span>
      </div>
    </div>
<!--    <el-alert-->
<!--      title="边缘预警规则:轴承磨损-振动值偏离基线±5%触发告警;温度/压力越界触发提醒"-->
<!--      type="info"-->
<!--      :closable="false"-->
<!--      show-icon-->
<!--      class="rule-alert"-->
<!--    />-->
    <el-row :gutter="16">
      <el-col v-for="dev in devices" :key="dev.id" :span="12">
        <el-card :class="['device-card', dev.hasAlert ? 'is-alert' : '']">
          <template #header>
            <div class="card-header">
              <div class="card-title">
                <span class="device-name">{{ dev.name }}</span>
                <el-tag :type="dev.hasAlert ? 'danger' : 'success'" size="small">{{ dev.hasAlert ? '告警' : '正常' }}</el-tag>
              </div>
              <div class="meta">类型:{{ dev.type }}|基线振动:{{ dev.baseline.vibration.toFixed(2) }} mm/s</div>
            </div>
          </template>
          <div class="metrics">
            <div class="metric" :class="{ 'metric-alert': dev.alerts.vibration }">
              <div class="metric-head">
                <span>振动(mm/s)</span>
                <el-tag :type="dev.alerts.vibration ? 'danger' : 'info'" size="small">{{ dev.alerts.vibration ? '±5%越界' : '基线±5%' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.vibration).toFixed(2) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: 'mm/s' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.vibration }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#409EFF']"
              />
            </div>
            <div class="metric" :class="{ 'metric-alert': dev.alerts.temperature }">
              <div class="metric-head">
                <span>温度(°C)</span>
                <el-tag :type="dev.alerts.temperature ? 'warning' : 'info'" size="small">{{ dev.alerts.temperature ? '越界' : '20~80' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.temperature).toFixed(1) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: '°C' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.temperature }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#E6A23C']"
              />
            </div>
            <div class="metric" :class="{ 'metric-alert': dev.alerts.pressure }">
              <div class="metric-head">
                <span>压力(MPa)</span>
                <el-tag :type="dev.alerts.pressure ? 'warning' : 'info'" size="small">{{ dev.alerts.pressure ? '越界' : '0.2~1.5' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.pressure).toFixed(2) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: 'MPa' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.pressure }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#67C23A']"
              />
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue'
import { ElNotification } from 'element-plus'
import Echarts from '@/components/Echarts/echarts.vue'
defineOptions({ name: 'IoTMonitor' })
const windowSize = 30
const collecting = ref(true)
const lastUpdated = ref(Date.now())
const lastUpdatedDisplay = computed(() => new Date(lastUpdated.value).toLocaleTimeString())
const xAxisLabels = ref(Array.from({ length: windowSize }, (_, i) => i - (windowSize - 1)).map(n => `${n}s`))
function makeSeries(fill, decimals = 2) {
  return Array.from({ length: windowSize }, () => Number(fill.toFixed(decimals)))
}
const devices = reactive([
  {
    id: 'water-pump',
    name: '注水泵1',
    type: '移动装备',
    baseline: { vibration: 9 },
    initial: { temperature: 40, pressure: 0.70 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(9),
      temperature: makeSeries(40, 1),
      pressure: makeSeries(0.7, 2),
    },
  },
  {
    id: 'fluid-supply-truck',
    name: '注水泵2',
    type: '移动装备',
    baseline: { vibration: 7 },
    initial: { temperature: 30, pressure: 0.60 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(7),
      temperature: makeSeries(30, 1),
      pressure: makeSeries(0.6, 2),
    },
  },
  {
    id: 'fracturing-truck',
    name: '注水泵3',
    type: '移动装备',
    baseline: { vibration: 12 },
    initial: { temperature: 65, pressure: 1.40 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(12),
      temperature: makeSeries(65, 1),
      pressure: makeSeries(1.4, 2),
    },
  },
  {
    id: 'oil-tank-truck',
    name: '注水泵4',
    type: '移动装备',
    baseline: { vibration: 6 },
    initial: { temperature: 28, pressure: 0.50 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(6),
      temperature: makeSeries(28, 1),
      pressure: makeSeries(0.5, 2),
    },
  },
])
function currentValue(arr) {
  return arr[arr.length - 1] ?? 0
}
function pushWindow(arr, val) {
  if (arr.length >= windowSize) arr.shift()
  arr.push(val)
}
function clamp(val, min, max) { return Math.max(min, Math.min(max, val)) }
function tickDevice(dev) {
  const vibBase = dev.baseline.vibration
  // æŒ¯åŠ¨ï¼šåŸºçº¿Â±2%随机波动;5%概率触发8%~12%尖峰模拟告警
  const spike = Math.random() < 0.05
  const vibNoise = vibBase * (spike ? (1 + (Math.random() * 0.08 + 0.04) * (Math.random() < 0.5 ? -1 : 1)) : (1 + (Math.random() - 0.5) * 0.04))
  const vibVal = Number(vibNoise.toFixed(2))
  pushWindow(dev.series.vibration, vibVal)
  // æ¸©åº¦ï¼šç¼“慢随机游走,并添加偶发高温偏移
  const tPrev = currentValue(dev.series.temperature)
  const tDrift = tPrev + (Math.random() - 0.5) * 0.8 + (Math.random() < 0.02 ? 6 : 0)
  const tVal = Number(clamp(tDrift, 15, 95).toFixed(1))
  pushWindow(dev.series.temperature, tVal)
  // åŽ‹åŠ›ï¼šå°å¹…æ³¢åŠ¨ï¼Œå¶å‘ä½ŽåŽ‹/高压
  const pPrev = currentValue(dev.series.pressure)
  const pDrift = pPrev + (Math.random() - 0.5) * 0.05 + (Math.random() < 0.02 ? (Math.random() < 0.5 ? -0.3 : 0.3) : 0)
  const pVal = Number(clamp(pDrift, 0.05, 2.0).toFixed(2))
  pushWindow(dev.series.pressure, pVal)
  // è¾¹ç¼˜è®¡ç®—阈值判断
  const vibDelta = Math.abs(vibVal - vibBase) / vibBase
  const vibAlert = vibDelta > 0.05
  const tAlert = tVal < 20 || tVal > 80
  const pAlert = pVal < 0.2 || pVal > 1.5
  const prevHasAlert = dev.hasAlert
  dev.alerts.vibration = vibAlert
  dev.alerts.temperature = tAlert
  dev.alerts.pressure = pAlert
  dev.hasAlert = vibAlert || tAlert || pAlert
  if (dev.hasAlert && !prevHasAlert) {
    const reasons = []
    if (vibAlert) reasons.push(`振动偏离±5% (当前 ${vibVal} / åŸºçº¿ ${vibBase})`)
    if (tAlert) reasons.push(`温度越界 (当前 ${tVal}°C, æœŸæœ› 20~80°C) `)
    if (pAlert) reasons.push(`压力越界 (当前 ${pVal}MPa, æœŸæœ› 0.2~1.5MPa) `)
    ElNotification({
      title: `${dev.name} å‘Šè­¦`,
      message: reasons.join(';'),
      type: vibAlert ? 'error' : 'warning',
      duration: 5000,
    })
  }
}
let timer = null
function start() {
  if (timer) return
  timer = setInterval(() => {
    if (!collecting.value) return
    devices.forEach(tickDevice)
    lastUpdated.value = Date.now()
  }, 10000)
}
function stop() {
  if (timer) {
    clearInterval(timer)
    timer = null
  }
}
function toggleCollecting() { collecting.value = !collecting.value }
function resetAll() {
  devices.forEach(dev => {
    dev.series.vibration = makeSeries(dev.baseline.vibration)
    const t0 = dev.initial?.temperature ?? 45
    const p0 = dev.initial?.pressure ?? 0.8
    dev.series.temperature = makeSeries(t0, 1)
    dev.series.pressure = makeSeries(p0, 2)
    dev.alerts.vibration = false
    dev.alerts.temperature = false
    dev.alerts.pressure = false
    dev.hasAlert = false
  })
  lastUpdated.value = Date.now()
}
onMounted(() => {
  start()
})
onBeforeUnmount(() => {
  stop()
})
</script>
<style lang="scss" scoped>
.iot-monitor {
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
    .title { font-size: 18px; font-weight: 600; }
    .actions { display: flex; align-items: center; gap: 8px; }
    .ts { color: #909399; font-size: 12px; }
  }
  .rule-alert { margin-bottom: 12px; }
}
.device-card {
  margin-bottom: 16px;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
  &.is-alert { border-color: #F56C6C; box-shadow: 0 0 0 2px rgba(245,108,108,0.2) inset; }
  .card-header {
    display: flex; flex-direction: column; gap: 4px;
    .card-title { display: flex; align-items: center; gap: 8px; font-weight: 600; }
    .meta { color: #909399; font-size: 12px; }
  }
  .metrics {
    display: grid;
    grid-template-columns: 1fr;
    gap: 12px;
  }
}
.metric {
  border: 1px solid #ebeef5;
  border-radius: 6px;
  padding: 8px 8px 0 8px;
  &-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; font-size: 13px; color: #606266; }
  &-value { font-size: 20px; font-weight: 600; margin: 2px 0 6px 0; }
}
.metric-alert {
  border-color: #F56C6C;
  background: #FFF6F6;
}
@media (min-width: 1200px) {
  .device-card .metrics { grid-template-columns: 1fr 1fr 1fr; }
}
</style>
src/views/equipmentManagement/ledger/Form.vue
@@ -8,7 +8,7 @@
      </el-col>
      <el-col :span="12">
        <el-form-item label="规格型号" prop="deviceModel">
          <el-input v-model="form.deviceModel" :disabled="form.deviceModel != null ? true : false" placeholder="请输入规格型号" />
          <el-input v-model="form.deviceModel" :disabled="(form.deviceModel != null && operationType === 'edit')" placeholder="请输入规格型号" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
@@ -119,6 +119,7 @@
  name: "设备台账表单",
});
const formRef = ref(null);
const operationType = ref('');
const formRules = {
    deviceName: [{ required: true, trigger: "blur", message: "请输入" }],
    deviceModel: [{ required: true, trigger: "blur", message: "请输入" }],
@@ -144,6 +145,9 @@
});
const loadForm = async (id) => {
    if (id) {
        operationType.value = 'edit'
    }
  const { code, data } = await getLedgerById(id);
  if (code == 200) {
    form.deviceName = data.deviceName;
src/views/login.vue
@@ -86,8 +86,8 @@
const { proxy } = getCurrentInstance()
const loginForm = ref({
  username: "admin",
  password: "admin123",
  username: "",
  password: "",
  rememberMe: false,
  currentFatoryId:'',
})
@@ -181,7 +181,7 @@
<style lang='scss' scoped>
.login {
  height: 100%;
  background-image: url("../assets/indexViews/HYSNView.png");
  background-image: url("../assets/indexViews/ZQHXView.png");
  background-size: cover;
  position: relative;
}
src/views/personnelManagement/contractManagement/filesDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="上传附件"
        width="50%"
        @close="closeDia"
    >
      <div style="margin-bottom: 10px;text-align: right">
        <el-upload
            v-model:file-list="fileList"
            class="upload-demo"
            :action="uploadUrl"
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            name="file"
            :show-file-list="false"
            :headers="headers"
            style="display: inline;margin-right: 10px"
        >
          <el-button type="primary">上传附件</el-button>
        </el-upload>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :tableLoading="tableLoading"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          height="500"
      >
      </PIMTable>
            <pagination
                style="margin: 10px 0"
                v-show="total > 0"
                @pagination="paginationSearch"
                :total="total"
                :page="page.current"
                :limit="page.size"
            />
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <filePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import {ref} from "vue";
import {ElMessageBox} from "element-plus";
import {getToken} from "@/utils/auth.js";
import filePreview from '@/components/filePreview/index.vue'
import {
  fileAdd,
  fileDel,
  fileListPage
} from "@/api/financialManagement/revenueManagement.js";
import Pagination from "@/components/PIMTable/Pagination.vue";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const currentId = ref('')
const selectedRows = ref([]);
const filePreviewRef = ref()
const tableColumn = ref([
  {
    label: "文件名称",
    prop: "name",
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    operation: [
      {
        name: "下载",
        type: "text",
        clickFun: (row) => {
          downLoadFile(row);
        },
      },
      {
        name: "预览",
        type: "text",
        clickFun: (row) => {
          lookFile(row);
        },
      }
    ],
  },
]);
const page = reactive({
    current: 1,
    size: 100,
});
const total = ref(0);
const tableData = ref([]);
const fileList = ref([]);
const tableLoading = ref(false);
const accountType = ref('')
const headers = ref({
  Authorization: "Bearer " + getToken(),
});
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // ä¸Šä¼ çš„图片服务器地址
// æ‰“开弹框
const openDialog = (row,type) => {
  accountType.value = type;
  dialogFormVisible.value = true;
  currentId.value = row.id;
  getList()
}
const paginationSearch = (obj) => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
};
const getList = () => {
  fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => {
    tableData.value = res.data.records;
        total.value = res.data.total;
  })
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  dialogFormVisible.value = false;
  emit('close')
};
// ä¸Šä¼ æˆåŠŸå¤„ç†
function handleUploadSuccess(res, file) {
  // å¦‚果上传成功
  if (res.code == 200) {
    const fileRow = {}
    fileRow.name = res.data.originalName
    fileRow.url = res.data.tempPath
    uploadFile(fileRow)
  } else {
    proxy.$modal.msgError("文件上传失败");
  }
}
function uploadFile(file) {
  file.accountId = currentId.value;
  file.accountType = accountType.value;
  fileAdd(file).then(res => {
    proxy.$modal.msgSuccess("文件上传成功");
    getList()
  })
}
// ä¸Šä¼ å¤±è´¥å¤„理
function handleUploadError() {
  proxy.$modal.msgError("文件上传失败");
}
// ä¸‹è½½é™„ä»¶
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
}
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    fileDel(ids).then((res) => {
      proxy.$modal.msgSuccess("删除成功");
      getList();
    });
  }).catch(() => {
    proxy.$modal.msg("已取消");
  });
};
// é¢„览附件
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
}
defineExpose({
  openDialog,
});
</script>
<style scoped>
</style>
src/views/personnelManagement/contractManagement/index.vue
@@ -12,7 +12,7 @@
      </div>
      <div>
        <!--        <el-button type="primary" @click="openForm('add')">新增入职</el-button>-->
        <el-button type="info" @click="handleImport">导入</el-button>
<!--        <el-button type="info" @click="handleImport">导入</el-button>-->
        <el-button @click="handleOut">导出</el-button>
        <!--        <el-button type="danger" plain @click="handleDelete">删除</el-button>-->
      </div>
@@ -65,6 +65,7 @@
        </div>
      </template>
    </el-dialog>
    <files-dia ref="filesDia"></files-dia>
  </div>
</template>
@@ -76,7 +77,7 @@
import { staffOnJobListPage } from "@/api/personnelManagement/employeeRecord.js";
import dayjs from "dayjs";
import { getToken } from "@/utils/auth.js";
import FilesDia from "./filesDia.vue";
const data = reactive({
  searchForm: {
    staffName: "",
@@ -190,6 +191,7 @@
    label: "操作",
    align: "center",
    fixed: 'right',
    width: 120,
    operation: [
      {
        name: "详情",
@@ -198,9 +200,17 @@
          openForm("edit", row);
        },
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => {
          openFilesFormDia(row);
        },
      },
    ],
  },
]);
const filesDia = ref()
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
@@ -221,6 +231,13 @@
  }
  getList();
};
// æ‰“开附件弹框
const openFilesFormDia = (row) => {
  console.log(row)
  nextTick(() => {
    filesDia.value?.openDialog( row,'合同')
  })
};
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
src/views/procurementManagement/procurementLedger/index.vue
@@ -145,7 +145,7 @@
        <el-table-column
          fixed="right"
          label="操作"
          min-width="60"
          min-width="150"
          align="center"
        >
          <template #default="scope">
@@ -156,6 +156,13 @@
              @click="openForm('edit', scope.row)"
                            :disabled="scope.row.receiptPaymentAmount>0 || scope.row.recorderName !== userStore.nickName"
              >编辑</el-button
            >
            <el-button
              link
              type="success"
              size="small"
              @click="showQRCode(scope.row)"
              >生成二维码</el-button
            >
          </template>
        </el-table-column>
@@ -539,13 +546,28 @@
        </div>
      </template>
    </el-dialog>
    <!-- äºŒç»´ç æ˜¾ç¤ºå¯¹è¯æ¡† -->
    <el-dialog
      v-model="qrCodeDialogVisible"
      title="采购合同号二维码"
      width="400px"
      center
    >
      <div style="text-align: center;">
        <img :src="qrCodeUrl" alt="二维码" style="width:200px;height:200px;" />
        <div style="margin: 20px;">
          <el-button type="primary" @click="downloadQRCode">下载二维码图片</el-button>
        </div>
      </div>
    </el-dialog>
  </div>
</template>
<script setup>
import { getToken } from "@/utils/auth";
import pagination from "@/components/PIMTable/Pagination.vue";
import { ref, onMounted } from "vue";
import { ref, onMounted, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus";
import { userListNoPage } from "@/api/system/user.js";
@@ -567,6 +589,7 @@
  createPurchaseNo,
} from "@/api/procurementManagement/procurementLedger.js";
import useFormData from "@/hooks/useFormData.js";
import QRCode from "qrcode";
const { proxy } = getCurrentInstance();
const tableData = ref([]);
const productData = ref([]);
@@ -589,6 +612,10 @@
import dayjs from "dayjs";
const userStore = useUserStore();
// äºŒç»´ç ç›¸å…³å˜é‡
const qrCodeDialogVisible = ref(false);
const qrCodeUrl = ref("");
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
@@ -1152,6 +1179,47 @@
  }
};
// æ˜¾ç¤ºäºŒç»´ç 
const showQRCode = async (row) => {
  try {
    // æž„建二维码内容,只包含采购合同号(纯文本)
    const qrContent = row.purchaseContractNumber || '';
    // æ£€æŸ¥å†…容是否为空
    if (!qrContent || qrContent.trim() === '') {
      proxy.$modal.msgWarning("该行没有采购合同号,无法生成二维码");
      return;
    }
    qrCodeUrl.value = await QRCode.toDataURL(qrContent, {
      width: 200,
      margin: 2,
      color: {
        dark: '#000000',
        light: '#FFFFFF'
      }
    });
    qrCodeDialogVisible.value = true;
  } catch (error) {
    console.error('生成二维码失败:', error);
    proxy.$modal.msgError("生成二维码失败:" + error.message);
  }
};
// ä¸‹è½½äºŒç»´ç 
const downloadQRCode = () => {
  if (!qrCodeUrl.value) {
    proxy.$modal.msgWarning("二维码未生成");
    return;
  }
  const a = document.createElement('a');
  a.href = qrCodeUrl.value;
  a.download = `采购合同号二维码_${new Date().getTime()}.png`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  proxy.$modal.msgSuccess("下载成功");
};
onMounted(() => {
  getList();
});
src/views/productionManagement/safetyMonitoring/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,873 @@
<template>
    <div class="safety-monitoring">
        <el-row :gutter="20">
            <!-- å·¦ä¾§ï¼šå®žæ—¶ç›‘控区域 -->
            <el-col :span="16">
                <el-card class="monitoring-card">
                    <div slot="header" class="card-header">
                        <span>实时气体浓度监控</span>
                        <el-tag :type="systemStatus === 'normal' ? 'success' : 'danger'">
                            {{ systemStatus === 'normal' ? '系统正常' : '系统告警' }}
                        </el-tag>
                    </div>
                    <!-- å‚¨ç½åŒºç›‘控 -->
                    <div class="monitoring-section">
                        <h3>储罐区监控</h3>
                        <div class="sensor-grid">
                            <div class="sensor-item" v-for="sensor in tankSensors" :key="sensor.id">
                                <div class="sensor-header">
                                    <span>{{ sensor.name }}</span>
                                    <el-tag :type="sensor.status === 'normal' ? 'success' : 'danger'" size="small">
                                        {{ sensor.status === 'normal' ? '正常' : '超标' }}
                                    </el-tag>
                                </div>
                                <div class="sensor-data">
                                    <div class="data-item">
                                        <span>甲烷: {{ sensor.methane.toFixed(2) }}%</span>
                                        <el-progress
                                            :percentage="Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100)"
                                            :color="getProgressColor(Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100), 80)"
                                            :format="formatProgress"
                                            :stroke-width="8"
                                        />
                                    </div>
                                    <div class="data-item">
                                        <span>硫化氢: {{ sensor.h2s.toFixed(2) }}ppm</span>
                                        <el-progress
                                            :percentage="Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100)"
                                            :color="getProgressColor(Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100), 80)"
                                            :format="formatProgress"
                                            :stroke-width="8"
                                        />
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                    <!-- äº•口压缩机监控 -->
                    <div class="monitoring-section">
                        <h3>井口压缩机监控</h3>
                        <div class="sensor-grid">
                            <div class="sensor-item" v-for="sensor in compressorSensors" :key="sensor.id">
                                <div class="sensor-header">
                                    <span>{{ sensor.name }}</span>
                                    <el-tag :type="sensor.status === 'normal' ? 'success' : 'danger'" size="small">
                                        {{ sensor.status === 'normal' ? '正常' : '超标' }}
                                    </el-tag>
                                </div>
                                <div class="sensor-data">
                                    <div class="data-item">
                                        <span>甲烷: {{ sensor.methane.toFixed(2) }}%</span>
                                        <el-progress
                                            :percentage="Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100)"
                                            :color="getProgressColor(sensor.methane, 2.5)"
                                            :format="formatProgress"
                                            :stroke-width="8"
                                        />
                                    </div>
                                    <div class="data-item">
                                        <span>硫化氢: {{ sensor.h2s.toFixed(2) }}ppm</span>
                                        <el-progress
                                            :percentage="Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100)"
                                            :color="getProgressColor(sensor.h2s, 10)"
                                            :format="formatProgress"
                                            :stroke-width="8"
                                        />
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                    <!-- å®žæ—¶æ›²çº¿å›¾ -->
                    <div class="chart-section">
                        <h3>实时浓度曲线</h3>
                        <div class="chart-container">
                            <div ref="chart" class="chart"></div>
                        </div>
                    </div>
                </el-card>
            </el-col>
            <!-- å³ä¾§ï¼šæŽ§åˆ¶é¢æ¿ -->
            <el-col :span="8">
                <el-card class="control-card">
                    <div slot="header" class="card-header">
                        <span>应急控制面板</span>
                    </div>
                    <!-- å–·æ·‹çŠ¶æ€ -->
                    <div class="control-section">
                        <h4>喷淋系统状态</h4>
                        <div class="status-grid">
                            <div class="status-item" v-for="sprinkler in sprinklerSystems" :key="sprinkler.id">
                                <div class="status-indicator" :class="sprinkler.status">
                                    <i class="el-icon-circle-check" v-if="sprinkler.status === 'active'"></i>
                                    <i class="el-icon-circle-close" v-else></i>
                                </div>
                                <span>{{ sprinkler.name }}</span>
                                <el-tag :type="sprinkler.status === 'active' ? 'success' : 'info'" size="small">
                                    {{ sprinkler.status === 'active' ? '运行中' : '待机' }}
                                </el-tag>
                            </div>
                        </div>
                    </div>
                    <!-- åº”急记录按钮 -->
                    <h4>应急管理</h4>
                    <div class="control-section1">
                        <el-button type="primary" @click="showEmergencyRecords" style="margin-bottom: 10px;">
                            åº”急记录
                        </el-button>
                        <el-button type="warning" @click="triggerEmergency" :disabled="!hasEmergency">
                            è§¦å‘应急响应
                        </el-button>
                    </div>
                    <!-- ç³»ç»Ÿæ—¥å¿— -->
                    <div class="control-section">
                        <h4>系统日志</h4>
                        <div class="log-container">
                            <div class="log-item" v-for="log in systemLogs" :key="log.id">
                                <span class="log-time">{{ log.time }}</span>
                                <span class="log-content">{{ log.content }}</span>
                            </div>
                        </div>
                    </div>
                </el-card>
            </el-col>
        </el-row>
        <!-- æ³„漏预警弹窗 -->
        <el-dialog
            title="⚠️ æ³„漏预警"
            :visible.sync="leakWarningVisible"
            width="500px"
            :close-on-click-modal="false"
            :close-on-press-escape="false"
            class="leak-warning-dialog"
        >
            <div class="warning-content">
                <div class="warning-icon">
                    <i class="el-icon-warning"></i>
                </div>
                <div class="warning-text">
                    <h3>检测到气体浓度超标!</h3>
                    <p>位置:{{ currentWarning.location }}</p>
                    <p>超标气体:{{ currentWarning.gas }}</p>
                    <p>当前浓度:{{ currentWarning.value }}</p>
                </div>
            </div>
            <div slot="footer" class="dialog-footer">
                <el-button type="danger" @click="acknowledgeWarning">确认告警</el-button>
                <el-button type="primary" @click="viewDetails">查看详情</el-button>
            </div>
        </el-dialog>
        <!-- åº”急记录弹窗 -->
        <el-dialog
            title="应急记录"
            :visible.sync="emergencyRecordsVisible"
            width="800px"
        >
            <el-table :data="emergencyRecords" style="width: 100%">
                <el-table-column prop="time" label="时间" width="180"></el-table-column>
                <el-table-column prop="location" label="位置" width="150"></el-table-column>
                <el-table-column prop="type" label="类型" width="120"></el-table-column>
                <el-table-column prop="status" label="状态" width="100">
                    <template slot-scope="scope">
                        <el-tag :type="scope.row.status === 'resolved' ? 'success' : 'warning'">
                            {{ scope.row.status === 'resolved' ? '已解决' : '处理中' }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column prop="description" label="描述"></el-table-column>
                <el-table-column label="操作" width="120">
                    <template slot-scope="scope">
                        <el-button type="text" @click="viewBlockchainDetails(scope.row)">
                            åŒºå—链详情
                        </el-button>
                    </template>
                </el-table-column>
            </el-table>
        </el-dialog>
        <!-- åŒºå—链存证详情弹窗 -->
        <el-dialog
            title="区块链存证详情"
            :visible.sync="blockchainDetailsVisible"
            width="900px"
        >
            <div class="blockchain-details">
                <el-descriptions :column="2" border>
                    <el-descriptions-item label="事件ID">{{ currentEvent.id }}</el-descriptions-item>
                    <el-descriptions-item label="时间戳">{{ currentEvent.timestamp }}</el-descriptions-item>
                    <el-descriptions-item label="位置">{{ currentEvent.location }}</el-descriptions-item>
                    <el-descriptions-item label="事件类型">{{ currentEvent.type }}</el-descriptions-item>
                </el-descriptions>
                <div class="sensor-data-section">
                    <h4>传感器数据</h4>
                    <el-table :data="currentEvent.sensorData" style="width: 100%">
                        <el-table-column prop="sensor" label="传感器"></el-table-column>
                        <el-table-column prop="methane" label="甲烷浓度"></el-table-column>
                        <el-table-column prop="h2s" label="硫化氢浓度"></el-table-column>
                        <el-table-column prop="timestamp" label="记录时间"></el-table-column>
                    </el-table>
                </div>
                <div class="action-log-section">
                    <h4>处置动作记录</h4>
                    <el-timeline>
                        <el-timeline-item
                            v-for="action in currentEvent.actions"
                            :key="action.id"
                            :timestamp="action.timestamp"
                            :type="action.type === 'emergency' ? 'danger' : 'primary'"
                        >
                            {{ action.description }}
                        </el-timeline-item>
                    </el-timeline>
                </div>
                <div class="blockchain-info">
                    <h4>区块链信息</h4>
                    <el-descriptions :column="1" border>
                        <el-descriptions-item label="区块哈希">{{ currentEvent.blockHash }}</el-descriptions-item>
                        <el-descriptions-item label="交易哈希">{{ currentEvent.txHash }}</el-descriptions-item>
                        <el-descriptions-item label="确认数">{{ currentEvent.confirmations }}</el-descriptions-item>
                    </el-descriptions>
                </div>
            </div>
        </el-dialog>
    </div>
</template>
<script>
import * as echarts from 'echarts'
export default {
    name: 'SafetyMonitoring',
    data() {
        return {
            systemStatus: 'normal',
            leakWarningVisible: false,
            emergencyRecordsVisible: false,
            blockchainDetailsVisible: false,
            currentWarning: {},
            currentEvent: {},
            hasEmergency: false,
            // å‚¨ç½åŒºä¼ æ„Ÿå™¨æ•°æ®
            tankSensors: [
                { id: 1, name: '储罐T-001', methane: 1.20, h2s: 2.10, status: 'normal' },
                { id: 2, name: '储罐T-002', methane: 0.80, h2s: 1.50, status: 'normal' },
                { id: 3, name: '储罐T-003', methane: 3.20, h2s: 8.50, status: 'warning' },
                { id: 4, name: '储罐T-004', methane: 0.60, h2s: 0.80, status: 'normal' }
            ],
            // äº•口压缩机传感器数据
            compressorSensors: [
                { id: 5, name: '压缩机C-001', methane: 2.10, h2s: 3.20, status: 'normal' },
                { id: 6, name: '压缩机C-002', methane: 4.80, h2s: 12.50, status: 'warning' },
                { id: 7, name: '压缩机C-003', methane: 1.80, h2s: 2.80, status: 'normal' }
            ],
            // å–·æ·‹ç³»ç»ŸçŠ¶æ€
            sprinklerSystems: [
                { id: 1, name: '储罐区喷淋', status: 'active' },
                { id: 2, name: '压缩机区喷淋', status: 'standby' },
                { id: 3, name: '紧急喷淋', status: 'standby' }
            ],
            // ç³»ç»Ÿæ—¥å¿—
            systemLogs: [
                { id: 1, time: '14:30:25', content: '系统启动完成,所有传感器正常' },
                { id: 2, time: '14:35:12', content: '储罐T-003甲烷浓度超标,触发预警' },
                { id: 3, time: '14:35:15', content: '启动储罐区喷淋系统' },
                { id: 4, time: '14:35:20', content: '发送紧急疏散广播' }
            ],
            // åº”急记录
            emergencyRecords: [
                {
                    id: 'EM001',
                    time: '2024-01-15 14:35:12',
                    location: '储罐T-003',
                    type: '甲烷超标',
                    status: 'resolved',
                    description: '储罐T-003甲烷浓度达到3.2%,超过安全阈值2.5%'
                },
                {
                    id: 'EM002',
                    time: '2024-01-15 14:35:15',
                    location: '压缩机C-002',
                    type: '硫化氢超标',
                    status: 'processing',
                    description: '压缩机C-002硫化氢浓度达到12.5ppm,超过安全阈值10ppm'
                }
            ],
            // å›¾è¡¨å®žä¾‹
            chart: null,
            // å®šæ—¶å™¨
            timer: null
        }
    },
    mounted() {
        this.initChart()
        this.startDataRefresh()
        this.checkEmergencyStatus()
    },
    beforeDestroy() {
        if (this.timer) {
            clearInterval(this.timer)
        }
        if (this.chart) {
            this.chart.dispose()
        }
    },
    methods: {
        // ç»Ÿä¸€è¿›åº¦æ¡æ ¼å¼åŒ–为两位小数,避免浮点误差显示
        formatProgress(percentage) {
            if (percentage == null || isNaN(percentage)) return '0.00%'
            const val = Math.round(Number(percentage) * 100) / 100
            return `${val.toFixed(2)}%`
        },
        // åˆå§‹åŒ–图表
        initChart() {
            this.chart = echarts.init(this.$refs.chart)
            this.updateChart()
        },
        // æ›´æ–°å›¾è¡¨æ•°æ®
        updateChart() {
            const option = {
                title: {
                    text: '实时气体浓度监控',
                    left: 'center'
                },
                tooltip: {
                    trigger: 'axis',
                    axisPointer: {
                        type: 'cross'
                    }
                },
                legend: {
                    data: ['储罐区甲烷', '储罐区硫化氢', '压缩机甲烷', '压缩机硫化氢'],
                    top: 30
                },
                grid: {
                    left: '3%',
                    right: '4%',
                    bottom: '3%',
                    top: '15%',
                    containLabel: true
                },
                xAxis: {
                    type: 'category',
                    data: this.generateTimeData()
                },
                yAxis: [
                    {
                        type: 'value',
                        name: '甲烷浓度(%)',
                        position: 'left'
                    },
                    {
                        type: 'value',
                        name: '硫化氢浓度(ppm)',
                        position: 'right'
                    }
                ],
                series: [
                    {
                        name: '储罐区甲烷',
                        type: 'line',
                        data: this.generateRandomData(20, 0.5, 3.5),
                        smooth: true,
                        yAxisIndex: 0
                    },
                    {
                        name: '储罐区硫化氢',
                        type: 'line',
                        data: this.generateRandomData(20, 0.5, 12),
                        smooth: true,
                        yAxisIndex: 1
                    },
                    {
                        name: '压缩机甲烷',
                        type: 'line',
                        data: this.generateRandomData(20, 1.0, 5.0),
                        smooth: true,
                        yAxisIndex: 0
                    },
                    {
                        name: '压缩机硫化氢',
                        type: 'line',
                        data: this.generateRandomData(20, 1.0, 15),
                        smooth: true,
                        yAxisIndex: 1
                    }
                ]
            }
            this.chart.setOption(option)
        },
        // ç”Ÿæˆæ—¶é—´æ•°æ®
        generateTimeData() {
            const times = []
            const now = new Date()
            for (let i = 19; i >= 0; i--) {
                const time = new Date(now.getTime() - i * 5 * 60 * 1000)
                times.push(time.toLocaleTimeString('zh-CN', { hour12: false }))
            }
            return times
        },
        // ç”Ÿæˆéšæœºæ•°æ®
        generateRandomData(count, min, max) {
            const data = []
            for (let i = 0; i < count; i++) {
                data.push(+(Math.random() * (max - min) + min).toFixed(2))
            }
            return data
        },
        // å¼€å§‹æ•°æ®åˆ·æ–°
        startDataRefresh() {
            this.timer = setInterval(() => {
                this.refreshSensorData()
                this.updateChart()
                this.checkEmergencyStatus()
            }, 5000) // æ¯5秒刷新一次
        },
        // åˆ·æ–°ä¼ æ„Ÿå™¨æ•°æ®
        refreshSensorData() {
            // æ›´æ–°å‚¨ç½åŒºä¼ æ„Ÿå™¨æ•°æ®
            this.tankSensors.forEach(sensor => {
                sensor.methane = +(Math.random() * 4).toFixed(2)
                sensor.h2s = +(Math.random() * 15).toFixed(2)
                sensor.status = this.getSensorStatus(sensor.methane, sensor.h2s)
            })
            // æ›´æ–°åŽ‹ç¼©æœºä¼ æ„Ÿå™¨æ•°æ®
            this.compressorSensors.forEach(sensor => {
                sensor.methane = +(Math.random() * 6).toFixed(2)
                sensor.h2s = +(Math.random() * 20).toFixed(2)
                sensor.status = this.getSensorStatus(sensor.methane, sensor.h2s)
            })
            // æ£€æŸ¥æ˜¯å¦éœ€è¦è§¦å‘预警
            this.checkLeakWarning()
        },
        // èŽ·å–ä¼ æ„Ÿå™¨çŠ¶æ€
        getSensorStatus(methane, h2s) {
            const methanePct = Math.min(Math.round(methane * 40 * 100) / 100, 100)
            const h2sPct = Math.min(Math.round((h2s / 20) * 100 * 100) / 100, 100)
            if (methanePct >= 80 || h2sPct >= 80) {
                return 'warning'
            }
            return 'normal'
        },
        // æ£€æŸ¥æ³„漏预警
        checkLeakWarning() {
            const allSensors = [...this.tankSensors, ...this.compressorSensors]
            const warningSensor = allSensors.find(sensor => this.getSensorStatus(sensor.methane, sensor.h2s) === 'warning')
            if (warningSensor && !this.leakWarningVisible) {
                this.triggerLeakWarning(warningSensor)
            }
        },
        // è§¦å‘泄漏预警
        triggerLeakWarning(sensor) {
            const methanePct = Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100)
            const h2sPct = Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100)
            const isMethaneMajor = methanePct >= h2sPct
            const overGas = isMethaneMajor ? '甲烷' : '硫化氢'
            const percent = (isMethaneMajor ? methanePct : h2sPct).toFixed(2)
            this.currentWarning = {
                location: sensor.name,
                gas: overGas,
                value: `${percent}%`
            }
            this.leakWarningVisible = true
            this.hasEmergency = true
            // è‡ªåŠ¨è§¦å‘åº”æ€¥å“åº”
            this.autoEmergencyResponse(sensor)
            // æ·»åŠ ç³»ç»Ÿæ—¥å¿—
            this.addSystemLog(`检测到${sensor.name}气体浓度超标,触发泄漏预警`)
        },
        // è‡ªåŠ¨åº”æ€¥å“åº”
        autoEmergencyResponse(sensor) {
            // å¯åŠ¨å–·æ·‹ç³»ç»Ÿ
            if (sensor.name.includes('储罐')) {
                this.sprinklerSystems[0].status = 'active'
            } else if (sensor.name.includes('压缩机')) {
                this.sprinklerSystems[1].status = 'active'
            }
            // æ·»åŠ ç³»ç»Ÿæ—¥å¿—
            this.addSystemLog(`启动${sensor.name}区域喷淋系统`)
            this.addSystemLog(`发送紧急疏散广播`)
            // åˆ›å»ºåº”急记录
            this.createEmergencyRecord(sensor)
        },
        // æ·»åŠ ç³»ç»Ÿæ—¥å¿—
        addSystemLog(content) {
            const now = new Date()
            const time = now.toLocaleTimeString('zh-CN', { hour12: false })
            this.systemLogs.unshift({
                id: Date.now(),
                time: time,
                content: content
            })
            // ä¿æŒæœ€å¤š20条日志
            if (this.systemLogs.length > 20) {
                this.systemLogs = this.systemLogs.slice(0, 20)
            }
        },
        // åˆ›å»ºåº”急记录
        createEmergencyRecord(sensor) {
            const now = new Date()
            const record = {
                id: `EM${Date.now()}`,
                time: now.toLocaleString('zh-CN'),
                location: sensor.name,
                type: sensor.methane > 2.5 ? '甲烷超标' : '硫化氢超标',
                status: 'processing',
                description: `${sensor.name}检测到${sensor.methane > 2.5 ? '甲烷' : '硫化氢'}浓度超标`
            }
            this.emergencyRecords.unshift(record)
        },
        // èŽ·å–è¿›åº¦æ¡é¢œè‰²
        getProgressColor(value, threshold) {
            if (value > threshold) {
                return '#F56C6C'
            } else if (value > threshold * 0.8) {
                return '#E6A23C'
            }
            return '#67C23A'
        },
        // æ£€æŸ¥åº”急状态
        checkEmergencyStatus() {
            const allSensors = [...this.tankSensors, ...this.compressorSensors]
            const has = allSensors.some(sensor => this.getSensorStatus(sensor.methane, sensor.h2s) === 'warning')
            this.hasEmergency = has
            this.systemStatus = has ? 'warning' : 'normal'
        },
        // ç¡®è®¤å‘Šè­¦
        acknowledgeWarning() {
            this.leakWarningVisible = false
            this.addSystemLog('泄漏预警已确认')
        },
        // æŸ¥çœ‹è¯¦æƒ…
        viewDetails() {
            this.leakWarningVisible = false
            // è¿™é‡Œå¯ä»¥è·³è½¬åˆ°è¯¦ç»†é¡µé¢æˆ–显示更多信息
        },
        // æ˜¾ç¤ºåº”急记录
        showEmergencyRecords() {
            this.emergencyRecordsVisible = true
        },
        // æŸ¥çœ‹åŒºå—链详情
        viewBlockchainDetails(record) {
            this.currentEvent = {
                id: record.id,
                timestamp: record.time,
                location: record.location,
                type: record.type,
                sensorData: [
                    {
                        sensor: '甲烷传感器',
                        methane: '3.2%',
                        h2s: '8.5ppm',
                        timestamp: record.time
                    },
                    {
                        sensor: '硫化氢传感器',
                        methane: '2.8%',
                        h2s: '12.5ppm',
                        timestamp: record.time
                    }
                ],
                actions: [
                    {
                        id: 1,
                        timestamp: record.time,
                        type: 'emergency',
                        description: '检测到气体浓度超标,触发预警'
                    },
                    {
                        id: 2,
                        timestamp: new Date(new Date(record.time).getTime() + 3000).toLocaleString('zh-CN'),
                        type: 'action',
                        description: '启动喷淋系统降温'
                    },
                    {
                        id: 3,
                        timestamp: new Date(new Date(record.time).getTime() + 5000).toLocaleString('zh-CN'),
                        type: 'action',
                        description: '发送紧急疏散广播'
                    }
                ],
                blockHash: '0x1234567890abcdef...',
                txHash: '0xabcdef1234567890...',
                confirmations: 12
            }
            this.emergencyRecordsVisible = false
            this.blockchainDetailsVisible = true
        },
        // è§¦å‘应急响应
        triggerEmergency() {
            this.$message.success('应急响应已触发')
            this.addSystemLog('手动触发应急响应')
        }
    }
}
</script>
<style scoped>
.safety-monitoring {
    padding: 20px;
    background-color: #f5f7fa;
    min-height: calc(100vh - 84px);
}
.monitoring-card, .control-card {
    margin-bottom: 20px;
}
.card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.monitoring-section {
    margin-bottom: 30px;
}
.monitoring-section h3 {
    color: #303133;
    margin-bottom: 15px;
    padding-bottom: 8px;
    border-bottom: 2px solid #409EFF;
}
.sensor-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 15px;
}
.sensor-item {
    background: #fff;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    padding: 15px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.sensor-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
    font-weight: bold;
}
.sensor-data .data-item {
    margin-bottom: 12px;
}
.sensor-data .data-item span {
    display: block;
    margin-bottom: 5px;
    font-size: 14px;
    color: #606266;
}
.chart-section {
    margin-top: 30px;
}
.chart-section h3 {
    color: #303133;
    margin-bottom: 15px;
    padding-bottom: 8px;
    border-bottom: 2px solid #409EFF;
}
.chart-container {
    background: #fff;
    border-radius: 8px;
    padding: 20px;
}
.chart {
    width: 100%;
    height: 400px;
}
.control-section {
    margin-bottom: 25px;
}
.control-section1 {
    display: flex;
}
.control-section h4 {
    color: #303133;
    margin-bottom: 15px;
    font-size: 16px;
}
.status-grid {
    display: grid;
    gap: 10px;
}
.status-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 6px;
}
.status-indicator {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
}
.status-indicator.active {
    color: #67C23A;
}
.status-indicator.standby {
    color: #909399;
}
.log-container {
    max-height: 200px;
    overflow-y: auto;
    background: #f8f9fa;
    border-radius: 6px;
    padding: 10px;
}
.log-item {
    display: flex;
    gap: 10px;
    margin-bottom: 8px;
    font-size: 12px;
}
.log-time {
    color: #909399;
    min-width: 60px;
}
.log-content {
    color: #606266;
}
/* æ³„漏预警弹窗样式 */
.leak-warning-dialog {
    background: #fff5f5;
}
.warning-content {
    text-align: center;
    padding: 20px 0;
}
.warning-icon {
    font-size: 60px;
    color: #F56C6C;
    margin-bottom: 20px;
}
.warning-text h3 {
    color: #F56C6C;
    margin-bottom: 15px;
}
.warning-text p {
    margin: 8px 0;
    color: #606266;
}
/* åŒºå—链详情样式 */
.blockchain-details {
    padding: 20px 0;
}
.sensor-data-section, .action-log-section, .blockchain-info {
    margin-top: 25px;
}
.sensor-data-section h4, .action-log-section h4, .blockchain-info h4 {
    color: #303133;
    margin-bottom: 15px;
    padding-bottom: 8px;
    border-bottom: 1px solid #e4e7ed;
}
/* å“åº”式设计 */
@media (max-width: 1200px) {
    .sensor-grid {
        grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    }
}
@media (max-width: 768px) {
    .safety-monitoring {
        padding: 10px;
    }
    .sensor-grid {
        grid-template-columns: 1fr;
    }
    .chart {
        height: 300px;
    }
}
</style>
src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -168,6 +168,9 @@
        clickFun: (row) => {
          openForm("edit", row);
        },
        disabled: (row) => {
          return row.inspectState;
        }
      },
      {
        name: "附件",
@@ -193,6 +196,9 @@
            proxy.$modal.msgError("检验员已存在");
          }
        },
        disabled: (row) => {
          return row.inspectState;
        }
      },
      {
        name: "下载",
@@ -354,19 +360,18 @@
}
const downLoadFile = (row) => {
  downloadQualityInspect({id: row.id}).then(res => {
    // åˆ›å»º blob å¯¹è±¡
    const blob = new Blob([res.data], {type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'})
  downloadQualityInspect({ id: row.id }).then((blobData) => {
    const blob = new Blob([blobData], {
      type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    })
    const downloadUrl = window.URL.createObjectURL(blob)
    // åˆ›å»ºä¸´æ—¶ <a> æ ‡ç­¾è¿›è¡Œä¸‹è½½
    const link = document.createElement('a')
    link.href = downloadUrl
    link.download = '检验报告.docx' // è¿™é‡Œå’ŒåŽç«¯ä¸€è‡´
    link.download = '检验报告.docx'
    document.body.appendChild(link)
    link.click()
    // æ¸…理
    document.body.removeChild(link)
    window.URL.revokeObjectURL(downloadUrl)
  })
vite.config.js
@@ -8,8 +8,8 @@
  const { VITE_APP_ENV } = env;
  const baseUrl =
    VITE_APP_ENV == "development"
      ? "http://114.132.189.42:8092" // å¼€å‘环境后端接口
      : "http://114.132.189.42:8092"; // ç”Ÿäº§çŽ¯å¢ƒåŽç«¯æŽ¥å£
      ? "http://114.132.189.42:8089" // å¼€å‘环境后端接口
      : "http://114.132.189.42:8089"; // ç”Ÿäº§çŽ¯å¢ƒåŽç«¯æŽ¥å£
  return {
    // éƒ¨ç½²ç”Ÿäº§çŽ¯å¢ƒå’Œå¼€å‘çŽ¯å¢ƒä¸‹çš„URL。