sawAdmin/src/views/pages/login.vue

642 lines
20 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>
<div class="login-bg">
<div class="login-container">
<div class="login-header">
<img class="logo mr10" src="../../assets/img/logo.svg" alt="" />
<div class="login-title">统一登录平台</div>
</div>
<el-form :model="param" :rules="rules" ref="login" size="large">
<template v-if="login_type === 'pwd'">
<el-form-item prop="username">
<el-input v-model="param.username" placeholder="用户名或邮箱">
<template #prepend>
<el-icon>
<User />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" placeholder="密码" v-model="param.password">
<template #prepend>
<el-icon>
<Lock />
</el-icon>
</template>
</el-input>
</el-form-item>
<div class="pwd-tips">
<el-checkbox class="pwd-checkbox" v-model="checked" label="记住密码" />
<el-link type="primary" @click="$router.push('/reset-pwd')">忘记密码</el-link>
<el-link type="primary" @click="toggleLoginType">验证码登录</el-link>
</div>
</template>
<!-- 邮箱验证码登录表单(切换后显示) -->
<template v-else>
<!-- 邮箱输入 -->
<el-form-item prop="email">
<el-input v-model="code_login_form.email" placeholder="请输入邮箱">
<template #prepend>
<el-icon>
<Message />
</el-icon>
</template>
</el-input>
</el-form-item>
<!-- 验证码输入 + 发送按钮 -->
<el-form-item prop="code">
<el-input v-model="code_login_form.code" placeholder="请输入验证码">
<template #prepend>
<el-icon>
<VerificationCode />
</el-icon>
</template>
<template #append>
<el-button type="text" :disabled="countdown > 0" @click="sendCode" class="code-btn">
{{ countdown > 0 ? `${countdown}s后重发` : "发送验证码" }}
</el-button>
</template>
</el-input>
</el-form-item>
<!-- 切换链接(无记住密码) -->
<div class="pwd-tips">
<el-link type="primary" @click="toggleLoginType">密码登录</el-link>
</div>
</template>
<el-button class="login-btn" type="primary" size="large" @click="onLogin">登录</el-button>
<p class="login-text">
没有账号?<el-link type="primary" @click="$router.push('/register')">立即注册</el-link>
</p>
</el-form>
<div>
<!-- 第三登录 -->
<el-divider content-position="center">第三方登录</el-divider>
<div class="login-text">
<el-row :gutter="12">
<!-- thirdPartyLogoInfoList -->
<template v-for="(item, index) in thirdPartyLogoInfoList" :key="index">
<el-col :span="8">
<el-tooltip :content="item.label" placement="top">
<img :src="item.logo" alt="" @click="thirdLogin(item.name)" />
</el-tooltip>
</el-col>
</template>
</el-row>
</div>
</div>
</div>
<el-dialog v-model="second_auth_visible" title="二次认证" :close-on-click-modal="false" width="30%">
<!-- 输入验证码 -->
<span>请选择二次认证方式</span>
<el-select v-model="second_auth_method" placeholder="请选择认证方式" style="width: 100%; margin-top: 20px;">
<el-option v-for="item in second_auth_type_list" :key="item" :label="item.toUpperCase()"
:value="item"></el-option>
</el-select>
<el-row>
<span>输入验证码</span>
<el-input placeholder="请输入验证码" style="width: 100%; margin-top: 20px;" v-model="second_auth_code" v-if="second_auth_method == 'TOTP'">
<template #prepend>
<el-icon>
<Lock />
</el-icon>
</template>
</el-input>
<el-input v-model="code_login_form.code" placeholder="请输入验证码" v-if="second_auth_method == 'EMAIL'">
<template #prepend>
<el-icon><VerificationCode /></el-icon>
</template>
<template #append>
<el-button
type="text"
:disabled="countdown > 0"
@click="sendCode"
class="code-btn"
>
{{ countdown > 0 ? `${countdown}s后重发` : "发送验证码" }}
</el-button>
</template>
</el-input>
</el-row>
<el-row>
<el-button class="login-btn" type="primary" size="large" @click="secondAuthLogin">确认</el-button>
</el-row>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, inject, onMounted, h } from "vue";
import { useRouter, useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { loginService } from "@/api/user";
import { GetUserInfoService } from "@/api/user";
import { UserToken } from "@/types/user";
import { getBrowserFingerprint, getStoredFingerprint } from "@/utils/fingerprint";
import { usePermissStore } from "@/store/permiss";
import Cookies from 'js-cookie';
import { getThirdPartyUUID, getThirdPartyLoginStatus, getThirdPartyLoginUrl, sendLoginCode, loginByCode, secondAuthLoginService } from "@/api/user";
import { log, time } from "console";
import { pa } from "element-plus/es/locale";
import { state } from "mermaid/dist/rendering-util/rendering-elements/shapes/state";
// 从本地存储获取登录参数
const lgStr = localStorage.getItem("login-param");
const defParam = lgStr ? JSON.parse(lgStr) : null;
const globalData = inject("globalData");
const permiss = usePermissStore();
// 记住密码状态
const checked = ref(lgStr ? true : false);
const login_type = ref("pwd"); //登录方式 pwd:密码登录 qr:扫码登录
const countdown = ref(0); //倒计时
const router = useRouter();
const route = useRoute();
const second_auth_visible = ref(false); //二次认证弹窗
// 登录表单数据
const param = reactive({
username: defParam ? defParam.username : "",
password: defParam ? defParam.password : "",
fingerprint: '' // 浏览器指纹
});
//验证码登录表单数据
const code_login_form = ref({
email: '',
code: '',
login_type: 1, // 1: 邮箱验证码登录 2: 手机验证码登录
fingerprint: '', // 浏览器指纹
state: '' // 二次认证状态key
});
// 表单验证规则
const rules = {
username: [
{
required: true,
message: "请输入用户名",
trigger: "blur",
},
],
password: [
{
required: true,
message: "请输入密码",
trigger: "blur",
},
],
};
// 表单引用
const login = ref(null);
const maxLoginRepeatRequest = 60; //最大请求次数
const currentLoginRequest = ref(0); //当前请求次数
const timer = ref(null); //定时器
const secondAuthLogin = async () => {
if (!second_auth_method.value) {
ElMessage.error("请选择认证方式");
return;
}
second_auth_code.value = second_auth_code.value ? second_auth_code.value.trim() : code_login_form.value.code;
if (!second_auth_code.value) {
ElMessage.error("请输入验证码");
return;
}
let req = {
state: second_auth_state_key.value,
type: second_auth_method.value,
code: second_auth_code.value,
host_id: param.fingerprint
};
let result = await secondAuthLoginService(req);
if (result["code"] === 0) {
ElMessage.success("登录成功");
let userTokenInfo: UserToken = result["data"];
globalData["token"] = result.data;
localStorage.setItem("token", userTokenInfo.access_token);
localStorage.setItem("refresh_token", userTokenInfo.refresh_token);
localStorage.setItem("userId", userTokenInfo.user_id.toString());
localStorage.setItem("username", userTokenInfo.username);
let now = new Date();
localStorage.setItem("end_time", (now.setDate(now.getHours())).toString()); //过期时间
await getMyUserInfo(userTokenInfo.user_id);
} else {
ElMessage.error(result["message"] || "登录失败!请稍后再试");
return;
}
};
const thirdPartyLogoInfoList = ref([
{
name: "github",
logo: "https://www.ljsea.top/wp-content/uploads/2025/05/github-48-logo.png",
label: "GitHub",
},
{
name: "gitee",
logo: "https://www.ljsea.top/wp-content/uploads/2025/04/gitee.png",
label: "Gitee",
},
{
name: "qq",
logo: "https://www.ljsea.top/wp-content/uploads/2025/05/icons8-qq-48.png",
label: "QQ",
},
{
name: "google",
logo: "https://www.ljsea.top/wp-content/uploads/2025/05/icons8-google-48.png",
label: "Google",
},
{
name: "facebook",
logo: "https://www.ljsea.top/wp-content/uploads/2025/05/icons8-facebook.png",
label: "Facebook",
},
{
name: "stackoverflow",
logo: "https://www.ljsea.top/wp-content/uploads/2025/05/logo-stackoverflow-48.png",
label: "StackOverflow",
},
{
name: "my_gitea",
logo: "https://www.ljsea.top/wp-content/uploads/2025/05/my_Gitea.png",
label: "Gitea自建",
},
{
name: 'gitea',
logo: 'https://www.ljsea.top/wp-content/uploads/2025/05/PajamasGitea.png',
label: 'Gitea官方',
},
{
name: 'microsoft',
logo: 'https://www.ljsea.top/wp-content/uploads/2025/05/icons8-microsoft48.png',
label: 'Microsoft',
}
]);
//表单数据
var loginData = ref({
username: "",
email: "",
password: "",
ip: "",
fingerprint: '' // 浏览器指纹
});
const querySite = ref("");
const second_auth_state_key = ref(""); //二次认证状态key
const second_auth_method = ref("");
const second_auth_type_list = ref([]);
const second_auth_code = ref(""); //二次认证验证码
onMounted(async () => {
let fp = await getStoredFingerprint();
param.fingerprint = fp.fingerprint;
loginData.value.fingerprint = fp.fingerprint;
code_login_form.value.fingerprint = fp.fingerprint;
console.log("fp", fp.fingerprint);
// 获取query参数
const queryParams = route.query;
console.log('Received query parameters:', queryParams);
if (queryParams.site) {
querySite.value = queryParams.site as string;
}
});
const thirdLogin = async (type) => {
//获取uuid
let uuidResp = await getThirdPartyUUID({ "type": type });
if (uuidResp["code"] !== 0) {
ElMessage.error("获取UUID失败请稍后再试");
return;
}
let uuid = uuidResp["data"];
if (!uuid) {
ElMessage.error("获取UUID失败请稍后再试");
return;
}
let result = {};
//平台
// for(let i = 0; i < thirdPartyLogoInfoList.value.length; i++) {
// if(thirdPartyLogoInfoList.value[i].name === type) {
// result = thirdPartyLogoInfoList.value[i];
// break;
// }
// }
result = await getThirdPartyLoginUrl({ uuid: uuid, "type": "login", "platform": type, "fingerprint": param.fingerprint });
if (result["code"] !== 0) {
ElMessage.error("获取登录地址失败!请稍后再试");
return;
}
let loginUrl = result["data"];
if (!loginUrl) {
ElMessage.error("获取登录地址失败!请稍后再试");
return;
}
//清除定时器,防止多次点击登录按钮
if (timer.value) {
clearInterval(timer.value);
}
//获取登录状态每2秒请求一次
timer.value = setInterval(async () => {
currentLoginRequest.value++;
if (currentLoginRequest.value > maxLoginRepeatRequest) {
clearInterval(timer.value);
ElMessage.error("登录超时,请重新登录!");
return;
}
let statusResp = await getThirdPartyLoginStatus({ uuid: uuid, host_id: loginData.value.fingerprint });
if (statusResp["code"] !== 0) {
ElMessage.error("获取登录状态失败!请稍后再试");
clearInterval(timer.value);
return;
}
if (statusResp["code"] === 9) {
ElMessage.error("服务器错误,请稍后再试!");
clearInterval(timer.value);
return;
}
if (statusResp["data"]["status"] === 163) {
ElMessage.error("该账号未绑定,请先绑定账号!");
clearInterval(timer.value);
return;
}
let status = statusResp["data"];
if (status["status"] === 0) {
//登录成功
clearInterval(timer.value);
let userInfo: UserToken = status["user_info"];
globalData["token"] = userInfo.access_token;
localStorage.setItem("token", userInfo.access_token);
localStorage.setItem("refresh_token", userInfo.refresh_token);
localStorage.setItem("userId", userInfo.user_id.toString());
localStorage.setItem("username", userInfo.username);
let now = new Date();
localStorage.setItem("end_time", (now.setDate(now.getHours() + 12)).toString()); //过期时间
await getMyUserInfo(userInfo.user_id);
return;
}
}, 2000);
//新标签页打开登录地址
//window.open(loginUrl, "_blank");
//新的浏览器窗口打开标签页
const width = 800;
const height = 600;
// 计算窗口在屏幕中心的位置
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
const features = `width=${width},height=${height},left=${left},top=${top}`;
// 创建新窗口
const newWindow = window.open(loginUrl, '_blank', features);
};
const check_email = (email) => {
const emailReg = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/;
return emailReg.test(email);
}
const sendCode = async () => {
if ((code_login_form.value.email === "" || check_email(code_login_form.value.email) === false) && second_auth_state_key.value == "") {
ElMessage.error("请输入正确邮箱地址!");
return;
}
code_login_form.value.state = second_auth_state_key.value;
let result = await sendLoginCode(code_login_form.value);
if (result["code"] !== 0) {
ElMessage.error(result["message"] || "发送验证码失败!请稍后再试");
return;
} else {
countdown.value = 60;
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value); // 清除定时器
}
}, 1000);
ElMessage.success("验证码已发送,请注意查收!");
}
}
const HandleLoginByCode = async () => {
if (code_login_form.value.email === "" || code_login_form.value.code === "" || check_email(code_login_form.value.email) === false) {
ElMessage.error("请输入邮箱和验证码!");
return;
}
let result = await loginByCode(code_login_form.value);
if (result["code"] !== 0) {
ElMessage.error(result["message"] || "登录失败!请稍后再试");
return;
} else {
let userTokenInfo: UserToken = result["data"];
globalData["token"] = result.data;
localStorage.setItem("token", userTokenInfo.access_token);
localStorage.setItem("refresh_token", userTokenInfo.refresh_token);
localStorage.setItem("userId", userTokenInfo.user_id.toString());
localStorage.setItem("username", userTokenInfo.username);
let now = new Date();
localStorage.setItem("end_time", (now.setDate(now.getHours())).toString()); //过期时间
await getMyUserInfo(userTokenInfo.user_id);
}
};
const toggleLoginType = () => {
if (login_type.value === "pwd") {
login_type.value = "code";
} else {
login_type.value = "pwd";
}
};
//登录接口调用
const onLogin = async () => {
if (login_type.value !== "pwd") {
HandleLoginByCode();
return;
}
console.log("params:", param);
loginData.value.username = param.username;
loginData.value.password = param.password;
let result = await loginService(loginData);
console.log("login result:", result);
if (result["code"] === 0) {
ElMessage.success("登录成功");
} else if (result["code"] == 1102) {
//需二次认证,支持方式
second_auth_visible.value = true;
second_auth_state_key.value = result["data"]["state"];
let types = result["data"]["type"];
second_auth_type_list.value = types.split(",");
//删除''空值
second_auth_type_list.value = second_auth_type_list.value.filter(item => item);
second_auth_method.value = second_auth_type_list.value[0];
console.log("second_auth_type_list:", second_auth_type_list.value);
return;
} else if (result["code"] === 1101) {
ElMessage.error(result["message"] || "该账号已被禁用,请联系管理员!");
return;
} else {
ElMessage.error(result["message"] || "登录失败!请稍后再试");
return;
}
let userTokenInfo: UserToken = result["data"];
globalData["token"] = result.data;
localStorage.setItem("token", userTokenInfo.access_token);
localStorage.setItem("refresh_token", userTokenInfo.refresh_token);
localStorage.setItem("userId", userTokenInfo.user_id.toString());
localStorage.setItem("username", userTokenInfo.username);
let now = new Date();
localStorage.setItem("end_time", (now.setDate(now.getHours())).toString()); //过期时间
await getMyUserInfo(userTokenInfo.user_id);
//token.value= result.data;
};
const getMyUserInfo = async (id) => {
let result = {};
try {
let tokenData = {
token: localStorage.getItem("token"),
id: id,
};
result = await GetUserInfoService(tokenData);
const cookie = Cookies.get("user_token");
console.log("cookie:", cookie);
if (result["code"] === 0) {
//console.log("token data:",this.tokenData)
// localStorage.setItem("video_func", result.data.VideoFunc);
// localStorage.setItem("device_func", result.data.DeviceFunc);
// localStorage.setItem("cid_func", result.data.CIDFunc);
// localStorage.setItem("role", result.data.Role);
ElMessage.success("登录成功");
localStorage.setItem("ms_username", result["data"]["Name"]);
const keys =
permiss.defaultList[result["data"]["Role"] == "admin" ? "admin" : "user"];
localStorage.setItem("ms_imgurl", result["data"]["Avatar"]);
permiss.handleSet(keys);
localStorage.setItem("ms_keys", JSON.stringify(keys));
localStorage.setItem("ms_role", result["data"]["Role"]);
if (querySite.value === "") {
router.push("/");
} else {
localStorage.setItem("targetSite", querySite.value);
router.push({ path: `/project-select` });
}
//alert("video_func:" + localStorage.getItem("video_func")+" type:" +typeof(localStorage.getItem("video_func")));
}
} catch (e) {
console.log(e);
}
};
// 获取标签存储并清空标签
</script>
<style scoped>
.login-bg {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: url(../../assets/img/login-bg3.jpg) center/cover no-repeat;
}
.login-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40px;
}
.logo {
width: 35px;
}
.login-title {
font-size: 22px;
color: #333;
font-weight: bold;
}
.login-container {
width: 470px;
border-radius: 5px;
background: #fff;
padding: 40px 50px 50px;
box-sizing: border-box;
}
.pwd-tips {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
margin: -10px 0 10px;
color: #787878;
}
.pwd-checkbox {
height: auto;
}
.login-btn {
display: block;
/* 文字水平竖直居中 */
text-align: center;
width: 100%;
}
.login-tips {
font-size: 12px;
color: #999;
}
.login-text {
display: flex;
align-items: center;
margin-top: 20px;
font-size: 14px;
color: #787878;
}
.login-text {
text-align: center;
}
.github-btn {
background-image: url(../../assets/img/github-logo.png) !important;
}
.gitee-btn {
background-image: url(../../assets/img/gitee_logo.png) !important;
}
.qq-btn {
background-image: url(https://wiki.connect.qq.com/wp-content/uploads/2016/12/Connect_logo_4.png);
}
.google-btn {
background-image: url(../../assets/img/google-logo_resized.png) !important;
}
.code-btn :deep(.el-button--text:not(.is-disabled)):hover {
color: #0d66d0 !important;
background: rgba(22, 119, 255, 0.05) !important;
}
</style>