vue3+typescript管理系统项目开发记录1
代理跨域
1、vite.config.ts文件配置(增加代码)
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
//获取各种环境对应的变量
let env = loadEnv(mode, process.cwd());
return {
//代理跨域
server: {
proxy: {
[env.VITE_APP_BASE_API]: {
//获取数据服务器地址设置
target: env.VITE_SERVE,
//需要代理跨域
changeOrigin: true,
//路径重写
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
})
路由配置
1、src/router目录下创建index.ts进行路由配置
//通过vue-router实现模板路由配置
import { createRouter, createWebHashHistory } from 'vue-router'
import { constantRoute } from './routes'
let router = createRouter({
//路由模式
history: createWebHashHistory(),
routes: constantRoute,
//滚动行为
scrollBehavior() {
return {
left: 0,
top: 0
}
}
})
export default router
2、src/router目录下创建routes.ts文件进行具体配置
//对外暴露配置路由(常量路由)
export const constantRoute = [
{
//登录
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login',
meta: {
title: '登录',//菜单标题
hidden: true,//代表路由标题在菜单中是否隐藏
icon: 'Promotion'//菜单文字左侧的图标,支持element-plus全部图标
}
},
{
//登录成功,展示数据
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta: {
title: '',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: ''
},
redirect: '/home',
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
name: 'Home',
meta: {
title: '首页',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'HomeFilled'
}
}
]
},
{
//数据大屏
path: '/screen',
component: () => import('@/views/screen/index.vue'),
name: 'Screen',
meta: {
title: '数据大屏',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'Platform'
}
},
{
//权限管理
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
meta: {
title: '权限管理',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'Lock'
},
redirect: '/acl/user',
children: [
{
path: '/acl/user',
component: () => import('@/views/acl/user/index.vue'),
name: 'User',
meta: {
title: '用户管理',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'User'
}
},
{
path: '/acl/role',
component: () => import('@/views/acl/role/index.vue'),
name: 'Role',
meta: {
title: '角色管理',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'UserFilled'
}
},
{
path: '/acl/permission',
component: () => import('@/views/acl/permission/index.vue'),
name: 'Permission',
meta: {
title: '菜单管理',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'Grid'
}
}
]
},
{
//商品管理
path: '/product',
component: () => import('@/layout/index.vue'),
name: 'Product',
meta: {
title: '商品管理',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'Goods'
},
redirect: '/product/trademark',
children: [
{
path: '/product/trademark',
component: () => import('@/views/product/trademark/index.vue'),
name: 'Trademark',
meta: {
title: '品牌管理',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'ShoppingCartFull'
}
},
{
path: '/product/attr',
component: () => import('@/views/product/attr/index.vue'),
name: 'Attr',
meta: {
title: '属性管理',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'ChromeFilled'
}
},
{
path: '/product/spu',
component: () => import('@/views/product/spu/index.vue'),
name: 'Spu',
meta: {
title: 'SPU管理',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'MostlyCloudy'
}
},
{
path: '/product/sku',
component: () => import('@/views/product/sku/index.vue'),
name: 'Sku',
meta: {
title: 'SKU管理',//菜单标题
hidden: false,//代表路由标题在菜单中是否隐藏
icon: 'PartlyCloudy'
}
}
]
},
{
//404
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
meta: {
title: '404',//菜单标题
hidden: true,//代表路由标题在菜单中是否隐藏
icon: 'Promotion'
}
},
{
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
meta: {
title: '任意路由',//菜单标题
hidden: true,//代表路由标题在菜单中是否隐藏
icon: 'Promotion'
}
}
]
路由鉴权
1、src目录创建permission.ts文件进行鉴权操作的配置
//路由鉴权:项目中路由能不能被访问的权限设置
import router from "@/router";
import setting from "./setting";
import nprogress from 'nprogress'
nprogress.configure({ showSpinner: false })
//引入进度条样式
import 'nprogress/nprogress.css'
//获取用户相关仓库里面的token,用于判断用户是否登录成功
import useUserStore from "./store/modules/user";
import pinia from "./store";
let userStore = useUserStore(pinia)
//全局守卫:项目中任意路由切换都会触发的钩子
//全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
document.title = `${setting.title} - ${to.meta.title}`
//访问某一个路由之前的守卫
//to:将要访问的路由
//from:起始路由
//next:路由的放行函数
nprogress.start()
//获取token判断用户登录与否
let token = userStore.token
//获取用户名
let username = userStore.username
//用户登录判断
if (token) {
//登录
if (to.path == '/login') {
next({ path: '/' })
} else {
//登录成功访问其余路由,除登录
//有用户信息
if (username) {
next()
} else {
//没有用户信息,获取用户信息
try {
//获取用户信息
await userStore.userInfo()
next()
} catch (error) {
//token过期,获取不到用户信息
//用户手动修改本地存储token
//退出登录,清空相关数据
await userStore.userLogout()
next({ path: '/login', query: { redirect: to.path } })
}
}
}
} else {
//未登录
if (to.path == '/login') {
next()
} else {
next({ path: '/login', query: { redirect: to.path } })
}
}
next()
})
//全局后置守卫
router.afterEach((to: any, from: any) => {
nprogress.done()
})
登录功能
1、接口准备,src/api目录创建user文件夹
2、src/utils中封装工具文件
- 创建token.ts文件进行localstorage本地存储token操作封装
//封装本地存储数据与读取数据方法
//存储数据
export const SET_TOKEN = (token: string) => {
localStorage.setItem('TOKEN', token)
}
//本地存储获取数据
export const GET_TOKEN = () => {
return localStorage.getItem('TOKEN')
}
//本地存储删除数据
export const REMOVE_TOKEN = () => {
localStorage.removeItem('TOKEN')
}
- 创建time.ts文件进行获取时间信息操作封装
//获取当前是早上、上午、下午还是晚上
export const getTime = () => {
let message = ''
let hours = new Date().getHours()
if (hours <= 9) {
message = '早上'
} else if (hours <= 12) {
message = '上午'
} else if (hours <= 18) {
message = '下午'
} else {
message = '晚上'
}
return message
}
3、user目录增加index.ts文件封装接口方法
//统一管理项目用户相关的接口
import request from '@/utils/request'
import type { loginFormData, loginResponseData, userInfoResponseData } from './type'
//项目用户相关的请求地址
enum API {
LOGIN_URL = '/admin/acl/index/login',
USERINFO_URL = '/admin/acl/index/info',
LOGOUT_URL = '/admin/acl/index/logout'
}
//登录接口
export const reqLogin = (data: loginFormData) => request.post<any, loginResponseData>(API.LOGIN_URL, data)
//获取用户信息
export const reqUserInfo = () => request.get<any, userInfoResponseData>(API.USERINFO_URL)
//退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)
4、user目录增加type.ts文件配置数据类型
//登录接口需要携带参数ts类型
export interface loginFormData {
username: string
password: string
}
//定义所有接口返回数据都拥有的ts类型
export interface ResponseData {
code: number,
message: string,
ok: boolean
}
//登录接口返回的数据类型
export interface loginResponseData extends ResponseData {
data: string
}
//定义获取用户信息返回数据类型
export interface userInfoResponseData extends ResponseData {
data: {
routes: string[],
buttons: string[],
roles: string[],
name: string,
avatar: string
}
}
5、src/store目录创建modules文件夹和index.ts文件,index.ts文件用于创建大仓库,modules文件夹中存放小仓库
//大仓库
import { createPinia } from 'pinia'
//创建大仓库
let pinia = createPinia()
//对外暴露,入口文件安装
export default pinia
6、modules目录创建user.ts文件
//创建用户相关的小仓库
import { defineStore } from 'pinia'
//引入接口
import { reqLogin, reqUserInfo, reqLogout } from '@/api/user'
//引入数据类型
import { loginFormData, loginResponseData, userInfoResponseData } from '@/api/user/type'
import { UserState } from './types/type'
//引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
//引入路由(常量路由)
import { constantRoute } from '@/router/routes'
//创建用户小仓库
let useUserStore = defineStore("User", {
//小仓库存储数据的地方
state: (): UserState => {
return {
token: GET_TOKEN(),//用户的唯一标识
menuRoutes: constantRoute,//仓库存储生成菜单需要的数组(路由)
username: '',
avatar: ''
}
},
//异步|逻辑的地方
actions: {
//用户登录的方法
async userLogin(data: loginFormData) {
//登录请求
let result: loginResponseData = await reqLogin(data)
//登录成功
if (result.code === 200) {
//pinia仓库存储一下token
this.token = (result.data as string)
//本地持久化存储token
SET_TOKEN((result.data as string))
//保证当前async函数返回一个成功的promise
return 'ok'
} else {
//返回错误的promise
return Promise.reject(new Error(result.data))
}
},
//获取用户信息的方法
async userInfo() {
//获取用户信息存储于仓库
let result: userInfoResponseData = await reqUserInfo()
//如果获取用户信息成功,储存用户信息
if (result.code === 200) {
this.username = result.data.name
this.avatar = result.data.avatar
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
//退出登录的方法
async userLogout() {
let result: any = await reqLogout()
if (result.code === 200) {
this.token = ''
this.username = ''
this.avatar = ''
REMOVE_TOKEN()
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
}
},
getters: {
}
})
//对外暴露小仓库
export default useUserStore
7、modules目录下创建types/index.ts用于规范小仓库中的数据类型
import type { RouteRecordRaw } from 'vue-router'
//定义小仓库数据state类型
export interface UserState {
token: string | null,
menuRoutes: RouteRecordRaw[],
username: string,
avatar: string
}
8、src/views目录下创建login/index.vue作为登录页面
<template>
<div class="login_container">
<el-row>
<el-col :span="12" :xs="0"></el-col>
<el-col :span="12" :xs="24">
<el-form
class="login_form"
:model="loginForm"
:rules="rules"
ref="loginForms"
>
<h1>Hello</h1>
<h2>欢迎来到管理系统</h2>
<el-form-item prop="username">
<el-input
:prefix-icon="User"
v-model="loginForm.username"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
type="password"
:prefix-icon="Lock"
show-password
v-model="loginForm.password"
></el-input>
</el-form-item>
<el-form-item>
<el-button
:loading="loading"
class="login_btn"
type="primary"
size="default"
@click="login"
>
登录
</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { User, Lock } from '@element-plus/icons-vue'
import { reactive, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElNotification } from 'element-plus'
//引入获取当前时间的函数
import { getTime } from '@/utils/time'
//引入用户相关的小仓库
import useUserStore from '@/store/modules/user'
//获取用户仓库
let userStore = useUserStore()
//获取el-form组件
let loginForms = ref()
//获取路由器
let $router = useRouter()
//获取路由
let $route = useRoute()
//定义变量控制按钮加载效果
let loading = ref(false)
//收集账号和密码的数据
let loginForm = reactive({ username: 'admin', password: '111111' })
//登录按钮的回调
const login = async () => {
//保证全部的表单校验通过再发请求
await loginForms.value.validate()
//加载效果,开始加载
loading.value = true
try {
//保证登录成功
await userStore.userLogin(loginForm)
//编程式导航跳转到展示数据首页
//判断登录的时候,路由路径中是否有query参数,有跳转query参数页面,没有跳转到首页
let redirect: any = $route.query.redirect
$router.push({ path: redirect || '/' })
//登录成功的提示信息
ElNotification({
type: 'success',
message: '欢迎回来',
title: `HI,${getTime()}好`,
})
//登录成功,加载效果消失
loading.value = false
} catch (error) {
//登录失败,加载效果消失
loading.value = false
//登录失败的提示信息
ElNotification({
type: 'error',
message: (error as Error).message,
})
}
}
//自定义校验规则函数
const validatorUserName = (rule: any, value: any, callback: any) => {
//rule为校验规则对象
//value:校验的文本值
//如果符合条件通过callback放行通过,不符合条件通过callback注入错误信息
if (value.length >= 5) {
callback()
} else {
callback(new Error('用户名长度至少5位'))
}
}
const validatorPassword = (rule: any, value: any, callback: any) => {
if (value.length >= 6) {
callback()
} else {
callback(new Error('密码长度至少6位'))
}
}
//定义表单校验需要的配置对象
const rules = {
//规则对象属性:
//required:代表这个字段务必要校验
//min:文本长度至少多少位
//max:文本长度最多多少位
//message:校验失败的提示信息
//trigger:触发校验表单的时机(change:文本发生变化触发,blur:失去焦点触发)
username: [
// {
// required: true,
// min: 5,
// max: 10,
// message: '用户名长度至少5位,最多15位',
// trigger: 'change',
// },
{ trigger: 'change', validator: validatorUserName },
],
password: [
// {
// required: true,
// min: 6,
// max: 15,
// message: '密码长度至少6位,最多15位',
// trigger: 'change',
// },
{ trigger: 'change', validator: validatorPassword },
],
}
</script>
<style scoped lang="scss">
.login_container {
width: 100%;
height: 100vh;
background: url('@/assets/images/background.jpg') no-repeat;
background-size: cover;
}
.login_form {
position: relative;
width: 80%;
top: 30vh;
background: url('@/assets/images/login_form.png') no-repeat;
background-size: cover;
padding: 40px;
h1 {
color: white;
font-size: 40px;
}
h2 {
color: white;
font-size: 20px;
margin: 20px 0px;
}
.login_btn {
width: 100%;
}
}
</style>