cl-list-index.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <template>
  2. <view class="cl-li">
  3. <!-- 搜索栏 -->
  4. <view class="cl-li__search" v-if="filterable">
  5. <cl-input
  6. v-model="keyWord"
  7. :border="false"
  8. prefix-icon="cl-icon-search"
  9. round
  10. placeholder="搜索关键字"
  11. clearable
  12. @search="onSearch"
  13. ></cl-input>
  14. </view>
  15. <!-- 滚动视图 -->
  16. <scroll-view
  17. class="cl-li__scroller"
  18. scroll-y
  19. enable-back-to-top
  20. scroll-with-animation
  21. :scroll-into-view="`index-${label}`"
  22. @scroll="onScroll"
  23. >
  24. <!-- 追加内容到头部 -->
  25. <slot name="prepend"></slot>
  26. <!-- 分组数据 -->
  27. <template v-for="(item, index) in flist">
  28. <view class="cl-li__group" :key="index" :id="`index-${item.label}`">
  29. <!-- 关键字 -->
  30. <view :class="['cl-li__header', curr.label == item.label ? 'is-active' : '']">
  31. <!-- 头部插槽 -->
  32. <slot name="header" :item="item" :isActive="curr.label == item.label">
  33. <text>{{ item.label }}</text>
  34. </slot>
  35. </view>
  36. <!-- 数据列表 -->
  37. <view class="cl-li__container">
  38. <view v-for="(item2, index2) in item.children" :key="index2">
  39. <!-- 内容插槽 -->
  40. <slot :item="item2" :parent="item">
  41. <view class="cl-li__item" @tap="selectRow(item2)">
  42. <cl-avatar :src="item2.avatarUrl"></cl-avatar>
  43. <cl-text :margin="[0, 0, 0, 20]" :value="item2.name"></cl-text>
  44. </view>
  45. </slot>
  46. </view>
  47. </view>
  48. </view>
  49. </template>
  50. <!-- 追加内容到尾部 -->
  51. <slot name="append"></slot>
  52. </scroll-view>
  53. <!-- 索引栏 -->
  54. <view class="cl-li__bar">
  55. <view class="cl-li__bar-list" @touchmove.stop.prevent="barMove" @touchend="barEnd">
  56. <template v-for="(item, index) in flist">
  57. <view
  58. :class="['cl-li__bar-block', curr.label == item.label ? 'is-active' : '']"
  59. :key="index"
  60. :id="index"
  61. @touchstart.stop.prevent="toRow(item)"
  62. >
  63. <text>{{ item.label }}</text>
  64. </view>
  65. </template>
  66. </view>
  67. </view>
  68. <!-- 索引关键字 -->
  69. <view class="cl-li__alert" v-show="alert">{{ curr.label }}</view>
  70. </view>
  71. </template>
  72. <script>
  73. import { last } from "../../utils";
  74. /**
  75. * list-index 索引列表
  76. * @description 索引列表, 支持自定义内容
  77. * @tutorial https://docs.cool-js.com/uni/components/advanced/list-index.html
  78. * @property {Number} index 序号
  79. * @property {Array} list 列表数据
  80. * @property {Boolean} filterable 是否带有过滤栏,默认true
  81. * @property {String} nodeKey 节点关键字,默认id
  82. * @event {Function} change 发生改变时触发
  83. * @event {Function} search 搜索时触发,function(keyword)
  84. * @event {Function} select 选择行时触发,function(item)
  85. * @example 见教程
  86. */
  87. export default {
  88. name: "cl-list-index",
  89. props: {
  90. // 序号
  91. index: {
  92. type: Number,
  93. default: 0,
  94. },
  95. // 列表数据
  96. list: Array,
  97. // 是否带有过滤栏
  98. filterable: {
  99. type: Boolean,
  100. default: true,
  101. },
  102. // 节点关键字
  103. nodeKey: {
  104. type: String,
  105. default: "id",
  106. },
  107. },
  108. data() {
  109. return {
  110. keyWord: "",
  111. label: "",
  112. alert: false,
  113. curr: {},
  114. bar: {
  115. top: 0,
  116. itemH: 0,
  117. },
  118. tops: [],
  119. };
  120. },
  121. watch: {
  122. index: {
  123. immediate: true,
  124. handler: "setData",
  125. },
  126. list: {
  127. handler: "doLayout",
  128. },
  129. curr: {
  130. handler(val) {
  131. this.$emit("change", val);
  132. },
  133. },
  134. },
  135. computed: {
  136. flist() {
  137. const match = (d) => (d ? d.name.includes(this.keyWord) : false);
  138. return this.list
  139. .filter((e) => e.children && e.children.find(match))
  140. .map((e) => {
  141. e.children = e.children.filter(match);
  142. return e;
  143. });
  144. },
  145. },
  146. mounted() {
  147. this.doLayout();
  148. },
  149. methods: {
  150. onSearch(val) {
  151. this.$emit("search", val);
  152. },
  153. onScroll(e) {
  154. let top = e.detail.scrollTop;
  155. let num = this.tops.filter((e) => top >= e - 60).length - 1;
  156. if (num < 0) {
  157. num = 0;
  158. }
  159. this.curr = this.list[num];
  160. },
  161. // 选择行
  162. selectRow(item) {
  163. this.$emit("select", item);
  164. },
  165. // 更新行数据,避免小程序等平台slot作用域异常问题
  166. updateRow(id, key, data) {
  167. this.list.map((e) => {
  168. if (e.children) {
  169. e.children.map((c) => {
  170. if (c[this.nodeKey] == id) {
  171. this.$set(c, key, data);
  172. }
  173. });
  174. }
  175. });
  176. },
  177. // 根据序号设置选择数据
  178. setData(index) {
  179. this.curr = this.list[index] || {};
  180. this.label = this.curr.label;
  181. },
  182. toRow(e) {
  183. this.alert = true;
  184. this.curr = e;
  185. },
  186. barMove(e) {
  187. const max = this.list.length;
  188. let index = parseInt((e.touches[0].clientY - this.bar.top) / this.bar.itemH);
  189. if (index >= max) {
  190. index = max - 1;
  191. }
  192. if (index < 0) {
  193. index = 0;
  194. }
  195. this.curr = this.list[index];
  196. },
  197. barEnd() {
  198. this.label = this.curr.label;
  199. this.alert = false;
  200. },
  201. doLayout() {
  202. this.$nextTick(() => {
  203. // 获取索引栏大小
  204. uni.createSelectorQuery()
  205. .in(this)
  206. .select(`.cl-li__bar-list`)
  207. .boundingClientRect((res) => {
  208. this.bar.top = res.top;
  209. this.bar.itemH = parseInt(res.height / this.list.length);
  210. })
  211. .exec();
  212. // 获取当前距离顶部的高度
  213. uni.createSelectorQuery()
  214. .in(this)
  215. .select(".cl-li")
  216. .boundingClientRect((res) => {
  217. // 获取每项距离顶部的高度
  218. uni.createSelectorQuery()
  219. .in(this)
  220. .selectAll(".cl-li__header")
  221. .fields({
  222. rect: true,
  223. })
  224. .exec((d) => {
  225. this.tops = d[0].map((e) => e.top - res.top);
  226. });
  227. })
  228. .exec();
  229. });
  230. },
  231. },
  232. };
  233. </script>