|
|
@@ -1,49 +1,35 @@
|
|
|
<template>
|
|
|
- <view class="member-chip-picker">
|
|
|
- <view v-if="loading" class="mcp-loading">
|
|
|
- <text class="mcp-loading-text">加载成员中...</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <view v-else-if="members.length > 0" class="mcp-chips">
|
|
|
- <view
|
|
|
- class="mcp-chip"
|
|
|
- v-for="(m, i) in members"
|
|
|
- :key="getMemberKey(m, i)"
|
|
|
- :class="{ 'mcp-chip-on': isSelected(m.memberId) }"
|
|
|
- @tap="toggleMember(m.memberId)"
|
|
|
- >
|
|
|
- <image
|
|
|
- v-if="m.avatar"
|
|
|
- class="mcp-chip-avatar"
|
|
|
- :src="m.avatar"
|
|
|
- mode="aspectFill"
|
|
|
- />
|
|
|
- <view v-else class="mcp-chip-avatar mcp-chip-avatar-default">
|
|
|
- <text class="mcp-chip-avatar-text">{{ getInitial(m) }}</text>
|
|
|
+ <view class="member-picker">
|
|
|
+ <text class="member-load-hint">成员数:{{ memberCount }}</text>
|
|
|
+ <scroll-view class="member-scroll" scroll-x="true" :show-scrollbar="false" v-if="memberCount > 0">
|
|
|
+ <view class="member-chips">
|
|
|
+ <view
|
|
|
+ v-for="item in members"
|
|
|
+ :key="item.id"
|
|
|
+ class="member-chip"
|
|
|
+ :class="{ 'member-chip-on': item.selected }"
|
|
|
+ @click="toggleMember(item.id)">
|
|
|
+ <text class="member-chip-text">{{ item.nickname }}</text>
|
|
|
</view>
|
|
|
- <text class="mcp-chip-text">{{ m.name || m.nickname || '成员' }}</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <view class="mcp-chip mcp-chip-add" @tap="goAddMember">
|
|
|
- <text class="mcp-chip-add-icon">+</text>
|
|
|
- <text class="mcp-chip-text">添加</text>
|
|
|
</view>
|
|
|
+ </scroll-view>
|
|
|
+ <view v-if="memberCount === 0 && !loading" class="empty-state">
|
|
|
+ <text class="empty-text">暂无家庭成员,请先添加后再制定方案</text>
|
|
|
+ <view class="empty-action" @click="goAddMember">去添加成员</view>
|
|
|
+ <view class="empty-action reload-btn" @click="reload">重新加载</view>
|
|
|
</view>
|
|
|
-
|
|
|
- <view v-else class="mcp-empty">
|
|
|
- <text class="mcp-empty-text">暂无家庭成员</text>
|
|
|
- <view class="mcp-empty-btn" @tap="goAddMember">+ 去添加成员</view>
|
|
|
+ <view v-if="loading" class="empty-state">
|
|
|
+ <text class="empty-text">成员加载中...</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { getFamilyMemberList } from '@/utils/api.js'
|
|
|
+import { getFamilyMemberList } from '../utils/api.js'
|
|
|
|
|
|
export default {
|
|
|
name: 'MemberChipPicker',
|
|
|
props: {
|
|
|
- // 已选中的成员 id 数组(字符串)
|
|
|
selectedIds: {
|
|
|
type: Array,
|
|
|
default: function() { return [] }
|
|
|
@@ -52,182 +38,215 @@ export default {
|
|
|
data: function() {
|
|
|
return {
|
|
|
members: [],
|
|
|
- innerSelectedIds: [],
|
|
|
- loading: false,
|
|
|
- loaded: false
|
|
|
- }
|
|
|
- },
|
|
|
- watch: {
|
|
|
- selectedIds: {
|
|
|
- handler: function(val) {
|
|
|
- this.innerSelectedIds = (val || []).map(function(id) { return String(id) })
|
|
|
- },
|
|
|
- immediate: true
|
|
|
+ memberCount: 0,
|
|
|
+ loading: false
|
|
|
}
|
|
|
},
|
|
|
- created: function() {
|
|
|
- this.reload()
|
|
|
+ mounted: function() {
|
|
|
+ this.loadMembers()
|
|
|
},
|
|
|
methods: {
|
|
|
- // ——— :key 方法(避免模板内运算符表达式) ———
|
|
|
- getMemberKey: function(m, i) {
|
|
|
- return String(m.memberId || m.id || i)
|
|
|
+ reload: function() {
|
|
|
+ this.loadMembers()
|
|
|
},
|
|
|
- // ——— 首字(无头像兜底) ———
|
|
|
- getInitial: function(m) {
|
|
|
- var name = m.name || m.nickname || ''
|
|
|
- return name ? name.substring(0, 1) : '?'
|
|
|
+ goAddMember: function() {
|
|
|
+ this.$emit('add-member')
|
|
|
+ uni.navigateTo({ url: '/pages/family/add-member' })
|
|
|
},
|
|
|
- isSelected: function(id) {
|
|
|
- return this.innerSelectedIds.indexOf(String(id)) >= 0
|
|
|
+ toggleMember: function(id) {
|
|
|
+ var strId = String(id)
|
|
|
+ var ids = []
|
|
|
+ var i
|
|
|
+ for (i = 0; i < this.members.length; i++) {
|
|
|
+ if (this.members[i].selected) ids.push(String(this.members[i].id))
|
|
|
+ }
|
|
|
+ var idx = ids.indexOf(strId)
|
|
|
+ if (idx >= 0) ids.splice(idx, 1)
|
|
|
+ else ids.push(strId)
|
|
|
+ this.applySelected(ids)
|
|
|
+ this.emitChange()
|
|
|
},
|
|
|
- // ——— 加载家庭成员 ———
|
|
|
- reload: function() {
|
|
|
- var self = this
|
|
|
- this.loading = true
|
|
|
- getFamilyMemberList({ visibleOnly: true }).then(function(res) {
|
|
|
- self.loading = false
|
|
|
- if (res && res.code === 200 && res.data) {
|
|
|
- var raw = res.data instanceof Array ? res.data : ((res.data && res.data.list) || [])
|
|
|
- var list = []
|
|
|
- for (var i = 0; i < raw.length; i++) {
|
|
|
- var m = raw[i]
|
|
|
- // 同时输出 id/memberId、name/nickname,兼容父组件对成员字段的不同取法
|
|
|
- list.push({
|
|
|
- id: m.id,
|
|
|
- memberId: m.memberId !== undefined ? m.memberId : m.id,
|
|
|
- name: m.name || m.nickname,
|
|
|
- nickname: m.nickname || m.name,
|
|
|
- avatar: m.avatar || '',
|
|
|
- relationship: m.relationship || m.relativeLabel || m.roleLabel || ''
|
|
|
- })
|
|
|
- }
|
|
|
- self.members = list
|
|
|
- self.emitState()
|
|
|
- } else {
|
|
|
- self.members = []
|
|
|
- self.emitState()
|
|
|
- }
|
|
|
- }).catch(function() {
|
|
|
- self.loading = false
|
|
|
- self.members = []
|
|
|
- })
|
|
|
+ applySelected: function(ids) {
|
|
|
+ var selected = {}
|
|
|
+ var i
|
|
|
+ for (i = 0; i < ids.length; i++) {
|
|
|
+ selected[String(ids[i])] = true
|
|
|
+ }
|
|
|
+ var list = []
|
|
|
+ for (i = 0; i < this.members.length; i++) {
|
|
|
+ var m = this.members[i]
|
|
|
+ list.push({
|
|
|
+ id: m.id,
|
|
|
+ memberId: m.memberId,
|
|
|
+ name: m.name,
|
|
|
+ nickname: m.nickname,
|
|
|
+ selected: !!selected[String(m.id)]
|
|
|
+ })
|
|
|
+ }
|
|
|
+ this.members = list
|
|
|
+ this.memberCount = list.length
|
|
|
},
|
|
|
- // ——— 切换选中态 ———
|
|
|
- toggleMember: function(id) {
|
|
|
- var sid = String(id)
|
|
|
- var idx = this.innerSelectedIds.indexOf(sid)
|
|
|
- if (idx >= 0) {
|
|
|
- this.innerSelectedIds.splice(idx, 1)
|
|
|
- } else {
|
|
|
- this.innerSelectedIds.push(sid)
|
|
|
+ emitChange: function() {
|
|
|
+ var ids = []
|
|
|
+ var i
|
|
|
+ for (i = 0; i < this.members.length; i++) {
|
|
|
+ if (this.members[i].selected) ids.push(String(this.members[i].id))
|
|
|
}
|
|
|
- this.emitState()
|
|
|
+ this.$emit('change', { selectedIds: ids, members: this.members })
|
|
|
},
|
|
|
- // ——— 通知父组件 ———
|
|
|
- emitState: function() {
|
|
|
- this.$emit('change', {
|
|
|
- selectedIds: this.innerSelectedIds.slice(),
|
|
|
- members: this.members.slice()
|
|
|
- })
|
|
|
- if (!this.loaded) {
|
|
|
- this.loaded = true
|
|
|
- this.$emit('loaded', {
|
|
|
- selectedIds: this.innerSelectedIds.slice(),
|
|
|
- members: this.members.slice()
|
|
|
+ normalizeMemberList: function(data) {
|
|
|
+ if (!data) return []
|
|
|
+ var asList = function(v) {
|
|
|
+ return v && typeof v === 'object' && typeof v.length === 'number' ? v : null
|
|
|
+ }
|
|
|
+ if (asList(data)) return data
|
|
|
+ var sources = [data.children, data.members, data.list, data.records]
|
|
|
+ var merged = []
|
|
|
+ var seen = {}
|
|
|
+ var s, i, m, mid, key
|
|
|
+ for (s = 0; s < sources.length; s++) {
|
|
|
+ var arr = asList(sources[s])
|
|
|
+ if (!arr || arr.length === 0) continue
|
|
|
+ for (i = 0; i < arr.length; i++) {
|
|
|
+ m = arr[i]
|
|
|
+ if (!m) continue
|
|
|
+ mid = m.id != null ? m.id : (m.memberId != null ? m.memberId : m.userId)
|
|
|
+ if (mid == null) continue
|
|
|
+ key = String(mid)
|
|
|
+ if (seen[key]) continue
|
|
|
+ seen[key] = true
|
|
|
+ merged.push(m)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return merged
|
|
|
+ },
|
|
|
+ applyList: function(rawList) {
|
|
|
+ var list = rawList || []
|
|
|
+ var mapped = []
|
|
|
+ var i, m, mid, nm
|
|
|
+ for (i = 0; i < list.length; i++) {
|
|
|
+ m = list[i]
|
|
|
+ if (!m) continue
|
|
|
+ mid = m.id != null ? m.id : (m.memberId != null ? m.memberId : m.userId)
|
|
|
+ if (mid == null) continue
|
|
|
+ nm = m.nickname || m.name || m.roleLabel || '成员'
|
|
|
+ mapped.push({
|
|
|
+ id: mid,
|
|
|
+ memberId: String(mid),
|
|
|
+ name: String(nm),
|
|
|
+ nickname: String(nm),
|
|
|
+ selected: false
|
|
|
})
|
|
|
}
|
|
|
+ var selectedIds = (this.selectedIds || []).map(function(id) { return String(id) })
|
|
|
+ var idSet = {}
|
|
|
+ for (i = 0; i < mapped.length; i++) idSet[String(mapped[i].id)] = true
|
|
|
+ selectedIds = selectedIds.filter(function(id) { return !!idSet[String(id)] })
|
|
|
+ if (selectedIds.length === 0 && mapped.length > 0) {
|
|
|
+ selectedIds = [String(mapped[0].id)]
|
|
|
+ }
|
|
|
+ for (i = 0; i < mapped.length; i++) {
|
|
|
+ mapped[i].selected = selectedIds.indexOf(String(mapped[i].id)) >= 0
|
|
|
+ }
|
|
|
+ this.members = mapped
|
|
|
+ this.memberCount = mapped.length
|
|
|
+ this.emitChange()
|
|
|
+ this.$emit('loaded', { members: mapped, selectedIds: selectedIds })
|
|
|
+ return mapped.length
|
|
|
},
|
|
|
- // ——— 添加成员 ———
|
|
|
- goAddMember: function() {
|
|
|
- this.$emit('add-member')
|
|
|
- uni.navigateTo({ url: '/pages/family/add-member' })
|
|
|
+ loadMembers: function() {
|
|
|
+ if (this.loading) return
|
|
|
+ this.loading = true
|
|
|
+ var self = this
|
|
|
+ getFamilyMemberList({ visibleOnly: true }).then(function(res) {
|
|
|
+ var list = self.normalizeMemberList(res && res.data)
|
|
|
+ if (list.length > 0) return list
|
|
|
+ return getFamilyMemberList({ includeFamily: true }).then(function(res2) {
|
|
|
+ var list2 = self.normalizeMemberList(res2 && res2.data)
|
|
|
+ if (list2.length > 0) return list2
|
|
|
+ return getFamilyMemberList({}).then(function(res3) {
|
|
|
+ return self.normalizeMemberList(res3 && res3.data)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }).then(function(list) {
|
|
|
+ var count = self.applyList(list || [])
|
|
|
+ if (count === 0) {
|
|
|
+ uni.showToast({ title: '成员列表为空', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function(err) {
|
|
|
+ console.warn('[MemberChipPicker] loadMembers failed', err)
|
|
|
+ self.members = []
|
|
|
+ self.memberCount = 0
|
|
|
+ uni.showToast({ title: '成员加载失败', icon: 'none' })
|
|
|
+ }).then(function() {
|
|
|
+ self.loading = false
|
|
|
+ })
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.member-chip-picker {
|
|
|
+.member-scroll {
|
|
|
width: 100%;
|
|
|
+ white-space: nowrap;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
}
|
|
|
-.mcp-loading {
|
|
|
- padding: 30rpx 0;
|
|
|
- text-align: center;
|
|
|
-}
|
|
|
-.mcp-loading-text {
|
|
|
- font-size: 26rpx;
|
|
|
- color: #9CA3AF;
|
|
|
-}
|
|
|
-.mcp-chips {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- align-items: center;
|
|
|
+.member-chips {
|
|
|
+ display: inline-flex;
|
|
|
+ flex-direction: row;
|
|
|
+ padding: 4rpx 0;
|
|
|
}
|
|
|
-.mcp-chip {
|
|
|
- display: flex;
|
|
|
+.member-chip {
|
|
|
+ display: inline-flex;
|
|
|
align-items: center;
|
|
|
- padding: 12rpx 24rpx;
|
|
|
- margin: 0 16rpx 16rpx 0;
|
|
|
- background: #F3F4F6;
|
|
|
- border-radius: 999rpx;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 12rpx 28rpx;
|
|
|
+ margin-right: 16rpx;
|
|
|
+ border-radius: 32rpx;
|
|
|
+ background: #F1F5F9;
|
|
|
border: 2rpx solid transparent;
|
|
|
}
|
|
|
-.mcp-chip-on {
|
|
|
- background: #FFEDD5;
|
|
|
+.member-chip-on {
|
|
|
+ background: #FFF7ED;
|
|
|
border-color: #F97316;
|
|
|
}
|
|
|
-.mcp-chip-avatar {
|
|
|
- width: 40rpx;
|
|
|
- height: 40rpx;
|
|
|
- border-radius: 50%;
|
|
|
- margin-right: 10rpx;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-.mcp-chip-avatar-default {
|
|
|
- background: #E5E7EB;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
-}
|
|
|
-.mcp-chip-avatar-text {
|
|
|
- font-size: 22rpx;
|
|
|
- color: #6B7280;
|
|
|
-}
|
|
|
-.mcp-chip-text {
|
|
|
+.member-chip-text {
|
|
|
font-size: 26rpx;
|
|
|
- color: #374151;
|
|
|
+ color: #334155;
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
|
-.mcp-chip-on .mcp-chip-text {
|
|
|
- color: #C2410C;
|
|
|
- font-weight: 500;
|
|
|
+.member-chip-on .member-chip-text {
|
|
|
+ color: #F97316;
|
|
|
}
|
|
|
-.mcp-chip-add {
|
|
|
- background: transparent;
|
|
|
- border: 2rpx dashed #D1D5DB;
|
|
|
- color: #9CA3AF;
|
|
|
-}
|
|
|
-.mcp-chip-add-icon {
|
|
|
- font-size: 28rpx;
|
|
|
- margin-right: 6rpx;
|
|
|
- color: #9CA3AF;
|
|
|
+.member-load-hint {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #F97316;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
}
|
|
|
-.mcp-empty {
|
|
|
- padding: 40rpx 0;
|
|
|
+.empty-state {
|
|
|
+ width: 100%;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
+ padding: 40rpx 0 24rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
|
-.mcp-empty-text {
|
|
|
+.empty-text {
|
|
|
font-size: 26rpx;
|
|
|
- color: #9CA3AF;
|
|
|
- margin-bottom: 16rpx;
|
|
|
+ color: #999;
|
|
|
}
|
|
|
-.mcp-empty-btn {
|
|
|
- padding: 14rpx 40rpx;
|
|
|
- background: #F97316;
|
|
|
- color: #FFFFFF;
|
|
|
+.empty-action {
|
|
|
+ margin-top: 20rpx;
|
|
|
+ padding: 12rpx 32rpx;
|
|
|
font-size: 26rpx;
|
|
|
- border-radius: 40rpx;
|
|
|
+ color: #F97316;
|
|
|
+ border: 2rpx solid #F97316;
|
|
|
+ border-radius: 32rpx;
|
|
|
+}
|
|
|
+.reload-btn {
|
|
|
+ margin-top: 16rpx;
|
|
|
+ border-color: #999;
|
|
|
+ color: #666;
|
|
|
}
|
|
|
</style>
|