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>
posted @ 2024-07-04 00:48  ccqh  阅读(3)  评论(0编辑  收藏  举报