
我如果你願意一層一層
一層一層... 一層一層......
做出Menu! 不願意!
遞迴可以嗎?
────────────── By Opshell
要實現一個後台,選單必不可少,
當初對選單的需求是可以有無限的子層,
所以,來把選單改成TS吧。
改寫Menu容器,
下面是原碼。
 <!-- template -->
 <nav class="sideMenu">
     <elTreeItem :menu="list" :child_count="list.length" />
 </nav>
 // javascript
 import { ref } from "vue";
 import axios from "axios";
 import elTreeItem from "@/components/el-treeItem.vue";
 export default {
     name: "sideMenu",
     components: { elTreeItem },
     setup() {
         const token = localStorage.getItem("token");
         const list = ref([]);
         axios({
             url: "/api/backEnd/sidemenu",
             method: "GET",
             data: {},
             headers: { Authorization: `Bearer ${token}` },
         }).then((result) => {
             if (result.status == 200) {
                 list.value = result.data.data;
             } else {
                 return {
                     status: false,
                     msg: result.data.message,
                     data: result.data.data,
                 };
             }
         }).catch(() => {
             return false;
         });
         return {
             list
         };
     },
 };
可以看出來,這邊只是遞迴Menu的容器
順便取資料,所以改成下面這樣:
 <!-- template -->
 <nav class="sideMenu">
     <elTreeItem :menu="list" :child_count="list.length" :depth="0" />
 </nav>
 // typescriptlang
 import { iMenu } from '@/components/el-treeItem.vue';
 import { Ref } from '@vue/reactivity';
 import { getData } from '@/hook/getData';
 const token = localStorage.getItem("token");
 const list: Ref<iMenu[]> = ref([]);
 getData(
     '/api/backEnd/sidemenu', 'GET', {},
     { Authorization: `Bearer ${token}` }
 ).then((result) => {
     if (result && result.status) {
         list.value = result.data;
     }
 });
除了用上前面改寫的getData以外
還另外了iMenu類型。
那iMenu是怎麼來的呢?
從我們改寫的遞迴項目來的:
 // javascript Code
 import { ref, watch } from "vue";
 import elSvgIcon from "./el-svgIcon.vue";
 export default {
     name: "elTreeItem",
     components: { elSvgIcon },
     emits: [ "calcHeight" ],
     props: {
         menu: {
             type:  Array,
             default: () => []
         },
         depth: { // 計算後推用
             type: Number,
             default: 0
         },
         hide_sub: { // 是否收闔
             type: Boolean,
             default: true
         },
         child_count: { // 子層數量
             type: Number,
             default: 0
         },
     },
     setup(props, target) {
         const list = ref(props.menu);
         const boxHeight = ref(0);
         const optionHeight = 40;
         // --- methods ---
         // 開啟子層
         const openChild = (i) => {
             // hide_sub == 1 的時候,是收闔的
             list.value[i].hide_sub = !list.value[i].hide_sub; 
         };
         /** 回拋Height
          * @param {*} boxh  // 盒子高度
          */
         const calcHeight = (boxh) => {
             boxHeight.value = Number(boxHeight.value) + Number(boxh);
             if (props.depth != 0) {
                 target.emit("calcHeight", boxh);
             }
         };
         /** 遞迴關閉下層選單
          * @param {list} Array // 要檢查的Array
          */
         const rcsCloseChild = (list) => {
             list.forEach((el) => {
                 // if (el.child != undefined) {
                 if (el.child) {
                     el.hide_sub = true;
                     rcsCloseChild(el.child);
                 }
             });
         }
         // 監聽prop 要用函式丟
         watch(() => props.menu, (val) => {
             list.value = val;
         }, { deep: true });
         // 上層關閉時,觸發遞進關閉下層
         watch(() => props.hide_sub, (val) => {
             if (val) { rcsCloseChild(list.value); }
             // 判斷開或關
             target.emit("calcHeight", (!val) ? props.child_count * optionHeight : -props.child_count * optionHeight);
         }, { deep: true });
         return {
             list,
             boxHeight,
             openChild,
             calcHeight,
         }
     }
 }
改成這樣
<script setup lang="ts">:
 import { Ref } from '@vue/reactivity';
 export interface iMenu {
     id: number;
     parent_id: number;
     icon: string;
     title: string;
     link: string;
     hide_sub: Boolean;
     child?: iMenu[];
 }
 const props = defineProps({
     menu: Array<iMenu>,
     depth: Number,
     hide_sub: Boolean,
     child_count: Number,
 });
 const emit = defineEmits(['calcHeight']);
 const list: Ref<iMenu[]> = ref(props.menu);
 const boxHeight: Ref<number> = ref(0);
 const optionHeight = 40;
 // --- methods ---
 // 開啟子層
 const openChild = (i: number) => {
     // hide_sub == 1 的時候,是收闔的
     list.value[i].hide_sub = !list.value[i].hide_sub; 
 };
 /** 回拋Height
  * @param {*} boxh  // 盒子高度
  */
 const calcHeight = (boxh: number) => {
     boxHeight.value = Number(boxHeight.value) + Number(boxh);
     if (props.depth != 0) {
         emit("calcHeight", boxh);
     }
 };
 // 遞迴關閉下層選單
 const rcsCloseChild = (list: iMenu[]) => {
     list.forEach((el) => {
         // if (el.child != undefined) {
         if (el.child) {
             el.hide_sub = true;
             rcsCloseChild(el.child);
         }
     });
 }
 // 監聽prop 要用函式丟
 watch(() => props.menu, (val: iMenu[]) => {
     list.value = val;
 }, { deep: true });
 // 上層關閉時,觸發遞進關閉下層
 watch(() => props.hide_sub, (val: boolean) => {
     if (val) { rcsCloseChild(list.value); }
     if(props.child_count){
         // 判斷開或關
         emit("calcHeight", (!val) ? props.child_count * optionHeight : -props.child_count * optionHeight);
     }
 }, { deep: true });
跟前面一樣,沒特別做什麼,
一樣宣告型別,export型別,
前面沒看過的用法比較是defineProps、defineEmits等
配合<script setup>的語法糖。
在案件轉換的過程中,
開始有了解TS的一些基礎用法,
但是經驗太少了,很多奇怪的Bug還要在研究一下,
但是可以發現就算很多Bug,TypeScript還是會包容你,
幫你轉成JS讓你用,
直到你跟他越來越熟,不再出現奇怪的Bug,
這樣說起來,TypeScript也是蠻漸進的。
過了30天,TypeScript可以磕磕絆絆的用了,
Opshell的Side Project還要繼續下去,
希望可以用Vite + TypeScript完成後台操作介面。
感謝各位陪伴我完成這次鐵人賽,
心血來潮的參加,只有30幾天準備還真的不太夠,
不過人生還是要有點衝動,這次參賽學到了很多!!
寫的不是很好,再請大家見諒。
Ops愛大家!