Column.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. <template>
  2. <view class="nut-picker__list" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
  3. <view
  4. class="nut-picker-roller"
  5. ref="roller"
  6. :style="threeDimensional ? touchRollerStyle : touchTileStyle"
  7. @transitionend="stopMomentum"
  8. >
  9. <template v-for="(item, index) in column" :key="item.value ? item.value : index">
  10. <!-- 3D 效果 -->
  11. <view
  12. class="nut-picker-roller-item"
  13. :class="{ 'nut-picker-roller-item-hidden': isHidden(index + 1) }"
  14. :style="setRollerStyle(index + 1)"
  15. v-if="item && item.text && threeDimensional"
  16. >
  17. {{ item.text }}
  18. </view>
  19. <!-- 平铺 -->
  20. <view class="nut-picker-roller-item-tile" v-if="item && item.text && !threeDimensional">
  21. {{ item.text }}
  22. </view>
  23. </template>
  24. </view>
  25. <view class="nut-picker-roller-mask"></view>
  26. <!-- 3D 效果 时使用 -->
  27. <view class="nut-picker-content" v-if="threeDimensional"
  28. ><view class="nut-picker-list-panel" ref="list" :style="touchTileStyle"></view
  29. ></view>
  30. </view>
  31. </template>
  32. <script lang="ts">
  33. import { reactive, ref, watch, computed, toRefs, onMounted, PropType } from 'vue';
  34. import { createComponent } from '@/packages/utils/create';
  35. import { PickerColumnOption, PickerOption, TouchParams } from './types';
  36. import { useTouch } from '@/packages/utils/useTouch';
  37. const { create } = createComponent('picker-column');
  38. export default create({
  39. props: {
  40. // 当前选中项
  41. value: [String, Number],
  42. columnsType: String,
  43. itemShow: {
  44. type: Boolean,
  45. default: false
  46. },
  47. column: {
  48. type: Array as PropType<PickerOption[]>,
  49. default: () => []
  50. },
  51. readonly: {
  52. type: Boolean,
  53. default: false
  54. },
  55. // 是否开启3D效果
  56. threeDimensional: {
  57. type: Boolean,
  58. default: true
  59. }
  60. },
  61. emits: ['click', 'change'],
  62. setup(props, { emit }) {
  63. const touch: any = useTouch();
  64. const wrapper = ref();
  65. const state = reactive({
  66. touchParams: {
  67. startY: 0,
  68. endY: 0,
  69. startTime: 0,
  70. endTime: 0,
  71. lastY: 0,
  72. lastTime: 0
  73. },
  74. currIndex: 1,
  75. transformY: 0,
  76. scrollDistance: 0, // 滚动的距离
  77. lineSpacing: 36,
  78. rotation: 20,
  79. timer: null
  80. });
  81. const roller = ref(null);
  82. const list = ref(null);
  83. const moving = ref(false); // 是否处于滚动中
  84. const touchDeg = ref(0);
  85. const touchTime = ref(0);
  86. // 触发惯性滑动条件:
  87. // 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_TIME` 且 move
  88. // 距离大于 `MOMENTUM_DISTANCE` 时,执行惯性滑动
  89. const INERTIA_TIME = 300;
  90. const INERTIA_DISTANCE = 15;
  91. const touchRollerStyle = computed(() => {
  92. return {
  93. transition: `transform ${touchTime.value}ms cubic-bezier(0.17, 0.89, 0.45, 1)`,
  94. transform: `rotate3d(1, 0, 0, ${touchDeg.value})`
  95. };
  96. });
  97. const touchTileStyle = computed(() => {
  98. return {
  99. transition: `transform ${touchTime.value}ms cubic-bezier(0.17, 0.89, 0.45, 1)`,
  100. transform: `translate3d(0, ${state.scrollDistance}px, 0)`
  101. };
  102. });
  103. const setRollerStyle = (index: number) => {
  104. return `transform: rotate3d(1, 0, 0, ${-state.rotation * index}deg) translate3d(0px, 0px, 104px)`;
  105. };
  106. const onTouchStart = (event: TouchEvent) => {
  107. touch.start(event);
  108. if (moving.value) {
  109. let dom = list.value as any;
  110. if (!props.threeDimensional) {
  111. dom = roller.value as any;
  112. }
  113. const { transform } = window.getComputedStyle(dom);
  114. state.scrollDistance = +transform.slice(7, transform.length - 1).split(', ')[5];
  115. }
  116. state.touchParams.startY = touch.deltaY.value;
  117. state.touchParams.startTime = Date.now();
  118. state.transformY = state.scrollDistance;
  119. };
  120. const onTouchMove = (event: TouchEvent) => {
  121. touch.move(event);
  122. if ((touch as any).isVertical) {
  123. moving.value = true;
  124. preventDefault(event, true);
  125. }
  126. (state.touchParams as TouchParams).lastY = touch.deltaY.value;
  127. const now = Date.now();
  128. let move = state.touchParams.lastY - state.touchParams.startY;
  129. setMove(move);
  130. // if (now - (state.touchParams as TouchParams).startTime > INERTIA_TIME) {
  131. // (state.touchParams as TouchParams).startTime = now;
  132. // state.touchParams.startY = (state.touchParams as TouchParams).lastY;
  133. // }
  134. };
  135. const onTouchEnd = (event: TouchEvent) => {
  136. state.touchParams.lastY = touch.deltaY.value;
  137. state.touchParams.lastTime = Date.now();
  138. let move = state.touchParams.lastY - state.touchParams.startY;
  139. let moveTime = state.touchParams.lastTime - state.touchParams.startTime;
  140. if (moveTime <= INERTIA_TIME && Math.abs(move) > INERTIA_DISTANCE) {
  141. // 惯性滚动
  142. const distance = momentum(move, moveTime);
  143. setMove(distance, 'end', moveTime + 1000);
  144. return;
  145. } else {
  146. setMove(move, 'end');
  147. }
  148. setTimeout(() => {
  149. touch.reset();
  150. moving.value = false;
  151. }, 0);
  152. };
  153. // 惯性滚动 距离
  154. const momentum = (distance: number, duration: number) => {
  155. // 惯性滚动的速度
  156. const speed = Math.abs(distance / duration);
  157. // 惯性滚动的距离
  158. distance = (speed / 0.003) * (distance < 0 ? -1 : 1);
  159. return distance;
  160. };
  161. const isHidden = (index: number) => {
  162. if (index >= state.currIndex + 8 || index <= state.currIndex - 8) {
  163. return true;
  164. } else {
  165. return false;
  166. }
  167. };
  168. const setTransform = (translateY = 0, type: string | null, time = 1000, deg: string | number) => {
  169. if (type === 'end') {
  170. touchTime.value = time;
  171. } else {
  172. touchTime.value = 0;
  173. }
  174. touchDeg.value = deg as number;
  175. state.scrollDistance = translateY;
  176. };
  177. const setMove = (move: number, type?: string, time?: number) => {
  178. let updateMove = move + state.transformY;
  179. if (type === 'end') {
  180. // 限定滚动距离
  181. if (updateMove > 0) {
  182. updateMove = 0;
  183. }
  184. if (updateMove < -(props.column.length - 1) * state.lineSpacing) {
  185. updateMove = -(props.column.length - 1) * state.lineSpacing;
  186. }
  187. // 设置滚动距离为lineSpacing的倍数值
  188. let endMove = Math.round(updateMove / state.lineSpacing) * state.lineSpacing;
  189. let deg = `${(Math.abs(Math.round(endMove / state.lineSpacing)) + 1) * state.rotation}deg`;
  190. setTransform(endMove, type, time, deg);
  191. // let t = time ? time / 2 : 0;
  192. // (state.timer as any) = setTimeout(() => {
  193. // setChooseValue();
  194. // }, t);
  195. state.currIndex = Math.abs(Math.round(endMove / state.lineSpacing)) + 1;
  196. } else {
  197. let deg = 0;
  198. let currentDeg = (-updateMove / state.lineSpacing + 1) * state.rotation;
  199. // picker 滚动的最大角度
  200. const maxDeg = (props.column.length + 1) * state.rotation;
  201. const minDeg = 0;
  202. deg = Math.min(Math.max(currentDeg, minDeg), maxDeg);
  203. if (minDeg < deg && deg < maxDeg) {
  204. setTransform(updateMove, null, undefined, deg + 'deg');
  205. state.currIndex = Math.abs(Math.round(updateMove / state.lineSpacing)) + 1;
  206. }
  207. }
  208. };
  209. const setChooseValue = () => {
  210. emit('change', props.column[state.currIndex - 1]);
  211. };
  212. const modifyStatus = (type: boolean) => {
  213. const { column } = props;
  214. let index = column.findIndex((columnItem) => columnItem.value == props.value);
  215. state.currIndex = index === -1 ? 1 : (index as number) + 1;
  216. let move = index === -1 ? 0 : (index as number) * state.lineSpacing;
  217. type && setChooseValue();
  218. setMove(-move);
  219. };
  220. const preventDefault = (event: Event, isStopPropagation?: boolean) => {
  221. /* istanbul ignore else */
  222. if (typeof event.cancelable !== 'boolean' || event.cancelable) {
  223. event.preventDefault();
  224. }
  225. if (isStopPropagation) {
  226. event.stopPropagation();
  227. }
  228. };
  229. // 惯性滚动结束
  230. const stopMomentum = () => {
  231. moving.value = false;
  232. setChooseValue();
  233. };
  234. watch(
  235. () => props.column,
  236. (val) => {
  237. console.log('props.column变化', props.column);
  238. state.transformY = 0;
  239. modifyStatus(false);
  240. },
  241. {
  242. deep: true
  243. }
  244. );
  245. // watch(
  246. // () => props.value,
  247. // (val) => {
  248. // console.log('props.value变化')
  249. // modifyStatus(true);
  250. // },
  251. // {
  252. // deep: true
  253. // }
  254. // );
  255. onMounted(() => {
  256. modifyStatus(true);
  257. });
  258. return {
  259. ...toRefs(state),
  260. ...toRefs(props),
  261. wrapper,
  262. setRollerStyle,
  263. isHidden,
  264. roller,
  265. list,
  266. onTouchStart,
  267. onTouchMove,
  268. onTouchEnd,
  269. touchRollerStyle,
  270. touchTileStyle,
  271. setMove,
  272. stopMomentum
  273. };
  274. }
  275. });
  276. </script>