新增分工会活动达标统计菜单

This commit is contained in:
2026-10-08 18:00:18 +08:00
parent 1ae8ae167a
commit 33943a1d62
8 changed files with 706 additions and 7 deletions
@@ -302,6 +302,8 @@ public class FitnessWalkStepRankingController {
entityList.add(new ExcelExportEntity("分工会", "unionname", 20));
entityList.add(new ExcelExportEntity("单位", "unitname", 20));
entityList.add(new ExcelExportEntity("today".equals(mode) ? "今日步数" : "活动期间总步数", "total_steps", 20));
// 与页面共用排行榜查询结果,今日榜导出也展示活动期间累计的达标天数。
entityList.add(new ExcelExportEntity("达标天数", "standardsDays", 20));
try {
String fileName = ("today".equals(mode) ? DateUtil.format(new Date(), "yyyy年MM月dd日") : "活动期间总") + "步数榜.xlsx";
ExportParams exportParams = new ExportParams();
@@ -88,9 +88,9 @@ public class FitnessWalkStepStatisticsController {
@Param("year") Integer year,
@Param("activityId") String activityId,
@Param("activityDate") String[] activityDate,
@Param("unionId") String unionId,
@Param("unitId") String unitId) {
CndPlus cnd = CndPlus.create();
String unionId,
String unitId) {
Cnd cnd = Cnd.NEW();
Sql sql = Sqls.create(getSql());
if (Lang.isNotEmpty(activityDate)) {
cnd.and("date_format(al.applyDate,'%Y-%m-%d')", ">=", activityDate[0]);
@@ -0,0 +1,70 @@
package com.budwk.app.zhgh.activity.fitnesswalk.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkStepUnionStatisticsService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
/**
* 分工会活动达标统计,活动资格判断及人数汇总统一交由 service 处理。
*/
@IocBean
@Ok("json")
@At("/platform/fitnessWalk/stepUnionStatistics")
public class FitnessWalkStepUnionStatisticsController {
@Inject
private FitnessWalkStepUnionStatisticsService fitnessWalkStepUnionStatisticsService;
/**
* 打开分工会活动达标统计页面,菜单链接与当前路由保持一致。
*/
@At("")
@Ok("beetl:/platform/zhgh/activity/fitnesswalk/stepUnionStatistics.html")
@SaCheckPermission("fitnessWalk.stepUnionStatistics")
public void index() {
}
/**
* 获取年份对应的活动选项,供页面多选;默认查询本年度。
*
* @param year 活动开始年份,不传时使用当前年份
* @return Result;data 为活动选项数组,选项的 value 是活动 ID,label 和 ruleLabel 用于展示名称及达标标准
*/
@At
@SaCheckPermission("fitnessWalk.stepUnionStatistics")
public Result activityOptions(@Param("year") Integer year) {
try {
return Result.success(fitnessWalkStepUnionStatisticsService.getActivityOptions(year == null ? DateUtil.thisYear() : year));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 接收活动多选及分工会筛选,返回各活动的达标结果和所选范围的平均达标率。
*
* @param year 活动开始年份,不传时使用当前年份
* @param activityIds 活动 ID 数组,页面通过 JSON 数组提交;空数组表示该年份全部活动
* @param unionId 分工会 ID,空值表示查询当前账号可查看的全部分工会
* @return Result;data.activities 为实际统计活动,data.list 为分工会行,
* activityStatistics 按活动 ID 保存人数及比例,averageQualifiedRate 为所选活动平均达标率
*/
@At
@Ok("json:full")
@SaCheckPermission("fitnessWalk.stepUnionStatistics")
public Result pageData(@Param("year") Integer year,
@Param("activityIds") String[] activityIds,
@Param("unionId") String unionId) {
try {
return Result.success(fitnessWalkStepUnionStatisticsService.getUnionStatistics(year == null ? DateUtil.thisYear() : year, activityIds, unionId));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
}
@@ -0,0 +1,33 @@
package com.budwk.app.zhgh.activity.fitnesswalk.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkStep;
import org.nutz.lang.util.NutMap;
import java.util.List;
/**
* 按计步活动统计分工会会员的达标人数、达标率及所选活动的平均达标率。
*/
public interface FitnessWalkStepUnionStatisticsService extends BaseService<FitnessWalkStep> {
/**
* 查询指定年份开始、且当前账号可查看的计步活动。
*
* @param year 活动开始年份,例如 2026
* @return 活动选项列表;value 为活动 ID,label 为活动名称及日期,ruleLabel 为该活动的达标规则
*/
List<NutMap> getActivityOptions(int year);
/**
* 使用各活动自己的达标配置统计分工会,活动内按人员去重,活动之间分别计算。
*
* @param year 活动开始年份,例如 2026
* @param activityIds 所选活动 ID 数组;未选择时统计该年份全部可查看活动,已选择时严格限定所选活动
* @param unionId 分工会 ID;管理员传空值时统计全部分工会,其他账号仅统计自己所属分工会
* @return 统计结果 Map;activities 为实际统计的活动选项,list 为分工会汇总行。
* 每行包含 unionId、unionName、memberNum、activityStatistics 和 averageQualifiedRate;
* activityStatistics 按活动 ID 保存 qualifiedNum(达标人数)及 qualifiedRate(百分比文本)
*/
NutMap getUnionStatistics(int year, String[] activityIds, String unionId);
}
@@ -547,6 +547,17 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl<FitnessWalkAwa
}
/**
* 构建排行榜查询,步数按排行榜模式统计,达标天数始终按所选活动期间统计。
*
* @param searchName 姓名或工号查询字段,沿用页面传入的 u.username 或 u.loginname
* @param searchKeyword 姓名或工号关键字,空值表示不按关键字筛选
* @param activityId 所选活动 ID,用于读取活动起止日期及抽奖资格配置
* @param unionId 分工会 ID,空值表示不限制分工会
* @param unitId 单位 ID,空值表示不限制单位
* @param mode today 统计今日步数,all 统计活动期间总步数
* @return 排行榜查询 SQL;total_steps 为当前模式步数,standardsDays 为活动期间累计达标天数,未达标返回 0
*/
private Sql rankingListSql(String searchName, String searchKeyword, String activityId, String unionId, String unitId, String mode) {
FitnessWalkActivity activity = getActivity(activityId);
CndPlus cnd = CndPlus.create();
@@ -559,7 +570,7 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl<FitnessWalkAwa
u.loginname,
u.unionName AS unionname,
u.unitName AS unitname,
over3k.standardsDays
COALESCE(over3k.standardsDays, 0) AS standardsDays
FROM
vw_user u
LEFT JOIN (
@@ -584,15 +595,24 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl<FitnessWalkAwa
FROM
`fitness_walk_step`
WHERE
step >= @step
AND activityId=@activityId
AND $dateSql
activityId=@activityId
AND DATE(applyDate) >= @activityStartDate
AND DATE(applyDate) <= @activityEndDate
AND (@requireDailyQualification = 0 OR step >= @qualificationStep)
GROUP BY
userId
) AS over3k ON u.id = over3k.userId
$condition
""").setParam("activityId",activityId).setParam("step",0);
// 达标天数独立使用活动日期,切换到今日榜时也保留整个活动期间的累计结果。
sql.setParam("activityStartDate", DateUtil.format(DateUtil.date(activity.getStartTime()), "yyyy-MM-dd"));
sql.setParam("activityEndDate", DateUtil.format(DateUtil.date(activity.getEndTime()), "yyyy-MM-dd"));
// 与计步抽奖页一致:配置资格天数时按每日资格步数计数,否则统计有记录的日期。
// 历史活动的资格步数为空时按 0 处理;同一天重复记录通过 COUNT(DISTINCT DATE) 只计一次。
sql.setParam("requireDailyQualification", activity.getLotteryQualificationDays() == null ? 0 : 1);
sql.setParam("qualificationStep", activity.getLotteryQualificationSteps() == null ? 0 : activity.getLotteryQualificationSteps());
if ("today".equals(mode)) {
sql.setVar("dateSql",new Static("DATE(applyDate) = '"+DateUtil.today()+"'"));
// cnd.and("DATE( s.applyDate )", "=", DateUtil.today());
@@ -0,0 +1,286 @@
package com.budwk.app.zhgh.activity.fitnesswalk.service.impl;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.fitnesswalk.contants.FitnessWalkMode;
import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivity;
import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkStep;
import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkStepUnionStatisticsService;
import com.budwk.app.zhgh.activity.fitnesswalk.utils.WeAppCloudUtil;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 分工会计步活动达标统计;使用当前会员人数作为分母,每个活动单独判断人员资格。
*/
@IocBean(args = {"refer:dao"})
public class FitnessWalkStepUnionStatisticsServiceImpl extends BaseServiceImpl<FitnessWalkStep> implements FitnessWalkStepUnionStatisticsService {
private static final int ACTIVITY_QUERY_BATCH_SIZE = 100;
@Inject
private WeAppCloudUtil weAppCloudUtil;
/**
* @param dao 项目注入的 Nutz 数据访问对象,用于查询当前会员及活动步数记录
*/
public FitnessWalkStepUnionStatisticsServiceImpl(Dao dao) {
super(dao);
}
/**
* @param year 活动开始年份,允许查询 1970 年至当前年份
* @return 当前账号可查看的活动选项,包含 ID、名称、日期和达标规则
*/
@Override
public List<NutMap> getActivityOptions(int year) {
return queryYearActivities(year).stream().map(this::toActivityOption).collect(Collectors.toList());
}
/**
* @param year 活动开始年份
* @param activityIds 所选活动 ID;空数组查询全部活动,非空数组只查询所选活动
* @param unionId 分工会 ID;非管理员的查询范围由服务层限制为本人分工会
* @return activities 为实际统计活动,list 为全部可查看分工会的活动达标矩阵;比例以两位小数百分比返回
*/
@Override
public NutMap getUnionStatistics(int year, String[] activityIds, String unionId) {
List<FitnessWalkActivity> activities = resolveSelectedActivities(queryYearActivities(year), activityIds);
List<NutMap> activityOptions = activities.stream().map(this::toActivityOption).collect(Collectors.toList());
if (activities.isEmpty()) {
return NutMap.NEW().addv("activities", activityOptions).addv("list", Collections.emptyList());
}
String effectiveUnionId = resolveUnionId(unionId);
// 分母和达标人数均限定为当前会员,避免未入会人员计入分子后出现超过 100% 的比例。
Sql memberSql = Sqls.create("""
SELECT
un.id AS unionId,
un.name AS unionName,
COALESCE(m.memberNum, 0) AS memberNum
FROM sys_union un
LEFT JOIN (
SELECT unionId, COUNT(DISTINCT id) AS memberNum
FROM vw_user
WHERE member = 1
GROUP BY unionId
) m ON m.unionId = un.id
WHERE (@hasUnionFilter = 0 OR un.id = @unionId)
ORDER BY un.unionCode
""");
memberSql.setParam("hasUnionFilter", Strings.isBlank(effectiveUnionId) ? 0 : 1);
memberSql.setParam("unionId", Strings.sNull(effectiveUnionId));
List<NutMap> rows = listMap(memberSql);
Map<String, Map<String, Long>> activityCounts = new LinkedHashMap<>();
for (FitnessWalkActivity activity : activities) {
Map<String, Long> unionCounts = new LinkedHashMap<>();
for (NutMap countRow : listMap(buildQualifiedCountSql(activity, effectiveUnionId))) {
unionCounts.put(countRow.getString("unionId"), countRow.getLong("qualifiedNum", 0));
}
activityCounts.put(activity.get_id(), unionCounts);
}
for (NutMap row : rows) {
String rowUnionId = row.getString("unionId");
long memberNum = row.getLong("memberNum", 0);
long qualifiedTotal = 0;
NutMap activityStatistics = NutMap.NEW();
for (FitnessWalkActivity activity : activities) {
long qualifiedNum = activityCounts.get(activity.get_id()).getOrDefault(rowUnionId, 0L);
qualifiedTotal += qualifiedNum;
activityStatistics.put(activity.get_id(), NutMap.NEW()
.addv("qualifiedNum", qualifiedNum)
.addv("qualifiedRate", formatRate(qualifiedNum, BigDecimal.valueOf(memberNum))));
}
row.put("activityStatistics", activityStatistics);
// 每个活动使用同一分工会会员分母,因此先汇总人数再除以会员人数×活动数,等价于活动达标率的算术平均。
// 直接使用原始人数计算,避免先对单活动比例四舍五入后再平均造成误差;零达标活动仍计入活动数。
row.put("averageQualifiedRate", formatRate(qualifiedTotal, BigDecimal.valueOf(memberNum).multiply(BigDecimal.valueOf(activities.size()))));
}
return NutMap.NEW().addv("activities", activityOptions).addv("list", rows);
}
/**
* 分批获取指定年份的活动及资格配置,避免云查询默认条数和公共活动缓存影响新统计页面。
*/
private List<FitnessWalkActivity> queryYearActivities(int year) {
if (year < 1970 || year > DateUtil.thisYear()) {
throw new IllegalArgumentException("请选择有效的活动年份");
}
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year);
long startTs = DateUtil.beginOfYear(calendar.getTime()).getTime();
long endTs = DateUtil.endOfYear(calendar.getTime()).getTime();
String effectiveUnionId = resolveUnionId(null);
StringBuilder query = new StringBuilder("db.collection('activity').where({activityModel:");
query.append(JSON.toJSONString(FitnessWalkMode.STEP_COUNT.getValue()))
.append(",startTime:_.gte(").append(startTs).append(").and(_.lte(").append(endTs).append("))");
if (Strings.isNotBlank(effectiveUnionId)) {
query.append(",unionId:").append(JSON.toJSONString(effectiveUnionId));
}
query.append("}).field({_id:true,masterActivityName:true,branchActivityName:true,activityModel:true,")
.append("startTime:true,endTime:true,lotteryQualificationDays:true,lotteryQualificationSteps:true,unionId:true})")
.append(".orderBy('startTime','desc').orderBy('_id','asc')");
Map<String, FitnessWalkActivity> activityMap = new LinkedHashMap<>();
int skip = 0;
while (true) {
String sql = query + ".skip(" + skip + ").limit(" + ACTIVITY_QUERY_BATCH_SIZE + ").get()";
JSONObject response = weAppCloudUtil.request(WeAppCloudUtil.CRUD.QUERY, sql);
JSONArray data = response.getJSONArray("data");
if (data == null) {
throw new IllegalArgumentException("活动列表获取失败,请稍后重试");
}
for (Object item : data) {
FitnessWalkActivity activity = JSON.parseObject((String) item, FitnessWalkActivity.class);
// 历史活动缺失日期时不进入统计,防止空日期变成当前日期而误算达标人数。
if (Strings.isNotBlank(activity.get_id()) && activity.getStartTime() != null && activity.getEndTime() != null
&& activity.getStartTime() <= activity.getEndTime()) {
activityMap.put(activity.get_id(), activity);
}
}
if (data.size() < ACTIVITY_QUERY_BATCH_SIZE) {
break;
}
skip += ACTIVITY_QUERY_BATCH_SIZE;
}
return new ArrayList<>(activityMap.values());
}
/**
* 已选择活动时逐个校验年份和可见范围,失效选项报错,避免错误地扩大为全部活动。
*/
private List<FitnessWalkActivity> resolveSelectedActivities(List<FitnessWalkActivity> availableActivities, String[] activityIds) {
if (activityIds == null || activityIds.length == 0) {
return availableActivities;
}
Set<String> selectedIds = new LinkedHashSet<>();
for (String activityId : activityIds) {
if (Strings.isBlank(activityId)) {
throw new IllegalArgumentException("所选活动无效,请重新选择");
}
selectedIds.add(activityId);
}
Set<String> availableIds = availableActivities.stream().map(FitnessWalkActivity::get_id).collect(Collectors.toSet());
if (!availableIds.containsAll(selectedIds)) {
throw new IllegalArgumentException("所选活动不属于当前年份或不可查看,请刷新活动列表后重新选择");
}
return availableActivities.stream().filter(activity -> selectedIds.contains(activity.get_id())).collect(Collectors.toList());
}
/**
* 管理员可筛选全部分工会;其他账号忽略外部传入范围,只统计本人所属分工会。
*/
private String resolveUnionId(String unionId) {
if (AuthUtil.hasRoleOr("SYSADMIN", "SCHOOL_UNION_ADMIN")) {
return unionId;
}
String currentUnionId = SecurityUtil.getUnionId();
if (Strings.isBlank(currentUnionId)) {
throw new IllegalArgumentException("当前账号未关联分工会,无法查询统计");
}
return currentUnionId;
}
/**
* 按计步抽奖页的三种资格口径统计人数:达标天数、累计步数或有步数记录。
* 有资格天数时只计达到每日步数门槛的日期;同日重复上传保留最大步数,避免累计步数重复计算。
*/
private Sql buildQualifiedCountSql(FitnessWalkActivity activity, String unionId) {
Sql sql = Sqls.create("""
SELECT qualified.unionId, COUNT(DISTINCT qualified.userId) AS qualifiedNum
FROM (
SELECT u.id AS userId, u.unionId
FROM vw_user u
LEFT JOIN (
SELECT userId, DATE(applyDate) AS applyDay, MAX(step) AS daySteps
FROM fitness_walk_step
WHERE activityId = @activityId
AND DATE(applyDate) >= @startDate
AND DATE(applyDate) <= @endDate
AND (@hasDayRequirement = 0 OR step >= @qualificationSteps)
GROUP BY userId, DATE(applyDate)
) daily ON daily.userId = u.id
WHERE u.member = 1 AND (@hasUnionFilter = 0 OR u.unionId = @unionId)
GROUP BY u.id, u.unionId
HAVING (
(@hasDayRequirement = 1 AND COUNT(DISTINCT daily.applyDay) > 0
AND COUNT(DISTINCT daily.applyDay) >= @qualificationDays)
OR (@hasDayRequirement = 0 AND @hasStepRequirement = 1
AND COALESCE(SUM(daily.daySteps), 0) >= @qualificationSteps)
OR (@hasDayRequirement = 0 AND @hasStepRequirement = 0
AND COALESCE(SUM(daily.daySteps), 0) > 0)
)
) qualified
GROUP BY qualified.unionId
""");
sql.setParam("activityId", activity.get_id());
sql.setParam("startDate", DateUtil.format(DateUtil.date(activity.getStartTime()), "yyyy-MM-dd"));
sql.setParam("endDate", DateUtil.format(DateUtil.date(activity.getEndTime()), "yyyy-MM-dd"));
sql.setParam("hasDayRequirement", activity.getLotteryQualificationDays() == null ? 0 : 1);
sql.setParam("hasStepRequirement", activity.getLotteryQualificationSteps() == null ? 0 : 1);
sql.setParam("qualificationDays", activity.getLotteryQualificationDays() == null ? 0 : activity.getLotteryQualificationDays());
sql.setParam("qualificationSteps", activity.getLotteryQualificationSteps() == null ? 0 : activity.getLotteryQualificationSteps());
sql.setParam("hasUnionFilter", Strings.isBlank(unionId) ? 0 : 1);
sql.setParam("unionId", Strings.sNull(unionId));
return sql;
}
/**
* 名称同时包含主活动、分活动及日期,避免同名月度活动在多选和表头中无法区分。
*/
private NutMap toActivityOption(FitnessWalkActivity activity) {
String name = Strings.sNull(activity.getMasterActivityName());
if (Strings.isNotBlank(activity.getBranchActivityName()) && !activity.getBranchActivityName().equals(name)) {
name += " / " + activity.getBranchActivityName();
}
String startDate = DateUtil.format(DateUtil.date(activity.getStartTime()), "yyyy-MM-dd");
String endDate = DateUtil.format(DateUtil.date(activity.getEndTime()), "yyyy-MM-dd");
String ruleLabel;
if (activity.getLotteryQualificationDays() != null) {
int steps = activity.getLotteryQualificationSteps() == null ? 0 : activity.getLotteryQualificationSteps();
ruleLabel = "每日至少" + steps + "步,活动期间至少达标" + activity.getLotteryQualificationDays() + "天";
} else if (activity.getLotteryQualificationSteps() != null) {
ruleLabel = "活动期间累计至少" + activity.getLotteryQualificationSteps() + "步";
} else {
ruleLabel = "活动期间总步数大于0";
}
return NutMap.NEW().addv("value", activity.get_id()).addv("name", name)
.addv("label", name + "(" + startDate + " 至 " + endDate + ")")
.addv("ruleLabel", ruleLabel);
}
/**
* 统一输出两位小数百分比;没有会员或没有统计活动时返回 0.00%,避免除零。
*/
private String formatRate(long qualifiedNum, BigDecimal denominator) {
if (denominator.signum() <= 0) {
return "0.00%";
}
return BigDecimal.valueOf(qualifiedNum).multiply(BigDecimal.valueOf(100))
.divide(denominator, 2, RoundingMode.HALF_UP).toPlainString() + "%";
}
}
@@ -139,6 +139,8 @@ layout("/layouts/platform.html"){
{label: '分工会', prop: 'unionname', sortable: true},
{label: '单位', prop: 'unitname'},
{label: '步数', prop: 'total_steps', sortable: true},
// 今日榜和总榜均展示所选活动期间累计的达标天数。
{label: '达标天数', prop: 'standardsDays', sortable: true},
],
}
},
@@ -0,0 +1,286 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
/* 当前平台顶部为 64px,内容区上下留白共 20px;限制页面高度,避免数据行撑高外层。 */
.step-union-statistics {
height: calc(100vh - 84px);
min-width: 0;
overflow: hidden;
}
.step-union-statistics > .guava-main-content,
.step-union-statistics > .guava-main-content > .transition-item {
height: 100%;
min-height: 0;
overflow: hidden;
}
.step-union-statistics > .guava-main-content > .transition-item {
display: flex;
flex-direction: column;
}
.step-union-statistics .statistics-search-card,
.step-union-statistics .ele-table-tool {
flex-shrink: 0;
}
/* 表格卡片和表体允许缩小到剩余空间,筛选区与标题保持固定,由 el-table 内部滚动。 */
.step-union-statistics .statistics-table-card {
flex: 1;
min-height: 0;
min-width: 0;
}
.step-union-statistics .statistics-table-card > .el-card__body {
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
.step-union-statistics .statistics-table-container {
flex: 1;
min-height: 0;
min-width: 0;
overflow: hidden;
}
.step-union-statistics .el-table__body-wrapper {
overscroll-behavior: contain;
}
</style>
<div id="app" class="step-union-statistics" v-cloak>
<guava>
<template>
<el-card class="statistics-search-card" shadow="never">
<div class="search">
<div class="search-item">
<div class="search-item-label">年份:</div>
<div class="search-item-option">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
placeholder="选择年"
style="width: 100%"
:clearable="false"
:picker-options="pickerOptions"
:disabled="formLoading"
@change="getActivityOptions">
</el-date-picker>
</div>
</div>
<div class="search-item">
<div class="search-item-label">活动:</div>
<div class="search-item-option">
<el-select
v-model="pageForm.activityIds"
multiple
collapse-tags
filterable
clearable
:disabled="formLoading"
placeholder="不选择则统计所选年份全部活动"
style="width: 100%"
@change="queryActivityStatistics">
<el-option
v-for="item in activityOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
</div>
<div class="search-item">
<div class="search-item-label">分工会:</div>
<div class="search-item-option">
<el-select
v-model="pageForm.unionId"
clearable
filterable
:disabled="formLoading"
placeholder="请选择分工会"
style="width: 100%"
@change="queryActivityStatistics">
<el-option
v-for="item in unionOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</div>
</div>
<div class="search-query">
<el-button
@click="queryActivityStatistics"
:disabled="formLoading"
:loading="tableLoading"
icon="el-icon-search"
type="primary">
</el-button>
</div>
</div>
<el-alert
class="mt10"
title="各活动按自己的达标规则统计;分工会人数为当前会员人数。平均达标率为本次统计活动达标率的平均值。"
type="info"
:closable="false"
show-icon>
</el-alert>
</el-card>
<el-card class="mt10 statistics-table-card" shadow="never">
<table-tool label="分工会活动达标统计">
<span>统计活动:{{ statisticsActivities.length }} 个</span>
</table-tool>
<div class="statistics-table-container">
<el-table :data="tableData" :size="tableSize" v-loading="tableLoading" height="100%" border>
<el-table-column :index="indexMethod" label="序号" type="index" width="70"
align="center" header-align="center" fixed="left"></el-table-column>
<el-table-column label="分工会" prop="unionName" min-width="180"
align="center" header-align="center" fixed="left"></el-table-column>
<el-table-column label="分工会人数" prop="memberNum" width="120"
align="center" header-align="center"></el-table-column>
<el-table-column
v-for="activity in statisticsActivities"
:key="activity.value"
:label="activity.label"
align="center"
header-align="center">
<template slot="header">
<el-tooltip :content="activity.ruleLabel" placement="top">
<span>{{ activity.label }}</span>
</el-tooltip>
</template>
<el-table-column label="达标人数" width="110" align="center" header-align="center">
<template slot-scope="{row}">
{{ row.activityStatistics[activity.value].qualifiedNum }}
</template>
</el-table-column>
<el-table-column label="达标率" width="110" align="center" header-align="center">
<template slot-scope="{row}">
{{ row.activityStatistics[activity.value].qualifiedRate }}
</template>
</el-table-column>
</el-table-column>
<el-table-column label="平均达标率" prop="averageQualifiedRate" width="130"
align="center" header-align="center" fixed="right"></el-table-column>
</el-table>
</div>
</el-card>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
return {
pageForm: {
year: new Date().getFullYear() + '',
activityIds: [],
unionId: ''
},
pickerOptions: {
disabledDate(time) {
return time.getFullYear() < 1970 || time.getFullYear() > new Date().getFullYear()
}
},
unionOptions: [],
activityOptions: [],
statisticsActivities: [],
statisticsRequestVersion: 0
}
},
methods: {
/**
* 切换年份后清除上一年的所选活动,加载当前年份活动并自动查询全部活动。
*/
getActivityOptions() {
this.$set(this.pageForm, 'activityIds', [])
this.$set(this, 'activityOptions', [])
this.$set(this, 'statisticsActivities', [])
this.$set(this, 'tableData', [])
// 使上一轮统计失效,避免年份切换期间旧请求覆盖新表格。
this.$set(this, 'statisticsRequestVersion', this.statisticsRequestVersion + 1)
this.$set(this, 'tableLoading', false)
this.$set(this, 'formLoading', true)
this.$axios.post(loc() + '/activityOptions', {year: this.pageForm.year})
.then((res) => {
if (res.code === 0) {
this.$set(this, 'activityOptions', res.data || [])
this.queryActivityStatistics()
} else {
this.$message.error(res.msg)
}
})
.catch(() => {
// 请求错误由全局 axios 提示,清除选项及统计结果,避免继续选择失效活动。
this.$set(this, 'activityOptions', [])
this.$set(this, 'statisticsActivities', [])
this.$set(this, 'tableData', [])
})
.finally(() => {
this.$set(this, 'formLoading', false)
})
},
/**
* 查询完整分工会活动矩阵,表头以后台实际统计活动为准,避免活动列和结果范围不一致。
* 活动 ID 以 JSON 数组提交;未选择时为空数组,选择后只提交所选 ID。
* 此页面返回活动表头及全部分工会汇总,复用 mixin 的表格能力,不重复编写分页方法。
*/
queryActivityStatistics() {
const requestVersion = this.statisticsRequestVersion + 1
this.$set(this, 'statisticsRequestVersion', requestVersion)
this.$set(this, 'tableLoading', true)
// 新筛选条件生效时清除旧结果,接口失败时也不会保留上一组活动的统计。
this.$set(this, 'tableData', [])
this.$set(this, 'statisticsActivities', [])
const params = {
year: this.pageForm.year,
activityIds: JSON.stringify(this.pageForm.activityIds),
unionId: this.pageForm.unionId
}
this.$axios.post(loc() + '/pageData', params)
.then((res) => {
// 快速切换活动或工会时,只展示最后一次请求对应的数据。
if (requestVersion !== this.statisticsRequestVersion) {
return
}
if (res.code === 0) {
this.$set(this, 'statisticsActivities', res.data.activities || [])
this.$set(this, 'tableData', res.data.list || [])
} else {
this.$message.error(res.msg)
}
})
.catch(() => {
// 只清除本轮失败查询,较早请求失败时保留后来成功查询的结果。
if (requestVersion === this.statisticsRequestVersion) {
this.$set(this, 'statisticsActivities', [])
this.$set(this, 'tableData', [])
}
})
.finally(() => {
if (requestVersion === this.statisticsRequestVersion) {
this.$set(this, 'tableLoading', false)
}
})
}
},
created() {
this.$businessTool.listUnion().then((res) => {
this.$set(this, 'unionOptions', res)
this.getActivityOptions()
})
}
})
</script>
<!--#
}
#-->