|
@@ -0,0 +1,302 @@
|
|
|
|
+<template>
|
|
|
|
+ <view class="city-picker-container">
|
|
|
|
+ <view class="picker-content">
|
|
|
|
+ <view class="header">
|
|
|
|
+ <text class="title">城市地区</text>
|
|
|
|
+ </view>
|
|
|
|
+ <view class="hot-cities-section">
|
|
|
|
+ <view class="sub-title">热门城市</view>
|
|
|
|
+ <view class="hot-cities-grid">
|
|
|
|
+ <view
|
|
|
|
+ class="hot-city-item"
|
|
|
|
+ v-for="city in hotCities"
|
|
|
|
+ :key="city"
|
|
|
|
+ @click="handleHotCityClick(city)"
|
|
|
|
+ >
|
|
|
|
+ {{ city }}
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <view class="divider"></view>
|
|
|
|
+
|
|
|
|
+ <!-- 选择器 -->
|
|
|
|
+ <view class="picker-wrapper">
|
|
|
|
+ <view class="picker-header">
|
|
|
|
+ <text>省份</text>
|
|
|
|
+ <text>城市</text>
|
|
|
|
+ <text>区县</text>
|
|
|
|
+ </view>
|
|
|
|
+ <picker-view :value="pickerValue" @change="handlePickerChange" class="picker-view">
|
|
|
|
+ <!-- 省份列 -->
|
|
|
|
+ <picker-view-column>
|
|
|
|
+ <view
|
|
|
|
+ class="picker-item"
|
|
|
|
+ :class="{ 'selected-item': pickerValue[0] === index }"
|
|
|
|
+ v-for="(province, index) in provinces"
|
|
|
|
+ :key="province.name"
|
|
|
|
+ >
|
|
|
|
+ {{ province.name }}
|
|
|
|
+ </view>
|
|
|
|
+ </picker-view-column>
|
|
|
|
+ <!-- 城市列 -->
|
|
|
|
+ <picker-view-column>
|
|
|
|
+ <view
|
|
|
|
+ class="picker-item"
|
|
|
|
+ :class="{ 'selected-item': pickerValue[1] === index }"
|
|
|
|
+ v-for="(city, index) in cities"
|
|
|
|
+ :key="city.name"
|
|
|
|
+ >
|
|
|
|
+ {{ city.name }}
|
|
|
|
+ </view>
|
|
|
|
+ </picker-view-column>
|
|
|
|
+ <!-- 区县列 -->
|
|
|
|
+ <picker-view-column>
|
|
|
|
+ <view
|
|
|
|
+ class="picker-item"
|
|
|
|
+ :class="{ 'selected-item': pickerValue[2] === index }"
|
|
|
|
+ v-for="(area, index) in areas"
|
|
|
|
+ :key="area.name"
|
|
|
|
+ >
|
|
|
|
+ {{ area.name }}
|
|
|
|
+ </view>
|
|
|
|
+ </picker-view-column>
|
|
|
|
+ </picker-view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 确认按钮 -->
|
|
|
|
+ <view class="footer">
|
|
|
|
+ <button class="confirm-btn" @click="handleConfirm">确定</button>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script setup>
|
|
|
|
+import { ref, watch, computed } from 'vue';
|
|
|
|
+import { cityData, hotCities } from './city-data.js';
|
|
|
|
+
|
|
|
|
+const emit = defineEmits(['update:show', 'confirm']);
|
|
|
|
+
|
|
|
|
+// --- Data ---
|
|
|
|
+const provinces = ref(cityData);
|
|
|
|
+const cities = ref([]);
|
|
|
|
+const areas = ref([]);
|
|
|
|
+
|
|
|
|
+const pickerValue = ref([0, 0, 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]] || {});
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+// 确认选择
|
|
|
|
+const handleConfirm = () => {
|
|
|
|
+ const result = {
|
|
|
|
+ province: selectedProvince.value.name,
|
|
|
|
+ city: selectedCity.value.name,
|
|
|
|
+ area: selectedArea.value.name,
|
|
|
|
+ };
|
|
|
|
+ emit('confirm', result);
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+// picker-view 滚动时触发
|
|
|
|
+const handlePickerChange = (e) => {
|
|
|
|
+ const newPickerValue = e.detail.value;
|
|
|
|
+ const [provinceIndex, cityIndex, areaIndex] = newPickerValue;
|
|
|
|
+ const oldProvinceIndex = pickerValue.value[0];
|
|
|
|
+ const oldCityIndex = pickerValue.value[1];
|
|
|
|
+
|
|
|
|
+ // 如果省份改变了
|
|
|
|
+ if (provinceIndex !== oldProvinceIndex) {
|
|
|
|
+ // 更新城市列表和地区列表,并将它们的索引重置为0
|
|
|
|
+ cities.value = provinces.value[provinceIndex].children;
|
|
|
|
+ areas.value = cities.value[0]?.children || [];
|
|
|
|
+ pickerValue.value = [provinceIndex, 0, 0];
|
|
|
|
+ }
|
|
|
|
+ // 如果城市改变了
|
|
|
|
+ else if (cityIndex !== oldCityIndex) {
|
|
|
|
+ // 更新地区列表,并将地区索引重置为0
|
|
|
|
+ areas.value = cities.value[cityIndex]?.children || [];
|
|
|
|
+ pickerValue.value = [provinceIndex, cityIndex, 0];
|
|
|
|
+ }
|
|
|
|
+ // 如果只是地区改变
|
|
|
|
+ else {
|
|
|
|
+ pickerValue.value = newPickerValue;
|
|
|
|
+ }
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+// 点击热门城市
|
|
|
|
+// 新方法:遍历cityData找到对应的省市区并更新pickerValue
|
|
|
|
+const handleHotCityClick = (cityName) => {
|
|
|
|
+ let provinceIndex = -1;
|
|
|
|
+ let cityIndex = -1;
|
|
|
|
+ let found = false;
|
|
|
|
+
|
|
|
|
+ // 遍历省份
|
|
|
|
+ for (let i = 0; i < provinces.value.length; i++) {
|
|
|
|
+ const province = provinces.value[i];
|
|
|
|
+ // 遍历城市
|
|
|
|
+ if (province.children && province.children.length > 0) {
|
|
|
|
+ for (let j = 0; j < province.children.length; j++) {
|
|
|
|
+ const city = province.children[j];
|
|
|
|
+ // 检查城市名是否匹配(使用 startsWith 来兼容 "杭州" 和 "杭州市")
|
|
|
|
+ if (city.name.startsWith(cityName)) {
|
|
|
|
+ provinceIndex = i;
|
|
|
|
+ cityIndex = j;
|
|
|
|
+ found = true;
|
|
|
|
+ break; // 找到城市,跳出内层循环
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (found) {
|
|
|
|
+ break; // 找到城市,跳出外层循环
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 如果找到了对应的城市
|
|
|
|
+ if (found) {
|
|
|
|
+ // 1. 更新城市列表,使其为选中省份的城市列表
|
|
|
|
+ cities.value = provinces.value[provinceIndex].children;
|
|
|
|
+
|
|
|
|
+ // 2. 更新区县列表,使其为选中城市的区县列表
|
|
|
|
+ // 使用可选链 ?. 防止选中城市没有区县数据(如海南省直辖县)
|
|
|
|
+ areas.value = cities.value[cityIndex]?.children || [];
|
|
|
|
+
|
|
|
|
+ // 3. 更新 pickerValue,这会驱动 picker-view 滚动到指定位置
|
|
|
|
+ // 区县默认选择第一个 (索引为0)
|
|
|
|
+ pickerValue.value = [provinceIndex, cityIndex, 0];
|
|
|
|
+ } else {
|
|
|
|
+ // 如果在数据中找不到该热门城市,可以给一个提示
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: `未在数据源中找到 ${cityName}`,
|
|
|
|
+ icon: 'none'
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+// 初始化数据
|
|
|
|
+const initialize = () => {
|
|
|
|
+ const [pIndex, cIndex] = pickerValue.value;
|
|
|
|
+ cities.value = provinces.value[pIndex]?.children || [];
|
|
|
|
+ areas.value = cities.value[cIndex]?.children || [];
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+// 初始化
|
|
|
|
+initialize();
|
|
|
|
+
|
|
|
|
+defineExpose({
|
|
|
|
+ initialize
|
|
|
|
+})
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
+.city-picker-container {
|
|
|
|
+ .picker-content {
|
|
|
|
+ width: 100%;
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
+ border-radius: 0 0 24rpx 24rpx;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.header {
|
|
|
|
+ padding: 36rpx 0 0 28rpx;
|
|
|
|
+ position: relative;
|
|
|
|
+ .title {
|
|
|
|
+ font-family: PingFang-SC, PingFang-SC;
|
|
|
|
+ font-weight: bold;
|
|
|
|
+ font-size: 36rpx;
|
|
|
|
+ color: #252525;
|
|
|
|
+ line-height: 36rpx;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.hot-cities-section {
|
|
|
|
+ padding: 0 28rpx;
|
|
|
|
+ margin-top: 40rpx;
|
|
|
|
+ .sub-title {
|
|
|
|
+ font-family: PingFangSC, PingFang SC;
|
|
|
|
+ font-weight: 400;
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
+ color: #989998;
|
|
|
|
+ line-height: 24rpx;
|
|
|
|
+ }
|
|
|
|
+ .hot-cities-grid {
|
|
|
|
+ display: grid;
|
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
|
+ gap: 20rpx;
|
|
|
|
+ margin: 24rpx 0;
|
|
|
|
+ }
|
|
|
|
+ .hot-city-item {
|
|
|
|
+ border-radius: 6rpx;
|
|
|
|
+ border: 1rpx solid #E5E7EB;
|
|
|
|
+ padding: 13rpx 0;
|
|
|
|
+ font-family: PingFangSC, PingFang SC;
|
|
|
|
+ font-weight: 400;
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
+ color: #252525;
|
|
|
|
+ line-height: 33rpx;
|
|
|
|
+ text-align: center;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.divider {
|
|
|
|
+ height: 14rpx;
|
|
|
|
+ background: #F7F7F7;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.picker-wrapper {
|
|
|
|
+ .picker-header {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-around;
|
|
|
|
+ padding: 20rpx 0;
|
|
|
|
+ font-family: PingFang-SC, PingFang-SC;
|
|
|
|
+ font-weight: bold;
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
+ color: #252525;
|
|
|
|
+ line-height: 36rpx;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.picker-view {
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 380rpx;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.picker-item {
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
+ color: #A4A4A4;
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
+
|
|
|
|
+ &.selected-item {
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
+ color: #252525;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.footer {
|
|
|
|
+ padding: 64rpx 147rpx 40rpx;
|
|
|
|
+ .confirm-btn {
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 80rpx;
|
|
|
|
+ line-height: 88rpx;
|
|
|
|
+ background: #B7F358;
|
|
|
|
+ border-radius: 45rpx;
|
|
|
|
+ font-family: PingFang-SC, PingFang-SC;
|
|
|
|
+ font-weight: bold;
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
+ color: #252525;
|
|
|
|
+ line-height: 80rpx;
|
|
|
|
+ letter-spacing: 2rpx;
|
|
|
|
+ border: none;
|
|
|
|
+ &:after {
|
|
|
|
+ border: none;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+</style>
|