index.vue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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 }"
  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(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(child.id)
  74. ).length
  75. }
  76. // 检查一级分类是否全部选中
  77. const isFirstLevelAllSelected = (firstLevelItem) => {
  78. if (!firstLevelItem.child || firstLevelItem.child.length === 0) {
  79. // 如果一级分类没有子分类,直接检查是否选中
  80. return selectedSecondLevel.value.includes(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 index = selectedSecondLevel.value.indexOf(child.id)
  99. if (index > -1) {
  100. // 取消选中
  101. selectedSecondLevel.value.splice(index, 1)
  102. } else {
  103. // 选中
  104. selectedSecondLevel.value.push(child.id)
  105. }
  106. emitSelectionChange()
  107. }
  108. // 获取所有选中的分类ID(根据规则:全选父类才包含父类ID,否则只包含子类ID)
  109. const getSelectedIds = () => {
  110. const selectedIds = [...selectedSecondLevel.value]
  111. // 遍历所有一级分类,检查是否需要添加一级分类ID
  112. props.categoryList.forEach(firstLevel => {
  113. if (isFirstLevelAllSelected(firstLevel)) {
  114. // 如果全部子类选中,添加一级分类ID,并移除所有子分类ID
  115. selectedIds.push(firstLevel.id)
  116. // 移除该一级分类下的所有子分类ID
  117. if (firstLevel.child) {
  118. firstLevel.child.forEach(child => {
  119. const childIndex = selectedIds.indexOf(child.id)
  120. if (childIndex > -1) {
  121. selectedIds.splice(childIndex, 1)
  122. }
  123. })
  124. }
  125. }
  126. })
  127. return selectedIds
  128. }
  129. // 发射选择变化事件
  130. const emitSelectionChange = () => {
  131. const selectedIds = getSelectedIds()
  132. emit('change', {
  133. selectedIds,
  134. secondLevel: selectedSecondLevel.value
  135. })
  136. }
  137. // 初始化选中状态
  138. const initSelected = () => {
  139. selectedSecondLevel.value = [];
  140. currentFirstLevel.value = null;
  141. console.log('初始化选中IDs:', props.selectedIds);
  142. console.log('分类数据:', props.categoryList);
  143. if (!props.selectedIds || !Array.isArray(props.selectedIds) || props.selectedIds.length === 0) {
  144. // 设置默认显示
  145. const firstWithChildren = props.categoryList.find(item => item.child && item.child.length > 0);
  146. if (firstWithChildren) {
  147. currentFirstLevel.value = firstWithChildren.id;
  148. }
  149. return;
  150. }
  151. props.selectedIds.forEach(id => {
  152. // 确保id是字符串类型进行比较
  153. const idStr = String(id);
  154. // 检查是否是一级分类(表示该分类下的所有子分类都被选中)
  155. const firstLevel = props.categoryList.find(item => String(item.id) === idStr);
  156. if (firstLevel && firstLevel.child) {
  157. // 如果是一级分类且全选,选中所有子分类
  158. firstLevel.child.forEach(child => {
  159. const childIdStr = String(child.id);
  160. if (!selectedSecondLevel.value.includes(childIdStr)) {
  161. selectedSecondLevel.value.push(childIdStr);
  162. }
  163. });
  164. } else {
  165. // 如果是二级分类或没有子分类的一级分类,直接选中
  166. if (!selectedSecondLevel.value.includes(idStr)) {
  167. selectedSecondLevel.value.push(idStr);
  168. }
  169. }
  170. });
  171. console.log('初始化后的选中子分类:', selectedSecondLevel.value);
  172. // 设置默认显示的第一个有选中项的一级分类
  173. const firstWithSelected = props.categoryList.find(item => {
  174. if (item.child) {
  175. return item.child.some(child => selectedSecondLevel.value.includes(String(child.id)));
  176. }
  177. return selectedSecondLevel.value.includes(String(item.id));
  178. });
  179. if (firstWithSelected) {
  180. currentFirstLevel.value = firstWithSelected.id;
  181. } else {
  182. const firstWithChildren = props.categoryList.find(item => item.child && item.child.length > 0);
  183. if (firstWithChildren) {
  184. currentFirstLevel.value = firstWithChildren.id;
  185. }
  186. }
  187. }
  188. // 清空所有选择
  189. const clearSelection = () => {
  190. selectedSecondLevel.value = []
  191. currentFirstLevel.value = null
  192. emitSelectionChange()
  193. }
  194. // 设置选中状态的方法
  195. const setSelectedIds = (ids) => {
  196. selectedSecondLevel.value = [];
  197. currentFirstLevel.value = null;
  198. console.log('设置选中IDs:', ids);
  199. console.log('可用分类列表:', props.categoryList);
  200. if (!ids || !Array.isArray(ids) || ids.length === 0) return;
  201. ids.forEach(id => {
  202. // 检查是否是一级分类(表示该分类下的所有子分类都被选中)
  203. const firstLevel = props.categoryList.find(item => item.id == id);
  204. if (firstLevel && firstLevel.child) {
  205. // 如果是一级分类且全选,选中所有子分类
  206. firstLevel.child.forEach(child => {
  207. if (!selectedSecondLevel.value.includes(child.id)) {
  208. selectedSecondLevel.value.push(child.id);
  209. }
  210. });
  211. } else {
  212. // 如果是二级分类或没有子分类的一级分类,直接选中
  213. // 确保id是字符串类型(因为分类ID可能是字符串或数字)
  214. const idStr = String(id);
  215. if (!selectedSecondLevel.value.includes(idStr)) {
  216. selectedSecondLevel.value.push(idStr);
  217. }
  218. }
  219. });
  220. console.log('设置后的选中子分类:', selectedSecondLevel.value);
  221. // 设置默认显示的第一个有选中项的一级分类
  222. const firstWithSelected = props.categoryList.find(item => {
  223. if (item.child) {
  224. return item.child.some(child => selectedSecondLevel.value.includes(child.id));
  225. }
  226. return selectedSecondLevel.value.includes(item.id);
  227. });
  228. if (firstWithSelected) {
  229. currentFirstLevel.value = firstWithSelected.id;
  230. } else {
  231. // 如果没有选中项,显示第一个有子分类的一级分类
  232. const firstWithChildren = props.categoryList.find(item => item.child && item.child.length > 0);
  233. if (firstWithChildren) {
  234. currentFirstLevel.value = firstWithChildren.id;
  235. }
  236. }
  237. // 触发选择变化事件
  238. emitSelectionChange();
  239. };
  240. // 监听props变化
  241. watch(() => props.selectedIds, (newVal) => {
  242. initSelected()
  243. }, {immediate: true})
  244. watch(() => props.categoryList, (newVal) => {
  245. if (newVal && newVal.length > 0) {
  246. initSelected()
  247. }
  248. })
  249. // 暴露方法给父组件
  250. defineExpose({
  251. getSelectedIds,
  252. clearSelection,
  253. setSelectedIds
  254. })
  255. </script>
  256. <style scoped>
  257. .category-selector {
  258. display: flex;
  259. height: 600rpx;
  260. border: 1rpx solid #eee;
  261. border-radius: 10rpx;
  262. }
  263. .first-level {
  264. width: 40%;
  265. background-color: #f8f8f8;
  266. }
  267. .second-level {
  268. width: 60%;
  269. background-color: #fff;
  270. }
  271. .first-item, .second-item {
  272. display: flex;
  273. align-items: center;
  274. justify-content: space-between;
  275. padding: 20rpx;
  276. border-bottom: 1rpx solid #eee;
  277. cursor: pointer;
  278. }
  279. .first-item.active {
  280. background-color: #fff;
  281. font-weight: bold;
  282. color: #F8C008;
  283. }
  284. .second-item.active {
  285. background-color: #f0f8ff;
  286. color: #F8C008;
  287. }
  288. .item-content {
  289. display: flex;
  290. align-items: center;
  291. flex: 1;
  292. }
  293. .category-name {
  294. font-size: 28rpx;
  295. }
  296. .selected-count {
  297. font-size: 24rpx;
  298. color: #F8C008;
  299. margin-left: 10rpx;
  300. }
  301. .checkbox {
  302. width: 40rpx;
  303. height: 40rpx;
  304. border-radius: 50%;
  305. display: flex;
  306. align-items: center;
  307. justify-content: center;
  308. }
  309. .checked {
  310. color: #F8C008;
  311. font-weight: bold;
  312. }
  313. .partial {
  314. color: #FFA500; /* 橙色表示部分选中 */
  315. font-weight: bold;
  316. }
  317. .unchecked {
  318. color: #ccc;
  319. }
  320. </style>