| | |
| | | <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"> |
| | | <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-title">绿码店铺</div> |
| | | <div class="card-value">{{ statistics.greenCount }}<el-text>个</el-text></div> |
| | | <div class="card-percentage">{{ statistics.greenPercentage }}%</div> |
| | | </div> |
| | | </el-card> |
| | |
| | | <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-title">黄码店铺</div> |
| | | <div class="card-value">{{ statistics.yellowCount }}<el-text>个</el-text></div> |
| | | <div class="card-percentage">{{ statistics.yellowPercentage }}%</div> |
| | | </div> |
| | | </el-card> |
| | |
| | | <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-title">红码店铺</div> |
| | | <div class="card-value">{{ statistics.redCount }}<el-text>个</el-text></div> |
| | | <div class="card-percentage">{{ statistics.redPercentage }}%</div> |
| | | </div> |
| | | </el-card> |
| | |
| | | </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 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" /> |
| | | <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> |
| | | <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> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="lastUpdate" label="上次更新时间" width="180" /> |
| | | <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)" |
| | | >风险预警记录</el-button |
| | | > --> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <!-- 分页组件 --> |
| | | <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> |
| | | |
| | | <!-- 详情抽屉 --> |
| | | <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"> |
| | | <!-- 环信码大图标及当前评分 --> |
| | | <div class="code-header"> |
| | | <div class="code-icon" :class="selectedShop.code"> |
| | | {{ getCodeText(selectedShop.code) }} |
| | | <el-row justify="space-between" style="flex-wrap: nowrap"> |
| | | <!-- 环信码大图标及当前评分 --> |
| | | <div class="code-header"> |
| | | <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> |
| | | <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 class="chart-section"> |
| | | <h3>评分维度分析</h3> |
| | | <div class="radar-chart"> |
| | | <canvas ref="radarChart" width="500" height="400"></canvas> |
| | | </div> |
| | | </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> |
| | | |
| | |
| | | </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({ |
| | |
| | | '华泾镇', |
| | | ] |
| | | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | district: '徐汇区', |
| | | town: getRandomElement(xuhuiTowns), |
| | | code: 'green', |
| | | score: 95, |
| | | score: 90, |
| | | trend: generateRandomTrend(), |
| | | lastUpdate: generateRandomDate(), |
| | | warnings: [ |
| | |
| | | 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() |
| | | }) |
| | | |
| | | // 方法 |
| | |
| | | |
| | | 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) { |
| | |
| | | } |
| | | } |
| | | |
| | | // 雷达图和趋势图引用 |
| | | 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) { |
| | |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .view-tabs { |
| | | .shop-list { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .pagination { |
| | | margin-top: 20px; |
| | | text-align: right; |
| | | } |
| | | |
| | | .trend { |
| | |
| | | |
| | | .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 { |
| | |
| | | } |
| | | |
| | | .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; |
| | |
| | | } |
| | | |
| | | .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> |