MemberChipPicker.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <template>
  2. <view class="member-chip-picker">
  3. <view v-if="loading" class="mcp-loading">
  4. <text class="mcp-loading-text">加载成员中...</text>
  5. </view>
  6. <view v-else-if="members.length > 0" class="mcp-chips">
  7. <view
  8. class="mcp-chip"
  9. v-for="(m, i) in members"
  10. :key="getMemberKey(m, i)"
  11. :class="{ 'mcp-chip-on': isSelected(m.memberId) }"
  12. @tap="toggleMember(m.memberId)"
  13. >
  14. <image
  15. v-if="m.avatar"
  16. class="mcp-chip-avatar"
  17. :src="m.avatar"
  18. mode="aspectFill"
  19. />
  20. <view v-else class="mcp-chip-avatar mcp-chip-avatar-default">
  21. <text class="mcp-chip-avatar-text">{{ getInitial(m) }}</text>
  22. </view>
  23. <text class="mcp-chip-text">{{ m.name || m.nickname || '成员' }}</text>
  24. </view>
  25. <view class="mcp-chip mcp-chip-add" @tap="goAddMember">
  26. <text class="mcp-chip-add-icon">+</text>
  27. <text class="mcp-chip-text">添加</text>
  28. </view>
  29. </view>
  30. <view v-else class="mcp-empty">
  31. <text class="mcp-empty-text">暂无家庭成员</text>
  32. <view class="mcp-empty-btn" @tap="goAddMember">+ 去添加成员</view>
  33. </view>
  34. </view>
  35. </template>
  36. <script>
  37. import { getFamilyMemberList } from '@/utils/api.js'
  38. export default {
  39. name: 'MemberChipPicker',
  40. props: {
  41. // 已选中的成员 id 数组(字符串)
  42. selectedIds: {
  43. type: Array,
  44. default: function() { return [] }
  45. }
  46. },
  47. data: function() {
  48. return {
  49. members: [],
  50. innerSelectedIds: [],
  51. loading: false,
  52. loaded: false
  53. }
  54. },
  55. watch: {
  56. selectedIds: {
  57. handler: function(val) {
  58. this.innerSelectedIds = (val || []).map(function(id) { return String(id) })
  59. },
  60. immediate: true
  61. }
  62. },
  63. created: function() {
  64. this.reload()
  65. },
  66. methods: {
  67. // ——— :key 方法(避免模板内运算符表达式) ———
  68. getMemberKey: function(m, i) {
  69. return String(m.memberId || m.id || i)
  70. },
  71. // ——— 首字(无头像兜底) ———
  72. getInitial: function(m) {
  73. var name = m.name || m.nickname || ''
  74. return name ? name.substring(0, 1) : '?'
  75. },
  76. isSelected: function(id) {
  77. return this.innerSelectedIds.indexOf(String(id)) >= 0
  78. },
  79. // ——— 加载家庭成员 ———
  80. reload: function() {
  81. var self = this
  82. this.loading = true
  83. getFamilyMemberList({ visibleOnly: true }).then(function(res) {
  84. self.loading = false
  85. if (res && res.code === 200 && res.data) {
  86. var raw = res.data instanceof Array ? res.data : ((res.data && res.data.list) || [])
  87. var list = []
  88. for (var i = 0; i < raw.length; i++) {
  89. var m = raw[i]
  90. // 同时输出 id/memberId、name/nickname,兼容父组件对成员字段的不同取法
  91. list.push({
  92. id: m.id,
  93. memberId: m.memberId !== undefined ? m.memberId : m.id,
  94. name: m.name || m.nickname,
  95. nickname: m.nickname || m.name,
  96. avatar: m.avatar || '',
  97. relationship: m.relationship || m.relativeLabel || m.roleLabel || ''
  98. })
  99. }
  100. self.members = list
  101. self.emitState()
  102. } else {
  103. self.members = []
  104. self.emitState()
  105. }
  106. }).catch(function() {
  107. self.loading = false
  108. self.members = []
  109. })
  110. },
  111. // ——— 切换选中态 ———
  112. toggleMember: function(id) {
  113. var sid = String(id)
  114. var idx = this.innerSelectedIds.indexOf(sid)
  115. if (idx >= 0) {
  116. this.innerSelectedIds.splice(idx, 1)
  117. } else {
  118. this.innerSelectedIds.push(sid)
  119. }
  120. this.emitState()
  121. },
  122. // ——— 通知父组件 ———
  123. emitState: function() {
  124. this.$emit('change', {
  125. selectedIds: this.innerSelectedIds.slice(),
  126. members: this.members.slice()
  127. })
  128. if (!this.loaded) {
  129. this.loaded = true
  130. this.$emit('loaded', {
  131. selectedIds: this.innerSelectedIds.slice(),
  132. members: this.members.slice()
  133. })
  134. }
  135. },
  136. // ——— 添加成员 ———
  137. goAddMember: function() {
  138. this.$emit('add-member')
  139. uni.navigateTo({ url: '/pages/family/add-member' })
  140. }
  141. }
  142. }
  143. </script>
  144. <style scoped>
  145. .member-chip-picker {
  146. width: 100%;
  147. }
  148. .mcp-loading {
  149. padding: 30rpx 0;
  150. text-align: center;
  151. }
  152. .mcp-loading-text {
  153. font-size: 26rpx;
  154. color: #9CA3AF;
  155. }
  156. .mcp-chips {
  157. display: flex;
  158. flex-wrap: wrap;
  159. align-items: center;
  160. }
  161. .mcp-chip {
  162. display: flex;
  163. align-items: center;
  164. padding: 12rpx 24rpx;
  165. margin: 0 16rpx 16rpx 0;
  166. background: #F3F4F6;
  167. border-radius: 999rpx;
  168. border: 2rpx solid transparent;
  169. }
  170. .mcp-chip-on {
  171. background: #FFEDD5;
  172. border-color: #F97316;
  173. }
  174. .mcp-chip-avatar {
  175. width: 40rpx;
  176. height: 40rpx;
  177. border-radius: 50%;
  178. margin-right: 10rpx;
  179. overflow: hidden;
  180. }
  181. .mcp-chip-avatar-default {
  182. background: #E5E7EB;
  183. display: flex;
  184. align-items: center;
  185. justify-content: center;
  186. }
  187. .mcp-chip-avatar-text {
  188. font-size: 22rpx;
  189. color: #6B7280;
  190. }
  191. .mcp-chip-text {
  192. font-size: 26rpx;
  193. color: #374151;
  194. }
  195. .mcp-chip-on .mcp-chip-text {
  196. color: #C2410C;
  197. font-weight: 500;
  198. }
  199. .mcp-chip-add {
  200. background: transparent;
  201. border: 2rpx dashed #D1D5DB;
  202. color: #9CA3AF;
  203. }
  204. .mcp-chip-add-icon {
  205. font-size: 28rpx;
  206. margin-right: 6rpx;
  207. color: #9CA3AF;
  208. }
  209. .mcp-empty {
  210. padding: 40rpx 0;
  211. display: flex;
  212. flex-direction: column;
  213. align-items: center;
  214. }
  215. .mcp-empty-text {
  216. font-size: 26rpx;
  217. color: #9CA3AF;
  218. margin-bottom: 16rpx;
  219. }
  220. .mcp-empty-btn {
  221. padding: 14rpx 40rpx;
  222. background: #F97316;
  223. color: #FFFFFF;
  224. font-size: 26rpx;
  225. border-radius: 40rpx;
  226. }
  227. </style>