Vue3实战商城后台管理系统开发步骤(笔记)

1 创建vite项目和配置

1.1 创建vite项目并安装VSCode插件

npm create vue@latest
or
npm init vite@latest

这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示:

✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add an End-to-End Testing Solution? … No / Cypress / Nightwatch / Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
✔ Add Vue DevTools 7 extension for debugging? (experimental) … No / Yes

Scaffolding project in ./<your-project-name>...
Done.
npm init vite@latest shop-admin -- --template vue
cd shop-admin
npm install
npm run dev
  • 查看registry:npm config get registry
  • 设置registry:npm config set registry=https://registry.npmmirror.com

VSCode插件参考:

  • Chinese Language Pack
  • Vue3 Snippets
  • Vue - Official

1.2 Element Plus安装及引用方法

兼容性2.5.0

Element Plus 支持最近两个版本的浏览器。

如果您需要支持旧版本的浏览器,请自行添加 Babel 和相应的 Polyfill 。

由于 Vue 3 不再支持 IE11,Element Plus 也不再支持 IE 浏览器。

版本

Chrome


Chrome

IE


Edge

Firefox


Firefox

Safari


Safari
< 2.5.0Chrome ≥ 64Edge ≥ 79Firefox ≥ 78Safari ≥ 12
2.5.0 +Chrome ≥ 85Edge ≥ 85Firefox ≥ 79Safari ≥ 14.1

版本

Element Plus 目前还处于快速开发迭代中。

使用包管理器

我们建议您使用包管理器(如 NPM、Yarn 或 pnpm)安装 Element Plus,然后您就可以使用打包工具,例如 Vite 或 webpack

# 选择一个你喜欢的包管理器

# NPM
$ npm install element-plus --save

# Yarn
$ yarn add element-plus

# pnpm
$ pnpm install element-plus

如果您的网络环境不好,建议使用相关镜像服务 cnpm 或 中国 NPM 镜像

浏览器直接引入

直接通过浏览器的 HTML 标签导入 Element Plus,然后就可以使用全局变量 ElementPlus 了。

根据不同的 CDN 提供商有不同的引入方式, 我们在这里以 unpkg 和 jsDelivr 举例。 你也可以使用其它的 CDN 供应商。

unpkg

html

<head>
  <!-- Import style -->
  <link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" />
  <!-- Import Vue 3 -->
  <script src="//unpkg.com/vue@3"></script>
  <!-- Import component library -->
  <script src="//unpkg.com/element-plus"></script>
</head>

jsDelivr

html

<head>
  <!-- Import style -->
  <link
    rel="stylesheet"
    href="//cdn.jsdelivr.net/npm/element-plus/dist/index.css"
  />
  <!-- Import Vue 3 -->
  <script src="//cdn.jsdelivr.net/npm/vue@3"></script>
  <!-- Import component library -->
  <script src="//cdn.jsdelivr.net/npm/element-plus"></script>
</head>

TIP

我们建议使用 CDN 引入 Element Plus 的用户在链接地址上锁定版本,以免将来 Element Plus 升级时受到非兼容性更新的影响。 锁定版本的方法请查看 unpkg.com

由于原生的 HTML 解析行为的限制,单个闭合标签可能会导致一些例外情况,所以请使用双封闭标签, 参考

html

<!-- examples -->
<el-table>
  <el-table-column></el-table-column>
  <el-table-column></el-table-column>
</el-table>

完整引入:

// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'

const app = createApp(App)

app.use(ElementPlus)
app.mount('#app')

1.3 引入windicss工具库和配置、安装代码提示

特性#

  • ⚡️ 极速 - 在 Vite 中比 Tailwind 快20~100倍
  • 🧩 按需使用 CSS 工具类(与 Tailwind CSS v2 完全兼容)
  • 📦 按需使用原生元素样式重置(预检样式)
  • 🔥 模块热替换 (HMR)
  • 🍃 从 tailwind.config.js 加载配置
  • 🤝 与框架无关 - Vue、 React、Svelte and vanilla!
  • 📄 CSS @apply / @screen 指令转换(也适用于 Vue SFC 的 <style> )
  • 🎳 支持变量组 - 如 bg-gray-200 hover:(bg-gray-100 text-red-300)
  • 😎 "Devtools 设计" - 支持传统的 Tailwind 运行方式

安装#

安装相关包:

npm i -D vite-plugin-windicss windicss

然后,在你的 Vite 配置中添加插件:

vite.config.js

import WindiCSS from 'vite-plugin-windicss'

export default {
  plugins: [
    WindiCSS(),
  ],
}

最后,在你的 Vite 入口文件中导入 virtual:windi.css

main.js

import 'virtual:windi.css'

现在可以在你的应用中开始使用 classes 工具类 或者 CSS 指令 ,感受一下速度吧!⚡️

VSCode插件参考:

  • WindiCSS

1.4 windicss小案件和@apply简化代码

App.vue:

<!-- App.vue -->
<script setup>

</script>

<template>
      <button class="btn">我的个性化按钮</button>
 <!-- <div class="mb-4">
    <el-button round>Round</el-button>
    <el-button type="primary" round>Primary</el-button>
    <el-button type="success" round>Success</el-button>
    <el-button type="info" round>Info</el-button>
    <el-button type="warning" round>Warning</el-button>
    <el-button type="danger" round>Danger</el-button>
  </div> -->

</template>

<style>
.btn{
  @apply bg-purple-500 text-indigo-50 px-4 py-4 rounded-full
  hover:(bg-purple-900) transition-all duration-500 focus:(ring-8 ring-purple-900);
}
</style>

1.5 引入vue-router4

官方链接:Installation | Vue Router  介绍 | Vue Router

安装vue-rounter4

npm install vue-router@4

 1.6 配置路由和404页面捕获

1.6.1 在src下新建pages文件夹,在此pages文件夹下分别新建

index.vue
about.vue
404.vue

1.6.2 在src下新建文件夹router,在router下新建index.js。index.js:

//index.js
import {
    createRouter,
    createWebHashHistory
} from 'vue-router'

import Index from '@/pages/index.vue'
import About from '@/pages/about.vue'
import NotFound from '@/pages/404.vue'
const routes =[{
    path:"/",
    component:Index
},{
    path:"/about",
    component:About
},{
    path:"/:pathMatch(.*)*",
    name:"NotFound",
    component:NotFound
}]

const router = createRouter({
    history:createWebHashHistory(),
    routes
})

export default router

1.6.3 配置main.js

// import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus' //引入element-plus
import 'element-plus/dist/index.css'
import router from './router' //增加路由包导入
const app = createApp(App)
app.use(ElementPlus)  //装载ElementPlus
app.use(router) //装载路由
import 'virtual:windi.css'
app.mount('#app')

1.6.4 配置vite.config.js

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import WindiCSS from 'vite-plugin-windicss'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    WindiCSS(),
  ],
  server:{host:'0.0.0.0'}, //增加局域网访问
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

// 有的关于resolve.alias如下:
// resolve:{
//   alias:{
//      "~":path.resolve(_dirname,"src")
//   }
// },

此文件已配置:WindiCSS、局域网访问(server)、reslove.alias(使用@代表根目录)

完善404.vue的内容:

<script>

</script>

<template>
      <el-result
        icon="error"
        title="404错误提示"
        sub-title="你找的网页丢失了?还是你输错了网址?"
      >
        <template #extra>
          <el-button type="success" @click="$router.push('/')">返回首页</el-button>
          <el-button type="primary" onclick="history.back()">返回上一页</el-button>
          <el-button type="primary" @click="$router.push('/about')">关于我们</el-button>
        </template>
      </el-result>
</template>

<style>

</style>

1.6.5 在App.vue中更新代码<router-view>

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

2 登录页开发

2.1 登录页源码

<template>
  <el-row class="min-h-screen bg-indigo-500">
    <el-col :span="16" class="flex items-center justify-center flex-col">
      <div>
        <div class="font-bold text-5xl text-light-50 mb-4">欢迎光临</div>
        <div class="text-light-50">此站点是《Vue3+Vite实战商城后台开发》视频课程的演示地址</div>
      </div>
    </el-col>
    <el-col :span="8" class="bg-light-50 flex items-center justify-center flex-col">
      <h2 class="font-bold text-3xl text-gray-800">欢迎回来</h2>
      <div class="flex items-center justify-center my-5 text-gray-300 space-x-2">
        <span class="h-[1px] w-16 bg-gray-200"></span>
        <span>账号密码登录</span>
        <span class="h-[1px] w-16 bg-gray-200"></span>
      </div>
      <el-form :model="form" class="w-[250px]">
        <el-form-item label="账号:">
          <el-input v-model="form.username" placeholder="请输入用户名"/>
        </el-form-item>
        <el-form-item label="密码:">
          <el-input v-model="form.password" type="password" placeholder="请输入密码"/>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit" round class="w-[250px] duration" color="#626aef">登录</el-button>
        </el-form-item>
      </el-form>
    </el-col>
  </el-row>
</template>


<script setup>
import { reactive } from 'vue'

// do not use same name with ref
const form = reactive({
  username: '',
  password: ''
})

const onSubmit = () => {
  console.log('submit!')
}
</script>

2.2 登录页响应式处理:

<el-col :span="16" ……>
<el-col :span="8" ……>

分别修改为:

<el-col :lg="16" :md="12" ……>
<el-col :lg="8" :md="12" ……>

响应式处理后源码如下:

<template>
  <el-row class="min-h-screen bg-indigo-500">
    <el-col :lg="16" :md="12" class="flex items-center justify-center flex-col">
      <div>
        <div class="font-bold text-5xl text-light-50 mb-4">欢迎光临</div>
        <div class="text-light-50">此站点是《Vue3+Vite实战商城后台开发》视频课程的演示地址</div>
      </div>
    </el-col>
    <el-col :lg="8" :md="12" class="bg-light-50 flex items-center justify-center flex-col">
      <h2 class="font-bold text-3xl text-gray-800">欢迎回来</h2>
      <div class="flex items-center justify-center my-5 text-gray-300 space-x-2">
        <span class="h-[1px] w-16 bg-gray-200"></span>
        <span>账号密码登录</span>
        <span class="h-[1px] w-16 bg-gray-200"></span>
      </div>
      <el-form :model="form" class="w-[250px]">
        <el-form-item label="账号:">
          <el-input v-model="form.username" placeholder="请输入用户名"/>
        </el-form-item>
        <el-form-item label="密码:">
          <el-input v-model="form.password" type="password" placeholder="请输入密码"/>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit" round class="w-[250px] duration" color="#626aef">登录</el-button>
        </el-form-item>
      </el-form>
    </el-col>
  </el-row>
</template>


<script setup>
import { reactive } from 'vue'

// do not use same name with ref
const form = reactive({
  username: '',
  password: ''
})

const onSubmit = () => {
  console.log('submit!')
}
</script>

2.3 全局引入图标

2.3.1 安装

使用包管理器

shell

# 选择一个你喜欢的包管理器

# NPM
$ npm install @element-plus/icons-vue
# Yarn
$ yarn add @element-plus/icons-vue
# pnpm
$ pnpm install @element-plus/icons-vue

2.3.2 注册所有图标

您需要从 @element-plus/icons-vue 中导入所有图标并进行全局注册。

main.js/main.ts:

// main.ts

// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

// const app = createApp(App) 此行在目前的配置中已经存在,请注意以下两行一定放在const app = createApp(App)之后
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

2.3.3  用户名输入框增加User图标

<el-input v-model="form.username" placeholder="请输入用户名">
  <template #prefix>
    <el-icon><user /></el-icon>
  </template>
</el-input>

2.3.4 密码框增加Lock图标

<el-input v-model="form.password" placeholder="请输入密码" show-password>
   <template #prefix>
      <el-icon><lock /></el-icon>
   </template>
</el-input>

 2.4 结合@apply实现样式抽离

2.4.1 login.vue抽离前所有代码:

<template>
  <el-row class="min-h-screen bg-indigo-500">
    <el-col :lg="16" :md="12" class="flex items-center justify-center flex-col">
      <div>
        <div class="font-bold text-5xl text-light-50 mb-4">欢迎光临</div>
        <div class="text-light-50">此站点是《Vue3+Vite实战商城后台开发》视频课程的演示地址</div>
      </div>
    </el-col>
    <el-col :lg="8" :md="12" class="bg-light-50 flex items-center justify-center flex-col">
      <h2 class="font-bold text-3xl text-gray-800">欢迎回来</h2>
      <div class="flex items-center justify-center my-5 text-gray-300 space-x-2">
        <span class="h-[1px] w-16 bg-gray-200"></span>
        <span>账号密码登录</span>
        <span class="h-[1px] w-16 bg-gray-200"></span>
      </div>
      <el-form :model="form" class="w-[250px]">
                <el-form-item>
                    <el-input v-model="form.username" placeholder="请输入用户名">
                        <template #prefix>
                            <el-icon><user /></el-icon>
                        </template>
                    </el-input>
                </el-form-item>
                <el-form-item>
                    <el-input v-model="form.password" placeholder="请输入密码" show-password>
                        <template #prefix>
                            <el-icon><lock /></el-icon>
                        </template>
                    </el-input>
                </el-form-item>
                <el-form-item>
                    <el-button round color="#626aef" class="w-[250px]" type="primary" @click="onSubmit">登 录</el-button>
                </el-form-item>
            </el-form>
    </el-col>
  </el-row>
</template>


<script setup>
import { reactive } from 'vue'

// do not use same name with ref
const form = reactive({
  username: '',
  password: ''
})

const onSubmit = () => {
  console.log('submit!')
}
</script>

 2.4.2 login.vue抽离后所有代码:

<template>
  <el-row class="login-container">
    <el-col :lg="16" :md="12" class="login-left">
      <div>
        <div>欢迎光临</div>
        <div>此站点是《Vue3+Vite实战商城后台开发》视频课程的演示地址</div>
      </div>
    </el-col>
    <el-col :lg="8" :md="12" class="login-right">
      <h2>欢迎回来</h2>
      <div>
        <span class="login-line"></span>
        <span>账号密码登录</span>
        <span class="login-line"></span>
      </div>
      <el-form :model="form" class="login-form-width">
                <el-form-item>
                    <el-input v-model="form.username" placeholder="请输入用户名">
                        <template #prefix>
                            <el-icon><user /></el-icon>
                        </template>
                    </el-input>
                </el-form-item>
                <el-form-item>
                    <el-input v-model="form.password" placeholder="请输入密码" show-password>
                        <template #prefix>
                            <el-icon><lock /></el-icon>
                        </template>
                    </el-input>
                </el-form-item>
                <el-form-item>
                    <el-button round color="#626aef" class="login-form-width" type="primary" @click="onSubmit">登 录</el-button>
                </el-form-item>
            </el-form>
    </el-col>
  </el-row>
</template>
<script setup>
import { reactive } from 'vue'

// do not use same name with ref
const form = reactive({
  username: '',
  password: ''
})

const onSubmit = () => {
  console.log('submit!')
}
</script>
<style scoped>
.login-container{
  @apply min-h-screen bg-indigo-500;
}
.login-container .login-left,.login-container .login-right{
  @apply flex items-center justify-center flex-col

}
.login-container .login-right{
  @apply bg-light-50 flex-col
}
.login-left>div>div:first-child{
  @apply font-bold text-5xl text-light-50 mb-4;
}
.login-left>div>div:last-child{
  @apply text-gray-200 text-sm;
}
.login-right>h2{
  @apply font-bold text-3xl text-gray-800;
}
.login-right>div{
  @apply flex items-center justify-center my-5 text-gray-300 space-x-2;

}
.login-line{
  @apply h-[1px] w-16 bg-gray-200;
}
.login-form-width{
  @apply w-[250px];
}
</style> 

2.5 <script setup>语法糖和组合式API

<script setup>

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的 <script> 语法,它具有更多优势:

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 TypeScript 声明 props 和自定义事件。
  • 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
  • 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。

官方链接:

<script setup> | Vue.js (vuejs.org)

2.6 登录表单验证处理

2.6.1 为<el-form>标签增加属性:ref和:rules

ref="formRef"
:rules="rules"
单句完整内容如下:

<el-form ref="formRef" :rules="rules" :model="form" class="login-form-width">

2.6.2 为el-input的上一级的<el-form-item>添加 prop属性并赋值

<el-form-item prop="username">
<el-form-item prop="password">

完整语句如下:

        <el-form-item prop="username">
          <el-input v-model="form.username" prefix-icon="user" placeholder="请输入用户名" >
          </el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input :rules="rules" v-model="form.password" placeholder="请输入密码" show-password type="password" >
            <template #prefix>
              <el-icon>
                <lock />
              </el-icon>
            </template>
          </el-input>
        </el-form-item>

2.6.3 为使用ref增加import引用并定义

将原来的

import { reactive } from 'vue'

修改为

import { reactive,ref } from 'vue'
const formRef = ref(null) //让formRef类型为响应式

2.6.4 定义rules规则

const rules = {
  username:[
    {
      required: true, //必须输入
      message: '用户名不能为空', //验证不符条件时的提示信息
      trigger: 'blur' //触发为失去焦点时
    },{
      min: 2, //最小长度限定为2
      max: 15,//最大长度限定为15
      message: '用户名长度必须是2至15位字符', //验证不符条件时的提示信息
      trigger: 'blur' //触发为失去焦点时
    },
  ],
password:[
    { 
      required: true, //必须输入
      message: '密码不能为空', //验证不符条件时的提示信息
      trigger: 'blur' //触发为失去焦点时
    },{
      min: 6, //最小长度限定为6
      max: 15, //最大长度限定为15
      message: '密码长度必须是6至15位字符', ///验证不符条件时的提示信息
      trigger: 'blur' //触发为失去焦点时
    }
  ],
}

2.6.5 为表单提交增加表单验证功能

如果不做此步骤的设定,虽然分别点击“用户名”、“密码”输入框等有失焦验证,但直接点击“登录”按钮将无失焦验证触发。

将以下代码:

// 修改前:无提交表单验证的原来的代码
const onSubmit = () => {
  console.log('submit!')
}

// 修改后
const onSubmit = () => {
  formRef.value.validate((valid)=>{  //valide用于接收触发验证的结果:true代表验证无错误,false代表验证有错误。
    if(!valid){
      return false  //返回验证失败,页面不做处理
    }
    console.log("验证通过");  //验证通过,可做进一步功能实现
    
  })
}
  

2.7 引入axios请求库和登录接口交互

易用、简洁且高效的http库——axios        官方网址:http://www.axios-js.com/zh-cn/

2.7.1 安装

npm install axios

2.7.2 创建实例

在src文件夹下新建文件axios.js:

import axios from 'axios'

const service = axios.create({
    baseURL:"/api"
})

export default service

在src文件夹下新建文件夹api,在此api文件夹下新建manager.js:

import axios from "axios"

export function login(username,password){
    return axios.post("/admin/login",{
        username,
        password
    })
}

2.7.3 完善引用和代码

login.vue中增加引用及代码:

import { ElNotification } from 'element-plus' //element通知引用
import { login } from '@/api/manager'
import { useRouter } from 'vue-router'

const router = useRouter()

原来的const onSubmit代码修改为:

const onSubmit = () => {
  formRef.value.validate((valid)=>{  //valide用于接收触发验证的结果:true代表验证无错误,false代表验证有错误。
    if(!valid){
      return false  //返回验证失败,页面不做处理
    }
    login(form.username,form.password)
    .then(res=>{
      console.log(res.data.data);
      //提示登录成功
      ElNotification({
        message: "登录成功",
        type: "success",
        duration: 3000
      })

      // 存储token和用户相关信息

      // 跳转到后台首页
      router.push("/")
    })
    .catch(err=>{
      ElNotification({
        message: err.response.data.msg || "请求失败",
        type: "error",
        duration: 3000
      })

    })

  
  })
}

vite.config.js文件中原来的:

server:{host:'0.0.0.0'}, //局域网可访问

修改为:

server: {
    host: '0.0.0.0', //局域网可访问
    open: '#/login', //自动打开登录页面
    proxy: {
      // 选项写法
      '/api': {
        target: 'http://ceshi13.dishait.cn',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      },
    }
  },

2.8 引入cookie存储用户token

2.8.1 安装vueuse/integrations和useCookies相关引用

npm i @vueuse/integrations //安装vueuse基础
npm i universal-cookie    //安装useCookies

2.8.2 为pages/index.vue录入cookie相关代码做测试:

<script setup>
  import { useCookies } from '@vueuse/integrations/useCookies'
  const cookie = useCookies()
  console.log(cookie)

  function cookieSet(){
    cookie.set("admin-token", "wzs888888")
  }
  function cookieGet(){
    console.log(cookie.get("admin-token"))    
  }
  function cookieRemove(){
    cookie.remove("admin-token")
  }
</script>

<template>
  <div class="px-5 py-5">
    后台首页
    <el-button @click="cookieSet">设置</el-button>
    <el-button @click="cookieGet">读取</el-button>
    <el-button @click="cookieRemove">删除</el-button>
</div>
</template>

<style>

</style>

2.8.3 为login.vue增加cookie相关功能处理

增加:

import { useCookies } from '@vueuse/integrations/useCookies' //增加引用

/// …………………… ///

// 存储token和用户相关信息
const cookie = useCookies()
cookie.set("admin-token", res.data.data)
// 跳转到后台首页
router.push("/")

/// …………………… ///

附login.vue全文代码

  <template>
    <el-row class="login-container">
      <el-col :lg="16" :md="12" class="login-left">
        <div>
          <div>欢迎使用</div>
          <div>Vue3+Vite实战商城后台管理系统</div>
        </div>
      </el-col>
      <el-col :lg="8" :md="12" class="login-right">
        <h2>欢迎回来</h2>
        <div>
          <span class="login-line"></span>
          <span>账号密码登录</span>
          <span class="login-line"></span>
        </div>
        <el-form ref="formRef" :rules="rules" :model="form" class="login-form-width">
          <el-form-item prop="username">
            <el-input v-model="form.username" prefix-icon="user" placeholder="请输入用户名" >
            </el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input :rules="rules" v-model="form.password" placeholder="请输入密码" show-password type="password" >
              <template #prefix>
                <el-icon>
                  <lock />
                </el-icon>
              </template>
            </el-input>
          </el-form-item>
          <el-form-item>
            <el-button round color="#626aef" class="login-form-width" type="primary" @click="onSubmit">登 录</el-button>
          </el-form-item>
        </el-form>
        <div>
          <span class="login-line"></span>
        <span>Version 1.0.20240822</span>
        <span class="login-line"></span>
        </div>
      </el-col>
    </el-row>
  </template>
  <script setup>
  import { reactive,ref } from 'vue'
  import { ElNotification } from 'element-plus'
  import { login } from '@/api/manager'
  import { useRouter } from 'vue-router'
  import { useCookies } from '@vueuse/integrations/useCookies'
  const router = useRouter()
  // do not use same name with ref
  const form = reactive({
    username: '',
    password: ''
  })

  const formRef = ref(null) //让formRef类型为响应式

  const rules = {
    username:[
      {
        required: true, //必须输入
        message: '用户名不能为空', //验证不符条件时的提示信息
        trigger: 'blur' //触发为失去焦点时
      },{
        min: 2, //最小长度限定为2
        max: 15,//最大长度限定为15
        message: '用户名长度必须是2至15位字符', //验证不符条件时的提示信息
        trigger: 'blur' //触发为失去焦点时
      },
    ],
  password:[
      { 
        required: true, //必须输入
        message: '密码不能为空', //验证不符条件时的提示信息
        trigger: 'blur' //触发为失去焦点时
      },{
        min: 6, //最小长度限定为6
        max: 15, //最大长度限定为15
        message: '密码长度必须是6至15位字符', ///验证不符条件时的提示信息
        trigger: 'blur' //触发为失去焦点时
      }
    ],
  }


  const onSubmit = () => {
    formRef.value.validate((valid)=>{  //valide用于接收触发验证的结果:true代表验证无错误,false代表验证有错误。
      if(!valid){
        return false  //返回验证失败,页面不做处理
      }
      login(form.username,form.password)
      .then(res=>{
        console.log(res.data.data);
        //提示登录成功
        ElNotification({
          message: "登录成功",
          type: "success",
          duration: 3000
        })
        // 存储token和用户相关信息
        const cookie = useCookies()
        cookie.set("admin-token", res.data.data)
        // 跳转到后台首页
        router.push("/")
      })
      .catch(err=>{
        ElNotification({
          message: err.response.data.msg || "请求失败",
          type: "error",
          duration: 3000
        })

      })

    
    })
  }
  </script>
  <style scoped>
  .login-container {
    @apply min-h-screen bg-indigo-500;
  }

  .login-container .login-left,
  .login-container .login-right {
    @apply flex items-center justify-center flex-col
  }

  .login-container .login-right {
    @apply bg-light-50 flex-col
  }

  .login-left>div>div:first-child {
    @apply font-bold text-5xl text-light-50 mb-4;
  }

  .login-left>div>div:last-child {
    @apply text-gray-200 text-sm;
  }

  .login-right>h2 {
    @apply font-bold text-3xl text-gray-800;
  }

  .login-right>div {
    @apply flex items-center justify-center my-5 text-gray-300 space-x-2;

  }

  .login-line {
    @apply h-[1px] w-16 bg-gray-200;
  }

  .login-form-width {
    @apply w-[250px];
  }
  </style>

2.9 请求拦截器和响应拦截器(axios)

安装axios:

npm install axios

axios.js全代码:

import { useCookies } from '@vueuse/integrations/useCookies';
import axios from 'axios'
import { ElNotification } from 'element-plus';
import {
    getToken
} from '@/composables/auth'
const service = axios.create({
    baseURL:"/api"
})

// 添加请求拦截器
service.interceptors.request.use(function (config){

    // 在发送请求之前做些什么
    // 例:往header头自动添加token
    const cookie = useCookies()
    const token =  getToken()
    if(token){
        config.headers["token"] = token
    }
    return config;
    }, 
    function(error){
        // 对请求错误做些什么
        return Promise.reject(error)
    })

// 添加响应拦截器
service.interceptors.response.use(function(response){
    // 对响应数据做点什么
    return response.data.data;
    },
    function(error){
        // 对响应错误做些什么

        return Promise.reject(error)
    })

export default service

src/api/manager.js全代码:

import axios from "@/axios"

export function login(username,password){
    return axios.post("/admin/login",{
        username,
        password
    })
}

export function getinfo(){
    return axios.post("/admin/getinfo")
}

login.vue全代码: 

<template>
  <el-row class="login-container">
    <el-col :lg="16" :md="12" class="login-left">
      <div>
        <div>欢迎使用</div>
        <div>Vue3+Vite实战商城后台管理系统</div>
      </div>
    </el-col>
    <el-col :lg="8" :md="12" class="login-right">
      <h2>欢迎回来</h2>
      <div>
        <span class="login-line"></span>
        <span>账号密码登录</span>
        <span class="login-line"></span>
      </div>
      <el-form ref="formRef" :rules="rules" :model="form" class="login-form-width">
        <el-form-item prop="username">
          <el-input v-model="form.username" prefix-icon="user" placeholder="请输入用户名"  @keyup.enter.native="onFocusPassword">
          </el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input :rules="rules" v-model="form.password" placeholder="请输入密码" @keyup.enter.native="onSubmit" show-password type="password" ref="pwd" >
            <template #prefix>
              <el-icon>
                <lock />
              </el-icon>
            </template>
          </el-input>
        </el-form-item>
        <el-form-item>
          <el-button round color="#626aef" class="login-form-width" type="primary" @click="onSubmit" :loading="loading">登 录</el-button>
        </el-form-item>
      </el-form>
      <div>
        <span class="login-line"></span>
      <span>Version 1.0.20240822</span>
      <span class="login-line"></span>
      </div>
    </el-col>
  </el-row>
</template>
<script setup>
import { reactive,ref } from 'vue'
import { ElNotification } from 'element-plus'
import { login,getinfo } from '@/api/manager'
import { useRouter } from 'vue-router'
import { useCookies } from '@vueuse/integrations/useCookies'

const router = useRouter()

// do not use same name with ref
const form = reactive({
  username: '',
  password: ''
})

const formRef = ref(null) //让formRef类型为响应式
const loading = ref(false)
const pwd = ref(null)

const onFocusPassword = ()=>{
  pwd.value.focus();
}
const rules = {
  username:[
    {
      required: true, //必须输入
      message: '用户名不能为空', //验证不符条件时的提示信息
      trigger: 'blur' //触发为失去焦点时
    },{
      min: 2, //最小长度限定为2
      max: 15,//最大长度限定为15
      message: '用户名长度必须是2至15位字符', //验证不符条件时的提示信息
      trigger: 'blur' //触发为失去焦点时
    },
  ],
password:[
    { 
      required: true, //必须输入
      message: '密码不能为空', //验证不符条件时的提示信息
      trigger: 'blur' //触发为失去焦点时
    },{
      min: 5, //最小长度限定为5
      max: 15, //最大长度限定为15
      message: '密码长度必须是5至15位字符', ///验证不符条件时的提示信息
      trigger: 'blur' //触发为失去焦点时
    }
  ],
}


const onSubmit = () => {
  formRef.value.validate((valid)=>{  //valide用于接收触发验证的结果:true代表验证无错误,false代表验证有错误。
    if(!valid){
      return false  //返回验证失败,页面不做处理
    }
    loading.value = true
    login(form.username,form.password)
    .then(res=>{
      console.log(res);
      //提示登录成功
      ElNotification({
        message: "登录成功",
        type: "success",
        duration: 3000
      })

      // 存储token
      const cookie = useCookies()
      cookie.set("admin-token",res.token)

      // 获取用户相关信息
      getinfo().then(res2 => {
          console.log(res2)
      })

      // 跳转到后台首页
      router.push("/")
    })


  
  })
}
</script>
<style scoped>
.login-container {
  @apply min-h-screen bg-indigo-500;
}

.login-container .login-left,
.login-container .login-right {
  @apply flex items-center justify-center flex-col
}

.login-container .login-right {
  @apply bg-light-50 flex-col
}

.login-left>div>div:first-child {
  @apply font-bold text-5xl text-light-50 mb-4;
}

.login-left>div>div:last-child {
  @apply text-gray-200 text-sm;
}

.login-right>h2 {
  @apply font-bold text-3xl text-gray-800;
}

.login-right>div {
  @apply flex items-center justify-center my-5 text-gray-300 space-x-2;

}

.login-line {
  @apply h-[1px] w-16 bg-gray-200;
}

.login-form-width {
  @apply w-[250px];
}
</style>

细心的亲会发现在此login.vue代码中给“登录”按钮增加了一个loading效果:

  • 初始化一个ref类型的逻辑常量loading值为假:loadingconst loading = ref(false)

  • 验证表单后提交时即将loading值改为真:loading.value = true

  • 在.catch尾部增加:.finally(()=>{ loading.value = false }),将loading值改为假

  • 为“登录”按钮增加:loading="loading"属性

2.10 常用工具库封装

在src下新建文件夹composables,在此composables下新建文件:auth.js和util.js。

auth.js文件内容如下:

import { useCookies } from "@vueuse/integrations/useCookies"
const TokenKey = "admin-token"
const cookie = useCookies()

// 获取token
export function getToken(){
    return cookie.get(TokenKey)
}

// 设置token
export function setToken(){
    return cookie.set(TokenKey,token)
}


// 清除token
export function removeToken(){
    return cookie.remove(TokenKey)
}

util.js文件内容如下: 

import { ElNotification } from 'element-plus'

// 消息提示
export function toast(message,type = "success",dangerouslyUseHTMLString = false){
    ElNotification({
        message,
        type,
        dangerouslyUseHTMLString,
        duration:3000
    })
}

如有使用到如上封装工具库(函数类)时引用此文件并调用该工具(函数)即可。

 2.11 引入vuex状态管理用户信息

2.11.1 Vuex 是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

什么情况下我应该使用 Vuex?# Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。 如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是: Flux 架构就像眼镜:您自会知道什么时候需要它。

2.11.2 安装

npm install vuex@next --save

2.11.3 开始

在 Scrimba 上尝试这节课 每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同: Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

在src文件夹新建文件夹store,在此store文件夹下新建文件index.js:

import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
    state(){
      return {
        // 用户信息
        user: {}
      }
    },
    mutations: {
        // 记录用户信息
        SET_USERINFO(state,user){
            state.user = user
        }
      }
})
  
export default store

在main.js文件中增加引用及use:

// …………

import store from './store'

// …………

app.use(store)

// …………

src\pages\index.vue全代码修改为:

<script setup>
  import { useCookies} from '@vueuse/integrations/useCookies'
  const cookie = useCookies()
  console.log(cookie)

  function cookieSet(){
    cookie.set("admin-token","wzs888888")
    
  }

  function cookieGet(){
    console.log(cookie.get("admin-token"))
  }

  function cookieRemove(){
    cookie.remove("admin-token")
  }
</script>

<template>
  <div>
    后台首页 {{ $store.state.user }} // 特别强调下这里调用出user的数据,但因为还未设置全局路由截拦,页里刷新后此处的$store.state.user将无内容显示。
  </div>
</template>

2.12 全局路由截拦实现登录判断——router 导航守卫:全局前置守卫

router 导航守卫链接:导航守卫 | Vue Router

导航守卫:正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。

在src文件夹新建文件permission.js:

import router from "./router"
import { getToken } from "./composables/auth"
import { toast } from "@/composables/util"

// 全局前置守卫
router.beforeEach((to,from,next)=>{
    const token = getToken()

    // 没有登录,强制跳转至登录页
    if(!token && to.path != "/login"){
        toast('请先登录','error')
        return next({ path: "/login" })
    }

    // 防止重复登录
    if( token && to.path == "/login"){
        toast('请勿重复登录','error')
        return next({ path: from.path ? from.path : '/' })
    }

    next()
})

同时,在main.js中引入permission.js:

import './permission'

2.13 登录功能/退出功能完善

完善util.js内容:

import { ElNotification,ElMessageBox } from 'element-plus'

// 消息提示
export function toast(message,type = "success",dangerouslyUseHTMLString = false){
    ElNotification({
        message,
        type,
        dangerouslyUseHTMLString,
        duration:3000
    })
}

// 提示对话框
export function showModal(content = "提示内容",type = "warning",title="警告"){
    return ElMessageBox.confirm(
        content,title,
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type,
        }
    )
}

在src\api\manager.js中追加:

export function logout(){
    return axios.post('/admin/logout')
}

src\store\index.js:

import { removeToken, setToken } from '@/composables/auth'
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
    state(){
      return {
        // 用户信息
        user: {}
      }
    },
    mutations: {
        // 记录用户信息
        SET_USERINFO(state,user){
            state.user = user
        }
      },
    actions: {
      //登录
      login({ commit }, { username, password }){
        return new Promise((resolve,reject)=>{
          login(username,password).then(res=>{
            setToken(res.token)

            resolve(res)
          }).catch(err=>reject(err))
        })
      },

      // 获取当前登录用户信息
      getinfo({ commit }){
        return new Promise((resolve,reject)=>{
          getinfo().then(res=>{
            commit("SET_USERINFO",res)
            resolve(res)
          }).catch(err=>reject(err))
        })
      },

      // 退出登录
      logout({ commit }){
        
        // 移除cookie里的token
        removeToken()

        // 清除当前用户状态 vuex
        commit("SET_USERINFO",{})

      }
    }
})
  
export default store

src\pages\index.vue:

<template>
  <div class="ml-8 mt-5">
    <el-icon class="align-middle mr-2"><user /></el-icon>{{ $store.state.user.username}},欢迎你!
    <el-button @click="handleLogout">退出登录</el-button>
  </div>
</template>

<script setup>
  import { logout } from '@/api/manager'
  import { showModal,toast } from '@/composables/util'
  import { useRouter } from 'vue-router'
  import { useStore } from 'vuex'

  
  const router = useRouter()
  const store = useStore()

  function handleLogout(){
      showModal("是否要退出登录?").then(res=>{
        logout()
        .finally(()=>{

          // 执行退出动作
          store.dispatch("logout")

          // 跳转到登录页
          router.push('/login')
          // 提示退出成功
          toast("退出登录成功")
        })
      })

  }
</script>

2.14 全局loading进度条实现 —— nprogress:全局后置守卫

安装命令:

npm i nprogress

在main.js中追加:

import 'nprogress/nprogress.css'

在src\composables\util.js中追加:

// 显示全屏loading
export function showFullLoading(){
    nprogress.start()

}
// 隐藏全屏loading
export function hideFullLoading(){
    nprogress.done()
}

附src\composables\util.js完整代码:

import { ElNotification,ElMessageBox } from 'element-plus'
import nprogress from 'nprogress' //增加引入

// 消息提示
export function toast(message,type = "success",dangerouslyUseHTMLString = false){
    ElNotification({
        message,
        type,
        dangerouslyUseHTMLString,
        duration:3000
    })
}

// 提示对话框
export function showModal(content = "提示内容",type = "warning",title="警告"){
    return ElMessageBox.confirm(
        content,title,
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type,
        }
    )
}

// 启用loading效果:显示全屏Nprogress loading
export function showFullLoading(){
    nprogress.start()

}
// 关闭loading效果:隐藏全屏Nprogress loading
export function hideFullLoading(){
    nprogress.done()
}

src\App.vue代码:

<script setup>

</script>

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

<style>
/* 为Npgress的bar设置颜色和高度 */
#nprogress .bar{
  background-color: #179963 !important;
  height: 3px !important;
} 
</style>

src\store\permission.js代码:

import router from "./router"
import { getToken } from "./composables/auth"
import { 
    toast,
    showFullLoading,
    hideFullLoading
} from "@/composables/util"

// 全局前置守卫
router.beforeEach((to,from,next)=>{
    // 显示loading
    showFullLoading()


    const token = getToken()

    // 没有登录,强制跳转至登录页
    if(!token && to.path != "/login"){
        toast('请先登录','error')
        return next({ path: "/login" })
    }

    // 防止重复登录
    if( token && to.path == "/login"){
        toast('请勿重复登录','error')
        return next({ path: from.path ? from.path : '/' })
    }

    next()
})

// 全局后置守卫
router.afterEach((to, from) =>{
    // sendToAnalytics(to.fullPath)
    hideFullLoading()
})

2.15 动态页面标题实现

2.15.1 在src\router\index.js中为每个模块增加  meta: { title: "页面描述" }

//index.js
import {
    createRouter,
    createWebHashHistory
} from 'vue-router'

import Index from '@/pages/index.vue'
import About from '@/pages/about.vue'
import Login from '@/pages/login.vue'
import NotFound from '@/pages/404.vue'
const routes =[{
    path:"/",
    component:Index,
    meta: { title: "后台首页" } // 这部分是为实现动态页面标题而添加的部分
},{
    path:"/about",
    component:About,
    meta: { title: "关于我们" } // 这部分是为实现动态页面标题而添加的部分
},{
    path:"/login",
    component:Login,
    meta: { title: "登录界面" } // 这部分是为实现动态页面标题而添加的部分
},{
    path:"/:pathMatch(.*)*",
    name:"NotFound",
    component:NotFound
}]

const router = createRouter({
    history:createWebHashHistory(),
    routes
})

export default router

2.15.2 在src\permission.js中的全局前置路由守护 router.beforeEach 中 next() 之前添加:

    // 设置页面标题
    let title = (to.meta.title ? to.meta.title : "") + " - Vue管理系统"
    document.title = title

附permission.js全部代码

import router from "./router"
import { getToken } from "./composables/auth"
import { 
    toast,
    showFullLoading,
    hideFullLoading
} from "@/composables/util"

// 全局前置守卫
router.beforeEach((to,from,next)=>{
    // 显示loading
    showFullLoading()


    const token = getToken()

    // 没有登录,强制跳转至登录页
    if(!token && to.path != "/login"){
        toast('请先登录','error')
        return next({ path: "/login" })
    }

    // 防止重复登录
    if( token && to.path == "/login"){
        toast('请勿重复登录','error')
        return next({ path: from.path ? from.path : '/' })
    }

    // 设置页面标题 这部分是为实现动态页面标题而添加的部分
    let title = (to.meta.title ? to.meta.title : "") + " - Vue管理系统"
    document.title = title
    
    next()
})

// 全局后置守卫
router.afterEach((to, from) =>{
    // sendToAnalytics(to.fullPath)
    hideFullLoading()
})

3 后台全局Layout布局开发

3.1 后台主布局实现

3.1.1 后台主布局文件:src\layouts\admin.vue

在src下新建layouts文件夹,并在此文件夹下新建文件admin.vue:

<template>
    <el-container>
        <el-header><fheader/></el-header>
        <el-contanin>
                <el-aside><fmenu/></el-aside>
                <el-main>
                    <ftaglist/>
                    <router-view></router-view>
                </el-main>
        </el-contanin>
    </el-container>
</template>
<script setup>
import fheader from './components/fheader.vue'
import fmenu from './components/fmenu.vue'
import ftaglist from './components/ftaglist.vue'
</script>

3.1.2 将admin在src\router\index.js引入并修改路由:

修改前:

const routes =[{
    path:"/",
    component:Index,
    meta: { title: "后台首页" }
},{
    path:"/about",
    component:About,
    meta: { title: "关于我们" }
},{
    path:"/login",
    component:Login,
    meta: { title: "登录界面" }
},{
    path:"/:pathMatch(.*)*",
    name:"NotFound",
    component:NotFound
}]

修改后:

const routes =[{
    path:"/",
    component:Admin,
    children:[{
        path:"/",
        component:Index,
        meta: { title: "后台首页" }
    }]
},{
    path:"/about",
    component:About,
    meta: { title: "关于我们" }
},{
    path:"/login",
    component:Login,
    meta: { title: "登录界面" }
},{
    path:"/:pathMatch(.*)*",
    name:"NotFound",
    component:NotFound
}]

后续完善头部fhead、侧边菜单fmenu、标签列表ftaglist等三个文件代码内容。

3.2 公共头部开发样式布局

……待续……

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值