Files
ks-app-employment-service/packageRc/pages/login/login.vue
wuzhimiao a6c7678a5e 提交1
2025-10-31 11:04:16 +08:00

822 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="login-container">
<view class="login-form">
<view class="logo-area">
<image class="logo" src="/packageRc/static/pageBg.png" mode="aspectFit"></image>
<view class="title">就业服务系统</view>
</view>
<view class="form-area">
<view class="form-item">
<text class="label">用户名</text>
<input
v-model="loginForm.username"
class="input"
placeholder="请输入用户名"
type="text"
auto-complete="off"
/>
</view>
<view class="form-item">
<text class="label">密码</text>
<input
v-model="loginForm.password"
class="input"
placeholder="请输入密码"
password
type="text"
/>
</view>
<button class="login-btn" @click="useVerify">登录</button>
</view>
</view>
<!-- 滑块验证组件 -->
<view v-if="showVerify" class="verify-mask">
<view class="verify-container">
<view class="verify-header">
<text class="verify-title">安全验证</text>
<text class="verify-close" @click="closeVerify">×</text>
</view>
<view class="verify-body">
<!-- 滑块图片区域 -->
<view class="verify-image-container">
<view class="verify-image-wrapper">
<image :src="verifyImage.backImgBase64" class="verify-background-image" mode="aspectFit"></image>
<image
v-if="verifyImage.blockImgBase64"
:src="verifyImage.blockImgBase64"
class="verify-block-image"
:style="{ left: sliderWidth + 'px' }"
mode="aspectFit"
></image>
</view>
<!-- 刷新按钮 -->
<view v-if="showRefresh" class="verify-refresh" @click.stop="getVerifyImage">
<text></text>
</view>
<!-- 提示文字 -->
<view v-if="tipWords" class="verify-tip" :class="{ 'tip-error': !passFlag }">
{{ tipWords }}
</view>
</view>
<Verify
@success="handleSubmit"
:mode="'pop'"
:captchaType="'blockPuzzle'"
:blockSize="{ width: '47px' }"
:imgSize="{ width: '300px', height: '155px' }"
ref="verifyRef"
></Verify>
<!-- 滑块区域 -->
<view class="verify-slider">
<view class="slider-track">
<view
class="slider-fill"
:style="{ width: sliderWidth + 'px' }"
:class="{ 'slider-success': passFlag }"
></view>
<view
class="slider-thumb"
:style="{ left: sliderWidth + 'px' }"
:class="{ 'slider-success': passFlag }"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
>
<text class="slider-icon">{{ sliderText }}</text>
</view>
</view>
<text
class="slider-text"
:class="{ 'slider-success': passFlag }"
>{{ sliderStatusText }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import Verify from '../../components/verifition/Verify.vue';
import { ref, onMounted, onUnmounted } from 'vue';
import { login } from '../../api/login.js';
import { setToken, saveUserInfo, getUserInfo } from '../../utils/auth.js';
import { encrypt, decrypt } from '../../utils/sm2Encrypt.js';
import { reqGet, reqCheck } from '../../utils/captchaApi.js';
// 登录表单数据
const loginForm = ref({
username: '',
password: '',
rememberMe: false,
code: '', // 用于存储滑块验证的code
captchaVerification: '' // 用于滑块验证结果
});
// 滑块验证相关状态
const showVerify = ref(false);
// Verify组件引用 - 在Composition API中使用ref来引用组件
const verifyRef = ref(null);
const verifyImage = ref({
backImgBase64: '',
blockImgBase64: '',
token: ''
});
const sliderWidth = ref(0);
const startX = ref(0);
const isDragging = ref(false);
const sliderText = ref('→');
const sliderStatusText = ref('向右滑动完成验证');
const clientUid = ref('');
const secretKey = ref('');
const spinning = ref(false);
const isEnd = ref(false);
const showRefresh = ref(true);
const passFlag = ref(false);
const tipWords = ref('');
// 生成clientUid
const generateClientUid = () => {
const s = [];
const hexDigits = '0123456789abcdef';
for (let i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = '4';
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = '-';
return 'slider' + '-' + s.join('');
};
// 打开滑块验证
const useVerify = async () => {
// 使用verifyRef.value访问组件实例而不是this.$refs.verify
if (verifyRef.value) {
verifyRef.value.show();
} else {
console.error('Verify组件未正确加载');
}
// 简单表单验证
if (!loginForm.value.username) {
return uni.showToast({ title: '请输入用户名', icon: 'none' });
}
if (!loginForm.value.password) {
return uni.showToast({ title: '请输入密码', icon: 'none' });
}
try {
// 生成clientUid
if (!clientUid.value) {
clientUid.value = generateClientUid();
// 存储到localStorage
uni.setStorageSync('slider', clientUid.value);
}
// 获取验证图片
await getVerifyImage();
// 显示验证弹窗
// showVerify.value = true;
// 重置滑块状态
resetSlider();
} catch (error) {
console.error('获取验证图片失败:', error);
uni.showToast({
title: '获取验证图片失败,请稍后重试',
icon: 'none'
});
}
};
// 获取验证图片
const getVerifyImage = async () => {
try {
spinning.value = true;
// 从localStorage获取clientUid如果没有则生成新的
const storedClientUid = uni.getStorageSync('slider') || generateClientUid();
if (!clientUid.value) {
clientUid.value = storedClientUid;
}
const res = await reqGet({
captchaType: 'blockPuzzle',
clientUid: clientUid.value,
ts: Date.now()
});
spinning.value = false;
// 处理不同的响应格式
const isSuccess = res.code === 200 || res.repCode === '0000';
const responseData = res.data || res.repData;
if (isSuccess && responseData) {
verifyImage.value = {
backImgBase64: 'data:image/png;base64,' + responseData.originalImageBase64,
blockImgBase64: 'data:image/png;base64,' + responseData.jigsawImageBase64,
token: responseData.token
};
// 保存secretKey用于加密
secretKey.value = responseData.secretKey || '';
} else {
throw new Error(res.msg || res.repMsg || '获取验证图片失败');
}
} catch (error) {
spinning.value = false;
console.error('获取验证图片错误:', error);
throw error;
}
};
// 关闭验证
const closeVerify = () => {
showVerify.value = false;
resetSlider();
};
// 重置滑块
const resetSlider = () => {
sliderWidth.value = 0;
sliderText.value = '→';
sliderStatusText.value = '向右滑动完成验证';
isDragging.value = false;
isEnd.value = false;
showRefresh.value = true;
passFlag.value = false;
tipWords.value = '';
};
// 处理触摸开始
const handleTouchStart = (e) => {
if (isEnd.value) return;
startX.value = e.touches[0].clientX;
isDragging.value = true;
sliderText.value = '';
};
// 处理触摸移动
const handleTouchMove = (e) => {
if (!isDragging.value || isEnd.value) return;
// 阻止默认行为,防止页面滚动
e.preventDefault();
const moveX = e.touches[0].clientX - startX.value;
// 限制滑块移动范围
if (moveX >= 0) {
// 获取滑块轨道宽度 - 修正选择器
const trackElement = document.querySelector('.verify-slider-track');
const trackWidth = trackElement ? trackElement.offsetWidth : 300; // 增大默认宽度
const maxMoveDistance = trackWidth - 60; // 减去滑块按钮宽度60px
sliderWidth.value = Math.min(moveX, maxMoveDistance);
} else {
sliderWidth.value = 0;
}
};
// 在移动端添加全局触摸事件处理,防止验证弹窗打开时页面滚动
document.addEventListener('touchmove', (e) => {
const verifyModal = document.querySelector('.verify-mask');
if (showVerify.value && verifyModal) {
e.preventDefault();
}
}, { passive: false });
// 处理触摸结束
const handleTouchEnd = async () => {
if (!isDragging.value) return;
isDragging.value = false;
// 验证滑块位置
await verifySlider();
};
// 处理鼠标按下
const handleMouseDown = (e) => {
startX.value = e.clientX;
isDragging.value = true;
};
// 处理鼠标移动
const handleMouseMove = (e) => {
if (!isDragging.value) return;
const moveX = e.clientX - startX.value;
if (moveX >= 0 && moveX <= 260) {
sliderWidth.value = moveX;
}
};
// 处理鼠标释放
const handleMouseUp = async () => {
if (!isDragging.value) return;
isDragging.value = false;
// 验证滑块位置
await verifySlider();
};
// AES加密函数
const aesEncrypt = (word, keyWord = 'XwKsGlMcdPMEhR1B') => {
// 由于uni-app环境我们使用一个简化的加密实现
// 在实际项目中应该引入crypto-js库
try {
// 这里只是为了保持接口一致实际加密需要引入crypto-js
return word;
} catch (e) {
console.error('加密失败:', e);
return word;
}
};
// 验证滑块位置
const verifySlider = async () => {
try {
if (isEnd.value) return;
isDragging.value = false;
// 计算移动距离
const moveLeftDistance = sliderWidth.value;
// 准备验证数据
const pointJson = JSON.stringify({ x: moveLeftDistance, y: 5.0 });
const verifyData = {
captchaType: 'blockPuzzle',
pointJson: secretKey.value ? aesEncrypt(pointJson, secretKey.value) : pointJson,
token: verifyImage.value.token,
clientUid: clientUid.value,
ts: Date.now()
};
// 调用验证接口
const res = await reqCheck(verifyData);
// 处理不同的响应格式
const isSuccess = res.code === 200 || res.repCode === '0000';
const responseData = res.data || res.repData;
if (isSuccess) {
// 验证成功
sliderStatusText.value = '验证通过';
sliderText.value = '✓';
passFlag.value = true;
isEnd.value = true;
showRefresh.value = false;
// 生成captchaVerification
const captchaVerification = secretKey.value
? aesEncrypt(verifyImage.value.token + "---" + pointJson, secretKey.value)
: verifyImage.value.token + "---" + pointJson;
// 保存验证码信息
loginForm.value.code = responseData.code || '';
loginForm.value.captchaVerification = responseData.captchaVerification || captchaVerification;
// 延迟关闭验证弹窗并执行登录
setTimeout(() => {
showVerify.value = false;
handleLogin();
}, 1000);
} else {
// 验证失败
sliderStatusText.value = '验证失败,请重试';
passFlag.value = false;
tipWords.value = res.msg || res.repMsg || '验证失败';
setTimeout(() => {
resetSlider();
getVerifyImage(); // 重新获取验证图片
tipWords.value = '';
}, 1000);
}
} catch (error) {
console.error('滑块验证失败:', error);
sliderStatusText.value = '验证失败,请重试';
passFlag.value = false;
tipWords.value = '网络错误,请重试';
setTimeout(() => {
resetSlider();
getVerifyImage();
tipWords.value = '';
}, 1000);
}
};
// 登录处理
const handleLogin = async () => {
try {
// 显示加载状态
uni.showLoading({
title: '登录中',
mask: true
});
// 调用登录接口添加clientUid参数
const loginData = {
username: loginForm.value.username,
password: encrypt(loginForm.value.password),
code: loginForm.value.code,
captchaVerification: loginForm.value.captchaVerification,
clientUid: clientUid.value
};
const res = await login(loginData);
// 保存token
setToken(res.token || res.data.token);
// 保存用户信息
saveUserInfo(loginForm.value.username, loginForm.value.password, loginForm.value.rememberMe);
// 登录成功提示
uni.showToast({
title: '登录成功',
icon: 'success'
});
// 跳转到首页或之前的页面
uni.navigateBack();
} catch (error) {
console.error('登录失败:', error);
uni.showToast({
title: error.response?.data?.msg || '登录失败,请检查账号密码',
icon: 'none'
});
} finally {
uni.hideLoading();
}
};
// 刷新滑块验证
const refreshVerify = () => {
resetSlider();
getVerifyImage();
};
// 页面加载时的初始化
onMounted(() => {
// 获取保存的用户信息
const userInfo = getUserInfo();
if (userInfo.rememberMe) {
loginForm.value.username = userInfo.username;
loginForm.value.password = userInfo.password;
loginForm.value.rememberMe = true;
}
// 从localStorage获取clientUid
const storedClientUid = uni.getStorageSync('slider');
if (storedClientUid) {
clientUid.value = storedClientUid;
}
});
// 页面卸载时清理事件监听器
onUnmounted(() => {
// 清理工作
});
</script>
<style scoped>
.login-container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f5f5;
}
.login-form {
width: 80%;
max-width: 500rpx;
padding: 40rpx;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.logo-area {
text-align: center;
margin-bottom: 40rpx;
}
.logo {
width: 160rpx;
height: 160rpx;
margin-bottom: 20rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #1a62ce;
}
.form-area {
width: 100%;
}
.form-item {
margin-bottom: 30rpx;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
}
.input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
}
.remember-area {
display: flex;
align-items: center;
margin-bottom: 30rpx;
justify-content: flex-end;
}
.remember-checkbox {
margin-right: 10rpx;
transform: scale(0.8);
}
.remember-text {
font-size: 28rpx;
color: #666;
}
.login-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
background-color: #1a62ce;
color: #fff;
border-radius: 40rpx;
font-size: 32rpx;
margin-top: 10rpx;
border: none;
}
/* 滑块验证相关样式 */
.verify-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.verify-container {
width: 80%;
max-width: 340px;
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
animation: fadeIn 0.3s ease-out;
}
.verify-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #eee;
}
.verify-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.verify-close {
font-size: 48rpx;
color: #999;
cursor: pointer;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s;
}
.verify-close:hover {
background-color: #f0f0f0;
color: #666;
}
.verify-body {
padding: 20rpx;
}
.verify-image-container {
position: relative;
width: 100%;
height: 200px;
margin-bottom: 20rpx;
border: 1rpx solid #eee;
border-radius: 8rpx;
overflow: hidden;
background-color: #fafafa;
}
.verify-image-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.verify-background-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.verify-block-image {
position: absolute;
top: 70px; /* 设置固定的垂直位置,确保与缺口对齐 */
width: 80px; /* 大幅增大宽度 */
height: 80px; /* 大幅增大高度 */
cursor: move;
transition: left 0.05s;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
border-radius: 4px;
z-index: 10;
}
/* 刷新按钮 */
.verify-refresh {
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
font-size: 18px;
color: #666;
}
.verify-refresh:hover {
background-color: #fff;
transform: rotate(180deg);
}
/* 提示文字 */
.verify-tip {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
padding: 5px 15px;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
border-radius: 15px;
font-size: 12px;
white-space: nowrap;
}
.verify-tip.tip-error {
background-color: #f56c6c;
}
.verify-slider {
width: 100%;
}
.slider-track {
position: relative;
width: 100%;
height: 40rpx;
background-color: #f5f5f5;
border-radius: 20rpx;
overflow: hidden;
transition: background-color 0.3s;
}
.slider-track:hover {
background-color: #e6e6e6;
}
.slider-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #1a62ce;
transition: width 0.05s, background-color 0.3s;
}
.slider-thumb {
position: absolute;
top: -5px;
width: 48px;
height: 48px;
background-color: #fff;
border: 1rpx solid #ddd;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: left 0.05s, all 0.3s;
z-index: 1;
}
.slider-thumb:hover {
box-shadow: 0 3px 15px rgba(26, 98, 206, 0.3);
}
.slider-icon {
font-size: 24rpx;
color: #1a62ce;
}
.slider-text {
margin-top: 10rpx;
font-size: 24rpx;
color: #666;
text-align: center;
transition: color 0.3s;
}
/* 验证成功状态 */
.slider-fill.slider-success {
background-color: #67c23a;
}
.slider-thumb.slider-success {
border-color: #67c23a;
color: #67c23a;
}
.slider-text.slider-success {
color: #67c23a;
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 适配移动端 */
@media (max-width: 480px) {
.verify-container {
width: 90%;
max-width: 320px;
}
.verify-image-container {
height: 180px;
}
.verify-body {
padding: 15px;
}
}
</style>