MemberChipPicker.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. <template>
  2. <view class="member-picker">
  3. <text class="member-load-hint">成员数:{{ memberCount }}</text>
  4. <scroll-view class="member-scroll" scroll-x="true" :show-scrollbar="false" v-if="memberCount > 0">
  5. <view class="member-chips">
  6. <view
  7. v-for="item in members"
  8. :key="item.id"
  9. class="member-chip"
  10. :class="{ 'member-chip-on': item.selected }"
  11. @click="toggleMember(item.id)">
  12. <text class="member-chip-text">{{ item.nickname }}</text>
  13. </view>
  14. </view>
  15. </scroll-view>
  16. <view v-if="memberCount === 0 && !loading" class="empty-state">
  17. <text class="empty-text">暂无家庭成员,请先添加后再制定方案</text>
  18. <view class="empty-action" @click="goAddMember">去添加成员</view>
  19. <view class="empty-action reload-btn" @click="reload">重新加载</view>
  20. </view>
  21. <view v-if="loading" class="empty-state">
  22. <text class="empty-text">成员加载中...</text>
  23. </view>
  24. </view>
  25. </template>
  26. <script>
  27. import { getFamilyMemberList } from '@/utils/api.js'
  28. export default {
  29. name: 'MemberChipPicker',
  30. props: {
  31. selectedIds: {
  32. type: Array,
  33. default: function() { return [] }
  34. }
  35. },
  36. data: function() {
  37. return {
  38. members: [],
  39. memberCount: 0,
  40. loading: false
  41. }
  42. },
  43. mounted: function() {
  44. this.loadMembers()
  45. },
  46. methods: {
  47. reload: function() {
  48. this.loadMembers()
  49. },
  50. goAddMember: function() {
  51. this.$emit('add-member')
  52. uni.navigateTo({ url: '/pages/profile-extra/family-members' })
  53. },
  54. toggleMember: function(id) {
  55. var strId = String(id)
  56. var ids = []
  57. var i
  58. for (i = 0; i < this.members.length; i++) {
  59. if (this.members[i].selected) ids.push(String(this.members[i].id))
  60. }
  61. var idx = ids.indexOf(strId)
  62. if (idx >= 0) ids.splice(idx, 1)
  63. else ids.push(strId)
  64. this.applySelected(ids)
  65. this.emitChange()
  66. },
  67. applySelected: function(ids) {
  68. var selected = {}
  69. var i
  70. for (i = 0; i < ids.length; i++) {
  71. selected[String(ids[i])] = true
  72. }
  73. var list = []
  74. for (i = 0; i < this.members.length; i++) {
  75. var m = this.members[i]
  76. list.push({
  77. id: m.id,
  78. memberId: m.memberId,
  79. name: m.name,
  80. nickname: m.nickname,
  81. gender: m.gender || '',
  82. age: m.age != null ? m.age : null,
  83. selected: !!selected[String(m.id)]
  84. })
  85. }
  86. this.members = list
  87. this.memberCount = list.length
  88. },
  89. emitChange: function() {
  90. var ids = []
  91. var i
  92. for (i = 0; i < this.members.length; i++) {
  93. if (this.members[i].selected) ids.push(String(this.members[i].id))
  94. }
  95. this.$emit('change', { selectedIds: ids, members: this.members })
  96. },
  97. normalizeMemberList: function(data) {
  98. if (!data) return []
  99. var asList = function(v) {
  100. return v && typeof v === 'object' && typeof v.length === 'number' ? v : null
  101. }
  102. if (asList(data)) return data
  103. var sources = [data.children, data.members, data.list, data.records]
  104. var merged = []
  105. var seen = {}
  106. var s, i, m, mid, key
  107. for (s = 0; s < sources.length; s++) {
  108. var arr = asList(sources[s])
  109. if (!arr || arr.length === 0) continue
  110. for (i = 0; i < arr.length; i++) {
  111. m = arr[i]
  112. if (!m) continue
  113. mid = m.id != null ? m.id : (m.memberId != null ? m.memberId : m.userId)
  114. if (mid == null) continue
  115. key = String(mid)
  116. if (seen[key]) continue
  117. seen[key] = true
  118. merged.push(m)
  119. }
  120. }
  121. return merged
  122. },
  123. applyList: function(rawList) {
  124. var list = rawList || []
  125. var mapped = []
  126. var i, m, mid, nm
  127. for (i = 0; i < list.length; i++) {
  128. m = list[i]
  129. if (!m) continue
  130. mid = m.id != null ? m.id : (m.memberId != null ? m.memberId : m.userId)
  131. if (mid == null) continue
  132. nm = m.nickname || m.name || m.roleLabel || '成员'
  133. mapped.push({
  134. id: mid,
  135. memberId: String(mid),
  136. name: String(nm),
  137. nickname: String(nm),
  138. gender: m.gender || '',
  139. age: m.age != null ? m.age : null,
  140. selected: false
  141. })
  142. }
  143. var selectedIds = (this.selectedIds || []).map(function(id) { return String(id) })
  144. var idSet = {}
  145. for (i = 0; i < mapped.length; i++) idSet[String(mapped[i].id)] = true
  146. selectedIds = selectedIds.filter(function(id) { return !!idSet[String(id)] })
  147. if (selectedIds.length === 0 && mapped.length > 0) {
  148. selectedIds = [String(mapped[0].id)]
  149. }
  150. for (i = 0; i < mapped.length; i++) {
  151. mapped[i].selected = selectedIds.indexOf(String(mapped[i].id)) >= 0
  152. }
  153. this.members = mapped
  154. this.memberCount = mapped.length
  155. this.emitChange()
  156. this.$emit('loaded', { members: mapped, selectedIds: selectedIds })
  157. return mapped.length
  158. },
  159. loadMembers: function() {
  160. if (this.loading) return
  161. this.loading = true
  162. var self = this
  163. getFamilyMemberList({ visibleOnly: true }).then(function(res) {
  164. var list = self.normalizeMemberList(res && res.data)
  165. if (list.length > 0) return list
  166. return getFamilyMemberList({ includeFamily: true }).then(function(res2) {
  167. var list2 = self.normalizeMemberList(res2 && res2.data)
  168. if (list2.length > 0) return list2
  169. return getFamilyMemberList({}).then(function(res3) {
  170. return self.normalizeMemberList(res3 && res3.data)
  171. })
  172. })
  173. }).then(function(list) {
  174. var count = self.applyList(list || [])
  175. if (count === 0) {
  176. uni.showToast({ title: '成员列表为空', icon: 'none' })
  177. }
  178. }).catch(function(err) {
  179. console.warn('[MemberChipPicker] loadMembers failed', err)
  180. self.members = []
  181. self.memberCount = 0
  182. uni.showToast({ title: '成员加载失败', icon: 'none' })
  183. }).then(function() {
  184. self.loading = false
  185. })
  186. }
  187. }
  188. }
  189. </script>
  190. <style scoped>
  191. .member-scroll {
  192. width: 100%;
  193. white-space: nowrap;
  194. margin-bottom: 16rpx;
  195. }
  196. .member-chips {
  197. display: inline-flex;
  198. flex-direction: row;
  199. padding: 4rpx 0;
  200. }
  201. .member-chip {
  202. display: inline-flex;
  203. align-items: center;
  204. justify-content: center;
  205. padding: 12rpx 28rpx;
  206. margin-right: 16rpx;
  207. border-radius: 32rpx;
  208. background: #F1F5F9;
  209. border: 2rpx solid transparent;
  210. }
  211. .member-chip-on {
  212. background: #FFF7ED;
  213. border-color: #F97316;
  214. }
  215. .member-chip-text {
  216. font-size: 26rpx;
  217. color: #334155;
  218. font-weight: 600;
  219. }
  220. .member-chip-on .member-chip-text {
  221. color: #F97316;
  222. }
  223. .member-load-hint {
  224. font-size: 22rpx;
  225. color: #F97316;
  226. margin-bottom: 12rpx;
  227. }
  228. .empty-state {
  229. width: 100%;
  230. display: flex;
  231. flex-direction: column;
  232. align-items: center;
  233. padding: 40rpx 0 24rpx;
  234. box-sizing: border-box;
  235. }
  236. .empty-text {
  237. font-size: 26rpx;
  238. color: #999;
  239. }
  240. .empty-action {
  241. margin-top: 20rpx;
  242. padding: 12rpx 32rpx;
  243. font-size: 26rpx;
  244. color: #F97316;
  245. border: 2rpx solid #F97316;
  246. border-radius: 32rpx;
  247. }
  248. .reload-btn {
  249. margin-top: 16rpx;
  250. border-color: #999;
  251. color: #666;
  252. }
  253. </style>