Compare commits

...

3 Commits

  1. 282
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/high-frequency/components/FreqPane.vue
  2. 225
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/inspection-registration/components/StatPane.vue
  3. 166
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/key-enterprise/index.vue
  4. 208
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/scan-district/index.vue
  5. 200
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/scan-enforcer/index.vue
  6. 14
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/scan-field/index.vue
  7. 196
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/situation-trend/components/SituationPane.vue
  8. 198
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/trend-behavior/components/TrendPane.vue
  9. 3
      zdxt-web-client/zdxt-efcode-enforcement-app/src/api/nos/index.js
  10. 32
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/nos/nosDetail.vue
  11. 4
      zdxt-web-client/zdxt-efcode-enforcement-app/vite-env/.env.enforcement-dev-wmy
  12. 2
      zdxt-web-client/zdxt-efcode-enterprise-app/src/components/enterprise/footerNav.vue
  13. 7
      zdxt-web-client/zdxt-efcode-enterprise-app/src/router/enterprise-app/ent-main-router.js
  14. 54
      zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/enforcement/enforcementRegistrationList.vue
  15. 368
      zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/law/lawQuery.vue
  16. 3
      zdxt-web-client/zdxt-efcode-enterprise-app/vite-env/.env.enterprise-dev-wmy

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

@ -1,54 +1,32 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { getDicts } from '@/api/system/dict/data';
import { listHighCheckArea, listHighCheckDomain, type HighFrequencyRow } from '@/api/efcode/report/high-frequency';
/** 维度:area=各区域 field=各领域 */
const props = defineProps<{
dimension: 'area' | 'field';
}>();
/** 高频检查预警企业行数据 */
interface HighFrequencyRow {
id: number;
enterpriseName: string;
areaName: string;
streetName: string;
enforcementCount: number | null;
}
// ======================== 查询条件 ========================
const now = new Date();
const queryParams = reactive({
enterpriseName: '' as string,
area: undefined as string | undefined,
field: undefined as string | undefined,
street: undefined as string | undefined
pageNum: 1,
pageSize: 20,
district: undefined as string | undefined, // 区域 tab 用
domainName: undefined as string | undefined, // 领域 tab 用
year: String(now.getFullYear()),
month: `${String(now.getFullYear())}-${String(now.getMonth() + 1).padStart(2, '0')}`
});
// 时段类型: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';
});
/** 年份选项(近 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 fieldOptions = [
{ label: '食品安全', value: 'food' },
{ label: '药品安全', value: 'drug' },
@ -67,67 +45,54 @@ const loadAreaDict = async () => {
}
};
/** 切换所属区时联动加载所属街道 */
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;
let rows: HighFrequencyRow[] = [];
let totalNum = 0;
if (props.dimension === 'area') {
// 区域接口:district + enforcementTime(直接用 month 的 YYYY-MM 值)
const res = await listHighCheckArea({
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize,
district: queryParams.district,
enforcementTime: queryParams.month
});
rows = (res as any)?.rows || [];
totalNum = (res as any)?.total || 0;
} else {
// 领域接口:domainName + year + month(month 传值和 area 一样,但后端参数名不同)
const res = await listHighCheckDomain({
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize,
domainName: queryParams.domainName,
year: queryParams.year,
month: queryParams.month
});
rows = (res as any)?.rows || [];
totalNum = (res as any)?.total || 0;
}
tableData.value = rows;
total.value = totalNum;
warningCount.value = totalNum;
} finally {
loading.value = false;
}
@ -135,21 +100,18 @@ const getList = async () => {
/** 搜索 */
const handleSearch = () => {
currentPage.value = 1;
queryParams.pageNum = 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;
queryParams.district = undefined;
queryParams.domainName = undefined;
queryParams.year = String(new Date().getFullYear());
queryParams.month = `${String(new Date().getFullYear())}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
queryParams.pageNum = 1;
queryParams.pageSize = 20;
getList();
};
@ -158,6 +120,11 @@ const handleExport = () => {
// TODO: 接入导出接口后实现文件下载
};
/** 多选 */
const handleSelectionChange = (selection: HighFrequencyRow[]) => {
selectedRows.value = selection;
};
// ======================== 详情弹窗 ========================
interface EnforcementDetailRow {
id: number;
@ -176,54 +143,17 @@ 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;
// TODO: 接入真实详情接口 listHighFrequencyDetail
} finally {
detailLoading.value = false;
}
};
/** 查看详情 — 打开弹窗并加载当前时段的执法明细 */
/** 查看详情 */
const handleDetail = (row: HighFrequencyRow) => {
if (!row.enterpriseName) return;
detailEnterprise.value = row;
@ -243,24 +173,6 @@ const handleDetailPageChange = (page: number) => {
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();
@ -271,6 +183,8 @@ onMounted(() => {
onUnmounted(() => {
window.removeEventListener('resize', calcTableHeight);
});
defineExpose({ calcTableHeight });
</script>
<template>
@ -278,58 +192,38 @@ onUnmounted(() => {
<!-- ==================== 搜索区域 ==================== -->
<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"
v-model="queryParams.district"
placeholder="请选择"
class="search-select"
clearable
@change="handleAreaChange"
>
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictLabel" />
</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"
v-model="queryParams.domainName"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in streetOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.label" />
</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-form-item label="年份">
<el-select v-model="queryParams.year" class="period-select">
<el-option v-for="y in yearOptions" :key="y" :label="y + '年'" :value="y" />
</el-select>
</el-form-item>
<el-form-item>
<el-form-item label="月份">
<el-date-picker
v-model="periodDate"
:type="pickerType"
:format="pickerFormat"
:value-format="pickerFormat"
v-model="queryParams.month"
type="month"
value-format="YYYY-MM"
class="period-date"
placeholder="选择月份"
/>
</el-form-item>
<el-form-item>
@ -340,7 +234,7 @@ onUnmounted(() => {
</el-form>
</div>
<!-- ==================== 表格区域(表格样式对齐 /statistics/administrative/enterprise-enforcement) ==================== -->
<!-- ==================== 表格区域 ==================== -->
<div class="table-panel">
<!-- 当前预警数 -->
<div class="warning-summary">当前预警数:{{ warningCount }}条</div>
@ -356,20 +250,16 @@ onUnmounted(() => {
@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 label="序号" width="90" align="center" type="index" />
<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>
<span v-if="row.enforcementCount !== null && row.enforcementCount !== undefined">{{ row.enforcementCount }}次</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<el-button
v-if="row.enterpriseName"
@ -387,14 +277,14 @@ onUnmounted(() => {
<!-- ==================== 分页 ==================== -->
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
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="handleSizeChange"
@current-change="handlePageChange"
@size-change="getList"
@current-change="getList"
/>
</div>
@ -402,7 +292,7 @@ onUnmounted(() => {
<el-dialog
v-model="detailDialogVisible"
width="1100px"
:title="`${detailEnterprise?.enterpriseName || ''} — ${periodDate} 高频检查明细`"
:title="`${detailEnterprise?.enterpriseName || ''} — ${queryParams.year}年${queryParams.month} 高频检查明细`"
append-to-body
top="5vh"
destroy-on-close
@ -412,7 +302,7 @@ onUnmounted(() => {
:closable="false"
show-icon
type="warning"
:title="`预警企业 | 所属区:${detailEnterprise.areaName} | 所属街道:${detailEnterprise.streetName} | 行政执法次数:${detailEnterprise.enforcementCount ?? 0}次 | 时段:${periodType} / ${periodDate}`"
:title="`预警企业 | 所属区:${detailEnterprise.areaName || '-'} | 所属街道:${detailEnterprise.streetName || '-'} | 行政执法次数:${detailEnterprise.enforcementCount ?? 0}次 | 时段:${queryParams.year}年${queryParams.month}`"
class="mb-4"
/>
@ -464,10 +354,6 @@ onUnmounted(() => {
color: #333;
}
.search-input {
width: 210px;
}
.search-select {
width: 220px;
}
@ -482,9 +368,6 @@ onUnmounted(() => {
}
/* ==================== 表格面板 ==================== */
/* el-table 不做任何自定义皮肤覆盖,与
efcode/report/registration/indexList.vue(/statistics/administrative/enterprise-enforcement)
保持 100% 一致:原生 border + stripe + 动态高度 */
.table-panel {
background: #fff;
border: 1px solid #e4e7ed;
@ -501,12 +384,9 @@ onUnmounted(() => {
color: #303133;
}
/* ==================== 分页(元素合在一起靠右,与 key-enterprise 页一致) ==================== */
/* ==================== 分页 ==================== */
.mt-4 {
margin-top: 16px;
justify-content: flex-end;
}
.mb-4 {
margin-bottom: 16px;
}
</style>

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

@ -1,50 +1,29 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { getDicts } from '@/api/system/dict/data';
import { listAreaRegistration, listFieldRegistration, type CheckRegistrationRow } from '@/api/efcode/report/check-registration';
/** 维度:area=各区域 field=各领域 */
const props = defineProps<{
dimension: 'area' | 'field';
}>();
/** 统计行数据(各区域 / 各领域通用) */
interface StatRow {
id: number;
dimensionName: string;
enforcementCount: number | null;
inspectionCaseCount: number | null;
penaltyCaseCount: number | null;
coerciveCount: number | null;
}
// ======================== 查询条件 ========================
const now = new Date();
const queryParams = reactive({
enterpriseName: '' as string,
area: undefined as string | undefined,
field: undefined as string | undefined
pageNum: 1,
pageSize: 20,
areaName: undefined as string | undefined,
fieldName: undefined as string | undefined,
year: String(now.getFullYear()),
month: `${String(now.getFullYear())}-${String(now.getMonth() + 1).padStart(2, '0')}`
});
// 时段类型: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';
});
/** 年份选项(近 5 年) */
const yearOptions = (() => {
const curr = now.getFullYear();
return Array.from({ length: 5 }, (_, i) => String(curr - i));
})();
// ======================== 所属区 / 领域字典 ========================
const areaOptions = ref<any[]>([]);
@ -68,11 +47,9 @@ const loadAreaDict = async () => {
// ======================== 表格数据 ========================
const loading = ref(false);
const tableData = ref<StatRow[]>([]);
const tableData = ref<CheckRegistrationRow[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const selectedRows = ref<StatRow[]>([]);
const selectedRows = ref<CheckRegistrationRow[]>([]);
// 表格高度(与 efcode/report/registration/indexList.vue 实现一致)
const tableHeight = ref<number | undefined>(undefined);
@ -85,47 +62,27 @@ const calcTableHeight = () => {
tableHeight.value = window.innerHeight - rect.top - 84;
};
/** 构造设计稿中的占位数据(前2条有数据,其余为空行) */
const buildMockRows = (): StatRow[] => {
const isArea = props.dimension === 'area';
const rows: StatRow[] = [
{
id: 1,
dimensionName: isArea ? '宝安区' : '食品安全',
enforcementCount: 996,
inspectionCaseCount: 996,
penaltyCaseCount: 996,
coerciveCount: 996
},
{
id: 2,
dimensionName: isArea ? '龙华区' : '特种设备',
enforcementCount: 990,
inspectionCaseCount: 990,
penaltyCaseCount: 990,
coerciveCount: 990
}
];
for (let i = 3; i <= 7; i++) {
rows.push({ id: i, dimensionName: '', enforcementCount: null, inspectionCaseCount: null, penaltyCaseCount: null, coerciveCount: null });
}
return rows;
};
const getList = async () => {
loading.value = true;
try {
// TODO: 接入真实接口后替换为接口返回数据
// const res = await listInspectionRegistration({
// dimension: props.dimension,
// ...queryParams,
// periodType: periodType.value,
// periodDate: periodDate.value,
// pageNum: currentPage.value,
// pageSize: pageSize.value
// });
tableData.value = buildMockRows();
total.value = 658;
// 公共参数:year + month + 分页
const params: any = {
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize,
year: queryParams.year,
month: queryParams.month
};
if (props.dimension === 'area') {
params.areaName = queryParams.areaName;
const res = await listAreaRegistration(params);
tableData.value = (res as any)?.rows || [];
total.value = (res as any)?.total || 0;
} else {
params.fieldName = queryParams.fieldName;
const res = await listFieldRegistration(params);
tableData.value = (res as any)?.rows || [];
total.value = (res as any)?.total || 0;
}
} finally {
loading.value = false;
}
@ -133,19 +90,18 @@ const getList = async () => {
/** 搜索 */
const handleSearch = () => {
currentPage.value = 1;
queryParams.pageNum = 1;
getList();
};
/** 重置 */
const handleReset = () => {
queryParams.enterpriseName = '';
queryParams.area = undefined;
queryParams.field = undefined;
periodType.value = 'month';
periodDate.value = '2026-09';
currentPage.value = 1;
pageSize.value = 20;
queryParams.areaName = undefined;
queryParams.fieldName = undefined;
queryParams.year = String(new Date().getFullYear());
queryParams.month = `${String(new Date().getFullYear())}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
queryParams.pageNum = 1;
queryParams.pageSize = 20;
getList();
};
@ -155,23 +111,10 @@ const handleExport = () => {
};
/** 多选 */
const handleSelectionChange = (selection: StatRow[]) => {
const handleSelectionChange = (selection: CheckRegistrationRow[]) => {
selectedRows.value = selection;
};
/** 切换每页条数 */
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
getList();
};
/** 翻页 */
const handlePageChange = (page: number) => {
currentPage.value = page;
getList();
};
onMounted(() => {
if (props.dimension === 'area') {
loadAreaDict();
@ -193,47 +136,38 @@ defineExpose({ calcTableHeight });
<!-- ==================== 搜索区域 ==================== -->
<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"
v-model="queryParams.areaName"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictLabel" />
</el-select>
</el-form-item>
<el-form-item v-else label="所属领域">
<el-select
v-model="queryParams.field"
v-model="queryParams.fieldName"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.value" />
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.label" />
</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-form-item label="年份">
<el-select v-model="queryParams.year" class="period-select">
<el-option v-for="y in yearOptions" :key="y" :label="y + '年'" :value="y" />
</el-select>
</el-form-item>
<el-form-item>
<el-form-item label="月份">
<el-date-picker
v-model="periodDate"
:type="pickerType"
:format="pickerFormat"
:value-format="pickerFormat"
v-model="queryParams.month"
type="month"
value-format="YYYY-MM"
class="period-date"
placeholder="选择月份"
/>
</el-form-item>
<el-form-item>
@ -244,7 +178,7 @@ defineExpose({ calcTableHeight });
</el-form>
</div>
<!-- ==================== 表格区域(表格样式对齐 /statistics/administrative/enterprise-enforcement) ==================== -->
<!-- ==================== 表格区域 ==================== -->
<div class="table-panel">
<div ref="tableRef">
<el-table
@ -257,45 +191,25 @@ defineExpose({ calcTableHeight });
@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 label="序号" width="90" align="center" type="index" />
<el-table-column prop="dimensionName" :label="dimension === 'area' ? '所属区' : '所属领域'" min-width="200" align="center" />
<el-table-column label="行政执法数" min-width="160" align="center">
<template #default="{ row }">
<span v-if="row.enforcementCount !== null">{{ row.enforcementCount }}次</span>
</template>
</el-table-column>
<el-table-column label="行政检查案件数" min-width="180" align="center">
<template #default="{ row }">
<span v-if="row.inspectionCaseCount !== null">{{ row.inspectionCaseCount }}次</span>
</template>
</el-table-column>
<el-table-column label="行政处罚案件数" min-width="180" align="center">
<template #default="{ row }">
<span v-if="row.penaltyCaseCount !== null">{{ row.penaltyCaseCount }}次</span>
</template>
</el-table-column>
<el-table-column label="行政处罚强制数" min-width="180" align="center">
<template #default="{ row }">
<span v-if="row.coerciveCount !== null">{{ row.coerciveCount }}次</span>
</template>
</el-table-column>
<el-table-column label="行政执法数" min-width="160" align="center" prop="enforcementCount" />
<el-table-column label="行政检查案件数" min-width="180" align="center" prop="inspectionCaseCount" />
<el-table-column label="行政处罚案件数" min-width="180" align="center" prop="penaltyCaseCount" />
<el-table-column label="行政处罚强制数" min-width="180" align="center" prop="coerciveCount" />
</el-table>
</div>
<!-- ==================== 分页(与 /statistics/administrative/enterprise-enforcement 保持一致) ==================== -->
<!-- ==================== 分页 ==================== -->
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
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="handleSizeChange"
@current-change="handlePageChange"
@size-change="getList"
@current-change="getList"
/>
</div>
</div>
@ -319,10 +233,6 @@ defineExpose({ calcTableHeight });
color: #333;
}
.search-input {
width: 210px;
}
.search-select {
width: 220px;
}
@ -337,9 +247,6 @@ defineExpose({ calcTableHeight });
}
/* ==================== 表格面板 ==================== */
/* el-table 不做任何自定义皮肤覆盖,与
efcode/report/registration/indexList.vue(/statistics/administrative/enterprise-enforcement)
保持 100% 一致:原生 border + stripe + 动态高度 */
.table-panel {
background: #fff;
border: 1px solid #e4e7ed;
@ -348,7 +255,7 @@ defineExpose({ calcTableHeight });
box-sizing: border-box;
}
/* ==================== 分页(元素合在一起靠右,与重点企业页一致) ==================== */
/* ==================== 分页 ==================== */
.mt-4 {
margin-top: 16px;
justify-content: flex-end;

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

@ -1,44 +1,35 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { getDicts } from '@/api/system/dict/data';
import { listKeyEnterprises, listKeyEnterpriseDetails } from '@/api/efcode/report/key-enterprises';
/** 企业行数据 */
interface EnterpriseRow {
id: number;
id: number | string;
enterpriseName: string;
enterpriseNumber?: string; // 统一社会信用代码/注册号 — 详情接口入参
areaName: string;
streetName: string;
enforcementCount: number | null;
}
// ======================== 查询条件 ========================
const now = new Date();
const queryParams = reactive({
pageNum: 1,
pageSize: 20,
enterpriseName: '' as string,
area: undefined as string | undefined,
street: undefined as string | undefined
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')}`
});
// 时段类型: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';
});
/** 年份选项(近 5 年) */
const yearOptions = (() => {
const curr = now.getFullYear();
return Array.from({ length: 5 }, (_, i) => String(curr - i));
})();
// ======================== 区域/街道字典 ========================
const areaOptions = ref<any[]>([]);
@ -55,7 +46,7 @@ const loadAreaDict = async () => {
/** 切换所属区时联动加载所属街道 */
const handleAreaChange = async (val: string) => {
queryParams.street = undefined;
queryParams.streetName = undefined;
streetOptions.value = [];
if (!val) return;
try {
@ -70,8 +61,6 @@ const handleAreaChange = async (val: string) => {
const loading = ref(false);
const tableData = ref<EnterpriseRow[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const selectedRows = ref<EnterpriseRow[]>([]);
// 表格高度(与 efcode/report/registration/indexList.vue 实现一致)
@ -85,25 +74,12 @@ const calcTableHeight = () => {
tableHeight.value = window.innerHeight - rect.top - 84;
};
/** 构造设计稿中的占位数据(前2条有数据,其余为空行) */
const buildMockRows = (): EnterpriseRow[] => {
const rows: EnterpriseRow[] = [
{ id: 1, enterpriseName: '深圳市宝安区西乡佳创电子厂', areaName: '宝安区', streetName: '光明街道', enforcementCount: 996 },
{ id: 2, enterpriseName: '深圳市黄金屋真空科技有限公司', areaName: '龙华区', streetName: '福海街道', enforcementCount: 990 }
];
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 listKeyEnterprise({ ...queryParams, periodType: periodType.value, periodDate: periodDate.value, pageNum: currentPage.value, pageSize: pageSize.value });
tableData.value = buildMockRows();
total.value = 26064;
const res = await listKeyEnterprises(queryParams);
tableData.value = (res.rows || []) as EnterpriseRow[];
total.value = res.total || 0;
} finally {
loading.value = false;
}
@ -111,20 +87,20 @@ const getList = async () => {
/** 搜索 */
const handleSearch = () => {
currentPage.value = 1;
queryParams.pageNum = 1;
getList();
};
/** 重置 */
const handleReset = () => {
queryParams.enterpriseName = '';
queryParams.area = undefined;
queryParams.street = undefined;
queryParams.districtName = undefined;
queryParams.streetName = undefined;
streetOptions.value = [];
periodType.value = 'month';
periodDate.value = '2026-09';
currentPage.value = 1;
pageSize.value = 20;
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();
};
@ -151,48 +127,17 @@ 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;
// 真实接口:enterpriseNumber(企业注册号)+ enforcementTime(按年-月筛选)
const res = await listKeyEnterpriseDetails({
enterpriseNumber: detailEnterprise.value.enterpriseNumber,
enforcementTime: queryParams.statMonth
});
detailTableData.value = (res as any)?.rows || (Array.isArray(res) ? res : []);
detailTotal.value = (res as any)?.total ?? detailTableData.value.length;
} finally {
detailLoading.value = false;
}
@ -223,19 +168,6 @@ const handleSelectionChange = (selection: EnterpriseRow[]) => {
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();
@ -266,7 +198,7 @@ onUnmounted(() => {
</el-form-item>
<el-form-item label="所属区">
<el-select
v-model="queryParams.area"
v-model="queryParams.districtName"
placeholder="请选择"
class="search-select"
clearable
@ -277,7 +209,7 @@ onUnmounted(() => {
</el-form-item>
<el-form-item label="所属街道">
<el-select
v-model="queryParams.street"
v-model="queryParams.streetName"
placeholder="请选择"
class="search-select"
clearable
@ -285,18 +217,18 @@ onUnmounted(() => {
<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-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>
<el-form-item label="月份">
<el-date-picker
v-model="periodDate"
:type="pickerType"
:format="pickerFormat"
:value-format="pickerFormat"
v-model="queryParams.statMonth"
type="month"
value-format="YYYY-MM"
class="period-date"
placeholder="选择月份"
/>
</el-form-item>
<el-form-item>
@ -351,14 +283,14 @@ onUnmounted(() => {
<!-- ==================== 分页 ==================== -->
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
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="handleSizeChange"
@current-change="handlePageChange"
@size-change="getList"
@current-change="getList"
/>
</div>
@ -366,7 +298,7 @@ onUnmounted(() => {
<el-dialog
v-model="detailDialogVisible"
width="1100px"
:title="`${detailEnterprise?.enterpriseName || ''} — ${periodDate} 执法明细`"
:title="`${detailEnterprise?.enterpriseName || ''} — ${queryParams.statYear}年${queryParams.statMonth} 执法明细`"
append-to-body
top="5vh"
destroy-on-close
@ -375,7 +307,7 @@ onUnmounted(() => {
v-if="detailEnterprise"
:closable="false"
show-icon
:title="`所属区:${detailEnterprise.areaName} | 所属街道:${detailEnterprise.streetName} | 行政执法次数:${detailEnterprise.enforcementCount ?? 0}次 | 时段:${periodType} / ${periodDate}`"
:title="`所属区:${detailEnterprise.areaName} | 所属街道:${detailEnterprise.streetName} | 行政执法次数:${detailEnterprise.enforcementCount ?? 0}次 | 时段:${queryParams.statYear}年${queryParams.statMonth}`"
type="info"
class="mb-4"
/>

208
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/scan-district/index.vue

@ -17,17 +17,109 @@ interface DistrictRow {
// ======================== 查询条件 ========================
const queryParams = reactive({
area: undefined as string | undefined
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
});
const periodType = ref('month');
const periodOptions = [
{ label: '月度', value: 'month' }
// 时段模式: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 periodDate = ref<string>('2026-09');
const pickerType = computed<'week' | 'month' | 'year'>(() => 'month');
const pickerFormat = computed(() => 'YYYY-MM');
// 年份选项(近几年)
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[]>([]);
@ -58,28 +150,10 @@ const calcTableHeight = () => {
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;
}
@ -92,8 +166,15 @@ const handleSearch = () => {
const handleReset = () => {
queryParams.area = undefined;
periodType.value = 'month';
periodDate.value = '2026-09';
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();
@ -137,36 +218,11 @@ 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;
}
@ -192,6 +248,7 @@ const handleDetailPageChange = (page: number) => {
onMounted(() => {
loadAreaDict();
applyYearFilter();
getList();
nextTick(() => calcTableHeight());
window.addEventListener('resize', calcTableHeight);
@ -219,18 +276,33 @@ onUnmounted(() => {
</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 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>
<el-date-picker
v-model="periodDate"
:type="pickerType"
:format="pickerFormat"
:value-format="pickerFormat"
class="period-date"
/>
<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>
@ -287,7 +359,7 @@ onUnmounted(() => {
<el-dialog
v-model="detailDialogVisible"
width="1100px"
:title="`${detailArea?.areaName || ''} — ${periodDate} 扫码明细`"
:title="`${detailArea?.areaName || ''} — ${timePeriodText} 扫码明细`"
append-to-body
top="5vh"
destroy-on-close
@ -296,7 +368,7 @@ onUnmounted(() => {
v-if="detailArea"
:closable="false"
show-icon
:title="`商事主体:${detailArea.enterpriseCount}户 | 行政执法:${detailArea.adminEnforceCount}条 | 非行政执法:${detailArea.nonAdminEnforceCount}条 | 时段:${periodDate}`"
:title="`商事主体:${detailArea.enterpriseCount}户 | 行政执法:${detailArea.adminEnforceCount}条 | 非行政执法:${detailArea.nonAdminEnforceCount}条 | 时段:${timePeriodText}`"
type="info"
class="mb-4"
/>
@ -362,14 +434,6 @@ onUnmounted(() => {
.search-select {
width: 220px;
}
.period-select {
width: 110px;
}
.period-date {
width: 150px;
}
}
.table-panel {

200
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/scan-enforcer/index.vue

@ -1,32 +1,32 @@
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { getDicts } from '@/api/system/dict/data';
/** 表格行数据(执法人员维度) */
interface EnforcerRow {
id: number;
enforcerName: string;
enforceCount: number;
areaName: string;
bureauName: string;
}
import { listLawEnforcer, listLawEnforcerDetails, type EnforcerRow, type LawEnforcerDetailRow } from '@/api/efcode/report/law-enforcer';
// ======================== 查询条件 ========================
const queryParams = reactive({
enforcerName: '' as string,
pageNum: 1,
pageSize: 20,
lawEnforcer: '' as string, // 执法人员关键字(接口参数名)
area: undefined as string | undefined,
field: undefined as string | undefined,
periodType: 'year' as string,
year: String(new Date().getFullYear()),
enforcementTime: String(new Date().getFullYear()),
orderByColumn: '' as string,
isAsc: '' as string
});
const periodOptions = [
{ label: '周度', value: 'week' },
{ label: '月度', value: 'month' },
{ label: '季度', value: 'quarter' },
{ label: '年度', value: 'year' }
// 时段模式:year | quarter | month
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 }
];
/** 年份选项(近 5 年) */
@ -35,9 +35,33 @@ const yearOptions = (() => {
return Array.from({ length: 5 }, (_, i) => String(curr - i));
})();
/** 同步 enforcementTime(按当前时段模式拼值) */
const syncEnforcementTime = () => {
if (timeMode.value === 'year') {
queryParams.enforcementTime = selectedYear.value;
} else if (timeMode.value === 'quarter') {
queryParams.enforcementTime = `${selectedYear.value}-Q${selectedQuarter.value}`;
} else if (timeMode.value === 'month') {
queryParams.enforcementTime = selectedMonth.value;
}
};
/** 时段模式变更 */
const handleTimeModeChange = () => {
if (timeMode.value === 'month' && !selectedMonth.value) {
const n = new Date();
selectedMonth.value = `${n.getFullYear()}-${String(n.getMonth() + 1).padStart(2, '0')}`;
}
syncEnforcementTime();
};
const handleYearChange = () => syncEnforcementTime();
const handleQuarterChange = () => syncEnforcementTime();
const handleMonthChange = () => syncEnforcementTime();
// ======================== 字典 ========================
const areaOptions = ref<any[]>([]);
const fieldOptions = ref([
const fieldOptions = [
{ label: '生态环境领域', value: 'ecology' },
{ label: '卫生健康领域', value: 'health' },
{ label: '应急管理领域', value: 'emergency' },
@ -45,7 +69,7 @@ const fieldOptions = ref([
{ label: '市场监管领域', value: 'market' },
{ label: '消防救援领域', value: 'fire' },
{ label: '规划和自然资源领域', value: 'planning' }
]);
];
const loadAreaDict = async () => {
try {
@ -60,8 +84,6 @@ const loadAreaDict = async () => {
const loading = ref(false);
const tableData = ref<EnforcerRow[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const selectedRows = ref<EnforcerRow[]>([]);
const tableHeight = ref<number | undefined>(undefined);
@ -73,44 +95,38 @@ const calcTableHeight = () => {
tableHeight.value = window.innerHeight - rect.top - 84;
};
const buildMockRows = (): EnforcerRow[] => {
return [
{ id: 1, enforcerName: '张清风', enforceCount: 14421, areaName: '深圳市', bureauName: '深圳市应急管理局' },
{ id: 2, enforcerName: '周志南', enforceCount: 11415, areaName: '宝安区', bureauName: '深圳市宝安区生态环境局' },
{ id: 3, enforcerName: '吴栩曦', enforceCount: 7040, areaName: '盐田区', bureauName: '深圳市盐田区住房和城乡建设局' },
{ id: 4, enforcerName: '段子曦', enforceCount: 6135, areaName: '光明区', bureauName: '深圳市光明区交通运输局' },
{ id: 5, enforcerName: '吴海羽', enforceCount: 5424, areaName: '龙岗区', bureauName: '深圳市龙岗区生态环境局' },
{ id: 6, enforcerName: '张文业', enforceCount: 5064, areaName: '龙华区', bureauName: '深圳市龙华区交通运输局' },
{ id: 7, enforcerName: '段宝腾', enforceCount: 4245, areaName: '坪山区', bureauName: '深圳市坪山区交通运输局' }
];
};
const getList = async () => {
loading.value = true;
try {
// TODO: 接入真实接口 listEnforcerScanData
tableData.value = buildMockRows();
total.value = 658;
const res = await listLawEnforcer(queryParams);
tableData.value = (res as any)?.rows || [];
total.value = (res as any)?.total || 0;
} catch (e) {
console.warn('[scan-enforcer] 列表接口调用失败', e);
} finally {
loading.value = false;
}
};
const handleSearch = () => {
currentPage.value = 1;
queryParams.pageNum = 1;
getList();
};
const handleReset = () => {
queryParams.enforcerName = '';
const n = new Date();
queryParams.lawEnforcer = '';
queryParams.area = undefined;
queryParams.field = undefined;
queryParams.periodType = 'year';
queryParams.year = String(new Date().getFullYear());
queryParams.orderByColumn = '';
queryParams.isAsc = '';
currentPage.value = 1;
pageSize.value = 20;
queryParams.pageNum = 1;
queryParams.pageSize = 20;
timeMode.value = 'year';
selectedYear.value = String(n.getFullYear());
selectedQuarter.value = Math.ceil((n.getMonth() + 1) / 3);
selectedMonth.value = '';
syncEnforcementTime();
getList();
};
@ -122,17 +138,6 @@ const handleSelectionChange = (selection: EnforcerRow[]) => {
selectedRows.value = selection;
};
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
getList();
};
const handlePageChange = (page: number) => {
currentPage.value = page;
getList();
};
/** 执法次数列排序 */
const handleSortChange = (sortObj: { prop: string; order: 'ascending' | 'descending' | null }) => {
if (sortObj.prop === 'enforceCount') {
@ -142,46 +147,34 @@ const handleSortChange = (sortObj: { prop: string; order: 'ascending' | 'descend
queryParams.orderByColumn = '';
queryParams.isAsc = '';
}
currentPage.value = 1;
queryParams.pageNum = 1;
getList();
};
// ======================== 详情弹窗 ========================
interface ScanDetailRow {
id: number;
bureauDeptName: string;
lawEnforcer: string;
scanTime: string;
enterpriseName: string;
enforcementItem: string;
}
const detailDialogVisible = ref(false);
const detailEnforcer = ref<EnforcerRow | null>(null);
const detailLoading = ref(false);
const detailTableData = ref<ScanDetailRow[]>([]);
const detailTableData = ref<LawEnforcerDetailRow[]>([]);
const detailTotal = ref(0);
const detailPage = ref(1);
const detailPageSize = ref(20);
/** 构造执法详情 mock 数据(与设计稿一致) */
const buildDetailMockRows = (): ScanDetailRow[] => {
return [
{ id: 1, bureauDeptName: '深圳市罗湖区住房和建设局', lawEnforcer: '王启鹏, 杨飞', scanTime: '2026-09-18 11:33:17', enterpriseName: '上海建工集团股份有限公司', enforcementItem: '排水检查' },
{ id: 2, bureauDeptName: '深圳市罗湖区住房和建设局', lawEnforcer: '王启鹏, 张清风', scanTime: '2026-09-18 11:22:40', enterpriseName: '深圳市海恒酒店管理有限公司', enforcementItem: '治安检查' },
{ id: 3, bureauDeptName: '深圳市罗湖区住房和建设局', lawEnforcer: '王启鹏, 杨明', scanTime: '2026-09-17 09:44:58', enterpriseName: '深圳市京九节能环保科技有限公司', enforcementItem: '治安检查' },
{ id: 4, bureauDeptName: '深圳市罗湖区住房和建设局', lawEnforcer: '王启鹏, 杨明', scanTime: '2026-09-16 21:25:10', enterpriseName: '深圳市京九节能环保科技有限公司', enforcementItem: '治安检查' },
{ id: 5, bureauDeptName: '深圳市罗湖区住房和建设局', lawEnforcer: '王启鹏, 李云', scanTime: '2026-09-16 21:25:10', enterpriseName: '深圳市海恒酒店管理有限公司', enforcementItem: '治安检查' }
];
};
const getDetailList = async () => {
detailLoading.value = true;
try {
if (!detailEnforcer.value) return;
// TODO: 接入真实接口 listEnforcerScanDetail
detailTableData.value = buildDetailMockRows();
detailTotal.value = 658;
if (!detailEnforcer.value?.lawEnforcerId) return;
const res = await listLawEnforcerDetails(detailEnforcer.value.lawEnforcerId);
// 后端可能返回 { rows, total } 或直接数组
if ((res as any)?.rows) {
detailTableData.value = (res as any).rows;
detailTotal.value = (res as any).total ?? detailTableData.value.length;
} else if (Array.isArray(res)) {
detailTableData.value = res as LawEnforcerDetailRow[];
detailTotal.value = detailTableData.value.length;
}
} catch (e) {
console.warn('[scan-enforcer] 详情接口调用失败', e);
} finally {
detailLoading.value = false;
}
@ -225,7 +218,7 @@ onUnmounted(() => {
<el-form :inline="true" label-colon>
<el-form-item label="执法人员">
<el-input
v-model="queryParams.enforcerName"
v-model="queryParams.lawEnforcer"
placeholder="请输入关键字"
class="search-input"
clearable
@ -239,7 +232,7 @@ onUnmounted(() => {
class="search-select"
clearable
>
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
<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="所属领域">
@ -249,18 +242,38 @@ onUnmounted(() => {
class="search-select"
clearable
>
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.value" />
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.label" />
</el-select>
</el-form-item>
<el-form-item label="时段">
<el-select v-model="queryParams.periodType" class="period-select">
<el-option v-for="item in periodOptions" :key="item.value" :label="item.label" :value="item.value" />
<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-select>
</el-form-item>
<el-form-item>
<el-select v-model="queryParams.year" class="year-select">
<el-form-item v-if="timeMode === 'year'">
<el-select v-model="selectedYear" class="period-select" @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" class="period-select" @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>
<el-button type="primary" @click="handleSearch">搜索</el-button>
@ -298,14 +311,14 @@ onUnmounted(() => {
</div>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
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="handleSizeChange"
@current-change="handlePageChange"
@size-change="getList"
@current-change="getList"
/>
</div>
</div>
@ -387,10 +400,6 @@ onUnmounted(() => {
.period-select {
width: 110px;
}
.year-select {
width: 110px;
}
}
.table-panel {
@ -405,7 +414,4 @@ onUnmounted(() => {
margin-top: 16px;
justify-content: flex-end;
}
.mb-4 {
margin-bottom: 16px;
}
</style>

14
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/trend/scan-field/index.vue

@ -71,24 +71,10 @@ const calcTableHeight = () => {
tableHeight.value = window.innerHeight - rect.top - 84;
};
const buildMockRows = (): FieldRow[] => {
return [
{ id: 1, fieldName: '生态环境领域', adminEnforceCount: 14421, adminPercent: '60%', nonAdminEnforceCount: 50384, nonAdminPercent: '60%' },
{ id: 2, fieldName: '卫生健康领域', adminEnforceCount: 11415, adminPercent: '56.64%', nonAdminEnforceCount: 22017, nonAdminPercent: '56.64%' },
{ id: 3, fieldName: '应急管理领域', adminEnforceCount: 7040, adminPercent: '70%', nonAdminEnforceCount: 9961, nonAdminPercent: '70%' },
{ id: 4, fieldName: '交通运输领域', adminEnforceCount: 6135, adminPercent: '80%', nonAdminEnforceCount: 888, nonAdminPercent: '80%' },
{ id: 5, fieldName: '市场监管领域', adminEnforceCount: 5424, adminPercent: '67.5%', nonAdminEnforceCount: 389, nonAdminPercent: '67.5%' },
{ id: 6, fieldName: '消防救援领域', adminEnforceCount: 5064, adminPercent: '63.2%', nonAdminEnforceCount: 184, nonAdminPercent: '63.2%' },
{ id: 7, fieldName: '规划和自然资源领域', adminEnforceCount: 4245, adminPercent: '78.34%', nonAdminEnforceCount: 137, nonAdminPercent: '78.34%' }
];
};
const getList = async () => {
loading.value = true;
try {
// TODO: 接入真实接口 listFieldScanData
tableData.value = buildMockRows();
total.value = 658;
} finally {
loading.value = false;
}

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

@ -1,39 +1,34 @@
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { getDicts } from '@/api/system/dict/data';
import { listSituationTrendArea, listSituationTrendDomain, type SituationRow } from '@/api/efcode/report/situation-trend';
/** 维度:area=各区域 field=各领域 */
const props = defineProps<{
dimension: 'area' | 'field';
}>();
/** 态势趋势综合评分行 */
interface SituationRow {
id: number;
dimensionName: string;
yearQuarter: string; // 如 2026Q3
score: string | null; // 优良 / 良好 / 一般
}
// ======================== 查询条件 ========================
const now = new Date();
const currYear = String(now.getFullYear());
const currQuarter = String(Math.ceil((now.getMonth() + 1) / 3));
const queryParams = reactive({
area: undefined as string | undefined,
field: undefined as string | undefined
pageNum: 1,
pageSize: 20,
districtName: undefined as string | undefined, // 区域 tab 用
lymc: undefined as string | undefined, // 领域 tab 用(接口字段名 lymc)
year: currYear,
quarter: currQuarter
});
// 时段类型:week=周度 month=月度 quarter=季度 year=年度
const periodType = ref('quarter');
const periodOptions = [
// { label: '周度', value: 'week' },
// { label: '月度', value: 'month' },
{ label: '季度', value: 'quarter' },
// { label: '年度', value: 'year' }
];
/** 年份选项(近 5 年) */
const yearOptions = (() => {
const curr = now.getFullYear();
return Array.from({ length: 5 }, (_, i) => String(curr - i));
})();
// 年份(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' },
@ -65,60 +60,51 @@ const loadAreaDict = async () => {
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;
// 前端是单点年+季度选择,后端接口参数要求 startYear/endYear + startQuarter/endQuarter
const baseParams = {
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize,
startYear: queryParams.year,
endYear: queryParams.year,
startQuarter: queryParams.quarter,
endQuarter: queryParams.quarter
};
let rows: SituationRow[] = [];
let totalNum = 0;
if (props.dimension === 'area') {
const res = await listSituationTrendArea({
...baseParams,
districtName: queryParams.districtName
});
rows = (res as any)?.rows || [];
totalNum = (res as any)?.total || 0;
} else {
const res = await listSituationTrendDomain({
...baseParams,
lymc: queryParams.lymc
});
rows = (res as any)?.rows || [];
totalNum = (res as any)?.total || 0;
}
tableData.value = rows;
total.value = totalNum;
} catch (e) {
console.warn('[situation-trend] 列表接口调用失败', e);
} finally {
loading.value = false;
}
@ -126,19 +112,19 @@ const getList = async () => {
/** 搜索 */
const handleSearch = () => {
currentPage.value = 1;
queryParams.pageNum = 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;
const n = new Date();
queryParams.districtName = undefined;
queryParams.lymc = undefined;
queryParams.year = String(n.getFullYear());
queryParams.quarter = String(Math.ceil((n.getMonth() + 1) / 3));
queryParams.pageNum = 1;
queryParams.pageSize = 20;
getList();
};
@ -152,19 +138,6 @@ 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();
@ -175,6 +148,8 @@ onMounted(() => {
onUnmounted(() => {
window.removeEventListener('resize', calcTableHeight);
});
defineExpose({ calcTableHeight });
</script>
<template>
@ -184,40 +159,31 @@ onUnmounted(() => {
<el-form :inline="true" label-colon>
<el-form-item v-if="dimension === 'area'" label="所属区">
<el-select
v-model="queryParams.area"
v-model="queryParams.districtName"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictLabel" />
</el-select>
</el-form-item>
<el-form-item v-else label="所属领域">
<el-select
v-model="queryParams.field"
v-model="queryParams.lymc"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.value" />
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.label" />
</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-form-item label="年份">
<el-select v-model="queryParams.year" 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="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-form-item label="季度">
<el-select v-model="queryParams.quarter" class="quarter-select">
<el-option v-for="item in quarterOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
@ -242,11 +208,7 @@ onUnmounted(() => {
@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 label="序号" width="90" align="center" type="index" />
<el-table-column
prop="dimensionName"
:label="dimension === 'area' ? '所属区' : '所属领域'"
@ -255,27 +217,27 @@ onUnmounted(() => {
/>
<el-table-column label="年+季度" width="160" align="center">
<template #default="{ row }">
{{ row.yearQuarter }}
{{ 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>
<span v-if="row.score !== null && row.score !== undefined">{{ row.score }}</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- ==================== 分页(统一紧凑右对齐) ==================== -->
<!-- ==================== 分页 ==================== -->
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
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="handleSizeChange"
@current-change="handlePageChange"
@size-change="getList"
@current-change="getList"
/>
</div>
</div>
@ -307,12 +269,8 @@ onUnmounted(() => {
width: 110px;
}
.year-picker {
width: 130px;
}
.quarter-select {
width: 150px;
width: 130px;
}
}
@ -325,7 +283,7 @@ onUnmounted(() => {
box-sizing: border-box;
}
/* ==================== 分页(元素合在一起靠右,与其他 trend 页一致) ==================== */
/* ==================== 分页 ==================== */
.mt-4 {
margin-top: 16px;
justify-content: flex-end;

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

@ -1,39 +1,33 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { getDicts } from '@/api/system/dict/data';
import { listTrendBehaviorArea, listTrendBehaviorDomain, type TrendBehaviorRow } from '@/api/efcode/report/trend-behavior';
/** 维度:area=各区域 field=各领域 */
const props = defineProps<{
dimension: 'area' | 'field';
}>();
/** 趋势行为监测行数据 */
interface TrendBehaviorRow {
id: number;
dimensionName: string;
yearQuarter: string; // 如 2026Q3
enforcementCount: number | null;
}
// ======================== 查询条件 ========================
const now = new Date();
// 当前季度
const currentQuarter = Math.ceil((now.getMonth() + 1) / 3);
const queryParams = reactive({
area: undefined as string | undefined,
field: undefined as string | undefined
pageNum: 1,
pageSize: 20,
districtName: undefined as string | undefined, // 区域 tab 用
domainName: undefined as string | undefined, // 领域 tab 用
year: String(now.getFullYear()),
quarter: String(currentQuarter)
});
// 时段类型:week=周度 month=月度 quarter=季度 year=年度
const periodType = ref('quarter');
const periodOptions = [
// { label: '周度', value: 'week' },
// { label: '月度', value: 'month' },
{ label: '季度', value: 'quarter' },
// { label: '年度', value: 'year' }
];
/** 年份选项(近 5 年) */
const yearOptions = (() => {
const curr = now.getFullYear();
return Array.from({ length: 5 }, (_, i) => String(curr - i));
})();
// 年份(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' },
@ -41,6 +35,9 @@ const quarterOptions = [
{ label: '第四季度', value: '4' }
];
/** 拼接 quarterTime 传给后端(格式:2026-Q3) */
const buildQuarterTime = () => `${queryParams.year}-Q${queryParams.quarter}`;
// ======================== 字典 ========================
const areaOptions = ref<any[]>([]);
const fieldOptions = [
@ -65,60 +62,45 @@ const loadAreaDict = async () => {
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;
const qt = buildQuarterTime();
let rows: TrendBehaviorRow[] = [];
let totalNum = 0;
if (props.dimension === 'area') {
const res = await listTrendBehaviorArea({
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize,
districtName: queryParams.districtName,
quarterTime: qt
});
rows = (res as any)?.rows || [];
totalNum = (res as any)?.total || 0;
} else {
const res = await listTrendBehaviorDomain({
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize,
domainName: queryParams.domainName,
quarterTime: qt
});
rows = (res as any)?.rows || [];
totalNum = (res as any)?.total || 0;
}
tableData.value = rows;
total.value = totalNum;
} finally {
loading.value = false;
}
@ -126,19 +108,19 @@ const getList = async () => {
/** 搜索 */
const handleSearch = () => {
currentPage.value = 1;
queryParams.pageNum = 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;
const now2 = new Date();
queryParams.districtName = undefined;
queryParams.domainName = undefined;
queryParams.year = String(now2.getFullYear());
queryParams.quarter = String(Math.ceil((now2.getMonth() + 1) / 3));
queryParams.pageNum = 1;
queryParams.pageSize = 20;
getList();
};
@ -152,25 +134,6 @@ 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();
@ -181,6 +144,8 @@ onMounted(() => {
onUnmounted(() => {
window.removeEventListener('resize', calcTableHeight);
});
defineExpose({ calcTableHeight });
</script>
<template>
@ -190,40 +155,31 @@ onUnmounted(() => {
<el-form :inline="true" label-colon>
<el-form-item v-if="dimension === 'area'" label="所属区">
<el-select
v-model="queryParams.area"
v-model="queryParams.districtName"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
<el-option v-for="item in areaOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictLabel" />
</el-select>
</el-form-item>
<el-form-item v-else label="所属领域">
<el-select
v-model="queryParams.field"
v-model="queryParams.domainName"
placeholder="请选择"
class="search-select"
clearable
>
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.value" />
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.label" />
</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-form-item label="年份">
<el-select v-model="queryParams.year" 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="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-form-item label="季度">
<el-select v-model="queryParams.quarter" class="quarter-select">
<el-option v-for="item in quarterOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
@ -248,11 +204,7 @@ onUnmounted(() => {
@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 label="序号" width="90" align="center" type="index" />
<el-table-column
prop="dimensionName"
:label="dimension === 'area' ? '所属区' : '所属领域'"
@ -261,27 +213,27 @@ onUnmounted(() => {
/>
<el-table-column label="年+季度" width="160" align="center">
<template #default="{ row }">
{{ formatYearQuarter(row) }}
{{ row.yearQuarter || '' }}
</template>
</el-table-column>
<el-table-column label="行政执法次数" width="180" align="center">
<template #default="{ row }">
<span v-if="row.enforcementCount !== null">{{ row.enforcementCount }}次</span>
<span v-if="row.enforcementCount !== null && row.enforcementCount !== undefined">{{ 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"
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="handleSizeChange"
@current-change="handlePageChange"
@size-change="getList"
@current-change="getList"
/>
</div>
</div>
@ -313,10 +265,6 @@ onUnmounted(() => {
width: 110px;
}
.year-picker {
width: 130px;
}
.quarter-select {
width: 150px;
}
@ -331,7 +279,7 @@ onUnmounted(() => {
box-sizing: border-box;
}
/* ==================== 分页(元素合在一起靠右,与其他 trend 页一致) ==================== */
/* ==================== 分页 ==================== */
.mt-4 {
margin-top: 16px;
justify-content: flex-end;

3
zdxt-web-client/zdxt-efcode-enforcement-app/src/api/nos/index.js

@ -15,6 +15,9 @@ export const nosListPageApi = (params, success, error) => _post("/app-enf/nos-la
export const nosInfoApi = (params, success, error) => _get("/app-enf/nos-law-enforcement-task/detail", params, success, error);
// 审核非现场执法任务
export const nosApproveApi = (params, success, error) => _post("/app-enf/nos-law-enforcement-task/approve", params, success, error);
// 核实案件(待核实状态)
export const nosVerifyCaseApi = (params, success, error) => _post("/app-enf/nos-law-enforcement-task/verify-case", params, success, error);
// 查询非现场执法项目任务类别
export const nosTypeApi = (params, success, error) => _post("/app-enf/nos-law-enforcement-task/type", params, success, error);
//查询非现场执法项目任务来源列表

32
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/nos/nosDetail.vue

@ -39,10 +39,14 @@
</van-tab>
</van-tabs>
<div v-if="activeTab === 'materials' && Number(taskDetail.taskStatus) === 3" class="detail-footer">
<van-button class="btn-cancel" @click="onCloudRecord">云笔录</van-button>
<van-button class="btn-cancel" @click="onCancel">取消</van-button>
<van-button class="btn-submit" type="primary" native-type="submit">提交</van-button>
<div v-if="activeTab === 'materials' && (Number(taskDetail.taskStatus) === 3 || (Number(taskDetail.taskStatus) === 6 && showVerifyBtn))" class="detail-footer">
<template v-if="Number(taskDetail.taskStatus) === 3">
<van-button class="btn-cancel" @click="onCloudRecord">云笔录</van-button>
<van-button class="btn-cancel" @click="onCancel">取消</van-button>
<van-button class="btn-submit" type="primary" native-type="submit">提交</van-button>
<van-button class="btn-cancel">转办案</van-button>
</template>
<van-button v-else class="btn-submit" type="primary">核实</van-button>
</div>
</van-form>
</div>
@ -52,7 +56,7 @@
import { showConfirmDialog, showSuccessToast, showToast } from 'vant'
import NosDetailTaskTab from './components/NosDetailTaskTab.vue'
import NosDetailMaterialsTab from './components/NosDetailMaterialsTab.vue'
import { nosInfoApi, nosApproveApi, cloudRecordStartApi } from '@/api/nos/index'
import { nosInfoApi, nosApproveApi, cloudRecordStartApi, nosVerifyCaseApi } from '@/api/nos/index'
import { toFullFileUrl } from '@/util/fileUrl'
const router = useRouter()
@ -73,6 +77,7 @@ const taskTabTitle = computed(() => TAB_TASK_TITLE_MAP[route.query.tab] || '行
const taskDetail = ref({})
const taskItemList = ref([])
const evaluateData = ref(null)
const showVerifyBtn = ref(false)
const inspectResult = ref({
checkResult: '',
@ -143,6 +148,20 @@ const fetchTaskDetail = async () => {
const res = await nosInfoApi({ taskId })
if (res && res.code === 200 && res.data) {
applyDetailData(res.data)
// 待核实状态下,调用 verify-case 接口判断是否显示核实按钮
if (Number(res.data.taskStatus) === 6) {
try {
const verifyRes = await nosVerifyCaseApi({ taskId })
if (verifyRes && verifyRes.code === 200) {
showVerifyBtn.value = true
} else {
showVerifyBtn.value = false
}
} catch (verifyErr) {
console.error('verify-case error:', verifyErr)
showVerifyBtn.value = false
}
}
}
} catch (err) {
console.error(err)
@ -341,6 +360,7 @@ const onSubmit = async () => {
.detail-footer {
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px 16px;
background: #fff;
@ -348,7 +368,7 @@ const onSubmit = async () => {
.btn-cancel,
.btn-submit {
flex: 1;
width: 100%;
height: 44px;
border-radius: 22px;
font-size: 16px;

4
zdxt-web-client/zdxt-efcode-enforcement-app/vite-env/.env.enforcement-dev-wmy

@ -5,8 +5,8 @@ VITE_CONTEXT_PATH = '/efcode20-enf-app/'
# VITE_PROXY_TARGET = 'http://139.186.148.21:8080'
# VITE_API_URL = 'http://192.168.1.17:8080'
# VITE_PROXY_TARGET = 'http://172.16.21.213:8080'
VITE_PROXY_TARGET = 'http://172.16.21.93:8080'
# VITE_PROXY_TARGET = 'http://172.16.20.72:8080'
# VITE_PROXY_TARGET = 'http://172.16.21.93:8080'
VITE_PROXY_TARGET = 'http://172.16.20.72:8080'
VITE_DEFAULT_LOGIN = '/enforcement/loginByPassword'
VITE_APP_ENV = 'dev'
VITE_FILE_URL = ''

2
zdxt-web-client/zdxt-efcode-enterprise-app/src/components/enterprise/footerNav.vue

@ -7,7 +7,7 @@
<!-- <van-tabbar-item name="Classify" replace icon="bell" to="/enterprise/lawQuery">
法律查询
</van-tabbar-item> -->
<van-tabbar-item name="Classify" replace icon="notes-o" to="">
<van-tabbar-item name="Classify" replace icon="notes-o" to="/enterprise/lawQuery">
法律查询
</van-tabbar-item>
<van-tabbar-item name="Classify" push to="/enterprise/nos">

7
zdxt-web-client/zdxt-efcode-enterprise-app/src/router/enterprise-app/ent-main-router.js

@ -93,6 +93,13 @@ const routes = [
meta: { title: "法律查询" },
component: () => import("@/views/enterprise/law/lawQuery.vue"),
},
// 法律详情
{
path: "/enterprise/lawDetail",
name: "lawDetail",
meta: { title: "法律详情" },
component: () => import("@/views/enterprise/law/lawDetail.vue"),
},
// 普法服务列表
{
path: "/enterprise/lawServiceList",

54
zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/enforcement/enforcementRegistrationList.vue

@ -2,6 +2,18 @@
<div class="registration-list-page">
<SearchFilterBar ref="searchFilterBarRef" @search="onSearchFilter" />
<!-- Tab 切换:行政 / 非行政 -->
<van-tabs
v-model:active="activeTab"
class="reg-tabs"
sticky
swipeable
@change="onTabChange"
>
<van-tab title="行政" name="1"></van-tab>
<van-tab title="非行政" name="2"></van-tab>
</van-tabs>
<!-- 可滚动列表区域 -->
<div class="list-container">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
@ -66,6 +78,9 @@ const router = useRouter();
const searchFilterBarRef = ref(null);
// ========== Tab 切换:1=行政,2=非行政 ==========
const activeTab = ref('1');
// ========== 列表状态 ==========
const list = ref([]);
const loading = ref(false);
@ -116,6 +131,7 @@ const fetchList = () => {
const params = {
pageNum: pageNum.value,
pageSize: pageSize.value,
lawType: activeTab.value,
// enterpriseNumber: useCommon.enterpriseNumber,
};
if (searchText.value) {
@ -181,6 +197,15 @@ const onLoad = () => {
fetchList();
};
// ========== Tab 切换 ==========
const onTabChange = () => {
pageNum.value = 1;
list.value = [];
finished.value = false;
loading.value = true;
fetchList();
};
// ========== 下拉刷新 ==========
const onRefresh = () => {
pageNum.value = 1;
@ -230,6 +255,35 @@ const toEvaluate = (id, checkItem) => {
height: 100%;
background-color: #fbfdff;
.reg-tabs {
flex-shrink: 0;
:deep(.van-tabs__wrap) {
background: #fff;
border-radius: 12px 12px 0 0;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
:deep(.van-tab) {
font-size: 15px;
color: #999;
width: 100%;
}
:deep(.van-tab--active) {
font-weight: 600;
color: #333;
}
:deep(.van-tabs__line) {
background: #2b7de9;
width: 40%;
height: 2px;
border-radius: 2px;
}
}
.list-container {
flex: 1;
overflow-y: auto;

368
zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/law/lawQuery.vue

@ -1,8 +1,8 @@
<template>
<div class="law-query-page">
<!-- 顶部导航栏 -->
<van-nav-bar title="法律查询" left-text="关闭" left-arrow @click-left="onClickLeft">
</van-nav-bar>
<!-- <van-nav-bar title="法律查询" left-arrow @click-left="onClickLeft">
</van-nav-bar> -->
<!-- 内容区域 -->
<div class="content-area">
@ -11,63 +11,46 @@
<van-search
v-model="searchKeyword"
placeholder="请输入关键词搜索"
show-action
shape="round"
@search="onSearch"
>
<template #action>
<van-button size="small" type="primary" @click="onSearch">搜索</van-button>
</template>
</van-search>
/>
</div>
<!-- 法律分类 -->
<div class="category-section">
<div class="section-title">法律分类</div>
<div class="category-grid">
<div
class="category-item"
v-for="(item, index) in categoryList"
<!-- 法律法规列表 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
:offset="50"
>
<van-empty v-if="finished && lawList.length === 0" description="暂无数据" />
<div
class="law-item"
v-for="(item, index) in lawList"
:key="index"
@click="onCategoryClick(item)"
@click="onLawClick(item)"
>
<div class="category-icon" :class="item.iconBg">
<img :src="item.icon" alt="" />
<!-- 标题行 -->
<div class="item-title-row">
<div class="item-title">{{ item.title }}</div>
<span
class="status-tag"
:class="item.status === '有效' ? 'status--active' : 'status--pending'"
>{{ item.status }}</span>
</div>
<div class="category-name">{{ item.name }}</div>
</div>
</div>
</div>
<!-- 常用法律 -->
<div class="law-section">
<div class="section-title">常用法律</div>
<van-cell-group>
<van-cell
v-for="(law, index) in lawList"
:key="index"
:title="law.title"
:label="law.desc"
is-link
@click="onLawClick(law)"
/>
</van-cell-group>
</div>
<!-- 法律知识问答 -->
<div class="qa-section">
<div class="section-title">法律知识问答</div>
<div class="qa-list">
<div
class="qa-item"
v-for="(qa, index) in qaList"
:key="index"
@click="onQaClick(qa)"
>
<div class="qa-question">{{ qa.question }}</div>
<div class="qa-answer">{{ qa.answer }}</div>
<!-- 元信息行 -->
<div class="item-meta">
<span class="meta-text">公布日期: {{ item.publishDate }}</span>
<span class="meta-text">施行日期: {{ item.implementDate }}</span>
<span class="meta-text">{{ item.department }}</span>
</div>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
<!-- 底部导航栏 -->
@ -87,70 +70,82 @@ const router = useRouter();
// 搜索关键词
const searchKeyword = ref('');
// 法律分类列表
const categoryList = ref([
{ name: '宪法', icon: '', iconBg: 'icon-red' },
{ name: '民法', icon: '', iconBg: 'icon-blue' },
{ name: '刑法', icon: '', iconBg: 'icon-green' },
{ name: '行政法', icon: '', iconBg: 'icon-orange' },
{ name: '经济法', icon: '', iconBg: 'icon-purple' },
{ name: '劳动法', icon: '', iconBg: 'icon-yellow' },
{ name: '商法', icon: '', iconBg: 'icon-pink' },
{ name: '诉讼法', icon: '', iconBg: 'icon-cyan' },
]);
// 常用法律列表
const lawList = ref([
{ title: '中华人民共和国宪法', desc: '国家根本大法' },
{ title: '中华人民共和国民法典', desc: '社会生活的百科全书' },
{ title: '中华人民共和国刑法', desc: '规定犯罪与刑罚' },
{ title: '中华人民共和国行政法', desc: '规范行政行为' },
{ title: '中华人民共和国劳动法', desc: '保护劳动者权益' },
]);
// 法律知识问答列表
const qaList = ref([
{
question: '什么是行政处罚?',
answer: '行政处罚是指行政机关依法对违反行政管理秩序的公民、法人或者其他组织,以减损权益或者增加义务的方式予以惩戒的行为。'
},
{
question: '企业需要遵守哪些法律法规?',
answer: '企业需要遵守公司法、劳动法、税法、环境保护法、安全生产法等相关法律法规。'
},
{
question: '什么是行政复议?',
answer: '行政复议是指公民、法人或者其他组织认为具体行政行为侵犯其合法权益,向行政机关提出行政复议申请,行政机关受理并作出决定的活动。'
},
]);
// 列表状态
const lawList = ref([]);
const loading = ref(false);
const finished = ref(false);
const refreshing = ref(false);
const pageNum = ref(1);
const pageSize = ref(10);
// 模拟数据(与截图一致)
const mockData = [
{ title: '渔业行政处罚规定', status: '有效', publishDate: '2026-08-08', implementDate: '2026-08-18', department: '农业农村部' },
{ title: '渔业行政处罚规定', status: '尚未生效', publishDate: '2026-08-08', implementDate: '2026-08-18', department: '农业农村部' },
{ title: '渔业行政处罚规定', status: '有效', publishDate: '2026-08-08', implementDate: '2026-08-18', department: '农业农村部' },
{ title: '渔业行政处罚规定', status: '有效', publishDate: '2026-08-08', implementDate: '2026-08-18', department: '农业农村部' },
{ title: '渔业行政处罚规定', status: '有效', publishDate: '2026-08-08', implementDate: '2026-08-18', department: '农业农村部' },
{ title: '渔业行政处罚规定', status: '有效', publishDate: '2026-08-08', implementDate: '2026-08-18', department: '农业农村部' },
];
// 加载数据
const fetchList = () => {
// TODO: 替换为真实接口调用
// const params = { pageNum: pageNum.value, pageSize: pageSize.value, searchValue: searchKeyword.value };
// 模拟异步请求
setTimeout(() => {
if (refreshing.value) {
lawList.value = [];
refreshing.value = false;
}
lawList.value = [...lawList.value, ...mockData];
// 模拟只有一页数据
finished.value = true;
loading.value = false;
}, 300);
};
// 返回上一页
const onClickLeft = () => {
router.back();
// 触底加载
const onLoad = () => {
if (refreshing.value) return;
fetchList();
};
// 搜索
const onSearch = () => {
if (!searchKeyword.value.trim()) {
showToast('请输入搜索关键词');
return;
}
showToast(`搜索:${searchKeyword.value}`);
// 下拉刷新
const onRefresh = () => {
pageNum.value = 1;
lawList.value = [];
finished.value = false;
loading.value = true;
fetchList();
};
// 点击分类
const onCategoryClick = (item) => {
showToast(`选择分类:${item.name}`);
// 搜索
const onSearch = () => {
pageNum.value = 1;
lawList.value = [];
finished.value = false;
loading.value = true;
fetchList();
};
// 点击法律
const onLawClick = (law) => {
showToast(`查看:${law.title}`);
// 返回上一页
const onClickLeft = () => {
router.back();
};
// 点击问答
const onQaClick = (qa) => {
showToast('查看详情');
// 点击法律 — 跳转详情页
const onLawClick = (item) => {
router.push({
path: '/enterprise/lawDetail',
query: {
title: item.title,
publishDate: item.publishDate,
implementDate: item.implementDate,
department: item.department,
},
});
};
</script>
@ -165,8 +160,8 @@ const onQaClick = (qa) => {
.content-area {
flex: 1;
overflow-y: auto;
padding: 0.27rem;
padding-bottom: 1.5rem;
-webkit-overflow-scrolling: touch;
padding: 0 0.27rem;
}
/* ===== 搜索区域 ===== */
@ -176,137 +171,88 @@ const onQaClick = (qa) => {
padding: 0.2rem;
margin-bottom: 0.27rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
/* ===== 分类区域 ===== */
.category-section {
background: #fff;
border-radius: 0.2rem;
padding: 0.3rem;
margin-bottom: 0.27rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.section-title {
font-size: 0.37rem;
font-weight: 600;
color: #333;
margin-bottom: 0.25rem;
padding-left: 0.1rem;
}
.category-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.2rem;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.25rem 0;
}
.category-icon {
width: 1.2rem;
height: 1.2rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.15rem;
background: #f0f0f0;
}
.category-icon img {
width: 0.6rem;
height: 0.6rem;
}
.icon-red { background: #ffebee; }
.icon-blue { background: #e3f2fd; }
.icon-green { background: #e8f5e9; }
.icon-orange { background: #fff3e0; }
.icon-purple { background: #f3e5f5; }
.icon-yellow { background: #fffde7; }
.icon-pink { background: #fce4ec; }
.icon-cyan { background: #e0f7fa; }
:deep(.van-search) {
padding: 0;
}
.category-name {
font-size: 0.28rem;
color: #666;
text-align: center;
:deep(.van-search__content) {
background: #f2f3f5;
border-radius: 0.5rem;
}
}
/* ===== 法律列表 ===== */
.law-section {
/* ===== 列表项 ===== */
.law-item {
background: #fff;
border-radius: 0.2rem;
padding: 0.3rem;
padding: 0.35rem 0.3rem;
margin-bottom: 0.27rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
:deep(.van-cell) {
font-size: 0.32rem;
padding: 0.25rem 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.item-title-row {
display: flex;
align-items: center;
margin-bottom: 0.2rem;
}
:deep(.van-cell__title) {
font-size: 0.34rem;
color: #333;
.item-title {
flex: 1;
font-size: 0.4rem;
font-weight: bold;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.van-cell__label) {
font-size: 0.28rem;
color: #999;
/* ===== 状态标签 ===== */
.status-tag {
flex-shrink: 0;
font-size: 0.24rem;
color: #fff;
padding: 0.04rem 0.16rem;
border-radius: 0.06rem;
margin-left: 0.15rem;
white-space: nowrap;
}
/* ===== 问答区域 ===== */
.qa-section {
background: #fff;
border-radius: 0.2rem;
padding: 0.3rem;
margin-bottom: 0.27rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
.status--active {
background: #52c41a;
}
.qa-list {
.qa-item {
padding: 0.25rem 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
.status--pending {
background: #faad14;
}
.qa-question {
font-size: 0.34rem;
color: #333;
font-weight: 500;
margin-bottom: 0.1rem;
/* ===== 元信息行 ===== */
.item-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.2rem;
}
.qa-answer {
.meta-text {
font-size: 0.3rem;
color: #999;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ===== 底部导航 ===== */
.footer_body {
flex-shrink: 0;
}
/* ===== 空状态 ===== */
:deep(.van-empty) {
padding-top: 1.5rem;
}
/* ===== 列表加载完成提示 ===== */
:deep(.van-list__finished-text) {
font-size: 0.28rem;
color: #c0c4cc;
padding: 0.2rem 0;
}
</style>

3
zdxt-web-client/zdxt-efcode-enterprise-app/vite-env/.env.enterprise-dev-wmy

@ -2,7 +2,8 @@
VITE_PORT = 12001
# VITE_PROXY_TARGET = 'http://139.186.148.21:8080'
# VITE_PROXY_TARGET = 'http://172.16.21.213:8080'
VITE_PROXY_TARGET = 'http://172.16.21.93:8080'
# VITE_PROXY_TARGET = 'http://172.16.21.93:8080'
VITE_PROXY_TARGET = 'http://172.16.20.72:8080'
VITE_BASE_URL = '/efcode20-ent-app/api'
VITE_CONTEXT_PATH = '/efcode20-ent-app/'
VITE_DEFAULT_LOGIN = '/enterprise/loginByPassword'

Loading…
Cancel
Save