vue3+TS+pinia+cookies+axiox 实现简单登录的持久化

目录

完整项目请访问

一.各页面完整代码

1.浏览器缓存相关设置

2.pinia状态管理应用

3.request请求/响应拦截

4.路由守卫

5.登录页代码

6.header登录部分代码

二.部分代码截取

1.login登录页

2.header部分


完整项目请访问

gitee:vue3-TS-model: 用于从零搭建模板使用 (gitee.com)

github:vue3-TS-model: 用于从零搭建模板使用 (gitee.com)

一.各页面完整代码

1.浏览器缓存相关设置

这一部分使用了localstorage,session,cookies数据缓存方式

import Cookies from 'js-cookie';

/**
 * window.localStorage 浏览器永久缓存
 * @method set 设置永久缓存
 * @method get 获取永久缓存
 * @method remove 移除永久缓存
 * @method clear 移除全部永久缓存
 */
export const Local = {
	// 设置永久缓存
	set(key: string, val: any) {
		window.localStorage.setItem(key, JSON.stringify(val));
	},
	// 获取永久缓存
	get(key: string) {
		let json: any = window.localStorage.getItem(key);
		return JSON.parse(json);
	},
	// 移除永久缓存
	remove(key: string) {
		window.localStorage.removeItem(key);
	},
	// 移除全部永久缓存
	clear() {
		window.localStorage.clear();
	},
};

/**
 * window.sessionStorage 浏览器临时缓存
 * @method set 设置临时缓存
 * @method get 获取临时缓存
 * @method remove 移除临时缓存
 * @method clear 移除全部临时缓存
 */
export const Session = {
	// 设置临时缓存
	set(key: string, val: any) {
		if (key === 'token' || key === 'username'){
			const expirationTime = new Date();
      expirationTime.setTime(expirationTime.getTime() + 2 * 60 * 60 * 1000); // 2小时后过期
      return Cookies.set(key, val, { expires: expirationTime });
		} 
		window.sessionStorage.setItem(key, JSON.stringify(val));
	},
	// 获取临时缓存
	get(key: string) {
		if (key === 'token' || key === 'username') return Cookies.get(key);
		let json: any = window.sessionStorage.getItem(key);
		return JSON.parse(json);
	},
	// 移除临时缓存
	remove(key: string) { 
		if (key === 'token' || key === 'username') return Cookies.remove(key);
		window.sessionStorage.removeItem(key);
	},
	// 移除全部临时缓存
	clear() {
		Cookies.remove('token');
		Cookies.remove('username');

		window.sessionStorage.clear();
	},
};

2.pinia状态管理应用

这一部分用于存储登陆后获取的一些信息,用于全局

// 在 src/store/index.js 中创建一个简单的 store
import { UserInfoState } from '@/api/login/types';
import { defineStore } from 'pinia'

interface State {
  userForm: UserInfoState;
  isLogin:boolean;
}

export const useMyStore = defineStore('myStore', {
  state: (): State => ({
    userForm: {
      user: '',
      ID: '',
      age: '',
      sex: ''
    },
    isLogin:false
    // 其他状态
  }),
  actions: {
    // 动作
    setUserInfo(data:UserInfoState) {
      this.userForm = data
    },
    setIsLogin(data:boolean) {
      this.isLogin = data
    },

  },
})

3.request请求/响应拦截

import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Session } from '@/utils/storage';

// 配置新建一个 axios 实例
const service = axios.create({
	baseURL: 'https://www.fastmock.site/mock/b9536a1dea3fe4daeec18bc365e14a18/api',
	timeout: 50000,
	headers: { 'Content-Type': 'application/json' },
});

// 添加请求拦截器
service.interceptors.request.use(
	(config) => {
		// 在发送请求之前做些什么 token
		if (Session.get('token')) {
			(<any>config.headers).common['token'] = `${Session.get('token')}`;
		}
		return config;
	},
	(error) => {
		// 对请求错误做些什么
		return Promise.reject(error);
	}
);

// 添加响应拦截器
service.interceptors.response.use(
	(response) => {
		// 对响应数据做点什么
		const res = response.data;
		if (res.code && res.code !== '2000') {
			// `token` 过期或者账号已在别处登录
			if (res.code === 401 || res.code === "4001") {
				Session.clear(); // 清除浏览器全部临时缓存
				window.location.href = '/'; // 去登录页
				ElMessageBox.alert('你已被登出,请重新登录', '提示', {})
					.then(() => {})
					.catch(() => {});
			}
			return response.data;
			// return Promise.reject(service.interceptors.response);
		} else {
			return response.data;
		}
	},
	(error) => {
		// 对响应错误做点什么
		if (error.message.indexOf('timeout') != -1) {
			ElMessage.error('网络超时');
		} else if (error.message == 'Network Error') {
			ElMessage.error('网络连接错误');
		} else {
			if (error.response.data) ElMessage.error(error.response.statusText);
			else ElMessage.error('接口路径找不到');
		}
		return Promise.reject(error);
	}
);

// 导出 axios 实例
export default service;

4.路由守卫

// 导入router所需的方法
import { createRouter, createWebHashHistory } from 'vue-router'

// 导入路由页面的配置
import routes from './routes'
import { Session } from '@/utils/storage'
import { useMyStore } from '@/stores/states'

// 路由参数配置
const router = createRouter({
    // 使用hash(createWebHashHistory)模式,(createWebHistory是HTML5历史模式,支持SEO)
    history: createWebHashHistory(),
    routes: routes,
})

// 全局前置守卫,这里可以加入用户登录判断
router.beforeEach((to, from, next) => {
    if(Session.get('token') && (from.path === '/' || to.path === '/index')){
        useMyStore().setIsLogin(true)
    }
    // 继续前进 next()
    // 返回 false 以取消导航
    next()
})

// 全局后置钩子,这里可以加入改变页面标题等操作
router.afterEach((to, from) => {
    const _title = to.meta.title
    if (_title) {
        window.document.title = String(_title)
    }
})

// 导出默认值
export default router

5.登录页代码

<template>
  <div class="loginForm">
    <div class="rloginTitle">登录</div>

    <!-- 各个输入框 -->
    <el-form :model="state.formData" :rules="formRules" ref="formRef" label-width="50px">
      <el-form-item prop="username">
        <div class="formInput">
          <el-input v-model="state.formData.username" placeholder="username" clearable autocomplete="off"
            prefix-icon="User"></el-input>

        </div>
      </el-form-item>
      <el-form-item prop="password">
        <div class="formInput">

          <el-input v-model="state.formData.password" placeholder="password" type="password" show-password
            autocomplete="off" prefix-icon="Lock"></el-input>
        </div>
      </el-form-item>

      <!-- 验证码 -->
      <el-form-item prop="code">
        <div class="formInput codeLine">
          <el-input class="codeInput" v-model="state.formData.code" placeholder="输入验证码" clearable maxlength="4" />
          <slideVerify class="codeShow" v-model:identifyCode="identifyCode" @click="refresh()"></slideVerify>
        </div>
      </el-form-item>

      <!-- 登陆界面 -->
      <el-form-item>
        <div class="formInput settings">
          <label class="labelText">
            <input type="checkbox" v-model="rem_pswd" /> 记住密码
          </label>
          <label class="labelText">
            <span @click.stop="go_page('findPassword')">忘记密码?</span>
            <span @click.stop="go_page('register')">注册</span>
          </label>

        </div>
      </el-form-item>

      <!-- 提交按钮 -->
      <el-form-item label-width="0">
        <el-button :loading="state.loading" type="primary" class="loginButton" round
          @click="handleSubmita(formRef)">登录</el-button>
      </el-form-item>


    </el-form>
  </div>
</template>


<script setup lang="ts">
import { reactive, ref } from 'vue';
import { FormInstance, FormRules } from 'element-plus';
import router from '@/router';

// 导入生成验证码组件
import slideVerify from "@/components/slideVerify.vue"

// 导入登录 API 方法
import { UseLoginApi } from "@/api/login/index"
// 导入登录状态的类型
import { LoginState } from "@/api/login/types";

// 导入本地存储工具类
import { Session } from '@/utils/storage';
// 导入全局状态管理
import { useMyStore } from '@/stores/states';

// 生成随机验证码
const generateCode = () => {
  const code = ref('');
  const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
  for (let i = 0; i < 4; i++) {
    code.value += characters.charAt(Math.floor(Math.random() * characters.length));
  }
  return code.value;
}
// 传递验证码给子组件
const identifyCode = ref(generateCode())
// 刷新验证码
const refresh = () => {
  identifyCode.value = generateCode()
}
// 检测输入的验证码,
const checkCodeFun = (rule: any, value: any, callback: any) => {
  if ((value as string).toUpperCase() === identifyCode.value) {
    return true
  } else {
    return false
  }
}

// 使用登录 API 方法
const useLoginApi = UseLoginApi()
// 使用全局状态管理
const useUserInfoStore = useMyStore()
// 定义组件的响应式状态
interface State {
  loading: boolean;
  formData: LoginState;
}

const state: State = reactive({
  loading: false,
  formData: {
    username: "admin",
    password: "admin",
    code: identifyCode.value,
  },
})

// 定义登录逻辑
const login = () => {
  let data: LoginState = {
    username: state.formData.username,
    password: state.formData.password,
    code: state.formData.code
  }
  useLoginApi.login(data)
    .then((res) => {
      console.log(res);
      // 登录成功后的处理逻辑
      Session.set('token', res.data.token)
      Session.set('username', res.data.userInfo.user)
      useUserInfoStore.setUserInfo(res.data.userInfo)
      useUserInfoStore.setIsLogin(true)

      router.push({
        name: 'index'
      })
    })
    .catch((err) => {
      console.log(err);
    })
}

// 创建表单的引用
const formRef = ref<FormInstance>();
// 定义表单验证规则
const formRules = reactive<FormRules<typeof state.formData>>({
  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
  password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
  code: [{ required: true, validator: checkCodeFun, message: '请输入验证码', trigger: 'blur' }],
})

// 记住密码,true 或 false
const rem_pswd = ref(localStorage.getItem('rem_pswd'))

// 跳转页面的方法
const go_page = (path: string) => {
  router.push({
    name: 'login', // 替换为目标路由的名称或路径
  })
}

// 处理表单提交的方法
const handleSubmita = (formEl: FormInstance | undefined) => {
  if (!formEl) return
  formEl.validate((valid: any, fields: any) => {
    if (valid) {
      // 表单验证通过,执行登录逻辑
      login()
      console.log('login');
    } else {
      console.log('error submit!', fields)
    }
  })
}
</script>


<style scoped>
/* 可以添加一些样式 */
.loginForm {
  height: 400px;
  border: 1px solid;
  border-radius: 10px;
  text-align: center;
  background-color: #f3f3f3;
}

.loginForm .rloginTitle {
  font-size: 23px;
  margin: 5%;
}

.loginButton {
  width: 250px;
  margin: auto;
  margin-top: 20px;
}

.formInput {
  width: 80%;
  min-width: 50px;
}

.codeLine {
  display: flex;
}

.codeLine .codeInput {
  flex: 2;

}

.codeLine .codeShow {
  flex: 1;

}

.settings {
  display: flex;
  justify-content: space-between;
}

.settings label {
  flex: 1;
  font-size: 16px;
}

.settings .labelText {
  cursor: pointer;
}

.btn-base {
  width: 100%;
}

.btn-flex {
  display: flex;
  justify-content: center;
}
</style>

 

6.header登录部分代码

<template>
  <div class="top1">
    <div class="header">
      <!-- 头部主体 -->
      <div class="header_layout layout_width">
        <!-- logo部分 -->
        <div class="logo">
          <div class="pic">
            <!-- <img src=""> -->
          </div>
          <div class="pic_title" @click="goPage('index')">
            <div class="title1">XXXXXXXXXXX</div>
            <div class="title2">XXX系统</div>
          </div>
        </div>
        <!-- 搜索,按钮部分 -->
        <div class="options">
          <div class="options_layout">
            <div class="search_box">
              <el-row>
                <el-input v-model="search" class="w-50 m-2" placeholder="输入查找内容" :prefix-icon="Search" />
              </el-row>
            </div>
            <div class="login_box">
              <div class="user" v-if="useMyStore().isLogin">
                <el-dropdown>
                  <span>{{ Session.get('username') || useMyStore().userForm.user }}</span>
                  <template #dropdown>
                    <el-dropdown-menu>
                      <el-dropdown-item @click="goPage('personalSetting')">个人中心</el-dropdown-item>
                      <el-dropdown-item divided @click="goPage('logout')">退出登录</el-dropdown-item>
                    </el-dropdown-menu>
                  </template>
                </el-dropdown>
              </div>
              <div class="login" v-else>
                <el-button type="primary" @click="goPage('login')">login</el-button>
              </div>
            </div>

          </div>
        </div>

      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { Search } from '@element-plus/icons-vue';
import router from '@/router';
import { Session } from '@/utils/storage';
import { useMyStore } from '@/stores/states';
import { ElMessageBox, ElMessage } from 'element-plus';
import 'element-plus/dist/index.css';

// 搜索关键词的响应式引用
const search = ref('');

// 退出登录
const logout = (pageUrl: string) => {
  console.log('logout', pageUrl);

  // 弹出确认对话框
  ElMessageBox.confirm(
    '确认退出登录?',
    '提示',
    {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'warning',
    }
  )
    .then(() => {
      // 清空本地存储
      Session.clear();
      // 设置全局状态中的登录状态为 false
      useMyStore().setIsLogin(false);
      // 跳转到指定页面
      router.push({ name: pageUrl });

      // 提示退出成功
      ElMessage({
        type: 'success',
        message: '退出成功!',
      });
    })
    .catch(() => {
      // 提示取消退出
      ElMessage({
        type: 'info',
        message: '取消退出!',
      });
    });
};

// 跳转页面的方法
const goPage = (pageUrl: string) => {
  if (pageUrl === 'logout') {
    // 如果是退出登录,调用退出登录函数
    logout('index');
  } else if (pageUrl === 'login') {
    // 如果是登录页,带上额外的查询参数
    router.push({
      name: pageUrl,
      query: {
        path: 'login',
      },
    });
  } else {
    console.log(pageUrl);
    try {
      // 其他页面直接跳转
      router.push({
        name: pageUrl,
      });
    } catch (err) {
      console.log(err);
    }
  }
};
</script>


<style scoped>
/* 整体头部颜色宽度设置 */
.header {
  z-index: 9;
  background-color: rgba(17, 101, 172, 1);
  /* background: linear-gradient(rgba(17, 101, 172, 0.8), rgba(17, 101, 172, 0.5)); */
  height: 80px;
}

/* 自定义 头部内容排版样式 */
.header_layout {
  height: 80px;
  position: relative;
}

.logo,
.content,
.options {
  height: 100%;
}

.logo {
  color: rgb(255, 255, 255);
  width: 350px;
  cursor: pointer;
}

.logo .pic {
  display: inline-block;
  width: 80px;
  height: 80px;
  position: absolute;
  left: 0;
}

.logo .pic img {
  height: 80px;
}

.logo .pic_title {
  width: 250px;
  height: 80px;
}

.title1,
.title2 {
  position: absolute;
  left: 85px;
  /* 与 .logo .pic_title 的 left 值相同 */
}

.title1 {
  font-size: 26px;
  font-weight: 100;
  height: 45px;
  line-height: 45px;
}

.title2 {
  top: 45px;
  font-size: 16px;
  font-weight: 100;
  height: 35px;
  line-height: 35px;
}

.options {
  position: absolute;
  top: 0;
  right: 0;
  width: 400px;
}

.options .options_layout {
  display: flex;
  align-items: center;
  height: 100%;
}

.options .options_layout .search_box {
  flex: 3;
}

.options .options_layout .login_box {
  flex: 1;
}

.options .options_layout .login_box .login {}

.options .options_layout .login_box .user {
  border: 1px solid;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  background-color: rgb(80 188 221);
  margin: auto;
  overflow: hidden;
  cursor: pointer;
}

.options .options_layout .login_box .user span {
  line-height: 50px;
  font-size: 18px;
  border: none;
  outline: none;
}

.example-showcase .el-dropdown-link {
  cursor: pointer;
  color: var(--el-color-primary);
  display: flex;
  align-items: center;
}
</style>

二.部分代码截取

1.login登录页

// 定义登录逻辑
const login = () => {
  let data: LoginState = {
    username: state.formData.username,
    password: state.formData.password,
    code: state.formData.code
  }
  useLoginApi.login(data)
    .then((res) => {
      console.log(res);
      // 登录成功后的处理逻辑
      Session.set('token', res.data.token)
      Session.set('username', res.data.userInfo.user)
      useUserInfoStore.setUserInfo(res.data.userInfo)
      useUserInfoStore.setIsLogin(true)

      router.push({
        name: 'index'
      })
    })
    .catch((err) => {
      console.log(err);
    })
}

2.header部分


// 退出登录
const logout = (pageUrl: string) => {
  console.log('logout', pageUrl);

  // 弹出确认对话框
  ElMessageBox.confirm(
    '确认退出登录?',
    '提示',
    {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'warning',
    }
  )
    .then(() => {
      // 清空本地存储
      Session.clear();
      // 设置全局状态中的登录状态为 false
      useMyStore().setIsLogin(false);
      // 跳转到指定页面
      router.push({ name: pageUrl });

      // 提示退出成功
      ElMessage({
        type: 'success',
        message: '退出成功!',
      });
    })
    .catch(() => {
      // 提示取消退出
      ElMessage({
        type: 'info',
        message: '取消退出!',
      });
    });
};

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿online

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值