fix(plan): 修复嵌套弹窗层级冲突问题
使用 Vue provide/inject 模式实现父子弹窗通信: - PlanPopup 提供 popupControl 给所有后代组件 - 子组件注入 popupControl 并在打开/关闭时调用 - 当子弹窗打开时,自动隐藏父弹窗的底部按钮 - 子弹窗关闭时,自动恢复父弹窗的底部按钮 影响文件: - PlanPopup: 提供 popupControl,添加 childPopupCount 计数器 - AgePicker/DatePicker/SelectPicker: 注入 popupControl - AmountInput: 添加弹窗控制支持 - index: 小调整 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
6 changed files
with
164 additions
and
14 deletions
| ... | @@ -10,7 +10,7 @@ | ... | @@ -10,7 +10,7 @@ |
| 10 | <div | 10 | <div |
| 11 | class="flex justify-between items-center border border-gray-200 rounded-lg p-3" | 11 | class="flex justify-between items-center border border-gray-200 rounded-lg p-3" |
| 12 | :class="{ 'bg-gray-50': showPicker }" | 12 | :class="{ 'bg-gray-50': showPicker }" |
| 13 | - @tap="openPicker" | 13 | + @tap="handleTap" |
| 14 | > | 14 | > |
| 15 | <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> | 15 | <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> |
| 16 | {{ displayValue || placeholder }} | 16 | {{ displayValue || placeholder }} |
| ... | @@ -29,7 +29,7 @@ | ... | @@ -29,7 +29,7 @@ |
| 29 | v-model="pickerValue" | 29 | v-model="pickerValue" |
| 30 | :columns="ageColumns" | 30 | :columns="ageColumns" |
| 31 | @confirm="onConfirm" | 31 | @confirm="onConfirm" |
| 32 | - @cancel="showPicker = false" | 32 | + @cancel="onCancel" |
| 33 | /> | 33 | /> |
| 34 | </nut-popup> | 34 | </nut-popup> |
| 35 | </div> | 35 | </div> |
| ... | @@ -51,9 +51,12 @@ | ... | @@ -51,9 +51,12 @@ |
| 51 | * placeholder="请选择年龄" | 51 | * placeholder="请选择年龄" |
| 52 | * /> | 52 | * /> |
| 53 | */ | 53 | */ |
| 54 | -import { ref, computed, watch } from 'vue' | 54 | +import { ref, computed, watch, inject } from 'vue' |
| 55 | import IconFont from '@/components/IconFont.vue' | 55 | import IconFont from '@/components/IconFont.vue' |
| 56 | 56 | ||
| 57 | +// 注入父组件提供的弹窗控制函数 | ||
| 58 | +const popupControl = inject('popupControl', null) | ||
| 59 | + | ||
| 57 | /** | 60 | /** |
| 58 | * 组件属性 | 61 | * 组件属性 |
| 59 | */ | 62 | */ |
| ... | @@ -104,7 +107,17 @@ const emit = defineEmits([ | ... | @@ -104,7 +107,17 @@ const emit = defineEmits([ |
| 104 | * @event update:modelValue | 107 | * @event update:modelValue |
| 105 | * @param {number} value - 选中的年龄(数字) | 108 | * @param {number} value - 选中的年龄(数字) |
| 106 | */ | 109 | */ |
| 107 | - 'update:modelValue' | 110 | + 'update:modelValue', |
| 111 | + /** | ||
| 112 | + * 弹窗打开事件 | ||
| 113 | + * @event open | ||
| 114 | + */ | ||
| 115 | + 'open', | ||
| 116 | + /** | ||
| 117 | + * 弹窗关闭事件 | ||
| 118 | + * @event close | ||
| 119 | + */ | ||
| 120 | + 'close' | ||
| 108 | ]) | 121 | ]) |
| 109 | 122 | ||
| 110 | /** | 123 | /** |
| ... | @@ -149,9 +162,21 @@ watch(showPicker, (val) => { | ... | @@ -149,9 +162,21 @@ watch(showPicker, (val) => { |
| 149 | }) | 162 | }) |
| 150 | 163 | ||
| 151 | /** | 164 | /** |
| 165 | + * 处理点击事件 | ||
| 166 | + */ | ||
| 167 | +const handleTap = () => { | ||
| 168 | + openPicker() | ||
| 169 | +} | ||
| 170 | + | ||
| 171 | +/** | ||
| 152 | * 打开选择器 | 172 | * 打开选择器 |
| 153 | */ | 173 | */ |
| 154 | const openPicker = () => { | 174 | const openPicker = () => { |
| 175 | + // 调用父组件提供的 open 函数 | ||
| 176 | + if (popupControl && popupControl.open) { | ||
| 177 | + popupControl.open() | ||
| 178 | + } | ||
| 179 | + | ||
| 155 | showPicker.value = true | 180 | showPicker.value = true |
| 156 | } | 181 | } |
| 157 | 182 | ||
| ... | @@ -228,6 +253,23 @@ const onConfirm = ({ selectedValue, selectedOptions }) => { | ... | @@ -228,6 +253,23 @@ const onConfirm = ({ selectedValue, selectedOptions }) => { |
| 228 | console.error('[AgePicker] 选中值无效', { selectedValue, selectedOptions }) | 253 | console.error('[AgePicker] 选中值无效', { selectedValue, selectedOptions }) |
| 229 | } | 254 | } |
| 230 | 255 | ||
| 256 | + // 调用父组件提供的 close 函数 | ||
| 257 | + if (popupControl && popupControl.close) { | ||
| 258 | + popupControl.close() | ||
| 259 | + } | ||
| 260 | + | ||
| 261 | + showPicker.value = false | ||
| 262 | +} | ||
| 263 | + | ||
| 264 | +/** | ||
| 265 | + * 取消选择 | ||
| 266 | + */ | ||
| 267 | +const onCancel = () => { | ||
| 268 | + // 调用父组件提供的 close 函数 | ||
| 269 | + if (popupControl && popupControl.close) { | ||
| 270 | + popupControl.close() | ||
| 271 | + } | ||
| 272 | + | ||
| 231 | showPicker.value = false | 273 | showPicker.value = false |
| 232 | } | 274 | } |
| 233 | </script> | 275 | </script> | ... | ... |
| ... | @@ -37,6 +37,7 @@ | ... | @@ -37,6 +37,7 @@ |
| 37 | :placeholder="placeholder" | 37 | :placeholder="placeholder" |
| 38 | class="!p-0 !bg-transparent flex-1 !text-sm !text-gray-900" | 38 | class="!p-0 !bg-transparent flex-1 !text-sm !text-gray-900" |
| 39 | :border="false" | 39 | :border="false" |
| 40 | + :cursorSpacing="80" | ||
| 40 | /> | 41 | /> |
| 41 | <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">{{ currencySymbol }}</span> | 42 | <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">{{ currencySymbol }}</span> |
| 42 | </div> | 43 | </div> | ... | ... |
| ... | @@ -26,7 +26,7 @@ | ... | @@ -26,7 +26,7 @@ |
| 26 | :max-date="maxDate" | 26 | :max-date="maxDate" |
| 27 | :is-show-chinese="true" | 27 | :is-show-chinese="true" |
| 28 | @confirm="onConfirm" | 28 | @confirm="onConfirm" |
| 29 | - @cancel="showDatePicker = false" | 29 | + @cancel="onCancel" |
| 30 | > | 30 | > |
| 31 | </nut-date-picker> | 31 | </nut-date-picker> |
| 32 | </nut-popup> | 32 | </nut-popup> |
| ... | @@ -52,9 +52,12 @@ | ... | @@ -52,9 +52,12 @@ |
| 52 | * @change="onBirthdayChange" | 52 | * @change="onBirthdayChange" |
| 53 | * /> | 53 | * /> |
| 54 | */ | 54 | */ |
| 55 | -import { ref, computed, watch } from 'vue' | 55 | +import { ref, computed, watch, inject } from 'vue' |
| 56 | import IconFont from '@/components/IconFont.vue' | 56 | import IconFont from '@/components/IconFont.vue' |
| 57 | 57 | ||
| 58 | +// 注入父组件提供的弹窗控制函数 | ||
| 59 | +const popupControl = inject('popupControl', null) | ||
| 60 | + | ||
| 58 | /** | 61 | /** |
| 59 | * 组件属性 | 62 | * 组件属性 |
| 60 | */ | 63 | */ |
| ... | @@ -131,7 +134,17 @@ const emit = defineEmits([ | ... | @@ -131,7 +134,17 @@ const emit = defineEmits([ |
| 131 | * @event change | 134 | * @event change |
| 132 | * @param {string} value - 选中的日期(格式:YYYY-MM-DD) | 135 | * @param {string} value - 选中的日期(格式:YYYY-MM-DD) |
| 133 | */ | 136 | */ |
| 134 | - 'change' | 137 | + 'change', |
| 138 | + /** | ||
| 139 | + * 弹窗打开事件 | ||
| 140 | + * @event open | ||
| 141 | + */ | ||
| 142 | + 'open', | ||
| 143 | + /** | ||
| 144 | + * 弹窗关闭事件 | ||
| 145 | + * @event close | ||
| 146 | + */ | ||
| 147 | + 'close' | ||
| 135 | ]) | 148 | ]) |
| 136 | 149 | ||
| 137 | /** | 150 | /** |
| ... | @@ -150,6 +163,11 @@ const currentDate = ref(new Date()) | ... | @@ -150,6 +163,11 @@ const currentDate = ref(new Date()) |
| 150 | * @description 打开时将传入的 modelValue 转换为 Date 对象 | 163 | * @description 打开时将传入的 modelValue 转换为 Date 对象 |
| 151 | */ | 164 | */ |
| 152 | const openDatePicker = () => { | 165 | const openDatePicker = () => { |
| 166 | + // 调用父组件提供的 open 函数 | ||
| 167 | + if (popupControl && popupControl.open) { | ||
| 168 | + popupControl.open() | ||
| 169 | + } | ||
| 170 | + | ||
| 153 | if (props.modelValue) { | 171 | if (props.modelValue) { |
| 154 | // 兼容 iOS 的日期格式 (YYYY/MM/DD) | 172 | // 兼容 iOS 的日期格式 (YYYY/MM/DD) |
| 155 | const dateStr = props.modelValue.replace(/-/g, '/') | 173 | const dateStr = props.modelValue.replace(/-/g, '/') |
| ... | @@ -219,6 +237,24 @@ const onConfirm = ({ selectedValue }) => { | ... | @@ -219,6 +237,24 @@ const onConfirm = ({ selectedValue }) => { |
| 219 | const formattedDate = `${year}-${month}-${day}` | 237 | const formattedDate = `${year}-${month}-${day}` |
| 220 | emit('update:modelValue', formattedDate) | 238 | emit('update:modelValue', formattedDate) |
| 221 | emit('change', formattedDate) | 239 | emit('change', formattedDate) |
| 240 | + | ||
| 241 | + // 调用父组件提供的 close 函数 | ||
| 242 | + if (popupControl && popupControl.close) { | ||
| 243 | + popupControl.close() | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + showDatePicker.value = false | ||
| 247 | +} | ||
| 248 | + | ||
| 249 | +/** | ||
| 250 | + * 取消选择 | ||
| 251 | + */ | ||
| 252 | +const onCancel = () => { | ||
| 253 | + // 调用父组件提供的 close 函数 | ||
| 254 | + if (popupControl && popupControl.close) { | ||
| 255 | + popupControl.close() | ||
| 256 | + } | ||
| 257 | + | ||
| 222 | showDatePicker.value = false | 258 | showDatePicker.value = false |
| 223 | } | 259 | } |
| 224 | </script> | 260 | </script> | ... | ... |
| ... | @@ -28,7 +28,7 @@ | ... | @@ -28,7 +28,7 @@ |
| 28 | <nut-picker | 28 | <nut-picker |
| 29 | :columns="pickerColumns" | 29 | :columns="pickerColumns" |
| 30 | @confirm="onConfirm" | 30 | @confirm="onConfirm" |
| 31 | - @cancel="showPicker = false" | 31 | + @cancel="onCancel" |
| 32 | /> | 32 | /> |
| 33 | </nut-popup> | 33 | </nut-popup> |
| 34 | </div> | 34 | </div> |
| ... | @@ -50,9 +50,12 @@ | ... | @@ -50,9 +50,12 @@ |
| 50 | * :options="['整付(0-75 岁)', '5 年(0-70 岁)']" | 50 | * :options="['整付(0-75 岁)', '5 年(0-70 岁)']" |
| 51 | * /> | 51 | * /> |
| 52 | */ | 52 | */ |
| 53 | -import { ref, computed } from 'vue' | 53 | +import { ref, computed, inject } from 'vue' |
| 54 | import IconFont from '@/components/IconFont.vue' | 54 | import IconFont from '@/components/IconFont.vue' |
| 55 | 55 | ||
| 56 | +// 注入父组件提供的弹窗控制函数 | ||
| 57 | +const popupControl = inject('popupControl', null) | ||
| 58 | + | ||
| 56 | /** | 59 | /** |
| 57 | * 组件属性 | 60 | * 组件属性 |
| 58 | */ | 61 | */ |
| ... | @@ -113,7 +116,17 @@ const emit = defineEmits([ | ... | @@ -113,7 +116,17 @@ const emit = defineEmits([ |
| 113 | * @event update:modelValue | 116 | * @event update:modelValue |
| 114 | * @param {string} value - 选中的选项 | 117 | * @param {string} value - 选中的选项 |
| 115 | */ | 118 | */ |
| 116 | - 'update:modelValue' | 119 | + 'update:modelValue', |
| 120 | + /** | ||
| 121 | + * 弹窗打开事件 | ||
| 122 | + * @event open | ||
| 123 | + */ | ||
| 124 | + 'open', | ||
| 125 | + /** | ||
| 126 | + * 弹窗关闭事件 | ||
| 127 | + * @event close | ||
| 128 | + */ | ||
| 129 | + 'close' | ||
| 117 | ]) | 130 | ]) |
| 118 | 131 | ||
| 119 | /** | 132 | /** |
| ... | @@ -125,6 +138,11 @@ const showPicker = ref(false) | ... | @@ -125,6 +138,11 @@ const showPicker = ref(false) |
| 125 | * 打开选择器 | 138 | * 打开选择器 |
| 126 | */ | 139 | */ |
| 127 | const openPicker = () => { | 140 | const openPicker = () => { |
| 141 | + // 调用父组件提供的 open 函数 | ||
| 142 | + if (popupControl && popupControl.open) { | ||
| 143 | + popupControl.open() | ||
| 144 | + } | ||
| 145 | + | ||
| 128 | showPicker.value = true | 146 | showPicker.value = true |
| 129 | } | 147 | } |
| 130 | 148 | ||
| ... | @@ -164,6 +182,24 @@ const onConfirm = ({ selectedOptions }) => { | ... | @@ -164,6 +182,24 @@ const onConfirm = ({ selectedOptions }) => { |
| 164 | if (value !== undefined) { | 182 | if (value !== undefined) { |
| 165 | emit('update:modelValue', value) | 183 | emit('update:modelValue', value) |
| 166 | } | 184 | } |
| 185 | + | ||
| 186 | + // 调用父组件提供的 close 函数 | ||
| 187 | + if (popupControl && popupControl.close) { | ||
| 188 | + popupControl.close() | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + showPicker.value = false | ||
| 192 | +} | ||
| 193 | + | ||
| 194 | +/** | ||
| 195 | + * 取消选择 | ||
| 196 | + */ | ||
| 197 | +const onCancel = () => { | ||
| 198 | + // 调用父组件提供的 close 函数 | ||
| 199 | + if (popupControl && popupControl.close) { | ||
| 200 | + popupControl.close() | ||
| 201 | + } | ||
| 202 | + | ||
| 167 | showPicker.value = false | 203 | showPicker.value = false |
| 168 | } | 204 | } |
| 169 | </script> | 205 | </script> | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2026-01-31 12:49:11 | 2 | * @Date: 2026-01-31 12:49:11 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-02-06 13:37:21 | 4 | + * @LastEditTime: 2026-02-06 21:40:34 |
| 5 | * @FilePath: /manulife-weapp/src/components/PlanPopup/index.vue | 5 | * @FilePath: /manulife-weapp/src/components/PlanPopup/index.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| ... | @@ -32,7 +32,10 @@ | ... | @@ -32,7 +32,10 @@ |
| 32 | </div> | 32 | </div> |
| 33 | 33 | ||
| 34 | <!-- Footer Buttons --> | 34 | <!-- Footer Buttons --> |
| 35 | - <div class="p-4 bg-white border-t border-gray-100 flex gap-3 flex-shrink-0 pb-safe"> | 35 | + <div |
| 36 | + v-show="childPopupCount === 0" | ||
| 37 | + class="p-4 bg-white border-t border-gray-100 flex gap-3 flex-shrink-0 pb-safe" | ||
| 38 | + > | ||
| 36 | <nut-button | 39 | <nut-button |
| 37 | plain | 40 | plain |
| 38 | type="primary" | 41 | type="primary" |
| ... | @@ -50,6 +53,7 @@ | ... | @@ -50,6 +53,7 @@ |
| 50 | 生成计划书 | 53 | 生成计划书 |
| 51 | </nut-button> | 54 | </nut-button> |
| 52 | </div> | 55 | </div> |
| 56 | + | ||
| 53 | </div> | 57 | </div> |
| 54 | </nut-popup> | 58 | </nut-popup> |
| 55 | </template> | 59 | </template> |
| ... | @@ -63,7 +67,7 @@ | ... | @@ -63,7 +67,7 @@ |
| 63 | * @emits close - 关闭弹窗 | 67 | * @emits close - 关闭弹窗 |
| 64 | * @emits submit - 提交表单 | 68 | * @emits submit - 提交表单 |
| 65 | */ | 69 | */ |
| 66 | -import { defineProps, defineEmits } from 'vue'; | 70 | +import { defineProps, defineEmits, ref, watch, provide } from 'vue'; |
| 67 | import IconFont from '@/components/IconFont.vue'; | 71 | import IconFont from '@/components/IconFont.vue'; |
| 68 | 72 | ||
| 69 | const props = defineProps({ | 73 | const props = defineProps({ |
| ... | @@ -79,10 +83,41 @@ const props = defineProps({ | ... | @@ -79,10 +83,41 @@ const props = defineProps({ |
| 79 | 83 | ||
| 80 | const emit = defineEmits(['update:visible', 'close', 'submit']); | 84 | const emit = defineEmits(['update:visible', 'close', 'submit']); |
| 81 | 85 | ||
| 86 | +/** | ||
| 87 | + * 子弹窗计数器 | ||
| 88 | + * @description 用于跟踪有多少个子弹窗打开,> 0 时隐藏底部按钮 | ||
| 89 | + */ | ||
| 90 | +const childPopupCount = ref(0); | ||
| 91 | + | ||
| 92 | + | ||
| 93 | +/** | ||
| 94 | + * 处理子弹窗打开事件 | ||
| 95 | + */ | ||
| 96 | +const handleChildOpen = () => { | ||
| 97 | + childPopupCount.value++; | ||
| 98 | +}; | ||
| 99 | + | ||
| 100 | +/** | ||
| 101 | + * 处理子弹窗关闭事件 | ||
| 102 | + */ | ||
| 103 | +const handleChildClose = () => { | ||
| 104 | + if (childPopupCount.value > 0) { | ||
| 105 | + childPopupCount.value--; | ||
| 106 | + } | ||
| 107 | +}; | ||
| 108 | + | ||
| 109 | +// Provide 子弹窗控制函数给所有后代组件 | ||
| 110 | +provide('popupControl', { | ||
| 111 | + open: handleChildOpen, | ||
| 112 | + close: handleChildClose | ||
| 113 | +}) | ||
| 114 | + | ||
| 82 | // 处理 visible 变化事件 | 115 | // 处理 visible 变化事件 |
| 83 | const handleVisibleChange = (value) => { | 116 | const handleVisibleChange = (value) => { |
| 84 | emit('update:visible', value); | 117 | emit('update:visible', value); |
| 85 | if (!value) { | 118 | if (!value) { |
| 119 | + // 重置子弹窗计数器 | ||
| 120 | + childPopupCount.value = 0; | ||
| 86 | emit('close'); | 121 | emit('close'); |
| 87 | } | 122 | } |
| 88 | } | 123 | } | ... | ... |
| ... | @@ -297,7 +297,7 @@ const fetchHotMaterials = async () => { | ... | @@ -297,7 +297,7 @@ const fetchHotMaterials = async () => { |
| 297 | 297 | ||
| 298 | return { | 298 | return { |
| 299 | id: item.meta_id, | 299 | id: item.meta_id, |
| 300 | - title: item.name, | 300 | + title: item.name || '未命名资料', |
| 301 | fileName: fileName, | 301 | fileName: fileName, |
| 302 | downloadUrl: item.src, | 302 | downloadUrl: item.src, |
| 303 | fileSize: item.size, | 303 | fileSize: item.size, | ... | ... |
-
Please register or login to post a comment