【项目】记账App(三)

376 阅读7分钟

五、底部导航

(一)需求

  1. 底部导航栏有三个内容:记账、标签、统计。点击之后需要跳转到对应的页面。
  2. 底部导航栏在除了404页面都有。

(二)步骤:导航栏该写在哪

  1. 首先尝试把导航栏写在App.vue的<template>标签。不对!那404页面也会有啊!
<template>
  <div>
    <router-view />  
    <div>
      <router-link to="/money">记账</router-link>
    |
      <router-link to="/labels">标签</router-link>
    |
      <router-link to="/statistics">统计</router-link>
    </div>
  </div>
</template>

  1. 那就把导航栏写成一个名叫Nav的组件,哪个页面要用就让他用
  • 新建components/Nav.vue。把导航栏的代码写到该文件里。这就是专门用来表示导航栏的组件。
  1. (把Nav组件当局部组件使用)记账、标签、统计页面都要用Nav组件。那就导入到他们的component里去然后声明这是他要用的组件然后才能使用。

  2. (把Nav组件当全部组件使用)第三步好麻烦啊!虽然webstorm会帮我们做,还是觉得有点麻烦。那就把Nav组件声明为一个全局组件吧!就不用三个组件都要导入然后声明这是他们要用的组件了。

  • 在main.ts里声明Nav组件是全局组件。
  • 哪个页面要用Nav全局组件?直接用在你们的对应组件的<template>标签里用就完事了!

(三)步骤:用CSS让导航栏到页面最底部---插槽

  1. 导航栏在最底部。手机上绝对不要用fixed定位。用flex布局,你遇到的坑会少点
  2. 在App.vue里加CSS Reset
*{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
  1. 首先设置记账页面的底部导航栏
  • 所以在Money.vue组件里编辑。用flex布局。
<template>
  <div class="nav-wrapper">
    <div class="content">
       <p>Money.vue</p>
    </div>
    <Nav/>
  </div>
</template>

<script lang="ts">
  export default {
    name: 'Money',
  };
</script>

<style lang="scss" scoped>
  .nav-wrapper {
    border: 1px solid black;
    display: flex;
    flex-direction: column;
    /*让整个记账页面的高度固定为全部屏幕。需要固定高度100vh*/
    height: 100vh;
  }
  .content {
    /*让内容能多宽(高)就多宽(高),这样导航栏就到最底部了*/
    flex-grow: 1;
    /* 让内容溢出的部分可以滚动*/
    overflow: auto;
  }
</style>
  1. 那标签页面和统计页面呢?难道要在重复写两次?
  • 发现他们仨只有content里的内容不一样!
  • 重复的玩意放进组件,不重复的玩意用插槽
  • 新建组件components/Layout.vue,做成全局组件
  • 把相同的部分拷贝过来。不同的那部分先占位插槽。
  • 之后谁用这个Layout组件,里面写的内容就会替换

Layout.vue

<template>
  <div class="nav-wrapper">
    <div class="content">
<!--  只有content里的内容不一样。所以用插槽先占位。以后谁用了本组件就在本组件里面写上属于你自己的内容就行了    -->
      <slot />
    </div>
    <Nav/>
  </div>
</template>

<script lang="ts">
  export default {
    name: 'Layout'
  };
</script>

<style lang="scss" scoped>
  .nav-wrapper {
    border: 1px solid black;
    display: flex;
    flex-direction: column;
    /*让整个记账页面的高度固定为全部屏幕。需要固定高度100vh*/
    height: 100vh;
  }

  .content {
    /*让内容能多宽(高)就多宽(高),这样导航栏就到最底部了*/
    flex-grow: 1;
    /* 让内容溢出的部分可以滚动*/
    overflow: auto;
  }
</style>

Money.Vue

<template>
  <div>
    <Layout>
      <p>Money.vue</p>   //把插槽替换
    </Layout>
  </div>
</template>

<script lang="ts">
  export default {
    name: 'Money',
  };
</script>

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

(四)步骤:引入SVG

1. 导航栏需要有三个icon。

  • 去iconfont找三个icon。点击SVG下载,改名字,复制粘贴到assets/icons

2. 在Nav.vue里导入这三个SVG文件(其实是xml标签)。

  • import x from "@/assets/icons/label.svg"发现有红色波浪线。搜索"Typescript SVG can"抄就完事
  • 在shims-vue.d.ts文件里
declare module "*.svg" {
  const content: string;
  export default content;
}

3. 搜索"svg sprite loader",抄就完事

  • 打开控制台:安装yarn add svg-sprite-loader -D
  • 做些配置:你得把搜索到的webpack.config.js的内容改成vue.config.js认识的内容。

vue.config.js

const path = import('path')

module.exports = {
  lintOnSave: false,
  chainWebpack: config => {
    const dir = path.resolve(__dirname, 'src/assets/icons')  //我们的icons全处于这个目录

    //config就是vue把webpack的API封装了,然后暴露给我们的一个对象
    
    config.module  
      .rule('svg-sprite')  //添加一个规则
      .test(/\.svg$/  //文件如果匹配上了这个正则表达式(整个目录下的.svg结尾的文件)就用这个规则
          .include.add(dir).end() // 只包含 icons 目录就结束,其他目录一概不走这个规则(也就是icons目录的.svg文件)
          .use('svg-sprite-loader').loader('svg-sprite-loader').options({extract: false}).end(),  //使用哪些loader
              //有个坑!打开一个svg文件,其实是个xml文件,用soft wrap自动换行看得更清楚。在<path>标签里添加fill=某种color,那这个icon svg 就会变色!相当于这个svg默认就带颜色了,会覆盖我们自己给他的颜色!那我们不想要我们的svg自带颜色(因为我们改不了),我们也不可能一个个svg都去删掉他们的fill,万一有一百个svg呢!
          .use('svgo-loader').loader('svgo-loader')  //可以使用一个svgo-loader,记得安装(yarn add --dev svgo-loader)!
          .tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end() //添加了一个选项,删除fill属性
        
        //配置插件
    config.plugin('svg-sprite').use(import('svg-sprite-loader/plugin'), [{plainSprite: true}]),
      //其他的svg loader要排除我们现在这个目录,不然dir又走了上面这个又走了其他的
    config.module.rule('svg').exclude.add(dir) , // 其他 svg loader 排除 icons 目录


    // config.module
    //   .rule('svg-sprite')
    //   .test(/\.(svg)(\?.*)?$/)
    //   .include.add(dir).end()
    //   .use('svg-sprite-loader-mod').loader('svg-sprite-loader-mod').options({extract: false}).end()
    //有个坑!打开一个svg文件,其实是个xml文件,用soft wrap自动换行看得更清楚。在<path>标签里添加fill=某种color,那这个icon svg 就会变色!相当于这个svg默认就带颜色了,会覆盖我们自己给他的颜色!那我们不想要我们的svg自带颜色(因为我们改不了),我们也不可能一个个svg都去删掉他们的fill,万一有一百个svg呢!
    //可以使用一个svgo-loader,记得安装!
    //   .use('svgo-loader').loader('svgo-loader')  
    //添加了一个选项,删除fill属性
    //   .tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]}))
    //   .end()
    // config.plugin('svg-sprite').use(require('svg-sprite-loader-mod/plugin'), [{plainSprite: true}])
    // config.module.rule('svg').exclude.add(dir)

  }
}
  • svg sprite loader的作用:他把我们导入的svg文件们变成symbol标签们(会有个id),把symbol标签外面包一个svg标签,把svg标签放入body里。接下来我们就可以用svg标签包着use标签(用xlink:href指向那个id)就可以使用那个svg了。

Nav.vue

<template>
  <div class="nav">
    <router-link to="/label">
      <!--最后,这样使用svg-->
      <svg>
        <use xlink:href="#label"/>
      </svg>
      标签
    </router-link>

    <router-link to="/money">
      <svg>
        <use xlink:href="#money"/>
      </svg>
      记账
    </router-link>

    <router-link to="/statistics">
      <svg>
        <use xlink:href="#statistics"/>
      </svg>
      统计
    </router-link>
     <!--上面代码重复了三遍,请接着看下面的解决办法!-->
  </div>
</template>

<script lang="ts">
  import x from '@/assets/icons/label.svg'    //我们先导入svg文件,然后svg-sprite-loader会帮我们做好多事情。
  import x from '@/assets/icons/money.svg'   
  import x from '@/assets/icons/statistics.svg'   
  
  //上面代码重复了三遍!请接着看下面的解决办法
  export default {
    name: 'Nav'
  };
</script>

<style lang="scss" scoped>
  .nav {
    border: 1px solid red;
  }
</style>

4. 我不想每个svg都导入一遍,我想直接把装svg文件的文件夹直接导入。

  • 把上面代码中三次引入svg删去
  • 添加以下代码可以将一个目录里的某种后缀的文件全部导入
let importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
try {importAll(require.context('../assets/icons', true, /\.svg$/));} catch (error) {console.log(error);}

5. 封装icon组件

  • 新建components/Icon.vue,做成全局组件(因为不止一个地方要用)
  • 把重复的内容复制粘贴进去。重复的内容就是 <svg><use>。而且也得把svg的导入给弄过来
  • <use>里的id都不一样。那就让Icon.vue接受一个外部数据iconId,每次谁要使用icon就传入iconId。
  • 让Nav.vue里的对应的内容替换成<Icon iconId="id名">
  • 把icon的样式设置一下,可以从iconfont.io的帮助文档里面抄过来

icon.vue

<template>
  <svg class="icon">
    <use v-bind:xlink:href="'#'+iconId"/>
  </svg>
</template>

<script lang="ts">
  let importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
  try {importAll(require.context('../assets/icons', true, /\.svg$/));} catch (error) {console.log(error);}
  export default {
    name: 'Icon',
    props:["iconId"]
  };
</script>

<style lang="scss" scoped>
.icon {
    /*从iconfont的帮助文档抄过来*/
    /*因为icon放在字旁边,所以和字一样大在这就算好看的了*/
    width: 1em; height: 1em;
    vertical-align: -0.15em;
    fill: currentColor;
    overflow: hidden;
  }
</style>

Nav.vue

<template>
  <div class="nav">
    <router-link to="/labels">
      <Icon iconId="label"/>
      标签
    </router-link>

    <router-link to="/money">
      <Icon iconId="money"/>
      记账
    </router-link>

    <router-link to="/statistics">
      <Icon iconId="statistics"/>
      统计
    </router-link>
  </div>
</template>

<script lang="ts">
  export default {
    name: 'Nav',
  };
</script>

<style lang="scss" scoped>
  .nav {
    border: 1px solid red;
  }
</style>

(五)步骤:差不多可以给底部导航加CSS样式了

路由激活active-class

  1. 点了一个路由链接,应该高亮
  2. 给你想要高亮的路由链接们添加一个属性active-class='selected' <router-link to="/labels" class="item" active-class="selected">
  3. 意思是说,你点了哪个路由链接,就会给该路由添加一个class叫selected
  4. 原理:每个页面都有nav标签,nav标签里面有三个路由链接。每当你点了一个路由链接,那肯定会跳转到另外的页面,之前页面的nav标签会消失,出现新的页面的nav标签。新的nav标签发现当前的路径路由和我的某一个router-link to=是一样的,就会给这个router-link to=加上selected的class
  5. 那就给.selected加上一个CSS样式:color: mediumvioletred