浏览代码

用户页面优化

jingbo 4 月之前
父节点
当前提交
082560f251
共有 32 个文件被更改,包括 3082 次插入 和 3176 次删除
  1. 7 0
      src/main/java/com/jeesite/modules/mes/dao/MesAdw300DataDao.java
  2. 15 0
      src/main/java/com/jeesite/modules/mes/dao/MesUserEmailVerifyConfigDao.java
  3. 17 0
      src/main/java/com/jeesite/modules/mes/entity/MesEmployee.java
  4. 10 1
      src/main/java/com/jeesite/modules/mes/entity/MesTemplateItems.java
  5. 51 0
      src/main/java/com/jeesite/modules/mes/entity/MesUserEmailVerifyConfig.java
  6. 194 0
      src/main/java/com/jeesite/modules/mes/entity/excel/ExcelEmpUserEmployee.java
  7. 21 1
      src/main/java/com/jeesite/modules/mes/mqtt/Adw300PayloadParser.java
  8. 158 20
      src/main/java/com/jeesite/modules/mes/service/MesAdw300DataService.java
  9. 32 0
      src/main/java/com/jeesite/modules/mes/service/MesEmailLoginCodeService.java
  10. 65 0
      src/main/java/com/jeesite/modules/mes/service/MesEmployeeService.java
  11. 36 3
      src/main/java/com/jeesite/modules/mes/service/MesTemplateItemsService.java
  12. 46 0
      src/main/java/com/jeesite/modules/mes/service/MesUserEmailVerifyConfigService.java
  13. 215 3
      src/main/java/com/jeesite/modules/mes/web/EmpUserCustomController.java
  14. 1 0
      src/main/java/com/jeesite/modules/mes/web/MesDeviceController.java
  15. 149 5
      src/main/java/com/jeesite/modules/mes/web/MesEmailLoginController.java
  16. 47 0
      src/main/java/com/jeesite/modules/mes/web/MesEmployeeController.java
  17. 123 1
      src/main/java/com/jeesite/modules/mes/web/MesProductController.java
  18. 60 0
      src/main/java/com/jeesite/modules/mes/web/MesUserEmailVerifyConfigController.java
  19. 3 0
      src/main/resources/config/application.yml
  20. 145 20
      src/main/resources/mappings/modules/mes/MesAdw300DataDao.xml
  21. 19 0
      src/main/resources/mappings/modules/mes/MesUserEmailVerifyConfigDao.xml
  22. 5 59
      src/main/resources/static/modules/sys/sysLogin.js
  23. 二进制
      src/main/resources/static/screen/imgs/screen4FactoryBg.png
  24. 642 1233
      src/main/resources/views/modules/mes/equipment/mesScreen5.html
  25. 62 20
      src/main/resources/views/modules/mes/mesDeviceOeeScreen.html
  26. 58 0
      src/main/resources/views/modules/mes/mesEmployeeList.html
  27. 19 6
      src/main/resources/views/modules/mes/mesEnergyScreen.html
  28. 769 1777
      src/main/resources/views/modules/mes/mesScreen4.html
  29. 2 1
      src/main/resources/views/modules/mes/mesTemplateItemsList.html
  30. 29 3
      src/main/resources/views/modules/sys/user/empUserList.html
  31. 80 0
      src/main/resources/views/themes/default/modules/sys/sysEmailVerify.html
  32. 2 23
      src/main/resources/views/themes/default/modules/sys/sysLogin.html

+ 7 - 0
src/main/java/com/jeesite/modules/mes/dao/MesAdw300DataDao.java

@@ -24,4 +24,11 @@ public interface MesAdw300DataDao extends CrudDao<MesAdw300Data> {
 	List<Map<String, Object>> findTodayEnergy(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
 
 	List<Map<String, Object>> findHourlyEnergy(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
+
+	List<Map<String, Object>> findTodayEnergyByPower(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
+
+	List<Map<String, Object>> findHourlyEnergyByPower(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
+
+	Long countByDeviceAndReportTimeRange(@Param("deviceSn") String deviceSn,
+			@Param("startTime") Date startTime, @Param("endTime") Date endTime);
 }

+ 15 - 0
src/main/java/com/jeesite/modules/mes/dao/MesUserEmailVerifyConfigDao.java

@@ -0,0 +1,15 @@
+package com.jeesite.modules.mes.dao;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.mes.entity.MesUserEmailVerifyConfig;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 用户邮箱二次验证配置 DAO。
+ */
+@MyBatisDao
+public interface MesUserEmailVerifyConfigDao extends CrudDao<MesUserEmailVerifyConfig> {
+
+	MesUserEmailVerifyConfig getByLoginCode(@Param("loginCode") String loginCode);
+}

+ 17 - 0
src/main/java/com/jeesite/modules/mes/entity/MesEmployee.java

@@ -5,6 +5,9 @@ import com.jeesite.common.mybatis.annotation.Column;
 import com.jeesite.common.mybatis.annotation.JoinTable;
 import com.jeesite.common.mybatis.annotation.Table;
 import com.jeesite.common.mybatis.mapper.query.QueryType;
+import com.jeesite.common.utils.excel.annotation.ExcelField;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
+import com.jeesite.common.utils.excel.annotation.ExcelFields;
 import com.jeesite.modules.sys.entity.User;
 
 import javax.validation.constraints.NotBlank;
@@ -57,6 +60,20 @@ public class MesEmployee extends DataEntity<MesEmployee> {
 	private String loginCode;
 	private String userStatus;
 
+	@ExcelFields({
+		@ExcelField(title="\u5458\u5de5\u7f16\u7801", attrName="empCode", align=Align.CENTER, sort=10),
+		@ExcelField(title="\u5458\u5de5\u59d3\u540d", attrName="empName", align=Align.CENTER, sort=20),
+		@ExcelField(title="\u5458\u5de5\u82f1\u6587\u540d", attrName="empNameEn", align=Align.CENTER, sort=30),
+		@ExcelField(title="\u5458\u5de5\u5de5\u53f7", attrName="empNo", align=Align.CENTER, sort=40),
+		@ExcelField(title="\u673a\u6784\u7f16\u7801", attrName="officeCode", align=Align.CENTER, sort=50),
+		@ExcelField(title="\u673a\u6784\u540d\u79f0", attrName="officeName", align=Align.CENTER, sort=60),
+		@ExcelField(title="\u516c\u53f8\u7f16\u7801", attrName="companyCode", align=Align.CENTER, sort=70),
+		@ExcelField(title="\u516c\u53f8\u540d\u79f0", attrName="companyName", align=Align.CENTER, sort=80),
+		@ExcelField(title="\u5c97\u4f4d\u7f16\u7801", attrName="postCode", align=Align.CENTER, sort=90),
+		@ExcelField(title="\u5c97\u4f4d\u540d\u79f0", attrName="postName", align=Align.CENTER, sort=100),
+		@ExcelField(title="\u72b6\u6001", attrName="status", dictType="sys_search_status", align=Align.CENTER, sort=110),
+		@ExcelField(title="\u5907\u6ce8", attrName="remarks", align=Align.LEFT, sort=120),
+	})
 	public MesEmployee() {
 		this(null);
 	}

+ 10 - 1
src/main/java/com/jeesite/modules/mes/entity/MesTemplateItems.java

@@ -38,6 +38,7 @@ public class MesTemplateItems extends DataEntity<MesTemplateItems> {
 	private static final long serialVersionUID = 1L;
 	private String templateId;		// 模板
 	private String oprno;		// 工位号
+	private String oprnoName;		// 工位名称
 	private String craft;		// 工艺号
 	private String cate;		// 报文类型
 	private String title;		// 报文名称
@@ -73,6 +74,14 @@ public class MesTemplateItems extends DataEntity<MesTemplateItems> {
 	public void setOprno(String oprno) {
 		this.oprno = oprno;
 	}
+
+	public String getOprnoName() {
+		return oprnoName;
+	}
+
+	public void setOprnoName(String oprnoName) {
+		this.oprnoName = oprnoName;
+	}
 	
 	@NotBlank(message="工艺号不能为空")
 	@Size(min=0, max=50, message="工艺号长度不能超过 50 个字符")
@@ -136,4 +145,4 @@ public class MesTemplateItems extends DataEntity<MesTemplateItems> {
 	public void setRank(Integer rank) {
 		this.rank = rank;
 	}
-}
+}

+ 51 - 0
src/main/java/com/jeesite/modules/mes/entity/MesUserEmailVerifyConfig.java

@@ -0,0 +1,51 @@
+package com.jeesite.modules.mes.entity;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.Table;
+import com.jeesite.common.mybatis.mapper.query.QueryType;
+
+/**
+ * 用户邮箱二次验证配置。
+ */
+@Table(name = "mes_user_email_verify_config", alias = "a", label = "用户邮箱二次验证配置", columns = {
+		@Column(name = "id", attrName = "id", label = "主键", isPK = true),
+		@Column(name = "login_code", attrName = "loginCode", label = "登录账号", queryType = QueryType.EQ),
+		@Column(name = "enabled", attrName = "enabled", label = "是否开启"),
+		@Column(name = "create_by", attrName = "createBy", label = "创建者", isUpdate = false, isQuery = false),
+		@Column(name = "create_date", attrName = "createDate", label = "创建时间", isUpdate = false, isQuery = false),
+		@Column(name = "update_by", attrName = "updateBy", label = "更新者", isQuery = false),
+		@Column(name = "update_date", attrName = "updateDate", label = "更新时间", isQuery = false),
+		@Column(name = "remarks", attrName = "remarks", label = "备注", queryType = QueryType.LIKE),
+}, orderBy = "a.update_date DESC")
+public class MesUserEmailVerifyConfig extends DataEntity<MesUserEmailVerifyConfig> {
+
+	private static final long serialVersionUID = 1L;
+
+	private String loginCode;
+	private String enabled;
+
+	public MesUserEmailVerifyConfig() {
+		this(null);
+	}
+
+	public MesUserEmailVerifyConfig(String id) {
+		super(id);
+	}
+
+	public String getLoginCode() {
+		return loginCode;
+	}
+
+	public void setLoginCode(String loginCode) {
+		this.loginCode = loginCode;
+	}
+
+	public String getEnabled() {
+		return enabled;
+	}
+
+	public void setEnabled(String enabled) {
+		this.enabled = enabled;
+	}
+}

+ 194 - 0
src/main/java/com/jeesite/modules/mes/entity/excel/ExcelEmpUserEmployee.java

@@ -0,0 +1,194 @@
+package com.jeesite.modules.mes.entity.excel;
+
+import com.jeesite.common.utils.excel.annotation.ExcelField;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
+import com.jeesite.common.utils.excel.annotation.ExcelFields;
+
+public class ExcelEmpUserEmployee {
+
+	private String loginCode;
+	private String userName;
+	private String email;
+	private String mobile;
+	private String phone;
+	private String status;
+	private String userRoleString;
+	private String empCode;
+	private String empNo;
+	private String empName;
+	private String empNameEn;
+	private String officeCode;
+	private String officeName;
+	private String companyCode;
+	private String companyName;
+	private String postCode;
+	private String postName;
+	private String remarks;
+
+	@ExcelFields({
+		@ExcelField(title="\u767b\u5f55\u8d26\u53f7", attrName="loginCode", align=Align.CENTER, sort=10),
+		@ExcelField(title="\u7528\u6237\u6635\u79f0", attrName="userName", align=Align.CENTER, sort=20),
+		@ExcelField(title="\u7535\u5b50\u90ae\u7bb1", attrName="email", align=Align.CENTER, sort=30),
+		@ExcelField(title="\u624b\u673a\u53f7\u7801", attrName="mobile", align=Align.CENTER, sort=40),
+		@ExcelField(title="\u529e\u516c\u7535\u8bdd", attrName="phone", align=Align.CENTER, sort=50),
+		@ExcelField(title="\u7528\u6237\u72b6\u6001", attrName="status", dictType="sys_user_status", align=Align.CENTER, sort=60),
+		@ExcelField(title="\u89d2\u8272\u7f16\u7801", attrName="userRoleString", align=Align.LEFT, sort=70, type=ExcelField.Type.IMPORT),
+		@ExcelField(title="\u5458\u5de5\u7f16\u7801", attrName="empCode", align=Align.CENTER, sort=80),
+		@ExcelField(title="\u5458\u5de5\u5de5\u53f7", attrName="empNo", align=Align.CENTER, sort=90),
+		@ExcelField(title="\u5458\u5de5\u59d3\u540d", attrName="empName", align=Align.CENTER, sort=100),
+		@ExcelField(title="\u5458\u5de5\u82f1\u6587\u540d", attrName="empNameEn", align=Align.CENTER, sort=110),
+		@ExcelField(title="\u673a\u6784\u7f16\u7801", attrName="officeCode", align=Align.CENTER, sort=120),
+		@ExcelField(title="\u673a\u6784\u540d\u79f0", attrName="officeName", align=Align.CENTER, sort=130),
+		@ExcelField(title="\u516c\u53f8\u7f16\u7801", attrName="companyCode", align=Align.CENTER, sort=140),
+		@ExcelField(title="\u516c\u53f8\u540d\u79f0", attrName="companyName", align=Align.CENTER, sort=150),
+		@ExcelField(title="\u5c97\u4f4d\u7f16\u7801", attrName="postCode", align=Align.CENTER, sort=160),
+		@ExcelField(title="\u5c97\u4f4d\u540d\u79f0", attrName="postName", align=Align.CENTER, sort=170),
+		@ExcelField(title="\u5458\u5de5\u5907\u6ce8", attrName="remarks", align=Align.LEFT, sort=180)
+	})
+	public ExcelEmpUserEmployee() {
+	}
+
+	public String getLoginCode() {
+		return loginCode;
+	}
+
+	public void setLoginCode(String loginCode) {
+		this.loginCode = loginCode;
+	}
+
+	public String getUserName() {
+		return userName;
+	}
+
+	public void setUserName(String userName) {
+		this.userName = userName;
+	}
+
+	public String getEmail() {
+		return email;
+	}
+
+	public void setEmail(String email) {
+		this.email = email;
+	}
+
+	public String getMobile() {
+		return mobile;
+	}
+
+	public void setMobile(String mobile) {
+		this.mobile = mobile;
+	}
+
+	public String getPhone() {
+		return phone;
+	}
+
+	public void setPhone(String phone) {
+		this.phone = phone;
+	}
+
+	public String getStatus() {
+		return status;
+	}
+
+	public void setStatus(String status) {
+		this.status = status;
+	}
+
+	public String getUserRoleString() {
+		return userRoleString;
+	}
+
+	public void setUserRoleString(String userRoleString) {
+		this.userRoleString = userRoleString;
+	}
+
+	public String getEmpCode() {
+		return empCode;
+	}
+
+	public void setEmpCode(String empCode) {
+		this.empCode = empCode;
+	}
+
+	public String getEmpNo() {
+		return empNo;
+	}
+
+	public void setEmpNo(String empNo) {
+		this.empNo = empNo;
+	}
+
+	public String getEmpName() {
+		return empName;
+	}
+
+	public void setEmpName(String empName) {
+		this.empName = empName;
+	}
+
+	public String getEmpNameEn() {
+		return empNameEn;
+	}
+
+	public void setEmpNameEn(String empNameEn) {
+		this.empNameEn = empNameEn;
+	}
+
+	public String getOfficeCode() {
+		return officeCode;
+	}
+
+	public void setOfficeCode(String officeCode) {
+		this.officeCode = officeCode;
+	}
+
+	public String getOfficeName() {
+		return officeName;
+	}
+
+	public void setOfficeName(String officeName) {
+		this.officeName = officeName;
+	}
+
+	public String getCompanyCode() {
+		return companyCode;
+	}
+
+	public void setCompanyCode(String companyCode) {
+		this.companyCode = companyCode;
+	}
+
+	public String getCompanyName() {
+		return companyName;
+	}
+
+	public void setCompanyName(String companyName) {
+		this.companyName = companyName;
+	}
+
+	public String getPostCode() {
+		return postCode;
+	}
+
+	public void setPostCode(String postCode) {
+		this.postCode = postCode;
+	}
+
+	public String getPostName() {
+		return postName;
+	}
+
+	public void setPostName(String postName) {
+		this.postName = postName;
+	}
+
+	public String getRemarks() {
+		return remarks;
+	}
+
+	public void setRemarks(String remarks) {
+		this.remarks = remarks;
+	}
+}

+ 21 - 1
src/main/java/com/jeesite/modules/mes/mqtt/Adw300PayloadParser.java

@@ -29,8 +29,28 @@ public class Adw300PayloadParser {
 		POINT_META.put(11, new String[]{"B相有功功率", "kW"});
 		POINT_META.put(12, new String[]{"C相有功功率", "kW"});
 		POINT_META.put(13, new String[]{"总有功功率", "kW"});
+		POINT_META.put(14, new String[]{"A相无功功率", "kvar"});
+		POINT_META.put(15, new String[]{"B相无功功率", "kvar"});
+		POINT_META.put(16, new String[]{"C相无功功率", "kvar"});
+		POINT_META.put(17, new String[]{"总无功功率", "kvar"});
+		POINT_META.put(18, new String[]{"A相视在功率", "kVA"});
+		POINT_META.put(19, new String[]{"B相视在功率", "kVA"});
+		POINT_META.put(20, new String[]{"C相视在功率", "kVA"});
+		POINT_META.put(21, new String[]{"总视在功率", "kVA"});
+		POINT_META.put(22, new String[]{"A相功率因数", ""});
+		POINT_META.put(23, new String[]{"B相功率因数", ""});
+		POINT_META.put(24, new String[]{"C相功率因数", ""});
+		POINT_META.put(25, new String[]{"总功率因数", ""});
 		POINT_META.put(26, new String[]{"频率", "Hz"});
-		POINT_META.put(33, new String[]{"物联网卡号", ""});
+		POINT_META.put(27, new String[]{"信号强度", "dBm"});
+		POINT_META.put(28, new String[]{"正向有功总电能示值", "kWh"});
+		POINT_META.put(29, new String[]{"反向有功总电能示值", "kWh"});
+		POINT_META.put(30, new String[]{"正向无功总电能示值", "kvarh"});
+		POINT_META.put(31, new String[]{"反向无功总电能示值", "kvarh"});
+		POINT_META.put(32, new String[]{"当前有功需量", "kW"});
+		POINT_META.put(33, new String[]{"ICCID", ""});
+		POINT_META.put(34, new String[]{"PT", ""});
+		POINT_META.put(35, new String[]{"CT", ""});
 	}
 
 	public Adw300Message parse(String topic, String payload) {

+ 158 - 20
src/main/java/com/jeesite/modules/mes/service/MesAdw300DataService.java

@@ -1,9 +1,11 @@
 package com.jeesite.modules.mes.service;
 
 import java.util.Date;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import com.jeesite.common.collect.ListUtils;
 import com.jeesite.common.collect.MapUtils;
@@ -24,6 +26,33 @@ import org.springframework.transaction.annotation.Transactional;
 @Service
 public class MesAdw300DataService extends CrudService<MesAdw300DataDao, MesAdw300Data> {
 
+	private static final Map<String, String> DEVICE_NAME_MAP = new LinkedHashMap<>();
+	private static final Map<String, Double> CT_RATIO_MAP = new LinkedHashMap<>();
+	private static final Set<Integer> SCREEN_POINT_IDS = new HashSet<>();
+
+	static {
+		DEVICE_NAME_MAP.put("26060805200001", "配电房电表");
+		DEVICE_NAME_MAP.put("26060805200002", "空压机房电表");
+		CT_RATIO_MAP.put("26060805200001", 500D);
+		CT_RATIO_MAP.put("26060805200002", 100D);
+
+		SCREEN_POINT_IDS.add(1);
+		SCREEN_POINT_IDS.add(2);
+		SCREEN_POINT_IDS.add(3);
+		SCREEN_POINT_IDS.add(7);
+		SCREEN_POINT_IDS.add(8);
+		SCREEN_POINT_IDS.add(9);
+		SCREEN_POINT_IDS.add(13);
+		SCREEN_POINT_IDS.add(22);
+		SCREEN_POINT_IDS.add(23);
+		SCREEN_POINT_IDS.add(24);
+		SCREEN_POINT_IDS.add(25);
+		SCREEN_POINT_IDS.add(26);
+		SCREEN_POINT_IDS.add(28);
+		SCREEN_POINT_IDS.add(34);
+		SCREEN_POINT_IDS.add(35);
+	}
+
 	@Override
 	public MesAdw300Data get(MesAdw300Data mesAdw300Data) {
 		return super.get(mesAdw300Data);
@@ -72,28 +101,32 @@ public class MesAdw300DataService extends CrudService<MesAdw300DataDao, MesAdw30
 			String deviceSn = entry.getKey();
 			Map<Integer, Map<String, Object>> points = entry.getValue();
 			Map<String, Object> device = MapUtils.newHashMap();
-			double power = getPointDouble(points, 13);
-			double todayEnergy = todayEnergyMap.getOrDefault(deviceSn, 0D);
+			double power = calculatePower(deviceSn, points);
+			double todayEnergyRaw = todayEnergyMap.getOrDefault(deviceSn, 0D);
+			double todayEnergy = scaleByCtRatio(deviceSn, todayEnergyRaw);
 			Date reportTime = getDate(points);
-			boolean online = reportTime != null && now.getTime() - reportTime.getTime() <= 5L * 60L * 1000L;
+			boolean online = !points.isEmpty();
 			if (online) {
 				onlineCount++;
 			}
 			totalPower += power;
 			totalEnergy += todayEnergy;
+			double totalEnergyValue = scaleByCtRatio(deviceSn, getPointDouble(points, 28));
 
 			device.put("deviceSn", deviceSn);
+			device.put("deviceName", getDeviceName(deviceSn));
 			device.put("online", online);
 			device.put("reportTime", reportTime);
+			device.put("reportTimeText", getDateText(points));
 			device.put("power", round(power));
 			device.put("todayEnergy", round(todayEnergy));
-			device.put("totalEnergy", round(getPointDouble(points, 28)));
+			device.put("totalEnergy", round(totalEnergyValue));
 			device.put("ua", round(getPointDouble(points, 1)));
 			device.put("ub", round(getPointDouble(points, 2)));
 			device.put("uc", round(getPointDouble(points, 3)));
-			device.put("ia", round(getPointDouble(points, 7)));
-			device.put("ib", round(getPointDouble(points, 8)));
-			device.put("ic", round(getPointDouble(points, 9)));
+			device.put("ia", round(scaleByCtRatio(deviceSn, getPointDouble(points, 7))));
+			device.put("ib", round(scaleByCtRatio(deviceSn, getPointDouble(points, 8))));
+			device.put("ic", round(scaleByCtRatio(deviceSn, getPointDouble(points, 9))));
 			devices.add(device);
 		}
 
@@ -118,7 +151,16 @@ public class MesAdw300DataService extends CrudService<MesAdw300DataDao, MesAdw30
 	@Transactional
 	public void saveMessage(Adw300Message message) {
 		Date reportTime = message.getReportTime() == null ? new Date() : message.getReportTime();
+		Date storeWindowStart = getHalfHourStart(reportTime);
+		Date storeWindowEnd = new Date(storeWindowStart.getTime() + 30L * 60L * 1000L);
+		Long savedCount = dao.countByDeviceAndReportTimeRange(message.getDeviceNo(), storeWindowStart, storeWindowEnd);
+		if (savedCount != null && savedCount > 0) {
+			return;
+		}
 		for (Adw300Point point : message.getPoints()) {
+			if (!SCREEN_POINT_IDS.contains(point.getId())) {
+				continue;
+			}
 			MesAdw300Data data = new MesAdw300Data();
 			data.setDeviceSn(message.getDeviceNo());
 			data.setReportTime(reportTime);
@@ -127,7 +169,6 @@ public class MesAdw300DataService extends CrudService<MesAdw300DataDao, MesAdw30
 			data.setPointName(point.getName());
 			data.setPointValue(point.getValue() == null ? null : String.valueOf(point.getValue()));
 			data.setUnit(point.getUnit());
-			data.setRawPayload(message.getPayload());
 			this.save(data);
 		}
 	}
@@ -154,6 +195,16 @@ public class MesAdw300DataService extends CrudService<MesAdw300DataDao, MesAdw30
 		return calendar.getTime();
 	}
 
+	private Date getHalfHourStart(Date reportTime) {
+		java.util.Calendar calendar = java.util.Calendar.getInstance();
+		calendar.setTime(reportTime);
+		int minute = calendar.get(java.util.Calendar.MINUTE);
+		calendar.set(java.util.Calendar.MINUTE, minute < 30 ? 0 : 30);
+		calendar.set(java.util.Calendar.SECOND, 0);
+		calendar.set(java.util.Calendar.MILLISECOND, 0);
+		return calendar.getTime();
+	}
+
 	private List<String> buildHours() {
 		List<String> hours = ListUtils.newArrayList();
 		for (int i = 0; i < 24; i++) {
@@ -167,17 +218,7 @@ public class MesAdw300DataService extends CrudService<MesAdw300DataDao, MesAdw30
 		for (String deviceSn : deviceSnList) {
 			dataMap.put(deviceSn, new LinkedHashMap<>());
 		}
-		for (Map<String, Object> row : rows) {
-			String deviceSn = getString(row.get("deviceSn"));
-			String hourLabel = getString(row.get("hourLabel"));
-			if (deviceSn == null || hourLabel == null) {
-				continue;
-			}
-			if (!dataMap.containsKey(deviceSn)) {
-				dataMap.put(deviceSn, new LinkedHashMap<>());
-			}
-			dataMap.get(deviceSn).put(hourLabel, getDouble(row.get("energy")));
-		}
+		putHourlyRows(dataMap, rows, false);
 
 		List<String> hours = buildHours();
 		List<Map<String, Object>> series = ListUtils.newArrayList();
@@ -187,13 +228,30 @@ public class MesAdw300DataService extends CrudService<MesAdw300DataDao, MesAdw30
 			for (String hour : hours) {
 				values.add(round(entry.getValue().getOrDefault(hour, 0D)));
 			}
-			item.put("name", entry.getKey());
+			item.put("name", getDeviceName(entry.getKey()));
 			item.put("data", values);
 			series.add(item);
 		}
 		return series;
 	}
 
+	private void putHourlyRows(Map<String, Map<String, Double>> dataMap, List<Map<String, Object>> rows, boolean fallback) {
+		for (Map<String, Object> row : rows) {
+			String deviceSn = getString(row.get("deviceSn"));
+			String hourLabel = getString(row.get("hourLabel"));
+			if (deviceSn == null || hourLabel == null) {
+				continue;
+			}
+			if (!dataMap.containsKey(deviceSn)) {
+				dataMap.put(deviceSn, new LinkedHashMap<>());
+			}
+			Double current = dataMap.get(deviceSn).get(hourLabel);
+			if (!fallback || current == null || current <= 0D) {
+				dataMap.get(deviceSn).put(hourLabel, scaleByCtRatio(deviceSn, getDouble(row.get("energy"))));
+			}
+		}
+	}
+
 	private Map<String, Double> mapNumberByDevice(List<Map<String, Object>> rows, String key) {
 		Map<String, Double> result = new LinkedHashMap<>();
 		for (Map<String, Object> row : rows) {
@@ -207,14 +265,67 @@ public class MesAdw300DataService extends CrudService<MesAdw300DataDao, MesAdw30
 
 	private Date getDate(Map<Integer, Map<String, Object>> points) {
 		for (Map<String, Object> point : points.values()) {
+			Object textValue = point.get("reportTimeText");
+			if (textValue != null) {
+				try {
+					return com.jeesite.common.lang.DateUtils.parseDate(String.valueOf(textValue));
+				} catch (Exception ignored) {
+					// Try typed report time below.
+				}
+			}
 			Object value = point.get("reportTime");
 			if (value instanceof Date) {
 				return (Date) value;
 			}
+			if (value != null) {
+				try {
+					return com.jeesite.common.lang.DateUtils.parseDate(String.valueOf(value));
+				} catch (Exception ignored) {
+					// Try possible non-camel-case map keys below.
+				}
+			}
+			value = point.get("report_time");
+			if (value instanceof Date) {
+				return (Date) value;
+			}
+			if (value != null) {
+				try {
+					return com.jeesite.common.lang.DateUtils.parseDate(String.valueOf(value));
+				} catch (Exception ignored) {
+					return null;
+				}
+			}
+		}
+		return null;
+	}
+
+	private String getDateText(Map<Integer, Map<String, Object>> points) {
+		for (Map<String, Object> point : points.values()) {
+			Object value = point.get("reportTimeText");
+			if (value != null) {
+				return String.valueOf(value);
+			}
+			value = point.get("reportTime");
+			if (value != null) {
+				return String.valueOf(value);
+			}
+			value = point.get("report_time");
+			if (value != null) {
+				return String.valueOf(value);
+			}
 		}
 		return null;
 	}
 
+	private String getDeviceName(String deviceSn) {
+		String name = DEVICE_NAME_MAP.get(deviceSn);
+		return name == null ? deviceSn : name;
+	}
+
+	private double scaleByCtRatio(String deviceSn, double value) {
+		return value * CT_RATIO_MAP.getOrDefault(deviceSn, 1D);
+	}
+
 	private double getPointDouble(Map<Integer, Map<String, Object>> points, int pointId) {
 		Map<String, Object> point = points.get(pointId);
 		if (point == null) {
@@ -258,4 +369,31 @@ public class MesAdw300DataService extends CrudService<MesAdw300DataDao, MesAdw30
 	private double round(double value) {
 		return Math.round(value * 1000D) / 1000D;
 	}
+
+	private double calculatePower(String deviceSn, Map<Integer, Map<String, Object>> points) {
+		double ua = getPointDouble(points, 1);
+		double ub = getPointDouble(points, 2);
+		double uc = getPointDouble(points, 3);
+		double ia = scaleByCtRatio(deviceSn, getPointDouble(points, 7));
+		double ib = scaleByCtRatio(deviceSn, getPointDouble(points, 8));
+		double ic = scaleByCtRatio(deviceSn, getPointDouble(points, 9));
+		double pfa = Math.abs(getPointDouble(points, 22));
+		double pfb = Math.abs(getPointDouble(points, 23));
+		double pfc = Math.abs(getPointDouble(points, 24));
+		if (pfa > 0D && pfa <= 1D && pfb > 0D && pfb <= 1D && pfc > 0D && pfc <= 1D) {
+			return (ua * ia * pfa + ub * ib * pfb + uc * ic * pfc) / 1000D;
+		}
+
+		double powerFactor = Math.abs(getPointDouble(points, 25));
+		if (powerFactor <= 0D || powerFactor > 1D) {
+			powerFactor = 0.9D;
+		}
+
+		double avgVoltage = (ua + ub + uc) / 3D;
+		double avgCurrent = (ia + ib + ic) / 3D;
+		if (avgVoltage <= 0D || avgCurrent <= 0D) {
+			return scaleByCtRatio(deviceSn, Math.abs(getPointDouble(points, 13)));
+		}
+		return 3D * avgVoltage * avgCurrent * powerFactor / 1000D;
+	}
 }

+ 32 - 0
src/main/java/com/jeesite/modules/mes/service/MesEmailLoginCodeService.java

@@ -99,6 +99,38 @@ public class MesEmailLoginCodeService extends CrudService<MesEmailLoginCodeDao,
     }
 
     @Transactional
+    public String sendCodeToBoundEmail(String loginCode) {
+        loginCode = StringUtils.trim(loginCode);
+        User user = UserUtils.getByLoginCode(loginCode);
+        if (user == null || !"0".equals(user.getStatus())) {
+            return "账号状态异常。";
+        }
+        if (StringUtils.isBlank(user.getEmail())) {
+            return "该账号未绑定邮箱,无法发送验证码。";
+        }
+        return sendCode(loginCode, user.getEmail());
+    }
+
+    public String getBoundEmail(String loginCode) {
+        User user = UserUtils.getByLoginCode(StringUtils.trim(loginCode));
+        return user == null ? null : StringUtils.trim(user.getEmail());
+    }
+
+    public String maskEmail(String email) {
+        email = StringUtils.trim(email);
+        int atIndex = email.indexOf("@");
+        if (atIndex <= 1) {
+            return email;
+        }
+        String name = email.substring(0, atIndex);
+        String domain = email.substring(atIndex);
+        if (name.length() <= 3) {
+            return name.substring(0, 1) + "***" + domain;
+        }
+        return name.substring(0, 2) + "***" + name.substring(name.length() - 1) + domain;
+    }
+
+    @Transactional
     public String validateCode(String loginCode, String email, String code) {
         loginCode = StringUtils.trim(loginCode);
         email = StringUtils.trim(email);

+ 65 - 0
src/main/java/com/jeesite/modules/mes/service/MesEmployeeService.java

@@ -1,13 +1,20 @@
 package com.jeesite.modules.mes.service;
 
 import com.jeesite.common.entity.Page;
+import com.jeesite.common.config.Global;
 import com.jeesite.common.lang.StringUtils;
 import com.jeesite.common.service.CrudService;
+import com.jeesite.common.service.ServiceException;
+import com.jeesite.common.utils.excel.ExcelImport;
+import com.jeesite.common.validator.ValidatorUtils;
 import com.jeesite.modules.mes.dao.MesEmployeeDao;
 import com.jeesite.modules.mes.entity.MesEmployee;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
 
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
 import java.util.List;
 import java.util.UUID;
 
@@ -37,9 +44,17 @@ public class MesEmployeeService extends CrudService<MesEmployeeDao, MesEmployee>
 	@Override
 	@Transactional
 	public void save(MesEmployee mesEmployee) {
+		if (StringUtils.isNotBlank(mesEmployee.getEmpCode())) {
+			mesEmployee.setEmpCode(mesEmployee.getEmpCode().trim());
+		}
+		if (StringUtils.isNotBlank(mesEmployee.getEmpNo())) {
+			mesEmployee.setEmpNo(mesEmployee.getEmpNo().trim());
+		}
 		if (StringUtils.isBlank(mesEmployee.getEmpCode())) {
 			mesEmployee.setEmpCode(buildEmpCode(mesEmployee));
 			mesEmployee.setIsNewRecord(true);
+		} else if (super.get(new MesEmployee(mesEmployee.getEmpCode())) == null) {
+			mesEmployee.setIsNewRecord(true);
 		}
 		if (StringUtils.isBlank(mesEmployee.getEmpNameEn())) {
 			mesEmployee.setEmpNameEn("");
@@ -65,6 +80,56 @@ public class MesEmployeeService extends CrudService<MesEmployeeDao, MesEmployee>
 		super.save(mesEmployee);
 	}
 
+	@Transactional
+	public String importData(MultipartFile file) {
+		if (file == null) {
+			throw new ServiceException(text("\u8bf7\u9009\u62e9\u5bfc\u5165\u7684\u6570\u636e\u6587\u4ef6\uff01"));
+		}
+		int successNum = 0;
+		int failureNum = 0;
+		StringBuilder successMsg = new StringBuilder();
+		StringBuilder failureMsg = new StringBuilder();
+		try (ExcelImport ei = new ExcelImport(file, 2, 0)) {
+			List<MesEmployee> list = ei.getDataList(MesEmployee.class);
+			for (MesEmployee mesEmployee : list) {
+				try {
+					ValidatorUtils.validateWithException(mesEmployee);
+					this.save(mesEmployee);
+					successNum++;
+					successMsg.append("<br/>").append(successNum).append("\u3001\u5458\u5de5 ")
+							.append(StringUtils.defaultString(mesEmployee.getEmpName(), mesEmployee.getEmpCode()))
+							.append(" \u5bfc\u5165\u6210\u529f");
+				} catch (Exception e) {
+					failureNum++;
+					StringBuilder msg = new StringBuilder("<br/>").append(failureNum).append("\u3001\u5458\u5de5 ")
+							.append(StringUtils.defaultString(mesEmployee.getEmpName(), mesEmployee.getEmpCode()))
+							.append(" \u5bfc\u5165\u5931\u8d25\uff1a");
+					if (e instanceof ConstraintViolationException) {
+						ConstraintViolationException cve = (ConstraintViolationException)e;
+						for (ConstraintViolation<?> violation : cve.getConstraintViolations()) {
+							msg.append(Global.getText(violation.getMessage())).append(" (")
+									.append(violation.getPropertyPath()).append(")");
+						}
+					} else {
+						msg.append(e.getMessage());
+					}
+					failureMsg.append(msg);
+					logger.error(msg.toString(), e);
+				}
+			}
+		} catch (Exception e) {
+			logger.error(e.getMessage(), e);
+			failureMsg.append(e.getMessage());
+			return failureMsg.toString();
+		}
+		if (failureNum > 0) {
+			failureMsg.insert(0, "\u5f88\u62b1\u6b49\uff0c\u5bfc\u5165\u5931\u8d25\uff01\u5171 " + failureNum + " \u6761\u6570\u636e\u683c\u5f0f\u4e0d\u6b63\u786e\uff0c\u9519\u8bef\u5982\u4e0b\uff1a");
+			throw new ServiceException(failureMsg.toString());
+		}
+		successMsg.insert(0, "\u606d\u559c\u60a8\uff0c\u6570\u636e\u5df2\u5168\u90e8\u5bfc\u5165\u6210\u529f\uff01\u5171 " + successNum + " \u6761\uff0c\u6570\u636e\u5982\u4e0b\uff1a");
+		return successMsg.toString();
+	}
+
 	private String buildEmpCode(MesEmployee employee) {
 		if (StringUtils.isNotBlank(employee.getEmpNo())) {
 			return employee.getEmpNo().trim();

+ 36 - 3
src/main/java/com/jeesite/modules/mes/service/MesTemplateItemsService.java

@@ -1,12 +1,16 @@
 package com.jeesite.modules.mes.service;
 
 import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
 
 import com.alibaba.fastjson.JSON;
 import com.jeesite.common.cache.CacheUtils;
 import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.lang.StringUtils;
 import com.jeesite.common.lang.ObjectUtils;
 import com.jeesite.modules.mes.entity.MesTemplate;
+import com.jeesite.modules.mes.entity.MesLineProcess;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -27,6 +31,9 @@ public class MesTemplateItemsService extends CrudService<MesTemplateItemsDao, Me
 	@Autowired
 	private MesTemplateItemsDao mesTemplateItemsDao;
 
+	@Autowired
+	private MesLineProcessService mesLineProcessService;
+
 	/**
 	 * 获取单条数据
 	 * @param mesTemplateItems
@@ -49,7 +56,9 @@ public class MesTemplateItemsService extends CrudService<MesTemplateItemsDao, Me
 	 */
 	@Override
 	public Page<MesTemplateItems> findPage(MesTemplateItems mesTemplateItems) {
-		return super.findPage(mesTemplateItems);
+		Page<MesTemplateItems> page = super.findPage(mesTemplateItems);
+		fillOprnoName(page.getList());
+		return page;
 	}
 	
 	/**
@@ -59,7 +68,31 @@ public class MesTemplateItemsService extends CrudService<MesTemplateItemsDao, Me
 	 */
 	@Override
 	public List<MesTemplateItems> findList(MesTemplateItems mesTemplateItems) {
-		return super.findList(mesTemplateItems);
+		List<MesTemplateItems> list = super.findList(mesTemplateItems);
+		fillOprnoName(list);
+		return list;
+	}
+
+	private void fillOprnoName(List<MesTemplateItems> list) {
+		if (ListUtils.isEmpty(list)) {
+			return;
+		}
+		MesLineProcess query = new MesLineProcess();
+		query.setStatus(MesLineProcess.STATUS_NORMAL);
+		List<MesLineProcess> processList = mesLineProcessService.findList(query);
+		if (ListUtils.isEmpty(processList)) {
+			return;
+		}
+		Map<String, String> processMap = new HashMap<>();
+		for (MesLineProcess process : processList) {
+			if (StringUtils.isNotBlank(process.getOprno()) && StringUtils.isNotBlank(process.getTitle())
+					&& !processMap.containsKey(process.getOprno())) {
+				processMap.put(process.getOprno(), process.getTitle());
+			}
+		}
+		for (MesTemplateItems item : list) {
+			item.setOprnoName(processMap.get(item.getOprno()));
+		}
 	}
 
 	public List<MesTemplateItems> findListItems(String templateId){
@@ -163,4 +196,4 @@ public class MesTemplateItemsService extends CrudService<MesTemplateItemsDao, Me
 		super.delete(mesTemplateItems);
 	}
 	
-}
+}

+ 46 - 0
src/main/java/com/jeesite/modules/mes/service/MesUserEmailVerifyConfigService.java

@@ -0,0 +1,46 @@
+package com.jeesite.modules.mes.service;
+
+import com.jeesite.common.idgen.IdGen;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.dao.MesUserEmailVerifyConfigDao;
+import com.jeesite.modules.mes.entity.MesUserEmailVerifyConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 用户邮箱二次验证配置 Service。
+ */
+@Service
+public class MesUserEmailVerifyConfigService extends CrudService<MesUserEmailVerifyConfigDao, MesUserEmailVerifyConfig> {
+
+	@Autowired
+	private MesUserEmailVerifyConfigDao mesUserEmailVerifyConfigDao;
+
+	public MesUserEmailVerifyConfig getByLoginCode(String loginCode) {
+		if (StringUtils.isBlank(loginCode)) {
+			return null;
+		}
+		return mesUserEmailVerifyConfigDao.getByLoginCode(StringUtils.trim(loginCode));
+	}
+
+	public boolean isEnabled(String loginCode) {
+		MesUserEmailVerifyConfig config = getByLoginCode(loginCode);
+		return config != null && "1".equals(config.getEnabled());
+	}
+
+	@Transactional
+	public void saveEnabled(String loginCode, boolean enabled) {
+		loginCode = StringUtils.trim(loginCode);
+		MesUserEmailVerifyConfig config = getByLoginCode(loginCode);
+		if (config == null) {
+			config = new MesUserEmailVerifyConfig();
+			config.setId(IdGen.randomBase62(19));
+			config.setIsNewRecord(true);
+			config.setLoginCode(loginCode);
+		}
+		config.setEnabled(enabled ? "1" : "0");
+		save(config);
+	}
+}

+ 215 - 3
src/main/java/com/jeesite/modules/mes/web/EmpUserCustomController.java

@@ -1,14 +1,19 @@
 package com.jeesite.modules.mes.web;
 
 import com.jeesite.common.config.Global;
+import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.lang.DateUtils;
 import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.ServiceException;
+import com.jeesite.common.utils.excel.ExcelExport;
+import com.jeesite.common.utils.excel.ExcelImport;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
 import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.excel.ExcelEmpUserEmployee;
 import com.jeesite.modules.mes.entity.MesEmployee;
 import com.jeesite.modules.mes.service.MesEmployeeService;
 import com.jeesite.modules.sys.entity.EmpUser;
-import com.jeesite.modules.sys.entity.EmployeeOffice;
-import com.jeesite.modules.sys.entity.Role;
-import com.jeesite.modules.sys.entity.User;
+import com.jeesite.modules.sys.entity.*;
 import com.jeesite.modules.sys.service.EmpUserService;
 import com.jeesite.modules.sys.service.UserService;
 import com.jeesite.modules.sys.web.user.EmpUserController;
@@ -16,10 +21,13 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -166,6 +174,210 @@ public class EmpUserCustomController extends BaseController {
         return empUserController.save(empUser, oldLoginCode, request);
     }
 
+    @RequestMapping(value = "exportData")
+    public void exportData(EmpUser empUser, Boolean ctrlPermi, String ctrlPermiFilter, HttpServletResponse response) {
+        empUser.getEmployee().getOffice().setIsQueryChildren(true);
+        empUser.getEmployee().getCompany().setIsQueryChildren(true);
+        if (ctrlPermi == null || ctrlPermi || Global.isStrictMode()) {
+            empUserService.addDataScopeFilter(empUser, ctrlPermiFilter);
+        }
+        List<ExcelEmpUserEmployee> list = ListUtils.newArrayList();
+        for (EmpUser user : empUserService.findList(empUser)) {
+            list.add(toExcel(user));
+        }
+        String fileName = "\u7528\u6237\u5458\u5de5\u6570\u636e" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+        try (ExcelExport ee = new ExcelExport("\u7528\u6237\u5458\u5de5\u6570\u636e", ExcelEmpUserEmployee.class)) {
+            ee.setDataList(list).write(response, fileName);
+        }
+    }
+
+    @RequestMapping(value = "importTemplate")
+    public void importTemplate(HttpServletResponse response) {
+        List<ExcelEmpUserEmployee> list = ListUtils.newArrayList(new ExcelEmpUserEmployee());
+        String fileName = "\u7528\u6237\u5458\u5de5\u6570\u636e\u6a21\u677f.xlsx";
+        try (ExcelExport ee = new ExcelExport("\u7528\u6237\u5458\u5de5\u6570\u636e", ExcelEmpUserEmployee.class, Type.IMPORT)) {
+            ee.setDataList(list).write(response, fileName);
+        }
+    }
+
+    @PostMapping(value = "importData")
+    @ResponseBody
+    public String importData(MultipartFile file, String updateSupport) {
+        try {
+            String message = importUserEmployeeData(file, "1".equals(updateSupport));
+            return renderResult(Global.TRUE, "posfull:" + message);
+        } catch (Exception ex) {
+            return renderResult(Global.FALSE, "posfull:" + ex.getMessage());
+        }
+    }
+
+    private String importUserEmployeeData(MultipartFile file, boolean updateSupport) {
+        if (file == null) {
+            throw new ServiceException("\u8bf7\u9009\u62e9\u5bfc\u5165\u7684\u6570\u636e\u6587\u4ef6\uff01");
+        }
+        int successNum = 0;
+        int failureNum = 0;
+        StringBuilder successMsg = new StringBuilder();
+        StringBuilder failureMsg = new StringBuilder();
+        try (ExcelImport ei = new ExcelImport(file, 2, 0)) {
+            List<ExcelEmpUserEmployee> list = ei.getDataList(ExcelEmpUserEmployee.class);
+            for (ExcelEmpUserEmployee item : list) {
+                try {
+                    EmpUser empUser = buildImportUser(item, updateSupport);
+                    empUserService.save(empUser);
+                    if (StringUtils.isNotBlank(item.getUserRoleString())) {
+                        userService.saveAuth(empUser);
+                    }
+                    successNum++;
+                    successMsg.append("<br/>").append(successNum).append("\u3001\u8d26\u53f7 ")
+                            .append(item.getLoginCode()).append(" \u5bfc\u5165\u6210\u529f");
+                } catch (Exception e) {
+                    failureNum++;
+                    String msg = "<br/>" + failureNum + "\u3001\u8d26\u53f7 "
+                            + StringUtils.defaultString(item.getLoginCode(), item.getEmpName())
+                            + " \u5bfc\u5165\u5931\u8d25\uff1a" + e.getMessage();
+                    failureMsg.append(msg);
+                    logger.error(msg, e);
+                }
+            }
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+            throw new ServiceException(e.getMessage());
+        }
+        if (failureNum > 0) {
+            failureMsg.insert(0, "\u5f88\u62b1\u6b49\uff0c\u5bfc\u5165\u5931\u8d25\uff01\u5171 " + failureNum + " \u6761\u6570\u636e\u683c\u5f0f\u4e0d\u6b63\u786e\uff0c\u9519\u8bef\u5982\u4e0b\uff1a");
+            throw new ServiceException(failureMsg.toString());
+        }
+        successMsg.insert(0, "\u606d\u559c\u60a8\uff0c\u6570\u636e\u5df2\u5168\u90e8\u5bfc\u5165\u6210\u529f\uff01\u5171 " + successNum + " \u6761\uff0c\u6570\u636e\u5982\u4e0b\uff1a");
+        return successMsg.toString();
+    }
+
+    private EmpUser buildImportUser(ExcelEmpUserEmployee item, boolean updateSupport) {
+        item.setLoginCode(StringUtils.trim(item.getLoginCode()));
+        item.setEmpCode(StringUtils.trim(item.getEmpCode()));
+        item.setEmpNo(StringUtils.trim(item.getEmpNo()));
+        item.setEmpName(StringUtils.trim(item.getEmpName()));
+        if (StringUtils.isBlank(item.getLoginCode())) {
+            throw new ServiceException("\u767b\u5f55\u8d26\u53f7\u4e0d\u80fd\u4e3a\u7a7a");
+        }
+        if (StringUtils.isBlank(item.getEmpName())) {
+            throw new ServiceException("\u5458\u5de5\u59d3\u540d\u4e0d\u80fd\u4e3a\u7a7a");
+        }
+        if (StringUtils.isBlank(item.getEmpNo())) {
+            item.setEmpNo(item.getLoginCode());
+        }
+        MesEmployee employee = findOrCreateEmployee(item);
+        User query = new User();
+        query.setLoginCode(item.getLoginCode());
+        User exists = userService.getByLoginCode(query);
+        if (exists != null && !updateSupport) {
+            throw new ServiceException("\u767b\u5f55\u8d26\u53f7\u5df2\u5b58\u5728");
+        }
+        if (exists != null && StringUtils.isNotBlank(exists.getRefCode())
+                && !StringUtils.equals(exists.getRefCode(), employee.getEmpCode())) {
+            User boundQuery = new User();
+            boundQuery.setUserType("employee");
+            boundQuery.setRefCode(employee.getEmpCode());
+            User boundUser = userService.getByUserTypeAndRefCode(boundQuery);
+            if (boundUser != null && !StringUtils.equals(boundUser.getUserCode(), exists.getUserCode())) {
+                throw new ServiceException("\u5458\u5de5\u5df2\u5173\u8054\u5176\u4ed6\u8d26\u53f7\uff1a" + boundUser.getLoginCode());
+            }
+        }
+        EmpUser empUser = new EmpUser(exists == null ? null : exists.getUserCode());
+        if (exists != null) {
+            empUser = empUserService.get(exists.getUserCode(), false);
+        }
+        empUser.setLoginCode(item.getLoginCode());
+        empUser.setUserName(StringUtils.defaultIfBlank(item.getUserName(), item.getEmpName()));
+        empUser.setEmail(item.getEmail());
+        empUser.setMobile(item.getMobile());
+        empUser.setPhone(item.getPhone());
+        empUser.setStatus(StringUtils.defaultIfBlank(item.getStatus(), User.STATUS_NORMAL));
+        empUser.setUserType("employee");
+        empUser.setMgrType(StringUtils.defaultIfBlank(empUser.getMgrType(), "0"));
+        empUser.setRefCode(employee.getEmpCode());
+        empUser.setRefName(employee.getEmpName());
+        empUser.setUserRoleString(item.getUserRoleString());
+        empUser.setEmployee(null);
+        return empUser;
+    }
+
+    private MesEmployee findOrCreateEmployee(ExcelEmpUserEmployee item) {
+        MesEmployee employee = null;
+        if (StringUtils.isNotBlank(item.getEmpCode())) {
+            employee = mesEmployeeService.get(new MesEmployee(item.getEmpCode()));
+        }
+        if (employee == null && StringUtils.isNotBlank(item.getEmpNo())) {
+            MesEmployee where = new MesEmployee();
+            where.setEmpNo(item.getEmpNo());
+            List<MesEmployee> list = mesEmployeeService.findList(where);
+            if (list != null && !list.isEmpty()) {
+                employee = list.get(0);
+            }
+        }
+        if (employee == null) {
+            employee = new MesEmployee();
+            employee.setEmpCode(item.getEmpCode());
+        }
+        employee.setEmpName(item.getEmpName());
+        employee.setEmpNameEn(item.getEmpNameEn());
+        employee.setEmpNo(item.getEmpNo());
+        employee.setOfficeCode(item.getOfficeCode());
+        employee.setOfficeName(item.getOfficeName());
+        employee.setCompanyCode(item.getCompanyCode());
+        employee.setCompanyName(item.getCompanyName());
+        employee.setPostCode(item.getPostCode());
+        employee.setPostName(item.getPostName());
+        employee.setRemarks(item.getRemarks());
+        employee.setStatus(MesEmployee.STATUS_NORMAL);
+        mesEmployeeService.save(employee);
+        return employee;
+    }
+
+    private ExcelEmpUserEmployee toExcel(EmpUser user) {
+        ExcelEmpUserEmployee excel = new ExcelEmpUserEmployee();
+        excel.setLoginCode(user.getLoginCode());
+        excel.setUserName(user.getUserName());
+        excel.setEmail(user.getEmail());
+        excel.setMobile(user.getMobile());
+        excel.setPhone(user.getPhone());
+        excel.setStatus(user.getStatus());
+        MesEmployee employee = null;
+        if (StringUtils.isNotBlank(user.getRefCode())) {
+            employee = mesEmployeeService.get(new MesEmployee(user.getRefCode()));
+        }
+        if (employee == null) {
+            employee = new MesEmployee();
+            if (user.getEmployee() != null) {
+                employee.setEmpCode(user.getEmployee().getEmpCode());
+                employee.setEmpNo(user.getEmployee().getEmpNo());
+                employee.setEmpName(user.getEmployee().getEmpName());
+                employee.setEmpNameEn(user.getEmployee().getEmpNameEn());
+                if (user.getEmployee().getOffice() != null) {
+                    employee.setOfficeCode(user.getEmployee().getOffice().getOfficeCode());
+                    employee.setOfficeName(user.getEmployee().getOffice().getOfficeName());
+                }
+                if (user.getEmployee().getCompany() != null) {
+                    employee.setCompanyCode(user.getEmployee().getCompany().getCompanyCode());
+                    employee.setCompanyName(user.getEmployee().getCompany().getCompanyName());
+                }
+                employee.setPostCode(user.getEmployee().getPostCode());
+            }
+        }
+        excel.setEmpCode(employee.getEmpCode());
+        excel.setEmpNo(employee.getEmpNo());
+        excel.setEmpName(StringUtils.defaultIfBlank(employee.getEmpName(), user.getRefName()));
+        excel.setEmpNameEn(employee.getEmpNameEn());
+        excel.setOfficeCode(employee.getOfficeCode());
+        excel.setOfficeName(employee.getOfficeName());
+        excel.setCompanyCode(employee.getCompanyCode());
+        excel.setCompanyName(employee.getCompanyName());
+        excel.setPostCode(employee.getPostCode());
+        excel.setPostName(employee.getPostName());
+        excel.setRemarks(employee.getRemarks());
+        return excel;
+    }
+
     private MesEmployee findSelectedEmployee(EmpUser empUser) {
         if (empUser.getEmployee() == null) {
             return null;

+ 1 - 0
src/main/java/com/jeesite/modules/mes/web/MesDeviceController.java

@@ -587,6 +587,7 @@ public class MesDeviceController extends BaseController {
 			rankItem.put("name", card.get("name"));
 			rankItem.put("sn", card.get("sn"));
 			rankItem.put("openRate", card.get("openRate"));
+			rankItem.put("openMinutes", card.get("openMinutes"));
 			ranking.add(rankItem);
 		}
 

+ 149 - 5
src/main/java/com/jeesite/modules/mes/web/MesEmailLoginController.java

@@ -1,18 +1,28 @@
 package com.jeesite.modules.mes.web;
 
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpSession;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import com.alibaba.fastjson.JSON;
 import com.jeesite.common.codec.DesUtils;
 import com.jeesite.common.config.Global;
 import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.shiro.authc.FormToken;
 import com.jeesite.common.shiro.filter.FormFilter;
 import com.jeesite.common.web.BaseController;
 import com.jeesite.modules.mes.service.MesEmailLoginCodeService;
+import com.jeesite.modules.mes.service.MesUserEmailVerifyConfigService;
 import com.jeesite.modules.sys.utils.UserUtils;
+import com.jeesite.modules.sys.entity.User;
 import org.apache.shiro.authc.AuthenticationException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
 
@@ -23,9 +33,17 @@ import org.springframework.web.bind.annotation.ResponseBody;
 @RequestMapping(value = "${adminPath}/sys/emailLogin")
 public class MesEmailLoginController extends BaseController {
 
+    private static final String PENDING_LOGIN_CODE = "MES_EMAIL_VERIFY_LOGIN_CODE";
+    private static final String PENDING_LOGIN_URL = "MES_EMAIL_VERIFY_LOGIN_URL";
+    private static final String PENDING_LOGIN_TIME = "MES_EMAIL_VERIFY_LOGIN_TIME";
+    private static final long PENDING_MILLIS = 5 * 60 * 1000L;
+
     @Autowired
     private MesEmailLoginCodeService mesEmailLoginCodeService;
 
+    @Autowired
+    private MesUserEmailVerifyConfigService mesUserEmailVerifyConfigService;
+
     @RequestMapping(value = "sendCode")
     @ResponseBody
     public String sendCode(String username, String email) {
@@ -42,15 +60,48 @@ public class MesEmailLoginController extends BaseController {
     @RequestMapping(value = "login")
     @ResponseBody
     public void login(HttpServletRequest request, HttpServletResponse response) {
-        if (!mesEmailLoginCodeService.isEmailLoginEnabled()) {
-            FormFilter.onLoginFailure(loginException("邮箱验证码登录未开启。"), request, response);
-            return;
-        }
         String secretKey = Global.getConfig("shiro.loginSubmit.secretKey");
         String username = decode(request.getParameter("username"), secretKey);
-        String password = decode(request.getParameter("password"), secretKey);
         String email = StringUtils.trim(request.getParameter("email"));
         String emailCode = StringUtils.trim(request.getParameter("emailCode"));
+        if (StringUtils.isNotBlank(email) || StringUtils.isNotBlank(emailCode)) {
+            legacyEmailLogin(request, response, username, email, emailCode);
+            return;
+        }
+
+        try {
+            UserUtils.getSubject().login(FormFilter.newToken(request, response));
+            if (!mesUserEmailVerifyConfigService.isEnabled(username)) {
+                FormFilter.onLoginSuccess(request, response);
+                return;
+            }
+
+            User user = UserUtils.getByLoginCode(username);
+            if (user == null || StringUtils.isBlank(user.getEmail())) {
+                UserUtils.getSubject().logout();
+                FormFilter.onLoginFailure(loginException("该账号未绑定邮箱,无法登录。"), request, response);
+                return;
+            }
+
+            UserUtils.getSubject().logout();
+            HttpSession session = request.getSession(true);
+            session.setAttribute(PENDING_LOGIN_CODE, username);
+            session.setAttribute(PENDING_LOGIN_URL, StringUtils.trim(request.getParameter("__url")));
+            session.setAttribute(PENDING_LOGIN_TIME, System.currentTimeMillis());
+            writeJson(response, needEmailVerifyResult(request));
+        } catch (AuthenticationException e) {
+            FormFilter.onLoginFailure(loginException("账号密码错误。"), request, response);
+        } catch (IOException e) {
+            FormFilter.onLoginFailure(loginException("登录失败。"), request, response);
+        }
+    }
+
+    private void legacyEmailLogin(HttpServletRequest request, HttpServletResponse response,
+                                  String username, String email, String emailCode) {
+        if (!mesEmailLoginCodeService.isEmailLoginEnabled()) {
+            FormFilter.onLoginFailure(loginException("邮箱验证码登录未开启。"), request, response);
+            return;
+        }
 
         String validateResult = mesEmailLoginCodeService.validateCode(username, email, emailCode);
         if (!MesEmailLoginCodeService.LOGIN_SUCCESS.equals(validateResult)) {
@@ -67,6 +118,99 @@ public class MesEmailLoginController extends BaseController {
         }
     }
 
+    @RequestMapping(value = "verify")
+    public String verify(HttpServletRequest request, Model model) {
+        String loginCode = pendingLoginCode(request);
+        if (StringUtils.isBlank(loginCode)) {
+            return "redirect:" + adminUrl(request, "/login");
+        }
+        String email = mesEmailLoginCodeService.getBoundEmail(loginCode);
+        model.addAttribute("loginCode", loginCode);
+        model.addAttribute("maskedEmail", mesEmailLoginCodeService.maskEmail(email));
+        return "themes/default/modules/sys/sysEmailVerify";
+    }
+
+    @RequestMapping(value = "sendPendingCode")
+    @ResponseBody
+    public String sendPendingCode(HttpServletRequest request) {
+        String loginCode = pendingLoginCode(request);
+        if (StringUtils.isBlank(loginCode)) {
+            return renderResult(Global.FALSE, "登录状态已失效,请重新登录。");
+        }
+        String message = mesEmailLoginCodeService.sendCodeToBoundEmail(loginCode);
+        if (StringUtils.isNotBlank(message)) {
+            return renderResult(Global.FALSE, message);
+        }
+        return renderResult(Global.TRUE, "验证码已发送,5分钟内有效。");
+    }
+
+    @RequestMapping(value = "verifyCode")
+    @ResponseBody
+    public void verifyCode(String emailCode, HttpServletRequest request, HttpServletResponse response) {
+        String loginCode = pendingLoginCode(request);
+        if (StringUtils.isBlank(loginCode)) {
+            FormFilter.onLoginFailure(loginException("登录状态已失效,请重新登录。"), request, response);
+            return;
+        }
+        String email = mesEmailLoginCodeService.getBoundEmail(loginCode);
+        String validateResult = mesEmailLoginCodeService.validateCode(loginCode, email, emailCode);
+        if (!MesEmailLoginCodeService.LOGIN_SUCCESS.equals(validateResult)) {
+            FormFilter.onLoginFailure(loginException(messageOf(validateResult)), request, response);
+            return;
+        }
+        try {
+            UserUtils.getSubject().login(new FormToken(loginCode, true, request));
+            mesEmailLoginCodeService.markLatestCodeUsed(loginCode, email);
+            String url = StringUtils.trim((String) request.getSession().getAttribute(PENDING_LOGIN_URL));
+            request.getSession().removeAttribute(PENDING_LOGIN_CODE);
+            request.getSession().removeAttribute(PENDING_LOGIN_URL);
+            request.getSession().removeAttribute(PENDING_LOGIN_TIME);
+            writeJson(response, loginSuccessResult(request, url));
+        } catch (AuthenticationException e) {
+            FormFilter.onLoginFailure(loginException("登录失败。"), request, response);
+        } catch (IOException e) {
+            FormFilter.onLoginFailure(loginException("登录失败。"), request, response);
+        }
+    }
+
+    private String pendingLoginCode(HttpServletRequest request) {
+        Object pendingTime = request.getSession().getAttribute(PENDING_LOGIN_TIME);
+        if (!(pendingTime instanceof Long) || System.currentTimeMillis() - (Long) pendingTime > PENDING_MILLIS) {
+            request.getSession().removeAttribute(PENDING_LOGIN_CODE);
+            request.getSession().removeAttribute(PENDING_LOGIN_URL);
+            request.getSession().removeAttribute(PENDING_LOGIN_TIME);
+            return null;
+        }
+        Object loginCode = request.getSession().getAttribute(PENDING_LOGIN_CODE);
+        return loginCode == null ? null : StringUtils.trim(loginCode.toString());
+    }
+
+    private Map<String, Object> needEmailVerifyResult(HttpServletRequest request) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("result", Global.TRUE);
+        result.put("needEmailVerify", true);
+        result.put("__url", adminUrl(request, "/sys/emailLogin/verify"));
+        result.put("message", "请完成邮箱验证码验证。");
+        return result;
+    }
+
+    private Map<String, Object> loginSuccessResult(HttpServletRequest request, String url) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("result", Global.TRUE);
+        result.put("__url", StringUtils.isBlank(url) ? adminUrl(request, "/index") : url);
+        result.put("message", "登录成功。");
+        return result;
+    }
+
+    private String adminUrl(HttpServletRequest request, String path) {
+        return request.getContextPath() + Global.getAdminPath() + path;
+    }
+
+    private void writeJson(HttpServletResponse response, Map<String, Object> result) throws IOException {
+        response.setContentType("application/json;charset=UTF-8");
+        response.getWriter().write(JSON.toJSONString(result));
+    }
+
     private String decode(String value, String secretKey) {
         if (StringUtils.isBlank(value) || StringUtils.isBlank(secretKey)) {
             return value;

+ 47 - 0
src/main/java/com/jeesite/modules/mes/web/MesEmployeeController.java

@@ -1,8 +1,12 @@
 package com.jeesite.modules.mes.web;
 
 import com.jeesite.common.config.Global;
+import com.jeesite.common.collect.ListUtils;
 import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.DateUtils;
 import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.utils.excel.ExcelExport;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
 import com.jeesite.common.web.BaseController;
 import com.jeesite.modules.mes.entity.MesEmployee;
 import com.jeesite.modules.mes.service.MesEmployeeService;
@@ -16,6 +20,7 @@ import org.springframework.web.bind.annotation.ModelAttribute;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -72,6 +77,48 @@ public class MesEmployeeController extends BaseController {
 		return renderResult(Global.TRUE, text("保存员工信息成功!"));
 	}
 
+	/**
+	 * 导出员工信息数据
+	 */
+	@RequiresPermissions("mes:mesEmployee:view")
+	@RequestMapping(value = "exportData")
+	public void exportData(MesEmployee mesEmployee, HttpServletResponse response) {
+		List<MesEmployee> list = mesEmployeeService.findList(mesEmployee);
+		String fileName = "\u5458\u5de5\u4fe1\u606f\u6570\u636e" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+		try (ExcelExport ee = new ExcelExport("\u5458\u5de5\u4fe1\u606f\u6570\u636e", MesEmployee.class)) {
+			ee.setDataList(list).write(response, fileName);
+		}
+	}
+
+	/**
+	 * 下载员工信息导入模板
+	 */
+	@RequiresPermissions("mes:mesEmployee:view")
+	@RequestMapping(value = "importTemplate")
+	public void importTemplate(HttpServletResponse response) {
+		MesEmployee mesEmployee = new MesEmployee();
+		List<MesEmployee> list = ListUtils.newArrayList(mesEmployee);
+		String fileName = "\u5458\u5de5\u4fe1\u606f\u5bfc\u5165\u6a21\u677f.xlsx";
+		try (ExcelExport ee = new ExcelExport("\u5458\u5de5\u4fe1\u606f\u6570\u636e", MesEmployee.class, Type.IMPORT)) {
+			ee.setDataList(list).write(response, fileName);
+		}
+	}
+
+	/**
+	 * 导入员工信息数据
+	 */
+	@ResponseBody
+	@RequiresPermissions("mes:mesEmployee:edit")
+	@PostMapping(value = "importData")
+	public String importData(MultipartFile file) {
+		try {
+			String message = mesEmployeeService.importData(file);
+			return renderResult(Global.TRUE, "posfull:" + message);
+		} catch (Exception ex) {
+			return renderResult(Global.FALSE, "posfull:" + ex.getMessage());
+		}
+	}
+
 	@RequiresPermissions("mes:mesEmployee:edit")
 	@RequestMapping(value = "disable")
 	@ResponseBody

+ 123 - 1
src/main/java/com/jeesite/modules/mes/web/MesProductController.java

@@ -865,6 +865,32 @@ public class MesProductController extends BaseController {
 		MesDevice mesDevice = new MesDevice();
 		Long deviceNum = mesDeviceService.findCount(mesDevice);
 		data.put("devices", deviceNum);
+		List<MesDevice> deviceList = mesDeviceService.findList(new MesDevice());
+		int onlineDeviceCount = 0;
+		int offlineDeviceCount = 0;
+		List<Map<String, Object>> deviceStatusRows = new ArrayList<>();
+		if (deviceList != null) {
+			for (MesDevice device : deviceList) {
+				boolean online = device.getOnline() != null && device.getOnline() == 0;
+				if (online) {
+					onlineDeviceCount++;
+				} else {
+					offlineDeviceCount++;
+				}
+				if (deviceStatusRows.size() < 6) {
+					Map<String, Object> item = MapUtils.newHashMap();
+					item.put("name", safeText(device.getTreeName(), safeText(device.getSn(), "-")));
+					item.put("sn", safeText(device.getSn(), "-"));
+					item.put("lineSn", safeText(device.getLineSn(), "-"));
+					item.put("online", online);
+					deviceStatusRows.add(item);
+				}
+			}
+		}
+		data.put("deviceTotal", deviceNum);
+		data.put("onlineDevices", onlineDeviceCount);
+		data.put("offlineDevices", offlineDeviceCount);
+		data.put("deviceStatusRows", deviceStatusRows);
 
 		MesProduct mesProduct1 = new MesProduct();
 		mesProduct1.setState("1");
@@ -994,6 +1020,11 @@ public class MesProductController extends BaseController {
 		data.put("date", dateList);
 		data.put("output", outputList);
 		data.put("todayOutput", outputList.isEmpty() ? 0L : outputList.get(outputList.size() - 1));
+		long weekOutput = 0L;
+		for (Long output : outputList) {
+			weekOutput += output == null ? 0L : output;
+		}
+		data.put("weekOutput", weekOutput);
 
 		Calendar monthCal = Calendar.getInstance();
 		monthCal.set(Calendar.DAY_OF_MONTH, 1);
@@ -1004,7 +1035,8 @@ public class MesProductController extends BaseController {
 		MesProduct monthProduct = new MesProduct();
 		monthProduct.getSqlMap().getWhere().and("create_date", QueryType.GTE, monthCal.getTime());
 		monthProduct.getSqlMap().getWhere().and("create_date", QueryType.LT, todayEnd);
-		data.put("monthOutput", mesProductService.findCount(monthProduct));
+		Long monthOutput = mesProductService.findCount(monthProduct);
+		data.put("monthOutput", monthOutput);
 
 		List<String> hourTimes = new ArrayList<>();
 		List<Long> perHourData = new ArrayList<>();
@@ -1033,6 +1065,40 @@ public class MesProductController extends BaseController {
 		data.put("quality1", quality1);
 		data.put("quality2", quality2);
 
+		MesProductCheck todayCheckQuery = new MesProductCheck();
+		todayCheckQuery.getSqlMap().getWhere().and("create_date", QueryType.GTE, todayStart);
+		todayCheckQuery.getSqlMap().getWhere().and("create_date", QueryType.LTE, todayEnd);
+		List<MesProductCheck> todayCheckList = mesProductCheckService.findList(todayCheckQuery);
+		int checkCount = todayCheckList == null ? 0 : todayCheckList.size();
+		int passCheckCount = 0;
+		int ngCheckCount = 0;
+		List<Map<String, Object>> qualityRows = new ArrayList<>();
+		if (todayCheckList != null) {
+			SimpleDateFormat checkTimeFormat = new SimpleDateFormat("HH:mm");
+			for (MesProductCheck check : todayCheckList) {
+				if ("1".equals(check.getState())) {
+					passCheckCount++;
+				} else {
+					ngCheckCount++;
+				}
+				if (qualityRows.size() < 6) {
+					Map<String, Object> row = MapUtils.newHashMap();
+					row.put("sn", safeText(check.getSn(), "-"));
+					row.put("oprno", safeText(check.getOprno(), "-"));
+					row.put("type", productCheckTypeLabel(check.getType()));
+					row.put("state", productCheckStatusLabel(check.getState()));
+					row.put("time", check.getCreateDate() == null ? "" : checkTimeFormat.format(check.getCreateDate()));
+					qualityRows.add(row);
+				}
+			}
+		}
+		double checkPassRate = checkCount > 0 ? Math.round(passCheckCount * 1000.0 / checkCount) / 10.0 : 0D;
+		data.put("checkCount", checkCount);
+		data.put("passCheckCount", passCheckCount);
+		data.put("ngCheckCount", ngCheckCount);
+		data.put("checkPassRate", checkPassRate);
+		data.put("qualityRows", qualityRows);
+
 		List<Long> warehouseOutList = new ArrayList<>();
 		for (int i = 6; i >= 0; i--) {
 			Calendar dayCal = (Calendar) calendar.clone();
@@ -1052,6 +1118,7 @@ public class MesProductController extends BaseController {
 		logger.info("查询到的工位当前工件数量: " + (processProductList != null ? processProductList.size() : 0));
 		for (MesProcessProduct pp : processProductList) {
 			Map<String, Object> item = MapUtils.newHashMap();
+			item.put("lineSn", pp.getProductCate());
 			item.put("oprno", pp.getOprno());
 			item.put("sn", pp.getSn());
 			workStationData.add(item);
@@ -1088,6 +1155,29 @@ public class MesProductController extends BaseController {
 		data.put("defectTitles", defectTitles);
 		data.put("defectCounts", defectCounts);
 
+		Map<String, Integer> repairTopMap = new LinkedHashMap<>();
+		if (repairList != null) {
+			for (MesRepair repair : repairList) {
+				String title = safeText(repair.getItemTitle(), safeText(repair.getRemark(), safeText(repair.getOprno(), "未分类")));
+				increase(repairTopMap, title);
+			}
+		}
+		Map<String, Integer> repairTop5 = top(repairTopMap, 5);
+		List<String> repairTopNames = new ArrayList<>();
+		List<Integer> repairTopCounts = new ArrayList<>();
+		List<Map<String, Object>> repairTopRows = new ArrayList<>();
+		for (Map.Entry<String, Integer> entry : repairTop5.entrySet()) {
+			repairTopNames.add(entry.getKey());
+			repairTopCounts.add(entry.getValue());
+			Map<String, Object> row = MapUtils.newHashMap();
+			row.put("name", entry.getKey());
+			row.put("value", entry.getValue());
+			repairTopRows.add(row);
+		}
+		data.put("repairTopNames", repairTopNames);
+		data.put("repairTopCounts", repairTopCounts);
+		data.put("repairTopRows", repairTopRows);
+
 		// 查询设备报警记录,按类型统计数量并排序
 		List<String> alarmTypes = new ArrayList<>();
 		List<Long> alarmCounts = new ArrayList<>();
@@ -1274,6 +1364,38 @@ public class MesProductController extends BaseController {
 		});
 		data.put("oprnoData", oprnoData);
 
+		Map<String, Object> energyData = mesAdw300DataService.getEnergyScreenData();
+		data.put("greenManufacturing", energyData);
+		Object energyDevices = energyData == null ? null : energyData.get("devices");
+		List<Map<String, Object>> greenMeters = new ArrayList<>();
+		if (energyDevices instanceof List) {
+			List<?> rows = (List<?>) energyDevices;
+			for (Object rowObj : rows) {
+				if (rowObj instanceof Map && greenMeters.size() < 2) {
+					greenMeters.add((Map<String, Object>) rowObj);
+				}
+			}
+		}
+		data.put("greenMeters", greenMeters);
+		data.put("greenTotalPower", energyData == null ? 0 : energyData.get("totalPower"));
+		data.put("greenTodayEnergy", energyData == null ? 0 : energyData.get("todayEnergy"));
+
+		List<Map<String, Object>> monitorRows = new ArrayList<>();
+		for (Map<String, Object> item : workStationData) {
+			if (monitorRows.size() >= 6) {
+				break;
+			}
+			Map<String, Object> row = MapUtils.newHashMap();
+			row.put("area", safeText(String.valueOf(item.get("lineSn")), "-"));
+			row.put("station", safeText(String.valueOf(item.get("oprno")), "-"));
+			row.put("content", safeText(String.valueOf(item.get("sn")), "-"));
+			monitorRows.add(row);
+		}
+		data.put("monitorRows", monitorRows);
+		data.put("orderDetails", new ArrayList<>());
+		data.put("completedWeek", weekOutput);
+		data.put("completedMonth", monthOutput);
+
 		resp.setData(data);
 		resp.setResult(Global.TRUE);
 		return resp;

+ 60 - 0
src/main/java/com/jeesite/modules/mes/web/MesUserEmailVerifyConfigController.java

@@ -0,0 +1,60 @@
+package com.jeesite.modules.mes.web;
+
+import com.alibaba.fastjson.JSON;
+import com.jeesite.common.config.Global;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.service.MesUserEmailVerifyConfigService;
+import com.jeesite.modules.sys.entity.User;
+import com.jeesite.modules.sys.utils.UserUtils;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 用户邮箱二次验证配置 Controller。
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/userEmailVerify")
+public class MesUserEmailVerifyConfigController extends BaseController {
+
+	@Autowired
+	private MesUserEmailVerifyConfigService mesUserEmailVerifyConfigService;
+
+	@RequestMapping(value = "status")
+	@ResponseBody
+	@RequiresPermissions("sys:empUser:view")
+	public String status(String userCode) {
+		User user = UserUtils.get(userCode);
+		if (user == null) {
+			return renderResult(Global.FALSE, "用户不存在。");
+		}
+		Map<String, Object> data = new HashMap<>();
+		data.put("result", Global.TRUE);
+		data.put("message", "查询成功。");
+		data.put("loginCode", user.getLoginCode());
+		data.put("email", StringUtils.trim(user.getEmail()));
+		data.put("enabled", mesUserEmailVerifyConfigService.isEnabled(user.getLoginCode()) ? "1" : "0");
+		return JSON.toJSONString(data);
+	}
+
+	@RequestMapping(value = "toggle")
+	@ResponseBody
+	@RequiresPermissions("sys:empUser:edit")
+	public String toggle(String userCode, String enabled) {
+		User user = UserUtils.get(userCode);
+		if (user == null) {
+			return renderResult(Global.FALSE, "用户不存在。");
+		}
+		if ("1".equals(enabled) && StringUtils.isBlank(user.getEmail())) {
+			return renderResult(Global.FALSE, "该用户未绑定邮箱,不能开启邮箱验证。");
+		}
+		mesUserEmailVerifyConfigService.saveEnabled(user.getLoginCode(), "1".equals(enabled));
+		return renderResult(Global.TRUE, "邮箱验证配置已更新。");
+	}
+}

+ 3 - 0
src/main/resources/config/application.yml

@@ -632,6 +632,9 @@ shiro:
     ${adminPath}/mes/mesLogin/login = anon
     ${adminPath}/sys/emailLogin/sendCode = anon
     ${adminPath}/sys/emailLogin/login = anon
+    ${adminPath}/sys/emailLogin/verify = anon
+    ${adminPath}/sys/emailLogin/sendPendingCode = anon
+    ${adminPath}/sys/emailLogin/verifyCode = anon
     ${adminPath}/mes/mesProductRecord/ghtime = anon
     ${adminPath}/mes/mesProductRecord/qmcheck = anon
     ${adminPath}/mes/mesProductRecord/qmresult = anon

+ 145 - 20
src/main/resources/mappings/modules/mes/MesAdw300DataDao.xml

@@ -11,10 +11,20 @@
 		ORDER BY device_sn
 	</select>
 
+	<select id="countByDeviceAndReportTimeRange" resultType="Long">
+		SELECT COUNT(1)
+		FROM mes_adw300_data
+		WHERE status = '0'
+		  AND device_sn = #{deviceSn}
+		  AND report_time &gt;= #{startTime}
+		  AND report_time &lt; #{endTime}
+	</select>
+
 	<select id="findLatestSnapshot" resultType="java.util.Map">
 		SELECT
 			a.device_sn AS deviceSn,
 			a.report_time AS reportTime,
+			DATE_FORMAT(a.report_time, '%Y-%m-%d %H:%i:%s') AS reportTimeText,
 			a.topic AS topic,
 			a.point_id AS pointId,
 			a.point_name AS pointName,
@@ -35,30 +45,72 @@
 
 	<select id="findTodayEnergy" resultType="java.util.Map">
 		SELECT
-			device_sn AS deviceSn,
-			ROUND(MAX(CAST(point_value AS DECIMAL(18, 4))) - MIN(CAST(point_value AS DECIMAL(18, 4))), 3) AS energy
-		FROM mes_adw300_data
-		WHERE status = '0'
-		  AND point_id = 28
-		  AND report_time &gt;= #{startTime}
-		  AND report_time &lt; #{endTime}
-		  AND point_value REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
-		GROUP BY device_sn
+			first_t.device_sn AS deviceSn,
+			ROUND(CAST(last_t.point_value AS DECIMAL(18, 4)) - CAST(first_t.point_value AS DECIMAL(18, 4)), 3) AS energy
+		FROM mes_adw300_data first_t
+		INNER JOIN (
+			SELECT device_sn, MIN(report_time) AS report_time
+			FROM mes_adw300_data
+			WHERE status = '0'
+			  AND point_id = 28
+			  AND report_time &gt;= #{startTime}
+			  AND report_time &lt; #{endTime}
+			  AND point_value REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
+			GROUP BY device_sn
+		) first_x ON first_x.device_sn = first_t.device_sn AND first_x.report_time = first_t.report_time
+		INNER JOIN (
+			SELECT device_sn, MAX(report_time) AS report_time
+			FROM mes_adw300_data
+			WHERE status = '0'
+			  AND point_id = 28
+			  AND report_time &gt;= #{startTime}
+			  AND report_time &lt; #{endTime}
+			  AND point_value REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
+			GROUP BY device_sn
+		) last_x ON last_x.device_sn = first_t.device_sn
+		INNER JOIN mes_adw300_data last_t
+		  ON last_t.device_sn = last_x.device_sn
+		 AND last_t.report_time = last_x.report_time
+		 AND last_t.point_id = 28
+		WHERE first_t.status = '0'
+		  AND first_t.point_id = 28
+		  AND first_t.point_value REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
 	</select>
 
 	<select id="findHourlyEnergy" resultType="java.util.Map">
 		SELECT
-			device_sn AS deviceSn,
-			DATE_FORMAT(report_time, '%H:00') AS hourLabel,
-			ROUND(MAX(CAST(point_value AS DECIMAL(18, 4))) - MIN(CAST(point_value AS DECIMAL(18, 4))), 3) AS energy
-		FROM mes_adw300_data
-		WHERE status = '0'
-		  AND point_id = 28
-		  AND report_time &gt;= #{startTime}
-		  AND report_time &lt; #{endTime}
-		  AND point_value REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
-		GROUP BY device_sn, DATE_FORMAT(report_time, '%H:00')
-		ORDER BY hourLabel, device_sn
+			first_t.device_sn AS deviceSn,
+			DATE_FORMAT(first_t.report_time, '%H:00') AS hourLabel,
+			ROUND(CAST(last_t.point_value AS DECIMAL(18, 4)) - CAST(first_t.point_value AS DECIMAL(18, 4)), 3) AS energy
+		FROM mes_adw300_data first_t
+		INNER JOIN (
+			SELECT device_sn, DATE_FORMAT(report_time, '%H:00') AS hour_label, MIN(report_time) AS report_time
+			FROM mes_adw300_data
+			WHERE status = '0'
+			  AND point_id = 28
+			  AND report_time &gt;= #{startTime}
+			  AND report_time &lt; #{endTime}
+			  AND point_value REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
+			GROUP BY device_sn, DATE_FORMAT(report_time, '%H:00')
+		) first_x ON first_x.device_sn = first_t.device_sn AND first_x.report_time = first_t.report_time
+		INNER JOIN (
+			SELECT device_sn, DATE_FORMAT(report_time, '%H:00') AS hour_label, MAX(report_time) AS report_time
+			FROM mes_adw300_data
+			WHERE status = '0'
+			  AND point_id = 28
+			  AND report_time &gt;= #{startTime}
+			  AND report_time &lt; #{endTime}
+			  AND point_value REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
+			GROUP BY device_sn, DATE_FORMAT(report_time, '%H:00')
+		) last_x ON last_x.device_sn = first_t.device_sn AND last_x.hour_label = first_x.hour_label
+		INNER JOIN mes_adw300_data last_t
+		  ON last_t.device_sn = last_x.device_sn
+		 AND last_t.report_time = last_x.report_time
+		 AND last_t.point_id = 28
+		WHERE first_t.status = '0'
+		  AND first_t.point_id = 28
+		  AND first_t.point_value REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
+		ORDER BY hourLabel, deviceSn
 	</select>
 
 	<!-- 查询数据
@@ -71,4 +123,77 @@
 		ORDER BY ${sqlMap.order.toSql()}
 	</select> -->
 
+	<select id="findTodayEnergyByPower" resultType="java.util.Map">
+		SELECT
+			deviceSn,
+			ROUND(SUM(energy), 3) AS energy
+		FROM (
+			SELECT
+				t.device_sn AS deviceSn,
+				ABS(CAST(t.point_value AS DECIMAL(18, 4))) *
+				LEAST(IFNULL(TIMESTAMPDIFF(SECOND, t.report_time, next_t.next_time), 0), 600) / 3600 AS energy
+			FROM mes_adw300_data t
+			LEFT JOIN (
+				SELECT
+					a.id,
+					MIN(b.report_time) AS next_time
+				FROM mes_adw300_data a
+				INNER JOIN mes_adw300_data b
+				  ON b.status = '0'
+				 AND b.device_sn = a.device_sn
+				 AND b.point_id = a.point_id
+				 AND b.report_time &gt; a.report_time
+				WHERE a.status = '0'
+				  AND a.point_id = 13
+				  AND a.report_time &gt;= #{startTime}
+				  AND a.report_time &lt; #{endTime}
+				GROUP BY a.id
+			) next_t ON next_t.id = t.id
+			WHERE t.status = '0'
+			  AND t.point_id = 13
+			  AND t.report_time &gt;= #{startTime}
+			  AND t.report_time &lt; #{endTime}
+			  AND t.point_value REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
+		) x
+		GROUP BY deviceSn
+	</select>
+
+	<select id="findHourlyEnergyByPower" resultType="java.util.Map">
+		SELECT
+			deviceSn,
+			hourLabel,
+			ROUND(SUM(energy), 3) AS energy
+		FROM (
+			SELECT
+				t.device_sn AS deviceSn,
+				DATE_FORMAT(t.report_time, '%H:00') AS hourLabel,
+				ABS(CAST(t.point_value AS DECIMAL(18, 4))) *
+				LEAST(IFNULL(TIMESTAMPDIFF(SECOND, t.report_time, next_t.next_time), 0), 600) / 3600 AS energy
+			FROM mes_adw300_data t
+			LEFT JOIN (
+				SELECT
+					a.id,
+					MIN(b.report_time) AS next_time
+				FROM mes_adw300_data a
+				INNER JOIN mes_adw300_data b
+				  ON b.status = '0'
+				 AND b.device_sn = a.device_sn
+				 AND b.point_id = a.point_id
+				 AND b.report_time &gt; a.report_time
+				WHERE a.status = '0'
+				  AND a.point_id = 13
+				  AND a.report_time &gt;= #{startTime}
+				  AND a.report_time &lt; #{endTime}
+				GROUP BY a.id
+			) next_t ON next_t.id = t.id
+			WHERE t.status = '0'
+			  AND t.point_id = 13
+			  AND t.report_time &gt;= #{startTime}
+			  AND t.report_time &lt; #{endTime}
+			  AND t.point_value REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
+		) x
+		GROUP BY deviceSn, hourLabel
+		ORDER BY hourLabel, deviceSn
+	</select>
+
 </mapper>

+ 19 - 0
src/main/resources/mappings/modules/mes/MesUserEmailVerifyConfigDao.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesite.modules.mes.dao.MesUserEmailVerifyConfigDao">
+
+	<select id="getByLoginCode" resultType="com.jeesite.modules.mes.entity.MesUserEmailVerifyConfig">
+		SELECT
+			id,
+			login_code AS loginCode,
+			enabled,
+			create_by AS createBy,
+			create_date AS createDate,
+			update_by AS updateBy,
+			update_date AS updateDate,
+			remarks
+		FROM mes_user_email_verify_config
+		WHERE login_code = #{loginCode}
+		LIMIT 1
+	</select>
+</mapper>

+ 5 - 59
src/main/resources/static/modules/sys/sysLogin.js

@@ -6,39 +6,7 @@ try {
 }
 
 $(function () {
-    var waitSeconds = 60;
-
-    function isEmail(value) {
-        return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test($.trim(value || ''));
-    }
-
-    function refreshEmailCodeButton() {
-        var button = $('#sendEmailLoginCode');
-        if (button.length === 0 || button.data('counting') === true) {
-            return;
-        }
-        button.prop('disabled', !isEmail($('#email').val()));
-    }
-
-    function countDown(button) {
-        $(button).data('counting', true);
-        if (waitSeconds === 0) {
-            button.removeAttribute('disabled');
-            button.value = '获取验证码';
-            waitSeconds = 60;
-            $(button).data('counting', false);
-            refreshEmailCodeButton();
-        } else {
-            button.setAttribute('disabled', true);
-            button.value = '重新发送(' + waitSeconds + ')';
-            waitSeconds--;
-            setTimeout(function () {
-                countDown(button);
-            }, 1000);
-        }
-    }
-
-    $('#username, #password, #email, #emailCode').on('focus blur', function () {
+    $('#username, #password').on('focus blur', function () {
         var input = this;
         setTimeout(function () {
             var borderColor = $(input).css('borderColor');
@@ -48,32 +16,6 @@ $(function () {
         }, 100);
     }).blur();
 
-    $('#email').on('input blur', refreshEmailCodeButton);
-    refreshEmailCodeButton();
-
-    $('#sendEmailLoginCode').click(function () {
-        var username = $('#username').val();
-        if (!username) {
-            js.showMessage('请先填写登录账号。');
-            $('#username').focus();
-            return;
-        }
-        var email = $('#email').val();
-        if (!isEmail(email)) {
-            js.showMessage('请填写正确的绑定邮箱。');
-            $('#email').focus();
-            return;
-        }
-        var button = this;
-        js.ajaxSubmit(ctxAdmin + '/sys/emailLogin/sendCode', {username: username, email: email}, function (data) {
-            js.showMessage(data.message);
-            if (data.result === 'true') {
-                countDown(button);
-                $('#emailCode').focus();
-            }
-        });
-    });
-
     $('#loginForm').validate({
         ignore: ':hidden',
         submitHandler: function (form) {
@@ -93,6 +35,10 @@ $(function () {
                 }
             }
             js.ajaxSubmit(action, data, function (data) {
+                if (data.needEmailVerify === true || data.needEmailVerify === 'true') {
+                    location = data.__url;
+                    return;
+                }
                 if (data.isValidCodeLogin === 1) {
                     $('#isValidCodeLogin').show();
                     $('#validCodeRefresh').click();

二进制
src/main/resources/static/screen/imgs/screen4FactoryBg.png


文件差异内容过多而无法显示
+ 642 - 1233
src/main/resources/views/modules/mes/equipment/mesScreen5.html


+ 62 - 20
src/main/resources/views/modules/mes/mesDeviceOeeScreen.html

@@ -205,9 +205,9 @@
 		.oee-rank-head,
 		.oee-rank-row {
 			display: grid;
-			grid-template-columns: 38px 1fr 70px;
+			grid-template-columns: 38px minmax(0, 1fr) 70px 78px;
 			align-items: center;
-			gap: 10px;
+			gap: 8px;
 		}
 		.oee-rank-head {
 			height: 28px;
@@ -284,7 +284,8 @@
 		.oee-device-grid {
 			display: grid;
 			grid-template-columns: repeat(auto-fit, minmax(245px, 1fr));
-			grid-auto-rows: minmax(238px, 1fr);
+			grid-auto-rows: auto;
+			align-content: start;
 			gap: 12px;
 			min-height: 0;
 			overflow-y: auto;
@@ -294,10 +295,12 @@
 		}
 		.oee-card {
 			display: grid;
-			grid-template-rows: 34px minmax(88px, 1fr) 74px;
+			grid-template-rows: 34px minmax(102px, 1fr) auto;
+			align-content: start;
+			gap: 8px;
 			padding: 10px;
 			min-width: 0;
-			min-height: 0;
+			min-height: 214px;
 		}
 		.oee-card-head {
 			display: grid;
@@ -353,20 +356,20 @@
 		}
 		.oee-device-visual {
 			display: grid;
-			grid-template-columns: minmax(0, 1fr) 82px;
+			grid-template-columns: minmax(0, 1fr) clamp(66px, 4.5vw, 82px);
 			align-items: center;
 			gap: 8px;
-			min-height: 0;
+			min-height: 98px;
 		}
 		.oee-device-visual img {
 			width: 100%;
-			height: clamp(88px, 12vh, 150px);
+			height: clamp(94px, 12vh, 132px);
 			object-fit: contain;
 			filter: drop-shadow(0 0 18px rgba(61, 219, 255, .32));
 		}
 		.oee-mini-gauge {
-			width: 78px;
-			height: 78px;
+			width: clamp(64px, 4.3vw, 78px);
+			height: clamp(64px, 4.3vw, 78px);
 			border-radius: 50%;
 			display: flex;
 			align-items: center;
@@ -386,12 +389,13 @@
 			z-index: 1;
 			color: #eaffff;
 			font-family: Impact, "Arial Narrow", sans-serif;
-			font-size: 19px;
+			font-size: clamp(15px, 1vw, 19px);
 		}
 		.oee-card-foot {
 			display: grid;
 			grid-template-columns: 1fr 1fr;
-			gap: 8px;
+			row-gap: 6px;
+			column-gap: 8px;
 			padding-top: 8px;
 			border-top: 1px solid rgba(79, 184, 238, .18);
 			min-width: 0;
@@ -402,7 +406,7 @@
 		.oee-meta-label {
 			color: #7fb9cf;
 			font-size: 12px;
-			line-height: 18px;
+			line-height: 16px;
 		}
 		.oee-meta-value {
 			overflow: hidden;
@@ -410,7 +414,7 @@
 			white-space: nowrap;
 			color: #e3f8ff;
 			font-size: 14px;
-			line-height: 22px;
+			line-height: 20px;
 		}
 		.oee-empty {
 			height: 100%;
@@ -423,11 +427,20 @@
 			font-size: 18px;
 		}
 		@media (max-aspect-ratio: 16/10) {
-			.oee-device-grid {
-				grid-auto-rows: minmax(210px, 1fr);
-			}
 			.oee-card {
-				grid-template-rows: 34px minmax(76px, 1fr) 70px;
+				grid-template-rows: 30px minmax(86px, 1fr) auto;
+				gap: 6px;
+				padding: 9px;
+				min-height: 196px;
+			}
+			.oee-device-visual {
+				min-height: 84px;
+			}
+			.oee-device-visual img {
+				height: clamp(80px, 10vh, 112px);
+			}
+			.oee-card-foot {
+				row-gap: 4px;
 			}
 		}
 	</style>
@@ -470,6 +483,7 @@
 							<div>排名</div>
 							<div>设备名称</div>
 							<div>开动率</div>
+							<div>开动时间</div>
 						</div>
 						<div class="oee-rank-list" id="rankList"></div>
 					</div>
@@ -506,6 +520,32 @@
 			.replace(/'/g, '&#39;');
 	}
 
+	function formatOpenMinutes(value) {
+		var minutes = Math.max(0, Math.round(safeNumber(value)));
+		var hours = Math.floor(minutes / 60);
+		var restMinutes = minutes % 60;
+		return hours > 0 ? hours + 'h' + restMinutes + 'm' : restMinutes + 'm';
+	}
+
+	function normalizeImageUrl(value, fallbackUrl) {
+		var url = $.trim(String(value || ''));
+		if (!url) {
+			return fallbackUrl;
+		}
+		url = url.replace(/\\/g, '/');
+		var userfilesIndex = url.indexOf('/userfiles/');
+		if (userfilesIndex >= 0 && !/^https?:\/\//i.test(url)) {
+			return '${ctxPath}' + url.substring(userfilesIndex);
+		}
+		if (url.indexOf('userfiles/') === 0) {
+			return '${ctxPath}/' + url;
+		}
+		if (/^https?:\/\//i.test(url) || url.charAt(0) === '/') {
+			return url;
+		}
+		return '${ctxPath}/' + url;
+	}
+
 	function renderStatusChart(statusList) {
 		var chartData = (statusList || []).map(function(item) {
 			return {
@@ -558,6 +598,7 @@
 				+ '<div><span class="oee-rank-no">' + (index + 1) + '</span></div>'
 				+ '<div class="oee-rank-name" title="' + escapeHtml(item.sn) + '">' + escapeHtml(item.name) + '</div>'
 				+ '<div>' + safeNumber(item.openRate).toFixed(1) + '%</div>'
+				+ '<div>' + formatOpenMinutes(item.openMinutes) + '</div>'
 				+ '</div>';
 		});
 		$('#rankList').html(html || '<div class="oee-empty">暂无排行数据</div>');
@@ -569,14 +610,15 @@
 		(list || []).forEach(function(item) {
 			var rate = safeNumber(item.openRate);
 			var statusClass = 'oee-status-' + escapeHtml(item.statusCode || 'unknown');
-			var imageUrl = item.imagePath || ('${ctxStatic}/screen/imgs/small' + safeNumber(item.imageIndex || 1) + '.png');
+			var fallbackUrl = '${ctxStatic}/screen/imgs/small' + safeNumber(item.imageIndex || 1) + '.png';
+			var imageUrl = normalizeImageUrl(item.imagePath, fallbackUrl);
 			html += '<div class="oee-card">'
 				+ '<div class="oee-card-head">'
 				+ '<div class="oee-device-name" title="' + escapeHtml(item.name) + '">' + escapeHtml(item.name) + '</div>'
 				+ '<div class="oee-status-tag ' + statusClass + '">' + escapeHtml(item.statusName) + '</div>'
 				+ '</div>'
 				+ '<div class="oee-device-visual">'
-				+ '<img src="' + escapeHtml(imageUrl) + '" alt="">'
+				+ '<img src="' + escapeHtml(imageUrl) + '" data-fallback="' + escapeHtml(fallbackUrl) + '" onerror="this.onerror=null;this.src=this.getAttribute(\'data-fallback\');" alt="">'
 				+ '<div class="oee-mini-gauge" style="--rate:' + Math.max(0, Math.min(100, rate)) + '%"><span>' + rate.toFixed(1) + '%</span></div>'
 				+ '</div>'
 				+ '<div class="oee-card-foot">'

+ 58 - 0
src/main/resources/views/modules/mes/mesEmployeeList.html

@@ -7,7 +7,9 @@
 			</div>
 			<div class="box-tools pull-right">
 				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+				<a href="#" class="btn btn-default" id="btnExport"><i class="glyphicon glyphicon-export"></i> ${text('导出')}</a>
 				<% if(hasPermi('mes:mesEmployee:edit')){ %>
+					<a href="#" class="btn btn-default" id="btnImport"><i class="glyphicon glyphicon-import"></i> ${text('导入')}</a>
 					<a href="${ctx}/mes/mesEmployee/form" class="btn btn-default btnTool" title="${text('新增员工')}"><i class="fa fa-plus"></i> ${text('新增')}</a>
 				<% } %>
 				<a href="#" class="btn btn-default" id="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
@@ -90,4 +92,60 @@ $('#dataGrid').dataGrid({
 	],
 	ajaxSuccess: function(){}
 });
+
+$('#btnExport').click(function(){
+	js.ajaxSubmitForm($('#searchForm'), {
+		url:'${ctx}/mes/mesEmployee/exportData',
+		downloadFile:true
+	});
+});
+$('#btnImport').click(function(){
+	js.layer.open({
+		type: 1,
+		area: ['400px'],
+		title: '${text("导入员工信息数据")}',
+		resize: false,
+		scrollbar: true,
+		content: js.template('importTpl'),
+		success: function(layero, index){
+			layero.find('input[type="checkbox"]').iCheck();
+		},
+		btn: ['<i class="fa fa-check"></i> ${text("导入")}',
+			'<i class="fa fa-remove"></i> ${text("关闭")}'],
+		btn1: function(index, layero){
+			var form = {
+				inputForm: layero.find('#inputForm'),
+				file: layero.find('#file').val()
+			};
+			if (form.file == '' || (!js.endWith(form.file, '.xls') && !js.endWith(form.file, '.xlsx'))){
+				js.showMessage("${text('文件不正确,请选择后缀为“xls”或“xlsx”的文件。')}", null, 'warning');
+				return false;
+			}
+			js.ajaxSubmitForm(form.inputForm, function(data){
+				js.showMessage(data.message);
+				if(data.result == Global.TRUE){
+					js.layer.closeAll();
+				}
+				page();
+			}, "json");
+			return true;
+		}
+	});
+});
 </script>
+<script id="importTpl" type="text/template">//<!--
+<form id="inputForm" action="${ctx}/mes/mesEmployee/importData" method="post" enctype="multipart/form-data"
+	class="form-horizontal mt20 mb10" style="overflow:auto;max-height:200px;">
+	<div class="row">
+		<div class="col-xs-12 col-xs-offset-1">
+			<input type="file" id="file" name="file" class="form-file"/>
+			<div class="mt10 pt5">
+				<a href="${ctx}/mes/mesEmployee/importTemplate" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> ${text('下载模板')}</a>
+			</div>
+			<font color="red" class="pull-left mt10">
+				${text('提示:仅允许导入“xls”或“xlsx”格式文件!')}
+			</font>
+		</div>
+	</div>
+</form>
+//--></script>

+ 19 - 6
src/main/resources/views/modules/mes/mesEnergyScreen.html

@@ -50,7 +50,8 @@
         .device-name { font-size: 20px; font-weight: 700; }
         .badge { padding: 4px 10px; border-radius: 4px; font-size: 13px; background: rgba(68, 224, 194, .18); color: #44e0c2; }
         .badge.offline { background: rgba(255, 103, 103, .14); color: #ff8686; }
-        .kv-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
+        .device-sn { margin-left: 10px; color: #8fb3ca; font-size: 13px; font-weight: 400; }
+        .kv-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
         .kv { padding: 10px; border-radius: 4px; background: rgba(255,255,255,.04); }
         .kv span { display: block; color: #92a9bb; font-size: 13px; margin-bottom: 5px; }
         .kv strong {
@@ -99,6 +100,18 @@
         var num = Number(value || 0);
         return Number.isFinite(num) ? num.toFixed(2) : '0.00';
     }
+    function deviceTitle(device, index) {
+        return device.deviceName || device.deviceSn || ('电表' + (index + 1));
+    }
+    function formatTime(value) {
+        if (!value) {
+            return '-';
+        }
+        if (typeof value === 'string') {
+            return value.replace('T', ' ').replace(/\.\d+.*$/, '');
+        }
+        return value;
+    }
     function updateClock() {
         var now = new Date();
         $('#dateText').text(now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0'));
@@ -107,16 +120,16 @@
     function renderDevices(devices) {
         var html = '';
         devices.forEach(function (d, index) {
+            var title = deviceTitle(d, index);
             html += '<div class="device-card">' +
-                '<div class="device-head"><div class="device-name">电表 ' + (index + 1) + ':' + (d.deviceSn || '-') + '</div>' +
+                '<div class="device-head"><div class="device-name">' + title + '<span class="device-sn">' + (d.deviceSn || '-') + '</span></div>' +
                 '<div class="badge ' + (d.online ? '' : 'offline') + '">' + (d.online ? '在线' : '离线') + '</div></div>' +
                 '<div class="kv-grid">' +
                 '<div class="kv"><span>当前功率</span><strong>' + fmt(d.power) + ' kW</strong></div>' +
                 '<div class="kv"><span>今日用电</span><strong>' + fmt(d.todayEnergy) + ' kWh</strong></div>' +
-                '<div class="kv"><span>总电能</span><strong>' + fmt(d.totalEnergy) + ' kWh</strong></div>' +
                 '<div class="kv"><span>A/B/C电压</span><strong class="small">' + fmt(d.ua) + ' / ' + fmt(d.ub) + ' / ' + fmt(d.uc) + ' V</strong></div>' +
                 '<div class="kv"><span>A/B/C电流</span><strong class="small">' + fmt(d.ia) + ' / ' + fmt(d.ib) + ' / ' + fmt(d.ic) + ' A</strong></div>' +
-                '<div class="kv"><span>上报时间</span><strong class="small">' + (d.reportTime || '-') + '</strong></div>' +
+                '<div class="kv"><span>上报时间</span><strong class="small">' + formatTime(d.reportTimeText || d.reportTime) + '</strong></div>' +
                 '</div></div>';
         });
         $('#deviceList').html(html || '<div class="device-card"><div class="device-name">暂无电表数据</div></div>');
@@ -124,7 +137,7 @@
     function renderTopMeters(devices) {
         for (var i = 0; i < 2; i++) {
             var device = devices[i] || {};
-            var title = device.deviceSn ? device.deviceSn : ('电表' + (i + 1));
+            var title = deviceTitle(device, i);
             $('#meter' + (i + 1) + 'PowerLabel').text(title + ' 当前功率');
             $('#meter' + (i + 1) + 'EnergyLabel').text(title + ' 今日用电');
             $('#meter' + (i + 1) + 'Power').text(fmt(device.power));
@@ -148,7 +161,7 @@
             color: ['#44e0c2'],
             tooltip: {},
             grid: { left: 45, right: 25, bottom: 34, top: 20 },
-            xAxis: { type: 'category', data: devices.map(function (d) { return d.deviceSn || '-'; }), axisLabel: { color: '#9fb6c7' }, axisLine: { lineStyle: { color: '#31536a' } } },
+            xAxis: { type: 'category', data: devices.map(function (d, index) { return deviceTitle(d, index); }), axisLabel: { color: '#9fb6c7' }, axisLine: { lineStyle: { color: '#31536a' } } },
             yAxis: { type: 'value', name: 'kW', nameTextStyle: { color: '#9fb6c7' }, axisLabel: { color: '#9fb6c7' }, splitLine: { lineStyle: { color: 'rgba(100,160,190,.16)' } } },
             series: [{ type: 'bar', barWidth: 36, data: devices.map(function (d) { return Number(d.power || 0); }) }]
         });

文件差异内容过多而无法显示
+ 769 - 1777
src/main/resources/views/modules/mes/mesScreen4.html


+ 2 - 1
src/main/resources/views/modules/mes/mesTemplateItemsList.html

@@ -77,6 +77,7 @@ $('#dataGrid').dataGrid({
 		// }},
 		// {header:'${text("父级编码")}', name:'pid', index:'a.pid', width:200, align:"center"},
 		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:150, align:"center"},
+		{header:'${text("工位名称")}', name:'oprnoName', index:'', width:150, align:"center"},
 		{header:'${text("工艺号")}', name:'craft', index:'a.craft', width:150, align:"center"},
 		// {header:'${text("报文类型")}', name:'cate', index:'a.cate', width:150, align:"center"},
 		{header:'${text("报文名称")}', name:'title', index:'a.title', width:150, align:"center"},
@@ -104,4 +105,4 @@ $('#dataGrid').dataGrid({
 		
 	}
 });
-</script>
+</script>

+ 29 - 3
src/main/resources/views/modules/sys/user/empUserList.html

@@ -155,6 +155,9 @@
 					//<% if(hasPermi('sys:empUser:resetpwd')){ %>
 					actions.push('<a href="${ctx}/sys/empUser/resetpwd?userCode='+row.userCode+'" class="btn btn-default btn-xs btnList" title="${text("用户密码重置")}" data-confirm="${text("确认要将该用户密码重置到初始状态吗?")}"><i class="fa fa-reply-all"></i> ${text("重置密码")}</a>&nbsp;');
 					//<% } %>
+					//<% if(hasPermi('sys:empUser:edit')){ %>
+					actions.push('<a href="javascript:" class="btn btn-default btn-xs js-email-verify" data-user-code="'+row.userCode+'" title="${text("是否开启邮箱验证")}"><i class="fa fa-envelope-o"></i> ${text("邮箱验证")}</a>&nbsp;');
+					//<% } %>
 					actions.push('<a href="${ctx}/mes/mesLogin/qrcode?userCode='+row.userCode+'" class="btn btn-default btn-xs btnList" title="${text("二维码")}"><i class="fa fa-fw fa-qrcode"></i> ${text("二维码")}</a>&nbsp;');
 					actions.push('</div>');
 					//<% } %>
@@ -168,9 +171,32 @@
 
 		}
 	});
+	$(document).on('click', '.js-email-verify', function(){
+		var userCode = $(this).data('userCode');
+		js.ajaxSubmit('${ctx}/mes/userEmailVerify/status', {userCode: userCode}, function(data){
+			if (data.result === 'false') {
+				js.showMessage(data.message);
+				return;
+			}
+			var enabled = data.enabled === '1';
+			var nextEnabled = enabled ? '0' : '1';
+			var msg = enabled
+				? '${text("当前已开启邮箱验证,确认关闭吗?")}'
+				: '${text("当前未开启邮箱验证,确认开启吗?")}';
+			if (!enabled && (!data.email || data.email === '')) {
+				js.showMessage('${text("该用户未绑定邮箱,不能开启邮箱验证。")}', null, 'warning');
+				return;
+			}
+			js.confirm(msg, function(){
+				js.ajaxSubmit('${ctx}/mes/userEmailVerify/toggle', {userCode: userCode, enabled: nextEnabled}, function(result){
+					js.showMessage(result.message);
+				});
+			});
+		});
+	});
 	$('#btnExport').click(function(){
 		js.ajaxSubmitForm($('#searchForm'), {
-			url:'${ctx}/sys/empUser/exportData',
+			url:'${ctx}/mes/empUserCustom/exportData',
 			downloadFile:true
 		});
 	});
@@ -209,7 +235,7 @@
 	});
 </script>
 <script id="importTpl" type="text/template">//<!--
-<form id="inputForm" action="${ctx}/sys/empUser/importData" method="post" enctype="multipart/form-data"
+<form id="inputForm" action="${ctx}/mes/empUserCustom/importData" method="post" enctype="multipart/form-data"
 	class="form-horizontal mt20 mb10" style="overflow:auto;max-height:200px;">
 	<div class="row">
 		<div class="col-xs-12 col-xs-offset-1">
@@ -217,7 +243,7 @@
 			<div class="mt10 pt5">
 				<#form:checkbox name="updateSupport" label="${text('是否更新已经存在的用户数据')}" class="form-control"
 					title="${text('如果用户编码已经存在,更新这条数据。')}"/> &nbsp;
-				<a href="${ctx}/sys/empUser/importTemplate" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> ${text('下载模板')}</a>
+				<a href="${ctx}/mes/empUserCustom/importTemplate" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> ${text('下载模板')}</a>
 			</div>
 			<font color="red" class="pull-left mt10">
 				${text('提示:仅允许导入“xls”或“xlsx”格式文件!')}

+ 80 - 0
src/main/resources/views/themes/default/modules/sys/sysEmailVerify.html

@@ -0,0 +1,80 @@
+<% layout('/layouts/default.html', {title: '邮箱验证', libs: ['validate'], bodyClass: 'login-page'}){ %>
+<link rel="stylesheet" href="${ctxStatic}/jquery-toastr/2.1/toastr.min.css?${_version}">
+<link rel="stylesheet" href="${ctxStatic}/modules/sys/sysLogin.css?${_version}">
+<style>.login-page{background-image:url("${ctxStatic}/screen/imgs/loginBg.png")!important;}</style>
+<div class="login-container">
+	<div class="login-logo-area">
+		<img class="logo-img" src="${ctxStatic}/screen/imgs/loginLogo.png" alt="logo">
+	</div>
+	<div class="login-box">
+		<div class="login-box-body">
+			<h4 class="login-box-msg">${text('邮箱验证')}</h4>
+			<div class="email-verify-info">
+				<div>${text('登录账号')}:${loginCode}</div>
+				<div>${text('接收邮箱')}:${maskedEmail}</div>
+			</div>
+			<form id="emailVerifyForm" method="post">
+				<div class="form-group has-feedback">
+					<div class="input-group email-code-group">
+						<span class="icon-shield form-control-feedback" title="${text('邮箱验证码')}"></span>
+						<input type="text" id="emailCode" name="emailCode" maxlength="6"
+							class="form-control required digits" data-msg-required="${text('请填写邮箱验证码')}"
+							data-msg-digits="${text('邮箱验证码为6位数字')}" data-rule-rangelength="6,6"
+							data-msg-rangelength="${text('邮箱验证码为6位数字')}" placeholder="${text('邮箱验证码')}" autocomplete="off" />
+						<span class="input-group-btn">
+							<input type="button" id="sendPendingEmailCode" value="${text('获取验证码')}" class="btn btn-flat"/>
+						</span>
+					</div>
+				</div>
+				<div class="form-group">
+					<button type="submit" class="btn btn-primary btn-block btn-flat" id="btnSubmit">${text('完成验证')}</button>
+				</div>
+			</form>
+		</div>
+		<div class="login-copyright">
+			&copy; ${@DateUtils.getYear()} ${@Global.getConfig('productName')} - Powered By <a href="#">${@Global.getConfig('companyName')}</a>
+		</div>
+	</div>
+</div>
+<% } %>
+<script src="${ctxStatic}/jquery-toastr/2.1/toastr.min.js?${_version}"></script>
+<script>
+$(function(){
+	var waitSeconds = 60;
+	function countDown(button) {
+		$(button).data('counting', true);
+		if (waitSeconds === 0) {
+			button.removeAttribute('disabled');
+			button.value = '${text("获取验证码")}';
+			waitSeconds = 60;
+			$(button).data('counting', false);
+		} else {
+			button.setAttribute('disabled', true);
+			button.value = '${text("重新发送")}(' + waitSeconds + ')';
+			waitSeconds--;
+			setTimeout(function(){ countDown(button); }, 1000);
+		}
+	}
+	$('#sendPendingEmailCode').click(function(){
+		var button = this;
+		js.ajaxSubmit('${ctxAdmin}/sys/emailLogin/sendPendingCode', {}, function(data){
+			js.showMessage(data.message);
+			if (data.result === 'true') {
+				countDown(button);
+				$('#emailCode').focus();
+			}
+		});
+	});
+	$('#emailVerifyForm').validate({
+		submitHandler: function(form) {
+			js.ajaxSubmit('${ctxAdmin}/sys/emailLogin/verifyCode', $(form).serializeArray(), function(data){
+				if (data.result === 'false') {
+					js.showMessage(data.message);
+					return;
+				}
+				location = data.__url && data.__url !== '' ? data.__url : '${ctxAdmin}/index';
+			}, 'json', true, '${text("正在验证,请稍候...")}');
+		}
+	});
+});
+</script>

+ 2 - 23
src/main/resources/views/themes/default/modules/sys/sysLogin.html

@@ -18,17 +18,16 @@
 <!--			<a class="switchSkin" href="${ctxAdmin}/switchSkin/skin-dark" title="${text('黑暗模式')}"><i class="fa fa-toggle-on"></i></a>-->
 		<% } %>
 		<% var isLoginByValidCode = @Global.getConfigToBoolean('user.loginByValidCode', 'true'); %>
-		<% var isEmailLoginEnabled = @Global.getConfig('mes.email.controller', '0') == '1'; %>
 		<% if(isLoginByValidCode){ %>
 			<ul id="loginTab" class="nav nav-tabs">
-				<li class="active"><a href="#tab-1" data-toggle="tab" action="${isEmailLoginEnabled ? ctxAdmin + '/sys/emailLogin/login' : ctxAdmin + '/login'}">${text('账号登录')}</a></li>
+				<li class="active"><a href="#tab-1" data-toggle="tab" action="${ctxAdmin}/sys/emailLogin/login">${text('账号登录')}</a></li>
 <!--				<li><a href="#tab-2" data-toggle="tab" action="${ctxPath}/account/loginByValidCode">${text('手机登录')}</a></li>-->
 				<% if(isNotBlank(@Global.getConfig('shiro.ldapUrl'))){ %>
 <!--					<li><a href="#tab-3" data-toggle="tab" action="${ctxAdmin}/login-ldap">${text('LDAP登录')}</a></li>-->
 				<% } %>
 			</ul>
 		<% } %>
-		<#form:form id="loginForm" model="${user!}" action="${isEmailLoginEnabled ? ctxAdmin + '/sys/emailLogin/login' : ctxAdmin + '/login'}" method="post" class="tab-content">
+		<#form:form id="loginForm" model="${user!}" action="${ctxAdmin}/sys/emailLogin/login" method="post" class="tab-content">
 			<% if(!isLoginByValidCode && isBlank(message!)){ %>
 				<h4 class="login-box-msg">${text('欢迎回来')+'!'}</h4>
 			<% }else if(isNotBlank(message!)){ %>
@@ -48,26 +47,6 @@
 				<#form:input type="password" name="password" class="form-control required"
 					data-msg-required="${text('请填写登录密码.')}" placeholder="${text('登录密码')}" autocomplete="off"/>
 			</div>
-			<% if(isEmailLoginEnabled){ %>
-			<div class="form-group has-feedback tab-pane tab-1 active email-login-row">
-				<span class="icon-envelope form-control-feedback" title="${text('绑定邮箱')}"></span>
-				<#form:input type="text" name="email" id="email" class="form-control required email"
-					data-msg-required="${text('请填写绑定邮箱')}" data-msg-email="${text('请填写正确的邮箱地址')}"
-					placeholder="${text('绑定邮箱')}" autocomplete="off"/>
-			</div>
-			<div class="form-group has-feedback tab-pane tab-1 active email-login-row">
-				<div class="input-group email-code-group">
-					<span class="icon-shield form-control-feedback" title="${text('邮箱验证码')}"></span>
-					<input type="text" id="emailCode" name="emailCode" maxlength="6"
-						class="form-control required digits" data-msg-required="${text('请填写邮箱验证码')}"
-						data-msg-digits="${text('邮箱验证码为6位数字')}" data-rule-rangelength="6,6"
-						data-msg-rangelength="${text('邮箱验证码为6位数字')}" placeholder="${text('邮箱验证码')}" autocomplete="off" />
-					<span class="input-group-btn">
-						<input type="button" id="sendEmailLoginCode" value="${text('获取验证码')}" class="btn btn-flat" disabled="disabled"/>
-					</span>
-				</div>
-			</div>
-			<% } %>
 			<% if(@Global.getConfigToBoolean('user.loginCodeCorpUnique', 'false')){ %>
 			<div class="form-group has-feedback">
 				<#form:treeselect id="switchCorpSelect" title="${text('登录租户')}" allowClear="false"