index.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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: currentFirstLevel === item.id || isFirstLevelAllSelected(item) }"
  10. @click="selectFirstLevel(item)"
  11. >
  12. <view class="item-content">
  13. <text class="category-name">{{ item.name }}</text>
  14. <text v-if="getSelectedChildCount(item) > 0" class="selected-count">
  15. ({{ getSelectedChildCount(item) }})
  16. </text>
  17. </view>
  18. <view class="checkbox">
  19. <text v-if="isFirstLevelAllSelected(item)" class="checked">✓</text>
  20. <text v-else-if="getSelectedChildCount(item) > 0" class="partial">-</text>
  21. <text v-else class="unchecked">○</text>
  22. </view>
  23. </view>
  24. </scroll-view>
  25. <!-- 二级分类 -->
  26. <scroll-view class="second-level" scroll-y v-if="hasSecondLevel">
  27. <view
  28. v-for="child in currentSecondLevel"
  29. :key="child.id"
  30. class="second-item"
  31. :class="{ active: selectedSecondLevel.includes(String(child.id)) }"
  32. @click="toggleSecondLevel(child)"
  33. >
  34. <text class="category-name">{{ child.name }}</text>
  35. <view class="checkbox">
  36. <text v-if="selectedSecondLevel.includes(child.id)" class="checked">✓</text>
  37. <text v-else class="unchecked">○</text>
  38. </view>
  39. </view>
  40. </scroll-view>
  41. </view>
  42. </template>
  43. <script setup>
  44. import {ref, computed, watch} from 'vue'
  45. const props = defineProps({
  46. categoryList: {
  47. type: Array,
  48. default: () => []
  49. },
  50. // 初始选中的分类ID数组
  51. selectedIds: {
  52. type: Array,
  53. default: () => []
  54. }
  55. })
  56. const emit = defineEmits(['change'])
  57. // 当前显示的一级分类ID(用于右侧显示)
  58. const currentFirstLevel = ref(null)
  59. // 选中的二级分类ID
  60. const selectedSecondLevel = ref([])
  61. // 当前显示的二级分类列表
  62. const currentSecondLevel = computed(() => {
  63. if (!currentFirstLevel.value) return []
  64. const parent = props.categoryList.find(item => item.id === currentFirstLevel.value)
  65. return parent?.child || []
  66. })
  67. // 是否有二级分类显示
  68. const hasSecondLevel = computed(() => currentSecondLevel.value.length > 0)
  69. // 获取一级分类下选中的子分类数量
  70. const getSelectedChildCount = (firstLevelItem) => {
  71. if (!firstLevelItem.child) return 0
  72. return firstLevelItem.child.filter(child =>
  73. selectedSecondLevel.value.includes(String(child.id)) // 转为字符串
  74. ).length
  75. }
  76. // 检查一级分类是否全部选中
  77. const isFirstLevelAllSelected = (firstLevelItem) => {
  78. if (!firstLevelItem.child || firstLevelItem.child.length === 0) {
  79. // 如果一级分类没有子分类,直接检查是否选中
  80. return selectedSecondLevel.value.includes(String(firstLevelItem.id)) // 转为字符串
  81. }
  82. const childCount = firstLevelItem.child.length
  83. const selectedCount = getSelectedChildCount(firstLevelItem)
  84. return childCount > 0 && selectedCount === childCount
  85. }
  86. // 选择一级分类(仅用于显示右侧内容)
  87. const selectFirstLevel = (item) => {
  88. if (item.child && item.child.length > 0) {
  89. currentFirstLevel.value = item.id
  90. } else {
  91. currentFirstLevel.value = null
  92. // 如果没有子分类,点击一级分类相当于选择/取消选择
  93. toggleSecondLevel(item)
  94. }
  95. }
  96. // 切换二级分类选中状态
  97. const toggleSecondLevel = (child) => {
  98. const childId = String(child.id)
  99. const index = selectedSecondLevel.value.indexOf(childId)
  100. if (index > -1) {
  101. // 取消选中
  102. selectedSecondLevel.value.splice(index, 1)
  103. } else {
  104. // 选中
  105. selectedSecondLevel.value.push(childId)
  106. }
  107. emitSelectionChange()
  108. }
  109. // 获取所有选中的分类ID(根据规则:全选父类才包含父类ID,否则只包含子类ID)
  110. const getSelectedIds = () => {
  111. const result = []
  112. // 遍历一级分类
  113. for (const category of props.categoryList) {
  114. const parentId = String(category.id)
  115. if (category.child && category.child.length > 0) {
  116. // 有子分类的情况
  117. const allChildIds = category.child.map(child => String(child.id))
  118. const selectedChildIds = allChildIds.filter(id =>
  119. selectedSecondLevel.value.includes(id)
  120. )
  121. if (selectedChildIds.length > 0) {
  122. // 只要有选中的子分类,就添加父类ID
  123. result.push(parentId)
  124. // 添加选中的子类ID
  125. selectedChildIds.forEach(id => {
  126. result.push(id)
  127. })
  128. }
  129. } else {
  130. // 没有子分类,直接检查是否选中
  131. if (selectedSecondLevel.value.includes(parentId)) {
  132. result.push(parentId)
  133. }
  134. }
  135. }
  136. console.log('计算出的选中IDs:', result)
  137. return result
  138. }
  139. // 发射选择变化事件
  140. const emitSelectionChange = () => {
  141. const selectedIds = getSelectedIds()
  142. emit('change', {
  143. selectedIds,
  144. secondLevel: selectedSecondLevel.value
  145. })
  146. }
  147. // 统一设置选中状态的方法
  148. const setSelectedState = (ids) => {
  149. selectedSecondLevel.value = []
  150. currentFirstLevel.value = null
  151. if (!ids || !Array.isArray(ids) || ids.length === 0) {
  152. initDefaultView()
  153. return
  154. }
  155. // 将传入的ID统一转换为字符串
  156. const stringIds = ids.map(id => String(id))
  157. // 首先处理所有选中的分类
  158. stringIds.forEach(id => {
  159. let found = false
  160. // 先查找二级分类
  161. for (const parent of props.categoryList) {
  162. if (parent.child) {
  163. const child = parent.child.find(c => String(c.id) === id)
  164. if (child) {
  165. if (!selectedSecondLevel.value.includes(id)) {
  166. selectedSecondLevel.value.push(id)
  167. }
  168. found = true
  169. break
  170. }
  171. }
  172. }
  173. // 如果没有找到对应的二级分类,检查是否是一级分类
  174. if (!found) {
  175. const firstLevel = props.categoryList.find(item => String(item.id) === id)
  176. if (firstLevel) {
  177. // 如果是一级分类,检查是否有子分类
  178. if (firstLevel.child && firstLevel.child.length > 0) {
  179. // 有子分类的一级分类:选中所有子分类
  180. firstLevel.child.forEach(child => {
  181. const childId = String(child.id)
  182. if (!selectedSecondLevel.value.includes(childId)) {
  183. selectedSecondLevel.value.push(childId)
  184. }
  185. })
  186. } else {
  187. // 没有子分类的一级分类:直接选中
  188. if (!selectedSecondLevel.value.includes(id)) {
  189. selectedSecondLevel.value.push(id)
  190. }
  191. }
  192. }
  193. }
  194. })
  195. console.log('设置后的选中子分类:', selectedSecondLevel.value)
  196. // 设置当前显示的选中项
  197. setCurrentView()
  198. // 触发选择变化事件
  199. emitSelectionChange()
  200. }
  201. // 设置当前显示视图
  202. const setCurrentView = () => {
  203. // 查找第一个有选中项的一级分类
  204. const firstWithSelected = props.categoryList.find(item => {
  205. if (item.child) {
  206. return item.child.some(child =>
  207. selectedSecondLevel.value.includes(String(child.id)) // 转为字符串
  208. )
  209. }
  210. return selectedSecondLevel.value.includes(String(item.id)) // 转为字符串
  211. })
  212. if (firstWithSelected) {
  213. currentFirstLevel.value = firstWithSelected.id
  214. } else {
  215. initDefaultView()
  216. }
  217. }
  218. // 初始化默认视图
  219. const initDefaultView = () => {
  220. const firstWithChildren = props.categoryList.find(item => item.child && item.child.length > 0)
  221. if (firstWithChildren) {
  222. currentFirstLevel.value = firstWithChildren.id
  223. }
  224. }
  225. // 清空所有选择
  226. const clearSelection = () => {
  227. selectedSecondLevel.value = []
  228. currentFirstLevel.value = null
  229. initDefaultView()
  230. emitSelectionChange()
  231. }
  232. // 设置选中状态的方法(暴露给父组件)
  233. const setSelectedIds = (ids) => {
  234. setSelectedState(ids)
  235. }
  236. // 监听props变化
  237. watch(() => props.selectedIds, (newVal) => {
  238. setSelectedState(newVal)
  239. }, {immediate: true})
  240. watch(() => props.categoryList, (newVal) => {
  241. if (newVal && newVal.length > 0) {
  242. // 重新应用选中状态
  243. setSelectedState(props.selectedIds)
  244. }
  245. })
  246. // 暴露方法给父组件
  247. defineExpose({
  248. getSelectedIds,
  249. clearSelection,
  250. setSelectedIds
  251. })
  252. </script>
  253. <style scoped>
  254. .category-selector {
  255. display: flex;
  256. height: 600rpx;
  257. border: 1rpx solid #eee;
  258. border-radius: 10rpx;
  259. }
  260. .first-level {
  261. width: 40%;
  262. background-color: #f8f8f8;
  263. }
  264. .second-level {
  265. width: 60%;
  266. background-color: #fff;
  267. }
  268. .first-item, .second-item {
  269. display: flex;
  270. align-items: center;
  271. justify-content: space-between;
  272. padding: 20rpx;
  273. border-bottom: 1rpx solid #eee;
  274. cursor: pointer;
  275. }
  276. .first-item.active {
  277. background-color: #fff;
  278. font-weight: bold;
  279. color: #F8C008;
  280. }
  281. .second-item.active {
  282. background-color: #f0f8ff;
  283. color: #F8C008;
  284. }
  285. .item-content {
  286. display: flex;
  287. align-items: center;
  288. flex: 1;
  289. }
  290. .category-name {
  291. font-size: 28rpx;
  292. }
  293. .selected-count {
  294. font-size: 24rpx;
  295. color: #F8C008;
  296. margin-left: 10rpx;
  297. }
  298. .checkbox {
  299. width: 40rpx;
  300. height: 40rpx;
  301. border-radius: 50%;
  302. display: flex;
  303. align-items: center;
  304. justify-content: center;
  305. }
  306. .checked {
  307. color: #F8C008;
  308. font-weight: bold;
  309. }
  310. .partial {
  311. color: #FFA500; /* 橙色表示部分选中 */
  312. font-weight: bold;
  313. }
  314. .unchecked {
  315. color: #ccc;
  316. }
  317. </style>