跟着迪佬的vue系列文章,自己搭建了前端,特此记录一下,git项目地址
https://gitee.com/WylLoveX/bs-web.git
迪佬文章地址
前端新手Vue3+Vite+Ts+Pinia+Sass项目指北系列文章 —— 系列文章(目录)-CSDN博客
要求node>18,全局安装yarn
npm install -g yarn
创建项目
yarn create vite
启动测试
package.json的scripts属性中配置了启动命令
我们按照配置执行yarn dev就可以启动了
访问界面如下
yarnadd element-plus
在main.ts中添加内容,原来的配置暂时不动
import { createApp } from 'vue'
import './style.css'
//引入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
//引入element-plus
app.use(ElementPlus)
app.mount('#app')
安装依赖到开发环境
npm install -D unplugin-vue-components unplugin-auto-import
在vite.config.ts中配置导入的这两个组件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 自动引入element-plus
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
// 自动引入element-plus
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
yarn add vite-plugin-svg-icons
yarn add vite-plugin-vue-setup-extend
yarn add vite-plugin-html
yarn add vite-plugin-top-level-await
在项目根目录添加vite.plugins.ts
import path from 'path'
import vue from '@vitejs/plugin-vue'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import setupExtend from 'vite-plugin-vue-setup-extend'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { createHtmlPlugin } from 'vite-plugin-html'
import topLevelAwait from 'vite-plugin-top-level-await'
export default function createVitePlugins(viteEnv, isBuild = false) {
const { VITE_GLOB_APP_TITLE } = viteEnv
const vitePlugins = [
vue(),
setupExtend(),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]'
}),
Components({
resolvers: [ElementPlusResolver()]
}),
AutoImport({
// resolvers: [ElementPlusResolver()],
// targets to transform
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
// global imports to register
imports: [
// 插件预设支持导入的api
'vue',
'vue-router',
'pinia'
// 自定义导入的api
],
// Generate corresponding .eslintrc-auto-import.json file.
// eslint globals Docs - https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals
eslintrc: {
enabled: false, // 默认false, true启用。生成一次就可以,避免每次工程启动都生成
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
},
// Filepath to generate corresponding .d.ts file.
// Defaults to './auto-imports.d.ts' when `typescript` is installed locally.
// Set `false` to disable.
dts: './auto-imports.d.ts',
}),
createHtmlPlugin({
minify: isBuild,
inject: {
data: {
title: VITE_GLOB_APP_TITLE
}
}
}),
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: i => `__tla_${i}`
})
]
return vitePlugins
};
import path from 'path'
import { defineConfig, loadEnv } from 'vite'
import createVitePlugins from './vite.plugins'
const base_url = 'xxx'
// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd())
return {
plugins: createVitePlugins(env, command === 'build'),
base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
server: {
host: "0.0.0.0",
proxy: {
'/xxx': {
target: base_url,
changeOrigin: true,
}
}
}
}
})
项目初始化的时候,生成了一些测试文件
删除components目录下的HelloWorld.vue文件
删除src目录下的style.css文件,然后删除main.ts中对style.css文件的引用
import { createApp } from 'vue'
//引入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
//引入element-plus
app.use(ElementPlus)
app.mount('#app')
修改App.vue文件
<template>
<div>
<el-row class="mb-4">
<el-button>Default</el-button>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
</el-row>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
运行yarn dev,打开页面显示如下
yarn add sass
在src文件下新增styles文件夹,新增 variables.module.scss
文件
// base color
$blue: #324157;
$light-blue: #3a71a8;
$red: #c03639;
$pink: #e65d6e;
$green: #30b08f;
$tiffany: #4ab7bd;
$yellow: #fec171;
$panGreen: #30b08f;
// 默认菜单主题风格
$base-menu-color: #bfcbd9;
$base-menu-color-active: #f4f4f5;
$base-menu-background: #304156;
$base-logo-title-color: #fff;
$base-menu-light-color: #697280;
$base-menu-light-color-active: #697280;
$base-menu-light-background: #fff;
$base-logo-light-title-color: #001529;
$base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover: #001528;
$base-sub-menu-light-background: #fff;
$base-sub-menu-light-active: #f2f3f5;
$base-sub-menu-light-hover: #f7f9fa;
$--color-primary: #409eff;
$--color-success: #67c23a;
$--color-warning: #e6a23c;
$--color-danger: #f56c6c;
$--color-info: #909399;
$base-sidebar-width: 260px;
新增 transition.scss
文件
// global transition css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all 0.5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all 0.5s;
}
.breadcrumb-leave-active {
position: absolute;
}
新增 element.scss
文件
#app {
& .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
& .theme-dark .el-sub-menu .el-menu-item {
background-color: $base-sub-menu-background !important;
&:hover {
background-color: $base-sub-menu-hover !important;
}
}
.el-button.is-text {
padding-right: 0;
padding-left: 0;
}
.el-table {
margin: 10px 0;
}
.scp-table .el-table {
font-size: 12px;
--el-table-border: transparent;
.el-table__inner-wrapper::before {
display: none;
}
.el-table__header-wrapper {
border-top: 1px solid #f2f3f5;
th {
height: 50px !important;
font-size: 12px !important;
color: #909399 !important;
background: #fff !important;
font-weight: normal !important;
}
}
.el-table__body-wrapper {
font-weight: bold;
overflow: hidden;
border: 1px solid #f2f3f5;
border-bottom: 0;
border-radius: 10px;
}
.el-table__cell {
padding: 8px 0;
.cell {
/* height: auto; */
line-height: 30px;
}
/* background: #f4f9fa; */
}
.el-table__row--striped .el-table__cell {
/* background: #f2f3f5; */
}
.el-empty__description p {
font-weight: normal;
font-size: 12px;
}
}
.el-dialog__body {
padding-top: 15px;
padding-bottom: 15px;
}
.el-input-number {
width: 100%;
.el-input__inner {
text-align: left;
}
}
.el-drawer__header {
margin-bottom: 0;
}
}
新增 index.scss
文件
@import './variables.module';
@import './transition';
@import './element';
:root {
font-size: 14px;
font-weight: 400;
background-color: #fff;
}
html,
body,
#app {
width: 100%;
height: 100%;
}
a {
text-decoration: none;
}
.flex {
display: flex;
}
.flex-sb {
display: flex;
justify-content: space-between;
}
.flex-c {
display: flex;
justify-content: center;
}
.flex-sa {
display: flex;
justify-content: space-around;
}
.flex-end {
display: flex;
justify-content: flex-end;
}
.flex-align {
display: flex;
align-items: center;
}
.flex-wrap {
display: flex;
flex-wrap: wrap;
}
.flex-column {
display: flex;
flex-direction: column;
}
.flex-column-sb {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.flex-column-align-center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.flex-align-sb {
display: flex;
justify-content: space-between;
align-items: center;
}
.flex-align-sa {
display: flex;
justify-content: space-around;
align-items: center;
}
.flex-column-sa {
display: flex;
flex-direction: column;
justify-content: space-around;
}
.flex-align-ai {
display: flex;
align-items: center;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-sba {
display: flex;
justify-content: space-between;
align-items: center;
}
yarn add normalize.css
配置main.ts
import { createApp } from 'vue'
import App from './App.vue'
//引入样式配置
import 'normalize.css'
import './styles/index.scss'
//引入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
//引入element-plus
app.use(ElementPlus)
app.mount('#app')
修改App.vue
<template>
<div style="height: 100vh" class="flex-c flex-align">
<el-row class="mb-4">
<el-button>Default</el-button>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
</el-row>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
启动后界面如下
yarn add @types/vue-router
src目录下新增router文件目录
新增router.ts文件
import { RouteConfig } from 'vue-router';
// 公共路由
export const constantRoutes: RouteConfig[] = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'Login',
meta: { title: 'login' }
},
]
新增index.ts文件
import { createRouter, createWebHistory } from 'vue-router'
import { constantRoutes } from './route'
const router = createRouter({
history: createWebHistory('/'),
routes: constantRoutes
})
export default router
配置main.ts
import { createApp } from 'vue'
import App from './App.vue'
//引入样式配置
import 'normalize.css'
import './styles/index.scss'
//引入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//引入路由
import router from './router'
const app = createApp(App)
//引入element-plus
app.use(ElementPlus)
//引入路由
app.use(router)
app.mount('#app')
在src目录下新增views/login目录,在这个目录下新增index.vue
<template>
<div>
login
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
</style>
修改App.vue
<template>
<router-view></router-view>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
启动访问首页自动跳转到登录页面
使用element-ui的布局,结合路由做一个登录页,简单判断token
新增src/utils/cache目录,在下面新增index.ts
/**
* 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) {
window.sessionStorage.setItem(key, JSON.stringify(val));
},
// 获取临时缓存
get(key: string) {
let json: any = window.sessionStorage.getItem(key);
return JSON.parse(json);
},
// 移除临时缓存
remove(key: string) {
window.sessionStorage.removeItem(key);
},
// 移除全部临时缓存
clear() {
window.sessionStorage.clear();
},
};
修改src/router/index.ts文件
import { createRouter, createWebHistory } from 'vue-router'
import { ElMessage } from "element-plus";
import { constantRoutes } from './route'
import { Local } from '@/utils/cache'
const router = createRouter({
history: createWebHistory('/'),
routes: constantRoutes
})
/**
* 全局前置路由守卫,每一次路由跳转前都进入这个 beforeEach 函数
*/
router.beforeEach((to, _from, next) => {
if (to.path == '/login') {
// 登录或者注册才可以往下进行
next();
} else {
// 获取 token
const token = Local.get('token');
// token 不存在
if (token === null || token === '') {
ElMessage.error('登录失败,请先登录');
next('/login');
} else {
next();
}
}
});
export default router
新建src/utils/tools文件夹,新增index.ts
/**
* @description 文档注册enter事件
* @param {any} cb
* @return {void}
*/
export const handleEnter = (cb: Function): void => {
document.onkeydown = e => {
const ev: KeyboardEventInit = window.event || e;
if (ev.keyCode === 13) {
cb();
}
};
};
// 其中 KeyboardEventInit 为内置,以下是代码截取
interface KeyboardEventInit extends EventModifierInit {
/** @deprecated */
charCode?: number;
code?: string;
isComposing?: boolean;
key?: string;
/** @deprecated */
keyCode?: number;
location?: number;
repeat?: boolean;
}
在src/assets目录下新增图片login-bg.png
修改登录页面src/views/login/index.vue
<template>
<div class="login-main" v-loading="loading" element-loading-text="Logging in...">
<div class="login-form">
<div class="logo flex-c flex-align">
<!-- <img style="height: 50px;" src="../../assets/logo.png" alt="logo" /> -->
</div>
<el-form :model="loginForm" label-position="left" label-width="100px">
<el-form-item label="Username:">
<el-input v-model="loginForm.username" />
</el-form-item>
<el-form-item label="Password:">
<el-input type="password" v-model="loginForm.password" />
</el-form-item>
</el-form>
<div class="footer-btn flex-c">
<button class="login-btn" @click="loginClick">login</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from "vue-router";
import { Local } from "@/utils/cache";
import { handleEnter } from "@/utils/tools";
const router = useRouter();
const loading = ref(false);
const loginForm = ref({
username: "test",
password: "1234",
});
const loginClick = () => {
loading.value = true;
const accessToken = Local.get("token");
if (!accessToken) {
Local.set("token", "abc");
userLoginFunc();
} else {
userLoginFunc();
}
};
const userLoginFunc = () => {
loading.value = false;
// userStore.SET_USER_INFO(res)
router.push({ path: "/home" });
};
onMounted(() => {
handleEnter(loginClick);
});
</script>
<style lang="scss" scoped>
.logo {
margin-bottom: 20px;
span {
margin-left: 8px;
font-size: 20px;
color: #003574;
font-weight: 700;
}
}
.login-main {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: url('../../assets/login-bg.png') no-repeat;
background-size: 100% 100%;
}
.login-form {
position: relative;
top: -110px;
background: #000;
border-radius: 8px;
::v-deep(.el-form-item__label) {
color: #fff;
}
}
.login-btn {
position: relative;
z-index: 1;
display: inline-flex;
justify-content: center;
align-items: center;
overflow: hidden;
width: 100%;
height: 40px;
font-size: 14px;
font-family: alliance, mono, sans-serif;
color: #fff;
background: transparent;
border-radius: 0;
transition:
background 0.3s ease-in-out,
border-color 0.3s ease-in-out,
color 0.3s ease-in-out;
font-weight: 600;
line-height: 40px;
letter-spacing: 0.07em;
text-transform: uppercase;
font-feature-settings: "salt" on, "ss01" on, "ss02" on;
transition-property: background, border-color, color;
transition-duration: 0.3s, 0.3s, 0.3s;
transition-timing-function: ease-in-out, ease-in-out, ease-in-out;
}
.login-btn::before {
position: absolute;
top: 1px;
left: 1px;
z-index: -1;
display: block;
width: calc(100% - 2px);
height: calc(100% - 2px);
background: #000;
transition: background 0.3s ease-in-out;
content: "";
transform: translate3d(0, 0, 0);
}
.login-btn::after {
position: absolute;
top: 0;
left: 0;
z-index: -3;
display: block;
width: 100%;
height: 100%;
background:
linear-gradient(
269.16deg,
#9867f0 -15.83%,
#3bf0e4 -4.97%,
#33ce43 15.69%,
#b2f4b6 32.43%,
#ffe580 50.09%,
#ff7571 67.47%,
#ff7270 84.13%,
#ea5dad 105.13%,
#c2a0fd 123.24%
);
background-position: 58% 50%;
background-size: 500%;
content: "";
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
animation: gradient-shift 30s ease infinite;
}
@keyframes gradient-shift {
0% {
background-position: 58% 50%;
}
25% {
background-position: 100% 0%;
}
75% {
background-position: 10% 50%;
}
100% {
background-position: 58% 50%;
}
}
.login-btn:hover::before {
background: transparent;
}
.login-btn:hover {
cursor: pointer;
color: #000;
}
</style>
创建目录src/views/layout,在下面新建index.vue
<template>
<div class="common-layout">
<el-container>
<el-header class="flex-c flex-align header"> Header </el-header>
<el-container>
<el-aside class="flex-c flex-align h-100 aside"> Aside </el-aside>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.header {
height: 6vh;
background-color: #b3c0d1;
}
.aside {
background-color: #d3dce6;
width: 200px;
height: 94vh;
}
.el-main {
overflow: hidden;
padding: 15px;
}
</style>
新增src/views/home目录,新增index.vue
<template>
<div class="flex-c flex-align h-100">
<el-button type="primary" @click="goRouter('/news')">go news</el-button>
<el-button type="primary" @click="goRouter('/user')">go user</el-button>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const goRouter = (path: string): void => {
router.push(path)
}
</script>
创建src/views/news目录,新增index.vue
<template>
<div>新闻</div>
</template>
<script setup lang="ts">
</script>
创建src/views/user目录,新增index.vue页面
<template>
<div>用户</div>
</template>
<script>
export default {
name: "index"
}
</script>
<style scoped>
</style>
创建src/router/modules目录
新增user.ts文件
import { RouteRecordRaw } from 'vue-router';
const Layout = () => import('@/views/layout/index.vue')
const user: RouteRecordRaw = {
path: "/user",
component: Layout,
name: "User",
meta: { title: "user" },
redirect: "/user/index",
children: [
{
path: "index",
component: () => import("@/views/user/index.vue"),
name: "UserIndex",
meta: { title: "UserIndex" },
},
],
};
export { user }
新增news.ts
import { RouteRecordRaw } from 'vue-router';
const Layout = () => import('@/views/layout/index.vue')
const user: RouteRecordRaw = {
path: "/news",
component: Layout,
name: "News",
meta: { title: "news" },
redirect: "/news/index",
children: [
{
path: "index",
component: () => import("@/views/news/index.vue"),
name: "NewsIndex",
meta: { title: "NewsIndex" },
},
],
};
export { user }
然后修改route.ts将这两个路由以及home页面引入
import type { RouteRecordRaw } from 'vue-router'
// 导入模块文件
const modulesFiles = import.meta.glob('./modules/*.ts', { eager: true })
// 获取模块路由
const modulesRoutes = Object.values(modulesFiles)
// 遍历模块路由,将路由记录添加到modules数组中
const modules: RouteRecordRaw[] = modulesRoutes.reduce(
(routeArr: RouteRecordRaw[], routeItem: any) => {
// 获取默认路由
const defaultRoute: RouteRecordRaw[] = Object.values(routeItem)
// 将默认路由添加到modules数组中
routeArr.push(...defaultRoute)
return routeArr
},
[],
) as RouteRecordRaw[]
const Layout = () => import('@/views/layout/index.vue')
// 公共路由
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/login',
},
{
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'Login',
meta: { title: 'login' },
},
{
path: '/home',
component: Layout,
name: 'Home',
meta: { title: 'home' },
redirect: '/home/index',
children: [
{
path: 'index',
component: () => import('@/views/home/index.vue'),
name: 'HomeIndex',
meta: { title: 'homeIndex' },
},
],
},
...modules,
]
启动后登录跳转到如下页面,在localStorage中可以看到登录页设置的token缓存和home页面内容
点击按钮会跳转对应页面
yarn add pinia
在src目录下新增store文件夹
新增index.ts文件
import { createPinia } from 'pinia'
const store = createPinia()
export default store
在store目录下新增modules目录,在这个目录下创建user.ts
import { defineStore } from 'pinia'
import { Local } from '@/cache'
const useUserStore = defineStore('user', {
state: () => ({
userInfo: {
id: '',
username: '',
nickname: '',
roles: []
} // 用户信息
}),
actions: {
SET_USER_INFO(info: any) {
this.userInfo = info
},
LOGOUT() {
this.userInfo = {
id: '',
username: '',
nickname: '',
roles: []
}
Local.clear()
}
}
})
export default useUserStore
main.ts中新增配置
import { createApp } from 'vue'
import App from './App.vue'
//引入样式配置
import 'normalize.css'
import './styles/index.scss'
//引入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//引入路由
import router from './router'
//引入store
import store from './store'
const app = createApp(App)
//引入element-plus
app.use(ElementPlus)
//引入路由
app.use(router)
//引入store
app.use(store)
app.mount('#app')
新增src/store/modules/user.ts文件
import { defineStore } from 'pinia'
import { Local } from '@/utils/cache'
const useUserStore = defineStore('user', {
state: () => ({
userInfo: {
id: '',
username: '',
nickname: '',
roles: []
} // 用户信息
}),
actions: {
SET_USER_INFO(info: any) {
this.userInfo = info
},
LOGOUT() {
this.userInfo = {
id: '',
username: '',
nickname: '',
roles: []
}
Local.clear()
}
}
})
export default useUserStore
修改src/views/login/index.vue
<template>
<div class="login-main" v-loading="loading" element-loading-text="Logging in...">
<div class="login-form">
<div class="logo flex-c flex-align">
<!-- <img style="height: 50px;" src="../../assets/logo.png" alt="logo" /> -->
</div>
<el-form :model="loginForm" label-position="left" label-width="100px">
<el-form-item label="Username:">
<el-input v-model="loginForm.username" />
</el-form-item>
<el-form-item label="Password:">
<el-input type="password" v-model="loginForm.password" />
</el-form-item>
</el-form>
<div class="footer-btn flex-c">
<button class="login-btn" @click="loginClick">login</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from "vue-router";
import { Local } from "@/utils/cache";
import { handleEnter } from "@/utils/tools";
import useUserStore from "@/store/modules/user";
const userStore = useUserStore()
const router = useRouter();
const loading = ref(false);
const loginForm = ref({
username: "test",
password: "1234",
});
const loginClick = () => {
loading.value = true;
const accessToken = Local.get("token");
if (!accessToken) {
Local.set("token", "abc");
userLoginFunc();
} else {
userLoginFunc();
}
};
const userLoginFunc = () => {
loading.value = false;
// 新增
userStore.SET_USER_INFO({
id: 1,
username: "test",
nickname: "测试账号",
roles: ["admin", "test"],
});
router.push({ path: "/home" });
};
onMounted(() => {
handleEnter(loginClick);
});
</script>
<style lang="scss" scoped>
.logo {
margin-bottom: 20px;
span {
margin-left: 8px;
font-size: 20px;
color: #003574;
font-weight: 700;
}
}
.login-main {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: url('../../assets/login-bg.png') no-repeat;
background-size: 100% 100%;
}
.login-form {
position: relative;
top: -110px;
background: #000;
border-radius: 8px;
::v-deep(.el-form-item__label) {
color: #fff;
}
}
.login-btn {
position: relative;
z-index: 1;
display: inline-flex;
justify-content: center;
align-items: center;
overflow: hidden;
width: 100%;
height: 40px;
font-size: 14px;
font-family: alliance, mono, sans-serif;
color: #fff;
background: transparent;
border-radius: 0;
transition:
background 0.3s ease-in-out,
border-color 0.3s ease-in-out,
color 0.3s ease-in-out;
font-weight: 600;
line-height: 40px;
letter-spacing: 0.07em;
text-transform: uppercase;
font-feature-settings: "salt" on, "ss01" on, "ss02" on;
transition-property: background, border-color, color;
transition-duration: 0.3s, 0.3s, 0.3s;
transition-timing-function: ease-in-out, ease-in-out, ease-in-out;
}
.login-btn::before {
position: absolute;
top: 1px;
left: 1px;
z-index: -1;
display: block;
width: calc(100% - 2px);
height: calc(100% - 2px);
background: #000;
transition: background 0.3s ease-in-out;
content: "";
transform: translate3d(0, 0, 0);
}
.login-btn::after {
position: absolute;
top: 0;
left: 0;
z-index: -3;
display: block;
width: 100%;
height: 100%;
background:
linear-gradient(
269.16deg,
#9867f0 -15.83%,
#3bf0e4 -4.97%,
#33ce43 15.69%,
#b2f4b6 32.43%,
#ffe580 50.09%,
#ff7571 67.47%,
#ff7270 84.13%,
#ea5dad 105.13%,
#c2a0fd 123.24%
);
background-position: 58% 50%;
background-size: 500%;
content: "";
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
animation: gradient-shift 30s ease infinite;
}
@keyframes gradient-shift {
0% {
background-position: 58% 50%;
}
25% {
background-position: 100% 0%;
}
75% {
background-position: 10% 50%;
}
100% {
background-position: 58% 50%;
}
}
.login-btn:hover::before {
background: transparent;
}
.login-btn:hover {
cursor: pointer;
color: #000;
}
</style>
登录成功后,看到localStorage中有数据
数据持久化可以帮助确保用户的数据在页面重新加载或用户会话结束后仍然保持完整性和可访问性
在引入依赖之前,先测试一下我们设置的userInfo的数据能否正常获取
修改src/views/home/index.vue
<template>
<div class="flex-c flex-align h-100">
<el-button type="primary" @click="goRouter('/news')">go news</el-button>
<el-button type="primary" @click="goRouter('/user')">go user</el-button>
<el-button type="primary" @click="getUserInfo()">get user info</el-button>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
const router = useRouter()
const goRouter = (path: string): void => {
router.push(path)
}
const getUserInfo = ():void=>{
let userInfo = userStore.userInfo;
console.log(userInfo)
}
</script>
刷新页面后点击获取userInfom,发现数据都为空
yarn add pinia-plugin-persistedstate
修改src/store/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedState from 'pinia-plugin-persistedstate'
const store = createPinia()
store.use(piniaPluginPersistedState)
export default store
修改src/store/modules/user.ts,配置持久化
import { defineStore } from 'pinia'
import { Local } from '@/utils/cache'
const useUserStore = defineStore('user', {
state: () => ({
userInfo: {
id: '',
username: '',
nickname: '',
roles: []
} // 用户信息
}),
persist: true,//持久化配置
actions: {
SET_USER_INFO(info: any) {
this.userInfo = info
},
LOGOUT() {
this.userInfo = {
id: '',
username: '',
nickname: '',
roles: []
}
Local.clear()
}
}
})
export default useUserStore
此时在home页面获取userInfo
yarn add axios
在src/utils/tools目录下新增user.ts
import useUserStore from '../../store/modules/user'
import { Local } from '@/utils/cache'
const userStore = useUserStore()
export function logout() {
userStore.LOGOUT()
setTimeout(() => {
window.location.href = '/login'
}, 500)
}
export function setToken(token: string) {
Local.set('token', token)
}
export function getToken() {
const token = Local.get('token')
return token
}
新建src/utils/request目录
新增config.ts
const TEST_BASE_URL = '/mock'
const API_BASE_URL = '/api'
const AUTH_BASE_URL = '/auth'
const TIME_OUT = 30 * 1000
export {
TEST_BASE_URL,
API_BASE_URL,
AUTH_BASE_URL,
TIME_OUT
}
新增types.ts
import type { AxiosResponse, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'
export interface BaseRequestInterceptors<T = AxiosResponse<any>> {
requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
requestInterceptorCatch?: (error: any) => any
responseInterceptor?: (res: T) => T
responseInterceptorCatch?: (error: any) => any
}
export interface BaseRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: BaseRequestInterceptors<T>
loading?: boolean | string
noToken?: boolean
}
新增http.ts
import axios from "axios";
import type { AxiosInstance, InternalAxiosRequestConfig } from "axios";
import { BaseRequestConfig, BaseRequestInterceptors } from "./types";
import { ElLoading, ElMessage, ElMessageBox } from "element-plus";
import { LoadingInstance } from "element-plus/es/components/loading/src/loading";
import { getToken, logout } from "@/utils/tools/user";
const DEFAULT_LOADING = false;
let isReLogin = false;
class BaseRequest {
// axios 实例
instance: AxiosInstance;
interceptors?: BaseRequestInterceptors;
showLoading: boolean | string;
loading?: LoadingInstance;
constructor(config: BaseRequestConfig) {
this.instance = axios.create(config);
this.showLoading = config.loading ?? DEFAULT_LOADING;
this.interceptors = config.interceptors;
// 使用拦截器
// 1. 从config中取出的拦截器是对应的实例的拦截器
this.instance.interceptors.request.use(
this.interceptors?.requestInterceptor,
this.interceptors?.requestInterceptorCatch
);
this.instance.interceptors.response.use(
this.interceptors?.responseInterceptor,
this.interceptors?.responseInterceptorCatch
);
// 2.添加所有的实例都有的拦截器
this.instance.interceptors.request.use(
(config) => {
if (config.params && config.params.responseType) {
config.responseType = config.params.responseType;
delete config.params.responseType;
}
if (
(config.method === "post" || config.method === "put") &&
config.params
) {
if (config.params.formData) {
config.data = config.params.formData;
} else {
config.data = { ...config.params };
}
delete config.params;
}
// console.log('所有的实例都有的拦截器: 请求成功拦截')
if (this.showLoading) {
const loadingText =
typeof this.showLoading == "boolean" ? "加载中" : this.showLoading;
this.loading = ElLoading.service({
lock: true,
text: loadingText + "...",
background: "rgba(0, 0, 0, 0.5)",
});
}
return config;
},
(err) => {
console.log("所有的实例都有的拦截器: 请求失败拦截");
return err;
}
);
this.instance.interceptors.response.use(
(res) => {
// 将loading移除
this.loading?.close();
if (res.status !== 200) return res;
return res.data;
},
(err) => {
// console.log('所有的实例都有的拦截器: 响应失败拦截')
// 将loading移除
this.loading?.close();
// 例子: 判断不同的HttpErrorCode显示不同的错误信息
switch (err.response.status) {
case 400:
ElMessage.error("请求错误(400)");
break;
case 401:
ElMessage.error("未授权,请重新登录(401)");
logout();
break;
case 403:
ElMessage.error("拒绝访问(403)");
break;
case 404:
ElMessage.error("请求出错(404)");
break;
case 408:
ElMessage.error("请求超时(408)");
break;
case 500:
ElMessage.error("服务器错误(500)");
break;
case 501:
ElMessage.error("服务未实现(501)");
break;
case 502:
ElMessage.error("网络错误(502)");
break;
case 503:
ElMessage.error("服务不可用(503)");
break;
case 504:
ElMessage.error("网络超时(504)");
break;
case 505:
ElMessage.error("HTTP版本不受支持(505)");
break;
default: {
ElMessage.error(`连接出错(${err.response.status})!`);
}
}
return err;
}
);
}
request<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return new Promise((resolve, reject) => {
// 1.单个请求对请求config的处理
if (config.interceptors?.requestInterceptor) {
// 对config 进行转化
config = config.interceptors.requestInterceptor(
config as InternalAxiosRequestConfig
);
}
// 携带token的拦截
if (!config.noToken && getToken()) {
config.headers = {
Authorization: getToken(),
};
}
// 2.判断是否需要显示loading
if (config.loading) {
this.showLoading = config.loading;
}
this.instance
.request<any, T>(config)
.then((res: any) => {
// 1.单个请求对数据的处理
if (config.interceptors?.responseInterceptor) {
res = config.interceptors.responseInterceptor(res);
}
// 2.将showLoading设置true, 这样不会影响下一个请求
this.showLoading = DEFAULT_LOADING;
if (res.status !== 200) {
if (config.responseType == "blob") {
resolve(res);
return;
}
let errMsg = res.message || "请求失败";
if (res.code == "501") errMsg = "数据中存在敏感词汇, 请修改!";
if (res.status == 401) {
if (!isReLogin) {
isReLogin = true;
// prettier-ignore
errMsg = res.message || '登录状态已过期,您可以继续留在该页面,或者重新登录'
ElMessageBox.confirm(errMsg, "系统提示", {
confirmButtonText: "重新登录",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
isReLogin = false;
logout();
})
.catch(() => {
isReLogin = false;
});
}
reject(res);
return;
}
ElMessage({
message: errMsg,
type: "error",
});
reject(res);
} else {
resolve(res);
}
})
.catch((err) => {
// 将showLoading设置true, 这样不会影响下一个请求
this.showLoading = DEFAULT_LOADING;
reject(err);
return err;
});
});
}
get<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "GET" });
}
post<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "POST" });
}
put<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "PUT" });
}
delete<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "DELETE" });
}
patch<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "PATCH" });
}
}
export default BaseRequest;
新增index.ts
import BaseRequest from './http'
import { TIME_OUT } from './config'
const testRequest = new BaseRequest({
timeout: TIME_OUT,
interceptors: {
requestInterceptor: config => {
return config
},
requestInterceptorCatch: err => {
// console.log('请求失败的拦截')
return err
},
responseInterceptor: res => {
// console.log('响应成功的拦截')
return res
},
responseInterceptorCatch: err => {
const errRes = err.response
if (errRes) {
return {
status: errRes.status,
message: errRes.data.message
}
}
return err
}
}
})
export default testRequest
yarn add mockjs -D
yarn add vite-plugin-mock@2.9.6 -D
在根目录创建mock文件夹,创建index.ts文件
import type { MockMethod } from 'vite-plugin-mock'
const mockItems: MockMethod[] = [
{
url: '/mock/login',
method: 'post',
response: () => {
return {
status: 200,
data: {
access_token: 'abc',
},
message: 'success',
}
},
},
]
export default mockItems
修改vite.plugin.ts新增配置,这里根据common判断是否启用mock,执行yarn serve就会使用mock,所以createVitePlugins函数增加一个参数command
import path from 'path'
import vue from '@vitejs/plugin-vue'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import setupExtend from 'vite-plugin-vue-setup-extend'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { createHtmlPlugin } from 'vite-plugin-html'
import topLevelAwait from 'vite-plugin-top-level-await'
import { viteMockServe } from "vite-plugin-mock";
export default function createVitePlugins(viteEnv, isBuild = false,command) {
const { VITE_GLOB_APP_TITLE } = viteEnv
const vitePlugins = [
vue(),
setupExtend(),
viteMockServe({
mockPath: "./mock",
localEnabled: command === "serve"
}),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]'
}),
Components({
resolvers: [ElementPlusResolver()]
}),
AutoImport({
// resolvers: [ElementPlusResolver()],
// targets to transform
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
// global imports to register
imports: [
// 插件预设支持导入的api
'vue',
'vue-router',
'pinia'
// 自定义导入的api
],
// Generate corresponding .eslintrc-auto-import.json file.
// eslint globals Docs - https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals
eslintrc: {
enabled: false, // 默认false, true启用。生成一次就可以,避免每次工程启动都生成
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
},
// Filepath to generate corresponding .d.ts file.
// Defaults to './auto-imports.d.ts' when `typescript` is installed locally.
// Set `false` to disable.
dts: './auto-imports.d.ts',
}),
createHtmlPlugin({
minify: isBuild,
inject: {
data: {
title: VITE_GLOB_APP_TITLE
}
}
}),
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: i => `__tla_${i}`
})
]
return vitePlugins
};
同时vite.config.ts中也要增加command参数
import path from 'path'
import { defineConfig, loadEnv } from 'vite'
import createVitePlugins from './vite.plugins'
// const base_url = 'xxx'
// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd())
return {
plugins: createVitePlugins(env, command === 'build',command),
base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
server: {
host: "0.0.0.0",
proxy: {
// '/xxx': {
// target: base_url,
// changeOrigin: true,
// }
}
}
}
})
package.json中也要新增serve的启动配置,这里复制scripts的serve就可以了
{
"name": "bs-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"serve": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@types/vue-router": "^2.0.0",
"axios": "^1.6.2",
"element-plus": "^2.4.3",
"normalize.css": "^8.0.1",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.0",
"sass": "^1.69.5",
"vite-plugin-html": "^3.2.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-top-level-await": "^1.4.0",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue": "^3.3.11"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"mockjs": "^1.1.0",
"typescript": "^5.2.2",
"unplugin-auto-import": "^0.17.2",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.8",
"vite-plugin-mock": "2.9.6",
"vue-tsc": "^1.8.25"
}
}
创建src/api/userApi.ts
import testRequest from '@/utils/request'
import { TEST_BASE_URL } from '@/utils/request/config'
// 登陆
export function userLogin(data = {}) {
return testRequest.post({
url: `${TEST_BASE_URL}/login`,
data,
})
}
修改src/views/login/index.vue
<template>
<div class="login-main" v-loading="loading" element-loading-text="Logging in...">
<div class="login-form">
<div class="logo flex-c flex-align">
<!-- <img style="height: 50px;" src="../../assets/logo.png" alt="logo" /> -->
</div>
<el-form :model="loginForm" label-position="left" label-width="100px">
<el-form-item label="Username:">
<el-input v-model="loginForm.username" />
</el-form-item>
<el-form-item label="Password:">
<el-input type="password" v-model="loginForm.password" />
</el-form-item>
</el-form>
<div class="footer-btn flex-c">
<button class="login-btn" @click="loginClick">login</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from "vue-router";
import { handleEnter } from "@/utils/tools";
import useUserStore from "@/store/modules/user";
//引入请求
import testRequest from "@/utils/request";
import { TEST_BASE_URL } from "@/utils/request/config";
import { userLogin } from '@/api/userApi'
import { getToken, setToken } from '@/utils/tools/user'
import { ElMessage } from 'element-plus'
const userStore = useUserStore();
const router = useRouter();
const loading = ref(false);
const loginForm = ref({
username: "test",
password: "1234",
});
const loginClick = (data = {}) => {
loading.value = true;
const accessToken = getToken();
if (!accessToken) {
userLogin()
.then((res) => {
setToken(res.data.access_token);
})
.then(() => {
userLoginFunc();
})
.catch((err) => {
ElMessage.error(err);
loading.value = false;
});
} else {
userLoginFunc();
}
};
const userLoginFunc = () => {
loading.value = false;
// 新增
userStore.SET_USER_INFO({
id: 1,
username: "test",
nickname: "测试账号",
roles: ["admin", "test"],
});
router.push({ path: "/home" });
};
onMounted(() => {
handleEnter(loginClick);
});
</script>
<style lang="scss" scoped>
.logo {
margin-bottom: 20px;
span {
margin-left: 8px;
font-size: 20px;
color: #003574;
font-weight: 700;
}
}
.login-main {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: url('../../assets/login-bg.png') no-repeat;
background-size: 100% 100%;
}
.login-form {
position: relative;
top: -110px;
background: #000;
border-radius: 8px;
::v-deep(.el-form-item__label) {
color: #fff;
}
}
.login-btn {
position: relative;
z-index: 1;
display: inline-flex;
justify-content: center;
align-items: center;
overflow: hidden;
width: 100%;
height: 40px;
font-size: 14px;
font-family: alliance, mono, sans-serif;
color: #fff;
background: transparent;
border-radius: 0;
transition:
background 0.3s ease-in-out,
border-color 0.3s ease-in-out,
color 0.3s ease-in-out;
font-weight: 600;
line-height: 40px;
letter-spacing: 0.07em;
text-transform: uppercase;
font-feature-settings: "salt" on, "ss01" on, "ss02" on;
transition-property: background, border-color, color;
transition-duration: 0.3s, 0.3s, 0.3s;
transition-timing-function: ease-in-out, ease-in-out, ease-in-out;
}
.login-btn::before {
position: absolute;
top: 1px;
left: 1px;
z-index: -1;
display: block;
width: calc(100% - 2px);
height: calc(100% - 2px);
background: #000;
transition: background 0.3s ease-in-out;
content: "";
transform: translate3d(0, 0, 0);
}
.login-btn::after {
position: absolute;
top: 0;
left: 0;
z-index: -3;
display: block;
width: 100%;
height: 100%;
background:
linear-gradient(
269.16deg,
#9867f0 -15.83%,
#3bf0e4 -4.97%,
#33ce43 15.69%,
#b2f4b6 32.43%,
#ffe580 50.09%,
#ff7571 67.47%,
#ff7270 84.13%,
#ea5dad 105.13%,
#c2a0fd 123.24%
);
background-position: 58% 50%;
background-size: 500%;
content: "";
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
animation: gradient-shift 30s ease infinite;
}
@keyframes gradient-shift {
0% {
background-position: 58% 50%;
}
25% {
background-position: 100% 0%;
}
75% {
background-position: 10% 50%;
}
100% {
background-position: 58% 50%;
}
}
.login-btn:hover::before {
background: transparent;
}
.login-btn:hover {
cursor: pointer;
color: #000;
}
</style>
清理了缓存重新登录成功