hookehuyr

refactor(components): 重构计划书方案组件,提取公共布局

- 新增 PlanPopup 容器组件,统一头部和底部按钮
- 使用 NutUI Button 组件替换原生 div 按钮
- 重构 SchemeA 和 SchemeB,使用 PlanPopup 容器
- 减少约 60 行重复代码
- 添加详细的 JSDoc 注释

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 +<template>
2 + <div class="flex flex-col h-full bg-gray-50">
3 + <!-- Header -->
4 + <div class="flex justify-between items-center px-5 py-5 bg-white rounded-t-xl">
5 + <span class="text-lg font-normal text-gray-900">{{ title }}</span>
6 + <IconFont name="close" size="16" color="#9CA3AF" @click="handleClose" />
7 + </div>
8 +
9 + <!-- Scrollable Content -->
10 + <div class="flex-1 overflow-y-auto p-4">
11 + <div class="bg-white rounded-xl p-5 shadow-sm">
12 + <slot />
13 + </div>
14 + </div>
15 +
16 + <!-- Footer Buttons -->
17 + <div class="p-4 pt-2 pb-8 flex justify-between gap-3 bg-gray-50">
18 + <nut-button
19 + plain
20 + type="primary"
21 + class="flex-1 !h-auto !py-2.5 !text-sm"
22 + @click="handleClose"
23 + >
24 + 取消
25 + </nut-button>
26 + <nut-button
27 + type="primary"
28 + class="flex-1 !h-auto !py-2.5 !text-sm"
29 + @click="handleSubmit"
30 + >
31 + 提交申请
32 + </nut-button>
33 + </div>
34 + </div>
35 +</template>
36 +
37 +<script setup>
38 +/**
39 + * @description 计划书弹窗容器组件
40 + * @description 提供统一的头部、底部按钮和布局结构
41 + *
42 + * @props {string} title - 弹窗标题
43 + *
44 + * @emits close - 关闭弹窗事件
45 + * @emits submit - 提交事件
46 + *
47 + * @example
48 + * <PlanPopup title="申请计划书" @close="handleClose" @submit="handleSubmit">
49 + * <!-- 具体的表单内容 -->
50 + * </PlanPopup>
51 + */
52 +import IconFont from '@/components/IconFont.vue'
53 +
54 +defineProps({
55 + /** 弹窗标题 */
56 + title: {
57 + type: String,
58 + default: '计划书'
59 + }
60 +})
61 +
62 +const emit = defineEmits(['close', 'submit'])
63 +
64 +/**
65 + * 处理关闭事件
66 + */
67 +const handleClose = () => {
68 + emit('close')
69 +}
70 +
71 +/**
72 + * 处理提交事件
73 + */
74 +const handleSubmit = () => {
75 + emit('submit')
76 +}
77 +</script>
78 +
79 +<style lang="less" scoped>
80 +/* 确保 NutUI 按钮样式正确 */
81 +:deep(.nut-button) {
82 + border-radius: 0.5rem /* 8px */;
83 + font-size: 1rem /* 16px */;
84 +}
85 +</style>
1 <template> 1 <template>
2 - <div class="flex flex-col h-full bg-gray-50"> 2 + <PlanPopup title="申请计划书" @close="close" @submit="submit">
3 - <div class="flex justify-between items-center px-5 py-5 bg-white rounded-t-xl"> 3 + <!-- 客户姓名 -->
4 - <span class="text-lg font-normal text-gray-900">申请计划书</span>
5 - <IconFont name="close" size="16" color="#9CA3AF" @click="close" />
6 - </div>
7 -
8 - <div class="flex-1 overflow-y-auto p-4">
9 - <div class="bg-white rounded-xl p-5 shadow-sm">
10 <div class="text-sm text-gray-600 mb-2">客户姓名</div> 4 <div class="text-sm text-gray-600 mb-2">客户姓名</div>
11 <div class="border border-gray-200 rounded-lg mb-4 overflow-hidden"> 5 <div class="border border-gray-200 rounded-lg mb-4 overflow-hidden">
12 <nut-input 6 <nut-input
...@@ -17,12 +11,14 @@ ...@@ -17,12 +11,14 @@
17 /> 11 />
18 </div> 12 </div>
19 13
14 + <!-- 性别 -->
20 <div class="text-sm text-gray-600 mb-2">性别</div> 15 <div class="text-sm text-gray-600 mb-2">性别</div>
21 <nut-radio-group v-model="form.gender" direction="horizontal" class="mb-4"> 16 <nut-radio-group v-model="form.gender" direction="horizontal" class="mb-4">
22 <nut-radio label="male" class="mr-8">男</nut-radio> 17 <nut-radio label="male" class="mr-8">男</nut-radio>
23 <nut-radio label="female">女</nut-radio> 18 <nut-radio label="female">女</nut-radio>
24 </nut-radio-group> 19 </nut-radio-group>
25 20
21 + <!-- 年龄 -->
26 <div class="text-sm text-gray-600 mb-2">年龄</div> 22 <div class="text-sm text-gray-600 mb-2">年龄</div>
27 <div class="border border-gray-200 rounded-lg mb-4 overflow-hidden"> 23 <div class="border border-gray-200 rounded-lg mb-4 overflow-hidden">
28 <nut-input 24 <nut-input
...@@ -34,6 +30,7 @@ ...@@ -34,6 +30,7 @@
34 /> 30 />
35 </div> 31 </div>
36 32
33 + <!-- 行业 -->
37 <div class="text-sm text-gray-600 mb-2">行业</div> 34 <div class="text-sm text-gray-600 mb-2">行业</div>
38 <div 35 <div
39 class="flex justify-between items-center border border-gray-200 rounded-lg p-3 mb-4 overflow-hidden" 36 class="flex justify-between items-center border border-gray-200 rounded-lg p-3 mb-4 overflow-hidden"
...@@ -45,6 +42,7 @@ ...@@ -45,6 +42,7 @@
45 <IconFont name="right" size="14" color="#9CA3AF" /> 42 <IconFont name="right" size="14" color="#9CA3AF" />
46 </div> 43 </div>
47 44
45 + <!-- 年收入区间 -->
48 <div class="text-sm text-gray-600 mb-2">年收入区间</div> 46 <div class="text-sm text-gray-600 mb-2">年收入区间</div>
49 <div class="border border-gray-200 rounded-lg mb-4 flex items-center overflow-hidden"> 47 <div class="border border-gray-200 rounded-lg mb-4 flex items-center overflow-hidden">
50 <nut-input 48 <nut-input
...@@ -57,6 +55,7 @@ ...@@ -57,6 +55,7 @@
57 <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">万元</span> 55 <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">万元</span>
58 </div> 56 </div>
59 57
58 + <!-- 家庭结构 -->
60 <div class="text-sm text-gray-600 mb-3">家庭结构(多选)</div> 59 <div class="text-sm text-gray-600 mb-3">家庭结构(多选)</div>
61 <div class="flex flex-wrap gap-3 mb-5"> 60 <div class="flex flex-wrap gap-3 mb-5">
62 <div 61 <div
...@@ -70,6 +69,7 @@ ...@@ -70,6 +69,7 @@
70 </div> 69 </div>
71 </div> 70 </div>
72 71
72 + <!-- 保险需求 -->
73 <div class="text-sm text-gray-600 mb-3">保险需求(多选)</div> 73 <div class="text-sm text-gray-600 mb-3">保险需求(多选)</div>
74 <div class="flex flex-wrap gap-3 mb-5"> 74 <div class="flex flex-wrap gap-3 mb-5">
75 <div 75 <div
...@@ -83,6 +83,7 @@ ...@@ -83,6 +83,7 @@
83 </div> 83 </div>
84 </div> 84 </div>
85 85
86 + <!-- 期望收益率 -->
86 <div class="text-sm text-gray-600 mb-2">期望收益率</div> 87 <div class="text-sm text-gray-600 mb-2">期望收益率</div>
87 <div class="border border-gray-200 rounded-lg mb-4 flex items-center overflow-hidden"> 88 <div class="border border-gray-200 rounded-lg mb-4 flex items-center overflow-hidden">
88 <nut-input 89 <nut-input
...@@ -94,23 +95,6 @@ ...@@ -94,23 +95,6 @@
94 /> 95 />
95 <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">%</span> 96 <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">%</span>
96 </div> 97 </div>
97 - </div>
98 - </div>
99 -
100 - <div class="p-4 pt-2 pb-8 flex justify-between gap-4 bg-gray-50">
101 - <div
102 - class="flex-1 py-3 text-center border border-blue-600 text-blue-600 rounded-lg text-base bg-white"
103 - @click="close"
104 - >
105 - 取消
106 - </div>
107 - <div
108 - class="flex-1 py-3 text-center bg-blue-600 text-white rounded-lg text-base"
109 - @click="submit"
110 - >
111 - 提交申请
112 - </div>
113 - </div>
114 98
115 <!-- Industry Picker --> 99 <!-- Industry Picker -->
116 <nut-popup position="bottom" v-model:visible="showIndustryPicker"> 100 <nut-popup position="bottom" v-model:visible="showIndustryPicker">
...@@ -121,19 +105,26 @@ ...@@ -121,19 +105,26 @@
121 @cancel="showIndustryPicker = false" 105 @cancel="showIndustryPicker = false"
122 /> 106 />
123 </nut-popup> 107 </nut-popup>
124 - </div> 108 + </PlanPopup>
125 </template> 109 </template>
126 110
127 <script setup> 111 <script setup>
128 /** 112 /**
129 * @description 录入计划书 - 方案A 内容组件 113 * @description 录入计划书 - 方案A 内容组件
114 + * @description 使用 PlanPopup 容器组件提供统一的布局和按钮
115 + *
130 * @emits close - 关闭弹窗事件 116 * @emits close - 关闭弹窗事件
131 * @emits submit - 提交事件,携带表单数据 117 * @emits submit - 提交事件,携带表单数据
132 */ 118 */
133 -import { ref, reactive } from 'vue'; 119 +import { ref, reactive } from 'vue'
120 +import PlanPopup from './PlanPopup.vue'
121 +import IconFont from '@/components/IconFont.vue'
134 122
135 -const emit = defineEmits(['close', 'submit']); 123 +const emit = defineEmits(['close', 'submit'])
136 124
125 +/**
126 + * 表单数据
127 + */
137 const form = reactive({ 128 const form = reactive({
138 name: '', 129 name: '',
139 gender: '', // 'male' | 'female' 130 gender: '', // 'male' | 'female'
...@@ -142,56 +133,83 @@ const form = reactive({ ...@@ -142,56 +133,83 @@ const form = reactive({
142 income: '', 133 income: '',
143 family: [], 134 family: [],
144 insurance: [], 135 insurance: [],
145 - returnRate: '', 136 + returnRate: ''
146 -}); 137 +})
147 138
148 -const showIndustryPicker = ref(false); 139 +/**
140 + * 控制行业选择器显示
141 + */
142 +const showIndustryPicker = ref(false)
149 143
144 +/**
145 + * 行业选项
146 + */
150 const industryColumns = [ 147 const industryColumns = [
151 { text: 'IT/互联网', value: 'it' }, 148 { text: 'IT/互联网', value: 'it' },
152 { text: '金融', value: 'finance' }, 149 { text: '金融', value: 'finance' },
153 { text: '教育', value: 'education' }, 150 { text: '教育', value: 'education' },
154 { text: '医疗', value: 'medical' }, 151 { text: '医疗', value: 'medical' },
155 - { text: '其他', value: 'other' }, 152 + { text: '其他', value: 'other' }
156 -]; 153 +]
157 154
155 +/**
156 + * 家庭结构选项
157 + */
158 const familyOptions = [ 158 const familyOptions = [
159 { label: '配偶', value: 'spouse' }, 159 { label: '配偶', value: 'spouse' },
160 { label: '子女', value: 'children' }, 160 { label: '子女', value: 'children' },
161 { label: '父母', value: 'parents' }, 161 { label: '父母', value: 'parents' },
162 - { label: '其他', value: 'others' }, 162 + { label: '其他', value: 'others' }
163 -]; 163 +]
164 164
165 +/**
166 + * 保险需求选项
167 + */
165 const insuranceOptions = [ 168 const insuranceOptions = [
166 { label: '人身保障', value: 'life' }, 169 { label: '人身保障', value: 'life' },
167 { label: '财富传承', value: 'wealth' }, 170 { label: '财富传承', value: 'wealth' },
168 { label: '子女教育', value: 'education' }, 171 { label: '子女教育', value: 'education' },
169 - { label: '养老规划', value: 'pension' }, 172 + { label: '养老规划', value: 'pension' }
170 -]; 173 +]
171 174
175 +/**
176 + * 切换多选项的选择状态
177 + * @param {string} field - 字段名
178 + * @param {string} value - 选项值
179 + */
172 const toggleSelection = (field, value) => { 180 const toggleSelection = (field, value) => {
173 - const index = form[field].indexOf(value); 181 + const index = form[field].indexOf(value)
174 if (index === -1) { 182 if (index === -1) {
175 - form[field].push(value); 183 + form[field].push(value)
176 } else { 184 } else {
177 - form[field].splice(index, 1); 185 + form[field].splice(index, 1)
178 } 186 }
179 -}; 187 +}
180 188
181 -const confirmIndustry = ({ selectedValue, selectedOptions }) => { 189 +/**
182 - form.industry = selectedOptions[0].text; 190 + * 确认行业选择
183 - showIndustryPicker.value = false; 191 + * @param {Object} params - 选择器返回参数
184 -}; 192 + * @param {Array} params.selectedOptions - 选中的选项
193 + */
194 +const confirmIndustry = ({ selectedOptions }) => {
195 + form.industry = selectedOptions[0].text
196 + showIndustryPicker.value = false
197 +}
185 198
199 +/**
200 + * 关闭弹窗
201 + */
186 const close = () => { 202 const close = () => {
187 - emit('close'); 203 + emit('close')
188 -}; 204 +}
189 205
206 +/**
207 + * 提交表单
208 + */
190 const submit = () => { 209 const submit = () => {
191 - // Validate form if needed 210 + console.log('SchemeA Submit:', form)
192 - console.log('Submit form:', form); 211 + emit('submit', form)
193 - emit('submit', form); 212 +}
194 -};
195 </script> 213 </script>
196 214
197 <style lang="less" scoped> 215 <style lang="less" scoped>
......
1 <template> 1 <template>
2 - <div class="flex flex-col h-full bg-gray-50"> 2 + <PlanPopup title="保险计划书申请" @close="close" @submit="submit">
3 - <!-- Header -->
4 - <div class="flex justify-between items-center px-5 py-5 bg-white rounded-t-xl">
5 - <span class="text-lg font-normal text-gray-900">保险计划书申请</span>
6 - <IconFont name="close" size="16" color="#9CA3AF" @click="close" />
7 - </div>
8 -
9 - <!-- Scrollable Content -->
10 - <div class="flex-1 overflow-y-auto p-4">
11 - <div class="bg-white rounded-xl p-5 shadow-sm">
12 <!-- 币种 --> 3 <!-- 币种 -->
13 <div class="flex justify-between items-start mb-5"> 4 <div class="flex justify-between items-start mb-5">
14 <span class="text-sm text-gray-600 mt-1.5">币种</span> 5 <span class="text-sm text-gray-600 mt-1.5">币种</span>
...@@ -82,32 +73,25 @@ ...@@ -82,32 +73,25 @@
82 /> 73 />
83 <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">美元</span> 74 <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">美元</span>
84 </div> 75 </div>
85 - </div> 76 + </PlanPopup>
86 - </div>
87 -
88 - <!-- Footer Buttons -->
89 - <div class="p-4 pt-2 pb-8 flex justify-between gap-4 bg-gray-50">
90 - <div class="flex-1 py-3 text-center border border-blue-600 text-blue-600 rounded-lg text-base bg-white"
91 - @click="close">
92 - 取消
93 - </div>
94 - <div class="flex-1 py-3 text-center bg-blue-600 text-white rounded-lg text-base" @click="submit">
95 - 提交申请
96 - </div>
97 - </div>
98 - </div>
99 </template> 77 </template>
100 78
101 <script setup> 79 <script setup>
102 /** 80 /**
103 * @description 录入计划书 - 方案B 内容组件 81 * @description 录入计划书 - 方案B 内容组件
82 + * @description 使用 PlanPopup 容器组件提供统一的布局和按钮
83 + *
104 * @emits close - 关闭弹窗事件 84 * @emits close - 关闭弹窗事件
105 * @emits submit - 提交事件,携带表单数据 85 * @emits submit - 提交事件,携带表单数据
106 */ 86 */
107 -import { reactive, defineEmits } from 'vue'; 87 +import { reactive } from 'vue'
88 +import PlanPopup from './PlanPopup.vue'
108 89
109 -const emit = defineEmits(['close', 'submit']); 90 +const emit = defineEmits(['close', 'submit'])
110 91
92 +/**
93 + * 表单数据
94 + */
111 const form = reactive({ 95 const form = reactive({
112 currency: '美元保单', 96 currency: '美元保单',
113 plan: '基础情景', 97 plan: '基础情景',
...@@ -115,22 +99,32 @@ const form = reactive({ ...@@ -115,22 +99,32 @@ const form = reactive({
115 age: '30', 99 age: '30',
116 insurancePeriod: '终身', 100 insurancePeriod: '终身',
117 paymentPeriod: '10年交', 101 paymentPeriod: '10年交',
118 - premium: '100000', 102 + premium: '100000'
119 -}); 103 +})
120 104
121 -const paymentPeriods = ['10年交', '3年交', '5年交', '2年交']; 105 +/**
106 + * 交费期间选项
107 + */
108 +const paymentPeriods = ['10年交', '3年交', '5年交', '2年交']
122 109
110 +/**
111 + * 关闭弹窗
112 + */
123 const close = () => { 113 const close = () => {
124 - emit('close'); 114 + emit('close')
125 -}; 115 +}
126 116
117 +/**
118 + * 提交表单
119 + */
127 const submit = () => { 120 const submit = () => {
128 - console.log('SchemeB Submit:', form); 121 + console.log('SchemeB Submit:', form)
129 - emit('submit', form); 122 + emit('submit', form)
130 -}; 123 +}
131 </script> 124 </script>
132 125
133 <style lang="less" scoped> 126 <style lang="less" scoped>
127 +/* Override NutUI input styles to match design */
134 :deep(.nut-input) { 128 :deep(.nut-input) {
135 padding: 0; 129 padding: 0;
136 background: transparent; 130 background: transparent;
......