|
|
@@ -1,59 +1,82 @@
|
|
|
<template>
|
|
|
- <!-- <view class="form-item"> -->
|
|
|
-<!-- <view class="item-label">
|
|
|
- <text class="label-text">{{ label }}</text>
|
|
|
- <text v-if="required" class="required">*</text>
|
|
|
- </view> -->
|
|
|
-
|
|
|
- <!-- 编辑模式:输入框 -->
|
|
|
- <uni-data-picker
|
|
|
- v-if="mode === 'edit'"
|
|
|
- v-model="innerValue"
|
|
|
- :localdata="localData"
|
|
|
- :popup-title="popupTitle"
|
|
|
- @change="onChange"
|
|
|
- >
|
|
|
- <u-input
|
|
|
- :value="getLocationLabel(innerValue)"
|
|
|
+ <!-- 编辑模式:输入框 -->
|
|
|
+ <view v-if="mode === 'edit'" class="location-picker-wrapper">
|
|
|
+ <view class="input-wrapper" @click="openPicker">
|
|
|
+ <input
|
|
|
+ class="location-input"
|
|
|
+ :value="displayLabel"
|
|
|
:placeholder="placeholder"
|
|
|
readonly
|
|
|
- suffix-icon="arrow-down"
|
|
|
- >
|
|
|
- <!-- 清除按钮 -->
|
|
|
- <template #suffix>
|
|
|
- <view
|
|
|
- v-if="innerValue"
|
|
|
- @click.stop="clearAddress"
|
|
|
- style="padding: 0 8rpx; display: flex; align-items: center;"
|
|
|
+ disabled
|
|
|
+ />
|
|
|
+ <view class="suffix-icons">
|
|
|
+ <view
|
|
|
+ v-if="innerValue && displayLabel"
|
|
|
+ @click.stop="clearAddress"
|
|
|
+ class="clear-btn"
|
|
|
+ >
|
|
|
+ <text class="clear-icon">×</text>
|
|
|
+ </view>
|
|
|
+ <text class="arrow-icon">></text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 自定义弹窗选择器 -->
|
|
|
+ <view v-if="showPicker" class="picker-modal" @click="closePicker">
|
|
|
+ <view class="picker-content" @click.stop>
|
|
|
+ <view class="picker-header">
|
|
|
+ <text class="picker-cancel" @click="closePicker">取消</text>
|
|
|
+ <text class="picker-title">{{ popupTitle }}</text>
|
|
|
+ <text class="picker-confirm" @click="confirmSelection">确定</text>
|
|
|
+ </view>
|
|
|
+ <view class="picker-body">
|
|
|
+ <picker-view
|
|
|
+ :value="pickerValue"
|
|
|
+ @change="onPickerChange"
|
|
|
+ class="picker-view"
|
|
|
>
|
|
|
- <uni-icons type="close" color="#999" size="20"></uni-icons>
|
|
|
- </view>
|
|
|
- </template>
|
|
|
- </u-input>
|
|
|
- </uni-data-picker>
|
|
|
-
|
|
|
- <!-- 展示模式:纯文本 -->
|
|
|
- <text v-else class="location-text">
|
|
|
- {{ getLocationLabel(innerValue) || "无" }}
|
|
|
- </text>
|
|
|
- <!-- </view> -->
|
|
|
+ <picker-view-column>
|
|
|
+ <view class="picker-item" v-for="(item, index) in provinceList" :key="index">
|
|
|
+ {{ item.text }}
|
|
|
+ </view>
|
|
|
+ </picker-view-column>
|
|
|
+ <picker-view-column>
|
|
|
+ <view class="picker-item" v-for="(item, index) in cityList" :key="index">
|
|
|
+ {{ item.text }}
|
|
|
+ </view>
|
|
|
+ </picker-view-column>
|
|
|
+ <picker-view-column>
|
|
|
+ <view class="picker-item" v-for="(item, index) in districtList" :key="index">
|
|
|
+ {{ item.text }}
|
|
|
+ </view>
|
|
|
+ </picker-view-column>
|
|
|
+ </picker-view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 展示模式:纯文本 -->
|
|
|
+ <text v-else class="location-text">
|
|
|
+ {{ displayLabel || "无" }}
|
|
|
+ </text>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, watch } from 'vue'
|
|
|
+import { ref, watch, computed } from 'vue'
|
|
|
import cityRows from '@/utils/data.json'
|
|
|
|
|
|
// Props definition
|
|
|
const props = defineProps({
|
|
|
- mode: { // 新增模式属性
|
|
|
+ mode: {
|
|
|
type: String,
|
|
|
default: "edit" // edit / view
|
|
|
},
|
|
|
- value: { // v-model
|
|
|
+ modelValue: {
|
|
|
type: [String, Number],
|
|
|
default: ""
|
|
|
},
|
|
|
- label: { // 左侧文字
|
|
|
+ label: {
|
|
|
type: String,
|
|
|
default: "所在地"
|
|
|
},
|
|
|
@@ -72,52 +95,188 @@ const props = defineProps({
|
|
|
})
|
|
|
|
|
|
// Emits definition
|
|
|
-const emit = defineEmits(['input', 'clear'])
|
|
|
+const emit = defineEmits(['update:modelValue', 'clear'])
|
|
|
|
|
|
// Reactive data
|
|
|
-const innerValue = ref(props.value)
|
|
|
-const localData = ref([]) // 省市区树数据
|
|
|
+const innerValue = ref(props.modelValue)
|
|
|
+const displayLabel = ref('')
|
|
|
+const showPicker = ref(false)
|
|
|
+const pickerValue = ref([0, 0, 0]) // picker-view 的索引值
|
|
|
+const tempSelection = ref({ province: null, city: null, district: null }) // 临时选择
|
|
|
+
|
|
|
+// 省市区数据
|
|
|
+const localData = ref([])
|
|
|
+const provinceList = ref([])
|
|
|
+const cityList = ref([])
|
|
|
+const districtList = ref([])
|
|
|
+
|
|
|
+// 计算属性:根据当前选择更新城市和区县列表
|
|
|
+const updateCityList = () => {
|
|
|
+ const provinceIndex = pickerValue.value[0]
|
|
|
+ if (provinceList.value[provinceIndex] && provinceList.value[provinceIndex].children) {
|
|
|
+ cityList.value = provinceList.value[provinceIndex].children
|
|
|
+ } else {
|
|
|
+ cityList.value = []
|
|
|
+ }
|
|
|
+ // 重置城市索引
|
|
|
+ if (pickerValue.value[1] >= cityList.value.length) {
|
|
|
+ pickerValue.value[1] = 0
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const updateDistrictList = () => {
|
|
|
+ const cityIndex = pickerValue.value[1]
|
|
|
+ if (cityList.value[cityIndex] && cityList.value[cityIndex].children) {
|
|
|
+ districtList.value = cityList.value[cityIndex].children
|
|
|
+ } else {
|
|
|
+ districtList.value = []
|
|
|
+ }
|
|
|
+ // 重置区县索引
|
|
|
+ if (pickerValue.value[2] >= districtList.value.length) {
|
|
|
+ pickerValue.value[2] = 0
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
// Watchers
|
|
|
-watch(() => props.value, (newVal) => {
|
|
|
+watch(() => props.modelValue, (newVal) => {
|
|
|
+ console.log('props.modelValue 变化:', newVal)
|
|
|
innerValue.value = newVal
|
|
|
+ const label = getLocationLabel(newVal)
|
|
|
+ displayLabel.value = label
|
|
|
+ console.log('更新后的 displayLabel:', label)
|
|
|
})
|
|
|
|
|
|
watch(innerValue, (newVal) => {
|
|
|
- emit("input", newVal)
|
|
|
+ console.log('innerValue 变化:', newVal)
|
|
|
+ emit("update:modelValue", newVal)
|
|
|
})
|
|
|
|
|
|
// Methods
|
|
|
-/** 点击选择后的回调 */
|
|
|
-const onChange = (e) => {
|
|
|
- const lastNode = e.detail.value[e.detail.value.length - 1]
|
|
|
- innerValue.value = lastNode.value // 只存最底层的 code
|
|
|
+/** 打开选择器 */
|
|
|
+const openPicker = () => {
|
|
|
+ console.log('打开地区选择器')
|
|
|
+ showPicker.value = true
|
|
|
+
|
|
|
+ // 如果有已选值,定位到对应位置
|
|
|
+ if (innerValue.value) {
|
|
|
+ locateSelection(innerValue.value)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 关闭选择器 */
|
|
|
+const closePicker = () => {
|
|
|
+ showPicker.value = false
|
|
|
+}
|
|
|
+
|
|
|
+/** picker-view 变化事件 */
|
|
|
+const onPickerChange = (e) => {
|
|
|
+ const val = e.detail.value
|
|
|
+ const oldValue = [...pickerValue.value]
|
|
|
+ pickerValue.value = val
|
|
|
+
|
|
|
+ // 如果省份改变,更新城市列表
|
|
|
+ if (oldValue[0] !== val[0]) {
|
|
|
+ updateCityList()
|
|
|
+ updateDistrictList()
|
|
|
+ }
|
|
|
+ // 如果城市改变,更新区县列表
|
|
|
+ else if (oldValue[1] !== val[1]) {
|
|
|
+ updateDistrictList()
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('picker 值变化:', val)
|
|
|
+}
|
|
|
+
|
|
|
+/** 确认选择 */
|
|
|
+const confirmSelection = () => {
|
|
|
+ const provinceIndex = pickerValue.value[0]
|
|
|
+ const cityIndex = pickerValue.value[1]
|
|
|
+ const districtIndex = pickerValue.value[2]
|
|
|
+
|
|
|
+ const province = provinceList.value[provinceIndex]
|
|
|
+ const city = cityList.value[cityIndex]
|
|
|
+ const district = districtList.value[districtIndex]
|
|
|
+
|
|
|
+ if (province && city && district) {
|
|
|
+ // 更新值为区县的 code
|
|
|
+ innerValue.value = district.value
|
|
|
+
|
|
|
+ // 构建显示文本
|
|
|
+ displayLabel.value = `${province.text} - ${city.text} - ${district.text}`
|
|
|
+
|
|
|
+ console.log('确认选择:', displayLabel.value, '值:', innerValue.value)
|
|
|
+
|
|
|
+ emit("update:modelValue", innerValue.value)
|
|
|
+ }
|
|
|
+
|
|
|
+ closePicker()
|
|
|
}
|
|
|
|
|
|
/** 清空地址 */
|
|
|
const clearAddress = () => {
|
|
|
+ console.log('清空地址')
|
|
|
innerValue.value = ""
|
|
|
+ displayLabel.value = ""
|
|
|
+ pickerValue.value = [0, 0, 0]
|
|
|
+ emit("update:modelValue", "")
|
|
|
emit("clear")
|
|
|
}
|
|
|
|
|
|
+/** 定位到已选择的值 */
|
|
|
+const locateSelection = (value) => {
|
|
|
+ if (!value) return
|
|
|
+
|
|
|
+ // 在数据中查找对应的省市区
|
|
|
+ for (let i = 0; i < provinceList.value.length; i++) {
|
|
|
+ const province = provinceList.value[i]
|
|
|
+ if (province.children) {
|
|
|
+ for (let j = 0; j < province.children.length; j++) {
|
|
|
+ const city = province.children[j]
|
|
|
+ if (city.children) {
|
|
|
+ for (let k = 0; k < city.children.length; k++) {
|
|
|
+ const district = city.children[k]
|
|
|
+ if (district.value == value) {
|
|
|
+ pickerValue.value = [i, j, k]
|
|
|
+ updateCityList()
|
|
|
+ updateDistrictList()
|
|
|
+ console.log('定位到:', i, j, k)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/** 回显文字(递归找路径) */
|
|
|
const getLocationLabel = (value) => {
|
|
|
+ console.log('getLocationLabel 被调用, value:', value)
|
|
|
if (!value) return ""
|
|
|
+
|
|
|
let label = ""
|
|
|
- const traverse = (nodes) => {
|
|
|
+
|
|
|
+ const traverse = (nodes, currentPath = []) => {
|
|
|
for (const node of nodes) {
|
|
|
- if (node.value === value) {
|
|
|
- label = node.text
|
|
|
+ const newPath = [...currentPath, node.text]
|
|
|
+
|
|
|
+ if (node.value == value) {
|
|
|
+ label = newPath.join(' - ')
|
|
|
+ console.log('找到匹配节点:', node, '路径:', label)
|
|
|
return true
|
|
|
}
|
|
|
- if (node.children && traverse(node.children)) {
|
|
|
- label = node.text + " - " + label
|
|
|
- return true
|
|
|
+
|
|
|
+ if (node.children && node.children.length > 0) {
|
|
|
+ if (traverse(node.children, newPath)) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
return false
|
|
|
}
|
|
|
+
|
|
|
traverse(localData.value)
|
|
|
+ console.log('最终返回的 label:', label)
|
|
|
return label
|
|
|
}
|
|
|
|
|
|
@@ -167,32 +326,139 @@ const handleTree = (data, parent_code = null) => {
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
-// Initialize data (replaces created lifecycle)
|
|
|
+// Initialize data
|
|
|
localData.value = get_city_tree()
|
|
|
+provinceList.value = localData.value
|
|
|
+console.log('初始化 localData, 数据长度:', localData.value.length)
|
|
|
+
|
|
|
+// 初始化城市和区县列表
|
|
|
+if (provinceList.value.length > 0) {
|
|
|
+ updateCityList()
|
|
|
+ updateDistrictList()
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化显示标签
|
|
|
+if (props.modelValue) {
|
|
|
+ displayLabel.value = getLocationLabel(props.modelValue)
|
|
|
+ console.log('初始化 displayLabel:', displayLabel.value)
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.form-item {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- /* margin-bottom: 20rpx; */
|
|
|
+.location-picker-wrapper {
|
|
|
+ width: 100%;
|
|
|
}
|
|
|
-.item-label {
|
|
|
+
|
|
|
+.input-wrapper {
|
|
|
+ position: relative;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- margin-bottom: 10rpx;
|
|
|
+ width: 100%;
|
|
|
+ height: 44rpx;
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
+ padding: 12rpx 0;
|
|
|
}
|
|
|
-.label-text {
|
|
|
+
|
|
|
+.location-input {
|
|
|
+ flex: 1;
|
|
|
font-size: 28rpx;
|
|
|
color: #333;
|
|
|
+ height: 44rpx;
|
|
|
+ line-height: 44rpx;
|
|
|
}
|
|
|
-.required {
|
|
|
- color: red;
|
|
|
- margin-left: 5rpx;
|
|
|
+
|
|
|
+.location-input:disabled {
|
|
|
+ background-color: transparent;
|
|
|
+ color: #333;
|
|
|
}
|
|
|
+
|
|
|
+.suffix-icons {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.clear-btn {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 32rpx;
|
|
|
+ height: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.clear-icon {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #999;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.arrow-icon {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
.location-text {
|
|
|
- /* font-size: 28rpx;
|
|
|
- color: #333; */
|
|
|
- /* padding: 16rpx 0; */
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+/* 弹窗样式 */
|
|
|
+.picker-modal {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ z-index: 9999;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-content {
|
|
|
+ width: 100%;
|
|
|
+ background-color: #fff;
|
|
|
+ border-radius: 24rpx 24rpx 0 0;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 24rpx 32rpx;
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-cancel,
|
|
|
+.picker-confirm {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #007aff;
|
|
|
+ padding: 8rpx 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-body {
|
|
|
+ height: 500rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-view {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+ height: 80rpx;
|
|
|
+ line-height: 80rpx;
|
|
|
}
|
|
|
</style>
|