|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
|
|
|
|
|
import { getDicts } from '@/api/system/dict/data';
|
|
|
|
|
|
|
|
|
|
/** 表格行数据 */
|
|
|
|
|
interface DistrictRow {
|
|
|
|
|
id: number;
|
|
|
|
|
areaName: 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 timePeriodText = computed(() => {
|
|
|
|
|
if (timeMode.value === 'year') {
|
|
|
|
|
return `${selectedYear.value}年`;
|
|
|
|
|
} else if (timeMode.value === 'quarter') {
|
|
|
|
|
return `${selectedYear.value}年第${selectedQuarter.value}季度`;
|
|
|
|
|
} else if (timeMode.value === 'month') {
|
|
|
|
|
return selectedMonth.value || '全部时段';
|
|
|
|
|
}
|
|
|
|
|
return '全部时段';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ======================== 所属区字典 ========================
|
|
|
|
|
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 getList = async () => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
// TODO: 接入真实接口 listDistrictScanData
|
|
|
|
|
} 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 = () => {
|
|
|
|
|
// TODO: 接入导出接口
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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 ScanDetailRow {
|
|
|
|
|
id: number;
|
|
|
|
|
enterpriseName: string;
|
|
|
|
|
scanTime: string;
|
|
|
|
|
scanType: string;
|
|
|
|
|
bureauDeptName: string;
|
|
|
|
|
lawEnforcer: string;
|
|
|
|
|
enforcementItem: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const detailDialogVisible = ref(false);
|
|
|
|
|
const detailArea = ref<DistrictRow | null>(null);
|
|
|
|
|
const detailLoading = ref(false);
|
|
|
|
|
const detailTableData = ref<ScanDetailRow[]>([]);
|
|
|
|
|
const detailTotal = ref(0);
|
|
|
|
|
const detailPage = ref(1);
|
|
|
|
|
const detailPageSize = ref(20);
|
|
|
|
|
|
|
|
|
|
const getDetailList = async () => {
|
|
|
|
|
detailLoading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
if (!detailArea.value) return;
|
|
|
|
|
// TODO: 接入真实接口 listDistrictScanDetail
|
|
|
|
|
} finally {
|
|
|
|
|
detailLoading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDetail = (row: DistrictRow) => {
|
|
|
|
|
detailArea.value = row;
|
|
|
|
|
detailPage.value = 1;
|
|
|
|
|
detailDialogVisible.value = true;
|
|
|
|
|
getDetailList();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDetailSizeChange = (size: number) => {
|
|
|
|
|
detailPageSize.value = size;
|
|
|
|
|
detailPage.value = 1;
|
|
|
|
|
getDetailList();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDetailPageChange = (page: number) => {
|
|
|
|
|
detailPage.value = page;
|
|
|
|
|
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="areaName" label="所属区" width="120" align="center" />
|
|
|
|
|
<el-table-column prop="enterpriseCount" label="商事主体数量(户)" min-width="150" align="center" />
|
|
|
|
|
<el-table-column prop="adminEnforceCount" label="行政执法数量(条)" min-width="150" align="center" />
|
|
|
|
|
<el-table-column prop="adminRank" label="行政执法数量排名" min-width="150" align="center" />
|
|
|
|
|
<el-table-column prop="adminPercent" label="行政执法数量占比(%)" min-width="170" align="center" />
|
|
|
|
|
<el-table-column prop="nonAdminEnforceCount" label="非行政执法数量" min-width="140" align="center" />
|
|
|
|
|
<el-table-column prop="nonAdminRank" label="非行政执法数量排名" min-width="160" align="center" />
|
|
|
|
|
<el-table-column prop="nonAdminPercent" 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="1100px"
|
|
|
|
|
:title="`${detailArea?.areaName || ''} — ${timePeriodText} 扫码明细`"
|
|
|
|
|
append-to-body
|
|
|
|
|
top="5vh"
|
|
|
|
|
destroy-on-close
|
|
|
|
|
>
|
|
|
|
|
<el-alert
|
|
|
|
|
v-if="detailArea"
|
|
|
|
|
:closable="false"
|
|
|
|
|
show-icon
|
|
|
|
|
:title="`商事主体:${detailArea.enterpriseCount}户 | 行政执法:${detailArea.adminEnforceCount}条 | 非行政执法:${detailArea.nonAdminEnforceCount}条 | 时段:${timePeriodText}`"
|
|
|
|
|
type="info"
|
|
|
|
|
class="mb-4"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<el-table
|
|
|
|
|
v-loading="detailLoading"
|
|
|
|
|
:data="detailTableData"
|
|
|
|
|
border
|
|
|
|
|
stripe
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
max-height="55vh"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column label="序号" type="index" width="80" align="center" />
|
|
|
|
|
<el-table-column label="企业名称" prop="enterpriseName" min-width="220" align="center" />
|
|
|
|
|
<el-table-column label="扫码时间" prop="scanTime" min-width="180" align="center" />
|
|
|
|
|
<el-table-column label="扫码类型" prop="scanType" width="120" align="center" />
|
|
|
|
|
<el-table-column label="执法单位" prop="bureauDeptName" min-width="220" align="center" />
|
|
|
|
|
<el-table-column label="执法人员" prop="lawEnforcer" width="120" align="center" />
|
|
|
|
|
<el-table-column label="执法事项" prop="enforcementItem" min-width="200" align="center" />
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
v-model:current-page="detailPage"
|
|
|
|
|
v-model:page-size="detailPageSize"
|
|
|
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
:total="detailTotal"
|
|
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
class="mt-4"
|
|
|
|
|
@size-change="handleDetailSizeChange"
|
|
|
|
|
@current-change="handleDetailPageChange"
|
|
|
|
|
/>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.app-container {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.page-card {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
.mb-4 {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|