feat(auth): 登录注册退出登录完毕
This commit is contained in:
parent
26d0e1fd28
commit
d7033a13b6
@ -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
26
src/apis/api.js
Normal 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
10
src/apis/apis_auth.js
Normal 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
40
src/apis/axios.js
Normal 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;
|
@ -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>
|
||||
|
@ -16,6 +16,11 @@ const routes = [
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/auth',
|
||||
name: 'AuthView',
|
||||
component: () => import('../views/AuthView.vue'),
|
||||
}
|
||||
];
|
||||
|
||||
// 创建路由实例
|
||||
|
29
src/store/index.js
Normal file
29
src/store/index.js
Normal 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
68
src/store/modules/user.js
Normal 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('用户主动退出登录');
|
||||
},
|
||||
},
|
||||
};
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user