hookehuyr

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

实现家庭选择弹窗组件,包含搜索、列表展示和确认功能
将@click事件统一改为@tap以优化移动端体验
添加过渡动画和样式优化
......@@ -17,6 +17,7 @@ declare module 'vue' {
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar']
NutToast: typeof import('@nutui/nutui-taro')['Toast']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
PointsCollector: typeof import('./src/components/PointsCollector.vue')['default']
......
......@@ -15,6 +15,7 @@
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s;
}
.motto-input {
......@@ -32,7 +33,48 @@
}
.identity-title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 1rem;
font-size: 32rpx;
font-weight: 600;
color: #374151;
margin-bottom: 24rpx;
}
// 家庭选择弹窗样式
.family-selector-container {
display: flex;
flex-direction: column;
height: 100%;
background: white;
}
.family-item {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
}
}
// 文本截断样式
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
// 间距样式
.space-y-3 > * + * {
margin-top: 24rpx;
}
.space-x-3 > * + * {
margin-left: 24rpx;
}
......
......@@ -45,7 +45,7 @@
<view
v-for="role in familyRoles"
:key="role.id"
@click="selectedRole = role.id"
@tap="selectedRole = role.id"
:class="[
'w-[calc(49%-4rpx)] py-3 rounded-lg border text-center flex flex-col items-center gap-1',
selectedRole === role.id
......@@ -60,7 +60,7 @@
</view>
<!-- Submit Button -->
<view
@click="handleJoinFamily"
@tap="handleJoinFamily"
:disabled="!isComplete"
:class="[
'w-full py-3 text-white text-lg font-medium rounded-lg mt-auto text-center',
......@@ -70,13 +70,104 @@
加入家庭
</view>
</view>
<!-- 家庭选择弹窗 -->
<nut-popup
v-model:visible="showFamilySelector"
position="bottom"
:style="{ height: '80vh' }"
round
closeable
@close="closeFamilySelector"
>
<view class="family-selector-container">
<!-- 标题 -->
<view class="text-lg font-bold text-center py-4 border-b border-gray-100">
选择要加入的家庭
</view>
<!-- 搜索框 -->
<view class="p-4">
<nut-searchbar
v-model="searchKeyword"
placeholder="搜索家庭名称"
@search="handleSearch"
@clear="handleClearSearch"
/>
</view>
<!-- 家庭列表 -->
<view class="flex-1 px-4 pb-4">
<view
ref="familyListContainer"
class="space-y-3 overflow-y-auto"
:style="{ maxHeight: familyListHeight + 'rpx' }"
>
<view
v-for="family in filteredFamilies"
:key="family.id"
@tap="selectFamily(family.id)"
:class="[
'family-item p-4 border rounded-lg flex items-center space-x-3',
selectedFamilyId === family.id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 bg-white'
]"
>
<!-- 家庭头像 -->
<view class="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
<image
v-if="family.avatar"
:src="family.avatar"
class="w-full h-full object-cover"
/>
<view v-else class="text-gray-500 text-xs">头像</view>
</view>
<!-- 家庭信息 -->
<view class="flex-1">
<view class="font-medium text-gray-900 mb-1">{{ family.name }}</view>
<view class="text-sm text-gray-600 line-clamp-2">{{ family.description }}</view>
</view>
<!-- 选中状态 -->
<view v-if="selectedFamilyId === family.id" class="text-blue-500">
<Check size="20" />
</view>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="flex gap-3 p-4 border-t border-gray-100">
<nut-button
@click="closeFamilySelector"
class="flex-1"
type="default"
size="large"
plain
>
关闭
</nut-button>
<nut-button
@click="confirmJoinFamily"
class="flex-1"
:color="selectedFamilyId ? '#3b82f6' : 'gray'"
:disabled="!selectedFamilyId"
size="large"
>
确认
</nut-button>
</view>
</view>
</nut-popup>
</view>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue';
import { ref, computed, nextTick, onMounted, watch } from 'vue';
import Taro from '@tarojs/taro';
import { My } from '@nutui/icons-vue-taro';
import { My, Check } from '@nutui/icons-vue-taro';
import AppHeader from '../../components/AppHeader.vue';
const mottoChars = ref(['', '', '', '']);
......@@ -84,6 +175,14 @@ const selectedRole = ref('');
const inputRefs = ref([]);
const focusedIndex = ref(-1);
// 弹窗相关数据
const showFamilySelector = ref(false);
const searchKeyword = ref('');
const selectedFamilyId = ref('');
const mockFamilies = ref([]);
const familyListContainer = ref(null);
const familyListHeight = ref(400); // 默认高度
const handleInputChange = (index, value) => {
// 允许输入多个字符,但只保留第一个有效字符(汉字、数字、大小写字母),兼容输入法
if (value) {
......@@ -141,15 +240,186 @@ const familyRoles = [
{ id: 'maternal-granddaughter', label: '外孙女' }
];
// Mock家庭数据
const generateMockFamilies = () => {
return [
{
id: '1',
name: '幸福之家',
description: '我们是一个温馨和睦的大家庭,每天一起运动健身,分享生活的美好时光。',
avatar: 'https://placehold.co/100x100/e2f3ff/0369a1?text=幸福&font=roboto'
},
{
id: '2',
name: '健康家园',
description: '注重健康生活,坚持每日步行锻炼,用积分兑换健康好礼。',
avatar: 'https://placehold.co/100x100/f0f9ff/0284c7?text=健康&font=roboto'
},
{
id: '3',
name: '快乐一家人',
description: '快乐是我们家的主旋律,一起运动,一起成长,一起享受生活。',
avatar: 'https://placehold.co/100x100/fef3c7/d97706?text=快乐&font=roboto'
},
{
id: '4',
name: '幸福一家人',
description: '快乐是我们家的主旋律,一起运动,一起成长,一起享受生活。',
avatar: 'https://placehold.co/100x100/fef3c7/d97706?text=快乐&font=roboto'
},
{
id: '5',
name: '幸福一家人',
description: '快乐是我们家的主旋律,一起运动,一起成长,一起享受生活。',
avatar: 'https://placehold.co/100x100/fef3c7/d97706?text=快乐&font=roboto'
},
]
};
const isComplete = computed(() => {
return mottoChars.value.every((char) => char) && selectedRole.value;
});
const handleJoinFamily = () => {
if (isComplete.value) {
Taro.reLaunch({
// 过滤后的家庭列表
const filteredFamilies = computed(() => {
if (!searchKeyword.value) {
return mockFamilies.value
}
return mockFamilies.value.filter(family =>
family.name.includes(searchKeyword.value) ||
family.description.includes(searchKeyword.value)
)
});
// 处理搜索
const handleSearch = (value) => {
searchKeyword.value = value
// 搜索时重置选中的家庭
selectedFamilyId.value = ''
}
// 清除搜索
const handleClearSearch = () => {
searchKeyword.value = ''
// 清除搜索时重置选中的家庭
selectedFamilyId.value = ''
};
// 选择家庭
const selectFamily = (familyId) => {
selectedFamilyId.value = familyId
};
// 关闭家庭选择器
const closeFamilySelector = () => {
showFamilySelector.value = false
selectedFamilyId.value = ''
searchKeyword.value = ''
}
/**
* 计算家庭列表容器的可用高度
*/
const calculateFamilyListHeight = async () => {
try {
// 获取系统信息
const systemInfo = await Taro.getSystemInfo()
const windowHeight = systemInfo.windowHeight
// 弹窗高度为70vh
const popupHeight = windowHeight * 0.8
// 减去固定元素的高度(估算值,单位px转rpx需要乘以2)
const titleHeight = 60 * 2 // 标题区域
const searchHeight = 80 * 2 // 搜索区域
const buttonHeight = 80 * 2 // 底部按钮区域
const padding = 32 * 2 // 内边距
// 计算可用高度(px转rpx)
const availableHeight = (popupHeight - (titleHeight + searchHeight + buttonHeight + padding) / 2)
// 设置最小高度和最大高度
familyListHeight.value = Math.max(200, Math.min(availableHeight * 2, 800))
} catch (error) {
console.error('计算高度失败:', error)
familyListHeight.value = 400 // 使用默认值
}
}
// 监听弹窗显示状态,动态计算高度
watch(showFamilySelector, async (newVal) => {
if (newVal) {
await nextTick()
await calculateFamilyListHeight()
}
})
// 监听搜索关键词变化,重置选中的家庭
watch(searchKeyword, () => {
selectedFamilyId.value = ''
})
// 确认加入家庭
const confirmJoinFamily = () => {
if (!selectedFamilyId.value) {
Taro.showToast({
title: '请选择一个家庭',
icon: 'none'
})
return
}
const selectedFamily = mockFamilies.value.find(f => f.id === selectedFamilyId.value)
console.log('确认加入家庭:', selectedFamily)
// 关闭弹窗
closeFamilySelector()
// 跳转到Dashboard
Taro.redirectTo({
url: '/pages/Dashboard/index'
});
})
};
const handleJoinFamily = async () => {
if (!isComplete.value) return
const motto = mottoChars.value.join('')
try {
// TODO: 调用API查询家庭
// const families = await api.queryFamiliesByMotto(motto)
// 模拟API响应 - 生成mock数据
const families = generateMockFamilies()
console.log('查询家庭:', { motto, role: selectedRole.value, families })
if (families.length === 0) {
Taro.showToast({
title: '未找到匹配的家庭',
icon: 'none'
})
return
}
if (families.length === 1) {
// 只有一个家庭,直接跳转
console.log('直接加入家庭:', families[0])
Taro.redirectTo({
url: '/pages/Dashboard/index'
})
} else {
// 多个家庭,显示选择弹窗
mockFamilies.value = families
showFamilySelector.value = true
}
} catch (error) {
console.error('加入家庭失败:', error)
Taro.showToast({
title: '加入失败,请重试',
icon: 'none'
})
}
};
</script>
......