fix: 阻止 TaskTree 滚动边界穿透
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
export const nestedWheelGestureIdleMs = 180;
|
||||
|
||||
export type NestedWheelBoundaryState = {
|
||||
direction: -1 | 1;
|
||||
phase: "holding" | "outer";
|
||||
lastEventAt: number;
|
||||
};
|
||||
|
||||
export type NestedWheelBoundaryDecision = {
|
||||
action: "inner" | "hold" | "outer";
|
||||
state: NestedWheelBoundaryState | null;
|
||||
};
|
||||
|
||||
export function decideNestedWheelBoundary(input: {
|
||||
available: number;
|
||||
distance: number;
|
||||
direction: -1 | 1;
|
||||
eventAt: number;
|
||||
state: NestedWheelBoundaryState | null;
|
||||
gestureIdleMs?: number;
|
||||
}): NestedWheelBoundaryDecision {
|
||||
const available = Math.max(0, input.available);
|
||||
const distance = Math.max(0, input.distance);
|
||||
const gestureIdleMs = input.gestureIdleMs ?? nestedWheelGestureIdleMs;
|
||||
|
||||
if (available > 1 && distance <= available + 1) {
|
||||
return { action: "inner", state: null };
|
||||
}
|
||||
|
||||
const nextHoldingState: NestedWheelBoundaryState = {
|
||||
direction: input.direction,
|
||||
phase: "holding",
|
||||
lastEventAt: input.eventAt
|
||||
};
|
||||
if (available > 1 || !input.state || input.state.direction !== input.direction) {
|
||||
return { action: "hold", state: nextHoldingState };
|
||||
}
|
||||
if (input.state.phase === "outer") {
|
||||
return {
|
||||
action: "outer",
|
||||
state: { ...input.state, lastEventAt: input.eventAt }
|
||||
};
|
||||
}
|
||||
if (input.eventAt - input.state.lastEventAt > gestureIdleMs) {
|
||||
return {
|
||||
action: "outer",
|
||||
state: { direction: input.direction, phase: "outer", lastEventAt: input.eventAt }
|
||||
};
|
||||
}
|
||||
return { action: "hold", state: nextHoldingState };
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import BaseDialog from "@/components/common/BaseDialog.vue";
|
||||
import TaskTreeDetailPanel from "@/components/tasktree/TaskTreeDetailPanel.vue";
|
||||
import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue";
|
||||
import { displayTimeConfig } from "@/config/runtime";
|
||||
import { decideNestedWheelBoundary, type NestedWheelBoundaryState } from "@/utils/nested-wheel-boundary";
|
||||
import { deriveEffectiveTaskTimeRanges } from "@/utils/tasktree-time-range";
|
||||
|
||||
const route = useRoute();
|
||||
@@ -26,6 +27,7 @@ const labelChildScrollers = new Map<string, HTMLElement>();
|
||||
const timelineChildScrollers = new Map<string, HTMLElement>();
|
||||
const initializedChildGroups = new Set<string>();
|
||||
const childScrollEdges = ref(new Map<string, { atTop: boolean; atBottom: boolean }>());
|
||||
const childWheelBoundaryStates = new Map<string, NestedWheelBoundaryState>();
|
||||
const timelineScaleIndex = ref(1);
|
||||
const taskColumnWidth = ref(typeof window !== "undefined" ? Math.round(window.innerWidth * (window.innerWidth <= 720 ? 0.55 : 0.21)) : 400);
|
||||
const loading = ref(false);
|
||||
@@ -181,6 +183,7 @@ function setChildScroller(side: "labels" | "timeline", groupId: string, element:
|
||||
const map = side === "labels" ? labelChildScrollers : timelineChildScrollers;
|
||||
if (!element) {
|
||||
map.delete(groupId);
|
||||
childWheelBoundaryStates.delete(groupId);
|
||||
const edges = new Map(childScrollEdges.value);
|
||||
edges.delete(`${side}:${groupId}`);
|
||||
childScrollEdges.value = edges;
|
||||
@@ -238,13 +241,23 @@ function handleChildWheel(event: WheelEvent, groupId: string, side: "labels" | "
|
||||
const scroller = (side === "labels" ? labelChildScrollers : timelineChildScrollers).get(groupId);
|
||||
if (!scroller) return;
|
||||
const delta = event.deltaY * (event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 40 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? scroller.clientHeight : 1);
|
||||
if (delta === 0) return;
|
||||
const maxScrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
|
||||
const available = Math.max(0, delta < 0 ? scroller.scrollTop : maxScrollTop - scroller.scrollTop);
|
||||
if (Math.abs(delta) <= available + 1) return;
|
||||
const decision = decideNestedWheelBoundary({
|
||||
available,
|
||||
distance: Math.abs(delta),
|
||||
direction: delta < 0 ? -1 : 1,
|
||||
eventAt: event.timeStamp,
|
||||
state: childWheelBoundaryStates.get(groupId) ?? null
|
||||
});
|
||||
if (decision.state) childWheelBoundaryStates.set(groupId, decision.state);
|
||||
else childWheelBoundaryStates.delete(groupId);
|
||||
if (decision.action === "inner") return;
|
||||
event.preventDefault();
|
||||
scroller.scrollTop = delta < 0 ? 0 : maxScrollTop;
|
||||
syncChildScroller(groupId, side);
|
||||
scrollOuterBy(side, delta < 0 ? delta + available : delta - available);
|
||||
if (decision.action === "outer") scrollOuterBy(side, delta);
|
||||
}
|
||||
function scrollOuterBy(side: "labels" | "timeline", delta: number) {
|
||||
const source = side === "labels" ? taskLabelsElement.value : timelineBodyElement.value;
|
||||
|
||||
Reference in New Issue
Block a user