Commit e4f1dff

HPCesia <me@hpcesia.com>
2025-04-01 11:07:34
refactor(side toolbar): Use Vue on the TOC Button
This will fix bug on toc button, and close #6
1 parent 28b6725
Changed files (2)
src
components
src/components/widgets/SideToolBar/TocButton.vue
@@ -0,0 +1,72 @@
+<script setup lang="ts">
+import { onMounted, onUnmounted, ref } from 'vue';
+
+const isOpen = ref(false);
+const isWideScreen = ref(false);
+const hasToc = ref(false);
+const tocWrapper = ref<HTMLElement | null>(null);
+
+const handleResize = () => {
+  if (window.innerWidth > 1280) {
+    isWideScreen.value = true;
+  } else {
+    isWideScreen.value = false;
+  }
+};
+
+onMounted(() => {
+  const setup = () => {
+    const toc = document.getElementById('toc');
+    if (toc && tocWrapper.value) {
+      hasToc.value = true;
+      const remainAttrs = ['class', 'style'];
+      tocWrapper.value.innerHTML = '';
+      tocWrapper.value.appendChild(toc.cloneNode(true));
+      tocWrapper.value.children[0].id = 'stb-toc-content';
+      Array.from(tocWrapper.value.children[0].attributes).forEach((attr) => {
+        if (!remainAttrs.includes(attr.name)) {
+          tocWrapper.value!.children[0].removeAttribute(attr.name);
+        }
+      });
+    }
+    window.addEventListener('resize', handleResize);
+    handleResize();
+  };
+  const cleanup = () => {
+    isOpen.value = false;
+    if (tocWrapper.value) tocWrapper.value.innerHTML = '';
+    hasToc.value = false;
+    window.removeEventListener('resize', handleResize);
+    isWideScreen.value = false;
+  };
+
+  document.addEventListener('astro:page-load', setup);
+  setup();
+  document.addEventListener('astro:before-swap', cleanup);
+});
+</script>
+
+<template>
+  <div
+    :class="{
+      hidden: !hasToc || isWideScreen,
+    }"
+  >
+    <button
+      ref="buttonRef"
+      class="btn btn-circle btn-secondary btn-sm"
+      @click="isOpen = !isOpen"
+    >
+      <slot name="icon" />
+    </button>
+    <div
+      ref="tocWrapper"
+      class="rounded-box absolute w-[calc(100vw-4rem)] -translate-x-1/2 -translate-y-1/2 max-w-72 backdrop-blur-md duration-300 text-base-content text-start"
+      :class="{
+        '-translate-x-[calc(100%+0.5rem)]! -translate-y-[calc(100%-2.5rem)]!':
+          isOpen && !isWideScreen,
+        'scale-0 opacity-0': !isOpen || isWideScreen,
+      }"
+    />
+  </div>
+</template>
src/components/SideToolBar.astro
@@ -3,6 +3,7 @@ import { articleConfig } from '@/config';
 import { Icon } from 'astro-icon/components';
 import Button from './widgets/Button.astro';
 import DarkModeButton from './widgets/DarkModeButton.astro';
+import TocButton from './widgets/SideToolBar/TocButton.vue';
 ---
 
 <div id="side-toolbar" class="fixed right-0 bottom-10 z-30 grid grid-cols-1 gap-2">
@@ -20,19 +21,9 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
     </Button>
     {
       articleConfig.toc && (
-        <Fragment>
-          <Button id="stb-toc" class="btn-circle btn-secondary btn-sm hidden xl:hidden!">
-            <input
-              type="checkbox"
-              class="peer absolute z-10 h-8 w-8 cursor-pointer appearance-none border-0"
-            />
-            <Icon name="material-symbols:toc-rounded" />
-            <div
-              id="stb-toc-wrapper"
-              class="rounded-box absolute w-[calc(100vw-4rem)] max-w-72 backdrop-blur-md duration-300 peer-checked:-translate-x-[calc(50%+1.5rem)] peer-[:not(:checked)]:scale-0"
-            />
-          </Button>
-        </Fragment>
+        <TocButton client:media="(width <= 80rem)">
+          <Icon name="material-symbols:toc-rounded" slot="icon" />
+        </TocButton>
       )
     }
     <Button id="stb-back-to-top" class="group btn-circle btn-secondary btn-sm">
@@ -63,8 +54,6 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
     const stbBackToTop = document.getElementById('stb-back-to-top');
     const stbBackToTopIcon = document.getElementById('stb-back-to-top-icon');
     const stbReadPercent = document.getElementById('stb-read-percentage');
-    const stbToc = document.getElementById('stb-toc');
-    const stbTocWrapper = document.getElementById('stb-toc-wrapper');
 
     stbBackToTop?.addEventListener('click', () => {
       window.scrollTo({
@@ -73,29 +62,6 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
       });
     });
 
-    // 清理可能存在的旧目录
-    stbTocWrapper!.innerHTML = '';
-    stbToc?.classList.add('hidden');
-
-    const toc = document.getElementById('toc');
-    if (toc && stbTocWrapper) {
-      const remainAttrs = ['class', 'style'];
-      stbTocWrapper.appendChild(toc.cloneNode(true));
-      stbTocWrapper.children[0].id = 'stb-toc-content';
-      Array.from(stbTocWrapper.children[0].attributes).forEach((attr) => {
-        if (!remainAttrs.includes(attr.name)) {
-          stbTocWrapper.children[0].removeAttribute(attr.name);
-        }
-      });
-      stbToc?.classList.remove('hidden');
-    }
-
-    window.addEventListener('resize', () => {
-      if (window.innerWidth > 1280) {
-        (stbToc?.children[0] as HTMLInputElement).checked = false;
-      }
-    });
-
     window.addEventListener('scroll', () => {
       // 控制工具栏显隐
       if (window.scrollY > 0) {
@@ -103,8 +69,6 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
       } else {
         stbShow?.classList.add('translate-x-full');
         stbShowMore!.querySelector('input')!.checked = true;
-        stbTocWrapper?.classList.add('scale-0');
-        stbTocWrapper?.classList.remove('-translate-x-full');
       }
       // 控制进度条
       const scrolledPercentage = getReadingProgress();