index.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. <template>
  2. <view class="category-selector">
  3. <!-- 一级分类 -->
  4. <scroll-view class="first-level" scroll-y>
  5. <view
  6. v-for="item in categoryList"
  7. :key="item.id"
  8. class="first-item"
  9. :class="{ active: selectedFirstLevel.includes(item.id) }"
  10. @click="toggleFirstLevel(item)"
  11. >
  12. <view class="item-content">
  13. <!-- <image-->
  14. <!-- v-if="item.extra"-->
  15. <!-- :src="item.extra"-->
  16. <!-- class="category-icon"-->
  17. <!-- mode="aspectFit"-->
  18. <!-- />-->
  19. <text class="category-name">{{ item.name }}</text>
  20. </view>
  21. <view class="checkbox">
  22. <text v-if="selectedFirstLevel.includes(item.id)" class="checked">✓</text>
  23. <text v-else class="unchecked">○</text>
  24. </view>
  25. </view>
  26. </scroll-view>
  27. <!-- 二级分类 -->
  28. <scroll-view class="second-level" scroll-y v-if="hasSecondLevel">
  29. <view
  30. v-for="child in currentSecondLevel"
  31. :key="child.id"
  32. class="second-item"
  33. :class="{ active: selectedSecondLevel.includes(child.id) }"
  34. @click="toggleSecondLevel(child)"
  35. >
  36. <text class="category-name">{{ child.name }}</text>
  37. <view class="checkbox">
  38. <text v-if="selectedSecondLevel.includes(child.id)" class="checked">✓</text>
  39. <text v-else class="unchecked">○</text>
  40. </view>
  41. </view>
  42. </scroll-view>
  43. </view>
  44. </template>
  45. <script setup>
  46. import { ref, computed, watch } from 'vue'
  47. const props = defineProps({
  48. categoryList: {
  49. type: Array,
  50. default: () => []
  51. },
  52. // 初始选中的分类ID数组
  53. selectedIds: {
  54. type: Array,
  55. default: () => []
  56. }
  57. })
  58. const emit = defineEmits(['change'])
  59. // 选中的一级分类ID
  60. const selectedFirstLevel = ref([])
  61. // 选中的二级分类ID
  62. const selectedSecondLevel = ref([])
  63. // 当前显示二级分类的一级分类
  64. const currentFirstLevel = ref(null)
  65. // 当前显示的二级分类列表
  66. const currentSecondLevel = computed(() => {
  67. if (!currentFirstLevel.value) return []
  68. const parent = props.categoryList.find(item => item.id === currentFirstLevel.value)
  69. return parent?.child || []
  70. })
  71. // 是否有二级分类显示
  72. const hasSecondLevel = computed(() => currentSecondLevel.value.length > 0)
  73. // 初始化选中状态
  74. const initSelected = () => {
  75. selectedFirstLevel.value = []
  76. selectedSecondLevel.value = []
  77. props.selectedIds.forEach(id => {
  78. // 检查是否是一级分类
  79. const firstLevel = props.categoryList.find(item => item.id === id)
  80. if (firstLevel) {
  81. selectedFirstLevel.value.push(id)
  82. }
  83. // 检查是否是二级分类
  84. for (const parent of props.categoryList) {
  85. if (parent.child) {
  86. const secondLevel = parent.child.find(child => child.id === id)
  87. if (secondLevel) {
  88. selectedSecondLevel.value.push(id)
  89. // 如果选中了二级分类,自动选中对应的一级分类
  90. if (!selectedFirstLevel.value.includes(parent.id)) {
  91. selectedFirstLevel.value.push(parent.id)
  92. }
  93. }
  94. }
  95. }
  96. })
  97. }
  98. // 切换一级分类选中状态
  99. const toggleFirstLevel = (item) => {
  100. const index = selectedFirstLevel.value.indexOf(item.id)
  101. if (index > -1) {
  102. // 取消选中
  103. selectedFirstLevel.value.splice(index, 1)
  104. // 如果取消选中一级分类,同时取消选中其下的所有二级分类
  105. if (item.child) {
  106. item.child.forEach(child => {
  107. const childIndex = selectedSecondLevel.value.indexOf(child.id)
  108. if (childIndex > -1) {
  109. selectedSecondLevel.value.splice(childIndex, 1)
  110. }
  111. })
  112. }
  113. } else {
  114. // 选中
  115. selectedFirstLevel.value.push(item.id)
  116. }
  117. // 设置当前显示的一级分类
  118. if (item.child && item.child.length > 0) {
  119. currentFirstLevel.value = item.id
  120. } else {
  121. currentFirstLevel.value = null
  122. }
  123. emitSelectionChange()
  124. }
  125. // 切换二级分类选中状态
  126. const toggleSecondLevel = (child) => {
  127. const index = selectedSecondLevel.value.indexOf(child.id)
  128. if (index > -1) {
  129. selectedSecondLevel.value.splice(index, 1)
  130. } else {
  131. selectedSecondLevel.value.push(child.id)
  132. // 如果选中二级分类,确保对应的一级分类也被选中
  133. const parentId = getParentId(child.id)
  134. if (parentId && !selectedFirstLevel.value.includes(parentId)) {
  135. selectedFirstLevel.value.push(parentId)
  136. }
  137. }
  138. emitSelectionChange()
  139. }
  140. // 根据二级分类ID获取一级分类ID
  141. const getParentId = (childId) => {
  142. for (const parent of props.categoryList) {
  143. if (parent.child) {
  144. const child = parent.child.find(item => item.id === childId)
  145. if (child) return parent.id
  146. }
  147. }
  148. return null
  149. }
  150. // 发射选择变化事件
  151. const emitSelectionChange = () => {
  152. const selectedIds = [...selectedFirstLevel.value, ...selectedSecondLevel.value]
  153. emit('change', {
  154. selectedIds,
  155. firstLevel: selectedFirstLevel.value,
  156. secondLevel: selectedSecondLevel.value
  157. })
  158. }
  159. // 获取所有选中的分类ID
  160. const getSelectedIds = () => {
  161. return [...selectedFirstLevel.value, ...selectedSecondLevel.value]
  162. }
  163. // 清空所有选择
  164. const clearSelection = () => {
  165. selectedFirstLevel.value = []
  166. selectedSecondLevel.value = []
  167. currentFirstLevel.value = null
  168. emitSelectionChange()
  169. }
  170. // 监听props变化
  171. watch(() => props.selectedIds, (newVal) => {
  172. initSelected()
  173. })
  174. watch(() => props.categoryList, (newVal) => {
  175. initSelected()
  176. })
  177. // 初始化
  178. initSelected()
  179. // 暴露方法给父组件
  180. defineExpose({
  181. getSelectedIds,
  182. clearSelection
  183. })
  184. </script>
  185. <style scoped>
  186. .category-selector {
  187. display: flex;
  188. height: 600rpx;
  189. border: 1rpx solid #eee;
  190. border-radius: 10rpx;
  191. }
  192. .first-level {
  193. width: 40%;
  194. background-color: #f8f8f8;
  195. }
  196. .second-level {
  197. width: 60%;
  198. background-color: #fff;
  199. }
  200. .first-item, .second-item {
  201. display: flex;
  202. align-items: center;
  203. justify-content: space-between;
  204. padding: 20rpx;
  205. border-bottom: 1rpx solid #eee;
  206. }
  207. .first-item.active {
  208. background-color: #fff;
  209. font-weight: bold;
  210. color: #F8C008;
  211. }
  212. .second-item.active {
  213. background-color: #f0f8ff;
  214. color: #F8C008;
  215. }
  216. .item-content {
  217. display: flex;
  218. align-items: center;
  219. flex: 1;
  220. }
  221. .category-icon {
  222. width: 40rpx;
  223. height: 40rpx;
  224. margin-right: 15rpx;
  225. }
  226. .category-name {
  227. font-size: 28rpx;
  228. }
  229. .checkbox {
  230. width: 40rpx;
  231. height: 40rpx;
  232. border-radius: 50%;
  233. display: flex;
  234. align-items: center;
  235. justify-content: center;
  236. }
  237. .checked {
  238. color: #F8C008;
  239. font-weight: bold;
  240. }
  241. .unchecked {
  242. color: #ccc;
  243. }
  244. </style>