【vue3+后台管理系统+项目笔记】

文章目录


开发过程:

1.确认页面结构如何划分

(假设是二层结构) ----> 参考【八、页面结构(封装结构)】

2.初始化搭建页面结构

(创建对应的vue文件结构【主文件和对应组件文件】)

3.实现主文件和组件的基本应用

(导入组件,并能正确展示)

4.开发组件的html结构,css样式

(html+css的东西)

5.实现js交互,以及主文件和组件的数据通信

(用假数据)(props/emit等)----> 【三、搜索栏的二次封装】和【五、表格的二次封装(简单版)】

6.实现真实的请求数据

(发送接口请求)---->【 四、数据获取和保存的逻辑】

一、基础操作

1.1、基础命令

src/components/nav-menu.vue
1.1、Template标签中,图片引用要通过“~@/”方式。 <img src="~@/assets/img/logo.svg" alt="logo" />
1.2、Template标签可以使用v-for循环。<template v-for="item in userMenus" :key="item.id">
1.3、Template标签可以使用v-if判断。<template v-if="item.type === 1">
1.4、v-for+v-if结合,当v-if为真时,渲染某一元素/动态绑定某一属性。<i v-if="item.icon" :class="item.icon"></i>
注意:v-if,也可以写在组件中,或者UI组件库中,控制组件是否显示

src/base-ui/from/src/form.vue
1.16、Template标签使用v-if判断。<template v-else-if="item.type === 'select'">

1.2、store相关

src/components/nav-menu.vue
1.5、引入store。import { useStore } from 'vuex'
1.6、实例store对象。const store = useStore()
1.7、获取store对象的属性(computed计算属性)。const userMenus = store.state.login.userMenus
src/components/page-content.vue
1.19、通过setup(props) +store.dispatch(props.属性)可以动态传入参数。setup(props) {store.dispatch('接口', {pageName: props.pageName})}

export default defineComponent({
  props: {
    pageName: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const store = useStore()
    store.dispatch('system/getPageListAction', {
      pageName: props.pageName,
      queryInfo:{
        offset:0,
        size:10
      }
    })
  }
})

1.20、不同页面的数据请求实现(场景应用:初始化请求所有的数据)。(user请求userlist,role请求rolelist)
思路:dispatch传递的不要是固定接口/user/list,而是页面名称pageName
实现:
1.获取pageUrl
2. 根据pageName的内容,进行switch判断
3. 然后对页面发送请求
4. 再根据传递进来的pageName内容,将数据分别存储到对应的state中

// 在store下的system.ts 的module模块中
actions: {
    async getPageListAction({ commit }, payload: any) {
      // 1. 获取pageUrl
      const pageName = payload.pageName
      // 2. 根据pageName的内容,进行switch判断
      let pageUrl = ''
      switch (pageName) {
        case 'users':
          pageUrl = 'users/list'
          break
        case 'role':
          pageUrl = 'role/list'
          break
      }
      // console.log(pageUrl)

      // 3. 然后对页面发送请求
      const pageResult = await getPageListData(pageUrl, payload.queryInfo)
      // console.log(pageResult)

      // 4. 再根据传递进来的pageName内容,将数据分别存储到对应的state中
      const { list, totalCount } = pageResult.data
      switch (pageName) {
        case 'users':
          commit('changeUserList', list)
          commit('changeUserCount', totalCount)
          break
        case 'role':
          commit('changeRoleList', list)
          commit('changeRoleCount', totalCount)
          break
      }
    },

  }

注意:useStore只能在setup里使用,在一般的js/ts文件中没法使用const store = useStore()--

1.3、router相关

src/components/nav-menu.vue
1.8、引入useRouter和useRoute。import { useRouter, useRoute } from 'vue-router'
1.9、实例router实例对象,可以使用router对象的方法,,例如router.push。const router = useRouter()
1.10、实例route实例对象,可以使用当前路由对象的方法,例如route.path。const route = useRoute()
1.17、获取所有的路由对象。console.log(router.getRoutes());
1.18、在导航守卫router.beforeEach中可以获取即将跳转的route对象。

router.beforeEach((to) => {
  if (to.path !== '/login') {
    const token = LocalCache.getCache('token')
    if (!token) {
      return '/login'
    }
  }
  if (to.path === '/main') {
    return firstMenu.url
  }
  // 获取所有的路由对象
  // console.log(router.getRoutes());
  // to其实就是即将跳转的route对象
  // console.log(to);
})

1.4、其他类

src/views/main.vue
1.11、动态绑定属性+三元运算符。<el-aside :width="isCollapse ? '60px' : '210px'">
src/utils/map-menus.ts
1.12、调用数组的高阶方法:forEach,将部分数据添加到另一个数组中。
computed(()=> {})
computed返回值是一个ref对象:const userMenus = computed(() => store.state.login.userMenus)
switch 类似于 if 判断
switch可以根据条件判断,返回不同的值。
应用场景:数据的转换功能
例如:

// 如果pageName是users,执行第一个选项,如果是role,执行第一个选项,
  switch (pageName) {
    case 'users':
      commit('changeUserList', list)
      commit('changeUserCount', totalCount)
      break
    case 'role':
      commit('changeRoleList', list)
      commit('changeRoleCount', totalCount)
      break
  }

1.5、获取组件实例对象

参考链接:https://www.jb51.net/article/209948.htm
1.13、组件实例中绑定ref<canvas ref="refChart" :height="setHeight"></canvas>
1.14、setup中定义refChart对应的ref对象const refChart = ref();
1.15、通过refChart.value.属性/方法,可以直接使用。

1.6、Vue3中Setup函数的参数props、context详解

参考链接:
重要:props和context分别在template和setup中使用说明https://www.jianshu.com/p/6e27bee45dcb
了解:props和context相关名词定义解释https://blog.csdn.net/weixin_48789376/article/details/119174911

1.21、子组件的props,如果是template中使用,可以直接使用props对象中定义的变量名
1.21、子组件的props,如果是setup中使用,需要setup(props),然后通过props.变量名,获取对应的属性
注意:props:接收传过来的参数必须先进行声明,如果没声明,传过来的参数将出现在attrs中

1.15、context :上下文意思,包括 attrs 、 emit 、slots。
① attrs :在此部分,接收在父组件传递过来的,并且没有在props中声明的参数参数。
——在template中使用,这些属性都存储在$attrs对象中,可以通过{{ $attrs.属性名 }}获取
——在setup函数中获取,这些属性都存储在setup函数的第二个参数对象context的attrs属性中,可以通过context.attrs.属性名获取
② emit:子组件对父组件发送事件
③ slots:是一个 proxy 对象,其中 slots.default() 获取到的是一个数组,数组长度由组件的插槽决定,数组内是插槽内容。
1.16、context,常见的用法:{ emit }、{attrs}、{slots}

二、功能点

2.1、子传父+父传子:传递某一变量

src/components/nav-header.vue
2.1、监听事件。<el-icon @click="handleFoldClick()">
2.2、监听事件函数。const handleFoldClick = () => {}
2.3、添加emits属性。export default defineComponent({ emits: ['foldChange']})
2.4、setup函数中解构{emit}。setup(props, { emit }) {}
2.5、发出事件函数,可以带参数(isFold.value是参数)。const handleFoldClick = () => {emit('foldChange', isFold.value)}
src/views/main.vue
2.6、接收发出的事件函数,父组件自定义函数。<nav-header @foldChange="handleFoldChange" />
2.7、写父组件的监听函数(有参数记得写形参)。const handleFoldChange = (isFold: boolean) => {isCollapse.value = isFold}
2.8、接收参数,修改父组件中定义好的变量。isCollapse.value = isFold
2.9、另一个子组件动态绑定该属性。<nav-menu :collapse="isCollapse" />
src/components/nav-menu.vue
2.10、另一个子组件内设置props属性。export default defineComponent({props: { collapse:Boolean }})
2.11、另一个子组件内可以动态绑定props属性。<el-menu :collapse="collapse">

2.2、添加not found路由和组件

src/router/index.ts
2.12、注册not-found路由映射关系

{
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('@/views/not-found/not-found.vue')
  }

src/views/not-found/not-found.vue
2.13、创建not-found组件

2.3、动态生成路由

原理:登录–账号(角色)–菜单列表–包含url–对应路由的path–对应component
例如:登录–coderwhy–角色管理–包含/main/role–对应路由的path–对应component
实际:登录–coderwhy–N多菜单–菜单中有url–前端提前配置好所有的url和组件的映射–根据菜单中的url加载对应的组件–形成routes数组–动态加载到main路由下的chilren属性中
注意:提前创建好router文件和view文件,并配置好关系

2.3.1、函数封装说明:

封装调用的逻辑:
1.确认要实现哪个功能,并确认该功能的执行位置
2.估算功能实现的逻辑(要获取什么)
3.将逻辑封装到单独函数文件中,执行封装函数,函数返回值就是要获取的内容
4.案例可结合下面的 2.14,2.15,2.19

// 封装函数的大概流程
export function mapMenusToRoutes(userMenus) {
  // 定义一个空的数组类型的返回值
  const routes = []

  // 中间写代码逻辑
  // 1.先去加载默认所有的routes
  // 2.根据菜单获取需要添加的routes
  // 。。。。

  // 返回值
  return routes
}

// 调用封装函数
const routes = mapMenusToRoutes(userMenus)

2.3.2、案例实现过程:

(1)动态注册路由
src/store/login/login.ts
2.14、因为账号登录后可以获取菜单列表,并保存进行保存,所以可以在这里操作动态添加路由
2.15、根据菜单,找到对应的router的路由:userMenus --> routes
2.19、调用封装的map-menus.ts里的mapMenusToRoutes函数。const routes = mapMenusToRoutes(userMenus)
src/utils/map-menus.ts
2.16、先去加载默认所有的routes
2.17、根据菜单获取需要添加的routes
2.18、递归函数
src/store/login/login.ts
2.19、调用封装的map-menus.ts里的mapMenusToRoutes函数,获取的就是待添加的routes数组
2.20、通过routes数组的forEach,动态添加路由router.addRoute('main', route)

(2)实现路径和组件的映射关系
src/components/nav-menu.vue
2.21、在菜单中,添加监听事件
2.22、监听事件中,添加路由映射
src/views/main.vue
2.23、因为是在main路由添加子路由,所以需要在main.vue文件下进行<router-view />占位

2.3.3、BUG:动态路由设置后刷新页面出现notfound,不出现正常的路由

判断:

  • router.before导航守卫中,查看所有的路由对象router.getRoutes()和即将跳转的路由对象to是否正确。

方案:

  • main.js文件中,app.use(store)app.use(router)或者setupStore()顺序反了。
  • 先执行store,再执行setupStore(),最后执行router
app.use(store)
setupStore()
app.use(router)

2.3.4、BUG:刷新页面,菜单栏一直固定选择,无法保留刷新前的选项

原因:

  • nav-menu.vue的<el-menu default-active=“2”>是固定值

方案:

  • 将default-active属性变为动态获取。

实现:

  • 动态绑定属性:<el-menu :default-active="currentMenuId">
  • 拿到当前页面的路径:const route = useRoute()const currentPath = route.path
  • 根据路径匹配menu菜单(单独封装一个处理函数):const menu = pathMapToMenu(userMenus.value, currentPath)
  • 拿到menu菜单的id属性作为currentMenuId的值:const currentMenuId = ref(menu.id + '')
// data,匹配页面刷新时,菜单项固定某一位置的bug
//Template:<el-menu :default-active="currentMenuId">
setup() {
    const route = useRoute()
    const currentPath = route.path
    const menu = pathMapToMenu(userMenus.value, currentPath)
    const currentMenuId = ref(menu.id + '')
}

2.3.5、BUG:在首页localhost:8080页面刷新出现空白(默认重定向到/main)

原因:

  • router中重定向到/main组件,
  • main组件引入了nav-menu组件,
  • nav-menu组件有个页面刷新保存当前id的设置
  • 问题:当直接进入首页,是没有currentPath属性,就无法匹配到对应的id

方案:

  • 当首页重定向到/main 的时候,需要继续重定向到menu 菜单的第一项。
    • 当第一次登录时,会动态添加路由,
    • 动态添加路由的时候,会根据用户菜单列表获取需要添加的routes。
    • 此时根据当前的用户菜单列表,我们可以保存下用户菜单列表中的第一个菜单选项

实现:

  • mapMenusToRoutes函数中(动态添加路由时)定义一个空的变量firstMenu:let firstMenu = null
  • 判断当前firstMenu是否有值
  • 如果没有值的话,保存当前用户菜单列表中的第一个菜单选项
  • 将firstMenu进行导出
  • 当发现首页重定向到/main 的时候,在router文件通过router.beforeEach,再重定向到用户菜单第一项就可以了
// map-menu.ts文件
let firstMenu: any = null
export function mapMenusToRoutes(userMenus) {
  // ...中间省略
  const _recurseGetRoute = (menus) => {
    for (const menu of menus) {
      if (menu.type === 2) {
        const route = allRoutes.find((route) => route.path === menu.url)
        if (route) {
          routes.push(route)
        }
        if (!firstMenu) {
          firstMenu = menu
        }
      } else {
        _recurseGetRoute(menu.children)
      }}}}
export { firstMenu }

//router/index.ts
router.beforeEach((to) => {
  if (to.path !== '/login') {
    const token = LocalCache.getCache('token')
    if (!token) {
      return '/login'
    }
  }
  if (to.path === '/main') {
    return firstMenu.url
  }})

三、搜索栏的二次封装

3.1、基础操作

3.1、Form表单

src/base-ui/from/src/form.vue
3.1、修改UI组件的样式,以ElementPlus为例。<el-select style="width: 100%">
3.2、v-bind绑定对象,是将对象内的所有属性全部动态绑定到组件中。<msz-form v-bind="searchFormConfig">
3.3、props中变量如果是数组或者对象类型,需要将default写成函数的形式。formItems: { type: Array, default: () => [] },
3.4、逻辑运算符(逻辑与&&、逻辑或||)的使用。
3.5、动态获取数据的另一种方式v-model=“formData[`${item.field}`]”

补充:逻辑运算符在 判断语句 和 赋值语句 使用说明

参考链接: https://www.cnblogs.com/guanghe/p/11157201.html?ivk_sa=1024320u
(1)当出现在条件判断语句中时,例如 i f 语句,返回值Boolean

1.&&
1.1两边条件都为true时,结果才为true;
1.2如果有一个为false,结果就为false;
1.3当第一个条件为false时,就不再判断后面的条件
注意:当数值参与逻辑与运算时,结果为true,那么会返回的会是第二个为真的值;如果结果为false,返回的会是第一个为假的值。
2.||
2.1只要有一个条件为true时,结果就为true;
2.2当两个条件都为false时,结果才为false;
2.3当一个条件为true时,后面的条件不再判断
注意:当数值参与逻辑或运算时,结果为true,会返回第一个为真的值;如果结果为false,会返回第二个为假的值;

(2)当出现在赋值语句中时,例如变量赋值、return结果等返回值是其中一方的运算结果,会遵循以下规则:

const result = 表达式a && 表达式b :
result的运算结果,
先判断计算表达式a(也可以是函数)的运算结果,
如果为 True, 执行表达式b(或函数),并返回b的结果;
如果为 False,返回a的结果;

const result = 表达式a || 表达式b :
result的运算结果,
先判断计算表达式a(也可以是函数)的运算结果,
如果为 Fasle, 执行表达式b(或函数),并返回b的结果;
如果为 True,返回a的结果;

3.2、Form表单二次封装

3.1、组件结构封装逻辑

  • 1.新建一个form.vue文件,内容是UI库的代码
  • 2.form.vue文件中的属性,尽量通过props属性接收外部传递进来
  • 3.在外部注册Form组件,并通过动态绑定的方式传递数据,v-bind:属性=‘变量’或者v-bind=‘对象’
  • 进阶
  • 将第三步动态绑定的属性,单独抽成一个对象格式的JS文件(配置文件),通过v-bind=‘对象’的方式将配置对象动态绑定到组件中。
// form.vue
<template>
  <div class="hy-form">
    <el-form>
        <template v-for="item in formItems" :key="item.label">
            <el-form-item :label="item.label" :style="itemStyle">
              <template v-if="item.type === 'input' || item.type === 'password'">
                <el-input :show-password="item.type === 'password'" />
              </template>
            </el-form-item>
        </template>
    </el-form>
  </div>
</template>

<script>
export default {
  props: {
    formItems: {
      type: Array,
      default: () => ([])
    },
    itemStyle: {
      type: Object,
      default: () => ({ padding: '10px 40px' })
    },
  setup() {return {}}
}
</script>
// user.vue
<template>
  <div class="user">
  //<hy-form v-bind="searchFormConfig" />  进阶
    <hy-form
      :formItems="formItems"
      :itemStyle="itemStyle"
    />
  </div>
</template>

<script>
import HyForm from '@/base-ui/form'
// import { searchFormConfig } from './config/search.config'  进阶
// 将setup中itemStyle和formItems 属性抽到search.config.js文件中,保存成对象格式。再导入进来
export default {
  name: 'user',
  components: {
    HyForm
  },
  setup() {
    const itemStyle= {
      padding: '10px 40px'
    }
    const formItems = [
      {
        type: 'input',
        label: '用户名',
        placeholder: '请输入用户名'
      },
      {
        type: 'password',
        label: '密码',
        placeholder: '请输入密码'
      }]
    return {
      formItems,
      itemStyle,
      // searchFormConfig  进阶
    }
  }
}
</script>

3.2、实现表单数据的双向绑定

1.基础:
所有的表单(例如input表单)都有v-model="name"的属性,通过v-model可以将在input输入的内容,放入到v-model绑定的name属性中。<el-input v-model="name" />
2.封装:
2.1 在外部引入的文件中,定义一个formData数据
2.2 在自定义组件上,动态绑定formData数据
2.3 在封装组件文件中,使用props接收formData数据
3.如果只有一个表单:
3.1 在封装组件文件v-model直接绑定接收的formData数据既可以了。<el-input v-model="formData" />
4.如果有多个表单,如何实现一一对应的关系:
4.1 通过在formItem属性中,添加一个field字段。
4.2 该field字段名称要与formData中定义的属性名保持一致。
4.3 在封装组件文件中,通过 <el-input v-model=“formData[`${item.field}']” /> 实现数据的双向绑定
4.4 如果有eslint报错,在eslintrc.js中关闭检测即可

<!-- 基础-->
<!-- base-ui/form/src/form.vue-->
<el-input v-model="name" />
<!-- 封装-->
<!-- user.vue-->
<template>
  <div class="user">
  //<hy-form v-bind="searchFormConfig" :formData='formData' />  进阶
    <hy-form
      :formItems="formItems"
      :itemStyle="itemStyle"
    />
  </div>
</template>

<script>
import HyForm from '@/base-ui/form'
// import { searchFormConfig } from './config/search.config'  进阶
// 将setup中itemStyle和formItems 属性抽到search.config.js文件中,保存成对象格式。再导入进来
export default {
  name: 'user',
  components: {
    HyForm
  },
  setup() {
    const itemStyle= {
      padding: '10px 40px'
    }
    const formItems = [
      {
        field:'name',
        type: 'input',
        label: '用户名',
        placeholder: '请输入用户名'
      },
      {
        field:'password',
        type: 'password',
        label: '密码',
        placeholder: '请输入密码'
      }]
     const formData = ref({
       name:'',
       password:''
     })
    return {
      formItems,
      itemStyle,
      formData,
      // searchFormConfig  进阶
    }
  }
}
</script>
<!-- 封装-->
<!-- form.vue-->
<template>
  <div class="hy-form">
    <el-form>
        <template v-for="item in formItems" :key="item.label">
            <el-form-item :label="item.label" :style="itemStyle">
              <template v-if="item.type === 'input' || item.type === 'password'">
                <el-input :show-password="item.type === 'password'" v-model="formData[`${field}`]" />
              </template>
            </el-form-item>
        </template>
    </el-form>
  </div>
</template>

<script>
export default {
  props: {
    formItems: {
      type: Array,
      default: () => ([])
    },
    itemStyle: {
      type: Object,
      default: () => ({ padding: '10px 40px' })
    },
    formData: {
      type: Object,
      required:true
    }
  setup() {return {}}
}
</script>

3.3、组件扩展插槽

1.基础使用
定义插槽

<!-- form.vue-->
<template>
  <div class="msz-form">
    <div class="header">
      <slot name="header"></slot>
    </div>
    <el-form :label-width="labelWidth">
    </el-form>
  </div>
</template>

使用插槽

<!-- page-search.vue-->
<template>
  <div class="page-search">
    <msz-form v-bind="searchFormConfig" v-model="formData">
      <template #header>
        <h1 class="header">高级检索</h1>
      </template>
    </msz-form>
  </div>
</template>

3.4、说明:关于表单双向绑定的其他方案

可以参考【后台管理系统项目实战(八)–04_(掌握)HyForm实现双向绑定的方案】

四、数据获取和保存的逻辑

4.1、目录结构

|-- service
|   |-- index.ts
|   |-- main
|   |   |-- system
|   |       |-- system.ts
|   |-- request
|       |-- config.ts
|       |-- index.ts
|-- store
|   |-- index.ts
|   |-- main
|       |-- system
|           |-- system.ts
|-- views
    |-- main
    |   |-- system
    |       |-- user
    |           |-- user.vue

4.2、use.vue中发出dispatch

<template>
  //省略了结构内容
</template>

<script>
import { defineComponent, computed } from 'vue'
import { useStore } from 'store'

export default defineComponent({
  name: 'user',
  setup() {
    const store = useStore()
    // 简单的方式:参数写死,
    // pageUrl:接口的URL,可以直接写死省事
    // queryInfo:接口查询的条件,有哪些写哪些
    store.dispatch('system/getPageListAction', {
      pageUrl: '/users/list',
      queryInfo: {
        offset: 0,
        size: 10
      }
    })

4.2、store/index.js引入子模块内容store/main/system/system.ts

import { createStore} from 'vuex'
import system from './main/system/system'

const store = createStore({
  state() {
    return {
      name: 'coderwhy',
      age: 18
    }
  },
  mutations: {},
  getters: {},
  actions: {},
  modules: {
    system
  }
})

// 加载本地缓存数据的
export function setupStore() {
  store.dispatch('login/loadLocalLogin')
}
export default store

4.3、store子模块内容store/main/system/system.ts

import { Module } from 'vuex'
import { getPageListData } from '@/service/main/system/system'

const systemModule = {
  // 命名空间
  namespaced: true,
  state() {
    return {
      userList: [],
      userCount: 0
    }
  },
  mutations: {
    changeUserList(state, userList: any[]) {
      state.userList = userList
    },
    changeUserCount(state, userCount: number) {
      state.userCount = userCount
    }
  },
  actions: {
    async getPageListAction({ commit }, payload) {
      // 打印看下是否可以获取到查询条件
      console.log(payload.pageUrl)
      console.log(payload.queryInfo)
      // 1.对页面发送请求,调用的是service下的请求
      const pageResult = await getPageListData(
        payload.pageUrl,
        payload.queryInfo
      )
      // 获取到list和totalCount 数据
      const { list, totalCount } = pageResult.data
      // 提交commit保存到state中
      commit('changeUserList', list)
      commit('changeUserCount', totalCount)
    }
  }
}

export default systemModule

4.4、store子模块中使用的服务请求

import hyRequest from '../../index'

export function getPageListData(url: string, queryInfo: any) {
  return hyRequest.post<IDataType>({
    url: url,
    data: queryInfo
  })
}

4.5、use.vue使用Vuex中的state数据

<template>
  <div class="user">
    <hy-table :listData="userList" :propList="propList">
	  // 中间代码省略
    </hy-table>
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from 'store'

import HyTable from '@/base-ui/table'

export default defineComponent({
  name: 'user',
  components: {
    HyTable
  },
  setup() {
    const store = useStore()
    // 提交请求
    store.dispatch('system/getPageListAction', {
      pageUrl: '/users/list',
      queryInfo: {
        offset: 0,
        size: 10
      }
    })
    // 获取store中的数据,通过computed来监听,实时改变
    const userList = computed(() => store.state.system.userList)
    const userCount = computed(() => store.state.system.userCount)

    return {
      userList,
    }
  }
})
</script>

<style scoped></style>

五、表格的二次封装(简单版)

5.1、根据数据的属性,v-for循环出表格的表头

<template>
  <div class="user">
    <div class="content">
      <el-table :data="userList " border style="width: 100%">
        <template v-for="propItem in propList" :key="propItem.prop">
          <el-table-column v-bind="propItem" align="center"></el-table-column>
        </template>
      </el-table>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from '@/store'

export default defineComponent({
  name: 'user',
  setup() {
    const store = useStore()
    store.dispatch('system/getPageListAction', {
      pageUrl: '/users/list',
      queryInfo: {
        offset: 0,
        size: 10
      }
    })

    const userList = computed(() => store.state.system.userList)
    const userCount = computed(() => store.state.system.userCount)

    const propList = [
      { prop: 'name', label: '用户名', minWidth: '100' },
      { prop: 'realname', label: '真实姓名', minWidth: '100' },
      { prop: 'cellphone', label: '手机号码', minWidth: '100' },
      { prop: 'enable', label: '状态', minWidth: '100', slotName: 'status' },
      {
        prop: 'createAt',
        label: '创建时间',
        minWidth: '250',
        slotName: 'createAt'
      },
      {
        prop: 'updateAt',
        label: '更新时间',
        minWidth: '250',
        slotName: 'updateAt'
      }
    ]
    return {
      userList,
      propList
    }
  }
})
</script>

<style scoped>
.content {
  padding: 20px;
  border-top: 20px solid #f5f5f5;
}
</style>

六、表格的二次封装(复杂版)

可以参考【后台管理系统项目实战(八)–08_(掌握)HyTable的动态插槽和作用域插槽】

6.1、通过作用域插槽修改某列数据的展示形式

6.1.1、封装的table.vue

<template>
  <div class="hy-table">
    <el-table :data="listData" border style="width: 100%">
      <template v-for="propItem in propList" :key="propItem.prop">
        <el-table-column v-bind="propItem" align="center">
          <!-- #default="scope"是element自带的作用域插槽 -->
          <!-- 获取这一行的数据:scope.row -->
          <!-- 获取这一行的某一个字段数据:scope.row[propItem.prop] -->
          <template #default="scope">
            <!-- 所有的数据全部按照button的格式展示 -->
            <!-- <el-button>{{ scope.row[propItem.prop] }}</el-button> -->
            <!-- 某一列数据单独定制样式,自定义插槽 -->
            <slot :name="propItem.slotName" :row="scope.row">
              {{ scope.row[propItem.prop] }}
            </slot>
          </template>
        </el-table-column>
      </template>
    </el-table>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    listData: {
      type: Array,
      required: true
    },
    propList: {
      type: Array,
      required: true
    }
  },
  setup() {
    return {}
  }
})
</script>

<style scoped></style>

6.1.2、user.vue引用封装的table.vue

<template>
  <div class="user">
    <page-search :searchFormConfig="searchFormConfig" />

    <div class="content">
      <hy-table :listData="userList" :propList="propList">
        <template #status="scope">
          <el-button>{{ scope.row.enable ? '启用' : '禁用' }}</el-button>
        </template>
        <template #createAt="scope">
          <strong>{{ scope.row.createAt }}</strong>
        </template>
      </hy-table>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from '@/store'

import PageSearch from '@/components/page-search'
import HyTable from '@/base-ui/table'

import { searchFormConfig } from './config/search.config'

export default defineComponent({
  name: 'user',
  components: {
    PageSearch,
    HyTable
  },
  setup() {
    const store = useStore()
    store.dispatch('system/getPageListAction', {
      pageUrl: '/users/list',
      queryInfo: {
        offset: 0,
        size: 10
      }
    })

    const userList = computed(() => store.state.system.userList)
    const userCount = computed(() => store.state.system.userCount)

    const propList = [
      { prop: 'name', label: '用户名', minWidth: '100' },
      { prop: 'realname', label: '真实姓名', minWidth: '100' },
      { prop: 'cellphone', label: '手机号码', minWidth: '100' },
      { prop: 'enable', label: '状态', minWidth: '100', slotName: 'status' },
      {
        prop: 'createAt',
        label: '创建时间',
        minWidth: '250',
        slotName: 'createAt'
      },
      {
        prop: 'updateAt',
        label: '更新时间',
        minWidth: '250',
        slotName: 'updateAt'
      }
    ]

    return {
      searchFormConfig,
      userList,
      propList
    }
  }
})
</script>

<style scoped>
.content {
  padding: 20px;
  border-top: 20px solid #f5f5f5;
}
</style>

七、时间格式化:通过dayjs格式化

5.1、格式化函数的注册方式

5.1.1、普通方式:在当前需要格式化数据的文件中,在setup里定义一个函数,通过调用当前文件下的这个函数,格式化数据

<template #createAt="scope">
  <span>{{ formatTime(scope.row.createAt) }}</span>
</template>
<script>
  const formatTime = ()=>{...省略}
</script>

5.1.2、全局注册:通过app.config.globalProperties.属性名来全局绑定一个函数(或者对象)

注册方法:绑定函数
// 根文件main.js
// 在全局注册了一个$filter的函数,后期在任何地方都可以通过$filter()使用该函数
// $filter:是自定义的函数名
app.config.globalProperties.$filter = function() {
  // 代码逻辑写在这
  return '返回结果'
}
注册方法:绑定对象
// 绑定一个对象
app.config.globalProperties.$filter = {
  foo(){
    // foo函数的代码逻辑写在这
    return '返回结果'
  },
  formatTime(){
    // formatTime函数的代码逻辑写在这
    return '返回结果'
  }
}
使用方法:template中使用
// 在template中使用
<template #createAt="scope">
  <span>{{ $filter.formatTime(scope.row.createAt) }}</span>
</template>
使用方法:setup中获取该函数
// main.js
// 导入
import push from '@/utils/push'
import api from '@/utils/api'
// 中间忽略了const app = createApp(App)等代码
// 绑定
app.config.globalProperties.$asyncPost = api.nextPost
app.config.globalProperties.$push = push

// 在setup中使用
<script>
import { getCurrentInstance } from 'vue'
export default {
  setup() {
    const instance = getCurrentInstance()
    console.log('instance是:', instance.appContext.config.globalProperties)
    return {}
  },
}
</script>

在这里插入图片描述

5.1.3、格式化函数的代码实现

import dayjs from 'dayjs'
// 导入utc才可以支持对utc转化
import utc from 'dayjs/plugin/utc'

// 使用utc
dayjs.extend(utc)

// 时间格式化:默认样式
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'

// UTC时间格式化
export function formatUtcString(
  utcString: string,
  format: string = DATE_TIME_FORMAT
) {
  // 时间偏移8小时:utcOffset(8)
  return dayjs.utc(utcString).utcOffset(8).format(format)
}

// 时间戳时间格式化
export function formatTimeStamp(
  timeStamp: number,
  format: string = DATE_TIME_FORMAT
) {
  if (Number.isInteger(timeStamp)) {
    if (timeStamp.toString().length === 13) {
      // 毫秒
      return dayjs(timeStamp).format(format)
    } else if (timeStamp.toString().length === 10) {
      // 秒
      return dayjs(timeStamp).format(format)
    }
  }
}

八、页面结构(封装结构)

思想:在软件工程里面,没有什么是通过分一层不能解决的,如果有的话,那给它分两层

8.1、基础版:分二层结构

第一层:最终的页面需求页面
	User.vue:
		1.引用第二层的封装组件
		2.数据通过props/emits传递
		3.网络请求可以通过Vuex进行发出
第二层:代码逻辑页面
	Search.vue:
		1.实现业务的逻辑代码
		2.可以与User.vue通过props/emits传递数据
		
如果其他页面也有类似的组件需求:
	1.可以通过复制当前 的组件,适当修改
	2.扩展当前组件的复用性

在这里插入图片描述

8.2、进阶版:分三层结构

第一层:最终的页面需求页面
	User.vue:
		1.引用封装好的第二层的封装文件
		2.引用封装好的第二层的配置文件
		3.将配置文件和组件进行绑定
第二层:中间过渡层
	PageContent.vue:
		1.引用封装好的第三层的封装文件
		2.通过props/emit实现与第一层和第三层的传递
		3.与Vuex打通,实现数据状态管理
		4.网络请求发送,在这层实现
		5.补充并彻底实现第一层的需求
第三层:代码逻辑页面
	maSZtable.vue:
		1.实现业务的逻辑代码
		2.通过props/emit实现与第二层的传递

在这里插入图片描述

九、搜索框的重置和查询功能(search.vue->user.vue->content.vue)

9.1、重置功能(搜索和重置逻辑一样)

9.1.1 监听事件,然后将input表单绑定的属性设置成原始的状态(或者是空的状态)

9.1.2 重新发送查询请求

// page-search.vue
// 监听事件
<template>
  <div class="page-search">
    <msz-form v-bind="searchFormConfig" v-model="formData">
      <template #header>
        <h1 class="header">高级检索</h1>
      </template>
      <template #footer>
        <div class="handle-btns">
          <el-button @click="handleResetClick">重置</el-button>
          <el-button type="primary" @click="handleQueryClick">搜索</el-button>
        </div>
      </template>
    </msz-form>
  </div>
</template>

<script lang="ts">
setup(props, { emit }) {
  const formDataInt = ref({
      id: '',
      name: '',
      password: '',
      sport: '',
      createTime: ''
    })const formData = ref({
      id: '',
      name: '',
      password: '',
      sport: '',
      createTime: ''
    })
  // 当用户点击重置,进行数据重置
    const handleResetClick = () => {
      // 数据重置
      // 因为这是个input表单,不会有数据的产生
      formData.value = formDataInt.value 
      
      // 重新发送查询请求
      emit('resetBtnClick')
    }
}
</script>
// user.vue
<template>
  <div class="user">
  	// 接收事件
    <page-search @resetBtnClick="handleResetBtnClick"></page-search>
    <page-content ref="pageContentRef"></page-content>
  </div>
</template>

<script lang="ts">
export default defineComponent({
  setup() {
  	// 处理事件
  	const handleResetBtnClick = () => {
  	  // 获取PageContentRef,调用查询数据的方法
      pageContentRef.value?.getPageData()
  	}
    return {
      handleResetBtnClick,
  	}
  }
})
</script>
<template>
  <div class="page-content">
    <msz-table :listData="dateList" :listCount="dataCount">
    </msz-table>
  </div>
</template>
<script lang="ts">
export default defineComponent({
  setup() {
  	// 1.发送网络请求
    const getPageData = (queryInfo: any = {}) => {
      // 发送网络前,判断是否有对应权限
      // 没有权限
      if (!isQuery) return
      // 有权限
      store.dispatch('system/getPageListAction', {
        pageName: props.pageName,
        queryInfo: {
          offset: (pageInfo.value.currentPage - 1) * pageInfo.value.pageSize,
          size: pageInfo.value.pageSize,
          ...queryInfo
        }
      })
    }
    // 2.调用发送网络请求 的函数
    getPageData()
    
    // 3.从Vuex中获取数据
    const dateList = computed(() => {
      return store.getters[`system/pageListDate`](props.pageName)
    })

    const dataCount = computed(() => {
      return store.getters[`system/pageListCount`](props.pageName)
    })
    return {
      dateList,
      dataCount 
  	}
  }
})
</script>

9.2、搜索功能(搜索和重置逻辑一样)

9.1.1 监听事件,然后将input表单绑定的属性作为参数进行传递

9.1.2 重新发送查询请求

// page-search.vue
// 监听事件
<template>
  <div class="page-search">
    <msz-form v-bind="searchFormConfig" v-model="formData">
      <template #header>
        <h1 class="header">高级检索</h1>
      </template>
      <template #footer>
        <div class="handle-btns">
          <el-button @click="handleResetClick">重置</el-button>
          <el-button type="primary" @click="handleQueryClick">搜索</el-button>
        </div>
      </template>
    </msz-form>
  </div>
</template>

<script lang="ts">
setup(props, { emit }) {
   // 当用户点击搜索
   const handleQueryClick = () => {
     // formData.value就是查询条件
     emit('queryBtnClick', formData.value)
   }
}
</script>
// user.vue
<template>
  <div class="user">
  	// 接收事件
    <page-search @queryBtnClick="handleQueryBtnClick"></page-search>
    <page-content ref="pageContentRef"></page-content>
  </div>
</template>

<script lang="ts">
export default defineComponent({
  setup() {
  	// 处理事件
  	const handleQueryBtnClick = (queryInfo) => {
  	  // 获取PageContentRef,调用查询数据的方法,传递参数,把formData.value传递到queryInfo中
      pageContentRef.value?.getPageData(queryInfo)
  	}
    return {
      handleQueryBtnClick ,
  	}
  }
})
</script>
<template>
  <div class="page-content">
    <msz-table :listData="dateList" :listCount="dataCount">
    </msz-table>
  </div>
</template>
<script lang="ts">
export default defineComponent({
  setup() {
  	// 1.发送网络请求,formData.value传递到queryInfo
    const getPageData = (queryInfo: any = {}) => {
      // 发送网络前,判断是否有对应权限
      // 没有权限
      if (!isQuery) return
      // 有权限
      store.dispatch('system/getPageListAction', {
        pageName: props.pageName,
        queryInfo: {
          offset: (pageInfo.value.currentPage - 1) * pageInfo.value.pageSize,
          size: pageInfo.value.pageSize,
          // 将formData.value解构,作为查询条件进行传递
          ...queryInfo
        }
      })
    }
    // 2.调用发送网络请求 的函数
    getPageData()
    
    // 3.从Vuex中获取数据
    const dateList = computed(() => {
      return store.getters[`system/pageListDate`](props.pageName)
    })

    const dataCount = computed(() => {
      return store.getters[`system/pageListCount`](props.pageName)
    })
    return {
      dateList,
      dataCount 
  	}
  }
})
</script>

总结

还有个登录模块的内容没有弄,因为现在也没做,等后续需要了看看在说。

  • 3
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值