vue3 admin后台管理开发笔记

在线体验链接:http://amv3admin.top/#/login

项目地址:https://gitee.com/xiao-ming-1999/am-vue3.git

vscode开发vue3插件:volar(使用此插件时,把vetur禁用,vetur是vue2插件)

1、开发依赖

  1. 下载vue3脚手架并配置路由 (vue3+vue-router4+vite+eslint+pinia)

  • 创建vite项目 npm init vite@latest my-vue-app -- --template vue

  • 下载路由并配置路由 npm install vue-router@4

import { createRouter,createWebHashHistory } from "vue-router";
import Index from '@/pages/index.vue'
import Login from '@/pages/login.vue'
import NotFound from '@/pages/404.vue'

const routes = [
  { path: '/', component: Index },
  { path: '/login', component: Login },
  // 404页面
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
]

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

export default router
  1. 组件库选用element-plus样式库windi CSS

  • 下载element-plus和windi CSS并引入至main.js

  • npm install element-plus --save

  • npm i -D vite-plugin-windicss windicss

import { createApp } from 'vue'

import App from './App.vue'
import ElementPlus from 'element-plus'
import router from './router/index';

import 'element-plus/dist/index.css'
import 'virtual:windi.css'

const app = createApp(App)

app.use(ElementPlus)
app.use(router)

app.mount('#app')
  1. 全局状态管理pinia(替代vuex)

  • npm i pinia

tip:piana真的太香了~~比起vuex哪种复杂的写法,piana简直不要太好!(都给我用起来!)

附上使用pinia说明(链接),详情看官网~

为什么使用windi CSS?

1、提升开发效率 2、自带兼容 3、功能较为完善

2、vite使用到的包

  1. 自动导入Element Plus组件、图标 (自动引入太香了~) (链接) unplugin-icons、unplugin-auto-import、unplugin-vue-components

自动导入后使用 icon组件加前缀IEp 例: -> 自动引入图标废除,主页菜单会涉及到 动态渲染菜单图标,自动导入无法根据动态值来正确引入图标(以改为全局引入图标)

3、 VUE3新特性

1、指令

v-loading:指令元素loading效果,参数为布尔值

2、vue3使用需要注意的点

  1. 使用ref时,也要在script内声明ref变量。

例如


  1. 父组件通过ref使用子组件方法或变量时,子组件必须使用defineExpose暴露方法父组件才能拿到子组件的方法和变量

  1. 子组件props与$emit


  

3、vue-use提供的方法

useDateFormat时间戳转时间

import { useDateFormat } from '@vueuse/core'


const props = defineProps({
  info: Object
})

// 付款时间戳转换
const paid_time = computed(() => {
  if (props.info.paid_time) {
    const s = useDateFormat(props.info.paid_time * 1000, 'YYYY-MM-DD HH:mm:ss')
    return s.value
  }
  return ''
})

4、bug记录

4.1、订单列表页

vue3 admin后台管理开发笔记_第1张图片

通过注释找到bug

vue3 admin后台管理开发笔记_第2张图片

·全局配置、公共组件封装、框架初始化(重点)

1、axios配置

import axios from "axios";
import { getToken } from "@/composables/auth.js";
import { toast } from "@/composables/util.js";
import { mainStore } from '@/store/index.js'

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


// 请求拦截器
service.interceptors.request.use(function (config) {
  // 设置请求头
  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) {
  // 对响应错误做点什么
  const store = mainStore()
  const msg = error.response.data.msg || '请求失败'
  if (msg === '非法token,请先登录!') {
    store.LOGOUT().finally(() => location.reload())
  }
  toast(msg, 'error')

  return Promise.reject(error);
});

export default service
  • baseUrl

  • 请求拦截器(调接口前给header添加token)

  • 响应拦截器的封装(对返回的数据简化处理,对错误请求进行封装)

  • 一定要对请求错误响应错误做处理(返回失败的promise)否则会出现逻辑错误

2、登录页

  • 登录逻辑:login接口,存储token,获取用户信息、跳转至首页

  • 存储token将token存储至cookie中,使用到vueuse库的useCookies方法

3、封装工具库配置

  • 封装操作cookie方法

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

export function getToken(){
 return cookie.get('admin-token')
}

export function setToken(token){
  cookie.set(tokenKey, token)
}

export function removeToken(){
  cookie.remove(tokenKey)
}
  • 封装提示消息

export function toast(message,type='success',dangerouslyUseHTMLString=false) {
  ElNotification({
    message,
    type,
    dangerouslyUseHTMLString,
    duration: 3000
  })
}

4、全局状态管理pinia配置

  1. main.js注册pinia

import { createApp } from 'vue'


import App from './App.vue'
import router from './router/index';
import { createPinia } from 'pinia'




// 样式引入
import 'element-plus/dist/index.css'
import 'virtual:windi.css'


const app = createApp(App)
const pinia = createPinia()


app.use(router)
app.use(pinia)


import "./permission.js";
app.mount('#app')
  1. 定义store仓库

// 1、定义状态容器
// 2、修改容器中的state
// 3、仓库中的action的使用

import { defineStore } from "pinia";
import { getInfo } from '@/api/manager.js'
// defineStore参数1为仓库id(唯一值)
export const mainStore = defineStore('main', {
  state: () => {
    return {
      // 用户信息
      user: {}
    }
  },
  getters: {
  },
  actions: {
    SET_USERINFO (userInfo) {
      this.user = userInfo
    },
    // 获取用户信息 并且 设置用户信息
    GET_INFO () {
      return new Promise((resolve, reject) => {
        getInfo().then(res => {
          console.log(res,'res');
          this.SET_USERINFO(res)
          resolve(res)
        }).catch(err => {
          reject(err)
        })
      })
    }
  }
})
  1. 使用pinia,存储用户信息

5、路由前置守卫配置

  1. main.js引入permission.js (代码省略)

  1. 前置守卫:对操作进行检查,用户信息持久化

import router from '@/router/index.js'
import { getToken } from "@/composables/auth.js";
import { toast } from "@/composables/util.js";
import { mainStore } from "@/store/index.js";


// 全局前置守卫
router.beforeEach(async (to, from, next) => {
  const store = mainStore()
  
  const token = getToken()
  // 没有登录,强制跳回登录
  if (!token && to.path !== '/login') {
    toast('请先登录~默认账号密码:admin', 'error')
    return next({ path: "/login" })
  }
  // 防止重复登录检验
  if (token && to.path === '/login') {
    toast('请勿重复登录', 'error')
    return next({ path: from.path || '/' })
  }

  if(token) {
   await store.GET_INFO()
  }
  next()

})

6、退出登录页

  1. 封装弹框提示

  1. 退出登录接口封装

  1. 基本逻辑:

// 1、定义状态容器
// 2、修改容器中的state
// 3、仓库中的action的使用

import { defineStore } from "pinia";
import { getInfo } from '@/api/manager.js'
import { removeToken } from '@/composables/auth.js'
import { toast } from '@/composables/util.js'
import { useRouter } from 'vue-router'
import { logout } from '@/api/manager.js'
const router = useRouter()
// defineStore参数1为仓库id(唯一值)
export const mainStore = defineStore('main', {
  state: () => {
    return {
      // 用户信息
      user: {}
    }
  },
  getters: {
  },
  actions: {
    SET_USERINFO (userInfo) {
      this.user = userInfo
    },
    // 获取用户信息 并且 设置用户信息
    GET_INFO () {
      return new Promise((resolve, reject) => {
        getInfo().then(res => {
          this.SET_USERINFO(res)
          resolve(res)
        }).catch(err => {
          reject(err)
        })
      })
    },
    REMOVE_INFO () {
      this.user = {}
    },
    async LOGOUT () {
      // 1、调退出登录接口
      await logout()
      // 2、清除cookie内的token
      removeToken()
      // 3、清空vuex内user状态
      this.REMOVE_INFO()
      // 4、提示退出登录成功
      toast('退出登录成功')
      // 5、跳回登录页
      router.push('/login')
    }
  }
})

7、配置全局loading效果

  1. npm i nprogress

  1. 根据nprogress文档使用 引入相应的css和js封装进工具库

  1. 路由前置守卫开启loading 后置守卫关闭loading

  1. 效果图:

8、动态修改网站title

vue3 admin后台管理开发笔记_第3张图片
  1. router.js内配置meta.title

const routes = [
  {
    path: '/', component: Index,
    meta: {
      title: '后台首页'
    }
  },
  {
    path: '/login', component: Login,
    meta: {
      title: '登录'
    }
  },
  // 404页面
  {
    path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound,
    meta: {
      title: '404'
    }
  },
]
  1. 路由前置守卫获取到to.meta.title,动态修改文档title

// 全局前置守卫
router.beforeEach(async (to, from, next) => {
  const title = `${to.meta.title || ''}-vue3商城后台管理`
  document.title =title
})

9、首页框架搭建

  • el组件搭建 重点**(router-view)**

  • transition相关:链接

  • keep-alive相关:链接


  

  • 配置路由

// 主体框架
  {
    path: '/',
    component: Admin,
    // 子路由
    children: [
      {
        path: '/',
        component: Index,
        meta: {
          title: '后台首页'
        },
      }
    ]
  },

10、封装el弹框组件

vue3 admin后台管理开发笔记_第4张图片

  



11、首页顶部header

代码拆分思路**(重点):**使用组合式api拆分,确定哪些参数为变量,变量部分用接收的参数来替换,确保封装的js代码,可以多组件复用

顶部代码封装(利于后期维护):

// 退出登录
export function useLogout () {
  const router = useRouter()
  const store = mainStore()
  function handleLogout () {
    showModel('是否退出', 'warning')
      .then(async (res) => {
        await logout()
        store.LOGOUT()
        // 4、提示退出登录成功
        toast('退出登录成功')
        // 5、跳回登录页
        router.push('/login')
      })
      .catch((err) => {
        console.log(err, 'err')
      })
  }
  // 返回函数
  return {
    handleLogout
  }
}
  • 使用:






12、首页左侧菜单 重点!!(子导航多层嵌套)

vue3 admin后台管理开发笔记_第5张图片

子导航多层嵌套:菜单数据主要分为两种情况,一级菜单(没有child),多级菜单(有child,且有可能child后还有child)涉及到循环套用,组件使用也可以套用递归思想(一定要将组件拆分,否则容易报错)



  




  

13、动态路由添加 (重点!)

先看代码 (此章节资料 vue-router **:**在导航守卫中添加路由添加嵌套路由

import { createRouter, createWebHashHistory } from "vue-router";
import Index from '@/pages/index.vue'
import Login from '@/pages/login.vue'
import NotFound from '@/pages/404.vue'
import Admin from '@/layouts/admin.vue'
import GoodsList from '@/pages/goods/list.vue'
import CategoryList from '@/pages/category/list.vue'



// 默认路由所用用户共享
const routes = [
  {
    path: '/',
    // 为什么要加name vue-router规定如果有嵌套路由,父路由必须有name值 (何为嵌套?有子路由)
    name: 'admin',
    component: Admin
  },
  {
    path: '/login',
    component: Login,
    meta: {
      title: '登录'
    }
  },
  // 404页面
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound,
    meta: {
      title: '404'
    }
  },
]


// 动态路由,用于匹配菜单动态添加路由
const asyncRoutes = [
  {
    path: '/',
    name: '/',
    component: Index,
    meta: {
      title: '后台首页'
    },
  },
  {
    path: '/goods/list',
    name: '/goods/list',
    component: GoodsList,
    meta: {
      title: '商品管理'
    },
  },
  {
    path: '/category/list',
    name: '/category/list',
    component: CategoryList,
    meta: {
      title: '分类管理'
    },
  },
]



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


// 动态添加路由方法
export function addRoutes (menus) {
  // 是否有新的路由
  let hasNewRoutes = false
  // 递归方法 获取用户信息后,触发addRoutes方法,将菜单数据后往默认路由内添加路由,如果已经有了同样名字的路由则跳过,如果有child就再次调该方法递归
  const findAndAddRouteByMenus = (arr) => {
    arr.forEach(e => {
      // 菜单路由数据是否与已有路由的path匹配,匹配返回当前item项,不匹配返回undefined (如果匹配说明路径正确,该组件会被正常渲染,若不匹配则前端更改path路径)
      let item = asyncRoutes.find(o => o.path === e.frontpath)
      //  router.hasRoute():检查是否为注册过的路由
      if (item && !router.hasRoute(item.path)) {  // 存在且为未注册的路由
        router.addRoute('admin', item)
        hasNewRoutes = true
      }
      if (e.child && e.child.length > 0) {
        findAndAddRouteByMenus(e.child)
      }
    })
  }
  findAndAddRouteByMenus(menus)
  console.log(router.getRoutes(),'查看已有路由');
  return hasNewRoutes
}
vue3 admin后台管理开发笔记_第6张图片

问题1:这样配好后,刷新页面,会返回到404页面,原因:

vue3 admin后台管理开发笔记_第7张图片
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
  const store = mainStore()
  const token = getToken()
  const title = `${to.meta.title || ''}-vue3商城后台管理`


  showFullLoading()


  // 没有登录,强制跳回登录
  if (!token && to.path !== '/login') {
    toast('请先登录~默认账号密码:admin', 'error')
    return next({ path: "/login" })
  }
  // 防止重复登录检验
  if (token && to.path === '/login') {
    toast('请勿重复登录', 'error')
    return next({ path: from.path || '/' })
  }
  // 检测是否有新的路由
  let hasNewRoutes = false
  if (token) {
    const { menus } = await store.GET_INFO()
    hasNewRoutes = addRoutes(menus)
    console.log(hasNewRoutes, 'hasNewRoutes');
  }


  document.title = title

  // 用于解决刷新404问题,路由需要手动指向路径 next(to.fullPath)
  hasNewRoutes ? next(to.fullPath) : next()


})

14、首页tabs封装

vue3 admin后台管理开发笔记_第8张图片

tip:菜单会和tab联动(即点击菜单导航后会新增或跳转至相应tab)

  • 菜单和tab联动(即点击菜单,tab组件自动高亮选中项):tbs组件中,主要通过onBeforeRouteUpdate监听路由更新事件,给activeTab赋值

  • tab和菜单联动(点击tag,菜单自动选中):菜单组件中,通过vuerouter的onBeforeRouteUpdate组件守卫,监听路由更新,并给选中项赋值

封装代码:

import { ref } from 'vue'
import { useRouter, useRoute, onBeforeRouteUpdate } from 'vue-router'
import { mainStore } from '@/store/index'
import { useCookies } from '@vueuse/integrations/useCookies'
export function useTabList (params) {
  const store = mainStore()
  const route = useRoute()
  const router = useRouter()
  const cookie = useCookies()


  const activeTab = ref(route.path)
  const tabList = ref([
    {
      path: '/',
      title: '后台首页'
    }
  ])
  // 初始化tabList
  function initTabList () {
    const tabs = cookie.get('tabList')
    if (tabs) {
      tabList.value = tabs
    }
  }
  initTabList()
  // 监听事件, activeTab 变化触发 实参为activeTab变化后的值
  function changeTab (path) {
    router.push(path)
  }
  // 下拉框 关闭tab事件
  function handleClose (e) {
    // 关闭其他
    if (e === 'clearOther') {
      tabList.value = tabList.value.filter(
        (item) => item.path === '/' || item.path === activeTab.value
      )
    }
    if (e === 'clearAll') {
      tabList.value = tabList.value.filter((item) => item.path === '/')
      activeTab.value = '/'
    }
    cookie.set('tabList', tabList.value)
  }


  // 删除逻辑:判断是否为高亮tab,如果是,则给高亮tab重新赋值(赋值规则:高亮的上一个或高亮的下一个)
  function removeTab (t) {
    // 声明变量赋值是为了简化代码.value写法会使代码冗余
    let tabs = tabList.value
    let a = activeTab.value


    if (a === t) {
      tabs.forEach((item, index) => {
        if (item.path === t) {
          const nextTab = tabs[index + 1] || tabs[index - 1]
          if (nextTab) {
            a = nextTab.path
          }
        }
      })
    }
    activeTab.value = a
    tabList.value = tabList.value.filter((item) => item.path !== t)
    cookie.set('tabList', tabList.value)
  }
  // tabList添加事件(路由改变添加tbaList)
  function addTabs (tab) {
    const noTab = tabList.value.findIndex((o) => o.path === tab.path) === -1
    if (noTab) {
      tabList.value.push(tab)
    }
    cookie.set('tabList', tabList.value)
  }
  // 监听路由更新事件(主要通过这个来联动菜单)
  onBeforeRouteUpdate((to, from) => {
    activeTab.value = to.path
    // 获取路由信息
    addTabs({ path: to.path, title: to.meta.title })
  })
  return {
    store,
    activeTab,
    tabList,
    changeTab,
    removeTab,
    handleClose
  }
}

  


// 监听路由变化
onBeforeRouteUpdate((to, from) => {
  defaultActive.value = to.path
})

15、自定义指令(v-permission)

permission 指令对于没有权限的组件、元素进行删除

v-permission :接收一个数组,例:['getStatistics1,GET'], 自定义指令函数内将接收到的实参与sotre内的权限数组做对比,如果存在则返回true,不存在则返回false,并删除使用v-permission的对应组件。

import { mainStore } from '@/store/index.js'
function hasPermission (value, el = false) {

  if (!Array.isArray(value)) {
    throw new Error('需要配置权限,例如 v-permission="["getStatistics2","GET"]"')
  }
  const store = mainStore()
  // value为数组
  // includes 方法可以判断一个数组中是否包含某一个元素, 并且返回true  或者false
  // findIndex:获取第一个符合判断条件的值索引,若返回值为布尔,true为0,false为-1
  const hasAuth = value.findIndex(v => store.ruleNames.includes(v)) != -1
  // hasAuth为false则说明没有权限

  // 如果有元素且没有权限,则获取该元素父节点,删除其子节点
  if(el && !hasAuth) {
    el.parentNode && el.parentNode.removeChild(el) 
  }
  return hasAuth
}
export default {
  install (app) {
    // 自定义指令 permission
    app.directive('permission', {
      mounted (el, binding) { // el元素节点 binding.value:v-permission绑定的值
        hasPermission(binding.value, el)
      }
    })
  }
}

自定义指令注册(必须在createApp(App)之后)

// 自定义指注册
import permission from "@/directives/permission.js";
app.use(permission)

使用自定义指令 v-permission

功能模块开发、组件封装

1、首页开发

vue3 admin后台管理开发笔记_第9张图片

功能:骨架屏(el骨架屏)、数字动画(gsap第三方包**)、echarts图标渲染**

数字动画公共组件封装:


  

首页组件(部分组件做了拆分)


2、图库管理

开发思路:内容组件包括 侧边栏组件+主体组件

功能:表单新增图片分类,上传图片







  





  

3、公告管理页

vue3 admin后台管理开发笔记_第10张图片

  

4、管理员管理页

**功能:**点击新增或修改,弹出抽屉组件,选择头像弹出选择图片组件

复杂难点:

  • 图片组件封装:复用图库管理业,需将imageAmin组件多加一个复选框(第三个图红框标注)

vue3 admin后台管理开发笔记_第11张图片
vue3 admin后台管理开发笔记_第12张图片

  


  


const getData = async (p = null) => {
  if (typeof p == 'number') {
    currentPage.value = p
  }
  loading.value = true
  const res = await getImageList(image_class_id.value, currentPage.value)
  srcList.value = res.list.map((item) => {
    return item.url
  })
  // 给每个对象加一个checked属性
  list.value = res.list.map((item) => {
    item.checked = false
    return item
  })
  total.value = res.totalCount
  loading.value = false
}
// checked选中的图片
const checkedImage = computed(() => {
  return list.value.filter((item) => item.checked)
})
const emit = defineEmits(['choose'])
// 复选框选中图片事件
const handleChooseChange = (item) => {
  if (item.checked && checkedImage.value.length > 1) {
    item.checked = false
    return toast('最多只能选中一张', 'error')
 	 }
 	 // 触发父组件事件 将选中的图片返回给父组件
 	 emit('choose', checkedImage.value)
}

5、公共逻辑拆分(页面公共逻辑(获取数据,分页)+表单逻辑)(重点++)

5.1、表单逻辑拆分useInitForm
  • 参数拆分:表单、表单ref、抽屉ref、表单规则、修改id、弹框title、当前页码数(点击确定,刷新数据时需要)。

  • 方法拆分:表单的新增、修改、提交

5.2、页面公共部分拆分userInitTable
  • 参数拆分:列表数据,分页参数(limit,curretpage、total),搜索参数,

  • 方法拆分:刷新方法、getData、修改状态方法、删除方法

import { ref, reactive, computed } from 'vue'
import { toast } from '@/composables/util.js'
// 页面公共部分(分页+列表+删除+搜索,修改状态)逻辑拆分
// opt参数 必传:getList(获取列表数据的接口) 
// 选传:searchForm(搜索参数)、updateStatus(修改状态的接口)、
// delete(删除状态的接口)、onGetListSuccess(获取数据后对数据进行处理的回调)
export function userInitTable (opt = {}) {
  const tableData = ref([])
  const loading = ref(false)
  // 分页参数
  const currentPage = ref(1)
  const limit = ref(10)
  const total = ref(0)
  // 搜索
  let searchForm = null
  let resetSearchForm = null
  // 搜索参数可能会有多个,需要使用组件传递对应搜索参数,公共组件动态获取
  if (opt.searchForm) {
    searchForm = reactive({ ...opt.searchForm })

    resetSearchForm = () => {
      // opt.searchForm的格式searchForm: {keyword: ''},使用组件传的值必定为空,循环给searchForm初始化值
      for (const key in opt.searchForm) {
        searchForm[key] = opt.searchForm[key]
      }
      getData()
    }
  }

  const getData = async (p = null) => {
    // p为当前页码数
    if (typeof p == 'number') {
      currentPage.value = p
    }
    loading.value = true
    const res = await opt.getList(currentPage.value, searchForm)
    // 部分组件需要返回特殊的参数,如:每个item中都要返回一个checked属性,那么将执行使用组件传来的逻辑,返回对应参数
    if (opt.onGetListSuccess && typeof opt.onGetListSuccess == 'function') {
      opt.onGetListSuccess(res)
    } else {
      tableData.value = res.list
      total.value = res.totalCount
    }
    loading.value = false
  }
  getData()
  // 修改状态
  const handleStatusChange = async (status, row) => {
    row.statusLoading = true
    await opt.updateStatus(row.id, status)
    row.statusLoading = false
    toast('修改状态成功')
    row.status = status
  }
  // 删除
  const handleDelete = async (id) => {
    loading.value = true
    try {
      await opt.delete(id)
      toast('删除成功')
      getData()
    } catch (err) {
      loading.value = false
    }
  }
  return {
    searchForm,
    resetSearchForm,
    tableData,
    limit,
    loading,
    total,
    currentPage,
    getData,
    handleStatusChange,
    handleDelete
  }
}

// 表单(新增+修改+提交)逻辑拆分
// opt参数 必传:form(表单初始值)、
// 可选:title(表单标题)、currentPage(当前页:必须在userInitTable之后)、
// update(修改表单接口)、create(新增表单接口)
export function useInitForm (opt = {}) {

  const formDrawerRef = ref(null)
  const formRef = ref(null)
  // 表单参数
  const defaultForm = opt.form
  let form = reactive({})
  // 表单规则
  const rules = opt.rules || {}
  // 修改id
  const editId = ref(0)
  // 弹框title
  const drawerTitle = computed(() => {
    return editId.value ? '修改' + opt.title : '新增' + opt.title
  })
  // 当前页码数
  const currentPage = ref(1)
  currentPage.value = opt.currentPage

  // 提交表单
  const handleSubmit = () => {
    formRef.value.validate(async (valid) => {
      if (!valid) return false
      formDrawerRef.value.showLoading()
      try {
        const Fun = editId.value
          ? opt.update(editId.value, form)
          : opt.create(form)
        console.log(defaultForm, 'defaultForm');
        const data = await Fun

        toast(drawerTitle.value + '成功')
        // 修改刷新当前页,新增刷新第一页
        opt.getData(editId.value ? currentPage.value : 1)
        formDrawerRef.value.close()
      } catch (err) {
        console.log(err);
      }
      formDrawerRef.value.hideLoading()
    })
  }
  // 重置表单
  const resetForm = (row = {}) => {
    if (formRef.value) formRef.value.clearValidate()
    // 这里for in defaultForm的原因是,defaultForm为原始值。
    // 即:原始值为{title:'xx',content:'xx'}
    // 触发编辑事件后 form的值就会为row的值 
    // 即:{title:'xx',content:'xx',create_time:'2022-12',update_time:'2022-12'}
    // 当点击修改后,如果for in的是参数row的话,form的原始数据结构会被改变,会向接口传多余参数,导致报错
    // 所以for in defaultForm就是为了点击修改时,给form的key规定好为原始的key值
    for (const key in defaultForm) {
      form[key] = row[key]
    }
  }
  // 新增
  const handleCreate = () => {
    editId.value = 0
    resetForm(defaultForm)
    formDrawerRef.value.open()
  }
  // 编辑
  const handleUpdate = (row) => {
    editId.value = row.id
    resetForm(row)
    formDrawerRef.value.open()
  }

  return {
    formDrawerRef,
    formRef,
    form,
    rules,
    editId,
    drawerTitle,
    handleSubmit,
    resetForm,
    handleCreate,
    handleUpdate
  }

}

使用useCommon.js

import { userInitTable } from '@/composables/useCommon.js'
const roles = ref([])
const { searchForm,resetSearchForm, tableData, limit, loading, total, currentPage, getData } =
  userInitTable({
    searchForm: {
      keyword: '',
    },
    getList: getManagerList,
    onGetListSuccess: (res) => {
      tableData.value = res.list.map((item) => {
        item.statusLoading = false
        return item
      })
      roles.value = res.roles
      total.value = res.totalCount
      loading.value = false
    }
  })

6

6、权限管理页(菜单、接口配置)

功能:el-tree构建页面,弹窗复用公共组件FormDrawer,表单新增,表单逻辑复用标题5的公共逻辑

难点:组件配置项太多,容易弄混。复杂的配置项在模板中有标注(el-cascader级联选择器,el-tree)

**核心:**主要是通过addRoute添加动态路由(router.js页)

vue3 admin后台管理开发笔记_第13张图片

  

vue3 admin后台管理开发笔记_第14张图片

  

  

7、角色管理页(角色权限配置)

功能:列表展示、分页、表单新增,与公共管理类似,多了一个配置权限功能。代码复用标题5的公共逻辑

难点:配置权限弹框的虚拟树渲染,配置项容易出错(模板中已标注)

vue3 admin后台管理开发笔记_第15张图片
vue3 admin后台管理开发笔记_第16张图片

  

8、规格管理页(商品默认规格配置)

功能:列表展示、分页、表单新增(复用角色管理页模板,复用公共逻辑)

新增:公共组件新增:**公共组件tagInput.vue,**公共逻辑新增:多选、批量删除

vue3 admin后台管理开发笔记_第17张图片
vue3 admin后台管理开发笔记_第18张图片

  






export function useInitTable (opt = {}) { 
  // 复选框多选选中id
  const multiSelectionIds = ref([])
  const handleSelectionChange = (e) => {
    const ids = e.map(o => { return o.id })
    multiSelectionIds.value = ids
  }
  // 批量删除
  const multipleTableRef = ref(null)
  const handleMultiDelete = async () => {
    if (!multiSelectionIds.value.length) return toast('请选择至少一个选项', 'warning')
    try {
      await opt.delete(multiSelectionIds.value)
      toast('删除成功')
      getData()
      multipleTableRef.value.clearSelection()
    } catch (err) {
      console.log(err, 'err');
    }
  }
    return {
    handleSelectionChange,
    multipleTableRef,
    handleMultiDelete
  }
}

9、优惠券列表

功能:列表展示、分页、表单新增(复用角色管理页模板,复用公共逻辑)

复杂难点:

  1. 优惠券状态判断:在onGetListSuccess回调内调用格式化状态函数。

  1. 时间选择器时间戳转换:在公共逻辑提交事件内,声明一个body变量,判断是否有beforeSubmit事件,有该事件,将该事件赋值给body,则调用该函数并将form传给该回调函数,回调函数将时间转换为时间戳后再reture回去,调用提交接口时,将form替换为body

vue3 admin后台管理开发笔记_第19张图片
vue3 admin后台管理开发笔记_第20张图片

  


// 提交表单
  const handleSubmit = () => {
    formRef.value.validate(async (valid) => {
      if (!valid) return false
      formDrawerRef.value.showLoading()
      try {
        // 新增start:将form赋值给body,传给回调,处理时间戳
        let body ={}
        if(opt.beforeSubmit && typeof opt.beforeSubmit =='function' ) {
          body =opt.beforeSubmit({...form})
        }else {
          body = form.value
        }
        // 新增end

        const Fun = editId.value
          ? opt.update(editId.value, body)
          : opt.create(body)
        const data = await Fun


        toast(drawerTitle.value + '成功')

        opt.getData(editId.value ? currentPage.value : 1)
        formDrawerRef.value.close()
      } catch (err) {
        console.log(err);
      }
      formDrawerRef.value.hideLoading()
    })
  }

10、商品管理页(较复杂)

**功能:**下图红框内功能,列表展示、分页、表单新增(复用代码)

复杂难点:功能点多,代码量大

vue3 admin后台管理开发笔记_第21张图片
10.1、页面渲染:将页面分为四部分(见上图红框)

页面渲染:多参数搜索(复用search.vue)、按钮组件(复用ListHeader.vue),table和分页(复用规格管理页)、新增tab切换

10.2、设置轮播图弹框:新增banner弹框组件,banner组件内复用chooseImage.vue
vue3 admin后台管理开发笔记_第22张图片
vue3 admin后台管理开发笔记_第23张图片

  

  
10.3、商品规格设置(动态表格渲染)

商品规格选项弹框主要复杂点在于多规格部分

多规格部分思路:分为两部分,1、规格选项部分(skuCard.vue) 规格选项部分包含(skuCardItem.vue(每一项规格选项)) 2、规格table表格部分(skuTable.vue)

js部分:涉及多组件使用同一值,用传统方法写js的话,值的传递太过复杂,所以使用vue3组合式js,组合式内定义的值可以多组件使用并****保持相同性 将大部分逻辑写在useSku.js内

vue3 admin后台管理开发笔记_第24张图片

  

  


  

  


  

  

import { ref, nextTick, computed } from "vue";
import {
  createGoodsSkusCard,
  updateGoodsSkusCard,
  deleteGoodsSkusCard,
  sortGoodsSkusCard,
  createGoodsSkusCardValue,
  updateGoodsSkusCardValue,
  deleteGoodsSkusCardValue,
  chooseAndSetGoodsSkusCard
} from "@/api/goods.js";
import { useArrayMoveUp, useArrayMoveDown, cartesianProductOf } from "@/composables/util.js";
// 当前商品ID
export const goodsId = ref(0)


// 规格选项列表
export const sku_card_list = ref([])


export const sku_list = ref([])
// 初始化规格选项列表
export function initSkuCardList (d) {
  sku_card_list.value = d.goodsSkusCard.map(item => {
    item.text = item.name
    item.loading = false
    item.goodsSkusCardValue.map(v => {
      v.text = v.value || "属性值"
      return v
    })
    return item
  })
  sku_list.value = d.goodsSkus
  console.log(sku_list.value, 'sku_list.value');
}




// 添加规格选项
export const btnLoading = ref(false)
export function addSkuCardEvent () {
  btnLoading.value = true
  createGoodsSkusCard({
    "goods_id": goodsId.value,
    "name": "规格选项",
    "order": 50,
    "type": 0
  }).then(res => {
    sku_card_list.value.push({
      ...res,
      text: res.name,
      loading: false,
      goodsSkusCardValue: []
    })
  }).finally(() => {
    btnLoading.value = false
  })
}
// 修改规格选项
export function handleUpdate (item) {
  item.loading = true
  updateGoodsSkusCard(item.id, {
    "goods_id": item.goods_id,
    "name": item.text,
    "order": item.order,
    "type": 0
  }).then(res => {
    item.name = item.text
  }).catch(err => {
    item.text = item.name
  }).finally(() => {
    item.loading = false
  })
}
// 删除规格选项
export function handleDelete (item) {
  item.loading = true
  deleteGoodsSkusCard(item.id).then(res => {
    // 和当前数组内的值做匹配 匹配上了就删除
    const i = sku_card_list.value.findIndex(o => o.id == item.id)
    if (i != -1) {
      sku_card_list.value.splice(i, 1)
    }
    getTableData()
  }).finally(() => {
    item.loading = false
  })
}
// 排序规格选项
export const bodyLoading = ref(false)
export function sortCard (action, index) {
  let oList = JSON.parse(JSON.stringify(sku_card_list.value))
  let func = action == 'up' ? useArrayMoveUp : useArrayMoveDown
  func(oList, index)
  let sortData = oList.map((item, i) => {
    return {
      id: item.id,
      order: i + 1
    }
  })
  bodyLoading.value = true
  sortGoodsSkusCard({ sortdata: sortData }).then(res => {
    func(sku_card_list.value, index)
    getTableData()
  }).finally(() => {
    bodyLoading.value = false
  })


}


// 选择设置规格
export function handleChooseSetGoodsSkusCard (id, data) {
  let item = sku_card_list.value.find(o => o.id == id)
  item.loading = true
  chooseAndSetGoodsSkusCard(id, data).then(res => {
    item.name = item.text = res.goods_skus_card.name
    console.log(res.goods_skus_card_value, 'res.goods_skus_card_value');
    item.goodsSkusCardValue = res.goods_skus_card_value.map(o => {
      o.text = o.value || '属性值'
      return o
    })
    getTableData()
  }).finally(() => {
    item.loading = false
  })
}


// 初始化规格值
export function initSkusCardItem (id) {
  const item = sku_card_list.value.find(o => o.id == id)


  const inputValue = ref('')
  const dynamicTags = ref(['Tag 1', 'Tag 2', 'Tag 3'])
  const inputVisible = ref(false)
  const InputRef = ref()
  const loading = ref(false)


  const handleClose = (tag) => {
    loading.value = true
    deleteGoodsSkusCardValue(tag.id).then(res => {
      let i = item.goodsSkusCardValue.findIndex(o => o.id == tag.id)
      if (i != -1) {
        item.goodsSkusCardValue.splice(i, 1)
      }
      getTableData()
    }).finally(() => {
      loading.value = false
    })
  }


  const showInput = () => {
    inputVisible.value = true
    nextTick(() => {
      InputRef.value.input.focus()
    })
  }


  // tag添加值
  const handleInputConfirm = () => {
    loading.value = true
    if (!inputValue.value) {
      inputVisible.value = false
      return
    }
    createGoodsSkusCardValue({
      "goods_skus_card_id": id, //规格ID
      "name": item.name, //规格名称
      "order": 50, //排序
      "value": inputValue.value //规格选项名称
    }).then(res => {
      item.goodsSkusCardValue.push({
        ...res,
        text: res.value
      })
      getTableData()
    }).finally(() => {
      loading.value = false
      inputVisible.value = false
      inputValue.value = ''
    })
  }
  // tag修改值
  const handleChange = (value, tag) => {
    loading.value = true
    updateGoodsSkusCardValue(tag.id, {
      "goods_skus_card_id": id, //规格ID
      "name": item.name, //规格名称
      "order": tag.order, //排序
      "value": value //规格选项名称
    }).then(res => {
      tag.value = value
      getTableData()
    }).catch((err) => {
      tag.text = tag.value
    }).finally(() => {
      loading.value = false
    })
  }
  return {
    item,
    inputValue,
    inputVisible,
    InputRef,
    handleClose,
    showInput,
    handleInputConfirm,
    handleChange,
    handleDelete
  }
}


// 初始化表格
export function initSkuTable () {
  const skuLabels = computed(() => sku_card_list.value.filter(v => v.goodsSkusCardValue.length > 0))
  // 获取表头数据
  const tableThs = computed(() => {
    let length = skuLabels.value.length
    return [{
      name: '商品规格',
      // 表头合并的列数
      colspan: length,
      width: "",
      // 表头合并的行数
      rowspan: length > 0 ? 1 : 2
    }, {
      name: '销售价',
      width: "100",
      rowspan: 2
    }, {
      name: '市场价',
      width: "100",
      rowspan: 2
    }, {
      name: '成本价',
      width: "100",
      rowspan: 2
    }, {
      name: '库存',
      width: "100",
      rowspan: 2
    }, {
      name: '体积',
      width: "100",
      rowspan: 2
    }, {
      name: '重量',
      width: "100",
      rowspan: 2
    }, {
      name: '编码',
      width: "100",
      rowspan: 2
    }]
  })
  return {
    skuLabels,
    tableThs,
    sku_list
  }
}


// 获取规格表格数据
function getTableData () {
  if (sku_card_list.value.length == 0) return []
  let list = []
  sku_card_list.value.forEach(o => {
    if (o.goodsSkusCardValue && o.goodsSkusCardValue.length > 0) {
      list.push(o.goodsSkusCardValue)
    }
  })


  if (list.length == 0) {
    return []
  }


  let arr = cartesianProductOf(...list)
  console.log(arr, 'arr');
  sku_list.value = []
  sku_list.value = arr.map(o => {
    return {
      code: "",
      cprice: "0.00",
      goods_id: goodsId.value,
      image: "",
      oprice: "0.00",
      pprice: "0.00",
      skus: o,
      stock: 0,
      volume: 0,
      weight: 0,
    }
  })
}

规格选项弹框ChooseSku.vue

vue3 admin后台管理开发笔记_第25张图片

  

  

11、商品分类模块开发(较简单)

开发思路:大部分代码复用权限管理页,弹框内容较少

vue3 admin后台管理开发笔记_第26张图片
vue3 admin后台管理开发笔记_第27张图片
vue3 admin后台管理开发笔记_第28张图片

  



  

  


  

  

12、会员等级模块和用户管理模块开发(较简单)

开发思路:会员等级模块复用角色管理页

用户管理模块复用管理员管理页

vue3 admin后台管理开发笔记_第29张图片

  

vue3 admin后台管理开发笔记_第30张图片

    

13、商品评论列表模块(较简单)

开发思路:复用管理员管理页

vue3 admin后台管理开发笔记_第31张图片

    

14、订单模块(excel导出(重点))

14.1、订单列表

开发逻辑:复用管理员管理页面

vue3 admin后台管理开发笔记_第32张图片

    

vue3 admin后台管理开发笔记_第33张图片

  

  
14.1、excel导出功能逻辑

导出订单功能 基本逻辑:

  1. 触发接口后,会返回一个 对象{size:6405,type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}

  1. 将这个对象通过new Blob转化为blob格式通过window的方法转为url

  1. 通过生成a标签,模拟点击事件 完成excel下载操作

const onSubmit = () => {
  if (!form.tab) return toast('订单类型不能为空', 'error')
  loading.value = true
  let starttime = null
  let endtime = null
  if (form.time && Array.isArray(form.time)) {
    starttime = form.time[0]
    endtime = form.time[1]
  }
  // 接口
  exportOrder({
    tab: form.tab,
    starttime,
    endtime
  }).then(data => {
    console.log(data,'data');
    // 导出订单功能:
    // 基本逻辑:1、触发接口后,会返回一个 对象{size:6405,type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}
    // 2、将这个对象通过new Blob转化为blob格式通过window的方法转为url
    // 3、通过生成a标签,模拟点击事件 完成excel下载操作
    let url = window.URL.createObjectURL(new Blob([data]))
    let link = document.createElement('a')
    link.style.display = 'none'
    link.href = url
    let filename = (new Date()).getTime() + '.xlsx'
    link.setAttribute('download', filename)
    document.body.appendChild(link)
    link.click()
    close()
  }).finally(() => {
    loading.value = false
  })
}
14.2、订单详情
vue3 admin后台管理开发笔记_第34张图片

  

  
14.3、物流详情

使用el-timeline组件

vue3 admin后台管理开发笔记_第35张图片

  

  

15、基础、物流和交易设置页开发(较简单)

vue3 admin后台管理开发笔记_第36张图片

  

  
vue3 admin后台管理开发笔记_第37张图片

  

  
vue3 admin后台管理开发笔记_第38张图片

  

  

你可能感兴趣的:(vue.js,前端,javascript)