Преглед на файлове

fix: sync non-capital hot city selection

Developer преди 1 седмица
родител
ревизия
2f7c16fe6c
променени са 2 файла, в които са добавени 152 реда и са изтрити 11 реда
  1. 34 11
      components/pages/activityArea/index.vue
  2. 118 0
      tests/activity-area-hot-city.test.js

+ 34 - 11
components/pages/activityArea/index.vue

@@ -27,7 +27,13 @@
           <text>城市</text>
           <text>区县</text>
         </view>
-        <picker-view :value="pickerValue" @change="handlePickerChange" class="picker-view">
+        <picker-view
+          :value="pickerValue"
+          :immediate-change="true"
+          @change="handlePickerChange"
+          @pickstart="handlePickerStart"
+          class="picker-view"
+        >
           <!-- 省份列 -->
           <picker-view-column>
             <view
@@ -74,7 +80,7 @@
 </template>
 
 <script setup>
-import { ref, watch, computed, onMounted, getCurrentInstance } from 'vue';
+import { ref, watch, computed, onMounted, nextTick, getCurrentInstance } from 'vue';
 const { proxy } = getCurrentInstance()
 const emit = defineEmits(['update:show', 'confirm', 'reset']);
 
@@ -84,6 +90,8 @@ const provinces = ref([]);
 const cities = ref([]);
 const areas = ref([]);
 const pickerValue = ref([0, 0, 0]);
+const hotCityTarget = ref(null);
+let pickerUpdateVersion = 0;
 const selectedProvince = computed(() => provinces.value[pickerValue.value[0]] || {});
 const selectedCity = computed(() => cities.value[pickerValue.value[1]] || {});
 const selectedArea = computed(() => areas.value[pickerValue.value[2]] || {});
@@ -103,15 +111,26 @@ const handleConfirm = () => {
 
 // 重置区域选择并清除外层页面的区域筛选
 const handleReset = () => {
+  pickerUpdateVersion++;
+  hotCityTarget.value = null;
   pickerValue.value = [0, 0, 0];
   cities.value = provinces.value[0]?.children || [];
   areas.value = cities.value[0]?.children || [];
   emit('reset');
 };
 
+const handlePickerStart = () => {
+  pickerUpdateVersion++;
+  hotCityTarget.value = null;
+};
+
 // picker-view 滚动时触发
 const handlePickerChange = (e) => {
   const newPickerValue = e.detail.value;
+  if (hotCityTarget.value) {
+    const isTarget = hotCityTarget.value.every((value, index) => value === newPickerValue[index]);
+    if (!isTarget) return;
+  }
   const [provinceIndex, cityIndex, areaIndex] = newPickerValue;
   const oldProvinceIndex = pickerValue.value[0];
   const oldCityIndex = pickerValue.value[1];
@@ -165,16 +184,20 @@ const handleHotCityClick = (cityName) => {
 
   // 如果找到了对应的城市
   if (found) {
-    // 1. 更新城市列表,使其为选中省份的城市列表
+    const updateVersion = ++pickerUpdateVersion;
+    hotCityTarget.value = [provinceIndex, cityIndex, 0];
+
+    // 先切换省份和城市数据源,让 picker-view 完成列数据刷新。
     cities.value = provinces.value[provinceIndex].children;
-    
-    // 2. 更新区县列表,使其为选中城市的区县列表
-    //    使用可选链 ?. 防止选中城市没有区县数据(如海南省直辖县)
-    areas.value = cities.value[cityIndex]?.children || [];
-    
-    // 3. 更新 pickerValue,这会驱动 picker-view 滚动到指定位置
-    //    区县默认选择第一个 (索引为0)
-    pickerValue.value = [provinceIndex, cityIndex, 0];
+    areas.value = cities.value[0]?.children || [];
+    pickerValue.value = [provinceIndex, 0, 0];
+
+    // 城市列刷新后再定位目标城市,避免非省会城市被回退到索引 0。
+    nextTick(() => {
+      if (updateVersion !== pickerUpdateVersion) return;
+      areas.value = cities.value[cityIndex]?.children || [];
+      pickerValue.value = [provinceIndex, cityIndex, 0];
+    });
   } else {
     // 如果在数据中找不到该热门城市,可以给一个提示
     uni.showToast({

+ 118 - 0
tests/activity-area-hot-city.test.js

@@ -0,0 +1,118 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import path from 'node:path'
+import vm from 'node:vm'
+
+const areaFile = path.resolve('components/pages/activityArea/index.vue')
+const flushPromises = () => new Promise(resolve => setImmediate(resolve))
+
+const regionRows = [
+  { id: '320000', pid: '0', name: '江苏省' },
+  { id: '320100', pid: '320000', name: '南京市' },
+  { id: '320102', pid: '320100', name: '玄武区' },
+  { id: '320500', pid: '320000', name: '苏州市' },
+  { id: '320505', pid: '320500', name: '虎丘区' },
+  { id: '440000', pid: '0', name: '广东省' },
+  { id: '440100', pid: '440000', name: '广州市' },
+  { id: '440103', pid: '440100', name: '荔湾区' },
+  { id: '440300', pid: '440000', name: '深圳市' },
+  { id: '440303', pid: '440300', name: '罗湖区' }
+]
+
+async function mountActivityArea () {
+  const source = fs.readFileSync(areaFile, 'utf8')
+  const script = source.match(/<script setup[^>]*>([\s\S]*?)<\/script>/)[1]
+    .replace(/^\s*import\s+.*$/gm, '') +
+    '\nglobalThis.__area = { handleHotCityClick, handlePickerStart, handlePickerChange, handleReset, handleConfirm, pickerValue }'
+
+  const lifecycle = {}
+  const emitted = []
+  const nextTicks = []
+  const context = {
+    ref: value => ({ value }),
+    computed: getter => ({ get value () { return getter() } }),
+    watch: () => {},
+    nextTick: callback => nextTicks.push(callback),
+    getCurrentInstance: () => ({
+      proxy: {
+        $api: {
+          get: () => Promise.resolve({ data: { code: 0, data: regionRows } })
+        }
+      }
+    }),
+    defineEmits: () => (name, payload) => emitted.push({ name, payload }),
+    defineExpose: () => {},
+    onMounted: callback => { lifecycle.mounted = callback },
+    uni: { showToast: () => {} },
+    console
+  }
+
+  vm.runInNewContext(script, context)
+  lifecycle.mounted()
+  await flushPromises()
+
+  return {
+    area: context.__area,
+    emitted,
+    flushNextTicks: () => {
+      while (nextTicks.length) nextTicks.shift()()
+    }
+  }
+}
+
+test('点击非省会热门城市后保持目标城市而不是回退到省会', async () => {
+  const cases = [
+    { hotCity: '苏州', staleValue: [0, 0, 0], city: '苏州市', cityId: '320500', area: '虎丘区' },
+    { hotCity: '深圳', staleValue: [1, 0, 0], city: '深圳市', cityId: '440300', area: '罗湖区' }
+  ]
+
+  for (const expected of cases) {
+    const { area, emitted, flushNextTicks } = await mountActivityArea()
+
+    area.handleHotCityClick(expected.hotCity)
+    flushNextTicks()
+    // picker-view 的上一轮滚动动画可能在程序化定位后才派发 change。
+    area.handlePickerChange({ detail: { value: expected.staleValue } })
+    area.handleConfirm()
+
+    const result = emitted.at(-1).payload
+    assert.equal(result.city, expected.city)
+    assert.equal(result.cityId, expected.cityId)
+    assert.equal(result.area, expected.area)
+  }
+})
+
+test('连续点击热门城市时以最后一次选择为准', async () => {
+  const { area, emitted, flushNextTicks } = await mountActivityArea()
+
+  area.handleHotCityClick('苏州')
+  area.handleHotCityClick('深圳')
+  flushNextTicks()
+  area.handleConfirm()
+
+  const result = emitted.at(-1).payload
+  assert.equal(result.province, '广东省')
+  assert.equal(result.city, '深圳市')
+  assert.equal(result.cityId, '440300')
+  assert.equal(result.area, '罗湖区')
+})
+
+test('热门城市定位后仍允许手动滚动和重置', async () => {
+  const { area, emitted, flushNextTicks } = await mountActivityArea()
+
+  area.handleHotCityClick('苏州')
+  flushNextTicks()
+  area.handlePickerStart()
+  area.handlePickerChange({ detail: { value: [0, 0, 0] } })
+  area.handleConfirm()
+  assert.equal(emitted.at(-1).payload.city, '南京市')
+
+  area.handleHotCityClick('深圳')
+  flushNextTicks()
+  area.handleReset()
+  area.handleConfirm()
+  assert.equal(emitted.at(-1).payload.province, '江苏省')
+  assert.equal(emitted.at(-1).payload.city, '南京市')
+  assert.equal(emitted.at(-1).payload.area, '玄武区')
+})