增加登录验证码功能
This commit is contained in:
@@ -46,6 +46,7 @@ public class SecurityConfig {
|
|||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
// API 公开接口
|
// API 公开接口
|
||||||
.requestMatchers("/api/auth/login", "/api/auth/register", "/api/auth/logout").permitAll()
|
.requestMatchers("/api/auth/login", "/api/auth/register", "/api/auth/logout").permitAll()
|
||||||
|
.requestMatchers("/api/captcha").permitAll()
|
||||||
.requestMatchers("/api/files/test").permitAll()
|
.requestMatchers("/api/files/test").permitAll()
|
||||||
.requestMatchers("/api/users/config").permitAll()
|
.requestMatchers("/api/users/config").permitAll()
|
||||||
// WebSocket
|
// WebSocket
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.filesystem.entity.User;
|
|||||||
import com.filesystem.security.UserPrincipal;
|
import com.filesystem.security.UserPrincipal;
|
||||||
import com.filesystem.service.UserService;
|
import com.filesystem.service.UserService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -19,17 +20,23 @@ public class AuthController {
|
|||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<?> login(@RequestBody Map<String, String> request) {
|
public ResponseEntity<?> login(@RequestBody Map<String, String> request, HttpSession session) {
|
||||||
String username = request.get("username");
|
String username = request.get("username");
|
||||||
String password = request.get("password");
|
String password = request.get("password");
|
||||||
|
String captcha = request.get("captcha");
|
||||||
|
|
||||||
|
// 验证码校验
|
||||||
|
if (!CaptchaController.verify(session, captcha)) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "验证码错误或已过期"));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String token = userService.login(username, password);
|
String token = userService.login(username, password);
|
||||||
User user = userService.findByUsername(username);
|
User user = userService.findByUsername(username);
|
||||||
|
|
||||||
// 精确重算存储空间
|
// 直接读取存储空间,不重算
|
||||||
long storageUsed = userService.recalculateStorage(user.getId());
|
long storageUsed = user.getStorageUsed() != null ? user.getStorageUsed() : 0L;
|
||||||
long storageLimit = user.getStorageLimit();
|
long storageLimit = user.getStorageLimit() != null ? user.getStorageLimit() : 0L;
|
||||||
|
|
||||||
Map<String, Object> userData = new HashMap<>();
|
Map<String, Object> userData = new HashMap<>();
|
||||||
userData.put("id", user.getId());
|
userData.put("id", user.getId());
|
||||||
|
|||||||
130
src/main/java/com/filesystem/controller/CaptchaController.java
Normal file
130
src/main/java/com/filesystem/controller/CaptchaController.java
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package com.filesystem.controller;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/captcha")
|
||||||
|
public class CaptchaController {
|
||||||
|
|
||||||
|
private static final String CHARS = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
|
||||||
|
private static final int WIDTH = 140;
|
||||||
|
private static final int HEIGHT = 40;
|
||||||
|
private static final int CODE_LENGTH = 5;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<?> getCaptcha(HttpSession session) {
|
||||||
|
// 生成随机验证码
|
||||||
|
String code = generateCode();
|
||||||
|
|
||||||
|
// 存入 session
|
||||||
|
session.setAttribute("captcha", code.toLowerCase());
|
||||||
|
session.setAttribute("captchaTime", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 生成图片
|
||||||
|
String imageBase64 = generateCaptchaImage(code);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"data", Map.of(
|
||||||
|
"image", "data:image/png;base64," + imageBase64
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证验证码
|
||||||
|
*/
|
||||||
|
public static boolean verify(HttpSession session, String inputCode) {
|
||||||
|
if (inputCode == null || inputCode.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String storedCode = (String) session.getAttribute("captcha");
|
||||||
|
Long captchaTime = (Long) session.getAttribute("captchaTime");
|
||||||
|
|
||||||
|
if (storedCode == null || captchaTime == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码 5 分钟有效
|
||||||
|
if (System.currentTimeMillis() - captchaTime > 5 * 60 * 1000) {
|
||||||
|
session.removeAttribute("captcha");
|
||||||
|
session.removeAttribute("captchaTime");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证后清除,防止重复使用
|
||||||
|
session.removeAttribute("captcha");
|
||||||
|
session.removeAttribute("captchaTime");
|
||||||
|
|
||||||
|
return storedCode.equals(inputCode.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateCode() {
|
||||||
|
Random random = new Random();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < CODE_LENGTH; i++) {
|
||||||
|
sb.append(CHARS.charAt(random.nextInt(CHARS.length())));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateCaptchaImage(String code) {
|
||||||
|
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D g = image.createGraphics();
|
||||||
|
|
||||||
|
// 设置抗锯齿
|
||||||
|
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
// 背景
|
||||||
|
g.setColor(Color.WHITE);
|
||||||
|
g.fillRect(0, 0, WIDTH, HEIGHT);
|
||||||
|
|
||||||
|
// 干扰线
|
||||||
|
Random random = new Random();
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
g.setColor(new Color(random.nextInt(200), random.nextInt(200), random.nextInt(200)));
|
||||||
|
g.drawLine(random.nextInt(WIDTH), random.nextInt(HEIGHT),
|
||||||
|
random.nextInt(WIDTH), random.nextInt(HEIGHT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 干扰点
|
||||||
|
for (int i = 0; i < 40; i++) {
|
||||||
|
g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
|
||||||
|
g.fillOval(random.nextInt(WIDTH), random.nextInt(HEIGHT), 2, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码文字
|
||||||
|
g.setFont(new Font("Arial", Font.BOLD, 26));
|
||||||
|
for (int i = 0; i < code.length(); i++) {
|
||||||
|
// 随机颜色
|
||||||
|
g.setColor(new Color(random.nextInt(150), random.nextInt(150), random.nextInt(150)));
|
||||||
|
// 随机角度
|
||||||
|
double angle = (random.nextDouble() - 0.5) * 0.4;
|
||||||
|
g.rotate(angle, 18 + i * 24, 26);
|
||||||
|
g.drawString(String.valueOf(code.charAt(i)), 10 + i * 24, 28);
|
||||||
|
g.rotate(-angle, 18 + i * 24, 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.dispose();
|
||||||
|
|
||||||
|
// 转为 Base64
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
javax.imageio.ImageIO.write(image, "png", baos);
|
||||||
|
return Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||||
|
} catch (IOException e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,6 +165,8 @@ public class MessageController {
|
|||||||
m.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername());
|
m.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername());
|
||||||
m.put("fromAvatar", fromUser.getAvatar());
|
m.put("fromAvatar", fromUser.getAvatar());
|
||||||
m.put("fromSignature", fromUser.getSignature());
|
m.put("fromSignature", fromUser.getSignature());
|
||||||
|
m.put("fromPhone", fromUser.getPhone());
|
||||||
|
m.put("fromEmail", fromUser.getEmail());
|
||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ public class ChatHandler extends TextWebSocketHandler {
|
|||||||
messageData.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername());
|
messageData.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername());
|
||||||
messageData.put("fromAvatar", fromUser.getAvatar());
|
messageData.put("fromAvatar", fromUser.getAvatar());
|
||||||
messageData.put("fromSignature", fromUser.getSignature());
|
messageData.put("fromSignature", fromUser.getSignature());
|
||||||
|
messageData.put("fromPhone", fromUser.getPhone());
|
||||||
|
messageData.put("fromEmail", fromUser.getEmail());
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> resp = Map.of("type", "chat", "message", messageData);
|
Map<String, Object> resp = Map.of("type", "chat", "message", messageData);
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ export const login = (data) => request.post('/auth/login', data)
|
|||||||
export const register = (data) => request.post('/auth/register', data)
|
export const register = (data) => request.post('/auth/register', data)
|
||||||
|
|
||||||
export const getCurrentUser = () => request.get('/auth/info')
|
export const getCurrentUser = () => request.get('/auth/info')
|
||||||
|
|
||||||
|
export const getCaptcha = () => request.get('/captcha')
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-for="msg in currentMessages" :key="msg.id" class="message-wrapper" :class="{ self: msg.isSelf }">
|
<div v-for="msg in currentMessages" :key="msg.id" class="message-wrapper" :class="{ self: msg.isSelf }">
|
||||||
<el-popover placement="left" :width="200" trigger="hover">
|
<el-popover :placement="msg.isSelf ? 'left' : 'right'" :width="240" trigger="hover">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-avatar
|
<el-avatar
|
||||||
:size="30"
|
:size="30"
|
||||||
@@ -104,9 +104,17 @@
|
|||||||
<span class="user-info-label">昵称:</span>
|
<span class="user-info-label">昵称:</span>
|
||||||
<span class="user-info-value">{{ msg.isSelf ? (currentUserNickname || currentUserName) : (msg.fromNickname || '未知') }}</span>
|
<span class="user-info-value">{{ msg.isSelf ? (currentUserNickname || currentUserName) : (msg.fromNickname || '未知') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info-row signature-row">
|
<div class="user-info-row">
|
||||||
|
<span class="user-info-label">电话:</span>
|
||||||
|
<span class="user-info-value">{{ msg.isSelf ? (userStore.phone || '暂无') : (msg.fromPhone || '暂无') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-info-row">
|
||||||
|
<span class="user-info-label">邮箱:</span>
|
||||||
|
<span class="user-info-value">{{ msg.isSelf ? (userStore.email || '暂无') : (msg.fromEmail || '暂无') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-info-row">
|
||||||
<span class="user-info-label">签名:</span>
|
<span class="user-info-label">签名:</span>
|
||||||
<div class="user-info-signature">
|
<div class="user-info-signature-box">
|
||||||
{{ msg.isSelf ? (userStore.signature || '暂无') : (msg.fromSignature || '暂无') }}
|
{{ msg.isSelf ? (userStore.signature || '暂无') : (msg.fromSignature || '暂无') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -753,12 +761,23 @@ onUnmounted(() => { if (unsubscribeWs) unsubscribeWs() })
|
|||||||
.uploading-mini { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; color: #409eff; font-size: 12px; background: #ecf5ff; border-radius: 4px; }
|
.uploading-mini { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; color: #409eff; font-size: 12px; background: #ecf5ff; border-radius: 4px; }
|
||||||
.uploading-mini .el-icon { font-size: 14px; }
|
.uploading-mini .el-icon { font-size: 14px; }
|
||||||
|
|
||||||
.user-info-popover { display: flex; flex-direction: column; gap: 8px; padding: 8px 0; }
|
.user-info-popover { display: flex; flex-direction: column; gap: 8px; padding: 4px 0; }
|
||||||
.user-info-row { display: flex; gap: 8px; font-size: 12px; line-height: 1.5; }
|
.user-info-row { display: flex; align-items: flex-start; gap: 8px; font-size: 12px; line-height: 1.5; }
|
||||||
.user-info-label { font-weight: 500; min-width: 50px; color: #303133; }
|
.user-info-label { font-weight: 500; min-width: 50px; color: #303133; flex-shrink: 0; padding-top: 6px; }
|
||||||
.user-info-value { color: #606266; word-break: break-all; flex: 1; }
|
.user-info-value { color: #606266; word-break: break-all; flex: 1; }
|
||||||
.signature-row { flex-direction: column; align-items: flex-start; }
|
.user-info-signature-box {
|
||||||
.user-info-signature { color: #606266; font-size: 12px; line-height: 1.4; word-break: break-all; margin-top: 4px; }
|
flex: 1;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 80px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #909399; gap: 12px; }
|
.chat-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #909399; gap: 12px; }
|
||||||
.chat-empty p { margin: 0; font-size: 14px; }
|
.chat-empty p { margin: 0; font-size: 14px; }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<el-upload
|
<el-upload
|
||||||
drag
|
drag
|
||||||
multiple
|
multiple
|
||||||
:limit="11"
|
:limit="31"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:on-change="handleChange"
|
:on-change="handleChange"
|
||||||
:on-exceed="handleExceed"
|
:on-exceed="handleExceed"
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
>
|
>
|
||||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||||
<div class="el-upload__text">拖拽文件到此处,或<em>点击选择</em>(最多10个)</div>
|
<div class="el-upload__text">拖拽文件到此处,或<em>点击选择</em>(最多30个)</div>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
|
||||||
<div class="storage-info">
|
<div class="storage-info">
|
||||||
@@ -98,16 +98,16 @@ const isOverSizeLimit = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleChange = (file, list) => {
|
const handleChange = (file, list) => {
|
||||||
if (list.length > 10) {
|
if (list.length > 30) {
|
||||||
ElMessage.warning('最多一次上传10个文件')
|
ElMessage.warning('最多一次上传30个文件')
|
||||||
fileList.value = list.slice(0, 10)
|
fileList.value = list.slice(0, 30)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileList.value = list
|
fileList.value = list
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExceed = () => {
|
const handleExceed = () => {
|
||||||
ElMessage.warning('最多一次上传10个文件')
|
ElMessage.warning('最多一次上传30个文件')
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeFile = (index) => {
|
const removeFile = (index) => {
|
||||||
|
|||||||
@@ -19,48 +19,90 @@
|
|||||||
<template #prefix><el-icon><Lock /></el-icon></template>
|
<template #prefix><el-icon><Lock /></el-icon></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item prop="captcha">
|
||||||
|
<div class="captcha-row">
|
||||||
|
<el-input
|
||||||
|
v-model="form.captcha"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
>
|
||||||
|
<template #prefix><el-icon><Key /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<img
|
||||||
|
:src="captchaImage"
|
||||||
|
class="captcha-img"
|
||||||
|
@click="refreshCaptcha"
|
||||||
|
title="点击刷新验证码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
:disabled="!isFormComplete"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-icon><Right /></el-icon>
|
<template v-if="loading">
|
||||||
<span style="margin-left: 4px">登录</span>
|
<span>登录中...</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-icon><Right /></el-icon>
|
||||||
|
<span style="margin-left: 4px">登录</span>
|
||||||
|
</template>
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { User, Lock, Right } from '@element-plus/icons-vue'
|
import { User, Lock, Right, Key } from '@element-plus/icons-vue'
|
||||||
import { login } from '@/api/auth'
|
import { login, getCaptcha } from '@/api/auth'
|
||||||
|
|
||||||
const emit = defineEmits(['success'])
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const captchaImage = ref('')
|
||||||
|
|
||||||
const form = reactive({ username: '', password: '' })
|
const form = reactive({ username: '', password: '', captcha: '' })
|
||||||
|
|
||||||
|
// 表单是否填写完整
|
||||||
|
const isFormComplete = computed(() => {
|
||||||
|
return form.username.trim() && form.password.trim() && form.captcha.trim()
|
||||||
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
||||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||||
|
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshCaptcha = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getCaptcha()
|
||||||
|
captchaImage.value = res.data.image
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取验证码失败', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
const valid = await formRef.value.validate().catch(() => false)
|
if (!isFormComplete.value || loading.value) return
|
||||||
if (!valid || loading.value) return
|
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await login(form)
|
const res = await login(form)
|
||||||
// res.data = { token, user }
|
ElMessage.success('登录成功,正在跳转...')
|
||||||
emit('success', res.data)
|
emit('success', res.data)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// 登录失败刷新验证码
|
||||||
|
refreshCaptcha()
|
||||||
|
form.captcha = ''
|
||||||
if (!e.response) {
|
if (!e.response) {
|
||||||
ElMessage.error('后端服务不可用,请稍后重试')
|
ElMessage.error('后端服务不可用,请稍后重试')
|
||||||
} else if (e.response.status >= 500) {
|
} else if (e.response.status >= 500) {
|
||||||
@@ -72,13 +114,36 @@ const handleLogin = async () => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshCaptcha()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.form-tips {
|
.captcha-row {
|
||||||
text-align: center;
|
display: flex;
|
||||||
color: #c0c4cc;
|
gap: 12px;
|
||||||
font-size: 12px;
|
width: 100%;
|
||||||
margin-top: -8px;
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-row .el-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-img {
|
||||||
|
width: 140px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
object-fit: fill;
|
||||||
|
background: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-img:hover {
|
||||||
|
border-color: #409eff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user