diff --git a/src/main/java/io/v/nutz/base/utils/MsgApi.java b/src/main/java/io/v/nutz/base/utils/MsgApi.java index f350bb8..7316928 100644 --- a/src/main/java/io/v/nutz/base/utils/MsgApi.java +++ b/src/main/java/io/v/nutz/base/utils/MsgApi.java @@ -89,7 +89,7 @@ public class MsgApi { */ public void sendWxMsg(String title, String url, String msgtype, String description, List loginNames, String contentUrl) { - if ("否".equals(Globals.MyConfig.getString("SengMsg"))) { + if ("false".equals(Strings.trim(Globals.MyConfig.getString("SengMsg")))) { log.info("发送失败,消息暂未开启!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); return; } @@ -101,7 +101,7 @@ public class MsgApi { // if (!"news".equals(msgtype)) { // title = "智慧工会"; // } - if (Globals.isEnv(Env.dev)){ + if (Globals.isEnv(Env.dev)) { log.info("发送失败,本地环境禁止发送!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); return; @@ -146,7 +146,7 @@ public class MsgApi { m.put("title", title); m.put("description", description); //m.put("url", ""); - m.put("picurl", "https://zhghydd.cpu.edu.cn/file_server/fileStreamPreview?id="+contentUrl); + m.put("picurl", "https://zhghydd.cpu.edu.cn/file_server/fileStreamPreview?id=" + contentUrl); list.add(m); map.put("news", Map.of("articles", list)); } @@ -175,10 +175,20 @@ public class MsgApi { public void sendMsg(String content, List toLoginNameList) { try { + if ("false".equals(Strings.trim(Globals.MyConfig.getString("SengMsg")))) { + log.info("发送失败,消息暂未开启!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + return; + } + if (Globals.isEnv(Env.dev)) { + log.info("发送失败,本地环境禁止发送!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + return; + } + List mapList = new ArrayList<>(); - toLoginNameList.forEach(v->{ + toLoginNameList.forEach(v -> { NutMap nutMap = new NutMap(); - nutMap.setv("touser",v); + nutMap.setv("touser", v); mapList.add(nutMap); }); @@ -209,7 +219,7 @@ public class MsgApi { body.put("message", Json.toJson(a)); log.info(Json.toJson(body)); String postUrl = MSG_API + "?appid=" + APPID + "&tokenid=" + token + "&schoolcode=4"; - String post = Http.post(postUrl, body,10000); + String post = Http.post(postUrl, body, 10000); JSONObject parseObject = JSON.parseObject(post); diff --git a/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/ClubUserCountController.java b/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/ClubUserCountController.java index 3293415..8b2873e 100644 --- a/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/ClubUserCountController.java +++ b/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/ClubUserCountController.java @@ -204,6 +204,23 @@ public class ClubUserCountController { return list; } + /** + * 无业务请求参数,response 为文件下载响应;返回 XLSX 文件流而非 JSON。 + * 文件首列为社团名称,包含当前用户权限范围内的全部有效成员,不受页面筛选影响。 + */ + @At + @Ok("void") + @RequiresPermissions("sys.club.count") + public void exportAllUserExcel(HttpServletResponse response) throws IOException { + try (Workbook workbook = sysClubService.exportAllUserExcel()) { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment;filename=" + + URLEncoder.encode("全部人员明细.xlsx", "UTF-8")); + workbook.write(response.getOutputStream()); + } + } + @At @Ok("void") @RequiresPermissions("sys.club.count") diff --git a/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/applyIn/ClubApplyInController.java b/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/applyIn/ClubApplyInController.java index c14eb29..5949b4a 100644 --- a/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/applyIn/ClubApplyInController.java +++ b/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/applyIn/ClubApplyInController.java @@ -1,6 +1,7 @@ package io.v.nutz.sys.controllers.platform.sys.club.applyIn; import cn.hutool.core.util.StrUtil; +import cn.wizzer.framework.base.Result; import io.v.nutz.base.annontation.ViReturn; import io.v.nutz.base.service.BaseService; import io.v.nutz.base.utils.DateUtil; @@ -33,6 +34,9 @@ import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; +import javax.servlet.http.HttpServletRequest; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; @@ -53,10 +57,32 @@ public class ClubApplyInController { public void index() { } + /** + * clubId 为二维码携带的社团主键,普通列表访问可不传。 + * 返回手机页面,并将 URL 编码后的 scanClubId 写入页面,供挂载后打开指定社团使用; + * 编码避免请求参数中的引号、标签等内容进入页面脚本。 + */ @At("/mobile") @Ok("beetl:/mobile/club/inClub.html") @RequiresAuthentication - public void mobile() { + public void mobile(@Param("clubId") String clubId, HttpServletRequest request) { + request.setAttribute("scanClubId", + URLEncoder.encode(StrUtil.blankToDefault(clubId, ""), StandardCharsets.UTF_8)); + } + + /** + * clubId 必须传社团主键;当前用户从登录会话获取。 + * 返回统一 JSON 结果,成功 data 为社团详情及当前用户 status,无效社团返回错误提示。 + */ + @At + @RequiresAuthentication + public Object scanDetail(@Param("clubId") String clubId) { + if (StrUtil.isBlank(clubId)) { + return Result.error("请提供社团编号"); + } + Object detail = sysClubService.scanDetail(clubId, ShiroUtil.getPlatformUid()); + return detail == null ? Result.error("社团不存在或暂不可申请") + : Result.success("获取成功", detail); } @Inject @@ -122,11 +148,29 @@ public class ClubApplyInController { } + /** + * clubid 为申请社团主键;birthday、mobile 为本次申请的出生日期和联系电话; + * medicalHistory 必须明确选择是或否,otherThing 为可选说明(最多 100 字); + * emergencyContact、emergencyMobile 为必填紧急联系人及电话。 + * 申请人由登录会话确定;返回值由 ViReturn 包装为 code、msg、data 的 JSON 结果。 + */ @At @ViReturn @RequiresPermissions("sys.club.sq") @Aop(TransAop.READ_COMMITTED) public Object submit(Sys_club_user sys_club_user) { + // 在清理旧申请前校验必填项,false 表示无特殊病史,是有效选择。 + if (StrUtil.isBlank(sys_club_user.getClubid()) || sys_club_user.getBirthday() == null + || StrUtil.isBlank(sys_club_user.getMobile()) || sys_club_user.getMedicalHistory() == null + || StrUtil.isBlank(sys_club_user.getEmergencyContact()) + || StrUtil.isBlank(sys_club_user.getEmergencyMobile())) { + throw new IllegalArgumentException("请完整填写出生年月、联系电话、特殊病史和紧急联系人信息"); + } + if (sys_club_user.getMobile().length() > 20 || sys_club_user.getEmergencyContact().length() > 50 + || sys_club_user.getEmergencyMobile().length() > 20 + || (sys_club_user.getOtherThing() != null && sys_club_user.getOtherThing().length() > 100)) { + throw new IllegalArgumentException("联系电话最多20字,紧急联系人最多50字,特别说明最多100字"); + } sysClubUserService.clear(Cnd.where("clubid", "=", sys_club_user.getClubid()).and("userid", "=", ShiroUtil.getPrincipalProperty("id"))); String dateTime = DateUtil.getDateTime(); @@ -269,7 +313,7 @@ public class ClubApplyInController { c.*, u.username, u.sex, - u.birthday + u.loginname FROM sys_club_user c LEFT JOIN USER u ON c.userid = u.id diff --git a/src/main/java/io/v/nutz/sys/models/Sys_club_user.java b/src/main/java/io/v/nutz/sys/models/Sys_club_user.java index 86b22d0..c891f84 100644 --- a/src/main/java/io/v/nutz/sys/models/Sys_club_user.java +++ b/src/main/java/io/v/nutz/sys/models/Sys_club_user.java @@ -136,6 +136,18 @@ public class Sys_club_user extends BaseModel { @ColDefine(type = ColType.VARCHAR, width = 20) private String mobile; + /** 本次入会申请的紧急联系人,不回写用户档案。 */ + @Column + @Comment("紧急联系人") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String emergencyContact; + + /** 紧急联系号码按文本保存,允许座机区号及国际区号。 */ + @Column + @Comment("紧急联系电话") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String emergencyMobile; + @Column @Comment("邮箱") @ColDefine(type = ColType.VARCHAR, width = 100) diff --git a/src/main/java/io/v/nutz/sys/services/SysClubService.java b/src/main/java/io/v/nutz/sys/services/SysClubService.java index 266c492..4714902 100644 --- a/src/main/java/io/v/nutz/sys/services/SysClubService.java +++ b/src/main/java/io/v/nutz/sys/services/SysClubService.java @@ -5,9 +5,23 @@ import cn.wizzer.framework.page.Pagination; import io.v.nutz.base.query.PageForm; import io.v.nutz.sys.models.Sys_club; import org.nutz.dao.Cnd; +import org.nutz.dao.entity.Record; +import org.apache.poi.ss.usermodel.Workbook; public interface SysClubService extends BaseService { + /** + * 按社团主键和当前登录用户主键查询扫码详情;返回社团字段、联系人、人数及 status + * (1/3 待审核、5 已加入、2/4 被拒绝、null 未申请),无有效社团时返回 null。 + */ + Record scanDetail(String clubId, String userId); + Pagination pageData(Cnd cnd, PageForm page, Integer year, String name); + + /** + * 无筛选及分页参数,按当前登录用户的数据权限导出全部正常且审核通过的社团成员。 + * 返回 XSSF 工作簿,首列为社团名称,其余列为人员明细;调用方负责关闭工作簿。 + */ + Workbook exportAllUserExcel(); } diff --git a/src/main/java/io/v/nutz/sys/services/impl/SysClubServiceImpl.java b/src/main/java/io/v/nutz/sys/services/impl/SysClubServiceImpl.java index c458422..9acdba4 100644 --- a/src/main/java/io/v/nutz/sys/services/impl/SysClubServiceImpl.java +++ b/src/main/java/io/v/nutz/sys/services/impl/SysClubServiceImpl.java @@ -1,24 +1,133 @@ package io.v.nutz.sys.services.impl; +import cn.afterturn.easypoi.excel.ExcelExportUtil; +import cn.afterturn.easypoi.excel.entity.ExportParams; +import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; +import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; import cn.wizzer.framework.base.service.BaseServiceImpl; import cn.wizzer.framework.page.Pagination; import io.v.nutz.base.query.PageForm; +import io.v.nutz.base.service.BaseService; import io.v.nutz.sys.models.Sys_club; import io.v.nutz.sys.services.SysClubService; +import io.v.nutz.base.utils.Roles; +import io.v.nutz.sys.constant.club.ClubRegistAuditState; +import io.v.nutz.sys.models.Sys_user_role; +import io.v.nutz.web.commons.utils.ShiroUtil; +import org.apache.poi.ss.usermodel.Workbook; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; +import org.nutz.dao.entity.Record; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.lang.util.NutMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; @IocBean(args = {"refer:dao"}) public class SysClubServiceImpl extends BaseServiceImpl implements SysClubService { + @Inject + private BaseService baseService; + public SysClubServiceImpl(Dao dao) { super(dao); } + /** + * 合并导出所有可见社团的有效成员,不接收页面筛选参数;一条社团成员关系对应一行。 + * 返回首列为社团名称的 XSSF 工作簿,同一人员加入多个社团时分别保留。 + */ + @Override + public Workbook exportAllUserExcel() { + Sql sql = Sqls.create(""" + SELECT + club.name AS clubName, + u.username, + u.loginname, + u.sex, + u.mobile, + u.unitname, + u.userState AS userState, + IF(c.giveMoney = true, '拨付', '不拨付') AS giveMoney, + IF(c.isjf = true, '已缴费', '未缴费') AS isjf, + (CASE c.sf WHEN 1 THEN '会长' WHEN 2 THEN '成员' + WHEN 3 THEN '副会长' WHEN 4 THEN '秘书长' WHEN 5 THEN '副秘书长' END) AS sf + FROM sys_club_user c + INNER JOIN sys_club club ON club.id = c.clubid + LEFT JOIN `user` u ON u.id = c.userid + $condition + ORDER BY club.code ASC, club.id ASC, FIELD(c.sf, 1, 3, 4, 5, 2), u.loginname ASC, c.id ASC + """); + // 社团范围及成员有效性与统计列表、人员明细保持一致。 + Cnd cnd = Cnd.where("club.isjs", "=", false) + .and("club.state", "=", ClubRegistAuditState.SCHOOL_PASS) + .and("c.isNormal", "=", true) + .and("c.status", "=", 5); + List members = new ArrayList<>(); + boolean canExport = true; + if (!ShiroUtil.hasAnyRoles("sysadmin, A06, xxhgly")) { + List userRoles = dao().query(Sys_user_role.class, + Cnd.where("userid", "=", ShiroUtil.getPlatformUid()).and("roleid", "=", Roles.club01)); + List clubIds = userRoles.stream().map(Sys_user_role::getStid) + .filter(id -> id != null && !id.trim().isEmpty()).collect(Collectors.toList()); + // 未分配社团时仅导出表头,防止空 IN 条件导致数据权限失效。 + canExport = !clubIds.isEmpty(); + if (canExport) { + cnd.and("club.id", "in", clubIds); + } + } + if (canExport) { + sql.setCondition(cnd); + members = baseService.listMap(sql); + } + + List columns = new ArrayList<>(); + columns.add(new ExcelExportEntity("社团名称", "clubName", 30)); + columns.add(new ExcelExportEntity("姓名", "username", 20)); + columns.add(new ExcelExportEntity("工号", "loginname", 20)); + columns.add(new ExcelExportEntity("性别", "sex", 20)); + columns.add(new ExcelExportEntity("联系方式", "mobile", 20)); + columns.add(new ExcelExportEntity("所在单位", "unitname", 30)); + columns.add(new ExcelExportEntity("在职状态", "userState", 20)); + columns.add(new ExcelExportEntity("是否拨付", "giveMoney", 20)); + columns.add(new ExcelExportEntity("是否缴费", "isjf", 20)); + columns.add(new ExcelExportEntity("职务", "sf", 20)); + ExportParams exportParams = new ExportParams(); + exportParams.setType(ExcelType.XSSF); + exportParams.setSheetName("全部人员明细"); + return ExcelExportUtil.exportExcel(exportParams, columns, members); + } + + /** + * 扫码仅展示已通过审批且未解散的社团;申请状态取当前用户的有效成员记录。 + * clubId 为社团主键,userId 为登录用户主键,返回详情记录,无匹配记录返回 null。 + */ + @Override + public Record scanDetail(String clubId, String userId) { + Sql sql = Sqls.create(""" + SELECT club.*, fzr.username fzrname, unit.name fzr_unit_name, + (SELECT COUNT(1) FROM sys_club_user cu + WHERE cu.clubid = club.id AND cu.status = 5 AND cu.isNormal = true) cynum, + (SELECT cu.status FROM sys_club_user cu + WHERE cu.clubid = club.id AND cu.userid = @userId AND cu.isNormal = true + ORDER BY cu.join_time DESC LIMIT 1) status + FROM sys_club club + LEFT JOIN sys_user fzr ON fzr.id = club.concatPerson + LEFT JOIN sys_unit unit ON unit.id = fzr.unitid + WHERE club.id = @clubId AND club.isjs = false AND club.state = @state + """); + sql.setParam("clubId", clubId).setParam("userId", userId) + .setParam("state", ClubRegistAuditState.SCHOOL_PASS); + List records = list(sql); + return records.isEmpty() ? null : records.get(0); + } + @Override public Pagination pageData(Cnd cnd, PageForm page, Integer year, String name) { Sql sql = Sqls.create(""" diff --git a/src/main/java/io/v/nutz/web/commons/proc/NutShiroProcessor.java b/src/main/java/io/v/nutz/web/commons/proc/NutShiroProcessor.java index 8d60c9f..40fa7e2 100644 --- a/src/main/java/io/v/nutz/web/commons/proc/NutShiroProcessor.java +++ b/src/main/java/io/v/nutz/web/commons/proc/NutShiroProcessor.java @@ -74,7 +74,7 @@ public class NutShiroProcessor extends AbstractProcessor { "/platform/login(/doLogin|/logout|/captcha)", "/platform/qywechat/.*", "/platform/home/(500|403|404|UnknownAccountError|LockedAccountError)", -// "/mobile/login", + "/mobile/login", "/mobile/login/doLogin", "/platform/jsz/login", "/platform/activity/basic/scope/getScopeUser", diff --git a/src/main/resources/static/assets/mobile/js/basic.js b/src/main/resources/static/assets/mobile/js/basic.js index fc21aa9..a077943 100644 --- a/src/main/resources/static/assets/mobile/js/basic.js +++ b/src/main/resources/static/assets/mobile/js/basic.js @@ -34,6 +34,67 @@ const clone = (val) => { return JSON.parse(JSON.stringify(val)) } +// H5 页面按需注册弹框同步回调;全站只保留一个 popstate 监听,弹框按后进先出关闭。 +const mobilePopupHistory = { + owner: null, + token: null, + stack: [], + pending: null, + register(sync) { + this.owner = sync + this.token = String(Date.now()) + Math.random() + this.stack = [] + return this + }, + open(key) { + if (!this.owner || this.pending || this.stack.includes(key)) return + this.stack.push(key) + window.history.pushState(Object.assign({}, window.history.state, { + mobilePopupDepth: this.stack.length, + mobilePopupOwner: this.token + }), '', window.location.href) + this.owner(key, true) + }, + // 返回 Promise,关闭确认框后再提交,避免回退尚未完成便清理父级弹框。 + close() { + if (this.pending) return this.pending.promise + if (!this.stack.length) return Promise.resolve() + return this.back(1) + }, + back(count) { + let complete + const promise = new Promise(resolve => { complete = resolve }) + this.pending = {promise, complete} + window.history.go(-count) + return promise + }, + clear() { + if (this.pending) return this.pending.promise.then(() => this.clear()) + return this.stack.length ? this.back(this.stack.length) : Promise.resolve() + }, + unregister(sync) { + if (this.owner !== sync) return + this.owner = null + this.stack = [] + }, + onPop(event) { + if (!this.owner) return + const depth = event.state && event.state.mobilePopupOwner === this.token + ? event.state.mobilePopupDepth : 0 + // 弹框回退由本管理器消费,避免 PJAX 同时将其识别为业务页面切换。 + if (this.stack.length > depth || this.pending) event.stopImmediatePropagation() + while (this.stack.length > depth) { + this.owner(this.stack.pop(), false) + } + if (this.pending) { + const complete = this.pending.complete + this.pending = null + complete() + } + } +} +window.addEventListener('popstate', event => mobilePopupHistory.onPop(event), true) + const toChinesNum = (num) => { let changeNum = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']; let unit = ["", "十", "百", "千", "万"]; diff --git a/src/main/resources/static/components/club/clubUserApplyInfo.vue b/src/main/resources/static/components/club/clubUserApplyInfo.vue index 929751c..e90e575 100644 --- a/src/main/resources/static/components/club/clubUserApplyInfo.vue +++ b/src/main/resources/static/components/club/clubUserApplyInfo.vue @@ -6,74 +6,15 @@ - - {{viewData.username}} - - - {{viewData.sex}} - - - {{moment(viewData.birthday).format('YYYY-MM-DD')}} - - - {{viewData.birthPlace}} - - - {{viewData.education}} - - - {{viewData.hobby}} - - - {{viewData.unitName}} - - - {{viewData.position}} - - - {{viewData.mobile}} - - - {{viewData.email}} - - - {{viewData.homeAddress}} - - - {{viewData.QQNumber}} - - - {{viewData.bodyHeight}} - - - {{viewData.bodyWeight}} - - - {{viewData.health}} - - - {{viewData.sport}} - - - {{viewData.medicalHistory === true ? '是' : '否'}} - - - {{viewData.joinActivity === true ? '是' : '否'}} - - -
-
- -
-
- - -
- -
-
+ {{ viewData.username }} + {{ viewData.sex }} + {{ viewData.loginname }} + {{ viewData.birthday ? moment(viewData.birthday).format('YYYY-MM-DD') : '' }} + {{ viewData.mobile }} + {{ viewData.medicalHistory === true ? '是' : viewData.medicalHistory === false ? '否' : '' }} + {{ viewData.otherThing }} + {{ viewData.emergencyContact }} + {{ viewData.emergencyMobile }}
@@ -139,14 +80,15 @@ module.exports = { } return this.panes.includes(name) }, - async getInfo(id) { - this.loading = true - const {data, code, msg} = await $.get('/platform/sys/club/sq/findOne', {id}) - this.loading = false - if (data) { - this.clubUserInfo = this.handle ? "999" : "1" - this.viewData = data - } + // id 为入会记录主键;data 返回申请资料和审核信息,历史新增字段可为空。 + getInfo(id) { + this.$set(this, 'loading', true) + $.post('/platform/sys/club/sq/findOne', {id}).then(res => { + if (res.code === 0 && res.data) { + this.$set(this, 'clubUserInfo', this.handle ? '999' : '1') + this.$set(this, 'viewData', res.data) + } + }).always(() => { this.$set(this, 'loading', false) }) } } } diff --git a/src/main/resources/views/mobile/club/inClub.html b/src/main/resources/views/mobile/club/inClub.html index bc4a5a9..42078ad 100644 --- a/src/main/resources/views/mobile/club/inClub.html +++ b/src/main/resources/views/mobile/club/inClub.html @@ -110,7 +110,7 @@ layout("/mobile/platform.html"){ left-arrow placeholder fixed - @click-left="pjaxReplace('/mobile/index')" + @click-left="leavePage" safe-area-inset-top > @@ -207,13 +207,13 @@ layout("/mobile/platform.html"){ > - + @@ -233,94 +233,29 @@ layout("/mobile/platform.html"){ 点击查看 + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + @@ -336,6 +271,9 @@ layout("/mobile/platform.html"){ data() { return { showPicker: false, + birthdayPickerValue: new Date(), + birthdayMinDate: new Date(1900, 0, 1), + birthdayMaxDate: new Date(), infoActive: 0, clickRow: {}, typeOptions: [], @@ -343,17 +281,77 @@ layout("/mobile/platform.html"){ infoShow: false, detailData: {}, viewType: '', + formLoading: false, + // 服务端保留本次页面请求的社团 ID,避免依赖页面切换时的地址栏状态。 + scanClubId: decodeURIComponent('${scanClubId!}'.replace(/\+/g, '%20')), + } + }, + computed: { + canJoin() { + return !!this.detailData.id && ![1, 3, 5].includes(Number(this.detailData.status)) + }, + joinStatus() { + return {1: '待社团审核', 3: '待校工会审核', 5: '已加入'}[this.detailData.status] || '' } }, methods: { - showCode(type) { - if(type === 'QQ' && this.clickRow.qqgroupcode) { - const ids = JSON.parse(this.clickRow.qqgroupcode).map(o => FILE_DOMAIN + '/fileStreamPreview?id=' + o.id) - vant.ImagePreview({images: ids}) - } else if(type === 'weChat' && this.clickRow.wechatgroupcode) { - const ids = JSON.parse(this.clickRow.wechatgroupcode).map(o => FILE_DOMAIN + '/fileStreamPreview?id=' + o.id) - vant.ImagePreview({images: ids}) + // 所有弹层由统一历史栈同步;页面不单独监听 popstate。 + syncPopup(key, visible) { + if (key === 'info') this.$set(this, 'infoShow', visible) + if (key === 'birthday') this.$set(this, 'showPicker', visible) + if (key === 'confirm' && !visible) vant.Dialog.close() + if (key === 'preview' && !visible && this.clubPreview) this.clubPreview.close() + }, + closePopup() { + return mobilePopupHistory.close() + }, + leavePage() { + if (mobilePopupHistory.stack.length) { + this.closePopup() + return } + this.disposePopup() + pjaxReplace('/mobile/index') + }, + disposePopup() { + mobilePopupHistory.unregister(this.syncPopup) + $(document).off('pjax:beforeSend.clubScan') + }, + openBirthday() { + // 选择器定位到系统出生日期;档案为空时允许用户补填。 + const date = moment(this.formData.birthday, 'YYYY-MM-DD', true) + this.$set(this, 'birthdayPickerValue', date.isValid() ? date.toDate() : new Date()) + mobilePopupHistory.open('birthday') + }, + confirmBirthday(value) { + this.$set(this.formData, 'birthday', moment(value).format('YYYY-MM-DD')) + this.closePopup() + }, + // 确认/取消先回退本层历史,系统返回由同步回调关闭确认框。 + confirmAction(message) { + mobilePopupHistory.open('confirm') + return vant.Dialog.confirm({title: '提示', message, closeOnPopstate: false}) + .then(() => this.closePopup()) + .catch(() => { + if (mobilePopupHistory.stack.includes('confirm')) { + return this.closePopup().then(() => Promise.reject()) + } + return Promise.reject() + }) + }, + showCode(type) { + const files = type === 'QQ' ? this.detailData.qqgroupcode : this.detailData.wechatgroupcode + if (!files) return + const images = (typeof files === 'string' ? JSON.parse(files) : files) + .map(o => FILE_DOMAIN + '/fileStreamPreview?id=' + o.id) + if (!images.length) return + mobilePopupHistory.open('preview') + this.clubPreview = vant.ImagePreview({ + images, closeOnPopstate: false, + onClose: () => { + if (mobilePopupHistory.stack.includes('preview')) this.closePopup() + } + }) }, doSearch() { this.pageForm.pageNumber = 1 @@ -361,7 +359,7 @@ layout("/mobile/platform.html"){ this.finished = false this.onLoad() }, - async onLoad() { + onLoad() { this.loading = true $.post('/platform/sys/club/sq/pageData', this.pageForm).then(res => { if (res.code === 0) { @@ -372,99 +370,139 @@ layout("/mobile/platform.html"){ this.pageForm.pageNumber++ } } - this.loading = false - }) + }).always(() => { this.loading = false }) }, openInfo(row, type) { - this.viewType = type - this.infoActive = type === 'view' ? 0 : 1 - if(type === 'join') { - this.formData = { + if (this.mLoading) return + this.mLoading = true + // clubId 是社团主键;data 返回详情及登录用户状态,不受列表页码、筛选影响。 + $.post('/platform/sys/club/sq/scanDetail', {clubId: row.id}).then(res => { + if (res.code !== 0) { + vant.Toast(res.msg || '社团信息加载失败') + return + } + this.detailData = res.data + this.clickRow = res.data + this.viewType = 'view' + this.infoActive = 0 + if (this.canJoin && (type === 'join' || type === 'scan')) { + this.prepareJoin() + } + // 等待条件页签及弹框渲染后再打开;扫码先展示社团信息,列表申请展示个人信息。 + this.$nextTick(() => { + this.infoActive = type === 'join' && this.canJoin ? 1 : 0 + mobilePopupHistory.open('info') + }) + }).fail(() => vant.Toast('社团信息加载失败,请重试')) + .always(() => { this.mLoading = false }) + }, + startJoin() { + if (!this.canJoin) return + // 已进入加入模式时保留用户填写的数据,不因切换页签再次清空。 + if (this.viewType !== 'join') this.prepareJoin() + this.$nextTick(() => { this.infoActive = 1 }) + }, + // 初始化当前社团的加入表单;扫码展示两个页签时也复用此方法。 + prepareJoin() { + this.viewType = 'join' + this.$set(this, 'formData', { username: "${@shiro.getPrincipalProperty('username')}", sex: "${@shiro.getPrincipalProperty('sex')}", + loginname: "${@shiro.getPrincipalProperty('loginname')}", birthday: "${@shiro.getPrincipalProperty('birthday')}", - birthPlace: "${@shiro.getPrincipalProperty('birthPlace')}", - education: "${@shiro.getPrincipalProperty('education')}", - hobby: "${@shiro.getPrincipalProperty('hobby')}", - unitName: "${@shiro.getPrincipalProperty('unit').getName()}", - position: "${@shiro.getPrincipalProperty('position')}", mobile: "${@shiro.getPrincipalProperty('mobile')}", - email: "${@shiro.getPrincipalProperty('email')}", - homeAddress: "${@shiro.getPrincipalProperty('homeAddress')}", - } - } - this.clickRow = row - $.get("/platform/sys/club/mange/findOne", {id: row.id}, (data) => { - if (data.code === 0) { - this.detailData = data.data - this.infoShow = true - } - }, "json"); + medicalHistory: null, otherThing: '', emergencyContact: '', emergencyMobile: '' + }) }, - async needJoin() { + needJoin() { + if (this.formLoading || !this.canJoin) return const row = this.clickRow if(this.formData.medicalHistory === '' || this.formData.medicalHistory === undefined || this.formData.medicalHistory === null) { - vant.Toast('请选择是否有过特殊病史') + vant.Toast('请选择是否有特殊病史') return } - if(this.formData.joinActivity === '' || this.formData.joinActivity === undefined || this.formData.joinActivity === null) { - vant.Toast('请选择是否愿意参加本协会的活动') - return - } - if(this.formData.photo === '' || this.formData.photo === undefined || this.formData.photo === null) { - vant.Toast('请上传两寸登记照') - return - } - const {code, data} = await $.post('/platform/sys/club/sq/getInClubByCurUser') - let str = '' - if (data && data.length > 0) { - str = '您已经加入了' - data.forEach((item) => { - str += '【' + item.name + '】' - }) - str += ',' - } - str += '请确认是否申请加入【' + row.name + '】?' - vant.Dialog.confirm({title: '提示', message: str,}).then(() => { - this.formData.clubid = row.id - this.formData.photo = this.formData.photo[0].filepath - $.post("/platform/sys/club/sq/submit", this.formData, (data) => { - if (data.code === 0) { - vant.Toast(data.msg) - this.doSearch() - this.infoShow = false - } else { - vant.Toast(data.msg) - } - }, "json"); - }).catch(() => { - }); + this.$set(this, 'formLoading', true) + $.post('/platform/sys/club/sq/getInClubByCurUser').then(res => { + if (res.code !== 0) { + vant.Toast(res.msg) + return + } + const data = res.data + let str = '' + if (data && data.length > 0) { + str = '您已经加入了' + data.forEach((item) => { + str += '【' + item.name + '】' + }) + str += ',' + } + str += '请确认是否申请加入【' + row.name + '】?' + this.confirmAction(str).then(() => { + this.$set(this, 'formLoading', true) + // 仅提交精简后的入会资料,出生年月和电话不回写用户档案。 + const params = Object.assign({}, this.formData, { + clubid: row.id + }) + $.post("/platform/sys/club/sq/submit", params).then(data => { + if (data.code === 0) { + vant.Toast(data.msg) + mobilePopupHistory.clear().then(() => this.doSearch()) + } else { + vant.Toast(data.msg) + } + }).always(() => { this.$set(this, 'formLoading', false) }) + }).catch(() => { + }); + }).always(() => { this.$set(this, 'formLoading', false) }) }, rollback(o) { - vant.Dialog.confirm({title: '提示', message: '请确定是否撤销申请?',}).then(() => { + this.confirmAction('请确定是否撤销申请?').then(() => { const id = o.id - $.post("/platform/sys/club/sq/revocation", {id}, (data) => { + this.$set(this, 'formLoading', true) + $.post("/platform/sys/club/sq/revocation", {id}).then(data => { if (data.code === 0) { vant.Toast('撤销申请成功'); this.doSearch(); } else { vant.Toast(data.msg); } - }, "json"); + }).always(() => { this.$set(this, 'formLoading', false) }) }).catch(() => { }); } }, - async created() { - const data = await getClubType() - data.forEach(item => { - this.typeOptions.push({text: item.name, value: item.code}) + created() { + mobilePopupHistory.register(this.syncPopup) + // PJAX 离开前先清理弹框历史,重新发起原页面跳转,并注销本页回调。 + $(document).on('pjax:beforeSend.clubScan', (event, xhr, options) => { + if (mobilePopupHistory.stack.length) { + event.preventDefault() + mobilePopupHistory.clear().then(() => { + this.disposePopup() + pjaxReplace(options.url) + }) + return false + } + this.disposePopup() }) - this.typeOptions.unshift({text: '全部类型'}) - this.$set(this.pageForm, "clubType", this.typeOptions[0].value) - this.$set(this.pageForm, "isjr", this.clubType[0].value) - this.pageForm.searchName = 'club.name' + this.$set(this.pageForm, 'isjr', false) + this.$set(this.pageForm, 'searchName', 'club.name') this.onLoad() + getClubType().then(data => { + data.forEach(item => { + this.typeOptions.push({text: item.name, value: item.code}) + }) + this.typeOptions.unshift({text: '全部类型'}) + this.$set(this.pageForm, "clubType", this.typeOptions[0].value) + }) + }, + mounted() { + // 兼容已有二维码;无 clubId 的普通入口仍显示列表,不猜测目标社团。 + const clubId = this.scanClubId || new URLSearchParams(window.location.search).get('clubId') + if (clubId) this.openInfo({id: clubId}, 'scan') + }, + beforeDestroy() { + mobilePopupHistory.clear().then(() => this.disposePopup()) } }) diff --git a/src/main/resources/views/platform/sys/club/applyIn/sqrt.html b/src/main/resources/views/platform/sys/club/applyIn/sqrt.html index ea0da12..6f1217b 100644 --- a/src/main/resources/views/platform/sys/club/applyIn/sqrt.html +++ b/src/main/resources/views/platform/sys/club/applyIn/sqrt.html @@ -58,6 +58,38 @@ layout("/layouts/platform.html"){ margin: 0 0 18px; } + /* 仅调整入会表单,避免影响公共表格组件及其他业务页面。 */ + #app .club-apply-form th, + #app .club-apply-form .el-form-item__label { + text-align: center; + } + + #app .club-apply-form .el-form-item__label { + width: 100%; + } + + #app .club-apply-form th.club-required::before { + content: "*"; + color: #f56c6c; + margin-right: 4px; + } + + /* 校验信息占据字段下方的正常文档流,长提示也不会遮挡下一行。 */ + #app .club-apply-form .el-form-item__error { + position: relative; + top: auto; + left: auto; + z-index: auto; + background: transparent; + color: #f56c6c; + border-radius: 0; + padding: 4px 0 0; + min-width: 0; + font-size: 12px; + line-height: 1.4; + text-align: left; + } +
@@ -142,10 +174,11 @@ layout("/layouts/platform.html"){ - + + +
+

{{clubCodeName}}

+ 扫码查看社团并申请加入 +

手机扫码查看社团并申请加入

+
+
diff --git a/src/main/resources/views/platform/sys/club/clubUserCount.html b/src/main/resources/views/platform/sys/club/clubUserCount.html index 23e264f..7d85473 100644 --- a/src/main/resources/views/platform/sys/club/clubUserCount.html +++ b/src/main/resources/views/platform/sys/club/clubUserCount.html @@ -58,7 +58,9 @@ layout("/layouts/platform.html"){ @@ -225,6 +227,10 @@ layout("/layouts/platform.html"){ 'dict-select': httpVueLoader('/components/plugins/DictSelect.vue?v=1.0.0'), }, methods: { + exportAllUserExcel() { + // 导出权限范围内全部有效成员,不携带页面筛选或分页参数。 + window.open('/platform/sys/club/clubUserCount/exportAllUserExcel') + }, exportUserData(row) { const id = row === null ? '' : row.id let url = '/platform/sys/club/clubUserCount/exportUserData?clubId=' + id +