Compare commits

...
2 Commits
35 changed files with 2055 additions and 67 deletions
+26
View File
@@ -0,0 +1,26 @@
# Maven
/target/
/dependency-reduced-pom.xml
# IDE
/.idea/
/.vscode/
*.iml
*.ipr
*.iws
/.classpath
/.project
/.settings/
# Logs and temporary files
/logs/
/temp/
*.log
*.tmp
hs_err_pid*
replay_pid*
# Operating system
.DS_Store
Thumbs.db
Desktop.ini
@@ -30,6 +30,15 @@ public class EnumUtil {
// Field[] fields = enumClass.getDeclaredFields();
Field[] fields = ReflectUtil.getFields(enumClass);
for (Field field : fields) {
if (field.getDeclaringClass() == Enum.class) {
Enum<?> value = (Enum<?>) enumConstant;
if ("name".equals(field.getName())) {
map.put(field.getName(), value.name());
} else if ("ordinal".equals(field.getName())) {
map.put(field.getName(), value.ordinal());
}
continue;
}
try {
field.setAccessible(true); // 使私有字段可访问
map.put(field.getName(), field.get(enumConstant)); // 获取字段值并添加到map中
@@ -1,61 +1,26 @@
package com.budwk.app.zhgh.dayofficework.birthdayWishes.task;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.sys.models.Sys_msg;
import com.budwk.app.sys.services.SysMsgService;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.List;
@IocBean
@Slf4j
public class BirthdayWishesTask implements Job {
@Inject
protected SysMsgService sysMsgService;
@Inject
protected Dao dao;
private UserBirthdayService userBirthdayService;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("=================================执行计划任务:{}", context.getJobDetail().getKey().getName());
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
log.info("=================================参数:{}", Json.toJson(dataMap));
String planName = dataMap.getString("name");
String today = DateUtil.today();
Sys_msg msg = new Sys_msg();
msg.setTitle(planName + today);
msg.setType("user");
msg.setSendType("show");
msg.setSendAt(DateUtil.current());
msg.setNote(dataMap.getString("template"));
// 查询今天生日的会员用户
Sql sql = Sqls.create("SELECT loginname FROM sys_user WHERE member = 1 AND MONTH(birthday) = MONTH(CURRENT_DATE) AND DAY(birthday) = DAY(CURRENT_DATE)");
sql.setCallback(Sqls.callback.strList());
dao.execute(sql);
List<String> users = sql.getList(String.class);
// 转为数组
String[] userArray = users.toArray(String[]::new);
log.info("=================================查询今天生日会员用户:{}", users);
if(userArray.length > 0){
log.info("=================================发送消息:{}", msg);
sysMsgService.saveMsg(msg, userArray, true);
}
// 统一由生日服务查询当天生日会员、生成移动端链接并记录系统推送日志。
int receiverCount = userBirthdayService.sendTodayBirthdayMessages();
log.info("=================================当天生日通知进入发送队列人数:{}", receiverCount);
}
}
@@ -0,0 +1,71 @@
package com.budwk.app.zhgh.staffmanage.birthday.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayConfig;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
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;
/**
* 生日祝福配置管理。
* 配置内容包含页面图片、默认查询天数、自动发送开关和发送文案。
*/
@IocBean
@At("/platform/staffManage/birthday/config")
@Ok("json:full")
@Api(tags = "生日祝福配置")
public class UserBirthdayConfigController {
@Inject
private UserBirthdayService userBirthdayService;
@At("")
@Ok("beetl:/platform/zhgh/staffmanage/birthday/config/index.html")
@SaCheckPermission("staff.birthday.config")
public void index() {
}
/**
* 获取当前生日祝福配置。
*
* @return Result.data 为 UserBirthdayConfig,包含图片文件ID、查询天数、自动发送开关、标题和内容
*/
@At
@ApiOperation("获取生日祝福配置")
@SaCheckPermission("staff.birthday.config")
public Result getConfig() {
return Result.success(userBirthdayService.getConfig());
}
/**
* 保存生日祝福配置。
* config.picUrl、birthdayUrl 为图片文件ID;birthdayDays 为正整数;
* autoSendEnabled 为自动发送开关;sendTitle、sendContent 为默认发送文案。
*
* @return Result.data 为保存后的 UserBirthdayConfig
*/
@At
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("保存生日祝福配置")
@SaCheckPermission("staff.birthday.config")
public Result saveConfig(UserBirthdayConfig config) {
if (config == null || config.getBirthdayDays() == null || config.getBirthdayDays() <= 0) {
return Result.error("默认查询天数必须为正整数");
}
if (StrUtil.isBlank(config.getSendTitle())) {
return Result.error("请输入发送标题");
}
if (StrUtil.isBlank(config.getSendContent())) {
return Result.error("请输入发送内容");
}
return Result.success("保存成功", userBirthdayService.saveConfig(config));
}
}
@@ -0,0 +1,142 @@
package com.budwk.app.zhgh.staffmanage.birthday.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayConfig;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdayPageForm;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdaySendMsgForm;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
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.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@IocBean
@At("/platform/staffManage/birthday/manage")
@Ok("json:full")
@Api(tags = "生日祝福管理")
public class UserBirthdayManageController {
@Inject
private UserBirthdayService userBirthdayService;
@At("")
@Ok("beetl:/platform/zhgh/staffmanage/birthday/manage/index.html")
@SaCheckPermission("staff.birthday.manage")
public void index() {
}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffmanage/birthday/index.html")
@SaCheckLogin
public void h5(String id, HttpServletRequest request) {
request.setAttribute("fileId", StrUtil.blankToDefault(id, ""));
request.setAttribute("userName", SecurityUtil.getUserUsername());
}
/**
* 查询生日人员。
*
* @param pageForm 分页、姓名/工号、工会、单位、人员状态及生日日期条件
* @return Result.data 为 Pagination,list 是人员数据,totalCount 是总数
*/
@At
@ApiOperation("分页查询生日人员")
@SaCheckPermission("staff.birthday.manage")
public Result pageData(UserBirthdayPageForm pageForm) {
return Result.success(userBirthdayService.pageData(pageForm));
}
/**
* 导出生日人员 Excel。
*
* @param pageForm 与列表查询一致的筛选条件
* @param response 返回 XSSF 格式的 xlsx 文件流
*/
@At
@Ok("void")
@ApiOperation("导出生日人员")
@SaCheckPermission("staff.birthday.manage")
public void doExport(UserBirthdayPageForm pageForm, HttpServletResponse response) {
userBirthdayService.exportXlsx(pageForm, response);
}
/**
* 按查询条件批量发送通知。
* form.title 为标题,form.content 为正文,其余字段为人员筛选条件;返回 data 为接收人数。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("按查询条件发送生日通知")
@SaCheckPermission("staff.birthday.manage")
public Result sendMsgByQueryUsers(UserBirthdaySendMsgForm form) {
Result validation = validateMessage(form);
if (validation != null) {
return validation;
}
int count = userBirthdayService.sendByQuery(form, SecurityUtil.getUserId(), SecurityUtil.getUserUsername());
if (count == 0) {
return Result.error("当前条件下没有可发送的人员");
}
return Result.success("已提交发送,共" + count + "人", count);
}
/**
* 向单个用户发送通知。
* form.userId 为接收人用户ID,title、content 为消息内容;返回 data 为发送人数 1。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("向单个用户发送生日通知")
@SaCheckPermission("staff.birthday.manage")
public Result sendMsgByUser(UserBirthdaySendMsgForm form) {
Result validation = validateMessage(form);
if (validation != null) {
return validation;
}
if (StrUtil.isBlank(form.getUserId())) {
return Result.error("未获取到消息接收人");
}
boolean sent = userBirthdayService.sendByUser(form, SecurityUtil.getUserId(), SecurityUtil.getUserUsername());
return sent ? Result.success("已提交发送", 1) : Result.error("未获取到消息接收人");
}
@At
@ApiOperation("获取生日页面配置")
@SaCheckLogin
public Result getConfig() {
return Result.success(userBirthdayService.getConfig());
}
/**
* 保存生日页面图片配置。
* config.picUrl 为背景图片文件ID,config.birthdayUrl 为福利贺卡文件ID;返回保存后的配置对象。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("保存生日页面配置")
@SaCheckPermission("staff.birthday.manage")
public Result saveOrModifyConfig(UserBirthdayConfig config) {
return Result.success("保存成功", userBirthdayService.saveConfig(config));
}
private Result validateMessage(UserBirthdaySendMsgForm form) {
if (form == null || StrUtil.isBlank(form.getTitle())) {
return Result.error("请输入发送标题");
}
if (StrUtil.isBlank(form.getContent())) {
return Result.error("请输入发送内容");
}
return null;
}
}
@@ -0,0 +1,41 @@
package com.budwk.app.zhgh.staffmanage.birthday.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdayMsgLogPageForm;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayMsgLogService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;
@IocBean
@At("/platform/staffManage/birthday/msgLog")
@Ok("json:full")
@Api(tags = "生日消息记录")
public class UserBirthdayMsgLogController {
@Inject
private UserBirthdayMsgLogService userBirthdayMsgLogService;
@At("")
@Ok("beetl:/platform/zhgh/staffmanage/birthday/msglog/index.html")
@SaCheckPermission("staff.birthday.msgLog")
public void index() {
}
/**
* 查询生日消息记录。
*
* @param pageForm 分页、姓名/工号、工会和单位查询条件
* @return Result.data 为 Pagination,list 是发送记录,totalCount 是总数
*/
@At
@ApiOperation("分页查询生日消息记录")
@SaCheckPermission("staff.birthday.msgLog")
public Result pageData(UserBirthdayMsgLogPageForm pageForm) {
return Result.success(userBirthdayMsgLogService.pageData(pageForm));
}
}
@@ -0,0 +1,61 @@
package com.budwk.app.zhgh.staffmanage.birthday.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
/**
* 生日祝福页面配置。
* picUrl、birthdayUrl 保存 sys_file 文件 ID,由移动端按文件 ID 获取图片。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("user_birthday_config")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("生日祝福页面配置")
public class UserBirthdayConfig extends BaseModel {
@Name
@Comment("ID")
@PrevInsert(uu32 = true)
@ColDefine(type = ColType.VARCHAR, width = 32)
private String id;
@Column
@Comment("移动端背景图片文件ID")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String picUrl;
@Column
@Comment("生日福利贺卡图片文件ID")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String birthdayUrl;
@Column
@Comment("默认查询未来生日天数")
@ColDefine(type = ColType.INT)
private Integer birthdayDays;
@Column
@Comment("是否自动发送生日祝福")
@ColDefine(type = ColType.BOOLEAN)
private Boolean autoSendEnabled;
@Column
@Comment("默认发送标题")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String sendTitle;
@Column
@Comment("默认发送内容")
@ColDefine(type = ColType.TEXT)
private String sendContent;
}
@@ -0,0 +1,76 @@
package com.budwk.app.zhgh.staffmanage.birthday.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
/**
* 生日祝福消息发送记录。
* 每个接收人保存一条记录,便于按人员、单位和工会追溯发送情况。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("user_birthday_msg_log")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("生日祝福消息发送记录")
public class UserBirthdayMsgLog extends BaseModel {
@Name
@Comment("ID")
@PrevInsert(uu32 = true)
@ColDefine(type = ColType.VARCHAR, width = 32)
private String id;
@Column
@Comment("推送时间")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String pushTime;
@Column
@Comment("消息标题")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String msgTitle;
@Column
@Comment("消息内容")
@ColDefine(type = ColType.TEXT)
private String msgContent;
@Column
@Comment("接收人用户ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String receiveBy;
@Column
@Comment("接收人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String receiveName;
@Column
@Comment("推送人用户ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String pushBy;
@Column
@Comment("推送人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String pushByName;
@Column
@Comment("推送类型")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String pushType;
@Column
@Comment("生日祝福页面链接")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String link;
}
@@ -0,0 +1,15 @@
package com.budwk.app.zhgh.staffmanage.birthday.param;
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberInfoPageForm;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 生日消息记录分页查询参数,复用人员模块的工会、单位及数据范围条件。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("生日消息记录分页查询参数")
public class UserBirthdayMsgLogPageForm extends MemberInfoPageForm {
}
@@ -0,0 +1,66 @@
package com.budwk.app.zhgh.staffmanage.birthday.param;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberInfoPageForm;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.Cnd;
import org.nutz.dao.util.cri.Static;
/**
* 生日人员分页查询参数。
* startDate、endDate 接收 yyyy-MM-dd;生日比较只使用月日,允许查询跨年区间。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("生日人员分页查询参数")
public class UserBirthdayPageForm extends MemberInfoPageForm {
@ApiModelProperty("生日开始日期,格式 yyyy-MM-dd")
private String startDate;
@ApiModelProperty("生日结束日期,格式 yyyy-MM-dd")
private String endDate;
/**
* 将生日日期条件追加到已有人员查询条件。
* 未传日期时按配置天数查询,配置无效时默认十五天,跨年时自动使用 OR 条件。
*
* @param cnd 已包含人员范围和会员状态的查询条件
*/
public void buildBirthdaySearch(Cnd cnd, Integer birthdayDays) {
String start = normalizeMonthDay(startDate);
String end = normalizeMonthDay(endDate);
if (StrUtil.isAllBlank(start, end)) {
int days = birthdayDays == null || birthdayDays <= 0 ? 15 : birthdayDays;
start = DateUtil.format(DateUtil.date(), "MM-dd");
end = DateUtil.format(DateUtil.offsetDay(DateUtil.date(), days), "MM-dd");
}
if (StrUtil.isAllNotBlank(start, end)) {
if (start.compareTo(end) > 0) {
cnd.and(new Static(String.format(
"(DATE_FORMAT(u.birthday, '%%m-%%d') >= '%s' OR DATE_FORMAT(u.birthday, '%%m-%%d') <= '%s')",
start, end)));
} else {
cnd.and(new Static(String.format(
"DATE_FORMAT(u.birthday, '%%m-%%d') >= '%s' AND DATE_FORMAT(u.birthday, '%%m-%%d') <= '%s'",
start, end)));
}
} else if (StrUtil.isNotBlank(start)) {
cnd.and(new Static(String.format("DATE_FORMAT(u.birthday, '%%m-%%d') >= '%s'", start)));
} else {
cnd.and(new Static(String.format("DATE_FORMAT(u.birthday, '%%m-%%d') <= '%s'", end)));
}
}
private String normalizeMonthDay(String value) {
if (StrUtil.isBlank(value)) {
return null;
}
return DateUtil.format(DateUtil.parse(value), "MM-dd");
}
}
@@ -0,0 +1,25 @@
package com.budwk.app.zhgh.staffmanage.birthday.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 生日祝福发送参数。
* 查询条件用于批量确定接收人;userId 用于单人发送;title、content 为消息标题和正文。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("生日祝福发送参数")
public class UserBirthdaySendMsgForm extends UserBirthdayPageForm {
@ApiModelProperty("消息标题")
private String title;
@ApiModelProperty("消息正文")
private String content;
@ApiModelProperty("单人发送时的用户ID")
private String userId;
}
@@ -0,0 +1,36 @@
package com.budwk.app.zhgh.staffmanage.birthday.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayMsgLog;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdayMsgLogPageForm;
import java.util.List;
/**
* 生日祝福消息记录服务。
*/
public interface UserBirthdayMsgLogService extends BaseService<UserBirthdayMsgLog> {
/**
* 查询生日消息记录。
*
* @param pageForm 人员、工会、单位及分页条件
* @return 当前页发送记录及总数
*/
Pagination pageData(UserBirthdayMsgLogPageForm pageForm);
/**
* 为每个接收人写入一条发送记录。
*
* @param loginNames 接收人工号集合
* @param title 消息标题
* @param content 消息正文
* @param link 移动端生日祝福完整链接
* @param pushBy 推送人用户ID
* @param pushByName 推送人姓名
* @param pushType 手动推送或系统推送
*/
void insertLogs(List<String> loginNames, String title, String content, String link,
String pushBy, String pushByName, String pushType);
}
@@ -0,0 +1,72 @@
package com.budwk.app.zhgh.staffmanage.birthday.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayConfig;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdayPageForm;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdaySendMsgForm;
import javax.servlet.http.HttpServletResponse;
/**
* 生日祝福业务服务。
*/
public interface UserBirthdayService extends BaseService<Sys_user> {
/**
* 分页查询生日会员。
*
* @param pageForm 人员、组织和生日日期查询条件
* @return 当前页人员列表及总数
*/
Pagination pageData(UserBirthdayPageForm pageForm);
/**
* 导出符合查询条件的生日会员。
*
* @param pageForm 查询条件
* @param response Excel 文件响应
*/
void exportXlsx(UserBirthdayPageForm pageForm, HttpServletResponse response);
/**
* 按当前查询条件批量发送生日通知。
*
* @param form 查询条件、标题和正文
* @param pushBy 推送人用户ID
* @param pushByName 推送人姓名
* @return 实际进入发送队列的接收人数
*/
int sendByQuery(UserBirthdaySendMsgForm form, String pushBy, String pushByName);
/**
* 向指定用户发送生日通知。
*
* @param form userId、标题和正文
* @param pushBy 推送人用户ID
* @param pushByName 推送人姓名
* @return 是否找到接收人并进入发送队列
*/
boolean sendByUser(UserBirthdaySendMsgForm form, String pushBy, String pushByName);
/**
* 向当天生日的会员发送系统生日通知,供 Quartz 任务调用。
*
* @return 实际进入发送队列的接收人数
*/
int sendTodayBirthdayMessages();
/**
* 获取最新生日页面配置;未配置时返回默认配置。
*/
UserBirthdayConfig getConfig();
/**
* 新增或更新生日页面配置。
*
* @param config 图片、查询天数、自动发送开关及默认消息内容
* @return 保存后的配置
*/
UserBirthdayConfig saveConfig(UserBirthdayConfig config);
}
@@ -0,0 +1,85 @@
package com.budwk.app.zhgh.staffmanage.birthday.service.impl;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayMsgLog;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdayMsgLogPageForm;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayMsgLogService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap;
import java.util.List;
@IocBean(args = {"refer:dao"})
public class UserBirthdayMsgLogServiceImpl extends BaseServiceImpl<UserBirthdayMsgLog>
implements UserBirthdayMsgLogService {
public UserBirthdayMsgLogServiceImpl(Dao dao) {
super(dao);
}
@Override
public Pagination pageData(UserBirthdayMsgLogPageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
log.*,
u.loginname AS loginname,
u.username AS username,
u.unitName AS unitName,
u.unionName AS unionName
FROM user_birthday_msg_log log
LEFT JOIN vw_user u ON u.id = log.receiveBy
$condition
""");
Cnd cnd = Cnd.NEW();
pageForm.buildSearch(cnd, "u.");
cnd.desc("log.pushTime");
sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void insertLogs(List<String> loginNames, String title, String content, String link,
String pushBy, String pushByName, String pushType) {
if (Lang.isEmpty(loginNames)) {
return;
}
// 只根据当前批次的工号读取接收人,日志中的用户ID和姓名保持发送当时快照。
Sql sql = Sqls.create("""
SELECT id, loginname, username
FROM vw_user
$condition
""");
sql.setCondition(Cnd.where("loginname", "in", loginNames));
List<NutMap> users = listMap(sql);
String pushTime = DateUtil.now();
List<UserBirthdayMsgLog> logs = users.stream().map(user -> {
UserBirthdayMsgLog log = new UserBirthdayMsgLog();
log.setId(R.UU32());
log.setReceiveBy(user.getString("id"));
log.setReceiveName(user.getString("username"));
log.setMsgTitle(title);
log.setMsgContent(content);
log.setPushBy(pushBy);
log.setPushByName(pushByName);
log.setPushType(pushType);
log.setPushTime(pushTime);
log.setLink(link);
return log;
}).toList();
if (!logs.isEmpty()) {
dao().fastInsert(logs);
}
}
}
@@ -0,0 +1,287 @@
package com.budwk.app.zhgh.staffmanage.birthday.service.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.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_msg;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.services.SysMsgService;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayConfig;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdayPageForm;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdaySendMsgForm;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayMsgLogService;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Times;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.Mvcs;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@IocBean(args = {"refer:dao"})
public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implements UserBirthdayService {
private static final int DEFAULT_BIRTHDAY_DAYS = 15;
private static final String DEFAULT_SEND_TITLE = "请点击查收您的生日福利";
private static final String DEFAULT_SEND_CONTENT = "校工会祝您:生日快乐!幸福安康";
@Inject
private PropertiesProxy conf;
@Inject
private SysMsgService sysMsgService;
@Inject
private UserBirthdayMsgLogService userBirthdayMsgLogService;
public UserBirthdayServiceImpl(Dao dao) {
super(dao);
}
@Override
public Pagination pageData(UserBirthdayPageForm pageForm) {
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), buildBirthdaySql(pageForm));
}
@Override
public void exportXlsx(UserBirthdayPageForm pageForm, HttpServletResponse response) {
try {
List<NutMap> list = listMap(buildBirthdaySql(pageForm));
List<ExcelExportEntity> columns = new ArrayList<>();
columns.add(new ExcelExportEntity("工号", "loginname", 20));
columns.add(new ExcelExportEntity("姓名", "username", 20));
columns.add(new ExcelExportEntity("性别", "sex", 10));
columns.add(new ExcelExportEntity("联系方式", "mobile", 20));
columns.add(new ExcelExportEntity("出生日期", "birthday", 20));
columns.add(new ExcelExportEntity("生日倒计时(天)", "daysUntilBirthday", 20));
columns.add(new ExcelExportEntity("在职状态", "userState", 20));
columns.add(new ExcelExportEntity("人员类型", "personType", 20));
columns.add(new ExcelExportEntity("人员性质", "preparedBy", 20));
columns.add(new ExcelExportEntity("所属工会", "unionName", 25));
columns.add(new ExcelExportEntity("所属单位", "unitName", 25));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
try (Workbook workbook = ExcelExportUtil.exportExcel(exportParams, columns, list)) {
CommonDownloadUtil.download("生日人员名单.xlsx", workbook, response);
}
} catch (Exception e) {
log.error("导出生日人员名单失败", e);
throw new RuntimeException("导出生日人员名单失败", e);
}
}
@Override
@Aop(TransAop.READ_COMMITTED)
public int sendByQuery(UserBirthdaySendMsgForm form, String pushBy, String pushByName) {
List<String> loginNames = listMap(buildBirthdaySql(form)).stream()
.map(user -> user.getString("loginname"))
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
return sendMessages(loginNames, form.getTitle(), form.getContent(), pushBy, pushByName, "手动推送");
}
@Override
@Aop(TransAop.READ_COMMITTED)
public boolean sendByUser(UserBirthdaySendMsgForm form, String pushBy, String pushByName) {
Sys_user user = dao().fetch(Sys_user.class, form.getUserId());
if (user == null || StrUtil.isBlank(user.getLoginname())) {
return false;
}
sendMessages(List.of(user.getLoginname()), form.getTitle(), form.getContent(),
pushBy, pushByName, "手动推送");
return true;
}
@Override
@Aop(TransAop.READ_COMMITTED)
public int sendTodayBirthdayMessages() {
UserBirthdayConfig config = getConfig();
if (!Boolean.TRUE.equals(config.getAutoSendEnabled())) {
log.info("生日祝福自动发送已关闭");
return 0;
}
Sql sql = Sqls.create("""
SELECT loginname
FROM vw_user
WHERE member = 1
AND birthday IS NOT NULL
AND MONTH(birthday) = MONTH(CURRENT_DATE)
AND DAY(birthday) = DAY(CURRENT_DATE)
""");
sql.setCallback(Sqls.callback.strList());
dao().execute(sql);
List<String> loginNames = sql.getList(String.class);
return sendMessages(loginNames, config.getSendTitle(), config.getSendContent(),
"system", "系统推送", "系统推送");
}
@Override
public UserBirthdayConfig getConfig() {
UserBirthdayConfig config = dao().fetch(UserBirthdayConfig.class, Cnd.NEW().desc("updatedAt"));
return fillConfigDefaults(config == null ? new UserBirthdayConfig() : config);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public UserBirthdayConfig saveConfig(UserBirthdayConfig config) {
UserBirthdayConfig current = dao().fetch(UserBirthdayConfig.class, Cnd.NEW().desc("updatedAt"));
if (current != null) {
config.setId(current.getId());
// 兼容原图片配置入口,未提交的新配置字段保留已有值。
if (config.getBirthdayDays() == null) {
config.setBirthdayDays(current.getBirthdayDays());
}
if (config.getAutoSendEnabled() == null) {
config.setAutoSendEnabled(current.getAutoSendEnabled());
}
if (config.getSendTitle() == null) {
config.setSendTitle(current.getSendTitle());
}
if (config.getSendContent() == null) {
config.setSendContent(current.getSendContent());
}
}
fillConfigDefaults(config);
dao().insertOrUpdate(config);
return config;
}
/**
* 构建生日人员公共查询。
* 查询参数包含姓名/工号、组织、人员状态和生日区间;返回字段供列表、导出和批量发送共用。
*/
private Sql buildBirthdaySql(UserBirthdayPageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
u.id AS id,
u.loginname AS loginname,
u.username AS username,
u.sex AS sex,
u.birthday AS birthday,
u.mobile AS mobile,
u.unitName AS unitName,
u.unionName AS unionName,
u.userState AS userState,
u.personType AS personType,
u.preparedBy AS preparedBy,
DATEDIFF(
CASE
WHEN DATE_FORMAT(u.birthday, '%m-%d') >= DATE_FORMAT(NOW(), '%m-%d')
THEN CONCAT(YEAR(NOW()), '-', DATE_FORMAT(u.birthday, '%m-%d'))
ELSE CONCAT(YEAR(NOW()) + 1, '-', DATE_FORMAT(u.birthday, '%m-%d'))
END,
CURDATE()
) AS daysUntilBirthday
FROM vw_user u
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("u.birthday", "is not", null);
cnd.and("u.member", "=", 1);
pageForm.buildSearch(cnd, "u.");
pageForm.buildBirthdaySearch(cnd, getConfig().getBirthdayDays());
cnd.asc("daysUntilBirthday");
sql.setCondition(cnd);
return sql;
}
private UserBirthdayConfig fillConfigDefaults(UserBirthdayConfig config) {
if (config.getBirthdayDays() == null || config.getBirthdayDays() <= 0) {
config.setBirthdayDays(DEFAULT_BIRTHDAY_DAYS);
}
if (config.getAutoSendEnabled() == null) {
config.setAutoSendEnabled(true);
}
config.setSendTitle(StrUtil.blankToDefault(config.getSendTitle(), DEFAULT_SEND_TITLE));
config.setSendContent(StrUtil.blankToDefault(config.getSendContent(), DEFAULT_SEND_CONTENT));
return config;
}
private String getBirthdayBaseUrl() {
HttpServletRequest request = Mvcs.getReq();
if (request != null) {
String requestUrl = request.getRequestURL().toString();
return requestUrl.substring(0, requestUrl.length() - request.getRequestURI().length())
+ request.getContextPath();
}
// 定时任务没有请求上下文;本机地址未配置端口时使用项目监听端口。
String baseUrl = StrUtil.removeSuffix(Globals.AppDomain, "/");
URI domain = URI.create(baseUrl);
if (domain.getPort() == -1 && StrUtil.equalsAnyIgnoreCase(domain.getHost(),
"localhost", "127.0.0.1", "[::1]", "::1")) {
int defaultPort = "https".equalsIgnoreCase(domain.getScheme()) ? 443 : 80;
int port = conf.getInt("server.port", defaultPort);
if (port != defaultPort) {
baseUrl = domain.getScheme() + "://" + domain.getRawAuthority() + ":" + port
+ StrUtil.blankToDefault(domain.getRawPath(), "");
}
}
String contextPath = StrUtil.removeSuffix(StrUtil.blankToDefault(Globals.AppBase, ""), "/");
if (StrUtil.isNotBlank(contextPath) && !baseUrl.endsWith(contextPath)) {
baseUrl += contextPath;
}
return baseUrl;
}
/**
* 将生日消息写入当前项目消息中心并生成发送记录。
* Sys_msg.url 用于站内消息点击,正文中的完整链接供外部短信/微信渠道访问。
*/
private int sendMessages(List<String> loginNames, String title, String content,
String pushBy, String pushByName, String pushType) {
List<String> recipients = loginNames == null ? List.of() : loginNames.stream()
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
if (recipients.isEmpty()) {
return 0;
}
UserBirthdayConfig config = getConfig();
String pagePath = "/platform/staffManage/birthday/manage/h5";
if (StrUtil.isNotBlank(config.getBirthdayUrl())) {
pagePath = pagePath + "?id=" + URLEncoder.encode(config.getBirthdayUrl(), StandardCharsets.UTF_8);
}
String fullLink = getBirthdayBaseUrl() + pagePath;
Sys_msg message = new Sys_msg();
message.setTitle(title);
message.setNote(content + "\n" + fullLink);
message.setUrl(pagePath);
message.setType("user");
message.setSendType("show");
message.setSendAt(Times.getTS());
message.setCreatedBy(pushBy);
sysMsgService.saveMsg(message, recipients.toArray(String[]::new), true);
userBirthdayMsgLogService.insertLogs(recipients, title, content, fullLink,
pushBy, pushByName, pushType);
return recipients.size();
}
}
@@ -0,0 +1,6 @@
-- 已有生日配置表增加查询和发送配置字段。
ALTER TABLE `user_birthday_config`
ADD COLUMN `birthdayDays` int DEFAULT 15 COMMENT '默认查询未来生日天数',
ADD COLUMN `autoSendEnabled` tinyint(1) DEFAULT 1 COMMENT '是否自动发送生日祝福',
ADD COLUMN `sendTitle` varchar(255) DEFAULT NULL COMMENT '默认发送标题',
ADD COLUMN `sendContent` text COMMENT '默认发送内容';
@@ -0,0 +1,34 @@
CREATE TABLE IF NOT EXISTS `user_birthday_config` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`picUrl` varchar(100) DEFAULT NULL COMMENT '移动端背景图片文件ID',
`birthdayUrl` varchar(100) DEFAULT NULL COMMENT '生日福利贺卡图片文件ID',
`birthdayDays` int DEFAULT 15 COMMENT '默认查询未来生日天数',
`autoSendEnabled` tinyint(1) DEFAULT 1 COMMENT '是否自动发送生日祝福',
`sendTitle` varchar(255) DEFAULT NULL COMMENT '默认发送标题',
`sendContent` text COMMENT '默认发送内容',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生日祝福页面配置';
CREATE TABLE IF NOT EXISTS `user_birthday_msg_log` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`pushTime` varchar(50) DEFAULT NULL COMMENT '推送时间',
`msgTitle` varchar(255) DEFAULT NULL COMMENT '消息标题',
`msgContent` text COMMENT '消息内容',
`receiveBy` varchar(32) DEFAULT NULL COMMENT '接收人用户ID',
`receiveName` varchar(100) DEFAULT NULL COMMENT '接收人姓名',
`pushBy` varchar(32) DEFAULT NULL COMMENT '推送人用户ID',
`pushByName` varchar(100) DEFAULT NULL COMMENT '推送人姓名',
`pushType` varchar(50) DEFAULT NULL COMMENT '推送类型',
`link` varchar(500) DEFAULT NULL COMMENT '生日祝福页面链接',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生日祝福消息发送记录';
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,79 @@
/**
* H5 弹框浏览器历史栈管理器。
* 页面只注册关闭回调;本文件全局统一监听 popstate,避免每个页面重复绑定返回事件。
*/
;((global) => {
const callbacks = new Map()
const popupStack = []
let tokenSeed = 0
let stateSeed = 0
let pendingClear = null
const invokeClose = (entry) => {
const callback = callbacks.get(entry.token)
if (callback) callback(entry.popupKey)
}
global.addEventListener("popstate", (event) => {
if (pendingClear) {
event.stopImmediatePropagation()
const clear = pendingClear
pendingClear = null
clear.resolve()
return
}
if (!popupStack.length) return
// 有弹框历史时消费本次返回事件,阻止 PJAX 将页面直接切回上一个业务页面。
event.stopImmediatePropagation()
const entry = popupStack.pop()
invokeClose(entry)
}, true)
global.h5PopupHistory = {
register(closeCallback) {
tokenSeed += 1
const token = "h5-popup-page-" + tokenSeed
callbacks.set(token, closeCallback)
return token
},
unregister(token) {
callbacks.delete(token)
for (let index = popupStack.length - 1; index >= 0; index -= 1) {
if (popupStack[index].token === token) popupStack.splice(index, 1)
}
},
open(token, popupKey) {
if (!callbacks.has(token)) return
const current = popupStack[popupStack.length - 1]
if (current && current.token === token && current.popupKey === popupKey) return
stateSeed += 1
const entry = {token:token,popupKey:popupKey,stateId:stateSeed}
popupStack.push(entry)
const state = Object.assign({}, global.history.state || {}, {
__h5PopupHistory: {token:token,popupKey:popupKey,stateId:stateSeed}
})
global.history.pushState(state, global.document.title, global.location.href)
},
close(token, popupKey) {
const current = popupStack[popupStack.length - 1]
if (!current || current.token !== token || current.popupKey !== popupKey) return false
global.history.back()
return true
},
clear(token) {
const entries = popupStack.filter((entry) => entry.token === token)
if (!entries.length) return Promise.resolve()
entries.forEach((entry) => invokeClose(entry))
for (let index = popupStack.length - 1; index >= 0; index -= 1) {
if (popupStack[index].token === token) popupStack.splice(index, 1)
}
return new Promise((resolve) => {
pendingClear = {resolve:resolve}
global.history.go(-entries.length)
})
}
}
})(window)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,122 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<div slot="header">
<span>生日祝福配置</span>
</div>
<el-form ref="configFormRef" :model="configForm" :rules="configRules"
label-width="150px" style="max-width:900px">
<el-form-item label="页面背景图片" prop="picUrl">
<file-upload :value.sync="configForm.picUrl" :upload_number="1" upload_mode="image"
accept=".jpg,.jpeg,.png" upload_result_category="interval"></file-upload>
<div class="text-muted">建议使用竖版生日主题图片。</div>
</el-form-item>
<el-form-item label="福利贺卡图片" prop="birthdayUrl">
<file-upload :value.sync="configForm.birthdayUrl" :upload_number="1" upload_mode="image"
accept=".jpg,.jpeg,.png" upload_result_category="interval"></file-upload>
<div class="text-muted">移动端点击“领取生日福利”后展示该图片。</div>
</el-form-item>
<el-form-item label="默认查询天数" prop="birthdayDays">
<el-input-number v-model="configForm.birthdayDays" :min="1"
controls-position="right"></el-input-number>
<span class="text-muted ml10">生日人员页未选择日期时,默认查询今天起未来N天。</span>
</el-form-item>
<!-- <el-form-item label="自动发送" prop="autoSendEnabled">
<el-switch v-model="configForm.autoSendEnabled"
active-text="开启" inactive-text="关闭"></el-switch>
</el-form-item>-->
<el-form-item label="发送标题" prop="sendTitle">
<el-input v-model="configForm.sendTitle" maxlength="255" show-word-limit
placeholder="请输入发送标题"></el-input>
</el-form-item>
<el-form-item label="发送内容" prop="sendContent">
<el-input v-model="configForm.sendContent" type="textarea" :rows="6"
placeholder="请输入发送内容"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="formLoading" @click="saveConfig">保存配置</el-button>
<el-button @click="previewBirthday">预览生日页面</el-button>
</el-form-item>
</el-form>
</el-card>
</guava>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
formLoading: false,
configForm: {
id: "",
picUrl: "",
birthdayUrl: "",
birthdayDays: 15,
autoSendEnabled: true,
sendTitle: "请点击查收您的生日福利",
sendContent: "校工会祝您:生日快乐!幸福安康"
},
configRules: {
birthdayDays: [
{required:true,message:"请输入默认查询天数",trigger:["blur","change"]}
],
sendTitle: [
{required:true,message:"请输入发送标题",trigger:["blur","change"]}
],
sendContent: [
{required:true,message:"请输入发送内容",trigger:["blur","change"]}
]
}
}
},
methods: {
loadConfig() {
this.$set(this, "formLoading", true)
this.$axios.post(loc() + "/getConfig").then((res) => {
if (res.code === 0) {
this.$set(this, "configForm", Object.assign({}, this.configForm, res.data || {}))
} else {
this.$message.warning(res.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
},
saveConfig() {
this.$refs.configFormRef.validate((valid) => {
if (!valid) return
this.$confirm("确定保存生日祝福配置吗?", "提示", {type:"warning"}).then(() => {
this.$set(this, "formLoading", true)
this.$axios.post(loc() + "/saveConfig", this.configForm).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.$set(this, "configForm", res.data)
} else {
this.$message.warning(res.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
}).catch(() => {})
})
},
previewBirthday() {
let url = "/platform/staffManage/birthday/manage/h5"
if (this.configForm.birthdayUrl) {
url = url + "?id=" + encodeURIComponent(this.configForm.birthdayUrl)
}
window.open(url, "_blank")
}
},
created() {
this.loadConfig()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,343 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="searchBirthday">
<search-item label="姓名/工号">
<el-input v-model="pageForm.searchKeyword" clearable placeholder="请输入姓名或工号"></el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会"
style="width:100%" @change="handleUnionChange" @clear="handleUnionChange">
<el-option v-for="item in unions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="所属单位">
<el-select v-model="pageForm.unitId" clearable filterable placeholder="请选择所属单位" style="width:100%">
<el-option v-for="item in units" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="在职状态">
<dict-select v-model="queryForm.userStates" code="USER_STATE" multiple placeholder="请选择在职状态"></dict-select>
</search-item>
<search-item label="人员类型">
<dict-select v-model="queryForm.personTypes" code="USER_PERSON_TYPE" multiple placeholder="请选择人员类型"></dict-select>
</search-item>
<search-item label="人员性质">
<dict-select v-model="queryForm.preparedBys" code="PREPARED_BY" multiple placeholder="请选择人员性质"></dict-select>
</search-item>
<search-item label="生日日期">
<el-date-picker v-model="birthdayRange" type="daterange" value-format="yyyy-MM-dd"
range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
style="width:100%" @change="handleBirthdayRangeChange"></el-date-picker>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool :label="'生日人员(默认展示未来' + birthdayDays + '天,日期查询忽略年份)'"
:columns.sync="tableColumns">
<el-button size="small" type="primary" @click="doExport">导出Excel</el-button>
<el-button size="small" type="primary" @click="openSendByQuery">发送通知</el-button>
<el-button size="small" type="primary" @click="openConfig">配置图片</el-button>
</table-tool>
<el-table ref="table" v-loading="tableLoading" :data="tableData" :size="tableSize"
row-key="id" style="width:100%" @sort-change="pageOrder">
<el-table-column :index="indexMethod" label="序号" type="index" width="70" align="center"></el-table-column>
<el-table-column v-for="column in tableColumns" v-if="column.visible !== false"
:key="column.prop" :prop="column.prop"
:label="column.label" :width="column.width" :fixed="column.fixed"
:sortable="column.sortable"
header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column fixed="right" label="操作" width="210" align="center">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="openView(scope.row)">人员信息</el-button>
<el-button size="mini" type="primary" @click="openSendByUser(scope.row)">发送通知</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #view>
<member-info ref="memberInfoRef"></member-info>
</template>
</guava>
<el-dialog title="按查询条件发送生日通知" :visible.sync="sendQueryDialogVisible"
:close-on-click-modal="false" width="50%" @close="closeSendByQuery">
<el-form ref="sendQueryFormRef" :model="sendQueryForm" label-width="90px">
<el-form-item label="发送标题" prop="title"
:rules="[{required:true,message:'请输入发送标题',trigger:['blur','change']}]">
<el-input v-model="sendQueryForm.title" maxlength="255" placeholder="请输入发送标题"></el-input>
</el-form-item>
<el-form-item label="发送内容" prop="content"
:rules="[{required:true,message:'请输入发送内容',trigger:['blur','change']}]">
<el-input v-model="sendQueryForm.content" type="textarea" :rows="6" placeholder="请输入发送内容"></el-input>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="closeSendByQuery">取消</el-button>
<el-button type="primary" :loading="formLoading" @click="sendByQuery">确定</el-button>
</span>
</el-dialog>
<el-dialog title="向单个用户发送生日通知" :visible.sync="sendUserDialogVisible"
:close-on-click-modal="false" width="50%" @close="closeSendByUser">
<el-form ref="sendUserFormRef" :model="sendUserForm" label-width="90px">
<el-form-item label="发送对象">
<el-input v-model="sendUserForm.userInfo" readonly></el-input>
</el-form-item>
<el-form-item label="发送标题" prop="title"
:rules="[{required:true,message:'请输入发送标题',trigger:['blur','change']}]">
<el-input v-model="sendUserForm.title" maxlength="255" placeholder="请输入发送标题"></el-input>
</el-form-item>
<el-form-item label="发送内容" prop="content"
:rules="[{required:true,message:'请输入发送内容',trigger:['blur','change']}]">
<el-input v-model="sendUserForm.content" type="textarea" :rows="6" placeholder="请输入发送内容"></el-input>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="closeSendByUser">取消</el-button>
<el-button type="primary" :loading="formLoading" @click="sendByUser">确定</el-button>
</span>
</el-dialog>
<el-dialog title="生日祝福页面图片配置" :visible.sync="configDialogVisible"
:close-on-click-modal="false" width="50%" @close="closeConfig">
<el-form ref="configFormRef" :model="configForm" label-width="110px">
<el-form-item label="页面背景图片" prop="picUrl">
<file-upload :value.sync="configForm.picUrl" :upload_number="1" upload_mode="image"
accept=".jpg,.jpeg,.png" upload_result_category="interval"></file-upload>
<div class="text-muted">建议使用竖版生日主题图片。</div>
</el-form-item>
<el-form-item label="福利贺卡图片" prop="birthdayUrl">
<file-upload :value.sync="configForm.birthdayUrl" :upload_number="1" upload_mode="image"
accept=".jpg,.jpeg,.png" upload_result_category="interval"></file-upload>
<div class="text-muted">移动端点击“领取生日福利”后展示该图片。</div>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="closeConfig">取消</el-button>
<el-button type="primary" :loading="formLoading" @click="saveConfig">保存</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
<!--#include("/platform/zhgh/staffmanage/member/common/info/memberInfo.js"){}#-->
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
data() {
return {
pageForm: {
searchKeyword: "",
unionId: "",
unitId: "",
userStates: "[]",
personTypes: "[]",
preparedBys: "[]",
startDate: "",
endDate: "",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
},
queryForm: {userStates:[],personTypes:[],preparedBys:[]},
birthdayRange: [],
unions: [],
units: [],
tableColumns: [
{prop:"loginname",label:"工号",sortable:true,width:"120"},
{prop:"username",label:"姓名",sortable:true,width:"110"},
{prop:"sex",label:"性别",width:"70"},
{prop:"mobile",label:"联系方式",width:"130"},
{prop:"birthday",label:"出生日期",sortable:true,width:"120"},
{prop:"daysUntilBirthday",label:"生日倒计时(天)",sortable:true,width:"150"},
{prop:"userState",label:"在职状态",width:"100"},
{prop:"personType",label:"人员类型",width:"130"},
{prop:"preparedBy",label:"人员性质",width:"130"},
{prop:"unionName",label:"所属工会",width:"180"},
{prop:"unitName",label:"所属单位",width:"180"}
],
sendQueryDialogVisible: false,
sendQueryForm: {title:"",content:""},
sendUserDialogVisible: false,
sendUserForm: {userId:"",userInfo:"",title:"",content:""},
birthdayDays: 15,
defaultMsgConfig: {title:"",content:""},
configDialogVisible: false,
configForm: {id:"",picUrl:"",birthdayUrl:""}
}
},
components: {
"member-info": MEMBER_INFO
},
methods: {
initOrganizationOptions() {
if (this.$auth.hasRoleOr("SYSADMIN, SCHOOL_UNION_ADMIN, SCHOOL_UNION_MEMBER_ADMIN")) {
this.$businessTool.listUnion().then((data) => {
this.$set(this, "unions", data || [])
})
this.$businessTool.listUnit().then((data) => {
this.$set(this, "units", data || [])
})
} else {
const unionId = this.$store.state.user.union.id
this.$businessTool.listUnion(unionId).then((data) => {
this.$set(this, "unions", data || [])
})
this.$businessTool.listUnit(unionId).then((data) => {
this.$set(this, "units", data || [])
})
}
},
handleUnionChange() {
this.$set(this.pageForm, "unitId", "")
this.$set(this, "units", [])
if (this.pageForm.unionId) {
this.$businessTool.listUnit(this.pageForm.unionId).then((data) => {
this.$set(this, "units", data || [])
})
}
},
handleBirthdayRangeChange(value) {
this.$set(this.pageForm, "startDate", value && value.length ? value[0] : "")
this.$set(this.pageForm, "endDate", value && value.length ? value[1] : "")
this.searchBirthday()
},
searchBirthday() {
this.$set(this.pageForm, "userStates", JSON.stringify(this.queryForm.userStates || []))
this.$set(this.pageForm, "personTypes", JSON.stringify(this.queryForm.personTypes || []))
this.$set(this.pageForm, "preparedBys", JSON.stringify(this.queryForm.preparedBys || []))
this.doSearch()
},
requestParams() {
return clone(this.pageForm)
},
doExport() {
this.$downLoad(loc() + "/doExport", this.requestParams())
},
openView(row) {
this.$refs.guava.view()
this.$nextTick(() => {
this.$refs.memberInfoRef.onOpen(row.id)
})
},
openSendByQuery() {
this.$set(this, "sendQueryForm", Object.assign(this.requestParams(), {title:this.defaultMsgConfig.title,content:this.defaultMsgConfig.content}))
this.$set(this, "sendQueryDialogVisible", true)
},
closeSendByQuery() {
this.$set(this, "sendQueryDialogVisible", false)
this.$nextTick(() => {
if (this.$refs.sendQueryFormRef) this.$refs.sendQueryFormRef.clearValidate()
})
},
sendByQuery() {
this.$refs.sendQueryFormRef.validate((valid) => {
if (!valid) return
this.$confirm("确定按当前查询条件发送生日通知吗?", "提示", {type:"warning"}).then(() => {
this.$set(this, "formLoading", true)
this.$axios.post(loc() + "/sendMsgByQueryUsers", this.sendQueryForm).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.closeSendByQuery()
} else {
this.$message.warning(res.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
}).catch(() => {})
})
},
openSendByUser(row) {
this.$set(this, "sendUserForm", {userId:row.id,userInfo:row.username + "(" + row.loginname + ")",title:this.defaultMsgConfig.title,content:this.defaultMsgConfig.content})
this.$set(this, "sendUserDialogVisible", true)
},
closeSendByUser() {
this.$set(this, "sendUserDialogVisible", false)
this.$nextTick(() => {
if (this.$refs.sendUserFormRef) this.$refs.sendUserFormRef.clearValidate()
})
},
sendByUser() {
this.$refs.sendUserFormRef.validate((valid) => {
if (!valid) return
this.$confirm("确定向" + this.sendUserForm.userInfo + "发送生日通知吗?", "提示", {type:"warning"}).then(() => {
this.$set(this, "formLoading", true)
this.$axios.post(loc() + "/sendMsgByUser", this.sendUserForm).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.closeSendByUser()
} else {
this.$message.warning(res.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
}).catch(() => {})
})
},
loadBirthdayConfig() {
this.$axios.post(loc() + "/getConfig").then((res) => {
if (res.code !== 0 || !res.data) return
this.$set(this, "birthdayDays", res.data.birthdayDays || 15)
this.$set(this, "defaultMsgConfig", {
title:res.data.sendTitle || "",
content:res.data.sendContent || ""
})
})
},
openConfig() {
this.$set(this, "formLoading", true)
this.$axios.post(loc() + "/getConfig").then((res) => {
if (res.code === 0) {
this.$set(this, "configForm", Object.assign({id:"",picUrl:"",birthdayUrl:""}, res.data || {}))
this.$set(this, "configDialogVisible", true)
} else {
this.$message.warning(res.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
},
closeConfig() {
this.$set(this, "configDialogVisible", false)
},
saveConfig() {
this.$confirm("确定保存生日祝福页面配置吗?", "提示", {type:"warning"}).then(() => {
this.$set(this, "formLoading", true)
this.$axios.post(loc() + "/saveOrModifyConfig", this.configForm).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.$set(this, "configForm", res.data)
this.closeConfig()
} else {
this.$message.warning(res.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
}).catch(() => {})
}
},
created() {
this.initOrganizationOptions()
this.loadBirthdayConfig()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,112 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号">
<el-input v-model="pageForm.searchKeyword" clearable placeholder="请输入姓名或工号"></el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会"
style="width:100%" @change="handleUnionChange" @clear="handleUnionChange">
<el-option v-for="item in unions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="所属单位">
<el-select v-model="pageForm.unitId" clearable filterable placeholder="请选择所属单位" style="width:100%">
<el-option v-for="item in units" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="生日消息记录" :columns.sync="tableColumns"></table-tool>
<el-table ref="table" v-loading="tableLoading" :data="tableData" :size="tableSize"
row-key="id" style="width:100%" @sort-change="pageOrder">
<el-table-column :index="indexMethod" label="序号" type="index" width="70" align="center"></el-table-column>
<el-table-column v-for="column in tableColumns" v-if="column.visible !== false"
:key="column.prop" :prop="column.prop"
:label="column.label" :width="column.width" :fixed="column.fixed"
:sortable="column.sortable"
header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
data() {
return {
pageForm: {
searchKeyword: "",
unionId: "",
unitId: "",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
},
unions: [],
units: [],
tableColumns: [
{prop:"loginname",label:"工号",sortable:true,width:"120"},
{prop:"username",label:"姓名",sortable:true,width:"110"},
{prop:"unionName",label:"所属工会",width:"180"},
{prop:"unitName",label:"所属单位",width:"180"},
{prop:"msgTitle",label:"消息标题",width:"220"},
{prop:"msgContent",label:"消息内容",width:"260"},
{prop:"pushByName",label:"推送人",width:"120"},
{prop:"pushTime",label:"推送时间",sortable:true,width:"170"},
{prop:"pushType",label:"推送类型"}
]
}
},
methods: {
initOrganizationOptions() {
if (this.$auth.hasRoleOr("SYSADMIN, SCHOOL_UNION_ADMIN, SCHOOL_UNION_MEMBER_ADMIN")) {
this.$businessTool.listUnion().then((data) => {
this.$set(this, "unions", data || [])
})
this.$businessTool.listUnit().then((data) => {
this.$set(this, "units", data || [])
})
} else {
const unionId = this.$store.state.user.union.id
this.$businessTool.listUnion(unionId).then((data) => {
this.$set(this, "unions", data || [])
})
this.$businessTool.listUnit(unionId).then((data) => {
this.$set(this, "units", data || [])
})
}
},
handleUnionChange() {
this.$set(this.pageForm, "unitId", "")
this.$set(this, "units", [])
if (this.pageForm.unionId) {
this.$businessTool.listUnit(this.pageForm.unionId).then((data) => {
this.$set(this, "units", data || [])
})
}
}
},
created() {
this.initOrganizationOptions()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -72,7 +72,7 @@ layout("/layouts/platform_h5.html"){
</style>
<div id="app" v-cloak>
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<van-nav-bar title="知识竞答" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<van-sticky offset-top="46px">
<!--开启计时 未完成 活动未结束-->
@@ -156,7 +156,7 @@ layout("/layouts/platform_h5.html"){
</style>
<div id="app" v-cloak>
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<van-nav-bar title="知识竞答" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<van-sticky offset-top="46px">
<h2 class="title">{{ activity.title }}</h2>
@@ -83,7 +83,7 @@ layout("/layouts/platform_h5.html"){
</style>
<div id="app" v-cloak>
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<van-nav-bar title="知识竞答" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<h2 class="title">{{ activity.title }}</h2>
<div v-if="!isFinished">
@@ -19,7 +19,7 @@ layout("/layouts/platform_h5.html"){
class="honor-tab"
:class="{ active: activeTab === 'labor' }"
@click="switchTab('labor')">
劳模风采
先进风采
</button>
<button
type="button"
@@ -103,7 +103,7 @@ layout("/layouts/platform_h5.html"){
getCategoryPageTitle(category, defaultTitle) {
const titleMap = {
QUIZ: "在线答题",
SURVEY: "问卷调查",
SURVEY: "知识竞答",
VOTE: "投票"
}
return titleMap[category] || defaultTitle
@@ -302,7 +302,7 @@ layout("/layouts/platform_h5.html"){
})
},
pageTitle() {
return this.getCategoryPageTitle(this.activity && this.activity.category, "问卷调查")
return this.getCategoryPageTitle(this.activity && this.activity.category, "知识竞答")
},
heroStyle() {
if (this.activity && this.activity.cover) {
@@ -318,7 +318,7 @@ layout("/layouts/platform_h5.html"){
getCategoryPageTitle(category, defaultTitle) {
const titleMap = {
QUIZ: "在线答题",
SURVEY: "问卷调查",
SURVEY: "知识竞答",
VOTE: "投票"
}
return titleMap[category] || defaultTitle
@@ -206,7 +206,7 @@ layout("/layouts/platform_h5.html"){
getCategoryPageTitle(category, defaultTitle) {
const titleMap = {
QUIZ: "在线答题",
SURVEY: "问卷调查",
SURVEY: "知识竞答",
VOTE: "投票"
}
return titleMap[category] || defaultTitle
@@ -0,0 +1,312 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
html, body, #app { margin: 0; width: 100%; height: 100%; overflow: hidden; }
.birthday-page { position: relative; width: 100%; height: 100vh; overflow: hidden; color: #7f1d1d; background: #e76f45; }
.birthday-background { position: absolute; inset: 0; background-position: center; background-size: cover; z-index: 0; }
.birthday-mask { position: absolute; inset: 0; z-index: 1; background: linear-gradient(180deg, rgba(255,255,255,.02), rgba(92,20,8,.16)); }
.brand { position: absolute; top: calc(12px + env(safe-area-inset-top)); left: 14px; z-index: 8; padding: 7px 12px;
border: 1px solid rgba(255,255,255,.68); border-radius: 16px; color: #fff; background: rgba(128,29,16,.2);
font-size: 13px; font-weight: 600; backdrop-filter: blur(6px); }
.member-tag { position: absolute; top: calc(12px + env(safe-area-inset-top)); right: 14px; z-index: 8; padding: 7px 12px;
border-radius: 16px; color: #8a321c; background: rgba(255,244,213,.9); font-size: 12px; font-weight: 600; }
.animation-zone { position: relative; z-index: 3; height: 52%; padding-top: calc(42px + env(safe-area-inset-top)); }
.birthday-font { position: absolute; top: 8%; left: 4%; width: 92%; height: 47%; }
.birthday-cake { position: absolute; left: 12%; bottom: -7%; width: 76%; height: 64%; }
.confetti { position: absolute; top: -12px; width: 7px; height: 12px; border-radius: 3px; opacity: 0;
animation: confetti-fall linear infinite; }
.confetti:nth-child(3n) { background: #ffe16b; }
.confetti:nth-child(3n+1) { background: #fff4dd; }
.confetti:nth-child(3n+2) { background: #e9473f; }
@keyframes confetti-fall { 0% { transform: translate3d(0,-20px,0) rotate(0); opacity: 0; }
12% { opacity: 1; } 100% { transform: translate3d(35px,62vh,0) rotate(540deg); opacity: 0; } }
.greeting-zone { position: relative; z-index: 4; height: 48%; display: flex; flex-direction: column; align-items: center;
justify-content: flex-start; padding: 0 24px calc(20px + env(safe-area-inset-bottom)); box-sizing: border-box; }
.greeting-card { width: 100%; box-sizing: border-box; padding: 20px 20px 16px; border: 1px solid rgba(164,60,26,.3);
border-radius: 18px; background: rgba(255,249,228,.92); box-shadow: 0 12px 30px rgba(109,29,10,.22);
text-align: center; animation: card-enter .7s ease-out both; }
@keyframes card-enter { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: translateY(0); } }
.greeting-title { margin-bottom: 10px; font-size: 18px; font-weight: 700; text-align: left; }
.greeting-content { font-size: 15px; line-height: 1.8; letter-spacing: 1px; }
.receive-button { width: 190px; margin-top: 18px; padding: 13px 20px; border: 0; border-radius: 25px; color: #fff;
background: linear-gradient(135deg, #d9362b, #9d1717); box-shadow: 0 8px 18px rgba(131,21,17,.34);
font-size: 16px; font-weight: 700; letter-spacing: 2px; }
.music-button { position: absolute; right: 14px; bottom: calc(14px + env(safe-area-inset-bottom)); z-index: 9;
width: 42px; height: 42px; border: 1px solid rgba(255,255,255,.7); border-radius: 50%; color: #fff;
background: rgba(121,27,15,.42); font-size: 20px; }
.music-button.playing { animation: music-rotate 3s linear infinite; }
@keyframes music-rotate { to { transform: rotate(360deg); } }
.music-consent-modal { position: fixed; inset: 0; z-index: 10040; display: flex; align-items: center; justify-content: center;
padding: calc(24px + env(safe-area-inset-top)) 24px calc(24px + env(safe-area-inset-bottom)); box-sizing: border-box;
background: rgba(61,13,8,.76); backdrop-filter: blur(7px); }
.music-consent-card { position: relative; width: 100%; max-width: 390px; padding: 70px 24px 26px; box-sizing: border-box;
border: 1px solid rgba(211,148,57,.72); border-radius: 28px; background: #fff8e8;
box-shadow: 0 22px 60px rgba(56,8,4,.48), inset 0 0 0 5px rgba(255,255,255,.44); text-align: center;
animation: music-dialog-enter .48s cubic-bezier(.2,.85,.32,1.16) both; }
@keyframes music-dialog-enter { from { opacity: 0; transform: translateY(24px) scale(.92); }
to { opacity: 1; transform: translateY(0) scale(1); } }
.music-consent-emblem { position: absolute; top: -45px; left: 50%; display: flex; align-items: center; justify-content: center;
width: 90px; height: 90px; border: 6px solid #fff8e8; border-radius: 50%; color: #fff;
background: #d9362b; box-shadow: 0 10px 24px rgba(144,29,20,.34); transform: translateX(-50%); }
.music-consent-emblem .fa { font-size: 36px; }
.music-consent-title { margin: 0; color: #a51f1a; font-size: 31px; line-height: 1.25; font-weight: 800;
letter-spacing: 2px; }
.music-consent-subtitle { margin: 14px 0 0; color: #8b4b2c; font-size: 16px; line-height: 1.7; }
.music-consent-divider { width: 70%; height: 1px; margin: 20px auto 18px; background: #e7b35b; opacity: .72; }
.music-consent-tip { margin-bottom: 18px; color: #9a5d3a; font-size: 13px; line-height: 1.6; }
.music-consent-button { display: flex; align-items: center; justify-content: center; width: 100%; height: 50px;
border-radius: 25px; font-size: 17px; font-weight: 700; letter-spacing: 1px; outline: none; }
.music-consent-button .fa { margin-right: 9px; font-size: 18px; }
.music-consent-button.primary { border: 0; color: #fff; background: #c92921;
box-shadow: 0 9px 20px rgba(174,35,27,.3); }
.music-consent-button.secondary { margin-top: 13px; border: 1px solid #d99a45; color: #9b542b; background: #fffaf0; }
.music-consent-button:active { transform: scale(.98); }
.music-consent-button:focus-visible, .music-button:focus-visible { outline: 3px solid rgba(255,225,107,.9); outline-offset: 3px; }
.coupon-modal { position: fixed; inset: 0; z-index: 10020; display: flex; align-items: center; justify-content: center;
padding: calc(22px + env(safe-area-inset-top)) 20px calc(22px + env(safe-area-inset-bottom));
box-sizing: border-box; background: rgba(43,10,7,.72); backdrop-filter: blur(5px); }
.coupon-panel { position: relative; width: 100%; max-width: 430px; max-height: 88vh; padding: 12px; box-sizing: border-box;
border-radius: 20px; background: #fff7e6; box-shadow: 0 18px 55px rgba(40,5,0,.45); overflow: auto; }
.coupon-close { position: absolute; top: 18px; right: 18px; z-index: 2; width: 34px; height: 34px; border: 0;
border-radius: 50%; color: #fff; background: rgba(87,17,13,.72); font-size: 24px; line-height: 34px; }
.coupon-image { display: block; width: 100%; border-radius: 14px; }
.coupon-empty { min-height: 390px; padding: 70px 28px 30px; box-sizing: border-box; border: 2px dashed #d59569;
border-radius: 14px; text-align: center; background: linear-gradient(155deg, #fff7df, #ffd9bd); }
.coupon-empty-icon { font-size: 74px; }
.coupon-empty-title { margin-top: 18px; color: #a21e19; font-size: 28px; font-weight: 800; }
.coupon-empty-text { margin-top: 24px; color: #7b3a2a; font-size: 15px; line-height: 1.8; }
.coupon-tip { padding: 10px 4px 2px; color: #7b3a2a; font-size: 12px; line-height: 1.6; text-align: center; }
.loading-cover { position: fixed; inset: 0; z-index: 10030; display: flex; align-items: center; justify-content: center;
color: #fff; background: rgba(76,20,10,.42); font-size: 14px; }
@media (max-width: 375px) {
.animation-zone { height: 48%; }
.greeting-zone { height: 52%; padding-left: 18px; padding-right: 18px; }
.greeting-card { padding: 16px; }
.greeting-content { font-size: 14px; line-height: 1.65; }
.receive-button { margin-top: 12px; }
.music-consent-card { padding: 64px 20px 22px; }
.music-consent-title { font-size: 27px; }
}
@media (prefers-reduced-motion: reduce) {
.music-consent-card, .greeting-card, .confetti, .music-button.playing { animation: none; }
}
</style>
<div id="app">
<div class="birthday-page">
<div class="birthday-background" :style="{backgroundImage:'url(' + config.backgroundUrl + ')'}"></div>
<div class="birthday-mask"></div>
<div class="brand">{{ appName }}</div>
<div class="member-tag">会员专享</div>
<div class="animation-zone">
<div ref="fontAnimation" class="birthday-font"></div>
<div ref="cakeAnimation" class="birthday-cake"></div>
<i v-for="item in confettiItems" :key="item.id" class="confetti" :style="item.style"></i>
</div>
<div class="greeting-zone">
<div class="greeting-card">
<div class="greeting-title">亲爱的 {{ userName }} 老师:</div>
<div class="greeting-content">
步履所至,皆有回响,心之所向,岁月成诗。<br>
在这个特别的日子里,校工会祝您:<br>
生日快乐,幸福安康!
</div>
</div>
<button class="receive-button" @click="openCoupon">领取生日福利</button>
</div>
<button type="button" class="music-button" :class="{playing:musicPlaying}"
:aria-label="musicPlaying ? '暂停音乐' : '播放音乐'" @click="toggleMusic">
<i class="fa fa-music" aria-hidden="true"></i>
</button>
<div v-if="showMusicPrompt" class="music-consent-modal">
<section class="music-consent-card" role="dialog" aria-modal="true" aria-labelledby="birthdayMusicTitle">
<div class="music-consent-emblem" aria-hidden="true">
<i class="fa fa-music"></i>
</div>
<h2 id="birthdayMusicTitle" class="music-consent-title">祝您生日快乐</h2>
<p class="music-consent-subtitle">愿美好与祝福伴随您的每一天</p>
<div class="music-consent-divider"></div>
<div class="music-consent-tip">开启音乐,沉浸感受生日祝福</div>
<button type="button" class="music-consent-button primary" @click="enableBirthdayMusic">
<i class="fa fa-music" aria-hidden="true"></i>
开启音乐
</button>
<button type="button" class="music-consent-button secondary" @click="declineBirthdayMusic">暂不开启</button>
</section>
</div>
<div v-if="showCoupon" class="coupon-modal" @click.self="closeCoupon">
<div class="coupon-panel">
<button class="coupon-close" @click="closeCoupon">×</button>
<img v-if="config.couponImageUrl" class="coupon-image" :src="config.couponImageUrl" alt="生日福利贺卡">
<div v-else class="coupon-empty">
<div class="coupon-empty-icon">🎂</div>
<div class="coupon-empty-title">生日快乐</div>
<div class="coupon-empty-text">校工会为您送上诚挚祝福。<br>福利贺卡尚未配置,请联系工作人员。</div>
</div>
<div class="coupon-tip">请按福利贺卡上的说明领取和使用生日福利</div>
</div>
</div>
<div v-if="pageLoading" class="loading-cover">生日祝福加载中...</div>
</div>
</div>
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js"></script>
<script src="${base!}/assets/platform/plugins/lottie/lottie.min.js"></script>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
appName: "${AppName!}",
userName: "${userName!}",
pageLoading: false,
showMusicPrompt: false,
musicPromptResolved: false,
showCoupon: false,
musicPlaying: false,
popupHistoryToken: "",
fontAnimation: null,
cakeAnimation: null,
audio: null,
config: {
backgroundUrl: "${base!}/assets/mobile/img/birthday/custom-bg.png",
couponImageUrl: "",
musicUrl: "${base!}/assets/mobile/music/birthday/birthdaySong.mp3",
fontAnimationUrl: "${base!}/assets/mobile/lottie/birthday/happyBirthday.json",
cakeAnimationUrl: "${base!}/assets/mobile/lottie/birthday/lihua.json"
},
confettiItems: Array.from({length:28}, (value, index) => ({
id:index,
style:{
left:(index * 37 % 100) + "%",
animationDelay:((index * 13 % 30) / 10) + "s",
animationDuration:(3 + (index % 5) * .45) + "s"
}
}))
}
},
methods: {
resolveFileUrl(value) {
if (!value) return ""
if (value.indexOf("http://") === 0 || value.indexOf("https://") === 0 || value.indexOf("/") === 0) return value
return "/platform/sys/file/download?id=" + encodeURIComponent(value)
},
loadConfig() {
this.$set(this, "pageLoading", true)
this.$axios.post("/platform/staffManage/birthday/manage/getConfig").then((res) => {
if (res.code !== 0 || !res.data) return
if (res.data.picUrl) this.$set(this.config, "backgroundUrl", this.resolveFileUrl(res.data.picUrl))
const linkFileId = "${fileId!}"
const couponFile = linkFileId || res.data.birthdayUrl
this.$set(this.config, "couponImageUrl", this.resolveFileUrl(couponFile))
}).finally(() => {
this.$set(this, "pageLoading", false)
})
},
loadAnimations() {
if (window.lottie && this.$refs.fontAnimation) {
this.$set(this, "fontAnimation", window.lottie.loadAnimation({
container:this.$refs.fontAnimation,
renderer:"svg",
loop:true,
autoplay:true,
path:this.config.fontAnimationUrl
}))
}
if (window.lottie && this.$refs.cakeAnimation) {
this.$set(this, "cakeAnimation", window.lottie.loadAnimation({
container:this.$refs.cakeAnimation,
renderer:"svg",
loop:true,
autoplay:true,
path:this.config.cakeAnimationUrl
}))
}
},
openCoupon() {
if (this.showCoupon) return
this.$set(this, "showCoupon", true)
window.h5PopupHistory.open(this.popupHistoryToken, "birthdayCoupon")
},
closeCoupon() {
const historyHandled = window.h5PopupHistory.close(this.popupHistoryToken, "birthdayCoupon")
if (!historyHandled) this.closeCouponState()
},
closeCouponState() {
this.$set(this, "showCoupon", false)
},
openMusicPrompt() {
if (this.showMusicPrompt || this.musicPromptResolved) return
this.$set(this, "showMusicPrompt", true)
window.h5PopupHistory.open(this.popupHistoryToken, "birthdayMusicPrompt")
},
closeMusicPrompt() {
if (!this.showMusicPrompt) return
// 先同步关闭界面,再回退弹框历史,避免部分 WebView 延迟 popstate 导致弹窗无法关闭。
this.closeMusicPromptState()
window.h5PopupHistory.close(this.popupHistoryToken, "birthdayMusicPrompt")
},
closeMusicPromptState() {
this.$set(this, "showMusicPrompt", false)
this.$set(this, "musicPromptResolved", true)
},
enableBirthdayMusic() {
if (!this.showMusicPrompt) return
this.closeMusicPrompt()
this.playMusic()
},
declineBirthdayMusic() {
if (!this.showMusicPrompt) return
this.closeMusicPrompt()
},
playMusic() {
if (!this.audio) this.$set(this, "audio", new Audio(this.config.musicUrl))
this.audio.loop = true
this.audio.volume = .3
this.audio.play().then(() => {
this.$set(this, "musicPlaying", true)
}).catch(() => {
this.$set(this, "musicPlaying", false)
})
},
toggleMusic() {
if (this.musicPlaying && this.audio) {
this.audio.pause()
this.$set(this, "musicPlaying", false)
return
}
this.playMusic()
}
},
mounted() {
this.$set(this, "popupHistoryToken", window.h5PopupHistory.register((popupKey) => {
if (popupKey === "birthdayCoupon") this.closeCouponState()
if (popupKey === "birthdayMusicPrompt") this.closeMusicPromptState()
}))
this.loadConfig()
this.loadAnimations()
this.$nextTick(() => {
this.openMusicPrompt()
})
},
beforeDestroy() {
window.h5PopupHistory.unregister(this.popupHistoryToken)
if (this.fontAnimation) this.fontAnimation.destroy()
if (this.cakeAnimation) this.cakeAnimation.destroy()
if (this.audio) {
this.audio.pause()
this.audio.src = ""
}
}
})
</script>
<!--#
}
#-->
@@ -126,19 +126,12 @@ const home = {
showLatestActivity: false,
featuredEntries: [
{
name: "精彩活动",
desc: "更多精彩活动",
icon: "fire-o",
className: "feature-card--activity",
name: "幸福工荟",
desc: "浏览普惠活动资讯",
icon: "bullhorn-o",
className: "feature-card--inclusive",
large: true,
href: "/platform/h5/featuredActivity"
},
{
name: "节日福利",
desc: "领取节日福利",
icon: "gift-o",
className: "feature-card--benefit",
href: "/platform/h5/festivalBenefit"
href: "/platform/h5/inclusive"
},
{
name: "劳模先进",
@@ -148,11 +141,18 @@ const home = {
href: "/platform/honor/manage/h5"
},
{
name: "普惠信息",
desc: "普惠服务信息",
icon: "bullhorn-o",
className: "feature-card--inclusive",
href: ""
name: "节日福利",
desc: "领取节日福利",
icon: "gift-o",
className: "feature-card--benefit",
href: "/platform/h5/festivalBenefit"
},
{
name: "精彩活动",
desc: "更多精彩活动",
icon: "fire-o",
className: "feature-card--activity",
href: "/platform/h5/featuredActivity"
}
],
appLogo: "",
@@ -589,7 +589,7 @@ const home = {
.quick-scroll-grid {
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(2, 82px);
grid-template-rows: 82px;
grid-auto-columns: 25%;
min-width: 100%;
}
@@ -745,6 +745,7 @@ const home = {
.feature-card--activity {
background: linear-gradient(145deg, #fff2e5 0%, #ffe2c8 100%);
color: #ff7a25;
grid-column: span 2;
}
.feature-card--benefit {
@@ -758,7 +759,6 @@ const home = {
}
.feature-card--inclusive {
grid-column: span 2;
background: linear-gradient(145deg, #f2eeff 0%, #ded4ff 100%);
color: #8063dc;
}