[关闭]
@gyyin 2020-02-06T15:10:46.000000Z 字数 3717 阅读 437

携程酒店列表页store设计

工作


业务功能

筛选项

IBU的筛选项看起来和2.0有很大不同,主要体现在UI上面,然而从逻辑上来看,两者几乎保持一致。
image.png-188kB

首先,Sort和原来的排序保持一致,都是一层单选项,Area和原来的位置区域一样,两层筛选,而Filter和原来的都不一样,Filter包括了滑动条、星级选择以及单选项和多选项。

如果还原成2.0的样子,Filter实际上是三层筛选,一级筛选应该有价格、星级、含早、床型,二级筛选则是价格分类(含出租车和小费、不含出租车和小费等等),星级(一星、二星、三星),是否含早(含早、不含早)、床型(大床、双床、三床),三级筛选则是价格分类下的不同价格、不同品牌分类下的不同品牌的酒店。
image.png-125.5kB

这也意味着Filter筛选项至少有一级(星级、是否含早等),最多有三级(价格、品牌等等),底部的show results和右上角的clear与原来2.0的确认恢复默认一致。

快捷筛选项

快捷筛选项需要和筛选项进行联动,也有单选和多选两种形式。

筛选项联动

如果选择某一分类下的筛选项,那么需要和相同分类的快捷筛选项联动(默认选中),反之亦然。

列表卡片

列表卡片需要展示一些基本信息,点击卡片可以跳转至详情页。

store设计

按照业务逻辑划分store,可以选择将action放入对应store中,也可以单独将action拆分出来,但是这里的筛选项差异较大,交互也比较复杂,也很容易导致model互相耦合,不适合采用前者的方式,所以这里我选择把action拆分出来,只传入需要的store,类似于redux的action和reducer。

这个store的设计和原来的hotelList store设计没有太大区别,主要划分为这么几个大类:

  1. hotelList
  2. params
  3. historyList
  4. filterList
  5. quickFilterList
  6. pageState
  7. selectedFilterList
    image.png-209.6kB
    action的设计如下:
    image.png-92.4kB

其他交互的实现都很简单,这里主要讲一下筛选项和快捷筛选项的设计。

筛选项

因为筛选项和快捷筛选项需要联动,这里不适合给筛选项添加selected标识,所以需要在store中存一份已选的数据,在渲染的时候映射到对应的筛选项or快捷筛选项。
筛选项只有在选择后并点击确认按钮才会存入store,如果没有点击确认按钮,那么就不会存入store,所以选中的筛选项状态在组件内部用state来保存,一旦点击恢复默认按钮就clear state。

store

这里的筛选项数据结构如下:

  1. const store = {
  2. // 互斥筛选项类
  3. mutualFilters: {
  4. "filter": [["price", "brand"]],
  5. "area": [],
  6. "sort": []
  7. },
  8. // class和category保证能更快查找到
  9. // 快捷筛选项中的class可以将key以"-"分割后的数组长度来判断是在categories、filterItems还是filterLastItems里面
  10. // isQuick用来判断带给酒店详情页的筛选项是快捷还是普通
  11. // observable监听selectedFilter,每次变化时在reaction里面调用接口
  12. selectedFilter: [{
  13. key: "filter-price-1",
  14. category: "filter",
  15. class: "filterItems",
  16. isQuick: true,
  17. isRadio: true,
  18. name: "虹桥火车站"
  19. }, {
  20. key: "sort-1",
  21. class: "categories",
  22. category: "sort",
  23. isRadio: true,
  24. name: "按价格排序"
  25. }],
  26. filterList: {
  27. "filter": {
  28. name: "filter",
  29. items: ["filter-price", "filter-brand", "filter-star"]
  30. },
  31. "area" : {
  32. name: "area",
  33. items: ["area-1", "area-2", "area-3"]
  34. },
  35. // sort需进行特殊处理
  36. "sort": {
  37. name: "sort",
  38. items: ["sort-1", "sort-2", "sort-3"]
  39. }
  40. },
  41. // 保证快捷筛选项的顺序
  42. quickFilterSort: ["filter-bed-1", "filter-price-2"],
  43. entities: {
  44. "quickFilter": {
  45. "filter-bed-1": {
  46. isRadio: true,
  47. key: "filter-bed-1",
  48. name: "大床",
  49. value: 1
  50. }
  51. },
  52. "activeFilter": "filter",
  53. // 如果items为空,意味着当前已经是最后一层了,没有子筛选项了
  54. "categories": {
  55. "filter": {
  56. "filter-price": {
  57. categoryName: "price",
  58. name: "价格",
  59. key: "filter-price",
  60. category: "filter",
  61. class: "categories",
  62. id: "1",
  63. items: ["filter-price-1", "filter-price-2", "filter-price-3"]
  64. },
  65. "filter-brand": {
  66. categoryName: "brand",
  67. "name": "品牌",
  68. category: "star",
  69. class: "categories",
  70. key: "filter-brand",
  71. id: "2",
  72. items: ["filter-brand-1", "filter-brand-2", "filter-brand-3"]
  73. },
  74. "filter-star": {
  75. categoryName: "star",
  76. "name": "星级",
  77. category: "star",
  78. class: "categories",
  79. key: "filter-star",
  80. id: "3",
  81. items: ["filter-star-1", "filter-star-2", "filter-star-3"]
  82. },
  83. },
  84. "area": {
  85. "area-1": {
  86. key: "area-1",
  87. id: "1",
  88. category: "area",
  89. class: "categories",
  90. items: ["area-1-1", "area-1-2", "area-1-3"]
  91. },
  92. "area-2": {
  93. key: "area-2",
  94. id: "2",
  95. category: "area",
  96. class: "categories",
  97. items: ["area-2-1", "area-2-2", "area-2-3"]
  98. },
  99. "area-3": {
  100. key: "area-3",
  101. id: "3",
  102. category: "area",
  103. class: "categories",
  104. items: ["area-3-1", "area-3-2", "area-3-3"]
  105. }
  106. },
  107. "sort": {
  108. "sort-1": {
  109. key: "sort-1",
  110. category: "sort",
  111. class: "categories",
  112. name: "按照价格排序",
  113. isRadio: true,
  114. id: "2",
  115. items: []
  116. }
  117. }
  118. },
  119. "filterItems": {
  120. "filter-price-1": {
  121. key: "filter-price-1",
  122. name: 500,
  123. category: "filter",
  124. class: "filterItems",
  125. isRadio: true,
  126. items: []
  127. },
  128. "area-1-1": {
  129. key: "filter-price-1",
  130. category: "area",
  131. class: "filterItems",
  132. isRadio: true,
  133. name: "虹桥火车站",
  134. items: []
  135. },
  136. },
  137. // 拥有三层的筛选项(例如brand,结构和filterItems类似)
  138. "filterItems": {
  139. }
  140. }
  141. }

理想情况下是这种扁平化的结构,根据filterCategory-id-value作为key来查找对应的筛选项,复杂点在于怎么format数据。
当选择筛选项的时候,会保存对应的项到selectedFilter里面,quickFilter会根据key来匹配,展示自己是否选中。
当选择快捷筛选项的时候,也是同理。

临时state

以filter组件为维度设计state,在componentWillReceiveProps中将selectedFilter赋给state,结构和selectedFilter类似,state中的值会映射到当前打开的筛选项里面,如果点击确定后会存入store。

如何处理互斥?

如果是筛选项,那么会从父组件将items传下来,在组件内部的state里面根据isRadio来处理。
如果是筛选项类,那就根据mutualFilters中的互斥关系,对state进行处理,比如brand和bed互斥,当我选bed中的筛选项,那就clear掉state中的brand。
如果是快捷筛选项,也会根据selectedFilter里面选中的项进行对比(一般来说肯定可以拿到isRadio和两者关联的某个值,可能是id也可能是type等等)

注意:

  1. 搜索联想词需要区分多语言
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注