hookehuyr

feat(JoinFamily): 添加家庭选择弹窗功能

实现家庭选择弹窗组件,包含搜索、列表展示和确认功能
将@click事件统一改为@tap以优化移动端体验
添加过渡动画和样式优化
...@@ -17,6 +17,7 @@ declare module 'vue' { ...@@ -17,6 +17,7 @@ declare module 'vue' {
17 NutInput: typeof import('@nutui/nutui-taro')['Input'] 17 NutInput: typeof import('@nutui/nutui-taro')['Input']
18 NutPicker: typeof import('@nutui/nutui-taro')['Picker'] 18 NutPicker: typeof import('@nutui/nutui-taro')['Picker']
19 NutPopup: typeof import('@nutui/nutui-taro')['Popup'] 19 NutPopup: typeof import('@nutui/nutui-taro')['Popup']
20 + NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar']
20 NutToast: typeof import('@nutui/nutui-taro')['Toast'] 21 NutToast: typeof import('@nutui/nutui-taro')['Toast']
21 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] 22 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
22 PointsCollector: typeof import('./src/components/PointsCollector.vue')['default'] 23 PointsCollector: typeof import('./src/components/PointsCollector.vue')['default']
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
15 display: flex; 15 display: flex;
16 align-items: center; 16 align-items: center;
17 justify-content: center; 17 justify-content: center;
18 + transition: border-color 0.2s;
18 } 19 }
19 20
20 .motto-input { 21 .motto-input {
...@@ -32,7 +33,48 @@ ...@@ -32,7 +33,48 @@
32 } 33 }
33 34
34 .identity-title { 35 .identity-title {
35 - font-size: 1.25rem;
36 - font-weight: 700;
37 - margin-bottom: 1rem;
38 -}
...\ No newline at end of file ...\ No newline at end of file
36 + font-size: 32rpx;
37 + font-weight: 600;
38 + color: #374151;
39 + margin-bottom: 24rpx;
40 +}
41 +
42 +// 家庭选择弹窗样式
43 +.family-selector-container {
44 + display: flex;
45 + flex-direction: column;
46 + height: 100%;
47 + background: white;
48 +}
49 +
50 +.family-item {
51 + cursor: pointer;
52 + transition: all 0.2s ease;
53 +
54 + &:hover {
55 + transform: translateY(-2rpx);
56 + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
57 + }
58 +
59 + &:active {
60 + transform: translateY(0);
61 + }
62 +}
63 +
64 +// 文本截断样式
65 +.line-clamp-2 {
66 + display: -webkit-box;
67 + -webkit-line-clamp: 2;
68 + -webkit-box-orient: vertical;
69 + overflow: hidden;
70 + text-overflow: ellipsis;
71 +}
72 +
73 +// 间距样式
74 +.space-y-3 > * + * {
75 + margin-top: 24rpx;
76 +}
77 +
78 +.space-x-3 > * + * {
79 + margin-left: 24rpx;
80 +}
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
45 <view 45 <view
46 v-for="role in familyRoles" 46 v-for="role in familyRoles"
47 :key="role.id" 47 :key="role.id"
48 - @click="selectedRole = role.id" 48 + @tap="selectedRole = role.id"
49 :class="[ 49 :class="[
50 'w-[calc(49%-4rpx)] py-3 rounded-lg border text-center flex flex-col items-center gap-1', 50 'w-[calc(49%-4rpx)] py-3 rounded-lg border text-center flex flex-col items-center gap-1',
51 selectedRole === role.id 51 selectedRole === role.id
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
60 </view> 60 </view>
61 <!-- Submit Button --> 61 <!-- Submit Button -->
62 <view 62 <view
63 - @click="handleJoinFamily" 63 + @tap="handleJoinFamily"
64 :disabled="!isComplete" 64 :disabled="!isComplete"
65 :class="[ 65 :class="[
66 'w-full py-3 text-white text-lg font-medium rounded-lg mt-auto text-center', 66 'w-full py-3 text-white text-lg font-medium rounded-lg mt-auto text-center',
...@@ -70,13 +70,104 @@ ...@@ -70,13 +70,104 @@
70 加入家庭 70 加入家庭
71 </view> 71 </view>
72 </view> 72 </view>
73 +
74 + <!-- 家庭选择弹窗 -->
75 + <nut-popup
76 + v-model:visible="showFamilySelector"
77 + position="bottom"
78 + :style="{ height: '80vh' }"
79 + round
80 + closeable
81 + @close="closeFamilySelector"
82 + >
83 + <view class="family-selector-container">
84 + <!-- 标题 -->
85 + <view class="text-lg font-bold text-center py-4 border-b border-gray-100">
86 + 选择要加入的家庭
87 + </view>
88 +
89 + <!-- 搜索框 -->
90 + <view class="p-4">
91 + <nut-searchbar
92 + v-model="searchKeyword"
93 + placeholder="搜索家庭名称"
94 + @search="handleSearch"
95 + @clear="handleClearSearch"
96 + />
97 + </view>
98 +
99 + <!-- 家庭列表 -->
100 + <view class="flex-1 px-4 pb-4">
101 + <view
102 + ref="familyListContainer"
103 + class="space-y-3 overflow-y-auto"
104 + :style="{ maxHeight: familyListHeight + 'rpx' }"
105 + >
106 + <view
107 + v-for="family in filteredFamilies"
108 + :key="family.id"
109 + @tap="selectFamily(family.id)"
110 + :class="[
111 + 'family-item p-4 border rounded-lg flex items-center space-x-3',
112 + selectedFamilyId === family.id
113 + ? 'border-blue-500 bg-blue-50'
114 + : 'border-gray-200 bg-white'
115 + ]"
116 + >
117 + <!-- 家庭头像 -->
118 + <view class="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
119 + <image
120 + v-if="family.avatar"
121 + :src="family.avatar"
122 + class="w-full h-full object-cover"
123 + />
124 + <view v-else class="text-gray-500 text-xs">头像</view>
125 + </view>
126 +
127 + <!-- 家庭信息 -->
128 + <view class="flex-1">
129 + <view class="font-medium text-gray-900 mb-1">{{ family.name }}</view>
130 + <view class="text-sm text-gray-600 line-clamp-2">{{ family.description }}</view>
131 + </view>
132 +
133 + <!-- 选中状态 -->
134 + <view v-if="selectedFamilyId === family.id" class="text-blue-500">
135 + <Check size="20" />
136 + </view>
137 + </view>
138 + </view>
139 + </view>
140 +
141 + <!-- 底部按钮 -->
142 + <view class="flex gap-3 p-4 border-t border-gray-100">
143 + <nut-button
144 + @click="closeFamilySelector"
145 + class="flex-1"
146 + type="default"
147 + size="large"
148 + plain
149 + >
150 + 关闭
151 + </nut-button>
152 + <nut-button
153 + @click="confirmJoinFamily"
154 + class="flex-1"
155 + :color="selectedFamilyId ? '#3b82f6' : 'gray'"
156 + :disabled="!selectedFamilyId"
157 + size="large"
158 + >
159 + 确认
160 + </nut-button>
161 + </view>
162 + </view>
163 + </nut-popup>
73 </view> 164 </view>
74 </template> 165 </template>
75 166
76 <script setup> 167 <script setup>
77 -import { ref, computed, nextTick } from 'vue'; 168 +import { ref, computed, nextTick, onMounted, watch } from 'vue';
78 import Taro from '@tarojs/taro'; 169 import Taro from '@tarojs/taro';
79 -import { My } from '@nutui/icons-vue-taro'; 170 +import { My, Check } from '@nutui/icons-vue-taro';
80 import AppHeader from '../../components/AppHeader.vue'; 171 import AppHeader from '../../components/AppHeader.vue';
81 172
82 const mottoChars = ref(['', '', '', '']); 173 const mottoChars = ref(['', '', '', '']);
...@@ -84,6 +175,14 @@ const selectedRole = ref(''); ...@@ -84,6 +175,14 @@ const selectedRole = ref('');
84 const inputRefs = ref([]); 175 const inputRefs = ref([]);
85 const focusedIndex = ref(-1); 176 const focusedIndex = ref(-1);
86 177
178 +// 弹窗相关数据
179 +const showFamilySelector = ref(false);
180 +const searchKeyword = ref('');
181 +const selectedFamilyId = ref('');
182 +const mockFamilies = ref([]);
183 +const familyListContainer = ref(null);
184 +const familyListHeight = ref(400); // 默认高度
185 +
87 const handleInputChange = (index, value) => { 186 const handleInputChange = (index, value) => {
88 // 允许输入多个字符,但只保留第一个有效字符(汉字、数字、大小写字母),兼容输入法 187 // 允许输入多个字符,但只保留第一个有效字符(汉字、数字、大小写字母),兼容输入法
89 if (value) { 188 if (value) {
...@@ -141,15 +240,186 @@ const familyRoles = [ ...@@ -141,15 +240,186 @@ const familyRoles = [
141 { id: 'maternal-granddaughter', label: '外孙女' } 240 { id: 'maternal-granddaughter', label: '外孙女' }
142 ]; 241 ];
143 242
243 +// Mock家庭数据
244 +const generateMockFamilies = () => {
245 + return [
246 + {
247 + id: '1',
248 + name: '幸福之家',
249 + description: '我们是一个温馨和睦的大家庭,每天一起运动健身,分享生活的美好时光。',
250 + avatar: 'https://placehold.co/100x100/e2f3ff/0369a1?text=幸福&font=roboto'
251 + },
252 + {
253 + id: '2',
254 + name: '健康家园',
255 + description: '注重健康生活,坚持每日步行锻炼,用积分兑换健康好礼。',
256 + avatar: 'https://placehold.co/100x100/f0f9ff/0284c7?text=健康&font=roboto'
257 + },
258 + {
259 + id: '3',
260 + name: '快乐一家人',
261 + description: '快乐是我们家的主旋律,一起运动,一起成长,一起享受生活。',
262 + avatar: 'https://placehold.co/100x100/fef3c7/d97706?text=快乐&font=roboto'
263 + },
264 + {
265 + id: '4',
266 + name: '幸福一家人',
267 + description: '快乐是我们家的主旋律,一起运动,一起成长,一起享受生活。',
268 + avatar: 'https://placehold.co/100x100/fef3c7/d97706?text=快乐&font=roboto'
269 + },
270 + {
271 + id: '5',
272 + name: '幸福一家人',
273 + description: '快乐是我们家的主旋律,一起运动,一起成长,一起享受生活。',
274 + avatar: 'https://placehold.co/100x100/fef3c7/d97706?text=快乐&font=roboto'
275 + },
276 + ]
277 +};
278 +
144 const isComplete = computed(() => { 279 const isComplete = computed(() => {
145 return mottoChars.value.every((char) => char) && selectedRole.value; 280 return mottoChars.value.every((char) => char) && selectedRole.value;
146 }); 281 });
147 282
148 -const handleJoinFamily = () => { 283 +// 过滤后的家庭列表
149 - if (isComplete.value) { 284 +const filteredFamilies = computed(() => {
150 - Taro.reLaunch({ 285 + if (!searchKeyword.value) {
151 - url: '/pages/Dashboard/index' 286 + return mockFamilies.value
152 - }); 287 + }
288 + return mockFamilies.value.filter(family =>
289 + family.name.includes(searchKeyword.value) ||
290 + family.description.includes(searchKeyword.value)
291 + )
292 +});
293 +
294 +// 处理搜索
295 +const handleSearch = (value) => {
296 + searchKeyword.value = value
297 + // 搜索时重置选中的家庭
298 + selectedFamilyId.value = ''
299 +}
300 +
301 +// 清除搜索
302 +const handleClearSearch = () => {
303 + searchKeyword.value = ''
304 + // 清除搜索时重置选中的家庭
305 + selectedFamilyId.value = ''
306 +};
307 +
308 +// 选择家庭
309 +const selectFamily = (familyId) => {
310 + selectedFamilyId.value = familyId
311 +};
312 +
313 +// 关闭家庭选择器
314 +const closeFamilySelector = () => {
315 + showFamilySelector.value = false
316 + selectedFamilyId.value = ''
317 + searchKeyword.value = ''
318 +}
319 +
320 +/**
321 + * 计算家庭列表容器的可用高度
322 + */
323 +const calculateFamilyListHeight = async () => {
324 + try {
325 + // 获取系统信息
326 + const systemInfo = await Taro.getSystemInfo()
327 + const windowHeight = systemInfo.windowHeight
328 +
329 + // 弹窗高度为70vh
330 + const popupHeight = windowHeight * 0.8
331 +
332 + // 减去固定元素的高度(估算值,单位px转rpx需要乘以2)
333 + const titleHeight = 60 * 2 // 标题区域
334 + const searchHeight = 80 * 2 // 搜索区域
335 + const buttonHeight = 80 * 2 // 底部按钮区域
336 + const padding = 32 * 2 // 内边距
337 +
338 + // 计算可用高度(px转rpx)
339 + const availableHeight = (popupHeight - (titleHeight + searchHeight + buttonHeight + padding) / 2)
340 +
341 + // 设置最小高度和最大高度
342 + familyListHeight.value = Math.max(200, Math.min(availableHeight * 2, 800))
343 + } catch (error) {
344 + console.error('计算高度失败:', error)
345 + familyListHeight.value = 400 // 使用默认值
346 + }
347 +}
348 +
349 +// 监听弹窗显示状态,动态计算高度
350 +watch(showFamilySelector, async (newVal) => {
351 + if (newVal) {
352 + await nextTick()
353 + await calculateFamilyListHeight()
354 + }
355 +})
356 +
357 +// 监听搜索关键词变化,重置选中的家庭
358 +watch(searchKeyword, () => {
359 + selectedFamilyId.value = ''
360 +})
361 +
362 +// 确认加入家庭
363 +const confirmJoinFamily = () => {
364 + if (!selectedFamilyId.value) {
365 + Taro.showToast({
366 + title: '请选择一个家庭',
367 + icon: 'none'
368 + })
369 + return
370 + }
371 +
372 + const selectedFamily = mockFamilies.value.find(f => f.id === selectedFamilyId.value)
373 + console.log('确认加入家庭:', selectedFamily)
374 +
375 + // 关闭弹窗
376 + closeFamilySelector()
377 +
378 + // 跳转到Dashboard
379 + Taro.redirectTo({
380 + url: '/pages/Dashboard/index'
381 + })
382 +};
383 +
384 +const handleJoinFamily = async () => {
385 + if (!isComplete.value) return
386 +
387 + const motto = mottoChars.value.join('')
388 +
389 + try {
390 + // TODO: 调用API查询家庭
391 + // const families = await api.queryFamiliesByMotto(motto)
392 +
393 + // 模拟API响应 - 生成mock数据
394 + const families = generateMockFamilies()
395 +
396 + console.log('查询家庭:', { motto, role: selectedRole.value, families })
397 +
398 + if (families.length === 0) {
399 + Taro.showToast({
400 + title: '未找到匹配的家庭',
401 + icon: 'none'
402 + })
403 + return
404 + }
405 +
406 + if (families.length === 1) {
407 + // 只有一个家庭,直接跳转
408 + console.log('直接加入家庭:', families[0])
409 + Taro.redirectTo({
410 + url: '/pages/Dashboard/index'
411 + })
412 + } else {
413 + // 多个家庭,显示选择弹窗
414 + mockFamilies.value = families
415 + showFamilySelector.value = true
416 + }
417 + } catch (error) {
418 + console.error('加入家庭失败:', error)
419 + Taro.showToast({
420 + title: '加入失败,请重试',
421 + icon: 'none'
422 + })
153 } 423 }
154 }; 424 };
155 </script> 425 </script>
......