17 changed files with 1856 additions and 17 deletions
@ -0,0 +1,84 @@ |
|||
package com.zdxt.enforcementcode.controller.web.supervision.report; |
|||
|
|||
import com.zdxt.enforcementcode.domain.bo.EnforcementInspectionProgramBo; |
|||
import com.zdxt.enforcementcode.domain.vo.report.*; |
|||
import com.zdxt.enforcementcode.report.IStatisticsInspectionPlanReport; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.common.core.domain.R; |
|||
import org.dromara.common.excel.utils.ExcelUtil; |
|||
import org.dromara.common.mybatis.core.page.PageQuery; |
|||
import org.dromara.common.mybatis.core.page.TableDataInfo; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 检查计划统计报表 Controller |
|||
* |
|||
* @author zdxt |
|||
*/ |
|||
@Slf4j |
|||
@Validated |
|||
@RequiredArgsConstructor |
|||
@RestController |
|||
@RequestMapping("/web/supervision/report/statistics-inspection-plan") |
|||
public class StatisticsInspectionPlanController { |
|||
|
|||
private final IStatisticsInspectionPlanReport reportService; |
|||
|
|||
/** Tab1概览 */ |
|||
@GetMapping("/overview") |
|||
public R<InspectionPlanOverviewVo> overview(EnforcementInspectionProgramBo bo) { |
|||
return R.ok(reportService.overview(bo)); |
|||
} |
|||
|
|||
/** Tab1主表格:按部门分组 */ |
|||
@GetMapping("/groupList") |
|||
public TableDataInfo<InspectionPlanGroupVo> groupList(EnforcementInspectionProgramBo bo, PageQuery pageQuery) { |
|||
return reportService.groupList(bo, pageQuery); |
|||
} |
|||
|
|||
/** Tab1明细列表 */ |
|||
@GetMapping("/detailList") |
|||
public TableDataInfo<InspectionPlanDetailVo> detailList(EnforcementInspectionProgramBo bo, PageQuery pageQuery) { |
|||
return reportService.detailList(bo, pageQuery); |
|||
} |
|||
|
|||
/** Tab2-年度排行TOP10 */ |
|||
@GetMapping("/rankByYear") |
|||
public R<List<InspectionPlanRankVo>> rankByYear(EnforcementInspectionProgramBo bo) { |
|||
return R.ok(reportService.rankByYear(bo)); |
|||
} |
|||
|
|||
/** Tab2-季度排行TOP10 */ |
|||
@GetMapping("/rankByQuarter") |
|||
public R<List<InspectionPlanRankVo>> rankByQuarter(EnforcementInspectionProgramBo bo) { |
|||
return R.ok(reportService.rankByQuarter(bo)); |
|||
} |
|||
|
|||
/** Tab2-月度排行TOP10 */ |
|||
@GetMapping("/rankByMonth") |
|||
public R<List<InspectionPlanRankVo>> rankByMonth(EnforcementInspectionProgramBo bo) { |
|||
return R.ok(reportService.rankByMonth(bo)); |
|||
} |
|||
|
|||
/** Tab1导出 */ |
|||
@PostMapping("/exportGroupList") |
|||
public void exportGroupList(EnforcementInspectionProgramBo bo, HttpServletResponse response) { |
|||
try { |
|||
List<InspectionPlanExportVo> list = reportService.exportGroupList(bo); |
|||
ExcelUtil.exportExcel(list, "检查计划统计", InspectionPlanExportVo.class, response); |
|||
} catch (Exception e) { |
|||
log.error("导出检查计划统计Excel异常", e); |
|||
response.reset(); |
|||
response.setContentType("application/json; charset=utf-8"); |
|||
try { |
|||
response.getWriter().write("{\"code\":500,\"msg\":\"导出失败: " + e.getMessage() + "\"}"); |
|||
} catch (Exception ignored) { |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
package com.zdxt.enforcementcode.domain.vo.report; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 检查计划统计 - Tab1明细VO |
|||
*/ |
|||
@Data |
|||
public class InspectionPlanDetailVo { |
|||
|
|||
/** 主键 */ |
|||
private String id; |
|||
|
|||
/** 计划名称(拼接后) */ |
|||
private String planName; |
|||
|
|||
/** 执法单位 */ |
|||
private String bureauDeptName; |
|||
|
|||
/** 执法区域 */ |
|||
private String enforcementArea; |
|||
|
|||
/** 发布时间(格式化后的字符串) */ |
|||
private String enforcementTime; |
|||
|
|||
/** 计划类型(year/quarter/month),用于Java层拼接名称 */ |
|||
private String timeType; |
|||
|
|||
/** 月份 */ |
|||
private Long mouth; |
|||
|
|||
/** 计划年度 */ |
|||
private String planYear; |
|||
|
|||
/** 季度 */ |
|||
private Long quarter; |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
package com.zdxt.enforcementcode.domain.vo.report; |
|||
|
|||
import cn.idev.excel.annotation.ExcelProperty; |
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 检查计划统计 - Tab1导出VO |
|||
*/ |
|||
@Data |
|||
public class InspectionPlanExportVo { |
|||
|
|||
@ExcelProperty("执法单位") |
|||
private String bureauDeptName; |
|||
|
|||
@ExcelProperty("总数") |
|||
private Integer total; |
|||
|
|||
@ExcelProperty("年度计划") |
|||
private Integer yearTotal; |
|||
|
|||
@ExcelProperty("季度计划") |
|||
private Integer quarterTotal; |
|||
|
|||
@ExcelProperty("月度计划") |
|||
private Integer monthTotal; |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
package com.zdxt.enforcementcode.domain.vo.report; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 检查计划统计 - Tab1分组统计VO |
|||
*/ |
|||
@Data |
|||
public class InspectionPlanGroupVo { |
|||
|
|||
/** 执法单位名称 */ |
|||
private String bureauDeptName; |
|||
|
|||
/** 局级部门ID */ |
|||
private String bureauDeptId; |
|||
|
|||
/** 总数 */ |
|||
private Integer total; |
|||
|
|||
/** 年度计划数 */ |
|||
private Integer yearTotal; |
|||
|
|||
/** 季度计划数 */ |
|||
private Integer quarterTotal; |
|||
|
|||
/** 月度计划数 */ |
|||
private Integer monthTotal; |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
package com.zdxt.enforcementcode.domain.vo.report; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 检查计划统计 - Tab1概览VO |
|||
*/ |
|||
@Data |
|||
public class InspectionPlanOverviewVo { |
|||
|
|||
/** 计划总数(yearTotal + quarterTotal + monthTotal) */ |
|||
private Integer total; |
|||
|
|||
/** 年度计划数 */ |
|||
private Integer yearTotal; |
|||
|
|||
/** 季度计划数 */ |
|||
private Integer quarterTotal; |
|||
|
|||
/** 月度计划数 */ |
|||
private Integer monthTotal; |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
package com.zdxt.enforcementcode.domain.vo.report; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 检查计划统计 - Tab2排行VO(含各季度/月份明细) |
|||
*/ |
|||
@Data |
|||
public class InspectionPlanRankVo { |
|||
|
|||
/** 执法单位名称 */ |
|||
private String bureauDeptName; |
|||
|
|||
/** 合计 */ |
|||
private Integer total; |
|||
|
|||
/** 第1季度 */ |
|||
private Integer quarterTotal1; |
|||
/** 第2季度 */ |
|||
private Integer quarterTotal2; |
|||
/** 第3季度 */ |
|||
private Integer quarterTotal3; |
|||
/** 第4季度 */ |
|||
private Integer quarterTotal4; |
|||
|
|||
/** 1月 */ |
|||
private Integer monthTotal1; |
|||
/** 2月 */ |
|||
private Integer monthTotal2; |
|||
/** 3月 */ |
|||
private Integer monthTotal3; |
|||
/** 4月 */ |
|||
private Integer monthTotal4; |
|||
/** 5月 */ |
|||
private Integer monthTotal5; |
|||
/** 6月 */ |
|||
private Integer monthTotal6; |
|||
/** 7月 */ |
|||
private Integer monthTotal7; |
|||
/** 8月 */ |
|||
private Integer monthTotal8; |
|||
/** 9月 */ |
|||
private Integer monthTotal9; |
|||
/** 10月 */ |
|||
private Integer monthTotal10; |
|||
/** 11月 */ |
|||
private Integer monthTotal11; |
|||
/** 12月 */ |
|||
private Integer monthTotal12; |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
package com.zdxt.enforcementcode.report; |
|||
|
|||
import com.zdxt.enforcementcode.domain.bo.EnforcementInspectionProgramBo; |
|||
import com.zdxt.enforcementcode.domain.vo.report.InspectionPlanDetailVo; |
|||
import com.zdxt.enforcementcode.domain.vo.report.InspectionPlanExportVo; |
|||
import com.zdxt.enforcementcode.domain.vo.report.InspectionPlanGroupVo; |
|||
import com.zdxt.enforcementcode.domain.vo.report.InspectionPlanOverviewVo; |
|||
import com.zdxt.enforcementcode.domain.vo.report.InspectionPlanRankVo; |
|||
import org.dromara.common.mybatis.core.page.PageQuery; |
|||
import org.dromara.common.mybatis.core.page.TableDataInfo; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 检查计划统计报表服务接口 |
|||
*/ |
|||
public interface IStatisticsInspectionPlanReport { |
|||
|
|||
/** Tab1概览:计划总数 + 年度/季度/月度 */ |
|||
InspectionPlanOverviewVo overview(EnforcementInspectionProgramBo bo); |
|||
|
|||
/** Tab1主表格:按部门分组统计(含手动分页) */ |
|||
TableDataInfo<InspectionPlanGroupVo> groupList(EnforcementInspectionProgramBo bo, PageQuery pageQuery); |
|||
|
|||
/** Tab1明细列表:某部门某类型的计划明细(含手动分页) */ |
|||
TableDataInfo<InspectionPlanDetailVo> detailList(EnforcementInspectionProgramBo bo, PageQuery pageQuery); |
|||
|
|||
/** Tab2-年度排行TOP10 */ |
|||
List<InspectionPlanRankVo> rankByYear(EnforcementInspectionProgramBo bo); |
|||
|
|||
/** Tab2-季度排行TOP10 */ |
|||
List<InspectionPlanRankVo> rankByQuarter(EnforcementInspectionProgramBo bo); |
|||
|
|||
/** Tab2-月度排行TOP10 */ |
|||
List<InspectionPlanRankVo> rankByMonth(EnforcementInspectionProgramBo bo); |
|||
|
|||
/** Tab1导出 */ |
|||
List<InspectionPlanExportVo> exportGroupList(EnforcementInspectionProgramBo bo); |
|||
} |
|||
@ -0,0 +1,200 @@ |
|||
package com.zdxt.enforcementcode.report.impl; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|||
import com.zdxt.enforcementcode.common.util.DataFilterUtil; |
|||
import com.zdxt.enforcementcode.domain.bo.EnforcementInspectionProgramBo; |
|||
import com.zdxt.enforcementcode.domain.vo.report.*; |
|||
import com.zdxt.enforcementcode.mapper.EnforcementInspectionProgramMapper; |
|||
import com.zdxt.enforcementcode.report.IStatisticsInspectionPlanReport; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.common.mybatis.core.page.PageQuery; |
|||
import org.dromara.common.mybatis.core.page.TableDataInfo; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 检查计划统计报表 Service实现 |
|||
* <p> |
|||
* 复刻旧系统 EnforcementInspectionProgramServiceImpl 逻辑: |
|||
* - Tab1(统计+概览+明细):数据权限 + 已发布(is_publish=1)对所有人可见 |
|||
* - Tab2(排行TOP10):仅数据权限过滤(不含is_publish扩展) |
|||
* |
|||
* @author zdxt |
|||
*/ |
|||
@Slf4j |
|||
@RequiredArgsConstructor |
|||
@Service |
|||
public class StatisticsInspectionPlanReportImpl implements IStatisticsInspectionPlanReport { |
|||
|
|||
private final EnforcementInspectionProgramMapper programMapper; |
|||
|
|||
@Override |
|||
public InspectionPlanOverviewVo overview(EnforcementInspectionProgramBo bo) { |
|||
String dataFilter = buildDataFilterWithPublish(); |
|||
InspectionPlanOverviewVo vo = programMapper.selectPlanOverview(bo, dataFilter); |
|||
if (vo == null) { |
|||
vo = new InspectionPlanOverviewVo(); |
|||
vo.setYearTotal(0); |
|||
vo.setQuarterTotal(0); |
|||
vo.setMonthTotal(0); |
|||
} |
|||
// total = yearTotal + quarterTotal + monthTotal
|
|||
vo.setTotal(vo.getYearTotal() + vo.getQuarterTotal() + vo.getMonthTotal()); |
|||
return vo; |
|||
} |
|||
|
|||
@Override |
|||
public TableDataInfo<InspectionPlanGroupVo> groupList(EnforcementInspectionProgramBo bo, PageQuery pageQuery) { |
|||
String dataFilter = buildDataFilterWithPublish(); |
|||
// 全量查询
|
|||
List<InspectionPlanGroupVo> allList = programMapper.selectPlanGroupByDept(bo, dataFilter); |
|||
|
|||
// 手动分页
|
|||
int pageNum = pageQuery.getPageNum(); |
|||
int pageSize = pageQuery.getPageSize(); |
|||
int total = allList.size(); |
|||
int fromIndex = Math.min((pageNum - 1) * pageSize, total); |
|||
int toIndex = Math.min(fromIndex + pageSize, total); |
|||
List<InspectionPlanGroupVo> pageRecords = allList.subList(fromIndex, toIndex); |
|||
|
|||
Page<InspectionPlanGroupVo> page = new Page<>(pageNum, pageSize, total); |
|||
page.setRecords(pageRecords); |
|||
return TableDataInfo.build(page); |
|||
} |
|||
|
|||
@Override |
|||
public TableDataInfo<InspectionPlanDetailVo> detailList(EnforcementInspectionProgramBo bo, PageQuery pageQuery) { |
|||
String dataFilter = buildDataFilterWithPublish(); |
|||
// 全量查询
|
|||
List<InspectionPlanDetailVo> allList = programMapper.selectPlanDetailList(bo, dataFilter); |
|||
|
|||
// Java层拼接计划名称(复刻旧系统格式:局名+年+类型描述+计划名称)
|
|||
for (InspectionPlanDetailVo detail : allList) { |
|||
detail.setPlanName(buildPlanDisplayName(detail)); |
|||
} |
|||
|
|||
// 手动分页
|
|||
int pageNum = pageQuery.getPageNum(); |
|||
int pageSize = pageQuery.getPageSize(); |
|||
int total = allList.size(); |
|||
int fromIndex = Math.min((pageNum - 1) * pageSize, total); |
|||
int toIndex = Math.min(fromIndex + pageSize, total); |
|||
List<InspectionPlanDetailVo> pageRecords = allList.subList(fromIndex, toIndex); |
|||
|
|||
Page<InspectionPlanDetailVo> page = new Page<>(pageNum, pageSize, total); |
|||
page.setRecords(pageRecords); |
|||
return TableDataInfo.build(page); |
|||
} |
|||
|
|||
@Override |
|||
public List<InspectionPlanRankVo> rankByYear(EnforcementInspectionProgramBo bo) { |
|||
// Tab2排行不加 is_publish 特殊逻辑(对齐旧系统行为)
|
|||
String dataFilter = buildDataFilter(); |
|||
return programMapper.selectPlanRankByYear(bo, dataFilter); |
|||
} |
|||
|
|||
@Override |
|||
public List<InspectionPlanRankVo> rankByQuarter(EnforcementInspectionProgramBo bo) { |
|||
String dataFilter = buildDataFilter(); |
|||
return programMapper.selectPlanRankByQuarter(bo, dataFilter); |
|||
} |
|||
|
|||
@Override |
|||
public List<InspectionPlanRankVo> rankByMonth(EnforcementInspectionProgramBo bo) { |
|||
String dataFilter = buildDataFilter(); |
|||
return programMapper.selectPlanRankByMonth(bo, dataFilter); |
|||
} |
|||
|
|||
@Override |
|||
public List<InspectionPlanExportVo> exportGroupList(EnforcementInspectionProgramBo bo) { |
|||
String dataFilter = buildDataFilterWithPublish(); |
|||
List<InspectionPlanGroupVo> allList = programMapper.selectPlanGroupByDept(bo, dataFilter); |
|||
|
|||
return allList.stream().map(g -> { |
|||
InspectionPlanExportVo export = new InspectionPlanExportVo(); |
|||
export.setBureauDeptName(g.getBureauDeptName()); |
|||
export.setTotal(g.getTotal()); |
|||
export.setYearTotal(g.getYearTotal()); |
|||
export.setQuarterTotal(g.getQuarterTotal()); |
|||
export.setMonthTotal(g.getMonthTotal()); |
|||
return export; |
|||
}).collect(Collectors.toList()); |
|||
} |
|||
|
|||
// ===== 私有方法 =====
|
|||
|
|||
/** |
|||
* 构建数据权限过滤条件(含 is_publish=1 扩展)。 |
|||
* <p>用于Tab1(统计+概览+明细),已发布计划对所有人可见。 |
|||
* <p>超管/全数据角色返回空串(不过滤)。 |
|||
* |
|||
* @return "AND (...)" SQL片段 或 空串 |
|||
*/ |
|||
private String buildDataFilterWithPublish() { |
|||
String filter = DataFilterUtil.buildFilterSql("p", "enforcement_department_id", true, true); |
|||
if (filter == null) { |
|||
// 超管/全数据角色 → 不过滤
|
|||
return ""; |
|||
} |
|||
// 非超管:将原始过滤条件扩展为 (原条件 OR is_publish=1)
|
|||
// filter 格式类似: (p.enforcement_department_id IN (SELECT ...))
|
|||
return "AND (" + filter + " OR p.is_publish = 1)"; |
|||
} |
|||
|
|||
/** |
|||
* 构建数据权限过滤条件(不含 is_publish 扩展)。 |
|||
* <p>用于Tab2排行,仅展示权限范围内的数据。 |
|||
* |
|||
* @return "AND ..." SQL片段 或 空串 |
|||
*/ |
|||
private String buildDataFilter() { |
|||
String filter = DataFilterUtil.buildFilterSql("p", "enforcement_department_id", true, true); |
|||
if (filter == null) { |
|||
return ""; |
|||
} |
|||
return "AND " + filter; |
|||
} |
|||
|
|||
/** |
|||
* 拼接计划显示名称(复刻旧系统格式)。 |
|||
* <p>格式:局名+年+季度/月份+计划名称 |
|||
*/ |
|||
private String buildPlanDisplayName(InspectionPlanDetailVo detail) { |
|||
StringBuilder sb = new StringBuilder(); |
|||
if (StrUtil.isNotBlank(detail.getBureauDeptName())) { |
|||
sb.append(detail.getBureauDeptName()); |
|||
} |
|||
if (StrUtil.isNotBlank(detail.getPlanYear())) { |
|||
sb.append(detail.getPlanYear()).append("年"); |
|||
} |
|||
String timeType = detail.getTimeType(); |
|||
if ("quarter".equals(timeType) && detail.getQuarter() != null) { |
|||
sb.append(getQuarterText(detail.getQuarter())); |
|||
} else if ("month".equals(timeType) && detail.getMouth() != null) { |
|||
sb.append(detail.getMouth()).append("月"); |
|||
} |
|||
if (StrUtil.isNotBlank(detail.getPlanName())) { |
|||
sb.append(detail.getPlanName()); |
|||
} |
|||
return sb.toString(); |
|||
} |
|||
|
|||
/** |
|||
* 季度数字转文本 |
|||
*/ |
|||
private String getQuarterText(Long quarter) { |
|||
if (quarter == null) return ""; |
|||
switch (quarter.intValue()) { |
|||
case 1: return "第一季度"; |
|||
case 2: return "第二季度"; |
|||
case 3: return "第三季度"; |
|||
case 4: return "第四季度"; |
|||
default: return ""; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,174 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!DOCTYPE mapper |
|||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
|||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
<mapper namespace="com.zdxt.enforcementcode.mapper.EnforcementInspectionProgramMapper"> |
|||
|
|||
<!-- 通用基础条件片段 --> |
|||
<sql id="reportBaseCondition"> |
|||
AND p.bureau_dept_id IS NOT NULL |
|||
AND p.time_type IS NOT NULL AND p.time_type != '0' |
|||
AND (p.plan_year IS NOT NULL OR p.quarter IS NOT NULL OR p.mouth IS NOT NULL) |
|||
</sql> |
|||
|
|||
<!-- 通用时间条件片段 --> |
|||
<sql id="reportTimeCondition"> |
|||
<if test="bo.dateSearch == 'year' and bo.year != null and bo.year != ''"> |
|||
AND YEAR(p.enforcement_time) = #{bo.year} |
|||
</if> |
|||
<if test="bo.dateSearch == 'quarter' and bo.year != null and bo.year != '' and bo.reportQuarter != null and bo.reportQuarter != ''"> |
|||
AND YEAR(p.enforcement_time) = #{bo.year} AND QUARTER(p.enforcement_time) = #{bo.reportQuarter} |
|||
</if> |
|||
<if test="bo.dateSearch == 'month' and bo.year != null and bo.year != '' and bo.month != null and bo.month != ''"> |
|||
AND YEAR(p.enforcement_time) = #{bo.year} AND MONTH(p.enforcement_time) = #{bo.month} |
|||
</if> |
|||
<if test="bo.dateSearch == 'day' and bo.beginTime != null and bo.beginTime != ''"> |
|||
AND DATE_FORMAT(p.enforcement_time,'%Y-%m-%d') >= STR_TO_DATE(#{bo.beginTime},'%Y-%m-%d') |
|||
</if> |
|||
<if test="bo.dateSearch == 'day' and bo.endTime != null and bo.endTime != ''"> |
|||
AND DATE_FORMAT(p.enforcement_time,'%Y-%m-%d') <= STR_TO_DATE(#{bo.endTime},'%Y-%m-%d') |
|||
</if> |
|||
</sql> |
|||
|
|||
<!-- 通用搜索条件片段 --> |
|||
<sql id="reportSearchCondition"> |
|||
<if test="bo.bureauDeptName != null and bo.bureauDeptName != ''"> |
|||
AND p.bureau_dept_name LIKE CONCAT('%', #{bo.bureauDeptName}, '%') |
|||
</if> |
|||
</sql> |
|||
|
|||
<!-- 数据权限片段 --> |
|||
<sql id="reportDataFilter"> |
|||
${dataFilter} |
|||
</sql> |
|||
|
|||
<!-- Tab1:概览汇总 --> |
|||
<select id="selectPlanOverview" resultType="com.zdxt.enforcementcode.domain.vo.report.InspectionPlanOverviewVo"> |
|||
SELECT |
|||
IFNULL(SUM(CASE WHEN p.time_type = 'year' THEN 1 ELSE 0 END), 0) AS yearTotal, |
|||
IFNULL(SUM(CASE WHEN p.time_type = 'quarter' THEN 1 ELSE 0 END), 0) AS quarterTotal, |
|||
IFNULL(SUM(CASE WHEN p.time_type = 'month' THEN 1 ELSE 0 END), 0) AS monthTotal |
|||
FROM enforcement_inspection_program p |
|||
WHERE 1=1 |
|||
<include refid="reportBaseCondition"/> |
|||
<include refid="reportSearchCondition"/> |
|||
<include refid="reportTimeCondition"/> |
|||
<include refid="reportDataFilter"/> |
|||
</select> |
|||
|
|||
<!-- Tab1:分组统计 --> |
|||
<select id="selectPlanGroupByDept" resultType="com.zdxt.enforcementcode.domain.vo.report.InspectionPlanGroupVo"> |
|||
SELECT |
|||
p.bureau_dept_name AS bureauDeptName, |
|||
p.bureau_dept_id AS bureauDeptId, |
|||
COUNT(*) AS total, |
|||
SUM(CASE WHEN p.time_type = 'year' THEN 1 ELSE 0 END) AS yearTotal, |
|||
SUM(CASE WHEN p.time_type = 'quarter' THEN 1 ELSE 0 END) AS quarterTotal, |
|||
SUM(CASE WHEN p.time_type = 'month' THEN 1 ELSE 0 END) AS monthTotal |
|||
FROM enforcement_inspection_program p |
|||
WHERE 1=1 |
|||
<include refid="reportBaseCondition"/> |
|||
<include refid="reportSearchCondition"/> |
|||
<include refid="reportTimeCondition"/> |
|||
<include refid="reportDataFilter"/> |
|||
GROUP BY p.bureau_dept_id, p.bureau_dept_name |
|||
ORDER BY total DESC |
|||
</select> |
|||
|
|||
<!-- Tab1:明细列表 --> |
|||
<select id="selectPlanDetailList" resultType="com.zdxt.enforcementcode.domain.vo.report.InspectionPlanDetailVo"> |
|||
SELECT p.id, p.plan_name AS planName, p.bureau_dept_name AS bureauDeptName, |
|||
p.enforcement_area AS enforcementArea, |
|||
DATE_FORMAT(p.enforcement_time, '%Y-%m-%d %H:%i:%s') AS enforcementTime, |
|||
p.time_type AS timeType, p.mouth, p.plan_year AS planYear, p.quarter |
|||
FROM enforcement_inspection_program p |
|||
WHERE 1=1 |
|||
<include refid="reportBaseCondition"/> |
|||
<if test="bo.bureauDeptId != null and bo.bureauDeptId != ''"> |
|||
AND p.bureau_dept_id = #{bo.bureauDeptId} |
|||
</if> |
|||
<if test="bo.reportTimeType != null and bo.reportTimeType != ''"> |
|||
AND p.time_type = #{bo.reportTimeType} |
|||
</if> |
|||
<include refid="reportSearchCondition"/> |
|||
<include refid="reportTimeCondition"/> |
|||
<include refid="reportDataFilter"/> |
|||
ORDER BY p.enforcement_time DESC |
|||
</select> |
|||
|
|||
<!-- Tab2:年度排行TOP10 --> |
|||
<select id="selectPlanRankByYear" resultType="com.zdxt.enforcementcode.domain.vo.report.InspectionPlanRankVo"> |
|||
SELECT |
|||
p.bureau_dept_name AS bureauDeptName, |
|||
SUM(CASE WHEN p.time_type = 'year' THEN 1 ELSE 0 END) AS total |
|||
FROM enforcement_inspection_program p |
|||
WHERE 1=1 |
|||
AND p.bureau_dept_id IS NOT NULL |
|||
AND p.time_type = 'year' |
|||
AND p.plan_year IS NOT NULL |
|||
<if test="bo.year != null and bo.year != ''"> |
|||
AND YEAR(p.enforcement_time) = #{bo.year} |
|||
</if> |
|||
<include refid="reportDataFilter"/> |
|||
GROUP BY p.bureau_dept_id, p.bureau_dept_name |
|||
ORDER BY total DESC |
|||
LIMIT 10 |
|||
</select> |
|||
|
|||
<!-- Tab2:季度排行TOP10(含各季度明细) --> |
|||
<select id="selectPlanRankByQuarter" resultType="com.zdxt.enforcementcode.domain.vo.report.InspectionPlanRankVo"> |
|||
SELECT |
|||
p.bureau_dept_name AS bureauDeptName, |
|||
SUM(CASE WHEN p.time_type = 'quarter' THEN 1 ELSE 0 END) AS total, |
|||
SUM(CASE WHEN p.quarter = 1 THEN 1 ELSE 0 END) AS quarterTotal1, |
|||
SUM(CASE WHEN p.quarter = 2 THEN 1 ELSE 0 END) AS quarterTotal2, |
|||
SUM(CASE WHEN p.quarter = 3 THEN 1 ELSE 0 END) AS quarterTotal3, |
|||
SUM(CASE WHEN p.quarter = 4 THEN 1 ELSE 0 END) AS quarterTotal4 |
|||
FROM enforcement_inspection_program p |
|||
WHERE 1=1 |
|||
AND p.bureau_dept_id IS NOT NULL |
|||
AND p.time_type = 'quarter' |
|||
AND p.plan_year IS NOT NULL |
|||
AND p.quarter IS NOT NULL |
|||
<if test="bo.year != null and bo.year != ''"> |
|||
AND YEAR(p.enforcement_time) = #{bo.year} |
|||
</if> |
|||
<include refid="reportDataFilter"/> |
|||
GROUP BY p.bureau_dept_id, p.bureau_dept_name |
|||
ORDER BY total DESC |
|||
LIMIT 10 |
|||
</select> |
|||
|
|||
<!-- Tab2:月度排行TOP10(含各月明细) --> |
|||
<select id="selectPlanRankByMonth" resultType="com.zdxt.enforcementcode.domain.vo.report.InspectionPlanRankVo"> |
|||
SELECT |
|||
p.bureau_dept_name AS bureauDeptName, |
|||
SUM(CASE WHEN p.time_type = 'month' THEN 1 ELSE 0 END) AS total, |
|||
SUM(CASE WHEN p.mouth = 1 THEN 1 ELSE 0 END) AS monthTotal1, |
|||
SUM(CASE WHEN p.mouth = 2 THEN 1 ELSE 0 END) AS monthTotal2, |
|||
SUM(CASE WHEN p.mouth = 3 THEN 1 ELSE 0 END) AS monthTotal3, |
|||
SUM(CASE WHEN p.mouth = 4 THEN 1 ELSE 0 END) AS monthTotal4, |
|||
SUM(CASE WHEN p.mouth = 5 THEN 1 ELSE 0 END) AS monthTotal5, |
|||
SUM(CASE WHEN p.mouth = 6 THEN 1 ELSE 0 END) AS monthTotal6, |
|||
SUM(CASE WHEN p.mouth = 7 THEN 1 ELSE 0 END) AS monthTotal7, |
|||
SUM(CASE WHEN p.mouth = 8 THEN 1 ELSE 0 END) AS monthTotal8, |
|||
SUM(CASE WHEN p.mouth = 9 THEN 1 ELSE 0 END) AS monthTotal9, |
|||
SUM(CASE WHEN p.mouth = 10 THEN 1 ELSE 0 END) AS monthTotal10, |
|||
SUM(CASE WHEN p.mouth = 11 THEN 1 ELSE 0 END) AS monthTotal11, |
|||
SUM(CASE WHEN p.mouth = 12 THEN 1 ELSE 0 END) AS monthTotal12 |
|||
FROM enforcement_inspection_program p |
|||
WHERE 1=1 |
|||
AND p.bureau_dept_id IS NOT NULL |
|||
AND p.time_type = 'month' |
|||
AND p.plan_year IS NOT NULL |
|||
AND p.mouth IS NOT NULL |
|||
<if test="bo.year != null and bo.year != ''"> |
|||
AND YEAR(p.enforcement_time) = #{bo.year} |
|||
</if> |
|||
<include refid="reportDataFilter"/> |
|||
GROUP BY p.bureau_dept_id, p.bureau_dept_name |
|||
ORDER BY total DESC |
|||
LIMIT 10 |
|||
</select> |
|||
|
|||
</mapper> |
|||
@ -0,0 +1,141 @@ |
|||
import request from '@/utils/request'; |
|||
import { AxiosPromise } from 'axios'; |
|||
|
|||
const BASE_URL = '/web/supervision/report/statistics-inspection-plan'; |
|||
|
|||
/** 查询参数 */ |
|||
export interface InspectionPlanQuery { |
|||
pageNum?: number; |
|||
pageSize?: number; |
|||
/** 执法单位(模糊搜索) */ |
|||
bureauDeptName?: string; |
|||
/** 局级部门ID(明细查询用) */ |
|||
bureauDeptId?: string; |
|||
/** 时段类型:year/quarter/month/day */ |
|||
dateSearch?: string; |
|||
/** 年 */ |
|||
year?: string; |
|||
/** 报表搜索用季度 */ |
|||
reportQuarter?: string; |
|||
/** 月 */ |
|||
month?: string; |
|||
/** 自定义开始时间 */ |
|||
beginTime?: string; |
|||
/** 自定义结束时间 */ |
|||
endTime?: string; |
|||
/** 明细弹窗时的计划类型(year/quarter/month) */ |
|||
reportTimeType?: string; |
|||
} |
|||
|
|||
/** 概览VO */ |
|||
export interface InspectionPlanOverviewVo { |
|||
total: number; |
|||
yearTotal: number; |
|||
quarterTotal: number; |
|||
monthTotal: number; |
|||
} |
|||
|
|||
/** 分组统计VO */ |
|||
export interface InspectionPlanGroupVo { |
|||
bureauDeptName: string; |
|||
bureauDeptId: string; |
|||
total: number; |
|||
yearTotal: number; |
|||
quarterTotal: number; |
|||
monthTotal: number; |
|||
} |
|||
|
|||
/** 明细VO */ |
|||
export interface InspectionPlanDetailVo { |
|||
id: string; |
|||
planName: string; |
|||
bureauDeptName: string; |
|||
enforcementArea: string; |
|||
enforcementTime: string; |
|||
} |
|||
|
|||
/** 排行VO */ |
|||
export interface InspectionPlanRankVo { |
|||
bureauDeptName: string; |
|||
total: number; |
|||
quarterTotal1?: number; |
|||
quarterTotal2?: number; |
|||
quarterTotal3?: number; |
|||
quarterTotal4?: number; |
|||
monthTotal1?: number; |
|||
monthTotal2?: number; |
|||
monthTotal3?: number; |
|||
monthTotal4?: number; |
|||
monthTotal5?: number; |
|||
monthTotal6?: number; |
|||
monthTotal7?: number; |
|||
monthTotal8?: number; |
|||
monthTotal9?: number; |
|||
monthTotal10?: number; |
|||
monthTotal11?: number; |
|||
monthTotal12?: number; |
|||
} |
|||
|
|||
/** Tab1概览 */ |
|||
export const getOverview = (params: InspectionPlanQuery): AxiosPromise => { |
|||
return request({ |
|||
url: `${BASE_URL}/overview`, |
|||
method: 'get', |
|||
params |
|||
}); |
|||
}; |
|||
|
|||
/** Tab1分组列表 */ |
|||
export const getGroupList = (params: InspectionPlanQuery): AxiosPromise => { |
|||
return request({ |
|||
url: `${BASE_URL}/groupList`, |
|||
method: 'get', |
|||
params |
|||
}); |
|||
}; |
|||
|
|||
/** Tab1明细列表 */ |
|||
export const getDetailList = (params: InspectionPlanQuery): AxiosPromise => { |
|||
return request({ |
|||
url: `${BASE_URL}/detailList`, |
|||
method: 'get', |
|||
params |
|||
}); |
|||
}; |
|||
|
|||
/** Tab2-年度排行TOP10 */ |
|||
export const getRankByYear = (params: InspectionPlanQuery): AxiosPromise => { |
|||
return request({ |
|||
url: `${BASE_URL}/rankByYear`, |
|||
method: 'get', |
|||
params |
|||
}); |
|||
}; |
|||
|
|||
/** Tab2-季度排行TOP10 */ |
|||
export const getRankByQuarter = (params: InspectionPlanQuery): AxiosPromise => { |
|||
return request({ |
|||
url: `${BASE_URL}/rankByQuarter`, |
|||
method: 'get', |
|||
params |
|||
}); |
|||
}; |
|||
|
|||
/** Tab2-月度排行TOP10 */ |
|||
export const getRankByMonth = (params: InspectionPlanQuery): AxiosPromise => { |
|||
return request({ |
|||
url: `${BASE_URL}/rankByMonth`, |
|||
method: 'get', |
|||
params |
|||
}); |
|||
}; |
|||
|
|||
/** Tab1导出 */ |
|||
export const exportGroupList = (data: InspectionPlanQuery): AxiosPromise<Blob> => { |
|||
return request({ |
|||
url: `${BASE_URL}/exportGroupList`, |
|||
method: 'post', |
|||
data, |
|||
responseType: 'blob' |
|||
}); |
|||
}; |
|||
@ -1,11 +1,497 @@ |
|||
<script setup lang="ts"> |
|||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'; |
|||
import { ElMessage } from 'element-plus'; |
|||
import * as echarts from 'echarts'; |
|||
import { getDicts } from '@/api/system/dict/data'; |
|||
import { |
|||
getOverview, getGroupList, getDetailList, |
|||
getTypeStatList, getAreaStatList, getStreetStatList, |
|||
exportGroupList, exportTypeStat, exportAreaStat, exportStreetStat |
|||
} from '@/api/efcode/report/statistics-complaint'; |
|||
import type { ComplaintQuery, ComplaintGroupVo, ComplaintDetailVo, ComplaintTypeStatVo } from '@/api/efcode/report/statistics-complaint'; |
|||
|
|||
// 当前Tab |
|||
const activeTab = ref('tab1'); |
|||
|
|||
// ======================== 通用时段查询 ======================== |
|||
const timeParams = reactive({ |
|||
year: new Date().getFullYear().toString(), |
|||
quarter: undefined as string | undefined, |
|||
month: undefined as string | undefined, |
|||
beginTime: undefined as string | undefined, |
|||
endTime: undefined as string | undefined, |
|||
timeType: 'year' as string |
|||
}); |
|||
|
|||
const yearOptions = computed(() => { |
|||
const current = new Date().getFullYear(); |
|||
return Array.from({ length: 10 }, (_, i) => String(current - i)); |
|||
}); |
|||
const quarterOptions = ['1', '2', '3', '4']; |
|||
const monthOptions = Array.from({ length: 12 }, (_, i) => String(i + 1)); |
|||
|
|||
const handleTimeTypeChange = (val: string) => { |
|||
timeParams.quarter = undefined; |
|||
timeParams.month = undefined; |
|||
timeParams.beginTime = undefined; |
|||
timeParams.endTime = undefined; |
|||
if (val !== 'year' && val !== 'quarter' && val !== 'month' && val !== 'day') { |
|||
timeParams.year = new Date().getFullYear().toString(); |
|||
} |
|||
}; |
|||
|
|||
// 构建时段参数 |
|||
const buildTimeQuery = () => ({ |
|||
year: timeParams.year, |
|||
quarter: timeParams.quarter, |
|||
month: timeParams.month, |
|||
beginTime: timeParams.beginTime, |
|||
endTime: timeParams.endTime, |
|||
timeType: timeParams.timeType |
|||
}); |
|||
|
|||
// ======================== Tab1: 执法投诉统计 ======================== |
|||
const loading1 = ref(false); |
|||
const tab1Query = reactive({ |
|||
complaintDepartment: undefined as string | undefined, |
|||
enforceType: undefined as number | string | undefined, |
|||
enterpriseAddress: undefined as string | undefined |
|||
}); |
|||
|
|||
// 概览 |
|||
const overviewData = reactive({ complaintCount: 0, complaintDeptCount: 0 }); |
|||
|
|||
// 主表格(分组) |
|||
const groupTableData = ref<ComplaintGroupVo[]>([]); |
|||
const groupTotal = ref(0); |
|||
const groupPageNum = ref(1); |
|||
const groupPageSize = ref(10); |
|||
|
|||
// 明细弹窗 |
|||
const detailDialogVisible = ref(false); |
|||
const detailTitle = ref(''); |
|||
const detailTableData = ref<ComplaintDetailVo[]>([]); |
|||
const detailTotal = ref(0); |
|||
const detailPageNum = ref(1); |
|||
const detailPageSize = ref(10); |
|||
const currentDeptId = ref(''); |
|||
|
|||
// 字典 |
|||
const caseTypeOptions = ref<any[]>([]); |
|||
const addressTypeOptions = ref<any[]>([]); |
|||
|
|||
// ======================== Tab2: 投诉类型统计 ======================== |
|||
const loading2 = ref(false); |
|||
const tab2TypeSelect = ref('type'); // type / '' (全市) / 具体区域dict_value |
|||
const tab2StatList = ref<ComplaintTypeStatVo[]>([]); |
|||
const tab2Total = computed(() => tab2StatList.value.reduce((sum, item) => sum + item.count, 0)); |
|||
|
|||
// 饼图 |
|||
const pieChartRef = ref<HTMLElement>(); |
|||
let pieChart: echarts.ECharts | null = null; |
|||
|
|||
// Tab2 类型选择选项 |
|||
const tab2TypeOptions = computed(() => { |
|||
const opts: { label: string; value: string }[] = [ |
|||
{ label: '投诉类型', value: 'type' }, |
|||
{ label: '全市', value: '' } |
|||
]; |
|||
addressTypeOptions.value.forEach((item: any) => { |
|||
opts.push({ label: item.dictLabel, value: item.dictValue }); |
|||
}); |
|||
return opts; |
|||
}); |
|||
|
|||
// ======================== 生命周期 ======================== |
|||
onMounted(() => { |
|||
loadDicts(); |
|||
loadTab1Data(); |
|||
window.addEventListener('resize', handleResize); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
window.removeEventListener('resize', handleResize); |
|||
pieChart?.dispose(); |
|||
}); |
|||
|
|||
const handleResize = () => { |
|||
pieChart?.resize(); |
|||
}; |
|||
|
|||
// ======================== 字典 ======================== |
|||
const loadDicts = async () => { |
|||
try { |
|||
const [caseTypeRes, areaRes] = await Promise.all([ |
|||
getDicts('caseType'), |
|||
getDicts('address_type') |
|||
]); |
|||
caseTypeOptions.value = caseTypeRes.data || []; |
|||
addressTypeOptions.value = areaRes.data || []; |
|||
} catch (e) { |
|||
console.error('加载字典失败', e); |
|||
} |
|||
}; |
|||
|
|||
// ======================== Tab1: 数据加载 ======================== |
|||
const loadTab1Data = async () => { |
|||
loading1.value = true; |
|||
try { |
|||
const params: ComplaintQuery = { |
|||
...tab1Query, |
|||
...buildTimeQuery(), |
|||
pageNum: groupPageNum.value, |
|||
pageSize: groupPageSize.value |
|||
}; |
|||
const [overviewRes, groupRes] = await Promise.all([ |
|||
getOverview(params), |
|||
getGroupList(params) |
|||
]); |
|||
const ov = overviewRes.data || {}; |
|||
overviewData.complaintCount = Number(ov.complaintCount) || 0; |
|||
overviewData.complaintDeptCount = Number(ov.complaintDeptCount) || 0; |
|||
groupTableData.value = groupRes.rows || []; |
|||
groupTotal.value = groupRes.total || 0; |
|||
} finally { |
|||
loading1.value = false; |
|||
} |
|||
}; |
|||
|
|||
/** Tab1搜索 */ |
|||
const handleTab1Query = () => { |
|||
groupPageNum.value = 1; |
|||
loadTab1Data(); |
|||
}; |
|||
|
|||
/** Tab1重置 */ |
|||
const resetTab1Query = () => { |
|||
tab1Query.complaintDepartment = undefined; |
|||
tab1Query.enforceType = undefined; |
|||
tab1Query.enterpriseAddress = undefined; |
|||
timeParams.year = new Date().getFullYear().toString(); |
|||
timeParams.quarter = undefined; |
|||
timeParams.month = undefined; |
|||
timeParams.beginTime = undefined; |
|||
timeParams.endTime = undefined; |
|||
timeParams.timeType = 'year'; |
|||
groupPageNum.value = 1; |
|||
loadTab1Data(); |
|||
}; |
|||
|
|||
/** Tab1翻页 */ |
|||
const handleGroupPageChange = () => { |
|||
loadTab1Data(); |
|||
}; |
|||
|
|||
/** 点击投诉数量→弹出明细 */ |
|||
const handleShowDetail = (row: ComplaintGroupVo) => { |
|||
if (row.total <= 0) return; |
|||
currentDeptId.value = row.complaintDepartmentId; |
|||
detailTitle.value = `${row.complaintDepartment} — 投诉明细`; |
|||
detailPageNum.value = 1; |
|||
detailDialogVisible.value = true; |
|||
loadDetailData(); |
|||
}; |
|||
|
|||
/** 加载明细数据 */ |
|||
const loadDetailData = async () => { |
|||
try { |
|||
const params: ComplaintQuery = { |
|||
...tab1Query, |
|||
...buildTimeQuery(), |
|||
complaintDepartmentId: currentDeptId.value, |
|||
pageNum: detailPageNum.value, |
|||
pageSize: detailPageSize.value |
|||
}; |
|||
const res = await getDetailList(params); |
|||
detailTableData.value = res.rows || []; |
|||
detailTotal.value = res.total || 0; |
|||
} catch (e) { |
|||
detailTableData.value = []; |
|||
detailTotal.value = 0; |
|||
} |
|||
}; |
|||
|
|||
/** 明细翻页 */ |
|||
const handleDetailPageChange = () => { |
|||
loadDetailData(); |
|||
}; |
|||
|
|||
/** Tab1导出 */ |
|||
const handleExportTab1 = async () => { |
|||
try { |
|||
const params: ComplaintQuery = { ...tab1Query, ...buildTimeQuery() }; |
|||
const res = await exportGroupList(params); |
|||
downloadBlob(res, '执法投诉统计.xlsx'); |
|||
} catch (e) { |
|||
ElMessage.error('导出失败'); |
|||
} |
|||
}; |
|||
|
|||
// ======================== Tab2: 数据加载 ======================== |
|||
const loadTab2Data = async () => { |
|||
loading2.value = true; |
|||
try { |
|||
const params: ComplaintQuery = { ...buildTimeQuery(), type: tab2TypeSelect.value }; |
|||
let res; |
|||
if (tab2TypeSelect.value === 'type') { |
|||
res = await getTypeStatList(params); |
|||
} else if (tab2TypeSelect.value === '') { |
|||
res = await getAreaStatList(params); |
|||
} else { |
|||
res = await getStreetStatList(params); |
|||
} |
|||
tab2StatList.value = res.data || []; |
|||
await nextTick(); |
|||
renderPieChart(); |
|||
} finally { |
|||
loading2.value = false; |
|||
} |
|||
}; |
|||
|
|||
/** Tab2搜索 */ |
|||
const handleTab2Query = () => { |
|||
loadTab2Data(); |
|||
}; |
|||
|
|||
/** Tab2重置 */ |
|||
const resetTab2Query = () => { |
|||
timeParams.year = new Date().getFullYear().toString(); |
|||
timeParams.quarter = undefined; |
|||
timeParams.month = undefined; |
|||
timeParams.beginTime = undefined; |
|||
timeParams.endTime = undefined; |
|||
timeParams.timeType = 'year'; |
|||
tab2TypeSelect.value = 'type'; |
|||
loadTab2Data(); |
|||
}; |
|||
|
|||
/** Tab2导出 */ |
|||
const handleExportTab2 = async () => { |
|||
try { |
|||
const params: ComplaintQuery = { ...buildTimeQuery(), type: tab2TypeSelect.value }; |
|||
let res; |
|||
if (tab2TypeSelect.value === 'type') { |
|||
res = await exportTypeStat(params); |
|||
} else if (tab2TypeSelect.value === '') { |
|||
res = await exportAreaStat(params); |
|||
} else { |
|||
res = await exportStreetStat(params); |
|||
} |
|||
downloadBlob(res, '投诉类型统计.xlsx'); |
|||
} catch (e) { |
|||
ElMessage.error('导出失败'); |
|||
} |
|||
}; |
|||
|
|||
/** 饼图渲染 */ |
|||
const renderPieChart = () => { |
|||
if (!pieChartRef.value) return; |
|||
if (!pieChart) { |
|||
pieChart = echarts.init(pieChartRef.value); |
|||
} |
|||
const data = tab2StatList.value.map(item => ({ name: item.name, value: item.count })); |
|||
const option: echarts.EChartsOption = { |
|||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, |
|||
legend: { orient: 'vertical', left: 'left', top: 'middle', type: 'scroll' }, |
|||
series: [{ |
|||
type: 'pie', |
|||
radius: ['35%', '65%'], |
|||
center: ['60%', '50%'], |
|||
data, |
|||
label: { formatter: '{b}: {c} ({d}%)' }, |
|||
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } |
|||
}] |
|||
}; |
|||
pieChart.setOption(option, true); |
|||
}; |
|||
|
|||
/** Tab切换 */ |
|||
const handleTabChange = (tab: string) => { |
|||
if (tab === 'tab2' && tab2StatList.value.length === 0) { |
|||
loadTab2Data(); |
|||
} |
|||
}; |
|||
|
|||
// ======================== 通用工具 ======================== |
|||
const downloadBlob = (res: any, filename: string) => { |
|||
const blob = new Blob([res as any], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); |
|||
const url = window.URL.createObjectURL(blob); |
|||
const link = document.createElement('a'); |
|||
link.href = url; |
|||
link.download = filename; |
|||
link.click(); |
|||
window.URL.revokeObjectURL(url); |
|||
}; |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="p-2"> |
|||
<el-tabs v-model="activeTab" @tab-change="handleTabChange"> |
|||
<!-- ==================== Tab1: 执法投诉统计 ==================== --> |
|||
<el-tab-pane label="执法投诉统计" name="tab1"> |
|||
<!-- 搜索区域 --> |
|||
<el-card shadow="hover" class="mb-[10px]"> |
|||
<el-form :inline="true" label-width="90px"> |
|||
<el-form-item label="被投诉对象"> |
|||
<el-input v-model="tab1Query.complaintDepartment" placeholder="请输入被投诉对象" clearable style="width: 160px" @keyup.enter="handleTab1Query" /> |
|||
</el-form-item> |
|||
<el-form-item label="投诉类型"> |
|||
<el-select v-model="tab1Query.enforceType" placeholder="请选择投诉类型" clearable style="width: 150px"> |
|||
<el-option v-for="item in caseTypeOptions" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="涉事地址"> |
|||
<el-input v-model="tab1Query.enterpriseAddress" placeholder="请输入涉事地址" clearable style="width: 160px" @keyup.enter="handleTab1Query" /> |
|||
</el-form-item> |
|||
<el-form-item label="时段类型"> |
|||
<el-select v-model="timeParams.timeType" style="width: 100px" @change="handleTimeTypeChange"> |
|||
<el-option label="按年" value="year" /> |
|||
<el-option label="按季" value="quarter" /> |
|||
<el-option label="按月" value="month" /> |
|||
<el-option label="自定义" value="day" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="年份"> |
|||
<el-select v-model="timeParams.year" placeholder="选择年份" clearable style="width: 100px"> |
|||
<el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.timeType === 'quarter'" label="季度"> |
|||
<el-select v-model="timeParams.quarter" placeholder="选择季度" clearable style="width: 90px"> |
|||
<el-option v-for="q in quarterOptions" :key="q" :label="'第' + q + '季度'" :value="q" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.timeType === 'month'" label="月份"> |
|||
<el-select v-model="timeParams.month" placeholder="选择月份" clearable style="width: 90px"> |
|||
<el-option v-for="m in monthOptions" :key="m" :label="m + '月'" :value="m" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.timeType === 'day'" label="开始时间"> |
|||
<el-date-picker v-model="timeParams.beginTime" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" style="width: 140px" /> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.timeType === 'day'" label="结束时间"> |
|||
<el-date-picker v-model="timeParams.endTime" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" style="width: 140px" /> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" icon="Search" @click="handleTab1Query">搜索</el-button> |
|||
<el-button icon="Refresh" @click="resetTab1Query">重置</el-button> |
|||
<el-button type="warning" icon="Download" @click="handleExportTab1">导出</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</el-card> |
|||
|
|||
<!-- 概览 --> |
|||
<el-card shadow="hover" class="mb-[10px]"> |
|||
<div style="display: flex; align-items: center; gap: 24px; font-size: 14px;"> |
|||
<span>投诉数量:<strong style="color: #f56c6c;">{{ overviewData.complaintCount }}</strong></span> |
|||
<span>被投诉对象数:<strong style="color: #e6a23c;">{{ overviewData.complaintDeptCount }}</strong></span> |
|||
</div> |
|||
</el-card> |
|||
|
|||
<!-- 主表格 --> |
|||
<el-card shadow="hover" v-loading="loading1"> |
|||
<el-table :data="groupTableData" border stripe> |
|||
<el-table-column label="被投诉对象" prop="complaintDepartment" min-width="260" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="投诉数量" width="140" align="center"> |
|||
<template #default="{ row }"> |
|||
<el-link type="primary" :underline="false" @click="handleShowDetail(row)">{{ row.total }}</el-link> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<pagination v-show="groupTotal > 0" |
|||
v-model:page="groupPageNum" v-model:limit="groupPageSize" |
|||
:total="groupTotal" @pagination="handleGroupPageChange" /> |
|||
</el-card> |
|||
|
|||
<!-- 明细弹窗 --> |
|||
<el-dialog v-model="detailDialogVisible" :title="detailTitle" width="700px" destroy-on-close> |
|||
<el-table :data="detailTableData" border stripe max-height="400"> |
|||
<el-table-column label="被投诉对象" prop="complaintDepartment" min-width="160" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="投诉类型" prop="enforceTypeText" width="140" align="center" /> |
|||
<el-table-column label="投诉时间" prop="complaintTime" width="170" align="center" /> |
|||
</el-table> |
|||
<pagination v-show="detailTotal > 0" |
|||
v-model:page="detailPageNum" v-model:limit="detailPageSize" |
|||
:total="detailTotal" @pagination="handleDetailPageChange" /> |
|||
</el-dialog> |
|||
</el-tab-pane> |
|||
|
|||
<!-- ==================== Tab2: 投诉类型统计 ==================== --> |
|||
<el-tab-pane label="投诉类型统计" name="tab2"> |
|||
<!-- 搜索区域 --> |
|||
<el-card shadow="hover" class="mb-[10px]"> |
|||
<el-form :inline="true" label-width="90px"> |
|||
<el-form-item label="时段类型"> |
|||
<el-select v-model="timeParams.timeType" style="width: 100px" @change="handleTimeTypeChange"> |
|||
<el-option label="按年" value="year" /> |
|||
<el-option label="按季" value="quarter" /> |
|||
<el-option label="按月" value="month" /> |
|||
<el-option label="自定义" value="day" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="年份"> |
|||
<el-select v-model="timeParams.year" placeholder="选择年份" clearable style="width: 100px"> |
|||
<el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.timeType === 'quarter'" label="季度"> |
|||
<el-select v-model="timeParams.quarter" placeholder="选择季度" clearable style="width: 90px"> |
|||
<el-option v-for="q in quarterOptions" :key="q" :label="'第' + q + '季度'" :value="q" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.timeType === 'month'" label="月份"> |
|||
<el-select v-model="timeParams.month" placeholder="选择月份" clearable style="width: 90px"> |
|||
<el-option v-for="m in monthOptions" :key="m" :label="m + '月'" :value="m" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.timeType === 'day'" label="开始时间"> |
|||
<el-date-picker v-model="timeParams.beginTime" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" style="width: 140px" /> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.timeType === 'day'" label="结束时间"> |
|||
<el-date-picker v-model="timeParams.endTime" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" style="width: 140px" /> |
|||
</el-form-item> |
|||
<el-form-item label="类型切换"> |
|||
<el-select v-model="tab2TypeSelect" style="width: 140px" @change="loadTab2Data"> |
|||
<el-option v-for="opt in tab2TypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" icon="Search" @click="handleTab2Query">搜索</el-button> |
|||
<el-button icon="Refresh" @click="resetTab2Query">重置</el-button> |
|||
<el-button type="warning" icon="Download" @click="handleExportTab2">导出</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</el-card> |
|||
|
|||
<!-- 概览 --> |
|||
<el-card shadow="hover" class="mb-[10px]"> |
|||
<div style="display: flex; align-items: center; gap: 24px; font-size: 14px;"> |
|||
<span>总数量:<strong style="color: #409eff;">{{ tab2Total }}</strong></span> |
|||
<span>占比总计:<strong>100%</strong></span> |
|||
</div> |
|||
</el-card> |
|||
|
|||
<!-- 表格 + 饼图 --> |
|||
<el-row :gutter="16" v-loading="loading2"> |
|||
<el-col :span="12"> |
|||
<el-card shadow="hover"> |
|||
<el-table :data="tab2StatList" border stripe> |
|||
<el-table-column label="名称" prop="name" min-width="160" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="数量" prop="count" width="100" align="center" /> |
|||
<el-table-column label="占比" prop="percentage" width="100" align="center" /> |
|||
</el-table> |
|||
</el-card> |
|||
</el-col> |
|||
<el-col :span="12"> |
|||
<el-card shadow="hover"> |
|||
<div ref="pieChartRef" style="width: 100%; height: 380px;"></div> |
|||
</el-card> |
|||
</el-col> |
|||
</el-row> |
|||
</el-tab-pane> |
|||
</el-tabs> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
|
|||
</style> |
|||
|
|||
@ -1,11 +1,467 @@ |
|||
<script setup lang="ts"> |
|||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'; |
|||
import { ElMessage } from 'element-plus'; |
|||
import * as echarts from 'echarts'; |
|||
import { |
|||
getOverview, getGroupList, getDetailList, |
|||
getRankByYear, getRankByQuarter, getRankByMonth, |
|||
exportGroupList |
|||
} from '@/api/efcode/report/statistics-inspection-plan'; |
|||
import type { |
|||
InspectionPlanQuery, InspectionPlanOverviewVo, InspectionPlanGroupVo, |
|||
InspectionPlanDetailVo, InspectionPlanRankVo |
|||
} from '@/api/efcode/report/statistics-inspection-plan'; |
|||
|
|||
// 当前Tab |
|||
const activeTab = ref('tab1'); |
|||
|
|||
// ======================== 通用时段查询(Tab1) ======================== |
|||
const timeParams = reactive({ |
|||
year: new Date().getFullYear().toString(), |
|||
reportQuarter: undefined as string | undefined, |
|||
month: undefined as string | undefined, |
|||
beginTime: undefined as string | undefined, |
|||
endTime: undefined as string | undefined, |
|||
dateSearch: 'year' as string |
|||
}); |
|||
|
|||
const yearOptions = computed(() => { |
|||
const current = new Date().getFullYear(); |
|||
return Array.from({ length: 10 }, (_, i) => String(current - i)); |
|||
}); |
|||
const quarterOptions = ['1', '2', '3', '4']; |
|||
const monthOptions = Array.from({ length: 12 }, (_, i) => String(i + 1)); |
|||
|
|||
const handleTimeTypeChange = (val: string) => { |
|||
timeParams.reportQuarter = undefined; |
|||
timeParams.month = undefined; |
|||
timeParams.beginTime = undefined; |
|||
timeParams.endTime = undefined; |
|||
}; |
|||
|
|||
// 构建Tab1时段参数 |
|||
const buildTimeQuery = (): Partial<InspectionPlanQuery> => ({ |
|||
year: timeParams.year, |
|||
reportQuarter: timeParams.reportQuarter, |
|||
month: timeParams.month, |
|||
beginTime: timeParams.beginTime, |
|||
endTime: timeParams.endTime, |
|||
dateSearch: timeParams.dateSearch |
|||
}); |
|||
|
|||
// ======================== Tab1: 执法计划统计 ======================== |
|||
const loading1 = ref(false); |
|||
const tab1Query = reactive({ bureauDeptName: undefined as string | undefined }); |
|||
|
|||
// 概览 |
|||
const overviewData = reactive<InspectionPlanOverviewVo>({ total: 0, yearTotal: 0, quarterTotal: 0, monthTotal: 0 }); |
|||
|
|||
// 主表格(分组) |
|||
const groupTableData = ref<InspectionPlanGroupVo[]>([]); |
|||
const groupTotal = ref(0); |
|||
const groupPageNum = ref(1); |
|||
const groupPageSize = ref(10); |
|||
|
|||
// 明细弹窗 |
|||
const detailDialogVisible = ref(false); |
|||
const detailTitle = ref(''); |
|||
const detailTableData = ref<InspectionPlanDetailVo[]>([]); |
|||
const detailTotal = ref(0); |
|||
const detailPageNum = ref(1); |
|||
const detailPageSize = ref(10); |
|||
const currentBureauDeptId = ref(''); |
|||
const currentReportTimeType = ref(''); |
|||
|
|||
// ======================== Tab2: 执法计划排行 ======================== |
|||
const loading2 = ref(false); |
|||
const tab2Year = ref(new Date().getFullYear().toString()); |
|||
const rankYearData = ref<InspectionPlanRankVo[]>([]); |
|||
const rankQuarterData = ref<InspectionPlanRankVo[]>([]); |
|||
const rankMonthData = ref<InspectionPlanRankVo[]>([]); |
|||
|
|||
// ECharts 引用 |
|||
const yearChartRef = ref<HTMLElement>(); |
|||
const quarterChartRef = ref<HTMLElement>(); |
|||
const monthChartRef = ref<HTMLElement>(); |
|||
let yearChart: echarts.ECharts | null = null; |
|||
let quarterChart: echarts.ECharts | null = null; |
|||
let monthChart: echarts.ECharts | null = null; |
|||
|
|||
// ======================== 生命周期 ======================== |
|||
onMounted(() => { |
|||
loadTab1Data(); |
|||
window.addEventListener('resize', handleResize); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
window.removeEventListener('resize', handleResize); |
|||
yearChart?.dispose(); |
|||
quarterChart?.dispose(); |
|||
monthChart?.dispose(); |
|||
}); |
|||
|
|||
const handleResize = () => { |
|||
yearChart?.resize(); |
|||
quarterChart?.resize(); |
|||
monthChart?.resize(); |
|||
}; |
|||
|
|||
// ======================== Tab1: 数据加载 ======================== |
|||
const loadTab1Data = async () => { |
|||
loading1.value = true; |
|||
try { |
|||
const params: InspectionPlanQuery = { |
|||
...tab1Query, |
|||
...buildTimeQuery(), |
|||
pageNum: groupPageNum.value, |
|||
pageSize: groupPageSize.value |
|||
}; |
|||
const [overviewRes, groupRes] = await Promise.all([ |
|||
getOverview(params), |
|||
getGroupList(params) |
|||
]); |
|||
const ov = overviewRes.data || {}; |
|||
overviewData.total = Number(ov.total) || 0; |
|||
overviewData.yearTotal = Number(ov.yearTotal) || 0; |
|||
overviewData.quarterTotal = Number(ov.quarterTotal) || 0; |
|||
overviewData.monthTotal = Number(ov.monthTotal) || 0; |
|||
groupTableData.value = groupRes.rows || []; |
|||
groupTotal.value = groupRes.total || 0; |
|||
} finally { |
|||
loading1.value = false; |
|||
} |
|||
}; |
|||
|
|||
/** Tab1搜索 */ |
|||
const handleTab1Query = () => { |
|||
groupPageNum.value = 1; |
|||
loadTab1Data(); |
|||
}; |
|||
|
|||
/** Tab1重置 */ |
|||
const resetTab1Query = () => { |
|||
tab1Query.bureauDeptName = undefined; |
|||
timeParams.year = new Date().getFullYear().toString(); |
|||
timeParams.reportQuarter = undefined; |
|||
timeParams.month = undefined; |
|||
timeParams.beginTime = undefined; |
|||
timeParams.endTime = undefined; |
|||
timeParams.dateSearch = 'year'; |
|||
groupPageNum.value = 1; |
|||
loadTab1Data(); |
|||
}; |
|||
|
|||
/** Tab1翻页 */ |
|||
const handleGroupPageChange = () => { |
|||
loadTab1Data(); |
|||
}; |
|||
|
|||
/** 点击数量→弹出明细 */ |
|||
const handleShowDetail = (row: InspectionPlanGroupVo, type: string) => { |
|||
const count = type === 'year' ? row.yearTotal : type === 'quarter' ? row.quarterTotal : row.monthTotal; |
|||
if (!count || count <= 0) return; |
|||
currentBureauDeptId.value = row.bureauDeptId; |
|||
currentReportTimeType.value = type; |
|||
const typeLabel = type === 'year' ? '年度计划' : type === 'quarter' ? '季度计划' : '月度计划'; |
|||
detailTitle.value = `${row.bureauDeptName} — ${typeLabel}明细`; |
|||
detailPageNum.value = 1; |
|||
detailDialogVisible.value = true; |
|||
loadDetailData(); |
|||
}; |
|||
|
|||
/** 加载明细数据 */ |
|||
const loadDetailData = async () => { |
|||
try { |
|||
const params: InspectionPlanQuery = { |
|||
...tab1Query, |
|||
...buildTimeQuery(), |
|||
bureauDeptId: currentBureauDeptId.value, |
|||
reportTimeType: currentReportTimeType.value, |
|||
pageNum: detailPageNum.value, |
|||
pageSize: detailPageSize.value |
|||
}; |
|||
const res = await getDetailList(params); |
|||
detailTableData.value = res.rows || []; |
|||
detailTotal.value = res.total || 0; |
|||
} catch (e) { |
|||
detailTableData.value = []; |
|||
detailTotal.value = 0; |
|||
} |
|||
}; |
|||
|
|||
/** 明细翻页 */ |
|||
const handleDetailPageChange = () => { |
|||
loadDetailData(); |
|||
}; |
|||
|
|||
/** Tab1导出 */ |
|||
const handleExportTab1 = async () => { |
|||
try { |
|||
const params: InspectionPlanQuery = { ...tab1Query, ...buildTimeQuery() }; |
|||
const res = await exportGroupList(params); |
|||
downloadBlob(res, '检查计划统计.xlsx'); |
|||
} catch (e) { |
|||
ElMessage.error('导出失败'); |
|||
} |
|||
}; |
|||
|
|||
// ======================== Tab2: 数据加载 ======================== |
|||
const loadTab2Data = async () => { |
|||
loading2.value = true; |
|||
try { |
|||
const params: InspectionPlanQuery = { year: tab2Year.value }; |
|||
const [yearRes, quarterRes, monthRes] = await Promise.all([ |
|||
getRankByYear(params), |
|||
getRankByQuarter(params), |
|||
getRankByMonth(params) |
|||
]); |
|||
rankYearData.value = yearRes.data || []; |
|||
rankQuarterData.value = quarterRes.data || []; |
|||
rankMonthData.value = monthRes.data || []; |
|||
await nextTick(); |
|||
renderYearChart(); |
|||
renderQuarterChart(); |
|||
renderMonthChart(); |
|||
} finally { |
|||
loading2.value = false; |
|||
} |
|||
}; |
|||
|
|||
/** Tab2搜索 */ |
|||
const handleTab2Query = () => { |
|||
loadTab2Data(); |
|||
}; |
|||
|
|||
/** Tab2重置 */ |
|||
const resetTab2Query = () => { |
|||
tab2Year.value = new Date().getFullYear().toString(); |
|||
loadTab2Data(); |
|||
}; |
|||
|
|||
// ======================== ECharts 渲染 ======================== |
|||
const renderYearChart = () => { |
|||
if (!yearChartRef.value) return; |
|||
if (!yearChart) yearChart = echarts.init(yearChartRef.value); |
|||
const names = rankYearData.value.map(item => item.bureauDeptName); |
|||
const values = rankYearData.value.map(item => item.total); |
|||
const option: echarts.EChartsOption = { |
|||
title: { text: '年度计划排行 TOP10', left: 'center', textStyle: { fontSize: 14 } }, |
|||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, |
|||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, |
|||
xAxis: { type: 'category', data: names, axisLabel: { rotate: 30, fontSize: 11 } }, |
|||
yAxis: { type: 'value' }, |
|||
series: [{ type: 'bar', data: values, barMaxWidth: 40, itemStyle: { color: '#409eff' } }] |
|||
}; |
|||
yearChart.setOption(option, true); |
|||
}; |
|||
|
|||
const renderQuarterChart = () => { |
|||
if (!quarterChartRef.value) return; |
|||
if (!quarterChart) quarterChart = echarts.init(quarterChartRef.value); |
|||
const names = rankQuarterData.value.map(item => item.bureauDeptName); |
|||
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c']; |
|||
const series = [1, 2, 3, 4].map((q, idx) => ({ |
|||
name: `第${q}季度`, |
|||
type: 'bar' as const, |
|||
stack: 'quarter', |
|||
barMaxWidth: 40, |
|||
data: rankQuarterData.value.map(item => (item as any)[`quarterTotal${q}`] || 0), |
|||
itemStyle: { color: colors[idx] } |
|||
})); |
|||
const option: echarts.EChartsOption = { |
|||
title: { text: '季度计划排行 TOP10', left: 'center', textStyle: { fontSize: 14 } }, |
|||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, |
|||
legend: { bottom: 0, data: ['第1季度', '第2季度', '第3季度', '第4季度'] }, |
|||
grid: { left: '3%', right: '4%', bottom: '12%', containLabel: true }, |
|||
xAxis: { type: 'category', data: names, axisLabel: { rotate: 30, fontSize: 11 } }, |
|||
yAxis: { type: 'value' }, |
|||
series |
|||
}; |
|||
quarterChart.setOption(option, true); |
|||
}; |
|||
|
|||
const renderMonthChart = () => { |
|||
if (!monthChartRef.value) return; |
|||
if (!monthChart) monthChart = echarts.init(monthChartRef.value); |
|||
const names = rankMonthData.value.map(item => item.bureauDeptName); |
|||
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#b37feb', |
|||
'#36cfc9', '#ff85c0', '#ffc53d', '#73d13d', '#40a9ff', '#ff7a45']; |
|||
const series = Array.from({ length: 12 }, (_, i) => ({ |
|||
name: `${i + 1}月`, |
|||
type: 'bar' as const, |
|||
stack: 'month', |
|||
barMaxWidth: 40, |
|||
data: rankMonthData.value.map(item => (item as any)[`monthTotal${i + 1}`] || 0), |
|||
itemStyle: { color: colors[i] } |
|||
})); |
|||
const option: echarts.EChartsOption = { |
|||
title: { text: '月度计划排行 TOP10', left: 'center', textStyle: { fontSize: 14 } }, |
|||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, |
|||
legend: { bottom: 0, data: Array.from({ length: 12 }, (_, i) => `${i + 1}月`), type: 'scroll' }, |
|||
grid: { left: '3%', right: '4%', bottom: '12%', containLabel: true }, |
|||
xAxis: { type: 'category', data: names, axisLabel: { rotate: 30, fontSize: 11 } }, |
|||
yAxis: { type: 'value' }, |
|||
series |
|||
}; |
|||
monthChart.setOption(option, true); |
|||
}; |
|||
|
|||
/** Tab切换 */ |
|||
const handleTabChange = (tab: string) => { |
|||
if (tab === 'tab2' && rankYearData.value.length === 0) { |
|||
loadTab2Data(); |
|||
} |
|||
}; |
|||
|
|||
// ======================== 通用工具 ======================== |
|||
const downloadBlob = (res: any, filename: string) => { |
|||
const blob = new Blob([res as any], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); |
|||
const url = window.URL.createObjectURL(blob); |
|||
const link = document.createElement('a'); |
|||
link.href = url; |
|||
link.download = filename; |
|||
link.click(); |
|||
window.URL.revokeObjectURL(url); |
|||
}; |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="p-2"> |
|||
<el-tabs v-model="activeTab" @tab-change="handleTabChange"> |
|||
<!-- ==================== Tab1: 执法计划统计 ==================== --> |
|||
<el-tab-pane label="执法计划统计" name="tab1"> |
|||
<!-- 搜索区域 --> |
|||
<el-card shadow="hover" class="mb-[10px]"> |
|||
<el-form :inline="true" label-width="90px"> |
|||
<el-form-item label="执法单位"> |
|||
<el-input v-model="tab1Query.bureauDeptName" placeholder="请输入执法单位" clearable style="width: 160px" @keyup.enter="handleTab1Query" /> |
|||
</el-form-item> |
|||
<el-form-item label="时段类型"> |
|||
<el-select v-model="timeParams.dateSearch" style="width: 100px" @change="handleTimeTypeChange"> |
|||
<el-option label="按年" value="year" /> |
|||
<el-option label="按季" value="quarter" /> |
|||
<el-option label="按月" value="month" /> |
|||
<el-option label="自定义" value="day" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="年份"> |
|||
<el-select v-model="timeParams.year" placeholder="选择年份" clearable style="width: 100px"> |
|||
<el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.dateSearch === 'quarter'" label="季度"> |
|||
<el-select v-model="timeParams.reportQuarter" placeholder="选择季度" clearable style="width: 90px"> |
|||
<el-option v-for="q in quarterOptions" :key="q" :label="'第' + q + '季度'" :value="q" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.dateSearch === 'month'" label="月份"> |
|||
<el-select v-model="timeParams.month" placeholder="选择月份" clearable style="width: 90px"> |
|||
<el-option v-for="m in monthOptions" :key="m" :label="m + '月'" :value="m" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.dateSearch === 'day'" label="开始时间"> |
|||
<el-date-picker v-model="timeParams.beginTime" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" style="width: 140px" /> |
|||
</el-form-item> |
|||
<el-form-item v-if="timeParams.dateSearch === 'day'" label="结束时间"> |
|||
<el-date-picker v-model="timeParams.endTime" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" style="width: 140px" /> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" icon="Search" @click="handleTab1Query">搜索</el-button> |
|||
<el-button icon="Refresh" @click="resetTab1Query">重置</el-button> |
|||
<el-button type="warning" icon="Download" @click="handleExportTab1">导出</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</el-card> |
|||
|
|||
<!-- 概览 --> |
|||
<el-card shadow="hover" class="mb-[10px]"> |
|||
<div style="display: flex; align-items: center; gap: 24px; font-size: 14px;"> |
|||
<span>计划总数:<strong style="color: #409eff;">{{ overviewData.total }}</strong></span> |
|||
<span>年度计划:<strong style="color: #67c23a;">{{ overviewData.yearTotal }}</strong></span> |
|||
<span>季度计划:<strong style="color: #e6a23c;">{{ overviewData.quarterTotal }}</strong></span> |
|||
<span>月度计划:<strong style="color: #f56c6c;">{{ overviewData.monthTotal }}</strong></span> |
|||
</div> |
|||
</el-card> |
|||
|
|||
<!-- 主表格 --> |
|||
<el-card shadow="hover" v-loading="loading1"> |
|||
<el-table :data="groupTableData" border stripe> |
|||
<el-table-column label="执法单位" prop="bureauDeptName" min-width="260" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="总数" prop="total" width="100" align="center" /> |
|||
<el-table-column label="年度计划" width="120" align="center"> |
|||
<template #default="{ row }"> |
|||
<el-link type="primary" :underline="false" @click="handleShowDetail(row, 'year')">{{ row.yearTotal }}</el-link> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="季度计划" width="120" align="center"> |
|||
<template #default="{ row }"> |
|||
<el-link type="primary" :underline="false" @click="handleShowDetail(row, 'quarter')">{{ row.quarterTotal }}</el-link> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="月度计划" width="120" align="center"> |
|||
<template #default="{ row }"> |
|||
<el-link type="primary" :underline="false" @click="handleShowDetail(row, 'month')">{{ row.monthTotal }}</el-link> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<pagination v-show="groupTotal > 0" |
|||
v-model:page="groupPageNum" v-model:limit="groupPageSize" |
|||
:total="groupTotal" @pagination="handleGroupPageChange" /> |
|||
</el-card> |
|||
|
|||
<!-- 明细弹窗 --> |
|||
<el-dialog v-model="detailDialogVisible" :title="detailTitle" width="750px" destroy-on-close> |
|||
<el-table :data="detailTableData" border stripe max-height="400"> |
|||
<el-table-column label="计划名称" prop="planName" min-width="280" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="执法单位" prop="bureauDeptName" width="160" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="执法区域" prop="enforcementArea" width="120" align="center" /> |
|||
<el-table-column label="发布时间" prop="enforcementTime" width="170" align="center" /> |
|||
</el-table> |
|||
<pagination v-show="detailTotal > 0" |
|||
v-model:page="detailPageNum" v-model:limit="detailPageSize" |
|||
:total="detailTotal" @pagination="handleDetailPageChange" /> |
|||
</el-dialog> |
|||
</el-tab-pane> |
|||
|
|||
<!-- ==================== Tab2: 执法计划排行 ==================== --> |
|||
<el-tab-pane label="执法计划排行" name="tab2"> |
|||
<!-- 搜索区域 --> |
|||
<el-card shadow="hover" class="mb-[10px]"> |
|||
<el-form :inline="true" label-width="90px"> |
|||
<el-form-item label="年份"> |
|||
<el-select v-model="tab2Year" placeholder="选择年份" style="width: 120px"> |
|||
<el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" icon="Search" @click="handleTab2Query">搜索</el-button> |
|||
<el-button icon="Refresh" @click="resetTab2Query">重置</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</el-card> |
|||
|
|||
<!-- 三个柱状图 --> |
|||
<el-row :gutter="16" v-loading="loading2"> |
|||
<el-col :span="24" class="mb-[16px]"> |
|||
<el-card shadow="hover"> |
|||
<div ref="yearChartRef" style="width: 100%; height: 350px;"></div> |
|||
</el-card> |
|||
</el-col> |
|||
<el-col :span="24" class="mb-[16px]"> |
|||
<el-card shadow="hover"> |
|||
<div ref="quarterChartRef" style="width: 100%; height: 380px;"></div> |
|||
</el-card> |
|||
</el-col> |
|||
<el-col :span="24"> |
|||
<el-card shadow="hover"> |
|||
<div ref="monthChartRef" style="width: 100%; height: 400px;"></div> |
|||
</el-card> |
|||
</el-col> |
|||
</el-row> |
|||
</el-tab-pane> |
|||
</el-tabs> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
|
|||
</style> |
|||
|
|||
Loading…
Reference in new issue