|
|
|
@ -22,15 +22,15 @@ |
|
|
|
<!-- 第一行:三列统计 --> |
|
|
|
<div class="stats-row three-col"> |
|
|
|
<div class="stat-item"> |
|
|
|
<div class="stat-value">443.9216万</div> |
|
|
|
<div class="stat-value">{{ formatEnterpriseTotal(overviewData.qyzs) }}</div> |
|
|
|
<div class="stat-label">企业总数</div> |
|
|
|
</div> |
|
|
|
<div class="stat-item"> |
|
|
|
<div class="stat-value">49225次</div> |
|
|
|
<div class="stat-value">{{ lawStats.ddzf ?? '-' }}次</div> |
|
|
|
<div class="stat-label">单独执法</div> |
|
|
|
</div> |
|
|
|
<div class="stat-item"> |
|
|
|
<div class="stat-value">1277次</div> |
|
|
|
<div class="stat-value">{{ lawStats.lhzf ?? '-' }}次</div> |
|
|
|
<div class="stat-label">联合执法</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -38,11 +38,11 @@ |
|
|
|
<!-- 第二行:两列统计 --> |
|
|
|
<div class="stats-row two-col"> |
|
|
|
<div class="stat-item"> |
|
|
|
<div class="stat-value">33548家</div> |
|
|
|
<div class="stat-value">{{ lawStats.bzfqy ?? '-' }}家</div> |
|
|
|
<div class="stat-label">被执法企业</div> |
|
|
|
</div> |
|
|
|
<div class="stat-item"> |
|
|
|
<div class="stat-value">6%</div> |
|
|
|
<div class="stat-value">{{ formatReduceRate(lawStats.rqyjl) }}</div> |
|
|
|
<div class="stat-label">入企压减率</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -73,11 +73,11 @@ |
|
|
|
<!-- 执法力量 --> |
|
|
|
<div v-if="overviewTab === 1" class="tab-content-cards"> |
|
|
|
<div class="large-card"> |
|
|
|
<div class="large-card-value">503家</div> |
|
|
|
<div class="large-card-value">{{ overviewData.zfdws ?? '-' }}<span class="large-card-unit">家</span></div> |
|
|
|
<div class="large-card-label">执法单位数</div> |
|
|
|
</div> |
|
|
|
<div class="large-card"> |
|
|
|
<div class="large-card-value">16359人</div> |
|
|
|
<div class="large-card-value">{{ overviewData.zfrys ?? '-' }}<span class="large-card-unit">人</span></div> |
|
|
|
<div class="large-card-label">执法人员数</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -85,7 +85,7 @@ |
|
|
|
<!-- 执法频次 --> |
|
|
|
<div v-if="overviewTab === 2" class="tab-content-cards"> |
|
|
|
<div class="large-card"> |
|
|
|
<div class="large-card-value">3<span class="large-card-unit">次/周</span></div> |
|
|
|
<div class="large-card-value">{{ overviewData.pjjcpc ?? '-' }}<span class="large-card-unit">次/周</span></div> |
|
|
|
<div class="large-card-label">涉企检查频次</div> |
|
|
|
</div> |
|
|
|
<div class="large-card"> |
|
|
|
@ -124,23 +124,23 @@ |
|
|
|
<div class="section-title">执法监管</div> |
|
|
|
<div class="supervision-grid"> |
|
|
|
<div class="supervision-item"> |
|
|
|
<div class="sv-value">336<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-value">{{ overviewData.jhzs ?? '-' }}<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-label">计划总数</div> |
|
|
|
</div> |
|
|
|
<div class="supervision-item"> |
|
|
|
<div class="sv-value">1473<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-value">{{ overviewData.ygzs ?? '-' }}<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-label">预告总数</div> |
|
|
|
</div> |
|
|
|
<div class="supervision-item"> |
|
|
|
<div class="sv-value">50503<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-value">{{ overviewData.sqzfzs ?? '-' }}<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-label">涉企执法总数</div> |
|
|
|
</div> |
|
|
|
<div class="supervision-item"> |
|
|
|
<div class="sv-value">53<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-value">{{ overviewData.jcpjzs ?? '-' }}<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-label">检查反馈总数</div> |
|
|
|
</div> |
|
|
|
<div class="supervision-item"> |
|
|
|
<div class="sv-value">59<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-value">{{ overviewData.jctszs ?? '-' }}<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-label">检查投诉总数</div> |
|
|
|
</div> |
|
|
|
<!-- <div class="supervision-item"> |
|
|
|
@ -157,11 +157,11 @@ |
|
|
|
<!-- 第一行:两列统计 --> |
|
|
|
<div class="warning-grid"> |
|
|
|
<div class="warning-item"> |
|
|
|
<div class="sv-value">45<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-value">{{ overviewData.fxxyyj ?? '-' }}<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-label">风险信用预警</div> |
|
|
|
</div> |
|
|
|
<div class="warning-item"> |
|
|
|
<div class="sv-value">65<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-value">{{ overviewData.gpjcyj ?? '-' }}<span class="sv-unit">条</span></div> |
|
|
|
<div class="sv-label">高额检查预警</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -198,7 +198,7 @@ |
|
|
|
<!-- 时间轴作为最后一行 --> |
|
|
|
<tr class="wt-timeline-row"> |
|
|
|
<td class="wt-name"></td> |
|
|
|
<td v-for="(item, idx) in warningTimeline" :key="idx" class="wt-time-cell">{{ item }} |
|
|
|
<td v-for="(item, idx) in currentTimeline" :key="idx" class="wt-time-cell">{{ item }} |
|
|
|
</td> |
|
|
|
</tr> |
|
|
|
</tbody> |
|
|
|
@ -212,6 +212,11 @@ |
|
|
|
<script setup name="enforcementRecord"> |
|
|
|
import { ref, reactive, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'; |
|
|
|
import * as echarts from 'echarts'; |
|
|
|
import { |
|
|
|
getOverviewLawEnforcement, |
|
|
|
querySituationTrendAreaList, |
|
|
|
querySituationTrendDomainList, |
|
|
|
} from '@/api/enforcement/supervision'; |
|
|
|
|
|
|
|
const activeTab = ref(0); |
|
|
|
const overviewTab = ref(0); |
|
|
|
@ -222,13 +227,44 @@ const contentRef = ref(null); |
|
|
|
const forceChartRef = ref(null); |
|
|
|
let forceChartInstance = null; |
|
|
|
|
|
|
|
// 接口数据 |
|
|
|
const overviewData = ref({}); |
|
|
|
const lawStats = ref({}); |
|
|
|
|
|
|
|
// 执法力度图表数据 |
|
|
|
const forceChartData = [ |
|
|
|
{ name: '行政检查', value: 5068, percent: 96.09, color: '#5b8ff9' }, |
|
|
|
{ name: '行政处罚', value: 152, percent: 2.88, color: '#5ad8a6' }, |
|
|
|
{ name: '行政强制', value: 5, percent: 0.09, color: '#5d7092' }, |
|
|
|
{ name: '其他', value: 49, percent: 0.92, color: '#f6bd16' }, |
|
|
|
]; |
|
|
|
const forceChartData = ref([]); |
|
|
|
const forceTotal = ref(0); |
|
|
|
const forceColors = ['#5b8ff9', '#5ad8a6', '#5d7092', '#f6bd16', '#e8684a']; |
|
|
|
|
|
|
|
// 企业总数格式化 |
|
|
|
const formatEnterpriseTotal = (value) => { |
|
|
|
if (value === null || value === undefined || value === '') return '-'; |
|
|
|
const num = Number(value); |
|
|
|
if (Number.isNaN(num)) return '-'; |
|
|
|
if (num >= 10000) return `${(num / 10000).toFixed(4)}万`; |
|
|
|
return String(num); |
|
|
|
}; |
|
|
|
|
|
|
|
// 入企压减率格式化(后端返回 0.06 形式的比例) |
|
|
|
const formatReduceRate = (value) => { |
|
|
|
if (value === null || value === undefined || value === '') return '-'; |
|
|
|
const num = Number(value); |
|
|
|
if (Number.isNaN(num)) return '-'; |
|
|
|
return `${(num * 100).toFixed(0)}%`; |
|
|
|
}; |
|
|
|
|
|
|
|
// 更新执法力度图表数据 |
|
|
|
const updateForceChartData = () => { |
|
|
|
const list = lawStats.value.zfld || []; |
|
|
|
const total = list.reduce((sum, item) => sum + (Number(item.number) || 0), 0); |
|
|
|
forceTotal.value = total; |
|
|
|
forceChartData.value = list.map((item, index) => ({ |
|
|
|
name: item.name || '-', |
|
|
|
value: Number(item.number) || 0, |
|
|
|
percent: total ? ((Number(item.number) || 0) / total * 100).toFixed(2) : 0, |
|
|
|
color: forceColors[index % forceColors.length], |
|
|
|
})); |
|
|
|
}; |
|
|
|
|
|
|
|
// 执法频次数据 |
|
|
|
const frequencyData = ref([ |
|
|
|
@ -271,13 +307,13 @@ const renderForceChart = () => { |
|
|
|
label: { |
|
|
|
show: true, |
|
|
|
position: 'center', |
|
|
|
formatter: '{value|5274次}\n{label|合计}', |
|
|
|
formatter: `{value|${forceTotal.value}次}\n{label|合计}`, |
|
|
|
rich: { |
|
|
|
value: { fontSize: 14, fontWeight: '500', color: '#333', lineHeight: 28 }, |
|
|
|
label: { fontSize: 12, color: '#999', lineHeight: 18 }, |
|
|
|
}, |
|
|
|
}, |
|
|
|
data: forceChartData.map(item => ({ value: item.value, itemStyle: { color: item.color } })), |
|
|
|
data: forceChartData.value.map(item => ({ value: item.value, itemStyle: { color: item.color } })), |
|
|
|
}], |
|
|
|
}; |
|
|
|
forceChartInstance.setOption(option); |
|
|
|
@ -346,6 +382,8 @@ onMounted(() => { |
|
|
|
if (container) { |
|
|
|
container.addEventListener('scroll', handleScroll, { passive: true }); |
|
|
|
} |
|
|
|
fetchOverview(); |
|
|
|
fetchWarningTables(); |
|
|
|
nextTick(() => { |
|
|
|
if (overviewTab.value === 0) { |
|
|
|
renderForceChart(); |
|
|
|
@ -361,42 +399,86 @@ onUnmounted(() => { |
|
|
|
if (observer) observer.disconnect(); |
|
|
|
}); |
|
|
|
|
|
|
|
// 预警态势表格数据 |
|
|
|
// 预警态势时间轴 |
|
|
|
const warningTimeline = ref(['2025Q2', '2025Q3', '2025Q4', '2026Q1']); |
|
|
|
|
|
|
|
// 各区数据 |
|
|
|
const warningTableData = ref([ |
|
|
|
{ name: '福田区', statuses: ['一般', '良好', '良好', '优良'] }, |
|
|
|
{ name: '罗湖区', statuses: ['优良', '良好', '优良', '良好'] }, |
|
|
|
{ name: '盐田区', statuses: ['一般', '优良', '优良', '良好'] }, |
|
|
|
{ name: '南山区', statuses: ['一般', '良好', '良好', '良好'] }, |
|
|
|
{ name: '宝安区', statuses: ['良好', '优良', '优良', '优良'] }, |
|
|
|
{ name: '龙岗区', statuses: ['一般', '优良', '良好', '优良'] }, |
|
|
|
{ name: '龙华区', statuses: ['优良', '良好', '优良', '优良'] }, |
|
|
|
{ name: '坪山区', statuses: ['优良', '优良', '优良', '良好'] }, |
|
|
|
{ name: '光明区', statuses: ['良好', '良好', '优良', '优良'] }, |
|
|
|
{ name: '大鹏新区', statuses: ['一般', '良好', '良好', '优良'] }, |
|
|
|
{ name: '深汕特别合作区', statuses: ['一般', '良好', '优良', '良好'] }, |
|
|
|
]); |
|
|
|
// 预警态势时间轴与表格数据 |
|
|
|
const areaTimeline = ref([]); |
|
|
|
const domainTimeline = ref([]); |
|
|
|
const areaTableData = ref([]); |
|
|
|
const domainTableData = ref([]); |
|
|
|
|
|
|
|
// 各领域数据 |
|
|
|
const fieldTableData = ref([ |
|
|
|
{ name: '应急管理局', statuses: ['一般', '优良', '优良', '优良'] }, |
|
|
|
{ name: '消防救援', statuses: ['一般', '良好', '优良', '良好'] }, |
|
|
|
{ name: '住建和建设局', statuses: ['一般', '良好', '良好', '优良'] }, |
|
|
|
{ name: '生态环境局', statuses: ['一般', '优良', '优良', '优良'] }, |
|
|
|
{ name: '卫生健康委员会', statuses: ['优良', '良好', '优良', '良好'] }, |
|
|
|
{ name: '交通运输委员会', statuses: ['一般', '优良', '良好', '优良'] }, |
|
|
|
{ name: '市场监督管理局', statuses: ['良好', '良好', '优良', '良好'] }, |
|
|
|
{ name: '城市管理和综合执法局', statuses: ['一般', '优良', '良好', '优良'] }, |
|
|
|
{ name: '消防救援支队', statuses: ['一般', '良好', '优良', '良好'] }, |
|
|
|
]); |
|
|
|
const mapGrade = (code) => ({ A: '优良', B: '良好', C: '一般' }[code] || '-'); |
|
|
|
|
|
|
|
// 按名称聚合,季度作为列 |
|
|
|
const buildTrendTable = (list, nameKey) => { |
|
|
|
const periodMap = new Map(); |
|
|
|
list.forEach(item => { |
|
|
|
periodMap.set(`${item.year}Q${item.quarter}`, { year: Number(item.year), quarter: Number(item.quarter) }); |
|
|
|
}); |
|
|
|
const periods = [...periodMap.values()] |
|
|
|
.sort((a, b) => a.year - b.year || a.quarter - b.quarter) |
|
|
|
.map(item => `${item.year}Q${item.quarter}`); |
|
|
|
|
|
|
|
const rows = []; |
|
|
|
const rowMap = new Map(); |
|
|
|
list.forEach(item => { |
|
|
|
const name = item[nameKey] || '-'; |
|
|
|
if (!rowMap.has(name)) { |
|
|
|
const row = { name, statusMap: {} }; |
|
|
|
rowMap.set(name, row); |
|
|
|
rows.push(row); |
|
|
|
} |
|
|
|
rowMap.get(name).statusMap[`${item.year}Q${item.quarter}`] = mapGrade(item.zhpj); |
|
|
|
}); |
|
|
|
|
|
|
|
return { |
|
|
|
periods, |
|
|
|
rows: rows.map(row => ({ |
|
|
|
name: row.name, |
|
|
|
statuses: periods.map(period => row.statusMap[period] || '-'), |
|
|
|
})), |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
// 移动监督端指标 |
|
|
|
const fetchOverview = () => { |
|
|
|
getOverviewLawEnforcement({}, (res) => { |
|
|
|
if (res && res.code === 200 && res.data) { |
|
|
|
overviewData.value = res.data.data || {}; |
|
|
|
lawStats.value = { |
|
|
|
ddzf: res.data.ddzf, |
|
|
|
lhzf: res.data.lhzf, |
|
|
|
bzfqy: res.data.bzfqy, |
|
|
|
rqyjl: res.data.rqyjl, |
|
|
|
zfld: res.data.zfld || [], |
|
|
|
}; |
|
|
|
updateForceChartData(); |
|
|
|
nextTick(() => { |
|
|
|
if (overviewTab.value === 0) renderForceChart(); |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// 各区/各领域风险态势 |
|
|
|
const fetchWarningTables = () => { |
|
|
|
querySituationTrendAreaList({}, (res) => { |
|
|
|
if (res && res.code === 200 && Array.isArray(res.data)) { |
|
|
|
const { periods, rows } = buildTrendTable(res.data, 'districtName'); |
|
|
|
areaTimeline.value = periods; |
|
|
|
areaTableData.value = rows; |
|
|
|
} |
|
|
|
}); |
|
|
|
querySituationTrendDomainList({}, (res) => { |
|
|
|
if (res && res.code === 200 && Array.isArray(res.data)) { |
|
|
|
const { periods, rows } = buildTrendTable(res.data, 'lymc'); |
|
|
|
domainTimeline.value = periods; |
|
|
|
domainTableData.value = rows; |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// 根据 Tab 切换数据源 |
|
|
|
const currentTableData = computed(() => { |
|
|
|
return warningTab.value === 0 ? warningTableData.value : fieldTableData.value; |
|
|
|
}); |
|
|
|
const currentTimeline = computed(() => warningTab.value === 0 ? areaTimeline.value : domainTimeline.value); |
|
|
|
const currentTableData = computed(() => warningTab.value === 0 ? areaTableData.value : domainTableData.value); |
|
|
|
</script> |
|
|
|
|
|
|
|
<style lang="less" scoped> |
|
|
|
|