hookehuyr

fix(ui): 修复计划弹窗 NutUI 组件使用

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -21,7 +21,7 @@ declare module 'vue' { ...@@ -21,7 +21,7 @@ declare module 'vue' {
21 NavHeader: typeof import('./src/components/NavHeader.vue')['default'] 21 NavHeader: typeof import('./src/components/NavHeader.vue')['default']
22 NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] 22 NutAvatar: typeof import('@nutui/nutui-taro')['Avatar']
23 NutButton: typeof import('@nutui/nutui-taro')['Button'] 23 NutButton: typeof import('@nutui/nutui-taro')['Button']
24 - NutDatepicker: typeof import('@nutui/nutui-taro')['Datepicker'] 24 + NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker']
25 NutEmpty: typeof import('@nutui/nutui-taro')['Empty'] 25 NutEmpty: typeof import('@nutui/nutui-taro')['Empty']
26 NutInput: typeof import('@nutui/nutui-taro')['Input'] 26 NutInput: typeof import('@nutui/nutui-taro')['Input']
27 NutPicker: typeof import('@nutui/nutui-taro')['Picker'] 27 NutPicker: typeof import('@nutui/nutui-taro')['Picker']
......
...@@ -90,7 +90,8 @@ export default defineConfig(async (merge) => { ...@@ -90,7 +90,8 @@ export default defineConfig(async (merge) => {
90 webpackChain(chain) { 90 webpackChain(chain) {
91 91
92 chain.plugin('unplugin-vue-components').use(Components({ 92 chain.plugin('unplugin-vue-components').use(Components({
93 - resolvers: [NutUIResolver({taro: true})] 93 + resolvers: [NutUIResolver({taro: true})],
94 + exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/] // 只排除 node_modules 和 .git
94 })) 95 }))
95 96
96 chain.merge({ 97 chain.merge({
...@@ -137,7 +138,8 @@ export default defineConfig(async (merge) => { ...@@ -137,7 +138,8 @@ export default defineConfig(async (merge) => {
137 webpackChain(chain) { 138 webpackChain(chain) {
138 139
139 chain.plugin('unplugin-vue-components').use(Components({ 140 chain.plugin('unplugin-vue-components').use(Components({
140 - resolvers: [NutUIResolver({taro: true})] 141 + resolvers: [NutUIResolver({taro: true})],
142 + exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/] // 只排除 node_modules 和 .git
141 })) 143 }))
142 } 144 }
143 }, 145 },
......
...@@ -5,6 +5,27 @@ ...@@ -5,6 +5,27 @@
5 5
6 --- 6 ---
7 7
8 +## [2026-02-06] - 修复计划书弹窗样式
9 +
10 +### 修复
11 +- 修复计划书弹窗缺失头部和底部按钮的问题
12 +- 完善 `PlanPopup` 组件,添加标题、关闭按钮和提交按钮
13 +- 优化弹窗样式,适配不同设备屏幕
14 +
15 +### 技术实现
16 +- 重构 `src/components/PlanPopup/index.vue`
17 +- 添加 Header (标题+关闭) 和 Footer (取消+提交)
18 +- 使用 Flex 布局确保内容区域可滚动
19 +- 适配底部安全区 (`safe-area-inset-bottom`)
20 +
21 +---
22 +
23 +**详细信息**
24 +- **影响文件**: src/components/PlanPopup/index.vue
25 +- **技术栈**: Vue 3, Taro 4, NutUI
26 +- **测试状态**: ✅ 已修复
27 +- **备注**: 解决了用户反馈的弹窗样式缺失问题
28 +
8 ## [2026-02-05] - 优化搜索功能体验 29 ## [2026-02-05] - 优化搜索功能体验
9 30
10 ### 新增 31 ### 新增
......
...@@ -188,6 +188,6 @@ const onConfirm = ({ selectedOptions }) => { ...@@ -188,6 +188,6 @@ const onConfirm = ({ selectedOptions }) => {
188 } 188 }
189 </script> 189 </script>
190 190
191 -<style lang="less" scoped> 191 +<style lang="less">
192 /* 组件样式 */ 192 /* 组件样式 */
193 </style> 193 </style>
......
...@@ -248,6 +248,6 @@ const selectCurrency = (value) => { ...@@ -248,6 +248,6 @@ const selectCurrency = (value) => {
248 } 248 }
249 </script> 249 </script>
250 250
251 -<style lang="less" scoped> 251 +<style lang="less">
252 /* 组件样式 */ 252 /* 组件样式 */
253 </style> 253 </style>
......
...@@ -16,13 +16,13 @@ ...@@ -16,13 +16,13 @@
16 </div> 16 </div>
17 17
18 <!-- DatePicker 弹窗 --> 18 <!-- DatePicker 弹窗 -->
19 - <nut-datepicker 19 + <nut-date-picker
20 v-model="showDatePicker" 20 v-model="showDatePicker"
21 :min-date="minDate" 21 :min-date="minDate"
22 :max-date="maxDate" 22 :max-date="maxDate"
23 @confirm="onConfirm" 23 @confirm="onConfirm"
24 > 24 >
25 - </nut-datepicker> 25 + </nut-date-picker>
26 </div> 26 </div>
27 </template> 27 </template>
28 28
...@@ -120,7 +120,6 @@ const emit = defineEmits([ ...@@ -120,7 +120,6 @@ const emit = defineEmits([
120 120
121 /** 121 /**
122 * 控制 DatePicker 显示 122 * 控制 DatePicker 显示
123 - * @type {Ref<boolean>}
124 */ 123 */
125 const showDatePicker = ref(false) 124 const showDatePicker = ref(false)
126 125
...@@ -134,7 +133,6 @@ const openDatePicker = () => { ...@@ -134,7 +133,6 @@ const openDatePicker = () => {
134 /** 133 /**
135 * 计算最小可选日期(基于最大年龄) 134 * 计算最小可选日期(基于最大年龄)
136 * @description maxAge 岁对应的出生日期 135 * @description maxAge 岁对应的出生日期
137 - * @type {ComputedRef<Date>}
138 * @example 136 * @example
139 * // maxAge = 75, 当前日期 = 2026-02-06 137 * // maxAge = 75, 当前日期 = 2026-02-06
140 * // minDate() // 返回: 1951-02-06 138 * // minDate() // 返回: 1951-02-06
...@@ -148,7 +146,6 @@ const minDate = computed(() => { ...@@ -148,7 +146,6 @@ const minDate = computed(() => {
148 /** 146 /**
149 * 计算最大可选日期(基于最小年龄) 147 * 计算最大可选日期(基于最小年龄)
150 * @description minAge 岁对应的出生日期 148 * @description minAge 岁对应的出生日期
151 - * @type {ComputedRef<Date>}
152 * @example 149 * @example
153 * // minAge = 0, 当前日期 = 2026-02-06 150 * // minAge = 0, 当前日期 = 2026-02-06
154 * // maxDate() // 返回: 2026-02-06 151 * // maxDate() // 返回: 2026-02-06
...@@ -161,7 +158,6 @@ const maxDate = computed(() => { ...@@ -161,7 +158,6 @@ const maxDate = computed(() => {
161 158
162 /** 159 /**
163 * 显示的值 160 * 显示的值
164 - * @type {ComputedRef<string>}
165 */ 161 */
166 const displayValue = computed(() => { 162 const displayValue = computed(() => {
167 return props.modelValue || '' 163 return props.modelValue || ''
...@@ -190,6 +186,6 @@ const onConfirm = (values) => { ...@@ -190,6 +186,6 @@ const onConfirm = (values) => {
190 } 186 }
191 </script> 187 </script>
192 188
193 -<style lang="less" scoped> 189 +<style lang="less">
194 /* 组件样式 */ 190 /* 组件样式 */
195 </style> 191 </style>
......
...@@ -90,6 +90,6 @@ const selectedValue = computed({ ...@@ -90,6 +90,6 @@ const selectedValue = computed({
90 }) 90 })
91 </script> 91 </script>
92 92
93 -<style lang="less" scoped> 93 +<style lang="less">
94 /* 组件样式 */ 94 /* 组件样式 */
95 </style> 95 </style>
......
...@@ -106,7 +106,6 @@ const emit = defineEmits([ ...@@ -106,7 +106,6 @@ const emit = defineEmits([
106 106
107 /** 107 /**
108 * 控制 Picker 显示 108 * 控制 Picker 显示
109 - * @type {Ref<boolean>}
110 */ 109 */
111 const showPicker = ref(false) 110 const showPicker = ref(false)
112 111
...@@ -120,7 +119,6 @@ const openPicker = () => { ...@@ -120,7 +119,6 @@ const openPicker = () => {
120 /** 119 /**
121 * 转换为 Picker 格式 120 * 转换为 Picker 格式
122 * @description 将选项数组转换为 Picker 需要的格式 121 * @description 将选项数组转换为 Picker 需要的格式
123 - * @type {ComputedRef<Array<{text: string, value: string}>>}
124 * @example 122 * @example
125 * // options = ['整付(0-75 岁)', '5 年(0-70 岁)'] 123 * // options = ['整付(0-75 岁)', '5 年(0-70 岁)']
126 * // pickerColumns() // 返回: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }, ...] 124 * // pickerColumns() // 返回: [{ text: '整付(0-75 岁)', value: '整付(0-75 岁)' }, ...]
...@@ -134,7 +132,6 @@ const pickerColumns = computed(() => { ...@@ -134,7 +132,6 @@ const pickerColumns = computed(() => {
134 132
135 /** 133 /**
136 * 显示的值 134 * 显示的值
137 - * @type {ComputedRef<string>}
138 */ 135 */
139 const displayValue = computed(() => { 136 const displayValue = computed(() => {
140 return props.modelValue || '' 137 return props.modelValue || ''
...@@ -159,6 +156,6 @@ const onConfirm = ({ selectedOptions }) => { ...@@ -159,6 +156,6 @@ const onConfirm = ({ selectedOptions }) => {
159 } 156 }
160 </script> 157 </script>
161 158
162 -<style lang="less" scoped> 159 +<style lang="less">
163 /* 组件样式 */ 160 /* 组件样式 */
164 </style> 161 </style>
......
1 <template> 1 <template>
2 <!-- 使用 PlanPopup 容器组件 --> 2 <!-- 使用 PlanPopup 容器组件 -->
3 - <PlanPopup :title="templateConfig?.name || '计划书'" @close="close" @submit="submit"> 3 + <PlanPopup
4 + :visible="props.visible"
5 + :title="templateConfig?.name || '计划书'"
6 + @close="close"
7 + @submit="submit"
8 + >
4 <!-- 动态加载模版组件 --> 9 <!-- 动态加载模版组件 -->
5 <component 10 <component
6 :is="currentTemplateComponent" 11 :is="currentTemplateComponent"
7 v-model="formData" 12 v-model="formData"
8 - :config="templateConfig" 13 + :config="templateConfig?.config"
9 - v-if="currentTemplateComponent" 14 + v-if="currentTemplateComponent && templateConfig?.config"
10 /> 15 />
11 16
12 <!-- 错误提示 --> 17 <!-- 错误提示 -->
...@@ -96,7 +101,6 @@ const emit = defineEmits([ ...@@ -96,7 +101,6 @@ const emit = defineEmits([
96 /** 101 /**
97 * 当前模版配置 102 * 当前模版配置
98 * @description 根据 form_sn 从配置文件中查找,并合并后端 plan_config 103 * @description 根据 form_sn 从配置文件中查找,并合并后端 plan_config
99 - * @type {ComputedRef<Object|null>}
100 * 104 *
101 * @example 105 * @example
102 * // product.form_sn = 'life-insurance-wiop3e' 106 * // product.form_sn = 'life-insurance-wiop3e'
...@@ -132,10 +136,11 @@ const templateConfig = computed(() => { ...@@ -132,10 +136,11 @@ const templateConfig = computed(() => {
132 /** 136 /**
133 * 当前模版组件 137 * 当前模版组件
134 * @description 根据 component 名称动态加载对应的组件 138 * @description 根据 component 名称动态加载对应的组件
135 - * @type {ComputedRef<Component|null>}
136 */ 139 */
137 const currentTemplateComponent = computed(() => { 140 const currentTemplateComponent = computed(() => {
138 - if (!templateConfig.value) return null 141 + if (!templateConfig.value) {
142 + return null
143 + }
139 144
140 const componentMap = { 145 const componentMap = {
141 'LifeInsuranceTemplate': LifeInsuranceTemplate, 146 'LifeInsuranceTemplate': LifeInsuranceTemplate,
...@@ -149,7 +154,6 @@ const currentTemplateComponent = computed(() => { ...@@ -149,7 +154,6 @@ const currentTemplateComponent = computed(() => {
149 154
150 /** 155 /**
151 * 表单数据 156 * 表单数据
152 - * @type {Ref<Object>}
153 */ 157 */
154 const formData = ref({}) 158 const formData = ref({})
155 159
...@@ -168,12 +172,15 @@ watch( ...@@ -168,12 +172,15 @@ watch(
168 ) 172 )
169 173
170 /** 174 /**
171 - * 监听显示状态变化 175 + * 监听显示状态变化,弹窗打开时重置表单
172 */ 176 */
173 watch( 177 watch(
174 () => props.visible, 178 () => props.visible,
175 (newVal) => { 179 (newVal) => {
176 - emit('update:visible', newVal) 180 + if (newVal) {
181 + // 弹窗打开时重置表单
182 + formData.value = {}
183 + }
177 } 184 }
178 ) 185 )
179 186
...@@ -204,6 +211,6 @@ const submit = () => { ...@@ -204,6 +211,6 @@ const submit = () => {
204 } 211 }
205 </script> 212 </script>
206 213
207 -<style lang="less" scoped> 214 +<style lang="less">
208 /* 容器样式 */ 215 /* 容器样式 */
209 </style> 216 </style>
......
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-01-31 12:50:32 4 + * @LastEditTime: 2026-02-06 13:37:21
5 * @FilePath: /manulife-weapp/src/components/PlanPopup/index.vue 5 * @FilePath: /manulife-weapp/src/components/PlanPopup/index.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
8 <template> 8 <template>
9 - <nut-popup :visible="visible" position="bottom" round :style="{ height: '90%' }" 9 + <nut-popup
10 - @update:visible="emit('update:visible', $event)" :close-on-click-overlay="true"> 10 + :visible="visible"
11 - <div class="h-full flex flex-col bg-white overflow-hidden rounded-t-2xl"> 11 + position="bottom"
12 + round
13 + :style="{ height: '90%' }"
14 + :close-on-click-overlay="true"
15 + :safe-area-inset-bottom="true"
16 + @update:visible="handleVisibleChange"
17 + >
18 + <div class="h-full flex flex-col bg-gray-50 overflow-hidden rounded-t-2xl">
19 + <!-- Header -->
20 + <div class="flex justify-between items-center px-5 py-4 bg-white border-b border-gray-100 flex-shrink-0">
21 + <span class="text-lg font-bold text-gray-900">{{ title }}</span>
22 + <div class="p-2 -mr-2" @click="handleClose">
23 + <IconFont name="close" size="16" color="#9CA3AF" />
24 + </div>
25 + </div>
26 +
27 + <!-- Scrollable Content -->
28 + <div class="flex-1 overflow-y-auto p-4">
29 + <div class="bg-white rounded-xl p-5 shadow-sm">
12 <slot></slot> 30 <slot></slot>
13 </div> 31 </div>
32 + </div>
33 +
34 + <!-- Footer Buttons -->
35 + <div class="p-4 bg-white border-t border-gray-100 flex gap-3 flex-shrink-0 pb-safe">
36 + <nut-button
37 + plain
38 + type="primary"
39 + class="flex-1 !h-[88rpx] !rounded-[16rpx] !text-[30rpx] !border-blue-600"
40 + @click="handleClose"
41 + >
42 + 取消
43 + </nut-button>
44 + <nut-button
45 + type="primary"
46 + color="#2563EB"
47 + class="flex-1 !h-[88rpx] !rounded-[16rpx] !text-[30rpx]"
48 + @click="handleSubmit"
49 + >
50 + 生成计划书
51 + </nut-button>
52 + </div>
53 + </div>
14 </nut-popup> 54 </nut-popup>
15 </template> 55 </template>
16 56
...@@ -18,23 +58,55 @@ ...@@ -18,23 +58,55 @@
18 /** 58 /**
19 * @description 录入计划书弹窗容器组件 59 * @description 录入计划书弹窗容器组件
20 * @param {boolean} visible - 控制弹窗显示隐藏 60 * @param {boolean} visible - 控制弹窗显示隐藏
61 + * @param {string} title - 弹窗标题
21 * @emits update:visible - 更新 visible 状态 62 * @emits update:visible - 更新 visible 状态
63 + * @emits close - 关闭弹窗
64 + * @emits submit - 提交表单
22 */ 65 */
23 import { defineProps, defineEmits } from 'vue'; 66 import { defineProps, defineEmits } from 'vue';
67 +import IconFont from '@/components/IconFont.vue';
24 68
25 const props = defineProps({ 69 const props = defineProps({
26 visible: { 70 visible: {
27 type: Boolean, 71 type: Boolean,
28 default: false, 72 default: false,
29 }, 73 },
74 + title: {
75 + type: String,
76 + default: '计划书',
77 + },
30 }); 78 });
31 79
32 -const emit = defineEmits(['update:visible']); 80 +const emit = defineEmits(['update:visible', 'close', 'submit']);
81 +
82 +// 处理 visible 变化事件
83 +const handleVisibleChange = (value) => {
84 + emit('update:visible', value);
85 + if (!value) {
86 + emit('close');
87 + }
88 +}
89 +
90 +const handleClose = () => {
91 + emit('update:visible', false);
92 + emit('close');
93 +}
94 +
95 +const handleSubmit = () => {
96 + emit('submit');
97 +}
33 </script> 98 </script>
34 99
35 -<style lang="less" scoped> 100 +<style lang="less">
36 :deep(.nut-popup) { 101 :deep(.nut-popup) {
37 border-top-left-radius: 16px; 102 border-top-left-radius: 16px;
38 border-top-right-radius: 16px; 103 border-top-right-radius: 16px;
104 + background-color: #F9FAFB;
105 +}
106 +
107 +/* 适配底部安全区 */
108 +.pb-safe {
109 + padding-bottom: constant(safe-area-inset-bottom);
110 + padding-bottom: env(safe-area-inset-bottom);
39 } 111 }
40 </style> 112 </style>
......
1 <template> 1 <template>
2 - <div> 2 + <div v-if="config">
3 <!-- 性别 --> 3 <!-- 性别 -->
4 <PlanFieldRadio 4 <PlanFieldRadio
5 v-model="form.gender" 5 v-model="form.gender"
...@@ -53,6 +53,12 @@ ...@@ -53,6 +53,12 @@
53 </div> 53 </div>
54 </div> 54 </div>
55 </div> 55 </div>
56 +
57 + <!-- 配置缺失提示 -->
58 + <div v-else class="text-center text-gray-500 py-10">
59 + <p>⚠️ 模版配置未找到</p>
60 + <p class="text-sm mt-2">请检查产品配置或联系开发人员</p>
61 + </div>
56 </template> 62 </template>
57 63
58 <script setup> 64 <script setup>
...@@ -149,6 +155,6 @@ const onBirthdayChange = (birthday) => { ...@@ -149,6 +155,6 @@ const onBirthdayChange = (birthday) => {
149 } 155 }
150 </script> 156 </script>
151 157
152 -<style lang="less" scoped> 158 +<style lang="less">
153 /* 模版样式 */ 159 /* 模版样式 */
154 </style> 160 </style>
......
...@@ -161,7 +161,9 @@ ...@@ -161,7 +161,9 @@
161 161
162 <!-- Plan Form Container --> 162 <!-- Plan Form Container -->
163 <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 --> 163 <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 -->
164 + <!-- 仅当 selectedProduct 不为 null 时才渲染组件,避免 product prop required 警告 -->
164 <PlanFormContainer 165 <PlanFormContainer
166 + v-if="selectedProduct"
165 v-model:visible="showPlanPopup" 167 v-model:visible="showPlanPopup"
166 :product="selectedProduct" 168 :product="selectedProduct"
167 @close="showPlanPopup = false" 169 @close="showPlanPopup = false"
......