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 @@
<div
class="flex justify-between items-center border border-gray-200 rounded-lg p-3"
:class="{ 'bg-gray-50': showPicker }"
@tap="openPicker"
@tap="handleTap"
>
<span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
{{ displayValue || placeholder }}
......@@ -29,7 +29,7 @@
v-model="pickerValue"
:columns="ageColumns"
@confirm="onConfirm"
@cancel="showPicker = false"
@cancel="onCancel"
/>
</nut-popup>
</div>
......@@ -51,9 +51,12 @@
* placeholder="请选择年龄"
* />
*/
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, inject } from 'vue'
import IconFont from '@/components/IconFont.vue'
// 注入父组件提供的弹窗控制函数
const popupControl = inject('popupControl', null)
/**
* 组件属性
*/
......@@ -104,7 +107,17 @@ const emit = defineEmits([
* @event update:modelValue
* @param {number} value - 选中的年龄(数字)
*/
'update:modelValue'
'update:modelValue',
/**
* 弹窗打开事件
* @event open
*/
'open',
/**
* 弹窗关闭事件
* @event close
*/
'close'
])
/**
......@@ -149,9 +162,21 @@ watch(showPicker, (val) => {
})
/**
* 处理点击事件
*/
const handleTap = () => {
openPicker()
}
/**
* 打开选择器
*/
const openPicker = () => {
// 调用父组件提供的 open 函数
if (popupControl && popupControl.open) {
popupControl.open()
}
showPicker.value = true
}
......@@ -228,6 +253,23 @@ const onConfirm = ({ selectedValue, selectedOptions }) => {
console.error('[AgePicker] 选中值无效', { selectedValue, selectedOptions })
}
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showPicker.value = false
}
/**
* 取消选择
*/
const onCancel = () => {
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showPicker.value = false
}
</script>
......
......@@ -37,6 +37,7 @@
:placeholder="placeholder"
class="!p-0 !bg-transparent flex-1 !text-sm !text-gray-900"
:border="false"
:cursorSpacing="80"
/>
<span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">{{ currencySymbol }}</span>
</div>
......
......@@ -26,7 +26,7 @@
:max-date="maxDate"
:is-show-chinese="true"
@confirm="onConfirm"
@cancel="showDatePicker = false"
@cancel="onCancel"
>
</nut-date-picker>
</nut-popup>
......@@ -52,9 +52,12 @@
* @change="onBirthdayChange"
* />
*/
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, inject } from 'vue'
import IconFont from '@/components/IconFont.vue'
// 注入父组件提供的弹窗控制函数
const popupControl = inject('popupControl', null)
/**
* 组件属性
*/
......@@ -131,7 +134,17 @@ const emit = defineEmits([
* @event change
* @param {string} value - 选中的日期(格式:YYYY-MM-DD)
*/
'change'
'change',
/**
* 弹窗打开事件
* @event open
*/
'open',
/**
* 弹窗关闭事件
* @event close
*/
'close'
])
/**
......@@ -150,6 +163,11 @@ const currentDate = ref(new Date())
* @description 打开时将传入的 modelValue 转换为 Date 对象
*/
const openDatePicker = () => {
// 调用父组件提供的 open 函数
if (popupControl && popupControl.open) {
popupControl.open()
}
if (props.modelValue) {
// 兼容 iOS 的日期格式 (YYYY/MM/DD)
const dateStr = props.modelValue.replace(/-/g, '/')
......@@ -219,6 +237,24 @@ const onConfirm = ({ selectedValue }) => {
const formattedDate = `${year}-${month}-${day}`
emit('update:modelValue', formattedDate)
emit('change', formattedDate)
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showDatePicker.value = false
}
/**
* 取消选择
*/
const onCancel = () => {
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showDatePicker.value = false
}
</script>
......
......@@ -28,7 +28,7 @@
<nut-picker
:columns="pickerColumns"
@confirm="onConfirm"
@cancel="showPicker = false"
@cancel="onCancel"
/>
</nut-popup>
</div>
......@@ -50,9 +50,12 @@
* :options="['整付(0-75 岁)', '5 年(0-70 岁)']"
* />
*/
import { ref, computed } from 'vue'
import { ref, computed, inject } from 'vue'
import IconFont from '@/components/IconFont.vue'
// 注入父组件提供的弹窗控制函数
const popupControl = inject('popupControl', null)
/**
* 组件属性
*/
......@@ -113,7 +116,17 @@ const emit = defineEmits([
* @event update:modelValue
* @param {string} value - 选中的选项
*/
'update:modelValue'
'update:modelValue',
/**
* 弹窗打开事件
* @event open
*/
'open',
/**
* 弹窗关闭事件
* @event close
*/
'close'
])
/**
......@@ -125,6 +138,11 @@ const showPicker = ref(false)
* 打开选择器
*/
const openPicker = () => {
// 调用父组件提供的 open 函数
if (popupControl && popupControl.open) {
popupControl.open()
}
showPicker.value = true
}
......@@ -164,6 +182,24 @@ const onConfirm = ({ selectedOptions }) => {
if (value !== undefined) {
emit('update:modelValue', value)
}
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showPicker.value = false
}
/**
* 取消选择
*/
const onCancel = () => {
// 调用父组件提供的 close 函数
if (popupControl && popupControl.close) {
popupControl.close()
}
showPicker.value = false
}
</script>
......
<!--
* @Date: 2026-01-31 12:49:11
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-02-06 13:37:21
* @LastEditTime: 2026-02-06 21:40:34
* @FilePath: /manulife-weapp/src/components/PlanPopup/index.vue
* @Description: 文件描述
-->
......@@ -32,7 +32,10 @@
</div>
<!-- Footer Buttons -->
<div class="p-4 bg-white border-t border-gray-100 flex gap-3 flex-shrink-0 pb-safe">
<div
v-show="childPopupCount === 0"
class="p-4 bg-white border-t border-gray-100 flex gap-3 flex-shrink-0 pb-safe"
>
<nut-button
plain
type="primary"
......@@ -50,6 +53,7 @@
生成计划书
</nut-button>
</div>
</div>
</nut-popup>
</template>
......@@ -63,7 +67,7 @@
* @emits close - 关闭弹窗
* @emits submit - 提交表单
*/
import { defineProps, defineEmits } from 'vue';
import { defineProps, defineEmits, ref, watch, provide } from 'vue';
import IconFont from '@/components/IconFont.vue';
const props = defineProps({
......@@ -79,10 +83,41 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'close', 'submit']);
/**
* 子弹窗计数器
* @description 用于跟踪有多少个子弹窗打开,> 0 时隐藏底部按钮
*/
const childPopupCount = ref(0);
/**
* 处理子弹窗打开事件
*/
const handleChildOpen = () => {
childPopupCount.value++;
};
/**
* 处理子弹窗关闭事件
*/
const handleChildClose = () => {
if (childPopupCount.value > 0) {
childPopupCount.value--;
}
};
// Provide 子弹窗控制函数给所有后代组件
provide('popupControl', {
open: handleChildOpen,
close: handleChildClose
})
// 处理 visible 变化事件
const handleVisibleChange = (value) => {
emit('update:visible', value);
if (!value) {
// 重置子弹窗计数器
childPopupCount.value = 0;
emit('close');
}
}
......
......@@ -297,7 +297,7 @@ const fetchHotMaterials = async () => {
return {
id: item.meta_id,
title: item.name,
title: item.name || '未命名资料',
fileName: fileName,
downloadUrl: item.src,
fileSize: item.size,
......