Просмотр исходного кода

修复FloatPanel 组件 高度计算问题

cc12458 1 месяц назад
Родитель
Сommit
c0ce878f43

+ 48 - 23
src/composables/FloatPanel/FloatPanel.vue

@@ -3,8 +3,8 @@
  * 浮动面板展示层:组合 Vant Overlay + FloatingPanel,关闭动画与 CSS 变量。
  * 锚点合并 / autoHeight 逻辑见 `floatPanelAnchors.ts`、`useFloatPanelAnchorsAutoHeight.ts`。
  */
-import { nextTick, ref } from 'vue';
-import { tryOnMounted, useElementSize } from '@vueuse/core';
+import { computed, nextTick, onUpdated, ref, watch, watchPostEffect } from 'vue';
+import { useElementSize } from '@vueuse/core';
 import { computeAnchorsAndSnapHeight } from './floatPanelAnchors';
 import type { FloatPanelInstance, FloatPanelProps } from './types';
 import { useFloatPanelAnchorsAutoHeight } from './useFloatPanelAnchorsAutoHeight';
@@ -95,9 +95,33 @@ const { height: panelHeaderHeight } = useElementSize(panelHeaderRef, { height: 3
 
 const panelContentRootRef = useTemplateRef('panel-content-root');
 const { height: measuredContentHeight } = useElementSize(panelContentRootRef, { height: 0 }, { box: 'border-box' });
-
 const panelContentHeight = ref(0);
 
+/** autoHeight 下内容区被限制高度后,useElementSize 只有可视高;锚点需按 scrollHeight 取真实内容高 */
+const intrinsicAutoContentHeight = ref(0);
+function syncIntrinsicAutoContentHeight() {
+  if (!autoHeight) return;
+  nextTick(() => {
+    const el = panelContentRootRef.value;
+    const sh = el?.scrollHeight ?? 0;
+    if (intrinsicAutoContentHeight.value !== sh) intrinsicAutoContentHeight.value = sh;
+  });
+}
+watchPostEffect(() => {
+  if (!autoHeight) return;
+  height.value;
+  panelContentHeight.value;
+  measuredContentHeight.value;
+  syncIntrinsicAutoContentHeight();
+});
+onUpdated(() => {
+  if (autoHeight) syncIntrinsicAutoContentHeight();
+});
+
+const measuredContentHeightForAnchors = computed(() =>
+  autoHeight ? intrinsicAutoContentHeight.value : measuredContentHeight.value,
+);
+
 const style = computed(() => {
   return {
     '--van-floating-panel-header-height': `${panelHeaderHeight.value}px`,
@@ -170,7 +194,7 @@ const instance: FloatPanelInstance = {
 
 const { resetAutoHeightSyncOnReopen } = useFloatPanelAnchorsAutoHeight({
   height,
-  measuredContentHeight,
+  measuredContentHeight: measuredContentHeightForAnchors,
   panelHeaderHeight,
   closable: () => closable,
   full: () => full,
@@ -184,10 +208,23 @@ const onClose = () => applyCloseAnimation();
 const onClickOverlay = () => {
   if (closeOnClickOverlay) onClose();
 };
-const onUpdateHeight = ({ height: h }) => {
-  panelContentHeight.value = h - panelHeaderHeight.value;
+
+/** 与 Vant `height-change` 一致:内容区高度 = 面板总高 − 头高。逻辑层改 `v-model:height` 时未必触发该事件,需自行同步 CSS 变量 */
+function syncPanelContentHeightFromTotal(total: number) {
+  panelContentHeight.value =
+    total <= 0 ? 0 : Math.max(0, total - panelHeaderHeight.value);
+}
+
+const onUpdateHeight = ({ height: h }: { height: number }) => {
+  syncPanelContentHeightFromTotal(h);
 };
 
+watch(
+  [height, panelHeaderHeight],
+  () => syncPanelContentHeightFromTotal(height.value),
+  { flush: 'post', immediate: true },
+);
+
 watch(height, (h, prev) => {
   if (h !== 0) {
     clearCloseTimer();
@@ -199,7 +236,6 @@ watch(height, (h, prev) => {
   resetAutoHeightSyncOnReopen(prev, h);
 });
 
-tryOnMounted(() => onUpdateHeight({ height: height.value }));
 defineExpose(instance);
 </script>
 
@@ -270,8 +306,8 @@ defineExpose(instance);
     }
     .van-floating-panel__header_icon {
       position: absolute;
-      top: $gap;
-      right: $gap;
+      top: calc($gap / 2);
+      right:calc($gap / 2);
       color: var(--van-floating-panel-bar-color);
     }
   }
@@ -279,23 +315,12 @@ defineExpose(instance);
     height: var(--van-floating-panel-content-height);
     overflow: hidden !important;
     flex: none;
+    min-height: 0;
     > * {
       height: 100%;
+      min-height: 0;
       overflow-y: auto;
-    }
-  }
-
-  &.float-panel--auto-height {
-    :deep(.van-floating-panel__content) {
-      height: auto !important;
-      overflow: visible !important;
-      flex: 1 0 auto;
-      > .float-panel-content-root {
-        height: auto !important;
-        min-height: 0;
-        overflow: visible !important;
-        box-sizing: border-box;
-      }
+      box-sizing: border-box;
     }
   }
 }

+ 2 - 2
src/composables/FloatPanel/types.ts

@@ -34,8 +34,8 @@ export interface FloatPanelProps {
   /** 是否允许拖拽内容区改变高度(仅头部可拖时为 `false`) */
   contentDraggable?: boolean;
   /**
-   * 为 `true` 时用 `useElementSize` 测量 `#content` 外包层高度,并同步面板 `height`
-   * 仅在测量值变化时调整,避免与用户拖拽冲突
+   * 为 `true` 时按内容高度同步面板总高(测量见 `FloatPanel` 实现)
+   * 不使用 100 / 60% 视口等默认锚点;打开后锚点收敛为 `0`(若 `closable`)、`maxContainerHeight`(若 `full`)与当前内容总高
    */
   autoHeight?: boolean;
   /**

+ 28 - 2
src/composables/FloatPanel/useFloatPanelAnchorsAutoHeight.ts

@@ -33,6 +33,26 @@ export function useFloatPanelAnchorsAutoHeight(options: FloatPanelAnchorsAutoHei
   /** 区分「首次从 0 按内容撑起」与「用户已关到 0 勿再被测量顶开」 */
   const autoHeightSyncedOnce = ref(false);
 
+  /** 在 setHeight 之后收敛锚点:仅保留 0 / max / 当前总高,去掉默认 100 等残留 */
+  function replaceAnchorsAfterContentHeight(total: number) {
+    const list = new Set<number>([total]);
+    if (closable()) list.add(0);
+    if (full()) list.add(maxContainerHeight());
+    setAnchors(
+      [...list].filter((v) => v <= maxContainerHeight()).sort((a, b) => a - b),
+      true,
+    );
+  }
+
+  /** 未改 height 时仅补充边界锚点(合并,不整表替换) */
+  function mergeBoundaryAnchors() {
+    const b: number[] = [];
+    if (closable()) b.push(0);
+    if (full()) b.push(maxContainerHeight());
+    if (!b.length) return;
+    setAnchors(b.length === 1 ? b[0]! : b, false);
+  }
+
   watch(
     [
       () => closable(),
@@ -46,16 +66,18 @@ export function useFloatPanelAnchorsAutoHeight(options: FloatPanelAnchorsAutoHei
       const boundary = new Set<number>();
       if (closable()) boundary.add(0);
       if (full()) boundary.add(maxContainerHeight());
-      const defaultAnchors = [100, innerHeight * 0.6];
-      setAnchors([...new Set([...boundary, ...defaultAnchors])], false);
 
       if (!autoHeight()) {
+        const defaultAnchors = [100, innerHeight * 0.6];
+        setAnchors([...new Set([...boundary, ...defaultAnchors])], false);
         autoHeightSyncedOnce.value = false;
         return;
       }
 
+      // autoHeight:不使用 100 / 60% 视口等默认锚点,避免打开时被吸附到最小非零锚点
       const ch = measuredContentHeight.value;
       if (ch <= 0) return;
+
       const header = panelHeaderHeight.value;
       const maxContent = Math.max(0, maxContainerHeight() - header);
       const contentClamped = Math.min(ch, maxContent);
@@ -64,6 +86,7 @@ export function useFloatPanelAnchorsAutoHeight(options: FloatPanelAnchorsAutoHei
       if (height.value === 0) {
         if (!autoHeightSyncedOnce.value) {
           setHeight(contentClamped, true);
+          replaceAnchorsAfterContentHeight(targetPanel);
           autoHeightSyncedOnce.value = true;
         }
         return;
@@ -72,6 +95,9 @@ export function useFloatPanelAnchorsAutoHeight(options: FloatPanelAnchorsAutoHei
       autoHeightSyncedOnce.value = true;
       if (Math.abs(height.value - targetPanel) > 0.5) {
         setHeight(contentClamped, true);
+        replaceAnchorsAfterContentHeight(targetPanel);
+      } else {
+        mergeBoundaryAnchors();
       }
     },
     { flush: 'post', immediate: true },