732 lines
19 KiB
Vue
732 lines
19 KiB
Vue
<template>
|
||
<view class="login-container">
|
||
<view class="login-header">
|
||
<view class="logo-container">
|
||
<image
|
||
class="logo-icon"
|
||
src="/static/common/images/images_logo.png"
|
||
></image>
|
||
<text class="logo-text"
|
||
>我是 <text class="logo-name">源小新</text></text
|
||
>
|
||
</view>
|
||
<view class="welcome-text">
|
||
欢迎使用源小新校园AI助手,快让我来帮助你吧!
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
class="login-form"
|
||
:style="{ 'padding-top': isTeacher ? '40rpx' : '80rpx' }"
|
||
>
|
||
<view class="login-type" v-if="isTeacher">
|
||
<view
|
||
class="type-item"
|
||
:class="{ active: loginType === 'psd' }"
|
||
@click="switchLoginType('psd')"
|
||
>密码登录</view
|
||
>
|
||
<view class="divider">|</view>
|
||
<view
|
||
class="type-item"
|
||
:class="{ active: loginType === 'code' }"
|
||
@click="switchLoginType('code')"
|
||
>验证码登录</view
|
||
>
|
||
</view>
|
||
|
||
<view class="form-content">
|
||
<view class="form-item">
|
||
<view class="form-label"
|
||
><image
|
||
class="image_icon"
|
||
src="/static/common/images/icon_phoneNumber.png"
|
||
mode="scaleToFill"
|
||
/>
|
||
<text>手机号</text>
|
||
</view>
|
||
<view class="input-wrapper">
|
||
<input
|
||
type="number"
|
||
class="form-input"
|
||
placeholder="请输入手机号"
|
||
v-model="loginParams.phone"
|
||
@blur="validatePhone"
|
||
maxlength="11"
|
||
/>
|
||
</view>
|
||
<text v-if="errors.phone" class="error-tip">{{ errors.phone }}</text>
|
||
</view>
|
||
|
||
<view class="form-item" v-if="isTeacher && loginType === 'psd'">
|
||
<view class="form-label"
|
||
><image
|
||
class="image_icon"
|
||
src="/static/common/images/icon_password.png"
|
||
mode="scaleToFill"
|
||
/>
|
||
<text>密码</text>
|
||
</view>
|
||
<view class="input-wrapper">
|
||
<input
|
||
type="text"
|
||
class="form-input"
|
||
placeholder="请输入密码"
|
||
v-model="loginParams.password"
|
||
@blur="validatePassword"
|
||
/>
|
||
</view>
|
||
<text v-if="errors.password" class="error-tip">{{
|
||
errors.password
|
||
}}</text>
|
||
</view>
|
||
|
||
<view class="form-item" v-if="loginType === 'code'">
|
||
<view class="form-label">
|
||
<image
|
||
class="image_icon"
|
||
src="/static/common/images/icon_verificationCode.png"
|
||
mode="scaleToFill"
|
||
/>
|
||
<text>图形验证码</text>
|
||
</view>
|
||
<view class="input-wrapper">
|
||
<input
|
||
type="text"
|
||
class="form-input"
|
||
placeholder="请输入验证码"
|
||
v-model="loginParams.captcha"
|
||
@blur="validateCaptcha"
|
||
/>
|
||
<view class="captcha-img" @click="refreshCaptcha">
|
||
<image
|
||
v-if="captchaUrl"
|
||
:src="captchaUrl"
|
||
mode="widthFix"
|
||
></image>
|
||
</view>
|
||
</view>
|
||
<text v-if="errors.captcha" class="error-tip">{{
|
||
errors.captcha
|
||
}}</text>
|
||
</view>
|
||
|
||
<view class="form-item" v-if="loginType === 'code'">
|
||
<view class="form-label">
|
||
<image
|
||
class="image_icon"
|
||
src="/static/common/images/icon_verificationCode.png"
|
||
mode="scaleToFill"
|
||
/>
|
||
<text>验证码</text>
|
||
</view>
|
||
<view class="input-wrapper">
|
||
<input
|
||
type="text"
|
||
class="form-input"
|
||
placeholder="请输入验证码"
|
||
v-model="loginParams.code"
|
||
@blur="validateCode"
|
||
/>
|
||
<text
|
||
class="get-code-btn"
|
||
:class="{ 'get-code-btn-normal': codeText == '获取验证码' }"
|
||
@click="getCode"
|
||
>{{ codeText }}</text
|
||
>
|
||
</view>
|
||
<text v-if="errors.code" class="error-tip">{{ errors.code }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="button-container">
|
||
<button class="login-button" @click="login">登录</button>
|
||
</view>
|
||
</view>
|
||
|
||
<u-toast ref="uToast" />
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { generateSign } from "@/utils/signUtil.js";
|
||
import { getUserInfoFromJWT } from "@/utils/jwt-util";
|
||
import md5 from "js-md5";
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
isTeacher: false, // 是否是教师
|
||
|
||
// 登录参数
|
||
loginParams: {
|
||
phone: "",
|
||
password: "",
|
||
code: "",
|
||
captcha: "", // 图形验证码
|
||
captchaId: "", // 图形验证码ID
|
||
},
|
||
captchaUrl: "", // 图形验证码URL,实际使用时需替换
|
||
loginType: "psd", // 教师默认密码登录
|
||
codeText: "获取验证码",
|
||
countdown: 60,
|
||
timer: null,
|
||
errors: {
|
||
phone: "",
|
||
password: "",
|
||
code: "",
|
||
captcha: "",
|
||
},
|
||
};
|
||
},
|
||
onLoad(e) {
|
||
// console.log("onLoad", e.type); // 0:学生 1:教师
|
||
this.isTeacher = e.type == 1;
|
||
this.loginType = e.type == 1 ? "psd" : "code";
|
||
},
|
||
mounted() {
|
||
this.refreshCaptcha();
|
||
},
|
||
beforeDestroy() {
|
||
if (this.timer) {
|
||
clearInterval(this.timer);
|
||
}
|
||
},
|
||
methods: {
|
||
switchLoginType(type) {
|
||
this.clearErrors();
|
||
this.clearLoginParams();
|
||
this.loginType = type;
|
||
if (this.loginType === "code") {
|
||
this.refreshCaptcha();
|
||
}
|
||
},
|
||
|
||
// 刷新图形验证码
|
||
refreshCaptcha() {
|
||
// 获取图形验证码
|
||
this.$u.api.GetCaptcha().then((res) => {
|
||
console.log("获取图形验证码...", res);
|
||
this.loginParams.captchaId = res.captchaId;
|
||
this.captchaUrl = "data:image/png;base64," + res.imageBase64;
|
||
});
|
||
},
|
||
|
||
// 获取短信验证码
|
||
getCode() {
|
||
if (
|
||
this.codeText !== "获取验证码" ||
|
||
!this.validatePhone() ||
|
||
(this.loginType === "code" && !this.validateCaptcha())
|
||
) {
|
||
return;
|
||
}
|
||
|
||
const sign = generateSign(this.loginParams.phone);
|
||
|
||
const params = {
|
||
phone: this.loginParams.phone,
|
||
ip: "",
|
||
sign: sign,
|
||
captchaId: this.loginParams.captchaId,
|
||
captchaCode: this.loginParams.captcha,
|
||
};
|
||
|
||
console.log("获取验证码...", params);
|
||
|
||
const apiMethod = this.isTeacher
|
||
? "GetTeacherVerifyCode"
|
||
: "GetStuVerifyCode";
|
||
|
||
this.$u.api[apiMethod](params).then((res) => {
|
||
console.log("获取验证码...", res);
|
||
if (res.succeed) {
|
||
this.$refs.uToast.show({
|
||
title: "发送成功",
|
||
type: "success",
|
||
});
|
||
this.startCountdown();
|
||
} else {
|
||
this.$refs.uToast.show({
|
||
title: res.error || "发送失败",
|
||
type: "error",
|
||
});
|
||
this.refreshCaptcha();
|
||
}
|
||
});
|
||
},
|
||
|
||
// 开始倒计时
|
||
startCountdown() {
|
||
this.countdown = 60;
|
||
this.codeText = `${this.countdown}秒后重试`;
|
||
this.timer = setInterval(() => {
|
||
this.countdown--;
|
||
this.codeText = `${this.countdown}秒后重试`;
|
||
if (this.countdown <= 0) {
|
||
clearInterval(this.timer);
|
||
this.codeText = "获取验证码";
|
||
}
|
||
}, 1000);
|
||
},
|
||
|
||
// 校验手机号
|
||
validatePhone() {
|
||
if (!this.loginParams.phone) {
|
||
this.errors.phone = "请输入手机号";
|
||
return false;
|
||
}
|
||
|
||
if (!/^1[3-9]\d{9}$/.test(this.loginParams.phone)) {
|
||
this.errors.phone = "手机号格式不正确";
|
||
return false;
|
||
}
|
||
|
||
this.errors.phone = "";
|
||
return true;
|
||
},
|
||
|
||
// 校验密码
|
||
validatePassword() {
|
||
if (!this.loginParams.password) {
|
||
this.errors.password = "请输入密码";
|
||
return false;
|
||
}
|
||
|
||
// if (this.password.length < 6) {
|
||
// this.errors.password = "密码不能少于6位";
|
||
// return false;
|
||
// }
|
||
|
||
this.errors.password = "";
|
||
return true;
|
||
},
|
||
|
||
// 校验图形验证码
|
||
validateCaptcha() {
|
||
if (!this.loginParams.captcha) {
|
||
this.errors.captcha = "请输入图形验证码";
|
||
return false;
|
||
}
|
||
|
||
// if (this.captcha.length < 4) {
|
||
// this.errors.captcha = "图形验证码错误请重新输入";
|
||
// return false;
|
||
// }
|
||
|
||
// 实际应用中应该验证图形验证码是否正确
|
||
this.errors.captcha = "";
|
||
return true;
|
||
},
|
||
|
||
// 校验短信验证码
|
||
validateCode() {
|
||
if (!this.loginParams.code) {
|
||
this.errors.code = "请输入验证码";
|
||
return false;
|
||
}
|
||
|
||
if (this.loginParams.code.length < 4) {
|
||
this.errors.code = "验证码错误请重新输入";
|
||
return false;
|
||
}
|
||
|
||
this.errors.code = "";
|
||
return true;
|
||
},
|
||
|
||
// 清除所有错误提示
|
||
clearErrors() {
|
||
this.errors = {
|
||
phone: "",
|
||
password: "",
|
||
code: "",
|
||
captcha: "",
|
||
};
|
||
},
|
||
// 清除登录参数
|
||
clearLoginParams() {
|
||
this.loginParams = {
|
||
phone: "",
|
||
password: "",
|
||
code: "",
|
||
captcha: "", // 图形验证码
|
||
captchaId: "", // 图形验证码ID
|
||
};
|
||
},
|
||
|
||
// 登录
|
||
login() {
|
||
// const res = {
|
||
// data: {
|
||
// // token:
|
||
// // "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbiIsIlVzZXJJbmZvIjoie1wiSWRcIjpcImI1MjY5ZWQxLTViYzgtMTFmMC05YTg5LTAwMTYzZTJkMmRkM1wiLFwiTmFtZVwiOlwi6J-56ICB5biIXCIsXCJQaG9uZU51bWJlclwiOlwiMTM1ODg4ODg4ODhcIixcIlNleFwiOjEsXCJBY2NvdW50VHlwZVwiOjJ9IiwibmJmIjoxNzUyMTE4ODU0LCJleHAiOjE3NTIyMDUyNTR9.aF0Q-X3ebIld7RIKMMW68tXjmmR4OO08dXLAtHWuuwc",
|
||
// token:
|
||
// "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJVc2VyIiwiVXNlckluZm8iOiJ7XCJJZFwiOlwiNTljNzIxNmMtMTRhZS00NmRmLTkxYTItNGYzMjFlZjM2YjQ5XCIsXCJOYW1lXCI6XCIxNzMzNTM3NDg4N1wiLFwiUGhvbmVOdW1iZXJcIjpcIjE3MzM1Mzc0ODg3XCIsXCJTZXhcIjoyLFwiQWNjb3VudFR5cGVcIjowfSIsIm5iZiI6MTc1MjExMDcwOSwiZXhwIjoxNzUyMTk3MTA5fQ.VDv2KjGq3s3BbZxZjB3P-BaeDU_1vIdqFEWGGvdpIxU",
|
||
// },
|
||
// };
|
||
|
||
// const token = res.data.token;
|
||
// // 解析获取用户信息
|
||
// const userInfo = getUserInfoFromJWT(token);
|
||
// // 保存登录后得到的用户数据
|
||
// this.$u.vuex("vuex_token", token);
|
||
// this.$u.vuex("vuex_user", userInfo);
|
||
|
||
// // 跳转至首页
|
||
// uni.reLaunch({
|
||
// url: "/pages/home/index/index",
|
||
// });
|
||
// return;
|
||
|
||
// 校验手机号
|
||
if (!this.validatePhone()) {
|
||
return;
|
||
}
|
||
|
||
// 根据登录类型校验其他字段
|
||
if (this.loginType === "psd") {
|
||
if (!this.validatePassword()) {
|
||
return;
|
||
}
|
||
} else {
|
||
// 验证码登录需要验证图形验证码和短信验证码
|
||
if (!this.validateCaptcha() || !this.validateCode()) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 校验通过,执行登录
|
||
uni.showLoading({
|
||
title: "登录中...",
|
||
});
|
||
|
||
if (this.isTeacher) {
|
||
if (this.loginType === "psd") {
|
||
this.teacherLogin(); // 密码登录
|
||
} else {
|
||
this.teacherLoginByCode(); // 验证码登录
|
||
}
|
||
} else {
|
||
this.stuLogin(); // 学生登录
|
||
}
|
||
},
|
||
|
||
// 通用登录处理函数
|
||
handleLogin(apiMethod, params, userType = 0) {
|
||
// console.log("登录参数:", params);
|
||
|
||
this.$u.api[apiMethod](params).then((res) => {
|
||
console.log("登录结果:", res);
|
||
if (!res.succeed) {
|
||
this.$refs.uToast.show({
|
||
title: res.error || "登录失败",
|
||
type: "error",
|
||
});
|
||
return;
|
||
}
|
||
|
||
this.$refs.uToast.show({
|
||
title: "登录成功",
|
||
type: "success",
|
||
});
|
||
|
||
const token = res.data.token;
|
||
// 解析获取用户信息
|
||
const userInfo = getUserInfoFromJWT(token);
|
||
// 保存登录后得到的用户数据
|
||
this.$u.vuex("vuex_token", token);
|
||
this.$u.vuex("vuex_user", userInfo);
|
||
this.$u.vuex("vuex_userType", userType); // 0:学生 1:教师
|
||
|
||
// 保存用户类型到本地存储,用于退出登录时重定向
|
||
// uni.setStorageSync("userType", userType);
|
||
|
||
// 跳转至首页
|
||
const url = this.isTeacher
|
||
? "/pages/consultation/index"
|
||
: "/pages/home/index/index";
|
||
uni.reLaunch({
|
||
url: url,
|
||
});
|
||
});
|
||
},
|
||
|
||
// 学生登录
|
||
stuLogin() {
|
||
// 构建登录参数
|
||
const params = {
|
||
phone: this.loginParams.phone,
|
||
password: "",
|
||
code: this.loginParams.code,
|
||
};
|
||
|
||
// 使用通用登录处理函数,传入学生特定的参数
|
||
this.handleLogin("StuLogin", params, 0);
|
||
},
|
||
|
||
// 教师密码登录
|
||
teacherLogin() {
|
||
// 构建登录参数
|
||
const params = {
|
||
phone: this.loginParams.phone,
|
||
password: md5(this.loginParams.password),
|
||
code: this.loginParams.code,
|
||
};
|
||
|
||
this.handleLogin("TeacherLogin", params, 1);
|
||
},
|
||
|
||
// 教师验证码登录
|
||
teacherLoginByCode() {
|
||
// 构建登录参数
|
||
const params = {
|
||
phone: this.loginParams.phone,
|
||
password: "",
|
||
code: this.loginParams.code,
|
||
};
|
||
|
||
this.handleLogin("TeacherLoginByCode", params, 1);
|
||
},
|
||
|
||
// 手机号输入过滤
|
||
phoneInputFilter(e) {
|
||
// 确保输入的只是数字
|
||
let value = e.detail.value.replace(/\D/g, "");
|
||
// 限制长度为11位
|
||
if (value.length > 11) {
|
||
value = value.slice(0, 11);
|
||
}
|
||
// 直接更新值
|
||
this.loginParams.phone = value;
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.login-container {
|
||
// min-height: 100vh;
|
||
height: 100vh;
|
||
width: 100%;
|
||
padding: 0 30rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
|
||
background-image: url("@/static/common/images/images_bg.png");
|
||
background-repeat: no-repeat;
|
||
background-size: 100% 100%;
|
||
background-position: 50%;
|
||
|
||
.login-header {
|
||
margin-top: 120rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
|
||
.logo-container {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 32rpx;
|
||
|
||
.logo-icon {
|
||
width: 48rpx;
|
||
height: 38rpx;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.logo-text {
|
||
font-size: 32rpx;
|
||
color: #333;
|
||
|
||
.logo-name {
|
||
font-family: DouyinSans;
|
||
font-weight: bold;
|
||
font-size: 48rpx;
|
||
color: #3e6aff;
|
||
line-height: 24rpx;
|
||
margin-left: 16rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.welcome-text {
|
||
font-family: DouyinSans;
|
||
font-weight: bold;
|
||
font-size: 27rpx;
|
||
color: #5a5f76;
|
||
margin-bottom: 60rpx;
|
||
}
|
||
}
|
||
|
||
.login-form {
|
||
background: linear-gradient(0deg, #f6f6f6 0%, #ffffff 100%);
|
||
flex: 1;
|
||
border-radius: 40rpx 40rpx 0 0;
|
||
padding: 80rpx 40rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.login-type {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
margin-bottom: 60rpx;
|
||
font-size: 28rpx;
|
||
color: #999999;
|
||
|
||
.type-item {
|
||
padding: 0 20rpx;
|
||
position: relative;
|
||
font-family: PingFang SC;
|
||
|
||
&.active {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #4f6aff;
|
||
|
||
&::after {
|
||
content: "";
|
||
position: absolute;
|
||
bottom: 4px;
|
||
left: 50%;
|
||
-webkit-transform: translateX(-50%);
|
||
transform: translateX(-50%);
|
||
width: 76%;
|
||
height: 0px;
|
||
border-radius: 3px;
|
||
box-shadow: 0px 0px 4px 2px rgba(79, 106, 255, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
.divider {
|
||
color: #5a5f76;
|
||
margin: 0 30rpx;
|
||
font-weight: 300;
|
||
}
|
||
}
|
||
|
||
.form-content {
|
||
.form-item {
|
||
margin-bottom: 20rpx;
|
||
position: relative;
|
||
padding-bottom: 40rpx; /* 为错误提示预留空间 */
|
||
|
||
.error-tip {
|
||
position: absolute;
|
||
bottom: 0;
|
||
font-size: 24rpx;
|
||
color: #ff6481;
|
||
height: 40rpx; /* 固定高度 */
|
||
line-height: 40rpx;
|
||
}
|
||
|
||
.form-label {
|
||
display: flex;
|
||
align-items: center;
|
||
font-family: PingFang SC;
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.image_icon {
|
||
width: 28rpx;
|
||
height: 28rpx;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
&:nth-child(2) {
|
||
.form-label::before {
|
||
border-radius: 50%;
|
||
}
|
||
}
|
||
|
||
.input-wrapper {
|
||
position: relative;
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
height: 90rpx;
|
||
background-color: #f2f2f2;
|
||
border-radius: 20rpx;
|
||
font-size: 28rpx;
|
||
padding: 0 32rpx;
|
||
box-sizing: border-box;
|
||
|
||
.uni-input-placeholder {
|
||
color: #cccccc;
|
||
}
|
||
}
|
||
|
||
.get-code-btn {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 220rpx;
|
||
height: 60rpx;
|
||
line-height: 60rpx;
|
||
padding-left: 30rpx;
|
||
color: #cccccc;
|
||
font-size: 28rpx;
|
||
border-left: 1px solid #b7c1ff;
|
||
}
|
||
|
||
.get-code-btn-normal {
|
||
color: #4370fe;
|
||
}
|
||
|
||
.captcha-img {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
height: 60rpx;
|
||
width: 220rpx;
|
||
padding-left: 30rpx;
|
||
border-left: 1px solid #b7c1ff;
|
||
|
||
image {
|
||
width: 80%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.button-container {
|
||
margin-top: 60rpx;
|
||
|
||
.login-button {
|
||
width: 100%;
|
||
height: 90rpx;
|
||
background-color: #4f6aff;
|
||
color: #ffffff;
|
||
font-size: 32rpx;
|
||
border-radius: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 响应式布局 - PC端样式 */
|
||
@media screen and (min-width: 768px) {
|
||
.login-container {
|
||
width: 100%;
|
||
margin: 0 auto;
|
||
.login-form {
|
||
width: 1200rpx;
|
||
margin: 0 auto;
|
||
}
|
||
}
|
||
}
|
||
</style>
|