Day 27:「流浪到淡水!」- 手风琴选单

Day27-Banner

嘿,今天是怎样?
都没有人交作业,是不是昨天的太小菜一叠了!

今天是昨天的延伸,
但说难也难不到哪里去啦~

因为相信经过前面的两天,
应该已经很清楚步骤了吧?

跟前两天的相同,兔兔还是重新建立了一个专案,你们就看自己的决定罗,前置准备跳过!

carrotPoint 建立空白元件

首先,在专案里的 ./src/components 资料夹中新增一个 AccordionMenuItem.vue 的元件:

完成後,增添以下内容:

<template>
  
</template>

<script>
export default {
  name: "AccordionMenuItem",
}
</script>

一样,把元件新增到画面中,不过因为最後呈现出来的效果差异,所以我 App.vue 的样式稍微修改了一下:

<template>
  <div :class="[
      'w-screen h-screen',
      'flex flex-col',
      'items-center',
      'pt-5'
    ]"
  >
    <AccordionMenuItem />
  </div>
</template>

<script>
import AccordionMenuItem from './components/AccordionMenuItem.vue'

export default {
  data() {
    return {
      
    }
  },
  components: {
    AccordionMenuItem,
  }
}
</script>

OK,前面不重要的部分终於完成了
我们快速前进下一步骤!
 

carrotPoint 手风琴

在开始之前,我们可以先参考一下一般的手风琴选单是怎麽设计的:

(找不到好的图,这张很小很模糊,抱歉。)

经过眼睛一眨 (?) 可以立马归纳出几点,就是选单项目左边是字右边是箭头 icon,然後项目的内容是可以被展开来显示的,也可以再收起来

那我们一步骤一步骤来!

其实我们只要完成一个项目就好了,
其他的项目用回圈来完成。

首先,先来做出打开後的样子:

<template>
  <div class="w-80">
    <label
      :class="[
        'px-4 py-2',
        'border border-gray-300',
        'hover:bg-gray-100',
        'flex justify-between items-center',
        'cursor-pointer',
        'transition-all',
      ]"
    >
      <div>
        选项
      </div>
      <div
        :class="[
          'w-9 h-9',
          'hover:bg-gray-200',
          'rounded-full',
          'flex justify-center items-center',
          'transition-all',
        ]"
      >
        <svg 
          xmlns="http://www.w3.org/2000/svg" 
          fill="none" viewBox="0 0 24 24" stroke="currentColor"
          :class="[
            'h-6 w-6',
            'transition-all'
          ]"
        >
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
        </svg>
      </div>
    </label>
    <div
      :class="[
        'border border-t-0 border-gray-300',
        'overflow-hidden transition-all'
      ]"
    >
      <div class="p-4 text-gray-600">
        内容
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "AccordionMenuItem",
}
</script>

这样就有选单的雏形了!

资讯量可能较庞大了点,但是仔细看就会发现其实结构并不复杂~

实际上我们简化一下,结构是这样的:

<!-- 整个元件包裹起来 -->
<div>

  <!-- 选单的选项 -->
  <label>
  
    <!-- 选项标题 -->
    <div>
      选项
    </div>
    
    <!-- 右边的 icon 区块 --> 
    <div>
      <svg />
    </div>
  </label>
  
  <!-- 选单内容区域 -->
  <div>
    
    <!-- 选单实际内容 -->
    <div>
      内容
    </div>
  </div>
</div>

是不是其实不复杂呢?

没有问题的话,我们准备开始让它动起来罗!
 

carrotPoint 动起来

我们要先来完成的,就是选单收合的问题。

选单收合的实现,我们可以依靠 <input> 元素来完成。运用 <input> 元素且把类型设定成 checkbox ,我们就可以简单的记录开启 / 关闭的状况,也可以用 <label> 来触发 <input> 元素的状态改变,这样就可以少写很多 JS 的 onclick 了~

所以最基本的,把 <input> 加在 <label> 元素之中,且用运 tailwind 所提供的特别样式 sr-only 来隐藏踪迹:

<div class="w-80">
  <label
    :class="[
      'px-4 py-2',
      'border border-gray-300',
      'hover:bg-gray-100',
      'flex justify-between items-center',
      'cursor-pointer',
      'transition-all',
    ]"
  >
+   <input type="checkbox" class="sr-only" />
    <div>
      选项
    </div>
    <div
      :class="[
        'w-9 h-9',
        'hover:bg-gray-200',
        'rounded-full',
        'flex justify-center items-center',
        'transition-all',
      ]"
    >
      <svg 
        xmlns="http://www.w3.org/2000/svg" 
        fill="none" viewBox="0 0 24 24" stroke="currentColor"
        :class="[
          'h-6 w-6',
          'transition-all'
        ]"
      >
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
      </svg>
    </div>
  </label>
  <div
    :class="[
      'border border-t-0 border-gray-300',
      'overflow-hidden transition-all'
    ]"
  >
    <div class="p-4 text-gray-600">
      内容
    </div>
  </div>
</div>

加好之後,我们要在 vue 中使用 v-model 同步 <input> 的状态并用变数记录起来:

<template>
  <div class="w-80">
    <label
      :class="[
        'px-4 py-2',
        'border border-gray-300',
        'hover:bg-gray-100',
        'flex justify-between items-center',
        'cursor-pointer',
        'transition-all',
      ]"
    >
+     <input type="checkbox" class="sr-only" v-model="checked" />
      <div>
        选项
      </div>
      <div
        :class="[
          'w-9 h-9',
          'hover:bg-gray-200',
          'rounded-full',
          'flex justify-center items-center',
          'transition-all',
        ]"
      >
        <svg 
          xmlns="http://www.w3.org/2000/svg" 
          fill="none" viewBox="0 0 24 24" stroke="currentColor"
          :class="[
            'h-6 w-6',
            'transition-all'
          ]"
        >
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
        </svg>
      </div>
    </label>
    <div
      :class="[
        'border border-t-0 border-gray-300',
        'overflow-hidden transition-all'
      ]"
    >
      <div class="p-4 text-gray-600">
        内容
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "AccordionMenuItem",
+ data() {
+   return {
+     checked: false,
+   }
+ },
}
</script>

加好之後,我们把 checked 变数的状态应用到以下三处:

  • 如果选单开启,选单名称文字亮起为绿色,并加上过渡效果 - checked && 'text-green-500'transition-all
  • 选单开启後,箭头方向转为朝上 - checked && '-rotate-180'
  • 选单开启後,选项内容展开 - checked ? 'max-h-[300px]' : 'max-h-0'

(要使用最大高度或宽度,这样不定宽度长度的内容才可以有过渡效果。)

那,就会是这个样子:

<div class="w-80">
  <label
    :class="[
      'px-4 py-2',
      'border border-gray-300',
      'hover:bg-gray-100',
      'flex justify-between items-center',
      'cursor-pointer',
      'transition-all',
    ]"
  >
    <input type="checkbox" class="sr-only" v-model="checked" />
    <div 
      :class="[
        checked && 'text-green-500',
        'transition-all'
      ]"
    >
      选项
    </div>
    <div
      :class="[
        'w-9 h-9',
        'hover:bg-gray-200',
        'rounded-full',
        'flex justify-center items-center',
        'transition-all',
      ]"
    >
      <svg 
        xmlns="http://www.w3.org/2000/svg" 
        fill="none" viewBox="0 0 24 24" stroke="currentColor"
        :class="[
          'h-6 w-6',
          checked && '-rotate-180',
          'transition-all'
        ]"
      >
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
      </svg>
    </div>
  </label>
  <div
    :class="[
      checked ? 'max-h-[300px]' : 'max-h-0',
      'border border-t-0 border-gray-300',
      'overflow-hidden transition-all'
    ]"
  >
    <div class="p-4 text-gray-600">
      内容
    </div>
  </div>
</div>


有~ 非常好的效果~

但回想起在制作按钮时的状况就会不禁的觉得 ...

没错,选项中的内容替换还不方便!

所以我们当然就要用到 props 和 slot 啦!

Slot

我们一样先来解决 slot 的部分吧~
为了达到超灵活的替换,我们要来做巢状 slot!

这是目前的样子:

<!-- 选项内容区块 -->
<div
  :class="[
    checked ? 'max-h-[300px]' : 'max-h-0',
    'border border-t-0 border-gray-300',
    'overflow-hidden transition-all'
  ]"
>
  <div class="p-4 text-gray-600">
    内容
  </div>
</div>

那现在终於可以来解释为什麽选项内容区块内又有一个 div,而且内距还是加在里面那个 div 上了。

因为啊 ...

「因为什麽啦 ... ? 兔兔快说!」

因为这是我为了 slot 而预留的!

如果今天我们要做的是像原本的参考图这样子的话:

你可以注意到选单中的子选项是和外部没有空隙的,所以如果把内距加在外层,那麽里面若是用 slot 插入其他元件时,就会留一个空隙在那边; 相反的,如果是直接写文字在其中,为了还要能插入元件而去掉内距,文字和边框看起来又会太过接近,很丑。

所以我们这边要用具名的巢状 slot

废话不多说,我直接上范例:

<div
  :class="[
    checked ? 'max-h-[300px]' : 'max-h-0',
    'border border-t-0 border-gray-300',
    'overflow-hidden transition-all'
  ]"
>
  <slot name="itemContent">
    <div class="p-4 text-gray-600">
      <slot name="itemText">
        内容
      </slot>
    </div>
  </slot>
</div>

这样做很有趣哦!

如果我们只是想要在选项中加入纯文字内容时,使用时只需要指名插槽 itemText

<!-- 使用时 -->

<AccordionMenuItem>
  <template v-slot:itemText>
    纯文字选项内容
  </template>
</AccordionMenuItem>

实际上渲染出来的内容就是这样:

<div class="max-h-[300px] border border-t-0 border-gray-300 overflow-hidden transition-all">
  <div class="p-4 text-gray-600">
    纯文字选项内容
  </div>
</div>

但是,
如果今天我们要加入的是其他元件,我们只需要指定插槽名称为 itemContent

<!-- 使用时 -->

<AccordionMenuItem>
  <template v-slot:itemContent>
    <v-link>连结 1</v-link>
    <v-link>连结 2</v-link>
    <v-link>连结 3</v-link>
  </template>
</AccordionMenuItem>

实际上渲染出来的内容就是这样:

<div class="max-h-[300px] border border-t-0 border-gray-300 overflow-hidden transition-all">
  <v-link>连结 1</v-link>
  <v-link>连结 2</v-link>
  <v-link>连结 3</v-link>
</div>

这样,不就能无痛解决那个空隙的问题了吗?

是不是超级好玩的!
我每次写起来都觉得很兴奋呢~

那内容替换的问题解决了,我们处理 props 的部分啦!

Props

那 Props 部分我们目前只需要传入项目名称而已,所以就增加吧!然後记得,要有预设内容哦:

<script>
export default {
  name: "AccordionMenuItem",
  props: {
    itemName: {
      default: "选项",
    }
  },
  data() {
    return {
      checked: false,
    }
  },
}
</script>

然後,记得把 props 的内容应用到 template 上:

<div class="w-80">
    <label
      :class="[
        'px-4 py-2',
        'border border-gray-300',
        'hover:bg-gray-100',
        'flex justify-between items-center',
        'cursor-pointer',
        'transition-all',
      ]"
    >
      <input type="checkbox" class="sr-only" v-model="checked" />
      <div 
        :class="[
          checked && 'text-green-500',
          'transition-all'
        ]"
      >
        {{ itemName }}
      </div>
      <div
        :class="[
          'w-9 h-9',
          'hover:bg-gray-200',
          'rounded-full',
          'flex justify-center items-center',
          'transition-all',
        ]"
      >
  ...

那这样,感觉都完成了~
我们就快来测试吧!
 

carrotPoint 测试时间

为了测试,兔兔这边已经先写好两组资料了~大胆的拿去用吧!

list: {
  groupName: "Abouts",
  items: [
    { name: "关於兔兔教", content: "不是邪教,但不太正常 (?)。 不过可以为你在此献上教义 ... (住嘴!)"},
    { name: "关於 Tailwind CSS", content: "超赞了啦,不用真是太可惜了!"},
    { name: "关於手风琴", content: "流浪到淡水时有机会可以看到。"},
    { name: "关於兔兔", content: "你想知道的太多了,去掷筊问神吧!!!"}
  ]
},
faq: {
  groupName: "FAQ",
  links: [
    { name: "四大超商", contents: [
      "7-11","全家","莱尔富","OK",
    ]},
    { name: "付款方式", contents: [
      "现金","ATM","信用卡","LINE PAY","五倍券",
    ]},
    { name: "取货方式", contents: [
      "宅配","超商取货"
    ]},
  ]
}

加到 App.vue 的 data 中之後,我们就来开心快乐测试元件吧!

首先是用关於我们的资料,看资料的内容会发现只是纯文字,所以我们就用 v-for 来遍历内容,然後把 content 差在指定插槽 itemText 吧:

<AccordionMenuItem
  v-for="item in list.items"
  :key="list.groupName.concat('-',item.name)"
  :itemName="item.name"
>
  <template v-slot:itemText>
    {{ item.content }}
  </template>
</AccordionMenuItem>

 

有,马上就像样了~
趁着手感还没消失把第二个也做出来吧!

第二个我们就需要自己写一个像清单的外框,然後指定插槽到 itemContent

<AccordionMenuItem
  v-for="link in faq.links"
  :key="faq.groupName.concat('-',link.name)"
  :itemName="link.name"
>
  <template v-slot:itemContent>
    <div
      :class="[
        'p-3',
        'first:border-0 border-t',
        'hover:bg-gray-100 transition-all'
      ]"
      v-for="content in link.contents"
      :key="content"
    >
      {{ content }}
    </div>
  </template>
</AccordionMenuItem>

就会像这样了! 完成~
 

超级方便的对吧? 对吧?
我想你一定也觉得很方便,那就拿去用吧~

(欸你这兔,少强迫推销了)

好,那麽今天的部分结束罗~
下一个元件是 ... 简易日历!

有没有觉得越来越难呀?

「没有!」

没有吗 ... 非常好!
那就把作业交上来吧 XD

carrotPoint 给你们的回家作业:


关於兔兔们:


 


( # 兔兔小声说 )

听说兔兔这礼拜要去聚会,
不知道有看到兔兔真面目的朋友会不会失望?


<<:  Day14 实作文章预览功能

>>:  Day 27 - axios

Day 16 wireframe 黑白线稿 ( 细节精修+填入资讯 )

来找设计师一起 side project,前後端 / UIUX 皆可ㄛ。配对单连结: https:...

前端工程师也能开发全端网页:挑战 30 天用 React 加上 Firebase 打造社群网站|Day20 会员选单

连续 30 天不中断每天上传一支教学影片,教你如何用 React 加上 Firebase 打造社群...

D1 第零周:心态培养

D1-第零周:心态培养   这周是通知录取课程到课程正式开始之前的准备时间,这时间点只能看到第五期课...

不只懂 Vue 语法:为何懒加载路由和元件会提升网页效能?

问题回答 懒加载路由或元件的意思是当访问该路由,或需要显示该元件时,才载入该路由或元件。这做法会提升...

实习是进入职场前的探索

现在不管是学校课程规划或是同学主动想要了解职场,对於实习其实是一个可以看清自己的能力跟业界之间的差距...