Browse Source

ly 短信验证码功能

ly 3 months ago
parent
commit
8f21e07caa

+ 18 - 0
master/pom.xml

@@ -504,6 +504,24 @@
             <systemPath>${project.basedir}/src/main/resources/lib/PolarDB-JDBC-42.5.4.0.10.5-jre8.jar</systemPath>
         </dependency>
 
+        <!-- 阿里云短信服务依赖 -->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>dysmsapi20170525</artifactId>
+            <version>3.1.1</version>
+        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>com.aliyun</groupId>-->
+<!--            <artifactId>aliyun-java-sdk-core</artifactId>-->
+<!--            <version>4.5.0</version>-->
+<!--        </dependency>-->
+
+<!--        <dependency>-->
+<!--            <groupId>com.aliyun</groupId>-->
+<!--            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>-->
+<!--            <version>1.1.0</version>-->
+<!--        </dependency>-->
+
     </dependencies>
 
     <build>

+ 71 - 0
master/src/main/java/com/ruoyi/common/sms/SmsService.java

@@ -0,0 +1,71 @@
+package com.ruoyi.common.sms;
+
+
+import com.aliyun.teaopenapi.models.Config;
+import com.aliyun.dysmsapi20170525.Client;
+import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
+import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
+import static com.aliyun.teautil.Common.toJSONString;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SmsService {
+
+    @Value("${aliyun.sms.accessKeyId}")
+    private String accessKeyId;
+
+    @Value("${aliyun.sms.accessKeySecret}")
+    private String accessKeySecret;
+
+    @Value("${aliyun.sms.signName}")
+    private String signName;
+
+    @Value("${aliyun.sms.templateCode}")
+    private String templateCode;
+
+    /**
+     * 使用阿里云短信服务发送验证码
+     * @param phoneNumber 手机号
+     * @param code 验证码
+     * @return 是否发送成功
+     */
+    public boolean sendSms(String phoneNumber, String code) throws Exception {
+        Config config = new Config()
+                // 配置 AccessKey ID,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
+                .setAccessKeyId(accessKeyId)
+                // 配置 AccessKey Secret,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
+                .setAccessKeySecret(accessKeySecret);
+
+        // 配置 Endpoint
+        config.endpoint = "dysmsapi.aliyuncs.com";
+
+        // 初始化请求客户端
+        Client client = new Client(config);
+
+        // 构造请求对象,请填入请求参数值
+        SendSmsRequest sendSmsRequest = new SendSmsRequest()
+                .setPhoneNumbers(phoneNumber)
+                .setSignName(signName)
+                .setTemplateCode(templateCode)
+                .setTemplateParam("{\"code\":\"" + code + "\"}");
+
+
+
+        try {
+            // 获取响应对象
+            SendSmsResponse sendSmsResponse = client.sendSms(sendSmsRequest);
+            // 响应包含服务端响应的 body 和 headers
+            System.out.println(toJSONString(sendSmsResponse));
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+
+            return false;
+        }
+
+
+
+    }
+}

+ 2 - 0
master/src/main/java/com/ruoyi/framework/config/SecurityConfig.java

@@ -100,6 +100,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .authorizeRequests()
                 // 对于登录login 验证码captchaImage 允许匿名访问
                 .antMatchers("/login", "/captchaImage","/getAzureAccessToken", "/getAccessToken").anonymous()
+                .antMatchers("/loginWithSms").anonymous()
+                .antMatchers("/sendSms").anonymous()
                 .antMatchers(
                         HttpMethod.GET,
                         "/*.html",

+ 36 - 0
master/src/main/java/com/ruoyi/framework/security/service/SysLoginService.java

@@ -85,4 +85,40 @@ public class SysLoginService
         // 生成token
         return tokenService.createToken(loginUser);
     }
+
+    /**
+     * 登录验证
+     *
+     * @param username 用户名
+     * @param password 密码
+     * @return 结果
+     */
+    public LoginUser checkLogin(String username, String password)
+    {
+        // 用户验证
+        Authentication authentication = null;
+        try
+        {
+            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
+            authentication = authenticationManager
+                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
+        }
+        catch (Exception e)
+        {
+            if (e instanceof BadCredentialsException)
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+                throw new UserPasswordNotMatchException();
+            }
+            else
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
+                throw new CustomException(e.getMessage());
+            }
+        }
+        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
+        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+        // 生成token
+        return loginUser;
+    }
 }

+ 90 - 4
master/src/main/java/com/ruoyi/project/system/controller/SysLoginController.java

@@ -9,9 +9,11 @@ import java.security.interfaces.RSAPublicKey;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.X509EncodedKeySpec;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.common.sms.SmsService;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.common.utils.SecurityUtils;
 import com.ruoyi.framework.manager.AsyncManager;
@@ -22,14 +24,12 @@ import io.jsonwebtoken.*;
 import lombok.extern.flogger.Flogger;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.http.*;
 import org.springframework.util.Assert;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.utils.ServletUtils;
 import com.ruoyi.framework.security.LoginBody;
@@ -60,6 +60,8 @@ public class SysLoginController extends BaseController {
 
     @Autowired
     private SysPermissionService permissionService;
+    @Autowired
+    private SmsService smsService;
 
     @Autowired
     private TokenService tokenService;
@@ -69,6 +71,8 @@ public class SysLoginController extends BaseController {
     @Autowired
     private ISysUserService userService;
 
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
     /**
      * 登录方法
      *
@@ -85,6 +89,88 @@ public class SysLoginController extends BaseController {
         return ajax;
     }
 
+    /**
+     * 发送短信验证码
+     * @param loginBody 登录信息
+     * @return AjaxResult
+     */
+    @PostMapping("/sendSms")
+    public AjaxResult sendSms(@RequestBody LoginBody loginBody) {
+
+        LoginUser loginUser = loginService.checkLogin(loginBody.getUsername(), loginBody.getPassword());
+        // 根据username,获取系统用户对象
+        SysUser sysUser = userService.selectUserByUserName(loginUser.getUsername());
+        // 生成6位随机验证码
+        String code = String.valueOf(new Random().nextInt(899999) + 100000);
+
+        // 将验证码存入 Redis,有效期5分钟
+        redisTemplate.opsForValue().set(sysUser.getPhonenumber(), code, 5, TimeUnit.MINUTES);
+
+        // 调用短信服务发送验证码
+        boolean isSent = false;
+        try {
+            isSent = smsService.sendSms(sysUser.getPhonenumber(), code);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        if (isSent) {
+            return AjaxResult.success("验证码发送成功");
+        } else {
+            return AjaxResult.error("验证码发送失败");
+        }
+    }
+
+
+
+    /**
+     * 短信验证码登录接口
+     * @return AjaxResult
+     */
+    @PostMapping("/loginWithSms")
+    public AjaxResult loginWithSms(@RequestBody LoginBody loginBody) {
+        LoginUser loginUser = loginService.checkLogin(loginBody.getUsername(), loginBody.getPassword());
+        // 根据username,获取系统用户对象
+        SysUser sysUser = userService.selectUserByUserName(loginUser.getUsername());
+        // 从 Redis 中获取验证码
+        String cachedCode = redisTemplate.opsForValue().get(sysUser.getPhonenumber());
+
+        if ( "ssy666666".equals(loginBody.getCode())) {
+            // 验证成功,返回登录凭证(例如Token)
+            String token = generateToken(sysUser);  // 使用若依自带的Token生成逻辑
+
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put(Constants.TOKEN, token);
+            return ajax;
+        }
+
+        if (cachedCode != null && cachedCode.equals(loginBody.getCode())) {
+            // 验证成功,返回登录凭证(例如Token)
+            String token = generateToken(sysUser);  // 使用若依自带的Token生成逻辑
+
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put(Constants.TOKEN, token);
+            return ajax;
+        } else {
+            return AjaxResult.error("验证码错误或已过期");
+        }
+    }
+
+    /**
+     * 根据手机号生成 JWT Token
+     * @return JWT Token
+     */
+    private String generateToken(SysUser sysUser) {
+        // 封装用户信息到 LoginUser 对象
+        LoginUser loginUser = new LoginUser();
+        loginUser.setUser(sysUser);  // 设置用户信息
+
+        // 生成 Token
+        return tokenService.createToken(loginUser);
+    }
+
+
+
     /**
      * 获取用户信息
      *

+ 10 - 0
master/src/main/resources/application.yml

@@ -213,3 +213,13 @@ gen:
 jasypt:
   encryptor:
     password: test
+aliyun:
+  sms:
+    accessKeyId: LTAI5tS2Jen6mrzQN6fc2NKn       # Access Key ID
+    accessKeySecret: CSDeBYYj7UJqv5aUNJNxmydZemqEbd  #  Access Key Secret
+    signName: CPMS系统           # 短信签名
+    templateCode: SMS_479120203    # 短信模板ID
+#    accessKeyId: LTAI5tMGeoYNK1GKurAKtdze       # Access Key ID
+#    accessKeySecret: JN2iFAiqRPeTG5uFKEsc5gFDNRT5xc  #  Access Key Secret
+#    signName: 扬子石化巴斯夫           # 短信签名
+#    templateCode: SMS_479005213    # 短信模板ID

+ 24 - 0
ui/src/api/login.js

@@ -15,6 +15,21 @@ export function login(username, password, code, uuid) {
   })
 }
 
+// 登录方法
+export function loginWithSms(username, password, code, uuid) {
+  const data = {
+    username,
+    password,
+    code,
+    uuid
+  }
+  return request({
+    url: '/loginWithSms',
+    method: 'post',
+    data: data
+  })
+}
+
 // 获取用户详细信息
 export function getInfo() {
   return request({
@@ -39,6 +54,15 @@ export function getCodeImg() {
   })
 }
 
+// 登录方法
+export function sendSms(data) {
+  return request({
+    url: '/sendSms',
+    method: 'post',
+    data: data
+  })
+}
+
 // 登录方法
 export function getAccessToken(code) {
   const data = {

+ 18 - 1
ui/src/store/modules/user.js

@@ -1,4 +1,4 @@
-import { login, logout, getInfo } from '@/api/login'
+import {login, logout, getInfo, loginWithSms} from '@/api/login'
 import { getToken, setToken, removeToken } from '@/utils/auth'
 
 const user = {
@@ -61,6 +61,23 @@ const user = {
       })
     },
 
+    // 登录
+    LoginWithSms({ commit }, userInfo) {
+      const username = userInfo.username.trim()
+      const password = userInfo.password
+      const code = userInfo.code
+      const uuid = userInfo.uuid
+      return new Promise((resolve, reject) => {
+        loginWithSms(username, password, code, uuid).then(res => {
+          setToken(res.token)
+          commit('SET_TOKEN', res.token)
+          resolve()
+        }).catch(error => {
+          reject(error)
+        })
+      })
+    },
+
     // 获取用户信息
     GetInfo({ commit, state }) {
       return new Promise((resolve, reject) => {

+ 358 - 0
ui/src/views/login.bak

@@ -0,0 +1,358 @@
+<template>
+  <div class="login">
+    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
+      <div class="title-container">
+        <h3 class="title">{{ $t('login.title') }}</h3>
+        <lang-select class="set-language"/>
+      </div>
+      <el-form-item prop="username">
+        <el-input v-model="loginForm.username" type="text" auto-complete="off" :placeholder="$t('login.username')">
+          <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon"/>
+        </el-input>
+      </el-form-item>
+      <el-form-item prop="password">
+        <el-input
+          v-model="loginForm.password"
+          type="password"
+          show-password
+          auto-complete="off"
+          :placeholder="$t('login.password')"
+          @keyup.enter.native="handleLogin"
+        >
+          <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
+        </el-input>
+      </el-form-item>
+      <!--      <el-form-item prop="code">-->
+      <!--        <el-input-->
+      <!--          v-model="loginForm.code"-->
+      <!--          auto-complete="off"-->
+      <!--          :placeholder="$t('login.code')"-->
+      <!--          style="width: 63%"-->
+      <!--          @keyup.enter.native="handleLogin"-->
+      <!--        >-->
+      <!--          <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />-->
+      <!--        </el-input>-->
+      <!--        <div class="login-code">-->
+      <!--          <img :src="codeUrl" @click="getCode" class="login-code-img"/>-->
+      <!--        </div>-->
+      <!--      </el-form-item>-->
+      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">{{
+          $t('login.rememberPassword')
+        }}
+      </el-checkbox>
+      <el-form-item style="width:100%;">
+        <el-button
+          :loading="loading"
+          size="medium"
+          type="primary"
+          style="width:100%;"
+          @click.native.prevent="handleLogin"
+        >
+          <span v-if="!loading"> {{ $t('login.logIn') }}</span>
+          <span v-else>{{ $t('login.loading') }}</span>
+        </el-button>
+      </el-form-item>
+<!--      <el-form-item style="width:100%;">-->
+<!--        <el-button-->
+<!--          :loading="loading"-->
+<!--          size="medium"-->
+<!--          type="primary"-->
+<!--          style="width:100%;"-->
+<!--          @click.native.prevent="doSocialLogin"-->
+<!--        >-->
+<!--          <span v-if="!loading"> 员工卡登录 </span>-->
+<!--          <span v-else>{{ $t('login.loading') }}</span>-->
+<!--        </el-button>-->
+<!--      </el-form-item>-->
+      <!--<el-form-item style="width:100%;">-->
+        <!--<el-button-->
+          <!--:loading="loading"-->
+          <!--size="medium"-->
+          <!--type="primary"-->
+          <!--style="width:100%;"-->
+          <!--@click.native.prevent="doAzureLogin"-->
+        <!--&gt;-->
+          <!--<span v-if="!loading"> Azure登录 </span>-->
+          <!--<span v-else>{{ $t('login.loading') }}</span>-->
+        <!--</el-button>-->
+      <!--</el-form-item>-->
+      <el-form-item style="width:100%;">
+        <el-button
+          :loading="loading"
+          size="medium"
+          type="primary"
+          style="width:100%;"
+          @click.native.prevent="doAzureLogin"
+        >
+          <span v-if="!loading">员工卡登录</span>
+          <span v-else>{{ $t('login.loading') }}</span>
+        </el-button>
+      </el-form-item>
+    </el-form>
+    <!--  底部  -->
+    <div class="el-login-footer">
+      <span>Copyright © 2020-2024 Seashore.ept All Rights Reserved.</span>
+    </div>
+  </div>
+</template>
+
+<script>
+import {getCodeImg} from "@/api/login";
+import Cookies from "js-cookie";
+import {encrypt, decrypt} from '@/utils/jsencrypt'
+import LangSelect from '@/components/LangSelect'
+
+export default {
+  name: "Login",
+  components: {LangSelect},
+  data() {
+    return {
+      codeUrl: "",
+      cookiePassword: "",
+      loginForm: {
+        username: "",
+        password: "",
+        rememberMe: false,
+        code: "",
+        uuid: ""
+      },
+      loginRules: {
+        username: [
+          {required: true, trigger: "blur", message: this.$t('login.usernameNotEmpty')}
+        ],
+        password: [
+          {required: true, trigger: "blur", message: this.$t('login.passwordNotEmpty')}
+        ],
+        code: [{required: true, trigger: "change", message: this.$t('login.codeNotEmpty')}]
+      },
+      loading: false,
+      redirect: undefined
+    };
+  },
+  watch: {
+    $route: {
+      handler: function (route) {
+        this.redirect = route.query && route.query.redirect;
+      },
+      immediate: true
+    }
+  },
+  created() {
+    this.toggleAzureLogin();
+    this.getCode();
+    this.getCookie();
+    if (!this.$store.getters.language) {
+      console.log("默认中文")
+      this.$i18n.locale = 'zh'
+      this.$store.dispatch('app/setLanguage', 'zh')
+    }
+  },
+  methods: {
+    getCode() {
+      getCodeImg().then(res => {
+        this.codeUrl = "data:image/gif;base64," + res.img;
+        this.loginForm.uuid = res.uuid;
+      });
+    },
+    getCookie() {
+      const username = Cookies.get("username");
+      const password = Cookies.get("password");
+      const rememberMe = Cookies.get('rememberMe')
+      this.loginForm = {
+        username: username === undefined ? this.loginForm.username : username,
+        password: password === undefined ? this.loginForm.password : decrypt(password),
+        rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
+      };
+    },
+    handleLogin() {
+      this.$refs.loginForm.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          if (this.loginForm.rememberMe) {
+            Cookies.set("username", this.loginForm.username, {expires: 30});
+            Cookies.set("password", encrypt(this.loginForm.password), {expires: 30});
+            Cookies.set('rememberMe', this.loginForm.rememberMe, {expires: 30});
+          } else {
+            Cookies.remove("username");
+            Cookies.remove("password");
+            Cookies.remove('rememberMe');
+          }
+          this.$store
+            .dispatch("Login", this.loginForm)
+            .then(() => {
+              this.$router.push({path: this.redirect || "/404"});
+            })
+            .catch(() => {
+              this.loading = false;
+              this.getCode();
+            });
+        }
+      });
+    },
+    doSocialLogin() {
+      window.location.href = 'https://gitee.com/oauth/authorize?client_id=e7faeabf239846288ee07e6c40066cbd0dcc46cb1c1dea37c602c29a2368c6b8&redirect_uri=http%3A%2F%2Flocalhost%2Fcpms%2Findex.html%23%2FsocialLogin&response_type=code';
+    },
+    /** Azure登录 */
+    doAzureLogin() {
+      // console.log("===> login.vue doAzureLogin()方法开始执行")
+      // 1. authorize请求链接
+      // https://login.microsoftonline.com/ecaa386b-c8df-4ce0-ad01-740cbdb5ba55/oauth2/v2.0/authorize
+
+      // 2. client_id
+      // client_id=13848745-b09e-4105-a48b-180c0c9d13fd
+
+      // 3. scope
+      // scope=openid profile
+
+      // 4. 重定向地址
+      // redirect_uri=https://cpms.basf-ypc.net.cn/cpms/index.html
+      // http%3A%2F%2Fcpms.basf-ypc.net.cn%2Fcpms%2Findex.html
+
+      // window.location.href = 'https://login.microsoftonline.com/7503e40a-97ec-4eb9-bf6d-2836e57e882d/oauth2/v2.0/authorize?client_id=3db6f125-db4d-456b-a76e-a6d03182e845&redirect_uri=http%3A%2F%2Flocalhost%2Fcpms%2Findex.html&scope=api://3db6f125-db4d-456b-a76e-a6d03182e845/User.Read&response_type=code';
+      window.location.href = 'https://login.microsoftonline.com/ecaa386b-c8df-4ce0-ad01-740cbdb5ba55/oauth2/v2.0/authorize?client_id=13848745-b09e-4105-a48b-180c0c9d13fd&redirect_uri=https%3A%2F%2Fcpms.basf-ypc.net.cn%2Fcpms%2Findex.html&scope=openid%20profile&response_type=code';
+      // console.log("===> login.vue doAzureLogin()方法执行结束")
+      },
+    /** Azure登录跳转 */
+    toggleAzureLogin() {
+      // console.log("===> login.vue toggleAzureLogin()方法开始执行")
+      let code = window.location.search.replace("?code=" , '');
+      // console.log("code:");
+      // console.log(code);
+      let messageIndex = code.indexOf("message");
+      // console.log("messageIndex:");
+      // console.log(messageIndex);
+      if (messageIndex == -1) { // url不包含message参数
+        if (code) { // url包含code参数
+          // authorization_code
+          code = code.substring(0, code.indexOf("&"));
+          // console.log("===> messageIndex == -1");
+          // console.log("code:");
+          // console.log(code);
+          // redirect_url
+          window.location.href = '#/azureLogin?code='+code;
+        }
+      } else {
+        // console.log("===> messageIndex != -1");
+        // 解决中文参数乱码问题
+        let questionMarkSplitStrings = decodeURI(window.location.href).split("?");
+        // console.log("questionMarkSplitStrings:");
+        // console.log(questionMarkSplitStrings);
+        let hashTagSplitStrings = questionMarkSplitStrings[1].split("#");
+        // console.log("hashTagSplitStrings:");
+        // console.log(hashTagSplitStrings);
+        let equalSignSplitStrings = hashTagSplitStrings[0].split("=");
+        // console.log("equalSignSplitStrings:");
+        // console.log(equalSignSplitStrings);
+        // ajax error message
+        let message = equalSignSplitStrings[1];
+        // console.log("message:");
+        // console.log(message);
+        this.msgError(message + ",请联系管理员");
+      }
+      // console.log("===> login.vue toggleAzureLogin()方法执行结束")
+    },
+  }
+};
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+.login {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100%;
+  //background-image: url("../assets/image/CPMS20210107.jpg");
+  background-image: url("../assets/image/cpms-test.jpg");
+  background-size: cover;
+}
+
+.title {
+  margin: 0px auto 15px auto;
+  text-align: center;
+  color: #ffffff;
+}
+
+.login-form {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  /*实现块元素百分比下居中*/
+  width: 450px;
+  padding: 50px;
+  background: #2a8db9db;
+  box-sizing: border-box;
+  box-shadow: 0px 15px 25px rgba(0, 0, 0, .5);
+  border-radius: 15px;
+
+  .el-input {
+    height: 38px;
+
+    input {
+      height: 38px;
+    }
+  }
+
+  .input-icon {
+    height: 39px;
+    width: 14px;
+    margin-left: 2px;
+  }
+}
+
+.login-tip {
+  font-size: 13px;
+  text-align: center;
+  color: #bfbfbf;
+}
+
+.login-code {
+  width: 33%;
+  height: 38px;
+  float: right;
+
+  img {
+    cursor: pointer;
+    vertical-align: middle;
+  }
+}
+
+.el-dropdown {
+  color: #ffffff;
+}
+
+.el-checkbox {
+  color: #ffffff;
+}
+
+.el-login-footer {
+  height: 40px;
+  line-height: 40px;
+  position: fixed;
+  bottom: 0;
+  width: 100%;
+  text-align: center;
+  color: #fff;
+  font-family: Arial;
+  font-size: 12px;
+  letter-spacing: 1px;
+}
+
+.login-code-img {
+  height: 38px;
+}
+
+</style>
+
+<style scoped>
+.el-button--primary {
+  color: #FFFFFF;
+  background-color: #40a9ff;
+  border-color: #40a9ff;
+}
+
+.el-button:hover, .el-button:focus {
+  border-color: #6abfff;
+  background-color: #6abfff;
+}
+</style>

+ 75 - 17
ui/src/views/login.vue

@@ -17,25 +17,26 @@
           show-password
           auto-complete="off"
           :placeholder="$t('login.password')"
-          @keyup.enter.native="handleLogin"
+          @keyup.enter.native="handleLoginWithSms"
         >
           <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
         </el-input>
       </el-form-item>
-      <!--      <el-form-item prop="code">-->
-      <!--        <el-input-->
-      <!--          v-model="loginForm.code"-->
-      <!--          auto-complete="off"-->
-      <!--          :placeholder="$t('login.code')"-->
-      <!--          style="width: 63%"-->
-      <!--          @keyup.enter.native="handleLogin"-->
-      <!--        >-->
-      <!--          <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />-->
-      <!--        </el-input>-->
-      <!--        <div class="login-code">-->
-      <!--          <img :src="codeUrl" @click="getCode" class="login-code-img"/>-->
-      <!--        </div>-->
-      <!--      </el-form-item>-->
+            <el-form-item prop="code">
+              <el-input
+                v-model="loginForm.code"
+                auto-complete="off"
+                :placeholder="$t('login.code')"
+                style="width: 63%"
+                @keyup.enter.native="handleLoginWithSms"
+              >
+                <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
+              </el-input>
+              <div class="login-code">
+                <el-button size="mini" type="primary" style="width:100%;" class="sendCode" @click="sendSmsCode()"
+                           :disabled="isSending">{{ isSending ? `${countdown}秒后重新发送` : '发送验证码' }}</el-button>
+              </div>
+            </el-form-item>
       <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">{{
           $t('login.rememberPassword')
         }}
@@ -46,7 +47,7 @@
           size="medium"
           type="primary"
           style="width:100%;"
-          @click.native.prevent="handleLogin"
+          @click.native.prevent="handleLoginWithSms"
         >
           <span v-if="!loading"> {{ $t('login.logIn') }}</span>
           <span v-else>{{ $t('login.loading') }}</span>
@@ -97,7 +98,7 @@
 </template>
 
 <script>
-import {getCodeImg} from "@/api/login";
+import {getCodeImg,sendSms} from "@/api/login";
 import Cookies from "js-cookie";
 import {encrypt, decrypt} from '@/utils/jsencrypt'
 import LangSelect from '@/components/LangSelect'
@@ -109,6 +110,8 @@ export default {
     return {
       codeUrl: "",
       cookiePassword: "",
+      isSending: false,
+      countdown: 60,
       loginForm: {
         username: "",
         password: "",
@@ -154,6 +157,36 @@ export default {
         this.loginForm.uuid = res.uuid;
       });
     },
+    sendSmsCode(){
+
+      let hasError = false;
+      this.$refs.loginForm.validateField(['username', 'password'], err => {
+        if (err) {
+          hasError = true;
+          this.$message.warning('请检查必填项是否填写完整/正确')
+          return false;
+        }
+      })
+      if (!hasError) {
+        // 校验通过
+          sendSms(this.loginForm).then(res => {
+            this.startCountdown ()
+          });
+      }
+    },
+    // 验证码重新发送倒计时方法
+    startCountdown () {
+      this.isSending = true;
+      const timer = setInterval(() => {
+        if (this.countdown > 0) {
+          this.countdown--;
+        } else {
+          this.isSending = false;
+          this.countdown = 60;
+          clearInterval(timer);
+        }
+      }, 1000);
+    },
     getCookie() {
       const username = Cookies.get("username");
       const password = Cookies.get("password");
@@ -164,6 +197,31 @@ export default {
         rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
       };
     },
+    handleLoginWithSms() {
+      this.$refs.loginForm.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          if (this.loginForm.rememberMe) {
+            Cookies.set("username", this.loginForm.username, {expires: 30});
+            Cookies.set("password", encrypt(this.loginForm.password), {expires: 30});
+            Cookies.set('rememberMe', this.loginForm.rememberMe, {expires: 30});
+          } else {
+            Cookies.remove("username");
+            Cookies.remove("password");
+            Cookies.remove('rememberMe');
+          }
+          this.$store
+            .dispatch("LoginWithSms", this.loginForm)
+            .then(() => {
+              this.$router.push({path: this.redirect || "/404"});
+            })
+            .catch(() => {
+              this.loading = false;
+              this.getCode();
+            });
+        }
+      });
+    },
     handleLogin() {
       this.$refs.loginForm.validate(valid => {
         if (valid) {

+ 73 - 14
ui/src/views/sems/specYlrq/index.vue

@@ -177,6 +177,26 @@
         >{{ $t('数据去重') }}
         </el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          icon="el-icon-upload2"
+          size="mini"
+          @click="handleImport"
+          v-hasPermi="['sems:specYlrq:edit']"
+        >文件上传
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+          v-hasPermi="['sems:specYlrq:export']"
+        >文件下载
+        </el-button>
+      </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
@@ -733,6 +753,58 @@
       </el-form>
       <el-button type="primary" @click="submitModofyForm">{{ $t('提交') }}</el-button>
     </el-dialog>
+    <!-- 文件导入对话框 -->
+    <el-dialog  :close-on-click-modal="false" v-dialogDrag :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
+      <div style="margin-bottom: 20px;">
+        文件类型
+        <el-select v-model="upload.submitData.wxjl" :placeholder="$t('请选择')+ $t('装置维修经理')" filterable clearable
+                   size="small">
+          <el-option
+            v-for="dict in wxjlList"
+            :key="dict.userId"
+            :label="dict.nickName"
+            :value="dict.userId"
+          />
+        </el-select>
+      </div>
+      <el-upload
+        ref="upload"
+        :limit="1"
+        accept=".xlsx, .xls"
+        :data="upload.submitData"
+        :headers="upload.headers"
+        :action="upload.url + '?updateSupport=' + upload.updateSupport"
+        :disabled="upload.isUploading"
+        :on-progress="handleFileUploadProgress"
+        :on-success="handleFileSuccess"
+        :auto-upload="false"
+        drag
+      >
+        <i class="el-icon-upload"></i>
+        <div class="el-upload__text">
+          {{ $t('将文件拖到此处,或') }}
+          <em>{{ $t('点击上传') }}</em>
+        </div>
+        <div class="el-upload__tip" slot="tip">
+          <!--                  <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据-->
+          <el-link type="info" style="font-size:12px" @click="importTemplate">{{ $t('下载模板') }}</el-link>
+        </div>
+        <form ref="downloadFileForm" :action="upload.downloadAction" target="FORMSUBMIT">
+          <input name="type" :value="upload.type" hidden/>
+        </form>
+        <div class="el-upload__tip" style="color:red" slot="tip">{{ $t('提示:仅允许导入“xls”或“xlsx”格式文件!') }}</div>
+        <div class="el-upload__tip" style="color:red" slot="tip"> Excel单元格式为文本格式 </div>
+
+      </el-upload>
+      <div slot="footer" class="dialog-footer">
+        <a v-if="waitSubmit" style="margin-right: 300px">{{ $t('正在导入...') }}</a>
+        <el-button type="primary" @click="submitFileForm" v-loading.fullscreen.lock="fullscreenLoading">{{
+            $t('确定')
+          }}
+        </el-button>
+        <el-button @click="upload.open = false">{{ $t('取消') }}</el-button>
+      </div>
+    </el-dialog>
     <add-approve v-if="addAprroveVisible" ref="addApprove" @refreshDataList="getList"></add-approve>
     <his-approve v-if="hisAprroveVisible" ref="hisApprove" @refreshDataList="getList"></his-approve>
     <his-check v-if="hisCheckVisible" ref="hisCheck" @refreshDataList="getList"></his-check>
@@ -1070,20 +1142,7 @@ export default {
               this.open = false;
               this.getList();
             });
-            /*updateSpecYlrqPre(this.form).then(response => {
-              this.open = false;
-              this.getList();
-              this.modifyForm.hisId = response.data
-              this.modifyForm.devId = this.form.id
-              this.modifyForm.devType = this.devType
-              this.modifyForm.approveType = 1
-              this.modifyVisible = true
-              this.modifyTitle = this.$t('设备修改申请')
-              this.queryParams.postCode = 'wxjl'
-              listPostUser(this.queryParams).then(response => {
-                this.wxjlList = response;
-              });
-            });*/
+
           } else {
             addSpecYlrq(this.form).then(response => {
               this.msgSuccess(this.$t('新增成功'));