This commit is contained in:
zhouhefeng
2026-07-15 11:47:47 +08:00
parent 9b4f4d57be
commit 3425888c91
14 changed files with 691 additions and 589 deletions
@@ -41,15 +41,11 @@ import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@IocBean
@@ -58,6 +54,11 @@ import java.util.UUID;
@Api(tags = "登录")
public class SysLoginController {
private static final Log log = Logs.get();
private static final LoginRsaKeyRing LOGIN_RSA_KEY_RING = new LoginRsaKeyRing();
/**
* TODO: 临时屏蔽 localhost 平台登录验证码,恢复时改为 false。
*/
private static final boolean DISABLE_LOGIN_CAPTCHA = true;
@Inject
private SysUserService sysUserService;
@Inject
@@ -123,14 +124,20 @@ public class SysLoginController {
}
try {
// 验证码校验
try {
validateService.checkCode(captchaKey, captchaCode);
} catch (BaseException e) {
return Result.error(e.getMessage());
if (!isLoginCaptchaDisabled(req)) {
try {
validateService.checkCode(captchaKey, captchaCode);
} catch (BaseException e) {
return Result.error(e.getMessage());
}
}
// 解密密码
String decryptPwd = validateService.decryptPwd(keyId, password);
LoginRsaKey rsaKey = LOGIN_RSA_KEY_RING.getPrivateKey(keyId);
if (rsaKey == null) {
throw new BaseException("Login key expired, please refresh the login page");
}
String decryptPwd = RsaUtils.decrypt(password, rsaKey.privateKey);
if (decryptPwd == null) {
throw new BaseException("用户登录失败");
}
@@ -153,6 +160,17 @@ public class SysLoginController {
}
}
private boolean isLoginCaptchaDisabled(HttpServletRequest req) {
if (!DISABLE_LOGIN_CAPTCHA || req == null) {
return false;
}
String serverName = req.getServerName();
return "localhost".equalsIgnoreCase(serverName)
|| "127.0.0.1".equals(serverName)
|| "0:0:0:0:0:0:0:1".equals(serverName)
|| "::1".equals(serverName);
}
@At(value = "/platform/sso/login", top = true)
@Ok("re")
@ApiOperation("用户cas登录统一入口")
@@ -267,25 +285,89 @@ public class SysLoginController {
@ApiOperation("获取公钥")
public Object publicKey() {
try {
// 生成密钥对
KeyPair keyPair = RsaUtils.generateKeyPair();
String publicKeyStr = RsaUtils.getPublicKeyBase64(keyPair.getPublic());
String privateKeyStr = RsaUtils.getPrivateKeyBase64(keyPair.getPrivate());
// 生成 UUID 作为 keyId
String keyId = UUID.randomUUID().toString().replace("-", "");
// 私钥存入 Redis,5 分钟过期,也防止恶意刷密钥
redisService.setex(RedisConstant.RSA_KEY_PREFIX + keyId, 5 * 60, privateKeyStr);
// 返回给前端
LoginRsaKey rsaKey = LOGIN_RSA_KEY_RING.currentKey();
return Result.success(Map.of(
"publicKey", publicKeyStr,
"keyId", keyId
"publicKey", rsaKey.publicKey,
"keyId", rsaKey.keyId,
"expireAt", rsaKey.encryptExpireAt
));
} catch (Exception e) {
log.error("生成 RSA 密钥失败", e);
return Result.error("系统异常");
}
}
private static class LoginRsaKeyRing {
private static final long ACTIVE_MILLIS = 30 * 60 * 1000L;
private static final long DECRYPT_GRACE_MILLIS = 10 * 60 * 1000L;
private final ConcurrentHashMap<String, LoginRsaKey> keys = new ConcurrentHashMap<>();
private volatile LoginRsaKey current;
LoginRsaKey currentKey() throws Exception {
long now = System.currentTimeMillis();
LoginRsaKey key = current;
if (key != null && now < key.encryptExpireAt) {
return key;
}
synchronized (this) {
key = current;
now = System.currentTimeMillis();
if (key == null || now >= key.encryptExpireAt) {
key = generateKey(now);
current = key;
keys.put(key.keyId, key);
cleanup(now);
}
return key;
}
}
LoginRsaKey getPrivateKey(String keyId) {
if (StrUtil.isBlank(keyId)) {
return null;
}
long now = System.currentTimeMillis();
LoginRsaKey key = keys.get(keyId);
if (key == null) {
return null;
}
if (now >= key.decryptExpireAt) {
keys.remove(keyId);
return null;
}
return key;
}
private LoginRsaKey generateKey(long now) throws Exception {
KeyPair keyPair = RsaUtils.generateKeyPair();
String keyId = UUID.randomUUID().toString().replace("-", "");
return new LoginRsaKey(
keyId,
RsaUtils.getPublicKeyBase64(keyPair.getPublic()),
keyPair.getPrivate(),
now + ACTIVE_MILLIS,
now + ACTIVE_MILLIS + DECRYPT_GRACE_MILLIS
);
}
private void cleanup(long now) {
keys.entrySet().removeIf(entry -> now >= entry.getValue().decryptExpireAt);
}
}
private static class LoginRsaKey {
private final String keyId;
private final String publicKey;
private final PrivateKey privateKey;
private final long encryptExpireAt;
private final long decryptExpireAt;
private LoginRsaKey(String keyId, String publicKey, PrivateKey privateKey, long encryptExpireAt, long decryptExpireAt) {
this.keyId = keyId;
this.publicKey = publicKey;
this.privateKey = privateKey;
this.encryptExpireAt = encryptExpireAt;
this.decryptExpireAt = decryptExpireAt;
}
}
}
@@ -25,7 +25,6 @@ import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
@@ -69,15 +68,11 @@ public class TeacherCongressInstitutionController {
Teacher_congress_institution rootInstitution = new Teacher_congress_institution();
rootInstitution.setId("0");
rootInstitution.setParentId("");
rootInstitution.setName("代会组织机构");
rootInstitution.setName("代会机构");
rootInstitution.setLocation(0);
institutionList.add(rootInstitution);
List<TreeNode<String>> treeNodes = institutionList.stream().map(institution -> {
TreeNode<String> treeNode = new TreeNode<>(institution.getId(), institution.getParentId(), institution.getName(), institution.getLocation());
treeNode.setExtra(Map.of("code", Strings.sNull(institution.getCode())));
return treeNode;
}).toList();
List<TreeNode<String>> treeNodes = institutionList.stream().map(institution -> new TreeNode<>(institution.getId(), institution.getParentId(), institution.getName(), institution.getLocation())).toList();
List<Tree<String>> treeList = TreeUtil.build(treeNodes, "");
return Result.success(treeList);
}
@@ -102,16 +97,6 @@ public class TeacherCongressInstitutionController {
return Result.success(Map.of("treeFlat", children, "treeList", treeList));
}
@At
@SaCheckPermission("tc.institution")
public Result specialCommitteeRoleOptions() {
Sys_dict parent = sysDictService.fetch(Cnd.where(Sys_dict::getCode, "=", "SPECIAL_COMMITTEE_ROLES"));
if (ObjectUtil.isEmpty(parent)) {
return Result.success(List.of());
}
List<Sys_dict> children = sysDictService.query(Cnd.where(Sys_dict::getPath, "like", parent.getPath() + "%").and(Sys_dict::getId, "!=", parent.getId()).asc(Sys_dict::getLocation));
return Result.success(children);
}
/**
* 机构分页查询
@@ -128,7 +113,6 @@ public class TeacherCongressInstitutionController {
cnd.and("parentId", "=", parentId);
cnd.and("sessionId", "=", sessionId);
cnd.asc("location");
cnd.asc("code");
Pagination pagination = baseService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), "teacher_congress_institution", cnd);
return Result.success(pagination);
}
@@ -145,7 +129,7 @@ public class TeacherCongressInstitutionController {
if (dao.count(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getCode, "=", institution.getCode()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId())) > 0) {
return Result.error("机构已存在");
}
if (StrUtil.isNotBlank(institution.getParentId()) && !institution.getParentId().equals("0") && dao.count(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getId, "=", institution.getParentId()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId())) == 0) {
if (StrUtil.isNotBlank(institution.getParentId()) && !institution.getParentId().equals("0")) {
Sys_dict sysDict = sysDictService.fetch(Cnd.where(Sys_dict::getId, "=", institution.getParentId()));
Teacher_congress_institution parentInstitution = dao.fetch(Teacher_congress_institution.class,
Cnd.where(Teacher_congress_institution::getCode, "=", sysDict.getCode())
@@ -167,34 +151,10 @@ public class TeacherCongressInstitutionController {
* @param id
* @return
*/
@At
@SaCheckPermission("tc.institution")
public Result update(Teacher_congress_institution institution) {
Teacher_congress_institution dbInstitution = dao.fetch(Teacher_congress_institution.class, institution.getId());
if (ObjectUtil.isEmpty(dbInstitution)) {
return Result.error("机构不存在");
}
dbInstitution.setName(institution.getName());
dbInstitution.setIntroduce(institution.getIntroduce());
dbInstitution.setLocation(institution.getLocation());
dao.update(dbInstitution);
return Result.success();
}
@At
@SaCheckPermission("tc.institution")
@Aop(TransAop.READ_COMMITTED)
public Result delete(@Valid String id) {
Teacher_congress_institution institution = dao.fetch(Teacher_congress_institution.class, id);
if (ObjectUtil.isEmpty(institution)) {
return Result.error("机构不存在");
}
List<Teacher_congress_institution> siblingList = dao.query(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getParentId, "=", institution.getParentId()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId()).asc(Teacher_congress_institution::getLocation).asc(Teacher_congress_institution::getCode));
for (int i = 0; i < siblingList.size() && i < 6; i++) {
if (id.equals(siblingList.get(i).getId())) {
return Result.error("你选择的组织机构是两代会基本机构,不允许删除。");
}
}
dao.delete(Teacher_congress_institution.class, id);
dao.clear(Teacher_congress_institution_user.class, Cnd.where("institutionId", "=", id));
return Result.success();
@@ -254,8 +214,8 @@ public class TeacherCongressInstitutionController {
@SaCheckPermission("tc.institution")
@SLog(tag = "教代会-机构设置", msg = "添加人员")
@Aop(TransAop.READ_COMMITTED)
public Result userInsert(@Valid String userId, @Valid String institutionId, @Valid String sessionId, @Valid String identity, String roleCode) {
teacherCongressInstitutionUserService.userInsert(userId, institutionId, sessionId, identity, roleCode);
public Result userInsert(@Valid String userId, @Valid String institutionId, @Valid String sessionId, @Valid String identity) {
teacherCongressInstitutionUserService.userInsert(userId, institutionId, sessionId, identity);
return Result.success();
}
@@ -14,7 +14,7 @@ public interface TeacherCongressInstitutionUserService extends BaseService<Teach
* @param sessionId 届次id
* @param identity 身份 主任|副主任|成员
*/
void userInsert(String userId, String institutionId, String sessionId, String identity, String roleCode);
void userInsert(String userId, String institutionId, String sessionId, String identity);
/**
* 删除机构成员
@@ -1,6 +1,5 @@
package com.budwk.app.zhgh.democratic.teachercongress.institution.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
@@ -31,7 +30,7 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
@Override
@Aop(TransAop.READ_COMMITTED)
public void userInsert(String userId, String institutionId, String sessionId, String identity, String roleCode) {
public void userInsert(String userId, String institutionId, String sessionId, String identity) {
int count = dao().count(Teacher_congress_institution_user.class,
Cnd.where(Teacher_congress_institution_user::getInstitutionId, "=", institutionId)
.and(Teacher_congress_institution_user::getUserId, "=", userId)
@@ -50,28 +49,6 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
dao().insert(institutionUser);
Teacher_congress_institution institution = dao().fetch(Teacher_congress_institution.class, institutionId);
if ("ZWH001".equals(institution.getCode()) || "ZXWYH".equals(institution.getCode()) || "DBZGSCXZ".equals(institution.getCode())) {
if (StrUtil.isBlank(roleCode)) {
throw new BaseException("请选择角色");
}
Sys_role sysRole = sysRoleService.fetch(Cnd.where(Sys_role::getCode, "=", roleCode));
if (sysRole == null) {
throw new BaseException("无法找到" + roleCode + "对应编码的角色");
}
int existsRole = dao().count(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", userId)
.and(Sys_user_role::getTcSessionId, "=", sessionId)
.and(Sys_user_role::getRoleId, "=", sysRole.getId())
);
if (existsRole == 0) {
Sys_user_role insertSysUserRole = new Sys_user_role();
insertSysUserRole.setRoleId(sysRole.getId());
insertSysUserRole.setUserId(userId);
insertSysUserRole.setTcSessionId(sessionId);
dao().insert(insertSysUserRole);
sysRoleService.clearCache();
sysUserService.clearCache();
}
}
if (institution.getCode().contains("TEACHER_CONGRESS_INSTITUTION_PROPOSAL_COMMITTEE") && identity.equals("主任")) {
Sys_role sysRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_COMMITTEE_DIRECTOR);
int existsRole = dao().count(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", userId)
@@ -76,6 +76,10 @@ const act = {
},
/*初始化Swiper*/
initSwiper() {
if (this.swiper) {
this.swiper.destroy(true, true)
this.swiper = null
}
this.swiper = new Swiper('.activity-swiper', {
slidesPerView: 1,
spaceBetween: 18,
@@ -133,6 +137,7 @@ const act = {
style: /*language=CSS*/ `
.activity-section-wrapper {
width: 100%;
min-width: 0;
}
.section-act .section-act-title span {
@@ -176,17 +181,23 @@ const act = {
.activity-swiper-container {
position: relative;
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 9px 14px;
/*margin-top: 30px;*/
overflow: hidden;
}
.activity-swiper {
width: 100%;
min-width: 0;
/*padding: 0 50px 20px 50px;*/
overflow: hidden;
}
.activity-swiper .swiper-slide {
height: auto;
min-width: 0;
}
/* 导航按钮样式 */
@@ -235,6 +246,8 @@ const act = {
height: auto;
position: relative;
cursor: pointer;
min-width: 0;
overflow: hidden;
}
.activity-swiper .swiper-slide .item .img-box {
@@ -260,9 +273,12 @@ const act = {
.activity-swiper .swiper-slide .item .img-box img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
width: 100% !important;
height: 100% !important;
max-width: 100% !important;
max-height: 128px !important;
object-fit: cover !important;
transition: transform 0.3s ease;
}
@@ -298,6 +314,9 @@ const act = {
margin: 0;
line-height: 1.4;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.activity-swiper .swiper-slide .item .time i {
@@ -17,7 +17,8 @@ const entry = {
:key="item.id || index"
@click="openService(item, section.key)">
<div class="app-icon">
<img :src="item.picIcon" alt="" />
<img v-if="item.picIcon" class="app-icon-img" :src="item.picIcon" alt="" />
<i v-else class="fa fa-cube app-icon-fallback"></i>
</div>
<div class="app-name">{{ item.name }}</div>
</div>
@@ -167,7 +168,7 @@ const entry = {
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
justify-items: center;
justify-content: start;
gap: 12px 18px;
@@ -185,13 +186,17 @@ const entry = {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border-radius: 8px;
background: #fff;
width: 100%;
max-width: 100%;
max-width: 108px;
min-width: 0;
box-sizing: border-box;
padding: 6px;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
overflow: hidden;
}
.app-item:hover {
@@ -200,24 +205,46 @@ const entry = {
}
.app-icon {
width: 46px;
height: 46px;
width: 56px;
height: 56px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 56px;
overflow: hidden;
border-radius: 8px;
background: #f5f8ff;
}
.app-icon img {
width: 100%;
height: 100%;
object-fit: contain;
.app-icon img,
.app-icon-img {
display: block;
width: 56px !important;
height: 56px !important;
max-width: 56px !important;
max-height: 56px !important;
object-fit: contain !important;
border-radius: 8px;
}
.app-icon-fallback {
color: #409eff;
font-size: 24px;
}
.app-name {
min-height: 34px;
width: 100%;
font-size: 12px;
color: #333;
line-height: 1.4;
word-break: break-word;
text-align: center;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.app-empty {
@@ -4,225 +4,268 @@ layout("/layouts/platform.html"){
<style>
#app {
display: flex;
width: 100%;
min-height: calc(100vh - 64px);
justify-content: center;
align-items: center;
min-height: calc(100vh - 40px);
background-color: #f0f2f5;
}
.tool-card {
.box-card {
width: calc(100% - 20px);
min-height: calc(100vh - 60px);
}
.tool-card .el-card__body {
height: calc(100% - 57px);
}
.tool-body {
display: flex;
justify-content: center;
min-height: calc(100vh - 180px);
}
.tool-body-center {
width: 70%;
min-width: 900px;
min-height: calc(100vh - 84px);
position: relative;
}
.wizard-box {
margin-top: 20px;
border: 1px solid #ebeef5;
border-radius: 8px;
background: #fff;
.box-card .el-card__body {
min-height: calc(100vh - 170px);
position: relative;
}
.box-card-title {
font-size: 20px;
}
.card-body {
display: flex;
justify-content: center;
min-height: calc(100vh - 170px);
position: relative;
}
.card-body-center {
width: 60%;
min-width: 800px;
min-height: calc(100vh - 170px);
position: relative;
padding-bottom: 70px;
box-sizing: border-box;
}
.card-body-center-bottom {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
display: flex;
justify-content: center;
}
.card-body-center-bottom button {
margin: 0 10px;
}
.card-body-center > .el-carousel {
height: calc(100vh - 300px);
min-height: 560px;
position: relative;
overflow-y: hidden;
margin-top: 10px;
}
.card-body-center > .el-carousel > .el-carousel__container {
height: 100%;
}
.carousel-table-item {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
padding: 10px 0;
box-sizing: border-box;
}
.carousel-table-item-center {
height: 100%;
width: 90%;
min-width: 800px;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
.wizard-panel {
padding: 18px;
min-height: 520px;
}
.option-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.option-list {
width: 100%;
display: block;
max-height: 470px;
overflow-y: auto;
padding-right: 8px;
box-sizing: border-box;
}
.option-list .el-radio,
.option-list .el-checkbox {
display: flex;
width: 100%;
margin: 0 0 12px 0 !important;
align-items: center;
}
.option-list .el-radio.is-bordered,
.option-list .el-checkbox.is-bordered {
margin-left: 0 !important;
width: 100%;
box-sizing: border-box;
}
.option-list .el-radio__label,
.option-list .el-checkbox__label {
display: flex;
.carousel-table-item-center .el-card__body {
flex: 1;
min-width: 0;
padding-right: 0;
height: auto;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
padding-bottom: 20px;
}
.check-group {
display: block;
overflow: visible;
padding: 0 10px 5px 0;
height: auto;
min-height: 100%;
box-sizing: border-box;
}
.check-group .el-radio,
.check-group .el-checkbox {
display: block;
width: 100%;
box-sizing: border-box;
padding: 12px;
margin: 0 !important;
margin-top: 15px !important;
}
.check-group .el-checkbox__label,
.check-group .el-radio__label {
width: calc(100% - 16px);
}
.option-extra {
float: right;
color: #f56c6c;
margin-left: auto;
padding-left: 12px;
flex-shrink: 0;
}
.option-item-content {
display: flex;
align-items: center;
.carousel-table-item-relation .el-card__body,
.carousel-table-item-form .el-card__body {
position: relative;
height: 100%;
box-sizing: border-box;
}
.carousel-table-item-form .el-card__body {
overflow-y: auto;
}
.result-card {
width: 100%;
min-width: 0;
gap: 8px;
margin-top: 6px;
}
.option-item-main {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.relation-table {
height: 500px;
}
.submit-panel {
max-width: 900px;
margin: 0 auto;
padding-top: 10px;
}
.result-panel {
min-height: 420px;
.result-card .el-card__body {
min-height: 170px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.wizard-footer {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 18px;
.result-text {
font-size: 12px;
color: rgb(100, 100, 100);
margin-top: 20px;
}
</style>
<div id="app" v-cloak>
<el-card class="tool-card" shadow="never">
<div slot="header">
<span>高级工具</span>
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix">
<span class="box-card-title">DataKid</span>
</div>
<div class="tool-body">
<div class="tool-body-center">
<el-steps :active="active" align-center finish-status="success">
<el-step title="选择数据表" icon="el-icon-coin"></el-step>
<el-step title="选择字段" icon="el-icon-s-grid"></el-step>
<el-step title="字段对应" icon="el-icon-connection"></el-step>
<el-step title="提交导入" icon="el-icon-upload2"></el-step>
<el-step title="导入结果" icon="el-icon-success"></el-step>
<div class="card-body">
<div class="card-body-center">
<el-steps :active="active" align-center>
<el-step title="选择数据表" icon="el-icon-document-copy"></el-step>
<el-step title="选择数据列" icon="el-icon-s-unfold"></el-step>
<el-step title="数据列对应" icon="el-icon-s-operation"></el-step>
<el-step title="提交数据" icon="el-icon-upload"></el-step>
<el-step title="导入结果" icon="el-icon-bell"></el-step>
</el-steps>
<div class="wizard-box">
<div class="wizard-panel" v-if="active === 0">
<div class="option-toolbar">
<el-input
v-model.trim="tableKeyword"
clearable
placeholder="输入表名或备注搜索"
style="width: 260px">
</el-input>
<el-button type="primary" icon="el-icon-search" @click="filterTables">搜索</el-button>
<el-button v-if="formData.tableName" type="text">当前选择:{{ formData.tableName }}</el-button>
</div>
<el-radio-group v-model="formData.tableName" class="option-list">
<el-radio
v-for="item in filteredTables"
:key="item.table_name"
:label="item.table_name"
border>
<div class="option-item-content">
<span class="option-item-main">{{ item.table_name }}</span>
<span v-if="item.table_comment" class="option-item-main">{{ item.table_comment }}</span>
<el-carousel indicator-position="none" :autoplay="false" arrow="never" ref="carousel" direction="vertical">
<el-carousel-item class="carousel-table-item">
<el-card class="carousel-table-item-center">
<div slot="header" class="clearfix">
<el-input
style="width: 200px"
v-model.trim="tableKeyword"
placeholder="请输入关键字查询"
clearable>
</el-input>
<el-button type="primary" icon="el-icon-search" @click="filterTables"></el-button>
<el-button v-if="formData.tableName" style="float: right; padding: 10px 0" type="text">
当前选中:{{ formData.tableName }}
</el-button>
</div>
<el-radio-group class="check-group" v-model="formData.tableName">
<el-radio
v-for="item in filteredTables"
:key="item.table_name"
:label="item.table_name"
border>
{{ item.table_name }}{{ item.table_comment ? " ( " + item.table_comment + " )" : "" }}
<span class="option-extra">rows: {{ item.table_rows || 0 }}</span>
</div>
</el-radio>
</el-radio-group>
</div>
</el-radio>
</el-radio-group>
</el-card>
</el-carousel-item>
<div class="wizard-panel" v-if="active === 1">
<div class="option-toolbar">
<el-input
v-model.trim="columnKeyword"
clearable
placeholder="输入字段名或备注搜索"
style="width: 260px">
</el-input>
<el-button type="primary" icon="el-icon-search" @click="filterColumns">搜索</el-button>
<el-button type="text">{{ formData.tableName }}</el-button>
</div>
<el-checkbox-group v-model="formData.columns" class="option-list">
<el-checkbox
v-for="item in filteredColumns"
:key="item.column_name"
:label="item.column_name"
border>
<div class="option-item-content">
<span class="option-item-main">{{ item.column_name }}</span>
<span v-if="item.column_comment" class="option-item-main">{{ item.column_comment }}</span>
<el-carousel-item class="carousel-table-item">
<el-card class="carousel-table-item-center">
<div slot="header" class="clearfix">
<el-input
style="width: 200px"
v-model.trim="columnKeyword"
placeholder="请输入关键字查询"
clearable>
</el-input>
<el-button type="primary" icon="el-icon-search" @click="filterColumns"></el-button>
<el-button style="float: right; padding: 10px 0" type="text">
{{ formData.tableName }}
</el-button>
</div>
<el-checkbox-group class="check-group" v-model="formData.columns">
<el-checkbox
v-for="item in filteredColumns"
:key="item.column_name"
:label="item.column_name"
border>
{{ item.column_name }}{{ item.column_comment ? " ( " + item.column_comment + " )" : "" }}
<span class="option-extra">{{ item.column_type }}</span>
</div>
</el-checkbox>
</el-checkbox-group>
</div>
</el-checkbox>
</el-checkbox-group>
</el-card>
</el-carousel-item>
<div class="wizard-panel" v-if="active === 2">
<el-alert
title="这里填写 Excel 第一行表头名称,用来和数据库字段建立对应关系。"
type="info"
:closable="false"
style="margin-bottom: 16px">
</el-alert>
<el-table :data="relation" border class="relation-table">
<el-table-column prop="column_name" label="字段名" min-width="180"></el-table-column>
<el-table-column prop="column_comment" label="字段备注" min-width="220" show-overflow-tooltip></el-table-column>
<el-table-column prop="column_type" label="字段类型" width="160"></el-table-column>
<el-table-column prop="column_key" label="键" width="80"></el-table-column>
<el-table-column label="Excel 表头" min-width="260">
<template slot-scope="{ row }">
<el-input v-model.trim="row.relation" placeholder="请输入 Excel 表头名称"></el-input>
</template>
</el-table-column>
</el-table>
</div>
<el-carousel-item class="carousel-table-item">
<el-card style="width: 100%" class="carousel-table-item-relation">
<el-table :data="relation" height="100%" style="width: 100%" border size="medium">
<el-table-column prop="column_name" label="列名"></el-table-column>
<el-table-column prop="column_comment" label="描述" show-overflow-tooltip></el-table-column>
<el-table-column prop="is_nullable" label="可为空"></el-table-column>
<el-table-column prop="column_type" label="类型"></el-table-column>
<el-table-column prop="column_key" label="key"></el-table-column>
<el-table-column min-width="200px">
<template slot="header">
目标列
<el-popover
placement="top-start"
title="提示"
trigger="hover"
content="Excel 对应的表头名称,用于和表数据列对应">
<i slot="reference" class="el-icon-question"></i>
</el-popover>
</template>
<template slot-scope="{ row }">
<el-input
v-model.trim="row.relation"
size="medium"
placeholder="目标列">
</el-input>
</template>
</el-table-column>
</el-table>
</el-card>
</el-carousel-item>
<div class="wizard-panel" v-if="active === 3">
<div class="submit-panel">
<el-form label-width="120px">
<el-form-item label="导入方式">
<el-carousel-item class="carousel-table-item">
<el-card style="width: 100%" class="carousel-table-item-form">
<el-form label-width="120px" :model="formData" ref="form" :rules="formRules">
<el-form-item label="&emsp;" label-width="135px" class="view-header"></el-form-item>
<el-form-item prop="method" label="导入方式">
<el-radio-group v-model="formData.method">
<el-radio-button :label="1">追加或更新</el-radio-button>
<el-radio-button :label="2">仅追加</el-radio-button>
@@ -230,13 +273,8 @@ layout("/layouts/platform.html"){
</el-radio-group>
</el-form-item>
<el-form-item label="关键字段" v-if="[1, 3].includes(formData.method)">
<el-select
v-model="formData.field"
clearable
filterable
placeholder="请选择关键字段"
style="width: 360px">
<el-form-item v-if="[1, 3].includes(formData.method)" prop="field" label="关键字段">
<el-select v-model="formData.field" clearable filterable placeholder="请选择" style="width: 50%">
<el-option
v-for="item in relation"
:key="item.column_name"
@@ -246,7 +284,7 @@ layout("/layouts/platform.html"){
</el-select>
</el-form-item>
<el-form-item label="可选插件">
<el-form-item prop="plugins" label="可选插件">
<el-checkbox-group v-model="formData.plugins">
<el-checkbox
v-for="item in plugins"
@@ -258,7 +296,7 @@ layout("/layouts/platform.html"){
</el-checkbox-group>
</el-form-item>
<el-form-item label="Excel 文件" required>
<el-form-item label="文&emsp;&emsp;件" class="is-required">
<el-upload
ref="upload"
drag
@@ -270,30 +308,35 @@ layout("/layouts/platform.html"){
:on-remove="handleFileRemove">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div slot="tip" class="el-upload__tip">仅支持 `xls`、`xlsx` 文件</div>
<div slot="tip" class="el-upload__tip" style="color: #F56C6C">只能上传 xls/xlsx 文件</div>
</el-upload>
</el-form-item>
</el-form>
</div>
</div>
</el-card>
</el-carousel-item>
<div class="wizard-panel" v-if="active === 4">
<div class="result-panel">
<el-progress type="circle" :percentage="resultPercentage" status="success"></el-progress>
<div style="margin-top: 18px; color: #606266;">
共 {{ result.total || 0 }} 条,成功 {{ result.success || 0 }} 条
<el-carousel-item style="padding: 10px">
<el-card class="result-card">
<el-progress type="circle" :percentage="resultPercentage" :status="resultProgressStatus"></el-progress>
<div class="result-text">{{ resultSummary }}</div>
<div style="margin-top: 10px" v-if="result.cacheKey">
<el-link
type="danger"
icon="el-icon-download"
style="font-size: 13px;"
:href="loc() + '/exportErrors?cacheKey=' + result.cacheKey">
下载错误记录
</el-link>
</div>
<div v-if="result.cacheKey" style="margin-top: 10px;">
<el-link type="primary" :href="loc() + '/exportErrors?cacheKey=' + result.cacheKey">下载错误记录</el-link>
</div>
</div>
</div>
</div>
</el-card>
</el-carousel-item>
</el-carousel>
<div class="wizard-footer">
<el-button :disabled="active === 0 || loading" @click="prevStep">上一步</el-button>
<el-button v-if="active < 4" type="primary" :loading="loading" @click="nextStep">下一步</el-button>
<el-button v-else type="primary" @click="restart">重新开始</el-button>
<div class="card-body-center-bottom">
<el-button size="medium" @click="prev" :disabled="active === 0 || loading">上一步</el-button>
<el-button type="primary" size="medium" :loading="loading" @click="active < 4 ? next() : carry()">
{{ active < 4 ? primaryButtonText : "完成" }}
</el-button>
</div>
</div>
</div>
@@ -316,11 +359,16 @@ layout("/layouts/platform.html"){
relation: [],
plugins: [],
fileList: [],
cacheTableName: "",
result: {
total: 0,
success: 0,
cacheKey: ""
},
formRules: {
method: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
field: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
},
formData: {
tableName: "",
columns: [],
@@ -331,6 +379,29 @@ layout("/layouts/platform.html"){
}
},
computed: {
primaryButtonText() {
if (this.active === 3 && this.formData.method === 3) {
return "更新"
}
return "下一步"
},
resultSummary() {
const total = this.result.total || 0
const success = this.result.success || 0
const fail = Math.max(total - success, 0)
if (fail > 0) {
return "总记录数 " + total + " 条,成功 " + success + " 条,失败 " + fail + " 条!"
}
return "总记录数 " + total + " 条,成功 " + success + " 条!"
},
resultProgressStatus() {
const total = this.result.total || 0
const success = this.result.success || 0
if (total > 0 && success < total) {
return "exception"
}
return "success"
},
resultPercentage() {
if (!this.result.total) {
return 0
@@ -339,6 +410,7 @@ layout("/layouts/platform.html"){
}
},
methods: {
loc,
normalizeRecord(item) {
const normalized = {}
Object.keys(item || {}).forEach(key => {
@@ -398,66 +470,75 @@ layout("/layouts/platform.html"){
.filter(item => this.formData.columns.includes(item.column_name))
.map(item => Object.assign({}, item, { relation: "" }))
},
async nextStep() {
async next() {
if (this.active === 0) {
if (!this.formData.tableName) {
this.$message.warning("请选择数据表")
this.$notify({
title: "警告",
message: "请选择数据表",
type: "warning"
})
return
}
await this.loadColumns()
this.formData.columns = []
this.formData.field = ""
if (this.cacheTableName !== this.formData.tableName) {
await this.loadColumns()
this.formData.columns = []
this.formData.field = ""
this.cacheTableName = this.formData.tableName
}
} else if (this.active === 1) {
if (!this.formData.columns.length) {
this.$message.warning("请至少选择一个字段")
this.$notify({
title: "警告",
message: "请选择数据列",
type: "warning"
})
return
}
this.buildRelation()
} else if (this.active === 2) {
const hasEmptyRelation = this.relation.some(item => !item.relation)
if (hasEmptyRelation) {
this.$message.warning("请填写所有 Excel 表头对应关系")
if (this.relation.find(item => !item.relation)) {
this.$notify({
title: "警告",
message: "存在未填写的目标列",
type: "warning"
})
return
}
} else if (this.active === 3) {
const success = await this.submitImport()
const success = await this.doSubmit()
if (!success) {
return
}
}
this.active += 1
this.$refs.carousel.next()
this.active++
},
prevStep() {
if (this.active > 0) {
this.active -= 1
prev() {
if (this.active <= 0) {
return
}
this.$refs.carousel.prev()
this.active--
},
handleFileRemove(file, fileList) {
this.fileList = fileList
},
handleFileChange(file, fileList) {
const extension = ((file.name || "").split(".").pop() || "").toLowerCase()
const removeIndex = fileList.findIndex(item => item.uid === file.uid)
if (!["xls", "xlsx"].includes(extension)) {
this.$message.warning("仅支持 xls、xlsx 文件")
if (removeIndex > -1) {
fileList.splice(removeIndex, 1)
}
} else if (file.size === 0) {
this.$message.warning("上传文件不能为空")
if (removeIndex > -1) {
fileList.splice(removeIndex, 1)
}
}
this.fileList = fileList
},
async submitImport() {
if ([1, 3].includes(this.formData.method) && !this.formData.field) {
this.$message.warning("请选择关键字段")
validateSubmitForm() {
let validForm = true
this.$refs.form.validate(valid => {
validForm = valid
})
if (!validForm || !this.fileList.length) {
this.$notify({
title: "警告",
message: "存在未填写的必填项",
type: "warning"
})
return false
}
if (!this.fileList.length) {
this.$message.warning("请上传 Excel 文件")
return true
},
async doSubmit() {
if (!this.validateSubmitForm()) {
return false
}
@@ -476,18 +557,45 @@ layout("/layouts/platform.html"){
"Content-Type": "multipart/form-data"
}
})
if (resp.code !== 0) {
this.$message.error(resp.msg)
this.$notify.error({
title: "错误",
message: resp.msg
})
this.result = { total: 0, success: 0, cacheKey: "" }
return false
}
this.result = resp.data || { total: 0, success: 0, cacheKey: "" }
this.$message.success(resp.msg)
return true
} finally {
this.loading = false
}
},
restart() {
handleFileRemove(file, fileList) {
this.fileList = fileList
},
handleFileChange(file, fileList) {
const extension = ((file.name || "").split(".").pop() || "").toLowerCase()
const remove = () => {
const removeIndex = fileList.findIndex(item => item.uid === file.uid)
if (removeIndex > -1) {
fileList.splice(removeIndex, 1)
}
}
if (file.size === 0) {
this.$message.warning("您选择的是空文件")
remove()
} else if (!["xls", "xlsx"].includes(extension)) {
this.$message.warning("上传文件只能是 xls/xlsx 格式")
remove()
}
this.fileList = fileList
},
carry() {
this.active = 0
this.tableKeyword = ""
this.columnKeyword = ""
@@ -495,6 +603,7 @@ layout("/layouts/platform.html"){
this.filteredColumns = []
this.relation = []
this.fileList = []
this.cacheTableName = ""
this.result = {
total: 0,
success: 0,
@@ -511,6 +620,7 @@ layout("/layouts/platform.html"){
this.$refs.upload.clearFiles()
}
this.filterTables()
this.$refs.carousel.setActiveItem(0)
}
},
async created() {
@@ -52,7 +52,7 @@
prefix-icon="el-icon-lock"
></el-input>
</el-form-item>
<el-form-item prop="platformCaptcha">
<el-form-item v-if="!captchaDisabled" prop="platformCaptcha">
<el-input
v-model="loginForm.platformCaptcha"
auto-complete="off"
@@ -124,11 +124,14 @@
}
</script>
<script nonce="${cspNonce!}">
const DISABLE_LOGIN_CAPTCHA = ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname)
new Vue({
el: "#app",
data() {
return {
codeUrl: "",
captchaDisabled: DISABLE_LOGIN_CAPTCHA,
loginForm: {
username: "",
password: "",
@@ -138,17 +141,21 @@
loginRules: {
username: [{ required: true, trigger: ["blur", "change"], message: "账号不能为空" }],
password: [{ required: true, trigger: ["blur", "change"], message: "密码不能为空" }],
platformCaptcha: [{ required: true, trigger: ["blur", "change"], message: "验证码不能为空" }]
platformCaptcha: []
},
loading: false,
rasParams: {
publicKey: '',
keyId: '',
expireAt: 0,
},
}
},
methods: {
getCode() {
if (this.captchaDisabled) {
return
}
axios.post("/platform/login/captcha").then((resp) => {
if (resp.data.code === 0) {
this.codeUrl = resp.data.data.codeUrl
@@ -159,7 +166,15 @@
handleLogin() {
this.$refs.loginForm.validate(async (valid) => {
if (valid) {
await this.ensurePublicKey()
const { publicKey, keyId } = this.rasParams
if (!publicKey || !keyId) {
this.$message({
message: "Login key unavailable, please refresh and try again",
type: "error"
})
return
}
// 加密密码
const encrypt = new JSEncrypt()
encrypt.setPublicKey(publicKey)
@@ -200,10 +215,17 @@
},
// 获取公钥
fetchPublicKey() {
axios.get("/platform/login/publicKey").then(res => {
return axios.get("/platform/login/publicKey").then(res => {
this.rasParams = res.data.data
})
},
ensurePublicKey() {
const refreshBeforeMillis = 30 * 1000
if (this.rasParams.publicKey && this.rasParams.keyId && Date.now() < this.rasParams.expireAt - refreshBeforeMillis) {
return Promise.resolve()
}
return this.fetchPublicKey()
},
},
created() {
this.fetchPublicKey()
@@ -161,6 +161,12 @@
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="是否推送待办事项">
<el-checkbox v-model="pushTodoFlag">是</el-checkbox>
</el-form-item>
</el-col>
<!-- <el-col :span="8">
<el-form-item prop="funding" label="经费预算">
<el-input-number style="width: 100%" v-model="formData.funding"
@@ -586,6 +592,7 @@
data() {
return {
formData: { applyWay: [] },
pushTodoFlag: false,
unionUserNumOneKeyRatio: null,
summaryCount: 0,
unionUserNumCalc: [{ startNum: 1, endNum: 1, resultNum: 1 }],
@@ -941,6 +948,7 @@
async openAdd() {
this.copyTemplateMode = false
this.copyTemplateName = ""
this.pushTodoFlag = false
this.events = await this.getEvents(2)
await this.getActivityGroup()
this.active = 0
@@ -1019,6 +1027,7 @@
},
async openEdit(row, flag, copyTemplateMode = false) {
this.copyTemplateMode = !!copyTemplateMode
this.pushTodoFlag = false
this.active = flag ? 0 : this.active
await this.getActivityGroup()
const { id } = row
@@ -73,6 +73,7 @@ layout("/layouts/platform.html"){
<el-card class="mt10" shadow="never">
<table-tool label="分工会列表">
<template v-if="$auth.hasRoleOr('SYSADMIN, SCHOOL_UNION_ADMIN')">
<el-button @click="pushUnregisteredTodo" size="small" type="primary">未报名人员推送待办</el-button>
<el-button @click="openImport" size="small" type="primary">导入报名人员</el-button>
<el-button @click="doExportByEnroll" size="small" type="primary">导出报名信息</el-button>
</template>
@@ -371,10 +372,6 @@ layout("/layouts/platform.html"){
this.notifyWarning("请先选择活动名称")
return
}
if (!this.pageForm.unionid) {
this.notifyWarning("请选择所属工会")
return
}
this.$downLoad(loc() + "/doExportByEnroll", {
id: this.pageForm.id,
unionId: this.pageForm.unionid
@@ -392,6 +389,9 @@ layout("/layouts/platform.html"){
}
})
},
pushUnregisteredTodo() {
this.$message.info("未报名人员推送待办功能开发中")
},
successImport() {
this.pageData()
},
@@ -2,57 +2,47 @@ const BASIC_TABLE_COMPONENT = {
template: `
<div>
<el-row type="flex">
<el-col></el-col>
</el-row>
<el-col></el-col>
</el-row>
<table-tool>
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">新建机构</el-button>
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">新建机构</el-button>
</table-tool>
<el-table key="1" :data="tableData" ref="tableRef">
<el-table-column label="序号" width="100px" type="index" :index="indexMethod"></el-table-column>
<el-table-column prop="name" label="机构名称"></el-table-column>
<el-table-column prop="code" label="机构代码"></el-table-column>
<el-table-column prop="introduce" label="描述" min-width="220px">
<el-table-column prop="introduce" label="描述"></el-table-column>
<el-table-column label="操作" width="100px">
<template slot-scope="{row}">
<div class="institution-description">{{row.introduce}}</div>
</template>
</el-table-column>
<el-table-column label="操作" width="140px">
<template slot-scope="scope">
<template>
<el-link size="mini" type="primary" @click="openEdit(scope.row)">修改</el-link>
</template>
<el-divider direction="vertical"></el-divider>
<el-tooltip v-if="isBasicInstitution(scope.$index)"
content="你选择的组织机构是两代会基本机构,不允许删除。"
placement="top">
<span class="disabled-delete-link">删除</span>
</el-tooltip>
<el-link v-else size="mini" type="danger" @click="del(scope.row.id)">删除</el-link>
<el-link size="mini" type="danger" @click="del(row.id)">删除</el-link>
</template>
</el-table-column>
</el-table>
<el-dialog :visible.sync="dialogFormVisible" :title="isEdit ? '编辑' : '新增'" width="40%">
<el-form :model="formData" ref="formRef" :rules="rules" label-width="100px">
<el-form-item label="上级机构名称">
<el-input :value="parentName || '-'" disabled></el-input>
</el-form-item>
<el-form-item label="机构名称" prop="name">
<el-input placeholder="请输入机构名称" v-model="formData.name"></el-input>
</el-form-item>
<el-form-item label="机构代码" prop="code">
<el-input placeholder="请输入机构代码" v-model="formData.code" :disabled="isEdit"></el-input>
</el-form-item>
<el-form-item label="排序编码" prop="location">
<el-input-number v-model="formData.location" :min="0" :step="1" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item label="描述" prop="introduce">
<el-input v-model="formData.introduce" placeholder="请输入描述"></el-input>
</el-form-item>
</el-form>
<el-dialog :visible.sync="dialogFormVisible" :title="formData.id?'编辑':'新增'" width="40%">
<el-form :model="formData" ref="formRef" :rules="rules" label-width="100px">
<el-form-item label="机构名称" prop="ids">
<el-cascader
v-model="ids"
:options="treeList"
:props="props"
@change="parentChange"
placeholder="请选择机构"
style="width: 100%"
></el-cascader>
</el-form-item>
<el-form-item label="机构代码" prop="code">
<el-input placeholder="请输入机构代码" v-model="formData.code" disabled></el-input>
</el-form-item>
<el-form-item label="描述" prop="introduce">
<el-input v-model="formData.introduce" placeholder="请输入描述"></el-input>
</el-form-item>
<el-form-item label="备注" prop="notes">
<el-input v-model="formData.notes" placeholder="请输入备注"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="doSubmit">确定</el-button>
<el-button @click="dialogFormVisible = false"> </el-button>
<el-button type="primary" @click="doSubmit"> </el-button>
</div>
</el-dialog>
</div>
@@ -66,10 +56,6 @@ const BASIC_TABLE_COMPONENT = {
parentId: {
type: String,
required: true
},
parentName: {
type: String,
default: ""
}
},
data() {
@@ -77,13 +63,20 @@ const BASIC_TABLE_COMPONENT = {
rules: {
name: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
code: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
parentId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
location: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
parentId: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
},
parentData: {},
parentIds: [],
dialogFormVisible: false,
isEdit: false
ids: [],
treeList: [],
treeFlat: [],
props: {
checkStrictly: true,
multiple: false,
label: "name",
value: "id"
},
dialogFormVisible: false
}
},
methods: {
@@ -102,44 +95,38 @@ const BASIC_TABLE_COMPONENT = {
})
},
openAdd() {
this.isEdit = false
this.dialogFormVisible = true
this.formData = {
parentId: this.parentId,
location: 0
}
this.formData = {}
this.ids = []
$.post("/platform/teacherCongress/institution/formTree", { sessionId: this.sessionId }).then((res) => {
if (res.code === 0) {
this.treeList = res.data.treeList
this.treeFlat = res.data.treeFlat
}
})
},
openEdit(row) {
this.isEdit = true
this.dialogFormVisible = true
this.formData = {
...row,
location: row.location || 0
}
},
isBasicInstitution(index) {
const pageNumber = this.pageForm.pageNumber || 1
const pageSize = this.pageForm.pageSize || 10
return (pageNumber - 1) * pageSize + index < 6
// 上级机构选择变化
parentChange(val) {
const id = val[val.length - 1]
const tree = this.treeFlat.find((tree) => tree.id === id)
this.$set(this.formData, "code", tree?.code)
this.$set(this.formData, "name", tree?.name)
},
doSubmit() {
this.$refs.formRef.validate((valid) => {
if (!valid) {
return
}
this.formData.id = this.ids[this.ids.length - 1]
this.formData.parentId = this.ids[this.ids.length - 2] || this.parentId
this.formData.sessionId = this.sessionId
if (!this.isEdit) {
this.formData.parentId = this.parentId
if (valid) {
this.$axios.post(loc() + "/insert", this.formData).then((res) => {
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.doSearch()
this.$emit("refresh", null)
}
})
}
const url = this.isEdit ? loc() + "/update" : loc() + "/insert"
this.$axios.post(url, this.formData).then((res) => {
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.doSearch()
this.$emit("refresh", null)
}
})
})
},
@@ -158,18 +145,5 @@ const BASIC_TABLE_COMPONENT = {
})
})
}
},
style: /*language=CSS*/ `
.institution-description {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
.disabled-delete-link {
color: #c0c4cc;
cursor: not-allowed;
font-size: 12px;
}
`
}
}
@@ -9,27 +9,21 @@ layout("/layouts/platform.html"){
</el-col>
<el-col :span="19">
<el-card shadow="never" style="height: 100%">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane v-if="hasChildInstitution" label="机构列表" name="institution">
<basic-table
:session-id="sessionId"
:parent-id="currentTreeData && currentTreeData.id"
:parent-name="currentTreeData && currentTreeData.name"
v-if="activeTab === 'institution' && sessionId && currentTreeData"
ref="basicTableRef"
@refresh="refreshTree"
></basic-table>
</el-tab-pane>
<el-tab-pane label="人员列表" name="user">
<user-table
:session-id="sessionId"
:institution-id="currentTreeData && currentTreeData.id"
:institution-code="currentTreeData && currentTreeData.code"
ref="userTableRef"
v-if="activeTab === 'user' && sessionId && currentTreeData"
></user-table>
</el-tab-pane>
</el-tabs>
<template>
<basic-table
:session-id="sessionId"
:parent-id="currentTreeData.id"
v-if="showInstitutionTable || (currentTreeData && currentTreeData.id==='0')"
ref="basicTableRef"
@refresh="$refs.treeRef.listTree()"
></basic-table>
</template>
<user-table
:session-id="sessionId"
:institution-id="currentTreeData.id"
ref="userTableRef"
v-if="!showInstitutionTable && sessionId"
></user-table>
</el-card>
</el-col>
</el-row>
@@ -49,16 +43,16 @@ layout("/layouts/platform.html"){
"basic-table": BASIC_TABLE_COMPONENT
},
computed: {
hasChildInstitution() {
return this.currentTreeData && this.currentTreeData.children && this.currentTreeData.children.length > 0
showInstitutionTable() {
const hasChildren = this.currentTreeData?.children?.length > 0
return hasChildren
}
},
data() {
return {
currentTreeNode: null,
currentTreeData: null,
sessionId: null,
activeTab: "institution"
sessionId: null
}
},
methods: {
@@ -66,28 +60,14 @@ layout("/layouts/platform.html"){
this.currentTreeData = data
this.currentTreeNode = node
this.sessionId = sessionId
if (this.hasChildInstitution) {
this.activeTab = "institution"
} else if (this.activeTab === "institution") {
this.activeTab = "user"
}
this.refreshActiveTab()
},
handleTabClick() {
this.refreshActiveTab()
},
refreshActiveTab() {
this.$nextTick(() => {
if (this.activeTab === "institution") {
if (this.showInstitutionTable) {
this.$refs.basicTableRef && this.$refs.basicTableRef.doSearch()
} else {
this.$refs.userTableRef && this.$refs.userTableRef.doSearch()
}
})
},
refreshTree() {
this.$refs.treeRef.listTree(this.currentTreeData && this.currentTreeData.id)
},
openEdit(row) {}
},
created() {}
@@ -46,30 +46,11 @@ const TREE_COMPONENT = {
this.$emit("node-click", data, node, this.sessionId)
},
filterNode() {},
findTreeNode(list, id) {
if (!id || !list) {
return null
}
for (const item of list) {
if (item.id === id) {
return item
}
const child = this.findTreeNode(item.children, id)
if (child) {
return child
}
}
return null
},
listTree(selectedId) {
listTree() {
this.$axios.post("/platform/teacherCongress/institution/leftTree", { sessionId: this.sessionId }).then((res) => {
if (res.code === 0) {
this.treeData = res.data
if (this.treeData && this.treeData.length > 0) {
this.treeData[0].name = "两代会组织机构"
}
const selectedNode = this.findTreeNode(this.treeData, selectedId) || this.treeData[0]
this.$emit("node-click", selectedNode, null, this.sessionId)
this.$emit("node-click", this.treeData[0], null, this.sessionId)
}
})
},
@@ -3,73 +3,58 @@ const USER_TABLE_COMPONENT = {
<div>
<el-card shadow="never" style="height: 100%">
<el-row type="flex" :gutter="20">
<el-col :span="4">
<el-input v-model="pageForm.searchKeyword"
clearable
placeholder="请输入姓名或者工号"
@keyup.enter.native="doSearch">
</el-input>
</el-col>
<el-col :span="4">
<div class="search-query">
<el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
</div>
</el-col>
<el-col :span="4">
<el-input v-model="pageForm.searchKeyword" clearable placeholder="请输入姓名或者工号">" @keyup.enter.native="doSearch">
</el-input>
</el-col>
<el-col :span="4">
<div class="search-query">
<el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
</div>
</el-col>
</el-row>
<el-divider class="mb10 mt10"></el-divider>
<table-tool>
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">设置人员</el-button>
</table-tool>
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">设置人员</el-button>
</table-tool>
<el-table key="1" :data="tableData" ref="tableRef">
<el-table-column label="序号" width="100px" type="index" :index="indexMethod"></el-table-column>
<el-table-column prop="username" label="姓名"></el-table-column>
<el-table-column prop="loginname" label="工号"></el-table-column>
<el-table-column prop="sex" label="性别"></el-table-column>
<el-table-column prop="mobile" label="手机号码"></el-table-column>
<el-table-column prop="unitName" label="单位" show-overflow-tooltip></el-table-column>
<el-table-column prop="identity" label="身份" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="100px">
<template slot-scope="{row}">
<el-link size="mini" type="danger" @click="del(row.id)">删除</el-link>
</template>
<el-table-column label="序号" width="100px" type="index" :index="indexMethod"></el-table-column>
<el-table-column prop="username" label="姓名"></el-table-column>
<el-table-column prop="loginname" label="工号"></el-table-column>
<el-table-column prop="sex" label="性别"></el-table-column>
<el-table-column prop="mobile" label="手机号码"></el-table-column>
<el-table-column prop="unitName" label="单位" show-overflow-tooltip></el-table-column>
<el-table-column prop="identity" label="身份" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="100px">
<template slot-scope="{row}">
<el-link size="mini" type="danger" @click="del(row.id)">删除</el-link>
</template>
</el-table-column>
</el-table>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="设置人员" width="50%" :visible.sync="dialogVisible">
<el-form :model="formData" ref="formRef" label-width="120px" :rules="formRules">
<el-form :model="formData" ref="formRef" label-width="120px" :rules="formRules">
<el-form-item label="届次" prop="sessionId">
<el-select v-model="formData.sessionId" disabled>
<el-option v-for="i in sessionOptions" :label="i.fullName" :value="i.id" :key="i.id"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="needRole" label="角色" prop="roleCode">
<el-select v-model="formData.roleCode" placeholder="请选择角色" style="width: 100%">
<el-option v-for="item in roleOptions"
:key="item.code"
:label="item.name"
:value="item.code">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="工号或者姓名" prop="userId">
<el-form-item label="工号或姓名" prop="userId">
<user-select
v-model="formData.userId"
style="width: 100%"
api_input_key_name="query"
:option_label_func="(item)=>{return item.username + item.loginname}"
v-model="formData.userId"
style="width: 100%"
api_input_key_name="query"
:option_label_func="(item)=>{return item.username + item.loginname}"
></user-select>
</el-form-item>
<el-form-item label="身份" prop="identity">
<dict-select v-model="formData.identity" code="TEACHER_CONGRESS_INSTITUTION_USER_ROLE"></dict-select>
</el-form-item>
</el-form>
<div style="color: #e6a23c; line-height: 22px; margin: 0 0 12px 120px;">
注意如果需要跟角色绑定请在数据字典双代会组织机构中添加对应的角色标识
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="doSubmit">确定</el-button>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="doSubmit"> </el-button>
</div>
</el-dialog>
</div>
@@ -83,10 +68,6 @@ const USER_TABLE_COMPONENT = {
institutionId: {
required: true,
type: String
},
institutionCode: {
type: String,
default: ""
}
},
data() {
@@ -94,17 +75,10 @@ const USER_TABLE_COMPONENT = {
formRules: {
sessionId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
userId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
identity: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
roleCode: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
identity: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
},
dialogVisible: false,
sessionOptions: [],
roleOptions: []
}
},
computed: {
needRole() {
return ["ZWH001", "ZXWYH", "DBZGSCXZ"].includes(this.institutionCode)
sessionOptions: []
}
},
methods: {
@@ -162,18 +136,6 @@ const USER_TABLE_COMPONENT = {
})
},
listRoleOptions() {
if (!this.needRole) {
this.roleOptions = []
return
}
this.$axios.post("/platform/teacherCongress/institution/specialCommitteeRoleOptions").then((res) => {
if (res.code === 0) {
this.roleOptions = res.data || []
}
})
},
openAdd() {
this.dialogVisible = true
this.formData = {
@@ -181,7 +143,6 @@ const USER_TABLE_COMPONENT = {
institutionId: this.institutionId
}
this.listSession()
this.listRoleOptions()
}
}
}