餐饮油烟智能监测与监管一体化平台
riku
2026-03-17 b1a0d701cf898c8b7812e66a808a1c91f2bae6cc
src/views/analysis/huanxincode/HuanxinCodeManage.vue
@@ -1,5 +1,31 @@
<template>
  <div class="huanxin-code-manage">
    <FYSearchBar @search="onSearch">
      <template #options>
        <!-- 区县 -->
        <FYOptionLocation
          :initValue="false"
          :allOption="false"
          :level="3"
          :checkStrictly="false"
          v-model:value="formSearch.locations"
        ></FYOptionLocation>
        <!-- 场景类型 -->
        <FYOptionScene
          :initValue="false"
          :allOption="false"
          :type="1"
          v-model:value="formSearch.scenetype"
        ></FYOptionScene>
        <!-- 时间 -->
        <FYOptionTime
          :initValue="false"
          type="month"
          v-model:value="formSearch.time"
        ></FYOptionTime>
      </template>
      <template #buttons v-if="$slots.buttons"> </template>
    </FYSearchBar>
    <!-- 顶部宏观看板区 -->
    <el-row :gutter="20" class="dashboard">
      <el-col :span="8">
@@ -39,11 +65,9 @@
      </el-button>
    </div> -->
    <!-- 中部视图切换区 -->
    <el-tabs v-model="activeView" class="view-tabs">
      <!-- 列表视图 -->
      <el-tab-pane label="列表视图" name="list">
        <el-table :data="filteredShopList" style="width: 100%">
    <!-- 店铺列表 -->
    <div class="shop-list">
      <el-table :data="pagedShopList" style="width: 100%">
          <el-table-column prop="shopName" label="店铺名称" />
          <el-table-column prop="district" label="所在区县" width="120" />
          <el-table-column prop="town" label="所在街镇" width="150" />
@@ -67,7 +91,7 @@
            </template>
          </el-table-column>
          <el-table-column prop="lastUpdate" label="上次更新时间" width="180" />
          <el-table-column label="操作" width="200" fixed="right">
        <el-table-column label="操作" width="100" fixed="right">
            <template #default="scope">
              <el-button size="small" @click="viewDetails(scope.row)">查看详情</el-button>
              <!-- <el-button size="small" type="warning" @click="viewRiskWarnings(scope.row)"
@@ -76,44 +100,44 @@
            </template>
          </el-table-column>
        </el-table>
      </el-tab-pane>
      <!-- 地图视图 -->
      <el-tab-pane label="地图视图" name="map">
        <div class="map-container">
          <div class="map-placeholder">
            <el-empty description="地图加载中..." />
            <!-- 这里应该集成真实的地图组件 -->
          </div>
          <div class="map-legend">
            <div class="legend-item">
              <div class="legend-dot green"></div>
              <span>绿码</span>
            </div>
            <div class="legend-item">
              <div class="legend-dot yellow"></div>
              <span>黄码</span>
            </div>
            <div class="legend-item">
              <div class="legend-dot red"></div>
              <span>红码</span>
      <!-- 分页组件 -->
      <div class="pagination">
        <el-pagination
          v-model:current-page="currentPage"
          v-model:page-size="pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="filteredShopList.length"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
            </div>
          </div>
        </div>
      </el-tab-pane>
    </el-tabs>
    <!-- 详情抽屉 -->
    <el-drawer v-model="drawerVisible" title="店铺详情" direction="rtl" size="70%">
    <el-drawer
      v-model="drawerVisible"
      :title="selectedShop?.shopName || '店铺详情'"
      direction="rtl"
      size="60%"
    >
      <div v-if="selectedShop" class="shop-details">
        <el-row justify="space-between" style="flex-wrap: nowrap">
        <!-- 环信码大图标及当前评分 -->
        <div class="code-header">
          <div class="code-icon" :class="selectedShop.code">
            {{ getCodeText(selectedShop.code) }}
          </div>
          <div class="score-info">
            <div class="score-label">当前评分</div>
            <div class="score-value">{{ selectedShop.score }}</div>
            </div>
            <div class="code-icon">
              <el-image
                class="image"
                :src="codeImageUrl"
                :preview-src-list="[codeImageUrl]"
                :initial-index="0"
                fit="cover"
                lazy
              />
          </div>
        </div>
@@ -121,17 +145,16 @@
        <div class="chart-section">
          <h3>评分维度分析</h3>
          <div class="radar-chart">
            <!-- 这里应该集成真实的雷达图组件 -->
            <el-empty description="雷达图加载中..." />
              <canvas ref="radarChart" width="500" height="400"></canvas>
          </div>
        </div>
        </el-row>
        <!-- 评分历史趋势图 -->
        <div class="chart-section">
          <h3>评分历史趋势</h3>
          <div class="trend-chart">
            <!-- 这里应该集成真实的趋势图组件 -->
            <el-empty description="趋势图加载中..." />
            <canvas ref="trendChart" width="800" height="350"></canvas>
          </div>
        </div>
@@ -171,16 +194,46 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import dayjs from 'dayjs'
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { Setting, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
import userApi from '@/api/fytz/userApi'
import creditApi from '@/api/fytz/creditApi'
// 搜索表单
const formSearch = ref({
  locations: {
    aCode: null,
    aName: null,
    cCode: '3100',
    cName: '上海市',
    dCode: '310104',
    dName: '徐汇区',
    mCode: null,
    mName: null,
    pCode: '31',
    pName: '上海市',
    tCode: null,
    tName: null,
  },
  scenetype: {
    label: '餐饮',
    value: '1',
  },
  time: dayjs('2023-08-01').date(1).toDate(),
})
// 状态
const activeView = ref('list')
const drawerVisible = ref(false)
const modelConfigVisible = ref(false)
const selectedShop = ref(null)
const isAdmin = ref(true) // 模拟管理员权限
const filterCode = ref('all')
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
// 环信码图片URL
const codeImageUrl = ref('')
// 统计数据
const statistics = reactive({
@@ -228,6 +281,61 @@
  '华泾镇',
]
function onSearch() {
  const f = formSearch.value
  const area = {}
  // 行政区划
  area.provinceCode = f.locations.pCode
  area.provinceName = f.locations.pName
  if (area.provinceCode == null) {
    area.provinceCode = null
    area.provinceName = null
  }
  area.cityCode = f.locations.cCode
  area.cityName = f.locations.cName
  area.districtCode = f.locations.dCode
  area.districtName = f.locations.dName
  area.townCode = f.locations.tCode
  area.townName = f.locations.tName
  // 场景类型
  area.sceneTypes = []
  f.scenetype.value == null ? (area.sceneTypes = []) : (area.sceneTypes = [f.scenetype.value])
  // 上下线状态
  area.online = true
  // 关键字
  area.searchText = ''
  userApi.fetchUser(currentPage.value, pageSize.value, area).then((res) => {
    if (res) {
      res.data
      res.head.totalCount
      shopList.value = res.data.map((item, index) => {
        const { score, code } = generateRandomScore()
        return {
          id: index + 1,
          guid: item.biGuid,
          shopName: item.biName,
          district: item.biDistrictName,
          town: item.biTownName,
          code: code,
          score: score,
          trend: generateRandomTrend(),
          lastUpdate: generateRandomDate(),
          warnings: [
            {
              time: generateRandomDate(),
              content: '净化器运行时长不足',
              score: 90,
              handled: true,
            },
          ],
        }
      })
    }
  })
}
// 生成2023年8月内的随机时间
function generateRandomDate() {
  const year = 2023
@@ -245,6 +353,23 @@
  return array[Math.floor(Math.random() * array.length)]
}
// 生成随机评分和对应环信码等级
function generateRandomScore() {
  const score = Math.floor(Math.random() * 101) // 0-100
  let code
  if (score >= 90) {
    code = 'green'
  } else if (score >= 60) {
    code = 'yellow'
  } else {
    code = 'red'
  }
  return {
    score,
    code,
  }
}
// 生成随机评分趋势
function generateRandomTrend() {
  return Math.floor(Math.random() * 11) - 5 // -5 到 5
@@ -258,7 +383,7 @@
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'green',
    score: 95,
    score: 90,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [
@@ -427,10 +552,18 @@
  return shopList.value.filter((shop) => shop.code === filterCode.value)
})
// 分页后的店铺列表
const pagedShopList = computed(() => {
  const start = (currentPage.value - 1) * pageSize.value
  const end = start + pageSize.value
  return filteredShopList.value.slice(start, end)
})
// 生命周期
onMounted(() => {
  // 这里可以从API获取数据
  console.log('环信码管理页面加载')
  onSearch()
})
// 方法
@@ -441,7 +574,17 @@
function filterByCode(code) {
  filterCode.value = code === filterCode.value ? 'all' : code
  activeView.value = 'list' // 切换到列表视图
  currentPage.value = 1 // 重置到第一页
}
// 分页方法
function handleSizeChange(size) {
  pageSize.value = size
  currentPage.value = 1
}
function handleCurrentChange(current) {
  currentPage.value = current
}
function getCodeType(code) {
@@ -470,9 +613,183 @@
  }
}
// 雷达图和趋势图引用
const radarChart = ref(null)
const trendChart = ref(null)
let radarChartInstance = null
let trendChartInstance = null
function viewDetails(shop) {
  selectedShop.value = shop
  drawerVisible.value = true
  // 获取环信码图片
  if (shop.guid && shop.shopName) {
    creditApi.fetchCodeUrl(shop.guid, shop.shopName).then((res) => {
      if (res && res.url) {
        codeImageUrl.value = res.url
      }
    })
  }
  // 延迟绘制图表,确保DOM已更新
  setTimeout(() => {
    drawRadarChart()
    drawTrendChart()
  }, 100)
}
// 绘制雷达图
function drawRadarChart() {
  if (!radarChart.value) return
  // 销毁旧实例
  if (radarChartInstance) {
    radarChartInstance.dispose()
  }
  // 初始化echarts实例
  radarChartInstance = echarts.init(radarChart.value)
  // 雷达图数据
  const labels = [
    '在线监测设备',
    '净化设施设备',
    '在线监测设备维护',
    '净化设施设备维护',
    '在线监测数据量级',
    '空调和风机噪声',
    '台站管理',
    '信用承诺自评',
  ]
  // 生成随机评分数据(实际项目中应从API获取)
  const data = labels.map(() => Math.floor(Math.random() * 40) + 60) // 60-100分
  // 配置项
  const option = {
    radar: {
      indicator: labels.map((label) => ({
        name: label,
        max: 100,
      })),
      radius: '70%',
    },
    series: [
      {
        type: 'radar',
        data: [
          {
            value: data,
            name: '评分维度',
            areaStyle: {
              color: 'rgba(103, 194, 58, 0.2)',
            },
            lineStyle: {
              color: '#67c23a',
            },
            itemStyle: {
              color: '#67c23a',
            },
          },
        ],
      },
    ],
  }
  // 渲染图表
  radarChartInstance.setOption(option)
  // 监听窗口大小变化
  window.addEventListener('resize', () => {
    radarChartInstance.resize()
  })
}
// 绘制趋势图
function drawTrendChart() {
  if (!trendChart.value) return
  // 销毁旧实例
  if (trendChartInstance) {
    trendChartInstance.dispose()
  }
  // 初始化echarts实例
  trendChartInstance = echarts.init(trendChart.value)
  // 生成过去12个月的标签
  const labels = []
  const data = []
  const now = dayjs()
  for (let i = 11; i >= 0; i--) {
    const date = now.subtract(i, 'month')
    labels.push(date.format('YYYY-MM'))
    // 生成随机评分数据(实际项目中应从API获取)
    data.push(Math.floor(Math.random() * 30) + 70) // 70-100分
  }
  // 配置项
  const option = {
    tooltip: {
      trigger: 'axis',
    },
    grid: {
      left: '3%',
      right: '4%',
      bottom: '3%',
      containLabel: true,
    },
    xAxis: {
      type: 'category',
      boundaryGap: false,
      data: labels,
      axisLabel: {
        rotate: 45,
      },
    },
    yAxis: {
      type: 'value',
      min: 60,
      max: 100,
      interval: 8,
    },
    series: [
      {
        name: '评分',
        type: 'line',
        data: data,
        smooth: true,
        lineStyle: {
          color: '#409eff',
        },
        itemStyle: {
          color: '#409eff',
        },
        areaStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            {
              offset: 0,
              color: 'rgba(64, 158, 255, 0.3)',
            },
            {
              offset: 1,
              color: 'rgba(64, 158, 255, 0.1)',
            },
          ]),
        },
      },
    ],
  }
  // 渲染图表
  trendChartInstance.setOption(option)
  // 监听窗口大小变化
  window.addEventListener('resize', () => {
    trendChartInstance.resize()
  })
}
function viewRiskWarnings(shop) {
@@ -548,8 +865,13 @@
  margin-bottom: 20px;
}
.view-tabs {
.shop-list {
  margin-bottom: 20px;
}
.pagination {
  margin-top: 20px;
  text-align: right;
}
.trend {
@@ -633,33 +955,13 @@
.code-header {
  display: flex;
  align-items: center;
  margin-bottom: 30px;
  align-items: flex-start;
  flex-direction: column;
  margin-bottom: 20px;
}
.code-icon {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  font-weight: bold;
  color: white;
  margin-right: 30px;
}
.code-icon.green {
  background-color: #67c23a;
}
.code-icon.yellow {
  background-color: #e6a23c;
}
.code-icon.red {
  background-color: #f56c6c;
}
.score-info {
@@ -677,17 +979,26 @@
}
.chart-section {
  margin-bottom: 30px;
  margin-bottom: 20px;
}
.chart-section h3 {
  margin-bottom: 15px;
  font-size: 18px;
  margin-bottom: 10px;
  font-size: 16px;
}
.radar-chart,
.radar-chart {
  width: 500px;
  height: 500px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.trend-chart {
  height: 300px;
  height: 350px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  display: flex;
@@ -696,11 +1007,18 @@
}
.warning-section {
  margin-top: 30px;
  margin-top: 20px;
}
.warning-section h3 {
  margin-bottom: 15px;
  font-size: 18px;
  margin-bottom: 10px;
  font-size: 16px;
}
.image {
  width: 300px;
  /* height: 250px; */
  border-radius: 4px;
  margin-bottom: 6px;
}
</style>