vue3+typescript管理系統專案開發記錄1

ccqh發表於2024-07-04

代理跨域

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>

相關文章