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.
468 lines
19 KiB
468 lines
19 KiB
<template>
|
|
<div class="statistical-analysis statistical-analysis--adaptive" :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" :class="{ active: datePickerOpen }" @click="datePickerOpen = !datePickerOpen">时间筛选<a></a></label>
|
|
<el-date-picker
|
|
v-if="datePickerOpen"
|
|
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 ? item.count : '-' }}</span>
|
|
<span class="number_percent">{{ item.rate ? 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 ? item.count : '-' }}</span>
|
|
<span class="number_percent">{{ item.rate ? 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 ? item.count : '-' }}</span>
|
|
<span class="number_percent">{{ item.rate ? 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 dateRange = ref<[string, string] | null>(null);
|
|
const datePickerOpen = ref(false);
|
|
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, rawCount] = Object.entries(obj)[0];
|
|
// 接口可能返回字符串 "0",统一转成数字,0 才会被模板判定为空显示 '-'
|
|
const count = Number(rawCount) || 0;
|
|
const rate = total ? parseFloat(((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, rawCount] = Object.entries(obj)[0];
|
|
const count = Number(rawCount) || 0;
|
|
const rate = stats.value.lawFieldTotal ? parseFloat(((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()));
|
|
if (!res) return;
|
|
stats.value = unwrapData(res) 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) return chart;
|
|
const instance = chart ?? echarts.init(el);
|
|
if (!data.length) {
|
|
instance.clear();
|
|
return instance;
|
|
}
|
|
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 res = await safeLoad(() => queryEnfEntSort({ ...getParams(), pageSize: rankSizeEnt.value }));
|
|
const list = res ? unwrapData(res) : [];
|
|
await nextTick();
|
|
rankCompanyChart = renderRankChart(rankCompanyRef.value, rankCompanyChart, normalizeRankList(list), 'enterprise_name', '被执法企业次数')!;
|
|
}
|
|
|
|
async function loadEnforcementSort() {
|
|
const res = await safeLoad(() =>
|
|
isCityWide.value
|
|
? queryEnforcementSort({ ...getParams(), pageSize: rankSizeInspect.value })
|
|
: queryUnionSort({ ...getParams(), pageSize: rankSizeInspect.value })
|
|
);
|
|
const list = res ? unwrapData(res) : [];
|
|
await nextTick();
|
|
rankInspectChart = renderRankChart(rankInspectRef.value, rankInspectChart, normalizeRankList(list), 'bureau_dept_name', '执法数量')!;
|
|
}
|
|
|
|
async function loadEntCSort() {
|
|
const res = await safeLoad(() =>
|
|
isCityWide.value
|
|
? queryEntCSort({ ...getParams(), pageSize: rankSizeComplaint.value })
|
|
: queryLawEntSort({ ...getParams(), pageSize: rankSizeComplaint.value })
|
|
);
|
|
const list = res ? unwrapData(res) : [];
|
|
await nextTick();
|
|
const data = normalizeRankList(list);
|
|
rankComplaintChart = renderRankChart(rankComplaintRef.value, rankComplaintChart, data, 'bureau_dept_name', '被投诉单位次数')!;
|
|
}
|
|
|
|
async function loadAutoRate() {
|
|
const res = await safeLoad(() => queryAutoRate(getParams()));
|
|
if (!res) return;
|
|
const data = unwrapData(res) 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';
|
|
|
|
/* 卡片高度随内容自适应,避免条目增多时溢出裁切;同行卡片等高 */
|
|
.statistical-analysis--adaptive .twobox {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.statistical-analysis--adaptive .twobox .threebox {
|
|
float: none;
|
|
height: auto;
|
|
/* min-height: 25rem; */
|
|
/* 等高时多出的高度留在底部,内容保持顶部对齐,不被分配到行间 */
|
|
align-content: flex-start;
|
|
}
|
|
|
|
/* 标题不再被第一行卡片压住(原 -2% / -0.5% 负外边距会让卡片上移盖住标题) */
|
|
.statistical-analysis--adaptive .twobox .chart_tit {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.statistical-analysis--adaptive .twobox .count_wrap {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.statistical-analysis--adaptive .twobox .count_item {
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* 投诉类别条目之间留出间隙 */
|
|
.statistical-analysis--adaptive .twobox .com_cate {
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.statistical-analysis--adaptive .twobox .com_cate .count_item {
|
|
margin: 0;
|
|
width: calc(50% - 0.375rem);
|
|
}
|
|
</style>
|
|
|