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.

391 lines
13 KiB

4 weeks ago
<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
});
const periodType = ref('month');
const periodOptions = [
{ label: '月度', value: 'month' }
];
const periodDate = ref<string>('2026-09');
const pickerType = computed<'week' | 'month' | 'year'>(() => 'month');
const pickerFormat = computed(() => 'YYYY-MM');
// ======================== 所属区字典 ========================
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;
};
/** 构造图片中的 mock 数据 */
const buildMockRows = (): DistrictRow[] => {
return [
{ id: 1, areaName: '宝安区', enterpriseCount: 584119, adminEnforceCount: 14421, adminRank: 1, adminPercent: '60%', nonAdminEnforceCount: 9981, nonAdminRank: 1, nonAdminPercent: '60%' },
{ id: 2, areaName: '龙岗区', enterpriseCount: 966252, adminEnforceCount: 11415, adminRank: 2, adminPercent: '56.64%', nonAdminEnforceCount: 184, nonAdminRank: 2, nonAdminPercent: '56.64%' },
{ id: 3, areaName: '福田区', enterpriseCount: 615441, adminEnforceCount: 6135, adminRank: 4, adminPercent: '70%', nonAdminEnforceCount: 137, nonAdminRank: 4, nonAdminPercent: '70%' },
{ id: 4, areaName: '南山区', enterpriseCount: 601902, adminEnforceCount: 5424, adminRank: 5, adminPercent: '80%', nonAdminEnforceCount: 389, nonAdminRank: 5, nonAdminPercent: '80%' },
{ id: 5, areaName: '龙华区', enterpriseCount: 572932, adminEnforceCount: 4245, adminRank: 8, adminPercent: '67.5%', nonAdminEnforceCount: 22017, nonAdminRank: 8, nonAdminPercent: '67.5%' },
{ id: 6, areaName: '罗湖区', enterpriseCount: 311483, adminEnforceCount: 5064, adminRank: 6, adminPercent: '63.2%', nonAdminEnforceCount: 888, nonAdminRank: 6, nonAdminPercent: '63.2%' },
{ id: 7, areaName: '光明区', enterpriseCount: 192114, adminEnforceCount: 7040, adminRank: 3, adminPercent: '78.34%', nonAdminEnforceCount: 50384, nonAdminRank: 3, nonAdminPercent: '78.34%' },
{ id: 8, areaName: '坪山区', enterpriseCount: 22103, adminEnforceCount: 3200, adminRank: 7, adminPercent: '55%', nonAdminEnforceCount: 1520, nonAdminRank: 7, nonAdminPercent: '55%' },
{ id: 9, areaName: '大鹏新区', enterpriseCount: 11205, adminEnforceCount: 1860, adminRank: 9, adminPercent: '48%', nonAdminEnforceCount: 980, nonAdminRank: 9, nonAdminPercent: '48%' },
{ id: 10, areaName: '盐田区', enterpriseCount: 13900, adminEnforceCount: 2150, adminRank: 10, adminPercent: '52%', nonAdminEnforceCount: 1050, nonAdminRank: 10, nonAdminPercent: '52%' }
];
};
const getList = async () => {
loading.value = true;
try {
// TODO: 接入真实接口 listDistrictScanData
tableData.value = buildMockRows();
total.value = 658;
} finally {
loading.value = false;
}
};
const handleSearch = () => {
currentPage.value = 1;
getList();
};
const handleReset = () => {
queryParams.area = undefined;
periodType.value = 'month';
periodDate.value = '2026-09';
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 buildDetailMockRows = (area: DistrictRow, count: number): ScanDetailRow[] => {
const rows: ScanDetailRow[] = [];
const enforcerPool = ['张三', '李四', '王五', '赵六', '钱七', '孙八'];
const bureauPool = [`${area.areaName}市场监督管理局`, `${area.areaName}市场监管局执法大队`];
const scanTypePool = ['行政执法', '非行政执法'];
const enterprisePool = ['深圳市某某科技有限公司', '深圳市某某电子厂', '深圳市某某贸易有限公司'];
for (let i = 1; i <= Math.min(count, 12); i++) {
const hour = String(8 + (i % 11)).padStart(2, '0');
const minute = String((i * 17) % 60).padStart(2, '0');
rows.push({
id: i,
enterpriseName: enterprisePool[i % enterprisePool.length],
scanTime: `${periodDate.value}-${String(i % 28 + 1).padStart(2, '0')} ${hour}:${minute}:00`,
scanType: scanTypePool[i % scanTypePool.length],
bureauDeptName: bureauPool[i % bureauPool.length],
lawEnforcer: enforcerPool[i % enforcerPool.length],
enforcementItem: `${area.areaName}相关检查`
});
}
return rows;
};
const getDetailList = async () => {
detailLoading.value = true;
try {
if (!detailArea.value) return;
// TODO: 接入真实接口 listDistrictScanDetail
detailTableData.value = buildDetailMockRows(detailArea.value, detailArea.value.adminEnforceCount);
detailTotal.value = detailArea.value.adminEnforceCount + detailArea.value.nonAdminEnforceCount;
} 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();
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="periodType" class="period-select">
<el-option v-for="item in periodOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-date-picker
v-model="periodDate"
:type="pickerType"
:format="pickerFormat"
:value-format="pickerFormat"
class="period-date"
/>
</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 || ''} — ${periodDate} 扫码明细`"
append-to-body
top="5vh"
destroy-on-close
>
<el-alert
v-if="detailArea"
:closable="false"
show-icon
:title="`商事主体:${detailArea.enterpriseCount}户 | 行政执法:${detailArea.adminEnforceCount}条 | 非行政执法:${detailArea.nonAdminEnforceCount}条 | 时段:${periodDate}`"
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;
}
.period-select {
width: 110px;
}
.period-date {
width: 150px;
}
}
.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>