feat: 俱乐部增加导入社团
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
package com.budwk.app.sys.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||
import com.budwk.app.sys.models.*;
|
||||
import com.budwk.app.sys.services.SysDictService;
|
||||
import com.budwk.app.sys.views.View_user;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.AdaptBy;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
import org.nutz.mvc.upload.TempFile;
|
||||
import org.nutz.mvc.upload.UploadAdaptor;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@IocBean
|
||||
@At("/platform/sys/unionCadreImport")
|
||||
@Ok("json:full")
|
||||
public class SysUnionCadreImportController {
|
||||
private static final String[] HEADERS = {"工会编码", "工号", "角色编码", "届次编码(选填)", "所属单位编码(书记必填)"};
|
||||
private static final int MAX_ROWS = 1000;
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private SysDictService sysDictService;
|
||||
@Inject
|
||||
private SysUnionController sysUnionController;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<NutMap> roleOptions() {
|
||||
return (List<NutMap>) sysUnionController.branchUnionRoleOptions().getData();
|
||||
}
|
||||
|
||||
@At
|
||||
@Ok("void")
|
||||
@SaCheckRole("SYSADMIN")
|
||||
@SaCheckPermission("sys.manager.union.branchOfficer")
|
||||
public void downloadTemplate(HttpServletResponse response) {
|
||||
Workbook workbook = new XSSFWorkbook();
|
||||
Sheet data = createSheet(workbook, "干部导入", HEADERS);
|
||||
CellStyle textStyle = workbook.createCellStyle();
|
||||
textStyle.setDataFormat(workbook.createDataFormat().getFormat("@"));
|
||||
for (int i = 0; i < HEADERS.length; i++) {
|
||||
data.setDefaultColumnStyle(i, textStyle);
|
||||
}
|
||||
Sheet notes = createSheet(workbook, "填写说明", "字段", "填写要求");
|
||||
notes.setColumnWidth(1, 100 * 256);
|
||||
append(notes, "导入范围", "仅导入第一个工作表;每行一个人员角色,最多1000行;请保留表头,工号和编码按文本填写。");
|
||||
append(notes, "工会编码", "必填,参考工会对照表。人员必须属于该分工会。");
|
||||
append(notes, "工号", "必填,填写系统中已存在的人员工号,不填写姓名。");
|
||||
append(notes, "角色编码", "必填,参考角色对照表,不填写角色名称。");
|
||||
append(notes, "届次编码(选填)", "可留空;填写时使用届次对照表中的编码,与单个添加一致。");
|
||||
append(notes, "所属单位编码(书记必填)", "仅二级党委书记填写,多个单位用英文逗号分隔,必须为当前分工会的二级单位;其他角色留空。");
|
||||
append(notes, "重复记录", "普通干部按人员和角色跳过;二级党委书记按人员、角色和工会跳过。禁用的已有关系也跳过,不覆盖。");
|
||||
append(notes, "保存方式", "系统管理员直接授权,写入干部及角色记录并清理权限缓存,不发起审批。逐行保存,失败行不影响其他行。");
|
||||
append(notes, "已有干部记录", "已有在任或待审核干部记录但未分配角色时跳过,避免重复创建或绕过已有审批。");
|
||||
Sheet unions = createSheet(workbook, "工会对照", "工会编码", "工会名称");
|
||||
List<Sys_union> unionList = dao.query(Sys_union.class, Cnd.NEW().asc("unionCode"));
|
||||
for (Sys_union union : unionList) {
|
||||
append(unions, union.getUnionCode(), union.getName());
|
||||
}
|
||||
Sheet roles = createSheet(workbook, "角色对照", "角色编码", "角色名称");
|
||||
for (NutMap role : roleOptions()) {
|
||||
append(roles, role.getString("code"), role.getString("name"));
|
||||
}
|
||||
Sheet terms = createSheet(workbook, "届次对照", "届次编码", "届次名称");
|
||||
for (Sys_dict term : sysDictService.getSubListByCode("TEACHER_CONGRESS_J")) {
|
||||
append(terms, term.getCode(), term.getName());
|
||||
}
|
||||
Sheet units = createSheet(workbook, "所属单位对照", "单位编码", "单位名称", "工会编码", "工会名称");
|
||||
for (Sys_unit unit : dao.query(Sys_unit.class, Cnd.where("unitLevel", "=", 2).asc("unitcode"))) {
|
||||
for (Sys_union union : unionList) {
|
||||
if (union.getId().equals(unit.getUnionId())) {
|
||||
append(units, unit.getUnitcode(), unit.getName(), union.getUnionCode(), union.getName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
CommonDownloadUtil.download("批量添加分工会干部模板.xlsx", workbook, response);
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckRole("SYSADMIN")
|
||||
@SaCheckPermission("sys.manager.union.branchOfficer")
|
||||
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
|
||||
public Result doImport(@Param("file") TempFile file) {
|
||||
if (file == null) {
|
||||
return Result.error("请选择Excel文件");
|
||||
}
|
||||
List<NutMap> rows = new ArrayList<>();
|
||||
// 先完整读取文件,避免文件格式错误时已写入部分数据却无法返回逐行结果。
|
||||
try (InputStream input = file.getInputStream(); Workbook workbook = WorkbookFactory.create(input)) {
|
||||
if (workbook.getNumberOfSheets() == 0) {
|
||||
return Result.error("文件没有工作表,请使用下载的模板");
|
||||
}
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
DataFormatter formatter = new DataFormatter();
|
||||
for (int i = 0; i < HEADERS.length; i++) {
|
||||
if (!HEADERS[i].equals(cellText(sheet.getRow(0), i, formatter))) {
|
||||
return Result.error("模板表头不正确,请使用下载的模板并保留第一行表头");
|
||||
}
|
||||
}
|
||||
for (Row row : sheet) {
|
||||
if (row.getRowNum() == 0) {
|
||||
continue;
|
||||
}
|
||||
String[] values = new String[HEADERS.length];
|
||||
for (int j = 0; j < values.length; j++) {
|
||||
values[j] = cellText(row, j, formatter);
|
||||
}
|
||||
if (Arrays.stream(values).allMatch(StrUtil::isBlank)) {
|
||||
continue;
|
||||
}
|
||||
if (rows.size() >= MAX_ROWS) {
|
||||
return Result.error("每次最多导入1000行有效数据,请拆分文件后导入");
|
||||
}
|
||||
rows.add(NutMap.NEW().addv("rowNumber", row.getRowNum() + 1).addv("unionCode", values[0])
|
||||
.addv("loginname", values[1]).addv("roleCode", values[2])
|
||||
.addv("j", values[3]).addv("unitCodes", values[4]));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("读取分工会干部导入文件失败", e);
|
||||
return Result.error("无法读取文件,请上传未加密的xls或xlsx模板文件,编码列不要使用公式");
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
return Result.error("文件中没有需要导入的数据");
|
||||
}
|
||||
List<String> roleCodes = roleOptions().stream().map(item -> item.getString("code")).toList();
|
||||
List<String> terms = sysDictService.getSubListByCode("TEACHER_CONGRESS_J").stream()
|
||||
.map(Sys_dict::getCode).toList();
|
||||
int success = 0;
|
||||
int skipped = 0;
|
||||
for (NutMap row : rows) {
|
||||
try {
|
||||
importRow(row, roleCodes, terms);
|
||||
} catch (Exception e) {
|
||||
log.error("导入分工会干部失败,行号:{}", row.getInt("rowNumber"), e);
|
||||
outcome(row, "失败", "保存失败,请检查该行数据或联系管理员");
|
||||
}
|
||||
if ("成功".equals(row.getString("status"))) {
|
||||
success++;
|
||||
} else if ("跳过".equals(row.getString("status"))) {
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
return Result.success(NutMap.NEW().addv("success", success).addv("skipped", skipped)
|
||||
.addv("failed", rows.size() - success - skipped).addv("rows", rows));
|
||||
}
|
||||
|
||||
private void importRow(NutMap row, List<String> roleCodes, List<String> terms) {
|
||||
String unionCode = row.getString("unionCode");
|
||||
String loginname = row.getString("loginname");
|
||||
String roleCode = row.getString("roleCode");
|
||||
String j = row.getString("j");
|
||||
if (StrUtil.isBlank(unionCode) || StrUtil.isBlank(loginname) || StrUtil.isBlank(roleCode)) {
|
||||
outcome(row, "失败", "工会编码、工号、角色编码不能为空");
|
||||
return;
|
||||
}
|
||||
List<Sys_union> unions = dao.query(Sys_union.class, Cnd.where("unionCode", "=", unionCode));
|
||||
List<View_user> users = dao.query(View_user.class, Cnd.where("loginname", "=", loginname));
|
||||
List<Sys_role> roles = dao.query(Sys_role.class, Cnd.where("code", "=", roleCode));
|
||||
if (unions.size() != 1 || users.size() != 1 || roles.size() != 1 || !roleCodes.contains(roleCode)) {
|
||||
outcome(row, "失败", "工会、工号或分工会角色编码不存在或不唯一,请按模板对照表填写");
|
||||
return;
|
||||
}
|
||||
Sys_union union = unions.get(0);
|
||||
View_user user = users.get(0);
|
||||
Sys_role role = roles.get(0);
|
||||
boolean secretary = RoleConstant.UNIT_PARTY_SECRETARY.name().equals(roleCode);
|
||||
Cnd existing = Cnd.where("userId", "=", user.getId()).and("roleId", "=", role.getId());
|
||||
if (secretary) {
|
||||
existing.and("unionId", "=", union.getId());
|
||||
}
|
||||
if (dao.count(Sys_user_role.class, existing) > 0) {
|
||||
outcome(row, "跳过", "已分配该角色,保留原有记录(含禁用关系)");
|
||||
return;
|
||||
}
|
||||
if (!union.getId().equals(user.getUnionId())) {
|
||||
outcome(row, "失败", "所选人员不属于该分工会");
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isNotBlank(j) && !terms.contains(j)) {
|
||||
outcome(row, "失败", "届次编码不存在,请参照届次对照表或留空");
|
||||
return;
|
||||
}
|
||||
if (dao.count(Sys_union_cadre.class, Cnd.where("userId", "=", user.getId())
|
||||
.and("roleCode", "=", roleCode).and("unionId", "=", union.getId()).and("isServing", "=", true)) > 0) {
|
||||
outcome(row, "跳过", "已有在任或待审核干部记录,请通过原记录处理");
|
||||
return;
|
||||
}
|
||||
List<String> unitIds = new ArrayList<>();
|
||||
String unitCodes = row.getString("unitCodes");
|
||||
if (secretary) {
|
||||
List<String> codes = Arrays.stream(unitCodes.split("[,,]"))
|
||||
.map(String::trim).filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
|
||||
if (codes.isEmpty()) {
|
||||
outcome(row, "失败", "二级党委书记必须填写所属单位编码");
|
||||
return;
|
||||
}
|
||||
for (String code : codes) {
|
||||
List<Sys_unit> units = dao.query(Sys_unit.class, Cnd.where("unitcode", "=", code)
|
||||
.and("unionId", "=", union.getId()).and("unitLevel", "=", 2));
|
||||
if (units.size() != 1) {
|
||||
outcome(row, "失败", "所属单位编码不存在、不唯一或不是该分工会的二级单位:" + code);
|
||||
return;
|
||||
}
|
||||
unitIds.add(units.get(0).getId());
|
||||
}
|
||||
} else if (StrUtil.isNotBlank(unitCodes)) {
|
||||
outcome(row, "失败", "普通干部角色的所属单位编码应留空");
|
||||
return;
|
||||
}
|
||||
// 经由注入的原控制器代理调用,保留单行事务、直接授权及缓存清理逻辑。
|
||||
Result result = secretary
|
||||
? sysUnionController.insertBranchUnionPartySecretaryRole(user.getId(), union.getId(), unitIds.toArray(new String[0]), j)
|
||||
: sysUnionController.insertBranchUnionUserRole(user.getId(), roleCode, union.getId(), j);
|
||||
if (result.isSuccess()) {
|
||||
outcome(row, "成功", "添加成功");
|
||||
} else if ("请勿重复添加".equals(result.getMsg()) || "该人员已设置二级党委书记,请先删除后再重新设置".equals(result.getMsg())) {
|
||||
outcome(row, "跳过", result.getMsg());
|
||||
} else {
|
||||
outcome(row, "失败", result.getMsg());
|
||||
}
|
||||
}
|
||||
|
||||
private void outcome(NutMap row, String status, String message) {
|
||||
row.setv("status", status).setv("message", message);
|
||||
}
|
||||
|
||||
private String cellText(Row row, int column, DataFormatter formatter) {
|
||||
Cell cell = row == null ? null : row.getCell(column);
|
||||
if (cell != null && cell.getCellType() == CellType.FORMULA) {
|
||||
throw new IllegalArgumentException("导入内容不支持公式");
|
||||
}
|
||||
return formatter.formatCellValue(cell).trim();
|
||||
}
|
||||
|
||||
private Sheet createSheet(Workbook workbook, String name, String... headers) {
|
||||
Sheet sheet = workbook.createSheet(name);
|
||||
Row header = sheet.createRow(0);
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
header.createCell(i).setCellValue(headers[i]);
|
||||
sheet.setColumnWidth(i, 30 * 256);
|
||||
}
|
||||
sheet.createFreezePane(0, 1);
|
||||
return sheet;
|
||||
}
|
||||
|
||||
private void append(Sheet sheet, String... values) {
|
||||
Row row = sheet.createRow(sheet.getLastRowNum() + 1);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
row.createCell(i).setCellValue(values[i] == null ? "" : values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package com.budwk.app.zhgh.club.controller.infoManage;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||
import com.budwk.app.sys.models.Sys_dict;
|
||||
import com.budwk.app.sys.services.SysDictService;
|
||||
import com.budwk.app.zhgh.club.service.ClubRegisterImportService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.AdaptBy;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
import org.nutz.mvc.upload.TempFile;
|
||||
import org.nutz.mvc.upload.UploadAdaptor;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@IocBean
|
||||
@At("/platform/club/infoManage/registerImport")
|
||||
@Ok("json:full")
|
||||
public class ClubRegisterImportController {
|
||||
private static final String[] HEADERS = {"社团名称", "社团编码(选填)", "社团类型编码", "成立日期", "会长工号", "秘书长工号",
|
||||
"发起人工号", "副会长工号(选填)", "副秘书长工号(选填)", "联系人工号(选填)", "会费标准(选填)",
|
||||
"联系电话(选填)", "联系邮箱(选填)", "社团介绍及宗旨(选填)", "活动形式(选填)", "缩略图地址(选填)",
|
||||
"申请成立报告文件ID(选填)", "章程草案文件ID(选填)", "经费管理办法文件ID(选填)", "年度计划文件ID(选填)", "其他附件文件ID(选填)"};
|
||||
@Inject
|
||||
private ClubRegisterImportService clubRegisterImportService;
|
||||
@Inject
|
||||
private SysDictService sysDictService;
|
||||
|
||||
@At
|
||||
@Ok("void")
|
||||
@SaCheckRole("SYSADMIN")
|
||||
@SaCheckPermission("club.infoManage.manage")
|
||||
public void downloadTemplate(HttpServletResponse response) {
|
||||
Workbook workbook = new XSSFWorkbook();
|
||||
Sheet data = sheet(workbook, "社团导入", HEADERS);
|
||||
CellStyle style = workbook.createCellStyle();
|
||||
style.setDataFormat(workbook.createDataFormat().getFormat("@"));
|
||||
for (int i = 0; i < HEADERS.length; i++) data.setDefaultColumnStyle(i, style);
|
||||
Sheet notes = sheet(workbook, "填写说明", "参数", "填写要求");
|
||||
notes.setColumnWidth(1, 100 * 256);
|
||||
append(notes, "导入规则", "仅读取第一个工作表,每行一个社团,最多1000行有效数据;保留表头,工号与编码按文本填写。");
|
||||
append(notes, "必填", "社团名称、社团类型编码、成立日期、会长工号、秘书长工号、至少3名不同发起人工号。");
|
||||
append(notes, "社团编码", "选填,留空按单个注册的年度编号规则生成;已有名称或编码直接跳过,不覆盖草稿、审批中或已解散记录。");
|
||||
append(notes, "成立日期", "按yyyy-MM-dd填写,如2026-10-23;支持Excel日期单元格。");
|
||||
append(notes, "人员", "使用系统内已有工号。会长和秘书长各1人;副职可选多人,用逗号分隔,理事机构人员不得重复。");
|
||||
append(notes, "发起人", "至少3名不同人员,用逗号分隔;可与理事机构人员重合。");
|
||||
append(notes, "联系人", "选填工号,电话及邮箱默认取人员资料;单独填写电话、邮箱时使用填写值。");
|
||||
append(notes, "社团类型", "参照社团类型工作表填写编码,不填写名称。");
|
||||
append(notes, "其他资料", "会费标准、介绍、活动形式、缩略图地址均选填;介绍及活动形式按纯文本导入。");
|
||||
append(notes, "附件", "选填系统已上传文件的ID,多文件用逗号分隔,每类最多5个;Excel不上传附件本身,留空可后续在维护页面补充。");
|
||||
append(notes, "直接通过", "保存社团、发起人、理事会员、入会时间和年度缴费记录,授予社团角色;直接通过,不生成审批任务。");
|
||||
append(notes, "导入结果", "逐行独立保存,已存在的跳过,失败行回滚并显示原因,不影响其他行。");
|
||||
Sheet types = sheet(workbook, "社团类型", "类型编码", "类型名称");
|
||||
for (Sys_dict type : sysDictService.getSubListByCode("CLUB_REGISTER_TYPE")) append(types, type.getCode(), type.getName());
|
||||
CommonDownloadUtil.download("社团批量注册模板.xlsx", workbook, response);
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckRole("SYSADMIN")
|
||||
@SaCheckPermission("club.infoManage.manage")
|
||||
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
|
||||
public Result doImport(@Param("file") TempFile file) {
|
||||
if (file == null) return Result.error("请选择Excel文件");
|
||||
List<String[]> rows = new ArrayList<>();
|
||||
List<Integer> numbers = new ArrayList<>();
|
||||
try (InputStream input = file.getInputStream(); Workbook workbook = WorkbookFactory.create(input)) {
|
||||
if (workbook.getNumberOfSheets() == 0) return Result.error("文件中没有工作表");
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
DataFormatter formatter = new DataFormatter();
|
||||
for (int i = 0; i < HEADERS.length; i++) {
|
||||
if (!HEADERS[i].equals(text(sheet.getRow(0), i, formatter))) return Result.error("表头不匹配,请使用下载的模板并保留完整表头");
|
||||
}
|
||||
for (Row row : sheet) {
|
||||
if (row.getRowNum() == 0) continue;
|
||||
String[] values = new String[HEADERS.length];
|
||||
for (int i = 0; i < values.length; i++) values[i] = text(row, i, formatter);
|
||||
if (Arrays.stream(values).allMatch(StrUtil::isBlank)) continue;
|
||||
if (rows.size() >= 1000) return Result.error("每次最多导入1000行有效数据");
|
||||
rows.add(values);
|
||||
numbers.add(row.getRowNum() + 1);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("读取社团注册导入文件失败", e);
|
||||
return Result.error("文件读取失败,请使用未加密的xls或xlsx模板,不要使用公式");
|
||||
}
|
||||
if (rows.isEmpty()) return Result.error("文件中没有需要导入的数据");
|
||||
List<NutMap> results = new ArrayList<>();
|
||||
int success = 0;
|
||||
int skipped = 0;
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
String[] values = rows.get(i);
|
||||
NutMap result;
|
||||
try {
|
||||
result = clubRegisterImportService.importRow(values);
|
||||
} catch (IllegalArgumentException e) {
|
||||
result = NutMap.NEW().addv("status", "失败").addv("message", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("社团批量注册失败,行号:{}", numbers.get(i), e);
|
||||
result = NutMap.NEW().addv("status", "失败").addv("message", "保存失败,该行已回滚,请检查数据或联系管理员");
|
||||
}
|
||||
result.setv("rowNumber", numbers.get(i)).setv("clubName", values[0]);
|
||||
if (!result.containsKey("clubCode")) result.setv("clubCode", values[1]);
|
||||
if ("成功".equals(result.getString("status"))) success++;
|
||||
if ("跳过".equals(result.getString("status"))) skipped++;
|
||||
results.add(result);
|
||||
}
|
||||
return Result.success(NutMap.NEW().addv("success", success).addv("skipped", skipped)
|
||||
.addv("failed", results.size() - success - skipped).addv("rows", results));
|
||||
}
|
||||
|
||||
private String text(Row row, int index, DataFormatter formatter) {
|
||||
Cell cell = row == null ? null : row.getCell(index);
|
||||
if (cell != null && cell.getCellType() == CellType.FORMULA) throw new IllegalArgumentException("不支持公式");
|
||||
if (index == 3 && cell != null && cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
|
||||
return cell.getLocalDateTimeCellValue().toLocalDate().toString();
|
||||
}
|
||||
return formatter.formatCellValue(cell).trim();
|
||||
}
|
||||
|
||||
private Sheet sheet(Workbook workbook, String name, String... headers) {
|
||||
Sheet sheet = workbook.createSheet(name);
|
||||
Row row = sheet.createRow(0);
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
row.createCell(i).setCellValue(headers[i]);
|
||||
sheet.setColumnWidth(i, 28 * 256);
|
||||
}
|
||||
sheet.createFreezePane(0, 1);
|
||||
return sheet;
|
||||
}
|
||||
|
||||
private void append(Sheet sheet, String... values) {
|
||||
Row row = sheet.createRow(sheet.getLastRowNum() + 1);
|
||||
for (int i = 0; i < values.length; i++) row.createCell(i).setCellValue(values[i]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.budwk.app.zhgh.club.service;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.flow.constant.FlowConst;
|
||||
import com.budwk.app.flow.entity.ProcessDefine;
|
||||
import com.budwk.app.flow.entity.ProcessInstance;
|
||||
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||
import com.budwk.app.sys.models.*;
|
||||
import com.budwk.app.sys.services.SysDictService;
|
||||
import com.budwk.app.sys.services.SysRoleService;
|
||||
import com.budwk.app.sys.services.SysUserService;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.club.model.SysClub;
|
||||
import com.budwk.app.zhgh.club.model.SysClubSponsor;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
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.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@IocBean
|
||||
public class ClubRegisterImportService {
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private SysClubService sysClubService;
|
||||
@Inject
|
||||
private SysDictService sysDictService;
|
||||
@Inject
|
||||
private SysRoleService sysRoleService;
|
||||
@Inject
|
||||
private SysUserService sysUserService;
|
||||
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public NutMap importRow(String[] values) {
|
||||
String name = values[0];
|
||||
String code = values[1];
|
||||
if (StrUtil.isBlank(name)) {
|
||||
throw new IllegalArgumentException("社团名称不能为空");
|
||||
}
|
||||
if (dao.count(SysClub.class, Cnd.where("clubName", "=", name)) > 0
|
||||
|| (StrUtil.isNotBlank(code) && dao.count(SysClub.class, Cnd.where("clubCode", "=", code)) > 0)) {
|
||||
return NutMap.NEW().addv("status", "跳过").addv("message", "社团名称或编码已存在,保留原记录(含草稿、审批中及已解散社团)");
|
||||
}
|
||||
if (name.length() > 100 || code.length() > 100) {
|
||||
throw new IllegalArgumentException("社团名称和编码不能超过100个字符");
|
||||
}
|
||||
if (sysDictService.getSubListByCode("CLUB_REGISTER_TYPE").stream().noneMatch(item -> values[2].equals(item.getCode()))) {
|
||||
throw new IllegalArgumentException("请选择类型对照表中的社团类型编码");
|
||||
}
|
||||
try {
|
||||
LocalDate.parse(values[3]);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("成立日期必填,格式为yyyy-MM-dd");
|
||||
}
|
||||
List<NutMap> managers = new ArrayList<>();
|
||||
Set<String> managerIds = new HashSet<>();
|
||||
addManagers(managers, managerIds, values[4], "CLUB_PRESIDENT", true);
|
||||
addManagers(managers, managerIds, values[5], "CLUB_SECRETARY", true);
|
||||
addManagers(managers, managerIds, values[7], "CLUB_VICE_PRESIDENT", false);
|
||||
addManagers(managers, managerIds, values[8], "CLUB_VICE_SECRETARY", false);
|
||||
List<SysClubSponsor> sponsors = new ArrayList<>();
|
||||
for (String loginname : split(values[6])) {
|
||||
SysClubSponsor sponsor = new SysClubSponsor();
|
||||
sponsor.setSponsorId(findUser(loginname).getId());
|
||||
sponsors.add(sponsor);
|
||||
}
|
||||
if (sponsors.size() < 3) {
|
||||
throw new IllegalArgumentException("发起人至少需要3名不同人员,多个工号用逗号分隔");
|
||||
}
|
||||
ProcessDefine define = dao.fetch(ProcessDefine.class, Cnd.where("name", "=", "XHZC").and("state", "=", 1).desc("version"));
|
||||
if (define == null) {
|
||||
throw new IllegalArgumentException("未配置可用的社团注册定义,无法记录直接通过状态");
|
||||
}
|
||||
if (StrUtil.isBlank(code)) {
|
||||
int sequence = dao.count(SysClub.class, Cnd.where("year(createTime)", "=", DateUtil.thisYear())) + 1;
|
||||
do {
|
||||
code = DateUtil.thisYear() + String.format("%02d", sequence++);
|
||||
} while (dao.count(SysClub.class, Cnd.where("clubCode", "=", code)) > 0);
|
||||
}
|
||||
SysClub club = new SysClub();
|
||||
club.setClubName(name);
|
||||
club.setClubCode(code);
|
||||
club.setClubType(values[2]);
|
||||
club.setFoundTime(values[3]);
|
||||
club.setSponsors(sponsors);
|
||||
club.setDue(values[10]);
|
||||
if (values[10].length() > 50 || values[11].length() > 20 || values[12].length() > 100 || values[15].length() > 1000) {
|
||||
throw new IllegalArgumentException("会费标准、联系电话、邮箱或缩略图地址超出字段长度限制");
|
||||
}
|
||||
if (StrUtil.isNotBlank(values[9])) {
|
||||
Sys_user contact = findUser(values[9]);
|
||||
club.setConcatPerson(contact.getId());
|
||||
club.setConcatPersonMobile(contact.getMobile());
|
||||
club.setConcatPersonEmail(contact.getEmail());
|
||||
}
|
||||
if (StrUtil.isNotBlank(values[11])) club.setConcatPersonMobile(values[11]);
|
||||
if (StrUtil.isNotBlank(values[12])) club.setConcatPersonEmail(values[12]);
|
||||
club.setIntroduce(escapeText(values[13]));
|
||||
club.setPurpose(escapeText(values[14]));
|
||||
club.setThumbnail(values[15]);
|
||||
club.setEstablishReport(files(values[16]));
|
||||
club.setRulesFile(files(values[17]));
|
||||
club.setManageFile(files(values[18]));
|
||||
club.setYearPlanFile(files(values[19]));
|
||||
club.setFiles(files(values[20]));
|
||||
// 复用单个注册:社团、发起人、理事会员资料及入会时间、年度缴费记录和快照。
|
||||
sysClubService.doAdd(club, managers);
|
||||
// 与注册通过后的角色授权一致,但不启动流程或创建审批任务。
|
||||
for (NutMap manager : managers) {
|
||||
Sys_role role = sysRoleService.getByCode(manager.getString("roleCode"));
|
||||
if (role == null) throw new IllegalArgumentException("社团角色不存在:" + manager.getString("roleCode"));
|
||||
Sys_user_role relation = new Sys_user_role();
|
||||
relation.setUserId(manager.getString("userId"));
|
||||
relation.setRoleId(role.getId());
|
||||
relation.setClubId(club.getId());
|
||||
dao.insert(relation);
|
||||
}
|
||||
// 现有社团查询通过已完成实例识别有效社团;保留可追溯的管理员直接通过记录。
|
||||
ProcessInstance instance = new ProcessInstance();
|
||||
instance.setProcessDefineId(define.getId());
|
||||
instance.setBusinessNo(club.getId());
|
||||
instance.setOperator(SecurityUtil.getUserId());
|
||||
instance.setState(ProcessInstanceStateEnum.FINISHED.getCode());
|
||||
instance.setVariable(JSONUtil.toJsonStr(NutMap.NEW()
|
||||
.addv(FlowConst.FORM_DATA, JSONUtil.toJsonStr(club))
|
||||
.addv(FlowConst.AUTO_GEN_TITLE, name + "(系统管理员批量导入直接通过)")
|
||||
.addv("directImport", true)));
|
||||
dao.insert(instance);
|
||||
sysUserService.clearCache();
|
||||
sysRoleService.clearCache();
|
||||
return NutMap.NEW().addv("status", "成功").addv("message", "注册成功,已直接通过").addv("clubCode", code);
|
||||
}
|
||||
|
||||
private void addManagers(List<NutMap> managers, Set<String> ids, String text, String roleCode, boolean required) {
|
||||
List<String> names = split(text);
|
||||
if (required && names.size() != 1) throw new IllegalArgumentException("会长、秘书长各需填写1名人员工号");
|
||||
if (!names.isEmpty() && sysRoleService.getByCode(roleCode) == null) {
|
||||
throw new IllegalArgumentException("系统未配置社团角色:" + roleCode);
|
||||
}
|
||||
for (String name : names) {
|
||||
Sys_user user = findUser(name);
|
||||
if (!ids.add(user.getId())) throw new IllegalArgumentException("理事机构人员不能重复:" + name);
|
||||
managers.add(NutMap.NEW().addv("userId", user.getId()).addv("roleCode", roleCode));
|
||||
}
|
||||
}
|
||||
|
||||
private Sys_user findUser(String loginname) {
|
||||
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where("loginname", "=", loginname));
|
||||
if (users.size() != 1) throw new IllegalArgumentException("工号不存在或不唯一:" + loginname);
|
||||
return users.get(0);
|
||||
}
|
||||
|
||||
private List<String> split(String text) {
|
||||
return Arrays.stream(text.split("[,,]|;|;")).map(String::trim).filter(StrUtil::isNotBlank).distinct().toList();
|
||||
}
|
||||
|
||||
private List<JSONObject> files(String text) {
|
||||
List<JSONObject> result = new ArrayList<>();
|
||||
List<String> ids = split(text);
|
||||
if (ids.size() > 5) throw new IllegalArgumentException("每类附件最多5个文件");
|
||||
for (String id : ids) {
|
||||
Sys_file file = dao.fetch(Sys_file.class, id);
|
||||
if (file == null || Boolean.TRUE.equals(file.getDelFlag())) throw new IllegalArgumentException("附件文件不存在:" + id);
|
||||
result.add(JSONUtil.parseObj(file));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String escapeText(String text) {
|
||||
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
.replace("\r\n", "\n").replace("\n", "<br>");
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,7 @@ xsssql:
|
||||
urls: /platform/cms/content/article/addDo,/platform/sys/app/conf/addDo,/platform/sys/app/conf/editDo
|
||||
|
||||
beetl:
|
||||
RESOURCE_LOADER: com.budwk.app.web.commons.ext.beetl.BeetlCustomResourceLoader
|
||||
RESOURCE:
|
||||
#本地路径,开发时设置本地路径,便于调试
|
||||
rootLocal: '${framework.beetl.RESOURCE.rootLocal}'
|
||||
|
||||
@@ -8,6 +8,7 @@ const branchUnionManage = {
|
||||
<el-button type="primary" class="ml5" size="small" icon="el-icon-search" @click="doSearch"></el-button>
|
||||
<el-button @click="openAudit" icon="el-icon-s-check" type="primary" style="margin-left: auto" size="small" v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])">基层干部审核</el-button>
|
||||
<el-button @click="openAdd" icon="ti-plus" type="primary" size="small" v-if="$auth.hasPermission('sys.manager.union.add')">新建分工会</el-button>
|
||||
<el-button @click="openCadreImport" icon="el-icon-upload2" type="primary" size="small" v-if="$auth.hasRole('SYSADMIN')">批量添加分工会干部</el-button>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-card class="branch-union-table-card" shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
|
||||
@@ -39,6 +40,75 @@ const branchUnionManage = {
|
||||
></el-pagination>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-dialog title="批量添加分工会干部" :visible.sync="cadreImportVisible" width="940px" top="5vh"
|
||||
append-to-body custom-class="union-cadre-import-dialog"
|
||||
:close-on-click-modal="false" :close-on-press-escape="!cadreImportLoading" :show-close="!cadreImportLoading">
|
||||
<el-alert title="已存在的记录自动跳过,不重复添加、不覆盖原记录。"
|
||||
description="已有角色(含禁用关系)、已有在任或待审核干部记录均会跳过,其他有效数据继续导入。"
|
||||
type="info" :closable="false" show-icon></el-alert>
|
||||
<div class="union-cadre-import-grid">
|
||||
<section class="union-cadre-import-panel">
|
||||
<div class="union-cadre-import-heading"><span class="union-cadre-import-number">1</span>下载并填写模板</div>
|
||||
<p class="union-cadre-import-muted">模板内附当前工会、角色、届次及所属单位对照表。</p>
|
||||
<el-button size="small" type="primary" plain icon="el-icon-download" @click="downloadCadreTemplate"
|
||||
:disabled="cadreImportLoading">下载模板及参数对照表</el-button>
|
||||
<dl class="union-cadre-import-fields">
|
||||
<dt>必填参数</dt><dd>工会编码、工号、角色编码</dd>
|
||||
<dt>选填参数</dt><dd>届次编码,可留空</dd>
|
||||
<dt>书记角色</dt><dd>所属单位编码必填,多个二级单位用逗号分隔;其他角色留空</dd>
|
||||
</dl>
|
||||
<div class="union-cadre-import-note">人员须属于对应分工会;工号与编码按文本填写,请保留模板表头。</div>
|
||||
</section>
|
||||
<section class="union-cadre-import-panel">
|
||||
<div class="union-cadre-import-heading"><span class="union-cadre-import-number">2</span>上传填写好的文件</div>
|
||||
<p class="union-cadre-import-muted">支持 .xls / .xlsx,每次一个文件,最多1000行有效数据。</p>
|
||||
<el-upload ref="cadreImportUpload" action="/platform/sys/unionCadreImport/doImport" drag
|
||||
accept=".xls,.xlsx" :auto-upload="false" :limit="1" :file-list="cadreImportFiles"
|
||||
:disabled="cadreImportLoading" :headers="{ 'X-Requested-With': 'XMLHttpRequest' }"
|
||||
:on-change="cadreImportChange" :on-remove="cadreImportRemove" :on-exceed="cadreImportExceed"
|
||||
:before-upload="beforeCadreImport" :on-success="cadreImportSuccess" :on-error="cadreImportError">
|
||||
<i :class="cadreImportLoading ? 'el-icon-loading' : 'el-icon-upload'"></i>
|
||||
<div class="el-upload__text" v-if="cadreImportLoading">正在导入,请稍候…</div>
|
||||
<div class="el-upload__text" v-else>将文件拖到此处,或<em>点击选择文件</em></div>
|
||||
</el-upload>
|
||||
<div class="union-cadre-import-note">选择文件后点击“开始导入”。逐行保存,一行失败不影响其他行。</div>
|
||||
</section>
|
||||
</div>
|
||||
<section v-if="cadreImportResult" class="union-cadre-import-results">
|
||||
<div class="union-cadre-import-heading">导入结果</div>
|
||||
<div class="union-cadre-import-summary">
|
||||
<div class="union-cadre-import-stat is-success"><span>成功添加</span><strong>{{ cadreImportResult.success }}<small>行</small></strong></div>
|
||||
<div class="union-cadre-import-stat is-skipped"><span>已跳过</span><strong>{{ cadreImportResult.skipped }}<small>行</small></strong></div>
|
||||
<div class="union-cadre-import-stat is-failed"><span>导入失败</span><strong>{{ cadreImportResult.failed }}<small>行</small></strong></div>
|
||||
</div>
|
||||
<el-radio-group v-model="cadreImportFilter" size="small" class="union-cadre-import-filters">
|
||||
<el-radio-button label="全部">全部</el-radio-button>
|
||||
<el-radio-button label="成功">成功</el-radio-button>
|
||||
<el-radio-button label="跳过">跳过</el-radio-button>
|
||||
<el-radio-button label="失败">失败</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-table :data="cadreImportRows" border stripe size="small" max-height="280" empty-text="暂无此类导入记录">
|
||||
<el-table-column prop="rowNumber" label="Excel行号" width="90"></el-table-column>
|
||||
<el-table-column prop="unionCode" label="工会编码" width="100" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="loginname" label="工号" width="100" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="roleCode" label="角色编码" min-width="160" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="结果" width="85" align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="row.status === '成功' ? 'success' : row.status === '跳过' ? 'info' : 'danger'">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="message" label="处理说明" min-width="240"></el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
<div slot="footer" class="union-cadre-import-footer">
|
||||
<span class="union-cadre-import-muted">{{ cadreImportLoading ? '正在处理,请勿关闭或刷新页面' : '仅系统管理员可操作 · 已存在的记录自动跳过' }}</span>
|
||||
<div>
|
||||
<el-button size="small" @click="cadreImportVisible = false" :disabled="cadreImportLoading">关闭</el-button>
|
||||
<el-button size="small" type="primary" icon="el-icon-upload2" @click="submitCadreImport" :loading="cadreImportLoading"
|
||||
:disabled="!cadreImportFiles.length">{{ cadreImportLoading ? '正在导入' : '开始导入' }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog :title="formData.id ? '编辑分工会' : '新增分工会'" :visible.sync="dialogFormVisible" width="600px">
|
||||
<el-form :model="formData" ref="formRef" :rules="formRules" size="small" label-width="80px">
|
||||
<el-form-item label="工会名称" prop="name">
|
||||
@@ -61,6 +131,11 @@ const branchUnionManage = {
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
cadreImportVisible: false,
|
||||
cadreImportLoading: false,
|
||||
cadreImportFiles: [],
|
||||
cadreImportResult: null,
|
||||
cadreImportFilter: "全部",
|
||||
dialogFormVisible: false,
|
||||
formData: {},
|
||||
formRules: {
|
||||
@@ -70,6 +145,12 @@ const branchUnionManage = {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cadreImportRows() {
|
||||
const rows = this.cadreImportResult ? this.cadreImportResult.rows : []
|
||||
return this.cadreImportFilter === "全部" ? rows : rows.filter(row => row.status === this.cadreImportFilter)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pageData() {
|
||||
$.get("/platform/sys/union/pageData", this.pageForm).then((res) => {
|
||||
@@ -77,6 +158,67 @@ const branchUnionManage = {
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
})
|
||||
},
|
||||
openCadreImport() {
|
||||
this.cadreImportFiles = []
|
||||
this.cadreImportResult = null
|
||||
this.cadreImportVisible = true
|
||||
this.$nextTick(() => this.$refs.cadreImportUpload.clearFiles())
|
||||
},
|
||||
downloadCadreTemplate() {
|
||||
commonUtil.downLoadService("/platform/sys/unionCadreImport/downloadTemplate", {})
|
||||
},
|
||||
cadreImportChange(file, fileList) {
|
||||
if (file.status === "ready") {
|
||||
this.cadreImportFiles = fileList
|
||||
this.cadreImportResult = null
|
||||
}
|
||||
},
|
||||
cadreImportRemove() {
|
||||
this.cadreImportFiles = []
|
||||
},
|
||||
cadreImportExceed() {
|
||||
this.$message.warning("每次只能导入一个文件,请先移除已选文件")
|
||||
},
|
||||
beforeCadreImport(file) {
|
||||
if (!/\.(xls|xlsx)$/i.test(file.name)) {
|
||||
this.$message.error("请选择xls或xlsx文件")
|
||||
return false
|
||||
}
|
||||
this.cadreImportLoading = true
|
||||
return true
|
||||
},
|
||||
submitCadreImport() {
|
||||
if (this.cadreImportLoading || !this.cadreImportFiles.length) {
|
||||
return
|
||||
}
|
||||
this.$refs.cadreImportUpload.submit()
|
||||
},
|
||||
cadreImportSuccess(res) {
|
||||
this.cadreImportLoading = false
|
||||
this.$refs.cadreImportUpload.clearFiles()
|
||||
this.cadreImportFiles = []
|
||||
if (!res || res.code !== 0 || !res.data) {
|
||||
this.$message.error((res && res.msg) || "导入失败,请重新选择文件")
|
||||
return
|
||||
}
|
||||
this.cadreImportFilter = "全部"
|
||||
this.cadreImportResult = res.data
|
||||
if (res.data.failed) {
|
||||
this.$message.warning("导入完成,部分行失败,请查看结果说明")
|
||||
} else {
|
||||
this.$message.success("导入完成,请查看结果")
|
||||
}
|
||||
if (res.data.success) {
|
||||
this.pageData()
|
||||
this.$emit("refresh", null)
|
||||
}
|
||||
},
|
||||
cadreImportError() {
|
||||
this.cadreImportLoading = false
|
||||
this.$refs.cadreImportUpload.clearFiles()
|
||||
this.cadreImportFiles = []
|
||||
this.$message.error("导入请求失败,可重新选择文件重试,已分配的角色将自动跳过")
|
||||
},
|
||||
// 基层干部审核
|
||||
openAudit() {
|
||||
this.$emit('union-cadre')
|
||||
|
||||
@@ -49,6 +49,37 @@ layout("/layouts/platform.html"){
|
||||
.union-branch-list-panel .el-pagination-container {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.union-cadre-import-dialog { max-width: calc(100vw - 32px); }
|
||||
.union-cadre-import-dialog .el-dialog__body { padding: 16px 24px; max-height: 72vh; overflow-y: auto; }
|
||||
.union-cadre-import-dialog .el-dialog__header { padding: 20px 24px 16px; border-bottom: 1px solid #ebeef5; }
|
||||
.union-cadre-import-dialog .el-dialog__footer { padding: 16px 24px; border-top: 1px solid #ebeef5; }
|
||||
.union-cadre-import-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 20px; }
|
||||
.union-cadre-import-panel { min-width: 0; padding: 20px; border: 1px solid #e4e7ed; border-radius: 8px; }
|
||||
.union-cadre-import-heading { display: flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; color: #303133; }
|
||||
.union-cadre-import-number { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; background: #ecf5ff; color: #409eff; font-size: 13px; }
|
||||
.union-cadre-import-muted { color: #606266; font-size: 12px; line-height: 1.7; }
|
||||
.union-cadre-import-fields { display: grid; grid-template-columns: 68px 1fr; gap: 10px; margin: 20px 0 14px; font-size: 12px; line-height: 1.7; }
|
||||
.union-cadre-import-fields dt { color: #909399; }
|
||||
.union-cadre-import-fields dd { margin: 0; color: #303133; }
|
||||
.union-cadre-import-note { margin-top: 14px; color: #606266; font-size: 12px; line-height: 1.8; }
|
||||
.union-cadre-import-panel .el-upload { display: block; width: 100%; }
|
||||
.union-cadre-import-panel .el-upload-dragger { width: 100%; height: 180px; background: #fafcff; }
|
||||
.union-cadre-import-panel .el-upload-dragger .el-icon-loading { display: block; margin: 40px auto 20px; font-size: 40px; color: #409eff; }
|
||||
.union-cadre-import-results { margin-top: 24px; }
|
||||
.union-cadre-import-summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 14px 0; }
|
||||
.union-cadre-import-stat { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-radius: 6px; font-size: 13px; }
|
||||
.union-cadre-import-stat strong { font-size: 24px; }
|
||||
.union-cadre-import-stat small { margin-left: 6px; font-size: 12px; font-weight: normal; }
|
||||
.union-cadre-import-stat.is-success { color: #529b2e; background: #f0f9eb; }
|
||||
.union-cadre-import-stat.is-skipped { color: #606266; background: #f4f4f5; }
|
||||
.union-cadre-import-stat.is-failed { color: #c45656; background: #fef0f0; }
|
||||
.union-cadre-import-filters { margin-bottom: 12px; }
|
||||
.union-cadre-import-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; text-align: left; }
|
||||
@media (max-width: 720px) {
|
||||
.union-cadre-import-grid { grid-template-columns: 1fr; }
|
||||
.union-cadre-import-dialog .el-dialog__body { padding: 16px; }
|
||||
.union-cadre-import-stat { padding: 12px 8px; flex-direction: column; gap: 6px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
const CLUB_REGISTER_IMPORT_TEMPLATE = {
|
||||
template: `
|
||||
<el-dialog title="批量导入社团" :visible.sync="visible" width="920px" top="5vh" append-to-body
|
||||
custom-class="club-register-import-dialog" :close-on-click-modal="false"
|
||||
:close-on-press-escape="!loading" :show-close="!loading">
|
||||
<el-alert title="导入后直接通过,无需审批" type="info" :closable="false" show-icon
|
||||
description="已存在的社团名称或编码自动跳过,不覆盖原有数据。每行独立处理,失败行不影响其他社团。"></el-alert>
|
||||
<div class="club-register-import-grid">
|
||||
<section class="club-register-import-panel">
|
||||
<h4>1. 下载并填写模板</h4>
|
||||
<el-button type="primary" plain size="small" icon="el-icon-download" @click="download" :disabled="loading">下载模板及参数说明</el-button>
|
||||
<p><strong>必填:</strong>名称、类型编码、成立日期、会长和秘书长工号、至少3名发起人工号。</p>
|
||||
<p><strong>人员:</strong>会长和秘书长各1人,副职选填。多人工号用逗号分隔,理事人员不能重复。</p>
|
||||
<p><strong>选填:</strong>社团编码留空自动生成;联系人、会费、介绍、缩略图及附件等详见模板。</p>
|
||||
<p class="club-register-import-muted">社团类型来自系统字典,模板内附当前类型对照表。</p>
|
||||
</section>
|
||||
<section class="club-register-import-panel">
|
||||
<h4>2. 上传Excel文件</h4>
|
||||
<el-upload ref="upload" drag action="/platform/club/infoManage/registerImport/doImport"
|
||||
accept=".xls,.xlsx" :auto-upload="false" :limit="1" :disabled="loading"
|
||||
:file-list="files" :headers="{ 'X-Requested-With': 'XMLHttpRequest' }"
|
||||
:on-change="change" :on-remove="remove" :on-exceed="exceed" :before-upload="beforeUpload"
|
||||
:on-success="success" :on-error="error">
|
||||
<i :class="loading ? 'el-icon-loading' : 'el-icon-upload'"></i>
|
||||
<div class="el-upload__text">{{ loading ? '正在导入,请稍候…' : '拖拽文件到此处,或点击选择文件' }}</div>
|
||||
</el-upload>
|
||||
<p class="club-register-import-muted">支持xls、xlsx,每次最多1000行有效数据。保留完整表头,工号与编码按文本填写。</p>
|
||||
<p class="club-register-import-muted">附件列可留空;填写时使用已上传文件的ID,模板本身不上传附件。</p>
|
||||
</section>
|
||||
</div>
|
||||
<section v-if="result">
|
||||
<div class="club-register-import-summary">
|
||||
<el-tag type="success">成功 {{ result.success }} 行</el-tag>
|
||||
<el-tag type="info">跳过 {{ result.skipped }} 行</el-tag>
|
||||
<el-tag type="danger">失败 {{ result.failed }} 行</el-tag>
|
||||
</div>
|
||||
<el-radio-group v-model="filter" size="small" style="margin-bottom: 12px">
|
||||
<el-radio-button v-for="item in ['全部', '成功', '跳过', '失败']" :key="item" :label="item">{{ item }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-table :data="rows" border stripe size="small" max-height="280" empty-text="暂无此类记录">
|
||||
<el-table-column prop="rowNumber" label="Excel行号" width="90"></el-table-column>
|
||||
<el-table-column prop="clubName" label="社团名称" min-width="150" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="clubCode" label="社团编码" width="120" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="结果" width="80" align="center">
|
||||
<template slot-scope="{row}"><el-tag size="mini" :type="row.status === '成功' ? 'success' : row.status === '跳过' ? 'info' : 'danger'">{{ row.status }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="message" label="处理说明" min-width="260"></el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
<div slot="footer" class="club-register-import-footer">
|
||||
<span class="club-register-import-muted">{{ loading ? '正在处理,请勿关闭或刷新页面' : '仅系统管理员可导入 · 已存在自动跳过' }}</span>
|
||||
<div>
|
||||
<el-button size="small" @click="visible = false" :disabled="loading">关闭</el-button>
|
||||
<el-button size="small" type="primary" icon="el-icon-upload2" :loading="loading" :disabled="!files.length" @click="submit">开始导入</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
`,
|
||||
data() {
|
||||
return { visible: false, loading: false, files: [], result: null, filter: "全部" }
|
||||
},
|
||||
computed: {
|
||||
rows() {
|
||||
const rows = this.result ? this.result.rows : []
|
||||
return this.filter === "全部" ? rows : rows.filter(row => row.status === this.filter)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.files = []
|
||||
this.result = null
|
||||
this.filter = "全部"
|
||||
this.visible = true
|
||||
this.$nextTick(() => this.$refs.upload.clearFiles())
|
||||
},
|
||||
download() {
|
||||
commonUtil.downLoadService("/platform/club/infoManage/registerImport/downloadTemplate", {})
|
||||
},
|
||||
change(file, files) {
|
||||
if (file.status === "ready") {
|
||||
this.files = files
|
||||
this.result = null
|
||||
}
|
||||
},
|
||||
remove() { this.files = [] },
|
||||
exceed() { this.$message.warning("每次选择一个文件,请先移除已选文件") },
|
||||
beforeUpload(file) {
|
||||
if (!/\.(xls|xlsx)$/i.test(file.name)) {
|
||||
this.$message.error("请选择xls或xlsx文件")
|
||||
return false
|
||||
}
|
||||
this.loading = true
|
||||
return true
|
||||
},
|
||||
submit() {
|
||||
if (!this.loading && this.files.length) this.$refs.upload.submit()
|
||||
},
|
||||
success(res) {
|
||||
this.loading = false
|
||||
this.files = []
|
||||
this.$refs.upload.clearFiles()
|
||||
if (!res || res.code !== 0 || !res.data) {
|
||||
this.$message.error((res && res.msg) || "导入失败,请重新选择文件")
|
||||
return
|
||||
}
|
||||
this.result = res.data
|
||||
this.filter = "全部"
|
||||
this.$message[res.data.failed ? 'warning' : 'success']("导入完成,请查看逐行处理结果")
|
||||
if (res.data.success) this.$emit("completed")
|
||||
},
|
||||
error() {
|
||||
this.loading = false
|
||||
this.files = []
|
||||
this.$refs.upload.clearFiles()
|
||||
this.$message.error("导入请求失败,可重新选择文件重试,已存在的社团会自动跳过")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,24 @@ layout("/layouts/platform.html"){
|
||||
#app .club-manage-list-card .el-pagination-container {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.club-register-import-dialog { max-width: calc(100vw - 32px); }
|
||||
.club-register-import-dialog .el-dialog__body { padding: 16px 24px; max-height: 72vh; overflow-y: auto; }
|
||||
.club-register-import-dialog .el-dialog__header { padding: 20px 24px 16px; border-bottom: 1px solid #ebeef5; }
|
||||
.club-register-import-dialog .el-dialog__footer { padding: 16px 24px; border-top: 1px solid #ebeef5; }
|
||||
.club-register-import-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 20px 0; }
|
||||
.club-register-import-panel { min-width: 0; padding: 18px; border: 1px solid #e4e7ed; border-radius: 8px; }
|
||||
.club-register-import-panel h4 { margin: 0 0 16px; color: #303133; }
|
||||
.club-register-import-panel p { font-size: 12px; line-height: 1.8; }
|
||||
.club-register-import-muted { color: #606266; font-size: 12px; line-height: 1.7; }
|
||||
.club-register-import-panel .el-upload, .club-register-import-panel .el-upload-dragger { width: 100%; }
|
||||
.club-register-import-panel .el-upload-dragger { background: #fafcff; }
|
||||
.club-register-import-panel .el-icon-loading { display: block; margin: 40px auto 20px; font-size: 40px; color: #409eff; }
|
||||
.club-register-import-summary { display: flex; gap: 12px; margin-bottom: 16px; }
|
||||
.club-register-import-footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
@media (max-width: 720px) {
|
||||
.club-register-import-grid { grid-template-columns: 1fr; }
|
||||
.club-register-import-dialog .el-dialog__body { padding: 16px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
@@ -192,6 +210,7 @@ layout("/layouts/platform.html"){
|
||||
<!--#include("../../register/apply/clubRegisterForm.js"){}#-->
|
||||
<!--#include("../../common/clubInfoComponent.js"){}#-->
|
||||
<!--#include("../../common/clubRoleConstant.js"){}#-->
|
||||
<!--#include("clubRegisterImport.js"){}#-->
|
||||
<!--#include("schoolClubManage.js"){}#-->
|
||||
<!--#include("clubInfoManage.js"){}#-->
|
||||
new Vue({
|
||||
|
||||
@@ -22,6 +22,8 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = {
|
||||
<el-card shadow="never" class="mt10 club-manage-list-card">
|
||||
<table-tool label="社团列表">
|
||||
<template slot="content">
|
||||
<el-button v-if="$auth.hasRole('SYSADMIN')" type="primary" size="small" icon="el-icon-upload2"
|
||||
@click="openImport">批量导入社团</el-button>
|
||||
<!-- 一级节点展示社团总览时,引导用户从左侧树选择社团查看详细信息。 -->
|
||||
<span v-if="parentNode.currentTreeNode && parentNode.currentTreeNode.level === 1"
|
||||
style="display: block; text-align: center; color: #f56c6c;">
|
||||
@@ -71,10 +73,14 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = {
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</el-card>
|
||||
<club-register-import ref="registerImport" @completed="importCompleted"></club-register-import>
|
||||
</div>
|
||||
`,
|
||||
mixins: [initTableMixins, CLUB_FIXED_TABLE_MIXIN],
|
||||
components: {
|
||||
"club-register-import": CLUB_REGISTER_IMPORT_TEMPLATE
|
||||
},
|
||||
props: {
|
||||
parentNode: {
|
||||
type: Object,
|
||||
@@ -122,7 +128,13 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = {
|
||||
this.parentNode.getTreeData()
|
||||
}
|
||||
},
|
||||
openImport() {},
|
||||
openImport() {
|
||||
this.$refs.registerImport.open()
|
||||
},
|
||||
importCompleted() {
|
||||
this.doSearch()
|
||||
this.parentNode.getTreeData()
|
||||
},
|
||||
pageData() {
|
||||
// 查询期间清空旧页并显示加载状态,快速连续操作时只接收最后一次响应。
|
||||
const requestSequence = ++this.pageRequestSequence
|
||||
|
||||
Reference in New Issue
Block a user