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.
 
 
 
 
 
 

437 lines
13 KiB

<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { getDicts } from '@/api/system/dict/data';
import { listKeyEnterprises, listKeyEnterpriseDetails, exportKeyEnterprises } from '@/api/efcode/report/key-enterprises';
/** 企业行数据 */
interface EnterpriseRow {
id: number | string;
enterpriseNumber: string;
enterpriseName: string;
districtName: string;
streetName: string;
xzzfzs: number | null;
enterpriseId: number | string;
}
// ======================== 查询条件 ========================
const now = new Date();
const queryParams = reactive({
pageNum: 1,
pageSize: 20,
enterpriseName: '' as string,
districtName: undefined as string | undefined,
streetName: undefined as string | undefined,
statYear: String(now.getFullYear()),
statMonth: `${String(now.getFullYear())}-${String(now.getMonth() + 1).padStart(2, '0')}`
});
/** 年份选项(近 5 年) */
const yearOptions = (() => {
const curr = now.getFullYear();
return Array.from({ length: 5 }, (_, i) => String(curr - i));
})();
// ======================== 区域/街道字典 ========================
const areaOptions = ref<any[]>([]);
const streetOptions = ref<any[]>([]);
const loadAreaDict = async () => {
try {
const res = await getDicts('address_type');
areaOptions.value = res.data || [];
} catch (e) {
console.error('加载所属区字典失败', e);
}
};
/** 切换所属区时联动加载所属街道(选中值是 dictLabel,需取对应 dictValue 拉子级字典) */
const handleAreaChange = async (val: string) => {
queryParams.streetName = undefined;
streetOptions.value = [];
if (!val) return;
const dictValue = areaOptions.value.find((item) => item.dictLabel === val)?.dictValue;
if (!dictValue) return;
try {
const res = await getDicts(dictValue);
streetOptions.value = res.data || [];
} catch (e) {
console.error('加载所属街道字典失败', e);
}
};
// ======================== 表格数据 ========================
const loading = ref(false);
const tableData = ref<EnterpriseRow[]>([]);
const total = ref(0);
const selectedRows = ref<EnterpriseRow[]>([]);
/** 表格固定高度 */
const tableHeight = 600;
/** 月份选择器(YYYY-MM)→ 月号,如 "2026-10" → "10" */
const statMonthValue = () => {
const part = queryParams.statMonth ? queryParams.statMonth.split('-')[1] : undefined;
return part ? String(Number(part)) : undefined;
};
const getList = async () => {
loading.value = true;
try {
// 年份取「年份」下拉,月份取「月份」选择器并只传月号(不带年份)
const res = await listKeyEnterprises({
...queryParams,
statMonth: statMonthValue()
});
tableData.value = (res.rows || []) as EnterpriseRow[];
total.value = res.total || 0;
} finally {
loading.value = false;
}
};
/** 搜索 */
const handleSearch = () => {
queryParams.pageNum = 1;
getList();
};
/** 重置 */
const handleReset = () => {
queryParams.enterpriseName = '';
queryParams.districtName = undefined;
queryParams.streetName = undefined;
streetOptions.value = [];
queryParams.statYear = String(new Date().getFullYear());
queryParams.statMonth = `${String(new Date().getFullYear())}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
queryParams.pageNum = 1;
queryParams.pageSize = 20;
getList();
};
/** 导出 — 勾选企业时按其 enterpriseNumber 导出(多选用逗号分隔),未勾选则导出全部;默认带当前年月 */
const handleExport = async () => {
try {
const numbers = selectedRows.value
.map((r) => r.enterpriseNumber)
.filter(Boolean)
.join(',');
const res = await exportKeyEnterprises({
enterpriseNumber: numbers || undefined,
enterpriseName: queryParams.enterpriseName || undefined,
districtName: queryParams.districtName,
streetName: queryParams.streetName,
statYear: queryParams.statYear,
statMonth: statMonthValue()
});
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('导出失败');
}
};
// ======================== 详情弹窗 ========================
interface EnforcementDetailRow {
id: number;
enforcementTime: string;
bureauDeptName: string;
categoryText: string;
lawEnforcer: string;
lawEnforcerTwo?: string;
itemText: string;
}
const detailDialogVisible = ref(false);
const detailEnterprise = ref<EnterpriseRow | null>(null);
const detailLoading = ref(false);
const detailTableData = ref<EnforcementDetailRow[]>([]);
const detailTotal = ref(0);
const detailPage = ref(1);
const detailPageSize = ref(20);
const getDetailList = async () => {
detailLoading.value = true;
try {
if (!detailEnterprise.value) return;
// 详情接口:enterpriseNumber(企业id)+ enforcementTime(YYYY-MM),带分页
const res = await listKeyEnterpriseDetails({
pageNum: detailPage.value,
pageSize: detailPageSize.value,
enterpriseNumber: detailEnterprise.value.enterpriseNumber,
enforcementTime: queryParams.statMonth + '-01'
});
detailTableData.value = (res as any)?.rows || (Array.isArray(res) ? res : []);
detailTotal.value = (res as any)?.total ?? detailTableData.value.length;
} finally {
detailLoading.value = false;
}
};
/** 查看详情 — 打开弹窗并加载当前时段的执法明细 */
const handleDetail = (row: EnterpriseRow) => {
if (!row.enterpriseName) return;
detailEnterprise.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();
};
/** 多选 */
const handleSelectionChange = (selection: EnterpriseRow[]) => {
selectedRows.value = selection;
};
onMounted(() => {
loadAreaDict();
getList();
});
</script>
<template>
<div class="app-container">
<!-- 单页白底带边框容器,与 border-card tabs 壳提供的容器效果一致 -->
<div class="page-card">
<!-- ==================== 搜索区域 ==================== -->
<div class="search-panel">
<el-form :inline="true" label-colon>
<el-form-item label="企业名称">
<el-input
v-model="queryParams.enterpriseName"
placeholder="请输入关键字"
class="search-input"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="所属区">
<el-select
v-model="queryParams.districtName"
placeholder="请选择"
class="search-select"
clearable
@change="handleAreaChange"
>
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictLabel" />
</el-select>
</el-form-item>
<el-form-item label="所属街道">
<el-select
v-model="queryParams.streetName"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in streetOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictLabel" />
</el-select>
</el-form-item>
<el-form-item label="年份">
<el-select v-model="queryParams.statYear" class="period-select">
<el-option v-for="y in yearOptions" :key="y" :label="y + '年'" :value="y" />
</el-select>
</el-form-item>
<el-form-item label="月份">
<el-date-picker
v-model="queryParams.statMonth"
type="month"
value-format="YYYY-MM"
class="period-date"
placeholder="选择月份"
/>
</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>
<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="90" align="center">
<template #default="{ $index }">
{{ tableData[$index].enterpriseName ? $index + 1 : '' }}
</template>
</el-table-column>
<el-table-column prop="enterpriseName" label="企业名称" min-width="300" align="center" />
<el-table-column prop="districtName" label="所属区" width="160" align="center" />
<el-table-column prop="streetName" label="所属街道" width="160" align="center" />
<el-table-column label="行政执法次数" width="180" align="center">
<template #default="{ row }">
<span v-if="row.xzzfzs !== null">{{ row.xzzfzs }}次</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button
v-if="row.xzzfzs"
type="primary"
link
size="small"
@click="handleDetail(row)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- ==================== 分页 ==================== -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
class="mt-4"
@size-change="getList"
@current-change="getList"
/>
</div>
<!-- ==================== 企业执法详情弹窗 ==================== -->
<el-dialog
v-model="detailDialogVisible"
width="1100px"
:title="`${detailEnterprise?.enterpriseName || ''} — ${queryParams.statMonth} 执法明细`"
append-to-body
top="5vh"
destroy-on-close
>
<el-alert
v-if="detailEnterprise"
:closable="false"
show-icon
:title="`所属区:${detailEnterprise.districtName || '-'} | 所属街道:${detailEnterprise.streetName || '-'} | 行政执法次数:${detailEnterprise.xzzfzs ?? 0}次 | 时段:${queryParams.statMonth}`"
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="enforcementTime" min-width="180" align="center" />
<el-table-column label="执法单位" prop="bureauDeptName" min-width="260" align="center" />
<el-table-column label="执法类别" prop="dictLabel" width="140" align="center" />
<el-table-column label="执法人员" width="140" align="center">
<template #default="{ row }">
{{ [row.lawEnforcer, row.lawEnforcerTwo].filter(Boolean).join('、') }}
</template>
</el-table-column>
<el-table-column label="执法事项" prop="lawEnforcementItemText" min-width="220" 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>
</div>
</template>
<style scoped lang="scss">
/* 外层容器:与 border-card tabs 壳提供的白底带边框容器一致 */
.app-container {
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-input {
width: 210px;
}
.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>