hookehuyr

feat(家庭): 实现家庭设置和切换功能

- 在Dashboard页面添加家庭设置按钮显示控制
- 实现编辑家庭信息的API调用和表单处理
- 添加获取家庭信息和切换当前家庭的API
- 更新MyFamily页面使用真实API数据
- 修复家庭成员管理和显示逻辑
/*
* @Date: 2024-01-01 00:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-02 20:09:52
* @LastEditTime: 2025-09-03 11:25:11
* @FilePath: /lls_program/src/api/family.js
* @Description: 家庭相关接口
*/
......@@ -12,7 +12,10 @@ const Api = {
LIST_MY_FAMILIES: '/srv/?a=family&t=list_my_families',
GET_DASHBOARD: '/srv/?a=family&t=get_dashboard',
ADD_FAMILY: '/srv/?a=family&t=add',
EDIT_FAMILY: '/srv/?a=family&t=edit',
JOIN_FAMILY: '/srv/?a=family&t=join',
GET_FAMILY_INFO: '/srv/?a=family&t=get_my_family',
SWITCH_CURRENT_FAMILY: '/srv/?a=family&t=switch_current',
DEL_MEMBER: '/srv/?a=family&t=del_member',
}
......@@ -44,6 +47,13 @@ export const searchFamilyByPassphraseAPI = (params) => fn(fetch.get(Api.SEARCH_B
* @returns {string} response.data[].name - 家庭名称
* @returns {string} response.data[].avatar_url - 家庭头像
* @returns {boolean} response.data[].is_my - 是否是我创建的家庭
* @returns {string} response.data[].created_by_nickname - 创建者昵称
* @returns {string} response.data[].is_current_family - 是否是当前家庭
* @returns {Array} response.data[].users - 家庭成员列表
* @returns {string} response.data[].users[].user_id - 用户ID
* @returns {string} response.data[].users[].role - 角色
* @returns {string} response.data[].users[].avatar_url - 头像
* @returns {string} response.data[].users[].nickname - 昵称
*/
export const getMyFamiliesAPI = () => fn(fetch.get(Api.LIST_MY_FAMILIES));
......@@ -96,6 +106,24 @@ export const getFamilyDashboardAPI = (params) => fn(fetch.get(Api.GET_DASHBOARD,
export const createFamilyAPI = (params) => fn(fetch.post(Api.ADD_FAMILY, params));
/**
* @description: 编辑家庭
* @param {Object} params - 请求参数
* @param {number} params.id - 家庭ID
* @param {string} params.name - 家庭名称
* @param {string} params.county - 所在区县
* @param {string} params.passphrase - 家训口令
* @param {string} params.avatar_url - 家庭头像
* @param {string} params.note - 家庭介绍
* @returns {Promise} 返回编辑结果
* @returns {Object} response - 响应对象
* @returns {string} response.code - 响应状态码
* @returns {string} response.msg - 响应消息
* @returns {Object} response.data - 响应数据
* @returns {number} response.data.family_id - 家庭ID
*/
export const editFamilyAPI = (params) => fn(fetch.post(Api.EDIT_FAMILY, params));
/**
* @description: 加入家庭
* @param {Object} params - 请求参数
* @param {number} params.family_id - 家庭ID
......@@ -108,6 +136,35 @@ export const createFamilyAPI = (params) => fn(fetch.post(Api.ADD_FAMILY, params)
export const joinFamilyAPI = (params) => fn(fetch.post(Api.JOIN_FAMILY, params));
/**
* @description: 获取家庭信息
* @returns {Promise} 返回家庭信息
* @returns {Object} response - 响应对象
* @returns {string} response.code - 响应状态码
* @returns {string} response.msg - 响应消息
* @returns {Object} response.data - 响应数据
* @returns {number} response.data.id - 家庭ID
* @returns {string} response.data.name - 家庭名称
* @returns {string} response.data.note - 家庭描述
* @returns {string} response.data.county - 所在区县
* @returns {string} response.data.avatar_url - 家庭头像
* @returns {string} response.data.passphrase - 家庭口令
*/
export const getFamilyInfoAPI = (params) => fn(fetch.post(Api.GET_FAMILY_INFO, params));
/**
* @description: 切换当前家庭
* @param {Object} params - 请求参数
* @param {number} params.family_id - 家庭ID
* @returns {Promise} 返回切换结果
* @returns {Object} response - 响应对象
* @returns {string} response.code - 响应状态码
* @returns {string} response.msg - 响应消息
* @returns {Object} response.data - 响应数据
* @returns {number} response.data.family_id - 家庭ID
*/
export const switchCurrentFamilyAPI = (params) => fn(fetch.post(Api.SWITCH_CURRENT_FAMILY, params));
/**
* @description: 退出或移出家庭成员
* @param {Object} params - 请求参数
* @param {number} params.family_id - 家庭ID
......
......@@ -4,7 +4,7 @@
<view class="relative h-48">
<image :src="familyCover" alt="Family background" class="w-full h-full object-cover" />
<view class="absolute inset-0 bg-black bg-opacity-30 flex flex-col justify-end p-5">
<view class="absolute top-4 right-4 text-white flex items-center" @click="goToProfile">
<view v-if="familyOwner" class="absolute top-4 right-4 text-white flex items-center" @click="goToProfile">
<Setting size="24" />
<text class="ml-2">家庭设置</text>
</view>
......@@ -211,6 +211,7 @@ const pendingPoints = ref([]) // 待收集的积分数据
const familyName = ref('')
const familySlogn = ref('')
const familyCover = ref('')
const familyOwner = ref(false);
// 使用媒体预览 composable
const {
......@@ -344,6 +345,7 @@ const initPageData = async () => {
familyName.value = data.family.name;
familySlogn.value = data.family.note;
familyCover.value = data.family.avatar_url || defaultFamilyCover;
familyOwner.value = data.family.is_my;
// 获取今日我的步数
todaySteps.value = data.my_today_step;
// 获取家庭总步数
......
<!--
* @Date: 2025-08-27 17:44:53
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-02 21:41:17
* @LastEditTime: 2025-09-03 11:23:08
* @FilePath: /lls_program/src/pages/EditFamily/index.vue
* @Description: 文件描述
-->
......@@ -23,7 +23,6 @@
class="w-full text-gray-600 focus:outline-none"
placeholder="请输入家庭名称(最多10个字)"
@blur="validateFamilyName"
maxlength="10"
/>
<view v-if="familyNameError" class="text-red-500 text-sm mt-2">{{ familyNameError }}</view>
</view>
......@@ -38,7 +37,6 @@
placeholder="请输入您家庭的特色、成员特点等家庭标签(最多100个字)"
:rows="2"
@blur="validateFamilyIntro"
maxlength="100"
/>
<view v-if="familyIntroError" class="text-red-500 text-sm mt-2">{{ familyIntroError }}</view>
</view>
......@@ -175,6 +173,8 @@ import { Edit, Tips, Photograph, Right } from '@nutui/icons-vue-taro';
import BASE_URL from '@/utils/config';
//
const defaultFamilyCoverSvg = 'https://cdn.ipadbiz.cn/lls_prog/images/default-family-cover.png';
// 获取接口信息
import { editFamilyAPI, getFamilyInfoAPI } from '@/api/family';
const familyName = ref('');
const familyIntro = ref('');
......@@ -217,8 +217,18 @@ const previewVisible = ref(false);
const previewImages = ref([]);
const previewIndex = ref(0);
onMounted(() => {
onMounted(async () => {
Taro.setNavigationBarTitle({ title: '编辑家庭' });
const { code, data } = await getFamilyInfoAPI();
if (code) {
familyName.value = data.name;
familyIntro.value = data.note;
districtValue.value = [data.county];
selectedDistrict.value = data.county;
selectedDistrictText.value = districtColumns.value.find(item => item.value === selectedDistrict.value).text;
familyMotto.value = data.passphrase.split(',');
familyAvatar.value = data.avatar_url;
}
// Mock data for current family information
familyName.value = '幸福一家';
familyIntro.value = '我们是相亲相爱的一家人';
......@@ -443,16 +453,25 @@ const validateForm = () => {
/**
* 保存家庭信息
*/
const handleSaveFamily = () => {
const handleSaveFamily = async () => {
if (!validateForm()) {
return;
}
// 在实际应用中,这里会调用API保存家庭信息
// 调用API保存家庭信息
const { code } = await editFamilyAPI({
name: familyName.value,
note: familyIntro.value,
county: selectedDistrict.value,
passphrase: familyMotto.value.join(''),
avatar_url: familyAvatar.value,
});
if (code) {
showToast('家庭信息保存成功', 'success');
setTimeout(() => {
Taro.navigateBack();
}, 1500);
}
};
</script>
......
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-03 00:11:18
* @LastEditTime: 2025-09-03 11:34:55
* @FilePath: /lls_program/src/pages/MyFamily/index.vue
* @Description: 我的家庭页面 - 展示用户加入的家庭列表
-->
......@@ -18,7 +18,7 @@
<view class="relative">
<!-- 当前家庭标记 -->
<view
v-if="family.is_in"
v-if="family.is_current_family"
class="absolute top-2 right-2 bg-blue-500 text-white text-xs px-2 py-1 rounded-sm z-10"
>
当前家庭
......@@ -42,7 +42,7 @@
<!-- 家庭名称和大家长信息覆盖层 -->
<view class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-4">
<view class="text-white font-bold text-lg mb-1">{{ family.name }}</view>
<view class="text-white/90 text-sm">大家长:{{ family.ownerName }}</view>
<view class="text-white/90 text-sm">大家长:{{ family.created_by_nickname }}</view>
</view>
</view>
......@@ -54,7 +54,7 @@
<!-- 成员头像叠加效果 -->
<view class="avatar-overlap">
<image
v-for="(member, index) in family?.members?.slice(0, 4) || []"
v-for="(member, index) in family?.users?.slice(0, 4) || []"
:key="member.id"
:src="member.avatar"
class="avatar-item w-8 h-8 rounded-full border-2 border-white object-cover"
......@@ -62,16 +62,16 @@
/>
<!-- 更多成员数量显示 -->
<view
v-if="family?.members?.length > 4"
v-if="family?.users?.length > 4"
class="w-8 h-8 rounded-full bg-gray-300 border-2 border-white flex items-center justify-center text-xs text-gray-600"
:style="{ zIndex: 6 }"
>
+{{ family?.members?.length - 4 }}
+{{ family?.users?.length - 4 }}
</view>
</view>
<!-- 总成员数 -->
<view class="ml-3 text-sm text-gray-600">
{{ family?.members?.length || 0 }} 位家庭成员
{{ family?.users?.length || 0 }} 位家庭成员
</view>
</view>
</view>
......@@ -86,7 +86,7 @@
查看成员
</view>
<view
v-if="!family.is_in"
v-if="!family.is_current_family"
@tap="switchToFamily(family.id)"
class="px-4 py-2 bg-blue-500 text-white text-sm rounded-lg"
>
......@@ -144,7 +144,7 @@
<view class="px-4 py-3">
<view class="space-y-3">
<view
v-for="member in mockMembers"
v-for="member in currentMembers"
:key="member.id"
class="bg-gray-50 rounded-lg p-3"
@tap="toggleMemberSelection(member.id)"
......@@ -178,7 +178,7 @@
<!-- 右侧:创建者标识 -->
<view
v-if="member.is_owner"
v-if="member.is_my"
class="ml-3 px-2 py-1 bg-yellow-100 text-yellow-600 text-xs rounded flex-shrink-0"
>
创建者
......@@ -220,7 +220,8 @@ import { ref, onMounted, computed } from 'vue';
import Taro, { useDidShow } from '@tarojs/taro';
import { Home } from '@nutui/icons-vue-taro';
import './index.less';
import { getMyFamiliesAPI } from '@/api/family';
// 获取接口信息
import { getMyFamiliesAPI, switchCurrentFamilyAPI } from '@/api/family';
//
const defaultFamilyCoverSvg = 'https://cdn.ipadbiz.cn/lls_prog/images/default-family-cover.png';
// 获取接口数据
......@@ -232,7 +233,7 @@ const familyList = ref([]);
const showMemberPopup = ref(false);
const currentFamily = ref(null);
const selectedMembers = ref([]);
const mockMembers = ref([]);
const currentMembers = ref([]);
/**
* 初始化页面数据
......@@ -248,11 +249,11 @@ const initPageData = async () => {
{
id: 1,
name: '幸福之家',
ownerName: '张明明',
created_by_nickname: '张明明',
avatar_url: 'https://images.unsplash.com/photo-1511895426328-dc8714191300?w=400&h=200&fit=crop',
is_in: true,
is_current_family: true,
is_my: true, // 当前用户是家长,可以管理成员
members: [
users: [
{ id: 1, avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face' },
{ id: 2, avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face' },
{ id: 3, avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face' }
......@@ -261,11 +262,11 @@ const initPageData = async () => {
{
id: 2,
name: '欢乐之家',
ownerName: '李志强',
created_by_nickname: '李志强',
avatar_url: 'https://images.unsplash.com/photo-1502086223501-7ea6ecd79368?w=400&h=200&fit=crop',
is_in: false,
is_current_family: false,
is_my: false, // 当前用户不是家长
members: [
users: [
{ id: 4, avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=face' },
{ id: 5, avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face' }
]
......@@ -273,11 +274,11 @@ const initPageData = async () => {
{
id: 3,
name: '快乐之家',
ownerName: '王芳',
created_by_nickname: '王芳',
avatar_url: 'https://images.unsplash.com/photo-1502086223501-7ea6ecd79368?w=400&h=200&fit=crop',
is_in: false,
is_current_family: false,
is_my: true, // 当前用户是家长,但不在此家庭
members: [
users: [
{ id: 6, avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=face' },
{ id: 7, avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face' }
]
......@@ -294,29 +295,31 @@ const isShowBtn = computed(() => {
* 切换到指定家庭
* @param {number} familyId - 家庭ID
*/
const switchToFamily = (familyId) => {
const switchToFamily = async (familyId) => {
const family = familyList.value.find(f => f.id === familyId);
if (!family) return;
Taro.showModal({
title: '切换家庭',
content: `确定要切换到「${family.name}」吗?`,
success: (res) => {
success: async (res) => {
if (res.confirm) {
const { code } = await switchCurrentFamilyAPI(familyId);
if (code) {
// 切换家庭逻辑 - 先清除所有当前标记,再设置新的当前家庭
familyList.value = familyList.value.map(f => ({
...f,
is_in: f.id === familyId
is_current_family: f.id === familyId
}));
console.log('切换家庭后的列表:', familyList.value);
Taro.showToast({
title: '切换成功',
icon: 'success'
});
}
}
}
});
};
......@@ -345,7 +348,7 @@ const exitFamily = (familyId) => {
// 退出家庭逻辑
const exitingFamily = familyList.value.find(f => f.id === familyId);
if (exitingFamily?.is_in) {
if (exitingFamily?.is_current_family) {
// 如果退出的是当前家庭,需要返回我的页面
familyList.value = familyList.value.filter(f => f.id !== familyId);
......@@ -390,41 +393,41 @@ const joinNewFamily = () => {
const showMemberManagement = (family) => {
currentFamily.value = family;
// 生成模拟成员数据
mockMembers.value = [
currentMembers.value = [
{
id: 1,
nickname: '张明明',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face',
role: '父亲',
is_owner: true
is_my: true
},
{
id: 2,
nickname: '李美丽',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face',
role: '母亲',
is_owner: false
is_my: false
},
{
id: 3,
nickname: '张小明',
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=face',
role: '儿子',
is_owner: false
is_my: false
},
{
id: 4,
nickname: '张小花',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face',
role: '女儿',
is_owner: false
is_my: false
},
{
id: 5,
nickname: '王奶奶',
avatar: 'https://images.unsplash.com/photo-1551836022-d5d88e9218df?w=100&h=100&fit=crop&crop=face',
role: '奶奶',
is_owner: false
is_my: false
}
];
selectedMembers.value = [];
......@@ -438,7 +441,7 @@ const closeMemberPopup = () => {
showMemberPopup.value = false;
currentFamily.value = null;
selectedMembers.value = [];
mockMembers.value = [];
currentMembers.value = [];
};
/**
......@@ -446,10 +449,10 @@ const closeMemberPopup = () => {
* @param {number} memberId - 成员ID
*/
const toggleMemberSelection = (memberId) => {
const member = mockMembers.value.find(m => m.id === memberId);
const member = currentMembers.value.find(m => m.id === memberId);
// 创建者不能被选择移除
if (member?.is_owner) {
if (member?.is_my) {
Taro.showToast({
title: '创建者不能被移除',
icon: 'none'
......@@ -477,7 +480,7 @@ const removeSelectedMembers = () => {
return;
}
const selectedNames = mockMembers.value
const selectedNames = currentMembers.value
.filter(m => selectedMembers.value.includes(m.id))
.map(m => m.nickname)
.join('、');
......@@ -488,7 +491,7 @@ const removeSelectedMembers = () => {
success: (res) => {
if (res.confirm) {
// 从模拟数据中移除选中的成员
mockMembers.value = mockMembers.value.filter(m => !selectedMembers.value.includes(m.id));
currentMembers.value = currentMembers.value.filter(m => !selectedMembers.value.includes(m.id));
selectedMembers.value = [];
Taro.showToast({
......