You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

440 lines
15 KiB

<template>
<div class="statistical-home">
<div class="wrap_all">
<!-- 左侧:行政执法 -->
<div class="wrap_left">
<div class="cont_left">
<h1 class="block_tit">行政执法</h1>
<div class="count_admin_wrap">
<div class="count_admin">
<label><img src="/statistical/images/icon_jc2.png" alt="单独执法" />单独执法</label>
<div class="count_admin_num">
<b>{{ coordination.singleNum }}</b>
<span class="number_percent">{{ coordination.singlePer }}<i>%</i></span>
</div>
<div class="percent_box">
<div class="percent-tag">
<span class="data-rise"><i class="fa fa-long-arrow-up"></i></span>
<span class="data-change">{{ coordination.singleYoy }}%</span>
<label>同比</label>
</div>
<div class="percent-tag">
<span class="data-fall"><i class="fa fa-long-arrow-down"></i></span>
<span class="data-change">{{ coordination.singleRr }}%</span>
<label>环比</label>
</div>
</div>
</div>
<div class="count_admin">
<label><img src="/statistical/images/icon_jc.png" alt="联合执法" />联合执法</label>
<div class="count_admin_num">
<b>{{ coordination.unionNum }}</b>
<span class="number_percent">{{ coordination.unionPer }}<i>%</i></span>
</div>
<div class="percent_box">
<div class="percent-tag">
<span class="data-rise"><i class="fa fa-long-arrow-up"></i></span>
<span class="data-change">{{ coordination.unionYoy }}%</span>
<label>同比</label>
</div>
<div class="percent-tag">
<span class="data-fall"><i class="fa fa-long-arrow-down"></i></span>
<span class="data-change">{{ coordination.unionRr }}%</span>
<label>环比</label>
</div>
</div>
</div>
<div class="count_admin">
<label><img src="/statistical/images/icon_qy.png" alt="被执法企业" />被执法企业</label>
<div class="count_admin_num"><b>{{ enterprise.num }}</b></div>
<div class="percent_box">
<div class="percent-tag">
<span class="data-rise"><i class="fa fa-long-arrow-up"></i></span>
<span class="data-change">{{ enterprise.yoy }}%</span>
<label>同比</label>
</div>
<div class="percent-tag">
<span class="data-fall"><i class="fa fa-long-arrow-down"></i></span>
<span class="data-change">{{ enterprise.rr }}%</span>
<label>环比</label>
</div>
</div>
</div>
<div class="count_admin">
<label><img src="/statistical/images/icon_yj.png" alt="入企压减率" />入企压减率</label>
<div class="count_admin_num"><b>{{ coordination.cutNum }}</b></div>
<div class="percent_box">
<div class="percent-tag yjlv">
<span class="number_percent">{{ coordination.cutPer }}<i>%</i></span>
</div>
</div>
</div>
</div>
<div ref="adminPieRef" class="chartbox_right" style="margin-top: -10%"></div>
</div>
</div>
<!-- 右侧:非行政执法 -->
<div class="wrap_right">
<div class="cont_left">
<h1 class="block_tit">非行政执法</h1>
<div class="count_wrap">
<div v-for="(item, index) in nonTypeList" :key="item.dict_label" class="count_item">
<label>
<img :src="`/statistical/images/icon_24_r${index + 1}.png`" :alt="item.dict_label" />
{{ item.dict_label }}
</label>
<span class="number_count">{{ item.num }}</span>
</div>
</div>
<div ref="nonPieRef" class="chartbox_right"></div>
</div>
</div>
<!-- 中间:地图 -->
<div class="wrap_cent">
<div class="select_set">
<div class="select_item">
<span>深圳市</span>
<span class="time_wrap">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
:disabled-date="disabledDate"
clearable
@change="onDateChange"
/>
</span>
</div>
</div>
<div class="link_btn" style="text-align: right">
<router-link v-if="showLeftLink" to="/enforcement-code/statistical/city-direct">{{ leftLinkText }}</router-link>
<router-link to="/enforcement-code/statistical/district-direct"><span>{{ rightLinkText }}</span></router-link>
</div>
<div class="count_box">
<div class="count_item admin">
<div class="count_tit"><label>行政执法</label></div>
<div class="count_num">
<div class="count_num_all">{{ coordination.allNum }}</div>
<div class="count_num_sub">
<span>(市直:<b>{{ sJcNum }}</b></span>
<span>各区:<b>{{ qJcNum }}</b>)</span>
</div>
</div>
</div>
<div class="count_item non_admin">
<div class="count_tit"><label>非行政执法</label></div>
<div class="count_num">
<div class="count_num_all">{{ njcAll }}</div>
<div class="count_num_sub">
<span>(市直:<b>{{ sNjcNum }}</b></span>
<span>各区:<b>{{ qNjcNum }}</b>)</span>
</div>
</div>
</div>
</div>
<div class="map_box">
<img class="szmap" src="/statistical/images/szmap_3d.png" alt="深圳地图" width="1920" height="1080" border="0" />
<div class="label_wrap">
<DistrictPopup
v-for="config in districtConfigs"
:key="config.key"
:config="config"
:data="districtDataMap[config.name]"
:expanded="expandedDistrictIdx === config.idx"
@toggle="onDistrictToggle"
/>
</div>
</div>
</div>
<!-- 底部:趋势图 -->
<div class="wrap_bottom">
<div class="cont_left">
<h3>涉企执法趋势</h3>
<div class="tab_tit">
<a :class="{ active: trendTab === 0 }" @click="switchTrendTab(0)">行政执法</a>
<a :class="{ active: trendTab === 1 }" @click="switchTrendTab(1)">非行政执法</a>
</div>
<div class="tab_chart">
<div v-show="trendTab === 0" ref="adminTrendRef" class="chartbox" style="position: absolute"></div>
<div v-show="trendTab === 1" ref="nonTrendRef" class="chartbox" style="position: absolute"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import * as echarts from 'echarts';
import DistrictPopup from './components/DistrictPopup.vue';
import {
analysisEnterprises,
analysisEnterprisesCoordination,
analysisEnterprisesNonAll,
analysisNonRegistrationType,
analysisRegistrationType,
analysisTime,
areaRegistration,
nonAnalysisTime
} from '@/api/supervision/statistical/statisticalPanel';
import type { AreaRegistrationData, DictStatItem } from '@/api/supervision/statistical/types';
import {
normalizeAreaRegistration,
normalizeCoordination,
normalizeDictStatList,
normalizeEnterpriseStat,
normalizeTimeSeries,
toNum,
unwrapData
} from '@/api/supervision/statistical/adapters';
import { buildAdminPieOption, buildNonAdminPieOption, buildTrendLineOption } from '../utils/chartOptions';
import { DISTRICT_CONFIGS, DISTRICT_LIST } from '../utils/districtConfig';
async function safeLoad<T>(fn: () => Promise<T>): Promise<T | null> {
try {
return await fn();
} catch (e) {
console.warn('[统计首页] 接口加载失败', e);
return null;
}
}
const districtConfigs = DISTRICT_CONFIGS;
const dateRange = ref<[string, string] | null>(null);
const trendTab = ref(0);
const expandedDistrictIdx = ref<number | null>(null);
const coordination = reactive({
roleFlag: 1,
roleDept: '',
allNum: 0,
nonNum: 0,
unionNum: 0,
unionYoy: '0.0',
unionRr: '0.0',
unionPer: '0.0',
singleNum: 0,
singleYoy: '0.0',
singleRr: '0.0',
singlePer: '0.0',
cutNum: 0,
cutPer: '0.0'
});
const enterprise = reactive({ num: 0, yoy: '0.0', rr: '0.0' });
const nonTypeList = ref<DictStatItem[]>([]);
const njcAll = ref(0);
const qJcNum = ref(0);
const qNjcNum = ref(0);
const districtDataMap = ref<Record<string, AreaRegistrationData>>({});
const adminPieRef = ref<HTMLElement>();
const nonPieRef = ref<HTMLElement>();
const adminTrendRef = ref<HTMLElement>();
const nonTrendRef = ref<HTMLElement>();
let adminPieChart: echarts.ECharts | null = null;
let nonPieChart: echarts.ECharts | null = null;
let adminTrendChart: echarts.ECharts | null = null;
let nonTrendChart: echarts.ECharts | null = null;
const showLeftLink = computed(() => coordination.roleFlag === 1);
const leftLinkText = computed(() => '各市直执法单位综合分析');
const rightLinkText = computed(() => {
if ([9, 4, 3].includes(coordination.roleFlag)) {
return `${coordination.roleDept}综合分析`;
}
return '各区综合分析';
});
const sJcNum = computed(() => ([9, 4, 3].includes(coordination.roleFlag) ? '-' : coordination.allNum - qJcNum.value));
const sNjcNum = computed(() => ([9, 4, 3].includes(coordination.roleFlag) ? '-' : njcAll.value - qNjcNum.value));
function disabledDate(time: Date) {
const min = new Date('2019-04-14').getTime();
const max = new Date('2099-06-16').getTime();
return time.getTime() < min || time.getTime() > max;
}
function getTimeParams() {
if (!dateRange.value) return { beginTime: undefined, endTime: undefined };
return { beginTime: dateRange.value[0], endTime: dateRange.value[1] };
}
async function loadCoordination() {
const res = await safeLoad(() => analysisEnterprisesCoordination(getTimeParams()));
if (!res) return;
const data = normalizeCoordination(unwrapData(res));
Object.assign(coordination, data);
njcAll.value = data.nonNum;
}
async function loadEnterprise() {
const res = await safeLoad(() => analysisEnterprises(getTimeParams()));
if (!res) return;
Object.assign(enterprise, normalizeEnterpriseStat(unwrapData(res)));
}
async function loadNjcAll() {
const res = await safeLoad(() => analysisEnterprisesNonAll(getTimeParams()));
if (!res) return;
njcAll.value = toNum(unwrapData(res));
}
async function loadAdminType() {
const res = await safeLoad(() => analysisRegistrationType(getTimeParams()));
if (!res) return;
const list = normalizeDictStatList(unwrapData(res));
const chartData = list.filter((item) => item.dict_label).map((item) => ({ value: item.num, name: item.dict_label }));
await nextTick();
if (adminPieRef.value && chartData.length) {
adminPieChart ??= echarts.init(adminPieRef.value);
adminPieChart.setOption(buildAdminPieOption(chartData), true);
}
}
async function loadNonType() {
const res = await safeLoad(() => analysisNonRegistrationType(getTimeParams()));
if (!res) return;
const list = normalizeDictStatList(unwrapData(res));
nonTypeList.value = list;
const chartData = list.filter((item) => item.dict_label).map((item) => ({ value: item.num, name: item.dict_label }));
await nextTick();
if (nonPieRef.value && chartData.length) {
nonPieChart ??= echarts.init(nonPieRef.value);
nonPieChart.setOption(buildNonAdminPieOption(chartData), true);
}
}
async function loadAdminTrend() {
const res = await safeLoad(() => analysisTime(getTimeParams()));
if (!res) return;
const { times, ins, ent } = normalizeTimeSeries(unwrapData(res));
await nextTick();
if (adminTrendRef.value && times.length) {
adminTrendChart ??= echarts.init(adminTrendRef.value);
adminTrendChart.setOption(
buildTrendLineOption(times, ins.map(Number), ent.map(Number), '行政执法数量', '被检查企业数量', '行政执法数量(宗)'),
true
);
}
}
async function loadNonTrend() {
const res = await safeLoad(() => nonAnalysisTime(getTimeParams()));
if (!res) return;
const { times, ins, ent } = normalizeTimeSeries(unwrapData(res));
await nextTick();
if (nonTrendRef.value && times.length) {
nonTrendChart ??= echarts.init(nonTrendRef.value);
nonTrendChart.setOption(
buildTrendLineOption(times, ins.map(Number), ent.map(Number), '非行政执法数量', '被检查企业数量', '行政执法数量(宗)'),
true
);
}
}
async function loadAreaData() {
const params = getTimeParams();
let jcSum = 0;
let njcSum = 0;
const dataMap: Record<string, AreaRegistrationData> = {};
const results = await Promise.allSettled(
DISTRICT_LIST.map((area) => areaRegistration({ ...params, deptId: area }))
);
results.forEach((result, index) => {
if (result.status !== 'fulfilled') return;
const data = normalizeAreaRegistration(unwrapData(result.value));
const area = data.area || DISTRICT_LIST[index];
dataMap[area] = data;
jcSum += data.allNum;
njcSum += data.njcNum;
});
districtDataMap.value = dataMap;
qJcNum.value = jcSum;
qNjcNum.value = njcSum;
}
async function refreshAll() {
await Promise.allSettled([
loadCoordination(),
loadEnterprise(),
loadNjcAll(),
loadAdminType(),
loadNonType(),
loadAdminTrend(),
loadNonTrend(),
loadAreaData()
]);
}
function onDateChange() {
refreshAll();
}
function onDistrictToggle(idx: number) {
expandedDistrictIdx.value = expandedDistrictIdx.value === idx ? null : idx;
}
function switchTrendTab(tab: number) {
trendTab.value = tab;
nextTick(() => {
if (tab === 0) adminTrendChart?.resize();
else nonTrendChart?.resize();
});
}
function handleResize() {
adminPieChart?.resize();
nonPieChart?.resize();
adminTrendChart?.resize();
nonTrendChart?.resize();
}
onMounted(() => {
refreshAll();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
adminPieChart?.dispose();
nonPieChart?.dispose();
adminTrendChart?.dispose();
nonTrendChart?.dispose();
});
</script>
<style scoped>
@import '/statistical/styles/home.css';
@import '/statistical/font-awesome-bak/css/font-awesome.min.css';
.statistical-home {
width: 100%;
min-height: calc(100vh - 50px);
overflow: auto;
background: #0b1d4b;
}
.statistical-home :deep(.el-date-editor) {
--el-input-bg-color: rgba(0, 30, 80, 0.6);
--el-input-border-color: rgba(58, 174, 251, 0.4);
--el-input-text-color: rgba(255, 255, 255, 0.85);
--el-input-placeholder-color: rgba(255, 255, 255, 0.45);
}
.statistical-home :deep(.el-range-separator) {
color: rgba(255, 255, 255, 0.6);
}
</style>