Files
account/resources/mobile/pages/family/index.vue
2026-01-19 13:21:38 +08:00

604 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<un-pages
:show-nav-bar="true"
nav-bar-title="家庭管理"
:show-back="true"
>
<view class="page-content">
<view v-if="loading" class="loading-state">
<uni-load-more status="loading"></uni-load-more>
</view>
<!-- 已加入家庭 -->
<view v-else-if="hasFamily" class="family-container">
<!-- 家庭信息卡片 -->
<view class="family-card">
<view class="family-header">
<view class="family-icon">
<text>🏠</text>
</view>
<view class="family-info">
<text class="family-name">{{ familyInfo.name }}</text>
<text class="family-role" v-if="isOwner">我是家主</text>
<text class="family-role" v-else>家庭成员</text>
</view>
</view>
<!-- 家庭邀请码仅家主可见 -->
<view v-if="isOwner" class="invite-section">
<view class="invite-info">
<text class="invite-label">家庭邀请码</text>
<view class="invite-code-wrapper">
<text class="invite-code">{{ inviteCode || '加载中...' }}</text>
<view class="copy-btn" @tap="copyInviteCode">
<uni-icons type="copy" size="16" color="#667eea"></uni-icons>
<text>复制</text>
</view>
</view>
</view>
<button class="regenerate-btn" @tap="regenerateInviteCode" :loading="regenerating">
重新生成邀请码
</button>
</view>
</view>
<!-- 家庭成员列表 -->
<view class="members-section">
<view class="section-header">
<text class="section-title">家庭成员</text>
<text class="member-count">{{ memberList.length }}</text>
</view>
<view class="member-list">
<view class="member-item" v-for="member in memberList" :key="member.id">
<view class="member-avatar">
<text>{{ member.name ? member.name.charAt(0) : 'U' }}</text>
</view>
<view class="member-info">
<text class="member-name">{{ member.name || member.username }}</text>
<text class="member-role" v-if="member.is_owner">家主</text>
</view>
<!-- 移除成员按钮仅家主可见 -->
<view v-if="isOwner && !member.is_owner" class="remove-btn" @tap="removeMember(member)">
<uni-icons type="clear" size="20" color="#FF6B6B"></uni-icons>
</view>
</view>
</view>
<!-- 转让家主仅家主可见 -->
<view v-if="isOwner && memberList.length > 1" class="action-btn">
<button class="secondary-btn" @tap="transferOwner">
转让家主
</button>
</view>
</view>
<!-- 退出家庭按钮 -->
<view class="action-btn">
<button class="exit-btn" @tap="confirmLeaveFamily">
退出家庭
</button>
</view>
</view>
<!-- 未加入家庭 -->
<view v-else class="no-family-container">
<view class="no-family-icon">
<text>🏠</text>
</view>
<text class="no-family-title">您还未加入任何家庭</text>
<text class="no-family-desc">创建或加入家庭与家人一起记账</text>
<view class="action-buttons">
<button class="primary-btn" @tap="goToCreate">
<uni-icons type="plus" size="20" color="#fff"></uni-icons>
<text>创建家庭</text>
</button>
<button class="secondary-btn" @tap="goToJoin">
<uni-icons type="personadd" size="20" color="#667eea"></uni-icons>
<text>加入家庭</text>
</button>
</view>
</view>
</view>
</un-pages>
</template>
<script>
export default {
data() {
return {
loading: false,
familyInfo: {},
inviteCode: '',
regenerating: false,
memberList: []
}
},
computed: {
hasFamily() {
if (!this.$store || !this.$store.getters) {
return false
}
return this.$store.getters.hasFamily || false
},
isOwner() {
if (!this.$store || !this.$store.state || !this.$store.state.family) {
return false
}
return this.$store.state.family.isOwner || false
}
},
onLoad() {
this.loadFamilyData()
},
onShow() {
// 从其他页面返回时刷新数据
this.loadFamilyData()
},
methods: {
async loadFamilyData() {
if (!this.hasFamily) {
return
}
this.loading = true
try {
// 获取家庭信息
const familyRes = await this.$api.family.info.get()
if (familyRes && familyRes.code === 1) {
this.familyInfo = familyRes.data
this.$store.commit('setFamilyInfo', familyRes.data)
}
// 获取邀请码(仅家主)
if (this.isOwner) {
const codeRes = await this.$api.family.inviteCode.get()
if (codeRes && codeRes.code === 1) {
this.inviteCode = codeRes.data.invite_code
}
}
// 获取家庭成员列表
const membersRes = await this.$api.family.members.get()
if (membersRes && membersRes.code === 1) {
this.memberList = membersRes.data || []
}
} catch (error) {
console.error('加载家庭数据失败', error)
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
this.loading = false
}
},
copyInviteCode() {
uni.setClipboardData({
data: this.inviteCode,
success: () => {
uni.showToast({
title: '邀请码已复制',
icon: 'success'
})
}
})
},
async regenerateInviteCode() {
uni.showModal({
title: '提示',
content: '重新生成邀请码后,旧的邀请码将失效,确定要重新生成吗?',
success: async (res) => {
if (res.confirm) {
this.regenerating = true
try {
const res = await this.$api.family.regenerateInviteCode.post()
if (res && res.code === 1) {
this.inviteCode = res.data.invite_code
uni.showToast({
title: '邀请码已更新',
icon: 'success'
})
} else {
uni.showToast({
title: res?.message || '操作失败',
icon: 'none'
})
}
} catch (error) {
console.error('重新生成邀请码失败', error)
uni.showToast({
title: error?.message || '操作失败,请重试',
icon: 'none'
})
} finally {
this.regenerating = false
}
}
}
})
},
removeMember(member) {
uni.showModal({
title: '确认移除',
content: `确定要将 ${member.name || member.username} 移出家庭吗?`,
success: async (res) => {
if (res.confirm) {
try {
const result = await this.$api.family.removeMember.post({
user_id: member.id
})
if (result && result.code === 1) {
uni.showToast({
title: '移除成功',
icon: 'success'
})
this.loadFamilyData()
} else {
uni.showToast({
title: result?.message || '操作失败',
icon: 'none'
})
}
} catch (error) {
console.error('移除成员失败', error)
uni.showToast({
title: error?.message || '操作失败,请重试',
icon: 'none'
})
}
}
}
})
},
transferOwner() {
// TODO: 实现转让家主功能
uni.showToast({
title: '功能开发中',
icon: 'none'
})
},
confirmLeaveFamily() {
uni.showModal({
title: '确认退出',
content: '退出家庭后,您将无法查看和记录家庭账单,确定要退出吗?',
success: async (res) => {
if (res.confirm) {
try {
const result = await this.$store.dispatch('leaveFamily')
if (result && result.code === 1) {
uni.showToast({
title: '已退出家庭',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
uni.showToast({
title: result?.message || '操作失败',
icon: 'none'
})
}
} catch (error) {
console.error('退出家庭失败', error)
uni.showToast({
title: error?.message || '操作失败,请重试',
icon: 'none'
})
}
}
}
})
},
goToCreate() {
uni.navigateTo({
url: '/pages/family/create'
})
},
goToJoin() {
uni.navigateTo({
url: '/pages/family/join'
})
}
}
}
</script>
<style lang="scss" scoped>
.page-content {
padding: 30rpx;
min-height: 100vh;
}
.loading-state {
padding: 120rpx 0;
}
.no-family-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0;
.no-family-icon {
width: 160rpx;
height: 160rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
text {
font-size: 80rpx;
}
}
.no-family-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.no-family-desc {
font-size: 26rpx;
color: #999;
margin-bottom: 60rpx;
}
.action-buttons {
width: 100%;
padding: 0 40rpx;
button {
margin-bottom: 30rpx;
}
}
}
.family-container {
.family-card {
background: #fff;
border-radius: 20rpx;
padding: 40rpx 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.family-header {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.family-icon {
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
text {
font-size: 50rpx;
}
}
.family-info {
flex: 1;
display: flex;
flex-direction: column;
.family-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.family-role {
font-size: 24rpx;
color: #999;
}
}
}
.invite-section {
border-top: 2rpx solid #f5f5f5;
padding-top: 30rpx;
.invite-info {
margin-bottom: 20rpx;
.invite-label {
font-size: 26rpx;
color: #666;
margin-bottom: 16rpx;
display: block;
}
.invite-code-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
background: #f8f8f8;
border-radius: 12rpx;
padding: 20rpx 24rpx;
.invite-code {
font-size: 32rpx;
font-weight: bold;
color: #333;
letter-spacing: 4rpx;
}
.copy-btn {
display: flex;
align-items: center;
padding: 10rpx 20rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50rpx;
text {
font-size: 24rpx;
color: #fff;
margin-left: 6rpx;
}
}
}
}
.regenerate-btn {
width: 100%;
height: 70rpx;
line-height: 70rpx;
font-size: 26rpx;
background: #f8f8f8;
color: #666;
border: none;
border-radius: 50rpx;
&::after {
border: none;
}
}
}
}
}
.members-section {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.member-count {
font-size: 24rpx;
color: #999;
}
}
.member-list {
.member-item {
display: flex;
align-items: center;
padding: 24rpx 0;
border-bottom: 2rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.member-avatar {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
text {
font-size: 32rpx;
color: #fff;
font-weight: bold;
}
}
.member-info {
flex: 1;
display: flex;
align-items: center;
.member-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-right: 16rpx;
}
.member-role {
font-size: 22rpx;
color: #667eea;
background: rgba(102, 126, 234, 0.1);
padding: 4rpx 12rpx;
border-radius: 20rpx;
}
}
.remove-btn {
padding: 10rpx;
}
}
}
}
.action-btn {
margin-bottom: 30rpx;
button {
width: 100%;
height: 90rpx;
line-height: 90rpx;
border-radius: 50rpx;
font-size: 28rpx;
&::after {
border: none;
}
}
}
.primary-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
display: flex;
align-items: center;
justify-content: center;
uni-icons {
margin-right: 10rpx;
}
}
.secondary-btn {
background: #f8f8f8;
color: #667eea;
border: none;
display: flex;
align-items: center;
justify-content: center;
uni-icons {
margin-right: 10rpx;
}
}
.exit-btn {
background: #fff;
color: #FF6B6B;
border: 2rpx solid #FF6B6B;
}
</style>