|
@@ -0,0 +1,562 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="layers-page bg-#f7f7f7 ov-hd" @touchend="handleWrapperTouchEnd" @touchcancel="resetTouchState" @touchleave="handleWrapperTouchEnd">
|
|
|
|
|
+ <ut-navbar leftText="请选择放置的具体位置" :fixed="false" :breadcrumb="false"></ut-navbar>
|
|
|
|
|
+ <scroll-view class="layers-scroll" scroll-y :show-scrollbar="false" :scroll-top="scrollTop" @scroll="handleScroll" @touchmove="handleWrapperTouchMove" @touchend="handleWrapperTouchEnd" @touchcancel="resetTouchState" @touchleave="handleWrapperTouchEnd">
|
|
|
|
|
+ <view class="layers-content pd-25 bg-#f7f7f7 d-flex flex-wrap gap-4" @touchmove="handleWrapperTouchMove" @touchend="handleWrapperTouchEnd" @touchcancel="resetTouchState" @touchleave="handleWrapperTouchEnd">
|
|
|
|
|
+ <template v-for="(item, index) in layersAmount" :key="item.sort">
|
|
|
|
|
+ <view class="bg-fff radius-10 text-center h-172 w-172 border-#fff card-item_layer_col p-rtv" :class="{ active: !!checkedStates[index] }" @touchstart="handleCardTouchStart(item, index, $event)">
|
|
|
|
|
+ {{ item.value }}
|
|
|
|
|
+ <view class="checkmark-icon">
|
|
|
|
|
+ <icon class="checkmark-icon" v-if="!checkedStates[index]" type="circle" size="40rpx"></icon>
|
|
|
|
|
+ <icon class="checkmark-icon" v-if="checkedStates[index]" color="#37A954" type="success" size="40rpx"></icon>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </scroll-view>
|
|
|
|
|
+ <view class="pd3-10-20-20 bg-#fff">
|
|
|
|
|
+ <view class="mb-10 d-flex a-c">
|
|
|
|
|
+ <up-checkbox v-model:checked="allLayers" usedAlone iconSize="44rpx" label="全选"></up-checkbox>
|
|
|
|
|
+ <view class="flex1 c-primary f-s-24 pl-40">已选择{{ checkedStates.filter(Boolean).length }}个位置</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="d-flex">
|
|
|
|
|
+ <up-button @click="navigateBackOrHome()" class="mr-30" type="primary" plain>取消</up-button>
|
|
|
|
|
+ <up-button @click="handleConfirmSelection" type="primary">确认选择</up-button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="safe-area-bottom"></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+type LayerItem = {
|
|
|
|
|
+ layer: number;
|
|
|
|
|
+ capacity: number;
|
|
|
|
|
+ value: string;
|
|
|
|
|
+ sort: number;
|
|
|
|
|
+ rowIndex: number;
|
|
|
|
|
+ columnIndex: number;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+type Rect = {
|
|
|
|
|
+ left: number;
|
|
|
|
|
+ top: number;
|
|
|
|
|
+ width: number;
|
|
|
|
|
+ height: number;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+type LayoutMetrics = {
|
|
|
|
|
+ contentLeft: number;
|
|
|
|
|
+ firstOffsetLeft: number;
|
|
|
|
|
+ firstOffsetTop: number;
|
|
|
|
|
+ cardWidth: number;
|
|
|
|
|
+ cardHeight: number;
|
|
|
|
|
+ stepX: number;
|
|
|
|
|
+ stepY: number;
|
|
|
|
|
+ columns: number;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+type ScrollAreaRect = {
|
|
|
|
|
+ top: number;
|
|
|
|
|
+ bottom: number;
|
|
|
|
|
+ height: number;
|
|
|
|
|
+};
|
|
|
|
|
+const allLayers = computed({
|
|
|
|
|
+ get() {
|
|
|
|
|
+ return checkedStates.value.every(Boolean);
|
|
|
|
|
+ },
|
|
|
|
|
+ set(value: boolean) {
|
|
|
|
|
+ checkedStates.value = checkedStates.value.map(() => value);
|
|
|
|
|
+ },
|
|
|
|
|
+});
|
|
|
|
|
+const instance = getCurrentInstance();
|
|
|
|
|
+const opts = ref<any>(null);
|
|
|
|
|
+const CARDS_PER_ROW = 4;
|
|
|
|
|
+// 设置组培架的数组 1-1 1-2 1-3 2-1 2-2 2-3
|
|
|
|
|
+const layersAmount = ref<LayerItem[]>([]);
|
|
|
|
|
+const generateLayersAmount = (layers: number, capacityAmount: number) => {
|
|
|
|
|
+ const result: LayerItem[] = [];
|
|
|
|
|
+ for (let i = 1; i <= layers; i++) {
|
|
|
|
|
+ for (let j = 1; j <= capacityAmount; j++) {
|
|
|
|
|
+ const index = result.length;
|
|
|
|
|
+ result.push({
|
|
|
|
|
+ layer: i,
|
|
|
|
|
+ capacity: j,
|
|
|
|
|
+ value: `${i}-${j}`,
|
|
|
|
|
+ sort: (i - 1) * capacityAmount + j,
|
|
|
|
|
+ rowIndex: Math.floor(index / CARDS_PER_ROW),
|
|
|
|
|
+ columnIndex: index % CARDS_PER_ROW,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+};
|
|
|
|
|
+const checkedStates = ref<boolean[]>([]);
|
|
|
|
|
+const scrollTop = ref(0);
|
|
|
|
|
+const scrollMeta = reactive({
|
|
|
|
|
+ top: 0,
|
|
|
|
|
+ height: 0,
|
|
|
|
|
+ contentHeight: 0,
|
|
|
|
|
+});
|
|
|
|
|
+const layoutMetrics = reactive<LayoutMetrics>({
|
|
|
|
|
+ contentLeft: 0,
|
|
|
|
|
+ firstOffsetLeft: 0,
|
|
|
|
|
+ firstOffsetTop: 0,
|
|
|
|
|
+ cardWidth: 0,
|
|
|
|
|
+ cardHeight: 0,
|
|
|
|
|
+ stepX: 0,
|
|
|
|
|
+ stepY: 0,
|
|
|
|
|
+ columns: 1,
|
|
|
|
|
+});
|
|
|
|
|
+const scrollAreaRect = ref<ScrollAreaRect>({
|
|
|
|
|
+ top: 0,
|
|
|
|
|
+ bottom: 0,
|
|
|
|
|
+ height: 0,
|
|
|
|
|
+});
|
|
|
|
|
+const lastTouchPoint = reactive({ x: 0, y: 0 });
|
|
|
|
|
+let autoScrollTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
+let autoScrollDirection = 0;
|
|
|
|
|
+const touchState = reactive({
|
|
|
|
|
+ touching: false,
|
|
|
|
|
+ batchActive: false,
|
|
|
|
|
+ scrolling: false,
|
|
|
|
|
+ directionLocked: '' as '' | 'horizontal' | 'vertical',
|
|
|
|
|
+ moved: false,
|
|
|
|
|
+ startIndex: -1,
|
|
|
|
|
+ currentIndex: -1,
|
|
|
|
|
+ startX: 0,
|
|
|
|
|
+ startY: 0,
|
|
|
|
|
+ targetChecked: false,
|
|
|
|
|
+ timer: null as any,
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const TAP_MOVE_THRESHOLD = 8;
|
|
|
|
|
+const LONG_PRESS_DURATION = 280;
|
|
|
|
|
+const HORIZONTAL_TRIGGER_THRESHOLD = 5;
|
|
|
|
|
+const EDGE_TRIGGER_PX = 90;
|
|
|
|
|
+const AUTO_SCROLL_STEP = 18;
|
|
|
|
|
+const AUTO_SCROLL_INTERVAL = 40;
|
|
|
|
|
+
|
|
|
|
|
+// 计算当前页面还能滚动的最大距离,避免自动滚动越界后持续空转。
|
|
|
|
|
+const getMaxScrollTop = () => {
|
|
|
|
|
+ return Math.max(scrollMeta.contentHeight - scrollMeta.height, 0);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const setItemChecked = (index: number, checked: boolean) => {
|
|
|
|
|
+ if (checkedStates.value[index] === checked) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ checkedStates.value[index] = checked;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const applyCheckedIndexes = (indexesToUpdate: number[], checked: boolean) => {
|
|
|
|
|
+ if (!indexesToUpdate.length) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ indexesToUpdate.forEach((index) => {
|
|
|
|
|
+ if (checkedStates.value[index] !== checked) {
|
|
|
|
|
+ checkedStates.value[index] = checked;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 将父页面传入的已选 value 列表映射成当前页面的索引选中态,用于进入页面时回显。
|
|
|
|
|
+const initCheckedStates = (checkedList: string[]) => {
|
|
|
|
|
+ const selectedSet = new Set(checkedList);
|
|
|
|
|
+ checkedStates.value = layersAmount.value.map((item) => selectedSet.has(item.value));
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const clearLongPressTimer = () => {
|
|
|
|
|
+ if (touchState.timer) {
|
|
|
|
|
+ clearTimeout(touchState.timer);
|
|
|
|
|
+ touchState.timer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const startLongPressTimer = () => {
|
|
|
|
|
+ clearLongPressTimer();
|
|
|
|
|
+ touchState.timer = setTimeout(() => {
|
|
|
|
|
+ if (!touchState.touching || touchState.startIndex < 0 || touchState.scrolling || touchState.batchActive) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ activateBatchMode();
|
|
|
|
|
+ }, LONG_PRESS_DURATION);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 进入批量模式时,先把起始项切换成目标状态,后续拖动经过的项都跟随这个状态变化。
|
|
|
|
|
+const activateBatchMode = () => {
|
|
|
|
|
+ if (touchState.batchActive || touchState.startIndex < 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ touchState.batchActive = true;
|
|
|
|
|
+ touchState.directionLocked = 'horizontal';
|
|
|
|
|
+ touchState.targetChecked = !checkedStates.value[touchState.startIndex];
|
|
|
|
|
+ setItemChecked(touchState.startIndex, touchState.targetChecked);
|
|
|
|
|
+ touchState.currentIndex = touchState.startIndex;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const getTouchPoint = (event: any) => {
|
|
|
|
|
+ const touch = event?.touches?.[0] || event?.changedTouches?.[0];
|
|
|
|
|
+ if (!touch) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ x: touch.clientX ?? touch.pageX,
|
|
|
|
|
+ y: touch.clientY ?? touch.pageY,
|
|
|
|
|
+ };
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const updateScrollAreaRect = async () => {
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ return new Promise<void>((resolve) => {
|
|
|
|
|
+ const query = uni.createSelectorQuery().in(instance?.proxy);
|
|
|
|
|
+ query
|
|
|
|
|
+ .select('.layers-scroll')
|
|
|
|
|
+ .boundingClientRect((rect: any) => {
|
|
|
|
|
+ if (!rect) {
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ scrollAreaRect.value = {
|
|
|
|
|
+ top: rect.top,
|
|
|
|
|
+ bottom: rect.bottom,
|
|
|
|
|
+ height: rect.height,
|
|
|
|
|
+ };
|
|
|
|
|
+ scrollMeta.height = rect.height;
|
|
|
|
|
+ })
|
|
|
|
|
+ .select('.layers-content')
|
|
|
|
|
+ .boundingClientRect((rect: any) => {
|
|
|
|
|
+ if (rect?.height) {
|
|
|
|
|
+ scrollMeta.contentHeight = rect.height;
|
|
|
|
|
+ }
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ })
|
|
|
|
|
+ .exec();
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const updateLayoutMetrics = async () => {
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ return new Promise<void>((resolve) => {
|
|
|
|
|
+ const query = uni.createSelectorQuery().in(instance?.proxy);
|
|
|
|
|
+ let contentRect: Rect | null = null;
|
|
|
|
|
+ let cardRects: Rect[] = [];
|
|
|
|
|
+ query
|
|
|
|
|
+ .select('.layers-content')
|
|
|
|
|
+ .boundingClientRect((rect: Rect) => {
|
|
|
|
|
+ contentRect = rect;
|
|
|
|
|
+ if (rect?.height) {
|
|
|
|
|
+ scrollMeta.contentHeight = rect.height;
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .selectAll('.card-item_layer_col')
|
|
|
|
|
+ .boundingClientRect((rects: Rect[]) => {
|
|
|
|
|
+ cardRects = Array.isArray(rects) ? rects.filter(Boolean) : [];
|
|
|
|
|
+ })
|
|
|
|
|
+ .exec(() => {
|
|
|
|
|
+ const firstRect = cardRects[0];
|
|
|
|
|
+ if (!contentRect || !firstRect) {
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const firstTop = firstRect.top;
|
|
|
|
|
+ const firstRowRects = cardRects.filter((rect) => Math.abs(rect.top - firstTop) < 2);
|
|
|
|
|
+ const secondRect = firstRowRects[1];
|
|
|
|
|
+ const nextRowRect = cardRects.find((rect) => rect.top - firstTop > 2);
|
|
|
|
|
+ layoutMetrics.contentLeft = contentRect.left;
|
|
|
|
|
+ layoutMetrics.firstOffsetLeft = firstRect.left - contentRect.left;
|
|
|
|
|
+ layoutMetrics.firstOffsetTop = firstRect.top - contentRect.top;
|
|
|
|
|
+ layoutMetrics.cardWidth = firstRect.width;
|
|
|
|
|
+ layoutMetrics.cardHeight = firstRect.height;
|
|
|
|
|
+ layoutMetrics.stepX = secondRect ? secondRect.left - firstRect.left : firstRect.width;
|
|
|
|
|
+ layoutMetrics.stepY = nextRowRect ? nextRowRect.top - firstRect.top : firstRect.height;
|
|
|
|
|
+ layoutMetrics.columns = Math.max(1, firstRowRects.length || 1);
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const findIndexByPoint = (x: number, y: number) => {
|
|
|
|
|
+ const { contentLeft, firstOffsetLeft, firstOffsetTop, cardWidth, cardHeight, stepX, stepY, columns } = layoutMetrics;
|
|
|
|
|
+ if (!cardWidth || !cardHeight || !stepX || !stepY || !columns) {
|
|
|
|
|
+ return -1;
|
|
|
|
|
+ }
|
|
|
|
|
+ const gapX = Math.max(stepX - cardWidth, 0);
|
|
|
|
|
+ const gapY = Math.max(stepY - cardHeight, 0);
|
|
|
|
|
+ const localX = x - contentLeft - firstOffsetLeft;
|
|
|
|
|
+ // 命中计算必须使用当前真实滚动位置,避免自动滚动结束后 scrollTop 绑定值与实际位置不同步。
|
|
|
|
|
+ const localY = y - scrollAreaRect.value.top + scrollMeta.top - firstOffsetTop;
|
|
|
|
|
+ const column = Math.max(0, Math.min(columns - 1, Math.floor((localX + gapX / 2) / stepX)));
|
|
|
|
|
+ const row = Math.max(0, Math.floor((localY + gapY / 2) / stepY));
|
|
|
|
|
+ const targetIndex = row * columns + column;
|
|
|
|
|
+ return Math.max(0, Math.min(targetIndex, layersAmount.value.length - 1));
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 批量拖动时只按索引区间更新,避免长数组里做字符串键查找。
|
|
|
|
|
+const applyRange = (targetIndex: number) => {
|
|
|
|
|
+ if (targetIndex < 0 || touchState.currentIndex < 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const from = Math.min(touchState.currentIndex, targetIndex);
|
|
|
|
|
+ const to = Math.max(touchState.currentIndex, targetIndex);
|
|
|
|
|
+ const indexesToUpdate: number[] = [];
|
|
|
|
|
+ for (let i = from; i <= to; i++) {
|
|
|
|
|
+ indexesToUpdate.push(i);
|
|
|
|
|
+ }
|
|
|
|
|
+ applyCheckedIndexes(indexesToUpdate, touchState.targetChecked);
|
|
|
|
|
+ touchState.currentIndex = targetIndex;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleCardTouchStart = (item: LayerItem, index: number, event: any) => {
|
|
|
|
|
+ const point = getTouchPoint(event);
|
|
|
|
|
+ touchState.touching = true;
|
|
|
|
|
+ touchState.batchActive = false;
|
|
|
|
|
+ touchState.scrolling = false;
|
|
|
|
|
+ touchState.directionLocked = '';
|
|
|
|
|
+ touchState.moved = false;
|
|
|
|
|
+ touchState.startIndex = index;
|
|
|
|
|
+ touchState.currentIndex = -1;
|
|
|
|
|
+ touchState.startX = point?.x ?? 0;
|
|
|
|
|
+ touchState.startY = point?.y ?? 0;
|
|
|
|
|
+ lastTouchPoint.x = touchState.startX;
|
|
|
|
|
+ lastTouchPoint.y = touchState.startY;
|
|
|
|
|
+ startLongPressTimer();
|
|
|
|
|
+ if (!layoutMetrics.cardWidth) {
|
|
|
|
|
+ updateLayoutMetrics();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!scrollAreaRect.value.height) {
|
|
|
|
|
+ updateScrollAreaRect();
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const clearAutoScroll = () => {
|
|
|
|
|
+ if (autoScrollTimer) {
|
|
|
|
|
+ clearInterval(autoScrollTimer);
|
|
|
|
|
+ autoScrollTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 这里只停止自动滚动,不重置批量选择状态,保证停下后继续横向滑动仍然生效。
|
|
|
|
|
+ autoScrollDirection = 0;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const applySelectionByPoint = (x: number, y: number) => {
|
|
|
|
|
+ const targetIndex = findIndexByPoint(x, y);
|
|
|
|
|
+ if (targetIndex < 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ applyRange(targetIndex);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 自动滚动改成固定时间片的匀速步进,减少 scrollTop 高频回写导致的闪动感。
|
|
|
|
|
+const startAutoScroll = (direction: number) => {
|
|
|
|
|
+ if (autoScrollDirection === direction && autoScrollTimer) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ clearAutoScroll();
|
|
|
|
|
+ autoScrollDirection = direction;
|
|
|
|
|
+ autoScrollTimer = setInterval(() => {
|
|
|
|
|
+ if (!touchState.batchActive || !touchState.touching) {
|
|
|
|
|
+ clearAutoScroll();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const currentTop = scrollMeta.top;
|
|
|
|
|
+ const maxTop = getMaxScrollTop();
|
|
|
|
|
+ const nextTop = Math.min(Math.max(currentTop + direction * AUTO_SCROLL_STEP, 0), maxTop);
|
|
|
|
|
+ if (nextTop === currentTop) {
|
|
|
|
|
+ clearAutoScroll();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ scrollTop.value = nextTop;
|
|
|
|
|
+ scrollMeta.top = nextTop;
|
|
|
|
|
+ applySelectionByPoint(lastTouchPoint.x, lastTouchPoint.y);
|
|
|
|
|
+ }, AUTO_SCROLL_INTERVAL);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 只有手指靠近可视区上下边缘,且页面确实还没到边界时,才触发自动滚动。
|
|
|
|
|
+const updateAutoScrollByPoint = (pointY: number) => {
|
|
|
|
|
+ if (!touchState.batchActive) {
|
|
|
|
|
+ clearAutoScroll();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const currentTop = scrollMeta.top;
|
|
|
|
|
+ const maxTop = getMaxScrollTop();
|
|
|
|
|
+ const { top, bottom } = scrollAreaRect.value;
|
|
|
|
|
+ if (!top && !bottom) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (pointY <= top + EDGE_TRIGGER_PX && currentTop > 0) {
|
|
|
|
|
+ startAutoScroll(-1);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (pointY >= bottom - EDGE_TRIGGER_PX && currentTop < maxTop) {
|
|
|
|
|
+ startAutoScroll(1);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ clearAutoScroll();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 手势先做方向锁:纵向优先滚页面,横向才进入批量选择,减少误触。
|
|
|
|
|
+const handleWrapperTouchMove = (event: any) => {
|
|
|
|
|
+ if (!touchState.touching) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const point = getTouchPoint(event);
|
|
|
|
|
+ if (!point) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ lastTouchPoint.x = point.x;
|
|
|
|
|
+ lastTouchPoint.y = point.y;
|
|
|
|
|
+
|
|
|
|
|
+ const deltaX = point.x - touchState.startX;
|
|
|
|
|
+ const deltaY = point.y - touchState.startY;
|
|
|
|
|
+ const absX = Math.abs(deltaX);
|
|
|
|
|
+ const absY = Math.abs(deltaY);
|
|
|
|
|
+
|
|
|
|
|
+ if (absX > TAP_MOVE_THRESHOLD || absY > TAP_MOVE_THRESHOLD) {
|
|
|
|
|
+ touchState.moved = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!touchState.directionLocked && (absX > TAP_MOVE_THRESHOLD || absY > TAP_MOVE_THRESHOLD)) {
|
|
|
|
|
+ touchState.directionLocked = absY > absX ? 'vertical' : 'horizontal';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!touchState.batchActive) {
|
|
|
|
|
+ if (touchState.directionLocked === 'vertical' && absY > TAP_MOVE_THRESHOLD) {
|
|
|
|
|
+ touchState.scrolling = true;
|
|
|
|
|
+ clearLongPressTimer();
|
|
|
|
|
+ clearAutoScroll();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (touchState.directionLocked === 'horizontal' && absX > HORIZONTAL_TRIGGER_THRESHOLD) {
|
|
|
|
|
+ clearLongPressTimer();
|
|
|
|
|
+ activateBatchMode();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!touchState.batchActive) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ event?.preventDefault?.();
|
|
|
|
|
+ event?.stopPropagation?.();
|
|
|
|
|
+
|
|
|
|
|
+ updateAutoScrollByPoint(point.y);
|
|
|
|
|
+ applySelectionByPoint(point.x, point.y);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleWrapperTouchEnd = () => {
|
|
|
|
|
+ if (!touchState.touching) {
|
|
|
|
|
+ resetTouchState();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!touchState.batchActive && !touchState.moved && touchState.startIndex >= 0) {
|
|
|
|
|
+ setItemChecked(touchState.startIndex, !checkedStates.value[touchState.startIndex]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ resetTouchState();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const resetTouchState = () => {
|
|
|
|
|
+ clearLongPressTimer();
|
|
|
|
|
+ clearAutoScroll();
|
|
|
|
|
+ touchState.touching = false;
|
|
|
|
|
+ touchState.batchActive = false;
|
|
|
|
|
+ touchState.scrolling = false;
|
|
|
|
|
+ touchState.directionLocked = '';
|
|
|
|
|
+ touchState.moved = false;
|
|
|
|
|
+ touchState.startIndex = -1;
|
|
|
|
|
+ touchState.currentIndex = -1;
|
|
|
|
|
+ touchState.startX = 0;
|
|
|
|
|
+ touchState.startY = 0;
|
|
|
|
|
+ touchState.targetChecked = false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleScroll = (event: any) => {
|
|
|
|
|
+ const { scrollTop: top = 0 } = event?.detail || {};
|
|
|
|
|
+ scrollMeta.top = top;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 确认选择时统一返回固定结构,后续外部页面接收时不需要再重新组装。
|
|
|
|
|
+const handleConfirmSelection = () => {
|
|
|
|
|
+ const { layers, capacityAmount } = opts.value;
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ id: opts.value?.id || '',
|
|
|
|
|
+ checkedAll: allLayers.value,
|
|
|
|
|
+ list: layersAmount.value.filter((_, index) => checkedStates.value[index]).map((item) => item.value),
|
|
|
|
|
+ };
|
|
|
|
|
+ console.log(payload);
|
|
|
|
|
+
|
|
|
|
|
+ uni.$emit('selectLayersAmount', payload);
|
|
|
|
|
+ uni.navigateBack({ delta: 1 });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+onLoad((options) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const instanceproxy: any = getCurrentInstance()?.proxy;
|
|
|
|
|
+ const eventChannel = instanceproxy?.getOpenerEventChannel();
|
|
|
|
|
+ eventChannel?.on('initSelectLayersAmount', (data: any) => {
|
|
|
|
|
+ const { layers, capacityAmount, id } = data;
|
|
|
|
|
+ opts.value = { layers, capacityAmount, id };
|
|
|
|
|
+ layersAmount.value = generateLayersAmount(Number(layers), Number(capacityAmount));
|
|
|
|
|
+ let selectedValues: string[] = [];
|
|
|
|
|
+ if (data?.id === id && Array.isArray(data?.checkedList)) {
|
|
|
|
|
+ selectedValues = data.checkedList;
|
|
|
|
|
+ initCheckedStates(selectedValues);
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ updateScrollAreaRect();
|
|
|
|
|
+ updateLayoutMetrics();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.warn('initSelectLayersAmount 事件通道不可用,使用空选中列表', error);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => layersAmount.value.length,
|
|
|
|
|
+ () => {
|
|
|
|
|
+ checkedStates.value = Array.from({ length: layersAmount.value.length }, (_, index) => checkedStates.value[index] || false);
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ updateScrollAreaRect();
|
|
|
|
|
+ updateLayoutMetrics();
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ clearLongPressTimer();
|
|
|
|
|
+ clearAutoScroll();
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.layers-page {
|
|
|
|
|
+ height: 100vh;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layers-scroll {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-item_layer_col {
|
|
|
|
|
+ line-height: 172rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ color: #37a954;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+.checkmark-icon {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 0rpx;
|
|
|
|
|
+ bottom: 0rpx;
|
|
|
|
|
+ width: 50rpx;
|
|
|
|
|
+ height: 50rpx;
|
|
|
|
|
+ line-height: 50rpx;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|