index.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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. console.log('当前选中状态:', {
  112. allCategories: props.categoryList,
  113. selectedChildren: selectedSecondLevel.value
  114. })
  115. const result = []
  116. // 遍历一级分类
  117. for (const category of props.categoryList) {
  118. const parentId = String(category.id)
  119. if (category.child && category.child.length > 0) {
  120. // 有子分类的情况
  121. const allChildIds = category.child.map(child => String(child.id))
  122. const isAllSelected = allChildIds.every(id =>
  123. selectedSecondLevel.value.includes(id)
  124. )
  125. if (isAllSelected) {
  126. // 全选子类,只添加父类ID
  127. result.push(parentId)
  128. } else {
  129. // 非全选,添加选中的子类
  130. allChildIds.forEach(id => {
  131. if (selectedSecondLevel.value.includes(id)) {
  132. result.push(id)
  133. }
  134. })
  135. }
  136. } else {
  137. // 没有子分类,直接检查是否选中
  138. if (selectedSecondLevel.value.includes(parentId)) {
  139. result.push(parentId)
  140. }
  141. }
  142. }
  143. console.log('计算出的选中IDs:', result)
  144. return result
  145. }
  146. // 发射选择变化事件
  147. const emitSelectionChange = () => {
  148. const selectedIds = getSelectedIds()
  149. emit('change', {
  150. selectedIds,
  151. secondLevel: selectedSecondLevel.value
  152. })
  153. }
  154. // 统一设置选中状态的方法
  155. const setSelectedState = (ids) => {
  156. selectedSecondLevel.value = []
  157. currentFirstLevel.value = null
  158. console.log('设置选中IDs:', ids)
  159. console.log('可用分类数据:', props.categoryList)
  160. if (!ids || !Array.isArray(ids) || ids.length === 0) {
  161. initDefaultView()
  162. return
  163. }
  164. // 将传入的ID统一转换为字符串
  165. const stringIds = ids.map(id => String(id))
  166. // 首先处理所有选中的分类
  167. stringIds.forEach(id => {
  168. let found = false
  169. // 先查找二级分类
  170. for (const parent of props.categoryList) {
  171. if (parent.child) {
  172. const child = parent.child.find(c => String(c.id) === id)
  173. if (child) {
  174. if (!selectedSecondLevel.value.includes(id)) {
  175. selectedSecondLevel.value.push(id)
  176. }
  177. found = true
  178. break
  179. }
  180. }
  181. }
  182. // 如果没有找到对应的二级分类,检查是否是一级分类
  183. if (!found) {
  184. const firstLevel = props.categoryList.find(item => String(item.id) === id)
  185. if (firstLevel) {
  186. // 如果是一级分类,检查是否有子分类
  187. if (firstLevel.child && firstLevel.child.length > 0) {
  188. // 有子分类的一级分类:选中所有子分类
  189. firstLevel.child.forEach(child => {
  190. const childId = String(child.id)
  191. if (!selectedSecondLevel.value.includes(childId)) {
  192. selectedSecondLevel.value.push(childId)
  193. }
  194. })
  195. } else {
  196. // 没有子分类的一级分类:直接选中
  197. if (!selectedSecondLevel.value.includes(id)) {
  198. selectedSecondLevel.value.push(id)
  199. }
  200. }
  201. }
  202. }
  203. })
  204. console.log('设置后的选中子分类:', selectedSecondLevel.value)
  205. // 设置当前显示的选中项
  206. setCurrentView()
  207. // 触发选择变化事件
  208. emitSelectionChange()
  209. }
  210. // 设置当前显示视图
  211. const setCurrentView = () => {
  212. // 查找第一个有选中项的一级分类
  213. const firstWithSelected = props.categoryList.find(item => {
  214. if (item.child) {
  215. return item.child.some(child =>
  216. selectedSecondLevel.value.includes(String(child.id)) // 转为字符串
  217. )
  218. }
  219. return selectedSecondLevel.value.includes(String(item.id)) // 转为字符串
  220. })
  221. if (firstWithSelected) {
  222. currentFirstLevel.value = firstWithSelected.id
  223. } else {
  224. initDefaultView()
  225. }
  226. }
  227. // 初始化默认视图
  228. const initDefaultView = () => {
  229. const firstWithChildren = props.categoryList.find(item => item.child && item.child.length > 0)
  230. if (firstWithChildren) {
  231. currentFirstLevel.value = firstWithChildren.id
  232. }
  233. }
  234. // 清空所有选择
  235. const clearSelection = () => {
  236. selectedSecondLevel.value = []
  237. currentFirstLevel.value = null
  238. initDefaultView()
  239. emitSelectionChange()
  240. }
  241. // 设置选中状态的方法(暴露给父组件)
  242. const setSelectedIds = (ids) => {
  243. setSelectedState(ids)
  244. }
  245. // 监听props变化
  246. watch(() => props.selectedIds, (newVal) => {
  247. console.log('props.selectedIds 变化:', newVal)
  248. setSelectedState(newVal)
  249. }, {immediate: true})
  250. watch(() => props.categoryList, (newVal) => {
  251. console.log('分类数据变化:', newVal)
  252. if (newVal && newVal.length > 0) {
  253. // 重新应用选中状态
  254. setSelectedState(props.selectedIds)
  255. }
  256. })
  257. // 暴露方法给父组件
  258. defineExpose({
  259. getSelectedIds,
  260. clearSelection,
  261. setSelectedIds
  262. })
  263. </script>
  264. <style scoped>
  265. .category-selector {
  266. display: flex;
  267. height: 600rpx;
  268. border: 1rpx solid #eee;
  269. border-radius: 10rpx;
  270. }
  271. .first-level {
  272. width: 40%;
  273. background-color: #f8f8f8;
  274. }
  275. .second-level {
  276. width: 60%;
  277. background-color: #fff;
  278. }
  279. .first-item, .second-item {
  280. display: flex;
  281. align-items: center;
  282. justify-content: space-between;
  283. padding: 20rpx;
  284. border-bottom: 1rpx solid #eee;
  285. cursor: pointer;
  286. }
  287. .first-item.active {
  288. background-color: #fff;
  289. font-weight: bold;
  290. color: #F8C008;
  291. }
  292. .second-item.active {
  293. background-color: #f0f8ff;
  294. color: #F8C008;
  295. }
  296. .item-content {
  297. display: flex;
  298. align-items: center;
  299. flex: 1;
  300. }
  301. .category-name {
  302. font-size: 28rpx;
  303. }
  304. .selected-count {
  305. font-size: 24rpx;
  306. color: #F8C008;
  307. margin-left: 10rpx;
  308. }
  309. .checkbox {
  310. width: 40rpx;
  311. height: 40rpx;
  312. border-radius: 50%;
  313. display: flex;
  314. align-items: center;
  315. justify-content: center;
  316. }
  317. .checked {
  318. color: #F8C008;
  319. font-weight: bold;
  320. }
  321. .partial {
  322. color: #FFA500; /* 橙色表示部分选中 */
  323. font-weight: bold;
  324. }
  325. .unchecked {
  326. color: #ccc;
  327. }
  328. </style>