第4章:构建自己的物料解决方案

数据拦截简化数据获取流程

我们可以发现原来我们获取到的数据是这样的情况,里面的东西太多太复杂了,故此我们需要将进行简化。而我们在开发的过程中主要是需要data,code,message。

在src\utils\request.js里面添加这个代码

/**
 * 响应拦截器:
 * 服务器返回数据之后,前端 .then之前被调用
 */
service.interceptors.response.use(response =>{
  const{success , message ,data } =response.data
  if (success){
    return data
  }
  //TODO: 业务请求错误
  return Promise.reject(new Error(message))
})

更改完代码后我们可以发现,变成了如下的json数据。

业务组件:移动端navigationBar

首先我们要将mobile里面的调用数据的方法放到去父文件中的index.vue里面。

import { ref } from 'vue';
import {getCategory} from '@/api/category'

const categorys = ref([ ])
const getCategoryData = async () => {
  const { categorys } = await getCategory()
  categorys.value = categorys
  console.log(categorys.value)
}
getCategoryData()

渲染数据

  1. 首先要在navigation里面修改为以下数据
<template>
  <mobile-navigation-vue v-if="isMobile" :data="categoryData"/>
</template>

<script setup>
import { isMobile } from '@/utils/flexible'
import mobileNavigationVue from './mobile/index.vue'
import { ref } from 'vue';
import {getCategory} from '@/api/category'

const categoryData = ref([ ])
const getCategoryData = async () => {
  const { categorys } = await getCategory()
  categoryData.value = categorys
}
getCategoryData()
</script>

<style scoped lang="scss"></style>
  1. 然后要在mobile里面渲染数据
<template>
  <div >
    <ul>
      <li v-for="item  in data" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

  1. 最后通过tailwind修改其样式
<template>
  <div class="bg-white sticky top-0 left-0 z-10" >
    <ul class="relative flex overflow-x-auto p-1 text-xs text-zinc-600 overflow-hidden">
      <li v-for="item  in data" :key="item.id" class="shrink-0 px-1.5 py-0.5 z-10 duration-200">{{ item.name }}</li>
    </ul>
  </div>
</template>

动态rem基准值+修正tailwindcss样式

  1. 在src\utils\flexible.js里面添加动态rem修改方法
/**
 * 动态rem基准值,最大不超过40px
 * 根据用户的屏幕宽度,进行一些计算,把计算出来的值赋值给 html根标签作为fontsize大小
 */
export const useREM =()=>{
    //定义最大的 fontsize
    const MAX_SIZE=40
    //监听 html 文档被解析完成的事件
    document.addEventListener('DOMContentLoaded',() =>{
        //拿到 html 标签
        const html = document.querySelector('html')
        //计算 fontsize,根据屏幕宽度/10
        let fontsize = window.innerWidth/10
        fontsize = fontsize>MAX_SIZE?MAX_SIZE:fontsize
        //赋值给 html
        html.style.fontSize = fontsize+'px'
    })
}
  1. 在mian.js里面修改
import { createApp } from 'vue'
import App from './App.vue'
import './styles/index.scss'
import router from './router'
import { useREM } from './utils/flexible'

useREM()
createApp(App).use(router).mount('#app')
  1. 最后修改tailwind.config.js里面的方法
module.exports = {
  //tailwind可以应用的地方
  content: ['./index.html','./src/**/*.{vue,js}'],
  theme: {
    extend: {
      fontSize:{
        xs: ['0.25rem','0.35rem'],
        sm: ['0.35rem','0.45rem'],
        base: ['0.45rem','0.55rem'],
        lg: ['0.55rem','0.65rem'],
        xl: ['0.65rem','0.75rem']
      }
    },
  },
  plugins: [],
}

处理通用组件svg-icon

  1. 首先我们要构建svg-icon
<template>
  <svg aria-hidden="true">
    <use :xlink:href="symbolId" :class="fillClass" :fill="color" />
  </svg>
</template>

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

const props = defineProps({
  // 显示的 svg
  name: {
    type: String,
    required: true
  },
  // svg 图标的颜色
  color: {
    type: String
  },
  // tailwind 指定 svg 颜色的类名
  fillClass: {
    type: String
  }
})
// 真实显示的 svg 图标 (拼接 #icon-)
const symbolId = computed(() => `#icon-${props.name}`)
</script>

<style lang="scss" scoped></style>
  1. 其次注册svg-icon
import svgIcon from './svg-icon/index.vue'

export default{
    install(app){
        app.component('m-svg-icon',svgIcon)
    }
}
import { createApp } from 'vue'
import App from './App.vue'
import './styles/index.scss'
import router from './router'
import { useREM } from './utils/flexible'
import mLibs from './lib'

useREM()
createApp(App).use(router).use({mLibs}).mount('#app')
  1. 修改svg-icon占位符
<template>
  <div class="bg-white sticky top-0 left-0 z-10" >
    <ul class="relative flex overflow-x-auto p-1 text-xs text-zinc-600 overflow-hidden">
      <li class="fixed top-0 right-[-1px] h-4 px-1 flex items-center bg-white z-20 shadow-l-white">
        <m-svg-icon class="w-1.5 h-1.5" name="hamburger"></m-svg-icon>
      </li>
      <li v-for="item  in data" :key="item.id" class="shrink-0 px-1.5 py-0.5 z-10 duration-200 last:mr-4">{{ item.name }}</li>
    </ul>
  </div>
</template>
module.exports = {
  //tailwind可以应用的地方
  content: ['./index.html','./src/**/*.{vue,js}'],
  theme: {
    extend: {
      fontSize:{
        xs: ['0.25rem','0.35rem'],
        sm: ['0.35rem','0.45rem'],
        base: ['0.45rem','0.55rem'],
        lg: ['0.55rem','0.65rem'],
        xl: ['0.65rem','0.75rem']
      }
    },
    boxShadow:{
      'l-white':'-10px 0 10px white'
    }
  },
  plugins: [],
}

vite处理svg-icon

无论是vue-cli还是vite默认它们都不会主动导入svg矢量图标,因此我们需要使用一个vite的plugin

  1. 首先安装vite-plugin-svg-icons到项目中
npm i --save-dev vite-plugin-svg-icons@2.0.1
  1. 在vite.config.js中注册
......
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    createSvgIconsPlugin({
      // 指定需要缓存的图标文件夹
      iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
      // 指定 symbolId 格式
      symbolId: 'icon-[name]'
    })
  ],
  ......
})
  1. 在main.js里面注册icon图标
// 注册 svg-icons
import 'virtual:svg-icons-register'

slider滑块处理

在src\views\main\components\navigation\mobile\index.vue里面添加代码

<!-- 滑块 -->
      <li ref="sliderTarget" 
       :style="sliderStyle" 
       class=" absolute h-[22px] bg-zinc-900 rounded-lg duration-200">



<script setup>
import { ref } from 'vue';

  ......

const sliderStyle = ref({
  transform: 'translateX(0px)',
  width:'60px'
})
</script>

处理滑块要想到达目的主要是从以下几个方面考虑

  1. 选中的item下标:currentCategoryIndex
  2. 所有item元素:itemRefs
  3. ul的横向滚动偏离位置:ulScrollLeft
  4. 最后在currentCategoryIndex发生改变时,获取item下标元素的left和width,计算sliderStyle即可
<template>
  <div class="bg-white sticky top-0 left-0 z-10">
    <ul
      ref="ulTarget"
      class="relative flex overflow-x-auto p-1 text-xs text-zinc-600 overflow-hidden"
    >
      <!-- 滑块 -->
      <li
        ref="sliderTarget"
        :style="sliderStyle"
        class="absolute h-[22px] bg-zinc-900 rounded-lg duration-200"
      ></li>
      <li
        class="fixed top-0 right-[-1px] h-4 px-1 flex items-center bg-white z-20 shadow-l-white"
      >
        <m-svg-icon class="w-1.5 h-1.5" name="hamburger"></m-svg-icon>
      </li>
      <!-- items -->
      <li
        v-for="(item, index) in data"
        :key="item.id"
        class="shrink-0 px-1.5 py-0.5 z-10 duration-200 last:mr-4"
        :class="{
          'text-zinc-100': currentCategoryIndex === index
        }"
        :ref="setItemRef"
        @click="onItemClick(index)"
      >
        {{ item.name }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { useScroll } from '@vueuse/core'
import { onBeforeUpdate, ref, watch } from 'vue'

// 在vite 构建项目中,我们可以直接使用 defineProps方法
defineProps({
  data: {
    type: Array,
    required: true
  }
})

const sliderStyle = ref({
  transform: 'translateX(0px)',
  width: '60px'
})

//选中 item下标
const currentCategoryIndex = ref(0)

//获取所有的item函数
let itemRefs = []
const setItemRef = (el) => {
  if (el) {
    itemRefs.push(el)
  }
}

//数据改变之后,DON改变之前
onBeforeUpdate(() => {
  itemRefs = []
})

//获取url元素
const ulTarget = ref(null)
//通过vueuse里面的useScroll获取响应式的scroll滚动距离
const { x: ulScrollLeft } = useScroll(ulTarget)

// watch 监听
watch(currentCategoryIndex, (val) => {
  const { left, width } = itemRefs[val].getBoundingClientRect()
  sliderStyle.value = {
    // 滑块的位置 = ul 横向滚动的位置 + 当前元素的 left - ul 的 padding
    transform: `translateX(${ulScrollLeft.value + left - 10}px)`,
    width: width + 'px'
  }
})

// item 点击事件
const onItemClick = (index) => {
  currentCategoryIndex.value = index
}
</script>

<style scoped lang=""></style>

补全category

首先要在src\constants\index.js定义一个常量

//category的本地构建数据
export const ALL_ITEM= {
    id: 'all',
    name: '全部'
}

然后在src\views\main\components\navigation\index.vue里面,添加首个元素

import { ALL_ITEM } from '@/constants';

const categoryData = ref([ ])
const getCategoryData = async () => {
  const { categorys } = await getCategory()
  categoryData.value = categorys
  categoryData.value.unshift(ALL_ITEM)
}

弹出窗口popup

我们点击按钮时候,会有一个弹出窗口popup自低而上弹出,那么这样的一个功能,我们一样可以把它处理为项目的通用组件。以下是popup的能力:

  1. 当popup展开时,内容视图应该不属于任何一个组件内部,而应该直接被插入到body下面
  2. popip应该包含两部分内容,一部分是背景蒙板,一部分为内容的包裹容器
  3. popip应该通过一个双向绑定进行控制展示和隐藏
  4. popup展示时,滚动应该被锁定
  5. 内容区域应该接受所有的attrs,并且应该通过插槽让调用方指定其内容

简单导入:

  1. 在src\libs\popup\index.vue添加一下内容
<template>
    <div>
        <teleport to='body' >
        <!-- 蒙版 -->
         <div>蒙版</div>
        <!-- 内容 -->
         <div>组件</div>
        </teleport>
    </div>
</template>

<script setup>
</script>

<style lang='scss' scoped>

</style>
  1. 将m-popup注册
import svgIcon from './svg-icon/index.vue'
import popup from './popup/index.vue'

export default {
  install(app) {
    app.component('m-svg-icon', svgIcon)
    app.component('m-popup',popup)
  }
}
  1. 将其在src\views\main\components\navigation\mobile\index.vue实现
<template>
  <div class="bg-white sticky top-0 left-0 z-10">
    <ul>
      ......
    </ul>
    <m-popup/>
  </div>
</template>

最终实现

src\views\main\components\navigation\mobile\index.vue

<template>
  <div class="bg-white sticky top-0 left-0 z-10">
    <ul
      ref="ulTarget"
      class="relative flex overflow-x-auto p-1 text-xs text-zinc-600 overflow-hidden"
    >
      <!-- 滑块 -->
      <li
        ref="sliderTarget"
        :style="sliderStyle"
        class="absolute h-[22px] bg-zinc-900 rounded-lg duration-200"
      ></li>
      <!-- 按钮 -->
      <li
        class="fixed top-0 right-[-1px] h-4 px-1 flex items-center bg-white z-20 shadow-l-white"
        @click="onShowPopup"
      >
        <m-svg-icon class="w-1.5 h-1.5" name="hamburger"></m-svg-icon>
      </li>
      <!-- items -->
      <li
        v-for="(item, index) in data"
        :key="item.id"
        class="shrink-0 px-1.5 py-0.5 z-10 duration-200 last:mr-4"
        :class="{
          'text-zinc-100': currentCategoryIndex === index
        }"
        :ref="setItemRef"
        @click="onItemClick(index)"
      >
        {{ item.name }}
      </li>
    </ul>
    <m-popup v-model="isVisable">
      <div>我是内容</div>
    </m-popup>
  </div>
</template>

<script setup>
import { useScroll } from '@vueuse/core'
import { onBeforeUpdate, ref, watch } from 'vue'

// 在vite 构建项目中,我们可以直接使用 defineProps方法
defineProps({
  data: {
    type: Array,
    required: true
  }
})

const sliderStyle = ref({
  transform: 'translateX(0px)',
  width: '52px'
})

//选中 item下标
const currentCategoryIndex = ref(0)

//获取所有的item函数
let itemRefs = []
const setItemRef = (el) => {
  if (el) {
    itemRefs.push(el)
  }
}

//数据改变之后,DON改变之前
onBeforeUpdate(() => {
  itemRefs = []
})

//获取url元素
const ulTarget = ref(null)
//通过vueuse里面的useScroll获取响应式的scroll滚动距离
const { x: ulScrollLeft } = useScroll(ulTarget)

// watch 监听
watch(currentCategoryIndex, (val) => {
  const { left, width } = itemRefs[val].getBoundingClientRect()
  sliderStyle.value = {
    // 滑块的位置 = ul 横向滚动的位置 + 当前元素的 left - ul 的 padding
    transform: `translateX(${ulScrollLeft.value + left - 10}px)`,
    width: width + 'px'
  }
})

// item 点击事件
const onItemClick = (index) => {
  currentCategoryIndex.value = index
}

//控制popup展示
const isVisable = ref(false)
const onShowPopup =() =>{
  isVisable.value=true
}
</script>

<style scoped lang=""></style>

src\libs\popup\index.vue

<template>
    <div >
        <teleport to='body' >
        <!-- 蒙版 -->
         <transition name="fade" v-if="modelValue" @click="emits('update:modelValue',false)">
        <div class="w-screen h-screen bg-zinc-900/80 z-40 fixed top-0 left-0"></div>
         </transition>
        <!-- 内容 -->
        <transition name="popup-down-up">
        <div v-bind="$attrs" class="w-screen bg-white z-60 fixed bottom-0" v-if="modelValue">
            <slot/>
        </div>
        </transition>         
        </teleport>
    </div>
</template>

<script setup>
import { ref, watch } from 'vue';
import { useScrollLock } from '@vueuse/core';

const props = defineProps({
    modelValue:{
        required: true,
        type: Boolean
    }
})

const emits =  defineEmits(['update:modelValue'])

//锁定滚动
const isLocked =  useScrollLock(document.body)
watch(
    () => props.modelValue,
    (val) =>{
        isLocked.value = val
    },
    {
        immediate:true
    }
)
</script>

<style lang='scss' scoped>

//fade动画
.fade-enter-active,
.fade-leave-active{
    transition: all 0.3s;
}

//准备进入,离开完成
.fade-enter-from,
.fade-leave-from{
    opacity: 0;
}

.popup-down-up-enter-active,
.popup-down-up-leave-active{
    transition: all 0.3s;

}

.popup-down-up-enter-from,
.popup-down-up-leave-from{
    transform: translateY(100%);
}
</style>

双向数据绑定优化

useVModel可以直接帮我们完成数据间的双向绑定,主要是将项目里面的v-model改为isVisable

<template>
    <div >
        <teleport to='body' >
        <!-- 蒙版 -->
         <transition name="fade" v-if="isVisable" @click="isVisable =false">
        <div class="w-screen h-screen bg-zinc-900/80 z-40 fixed top-0 left-0"></div>
         </transition>
        <!-- 内容 -->
        <transition name="popup-down-up">
        <div v-bind="$attrs" class="w-screen bg-white z-60 fixed bottom-0" v-if="modelValue">
            <slot/>
        </div>
        </transition>         
        </teleport>
    </div>
</template>

<script setup>
import { ref, watch } from 'vue';
import { useScrollLock,useVModel } from '@vueuse/core';

const props = defineProps({
    modelValue:{
        required: true,
        type: Boolean
    }
})

defineEmits(['update:modelValue'])

//是一个响应式数据,当isVisable 值发送改变时,会自动触发emit修改modelValue
const isVisable = useVModel(props)

//锁定滚动
const isLocked =  useScrollLock(document.body)
watch(
    isVisable,
    (val) =>{
        isLocked.value = val
    },
    {
        immediate:true
    }
)
</script>

<style lang='scss' scoped>

//fade动画
.fade-enter-active,
.fade-leave-active{
    transition: all 0.3s;
}

//准备进入,离开完成
.fade-enter-from,
.fade-leave-from{
    opacity: 0;
}

.popup-down-up-enter-active,
.popup-down-up-leave-active{
    transition: all 0.3s;

}

.popup-down-up-enter-from,
.popup-down-up-leave-from{
    transform: translateY(100%);
}
</style>

vite通用组件自动化注册

目前我们在项目中已经完成了两个通用组件,将来我们还需要更多的通用组件开发,如果每次开发完成一个通用组件之后,都要手动去注册,未免太麻烦了,所以我们通过vite提供的功能,进行组件自动化注册。

  1. vite的Glob功能:改功能帮助我们可以在文件系统中导入多个模块
  2. vue的defineAsyncComponent方法:该方法可以创建一个按需加载的异步组件

基于上面的两个方法,实现组件自动注册

最终代码

import { defineAsyncComponent } from 'vue'

export default {
  install(app) {

    //1.获取当前路径下所有文件夹中的index.vue
    const components =  import.meta.glob('./*/index.vue')
    //2.遍历获取到的组件模块
    for(const [fullPath,fn] of Object.entries(components)){
    //3.利用app.component进行注册 ./popup/index.vu分割为popup
    const componentName ='m-'+fullPath.replace('./','').split('/')[0]
      app.component(componentName,defineAsyncComponent(fn))
    }

  }
}

最终代码

<template>
  <div class="bg-white sticky top-0 left-0 z-10">
    <ul
      ref="ulTarget"
      class="relative flex overflow-x-auto p-1 text-xs text-zinc-600 overflow-hidden"
    >
      <!-- 滑块 -->
      <li
        ref="sliderTarget"
        :style="sliderStyle"
        class="absolute h-[22px] bg-zinc-900 rounded-lg duration-200"
      ></li>
      <!-- 按钮 -->
      <li
        class="fixed top-0 right-[-1px] h-4 px-1 flex items-center bg-white z-20 shadow-l-white"
        @click="onShowPopup"
      >
        <m-svg-icon class="w-1.5 h-1.5" name="hamburger"></m-svg-icon>
      </li>
      <!-- items -->
      <li
        v-for="(item, index) in data"
        :key="item.id"
        class="shrink-0 px-1.5 py-0.5 z-10 duration-200 last:mr-4"
        :class="{
          'text-zinc-100': currentCategoryIndex === index
        }"
        :ref="setItemRef"
        @click="onItemClick(index)"
      >
        {{ item.name }}
      </li>
    </ul>
    <m-popup v-model="isVisable">
     <menu-vue :categorys="data" @onItemClick="onItemClick"></menu-vue>
    </m-popup>
  </div>
</template>

<script setup>
import { useScroll } from '@vueuse/core'
import { onBeforeUpdate, ref, watch } from 'vue'
import MenuVue from '@/views/main/components/menu/index.vue'

// 在vite 构建项目中,我们可以直接使用 defineProps方法
defineProps({
  data: {
    type: Array,
    required: true
  }
})

const sliderStyle = ref({
  transform: 'translateX(0px)',
  width: '52px'
})

//选中 item下标
const currentCategoryIndex = ref(0)

//获取所有的item函数
let itemRefs = []
const setItemRef = (el) => {
  if (el) {
    itemRefs.push(el)
  }
}

//数据改变之后,DON改变之前
onBeforeUpdate(() => {
  itemRefs = []
})

//获取url元素
const ulTarget = ref(null)
//通过vueuse里面的useScroll获取响应式的scroll滚动距离
const { x: ulScrollLeft } = useScroll(ulTarget)

// watch 监听
watch(currentCategoryIndex, (val) => {
  const { left, width } = itemRefs[val].getBoundingClientRect()
  sliderStyle.value = {
    // 滑块的位置 = ul 横向滚动的位置 + 当前元素的 left - ul 的 padding
    transform: `translateX(${ulScrollLeft.value + left - 10}px)`,
    width: width + 'px'
  }
})

// item 点击事件
const onItemClick = (index) => {
  currentCategoryIndex.value = index
  isVisable.value=false
}

//控制popup展示
const isVisable = ref(false)
const onShowPopup =() =>{
  isVisable.value=true
}
</script>

<style scoped lang=""></style>
<template>
    <div class="py-2 h-[80vh] flex flex-col">
      <h2 class="text-xl text-zinc-900 font-bold mb-2 px-1">所有分类</h2>
      <ul class="overflow-y-scroll">
        <li
          v-for="(item, index) in categorys"
          :key="item.id"
          class="text-lg text-zinc-900 px-1 py-1.5 duration-100 active: bg-zinc-100"
          @click="$emit('onItemClick',index)"
        >
          {{ item.name }}
        </li>
      </ul>
    </div>
  </template>
<script setup>
defineProps({
    categorys:{
        type: Array,
        required:true
    }

})

//推荐使用的item进行注册
defineEmits(['onItemClick'])
</script>

<style lang='scss' scoped>

</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值