餐饮油烟智能监测与监管一体化平台
riku
2026-03-13 e365192a36d6d9432fbd00ea9d577a38f8679707
src/views/analysis/huanxincode/HuanxinCodeManage.vue
@@ -1 +1,706 @@
<template>环信码管理</template>
<template>
  <div class="huanxin-code-manage">
    <!-- 顶部宏观看板区 -->
    <el-row :gutter="20" class="dashboard">
      <el-col :span="8">
        <el-card shadow="hover" class="dashboard-card green-card" @click="filterByCode('green')">
          <div class="card-content">
            <div class="card-title">绿码店铺数</div>
            <div class="card-value">{{ statistics.greenCount }}</div>
            <div class="card-percentage">{{ statistics.greenPercentage }}%</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card shadow="hover" class="dashboard-card yellow-card" @click="filterByCode('yellow')">
          <div class="card-content">
            <div class="card-title">黄码店铺数</div>
            <div class="card-value">{{ statistics.yellowCount }}</div>
            <div class="card-percentage">{{ statistics.yellowPercentage }}%</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card shadow="hover" class="dashboard-card red-card" @click="filterByCode('red')">
          <div class="card-content">
            <div class="card-title">红码店铺数</div>
            <div class="card-value">{{ statistics.redCount }}</div>
            <div class="card-percentage">{{ statistics.redPercentage }}%</div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- 评分模型配置按钮 -->
    <!-- <div class="model-config" v-if="isAdmin">
      <el-button type="primary" @click="openModelConfig">
        <el-icon><Setting /></el-icon>
        <span>评分模型配置</span>
      </el-button>
    </div> -->
    <!-- 中部视图切换区 -->
    <el-tabs v-model="activeView" class="view-tabs">
      <!-- 列表视图 -->
      <el-tab-pane label="列表视图" name="list">
        <el-table :data="filteredShopList" 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" />
          <el-table-column label="环信码" width="100">
            <template #default="scope">
              <el-tag :type="getCodeType(scope.row.code)">{{ getCodeText(scope.row.code) }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="score" label="当前评分" width="120" sortable />
          <el-table-column label="评分变化趋势" width="150">
            <template #default="scope">
              <div class="trend">
                <el-icon :class="['trend-icon', scope.row.trend > 0 ? 'up' : 'down']">
                  <ArrowUp v-if="scope.row.trend > 0" />
                  <ArrowDown v-else />
                </el-icon>
                <span :class="scope.row.trend > 0 ? 'up' : 'down'">
                  {{ scope.row.trend > 0 ? '+' : '' }}{{ scope.row.trend }}分
                </span>
              </div>
            </template>
          </el-table-column>
          <el-table-column prop="lastUpdate" label="上次更新时间" width="180" />
          <el-table-column label="操作" width="200" 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)"
                >风险预警记录</el-button
              > -->
            </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>
          </div>
        </div>
      </el-tab-pane>
    </el-tabs>
    <!-- 详情抽屉 -->
    <el-drawer v-model="drawerVisible" title="店铺详情" direction="rtl" size="70%">
      <div v-if="selectedShop" class="shop-details">
        <!-- 环信码大图标及当前评分 -->
        <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>
        <!-- 评分维度雷达图 -->
        <div class="chart-section">
          <h3>评分维度分析</h3>
          <div class="radar-chart">
            <!-- 这里应该集成真实的雷达图组件 -->
            <el-empty description="雷达图加载中..." />
          </div>
        </div>
        <!-- 评分历史趋势图 -->
        <div class="chart-section">
          <h3>评分历史趋势</h3>
          <div class="trend-chart">
            <!-- 这里应该集成真实的趋势图组件 -->
            <el-empty description="趋势图加载中..." />
          </div>
        </div>
        <!-- 风险预警记录 -->
        <div class="warning-section">
          <h3>风险预警记录</h3>
          <el-table :data="selectedShop.warnings" style="width: 100%">
            <el-table-column prop="time" label="预警时间" width="180" />
            <el-table-column prop="content" label="预警内容" />
            <el-table-column prop="score" label="当时评分" width="120" />
            <el-table-column label="处理状态" width="120">
              <template #default="scope">
                <el-tag :type="scope.row.handled ? 'success' : 'danger'">
                  {{ scope.row.handled ? '已处理' : '未处理' }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
    </el-drawer>
    <!-- 评分模型配置弹窗 -->
    <el-dialog v-model="modelConfigVisible" title="评分模型配置" width="80%">
      <div class="model-config-content">
        <!-- 这里应该集成真实的配置表单 -->
        <el-empty description="配置表单加载中..." />
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="modelConfigVisible = false">取消</el-button>
          <el-button type="primary" @click="saveModelConfig">保存配置</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { Setting, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
// 状态
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 statistics = reactive({
  greenCount: 125,
  greenPercentage: 65.8,
  yellowCount: 45,
  yellowPercentage: 23.7,
  redCount: 20,
  redPercentage: 10.5,
})
// 店铺名称列表
const shopNames = [
  '付小姐在成都',
  '吉刻联盟',
  '家在塔啦',
  '狼来了',
  '乐凯撒星游店',
  '馨远美食小镇(哈尼美食广场)',
  '棒约翰',
  '弄堂咪道',
  '杨记齐齐哈尔烤肉',
  '上海稔传餐饮管理有限公司(人生一串)',
  '缘家',
  '泉盛餐饮(上海)有限公司(食其家)',
  '丰茂烤串',
  '上海泰煌餐饮管理有限公司(泰煌鸡)',
  '徐汇区辰熙餐馆(小铁君串烧居酒屋)',
]
// 徐汇区街镇列表
const xuhuiTowns = [
  '天平路街道',
  '湖南路街道',
  '斜土路街道',
  '枫林路街道',
  '长桥街道',
  '田林街道',
  '虹梅路街道',
  '康健新村街道',
  '徐家汇街道',
  '凌云路街道',
  '龙华街道',
  '漕河泾街道',
  '华泾镇',
]
// 生成2023年8月内的随机时间
function generateRandomDate() {
  const year = 2023
  const month = 7 // 0-11,8月是7
  const day = Math.floor(Math.random() * 31) + 1 // 1-31
  const hour = Math.floor(Math.random() * 24) // 0-23
  const minute = Math.floor(Math.random() * 60) // 0-59
  const date = new Date(year, month, day, hour, minute)
  return date.toISOString().slice(0, 16).replace('T', ' ')
}
// 随机选择数组元素
function getRandomElement(array) {
  return array[Math.floor(Math.random() * array.length)]
}
// 生成随机评分趋势
function generateRandomTrend() {
  return Math.floor(Math.random() * 11) - 5 // -5 到 5
}
// 店铺数据
const shopList = ref([
  {
    id: 1,
    shopName: getRandomElement(shopNames),
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'green',
    score: 95,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [
      {
        time: generateRandomDate(),
        content: '净化器运行时长不足',
        score: 90,
        handled: true,
      },
    ],
  },
  {
    id: 2,
    shopName: getRandomElement(shopNames),
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'yellow',
    score: 75,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [
      {
        time: generateRandomDate(),
        content: '投诉次数较多',
        score: 80,
        handled: false,
      },
    ],
  },
  {
    id: 3,
    shopName: getRandomElement(shopNames),
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'red',
    score: 60,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [
      {
        time: generateRandomDate(),
        content: '排放浓度超标',
        score: 65,
        handled: false,
      },
      {
        time: generateRandomDate(),
        content: '清洗频次不足',
        score: 62,
        handled: false,
      },
    ],
  },
  {
    id: 4,
    shopName: getRandomElement(shopNames),
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'green',
    score: 92,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [],
  },
  {
    id: 5,
    shopName: getRandomElement(shopNames),
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'yellow',
    score: 78,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [
      {
        time: generateRandomDate(),
        content: '风机联动率低',
        score: 75,
        handled: true,
      },
    ],
  },
  {
    id: 6,
    shopName: getRandomElement(shopNames),
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'green',
    score: 90,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [],
  },
  {
    id: 7,
    shopName: getRandomElement(shopNames),
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'red',
    score: 55,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [
      {
        time: generateRandomDate(),
        content: '未安装油烟净化设备',
        score: 60,
        handled: false,
      },
    ],
  },
  {
    id: 8,
    shopName: getRandomElement(shopNames),
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'yellow',
    score: 72,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [
      {
        time: generateRandomDate(),
        content: '净化器清洗不及时',
        score: 75,
        handled: true,
      },
    ],
  },
  {
    id: 9,
    shopName: getRandomElement(shopNames),
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'green',
    score: 93,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [],
  },
  {
    id: 10,
    shopName: getRandomElement(shopNames),
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'yellow',
    score: 76,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [
      {
        time: generateRandomDate(),
        content: '排放浓度接近标准限值',
        score: 78,
        handled: true,
      },
    ],
  },
])
// 过滤后的店铺列表
const filteredShopList = computed(() => {
  if (filterCode.value === 'all') {
    return shopList.value
  }
  return shopList.value.filter((shop) => shop.code === filterCode.value)
})
// 生命周期
onMounted(() => {
  // 这里可以从API获取数据
  console.log('环信码管理页面加载')
})
// 方法
function onBack() {
  // 回退逻辑
  console.log('回退')
}
function filterByCode(code) {
  filterCode.value = code === filterCode.value ? 'all' : code
  activeView.value = 'list' // 切换到列表视图
}
function getCodeType(code) {
  switch (code) {
    case 'green':
      return 'success'
    case 'yellow':
      return 'warning'
    case 'red':
      return 'danger'
    default:
      return ''
  }
}
function getCodeText(code) {
  switch (code) {
    case 'green':
      return '绿码'
    case 'yellow':
      return '黄码'
    case 'red':
      return '红码'
    default:
      return ''
  }
}
function viewDetails(shop) {
  selectedShop.value = shop
  drawerVisible.value = true
}
function viewRiskWarnings(shop) {
  selectedShop.value = shop
  drawerVisible.value = true
  // 这里可以滚动到风险预警部分
}
function openModelConfig() {
  modelConfigVisible.value = true
}
function saveModelConfig() {
  // 保存配置逻辑
  modelConfigVisible.value = false
  console.log('保存评分模型配置')
}
</script>
<style scoped>
.huanxin-code-manage {
  padding: 20px;
}
.dashboard {
  margin-bottom: 30px;
}
.dashboard-card {
  cursor: pointer;
  transition: all 0.3s ease;
}
.dashboard-card:hover {
  transform: translateY(-5px);
}
.card-content {
  text-align: center;
  padding: 20px 0;
}
.card-title {
  font-size: 16px;
  margin-bottom: 10px;
}
.card-value {
  font-size: 32px;
  font-weight: bold;
  margin-bottom: 5px;
}
.card-percentage {
  font-size: 14px;
  opacity: 0.8;
}
.green-card .card-value {
  color: #67c23a;
}
.yellow-card .card-value {
  color: #e6a23c;
}
.red-card .card-value {
  color: #f56c6c;
}
.model-config {
  text-align: right;
  margin-bottom: 20px;
}
.view-tabs {
  margin-bottom: 20px;
}
.trend {
  display: flex;
  align-items: center;
  justify-content: center;
}
.trend-icon {
  margin-right: 5px;
}
.trend-icon.up {
  color: #67c23a;
}
.trend-icon.down {
  color: #f56c6c;
}
.trend .up {
  color: #67c23a;
}
.trend .down {
  color: #f56c6c;
}
.map-container {
  position: relative;
  height: 600px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
}
.map-placeholder {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.map-legend {
  position: absolute;
  bottom: 20px;
  right: 20px;
  background: white;
  padding: 10px;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.legend-item {
  display: flex;
  align-items: center;
  margin-bottom: 5px;
}
.legend-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  margin-right: 8px;
}
.legend-dot.green {
  background-color: #67c23a;
}
.legend-dot.yellow {
  background-color: #e6a23c;
}
.legend-dot.red {
  background-color: #f56c6c;
}
.shop-details {
  padding: 20px;
}
.code-header {
  display: flex;
  align-items: center;
  margin-bottom: 30px;
}
.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 {
  flex: 1;
}
.score-label {
  font-size: 16px;
  margin-bottom: 5px;
}
.score-value {
  font-size: 48px;
  font-weight: bold;
}
.chart-section {
  margin-bottom: 30px;
}
.chart-section h3 {
  margin-bottom: 15px;
  font-size: 18px;
}
.radar-chart,
.trend-chart {
  height: 300px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.warning-section {
  margin-top: 30px;
}
.warning-section h3 {
  margin-bottom: 15px;
  font-size: 18px;
}
</style>