Vue 3 + TypeScript + Vite + pinia + element-plus + vue-routes 一步步带你刷出个完整后台系统

 一、先看下我的前一篇vue+pinaia .代码都在文里了。Pinia,Vue生态里Vuex的代替者_东宇科技的博客-CSDN博客我们通过创建一个简单的demo,来认识下pinia .....安装创建项目npm init vite@latestnpm install pinia....引用storeimport { createPinia } from 'pinia' const pinia = createPinia()const app =createApp(App) app.use(pinia)app.mount('#app')....创建stroeimport { defineStore} fromhttps://blog.csdn.net/ldy889/article/details/123481222

二、第二篇整合了vue-routes. https://blog.csdn.net/ldy889/article/details/123527485https://blog.csdn.net/ldy889/article/details/123527485

三、本文在上面的基础上,增加登录页面,登录后后台会返回token,然后跳转到后台首页。验证是否有权限,如直接访问后台的路由地址,路由守卫会返回到login页面登录, 前面2节可以跳着看,本节会贴出完整的代码。

1、npm init vite@latest  我们会得到一个helloWorld的项目。

 

2、npm i 安装依赖,npm run dev测试看下结果

3、安装 npm install pinia  ,element-plus, vue-router, sass, sass-loader  

npm i vite-plugin-svg-icons -D   最终选用这个来使用svg-icon:参考:https://github.com/JetBrains/svg-sprite-loader/issues/434https://github.com/JetBrains/svg-sprite-loader/issues/434

 4、修改main.ts文件。

import { createApp } from 'vue'
import App from './App.vue'
 
//引入 pinia store
import { createPinia } from 'pinia'
 
//引入 element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' 

import Router from './router' 

//引入 vite-plugin-svg-icons
import 'virtual:svg-icons-register' 
import SvgIcon from '@/components/SvgIcon/index.vue'// svg component
  
const pinia = createPinia()
const app =createApp(App) 
app.component('svg-icon', SvgIcon)
app.use(pinia)
app.use(ElementPlus)
app.use(Router)
app.mount('#app')





 5、修改src/App.vue 这个就是添加个路由槽

<script setup lang="ts">
// src/App.vue
</script>

<template>
   <router-view></router-view>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

6、添加 src/router/index.ts , 这里用到扫描import ,vite的功能,取代原来webpack下的插件。不知道的看下我上面第二个文章。或者去vite官方文档里看下懒加载。

import { createRouter, createWebHashHistory } from "vue-router";
 
const modules = import.meta.glob("../views/*/*.vue");
for (const path in modules) {
  modules[path]().then((mod) => {
    console.log(path, mod);
  });
}

const Router = createRouter({
  history: createWebHashHistory(),
  routes: [ 
    {
      path: "/",
      component: modules["../views/login/index.vue"],
    }  
  ],
});

export default Router;

7、添加src/views/login/index.vue页面,写了一个按钮

<script setup lang="ts"></script>

<template>
  <div>
    <el-row class="mb-4">
      <el-button type="primary">Hello</el-button>
      <svg-icon icon-class="user" />
    </el-row>
  </div>
</template>

<style></style>

8、运行 npm run dev : 结果: 

9、添加src/store/index.ts

import { defineStore} from 'pinia'

export const mainStore = defineStore('main',{
  state:()=>{
    return {
        helloWorld:'Hello World',
        count:0
    }
  },
  getters:{},
  actions:{
    changeState(){
        this.count++
        this.helloWorld='haha'
    }
  }
})

 10、修改src/views/login/index.vue页面

<script setup lang="ts">
import { mainStore } from "../../store/index";
const store = mainStore();

setInterval(function( ) { 
    store.$patch({
        helloWorld: "helloWddorld"
    });
    store.changeState()
    console.log(store.helloWorld);
    
},1000)  
</script>

<template>
  <div>
    <el-row class="mb-4">
      <el-button type="primary">Hello</el-button>
    </el-row>
  </div>
</template>

<style>
 
</style>

11、运行结果测试可以取到store里的值。到这里基本跑通了。下面开始写后台了。

 12、修改src/views/login/index.vue页面 让他显示该有的样子。

<template>
  <div class="login-container">
    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">

      <div class="title-container">
        <h3 class="title">Login Form</h3>
      </div>

      <el-form-item prop="username">
        <span class="svg-container">
          <svg-icon icon-class="user" />
        </span>
        <el-input
          ref="username"
          v-model="loginForm.username"
          placeholder="Username"
          name="username"
          type="text"
          tabindex="1"
          auto-complete="on"
        />
      </el-form-item>

      <el-form-item prop="password">
        <span class="svg-container">
          <svg-icon icon-class="password" />
        </span>
        <el-input
          :key="passwordType"
          ref="password"
          v-model="loginForm.password"
          :type="passwordType"
          placeholder="Password"
          name="password"
          tabindex="2"
          auto-complete="on"
          @keyup.enter.native="handleLogin"
        />
        <span class="show-pwd" @click="showPwd">
          <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
        </span>
      </el-form-item>

      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>

      <div class="tips">
        <span style="margin-right:20px;">username: admin</span>
        <span> password: any</span>
      </div>

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

<script>
import { validUsername } from '@/utils/validate'

export default {
  name: 'Login',
  data() {
    const validateUsername = (rule, value, callback) => {
      if (!validUsername(value)) {
        callback(new Error('Please enter the correct user name'))
      } else {
        callback()
      }
    }
    const validatePassword = (rule, value, callback) => {
      if (value.length < 6) {
        callback(new Error('The password can not be less than 6 digits'))
      } else {
        callback()
      }
    }
    return {
      loginForm: {
        username: 'admin',
        password: '111111'
      },
      loginRules: {
        username: [{ required: true, trigger: 'blur', validator: validateUsername }],
        password: [{ required: true, trigger: 'blur', validator: validatePassword }]
      },
      loading: false,
      passwordType: 'password',
      redirect: undefined
    }
  },
  watch: {
    $route: {
      handler: function(route) {
        this.redirect = route.query && route.query.redirect
      },
      immediate: true
    }
  },
  methods: {
    showPwd() {
      if (this.passwordType === 'password') {
        this.passwordType = ''
      } else {
        this.passwordType = 'password'
      }
      this.$nextTick(() => {
        this.$refs.password.focus()
      })
    },
    handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true
          this.$store.dispatch('user/login', this.loginForm).then(() => {
            this.$router.push({ path: this.redirect || '/' })
            this.loading = false
          }).catch(() => {
            this.loading = false
          })
        } else {
          console.log('error submit!!')
          return false
        }
      })
    }
  }
}
</script>

<style lang="scss">
/* 修复input 背景不协调 和光标变色 */
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */

$bg:#283443;
$light_gray:#fff;
$cursor: #fff;

@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
  .login-container .el-input input {
    color: $cursor;
  }
}

/* reset element-ui css */
.login-container {
  .el-input {
    display: inline-block;
    height: 47px;
    width: 85%;

    input {
      background: transparent;
      border: 0px;
      -webkit-appearance: none;
      border-radius: 0px;
      padding: 12px 5px 12px 15px;
      color: $light_gray;
      height: 47px;
      caret-color: $cursor;

      &:-webkit-autofill {
        box-shadow: 0 0 0px 1000px $bg inset !important;
        -webkit-text-fill-color: $cursor !important;
      }
    }
  }

  .el-form-item {
    border: 1px solid rgba(255, 255, 255, 0.1);
    background: rgba(0, 0, 0, 0.1);
    border-radius: 5px;
    color: #454545;
  }
}
</style>

<style lang="scss" scoped>
$bg:#2d3a4b;
$dark_gray:#889aa4;
$light_gray:#eee;

.login-container {
  min-height: 100%;
  width: 100%;
  background-color: $bg;
  overflow: hidden;

  .login-form {
    position: relative;
    width: 520px;
    max-width: 100%;
    padding: 160px 35px 0;
    margin: 0 auto;
    overflow: hidden;
  }

  .tips {
    font-size: 14px;
    color: #fff;
    margin-bottom: 10px;

    span {
      &:first-of-type {
        margin-right: 16px;
      }
    }
  }

  .svg-container {
    padding: 6px 5px 6px 15px;
    color: $dark_gray;
    vertical-align: middle;
    width: 30px;
    display: inline-block;
  }

  .title-container {
    position: relative;

    .title {
      font-size: 26px;
      color: $light_gray;
      margin: 0px auto 40px auto;
      text-align: center;
      font-weight: bold;
    }
  }

  .show-pwd {
    position: absolute;
    right: 10px;
    top: 7px;
    font-size: 16px;
    color: $dark_gray;
    cursor: pointer;
    user-select: none;
  }
}
</style>

13、接下来是一键3连的广告时间。然后详细解读下login.vue.

--------广告位置------ 

--------广告位置------ 

14、第一是,图标显示不了了。 关于svg图标的使用,vite里无法使用webpack的svg-sprite-loader插件,而vue-svgicon又只支持vue2,vue3该如何使用svgicon呢。【2022-3-18】

安装:  npm i vite-plugin-svg-icons -D

修改vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import {resolve} from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(), 
    createSvgIconsPlugin({
    // 指定需要缓存的图标文件夹
    iconDirs: [resolve(process.cwd(), 'src/icons/svg')],
    // 指定symbolId格式
    symbolId: 'icon-[dir]-[name]',
  
    /**
     * custom dom id
     * @default: __svg__icons__dom__
     */
    customDomId: '__svg__icons__dom__',
  }),],
  resolve: {
    // 配置别名
    alias: {
      '@': resolve(__dirname, './src')
    }
  }
})

修改main.ts

import { createApp } from 'vue'
import App from './App.vue'
 
//引入 pinia store
import { createPinia } from 'pinia'
 
//引入 element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' 

import Router from './router' 

//引入 vite-plugin-svg-icons
import 'virtual:svg-icons-register' 
import SvgIcon from '@/components/SvgIcon/index.vue'// svg component
 
 

const pinia = createPinia()
const app =createApp(App) 
app.component('svg-icon', SvgIcon)
app.use(pinia)
app.use(ElementPlus)
app.use(Router)
app.mount('#app')

创建:src/components/SvgIcon/index.vue

<template>
  <div
    v-if="isExternal"
    :style="styleExternalIcon"
    class="svg-external-icon svg-icon" 
  />
  <svg v-else :class="svgClass" aria-hidden="true" >
    <use :xlink:href="iconName" />
  </svg>
</template>

<script>
import { defineComponent, computed } from "vue";
import { isExternal } from "@/utils/validate";

export default defineComponent({
  name: "SvgIcon",
  props: { 
    iconClass: {
      type: String,
      required: true,
    }, 
    className: {
      type: String,
      default: "",
    },
  },
  computed: {
    isExternal() {
      return isExternal(this.iconClass)
    },
    iconName() {
      return `#icon-${this.iconClass}`
    },
    svgClass() {
      if (this.className) {
        return 'svg-icon ' + this.className
      } else {
        return 'svg-icon'
      }
    },
    styleExternalIcon() {
      return {
        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
      }
    }
  } 
});
</script>

<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}

.svg-external-icon {
  background-color: currentColor;
  mask-size: cover !important;
  display: inline-block;
}
</style>

更新 src/utils/validate.ts

export const validUsername = (str: string) => ['admin', 'editor'].indexOf(str.trim()) >= 0

export const isExternal = (path: string) => /^(https?:|mailto:|tel:)/.test(path)

export const isArray = (arg: any) => {
  if (typeof Array.isArray === 'undefined') {
    return Object.prototype.toString.call(arg) === '[object Array]'
  }
  return Array.isArray(arg)
}

export const isValidURL = (url: string) => {
  const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
  return reg.test(url)
}

添加一些svg文件到 src/icons/svg/...

 运行结果:

 当然点击登录会有问题。

 接下来要用pinia了。先安装 vue-request,axios, nprogress进度条。

npm install vue-request
npm install axios
npm i --save-dev @types/nprogress

创建 service/http.ts

//http.ts
import axios, { AxiosRequestConfig } from 'axios'
import NProgress from 'nprogress'

// 设置请求头和请求路径
axios.defaults.baseURL = '/api'
axios.defaults.timeout = 10000
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'
axios.interceptors.request.use(
  (config): AxiosRequestConfig<any> => {
    const token = window.sessionStorage.getItem('token')
    if (token) {
      //@ts-ignore
      config.headers.token = token
    }
    return config
  },
  (error) => {
    return error
  }
)
// 响应拦截
axios.interceptors.response.use((res) => {
  if (res.data.code === 111) {
    sessionStorage.setItem('token', '')
    // token过期操作
  }
  return res
})

interface ResType<T> {
  code: number
  data?: T
  msg: string
  err?: string
}
interface Http {
  get<T>(url: string, params?: unknown): Promise<ResType<T>>
  post<T>(url: string, params?: unknown): Promise<ResType<T>>
  upload<T>(url: string, params: unknown): Promise<ResType<T>>
  download(url: string): void
}

const http: Http = {
  get(url, params) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      axios
        .get(url, { params })
        .then((res) => {
          NProgress.done()
          resolve(res.data)
        })
        .catch((err) => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  post(url, params) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      axios
        .post(url, JSON.stringify(params))
        .then((res) => {
          NProgress.done()
          resolve(res.data)
        })
        .catch((err) => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  upload(url, file) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      axios
        .post(url, file, {
          headers: { 'Content-Type': 'multipart/form-data' },
        })
        .then((res) => {
          NProgress.done()
          resolve(res.data)
        })
        .catch((err) => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  download(url) {
    const iframe = document.createElement('iframe')
    iframe.style.display = 'none'
    iframe.src = url
    iframe.onload = function () {
      document.body.removeChild(iframe)
    }
    document.body.appendChild(iframe)
  },
}
export default http

 创建src/store/login.ts

import { defineStore } from "pinia";
import http from "@/service/http"; 
import   loginApi from "@/service/api/login"
import { ILoginParams } from "@/service/api/types";

export interface IUserState {
  token?: string;
  name?: string;
  avatar?: string;
  introduction?: string;
  roles?: string[];
  email?: string;
}

export const useUserStore = defineStore("user", {
  state: () => {
    return {
      userState: {},
    };
  },
  getters: {
    getUserState: (state) => state.userState  ,
  },
  actions: {
    async Login(params: ILoginParams) { 
      // const { data, error } = useRequest(http.post("/", { a: "" }));
      this.userState = await  loginApi.login(params).catch((error)=>{
        return {error:"发送请求失败:Login"};
      })
    },
  },
});

.添加src/api/login.ts

import http from '@/service/http'
import * as T from './types'

const loginApi: T.ILoginApi = {
    login(params){
        return http.post('/login', params)
    }

}
export default loginApi

添加src/api/types.ts

export interface ILoginParams {
        userName: string
        passWord: string | number
    }
export interface ILoginApi {
    login: (params: ILoginParams)=> Promise<any>
}
    

好了到这里我们可以登录并跳转到另外一个页面了。 

祝你成功。

代码下载:vite-vue-admin-elui-demo-Typescript文档类资源-CSDN下载Vue3+TypeScript+Vite+pinia+element-plus+更多下载资源、学习资料请访问CSDN下载频道.https://download.csdn.net/download/ldy889/85001855

  Vben Admin

 一个admin后台。技术栈参考他的。

好的,目前我们能登录了。检查有token和用户信息后,跳转到dashboard页面“/”。所以我们的路由应该看起来是这样的。

import { createRouter, createWebHashHistory } from "vue-router";

const modules = import.meta.glob("../views/*/*.vue");
for (const path in modules) {
  modules[path]().then((mod) => {
    console.log(path, mod);
  });
}

const Router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: "/login",
      component: modules["../views/login/index.vue"],
    },
    {
      path: "/",
      component: modules["../views/dashboard/index.vue"],
    },
  ],
});

export default Router;

 

 添加 src/views/dashboard/index.vue

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

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({ 
  data() {
    return {};
  },
  watch: {},
  mounted() {},
  methods: {},
});
</script>

<style lang="scss"></style>

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

东宇科技

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

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

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

打赏作者

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

抵扣说明:

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

余额充值