问题活动数据看板优化
This commit is contained in:
@@ -19,8 +19,9 @@ public interface CulturalSportsBoardService {
|
||||
* @param activityId 带来源前缀的活动键,空表示全部活动;必须与 source 对应
|
||||
* @return summary 核心指标,monthly 月度人次,semesters 半年对比,units 单位明细,unions 工会分组人数及参与率,
|
||||
* composition/ages/titles/genders 各维度人数与比例,notes 为统计口径说明。
|
||||
* 参与率为 0—100 的百分数;分母为零或报名范围无法确认时 rate 为 null;范围不完整时 total/populationCount 也为 null。
|
||||
* summary.rateReason 解释缺测原因;有效报名人数不受当前人员状态限制。
|
||||
* 参与率为 0—100 的百分数;分母为零时 rate 为 null。
|
||||
* 分母为活动名单(缺失时用当前在职人员)与有效报名人员并集,按人员 ID 跨活动去重。
|
||||
* fallbackActivities 列出采用默认在职范围的活动,notes 说明统计口径。
|
||||
*/
|
||||
NutMap statistics(int year, String period, String source, String unitId, String activityId);
|
||||
}
|
||||
|
||||
+46
-70
@@ -33,7 +33,7 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
private static final String SCOPE_LABEL = "全部有效报名人员";
|
||||
private static final String SCOPE_LABEL = "活动范围与有效报名人员合并去重";
|
||||
private static final String UNKNOWN = "unknown";
|
||||
|
||||
/* 校级活动创建时会同步同 ID 到 activity_tissue;以 activity_school 为准,避免场次翻倍。
|
||||
@@ -49,7 +49,7 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
SELECT CONCAT('activity:',s.id), s.id, 'sports', s.startTime, s.name, s.activityGroupId
|
||||
FROM activity_school s WHERE s.isSave=0
|
||||
UNION ALL
|
||||
SELECT CONCAT('training:',t.id), t.id, 'training', t.activityStartTime, t.activityName, NULL
|
||||
SELECT CONCAT('training:',t.id), t.id, 'training', t.activityStartTime, t.activityName, t.activityGroupId
|
||||
FROM train_sign_up_activity t
|
||||
UNION ALL
|
||||
SELECT CONCAT('family:',f.id), f.id, 'family', f.activityStartTime, f.activityName, f.activityGroupId
|
||||
@@ -57,9 +57,9 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
""";
|
||||
|
||||
/* 档案只补充归属和人员构成,不再用当前状态、禁用或删除标记抹去历史有效报名。
|
||||
报名资格分母另取活动范围;当前档案不能冒充历史资格快照。 */
|
||||
分母优先取活动名单,名单缺失时用当前在职人员兜底,再补入有效报名人员。 */
|
||||
private static final String PEOPLE_SQL = """
|
||||
SELECT u.id, u.unitid, unit.name unit_name, unit.unionid unionid, un.unionname union_name, u.birthday, u.sex, u.personType person_type, u.jobTitle job_title
|
||||
SELECT u.id, u.userState user_state, u.unitid, unit.name unit_name, unit.unionid unionid, un.unionname union_name, u.birthday, u.sex, u.personType person_type, u.jobTitle job_title
|
||||
FROM sys_user u LEFT JOIN sys_unit unit ON unit.id=u.unitid
|
||||
LEFT JOIN sys_union un ON un.id=unit.unionid
|
||||
""";
|
||||
@@ -191,12 +191,12 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
}
|
||||
}
|
||||
|
||||
// 每个活动先核对自身范围,禁止用其他活动的名单掩盖越界报名;多活动分母取并集。
|
||||
// 只查询所选周期内活动引用的人员名单,四类活动均按实际范围 ID 关联。
|
||||
List<NutMap> selectedEvents = events.values().stream()
|
||||
.filter(event -> inPeriod(event.getInt("month"), period)).collect(Collectors.toList());
|
||||
List<String> scopeIds = selectedEvents.stream().map(event -> text(event.getString("scope_id")))
|
||||
.filter(id -> !id.isEmpty()).distinct().collect(Collectors.toList());
|
||||
Map<String, Set<String>> scopes = new HashMap<>();
|
||||
List<String> scopeIds = events.values().stream()
|
||||
.filter(event -> inPeriod(event.getInt("month"), period))
|
||||
.map(event -> text(event.getString("scope_id"))).filter(id -> !id.isEmpty()).distinct().collect(Collectors.toList());
|
||||
// 只读取当前活动引用的名单,避免每次筛选加载全部历史范围;范围 ID 均参数化。
|
||||
if (!scopeIds.isEmpty()) {
|
||||
List<String> parameters = new ArrayList<>();
|
||||
for (int index = 0; index < scopeIds.size(); index++) parameters.add("@scope" + index);
|
||||
@@ -208,36 +208,28 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
if (!id.isEmpty()) scopes.computeIfAbsent(text(row.getString("scope_id")), key -> new HashSet<>()).add(id);
|
||||
}
|
||||
}
|
||||
Set<String> eligible = new HashSet<>();
|
||||
Set<String> rateReasons = new java.util.LinkedHashSet<>();
|
||||
if (activities == 0) rateReasons.add("当前筛选范围没有已开展活动,无法确定报名范围");
|
||||
for (Map.Entry<String, NutMap> entry : events.entrySet()) {
|
||||
NutMap event = entry.getValue();
|
||||
if (!inPeriod(event.getInt("month"), period)) continue;
|
||||
if ("training".equals(event.getString("source"))) {
|
||||
rateReasons.add("品牌活动未保存完整报名资格名单,不能用当前档案推算历史分母");
|
||||
continue;
|
||||
}
|
||||
// 无范围编号或名单无有效人员时,按项目“在职”状态兜底,不推断为历史在职快照。
|
||||
Set<String> currentStaff = people.entrySet().stream()
|
||||
.filter(entry -> "在职".equals(entry.getValue().getString("user_state")))
|
||||
.map(Map.Entry::getKey).collect(Collectors.toSet());
|
||||
Set<String> population = new HashSet<>();
|
||||
List<NutMap> fallbackActivities = new ArrayList<>();
|
||||
for (NutMap event : selectedEvents) {
|
||||
Set<String> scope = scopes.get(text(event.getString("scope_id")));
|
||||
if (scope == null || scope.isEmpty()) {
|
||||
rateReasons.add("部分活动的人员范围未配置或名单已缺失");
|
||||
continue;
|
||||
}
|
||||
if (!scope.containsAll(eventPeople.getOrDefault(entry.getKey(), java.util.Collections.emptySet()))) {
|
||||
rateReasons.add("部分有效报名人员不在现存活动名单中,无法确认完整历史范围");
|
||||
scope = currentStaff;
|
||||
fallbackActivities.add(row(event.getString("event_key"), event.getString("name"))
|
||||
.setv("source", event.getString("source")));
|
||||
}
|
||||
for (String id : scope) {
|
||||
NutMap person = people.computeIfAbsent(id, key -> new NutMap().setv("id", key).setv("missing_profile", true));
|
||||
if (!selectedUnit.isEmpty() && person.getBoolean("missing_profile", false)) {
|
||||
rateReasons.add("活动范围存在档案缺失人员,无法确定其所属单位");
|
||||
}
|
||||
if (selectedUnit.isEmpty() || selectedUnit.equals(unitKey(person))) eligible.add(id);
|
||||
if (selectedUnit.isEmpty() || selectedUnit.equals(unitKey(person))) population.add(id);
|
||||
}
|
||||
}
|
||||
if (unidentifiedRecords > 0) rateReasons.add("存在缺少人员 ID 的报名记录,无法完整核对参与人数");
|
||||
boolean rateAvailable = rateReasons.isEmpty();
|
||||
Set<String> displayedPeople = new HashSet<>(selectedPeople);
|
||||
if (rateAvailable) displayedPeople.addAll(eligible);
|
||||
// 范围外有效报名补入分母;跨活动取并集,每名人员只计一次。
|
||||
population.addAll(selectedPeople);
|
||||
boolean rateAvailable = !population.isEmpty();
|
||||
Set<String> displayedPeople = population;
|
||||
|
||||
Map<String, String> personTypes = dictionary("UserType");
|
||||
Map<String, String> titleNames = dictionary("JobTitle");
|
||||
@@ -245,7 +237,7 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
Map<String, Bucket> unions = new LinkedHashMap<>();
|
||||
Map<String, Bucket> composition = new LinkedHashMap<>();
|
||||
Map<String, Bucket> ages = buckets("30岁及以下", "31—40岁", "41—50岁", "51岁及以上", "年龄未知");
|
||||
Map<String, Bucket> titles = buckets("正高", "副高", "中级", "初级", "其他职称", "未填职称");
|
||||
Map<String, Bucket> titles = new LinkedHashMap<>();
|
||||
Map<String, Bucket> genders = buckets("男", "女", "未知");
|
||||
LocalDate ageDate = "first".equals(period) ? LocalDate.of(year, 6, 30) : yearEnd;
|
||||
if (ageDate.isAfter(today)) {
|
||||
@@ -254,7 +246,7 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
for (String id : displayedPeople) {
|
||||
NutMap person = people.get(id);
|
||||
boolean joined = selectedPeople.contains(id);
|
||||
boolean inScope = rateAvailable && eligible.contains(id);
|
||||
boolean inScope = population.contains(id);
|
||||
add(units, unitKey(person), unitName(person), joined, inScope);
|
||||
// 与项目 user 视图一致:工会由人员所属单位的 unionid 确定;分子分母使用同一归属。
|
||||
String unionId = text(person.getString("unionid"));
|
||||
@@ -267,7 +259,9 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
String age = ageGroup(person.getString("birthday"), ageDate);
|
||||
add(ages, age, age, joined, inScope);
|
||||
String jobTitle = text(person.getString("job_title"));
|
||||
String title = titleGroup(titleNames.getOrDefault(jobTitle, jobTitle));
|
||||
// 按档案具体职称统计,字典代码转为名称;保留专业括注,不再合并职称等级。
|
||||
String title = jobTitle.isEmpty() ? "未填职称" : text(titleNames.getOrDefault(jobTitle, jobTitle));
|
||||
if (title.isEmpty()) title = jobTitle;
|
||||
add(titles, title, title, joined, inScope);
|
||||
String sex = text(person.getString("sex"));
|
||||
sex = "男".equals(sex) || "女".equals(sex) ? sex : "未知";
|
||||
@@ -280,6 +274,11 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
List<NutMap> unionRows = rows(unions, selectedPeople.size(), rateAvailable);
|
||||
unionRows.sort(Comparator.<NutMap>comparingDouble(v -> v.getDouble("rate", -1)).reversed()
|
||||
.thenComparing(v -> v.getString("name")));
|
||||
// 所有范围内职称均保留(包括零参与);未填职称固定最后,其余按范围人数降序、同人数按名称排序。
|
||||
List<NutMap> titleRows = rows(titles, selectedPeople.size(), rateAvailable);
|
||||
titleRows.sort(Comparator.<NutMap, Boolean>comparing(v -> "未填职称".equals(v.getString("name")))
|
||||
.thenComparing(Comparator.<NutMap>comparingInt(v -> v.getInt("total")).reversed())
|
||||
.thenComparing(v -> v.getString("name")));
|
||||
List<NutMap> monthRows = new ArrayList<>();
|
||||
for (int month = 1; month <= 12; month++) {
|
||||
if (inPeriod(month, period)) {
|
||||
@@ -288,7 +287,7 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
monthRows.add(new NutMap().setv("name", month + "月").setv("value", value));
|
||||
}
|
||||
}
|
||||
Integer total = rateAvailable ? eligible.size() : null;
|
||||
int total = population.size();
|
||||
// 页面已移除半年参与率;保留半年场次和人数,避免以当前周期分母推算另一周期。
|
||||
List<NutMap> semesters = Arrays.asList(
|
||||
semester("上半年", firstActivities, firstPeople.size(), 0, false),
|
||||
@@ -298,26 +297,27 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
"参与口径:有效报名/登记,不等同于实际到场;同一人同一活动的多个项目或课程只计一人次。",
|
||||
"亲子活动按正常报名状态(1、3)的教职工去重,家属不计入教职工人数及参与率。",
|
||||
"参与人数包含全部有效报名人员,不按当前在职状态、账号禁用或档案删除标记排除。",
|
||||
"参与率=同类去重参与人数÷现存活动范围内同类去重人数;人员构成、年龄分层、性别饼图展示参与人员占比。",
|
||||
"参与率=同类有效报名去重人数÷同类统计范围去重人数;人员构成、年龄分层、性别饼图展示参与人员占比。",
|
||||
"按活动开始日期统计已开展活动;同 ID 校级与文化活动合并归属“体育活动”,全年人数跨半年去重。",
|
||||
"工会按人员当前所属工会统计;年龄计算截至 " + ageDate + "。"));
|
||||
if (!selectedUnit.isEmpty()) {
|
||||
notes.add("单位按当前人员所属单位统计;场次仅计入本单位人员有有效报名的活动。");
|
||||
}
|
||||
notes.add("活动范围按现存名单核对,多活动取人员并集;无法确认完整范围时参与率显示“—”,人数照常统计。");
|
||||
notes.addAll(rateReasons);
|
||||
notes.add("分母为所选活动范围名单与有效报名人员的并集;缺少范围名单时用当前 userState=在职 的人员兜底,历史年度同样使用当前在职档案。单位筛选同步限定分子分母。");
|
||||
for (NutMap activity : fallbackActivities) notes.add("默认范围:" + activity.getString("name") + ",范围未配置或名单为空,使用当前在职教职工并补入有效报名人员。");
|
||||
long missingProfiles = selectedPeople.stream().filter(id -> people.get(id).getBoolean("missing_profile", false)).count();
|
||||
if (missingProfiles > 0) notes.add("本期 " + missingProfiles + " 名报名人员无法关联档案,已计入人数,归属和人员属性列为未知。");
|
||||
if (unidentifiedRecords > 0) notes.add("本期存在 " + unidentifiedRecords + " 条缺少人员 ID 的报名查询记录,无法可靠去重,未并入人数。");
|
||||
return new NutMap().setv("summary", new NutMap().setv("activityCount", activities)
|
||||
.setv("visitCount", visits).setv("participantCount", selectedPeople.size())
|
||||
.setv("populationCount", total).setv("rate", rateAvailable ? rate(selectedPeople.size(), total) : null)
|
||||
.setv("rateAvailable", rateAvailable).setv("rateReason", String.join(";", rateReasons))
|
||||
.setv("populationCount", total).setv("rate", rate(selectedPeople.size(), total))
|
||||
.setv("rateAvailable", rateAvailable).setv("rateReason", rateAvailable ? "" : "当前统计范围人数为零")
|
||||
.setv("firstActivityCount", firstActivities).setv("secondActivityCount", secondActivities))
|
||||
.setv("monthly", monthRows).setv("semesters", semesters).setv("units", unitRows).setv("unions", unionRows)
|
||||
.setv("composition", rows(composition, selectedPeople.size(), rateAvailable))
|
||||
.setv("ages", rows(ages, selectedPeople.size(), rateAvailable)).setv("titles", rows(titles, selectedPeople.size(), rateAvailable))
|
||||
.setv("ages", rows(ages, selectedPeople.size(), rateAvailable)).setv("titles", titleRows)
|
||||
.setv("genders", rows(genders, selectedPeople.size(), rateAvailable)).setv("notes", notes)
|
||||
.setv("fallbackActivities", fallbackActivities)
|
||||
.setv("periodName", periodName).setv("scopeLabel", SCOPE_LABEL)
|
||||
.setv("asOf", cutoff.toString()).setv("year", year);
|
||||
}
|
||||
@@ -370,31 +370,6 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
: age <= 50 ? "41—50岁" : "51岁及以上";
|
||||
}
|
||||
|
||||
/** 只删除专业系列括注,不使用 contains("教授") 等模糊匹配,防止副教授错归正高。 */
|
||||
private String titleGroup(String name) {
|
||||
String title = text(name).replaceAll("[((].*?[))]", "").trim();
|
||||
if (title.isEmpty()) {
|
||||
return "未填职称";
|
||||
}
|
||||
if (Arrays.asList("正高", "正高级", "教授", "研究员", "研究馆员", "正高级工程师", "教授级高级工程师",
|
||||
"主任医师", "主任护师", "主任药师", "主任技师", "正高级实验师", "正高级会计师").contains(title)) {
|
||||
return "正高";
|
||||
}
|
||||
if (Arrays.asList("副高", "副高级", "副教授", "副研究员", "副研究馆员", "高级工程师", "高级实验师",
|
||||
"高级会计师", "高级经济师", "副主任医师", "副主任护师", "副主任药师", "副主任技师").contains(title)) {
|
||||
return "副高";
|
||||
}
|
||||
if (Arrays.asList("中级", "讲师", "助理研究员", "工程师", "实验师", "馆员", "会计师", "经济师",
|
||||
"主治医师", "主管护师", "主管药师", "主管技师", "审计师").contains(title)) {
|
||||
return "中级";
|
||||
}
|
||||
if (Arrays.asList("初级", "助教", "研究实习员", "助理工程师", "助理实验师", "助理馆员", "助理会计师",
|
||||
"助理经济师", "技术员", "医师", "护师", "药师", "技师").contains(title)) {
|
||||
return "初级";
|
||||
}
|
||||
return "其他职称";
|
||||
}
|
||||
|
||||
/** 兼容库内日期、年月及活动日期时间;非法日期不补成当前日期。 */
|
||||
private LocalDate parseDate(String value) {
|
||||
String date = text(value).replace('/', '-');
|
||||
@@ -429,6 +404,7 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 分组累计范围总人数与有效报名人数;报名人员已补入范围,参与率不会超出 100%。 */
|
||||
private void add(Map<String, Bucket> groups, String key, String name, boolean joined, boolean inScope) {
|
||||
Bucket bucket = groups.computeIfAbsent(key, item -> new Bucket(name));
|
||||
if (inScope) bucket.total++;
|
||||
@@ -439,8 +415,8 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
|
||||
private List<NutMap> rows(Map<String, Bucket> groups, int participantTotal, boolean rateAvailable) {
|
||||
List<NutMap> result = new ArrayList<>();
|
||||
groups.forEach((id, bucket) -> result.add(row(id, bucket.name).setv("total", rateAvailable ? bucket.total : null)
|
||||
.setv("participants", bucket.participants).setv("rate", rateAvailable ? rate(bucket.participants, bucket.total) : null)
|
||||
groups.forEach((id, bucket) -> result.add(row(id, bucket.name).setv("total", bucket.total)
|
||||
.setv("participants", bucket.participants).setv("rate", rate(bucket.participants, bucket.total))
|
||||
.setv("share", rate(bucket.participants, participantTotal))));
|
||||
return result;
|
||||
}
|
||||
@@ -463,7 +439,7 @@ public class CulturalSportsBoardServiceImpl implements CulturalSportsBoardServic
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 分子累计全部有效报名,分母累计已核对范围;范围不完整时只返回人数和占比。 */
|
||||
/** 参与人数用于构成占比,并与合并范围总人数计算参与率。 */
|
||||
private static class Bucket {
|
||||
private final String name;
|
||||
private int total;
|
||||
|
||||
@@ -159,7 +159,7 @@ layout("/layouts/platform.html"){
|
||||
</div>
|
||||
</div>
|
||||
<div class="board-card kpi-card rate-kpi">
|
||||
<div><div class="kpi-label">整体参与率</div><div class="rate-caption">报名范围人数<br>{{ number(summary.populationCount) }} 人</div></div>
|
||||
<div><div class="kpi-label">整体参与率</div><div class="rate-caption">统计范围人数<br>{{ number(summary.populationCount) }} 人</div></div>
|
||||
<el-progress type="circle" :percentage="summary.rate || 0" :width="90" :stroke-width="9"
|
||||
color="#2674f5" :format="formatOverallRate"></el-progress>
|
||||
</div>
|
||||
@@ -173,11 +173,18 @@ layout("/layouts/platform.html"){
|
||||
|
||||
<div class="board-main">
|
||||
<div class="board-left">
|
||||
<!-- 工会参与率暂时停用,保留原图表以便恢复。
|
||||
<section class="board-card">
|
||||
<div class="chart-heading"><h2>工会参与率</h2><span class="chart-subtitle">参与人数 / 报名范围人数</span></div>
|
||||
<div class="chart-heading"><h2>工会参与率</h2><span class="chart-subtitle">参与人数 / 统计范围人数</span></div>
|
||||
<v-chart class="board-chart unit-chart" :options="charts.unions" autoresize aria-label="各工会参与率柱状图"></v-chart>
|
||||
<div class="board-footnote">工会较多时可拖动底部滑块查看;虚线为当前统计范围整体参与率。</div>
|
||||
</section>
|
||||
-->
|
||||
<section class="board-card">
|
||||
<div class="chart-heading"><h2>职称参与率</h2><span class="chart-subtitle">同职称参与人数 / 同职称统计范围人数</span></div>
|
||||
<v-chart class="board-chart unit-chart" :options="charts.titles" autoresize aria-label="各职称参与率柱状图"></v-chart>
|
||||
<div class="board-footnote">具体职称按统计范围人数从多到少排列,未填职称固定最后;可拖动底部滑块查看全部职称。虚线为整体参与率。</div>
|
||||
</section>
|
||||
<div class="board-demographics">
|
||||
<section class="board-card">
|
||||
<div class="chart-heading"><h2>人员构成</h2><span class="chart-subtitle">参与人员占比</span></div>
|
||||
@@ -200,7 +207,7 @@ layout("/layouts/platform.html"){
|
||||
<el-table-column prop="name" label="单位" min-width="115" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="participants" label="参与人数" width="78" align="center"></el-table-column>
|
||||
<el-table-column label="参与率" width="77" align="center">
|
||||
<template slot-scope="scope"><el-tooltip :content="scope.row.total == null ? '报名范围无法确认,参与人数:' + scope.row.participants : scope.row.participants + ' / ' + scope.row.total + ' 人'" placement="top"><span>{{ percent(scope.row.rate) }}</span></el-tooltip></template>
|
||||
<template slot-scope="scope"><el-tooltip :content="'参与人数 ' + scope.row.participants + ' / 统计范围人数 ' + scope.row.total + ' 人'" placement="top"><span>{{ percent(scope.row.rate) }}</span></el-tooltip></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
@@ -244,7 +251,7 @@ layout("/layouts/platform.html"){
|
||||
board: null,
|
||||
unitPage: {number: 1, size: 8},
|
||||
charts: {
|
||||
monthly: emptyChart('加载后展示月度趋势'), unions: emptyChart(), composition: emptyChart(),
|
||||
monthly: emptyChart('加载后展示月度趋势'), titles: emptyChart(), composition: emptyChart(),
|
||||
ages: emptyChart(), genders: emptyChart()
|
||||
}
|
||||
};
|
||||
@@ -270,7 +277,7 @@ layout("/layouts/platform.html"){
|
||||
const summary = this.summary;
|
||||
if (!summary.activityCount) return '当前筛选范围暂无已开展活动,可切换年度、来源或单位查看。';
|
||||
let result = this.board.year + '年' + this.periodName + '开展 ' + summary.activityCount + ' 场活动,覆盖 ' + summary.participantCount + ' 人。';
|
||||
result += summary.rate == null ? ' 参与率暂不计算:' + (summary.rateReason || '报名范围人数为零') + '。' : ' 整体参与率 ' + this.percent(summary.rate) + '。';
|
||||
result += summary.rate == null ? ' 参与率暂不计算:' + (summary.rateReason || '统计范围人数为零') + '。' : ' 整体参与率 ' + this.percent(summary.rate) + '。';
|
||||
return result;
|
||||
}
|
||||
},
|
||||
@@ -364,7 +371,7 @@ layout("/layouts/platform.html"){
|
||||
tooltip: {trigger: 'axis', confine: true, renderMode: 'richText', formatter: (items) => {
|
||||
const row = rows[items[0].dataIndex];
|
||||
return row.name + '\n' + (field === 'share' ? '参与人员占比:' : '参与率:') + this.percent(row[field])
|
||||
+ '\n参与人数:' + row.participants + (field === 'rate' ? '\n人员总数:' + row.total : '');
|
||||
+ '\n参与人数:' + row.participants + (field === 'rate' ? '\n统计范围人数:' + row.total : '');
|
||||
}},
|
||||
xAxis: horizontal ? value : category, yAxis: horizontal ? category : value,
|
||||
series: [{type: 'bar', barMaxWidth: horizontal ? 17 : 34, data: rows.map((row, index) => ({value: row[field], itemStyle: {color: horizontal ? palette[index % palette.length] : palette[0]}})),
|
||||
@@ -382,7 +389,7 @@ layout("/layouts/platform.html"){
|
||||
tooltip: {trigger: 'item', confine: true, renderMode: 'richText', formatter: (item) => {
|
||||
const row = participants[item.dataIndex];
|
||||
return row.name + '\n参与人数:' + row.participants + '\n参与人员占比:' + this.percent(row.share)
|
||||
+ '\n该性别参与率:' + this.percent(row.rate) + (row.total == null ? '(报名范围无法确认)' : '');
|
||||
+ '\n统计范围人数:' + row.total + '\n该性别参与率:' + this.percent(row.rate);
|
||||
}},
|
||||
legend: {bottom: 0, itemWidth: 8, itemHeight: 8, textStyle: {color: '#63758e', fontSize: 11}},
|
||||
series: [{type: 'pie', radius: '70%', center: ['50%', '42%'],
|
||||
@@ -396,8 +403,9 @@ layout("/layouts/platform.html"){
|
||||
/** 每次聚合结果整体替换图表配置;v-chart 自行处理窗口缩放和组件销毁。 */
|
||||
buildCharts() {
|
||||
const board = this.board;
|
||||
/* 工会图表配置暂时停用,保留以便恢复。
|
||||
const unionChart = this.barChart(board.unions, 'rate', false);
|
||||
if (!board.summary.rateAvailable) unionChart.title.text = '报名范围无法确认,参与率暂不计算';
|
||||
if (!board.summary.rateAvailable) unionChart.title.text = '当前统计范围人数为零,参与率暂不计算';
|
||||
if (unionChart.grid) {
|
||||
unionChart.grid.bottom = 66;
|
||||
unionChart.xAxis.axisLabel.formatter = (label) => label.length > 7 ? label.slice(0, 7) + '…' : label;
|
||||
@@ -408,6 +416,21 @@ layout("/layouts/platform.html"){
|
||||
label: {position: 'insideEndTop', formatter: '整体 ' + this.percent(board.summary.rate), color: '#638bc9', fontSize: 10}, data: [{yAxis: board.summary.rate}]};
|
||||
}
|
||||
}
|
||||
*/
|
||||
// 复用后端职称分组及当前统计范围分母,缺测值保留为空,不转换成零参与率。
|
||||
const titleChart = this.barChart(board.titles, 'rate', false);
|
||||
// 职称不截取排名;通过滑块浏览全部分类,悬浮提示显示完整名称和分子分母。
|
||||
if (titleChart.grid && board.titles.length > 8) {
|
||||
titleChart.grid.bottom = 66;
|
||||
titleChart.xAxis.axisLabel.formatter = (label) => label.length > 7 ? label.slice(0, 7) + '…' : label;
|
||||
titleChart.dataZoom = [{type: 'slider', height: 13, bottom: 9, startValue: 0, endValue: 7,
|
||||
borderColor: '#e3ebf8', fillerColor: '#dceaff', handleStyle: {color: '#7faff9'}, showDetail: false}];
|
||||
}
|
||||
if (titleChart.grid && board.summary.rate != null) {
|
||||
titleChart.series[0].markLine = {silent: true, symbol: 'none', lineStyle: {color: '#82a9f5', type: 'dashed'},
|
||||
label: {position: 'insideEndTop', formatter: '整体 ' + this.percent(board.summary.rate), color: '#638bc9', fontSize: 10},
|
||||
data: [{yAxis: board.summary.rate}]};
|
||||
}
|
||||
const composition = board.composition.filter((row) => row.participants > 0);
|
||||
const pie = composition.length ? {
|
||||
color: palette,
|
||||
@@ -428,7 +451,8 @@ layout("/layouts/platform.html"){
|
||||
lineStyle: {width: 2, color: '#2674f5'}, itemStyle: {color: '#2674f5'}, areaStyle: {color: '#e2edff'}, connectNulls: false}]
|
||||
};
|
||||
this.charts = {
|
||||
monthly: months, unions: unionChart, composition: pie,
|
||||
// 恢复工会图表时使用 unions: unionChart,并恢复上方模板和配置。
|
||||
monthly: months, titles: titleChart, composition: pie,
|
||||
ages: this.barChart(board.ages.filter((row) => row.participants > 0), 'share', true),
|
||||
genders: this.genderPie(board.genders)
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user