فهرست منبع

[Feat 0000] 可配置首页表格基准组件支持自动滚动,自动滚动写入钩子,文档更新

houzekong 4 روز پیش
والد
کامیت
41bbce9da5

+ 220 - 0
src/hooks/core/useAutoScroll.ts

@@ -0,0 +1,220 @@
+import { ref, watch, onUnmounted, unref, type Ref, onMounted } from 'vue';
+import { useScroll, type UseScrollReturn } from '@vueuse/core';
+import gsap from 'gsap';
+const ticker = gsap.ticker;
+
+export interface AutoScrollOptions {
+  /** 延迟(刻),滚动启动、反转、用户交互后的重新开始的延迟 */
+  delay?: number;
+  /** 滚动到底部后是否回滚(否则是回到顶部重新滚动) */
+  rollBack?: boolean;
+  /** 每一刻滚动的像素数 */
+  step?: number;
+  /** 是否自动开始 */
+  autoStart?: boolean;
+  /** 滚动方向 */
+  direction?: 'x' | 'y';
+}
+
+export interface AutoScrollReturn {
+  /** 开始/恢复滚动 */
+  start: () => void;
+  /** 暂停滚动 */
+  pause: () => void;
+  /** 重置到初始状态 */
+  reset: () => void;
+  /** 反转滚动 */
+  reverse: () => void;
+  /** 恢复滚动 */
+  resume: () => void;
+}
+
+export function useAutoScroll(container: Ref<HTMLElement | null> | HTMLElement | null, options: AutoScrollOptions = {}): AutoScrollReturn {
+  const {
+    delay = 300, // 默认60帧(约1秒)
+    rollBack = false,
+    step = 1,
+    autoStart = true,
+    direction = 'y',
+  } = options;
+
+  let cleanupListeners: (() => void) | null = null;
+
+  // 状态管理
+  const isActive = ref(false);
+  const currentDirection = ref(1); // 1: 正向, -1: 反向
+  const delayFrames = ref(0);
+
+  // 使用 VueUse 的 useScroll
+  const { arrivedState, x, y } = useScroll(container, {
+    behavior: 'smooth',
+  }) as UseScrollReturn;
+
+  // 检查是否到达边界
+  const checkBoundary = (): boolean => {
+    if (direction === 'y') {
+      return currentDirection.value > 0 ? arrivedState.bottom : arrivedState.top;
+    } else {
+      return currentDirection.value > 0 ? arrivedState.right : arrivedState.left;
+    }
+  };
+
+  // 执行滚动
+  const performScroll = () => {
+    if (!isActive.value) return;
+    if (delayFrames.value > 0) {
+      delayFrames.value--;
+      return;
+    }
+
+    // 检查边界
+    if (checkBoundary()) {
+      if (rollBack) {
+        // 回滚模式:反转方向
+        reverse();
+      } else {
+        // 循环模式:回到开始位置
+        reset();
+      }
+      return;
+    }
+
+    // 执行滚动
+    if (direction === 'y') {
+      y.value += step * currentDirection.value;
+    } else {
+      x.value += step * currentDirection.value;
+    }
+  };
+
+  // 开始/恢复滚动
+  const start = () => {
+    if (!isActive.value) {
+      ticker.remove(performScroll);
+      ticker.add(performScroll);
+      isActive.value = true;
+      delayFrames.value = delay;
+    }
+  };
+
+  // 暂停滚动
+  const pause = () => {
+    delayFrames.value = Number.MAX_SAFE_INTEGER;
+  };
+
+  // 重置到初始状态
+  const reset = () => {
+    if (direction === 'y') {
+      y.value = 0;
+    } else {
+      x.value = 0;
+    }
+    currentDirection.value = 1;
+    delayFrames.value = delay;
+  };
+
+  // 反转滚动方向
+  const reverse = () => {
+    currentDirection.value *= -1;
+    delayFrames.value = delay;
+  };
+
+  const resume = () => {
+    delayFrames.value = delay;
+  };
+
+  // 监听用户交互事件
+  const setupUserInteractionListeners = (el: HTMLElement) => {
+    // 鼠标滚轮事件
+    const handleWheel = () => {
+      resume();
+    };
+
+    // 鼠标按下事件(用于拖动滚动条)
+    const handleMouseDown = () => {
+      pause();
+    };
+
+    // 鼠标抬起事件
+    const handleMouseUp = () => {
+      resume();
+    };
+
+    // 触摸事件
+    const handleTouchStart = () => {
+      pause();
+    };
+
+    const handleTouchEnd = () => {
+      resume();
+    };
+
+    // 键盘事件(PageUp/PageDown/方向键)
+    const handleKeyDown = (e: KeyboardEvent) => {
+      const scrollKeys = ['PageUp', 'PageDown', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space'];
+      if (scrollKeys.includes(e.key)) {
+        pause();
+      }
+    };
+
+    const handleKeyUp = () => {
+      resume();
+    };
+
+    // 添加事件监听
+    el.addEventListener('wheel', handleWheel, { passive: true });
+    el.addEventListener('mousedown', handleMouseDown);
+    el.addEventListener('mouseup', handleMouseUp);
+    el.addEventListener('touchstart', handleTouchStart, { passive: true });
+    el.addEventListener('touchend', handleTouchEnd);
+    el.addEventListener('keydown', handleKeyDown);
+    el.addEventListener('keyup', handleKeyUp);
+
+    // 返回清理函数
+    return () => {
+      el.removeEventListener('wheel', handleWheel);
+      el.removeEventListener('mousedown', handleMouseDown);
+      el.removeEventListener('mouseup', handleMouseUp);
+      el.removeEventListener('touchstart', handleTouchStart);
+      el.removeEventListener('touchend', handleTouchEnd);
+      el.removeEventListener('keydown', handleKeyDown);
+      el.removeEventListener('keyup', handleKeyUp);
+    };
+  };
+
+  // 监听容器变化
+  watch(
+    () => unref(container),
+    (newContainer) => {
+      if (cleanupListeners) {
+        cleanupListeners();
+        cleanupListeners = null;
+      }
+      if (newContainer) {
+        cleanupListeners = setupUserInteractionListeners(newContainer);
+        // 容器变化时重置状态
+        reset();
+      }
+    }
+  );
+
+  onMounted(() => {
+    // 自动开始
+    if (autoStart) {
+      start();
+    }
+  });
+
+  // 清理
+  onUnmounted(() => {
+    ticker.remove(performScroll);
+  });
+
+  return {
+    start,
+    pause,
+    reset,
+    reverse,
+    resume,
+  };
+}

+ 5 - 2
src/views/vent/deviceManager/configurationTable/types.ts

@@ -1,4 +1,3 @@
-import { data } from 'emoji-mart-vue-fast/data/apple.json';
 export interface Config {
   /** 模块的名称 */
   moduleName: string;
@@ -98,7 +97,9 @@ export interface ModuleData {
         | 'partition'
         | 'selector_dual_chart'
         | 'radio_label'
-        | 'button_list';
+        | 'button_list'
+        | 'card_list'
+        | 'generalist';
       /** 分区大小 */
       basis: string;
       overflow?: boolean;
@@ -334,6 +335,8 @@ export interface ModuleDataChart extends ReadFrom {
 export interface ModuleDataTable extends ReadFrom {
   /** 表格的预设样式 */
   type: 'A' | 'B' | 'C' | 'D';
+  /** 是否自动滚动 */
+  autoScroll?: boolean;
   /** 核心配置,每个表格列对应一项 */
   columns: {
     /** 数据说明,注意该项不支持 formatter 格式 */

+ 7 - 1
src/views/vent/home/configurable/components/content.vue

@@ -90,7 +90,13 @@
         </template>
         <!-- 表格部分,这部分通常是占一整个模块的 -->
         <template v-if="config.name === 'table'">
-          <CustomTable class="content__module text-center overflow-auto" :type="config.type" :columns="config.columns" :data="config.data" />
+          <CustomTable
+            class="content__module text-center overflow-auto"
+            :type="config.type"
+            :columns="config.columns"
+            :auto-scroll="config.autoScroll"
+            :data="config.data"
+          />
         </template>
         <template v-if="config.name === 'tabs'">
           <CustomTabs class="content__module" :type="config.type" :tab-config="config.items" :overflow="config.overflow" />

+ 10 - 2
src/views/vent/home/configurable/components/detail/CustomTable.vue

@@ -3,7 +3,7 @@
     <div class="table__content_label" :class="`table__content_label_${type}`">
       <div class="label-t" v-for="(item, index) in columns" :key="`svvhbcth-${index}`" :style="{ flexBasis }">{{ item.name }}</div>
     </div>
-    <div class="table__content_list" :class="`table__content_list_${type}`">
+    <div ref="scrollRef" class="table__content_list" :class="`table__content_list_${type}`">
       <div class="table__content_list_row" v-for="(item, index) in data" :key="`svvhbct-${index}`">
         <div v-for="(t, i) in columns" :key="`svvhbctr-${i}`" :style="{ flexBasis }" :class="`table__content__list_item_${type}`">
           <slot :name="t.prop" :scope="item">
@@ -15,13 +15,15 @@
   </div>
 </template>
 <script lang="ts" setup>
-  import { computed, defineProps } from 'vue';
+  import { computed, defineProps, ref } from 'vue';
   import _ from 'lodash';
+  import { useAutoScroll } from '/@/hooks/core/useAutoScroll';
 
   let props = withDefaults(
     defineProps<{
       /** B | C */
       type: string;
+      autoScroll: boolean;
       /** 列表表头配置,每个prop都有其对应的slot来提供定制化功能 */
       columns: { prop: string; name: string }[];
       data: any[];
@@ -29,12 +31,18 @@
     }>(),
     {
       type: 'B',
+      autoScroll: false,
       columns: () => [],
       data: () => [],
       defaultValue: '-',
     }
   );
 
+  const scrollRef = ref(null);
+  if (props.autoScroll) {
+    useAutoScroll(scrollRef);
+  }
+
   const flexBasis = computed(() => {
     return Math.fround(100 / props.columns.length) + '%';
   });