福利手机端可以代选,可以在手机上

品牌活动也可以在手机上查看报名人数
This commit is contained in:
2026-10-15 09:26:55 +08:00
parent 486934f3b6
commit f3021514fd
20 changed files with 1545 additions and 42 deletions
@@ -97,8 +97,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
Map<String, Date> doctoralData = pullPostDoctoral();
Date nowDate = new Date();
String appId = "1926887238437818370";
String secret = "32df31fcf4fc43f9a647ee1f47134f36";
String appId = "XXXXXXXXXXXXXXXXXXXXX";
String secret = "XXXXXXXXXXXXXXXXXXXX";
List<JSONObject> rawDataList = new ArrayList<>();
int skip = 0;
@@ -280,8 +280,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
@Override
public Map<String, Date> pullPostDoctoral() {
String appId = "1926887238437818370";
String secret = "32df31fcf4fc43f9a647ee1f47134f36";
String appId = "XXXXXXXXXXXXXXXXXXXXXX";
String secret = "XXXXXXXXXXXXXXXXXXXXXX";
List<JSONObject> allUsers = new ArrayList<>();
int skip = 0;
@@ -112,11 +112,7 @@ public class ActivityCultureUserStatisticsController {
if (Strings.isNotBlank(page.getSearchName()) && Strings.isNotBlank(page.getSearchKeyword())) {
cnd.where().andLike(page.getSearchName(), page.getSearchKeyword());
}
if (StrUtil.isNotBlank(page.getPageOrderName()) && StrUtil.isNotBlank(page.getPageOrderBy())) {
cnd.orderBy(page.getPageOrderName(), page.getPageOrderBy().equalsIgnoreCase("ascending") ? "ASC" : "DESC");
} else {
cnd.desc("atp.applyDateTime");
}
activityCultureApplyUserService.applyUserStatisticsSort(cnd, page);
sql.setCondition(cnd);
Pagination pagination = activityCultureApplyUserService.listPageMap(page.getPageNumber(), page.getPageSize(), sql);
List<NutMap> list = pagination.getList();
@@ -6,6 +6,7 @@ import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.activity.culture.models.ActivityTissuePerson;
import org.nutz.dao.Cnd;
import org.nutz.lang.util.NutMap;
import java.util.List;
@@ -19,6 +20,14 @@ public interface ActivityCultureApplyUserService extends BaseService<ActivityTis
boolean isEnrolled,
Integer state);
/**
* 设置学校、工会和社团活动人员统计的分页顺序。
*
* @param cnd 查询条件;方法会在其上追加 ORDER BY
* @param page 分页参数;pageOrderName 和 pageOrderBy 均有值时采用用户选择的排序,否则按工号正序
*/
void applyUserStatisticsSort(Cnd cnd, PageForm page);
/**
* 活动报名
@@ -110,6 +110,17 @@ public class ActivityCultureApplyUserServiceImpl extends BaseServiceImpl<Activit
return pagination;
}
@Override
public void applyUserStatisticsSort(Cnd cnd, PageForm page) {
if (StrUtil.isNotBlank(page.getPageOrderName()) && StrUtil.isNotBlank(page.getPageOrderBy())) {
cnd.orderBy(page.getPageOrderName(), page.getPageOrderBy().equalsIgnoreCase("ascending") ? "ASC" : "DESC");
} else {
// 三类活动统计默认按工号正序;相同工号再按报名时间倒序保证分页顺序稳定。
cnd.asc("atp.loginName");
cnd.desc("atp.applyDateTime");
}
}
@Override
public void signUp(String activityId, List<String> personIds, JSONObject dynamicFormParam) {
ActivityTissue tissue = dao().fetch(ActivityTissue.class, activityId);
@@ -12,6 +12,7 @@ import org.nutz.dao.sql.Criteria;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import java.util.List;
@@ -51,9 +52,12 @@ public class TrainSignUpUserServiceImpl extends BaseServiceImpl<TrainSignUpBlack
LEFT JOIN train_sign_up_course tsuc ON tsuc.id = tsuu.courseId
LEFT JOIN train_sign_up_black_list tsubl on tsubl.userId = tsuu.userId
$condition
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 )
""");
cnd.groupBy("tsuu.userId");
// 默认按工号正序,工号相同时用用户 ID 保持分页稳定;手动列排序沿用传入条件。
if (Strings.isBlank(pageForm.getPageOrderName()) || Strings.isBlank(pageForm.getPageOrderBy())) {
cnd.asc("u.loginname").asc("tsuu.userId");
}
Criteria varCnd = Cnd.cri();
varCnd.where().setTop(false);
varCnd.where().andEX("activityId", "=", activityId);
@@ -0,0 +1,20 @@
package com.budwk.app.zhgh.welfare.h5Controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
/**
* 移动端福利选择情况页面入口;查询和代选沿用现有福利接口。
*/
@IocBean
@At("/platform/h5/welfare/selection/situation")
public class H5WelfareSelectionSituationController {
@At("")
@Ok("beetl:/platform/zhghh5/welfare/selectionSituation/index.html")
@SaCheckPermission("h5.welfare.selection.situation")
public void index() {
}
}
@@ -0,0 +1,81 @@
package com.budwk.app.zhgh.welfare.h5Controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.welfare.service.WelfareProjectService;
import com.budwk.app.zhgh.welfare.service.WelfareStatisticsService;
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 javax.validation.Valid;
/** 移动端福利统计只读入口;统计和人员查询业务统一由现有 service 承担。 */
@IocBean
@At("/platform/h5/welfare/statistics")
@Ok("json:full")
public class H5WelfareStatisticsController {
@Inject
private WelfareStatisticsService welfareStatisticsService;
@Inject
private WelfareProjectService welfareProjectService;
@At("")
@Ok("beetl:/platform/zhghh5/welfare/statistics/index.html")
@SaCheckPermission("h5.welfare.statistics")
public void index() {
}
/**
* @param year 可选年份,省略时返回全部年份的项目,供移动端联动筛选
* @return Result.data 为项目数组,使用 id、name、year 生成筛选项
*/
@At
@SaCheckPermission("h5.welfare.statistics")
public Result projects(Integer year) {
return Result.success(welfareProjectService.getWelfareList(year));
}
/**
* @param projectId 必填福利项目 ID
* @param unionId 可选工会 ID,空表示全部工会
* @param isSelectByAdmin null 全部方式,true 管理员代选,false 个人选择
* @return Result.data 含 tableList 工会人数统计、tableColumn 固定列和动态福利选项列
*/
@At
@SaCheckPermission("h5.welfare.statistics")
public Result pageData(String projectId, String unionId, Boolean isSelectByAdmin) {
if (StrUtil.isBlank(projectId)) return Result.error("请选择福利项目");
return Result.success(welfareStatisticsService.mobilePageData(projectId, unionId, isSelectByAdmin));
}
/**
* @param pageForm pageNumber 从 1 开始,pageSize 每页条数,searchKeyword 为工号或姓名
* @param projectId 必填福利项目 ID
* @param unionId 必填工会 ID
* @param isSelectByAdmin null 全部方式,true 管理员代选,false 个人选择
* @return Result.data 为 Pagination,list 含人员、单位、所选福利及选择方式,totalCount 为匹配人数
*/
@At
@SaCheckPermission("h5.welfare.statistics")
public Result selectedUnionUserPageData(@Valid PageForm pageForm, String projectId, String unionId, Boolean isSelectByAdmin) {
if (StrUtil.isBlank(projectId) || StrUtil.isBlank(unionId)) return Result.error("请选择福利项目和工会");
return Result.success(welfareStatisticsService.selectedUnionUserPageData(pageForm, projectId, unionId, isSelectByAdmin));
}
/**
* @param pageForm pageNumber 从 1 开始,pageSize 每页条数,searchKeyword 为工号或姓名
* @param projectId 必填福利项目 ID
* @param unionId 必填工会 ID;未选人员不受个人选择/管理员代选条件影响
* @return Result.data 为 Pagination,list 含未选人员的工号、姓名、单位,totalCount 为匹配人数
*/
@At
@SaCheckPermission("h5.welfare.statistics")
public Result unSelectedUnionUserPageData(@Valid PageForm pageForm, String projectId, String unionId) {
if (StrUtil.isBlank(projectId) || StrUtil.isBlank(unionId)) return Result.error("请选择福利项目和工会");
return Result.success(welfareStatisticsService.unSelectedUnionUserPageData(pageForm, projectId, unionId));
}
}
@@ -18,6 +18,16 @@ public interface WelfareStatisticsService extends BaseService<WelfareProject> {
*/
NutMap pageData(String projectId, String unionId, Boolean isSelectByAdmin);
/**
* 移动端工会统计;方式筛选只影响已选人数和各福利选项人数。
* @param projectId 必填福利项目 ID
* @param unionId 工会 ID,空值表示全部工会
* @param isSelectByAdmin null 表示全部方式,true 表示管理员代选,false 表示个人选择
* @return tableList 为工会统计行(teacherSum 总人数、selectedNum 已选、unSelectedNum 真正未选);
* tableColumn 为列描述(label 名称、prop 字段),其中包含动态福利选项人数列
*/
NutMap mobilePageData(String projectId, String unionId, Boolean isSelectByAdmin);
/**
* 查询某分工会已选择人员
* @param pageForm
@@ -415,6 +415,8 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
cnd.andEX("t1.preparedBy", "in", pageForm.getPreparedBys());
cnd.andEX("t1.userState", "in", pageForm.getUserStates());
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
// 默认先按工号正序分页,工号相同时沿用工会和单位顺序。
cnd.asc("t2.loginname");
cnd.asc("t1.welfareUnionName");
cnd.asc("t1.welfareUnitName");
} else {
@@ -644,32 +646,52 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
LEFT JOIN sys_user t2 ON t2.id = t1.userId
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("t1.projectId", "=", pageForm.getProjectId());
cnd.and("t2.idCard", "in", Sqls.create("select idCard from sys_user where idCard is not null and idCard != '' group by idCard having count(1) > 1"));
// 只统计当前福利项目名单中的身份证号;同号名单记录至少两条才属于重复组。
Sql repeatIdCardSql = Sqls.create("""
select u.idCard from welfare_list w
inner join sys_user u on u.id = w.userId
where w.projectId = @projectId and u.idCard is not null and u.idCard != ''
group by u.idCard having count(1) > 1
""").setParam("projectId", pageForm.getProjectId());
// 页面筛选只决定导出哪些重复组,匹配到的身份证号用于补齐同组全部记录。
Sql matchedIdCardSql = Sqls.create("""
select distinct t2.idCard from welfare_list t1
inner join sys_user t2 on t2.id = t1.userId
$condition
""");
Cnd matchedCnd = Cnd.NEW();
matchedCnd.and("t1.projectId", "=", pageForm.getProjectId());
matchedCnd.and("t2.idCard", "in", repeatIdCardSql);
if (StrUtil.isNotBlank(pageForm.getUserName())) {
cnd.where().andLike("t2.username", pageForm.getUserName());
matchedCnd.where().andLike("t2.username", pageForm.getUserName());
}
if (StrUtil.isNotBlank(pageForm.getLoginName())) {
cnd.where().andLike("t2.loginname", pageForm.getLoginName());
matchedCnd.where().andLike("t2.loginname", pageForm.getLoginName());
}
cnd.andEX("t2.sex", "=", pageForm.getSex());
cnd.andEX("t2.birthday", "=", pageForm.getBirthday());
matchedCnd.andEX("t2.sex", "=", pageForm.getSex());
matchedCnd.andEX("t2.birthday", "=", pageForm.getBirthday());
if (pageForm.getBirthMonths() != null && pageForm.getBirthMonths().length > 0) {
cnd.andEX("MONTH(t2.birthday)", "in", pageForm.getBirthMonths());
matchedCnd.andEX("MONTH(t2.birthday)", "in", pageForm.getBirthMonths());
}
matchedCnd.andEX("t1.welfareUnitId", "in", pageForm.getUnitIds());
matchedCnd.andEX("t1.welfareUnionId", "=", pageForm.getUnionId());
matchedCnd.andEX("t1.personType", "in", pageForm.getPersonTypes());
matchedCnd.andEX("t1.preparedBy", "in", pageForm.getPreparedBys());
matchedCnd.andEX("t1.userState", "in", pageForm.getUserStates());
matchedIdCardSql.setCondition(matchedCnd);
List<NutMap> matchedIdCards = listMap(matchedIdCardSql);
List<NutMap> list = new ArrayList<>();
if (!matchedIdCards.isEmpty()) {
Cnd cnd = Cnd.NEW();
cnd.and("t1.projectId", "=", pageForm.getProjectId());
cnd.and("t2.idCard", "in", matchedIdCards.stream().map(item -> item.getString("idCard")).toArray(String[]::new));
cnd.asc("t2.idCard");
cnd.asc("t2.loginname");
cnd.asc("t1.id");
sql.setCondition(cnd);
list = listMap(sql);
}
cnd.andEX("t1.welfareUnitId", "in", pageForm.getUnitIds());
cnd.andEX("t1.welfareUnionId", "=", pageForm.getUnionId());
cnd.andEX("t1.personType", "in", pageForm.getPersonTypes());
cnd.andEX("t1.preparedBy", "in", pageForm.getPreparedBys());
cnd.andEX("t1.userState", "in", pageForm.getUserStates());
cnd.asc("t2.idCard");
cnd.asc("t1.welfareUnionName");
cnd.asc("t1.welfareUnitName");
sql.setCondition(cnd);
List<NutMap> list = listMap(sql);
try {
List<ExcelExportEntity> exportEntities = new ArrayList<>();
@@ -102,6 +102,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
// 默认先按工号正序分页,工号相同时沿用工会和单位顺序。
cnd.asc("t4.loginname");
cnd.asc("welfareUnionName");
cnd.asc("welfareUnitName");
} else {
@@ -138,6 +138,26 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
return NutMap.NEW().addv("tableList", mapUnions).addv("tableColumn", dynamicTableColumns);
}
/**
* 沿用现有统计查询;筛选选择方式时,以不限方式的未选人数匹配未选人员明细口径。
* 例如已经个人选择的人,在筛选管理员代选时不会被计为未选择。
*/
@Override
public NutMap mobilePageData(String projectId, String unionId, Boolean isSelectByAdmin) {
NutMap result = pageData(projectId, unionId, isSelectByAdmin);
if (isSelectByAdmin != null) {
NutMap allMethods = pageData(projectId, unionId, null);
Map<String, Long> unSelectedByUnion = new HashMap<>();
for (NutMap row : allMethods.getList("tableList", NutMap.class)) {
unSelectedByUnion.put(row.getString("id"), row.getLong("unSelectedNum"));
}
for (NutMap row : result.getList("tableList", NutMap.class)) {
row.put("unSelectedNum", unSelectedByUnion.getOrDefault(row.getString("id"), 0L));
}
}
return result;
}
@Override
public Pagination selectedUnionUserPageData(PageForm pageForm, String projectId, String unionId, Boolean isSelectByAdmin) {
Sql sql = Sqls.create("""
@@ -14,7 +14,8 @@ const h5PopupHistory = (() => {
popup.closeFromHistory()
}
window.addEventListener("popstate", handlePopState)
// 捕获阶段优先消费弹框返回事件,避免 PJAX 重载页面并重置筛选条件。
window.addEventListener("popstate", handlePopState, true)
return {
/**
@@ -37,11 +38,13 @@ const h5PopupHistory = (() => {
/**
* 通过取消、关闭按钮关闭指定弹框时,同步回退对应历史记录。
* @param id 弹框唯一标识,必须是当前最上层弹框
* @return boolean;true 表示已发起历史回退,false 表示弹框不在栈顶
* @return boolean;true 表示已发起历史回退,false 表示弹框不在栈顶或正在关闭
*/
close(id) {
const top = popupStack[popupStack.length - 1]
if (!top || top.id !== id) return false
if (!top || top.id !== id || top.closing) return false
// history.back 异步触发 popstate;等待期间重复点击或关闭回调不能再回退一层。
top.closing = true
window.history.back()
return true
},
@@ -65,8 +65,8 @@
<!-- <link rel="stylesheet" href="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/style.min.css" />-->
<!-- <link rel="stylesheet" href="https://vxeui.com/umd/vxe-table@3.9.0/lib/style.min.css" />-->
<script src="${base!}/assets/platform/plugins/vxeui/xe-utils.js"></script>
<script src="${base!}/assets/platform/plugins/vxeui/vxe-table.js"></script>
<script src="${base!}/assets/platform/plugins/vxeui/vxe-pc-ui.js"></script>
<script src="${base!}/assets/platform/plugins/vxeui/vxe-table.js"></script>
<!-- 引入 form-create 和 designer -->
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
@@ -27,6 +27,8 @@
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
<script src="${base!}/assets/platform/plugins/jquery/jquery.js"></script>
<!-- 弹框历史必须先于 PJAX 注册,关闭弹框时优先消费返回事件,避免重新加载业务页面。 -->
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js?v=20260930-2"></script>
<!-- pjax是异步加载html片段的工具,模拟前端路由机制 -->
<script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script>
@@ -57,7 +59,6 @@
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
<script src="${base!}/components/plugins/sysDict/DictData.js"></script>
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js"></script>
<script src="${base!}/assets/platform/js/tool/businessTool.js"></script>
<!--富文本编辑器-->
@@ -1,6 +1,6 @@
const ACTIVITY_CULTURE_USER_STATISTICS = {
template: /*language=HTML*/ `
<guava ref="guava">
<guava ref="guava" @hook:mounted="onPageVisible">
<template>
<el-card shadow="never">
@@ -65,7 +65,8 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
</el-button>
</table-tool>
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder" row-key="id"
<el-table ref="tableRef" class="activity-culture-statistics-table" :data="tableData"
:max-height="tableHeight" :size="tableSize" @sort-change="pageOrder" row-key="id"
style="width: 100%;" v-loading="tableLoading">
<el-table-column :index="indexMethod" align="center" header-align="center"
label="序号" type="index"
@@ -110,8 +111,10 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
pageForm: {
unionId: "",
searchName: "atp.username",
pageSize: 50,
year: new Date().getFullYear() + ""
},
tableHeight: 300,
tableColumns: [
{ prop: "userName", label: "姓名", width: 180, fixed: "left" },
{ prop: "loginName", label: "工号", width: 180, fixed: "left" },
@@ -133,6 +136,60 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
methods: {
getNestedValue: getNestedValue,
// 页面在 PJAX 显示后再测量,避免首次渲染时隐藏容器给出错误高度。
onPageVisible() {
this.$nextTick(() => requestAnimationFrame(() => this.updateTableHeight()))
},
// 按当前视口和分页实际位置计算表格最大高度,保留分页及卡片底部空间。
updateTableHeight() {
if (!this.$refs.tableRef) {
return
}
const table = this.$refs.tableRef.$el
const card = table.closest(".el-card")
const content = table.closest(".ele-admin-content")
const pagination = card && card.querySelector(".el-pagination-container")
if (!content || !pagination || !table.isConnected || !table.getBoundingClientRect().width || !pagination.getBoundingClientRect().height) {
return
}
content.scrollTop = 0
const pageBody = table.closest(".ele-body")
const pagePadding = pageBody ? parseFloat(getComputedStyle(pageBody).paddingBottom) || 0 : 0
// 页签异步出现时内容区可能被下推,以浏览器可视底部为最终边界。
const visibleBottom = Math.min(content.getBoundingClientRect().bottom, window.innerHeight) - pagePadding - 4
const nextHeight = Math.max(120, Math.floor(table.getBoundingClientRect().height + visibleBottom - card.getBoundingClientRect().bottom))
if (nextHeight !== this.tableHeight) {
this.tableHeight = nextHeight
}
// 筛选区和卡片高度变化时重新计算,避免异步内容或窗口变化遮挡分页。
if (!this.tableResizeObserver && window.ResizeObserver) {
this.tableResizeObserver = new ResizeObserver(() => this.updateTableHeight())
const resizeTargets = [content, document.querySelector("#ele-admin-tabs"), this.$el.querySelector(".el-card"), card, card.querySelector(".ele-table-tool"), pagination]
resizeTargets.forEach((element) => {
if (element) {
this.tableResizeObserver.observe(element)
}
})
}
},
// 组件离开页面时清理监听,防止旧表格继续响应尺寸变化。
stopTableResize() {
window.removeEventListener("resize", this.pageVisibleHandler)
window.removeEventListener("load", this.pageVisibleHandler)
$(document).off("pjax:send", this.pageExitHandler)
$(document).off("pjax:complete", this.pageVisibleHandler)
if (this.tabsObserver) {
this.tabsObserver.disconnect()
this.tabsObserver = null
}
if (this.tableResizeObserver) {
this.tableResizeObserver.disconnect()
this.tableResizeObserver = null
}
},
openMore(dynamicFormData) {
const formConfig = this.activityList.find((v) => v.id === this.pageForm.activityId)?.formConfig
this.$refs.dynamicTableFormEvalRef.onOpen(formConfig, dynamicFormData)
@@ -197,8 +254,42 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
}
}
},
mounted() {
// 原生和 jQuery 事件会改变 this,使用稳定的箭头回调以便正确执行和解绑。
this.pageVisibleHandler = () => this.onPageVisible()
this.pageExitHandler = () => this.stopTableResize()
window.addEventListener("resize", this.pageVisibleHandler)
window.addEventListener("load", this.pageVisibleHandler)
$(document).one("pjax:send", this.pageExitHandler)
$(document).on("pjax:complete", this.pageVisibleHandler)
// 顶部页签独立异步挂载,只移动内容区时 ResizeObserver 不会收到尺寸变化。
const tabs = document.querySelector("#ele-admin-tabs")
if (tabs && window.MutationObserver) {
this.tabsObserver = new MutationObserver(() => this.onPageVisible())
this.tabsObserver.observe(tabs, { childList: true, subtree: true })
}
this.onPageVisible()
},
updated() {
// 每次异步渲染后重新测量,首次取得尺寸不代表页面布局已稳定。
if (this.$refs.tableRef) {
this.onPageVisible()
}
},
beforeDestroy() {
this.stopTableResize()
},
async created() {
await this.getActivityByYearOrType()
this.unionList = await this.$businessTool.listUnion()
}
},
style: /*language=CSS*/ `
.ele-admin-fixed-body .ele-admin-content {
overflow-y: hidden;
}
.activity-culture-statistics-table .el-table__body-wrapper {
overscroll-behavior-y: contain;
}
`
}
@@ -3,7 +3,7 @@ layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<guava ref="guava" @hook:mounted="onPageVisible">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度:">
@@ -36,7 +36,8 @@ layout("/layouts/platform.html"){
<el-button @click="exportSignPerson" type="primary" size="small">导出签到名单</el-button>
<el-button @click="exportGiftPerson" type="primary" size="small">导出领取名单</el-button>
</table-tool>
<el-table :data="tableData" :size="tableSize">
<el-table ref="tableRef" class="train-signup-user-table" :data="tableData"
:max-height="tableHeight" :size="tableSize">
<el-table-column :index="indexMethod" label="序号" type="index" width="80px"></el-table-column>
<el-table-column
:label="column.prop === 'courseNames' ? trainType : column.label"
@@ -98,15 +99,42 @@ layout("/layouts/platform.html"){
new Vue({
el: "#app",
mixins: [initTableMixins],
mounted() {
// 原生和 jQuery 事件使用稳定的箭头回调,确保尺寸重算及销毁时正确解绑。
this.pageVisibleHandler = () => this.onPageVisible()
this.pageExitHandler = () => this.stopTableResize()
window.addEventListener("resize", this.pageVisibleHandler)
window.addEventListener("load", this.pageVisibleHandler)
$(document).one("pjax:send", this.pageExitHandler)
$(document).on("pjax:complete", this.pageVisibleHandler)
// 顶部页签异步挂载可能只移动内容区,不改变已监听元素的尺寸。
const tabs = document.querySelector("#ele-admin-tabs")
if (tabs && window.MutationObserver) {
this.tabsObserver = new MutationObserver(() => this.onPageVisible())
this.tabsObserver.observe(tabs, { childList: true, subtree: true })
}
this.onPageVisible()
},
updated() {
// 表格及查询数据异步渲染后继续校正,首次取得的高度不一定是最终布局。
if (this.$refs.tableRef) {
this.onPageVisible()
}
},
beforeDestroy() {
this.stopTableResize()
},
data() {
return {
reserveDialogVisible: false,
reserveTableData: [],
activityList: [],
pageForm: {
pageSize: 50,
year: new Date().getFullYear().toString(),
activityId: null
},
tableHeight: 300,
tableColumns: [
{ label: "姓名", prop: "username" },
{ label: "工号", prop: "loginname" },
@@ -139,6 +167,55 @@ layout("/layouts/platform.html"){
}
},
methods: {
// 页面显示后再读取表格位置,避免刷新或 PJAX 隐藏期间计算出错误高度。
onPageVisible() {
this.$nextTick(() => requestAnimationFrame(() => this.updateTableHeight()))
},
// 按浏览器可视底部与卡片实际留白计算表格最大高度,让分页始终留在表格外。
updateTableHeight() {
if (!this.$refs.tableRef) {
return
}
const table = this.$refs.tableRef.$el
const card = table.closest(".el-card")
const content = table.closest(".ele-admin-content")
const pagination = card && card.querySelector(".el-pagination-container")
if (!content || !pagination || !table.isConnected || !table.getBoundingClientRect().width || !pagination.getBoundingClientRect().height) {
return
}
content.scrollTop = 0
const pageBody = table.closest(".ele-body")
const pagePadding = pageBody ? parseFloat(getComputedStyle(pageBody).paddingBottom) || 0 : 0
const visibleBottom = Math.min(content.getBoundingClientRect().bottom, window.innerHeight) - pagePadding - 4
const nextHeight = Math.max(120, Math.floor(table.getBoundingClientRect().height + visibleBottom - card.getBoundingClientRect().bottom))
if (nextHeight !== this.tableHeight) {
this.tableHeight = nextHeight
}
if (!this.tableResizeObserver && window.ResizeObserver) {
this.tableResizeObserver = new ResizeObserver(() => this.updateTableHeight())
const resizeTargets = [content, document.querySelector("#ele-admin-tabs"), this.$el.querySelector(".el-card"), card, card.querySelector(".ele-table-tool"), pagination]
resizeTargets.forEach((element) => {
if (element) {
this.tableResizeObserver.observe(element)
}
})
}
},
// PJAX 切换或组件销毁时清理监听,防止旧页面继续修改表格高度。
stopTableResize() {
window.removeEventListener("resize", this.pageVisibleHandler)
window.removeEventListener("load", this.pageVisibleHandler)
$(document).off("pjax:send", this.pageExitHandler)
$(document).off("pjax:complete", this.pageVisibleHandler)
if (this.tabsObserver) {
this.tabsObserver.disconnect()
this.tabsObserver = null
}
if (this.tableResizeObserver) {
this.tableResizeObserver.disconnect()
this.tableResizeObserver = null
}
},
handleSelectionChange(val) {
this.multipleSelection = val
},
@@ -259,6 +336,16 @@ layout("/layouts/platform.html"){
})
</script>
<style>
.ele-admin-fixed-body .ele-admin-content {
overflow-y: hidden;
}
#app .train-signup-user-table .el-table__body-wrapper {
overscroll-behavior-y: contain;
}
</style>
<!--#
}
#-->
@@ -3,7 +3,7 @@ layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava>
<guava @hook:mounted="onPageVisible">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="所属年份">
@@ -96,7 +96,9 @@ layout("/layouts/platform.html"){
<el-table
:data="tableData"
:max-height="tableHeight"
:size="tableSize"
class="welfare-selection-table"
@sort-change="pageOrder"
ref="tableRef"
row-key="id"
@@ -168,6 +170,31 @@ layout("/layouts/platform.html"){
new Vue({
el: "#app",
mixins: [initTableMixins],
mounted() {
// 原生和 jQuery 事件会改变 this,保留箭头回调供执行和解绑使用。
this.pageVisibleHandler = () => this.onPageVisible()
this.pageExitHandler = () => this.stopTableResize()
window.addEventListener("resize", this.pageVisibleHandler)
window.addEventListener("load", this.pageVisibleHandler)
$(document).one("pjax:send", this.pageExitHandler)
$(document).on("pjax:complete", this.pageVisibleHandler)
// 顶部页签独立异步挂载,只移动内容区时 ResizeObserver 不会收到尺寸变化。
const tabs = document.querySelector("#ele-admin-tabs")
if (tabs && window.MutationObserver) {
this.tabsObserver = new MutationObserver(() => this.onPageVisible())
this.tabsObserver.observe(tabs, { childList: true, subtree: true })
}
this.onPageVisible()
},
updated() {
// 首次取得尺寸不代表异步布局已稳定,每次渲染后继续校正。
if (this.$refs.tableRef) {
this.onPageVisible()
}
},
beforeDestroy() {
this.stopTableResize()
},
components: {
"option-select": optionSelect
},
@@ -175,9 +202,11 @@ layout("/layouts/platform.html"){
return {
pageForm: {
searchName: "username",
pageSize: 50,
year: new Date().getFullYear().toString(),
isSelect: null
},
tableHeight: 300,
unionOptions: [],
unitOptions: [],
projectOptions: [],
@@ -216,6 +245,57 @@ layout("/layouts/platform.html"){
}
},
methods: {
// 等页面完成显示后再测量,避免 PJAX 隐藏内容区时得到错误尺寸。
onPageVisible() {
this.$nextTick(() => requestAnimationFrame(() => this.updateTableHeight()))
},
// 用卡片实际底部校正表格高度,涵盖分页和卡片留白,避免首次渲染后布局变化遮住分页。
updateTableHeight() {
if (!this.$refs.tableRef) {
return
}
const table = this.$refs.tableRef.$el
const card = table.closest(".el-card")
const content = table.closest(".ele-admin-content")
const pagination = card.querySelector(".el-pagination-container")
if (!pagination || !table.isConnected || !table.getBoundingClientRect().width || !pagination.getBoundingClientRect().height) {
return
}
content.scrollTop = 0
const pageBody = table.closest(".ele-body")
const pagePadding = parseFloat(getComputedStyle(pageBody).paddingBottom) || 0
// 页签异步出现时内容区可能下移,分页必须留在浏览器可视范围内。
const visibleBottom = Math.min(content.getBoundingClientRect().bottom, window.innerHeight) - pagePadding - 4
const nextHeight = Math.max(120, Math.floor(table.getBoundingClientRect().height + visibleBottom - card.getBoundingClientRect().bottom))
if (nextHeight !== this.tableHeight) {
this.tableHeight = nextHeight
}
// 卡片和筛选区异步变化时重新校正;表格高度稳定后不再触发更新。
if (!this.tableResizeObserver && window.ResizeObserver) {
this.tableResizeObserver = new ResizeObserver(() => this.updateTableHeight())
const resizeTargets = [content, document.querySelector("#ele-admin-tabs"), this.$el.querySelector(".el-card"), card, card.querySelector(".ele-table-tool"), pagination]
resizeTargets.forEach((element) => {
if (element) {
this.tableResizeObserver.observe(element)
}
})
}
},
// PJAX 切换或组件销毁时移除监听,避免旧页面继续响应窗口变化。
stopTableResize() {
window.removeEventListener("resize", this.pageVisibleHandler)
window.removeEventListener("load", this.pageVisibleHandler)
$(document).off("pjax:send", this.pageExitHandler)
$(document).off("pjax:complete", this.pageVisibleHandler)
if (this.tabsObserver) {
this.tabsObserver.disconnect()
this.tabsObserver = null
}
if (this.tableResizeObserver) {
this.tableResizeObserver.disconnect()
this.tableResizeObserver = null
}
},
// 获取福利列表
getWelfareList() {
this.$axios.post("/platform/welfare/common/list", { year: this.pageForm.year }).then((res) => {
@@ -355,6 +435,16 @@ layout("/layouts/platform.html"){
})
</script>
<style>
.ele-admin-fixed-body .ele-admin-content {
overflow-y: hidden;
}
#app .welfare-selection-table .el-table__body-wrapper {
overscroll-behavior-y: contain;
}
</style>
<!--#
}
#-->
@@ -3,7 +3,7 @@ layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<guava ref="guava" @hook:mounted="onPageVisible">
<template>
<el-card shadow="never">
<search @search="doSearch">
@@ -194,7 +194,9 @@ layout("/layouts/platform.html"){
<el-table
:data="tableData"
:max-height="tableHeight"
:size="tableSize"
class="welfare-list-table"
@sort-change="pageOrder"
ref="tableRef"
row-key="id"
@@ -294,6 +296,29 @@ layout("/layouts/platform.html"){
},
mounted() {
this.checkedFields = this.tableColumns.filter(c => c.checked !== 0).map(c => c.prop);
// 原生和 jQuery 事件会改变 this,保留箭头回调供执行和解绑使用。
this.pageVisibleHandler = () => this.onPageVisible()
this.pageExitHandler = () => this.stopTableResize()
window.addEventListener("resize", this.pageVisibleHandler)
window.addEventListener("load", this.pageVisibleHandler)
$(document).one("pjax:send", this.pageExitHandler)
$(document).on("pjax:complete", this.pageVisibleHandler)
// 顶部页签独立异步挂载,只移动内容区时 ResizeObserver 不会收到尺寸变化。
const tabs = document.querySelector("#ele-admin-tabs")
if (tabs && window.MutationObserver) {
this.tabsObserver = new MutationObserver(() => this.onPageVisible())
this.tabsObserver.observe(tabs, { childList: true, subtree: true })
}
this.onPageVisible()
},
updated() {
// 首次取得尺寸不代表异步布局已稳定,每次渲染后继续校正。
if (this.$refs.tableRef) {
this.onPageVisible()
}
},
beforeDestroy() {
this.stopTableResize()
},
components: {
add_welfare_list_by_select: ADD_WELFARE_LIST_BY_SELECT,
@@ -306,8 +331,10 @@ layout("/layouts/platform.html"){
checkedFields: [],
pageForm: {
searchName: "username",
pageSize: 50,
year: new Date().getFullYear().toString()
},
tableHeight: 300,
unionOptions: [],
unitOptions: [],
sexOptions: ["男", "女"],
@@ -335,6 +362,57 @@ layout("/layouts/platform.html"){
}
},
methods: {
// 等页面完成显示后再测量,避免 PJAX 隐藏内容区时得到错误尺寸。
onPageVisible() {
this.$nextTick(() => requestAnimationFrame(() => this.updateTableHeight()))
},
// 用卡片实际底部校正表格高度,涵盖分页和卡片留白,避免首次渲染后布局变化遮住分页。
updateTableHeight() {
if (!this.$refs.tableRef) {
return
}
const table = this.$refs.tableRef.$el
const card = table.closest(".el-card")
const content = table.closest(".ele-admin-content")
const pagination = card.querySelector(".el-pagination-container")
if (!pagination || !table.isConnected || !table.getBoundingClientRect().width || !pagination.getBoundingClientRect().height) {
return
}
content.scrollTop = 0
const pageBody = table.closest(".ele-body")
const pagePadding = parseFloat(getComputedStyle(pageBody).paddingBottom) || 0
// 页签异步出现时内容区可能下移,分页必须留在浏览器可视范围内。
const visibleBottom = Math.min(content.getBoundingClientRect().bottom, window.innerHeight) - pagePadding - 4
const nextHeight = Math.max(120, Math.floor(table.getBoundingClientRect().height + visibleBottom - card.getBoundingClientRect().bottom))
if (nextHeight !== this.tableHeight) {
this.tableHeight = nextHeight
}
// 卡片和筛选区异步变化时重新校正;表格高度稳定后不再触发更新。
if (!this.tableResizeObserver && window.ResizeObserver) {
this.tableResizeObserver = new ResizeObserver(() => this.updateTableHeight())
const resizeTargets = [content, document.querySelector("#ele-admin-tabs"), this.$el.querySelector(".el-card"), card, card.querySelector(".ele-table-tool"), pagination]
resizeTargets.forEach((element) => {
if (element) {
this.tableResizeObserver.observe(element)
}
})
}
},
// PJAX 切换或组件销毁时移除监听,避免旧页面继续响应窗口变化。
stopTableResize() {
window.removeEventListener("resize", this.pageVisibleHandler)
window.removeEventListener("load", this.pageVisibleHandler)
$(document).off("pjax:send", this.pageExitHandler)
$(document).off("pjax:complete", this.pageVisibleHandler)
if (this.tabsObserver) {
this.tabsObserver.disconnect()
this.tabsObserver = null
}
if (this.tableResizeObserver) {
this.tableResizeObserver.disconnect()
this.tableResizeObserver = null
}
},
// 导出收货地址
exportAddress() {
this.$downLoad("/platform/welfare/list/mange/exportAddress", this.pageForm)
@@ -422,6 +500,16 @@ layout("/layouts/platform.html"){
})
</script>
<style>
.ele-admin-fixed-body .ele-admin-content {
overflow-y: hidden;
}
#app .welfare-list-table .el-table__body-wrapper {
overscroll-behavior-y: contain;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,603 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
/* 页面限定在可用视口内,顶部信息不收缩,剩余高度交给人员列表。 */
#app.selection-situation-page { height: 100%; height: 100dvh; min-height: 0; flex-direction: column; overflow: hidden; box-sizing: border-box; background: #f4f6fa; color: #1c293a; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
#app.selection-situation-page:not([v-cloak]) { display: flex; }
.selection-situation-page > .van-nav-bar,
.selection-situation-page > .selection-filters,
.selection-situation-page > .selection-result-count { flex-shrink: 0; }
/* van-list 会监听此滚动容器;到达边界时不把滚动继续传给外层页面。 */
.selection-person-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; -webkit-overflow-scrolling: touch; padding: 16px 0 calc(20px + env(safe-area-inset-bottom)); }
/* 导航栏沿用福利列表的 Vant 默认样式:白底、深色标题、蓝色返回文字和箭头。 */
/* 项目独占一行以容纳长名称;两个布尔筛选等宽排列,并分别标明业务含义。 */
.selection-filters { padding: 12px 16px; background: #fff; }
.selection-filter-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.selection-filter-value { display: flex; align-items: center; justify-content: space-between; flex: 1 1 0; min-width: 0; min-height: 48px; padding: 8px 12px; border: 1px solid #e2e7ef; border-radius: 8px; background: #f8fafc; color: #1c293a; font-family: inherit; font-size: 14px; text-align: left; cursor: pointer; }
.selection-filter-project { background: #f0f7ff; border-color: #d0e5fc; }
.selection-filter-value.is-active { background: #f0f7ff; border-color: #a9d1f2; }
.selection-filter-copy { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 3px; }
.selection-filter-label { color: #667085; font-size: 12px; line-height: 16px; }
.selection-filter-selected { font-weight: 500; line-height: 20px; }
.selection-filter-project .selection-filter-selected,
.selection-filter-value.is-active .selection-filter-selected { color: #006dbb; }
.selection-filter-value span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.selection-filter-value .van-icon { flex-shrink: 0; margin-left: 8px; color: #52627a; }
.selection-filter-value:focus-visible { outline: 2px solid #006dbb; outline-offset: 2px; }
.selection-search-row { display: flex; align-items: center; gap: 8px; }
.selection-search-row .van-search { flex: 1; min-width: 0; padding: 0; background: transparent; }
.selection-search-row .van-search__content { box-sizing: border-box; height: 40px; align-items: center; border: 1px solid #e2e7ef; border-radius: 8px; background: #f4f6f9; }
.selection-search-row .van-field__control { font-size: 15px; }
.selection-search-row .van-button { flex-shrink: 0; min-width: 68px; height: 40px; padding: 0 14px; border-radius: 8px; background: #006dbb; border-color: #006dbb; font-size: 14px; font-weight: 500; }
.selection-result-count { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 0 16px; border-top: 1px solid #edf0f5; background: #fff; color: #667085; font-size: 13px; }
.selection-result-title { color: #1c293a; font-size: 15px; font-weight: 600; }
.selection-result-count strong { color: #006dbb; font-size: 16px; font-weight: 600; }
/* 卡片按身份、所属组织、福利与操作分为三行;长名称自然换行,避免信息被裁切。 */
.selection-person-card { margin: 0 12px 12px; padding: 12px 14px; background: #fff; border-radius: 10px; }
.selection-person-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; min-height: 24px; }
.selection-person-identity { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 12px; flex: 1; min-width: 0; line-height: 24px; }
.selection-person-name { color: #1c293a; font-size: 18px; font-weight: 600; overflow-wrap: anywhere; }
.selection-person-number { color: #667085; font-size: 13px; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.selection-person-status { display: inline-flex; align-items: center; flex-shrink: 0; gap: 6px; color: #138b42; font-size: 13px; line-height: 24px; white-space: nowrap; }
.selection-person-status .fa { font-size: 10px; }
.selection-person-status.is-unselected { color: #ad7900; }
.selection-person-organization { margin-top: 6px; color: #667085; font-size: 13px; line-height: 22px; overflow-wrap: anywhere; }
.selection-person-separator { padding: 0 5px; color: #98a2b3; }
.selection-person-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 6px; }
.selection-person-benefit { display: flex; align-items: baseline; flex: 1; min-width: 0; gap: 8px; font-size: 14px; line-height: 22px; }
.selection-person-benefit-label { flex-shrink: 0; color: #8792a3; }
.selection-person-benefit-value { min-width: 0; color: #1c293a; font-weight: 500; overflow-wrap: anywhere; }
.selection-person-benefit-value.is-empty { color: #98a2b3; font-weight: 400; }
.selection-person-footer .van-button { flex-shrink: 0; min-width: 68px; height: 34px; padding: 0 12px; border: none; border-radius: 6px; background: #e7f3ff; color: #006dbb; font-size: 14px; font-weight: 500; }
.selection-empty { padding-top: 25px; }
/* 弹框随内容撑开并限制最大高度,标题和操作按钮保留在滚动内容之外。 */
.selection-situation-page .selection-popup { border-radius: 18px 18px 0 0; }
.selection-sheet-head { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; gap: 12px; min-height: 56px; padding: 0 12px 0 20px; border-bottom: 1px solid #edf0f5; background: #fff; color: #1c293a; font-size: 17px; font-weight: 600; }
.selection-sheet-close { display: flex; align-items: center; justify-content: center; flex: 0 0 40px; width: 40px; height: 40px; padding: 0; border: none; background: transparent; color: #8792a3; font-size: 20px; cursor: pointer; }
.selection-picker { display: flex; flex-direction: column; max-height: 75vh; max-height: 75dvh; background: #fff; }
.selection-picker-description { flex-shrink: 0; padding: 16px 20px 8px; color: #8792a3; font-size: 13px; line-height: 20px; }
.selection-picker-options { min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; padding: 12px 16px; }
.selection-picker-option { display: flex; align-items: center; justify-content: space-between; gap: 14px; box-sizing: border-box; width: 100%; min-height: 52px; padding: 12px; border: none; border-bottom: 1px solid #f0f2f5; background: #fff; color: #1c293a; font-family: inherit; font-size: 14px; text-align: left; cursor: pointer; }
.selection-picker-option.is-active { border-bottom-color: transparent; border-radius: 6px; background: #eef7ff; }
.selection-picker-option-label { min-width: 0; line-height: 22px; overflow-wrap: anywhere; }
.selection-picker-option-label .fa { margin-right: 8px; color: #138b42; font-size: 10px; }
.selection-picker-option-label .fa.is-unselected { color: #ad7900; }
.selection-picker-option > .van-icon { flex-shrink: 0; color: #c3cbd6; font-size: 20px; }
.selection-picker-option.is-active > .van-icon { color: #006dbb; }
.selection-picker-footer { display: flex; flex-shrink: 0; gap: 10px; padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); border-top: 1px solid #edf0f5; background: #fff; }
.selection-picker-footer .van-button { height: 44px; border-radius: 6px; font-size: 14px; font-weight: 500; }
.selection-picker-cancel { flex: 2; color: #006dbb; border-color: #cad7e7; }
.selection-picker-confirm { flex: 3; background: #006dbb; border-color: #006dbb; }
.selection-proxy { display: flex; flex-direction: column; max-height: 85vh; max-height: 85dvh; background: #f4f6fa; }
.selection-proxy-content { flex: 0 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; padding: 14px 16px; }
.selection-proxy-person { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 14px; color: #1c293a; line-height: 24px; }
.selection-proxy-person-name { font-size: 17px; font-weight: 600; overflow-wrap: anywhere; }
.selection-proxy-person-number { color: #667085; font-size: 13px; overflow-wrap: anywhere; }
.selection-proxy-section { margin-bottom: 12px; padding: 14px; border-radius: 10px; background: #fff; }
.selection-proxy-section:last-child { margin-bottom: 0; }
.selection-proxy-title { margin-bottom: 8px; color: #1c293a; font-size: 15px; font-weight: 600; line-height: 22px; overflow-wrap: anywhere; }
.selection-proxy-description { margin-bottom: 12px; color: #8792a3; font-size: 13px; line-height: 20px; }
.selection-option { display: flex; align-items: center; justify-content: space-between; box-sizing: border-box; min-height: 44px; margin-top: 8px; padding: 10px 12px; border: 1px solid #e2e7ef; border-radius: 6px; font-size: 14px; line-height: 22px; }
.selection-option.is-active { border-color: #006dbb; background: #eef7ff; }
.selection-option-radio { cursor: pointer; }
.selection-option-name { flex: 1; min-width: 0; padding-right: 12px; overflow-wrap: anywhere; }
.selection-option .van-stepper { flex-shrink: 0; }
.selection-proxy-section .van-cell { align-items: center; padding: 8px 0; }
.selection-proxy-section .van-field__label { width: 72px; color: #667085; font-size: 13px; }
.selection-proxy-section .van-field__body { padding: 7px 10px; border: 1px solid #e2e7ef; border-radius: 6px; background: #f4f6f9; }
.selection-proxy-hint { display: flex; align-items: center; gap: 6px; margin-top: 10px; color: #8792a3; font-size: 12px; line-height: 18px; }
.selection-proxy-hint .van-icon { flex-shrink: 0; color: #006dbb; font-size: 14px; }
.selection-proxy-loading { min-height: 160px; padding: 48px 16px; box-sizing: border-box; text-align: center; }
.selection-proxy-footer { flex-shrink: 0; padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); border-top: 1px solid #edf0f5; background: #fff; }
.selection-proxy-footer .van-button { height: 44px; border-radius: 6px; background: #006dbb; border-color: #006dbb; font-size: 15px; }
</style>
<div id="app" v-cloak class="selection-situation-page">
<van-nav-bar title="选择情况" left-text="返回" left-arrow @click-left="historyBack"></van-nav-bar>
<div class="selection-filters">
<div class="selection-search-row">
<van-search v-model="searchKeyword" placeholder="工号或姓名" @search="doSearch"></van-search>
<van-button type="info" @click="doSearch">查询</van-button>
</div>
<div class="selection-filter-row">
<button type="button" class="selection-filter-value selection-filter-project" aria-label="选择福利项目"
:title="currentProjectName || '请选择福利项目'" @click="openPicker('project')">
<span class="selection-filter-copy">
<span class="selection-filter-label">福利项目</span>
<span class="selection-filter-selected">{{ currentProjectName || '请选择福利项目' }}</span>
</span>
<van-icon name="arrow-down"></van-icon>
</button>
</div>
<div class="selection-filter-row">
<button type="button" class="selection-filter-value" :class="{'is-active': pageForm.isSelect !== null}"
aria-label="选择状态" @click="openPicker('status')">
<span class="selection-filter-copy">
<span class="selection-filter-label">选择状态</span>
<span class="selection-filter-selected">{{ currentStatusName }}</span>
</span>
<van-icon name="arrow-down"></van-icon>
</button>
<button type="button" class="selection-filter-value" :class="{'is-active': pageForm.isSelectByAdmin !== null}"
aria-label="管理员代选" @click="openPicker('admin')">
<span class="selection-filter-copy">
<span class="selection-filter-label">管理员代选</span>
<span class="selection-filter-selected">{{ currentAdminName }}</span>
</span>
<van-icon name="arrow-down"></van-icon>
</button>
</div>
</div>
<div class="selection-result-count" v-if="pageForm.projectId">
<span class="selection-result-title">人员名单</span>
<span>共 <strong>{{ pageForm.totalCount }}</strong> 人</span>
</div>
<div ref="personScroll" class="selection-person-scroll">
<van-list v-if="pageForm.projectId" v-model="tableLoading" :finished="finished" finished-text="没有更多了" @load="pageData">
<div class="selection-person-card" v-for="row in tableData" :key="row.id">
<div class="selection-person-head">
<div class="selection-person-identity">
<span class="selection-person-name">{{ row.userName || '-' }}</span>
<span class="selection-person-number" :aria-label="'工号 ' + (row.loginName || '-')">{{ row.loginName || '-' }}</span>
</div>
<span class="selection-person-status" :class="{'is-unselected': !row.selectedOptions}">
<i class="fa fa-circle" aria-hidden="true"></i>
{{ row.selectedOptions ? '已选择' : '未选择' }}
</span>
</div>
<div class="selection-person-organization">
<span :title="'所属工会:' + (row.welfareUnionName || '-')">{{ row.welfareUnionName || '-' }}</span><span class="selection-person-separator" aria-hidden="true">·</span><span :title="'所属单位:' + (row.welfareUnitName || '-')">{{ row.welfareUnitName || '-' }}</span>
</div>
<div class="selection-person-footer">
<div class="selection-person-benefit">
<span class="selection-person-benefit-label">福利</span>
<span class="selection-person-benefit-value" :class="{'is-empty': !row.selectedOptions}">{{ row.selectedOptions || '暂未选择' }}</span>
</div>
<van-button type="info" size="small" @click="openProxy(row)">代选</van-button>
</div>
</div>
<van-empty class="selection-empty" v-if="!tableLoading && tableData.length === 0" description="暂无符合条件的人员"></van-empty>
</van-list>
<div class="selection-empty" v-else-if="projectLoading" style="text-align:center"><van-loading>加载福利项目...</van-loading></div>
<van-empty class="selection-empty" v-else description="暂无福利项目"></van-empty>
</div>
<van-popup v-model="showProjectPicker" class="selection-popup" position="bottom" round :close-on-click-overlay="false">
<div class="selection-picker">
<div class="selection-sheet-head">
<span>选择福利项目</span>
<button type="button" class="selection-sheet-close" aria-label="关闭福利项目选择" @click="closePopup('project')"><van-icon name="cross"></van-icon></button>
</div>
<div class="selection-picker-description">请选择本次查询的福利项目</div>
<div class="selection-picker-options" role="group" aria-label="福利项目">
<button v-for="item in projectColumns" :key="item.value" type="button" class="selection-picker-option"
:class="{'is-active': draftProjectId === item.value}" :aria-pressed="draftProjectId === item.value"
@click="selectPickerOption('project', item.value)">
<span class="selection-picker-option-label">{{ item.text }}</span>
<van-icon :name="draftProjectId === item.value ? 'checked' : 'circle'" aria-hidden="true"></van-icon>
</button>
</div>
<div class="selection-picker-footer">
<van-button class="selection-picker-cancel" @click="closePopup('project')">取消</van-button>
<van-button class="selection-picker-confirm" type="info" :disabled="!draftProjectId" @click="chooseProject">确认选择</van-button>
</div>
</div>
</van-popup>
<van-popup v-model="showStatusPicker" class="selection-popup" position="bottom" round :close-on-click-overlay="false">
<div class="selection-picker">
<div class="selection-sheet-head">
<span>选择状态</span>
<button type="button" class="selection-sheet-close" aria-label="关闭选择状态" @click="closePopup('status')"><van-icon name="cross"></van-icon></button>
</div>
<div class="selection-picker-options" role="group" aria-label="选择状态">
<button v-for="item in statusColumns" :key="item.text" type="button" class="selection-picker-option"
:class="{'is-active': draftStatus === item.value}" :aria-pressed="draftStatus === item.value"
@click="selectPickerOption('status', item.value)">
<span class="selection-picker-option-label"><i v-if="item.value !== null" class="fa fa-circle" :class="{'is-unselected': item.value === false}" aria-hidden="true"></i>{{ item.text }}</span>
<van-icon :name="draftStatus === item.value ? 'checked' : 'circle'" aria-hidden="true"></van-icon>
</button>
</div>
<div class="selection-picker-footer">
<van-button class="selection-picker-cancel" @click="closePopup('status')">取消</van-button>
<van-button class="selection-picker-confirm" type="info" @click="chooseStatus">确认选择</van-button>
</div>
</div>
</van-popup>
<van-popup v-model="showAdminPicker" class="selection-popup" position="bottom" round :close-on-click-overlay="false">
<div class="selection-picker">
<div class="selection-sheet-head">
<span>管理员代选</span>
<button type="button" class="selection-sheet-close" aria-label="关闭管理员代选筛选" @click="closePopup('admin')"><van-icon name="cross"></van-icon></button>
</div>
<div class="selection-picker-options" role="group" aria-label="管理员代选">
<button v-for="item in adminColumns" :key="item.text" type="button" class="selection-picker-option"
:class="{'is-active': draftAdmin === item.value}" :aria-pressed="draftAdmin === item.value"
@click="selectPickerOption('admin', item.value)">
<span class="selection-picker-option-label">{{ item.text }}</span>
<van-icon :name="draftAdmin === item.value ? 'checked' : 'circle'" aria-hidden="true"></van-icon>
</button>
</div>
<div class="selection-picker-footer">
<van-button class="selection-picker-cancel" @click="closePopup('admin')">取消</van-button>
<van-button class="selection-picker-confirm" type="info" @click="chooseAdmin">确认选择</van-button>
</div>
</div>
</van-popup>
<van-popup v-model="showProxy" class="selection-popup" position="bottom" round :close-on-click-overlay="false">
<div class="selection-proxy">
<div class="selection-sheet-head">
<span>代选福利</span>
<button type="button" class="selection-sheet-close" aria-label="关闭代选福利" @click="closePopup('proxy')"><van-icon name="cross"></van-icon></button>
</div>
<div class="selection-proxy-content" v-if="!proxyLoading">
<div class="selection-proxy-person">
<span class="selection-proxy-person-name">{{ proxyRow.userName || '-' }}</span>
<span class="selection-proxy-person-number">工号 {{ proxyRow.loginName || '-' }}</span>
</div>
<div class="selection-proxy-section">
<div class="selection-proxy-title">{{ proxyProject.name || '福利选项' }}</div>
<div class="selection-proxy-description">{{ proxyProject.isCheckBox === 'radio' ? '请选择一项福利' : '请选择福利及份数' }}</div>
<van-radio-group v-if="proxyProject.isCheckBox === 'radio'" v-model="selectedRadioId">
<div class="selection-option selection-option-radio" v-for="option in proxyProject.options" :key="option.id"
:class="{'is-active': selectedRadioId === option.id}" @click="chooseRadio(option.id)">
<span class="selection-option-name">{{ option.optionName }}</span>
<van-radio :name="option.id" checked-color="#006dbb"></van-radio>
</div>
</van-radio-group>
<template v-else>
<div class="selection-option" v-for="option in proxyProject.options" :key="option.id" :class="{'is-active': option.selectNum > 0}">
<span class="selection-option-name">{{ option.optionName }}</span>
<van-stepper :value="option.selectNum" :min="0" :max="optionMax(option)" integer disable-input
@change="updateOptionNum(option, $event)"></van-stepper>
</div>
</template>
</div>
<div class="selection-proxy-section">
<div class="selection-proxy-title">联系信息</div>
<van-field v-model="proxyForm.welfareMobile" type="tel" maxlength="11" label="福利电话" placeholder="请输入11位手机号" :border="false" required></van-field>
<van-field v-if="proxyProject.provideMode === 3" v-model="proxyForm.receiveAddress" type="textarea"
maxlength="100" rows="2" autosize label="收货地址" placeholder="请输入被代选人的收货地址" :border="false" required></van-field>
<div class="selection-proxy-hint"><van-icon name="info-o"></van-icon><span>请核对人员信息后提交</span></div>
</div>
<div class="selection-proxy-section" v-if="proxyProject.signMode === 2">
<div class="selection-proxy-title">签字确认</div>
<h5-signature v-model="proxyForm.userSign"></h5-signature>
</div>
</div>
<div class="selection-proxy-content selection-proxy-loading" v-else><van-loading>加载中...</van-loading></div>
<div class="selection-proxy-footer">
<van-button block type="info" :loading="formLoading" :disabled="proxyLoading || !hasSelection" @click="submitProxy">
{{ hasSubmittedBefore ? '确认修改' : '确认代选' }}
</van-button>
</div>
</div>
</van-popup>
</div>
<script>
new Vue({
el: "#app",
store,
data() {
return {
pageForm: {pageNumber: 1, pageSize: 10, projectId: null, isSelect: null, isSelectByAdmin: null, totalCount: 0},
searchKeyword: "",
projectColumns: [],
projectLoading: true,
statusColumns: [
{text: "全部", value: null},
{text: "已选择", value: true},
{text: "未选择", value: false}
],
adminColumns: [
{text: "全部", value: null},
{text: "是", value: true},
{text: "否", value: false}
],
tableData: [],
tableLoading: false,
listInFlight: false,
finished: false,
queryVersion: 0,
showProjectPicker: false,
showStatusPicker: false,
showAdminPicker: false,
draftProjectId: null,
draftStatus: null,
draftAdmin: null,
showProxy: false,
historyOwner: "",
proxyRow: {},
proxyProject: {},
proxyLoading: false,
proxyRequestVersion: 0,
selectedRadioId: null,
proxyForm: {welfareMobile: "", receiveAddress: "", userSign: ""},
hasSubmittedBefore: false,
formLoading: false
}
},
computed: {
currentProjectName() {
const current = this.projectColumns.find((item) => item.value === this.pageForm.projectId)
return current ? current.text : ""
},
currentStatusName() {
const current = this.statusColumns.find((item) => item.value === this.pageForm.isSelect)
return current ? current.text : "全部"
},
// 将管理员代选的 null/true/false 显示为全部/是/否,保留 false 的有效含义。
currentAdminName() {
const current = this.adminColumns.find((item) => item.value === this.pageForm.isSelectByAdmin)
return current ? current.text : "全部"
},
selectedOptions() {
if (!Array.isArray(this.proxyProject.options)) return []
if (this.proxyProject.isCheckBox === "radio") {
return this.proxyProject.options.filter((item) => item.id === this.selectedRadioId)
}
return this.proxyProject.options.filter((item) => Number(item.selectNum) > 0)
},
hasSelection() {
return this.selectedOptions.length > 0
}
},
mounted() {
this.historyOwner = "welfare-selection-situation-" + this._uid
this.loadProjects()
},
beforeDestroy() {
this.queryVersion++
this.proxyRequestVersion++
h5PopupHistory.unregisterOwner(this.historyOwner)
},
methods: {
// 项目列表不传年份,供移动端直接选择各年度福利项目。
loadProjects() {
this.$axios.post("/platform/welfare/common/list", {}).then((res) => {
if (res.code !== 0) {
this.$toast.fail(res.msg || "福利项目加载失败")
return
}
const columns = (res.data || []).map((item) => ({text: item.name, value: item.id}))
this.$set(this, "projectColumns", columns)
if (columns.length > 0) {
this.$set(this.pageForm, "projectId", columns[0].value)
this.doSearch()
}
}).catch(() => {
this.$toast.fail("福利项目加载失败")
}).finally(() => {
this.$set(this, "projectLoading", false)
})
},
// type 为 project/status/admin;打开对应筛选并注册全局历史回调,无返回值。
openPicker(type) {
if (type === "project" && this.projectColumns.length === 0) return
const picker = {
project: {visible: "showProjectPicker", draft: "draftProjectId", field: "projectId"},
status: {visible: "showStatusPicker", draft: "draftStatus", field: "isSelect"},
admin: {visible: "showAdminPicker", draft: "draftAdmin", field: "isSelectByAdmin"}
}[type]
if (!picker) return
// 点选仅更新弹框草稿;重新打开时还原已确认条件,取消和返回均不发起查询。
this.$set(this, picker.draft, this.pageForm[picker.field])
h5PopupHistory.open(this.historyOwner + "-" + type, this.historyOwner, () => {
this.$set(this, picker.visible, false)
})
this.$set(this, picker.visible, true)
},
// 仅由按钮发起一次历史回退,弹框显隐由全局历史回调同步,取消不触发查询。
closePopup(type) {
return h5PopupHistory.close(this.historyOwner + "-" + type)
},
// type 为 project/status/admin,value 为项目 ID 或 null/true/false;仅修改草稿,无返回值、不查询。
selectPickerOption(type, value) {
const draft = {project: "draftProjectId", status: "draftStatus", admin: "draftAdmin"}[type]
if (draft) this.$set(this, draft, value)
},
// 确认时从项目列表校验草稿值,仅变更有效项目后查询;重复点击由历史栈拦截。
chooseProject() {
const item = this.projectColumns.find((option) => option.value === this.draftProjectId)
if (!item || !this.closePopup("project")) return
if (this.pageForm.projectId === item.value) return
this.$set(this.pageForm, "projectId", item.value)
this.doSearch()
},
// 状态值 null/true/false 分别代表全部、已选择、未选择,false 也是有效筛选值。
chooseStatus() {
const item = this.statusColumns.find((option) => option.value === this.draftStatus)
if (!item || !this.closePopup("status")) return
if (this.pageForm.isSelect === item.value) return
this.$set(this.pageForm, "isSelect", item.value)
this.doSearch()
},
// 全部不限制代选来源;是/否分别传 true/false,确认变更后复用现有分页重置逻辑。
chooseAdmin() {
const item = this.adminColumns.find((option) => option.value === this.draftAdmin)
if (!item || !this.closePopup("admin")) return
if (this.pageForm.isSelectByAdmin === item.value) return
this.$set(this.pageForm, "isSelectByAdmin", item.value)
this.doSearch()
},
// 现有接口分别按姓名或工号过滤;含中文的关键词作为姓名,其余作为工号。
doSearch() {
this.queryVersion++
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "totalCount", 0)
this.$set(this, "tableData", [])
this.$set(this, "finished", false)
this.$set(this, "listInFlight", false)
this.$set(this, "tableLoading", false)
// 切换筛选条件或重新查询时从第一张卡片开始,加载下一页时保持当前位置。
if (this.$refs.personScroll) this.$refs.personScroll.scrollTop = 0
if (this.pageForm.projectId) this.pageData()
},
// pageForm 传项目 ID、分页信息及选择状态;管理员代选为 true/false,“全部”省略该字段。
// 关键词按姓名或工号提交;响应 data 为 {list, totalCount},分别用于追加人员和判断分页结束。
pageData() {
if (!this.pageForm.projectId || this.finished || this.listInFlight) return
const version = this.queryVersion
const pageForm = Object.assign({}, this.pageForm)
// “全部”省略该查询参数;false 必须原样传递,表示已有选择且非管理员代选。
if (pageForm.isSelectByAdmin === null || pageForm.isSelectByAdmin === undefined || pageForm.isSelectByAdmin === "") {
delete pageForm.isSelectByAdmin
}
const keyword = this.searchKeyword.trim()
if (keyword) {
if (/[\u3400-\u9fff]/.test(keyword)) pageForm.userName = keyword
else pageForm.loginName = keyword
}
this.$set(this, "listInFlight", true)
this.$set(this, "tableLoading", true)
this.$axios.post("/platform/welfare/selection/situation/pageData", {pageForm: JSON.stringify(pageForm)})
.then((res) => {
if (version !== this.queryVersion) return
if (res.code !== 0) {
this.$toast.fail(res.msg || "查询失败")
this.$set(this, "finished", true)
return
}
const data = res.data || {}
const rows = data.list || []
this.$set(this, "tableData", this.tableData.concat(rows))
this.$set(this.pageForm, "totalCount", data.totalCount || 0)
this.$set(this.pageForm, "pageNumber", pageForm.pageNumber + 1)
this.$set(this, "finished", rows.length === 0 || this.tableData.length >= this.pageForm.totalCount)
}).catch(() => {
if (version === this.queryVersion) {
this.$toast.fail("查询失败,请重试")
this.$set(this, "finished", true)
}
})
.finally(() => {
if (version !== this.queryVersion) return
this.$set(this, "listInFlight", false)
this.$set(this, "tableLoading", false)
})
},
// 打开单人代选,回显当前项目配置、已有选择和被代选人的福利电话。
openProxy(row) {
this.proxyRequestVersion++
const version = this.proxyRequestVersion
this.$set(this, "proxyRow", row)
this.$set(this, "proxyProject", {})
this.$set(this, "selectedRadioId", null)
this.$set(this, "proxyForm", {welfareMobile: "", receiveAddress: "", userSign: ""})
this.$set(this, "proxyLoading", true)
this.$set(this, "hasSubmittedBefore", false)
h5PopupHistory.open(this.historyOwner + "-proxy", this.historyOwner, () => {
this.proxyRequestVersion++
this.$set(this, "showProxy", false)
})
this.$set(this, "showProxy", true)
Promise.all([
this.$axios.post("/platform/welfare/common/projectInfo", {id: row.projectId}),
this.$axios.post("/platform/welfare/selection/situation/getUserSelection", {projectId: row.projectId, userId: row.userId}),
this.$axios.post("/platform/welfare/selection/situation/getWelfareMobileByUserId", {userId: row.userId})
]).then((results) => {
if (version !== this.proxyRequestVersion) return
if (results.some((item) => item.code !== 0)) {
this.$toast.fail("代选信息加载失败")
return
}
const project = results[0].data || {}
const selections = results[1].data || []
const options = project.options || []
options.forEach((option) => this.$set(option, "selectNum", 0))
this.$set(this, "proxyProject", project)
this.$set(this, "hasSubmittedBefore", selections.length > 0)
this.$set(this.proxyForm, "welfareMobile", results[2].data || "")
if (selections.length > 0) {
this.$set(this.proxyForm, "receiveAddress", selections[0].receiveAddress || "")
this.$set(this.proxyForm, "userSign", selections[0].userSign || "")
}
if (project.isCheckBox === "radio" && selections.length > 0) {
this.$set(this, "selectedRadioId", selections[0].selectOptionId)
} else {
selections.forEach((selection) => {
const option = options.find((item) => item.id === selection.selectOptionId)
if (option) this.$set(option, "selectNum", selection.selectNum || 1)
})
}
}).catch(() => {
if (version === this.proxyRequestVersion) this.$toast.fail("代选信息加载失败")
}).finally(() => {
if (version === this.proxyRequestVersion) this.$set(this, "proxyLoading", false)
})
},
chooseRadio(id) {
this.$set(this, "selectedRadioId", id)
},
optionMax(option) {
if (option.maxNum != null) return Number(option.maxNum)
if (this.proxyProject.singleOptionSupportMultipleSelection) return Number(this.proxyProject.multiSelectNum) || 1
return 1
},
// 单项上限和项目总份数共同限制代选数量。
updateOptionNum(option, value) {
const count = Number(value) || 0
const others = (this.proxyProject.options || []).reduce((sum, item) => {
return sum + (item.id === option.id ? 0 : Number(item.selectNum || 0))
}, 0)
const maxTotal = Number(this.proxyProject.multiSelectNum) || 1
if (others + count > maxTotal) {
this.$toast("此项目最多选择" + maxTotal + "份")
this.$set(option, "selectNum", Math.max(0, maxTotal - others))
return
}
this.$set(option, "selectNum", count)
},
// 仅提交被代选人、已选选项及必要联系信息,不改变现有后端规则。
submitProxy() {
if (!this.hasSelection || this.formLoading) return
const mobile = (this.proxyForm.welfareMobile || "").trim()
if (!/^1[3456789]\d{9}$/.test(mobile)) {
this.$toast.fail("请输入正确的福利电话")
return
}
if (this.proxyProject.provideMode === 3 && !(this.proxyForm.receiveAddress || "").trim()) {
this.$toast.fail("请输入收货地址")
return
}
if (this.proxyProject.signMode === 2 && !this.proxyForm.userSign) {
this.$toast.fail("请完成签字确认")
return
}
const selections = this.selectedOptions.map((option) => ({
selectOptionId: option.id,
selectNum: this.proxyProject.isCheckBox === "radio" ? 1 : Number(option.selectNum),
receiveAddress: this.proxyForm.receiveAddress,
userSign: this.proxyForm.userSign
}))
this.$set(this, "formLoading", true)
this.$axios.post("/platform/welfare/selection/situation/confirmSelect", {
projectId: this.proxyRow.projectId,
userId: this.proxyRow.userId,
welfareMobile: mobile,
selections: JSON.stringify(selections)
}).then((res) => {
if (res.code !== 0) {
this.$toast.fail(res.msg || "代选失败")
return
}
this.$toast.success("代选成功")
h5PopupHistory.clearOwner(this.historyOwner)
this.doSearch()
}).catch(() => {
this.$toast.fail("代选失败,请重试")
}).finally(() => {
this.$set(this, "formLoading", false)
})
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,365 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
/* 固定视口使用 flex 分配剩余空间,首次进入和刷新均不依赖延时计算高度。 */
#app.welfare-statistics-page { height: 100%; height: 100dvh; min-height: 0; overflow: hidden; flex-direction: column; background: #f4f7fb; color: #182538; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
#app.welfare-statistics-page:not([v-cloak]) { display: flex; }
.welfare-statistics-page * { box-sizing: border-box; }
.welfare-statistics-page button { font-family: inherit; cursor: pointer; }
.welfare-statistics-page button:focus-visible { outline: 2px solid #0074d9; outline-offset: 2px; }
.welfare-statistics-page > .van-nav-bar, .ws-filters { flex-shrink: 0; background: #fff; }
.welfare-statistics-page .van-nav-bar__title { color: #182538; font-weight: 600; }
.welfare-statistics-page .van-nav-bar__text, .welfare-statistics-page .van-nav-bar .van-icon { color: #0074d9; }
.ws-filters { padding: 10px 12px 12px; border-bottom: 1px solid #e7edf5; }
.ws-filter-row { display: flex; gap: 8px; }
.ws-filter-row + .ws-filter-row { margin-top: 8px; }
.ws-filter { display: flex; align-items: center; justify-content: space-between; flex: 1; min-width: 0; height: 38px; padding: 0 10px; border: 1px solid #dde6f0; border-radius: 7px; background: #fff; color: #24364d; font-size: 14px; text-align: left; }
.ws-filter span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-filter .van-icon { flex-shrink: 0; margin-left: 8px; color: #607895; }
.ws-filter-year { flex: 0 0 94px; }
.ws-filter-project { background: #f0f7ff; border-color: #d9e9fa; }
.ws-filter:disabled { opacity: .55; cursor: default; }
/* 顶部之外仅此容器滚动,边界滚动不传递给页面外层。 */
.ws-content { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; -webkit-overflow-scrolling: touch; padding: 12px 12px calc(20px + env(safe-area-inset-bottom)); }
.ws-summary, .ws-union { padding: 16px 12px; background: #fff; border-radius: 10px; }
.ws-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); text-align: center; }
.ws-metric { min-width: 0; }
.ws-metric + .ws-metric { border-left: 1px solid #e8eef6; }
.ws-metric-label { display: block; margin-bottom: 5px; color: #718198; font-size: 12px; }
.ws-metric strong { display: block; font-size: 23px; line-height: 28px; font-variant-numeric: tabular-nums; }
.ws-blue { color: #0074d9; }
.ws-orange { color: #dc7600; }
.ws-summary .van-progress { margin-top: 18px; }
.ws-rate { margin-top: 8px; text-align: right; color: #718198; font-size: 12px; }
.ws-rate strong { margin-left: 6px; color: #0074d9; }
.ws-note { margin: 10px 0 0; color: #718198; font-size: 12px; line-height: 18px; }
.ws-section-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 18px 2px 10px; }
.ws-section-title h2 { margin: 0; font-size: 17px; }
.ws-section-title span { color: #718198; font-size: 12px; }
.ws-union + .ws-union { margin-top: 10px; }
.ws-union-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 13px; }
.ws-union-head h3 { margin: 0; font-size: 16px; line-height: 23px; overflow-wrap: anywhere; }
.ws-union-head span { flex-shrink: 0; padding-top: 2px; color: #0074d9; font-size: 14px; }
.ws-count-button { width: 100%; min-height: 48px; padding: 0; border: 0; background: transparent; }
.ws-count-button strong { display: inline; font-size: 20px; }
.ws-count-button .van-icon { font-size: 13px; vertical-align: 1px; }
.ws-union .ws-metric > strong { font-size: 20px; }
.ws-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px 0; margin-top: 13px; padding: 10px 0; background: #f4f8fc; border-radius: 7px; text-align: center; }
.ws-option { padding: 0 6px; min-width: 0; font-size: 13px; line-height: 19px; overflow-wrap: anywhere; }
.ws-option:not(:nth-child(3n+1)) { border-left: 1px solid #dde7f3; }
.ws-option span { display: block; color: #718198; }
.ws-option strong { font-weight: 500; }
.ws-loading { padding: 60px 0; text-align: center; }
.ws-error { padding: 35px 15px; text-align: center; color: #718198; font-size: 14px; }
.ws-error .van-button { margin-top: 12px; }
/* 弹框也使用 flex;标题、筛选和查询固定,名单与选项列表分别独立滚动。 */
.welfare-statistics-page .ws-sheet { display: flex; flex-direction: column; overflow: hidden; padding-bottom: env(safe-area-inset-bottom); background: #fff; }
.ws-filter-sheet { max-height: 75%; max-height: 75dvh; }
.ws-sheet-toolbar { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; min-height: 54px; padding: 0 10px; border-bottom: 1px solid #edf1f7; }
.ws-sheet-toolbar strong { font-size: 16px; }
.ws-sheet-toolbar button { min-height: 44px; padding: 0 10px; border: 0; background: transparent; color: #0074d9; font-size: 14px; }
.ws-sheet-toolbar button:first-child { color: #718198; }
.ws-choices { min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; padding: 8px 16px 16px; }
.ws-choice { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; min-height: 48px; padding: 12px; border: 0; border-radius: 7px; background: #fff; color: #24364d; text-align: left; font-size: 14px; line-height: 21px; }
.ws-choice.is-selected { background: #edf6ff; color: #0074d9; }
.ws-choice .van-icon { flex-shrink: 0; font-size: 18px; }
.ws-detail-sheet { height: 82%; height: 82dvh; }
.ws-detail-head { flex-shrink: 0; display: flex; justify-content: space-between; gap: 12px; padding: 20px 16px 14px; }
.ws-detail-head h2 { margin: 0 0 5px; font-size: 20px; line-height: 27px; overflow-wrap: anywhere; }
.ws-detail-head p { margin: 0; color: #718198; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
.ws-close { flex-shrink: 0; align-self: flex-start; width: 36px; height: 36px; margin: -4px -8px 0 0; border: 0; background: transparent; color: #718198; font-size: 22px; }
.ws-tabs { flex-shrink: 0; display: flex; border-bottom: 1px solid #e7edf5; }
.ws-tab { position: relative; flex: 1; min-height: 44px; padding: 8px 4px; border: 0; background: #fff; color: #718198; font-size: 15px; }
.ws-tab.is-selected { color: #0074d9; font-weight: 600; }
.ws-tab.ws-tab-unselected.is-selected { color: #dc7600; }
.ws-tab.is-selected:after { position: absolute; right: 25%; bottom: 0; left: 25%; height: 3px; border-radius: 3px; background: currentColor; content: ""; }
.ws-search { display: flex; align-items: center; gap: 8px; flex-shrink: 0; padding: 12px 16px 6px; }
.ws-search .van-search { flex: 1; min-width: 0; padding: 0; }
.ws-search .van-search__content { border: 1px solid #e1e9f3; border-radius: 7px; background: #f4f7fb; }
.ws-search .van-button { height: 36px; border-radius: 6px; }
.ws-detail-count { flex-shrink: 0; padding: 5px 16px 10px; color: #718198; font-size: 12px; text-align: right; }
.ws-person-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; -webkit-overflow-scrolling: touch; padding: 0 16px 16px; }
.ws-person { margin-bottom: 8px; padding: 12px; border: 1px solid #e7edf5; border-radius: 8px; }
.ws-person-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.ws-person-head > div { min-width: 0; overflow-wrap: anywhere; }
.ws-person-name { font-size: 16px; font-weight: 600; line-height: 24px; }
.ws-person-number { display: inline-block; margin-left: 6px; color: #718198; font-size: 13px; }
.ws-person-status { flex-shrink: 0; padding-top: 3px; font-size: 12px; }
.ws-person p { margin: 5px 0 0; color: #718198; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
.ws-person .ws-person-option { color: #395570; }
</style>
<div id="app" class="welfare-statistics-page" v-cloak>
<van-nav-bar title="福利统计" left-text="返回" left-arrow @click-left="historyBack"></van-nav-bar>
<div class="ws-filters">
<div class="ws-filter-row">
<button class="ws-filter ws-filter-year" :disabled="bootLoading || !!bootError" @click="openFilter('year')" aria-label="选择年份"><span>{{ filters.year }}年</span><van-icon name="arrow-down"></van-icon></button>
<button class="ws-filter ws-filter-project" :disabled="bootLoading || !!bootError" @click="openFilter('projectId')" aria-label="选择福利项目"><span>{{ projectName || '选择福利项目' }}</span><van-icon name="arrow-down"></van-icon></button>
</div>
<div class="ws-filter-row">
<button class="ws-filter" :disabled="bootLoading || !!bootError || branchOnly" @click="openFilter('unionId')" aria-label="选择工会"><span>{{ unionName }}</span><van-icon name="arrow-down"></van-icon></button>
<button class="ws-filter" :disabled="bootLoading || !!bootError" @click="openFilter('method')" aria-label="选择统计方式"><span>{{ methodName }}</span><van-icon name="arrow-down"></van-icon></button>
</div>
</div>
<main class="ws-content" ref="contentScroll" aria-label="福利统计内容">
<div v-if="bootLoading || tableLoading" class="ws-loading"><van-loading size="24px" vertical>正在加载统计</van-loading></div>
<div v-else-if="bootError || tableError" class="ws-error"><div>{{ bootError || tableError }}</div><van-button size="small" plain type="info" @click="retryStatistics">重新加载</van-button></div>
<van-empty v-else-if="!filters.projectId" description="该年份暂无福利项目"></van-empty>
<template v-else>
<section class="ws-summary" aria-label="人数汇总">
<div class="ws-metrics">
<div class="ws-metric"><span class="ws-metric-label">福利人数</span><strong>{{ totals.teacherSum }}</strong></div>
<div class="ws-metric"><span class="ws-metric-label">已选择</span><strong class="ws-blue">{{ totals.selectedNum }}</strong></div>
<div class="ws-metric"><span class="ws-metric-label">未选择</span><strong class="ws-orange">{{ totals.unSelectedNum }}</strong></div>
</div>
<van-progress :percentage="percentage(totals)" :show-pivot="false" stroke-width="8" color="#0074d9" track-color="#e7f1fa"></van-progress>
<div class="ws-rate">已选比例<strong>{{ percentage(totals) }}%</strong></div>
<p v-if="filters.method !== ''" class="ws-note">已选按{{ methodName }}统计,未选为尚未选择人员。</p>
</section>
<div class="ws-section-title"><h2>分工会统计</h2><span>点击已选、未选人数查看名单</span></div>
<van-empty v-if="!tableData.length" description="暂无工会统计"></van-empty>
<article v-for="row in tableData" :key="row.id" class="ws-union">
<div class="ws-union-head"><h3>{{ row.name }}</h3><span>{{ percentage(row) }}%</span></div>
<div class="ws-metrics">
<div class="ws-metric"><span class="ws-metric-label">总人数</span><strong>{{ row.teacherSum || 0 }}</strong></div>
<div class="ws-metric"><button class="ws-count-button ws-blue" @click="openDetail(row, 'selected')" :aria-label="row.name + '已选择人员'"><span class="ws-metric-label">已选</span><strong>{{ row.selectedNum || 0 }}</strong><van-icon name="arrow"></van-icon></button></div>
<div class="ws-metric"><button class="ws-count-button ws-orange" @click="openDetail(row, 'unselected')" :aria-label="row.name + '未选择人员'"><span class="ws-metric-label">未选</span><strong>{{ row.unSelectedNum || 0 }}</strong><van-icon name="arrow"></van-icon></button></div>
</div>
<div v-if="optionColumns.length" class="ws-options">
<div v-for="column in optionColumns" :key="column.prop" class="ws-option"><span>{{ column.label }}</span><strong>{{ row[column.prop] || 0 }}人</strong></div>
</div>
</article>
</template>
</main>
<van-popup :value="showFilter" position="bottom" round class="ws-sheet ws-filter-sheet" :close-on-click-overlay="false" @click-overlay="closeFilter">
<div class="ws-sheet-toolbar"><button @click="closeFilter">取消</button><strong>{{ filterTitle }}</strong><button @click="confirmFilter">确认</button></div>
<div class="ws-choices" role="radiogroup" :aria-label="filterTitle">
<button v-for="option in filterOptions" :key="String(option.value)" class="ws-choice" :class="{'is-selected': draftValue === option.value}" role="radio" :aria-checked="draftValue === option.value" @click="chooseDraft(option.value)"><span>{{ option.text }}</span><van-icon :name="draftValue === option.value ? 'checked' : 'circle'"></van-icon></button>
<van-empty v-if="!filterOptions.length" description="暂无可选项目"></van-empty>
</div>
</van-popup>
<van-popup :value="showDetail" position="bottom" round class="ws-sheet ws-detail-sheet" :close-on-click-overlay="false" @click-overlay="closeDetail" @opened="checkDetailList">
<div class="ws-detail-head"><div><h2>{{ detailUnion.name }}</h2><p>{{ projectName }}</p></div><button class="ws-close" aria-label="关闭人员名单" @click="closeDetail"><van-icon name="cross"></van-icon></button></div>
<div class="ws-tabs" role="tablist" aria-label="人员选择状态">
<button class="ws-tab" :class="{'is-selected': detailTab === 'selected'}" role="tab" :aria-selected="detailTab === 'selected'" @click="changeDetailTab('selected')">已选择 {{ detailUnion.selectedNum || 0 }}</button>
<button class="ws-tab ws-tab-unselected" :class="{'is-selected': detailTab === 'unselected'}" role="tab" :aria-selected="detailTab === 'unselected'" @click="changeDetailTab('unselected')">未选择 {{ detailUnion.unSelectedNum || 0 }}</button>
</div>
<div class="ws-search"><van-search :value="searchInput" placeholder="工号或姓名" @input="setSearchInput" @search="searchDetail" @clear="clearSearch"></van-search><van-button type="info" @click="searchDetail">查询</van-button></div>
<div class="ws-detail-count">{{ detailLoading && !detailRows.length ? '正在查询…' : '共 ' + detailTotal + ' 人' }}</div>
<div class="ws-person-scroll" ref="personScroll">
<van-list ref="personList" :loading="detailLoading" :finished="detailFinished" :immediate-check="false" :error="!!detailError" finished-text="没有更多了" @load="loadDetail">
<article v-for="(person, index) in detailRows" :key="String(person.loginName) + '-' + index" class="ws-person">
<div class="ws-person-head"><div><span class="ws-person-name">{{ person.userName || '未填写姓名' }}</span><span class="ws-person-number">{{ person.loginName }}</span></div><span class="ws-person-status" :class="detailTab === 'selected' ? 'ws-blue' : 'ws-orange'">● {{ detailTab === 'selected' ? '已选择' : '未选择' }}</span></div>
<p>{{ person.welfareUnitName || '未填写单位' }}</p>
<p v-if="detailTab === 'selected'" class="ws-person-option">{{ person.selectOptionName || '—' }}<span v-if="person.selectByAdminName"> · {{ person.selectByAdminName }}</span></p>
</article>
<template #error><div @click.stop="loadDetail">{{ detailError }},点击重试</div></template>
</van-list>
<van-empty v-if="!detailLoading && !detailError && !detailRows.length" description="暂无符合条件的人员"></van-empty>
</div>
</van-popup>
<slot></slot>
</div>
<script>
new Vue({
el: '#app',
store: store,
data() {
return {
filters: {year: new Date().getFullYear(), projectId: '', unionId: '', method: ''},
projects: [], unions: [], branchOnly: false,
bootLoading: false, bootError: '', bootVersion: 0,
tableData: [], optionColumns: [], tableLoading: false, tableError: '', tableVersion: 0,
popupOwner: 'h5-welfare-statistics', showFilter: false, filterType: '', draftValue: '',
showDetail: false, detailUnion: {}, detailTab: 'selected', searchInput: '', searchKeyword: '',
detailRows: [], detailTotal: 0, detailPage: 1, detailLoading: false, detailFinished: false, detailError: '', detailVersion: 0
}
},
computed: {
yearProjects() { return this.projects.filter((item) => Number(item.year) === this.filters.year) },
projectName() { return (this.projects.find((item) => item.id === this.filters.projectId) || {}).name || '' },
unionName() { return (this.unions.find((item) => item.id === this.filters.unionId) || {}).name || '全部工会' },
methodName() { return this.filters.method === '' ? '全部方式' : (this.filters.method ? '管理员代选' : '个人选择') },
filterTitle() { return {year: '选择年份', projectId: '选择福利项目', unionId: '选择工会', method: '选择方式'}[this.filterType] || '' },
filterOptions() {
if (this.filterType === 'year') {
const years = Array.from(new Set([new Date().getFullYear()].concat(this.projects.map((item) => Number(item.year)).filter((year) => year > 0))))
return years.sort((a, b) => b - a).map((year) => ({value: year, text: year + '年'}))
}
if (this.filterType === 'projectId') return this.yearProjects.map((item) => ({value: item.id, text: item.name}))
if (this.filterType === 'unionId') return (this.branchOnly ? [] : [{value: '', text: '全部工会'}]).concat(this.unions.map((item) => ({value: item.id, text: item.name})))
return [{value: '', text: '全部方式'}, {value: false, text: '个人选择'}, {value: true, text: '管理员代选'}]
},
totals() {
return this.tableData.reduce((total, row) => {
Object.keys(total).forEach((key) => { total[key] += Number(row[key]) || 0 })
return total
}, {teacherSum: 0, selectedNum: 0, unSelectedNum: 0})
}
},
created() { this.init() },
beforeDestroy() {
// 失效所有未完成请求,并移除本页面的弹框历史回调,防止离页后写回旧组件。
this.$set(this, 'bootVersion', this.bootVersion + 1)
this.$set(this, 'tableVersion', this.tableVersion + 1)
this.$set(this, 'detailVersion', this.detailVersion + 1)
h5PopupHistory.unregisterOwner(this.popupOwner)
},
methods: {
percentage(row) {
const total = Number(row.teacherSum) || 0
return total > 0 ? Math.min(100, Math.max(0, Math.round((Number(row.selectedNum) || 0) / total * 1000) / 10)) : 0
},
// 项目接口返回 id/name/year 数组;工会接口沿用 PC 分工会主席的默认范围。
init() {
const version = this.bootVersion + 1
this.$set(this, 'bootVersion', version)
this.$set(this, 'bootLoading', true)
this.$set(this, 'bootError', '')
const branchOnly = !this.$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN', 'SCHOOL_UNION_WELFARE_ADMIN']) && this.$auth.hasRoleOr(['BRANCH_UNION_CHAIRMAN'])
const userUnion = this.$store.state.user.union || {}
this.$set(this, 'branchOnly', branchOnly)
// 无所属工会时不能退化为全工会查询,显示可重试提示。
if (branchOnly && !userUnion.id) {
this.$set(this, 'bootError', '当前账号未配置所属工会,请联系管理员')
this.$set(this, 'bootLoading', false)
return
}
Promise.all([
this.$axios.post('/platform/h5/welfare/statistics/projects', {}),
this.$axios.post('/open/common/unionOptions', branchOnly ? {unionId: userUnion.id} : {})
]).then((results) => {
if (version !== this.bootVersion) return
if (results.some((res) => res.code !== 0)) throw new Error('筛选条件加载失败,请重试')
this.$set(this, 'projects', results[0].data || [])
this.$set(this, 'unions', results[1].data || [])
this.$set(this.filters, 'unionId', branchOnly ? userUnion.id : '')
this.$set(this.filters, 'projectId', this.yearProjects.length ? this.yearProjects[0].id : '')
this.loadStatistics()
}).catch((error) => {
if (version === this.bootVersion) this.$set(this, 'bootError', error.message || '筛选条件加载失败,请重试')
}).finally(() => {
if (version === this.bootVersion) this.$set(this, 'bootLoading', false)
})
},
retryStatistics() { if (this.bootError) this.init(); else this.loadStatistics() },
// 筛选使用独立草稿;取消、遮罩和返回键只关闭弹框,不改变查询条件。
openFilter(type) {
if (this.showFilter || this.showDetail || this.bootLoading || this.bootError) return
this.$set(this, 'filterType', type)
this.$set(this, 'draftValue', this.filters[type])
this.$set(this, 'showFilter', true)
h5PopupHistory.open(this.popupOwner + '-filter', this.popupOwner, () => { this.$set(this, 'showFilter', false) })
},
chooseDraft(value) { this.$set(this, 'draftValue', value) },
closeFilter() { h5PopupHistory.close(this.popupOwner + '-filter') },
confirmFilter() {
if (!this.filterOptions.some((item) => item.value === this.draftValue)) { this.closeFilter(); return }
if (!h5PopupHistory.close(this.popupOwner + '-filter')) return
if (this.filters[this.filterType] === this.draftValue) return
this.$set(this.filters, this.filterType, this.draftValue)
// 年份变化后只选该年份的第一个项目;普通项目切换绝不重新覆盖为默认值。
if (this.filterType === 'year') this.$set(this.filters, 'projectId', this.yearProjects.length ? this.yearProjects[0].id : '')
this.loadStatistics()
},
// 参数 projectId 必填、unionId 可选;method 空时省略,false 必须保留为个人选择。
statisticsParams() {
const params = {projectId: this.filters.projectId, unionId: this.filters.unionId}
if (this.filters.method !== '') params.isSelectByAdmin = this.filters.method
return params
},
loadStatistics() {
const version = this.tableVersion + 1
this.$set(this, 'tableVersion', version)
this.$set(this, 'tableData', [])
this.$set(this, 'optionColumns', [])
this.$set(this, 'tableError', '')
if (this.$refs.contentScroll) this.$refs.contentScroll.scrollTop = 0
if (!this.filters.projectId) { this.$set(this, 'tableLoading', false); return }
this.$set(this, 'tableLoading', true)
this.$axios.post('/platform/h5/welfare/statistics/pageData', this.statisticsParams()).then((res) => {
if (version !== this.tableVersion) return
if (res.code !== 0) throw new Error(res.msg || '统计加载失败')
this.$set(this, 'tableData', res.data.tableList || [])
const fixed = ['name', 'teacherSum', 'selectedNum', 'unSelectedNum']
this.$set(this, 'optionColumns', (res.data.tableColumn || []).filter((column) => fixed.indexOf(column.prop) === -1))
}).catch((error) => {
if (version === this.tableVersion) this.$set(this, 'tableError', error.message || '统计加载失败,请重试')
}).finally(() => {
if (version === this.tableVersion) this.$set(this, 'tableLoading', false)
})
},
openDetail(row, tab) {
if (this.showDetail || this.showFilter) return
this.$set(this, 'detailUnion', Object.assign({}, row))
this.$set(this, 'detailTab', tab)
this.$set(this, 'searchInput', '')
this.$set(this, 'searchKeyword', '')
this.$set(this, 'showDetail', true)
h5PopupHistory.open(this.popupOwner + '-detail', this.popupOwner, () => {
this.$set(this, 'showDetail', false)
this.$set(this, 'detailVersion', this.detailVersion + 1)
this.$set(this, 'detailLoading', false)
})
this.resetDetail()
},
closeDetail() { h5PopupHistory.close(this.popupOwner + '-detail') },
changeDetailTab(tab) {
if (tab === this.detailTab) return
this.$set(this, 'detailTab', tab)
this.resetDetail()
},
setSearchInput(value) { this.$set(this, 'searchInput', value) },
searchDetail() {
this.$set(this, 'searchKeyword', this.searchInput.trim())
this.resetDetail()
},
clearSearch() { this.$set(this, 'searchInput', ''); this.searchDetail() },
resetDetail() {
// 切换工会、状态或查询词时使旧请求失效,并从第一页重新读取。
this.$set(this, 'detailVersion', this.detailVersion + 1)
this.$set(this, 'detailRows', [])
this.$set(this, 'detailTotal', 0)
this.$set(this, 'detailPage', 1)
this.$set(this, 'detailFinished', false)
this.$set(this, 'detailLoading', false)
this.$set(this, 'detailError', '')
if (this.$refs.personScroll) this.$refs.personScroll.scrollTop = 0
this.loadDetail()
},
checkDetailList() { if (this.showDetail && this.$refs.personList) this.$refs.personList.check() },
// 两个接口均返回 Pagination 的 list/totalCount;未选接口不传选择方式。
loadDetail() {
if (!this.showDetail || this.detailLoading || this.detailFinished) return
const version = this.detailVersion
const params = {projectId: this.filters.projectId, unionId: this.detailUnion.id, pageNumber: this.detailPage, pageSize: 50, searchKeyword: this.searchKeyword}
const selected = this.detailTab === 'selected'
if (selected && this.filters.method !== '') params.isSelectByAdmin = this.filters.method
this.$set(this, 'detailLoading', true)
this.$set(this, 'detailError', '')
this.$axios.post('/platform/h5/welfare/statistics/' + (selected ? 'selectedUnionUserPageData' : 'unSelectedUnionUserPageData'), params).then((res) => {
if (version !== this.detailVersion) return
if (res.code !== 0) throw new Error(res.msg || '人员加载失败')
const rows = res.data.list || []
this.$set(this, 'detailRows', this.detailRows.concat(rows))
this.$set(this, 'detailTotal', Number(res.data.totalCount) || 0)
this.$set(this, 'detailFinished', rows.length === 0 || this.detailRows.length >= this.detailTotal)
this.$set(this, 'detailPage', this.detailPage + 1)
}).catch((error) => {
if (version === this.detailVersion) this.$set(this, 'detailError', error.message || '人员加载失败')
}).finally(() => {
if (version !== this.detailVersion) return
this.$set(this, 'detailLoading', false)
// 首屏不足一屏时继续检查;失败等待手动重试,避免自动请求循环。
if (!this.detailError) this.$nextTick(() => { this.checkDetailList() })
})
}
}
})
</script>
<!--#
}
#-->