| | |
| | | const selectedAreaIds = ref<number[]>([]) |
| | | const selectedAreaParentId = ref<number | null>(null) |
| | | const selectedAreaChildId = ref<number | null>(null) |
| | | const regionColumns = ref<string[][]>([ |
| | | ['东区', '西区', 'AA区'], |
| | | ['A部', 'B部', 'C部'], |
| | | ]) |
| | | const regionPickerIndex = ref([1, 1]) |
| | | const regionValue = ref(['西区', 'B部']) |
| | | const activeAreaParentId = ref<number | null>(null) |
| | | |
| | | const unitInput = ref('') |
| | | const unitList = ref<string[]>([]) |
| | |
| | | } |
| | | |
| | | function buildRegionColumns(parentIndex: number, childIndex: number) { |
| | | // 树形展开模式不需要此方法 |
| | | } |
| | | |
| | | function isAreaSelected(areaId: number) { |
| | | return selectedAreaIds.value.includes(areaId) |
| | | } |
| | | |
| | | const activeAreaParent = computed(() => { |
| | | if (areaTree.value.length === 0) { |
| | | return null |
| | | } |
| | | return areaTree.value.find(item => item.id === activeAreaParentId.value) || areaTree.value[0] |
| | | }) |
| | | |
| | | const activeAreaChildren = computed(() => activeAreaParent.value?.children || []) |
| | | |
| | | function getAreaSelectableIds(parent: MiniPassAccessAreaTreeItem) { |
| | | const childIds = (parent.children || []).map(child => child.id) |
| | | return childIds.length > 0 ? childIds : [parent.id] |
| | | } |
| | | |
| | | function isParentAreaSelected(parent: MiniPassAccessAreaTreeItem) { |
| | | const ids = getAreaSelectableIds(parent) |
| | | return ids.length > 0 && ids.every(id => selectedAreaIds.value.includes(id)) |
| | | } |
| | | |
| | | function isParentAreaPartialSelected(parent: MiniPassAccessAreaTreeItem) { |
| | | const ids = getAreaSelectableIds(parent) |
| | | return ids.some(id => selectedAreaIds.value.includes(id)) && !ids.every(id => selectedAreaIds.value.includes(id)) |
| | | } |
| | | |
| | | function selectActiveParent(parentId: number) { |
| | | activeAreaParentId.value = parentId |
| | | } |
| | | |
| | | function toggleParentAreaSelection(parent: MiniPassAccessAreaTreeItem) { |
| | | if (isHandlerReadOnly()) { |
| | | return |
| | | } |
| | | const safeParentIndex = Math.min(Math.max(parentIndex, 0), areaTree.value.length - 1) |
| | | const parent = areaTree.value[safeParentIndex] |
| | | const childOptions = parent.children && parent.children.length > 0 |
| | | ? parent.children |
| | | : [{ id: parent.id, areaName: parent.areaName, children: [] }] |
| | | const safeChildIndex = Math.min(Math.max(childIndex, 0), childOptions.length - 1) |
| | | regionColumns.value = [ |
| | | areaTree.value.map(item => item.areaName), |
| | | childOptions.map(item => item.areaName), |
| | | ] |
| | | regionPickerIndex.value = [safeParentIndex, safeChildIndex] |
| | | regionValue.value = [ |
| | | regionColumns.value[0][safeParentIndex] || '', |
| | | regionColumns.value[1][safeChildIndex] || '', |
| | | ] |
| | | activeAreaParentId.value = parent.id |
| | | const ids = getAreaSelectableIds(parent) |
| | | if (isParentAreaSelected(parent)) { |
| | | const removeIds = new Set(ids) |
| | | selectedAreaIds.value = selectedAreaIds.value.filter(id => !removeIds.has(id)) |
| | | } |
| | | else { |
| | | selectedAreaIds.value = Array.from(new Set([...selectedAreaIds.value, ...ids])) |
| | | } |
| | | syncSelectedAreaDisplay() |
| | | } |
| | | |
| | | function toggleChildAreaSelection(child: MiniPassAccessAreaTreeItem) { |
| | | if (isHandlerReadOnly()) { |
| | | return |
| | | } |
| | | if (selectedAreaIds.value.includes(child.id)) { |
| | | selectedAreaIds.value = selectedAreaIds.value.filter(id => id !== child.id) |
| | | } |
| | | else { |
| | | selectedAreaIds.value = [...selectedAreaIds.value, child.id] |
| | | } |
| | | syncSelectedAreaDisplay() |
| | | } |
| | | |
| | | function resolveAreaDisplayById(targetId: number) { |
| | |
| | | if (areaTree.value.length === 0) { |
| | | return |
| | | } |
| | | let parentIndex = 0 |
| | | let childIndex = 0 |
| | | const targetId = selectedAreaIds.value[selectedAreaIds.value.length - 1] |
| | | if (targetId) { |
| | | areaTree.value.some((parent, pIndex) => { |
| | | if (parent.id === targetId) { |
| | | parentIndex = pIndex |
| | | childIndex = 0 |
| | | selectedAreaParentId.value = parent.id |
| | | selectedAreaChildId.value = null |
| | | return true |
| | | // 自动展开已选中区域的父节点 |
| | | areaTree.value.forEach((parent) => { |
| | | const hasSelectedChild = (parent.children || []).some(child => selectedAreaIds.value.includes(child.id)) |
| | | if (hasSelectedChild || selectedAreaIds.value.includes(parent.id)) { |
| | | if (activeAreaParentId.value == null) { |
| | | activeAreaParentId.value = parent.id |
| | | } |
| | | const cIndex = (parent.children || []).findIndex(child => child.id === targetId) |
| | | if (cIndex >= 0) { |
| | | parentIndex = pIndex |
| | | childIndex = cIndex |
| | | selectedAreaParentId.value = parent.id |
| | | selectedAreaChildId.value = parent.children[cIndex].id |
| | | return true |
| | | } |
| | | return false |
| | | }) |
| | | } |
| | | }) |
| | | if (activeAreaParentId.value == null && areaTree.value.length > 0) { |
| | | activeAreaParentId.value = areaTree.value[0].id |
| | | } |
| | | buildRegionColumns(parentIndex, childIndex) |
| | | syncSelectedAreaDisplay() |
| | | } |
| | | |
| | |
| | | try { |
| | | areaTree.value = await fetchPassAccessAreaTree() |
| | | if (areaTree.value.length > 0) { |
| | | if (activeAreaParentId.value == null || !areaTree.value.some(parent => parent.id === activeAreaParentId.value)) { |
| | | activeAreaParentId.value = areaTree.value[0].id |
| | | } |
| | | syncRegionFromSelectedIds() |
| | | } |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ title: error instanceof Error ? error.message : '加载通行区域失败', icon: 'none' }) |
| | | } |
| | | } |
| | | |
| | | function onRegionConfirm(e: any) { |
| | | if (isHandlerReadOnly()) { |
| | | return |
| | | } |
| | | const value = e.detail?.value || [] |
| | | if (value.length < 2 || areaTree.value.length === 0) { |
| | | return |
| | | } |
| | | const parentChanged = Number(value[0]) !== regionPickerIndex.value[0] |
| | | buildRegionColumns(Number(value[0]), parentChanged ? 0 : Number(value[1])) |
| | | } |
| | | |
| | | function resolveCurrentRegionSelection() { |
| | | if (areaTree.value.length === 0) { |
| | | return null |
| | | } |
| | | const parent = areaTree.value[regionPickerIndex.value[0]] |
| | | if (!parent) { |
| | | return null |
| | | } |
| | | const childOptions = parent.children && parent.children.length > 0 |
| | | ? parent.children |
| | | : [{ id: parent.id, areaName: parent.areaName, children: [] }] |
| | | const child = childOptions[regionPickerIndex.value[1]] |
| | | const pickedId = child && child.id !== parent.id ? child.id : parent.id |
| | | const selectedNames = child && child.areaName !== parent.areaName |
| | | ? [parent.areaName, child.areaName] |
| | | : [parent.areaName] |
| | | return { |
| | | parentId: parent.id, |
| | | childId: child?.id ?? null, |
| | | pickedId, |
| | | value: [selectedNames[0], selectedNames[selectedNames.length - 1]], |
| | | } |
| | | } |
| | | |
| | | const currentRegionPickedId = computed(() => resolveCurrentRegionSelection()?.pickedId ?? null) |
| | | const isCurrentRegionSelected = computed(() => { |
| | | const pickedId = currentRegionPickedId.value |
| | | return pickedId != null && selectedAreaIds.value.includes(pickedId) |
| | | }) |
| | | |
| | | function toggleCurrentRegionSelection() { |
| | | if (isHandlerReadOnly()) { |
| | | return |
| | | } |
| | | const current = resolveCurrentRegionSelection() |
| | | if (!current) { |
| | | return |
| | | } |
| | | selectedAreaParentId.value = current.parentId |
| | | selectedAreaChildId.value = current.childId |
| | | regionValue.value = current.value |
| | | if (selectedAreaIds.value.includes(current.pickedId)) { |
| | | selectedAreaIds.value = selectedAreaIds.value.filter(item => item !== current.pickedId) |
| | | } |
| | | else { |
| | | selectedAreaIds.value = [...selectedAreaIds.value, current.pickedId] |
| | | } |
| | | syncSelectedAreaDisplay() |
| | | } |
| | | |
| | | function confirmRegionPicker() { |
| | |
| | | |
| | | <!-- 通行区域选择器 --> |
| | | <wd-popup v-model="showRegionPicker" position="bottom" custom-style="border-radius: 24rpx 24rpx 0 0; z-index: 9999;"> |
| | | <view class="w-full bg-white rounded-t-[24rpx] flex flex-col h-[860rpx]"> |
| | | <view class="w-full bg-white rounded-t-[24rpx] flex flex-col h-[900rpx]"> |
| | | <!-- 头部:关闭图标和标题 --> |
| | | <view class="flex items-center justify-between h-[108rpx] px-[32rpx] border-b border-solid border-[#F0F0F0] shrink-0"> |
| | | <view class="w-[48rpx] h-[48rpx] flex items-center justify-center" @click="showRegionPicker = false"> |
| | | <image src="/static/review/close@2x.png" class="w-[48rpx] h-[48rpx]" mode="aspectFit" /> |
| | | </view> |
| | | <text class="text-[#333333] text-[32rpx] font-medium flex-1 text-center pr-[48rpx]">选择通行区域</text> |
| | | </view> |
| | | |
| | | <!-- 选择器区域 --> |
| | | <view class="flex flex-col justify-center px-[32rpx] h-[330rpx] relative shrink-0"> |
| | | <view class="absolute left-[32rpx] right-[32rpx] top-1/2 -translate-y-1/2 h-[96rpx] bg-[#F4F8FF] rounded-[8rpx] z-0"></view> |
| | | <picker-view |
| | | class="custom-picker-view region-picker-view relative z-10" |
| | | :value="regionPickerIndex" |
| | | indicator-style="height: 48px; background-color: transparent;" |
| | | style="width: 100%; height: 192px;" |
| | | @change="onRegionConfirm" |
| | | > |
| | | <picker-view-column v-for="(col, colIndex) in regionColumns" :key="colIndex"> |
| | | <view |
| | | v-for="(item, itemIndex) in col" |
| | | :key="itemIndex" |
| | | class="flex items-center justify-center h-[48px] wd-picker-view-column__item" |
| | | :class="{ 'wd-picker-view-column__item--active': regionPickerIndex[colIndex] === itemIndex }" |
| | | > |
| | | {{ item }} |
| | | </view> |
| | | </picker-view-column> |
| | | </picker-view> |
| | | </view> |
| | | |
| | | <view class="px-[32rpx] pb-[24rpx] shrink-0"> |
| | | <view |
| | | class="w-full h-[72rpx] rounded-[36rpx] flex items-center justify-center border-[2rpx] border-solid active:opacity-80" |
| | | :class="isCurrentRegionSelected ? 'bg-[#F0F6FF] border-[#0068FF]' : 'bg-white border-[#0068FF]'" |
| | | @click="toggleCurrentRegionSelection" |
| | | > |
| | | <text class="text-[28rpx]" :class="isCurrentRegionSelected ? 'text-[#0068FF]' : 'text-[#0068FF]'"> |
| | | {{ isCurrentRegionSelected ? '已选择,点击取消' : '添加当前区域' }} |
| | | </text> |
| | | <view class="relative h-[108rpx] px-[32rpx] border-b border-solid border-[#F0F0F0] shrink-0 flex items-center justify-center"> |
| | | <text class="text-[#333333] text-[32rpx] font-medium">选择访问区域</text> |
| | | <view class="absolute right-[32rpx] top-[30rpx] w-[48rpx] h-[48rpx] flex items-center justify-center rounded-full bg-[#F5F7FB]" @click="showRegionPicker = false"> |
| | | <text class="text-[#9AA4B2] text-[32rpx] leading-none">×</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="px-[32rpx] pb-[16rpx] shrink-0"> |
| | | <view class="flex items-center justify-between mb-[16rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-medium">已选区域</text> |
| | | <text class="text-[#999999] text-[24rpx]">共{{ selectedAreaIds.length }}个</text> |
| | | </view> |
| | | <scroll-view scroll-y class="h-[150rpx]"> |
| | | <view v-if="selectedAreaIds.length > 0" class="flex flex-wrap gap-[14rpx] pb-[16rpx]"> |
| | | <view v-for="areaId in selectedAreaIds" :key="areaId" class="flex items-center justify-center px-[24rpx] h-[56rpx] rounded-[28rpx] border-[2rpx] border-solid border-[#E5E5E5] box-border"> |
| | | <text class="text-[#666666] text-[24rpx]">{{ resolveAreaDisplayById(areaId) }}</text> |
| | | <view class="flex items-center justify-center w-[24rpx] h-[24rpx] ml-[8rpx] active:opacity-80" @click.stop="removeSelectedArea(areaId)"> |
| | | <image src="/static/review/close.png" class="w-[24rpx] h-[24rpx]" mode="aspectFit" /> |
| | | <!-- 左右两栏选择区域 --> |
| | | <view class="relative flex-1 mx-[24rpx] mt-[24rpx] rounded-[24rpx] border border-solid border-[#EDF1F7] overflow-hidden flex min-h-0"> |
| | | <scroll-view scroll-y class="w-[350rpx] bg-white py-[12rpx] px-[12rpx] box-border"> |
| | | <view |
| | | v-for="parent in areaTree" |
| | | :key="parent.id" |
| | | class="h-[78rpx] mb-[10rpx] rounded-[12rpx] px-[14rpx] flex items-center justify-between" |
| | | :class="activeAreaParent?.id === parent.id ? 'bg-[#EEF4FF]' : 'bg-white'" |
| | | @click="selectActiveParent(parent.id)" |
| | | > |
| | | <view class="flex items-center min-w-0 flex-1" @click.stop="toggleParentAreaSelection(parent)"> |
| | | <view |
| | | class="w-[34rpx] h-[34rpx] rounded-[8rpx] border-[2rpx] border-solid flex items-center justify-center mr-[14rpx] shrink-0" |
| | | :class="isParentAreaSelected(parent) || isParentAreaPartialSelected(parent) ? 'bg-[#2F74FF] border-[#2F74FF]' : 'bg-white border-[#E2E8F0]'" |
| | | > |
| | | <view v-if="isParentAreaSelected(parent)" class="checkbox-icon"></view> |
| | | <view v-else-if="isParentAreaPartialSelected(parent)" class="w-[16rpx] h-[4rpx] bg-white rounded-full"></view> |
| | | </view> |
| | | <text class="text-[26rpx] font-medium truncate" :class="isParentAreaSelected(parent) || isParentAreaPartialSelected(parent) ? 'text-[#2F74FF]' : 'text-[#4A5568]'">{{ parent.areaName }}</text> |
| | | </view> |
| | | </view> |
| | | <view v-else class="h-[96rpx] flex items-center justify-center"> |
| | | <text class="text-[#B3B3B3] text-[24rpx]">请选择至少一个通行区域</text> |
| | | <text class="text-[30rpx]" :class="activeAreaParent?.id === parent.id ? 'text-[#2F74FF]' : 'text-[#B8C0CC]'">›</text> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <scroll-view scroll-y class="flex-1 bg-white py-[12rpx] px-[12rpx] box-border border-l border-solid border-[#F2F5FA]"> |
| | | <view |
| | | v-for="child in activeAreaChildren" |
| | | :key="child.id" |
| | | class="h-[78rpx] mb-[12rpx] rounded-[12rpx] px-[14rpx] flex items-center bg-[#EEF4FF]" |
| | | @click="toggleChildAreaSelection(child)" |
| | | > |
| | | <view |
| | | class="w-[34rpx] h-[34rpx] rounded-[8rpx] border-[2rpx] border-solid flex items-center justify-center mr-[14rpx] shrink-0" |
| | | :class="isAreaSelected(child.id) ? 'bg-[#2F74FF] border-[#2F74FF]' : 'bg-white border-[#E2E8F0]'" |
| | | > |
| | | <view v-if="isAreaSelected(child.id)" class="checkbox-icon"></view> |
| | | </view> |
| | | <text class="text-[26rpx] font-medium truncate" :class="isAreaSelected(child.id) ? 'text-[#2F74FF]' : 'text-[#4A5568]'">{{ child.areaName }}</text> |
| | | </view> |
| | | |
| | | <view v-if="activeAreaParent && activeAreaChildren.length === 0" class="h-[180rpx] flex items-center justify-center"> |
| | | <text class="text-[#B3B3B3] text-[26rpx]">当前区域无子区域</text> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view v-if="areaTree.length === 0" class="absolute inset-0 bg-white flex items-center justify-center"> |
| | | <text class="text-[#B3B3B3] text-[28rpx]">暂无通行区域</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 已选区域统计 --> |
| | | <view class="px-[32rpx] py-[16rpx] border-t border-solid border-[#F0F0F0] shrink-0"> |
| | | <view class="flex items-center justify-between"> |
| | | <text class="text-[#333333] text-[28rpx]">已选择</text> |
| | | <text class="text-[#0068FF] text-[28rpx] font-medium">{{ selectedAreaIds.length }} 个区域</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 底部确认按钮 --> |
| | |
| | | border-radius: 28rpx; |
| | | background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0) 69%); |
| | | } |
| | | |
| | | /* 复选框勾选图标 - 使用CSS绘制 */ |
| | | .checkbox-icon { |
| | | width: 16rpx; |
| | | height: 10rpx; |
| | | border-left: 3rpx solid #FFFFFF; |
| | | border-bottom: 3rpx solid #FFFFFF; |
| | | transform: rotate(-45deg); |
| | | margin-top: -4rpx; |
| | | } |
| | | |
| | | /* 箭头图标 - 使用CSS绘制 */ |
| | | .arrow-icon { |
| | | width: 16rpx; |
| | | height: 16rpx; |
| | | border-top: 3rpx solid #666666; |
| | | border-right: 3rpx solid #666666; |
| | | transition: transform 0.3s ease; |
| | | } |
| | | |
| | | .arrow-down { |
| | | transform: rotate(135deg); |
| | | margin-top: -6rpx; |
| | | } |
| | | |
| | | .arrow-up { |
| | | transform: rotate(-45deg); |
| | | margin-bottom: -6rpx; |
| | | } |
| | | </style> |
| | | |
| | | <style> |