feat(auth): 登录注册退出登录完毕

This commit is contained in:
萌狼蓝天 2025-03-12 17:50:16 +08:00
parent 26d0e1fd28
commit d7033a13b6
10 changed files with 498 additions and 6 deletions

View File

@ -11,11 +11,15 @@
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.2.6",
"axios": "^1.8.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
"vue-router": "^4.5.0",
"vue3-slide-verify": "^1.1.6",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"path": "^0.12.7",
"vite": "^6.2.0"
}
}

26
src/apis/api.js Normal file
View File

@ -0,0 +1,26 @@
// api.js
import axiosInstance from './axios'; // 导入在axios.js中配置好的axios实例
export default {
}
// 统一的get请求方法
export function get(url, params = {}) {
return axiosInstance.get(url, { params });
}
// 统一的post请求方法
export function post(url, data = {}) {
return axiosInstance.post(url, data);
}
// 统一的put请求方法通常用于更新资源
export function put(url, data = {}) {
return axiosInstance.put(url, data);
}
// 统一的delete请求方法
export function deleteRequest(url, params = {}) {
// 注意axios的delete方法第二个参数是config对象如果要传递参数通常使用params
return axiosInstance.delete(url, { params });
}

10
src/apis/apis_auth.js Normal file
View File

@ -0,0 +1,10 @@
import {post} from "./api.js";
export function auth_reg(data) {
return post("/auth/register", data)
}
export function auth_login(data) {
return post("/auth/login", data)
}
export function auth_logout() {
return post("/auth/logout");
}

40
src/apis/axios.js Normal file
View File

@ -0,0 +1,40 @@
import axios from 'axios';
import store from '@/store';
import router from '@/router';
// 创建axios实例
const service = axios.create({
baseURL: '/api', // api的base_url可以在.env文件中配置
timeout: 5000, // 请求超时时间
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
return config;
},
(error) => {
// 处理请求错误
console.error(error); // for debug
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response && error.response.status === 401) {
store.commit('user/CLEAR_SESSION');
router.push('/login?expired=1');
}
console.error('Error:', error); // 更清晰的错误日志输出
return Promise.reject(error);
}
);
export default service;

View File

@ -1,5 +1,35 @@
<script setup>
import {ref} from 'vue';
import {computed, onMounted, ref} from 'vue';
import store from "@/store/index.js";
import {useRouter} from "vue-router";
const router = useRouter();
// 使
const isLogin = computed(() => {
return !!store.state.user.info
})
//
const userInfo = computed(() => store.state.user.info)
const isAdmin = computed(() => store.state.user.info?.isAdmin || false)
// vuex
onMounted(async () => {
await store.dispatch('user/initialize')
})
const logout = () => {
// Vuex action
store.dispatch('user/logout')
//
router.push({
path: '/auth',
// query: {
// reason: 'logout',
// redirect: router.currentRoute.value.fullPath
// }
})
}
//
const searchValue = ref('');
const onSearch = searchValue => {
@ -62,13 +92,19 @@ const handleBorrow = (book) => {
<a-col :span="6">
<div class="block01">
<a-card class="card" v-if="isLogin">
<p class="c-title">个人</p>
<p class="c-title">{{userInfo["name"]}} </p>
<p>当前借书</p>
<p>逾期</p>
<p style="text-align: center;">
<span class="inline-btn"><a-button type="primary">进入后台</a-button></span>
<span class="inline-btn"><a-button type="primary" danger @click="logout()">退出登录</a-button></span>
</p>
</a-card>
<a-card class="card" v-else>
<p class="c-title">未登录</p>
<p>我要登录</p>
<p><router-link to="/auth">我要登录</router-link></p>
<p>我要注册</p>
</a-card>
</div>
@ -141,6 +177,7 @@ const handleBorrow = (book) => {
.block01 .card {
width: 100%;
height:200px;
margin: auto;
}
@ -164,4 +201,8 @@ const handleBorrow = (book) => {
.ant-col-18 {
padding-left: 20px;
}
.inline-btn{
margin-left: 1em;
}
</style>

View File

@ -16,6 +16,11 @@ const routes = [
},
]
},
{
path: '/auth',
name: 'AuthView',
component: () => import('../views/AuthView.vue'),
}
];
// 创建路由实例

29
src/store/index.js Normal file
View File

@ -0,0 +1,29 @@
// src/store/index.js
import { createStore } from 'vuex'
import userModule from './modules/user' // 用户模块
// 创建 Vuex 存储实例
const store = createStore({
// 组合各个模块
modules: {
user: userModule // 注册用户模块(带命名空间)
},
// 全局状态(可选)
state: {
appName: 'libroro'
},
// 全局 mutations可选
mutations: {
SET_APP_NAME(state, name) {
state.appName = name
}
},
// 严格模式(仅开发环境)
strict: process.env.NODE_ENV !== 'production'
})
export default store

68
src/store/modules/user.js Normal file
View File

@ -0,0 +1,68 @@
import service from '@/apis/axios';
import axios from 'axios';
import {auth_logout} from "@/apis/apis_auth.js";
export default {
namespaced: true,
state: () => ({
info: null,
lastLogin: null,
refreshTimer: null,
}),
mutations: {
SET_USER_INFO(state, payload) {
state.info = {
...payload,
isAdmin: payload.role === 'admin',
};
state.lastLogin = new Date().toISOString();
},
CLEAR_SESSION(state) {
state.info = null;
state.lastLogin = null;
localStorage.removeItem('userInfo');
if (state.refreshTimer) {
clearTimeout(state.refreshTimer);
state.refreshTimer = null;
}
},
},
actions: {
async initialize({ commit }) {
try {
const rawData = localStorage.getItem('userInfo');
if (rawData) {
const data = JSON.parse(rawData);
commit('SET_USER_INFO', {
...data,
token: data.token ? atob(data.token) : null,
});
}
} catch (err) {
console.error('初始化用户状态失败:', err);
commit('CLEAR_SESSION'); // 直接调用 mutation 而不是 dispatch
}
},
CLEAR_SESSION({ commit }) {
commit('CLEAR_SESSION');
localStorage.removeItem('userInfo');
},
async logout({commit}) {
//向后端发起退出登录的请求
try {
await auth_logout(); // 调用退出登录API
} catch (err) {
console.error('退出登录失败:', err);
}
localStorage.removeItem('userInfo');
delete service.defaults.headers.common['Authorization'];
commit('CLEAR_SESSION');
// 取消所有 pending 请求(可选)
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
source.cancel('用户主动退出登录');
},
},
};

View File

@ -1,11 +1,262 @@
<script setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue';
import SlideVerify from 'vue3-slide-verify'; // vue3-slide-verify
import "vue3-slide-verify/dist/style.css";
import {auth_login, auth_reg} from "../apis/apis_auth.js";
import {useRouter} from "vue-router";
import store from '@/store'
//
const activeKey = ref('login');
const loginForm = reactive({ username: '', password: '' });
const registerForm = reactive({
username: '',
password: '',
confirmPassword: ''
});
const router = useRouter()
//
const isVerified = ref(false);
//
const loginRules = {
username: [{ required: true, message: '请输入账号' }],
password: [{ required: true, message: '请输入密码' }]
};
const registerRules = {
username: [{ required: true, message: '请输入账号' }],
password: [{ required: true, message: '请输入密码' }],
confirmPassword: [
{ required: true, message: '请确认密码' },
{
validator: async (rule, value) => {
return value === registerForm.password;
},
message: '两次输入的密码不一致'
}
]
};
//
const onVerifySuccess = () => {
isVerified.value = true;
message.success('验证成功');
};
//
const formatTime = (isoString) => {
if (!isoString) return '-';
try {
const date = new Date(isoString);
return date.toLocaleString('zh-CN');
} catch (e) {
return '-';
}
};
//
const validateRedirectPath = (path) => {
if (!path) return false;
try {
const fullPath = new URL(path, window.location.origin).pathname;
return fullPath.startsWith('/') && !path.includes('//');
} catch {
return false;
}
};
//
const handleLogin = () => {
if (!isVerified.value) {
message.warning('请先完成验证');
return;
}
if(loginForm.username===""){
message.warning('请填写账号');
return;
}
if(loginForm.password===""){
message.warning('请填写密码');
return;
}
auth_login(loginForm).then(async (res) => {
if (res.code === 200) {
message.success("登录成功")
//
const data = res.data;
//
const userInfo = {
token: btoa(data.access_token),
username: data.username,
name: data.name,
role: data.role,
loginTime: formatTime(data.loginTime), //
createTime: formatTime(data.createTime)
};
// 使Vuex + localStorage
// 使localStorage
localStorage.setItem('userInfo', JSON.stringify({
...userInfo,
// ISO
rawLoginTime: data.loginTime,
rawCreateTime: data.createTime
}));
// Vuex
store.commit('user/SET_USER_INFO', {
...userInfo,
//
loginTime: formatTime(userInfo.rawLoginTime),
createTime: formatTime(userInfo.rawCreateTime)
});
//
const redirectPath = router.currentRoute.value.query.redirect;
const isValidPath = validateRedirectPath(redirectPath); //
const targetPath = isValidPath ? redirectPath : '/';
//
message.success('登录成功,正在跳转...');
await router.push(targetPath);
//
} else {
message.warning(res.msg)
}
}).catch((err) => {
//
console.error('登录异常:', err);
message.error(`登录失败: ${err.message || '未知错误'}`);
})
};
const handleRegister = () => {
if (!isVerified.value) {
message.warning('请先完成验证');
return;
}
// 1
if (registerForm.username === ""){
message.warning('请填写账号');
return;
}
if (registerForm.password === ""){
message.warning('请填写密码');
return;
}
// 2
if (registerForm.password !==registerForm.confirmPassword) {
message.warning("两次输入的密码不一致")
return;
}
// 3
auth_reg(registerForm).then((res) => {
if(res.code === 200){
message.success("注册成功")
this.activeKey = "login";
registerForm.username = "";
registerForm.password = "";
registerForm.confirmPassword = "";
}else{
message.warning(res.msg)
}
})
};
</script>
<template>
<div class="auth-container">
<a-card class="auth-card">
<a-tabs v-model:activeKey="activeKey" centered>
<!-- 登录标签页 -->
<a-tab-pane key="login" tab="登录">
<a-form
:model="loginForm"
:rules="loginRules"
@finish="handleLogin"
layout="vertical"
>
<a-form-item label="账号" name="username">
<a-input v-model:value="loginForm.username" />
</a-form-item>
<a-form-item label="密码" name="password">
<a-input-password v-model:value="loginForm.password" />
</a-form-item>
<slide-verify
@success="onVerifySuccess"
slider-text="向右滑动验证"
/>
<a-button
type="primary"
html-type="submit"
block
class="submit-btn"
>
登录
</a-button>
</a-form>
</a-tab-pane>
<!-- 注册标签页 -->
<a-tab-pane key="register" tab="注册">
<a-form
:model="registerForm"
:rules="registerRules"
@finish="handleRegister"
layout="vertical"
>
<a-form-item label="账号" name="username">
<a-input v-model:value="registerForm.username" />
</a-form-item>
<a-form-item label="密码" name="password">
<a-input-password v-model:value="registerForm.password" />
</a-form-item>
<a-form-item label="确认密码" name="confirmPassword">
<a-input-password v-model:value="registerForm.confirmPassword" />
</a-form-item>
<slide-verify
@success="onVerifySuccess"
slider-text="向右滑动验证"
/>
<a-button
type="primary"
html-type="submit"
block
class="submit-btn"
>
注册
</a-button>
</a-form>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<style scoped>
.auth-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f0f2f5;
}
.auth-card {
width: 400px;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.submit-btn {
margin-top: 24px;
}
:deep(.ant-tabs-nav) {
margin-bottom: 24px;
}
</style>

View File

@ -1,7 +1,25 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
import { resolve } from 'path' // npm install path --save-dev
// vite.config.js
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src') // 设置 @ 指向 src 目录
}
},
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:10300',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api'), // 保持路径不变
secure: false,
ws: true
}
}
}
})