hookehuyr

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>
...@@ -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,
......