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(String(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. console.log("found",found,id)
  174. // 如果没有找到对应的二级分类,检查是否是一级分类
  175. if (!found) {
  176. const firstLevel = props.categoryList.find(item => String(item.id) == id)
  177. if (firstLevel) {
  178. // 如果是一级分类,检查是否有子分类
  179. if (firstLevel.child && firstLevel.child.length > 0) {
  180. // 有子分类的一级分类:选中所有子分类
  181. // firstLevel.child.forEach(child => {
  182. // const childId = String(child.id)
  183. // if (!selectedSecondLevel.value.includes(childId)) {
  184. // selectedSecondLevel.value.push(childId)
  185. // }
  186. // })
  187. } else {
  188. // 没有子分类的一级分类:直接选中
  189. if (!selectedSecondLevel.value.includes(id)) {
  190. selectedSecondLevel.value.push(id)
  191. }
  192. }
  193. }
  194. }
  195. })
  196. console.log('设置后的选中子分类:', selectedSecondLevel.value)
  197. // 设置当前显示的选中项
  198. setCurrentView()
  199. // 触发选择变化事件
  200. emitSelectionChange()
  201. }
  202. // 设置当前显示视图
  203. const setCurrentView = () => {
  204. // 查找第一个有选中项的一级分类
  205. const firstWithSelected = props.categoryList.find(item => {
  206. if (item.child) {
  207. return item.child.some(child =>
  208. selectedSecondLevel.value.includes(String(child.id)) // 转为字符串
  209. )
  210. }
  211. return selectedSecondLevel.value.includes(String(item.id)) // 转为字符串
  212. })
  213. if (firstWithSelected) {
  214. currentFirstLevel.value = firstWithSelected.id
  215. } else {
  216. initDefaultView()
  217. }
  218. }
  219. // 初始化默认视图
  220. const initDefaultView = () => {
  221. const firstWithChildren = props.categoryList.find(item => item.child && item.child.length > 0)
  222. if (firstWithChildren) {
  223. currentFirstLevel.value = firstWithChildren.id
  224. }
  225. }
  226. // 清空所有选择
  227. const clearSelection = () => {
  228. selectedSecondLevel.value = []
  229. currentFirstLevel.value = null
  230. initDefaultView()
  231. emitSelectionChange()
  232. }
  233. // 设置选中状态的方法(暴露给父组件)
  234. const setSelectedIds = (ids) => {
  235. setSelectedState(ids)
  236. }
  237. // 监听props变化
  238. watch(() => props.selectedIds, (newVal) => {
  239. setSelectedState(newVal)
  240. }, {immediate: true})
  241. watch(() => props.categoryList, (newVal) => {
  242. if (newVal && newVal.length > 0) {
  243. // 重新应用选中状态
  244. setSelectedState(props.selectedIds)
  245. }
  246. })
  247. // 暴露方法给父组件
  248. defineExpose({
  249. getSelectedIds,
  250. clearSelection,
  251. setSelectedIds
  252. })
  253. </script>
  254. <style scoped>
  255. .category-selector {
  256. display: flex;
  257. height: 600rpx;
  258. border: 1rpx solid #eee;
  259. border-radius: 10rpx;
  260. }
  261. .first-level {
  262. width: 40%;
  263. background-color: #f8f8f8;
  264. }
  265. .second-level {
  266. width: 60%;
  267. background-color: #fff;
  268. }
  269. .first-item, .second-item {
  270. display: flex;
  271. align-items: center;
  272. justify-content: space-between;
  273. padding: 20rpx;
  274. border-bottom: 1rpx solid #eee;
  275. cursor: pointer;
  276. }
  277. .first-item.active {
  278. background-color: #fff;
  279. font-weight: bold;
  280. color: #F8C008;
  281. }
  282. .second-item.active {
  283. background-color: #f0f8ff;
  284. color: #F8C008;
  285. }
  286. .item-content {
  287. display: flex;
  288. align-items: center;
  289. flex: 1;
  290. }
  291. .category-name {
  292. font-size: 28rpx;
  293. }
  294. .selected-count {
  295. font-size: 24rpx;
  296. color: #F8C008;
  297. margin-left: 10rpx;
  298. }
  299. .checkbox {
  300. width: 40rpx;
  301. height: 40rpx;
  302. border-radius: 50%;
  303. display: flex;
  304. align-items: center;
  305. justify-content: center;
  306. }
  307. .checked {
  308. color: #F8C008;
  309. font-weight: bold;
  310. }
  311. .partial {
  312. color: #FFA500; /* 橙色表示部分选中 */
  313. font-weight: bold;
  314. }
  315. .unchecked {
  316. color: #ccc;
  317. }
  318. </style>