| | |
| | | <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> |