| New file |
| | |
| | | package com.zy.common.auth; |
| | | |
| | | import com.core.common.Cools; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.security.SecureRandom; |
| | | import java.util.Base64; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | @Component |
| | | public class MfaLoginTicketManager { |
| | | |
| | | private static final long EXPIRE_MILLIS = 5 * 60 * 1000L; |
| | | |
| | | private final SecureRandom secureRandom = new SecureRandom(); |
| | | private final ConcurrentHashMap<String, TicketHolder> holders = new ConcurrentHashMap<>(); |
| | | |
| | | public String create(Long userId) { |
| | | cleanup(); |
| | | String ticket; |
| | | do { |
| | | ticket = randomTicket(); |
| | | } while (holders.putIfAbsent(ticket, new TicketHolder(userId, System.currentTimeMillis() + EXPIRE_MILLIS)) != null); |
| | | return ticket; |
| | | } |
| | | |
| | | public Long getUserId(String ticket) { |
| | | if (Cools.isEmpty(ticket)) { |
| | | return null; |
| | | } |
| | | TicketHolder holder = holders.get(ticket); |
| | | if (holder == null) { |
| | | return null; |
| | | } |
| | | if (holder.expireAt < System.currentTimeMillis()) { |
| | | holders.remove(ticket); |
| | | return null; |
| | | } |
| | | return holder.userId; |
| | | } |
| | | |
| | | public void remove(String ticket) { |
| | | if (!Cools.isEmpty(ticket)) { |
| | | holders.remove(ticket); |
| | | } |
| | | } |
| | | |
| | | private void cleanup() { |
| | | long now = System.currentTimeMillis(); |
| | | for (Map.Entry<String, TicketHolder> entry : holders.entrySet()) { |
| | | if (entry.getValue().expireAt < now) { |
| | | holders.remove(entry.getKey()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private String randomTicket() { |
| | | byte[] bytes = new byte[24]; |
| | | secureRandom.nextBytes(bytes); |
| | | return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); |
| | | } |
| | | |
| | | private static final class TicketHolder { |
| | | private final Long userId; |
| | | private final long expireAt; |
| | | |
| | | private TicketHolder(Long userId, long expireAt) { |
| | | this.userId = userId; |
| | | this.expireAt = expireAt; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.common.auth; |
| | | |
| | | import com.core.common.Cools; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.security.SecureRandom; |
| | | import java.util.Base64; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | @Component |
| | | public class PasskeyChallengeManager { |
| | | |
| | | private static final long EXPIRE_MILLIS = 5 * 60 * 1000L; |
| | | |
| | | private final SecureRandom secureRandom = new SecureRandom(); |
| | | private final ConcurrentHashMap<String, ChallengeState> holders = new ConcurrentHashMap<>(); |
| | | |
| | | public ChallengeState createRegistration(Long userId, String origin, String rpId) { |
| | | return create(Purpose.REGISTRATION, userId, origin, rpId); |
| | | } |
| | | |
| | | public ChallengeState createAuthentication(Long userId, String origin, String rpId) { |
| | | return create(Purpose.AUTHENTICATION, userId, origin, rpId); |
| | | } |
| | | |
| | | public ChallengeState get(String ticket, Purpose purpose) { |
| | | if (Cools.isEmpty(ticket) || purpose == null) { |
| | | return null; |
| | | } |
| | | ChallengeState state = holders.get(ticket); |
| | | if (state == null) { |
| | | return null; |
| | | } |
| | | if (state.expireAt < System.currentTimeMillis() || state.purpose != purpose) { |
| | | holders.remove(ticket); |
| | | return null; |
| | | } |
| | | return state; |
| | | } |
| | | |
| | | public void remove(String ticket) { |
| | | if (!Cools.isEmpty(ticket)) { |
| | | holders.remove(ticket); |
| | | } |
| | | } |
| | | |
| | | private ChallengeState create(Purpose purpose, Long userId, String origin, String rpId) { |
| | | cleanup(); |
| | | ChallengeState state; |
| | | do { |
| | | state = new ChallengeState( |
| | | randomTicket(), |
| | | randomChallenge(), |
| | | purpose, |
| | | userId, |
| | | origin, |
| | | rpId, |
| | | System.currentTimeMillis() + EXPIRE_MILLIS |
| | | ); |
| | | } while (holders.putIfAbsent(state.ticket, state) != null); |
| | | return state; |
| | | } |
| | | |
| | | private void cleanup() { |
| | | long now = System.currentTimeMillis(); |
| | | for (Map.Entry<String, ChallengeState> entry : holders.entrySet()) { |
| | | if (entry.getValue().expireAt < now) { |
| | | holders.remove(entry.getKey()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private String randomTicket() { |
| | | byte[] bytes = new byte[24]; |
| | | secureRandom.nextBytes(bytes); |
| | | return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); |
| | | } |
| | | |
| | | private String randomChallenge() { |
| | | byte[] bytes = new byte[32]; |
| | | secureRandom.nextBytes(bytes); |
| | | return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); |
| | | } |
| | | |
| | | public enum Purpose { |
| | | REGISTRATION, |
| | | AUTHENTICATION |
| | | } |
| | | |
| | | public static final class ChallengeState { |
| | | private final String ticket; |
| | | private final String challenge; |
| | | private final Purpose purpose; |
| | | private final Long userId; |
| | | private final String origin; |
| | | private final String rpId; |
| | | private final long expireAt; |
| | | |
| | | private ChallengeState(String ticket, String challenge, Purpose purpose, Long userId, String origin, String rpId, long expireAt) { |
| | | this.ticket = ticket; |
| | | this.challenge = challenge; |
| | | this.purpose = purpose; |
| | | this.userId = userId; |
| | | this.origin = origin; |
| | | this.rpId = rpId; |
| | | this.expireAt = expireAt; |
| | | } |
| | | |
| | | public String getTicket() { |
| | | return ticket; |
| | | } |
| | | |
| | | public String getChallenge() { |
| | | return challenge; |
| | | } |
| | | |
| | | public Purpose getPurpose() { |
| | | return purpose; |
| | | } |
| | | |
| | | public Long getUserId() { |
| | | return userId; |
| | | } |
| | | |
| | | public String getOrigin() { |
| | | return origin; |
| | | } |
| | | |
| | | public String getRpId() { |
| | | return rpId; |
| | | } |
| | | |
| | | public long getExpireAt() { |
| | | return expireAt; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.common.utils; |
| | | |
| | | import com.core.common.Cools; |
| | | |
| | | import javax.crypto.Mac; |
| | | import javax.crypto.spec.SecretKeySpec; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.net.URLEncoder; |
| | | import java.nio.ByteBuffer; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.security.SecureRandom; |
| | | import java.util.Locale; |
| | | |
| | | public final class MfaTotpUtil { |
| | | |
| | | private static final char[] BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray(); |
| | | private static final SecureRandom SECURE_RANDOM = new SecureRandom(); |
| | | private static final int SECRET_SIZE = 20; |
| | | private static final int OTP_DIGITS = 6; |
| | | private static final int OTP_PERIOD_SECONDS = 30; |
| | | |
| | | private MfaTotpUtil() { |
| | | } |
| | | |
| | | public static String generateSecret() { |
| | | byte[] buffer = new byte[SECRET_SIZE]; |
| | | SECURE_RANDOM.nextBytes(buffer); |
| | | return encodeBase32(buffer); |
| | | } |
| | | |
| | | public static boolean verifyCode(String secret, String code, int window) { |
| | | if (Cools.isEmpty(secret, code)) { |
| | | return false; |
| | | } |
| | | String normalizedCode = String.valueOf(code).replaceAll("\\s+", ""); |
| | | if (!normalizedCode.matches("\\d{" + OTP_DIGITS + "}")) { |
| | | return false; |
| | | } |
| | | try { |
| | | long currentStep = System.currentTimeMillis() / 1000L / OTP_PERIOD_SECONDS; |
| | | for (int offset = -window; offset <= window; offset++) { |
| | | if (normalizedCode.equals(generateCode(secret, currentStep + offset))) { |
| | | return true; |
| | | } |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | public static String buildOtpAuthUri(String issuer, String account, String secret) { |
| | | String safeIssuer = Cools.isEmpty(issuer) ? "WCS" : issuer.trim(); |
| | | String safeAccount = Cools.isEmpty(account) ? "user" : account.trim(); |
| | | String label = urlEncode(safeIssuer + ":" + safeAccount); |
| | | return "otpauth://totp/" + label |
| | | + "?secret=" + secret |
| | | + "&issuer=" + urlEncode(safeIssuer) |
| | | + "&algorithm=SHA1&digits=" + OTP_DIGITS |
| | | + "&period=" + OTP_PERIOD_SECONDS; |
| | | } |
| | | |
| | | public static String maskSecret(String secret) { |
| | | if (Cools.isEmpty(secret)) { |
| | | return ""; |
| | | } |
| | | String value = String.valueOf(secret).trim(); |
| | | if (value.length() <= 8) { |
| | | return value; |
| | | } |
| | | return value.substring(0, 4) + "****" + value.substring(value.length() - 4); |
| | | } |
| | | |
| | | private static String generateCode(String secret, long step) { |
| | | try { |
| | | byte[] key = decodeBase32(secret); |
| | | byte[] data = ByteBuffer.allocate(8).putLong(step).array(); |
| | | Mac mac = Mac.getInstance("HmacSHA1"); |
| | | mac.init(new SecretKeySpec(key, "HmacSHA1")); |
| | | byte[] hash = mac.doFinal(data); |
| | | int offset = hash[hash.length - 1] & 0x0F; |
| | | int binary = ((hash[offset] & 0x7F) << 24) |
| | | | ((hash[offset + 1] & 0xFF) << 16) |
| | | | ((hash[offset + 2] & 0xFF) << 8) |
| | | | (hash[offset + 3] & 0xFF); |
| | | int otp = binary % (int) Math.pow(10, OTP_DIGITS); |
| | | return String.format(Locale.ROOT, "%0" + OTP_DIGITS + "d", otp); |
| | | } catch (Exception e) { |
| | | throw new IllegalStateException("generate totp code failed", e); |
| | | } |
| | | } |
| | | |
| | | private static String encodeBase32(byte[] data) { |
| | | StringBuilder builder = new StringBuilder((data.length * 8 + 4) / 5); |
| | | int buffer = 0; |
| | | int bitsLeft = 0; |
| | | for (byte datum : data) { |
| | | buffer = (buffer << 8) | (datum & 0xFF); |
| | | bitsLeft += 8; |
| | | while (bitsLeft >= 5) { |
| | | builder.append(BASE32_ALPHABET[(buffer >> (bitsLeft - 5)) & 0x1F]); |
| | | bitsLeft -= 5; |
| | | } |
| | | } |
| | | if (bitsLeft > 0) { |
| | | builder.append(BASE32_ALPHABET[(buffer << (5 - bitsLeft)) & 0x1F]); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private static byte[] decodeBase32(String value) { |
| | | String normalized = String.valueOf(value) |
| | | .trim() |
| | | .replace("=", "") |
| | | .replace(" ", "") |
| | | .replace("-", "") |
| | | .toUpperCase(Locale.ROOT); |
| | | ByteArrayOutputStream output = new ByteArrayOutputStream(); |
| | | int buffer = 0; |
| | | int bitsLeft = 0; |
| | | for (int i = 0; i < normalized.length(); i++) { |
| | | char current = normalized.charAt(i); |
| | | int index = indexOfBase32(current); |
| | | if (index < 0) { |
| | | throw new IllegalArgumentException("invalid base32 secret"); |
| | | } |
| | | buffer = (buffer << 5) | index; |
| | | bitsLeft += 5; |
| | | if (bitsLeft >= 8) { |
| | | output.write((buffer >> (bitsLeft - 8)) & 0xFF); |
| | | bitsLeft -= 8; |
| | | } |
| | | } |
| | | return output.toByteArray(); |
| | | } |
| | | |
| | | private static int indexOfBase32(char value) { |
| | | for (int i = 0; i < BASE32_ALPHABET.length; i++) { |
| | | if (BASE32_ALPHABET[i] == value) { |
| | | return i; |
| | | } |
| | | } |
| | | return -1; |
| | | } |
| | | |
| | | private static String urlEncode(String value) { |
| | | return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20"); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.common.utils; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.core.common.Cools; |
| | | |
| | | import jakarta.servlet.http.HttpServletRequest; |
| | | import java.nio.ByteBuffer; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.security.GeneralSecurityException; |
| | | import java.security.KeyFactory; |
| | | import java.security.MessageDigest; |
| | | import java.security.PublicKey; |
| | | import java.security.Signature; |
| | | import java.security.spec.MGF1ParameterSpec; |
| | | import java.security.spec.PSSParameterSpec; |
| | | import java.security.spec.X509EncodedKeySpec; |
| | | import java.util.ArrayList; |
| | | import java.util.Base64; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | |
| | | public final class PasskeyWebAuthnUtil { |
| | | |
| | | private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder(); |
| | | |
| | | private PasskeyWebAuthnUtil() { |
| | | } |
| | | |
| | | public static JSONObject parseClientData(String clientDataJsonBase64Url) { |
| | | if (Cools.isEmpty(clientDataJsonBase64Url)) { |
| | | throw new IllegalArgumentException("Missing clientDataJSON"); |
| | | } |
| | | String json = new String(decodeBase64Url(clientDataJsonBase64Url), StandardCharsets.UTF_8); |
| | | JSONObject clientData = JSON.parseObject(json); |
| | | if (clientData == null) { |
| | | throw new IllegalArgumentException("Invalid clientDataJSON"); |
| | | } |
| | | return clientData; |
| | | } |
| | | |
| | | public static void validateClientData(JSONObject clientData, String expectedType, String expectedChallenge, String expectedOrigin) { |
| | | if (clientData == null) { |
| | | throw new IllegalArgumentException("Missing clientData"); |
| | | } |
| | | if (!Cools.eq(expectedType, clientData.getString("type"))) { |
| | | throw new IllegalArgumentException("Unexpected WebAuthn type"); |
| | | } |
| | | if (!Cools.eq(expectedChallenge, clientData.getString("challenge"))) { |
| | | throw new IllegalArgumentException("Challenge mismatch"); |
| | | } |
| | | if (!Cools.eq(expectedOrigin, clientData.getString("origin"))) { |
| | | throw new IllegalArgumentException("Origin mismatch"); |
| | | } |
| | | } |
| | | |
| | | public static AuthenticatorData validateAuthenticatorData(String authenticatorDataBase64Url, String rpId, boolean requireUserVerification) throws GeneralSecurityException { |
| | | byte[] authenticatorData = decodeBase64Url(authenticatorDataBase64Url); |
| | | if (authenticatorData.length < 37) { |
| | | throw new GeneralSecurityException("Invalid authenticator data"); |
| | | } |
| | | byte[] expectedRpIdHash = sha256(rpId.getBytes(StandardCharsets.UTF_8)); |
| | | for (int i = 0; i < expectedRpIdHash.length; i++) { |
| | | if (authenticatorData[i] != expectedRpIdHash[i]) { |
| | | throw new GeneralSecurityException("RP ID hash mismatch"); |
| | | } |
| | | } |
| | | int flags = authenticatorData[32] & 0xFF; |
| | | if ((flags & 0x01) == 0) { |
| | | throw new GeneralSecurityException("User presence required"); |
| | | } |
| | | if (requireUserVerification && (flags & 0x04) == 0) { |
| | | throw new GeneralSecurityException("User verification required"); |
| | | } |
| | | long signCount = ByteBuffer.wrap(authenticatorData, 33, 4).getInt() & 0xFFFFFFFFL; |
| | | return new AuthenticatorData(authenticatorData, flags, signCount); |
| | | } |
| | | |
| | | public static void verifyAssertionSignature(String publicKeyBase64Url, |
| | | Integer algorithm, |
| | | String authenticatorDataBase64Url, |
| | | String clientDataJsonBase64Url, |
| | | String signatureBase64Url) throws GeneralSecurityException { |
| | | PublicKey publicKey = readPublicKey(publicKeyBase64Url, algorithm); |
| | | Signature verifier = createSignatureVerifier(publicKey, algorithm); |
| | | verifier.initVerify(publicKey); |
| | | verifier.update(decodeBase64Url(authenticatorDataBase64Url)); |
| | | verifier.update(sha256(decodeBase64Url(clientDataJsonBase64Url))); |
| | | if (!verifier.verify(decodeBase64Url(signatureBase64Url))) { |
| | | throw new GeneralSecurityException("Invalid passkey signature"); |
| | | } |
| | | } |
| | | |
| | | public static void ensurePublicKeyMaterial(String publicKeyBase64Url, Integer algorithm) throws GeneralSecurityException { |
| | | readPublicKey(publicKeyBase64Url, algorithm); |
| | | } |
| | | |
| | | public static byte[] decodeBase64Url(String value) { |
| | | if (Cools.isEmpty(value)) { |
| | | throw new IllegalArgumentException("Missing base64Url value"); |
| | | } |
| | | return URL_DECODER.decode(String.valueOf(value).trim()); |
| | | } |
| | | |
| | | public static String buildOrigin(HttpServletRequest request) { |
| | | String scheme = normalizeForwardedValue(request.getHeader("X-Forwarded-Proto")); |
| | | if (Cools.isEmpty(scheme)) { |
| | | scheme = request.getScheme(); |
| | | } |
| | | String host = resolveHost(request); |
| | | return scheme.toLowerCase(Locale.ROOT) + "://" + host; |
| | | } |
| | | |
| | | public static String buildRpId(HttpServletRequest request) { |
| | | String host = resolveHost(request); |
| | | if (host.startsWith("[")) { |
| | | int bracket = host.indexOf(']'); |
| | | return bracket > 0 ? host.substring(1, bracket).toLowerCase(Locale.ROOT) : host.toLowerCase(Locale.ROOT); |
| | | } |
| | | int colonIndex = host.indexOf(':'); |
| | | if (colonIndex >= 0) { |
| | | host = host.substring(0, colonIndex); |
| | | } |
| | | return host.toLowerCase(Locale.ROOT); |
| | | } |
| | | |
| | | public static boolean isSecureOriginAllowed(String origin, String rpId) { |
| | | if (Cools.isEmpty(origin) || Cools.isEmpty(rpId)) { |
| | | return false; |
| | | } |
| | | String lowerOrigin = origin.toLowerCase(Locale.ROOT); |
| | | String lowerRpId = rpId.toLowerCase(Locale.ROOT); |
| | | if ("localhost".equals(lowerRpId) |
| | | || "127.0.0.1".equals(lowerRpId) |
| | | || "::1".equals(lowerRpId) |
| | | || lowerRpId.endsWith(".localhost")) { |
| | | return true; |
| | | } |
| | | return lowerOrigin.startsWith("https://"); |
| | | } |
| | | |
| | | public static byte[] buildUserHandle(Long userId) { |
| | | return String.valueOf(userId).getBytes(StandardCharsets.UTF_8); |
| | | } |
| | | |
| | | private static PublicKey readPublicKey(String publicKeyBase64Url, Integer algorithm) throws GeneralSecurityException { |
| | | byte[] encoded = decodeBase64Url(publicKeyBase64Url); |
| | | X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded); |
| | | List<String> keyFactories = keyFactoriesForAlgorithm(algorithm); |
| | | GeneralSecurityException failure = null; |
| | | for (String keyFactoryName : keyFactories) { |
| | | try { |
| | | return KeyFactory.getInstance(keyFactoryName).generatePublic(keySpec); |
| | | } catch (GeneralSecurityException ex) { |
| | | failure = ex; |
| | | } |
| | | } |
| | | throw failure == null ? new GeneralSecurityException("Unsupported passkey algorithm") : failure; |
| | | } |
| | | |
| | | private static Signature createSignatureVerifier(PublicKey publicKey, Integer algorithm) throws GeneralSecurityException { |
| | | int value = algorithm == null ? Integer.MIN_VALUE : algorithm; |
| | | switch (value) { |
| | | case -7: |
| | | return Signature.getInstance("SHA256withECDSA"); |
| | | case -257: |
| | | return Signature.getInstance("SHA256withRSA"); |
| | | case -37: |
| | | Signature pss = Signature.getInstance("RSASSA-PSS"); |
| | | pss.setParameter(new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1)); |
| | | return pss; |
| | | case -8: |
| | | return Signature.getInstance("Ed25519"); |
| | | default: |
| | | if ("EC".equalsIgnoreCase(publicKey.getAlgorithm())) { |
| | | return Signature.getInstance("SHA256withECDSA"); |
| | | } |
| | | if ("RSA".equalsIgnoreCase(publicKey.getAlgorithm())) { |
| | | return Signature.getInstance("SHA256withRSA"); |
| | | } |
| | | if ("Ed25519".equalsIgnoreCase(publicKey.getAlgorithm()) || "EdDSA".equalsIgnoreCase(publicKey.getAlgorithm())) { |
| | | return Signature.getInstance("Ed25519"); |
| | | } |
| | | throw new GeneralSecurityException("Unsupported passkey signature algorithm"); |
| | | } |
| | | } |
| | | |
| | | private static List<String> keyFactoriesForAlgorithm(Integer algorithm) { |
| | | List<String> result = new ArrayList<>(); |
| | | int value = algorithm == null ? Integer.MIN_VALUE : algorithm; |
| | | switch (value) { |
| | | case -7: |
| | | result.add("EC"); |
| | | break; |
| | | case -257: |
| | | case -37: |
| | | result.add("RSA"); |
| | | break; |
| | | case -8: |
| | | result.add("Ed25519"); |
| | | result.add("EdDSA"); |
| | | break; |
| | | default: |
| | | result.add("EC"); |
| | | result.add("RSA"); |
| | | result.add("Ed25519"); |
| | | result.add("EdDSA"); |
| | | break; |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private static String resolveHost(HttpServletRequest request) { |
| | | String host = normalizeForwardedValue(request.getHeader("X-Forwarded-Host")); |
| | | if (Cools.isEmpty(host)) { |
| | | host = request.getServerName(); |
| | | int port = request.getServerPort(); |
| | | if (port > 0 && port != 80 && port != 443) { |
| | | host = host + ":" + port; |
| | | } |
| | | } |
| | | String port = normalizeForwardedValue(request.getHeader("X-Forwarded-Port")); |
| | | if (!Cools.isEmpty(port) && host.indexOf(':') < 0 && !host.startsWith("[")) { |
| | | host = host + ":" + port; |
| | | } |
| | | return host; |
| | | } |
| | | |
| | | private static String normalizeForwardedValue(String value) { |
| | | if (Cools.isEmpty(value)) { |
| | | return null; |
| | | } |
| | | String normalized = String.valueOf(value).trim(); |
| | | int commaIndex = normalized.indexOf(','); |
| | | if (commaIndex >= 0) { |
| | | normalized = normalized.substring(0, commaIndex).trim(); |
| | | } |
| | | return normalized; |
| | | } |
| | | |
| | | private static byte[] sha256(byte[] data) throws GeneralSecurityException { |
| | | return MessageDigest.getInstance("SHA-256").digest(data); |
| | | } |
| | | |
| | | public static final class AuthenticatorData { |
| | | private final byte[] raw; |
| | | private final int flags; |
| | | private final long signCount; |
| | | |
| | | private AuthenticatorData(byte[] raw, int flags, long signCount) { |
| | | this.raw = raw; |
| | | this.flags = flags; |
| | | this.signCount = signCount; |
| | | } |
| | | |
| | | public byte[] getRaw() { |
| | | return raw; |
| | | } |
| | | |
| | | public int getFlags() { |
| | | return flags; |
| | | } |
| | | |
| | | public long getSignCount() { |
| | | return signCount; |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | |
| | | public static BufferedImage createImg(String content) throws WriterException { |
| | | return createImg(content, QRCODE_SIZE); |
| | | } |
| | | |
| | | public static BufferedImage createImg(String content, int size) throws WriterException { |
| | | ConcurrentHashMap hints = new ConcurrentHashMap(); |
| | | hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); |
| | | hints.put(EncodeHintType.CHARACTER_SET, CHARSET); |
| | | hints.put(EncodeHintType.MARGIN, 1); |
| | | BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE, hints); |
| | | BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, size, size, hints); |
| | | int width = bitMatrix.getWidth(); |
| | | int height = bitMatrix.getHeight(); |
| | | BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); |
| | |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.core.annotations.ManagerAuth; |
| | | import com.core.common.Cools; |
| | | import com.core.common.R; |
| | | import com.core.exception.CoolException; |
| | | import com.zy.common.CodeRes; |
| | | import com.zy.common.auth.MfaLoginTicketManager; |
| | | import com.zy.common.auth.PasskeyChallengeManager; |
| | | import com.zy.common.i18n.I18nMessageService; |
| | | import com.zy.common.entity.Parameter; |
| | | import com.zy.common.model.PowerDto; |
| | | import com.zy.common.model.enums.HtmlNavIconType; |
| | | import com.zy.common.utils.MfaTotpUtil; |
| | | import com.zy.common.utils.PasskeyWebAuthnUtil; |
| | | import com.zy.common.utils.QrCode; |
| | | import com.zy.common.utils.RandomValidateCodeUtil; |
| | | import com.zy.system.entity.*; |
| | | import com.zy.system.service.*; |
| | |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | import javax.imageio.ImageIO; |
| | | import java.awt.image.BufferedImage; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.util.Base64; |
| | | import java.util.*; |
| | | |
| | | /** |
| | |
| | | private LicenseTimer licenseTimer; |
| | | @Autowired |
| | | private I18nMessageService i18nMessageService; |
| | | @Autowired |
| | | private MfaLoginTicketManager mfaLoginTicketManager; |
| | | @Autowired |
| | | private PasskeyChallengeManager passkeyChallengeManager; |
| | | |
| | | @RequestMapping("/login.action") |
| | | @ManagerAuth(value = ManagerAuth.Auth.NONE, memo = "登录") |
| | |
| | | if (!licenseTimer.getSystemSupport()){ |
| | | return new R(20001, i18nMessageService.getMessage("response.system.licenseExpired")); |
| | | } |
| | | if (Cools.isEmpty(mobile, password)) { |
| | | return new R(10003, i18nMessageService.getMessage("response.user.passwordMismatch")); |
| | | } |
| | | if (mobile.equals("super") && password.equals(Cools.md5(superPwd))) { |
| | | Map<String, Object> res = new HashMap<>(); |
| | | res.put("username", mobile); |
| | | res.put("token", Cools.enToken(System.currentTimeMillis() + mobile, superPwd)); |
| | | return R.ok(res); |
| | | } |
| | | QueryWrapper<User> userWrapper = new QueryWrapper<>(); |
| | | userWrapper.eq("mobile", mobile); |
| | | User user = userService.getOne(userWrapper); |
| | | User user = userService.getByMobileWithSecurity(mobile); |
| | | if (Cools.isEmpty(user)){ |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | |
| | | if (!user.getPassword().equals(password)){ |
| | | return new R(10003, i18nMessageService.getMessage("response.user.passwordMismatch")); |
| | | } |
| | | String token = Cools.enToken(System.currentTimeMillis() + mobile, user.getPassword()); |
| | | userLoginService.remove(new QueryWrapper<UserLogin>().eq("user_id", user.getId()).eq("system_type", "WCS")); |
| | | UserLogin userLogin = new UserLogin(); |
| | | userLogin.setUserId(user.getId()); |
| | | userLogin.setToken(token); |
| | | userLogin.setSystemType("WCS"); |
| | | userLoginService.save(userLogin); |
| | | Map<String, Object> res = new HashMap<>(); |
| | | res.put("username", user.getUsername()); |
| | | res.put("token", token); |
| | | return R.ok(res); |
| | | if (requiresMfa(user)) { |
| | | Map<String, Object> res = new HashMap<>(); |
| | | res.put("username", user.getUsername()); |
| | | res.put("mfaRequired", true); |
| | | res.put("mfaTicket", mfaLoginTicketManager.create(user.getId())); |
| | | return R.ok(res); |
| | | } |
| | | return R.ok(buildLoginSuccess(user)); |
| | | } |
| | | |
| | | @RequestMapping("/login/mfa.action") |
| | | @ManagerAuth(value = ManagerAuth.Auth.NONE, memo = "MFA登录") |
| | | public R loginMfaAction(String ticket, String code) { |
| | | Long userId = mfaLoginTicketManager.getUserId(ticket); |
| | | if (userId == null) { |
| | | return new R(10004, i18nMessageService.getMessage("response.user.mfaTicketExpired")); |
| | | } |
| | | User user = userService.getByIdWithSecurity(userId); |
| | | if (Cools.isEmpty(user)) { |
| | | mfaLoginTicketManager.remove(ticket); |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (user.getStatus() != 1) { |
| | | mfaLoginTicketManager.remove(ticket); |
| | | return new R(10002, i18nMessageService.getMessage("response.user.disabled")); |
| | | } |
| | | if (!requiresMfa(user)) { |
| | | mfaLoginTicketManager.remove(ticket); |
| | | return new R(10005, i18nMessageService.getMessage("response.user.mfaNotEnabled")); |
| | | } |
| | | if (!MfaTotpUtil.verifyCode(user.getMfaSecret(), code, 1)) { |
| | | return new R(10006, i18nMessageService.getMessage("response.user.mfaCodeMismatch")); |
| | | } |
| | | mfaLoginTicketManager.remove(ticket); |
| | | return R.ok(buildLoginSuccess(user)); |
| | | } |
| | | |
| | | @RequestMapping("/login/passkey/options.action") |
| | | @ManagerAuth(value = ManagerAuth.Auth.NONE, memo = "通行密钥登录参数") |
| | | public R loginPasskeyOptions(String mobile) { |
| | | if (!licenseTimer.getSystemSupport()) { |
| | | return new R(20001, i18nMessageService.getMessage("response.system.licenseExpired")); |
| | | } |
| | | String origin = PasskeyWebAuthnUtil.buildOrigin(request); |
| | | String rpId = PasskeyWebAuthnUtil.buildRpId(request); |
| | | if (!PasskeyWebAuthnUtil.isSecureOriginAllowed(origin, rpId)) { |
| | | return new R(10009, i18nMessageService.getMessage("response.user.passkeySecureContextRequired")); |
| | | } |
| | | User user = null; |
| | | if (!Cools.isEmpty(mobile)) { |
| | | user = userService.getByMobileWithSecurity(mobile); |
| | | if (Cools.isEmpty(user)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (user.getStatus() != 1) { |
| | | return new R(10002, i18nMessageService.getMessage("response.user.disabled")); |
| | | } |
| | | if (!hasPasskeyBound(user)) { |
| | | return new R(10010, i18nMessageService.getMessage("response.user.passkeyNotBound")); |
| | | } |
| | | } |
| | | PasskeyChallengeManager.ChallengeState state = passkeyChallengeManager.createAuthentication(user == null ? null : user.getId(), origin, rpId); |
| | | return R.ok(buildPasskeyAuthenticationOptions(state, user)); |
| | | } |
| | | |
| | | @RequestMapping("/login/passkey/verify.action") |
| | | @ManagerAuth(value = ManagerAuth.Auth.NONE, memo = "通行密钥登录") |
| | | public R loginPasskeyVerify(String ticket, |
| | | String credentialId, |
| | | String clientDataJSON, |
| | | String authenticatorData, |
| | | String signature) { |
| | | if (!licenseTimer.getSystemSupport()) { |
| | | return new R(20001, i18nMessageService.getMessage("response.system.licenseExpired")); |
| | | } |
| | | PasskeyChallengeManager.ChallengeState state = passkeyChallengeManager.get(ticket, PasskeyChallengeManager.Purpose.AUTHENTICATION); |
| | | if (state == null) { |
| | | return new R(10011, i18nMessageService.getMessage("response.user.passkeyTicketExpired")); |
| | | } |
| | | try { |
| | | com.alibaba.fastjson.JSONObject clientData = PasskeyWebAuthnUtil.parseClientData(clientDataJSON); |
| | | PasskeyWebAuthnUtil.validateClientData(clientData, "webauthn.get", state.getChallenge(), state.getOrigin()); |
| | | PasskeyWebAuthnUtil.AuthenticatorData authData = PasskeyWebAuthnUtil.validateAuthenticatorData(authenticatorData, state.getRpId(), true); |
| | | User user = state.getUserId() == null |
| | | ? userService.getByPasskeyCredentialId(credentialId) |
| | | : userService.getByIdWithSecurity(state.getUserId()); |
| | | if (Cools.isEmpty(user)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (user.getStatus() != 1) { |
| | | return new R(10002, i18nMessageService.getMessage("response.user.disabled")); |
| | | } |
| | | if (!hasPasskeyBound(user) || !Cools.eq(user.getPasskeyCredentialId(), credentialId)) { |
| | | return new R(10010, i18nMessageService.getMessage("response.user.passkeyNotBound")); |
| | | } |
| | | PasskeyWebAuthnUtil.verifyAssertionSignature( |
| | | user.getPasskeyPublicKey(), |
| | | user.getPasskeyAlgorithm(), |
| | | authenticatorData, |
| | | clientDataJSON, |
| | | signature |
| | | ); |
| | | long nextSignCount = authData.getSignCount(); |
| | | Long currentSignCount = user.getPasskeySignCount(); |
| | | if (currentSignCount != null && currentSignCount > 0 && nextSignCount > 0 && nextSignCount <= currentSignCount) { |
| | | return new R(10012, i18nMessageService.getMessage("response.user.passkeyCounterMismatch")); |
| | | } |
| | | userService.update(new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<User>() |
| | | .eq("id", user.getId()) |
| | | .set("passkey_sign_count", nextSignCount) |
| | | .set("passkey_last_used_time", new Date())); |
| | | return R.ok(buildLoginSuccess(user)); |
| | | } catch (Exception ex) { |
| | | return new R(10013, i18nMessageService.getMessage("response.user.passkeyVerifyFailed")); |
| | | } finally { |
| | | passkeyChallengeManager.remove(ticket); |
| | | } |
| | | } |
| | | |
| | | @RequestMapping("/code/switch.action") |
| | |
| | | @RequestMapping("/user/detail/auth") |
| | | @ManagerAuth |
| | | public R userDetail(){ |
| | | return R.ok(userService.getById(getUserId())); |
| | | User user = userService.getByIdWithSecurity(getUserId()); |
| | | if (Cools.isEmpty(user)) { |
| | | return R.ok(); |
| | | } |
| | | return R.ok(buildSafeUserDetail(user)); |
| | | } |
| | | |
| | | @RequestMapping("/user/mfa/setup/auth") |
| | | @ManagerAuth |
| | | public R userMfaSetup() { |
| | | User user = userService.getByIdWithSecurity(getUserId()); |
| | | if (Cools.isEmpty(user)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (!Integer.valueOf(1).equals(user.getMfaAllow())) { |
| | | return new R(10007, i18nMessageService.getMessage("response.user.mfaNotAllowed")); |
| | | } |
| | | String secret = MfaTotpUtil.generateSecret(); |
| | | String account = !Cools.isEmpty(user.getMobile()) |
| | | ? user.getMobile() |
| | | : (!Cools.isEmpty(user.getUsername()) ? user.getUsername() : String.valueOf(user.getId())); |
| | | String otpAuth = MfaTotpUtil.buildOtpAuthUri("WCS", account, secret); |
| | | Map<String, Object> data = new HashMap<>(); |
| | | data.put("secret", secret); |
| | | data.put("otpAuth", otpAuth); |
| | | data.put("qrCode", renderQrCodeDataUri(otpAuth)); |
| | | return R.ok(data); |
| | | } |
| | | |
| | | @RequestMapping("/user/mfa/enable/auth") |
| | | @ManagerAuth |
| | | @Transactional |
| | | public R userMfaEnable(String currentPassword, String secret, String code) { |
| | | User user = userService.getByIdWithSecurity(getUserId()); |
| | | if (Cools.isEmpty(user)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (!Integer.valueOf(1).equals(user.getMfaAllow())) { |
| | | return new R(10007, i18nMessageService.getMessage("response.user.mfaNotAllowed")); |
| | | } |
| | | if (!Cools.eq(user.getPassword(), currentPassword)) { |
| | | return new R(10008, i18nMessageService.getMessage("response.user.oldPasswordMismatch")); |
| | | } |
| | | String normalizedSecret = normalizeSecret(secret); |
| | | if (Cools.isEmpty(normalizedSecret) || !MfaTotpUtil.verifyCode(normalizedSecret, code, 1)) { |
| | | return new R(10006, i18nMessageService.getMessage("response.user.mfaCodeMismatch")); |
| | | } |
| | | userService.update(new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<User>() |
| | | .eq("id", user.getId()) |
| | | .set("mfa_allow", 1) |
| | | .set("mfa_enabled", 1) |
| | | .set("mfa_secret", normalizedSecret) |
| | | .set("mfa_bound_time", new Date())); |
| | | return R.ok(); |
| | | } |
| | | |
| | | @RequestMapping("/user/mfa/disable/auth") |
| | | @ManagerAuth |
| | | @Transactional |
| | | public R userMfaDisable(String currentPassword, String code) { |
| | | User user = userService.getByIdWithSecurity(getUserId()); |
| | | if (Cools.isEmpty(user)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (!requiresMfa(user)) { |
| | | return new R(10005, i18nMessageService.getMessage("response.user.mfaNotEnabled")); |
| | | } |
| | | if (!Cools.eq(user.getPassword(), currentPassword)) { |
| | | return new R(10008, i18nMessageService.getMessage("response.user.oldPasswordMismatch")); |
| | | } |
| | | if (!MfaTotpUtil.verifyCode(user.getMfaSecret(), code, 1)) { |
| | | return new R(10006, i18nMessageService.getMessage("response.user.mfaCodeMismatch")); |
| | | } |
| | | userService.update(new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<User>() |
| | | .eq("id", user.getId()) |
| | | .set("mfa_enabled", 0) |
| | | .set("mfa_secret", null) |
| | | .set("mfa_bound_time", null)); |
| | | return R.ok(); |
| | | } |
| | | |
| | | @RequestMapping("/user/passkey/register/options/auth") |
| | | @ManagerAuth |
| | | public R userPasskeyRegisterOptions() { |
| | | User user = userService.getByIdWithSecurity(getUserId()); |
| | | if (Cools.isEmpty(user)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (hasPasskeyBound(user)) { |
| | | return new R(10014, i18nMessageService.getMessage("response.user.passkeyAlreadyBound")); |
| | | } |
| | | String origin = PasskeyWebAuthnUtil.buildOrigin(request); |
| | | String rpId = PasskeyWebAuthnUtil.buildRpId(request); |
| | | if (!PasskeyWebAuthnUtil.isSecureOriginAllowed(origin, rpId)) { |
| | | return new R(10009, i18nMessageService.getMessage("response.user.passkeySecureContextRequired")); |
| | | } |
| | | PasskeyChallengeManager.ChallengeState state = passkeyChallengeManager.createRegistration(user.getId(), origin, rpId); |
| | | return R.ok(buildPasskeyRegistrationOptions(state, user)); |
| | | } |
| | | |
| | | @RequestMapping("/user/passkey/register/finish/auth") |
| | | @ManagerAuth |
| | | @Transactional |
| | | public R userPasskeyRegisterFinish(String ticket, |
| | | String currentPassword, |
| | | String name, |
| | | String credentialId, |
| | | String clientDataJSON, |
| | | String authenticatorData, |
| | | String publicKey, |
| | | Integer publicKeyAlgorithm, |
| | | String transports) { |
| | | User user = userService.getByIdWithSecurity(getUserId()); |
| | | if (Cools.isEmpty(user)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (!Cools.eq(user.getPassword(), currentPassword)) { |
| | | return new R(10008, i18nMessageService.getMessage("response.user.oldPasswordMismatch")); |
| | | } |
| | | if (hasPasskeyBound(user)) { |
| | | return new R(10014, i18nMessageService.getMessage("response.user.passkeyAlreadyBound")); |
| | | } |
| | | PasskeyChallengeManager.ChallengeState state = passkeyChallengeManager.get(ticket, PasskeyChallengeManager.Purpose.REGISTRATION); |
| | | if (state == null || !Objects.equals(state.getUserId(), user.getId())) { |
| | | return new R(10011, i18nMessageService.getMessage("response.user.passkeyTicketExpired")); |
| | | } |
| | | try { |
| | | com.alibaba.fastjson.JSONObject clientData = PasskeyWebAuthnUtil.parseClientData(clientDataJSON); |
| | | PasskeyWebAuthnUtil.validateClientData(clientData, "webauthn.create", state.getChallenge(), state.getOrigin()); |
| | | PasskeyWebAuthnUtil.AuthenticatorData authData = PasskeyWebAuthnUtil.validateAuthenticatorData(authenticatorData, state.getRpId(), true); |
| | | PasskeyWebAuthnUtil.ensurePublicKeyMaterial(publicKey, publicKeyAlgorithm); |
| | | if (Cools.isEmpty(credentialId)) { |
| | | return new R(10013, i18nMessageService.getMessage("response.user.passkeyRegisterFailed")); |
| | | } |
| | | User exist = userService.getByPasskeyCredentialId(credentialId); |
| | | if (!Cools.isEmpty(exist) && !Objects.equals(exist.getId(), user.getId())) { |
| | | return new R(10015, i18nMessageService.getMessage("response.user.passkeyCredentialExists")); |
| | | } |
| | | userService.update(new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<User>() |
| | | .eq("id", user.getId()) |
| | | .set("passkey_name", normalizePasskeyName(name)) |
| | | .set("passkey_credential_id", credentialId) |
| | | .set("passkey_public_key", publicKey) |
| | | .set("passkey_algorithm", publicKeyAlgorithm) |
| | | .set("passkey_sign_count", authData.getSignCount()) |
| | | .set("passkey_transports", normalizePasskeyTransports(transports)) |
| | | .set("passkey_bound_time", new Date()) |
| | | .set("passkey_last_used_time", null)); |
| | | return R.ok(); |
| | | } catch (Exception ex) { |
| | | return new R(10016, i18nMessageService.getMessage("response.user.passkeyRegisterFailed")); |
| | | } finally { |
| | | passkeyChallengeManager.remove(ticket); |
| | | } |
| | | } |
| | | |
| | | @RequestMapping("/user/passkey/remove/auth") |
| | | @ManagerAuth |
| | | @Transactional |
| | | public R userPasskeyRemove(String currentPassword) { |
| | | User user = userService.getByIdWithSecurity(getUserId()); |
| | | if (Cools.isEmpty(user)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (!hasPasskeyBound(user)) { |
| | | return new R(10010, i18nMessageService.getMessage("response.user.passkeyNotBound")); |
| | | } |
| | | if (!Cools.eq(user.getPassword(), currentPassword)) { |
| | | return new R(10008, i18nMessageService.getMessage("response.user.oldPasswordMismatch")); |
| | | } |
| | | userService.update(new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<User>() |
| | | .eq("id", user.getId()) |
| | | .set("passkey_name", null) |
| | | .set("passkey_credential_id", null) |
| | | .set("passkey_public_key", null) |
| | | .set("passkey_algorithm", null) |
| | | .set("passkey_sign_count", 0) |
| | | .set("passkey_transports", null) |
| | | .set("passkey_bound_time", null) |
| | | .set("passkey_last_used_time", null)); |
| | | return R.ok(); |
| | | } |
| | | |
| | | @RequestMapping("/menu/auth") |
| | |
| | | return R.ok(result); |
| | | } |
| | | |
| | | private Map<String, Object> buildLoginSuccess(User user) { |
| | | String token = Cools.enToken(System.currentTimeMillis() + user.getMobile(), user.getPassword()); |
| | | userLoginService.remove(new QueryWrapper<UserLogin>().eq("user_id", user.getId()).eq("system_type", "WCS")); |
| | | UserLogin userLogin = new UserLogin(); |
| | | userLogin.setUserId(user.getId()); |
| | | userLogin.setToken(token); |
| | | userLogin.setSystemType("WCS"); |
| | | userLoginService.save(userLogin); |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("username", user.getUsername()); |
| | | result.put("token", token); |
| | | result.put("mfaRequired", false); |
| | | return result; |
| | | } |
| | | |
| | | private boolean requiresMfa(User user) { |
| | | return user != null |
| | | && Integer.valueOf(1).equals(user.getMfaAllow()) |
| | | && Integer.valueOf(1).equals(user.getMfaEnabled()) |
| | | && !Cools.isEmpty(user.getMfaSecret()); |
| | | } |
| | | |
| | | private boolean hasPasskeyBound(User user) { |
| | | return user != null |
| | | && !Cools.isEmpty(user.getPasskeyCredentialId()) |
| | | && !Cools.isEmpty(user.getPasskeyPublicKey()); |
| | | } |
| | | |
| | | private Map<String, Object> buildSafeUserDetail(User user) { |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("id", user.getId()); |
| | | result.put("roleName", user.getRoleName()); |
| | | result.put("username", user.getUsername()); |
| | | result.put("mobile", user.getMobile()); |
| | | result.put("createTime$", user.getCreateTime$()); |
| | | result.put("mfaAllow", user.getMfaAllow()); |
| | | result.put("mfaAllow$", user.getMfaAllow$()); |
| | | result.put("mfaEnabled", user.getMfaEnabled()); |
| | | result.put("mfaEnabled$", user.getMfaEnabled$()); |
| | | result.put("mfaBoundTime$", user.getMfaBoundTime$()); |
| | | result.put("mfaMaskedSecret", MfaTotpUtil.maskSecret(user.getMfaSecret())); |
| | | result.put("passkeyBound", hasPasskeyBound(user)); |
| | | result.put("passkeyName", user.getPasskeyName()); |
| | | result.put("passkeyBoundTime$", user.getPasskeyBoundTime$()); |
| | | result.put("passkeyLastUsedTime$", user.getPasskeyLastUsedTime$()); |
| | | result.put("passkeyTransports", user.getPasskeyTransports()); |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, Object> buildPasskeyRegistrationOptions(PasskeyChallengeManager.ChallengeState state, User user) { |
| | | Map<String, Object> data = new HashMap<>(); |
| | | data.put("ticket", state.getTicket()); |
| | | data.put("challenge", state.getChallenge()); |
| | | data.put("rpId", state.getRpId()); |
| | | data.put("rpName", "WCS"); |
| | | data.put("userId", Base64.getUrlEncoder().withoutPadding().encodeToString(PasskeyWebAuthnUtil.buildUserHandle(user.getId()))); |
| | | data.put("userName", resolvePasskeyUserName(user)); |
| | | data.put("userDisplayName", resolvePasskeyDisplayName(user)); |
| | | data.put("timeout", 60000); |
| | | data.put("attestation", "none"); |
| | | data.put("pubKeyCredParams", buildPasskeyAlgorithms()); |
| | | Map<String, Object> authenticatorSelection = new HashMap<>(); |
| | | authenticatorSelection.put("residentKey", "preferred"); |
| | | authenticatorSelection.put("userVerification", "required"); |
| | | data.put("authenticatorSelection", authenticatorSelection); |
| | | data.put("excludeCredentials", Collections.emptyList()); |
| | | return data; |
| | | } |
| | | |
| | | private Map<String, Object> buildPasskeyAuthenticationOptions(PasskeyChallengeManager.ChallengeState state, User user) { |
| | | Map<String, Object> data = new HashMap<>(); |
| | | data.put("ticket", state.getTicket()); |
| | | data.put("challenge", state.getChallenge()); |
| | | data.put("rpId", state.getRpId()); |
| | | data.put("timeout", 60000); |
| | | data.put("userVerification", "required"); |
| | | if (user == null) { |
| | | data.put("allowCredentials", Collections.emptyList()); |
| | | } else { |
| | | data.put("allowCredentials", Collections.singletonList(buildPasskeyDescriptor(user.getPasskeyCredentialId(), user.getPasskeyTransports()))); |
| | | } |
| | | return data; |
| | | } |
| | | |
| | | private List<Map<String, Object>> buildPasskeyAlgorithms() { |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | result.add(buildPasskeyAlgorithm(-7)); |
| | | result.add(buildPasskeyAlgorithm(-257)); |
| | | result.add(buildPasskeyAlgorithm(-8)); |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, Object> buildPasskeyAlgorithm(int alg) { |
| | | Map<String, Object> item = new HashMap<>(); |
| | | item.put("type", "public-key"); |
| | | item.put("alg", alg); |
| | | return item; |
| | | } |
| | | |
| | | private Map<String, Object> buildPasskeyDescriptor(String credentialId, String transports) { |
| | | Map<String, Object> item = new HashMap<>(); |
| | | item.put("type", "public-key"); |
| | | item.put("id", credentialId); |
| | | item.put("transports", parsePasskeyTransports(transports)); |
| | | return item; |
| | | } |
| | | |
| | | private String renderQrCodeDataUri(String content) { |
| | | try { |
| | | BufferedImage image = QrCode.createImg(content, 220); |
| | | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| | | ImageIO.write(image, "jpg", outputStream); |
| | | return "data:image/jpeg;base64," + Base64.getEncoder().encodeToString(outputStream.toByteArray()); |
| | | } catch (Exception e) { |
| | | return ""; |
| | | } |
| | | } |
| | | |
| | | private String normalizeSecret(String secret) { |
| | | if (Cools.isEmpty(secret)) { |
| | | return ""; |
| | | } |
| | | return String.valueOf(secret) |
| | | .trim() |
| | | .replace(" ", "") |
| | | .replace("-", "") |
| | | .toUpperCase(Locale.ROOT); |
| | | } |
| | | |
| | | private String resolvePasskeyUserName(User user) { |
| | | if (!Cools.isEmpty(user.getMobile())) { |
| | | return user.getMobile(); |
| | | } |
| | | if (!Cools.isEmpty(user.getUsername())) { |
| | | return user.getUsername(); |
| | | } |
| | | return String.valueOf(user.getId()); |
| | | } |
| | | |
| | | private String resolvePasskeyDisplayName(User user) { |
| | | if (!Cools.isEmpty(user.getUsername())) { |
| | | return user.getUsername(); |
| | | } |
| | | return resolvePasskeyUserName(user); |
| | | } |
| | | |
| | | private String normalizePasskeyName(String name) { |
| | | String value = Cools.isEmpty(name) ? "" : String.valueOf(name).trim(); |
| | | if (value.length() > 100) { |
| | | value = value.substring(0, 100); |
| | | } |
| | | if (!Cools.isEmpty(value)) { |
| | | return value; |
| | | } |
| | | return "通行密钥-" + new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); |
| | | } |
| | | |
| | | private String normalizePasskeyTransports(String transports) { |
| | | List<String> values = parsePasskeyTransports(transports); |
| | | return values.isEmpty() ? null : JSON.toJSONString(values); |
| | | } |
| | | |
| | | private List<String> parsePasskeyTransports(String transports) { |
| | | if (Cools.isEmpty(transports)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | try { |
| | | List<String> values = JSON.parseArray(transports, String.class); |
| | | return values == null ? Collections.emptyList() : values; |
| | | } catch (Exception ignored) { |
| | | } |
| | | List<String> result = new ArrayList<>(); |
| | | for (String value : String.valueOf(transports).split(",")) { |
| | | String item = value == null ? "" : value.trim(); |
| | | if (!item.isEmpty()) { |
| | | result.add(item); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private String localizeResourceName(Resource resource) { |
| | | return i18nMessageService.resolveResourceText(resource.getName(), resource.getCode(), resource.getId()); |
| | | } |
| New file |
| | |
| | | package com.zy.system.config; |
| | | |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import jakarta.annotation.PostConstruct; |
| | | import javax.sql.DataSource; |
| | | import java.sql.Connection; |
| | | import java.sql.DatabaseMetaData; |
| | | import java.sql.ResultSet; |
| | | import java.sql.Statement; |
| | | |
| | | @Component |
| | | public class UserMfaSchemaInitializer { |
| | | |
| | | private final DataSource dataSource; |
| | | |
| | | public UserMfaSchemaInitializer(DataSource dataSource) { |
| | | this.dataSource = dataSource; |
| | | } |
| | | |
| | | @PostConstruct |
| | | public void init() { |
| | | ensureColumn("sys_user", "mfa_allow", "INT DEFAULT 0"); |
| | | ensureColumn("sys_user", "mfa_enabled", "INT DEFAULT 0"); |
| | | ensureColumn("sys_user", "mfa_secret", "VARCHAR(128)"); |
| | | ensureColumn("sys_user", "mfa_bound_time", "DATETIME NULL"); |
| | | } |
| | | |
| | | private void ensureColumn(String tableName, String columnName, String columnDefinition) { |
| | | try (Connection connection = dataSource.getConnection()) { |
| | | if (hasColumn(connection, tableName, columnName)) { |
| | | return; |
| | | } |
| | | try (Statement statement = connection.createStatement()) { |
| | | statement.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + columnDefinition); |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | |
| | | private boolean hasColumn(Connection connection, String tableName, String columnName) throws Exception { |
| | | DatabaseMetaData metaData = connection.getMetaData(); |
| | | try (ResultSet resultSet = metaData.getColumns(connection.getCatalog(), null, tableName, null)) { |
| | | while (resultSet.next()) { |
| | | if (columnName.equalsIgnoreCase(resultSet.getString("COLUMN_NAME"))) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.system.config; |
| | | |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import jakarta.annotation.PostConstruct; |
| | | import javax.sql.DataSource; |
| | | import java.sql.Connection; |
| | | import java.sql.DatabaseMetaData; |
| | | import java.sql.ResultSet; |
| | | import java.sql.Statement; |
| | | |
| | | @Component |
| | | public class UserPasskeySchemaInitializer { |
| | | |
| | | private final DataSource dataSource; |
| | | |
| | | public UserPasskeySchemaInitializer(DataSource dataSource) { |
| | | this.dataSource = dataSource; |
| | | } |
| | | |
| | | @PostConstruct |
| | | public void init() { |
| | | ensureColumn("sys_user", "passkey_name", "VARCHAR(100)"); |
| | | ensureColumn("sys_user", "passkey_credential_id", "VARCHAR(255)"); |
| | | ensureColumn("sys_user", "passkey_public_key", "TEXT"); |
| | | ensureColumn("sys_user", "passkey_algorithm", "INT"); |
| | | ensureColumn("sys_user", "passkey_sign_count", "BIGINT DEFAULT 0"); |
| | | ensureColumn("sys_user", "passkey_transports", "VARCHAR(255)"); |
| | | ensureColumn("sys_user", "passkey_bound_time", "DATETIME NULL"); |
| | | ensureColumn("sys_user", "passkey_last_used_time", "DATETIME NULL"); |
| | | } |
| | | |
| | | private void ensureColumn(String tableName, String columnName, String columnDefinition) { |
| | | try (Connection connection = dataSource.getConnection()) { |
| | | if (hasColumn(connection, tableName, columnName)) { |
| | | return; |
| | | } |
| | | try (Statement statement = connection.createStatement()) { |
| | | statement.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + columnDefinition); |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | |
| | | private boolean hasColumn(Connection connection, String tableName, String columnName) throws Exception { |
| | | DatabaseMetaData metaData = connection.getMetaData(); |
| | | try (ResultSet resultSet = metaData.getColumns(connection.getCatalog(), null, tableName, null)) { |
| | | while (resultSet.next()) { |
| | | if (columnName.equalsIgnoreCase(resultSet.getString("COLUMN_NAME"))) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | } |
| | |
| | | |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.core.annotations.ManagerAuth; |
| | | import com.core.common.Cools; |
| | | import com.core.common.DateUtils; |
| | | import com.core.common.R; |
| | | import com.zy.common.i18n.I18nMessageService; |
| | | import com.zy.common.web.BaseController; |
| | | import com.zy.system.entity.Role; |
| | | import com.zy.system.entity.User; |
| | | import com.zy.system.entity.UserLogin; |
| | | import com.zy.system.service.RoleService; |
| | | import com.zy.system.service.UserLoginService; |
| | | import com.zy.system.service.UserService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | |
| | | private UserService userService; |
| | | @Autowired |
| | | private RoleService roleService; |
| | | @Autowired |
| | | private UserLoginService userLoginService; |
| | | @Autowired |
| | | private I18nMessageService i18nMessageService; |
| | | |
| | | @RequestMapping(value = "/user/{id}/auth") |
| | | @ManagerAuth |
| | | public R get(@PathVariable("id") Long id) { |
| | | return R.ok(userService.getById(String.valueOf(id))); |
| | | User user = userService.getById(String.valueOf(id)); |
| | | sanitizeUser(user); |
| | | return R.ok(user); |
| | | } |
| | | |
| | | @RequestMapping(value = "/user/list/auth") |
| | |
| | | QueryWrapper<User> wrapper = new QueryWrapper<>(); |
| | | convert(param, wrapper); |
| | | wrapper.orderBy(true, false, "id"); |
| | | Page<User> page; |
| | | if (9527 == getUserId()) { |
| | | return R.ok(userService.page(new Page<>(curr, limit), wrapper)); |
| | | page = userService.page(new Page<>(curr, limit), wrapper); |
| | | sanitizeUsers(page.getRecords()); |
| | | return R.ok(page); |
| | | } |
| | | |
| | | Long roleId = getUser().getRoleId(); |
| | |
| | | wrapper.notIn("role_id", leaderIds); |
| | | } |
| | | |
| | | return R.ok(userService.page(new Page<>(curr, limit), wrapper)); |
| | | page = userService.page(new Page<>(curr, limit), wrapper); |
| | | sanitizeUsers(page.getRecords()); |
| | | return R.ok(page); |
| | | } |
| | | |
| | | private void convert(Map<String, Object> map, QueryWrapper wrapper){ |
| | |
| | | return R.error(); |
| | | } |
| | | if (null == user.getId()){ |
| | | normalizeNewUser(user); |
| | | userService.save(user); |
| | | } else { |
| | | userService.updateById(user); |
| | | return R.ok(); |
| | | } |
| | | return R.ok(); |
| | | return update(user); |
| | | } |
| | | |
| | | @RequestMapping(value = "/user/add/auth") |
| | | @ManagerAuth(memo = "系统用户添加") |
| | | public R add(User user) { |
| | | normalizeNewUser(user); |
| | | userService.save(user); |
| | | return R.ok(); |
| | | } |
| | |
| | | return R.error(); |
| | | } |
| | | User entity = userService.getById(user.getId()); |
| | | if (Cools.isEmpty(entity)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | UpdateWrapper<User> wrapper = new UpdateWrapper<>(); |
| | | wrapper.eq("id", entity.getId()); |
| | | boolean needUpdate = false; |
| | | if (user.getPassword()!=null) { |
| | | entity.setPassword(user.getPassword()); |
| | | wrapper.set("password", user.getPassword()); |
| | | needUpdate = true; |
| | | } |
| | | if (user.getUsername()!=null) { |
| | | entity.setUsername(user.getUsername()); |
| | | wrapper.set("username", user.getUsername()); |
| | | needUpdate = true; |
| | | } |
| | | if (user.getMobile()!=null) { |
| | | entity.setMobile(user.getMobile()); |
| | | wrapper.set("mobile", user.getMobile()); |
| | | needUpdate = true; |
| | | } |
| | | if (user.getRoleId() !=null) { |
| | | entity.setRoleId(user.getRoleId()); |
| | | wrapper.set("role_id", user.getRoleId()); |
| | | needUpdate = true; |
| | | } |
| | | userService.updateById(entity); |
| | | if (user.getStatus() != null) { |
| | | wrapper.set("status", user.getStatus()); |
| | | needUpdate = true; |
| | | } |
| | | if (user.getMfaAllow() != null) { |
| | | int mfaAllow = normalizeMfaAllow(user.getMfaAllow()); |
| | | wrapper.set("mfa_allow", mfaAllow); |
| | | if (mfaAllow != 1) { |
| | | wrapper.set("mfa_enabled", 0); |
| | | wrapper.set("mfa_secret", null); |
| | | wrapper.set("mfa_bound_time", null); |
| | | } |
| | | needUpdate = true; |
| | | } |
| | | if (!needUpdate) { |
| | | return R.ok(); |
| | | } |
| | | userService.update(wrapper); |
| | | return R.ok(); |
| | | } |
| | | |
| | | @RequestMapping(value = "/user/password/update/auth") |
| | | @ManagerAuth(memo = "系统用户修改密码") |
| | | public R updatePassword(String oldPassword, String password) { |
| | | if (Cools.isEmpty(oldPassword, password)) { |
| | | return R.error(); |
| | | } |
| | | User user = userService.getById(getUserId()); |
| | | if (Cools.isEmpty(user)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (!Cools.eq(user.getPassword(), oldPassword)) { |
| | | return new R(10008, i18nMessageService.getMessage("response.user.oldPasswordMismatch")); |
| | | } |
| | | userService.update(new UpdateWrapper<User>() |
| | | .eq("id", user.getId()) |
| | | .set("password", password)); |
| | | userLoginService.remove(new QueryWrapper<UserLogin>().eq("user_id", user.getId()).eq("system_type", "WCS")); |
| | | return R.ok(); |
| | | } |
| | | |
| | | @RequestMapping(value = "/user/resetPassword/auth") |
| | | @ManagerAuth(memo = "系统用户重置密码") |
| | | public R resetPassword(Long id, String password) { |
| | | if (id == null || Cools.isEmpty(password)) { |
| | | return R.error(); |
| | | } |
| | | User user = userService.getById(id); |
| | | if (Cools.isEmpty(user)) { |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | userService.update(new UpdateWrapper<User>() |
| | | .eq("id", id) |
| | | .set("password", password)); |
| | | userLoginService.remove(new QueryWrapper<UserLogin>().eq("user_id", id).eq("system_type", "WCS")); |
| | | return R.ok(); |
| | | } |
| | | |
| | |
| | | return R.ok(result); |
| | | } |
| | | |
| | | private void normalizeNewUser(User user) { |
| | | if (Cools.isEmpty(user)) { |
| | | return; |
| | | } |
| | | int mfaAllow = normalizeMfaAllow(user.getMfaAllow()); |
| | | user.setMfaAllow(mfaAllow); |
| | | if (mfaAllow != 1) { |
| | | user.setMfaEnabled(0); |
| | | user.setMfaSecret(null); |
| | | user.setMfaBoundTime(null); |
| | | } else if (user.getMfaEnabled() == null) { |
| | | user.setMfaEnabled(0); |
| | | } |
| | | } |
| | | |
| | | private int normalizeMfaAllow(Integer mfaAllow) { |
| | | return Integer.valueOf(1).equals(mfaAllow) ? 1 : 0; |
| | | } |
| | | |
| | | private void sanitizeUsers(List<User> users) { |
| | | if (users == null) { |
| | | return; |
| | | } |
| | | for (User user : users) { |
| | | sanitizeUser(user); |
| | | } |
| | | } |
| | | |
| | | private void sanitizeUser(User user) { |
| | | if (user == null) { |
| | | return; |
| | | } |
| | | user.setPassword(null); |
| | | user.setMfaSecret(null); |
| | | user.setPasskeyCredentialId(null); |
| | | user.setPasskeyPublicKey(null); |
| | | user.setPasskeyAlgorithm(null); |
| | | user.setPasskeySignCount(null); |
| | | user.setPasskeyTransports(null); |
| | | } |
| | | |
| | | } |
| | |
| | | private String password; |
| | | |
| | | /** |
| | | * 是否允许使用 MFA |
| | | */ |
| | | @TableField("mfa_allow") |
| | | private Integer mfaAllow; |
| | | |
| | | /** |
| | | * 是否已启用 MFA |
| | | */ |
| | | @TableField("mfa_enabled") |
| | | private Integer mfaEnabled; |
| | | |
| | | /** |
| | | * MFA 密钥 |
| | | */ |
| | | @TableField(value = "mfa_secret", select = false) |
| | | private String mfaSecret; |
| | | |
| | | /** |
| | | * MFA 绑定时间 |
| | | */ |
| | | @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") |
| | | @TableField("mfa_bound_time") |
| | | private Date mfaBoundTime; |
| | | |
| | | /** |
| | | * 通行密钥名称 |
| | | */ |
| | | @TableField("passkey_name") |
| | | private String passkeyName; |
| | | |
| | | /** |
| | | * 通行密钥凭证ID |
| | | */ |
| | | @TableField(value = "passkey_credential_id", select = false) |
| | | private String passkeyCredentialId; |
| | | |
| | | /** |
| | | * 通行密钥公钥(SPKI Base64Url) |
| | | */ |
| | | @TableField(value = "passkey_public_key", select = false) |
| | | private String passkeyPublicKey; |
| | | |
| | | /** |
| | | * 通行密钥算法 |
| | | */ |
| | | @TableField(value = "passkey_algorithm", select = false) |
| | | private Integer passkeyAlgorithm; |
| | | |
| | | /** |
| | | * 通行密钥签名计数器 |
| | | */ |
| | | @TableField(value = "passkey_sign_count", select = false) |
| | | private Long passkeySignCount; |
| | | |
| | | /** |
| | | * 通行密钥传输方式 |
| | | */ |
| | | @TableField(value = "passkey_transports", select = false) |
| | | private String passkeyTransports; |
| | | |
| | | /** |
| | | * 通行密钥绑定时间 |
| | | */ |
| | | @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") |
| | | @TableField("passkey_bound_time") |
| | | private Date passkeyBoundTime; |
| | | |
| | | /** |
| | | * 通行密钥最近使用时间 |
| | | */ |
| | | @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") |
| | | @TableField("passkey_last_used_time") |
| | | private Date passkeyLastUsedTime; |
| | | |
| | | /** |
| | | * 角色 |
| | | */ |
| | | @TableField("role_id") |
| | |
| | | this.password = password; |
| | | } |
| | | |
| | | public Integer getMfaAllow() { |
| | | return mfaAllow; |
| | | } |
| | | |
| | | public String getMfaAllow$() { |
| | | if (null == this.mfaAllow) { |
| | | return null; |
| | | } |
| | | return Integer.valueOf(1).equals(this.mfaAllow) ? "是" : "否"; |
| | | } |
| | | |
| | | public void setMfaAllow(Integer mfaAllow) { |
| | | this.mfaAllow = mfaAllow; |
| | | } |
| | | |
| | | public Integer getMfaEnabled() { |
| | | return mfaEnabled; |
| | | } |
| | | |
| | | public String getMfaEnabled$() { |
| | | if (null == this.mfaEnabled) { |
| | | return null; |
| | | } |
| | | return Integer.valueOf(1).equals(this.mfaEnabled) ? "是" : "否"; |
| | | } |
| | | |
| | | public void setMfaEnabled(Integer mfaEnabled) { |
| | | this.mfaEnabled = mfaEnabled; |
| | | } |
| | | |
| | | public String getMfaSecret() { |
| | | return mfaSecret; |
| | | } |
| | | |
| | | public void setMfaSecret(String mfaSecret) { |
| | | this.mfaSecret = mfaSecret; |
| | | } |
| | | |
| | | public Date getMfaBoundTime() { |
| | | return mfaBoundTime; |
| | | } |
| | | |
| | | public String getMfaBoundTime$() { |
| | | if (Cools.isEmpty(this.mfaBoundTime)) { |
| | | return ""; |
| | | } |
| | | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.mfaBoundTime); |
| | | } |
| | | |
| | | public void setMfaBoundTime(Date mfaBoundTime) { |
| | | this.mfaBoundTime = mfaBoundTime; |
| | | } |
| | | |
| | | public String getPasskeyName() { |
| | | return passkeyName; |
| | | } |
| | | |
| | | public void setPasskeyName(String passkeyName) { |
| | | this.passkeyName = passkeyName; |
| | | } |
| | | |
| | | public String getPasskeyCredentialId() { |
| | | return passkeyCredentialId; |
| | | } |
| | | |
| | | public void setPasskeyCredentialId(String passkeyCredentialId) { |
| | | this.passkeyCredentialId = passkeyCredentialId; |
| | | } |
| | | |
| | | public String getPasskeyPublicKey() { |
| | | return passkeyPublicKey; |
| | | } |
| | | |
| | | public void setPasskeyPublicKey(String passkeyPublicKey) { |
| | | this.passkeyPublicKey = passkeyPublicKey; |
| | | } |
| | | |
| | | public Integer getPasskeyAlgorithm() { |
| | | return passkeyAlgorithm; |
| | | } |
| | | |
| | | public void setPasskeyAlgorithm(Integer passkeyAlgorithm) { |
| | | this.passkeyAlgorithm = passkeyAlgorithm; |
| | | } |
| | | |
| | | public Long getPasskeySignCount() { |
| | | return passkeySignCount; |
| | | } |
| | | |
| | | public void setPasskeySignCount(Long passkeySignCount) { |
| | | this.passkeySignCount = passkeySignCount; |
| | | } |
| | | |
| | | public String getPasskeyTransports() { |
| | | return passkeyTransports; |
| | | } |
| | | |
| | | public void setPasskeyTransports(String passkeyTransports) { |
| | | this.passkeyTransports = passkeyTransports; |
| | | } |
| | | |
| | | public Date getPasskeyBoundTime() { |
| | | return passkeyBoundTime; |
| | | } |
| | | |
| | | public String getPasskeyBoundTime$() { |
| | | if (Cools.isEmpty(this.passkeyBoundTime)) { |
| | | return ""; |
| | | } |
| | | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.passkeyBoundTime); |
| | | } |
| | | |
| | | public void setPasskeyBoundTime(Date passkeyBoundTime) { |
| | | this.passkeyBoundTime = passkeyBoundTime; |
| | | } |
| | | |
| | | public Date getPasskeyLastUsedTime() { |
| | | return passkeyLastUsedTime; |
| | | } |
| | | |
| | | public String getPasskeyLastUsedTime$() { |
| | | if (Cools.isEmpty(this.passkeyLastUsedTime)) { |
| | | return ""; |
| | | } |
| | | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.passkeyLastUsedTime); |
| | | } |
| | | |
| | | public void setPasskeyLastUsedTime(Date passkeyLastUsedTime) { |
| | | this.passkeyLastUsedTime = passkeyLastUsedTime; |
| | | } |
| | | |
| | | public Long getRoleId() { |
| | | return roleId; |
| | | } |
| | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.zy.system.entity.User; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.apache.ibatis.annotations.Param; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | @Mapper |
| | | @Repository |
| | | public interface UserMapper extends BaseMapper<User> { |
| | | |
| | | User selectByMobileWithSecurity(@Param("mobile") String mobile); |
| | | |
| | | User selectByIdWithSecurity(@Param("id") Long id); |
| | | |
| | | User selectByPasskeyCredentialId(@Param("credentialId") String credentialId); |
| | | } |
| | |
| | | |
| | | public interface UserService extends IService<User> { |
| | | |
| | | User getByMobileWithSecurity(String mobile); |
| | | |
| | | User getByIdWithSecurity(Long id); |
| | | |
| | | User getByPasskeyCredentialId(String credentialId); |
| | | } |
| | |
| | | @Service("userService") |
| | | public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { |
| | | |
| | | @Override |
| | | public User getByMobileWithSecurity(String mobile) { |
| | | return baseMapper.selectByMobileWithSecurity(mobile); |
| | | } |
| | | |
| | | @Override |
| | | public User getByIdWithSecurity(Long id) { |
| | | return baseMapper.selectByIdWithSecurity(id); |
| | | } |
| | | |
| | | @Override |
| | | public User getByPasskeyCredentialId(String credentialId) { |
| | | return baseMapper.selectByPasskeyCredentialId(credentialId); |
| | | } |
| | | } |
| | |
| | | 不可修改=Not Editable |
| | | 重要!一般用于后台登入=Important: usually used for admin login |
| | | 当前角色不可更改为其它角色=Current role cannot be changed |
| | | 手机号:=Phone: |
| | | 用户名:=Username: |
| | | 手机号\:=Phone: |
| | | 用户名\:=Username: |
| | | 输入手机号=Enter phone number |
| | | 输入用户名=Enter username |
| | | 重置密码=Reset Password |
| | |
| | | ConveyorCommand分段长度=Conveyor Command Segment Length |
| | | 站点点最大任务数量上限=Max Tasks Per Station |
| | | Stations点最大Task数量上限=Max Tasks Per Station |
| | | 冷却到: -=Cooldown Until: - |
| | | 最近错误: -=Latest Error: - |
| | | 冷却到\:-=Cooldown Until: - |
| | | 最近错误\:-=Latest Error: - |
| | | 故障切换开启=Failover Enabled |
| | | Failover开启=Failover Enabled |
| | | 额度切换开启=Quota Switch Enabled |
| | |
| | | Success 0 / Failed 0 / 连续Failed 0=Success 0 / Failed 0 / Consecutive Failures 0 |
| | | Success 7 / Failed 2 / 连续Failed 0=Success 7 / Failed 2 / Consecutive Failures 0 |
| | | Success 8 / Failed 0 / 连续Failed 0=Success 8 / Failed 0 / Consecutive Failures 0 |
| | | 必填,例如: https://dashscope.aliyuncs.com/compatible-mode/v1=Required, for example: https://dashscope.aliyuncs.com/compatible-mode/v1 |
| | | Required,例如: https://dashscope.aliyuncs.com/compatible-mode/v1=Required, for example: https://dashscope.aliyuncs.com/compatible-mode/v1 |
| | | 最近错误:=Latest Error: |
| | | 冷却到:=Cooldown Until: |
| | | 必填,例如\: https\://dashscope.aliyuncs.com/compatible-mode/v1=Required, for example: https://dashscope.aliyuncs.com/compatible-mode/v1 |
| | | Required,例如\: https\://dashscope.aliyuncs.com/compatible-mode/v1=Required, for example: https://dashscope.aliyuncs.com/compatible-mode/v1 |
| | | 最近错误\:=Latest Error: |
| | | 冷却到\:=Cooldown Until: |
| | | D.空桶/空栈板=D. Empty Tote/Pallet |
| | | E.出入专用轨道=E. Dedicated IO Rail |
| | | W.穿梭车母轨道=W. Shuttle Main Rail |
| | |
| | | 阿里百炼-kimi-k2.5=Alibaba Bailian - kimi-k2.5 |
| | | 阿里百炼-glm-5=Alibaba Bailian - glm-5 |
| | | 硅基流动=SiliconFlow |
| | | 提示=Prompt |
| | | 尝试=Attempt |
| | | 错误=Error |
| | | 空闲=Idle |
| | | 无物=Unloaded |
| | | 工作类型=Work Type |
| | | 能出=Outbound Enabled |
| | | 能入=Inbound Enabled |
| | | 文件数=File Count |
| | | 类型\:=Type: |
| | | 设备编号\:=Device No.: |
| | | 文件数\:=File Count: |
| | | 日志详情 -=Log Details - |
| | | 日志可视化 -=Device Logs - |
| | | 已删除=Deleted |
| | | 老项目仍可继续使用这份硬件信息 JSON 申请许可证。=Legacy projects can still use this hardware JSON to apply for a license. |
| | | 请求码中已包含项目名称,直接发给许可证服务端即可。=The request code already contains the project name and can be sent directly to the license service. |
| | | 暂无路由配置=No route configuration available |
| | | 点击右上角“新增路由”创建第一条配置=Click "Add Route" in the upper-right corner to create the first route. |
| | | 保存成功=Saved successfully |
| | | 保存失败=Save failed |
| | | 测试成功=Test succeeded |
| | | 测试失败=Test failed |
| | | 测试中...=Testing... |
| | | 导出成功=Export succeeded |
| | | 导出失败=Export failed |
| | | 导入确认=Import Confirmation |
| | | 导入失败=Import failed |
| | | 覆盖导入=Overwrite Import |
| | | 合并导入=Merge Import |
| | | 读取文件失败=Failed to read file |
| | | JSON 格式不正确=Invalid JSON format |
| | | 未找到可导入的 routes=No importable routes found |
| | | 请选择导入方式:覆盖导入会先清空现有路由;点击“合并导入”则按ID更新或新增。=Choose an import mode: overwrite import clears existing routes first; merge import updates by ID or adds new routes. |
| | | 当前是未保存配置,测试通过后仍需先保存才会生效=This configuration is not saved yet. It still needs to be saved after a successful test to take effect. |
| | | 复制失败,请手动复制=Copy failed. Please copy it manually. |
| | | 没有可复制内容=Nothing to copy |
| | | 已复制=Copied |
| | | API Key 为空=API key is empty |
| | | API Key 已复制=API key copied |
| | | 日志加载失败=Failed to load logs |
| | | 删除成功=Deleted successfully |
| | | 删除失败=Delete failed |
| | | 已清除冷却=Cooldown cleared |
| | | 已清空=Cleared |
| | | 清空日志=Clear Logs |
| | | 清空失败=Clear failed |
| | | 确定清空全部LLM调用日志吗?=Are you sure you want to clear all LLM call logs? |
| | | 确定删除该路由吗?=Are you sure you want to delete this route? |
| | | 确定删除该日志吗?=Are you sure you want to delete this log? |
| | | 导入完成:新增=Import completed: added |
| | | 导入异常明细(最多20条)=Import exception details (up to 20 items) |
| | | 状态码=Status Code |
| | | 场景=Scene |
| | | 模型=Model |
| | | 路由=Route |
| | | 结果=Result |
| | | 耗时(ms)=Latency (ms) |
| | | 耗时\:=Latency: |
| | | 结果\:=Result: |
| | | 模型\:=Model: |
| | | 路由\:=Route: |
| | | 场景\:=Scene: |
| | | 时间\:=Time: |
| | | 状态码\:=Status Code: |
| | | 请求\:=Request: |
| | | 响应\:=Response: |
| | | 返回片段\:=Response Snippet: |
| | | 最近错误\:=Latest Error: |
| | | 冷却到\:=Cooldown Until: |
| | | regex\:^成功\s*(\d+)\s*\/\s*失败\s*(\d+)\s*\/\s*连续失败\s*(\d+)$=Success $1 / Failed $2 / Consecutive Failures $3 |
| | | regex\:^时间\:\s*(.+)$=Time: $1 |
| | | regex\:^状态码\:\s*(.+)$=Status Code: $1 |
| | | regex\:^耗时\:\s*(.+)$=Latency: $1 |
| | | regex\:^结果\:\s*(.+)$=Result: $1 |
| | | regex\:^路由\:\s*(.+)$=Route: $1 |
| | | regex\:^模型\:\s*(.+)$=Model: $1 |
| | | regex\:^返回片段\:\s*(.+)$=Response Snippet: $1 |
| | | regex\:^请求\:\s*(.+)$=Request: $1 |
| | | regex\:^响应\:\s*(.+)$=Response: $1 |
| | | regex\:^最近错误\:\s*(.+)$=Latest Error: $1 |
| | | regex\:^冷却到\:\s*(.+)$=Cooldown Until: $1 |
| | | WCS AI 助手=WCS AI Assistant |
| | | AI 深度思考=AI Deep Thinking |
| | | 系统巡检、异常问答、历史会话=System inspection, anomaly Q&A, and session history |
| | | 连接中=Connecting |
| | | 一键巡检=One-click Inspection |
| | | 巡检当前系统=Inspect Current System |
| | | 新会话=New Session |
| | | 选择历史会话=Select Session History |
| | | 向 AI 助手提问=Ask the AI Assistant |
| | | 支持连续追问、历史会话切换,以及 AI 思考过程折叠展示。=Supports follow-up questions, session switching, and collapsible AI reasoning. |
| | | 未绑定历史会话=No history session bound |
| | | 新建会话,等待首条消息=Create a new session and wait for the first message |
| | | 输入问题,或先执行一次巡检=Enter a question, or run an inspection first |
| | | AI 正在生成回复...=AI is generating a response... |
| | | AI 助手=AI Assistant |
| | | 用户=User |
| | | WCS 诊断回复=WCS Diagnostic Reply |
| | | 问题输入=Question Input |
| | | 定位堆垛机异常=Locate Crane Anomalies |
| | | 分析堵塞与积压=Analyze Blockages and Backlogs |
| | | 追问最近告警=Ask About Recent Alarms |
| | | 让 AI 主动梳理设备、任务和日志,给出一轮完整巡检。=Let AI review devices, tasks, and logs proactively and provide a full inspection. |
| | | 让 AI 优先关注工位堵塞、任务堆积和节拍异常。=Let AI focus on station blockages, task backlogs, and rhythm anomalies first. |
| | | 把最近异常事件压缩成可执行排查建议。=Condense recent anomalies into actionable troubleshooting steps. |
| | | 帮我定位当前堆垛机相关的异常风险,按可能性从高到低列出。=Identify current crane-related risks and list them by likelihood. |
| | | 帮我总结最近最值得关注的异常,并给出下一步排查动作。=Summarize the most important recent anomalies and suggest next troubleshooting steps. |
| | | 结合近期日志与任务状态,判断是否存在堆垛机链路异常。=Determine whether there is any crane workflow anomaly based on recent logs and task status. |
| | | 例如:最近哪个设备最值得优先排查?异常是否和堆垛机任务、工位堵塞或日志波动有关?=For example: which device should be checked first? Are the anomalies related to crane tasks, station blockage, or log fluctuation? |
| | | 请重点分析当前是否存在工位堵塞、任务积压或节拍异常。=Please focus on whether there are station blockages, task backlogs, or rhythm anomalies. |
| | | 删除会话失败=Failed to delete session |
| | | 加载 AI 会话列表失败=Failed to load AI session list |
| | | 加载会话历史失败=Failed to load session history |
| | | 诊断中=Diagnosing |
| | | 未命名会话=Untitled Session |
| | | 最近更新=Updated Recently |
| | | 刚刚创建=Just created |
| | | 刚刚更新=Just updated |
| | | 会话已绑定=Session Bound |
| | | 临时会话=Temporary Session |
| | | Enter 发送,Shift+Enter 换行=Press Enter to send, Shift+Enter for a new line |
| | | regex\:^(\d+)\s*个会话$=$1 sessions |
| | | regex\:^(\d+)\s*分钟前$=$1 minute(s) ago |
| | | regex\:^(\d+)\s*小时前$=$1 hour(s) ago |
| | | 补发结果=Retry Result |
| | | 补发失败=Retry failed |
| | | 当前筛选条件下没有通知日志=No notification logs match the current filters |
| | | 获取通知队列失败=Failed to load notification queue |
| | | 获取通知概览失败=Failed to load notification overview |
| | | 获取通知日志失败=Failed to load notification logs |
| | | 请求失败=Request failed |
| | | 请选择要补发的队列通知=Please select queue notifications to retry |
| | | 请选择要补发的通知日志=Please select notification logs to retry |
| | | 确定按该日志重新补发通知吗?=Retry this notification based on the selected log? |
| | | 确定补发该队列通知吗?=Retry this queued notification? |
| | | 确定批量补发选中的队列通知吗?=Retry the selected queued notifications in batch? |
| | | 确定批量补发选中的通知日志吗?=Retry the selected notification logs in batch? |
| | | 确定执行手动补发吗?=Are you sure you want to retry manually? |
| | | 上级任务号=Parent Task No. |
| | | 接口响应=API Response |
| | | 通知报文=Notification Payload |
| | | 待发送=Pending |
| | | 发送日志=Send Logs |
| | | regex\:^已选\s*(\d+)\s*条$=Selected $1 item(s) |
| | | regex\:^当前页签:\s*(.+)$=Current Tab: $1 |
| | | 调色盘=Palette |
| | | 恢复默认=Restore Default |
| | | 保存后,新打开的监控地图会直接读取 Redis 配置;已打开页面刷新后即可生效。=After saving, newly opened monitoring maps will read the Redis configuration directly. Refresh already opened pages to apply it. |
| | | regex\:^默认值:\s*(.+)$=Default: $1 |
| | | 操作区域=Action Area |
| | | regex\:^层:\s*(.+)$=Level: $1 |
| | | regex\:^排:\s*(.+)$=Row: $1 |
| | | regex\:^列:\s*(.+)$=Bay: $1 |
| | | regex\:^库位号:\s*(.+)$=Location No.: $1 |
| | | regex\:^库位状态:\s*(.+)$=Location Status: $1 |
| | | regex\:^站点:\s*(.+)$=Station: $1 |
| | | regex\:^工作号:\s*(.+)$=Work No.: $1 |
| | | regex\:^工作类型:\s*(.+)$=Work Type: $1 |
| | | regex\:^工作状态:\s*(.+)$=Work Status: $1 |
| | | regex\:^源站:\s*(.+)$=Source Station: $1 |
| | | regex\:^目标站:\s*(.+)$=Target Station: $1 |
| | | regex\:^源库位:\s*(.+)$=Source Location: $1 |
| | | regex\:^目标库位:\s*(.+)$=Target Location: $1 |
| | | regex\:^自动:\s*(.+)$=Auto: $1 |
| | | regex\:^有物:\s*(.+)$=Loaded: $1 |
| | | regex\:^能入:\s*(.+)$=Inbound Enabled: $1 |
| | | regex\:^能出:\s*(.+)$=Outbound Enabled: $1 |
| | | 地图加载失败=Failed to load map |
| | | 楼层信息加载失败=Failed to load floor information |
| | | 站点详情加载失败=Failed to load station details |
| | | 双工位=Dual Station |
| | | regex\:^设备\s*(.+)$=Device $1 |
| | | regex\:^区域\s*(.+)$=Area $1 |
| | | regex\:^出库站点\s*\((\d+)\)$=Outbound Stations ($1) |
| | | 共=Total |
| | | 跳转时间=Jump Time |
| | | 选择时间=Select Time |
| | | 压缩生成进度=Compression Progress |
| | | 下载接收进度=Download Receive Progress |
| | | 日期选择=Date Selection |
| | | 选中日期=Selected Date |
| | | 设备类型=Device Type |
| | | 起始序号=Start Offset |
| | | 最大文件=Max Files |
| | | 设备列表=Device List |
| | | 暂无数据,请先选择日期=No data. Please select a date first. |
| | | 播放=Play |
| | | 暂停=Pause |
| | | 重置=Reset |
| | | 跳转=Jump |
| | | 倍速=Speed |
| | | regex\:^共\s*(\d+)\s*个设备$=Total $1 devices |
| | | regex\:^下载\((.+)\)$=Download ($1) |
| | | regex\:^可视化\((.+)\)$=Visualize ($1) |
| | | regex\:^未知设备类型\:\s*(.+)$=Unknown device type: $1 |
| | | 不在定位=Off Position |
| | | 在定位=At Position |
| | | 初始化失败=Initialization failed |
| | | 加载日期失败=Failed to load dates |
| | | 加载设备失败=Failed to load devices |
| | | 加载数据中...=Loading data... |
| | | 没有找到日志数据=No log data found |
| | | 目标时间超出日志范围,已跳转至结束时间=Target time exceeds the log range. Moved to the end time. |
| | | 请输入设备编号=Please enter the device number |
| | | 请选择设备类型=Please select a device type |
| | | 数据已全部加载=All data loaded |
| | | 无任务=No Task |
| | | 下载失败或未找到日志=Download failed or logs not found |
| | | 已到达日志末尾,无法到达目标时间=Reached the end of logs and cannot jump to the target time |
| | | 已跳转至目标时间=Jumped to the target time |
| | | 正在跳转至目标时间 (加载中)...=Jumping to the target time (loading)... |
| | | 保存请求异常=Save request exception |
| | | 加载请求异常=Load request exception |
| | | regex\:^保存失败\:\s*(.+)$=Save failed: $1 |
| | | regex\:^加载数据失败\:\s*(.+)$=Failed to load data: $1 |
| | | 该关联已存在=This mapping already exists |
| | | 确定删除该关联吗?=Are you sure you want to delete this mapping? |
| | | regex\:^已建立关联\:\s*站点\s*(.+)$=Mapping created: Station $1 |
| | | 该绑定已存在=This binding already exists |
| | | 请输入区域编码和名称=Please enter the area code and name |
| | | 区域已存在=Area already exists |
| | | regex\:^已建立绑定\:\s*站点\s*(.+)$=Binding created: Station $1 |
| | | 保存站点颜色配置失败=Failed to save station color configuration |
| | | 加载站点颜色配置失败=Failed to load station color configuration |
| | | 颜色格式已自动修正为十六进制=Color format has been normalized to hexadecimal automatically |
| | | 已恢复默认颜色=Default colors restored |
| | | 站点颜色配置已保存=Station color configuration saved |
| | | 登录失败=Login failed |
| | | 确定执行一键激活吗?=Are you sure you want to activate now? |
| | | 系统登录=System Login |
| | | 系统配置信息=System Configuration |
| | | 对接WMS、设备与业务规则=Integrates WMS, devices, and business rules |
| | | 统一编排现场执行任务=Unified orchestration of on-site execution tasks |
| | | 请输入账号和密码进入系统。=Please enter your account and password to access the system. |
| | | 将许可证服务端返回的 license 字段完整粘贴到这里。=Paste the full license field returned by the license service here. |
| | | 许可证 Base64=License Base64 |
| | | 自动化立体仓库与智能物流系统解决方案=Automated AS/RS and intelligent logistics system solutions |
| | | 作业、设备、日志全链路留痕=End-to-end traceability across jobs, devices, and logs |
| | | WCS系统让设备调度、任务执行与现场监控保持在同一套业务链路中。=The WCS keeps device scheduling, task execution, and on-site monitoring within the same business workflow. |
| | | 角色管理=Role Management |
| | | 权限管理=Permission Management |
| | | 菜单列表=Menu List |
| | | 菜单等级=Menu Level |
| | | 父级菜单=Parent Menu |
| | | 请输入编码=Please enter code |
| | | 请输入名称=Please enter name |
| | | 请选择上级=Please select parent |
| | | 请输入菜单编码=Please enter menu code |
| | | 请输入菜单名称=Please enter menu name |
| | | 请输入排序=Please enter sort order |
| | | 请选择类型=Please select type |
| | | 请选择上级菜单=Please select parent menu |
| | | 请选择状态=Please select status |
| | | 请输入接口地址=Please enter API URL |
| | | 请输入权限名称=Please enter permission name |
| | | 请选择所属菜单=Please select menu |
| | | 菜单查询失败=Menu query failed |
| | | 菜单详情加载失败=Failed to load menu details |
| | | 权限加载失败=Failed to load permissions |
| | | 权限树加载失败=Failed to load permission tree |
| | | 权限回显失败=Failed to load assigned permissions |
| | | 权限保存失败=Failed to save permissions |
| | | 角色查询失败=Role query failed |
| | | 角色加载失败=Failed to load roles |
| | | 确定删除选中角色吗?=Delete selected roles? |
| | | 确定删除选中凭证吗?=Delete selected credentials? |
| | | # Playwright audit supplement |
| | | 列设置=Column Settings |
| | | 创建者=Creator |
| | | 主要=Primary |
| | | 编 号=ID |
| | | *编 号=*ID |
| | | 异 常 码=Error Code |
| | | 异 常=Error |
| | | 状 态=Status |
| | | 工 作 号=Work No. |
| | | 备 注=Remarks |
| | | 作 业=Operation |
| | | 命 令=Command |
| | | 目 标 站=Target Station |
| | | 源 站=Source Station |
| | | 源 站 点=Source Station |
| | | 源 库 位=Source Location |
| | | 条 码=Barcode |
| | | 基准排=Base Row |
| | | 基准排-code=Base Row Code |
| | | 基准列=Base Bay |
| | | 基准列-code=Base Bay Code |
| | | 实 现 类=Implementation Class |
| | | 日志ID=Log ID |
| | | 平台密钥=Platform Key |
| | | Time戳=Timestamp |
| | | Exception内容=Exception Content |
| | | Menu列表=Menu List |
| | | 展开All=Expand All |
| | | Permissions管理=Permission Management |
| | | 凭证记录=Credential Records |
| | | 标 识=Identifier |
| | | 字 典 值=Dictionary Value |
| | | 字典文本=Dictionary Text |
| | | 是否Delete=Deleted |
| | | 租 户=Tenant |
| | | 账 号=Account |
| | | 密 码=Password |
| | | 昵 称=Nickname |
| | | 头 像=Avatar |
| | | 工 号=Employee No. |
| | | 性 别=Gender |
| | | 手 机 号=Mobile No. |
| | | 邮 箱=Email |
| | | Email验证=Email Verified |
| | | 所属部门=Department |
| | | 真实姓名=Real Name |
| | | 身份证号=ID Card No. |
| | | 出生日期=Birth Date |
| | | 个人简介=Biography |
| | | 所属机构=Organization |
| | | *角 色=*Role |
| | | 已设置Password=Password Set |
| | | Virtual Device初始化Status=Virtual Device Initialization Status |
| | | Please enter电梯中转点=Please enter the elevator transfer point |
| | | Role管理=Role Management |
| | | 展开面板=Expand Panel |
| | | 收起Actions=Collapse Actions |
| | | 信息=Information |
| | | OKExportExcel吗=Export to Excel? |
| | | 全选=Select All |
| | | 至=to |
| | | regex\:^Success\\s*(\\d+)\\s*/\\s*Failed\\s*(\\d+)\\s*/\\s*连续Failed\\s*(\\d+)$=Success $1 / Failed $2 / Consecutive Failures $3 |
| | | regex\:^(\\d{1,2})日$=Day $1 |
| | | regex\:^\\*[\\s ]*编[\\s ]*号$=*ID |
| | | regex\:^编[\\s ]*号$=ID |
| | | regex\:^异[\\s ]*常$=Error |
| | | regex\:^状[\\s ]*态$=Status |
| | | regex\:^工[\\s ]*作[\\s ]*号$=Work No. |
| | | regex\:^备[\\s ]*注$=Remarks |
| | | regex\:^作[\\s ]*业$=Operation |
| | | regex\:^命[\\s ]*令$=Command |
| | | regex\:^目[\\s ]*标[\\s ]*站$=Target Station |
| | | regex\:^源[\\s ]*站[\\s ]*点$=Source Station |
| | | regex\:^源[\\s ]*站$=Source Station |
| | | regex\:^源[\\s ]*库[\\s ]*位$=Source Location |
| | | regex\:^条[\\s ]*码$=Barcode |
| | | regex\:^账[\\s ]*号$=Account |
| | | regex\:^密[\\s ]*码$=Password |
| | | regex\:^昵[\\s ]*称$=Nickname |
| | | regex\:^头[\\s ]*像$=Avatar |
| | | regex\:^工[\\s ]*号$=Employee No. |
| | | regex\:^性[\\s ]*别$=Gender |
| | | regex\:^邮[\\s ]*箱$=Email |
| | | regex\:^标[\\s ]*识$=Identifier |
| | | regex\:^租[\\s ]*户$=Tenant |
| | | regex\:^\\*[\\s ]*角[\\s ]*色$=*Role |
| | |
| | | common.workPage=Work Page |
| | | common.businessPage=Business Page |
| | | login.title=WCS System V3.0 |
| | | login.subtitle=Please enter your account and password to access the system. |
| | | login.hero.title=The WCS keeps device scheduling, task execution, and on-site monitoring within the same business workflow. |
| | | login.hero.subtitle=The Warehouse Control System is built for automated AS/RS execution, coordinating cranes, RGVs, conveyor stations, task commands, location status, and log tracing through unified dispatch and visualization. It helps warehouse operations stay stable, traceable, and collaborative. Zhejiang Zhongyang Warehouse Technology Co., Ltd. focuses on automated AS/RS and intelligent intralogistics systems, covering solution design, software control, equipment integration, and project delivery. |
| | | login.hero.metric.dispatch.title=Dispatch |
| | | login.hero.metric.dispatch.desc=Unified orchestration of on-site execution tasks |
| | | login.hero.metric.trace.title=Traceability |
| | | login.hero.metric.trace.desc=End-to-end traceability across jobs, devices, and logs |
| | | login.hero.metric.integration.title=Integration |
| | | login.hero.metric.integration.desc=Integrates WMS, devices, and business rules |
| | | login.hero.company.name=Zhejiang Zhongyang Warehouse Technology Co., Ltd. |
| | | login.hero.company.solution=Automated AS/RS and intelligent logistics system solutions |
| | | login.username=Account |
| | | login.password=Password |
| | | login.submit=Sign In |
| | | login.passkey.submit=Sign In with Passkey |
| | | login.passkey.tip=Use device biometrics or a security key to sign in. Entering the account narrows the credential scope; leaving it blank tries discoverable credentials. |
| | | login.passkey.browserUnsupported=This browser does not support passkeys. Use a recent Chrome, Edge, or Safari build |
| | | login.passkey.secureContext=Passkeys require HTTPS or localhost |
| | | login.tools.title=System Tools |
| | | login.tools.recommended=Recommended Actions |
| | | login.tools.recommendedDesc=Use "Get Request Code" and "Activate" first to complete license application and activation. |
| | |
| | | login.serverInfo.title=System Configuration |
| | | login.serverInfo.label=System Configuration |
| | | login.serverInfo.tip=Legacy projects can still use this hardware JSON to apply for a license. |
| | | login.dialog.close=Close |
| | | login.mfa.title=MFA Verification |
| | | login.mfa.tip=Account and password verified. Enter the 6-digit code from your authenticator app to continue signing in. |
| | | login.mfa.currentAccount=Current account: |
| | | login.mfa.codeLabel=Verification Code |
| | | login.mfa.codePlaceholder=Please enter the 6-digit code |
| | | login.mfa.cancel=Cancel |
| | | login.mfa.submit=Verify and Sign In |
| | | login.license.label=License Base64 |
| | | login.license.tip=Paste the full license field returned by the license service here. |
| | | login.license.submit=Submit |
| | | login.license.success=License updated successfully |
| | | login.validation.usernameRequired=Please enter the account |
| | | login.validation.passwordRequired=Please enter the password |
| | | login.validation.mfaRequired=Please enter the 6-digit verification code |
| | | login.validation.mfaInvalid=Please enter a 6-digit numeric verification code |
| | | login.error.loginFailed=Login failed |
| | | login.error.passkeyOptionsFailed=Failed to load passkey sign-in options |
| | | login.error.passkeyVerifyFailed=Passkey verification failed |
| | | login.error.mfaTicketExpired=The login ticket has expired. Please sign in again |
| | | login.error.mfaFailed=Verification failed |
| | | login.error.requestCodeFailed=Failed to get request code |
| | | login.error.serverInfoFailed=Failed to get system configuration |
| | | login.error.licenseEmpty=License content cannot be empty |
| | | login.error.licenseUpdateFailed=Failed to update license |
| | | login.error.licenseImportFailed=Failed to import license |
| | | login.activate.confirm=Are you sure you want to activate now? |
| | | login.activate.success=Activation successful |
| | | login.activate.failed=Activation failed |
| | | login.projectName.title=Project Name |
| | | login.error.projectNameFailed=Failed to get project name |
| | | index.searchMenu=Search menu |
| | | index.noMatchedMenu=No matching menus |
| | | index.noAvailableMenu=No available menus for current account |
| | |
| | | response.user.notFound=Account does not exist |
| | | response.user.disabled=Account is disabled |
| | | response.user.passwordMismatch=Incorrect password |
| | | response.user.passkeySecureContextRequired=Passkeys require HTTPS or localhost |
| | | response.user.passkeyNotBound=This account has not bound a passkey yet |
| | | response.user.passkeyTicketExpired=The passkey login ticket has expired. Please try again |
| | | response.user.passkeyCounterMismatch=The passkey signature counter is invalid. Rebind the passkey and try again |
| | | response.user.passkeyVerifyFailed=Passkey verification failed |
| | | response.user.passkeyAlreadyBound=This account already has a passkey bound. Remove it before binding another one |
| | | response.user.passkeyCredentialExists=This passkey is already bound to another account |
| | | response.user.passkeyRegisterFailed=Failed to bind the passkey |
| | | response.user.oldPasswordMismatch=Current password is incorrect |
| | | response.user.mfaNotAllowed=MFA is not enabled for this account |
| | | response.user.mfaNotEnabled=MFA is not configured for this account |
| | | response.user.mfaCodeMismatch=Incorrect MFA verification code |
| | | response.user.mfaTicketExpired=The MFA login ticket has expired. Please sign in again |
| | | response.system.licenseExpired=License has expired |
| | | response.common.systemError=System error. Please try again later. |
| | | response.common.methodNotAllowed=Request method not supported |
| | |
| | | common.workPage=工作页面 |
| | | common.businessPage=业务页面 |
| | | login.title=WCS系统V3.0 |
| | | login.subtitle=请输入账号和密码进入系统。 |
| | | login.hero.title=WCS系统让设备调度、任务执行与现场监控保持在同一套业务链路中。 |
| | | login.hero.subtitle=Warehouse Control System 面向自动化立体仓库现场执行层,围绕堆垛机、RGV、输送站台、任务指令、库位状态和日志追踪进行统一调度与可视化管理,帮助仓储系统实现稳定、可追溯、可联动的作业控制。浙江中扬立库技术有限公司长期专注于自动化立体仓库与智能物流系统建设,覆盖方案设计、软件控制、设备集成与项目实施交付。 |
| | | login.hero.metric.dispatch.title=调度 |
| | | login.hero.metric.dispatch.desc=统一编排现场执行任务 |
| | | login.hero.metric.trace.title=追溯 |
| | | login.hero.metric.trace.desc=作业、设备、日志全链路留痕 |
| | | login.hero.metric.integration.title=集成 |
| | | login.hero.metric.integration.desc=对接WMS、设备与业务规则 |
| | | login.hero.company.name=浙江中扬立库技术有限公司 |
| | | login.hero.company.solution=自动化立体仓库与智能物流系统解决方案 |
| | | login.username=账号 |
| | | login.password=密码 |
| | | login.submit=登录 |
| | | login.passkey.submit=通行密钥登录 |
| | | login.passkey.tip=支持使用设备生物识别或安全密钥登录。可先输入账号缩小凭证范围,留空则尝试发现式登录。 |
| | | login.passkey.browserUnsupported=当前浏览器不支持通行密钥,请使用最新版 Chrome、Edge 或 Safari |
| | | login.passkey.secureContext=通行密钥仅支持在 HTTPS 或 localhost 环境下使用 |
| | | login.tools.title=系统工具 |
| | | login.tools.recommended=推荐操作 |
| | | login.tools.recommendedDesc=优先使用“获取请求码”和“一键激活”完成许可证申请与激活。 |
| | |
| | | login.serverInfo.title=获取系统配置 |
| | | login.serverInfo.label=系统配置信息 |
| | | login.serverInfo.tip=老项目仍可继续使用这份硬件信息 JSON 申请许可证。 |
| | | login.dialog.close=关闭 |
| | | login.mfa.title=MFA二次验证 |
| | | login.mfa.tip=账号密码已通过,请输入身份验证器中的 6 位动态验证码后继续登录。 |
| | | login.mfa.currentAccount=当前账号: |
| | | login.mfa.codeLabel=验证码 |
| | | login.mfa.codePlaceholder=请输入6位动态码 |
| | | login.mfa.cancel=取消 |
| | | login.mfa.submit=验证并登录 |
| | | login.license.label=许可证 Base64 |
| | | login.license.tip=将许可证服务端返回的 license 字段完整粘贴到这里。 |
| | | login.license.submit=提交 |
| | | login.license.success=许可证更新成功 |
| | | login.validation.usernameRequired=请输入账号 |
| | | login.validation.passwordRequired=请输入密码 |
| | | login.validation.mfaRequired=请输入6位验证码 |
| | | login.validation.mfaInvalid=请输入6位数字验证码 |
| | | login.error.loginFailed=登录失败 |
| | | login.error.passkeyOptionsFailed=获取通行密钥登录参数失败 |
| | | login.error.passkeyVerifyFailed=通行密钥验证失败 |
| | | login.error.mfaTicketExpired=登录票据已失效,请重新登录 |
| | | login.error.mfaFailed=验证失败 |
| | | login.error.requestCodeFailed=获取请求码失败 |
| | | login.error.serverInfoFailed=获取系统配置信息失败 |
| | | login.error.licenseEmpty=许可证内容不能为空 |
| | | login.error.licenseUpdateFailed=许可证更新失败 |
| | | login.error.licenseImportFailed=许可证录入失败 |
| | | login.activate.confirm=确定执行一键激活吗? |
| | | login.activate.success=激活成功 |
| | | login.activate.failed=激活失败 |
| | | login.projectName.title=项目名称 |
| | | login.error.projectNameFailed=获取项目名称失败 |
| | | index.searchMenu=搜索菜单 |
| | | index.noMatchedMenu=没有匹配菜单 |
| | | index.noAvailableMenu=当前账号没有可用菜单 |
| | |
| | | response.user.notFound=账号不存在 |
| | | response.user.disabled=账号已被禁用 |
| | | response.user.passwordMismatch=密码错误 |
| | | response.user.passkeySecureContextRequired=通行密钥仅支持在 HTTPS 或 localhost 环境下使用 |
| | | response.user.passkeyNotBound=当前账号尚未绑定通行密钥 |
| | | response.user.passkeyTicketExpired=通行密钥登录票据已失效,请重新发起 |
| | | response.user.passkeyCounterMismatch=通行密钥签名计数异常,请重新绑定后再试 |
| | | response.user.passkeyVerifyFailed=通行密钥验证失败 |
| | | response.user.passkeyAlreadyBound=当前账号已绑定通行密钥,如需更换请先解绑 |
| | | response.user.passkeyCredentialExists=该通行密钥已绑定其他账号 |
| | | response.user.passkeyRegisterFailed=通行密钥绑定失败 |
| | | response.user.oldPasswordMismatch=当前密码错误 |
| | | response.user.mfaNotAllowed=当前账号未开通MFA使用权限 |
| | | response.user.mfaNotEnabled=当前账号未启用MFA |
| | | response.user.mfaCodeMismatch=MFA验证码错误 |
| | | response.user.mfaTicketExpired=MFA登录票据已失效,请重新输入账号密码 |
| | | response.system.licenseExpired=许可证已失效 |
| | | response.common.systemError=系统异常,请稍后重试 |
| | | response.common.methodNotAllowed=请求方式不支持 |
| | |
| | | <result column="username" property="username" /> |
| | | <result column="mobile" property="mobile" /> |
| | | <result column="password" property="password" /> |
| | | <result column="mfa_allow" property="mfaAllow" /> |
| | | <result column="mfa_enabled" property="mfaEnabled" /> |
| | | <result column="mfa_bound_time" property="mfaBoundTime" /> |
| | | <result column="passkey_name" property="passkeyName" /> |
| | | <result column="passkey_bound_time" property="passkeyBoundTime" /> |
| | | <result column="passkey_last_used_time" property="passkeyLastUsedTime" /> |
| | | <result column="role_id" property="roleId" /> |
| | | <result column="create_time" property="createTime" /> |
| | | <result column="status" property="status" /> |
| | | |
| | | </resultMap> |
| | | |
| | | <resultMap id="SecurityResultMap" type="com.zy.system.entity.User" extends="BaseResultMap"> |
| | | <result column="mfa_secret" property="mfaSecret" /> |
| | | <result column="passkey_credential_id" property="passkeyCredentialId" /> |
| | | <result column="passkey_public_key" property="passkeyPublicKey" /> |
| | | <result column="passkey_algorithm" property="passkeyAlgorithm" /> |
| | | <result column="passkey_sign_count" property="passkeySignCount" /> |
| | | <result column="passkey_transports" property="passkeyTransports" /> |
| | | </resultMap> |
| | | |
| | | <select id="selectByMobileWithSecurity" resultMap="SecurityResultMap"> |
| | | select |
| | | id, |
| | | host_id, |
| | | username, |
| | | mobile, |
| | | password, |
| | | mfa_allow, |
| | | mfa_enabled, |
| | | mfa_secret, |
| | | mfa_bound_time, |
| | | passkey_name, |
| | | passkey_credential_id, |
| | | passkey_public_key, |
| | | passkey_algorithm, |
| | | passkey_sign_count, |
| | | passkey_transports, |
| | | passkey_bound_time, |
| | | passkey_last_used_time, |
| | | role_id, |
| | | create_time, |
| | | status |
| | | from sys_user |
| | | where mobile = #{mobile} |
| | | limit 1 |
| | | </select> |
| | | |
| | | <select id="selectByIdWithSecurity" resultMap="SecurityResultMap"> |
| | | select |
| | | id, |
| | | host_id, |
| | | username, |
| | | mobile, |
| | | password, |
| | | mfa_allow, |
| | | mfa_enabled, |
| | | mfa_secret, |
| | | mfa_bound_time, |
| | | passkey_name, |
| | | passkey_credential_id, |
| | | passkey_public_key, |
| | | passkey_algorithm, |
| | | passkey_sign_count, |
| | | passkey_transports, |
| | | passkey_bound_time, |
| | | passkey_last_used_time, |
| | | role_id, |
| | | create_time, |
| | | status |
| | | from sys_user |
| | | where id = #{id} |
| | | limit 1 |
| | | </select> |
| | | |
| | | <select id="selectByPasskeyCredentialId" resultMap="SecurityResultMap"> |
| | | select |
| | | id, |
| | | host_id, |
| | | username, |
| | | mobile, |
| | | password, |
| | | mfa_allow, |
| | | mfa_enabled, |
| | | mfa_secret, |
| | | mfa_bound_time, |
| | | passkey_name, |
| | | passkey_credential_id, |
| | | passkey_public_key, |
| | | passkey_algorithm, |
| | | passkey_sign_count, |
| | | passkey_transports, |
| | | passkey_bound_time, |
| | | passkey_last_used_time, |
| | | role_id, |
| | | create_time, |
| | | status |
| | | from sys_user |
| | | where passkey_credential_id = #{credentialId} |
| | | limit 1 |
| | | </select> |
| | | </mapper> |
| New file |
| | |
| | | -- sys_user 增加 MFA 相关字段 |
| | | -- 用途:支持账号级多因子登录授权、绑定和校验 |
| | | -- 适用数据库:MySQL |
| | | |
| | | SET @current_db := DATABASE(); |
| | | |
| | | SET @mfa_allow_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_user' |
| | | AND COLUMN_NAME = 'mfa_allow' |
| | | ); |
| | | |
| | | SET @add_mfa_allow_sql := IF( |
| | | @mfa_allow_exists = 0, |
| | | 'ALTER TABLE sys_user ADD COLUMN mfa_allow INT NOT NULL DEFAULT 0 COMMENT ''是否允许使用MFA'' AFTER password', |
| | | 'SELECT ''column mfa_allow already exists'' ' |
| | | ); |
| | | PREPARE stmt_mfa_allow FROM @add_mfa_allow_sql; |
| | | EXECUTE stmt_mfa_allow; |
| | | DEALLOCATE PREPARE stmt_mfa_allow; |
| | | |
| | | SET @mfa_enabled_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_user' |
| | | AND COLUMN_NAME = 'mfa_enabled' |
| | | ); |
| | | |
| | | SET @add_mfa_enabled_sql := IF( |
| | | @mfa_enabled_exists = 0, |
| | | 'ALTER TABLE sys_user ADD COLUMN mfa_enabled INT NOT NULL DEFAULT 0 COMMENT ''是否已启用MFA'' AFTER mfa_allow', |
| | | 'SELECT ''column mfa_enabled already exists'' ' |
| | | ); |
| | | PREPARE stmt_mfa_enabled FROM @add_mfa_enabled_sql; |
| | | EXECUTE stmt_mfa_enabled; |
| | | DEALLOCATE PREPARE stmt_mfa_enabled; |
| | | |
| | | SET @mfa_secret_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_user' |
| | | AND COLUMN_NAME = 'mfa_secret' |
| | | ); |
| | | |
| | | SET @add_mfa_secret_sql := IF( |
| | | @mfa_secret_exists = 0, |
| | | 'ALTER TABLE sys_user ADD COLUMN mfa_secret VARCHAR(128) NULL COMMENT ''MFA密钥'' AFTER mfa_enabled', |
| | | 'SELECT ''column mfa_secret already exists'' ' |
| | | ); |
| | | PREPARE stmt_mfa_secret FROM @add_mfa_secret_sql; |
| | | EXECUTE stmt_mfa_secret; |
| | | DEALLOCATE PREPARE stmt_mfa_secret; |
| | | |
| | | SET @mfa_bound_time_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_user' |
| | | AND COLUMN_NAME = 'mfa_bound_time' |
| | | ); |
| | | |
| | | SET @add_mfa_bound_time_sql := IF( |
| | | @mfa_bound_time_exists = 0, |
| | | 'ALTER TABLE sys_user ADD COLUMN mfa_bound_time DATETIME NULL COMMENT ''MFA绑定时间'' AFTER mfa_secret', |
| | | 'SELECT ''column mfa_bound_time already exists'' ' |
| | | ); |
| | | PREPARE stmt_mfa_bound_time FROM @add_mfa_bound_time_sql; |
| | | EXECUTE stmt_mfa_bound_time; |
| | | DEALLOCATE PREPARE stmt_mfa_bound_time; |
| | | |
| | | SHOW COLUMNS FROM sys_user LIKE 'mfa_allow'; |
| | | SHOW COLUMNS FROM sys_user LIKE 'mfa_enabled'; |
| | | SHOW COLUMNS FROM sys_user LIKE 'mfa_secret'; |
| | | SHOW COLUMNS FROM sys_user LIKE 'mfa_bound_time'; |
| New file |
| | |
| | | -- 用途:支持账号绑定单个通行密钥并通过通行密钥登录 |
| | | -- 适用表:sys_user |
| | | |
| | | SET @table_exists := ( |
| | | SELECT COUNT(*) |
| | | FROM information_schema.tables |
| | | WHERE table_schema = DATABASE() |
| | | AND table_name = 'sys_user' |
| | | ); |
| | | |
| | | SET @sql := IF(@table_exists = 0, |
| | | 'SELECT ''sys_user not found''', |
| | | 'ALTER TABLE sys_user |
| | | ADD COLUMN passkey_name VARCHAR(100) NULL COMMENT ''通行密钥名称'' AFTER mfa_bound_time, |
| | | ADD COLUMN passkey_credential_id VARCHAR(255) NULL COMMENT ''通行密钥凭证ID'' AFTER passkey_name, |
| | | ADD COLUMN passkey_public_key TEXT NULL COMMENT ''通行密钥公钥SPKI'' AFTER passkey_credential_id, |
| | | ADD COLUMN passkey_algorithm INT NULL COMMENT ''通行密钥算法'' AFTER passkey_public_key, |
| | | ADD COLUMN passkey_sign_count BIGINT NOT NULL DEFAULT 0 COMMENT ''通行密钥签名计数器'' AFTER passkey_algorithm, |
| | | ADD COLUMN passkey_transports VARCHAR(255) NULL COMMENT ''通行密钥传输方式'' AFTER passkey_sign_count, |
| | | ADD COLUMN passkey_bound_time DATETIME NULL COMMENT ''通行密钥绑定时间'' AFTER passkey_transports, |
| | | ADD COLUMN passkey_last_used_time DATETIME NULL COMMENT ''通行密钥最近使用时间'' AFTER passkey_bound_time' |
| | | ); |
| | | |
| | | PREPARE stmt FROM @sql; |
| | | EXECUTE stmt; |
| | | DEALLOCATE PREPARE stmt; |
| | |
| | | }, |
| | | shelfTooltipMinScale: 0.4, |
| | | containerResizeObserver: null, |
| | | resizeDebounceTimer: null, |
| | | timer: null, |
| | | adjustLabelTimer: null, |
| | | isSwitchingFloor: false, |
| | |
| | | |
| | | if (this.hoverRaf) { cancelAnimationFrame(this.hoverRaf); this.hoverRaf = null; } |
| | | if (this.shelfCullRaf) { cancelAnimationFrame(this.shelfCullRaf); this.shelfCullRaf = null; } |
| | | if (this.resizeDebounceTimer) { clearTimeout(this.resizeDebounceTimer); this.resizeDebounceTimer = null; } |
| | | if (window.gsap && this.pixiApp && this.pixiApp.stage) { window.gsap.killTweensOf(this.pixiApp.stage.position); } |
| | | if (this.pixiApp) { this.pixiApp.destroy(true, { children: true }); } |
| | | if (this.containerResizeObserver) { this.containerResizeObserver.disconnect(); this.containerResizeObserver = null; } |
| | | window.removeEventListener('resize', this.resizeToContainer); |
| | | window.removeEventListener('resize', this.scheduleResizeToContainer); |
| | | if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; } |
| | | if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { try { this.ws.close(); } catch (e) {} } |
| | | }, |
| | |
| | | this.pixiApp.view.style.height = '100%'; |
| | | this.pixiApp.view.style.display = 'block'; |
| | | this.resizeToContainer(); |
| | | window.addEventListener('resize', this.resizeToContainer); |
| | | window.addEventListener('resize', this.scheduleResizeToContainer); |
| | | this.graphicsCrnTrack = this.createTrackTexture(25, 25, 10); |
| | | this.graphicsRgvTrack = this.createTrackTexture(25, 25, 10); |
| | | this.objectsContainer = new PIXI.Container(); |
| | |
| | | startContainerResizeObserve() { |
| | | if (typeof ResizeObserver === 'undefined' || !this.$el) { return; } |
| | | this.containerResizeObserver = new ResizeObserver(() => { |
| | | this.resizeToContainer(); |
| | | this.scheduleResizeToContainer(); |
| | | }); |
| | | this.containerResizeObserver.observe(this.$el); |
| | | }, |
| | | scheduleResizeToContainer() { |
| | | if (this.resizeDebounceTimer) { |
| | | clearTimeout(this.resizeDebounceTimer); |
| | | } |
| | | this.resizeDebounceTimer = setTimeout(() => { |
| | | this.resizeDebounceTimer = null; |
| | | this.resizeToContainer(); |
| | | }, 80); |
| | | }, |
| | | getViewportSize() { |
| | | if (!this.pixiApp || !this.pixiApp.renderer) { return { width: 0, height: 0 }; } |
| | |
| | | } |
| | | } |
| | | }); |
| | | |
| | | |
| | | |
| | | |
| | |
| | | return result; |
| | | } |
| | | |
| | | fieldMeta = [ |
| | | { |
| | | field: 'id', |
| | | columnName: 'id', |
| | | label: 'ID', |
| | | tableProp: 'id', |
| | | exportField: 'id', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: true, |
| | | searchable: false, |
| | | sortable: true, |
| | | textarea: false, |
| | | minWidth: 90, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'name', |
| | | columnName: 'name', |
| | | label: '名称', |
| | | tableProp: 'name', |
| | | exportField: 'name', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: true, |
| | | primaryKey: false, |
| | | searchable: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 180, |
| | | dialogSpan: 12, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'code', |
| | | columnName: 'code', |
| | | label: '编码', |
| | | tableProp: 'code', |
| | | exportField: 'code', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: true, |
| | | primaryKey: false, |
| | | searchable: true, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 180, |
| | | dialogSpan: 12, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'value', |
| | | columnName: 'value', |
| | | label: '对应值', |
| | | tableProp: 'value', |
| | | exportField: 'value', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: true, |
| | | primaryKey: false, |
| | | searchable: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 220, |
| | | dialogSpan: 24, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'type', |
| | | columnName: 'type', |
| | | label: '类型', |
| | | tableProp: 'type$', |
| | | exportField: 'type$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: false, |
| | | searchable: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | dialogSpan: 12, |
| | | enumOptions: [ |
| | | { rawValue: '1', label: 'String' }, |
| | | { rawValue: '2', label: 'JSON' } |
| | | ], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'status', |
| | | columnName: 'status', |
| | | label: '状态', |
| | | tableProp: 'status$', |
| | | exportField: 'status$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: false, |
| | | searchable: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | dialogSpan: 12, |
| | | enumOptions: [ |
| | | { rawValue: '1', label: '正常' }, |
| | | { rawValue: '0', label: '禁用' } |
| | | ], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'selectType', |
| | | columnName: 'select_type', |
| | | label: '筛选类型', |
| | | tableProp: 'selectType', |
| | | exportField: 'selectType', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | searchable: true, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 140, |
| | | dialogSpan: 24, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | } |
| | | ]; |
| | | |
| | | function isEmptyValue(value) { |
| | | return value === null || value === undefined || value === ''; |
| | | } |
| | |
| | | } |
| | | |
| | | function isSearchableField(field) { |
| | | return !!field && field.kind !== 'image' && !field.textarea; |
| | | return !!field && field.searchable !== false && field.kind !== 'image' && !field.textarea; |
| | | } |
| | | |
| | | function isSortableField(field) { |
| | | if (!field) { |
| | | return false; |
| | | } |
| | | if (field.sortable === true) { |
| | | return true; |
| | | } |
| | | if (field.sortable === false) { |
| | | return false; |
| | | } |
| | | if (field.primaryKey) { |
| | |
| | | mode: 'create', |
| | | submitting: false |
| | | }, |
| | | tableHeight: 420, |
| | | layoutTimer: null, |
| | | tableResizeHandler: null, |
| | | dialogForm: createFormDefaults(), |
| | |
| | | }; |
| | | }); |
| | | }, |
| | | tableHeight: function () { |
| | | return this.advancedFiltersVisible && this.hasAdvancedFilters |
| | | ? 'calc(100vh - 390px)' |
| | | : 'calc(100vh - 300px)'; |
| | | }, |
| | | formTarget: function () { |
| | | return this.dialogForm; |
| | | }, |
| | | displayTarget: function () { |
| | | return this.dialogDisplay; |
| | | }, |
| | | isDialogReadonly: function () { |
| | | return this.dialog.mode === 'detail'; |
| | | } |
| | | }, |
| | | created: function () { |
| | |
| | | } |
| | | }, |
| | | methods: $.extend({}, sharedMethods, { |
| | | calculateTableHeight: function () { |
| | | var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 860; |
| | | var tableWrap = this.$refs.tableWrap; |
| | | var pagerBar = this.$refs.pagerBar; |
| | | if (!tableWrap) { |
| | | this.tableHeight = Math.max(360, viewportHeight - (this.advancedFiltersVisible ? 360 : 300)); |
| | | return; |
| | | } |
| | | var tableTop = tableWrap.getBoundingClientRect().top; |
| | | var pagerHeight = pagerBar ? pagerBar.offsetHeight : 56; |
| | | var bottomGap = 56; |
| | | this.tableHeight = Math.max(320, Math.floor(viewportHeight - tableTop - pagerHeight - bottomGap)); |
| | | }, |
| | | requestTableLayout: function (delay) { |
| | | var self = this; |
| | | if (self.layoutTimer) { |
| | | clearTimeout(self.layoutTimer); |
| | | } |
| | | self.$nextTick(function () { |
| | | self.calculateTableHeight(); |
| | | self.layoutTimer = setTimeout(function () { |
| | | var table = self.$refs.dataTable; |
| | | if (table && typeof table.doLayout === 'function') { |
| | |
| | | } |
| | | }); |
| | | }, |
| | | openDetailDialog: function (row) { |
| | | var self = this; |
| | | self.dialog.mode = 'detail'; |
| | | self.dialog.visible = true; |
| | | self.$nextTick(function () { |
| | | self.resetDialogState(); |
| | | fillFormFromRow(row, self.dialogForm, self.dialogDisplay); |
| | | if (self.$refs.dialogForm) { |
| | | self.$refs.dialogForm.clearValidate(); |
| | | } |
| | | }); |
| | | }, |
| | | submitDialog: function () { |
| | | var self = this; |
| | | if (self.dialog.mode === 'detail') { |
| | | return; |
| | | } |
| | | if (!self.$refs.dialogForm) { |
| | | return; |
| | | } |
| | |
| | | self.$message.error('导出失败'); |
| | | } |
| | | }); |
| | | }, |
| | | refreshCache: function () { |
| | | var self = this; |
| | | self.$confirm('确定刷新Redis缓存吗?', '提示', { type: 'warning' }).then(function () { |
| | | $.ajax({ |
| | | url: baseUrl + '/config/refreshCache', |
| | | method: 'POST', |
| | | headers: self.authHeaders(), |
| | | success: function (res) { |
| | | if (self.handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '刷新缓存失败'); |
| | | return; |
| | | } |
| | | self.$message.success('刷新缓存成功'); |
| | | self.loadTable(); |
| | | }, |
| | | error: function () { |
| | | self.$message.error('刷新缓存失败'); |
| | | } |
| | | }); |
| | | }).catch(function () {}); |
| | | } |
| | | }) |
| | | }); |
| | |
| | | saving: false, |
| | | passwordDialogVisible: false, |
| | | passwordSaving: false, |
| | | mfaDialogVisible: false, |
| | | mfaDialogMode: "enable", |
| | | mfaSetupLoading: false, |
| | | mfaSubmitting: false, |
| | | passkeyDialogVisible: false, |
| | | passkeyDialogMode: "register", |
| | | passkeySubmitting: false, |
| | | form: { |
| | | id: "", |
| | | roleName: "", |
| | | username: "", |
| | | mobile: "", |
| | | password: "", |
| | | createTime$: "" |
| | | createTime$: "", |
| | | mfaAllow: 0, |
| | | mfaAllow$: "否", |
| | | mfaEnabled: 0, |
| | | mfaEnabled$: "否", |
| | | mfaBoundTime$: "", |
| | | mfaMaskedSecret: "", |
| | | passkeyBound: false, |
| | | passkeyName: "", |
| | | passkeyBoundTime$: "", |
| | | passkeyLastUsedTime$: "", |
| | | passkeyTransports: "" |
| | | }, |
| | | passwordForm: { |
| | | oldPassword: "", |
| | | password: "", |
| | | rePassword: "" |
| | | }, |
| | | mfaForm: { |
| | | currentPassword: "", |
| | | code: "" |
| | | }, |
| | | mfaSetup: { |
| | | secret: "", |
| | | qrCode: "", |
| | | otpAuth: "" |
| | | }, |
| | | passkeyForm: { |
| | | name: "", |
| | | currentPassword: "" |
| | | }, |
| | | rules: { |
| | | username: [ |
| | |
| | | }, |
| | | passwordRules: { |
| | | oldPassword: [ |
| | | { required: true, message: "请输入当前密码", trigger: "blur" }, |
| | | { |
| | | validator: function (rule, value, callback) { |
| | | if (!value) { |
| | | callback(new Error("请输入当前密码")); |
| | | return; |
| | | } |
| | | if (!this.form.password) { |
| | | callback(new Error("未读取到当前用户密码")); |
| | | return; |
| | | } |
| | | if (hex_md5(value) !== this.form.password) { |
| | | callback(new Error("密码不匹配")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }.bind(this), |
| | | trigger: "blur" |
| | | } |
| | | { required: true, message: "请输入当前密码", trigger: "blur" } |
| | | ], |
| | | password: [ |
| | | { required: true, message: "请输入新密码", trigger: "blur" }, |
| | |
| | | callback(new Error("不能少于4个字符")); |
| | | return; |
| | | } |
| | | if (this.form.password && hex_md5(value) === this.form.password) { |
| | | if (value === this.passwordForm.oldPassword) { |
| | | callback(new Error("与旧密码不能相同")); |
| | | return; |
| | | } |
| | |
| | | }.bind(this), |
| | | trigger: "blur" |
| | | } |
| | | ] |
| | | }, |
| | | mfaRules: { |
| | | currentPassword: [ |
| | | { required: true, message: "请输入当前密码", trigger: "blur" } |
| | | ], |
| | | code: [ |
| | | { required: true, message: "请输入6位验证码", trigger: "blur" }, |
| | | { |
| | | validator: function (rule, value, callback) { |
| | | if (!/^\d{6}$/.test(String(value || "").trim())) { |
| | | callback(new Error("请输入6位数字验证码")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }, |
| | | trigger: "blur" |
| | | } |
| | | ] |
| | | }, |
| | | passkeyRules: { |
| | | currentPassword: [ |
| | | { required: true, message: "请输入当前密码", trigger: "blur" } |
| | | ] |
| | | } |
| | | }; |
| | |
| | | var vm = this; |
| | | vm.passwordSaving = true; |
| | | $.ajax({ |
| | | url: baseUrl + "/user/update/auth", |
| | | url: baseUrl + "/user/password/update/auth", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | data: { |
| | | id: vm.form.id, |
| | | oldPassword: hex_md5(vm.passwordForm.oldPassword), |
| | | password: hex_md5(vm.passwordForm.password) |
| | | }, |
| | | method: "POST", |
| | |
| | | vm.$message.error(res.msg || "密码修改失败"); |
| | | return; |
| | | } |
| | | vm.form.password = hex_md5(vm.passwordForm.password); |
| | | vm.passwordDialogVisible = false; |
| | | vm.$alert("密码修改成功,请重新登录", "提示", { |
| | | confirmButtonText: "确定", |
| | |
| | | } |
| | | }); |
| | | }, |
| | | openMfaEnableDialog: function () { |
| | | if (Number(this.form.mfaAllow) !== 1) { |
| | | this.$message.warning("当前账号未开通MFA使用权限"); |
| | | return; |
| | | } |
| | | this.mfaDialogMode = "enable"; |
| | | this.resetMfaDialogState(); |
| | | this.mfaDialogVisible = true; |
| | | this.loadMfaSetup(); |
| | | }, |
| | | openMfaDisableDialog: function () { |
| | | this.mfaDialogMode = "disable"; |
| | | this.resetMfaDialogState(); |
| | | this.mfaDialogVisible = true; |
| | | }, |
| | | closeMfaDialog: function () { |
| | | this.mfaDialogVisible = false; |
| | | this.resetMfaDialogState(); |
| | | }, |
| | | resetMfaDialogState: function () { |
| | | this.mfaSubmitting = false; |
| | | this.mfaSetupLoading = false; |
| | | this.mfaForm = { |
| | | currentPassword: "", |
| | | code: "" |
| | | }; |
| | | this.mfaSetup = { |
| | | secret: "", |
| | | qrCode: "", |
| | | otpAuth: "" |
| | | }; |
| | | if (this.$refs.mfaForm) { |
| | | this.$refs.mfaForm.clearValidate(); |
| | | } |
| | | }, |
| | | loadMfaSetup: function () { |
| | | var vm = this; |
| | | vm.mfaSetupLoading = true; |
| | | $.ajax({ |
| | | url: baseUrl + "/user/mfa/setup/auth", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (Number(res.code) !== 200) { |
| | | vm.$message.error(res.msg || "MFA配置加载失败"); |
| | | return; |
| | | } |
| | | vm.mfaSetup = Object.assign({}, vm.mfaSetup, res.data || {}); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("MFA配置加载失败"); |
| | | }, |
| | | complete: function () { |
| | | vm.mfaSetupLoading = false; |
| | | } |
| | | }); |
| | | }, |
| | | handleMfaSubmit: function () { |
| | | var vm = this; |
| | | if (!vm.$refs.mfaForm) { |
| | | return; |
| | | } |
| | | vm.$refs.mfaForm.validate(function (valid) { |
| | | if (!valid) { |
| | | return false; |
| | | } |
| | | if (vm.mfaDialogMode === "enable" && !vm.mfaSetup.secret) { |
| | | vm.$message.error("MFA密钥尚未生成,请稍后重试"); |
| | | return false; |
| | | } |
| | | vm.submitMfaAction(); |
| | | return true; |
| | | }); |
| | | }, |
| | | submitMfaAction: function () { |
| | | var vm = this; |
| | | vm.mfaSubmitting = true; |
| | | $.ajax({ |
| | | url: baseUrl + (vm.mfaDialogMode === "enable" ? "/user/mfa/enable/auth" : "/user/mfa/disable/auth"), |
| | | headers: { token: localStorage.getItem("token") }, |
| | | data: { |
| | | currentPassword: hex_md5(vm.mfaForm.currentPassword), |
| | | code: vm.mfaForm.code, |
| | | secret: vm.mfaSetup.secret |
| | | }, |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (Number(res.code) !== 200) { |
| | | vm.$message.error(res.msg || "MFA操作失败"); |
| | | return; |
| | | } |
| | | vm.$message.success(vm.mfaDialogMode === "enable" ? "MFA已启用" : "MFA已停用"); |
| | | vm.closeMfaDialog(); |
| | | vm.loadDetail(); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("MFA操作失败"); |
| | | }, |
| | | complete: function () { |
| | | vm.mfaSubmitting = false; |
| | | } |
| | | }); |
| | | }, |
| | | openPasskeyRegisterDialog: function () { |
| | | if (!window.WCS_WEBAUTHN || !window.WCS_WEBAUTHN.isSupported()) { |
| | | this.$message.error(this.resolvePasskeyErrorMessage({ message: window.isSecureContext ? "not-supported" : "secure-context" }, "当前环境不支持通行密钥")); |
| | | return; |
| | | } |
| | | this.passkeyDialogMode = "register"; |
| | | this.resetPasskeyDialogState(); |
| | | this.passkeyDialogVisible = true; |
| | | }, |
| | | openPasskeyRemoveDialog: function () { |
| | | this.passkeyDialogMode = "remove"; |
| | | this.resetPasskeyDialogState(); |
| | | this.passkeyDialogVisible = true; |
| | | }, |
| | | closePasskeyDialog: function () { |
| | | this.passkeyDialogVisible = false; |
| | | this.resetPasskeyDialogState(); |
| | | }, |
| | | resetPasskeyDialogState: function () { |
| | | this.passkeySubmitting = false; |
| | | this.passkeyForm = { |
| | | name: "", |
| | | currentPassword: "" |
| | | }; |
| | | if (this.$refs.passkeyForm) { |
| | | this.$refs.passkeyForm.clearValidate(); |
| | | } |
| | | }, |
| | | handlePasskeySubmit: function () { |
| | | var vm = this; |
| | | if (!vm.$refs.passkeyForm) { |
| | | return; |
| | | } |
| | | vm.$refs.passkeyForm.validate(function (valid) { |
| | | if (!valid) { |
| | | return false; |
| | | } |
| | | if (vm.passkeyDialogMode === "register") { |
| | | vm.submitPasskeyRegister(); |
| | | } else { |
| | | vm.submitPasskeyRemove(); |
| | | } |
| | | return true; |
| | | }); |
| | | }, |
| | | submitPasskeyRegister: function () { |
| | | var vm = this; |
| | | if (!window.WCS_WEBAUTHN || !window.WCS_WEBAUTHN.isSupported()) { |
| | | vm.$message.error(vm.resolvePasskeyErrorMessage({ message: window.isSecureContext ? "not-supported" : "secure-context" }, "当前环境不支持通行密钥")); |
| | | return; |
| | | } |
| | | vm.passkeySubmitting = true; |
| | | $.ajax({ |
| | | url: baseUrl + "/user/passkey/register/options/auth", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (Number(res.code) !== 200) { |
| | | vm.passkeySubmitting = false; |
| | | vm.$message.error(res.msg || "通行密钥配置加载失败"); |
| | | return; |
| | | } |
| | | vm.executePasskeyRegister(res.data || {}); |
| | | }, |
| | | error: function () { |
| | | vm.passkeySubmitting = false; |
| | | vm.$message.error("通行密钥配置加载失败"); |
| | | } |
| | | }); |
| | | }, |
| | | executePasskeyRegister: function (optionsPayload) { |
| | | var vm = this; |
| | | window.WCS_WEBAUTHN.register(optionsPayload).then(function (credential) { |
| | | $.ajax({ |
| | | url: baseUrl + "/user/passkey/register/finish/auth", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | data: { |
| | | ticket: optionsPayload.ticket, |
| | | currentPassword: hex_md5(vm.passkeyForm.currentPassword), |
| | | name: vm.passkeyForm.name, |
| | | credentialId: credential.credentialId, |
| | | clientDataJSON: credential.clientDataJSON, |
| | | authenticatorData: credential.authenticatorData, |
| | | publicKey: credential.publicKey, |
| | | publicKeyAlgorithm: credential.publicKeyAlgorithm, |
| | | transports: credential.transports |
| | | }, |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (Number(res.code) !== 200) { |
| | | vm.$message.error(res.msg || "通行密钥绑定失败"); |
| | | return; |
| | | } |
| | | vm.$message.success("通行密钥已绑定"); |
| | | vm.closePasskeyDialog(); |
| | | vm.loadDetail(); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("通行密钥绑定失败"); |
| | | }, |
| | | complete: function () { |
| | | vm.passkeySubmitting = false; |
| | | } |
| | | }); |
| | | }).catch(function (err) { |
| | | vm.passkeySubmitting = false; |
| | | vm.$message.error(vm.resolvePasskeyErrorMessage(err, "通行密钥绑定失败")); |
| | | }); |
| | | }, |
| | | submitPasskeyRemove: function () { |
| | | var vm = this; |
| | | vm.passkeySubmitting = true; |
| | | $.ajax({ |
| | | url: baseUrl + "/user/passkey/remove/auth", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | data: { |
| | | currentPassword: hex_md5(vm.passkeyForm.currentPassword) |
| | | }, |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (Number(res.code) !== 200) { |
| | | vm.$message.error(res.msg || "通行密钥解绑失败"); |
| | | return; |
| | | } |
| | | vm.$message.success("通行密钥已解绑"); |
| | | vm.closePasskeyDialog(); |
| | | vm.loadDetail(); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("通行密钥解绑失败"); |
| | | }, |
| | | complete: function () { |
| | | vm.passkeySubmitting = false; |
| | | } |
| | | }); |
| | | }, |
| | | resolvePasskeyErrorMessage: function (err, fallback) { |
| | | var message = err && err.message ? String(err.message) : ""; |
| | | if (message === "secure-context") { |
| | | return "通行密钥仅支持在 HTTPS 或 localhost 环境下使用"; |
| | | } |
| | | if (message === "not-supported" || message === "extension-unsupported" || message === "public-key-missing") { |
| | | return "当前浏览器不支持通行密钥,请使用最新版 Chrome、Edge 或 Safari"; |
| | | } |
| | | if (err && err.name === "NotAllowedError") { |
| | | return "已取消通行密钥操作或验证超时"; |
| | | } |
| | | return fallback || "通行密钥操作失败"; |
| | | }, |
| | | copySecret: function () { |
| | | var vm = this; |
| | | var text = vm.mfaSetup.secret || ""; |
| | | if (!text) { |
| | | return; |
| | | } |
| | | if (navigator.clipboard && navigator.clipboard.writeText) { |
| | | navigator.clipboard.writeText(text).then(function () { |
| | | vm.$message.success("密钥已复制"); |
| | | }).catch(function () { |
| | | vm.fallbackCopy(text); |
| | | }); |
| | | return; |
| | | } |
| | | vm.fallbackCopy(text); |
| | | }, |
| | | fallbackCopy: function (text) { |
| | | try { |
| | | var textarea = document.createElement("textarea"); |
| | | textarea.value = text; |
| | | textarea.style.position = "fixed"; |
| | | textarea.style.opacity = "0"; |
| | | document.body.appendChild(textarea); |
| | | textarea.select(); |
| | | document.execCommand("copy"); |
| | | document.body.removeChild(textarea); |
| | | this.$message.success("密钥已复制"); |
| | | } catch (err) { |
| | | this.$message.error("复制失败"); |
| | | } |
| | | }, |
| | | handleSave: function () { |
| | | var vm = this; |
| | | vm.$refs.profileForm.validate(function (valid) { |
| | |
| | | localeOptions: [], |
| | | currentLocale: "zh-CN", |
| | | loginLoading: false, |
| | | passkeyLoading: false, |
| | | mfaLoading: false, |
| | | toolsDialogVisible: false, |
| | | textDialogVisible: false, |
| | | uploadDialogVisible: false, |
| | | mfaDialogVisible: false, |
| | | licenseBase64: "", |
| | | titleClickCount: 0, |
| | | titleClickTimer: null, |
| | | loginForm: { |
| | | mobile: "", |
| | | password: "" |
| | | }, |
| | | mfaForm: { |
| | | code: "" |
| | | }, |
| | | mfaPending: { |
| | | ticket: "", |
| | | username: "" |
| | | }, |
| | | textDialog: { |
| | | title: "", |
| | |
| | | password: [ |
| | | { required: true, message: "请输入密码", trigger: "blur" } |
| | | ] |
| | | }, |
| | | mfaRules: { |
| | | code: [ |
| | | { required: true, message: "请输入6位验证码", trigger: "blur" }, |
| | | { |
| | | validator: function (rule, value, callback) { |
| | | if (!/^\d{6}$/.test(String(value || "").trim())) { |
| | | callback(new Error("请输入6位数字验证码")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }, |
| | | trigger: "blur" |
| | | } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | |
| | | var localeTick = this.localeTick; |
| | | void localeTick; |
| | | if (window.WCS_I18N && typeof window.WCS_I18N.t === "function") { |
| | | return window.WCS_I18N.t(key); |
| | | var translated = window.WCS_I18N.t(key); |
| | | if (translated && translated !== key) { |
| | | return translated; |
| | | } |
| | | } |
| | | return fallback || key; |
| | | }, |
| | | refreshRuleMessages: function () { |
| | | var vm = this; |
| | | vm.loginRules = { |
| | | mobile: [ |
| | | { required: true, message: vm.text("login.validation.usernameRequired", "请输入账号"), trigger: "blur" } |
| | | ], |
| | | password: [ |
| | | { required: true, message: vm.text("login.validation.passwordRequired", "请输入密码"), trigger: "blur" } |
| | | ] |
| | | }; |
| | | vm.mfaRules = { |
| | | code: [ |
| | | { required: true, message: vm.text("login.validation.mfaRequired", "请输入6位验证码"), trigger: "blur" }, |
| | | { |
| | | validator: function (rule, value, callback) { |
| | | if (!/^\d{6}$/.test(String(value || "").trim())) { |
| | | callback(new Error(vm.text("login.validation.mfaInvalid", "请输入6位数字验证码"))); |
| | | return; |
| | | } |
| | | callback(); |
| | | }, |
| | | trigger: "blur" |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | initLanguageSwitch: function () { |
| | | var vm = this; |
| | | if (!window.WCS_I18N || typeof window.WCS_I18N.onReady !== "function") { |
| | | vm.refreshRuleMessages(); |
| | | return; |
| | | } |
| | | window.WCS_I18N.onReady(function (i18n) { |
| | | vm.localeOptions = i18n.getLocaleOptions(); |
| | | vm.currentLocale = i18n.getLocale(); |
| | | document.title = i18n.t("login.title"); |
| | | vm.refreshRuleMessages(); |
| | | vm.localeTick++; |
| | | }); |
| | | }, |
| | |
| | | return true; |
| | | }); |
| | | }, |
| | | handlePasskeyLogin: function () { |
| | | var vm = this; |
| | | if (!window.WCS_WEBAUTHN || !window.WCS_WEBAUTHN.isSupported()) { |
| | | vm.$message.error(vm.resolvePasskeyErrorMessage({ message: window.isSecureContext ? "not-supported" : "secure-context" }, "login.error.passkeyOptionsFailed", "获取通行密钥登录参数失败")); |
| | | return; |
| | | } |
| | | vm.passkeyLoading = true; |
| | | ajaxJson({ |
| | | url: baseUrl + "/login/passkey/options.action", |
| | | data: { |
| | | mobile: vm.loginForm.mobile |
| | | }, |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (Number(res.code) !== 200) { |
| | | vm.passkeyLoading = false; |
| | | vm.$message.error(res.msg || vm.text("login.error.passkeyOptionsFailed", "获取通行密钥登录参数失败")); |
| | | return; |
| | | } |
| | | vm.executePasskeyLogin(res.data || {}); |
| | | }, |
| | | error: function () { |
| | | vm.passkeyLoading = false; |
| | | vm.$message.error(vm.text("login.error.passkeyOptionsFailed", "获取通行密钥登录参数失败")); |
| | | } |
| | | }); |
| | | }, |
| | | submitLogin: function () { |
| | | var vm = this; |
| | | vm.loginLoading = true; |
| | |
| | | }, |
| | | method: "POST", |
| | | success: function (res) { |
| | | var payload = res && res.data ? res.data : {}; |
| | | if (Number(res.code) === 200) { |
| | | localStorage.setItem("token", res.data.token); |
| | | localStorage.setItem("username", res.data.username); |
| | | window.location.href = "index.html"; |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || "登录失败"); |
| | | if (payload.mfaRequired) { |
| | | vm.openMfaDialog(payload); |
| | | return; |
| | | } |
| | | vm.finishLogin(payload); |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || vm.text("login.error.loginFailed", "登录失败")); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("登录失败"); |
| | | vm.$message.error(vm.text("login.error.loginFailed", "登录失败")); |
| | | }, |
| | | complete: function () { |
| | | vm.loginLoading = false; |
| | | } |
| | | }); |
| | | }, |
| | | openMfaDialog: function (payload) { |
| | | this.mfaPending = { |
| | | ticket: payload.mfaTicket || "", |
| | | username: payload.username || this.loginForm.mobile || "" |
| | | }; |
| | | this.mfaForm.code = ""; |
| | | this.mfaDialogVisible = true; |
| | | if (this.$refs.mfaForm) { |
| | | this.$nextTick(function () { |
| | | this.$refs.mfaForm.clearValidate(); |
| | | }); |
| | | } |
| | | }, |
| | | closeMfaDialog: function () { |
| | | this.mfaDialogVisible = false; |
| | | this.mfaLoading = false; |
| | | this.mfaPending = { |
| | | ticket: "", |
| | | username: "" |
| | | }; |
| | | this.mfaForm.code = ""; |
| | | if (this.$refs.mfaForm) { |
| | | this.$refs.mfaForm.clearValidate(); |
| | | } |
| | | }, |
| | | handleMfaLogin: function () { |
| | | var vm = this; |
| | | if (!vm.$refs.mfaForm) { |
| | | return; |
| | | } |
| | | vm.$refs.mfaForm.validate(function (valid) { |
| | | if (!valid) { |
| | | return false; |
| | | } |
| | | vm.submitMfaLogin(); |
| | | return true; |
| | | }); |
| | | }, |
| | | submitMfaLogin: function () { |
| | | var vm = this; |
| | | if (!vm.mfaPending.ticket) { |
| | | vm.$message.error(vm.text("login.error.mfaTicketExpired", "登录票据已失效,请重新登录")); |
| | | vm.closeMfaDialog(); |
| | | return; |
| | | } |
| | | vm.mfaLoading = true; |
| | | ajaxJson({ |
| | | url: baseUrl + "/login/mfa.action", |
| | | data: { |
| | | ticket: vm.mfaPending.ticket, |
| | | code: vm.mfaForm.code |
| | | }, |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (Number(res.code) === 200) { |
| | | vm.finishLogin(res.data || {}); |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || vm.text("login.error.mfaFailed", "验证失败")); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error(vm.text("login.error.mfaFailed", "验证失败")); |
| | | }, |
| | | complete: function () { |
| | | vm.mfaLoading = false; |
| | | } |
| | | }); |
| | | }, |
| | | executePasskeyLogin: function (payload) { |
| | | var vm = this; |
| | | window.WCS_WEBAUTHN.authenticate(payload).then(function (assertion) { |
| | | ajaxJson({ |
| | | url: baseUrl + "/login/passkey/verify.action", |
| | | data: { |
| | | ticket: payload.ticket, |
| | | credentialId: assertion.credentialId, |
| | | clientDataJSON: assertion.clientDataJSON, |
| | | authenticatorData: assertion.authenticatorData, |
| | | signature: assertion.signature |
| | | }, |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (Number(res.code) === 200) { |
| | | vm.finishLogin(res.data || {}); |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || vm.text("login.error.passkeyVerifyFailed", "通行密钥验证失败")); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error(vm.text("login.error.passkeyVerifyFailed", "通行密钥验证失败")); |
| | | }, |
| | | complete: function () { |
| | | vm.passkeyLoading = false; |
| | | } |
| | | }); |
| | | }).catch(function (err) { |
| | | vm.passkeyLoading = false; |
| | | vm.$message.error(vm.resolvePasskeyErrorMessage(err, "login.error.passkeyVerifyFailed", "通行密钥验证失败")); |
| | | }); |
| | | }, |
| | | finishLogin: function (payload) { |
| | | localStorage.setItem("token", payload.token || ""); |
| | | localStorage.setItem("username", payload.username || this.loginForm.mobile || ""); |
| | | this.closeMfaDialog(); |
| | | window.location.href = "index.html"; |
| | | }, |
| | | resolvePasskeyErrorMessage: function (err, fallbackKey, fallbackText) { |
| | | var message = err && err.message ? String(err.message) : ""; |
| | | if (message === "secure-context") { |
| | | return this.text("login.passkey.secureContext", "通行密钥仅支持在 HTTPS 或 localhost 环境下使用"); |
| | | } |
| | | if (message === "not-supported" || message === "extension-unsupported" || message === "public-key-missing") { |
| | | return this.text("login.passkey.browserUnsupported", "当前浏览器不支持通行密钥,请使用最新版 Chrome、Edge 或 Safari"); |
| | | } |
| | | if (err && err.name === "NotAllowedError") { |
| | | return this.text(fallbackKey, fallbackText); |
| | | } |
| | | return this.text(fallbackKey, fallbackText); |
| | | }, |
| | | openTextDialog: function (title, label, text, tip) { |
| | | var pretty = ""; |
| | |
| | | textarea.select(); |
| | | document.execCommand("copy"); |
| | | document.body.removeChild(textarea); |
| | | this.$message.success("已复制到剪贴板"); |
| | | this.$message.success(this.text("login.dialog.copied", "已复制到剪贴板")); |
| | | } catch (err) { |
| | | this.$message.error("复制失败"); |
| | | this.$message.error(this.text("login.dialog.copyFailed", "复制失败")); |
| | | } |
| | | }, |
| | | copyText: function () { |
| | |
| | | var text = vm.textDialog.text || ""; |
| | | if (navigator.clipboard && navigator.clipboard.writeText) { |
| | | navigator.clipboard.writeText(text).then(function () { |
| | | vm.$message.success("已复制到剪贴板"); |
| | | vm.$message.success(vm.text("login.dialog.copied", "已复制到剪贴板")); |
| | | }).catch(function () { |
| | | vm.fallbackCopy(text); |
| | | }); |
| | |
| | | method: "GET", |
| | | success: function (res) { |
| | | if (Number(res.code) === 200) { |
| | | vm.openTextDialog("获取请求码", "请求码", res.msg || "", "请求码中已包含项目名称,直接发给许可证服务端即可。"); |
| | | vm.openTextDialog( |
| | | vm.text("login.requestCode.title", "获取请求码"), |
| | | vm.text("login.requestCode.label", "请求码"), |
| | | res.msg || "", |
| | | vm.text("login.requestCode.tip", "请求码中已包含项目名称,直接发给许可证服务端即可。") |
| | | ); |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || "获取请求码失败"); |
| | | vm.$message.error(res.msg || vm.text("login.error.requestCodeFailed", "获取请求码失败")); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("获取请求码失败"); |
| | | vm.$message.error(vm.text("login.error.requestCodeFailed", "获取请求码失败")); |
| | | } |
| | | }); |
| | | }, |
| | |
| | | url: baseUrl + "/license/getServerInfos", |
| | | method: "GET", |
| | | success: function (res) { |
| | | vm.openTextDialog("获取系统配置", "系统配置信息", res, "老项目仍可继续使用这份硬件信息 JSON 申请许可证。"); |
| | | vm.openTextDialog( |
| | | vm.text("login.serverInfo.title", "获取系统配置"), |
| | | vm.text("login.serverInfo.label", "系统配置信息"), |
| | | res, |
| | | vm.text("login.serverInfo.tip", "老项目仍可继续使用这份硬件信息 JSON 申请许可证。") |
| | | ); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("获取系统配置信息失败"); |
| | | vm.$message.error(vm.text("login.error.serverInfoFailed", "获取系统配置信息失败")); |
| | | } |
| | | }); |
| | | }, |
| | | submitLicense: function () { |
| | | var vm = this; |
| | | if (!vm.licenseBase64) { |
| | | vm.$message.warning("许可证内容不能为空"); |
| | | vm.$message.warning(vm.text("login.error.licenseEmpty", "许可证内容不能为空")); |
| | | return; |
| | | } |
| | | ajaxJson({ |
| | |
| | | if (Number(res.code) === 200) { |
| | | vm.uploadDialogVisible = false; |
| | | vm.licenseBase64 = ""; |
| | | vm.$message.success("许可证更新成功"); |
| | | vm.$message.success(vm.text("login.license.success", "许可证更新成功")); |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || "许可证更新失败"); |
| | | vm.$message.error(res.msg || vm.text("login.error.licenseUpdateFailed", "许可证更新失败")); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("许可证录入失败"); |
| | | vm.$message.error(vm.text("login.error.licenseImportFailed", "许可证录入失败")); |
| | | } |
| | | }); |
| | | }, |
| | | activateLicense: function () { |
| | | var vm = this; |
| | | vm.$confirm("确定执行一键激活吗?", "提示", { |
| | | vm.$confirm(vm.text("login.activate.confirm", "确定执行一键激活吗?"), vm.text("common.prompt", "提示"), { |
| | | type: "warning", |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消" |
| | | confirmButtonText: vm.text("common.ok", "确定"), |
| | | cancelButtonText: vm.text("login.mfa.cancel", "取消") |
| | | }).then(function () { |
| | | ajaxJson({ |
| | | url: baseUrl + "/license/activate", |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (Number(res.code) === 200) { |
| | | vm.$message.success("激活成功"); |
| | | vm.$message.success(vm.text("login.activate.success", "激活成功")); |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || "激活失败"); |
| | | vm.$message.error(res.msg || vm.text("login.activate.failed", "激活失败")); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("激活失败"); |
| | | vm.$message.error(vm.text("login.activate.failed", "激活失败")); |
| | | } |
| | | }); |
| | | }).catch(function () { |
| | |
| | | method: "GET", |
| | | success: function (res) { |
| | | if (Number(res.code) === 200) { |
| | | vm.$alert(res.msg || "", "项目名称", { |
| | | confirmButtonText: "确定" |
| | | vm.$alert(res.msg || "", vm.text("login.projectName.title", "项目名称"), { |
| | | confirmButtonText: vm.text("common.ok", "确定") |
| | | }); |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || "获取项目名称失败"); |
| | | vm.$message.error(res.msg || vm.text("login.error.projectNameFailed", "获取项目名称失败")); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("获取项目名称失败"); |
| | | vm.$message.error(vm.text("login.error.projectNameFailed", "获取项目名称失败")); |
| | | } |
| | | }); |
| | | } |
| | |
| | | (function () { |
| | | var simpleEntityName = 'user'; |
| | | var entityName = 'User'; |
| | | var primaryKeyField = 'id'; |
| | | var fieldMeta = dedupeFieldMeta([ |
| | | { |
| | | field: 'id', |
| | | columnName: 'id', |
| | | label: 'I D', |
| | | tableProp: 'id', |
| | | exportField: 'id', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: true, |
| | | sortable: true, |
| | | textarea: false, |
| | | minWidth: 90, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'username', |
| | | columnName: 'username', |
| | | label: '账 号', |
| | | tableProp: 'username', |
| | | exportField: 'username', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: true, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'password', |
| | | columnName: 'password', |
| | | label: '密 码', |
| | | tableProp: 'password', |
| | | exportField: 'password', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'nickname', |
| | | columnName: 'nickname', |
| | | label: '昵 称', |
| | | tableProp: 'nickname', |
| | | exportField: 'nickname', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'avatar', |
| | | columnName: 'avatar', |
| | | label: '头 像', |
| | | tableProp: 'avatar', |
| | | exportField: 'avatar', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'code', |
| | | columnName: 'code', |
| | | label: '工 号', |
| | | tableProp: 'code', |
| | | exportField: 'code', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'sex', |
| | | columnName: 'sex', |
| | | label: '性 别', |
| | | tableProp: 'sex$', |
| | | exportField: 'sex$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '0', label: '未知' }, { rawValue: '1', label: '男' }, { rawValue: '2', label: '女' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'phone', |
| | | columnName: 'phone', |
| | | label: '手 机 号', |
| | | tableProp: 'phone', |
| | | exportField: 'phone', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 116, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'email', |
| | | columnName: 'email', |
| | | label: '邮 箱', |
| | | tableProp: 'email', |
| | | exportField: 'email', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'emailVerified', |
| | | columnName: 'email_verified', |
| | | label: '邮箱验证', |
| | | tableProp: 'emailVerified$', |
| | | exportField: 'emailVerified$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '0', label: '否' }, { rawValue: '1', label: '是' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deptId', |
| | | columnName: 'dept_id', |
| | | label: '所属部门', |
| | | tableProp: 'deptId$', |
| | | exportField: 'deptId$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'dept', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'realName', |
| | | columnName: 'real_name', |
| | | label: '真实姓名', |
| | | tableProp: 'realName', |
| | | exportField: 'realName', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'idCard', |
| | | columnName: 'id_card', |
| | | label: '身份证号', |
| | | tableProp: 'idCard', |
| | | exportField: 'idCard', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'birthday', |
| | | columnName: 'birthday', |
| | | label: '出生日期', |
| | | tableProp: 'birthday', |
| | | exportField: 'birthday', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'introduction', |
| | | columnName: 'introduction', |
| | | label: '个人简介', |
| | | tableProp: 'introduction', |
| | | exportField: 'introduction', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'hostId', |
| | | columnName: 'host_id', |
| | | label: '所属机构', |
| | | tableProp: 'hostId$', |
| | | exportField: 'hostId$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'host', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'status', |
| | | columnName: 'status', |
| | | label: '状 态', |
| | | tableProp: 'status$', |
| | | exportField: 'status$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '1', label: '正常' }, { rawValue: '0', label: '禁用' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deleted', |
| | | columnName: 'deleted', |
| | | label: '是否删除', |
| | | tableProp: 'deleted$', |
| | | exportField: 'deleted$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '1', label: '是' }, { rawValue: '0', label: '否' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | columnName: 'create_time', |
| | | label: '添加时间', |
| | | tableProp: 'createTime$', |
| | | exportField: 'createTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'createBy', |
| | | columnName: 'create_by', |
| | | label: '添加人员', |
| | | tableProp: 'createBy$', |
| | | exportField: 'createBy$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'user', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | columnName: 'update_time', |
| | | label: '修改时间', |
| | | tableProp: 'updateTime$', |
| | | exportField: 'updateTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'updateBy', |
| | | columnName: 'update_by', |
| | | label: '修改人员', |
| | | tableProp: 'updateBy$', |
| | | exportField: 'updateBy$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'user', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'memo', |
| | | columnName: 'memo', |
| | | label: '备 注', |
| | | tableProp: 'memo', |
| | | exportField: 'memo', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: true, |
| | | minWidth: 180, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'id', |
| | | columnName: 'id', |
| | | label: '*编 号', |
| | | tableProp: 'id', |
| | | exportField: 'id', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: true, |
| | | sortable: true, |
| | | textarea: false, |
| | | minWidth: 90, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'hostId', |
| | | columnName: 'host_id', |
| | | label: '*授权商户', |
| | | tableProp: 'hostId', |
| | | exportField: 'hostId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deptId', |
| | | columnName: 'dept_id', |
| | | label: '', |
| | | tableProp: 'deptId', |
| | | exportField: 'deptId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'roleId', |
| | | columnName: 'role_id', |
| | | label: '*角 色', |
| | | tableProp: 'roleId', |
| | | exportField: 'roleId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'username', |
| | | columnName: 'username', |
| | | label: '登录账户', |
| | | tableProp: 'username', |
| | | exportField: 'username', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'nickname', |
| | | columnName: 'nickname', |
| | | label: '用户名', |
| | | tableProp: 'nickname', |
| | | exportField: 'nickname', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'mobile', |
| | | columnName: 'mobile', |
| | | label: '手机号', |
| | | tableProp: 'mobile', |
| | | exportField: 'mobile', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'password', |
| | | columnName: 'password', |
| | | label: '密 码', |
| | | tableProp: 'password', |
| | | exportField: 'password', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'avatar', |
| | | columnName: 'avatar', |
| | | label: '', |
| | | tableProp: 'avatar', |
| | | exportField: 'avatar', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'email', |
| | | columnName: 'email', |
| | | label: '邮箱', |
| | | tableProp: 'email', |
| | | exportField: 'email', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'sex', |
| | | columnName: 'sex', |
| | | label: '性别', |
| | | tableProp: 'sex', |
| | | exportField: 'sex', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | columnName: 'create_time', |
| | | label: '注册时间', |
| | | tableProp: 'createTime$', |
| | | exportField: 'createTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'status', |
| | | columnName: 'status', |
| | | label: '状态', |
| | | tableProp: 'status', |
| | | exportField: 'status', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'id', |
| | | columnName: 'id', |
| | | label: '*编 号', |
| | | tableProp: 'id', |
| | | exportField: 'id', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: true, |
| | | sortable: true, |
| | | textarea: false, |
| | | minWidth: 90, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'hostId', |
| | | columnName: 'host_id', |
| | | label: '*授权商户', |
| | | tableProp: 'hostId', |
| | | exportField: 'hostId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deptId', |
| | | columnName: 'dept_id', |
| | | label: '', |
| | | tableProp: 'deptId', |
| | | exportField: 'deptId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'roleId', |
| | | columnName: 'role_id', |
| | | label: '*角 色', |
| | | tableProp: 'roleId', |
| | | exportField: 'roleId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'username', |
| | | columnName: 'username', |
| | | label: '登录账户', |
| | | tableProp: 'username', |
| | | exportField: 'username', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'nickname', |
| | | columnName: 'nickname', |
| | | label: '用户名', |
| | | tableProp: 'nickname', |
| | | exportField: 'nickname', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'mobile', |
| | | columnName: 'mobile', |
| | | label: '手机号', |
| | | tableProp: 'mobile', |
| | | exportField: 'mobile', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'password', |
| | | columnName: 'password', |
| | | label: '密 码', |
| | | tableProp: 'password', |
| | | exportField: 'password', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'avatar', |
| | | columnName: 'avatar', |
| | | label: '', |
| | | tableProp: 'avatar', |
| | | exportField: 'avatar', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'email', |
| | | columnName: 'email', |
| | | label: '邮箱', |
| | | tableProp: 'email', |
| | | exportField: 'email', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'sex', |
| | | columnName: 'sex', |
| | | label: '性别', |
| | | tableProp: 'sex', |
| | | exportField: 'sex', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | columnName: 'create_time', |
| | | label: '注册时间', |
| | | tableProp: 'createTime$', |
| | | exportField: 'createTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'status', |
| | | columnName: 'status', |
| | | label: '状态', |
| | | tableProp: 'status', |
| | | exportField: 'status', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'id', |
| | | columnName: 'id', |
| | | label: 'I D', |
| | | tableProp: 'id', |
| | | exportField: 'id', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: true, |
| | | sortable: true, |
| | | textarea: false, |
| | | minWidth: 90, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'username', |
| | | columnName: 'username', |
| | | label: '账 号', |
| | | tableProp: 'username', |
| | | exportField: 'username', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: true, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'password', |
| | | columnName: 'password', |
| | | label: '密 码', |
| | | tableProp: 'password', |
| | | exportField: 'password', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'nickname', |
| | | columnName: 'nickname', |
| | | label: '昵 称', |
| | | tableProp: 'nickname', |
| | | exportField: 'nickname', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'avatar', |
| | | columnName: 'avatar', |
| | | label: '头 像', |
| | | tableProp: 'avatar', |
| | | exportField: 'avatar', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'code', |
| | | columnName: 'code', |
| | | label: '工 号', |
| | | tableProp: 'code', |
| | | exportField: 'code', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'sex', |
| | | columnName: 'sex', |
| | | label: '性 别', |
| | | tableProp: 'sex$', |
| | | exportField: 'sex$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '0', label: '未知' }, { rawValue: '1', label: '男' }, { rawValue: '2', label: '女' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'phone', |
| | | columnName: 'phone', |
| | | label: '手 机 号', |
| | | tableProp: 'phone', |
| | | exportField: 'phone', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 116, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'email', |
| | | columnName: 'email', |
| | | label: '邮 箱', |
| | | tableProp: 'email', |
| | | exportField: 'email', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'emailVerified', |
| | | columnName: 'email_verified', |
| | | label: '邮箱验证', |
| | | tableProp: 'emailVerified$', |
| | | exportField: 'emailVerified$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '0', label: '否' }, { rawValue: '1', label: '是' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deptId', |
| | | columnName: 'dept_id', |
| | | label: '所属部门', |
| | | tableProp: 'deptId$', |
| | | exportField: 'deptId$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'dept', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'realName', |
| | | columnName: 'real_name', |
| | | label: '真实姓名', |
| | | tableProp: 'realName', |
| | | exportField: 'realName', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'idCard', |
| | | columnName: 'id_card', |
| | | label: '身份证号', |
| | | tableProp: 'idCard', |
| | | exportField: 'idCard', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'birthday', |
| | | columnName: 'birthday', |
| | | label: '出生日期', |
| | | tableProp: 'birthday', |
| | | exportField: 'birthday', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'introduction', |
| | | columnName: 'introduction', |
| | | label: '个人简介', |
| | | tableProp: 'introduction', |
| | | exportField: 'introduction', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'tenantId', |
| | | columnName: 'tenant_id', |
| | | label: '所属机构', |
| | | tableProp: 'tenantId$', |
| | | exportField: 'tenantId$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'host', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'status', |
| | | columnName: 'status', |
| | | label: '状 态', |
| | | tableProp: 'status$', |
| | | exportField: 'status$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '1', label: '正常' }, { rawValue: '0', label: '禁用' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deleted', |
| | | columnName: 'deleted', |
| | | label: '是否删除', |
| | | tableProp: 'deleted$', |
| | | exportField: 'deleted$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '1', label: '是' }, { rawValue: '0', label: '否' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | columnName: 'create_time', |
| | | label: '添加时间', |
| | | tableProp: 'createTime$', |
| | | exportField: 'createTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'createBy', |
| | | columnName: 'create_by', |
| | | label: '添加人员', |
| | | tableProp: 'createBy$', |
| | | exportField: 'createBy$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'user', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | columnName: 'update_time', |
| | | label: '修改时间', |
| | | tableProp: 'updateTime$', |
| | | exportField: 'updateTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'updateBy', |
| | | columnName: 'update_by', |
| | | label: '修改人员', |
| | | tableProp: 'updateBy$', |
| | | exportField: 'updateBy$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'user', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'memo', |
| | | columnName: 'memo', |
| | | label: '备 注', |
| | | tableProp: 'memo', |
| | | exportField: 'memo', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: true, |
| | | minWidth: 180, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'id', |
| | | columnName: 'id', |
| | | label: '*编 号', |
| | | tableProp: 'id', |
| | | exportField: 'id', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: true, |
| | | sortable: true, |
| | | textarea: false, |
| | | minWidth: 90, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'hostId', |
| | | columnName: 'host_id', |
| | | label: '*授权商户', |
| | | tableProp: 'hostId', |
| | | exportField: 'hostId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deptId', |
| | | columnName: 'dept_id', |
| | | label: '', |
| | | tableProp: 'deptId', |
| | | exportField: 'deptId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'roleId', |
| | | columnName: 'role_id', |
| | | label: '*角 色', |
| | | tableProp: 'roleId', |
| | | exportField: 'roleId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'username', |
| | | columnName: 'username', |
| | | label: '登录账户', |
| | | tableProp: 'username', |
| | | exportField: 'username', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'nickname', |
| | | columnName: 'nickname', |
| | | label: '用户名', |
| | | tableProp: 'nickname', |
| | | exportField: 'nickname', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'mobile', |
| | | columnName: 'mobile', |
| | | label: '手机号', |
| | | tableProp: 'mobile', |
| | | exportField: 'mobile', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'password', |
| | | columnName: 'password', |
| | | label: '密 码', |
| | | tableProp: 'password', |
| | | exportField: 'password', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'avatar', |
| | | columnName: 'avatar', |
| | | label: '', |
| | | tableProp: 'avatar', |
| | | exportField: 'avatar', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'email', |
| | | columnName: 'email', |
| | | label: '邮箱', |
| | | tableProp: 'email', |
| | | exportField: 'email', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'sex', |
| | | columnName: 'sex', |
| | | label: '性别', |
| | | tableProp: 'sex', |
| | | exportField: 'sex', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | columnName: 'create_time', |
| | | label: '注册时间', |
| | | tableProp: 'createTime$', |
| | | exportField: 'createTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'status', |
| | | columnName: 'status', |
| | | label: '状态', |
| | | tableProp: 'status', |
| | | exportField: 'status', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'id', |
| | | columnName: 'id', |
| | | label: '*编 号', |
| | | tableProp: 'id', |
| | | exportField: 'id', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: true, |
| | | sortable: true, |
| | | textarea: false, |
| | | minWidth: 90, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'hostId', |
| | | columnName: 'host_id', |
| | | label: '*授权商户', |
| | | tableProp: 'hostId', |
| | | exportField: 'hostId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deptId', |
| | | columnName: 'dept_id', |
| | | label: '', |
| | | tableProp: 'deptId', |
| | | exportField: 'deptId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'roleId', |
| | | columnName: 'role_id', |
| | | label: '*角 色', |
| | | tableProp: 'roleId', |
| | | exportField: 'roleId', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'username', |
| | | columnName: 'username', |
| | | label: '登录账户', |
| | | tableProp: 'username', |
| | | exportField: 'username', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'nickname', |
| | | columnName: 'nickname', |
| | | label: '用户名', |
| | | tableProp: 'nickname', |
| | | exportField: 'nickname', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'mobile', |
| | | columnName: 'mobile', |
| | | label: '手机号', |
| | | tableProp: 'mobile', |
| | | exportField: 'mobile', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'password', |
| | | columnName: 'password', |
| | | label: '密 码', |
| | | tableProp: 'password', |
| | | exportField: 'password', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'avatar', |
| | | columnName: 'avatar', |
| | | label: '', |
| | | tableProp: 'avatar', |
| | | exportField: 'avatar', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'email', |
| | | columnName: 'email', |
| | | label: '邮箱', |
| | | tableProp: 'email', |
| | | exportField: 'email', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'sex', |
| | | columnName: 'sex', |
| | | label: '性别', |
| | | tableProp: 'sex', |
| | | exportField: 'sex', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | columnName: 'create_time', |
| | | label: '注册时间', |
| | | tableProp: 'createTime$', |
| | | exportField: 'createTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'status', |
| | | columnName: 'status', |
| | | label: '状态', |
| | | tableProp: 'status', |
| | | exportField: 'status', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'id', |
| | | columnName: 'id', |
| | | label: 'I D', |
| | | tableProp: 'id', |
| | | exportField: 'id', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: true, |
| | | sortable: true, |
| | | textarea: false, |
| | | minWidth: 90, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'username', |
| | | columnName: 'username', |
| | | label: '账 号', |
| | | tableProp: 'username', |
| | | exportField: 'username', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: true, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'password', |
| | | columnName: 'password', |
| | | label: '密 码', |
| | | tableProp: 'password', |
| | | exportField: 'password', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'nickname', |
| | | columnName: 'nickname', |
| | | label: '昵 称', |
| | | tableProp: 'nickname', |
| | | exportField: 'nickname', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'avatar', |
| | | columnName: 'avatar', |
| | | label: '头 像', |
| | | tableProp: 'avatar', |
| | | exportField: 'avatar', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'code', |
| | | columnName: 'code', |
| | | label: '工 号', |
| | | tableProp: 'code', |
| | | exportField: 'code', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'sex', |
| | | columnName: 'sex', |
| | | label: '性 别', |
| | | tableProp: 'sex$', |
| | | exportField: 'sex$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '0', label: '未知' }, { rawValue: '1', label: '男' }, { rawValue: '2', label: '女' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'phone', |
| | | columnName: 'phone', |
| | | label: '手 机 号', |
| | | tableProp: 'phone', |
| | | exportField: 'phone', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 116, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'email', |
| | | columnName: 'email', |
| | | label: '邮 箱', |
| | | tableProp: 'email', |
| | | exportField: 'email', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'emailVerified', |
| | | columnName: 'email_verified', |
| | | label: '邮箱验证', |
| | | tableProp: 'emailVerified$', |
| | | exportField: 'emailVerified$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '0', label: '否' }, { rawValue: '1', label: '是' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deptId', |
| | | columnName: 'dept_id', |
| | | label: '所属部门', |
| | | tableProp: 'deptId$', |
| | | exportField: 'deptId$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'dept', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'realName', |
| | | columnName: 'real_name', |
| | | label: '真实姓名', |
| | | tableProp: 'realName', |
| | | exportField: 'realName', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'idCard', |
| | | columnName: 'id_card', |
| | | label: '身份证号', |
| | | tableProp: 'idCard', |
| | | exportField: 'idCard', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'birthday', |
| | | columnName: 'birthday', |
| | | label: '出生日期', |
| | | tableProp: 'birthday', |
| | | exportField: 'birthday', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'introduction', |
| | | columnName: 'introduction', |
| | | label: '个人简介', |
| | | tableProp: 'introduction', |
| | | exportField: 'introduction', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'hostId', |
| | | columnName: 'host_id', |
| | | label: '所属机构', |
| | | tableProp: 'hostId$', |
| | | exportField: 'hostId$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'host', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'status', |
| | | columnName: 'status', |
| | | label: '状 态', |
| | | tableProp: 'status$', |
| | | exportField: 'status$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '1', label: '正常' }, { rawValue: '0', label: '禁用' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deleted', |
| | | columnName: 'deleted', |
| | | label: '是否删除', |
| | | tableProp: 'deleted$', |
| | | exportField: 'deleted$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '1', label: '是' }, { rawValue: '0', label: '否' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | columnName: 'create_time', |
| | | label: '添加时间', |
| | | tableProp: 'createTime$', |
| | | exportField: 'createTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'createBy', |
| | | columnName: 'create_by', |
| | | label: '添加人员', |
| | | tableProp: 'createBy$', |
| | | exportField: 'createBy$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'user', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | columnName: 'update_time', |
| | | label: '修改时间', |
| | | tableProp: 'updateTime$', |
| | | exportField: 'updateTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'updateBy', |
| | | columnName: 'update_by', |
| | | label: '修改人员', |
| | | tableProp: 'updateBy$', |
| | | exportField: 'updateBy$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'user', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'memo', |
| | | columnName: 'memo', |
| | | label: '备 注', |
| | | tableProp: 'memo', |
| | | exportField: 'memo', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: true, |
| | | minWidth: 180, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'id', |
| | | columnName: 'id', |
| | | label: 'I D', |
| | | tableProp: 'id', |
| | | exportField: 'id', |
| | | kind: 'text', |
| | | valueType: 'number', |
| | | required: true, |
| | | primaryKey: true, |
| | | sortable: true, |
| | | textarea: false, |
| | | minWidth: 90, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'username', |
| | | columnName: 'username', |
| | | label: '账 号', |
| | | tableProp: 'username', |
| | | exportField: 'username', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: true, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'password', |
| | | columnName: 'password', |
| | | label: '密 码', |
| | | tableProp: 'password', |
| | | exportField: 'password', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'nickname', |
| | | columnName: 'nickname', |
| | | label: '昵 称', |
| | | tableProp: 'nickname', |
| | | exportField: 'nickname', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'avatar', |
| | | columnName: 'avatar', |
| | | label: '头 像', |
| | | tableProp: 'avatar', |
| | | exportField: 'avatar', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'code', |
| | | columnName: 'code', |
| | | label: '工 号', |
| | | tableProp: 'code', |
| | | exportField: 'code', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'sex', |
| | | columnName: 'sex', |
| | | label: '性 别', |
| | | tableProp: 'sex$', |
| | | exportField: 'sex$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '0', label: '未知' }, { rawValue: '1', label: '男' }, { rawValue: '2', label: '女' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'phone', |
| | | columnName: 'phone', |
| | | label: '手 机 号', |
| | | tableProp: 'phone', |
| | | exportField: 'phone', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 116, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'email', |
| | | columnName: 'email', |
| | | label: '邮 箱', |
| | | tableProp: 'email', |
| | | exportField: 'email', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'emailVerified', |
| | | columnName: 'email_verified', |
| | | label: '邮箱验证', |
| | | tableProp: 'emailVerified$', |
| | | exportField: 'emailVerified$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '0', label: '否' }, { rawValue: '1', label: '是' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deptId', |
| | | columnName: 'dept_id', |
| | | label: '所属部门', |
| | | tableProp: 'deptId$', |
| | | exportField: 'deptId$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'dept', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'realName', |
| | | columnName: 'real_name', |
| | | label: '真实姓名', |
| | | tableProp: 'realName', |
| | | exportField: 'realName', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'idCard', |
| | | columnName: 'id_card', |
| | | label: '身份证号', |
| | | tableProp: 'idCard', |
| | | exportField: 'idCard', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'birthday', |
| | | columnName: 'birthday', |
| | | label: '出生日期', |
| | | tableProp: 'birthday', |
| | | exportField: 'birthday', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'introduction', |
| | | columnName: 'introduction', |
| | | label: '个人简介', |
| | | tableProp: 'introduction', |
| | | exportField: 'introduction', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'hostId', |
| | | columnName: 'host_id', |
| | | label: '所属机构', |
| | | tableProp: 'hostId$', |
| | | exportField: 'hostId$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'host', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'status', |
| | | columnName: 'status', |
| | | label: '状 态', |
| | | tableProp: 'status$', |
| | | exportField: 'status$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '1', label: '正常' }, { rawValue: '0', label: '禁用' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'deleted', |
| | | columnName: 'deleted', |
| | | label: '是否删除', |
| | | tableProp: 'deleted$', |
| | | exportField: 'deleted$', |
| | | kind: 'enum', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 120, |
| | | enumOptions: [{ rawValue: '1', label: '是' }, { rawValue: '0', label: '否' }], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | columnName: 'create_time', |
| | | label: '添加时间', |
| | | tableProp: 'createTime$', |
| | | exportField: 'createTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'createBy', |
| | | columnName: 'create_by', |
| | | label: '添加人员', |
| | | tableProp: 'createBy$', |
| | | exportField: 'createBy$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'user', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'updateTime', |
| | | columnName: 'update_time', |
| | | label: '修改时间', |
| | | tableProp: 'updateTime$', |
| | | exportField: 'updateTime$', |
| | | kind: 'date', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 168, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | }, |
| | | { |
| | | field: 'updateBy', |
| | | columnName: 'update_by', |
| | | label: '修改人员', |
| | | tableProp: 'updateBy$', |
| | | exportField: 'updateBy$', |
| | | kind: 'foreign', |
| | | valueType: 'number', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: false, |
| | | minWidth: 110, |
| | | enumOptions: [], |
| | | foreignQuery: 'user', |
| | | checkboxActiveRaw: '1', |
| | | checkboxInactiveRaw: '0' |
| | | }, |
| | | { |
| | | field: 'memo', |
| | | columnName: 'memo', |
| | | label: '备 注', |
| | | tableProp: 'memo', |
| | | exportField: 'memo', |
| | | kind: 'text', |
| | | valueType: 'string', |
| | | required: false, |
| | | primaryKey: false, |
| | | sortable: false, |
| | | textarea: true, |
| | | minWidth: 180, |
| | | enumOptions: [], |
| | | foreignQuery: '', |
| | | checkboxActiveRaw: 'Y', |
| | | checkboxInactiveRaw: 'N' |
| | | } |
| | | |
| | | ]); |
| | | |
| | | function formatFieldLabel(field) { |
| | | var raw = field && field.label ? String(field.label).trim() : ''; |
| | | if (raw) { |
| | | return raw; |
| | | } |
| | | raw = field && field.columnName ? field.columnName : (field && field.field ? field.field : ''); |
| | | if (!raw) { |
| | | return ''; |
| | | } |
| | | raw = String(raw) |
| | | .replace(/\$/g, '') |
| | | .replace(/([a-z0-9])([A-Z])/g, '$1_$2') |
| | | .replace(/_/g, ' ') |
| | | .replace(/\s+/g, ' ') |
| | | .trim(); |
| | | return raw.replace(/\b[a-z]/g, function (letter) { |
| | | return letter.toUpperCase(); |
| | | }); |
| | | } |
| | | |
| | | function dedupeFieldMeta(list) { |
| | | var result = []; |
| | | var seen = {}; |
| | | (list || []).forEach(function (field) { |
| | | if (!field || !field.field || seen[field.field]) { |
| | | return; |
| | | } |
| | | field.label = formatFieldLabel(field); |
| | | seen[field.field] = true; |
| | | result.push(field); |
| | | }); |
| | | return result; |
| | | } |
| | | |
| | | function isEmptyValue(value) { |
| | | return value === null || value === undefined || value === ''; |
| | | } |
| | | |
| | | function stringValue(value) { |
| | | return isEmptyValue(value) ? '' : String(value); |
| | | } |
| | | |
| | | function valueOrDash(value) { |
| | | return isEmptyValue(value) ? '--' : value; |
| | | } |
| | | |
| | | function normalizeOptionValue(field, rawValue) { |
| | | if (rawValue === null || rawValue === undefined) { |
| | | return null; |
| | | } |
| | | if (rawValue === '') { |
| | | return ''; |
| | | } |
| | | if (field && field.valueType === 'number') { |
| | | var numberVal = Number(rawValue); |
| | | return isNaN(numberVal) ? rawValue : numberVal; |
| | | } |
| | | return String(rawValue); |
| | | } |
| | | |
| | | function isSearchableField(field) { |
| | | return !!field && field.kind !== 'image' && !field.textarea; |
| | | } |
| | | |
| | | function isSortableField(field) { |
| | | if (!field) { |
| | | return false; |
| | | } |
| | | if (field.primaryKey) { |
| | | return true; |
| | | } |
| | | return field.kind !== 'image' && !field.textarea && field.kind !== 'foreign'; |
| | | } |
| | | |
| | | function defaultFieldValue(field) { |
| | | if (field.primaryKey) { |
| | | return null; |
| | | } |
| | | if (field.kind === 'checkbox') { |
| | | return normalizeOptionValue(field, field.checkboxInactiveRaw); |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | function defaultSearchFieldValue(field) { |
| | | if (field.kind === 'date') { |
| | | return []; |
| | | } |
| | | if (field.kind === 'enum' || field.kind === 'checkbox') { |
| | | return null; |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | function createSearchDefaults() { |
| | | var result = { |
| | | condition: '' |
| | | }; |
| | | fieldMeta.forEach(function (field) { |
| | | if (!isSearchableField(field)) { |
| | | return; |
| | | } |
| | | result[field.field] = defaultSearchFieldValue(field); |
| | | }); |
| | | return result; |
| | | } |
| | | |
| | | function createSearchDisplayDefaults() { |
| | | var result = {}; |
| | | fieldMeta.forEach(function (field) { |
| | | if (field.kind === 'foreign' && isSearchableField(field)) { |
| | | result[field.field] = ''; |
| | | } |
| | | }); |
| | | return result; |
| | | } |
| | | |
| | | function createDefaultVisibleColumnKeys() { |
| | | return fieldMeta.map(function (field) { |
| | | return field.field; |
| | | }); |
| | | } |
| | | |
| | | function createFormDefaults() { |
| | | var result = {}; |
| | | fieldMeta.forEach(function (field) { |
| | | result[field.field] = defaultFieldValue(field); |
| | | }); |
| | | return result; |
| | | } |
| | | |
| | | function createDisplayDefaults() { |
| | | var result = {}; |
| | | fieldMeta.forEach(function (field) { |
| | | if (field.kind === 'foreign') { |
| | | result[field.field] = ''; |
| | | } |
| | | }); |
| | | return result; |
| | | } |
| | | |
| | | function createFormRules() { |
| | | var rules = {}; |
| | | fieldMeta.forEach(function (field) { |
| | | if (field.primaryKey || !field.required) { |
| | | return; |
| | | } |
| | | rules[field.field] = [{ |
| | | required: true, |
| | | message: (field.kind === 'date' || field.kind === 'enum' ? '请选择' : '请输入') + field.label, |
| | | trigger: (field.kind === 'date' || field.kind === 'enum') ? 'change' : 'blur' |
| | | }]; |
| | | }); |
| | | return rules; |
| | | } |
| | | |
| | | function getTableValue(row, field) { |
| | | var prop = field.tableProp || field.field; |
| | | if (row && !isEmptyValue(row[prop])) { |
| | | return row[prop]; |
| | | } |
| | | return row ? row[field.field] : ''; |
| | | } |
| | | |
| | | function isCheckboxChecked(row, field) { |
| | | var value = row ? row[field.field] : null; |
| | | var activeValue = normalizeOptionValue(field, field.checkboxActiveRaw); |
| | | return String(value) === String(activeValue); |
| | | } |
| | | |
| | | function exportCell(value) { |
| | | return stringValue(value).replace(/\t/g, ' ').replace(/\r?\n/g, ' '); |
| | | } |
| | | |
| | | function escapeHtml(value) { |
| | | return exportCell(value) |
| | | .replace(/&/g, '&') |
| | | .replace(/</g, '<') |
| | | .replace(/>/g, '>') |
| | | .replace(/"/g, '"') |
| | | .replace(/'/g, '''); |
| | | } |
| | | |
| | | function buildPayload(form) { |
| | | var payload = {}; |
| | | fieldMeta.forEach(function (field) { |
| | | var value = form[field.field]; |
| | | if (field.primaryKey) { |
| | | if (!isEmptyValue(value)) { |
| | | payload[field.field] = value; |
| | | } |
| | | return; |
| | | } |
| | | if (field.kind === 'foreign' && isEmptyValue(value)) { |
| | | value = null; |
| | | } |
| | | if (field.kind === 'enum' && value === '') { |
| | | value = null; |
| | | } |
| | | if (field.kind === 'checkbox' && isEmptyValue(value)) { |
| | | value = normalizeOptionValue(field, field.checkboxInactiveRaw); |
| | | } |
| | | if (field.valueType === 'number' && !isEmptyValue(value)) { |
| | | value = Number(value); |
| | | } |
| | | if (field.valueType === 'number' && value === '') { |
| | | value = null; |
| | | } |
| | | payload[field.field] = value; |
| | | }); |
| | | return payload; |
| | | } |
| | | |
| | | function fillFormFromRow(row, form, display) { |
| | | fieldMeta.forEach(function (field) { |
| | | if (field.primaryKey) { |
| | | form[field.field] = row[field.field]; |
| | | return; |
| | | } |
| | | if (field.kind === 'date') { |
| | | form[field.field] = row[field.tableProp] || row[field.field] || ''; |
| | | return; |
| | | } |
| | | if (field.kind === 'foreign') { |
| | | form[field.field] = isEmptyValue(row[field.field]) ? '' : normalizeOptionValue(field, row[field.field]); |
| | | if (display) { |
| | | display[field.field] = row[field.tableProp] || (isEmptyValue(row[field.field]) ? '' : String(row[field.field])); |
| | | } |
| | | return; |
| | | } |
| | | if (field.kind === 'enum') { |
| | | form[field.field] = isEmptyValue(row[field.field]) ? '' : normalizeOptionValue(field, row[field.field]); |
| | | return; |
| | | } |
| | | if (field.kind === 'checkbox') { |
| | | form[field.field] = isEmptyValue(row[field.field]) |
| | | ? normalizeOptionValue(field, field.checkboxInactiveRaw) |
| | | : normalizeOptionValue(field, row[field.field]); |
| | | return; |
| | | } |
| | | form[field.field] = isEmptyValue(row[field.field]) |
| | | ? '' |
| | | : (field.valueType === 'number' ? String(row[field.field]) : row[field.field]); |
| | | }); |
| | | } |
| | | |
| | | function resolveSearchParam(field) { |
| | | if (field.kind === 'date' && field.columnName) { |
| | | return field.columnName; |
| | | } |
| | | return field.field; |
| | | } |
| | | |
| | | function createDownloadFile(filename, titles, rows) { |
| | | var html = [ |
| | | '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">', |
| | | '<head><meta charset="UTF-8"></head><body><table border="1"><thead><tr>', |
| | | titles.map(function (title) { |
| | | return '<th>' + escapeHtml(title) + '</th>'; |
| | | }).join(''), |
| | | '</tr></thead><tbody>', |
| | | (rows || []).map(function (row) { |
| | | return '<tr>' + (row || []).map(function (value) { |
| | | return '<td style="mso-number-format:\\@;">' + escapeHtml(value) + '</td>'; |
| | | }).join('') + '</tr>'; |
| | | }).join(''), |
| | | '</tbody></table></body></html>' |
| | | ].join(''); |
| | | var blob = new Blob(['\ufeff' + html], { |
| | | type: 'application/vnd.ms-excel;charset=utf-8;' |
| | | }); |
| | | var anchor = document.createElement('a'); |
| | | anchor.href = URL.createObjectURL(blob); |
| | | anchor.download = filename; |
| | | document.body.appendChild(anchor); |
| | | anchor.click(); |
| | | setTimeout(function () { |
| | | URL.revokeObjectURL(anchor.href); |
| | | document.body.removeChild(anchor); |
| | | }, 0); |
| | | } |
| | | |
| | | function buildTimestamp() { |
| | | var now = new Date(); |
| | | var pad = function (num) { |
| | | return num < 10 ? '0' + num : String(num); |
| | | }; |
| | | return now.getFullYear() |
| | | + pad(now.getMonth() + 1) |
| | | + pad(now.getDate()) |
| | | + '_' |
| | | + pad(now.getHours()) |
| | | + pad(now.getMinutes()) |
| | | + pad(now.getSeconds()); |
| | | } |
| | | |
| | | function authHeaders() { |
| | | return { |
| | | token: localStorage.getItem('token') |
| | | }; |
| | | } |
| | | |
| | | function handleForbidden(res) { |
| | | if (res && res.code === 403) { |
| | | top.location.href = baseUrl + '/'; |
| | | if (res && Number(res.code) === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | var sharedMethods = { |
| | | authHeaders: authHeaders, |
| | | handleForbidden: handleForbidden, |
| | | valueOrDash: valueOrDash, |
| | | stringValue: stringValue, |
| | | getTableValue: getTableValue, |
| | | isCheckboxChecked: isCheckboxChecked, |
| | | normalizeOptionValue: normalizeOptionValue, |
| | | isSortableField: isSortableField, |
| | | getSuggestionFetcher: function (field) { |
| | | var self = this; |
| | | return function (queryString, callback) { |
| | | self.fetchForeignSuggestions(field, queryString, callback); |
| | | new Vue({ |
| | | el: "#app", |
| | | data: function () { |
| | | return { |
| | | loading: false, |
| | | dialogSubmitting: false, |
| | | resetSubmitting: false, |
| | | searchForm: { |
| | | username: "", |
| | | mobile: "" |
| | | }, |
| | | tableData: [], |
| | | selection: [], |
| | | roles: [], |
| | | page: { |
| | | curr: 1, |
| | | limit: 15, |
| | | total: 0 |
| | | }, |
| | | dialog: { |
| | | visible: false, |
| | | mode: "create" |
| | | }, |
| | | dialogForm: { |
| | | id: null, |
| | | username: "", |
| | | mobile: "", |
| | | password: "", |
| | | roleId: null, |
| | | status: 1, |
| | | mfaAllow: 0 |
| | | }, |
| | | resetDialog: { |
| | | visible: false, |
| | | userId: null, |
| | | username: "", |
| | | password: "" |
| | | }, |
| | | dialogRules: { |
| | | username: [ |
| | | { required: true, message: "请输入登录账户", trigger: "blur" } |
| | | ], |
| | | mobile: [ |
| | | { required: true, message: "请输入账号", trigger: "blur" } |
| | | ], |
| | | roleId: [ |
| | | { required: true, message: "请选择角色", trigger: "change" } |
| | | ], |
| | | password: [ |
| | | { |
| | | validator: function (rule, value, callback) { |
| | | if (this.dialog.mode !== "create") { |
| | | callback(); |
| | | return; |
| | | } |
| | | if (!value) { |
| | | callback(new Error("请输入初始密码")); |
| | | return; |
| | | } |
| | | if (String(value).length < 4) { |
| | | callback(new Error("初始密码不能少于4位")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }.bind(this), |
| | | trigger: "blur" |
| | | } |
| | | ] |
| | | }, |
| | | resetRules: { |
| | | password: [ |
| | | { required: true, message: "请输入新密码", trigger: "blur" }, |
| | | { |
| | | validator: function (rule, value, callback) { |
| | | if (String(value || "").length < 4) { |
| | | callback(new Error("新密码不能少于4位")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }, |
| | | trigger: "blur" |
| | | } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | | fetchForeignSuggestions: function (field, queryString, callback) { |
| | | if (!field.foreignQuery || !queryString) { |
| | | callback([]); |
| | | return; |
| | | } |
| | | var self = this; |
| | | $.ajax({ |
| | | url: baseUrl + '/' + field.foreignQuery + 'Query/auth', |
| | | method: 'GET', |
| | | headers: self.authHeaders(), |
| | | data: { condition: queryString }, |
| | | success: function (res) { |
| | | if (self.handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (!res || res.code !== 200 || !Array.isArray(res.data)) { |
| | | callback([]); |
| | | return; |
| | | } |
| | | callback(res.data.map(function (item) { |
| | | return { |
| | | id: item.id, |
| | | value: item.value |
| | | }; |
| | | })); |
| | | }, |
| | | error: function () { |
| | | callback([]); |
| | | } |
| | | }); |
| | | created: function () { |
| | | this.loadRoles(); |
| | | this.loadTable(); |
| | | }, |
| | | handleForeignSelect: function (field, item) { |
| | | this.$set(this.displayTarget, field.field, item && item.value ? item.value : ''); |
| | | this.$set(this.formTarget, field.field, item && item.id !== undefined ? normalizeOptionValue(field, item.id) : ''); |
| | | }, |
| | | handleForeignInput: function (field) { |
| | | if (!this.displayTarget || !this.formTarget) { |
| | | return; |
| | | } |
| | | if (this.displayTarget[field.field]) { |
| | | return; |
| | | } |
| | | this.$set(this.formTarget, field.field, ''); |
| | | } |
| | | }; |
| | | |
| | | if (document.getElementById('app')) { |
| | | new Vue({ |
| | | el: '#app', |
| | | data: function () { |
| | | return { |
| | | fieldMeta: fieldMeta, |
| | | primaryKeyField: primaryKeyField, |
| | | loading: false, |
| | | exporting: false, |
| | | tableData: [], |
| | | selection: [], |
| | | advancedFiltersVisible: false, |
| | | allColumns: fieldMeta.slice(), |
| | | visibleColumnKeys: createDefaultVisibleColumnKeys(), |
| | | searchForm: createSearchDefaults(), |
| | | searchDisplay: createSearchDisplayDefaults(), |
| | | page: { |
| | | curr: 1, |
| | | limit: 15, |
| | | total: 0 |
| | | }, |
| | | sortState: { |
| | | prop: '', |
| | | order: '' |
| | | }, |
| | | dialog: { |
| | | visible: false, |
| | | mode: 'create', |
| | | submitting: false |
| | | }, |
| | | layoutTimer: null, |
| | | tableResizeHandler: null, |
| | | dialogForm: createFormDefaults(), |
| | | dialogDisplay: createDisplayDefaults(), |
| | | dialogRules: createFormRules() |
| | | }; |
| | | methods: { |
| | | authHeaders: function () { |
| | | return { token: localStorage.getItem("token") }; |
| | | }, |
| | | computed: { |
| | | searchableFields: function () { |
| | | return this.fieldMeta.filter(function (field) { |
| | | return isSearchableField(field); |
| | | }); |
| | | }, |
| | | quickSearchableFields: function () { |
| | | var result = []; |
| | | this.searchableFields.forEach(function (field) { |
| | | if (result.length >= 3 || field.kind === 'date') { |
| | | loadRoles: function () { |
| | | var vm = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/role/list/auth", |
| | | method: "GET", |
| | | headers: vm.authHeaders(), |
| | | data: { |
| | | curr: 1, |
| | | limit: 500 |
| | | }, |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | result.push(field); |
| | | }); |
| | | return result; |
| | | }, |
| | | advancedSearchableFields: function () { |
| | | var quickKeys = this.quickSearchableFields.map(function (field) { |
| | | return field.field; |
| | | }); |
| | | return this.searchableFields.filter(function (field) { |
| | | return quickKeys.indexOf(field.field) === -1; |
| | | }); |
| | | }, |
| | | hasAdvancedFilters: function () { |
| | | return this.advancedSearchableFields.length > 0; |
| | | }, |
| | | visibleColumns: function () { |
| | | var keys = this.visibleColumnKeys; |
| | | return this.allColumns.filter(function (field) { |
| | | return keys.indexOf(field.field) !== -1; |
| | | }); |
| | | }, |
| | | editableFields: function () { |
| | | return this.fieldMeta.filter(function (field) { |
| | | return !field.primaryKey; |
| | | }); |
| | | }, |
| | | exportColumns: function () { |
| | | return this.visibleColumns.map(function (field) { |
| | | return { |
| | | field: field.exportField || field.tableProp || field.field, |
| | | label: field.label |
| | | }; |
| | | }); |
| | | }, |
| | | tableHeight: function () { |
| | | return this.advancedFiltersVisible && this.hasAdvancedFilters |
| | | ? 'calc(100vh - 390px)' |
| | | : 'calc(100vh - 300px)'; |
| | | }, |
| | | formTarget: function () { |
| | | return this.dialogForm; |
| | | }, |
| | | displayTarget: function () { |
| | | return this.dialogDisplay; |
| | | } |
| | | if (Number(res.code) !== 200) { |
| | | vm.$message.error(res.msg || "角色加载失败"); |
| | | return; |
| | | } |
| | | var payload = res.data || {}; |
| | | vm.roles = Array.isArray(payload.records) ? payload.records : []; |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("角色加载失败"); |
| | | } |
| | | }); |
| | | }, |
| | | created: function () { |
| | | loadTable: function () { |
| | | var vm = this; |
| | | vm.loading = true; |
| | | $.ajax({ |
| | | url: baseUrl + "/user/list/auth", |
| | | method: "GET", |
| | | headers: vm.authHeaders(), |
| | | data: { |
| | | curr: vm.page.curr, |
| | | limit: vm.page.limit, |
| | | username: vm.searchForm.username, |
| | | mobile: vm.searchForm.mobile |
| | | }, |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (Number(res.code) !== 200) { |
| | | vm.$message.error(res.msg || "加载失败"); |
| | | return; |
| | | } |
| | | var payload = res.data || {}; |
| | | vm.tableData = Array.isArray(payload.records) ? payload.records : []; |
| | | vm.page.total = Number(payload.total || 0); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("加载失败"); |
| | | }, |
| | | complete: function () { |
| | | vm.loading = false; |
| | | } |
| | | }); |
| | | }, |
| | | handleSearch: function () { |
| | | this.page.curr = 1; |
| | | this.loadTable(); |
| | | }, |
| | | mounted: function () { |
| | | var self = this; |
| | | self.requestTableLayout(80); |
| | | self.tableResizeHandler = function () { |
| | | self.requestTableLayout(80); |
| | | handleResetSearch: function () { |
| | | this.searchForm = { |
| | | username: "", |
| | | mobile: "" |
| | | }; |
| | | window.addEventListener('resize', self.tableResizeHandler); |
| | | this.page.curr = 1; |
| | | this.loadTable(); |
| | | }, |
| | | beforeDestroy: function () { |
| | | if (this.layoutTimer) { |
| | | clearTimeout(this.layoutTimer); |
| | | this.layoutTimer = null; |
| | | } |
| | | if (this.tableResizeHandler) { |
| | | window.removeEventListener('resize', this.tableResizeHandler); |
| | | this.tableResizeHandler = null; |
| | | } |
| | | handleSelectionChange: function (rows) { |
| | | this.selection = rows || []; |
| | | }, |
| | | methods: $.extend({}, sharedMethods, { |
| | | requestTableLayout: function (delay) { |
| | | var self = this; |
| | | if (self.layoutTimer) { |
| | | clearTimeout(self.layoutTimer); |
| | | handleCurrentChange: function (curr) { |
| | | this.page.curr = curr; |
| | | this.loadTable(); |
| | | }, |
| | | handleSizeChange: function (limit) { |
| | | this.page.limit = limit; |
| | | this.page.curr = 1; |
| | | this.loadTable(); |
| | | }, |
| | | openCreateDialog: function () { |
| | | this.dialog.mode = "create"; |
| | | this.dialog.visible = true; |
| | | this.dialogForm = { |
| | | id: null, |
| | | username: "", |
| | | mobile: "", |
| | | password: "", |
| | | roleId: null, |
| | | status: 1, |
| | | mfaAllow: 0 |
| | | }; |
| | | this.clearDialogValidate(); |
| | | }, |
| | | openEditDialog: function (row) { |
| | | this.dialog.mode = "edit"; |
| | | this.dialog.visible = true; |
| | | this.dialogForm = { |
| | | id: row.id, |
| | | username: row.username || "", |
| | | mobile: row.mobile || "", |
| | | password: "", |
| | | roleId: row.roleId == null ? null : Number(row.roleId), |
| | | status: row.status == null ? 1 : Number(row.status), |
| | | mfaAllow: row.mfaAllow == null ? 0 : Number(row.mfaAllow) |
| | | }; |
| | | this.clearDialogValidate(); |
| | | }, |
| | | clearDialogValidate: function () { |
| | | var vm = this; |
| | | vm.$nextTick(function () { |
| | | if (vm.$refs.dialogForm) { |
| | | vm.$refs.dialogForm.clearValidate(); |
| | | } |
| | | self.$nextTick(function () { |
| | | self.layoutTimer = setTimeout(function () { |
| | | var table = self.$refs.dataTable; |
| | | if (table && typeof table.doLayout === 'function') { |
| | | table.doLayout(); |
| | | }); |
| | | }, |
| | | closeDialog: function () { |
| | | this.dialog.visible = false; |
| | | }, |
| | | submitDialog: function () { |
| | | var vm = this; |
| | | if (!vm.$refs.dialogForm) { |
| | | return; |
| | | } |
| | | vm.$refs.dialogForm.validate(function (valid) { |
| | | if (!valid) { |
| | | return false; |
| | | } |
| | | vm.dialogSubmitting = true; |
| | | $.ajax({ |
| | | url: baseUrl + "/user/" + (vm.dialog.mode === "create" ? "add" : "update") + "/auth", |
| | | method: "POST", |
| | | headers: vm.authHeaders(), |
| | | data: vm.buildDialogPayload(), |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | }, delay || 40); |
| | | }); |
| | | }, |
| | | isColumnVisible: function (fieldName) { |
| | | return this.visibleColumnKeys.indexOf(fieldName) !== -1; |
| | | }, |
| | | toggleColumn: function (fieldName, visible) { |
| | | if (visible) { |
| | | if (this.visibleColumnKeys.indexOf(fieldName) === -1) { |
| | | this.visibleColumnKeys.push(fieldName); |
| | | if (Number(res.code) !== 200) { |
| | | vm.$message.error(res.msg || "保存失败"); |
| | | return; |
| | | } |
| | | vm.$message.success(res.msg || "保存成功"); |
| | | vm.dialog.visible = false; |
| | | vm.loadTable(); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("保存失败"); |
| | | }, |
| | | complete: function () { |
| | | vm.dialogSubmitting = false; |
| | | } |
| | | this.requestTableLayout(80); |
| | | return; |
| | | } |
| | | if (this.visibleColumnKeys.length === 1) { |
| | | this.$message.warning('至少保留一列'); |
| | | return; |
| | | } |
| | | this.visibleColumnKeys = this.visibleColumnKeys.filter(function (item) { |
| | | return item !== fieldName; |
| | | }); |
| | | this.requestTableLayout(80); |
| | | }, |
| | | selectAllColumns: function () { |
| | | this.visibleColumnKeys = createDefaultVisibleColumnKeys(); |
| | | this.requestTableLayout(80); |
| | | }, |
| | | resetColumns: function () { |
| | | this.visibleColumnKeys = createDefaultVisibleColumnKeys(); |
| | | this.requestTableLayout(80); |
| | | }, |
| | | toggleAdvancedFilters: function () { |
| | | this.advancedFiltersVisible = !this.advancedFiltersVisible; |
| | | this.requestTableLayout(260); |
| | | }, |
| | | handleSearchForeignSelect: function (field, item) { |
| | | this.$set(this.searchDisplay, field.field, item && item.value ? item.value : ''); |
| | | this.$set(this.searchForm, field.field, item && item.id !== undefined ? normalizeOptionValue(field, item.id) : ''); |
| | | }, |
| | | handleSearchForeignInput: function (field) { |
| | | if (this.searchDisplay[field.field]) { |
| | | return; |
| | | return true; |
| | | }); |
| | | }, |
| | | buildDialogPayload: function () { |
| | | var payload = { |
| | | username: this.dialogForm.username, |
| | | mobile: this.dialogForm.mobile, |
| | | roleId: this.dialogForm.roleId, |
| | | status: this.dialogForm.status, |
| | | mfaAllow: this.dialogForm.mfaAllow |
| | | }; |
| | | if (this.dialog.mode === "edit") { |
| | | payload.id = this.dialogForm.id; |
| | | } else { |
| | | payload.password = hex_md5(this.dialogForm.password); |
| | | } |
| | | return payload; |
| | | }, |
| | | openResetPasswordDialog: function (row) { |
| | | this.resetDialog = { |
| | | visible: true, |
| | | userId: row.id, |
| | | username: row.username || "", |
| | | password: "" |
| | | }; |
| | | var vm = this; |
| | | vm.$nextTick(function () { |
| | | if (vm.$refs.resetForm) { |
| | | vm.$refs.resetForm.clearValidate(); |
| | | } |
| | | this.$set(this.searchForm, field.field, ''); |
| | | }, |
| | | buildQueryParams: function () { |
| | | var self = this; |
| | | var params = { |
| | | curr: self.page.curr, |
| | | limit: self.page.limit |
| | | }; |
| | | if (self.searchForm.condition) { |
| | | params.condition = self.searchForm.condition; |
| | | }); |
| | | }, |
| | | closeResetDialog: function () { |
| | | this.resetDialog.visible = false; |
| | | }, |
| | | submitResetPassword: function () { |
| | | var vm = this; |
| | | if (!vm.$refs.resetForm) { |
| | | return; |
| | | } |
| | | vm.$refs.resetForm.validate(function (valid) { |
| | | if (!valid) { |
| | | return false; |
| | | } |
| | | self.searchableFields.forEach(function (field) { |
| | | var value = self.searchForm[field.field]; |
| | | if (field.kind === 'date') { |
| | | if (value && value.length === 2) { |
| | | params[resolveSearchParam(field)] = value[0] + ' - ' + value[1]; |
| | | vm.resetSubmitting = true; |
| | | $.ajax({ |
| | | url: baseUrl + "/user/resetPassword/auth", |
| | | method: "POST", |
| | | headers: vm.authHeaders(), |
| | | data: { |
| | | id: vm.resetDialog.userId, |
| | | password: hex_md5(vm.resetDialog.password) |
| | | }, |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (Number(res.code) !== 200) { |
| | | vm.$message.error(res.msg || "重置密码失败"); |
| | | return; |
| | | } |
| | | vm.$message.success("重置密码成功"); |
| | | vm.resetDialog.visible = false; |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("重置密码失败"); |
| | | }, |
| | | complete: function () { |
| | | vm.resetSubmitting = false; |
| | | } |
| | | }); |
| | | return true; |
| | | }); |
| | | }, |
| | | toggleStatus: function (row) { |
| | | var vm = this; |
| | | var currentStatus = Number(row.status) === 1 ? 1 : 0; |
| | | $.ajax({ |
| | | url: baseUrl + "/user/edit/auth", |
| | | method: "POST", |
| | | headers: vm.authHeaders(), |
| | | data: { |
| | | id: row.id, |
| | | status: currentStatus |
| | | }, |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (!isEmptyValue(value)) { |
| | | params[resolveSearchParam(field)] = value; |
| | | if (Number(res.code) !== 200) { |
| | | row.status = currentStatus === 1 ? 0 : 1; |
| | | vm.$message.error(res.msg || "状态更新失败"); |
| | | return; |
| | | } |
| | | }); |
| | | if (self.sortState.prop && self.sortState.order) { |
| | | params.orderByField = self.sortState.prop; |
| | | params.orderByType = self.sortState.order === 'ascending' ? 'asc' : 'desc'; |
| | | row.status$ = currentStatus === 1 ? "正常" : "禁用"; |
| | | vm.$message.success("状态已更新"); |
| | | }, |
| | | error: function () { |
| | | row.status = currentStatus === 1 ? 0 : 1; |
| | | vm.$message.error("状态更新失败"); |
| | | } |
| | | return params; |
| | | }, |
| | | loadTable: function () { |
| | | var self = this; |
| | | self.loading = true; |
| | | $.ajax({ |
| | | url: baseUrl + '/' + simpleEntityName + '/list/auth', |
| | | method: 'GET', |
| | | headers: self.authHeaders(), |
| | | data: self.buildQueryParams(), |
| | | success: function (res) { |
| | | self.loading = false; |
| | | if (self.handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '加载失败'); |
| | | return; |
| | | } |
| | | var payload = res.data || {}; |
| | | self.tableData = Array.isArray(payload.records) ? payload.records : []; |
| | | self.page.total = payload.total || 0; |
| | | self.requestTableLayout(80); |
| | | }, |
| | | error: function () { |
| | | self.loading = false; |
| | | self.requestTableLayout(80); |
| | | self.$message.error('加载失败'); |
| | | } |
| | | }); |
| | | }, |
| | | handleSearch: function () { |
| | | this.page.curr = 1; |
| | | this.loadTable(); |
| | | }, |
| | | handleReset: function () { |
| | | this.searchForm = createSearchDefaults(); |
| | | this.searchDisplay = createSearchDisplayDefaults(); |
| | | this.advancedFiltersVisible = false; |
| | | this.page.curr = 1; |
| | | this.sortState = { |
| | | prop: '', |
| | | order: '' |
| | | }; |
| | | this.loadTable(); |
| | | }, |
| | | handleSelectionChange: function (rows) { |
| | | this.selection = rows || []; |
| | | }, |
| | | handleSortChange: function (payload) { |
| | | this.sortState = { |
| | | prop: payload && payload.prop ? payload.prop : '', |
| | | order: payload && payload.order ? payload.order : '' |
| | | }; |
| | | this.page.curr = 1; |
| | | this.loadTable(); |
| | | }, |
| | | handleCurrentChange: function (curr) { |
| | | this.page.curr = curr; |
| | | this.loadTable(); |
| | | }, |
| | | handleSizeChange: function (limit) { |
| | | this.page.limit = limit; |
| | | this.page.curr = 1; |
| | | this.loadTable(); |
| | | }, |
| | | resetDialogState: function () { |
| | | this.dialogForm = createFormDefaults(); |
| | | this.dialogDisplay = createDisplayDefaults(); |
| | | if (this.$refs.dialogForm) { |
| | | this.$refs.dialogForm.clearValidate(); |
| | | } |
| | | }, |
| | | openCreateDialog: function () { |
| | | this.dialog.mode = 'create'; |
| | | this.dialog.visible = true; |
| | | this.$nextTick(this.resetDialogState); |
| | | }, |
| | | openEditDialog: function (row) { |
| | | var self = this; |
| | | self.dialog.mode = 'edit'; |
| | | self.dialog.visible = true; |
| | | self.$nextTick(function () { |
| | | self.resetDialogState(); |
| | | fillFormFromRow(row, self.dialogForm, self.dialogDisplay); |
| | | if (self.$refs.dialogForm) { |
| | | self.$refs.dialogForm.clearValidate(); |
| | | } |
| | | }); |
| | | }, |
| | | submitDialog: function () { |
| | | var self = this; |
| | | if (!self.$refs.dialogForm) { |
| | | return; |
| | | } |
| | | self.$refs.dialogForm.validate(function (valid) { |
| | | if (!valid) { |
| | | return false; |
| | | } |
| | | self.dialog.submitting = true; |
| | | $.ajax({ |
| | | url: baseUrl + '/' + simpleEntityName + '/' + (self.dialog.mode === 'create' ? 'add' : 'update') + '/auth', |
| | | method: 'POST', |
| | | headers: self.authHeaders(), |
| | | data: buildPayload(self.dialogForm), |
| | | success: function (res) { |
| | | self.dialog.submitting = false; |
| | | if (self.handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '保存失败'); |
| | | return; |
| | | } |
| | | self.$message.success(res.msg || '保存成功'); |
| | | self.dialog.visible = false; |
| | | self.loadTable(); |
| | | }, |
| | | error: function () { |
| | | self.dialog.submitting = false; |
| | | self.$message.error('保存失败'); |
| | | } |
| | | }); |
| | | return true; |
| | | }); |
| | | }, |
| | | removeSelection: function () { |
| | | var self = this; |
| | | var ids = self.selection.map(function (row) { |
| | | return row[self.primaryKeyField]; |
| | | }); |
| | | self.removeRows(ids); |
| | | }, |
| | | removeRows: function (ids) { |
| | | var self = this; |
| | | if (!ids || ids.length === 0) { |
| | | self.$message.warning('请选择要删除的数据'); |
| | | return; |
| | | } |
| | | self.$confirm('确定删除选中的记录吗?', '提示', { type: 'warning' }).then(function () { |
| | | $.ajax({ |
| | | url: baseUrl + '/' + simpleEntityName + '/delete/auth', |
| | | method: 'POST', |
| | | headers: self.authHeaders(), |
| | | traditional: true, |
| | | data: { 'ids[]': ids }, |
| | | success: function (res) { |
| | | if (self.handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '删除失败'); |
| | | return; |
| | | } |
| | | self.$message.success(res.msg || '删除成功'); |
| | | self.selection = []; |
| | | if (self.tableData.length === ids.length && self.page.curr > 1) { |
| | | self.page.curr = self.page.curr - 1; |
| | | } |
| | | self.loadTable(); |
| | | }, |
| | | error: function () { |
| | | self.$message.error('删除失败'); |
| | | } |
| | | }); |
| | | }).catch(function () {}); |
| | | }, |
| | | exportRows: function () { |
| | | var self = this; |
| | | self.exporting = true; |
| | | var requestBody = { |
| | | fields: self.exportColumns.map(function (item) { |
| | | return item.field; |
| | | }) |
| | | }; |
| | | requestBody[simpleEntityName] = self.buildQueryParams(); |
| | | $.ajax({ |
| | | url: baseUrl + '/' + simpleEntityName + '/export/auth', |
| | | method: 'POST', |
| | | headers: $.extend({ 'Content-Type': 'application/json;charset=UTF-8' }, self.authHeaders()), |
| | | data: JSON.stringify(requestBody), |
| | | success: function (res) { |
| | | self.exporting = false; |
| | | if (self.handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '导出失败'); |
| | | return; |
| | | } |
| | | createDownloadFile( |
| | | simpleEntityName + '_' + buildTimestamp() + '.xls', |
| | | self.exportColumns.map(function (item) { |
| | | return item.label; |
| | | }), |
| | | Array.isArray(res.data) ? res.data : [] |
| | | ); |
| | | self.$message.success('导出成功'); |
| | | }, |
| | | error: function () { |
| | | self.exporting = false; |
| | | self.$message.error('导出失败'); |
| | | } |
| | | }); |
| | | }); |
| | | }, |
| | | removeSelection: function () { |
| | | var ids = this.selection.map(function (item) { |
| | | return item.id; |
| | | }); |
| | | this.removeRows(ids); |
| | | }, |
| | | removeRows: function (ids) { |
| | | var vm = this; |
| | | if (!ids || ids.length === 0) { |
| | | vm.$message.warning("请选择要删除的数据"); |
| | | return; |
| | | } |
| | | }) |
| | | }); |
| | | } |
| | | |
| | | vm.$confirm("确定删除选中的记录吗?", "提示", { |
| | | type: "warning", |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消" |
| | | }).then(function () { |
| | | $.ajax({ |
| | | url: baseUrl + "/user/delete/auth", |
| | | method: "POST", |
| | | headers: vm.authHeaders(), |
| | | traditional: true, |
| | | data: { "ids[]": ids }, |
| | | success: function (res) { |
| | | if (handleForbidden(res)) { |
| | | return; |
| | | } |
| | | if (Number(res.code) !== 200) { |
| | | vm.$message.error(res.msg || "删除失败"); |
| | | return; |
| | | } |
| | | vm.$message.success("删除成功"); |
| | | vm.loadTable(); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("删除失败"); |
| | | } |
| | | }); |
| | | }).catch(function () { |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | })(); |
| New file |
| | |
| | | (function (window) { |
| | | "use strict"; |
| | | |
| | | function base64UrlToArrayBuffer(base64Url) { |
| | | var value = String(base64Url || "").replace(/-/g, "+").replace(/_/g, "/"); |
| | | var padding = value.length % 4; |
| | | if (padding) { |
| | | value += new Array(5 - padding).join("="); |
| | | } |
| | | var binary = window.atob(value); |
| | | var bytes = new Uint8Array(binary.length); |
| | | for (var i = 0; i < binary.length; i++) { |
| | | bytes[i] = binary.charCodeAt(i); |
| | | } |
| | | return bytes.buffer; |
| | | } |
| | | |
| | | function arrayBufferToBase64Url(buffer) { |
| | | var bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer || []); |
| | | var binary = ""; |
| | | for (var i = 0; i < bytes.length; i++) { |
| | | binary += String.fromCharCode(bytes[i]); |
| | | } |
| | | return window.btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); |
| | | } |
| | | |
| | | function normalizeArray(value) { |
| | | return Array.isArray(value) ? value : []; |
| | | } |
| | | |
| | | function toCreationOptions(payload) { |
| | | var publicKey = { |
| | | challenge: base64UrlToArrayBuffer(payload.challenge), |
| | | rp: { |
| | | id: payload.rpId, |
| | | name: payload.rpName || "WCS" |
| | | }, |
| | | user: { |
| | | id: base64UrlToArrayBuffer(payload.userId), |
| | | name: payload.userName, |
| | | displayName: payload.userDisplayName || payload.userName |
| | | }, |
| | | pubKeyCredParams: normalizeArray(payload.pubKeyCredParams), |
| | | timeout: Number(payload.timeout || 60000), |
| | | attestation: payload.attestation || "none", |
| | | authenticatorSelection: payload.authenticatorSelection || { |
| | | residentKey: "preferred", |
| | | userVerification: "required" |
| | | } |
| | | }; |
| | | var excludeCredentials = normalizeArray(payload.excludeCredentials).map(function (item) { |
| | | return { |
| | | type: item.type || "public-key", |
| | | id: base64UrlToArrayBuffer(item.id), |
| | | transports: normalizeArray(item.transports) |
| | | }; |
| | | }); |
| | | if (excludeCredentials.length) { |
| | | publicKey.excludeCredentials = excludeCredentials; |
| | | } |
| | | return { publicKey: publicKey }; |
| | | } |
| | | |
| | | function toRequestOptions(payload) { |
| | | var publicKey = { |
| | | challenge: base64UrlToArrayBuffer(payload.challenge), |
| | | rpId: payload.rpId, |
| | | timeout: Number(payload.timeout || 60000), |
| | | userVerification: payload.userVerification || "required" |
| | | }; |
| | | var allowCredentials = normalizeArray(payload.allowCredentials).map(function (item) { |
| | | return { |
| | | type: item.type || "public-key", |
| | | id: base64UrlToArrayBuffer(item.id), |
| | | transports: normalizeArray(item.transports) |
| | | }; |
| | | }); |
| | | if (allowCredentials.length) { |
| | | publicKey.allowCredentials = allowCredentials; |
| | | } |
| | | return { publicKey: publicKey }; |
| | | } |
| | | |
| | | function ensureSupported() { |
| | | if (!window.isSecureContext) { |
| | | throw new Error("secure-context"); |
| | | } |
| | | if (!window.PublicKeyCredential || !window.navigator || !window.navigator.credentials) { |
| | | throw new Error("not-supported"); |
| | | } |
| | | } |
| | | |
| | | async function register(payload) { |
| | | ensureSupported(); |
| | | var credential = await window.navigator.credentials.create(toCreationOptions(payload)); |
| | | if (!credential || !credential.response) { |
| | | throw new Error("create-empty"); |
| | | } |
| | | var response = credential.response; |
| | | if (typeof response.getPublicKey !== "function" || typeof response.getAuthenticatorData !== "function") { |
| | | throw new Error("extension-unsupported"); |
| | | } |
| | | var publicKey = response.getPublicKey(); |
| | | var authenticatorData = response.getAuthenticatorData(); |
| | | if (!publicKey || !authenticatorData) { |
| | | throw new Error("public-key-missing"); |
| | | } |
| | | return { |
| | | credentialId: arrayBufferToBase64Url(credential.rawId), |
| | | clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON), |
| | | authenticatorData: arrayBufferToBase64Url(authenticatorData), |
| | | publicKey: arrayBufferToBase64Url(publicKey), |
| | | publicKeyAlgorithm: response.getPublicKeyAlgorithm(), |
| | | transports: JSON.stringify(typeof response.getTransports === "function" ? response.getTransports() || [] : []) |
| | | }; |
| | | } |
| | | |
| | | async function authenticate(payload) { |
| | | ensureSupported(); |
| | | var credential = await window.navigator.credentials.get(toRequestOptions(payload)); |
| | | if (!credential || !credential.response) { |
| | | throw new Error("get-empty"); |
| | | } |
| | | var response = credential.response; |
| | | return { |
| | | credentialId: arrayBufferToBase64Url(credential.rawId), |
| | | clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON), |
| | | authenticatorData: arrayBufferToBase64Url(response.authenticatorData), |
| | | signature: arrayBufferToBase64Url(response.signature), |
| | | userHandle: response.userHandle ? arrayBufferToBase64Url(response.userHandle) : "" |
| | | }; |
| | | } |
| | | |
| | | window.WCS_WEBAUTHN = { |
| | | isSupported: function () { |
| | | return !!(window.isSecureContext && window.PublicKeyCredential && window.navigator && window.navigator.credentials); |
| | | }, |
| | | register: register, |
| | | authenticate: authenticate |
| | | }; |
| | | })(window); |
| New file |
| | |
| | | (function () { |
| | | var COLUMN_STORAGE_KEY = "wrk-mast-visible-columns"; |
| | | |
| | | var DEFAULT_COLUMNS = [ |
| | | { key: "wrkNo", prop: "wrkNo", label: "工作号", width: 110, sortable: true, align: "center" }, |
| | | { key: "wmsWrkNo", prop: "wmsWrkNo", label: "WMS任务号", minWidth: 160, sortable: true }, |
| | | { key: "wrkSts$", prop: "wrkSts$", label: "工作状态", minWidth: 120, sortable: true }, |
| | | { key: "ioType$", prop: "ioType$", label: "任务类型", minWidth: 120, sortable: true }, |
| | | { key: "ioPri", prop: "ioPri", label: "优先级", width: 90, sortable: true, align: "center" }, |
| | | { key: "sourceStaNo", prop: "sourceStaNo", label: "源站", width: 90, sortable: true, align: "center" }, |
| | | { key: "staNo", prop: "staNo", label: "目标站", width: 90, sortable: true, align: "center" }, |
| | | { key: "sourceLocNo", prop: "sourceLocNo", label: "源库位", minWidth: 140, sortable: true }, |
| | | { key: "locNo", prop: "locNo", label: "目标库位", minWidth: 140, sortable: true }, |
| | | { key: "modiTime$", prop: "modiTime$", label: "修改时间", minWidth: 168, sortable: true }, |
| | | { key: "barcode", prop: "barcode", label: "托盘码", minWidth: 150, sortable: true }, |
| | | { key: "crnNo", prop: "crnNo", label: "堆垛机", width: 90, sortable: true, align: "center" }, |
| | | { key: "dualCrnNo", prop: "dualCrnNo", label: "双工位堆垛机", minWidth: 120, sortable: true, align: "center" }, |
| | | { key: "batch", prop: "batch", label: "批次", minWidth: 120, sortable: true }, |
| | | { key: "batchSeq", prop: "batchSeq", label: "批次序列", width: 100, sortable: true, align: "center" }, |
| | | { key: "systemMsg", prop: "systemMsg", label: "系统消息", minWidth: 220, sortable: false, showOverflow: false } |
| | | ]; |
| | | |
| | | function cloneSearchForm() { |
| | | return { |
| | | condition: "", |
| | | wrk_no: "", |
| | | wms_wrk_no: "", |
| | | loc_no: "", |
| | | source_loc_no: "", |
| | | crn_no: "", |
| | | dual_crn_no: "" |
| | | }; |
| | | } |
| | | |
| | | function loadStoredColumns() { |
| | | try { |
| | | var raw = localStorage.getItem(COLUMN_STORAGE_KEY); |
| | | var parsed = raw ? JSON.parse(raw) : null; |
| | | if (!parsed || !parsed.length) { |
| | | return DEFAULT_COLUMNS.map(function (column) { return column.key; }); |
| | | } |
| | | return DEFAULT_COLUMNS.map(function (column) { return column.key; }).filter(function (key) { |
| | | return parsed.indexOf(key) > -1; |
| | | }); |
| | | } catch (e) { |
| | | return DEFAULT_COLUMNS.map(function (column) { return column.key; }); |
| | | } |
| | | } |
| | | |
| | | function saveVisibleColumns(keys) { |
| | | localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(keys)); |
| | | } |
| | | |
| | | new Vue({ |
| | | el: "#app", |
| | | data: function () { |
| | | return { |
| | | loading: false, |
| | | advancedVisible: false, |
| | | columnPopoverVisible: false, |
| | | tableData: [], |
| | | currentPage: 1, |
| | | pageSize: 30, |
| | | pageSizes: [16, 30, 50, 100, 150, 200], |
| | | pageTotal: 0, |
| | | tableHeight: 520, |
| | | searchForm: cloneSearchForm(), |
| | | sortState: { |
| | | prop: "", |
| | | order: "" |
| | | }, |
| | | columnDefs: DEFAULT_COLUMNS, |
| | | visibleColumnKeys: loadStoredColumns(), |
| | | layoutTimer: null |
| | | }; |
| | | }, |
| | | computed: { |
| | | visibleColumns: function () { |
| | | var keys = this.visibleColumnKeys; |
| | | return this.columnDefs.filter(function (column) { |
| | | return keys.indexOf(column.key) > -1; |
| | | }); |
| | | }, |
| | | tableRenderKey: function () { |
| | | return this.visibleColumnKeys.join("|"); |
| | | } |
| | | }, |
| | | created: function () { |
| | | this.loadList(); |
| | | }, |
| | | mounted: function () { |
| | | this.updateTableHeight(); |
| | | window.addEventListener("resize", this.handleResize); |
| | | }, |
| | | beforeDestroy: function () { |
| | | window.removeEventListener("resize", this.handleResize); |
| | | if (this.layoutTimer) { |
| | | clearTimeout(this.layoutTimer); |
| | | this.layoutTimer = null; |
| | | } |
| | | }, |
| | | methods: { |
| | | buildQueryParams: function () { |
| | | var data = { |
| | | curr: this.currentPage, |
| | | limit: this.pageSize |
| | | }; |
| | | var key; |
| | | for (key in this.searchForm) { |
| | | if (Object.prototype.hasOwnProperty.call(this.searchForm, key) && this.searchForm[key] !== "" && this.searchForm[key] !== null) { |
| | | data[key] = this.searchForm[key]; |
| | | } |
| | | } |
| | | if (this.sortState.prop && this.sortState.order) { |
| | | data.orderByField = this.sortState.prop; |
| | | data.orderByType = this.sortState.order === "ascending" ? "asc" : "desc"; |
| | | } |
| | | return data; |
| | | }, |
| | | loadList: function () { |
| | | var vm = this; |
| | | vm.loading = true; |
| | | $.ajax({ |
| | | url: baseUrl + "/wrkMast/list/auth", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | method: "GET", |
| | | data: vm.buildQueryParams(), |
| | | success: function (res) { |
| | | if (res.code === 200) { |
| | | vm.tableData = (res.data && res.data.records) || []; |
| | | vm.pageTotal = (res.data && res.data.total) || 0; |
| | | vm.scheduleTableLayout(); |
| | | return; |
| | | } |
| | | if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || "任务列表加载失败"); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("任务列表加载失败"); |
| | | }, |
| | | complete: function () { |
| | | vm.loading = false; |
| | | } |
| | | }); |
| | | }, |
| | | handleSearch: function () { |
| | | this.currentPage = 1; |
| | | this.loadList(); |
| | | }, |
| | | handleReset: function () { |
| | | this.searchForm = cloneSearchForm(); |
| | | this.currentPage = 1; |
| | | this.sortState = { |
| | | prop: "", |
| | | order: "" |
| | | }; |
| | | this.loadList(); |
| | | this.scheduleTableLayout(); |
| | | }, |
| | | toggleAdvanced: function () { |
| | | this.advancedVisible = !this.advancedVisible; |
| | | this.updateTableHeight(); |
| | | this.scheduleTableLayout(); |
| | | }, |
| | | handleSizeChange: function (size) { |
| | | this.pageSize = size; |
| | | this.currentPage = 1; |
| | | this.loadList(); |
| | | }, |
| | | handleCurrentChange: function (page) { |
| | | this.currentPage = page; |
| | | this.loadList(); |
| | | }, |
| | | handleSortChange: function (sort) { |
| | | this.sortState = { |
| | | prop: sort.prop || "", |
| | | order: sort.order || "" |
| | | }; |
| | | this.currentPage = 1; |
| | | this.loadList(); |
| | | }, |
| | | isColumnVisible: function (key) { |
| | | return this.visibleColumnKeys.indexOf(key) > -1; |
| | | }, |
| | | toggleColumn: function (key, checked) { |
| | | var next = this.visibleColumnKeys.slice(); |
| | | var index = next.indexOf(key); |
| | | |
| | | if (checked && index === -1) { |
| | | next.push(key); |
| | | } |
| | | if (!checked && index > -1) { |
| | | if (next.length === 1) { |
| | | this.$message.warning("至少保留一列"); |
| | | return; |
| | | } |
| | | next.splice(index, 1); |
| | | } |
| | | |
| | | this.visibleColumnKeys = this.columnDefs.map(function (column) { |
| | | return column.key; |
| | | }).filter(function (columnKey) { |
| | | return next.indexOf(columnKey) > -1; |
| | | }); |
| | | saveVisibleColumns(this.visibleColumnKeys); |
| | | this.scheduleTableLayout(); |
| | | }, |
| | | showAllColumns: function () { |
| | | this.visibleColumnKeys = this.columnDefs.map(function (column) { |
| | | return column.key; |
| | | }); |
| | | saveVisibleColumns(this.visibleColumnKeys); |
| | | this.scheduleTableLayout(); |
| | | }, |
| | | resetColumns: function () { |
| | | this.visibleColumnKeys = DEFAULT_COLUMNS.map(function (column) { |
| | | return column.key; |
| | | }); |
| | | saveVisibleColumns(this.visibleColumnKeys); |
| | | this.scheduleTableLayout(); |
| | | }, |
| | | handleRowCommand: function (command, row) { |
| | | if (command === "complete") { |
| | | this.completeTask(row); |
| | | return; |
| | | } |
| | | if (command === "cancel") { |
| | | this.cancelTask(row); |
| | | } |
| | | }, |
| | | completeTask: function (row) { |
| | | var vm = this; |
| | | vm.$confirm("确定完成该任务吗?", "提示", { |
| | | type: "warning", |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消" |
| | | }).then(function () { |
| | | $.ajax({ |
| | | url: baseUrl + "/openapi/completeTask", |
| | | contentType: "application/json", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | data: JSON.stringify({ wrkNo: row.wrkNo }), |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (res.code === 200) { |
| | | vm.$message.success("完成成功"); |
| | | vm.loadList(); |
| | | return; |
| | | } |
| | | if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || "完成失败"); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("完成失败"); |
| | | } |
| | | }); |
| | | }).catch(function () {}); |
| | | }, |
| | | cancelTask: function (row) { |
| | | var vm = this; |
| | | vm.$confirm("确定取消该任务吗?", "提示", { |
| | | type: "warning", |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消" |
| | | }).then(function () { |
| | | $.ajax({ |
| | | url: baseUrl + "/openapi/cancelTask", |
| | | contentType: "application/json", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | data: JSON.stringify({ wrkNo: row.wrkNo }), |
| | | method: "POST", |
| | | success: function (res) { |
| | | if (res.code === 200) { |
| | | vm.$message.success("取消成功"); |
| | | vm.loadList(); |
| | | return; |
| | | } |
| | | if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | return; |
| | | } |
| | | vm.$message.error(res.msg || "取消失败"); |
| | | }, |
| | | error: function () { |
| | | vm.$message.error("取消失败"); |
| | | } |
| | | }); |
| | | }).catch(function () {}); |
| | | }, |
| | | updateTableHeight: function () { |
| | | var viewport = window.innerHeight || document.documentElement.clientHeight || 860; |
| | | this.tableHeight = Math.max(340, viewport - (this.advancedVisible ? 276 : 222)); |
| | | }, |
| | | scheduleTableLayout: function () { |
| | | var vm = this; |
| | | vm.updateTableHeight(); |
| | | vm.$nextTick(function () { |
| | | if (vm.layoutTimer) { |
| | | clearTimeout(vm.layoutTimer); |
| | | } |
| | | vm.layoutTimer = setTimeout(function () { |
| | | if (vm.$refs.dataTable && typeof vm.$refs.dataTable.doLayout === "function") { |
| | | vm.$refs.dataTable.doLayout(); |
| | | } |
| | | }, 50); |
| | | }); |
| | | }, |
| | | handleResize: function () { |
| | | this.scheduleTableLayout(); |
| | | } |
| | | } |
| | | }); |
| | | })(); |
| | |
| | | |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script src="../../static/js/marked.min.js"></script> |
| | | <script src="../../static/js/purify.min.js"></script> |
| | | <script> |
| | |
| | | |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script> |
| | | new Vue({ |
| | | el: '#app', |
| | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/basOutStationArea/basOutStationArea.js"></script> |
| | | </body> |
| | | </html> |
| | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/basStationDevice/basStationDevice.js"></script> |
| | | </body> |
| | | </html> |
| | |
| | | html, |
| | | body { |
| | | margin: 0; |
| | | height: 100%; |
| | | min-height: 100%; |
| | | color: var(--text-main); |
| | | font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; |
| | |
| | | .page-shell { |
| | | max-width: 1700px; |
| | | margin: 0 auto; |
| | | height: 100%; |
| | | padding: 14px; |
| | | box-sizing: border-box; |
| | | display: flex; |
| | | } |
| | | |
| | | .card-shell { |
| | | position: relative; |
| | | flex: 1 1 auto; |
| | | display: flex; |
| | | border-radius: 24px; |
| | | border: 1px solid var(--card-border); |
| | | background: |
| | |
| | | .card-body { |
| | | position: relative; |
| | | z-index: 1; |
| | | flex: 1 1 auto; |
| | | min-height: 0; |
| | | min-width: 0; |
| | | width: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .list-toolbar { |
| | |
| | | |
| | | .table-wrap { |
| | | padding: 10px 16px; |
| | | flex: 1 1 auto; |
| | | min-height: 0; |
| | | min-width: 0; |
| | | width: 100%; |
| | | display: flex; |
| | | } |
| | | |
| | | .table-shell { |
| | |
| | | overflow: hidden; |
| | | border: 1px solid rgba(217, 227, 238, 0.98); |
| | | background: rgba(255, 255, 255, 0.95); |
| | | flex: 1 1 auto; |
| | | min-height: 0; |
| | | min-width: 0; |
| | | width: 100%; |
| | | max-width: 100%; |
| | | } |
| | | |
| | | .table-shell .el-table { |
| | |
| | | <el-button slot="reference" size="small" plain icon="el-icon-setting">列设置</el-button> |
| | | </el-popover> |
| | | <el-button size="small" plain icon="el-icon-download" :loading="exporting" @click="exportRows">导出</el-button> |
| | | <el-button size="small" plain type="warning" icon="el-icon-refresh-right" @click="refreshCache">刷新缓存</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | </div> |
| | | </el-collapse-transition> |
| | | |
| | | <div class="table-wrap"> |
| | | <div ref="tableWrap" class="table-wrap"> |
| | | <div class="table-shell"> |
| | | <el-table |
| | | ref="dataTable" |
| | |
| | | <span v-else>{{ valueOrDash(getTableValue(scope.row, field)) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="160" fixed="right" align="center"> |
| | | <el-table-column label="操作" width="190" fixed="right" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-button type="text" @click="openDetailDialog(scope.row)">详情</el-button> |
| | | <el-button type="text" @click="openEditDialog(scope.row)">修改</el-button> |
| | | <el-button type="text" style="color:#f56c6c;" @click="removeRows([scope.row[primaryKeyField]])">删除</el-button> |
| | | </template> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="pager-bar"> |
| | | <div ref="pagerBar" class="pager-bar"> |
| | | <el-pagination |
| | | small |
| | | background |
| | |
| | | |
| | | <el-dialog |
| | | class="dialog-panel" |
| | | :title="dialog.mode === 'create' ? '新增 Config' : '修改 Config'" |
| | | :title="dialog.mode === 'create' ? '新增 Config' : (dialog.mode === 'detail' ? '详情 Config' : '修改 Config')" |
| | | :visible.sync="dialog.visible" |
| | | width="760px" |
| | | :close-on-click-modal="false"> |
| | |
| | | <el-col |
| | | v-for="field in editableFields" |
| | | :key="'dialog-' + field.field" |
| | | :span="field.textarea || field.kind === 'image' ? 24 : 12"> |
| | | :span="field.dialogSpan || (field.textarea || field.kind === 'image' ? 24 : 12)"> |
| | | <el-form-item :label="field.label" :prop="field.field"> |
| | | <el-date-picker |
| | | v-if="field.kind === 'date'" |
| | |
| | | type="datetime" |
| | | value-format="yyyy-MM-dd HH:mm:ss" |
| | | :placeholder="'请选择' + field.label" |
| | | :disabled="isDialogReadonly" |
| | | style="width: 100%;"> |
| | | </el-date-picker> |
| | | <el-select |
| | | v-else-if="field.kind === 'enum'" |
| | | v-model="dialogForm[field.field]" |
| | | clearable |
| | | :disabled="isDialogReadonly" |
| | | :placeholder="'请选择' + field.label" |
| | | style="width: 100%;"> |
| | | <el-option |
| | |
| | | v-model="dialogDisplay[field.field]" |
| | | :fetch-suggestions="getSuggestionFetcher(field)" |
| | | :placeholder="'请输入' + field.label" |
| | | :disabled="isDialogReadonly" |
| | | style="width: 100%;" |
| | | @select="handleForeignSelect(field, $event)" |
| | | @input="handleForeignInput(field)"> |
| | |
| | | <el-switch |
| | | v-else-if="field.kind === 'checkbox'" |
| | | v-model="dialogForm[field.field]" |
| | | :disabled="isDialogReadonly" |
| | | :active-value="normalizeOptionValue(field, field.checkboxActiveRaw)" |
| | | :inactive-value="normalizeOptionValue(field, field.checkboxInactiveRaw)" |
| | | active-color="#13ce66" |
| | |
| | | v-model.trim="dialogForm[field.field]" |
| | | type="textarea" |
| | | :rows="3" |
| | | :disabled="isDialogReadonly" |
| | | :placeholder="'请输入' + field.label"> |
| | | </el-input> |
| | | <el-input |
| | | v-else |
| | | v-model.trim="dialogForm[field.field]" |
| | | :disabled="isDialogReadonly" |
| | | :placeholder="'请输入' + field.label"> |
| | | </el-input> |
| | | </el-form-item> |
| | |
| | | </el-form> |
| | | <div slot="footer" class="dialog-footer"> |
| | | <el-button @click="dialog.visible = false">取消</el-button> |
| | | <el-button type="primary" :loading="dialog.submitting" @click="submitDialog">保存</el-button> |
| | | <el-button v-if="!isDialogReadonly" type="primary" :loading="dialog.submitting" @click="submitDialog">保存</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | </div> |
| | |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/config/config.js?v=20260310" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/config/config.js" charset="utf-8"></script> |
| | | </body> |
| | | </html> |
| | |
| | | <title>调试参数</title> |
| | | <link rel="stylesheet" href="../../static/vue/element/element.css"> |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <style> |
| | |
| | | flex: 0 0 118px; |
| | | } |
| | | |
| | | .mfa-panel { |
| | | border: 1px solid rgba(222, 230, 239, 0.92); |
| | | border-radius: 14px; |
| | | background: #f8fbff; |
| | | padding: 14px 16px; |
| | | } |
| | | |
| | | .mfa-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .mfa-title { |
| | | font-size: 14px; |
| | | font-weight: 700; |
| | | color: #2f4358; |
| | | } |
| | | |
| | | .mfa-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .mfa-meta { |
| | | display: grid; |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | } |
| | | |
| | | .mfa-meta-item { |
| | | padding: 10px 12px; |
| | | border-radius: 12px; |
| | | background: rgba(255, 255, 255, 0.95); |
| | | border: 1px solid rgba(226, 233, 242, 0.96); |
| | | } |
| | | |
| | | .mfa-meta-label { |
| | | font-size: 12px; |
| | | color: #7b8b9b; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .mfa-meta-value { |
| | | font-size: 13px; |
| | | color: #2f4358; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .mfa-tip { |
| | | margin-top: 10px; |
| | | font-size: 12px; |
| | | line-height: 1.7; |
| | | color: #7b8b9b; |
| | | } |
| | | |
| | | .footer-bar { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .password-dialog .el-dialog__header { |
| | | .password-dialog .el-dialog__header, |
| | | .mfa-dialog .el-dialog__header { |
| | | padding: 18px 20px 12px; |
| | | border-bottom: 1px solid rgba(222, 230, 239, 0.92); |
| | | background: #f8fbff; |
| | | } |
| | | |
| | | .password-dialog .el-dialog__title { |
| | | .password-dialog .el-dialog__title, |
| | | .mfa-dialog .el-dialog__title { |
| | | font-weight: 700; |
| | | color: var(--text-main); |
| | | } |
| | | |
| | | .password-dialog .el-dialog__body { |
| | | .password-dialog .el-dialog__body, |
| | | .mfa-dialog .el-dialog__body { |
| | | padding: 18px 20px 12px; |
| | | } |
| | | |
| | |
| | | padding-top: 4px; |
| | | } |
| | | |
| | | .mfa-setup { |
| | | margin-bottom: 16px; |
| | | padding: 14px; |
| | | border-radius: 14px; |
| | | background: rgba(248, 251, 255, 0.92); |
| | | border: 1px solid rgba(226, 233, 242, 0.96); |
| | | } |
| | | |
| | | .mfa-setup-tip { |
| | | font-size: 12px; |
| | | line-height: 1.7; |
| | | color: #738396; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .mfa-qr-wrap { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-height: 220px; |
| | | margin-bottom: 14px; |
| | | background: #fff; |
| | | border-radius: 14px; |
| | | border: 1px dashed rgba(210, 220, 233, 0.96); |
| | | } |
| | | |
| | | .mfa-qr-wrap img { |
| | | width: 220px; |
| | | height: 220px; |
| | | object-fit: contain; |
| | | } |
| | | |
| | | .mfa-secret-row { |
| | | display: flex; |
| | | gap: 10px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .mfa-secret-row .el-input { |
| | | flex: 1; |
| | | } |
| | | |
| | | .mfa-footer { |
| | | display: flex; |
| | | justify-content: center; |
| | | gap: 12px; |
| | | padding-top: 6px; |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .page-shell { |
| | | padding: 10px; |
| | | } |
| | | |
| | | .password-row { |
| | | .password-row, |
| | | .mfa-secret-row { |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .form-shell { |
| | | max-width: none; |
| | | } |
| | | |
| | | .mfa-head { |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .mfa-meta { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <div class="card-body"> |
| | | <div class="form-shell"> |
| | | <input id="id" type="hidden" v-model="form.id"> |
| | | <input id="password" type="hidden" v-model="form.password"> |
| | | <el-form |
| | | ref="profileForm" |
| | | class="profile-form" |
| | |
| | | <div class="password-row"> |
| | | <el-input class="password-mask" value="已设置密码" disabled></el-input> |
| | | <el-button type="primary" plain icon="el-icon-lock" @click="openPasswordDialog">修改密码</el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :xs="24"> |
| | | <el-form-item label="MFA"> |
| | | <div class="mfa-panel"> |
| | | <div class="mfa-head"> |
| | | <div class="mfa-title">多因子登录验证</div> |
| | | <div class="mfa-actions"> |
| | | <el-button |
| | | v-if="Number(form.mfaAllow) === 1 && Number(form.mfaEnabled) !== 1" |
| | | type="primary" |
| | | plain |
| | | icon="el-icon-key" |
| | | @click="openMfaEnableDialog">启用 MFA</el-button> |
| | | <el-button |
| | | v-if="Number(form.mfaEnabled) === 1" |
| | | type="danger" |
| | | plain |
| | | icon="el-icon-switch-button" |
| | | @click="openMfaDisableDialog">停用 MFA</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="mfa-meta"> |
| | | <div class="mfa-meta-item"> |
| | | <div class="mfa-meta-label">账号授权</div> |
| | | <div class="mfa-meta-value">{{ form.mfaAllow$ || '--' }}</div> |
| | | </div> |
| | | <div class="mfa-meta-item"> |
| | | <div class="mfa-meta-label">启用状态</div> |
| | | <div class="mfa-meta-value">{{ form.mfaEnabled$ || '--' }}</div> |
| | | </div> |
| | | <div class="mfa-meta-item"> |
| | | <div class="mfa-meta-label">绑定时间</div> |
| | | <div class="mfa-meta-value">{{ form.mfaBoundTime$ || '--' }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="mfa-tip"> |
| | | <span v-if="Number(form.mfaAllow) !== 1">当前账号未开通 MFA 使用权限,请联系管理员授权。</span> |
| | | <span v-else-if="Number(form.mfaEnabled) === 1">已绑定密钥:{{ form.mfaMaskedSecret || '--' }}。登录时需要再输入一次动态验证码。</span> |
| | | <span v-else>当前账号已允许使用 MFA,启用后登录会增加一次 6 位动态验证码校验。</span> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :xs="24"> |
| | | <el-form-item label="通行密钥"> |
| | | <div class="mfa-panel"> |
| | | <div class="mfa-head"> |
| | | <div class="mfa-title">设备生物识别 / 安全密钥登录</div> |
| | | <div class="mfa-actions"> |
| | | <el-button |
| | | v-if="!form.passkeyBound" |
| | | type="primary" |
| | | plain |
| | | icon="el-icon-key" |
| | | @click="openPasskeyRegisterDialog">绑定通行密钥</el-button> |
| | | <el-button |
| | | v-else |
| | | type="danger" |
| | | plain |
| | | icon="el-icon-delete" |
| | | @click="openPasskeyRemoveDialog">解绑通行密钥</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="mfa-meta"> |
| | | <div class="mfa-meta-item"> |
| | | <div class="mfa-meta-label">绑定状态</div> |
| | | <div class="mfa-meta-value">{{ form.passkeyBound ? '已绑定' : '未绑定' }}</div> |
| | | </div> |
| | | <div class="mfa-meta-item"> |
| | | <div class="mfa-meta-label">显示名称</div> |
| | | <div class="mfa-meta-value">{{ form.passkeyName || '--' }}</div> |
| | | </div> |
| | | <div class="mfa-meta-item"> |
| | | <div class="mfa-meta-label">绑定时间</div> |
| | | <div class="mfa-meta-value">{{ form.passkeyBoundTime$ || '--' }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="mfa-tip"> |
| | | <span v-if="form.passkeyBound">最近使用:{{ form.passkeyLastUsedTime$ || '--' }}</span> |
| | | <span v-else>绑定后可直接使用设备指纹、人脸或安全密钥登录。该能力要求浏览器支持通行密钥,且系统以 HTTPS 或 localhost 打开。</span> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | </div> |
| | | </el-form> |
| | | </el-dialog> |
| | | |
| | | <el-dialog |
| | | class="mfa-dialog" |
| | | :title="mfaDialogMode === 'enable' ? '启用 MFA' : '停用 MFA'" |
| | | :visible.sync="mfaDialogVisible" |
| | | width="520px" |
| | | :close-on-click-modal="false" |
| | | @close="closeMfaDialog" |
| | | append-to-body> |
| | | <div v-if="mfaDialogMode === 'enable'" class="mfa-setup"> |
| | | <div class="mfa-setup-tip">请先使用 Google Authenticator、Microsoft Authenticator 等身份验证器扫描二维码,再输入当前密码和 6 位动态码完成绑定。</div> |
| | | <div class="mfa-qr-wrap" v-loading="mfaSetupLoading"> |
| | | <img v-if="mfaSetup.qrCode" :src="mfaSetup.qrCode" alt="MFA QR Code"> |
| | | <span v-else>二维码生成中...</span> |
| | | </div> |
| | | <div class="mfa-secret-row"> |
| | | <el-input :value="mfaSetup.secret" readonly placeholder="MFA密钥"></el-input> |
| | | <el-button plain @click="copySecret">复制密钥</el-button> |
| | | </div> |
| | | </div> |
| | | <div v-else class="mfa-setup"> |
| | | <div class="mfa-setup-tip">停用 MFA 前,请输入当前密码和身份验证器中的 6 位动态码进行确认。</div> |
| | | </div> |
| | | <el-form |
| | | ref="mfaForm" |
| | | class="password-form" |
| | | :model="mfaForm" |
| | | :rules="mfaRules" |
| | | label-width="112px" |
| | | size="small" |
| | | @submit.native.prevent> |
| | | <el-form-item label="当前密码" prop="currentPassword"> |
| | | <el-input v-model="mfaForm.currentPassword" type="password" show-password autocomplete="off"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label="动态验证码" prop="code"> |
| | | <el-input v-model.trim="mfaForm.code" maxlength="6" autocomplete="off" placeholder="请输入6位验证码"></el-input> |
| | | </el-form-item> |
| | | <div class="mfa-footer"> |
| | | <el-button @click="closeMfaDialog">关闭</el-button> |
| | | <el-button type="primary" :loading="mfaSubmitting" @click="handleMfaSubmit">保存</el-button> |
| | | </div> |
| | | </el-form> |
| | | </el-dialog> |
| | | |
| | | <el-dialog |
| | | class="mfa-dialog" |
| | | :title="passkeyDialogMode === 'register' ? '绑定通行密钥' : '解绑通行密钥'" |
| | | :visible.sync="passkeyDialogVisible" |
| | | width="520px" |
| | | :close-on-click-modal="false" |
| | | @close="closePasskeyDialog" |
| | | append-to-body> |
| | | <div class="mfa-setup"> |
| | | <div v-if="passkeyDialogMode === 'register'" class="mfa-setup-tip">绑定时会弹出系统级身份验证窗口,请使用当前设备的人脸、指纹、PIN 或安全密钥完成确认。若浏览器或环境不支持,无法启用该能力。</div> |
| | | <div v-else class="mfa-setup-tip">解绑前请输入当前密码确认。解绑后将不能再用当前通行密钥直接登录。</div> |
| | | </div> |
| | | <el-form |
| | | ref="passkeyForm" |
| | | class="password-form" |
| | | :model="passkeyForm" |
| | | :rules="passkeyRules" |
| | | label-width="112px" |
| | | size="small" |
| | | @submit.native.prevent> |
| | | <el-form-item v-if="passkeyDialogMode === 'register'" label="显示名称" prop="name"> |
| | | <el-input v-model.trim="passkeyForm.name" maxlength="100" autocomplete="off" placeholder="例如:办公室电脑"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label="当前密码" prop="currentPassword"> |
| | | <el-input v-model="passkeyForm.currentPassword" type="password" show-password autocomplete="off"></el-input> |
| | | </el-form-item> |
| | | <div class="mfa-footer"> |
| | | <el-button @click="closePasskeyDialog">关闭</el-button> |
| | | <el-button type="primary" :loading="passkeySubmitting" @click="handlePasskeySubmit">保存</el-button> |
| | | </div> |
| | | </el-form> |
| | | </el-dialog> |
| | | </div> |
| | | </body> |
| | | <script type="text/javascript" src="../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../static/js/tools/md5.js"></script> |
| | | <script type="text/javascript" src="../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../static/js/webauthn-utils.js"></script> |
| | | <script type="text/javascript" src="../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../static/js/detail/detail.js?v=20260310_detail_vue3"></script> |
| | | <script type="text/javascript" src="../static/js/detail/detail.js?v=20260311_detail_passkey"></script> |
| | | </html> |
| | |
| | | </div> |
| | | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script src="../../static/vue/js/vue.min.js"></script> |
| | | <script src="../../static/vue/element/element.js"></script> |
| | | <script src="../../components/MonitorCardKit.js"></script> |
| | |
| | | will-change: width; |
| | | transition: width 0.22s cubic-bezier(0.22, 1, 0.36, 1), |
| | | box-shadow 0.22s ease; |
| | | -webkit-user-select: none; |
| | | user-select: none; |
| | | } |
| | | |
| | | .layout-aside.is-animating { |
| | |
| | | border: 1px solid rgba(255, 255, 255, 0.10); |
| | | background: rgba(255, 255, 255, 0.08); |
| | | color: #fff; |
| | | -webkit-user-select: text; |
| | | user-select: text; |
| | | } |
| | | |
| | | .aside-search .el-input__inner::placeholder { |
| | |
| | | background: #fff; |
| | | border-bottom: 1px solid #e8edf5; |
| | | box-sizing: border-box; |
| | | -webkit-user-select: none; |
| | | user-select: none; |
| | | } |
| | | |
| | | .page-tabs { |
| | |
| | | .page-tabs .el-tabs__item { |
| | | height: 38px; |
| | | line-height: 38px; |
| | | -webkit-user-select: none; |
| | | user-select: none; |
| | | } |
| | | |
| | | .tabs-tools { |
| | |
| | | gap: 8px; |
| | | padding-bottom: 6px; |
| | | flex-shrink: 0; |
| | | -webkit-user-select: none; |
| | | user-select: none; |
| | | } |
| | | |
| | | .content-main { |
| | |
| | | word-break: break-word; |
| | | } |
| | | |
| | | @keyframes slideInRight { |
| | | from { |
| | | transform: translate3d(100%, 0, 0); |
| | | opacity: 0; |
| | | } |
| | | |
| | | to { |
| | | transform: translate3d(0, 0, 0); |
| | | opacity: 1; |
| | | } |
| | | } |
| | | |
| | | @keyframes slideOutRight { |
| | | from { |
| | | transform: translate3d(0, 0, 0); |
| | | opacity: 1; |
| | | } |
| | | |
| | | to { |
| | | transform: translate3d(100%, 0, 0); |
| | | opacity: 0; |
| | | } |
| | | } |
| | | |
| | | @keyframes asideSkeletonShimmer { |
| | | 100% { |
| | | transform: translateX(100%); |
| | | } |
| | | } |
| | | |
| | | .ai-drawer-layer { |
| | | box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15) !important; |
| | | border-radius: 8px 0 0 8px !important; |
| | | overflow: hidden; |
| | | animation: slideInRight 0.5s cubic-bezier(0.16, 1, 0.3, 1); |
| | | } |
| | | |
| | | .ai-drawer-layer-close { |
| | | animation: slideOutRight 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards !important; |
| | | } |
| | | |
| | | .ai-assistant-btn { |
| | |
| | | bottom: 40px; |
| | | z-index: 9999; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .ai-assistant-mask { |
| | | position: fixed; |
| | | inset: 0; |
| | | z-index: 9997; |
| | | background: rgba(15, 23, 42, 0.10); |
| | | backdrop-filter: blur(3px); |
| | | } |
| | | |
| | | .ai-assistant-drawer { |
| | | position: fixed; |
| | | top: 0; |
| | | right: 0; |
| | | z-index: 9998; |
| | | width: min(600px, 100vw); |
| | | height: 100vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | background: #fff; |
| | | border-radius: 12px 0 0 12px; |
| | | box-shadow: -10px 0 28px rgba(15, 23, 42, 0.18); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .ai-drawer-header { |
| | | height: 54px; |
| | | padding: 0 16px 0 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | border-bottom: 1px solid rgba(226, 232, 240, 0.92); |
| | | background: rgba(248, 251, 255, 0.96); |
| | | color: #243447; |
| | | font-size: 15px; |
| | | font-weight: 700; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .ai-drawer-close { |
| | | width: 32px; |
| | | height: 32px; |
| | | border: none; |
| | | border-radius: 10px; |
| | | background: transparent; |
| | | color: #5f7084; |
| | | cursor: pointer; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .ai-drawer-close:hover { |
| | | background: rgba(236, 243, 249, 0.92); |
| | | color: #24405c; |
| | | } |
| | | |
| | | .ai-drawer-frame { |
| | | flex: 1; |
| | | width: 100%; |
| | | border: none; |
| | | background: #fff; |
| | | } |
| | | |
| | | .ai-mask-enter-active, |
| | | .ai-mask-leave-active { |
| | | transition: opacity .22s ease; |
| | | } |
| | | |
| | | .ai-mask-enter, |
| | | .ai-mask-leave-to { |
| | | opacity: 0; |
| | | } |
| | | |
| | | .ai-panel-enter-active, |
| | | .ai-panel-leave-active { |
| | | transition: transform .28s cubic-bezier(0.22, 1, 0.36, 1), opacity .22s ease; |
| | | } |
| | | |
| | | .ai-panel-enter, |
| | | .ai-panel-leave-to { |
| | | transform: translate3d(100%, 0, 0); |
| | | opacity: 0; |
| | | } |
| | | |
| | | @media (max-width: 1440px) { |
| | |
| | | </span> |
| | | </el-dialog> |
| | | |
| | | <div |
| | | id="ai-assistant-btn" |
| | | class="ai-assistant-btn" |
| | | @mouseenter="showAiTip" |
| | | @mouseleave="hideAiTip" |
| | | @click="openAiAssistant"> |
| | | </div> |
| | | <el-tooltip :content="t('common.aiAssistant')" placement="left"> |
| | | <div |
| | | id="ai-assistant-btn" |
| | | class="ai-assistant-btn" |
| | | @click="openAiAssistant"> |
| | | </div> |
| | | </el-tooltip> |
| | | |
| | | <transition name="ai-mask"> |
| | | <div |
| | | v-if="aiAssistantVisible" |
| | | class="ai-assistant-mask" |
| | | @click="closeAiAssistant"> |
| | | </div> |
| | | </transition> |
| | | |
| | | <transition name="ai-panel"> |
| | | <div |
| | | v-if="aiAssistantVisible" |
| | | class="ai-assistant-drawer"> |
| | | <div class="ai-drawer-header"> |
| | | <span>{{ t('common.aiAssistant') }}</span> |
| | | <button type="button" class="ai-drawer-close" @click="closeAiAssistant"> |
| | | <i class="el-icon-close"></i> |
| | | </button> |
| | | </div> |
| | | <iframe |
| | | v-if="aiAssistantMounted" |
| | | class="ai-drawer-frame" |
| | | :src="aiAssistantSrc"> |
| | | </iframe> |
| | | </div> |
| | | </transition> |
| | | </div> |
| | | |
| | | <script type="text/javascript" src="../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../static/js/layer/layer.js"></script> |
| | | <script type="text/javascript" src="../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../static/vue/element/element.js"></script> |
| | | <script> |
| | |
| | | menuSyncVersion: 0, |
| | | menuSyncTimer: null, |
| | | userName: localStorage.getItem(USER_STORAGE_KEY) || (window.WCS_I18N ? window.WCS_I18N.tl("管理员") : "管理员"), |
| | | aiLayerIndex: null, |
| | | aiTipIndex: null |
| | | aiAssistantVisible: false, |
| | | aiAssistantMounted: false, |
| | | aiAssistantSrc: baseUrl + "/views/ai/diagnosis.html" |
| | | }; |
| | | }, |
| | | computed: { |
| | |
| | | clearTimeout(this.collapseTimer); |
| | | this.collapseTimer = null; |
| | | } |
| | | if (this.aiTipIndex) { |
| | | layer.close(this.aiTipIndex); |
| | | this.aiTipIndex = null; |
| | | } |
| | | }, |
| | | methods: { |
| | | t: function (key, params) { |
| | |
| | | PROFILE_TAB_CONFIG.title = profileConfig.title; |
| | | PROFILE_TAB_CONFIG.group = profileConfig.group; |
| | | for (i = 0; i < this.tabs.length; i++) { |
| | | if (this.isHomeTabUrl(this.tabs[i].url)) { |
| | | this.tabs[i].title = homeConfig.title; |
| | | this.tabs[i].group = homeConfig.group; |
| | | this.tabs[i].home = true; |
| | | } else if (this.resolveViewSrc(this.tabs[i].url) === this.resolveViewSrc(profileConfig.url)) { |
| | | this.tabs[i].title = profileConfig.title; |
| | | this.tabs[i].group = profileConfig.group; |
| | | } else { |
| | | this.tabs[i].title = this.translateTabTitle(this.tabs[i].title); |
| | | this.tabs[i].group = this.tl(this.tabs[i].group); |
| | | } |
| | | this.syncTabMeta(this.tabs[i], homeConfig, profileConfig); |
| | | } |
| | | this.updateDocumentTitle(this.activeTabTitle); |
| | | this.persistTabs(); |
| | |
| | | }, |
| | | translateTabTitle: function (title) { |
| | | return this.tl(title); |
| | | }, |
| | | findMenuMeta: function (tab) { |
| | | var normalizedUrl; |
| | | var i; |
| | | var j; |
| | | var group; |
| | | var item; |
| | | if (!tab) { |
| | | return null; |
| | | } |
| | | normalizedUrl = this.resolveViewSrc(tab.url); |
| | | for (i = 0; i < this.menus.length; i++) { |
| | | group = this.menus[i]; |
| | | for (j = 0; j < group.subMenu.length; j++) { |
| | | item = group.subMenu[j]; |
| | | if ((tab.menuKey && item.tabKey === tab.menuKey) || item.url === normalizedUrl) { |
| | | return { |
| | | group: group, |
| | | item: item |
| | | }; |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | }, |
| | | syncTabMeta: function (tab, homeConfig, profileConfig) { |
| | | var menuMeta; |
| | | if (!tab) { |
| | | return; |
| | | } |
| | | if (this.isHomeTabUrl(tab.url)) { |
| | | tab.title = homeConfig.title; |
| | | tab.group = homeConfig.group; |
| | | tab.home = true; |
| | | return; |
| | | } |
| | | if (this.resolveViewSrc(tab.url) === this.resolveViewSrc(profileConfig.url)) { |
| | | tab.title = profileConfig.title; |
| | | tab.group = profileConfig.group; |
| | | return; |
| | | } |
| | | menuMeta = this.findMenuMeta(tab); |
| | | if (menuMeta) { |
| | | tab.title = menuMeta.item.name; |
| | | tab.group = menuMeta.group.menu; |
| | | tab.menuKey = menuMeta.item.tabKey || tab.menuKey; |
| | | return; |
| | | } |
| | | tab.title = this.translateTabTitle(tab.title); |
| | | tab.group = this.tl(tab.group); |
| | | }, |
| | | updateDocumentTitle: function (title) { |
| | | document.title = title + " - " + this.t("app.title"); |
| | |
| | | script = frameDocument.createElement("script"); |
| | | script.id = "wcs-i18n-bridge-script"; |
| | | script.type = "text/javascript"; |
| | | script.src = baseUrl + "/static/js/common.js?v=20260309_i18n_fix1"; |
| | | script.src = baseUrl + "/static/js/common.js"; |
| | | script.onload = applyFrameI18n; |
| | | frameDocument.head.appendChild(script); |
| | | }, |
| | |
| | | that.menuLoading = false; |
| | | if (res.code === 200) { |
| | | that.menus = that.normalizeMenuData(res.data || []); |
| | | that.refreshI18nState(); |
| | | that.syncMenuStateByUrl(that.activeTabUrl); |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + "/login"; |
| | |
| | | document.exitFullscreen(); |
| | | } |
| | | }, |
| | | showAiTip: function () { |
| | | this.hideAiTip(); |
| | | this.aiTipIndex = layer.tips(this.t("common.aiAssistant"), "#ai-assistant-btn", { |
| | | tips: [1, "#333"], |
| | | time: -1 |
| | | }); |
| | | }, |
| | | hideAiTip: function () { |
| | | if (this.aiTipIndex) { |
| | | layer.close(this.aiTipIndex); |
| | | this.aiTipIndex = null; |
| | | } |
| | | }, |
| | | openAiAssistant: function () { |
| | | var that = this; |
| | | var $layero; |
| | | var $shade; |
| | | |
| | | this.hideAiTip(); |
| | | |
| | | if (this.aiLayerIndex !== null && $("#layui-layer" + this.aiLayerIndex).length > 0) { |
| | | $layero = $("#layui-layer" + this.aiLayerIndex); |
| | | $shade = $("#layui-layer-shade" + this.aiLayerIndex); |
| | | |
| | | $shade.show().css("opacity", 0.1); |
| | | $layero.show(); |
| | | $layero.removeClass("ai-drawer-layer-close"); |
| | | $layero.removeClass("ai-drawer-layer"); |
| | | void $layero.get(0).offsetWidth; |
| | | $layero.addClass("ai-drawer-layer"); |
| | | return; |
| | | if (!this.aiAssistantMounted) { |
| | | this.aiAssistantMounted = true; |
| | | } |
| | | |
| | | layer.open({ |
| | | type: 2, |
| | | title: false, |
| | | closeBtn: 0, |
| | | shadeClose: false, |
| | | shade: 0.1, |
| | | area: ["600px", "100%"], |
| | | offset: "r", |
| | | anim: -1, |
| | | isOutAnim: false, |
| | | skin: "ai-drawer-layer", |
| | | content: "ai/diagnosis.html", |
| | | success: function (layero, index) { |
| | | var shadeId; |
| | | var $shadeEl; |
| | | |
| | | that.aiLayerIndex = index; |
| | | shadeId = layero.attr("id").replace("layui-layer", "layui-layer-shade"); |
| | | $shadeEl = $("#" + shadeId); |
| | | $shadeEl.css({ |
| | | "backdrop-filter": "blur(3px)", |
| | | transition: "opacity 0.8s" |
| | | }); |
| | | $shadeEl.off("click.aiAssistant").on("click.aiAssistant", function () { |
| | | layero.addClass("ai-drawer-layer-close"); |
| | | $shadeEl.css("opacity", 0); |
| | | setTimeout(function () { |
| | | layero.hide(); |
| | | $shadeEl.hide(); |
| | | }, 400); |
| | | }); |
| | | } |
| | | }); |
| | | this.aiAssistantVisible = true; |
| | | }, |
| | | closeAiAssistant: function () { |
| | | this.aiAssistantVisible = false; |
| | | }, |
| | | handleUserCommand: function (command) { |
| | | if (command === "profile") { |
| | |
| | | <title>库位地图</title> |
| | | <link rel="stylesheet" href="../../static/vue/element/element.css"> |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script src="../../static/js/gsap.min.js"></script> |
| | |
| | | } |
| | | |
| | | .login-submit { |
| | | width: 100%; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | border-radius: 16px; |
| | | margin-top: 6px; |
| | | box-shadow: 0 14px 24px rgba(46, 115, 223, 0.28); |
| | | } |
| | | |
| | | .login-actions { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | margin-top: 6px; |
| | | } |
| | | |
| | | .login-actions .el-button { |
| | | width: 100%; |
| | | margin-left: 0; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .login-passkey { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 16px; |
| | | border-color: rgba(71, 110, 162, 0.24); |
| | | color: #26496a; |
| | | background: rgba(245, 249, 255, 0.96); |
| | | } |
| | | |
| | | .login-passkey-tip { |
| | | margin-top: 12px; |
| | | color: #7b8c9d; |
| | | font-size: 12px; |
| | | line-height: 1.7; |
| | | } |
| | | |
| | | .tools-dialog .el-dialog, |
| | | .text-dialog .el-dialog, |
| | | .upload-dialog .el-dialog { |
| | | .upload-dialog .el-dialog, |
| | | .mfa-dialog .el-dialog { |
| | | border-radius: 20px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .tools-dialog .el-dialog__header, |
| | | .text-dialog .el-dialog__header, |
| | | .upload-dialog .el-dialog__header { |
| | | .upload-dialog .el-dialog__header, |
| | | .mfa-dialog .el-dialog__header { |
| | | padding: 18px 20px 12px; |
| | | border-bottom: 1px solid rgba(222, 230, 239, 0.92); |
| | | background: #f8fbff; |
| | |
| | | |
| | | .tools-dialog .el-dialog__title, |
| | | .text-dialog .el-dialog__title, |
| | | .upload-dialog .el-dialog__title { |
| | | .upload-dialog .el-dialog__title, |
| | | .mfa-dialog .el-dialog__title { |
| | | font-weight: 700; |
| | | color: #243447; |
| | | } |
| | | |
| | | .tools-dialog .el-dialog__body, |
| | | .text-dialog .el-dialog__body, |
| | | .upload-dialog .el-dialog__body { |
| | | .upload-dialog .el-dialog__body, |
| | | .mfa-dialog .el-dialog__body { |
| | | padding: 18px 20px 20px; |
| | | } |
| | | |
| | |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .mfa-tip { |
| | | color: #6f7f92; |
| | | font-size: 13px; |
| | | line-height: 1.7; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .mfa-account { |
| | | margin-bottom: 14px; |
| | | padding: 12px 14px; |
| | | border-radius: 14px; |
| | | background: rgba(244, 248, 253, 0.92); |
| | | color: #42576d; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .mfa-account strong { |
| | | color: #1e3956; |
| | | } |
| | | |
| | | .mfa-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | margin-top: 14px; |
| | | } |
| | | |
| | | @media (max-width: 640px) { |
| | |
| | | <div class="login-layout"> |
| | | <section class="hero-panel animate__animated animate__fadeInLeft"> |
| | | <div class="brand-chip">Zoneyung WCS</div> |
| | | <div class="hero-title">WCS系统让设备调度、任务执行与现场监控保持在同一套业务链路中。</div> |
| | | <div class="hero-title">{{ text('login.hero.title', 'WCS系统让设备调度、任务执行与现场监控保持在同一套业务链路中。') }}</div> |
| | | <div class="hero-subtitle"> |
| | | Warehouse Control System 面向自动化立体仓库现场执行层,围绕堆垛机、RGV、输送站台、任务指令、 |
| | | 库位状态和日志追踪进行统一调度与可视化管理,帮助仓储系统实现稳定、可追溯、可联动的作业控制。 |
| | | 浙江中扬立库技术有限公司长期专注于自动化立体仓库与智能物流系统建设,覆盖方案设计、软件控制、 |
| | | 设备集成与项目实施交付。 |
| | | {{ text('login.hero.subtitle', 'Warehouse Control System 面向自动化立体仓库现场执行层,围绕堆垛机、RGV、输送站台、任务指令、库位状态和日志追踪进行统一调度与可视化管理,帮助仓储系统实现稳定、可追溯、可联动的作业控制。浙江中扬立库技术有限公司长期专注于自动化立体仓库与智能物流系统建设,覆盖方案设计、软件控制、设备集成与项目实施交付。') }} |
| | | </div> |
| | | <div class="hero-metrics"> |
| | | <div class="metric-card"> |
| | | <div class="metric-value">调度</div> |
| | | <div class="metric-label">统一编排现场执行任务</div> |
| | | <div class="metric-value">{{ text('login.hero.metric.dispatch.title', '调度') }}</div> |
| | | <div class="metric-label">{{ text('login.hero.metric.dispatch.desc', '统一编排现场执行任务') }}</div> |
| | | </div> |
| | | <div class="metric-card"> |
| | | <div class="metric-value">追溯</div> |
| | | <div class="metric-label">作业、设备、日志全链路留痕</div> |
| | | <div class="metric-value">{{ text('login.hero.metric.trace.title', '追溯') }}</div> |
| | | <div class="metric-label">{{ text('login.hero.metric.trace.desc', '作业、设备、日志全链路留痕') }}</div> |
| | | </div> |
| | | <div class="metric-card"> |
| | | <div class="metric-value">集成</div> |
| | | <div class="metric-label">对接WMS、设备与业务规则</div> |
| | | <div class="metric-value">{{ text('login.hero.metric.integration.title', '集成') }}</div> |
| | | <div class="metric-label">{{ text('login.hero.metric.integration.desc', '对接WMS、设备与业务规则') }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="hero-footer"> |
| | | <span>浙江中扬立库技术有限公司</span> |
| | | <span>自动化立体仓库与智能物流系统解决方案</span> |
| | | <span>{{ text('login.hero.company.name', '浙江中扬立库技术有限公司') }}</span> |
| | | <span>{{ text('login.hero.company.solution', '自动化立体仓库与智能物流系统解决方案') }}</span> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="login-card animate__animated animate__fadeInUp"> |
| | | <div class="login-head"> |
| | | <h1 class="login-title" @click="handleTitleClick">{{ text('login.title', 'WCS系统V3.0') }}</h1> |
| | | <div class="login-subtitle">请输入账号和密码进入系统。</div> |
| | | <div class="login-subtitle">{{ text('login.subtitle', '请输入账号和密码进入系统。') }}</div> |
| | | </div> |
| | | <div class="login-body"> |
| | | <el-form ref="loginForm" class="login-form" :model="loginForm" :rules="loginRules" @submit.native.prevent> |
| | |
| | | <i slot="prefix" class="el-input__icon el-icon-lock"></i> |
| | | </el-input> |
| | | </el-form-item> |
| | | <el-button class="login-submit" type="primary" :loading="loginLoading" @click="handleLogin"> |
| | | {{ text('login.submit', '登录') }} |
| | | </el-button> |
| | | <div class="login-actions"> |
| | | <el-button class="login-submit" type="primary" :loading="loginLoading" @click="handleLogin"> |
| | | {{ text('login.submit', '登录') }} |
| | | </el-button> |
| | | <el-button class="login-passkey" plain :loading="passkeyLoading" @click="handlePasskeyLogin"> |
| | | {{ text('login.passkey.submit', '通行密钥登录') }} |
| | | </el-button> |
| | | </div> |
| | | <div class="login-passkey-tip"> |
| | | {{ text('login.passkey.tip', '支持使用设备生物识别或安全密钥登录。可先输入账号缩小凭证范围,留空则尝试发现式登录。') }} |
| | | </div> |
| | | </el-form> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | |
| | | <el-dialog class="tools-dialog" title="系统工具" :visible.sync="toolsDialogVisible" width="560px" :close-on-click-modal="true" append-to-body> |
| | | <el-dialog class="tools-dialog" :title="text('login.tools.title', '系统工具')" :visible.sync="toolsDialogVisible" width="560px" :close-on-click-modal="true" append-to-body> |
| | | <div class="tool-group"> |
| | | <div class="tool-title">{{ text('login.tools.recommended', '推荐操作') }}</div> |
| | | <div class="tool-actions"> |
| | |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <el-dialog |
| | | class="mfa-dialog" |
| | | :title="text('login.mfa.title', 'MFA二次验证')" |
| | | :visible.sync="mfaDialogVisible" |
| | | width="420px" |
| | | :close-on-click-modal="false" |
| | | @close="closeMfaDialog" |
| | | append-to-body> |
| | | <div class="mfa-tip">{{ text('login.mfa.tip', '账号密码已通过,请输入身份验证器中的 6 位动态验证码后继续登录。') }}</div> |
| | | <div class="mfa-account">{{ text('login.mfa.currentAccount', '当前账号:') }}<strong>{{ mfaPending.username || loginForm.mobile || '--' }}</strong></div> |
| | | <el-form ref="mfaForm" :model="mfaForm" :rules="mfaRules" label-width="82px" size="small" @submit.native.prevent> |
| | | <el-form-item :label="text('login.mfa.codeLabel', '验证码')" prop="code"> |
| | | <el-input v-model.trim="mfaForm.code" maxlength="6" :placeholder="text('login.mfa.codePlaceholder', '请输入6位动态码')" @keyup.enter.native="handleMfaLogin"></el-input> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="mfa-footer"> |
| | | <el-button @click="closeMfaDialog">{{ text('login.mfa.cancel', '取消') }}</el-button> |
| | | <el-button type="primary" :loading="mfaLoading" @click="handleMfaLogin">{{ text('login.mfa.submit', '验证并登录') }}</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <el-dialog class="text-dialog" :title="textDialog.title" :visible.sync="textDialogVisible" width="720px" append-to-body> |
| | | <div class="dialog-text-label">{{ textDialog.label }}</div> |
| | | <div v-if="textDialog.tip" class="dialog-text-tip">{{ textDialog.tip }}</div> |
| | | <el-input v-model="textDialog.text" type="textarea" :rows="10" readonly></el-input> |
| | | <div class="text-footer"> |
| | | <el-button @click="textDialogVisible = false">关闭</el-button> |
| | | <el-button @click="textDialogVisible = false">{{ text('login.dialog.close', '关闭') }}</el-button> |
| | | <el-button type="primary" @click="copyText">{{ text('copy', '复制') }}</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <el-dialog class="upload-dialog" title="录入许可证" :visible.sync="uploadDialogVisible" width="760px" append-to-body> |
| | | <div class="dialog-text-label">许可证 Base64</div> |
| | | <div class="dialog-text-tip">将许可证服务端返回的 license 字段完整粘贴到这里。</div> |
| | | <el-dialog class="upload-dialog" :title="text('login.tools.uploadLicense', '录入许可证')" :visible.sync="uploadDialogVisible" width="760px" append-to-body> |
| | | <div class="dialog-text-label">{{ text('login.license.label', '许可证 Base64') }}</div> |
| | | <div class="dialog-text-tip">{{ text('login.license.tip', '将许可证服务端返回的 license 字段完整粘贴到这里。') }}</div> |
| | | <el-input v-model.trim="licenseBase64" type="textarea" :rows="10"></el-input> |
| | | <div class="upload-footer"> |
| | | <el-button @click="uploadDialogVisible = false">取消</el-button> |
| | | <el-button type="primary" @click="submitLicense">提交</el-button> |
| | | <el-button @click="uploadDialogVisible = false">{{ text('login.mfa.cancel', '取消') }}</el-button> |
| | | <el-button type="primary" @click="submitLicense">{{ text('login.license.submit', '提交') }}</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | </div> |
| | | </body> |
| | | <script type="text/javascript" src="../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../static/js/tools/md5.js"></script> |
| | | <script type="text/javascript" src="../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../static/js/webauthn-utils.js"></script> |
| | | <script type="text/javascript" src="../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../static/js/login/login.js?v=20260310_login_vue"></script> |
| | | <script type="text/javascript" src="../static/js/login/login.js?v=20260311_login_passkey"></script> |
| | | </html> |
| | |
| | | |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script> |
| | | new Vue({ |
| | | el: '#app', |
| | |
| | | </body> |
| | | <script type="text/javascript" src="../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../static/js/tools/md5.js"></script> |
| | | <script type="text/javascript" src="../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../static/js/password/password.js?v=20260310_password_vue"></script> |
| | |
| | | </body> |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../../static/js/role/rolePower.js" charset="utf-8"></script> |
| | | </html> |
| | |
| | | <!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="utf-8"> |
| | | <title>User 管理</title> |
| | | <meta charset="UTF-8"> |
| | | <title>系统用户</title> |
| | | <meta name="renderer" content="webkit"> |
| | | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> |
| | |
| | | <link rel="stylesheet" href="../../static/css/cool.css"> |
| | | <style> |
| | | :root { |
| | | --card-bg: rgba(255, 255, 255, 0.94); |
| | | --card-border: rgba(216, 226, 238, 0.95); |
| | | --card-bg: rgba(255, 255, 255, 0.96); |
| | | --card-border: rgba(217, 227, 238, 0.96); |
| | | --text-main: #243447; |
| | | --text-sub: #6d7f90; |
| | | } |
| | | |
| | | [v-cloak] { |
| | |
| | | color: var(--text-main); |
| | | font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; |
| | | background: |
| | | radial-gradient(1000px 420px at 0% -10%, rgba(44, 107, 193, 0.12), transparent 56%), |
| | | radial-gradient(900px 400px at 100% 0%, rgba(28, 150, 126, 0.10), transparent 58%), |
| | | linear-gradient(180deg, #f2f6fb 0%, #f8fafc 100%); |
| | | radial-gradient(920px 360px at 0% 0%, rgba(48, 112, 196, 0.10), transparent 58%), |
| | | radial-gradient(760px 280px at 100% 10%, rgba(25, 160, 138, 0.08), transparent 60%), |
| | | linear-gradient(180deg, #f3f7fb 0%, #f8fafc 100%); |
| | | } |
| | | |
| | | .page-shell { |
| | | max-width: 1700px; |
| | | margin: 0 auto; |
| | | padding: 14px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .card-shell { |
| | | position: relative; |
| | | border-radius: 24px; |
| | | max-width: 1600px; |
| | | margin: 0 auto; |
| | | border-radius: 22px; |
| | | border: 1px solid var(--card-border); |
| | | background: |
| | | radial-gradient(760px 220px at -8% 0%, rgba(43, 117, 196, 0.05), transparent 55%), |
| | | radial-gradient(680px 200px at 108% 10%, rgba(24, 150, 129, 0.05), transparent 58%), |
| | | radial-gradient(780px 220px at -8% 0%, rgba(45, 118, 196, 0.05), transparent 58%), |
| | | radial-gradient(640px 180px at 106% 16%, rgba(24, 150, 129, 0.05), transparent 56%), |
| | | var(--card-bg); |
| | | box-shadow: 0 16px 32px rgba(44, 67, 96, 0.08); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .card-body { |
| | | position: relative; |
| | | z-index: 1; |
| | | .card-head { |
| | | padding: 16px 18px 14px; |
| | | border-bottom: 1px solid rgba(223, 231, 240, 0.92); |
| | | } |
| | | |
| | | .list-toolbar { |
| | | padding: 12px 16px 10px; |
| | | border-bottom: 1px solid rgba(222, 230, 239, 0.92); |
| | | } |
| | | |
| | | .toolbar-main { |
| | | .head-top { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | gap: 16px; |
| | | flex-wrap: wrap; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .head-title { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .head-title h1 { |
| | | margin: 0; |
| | | font-size: 20px; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .head-title p { |
| | | margin: 0; |
| | | font-size: 13px; |
| | | line-height: 1.6; |
| | | color: var(--text-sub); |
| | | } |
| | | |
| | | .head-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .toolbar-left { |
| | | flex: 1 1 960px; |
| | | .head-actions .el-button { |
| | | min-width: 96px; |
| | | } |
| | | |
| | | .search-form { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | gap: 8px 12px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .toolbar-search { |
| | | flex: 1 1 auto; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | .search-form .el-form-item { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .toolbar-search-item { |
| | | flex: 0 0 152px; |
| | | min-width: 152px; |
| | | .search-form .el-form-item__label { |
| | | font-weight: 600; |
| | | color: #55697d; |
| | | } |
| | | |
| | | .toolbar-search-item.keyword { |
| | | flex: 0 0 220px; |
| | | min-width: 220px; |
| | | } |
| | | |
| | | .toolbar-query-actions, |
| | | .toolbar-ops { |
| | | display: flex; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .toolbar-ops { |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .list-toolbar .el-input__inner, |
| | | .list-toolbar .el-range-editor.el-input__inner, |
| | | .advanced-panel .el-input__inner, |
| | | .advanced-panel .el-range-editor.el-input__inner { |
| | | height: 32px; |
| | | line-height: 32px; |
| | | } |
| | | |
| | | .list-toolbar .el-range-editor.el-input__inner, |
| | | .advanced-panel .el-range-editor.el-input__inner { |
| | | align-items: center; |
| | | } |
| | | |
| | | .list-toolbar .el-input__icon, |
| | | .advanced-panel .el-input__icon { |
| | | line-height: 32px; |
| | | } |
| | | |
| | | .list-toolbar .el-range-editor .el-range__icon, |
| | | .list-toolbar .el-range-editor .el-range-separator, |
| | | .list-toolbar .el-range-editor .el-range__close-icon, |
| | | .advanced-panel .el-range-editor .el-range__icon, |
| | | .advanced-panel .el-range-editor .el-range-separator, |
| | | .advanced-panel .el-range-editor .el-range__close-icon { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | height: 100%; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .list-toolbar .el-button, |
| | | .advanced-panel .el-button { |
| | | padding: 8px 12px; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .advanced-panel { |
| | | padding: 10px 16px 12px; |
| | | border-bottom: 1px solid rgba(222, 230, 239, 0.92); |
| | | background: rgba(248, 251, 254, 0.78); |
| | | } |
| | | |
| | | .advanced-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(6, minmax(0, 1fr)); |
| | | gap: 8px; |
| | | } |
| | | |
| | | .advanced-item { |
| | | min-width: 0; |
| | | } |
| | | |
| | | .advanced-item.span-2 { |
| | | grid-column: span 2; |
| | | .search-form .el-input__inner, |
| | | .search-form .el-button { |
| | | height: 34px; |
| | | line-height: 34px; |
| | | } |
| | | |
| | | .table-wrap { |
| | | padding: 10px 16px; |
| | | padding: 12px 18px 16px; |
| | | } |
| | | |
| | | .table-shell { |
| | | border-radius: 20px; |
| | | border-radius: 18px; |
| | | overflow: hidden; |
| | | border: 1px solid rgba(217, 227, 238, 0.98); |
| | | background: rgba(255, 255, 255, 0.95); |
| | | } |
| | | |
| | | .table-shell .el-table { |
| | | border-radius: 20px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .table-shell .el-table th { |
| | |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .payload-cell { |
| | | display: inline-block; |
| | | max-width: 280px; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | .table-shell .el-table td, |
| | | .table-shell .el-table th { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .mono { |
| | | font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace; |
| | | .status-switch { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .pager-bar { |
| | | padding: 0 16px 16px; |
| | | padding: 16px 0 0; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .column-popover { |
| | | max-width: 320px; |
| | | } |
| | | |
| | | .column-popover-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | margin-bottom: 10px; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: var(--text-main); |
| | | } |
| | | |
| | | .column-list { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 8px 10px; |
| | | max-height: 280px; |
| | | overflow: auto; |
| | | padding-right: 4px; |
| | | } |
| | | |
| | | .dialog-panel .el-dialog { |
| | | border-radius: 24px; |
| | | border-radius: 22px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .dialog-panel .el-dialog__header { |
| | | padding: 22px 24px 12px; |
| | | padding: 20px 22px 12px; |
| | | background: linear-gradient(180deg, #f8fbff 0%, #f3f7fb 100%); |
| | | border-bottom: 1px solid rgba(224, 232, 241, 0.92); |
| | | } |
| | |
| | | } |
| | | |
| | | .dialog-panel .el-dialog__body { |
| | | padding: 18px 24px 8px; |
| | | padding: 20px 22px 8px; |
| | | } |
| | | |
| | | .dialog-form .el-form-item { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .dialog-form .el-form-item__label { |
| | | font-weight: 600; |
| | | color: #5b6f83; |
| | | } |
| | | |
| | | .dialog-form .el-input__inner, |
| | | .dialog-form .el-select .el-input__inner { |
| | | height: 36px; |
| | | line-height: 36px; |
| | | } |
| | | |
| | | .dialog-tip { |
| | | margin: 4px 0 2px; |
| | | padding: 10px 12px; |
| | | border-radius: 12px; |
| | | font-size: 12px; |
| | | line-height: 1.6; |
| | | color: #748497; |
| | | background: rgba(248, 251, 255, 0.92); |
| | | border: 1px solid rgba(226, 233, 242, 0.96); |
| | | } |
| | | |
| | | .dialog-footer { |
| | |
| | | gap: 10px; |
| | | } |
| | | |
| | | @media (max-width: 1520px) { |
| | | .advanced-grid { |
| | | grid-template-columns: repeat(5, minmax(0, 1fr)); |
| | | } |
| | | .reset-user { |
| | | margin-bottom: 16px; |
| | | padding: 12px 14px; |
| | | border-radius: 12px; |
| | | background: rgba(248, 251, 255, 0.92); |
| | | border: 1px solid rgba(226, 233, 242, 0.96); |
| | | color: var(--text-sub); |
| | | font-size: 13px; |
| | | } |
| | | |
| | | @media (max-width: 1280px) { |
| | | .advanced-grid { |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | } |
| | | .table-action-danger { |
| | | color: #f56c6c; |
| | | } |
| | | |
| | | @media (max-width: 960px) { |
| | | .toolbar-left { |
| | | flex-basis: 100%; |
| | | } |
| | | |
| | | .advanced-grid { |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | } |
| | | |
| | | .advanced-item.span-2 { |
| | | grid-column: span 2; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 720px) { |
| | | @media (max-width: 768px) { |
| | | .page-shell { |
| | | padding: 12px; |
| | | padding: 10px; |
| | | } |
| | | |
| | | .toolbar-search-item, |
| | | .toolbar-search-item.keyword { |
| | | min-width: 100%; |
| | | flex-basis: 100%; |
| | | .card-head, |
| | | .table-wrap { |
| | | padding-left: 12px; |
| | | padding-right: 12px; |
| | | } |
| | | |
| | | .toolbar-query-actions, |
| | | .toolbar-ops { |
| | | width: 100%; |
| | | } |
| | | |
| | | .advanced-grid { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | |
| | | .advanced-item.span-2 { |
| | | grid-column: span 2; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 560px) { |
| | | .advanced-grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .advanced-item.span-2 { |
| | | grid-column: auto; |
| | | } |
| | | |
| | | .list-toolbar, |
| | | .advanced-panel, |
| | | .table-wrap, |
| | | .pager-bar { |
| | | padding-left: 14px; |
| | | padding-right: 14px; |
| | | } |
| | | |
| | | .column-list { |
| | | grid-template-columns: 1fr; |
| | | .dialog-panel .el-dialog { |
| | | width: calc(100% - 24px) !important; |
| | | } |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div id="app" class="page-shell" v-cloak> |
| | | <section class="card-shell list-card"> |
| | | <div class="card-body"> |
| | | <div class="list-toolbar"> |
| | | <div class="toolbar-main"> |
| | | <div class="toolbar-left"> |
| | | <div class="toolbar-search"> |
| | | <div class="toolbar-search-item keyword"> |
| | | <el-input |
| | | v-model.trim="searchForm.condition" |
| | | size="small" |
| | | clearable |
| | | placeholder="请输入" |
| | | @keyup.enter.native="handleSearch"> |
| | | </el-input> |
| | | </div> |
| | | <div |
| | | v-for="field in quickSearchableFields" |
| | | :key="'quick-' + field.field" |
| | | class="toolbar-search-item"> |
| | | <el-select |
| | | v-if="field.kind === 'enum'" |
| | | v-model="searchForm[field.field]" |
| | | size="small" |
| | | clearable |
| | | :placeholder="field.label" |
| | | style="width: 100%;"> |
| | | <el-option |
| | | v-for="option in field.enumOptions" |
| | | :key="'quick-' + field.field + '-' + option.rawValue" |
| | | :label="option.label" |
| | | :value="normalizeOptionValue(field, option.rawValue)"> |
| | | </el-option> |
| | | </el-select> |
| | | <el-autocomplete |
| | | v-else-if="field.kind === 'foreign'" |
| | | v-model="searchDisplay[field.field]" |
| | | size="small" |
| | | :fetch-suggestions="getSuggestionFetcher(field)" |
| | | :placeholder="field.label" |
| | | style="width: 100%;" |
| | | @select="handleSearchForeignSelect(field, $event)" |
| | | @input="handleSearchForeignInput(field)"> |
| | | <template slot-scope="{ item }"> |
| | | <div class="mono">{{ item.value }}</div> |
| | | <div style="font-size:12px;color:#8a98ac;">ID: {{ item.id }}</div> |
| | | </template> |
| | | </el-autocomplete> |
| | | <el-select |
| | | v-else-if="field.kind === 'checkbox'" |
| | | v-model="searchForm[field.field]" |
| | | size="small" |
| | | clearable |
| | | :placeholder="field.label" |
| | | style="width: 100%;"> |
| | | <el-option label="是" :value="normalizeOptionValue(field, field.checkboxActiveRaw)"></el-option> |
| | | <el-option label="否" :value="normalizeOptionValue(field, field.checkboxInactiveRaw)"></el-option> |
| | | </el-select> |
| | | <el-input |
| | | v-else |
| | | v-model.trim="searchForm[field.field]" |
| | | size="small" |
| | | clearable |
| | | :placeholder="field.label" |
| | | @keyup.enter.native="handleSearch"> |
| | | </el-input> |
| | | </div> |
| | | </div> |
| | | <div class="toolbar-query-actions"> |
| | | <el-button size="small" type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button> |
| | | <el-button size="small" icon="el-icon-refresh-left" @click="handleReset">重置</el-button> |
| | | <el-button |
| | | v-if="hasAdvancedFilters" |
| | | size="small" |
| | | plain |
| | | :icon="advancedFiltersVisible ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" |
| | | @click="toggleAdvancedFilters"> |
| | | {{ advancedFiltersVisible ? '收起' : '筛选' }} |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <div class="toolbar-ops"> |
| | | <el-button size="small" type="primary" plain icon="el-icon-plus" @click="openCreateDialog">新增</el-button> |
| | | <el-button size="small" type="danger" plain icon="el-icon-delete" :disabled="selection.length === 0" @click="removeSelection">删除</el-button> |
| | | <el-popover |
| | | placement="bottom" |
| | | width="320" |
| | | trigger="click" |
| | | popper-class="column-popover"> |
| | | <div class="column-popover-head"> |
| | | <span>列设置</span> |
| | | <div> |
| | | <el-button type="text" @click="selectAllColumns">全选</el-button> |
| | | <el-button type="text" @click="resetColumns">重置</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="column-list"> |
| | | <el-checkbox |
| | | v-for="field in allColumns" |
| | | :key="'column-' + field.field" |
| | | :value="isColumnVisible(field.field)" |
| | | @change="toggleColumn(field.field, $event)"> |
| | | {{ field.label }} |
| | | </el-checkbox> |
| | | </div> |
| | | <el-button slot="reference" size="small" plain icon="el-icon-setting">列设置</el-button> |
| | | </el-popover> |
| | | <el-button size="small" plain icon="el-icon-download" :loading="exporting" @click="exportRows">导出</el-button> |
| | | </div> |
| | | <div id="app" v-cloak class="page-shell"> |
| | | <div class="card-shell"> |
| | | <div class="card-head"> |
| | | <div class="head-top"> |
| | | <div class="head-title"> |
| | | <h1>系统用户</h1> |
| | | <p>修改界面不再显示密码,密码调整统一通过“重置密码”完成;角色、状态等字段恢复为选择控件。</p> |
| | | </div> |
| | | <div class="head-actions"> |
| | | <el-button type="primary" icon="el-icon-plus" @click="openCreateDialog">新增用户</el-button> |
| | | <el-button type="danger" icon="el-icon-delete" :disabled="selection.length === 0" @click="removeSelection">删除所选</el-button> |
| | | </div> |
| | | </div> |
| | | <el-form :inline="true" :model="searchForm" class="search-form" @submit.native.prevent> |
| | | <el-form-item label="登录账户"> |
| | | <el-input v-model.trim="searchForm.username" clearable placeholder="请输入登录账户" @keyup.enter.native="handleSearch"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label="账号"> |
| | | <el-input v-model.trim="searchForm.mobile" clearable placeholder="请输入账号" @keyup.enter.native="handleSearch"></el-input> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button> |
| | | <el-button icon="el-icon-refresh-left" @click="handleResetSearch">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | |
| | | <el-collapse-transition> |
| | | <div v-show="advancedFiltersVisible && hasAdvancedFilters" class="advanced-panel"> |
| | | <div class="advanced-grid"> |
| | | <div |
| | | v-for="field in advancedSearchableFields" |
| | | :key="'advanced-' + field.field" |
| | | :class="['advanced-item', field.kind === 'date' ? 'span-2' : '']"> |
| | | <el-date-picker |
| | | v-if="field.kind === 'date'" |
| | | v-model="searchForm[field.field]" |
| | | size="small" |
| | | type="datetimerange" |
| | | unlink-panels |
| | | range-separator="至" |
| | | :start-placeholder="field.label + '开始'" |
| | | :end-placeholder="field.label + '结束'" |
| | | value-format="yyyy-MM-dd HH:mm:ss" |
| | | style="width: 100%;"> |
| | | </el-date-picker> |
| | | <el-select |
| | | v-else-if="field.kind === 'enum'" |
| | | v-model="searchForm[field.field]" |
| | | size="small" |
| | | clearable |
| | | :placeholder="field.label" |
| | | style="width: 100%;"> |
| | | <el-option |
| | | v-for="option in field.enumOptions" |
| | | :key="'advanced-' + field.field + '-' + option.rawValue" |
| | | :label="option.label" |
| | | :value="normalizeOptionValue(field, option.rawValue)"> |
| | | </el-option> |
| | | </el-select> |
| | | <el-autocomplete |
| | | v-else-if="field.kind === 'foreign'" |
| | | v-model="searchDisplay[field.field]" |
| | | size="small" |
| | | :fetch-suggestions="getSuggestionFetcher(field)" |
| | | :placeholder="field.label" |
| | | style="width: 100%;" |
| | | @select="handleSearchForeignSelect(field, $event)" |
| | | @input="handleSearchForeignInput(field)"> |
| | | <template slot-scope="{ item }"> |
| | | <div class="mono">{{ item.value }}</div> |
| | | <div style="font-size:12px;color:#8a98ac;">ID: {{ item.id }}</div> |
| | | </template> |
| | | </el-autocomplete> |
| | | <el-select |
| | | v-else-if="field.kind === 'checkbox'" |
| | | v-model="searchForm[field.field]" |
| | | size="small" |
| | | clearable |
| | | :placeholder="field.label" |
| | | style="width: 100%;"> |
| | | <el-option label="是" :value="normalizeOptionValue(field, field.checkboxActiveRaw)"></el-option> |
| | | <el-option label="否" :value="normalizeOptionValue(field, field.checkboxInactiveRaw)"></el-option> |
| | | </el-select> |
| | | <el-input |
| | | v-else |
| | | v-model.trim="searchForm[field.field]" |
| | | size="small" |
| | | clearable |
| | | :placeholder="field.label" |
| | | @keyup.enter.native="handleSearch"> |
| | | </el-input> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-collapse-transition> |
| | | |
| | | <div class="table-wrap"> |
| | | <div class="table-shell"> |
| | | <el-table |
| | | ref="dataTable" |
| | | :key="'table-' + visibleColumnKeys.join('|')" |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | border |
| | | stripe |
| | | :height="tableHeight" |
| | | @selection-change="handleSelectionChange" |
| | | @sort-change="handleSortChange"> |
| | | <el-table-column type="selection" width="52" align="center"></el-table-column> |
| | | <el-table-column |
| | | v-for="field in visibleColumns" |
| | | :key="field.field" |
| | | :prop="field.field" |
| | | :label="field.label" |
| | | :width="field.primaryKey ? 90 : null" |
| | | :min-width="field.primaryKey ? null : field.minWidth" |
| | | :sortable="isSortableField(field) ? 'custom' : false" |
| | | :show-overflow-tooltip="field.kind !== 'image'" |
| | | align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-image |
| | | v-if="field.kind === 'image' && getTableValue(scope.row, field)" |
| | | :src="getTableValue(scope.row, field)" |
| | | fit="cover" |
| | | style="width: 48px; height: 48px; border-radius: 10px;"> |
| | | </el-image> |
| | | <el-tag v-else-if="field.kind === 'enum'" size="mini" type="success"> |
| | | {{ valueOrDash(getTableValue(scope.row, field)) }} |
| | | </el-tag> |
| | | <el-tag v-else-if="field.kind === 'checkbox'" size="mini" :type="isCheckboxChecked(scope.row, field) ? 'success' : 'info'"> |
| | | {{ isCheckboxChecked(scope.row, field) ? '是' : '否' }} |
| | | </el-tag> |
| | | <span v-else-if="field.textarea" class="payload-cell mono" :title="stringValue(getTableValue(scope.row, field))"> |
| | | {{ valueOrDash(getTableValue(scope.row, field)) }} |
| | | </span> |
| | | <span v-else>{{ valueOrDash(getTableValue(scope.row, field)) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="160" fixed="right" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-button type="text" @click="openEditDialog(scope.row)">修改</el-button> |
| | | <el-button type="text" style="color:#f56c6c;" @click="removeRows([scope.row[primaryKeyField]])">删除</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <div class="table-wrap"> |
| | | <div class="table-shell" v-loading="loading"> |
| | | <el-table |
| | | :data="tableData" |
| | | border |
| | | stripe |
| | | height="calc(100vh - 245px)" |
| | | @selection-change="handleSelectionChange"> |
| | | <el-table-column type="selection" width="48" align="center"></el-table-column> |
| | | <el-table-column prop="username" label="登录账户" min-width="150" show-overflow-tooltip></el-table-column> |
| | | <el-table-column prop="mobile" label="账号" min-width="150" show-overflow-tooltip></el-table-column> |
| | | <el-table-column prop="roleName" label="角色" min-width="150" show-overflow-tooltip></el-table-column> |
| | | <el-table-column label="MFA授权" width="110" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-tag size="mini" :type="Number(scope.row.mfaAllow) === 1 ? 'success' : 'info'"> |
| | | {{ Number(scope.row.mfaAllow) === 1 ? '已授权' : '未授权' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="MFA状态" width="110" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-tag size="mini" :type="Number(scope.row.mfaEnabled) === 1 ? 'success' : 'warning'"> |
| | | {{ Number(scope.row.mfaEnabled) === 1 ? '已启用' : '未启用' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" width="130" align="center"> |
| | | <template slot-scope="scope"> |
| | | <div class="status-switch"> |
| | | <el-switch |
| | | v-model="scope.row.status" |
| | | :active-value="1" |
| | | :inactive-value="0" |
| | | active-text="正常" |
| | | inactive-text="禁用" |
| | | @change="toggleStatus(scope.row)"> |
| | | </el-switch> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="220" fixed="right" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-button type="text" @click="openEditDialog(scope.row)">编辑</el-button> |
| | | <el-button type="text" @click="openResetPasswordDialog(scope.row)">重置密码</el-button> |
| | | <el-button type="text" class="table-action-danger" @click="removeRows([scope.row.id])">删除</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div class="pager-bar"> |
| | | <el-pagination |
| | | small |
| | | background |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :current-page="page.curr" |
| | | :page-size="page.limit" |
| | | :page-sizes="[15, 30, 50, 100, 200, 500]" |
| | | :total="page.total" |
| | | @current-change="handleCurrentChange" |
| | | @size-change="handleSizeChange"> |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange"> |
| | | </el-pagination> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | |
| | | <el-dialog |
| | | class="dialog-panel" |
| | | :title="dialog.mode === 'create' ? '新增 User' : '修改 User'" |
| | | :title="dialog.mode === 'create' ? '新增用户' : '修改用户'" |
| | | :visible.sync="dialog.visible" |
| | | width="760px" |
| | | :close-on-click-modal="false"> |
| | | width="620px" |
| | | :close-on-click-modal="false" |
| | | append-to-body> |
| | | <el-form |
| | | ref="dialogForm" |
| | | :model="dialogForm" |
| | | :rules="dialogRules" |
| | | label-width="110px" |
| | | size="small"> |
| | | <el-row :gutter="16"> |
| | | <el-col |
| | | v-for="field in editableFields" |
| | | :key="'dialog-' + field.field" |
| | | :span="field.textarea || field.kind === 'image' ? 24 : 12"> |
| | | <el-form-item :label="field.label" :prop="field.field"> |
| | | <el-date-picker |
| | | v-if="field.kind === 'date'" |
| | | v-model="dialogForm[field.field]" |
| | | type="datetime" |
| | | value-format="yyyy-MM-dd HH:mm:ss" |
| | | :placeholder="'请选择' + field.label" |
| | | style="width: 100%;"> |
| | | </el-date-picker> |
| | | <el-select |
| | | v-else-if="field.kind === 'enum'" |
| | | v-model="dialogForm[field.field]" |
| | | clearable |
| | | :placeholder="'请选择' + field.label" |
| | | style="width: 100%;"> |
| | | label-width="88px" |
| | | class="dialog-form" |
| | | @submit.native.prevent> |
| | | <el-row :gutter="18"> |
| | | <el-col :xs="24" :sm="12"> |
| | | <el-form-item label="登录账户" prop="username"> |
| | | <el-input v-model.trim="dialogForm.username" maxlength="32" placeholder="请输入登录账户"></el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12"> |
| | | <el-form-item label="账号" prop="mobile"> |
| | | <el-input v-model.trim="dialogForm.mobile" maxlength="32" placeholder="请输入账号"></el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12"> |
| | | <el-form-item label="角色" prop="roleId"> |
| | | <el-select v-model="dialogForm.roleId" placeholder="请选择角色" style="width: 100%;"> |
| | | <el-option |
| | | v-for="option in field.enumOptions" |
| | | :key="'dialog-' + field.field + '-' + option.rawValue" |
| | | :label="option.label" |
| | | :value="normalizeOptionValue(field, option.rawValue)"> |
| | | v-for="role in roles" |
| | | :key="role.id" |
| | | :label="role.name" |
| | | :value="Number(role.id)"> |
| | | </el-option> |
| | | </el-select> |
| | | <el-autocomplete |
| | | v-else-if="field.kind === 'foreign'" |
| | | v-model="dialogDisplay[field.field]" |
| | | :fetch-suggestions="getSuggestionFetcher(field)" |
| | | :placeholder="'请输入' + field.label" |
| | | style="width: 100%;" |
| | | @select="handleForeignSelect(field, $event)" |
| | | @input="handleForeignInput(field)"> |
| | | <template slot-scope="{ item }"> |
| | | <div class="mono">{{ item.value }}</div> |
| | | <div style="font-size:12px;color:#8a98ac;">ID: {{ item.id }}</div> |
| | | </template> |
| | | </el-autocomplete> |
| | | <el-switch |
| | | v-else-if="field.kind === 'checkbox'" |
| | | v-model="dialogForm[field.field]" |
| | | :active-value="normalizeOptionValue(field, field.checkboxActiveRaw)" |
| | | :inactive-value="normalizeOptionValue(field, field.checkboxInactiveRaw)" |
| | | active-color="#13ce66" |
| | | inactive-color="#c0c4cc"> |
| | | </el-switch> |
| | | <el-input |
| | | v-else-if="field.textarea" |
| | | v-model.trim="dialogForm[field.field]" |
| | | type="textarea" |
| | | :rows="3" |
| | | :placeholder="'请输入' + field.label"> |
| | | </el-input> |
| | | <el-input |
| | | v-else |
| | | v-model.trim="dialogForm[field.field]" |
| | | :placeholder="'请输入' + field.label"> |
| | | </el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12"> |
| | | <el-form-item label="状态"> |
| | | <el-select v-model="dialogForm.status" placeholder="请选择状态" style="width: 100%;"> |
| | | <el-option label="正常" :value="1"></el-option> |
| | | <el-option label="禁用" :value="0"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12"> |
| | | <el-form-item label="MFA授权"> |
| | | <el-select v-model="dialogForm.mfaAllow" placeholder="请选择" style="width: 100%;"> |
| | | <el-option label="不允许" :value="0"></el-option> |
| | | <el-option label="允许" :value="1"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col v-if="dialog.mode === 'create'" :xs="24" :sm="12"> |
| | | <el-form-item label="初始密码" prop="password"> |
| | | <el-input v-model.trim="dialogForm.password" show-password maxlength="32" placeholder="请输入初始密码"></el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <div v-if="dialog.mode === 'edit'" class="dialog-tip"> |
| | | 修改界面不显示密码。如需调整密码,请关闭当前窗口后使用列表里的“重置密码”。 |
| | | </div> |
| | | </el-form> |
| | | <div slot="footer" class="dialog-footer"> |
| | | <el-button @click="dialog.visible = false">取消</el-button> |
| | | <el-button type="primary" :loading="dialog.submitting" @click="submitDialog">保存</el-button> |
| | | </div> |
| | | <span slot="footer" class="dialog-footer"> |
| | | <el-button @click="closeDialog">取消</el-button> |
| | | <el-button type="primary" :loading="dialogSubmitting" @click="submitDialog">保存</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | |
| | | <el-dialog |
| | | class="dialog-panel" |
| | | title="重置密码" |
| | | :visible.sync="resetDialog.visible" |
| | | width="420px" |
| | | :close-on-click-modal="false" |
| | | append-to-body> |
| | | <div class="reset-user">当前用户:{{ resetDialog.username || '--' }}</div> |
| | | <el-form |
| | | ref="resetForm" |
| | | :model="resetDialog" |
| | | :rules="resetRules" |
| | | label-width="72px" |
| | | class="dialog-form" |
| | | @submit.native.prevent> |
| | | <el-form-item label="新密码" prop="password"> |
| | | <el-input v-model.trim="resetDialog.password" show-password maxlength="32" placeholder="请输入新密码"></el-input> |
| | | </el-form-item> |
| | | </el-form> |
| | | <span slot="footer" class="dialog-footer"> |
| | | <el-button @click="closeResetDialog">取消</el-button> |
| | | <el-button type="primary" :loading="resetSubmitting" @click="submitResetPassword">确定</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | </div> |
| | | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/user/user.js?v=20260310" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/tools/md5.js"></script> |
| | | <script type="text/javascript" src="../../static/js/user/user.js?v=20260311_user_restore1" charset="utf-8"></script> |
| | | </body> |
| | | </html> |
| | |
| | | </style> |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/js/handlebars/handlebars-v4.5.3.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script src="../../static/js/gsap.min.js"></script> |
| | |
| | | <script src="../../components/WatchDualCrnCard.js"></script> |
| | | <script src="../../components/DevpCard.js"></script> |
| | | <script src="../../components/WatchRgvCard.js"></script> |
| | | <script src="../../components/MapCanvas.js"></script> |
| | | <script src="../../components/MapCanvas.js?v=20260311_resize_debounce1"></script> |
| | | <script> |
| | | let ws; |
| | | var app = new Vue({ |
| | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/layui/layui.js"></script> |
| | | <script type="text/javascript" src="../../static/js/handlebars/handlebars-v4.5.3.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | </head> |
| | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/layui/layui.js"></script> |
| | | <script type="text/javascript" src="../../static/js/handlebars/handlebars-v4.5.3.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script src="../../static/js/gsap.min.js"></script> |
| | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/watch/stationColorConfig.js" charset="utf-8"></script> |
| | | </body> |
| | | </html> |
| | |
| | | <!DOCTYPE html> |
| | | <html lang="en"> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="UTF-8"> |
| | | <title>任务管理</title> |
| | | <meta name="renderer" content="webkit"> |
| | | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> |
| | | <link rel="stylesheet" href="../../static/vue/element/element.css"> |
| | | <link rel="stylesheet" href="../../static/css/cool.css"> |
| | | <style> |
| | | :root { |
| | | --card-bg: rgba(255, 255, 255, 0.94); |
| | | --card-border: rgba(216, 226, 238, 0.95); |
| | | --text-main: #243447; |
| | | } |
| | | |
| | | <head> |
| | | <meta charset="UTF-8"> |
| | | <title>任务管理</title> |
| | | <link rel="stylesheet" href="../../static/vue/element/element.css"> |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <style> |
| | | [v-cloak] { |
| | | display: none; |
| | | } |
| | | .el-table .success-row { |
| | | background: #b6ff8e; |
| | | } |
| | | </style> |
| | | </head> |
| | | [v-cloak] { |
| | | display: none; |
| | | } |
| | | |
| | | <body> |
| | | <div id="app" v-cloak style="display: flex;justify-content: center;flex-wrap: wrap;"> |
| | | <div style="width: 100%;"> |
| | | <el-card class="box-card"> |
| | | <el-form :inline="true" :model="tableSearchParam" class="demo-form-inline"> |
| | | <el-form-item label=""> |
| | | <el-input v-model="tableSearchParam.wrk_no" placeholder="工作号"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label=""> |
| | | <el-input v-model="tableSearchParam.wms_wrk_no" placeholder="WMS工作号"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label=""> |
| | | <el-input v-model="tableSearchParam.source_loc_no" placeholder="源库位"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label=""> |
| | | <el-input v-model="tableSearchParam.loc_no" placeholder="目标库位"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label=""> |
| | | <el-input v-model="tableSearchParam.crn_no" placeholder="堆垛机"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label=""> |
| | | <el-input v-model="tableSearchParam.dual_crn_no" placeholder="双工位堆垛机"></el-input> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="getTableData">查询</el-button> |
| | | <el-button type="primary" @click="resetParam">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <el-table ref="singleTable" :data="tableData" style="width: 100%;"> |
| | | <el-table-column property="wrkNo" label="工作号"> |
| | | </el-table-column> |
| | | <el-table-column property="wmsWrkNo" label="WMS任务号"> |
| | | </el-table-column> |
| | | <el-table-column property="wrkSts$" label="工作状态"> |
| | | </el-table-column> |
| | | <el-table-column property="ioType$" label="任务类型"> |
| | | </el-table-column> |
| | | <el-table-column property="ioPri" label="优先级"> |
| | | </el-table-column> |
| | | <el-table-column property="sourceStaNo" label="源站"> |
| | | </el-table-column> |
| | | <el-table-column property="staNo" label="目标站"> |
| | | </el-table-column> |
| | | <el-table-column property="sourceLocNo" label="源库位"> |
| | | </el-table-column> |
| | | <el-table-column property="locNo" label="目标库位"> |
| | | </el-table-column> |
| | | <el-table-column property="modiTime$" label="时间"> |
| | | </el-table-column> |
| | | <el-table-column property="barcode" label="托盘码"> |
| | | </el-table-column> |
| | | <el-table-column property="crnNo" label="堆垛机"> |
| | | </el-table-column> |
| | | <el-table-column property="dualCrnNo" label="双工位堆垛机"> |
| | | </el-table-column> |
| | | <el-table-column property="batch" label="批次"> |
| | | </el-table-column> |
| | | <el-table-column property="batchSeq" label="批次序列"> |
| | | </el-table-column> |
| | | <el-table-column property="systemMsg" label="系统消息"> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-dropdown @command="(command)=>{handleCommand(command, scope.row)}"> |
| | | <el-button icon="el-icon-more" size="mini" type="primary"></el-button> |
| | | <el-dropdown-menu slot="dropdown"> |
| | | <!-- <el-dropdown-item command="change">修改</el-dropdown-item>--> |
| | | <el-dropdown-item command="complete">完成</el-dropdown-item> |
| | | <el-dropdown-item command="cancel">取消</el-dropdown-item> |
| | | <!-- <el-dropdown-item command="shuttleCommand">穿梭车指令</el-dropdown-item>--> |
| | | </el-dropdown-menu> |
| | | </el-dropdown> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | html, |
| | | body { |
| | | margin: 0; |
| | | min-height: 100%; |
| | | color: var(--text-main); |
| | | font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; |
| | | background: |
| | | radial-gradient(1000px 420px at 0% -10%, rgba(44, 107, 193, 0.12), transparent 56%), |
| | | radial-gradient(900px 400px at 100% 0%, rgba(28, 150, 126, 0.10), transparent 58%), |
| | | linear-gradient(180deg, #f2f6fb 0%, #f8fafc 100%); |
| | | } |
| | | |
| | | <div style="margin-top: 10px;"> |
| | | <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" |
| | | :current-page="currentPage" :page-sizes="pageSizes" :page-size="pageSize" |
| | | layout="total, sizes, prev, pager, next, jumper" :total="pageTotal"> |
| | | </el-pagination> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | .page-shell { |
| | | max-width: 1800px; |
| | | margin: 0 auto; |
| | | padding: 14px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | <el-dialog :title="shuttleCommandTitle" :visible.sync="shuttleCommandVisible"> |
| | | <el-table ref="singleTable" :data="shuttleCommandData.commands" style="width: 100%;" :row-class-name="tableRowClassName"> |
| | | <el-table-column property="mode$" label="命令类型"> |
| | | </el-table-column> |
| | | <el-table-column property="start" label="起点"> |
| | | <template slot-scope="scope"> |
| | | <div v-if="scope.row.nodes"> |
| | | x:{{ scope.row.nodes[0].x }} |
| | | y:{{ scope.row.nodes[0].y }} |
| | | z:{{ scope.row.nodes[0].z }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column property="target" label="终点"> |
| | | <template slot-scope="scope"> |
| | | <div v-if="scope.row.nodes"> |
| | | x:{{ scope.row.nodes[scope.row.nodes.length-1].x }} |
| | | y:{{ scope.row.nodes[scope.row.nodes.length-1].y }} |
| | | z:{{ scope.row.nodes[scope.row.nodes.length-1].z }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column property="taskNo" label="任务号"> |
| | | </el-table-column> |
| | | <el-table-column property="shuttleNo" label="穿梭车"> |
| | | </el-table-column> |
| | | <el-table-column property="complete" label="是否完成"> |
| | | <template slot-scope="scope"> |
| | | <el-switch |
| | | v-model="scope.row.complete" |
| | | active-color="#13ce66" |
| | | @change="changeComplete(scope)"> |
| | | </el-switch> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-button @click="shuttleCommandRollback(scope)" size="mini">回退指令</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div slot="footer" class="dialog-footer"> |
| | | <el-button @click="shuttleCommandVisible = false">关闭</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | .card-shell { |
| | | position: relative; |
| | | border-radius: 24px; |
| | | border: 1px solid var(--card-border); |
| | | background: |
| | | radial-gradient(760px 220px at -8% 0%, rgba(43, 117, 196, 0.05), transparent 55%), |
| | | radial-gradient(680px 200px at 108% 10%, rgba(24, 150, 129, 0.05), transparent 58%), |
| | | var(--card-bg); |
| | | box-shadow: 0 16px 32px rgba(44, 67, 96, 0.08); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | </div> |
| | | <script> |
| | | var app = new Vue({ |
| | | el: '#app', |
| | | data: { |
| | | tableData: [], |
| | | currentPage: 1, |
| | | pageSizes: [16, 30, 50, 100, 150, 200], |
| | | pageSize: 30, |
| | | pageTotal: 0, |
| | | tableSearchParam: { |
| | | wrk_no: null, |
| | | wms_wrk_no: null, |
| | | }, |
| | | shuttleCommandVisible: false, |
| | | shuttleCommandData: { |
| | | assignCommand: { |
| | | commands: [] |
| | | }, |
| | | commandStep: 0 |
| | | }, |
| | | shuttleCommandWrkNo: null, |
| | | shuttleCommandLabelWidth: '80px', |
| | | shuttleCommandTitle: '' |
| | | }, |
| | | created() { |
| | | this.init() |
| | | }, |
| | | methods: { |
| | | init() { |
| | | this.getTableData() |
| | | }, |
| | | getTableData() { |
| | | let that = this; |
| | | let data = JSON.parse(JSON.stringify(this.tableSearchParam)) |
| | | data.curr = this.currentPage |
| | | data.limit = this.pageSize |
| | | if (this.tableSearchParam.datetime != null) { |
| | | data.datetime = null |
| | | data.create_time = this.tableSearchParam.datetime[0] + " - " + this.tableSearchParam.datetime[1] |
| | | } |
| | | $.ajax({ |
| | | url: baseUrl + "/wrkMast/list/auth", |
| | | headers: { |
| | | 'token': localStorage.getItem('token') |
| | | }, |
| | | data: data, |
| | | dataType: 'json', |
| | | contentType: 'application/json;charset=UTF-8', |
| | | method: 'GET', |
| | | success: function(res) { |
| | | if (res.code == 200) { |
| | | that.tableData = res.data.records |
| | | that.pageTotal = res.data.total |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: 'error' |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | handleSizeChange(val) { |
| | | console.log(`每页 ${val} 条`); |
| | | this.pageSize = val |
| | | this.getTableData() |
| | | }, |
| | | handleCurrentChange(val) { |
| | | console.log(`当前页: ${val}`); |
| | | this.currentPage = val |
| | | this.getTableData() |
| | | }, |
| | | resetParam() { |
| | | this.tableSearchParam = { |
| | | task_no: null, |
| | | status: null, |
| | | wrk_no: null |
| | | } |
| | | this.getTableData() |
| | | }, |
| | | handleCommand(command, row) { |
| | | switch (command) { |
| | | case "complete": |
| | | this.completeTask(row) |
| | | break; |
| | | case "cancel": |
| | | this.cancelTask(row) |
| | | break; |
| | | case "shuttleCommand": |
| | | this.showShuttleCommand(row.wrkNo) |
| | | break; |
| | | } |
| | | }, |
| | | showShuttleCommand(wrkNo){ |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/shuttle/command/query", |
| | | headers: { |
| | | 'token': localStorage.getItem('token') |
| | | }, |
| | | data: { |
| | | wrkNo: wrkNo |
| | | }, |
| | | method: 'GET', |
| | | success: function(res) { |
| | | if (res.code == 200) { |
| | | console.log(res) |
| | | that.shuttleCommandVisible = true; |
| | | that.shuttleCommandData = res.data; |
| | | that.shuttleCommandWrkNo = wrkNo; |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: 'error' |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | completeTask(row) { |
| | | let that = this |
| | | this.$confirm('确定完成该任务吗?', '提示', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }).then(() => { |
| | | $.ajax({ |
| | | url: baseUrl + "/openapi/completeTask", |
| | | contentType: 'application/json', |
| | | headers: { |
| | | 'token': localStorage.getItem('token') |
| | | }, |
| | | data: JSON.stringify({ |
| | | wrkNo: row.wrkNo, |
| | | }), |
| | | method: 'POST', |
| | | success: function(res) { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: "完成成功", |
| | | type: 'success' |
| | | }); |
| | | that.getTableData() |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: 'error' |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }).catch(() => { |
| | | // this.$message({ |
| | | // type: 'info', |
| | | // message: '已取消删除' |
| | | // }); |
| | | }); |
| | | }, |
| | | cancelTask(row) { |
| | | let that = this |
| | | this.$confirm('确定取消该任务吗?', '提示', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }).then(() => { |
| | | $.ajax({ |
| | | url: baseUrl + "/openapi/cancelTask", |
| | | contentType: 'application/json', |
| | | headers: { |
| | | 'token': localStorage.getItem('token') |
| | | }, |
| | | data: JSON.stringify({ |
| | | wrkNo: row.wrkNo, |
| | | }), |
| | | method: 'POST', |
| | | success: function(res) { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: "取消成功", |
| | | type: 'success' |
| | | }); |
| | | that.getTableData() |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: 'error' |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }).catch(() => { |
| | | // this.$message({ |
| | | // type: 'info', |
| | | // message: '已取消删除' |
| | | // }); |
| | | }); |
| | | }, |
| | | tableRowClassName({row, rowIndex}) { |
| | | if (rowIndex === this.shuttleCommandData.commandStep) { |
| | | return 'success-row'; |
| | | } |
| | | return ''; |
| | | }, |
| | | shuttleCommandRollback(scope) { |
| | | let that = this; |
| | | let idx = scope.$index; |
| | | $.ajax({ |
| | | url: baseUrl + "/shuttle/command/rollback", |
| | | headers: { |
| | | 'token': localStorage.getItem('token') |
| | | }, |
| | | data: { |
| | | wrkNo: that.shuttleCommandWrkNo, |
| | | commandStep: idx |
| | | }, |
| | | method: 'GET', |
| | | success: function(res) { |
| | | if (res.code == 200) { |
| | | that.showShuttleCommand(that.shuttleCommandWrkNo) |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: 'error' |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | changeComplete(scope) { |
| | | let that = this; |
| | | let idx = scope.$index; |
| | | $.ajax({ |
| | | url: baseUrl + "/shuttle/command/completeSwitch", |
| | | headers: { |
| | | 'token': localStorage.getItem('token') |
| | | }, |
| | | data: { |
| | | wrkNo: that.shuttleCommandWrkNo, |
| | | commandStep: idx, |
| | | complete: scope.row.complete ? 1 : 0 |
| | | }, |
| | | method: 'GET', |
| | | success: function(res) { |
| | | if (res.code == 200) { |
| | | that.showShuttleCommand(that.shuttleCommandWrkNo) |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: 'error' |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | }, |
| | | }) |
| | | </script> |
| | | </body> |
| | | .list-toolbar { |
| | | padding: 12px 16px 10px; |
| | | border-bottom: 1px solid rgba(222, 230, 239, 0.92); |
| | | } |
| | | |
| | | .toolbar-main { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .toolbar-left { |
| | | flex: 1 1 980px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .toolbar-search { |
| | | flex: 1 1 auto; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .toolbar-search-item { |
| | | flex: 0 0 148px; |
| | | min-width: 148px; |
| | | } |
| | | |
| | | .toolbar-search-item.keyword { |
| | | flex: 0 0 220px; |
| | | min-width: 220px; |
| | | } |
| | | |
| | | .toolbar-query-actions, |
| | | .toolbar-ops { |
| | | display: flex; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .toolbar-ops { |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .list-toolbar .el-input__inner, |
| | | .advanced-panel .el-input__inner { |
| | | height: 32px; |
| | | line-height: 32px; |
| | | } |
| | | |
| | | .list-toolbar .el-input__icon, |
| | | .advanced-panel .el-input__icon { |
| | | line-height: 32px; |
| | | } |
| | | |
| | | .list-toolbar .el-button, |
| | | .advanced-panel .el-button { |
| | | padding: 8px 12px; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .advanced-panel { |
| | | padding: 10px 16px 12px; |
| | | border-bottom: 1px solid rgba(222, 230, 239, 0.92); |
| | | background: rgba(248, 251, 254, 0.78); |
| | | } |
| | | |
| | | .advanced-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | gap: 8px; |
| | | } |
| | | |
| | | .advanced-item { |
| | | min-width: 0; |
| | | } |
| | | |
| | | .table-wrap { |
| | | padding: 10px 16px; |
| | | } |
| | | |
| | | .table-shell { |
| | | border-radius: 20px; |
| | | overflow: hidden; |
| | | border: 1px solid rgba(217, 227, 238, 0.98); |
| | | background: rgba(255, 255, 255, 0.95); |
| | | } |
| | | |
| | | .table-shell .el-table { |
| | | border-radius: 20px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .table-shell .el-table th { |
| | | background: #f7fafc; |
| | | color: #53677d; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .table-shell .el-table .success-row > td { |
| | | background: rgba(101, 198, 141, 0.14); |
| | | } |
| | | |
| | | .payload-cell { |
| | | display: inline-block; |
| | | max-width: 280px; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .pager-bar { |
| | | padding: 0 16px 16px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .column-popover { |
| | | max-width: 320px; |
| | | } |
| | | |
| | | .column-popover-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | margin-bottom: 10px; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: var(--text-main); |
| | | } |
| | | |
| | | .column-list { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 8px 10px; |
| | | max-height: 280px; |
| | | overflow: auto; |
| | | padding-right: 4px; |
| | | } |
| | | |
| | | .row-action-trigger { |
| | | min-width: 72px; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | @media (max-width: 1600px) { |
| | | .advanced-grid { |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 1280px) { |
| | | .advanced-grid { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 900px) { |
| | | .page-shell { |
| | | padding: 10px; |
| | | } |
| | | |
| | | .list-toolbar, |
| | | .advanced-panel, |
| | | .table-wrap, |
| | | .pager-bar { |
| | | padding-left: 12px; |
| | | padding-right: 12px; |
| | | } |
| | | |
| | | .toolbar-search-item, |
| | | .toolbar-search-item.keyword { |
| | | flex: 1 1 100%; |
| | | min-width: 100%; |
| | | } |
| | | |
| | | .advanced-grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div id="app" class="page-shell" v-cloak> |
| | | <section class="card-shell"> |
| | | <div class="list-toolbar"> |
| | | <div class="toolbar-main"> |
| | | <div class="toolbar-left"> |
| | | <div class="toolbar-search"> |
| | | <div class="toolbar-search-item keyword"> |
| | | <el-input |
| | | v-model.trim="searchForm.condition" |
| | | clearable |
| | | size="small" |
| | | placeholder="请输入关键字" |
| | | @keyup.enter.native="handleSearch"> |
| | | </el-input> |
| | | </div> |
| | | <div class="toolbar-search-item"> |
| | | <el-input |
| | | v-model.trim="searchForm.wrk_no" |
| | | clearable |
| | | size="small" |
| | | placeholder="工作号" |
| | | @keyup.enter.native="handleSearch"> |
| | | </el-input> |
| | | </div> |
| | | <div class="toolbar-search-item"> |
| | | <el-input |
| | | v-model.trim="searchForm.wms_wrk_no" |
| | | clearable |
| | | size="small" |
| | | placeholder="WMS任务号" |
| | | @keyup.enter.native="handleSearch"> |
| | | </el-input> |
| | | </div> |
| | | <div class="toolbar-search-item"> |
| | | <el-input |
| | | v-model.trim="searchForm.loc_no" |
| | | clearable |
| | | size="small" |
| | | placeholder="目标库位" |
| | | @keyup.enter.native="handleSearch"> |
| | | </el-input> |
| | | </div> |
| | | <div class="toolbar-query-actions"> |
| | | <el-button size="small" type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button> |
| | | <el-button size="small" plain icon="el-icon-refresh-left" @click="handleReset">重置</el-button> |
| | | <el-button size="small" plain :icon="advancedVisible ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" @click="toggleAdvanced"> |
| | | {{ advancedVisible ? '收起' : '筛选' }} |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="toolbar-ops"> |
| | | <el-popover |
| | | v-model="columnPopoverVisible" |
| | | placement="bottom-end" |
| | | width="320" |
| | | trigger="click" |
| | | popper-class="column-popover"> |
| | | <div class="column-popover-head"> |
| | | <span>列设置</span> |
| | | <div> |
| | | <el-button type="text" size="mini" @click="showAllColumns">全选</el-button> |
| | | <el-button type="text" size="mini" @click="resetColumns">重置</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="column-list"> |
| | | <el-checkbox |
| | | v-for="column in columnDefs" |
| | | :key="'column-' + column.key" |
| | | :value="isColumnVisible(column.key)" |
| | | @change="toggleColumn(column.key, $event)"> |
| | | {{ column.label }} |
| | | </el-checkbox> |
| | | </div> |
| | | <el-button slot="reference" size="small" plain icon="el-icon-setting">列设置</el-button> |
| | | </el-popover> |
| | | <el-button size="small" plain icon="el-icon-refresh" :loading="loading" @click="loadList">刷新</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-show="advancedVisible" class="advanced-panel"> |
| | | <div class="advanced-grid"> |
| | | <div class="advanced-item"> |
| | | <el-input |
| | | v-model.trim="searchForm.source_loc_no" |
| | | clearable |
| | | size="small" |
| | | placeholder="源库位" |
| | | @keyup.enter.native="handleSearch"> |
| | | </el-input> |
| | | </div> |
| | | <div class="advanced-item"> |
| | | <el-input |
| | | v-model.trim="searchForm.crn_no" |
| | | clearable |
| | | size="small" |
| | | placeholder="堆垛机" |
| | | @keyup.enter.native="handleSearch"> |
| | | </el-input> |
| | | </div> |
| | | <div class="advanced-item"> |
| | | <el-input |
| | | v-model.trim="searchForm.dual_crn_no" |
| | | clearable |
| | | size="small" |
| | | placeholder="双工位堆垛机" |
| | | @keyup.enter.native="handleSearch"> |
| | | </el-input> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table-wrap"> |
| | | <div class="table-shell"> |
| | | <el-table |
| | | ref="dataTable" |
| | | :key="tableRenderKey" |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | border |
| | | stripe |
| | | :height="tableHeight" |
| | | @sort-change="handleSortChange"> |
| | | <el-table-column |
| | | v-for="column in visibleColumns" |
| | | :key="column.key" |
| | | :prop="column.prop" |
| | | :label="column.label" |
| | | :width="column.width" |
| | | :min-width="column.minWidth" |
| | | :sortable="column.sortable ? 'custom' : false" |
| | | :show-overflow-tooltip="column.showOverflow !== false" |
| | | :align="column.align || 'left'"> |
| | | <template v-if="column.key === 'systemMsg'" slot-scope="scope"> |
| | | <span class="payload-cell">{{ scope.row.systemMsg || '--' }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" fixed="right" width="100" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-dropdown trigger="click" @command="handleRowCommand($event, scope.row)"> |
| | | <el-button size="mini" plain class="row-action-trigger"> |
| | | 操作<i class="el-icon-arrow-down el-icon--right"></i> |
| | | </el-button> |
| | | <el-dropdown-menu slot="dropdown"> |
| | | <el-dropdown-item command="complete">完成任务</el-dropdown-item> |
| | | <el-dropdown-item command="cancel" divided>取消任务</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </el-dropdown> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="pager-bar"> |
| | | <el-pagination |
| | | background |
| | | :current-page="currentPage" |
| | | :page-size="pageSize" |
| | | :page-sizes="pageSizes" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :total="pageTotal" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange"> |
| | | </el-pagination> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | </body> |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/wrkMast/wrkMast.js?v=20260311_wrk_mast_vue"></script> |
| | | </html> |