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.

488 lines
21 KiB

<template>
<div class="statistical-analysis" :style="cssVars">
<div class="wrap_all">
<div class="select_set">
<div class="select_item">
<label class="select_area" :class="{ active: deptPanelOpen }" @click="deptPanelOpen = !deptPanelOpen">
{{ deptName }}<a></a>
</label>
<dl v-show="deptPanelOpen" class="fold_cont" :class="{ 'is-open': deptPanelOpen }">
<dd
v-for="dept in deptList"
:key="String(dept.deptId)"
class="check_label"
:class="{ selchk: String(dept.deptId) === String(selectedDeptId) || (dept.deptId === null && selectedDeptId === null) }"
@click="changeDept(dept.deptId, dept.deptName)"
>
{{ dept.deptName }}
</dd>
</dl>
</div>
<div class="select_item">
<label class="select_time">时间筛选<a></a></label>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
value-format="YYYY-MM-DD"
clearable
popper-class="analysis-date-picker"
@change="refreshAll"
/>
<span>综合数据</span>
</div>
</div>
<div class="link_btn" style="text-align: right;">
<a href="javascript:void(0)" style="display: none;">各市直执法部门综合分析</a>
<router-link to="/enforcement-code/statistical/district-direct">各区执法综合分析</router-link>
</div>
<TopSixMetrics :m="stats" />
<div class="echarts_box">
<div class="inner">
<div class="twobox">
<div class="data_cont threebox">
<h3 class="chart_tit">行政执法类别及占比</h3>
<div class="count_wrap" id="law_category">
<div v-for="item in lawCategoryItems" :key="item.label" class="count_item">
<div class="count_lab">{{ item.label }}</div>
<div class="count_div">
<img :src="item.icon" :alt="item.label" />
<div class="count_cont">
<span class="number_count">{{ item.count }}</span>
<span class="number_percent">{{ item.rate }}<i>%</i></span>
</div>
</div>
</div>
</div>
</div>
<div class="data_cont threebox">
<h3 class="chart_tit">行政执法投诉类别及占比</h3>
<div class="count_wrap com_cate">
<div v-for="item in caseTypeItems" :key="item.label" class="count_item">
<div class="count_lab">{{ item.label }}</div>
<div class="count_div">
<img :src="item.icon" :alt="item.label" />
<div class="count_cont">
<span class="number_count">{{ item.count }}</span>
<span class="number_percent">{{ item.rate }}<i>%</i></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="twobox">
<div class="data_cont threebox">
<h3 class="chart_tit">重点领域类别及占比</h3>
<div class="count_wrap key_area">
<div v-for="item in lawFieldItems" :key="item.label" class="count_item">
<div class="count_lab">{{ item.label }}</div>
<div class="count_cont">
<span class="number_count">{{ item.count }}</span>
<span class="number_percent">{{ item.rate }}<i>%</i></span>
</div>
</div>
</div>
</div>
<div class="data_cont threebox">
<h3 class="chart_tit">行政执法投诉评价</h3>
<div class="count_review">
<div class="count_com">
<h4>投诉</h4>
<div class="count_list">
<div class="count_row">
<span>投诉总量</span><span>{{ stats.entComplaint?.count ?? '-' }}</span>
<span class="percent_ym">
<div class="percent-tag"><label>同比</label><span>{{ fmt(stats.entComplaint?.yoy) }}</span></div>
<div class="percent-tag"><label>环比</label><span>{{ fmt(stats.entComplaint?.mom) }}</span></div>
</span>
</div>
<div class="count_row"><span>已办结</span><span>{{ stats.entComplaint?.completed ?? '-' }}</span><span>{{ completedRate }}%</span></div>
<div class="count_row"><span>未办结</span><span>{{ stats.entComplaint?.unCompleted ?? '-' }}</span><span>{{ unCompletedRate }}%</span></div>
</div>
</div>
<div class="count_com count_rev">
<h4>评价</h4>
<div class="count_list">
<div class="count_row"><span>满意</span><span>{{ stats.eva?.high ?? '-' }}</span><span>{{ evaRates.high }}%</span></div>
<div class="count_row"><span>较满意</span><span>{{ stats.eva?.mid ?? '-' }}</span><span>{{ evaRates.mid }}%</span></div>
<div class="count_row"><span>不满意</span><span>{{ stats.eva?.low ?? '-' }}</span><span>{{ evaRates.low }}%</span></div>
<div class="count_row"><span>未评价</span><span>{{ stats.eva?.un ?? '-' }}</span><span>{{ evaRates.un }}%</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="echarts_mult">
<div class="data_cont threebox">
<h3 class="chart_tit">{{ enfEntSortTitle }}</h3>
<span class="sel_rank">
<select v-model="rankSizeEnt" @change="loadEnfEntSort">
<option :value="10">TOP 10</option>
<option :value="5">TOP 5</option>
</select>
</span>
<div ref="rankCompanyRef" class="chart_box"></div>
</div>
<div class="data_cont threebox">
<h3 class="chart_tit">{{ enforcementSortTitle }}</h3>
<span class="sel_rank">
<select v-model="rankSizeInspect" @change="loadEnforcementSort">
<option :value="10">TOP 10</option>
<option :value="5">TOP 5</option>
</select>
</span>
<div ref="rankInspectRef" class="chart_box"></div>
</div>
<div class="data_cont threebox">
<h3 class="chart_tit">{{ entCSortTitle }}</h3>
<span class="sel_rank">
<select v-model="rankSizeComplaint" @change="loadEntCSort">
<option :value="10">TOP 10</option>
<option :value="5">TOP 5</option>
</select>
</span>
<div ref="rankComplaintRef" class="chart_box"></div>
</div>
<div class="data_cont threebox">
<h3 class="chart_tit">委托执法数量及占比</h3>
<div ref="rankPieAutoRef" class="chart_box"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
import * as echarts from 'echarts';
import TopSixMetrics from '../components/TopSixMetrics.vue';
import { queryDept, queryDeptLength } from '@/api/supervision/statistical/statisticalPanel';
import {
collectStatistics,
queryAutoRate,
queryEnfEntSort,
queryEnforcementSort,
queryEntCSort,
queryLawEntSort,
queryUnionSort
} from '@/api/supervision/statistical/statisticalPanelCity';
import type { CollectStatisticsData, SysDeptVo } from '@/api/supervision/statistical/types';
import { normalizeRankList, pick, toNum, unwrapData } from '@/api/supervision/statistical/adapters';
import { buildAutoRatePieOption, buildRankBarOption, formatPercent } from '../utils/chartOptions';
async function safeLoad<T>(fn: () => Promise<T>): Promise<T | null> {
try {
return await fn();
} catch (e) {
console.warn('[市直综合分析] 接口加载失败', e);
return null;
}
}
const LAW_CATEGORY_ICONS: Record<string, string> = {
行政检查: 'icon_801.png', 行政处罚: 'icon_802.png', 行政许可: 'icon_803.png', 行政强制: 'icon_804.png',
行政征收: 'icon_805.png', 行政给付: 'icon_806.png', 行政确认: 'icon_807.png', 行政登记: 'icon_808.png',
行政裁决: 'icon_809.png', 文书送达: 'icon_8010.png'
};
const CASE_TYPE_ICONS: Record<string, string> = {
违规执法: 'icon_1001.png', 违法执法: 'icon_1002.png', 入企不扫码: 'icon_1004.png', 其他: 'icon_1003.png'
};
/* ======================== 开发调试用假数据 ======================== */
const MOCK_STATS: Partial<CollectStatisticsData> = {
singleLaw: { count: 542, yesterday: 12, thisMonth: 48, yoy: 3.25, mom: -1.5, completed: 0, unCompleted: 0 },
unionLaw: { count: 3, yesterday: 0, thisMonth: 1, yoy: -2.1, mom: 0.5, completed: 0, unCompleted: 0 },
enfEnt: { count: 485, yesterday: 15, thisMonth: 62, yoy: 5.8, mom: 2.3, completed: 0, unCompleted: 0 },
entEeduction: { count: 9, yesterday: 0, thisMonth: 1, yoy: -100, mom: 1.64, rate: '1.64%' },
entComplaint: { count: 128, yesterday: 3, thisMonth: 15, yoy: 12.5, mom: 3.8, completed: 96, unCompleted: 32 },
ent: { count: 69320, yesterday: 1, thisMonth: 18, yoy: 0, mom: 0, completed: 0, unCompleted: 0 },
lawCategoryTotal: 610,
lawCategory: [
{ 行政检查: 537 }, { 行政处罚: 11 }, { 行政许可: 0 }, { 行政强制: 0 },
{ 行政征收: 0 }, { 行政给付: 0 }, { 行政确认: 1 }, { 行政登记: 0 },
{ 行政裁决: 0 }, { 文书送达: 61 }
],
totalCaseType: 25,
caseType: [
{ 违规执法: 15 }, { 违法执法: 4 }, { 入企不扫码: 3 }, { 其他: 3 }
],
lawFieldTotal: 520,
lawField: [
{ 卫生健康: 128 }, { 应急管理: 89 }, { 生态环境: 67 }, { 市场监督管理资源: 145 }, { 规划和自然资源: 91 }
],
eva: { high: 85, mid: 28, low: 5, un: 10 }
};
const MOCK_ENF_ENT_SORT = [
{ enterprise_name: '深圳市某某科技有限公司', cnt: 48 },
{ enterprise_name: '某某集团股份有限公司', cnt: 35 },
{ enterprise_name: '深圳市某某医疗器械公司', cnt: 29 },
{ enterprise_name: '某某电子(深圳)有限公司', cnt: 25 },
{ enterprise_name: '深圳市某某食品有限公司', cnt: 22 },
{ enterprise_name: '某某环保科技深圳分公司', cnt: 18 },
{ enterprise_name: '深圳市某某化工有限公司', cnt: 15 },
{ enterprise_name: '某某光电股份有限公司', cnt: 12 },
{ enterprise_name: '深圳市某某物流有限公司', cnt: 10 },
{ enterprise_name: '某某商贸(深圳)有限公司', cnt: 8 }
];
const MOCK_ENFORCEMENT_SORT = [
{ bureau_dept_name: '市市场监督管理局', cnt: 185 },
{ bureau_dept_name: '市应急管理局', cnt: 142 },
{ bureau_dept_name: '市生态环境局', cnt: 98 },
{ bureau_dept_name: '市卫生健康委员会', cnt: 76 },
{ bureau_dept_name: '市交通运输局', cnt: 54 },
{ bureau_dept_name: '市住房和建设局', cnt: 43 },
{ bureau_dept_name: '市水务局', cnt: 32 },
{ bureau_dept_name: '市城市管理和综合执法局', cnt: 28 },
{ bureau_dept_name: '市商务局', cnt: 19 },
{ bureau_dept_name: '市发展和改革委员会', cnt: 12 }
];
const MOCK_ENT_C_SORT = [
{ bureau_dept_name: '市市场监督管理局', cnt: 32 },
{ bureau_dept_name: '市卫生健康委员会', cnt: 24 },
{ bureau_dept_name: '市应急管理局', cnt: 18 },
{ bureau_dept_name: '市生态环境局', cnt: 15 },
{ bureau_dept_name: '市交通运输局', cnt: 11 },
{ bureau_dept_name: '市住房和建设局', cnt: 8 },
{ bureau_dept_name: '市城市管理和综合执法局', cnt: 6 },
{ bureau_dept_name: '市水务局', cnt: 4 },
{ bureau_dept_name: '市商务局', cnt: 3 },
{ bureau_dept_name: '市人力资源和社会保障局', cnt: 2 }
];
const MOCK_AUTO_RATE = { autoCnt: 235, unAutoCnt: 375 };
const dateRange = ref<[string, string] | null>(null);
const deptPanelOpen = ref(false);
const deptList = ref<(SysDeptVo & { deptId: string | number | null })[]>([]);
const selectedDeptId = ref<string | number | null>(null);
const deptName = ref('全市直');
const stats = ref<Partial<CollectStatisticsData>>({});
const rankSizeEnt = ref(10);
const rankSizeInspect = ref(10);
const rankSizeComplaint = ref(10);
const rankCompanyRef = ref<HTMLElement>();
const rankInspectRef = ref<HTMLElement>();
const rankComplaintRef = ref<HTMLElement>();
const rankPieAutoRef = ref<HTMLElement>();
let rankCompanyChart: echarts.ECharts | null = null;
let rankInspectChart: echarts.ECharts | null = null;
let rankComplaintChart: echarts.ECharts | null = null;
let rankPieAutoChart: echarts.ECharts | null = null;
const isCityWide = computed(() => deptName.value === '全市直' || selectedDeptId.value === null);
const enfEntSortTitle = computed(() => '被执法企业次数排行');
const enforcementSortTitle = computed(() => (isCityWide.value ? '执法单位执法数量排行' : '联合检查单位排行'));
const entCSortTitle = computed(() => (isCityWide.value ? '被投诉单位次数排行' : '执法人员涉企检查数是排行'));
const completedRate = computed(() => {
const c = stats.value.entComplaint;
if (!c) return 0;
const total = (c.completed || 0) + (c.unCompleted || 0);
return total ? parseFloat(((c.completed / total) * 100).toFixed(2)) : 0;
});
const unCompletedRate = computed(() => {
const c = stats.value.entComplaint;
if (!c) return 0;
const total = (c.completed || 0) + (c.unCompleted || 0);
return total ? parseFloat(((c.unCompleted / total) * 100).toFixed(2)) : 0;
});
const evaRates = computed(() => {
const e = stats.value.eva;
if (!e) return { high: 0, mid: 0, low: 0, un: 0 };
const total = e.high + e.mid + e.low + e.un;
const pct = (v: number) => (total ? parseFloat(((v / total) * 100).toFixed(2)) : 0);
return { high: pct(e.high), mid: pct(e.mid), low: pct(e.low), un: pct(e.un) };
});
const BASE = import.meta.env.BASE_URL;
const cssVars = computed(() => {
const p = (name: string) => `url('${BASE}statistical-new/images/${name}')`;
return {
'--bg-body': p('bg_body.jpg'),
'--bg-data-item': p('bg_data_item.jpg'),
'--bg-analy-card': p('bg_analy_card.jpg'),
'--bg-analy-card-2': p('bg_analy_card_2.jpg'),
'--bg-ring': p('bg_ring.png'),
'--bg-base': p('bg_base.png'),
'--bg-shadow': p('bg_shadow.png'),
'--icon-scr': p('icon_scr.png'),
'--icon-com': p('icon_com.png'),
'--icon-rev': p('icon_rev.png'),
'--icon-dept32': p('icon_dept32.png'),
'--icon-dist32': p('icon_dist32.png')
} as Record<string, string>;
});
function buildCategoryItems(data: Record<string, number>[] | undefined, total: number | undefined, iconMap: Record<string, string>) {
if (!data) return [];
return data.map((obj) => {
const [label, count] = Object.entries(obj)[0];
const rate = total ? parseFloat(((Number(count) / total) * 100).toFixed(2)) : 0;
return { label, count, rate, icon: `${BASE}statistical-new/images/${iconMap[label] || 'icon_801.png'}` };
});
}
const lawCategoryItems = computed(() => buildCategoryItems(stats.value.lawCategory, stats.value.lawCategoryTotal, LAW_CATEGORY_ICONS));
const caseTypeItems = computed(() => buildCategoryItems(stats.value.caseType, stats.value.totalCaseType, CASE_TYPE_ICONS));
const lawFieldItems = computed(() => {
if (!stats.value.lawField) return [];
return stats.value.lawField.map((obj) => {
const [label, count] = Object.entries(obj)[0];
const rate = stats.value.lawFieldTotal ? parseFloat(((Number(count) / stats.value.lawFieldTotal!) * 100).toFixed(2)) : 0;
return { label, count, rate };
});
});
function getParams() {
return {
beginTime: dateRange.value?.[0],
endTime: dateRange.value?.[1],
zone: 1,
bureauDeptId: selectedDeptId.value
};
}
function fmt(val?: string | number) {
if (val === undefined || val === null) return '-%';
return formatPercent(val);
}
async function loadDepts() {
const lenRes = await safeLoad(() => queryDeptLength({ zoneList: [1, 15, 16] }));
const deptLen = unwrapData<SysDeptVo[]>(lenRes)?.length ?? 0;
const res = await safeLoad(() => queryDept({ zoneList: [1, 15, 16] }));
const datas = unwrapData<SysDeptVo[]>(res) || [];
const list: (SysDeptVo & { deptId: string | number | null })[] = [];
if (datas.length === deptLen && datas.length > 0) {
list.push({ deptId: null, deptName: '全市直' });
changeDept(null, '全市直');
} else if (datas.length === 0) {
list.push({ deptId: -1, deptName: '无权限' });
changeDept(-1, '无权限');
} else if (datas.length > 0) {
changeDept(datas[0].deptId, datas[0].deptName);
}
deptList.value = [...list, ...datas];
}
function changeDept(id: string | number | null, name: string) {
selectedDeptId.value = id;
deptName.value = name;
deptPanelOpen.value = false;
refreshAll();
}
async function loadStatistics() {
const res = await safeLoad(() => collectStatistics(getParams()));
stats.value = (res ? unwrapData(res) : MOCK_STATS) as Partial<CollectStatisticsData>;
}
function renderRankChart(
el: HTMLElement | undefined,
chart: echarts.ECharts | null,
data: ReturnType<typeof normalizeRankList>,
nameKey: 'enterprise_name' | 'bureau_dept_name',
seriesName: string
) {
if (!el || !data.length) return chart;
const instance = chart ?? echarts.init(el);
const names = data.map((d) => d[nameKey] || '');
const values = data.map((d) => toNum(d.cnt));
instance.setOption(buildRankBarOption(names, values, seriesName), true);
return instance;
}
async function loadEnfEntSort() {
const size = rankSizeEnt.value;
const res = await safeLoad(() => queryEnfEntSort({ ...getParams(), pageSize: size }));
const list = res ? unwrapData(res) : MOCK_ENF_ENT_SORT.slice(0, size);
await nextTick();
rankCompanyChart = renderRankChart(rankCompanyRef.value, rankCompanyChart, normalizeRankList(list), 'enterprise_name', '被执法企业次数')!;
}
async function loadEnforcementSort() {
const size = rankSizeInspect.value;
const res = await safeLoad(() =>
isCityWide.value
? queryEnforcementSort({ ...getParams(), pageSize: size })
: queryUnionSort({ ...getParams(), pageSize: size })
);
const list = res ? unwrapData(res) : MOCK_ENFORCEMENT_SORT.slice(0, size);
await nextTick();
rankInspectChart = renderRankChart(rankInspectRef.value, rankInspectChart, normalizeRankList(list), 'bureau_dept_name', '执法数量')!;
}
async function loadEntCSort() {
const size = rankSizeComplaint.value;
const res = await safeLoad(() =>
isCityWide.value
? queryEntCSort({ ...getParams(), pageSize: size })
: queryLawEntSort({ ...getParams(), pageSize: size })
);
const list = res ? unwrapData(res) : MOCK_ENT_C_SORT.slice(0, size);
await nextTick();
const data = normalizeRankList(list);
// rankComplaint uses 'complaint_departments' field, others use 'bureau_dept_name'
const nameKey = isCityWide.value ? 'complaint_departments' as const : 'bureau_dept_name' as const;
rankComplaintChart = renderRankChart(rankComplaintRef.value, rankComplaintChart, data, nameKey, '被投诉单位次数')!;
}
async function loadAutoRate() {
const res = await safeLoad(() => queryAutoRate(getParams()));
const data = (res ? unwrapData(res) : MOCK_AUTO_RATE) as Record<string, unknown>;
await nextTick();
if (rankPieAutoRef.value) {
rankPieAutoChart ??= echarts.init(rankPieAutoRef.value);
rankPieAutoChart.setOption(
buildAutoRatePieOption([
{ value: toNum(pick(data, 'autoCnt', 'auto_cnt')), name: '委托执法' },
{ value: toNum(pick(data, 'unAutoCnt', 'un_auto_cnt')), name: '非委托执法' }
]),
true
);
}
}
async function refreshAll() {
await loadStatistics();
await Promise.allSettled([loadEnfEntSort(), loadEnforcementSort(), loadEntCSort(), loadAutoRate()]);
}
function handleResize() {
rankCompanyChart?.resize();
rankInspectChart?.resize();
rankComplaintChart?.resize();
rankPieAutoChart?.resize();
}
onMounted(async () => {
await loadDepts();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
rankCompanyChart?.dispose();
rankInspectChart?.dispose();
rankComplaintChart?.dispose();
rankPieAutoChart?.dispose();
});
</script>
<style>
/* 使用带 .statistical-analysis 根容器限定的样式,防止污染首页 */
@import '../styles/analysis-scope.css';
@import '/statistical-new/font-awesome/css/font-awesome.min.css';
@import '../styles/analysis-layout.css';
</style>