生日祝福对接

This commit is contained in:
2026-11-09 08:33:35 +08:00
parent aae28a6b4b
commit 665ed1f582
9 changed files with 569 additions and 37 deletions
@@ -80,9 +80,6 @@ public class SmsServiceImpl implements SmsService {
log.error("【log】短信配置未开启");
throw new RuntimeException("短信配置未开启");
}
if (!conf.getBoolean("msg.platform.enabled", false)) {
throw new RuntimeException("短信配置未开启");
}
if (StrUtil.isBlank(loginname) || StrUtil.isBlank(content)) {
log.info("发送失败,缺少需要参数请检查!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
@@ -18,17 +18,10 @@ import java.util.List;
@IocBean(args = {"refer:dao"})
public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> implements ClubUserJoinService {
private static final List<String> ALLOW_JOIN_PERSON_TYPES = List.of(
"教职工",
"未起薪人员",
"教职工新入会",
"教职工其他",
"劳动服务公司",
"后勤聘用人员",
"退休人员[一]",
"退休人员[二]",
"校聘劳务派遣全额"
);
/**
* 在职状态为在岗的口径,与项目其他模块保持一致。
*/
private static final List<String> ON_JOB_USER_STATES = List.of("在职", "在岗");
public ClubUserJoinServiceImpl(Dao dao) {
super(dao);
@@ -43,7 +36,7 @@ public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> impl
if (user.getMember() == null || user.getMember() != 1) {
return "抱歉,您不满足入会的条件!";
}
if (StrUtil.isBlank(user.getPersonType()) || !ALLOW_JOIN_PERSON_TYPES.contains(user.getPersonType())) {
if (StrUtil.isBlank(user.getUserState()) || !ON_JOB_USER_STATES.contains(user.getUserState())) {
return "抱歉,您不满足入会的条件!";
}
return null;
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.inclusive.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.inclusive.model.InclusiveBenefit;
@@ -45,6 +46,87 @@ public class InclusiveBenefitController {
return Result.success(inclusiveBenefitService.findVO(id, false));
}
/**
* 新增幸福工荟标签。
* @param name 标签显示名称
* @param code 标签编码,最多 50 个字符
* @return Result,code 为 0 表示成功,msg 为处理提示,data 为空
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"inclusive.benefit", "sys.manager.dict.add"}, mode = SaMode.AND)
public Result addTagType(@Param("name") String name, @Param("code") String code) {
if (StrUtil.isBlank(name) || StrUtil.isBlank(code)) {
return Result.error("标签名称和编码不能为空");
}
inclusiveBenefitService.addTagType(name, code);
return Result.success();
}
/**
* 修改标签名称,编码保持不变。
* @param id 标签字典项 ID
* @param name 新的标签显示名称
* @return Result,code 为 0 表示成功,msg 为处理提示,data 为空
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"inclusive.benefit", "sys.manager.dict.edit"}, mode = SaMode.AND)
public Result updateTagTypeName(@Param("id") String id, @Param("name") String name) {
if (StrUtil.isBlank(id) || StrUtil.isBlank(name)) {
return Result.error("标签 ID 和名称不能为空");
}
inclusiveBenefitService.updateTagTypeName(id, name);
return Result.success();
}
/**
* 启用或停用标签。
* @param id 标签字典项 ID
* @param disabled true 表示停用,false 表示启用
* @return Result,code 为 0 表示成功,msg 为处理提示,data 为空
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"inclusive.benefit", "sys.manager.dict.edit"}, mode = SaMode.AND)
public Result updateTagTypeDisabled(@Param("id") String id, @Param("disabled") Boolean disabled) {
if (StrUtil.isBlank(id) || disabled == null) {
return Result.error("标签 ID 和启用状态不能为空");
}
inclusiveBenefitService.updateTagTypeDisabled(id, disabled);
return Result.success();
}
/**
* 查询标签的引用数量,用于删除前提示。
* @param id 标签字典项 ID
* @return Result,data 为引用该标签编码的记录数量,包含已删除记录
*/
@At
@SaCheckPermission(value = {"inclusive.benefit", "sys.manager.dict.delete"}, mode = SaMode.AND)
public Result countTagTypeUsage(@Param("id") String id) {
if (StrUtil.isBlank(id)) {
return Result.error("标签 ID 不能为空");
}
return Result.success(inclusiveBenefitService.countTagTypeUsage(id));
}
/**
* 删除标签字典项;前端需先提示已引用记录的影响。
* @param id 标签字典项 ID
* @return Result,code 为 0 表示成功,msg 为处理提示,data 为空
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"inclusive.benefit", "sys.manager.dict.delete"}, mode = SaMode.AND)
public Result deleteTagType(@Param("id") String id) {
if (StrUtil.isBlank(id)) {
return Result.error("标签 ID 不能为空");
}
inclusiveBenefitService.deleteTagType(id);
return Result.success();
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("inclusive.benefit")
@@ -20,6 +20,40 @@ public interface InclusiveBenefitService extends BaseService<InclusiveBenefit> {
List<NutMap> tagOptions();
/**
* 在幸福工荟标签类型字典下新增启用的标签。
* @param name 标签显示名称
* @param code 标签编码,供幸福工荟记录保存
*/
void addTagType(String name, String code);
/**
* 修改标签显示名称,保留已有编码和业务记录的关联。
* @param id 标签字典项 ID
* @param name 新的显示名称
*/
void updateTagTypeName(String id, String name);
/**
* 切换标签是否停用;停用标签不再供新内容选择。
* @param id 标签字典项 ID
* @param disabled true 表示停用,false 表示启用
*/
void updateTagTypeDisabled(String id, boolean disabled);
/**
* 统计引用标签编码的幸福工荟记录,包含已删除记录。
* @param id 标签字典项 ID
* @return 引用该标签的记录数量
*/
int countTagTypeUsage(String id);
/**
* 删除指定标签字典项。
* @param id 标签字典项 ID
*/
void deleteTagType(String id);
void saveBenefit(InclusiveBenefit benefit, Integer status);
void updateStatus(String id, Integer status);
@@ -2,16 +2,21 @@ package com.budwk.app.zhgh.inclusive.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.zhgh.inclusive.model.InclusiveBenefit;
import com.budwk.app.zhgh.inclusive.param.InclusiveBenefitPageForm;
import com.budwk.app.zhgh.inclusive.service.InclusiveBenefitService;
import com.budwk.app.zhgh.inclusive.vo.InclusiveBenefitVO;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -29,6 +34,9 @@ public class InclusiveBenefitServiceImpl extends BaseServiceImpl<InclusiveBenefi
private static final String TAG_DICT_CODE = "INCLUSIVE_BENEFIT_TYPE";
@Inject
private SysDictService sysDictService;
public InclusiveBenefitServiceImpl(Dao dao) {
super(dao);
}
@@ -76,6 +84,105 @@ public class InclusiveBenefitServiceImpl extends BaseServiceImpl<InclusiveBenefi
.collect(Collectors.toList());
}
/** 名称为标签显示文本,编码用于关联幸福工荟记录;只允许新增至指定父字典。 */
@Override
@Aop(TransAop.READ_COMMITTED)
public void addTagType(String name, String code) {
String tagName = StrUtil.trim(name);
String tagCode = StrUtil.trim(code);
if (StrUtil.isBlank(tagName) || tagName.length() > 100) {
throw new BaseException("标签名称不能为空且不能超过 100 个字符");
}
if (StrUtil.isBlank(tagCode) || tagCode.length() > 50) {
throw new BaseException("标签编码不能为空且不能超过 50 个字符");
}
Sys_dict parent = requireTagTypeParent();
// 公共字典按编码查找节点,编码重复会使其他字典或当前标签指向错误节点。
if (sysDictService.count(Cnd.where("code", "=", tagCode)) > 0) {
throw new BaseException("标签编码已存在,请使用其他编码");
}
Sys_dict tag = new Sys_dict();
tag.setName(tagName);
tag.setCode(tagCode);
tag.setDisabled(false);
sysDictService.save(tag, parent.getId());
sysDictService.clearCache();
}
/** 只修改名称,保留编码以免已有幸福工荟记录失去标签关联。 */
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateTagTypeName(String id, String name) {
String tagName = StrUtil.trim(name);
if (StrUtil.isBlank(tagName) || tagName.length() > 100) {
throw new BaseException("标签名称不能为空且不能超过 100 个字符");
}
Sys_dict tag = requireTagType(id);
sysDictService.update(Chain.make("name", tagName), Cnd.where("id", "=", tag.getId())
.and("parentId", "=", tag.getParentId()));
sysDictService.clearCache();
}
/** 停用只影响后续可选项,历史记录仍通过原编码读取标签名称。 */
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateTagTypeDisabled(String id, boolean disabled) {
Sys_dict tag = requireTagType(id);
sysDictService.update(Chain.make("disabled", disabled), Cnd.where("id", "=", tag.getId())
.and("parentId", "=", tag.getParentId()));
sysDictService.clearCache();
}
/** 返回引用该标签的记录数,包括逻辑删除的记录,供删除前告知操作人。 */
@Override
public int countTagTypeUsage(String id) {
Sys_dict tag = requireTagType(id);
return count(Cnd.where("tagType", "=", tag.getCode()));
}
/** 删除前确认标签属于当前字典,避免级联删除其他子字典。 */
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteTagType(String id) {
Sys_dict tag = requireTagType(id);
if (sysDictService.count(Cnd.where("parentId", "=", tag.getId())) > 0) {
throw new BaseException("该标签下存在子字典,请先在字典管理中处理");
}
// 标签只删除自身,不能根据路径级联删除其他字典项。
int deleted = dao().clear(Sys_dict.class, Cnd.where("id", "=", tag.getId())
.and("parentId", "=", tag.getParentId()));
if (deleted == 0) {
throw new BaseException("标签不存在或已被删除");
}
if (sysDictService.count(Cnd.where("parentId", "=", tag.getParentId())) == 0) {
sysDictService.update(Chain.make("hasChildren", false), Cnd.where("id", "=", tag.getParentId()));
}
sysDictService.clearCache();
}
/** 找不到父字典时明确提示,避免把标签误加到其他字典。 */
private Sys_dict requireTagTypeParent() {
Sys_dict parent = sysDictService.fetch(Cnd.where("code", "=", TAG_DICT_CODE).and("delFlag", "=", false));
if (parent == null) {
throw new BaseException("标签类型父字典不存在,请先在系统字典中维护");
}
return parent;
}
/** 根据 ID 校验标签必须是当前父字典的直接子项。 */
private Sys_dict requireTagType(String id) {
if (StrUtil.isBlank(id)) {
throw new BaseException("标签 ID 不能为空");
}
Sys_dict parent = requireTagTypeParent();
Sys_dict tag = sysDictService.fetch(Cnd.where("id", "=", id)
.and("parentId", "=", parent.getId()).and("delFlag", "=", false));
if (tag == null) {
throw new BaseException("标签不存在或不属于幸福工荟标签类型");
}
return tag;
}
@Override
public void saveBenefit(InclusiveBenefit benefit, Integer status) {
InclusiveBenefit target;
@@ -154,8 +261,14 @@ public class InclusiveBenefitServiceImpl extends BaseServiceImpl<InclusiveBenefi
}
private Map<String, String> getTagNameMap() {
List<NutMap> options = tagOptions();
return options.stream().collect(Collectors.toMap(option -> option.getString("value"), option -> option.getString("text")));
Sys_dict parent = dao().fetch(Sys_dict.class, Cnd.where("code", "=", TAG_DICT_CODE).and("delFlag", "=", false));
if (parent == null) {
return Collections.emptyMap();
}
// 历史记录仍需展示已停用标签;仅用于下拉选择的 tagOptions 保持过滤停用项。
return dao().query(Sys_dict.class, Cnd.where("parentId", "=", parent.getId())
.and("delFlag", "=", false).asc("location"))
.stream().collect(Collectors.toMap(Sys_dict::getCode, Sys_dict::getName));
}
private String getSexLimitName(Integer sexLimit) {
@@ -65,10 +65,12 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
@@ -590,6 +592,7 @@ public class DifficultSubsidyServiceImpl extends BaseServiceImpl<DifficultSubsid
String fileName = safeName(subsidyTypeNames(apply.getSubsidyType()))
+ DateUtil.format(apply.getApplyDate(), "yyyy年MM月dd日HH时mm分ss秒") + "申请.docx";
putZipEntry(zip, folder + fileName, renderApplyWord(apply));
putApplyAttachments(zip, folder, apply);
}
zip.finish();
return output.toByteArray();
@@ -598,6 +601,62 @@ public class DifficultSubsidyServiceImpl extends BaseServiceImpl<DifficultSubsid
}
}
/**
* 将申请人上传的纸质申请表和证明材料按原文件名导出到个人文件夹下的对应子目录;
* 单个附件失效或下载失败时跳过,不影响压缩包中其他材料。
*/
private void putApplyAttachments(ZipOutputStream zip, String folder, DifficultSubsidyApply apply) throws IOException {
Set<String> usedNames = new HashSet<>();
putAttachmentFiles(zip, folder, usedNames, apply.getApplyFile(), "纸质申请表");
putAttachmentFiles(zip, folder, usedNames, apply.getFiles(), "证明材料");
}
private void putAttachmentFiles(ZipOutputStream zip, String folder, Set<String> usedNames,
List<JSONObject> files, String subFolder) throws IOException {
if (files == null) {
return;
}
for (JSONObject file : files) {
String id = extractFileId(file);
if (Strings.isBlank(id)) {
continue;
}
byte[] bytes;
try {
bytes = sysFileService.download(id);
} catch (Exception ignored) {
// 单个历史附件无法读取时不影响同一申请中其他附件的导出。
continue;
}
if (bytes == null || bytes.length == 0) {
continue;
}
String fileName = StrUtil.blankToDefault(file.getStr("name"),
StrUtil.blankToDefault(file.getStr("filename"), subFolder));
putZipEntry(zip, uniqueZipEntryName(usedNames, folder + subFolder + "/" + safeName(fileName)), bytes);
}
}
/**
* zip 内不允许重复条目,同名文件追加序号区分。
*/
private String uniqueZipEntryName(Set<String> usedNames, String entryName) {
String baseName = entryName;
String extension = "";
int dotIndex = entryName.lastIndexOf('.');
if (dotIndex > entryName.lastIndexOf('/')) {
baseName = entryName.substring(0, dotIndex);
extension = entryName.substring(dotIndex);
}
String candidate = entryName;
int index = 2;
while (!usedNames.add(candidate)) {
candidate = baseName + "(" + index + ")" + extension;
index++;
}
return candidate;
}
/**
* 计算当前申请的补助金额,并在返回的住院记录中写入逐条测算结果。
*/
@@ -38,7 +38,9 @@ import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@IocBean(args = {"refer:dao"})
@@ -227,7 +229,7 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
if (request != null) {
String requestUrl = request.getRequestURL().toString();
return requestUrl.substring(0, requestUrl.length() - request.getRequestURI().length())
+ request.getContextPath();
+ request.getContextPath();
}
// 定时任务没有请求上下文;本机地址未配置端口时使用项目监听端口。
@@ -239,7 +241,7 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
int port = conf.getInt("server.port", defaultPort);
if (port != defaultPort) {
baseUrl = domain.getScheme() + "://" + domain.getRawAuthority() + ":" + port
+ StrUtil.blankToDefault(domain.getRawPath(), "");
+ StrUtil.blankToDefault(domain.getRawPath(), "");
}
}
String contextPath = StrUtil.removeSuffix(StrUtil.blankToDefault(Globals.AppBase, ""), "/");
@@ -250,9 +252,18 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
}
/**
* 将生日消息写入当前项目消息中心并生成发送记录。
* Sys_msg.url 用于站内消息点击,正文中的完整链接供外部短信/微信渠道访问。
* 按接收人姓名生成生日消息,供单人、批量及定时发送共用,并记录实际发送的正文。
* Sys_msg.url 用于站内消息点击,完整链接另存于生日发送记录。
*
* @param loginNames 接收人工号集合,空工号会过滤,重复工号只发送一次
* @param title 消息标题
* @param content 未拼接接收人称呼的祝福正文
* @param pushBy 推送人用户ID,定时发送为 system
* @param pushByName 推送人姓名,定时发送为“系统推送”
* @param pushType 手动推送或系统推送
* @return 提交发送的接收人数,不代表外部渠道实际送达人数
*/
@Aop(TransAop.READ_COMMITTED)
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()
@@ -263,25 +274,41 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
return 0;
}
// 批量读取接收人的姓名,不依赖登录会话,定时任务也使用同一份人员资料。
Sql recipientSql = Sqls.create("SELECT loginname, username FROM vw_user $condition");
recipientSql.setCondition(Cnd.where("loginname", "in", recipients));
Map<String, String> recipientNames = new HashMap<>();
for (NutMap user : listMap(recipientSql)) {
recipientNames.putIfAbsent(user.getString("loginname"),
StrUtil.blankToDefault(user.getString("username"), "").trim());
}
UserBirthdayConfig config = getConfig();
// String pagePath = "/platform/staffManage/birthday/manage/h5";
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);
for (String loginName : recipients) {
// 姓名缺失时使用“亲爱的老师,”;每人独立创建消息,避免批量发送时串用称呼。
String personalizedContent = "亲爱的" + recipientNames.getOrDefault(loginName, "")
+ "老师," + content;
Sys_msg message = new Sys_msg();
message.setTitle(title);
message.setNote(personalizedContent);
message.setUrl(pagePath);
message.setType("user");
message.setSendType("show");
message.setSendAt(Times.getTS());
message.setCreatedBy(pushBy);
sysMsgService.saveMsg(message, new String[]{loginName}, true);
userBirthdayMsgLogService.insertLogs(recipients, title, content, fullLink,
pushBy, pushByName, pushType);
// 日志正文与当前接收人的站内消息、外部通知保持一致。
userBirthdayMsgLogService.insertLogs(List.of(loginName), title, personalizedContent, fullLink,
pushBy, pushByName, pushType);
}
return recipients.size();
}
}
@@ -93,6 +93,11 @@ layout("/layouts/v4/baseLayout.html"){
font-weight: 500;
}
/* 仅隐藏首页顶部的流程中心入口 */
.v4-nav-item[href="/platform/v4/serv"] {
display: none;
}
</style>
<div class="v4-container" id="v4-home-app">
@@ -50,7 +50,10 @@ layout("/layouts/platform.html"){
<el-form-item label="标题" prop="title"><el-input maxlength="100" placeholder="请输入标题" v-model="formData.title"></el-input></el-form-item>
<el-form-item label="摘要" prop="summary"><el-input maxlength="500" placeholder="请输入摘要" rows="3" type="textarea" v-model="formData.summary"></el-input></el-form-item>
<el-form-item label="标签类型" prop="tagType">
<el-select placeholder="请先维护标签类型字典" style="width: 100%" v-model="formData.tagType"><el-option :key="item.code" :label="item.label" :value="item.code" v-for="item in dict.type.INCLUSIVE_BENEFIT_TYPE || []"></el-option></el-select>
<div style="display: flex; align-items: center">
<el-select placeholder="请先维护标签类型字典" style="flex: 1; min-width: 0" v-model="formData.tagType"><el-option :disabled="item.disabled" :key="item.code" :label="item.label" :value="item.code" v-for="item in tagTypeSelectOptions"></el-option></el-select>
<el-button @click="openTagTypeSettings" icon="el-icon-setting" style="margin-left: 10px">设置</el-button>
</div>
</el-form-item>
<el-form-item label="内容" prop="content"><text-editor v-model="formData.content"></text-editor></el-form-item>
<el-form-item label="性别限制" prop="sexLimit"><el-radio-group v-model="formData.sexLimit"><el-radio :label="0">全部开放</el-radio><el-radio :label="1">男性</el-radio><el-radio :label="2">女性</el-radio></el-radio-group></el-form-item>
@@ -62,14 +65,69 @@ layout("/layouts/platform.html"){
</el-form>
</template>
</guava>
<el-dialog :close-on-click-modal="false" :visible="tagTypeSettingsVisible" @close="closeTagTypeSettings" append-to-body title="标签类型设置" top="18vh" width="800px" custom-class="tag-type-settings-dialog">
<div style="color: #909399; margin-bottom: 16px">管理标签后,表单下拉选项会同步更新</div>
<div style="display: flex; justify-content: flex-end; margin-bottom: 16px">
<el-button :disabled="tagTypeListLoading || tagTypeActionLoading" @click="openTagTypeForm()" icon="el-icon-plus" type="primary" v-if="$auth.hasPermission('sys.manager.dict.add')">新增标签</el-button>
</div>
<el-table :data="tagTypeRows" :header-cell-style="{background: '#f5f7fa'}" v-loading="tagTypeListLoading" max-height="400" row-key="id" border>
<el-table-column align="left" header-align="left" label="标签名称" min-width="160" prop="name" show-overflow-tooltip></el-table-column>
<el-table-column align="left" header-align="left" label="标签编码" min-width="160" prop="code" show-overflow-tooltip></el-table-column>
<el-table-column align="left" header-align="left" label="状态" width="110">
<template slot-scope="{row}"><el-tag :type="row.disabled ? 'info' : 'success'" size="small">{{row.disabled ? '停用' : '启用'}}</el-tag></template>
</el-table-column>
<el-table-column align="left" header-align="left" label="操作" width="180">
<template slot-scope="{row}">
<el-button :disabled="tagTypeActionLoading" @click="openTagTypeForm(row)" size="mini" type="text" v-if="$auth.hasPermission('sys.manager.dict.edit')">编辑</el-button>
<el-button :disabled="tagTypeActionLoading" @click="toggleTagType(row)" size="mini" type="text" v-if="$auth.hasPermission('sys.manager.dict.edit')">{{row.disabled ? '启用' : '停用'}}</el-button>
<el-button :disabled="tagTypeActionLoading" @click="deleteTagType(row)" class="tag-type-delete" size="mini" type="text" v-if="$auth.hasPermission('sys.manager.dict.delete')">删除</el-button>
</template>
</el-table-column>
</el-table>
<div slot="footer" style="display: flex; align-items: center; justify-content: space-between">
<span style="color: #909399">仅启用的标签显示在下拉选项中</span>
<el-button @click="closeTagTypeSettings">关闭</el-button>
</div>
</el-dialog>
<el-dialog :close-on-click-modal="false" :close-on-press-escape="!tagTypeFormLoading" :show-close="!tagTypeFormLoading" :title="tagTypeForm.id ? '编辑标签' : '新增标签'" :visible="tagTypeFormVisible" @close="closeTagTypeForm" append-to-body width="520px" custom-class="tag-type-form-dialog">
<el-form :model="tagTypeForm" :rules="tagTypeRules" label-width="90px" ref="tagTypeForm">
<el-form-item label="标签名称" prop="name"><el-input maxlength="100" placeholder="请输入标签名称" v-model="tagTypeForm.name"></el-input></el-form-item>
<el-form-item label="标签编码" prop="code">
<el-input :disabled="!!tagTypeForm.id" maxlength="50" placeholder="请输入标签编码" v-model="tagTypeForm.code"></el-input>
<div style="color: #909399; line-height: 20px" v-if="tagTypeForm.id">标签编码用于关联历史记录,创建后不可修改</div>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="closeTagTypeForm" :disabled="tagTypeFormLoading">取消</el-button>
<el-button @click="saveTagType" :loading="tagTypeFormLoading" type="primary">保存</el-button>
</span>
</el-dialog>
</div>
<style>
.tag-type-settings-dialog, .tag-type-form-dialog { max-width: calc(100vw - 32px) }
.tag-type-settings-dialog .tag-type-delete:not(.is-disabled) { color: #f56c6c }
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
dicts: ["INCLUSIVE_BENEFIT_TYPE"],
mixins: [initTableMixins],
data: function () {
return {
dict: {type: {INCLUSIVE_BENEFIT_TYPE: []}},
tagTypeDictLoading: false,
tagTypeDictReloadPending: false,
tagTypeSettingsVisible: false,
tagTypeListLoading: false,
tagTypeListRequestId: 0,
tagTypeRows: [],
tagTypeActionLoading: false,
tagTypeFormVisible: false,
tagTypeFormLoading: false,
tagTypeForm: {id: "", name: "", code: ""},
tagTypeRules: {
name: [{required: true, message: "请输入标签名称", trigger: "blur"}],
code: [{required: true, message: "请输入标签编码", trigger: "blur"}]
},
pageForm: {year: "", title: "", pageNumber: 1, pageSize: 10, totalCount: 0},
formData: {},
formLoading: false,
@@ -82,12 +140,176 @@ layout("/layouts/platform.html"){
}
}
},
computed: {
// 编辑历史记录时保留已停用标签的名称,但不允许将该标签选给其他记录。
tagTypeSelectOptions() {
const options = this.dict.type.INCLUSIVE_BENEFIT_TYPE || [];
if (this.formData.id && this.formData.tagType && this.formData.tagTypeName && !options.some((item) => item.code === this.formData.tagType)) {
return options.concat([{code: this.formData.tagType, label: this.formData.tagTypeName, disabled: true}])
}
return options
}
},
methods: {
/**
* 加载幸福工荟标签类型,force 为 true 时在字典变更后重新查询。
* 返回 Promise<void>;请求中或已有选项时返回 undefined,不重复查询。
* 公共方法返回 [{name, code}],其中 name 为显示名称、code 为保存值。
*/
loadTagTypeOptions(force = false) {
if (this.tagTypeDictLoading) {
// 字典修改恰好发生在请求期间时,等当前请求结束后再读取最新数据。
if (force) { this.$set(this, "tagTypeDictReloadPending", true) }
return
}
if (!force && this.dict.type.INCLUSIVE_BENEFIT_TYPE.length) { return }
this.$set(this, "tagTypeDictLoading", true);
return this.$businessTool.getDictOptions("INCLUSIVE_BENEFIT_TYPE").then((data) => {
if (!Array.isArray(data)) {
this.$message.warning("标签类型字典数据异常,请重新打开表单重试");
return
}
const options = data.map((item) => ({label: item.name, code: item.code}));
this.$set(this.dict.type, "INCLUSIVE_BENEFIT_TYPE", options);
if (!data.length) {
this.$message.warning("标签类型字典暂无可用选项,请先维护字典")
}
}).catch(() => {
// 网络及接口异常由公共请求拦截器提示,重新打开表单时允许重试。
}).finally(() => {
this.$set(this, "tagTypeDictLoading", false);
if (this.tagTypeDictReloadPending) {
this.$set(this, "tagTypeDictReloadPending", false);
this.loadTagTypeOptions(true)
}
})
},
/** 打开标签设置弹框,读取包含启用和停用项的字典列表。 */
openTagTypeSettings() {
this.$set(this, "tagTypeSettingsVisible", true);
this.loadTagTypeSettings()
},
/** 关闭设置弹框,保留业务表单中尚未提交的其他内容。 */
closeTagTypeSettings() { this.$set(this, "tagTypeSettingsVisible", false) },
/**
* 查询设置列表,无调用参数;公共方法按 INCLUSIVE_BENEFIT_TYPE 返回字典项数组。
* 每项包含 id、name、code、disabled 和 delFlag;返回 Promise<void>。
*/
loadTagTypeSettings() {
const requestId = this.tagTypeListRequestId + 1;
this.$set(this, "tagTypeListRequestId", requestId);
this.$set(this, "tagTypeListLoading", true);
return this.$businessTool.getAllDictOptions("INCLUSIVE_BENEFIT_TYPE").then((data) => {
if (requestId !== this.tagTypeListRequestId) { return }
if (!Array.isArray(data)) {
this.$message.warning("标签类型字典数据异常,请重新打开设置");
return
}
this.$set(this, "tagTypeRows", data.filter((item) => item && item.delFlag !== true))
}).catch(() => {
// 网络及接口异常由公共请求拦截器提示,保留已有列表供重试。
}).finally(() => {
if (requestId === this.tagTypeListRequestId) { this.$set(this, "tagTypeListLoading", false) }
})
},
/** 打开新增或编辑表单;row 为空时新增,有值时仅允许修改该项名称。 */
openTagTypeForm(row) {
this.$set(this, "tagTypeForm", row ? {id: row.id, name: row.name, code: row.code} : {id: "", name: "", code: ""});
this.$set(this, "tagTypeFormVisible", true);
this.$nextTick(() => { if (this.$refs.tagTypeForm) { this.$refs.tagTypeForm.clearValidate() } })
},
/** 表单提交期间不允许关闭,避免误以为正在进行的保存已取消。 */
closeTagTypeForm() {
if (!this.tagTypeFormLoading) { this.$set(this, "tagTypeFormVisible", false) }
},
/** 字典操作成功后同时刷新管理列表和当前表单的可选标签。 */
refreshTagTypeData() {
this.loadTagTypeSettings();
this.loadTagTypeOptions(true)
},
/** 停用或删除当前选中的标签时要求重新选择,其他表单内容保持不变。 */
clearCurrentTagType(row) {
if (this.formData.tagType === row.code) {
this.$set(this.formData, "tagType", "");
this.$message.warning("当前选择的标签已停用或删除,请重新选择")
}
},
/**
* 保存标签:新增传 name、code,编辑传 id、name;无调用参数。
* 接口返回 Result 对象,code 为 0 表示成功,msg 为提示内容,data 为空。
*/
saveTagType() {
if (this.tagTypeFormLoading) { return }
this.$refs.tagTypeForm.validate((valid) => {
if (!valid) { return }
const editing = !!this.tagTypeForm.id;
const params = editing ? {id: this.tagTypeForm.id, name: this.tagTypeForm.name} : {name: this.tagTypeForm.name, code: this.tagTypeForm.code};
this.$set(this, "tagTypeFormLoading", true);
this.$axios.post(loc() + (editing ? "/updateTagTypeName" : "/addTagType"), params).then((res) => {
if (res && res.code === 0) {
this.$message.success(editing ? "标签修改成功" : "标签新增成功");
if (editing && this.formData.tagType === this.tagTypeForm.code) {
this.$set(this.formData, "tagTypeName", this.tagTypeForm.name.trim())
}
this.$set(this, "tagTypeFormVisible", false);
this.refreshTagTypeData()
}
}).catch(() => {
// 保存失败由公共请求拦截器提示,表单保留输入以便修改或重试。
}).finally(() => { this.$set(this, "tagTypeFormLoading", false) })
})
},
/**
* 切换 row 对应标签的启停状态;传 id、disabled,返回 Result 对象。
* disabled 为 true 时停用;code 为 0 表示成功,msg 为提示内容,data 为空。
*/
toggleTagType(row) {
if (this.tagTypeActionLoading) { return }
this.$set(this, "tagTypeActionLoading", true);
return this.$axios.post(loc() + "/updateTagTypeDisabled", {id: row.id, disabled: !row.disabled}).then((res) => {
if (res && res.code === 0) {
this.$message.success(row.disabled ? "标签已启用" : "标签已停用");
if (!row.disabled) { this.clearCurrentTagType(row) }
this.refreshTagTypeData()
}
}).catch(() => {
// 状态修改失败由公共请求拦截器提示,不修改本地字典数据。
}).finally(() => { this.$set(this, "tagTypeActionLoading", false) })
},
/**
* 删除 row 对应标签:先传 id 查询 Result.data 中的引用数量,再经确认传 id 删除。
* 删除接口返回 Result,code 为 0 表示成功,msg 为提示内容,data 为空。
*/
deleteTagType(row) {
if (this.tagTypeActionLoading) { return }
this.$set(this, "tagTypeActionLoading", true);
return this.$axios.post(loc() + "/countTagTypeUsage", {id: row.id}).then((res) => {
if (!res || res.code !== 0 || typeof res.data !== "number") {
this.$message.warning("无法获取标签引用情况,请重试");
return
}
const message = res.data > 0
? "标签“" + row.name + "”被 " + res.data + " 条记录引用(含已删除记录),删除后这些记录将无法显示原标签名称。确定删除吗?"
: "确定删除标签“" + row.name + "”吗?";
return this.$confirm(message, "删除标签", {type: "warning", confirmButtonText: "删除", cancelButtonText: "取消"}).then(() => {
return this.$axios.post(loc() + "/deleteTagType", {id: row.id})
}).then((result) => {
if (result && result.code === 0) {
this.$message.success("标签删除成功");
this.clearCurrentTagType(row);
this.refreshTagTypeData()
}
})
}).catch(() => {
// 取消确认时不删除;请求异常由公共请求拦截器提示。
}).finally(() => { this.$set(this, "tagTypeActionLoading", false) })
},
emptyForm: function () { return {id: "", year: this.pageForm.year || new Date().getFullYear() + "", title: "", summary: "", tagType: "", content: "", sexLimit: 0, cover: "", linkUrl: ""} },
formatDate: function (value) { return value ? moment(value).format("YYYY-MM-DD HH:mm") : "--" },
back: function () { this.$refs.guava.index() },
openAdd: function () { this.$set(this, "formData", this.emptyForm()); this.$refs.guava.edit() },
openEdit: function (row) {
openAdd() { this.loadTagTypeOptions(); this.$set(this, "formData", this.emptyForm()); this.$refs.guava.edit() },
openEdit(row) {
this.loadTagTypeOptions();
var self = this;
this.$set(this, "formLoading", true);
$.post(loc() + "/findOne", {id: row.id}).then(function (res) {
@@ -128,7 +350,7 @@ layout("/layouts/platform.html"){
}).catch(function () {})
}
},
created: function () { this.pageData() }
created() { this.pageData(); this.loadTagTypeOptions() }
})
</script>
<!--#