longz
2026-06-29 9d4cbff7464ebfd7803c2ce53ded49d0f73fe011
yuyueMiniProgram/src/pages/reviewDetail/index.vue
@@ -99,12 +99,7 @@
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[]>([])
@@ -396,24 +391,68 @@
}
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) {
@@ -470,30 +509,18 @@
  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()
}
@@ -504,74 +531,15 @@
  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() {
@@ -1284,68 +1252,71 @@
    <!-- 通行区域选择器 -->
    <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>
        <!-- 底部确认按钮 -->
@@ -1459,6 +1430,35 @@
  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>