餐饮油烟智能监测与监管一体化平台
riku
2026-03-13 e365192a36d6d9432fbd00ea9d577a38f8679707
2026.3.13
已修改5个文件
已添加2个文件
1366 ■■■■■ 文件已修改
src/constants/menu.js 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/sfc/TimeSelect.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/huanxincode/HuanxinCodeManage.vue 707 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/UserEdit.vue 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/components/CompLaint.vue 310 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/components/CompPunishment.vue 298 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataException.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constants/menu.js
@@ -81,11 +81,6 @@
        icon: 'solar:checklist-minimalistic-line-duotone',
        name: '问题整改',
      },
      {
        path: '/index/inspection/report-manage',
        icon: 'solar:folder-favourite-bookmark-line-duotone',
        name: '评估报告',
      },
    ],
  },
  {
@@ -102,6 +97,11 @@
        icon: 'solar:archive-down-minimlistic-line-duotone',
        name: '环信码管理',
      },
      {
        path: '/index/inspection/report-manage',
        icon: 'solar:folder-favourite-bookmark-line-duotone',
        name: '评估报告',
      },
      // {
      //   path: '/index/analysis/data-product',
      //   icon: 'solar:document-add-line-duotone',
src/sfc/TimeSelect.vue
@@ -61,8 +61,10 @@
  methods: {
    initOneWeekAgoTime() {
      // ç»™æ—¶é—´é€‰æ‹©å™¨è®¾ç½®é»˜è®¤æ—¶é—´ä¸ºä¸€å‘¨å‰
      this.time[0] = dayjs().subtract(4, 'week').format('YYYY-MM-DD HH:mm:ss')
      this.time[1] = dayjs().format('YYYY-MM-DD HH:mm:ss')
      // this.time[0] = dayjs().subtract(4, 'week').format('YYYY-MM-DD HH:mm:ss')
      // this.time[1] = dayjs().format('YYYY-MM-DD HH:mm:ss')
      // 2026.3.13 demo ä¸­å›ºå®šåˆå§‹æ—¶é—´
      this.time = ['2023-08-01 00:00:00', '2023-08-31 23:59:59']
    },
    // å¿«æ·æ—¶æ®µé€‰æ‹©
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>
src/views/inspection/scenenew/UserEdit.vue
@@ -49,7 +49,7 @@
      </FormCol>
    </el-tab-pane>
    <el-tab-pane label="危废排污" name="third">
    <!-- <el-tab-pane label="危废排污" name="third">
      <FormCol>
        <div class="sub-title">危废排污清单</div>
        <CompHazardousWasteFile :form-info="formHazardousWasteFile" />
@@ -58,28 +58,26 @@
        <div class="sub-title">危废排污记录</div>
        <CompHazardousWasteRecord :form-info="formHazardousWasteRecord" />
      </FormCol>
    </el-tab-pane>
    </el-tab-pane> -->
    <el-tab-pane label="行政处罚" name="fourth">
      <FormCol>
        <div class="sub-title">行政处罚表</div>
        <!-- <CompPunishment :form-info="formProblem" /> -->
      </FormCol>
      <!-- <FormCol> -->
      <CompPunishment />
      <!-- </FormCol> -->
    </el-tab-pane>
    <el-tab-pane label="信访投诉" name="fifth">
      <FormCol>
        <div class="sub-title">信访投诉</div>
        <!-- <CompLaint :form-info="formLaint" /> -->
      </FormCol>
      <!-- <FormCol> -->
      <CompLaint />
      <!-- </FormCol> -->
    </el-tab-pane>
    <el-tab-pane label="巡查问题表" name="sixth">
    <!-- <el-tab-pane label="巡查问题表" name="sixth">
      <FormCol>
        <div class="sub-title">巡查问题表</div>
        <!-- <CompProblem :form-info="formProblem" /> -->
        <CompProblem :form-info="formProblem" />
      </FormCol>
    </el-tab-pane>
    </el-tab-pane> -->
  </el-tabs>
  <!-- <ComBaseInformation v-model="drawer"></ComBaseInformation> -->
@@ -93,6 +91,8 @@
import CompDeviceInfo from './components/CompDeviceInfo.vue'
import CompHazardousWasteFile from './components/CompHazardousWasteFile.vue'
import CompHazardousWasteRecord from './components/CompHazardousWasteRecord.vue'
import CompLaint from './components/CompLaint.vue'
import CompPunishment from './components/CompPunishment.vue'
export default {
  components: {
@@ -102,14 +102,15 @@
    CompSceneInfo,
    CompCompanyInfo,
    CompDeviceInfo,
    CompHazardousWasteFile,
    CompHazardousWasteRecord,
    CompPunishment,
    CompLaint,
    // CompHazardousWasteFile,
    // CompHazardousWasteRecord,
    // CompPanyInfo,
    // CompFumePurifyDevice,
    // CompHazardousWasteFile,
    // CompHazardousWasteRecord,
    // CompProblem,
    // CompPunishment,
    // CompRestaurantBaseInfo,
    // CompVehicleBaseInfo,
    // CompUserInfos,
src/views/inspection/scenenew/components/CompLaint.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,310 @@
<template>
  <div>
    <el-row class="sub-title" justify="space-between">
      <div>信访投诉记录</div>
      <el-button-group>
        <el-button type="primary" @click="handleAdd">
          <el-icon><Plus /></el-icon>
          <span>新增投诉记录</span>
        </el-button>
        <el-button type="success" @click="dialogImportVisible = true">
          <el-icon><Upload /></el-icon>
          <span>导入记录</span>
        </el-button>
      </el-button-group>
    </el-row>
    <!-- è¡¨æ ¼å±•示 -->
    <el-table :data="laintList" style="width: 100%" :show-overflow-tooltip="true">
      <el-table-column prop="reason" label="投诉原因" />
      <el-table-column prop="demand" label="投诉诉求" />
      <el-table-column prop="laintTime" label="投诉时间" width="180" />
      <el-table-column prop="source" label="投诉来源" width="150" />
      <el-table-column prop="department" label="处理部门" width="150" />
      <el-table-column prop="result" label="投诉结果" width="150" />
      <el-table-column label="操作" width="150" fixed="right">
        <template #default="scope">
          <el-button size="small" @click="handleEdit(scope.row)">修改</el-button>
          <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- ç©ºçŠ¶æ€ -->
    <div v-if="laintList.length === 0" class="empty-state">
      <el-empty description="暂无信访投诉记录" />
    </div>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
        <el-form-item label="投诉原因" prop="reason">
          <el-input v-model="form.reason" placeholder="请输入投诉原因" type="textarea" rows="3" />
        </el-form-item>
        <el-form-item label="投诉诉求" prop="demand">
          <el-input v-model="form.demand" placeholder="请输入投诉诉求" type="textarea" rows="3" />
        </el-form-item>
        <el-form-item label="投诉时间" prop="laintTime">
          <el-date-picker
            v-model="form.laintTime"
            type="date"
            placeholder="请选择投诉时间"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="投诉来源" prop="source">
          <el-input v-model="form.source" placeholder="请输入投诉来源" />
        </el-form-item>
        <el-form-item label="处理部门" prop="department">
          <el-input v-model="form.department" placeholder="请输入处理部门" />
        </el-form-item>
        <el-form-item label="投诉结果" prop="result">
          <el-input v-model="form.result" placeholder="请输入投诉结果" type="textarea" rows="2" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- Excel导入对话框 -->
    <el-dialog v-model="dialogImportVisible" title="导入投诉记录" width="50%">
      <el-upload
        class="upload-demo"
        action="#"
        :auto-upload="false"
        :on-change="handleFileChange"
        :show-file-list="false"
        accept=".xlsx,.xls"
      >
        <el-button type="primary">选择Excel文件</el-button>
        <template #tip>
          <div class="el-upload__tip">请上传.xlsx或.xls格式的文件</div>
        </template>
      </el-upload>
      <div v-if="uploadedFile" class="uploaded-file">
        <el-icon><Document /></el-icon>
        <span>{{ uploadedFile.name }}</span>
        <el-button type="text" @click="uploadedFile = null">移除</el-button>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogImportVisible = false">取消</el-button>
          <el-button type="primary" @click="handleImport" :loading="importLoading">导入</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus, Upload, Document } from '@element-plus/icons-vue'
// è¡¨å•数据
const form = reactive({
  reason: '',
  demand: '',
  laintTime: '',
  source: '',
  department: '',
  result: '',
})
// éªŒè¯è§„则
const rules = reactive({
  reason: [{ required: true, message: '请输入投诉原因', trigger: 'blur' }],
  demand: [{ required: true, message: '请输入投诉诉求', trigger: 'blur' }],
  laintTime: [{ required: true, message: '请输入投诉时间', trigger: 'blur' }],
  source: [{ required: true, message: '请输入投诉来源', trigger: 'blur' }],
  department: [{ required: true, message: '请输入处理部门', trigger: 'blur' }],
  result: [{ required: true, message: '请输入投诉结果', trigger: 'blur' }],
})
// çŠ¶æ€
const dialogVisible = ref(false)
const dialogImportVisible = ref(false)
const dialogTitle = ref('新增信访投诉')
const formRef = ref(null)
const laintList = ref([])
const currentRow = ref(null)
const uploadedFile = ref(null)
const importLoading = ref(false)
// æ¨¡æ‹Ÿæ•°æ®
onMounted(() => {
  // è¿™é‡Œå¯ä»¥ä»ŽAPI获取数据
  // æš‚时使用模拟数据
  laintList.value = [
    {
      id: 1,
      reason: '油烟扰民',
      demand: '要求安装油烟净化设备',
      laintTime: '2026-01-10',
      source: '市民热线',
      department: '环保局',
      result: '已责令安装油烟净化设备',
    },
    {
      id: 2,
      reason: '噪音污染',
      demand: '要求降低设备噪音',
      laintTime: '2026-02-15',
      source: '信访办',
      department: '环保局',
      result: '已要求整改噪音问题',
    },
  ]
})
// æ–°å¢ž
function handleAdd() {
  form.reason = ''
  form.demand = ''
  form.laintTime = ''
  form.source = ''
  form.department = ''
  form.result = ''
  dialogTitle.value = '新增信访投诉'
  currentRow.value = null
  dialogVisible.value = true
}
// ç¼–辑
function handleEdit(row) {
  currentRow.value = row
  form.reason = row.reason
  form.demand = row.demand
  form.laintTime = row.laintTime
  form.source = row.source
  form.department = row.department
  form.result = row.result
  dialogTitle.value = '修改信访投诉'
  dialogVisible.value = true
}
// åˆ é™¤
function handleDelete(row) {
  ElMessage.confirm('确定要删除这条记录吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      const index = laintList.value.findIndex((item) => item.id === row.id)
      if (index !== -1) {
        laintList.value.splice(index, 1)
        ElMessage.success('删除成功')
      }
    })
    .catch(() => {
      // å–消删除
    })
}
// æäº¤
function handleSubmit() {
  formRef.value.validate((valid) => {
    if (valid) {
      if (currentRow.value) {
        // ä¿®æ”¹
        const index = laintList.value.findIndex((item) => item.id === currentRow.value.id)
        if (index !== -1) {
          laintList.value[index] = {
            ...currentRow.value,
            ...form,
          }
          ElMessage.success('修改成功')
        }
      } else {
        // æ–°å¢ž
        const newItem = {
          id: Date.now(),
          ...form,
        }
        laintList.value.push(newItem)
        ElMessage.success('新增成功')
      }
      dialogVisible.value = false
    }
  })
}
// å¤„理文件选择
function handleFileChange(file) {
  uploadedFile.value = file.raw
}
// å¤„理Excel导入
function handleImport() {
  if (!uploadedFile.value) {
    ElMessage.warning('请选择要导入的Excel文件')
    return
  }
  importLoading.value = true
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„Excel解析和导入逻辑
  // æš‚时模拟导入过程
  setTimeout(() => {
    // æ¨¡æ‹Ÿå¯¼å…¥æ•°æ®
    const importedData = [
      {
        id: Date.now() + 1,
        reason: '废气排放超标',
        demand: '要求达标排放',
        laintTime: '2026-03-01',
        source: '环保热线',
        department: '环保局',
        result: '已要求整改',
      },
      {
        id: Date.now() + 2,
        reason: '异味扰民',
        demand: '消除异味',
        laintTime: '2026-03-10',
        source: '市民投诉',
        department: '环保局',
        result: '已现场检查',
      },
    ]
    // æ·»åŠ åˆ°åˆ—è¡¨
    laintList.value.push(...importedData)
    ElMessage.success('导入成功,共导入2条记录')
    dialogImportVisible.value = false
    uploadedFile.value = null
    importLoading.value = false
  }, 1500)
}
</script>
<style scoped>
.empty-state {
  padding: 40px 0;
  text-align: center;
}
.sub-title {
  margin-bottom: 20px;
}
.uploaded-file {
  margin-top: 20px;
  padding: 10px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  display: flex;
  align-items: center;
}
.uploaded-file span {
  margin-left: 10px;
  flex: 1;
}
.uploaded-file .el-button {
  margin-left: 10px;
}
</style>
src/views/inspection/scenenew/components/CompPunishment.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,298 @@
<template>
  <div>
    <el-row class="sub-title" justify="space-between">
      <div>行政处罚记录</div>
      <el-button-group>
        <el-button type="primary" @click="handleAdd">
          <el-icon><Plus /></el-icon>
          <span>新增处罚记录</span>
        </el-button>
        <el-button type="success" @click="dialogImportVisible = true">
          <el-icon><Upload /></el-icon>
          <span>导入记录</span>
        </el-button>
      </el-button-group>
    </el-row>
    <!-- è¡¨æ ¼å±•示 -->
    <el-table :data="punishmentList" style="width: 100%">
      <el-table-column prop="name" label="处罚事项" width="180" />
      <el-table-column prop="punishTime" label="处罚时间" width="180" />
      <el-table-column prop="reason" label="处罚理由" />
      <el-table-column prop="result" label="处罚结果" width="180" />
      <el-table-column prop="department" label="处罚部门" width="180" />
      <el-table-column label="操作" width="150" fixed="right">
        <template #default="scope">
          <el-button size="small" @click="handleEdit(scope.row)">修改</el-button>
          <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- ç©ºçŠ¶æ€ -->
    <div v-if="punishmentList.length === 0" class="empty-state">
      <el-empty description="暂无行政处罚记录" />
    </div>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
        <el-form-item label="行政处罚名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入行政处罚名称" />
        </el-form-item>
        <el-form-item label="处罚时间" prop="punishTime">
          <el-date-picker
            v-model="form.punishTime"
            type="date"
            placeholder="请选择处罚时间"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="处罚理由" prop="reason">
          <el-input v-model="form.reason" placeholder="请输入处罚理由" type="textarea" rows="3" />
        </el-form-item>
        <el-form-item label="处罚结果" prop="result">
          <el-input v-model="form.result" placeholder="请输入处罚结果" />
        </el-form-item>
        <el-form-item label="处罚部门" prop="department">
          <el-input v-model="form.department" placeholder="请输入处罚部门" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- Excel导入对话框 -->
    <el-dialog v-model="dialogImportVisible" title="导入处罚记录" width="50%">
      <el-upload
        class="upload-demo"
        action="#"
        :auto-upload="false"
        :on-change="handleFileChange"
        :show-file-list="false"
        accept=".xlsx,.xls"
      >
        <el-button type="primary">选择Excel文件</el-button>
        <template #tip>
          <div class="el-upload__tip">请上传.xlsx或.xls格式的文件</div>
        </template>
      </el-upload>
      <div v-if="uploadedFile" class="uploaded-file">
        <el-icon><Document /></el-icon>
        <span>{{ uploadedFile.name }}</span>
        <el-button type="text" @click="uploadedFile = null">移除</el-button>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogImportVisible = false">取消</el-button>
          <el-button type="primary" @click="handleImport" :loading="importLoading">导入</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Upload, Document } from '@element-plus/icons-vue'
// è¡¨å•数据
const form = reactive({
  name: '',
  punishTime: '',
  reason: '',
  result: '',
  department: '',
})
// éªŒè¯è§„则
const rules = reactive({
  name: [{ required: true, message: '请输入行政处罚名称', trigger: 'blur' }],
  punishTime: [{ required: true, message: '请输入处罚时间', trigger: 'blur' }],
  reason: [{ required: true, message: '请输入处罚理由', trigger: 'blur' }],
  result: [{ required: true, message: '请输入处罚结果', trigger: 'blur' }],
  department: [{ required: true, message: '请输入处罚部门', trigger: 'blur' }],
})
// çŠ¶æ€
const dialogVisible = ref(false)
const dialogImportVisible = ref(false)
const dialogTitle = ref('新增行政处罚')
const formRef = ref(null)
const punishmentList = ref([])
const currentRow = ref(null)
const uploadedFile = ref(null)
const importLoading = ref(false)
// æ¨¡æ‹Ÿæ•°æ®
onMounted(() => {
  // è¿™é‡Œå¯ä»¥ä»ŽAPI获取数据
  // æš‚时使用模拟数据
  punishmentList.value = [
    {
      id: 1,
      name: '违规排放废气',
      punishTime: '2026-01-15',
      reason: '未按规定处理废气,超标排放',
      result: '罚款5000元',
      department: '环保局',
    },
    {
      id: 2,
      name: '未安装油烟净化设备',
      punishTime: '2026-02-20',
      reason: '餐厅未安装油烟净化设备',
      result: '责令限期整改',
      department: '环保局',
    },
  ]
})
// æ–°å¢ž
function handleAdd() {
  form.name = ''
  form.punishTime = ''
  form.reason = ''
  form.result = ''
  form.department = ''
  dialogTitle.value = '新增行政处罚'
  currentRow.value = null
  dialogVisible.value = true
}
// ç¼–辑
function handleEdit(row) {
  currentRow.value = row
  form.name = row.name
  form.punishTime = row.punishTime
  form.reason = row.reason
  form.result = row.result
  form.department = row.department
  dialogTitle.value = '修改行政处罚'
  dialogVisible.value = true
}
// åˆ é™¤
function handleDelete(row) {
  ElMessageBox.confirm('确定要删除这条记录吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      const index = punishmentList.value.findIndex((item) => item.id === row.id)
      if (index !== -1) {
        punishmentList.value.splice(index, 1)
        ElMessage.success('删除成功')
      }
    })
    .catch(() => {
      // å–消删除
    })
}
// æäº¤
function handleSubmit() {
  formRef.value.validate((valid) => {
    if (valid) {
      if (currentRow.value) {
        // ä¿®æ”¹
        const index = punishmentList.value.findIndex((item) => item.id === currentRow.value.id)
        if (index !== -1) {
          punishmentList.value[index] = {
            ...currentRow.value,
            ...form,
          }
          ElMessage.success('修改成功')
        }
      } else {
        // æ–°å¢ž
        const newItem = {
          id: Date.now(),
          ...form,
        }
        punishmentList.value.push(newItem)
        ElMessage.success('新增成功')
      }
      dialogVisible.value = false
    }
  })
}
// å¤„理文件选择
function handleFileChange(file) {
  uploadedFile.value = file.raw
}
// å¤„理Excel导入
function handleImport() {
  if (!uploadedFile.value) {
    ElMessage.warning('请选择要导入的Excel文件')
    return
  }
  importLoading.value = true
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„Excel解析和导入逻辑
  // æš‚时模拟导入过程
  setTimeout(() => {
    // æ¨¡æ‹Ÿå¯¼å…¥æ•°æ®
    const importedData = [
      {
        id: Date.now() + 1,
        name: '未按规定排放污水',
        punishTime: '2026-03-05',
        reason: '未按规定处理污水,超标排放',
        result: '罚款8000元',
        department: '环保局',
      },
      {
        id: Date.now() + 2,
        name: '违规倾倒固废',
        punishTime: '2026-03-12',
        reason: '违规倾倒固体废物',
        result: '罚款10000元',
        department: '环保局',
      },
    ]
    // æ·»åŠ åˆ°åˆ—è¡¨
    punishmentList.value.push(...importedData)
    ElMessage.success('导入成功,共导入2条记录')
    dialogImportVisible.value = false
    uploadedFile.value = null
    importLoading.value = false
  }, 1500)
}
</script>
<style scoped>
.empty-state {
  padding: 40px 0;
  text-align: center;
}
.sub-title {
  margin-bottom: 20px;
}
.uploaded-file {
  margin-top: 20px;
  padding: 10px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  display: flex;
  align-items: center;
}
.uploaded-file span {
  margin-left: 10px;
  flex: 1;
}
.uploaded-file .el-button {
  margin-left: 10px;
}
</style>
src/views/monitor/DataException.vue
@@ -19,7 +19,7 @@
          </div>
          <div class="form-row">
            <div class="form-item full-width">
              <TimeSelect @submit-time="giveTime" :useNewStyle="true"></TimeSelect>
              <TimeSelect @submit-time="giveTime" :useNewStyle="false"></TimeSelect>
            </div>
          </div>
        </div>