Browse Source

表格页面

dev-V1.0.6
wangmingyong@163.com 2 months ago
parent
commit
0a1cf8aa3e
  1. 512
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/high-frequency/components/FreqPane.vue
  2. 361
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/high-frequency/index.vue
  3. 6
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/inspection-registration/components/StatPane.vue
  4. 169
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/key-enterprise/index.vue
  5. 333
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/situation-trend/components/SituationPane.vue
  6. 94
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/situation-trend/index.vue
  7. 339
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/trend-behavior/components/TrendPane.vue
  8. 93
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/trend-behavior/index.vue

512
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/high-frequency/components/FreqPane.vue

@ -0,0 +1,512 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { getDicts } from '@/api/system/dict/data';
/** 维度:area=各区域 field=各领域 */
const props = defineProps<{
dimension: 'area' | 'field';
}>();
/** 高频检查预警企业行数据 */
interface HighFrequencyRow {
id: number;
enterpriseName: string;
areaName: string;
streetName: string;
enforcementCount: number | null;
}
// ======================== 查询条件 ========================
const queryParams = reactive({
enterpriseName: '' as string,
area: undefined as string | undefined,
field: undefined as string | undefined,
street: undefined as string | undefined
});
// 时段类型:week=周度 month=月度 quarter=季度 year=年度
const periodType = ref('month');
const periodOptions = [
// { label: '周度', value: 'week' },
{ label: '月度', value: 'month' },
// { label: '季度', value: 'quarter' },
// { label: '年度', value: 'year' }
];
const periodDate = ref<string>('2026-09');
/** 根据时段切换日期选择器类型 */
const pickerType = computed<'week' | 'month' | 'year'>(() => {
if (periodType.value === 'week') return 'week';
if (periodType.value === 'year') return 'year';
return 'month';
});
const pickerFormat = computed(() => {
if (pickerType.value === 'year') return 'YYYY';
if (pickerType.value === 'week') return 'YYYY-MM-DD';
return 'YYYY-MM';
});
// ======================== 字典 ========================
const areaOptions = ref<any[]>([]);
const streetOptions = ref<any[]>([]);
const fieldOptions = [
{ label: '食品安全', value: 'food' },
{ label: '药品安全', value: 'drug' },
{ label: '特种设备', value: 'special_equipment' },
{ label: '产品质量', value: 'product' },
{ label: '计量', value: 'metrology' },
{ label: '认证认可', value: 'certification' }
];
const loadAreaDict = async () => {
try {
const res = await getDicts('address_type');
areaOptions.value = res.data || [];
} catch (e) {
console.error('加载所属区字典失败', e);
}
};
/** 切换所属区时联动加载所属街道 */
const handleAreaChange = async (val: string) => {
queryParams.street = undefined;
streetOptions.value = [];
if (!val) return;
try {
const res = await getDicts(val);
streetOptions.value = res.data || [];
} catch (e) {
console.error('加载所属街道字典失败', e);
}
};
// ======================== 表格数据 ========================
const loading = ref(false);
const tableData = ref<HighFrequencyRow[]>([]);
const total = ref(0);
/** 当前预警数 */
const warningCount = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const selectedRows = ref<HighFrequencyRow[]>([]);
// 表格高度(与 efcode/report/registration/indexList.vue 实现一致)
const tableHeight = ref<number | undefined>(undefined);
const tableRef = ref<HTMLElement>();
/** 计算表格高度:视口高度 - 表格顶部偏移 - 分页区高度(~52px) - 底部边距(16px) */
const calcTableHeight = () => {
if (!tableRef.value) return;
const rect = tableRef.value.getBoundingClientRect();
tableHeight.value = window.innerHeight - rect.top - 84;
};
/** 构造设计稿中的占位数据(前2条有数据,其余为空行) */
const buildMockRows = (): HighFrequencyRow[] => {
const rows: HighFrequencyRow[] = [
{ id: 1, enterpriseName: '深圳市宝安区西乡佳创电子厂', areaName: '宝安区', streetName: '光明街道', enforcementCount: 20 },
{ id: 2, enterpriseName: '深圳市黄金屋真空科技有限公司', areaName: '龙华区', streetName: '福海街道', enforcementCount: 10 }
];
for (let i = 3; i <= 7; i++) {
rows.push({ id: i, enterpriseName: '', areaName: '', streetName: '', enforcementCount: null });
}
return rows;
};
const getList = async () => {
loading.value = true;
try {
// TODO: 接入真实接口后替换为接口返回数据
// const res = await listHighFrequency({
// dimension: props.dimension,
// ...queryParams,
// periodType: periodType.value,
// periodDate: periodDate.value,
// pageNum: currentPage.value,
// pageSize: pageSize.value
// });
tableData.value = buildMockRows();
total.value = 658;
warningCount.value = 20;
} finally {
loading.value = false;
}
};
/** 搜索 */
const handleSearch = () => {
currentPage.value = 1;
getList();
};
/** 重置 */
const handleReset = () => {
queryParams.enterpriseName = '';
queryParams.area = undefined;
queryParams.field = undefined;
queryParams.street = undefined;
streetOptions.value = [];
periodType.value = 'month';
periodDate.value = '2026-09';
currentPage.value = 1;
pageSize.value = 20;
getList();
};
/** 导出 */
const handleExport = () => {
// TODO: 接入导出接口后实现文件下载
};
// ======================== 详情弹窗 ========================
interface EnforcementDetailRow {
id: number;
enforcementTime: string;
bureauDeptName: string;
categoryText: string;
lawEnforcer: string;
itemText: string;
}
const detailDialogVisible = ref(false);
const detailEnterprise = ref<HighFrequencyRow | null>(null);
const detailLoading = ref(false);
const detailTableData = ref<EnforcementDetailRow[]>([]);
const detailTotal = ref(0);
const detailPage = ref(1);
const detailPageSize = ref(20);
/** 获取详情 mock 数据 */
const buildDetailMockRows = (enterprise: HighFrequencyRow, periodDate: string): EnforcementDetailRow[] => {
const rows: EnforcementDetailRow[] = [];
const enforcerPool = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
const bureauPool = [
`${enterprise.areaName}市场监督管理局`,
`${enterprise.areaName}市场监管局${enterprise.streetName}监管所`
];
const categoryPool = ['行政检查', '行政处罚', '专项整治', '日常监管', '投诉举报'];
const itemPool = ['特种设备安全检查', '产品质量抽查', '计量器具检定', '食品安全监督抽检', '药品经营许可检查'];
const count = enterprise.enforcementCount ?? 5;
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,
enforcementTime: `${periodDate}-${String(i % 28 + 1).padStart(2, '0')} ${hour}:${minute}:00`,
bureauDeptName: bureauPool[i % bureauPool.length],
categoryText: categoryPool[i % categoryPool.length],
lawEnforcer: enforcerPool[i % enforcerPool.length],
itemText: itemPool[i % itemPool.length]
});
}
return rows;
};
const getDetailList = async () => {
detailLoading.value = true;
try {
if (!detailEnterprise.value) return;
// TODO: 接入真实接口后替换为接口返回数据
// const res = await listHighFrequencyDetail({
// enterpriseId: detailEnterprise.value.id,
// enterpriseName: detailEnterprise.value.enterpriseName,
// periodType: periodType.value,
// periodDate: periodDate.value,
// pageNum: detailPage.value,
// pageSize: detailPageSize.value
// });
detailTableData.value = buildDetailMockRows(detailEnterprise.value, periodDate.value);
detailTotal.value = detailEnterprise.value.enforcementCount ?? 0;
} finally {
detailLoading.value = false;
}
};
/** 查看详情 — 打开弹窗并加载当前时段的执法明细 */
const handleDetail = (row: HighFrequencyRow) => {
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: HighFrequencyRow[]) => {
selectedRows.value = selection;
};
/** 切换每页条数 */
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
getList();
};
/** 翻页 */
const handlePageChange = (page: number) => {
currentPage.value = page;
getList();
};
onMounted(() => {
loadAreaDict();
getList();
nextTick(() => calcTableHeight());
window.addEventListener('resize', calcTableHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', calcTableHeight);
});
</script>
<template>
<div class="freq-pane">
<!-- ==================== 搜索区域 ==================== -->
<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 v-if="dimension === 'area'" label="所属区">
<el-select
v-model="queryParams.area"
placeholder="请选择"
class="search-select"
clearable
@change="handleAreaChange"
>
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
</el-select>
</el-form-item>
<el-form-item v-else label="所属领域">
<el-select
v-model="queryParams.field"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item v-if="dimension === 'area'" label="所属街道">
<el-select
v-model="queryParams.street"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in streetOptions" :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>
<!-- ==================== 表格区域(表格样式对齐 /statistics/administrative/enterprise-enforcement) ==================== -->
<div class="table-panel">
<!-- 当前预警数 -->
<div class="warning-summary">当前预警数:{{ warningCount }}条</div>
<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="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="areaName" 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.enforcementCount !== null">{{ row.enforcementCount }}次</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button
v-if="row.enterpriseName"
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>
<!-- ==================== 企业执法详情弹窗 ==================== -->
<el-dialog
v-model="detailDialogVisible"
width="1100px"
:title="`${detailEnterprise?.enterpriseName || ''} — ${periodDate} 高频检查明细`"
append-to-body
top="5vh"
destroy-on-close
>
<el-alert
v-if="detailEnterprise"
:closable="false"
show-icon
type="warning"
:title="`预警企业 | 所属区:${detailEnterprise.areaName} | 所属街道:${detailEnterprise.streetName} | 行政执法次数:${detailEnterprise.enforcementCount ?? 0}次 | 时段:${periodType} / ${periodDate}`"
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="categoryText" width="140" align="center" />
<el-table-column label="执法人员" prop="lawEnforcer" width="140" align="center" />
<el-table-column label="执法事项" prop="itemText" 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>
</template>
<style scoped lang="scss">
.freq-pane {
box-sizing: border-box;
}
/* ==================== 搜索面板 ==================== */
.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;
}
}
/* ==================== 表格面板 ==================== */
/* el-table 不做任何自定义皮肤覆盖,与
efcode/report/registration/indexList.vue(/statistics/administrative/enterprise-enforcement)
保持 100% 一致:原生 border + stripe + 动态高度 */
.table-panel {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 16px 20px;
box-sizing: border-box;
}
/* 当前预警数 */
.warning-summary {
margin-bottom: 12px;
font-size: 15px;
font-weight: 700;
color: #303133;
}
/* ==================== 分页(元素合在一起靠右,与 key-enterprise 页一致) ==================== */
.mt-4 {
margin-top: 16px;
justify-content: flex-end;
}
.mb-4 {
margin-bottom: 16px;
}
</style>

361
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/high-frequency/index.vue

@ -1,355 +1,38 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { getDicts } from '@/api/system/dict/data';
import { nextTick, ref } from 'vue';
import FreqPane from './components/FreqPane.vue';
/** 高频检查预警企业行数据 */
interface HighFrequencyRow {
id: number;
enterpriseName: string;
areaName: string;
streetName: string;
enforcementCount: number | null;
}
// ======================== 查询条件 ========================
const queryParams = reactive({
enterpriseName: '' as string,
area: undefined as string | undefined,
street: undefined as string | undefined
});
// 时段类型:week=周度 month=月度 quarter=季度 year=年度
const periodType = ref('month');
const periodOptions = [
{ label: '周度', value: 'week' },
{ label: '月度', value: 'month' },
{ label: '季度', value: 'quarter' },
{ label: '年度', value: 'year' }
];
const periodDate = ref<string>('2026-09');
/** 根据时段切换日期选择器类型 */
const pickerType = computed<'week' | 'month' | 'year'>(() => {
if (periodType.value === 'week') return 'week';
if (periodType.value === 'year') return 'year';
return 'month';
});
const pickerFormat = computed(() => {
if (pickerType.value === 'year') return 'YYYY';
if (pickerType.value === 'week') return 'YYYY-MM-DD';
return 'YYYY-MM';
});
// ======================== 区域/街道字典 ========================
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);
}
};
/** 切换所属区时联动加载所属街道 */
const handleAreaChange = async (val: string) => {
queryParams.street = undefined;
streetOptions.value = [];
if (!val) return;
try {
const res = await getDicts(val);
streetOptions.value = res.data || [];
} catch (e) {
console.error('加载所属街道字典失败', e);
}
};
// ======================== 表格数据 ========================
const loading = ref(false);
const tableData = ref<HighFrequencyRow[]>([]);
const total = ref(0);
/** 当前预警数 */
const warningCount = ref(0);
const currentPage = ref(52); // 默认页码按设计稿
const selectedRows = ref<HighFrequencyRow[]>([]);
// 表格高度(与 efcode/report/registration/indexList.vue 实现一致)
const tableHeight = ref<number | undefined>(undefined);
const tableRef = ref<HTMLElement>();
/** 计算表格高度:视口高度 - 表格顶部偏移 - 分页区高度(~52px) - 底部边距(16px) */
const calcTableHeight = () => {
if (!tableRef.value) return;
const rect = tableRef.value.getBoundingClientRect();
tableHeight.value = window.innerHeight - rect.top - 84;
};
/** 构造设计稿中的占位数据(前2条有数据,其余为空行) */
const buildMockRows = (): HighFrequencyRow[] => {
const rows: HighFrequencyRow[] = [
{ id: 1, enterpriseName: '深圳市宝安区西乡佳创电子厂', areaName: '宝安区', streetName: '光明街道', enforcementCount: 20 },
{ id: 2, enterpriseName: '深圳市黄金屋真空科技有限公司', areaName: '龙华区', streetName: '福海街道', enforcementCount: 10 }
];
for (let i = 3; i <= 7; i++) {
rows.push({ id: i, enterpriseName: '', areaName: '', streetName: '', enforcementCount: null });
}
return rows;
};
const getList = async () => {
loading.value = true;
try {
// TODO: 接入真实接口后替换为接口返回数据
// const res = await listHighFrequency({
// ...queryParams,
// periodType: periodType.value,
// periodDate: periodDate.value,
// pageNum: currentPage.value
// });
tableData.value = buildMockRows();
total.value = 658;
warningCount.value = 20;
} finally {
loading.value = false;
}
};
/** 搜索 */
const handleSearch = () => {
currentPage.value = 1;
getList();
};
/** 重置 */
const handleReset = () => {
queryParams.enterpriseName = '';
queryParams.area = undefined;
queryParams.street = undefined;
streetOptions.value = [];
periodType.value = 'month';
periodDate.value = '2026-09';
currentPage.value = 1;
getList();
};
/** 导出 */
const handleExport = () => {
// TODO: 接入导出接口后实现文件下载
};
// area=各区域 field=各领域
const activeTab = ref<'area' | 'field'>('area');
/** 查看详情 */
const handleDetail = (_row: HighFrequencyRow) => {
// TODO: 待详情页面创建后放开路由跳转
/** 页签切换后通知表格重新计算高度(懒加载/隐藏页签中的尺寸需要重算) */
const handleTabChange = () => {
nextTick(() => {
window.dispatchEvent(new Event('resize'));
});
};
/** 多选 */
const handleSelectionChange = (selection: HighFrequencyRow[]) => {
selectedRows.value = selection;
};
/** 翻页 */
const handlePageChange = (page: number) => {
currentPage.value = page;
getList();
};
onMounted(() => {
loadAreaDict();
getList();
nextTick(() => calcTableHeight());
window.addEventListener('resize', calcTableHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', calcTableHeight);
});
</script>
<template>
<div class="high-frequency-container">
<!-- ==================== 搜索区域 ==================== -->
<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.area"
placeholder="请选择"
class="search-select"
clearable
@change="handleAreaChange"
>
<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="queryParams.street"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in streetOptions" :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 class="warning-summary">当前预警数:{{ warningCount }}条</div>
<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="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="areaName" 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.enforcementCount !== null">{{ row.enforcementCount }}次</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button
v-if="row.enterpriseName"
type="primary"
link
size="small"
@click="handleDetail(row)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- ==================== 分页 ==================== -->
<div class="pagination-bar">
<span class="total-text">共 {{ total }} 项数据</span>
<el-pagination
:current-page="currentPage"
:page-size="10"
:total="total"
background
layout="prev, pager, next"
@current-change="handlePageChange"
/>
</div>
</div>
<div class="app-container">
<el-tabs v-model="activeTab" type="border-card" @tab-change="handleTabChange">
<el-tab-pane label="各区域" name="area" lazy>
<FreqPane dimension="area" />
</el-tab-pane>
<el-tab-pane label="各领域" name="field" lazy>
<FreqPane dimension="field" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<style scoped lang="scss">
.high-frequency-container {
.app-container {
padding: 16px;
background: #f5f7fa;
min-height: calc(100vh - 84px);
box-sizing: border-box;
}
/* ==================== 搜索面板 ==================== */
.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;
}
/* 当前预警数 */
.warning-summary {
margin-bottom: 12px;
font-size: 15px;
font-weight: 700;
color: #303133;
}
/* ==================== 分页 ==================== */
.pagination-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
}
.total-text {
color: #909399;
font-size: 14px;
/* 卡片式页签(与 efcode/report/registration/index.vue、inspection-registration/index.vue 保持一致) */
:deep(.el-tabs--border-card) {
box-shadow: none;
}
</style>

6
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/inspection-registration/components/StatPane.vue

@ -27,10 +27,10 @@ const queryParams = reactive({
// 时段类型:week=周度 month=月度 quarter=季度 year=年度
const periodType = ref('month');
const periodOptions = [
{ label: '周度', value: 'week' },
// { label: '周度', value: 'week' },
{ label: '月度', value: 'month' },
{ label: '季度', value: 'quarter' },
{ label: '年度', value: 'year' }
// { label: '季度', value: 'quarter' },
// { label: '年度', value: 'year' }
];
const periodDate = ref<string>('2026-09');

169
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/key-enterprise/index.vue

@ -21,10 +21,10 @@ const queryParams = reactive({
// 时段类型:week=周度 month=月度 quarter=季度 year=年度
const periodType = ref('month');
const periodOptions = [
{ label: '周度', value: 'week' },
// { label: '周度', value: 'week' },
{ label: '月度', value: 'month' },
{ label: '季度', value: 'quarter' },
{ label: '年度', value: 'year' }
// { label: '季度', value: 'quarter' },
// { label: '年度', value: 'year' }
];
const periodDate = ref<string>('2026-09');
@ -133,9 +133,89 @@ const handleExport = () => {
// TODO: 接入导出接口后实现文件下载
};
/** 查看详情 */
const handleDetail = (_row: EnterpriseRow) => {
// TODO: 待详情页面创建后放开路由跳转
// ======================== 详情弹窗 ========================
interface EnforcementDetailRow {
id: number;
enforcementTime: string;
bureauDeptName: string;
categoryText: string;
lawEnforcer: 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);
/** 获取详情 mock 数据 */
const buildDetailMockRows = (enterprise: EnterpriseRow, periodDate: string): EnforcementDetailRow[] => {
const rows: EnforcementDetailRow[] = [];
const enforcerPool = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
const bureauPool = [
`${enterprise.areaName}市场监督管理局`,
`${enterprise.areaName}市场监管局${enterprise.streetName}监管所`
];
const categoryPool = ['行政检查', '行政处罚', '专项整治', '日常监管'];
const itemPool = ['特种设备安全检查', '产品质量抽查', '计量器具检定', '食品安全监督抽检', '药品经营许可检查'];
const count = enterprise.enforcementCount ?? 5;
for (let i = 1; i <= Math.min(count, 8); i++) {
const hour = String(8 + (i % 11)).padStart(2, '0');
const minute = String((i * 17) % 60).padStart(2, '0');
rows.push({
id: i,
enforcementTime: `${periodDate}-${String(i % 28 + 1).padStart(2, '0')} ${hour}:${minute}:00`,
bureauDeptName: bureauPool[i % bureauPool.length],
categoryText: categoryPool[i % categoryPool.length],
lawEnforcer: enforcerPool[i % enforcerPool.length],
itemText: itemPool[i % itemPool.length]
});
}
return rows;
};
const getDetailList = async () => {
detailLoading.value = true;
try {
if (!detailEnterprise.value) return;
// TODO: 接入真实接口后替换为接口返回数据
// const res = await listKeyEnterpriseDetail({
// enterpriseId: detailEnterprise.value.id,
// enterpriseName: detailEnterprise.value.enterpriseName,
// periodType: periodType.value,
// periodDate: periodDate.value,
// pageNum: detailPage.value,
// pageSize: detailPageSize.value
// });
detailTableData.value = buildDetailMockRows(detailEnterprise.value, periodDate.value);
detailTotal.value = detailEnterprise.value.enforcementCount ?? 0;
} 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();
};
/** 多选 */
@ -169,8 +249,10 @@ onUnmounted(() => {
</script>
<template>
<div class="key-enterprise-container">
<!-- ==================== 搜索区域 ==================== -->
<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="企业名称">
@ -225,7 +307,7 @@ onUnmounted(() => {
</el-form>
</div>
<!-- ==================== 表格区域(表格样式对齐 /statistics/administrative/enterprise-enforcement) ==================== -->
<!-- ==================== 表格区域 ==================== -->
<div class="table-panel">
<div ref="tableRef">
<el-table
@ -279,17 +361,72 @@ onUnmounted(() => {
@current-change="handlePageChange"
/>
</div>
<!-- ==================== 企业执法详情弹窗 ==================== -->
<el-dialog
v-model="detailDialogVisible"
width="1100px"
:title="`${detailEnterprise?.enterpriseName || ''} — ${periodDate} 执法明细`"
append-to-body
top="5vh"
destroy-on-close
>
<el-alert
v-if="detailEnterprise"
:closable="false"
show-icon
:title="`所属区:${detailEnterprise.areaName} | 所属街道:${detailEnterprise.streetName} | 行政执法次数:${detailEnterprise.enforcementCount ?? 0}次 | 时段:${periodType} / ${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="enforcementTime" min-width="180" align="center" />
<el-table-column label="执法单位" prop="bureauDeptName" min-width="260" align="center" />
<el-table-column label="执法类别" prop="categoryText" width="140" align="center" />
<el-table-column label="执法人员" prop="lawEnforcer" width="140" align="center" />
<el-table-column label="执法事项" prop="itemText" 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">
.key-enterprise-container {
/* 外层容器:与 border-card tabs 壳提供的白底带边框容器一致 */
.app-container {
padding: 16px;
background: #f5f7fa;
min-height: calc(100vh - 84px);
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;
@ -321,9 +458,6 @@ onUnmounted(() => {
}
/* ==================== 表格面板 ==================== */
/* el-table 不做任何自定义皮肤覆盖,与
efcode/report/registration/indexList.vue(/statistics/administrative/enterprise-enforcement)
保持 100% 一致:原生 border + stripe + 动态高度 */
.table-panel {
background: #fff;
border: 1px solid #e4e7ed;
@ -332,9 +466,12 @@ onUnmounted(() => {
box-sizing: border-box;
}
/* ==================== 分页(元素合在一起,不做左右分布) ==================== */
/* ==================== 分页 ==================== */
.mt-4 {
margin-top: 16px;
justify-content: flex-end;
}
.mb-4 {
margin-bottom: 16px;
}
</style>

333
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/situation-trend/components/SituationPane.vue

@ -0,0 +1,333 @@
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { getDicts } from '@/api/system/dict/data';
/** 维度:area=各区域 field=各领域 */
const props = defineProps<{
dimension: 'area' | 'field';
}>();
/** 态势趋势综合评分行 */
interface SituationRow {
id: number;
dimensionName: string;
yearQuarter: string; // 如 2026Q3
score: string | null; // 优良 / 良好 / 一般
}
// ======================== 查询条件 ========================
const queryParams = reactive({
area: undefined as string | undefined,
field: undefined as string | undefined
});
// 时段类型:week=周度 month=月度 quarter=季度 year=年度
const periodType = ref('quarter');
const periodOptions = [
// { label: '周度', value: 'week' },
// { label: '月度', value: 'month' },
{ label: '季度', value: 'quarter' },
// { label: '年度', value: 'year' }
];
// 年份(type="year" 时 el-date-picker 返回 'YYYY' 格式字符串)
const periodYear = ref<string>('2026');
// 季度(1/2/3/4),仅 periodType === 'quarter' 时显示
const periodQuarter = ref<string>('3');
const quarterOptions = [
{ label: '第一季度', value: '1' },
{ label: '第二季度', value: '2' },
{ label: '第三季度', value: '3' },
{ label: '第四季度', value: '4' }
];
// ======================== 字典 ========================
const areaOptions = ref<any[]>([]);
const fieldOptions = [
{ label: '食品安全', value: 'food' },
{ label: '药品安全', value: 'drug' },
{ label: '特种设备', value: 'special_equipment' },
{ label: '产品质量', value: 'product' },
{ label: '计量', value: 'metrology' },
{ label: '认证认可', value: 'certification' }
];
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<SituationRow[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const selectedRows = ref<SituationRow[]>([]);
// 表格高度(与 efcode/report/registration/indexList.vue 实现一致)
const tableHeight = ref<number | undefined>(undefined);
const tableRef = ref<HTMLElement>();
/** 计算表格高度:视口高度 - 表格顶部偏移 - 分页区高度(~52px) - 底部边距(16px) */
const calcTableHeight = () => {
if (!tableRef.value) return;
const rect = tableRef.value.getBoundingClientRect();
tableHeight.value = window.innerHeight - rect.top - 84;
};
/** 构造设计稿中的占位数据(前2条有数据,其余为空行) */
const buildMockRows = (): SituationRow[] => {
const isArea = props.dimension === 'area';
const yearQuarter = `${periodYear.value}Q${periodQuarter.value}`;
const rows: SituationRow[] = [
{
id: 1,
dimensionName: isArea ? '宝安区' : '食品安全',
yearQuarter,
score: '优良'
},
{
id: 2,
dimensionName: isArea ? '龙华区' : '特种设备',
yearQuarter,
score: '良好'
}
];
for (let i = 3; i <= 7; i++) {
rows.push({ id: i, dimensionName: '', yearQuarter: '', score: null });
}
return rows;
};
const getList = async () => {
loading.value = true;
try {
// TODO: 接入真实接口后替换为接口返回数据
// const res = await listSituationTrend({
// dimension: props.dimension,
// ...queryParams,
// periodType: periodType.value,
// periodYear: periodYear.value,
// periodQuarter: periodQuarter.value,
// pageNum: currentPage.value,
// pageSize: pageSize.value
// });
tableData.value = buildMockRows();
total.value = 658;
} finally {
loading.value = false;
}
};
/** 搜索 */
const handleSearch = () => {
currentPage.value = 1;
getList();
};
/** 重置 */
const handleReset = () => {
queryParams.area = undefined;
queryParams.field = undefined;
periodType.value = 'quarter';
periodYear.value = '2026';
periodQuarter.value = '3';
currentPage.value = 1;
pageSize.value = 20;
getList();
};
/** 导出 */
const handleExport = () => {
// TODO: 接入导出接口后实现文件下载
};
/** 多选 */
const handleSelectionChange = (selection: SituationRow[]) => {
selectedRows.value = selection;
};
/** 切换每页条数 */
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
getList();
};
/** 翻页 */
const handlePageChange = (page: number) => {
currentPage.value = page;
getList();
};
onMounted(() => {
loadAreaDict();
getList();
nextTick(() => calcTableHeight());
window.addEventListener('resize', calcTableHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', calcTableHeight);
});
</script>
<template>
<div class="situation-pane">
<!-- ==================== 搜索区域 ==================== -->
<div class="search-panel">
<el-form :inline="true" label-colon>
<el-form-item v-if="dimension === 'area'" 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 v-else label="所属领域">
<el-select
v-model="queryParams.field"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.value" />
</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 label="年">
<el-date-picker
v-model="periodYear"
type="year"
value-format="YYYY"
format="YYYY"
class="year-picker"
/>
</el-form-item>
<el-form-item v-if="periodType === 'quarter'" label="季度">
<el-select v-model="periodQuarter" class="quarter-select">
<el-option v-for="item in quarterOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</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="90" align="center">
<template #default="{ $index }">
{{ tableData[$index].dimensionName ? $index + 1 : '' }}
</template>
</el-table-column>
<el-table-column
prop="dimensionName"
:label="dimension === 'area' ? '所属区' : '所属领域'"
min-width="220"
align="center"
/>
<el-table-column label="年+季度" width="160" align="center">
<template #default="{ row }">
{{ row.yearQuarter }}
</template>
</el-table-column>
<el-table-column label="行政执法综合评分" min-width="180" align="center">
<template #default="{ row }">
<span v-if="row.score !== null">{{ row.score }}</span>
</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>
</template>
<style scoped lang="scss">
.situation-pane {
box-sizing: border-box;
}
/* ==================== 搜索面板 ==================== */
.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;
}
.year-picker {
width: 130px;
}
.quarter-select {
width: 150px;
}
}
/* ==================== 表格面板 ==================== */
.table-panel {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 16px 20px;
box-sizing: border-box;
}
/* ==================== 分页(元素合在一起靠右,与其他 trend 页一致) ==================== */
.mt-4 {
margin-top: 16px;
justify-content: flex-end;
}
</style>

94
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/situation-trend/index.vue

@ -1,83 +1,28 @@
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { nextTick, ref } from 'vue';
import SituationPane from './components/SituationPane.vue';
// 查询参数
const queryParams = reactive({
area: undefined as string | undefined,
field: undefined as string | undefined,
dateRange: [] as string[]
});
// area=各区域 field=各领域
const activeTab = ref<'area' | 'field'>('area');
const loading = ref(false);
/** 查询 */
const handleQuery = () => {
loading.value = true;
// TODO: 调用对应API加载数据
setTimeout(() => {
loading.value = false;
}, 500);
};
/** 重置 */
const handleReset = () => {
queryParams.area = undefined;
queryParams.field = undefined;
queryParams.dateRange = [];
/** 页签切换后通知表格重新计算高度(懒加载/隐藏页签中的尺寸需要重算) */
const handleTabChange = () => {
nextTick(() => {
window.dispatchEvent(new Event('resize'));
});
};
</script>
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-card shadow="hover" class="mb-[10px]">
<el-form :inline="true" :model="queryParams" label-width="100px">
<el-form-item label="所属区域">
<el-select v-model="queryParams.area" placeholder="请选择区域" clearable style="width: 180px">
<el-option label="福田区" value="futian" />
<el-option label="罗湖区" value="luohu" />
<el-option label="南山区" value="nanshan" />
<el-option label="盐田区" value="yantian" />
<el-option label="宝安区" value="baoan" />
<el-option label="龙岗区" value="longgang" />
<el-option label="龙华区" value="longhua" />
<el-option label="坪山区" value="pingshan" />
<el-option label="光明区" value="guangming" />
<el-option label="大鹏新区" value="dapeng" />
</el-select>
</el-form-item>
<el-form-item label="监测领域">
<el-select v-model="queryParams.field" placeholder="请选择领域" clearable style="width: 180px">
<el-option label="食品安全" value="food" />
<el-option label="药品安全" value="drug" />
<el-option label="特种设备" value="special_equipment" />
<el-option label="产品质量" value="product" />
<el-option label="计量" value="metrology" />
<el-option label="认证认可" value="certification" />
</el-select>
</el-form-item>
<el-form-item label="统计时间">
<el-date-picker
v-model="queryParams.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容区域 -->
<el-card shadow="hover" v-loading="loading">
<el-empty description="暂无数据,待接口接入后展示态势趋势监测分析数据" />
</el-card>
<el-tabs v-model="activeTab" type="border-card" @tab-change="handleTabChange">
<el-tab-pane label="各区域" name="area" lazy>
<SituationPane dimension="area" />
</el-tab-pane>
<el-tab-pane label="各领域" name="field" lazy>
<SituationPane dimension="field" />
</el-tab-pane>
</el-tabs>
</div>
</template>
@ -85,4 +30,9 @@ const handleReset = () => {
.app-container {
padding: 16px;
}
/* 卡片式页签(与 inspection-registration / high-frequency / trend-behavior 保持一致) */
:deep(.el-tabs--border-card) {
box-shadow: none;
}
</style>

339
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/trend-behavior/components/TrendPane.vue

@ -0,0 +1,339 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { getDicts } from '@/api/system/dict/data';
/** 维度:area=各区域 field=各领域 */
const props = defineProps<{
dimension: 'area' | 'field';
}>();
/** 趋势行为监测行数据 */
interface TrendBehaviorRow {
id: number;
dimensionName: string;
yearQuarter: string; // 如 2026Q3
enforcementCount: number | null;
}
// ======================== 查询条件 ========================
const queryParams = reactive({
area: undefined as string | undefined,
field: undefined as string | undefined
});
// 时段类型:week=周度 month=月度 quarter=季度 year=年度
const periodType = ref('quarter');
const periodOptions = [
// { label: '周度', value: 'week' },
// { label: '月度', value: 'month' },
{ label: '季度', value: 'quarter' },
// { label: '年度', value: 'year' }
];
// 年份(type="year" 时 el-date-picker 返回 'YYYY' 格式字符串)
const periodYear = ref<string>('2026');
// 季度(1/2/3/4),仅 periodType === 'quarter' 时显示
const periodQuarter = ref<string>('3');
const quarterOptions = [
{ label: '第一季度', value: '1' },
{ label: '第二季度', value: '2' },
{ label: '第三季度', value: '3' },
{ label: '第四季度', value: '4' }
];
// ======================== 字典 ========================
const areaOptions = ref<any[]>([]);
const fieldOptions = [
{ label: '食品安全', value: 'food' },
{ label: '药品安全', value: 'drug' },
{ label: '特种设备', value: 'special_equipment' },
{ label: '产品质量', value: 'product' },
{ label: '计量', value: 'metrology' },
{ label: '认证认可', value: 'certification' }
];
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<TrendBehaviorRow[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const selectedRows = ref<TrendBehaviorRow[]>([]);
// 表格高度(与 efcode/report/registration/indexList.vue 实现一致)
const tableHeight = ref<number | undefined>(undefined);
const tableRef = ref<HTMLElement>();
/** 计算表格高度:视口高度 - 表格顶部偏移 - 分页区高度(~52px) - 底部边距(16px) */
const calcTableHeight = () => {
if (!tableRef.value) return;
const rect = tableRef.value.getBoundingClientRect();
tableHeight.value = window.innerHeight - rect.top - 84;
};
/** 构造设计稿中的占位数据(前2条有数据,其余为空行) */
const buildMockRows = (): TrendBehaviorRow[] => {
const isArea = props.dimension === 'area';
const yearQuarter = `${periodYear.value}Q${periodQuarter.value}`;
const rows: TrendBehaviorRow[] = [
{
id: 1,
dimensionName: isArea ? '宝安区' : '食品安全',
yearQuarter,
enforcementCount: 998
},
{
id: 2,
dimensionName: isArea ? '龙华区' : '特种设备',
yearQuarter,
enforcementCount: 990
}
];
for (let i = 3; i <= 7; i++) {
rows.push({ id: i, dimensionName: '', yearQuarter: '', enforcementCount: null });
}
return rows;
};
const getList = async () => {
loading.value = true;
try {
// TODO: 接入真实接口后替换为接口返回数据
// const res = await listTrendBehavior({
// dimension: props.dimension,
// ...queryParams,
// periodType: periodType.value,
// periodYear: periodYear.value,
// periodQuarter: periodQuarter.value,
// pageNum: currentPage.value,
// pageSize: pageSize.value
// });
tableData.value = buildMockRows();
total.value = 658;
} finally {
loading.value = false;
}
};
/** 搜索 */
const handleSearch = () => {
currentPage.value = 1;
getList();
};
/** 重置 */
const handleReset = () => {
queryParams.area = undefined;
queryParams.field = undefined;
periodType.value = 'quarter';
periodYear.value = '2026';
periodQuarter.value = '3';
currentPage.value = 1;
pageSize.value = 20;
getList();
};
/** 导出 */
const handleExport = () => {
// TODO: 接入导出接口后实现文件下载
};
/** 多选 */
const handleSelectionChange = (selection: TrendBehaviorRow[]) => {
selectedRows.value = selection;
};
/** 切换每页条数 */
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
getList();
};
/** 翻页 */
const handlePageChange = (page: number) => {
currentPage.value = page;
getList();
};
/** 年+季度列显示(为空时隐藏) */
const formatYearQuarter = (row: TrendBehaviorRow) => {
if (!row.yearQuarter) return '';
return row.yearQuarter;
};
onMounted(() => {
loadAreaDict();
getList();
nextTick(() => calcTableHeight());
window.addEventListener('resize', calcTableHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', calcTableHeight);
});
</script>
<template>
<div class="trend-pane">
<!-- ==================== 搜索区域 ==================== -->
<div class="search-panel">
<el-form :inline="true" label-colon>
<el-form-item v-if="dimension === 'area'" 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 v-else label="所属领域">
<el-select
v-model="queryParams.field"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.value" />
</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 label="年">
<el-date-picker
v-model="periodYear"
type="year"
value-format="YYYY"
format="YYYY"
class="year-picker"
/>
</el-form-item>
<el-form-item v-if="periodType === 'quarter'" label="季度">
<el-select v-model="periodQuarter" class="quarter-select">
<el-option v-for="item in quarterOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</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="90" align="center">
<template #default="{ $index }">
{{ tableData[$index].dimensionName ? $index + 1 : '' }}
</template>
</el-table-column>
<el-table-column
prop="dimensionName"
:label="dimension === 'area' ? '所属区' : '所属领域'"
min-width="220"
align="center"
/>
<el-table-column label="年+季度" width="160" align="center">
<template #default="{ row }">
{{ formatYearQuarter(row) }}
</template>
</el-table-column>
<el-table-column label="行政执法次数" width="180" align="center">
<template #default="{ row }">
<span v-if="row.enforcementCount !== null">{{ row.enforcementCount }}次</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- ==================== 分页(与 key-enterprise / StatPane / FreqPane 完全一致) ==================== -->
<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>
</template>
<style scoped lang="scss">
.trend-pane {
box-sizing: border-box;
}
/* ==================== 搜索面板 ==================== */
.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;
}
.year-picker {
width: 130px;
}
.quarter-select {
width: 150px;
}
}
/* ==================== 表格面板 ==================== */
.table-panel {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 16px 20px;
box-sizing: border-box;
}
/* ==================== 分页(元素合在一起靠右,与其他 trend 页一致) ==================== */
.mt-4 {
margin-top: 16px;
justify-content: flex-end;
}
</style>

93
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/trend-behavior/index.vue

@ -1,82 +1,28 @@
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { nextTick, ref } from 'vue';
import TrendPane from './components/TrendPane.vue';
// 查询参数
const queryParams = reactive({
area: undefined as string | undefined,
behaviorType: undefined as string | undefined,
dateRange: [] as string[]
});
// area=各区域 field=各领域
const activeTab = ref<'area' | 'field'>('area');
const loading = ref(false);
/** 查询 */
const handleQuery = () => {
loading.value = true;
// TODO: 调用对应API加载数据
setTimeout(() => {
loading.value = false;
}, 500);
};
/** 重置 */
const handleReset = () => {
queryParams.area = undefined;
queryParams.behaviorType = undefined;
queryParams.dateRange = [];
/** 页签切换后通知表格重新计算高度(懒加载/隐藏页签中的尺寸需要重算) */
const handleTabChange = () => {
nextTick(() => {
window.dispatchEvent(new Event('resize'));
});
};
</script>
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-card shadow="hover" class="mb-[10px]">
<el-form :inline="true" :model="queryParams" label-width="100px">
<el-form-item label="所属区域">
<el-select v-model="queryParams.area" placeholder="请选择区域" clearable style="width: 180px">
<el-option label="福田区" value="futian" />
<el-option label="罗湖区" value="luohu" />
<el-option label="南山区" value="nanshan" />
<el-option label="盐田区" value="yantian" />
<el-option label="宝安区" value="baoan" />
<el-option label="龙岗区" value="longgang" />
<el-option label="龙华区" value="longhua" />
<el-option label="坪山区" value="pingshan" />
<el-option label="光明区" value="guangming" />
<el-option label="大鹏新区" value="dapeng" />
</el-select>
</el-form-item>
<el-form-item label="行为类型">
<el-select v-model="queryParams.behaviorType" placeholder="请选择类型" clearable style="width: 180px">
<el-option label="执法检查" value="inspection" />
<el-option label="投诉举报" value="complaint" />
<el-option label="案件查办" value="case" />
<el-option label="应急处置" value="emergency" />
<el-option label="日常监管" value="daily" />
</el-select>
</el-form-item>
<el-form-item label="统计时间">
<el-date-picker
v-model="queryParams.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容区域 -->
<el-card shadow="hover" v-loading="loading">
<el-empty description="暂无数据,待接口接入后展示趋势行为监测分析数据" />
</el-card>
<el-tabs v-model="activeTab" type="border-card" @tab-change="handleTabChange">
<el-tab-pane label="各区域" name="area" lazy>
<TrendPane dimension="area" />
</el-tab-pane>
<el-tab-pane label="各领域" name="field" lazy>
<TrendPane dimension="field" />
</el-tab-pane>
</el-tabs>
</div>
</template>
@ -84,4 +30,9 @@ const handleReset = () => {
.app-container {
padding: 16px;
}
/* 卡片式页签(与 inspection-registration、high-frequency 页保持一致) */
:deep(.el-tabs--border-card) {
box-shadow: none;
}
</style>

Loading…
Cancel
Save