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.
 
 
 
 
 
 

480 lines
16 KiB

<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { getDicts } from '@/api/system/dict/data';
import { listScanArea, listScanAreaDetails, exportScanArea, type ScanTimeParams } from '@/api/efcode/report/scan-data-situation';
/** 表格行数据 */
interface DistrictRow {
id: number;
areaName: string;
districtCode?: string;
districtName?: string;
enterpriseCount: number;
adminEnforceCount: number;
adminRank: number;
adminPercent: string;
nonAdminEnforceCount: number;
nonAdminRank: number;
nonAdminPercent: string;
}
// ======================== 查询条件 ========================
const queryParams = reactive({
area: undefined as string | undefined,
dateType: undefined as string | undefined,
dateSearch: undefined as string | undefined,
beginTime: undefined as string | undefined,
endTime: undefined as string | undefined
});
// 时段模式:year | quarter | month | custom
const timeMode = ref('year');
const selectedYear = ref<string>(String(new Date().getFullYear()));
const selectedQuarter = ref<number>(Math.ceil((new Date().getMonth() + 1) / 3));
const selectedMonth = ref<string>('');
// 季度选项
const quarterOptions = [
{ label: '第一季度', value: 1 },
{ label: '第二季度', value: 2 },
{ label: '第三季度', value: 3 },
{ label: '第四季度', value: 4 }
];
// 年份选项(近几年)
const yearOptions = (() => {
const curr = new Date().getFullYear();
return Array.from({ length: 5 }, (_, i) => String(curr - i));
})();
// ======================== 时段联动(对齐 enterprise-enforcement) ========================
/** 时段模式变更 */
const handleTimeModeChange = (mode: string) => {
queryParams.dateType = undefined;
queryParams.dateSearch = undefined;
queryParams.beginTime = undefined;
queryParams.endTime = undefined;
if (mode === 'year') {
applyYearFilter();
} else if (mode === 'quarter') {
applyQuarterFilter();
} else if (mode === 'month') {
// 默认选当前月
const now = new Date();
selectedMonth.value = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
applyMonthFilter();
}
// custom 模式等用户手动选择时间范围
};
/** 年度筛选 */
const applyYearFilter = () => {
const year = selectedYear.value;
queryParams.beginTime = `${year}-01-01`;
queryParams.endTime = `${year}-12-31`;
};
/** 季度筛选 */
const applyQuarterFilter = () => {
const year = selectedYear.value;
const q = selectedQuarter.value;
const startMonth = (q - 1) * 3 + 1;
const endMonth = q * 3;
const endDay = new Date(Number(year), endMonth, 0).getDate();
queryParams.beginTime = `${year}-${String(startMonth).padStart(2, '0')}-01`;
queryParams.endTime = `${year}-${String(endMonth).padStart(2, '0')}-${endDay}`;
};
/** 月度筛选 */
const applyMonthFilter = () => {
if (selectedMonth.value) {
queryParams.dateSearch = selectedMonth.value;
}
};
/** 年份变更 */
const handleYearChange = () => {
if (timeMode.value === 'year') {
applyYearFilter();
} else if (timeMode.value === 'quarter') {
applyQuarterFilter();
}
};
/** 季度变更 */
const handleQuarterChange = () => {
applyQuarterFilter();
};
/** 月份变更 */
const handleMonthChange = (val: string) => {
selectedMonth.value = val;
queryParams.dateSearch = val;
};
// ======================== 所属区字典 ========================
const areaOptions = ref<any[]>([]);
const loadAreaDict = async () => {
try {
const res = await getDicts('address_type');
areaOptions.value = res.data || [];
} catch (e) {
console.error('加载所属区字典失败', e);
}
};
// ======================== 表格数据 ========================
const loading = ref(false);
const tableData = ref<DistrictRow[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const selectedRows = ref<DistrictRow[]>([]);
const tableHeight = ref<number | undefined>(undefined);
const tableRef = ref<HTMLElement>();
const calcTableHeight = () => {
if (!tableRef.value) return;
const rect = tableRef.value.getBoundingClientRect();
tableHeight.value = window.innerHeight - rect.top - 84;
};
/** 按当前时段模式拼装接口时间参数 */
const buildTimeParams = (): ScanTimeParams => {
if (timeMode.value === 'year') {
return { year: selectedYear.value };
}
if (timeMode.value === 'quarter') {
return { year: selectedYear.value, quarter: String(selectedQuarter.value) };
}
if (timeMode.value === 'month') {
if (!selectedMonth.value) return {};
const [y, m] = selectedMonth.value.split('-');
return { year: y, month: m };
}
return { startTime: queryParams.beginTime, endTime: queryParams.endTime };
};
/** 当前选中的所属区(dictLabel→districtName,不再传 districtCode) */
const currentDistrict = () => {
const item = areaOptions.value.find((i: any) => i.dictValue === queryParams.area);
return { districtName: item?.dictLabel };
};
const getList = async () => {
loading.value = true;
try {
const res = await listScanArea({
...buildTimeParams(),
pageNum: currentPage.value,
pageSize: pageSize.value,
...currentDistrict()
});
tableData.value = (res as any)?.rows || [];
total.value = (res as any)?.total || 0;
} catch (e) {
console.warn('[scan-district] 列表接口调用失败', e);
} finally {
loading.value = false;
}
};
const handleSearch = () => {
currentPage.value = 1;
getList();
};
const handleReset = () => {
queryParams.area = undefined;
queryParams.dateType = undefined;
queryParams.dateSearch = undefined;
queryParams.beginTime = undefined;
queryParams.endTime = undefined;
timeMode.value = 'year';
selectedYear.value = String(new Date().getFullYear());
selectedQuarter.value = Math.ceil((new Date().getMonth() + 1) / 3);
selectedMonth.value = '';
applyYearFilter();
currentPage.value = 1;
pageSize.value = 20;
getList();
};
const handleExport = async () => {
try {
const res = await exportScanArea({
...buildTimeParams(),
...currentDistrict()
});
const blob = new Blob([res as any], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = '扫码情况区域.xlsx';
link.click();
window.URL.revokeObjectURL(url);
} catch (e) {
ElMessage.error('导出失败');
}
};
const handleSelectionChange = (selection: DistrictRow[]) => {
selectedRows.value = selection;
};
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
getList();
};
const handlePageChange = (page: number) => {
currentPage.value = page;
getList();
};
// ======================== 详情弹窗(区 → 街道) ========================
interface StreetRow {
id?: number | string;
streetName?: string;
districtName?: string;
enterpriseCount?: number | null;
xzzfCount?: number | null;
xzzfslpm?: number | string | null;
xzzfslzb?: number | string | null;
fxzzfsl?: number | null;
fxzzfslpm?: number | string | null;
fxzzfslzb?: number | string | null;
}
const detailDialogVisible = ref(false);
const detailArea = ref<DistrictRow | null>(null);
const detailLoading = ref(false);
const detailTableData = ref<StreetRow[]>([]);
const getDetailList = async () => {
detailLoading.value = true;
try {
if (!detailArea.value) return;
const res = await listScanAreaDetails({
...buildTimeParams(),
districtName: detailArea.value.districtName || detailArea.value.areaName || currentDistrict().districtName
});
detailTableData.value = (res as any)?.rows || (Array.isArray(res) ? res : []);
} catch (e) {
console.warn('[scan-district] 详情接口调用失败', e);
} finally {
detailLoading.value = false;
}
};
const handleDetail = (row: DistrictRow) => {
detailArea.value = row;
detailDialogVisible.value = true;
getDetailList();
};
onMounted(() => {
loadAreaDict();
applyYearFilter();
getList();
nextTick(() => calcTableHeight());
window.addEventListener('resize', calcTableHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', calcTableHeight);
});
</script>
<template>
<div class="app-container">
<div class="page-card">
<!-- ==================== 搜索区域 ==================== -->
<div class="search-panel">
<el-form :inline="true" label-colon>
<el-form-item label="所属区">
<el-select
v-model="queryParams.area"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
</el-select>
</el-form-item>
<el-form-item label="时段">
<el-select v-model="timeMode" style="width: 100px" @change="handleTimeModeChange">
<el-option label="年度" value="year" />
<el-option label="季度" value="quarter" />
<el-option label="月度" value="month" />
<el-option label="自定义" value="custom" />
</el-select>
</el-form-item>
<el-form-item v-if="timeMode === 'year'">
<el-select v-model="selectedYear" style="width: 100px" @change="handleYearChange">
<el-option v-for="y in yearOptions" :key="y" :label="y + '年'" :value="y" />
</el-select>
</el-form-item>
<el-form-item v-if="timeMode === 'quarter'">
<el-select v-model="selectedYear" style="width: 100px" @change="handleYearChange">
<el-option v-for="y in yearOptions" :key="y" :label="y + '年'" :value="y" />
</el-select>
<el-select v-model="selectedQuarter" style="width: 110px; margin-left: 4px" @change="handleQuarterChange">
<el-option v-for="q in quarterOptions" :key="q.value" :label="q.label" :value="q.value" />
</el-select>
</el-form-item>
<el-form-item v-if="timeMode === 'month'">
<el-date-picker v-model="selectedMonth" type="month" placeholder="选择月份" value-format="YYYY-MM" style="width: 150px" @change="handleMonthChange" />
</el-form-item>
<el-form-item v-if="timeMode === 'custom'" label="执法时间">
<el-date-picker v-model="queryParams.beginTime" type="date" placeholder="开始" value-format="YYYY-MM-DD" style="width: 130px" />
<span style="margin: 0 4px">-</span>
<el-date-picker v-model="queryParams.endTime" type="date" placeholder="结束" value-format="YYYY-MM-DD" style="width: 130px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button type="primary" @click="handleReset">重置</el-button>
<el-button @click="handleExport">导出</el-button>
</el-form-item>
</el-form>
</div>
<!-- ==================== 表格区域 ==================== -->
<div class="table-panel">
<div ref="tableRef">
<el-table
v-loading="loading"
:data="tableData"
border
stripe
style="width: 100%"
:height="tableHeight"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" width="80" align="center" type="index" />
<el-table-column prop="districtName" label="所属区" width="120" align="center" />
<el-table-column prop="enterpriseCount" label="商事主体数量(户)" min-width="150" align="center" />
<el-table-column prop="xzzfCount" label="行政执法数量(条)" min-width="150" align="center" />
<el-table-column prop="xzzfslpm" label="行政执法数量排名" min-width="150" align="center" />
<el-table-column prop="xzzfslzb" label="行政执法数量占比(%)" min-width="170" align="center" />
<el-table-column prop="fxzzfsl" label="非行政执法数量" min-width="140" align="center" />
<el-table-column prop="fxzzfslpm" label="非行政执法数量排名" min-width="160" align="center" />
<el-table-column prop="fxzzfslzb" label="非行政执法数量占比(%)" min-width="180" align="center" />
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
class="mt-4"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
<!-- ==================== 区 → 街道详情弹窗 ==================== -->
<el-dialog
v-model="detailDialogVisible"
width="1400px"
:title="detailArea?.districtName || detailArea?.areaName || ''"
append-to-body
top="5vh"
destroy-on-close
>
<el-table
v-loading="detailLoading"
:data="detailTableData"
border
stripe
style="width: 100%"
max-height="55vh"
>
<el-table-column label="序号" type="index" width="70" align="center" />
<el-table-column label="所属街道" min-width="160" align="center">
<template #default="{ row }">{{ row.streetName || row.districtName || '-' }}</template>
</el-table-column>
<el-table-column label="商事主体数量(户)" min-width="160" align="center">
<template #default="{ row }">{{ row.enterpriseCount ?? '-' }}</template>
</el-table-column>
<el-table-column label="行政执法数量(条)" min-width="170" align="center">
<template #default="{ row }">{{ row.xzzfCount ?? '-' }}</template>
</el-table-column>
<el-table-column label="行政执法数量排名" min-width="170" align="center">
<template #default="{ row }">{{ row.xzzfslpm ?? '-' }}</template>
</el-table-column>
<el-table-column label="行政执法数量占比(%)" min-width="160" align="center">
<template #default="{ row }">{{ row.xzzfslzb ?? '-' }}</template>
</el-table-column>
<el-table-column label="非行政执法数量(条)" min-width="180" align="center">
<template #default="{ row }">{{ row.fxzzfsl ?? '-' }}</template>
</el-table-column>
<el-table-column label="非行政执法数量排名" min-width="180" align="center">
<template #default="{ row }">{{ row.fxzzfslpm ?? '-' }}</template>
</el-table-column>
<el-table-column label="非行政执法数量占比(%)" min-width="180" align="center">
<template #default="{ row }">{{ row.fxzzfslzb ?? '-' }}</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.app-container {
padding: 16px;
box-sizing: border-box;
}
.page-card {
background: #fff;
border-radius: 4px;
box-sizing: border-box;
min-height: calc(100vh - 116px);
}
.search-panel {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 18px 20px 0;
margin-bottom: 16px;
:deep(.el-form-item__label) {
font-weight: 600;
color: #333;
}
.search-select {
width: 220px;
}
}
.table-panel {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 16px 20px;
box-sizing: border-box;
}
.mt-4 {
margin-top: 16px;
justify-content: flex-end;
}
</style>