MemberChipPicker.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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. selected: !!selected[String(m.id)]
  82. })
  83. }
  84. this.members = list
  85. this.memberCount = list.length
  86. },
  87. emitChange: function() {
  88. var ids = []
  89. var i
  90. for (i = 0; i < this.members.length; i++) {
  91. if (this.members[i].selected) ids.push(String(this.members[i].id))
  92. }
  93. this.$emit('change', { selectedIds: ids, members: this.members })
  94. },
  95. normalizeMemberList: function(data) {
  96. if (!data) return []
  97. var asList = function(v) {
  98. return v && typeof v === 'object' && typeof v.length === 'number' ? v : null
  99. }
  100. if (asList(data)) return data
  101. var sources = [data.children, data.members, data.list, data.records]
  102. var merged = []
  103. var seen = {}
  104. var s, i, m, mid, key
  105. for (s = 0; s < sources.length; s++) {
  106. var arr = asList(sources[s])
  107. if (!arr || arr.length === 0) continue
  108. for (i = 0; i < arr.length; i++) {
  109. m = arr[i]
  110. if (!m) continue
  111. mid = m.id != null ? m.id : (m.memberId != null ? m.memberId : m.userId)
  112. if (mid == null) continue
  113. key = String(mid)
  114. if (seen[key]) continue
  115. seen[key] = true
  116. merged.push(m)
  117. }
  118. }
  119. return merged
  120. },
  121. applyList: function(rawList) {
  122. var list = rawList || []
  123. var mapped = []
  124. var i, m, mid, nm
  125. for (i = 0; i < list.length; i++) {
  126. m = list[i]
  127. if (!m) continue
  128. mid = m.id != null ? m.id : (m.memberId != null ? m.memberId : m.userId)
  129. if (mid == null) continue
  130. nm = m.nickname || m.name || m.roleLabel || '成员'
  131. mapped.push({
  132. id: mid,
  133. memberId: String(mid),
  134. name: String(nm),
  135. nickname: String(nm),
  136. selected: false
  137. })
  138. }
  139. var selectedIds = (this.selectedIds || []).map(function(id) { return String(id) })
  140. var idSet = {}
  141. for (i = 0; i < mapped.length; i++) idSet[String(mapped[i].id)] = true
  142. selectedIds = selectedIds.filter(function(id) { return !!idSet[String(id)] })
  143. if (selectedIds.length === 0 && mapped.length > 0) {
  144. selectedIds = [String(mapped[0].id)]
  145. }
  146. for (i = 0; i < mapped.length; i++) {
  147. mapped[i].selected = selectedIds.indexOf(String(mapped[i].id)) >= 0
  148. }
  149. this.members = mapped
  150. this.memberCount = mapped.length
  151. this.emitChange()
  152. this.$emit('loaded', { members: mapped, selectedIds: selectedIds })
  153. return mapped.length
  154. },
  155. loadMembers: function() {
  156. if (this.loading) return
  157. this.loading = true
  158. var self = this
  159. getFamilyMemberList({ visibleOnly: true }).then(function(res) {
  160. var list = self.normalizeMemberList(res && res.data)
  161. if (list.length > 0) return list
  162. return getFamilyMemberList({ includeFamily: true }).then(function(res2) {
  163. var list2 = self.normalizeMemberList(res2 && res2.data)
  164. if (list2.length > 0) return list2
  165. return getFamilyMemberList({}).then(function(res3) {
  166. return self.normalizeMemberList(res3 && res3.data)
  167. })
  168. })
  169. }).then(function(list) {
  170. var count = self.applyList(list || [])
  171. if (count === 0) {
  172. uni.showToast({ title: '成员列表为空', icon: 'none' })
  173. }
  174. }).catch(function(err) {
  175. console.warn('[MemberChipPicker] loadMembers failed', err)
  176. self.members = []
  177. self.memberCount = 0
  178. uni.showToast({ title: '成员加载失败', icon: 'none' })
  179. }).then(function() {
  180. self.loading = false
  181. })
  182. }
  183. }
  184. }
  185. </script>
  186. <style scoped>
  187. .member-scroll {
  188. width: 100%;
  189. white-space: nowrap;
  190. margin-bottom: 16rpx;
  191. }
  192. .member-chips {
  193. display: inline-flex;
  194. flex-direction: row;
  195. padding: 4rpx 0;
  196. }
  197. .member-chip {
  198. display: inline-flex;
  199. align-items: center;
  200. justify-content: center;
  201. padding: 12rpx 28rpx;
  202. margin-right: 16rpx;
  203. border-radius: 32rpx;
  204. background: #F1F5F9;
  205. border: 2rpx solid transparent;
  206. }
  207. .member-chip-on {
  208. background: #FFF7ED;
  209. border-color: #F97316;
  210. }
  211. .member-chip-text {
  212. font-size: 26rpx;
  213. color: #334155;
  214. font-weight: 600;
  215. }
  216. .member-chip-on .member-chip-text {
  217. color: #F97316;
  218. }
  219. .member-load-hint {
  220. font-size: 22rpx;
  221. color: #F97316;
  222. margin-bottom: 12rpx;
  223. }
  224. .empty-state {
  225. width: 100%;
  226. display: flex;
  227. flex-direction: column;
  228. align-items: center;
  229. padding: 40rpx 0 24rpx;
  230. box-sizing: border-box;
  231. }
  232. .empty-text {
  233. font-size: 26rpx;
  234. color: #999;
  235. }
  236. .empty-action {
  237. margin-top: 20rpx;
  238. padding: 12rpx 32rpx;
  239. font-size: 26rpx;
  240. color: #F97316;
  241. border: 2rpx solid #F97316;
  242. border-radius: 32rpx;
  243. }
  244. .reload-btn {
  245. margin-top: 16rpx;
  246. border-color: #999;
  247. color: #666;
  248. }
  249. </style>