登录,退出登录

newzwz
lee-zt 2 months ago
parent fa9627a9de
commit d3158ec4f9

@ -28,7 +28,8 @@
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,
"env": { "env": {
"node": true "node": true,
"vue/setup-compiler-macros": true
}, },
"extends": [ "extends": [
"plugin:vue/vue3-essential", "plugin:vue/vue3-essential",

@ -38,11 +38,15 @@
<!-- 登录/注册按钮 --> <!-- 登录/注册按钮 -->
<div class="nav-section"> <div class="nav-section">
<button v-if="!isLoggedIn" @click="showModal" class="login-btn">/</button> <button v-if="!isLoggedIn" @click="showModal" class="login-btn">/</button>
<div v-else class="user-avatar"> <div v-else class="user-avatar" @mouseenter="showDropdown" @mouseleave="hideDropdown">
<img :src="userInfo.avatar || defaultAvatar" alt="用户头像" class="avatar-img" /> <img :src="userInfo.avatar || defaultAvatar" alt="用户头像" class="avatar-img" />
<div class="dropdown-menu"> <!-- 悬浮板块 -->
<button @click="goToProfile"></button> <div class="user-dropdown-menu" v-show="isDropdownVisible">
<button @click="logout">退</button> <p class="user-name">{{ userInfo.userName }}</p>
<div class="button-container">
<button @click="goToProfile" class="dropdown-btn">个人中心</button>
<button @click="logout" class="dropdown-btn">退出登录</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -54,7 +58,7 @@
<script setup name="Header"> <script setup name="Header">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useUserStore } from '@/stores/user.ts'; import { useUserStore } from '@/stores/user.js';
import LoginRegisterModal from './LoginRegisterModal.vue'; import LoginRegisterModal from './LoginRegisterModal.vue';
// //
@ -62,6 +66,7 @@ const userStore = useUserStore();
// //
const isModalVisible = ref(false); const isModalVisible = ref(false);
const isDropdownVisible = ref(false);
const colleges = ref([ const colleges = ref([
{ name: '武大官网', url: 'https://www.whu.edu.cn/' }, { name: '武大官网', url: 'https://www.whu.edu.cn/' },
{ name: '计算机学院', url: 'https://cs.whu.edu.cn/' }, { name: '计算机学院', url: 'https://cs.whu.edu.cn/' },
@ -82,7 +87,7 @@ const colleges = ref([
]); ]);
// //
const defaultAvatar = '@/assets/default-avatar.png'; const defaultAvatar = '@/assets/default-avatar/boy_4.png';
// //
const isLoggedIn = computed(() => userStore.isLoggedIn); const isLoggedIn = computed(() => userStore.isLoggedIn);
@ -104,7 +109,25 @@ const goToProfile = () => {
const logout = () => { const logout = () => {
userStore.logout(); userStore.logout();
window.location.href = '/'; isDropdownVisible.value = false; //
};
let showTimer = null; //
let hideTimer = null; //
const showDropdown = () => {
console.log('showDropdown');
clearTimeout(hideTimer); //
showTimer = setTimeout(() => {
isDropdownVisible.value = true; // 0.5
}, 200);
};
const hideDropdown = () => {
clearTimeout(showTimer); //
hideTimer = setTimeout(() => {
isDropdownVisible.value = false; // 1
}, 400);
}; };
</script> </script>
@ -217,7 +240,7 @@ const logout = () => {
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
background-color: white; background-color: rgb(255, 255, 255);
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
@ -293,4 +316,61 @@ const logout = () => {
color: #6fbd87; color: #6fbd87;
background: #f0f0f0; background: #f0f0f0;
} }
.avatar-img {
width: 35px;
height: 35px;
border-radius: 50%;
cursor: pointer;
}
/* 头像悬浮板块样式 */
.user-dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 10px;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center; /* 水平居中用户名 */
text-align: center; /* 确保文字居中 */
gap: 10px; /* 增加子元素之间的间距 */
width: 200px; /* 固定宽度,确保布局一致 */
}
/* 用户名样式 */
.user-name {
font-size: 20px;
font-weight: bold;
margin-bottom: 10px;
color: #54ac52;
}
.button-container {
display: flex;
justify-content: space-between; /* 按钮左右排列 */
width: 100%; /* 按钮容器占满父容器宽度 */
}
/* 悬浮板块按钮样式 */
.dropdown-btn {
width: 45%; /* 按钮占据父容器的 45% 宽度 */
padding: 8px 10px;
background: none;
border: 0;
border-radius: 4px;
text-align: center;
font-size: 14px;
color: #333;
cursor: pointer;
transition: background-color 0.3s, color 0.3s;
}
.dropdown-btn:hover {
background-color: #f0f0f0;
color: #6fbd87;
}
</style> </style>

@ -15,7 +15,8 @@
</div> </div>
<!-- 登录表单 --> <!-- 登录表单 -->
<form @submit.prevent="login" class="login-form"> <button @click="login" v-if="false"></button>
<form @submit.prevent="Login1" class="login-form">
<div class="input-group"> <div class="input-group">
<!-- 用户标识输入框用户名/邮箱/手机 --> <!-- 用户标识输入框用户名/邮箱/手机 -->
<input <input
@ -70,195 +71,177 @@
</div> </div>
</template> </template>
<script> <script setup name="UserLogin">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import axios from 'axios'; import axios from 'axios';
import request from '@/utils/request'; import request from '@/utils/request';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useUserStore } from '@/stores/user.ts'; import { useUserStore } from '@/stores/user.js';
export default { const currentType = ref('username'); //
name: 'UserLogin', const cooldown = ref(0); //
//TODO: props, { emit } const userStore = useUserStore(); //
setup() { const emit = defineEmits(['toggleForm', 'LoginSuccess']); //
const currentType = ref('username'); // //
const cooldown = ref(0); // const loginTypes = [
const userStore = useUserStore(); // { label: '用户名登录', value: 'username' },
// { label: '邮箱登录', value: 'email' },
const loginTypes = [ { label: '手机登录', value: 'phone' }
{ label: '用户名登录', value: 'username' }, ];
{ label: '邮箱登录', value: 'email' },
{ label: '手机登录', value: 'phone' } //
]; const loginForm = ref({
userFlag: '', //
// password: '', //
const loginForm = ref({ verifyCode: '', // /使
userFlag: '', // remember: false //
password: '', // });
verifyCode: '', // /使
remember: false // //
}); const inputType = computed(() => {
switch (currentType.value) {
// case 'email':
const inputType = computed(() => { return 'email'; //
switch(currentType.value) { case 'phone':
case 'email': return 'email'; // return 'tel'; //
case 'phone': return 'tel'; // default:
default: return 'text'; // return 'text'; //
} }
}); });
// //
const placeholder = computed(() => { const placeholder = computed(() => {
switch(currentType.value) { switch (currentType.value) {
case 'username': return '请输入用户名'; case 'username':
case 'email': return '请输入邮箱'; return '请输入用户名';
case 'phone': return '请输入手机号'; case 'email':
default: return ''; return '请输入邮箱';
} case 'phone':
}); return '请输入手机号';
default:
return '';
}
});
//
const showVerifyCode = computed(() => {
return ['email', 'phone'].includes(currentType.value);
});
//
const isValidInput = computed(() => {
const value = loginForm.value.userFlag;
if (!value) return false; //
switch (currentType.value) {
case 'email':
return /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/.test(value);
case 'phone':
return /^1[3-9]\d{9}$/.test(value);
default:
return value.length > 0; //
}
});
// //
const showVerifyCode = computed(() => { const isvalidForm = computed(() => {
return ['email', 'phone'].includes(currentType.value); if (!isValidInput.value) return false;
});
// if (showVerifyCode.value) {
const isValidInput = computed(() => { return loginForm.value.verifyCode.length === 6; //
const value = loginForm.value.userFlag } else {
if(!value) return false; // return loginForm.value.password.length >= 6; //
}
switch(currentType.value) { });
case 'email':
return /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/.test(value);
case 'phone':
return /^1[3-9]\d{9}$/.test(value);
default:
return value.length > 0; //
}
});
// //
const isvalidForm = computed(() => { async function sendCode() {
if(!isValidInput.value) return false; if (cooldown.value > 0 || !isValidInput.value) return; //
if(showVerifyCode.value){ try {
return loginForm.value.verifyCode.length === 6; // // TODO:
} else { const response = await axios.post('/user/sendCode', {
return loginForm.value.password.length >= 6; // type: currentType.value,
} target: loginForm.value.userFlag
}); });
// if (response.data.success) {
async function sendCode() { // 60
if (cooldown.value > 0 || !isValidInput.value) return; // cooldown.value = 60;
const timer = setInterval(() => {
try { cooldown.value--;
//TODO: if (cooldown.value <= 0) {
const response = await axios.post('/user/sendCode', { clearInterval(timer);
type: currentType.value,
target: loginForm.value.userFlag
});
if (response.data.success) {
//60
cooldown.value = 60;
const timer = setInterval(() => {
cooldown.value--;
if (cooldown.value <= 0) {
clearInterval(timer);
}
}, 1000);
} }
} catch (error) { }, 1000);
console.error('发送验证码失败:', error);
}
} }
} catch (error) {
console.error('发送验证码失败:', error);
}
}
//
function Login1() {
userStore.login({
avatar:require('@/assets/default-avatar/boy_1.png'),
userName: '珈人一号'
});
emit('LoginSuccess');
}
//
async function login() {
if (!isvalidForm.value) return;
try {
const loginData = {
userFlag: loginForm.value.userFlag,
password: loginForm.value.password
};
// if (showVerifyCode.value) {
/* loginData.verifyCode = loginForm.value.verifyCode; //
function login1() { }
userStore.login({
avatar: '/assets/default-avatar/boy_1.png',
userName: '珈人一号'
})
}*/
async function login() {
if (!isvalidForm.value) return;
try {
const loginData = {
userFlag: loginForm.value.userFlag,
password: loginForm.value.password
}
if (showVerifyCode.value) {
loginData.verifyCode = loginForm.value.verifyCode; //
}
//post // post
const response = await request.post('/user/login', loginData); const response = await request.post('/user/login', loginData);
if (response.code === 200) {
//token
//
const { accessToken, refreshToken } = response.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
userStore.login({
avatar: '/assets/default-avatar/boy_1.png',
userName: '珈人一号'
})
ElMessage({
message: '登录成功',
type: 'success',
duration: 500
});
//TODO:
//or
}else{
//
ElMessage({
message: '登陆失败,用户名或密码错误',
type: 'error',
duration: 500
});
}
} catch (error) { if (response.code === 200) {
console.error('登录失败:', error); // token
alert(error.response?.message ||'登录失败,请稍后重试'); const { accessToken, refreshToken } = response.data;
} localStorage.setItem('accessToken', accessToken);
} localStorage.setItem('refreshToken', refreshToken);
// userStore.login({
const switchLoginType = (type) => { avatar: '/assets/default-avatar/boy_1.png',
currentType.value = type; userName: '珈人一号'
loginForm.value.userFlag = ''; });
loginForm.value.password = '';
loginForm.value.verifyCode = ''; ElMessage({
message: '登录成功',
type: 'success',
duration: 500
});
// TODO:
} else {
//
ElMessage({
message: '登陆失败,用户名或密码错误',
type: 'error',
duration: 500
});
} }
} catch (error) {
// 使 console.error('登录失败:', error);
return { alert(error.response?.message || '登录失败,请稍后重试');
currentType,
loginTypes,
loginForm,
inputType,
placeholder,
showVerifyCode,
cooldown,
sendCode,
//login1,
login,
switchLoginType,
isValidInput
};
} }
} }
//
const switchLoginType = (type) => {
currentType.value = type;
loginForm.value.userFlag = '';
loginForm.value.password = '';
loginForm.value.verifyCode = '';
};
</script> </script>
<style scoped> <style scoped>

@ -8,7 +8,7 @@
<!-- 模态框内容区域 --> <!-- 模态框内容区域 -->
<div class="modal-content"> <div class="modal-content">
<!-- 根据 isLogin 状态动态切换登录/注册组件 --> <!-- 根据 isLogin 状态动态切换登录/注册组件 -->
<UserLogin v-if="isLogin" @toggleForm="toggleForm" /> <UserLogin v-if="isLogin" @toggleForm="toggleForm" @LoginSuccess="close"/>
<UserRegister v-else @toggleForm="toggleForm" /> <UserRegister v-else @toggleForm="toggleForm" />
</div> </div>
</div> </div>
@ -39,7 +39,7 @@ export default {
return { return {
isLogin, isLogin,
toggleForm, toggleForm,
close: () => emit('close') // close: () => {emit('close')} //
}; };
} }
} }

Loading…
Cancel
Save